all repos — sz-chrome-remote-cdp @ ae3a6dcdbe584a9131a514ea3d738abaec6460f3

pi-browser-harness, patched to drive a Chrome on another host over CDP, with castle_cdp_search web search

feat(cdp): add typed command table with validated decoders

One typebox schema per CDP command is the source of both the TS types and
the runtime validator, so they cannot drift. Malformed responses become
invalid_response errors instead of TypeErrors several frames later.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
amankumarsingh77 amankumarsingh7702@gmail.com
Mon, 27 Jul 2026 00:35:38 +0530
commit

ae3a6dcdbe584a9131a514ea3d738abaec6460f3

parent

15a537a99efba614e6e108722462bcb78ec6c3c4

2 files changed, 262 insertions(+), 0 deletions(-)

jump to
A src/cdp/commands.ts

@@ -0,0 +1,217 @@

+import { Type, type Static, type TSchema } from "typebox"; +import { Compile } from "typebox/compile"; +import { type Result, err, ok } from "../util/result"; +import { type CdpError, cdpError } from "./errors"; + +const cmd = <P extends TSchema, R extends TSchema>(params: P, result: R) => ({ + params, + result, + validate: Compile(result), +}); + +const Empty = Type.Object({}); + +const TargetInfo = Type.Object( + { + targetId: Type.String(), + type: Type.String(), + title: Type.String(), + url: Type.String(), + attached: Type.Optional(Type.Boolean()), + openerId: Type.Optional(Type.String()), + browserContextId: Type.Optional(Type.String()), + }, + { additionalProperties: true }, +); + +const RemoteObject = Type.Object( + { + type: Type.String(), + subtype: Type.Optional(Type.String()), + className: Type.Optional(Type.String()), + value: Type.Optional(Type.Unknown()), + description: Type.Optional(Type.String()), + objectId: Type.Optional(Type.String()), + }, + { additionalProperties: true }, +); + +const ExceptionDetails = Type.Object( + { text: Type.String(), exception: Type.Optional(RemoteObject) }, + { additionalProperties: true }, +); + +export const COMMANDS = { + // --- Target --- + "Target.attachToTarget": cmd( + Type.Object({ targetId: Type.String(), flatten: Type.Optional(Type.Boolean()) }), + Type.Object({ sessionId: Type.String() }), + ), + "Target.detachFromTarget": cmd( + Type.Object({ sessionId: Type.Optional(Type.String()) }), + Empty, + ), + "Target.createTarget": cmd( + Type.Object({ + url: Type.String(), + newWindow: Type.Optional(Type.Boolean()), + background: Type.Optional(Type.Boolean()), + windowId: Type.Optional(Type.Number()), + }), + Type.Object({ targetId: Type.String() }), + ), + "Target.closeTarget": cmd( + Type.Object({ targetId: Type.String() }), + Type.Object({ success: Type.Optional(Type.Boolean()) }), + ), + "Target.activateTarget": cmd(Type.Object({ targetId: Type.String() }), Empty), + "Target.getTargetInfo": cmd( + Type.Object({ targetId: Type.Optional(Type.String()) }), + Type.Object({ targetInfo: TargetInfo }), + ), + "Target.getTargets": cmd( + Type.Object({}), + Type.Object({ targetInfos: Type.Array(TargetInfo) }), + ), + "Target.setDiscoverTargets": cmd(Type.Object({ discover: Type.Boolean() }), Empty), + + // --- Browser --- + "Browser.getWindowForTarget": cmd( + Type.Object({ targetId: Type.Optional(Type.String()) }), + Type.Object({ windowId: Type.Number(), bounds: Type.Optional(Type.Unknown()) }), + ), + "Browser.setDownloadBehavior": cmd( + Type.Object({ + behavior: Type.String(), + downloadPath: Type.Optional(Type.String()), + eventsEnabled: Type.Optional(Type.Boolean()), + }), + Empty, + ), + + // --- Page --- + "Page.enable": cmd(Type.Object({}), Empty), + "Page.bringToFront": cmd(Type.Object({}), Empty), + "Page.navigate": cmd( + Type.Object({ url: Type.String(), referrer: Type.Optional(Type.String()) }), + Type.Object({ + frameId: Type.String(), + loaderId: Type.Optional(Type.String()), + errorText: Type.Optional(Type.String()), + }), + ), + "Page.reload": cmd(Type.Object({ ignoreCache: Type.Optional(Type.Boolean()) }), Empty), + "Page.getNavigationHistory": cmd( + Type.Object({}), + Type.Object({ + currentIndex: Type.Number(), + entries: Type.Array( + Type.Object({ id: Type.Number(), url: Type.String(), title: Type.String() }), + ), + }), + ), + "Page.navigateToHistoryEntry": cmd(Type.Object({ entryId: Type.Number() }), Empty), + "Page.captureScreenshot": cmd( + Type.Object({ + format: Type.Optional(Type.String()), + quality: Type.Optional(Type.Number()), + clip: Type.Optional(Type.Unknown()), + captureBeyondViewport: Type.Optional(Type.Boolean()), + }), + Type.Object({ data: Type.String() }), + ), + "Page.printToPDF": cmd(Type.Object({}, { additionalProperties: true }), Type.Object({ data: Type.String() })), + "Page.handleJavaScriptDialog": cmd( + Type.Object({ accept: Type.Boolean(), promptText: Type.Optional(Type.String()) }), + Empty, + ), + + // --- Runtime --- + "Runtime.evaluate": cmd( + Type.Object( + { expression: Type.String(), returnByValue: Type.Optional(Type.Boolean()), awaitPromise: Type.Optional(Type.Boolean()) }, + { additionalProperties: true }, + ), + Type.Object({ result: RemoteObject, exceptionDetails: Type.Optional(ExceptionDetails) }), + ), + "Runtime.callFunctionOn": cmd( + Type.Object( + { functionDeclaration: Type.String(), objectId: Type.Optional(Type.String()) }, + { additionalProperties: true }, + ), + Type.Object({ result: RemoteObject, exceptionDetails: Type.Optional(ExceptionDetails) }), + ), + "Runtime.releaseObject": cmd(Type.Object({ objectId: Type.String() }), Empty), + + // --- DOM --- + "DOM.getDocument": cmd( + Type.Object({ depth: Type.Optional(Type.Number()), pierce: Type.Optional(Type.Boolean()) }), + Type.Object({ root: Type.Object({ nodeId: Type.Number() }, { additionalProperties: true }) }), + ), + "DOM.querySelector": cmd( + Type.Object({ nodeId: Type.Number(), selector: Type.String() }), + Type.Object({ nodeId: Type.Number() }), + ), + "DOM.getBoxModel": cmd( + Type.Object({ nodeId: Type.Optional(Type.Number()), backendNodeId: Type.Optional(Type.Number()) }), + Type.Object({ + model: Type.Object( + { content: Type.Array(Type.Number()), width: Type.Number(), height: Type.Number() }, + { additionalProperties: true }, + ), + }), + ), + "DOM.resolveNode": cmd( + Type.Object({ nodeId: Type.Optional(Type.Number()), backendNodeId: Type.Optional(Type.Number()) }), + Type.Object({ object: RemoteObject }), + ), + "DOM.setFileInputFiles": cmd( + Type.Object({ + files: Type.Array(Type.String()), + nodeId: Type.Optional(Type.Number()), + backendNodeId: Type.Optional(Type.Number()), + objectId: Type.Optional(Type.String()), + }), + Empty, + ), + + // --- Input --- + "Input.dispatchMouseEvent": cmd(Type.Object({}, { additionalProperties: true }), Empty), + "Input.dispatchKeyEvent": cmd(Type.Object({}, { additionalProperties: true }), Empty), + "Input.dispatchDragEvent": cmd(Type.Object({}, { additionalProperties: true }), Empty), + "Input.insertText": cmd(Type.Object({ text: Type.String() }), Empty), + + // --- Network / Emulation / Accessibility --- + "Network.getResponseBody": cmd( + Type.Object({ requestId: Type.String() }), + Type.Object({ body: Type.String(), base64Encoded: Type.Optional(Type.Boolean()) }), + ), + "Emulation.setDeviceMetricsOverride": cmd(Type.Object({}, { additionalProperties: true }), Empty), + "Accessibility.getFullAXTree": cmd( + Type.Object({ depth: Type.Optional(Type.Number()) }, { additionalProperties: true }), + Type.Object({ nodes: Type.Array(Type.Unknown()) }), + ), +}; + +export type CdpMethod = keyof typeof COMMANDS; +export type ParamsOf<M extends CdpMethod> = Static<(typeof COMMANDS)[M]["params"]>; +export type ResultOf<M extends CdpMethod> = Static<(typeof COMMANDS)[M]["result"]>; + +export const decodeResult = <M extends CdpMethod>( + method: M, + raw: unknown, +): Result<ResultOf<M>, CdpError> => { + const spec = COMMANDS[method]; + if (spec === undefined) { + return err(cdpError("invalid_response", `unknown CDP method`, method)); + } + if (!spec.validate.Check(raw)) { + const why = spec.validate.Errors(raw).map((e) => e.message).join("; "); + return err(cdpError("invalid_response", why, method)); + } + // Guarded assertion — see Verified Fact 4. The Check above proves `raw` + // matches COMMANDS[method].result; TS cannot correlate a generic key to its + // mapped value, so this carries what the check established. The only + // assertion in the CDP layer; do not add another. + return ok(raw as ResultOf<M>); +};
A test/cdp/commands-test.ts

@@ -0,0 +1,45 @@

+import { test, describe } from "node:test"; +import assert from "node:assert/strict"; +import { COMMANDS, decodeResult } from "../../src/cdp/commands"; + +describe("cdp command table", () => { + test("decodes a well-formed response", () => { + const r = decodeResult("Target.attachToTarget", { sessionId: "s1" }); + assert.equal(r.success, true); + if (r.success) assert.equal(r.data.sessionId, "s1"); + }); + + test("rejects a response with a wrong field type", () => { + const r = decodeResult("Browser.getWindowForTarget", { windowId: "not-a-number" }); + assert.equal(r.success, false); + if (!r.success) { + assert.equal(r.error.kind, "invalid_response"); + assert.equal(r.error.method, "Browser.getWindowForTarget"); + assert.match(r.error.message, /must be number/); + } + }); + + test("rejects a response missing a required field", () => { + const r = decodeResult("Target.attachToTarget", {}); + assert.equal(r.success, false); + }); + + test("rejects a non-object response", () => { + const r = decodeResult("Target.attachToTarget", null); + assert.equal(r.success, false); + }); + + test("every table entry has both params and result schemas", () => { + for (const [method, spec] of Object.entries(COMMANDS)) { + assert.ok(spec.params, `${method} missing params schema`); + assert.ok(spec.result, `${method} missing result schema`); + assert.ok(spec.validate, `${method} missing compiled validator`); + } + }); + + test("an unknown method decodes to an error rather than throwing", () => { + // Types prevent this, but a daemon could echo an unexpected method name. + const r = decodeResult("Nope.method" as "Page.enable", {}); + assert.equal(r.success, false); + }); +});