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
5 changes: 5 additions & 0 deletions .changeset/clear-terminals-workspace-error.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

Fix terminal path-escape errors so the CLI reports a consistent workspace message.
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { isAbsolute, relative, resolve } from 'node:path';

import { Service } from '#/_base/di/service';
import { LifecycleScope } from '#/app/scopes';
import { ScopeActivation, registerScopedService } from '#/_base/di/scope';
import { LifecycleScope } from '#/app/scopes';
import { defineState } from '#/state/state';
import { ErrorCodes, Error2 } from '#/errors';
import { ISessionContext } from '#/session/sessionContext/sessionContext';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { isAbsolute, resolve } from 'node:path';

import { afterEach, beforeEach, describe, expect, it } from 'vitest';

import { DisposableStore } from '#/_base/di/lifecycle';
import { createServices, type TestInstantiationService } from '#/_base/di/test';
import { Event } from '#/_base/event';
import { ErrorCodes, Error2 } from '#/errors';
import { makeSessionContext, ISessionContext } from '#/session/sessionContext/sessionContext';
import { ISessionStateService } from '#/session/state/sessionState';
import { SessionStateService } from '#/session/state/sessionStateService';
import { ISessionWorkspaceContext } from '#/session/workspaceContext/workspaceContext';
import { SessionWorkspaceContextService } from '#/session/workspaceContext/workspaceContextService';
import { ISessionWorkspaceInfo } from '#/session/workspaceInfo/workspaceInfo';

function stubSessionContext(cwd: string = '/repo'): ISessionContext {
return makeSessionContext({
sessionId: 's1',
workspaceId: 'w1',
sessionDir: '/repo/.session',
sessionScope: 'session:s1',
cwd,
});
}

describe('SessionWorkspaceContextService', () => {
let disposables: DisposableStore;
let ix: TestInstantiationService;
let additionalDirs: string[];

beforeEach(() => {
disposables = new DisposableStore();
additionalDirs = ['/extra'];
ix = createServices(disposables, {
additionalServices: (reg) => {
reg.defineInstance(ISessionContext, stubSessionContext());
reg.defineInstance(ISessionStateService, new SessionStateService());
reg.defineInstance(ISessionWorkspaceInfo, {
_serviceBrand: undefined,
ready: Promise.resolve(),
get additionalDirs() {
return additionalDirs;
},
onDidChange: Event.None,
});
reg.define(ISessionWorkspaceContext, SessionWorkspaceContextService);
},
});
});

afterEach(() => disposables.dispose());

it('resolves relative paths against the session work dir', () => {
const workspace = ix.get(ISessionWorkspaceContext);

expect(workspace.resolve('src/index.ts')).toBe(resolve('/repo/src/index.ts'));
expect(workspace.assertAllowed('src/index.ts', 'read')).toBe(resolve('/repo/src/index.ts'));
});

it('throws a coded error when a path escapes the workspace', () => {
const workspace = ix.get(ISessionWorkspaceContext);
const escapingPath = resolve('/repo/../outside');

expect(() => workspace.assertAllowed('../outside', 'execute')).toThrow(Error2);

try {
workspace.assertAllowed('../outside', 'execute');
throw new Error('expected assertAllowed to throw');
} catch (err) {
expect(err).toBeInstanceOf(Error2);
const coded = err as Error2;
expect(coded.code).toBe(ErrorCodes.FS_PATH_ESCAPES);
expect(coded.message).toBe(`Path outside workspace (execute): ${escapingPath}`);
expect(coded.details).toEqual({ op: 'execute', path: escapingPath });
}
});

it('allows paths inside additional dirs', () => {
const workspace = ix.get(ISessionWorkspaceContext);

const target = workspace.assertAllowed('/extra/file.txt', 'read');

expect(isAbsolute(target)).toBe(true);
expect(target).toBe(resolve('/extra/file.txt'));
});
});
1 change: 1 addition & 0 deletions packages/kap-server/test/terminals.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,7 @@ describe('server-v2 /api/v1/sessions/{sid}/terminals', () => {
cwd: '../outside',
});
expect(escaping.code).toBe(ErrorCode.FS_PATH_ESCAPES_SESSION);
expect(escaping.msg).toContain('outside');

const noSession = await get<unknown>(`/api/v1/sessions/sess_missing/terminals`);
expect(noSession.code).toBe(ErrorCode.SESSION_NOT_FOUND);
Expand Down