diff --git a/.changeset/error-envelope-cleanup.md b/.changeset/error-envelope-cleanup.md new file mode 100644 index 00000000000..a80d20e29e1 --- /dev/null +++ b/.changeset/error-envelope-cleanup.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Stop sending server stack traces in error responses, and report a missing provider, credential, or model as a setup error instead of an internal error. diff --git a/docs/en/reference/server-api.md b/docs/en/reference/server-api.md index aaa98d0ad25..e1495c89987 100644 --- a/docs/en/reference/server-api.md +++ b/docs/en/reference/server-api.md @@ -65,7 +65,7 @@ Error codes are grouped by band: | `400xx` | Bad request | `40001` validation failed (`details` lists each field), `40003` provider is OAuth-managed, `40006` duplicate workspace source folder | | `401xx` | Auth and readiness | `40101` unauthorized, `40110` no provider configured, `40113` model not resolved | | `404xx` | Not found | `40401` session, `40408` MCP server, `40409` file path | -| `409xx` | State conflict | `40901` session busy, `40902` approval already resolved, `40922` page conditions mismatch `page_token`, `40931` session occupied by another client, `40932` session updated by another client | +| `409xx` | State conflict | `40901` session busy, `40902` approval already resolved, `40922` page conditions mismatch `page_token`, `40931` session occupied by another client, `40932` session updated by another client, `40935` Tower mode cannot be entered | | `410xx` | Expired | `41001` approval timed out, `41002` question timed out, `41003` temporary file expired | | `413xx` | Size or boundary exceeded | `41302` file read over 10 MB, `41304` path escapes the session directory | | `429xx` | Rate limited | `42901` auth-failure ban, `42902` too many fs watches | diff --git a/docs/zh/reference/server-api.md b/docs/zh/reference/server-api.md index 86ac416f5fc..ae0d3b4412b 100644 --- a/docs/zh/reference/server-api.md +++ b/docs/zh/reference/server-api.md @@ -65,7 +65,7 @@ HTTP 状态码几乎总是 200,业务结果以 `code` 为准。例外情况: | `400xx` | 请求参数错误 | `40001` 校验失败(`details` 逐字段说明)、`40003` 供应商由 OAuth 托管、`40006` 工作区源文件夹重复 | | `401xx` | 鉴权与就绪状态 | `40101` 未授权、`40110` 未配置供应商、`40113` 模型未解析 | | `404xx` | 资源不存在 | `40401` 会话、`40408` MCP 服务、`40409` 文件路径 | -| `409xx` | 状态冲突 | `40901` 会话忙、`40902` 审批已解决、`40922` 分页条件与 `page_token` 不符、`40931` 会话正被其他客户端使用、`40932` 会话已被其他客户端更新 | +| `409xx` | 状态冲突 | `40901` 会话忙、`40902` 审批已解决、`40922` 分页条件与 `page_token` 不符、`40931` 会话正被其他客户端使用、`40932` 会话已被其他客户端更新、`40935` 无法进入 Tower 模式 | | `410xx` | 资源已过期 | `41001` 审批超时、`41002` 提问超时、`41003` 临时文件过期 | | `413xx` | 体积或边界超限 | `41302` 读取文件超 10 MB、`41304` 路径越出会话目录 | | `429xx` | 限流 | `42901` 鉴权失败封禁、`42902` 文件监听数超限 | diff --git a/packages/kap-server/src/error-handler.ts b/packages/kap-server/src/error-handler.ts index cff17e027bc..5d006db5520 100644 --- a/packages/kap-server/src/error-handler.ts +++ b/packages/kap-server/src/error-handler.ts @@ -20,7 +20,7 @@ export function installErrorHandler(app: ErrorHandlerHost): void { if (isError2(err) && err.code === ErrorCodes.CONFIG_INVALID) { reply .status(200) - .send(errEnvelope(ErrorCode.VALIDATION_FAILED, err.message, requestId, err.stack)); + .send(errEnvelope(ErrorCode.VALIDATION_FAILED, err.message, requestId)); return; } if (isError2(err) && err.code === ErrorCodes.WIRE_VERSION_TOO_LOW) { @@ -31,7 +31,6 @@ export function installErrorHandler(app: ErrorHandlerHost): void { ErrorCode.WIRE_VERSION_TOO_LOW, err.message, requestId, - err.stack, err.details, ), ); @@ -43,7 +42,6 @@ export function installErrorHandler(app: ErrorHandlerHost): void { ErrorCode.INTERNAL_ERROR, err.message !== undefined && err.message !== '' ? err.message : 'internal error', requestId, - err.stack, ), ); }); diff --git a/packages/kap-server/src/protocol/envelope.ts b/packages/kap-server/src/protocol/envelope.ts index 455cbce7b02..d8b8393e451 100644 --- a/packages/kap-server/src/protocol/envelope.ts +++ b/packages/kap-server/src/protocol/envelope.ts @@ -7,7 +7,6 @@ export const envelopeSchema = (data: T) => data: data.nullable(), request_id: z.string(), details: z.unknown().optional(), - stack: z.string().optional(), }); export interface Envelope { @@ -16,7 +15,6 @@ export interface Envelope { data: T | null; request_id: string; details?: unknown; - stack?: string; } export function okEnvelope(data: T, requestId: string): Envelope { @@ -27,8 +25,7 @@ export function errEnvelope( code: number, msg: string, requestId: string, - stack?: string, details?: unknown, ): Envelope { - return { code, msg, data: null, request_id: requestId, stack, details }; + return { code, msg, data: null, request_id: requestId, details }; } diff --git a/packages/kap-server/src/protocol/error-codes.ts b/packages/kap-server/src/protocol/error-codes.ts index aad5e358687..f9b44784285 100644 --- a/packages/kap-server/src/protocol/error-codes.ts +++ b/packages/kap-server/src/protocol/error-codes.ts @@ -2,7 +2,6 @@ export const ErrorCode = { SUCCESS: 0, VALIDATION_FAILED: 40001, - REQUEST_MALFORMED: 40002, PROVIDER_OAUTH_MANAGED: 40003, CATALOG_IMPORT_INVALID: 40004, REGISTRY_IMPORT_INVALID: 40005, @@ -26,7 +25,6 @@ export const ErrorCode = { PROVIDER_NOT_FOUND: 40412, MODEL_NOT_FOUND: 40413, SKILL_NOT_FOUND: 40415, - TOOL_CALL_NOT_FOUND: 40416, CATALOG_ENTRY_NOT_FOUND: 40417, CAPABILITY_NOT_FOUND: 40418, PLUGIN_NOT_FOUND: 40419, @@ -66,6 +64,7 @@ export const ErrorCode = { SESSION_STALE: 40932, TASK_NOT_DETACHABLE: 40933, PLUGIN_CATALOG_CHANGED: 40934, + TOWER_MODE_INVALID: 40935, APPROVAL_EXPIRED: 41001, QUESTION_EXPIRED: 41002, diff --git a/packages/kap-server/src/routes/capabilities.ts b/packages/kap-server/src/routes/capabilities.ts index 9906bcf73fb..ccd6ef5bc13 100644 --- a/packages/kap-server/src/routes/capabilities.ts +++ b/packages/kap-server/src/routes/capabilities.ts @@ -141,12 +141,11 @@ const CAPABILITY_ERROR_MAP: Readonly> = { function mapCapabilityError(error: unknown, requestId: string) { const mapped = isError2(error) ? CAPABILITY_ERROR_MAP[error.code] : undefined; if (mapped !== undefined && isError2(error)) { - return errEnvelope(mapped, error.message, requestId, error.stack); + return errEnvelope(mapped, error.message, requestId); } return errEnvelope( ErrorCode.INTERNAL_ERROR, error instanceof Error ? error.message : String(error), requestId, - error instanceof Error ? error.stack : undefined, ); } diff --git a/packages/kap-server/src/routes/fs.ts b/packages/kap-server/src/routes/fs.ts index a72b475c426..819b3983220 100644 --- a/packages/kap-server/src/routes/fs.ts +++ b/packages/kap-server/src/routes/fs.ts @@ -883,34 +883,34 @@ function sendMappedError(reply: Reply, req: { id: string }, err: unknown): void if (isError2(err)) { switch (err.code) { case ErrorCodes.FS_PATH_ESCAPES: - reply.send(errEnvelope(ErrorCode.FS_PATH_ESCAPES_SESSION, err.message, requestId, err.stack)); + reply.send(errEnvelope(ErrorCode.FS_PATH_ESCAPES_SESSION, err.message, requestId)); return; case ErrorCodes.FS_PATH_NOT_FOUND: - reply.send(errEnvelope(ErrorCode.FS_PATH_NOT_FOUND, err.message, requestId, err.stack)); + reply.send(errEnvelope(ErrorCode.FS_PATH_NOT_FOUND, err.message, requestId)); return; case ErrorCodes.FS_IS_DIRECTORY: - reply.send(errEnvelope(ErrorCode.FS_IS_DIRECTORY, err.message, requestId, err.stack)); + reply.send(errEnvelope(ErrorCode.FS_IS_DIRECTORY, err.message, requestId)); return; case ErrorCodes.FS_ALREADY_EXISTS: - reply.send(errEnvelope(ErrorCode.FS_ALREADY_EXISTS, err.message, requestId, err.stack)); + reply.send(errEnvelope(ErrorCode.FS_ALREADY_EXISTS, err.message, requestId)); return; case ErrorCodes.FS_IS_BINARY: - reply.send(errEnvelope(ErrorCode.FS_IS_BINARY, err.message, requestId, err.stack)); + reply.send(errEnvelope(ErrorCode.FS_IS_BINARY, err.message, requestId)); return; case ErrorCodes.FS_TOO_LARGE: - reply.send(errEnvelope(ErrorCode.FS_TOO_LARGE, err.message, requestId, err.stack)); + reply.send(errEnvelope(ErrorCode.FS_TOO_LARGE, err.message, requestId)); return; case ErrorCodes.FS_TOO_MANY_RESULTS: - reply.send(errEnvelope(ErrorCode.FS_TOO_MANY_RESULTS, err.message, requestId, err.stack)); + reply.send(errEnvelope(ErrorCode.FS_TOO_MANY_RESULTS, err.message, requestId)); return; case ErrorCodes.FS_GREP_TIMEOUT: - reply.send(errEnvelope(ErrorCode.FS_GREP_TIMEOUT, err.message, requestId, err.stack)); + reply.send(errEnvelope(ErrorCode.FS_GREP_TIMEOUT, err.message, requestId)); return; case ErrorCodes.FS_GIT_UNAVAILABLE: - reply.send(errEnvelope(ErrorCode.FS_GIT_UNAVAILABLE, err.message, requestId, err.stack)); + reply.send(errEnvelope(ErrorCode.FS_GIT_UNAVAILABLE, err.message, requestId)); return; case ErrorCodes.SESSION_NOT_FOUND: - reply.send(errEnvelope(ErrorCode.SESSION_NOT_FOUND, err.message, requestId, err.stack)); + reply.send(errEnvelope(ErrorCode.SESSION_NOT_FOUND, err.message, requestId)); return; case ErrorCodes.WIRE_VERSION_TOO_LOW: reply.send( @@ -918,23 +918,22 @@ function sendMappedError(reply: Reply, req: { id: string }, err: unknown): void ErrorCode.WIRE_VERSION_TOO_LOW, err.message, requestId, - err.stack, err.details, ), ); return; case ErrorCodes.OS_FS_NOT_FOUND: case ErrorCodes.OS_FS_NOT_DIRECTORY: - reply.send(errEnvelope(ErrorCode.FS_PATH_NOT_FOUND, err.message, requestId, err.stack)); + reply.send(errEnvelope(ErrorCode.FS_PATH_NOT_FOUND, err.message, requestId)); return; case ErrorCodes.OS_FS_IS_DIRECTORY: - reply.send(errEnvelope(ErrorCode.FS_IS_DIRECTORY, err.message, requestId, err.stack)); + reply.send(errEnvelope(ErrorCode.FS_IS_DIRECTORY, err.message, requestId)); return; case ErrorCodes.OS_FS_ALREADY_EXISTS: - reply.send(errEnvelope(ErrorCode.FS_ALREADY_EXISTS, err.message, requestId, err.stack)); + reply.send(errEnvelope(ErrorCode.FS_ALREADY_EXISTS, err.message, requestId)); return; case ErrorCodes.OS_FS_PERMISSION_DENIED: - reply.send(errEnvelope(ErrorCode.FS_PERMISSION_DENIED, err.message, requestId, err.stack)); + reply.send(errEnvelope(ErrorCode.FS_PERMISSION_DENIED, err.message, requestId)); return; } } @@ -944,7 +943,6 @@ function sendMappedError(reply: Reply, req: { id: string }, err: unknown): void ErrorCode.INTERNAL_ERROR, err instanceof Error ? err.message : String(err), requestId, - err instanceof Error ? err.stack : undefined, ), ); } diff --git a/packages/kap-server/src/routes/messages.ts b/packages/kap-server/src/routes/messages.ts index a964823cd2c..185c4d279ee 100644 --- a/packages/kap-server/src/routes/messages.ts +++ b/packages/kap-server/src/routes/messages.ts @@ -75,8 +75,8 @@ export function registerMessagesRoutes(app: MessageRouteHost, core: Scope): void const { session_id } = req.params; const page = await listMessages(core, session_id, req.query); reply.send(okEnvelope(page, req.id)); - } catch (err) { - sendMappedError(reply, req, err); + } catch (error) { + sendMappedError(reply, req, error); } }, ); @@ -106,8 +106,8 @@ export function registerMessagesRoutes(app: MessageRouteHost, core: Scope): void const { session_id, message_id } = req.params; const message = await getMessage(core, session_id, message_id); reply.send(okEnvelope(message, req.id)); - } catch (err) { - sendMappedError(reply, req, err); + } catch (error) { + sendMappedError(reply, req, error); } }, ); @@ -125,11 +125,11 @@ function sendMappedError( ): void { const requestId = req.id; if (err instanceof SessionNotFoundError) { - reply.send(errEnvelope(ErrorCode.SESSION_NOT_FOUND, err.message, requestId, err.stack)); + reply.send(errEnvelope(ErrorCode.SESSION_NOT_FOUND, err.message, requestId)); return; } if (err instanceof MessageNotFoundError) { - reply.send(errEnvelope(ErrorCode.MESSAGE_NOT_FOUND, err.message, requestId, err.stack)); + reply.send(errEnvelope(ErrorCode.MESSAGE_NOT_FOUND, err.message, requestId)); return; } sendSessionMappedError(reply, req, err); diff --git a/packages/kap-server/src/routes/modelCatalog.ts b/packages/kap-server/src/routes/modelCatalog.ts index 0a59134a167..3f7e14b3e39 100644 --- a/packages/kap-server/src/routes/modelCatalog.ts +++ b/packages/kap-server/src/routes/modelCatalog.ts @@ -742,11 +742,11 @@ function sendMappedError( ): boolean { if (!isError2(err)) return false; if (err.code === 'provider.not_found') { - reply.send(errEnvelope(ErrorCode.PROVIDER_NOT_FOUND, err.message, requestId, err.stack)); + reply.send(errEnvelope(ErrorCode.PROVIDER_NOT_FOUND, err.message, requestId)); return true; } if (err.code === 'model.not_found') { - reply.send(errEnvelope(ErrorCode.MODEL_NOT_FOUND, err.message, requestId, err.stack)); + reply.send(errEnvelope(ErrorCode.MODEL_NOT_FOUND, err.message, requestId)); return true; } return false; @@ -768,7 +768,7 @@ function sendModelsDevImportError( if (!isError2(err)) return false; const numeric = MODELS_DEV_IMPORT_ERROR_CODES[err.code]; if (numeric === undefined) return false; - reply.send(errEnvelope(numeric, err.message, requestId, err.stack)); + reply.send(errEnvelope(numeric, err.message, requestId)); return true; } diff --git a/packages/kap-server/src/routes/plugins.ts b/packages/kap-server/src/routes/plugins.ts index 8c224a985b4..bca2f6d4d67 100644 --- a/packages/kap-server/src/routes/plugins.ts +++ b/packages/kap-server/src/routes/plugins.ts @@ -347,12 +347,11 @@ const PLUGIN_ERROR_MAP: Readonly> = { function mapPluginError(error: unknown, requestId: string) { const mapped = isError2(error) ? PLUGIN_ERROR_MAP[error.code] : undefined; if (mapped !== undefined && isError2(error)) { - return errEnvelope(mapped, error.message, requestId, error.stack); + return errEnvelope(mapped, error.message, requestId); } return errEnvelope( ErrorCode.INTERNAL_ERROR, error instanceof Error ? error.message : String(error), requestId, - error instanceof Error ? error.stack : undefined, ); } diff --git a/packages/kap-server/src/routes/remoteControl.ts b/packages/kap-server/src/routes/remoteControl.ts index c16aab5006f..4cab10b6126 100644 --- a/packages/kap-server/src/routes/remoteControl.ts +++ b/packages/kap-server/src/routes/remoteControl.ts @@ -158,7 +158,7 @@ export function registerRemoteControlRoutes( if (error instanceof RemoteControlAlreadyRunningError) { opts.telemetry?.track2('remote_control_toggle', { enabled, outcome: 'already_running' }); reply.send( - errEnvelope(ErrorCode.REMOTE_CONTROL_ALREADY_RUNNING, error.message, req.id, undefined, { + errEnvelope(ErrorCode.REMOTE_CONTROL_ALREADY_RUNNING, error.message, req.id, { pid: error.holder.pid, local_origin: error.holder.localOrigin, device_id: error.holder.deviceId, diff --git a/packages/kap-server/src/routes/search.ts b/packages/kap-server/src/routes/search.ts index 250cd4f3992..e37417e35eb 100644 --- a/packages/kap-server/src/routes/search.ts +++ b/packages/kap-server/src/routes/search.ts @@ -96,7 +96,7 @@ export function registerSearchRoutes(app: SearchRouteHost, core: Scope): void { error instanceof GlobalSearchError && (error.reason === 'invalid_query' || error.reason === 'invalid_page_token') ) { - reply.send(errEnvelope(ErrorCode.VALIDATION_FAILED, error.message, req.id, error.stack)); + reply.send(errEnvelope(ErrorCode.VALIDATION_FAILED, error.message, req.id)); return; } requestLog(req)?.error({ err: error }, 'global search request failed'); @@ -105,7 +105,6 @@ export function registerSearchRoutes(app: SearchRouteHost, core: Scope): void { ErrorCode.INTERNAL_ERROR, error instanceof Error ? error.message : String(error), req.id, - error instanceof Error ? error.stack : undefined, ), ); } diff --git a/packages/kap-server/src/routes/sessionErrors.ts b/packages/kap-server/src/routes/sessionErrors.ts index 9877af768d7..f58a55354c0 100644 --- a/packages/kap-server/src/routes/sessionErrors.ts +++ b/packages/kap-server/src/routes/sessionErrors.ts @@ -21,7 +21,6 @@ export function sendMappedError( msg: error.message, data: error.details ?? null, request_id: req.id, - stack: error.stack, }); return; } diff --git a/packages/kap-server/src/routes/sessions.ts b/packages/kap-server/src/routes/sessions.ts index 531c4a371ca..8e20af42a9f 100644 --- a/packages/kap-server/src/routes/sessions.ts +++ b/packages/kap-server/src/routes/sessions.ts @@ -526,6 +526,7 @@ export function registerSessionsRoutes(app: SessionRouteHost, core: Scope, conve errors: { [ErrorCode.VALIDATION_FAILED]: { detailsSchema }, [ErrorCode.SESSION_NOT_FOUND]: {}, + [ErrorCode.TOWER_MODE_INVALID]: {}, ...sessionConflictErrors, }, description: 'Update session profile (title, metadata, agent_config)', diff --git a/packages/kap-server/src/routes/tools.ts b/packages/kap-server/src/routes/tools.ts index c7c00ede6aa..bb7a285ae51 100644 --- a/packages/kap-server/src/routes/tools.ts +++ b/packages/kap-server/src/routes/tools.ts @@ -263,7 +263,7 @@ function sendMappedError( err: unknown, ): void { if (err instanceof Error2 && err.code === ErrorCodes.MCP_SERVER_NOT_FOUND) { - reply.send(errEnvelope(ErrorCode.MCP_SERVER_NOT_FOUND, err.message, requestId, err.stack)); + reply.send(errEnvelope(ErrorCode.MCP_SERVER_NOT_FOUND, err.message, requestId)); return; } throw err; diff --git a/packages/kap-server/src/routes/v2/mcp.ts b/packages/kap-server/src/routes/v2/mcp.ts index 9976e774cb9..f35ff909a77 100644 --- a/packages/kap-server/src/routes/v2/mcp.ts +++ b/packages/kap-server/src/routes/v2/mcp.ts @@ -195,14 +195,14 @@ function sendMappedError( if (isError2(err)) { switch (err.code) { case ErrorCodes.MCP_SERVER_NOT_FOUND: - reply.send(errEnvelope(ErrorCode.MCP_SERVER_NOT_FOUND, err.message, requestId, err.stack)); + reply.send(errEnvelope(ErrorCode.MCP_SERVER_NOT_FOUND, err.message, requestId)); return; case ErrorCodes.REQUEST_INVALID: case ErrorCodes.CONFIG_INVALID: - reply.send(errEnvelope(ErrorCode.VALIDATION_FAILED, err.message, requestId, err.stack)); + reply.send(errEnvelope(ErrorCode.VALIDATION_FAILED, err.message, requestId)); return; case ErrorCodes.MCP_OAUTH_FAILED: - reply.send(errEnvelope(ErrorCode.MCP_OAUTH_FAILED, err.message, requestId, err.stack)); + reply.send(errEnvelope(ErrorCode.MCP_OAUTH_FAILED, err.message, requestId)); return; } } @@ -227,8 +227,8 @@ export function registerV2McpRoutes(app: V2McpRouteHost, core: Scope): void { try { const servers = await management().listServers({ cwd: req.query.cwd }); reply.send(okEnvelope(servers, req.id)); - } catch (err) { - sendMappedError(reply, req.id, err); + } catch (error) { + sendMappedError(reply, req.id, error); } }, ); @@ -253,8 +253,8 @@ export function registerV2McpRoutes(app: V2McpRouteHost, core: Scope): void { try { const server = await management().getServer(req.params.name, { cwd: req.query.cwd }); reply.send(okEnvelope(server, req.id)); - } catch (err) { - sendMappedError(reply, req.id, err); + } catch (error) { + sendMappedError(reply, req.id, error); } }, ); @@ -280,8 +280,8 @@ export function registerV2McpRoutes(app: V2McpRouteHost, core: Scope): void { try { const servers = await management().addServer(req.body, { cwd: req.query.cwd }); reply.send(okEnvelope(servers, req.id)); - } catch (err) { - sendMappedError(reply, req.id, err); + } catch (error) { + sendMappedError(reply, req.id, error); } }, ); @@ -311,8 +311,8 @@ export function registerV2McpRoutes(app: V2McpRouteHost, core: Scope): void { { cwd: req.query.cwd }, ); reply.send(okEnvelope(servers, req.id)); - } catch (err) { - sendMappedError(reply, req.id, err); + } catch (error) { + sendMappedError(reply, req.id, error); } }, ); @@ -338,8 +338,8 @@ export function registerV2McpRoutes(app: V2McpRouteHost, core: Scope): void { try { const servers = await management().removeServer(req.params.name, { cwd: req.query.cwd }); reply.send(okEnvelope(servers, req.id)); - } catch (err) { - sendMappedError(reply, req.id, err); + } catch (error) { + sendMappedError(reply, req.id, error); } }, ); @@ -364,8 +364,8 @@ export function registerV2McpRoutes(app: V2McpRouteHost, core: Scope): void { try { const result = await management().testServer(req.body); reply.send(okEnvelope(result, req.id)); - } catch (err) { - sendMappedError(reply, req.id, err); + } catch (error) { + sendMappedError(reply, req.id, error); } }, ); @@ -392,8 +392,8 @@ export function registerV2McpRoutes(app: V2McpRouteHost, core: Scope): void { cwd: req.body.cwd, }); reply.send(okEnvelope(inspections, req.id)); - } catch (err) { - sendMappedError(reply, req.id, err); + } catch (error) { + sendMappedError(reply, req.id, error); } }, ); @@ -421,8 +421,8 @@ export function registerV2McpRoutes(app: V2McpRouteHost, core: Scope): void { verify: req.query.verify === undefined ? undefined : req.query.verify === 'true', }); reply.send(okEnvelope(statuses, req.id)); - } catch (err) { - sendMappedError(reply, req.id, err); + } catch (error) { + sendMappedError(reply, req.id, error); } }, ); @@ -448,8 +448,8 @@ export function registerV2McpRoutes(app: V2McpRouteHost, core: Scope): void { try { const result = await management().beginServerAuth(req.body, { cwd: req.query.cwd }); reply.send(okEnvelope(result, req.id)); - } catch (err) { - sendMappedError(reply, req.id, err); + } catch (error) { + sendMappedError(reply, req.id, error); } }, ); @@ -481,8 +481,8 @@ export function registerV2McpRoutes(app: V2McpRouteHost, core: Scope): void { try { await management().completeServerAuth(req.body, { signal: disconnect.signal }); reply.send(okEnvelope(null, req.id)); - } catch (err) { - sendMappedError(reply, req.id, err); + } catch (error) { + sendMappedError(reply, req.id, error); } finally { raw.off('close', onClose); } @@ -508,8 +508,8 @@ export function registerV2McpRoutes(app: V2McpRouteHost, core: Scope): void { try { await management().cancelServerAuth(req.body); reply.send(okEnvelope(null, req.id)); - } catch (err) { - sendMappedError(reply, req.id, err); + } catch (error) { + sendMappedError(reply, req.id, error); } }, ); @@ -535,8 +535,8 @@ export function registerV2McpRoutes(app: V2McpRouteHost, core: Scope): void { try { await management().resetServerAuth(req.body, { cwd: req.query.cwd }); reply.send(okEnvelope(null, req.id)); - } catch (err) { - sendMappedError(reply, req.id, err); + } catch (error) { + sendMappedError(reply, req.id, error); } }, ); diff --git a/packages/kap-server/src/routes/workspaceFs.ts b/packages/kap-server/src/routes/workspaceFs.ts index 800d0dbfbbe..d301919a47f 100644 --- a/packages/kap-server/src/routes/workspaceFs.ts +++ b/packages/kap-server/src/routes/workspaceFs.ts @@ -400,15 +400,15 @@ function sendMappedError( err: unknown, ): void { if (err instanceof HostFolderNotAbsoluteError) { - reply.send(errEnvelope(ErrorCode.VALIDATION_FAILED, err.message, requestId, err.stack)); + reply.send(errEnvelope(ErrorCode.VALIDATION_FAILED, err.message, requestId)); return; } if (err instanceof HostFolderNotFoundError) { - reply.send(errEnvelope(ErrorCode.FS_PATH_NOT_FOUND, err.message, requestId, err.stack)); + reply.send(errEnvelope(ErrorCode.FS_PATH_NOT_FOUND, err.message, requestId)); return; } if (err instanceof HostFolderPermissionError) { - reply.send(errEnvelope(ErrorCode.FS_PERMISSION_DENIED, err.message, requestId, err.stack)); + reply.send(errEnvelope(ErrorCode.FS_PERMISSION_DENIED, err.message, requestId)); return; } throw err; diff --git a/packages/kap-server/src/routes/workspaces.ts b/packages/kap-server/src/routes/workspaces.ts index c9f4cc6c26a..f09551bd4cb 100644 --- a/packages/kap-server/src/routes/workspaces.ts +++ b/packages/kap-server/src/routes/workspaces.ts @@ -497,7 +497,6 @@ function buildDuplicateDirEnvelope(index: number, dir: string, requestId: string ErrorCode.WORKSPACE_DUPLICATE_DIR, `Duplicate source folder: ${directory}`, requestId, - undefined, [{ path: `additional_dirs.${index}`, directory }], ); } diff --git a/packages/kap-server/src/transport/businessSnapshotRoutes.ts b/packages/kap-server/src/transport/businessSnapshotRoutes.ts index cd20759fc55..855a8b258b7 100644 --- a/packages/kap-server/src/transport/businessSnapshotRoutes.ts +++ b/packages/kap-server/src/transport/businessSnapshotRoutes.ts @@ -1,7 +1,7 @@ import type { Scope } from '@moonshot-ai/agent-core-v2'; import { okEnvelope } from '../protocol/envelope'; -import { mapError, withTimeout } from './errors'; +import { mapDebugError, withTimeout } from './errors'; import type { RouteHost } from './serviceDispatcherRoutes'; import { agentEnvironmentBindingSnapshot, @@ -67,6 +67,6 @@ async function sendSnapshot( try { return reply.send(okEnvelope(await withTimeout(Promise.resolve(read()), callTimeoutMs), req.id)); } catch (error) { - return reply.send(mapError(error, req.id)); + return reply.send(mapDebugError(error, req.id)); } } diff --git a/packages/kap-server/src/transport/errors.ts b/packages/kap-server/src/transport/errors.ts index 1643ceabc5b..cc4a8093dd6 100644 --- a/packages/kap-server/src/transport/errors.ts +++ b/packages/kap-server/src/transport/errors.ts @@ -58,31 +58,43 @@ const KIMI_TO_PROTOCOL: Record = { [ErrorCodes.STORAGE_IO_FAILED]: ErrorCode.PERSISTENCE_FAILURE, [ErrorCodes.STORAGE_LOCKED]: ErrorCode.PERSISTENCE_FAILURE, [ErrorCodes.WIRE_VERSION_TOO_LOW]: ErrorCode.WIRE_VERSION_TOO_LOW, + [ErrorCodes.SESSION_TOWER_MODE_INVALID]: ErrorCode.TOWER_MODE_INVALID, + [ErrorCodes.AUTH_PROVISIONING_REQUIRED]: ErrorCode.AUTH_PROVISIONING_REQUIRED, + [ErrorCodes.AUTH_TOKEN_MISSING]: ErrorCode.AUTH_TOKEN_MISSING, + [ErrorCodes.AUTH_MODEL_NOT_RESOLVED]: ErrorCode.AUTH_MODEL_NOT_RESOLVED, }; const PUBLIC_DETAIL_CODES: ReadonlySet = new Set([ ErrorCodes.SESSION_OCCUPIED, ErrorCodes.SESSION_STALE, ErrorCodes.WIRE_VERSION_TOO_LOW, + ErrorCodes.AUTH_TOKEN_MISSING, + ErrorCodes.AUTH_MODEL_NOT_RESOLVED, ]); export function mapError(err: unknown, requestId: string): ReturnType { if (err instanceof Error2) { const code = KIMI_TO_PROTOCOL[err.code] ?? ErrorCode.INTERNAL_ERROR; const details = PUBLIC_DETAIL_CODES.has(err.code) ? err.details : undefined; - return errEnvelope(code, err.message, requestId, err.stack, details); + return errEnvelope(code, err.message, requestId, details); } if (err instanceof TimeoutError) { - return errEnvelope(ErrorCode.INTERNAL_ERROR, err.message, requestId, err.stack); + return errEnvelope(ErrorCode.INTERNAL_ERROR, err.message, requestId); } return errEnvelope( ErrorCode.INTERNAL_ERROR, err instanceof Error ? err.message : String(err), requestId, - err instanceof Error ? err.stack : undefined, ); } +export function mapDebugError( + err: unknown, + requestId: string, +): ReturnType & { stack?: string } { + return { ...mapError(err, requestId), stack: err instanceof Error ? err.stack : undefined }; +} + export function validationEnvelope( details: { path: string; message: string }[], requestId: string, diff --git a/packages/kap-server/src/transport/serviceDispatcherRoutes.ts b/packages/kap-server/src/transport/serviceDispatcherRoutes.ts index ad285a7c55a..31490f566a1 100644 --- a/packages/kap-server/src/transport/serviceDispatcherRoutes.ts +++ b/packages/kap-server/src/transport/serviceDispatcherRoutes.ts @@ -11,7 +11,7 @@ import { resolveAnyScopedServiceId, } from './channelRegistry'; import { type ChannelLookup, dispatch } from './dispatcher'; -import { mapError, validationEnvelope, withTimeout } from './errors'; +import { mapDebugError, validationEnvelope, withTimeout } from './errors'; interface RpcRequest { readonly id: string; @@ -113,7 +113,7 @@ function makeHandler( return reply.send(okEnvelope(result, requestId)); } catch (error) { promptAttempt?.fail(error); - const envelope = mapError(error, requestId); + const envelope = mapDebugError(error, requestId); const log = requestLog(req); if (envelope.code === ErrorCode.INTERNAL_ERROR) { log?.error({ err: error, service, method }, 'rpc dispatch failed'); diff --git a/packages/kap-server/test/openapi.test.ts b/packages/kap-server/test/openapi.test.ts index 54215bc48d2..7867aa33997 100644 --- a/packages/kap-server/test/openapi.test.ts +++ b/packages/kap-server/test/openapi.test.ts @@ -115,6 +115,19 @@ describe('server-v2 OpenAPI', () => { ).toBe(true); }); + it('documents the Tower mode failure on the profile route', async () => { + const doc = await fetchOpenApi(); + const op = operation(doc, '/api/v1/sessions/{session_id}/profile', 'post'); + const content = asRecord(asRecord(asRecord(op['responses'])['200'])['content']); + const variants = asRecord(asRecord(content['application/json'])['schema'])['oneOf']; + expect( + (variants as unknown[]).some((variant) => { + const values = asRecord(asRecord(asRecord(variant)['properties'])['code'])['enum']; + return Array.isArray(values) && values.includes(40935); + }), + ).toBe(true); + }); + it('documents session conflicts for session mutation routes', async () => { const doc = await fetchOpenApi(); const paths = [ diff --git a/packages/kap-server/test/sessions.test.ts b/packages/kap-server/test/sessions.test.ts index 747bb7e980f..50d1a49c8e7 100644 --- a/packages/kap-server/test/sessions.test.ts +++ b/packages/kap-server/test/sessions.test.ts @@ -906,7 +906,7 @@ describe('server-v2 /api/v1/sessions', () => { const on = await postJson(`/api/v1/sessions/${id}/profile`, { agent_config: { tower_mode: true }, }); - expect(on.body.code).toBe(50001); + expect(on.body.code).toBe(40935); expect(on.body.msg).toContain('the tower experiment is disabled'); expect(on.body.msg).toContain('KIMI_CODE_EXPERIMENTAL_TOWER=1'); expect(readAgentState(await mainAgentOf(id), false).tower_mode).toBe(false); diff --git a/packages/kap-server/test/transport-errors.test.ts b/packages/kap-server/test/transport-errors.test.ts index c8179020652..8458594375f 100644 --- a/packages/kap-server/test/transport-errors.test.ts +++ b/packages/kap-server/test/transport-errors.test.ts @@ -2,7 +2,7 @@ import { Error2, ErrorCodes } from '@moonshot-ai/agent-core-v2'; import { ErrorCode } from '../src/protocol/error-codes'; import { describe, expect, it } from 'vitest'; -import { mapError } from '../src/transport/errors'; +import { mapDebugError, mapError } from '../src/transport/errors'; import { installErrorHandler } from '../src/error-handler'; describe('ErrorCode', () => { @@ -24,6 +24,10 @@ describe('/api/v1/debug transport mapError', () => { [ErrorCodes.CONFIG_INVALID, ErrorCode.VALIDATION_FAILED], [ErrorCodes.GOAL_UNSUPPORTED_AGENT, ErrorCode.GOAL_UNSUPPORTED_AGENT], [ErrorCodes.PROMPT_ID_CONFLICT, ErrorCode.PROMPT_ID_CONFLICT], + [ErrorCodes.SESSION_TOWER_MODE_INVALID, ErrorCode.TOWER_MODE_INVALID], + [ErrorCodes.AUTH_PROVISIONING_REQUIRED, ErrorCode.AUTH_PROVISIONING_REQUIRED], + [ErrorCodes.AUTH_TOKEN_MISSING, ErrorCode.AUTH_TOKEN_MISSING], + [ErrorCodes.AUTH_MODEL_NOT_RESOLVED, ErrorCode.AUTH_MODEL_NOT_RESOLVED], ])('maps domain code %s to its wire equivalent', (code, wire) => { const env = mapError(new Error2(code, 'boom'), 'req-1'); expect(env.code).toBe(wire); @@ -46,6 +50,26 @@ describe('/api/v1/debug transport mapError', () => { expect(incompatible.details).toEqual(versionDetails); }); + it('keeps the provider and model details of auth readiness errors', () => { + const missing = mapError( + new Error2(ErrorCodes.AUTH_TOKEN_MISSING, 'no key', { details: { provider_id: 'example' } }), + 'req-1', + ); + expect(missing.details).toEqual({ provider_id: 'example' }); + const unresolved = mapError( + new Error2(ErrorCodes.AUTH_MODEL_NOT_RESOLVED, 'no model', { details: { model_id: 'm1' } }), + 'req-1', + ); + expect(unresolved.details).toEqual({ model_id: 'm1' }); + }); + + it('never sends the server stack trace, except to debug callers', () => { + const error = new Error2(ErrorCodes.OS_FS_UNKNOWN, 'boom'); + expect(mapError(error, 'req-1')).not.toHaveProperty('stack', expect.anything()); + expect(mapError(new Error('plain'), 'req-1')).not.toHaveProperty('stack', expect.anything()); + expect(mapDebugError(error, 'req-1').stack).toBe(error.stack); + }); + it('falls back to INTERNAL_ERROR for coded errors without a wire equivalent', () => { const env = mapError(new Error2(ErrorCodes.OS_FS_UNKNOWN, 'boom'), 'req-1'); expect(env.code).toBe(ErrorCode.INTERNAL_ERROR); @@ -88,7 +112,9 @@ describe('installErrorHandler (catch-all)', () => { expect(incompatible.details).toEqual(details); }); - it('keeps unknown exceptions at INTERNAL_ERROR', () => { - expect(run(new Error('boom')).code).toBe(ErrorCode.INTERNAL_ERROR); + it('keeps unknown exceptions at INTERNAL_ERROR without the stack trace', () => { + const env = run(new Error('boom')); + expect(env.code).toBe(ErrorCode.INTERNAL_ERROR); + expect(env).not.toHaveProperty('stack', expect.anything()); }); });