Skip to content

Commit a1b419a

Browse files
authored
fix: stop asking for yolo external writes (#606)
1 parent 99b3748 commit a1b419a

4 files changed

Lines changed: 52 additions & 180 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@moonshot-ai/agent-core": patch
3+
"@moonshot-ai/kimi-code": patch
4+
---
5+
6+
YOLO mode no longer asks before writing or editing files outside the working directory.

packages/agent-core/src/agent/permission/policies/file-access-ask.ts

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -63,26 +63,6 @@ export class GitControlPathAccessAskPermissionPolicy implements PermissionPolicy
6363
}
6464
}
6565

66-
export class CwdOutsideFileWriteAskPermissionPolicy implements PermissionPolicy {
67-
readonly name = 'cwd-outside-file-write-ask';
68-
69-
constructor(private readonly agent: Agent) {}
70-
71-
evaluate(context: PermissionPolicyContext): PermissionPolicyResult | undefined {
72-
const cwd = this.agent.config.cwd;
73-
if (cwd.length === 0) return;
74-
const pathClass = this.agent.kaos.pathClass();
75-
const access = writeFileAccesses(context).find((fileAccess) => {
76-
return !isWithinDirectory(fileAccess.path, cwd, pathClass);
77-
});
78-
if (access === undefined) return;
79-
return {
80-
kind: 'ask',
81-
reason: fileAccessReason(access, { cwd_outside: true }),
82-
};
83-
}
84-
}
85-
8666
function fileAccesses(context: PermissionPolicyContext): ToolFileAccess[] {
8767
return (
8868
context.execution.accesses?.filter((access): access is ToolFileAccess => access.kind === 'file') ??

packages/agent-core/src/agent/permission/policies/index.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import { DefaultToolApprovePermissionPolicy } from './default-tool-approve';
66
import { ExitPlanModeReviewAskPermissionPolicy } from './exit-plan-mode-review-ask';
77
import { FallbackAskPermissionPolicy } from './fallback-ask';
88
import {
9-
CwdOutsideFileWriteAskPermissionPolicy,
109
GitControlPathAccessAskPermissionPolicy,
1110
SensitiveFileAccessAskPermissionPolicy,
1211
} from './file-access-ask';
@@ -50,8 +49,6 @@ export function createPermissionDecisionPolicies(agent: Agent): PermissionPolicy
5049
new SensitiveFileAccessAskPermissionPolicy(agent),
5150
// Access touches .git or a git control-dir path → ask.
5251
new GitControlPathAccessAskPermissionPolicy(agent),
53-
// Write target is outside cwd → ask. Reads and searches outside cwd are allowed without prompting.
54-
new CwdOutsideFileWriteAskPermissionPolicy(agent),
5552
// yolo mode → approve.
5653
new YoloModeApprovePermissionPolicy(agent),
5754
// Swarm mode keeps AgentSwarm available without making it a globally default-approved tool.

packages/agent-core/test/agent/permission.test.ts

Lines changed: 46 additions & 157 deletions
Original file line numberDiff line numberDiff line change
@@ -404,20 +404,16 @@ describe('Permission auto mode', () => {
404404
},
405405
);
406406

407-
it.each(
408-
(['manual', 'yolo'] as const).flatMap((mode) =>
409-
[
410-
[mode, 'Write', { path: '/tmp/notes.md', content: 'x' }, 'write', 'write file'],
411-
[mode, 'Edit', { path: '/tmp/notes.md', old_string: 'a', new_string: 'b' }, 'edit', 'edit file'],
412-
] as const,
413-
),
414-
)(
415-
'requests approval in %s mode for %s outside the cwd',
416-
async (mode, toolName, args, operation, action) => {
417-
const { manager, requestApproval } = makePermissionManager(async () => ({
407+
it.each([
408+
['Write', { path: '/tmp/notes.md', content: 'x' }, 'write', 'write file'],
409+
['Edit', { path: '/tmp/notes.md', old_string: 'a', new_string: 'b' }, 'edit', 'edit file'],
410+
] as const)(
411+
'requests approval in manual mode for %s outside the cwd',
412+
async (toolName, args, operation, action) => {
413+
const { manager, requestApproval, telemetryTrack } = makePermissionManager(async () => ({
418414
decision: 'approved',
419415
}));
420-
manager.setMode(mode);
416+
manager.setMode('manual');
421417

422418
await expect(
423419
manager.beforeToolCall(hookContext({ id: `call_${toolName}`, toolName, args })),
@@ -435,9 +431,43 @@ describe('Permission auto mode', () => {
435431
}),
436432
expect.any(Object),
437433
);
434+
expect(telemetryTrack).toHaveBeenCalledWith(
435+
'permission_policy_decision',
436+
expect.objectContaining({
437+
policy_name: 'fallback-ask',
438+
tool_name: toolName,
439+
permission_mode: 'manual',
440+
decision: 'ask',
441+
}),
442+
);
438443
},
439444
);
440445

446+
it.each([
447+
['Write', { path: '/tmp/notes.md', content: 'x' }],
448+
['Edit', { path: '/tmp/notes.md', old_string: 'a', new_string: 'b' }],
449+
] as const)('approves %s outside the cwd in yolo mode', async (toolName, args) => {
450+
const { manager, requestApproval, telemetryTrack } = makePermissionManager(async () => ({
451+
decision: 'approved',
452+
}));
453+
manager.setMode('yolo');
454+
455+
await expect(
456+
manager.beforeToolCall(hookContext({ id: `call_${toolName}_yolo_outside`, toolName, args })),
457+
).resolves.toBeUndefined();
458+
459+
expect(requestApproval).not.toHaveBeenCalled();
460+
expect(telemetryTrack).toHaveBeenCalledWith(
461+
'permission_policy_decision',
462+
expect.objectContaining({
463+
policy_name: 'yolo-mode-approve',
464+
tool_name: toolName,
465+
permission_mode: 'yolo',
466+
decision: 'approve',
467+
}),
468+
);
469+
});
470+
441471
it.each(
442472
(['manual', 'yolo'] as const).flatMap((mode) =>
443473
[
@@ -638,7 +668,7 @@ describe('Permission auto mode', () => {
638668
);
639669
});
640670

641-
it('reuses approve-for-session for repeated outside-workspace writes in yolo mode', async () => {
671+
it('approves repeated outside-workspace writes in yolo mode without session approval', async () => {
642672
const { manager, requestApproval } = makePermissionManager(async () => ({
643673
decision: 'approved',
644674
scope: 'session',
@@ -657,8 +687,8 @@ describe('Permission auto mode', () => {
657687
await expect(call()).resolves.toBeUndefined();
658688
await expect(call()).resolves.toBeUndefined();
659689

660-
expect(requestApproval).toHaveBeenCalledTimes(1);
661-
expect(manager.sessionApprovalRulePatterns).toEqual(['Write(/tmp/notes.md)']);
690+
expect(requestApproval).not.toHaveBeenCalled();
691+
expect(manager.sessionApprovalRulePatterns).toEqual([]);
662692
expect(manager.data().rules).toEqual([]);
663693
});
664694
});
@@ -678,7 +708,6 @@ describe('Permission policy chain', () => {
678708
'plan-mode-tool-approve',
679709
'sensitive-file-access-ask',
680710
'git-control-path-access-ask',
681-
'cwd-outside-file-write-ask',
682711
'yolo-mode-approve',
683712
'swarm-mode-agent-swarm-approve',
684713
'default-tool-approve',
@@ -3301,7 +3330,7 @@ describe('Default git CWD Write/Edit permission', () => {
33013330
expect(requestApproval).toHaveBeenCalledTimes(1);
33023331
expect(telemetryTrack).toHaveBeenCalledWith(
33033332
'permission_policy_decision',
3304-
expect.objectContaining({ policy_name: 'cwd-outside-file-write-ask' }),
3333+
expect.objectContaining({ policy_name: 'fallback-ask' }),
33053334
);
33063335
expect(telemetryTrack).not.toHaveBeenCalledWith(
33073336
'permission_policy_decision',
@@ -3357,146 +3386,6 @@ describe('Default git CWD Write/Edit permission', () => {
33573386
});
33583387
});
33593388

3360-
describe('CWD outside file write permission policy', () => {
3361-
it('falls through when cwd is empty', async () => {
3362-
const { manager, requestApproval, telemetryTrack } = makePermissionManager(
3363-
async () => ({ decision: 'approved' }),
3364-
{ cwd: '' },
3365-
);
3366-
3367-
await expect(
3368-
manager.beforeToolCall(
3369-
hookContext({
3370-
id: 'call_empty_cwd_write',
3371-
toolName: 'Write',
3372-
args: { path: '/tmp/outside.ts', content: 'x' },
3373-
}),
3374-
),
3375-
).resolves.toBeUndefined();
3376-
3377-
expect(requestApproval).toHaveBeenCalledTimes(1);
3378-
expect(telemetryTrack).toHaveBeenCalledWith(
3379-
'permission_policy_decision',
3380-
expect.objectContaining({ policy_name: 'fallback-ask' }),
3381-
);
3382-
});
3383-
3384-
it('falls through when there are no file write accesses', async () => {
3385-
const args = { path: '/tmp/outside.ts', content: 'x' };
3386-
const { manager, requestApproval, telemetryTrack } = makePermissionManager(async () => ({
3387-
decision: 'approved',
3388-
}));
3389-
3390-
await expect(
3391-
manager.beforeToolCall(
3392-
hookContext({
3393-
id: 'call_no_write_accesses',
3394-
toolName: 'Write',
3395-
args,
3396-
execution: {
3397-
...testExecution('Write', args),
3398-
accesses: ToolAccesses.none(),
3399-
},
3400-
}),
3401-
),
3402-
).resolves.toBeUndefined();
3403-
3404-
expect(requestApproval).toHaveBeenCalledTimes(1);
3405-
expect(telemetryTrack).toHaveBeenCalledWith(
3406-
'permission_policy_decision',
3407-
expect.objectContaining({ policy_name: 'fallback-ask' }),
3408-
);
3409-
});
3410-
3411-
it('asks when any write access is outside the cwd', async () => {
3412-
const args = { path: '/workspace/src/a.ts', content: 'x' };
3413-
const { manager, requestApproval, telemetryTrack } = makePermissionManager(async () => ({
3414-
decision: 'approved',
3415-
}));
3416-
3417-
await expect(
3418-
manager.beforeToolCall(
3419-
hookContext({
3420-
id: 'call_mixed_write_accesses',
3421-
toolName: 'Write',
3422-
args,
3423-
execution: {
3424-
...testExecution('Write', args),
3425-
accesses: [
3426-
{ kind: 'file', operation: 'write', path: '/workspace/src/a.ts' },
3427-
{ kind: 'file', operation: 'readwrite', path: '/tmp/outside.ts' },
3428-
],
3429-
},
3430-
}),
3431-
),
3432-
).resolves.toBeUndefined();
3433-
3434-
expect(requestApproval).toHaveBeenCalledTimes(1);
3435-
expect(telemetryTrack).toHaveBeenCalledWith(
3436-
'permission_policy_decision',
3437-
expect.objectContaining({
3438-
policy_name: 'cwd-outside-file-write-ask',
3439-
decision: 'ask',
3440-
cwd_outside: true,
3441-
file_access_operation: 'readwrite',
3442-
}),
3443-
);
3444-
});
3445-
3446-
it.each([
3447-
['Read', { path: '/tmp/outside.ts' }],
3448-
['Grep', { pattern: 'TODO', path: '/tmp' }],
3449-
] as const)('does not ask for %s access outside cwd', async (toolName, args) => {
3450-
const { manager, requestApproval, telemetryTrack } = makePermissionManager(async () => ({
3451-
decision: 'approved',
3452-
}));
3453-
3454-
await expect(
3455-
manager.beforeToolCall(
3456-
hookContext({
3457-
id: `call_${toolName}_outside_cwd`,
3458-
toolName,
3459-
args,
3460-
}),
3461-
),
3462-
).resolves.toBeUndefined();
3463-
3464-
expect(requestApproval).not.toHaveBeenCalled();
3465-
expect(telemetryTrack).toHaveBeenCalledWith(
3466-
'permission_policy_decision',
3467-
expect.objectContaining({ policy_name: 'default-tool-approve' }),
3468-
);
3469-
});
3470-
3471-
it('uses Win32 path semantics for cwd containment', async () => {
3472-
const kaos = createFakeKaos({ pathClass: () => 'win32' });
3473-
const args = { path: 'c:\\repo\\src\\a.ts', content: 'x' };
3474-
const { manager, telemetryTrack } = makePermissionManager(
3475-
async () => ({ decision: 'approved' }),
3476-
{ cwd: 'C:\\Repo', kaos },
3477-
);
3478-
3479-
await expect(
3480-
manager.beforeToolCall(
3481-
hookContext({
3482-
id: 'call_win_inside_cwd',
3483-
toolName: 'Write',
3484-
args,
3485-
execution: {
3486-
...testExecution('Write', args),
3487-
accesses: ToolAccesses.writeFile('c:\\repo\\src\\a.ts'),
3488-
},
3489-
}),
3490-
),
3491-
).resolves.toBeUndefined();
3492-
3493-
expect(telemetryTrack).not.toHaveBeenCalledWith(
3494-
'permission_policy_decision',
3495-
expect.objectContaining({ policy_name: 'cwd-outside-file-write-ask' }),
3496-
);
3497-
});
3498-
});
3499-
35003389
describe('Permission rule helpers', () => {
35013390
it('parses permission patterns used by rule matching', () => {
35023391
expect(parsePattern('Write')).toEqual({ toolName: 'Write' });

0 commit comments

Comments
 (0)