diff --git a/src/gateway/server-methods/sessions.ts b/src/gateway/server-methods/sessions.ts index c10f825d45f..57855041d16 100644 --- a/src/gateway/server-methods/sessions.ts +++ b/src/gateway/server-methods/sessions.ts @@ -1,6 +1,6 @@ import { randomUUID } from "node:crypto"; import fs from "node:fs"; -import type { GatewayRequestHandlers } from "./types.js"; +import type { GatewayRequestHandlers, RespondFn } from "./types.js"; import { resolveDefaultAgentId } from "../../agents/agent-scope.js"; import { abortEmbeddedPiRun, waitForEmbeddedPiRunEnd } from "../../agents/pi-embedded.js"; import { stopSubagentsForRequester } from "../../auto-reply/reply/abort.js"; @@ -18,7 +18,6 @@ import { normalizeAgentId, parseAgentSessionKey } from "../../routing/session-ke import { ErrorCodes, errorShape, - formatValidationErrors, validateSessionsCompactParams, validateSessionsDeleteParams, validateSessionsListParams, @@ -44,6 +43,22 @@ import { } from "../session-utils.js"; import { applySessionsPatchToStore } from "../sessions-patch.js"; import { resolveSessionKeyFromResolveParams } from "../sessions-resolve.js"; +import { assertValidParams } from "./validation.js"; + +function requireSessionKey(key: unknown, respond: RespondFn): string | null { + const normalized = String(key ?? "").trim(); + if (!normalized) { + respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "key required")); + return null; + } + return normalized; +} + +function resolveGatewaySessionTargetFromKey(key: string) { + const cfg = loadConfig(); + const target = resolveGatewaySessionStoreTarget({ cfg, key }); + return { cfg, target, storePath: target.storePath }; +} function migrateAndPruneSessionStoreKey(params: { cfg: ReturnType; @@ -118,15 +133,7 @@ async function ensureSessionRuntimeCleanup(params: { export const sessionsHandlers: GatewayRequestHandlers = { "sessions.list": ({ params, respond }) => { - if (!validateSessionsListParams(params)) { - respond( - false, - undefined, - errorShape( - ErrorCodes.INVALID_REQUEST, - `invalid sessions.list params: ${formatValidationErrors(validateSessionsListParams.errors)}`, - ), - ); + if (!assertValidParams(params, validateSessionsListParams, "sessions.list", respond)) { return; } const p = params; @@ -141,17 +148,7 @@ export const sessionsHandlers: GatewayRequestHandlers = { respond(true, result, undefined); }, "sessions.preview": ({ params, respond }) => { - if (!validateSessionsPreviewParams(params)) { - respond( - false, - undefined, - errorShape( - ErrorCodes.INVALID_REQUEST, - `invalid sessions.preview params: ${formatValidationErrors( - validateSessionsPreviewParams.errors, - )}`, - ), - ); + if (!assertValidParams(params, validateSessionsPreviewParams, "sessions.preview", respond)) { return; } const p = params; @@ -213,15 +210,7 @@ export const sessionsHandlers: GatewayRequestHandlers = { respond(true, { ts: Date.now(), previews } satisfies SessionsPreviewResult, undefined); }, "sessions.resolve": async ({ params, respond }) => { - if (!validateSessionsResolveParams(params)) { - respond( - false, - undefined, - errorShape( - ErrorCodes.INVALID_REQUEST, - `invalid sessions.resolve params: ${formatValidationErrors(validateSessionsResolveParams.errors)}`, - ), - ); + if (!assertValidParams(params, validateSessionsResolveParams, "sessions.resolve", respond)) { return; } const p = params; @@ -235,27 +224,16 @@ export const sessionsHandlers: GatewayRequestHandlers = { respond(true, { ok: true, key: resolved.key }, undefined); }, "sessions.patch": async ({ params, respond, context }) => { - if (!validateSessionsPatchParams(params)) { - respond( - false, - undefined, - errorShape( - ErrorCodes.INVALID_REQUEST, - `invalid sessions.patch params: ${formatValidationErrors(validateSessionsPatchParams.errors)}`, - ), - ); + if (!assertValidParams(params, validateSessionsPatchParams, "sessions.patch", respond)) { return; } const p = params; - const key = String(p.key ?? "").trim(); + const key = requireSessionKey(p.key, respond); if (!key) { - respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "key required")); return; } - const cfg = loadConfig(); - const target = resolveGatewaySessionStoreTarget({ cfg, key }); - const storePath = target.storePath; + const { cfg, target, storePath } = resolveGatewaySessionTargetFromKey(key); const applied = await updateSessionStore(storePath, async (store) => { const { primaryKey } = migrateAndPruneSessionStoreKey({ cfg, key, store }); return await applySessionsPatchToStore({ @@ -286,26 +264,16 @@ export const sessionsHandlers: GatewayRequestHandlers = { respond(true, result, undefined); }, "sessions.reset": async ({ params, respond }) => { - if (!validateSessionsResetParams(params)) { - respond( - false, - undefined, - errorShape( - ErrorCodes.INVALID_REQUEST, - `invalid sessions.reset params: ${formatValidationErrors(validateSessionsResetParams.errors)}`, - ), - ); + if (!assertValidParams(params, validateSessionsResetParams, "sessions.reset", respond)) { return; } const p = params; - const key = String(p.key ?? "").trim(); + const key = requireSessionKey(p.key, respond); if (!key) { - respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "key required")); return; } - const cfg = loadConfig(); - const target = resolveGatewaySessionStoreTarget({ cfg, key }); + const { cfg, target, storePath } = resolveGatewaySessionTargetFromKey(key); const { entry } = loadSessionEntry(key); const commandReason = p.reason === "new" ? "new" : "reset"; const hookEvent = createInternalHookEvent( @@ -326,7 +294,6 @@ export const sessionsHandlers: GatewayRequestHandlers = { respond(false, undefined, cleanupError); return; } - const storePath = target.storePath; let oldSessionId: string | undefined; let oldSessionFile: string | undefined; const next = await updateSessionStore(storePath, (store) => { @@ -372,27 +339,17 @@ export const sessionsHandlers: GatewayRequestHandlers = { respond(true, { ok: true, key: target.canonicalKey, entry: next }, undefined); }, "sessions.delete": async ({ params, respond }) => { - if (!validateSessionsDeleteParams(params)) { - respond( - false, - undefined, - errorShape( - ErrorCodes.INVALID_REQUEST, - `invalid sessions.delete params: ${formatValidationErrors(validateSessionsDeleteParams.errors)}`, - ), - ); + if (!assertValidParams(params, validateSessionsDeleteParams, "sessions.delete", respond)) { return; } const p = params; - const key = String(p.key ?? "").trim(); + const key = requireSessionKey(p.key, respond); if (!key) { - respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "key required")); return; } - const cfg = loadConfig(); + const { cfg, target, storePath } = resolveGatewaySessionTargetFromKey(key); const mainKey = resolveMainSessionKey(cfg); - const target = resolveGatewaySessionStoreTarget({ cfg, key }); if (target.canonicalKey === mainKey) { respond( false, @@ -404,7 +361,6 @@ export const sessionsHandlers: GatewayRequestHandlers = { const deleteTranscript = typeof p.deleteTranscript === "boolean" ? p.deleteTranscript : true; - const storePath = target.storePath; const { entry } = loadSessionEntry(key); const sessionId = entry?.sessionId; const existed = Boolean(entry); @@ -433,21 +389,12 @@ export const sessionsHandlers: GatewayRequestHandlers = { respond(true, { ok: true, key: target.canonicalKey, deleted: existed, archived }, undefined); }, "sessions.compact": async ({ params, respond }) => { - if (!validateSessionsCompactParams(params)) { - respond( - false, - undefined, - errorShape( - ErrorCodes.INVALID_REQUEST, - `invalid sessions.compact params: ${formatValidationErrors(validateSessionsCompactParams.errors)}`, - ), - ); + if (!assertValidParams(params, validateSessionsCompactParams, "sessions.compact", respond)) { return; } const p = params; - const key = String(p.key ?? "").trim(); + const key = requireSessionKey(p.key, respond); if (!key) { - respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "key required")); return; } @@ -456,9 +403,7 @@ export const sessionsHandlers: GatewayRequestHandlers = { ? Math.max(1, Math.floor(p.maxLines)) : 400; - const cfg = loadConfig(); - const target = resolveGatewaySessionStoreTarget({ cfg, key }); - const storePath = target.storePath; + const { cfg, target, storePath } = resolveGatewaySessionTargetFromKey(key); // Lock + read in a short critical section; transcript work happens outside. const compactTarget = await updateSessionStore(storePath, (store) => { const { entry, primaryKey } = migrateAndPruneSessionStoreKey({ cfg, key, store }); diff --git a/src/gateway/server-methods/validation.ts b/src/gateway/server-methods/validation.ts new file mode 100644 index 00000000000..90c663d26cd --- /dev/null +++ b/src/gateway/server-methods/validation.ts @@ -0,0 +1,27 @@ +import type { ErrorObject } from "ajv"; +import type { RespondFn } from "./types.js"; +import { ErrorCodes, errorShape, formatValidationErrors } from "../protocol/index.js"; + +export type Validator = ((params: unknown) => params is T) & { + errors?: ErrorObject[] | null; +}; + +export function assertValidParams( + params: unknown, + validate: Validator, + method: string, + respond: RespondFn, +): params is T { + if (validate(params)) { + return true; + } + respond( + false, + undefined, + errorShape( + ErrorCodes.INVALID_REQUEST, + `invalid ${method} params: ${formatValidationErrors(validate.errors)}`, + ), + ); + return false; +} diff --git a/src/gateway/server-methods/wizard.ts b/src/gateway/server-methods/wizard.ts index 1fab55822dd..e98bd3ec44e 100644 --- a/src/gateway/server-methods/wizard.ts +++ b/src/gateway/server-methods/wizard.ts @@ -1,4 +1,3 @@ -import type { ErrorObject } from "ajv"; import { randomUUID } from "node:crypto"; import type { GatewayRequestHandlers, RespondFn } from "./types.js"; import { defaultRuntime } from "../../runtime.js"; @@ -6,37 +5,13 @@ import { WizardSession } from "../../wizard/session.js"; import { ErrorCodes, errorShape, - formatValidationErrors, validateWizardCancelParams, validateWizardNextParams, validateWizardStartParams, validateWizardStatusParams, } from "../protocol/index.js"; import { formatForLog } from "../ws-log.js"; - -type Validator = ((params: unknown) => params is T) & { - errors?: ErrorObject[] | null; -}; - -function assertValidParams( - params: unknown, - validate: Validator, - method: string, - respond: RespondFn, -): params is T { - if (validate(params)) { - return true; - } - respond( - false, - undefined, - errorShape( - ErrorCodes.INVALID_REQUEST, - `invalid ${method} params: ${formatValidationErrors(validate.errors)}`, - ), - ); - return false; -} +import { assertValidParams } from "./validation.js"; function readWizardStatus(session: WizardSession) { return {