Speed up inbound WebSocket validation (#1895)

* perf(protocol): generate inbound ws validators

* perf(client): use generated ws validation

* docs(protocol): document generated validation

* fix(protocol): make validator generation source-only

* test(protocol): cover explicit provider model normalization

* fix(protocol): preserve inbound compat defaults

* fix(protocol): make validator import rewrite portable

* fix(protocol): harden validation safety checks

* fix(protocol): guard generated validator boundaries

* fix(protocol): normalize legacy inbound defaults

* fix(protocol): simplify generated validation safety net

* fix(protocol): keep cold typecheck source-only

* fix(protocol): encapsulate validator codegen

* fix(protocol): bootstrap source-alias typechecks

* fix(app): keep source aliases out of bundler config

* fix(protocol): harden validator codegen packaging

* fix(protocol): own zod-aot patches in generator

* fix(protocol): trim validator safety net

* fix(client): normalize provider updates before dispatch

* fix(protocol): keep validator generation out of install
This commit is contained in:
Mohamed Boudra
2026-07-06 00:07:54 +08:00
committed by GitHub
parent 0ecf2fb8da
commit 9968f2ba50
25 changed files with 642 additions and 112 deletions
@@ -0,0 +1,11 @@
# Generated protocol validators
`ws-outbound.aot.ts` is generated by zod-aot from `packages/protocol/codegen/ws-outbound.compile.ts`.
Run:
```bash
npm run generate:validators --workspace=@getpaseo/protocol
```
The generated TypeScript is intentionally gitignored. zod-aot is exact-pinned because this is a young solo-maintainer project; keep the generated validator behind protocol regression tests when upgrading it.
+1 -1
View File
@@ -1,5 +1,5 @@
import { z } from "zod";
import { AgentProviderSchema } from "@getpaseo/protocol/provider-manifest";
import { AgentProviderSchema } from "../provider-manifest.js";
export const LoopLogEntrySchema = z.object({
seq: z.number().int().positive(),
@@ -38,41 +38,6 @@ describe("provider snapshot message schemas", () => {
expect(parsed.enabled).toBe(true);
});
test("normalizes thinking option defaults on provider snapshot models", () => {
const parsed = ProviderSnapshotEntrySchema.parse({
provider: "claude",
status: "ready",
models: [
{
provider: "claude",
id: "MiniMax-M2.7",
label: "MiniMax-M2.7",
isDefault: true,
contextWindowMaxTokens: 1_000_000,
thinkingOptions: [
{ id: "off", label: "Off" },
{ id: "max", label: "Max", isDefault: true },
],
},
],
});
expect(parsed.models).toEqual([
{
provider: "claude",
id: "MiniMax-M2.7",
label: "MiniMax-M2.7",
isDefault: true,
contextWindowMaxTokens: 1_000_000,
thinkingOptions: [
{ id: "off", label: "Off" },
{ id: "max", label: "Max", isDefault: true },
],
defaultThinkingOptionId: "max",
},
]);
});
test("defaults missing enabled state in providers snapshot response entries", () => {
const parsed = GetProvidersSnapshotResponseMessageSchema.parse({
type: "get_providers_snapshot_response",
+47 -41
View File
@@ -2,9 +2,9 @@ import { z } from "zod";
import { TerminalActivitySchema } from "./terminal-activity.js";
import { CLIENT_CAPS } from "./client-capabilities.js";
import { AGENT_LIFECYCLE_STATUSES } from "./agent-lifecycle.js";
import { MAX_EXPLICIT_AGENT_TITLE_CHARS } from "@getpaseo/protocol/agent-title-limits";
import { AgentProviderSchema } from "@getpaseo/protocol/provider-manifest";
import { normalizeAgentModelDefinition, TOOL_CALL_ICON_NAMES } from "./agent-types.js";
import { MAX_EXPLICIT_AGENT_TITLE_CHARS } from "./agent-title-limits.js";
import { AgentProviderSchema } from "./provider-manifest.js";
import { TOOL_CALL_ICON_NAMES } from "./agent-types.js";
import {
ChatCreateRequestSchema,
ChatListRequestSchema,
@@ -40,7 +40,7 @@ import {
ScheduleDeleteResponseSchema,
ScheduleRunOnceResponseSchema,
ScheduleUpdateResponseSchema,
} from "@getpaseo/protocol/schedule/rpc-schemas";
} from "./schedule/rpc-schemas.js";
import {
LoopRunRequestSchema,
LoopListRequestSchema,
@@ -52,7 +52,7 @@ import {
LoopInspectResponseSchema,
LoopLogsResponseSchema,
LoopStopResponseSchema,
} from "@getpaseo/protocol/loop/rpc-schemas";
} from "./loop/rpc-schemas.js";
import {
BrowserAutomationExecuteRequestSchema,
BrowserAutomationExecuteResponseSchema,
@@ -73,7 +73,7 @@ import {
type PaseoMetadataGenerationEntry,
type PaseoScriptEntryRaw,
type ProjectConfigRpcError,
} from "@getpaseo/protocol/paseo-config-schema";
} from "./paseo-config-schema.js";
export {
PaseoConfigRawSchema,
PaseoLifecycleCommandRawSchema,
@@ -247,19 +247,17 @@ export const AgentFeatureSchema = z.discriminatedUnion("type", [
AgentFeatureSelectSchema,
]);
const AgentModelDefinitionSchema: z.ZodType<AgentModelDefinition> = z
.object({
provider: AgentProviderSchema,
id: z.string(),
label: z.string(),
description: z.string().optional(),
isDefault: z.boolean().optional(),
metadata: z.record(z.string(), z.unknown()).optional(),
contextWindowMaxTokens: z.number().optional(),
thinkingOptions: z.array(AgentSelectOptionSchema).optional(),
defaultThinkingOptionId: z.string().optional(),
})
.transform(normalizeAgentModelDefinition);
const AgentModelDefinitionSchema: z.ZodType<AgentModelDefinition> = z.object({
provider: AgentProviderSchema,
id: z.string(),
label: z.string(),
description: z.string().optional(),
isDefault: z.boolean().optional(),
metadata: z.record(z.string(), z.unknown()).optional(),
contextWindowMaxTokens: z.number().optional(),
thinkingOptions: z.array(AgentSelectOptionSchema).optional(),
defaultThinkingOptionId: z.string().optional(),
});
export const ProviderSnapshotEntrySchema = z.object({
provider: AgentProviderSchema,
@@ -361,20 +359,21 @@ const AgentPermissionActionSchema = z.object({
intent: z.enum(["implement", "implement_resume", "dismiss"]).optional(),
});
export const AgentPermissionResponseSchema: z.ZodType<AgentPermissionResponse> = z.union([
z.object({
behavior: z.literal("allow"),
selectedActionId: z.string().optional(),
updatedInput: z.record(z.string(), z.unknown()).optional(),
updatedPermissions: z.array(AgentPermissionUpdateSchema).optional(),
}),
z.object({
behavior: z.literal("deny"),
selectedActionId: z.string().optional(),
message: z.string().optional(),
interrupt: z.boolean().optional(),
}),
]);
export const AgentPermissionResponseSchema: z.ZodType<AgentPermissionResponse> =
z.discriminatedUnion("behavior", [
z.object({
behavior: z.literal("allow"),
selectedActionId: z.string().optional(),
updatedInput: z.record(z.string(), z.unknown()).optional(),
updatedPermissions: z.array(AgentPermissionUpdateSchema).optional(),
}),
z.object({
behavior: z.literal("deny"),
selectedActionId: z.string().optional(),
message: z.string().optional(),
interrupt: z.boolean().optional(),
}),
]);
export const AgentPermissionRequestPayloadSchema: z.ZodType<AgentPermissionRequest, unknown> =
z.object({
@@ -554,13 +553,16 @@ const ToolCallCanceledPayloadSchema = ToolCallBasePayloadSchema.extend({
error: z.null(),
});
const ToolCallTimelineItemPayloadSchema: z.ZodType<ToolCallTimelineItem, unknown> = z.union([
ToolCallRunningPayloadSchema,
ToolCallCompletedPayloadSchema,
ToolCallFailedPayloadSchema,
ToolCallCanceledPayloadSchema,
]);
const ToolCallTimelineItemPayloadSchema: z.ZodType<ToolCallTimelineItem, unknown> =
z.discriminatedUnion("status", [
ToolCallRunningPayloadSchema,
ToolCallCompletedPayloadSchema,
ToolCallFailedPayloadSchema,
ToolCallCanceledPayloadSchema,
]);
// zod-aot 0.20.4 miscompiles this as a nested discriminated union by omitting
// the inner tool_call branch from the generated outer dispatch.
export const AgentTimelineItemPayloadSchema: z.ZodType<AgentTimelineItem, unknown> = z.union([
z.object({
type: z.literal("user_message"),
@@ -3113,7 +3115,9 @@ export const SetDaemonConfigResponseMessageSchema = z.object({
export const ReadProjectConfigResponseMessageSchema = z.object({
type: z.literal("read_project_config_response"),
payload: z.discriminatedUnion("ok", [
// zod-aot 0.2.0 miscompiles boolean discriminators as string options
// (`"true"`/`"false"`), so keep this sequential until upstream fixes it.
payload: z.union([
z.object({
requestId: z.string(),
repoRoot: z.string(),
@@ -3132,7 +3136,9 @@ export const ReadProjectConfigResponseMessageSchema = z.object({
export const WriteProjectConfigResponseMessageSchema = z.object({
type: z.literal("write_project_config_response"),
payload: z.discriminatedUnion("ok", [
// zod-aot 0.2.0 miscompiles boolean discriminators as string options
// (`"true"`/`"false"`), so keep this sequential until upstream fixes it.
payload: z.union([
z.object({
requestId: z.string(),
repoRoot: z.string(),
+1 -1
View File
@@ -1,5 +1,5 @@
import { z } from "zod";
import { AgentProviderSchema } from "@getpaseo/protocol/provider-manifest";
import { AgentProviderSchema } from "../provider-manifest.js";
export const ScheduleStatusSchema = z.enum(["active", "paused", "completed"]);
export type ScheduleStatus = z.infer<typeof ScheduleStatusSchema>;
+1 -1
View File
@@ -1,5 +1,5 @@
import type { ToolCallTimelineItem } from "./agent-types.js";
import { getPaseoToolLeafName, isPaseoToolName } from "@getpaseo/protocol/tool-name-normalization";
import { getPaseoToolLeafName, isPaseoToolName } from "./tool-name-normalization.js";
import { stripCwdPrefix } from "./path-utils.js";
export type ToolCallDisplayInput = Pick<
@@ -0,0 +1,3 @@
import { WSOutboundMessageSchema as SourceWSOutboundMessageSchema } from "../messages.js";
export const WSOutboundMessageSchema = { schema: SourceWSOutboundMessageSchema };
@@ -0,0 +1,19 @@
import type { z } from "zod";
import { WSOutboundMessageSchema } from "../generated/validation/ws-outbound.aot.js";
import type { WSOutboundMessage } from "../messages.js";
type WSOutboundValidationResult =
| { success: true; data: WSOutboundMessage }
| { success: false; error: z.ZodError };
interface WSOutboundGeneratedValidator {
safeParse(input: unknown): WSOutboundValidationResult;
}
// zod-aot emits runtime JavaScript from WSOutboundMessageSchema but not its TypeScript surface.
// Protocol regression tests cover the patched compiler cases behind this boundary.
const wsOutboundValidator = WSOutboundMessageSchema as WSOutboundGeneratedValidator;
export function validateWSOutboundMessage(input: unknown): WSOutboundValidationResult {
return wsOutboundValidator.safeParse(input);
}