diff --git a/.changeset/refactor-assert-allowed.md b/.changeset/refactor-assert-allowed.md new file mode 100644 index 0000000000..0acab2f83c --- /dev/null +++ b/.changeset/refactor-assert-allowed.md @@ -0,0 +1,6 @@ +--- +"@moonshot-ai/agent-core-v2": patch +"@moonshot-ai/kap-server": patch +--- + +Refactor `assertAllowed` to throw a coded `Error2` instead of a raw `Error` when a path escapes the workspace. This allows the terminal routing code to elegantly fold the error handling into the standard `isError2` switch block. diff --git a/packages/agent-core-v2/src/session/workspaceContext/workspaceContextService.ts b/packages/agent-core-v2/src/session/workspaceContext/workspaceContextService.ts index 4ca6b418de..2fd5e5180a 100644 --- a/packages/agent-core-v2/src/session/workspaceContext/workspaceContextService.ts +++ b/packages/agent-core-v2/src/session/workspaceContext/workspaceContextService.ts @@ -12,6 +12,7 @@ import { isAbsolute, relative, resolve } from 'node:path'; import { LifecycleScope, ScopeActivation, registerScopedService } from '#/_base/di/scope'; import { defineState } from '#/_base/state/stateRegistry'; +import { Error2, ErrorCodes } from '#/errors'; import { ISessionContext } from '#/session/sessionContext/sessionContext'; import { ISessionStateService } from '#/session/state/sessionState'; @@ -85,7 +86,7 @@ export class SessionWorkspaceContextService implements ISessionWorkspaceContext assertAllowed(absPath: string, op: PathAccessOperation): string { const target = this.resolve(absPath); if (!this.isWithin(target)) { - throw new Error(`Path outside workspace (${op}): ${target}`); + throw new Error2(ErrorCodes.FS_PATH_ESCAPES, `Path outside workspace (${op}): ${target}`); } return target; } diff --git a/packages/agent-core-v2/test/harness/agent.ts b/packages/agent-core-v2/test/harness/agent.ts index 7c9d868ca0..6467fff9c1 100644 --- a/packages/agent-core-v2/test/harness/agent.ts +++ b/packages/agent-core-v2/test/harness/agent.ts @@ -6,6 +6,7 @@ import { createControlledPromise } from '@antfu/utils'; import { expect, vi } from 'vitest'; import { toDisposable } from '#/_base/di/lifecycle'; +import { Error2, ErrorCodes } from '#/errors'; import type { IAgentScopeHandle } from '#/_base/di/scope'; import { Emitter, Event } from '#/_base/event'; import { IAgentLifecycleService } from '#/session/agentLifecycle/agentLifecycle'; @@ -2134,7 +2135,7 @@ function createWorkspaceContextStub( assertAllowed: (absPath: string, op: PathAccessOperation) => { const target = isAbsolute(absPath) ? resolve(absPath) : resolve(workDir, absPath); if (!isWithin(target)) { - throw new Error(`Path outside workspace (${op}): ${target}`); + throw new Error2(ErrorCodes.FS_PATH_ESCAPES, `Path outside workspace (${op}): ${target}`); } return target; }, diff --git a/packages/kap-server/src/routes/terminals.ts b/packages/kap-server/src/routes/terminals.ts index 8a0ce8b5c1..d81be8d68d 100644 --- a/packages/kap-server/src/routes/terminals.ts +++ b/packages/kap-server/src/routes/terminals.ts @@ -241,15 +241,10 @@ function sendMappedError( case ErrorCodes.TERMINAL_NOT_FOUND: reply.send(errEnvelope(ErrorCode.TERMINAL_NOT_FOUND, err.message, requestId, err.stack)); return; + case ErrorCodes.FS_PATH_ESCAPES: + reply.send(errEnvelope(ErrorCode.FS_PATH_ESCAPES_SESSION, err.message, requestId, err.stack)); + return; } } - // `ISessionWorkspaceContext.assertAllowed` throws a plain (uncoded) Error when a cwd - // escapes the workspace — map it to the same wire code v1 uses for path - // escapes. TODO: push a coded error into `assertAllowed` so this branch can - // be folded into the `isError2` switch above. - if (err instanceof Error && err.message.startsWith('Path outside workspace')) { - reply.send(errEnvelope(ErrorCode.FS_PATH_ESCAPES_SESSION, err.message, requestId, err.stack)); - return; - } throw err; }