refactor(gateway): share server-method param validation

This commit is contained in:
Peter Steinberger
2026-02-16 01:13:52 +00:00
parent a5cbd036de
commit dc5d234848
3 changed files with 60 additions and 113 deletions

View File

@@ -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<typeof loadConfig>;
@@ -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 });

View File

@@ -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<T> = ((params: unknown) => params is T) & {
errors?: ErrorObject[] | null;
};
export function assertValidParams<T>(
params: unknown,
validate: Validator<T>,
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;
}

View File

@@ -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<T> = ((params: unknown) => params is T) & {
errors?: ErrorObject[] | null;
};
function assertValidParams<T>(
params: unknown,
validate: Validator<T>,
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 {