mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-13 05:12:55 -07:00
feat(plan): support replace tool in plan mode to edit plans (#18379)
This commit is contained in:
@@ -323,116 +323,64 @@ describe('Policy Engine Integration Tests', () => {
|
|||||||
).toBe(PolicyDecision.DENY);
|
).toBe(PolicyDecision.DENY);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should allow write_file to plans directory in Plan mode', async () => {
|
describe.each(['write_file', 'replace'])(
|
||||||
|
'Plan Mode policy for %s',
|
||||||
|
(toolName) => {
|
||||||
|
it(`should allow ${toolName} to plans directory`, async () => {
|
||||||
const settings: Settings = {};
|
const settings: Settings = {};
|
||||||
|
|
||||||
const config = await createPolicyEngineConfig(
|
const config = await createPolicyEngineConfig(
|
||||||
settings,
|
settings,
|
||||||
ApprovalMode.PLAN,
|
ApprovalMode.PLAN,
|
||||||
);
|
);
|
||||||
const engine = new PolicyEngine(config);
|
const engine = new PolicyEngine(config);
|
||||||
|
|
||||||
// Valid plan file path (64-char hex hash, .md extension, safe filename)
|
// Valid plan file paths
|
||||||
const validPlanPath =
|
const validPaths = [
|
||||||
'/home/user/.gemini/tmp/a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2/plans/my-plan.md';
|
'/home/user/.gemini/tmp/a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2/plans/my-plan.md',
|
||||||
|
'/home/user/.gemini/tmp/a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2/plans/feature_auth.md',
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const file_path of validPaths) {
|
||||||
expect(
|
expect(
|
||||||
(
|
(
|
||||||
await engine.check(
|
await engine.check(
|
||||||
{ name: 'write_file', args: { file_path: validPlanPath } },
|
{ name: toolName, args: { file_path } },
|
||||||
undefined,
|
|
||||||
)
|
|
||||||
).decision,
|
|
||||||
).toBe(PolicyDecision.ALLOW);
|
|
||||||
|
|
||||||
// Valid plan with underscore in filename
|
|
||||||
const validPlanPath2 =
|
|
||||||
'/home/user/.gemini/tmp/a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2/plans/feature_auth.md';
|
|
||||||
expect(
|
|
||||||
(
|
|
||||||
await engine.check(
|
|
||||||
{ name: 'write_file', args: { file_path: validPlanPath2 } },
|
|
||||||
undefined,
|
undefined,
|
||||||
)
|
)
|
||||||
).decision,
|
).decision,
|
||||||
).toBe(PolicyDecision.ALLOW);
|
).toBe(PolicyDecision.ALLOW);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should deny write_file outside plans directory in Plan mode', async () => {
|
it(`should deny ${toolName} outside plans directory`, async () => {
|
||||||
const settings: Settings = {};
|
const settings: Settings = {};
|
||||||
|
|
||||||
const config = await createPolicyEngineConfig(
|
const config = await createPolicyEngineConfig(
|
||||||
settings,
|
settings,
|
||||||
ApprovalMode.PLAN,
|
ApprovalMode.PLAN,
|
||||||
);
|
);
|
||||||
const engine = new PolicyEngine(config);
|
const engine = new PolicyEngine(config);
|
||||||
|
|
||||||
// Write to workspace (not plans dir) should be denied
|
const invalidPaths = [
|
||||||
expect(
|
'/project/src/file.ts', // Workspace
|
||||||
(
|
'/home/user/.gemini/tmp/a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2/plans/script.js', // Wrong extension
|
||||||
await engine.check(
|
'/home/user/.gemini/tmp/a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2/plans/../../../etc/passwd.md', // Path traversal
|
||||||
{ name: 'write_file', args: { file_path: '/project/src/file.ts' } },
|
'/home/user/.gemini/tmp/abc123/plans/plan.md', // Invalid hash length
|
||||||
undefined,
|
'/home/user/.gemini/tmp/a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2/plans/subdir/plan.md', // Subdirectory
|
||||||
)
|
];
|
||||||
).decision,
|
|
||||||
).toBe(PolicyDecision.DENY);
|
|
||||||
|
|
||||||
// Write to plans dir but wrong extension should be denied
|
for (const file_path of invalidPaths) {
|
||||||
const wrongExtPath =
|
|
||||||
'/home/user/.gemini/tmp/a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2/plans/script.js';
|
|
||||||
expect(
|
expect(
|
||||||
(
|
(
|
||||||
await engine.check(
|
await engine.check(
|
||||||
{ name: 'write_file', args: { file_path: wrongExtPath } },
|
{ name: toolName, args: { file_path } },
|
||||||
undefined,
|
|
||||||
)
|
|
||||||
).decision,
|
|
||||||
).toBe(PolicyDecision.DENY);
|
|
||||||
|
|
||||||
// Path traversal attempt should be denied (filename contains /)
|
|
||||||
const traversalPath =
|
|
||||||
'/home/user/.gemini/tmp/a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2/plans/../../../etc/passwd.md';
|
|
||||||
expect(
|
|
||||||
(
|
|
||||||
await engine.check(
|
|
||||||
{ name: 'write_file', args: { file_path: traversalPath } },
|
|
||||||
undefined,
|
|
||||||
)
|
|
||||||
).decision,
|
|
||||||
).toBe(PolicyDecision.DENY);
|
|
||||||
|
|
||||||
// Invalid hash length should be denied
|
|
||||||
const shortHashPath = '/home/user/.gemini/tmp/abc123/plans/plan.md';
|
|
||||||
expect(
|
|
||||||
(
|
|
||||||
await engine.check(
|
|
||||||
{ name: 'write_file', args: { file_path: shortHashPath } },
|
|
||||||
undefined,
|
undefined,
|
||||||
)
|
)
|
||||||
).decision,
|
).decision,
|
||||||
).toBe(PolicyDecision.DENY);
|
).toBe(PolicyDecision.DENY);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
},
|
||||||
it('should deny write_file to subdirectories in Plan mode', async () => {
|
|
||||||
const settings: Settings = {};
|
|
||||||
|
|
||||||
const config = await createPolicyEngineConfig(
|
|
||||||
settings,
|
|
||||||
ApprovalMode.PLAN,
|
|
||||||
);
|
);
|
||||||
const engine = new PolicyEngine(config);
|
|
||||||
|
|
||||||
// Write to subdirectory should be denied
|
|
||||||
const subdirPath =
|
|
||||||
'/home/user/.gemini/tmp/a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2/plans/subdir/plan.md';
|
|
||||||
expect(
|
|
||||||
(
|
|
||||||
await engine.check(
|
|
||||||
{ name: 'write_file', args: { file_path: subdirPath } },
|
|
||||||
undefined,
|
|
||||||
)
|
|
||||||
).decision,
|
|
||||||
).toBe(PolicyDecision.DENY);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should verify priority ordering works correctly in practice', async () => {
|
it('should verify priority ordering works correctly in practice', async () => {
|
||||||
const settings: Settings = {
|
const settings: Settings = {
|
||||||
|
|||||||
@@ -130,6 +130,7 @@ You are operating in **Plan Mode** - a structured planning workflow for designin
|
|||||||
The following read-only tools are available in Plan Mode:
|
The following read-only tools are available in Plan Mode:
|
||||||
|
|
||||||
- \`write_file\` - Save plans to the plans directory (see Plan Storage below)
|
- \`write_file\` - Save plans to the plans directory (see Plan Storage below)
|
||||||
|
- \`replace\` - Update plans in the plans directory
|
||||||
|
|
||||||
## Plan Storage
|
## Plan Storage
|
||||||
- Save your plans as Markdown (.md) files ONLY within: \`/tmp/project-temp/plans/\`
|
- Save your plans as Markdown (.md) files ONLY within: \`/tmp/project-temp/plans/\`
|
||||||
|
|||||||
@@ -327,7 +327,10 @@ describe('createPolicyEngineConfig', () => {
|
|||||||
ApprovalMode.AUTO_EDIT,
|
ApprovalMode.AUTO_EDIT,
|
||||||
);
|
);
|
||||||
const rule = config.rules?.find(
|
const rule = config.rules?.find(
|
||||||
(r) => r.toolName === 'replace' && r.decision === PolicyDecision.ALLOW,
|
(r) =>
|
||||||
|
r.toolName === 'replace' &&
|
||||||
|
r.decision === PolicyDecision.ALLOW &&
|
||||||
|
r.modes?.includes(ApprovalMode.AUTO_EDIT),
|
||||||
);
|
);
|
||||||
expect(rule).toBeDefined();
|
expect(rule).toBeDefined();
|
||||||
// Priority 15 in default tier → 1.015
|
// Priority 15 in default tier → 1.015
|
||||||
|
|||||||
@@ -77,9 +77,9 @@ decision = "ask_user"
|
|||||||
priority = 50
|
priority = 50
|
||||||
modes = ["plan"]
|
modes = ["plan"]
|
||||||
|
|
||||||
# Allow write_file for .md files in plans directory
|
# Allow write_file and replace for .md files in plans directory
|
||||||
[[rule]]
|
[[rule]]
|
||||||
toolName = "write_file"
|
toolName = ["write_file", "replace"]
|
||||||
decision = "allow"
|
decision = "allow"
|
||||||
priority = 50
|
priority = 50
|
||||||
modes = ["plan"]
|
modes = ["plan"]
|
||||||
|
|||||||
@@ -305,6 +305,7 @@ You are operating in **Plan Mode** - a structured planning workflow for designin
|
|||||||
The following read-only tools are available in Plan Mode:
|
The following read-only tools are available in Plan Mode:
|
||||||
${options.planModeToolsList}
|
${options.planModeToolsList}
|
||||||
- \`${WRITE_FILE_TOOL_NAME}\` - Save plans to the plans directory (see Plan Storage below)
|
- \`${WRITE_FILE_TOOL_NAME}\` - Save plans to the plans directory (see Plan Storage below)
|
||||||
|
- \`${EDIT_TOOL_NAME}\` - Update plans in the plans directory
|
||||||
|
|
||||||
## Plan Storage
|
## Plan Storage
|
||||||
- Save your plans as Markdown (.md) files ONLY within: \`${options.plansDir}/\`
|
- Save your plans as Markdown (.md) files ONLY within: \`${options.plansDir}/\`
|
||||||
|
|||||||
Reference in New Issue
Block a user