From 48adb6108d462d2b2fe009749e8058c05767a291 Mon Sep 17 00:00:00 2001 From: "haozhe.yang" Date: Wed, 2 Sep 2026 00:28:35 +0800 Subject: [PATCH] fix(agent-core-v2): allow Bash cwd outside the workspace roots - drop the built-in escape assertion from RuntimeWorkspaceView.resolve so it is a pure path mapping - return the validated path from assertAllowed and compose it explicitly in terminalService - add a bash test covering cwd outside the workspace roots --- .changeset/bash-cwd-workspace-restriction.md | 5 +++++ .../src/runtime/runtimeWorkspaceView.ts | 8 +++----- .../src/session/terminal/terminalService.ts | 2 +- .../test/os/backends/node-local/tools/bash.test.ts | 13 +++++++++++++ .../test/runtime/runtimeWorkspaceView.test.ts | 11 ++++++----- 5 files changed, 28 insertions(+), 11 deletions(-) create mode 100644 .changeset/bash-cwd-workspace-restriction.md diff --git a/.changeset/bash-cwd-workspace-restriction.md b/.changeset/bash-cwd-workspace-restriction.md new file mode 100644 index 00000000000..22209b064d8 --- /dev/null +++ b/.changeset/bash-cwd-workspace-restriction.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Remove the workspace restriction on the Bash tool's cwd parameter. diff --git a/packages/agent-core-v2/src/runtime/runtimeWorkspaceView.ts b/packages/agent-core-v2/src/runtime/runtimeWorkspaceView.ts index 03edbdba4b1..01db1bbf8f5 100644 --- a/packages/agent-core-v2/src/runtime/runtimeWorkspaceView.ts +++ b/packages/agent-core-v2/src/runtime/runtimeWorkspaceView.ts @@ -30,16 +30,14 @@ export class RuntimeWorkspaceView { resolve(path: string, cwd = this.workDir): string { const env = this.runtime.environment; const bridged = env.pathClass === 'win32' ? getShellPathBridge(env).fromShellPath(path) : path; - const resolved = this.runtime.path.isAbsolute(bridged) + return this.runtime.path.isAbsolute(bridged) ? this.runtime.path.resolve(bridged) : this.runtime.path.resolve(cwd, bridged); - this.assertAllowed(resolved); - return resolved; } - assertAllowed(path: string): void { + assertAllowed(path: string): string { const resolved = this.runtime.path.resolve(path); - if (this.roots.some((root) => contains(this.runtime, root, resolved))) return; + if (this.roots.some((root) => contains(this.runtime, root, resolved))) return resolved; throw new Error2( ErrorCodes.FS_PATH_ESCAPES, `path ${path} is outside runtime workspace ${this.binding.runtimeId}`, diff --git a/packages/agent-core-v2/src/session/terminal/terminalService.ts b/packages/agent-core-v2/src/session/terminal/terminalService.ts index 048f29c6c09..7d778229ffa 100644 --- a/packages/agent-core-v2/src/session/terminal/terminalService.ts +++ b/packages/agent-core-v2/src/session/terminal/terminalService.ts @@ -79,7 +79,7 @@ export class SessionTerminalService extends Disposable implements ISessionTermin ['terminal'], ); const view = new RuntimeWorkspaceView(lease.runtime, this.workspace); - const cwd = input.cwd === undefined ? view.workDir : view.resolve(input.cwd); + const cwd = input.cwd === undefined ? view.workDir : view.assertAllowed(view.resolve(input.cwd)); const shell = input.shell ?? lease.runtime.environment.shellPath; let process: TerminalProcess; try { diff --git a/packages/agent-core-v2/test/os/backends/node-local/tools/bash.test.ts b/packages/agent-core-v2/test/os/backends/node-local/tools/bash.test.ts index 4609e51b6cc..a439a888133 100644 --- a/packages/agent-core-v2/test/os/backends/node-local/tools/bash.test.ts +++ b/packages/agent-core-v2/test/os/backends/node-local/tools/bash.test.ts @@ -859,6 +859,19 @@ describe('BashTool', () => { expect(exec.mock.calls[0]?.[1]).toEqual(['-c', "cd '/workspace/project' && pwd"]); }); + it('accepts args.cwd outside the workspace roots', async () => { + const { runner, exec } = createTestRunner(processWithOutput({ stdout: 'out\n' })); + const tool = bashTool(runner); + + const result = await executeTool( + tool, + context({ command: 'pwd', cwd: '/outside/workspace', timeout: 60 }), + ); + + expect(exec.mock.calls[0]?.[1]).toEqual(['-c', "cd '/outside/workspace' && pwd"]); + expect(result).toMatchObject({ output: 'out\n', isError: false }); + }); + it('uses the kaos cwd as the default working directory', async () => { const { runner, exec } = createTestRunner(processWithOutput({ stdout: '' })); const tool = bashTool(runner, posixEnv, createTestCtx('/var/app')); diff --git a/packages/agent-core-v2/test/runtime/runtimeWorkspaceView.test.ts b/packages/agent-core-v2/test/runtime/runtimeWorkspaceView.test.ts index 637538da777..406b1998804 100644 --- a/packages/agent-core-v2/test/runtime/runtimeWorkspaceView.test.ts +++ b/packages/agent-core-v2/test/runtime/runtimeWorkspaceView.test.ts @@ -19,7 +19,8 @@ describe('RuntimeWorkspaceView', () => { expect(view.resolve('src/index.ts')).toBe('/workspace/project/src/index.ts'); expect(view.resolve('../shared/file.txt', '/workspace/project/src')).toBe('/workspace/project/shared/file.txt'); expect(view.resolve('/shared/file.txt')).toBe('/shared/file.txt'); - expect(() => view.resolve('../../outside')).toThrow('outside runtime workspace'); + expect(view.resolve('../../outside')).toBe('/outside'); + expect(() => view.assertAllowed(view.resolve('../../outside'))).toThrow('outside runtime workspace'); }); it('uses win32 path semantics and rejects sibling prefixes', () => { @@ -29,7 +30,7 @@ describe('RuntimeWorkspaceView', () => { }); expect(view.resolve('src\\index.ts')).toBe('C:\\workspace\\project\\src\\index.ts'); expect(view.resolve('d:\\SHARED\\file.txt')).toBe('d:\\SHARED\\file.txt'); - expect(() => view.resolve('C:\\workspace\\project-other\\file.txt')).toThrow('outside runtime workspace'); + expect(() => view.assertAllowed(view.resolve('C:\\workspace\\project-other\\file.txt'))).toThrow('outside runtime workspace'); }); it('uses provider-owned workspace root mapping', () => { @@ -71,8 +72,8 @@ describe('RuntimeWorkspaceView', () => { expect(win32View.binding.runtimeId).toBe('remote-win32'); expect(posixView.resolve('/provider-a/shared/file.txt')).toBe('/provider-a/shared/file.txt'); expect(win32View.resolve('D:\\provider-b\\shared\\file.txt')).toBe('D:\\provider-b\\shared\\file.txt'); - expect(() => posixView.resolve('/provider-b/shared/file.txt')).toThrow('outside runtime workspace'); - expect(() => win32View.resolve('C:\\provider-a\\repo\\file.txt')).toThrow('outside runtime workspace'); + expect(() => posixView.assertAllowed(posixView.resolve('/provider-b/shared/file.txt'))).toThrow('outside runtime workspace'); + expect(() => win32View.assertAllowed(win32View.resolve('C:\\provider-a\\repo\\file.txt'))).toThrow('outside runtime workspace'); }); it('translates Git Bash POSIX paths on win32 bash runtimes', () => { @@ -95,7 +96,7 @@ describe('RuntimeWorkspaceView', () => { expect(view.resolve('/cygdrive/c/workspace/project/package.json')).toBe( 'C:\\workspace\\project\\package.json', ); - expect(() => view.resolve('/tmp/scratch.txt')).toThrow('outside runtime workspace'); + expect(() => view.assertAllowed(view.resolve('/tmp/scratch.txt'))).toThrow('outside runtime workspace'); }); it('deduplicates roots and preserves generation identity', () => {