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:
@@ -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,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",
|
||||
|
||||
@@ -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,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,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);
|
||||
}
|
||||
Reference in New Issue
Block a user