Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/yolo-temp-approval.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@moonshot-ai/agent-core": patch
"@moonshot-ai/kaos": patch
"@moonshot-ai/kimi-code": patch
---

Allow temp directory access outside workspace in yolo mode without approval.

Add `gettmpdir()` method to the `Kaos` interface and its implementations (`LocalKaos` and `SSHKaos`).
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { ToolInputDisplay } from '../../../tools/display';
import {
DEFAULT_WORKSPACE_ACCESS_POLICY,
isWithinDirectory,
resolvePathAccess,
type PathAccessOperation,
} from '../../../tools/policies/path-access';
Expand Down Expand Up @@ -55,6 +56,14 @@ export const YoloOutsideWorkspacePermissionPolicy: PermissionPolicy = {
}

if (!access.outsideWorkspace) return undefined;

// In yolo mode, temp directory access does not require approval
const kaos = agent.runtime.kaos;
const pathClass = kaos.pathClass();
if (isWithinDirectory(access.path, kaos.gettmpdir(), pathClass)) {
return undefined;
}

return {
kind: 'ask',
display: {
Expand Down
1 change: 1 addition & 0 deletions packages/agent-core/test/agent/harness/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -516,6 +516,7 @@ function createResumeNoSideEffectKaos(): Kaos {
normpath: (p: string) => p,
gethome: () => '/home/test',
getcwd: () => '/workspace',
gettmpdir: () => '/tmp',
chdir: () => fail('chdir'),
stat: () => fail('stat'),
iterdir: () => fail('iterdir'),
Expand Down
32 changes: 26 additions & 6 deletions packages/agent-core/test/agent/permission.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -389,11 +389,9 @@ describe('Permission auto mode', () => {
);

it.each([
['Read', { path: '/tmp/notes.md' }, 'read'],
['ReadMediaFile', { path: '/tmp/image.png' }, 'read'],
['Write', { path: '/tmp/notes.md', content: 'x' }, 'write'],
['Edit', { path: '/tmp/notes.md', old_string: 'a', new_string: 'b' }, 'edit'],
['Grep', { pattern: 'TODO', path: '/tmp' }, 'grep'],
['Read', { path: '/outside/notes.md' }, 'read'],
['ReadMediaFile', { path: '/outside/image.png' }, 'read'],
['Grep', { pattern: 'TODO', path: '/outside' }, 'grep'],
] as const)(
'requests approval for %s outside the workspace in yolo mode',
async (toolName, args, operation) => {
Expand Down Expand Up @@ -421,6 +419,28 @@ describe('Permission auto mode', () => {
},
);

it.each([
['Read', { path: '/tmp/notes.md' }],
['ReadMediaFile', { path: '/tmp/image.png' }],
['Write', { path: '/tmp/notes.md', content: 'x' }],
['Edit', { path: '/tmp/notes.md', old_string: 'a', new_string: 'b' }],
['Grep', { pattern: 'TODO', path: '/tmp' }],
] as const)(
'does not request approval for %s outside the workspace in yolo mode when target is temp directory',
async (toolName, args) => {
const { manager, requestApproval } = makePermissionManager(async () => ({
decision: 'approved',
}));
manager.setMode('yolo');

await expect(
manager.beforeToolCall(hookContext({ id: `call_${toolName}`, toolName, args })),
).resolves.toBeUndefined();

expect(requestApproval).not.toHaveBeenCalled();
},
);

it.each([
['Read', { path: '/workspace/notes.md' }],
['ReadMediaFile', { path: '/workspace/image.png' }],
Expand Down Expand Up @@ -504,7 +524,7 @@ describe('Permission auto mode', () => {
hookContext({
id: 'call_read_session',
toolName: 'Read',
args: { path: '/tmp/notes.md' },
args: { path: '/outside/notes.md' },
}),
);

Expand Down
1 change: 1 addition & 0 deletions packages/agent-core/test/tools/fixtures/fake-kaos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export function createFakeKaos(overrides?: Partial<Kaos>): Kaos {
normpath: (p: string) => p,
gethome: () => '/home/test',
getcwd: () => '/workspace',
gettmpdir: () => '/tmp',
chdir: () => notImplemented('chdir'),
stat: () => notImplemented('stat'),
iterdir: () => notImplemented('iterdir'),
Expand Down
2 changes: 2 additions & 0 deletions packages/kaos/src/kaos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ export interface Kaos {
gethome(): string;
/** Return the current working directory. */
getcwd(): string;
/** Return the temp directory for the current environment. */
gettmpdir(): string;

// ── Directory operations (async) ────────────────────────────────────

Expand Down
6 changes: 5 additions & 1 deletion packages/kaos/src/local.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
stat,
writeFile,
} from 'node:fs/promises';
import { homedir } from 'node:os';
import { homedir, tmpdir } from 'node:os';
import { isAbsolute, join as pathJoin, normalize } from 'node:path';
import type { Readable, Writable } from 'node:stream';

Expand Down Expand Up @@ -176,6 +176,10 @@ export class LocalKaos implements Kaos {
return this._cwd;
}

gettmpdir(): string {
return tmpdir();
}

/**
* Change the working directory of this LocalKaos instance.
*
Expand Down
17 changes: 15 additions & 2 deletions packages/kaos/src/ssh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -429,12 +429,14 @@ export class SSHKaos implements Kaos {
private _sftp: SFTPWrapper;
private _home: string;
private _cwd: string;
private _tmpdir: string;

private constructor(client: Client, sftp: SFTPWrapper, home: string, cwd: string) {
private constructor(client: Client, sftp: SFTPWrapper, home: string, cwd: string, tmpdir: string) {
this._client = client;
this._sftp = sftp;
this._home = home;
this._cwd = cwd;
this._tmpdir = tmpdir;
}

private _resolvePath(path: string): string {
Expand Down Expand Up @@ -501,7 +503,14 @@ export class SSHKaos implements Kaos {
}
}

return new SSHKaos(client, sftp, home, cwd);
let tmpdir = '/tmp';
try {
tmpdir = await sftpRealpath(sftp, '/tmp');
} catch {
// fallback to /tmp
}

return new SSHKaos(client, sftp, home, cwd, tmpdir);
} catch (error) {
client.end();
throw error;
Expand All @@ -526,6 +535,10 @@ export class SSHKaos implements Kaos {
return this._cwd;
}

gettmpdir(): string {
return this._tmpdir;
}

// ── Directory operations (async) ───────────────────────────────────

async chdir(path: string): Promise<void> {
Expand Down
1 change: 1 addition & 0 deletions packages/kaos/test/current.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ function createMockKaos(name: string): Kaos {
normpath: (p: string) => p,
gethome: () => '/',
getcwd: () => '/',
gettmpdir: () => '/tmp',
chdir: async () => {},
stat: () =>
Promise.resolve({
Expand Down
1 change: 1 addition & 0 deletions packages/kaos/test/e2e/async-isolation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ function createNamedKaos(kaosName: string): Kaos {
normpath: (p: string) => base.normpath(p),
gethome: () => base.gethome(),
getcwd: () => base.getcwd(),
gettmpdir: () => base.gettmpdir(),
chdir: async (p: string) => base.chdir(p),
stat: async (p: string, opts?: { followSymlinks?: boolean }) => base.stat(p, opts),
iterdir: (p: string) => base.iterdir(p),
Expand Down
3 changes: 3 additions & 0 deletions packages/kaos/test/e2e/path-cross-platform.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ function createMockKaos(overrides: Partial<Kaos> & { name: string }): Kaos {
getcwd(): string {
return '/default/cwd';
},
gettmpdir(): string {
return '/tmp';
},
async chdir(): Promise<void> {
// no-op
},
Expand Down
2 changes: 2 additions & 0 deletions packages/kaos/test/path.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ function makeMockKaos(pathClass: 'posix' | 'win32', overrides: Partial<Kaos> = {
normpath: (p: string) => (pathClass === 'win32' ? win32Path.normalize(p) : p),
gethome: () => (pathClass === 'win32' ? 'C:\\Users\\test' : '/home/test'),
getcwd: () => (pathClass === 'win32' ? 'C:\\work\\project' : '/work/project'),
gettmpdir: () => (pathClass === 'win32' ? 'C:\\Users\\test\\AppData\\Local\\Temp' : '/tmp'),
chdir: async () => {},
stat: async () => ({
stMode: 0,
Expand Down Expand Up @@ -225,6 +226,7 @@ describe('KaosPath', () => {
normpath: (p: string) => win32Path.normalize(p),
gethome: () => 'C:\\Users\\test',
getcwd: () => 'C:\\work\\project',
gettmpdir: () => 'C:\\Users\\test\\AppData\\Local\\Temp',
chdir: async () => {},
stat: async () => ({
stMode: 0,
Expand Down