Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/bash-cwd-workspace-restriction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

Remove the workspace restriction on the Bash tool's cwd parameter.
8 changes: 3 additions & 5 deletions packages/agent-core-v2/src/runtime/runtimeWorkspaceView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Comment on lines +33 to 35

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Include cwd in Bash permission matching

When a user has approved a command such as Bash(cat secrets) for the session, or configured the equivalent allow rule, BashTool.resolveExecution matches only args.command; after resolve stops rejecting paths outside the workspace, a later call with the same command and cwd: "/etc" is therefore auto-approved even though it accesses different files. Previously that cwd failed, while the equivalent cd /etc && cat secrets changed the permission-rule subject and required separate approval. Include the effective cwd in Bash's approval/matching subject or represent it as a permission-checked file access before lifting this restriction.

Useful? React with 👍 / 👎.

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}`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'));
Expand Down
11 changes: 6 additions & 5 deletions packages/agent-core-v2/test/runtime/runtimeWorkspaceView.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand All @@ -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', () => {
Expand Down Expand Up @@ -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', () => {
Expand All @@ -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', () => {
Expand Down
Loading