@smthrs/gateway 0.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2426 @@
1
+ // @smithers-type-exports-begin
2
+ /** @typedef {import("./gatewayRpcTypes.ts").SmithersApiVersion} SmithersApiVersion */
3
+ /** @typedef {import("./gatewayRpcTypes.ts").JsonSchema} JsonSchema */
4
+ /** @typedef {import("./gatewayRpcTypes.ts").GatewayRpcErrorCode} GatewayRpcErrorCode */
5
+ /** @typedef {import("./gatewayRpcTypes.ts").GatewayRpcErrorDetails} GatewayRpcErrorDetails */
6
+ /** @typedef {import("./gatewayRpcTypes.ts").GatewayRpcErrorDefinition} GatewayRpcErrorDefinition */
7
+ /** @typedef {import("./gatewayRpcTypes.ts").GatewayRpcDefinition} GatewayRpcDefinition */
8
+ /** @typedef {import("./gatewayRpcTypes.ts").GatewayRpcMethod} GatewayRpcMethod */
9
+ /** @typedef {import("./gatewayRpcTypes.ts").GatewayResponseFrame} GatewayResponseFrame */
10
+ /** @typedef {import("./gatewayRpcTypes.ts").LaunchRunRequest} LaunchRunRequest */
11
+ /** @typedef {import("./gatewayRpcTypes.ts").LaunchRunResponse} LaunchRunResponse */
12
+ /** @typedef {import("./gatewayRpcTypes.ts").ResumeRunRequest} ResumeRunRequest */
13
+ /** @typedef {import("./gatewayRpcTypes.ts").ResumeRunResponse} ResumeRunResponse */
14
+ /** @typedef {import("./gatewayRpcTypes.ts").CancelRunRequest} CancelRunRequest */
15
+ /** @typedef {import("./gatewayRpcTypes.ts").CancelRunResponse} CancelRunResponse */
16
+ /** @typedef {import("./gatewayRpcTypes.ts").PauseRunRequest} PauseRunRequest */
17
+ /** @typedef {import("./gatewayRpcTypes.ts").PauseRunResponse} PauseRunResponse */
18
+ /** @typedef {import("./gatewayRpcTypes.ts").HijackRunRequest} HijackRunRequest */
19
+ /** @typedef {import("./gatewayRpcTypes.ts").HijackRunResponse} HijackRunResponse */
20
+ /** @typedef {import("./gatewayRpcTypes.ts").RewindRunRequest} RewindRunRequest */
21
+ /** @typedef {import("./gatewayRpcTypes.ts").RewindRunResponse} RewindRunResponse */
22
+ /** @typedef {import("./gatewayRpcTypes.ts").CrossedEffect} CrossedEffect */
23
+ /** @typedef {import("./gatewayRpcTypes.ts").EffectBoundaryReport} EffectBoundaryReport */
24
+ /** @typedef {import("./gatewayRpcTypes.ts").EffectRevertStarted} EffectRevertStarted */
25
+ /** @typedef {import("./gatewayRpcTypes.ts").EffectRevertFinished} EffectRevertFinished */
26
+ /** @typedef {import("./gatewayRpcTypes.ts").EffectRevertFailed} EffectRevertFailed */
27
+ /** @typedef {import("./gatewayRpcTypes.ts").SideEffectBoundaryCrossed} SideEffectBoundaryCrossed */
28
+ /** @typedef {import("./gatewayRpcTypes.ts").SubmitApprovalRequest} SubmitApprovalRequest */
29
+ /** @typedef {import("./gatewayRpcTypes.ts").SubmitApprovalResponse} SubmitApprovalResponse */
30
+ /** @typedef {import("./gatewayRpcTypes.ts").SubmitSignalRequest} SubmitSignalRequest */
31
+ /** @typedef {import("./gatewayRpcTypes.ts").GetRunRequest} GetRunRequest */
32
+ /** @typedef {import("./gatewayRpcTypes.ts").RunTokenUsageEvent} RunTokenUsageEvent */
33
+ /** @typedef {import("./gatewayRpcTypes.ts").ListRunTokenUsageRequest} ListRunTokenUsageRequest */
34
+ /** @typedef {import("./gatewayRpcTypes.ts").ListRunTokenUsageResponse} ListRunTokenUsageResponse */
35
+ /** @typedef {import("./gatewayRpcTypes.ts").GetRunDiffRequest} GetRunDiffRequest */
36
+ /** @typedef {import("./gatewayRpcTypes.ts").GetRunDiffResponse} GetRunDiffResponse */
37
+ /** @typedef {import("./gatewayRpcTypes.ts").GetRunDiffOversizedResponse} GetRunDiffOversizedResponse */
38
+ /** @typedef {import("./gatewayRpcTypes.ts").GatewayDiffBundle} GatewayDiffBundle */
39
+ /** @typedef {import("./gatewayRpcTypes.ts").GatewayDiffPatch} GatewayDiffPatch */
40
+ /** @typedef {import("./gatewayRpcTypes.ts").ListRunsRequest} ListRunsRequest */
41
+ /** @typedef {import("./gatewayRpcTypes.ts").GatewayRunSummary} GatewayRunSummary */
42
+ /** @typedef {import("./gatewayRpcTypes.ts").ListRunsResponse} ListRunsResponse */
43
+ /** @typedef {import("./gatewayRpcTypes.ts").ListRunDescendantsRequest} ListRunDescendantsRequest */
44
+ /** @typedef {import("./gatewayRpcTypes.ts").GatewayRunDescendant} GatewayRunDescendant */
45
+ /** @typedef {import("./gatewayRpcTypes.ts").ListRunDescendantsResponse} ListRunDescendantsResponse */
46
+ /** @typedef {import("./gatewayRpcTypes.ts").GetSchemaSignatureRequest} GetSchemaSignatureRequest */
47
+ /** @typedef {import("./gatewayRpcTypes.ts").GetSchemaSignatureResponse} GetSchemaSignatureResponse */
48
+ /** @typedef {import("./gatewayRpcTypes.ts").GatewayWorkflowSummary} GatewayWorkflowSummary */
49
+ /** @typedef {import("./gatewayRpcTypes.ts").ListWorkflowsRequest} ListWorkflowsRequest */
50
+ /** @typedef {import("./gatewayRpcTypes.ts").ListWorkflowsResponse} ListWorkflowsResponse */
51
+ /** @typedef {import("./gatewayRpcTypes.ts").GatewayApprovalSummary} GatewayApprovalSummary */
52
+ /** @typedef {import("./gatewayRpcTypes.ts").ListApprovalsRequest} ListApprovalsRequest */
53
+ /** @typedef {import("./gatewayRpcTypes.ts").ListApprovalsResponse} ListApprovalsResponse */
54
+ /** @typedef {import("./gatewayRpcTypes.ts").GatewayDocRow} GatewayDocRow */
55
+ /** @typedef {import("./gatewayRpcTypes.ts").ListDocsRequest} ListDocsRequest */
56
+ /** @typedef {import("./gatewayRpcTypes.ts").ListDocsResponse} ListDocsResponse */
57
+ /** @typedef {import("./gatewayRpcTypes.ts").StreamRunEventsRequest} StreamRunEventsRequest */
58
+ /** @typedef {import("./gatewayRpcTypes.ts").StreamRunEventsResponse} StreamRunEventsResponse */
59
+ /** @typedef {import("./gatewayRpcTypes.ts").StreamDevToolsRequest} StreamDevToolsRequest */
60
+ /** @typedef {import("./gatewayRpcTypes.ts").GetDevToolsSnapshotRequest} GetDevToolsSnapshotRequest */
61
+ /** @typedef {import("./gatewayRpcTypes.ts").GetDevToolsSnapshotResponse} GetDevToolsSnapshotResponse */
62
+ /** @typedef {import("./gatewayRpcTypes.ts").NodeRequest} NodeRequest */
63
+ /** @typedef {import("./gatewayRpcTypes.ts").WhatHappenedRequest} WhatHappenedRequest */
64
+ /** @typedef {import("./gatewayRpcTypes.ts").WhatHappenedResponse} WhatHappenedResponse */
65
+ /** @typedef {import("./gatewayRpcTypes.ts").CronListRequest} CronListRequest */
66
+ /** @typedef {import("./gatewayRpcTypes.ts").CronCreateRequest} CronCreateRequest */
67
+ /** @typedef {import("./gatewayRpcTypes.ts").CronDeleteRequest} CronDeleteRequest */
68
+ /** @typedef {import("./gatewayRpcTypes.ts").CronRunRequest} CronRunRequest */
69
+ /** @typedef {import("./gatewayRpcTypes.ts").GatewayAccount} GatewayAccount */
70
+ /** @typedef {import("./gatewayRpcTypes.ts").ListAccountsRequest} ListAccountsRequest */
71
+ /** @typedef {import("./gatewayRpcTypes.ts").ListAccountsResponse} ListAccountsResponse */
72
+ /** @typedef {import("./gatewayRpcTypes.ts").GatewayMemoryFact} GatewayMemoryFact */
73
+ /** @typedef {import("./gatewayRpcTypes.ts").ListMemoryFactsRequest} ListMemoryFactsRequest */
74
+ /** @typedef {import("./gatewayRpcTypes.ts").ListMemoryFactsResponse} ListMemoryFactsResponse */
75
+ /** @typedef {import("./gatewayRpcTypes.ts").GatewayPrompt} GatewayPrompt */
76
+ /** @typedef {import("./gatewayRpcTypes.ts").ListPromptsRequest} ListPromptsRequest */
77
+ /** @typedef {import("./gatewayRpcTypes.ts").ListPromptsResponse} ListPromptsResponse */
78
+ /** @typedef {import("./gatewayRpcTypes.ts").GatewayScoreRow} GatewayScoreRow */
79
+ /** @typedef {import("./gatewayRpcTypes.ts").ListScoresRequest} ListScoresRequest */
80
+ /** @typedef {import("./gatewayRpcTypes.ts").ListScoresResponse} ListScoresResponse */
81
+ /** @typedef {import("./gatewayRpcTypes.ts").GatewayComparisonScoreRow} GatewayComparisonScoreRow */
82
+ /** @typedef {import("./gatewayRpcTypes.ts").ListScoresForRunsRequest} ListScoresForRunsRequest */
83
+ /** @typedef {import("./gatewayRpcTypes.ts").ListScoresForRunsResponse} ListScoresForRunsResponse */
84
+ /** @typedef {import("./gatewayRpcTypes.ts").GetScoreDetailRequest} GetScoreDetailRequest */
85
+ /** @typedef {import("./gatewayRpcTypes.ts").GatewayScoreDetail} GatewayScoreDetail */
86
+ /** @typedef {import("./gatewayRpcTypes.ts").GetScoreDetailResponse} GetScoreDetailResponse */
87
+ /** @typedef {import("./gatewayRpcTypes.ts").GatewayDocKind} GatewayDocKind */
88
+ /** @typedef {import("./gatewayRpcTypes.ts").GatewayTicketRow} GatewayTicketRow */
89
+ /** @typedef {import("./gatewayRpcTypes.ts").ListTicketsRequest} ListTicketsRequest */
90
+ /** @typedef {import("./gatewayRpcTypes.ts").ListTicketsResponse} ListTicketsResponse */
91
+ /** @typedef {import("./gatewayRpcTypes.ts").CreateTicketRequest} CreateTicketRequest */
92
+ /** @typedef {import("./gatewayRpcTypes.ts").UpdateTicketRequest} UpdateTicketRequest */
93
+ /** @typedef {import("./gatewayRpcTypes.ts").DeleteTicketRequest} DeleteTicketRequest */
94
+ /** @typedef {import("./gatewayRpcTypes.ts").BrowserSource} BrowserSource */
95
+ /** @typedef {import("./gatewayRpcTypes.ts").BrowserLocator} BrowserLocator */
96
+ /** @typedef {import("./gatewayRpcTypes.ts").BrowserSnapshot} BrowserSnapshot */
97
+ /** @typedef {import("./gatewayRpcTypes.ts").BrowserAction} BrowserAction */
98
+ /** @typedef {import("./gatewayRpcTypes.ts").CreateBrowserSessionRequest} CreateBrowserSessionRequest */
99
+ /** @typedef {import("./gatewayRpcTypes.ts").BrowserActRequest} BrowserActRequest */
100
+ /** @typedef {import("./gatewayRpcTypes.ts").BrowserContextRequest} BrowserContextRequest */
101
+ /** @typedef {import("./gatewayRpcTypes.ts").BrowserPickRequest} BrowserPickRequest */
102
+ /** @typedef {import("./gatewayRpcTypes.ts").CloseBrowserSessionRequest} CloseBrowserSessionRequest */
103
+ /** @typedef {import("./gatewayRpcTypes.ts").CreateBrowserSessionResponse} CreateBrowserSessionResponse */
104
+ /** @typedef {import("./gatewayRpcTypes.ts").BrowserActResponse} BrowserActResponse */
105
+ /** @typedef {import("./gatewayRpcTypes.ts").BrowserContextResponse} BrowserContextResponse */
106
+ /** @typedef {import("./gatewayRpcTypes.ts").BrowserPickResponse} BrowserPickResponse */
107
+ /** @typedef {import("./gatewayRpcTypes.ts").CloseBrowserSessionResponse} CloseBrowserSessionResponse */
108
+ /** @typedef {import("./gatewayRpcTypes.ts").ListBrowserSessionsResponse} ListBrowserSessionsResponse */
109
+ /** @typedef {import("./gatewayRpcTypes.ts").BrowserActor} BrowserActor */
110
+ /** @typedef {import("./gatewayRpcTypes.ts").BrowserViewport} BrowserViewport */
111
+ /** @typedef {import("./gatewayRpcTypes.ts").BrowserPoint} BrowserPoint */
112
+ /** @typedef {import("./gatewayRpcTypes.ts").BrowserRectangle} BrowserRectangle */
113
+ /** @typedef {import("./gatewayRpcTypes.ts").BrowserModifier} BrowserModifier */
114
+ /** @typedef {import("./gatewayRpcTypes.ts").BrowserRedaction} BrowserRedaction */
115
+ /** @typedef {import("./gatewayRpcTypes.ts").BrowserOutcome} BrowserOutcome */
116
+ /** @typedef {import("./gatewayRpcTypes.ts").BrowserJournalEntry} BrowserJournalEntry */
117
+ /** @typedef {import("./gatewayRpcTypes.ts").BrowserContextSlice} BrowserContextSlice */
118
+ /** @typedef {import("./gatewayRpcTypes.ts").BrowserSummary} BrowserSummary */
119
+ /** @typedef {import("./gatewayRpcTypes.ts").BrowserClickAction} BrowserClickAction */
120
+ /** @typedef {import("./gatewayRpcTypes.ts").BrowserFrameEvent} BrowserFrameEvent */
121
+ /** @typedef {import("./gatewayRpcTypes.ts").BrowserActivityEvent} BrowserActivityEvent */
122
+ /** @typedef {import("./gatewayRpcTypes.ts").BrowserScreenshot} BrowserScreenshot */
123
+ /** @typedef {import("./gatewayRpcTypes.ts").BrowserSelection} BrowserSelection */
124
+ // @smithers-type-exports-end
125
+ /** @typedef {import("../auth/scopes.js").GatewayScope} GatewayScope */
126
+
127
+ import { GATEWAY_SCOPE_VALUES } from "../auth/scopes.js";
128
+
129
+ export const SMITHERS_API_VERSION = /** @type {SmithersApiVersion} */ ("v1");
130
+ // Default `eventWindowSize` for the server: how many per-run events are retained
131
+ // for streamRunEvents bounded replay. A subscriber reconnecting from further back
132
+ // than this window gets GapResync semantics instead of a replay.
133
+ export const GATEWAY_EVENT_WINDOW_DEFAULT = 10_000;
134
+
135
+ // The fixed `smithers agents` provider catalog. The GatewayAccount["provider"]
136
+ // union and the listAccounts schema enum both derive from this so they cannot drift.
137
+ /** @type {readonly GatewayAccount["provider"][]} */
138
+ const ACCOUNT_PROVIDERS = [
139
+ "claude-code",
140
+ "antigravity",
141
+ "codex",
142
+ "gemini",
143
+ "kimi",
144
+ "anthropic-api",
145
+ "openai-api",
146
+ "gemini-api",
147
+ ];
148
+
149
+ // The closed set of work-doc kinds in `_smithers_docs`. The GatewayDocKind
150
+ // union and every ticket-RPC schema enum derive from this so they cannot drift.
151
+ /** @type {readonly GatewayDocKind[]} */
152
+ const DOC_KINDS = ["ticket", "plan", "spec", "proposal"];
153
+
154
+ /**
155
+ * @param {string} description
156
+ * @returns {JsonSchema}
157
+ */
158
+ const stringSchema = (description, minLength) => ({ type: "string", ...(minLength ? { minLength } : {}), description });
159
+ /**
160
+ * @param {string} description
161
+ * @returns {JsonSchema}
162
+ */
163
+ const booleanSchema = (description) => ({ type: "boolean", description });
164
+ /**
165
+ * @param {string} description
166
+ * @param {number} [minimum]
167
+ * @returns {JsonSchema}
168
+ */
169
+ const integerSchema = (description, minimum = 0) => ({
170
+ type: "integer",
171
+ minimum,
172
+ description,
173
+ });
174
+ /**
175
+ * @param {Record<string, JsonSchema>} properties
176
+ * @param {readonly string[]} [required]
177
+ * @param {string} [description]
178
+ * @param {boolean | JsonSchema} [additionalProperties]
179
+ * @returns {JsonSchema}
180
+ */
181
+ const objectSchema = (properties, required = [], description, additionalProperties = false) => ({
182
+ type: "object",
183
+ ...(description ? { description } : {}),
184
+ properties,
185
+ required,
186
+ additionalProperties,
187
+ });
188
+ /**
189
+ * @param {JsonSchema} items
190
+ * @param {string} description
191
+ * @returns {JsonSchema}
192
+ */
193
+ const arraySchema = (items, description) => ({
194
+ type: "array",
195
+ description,
196
+ items,
197
+ });
198
+
199
+ /** @type {JsonSchema} */
200
+ export const anyJsonSchema = {
201
+ // The branches are mutually exclusive so a value matches exactly one of them
202
+ // under strict `oneOf` semantics. The `number` branch already covers integers
203
+ // (an integer is a JSON number), so a separate `integer` branch would make
204
+ // every integer match two branches and fail `oneOf` validation.
205
+ description: "Any JSON value.",
206
+ oneOf: [
207
+ { type: "object", additionalProperties: true },
208
+ { type: "array", items: {} },
209
+ { type: "string" },
210
+ { type: "number" },
211
+ { type: "boolean" },
212
+ { type: "null" },
213
+ ],
214
+ };
215
+
216
+ const runId = stringSchema("Stable run identifier.");
217
+ const workflow = stringSchema("Registered Gateway workflow key.");
218
+ const nodeId = stringSchema("Workflow node id.");
219
+ const iteration = integerSchema("Node iteration.", 0);
220
+ const afterSeq = integerSchema("Replay events with sequence numbers greater than this value.", 0);
221
+ const fromSeq = integerSchema("Legacy alias for afterSeq on DevTools streams.", 0);
222
+ const runSummary = objectSchema(
223
+ {
224
+ runId,
225
+ workflowKey: workflow,
226
+ status: stringSchema("Current run status."),
227
+ createdAtMs: integerSchema("Unix epoch milliseconds.", 0),
228
+ parentRunId: { type: ["string", "null"], description: "Immediate parent run id, if this is a child run." },
229
+ system: booleanSchema("Whether this run belongs to an internal system workflow."),
230
+ },
231
+ ["runId", "status", "system"],
232
+ "Run summary view.",
233
+ true,
234
+ );
235
+ const runDescendant = objectSchema(
236
+ {
237
+ runId,
238
+ parentRunId: { type: ["string", "null"], description: "Immediate parent run id." },
239
+ depth: integerSchema("Distance from the requested root run.", 0),
240
+ },
241
+ ["runId", "parentRunId", "depth"],
242
+ "One run in a descendant tree.",
243
+ );
244
+ const runStateView = objectSchema(
245
+ {
246
+ runId,
247
+ state: stringSchema("Derived lifecycle state."),
248
+ computedAt: stringSchema("ISO timestamp for when the view was computed."),
249
+ blocked: objectSchema({}, [], "Optional blocked-run reason.", true),
250
+ unhealthy: objectSchema({}, [], "Optional unhealthy-run reason.", true),
251
+ },
252
+ ["runId", "state", "computedAt"],
253
+ "Derived RunStateView for the run.",
254
+ true,
255
+ );
256
+ const runRecord = objectSchema(
257
+ {
258
+ runId,
259
+ workflowKey: workflow,
260
+ system: booleanSchema("Whether this run belongs to an internal system workflow."),
261
+ status: stringSchema("Persisted run status."),
262
+ createdAtMs: integerSchema("Unix epoch milliseconds.", 0),
263
+ startedAtMs: { type: ["integer", "null"], minimum: 0, description: "Unix epoch milliseconds." },
264
+ finishedAtMs: { type: ["integer", "null"], minimum: 0, description: "Unix epoch milliseconds." },
265
+ summary: objectSchema({}, [], "Counts keyed by persisted node state.", true),
266
+ runState: runStateView,
267
+ },
268
+ ["runId", "system"],
269
+ "Current run record, including node-state counts and optional derived runState.",
270
+ true,
271
+ );
272
+ const crossedEffectSchema = objectSchema(
273
+ {
274
+ kind: { type: "string", enum: ["tool", "task"] },
275
+ toolName: stringSchema("Defined tool name or task node id."),
276
+ nodeId,
277
+ iteration,
278
+ attempt: integerSchema("Attempt number.", 1),
279
+ seq: integerSchema("Tool-call sequence.", 0),
280
+ effectStatus: { type: "string", enum: ["succeeded", "unknown"] },
281
+ idempotent: booleanSchema("Whether the original effect is idempotent."),
282
+ hasRevert: booleanSchema("Whether a revert handler was registered."),
283
+ startedAtMs: integerSchema("Effect start time.", 0),
284
+ reason: stringSchema("Disposition explanation."),
285
+ },
286
+ [
287
+ "kind",
288
+ "toolName",
289
+ "nodeId",
290
+ "iteration",
291
+ "attempt",
292
+ "seq",
293
+ "effectStatus",
294
+ "idempotent",
295
+ "hasRevert",
296
+ "startedAtMs",
297
+ ],
298
+ );
299
+ const effectBoundaryReportSchema = objectSchema(
300
+ {
301
+ blocking: arraySchema(crossedEffectSchema, "Uncompensated effects that block the operation."),
302
+ revertible: arraySchema(crossedEffectSchema, "Effects with registered compensation handlers."),
303
+ warnings: arraySchema(crossedEffectSchema, "Legacy or previously forced effects that do not block."),
304
+ },
305
+ ["blocking", "revertible", "warnings"],
306
+ );
307
+ const browserErrors = [
308
+ "InvalidRequest",
309
+ "Unauthorized",
310
+ "Forbidden",
311
+ "REVISION_CONFLICT",
312
+ "SSRF_BLOCKED",
313
+ "QUOTA_EXCEEDED",
314
+ "Internal",
315
+ ];
316
+ const browserSourceSchema = {
317
+ oneOf: [
318
+ objectSchema({ kind: { const: "url" }, url: stringSchema("Public http or https URL.", 1) }, ["kind", "url"]),
319
+ objectSchema(
320
+ {
321
+ kind: { const: "dev-server" },
322
+ port: integerSchema("Declared loopback port.", 1),
323
+ path: stringSchema("Absolute path on the dev server.", 1),
324
+ },
325
+ ["kind", "port"],
326
+ ),
327
+ ],
328
+ description: "Browser navigation source.",
329
+ };
330
+ const browserViewportSchema = objectSchema(
331
+ {
332
+ width: { ...integerSchema("Viewport width.", 1), maximum: 3840 },
333
+ height: { ...integerSchema("Viewport height.", 1), maximum: 2160 },
334
+ },
335
+ ["width", "height"],
336
+ );
337
+ const browserPageSchema = objectSchema(
338
+ {
339
+ url: stringSchema("Current URL."),
340
+ title: stringSchema("Page title."),
341
+ canGoBack: booleanSchema("Whether history can go back."),
342
+ canGoForward: booleanSchema("Whether history can go forward."),
343
+ },
344
+ ["url", "title", "canGoBack", "canGoForward"],
345
+ );
346
+ const browserSnapshotSchema = objectSchema(
347
+ {
348
+ sessionId: stringSchema("Opaque session identifier."),
349
+ source: browserSourceSchema,
350
+ status: { type: "string", enum: ["starting", "ready", "loading", "suspended", "closed", "failed"] },
351
+ revision: integerSchema("Settled action revision."),
352
+ page: { oneOf: [browserPageSchema, { type: "null" }] },
353
+ viewport: browserViewportSchema,
354
+ control: objectSchema({ owner: { type: ["string", "null"], enum: ["user", "agent", null] } }, ["owner"]),
355
+ },
356
+ ["sessionId", "source", "status", "revision", "page", "viewport", "control"],
357
+ );
358
+ const browserSnapshotExample = {
359
+ sessionId: "s1",
360
+ source: { kind: "url", url: "https://example.com" },
361
+ status: "ready",
362
+ revision: 0,
363
+ page: { url: "https://example.com/", title: "Example", canGoBack: false, canGoForward: false },
364
+ viewport: { width: 1280, height: 720 },
365
+ control: { owner: null },
366
+ };
367
+ const browserLocatorSchema = {
368
+ oneOf: [
369
+ objectSchema({ testId: stringSchema("Stable test id.", 1) }, ["testId"]),
370
+ objectSchema({ role: stringSchema("Accessible role.", 1), name: stringSchema("Accessible name.", 1) }, ["role"]),
371
+ objectSchema({ css: stringSchema("Safe CSS selector.", 1) }, ["css"]),
372
+ ],
373
+ };
374
+ const browserActionSchema = {
375
+ oneOf: [
376
+ {
377
+ type: "object",
378
+ properties: { kind: { const: "navigate" }, url: stringSchema("Destination URL.", 1) },
379
+ required: ["kind", "url"],
380
+ additionalProperties: false,
381
+ },
382
+ ...["back", "forward", "reload", "stop"].map((kind) => ({
383
+ type: "object",
384
+ properties: { kind: { const: kind } },
385
+ required: ["kind"],
386
+ additionalProperties: false,
387
+ })),
388
+ {
389
+ oneOf: [
390
+ {
391
+ type: "object",
392
+ properties: {
393
+ kind: { const: "click" },
394
+ locator: browserLocatorSchema,
395
+ button: { type: "string", enum: ["left", "right", "middle"] },
396
+ modifiers: arraySchema(stringSchema("Keyboard modifier.", 1), "Modifiers."),
397
+ },
398
+ required: ["kind", "locator"],
399
+ additionalProperties: false,
400
+ },
401
+ {
402
+ type: "object",
403
+ properties: {
404
+ kind: { const: "click" },
405
+ point: objectSchema({ x: { type: "number" }, y: { type: "number" } }, ["x", "y"]),
406
+ button: { type: "string", enum: ["left", "right", "middle"] },
407
+ modifiers: arraySchema(stringSchema("Keyboard modifier.", 1), "Modifiers."),
408
+ },
409
+ required: ["kind", "point"],
410
+ additionalProperties: false,
411
+ },
412
+ ],
413
+ },
414
+ {
415
+ type: "object",
416
+ properties: {
417
+ kind: { const: "type" },
418
+ locator: browserLocatorSchema,
419
+ text: stringSchema("Text input.", 1),
420
+ replace: booleanSchema("Replace existing value."),
421
+ },
422
+ required: ["kind", "locator", "text"],
423
+ additionalProperties: false,
424
+ },
425
+ {
426
+ type: "object",
427
+ properties: {
428
+ kind: { const: "press" },
429
+ key: stringSchema("Keyboard key.", 1),
430
+ modifiers: arraySchema(stringSchema("Keyboard modifier.", 1), "Modifiers."),
431
+ },
432
+ required: ["kind", "key"],
433
+ additionalProperties: false,
434
+ },
435
+ {
436
+ type: "object",
437
+ properties: { kind: { const: "scroll" }, deltaX: { type: "number" }, deltaY: { type: "number" } },
438
+ required: ["kind", "deltaX", "deltaY"],
439
+ additionalProperties: false,
440
+ },
441
+ {
442
+ type: "object",
443
+ properties: {
444
+ kind: { const: "dialog" },
445
+ decision: { type: "string", enum: ["accept", "dismiss"] },
446
+ promptText: stringSchema("Replacement prompt text.", 1),
447
+ },
448
+ required: ["kind", "decision"],
449
+ additionalProperties: false,
450
+ },
451
+ ],
452
+ };
453
+ const browserCreateRequestSchema = objectSchema({ source: browserSourceSchema, viewport: browserViewportSchema }, [
454
+ "source",
455
+ ]);
456
+ const browserActRequestSchema = objectSchema(
457
+ {
458
+ sessionId: stringSchema("Opaque session identifier.", 1),
459
+ actionId: stringSchema("Caller-created dedupe identifier.", 1),
460
+ expectedRevision: integerSchema("Optimistic revision fence.", 0),
461
+ action: browserActionSchema,
462
+ },
463
+ ["sessionId", "actionId", "action"],
464
+ );
465
+ const browserContextRequestSchema = objectSchema(
466
+ {
467
+ sessionId: stringSchema("Opaque session identifier.", 1),
468
+ sinceRevision: integerSchema("Freshness fence.", 0),
469
+ include: arraySchema(
470
+ {
471
+ type: "string",
472
+ minLength: 1,
473
+ enum: [
474
+ "visible-text",
475
+ "accessibility",
476
+ "interactive-elements",
477
+ "screenshot",
478
+ "selections",
479
+ "recent-actions",
480
+ "console-summary",
481
+ "network-summary",
482
+ ],
483
+ },
484
+ "Requested context slices.",
485
+ ),
486
+ },
487
+ ["sessionId"],
488
+ );
489
+ const browserOutcomeSchema = {
490
+ oneOf: [
491
+ objectSchema(
492
+ {
493
+ ok: { const: true },
494
+ redirectedTo: stringSchema("Redirect destination.", 1),
495
+ redacted: booleanSchema("Whether sensitive content was redacted."),
496
+ length: integerSchema("Redacted content length.", 0),
497
+ },
498
+ ["ok"],
499
+ "Successful browser action result.",
500
+ ),
501
+ objectSchema(
502
+ {
503
+ ok: { const: false },
504
+ code: stringSchema("Browser action failure code.", 1),
505
+ message: stringSchema("Browser action failure message.", 1),
506
+ },
507
+ ["ok", "code", "message"],
508
+ "Failed browser action result.",
509
+ ),
510
+ ],
511
+ };
512
+ const browserActResponseSchema = objectSchema(
513
+ {
514
+ revision: integerSchema("Settled revision."),
515
+ page: { oneOf: [browserPageSchema, { type: "null" }] },
516
+ outcome: browserOutcomeSchema,
517
+ },
518
+ ["revision", "page", "outcome"],
519
+ );
520
+ const browserScreenshotSchema = {
521
+ oneOf: [
522
+ objectSchema(
523
+ { data: stringSchema("Base64-encoded JPEG bytes; at most 512 KiB decoded."), mediaType: { const: "image/jpeg" } },
524
+ ["data", "mediaType"],
525
+ ),
526
+ { type: "null" },
527
+ ],
528
+ };
529
+ const browserSelectionSchema = objectSchema(
530
+ {
531
+ locator: objectSchema({}, [], "Safe locator ladder.", true),
532
+ role: stringSchema("Element role."),
533
+ name: stringSchema("Element accessible name."),
534
+ text: stringSchema("Redacted and bounded visible text."),
535
+ fingerprint: stringSchema("Staleness fingerprint."),
536
+ rect: objectSchema(
537
+ { x: { type: "number" }, y: { type: "number" }, width: { type: "number" }, height: { type: "number" } },
538
+ ["x", "y", "width", "height"],
539
+ ),
540
+ viewport: browserViewportSchema,
541
+ },
542
+ ["locator", "role", "name", "text", "fingerprint", "rect", "viewport"],
543
+ );
544
+ const browserContextResponseSchema = objectSchema(
545
+ {
546
+ fresh: booleanSchema("Whether all requested slices were captured at the current revision."),
547
+ reason: stringSchema("Freshness failure reason."),
548
+ snapshot: browserSnapshotSchema,
549
+ revision: integerSchema("Current revision."),
550
+ include: arraySchema(stringSchema("Slice name."), "Returned slices."),
551
+ screenshot: browserScreenshotSchema,
552
+ selections: arraySchema(browserSelectionSchema, "Bounded successful pick history."),
553
+ },
554
+ ["fresh", "snapshot", "revision", "include"],
555
+ "Bounded browser context.",
556
+ true,
557
+ );
558
+ const browserPickRequestSchema = objectSchema(
559
+ {
560
+ sessionId: stringSchema("Opaque session identifier.", 1),
561
+ point: objectSchema({ x: { type: "number" }, y: { type: "number" } }, ["x", "y"]),
562
+ },
563
+ ["sessionId", "point"],
564
+ );
565
+ const browserPickResponseSchema = objectSchema(
566
+ {
567
+ locator: objectSchema({}, [], "Safe locator ladder.", true),
568
+ role: stringSchema("Element role."),
569
+ name: stringSchema("Element accessible name."),
570
+ text: stringSchema("Redacted and bounded visible text."),
571
+ fingerprint: stringSchema("Staleness fingerprint."),
572
+ rect: objectSchema(
573
+ { x: { type: "number" }, y: { type: "number" }, width: { type: "number" }, height: { type: "number" } },
574
+ ["x", "y", "width", "height"],
575
+ ),
576
+ viewport: browserViewportSchema,
577
+ screenshot: browserScreenshotSchema,
578
+ },
579
+ ["locator", "role", "name", "text", "fingerprint", "rect", "viewport", "screenshot"],
580
+ );
581
+
582
+ /** @type {Record<GatewayRpcErrorCode, GatewayRpcErrorDefinition>} */
583
+ export const GATEWAY_RPC_ERRORS = {
584
+ InvalidRequest: {
585
+ version: SMITHERS_API_VERSION,
586
+ code: "InvalidRequest",
587
+ httpStatus: 400,
588
+ description: "The request shape is invalid.",
589
+ },
590
+ InvalidInput: {
591
+ version: SMITHERS_API_VERSION,
592
+ code: "InvalidInput",
593
+ httpStatus: 400,
594
+ description: "The request input failed validation.",
595
+ },
596
+ Unauthorized: {
597
+ version: SMITHERS_API_VERSION,
598
+ code: "Unauthorized",
599
+ httpStatus: 401,
600
+ description: "Authentication failed or the token expired.",
601
+ },
602
+ Forbidden: {
603
+ version: SMITHERS_API_VERSION,
604
+ code: "Forbidden",
605
+ httpStatus: 403,
606
+ description: "The token is missing the required scope.",
607
+ },
608
+ RunNotFound: {
609
+ version: SMITHERS_API_VERSION,
610
+ code: "RunNotFound",
611
+ httpStatus: 404,
612
+ description: "The run does not exist.",
613
+ },
614
+ ScoreNotFound: {
615
+ version: SMITHERS_API_VERSION,
616
+ code: "ScoreNotFound",
617
+ httpStatus: 404,
618
+ description: "The score does not exist on the requested run.",
619
+ },
620
+ RUN_NOT_ACTIVE: {
621
+ version: SMITHERS_API_VERSION,
622
+ code: "RUN_NOT_ACTIVE",
623
+ httpStatus: 409,
624
+ description: "The run is not currently active and cannot be cancelled.",
625
+ },
626
+ CronNotFound: {
627
+ version: SMITHERS_API_VERSION,
628
+ code: "CronNotFound",
629
+ httpStatus: 404,
630
+ description: "The cron schedule does not exist.",
631
+ },
632
+ TicketNotFound: {
633
+ version: SMITHERS_API_VERSION,
634
+ code: "TicketNotFound",
635
+ httpStatus: 404,
636
+ description: "The ticket/work doc does not exist.",
637
+ },
638
+ NodeNotFound: {
639
+ version: SMITHERS_API_VERSION,
640
+ code: "NodeNotFound",
641
+ httpStatus: 404,
642
+ description: "The node does not exist on the run.",
643
+ },
644
+ IterationNotFound: {
645
+ version: SMITHERS_API_VERSION,
646
+ code: "IterationNotFound",
647
+ httpStatus: 404,
648
+ description: "The requested node iteration does not exist.",
649
+ },
650
+ NodeHasNoOutput: {
651
+ version: SMITHERS_API_VERSION,
652
+ code: "NodeHasNoOutput",
653
+ httpStatus: 404,
654
+ description: "The node has not produced output.",
655
+ },
656
+ FrameOutOfRange: {
657
+ version: SMITHERS_API_VERSION,
658
+ code: "FrameOutOfRange",
659
+ httpStatus: 400,
660
+ description: "The requested frame is outside the available range.",
661
+ },
662
+ SeqOutOfRange: {
663
+ version: SMITHERS_API_VERSION,
664
+ code: "SeqOutOfRange",
665
+ httpStatus: 400,
666
+ description: "The requested stream sequence is in the future.",
667
+ },
668
+ Busy: {
669
+ version: SMITHERS_API_VERSION,
670
+ code: "Busy",
671
+ httpStatus: 409,
672
+ description: "Another conflicting mutation is in progress.",
673
+ },
674
+ AlreadyDecided: {
675
+ version: SMITHERS_API_VERSION,
676
+ code: "AlreadyDecided",
677
+ httpStatus: 409,
678
+ description: "The approval decision has already been submitted.",
679
+ },
680
+ RateLimited: {
681
+ version: SMITHERS_API_VERSION,
682
+ code: "RateLimited",
683
+ httpStatus: 429,
684
+ description: "The caller exceeded a configured quota.",
685
+ },
686
+ PayloadTooLarge: {
687
+ version: SMITHERS_API_VERSION,
688
+ code: "PayloadTooLarge",
689
+ httpStatus: 413,
690
+ description: "The response exceeds the configured payload limit.",
691
+ },
692
+ BackpressureDisconnect: {
693
+ version: SMITHERS_API_VERSION,
694
+ code: "BackpressureDisconnect",
695
+ httpStatus: 429,
696
+ description: "A stream subscriber exceeded the bounded outbound queue.",
697
+ },
698
+ UnsupportedSandbox: {
699
+ version: SMITHERS_API_VERSION,
700
+ code: "UnsupportedSandbox",
701
+ httpStatus: 501,
702
+ description: "A sandbox cannot be rewound safely.",
703
+ },
704
+ VcsError: {
705
+ version: SMITHERS_API_VERSION,
706
+ code: "VcsError",
707
+ httpStatus: 500,
708
+ description: "A version-control operation failed.",
709
+ },
710
+ RewindFailed: {
711
+ version: SMITHERS_API_VERSION,
712
+ code: "RewindFailed",
713
+ httpStatus: 500,
714
+ description: "The rewind failed and the run may need attention.",
715
+ },
716
+ TIME_TRAVEL_SIDE_EFFECT_BLOCKED: {
717
+ version: SMITHERS_API_VERSION,
718
+ code: "TIME_TRAVEL_SIDE_EFFECT_BLOCKED",
719
+ httpStatus: 409,
720
+ description: "The requested time travel crosses an unresolved external side effect.",
721
+ },
722
+ Internal: {
723
+ version: SMITHERS_API_VERSION,
724
+ code: "Internal",
725
+ httpStatus: 500,
726
+ description: "The Gateway encountered an internal error.",
727
+ },
728
+ REVISION_CONFLICT: {
729
+ version: SMITHERS_API_VERSION,
730
+ code: "REVISION_CONFLICT",
731
+ httpStatus: 409,
732
+ description: "The browser session revision is stale.",
733
+ },
734
+ SSRF_BLOCKED: {
735
+ version: SMITHERS_API_VERSION,
736
+ code: "SSRF_BLOCKED",
737
+ httpStatus: 400,
738
+ description: "The browser destination is not allowed.",
739
+ },
740
+ QUOTA_EXCEEDED: {
741
+ version: SMITHERS_API_VERSION,
742
+ code: "QUOTA_EXCEEDED",
743
+ httpStatus: 429,
744
+ description: "The browser session quota is exhausted.",
745
+ },
746
+ };
747
+
748
+ /** @type {Record<string, GatewayRpcMethod>} */
749
+ export const GATEWAY_RPC_LEGACY_METHOD_ALIASES = {
750
+ "runs.create": "launchRun",
751
+ "runs.get": "getRun",
752
+ "runs.list": "listRuns",
753
+ "runs.descendants": "listRunDescendants",
754
+ "runs.cancel": "cancelRun",
755
+ "runs.pause": "pauseRun",
756
+ "approvals.decide": "submitApproval",
757
+ "signals.send": "submitSignal",
758
+ jumpToFrame: "rewindRun",
759
+ "devtools.jumpToFrame": "rewindRun",
760
+ "devtools.getNodeOutput": "getNodeOutput",
761
+ "devtools.getNodeDiff": "getNodeDiff",
762
+ "cron.list": "cronList",
763
+ "cron.add": "cronCreate",
764
+ "cron.remove": "cronDelete",
765
+ "cron.trigger": "cronRun",
766
+ };
767
+
768
+ // packages/server HTTP/legacy routes with no GATEWAY_RPC_DEFINITIONS entry, kept
769
+ // here so scope enforcement (auth/scopes.js methodGrantSatisfiesRequiredScope)
770
+ // and the OpenAPI generator share one source of truth for required scopes.
771
+ /** @type {Record<string, GatewayScope>} */
772
+ const HTTP_ROUTE_SCOPES = {
773
+ health: "run:read",
774
+ "approvals.list": "run:read",
775
+ "workflows.list": "run:read",
776
+ "runs.diff": "run:read",
777
+ listNodeStates: "run:read",
778
+ retryTask: "run:write",
779
+ "frames.list": "run:read",
780
+ "frames.get": "run:read",
781
+ "attempts.list": "run:read",
782
+ "attempts.get": "run:read",
783
+ "runs.rerun": "run:write",
784
+ approve: "approval:submit",
785
+ };
786
+
787
+ /** @type {readonly GatewayRpcDefinition[]} */
788
+ export const GATEWAY_RPC_DEFINITIONS = [
789
+ {
790
+ version: SMITHERS_API_VERSION,
791
+ method: "launchRun",
792
+ title: "Launch Run",
793
+ description: "Start a registered workflow run.",
794
+ maturity: "stable",
795
+ transport: "http+websocket",
796
+ requiredScope: "run:write",
797
+ requestSchema: objectSchema(
798
+ {
799
+ workflow,
800
+ input: objectSchema({}, [], "Workflow input.", true),
801
+ options: objectSchema(
802
+ {
803
+ runId: stringSchema("Optional caller-supplied run id."),
804
+ idempotencyKey: stringSchema("Optional caller idempotency key."),
805
+ maxConcurrency: integerSchema("Maximum parallel workflow tasks.", 1),
806
+ allowNetwork: booleanSchema("Allow network access for workflow tools."),
807
+ maxOutputBytes: integerSchema("Maximum captured output bytes per tool/process.", 1),
808
+ toolTimeoutMs: integerSchema("Maximum wall-clock time per tool call in milliseconds.", 1),
809
+ startedBy: objectSchema(
810
+ {
811
+ harness: {
812
+ type: "string",
813
+ maxLength: 64,
814
+ description: "Self-reported launch harness; surrounding whitespace is trimmed.",
815
+ },
816
+ sessionId: {
817
+ type: "string",
818
+ maxLength: 256,
819
+ description: "Self-reported harness session; surrounding whitespace is trimmed.",
820
+ },
821
+ prompt: {
822
+ type: "string",
823
+ description:
824
+ "Explicit launch context, stored durably and visibly clipped to 8,192 Unicode code points. Never inferred from workflow input.",
825
+ },
826
+ detected: {
827
+ type: "boolean",
828
+ const: true,
829
+ description: "Present only when harness or session attribution was auto-detected.",
830
+ },
831
+ },
832
+ [],
833
+ "Optional self-reported launch provenance, distinct from authenticated identity.",
834
+ ),
835
+ },
836
+ [],
837
+ "Launch options.",
838
+ ),
839
+ },
840
+ ["workflow"],
841
+ ),
842
+ responseSchema: objectSchema(
843
+ { runId, workflow, system: booleanSchema("Whether the launched workflow is an internal system workflow.") },
844
+ ["runId", "workflow", "system"],
845
+ ),
846
+ errors: ["InvalidRequest", "InvalidInput", "Unauthorized", "Forbidden", "Internal"],
847
+ exampleRequest: {
848
+ workflow: "deploy",
849
+ input: { sha: "abc123" },
850
+ options: { runId: "deploy-abc123", allowNetwork: true, startedBy: { harness: "codex", sessionId: "thread_123" } },
851
+ },
852
+ exampleResponse: { runId: "deploy-abc123", workflow: "deploy", system: false },
853
+ },
854
+ {
855
+ version: SMITHERS_API_VERSION,
856
+ method: "resumeRun",
857
+ title: "Resume Run",
858
+ description: "Resume a waiting or interrupted run.",
859
+ maturity: "stable",
860
+ transport: "http+websocket",
861
+ requiredScope: "run:write",
862
+ requestSchema: objectSchema({ runId, options: objectSchema({ force: booleanSchema("Force a resume attempt.") }) }, [
863
+ "runId",
864
+ ]),
865
+ responseSchema: objectSchema(
866
+ { runId, status: { type: "string", enum: ["resume_requested", "already_terminal"] } },
867
+ ["runId", "status"],
868
+ ),
869
+ errors: ["InvalidRequest", "Unauthorized", "Forbidden", "RunNotFound", "Internal"],
870
+ exampleRequest: { runId: "run_01", options: { force: false } },
871
+ exampleResponse: { runId: "run_01", status: "resume_requested" },
872
+ },
873
+ {
874
+ version: SMITHERS_API_VERSION,
875
+ method: "cancelRun",
876
+ title: "Cancel Run",
877
+ description: "Cancel an active run.",
878
+ maturity: "stable",
879
+ transport: "http+websocket",
880
+ requiredScope: "run:write",
881
+ requestSchema: objectSchema({ runId }, ["runId"]),
882
+ responseSchema: objectSchema({ runId, status: { type: "string", enum: ["cancelling"] } }, ["runId", "status"]),
883
+ errors: ["InvalidRequest", "Unauthorized", "Forbidden", "RUN_NOT_ACTIVE", "Internal"],
884
+ exampleRequest: { runId: "run_01" },
885
+ exampleResponse: { runId: "run_01", status: "cancelling" },
886
+ },
887
+ {
888
+ version: SMITHERS_API_VERSION,
889
+ method: "pauseRun",
890
+ title: "Pause Run",
891
+ description:
892
+ "Gracefully pause an active run: stop scheduling new tasks, let in-flight tasks finish, then park it resumably.",
893
+ maturity: "stable",
894
+ transport: "http+websocket",
895
+ requiredScope: "run:write",
896
+ requestSchema: objectSchema({ runId }, ["runId"]),
897
+ responseSchema: objectSchema({ runId, status: { type: "string", enum: ["pausing"] } }, ["runId", "status"]),
898
+ errors: ["InvalidRequest", "Unauthorized", "Forbidden", "RunNotFound", "RUN_NOT_ACTIVE", "Internal"],
899
+ exampleRequest: { runId: "run_01" },
900
+ exampleResponse: { runId: "run_01", status: "pausing" },
901
+ },
902
+ {
903
+ version: SMITHERS_API_VERSION,
904
+ method: "hijackRun",
905
+ title: "Hijack Run",
906
+ description: "Create an elevated operator handoff session for a run.",
907
+ maturity: "stable",
908
+ transport: "http+websocket",
909
+ requiredScope: "run:admin",
910
+ requestSchema: objectSchema({ runId, options: objectSchema({}, [], "Hijack options.", true) }, ["runId"]),
911
+ responseSchema: objectSchema(
912
+ {
913
+ runId,
914
+ status: { type: "string", enum: ["hijack-ready"] },
915
+ sessionId: stringSchema("Hijack handoff session id."),
916
+ },
917
+ ["runId", "status", "sessionId"],
918
+ ),
919
+ errors: ["InvalidRequest", "Unauthorized", "Forbidden", "RunNotFound", "Internal"],
920
+ exampleRequest: { runId: "run_01", options: { reason: "operator takeover" } },
921
+ exampleResponse: { runId: "run_01", status: "hijack-ready", sessionId: "hijack_01" },
922
+ },
923
+ {
924
+ version: SMITHERS_API_VERSION,
925
+ method: "rewindRun",
926
+ title: "Rewind Run",
927
+ description: "Rewind a run to a prior frame.",
928
+ maturity: "stable",
929
+ transport: "http+websocket",
930
+ requiredScope: "run:admin",
931
+ requestSchema: objectSchema(
932
+ {
933
+ runId,
934
+ frameNo: integerSchema("Target frame number.", 0),
935
+ confirm: { const: true, description: "Must be true." },
936
+ force: booleanSchema("Cross unresolved effects and mark the run needs-attention."),
937
+ noRevert: booleanSchema("Skip registered revert handlers."),
938
+ },
939
+ ["runId", "frameNo", "confirm"],
940
+ ),
941
+ responseSchema: objectSchema(
942
+ {
943
+ ok: { const: true },
944
+ newFrameNo: integerSchema("Committed target frame.", 0),
945
+ revertedSandboxes: integerSchema("Reverted sandbox count.", 0),
946
+ deletedFrames: integerSchema("Discarded frame count.", 0),
947
+ deletedAttempts: integerSchema("Discarded attempt count.", 0),
948
+ invalidatedDiffs: integerSchema("Invalidated diff count.", 0),
949
+ durationMs: integerSchema("Operation duration.", 0),
950
+ effectBoundary: effectBoundaryReportSchema,
951
+ },
952
+ [
953
+ "ok",
954
+ "newFrameNo",
955
+ "revertedSandboxes",
956
+ "deletedFrames",
957
+ "deletedAttempts",
958
+ "invalidatedDiffs",
959
+ "durationMs",
960
+ "effectBoundary",
961
+ ],
962
+ ),
963
+ errors: [
964
+ "InvalidRequest",
965
+ "Unauthorized",
966
+ "Forbidden",
967
+ "RunNotFound",
968
+ "FrameOutOfRange",
969
+ "Busy",
970
+ "RateLimited",
971
+ "UnsupportedSandbox",
972
+ "VcsError",
973
+ "RewindFailed",
974
+ "TIME_TRAVEL_SIDE_EFFECT_BLOCKED",
975
+ ],
976
+ exampleRequest: { runId: "run_01", frameNo: 4, confirm: true, force: false, noRevert: false },
977
+ exampleResponse: {
978
+ ok: true,
979
+ newFrameNo: 4,
980
+ revertedSandboxes: 0,
981
+ deletedFrames: 2,
982
+ deletedAttempts: 1,
983
+ invalidatedDiffs: 0,
984
+ durationMs: 12,
985
+ effectBoundary: { blocking: [], revertible: [], warnings: [] },
986
+ },
987
+ },
988
+ {
989
+ version: SMITHERS_API_VERSION,
990
+ method: "submitApproval",
991
+ title: "Submit Approval",
992
+ description: "Submit an approval decision for a waiting approval node.",
993
+ maturity: "stable",
994
+ transport: "http+websocket",
995
+ requiredScope: "approval:submit",
996
+ requestSchema: objectSchema(
997
+ {
998
+ runId,
999
+ nodeId,
1000
+ iteration,
1001
+ approved: booleanSchema("Whether the approval is granted."),
1002
+ note: stringSchema("Optional approval note."),
1003
+ decision: objectSchema(
1004
+ {
1005
+ approved: booleanSchema("Whether the approval is granted."),
1006
+ value: anyJsonSchema,
1007
+ note: stringSchema("Optional decision note."),
1008
+ },
1009
+ [],
1010
+ "Approval decision payload.",
1011
+ true,
1012
+ ),
1013
+ },
1014
+ ["runId", "nodeId", "decision"],
1015
+ ),
1016
+ responseSchema: objectSchema(
1017
+ { runId, nodeId, iteration, approved: booleanSchema("Whether the approval was granted.") },
1018
+ ["runId", "nodeId", "iteration", "approved"],
1019
+ ),
1020
+ errors: [
1021
+ "InvalidRequest",
1022
+ "InvalidInput",
1023
+ "Unauthorized",
1024
+ "Forbidden",
1025
+ "RunNotFound",
1026
+ "AlreadyDecided",
1027
+ "Internal",
1028
+ ],
1029
+ exampleRequest: { runId: "run_01", nodeId: "approve", approved: true, decision: { note: "ship it" } },
1030
+ exampleResponse: { runId: "run_01", nodeId: "approve", iteration: 0, approved: true },
1031
+ },
1032
+ {
1033
+ version: SMITHERS_API_VERSION,
1034
+ method: "submitSignal",
1035
+ title: "Submit Signal",
1036
+ description: "Deliver a signal payload to a waiting run.",
1037
+ maturity: "stable",
1038
+ transport: "http+websocket",
1039
+ requiredScope: "signal:submit",
1040
+ requestSchema: objectSchema(
1041
+ {
1042
+ runId,
1043
+ correlationKey: stringSchema("Signal correlation key."),
1044
+ signalName: stringSchema("Optional explicit signal name."),
1045
+ payload: anyJsonSchema,
1046
+ },
1047
+ ["runId", "correlationKey"],
1048
+ ),
1049
+ responseSchema: objectSchema({}, [], "Signal delivery metadata.", true),
1050
+ errors: ["InvalidRequest", "Unauthorized", "Forbidden", "RunNotFound", "Internal"],
1051
+ exampleRequest: {
1052
+ runId: "run_01",
1053
+ correlationKey: "issue-42",
1054
+ signalName: "github.comment.created",
1055
+ payload: { body: "ready" },
1056
+ },
1057
+ exampleResponse: { runId: "run_01", signalName: "github.comment.created", correlationId: "issue-42", seq: 1 },
1058
+ },
1059
+ {
1060
+ version: SMITHERS_API_VERSION,
1061
+ method: "getRun",
1062
+ title: "Get Run",
1063
+ description: "Fetch one run record with node-state counts and optional derived runState.",
1064
+ maturity: "stable",
1065
+ transport: "http+websocket",
1066
+ requiredScope: "run:read",
1067
+ requestSchema: objectSchema({ runId }, ["runId"]),
1068
+ responseSchema: runRecord,
1069
+ errors: ["InvalidRequest", "Unauthorized", "Forbidden", "RunNotFound", "Internal"],
1070
+ exampleRequest: { runId: "run_01" },
1071
+ exampleResponse: {
1072
+ runId: "run_01",
1073
+ status: "finished",
1074
+ workflowKey: "deploy",
1075
+ system: false,
1076
+ summary: { finished: 3 },
1077
+ runState: { runId: "run_01", state: "succeeded", computedAt: "2026-01-01T00:00:00.000Z" },
1078
+ },
1079
+ },
1080
+ {
1081
+ version: SMITHERS_API_VERSION,
1082
+ method: "listRunTokenUsage",
1083
+ title: "List Run Token Usage",
1084
+ description: "List every persisted TokenUsageReported attempt event for a run, ordered by event sequence.",
1085
+ maturity: "stable",
1086
+ transport: "http+websocket",
1087
+ requiredScope: "run:read",
1088
+ requestSchema: objectSchema({ runId }, ["runId"]),
1089
+ responseSchema: objectSchema(
1090
+ {
1091
+ runId,
1092
+ events: arraySchema(
1093
+ objectSchema(
1094
+ {
1095
+ nodeId,
1096
+ iteration,
1097
+ attempt: integerSchema("Attempt number.", 0),
1098
+ model: stringSchema("Resolved provider model id."),
1099
+ agent: stringSchema("Agent id or adapter name."),
1100
+ inputTokens: integerSchema("Reported input tokens.", 0),
1101
+ outputTokens: integerSchema("Reported output tokens.", 0),
1102
+ cacheReadTokens: integerSchema("Reported cache-read tokens.", 0),
1103
+ cacheWriteTokens: integerSchema("Reported cache-write tokens.", 0),
1104
+ reasoningTokens: integerSchema("Reported reasoning tokens.", 0),
1105
+ timestampMs: integerSchema("Event timestamp in Unix epoch milliseconds.", 0),
1106
+ },
1107
+ [
1108
+ "nodeId",
1109
+ "iteration",
1110
+ "attempt",
1111
+ "model",
1112
+ "agent",
1113
+ "inputTokens",
1114
+ "outputTokens",
1115
+ "cacheReadTokens",
1116
+ "cacheWriteTokens",
1117
+ "reasoningTokens",
1118
+ "timestampMs",
1119
+ ],
1120
+ ),
1121
+ "Persisted token-usage attempt events.",
1122
+ ),
1123
+ },
1124
+ ["runId", "events"],
1125
+ ),
1126
+ errors: ["InvalidRequest", "Unauthorized", "Forbidden", "RunNotFound", "Internal"],
1127
+ exampleRequest: { runId: "run_01" },
1128
+ exampleResponse: {
1129
+ runId: "run_01",
1130
+ events: [
1131
+ {
1132
+ nodeId: "implement",
1133
+ iteration: 0,
1134
+ attempt: 1,
1135
+ model: "gpt-5.4-codex",
1136
+ agent: "codex-work",
1137
+ inputTokens: 120,
1138
+ outputTokens: 30,
1139
+ cacheReadTokens: 40,
1140
+ cacheWriteTokens: 5,
1141
+ reasoningTokens: 12,
1142
+ timestampMs: 1710000000000,
1143
+ },
1144
+ ],
1145
+ },
1146
+ },
1147
+ {
1148
+ version: SMITHERS_API_VERSION,
1149
+ method: "listRuns",
1150
+ title: "List Runs",
1151
+ description: "List recent runs matching an optional filter.",
1152
+ maturity: "stable",
1153
+ transport: "http+websocket",
1154
+ requiredScope: "run:read",
1155
+ requestSchema: objectSchema({
1156
+ filter: objectSchema({
1157
+ status: stringSchema("Optional run status filter."),
1158
+ limit: integerSchema("Maximum number of runs.", 1),
1159
+ offset: {
1160
+ type: "integer",
1161
+ minimum: 0,
1162
+ maximum: Number.MAX_SAFE_INTEGER,
1163
+ description:
1164
+ "Rows to skip after the newest-first sort (server-side pagination); a safe non-negative integer.",
1165
+ },
1166
+ workflow: stringSchema("Optional workflow key filter."),
1167
+ parentRunId: stringSchema("Return only direct children of this run."),
1168
+ includeSystem: booleanSchema("Include internal system runs; they are excluded by default."),
1169
+ }),
1170
+ }),
1171
+ responseSchema: arraySchema(runSummary, "Run summaries."),
1172
+ errors: ["InvalidRequest", "Unauthorized", "Forbidden", "Internal"],
1173
+ exampleRequest: { filter: { status: "finished", limit: 20, workflow: "deploy" } },
1174
+ exampleResponse: [
1175
+ { runId: "run_01", workflowKey: "deploy", status: "finished", createdAtMs: 1710000000000, system: false },
1176
+ ],
1177
+ },
1178
+ {
1179
+ version: SMITHERS_API_VERSION,
1180
+ method: "listRunDescendants",
1181
+ title: "List Run Descendants",
1182
+ description: "List a run and every transitive child with breadth-first depth metadata.",
1183
+ maturity: "stable",
1184
+ transport: "http+websocket",
1185
+ requiredScope: "run:read",
1186
+ requestSchema: objectSchema(
1187
+ {
1188
+ runId,
1189
+ limit: integerSchema("Maximum lineage rows, including the requested root run.", 1),
1190
+ },
1191
+ ["runId"],
1192
+ ),
1193
+ responseSchema: arraySchema(runDescendant, "The root run followed by its descendants."),
1194
+ errors: ["InvalidRequest", "Unauthorized", "Forbidden", "RunNotFound", "Internal"],
1195
+ exampleRequest: { runId: "run_01", limit: 1000 },
1196
+ exampleResponse: [
1197
+ { runId: "run_01", parentRunId: null, depth: 0 },
1198
+ { runId: "run_01:child:review:0", parentRunId: "run_01", depth: 1 },
1199
+ ],
1200
+ },
1201
+ {
1202
+ version: SMITHERS_API_VERSION,
1203
+ method: "getSchemaSignature",
1204
+ title: "Get Schema Signature",
1205
+ description:
1206
+ "Return the server Smithers schema migration head and table-catalog signature used by client persistence.",
1207
+ maturity: "stable",
1208
+ transport: "http+websocket",
1209
+ requiredScope: "run:read",
1210
+ requestSchema: objectSchema({}),
1211
+ responseSchema: objectSchema(
1212
+ {
1213
+ schemaVersion: stringSchema("Current _smithers_schema_migrations head id."),
1214
+ signature: stringSchema("Stable hash of the server schema catalog."),
1215
+ components: objectSchema({}, [], "Per-table schema component hashes.", true),
1216
+ },
1217
+ ["schemaVersion", "signature"],
1218
+ ),
1219
+ errors: ["InvalidRequest", "Unauthorized", "Forbidden", "Internal"],
1220
+ exampleRequest: {},
1221
+ exampleResponse: { schemaVersion: "0016", signature: "sha256" },
1222
+ },
1223
+ {
1224
+ version: SMITHERS_API_VERSION,
1225
+ method: "listWorkflows",
1226
+ title: "List Workflows",
1227
+ description: "List workflows registered with the Gateway.",
1228
+ maturity: "stable",
1229
+ transport: "http+websocket",
1230
+ requiredScope: "run:read",
1231
+ requestSchema: objectSchema({
1232
+ filter: objectSchema({
1233
+ hasUi: booleanSchema("Only return workflows with or without an attached UI."),
1234
+ includeSystem: booleanSchema("Include system (internal plumbing) workflows; they are excluded by default."),
1235
+ }),
1236
+ }),
1237
+ responseSchema: arraySchema(
1238
+ objectSchema(
1239
+ {
1240
+ key: workflow,
1241
+ readableName: stringSchema("Human-readable workflow name."),
1242
+ description: stringSchema("Workflow description."),
1243
+ hasUi: booleanSchema("Whether this workflow has a custom UI mounted."),
1244
+ uiPath: { type: ["string", "null"], description: "Mounted UI path when present." },
1245
+ system: booleanSchema("Whether this is a system (internal plumbing) workflow hidden from default listings."),
1246
+ },
1247
+ ["key", "hasUi", "uiPath", "system"],
1248
+ ),
1249
+ "Registered workflow summaries.",
1250
+ ),
1251
+ errors: ["InvalidRequest", "Unauthorized", "Forbidden", "Internal"],
1252
+ exampleRequest: { filter: { hasUi: true } },
1253
+ exampleResponse: [
1254
+ { key: "deploy", readableName: "Deploy", hasUi: true, uiPath: "/workflows/deploy", system: false },
1255
+ ],
1256
+ },
1257
+ {
1258
+ version: SMITHERS_API_VERSION,
1259
+ method: "listApprovals",
1260
+ title: "List Approvals",
1261
+ description: "List pending Gateway approval requests.",
1262
+ maturity: "stable",
1263
+ transport: "http+websocket",
1264
+ requiredScope: "run:read",
1265
+ requestSchema: objectSchema({
1266
+ filter: objectSchema({
1267
+ runId,
1268
+ workflow: stringSchema("Registered Gateway workflow key."),
1269
+ limit: integerSchema("Maximum number of approvals.", 1),
1270
+ }),
1271
+ }),
1272
+ responseSchema: arraySchema(objectSchema({}, [], "Pending approval summary.", true), "Pending approvals."),
1273
+ errors: ["InvalidRequest", "Unauthorized", "Forbidden", "Internal"],
1274
+ exampleRequest: { filter: { workflow: "deploy", limit: 20 } },
1275
+ exampleResponse: [
1276
+ {
1277
+ runId: "run_01",
1278
+ workflowKey: "deploy",
1279
+ nodeId: "approve",
1280
+ iteration: 0,
1281
+ requestTitle: "Approve deploy",
1282
+ requestedAtMs: 1710000000000,
1283
+ },
1284
+ ],
1285
+ },
1286
+ {
1287
+ version: SMITHERS_API_VERSION,
1288
+ method: "listDocs",
1289
+ title: "List Docs",
1290
+ description:
1291
+ "List DB-backed Smithers markdown artifacts for tickets, plans, specs, proposals, and conflict markers.",
1292
+ maturity: "stable",
1293
+ transport: "http+websocket",
1294
+ requiredScope: "run:read",
1295
+ requestSchema: objectSchema({
1296
+ filter: objectSchema({
1297
+ kind: stringSchema("Optional doc kind filter."),
1298
+ includeDeleted: booleanSchema("Include tombstone rows."),
1299
+ updatedAfterMs: integerSchema("Only return docs updated after this millisecond timestamp.", 0),
1300
+ limit: integerSchema("Maximum number of docs.", 1),
1301
+ }),
1302
+ }),
1303
+ responseSchema: arraySchema(
1304
+ objectSchema(
1305
+ {
1306
+ path: stringSchema("Path relative to .smithers, e.g. tickets/smithers/0030.md."),
1307
+ kind: stringSchema("ticket, plan, spec, proposal, or conflict."),
1308
+ content: stringSchema("Markdown or conflict marker content."),
1309
+ contentHash: stringSchema("SHA-256 hash of content."),
1310
+ updatedAtMs: integerSchema("Last DB update time in milliseconds.", 0),
1311
+ deletedAtMs: { type: ["integer", "null"], description: "Tombstone timestamp when deleted." },
1312
+ },
1313
+ ["path", "kind", "content", "contentHash", "updatedAtMs"],
1314
+ ),
1315
+ "Smithers docs rows.",
1316
+ ),
1317
+ errors: ["InvalidRequest", "Unauthorized", "Forbidden", "Internal"],
1318
+ exampleRequest: { filter: { kind: "ticket", limit: 100 } },
1319
+ exampleResponse: [
1320
+ {
1321
+ path: "tickets/smithers/0030-jjhub-sse-seam.md",
1322
+ kind: "ticket",
1323
+ content: "# Ticket",
1324
+ contentHash: "sha256",
1325
+ updatedAtMs: 1710000000000,
1326
+ deletedAtMs: null,
1327
+ },
1328
+ ],
1329
+ },
1330
+ {
1331
+ version: SMITHERS_API_VERSION,
1332
+ method: "streamRunEvents",
1333
+ title: "Stream Run Events",
1334
+ description: "Subscribe to a run event stream with bounded replay and GapResync semantics.",
1335
+ maturity: "stable",
1336
+ transport: "websocket",
1337
+ requiredScope: "run:read",
1338
+ requestSchema: objectSchema({ runId, afterSeq }, ["runId"]),
1339
+ responseSchema: objectSchema(
1340
+ {
1341
+ streamId: stringSchema("Stream id."),
1342
+ runId,
1343
+ afterSeq: { type: ["integer", "null"] },
1344
+ currentSeq: integerSchema("Current per-run event sequence.", 0),
1345
+ },
1346
+ ["streamId", "runId", "afterSeq", "currentSeq"],
1347
+ ),
1348
+ errors: ["InvalidRequest", "Unauthorized", "Forbidden", "RunNotFound", "SeqOutOfRange", "Internal"],
1349
+ exampleRequest: { runId: "run_01", afterSeq: 41 },
1350
+ exampleResponse: { streamId: "stream_01", runId: "run_01", afterSeq: 41, currentSeq: 45 },
1351
+ },
1352
+ {
1353
+ version: SMITHERS_API_VERSION,
1354
+ method: "streamDevTools",
1355
+ title: "Stream DevTools",
1356
+ description: "Subscribe to the DevTools snapshot and delta stream.",
1357
+ maturity: "stable",
1358
+ transport: "websocket",
1359
+ requiredScope: "observability:read",
1360
+ requestSchema: objectSchema({ runId, afterSeq, fromSeq }, ["runId"]),
1361
+ responseSchema: objectSchema(
1362
+ {
1363
+ streamId: stringSchema("Stream id."),
1364
+ runId,
1365
+ fromSeq: { type: ["integer", "null"] },
1366
+ afterSeq: { type: ["integer", "null"] },
1367
+ },
1368
+ ["streamId", "runId", "fromSeq", "afterSeq"],
1369
+ ),
1370
+ errors: [
1371
+ "InvalidRequest",
1372
+ "Unauthorized",
1373
+ "Forbidden",
1374
+ "RunNotFound",
1375
+ "SeqOutOfRange",
1376
+ "BackpressureDisconnect",
1377
+ "Internal",
1378
+ ],
1379
+ exampleRequest: { runId: "run_01", afterSeq: 10 },
1380
+ exampleResponse: { streamId: "stream_01", runId: "run_01", fromSeq: 10, afterSeq: 10 },
1381
+ },
1382
+ {
1383
+ version: SMITHERS_API_VERSION,
1384
+ method: "getDevToolsSnapshot",
1385
+ title: "Get DevTools Snapshot",
1386
+ description: "Fetch a DevTools snapshot tree for a run, optionally pinned to a frame number.",
1387
+ maturity: "stable",
1388
+ transport: "http+websocket",
1389
+ requiredScope: "observability:read",
1390
+ requestSchema: objectSchema({ runId, frameNo: integerSchema("Target frame number.", 0) }, ["runId"]),
1391
+ responseSchema: objectSchema({}, [], "DevTools snapshot payload.", true),
1392
+ errors: [
1393
+ "InvalidRequest",
1394
+ "Unauthorized",
1395
+ "Forbidden",
1396
+ "RunNotFound",
1397
+ "FrameOutOfRange",
1398
+ "PayloadTooLarge",
1399
+ "Internal",
1400
+ ],
1401
+ exampleRequest: { runId: "run_01", frameNo: 4 },
1402
+ exampleResponse: {
1403
+ version: 1,
1404
+ runId: "run_01",
1405
+ frameNo: 4,
1406
+ seq: 18,
1407
+ root: { id: 0, type: "workflow", name: "deploy", props: {}, children: [], depth: 0 },
1408
+ },
1409
+ },
1410
+ {
1411
+ version: SMITHERS_API_VERSION,
1412
+ method: "getNodeOutput",
1413
+ title: "Get Node Output",
1414
+ description: "Fetch a task node output payload.",
1415
+ maturity: "stable",
1416
+ transport: "http+websocket",
1417
+ requiredScope: "run:read",
1418
+ requestSchema: objectSchema({ runId, nodeId, iteration }, ["runId", "nodeId"]),
1419
+ responseSchema: objectSchema({}, [], "NodeOutputResponse.", true),
1420
+ errors: [
1421
+ "InvalidRequest",
1422
+ "Unauthorized",
1423
+ "Forbidden",
1424
+ "RunNotFound",
1425
+ "NodeNotFound",
1426
+ "IterationNotFound",
1427
+ "NodeHasNoOutput",
1428
+ "PayloadTooLarge",
1429
+ "Internal",
1430
+ ],
1431
+ exampleRequest: { runId: "run_01", nodeId: "task", iteration: 0 },
1432
+ exampleResponse: { status: "produced", row: { value: 1 }, schema: null },
1433
+ },
1434
+ {
1435
+ version: SMITHERS_API_VERSION,
1436
+ method: "getNodeDiff",
1437
+ title: "Get Node Diff",
1438
+ description: "Fetch a node-level diff bundle for one iteration.",
1439
+ maturity: "stable",
1440
+ transport: "http+websocket",
1441
+ requiredScope: "run:read",
1442
+ requestSchema: objectSchema({ runId, nodeId, iteration }, ["runId", "nodeId"]),
1443
+ responseSchema: objectSchema({}, [], "Node diff response.", true),
1444
+ errors: [
1445
+ "InvalidRequest",
1446
+ "Unauthorized",
1447
+ "Forbidden",
1448
+ "RunNotFound",
1449
+ "NodeNotFound",
1450
+ "IterationNotFound",
1451
+ "PayloadTooLarge",
1452
+ "VcsError",
1453
+ "Internal",
1454
+ ],
1455
+ exampleRequest: { runId: "run_01", nodeId: "task", iteration: 0 },
1456
+ exampleResponse: { summary: { filesChanged: 1 }, files: [] },
1457
+ },
1458
+ {
1459
+ version: SMITHERS_API_VERSION,
1460
+ method: "getRunDiff",
1461
+ title: "Get Run Diff",
1462
+ description:
1463
+ "Fetch the final base-to-terminal DiffBundle for a run. An oversized result is returned as an explicit marker.",
1464
+ maturity: "stable",
1465
+ transport: "http+websocket",
1466
+ requiredScope: "run:read",
1467
+ requestSchema: objectSchema({ runId }, ["runId"]),
1468
+ responseSchema: {
1469
+ oneOf: [
1470
+ objectSchema(
1471
+ {
1472
+ seq: integerSchema("Terminal diff sequence.", 0),
1473
+ baseRef: stringSchema("Immutable run base commit."),
1474
+ patches: arraySchema(
1475
+ {
1476
+ type: "object",
1477
+ properties: {
1478
+ path: stringSchema("Changed file path."),
1479
+ operation: { type: "string", enum: ["add", "modify", "delete"] },
1480
+ diff: stringSchema("Unified patch text."),
1481
+ binaryContent: { type: ["string", "null"], description: "Base64 binary content when applicable." },
1482
+ },
1483
+ required: ["path", "operation", "diff"],
1484
+ additionalProperties: false,
1485
+ },
1486
+ "Changed files.",
1487
+ ),
1488
+ },
1489
+ ["seq", "baseRef", "patches"],
1490
+ "Final run DiffBundle.",
1491
+ ),
1492
+ objectSchema(
1493
+ {
1494
+ status: { type: "string", enum: ["oversized"] },
1495
+ baseRef: stringSchema("Immutable run base commit."),
1496
+ terminalRef: stringSchema("Terminal commit, or multiple when lanes were merged."),
1497
+ sizeBytes: integerSchema("Serialized response size.", 0),
1498
+ maxBytes: integerSchema("Maximum response size.", 1),
1499
+ },
1500
+ ["status", "baseRef", "terminalRef", "sizeBytes", "maxBytes"],
1501
+ "Explicit oversized marker.",
1502
+ ),
1503
+ ],
1504
+ },
1505
+ errors: ["InvalidRequest", "Unauthorized", "Forbidden", "RunNotFound", "VcsError", "Internal"],
1506
+ exampleRequest: { runId: "run_01" },
1507
+ exampleResponse: { seq: 3, baseRef: "abc123", patches: [] },
1508
+ },
1509
+ {
1510
+ version: SMITHERS_API_VERSION,
1511
+ method: "whatHappened",
1512
+ title: "What Happened",
1513
+ description:
1514
+ "Summarize what happened in a run, or in one node when nodeId is provided. A Gateway-configured narrator agent produces a short plain-text recap; without one the Gateway answers with a deterministic fact summary.",
1515
+ maturity: "stable",
1516
+ transport: "http+websocket",
1517
+ requiredScope: "run:read",
1518
+ requestSchema: objectSchema({ runId, nodeId, iteration }, ["runId"]),
1519
+ responseSchema: objectSchema(
1520
+ {
1521
+ runId,
1522
+ nodeId: { type: ["string", "null"], description: "Node id when node-scoped, null for a run summary." },
1523
+ iteration: { type: ["integer", "null"], description: "Resolved node iteration, null for a run summary." },
1524
+ scope: { type: "string", enum: ["run", "node"] },
1525
+ summary: stringSchema("Short plain-text recap of what happened."),
1526
+ agentId: {
1527
+ type: ["string", "null"],
1528
+ description: "Narrator agent id, or null for the deterministic fallback.",
1529
+ },
1530
+ source: { type: "string", enum: ["agent", "facts"] },
1531
+ cached: booleanSchema("True when served from the Gateway summary cache."),
1532
+ generatedAtMs: integerSchema("Unix epoch milliseconds when the summary was generated.", 0),
1533
+ },
1534
+ ["runId", "nodeId", "iteration", "scope", "summary", "agentId", "source", "cached", "generatedAtMs"],
1535
+ ),
1536
+ errors: ["InvalidRequest", "Unauthorized", "Forbidden", "RunNotFound", "NodeNotFound", "Internal"],
1537
+ exampleRequest: { runId: "run_01", nodeId: "implement", iteration: 0 },
1538
+ exampleResponse: {
1539
+ runId: "run_01",
1540
+ nodeId: "implement",
1541
+ iteration: 0,
1542
+ scope: "node",
1543
+ summary: "The implement step finished in 42s after one retry.",
1544
+ agentId: "codex",
1545
+ source: "agent",
1546
+ cached: false,
1547
+ generatedAtMs: 1751000000000,
1548
+ },
1549
+ },
1550
+ {
1551
+ version: SMITHERS_API_VERSION,
1552
+ method: "cronList",
1553
+ title: "Cron List",
1554
+ description: "List Gateway cron schedules.",
1555
+ maturity: "stable",
1556
+ transport: "http+websocket",
1557
+ requiredScope: "cron:read",
1558
+ requestSchema: objectSchema({ filter: objectSchema({ workflow: stringSchema("Workflow key.") }) }),
1559
+ responseSchema: arraySchema(objectSchema({}, [], "Cron row.", true), "Cron rows."),
1560
+ errors: ["InvalidRequest", "Unauthorized", "Forbidden", "Internal"],
1561
+ exampleRequest: { filter: { workflow: "deploy" } },
1562
+ exampleResponse: [{ cronId: "cron_01", workflow: "deploy", pattern: "0 8 * * 1-5" }],
1563
+ },
1564
+ {
1565
+ version: SMITHERS_API_VERSION,
1566
+ method: "cronCreate",
1567
+ title: "Cron Create",
1568
+ description: "Create or replace a Gateway cron schedule.",
1569
+ maturity: "stable",
1570
+ transport: "http+websocket",
1571
+ requiredScope: "cron:write",
1572
+ requestSchema: objectSchema(
1573
+ {
1574
+ workflow,
1575
+ pattern: stringSchema("Cron expression."),
1576
+ cronId: stringSchema("Optional cron id."),
1577
+ enabled: booleanSchema("Whether the schedule is enabled."),
1578
+ },
1579
+ ["workflow", "pattern"],
1580
+ ),
1581
+ responseSchema: objectSchema({}, [], "Created cron row.", true),
1582
+ errors: ["InvalidRequest", "Unauthorized", "Forbidden", "Internal"],
1583
+ exampleRequest: { workflow: "deploy", pattern: "0 8 * * 1-5" },
1584
+ exampleResponse: { cronId: "cron_01", workflow: "deploy", pattern: "0 8 * * 1-5", enabled: true },
1585
+ },
1586
+ {
1587
+ version: SMITHERS_API_VERSION,
1588
+ method: "cronDelete",
1589
+ title: "Cron Delete",
1590
+ description: "Delete a Gateway cron schedule.",
1591
+ maturity: "stable",
1592
+ transport: "http+websocket",
1593
+ requiredScope: "cron:write",
1594
+ requestSchema: objectSchema({ cronId: stringSchema("Cron id.") }, ["cronId"]),
1595
+ responseSchema: objectSchema({ cronId: stringSchema("Cron id."), removed: booleanSchema("True when removed.") }, [
1596
+ "cronId",
1597
+ "removed",
1598
+ ]),
1599
+ errors: ["InvalidRequest", "Unauthorized", "Forbidden", "CronNotFound", "Internal"],
1600
+ exampleRequest: { cronId: "cron_01" },
1601
+ exampleResponse: { cronId: "cron_01", removed: true },
1602
+ },
1603
+ {
1604
+ version: SMITHERS_API_VERSION,
1605
+ method: "cronRun",
1606
+ title: "Cron Run",
1607
+ description: "Trigger a cron schedule or workflow immediately.",
1608
+ maturity: "stable",
1609
+ transport: "http+websocket",
1610
+ requiredScope: "cron:write",
1611
+ requestSchema: objectSchema({
1612
+ cronId: stringSchema("Cron id."),
1613
+ workflow,
1614
+ input: objectSchema({}, [], "Workflow input.", true),
1615
+ }),
1616
+ responseSchema: objectSchema(
1617
+ { runId, workflow, system: booleanSchema("Whether the launched workflow is an internal system workflow.") },
1618
+ ["runId", "workflow", "system"],
1619
+ ),
1620
+ errors: ["InvalidRequest", "InvalidInput", "Unauthorized", "Forbidden", "CronNotFound", "Internal"],
1621
+ exampleRequest: { cronId: "cron_01", input: { dryRun: true } },
1622
+ exampleResponse: { runId: "run_02", workflow: "deploy", system: false },
1623
+ },
1624
+ {
1625
+ version: SMITHERS_API_VERSION,
1626
+ method: "listAccounts",
1627
+ title: "List Accounts",
1628
+ description:
1629
+ "List the registered Smithers agent accounts — the entries in the user-level `~/.smithers/accounts.json` registry that the `smithers agents` CLI manages. Each account's raw API key is redacted; `hasApiKey`/`hasConfigDir` report its auth posture instead.",
1630
+ maturity: "stable",
1631
+ transport: "http+websocket",
1632
+ requiredScope: "account:read",
1633
+ requestSchema: objectSchema({}),
1634
+ responseSchema: arraySchema(
1635
+ objectSchema(
1636
+ {
1637
+ label: stringSchema("Unique account label (the registry key)."),
1638
+ provider: {
1639
+ type: "string",
1640
+ enum: ACCOUNT_PROVIDERS,
1641
+ description: "Provider id, one of the fixed `smithers agents` catalog.",
1642
+ },
1643
+ configDir: {
1644
+ type: ["string", "null"],
1645
+ description: "Per-account CLI config dir for subscription providers (null for api-key accounts).",
1646
+ },
1647
+ hasConfigDir: booleanSchema("True when a subscription account has a non-empty config dir."),
1648
+ hasApiKey: booleanSchema(
1649
+ "True when an api-key account carries a non-empty key (the key itself is never returned).",
1650
+ ),
1651
+ model: { type: ["string", "null"], description: "Optional default model baked into the account." },
1652
+ addedAt: {
1653
+ type: ["string", "null"],
1654
+ description: "ISO timestamp of when the account was added, when known.",
1655
+ },
1656
+ },
1657
+ ["label", "provider", "hasConfigDir", "hasApiKey"],
1658
+ ),
1659
+ "Registered agent accounts.",
1660
+ ),
1661
+ errors: ["InvalidRequest", "Unauthorized", "Forbidden", "Internal"],
1662
+ exampleRequest: {},
1663
+ exampleResponse: [
1664
+ {
1665
+ label: "claude-work",
1666
+ provider: "claude-code",
1667
+ configDir: "/Users/me/.claude",
1668
+ hasConfigDir: true,
1669
+ hasApiKey: false,
1670
+ model: "claude-opus-4-8",
1671
+ addedAt: "2026-01-01T00:00:00.000Z",
1672
+ },
1673
+ ],
1674
+ },
1675
+ {
1676
+ version: SMITHERS_API_VERSION,
1677
+ method: "listUsageReports",
1678
+ title: "List Usage Reports",
1679
+ description: "List normalized provider rate-limit and subscription-usage reports for registered Smithers accounts.",
1680
+ maturity: "stable",
1681
+ transport: "http+websocket",
1682
+ requiredScope: "account:read",
1683
+ requestSchema: objectSchema({ fresh: booleanSchema("Bypass the Gateway's 60-second in-memory cache.") }),
1684
+ responseSchema: arraySchema(
1685
+ objectSchema(
1686
+ {
1687
+ accountLabel: stringSchema("Registered account label."),
1688
+ provider: {
1689
+ type: "string",
1690
+ enum: ACCOUNT_PROVIDERS,
1691
+ description: "Provider id, one of the fixed `smithers agents` catalog.",
1692
+ },
1693
+ authMode: { type: "string", enum: ["subscription", "api-key"] },
1694
+ source: { type: "string", enum: ["oauth", "headers", "local", "none"] },
1695
+ windows: arraySchema(
1696
+ objectSchema(
1697
+ {
1698
+ id: stringSchema("Stable quota-window id."),
1699
+ label: stringSchema("Human-readable quota-window label."),
1700
+ unit: { type: "string", enum: ["percent", "count", "estimated"] },
1701
+ usedPercent: { type: "number" },
1702
+ used: { type: "number" },
1703
+ limit: { type: "number" },
1704
+ remaining: { type: "number" },
1705
+ resetsAt: stringSchema("ISO-8601 reset timestamp."),
1706
+ },
1707
+ ["id", "label", "unit"],
1708
+ ),
1709
+ "Provider quota windows.",
1710
+ ),
1711
+ planType: stringSchema("Provider plan or tier label."),
1712
+ credits: objectSchema(
1713
+ {
1714
+ hasCredits: booleanSchema("Whether a credit balance is present."),
1715
+ unlimited: booleanSchema("Whether credits are unlimited."),
1716
+ balance: stringSchema("Provider-formatted credit balance."),
1717
+ },
1718
+ ["hasCredits", "unlimited"],
1719
+ ),
1720
+ fetchedAt: stringSchema("ISO-8601 report timestamp."),
1721
+ stale: booleanSchema("Whether the usage package served a cached report."),
1722
+ estimate: booleanSchema("Whether the report is locally estimated."),
1723
+ error: stringSchema("Human-readable probe failure reason."),
1724
+ },
1725
+ ["accountLabel", "provider", "authMode", "source", "windows", "fetchedAt", "stale", "estimate"],
1726
+ ),
1727
+ "Provider usage reports.",
1728
+ ),
1729
+ errors: ["InvalidRequest", "Unauthorized", "Forbidden", "Internal"],
1730
+ exampleRequest: {},
1731
+ exampleResponse: [
1732
+ {
1733
+ accountLabel: "codex-work",
1734
+ provider: "codex",
1735
+ authMode: "subscription",
1736
+ source: "oauth",
1737
+ windows: [
1738
+ { id: "5h", label: "5-hour session", unit: "percent", usedPercent: 42, resetsAt: "2026-01-01T05:00:00.000Z" },
1739
+ ],
1740
+ planType: "pro",
1741
+ fetchedAt: "2026-01-01T00:00:00.000Z",
1742
+ stale: false,
1743
+ estimate: false,
1744
+ },
1745
+ ],
1746
+ },
1747
+ {
1748
+ version: SMITHERS_API_VERSION,
1749
+ method: "listMemoryFacts",
1750
+ title: "List Memory Facts",
1751
+ description: "List cross-run memory facts, optionally scoped to a namespace.",
1752
+ maturity: "stable",
1753
+ transport: "http+websocket",
1754
+ requiredScope: "memory:read",
1755
+ requestSchema: objectSchema({ namespace: stringSchema("Optional namespace filter (e.g. 'workflow:deploy').") }),
1756
+ responseSchema: arraySchema(
1757
+ objectSchema(
1758
+ {
1759
+ namespace: stringSchema("Fact namespace."),
1760
+ key: stringSchema("Fact key (unique within the namespace)."),
1761
+ valueJson: stringSchema("Stored value as a JSON string."),
1762
+ schemaSig: { type: ["string", "null"], description: "Optional value-schema signature." },
1763
+ createdAtMs: integerSchema("Unix epoch milliseconds when first written.", 0),
1764
+ updatedAtMs: integerSchema("Unix epoch milliseconds when last written.", 0),
1765
+ ttlMs: {
1766
+ type: ["integer", "null"],
1767
+ description: "Time-to-live in milliseconds, or null when the fact does not expire.",
1768
+ },
1769
+ },
1770
+ ["namespace", "key", "valueJson", "createdAtMs", "updatedAtMs"],
1771
+ ),
1772
+ "Memory facts.",
1773
+ ),
1774
+ errors: ["InvalidRequest", "Unauthorized", "Forbidden", "Internal"],
1775
+ exampleRequest: { namespace: "workflow:deploy" },
1776
+ exampleResponse: [
1777
+ {
1778
+ namespace: "workflow:deploy",
1779
+ key: "last-sha",
1780
+ valueJson: '"abc123"',
1781
+ createdAtMs: 1710000000000,
1782
+ updatedAtMs: 1710000000000,
1783
+ },
1784
+ ],
1785
+ },
1786
+ {
1787
+ version: SMITHERS_API_VERSION,
1788
+ method: "listPrompts",
1789
+ title: "List Prompts",
1790
+ description:
1791
+ "List registered prompts — the `.md`/`.mdx` files under the project's `.smithers/prompts/` directory, each returned with its source.",
1792
+ maturity: "stable",
1793
+ transport: "http+websocket",
1794
+ requiredScope: "prompt:read",
1795
+ requestSchema: objectSchema({}),
1796
+ responseSchema: arraySchema(
1797
+ objectSchema(
1798
+ {
1799
+ id: stringSchema(
1800
+ "Prompt id — the relative path under `.smithers/prompts/` without its extension (e.g. 'refactor').",
1801
+ ),
1802
+ entryFile: stringSchema("Workspace-relative source path (e.g. 'prompts/refactor.mdx')."),
1803
+ source: stringSchema("Raw prompt file text."),
1804
+ createdAtMs: integerSchema("Unix epoch milliseconds the file was created (fs birthtime).", 0),
1805
+ updatedAtMs: integerSchema("Unix epoch milliseconds the file was last modified (fs mtime).", 0),
1806
+ },
1807
+ ["id", "entryFile", "source"],
1808
+ ),
1809
+ "Registered prompts.",
1810
+ ),
1811
+ errors: ["InvalidRequest", "Unauthorized", "Forbidden", "Internal"],
1812
+ exampleRequest: {},
1813
+ exampleResponse: [
1814
+ {
1815
+ id: "refactor",
1816
+ entryFile: "prompts/refactor.mdx",
1817
+ source: "# Refactor\n\nRefactor {{file}}.",
1818
+ createdAtMs: 1710000000000,
1819
+ updatedAtMs: 1710000000000,
1820
+ },
1821
+ ],
1822
+ },
1823
+ {
1824
+ version: SMITHERS_API_VERSION,
1825
+ method: "listScores",
1826
+ title: "List Scores",
1827
+ description: "List scorer/eval results for one run, optionally scoped to a node.",
1828
+ maturity: "stable",
1829
+ transport: "http+websocket",
1830
+ requiredScope: "score:read",
1831
+ requestSchema: objectSchema(
1832
+ {
1833
+ runId: stringSchema("Run id whose scorer results to list."),
1834
+ nodeId: stringSchema("Optional node id filter."),
1835
+ },
1836
+ ["runId"],
1837
+ ),
1838
+ responseSchema: arraySchema(
1839
+ objectSchema(
1840
+ {
1841
+ runId: stringSchema("Run id the score belongs to."),
1842
+ nodeId: stringSchema("Node id the score was produced for."),
1843
+ iteration: integerSchema("Node iteration the score belongs to.", 0),
1844
+ attempt: integerSchema("Node attempt the score belongs to.", 0),
1845
+ scorerId: stringSchema("Stable scorer id."),
1846
+ scorerName: stringSchema("Human scorer name."),
1847
+ source: stringSchema("Persisted score provenance (`live` or `batch` for production scorer rows)."),
1848
+ score: { type: "number", description: "The scorer's numeric verdict." },
1849
+ reason: { type: ["string", "null"], description: "Optional human reason for the score." },
1850
+ scoredAtMs: integerSchema("Unix epoch milliseconds when the score was recorded.", 0),
1851
+ latencyMs: { type: ["number", "null"], description: "Optional scorer latency in milliseconds." },
1852
+ durationMs: { type: ["number", "null"], description: "Optional node duration in milliseconds." },
1853
+ },
1854
+ ["runId", "nodeId", "iteration", "attempt", "scorerId", "scorerName", "source", "score", "scoredAtMs"],
1855
+ ),
1856
+ "Scorer results.",
1857
+ ),
1858
+ errors: ["InvalidRequest", "Unauthorized", "Forbidden", "RunNotFound", "Internal"],
1859
+ exampleRequest: { runId: "run_01", nodeId: "review" },
1860
+ exampleResponse: [
1861
+ {
1862
+ runId: "run_01",
1863
+ nodeId: "review",
1864
+ iteration: 0,
1865
+ attempt: 0,
1866
+ scorerId: "correctness",
1867
+ scorerName: "correctness",
1868
+ source: "live",
1869
+ score: 0.92,
1870
+ scoredAtMs: 1710000000000,
1871
+ },
1872
+ ],
1873
+ },
1874
+ {
1875
+ version: SMITHERS_API_VERSION,
1876
+ method: "listScoresForRuns",
1877
+ title: "List Scores For Runs",
1878
+ description:
1879
+ "List and globally page persisted scorer rows across up to 30 explicit run ids. The caller must supply the scorer-producing run ids; this does not expand an eval wrapper into child case runs or provide experiment/case alignment.",
1880
+ maturity: "stable",
1881
+ transport: "http+websocket",
1882
+ requiredScope: "score:read",
1883
+ requestSchema: objectSchema(
1884
+ {
1885
+ runIds: {
1886
+ type: "array",
1887
+ description:
1888
+ "Up to 30 run-id entries to compare. The server trims values and deduplicates them in first-seen order.",
1889
+ items: { type: "string", minLength: 1, maxLength: 256 },
1890
+ maxItems: 30,
1891
+ },
1892
+ nodeId: stringSchema("Optional exact, case-sensitive node id filter."),
1893
+ scorerId: stringSchema("Optional exact, case-sensitive scorer id filter."),
1894
+ scorerName: stringSchema("Optional exact, case-sensitive scorer name filter."),
1895
+ source: {
1896
+ type: "string",
1897
+ enum: ["live", "batch"],
1898
+ description: "Optional exact production score provenance filter.",
1899
+ },
1900
+ order: {
1901
+ type: "string",
1902
+ enum: ["scoredAtAsc", "scoredAtDesc"],
1903
+ default: "scoredAtAsc",
1904
+ description: "Primary score timestamp order.",
1905
+ },
1906
+ offset: {
1907
+ type: "integer",
1908
+ minimum: 0,
1909
+ maximum: 9_999,
1910
+ default: 0,
1911
+ description:
1912
+ "Global result offset after merging distinct stores. The offset plus limit may not exceed the 10,000-row comparison window.",
1913
+ },
1914
+ limit: {
1915
+ type: "integer",
1916
+ minimum: 1,
1917
+ maximum: 500,
1918
+ default: 500,
1919
+ description:
1920
+ "Maximum globally merged rows to return. The offset plus limit may not exceed the 10,000-row comparison window.",
1921
+ },
1922
+ },
1923
+ ["runIds"],
1924
+ ),
1925
+ responseSchema: objectSchema(
1926
+ {
1927
+ rows: arraySchema(
1928
+ objectSchema(
1929
+ {
1930
+ scoreId: stringSchema("Exact persisted score id."),
1931
+ runId: stringSchema("Run id the score belongs to."),
1932
+ nodeId: stringSchema("Node id the score was produced for."),
1933
+ iteration: integerSchema("Node iteration the score belongs to.", 0),
1934
+ attempt: integerSchema("Node attempt the score belongs to.", 0),
1935
+ scorerId: stringSchema("Stable scorer id."),
1936
+ scorerName: stringSchema("Human scorer name."),
1937
+ source: stringSchema("Persisted score source."),
1938
+ score: { type: "number", description: "The scorer's numeric verdict." },
1939
+ reason: { type: ["string", "null"], description: "Optional human reason for the score." },
1940
+ scoredAtMs: integerSchema("Unix epoch milliseconds when the score was recorded.", 0),
1941
+ latencyMs: { type: ["number", "null"], description: "Optional scorer latency in milliseconds." },
1942
+ durationMs: { type: ["number", "null"], description: "Optional node duration in milliseconds." },
1943
+ },
1944
+ [
1945
+ "scoreId",
1946
+ "runId",
1947
+ "nodeId",
1948
+ "iteration",
1949
+ "attempt",
1950
+ "scorerId",
1951
+ "scorerName",
1952
+ "source",
1953
+ "score",
1954
+ "scoredAtMs",
1955
+ ],
1956
+ ),
1957
+ "Globally ordered persisted scorer rows.",
1958
+ ),
1959
+ total: integerSchema("Total filtered row count before global pagination.", 0),
1960
+ },
1961
+ ["rows", "total"],
1962
+ ),
1963
+ errors: ["InvalidRequest", "Unauthorized", "Forbidden", "RunNotFound", "Internal"],
1964
+ exampleRequest: { runIds: ["run_01", "run_02"], order: "scoredAtDesc", offset: 0, limit: 500 },
1965
+ exampleResponse: {
1966
+ rows: [
1967
+ {
1968
+ scoreId: "run_01:review:correctness",
1969
+ runId: "run_01",
1970
+ nodeId: "review",
1971
+ iteration: 0,
1972
+ attempt: 0,
1973
+ scorerId: "correctness",
1974
+ scorerName: "correctness",
1975
+ source: "live",
1976
+ score: 0.92,
1977
+ scoredAtMs: 1710000000000,
1978
+ },
1979
+ ],
1980
+ total: 1,
1981
+ },
1982
+ },
1983
+ {
1984
+ version: SMITHERS_API_VERSION,
1985
+ method: "getScoreDetail",
1986
+ title: "Get Score Detail",
1987
+ description: "Read one exact persisted score row with its JSON detail columns decoded.",
1988
+ maturity: "stable",
1989
+ transport: "http+websocket",
1990
+ requiredScope: "score:read",
1991
+ requestSchema: objectSchema(
1992
+ {
1993
+ runId: { type: "string", minLength: 1, maxLength: 256, description: "Owning run id (trimmed by the server)." },
1994
+ scoreId: {
1995
+ type: "string",
1996
+ minLength: 1,
1997
+ maxLength: 256,
1998
+ description: "Exact persisted score id (trimmed by the server).",
1999
+ },
2000
+ },
2001
+ ["runId", "scoreId"],
2002
+ ),
2003
+ responseSchema: objectSchema(
2004
+ {
2005
+ scoreId: stringSchema("Exact persisted score id."),
2006
+ runId: stringSchema("Run id the score belongs to."),
2007
+ nodeId: stringSchema("Node id the score was produced for."),
2008
+ iteration: integerSchema("Node iteration the score belongs to.", 0),
2009
+ attempt: integerSchema("Node attempt the score belongs to.", 0),
2010
+ scorerId: stringSchema("Stable scorer id."),
2011
+ scorerName: stringSchema("Human scorer name."),
2012
+ source: stringSchema("Persisted score source."),
2013
+ score: { type: "number", description: "The scorer's numeric verdict." },
2014
+ reason: { type: ["string", "null"], description: "Optional human reason for the score." },
2015
+ scoredAtMs: integerSchema("Unix epoch milliseconds when the score was recorded.", 0),
2016
+ latencyMs: { type: ["number", "null"], description: "Optional scorer latency in milliseconds." },
2017
+ durationMs: { type: ["number", "null"], description: "Optional node duration in milliseconds." },
2018
+ meta: anyJsonSchema,
2019
+ input: anyJsonSchema,
2020
+ output: anyJsonSchema,
2021
+ groundTruth: anyJsonSchema,
2022
+ context: anyJsonSchema,
2023
+ },
2024
+ [
2025
+ "scoreId",
2026
+ "runId",
2027
+ "nodeId",
2028
+ "iteration",
2029
+ "attempt",
2030
+ "scorerId",
2031
+ "scorerName",
2032
+ "source",
2033
+ "score",
2034
+ "scoredAtMs",
2035
+ "meta",
2036
+ "input",
2037
+ "output",
2038
+ "groundTruth",
2039
+ "context",
2040
+ ],
2041
+ ),
2042
+ errors: ["InvalidRequest", "Unauthorized", "Forbidden", "RunNotFound", "ScoreNotFound", "Internal"],
2043
+ exampleRequest: { runId: "run_01", scoreId: "run_01:review:correctness" },
2044
+ exampleResponse: {
2045
+ scoreId: "run_01:review:correctness",
2046
+ runId: "run_01",
2047
+ nodeId: "review",
2048
+ iteration: 0,
2049
+ attempt: 0,
2050
+ scorerId: "correctness",
2051
+ scorerName: "correctness",
2052
+ source: "live",
2053
+ score: 0.92,
2054
+ scoredAtMs: 1710000000000,
2055
+ meta: { rubric: "correctness" },
2056
+ input: { prompt: "Review this." },
2057
+ output: { verdict: "pass" },
2058
+ groundTruth: null,
2059
+ context: null,
2060
+ },
2061
+ },
2062
+ {
2063
+ version: SMITHERS_API_VERSION,
2064
+ method: "listTickets",
2065
+ title: "List Tickets",
2066
+ description:
2067
+ "List live work docs (tickets/plans/specs/proposals) from `_smithers_docs`; soft-deleted tombstones are never returned.",
2068
+ maturity: "stable",
2069
+ transport: "http+websocket",
2070
+ requiredScope: "ticket:read",
2071
+ requestSchema: objectSchema({
2072
+ kind: { type: "string", enum: DOC_KINDS, description: "Optional doc-kind filter; omit to list every kind." },
2073
+ }),
2074
+ responseSchema: arraySchema(
2075
+ objectSchema(
2076
+ {
2077
+ path: stringSchema("Doc identity (primary key); e.g. a ticket id."),
2078
+ kind: { type: "string", enum: DOC_KINDS, description: "Doc kind." },
2079
+ content: stringSchema("Full markdown body."),
2080
+ contentHash: stringSchema("sha256(content), lowercase hex."),
2081
+ status: {
2082
+ type: ["string", "null"],
2083
+ description: "Free-form status (e.g. todo/in-progress/done); rides the row so it survives reload.",
2084
+ },
2085
+ updatedAtMs: integerSchema("Unix epoch milliseconds of the last write.", 0),
2086
+ },
2087
+ ["path", "kind", "content", "contentHash", "updatedAtMs"],
2088
+ ),
2089
+ "Live work docs.",
2090
+ ),
2091
+ errors: ["InvalidRequest", "Unauthorized", "Forbidden", "Internal"],
2092
+ exampleRequest: { kind: "ticket" },
2093
+ exampleResponse: [
2094
+ {
2095
+ path: "feat-issues-card",
2096
+ kind: "ticket",
2097
+ content: "# Issues card",
2098
+ contentHash: "0000000000000000000000000000000000000000000000000000000000000000",
2099
+ status: "in-progress",
2100
+ updatedAtMs: 1710000000000,
2101
+ },
2102
+ ],
2103
+ },
2104
+ {
2105
+ version: SMITHERS_API_VERSION,
2106
+ method: "createTicket",
2107
+ title: "Create Ticket",
2108
+ description:
2109
+ "Create or replace a work doc by `path`. Stamps `content_hash = sha256(content)` and `updated_at_ms = now`; reviving a previously soft-deleted path is intentional.",
2110
+ maturity: "stable",
2111
+ transport: "http+websocket",
2112
+ requiredScope: "ticket:write",
2113
+ requestSchema: objectSchema(
2114
+ {
2115
+ path: stringSchema("Doc identity (primary key); e.g. `feat-issues-card`."),
2116
+ content: stringSchema("Full markdown body."),
2117
+ kind: { type: "string", enum: DOC_KINDS, description: "Doc kind (default `ticket`)." },
2118
+ status: stringSchema("Optional initial status."),
2119
+ },
2120
+ ["path", "content"],
2121
+ ),
2122
+ responseSchema: objectSchema(
2123
+ {
2124
+ path: stringSchema("Doc identity (primary key)."),
2125
+ kind: { type: "string", enum: DOC_KINDS, description: "Doc kind." },
2126
+ content: stringSchema("Full markdown body."),
2127
+ contentHash: stringSchema("sha256(content), lowercase hex."),
2128
+ status: { type: ["string", "null"], description: "Free-form status." },
2129
+ updatedAtMs: integerSchema("Unix epoch milliseconds of the write.", 0),
2130
+ },
2131
+ ["path", "kind", "content", "contentHash", "updatedAtMs"],
2132
+ "The created doc row.",
2133
+ ),
2134
+ errors: ["InvalidRequest", "Unauthorized", "Forbidden", "Internal"],
2135
+ exampleRequest: { path: "feat-issues-card", content: "# Issues card", kind: "ticket", status: "todo" },
2136
+ exampleResponse: {
2137
+ path: "feat-issues-card",
2138
+ kind: "ticket",
2139
+ content: "# Issues card",
2140
+ contentHash: "0000000000000000000000000000000000000000000000000000000000000000",
2141
+ status: "todo",
2142
+ updatedAtMs: 1710000000000,
2143
+ },
2144
+ },
2145
+ {
2146
+ version: SMITHERS_API_VERSION,
2147
+ method: "updateTicket",
2148
+ title: "Update Ticket",
2149
+ description:
2150
+ "Patch a work doc's `content` and/or `status` by `path`. Re-stamps `content_hash` + `updated_at_ms` when content changes.",
2151
+ maturity: "stable",
2152
+ transport: "http+websocket",
2153
+ requiredScope: "ticket:write",
2154
+ requestSchema: objectSchema(
2155
+ {
2156
+ path: stringSchema("Doc identity (primary key)."),
2157
+ content: stringSchema("Optional new markdown body."),
2158
+ status: stringSchema("Optional new status."),
2159
+ },
2160
+ ["path"],
2161
+ ),
2162
+ responseSchema: objectSchema(
2163
+ {
2164
+ path: stringSchema("Doc identity (primary key)."),
2165
+ kind: { type: "string", enum: DOC_KINDS, description: "Doc kind." },
2166
+ content: stringSchema("Full markdown body."),
2167
+ contentHash: stringSchema("sha256(content), lowercase hex."),
2168
+ status: { type: ["string", "null"], description: "Free-form status." },
2169
+ updatedAtMs: integerSchema("Unix epoch milliseconds of the write.", 0),
2170
+ },
2171
+ ["path", "kind", "content", "contentHash", "updatedAtMs"],
2172
+ "The updated doc row.",
2173
+ ),
2174
+ errors: ["InvalidRequest", "Unauthorized", "Forbidden", "TicketNotFound", "Internal"],
2175
+ exampleRequest: { path: "feat-issues-card", status: "in-progress" },
2176
+ exampleResponse: {
2177
+ path: "feat-issues-card",
2178
+ kind: "ticket",
2179
+ content: "# Issues card",
2180
+ contentHash: "0000000000000000000000000000000000000000000000000000000000000000",
2181
+ status: "in-progress",
2182
+ updatedAtMs: 1710000000000,
2183
+ },
2184
+ },
2185
+ {
2186
+ version: SMITHERS_API_VERSION,
2187
+ method: "deleteTicket",
2188
+ title: "Delete Ticket",
2189
+ description:
2190
+ "Soft-delete a work doc by `path` (stamps a `deleted_at_ms` tombstone). The row survives so `listTickets` hides it without losing history; the watcher never materializes a tombstone to disk.",
2191
+ maturity: "stable",
2192
+ transport: "http+websocket",
2193
+ requiredScope: "ticket:write",
2194
+ requestSchema: objectSchema({ path: stringSchema("Doc identity (primary key).") }, ["path"]),
2195
+ responseSchema: objectSchema(
2196
+ {
2197
+ path: stringSchema("Doc identity (primary key)."),
2198
+ deleted: booleanSchema("True when the doc was soft-deleted."),
2199
+ },
2200
+ ["path", "deleted"],
2201
+ "Soft-delete acknowledgement.",
2202
+ ),
2203
+ errors: ["InvalidRequest", "Unauthorized", "Forbidden", "TicketNotFound", "Internal"],
2204
+ exampleRequest: { path: "feat-issues-card" },
2205
+ exampleResponse: { path: "feat-issues-card", deleted: true },
2206
+ },
2207
+ {
2208
+ version: SMITHERS_API_VERSION,
2209
+ method: "createBrowserSession",
2210
+ title: "createBrowserSession",
2211
+ description: "Create an ephemeral Chromium browser session.",
2212
+ maturity: "stable",
2213
+ transport: "http+websocket",
2214
+ requiredScope: "run:write",
2215
+ requestSchema: browserCreateRequestSchema,
2216
+ responseSchema: browserSnapshotSchema,
2217
+ errors: [
2218
+ "InvalidRequest",
2219
+ "Unauthorized",
2220
+ "Forbidden",
2221
+ "REVISION_CONFLICT",
2222
+ "SSRF_BLOCKED",
2223
+ "QUOTA_EXCEEDED",
2224
+ "Internal",
2225
+ ],
2226
+ exampleRequest: { source: { kind: "url", url: "https://example.com" } },
2227
+ exampleResponse: browserSnapshotExample,
2228
+ },
2229
+ {
2230
+ version: SMITHERS_API_VERSION,
2231
+ method: "browserAct",
2232
+ title: "browserAct",
2233
+ description: "Perform one ordered, deduplicated browser action.",
2234
+ maturity: "stable",
2235
+ transport: "http+websocket",
2236
+ requiredScope: "run:write",
2237
+ requestSchema: browserActRequestSchema,
2238
+ responseSchema: browserActResponseSchema,
2239
+ errors: [
2240
+ "InvalidRequest",
2241
+ "Unauthorized",
2242
+ "Forbidden",
2243
+ "REVISION_CONFLICT",
2244
+ "SSRF_BLOCKED",
2245
+ "QUOTA_EXCEEDED",
2246
+ "Internal",
2247
+ ],
2248
+ exampleRequest: { sessionId: "s1", actionId: "a1", action: { kind: "reload" } },
2249
+ exampleResponse: { revision: 1, page: null, outcome: { ok: true } },
2250
+ },
2251
+ {
2252
+ version: SMITHERS_API_VERSION,
2253
+ method: "browserContext",
2254
+ title: "browserContext",
2255
+ description:
2256
+ "Read bounded live browser perception slices; screenshots are inline JPEGs and selections are a bounded metadata-only history.",
2257
+ maturity: "stable",
2258
+ transport: "http+websocket",
2259
+ requiredScope: "run:read",
2260
+ requestSchema: browserContextRequestSchema,
2261
+ responseSchema: browserContextResponseSchema,
2262
+ errors: [
2263
+ "InvalidRequest",
2264
+ "Unauthorized",
2265
+ "Forbidden",
2266
+ "REVISION_CONFLICT",
2267
+ "SSRF_BLOCKED",
2268
+ "QUOTA_EXCEEDED",
2269
+ "Internal",
2270
+ ],
2271
+ exampleRequest: { sessionId: "s1", include: ["visible-text", "screenshot"] },
2272
+ exampleResponse: {
2273
+ fresh: true,
2274
+ snapshot: browserSnapshotExample,
2275
+ revision: 0,
2276
+ include: ["visible-text"],
2277
+ screenshot: { data: "/9j/4AAQ", mediaType: "image/jpeg" },
2278
+ },
2279
+ },
2280
+ {
2281
+ version: SMITHERS_API_VERSION,
2282
+ method: "browserPick",
2283
+ title: "browserPick",
2284
+ description: "Hit-test a live page and return safe locator metadata with an inline JPEG (at most 512 KiB decoded).",
2285
+ maturity: "stable",
2286
+ transport: "http+websocket",
2287
+ requiredScope: "run:read",
2288
+ requestSchema: browserPickRequestSchema,
2289
+ responseSchema: browserPickResponseSchema,
2290
+ errors: [
2291
+ "InvalidRequest",
2292
+ "Unauthorized",
2293
+ "Forbidden",
2294
+ "REVISION_CONFLICT",
2295
+ "SSRF_BLOCKED",
2296
+ "QUOTA_EXCEEDED",
2297
+ "Internal",
2298
+ ],
2299
+ exampleRequest: { sessionId: "s1", point: { x: 10, y: 10 } },
2300
+ exampleResponse: {
2301
+ locator: { role: "button", name: "Continue" },
2302
+ role: "button",
2303
+ name: "Continue",
2304
+ text: "Continue",
2305
+ fingerprint: "button:Continue:0:0",
2306
+ rect: { x: 0, y: 0, width: 80, height: 30 },
2307
+ viewport: { width: 1280, height: 720 },
2308
+ screenshot: { data: "/9j/4AAQ", mediaType: "image/jpeg" },
2309
+ },
2310
+ },
2311
+ {
2312
+ version: SMITHERS_API_VERSION,
2313
+ method: "closeBrowserSession",
2314
+ title: "closeBrowserSession",
2315
+ description: "Close and wipe a browser session.",
2316
+ maturity: "stable",
2317
+ transport: "http+websocket",
2318
+ requiredScope: "run:write",
2319
+ requestSchema: objectSchema({ sessionId: stringSchema("Opaque browser session identifier.", 1) }, ["sessionId"]),
2320
+ responseSchema: objectSchema(
2321
+ {
2322
+ closed: booleanSchema("Whether the session is closed."),
2323
+ sessionId: stringSchema("Closed session identifier."),
2324
+ },
2325
+ ["closed"],
2326
+ ),
2327
+ errors: [
2328
+ "InvalidRequest",
2329
+ "Unauthorized",
2330
+ "Forbidden",
2331
+ "REVISION_CONFLICT",
2332
+ "SSRF_BLOCKED",
2333
+ "QUOTA_EXCEEDED",
2334
+ "Internal",
2335
+ ],
2336
+ exampleRequest: { sessionId: "s1" },
2337
+ exampleResponse: { closed: true, sessionId: "s1" },
2338
+ },
2339
+ {
2340
+ version: SMITHERS_API_VERSION,
2341
+ method: "listBrowserSessions",
2342
+ title: "listBrowserSessions",
2343
+ description: "List live browser sessions in this workspace.",
2344
+ maturity: "stable",
2345
+ transport: "http+websocket",
2346
+ requiredScope: "run:read",
2347
+ requestSchema: objectSchema({}, [], "No parameters."),
2348
+ responseSchema: arraySchema(browserSnapshotSchema, "Browser session snapshots."),
2349
+ errors: [
2350
+ "InvalidRequest",
2351
+ "Unauthorized",
2352
+ "Forbidden",
2353
+ "REVISION_CONFLICT",
2354
+ "SSRF_BLOCKED",
2355
+ "QUOTA_EXCEEDED",
2356
+ "Internal",
2357
+ ],
2358
+ exampleRequest: {},
2359
+ exampleResponse: [browserSnapshotExample],
2360
+ },
2361
+ ];
2362
+
2363
+ /** @type {Map<string, GatewayRpcDefinition>} */
2364
+ const definitionByMethod = new Map(
2365
+ GATEWAY_RPC_DEFINITIONS.map((definition) => /** @type {[string, GatewayRpcDefinition]} */ ([
2366
+ definition.method,
2367
+ definition,
2368
+ ])),
2369
+ );
2370
+
2371
+ /**
2372
+ * @param {string} method
2373
+ * @returns {GatewayRpcMethod | undefined}
2374
+ */
2375
+ export function canonicalGatewayRpcMethod(method) {
2376
+ if (definitionByMethod.has(method)) {
2377
+ return /** @type {GatewayRpcMethod} */ (method);
2378
+ }
2379
+ return Object.hasOwn(GATEWAY_RPC_LEGACY_METHOD_ALIASES, method)
2380
+ ? GATEWAY_RPC_LEGACY_METHOD_ALIASES[method]
2381
+ : undefined;
2382
+ }
2383
+
2384
+ /**
2385
+ * @param {string} method
2386
+ * @returns {GatewayRpcDefinition | undefined}
2387
+ */
2388
+ export function getGatewayRpcDefinition(method) {
2389
+ const canonical = canonicalGatewayRpcMethod(method);
2390
+ return canonical ? definitionByMethod.get(canonical) : undefined;
2391
+ }
2392
+
2393
+ /**
2394
+ * @param {string} method
2395
+ * @returns {GatewayScope | undefined}
2396
+ */
2397
+ export function getRequiredScopeForGatewayMethod(method) {
2398
+ // hasOwn (not plain indexing) so inherited Object.prototype keys such as
2399
+ // "toString" fall through to the catalog lookup instead of matching.
2400
+ if (Object.hasOwn(HTTP_ROUTE_SCOPES, method)) {
2401
+ return HTTP_ROUTE_SCOPES[method];
2402
+ }
2403
+ return getGatewayRpcDefinition(method)?.requiredScope;
2404
+ }
2405
+
2406
+ /**
2407
+ * @returns {readonly GatewayRpcMethod[]}
2408
+ */
2409
+ export function listGatewayRpcMethods() {
2410
+ return GATEWAY_RPC_DEFINITIONS.map((definition) => definition.method);
2411
+ }
2412
+
2413
+ /**
2414
+ * @param {string} method
2415
+ * @returns {method is GatewayRpcMethod}
2416
+ */
2417
+ export function isGatewayRpcMethod(method) {
2418
+ return definitionByMethod.has(method);
2419
+ }
2420
+
2421
+ /**
2422
+ * @returns {readonly GatewayScope[]}
2423
+ */
2424
+ export function getGatewayScopeValues() {
2425
+ return GATEWAY_SCOPE_VALUES;
2426
+ }