C5 follow-up: derive RpcMethod from zod enum (single source of truth)
The C4→C5 drift bug (hotfix 12556de) was caused by the same RPC
method list being declared twice — once as a zod enum in
worker/entry/types.ts for runtime validation, and once as a TS
union type in runtime-bridge.ts for compile-time handler wiring.
The two drifted when C4 added `invokeInternalTool`: the TS union
grew, the handler switch added a case, but the zod enum was
forgotten, so the runtime gate rejected envelopes the handler had
already registered.
This commit collapses the two declarations into one:
- Extract `workerRpcMethodSchema = z.enum([...])` in
worker/entry/types.ts as the canonical list.
- Export `WorkerRpcMethod = z.infer<typeof workerRpcMethodSchema>`.
- Replace the hand-written `RpcMethod` union in runtime-bridge.ts
with `type RpcMethod = WorkerRpcMethod` plus an explicit import.
- Reuse the schema inside the `rpc-request` variant of
`workerMessageSchema` so adding a method requires exactly one
edit instead of three.
- Add an exhaustiveness `default: { const _exhaustive: never = ... }`
guard to `handleRpc`'s switch. If the zod enum widens without a
matching case, TypeScript fails the build at the `never`
assignment — drift becomes a compile-time error, not a runtime
ZodError fired at the user.
typecheck clean, 529 unit tests green, 8/8 eval green.
This commit is contained in:
1 parent
12556deb05
commit
43337e535b
2 files changed
+42
-15
No files matched your search
@@ -68,13 +68,16 @@ export interface RuntimeBridgeDeps {
|
|||||||
credentialResolver: CredentialResolver;
|
credentialResolver: CredentialResolver;
|
||||||
}
|
}
|
||||||
|
|
||||||
type RpcMethod =
|
// Derived from the zod schema in `worker/entry/types.ts` so there is
|
||||||
| "persistMessage"
|
// exactly one source of truth for the RPC method name list. The C4→C5
|
||||||
| "prepareRun"
|
// hotfix (9d9f003) showed what happens when this union and the zod
|
||||||
| "beforeToolCall"
|
// enum drift — worker emits a new method, handler accepts it, and the
|
||||||
| "afterToolCall"
|
// runtime gate silently rejects the envelope. `z.infer` plus a
|
||||||
| "transformContext"
|
// compile-time `satisfies` check below makes that category of drift
|
||||||
| "invokeInternalTool";
|
// a TypeScript error rather than a production ZodError.
|
||||||
|
import type { WorkerRpcMethod } from "@main/worker/entry/types";
|
||||||
|
|
||||||
|
type RpcMethod = WorkerRpcMethod;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Single owner of pi runtime side-effects in main.
|
* Single owner of pi runtime side-effects in main.
|
||||||
@@ -564,6 +567,15 @@ export class RuntimeBridge {
|
|||||||
return this.handleTransformContextRpc(request.runId, request.payload as TransformContextRpcPayload);
|
return this.handleTransformContextRpc(request.runId, request.payload as TransformContextRpcPayload);
|
||||||
case "invokeInternalTool":
|
case "invokeInternalTool":
|
||||||
return this.handleInvokeInternalToolRpc(request.runId, request.payload as InvokeInternalToolRpcPayload);
|
return this.handleInvokeInternalToolRpc(request.runId, request.payload as InvokeInternalToolRpcPayload);
|
||||||
|
default: {
|
||||||
|
// Exhaustiveness guard. If `WorkerRpcMethod` widens in
|
||||||
|
// `worker/entry/types.ts` without a corresponding case here,
|
||||||
|
// the assignment to `never` is a TypeScript error — which is
|
||||||
|
// exactly what we want so the C4→C5 drift category cannot
|
||||||
|
// silently recur at runtime.
|
||||||
|
const _exhaustive: never = request.method;
|
||||||
|
return { ok: false, error: `unknown RPC method: ${String(_exhaustive)}` };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
||||||
|
|||||||
@@ -8,6 +8,28 @@ import type { RuntimeBridge, RuntimeMessageInjector } from "../../agent-runtime"
|
|||||||
|
|
||||||
const runStatusSchema = z.enum(["queued", "running", "succeeded", "failed", "cancelled", "timed_out", "interrupted"]);
|
const runStatusSchema = z.enum(["queued", "running", "succeeded", "failed", "cancelled", "timed_out", "interrupted"]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Single source of truth for worker → main RPC method names.
|
||||||
|
*
|
||||||
|
* This constant is consumed by both the zod validation schema in this
|
||||||
|
* file (the runtime gate at `handleWorkerMessage`) and the `RpcMethod`
|
||||||
|
* TypeScript union exported from `agent-runtime/runtime-bridge.ts` via
|
||||||
|
* `z.infer`. Adding a new internal RPC requires touching exactly one
|
||||||
|
* place — this literal list — which eliminates the C4→C5 hotfix class
|
||||||
|
* of bug where the two definitions silently drifted and the zod gate
|
||||||
|
* rejected a newly-minted method the handler had already registered.
|
||||||
|
*/
|
||||||
|
export const workerRpcMethodSchema = z.enum([
|
||||||
|
"persistMessage",
|
||||||
|
"prepareRun",
|
||||||
|
"beforeToolCall",
|
||||||
|
"afterToolCall",
|
||||||
|
"transformContext",
|
||||||
|
"invokeInternalTool",
|
||||||
|
]);
|
||||||
|
|
||||||
|
export type WorkerRpcMethod = z.infer<typeof workerRpcMethodSchema>;
|
||||||
|
|
||||||
export const workerMessageSchema = z.union([
|
export const workerMessageSchema = z.union([
|
||||||
z.object({
|
z.object({
|
||||||
type: z.literal("run-log"),
|
type: z.literal("run-log"),
|
||||||
@@ -47,14 +69,7 @@ export const workerMessageSchema = z.union([
|
|||||||
type: z.literal("rpc-request"),
|
type: z.literal("rpc-request"),
|
||||||
rpcId: z.string(),
|
rpcId: z.string(),
|
||||||
runId: z.string().uuid(),
|
runId: z.string().uuid(),
|
||||||
method: z.enum([
|
method: workerRpcMethodSchema,
|
||||||
"persistMessage",
|
|
||||||
"prepareRun",
|
|
||||||
"beforeToolCall",
|
|
||||||
"afterToolCall",
|
|
||||||
"transformContext",
|
|
||||||
"invokeInternalTool",
|
|
||||||
]),
|
|
||||||
payload: z.unknown(),
|
payload: z.unknown(),
|
||||||
}),
|
}),
|
||||||
z.object({
|
z.object({
|
||||||
|
|||||||
Reference in new issue
Block a user