@sema-agent/sdk 2.1.4 → 2.2.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.
package/openapi.yaml CHANGED
@@ -210,6 +210,7 @@ paths:
210
210
  schema: { $ref: '#/components/schemas/TaskResult' }
211
211
  '400': { $ref: '#/components/responses/BadRequest' }
212
212
  '401': { $ref: '#/components/responses/Unauthorized' }
213
+ '403': { $ref: '#/components/responses/Forbidden' } # (declared 2026-08-01) 带 `sessionId` 复用他人会话:归属不匹配 ⇒ 403(自公开快照起点即有,spec 此前漏登记)。server 3.15.0 起多一个触发条件:多租部署上 session store 无归属判别能力时,复用**任何** caller 自报的 sessionId 都 403(含本 worker 自己刚铸的 —— 无 register seam 时归属从未被记下)。core 2.11.0 起默认内存店自带该能力 ⇒ 该触发条件在默认部署上不再出现。
213
214
  '409': { $ref: '#/components/responses/Conflict' }
214
215
  '429': { $ref: '#/components/responses/RateLimited' }
215
216
  '503': { $ref: '#/components/responses/Unauthorized' }
@@ -262,7 +263,16 @@ paths:
262
263
  application/json:
263
264
  schema: { $ref: '#/components/schemas/TaskList' }
264
265
  '401': { $ref: '#/components/responses/Unauthorized' }
265
- '429': { $ref: '#/components/responses/RateLimited' }
266
+ # 🔴 429 REMOVED 2026-07-31 — `routes/trace-usage.ts` has zero rate/quota/lease gates in the whole file
267
+ # (the only gates on this route are the fleet-wide 401 and the run-store 501 below); nothing can emit it.
268
+ '501':
269
+ description: >-
270
+ errorCode "capability.run_store_required" — the trace API needs a durable run store
271
+ (`DB_BACKEND=mysql|pg`). Added 2026-07-31: the guard has always been there (it fires before the
272
+ list/turns/stream/artifacts dispatch) but no trace path declared the code.
273
+ content:
274
+ application/json:
275
+ schema: { $ref: '#/components/schemas/ErrorResponse' }
266
276
 
267
277
  /v1/tasks/stream:
268
278
  parameters:
@@ -300,6 +310,7 @@ paths:
300
310
  schema: { $ref: '#/components/schemas/ErrorResponse' }
301
311
  '401': { $ref: '#/components/responses/Unauthorized' }
302
312
  '429': { $ref: '#/components/responses/RateLimited' }
313
+ '503': { $ref: '#/components/responses/Unauthorized' } # (declared 2026-07-31) this path is in server.ts's isBillableSubmitPath set — same three 503 doors as POST /v1/tasks and /v1/runs (no service token / draining / model-roster-pending)
303
314
 
304
315
  /v1/runs:
305
316
  parameters:
@@ -381,8 +392,12 @@ paths:
381
392
  '404': { $ref: '#/components/responses/NotFound' }
382
393
  '416':
383
394
  description: >
384
- Resume point evicted past retention. Match the STATUS 416 (there is NO `code` field); read
385
- `retainedFrom`, full-sync via `GET /v1/tasks/:id/turns`, then resume from `retainedFrom`.
395
+ Resume point evicted past retention. Body carries `errorCode: "limit.retention_evicted"`
396
+ (matching the 416 STATUS alone also works); read `retainedFrom`, full-sync via
397
+ `GET /v1/tasks/:id/turns`, then resume from `retainedFrom`.
398
+ (Corrected 2026-07-31: this said "there is NO `code` field". It was true before the server moved
399
+ this site onto the shared `sendError`, which emits `errorCode` unconditionally — a stale denial
400
+ that steered consumers away from a field that is in fact on the wire.)
386
401
  content:
387
402
  application/json:
388
403
  schema: { $ref: '#/components/schemas/ResumeEvicted' }
@@ -401,8 +416,12 @@ paths:
401
416
  (non-owner → 404, no existence leak; missing principal → 401). All responses are synchronous ACKs; actual
402
417
  termination is async (same-replica immediate; cross-replica ≈30s via the heartbeat tick). A cancelled run
403
418
  settles to `status` failed + `errorCode` "cancelled" (NO new status — distinguish via errorCode; the UI
404
- shows "cancelled", not an error). A SUSPENDED run has no running stream to abort → 409; cancel it by
405
- DENYING its approval instead. LIVE (service ced3d88).
419
+ shows "cancelled", not an error). A SUSPENDED (or `needs_review`) run IS cancellable here — the server
420
+ settles its pending checkpoint (CAS `expire`, the reaper's "≈ deny" terminal — deliberately not a
421
+ resume, since cancel means the run dies) and THEN terminalizes the row, releasing the session claim;
422
+ the ack is 202. (Corrected 2026-07-31: this paragraph used to say "409 — cancel it by DENYING its
423
+ approval instead". That was the pre-[868] behavior and it dead-ended when the deny itself was lost,
424
+ leaving the session locked with no release path.) LIVE (service ced3d88).
406
425
  responses:
407
426
  '202':
408
427
  description: >
@@ -414,7 +433,17 @@ paths:
414
433
  '401': { $ref: '#/components/responses/Unauthorized' }
415
434
  '404': { $ref: '#/components/responses/NotFound' }
416
435
  '409':
417
- description: Run is suspended (HITL) — cancel by denying its approval (`/v1/approvals/:sessionId/decide`), not here.
436
+ description: >-
437
+ Concurrency loss on the suspended path only: someone else settled the pending checkpoint first
438
+ (an approve/deny won the CAS). Re-read the run — it is probably already resuming or terminal.
439
+ NOT "suspended runs are uncancellable" — those are cancelled here and 202.
440
+ content:
441
+ application/json:
442
+ schema: { $ref: '#/components/schemas/ErrorResponse' }
443
+ '500':
444
+ description: >-
445
+ errorCode "internal.cancel_not_terminalized" — the checkpoint was settled but the run row could
446
+ not be terminalized after retries. Retry cancel; a stuck row is reaped after the stale window.
418
447
  content:
419
448
  application/json:
420
449
  schema: { $ref: '#/components/schemas/ErrorResponse' }
@@ -433,8 +462,12 @@ paths:
433
462
  design/80 D-A. The core of *supervision* (vs mere approve/deny): inject direction into a running run.
434
463
  AT-MOST-ONCE — steer is NOT idempotent (no retry). `trusted` is NOT a body field — the server derives it
435
464
  from the authenticated operator (the BFF's operator role); a network steer defaults to untrusted. A
436
- durable-`suspended` run → 409 `errorCode` "steering.not_running" (steer is not queued; resolve its
437
- approval instead). The pendingSteer freeze / close-tag guard is server-side; the client only sends text.
465
+ durable-`suspended` run → the steer is PARKED on the pending checkpoint and answered **202**
466
+ `{status:"suspended", delivery:"queued"}`; it is injected when the run resumes. (Corrected 2026-07-31:
467
+ this paragraph used to say "409 — steer is not queued; resolve its approval instead", which the server
468
+ stopped doing — `routes/runs.ts` parks it. 409 now only means the park LOST: the checkpoint resolved or
469
+ expired between the read and the CAS.) A worker with no checkpoint store answers 501 instead.
470
+ The pendingSteer freeze / close-tag guard is server-side; the client only sends text.
438
471
  requestBody:
439
472
  required: true
440
473
  content:
@@ -444,6 +477,14 @@ paths:
444
477
  required: [text]
445
478
  properties:
446
479
  text: { type: string, description: 'The steering direction (untrusted DATA; the server fences it).' }
480
+ priority:
481
+ type: string
482
+ enum: [now, next, later]
483
+ description: >-
484
+ 🔴 Declared 2026-07-31 (always read + enum-validated + echoed, never documented). The
485
+ handler 400s `request.field_invalid` on any other value and echoes the accepted one back
486
+ in the 200/202 body. It was mentioned only in prose under `Capabilities.fleet`, never in
487
+ this operation's own schema.
447
488
  mode:
448
489
  type: string
449
490
  enum: [all, one-at-a-time]
@@ -454,11 +495,18 @@ paths:
454
495
  per-call; the per-call mapping is pending D-A core.
455
496
  responses:
456
497
  '200': { description: Steer accepted and applied. }
457
- '202': { description: Steer accepted (queued to the next turn-boundary drain). }
498
+ '202':
499
+ description: >-
500
+ Steer accepted and QUEUED. Two shapes share this code: a live run queues to the next turn-boundary
501
+ drain; a durable-`suspended` run parks on its pending checkpoint
502
+ (`{status:"suspended", delivery:"queued"}`) and drains on resume.
458
503
  '401': { $ref: '#/components/responses/Unauthorized' }
459
504
  '404': { $ref: '#/components/responses/NotFound' }
460
505
  '409':
461
- description: 'errorCode "steering.not_running" — the run is suspended/terminal, not running (steer is not queued).'
506
+ description: >-
507
+ errorCode "steering.not_running" — the run is terminal, is running on ANOTHER replica
508
+ (cross-replica live-steer is not wired), or the durable park lost its CAS (the checkpoint
509
+ resolved/expired in between). NOT the ordinary suspended case — that one parks and 202s.
462
510
  content:
463
511
  application/json:
464
512
  schema: { $ref: '#/components/schemas/ErrorResponse' }
@@ -475,6 +523,7 @@ paths:
475
523
  content:
476
524
  application/json:
477
525
  schema: { $ref: '#/components/schemas/ErrorResponse' }
526
+ '429': { $ref: '#/components/responses/RateLimited' } # (declared 2026-07-31) rateLimited||quotaExceeded||leaseDenied all gate this verb (routes/runs.ts)
478
527
 
479
528
  /v1/runs/{taskId}/subagents/{target}/steer:
480
529
  parameters:
@@ -524,6 +573,7 @@ paths:
524
573
  schema: { $ref: '#/components/schemas/ErrorResponse' }
525
574
  '413':
526
575
  description: 'errorCode "steer.content_too_large" — content over the per-steer cap; shorten and resend (retrying verbatim never succeeds).'
576
+ '429': { $ref: '#/components/responses/RateLimited' } # (declared 2026-07-31) rateLimited||quotaExceeded||leaseDenied all gate this verb (routes/runs.ts)
527
577
  '501': { $ref: '#/components/responses/NotImplemented' }
528
578
 
529
579
  /v1/runs/{taskId}/compact:
@@ -569,6 +619,7 @@ paths:
569
619
  content:
570
620
  application/json:
571
621
  schema: { $ref: '#/components/schemas/ErrorResponse' }
622
+ '429': { $ref: '#/components/responses/RateLimited' } # (declared 2026-07-31) rateLimited||quotaExceeded||leaseDenied all gate this verb (routes/runs.ts)
572
623
  '501': { $ref: '#/components/responses/NotImplemented' }
573
624
 
574
625
  /v1/runs/{taskId}/detach:
@@ -613,6 +664,14 @@ paths:
613
664
  content:
614
665
  application/json:
615
666
  schema: { $ref: '#/components/schemas/ErrorResponse' }
667
+ '429':
668
+ description: >-
669
+ (declared 2026-07-31) errorCode "limit.rate_exceeded" only — `routes/runs.ts` gates this verb with
670
+ `rateLimited(req,res)` alone (no quota/lease gate; mutating but runs no model, parity with cancel).
671
+ `Retry-After` (seconds) set.
672
+ content:
673
+ application/json:
674
+ schema: { $ref: '#/components/schemas/ErrorResponse' }
616
675
  '501': { $ref: '#/components/responses/NotImplemented' }
617
676
 
618
677
  /v1/runs/{taskId}/subagents/{target}/output:
@@ -624,6 +683,17 @@ paths:
624
683
  required: true
625
684
  schema: { type: string }
626
685
  description: 'Background-agent handle (`a*` short form) under this parent run.'
686
+ - name: session
687
+ in: query
688
+ required: false
689
+ schema: { type: string }
690
+ description: >-
691
+ 🔴 Declared 2026-07-31 (it was ALWAYS read, never documented). For a session-bound run, a
692
+ non-operator caller MUST send the session id here — the handler compares it to the run's
693
+ `sessionId` and answers **404 "run not found"** on mismatch or absence, byte-identical to the
694
+ unknown-run 404. So a consumer following the old spec (principal + path params only) hit a
695
+ silent, indistinguishable 404. Operator/trace callers bypass this, as they do the owner gate;
696
+ a run with no sessionId is not conversation-bound and needs no assertion.
627
697
  get:
628
698
  tags: [runs]
629
699
  operationId: runsSubagentOutput
@@ -699,6 +769,7 @@ paths:
699
769
  content:
700
770
  application/json:
701
771
  schema: { $ref: '#/components/schemas/ErrorResponse' }
772
+ '429': { $ref: '#/components/responses/RateLimited' } # (declared 2026-07-31) rateLimited||quotaExceeded||leaseDenied all gate this verb (routes/runs.ts) — BILLABLE (starts model work)
702
773
  '501': { $ref: '#/components/responses/NotImplemented' }
703
774
 
704
775
  /v1/runs/{taskId}/subagents/{target}/stream:
@@ -710,6 +781,17 @@ paths:
710
781
  required: true
711
782
  schema: { type: string }
712
783
  description: 'Background-agent handle (`a*` short form) under this parent run.'
784
+ - name: session
785
+ in: query
786
+ required: false
787
+ schema: { type: string }
788
+ description: >-
789
+ 🔴 Declared 2026-07-31 (it was ALWAYS read, never documented). For a session-bound run, a
790
+ non-operator caller MUST send the session id here — the handler compares it to the run's
791
+ `sessionId` and answers **404 "run not found"** on mismatch or absence, byte-identical to the
792
+ unknown-run 404. So a consumer following the old spec (principal + path params only) hit a
793
+ silent, indistinguishable 404. Operator/trace callers bypass this, as they do the owner gate;
794
+ a run with no sessionId is not conversation-bound and needs no assertion.
713
795
  get:
714
796
  tags: [runs]
715
797
  operationId: runsSubagentStream
@@ -744,6 +826,17 @@ paths:
744
826
  required: true
745
827
  schema: { type: string }
746
828
  description: 'EXACT task handle only (`b*` background bash, monitor, background agent) — agent-name / legacy-shellId resolution is deliberately not on the wire (fail-closed 404).'
829
+ - name: session
830
+ in: query
831
+ required: false
832
+ schema: { type: string }
833
+ description: >-
834
+ 🔴 Declared 2026-07-31 (it was ALWAYS read, never documented). For a session-bound run, a
835
+ non-operator caller MUST send the session id here — the handler compares it to the run's
836
+ `sessionId` and answers **404 "run not found"** on mismatch or absence, byte-identical to the
837
+ unknown-run 404. So a consumer following the old spec (principal + path params only) hit a
838
+ silent, indistinguishable 404. Operator/trace callers bypass this, as they do the owner gate;
839
+ a run with no sessionId is not conversation-bound and needs no assertion.
747
840
  get:
748
841
  tags: [runs]
749
842
  operationId: runsTaskOutput
@@ -801,6 +894,11 @@ paths:
801
894
  content:
802
895
  application/json:
803
896
  schema: { $ref: '#/components/schemas/SubagentOutputResult' }
897
+ '400':
898
+ # (declared 2026-07-31) shares the path-decode + `?filter=` validation of the sibling output verb
899
+ # (both ride `taskVerbMatch` in routes/runs.ts) — request.path_malformed (malformed subagent-path
900
+ # percent-encoding) and request.param_unsupported (`?filter=` is refused on the wire, GET and stop alike).
901
+ $ref: '#/components/responses/BadRequest'
804
902
  '401': { $ref: '#/components/responses/Unauthorized' }
805
903
  '404': { $ref: '#/components/responses/NotFound' }
806
904
  '409':
@@ -808,6 +906,14 @@ paths:
808
906
  content:
809
907
  application/json:
810
908
  schema: { $ref: '#/components/schemas/ErrorResponse' }
909
+ '429':
910
+ description: >-
911
+ (declared 2026-07-31) errorCode "limit.rate_exceeded" only — `routes/runs.ts` gates this verb with
912
+ `rateLimited(req,res)` alone (mutating but runs no model, parity with detach). `Retry-After`
913
+ (seconds) set.
914
+ content:
915
+ application/json:
916
+ schema: { $ref: '#/components/schemas/ErrorResponse' }
811
917
  '501': { $ref: '#/components/responses/NotImplemented' }
812
918
 
813
919
  /v1/sessions:
@@ -835,6 +941,14 @@ paths:
835
941
  name: owner
836
942
  schema: { type: string }
837
943
  description: Fleet-wide credential only — scope the page to a tenant. Ignored for a principal caller.
944
+ - in: query
945
+ name: q
946
+ required: false
947
+ schema: { type: string, maxLength: 256 }
948
+ description: >-
949
+ 🔴 Declared 2026-07-31 (always read, never documented). Case-sensitive SUBSTRING filter over the
950
+ row's objective preview; the server trims and caps it at 256 chars. A consumer following the old
951
+ spec had no way to know the search face existed.
838
952
  responses:
839
953
  '200':
840
954
  description: Keyset page of session summaries.
@@ -950,6 +1064,10 @@ paths:
950
1064
  required: [deleted]
951
1065
  properties:
952
1066
  deleted: { type: boolean }
1067
+ '400':
1068
+ # (declared 2026-07-31) errorCode "request.id_invalid" — the path sessionId must decode to a
1069
+ # well-formed uuidv7 (`routes/sessions.ts`); a malformed id never reaches the store.
1070
+ $ref: '#/components/responses/BadRequest'
953
1071
  '401': { $ref: '#/components/responses/Unauthorized' }
954
1072
  '404': { $ref: '#/components/responses/NotFound' }
955
1073
  '409': { $ref: '#/components/responses/Conflict' }
@@ -1065,6 +1183,14 @@ paths:
1065
1183
  schema: { type: string }
1066
1184
  '401': { $ref: '#/components/responses/Unauthorized' }
1067
1185
  '404': { $ref: '#/components/responses/NotFound' }
1186
+ '500':
1187
+ description: >-
1188
+ (declared 2026-07-31) errorCode "internal.error" — `routes/sessions.ts`'s catch around the owner
1189
+ lookup / subscribe setup, sent only when it fires BEFORE the SSE `200` headers went out
1190
+ (`!res.headersSent`); once streaming has started a failure just ends the connection instead.
1191
+ content:
1192
+ application/json:
1193
+ schema: { $ref: '#/components/schemas/ErrorResponse' }
1068
1194
  '501': { $ref: '#/components/responses/NotImplemented' }
1069
1195
  '503': { description: 'Subscription connection cap exceeded — Retry-After set; fall back to polling /head.' }
1070
1196
 
@@ -1526,6 +1652,10 @@ paths:
1526
1652
  required: [rules]
1527
1653
  properties:
1528
1654
  rules: { $ref: '#/components/schemas/StoredSessionRules' }
1655
+ '400':
1656
+ # (declared 2026-07-31) errorCode "request.id_invalid" — GET and PUT share the SAME id-shape check
1657
+ # in `routes/sessions.ts` (uuidv7 required) ahead of the method branch; only PUT had this declared.
1658
+ $ref: '#/components/responses/BadRequest'
1529
1659
  '401': { $ref: '#/components/responses/Unauthorized' }
1530
1660
  '404': { $ref: '#/components/responses/NotFound' }
1531
1661
  '409': { $ref: '#/components/responses/Conflict' }
@@ -1805,7 +1935,10 @@ paths:
1805
1935
  2c session-sync PUSH Phase A — a SMALL metadata body (`entryIds, snapshots, policy, anchors, resolution?`); the
1806
1936
  entries stream in Phase B. The cloud classifies (§7), pre-checks blob presence (a missing referenced hash →
1807
1937
  422 `missing_blob` — PUT it first), guards the import lease + any active run (409), then mints a stagingId.
1808
- `identical` → `{ relation:"identical" }` with NO stagingId (skip Phase B); `fresh`/`fast_forward` →
1938
+ `identical` → `{ relation:"identical", basis, payloadVerified }` with NO stagingId (skip Phase B);
1939
+ `basis` is `"entry-ids+digest"` when the caller supplied a comparable digest and it matched, else
1940
+ `"entry-ids"` (id-set equality only) — see the `ImportStaged` schema, which has carried these two
1941
+ keys all along; only this prose said "one key". `fresh`/`fast_forward` →
1809
1942
  `{ stagingId, relation }`. 🔴 a `fork`/`stale` without `resolution:"overwrite-dst"` → 409
1810
1943
  `conflict` carrying the `relation` (the SDK surfaces SyncConflictError).
1811
1944
  requestBody:
@@ -1840,6 +1973,7 @@ paths:
1840
1973
  schema: { $ref: '#/components/schemas/SyncConflict' }
1841
1974
  '422': { $ref: '#/components/responses/UnprocessableEntity' }
1842
1975
  '501': { $ref: '#/components/responses/NotImplemented' }
1976
+ '503': { $ref: '#/components/responses/Unauthorized' } # (declared 2026-08-01) E6 兄弟守卫:无 service token + 未开 ALLOW_UNAUTHED_WRITES 时,这条 import 提交腿与 `PUT …/policy` 写的是同一个 policy store ⇒ 同门同码 `auth.service_token_required`(server 3.15.0 起)
1843
1977
 
1844
1978
  /v1/sessions/{sessionId}/sync/import/{stagingId}/entries:
1845
1979
  parameters:
@@ -1896,6 +2030,15 @@ paths:
1896
2030
  /v1/approvals:
1897
2031
  parameters:
1898
2032
  - $ref: '#/components/parameters/PrincipalHeader'
2033
+ - name: owner
2034
+ in: query
2035
+ required: false
2036
+ schema: { type: string }
2037
+ description: >-
2038
+ 🔴 Declared 2026-07-31 (always read, never documented). OPERATOR-only tenant scoping: an operator
2039
+ caller narrows the view to one owner with this key; for a non-operator the server forces the scope
2040
+ to the caller's own principal and ignores it. The sibling `/v1/assistant/inbox` has declared the
2041
+ same-named param all along — this face just never caught up.
1899
2042
  get:
1900
2043
  tags: [approvals]
1901
2044
  operationId: approvalsList
@@ -1917,14 +2060,27 @@ paths:
1917
2060
  /v1/approvals/stream:
1918
2061
  parameters:
1919
2062
  - $ref: '#/components/parameters/PrincipalHeader'
2063
+ - name: owner
2064
+ in: query
2065
+ required: false
2066
+ schema: { type: string }
2067
+ description: >-
2068
+ 🔴 Declared 2026-07-31 (always read, never documented). OPERATOR-only tenant scoping: an operator
2069
+ caller narrows the view to one owner with this key; for a non-operator the server forces the scope
2070
+ to the caller's own principal and ignores it. The sibling `/v1/assistant/inbox` has declared the
2071
+ same-named param all along — this face just never caught up.
1920
2072
  get:
1921
2073
  tags: [approvals]
1922
2074
  operationId: approvalsStream
1923
2075
  x-status: live # spec-path-gate 首批回填 2026-07-27(design/80 native push;SDK approvals.stream 消费)
1924
2076
  summary: SSE — pending-approval deltas (subscribe once instead of polling GET /v1/approvals).
1925
2077
  description: >
1926
- Frames: `meta` ({type,version,mode:"approvals-delta",pollMs}) then `pending`/`resolved` deltas (each
1927
- data payload mirrors its SSE event name in `data.type` — proxy-safe dispatch). Cross-replica by
2078
+ Frames: `meta` ({type,version,mode:"approvals-delta",pollMs}), then `synced` ({type,count}) once the
2079
+ first full snapshot has been emitted, then `pending`/`resolved` deltas; plus `heartbeat` ({type}) on the
2080
+ keep-alive tick and a terminal `error` ({type,errorCode:"STREAM_MAX_DURATION",…}) when the 15-min cap
2081
+ fires. (Corrected 2026-07-31: this list used to name only meta/pending/resolved — a consumer written to
2082
+ it silently drops three frame kinds it really receives.) Each data payload mirrors its SSE event name in
2083
+ `data.type` — proxy-safe dispatch. Cross-replica by
1928
2084
  construction (polls the SHARED checkpoint table). 15-min cap + heartbeats; a DB blip retries, never
1929
2085
  kills the stream. Scope = the caller's principal (operator/trace token = fleet-wide).
1930
2086
  responses:
@@ -1962,6 +2118,18 @@ paths:
1962
2118
  content:
1963
2119
  application/json:
1964
2120
  schema: { $ref: '#/components/schemas/ApprovalDecisionResult' }
2121
+ '400':
2122
+ description: >-
2123
+ (declared 2026-07-31) `routes/approvals-assistant.ts` has 9+ distinct 400 sites on this verb, all
2124
+ `ErrorResponse`-shaped: "request.path_malformed" (sessionId %-decode) · "request.body_shape"
2125
+ (`decision` missing/invalid, or a malformed `answer`) · "request.field_invalid" (`reason` not a
2126
+ string; `remember` not `"session"`; `checkpointToken`/`boundCallId`/`boundInputHash` not a string)
2127
+ · "request.field_conflict" (`remember` without `decision:"approve"`) · "remember_not_in_proof" /
2128
+ "updated_input_not_in_proof" (`remember` / `updatedInput` on a direct-door worker — not covered by
2129
+ the decision proof) · "remember_requires_binding" (`remember` without the D-1 binding echo).
2130
+ content:
2131
+ application/json:
2132
+ schema: { $ref: '#/components/schemas/ErrorResponse' }
1965
2133
  '401': { $ref: '#/components/responses/Unauthorized' }
1966
2134
  '404': { $ref: '#/components/responses/NotFound' }
1967
2135
  '409':
@@ -1971,17 +2139,19 @@ paths:
1971
2139
  NOT match the current pending action (TOCTOU/swap). The SDK splits these by `errorCode` into
1972
2140
  ConflictError vs ApprovalBindingMismatchError. binding_mismatch = SAFETY signal: refetch + re-present
1973
2141
  to the human, NEVER auto-retry.
2142
+ THIRD member (moved here from a documented-but-nonexistent 410, 2026-07-31): `approval_stale`
2143
+ (`errorCode`; `terminal:"resolved"` when attributable, else the key is absent) — the checkpoint is
2144
+ already terminal (someone resolved it / abandonment-TTL GC / SLA resolve-deny). Client action:
2145
+ refetch the inbox; this pending is gone. SDK → ApprovalStaleError. Note the bare CAS 409 has NO
2146
+ `errorCode`, which is exactly how the three are told apart.
1974
2147
  content:
1975
2148
  application/json:
1976
2149
  schema: { $ref: '#/components/schemas/ErrorResponse' }
1977
- '410':
1978
- description: >
1979
- design/80 D-1 `approval_stale` (`errorCode`, `terminal`: resolved|expired_abort|expired_sla) — the
1980
- checkpoint is already terminal (someone resolved it / abandonment-TTL GC / SLA resolve-deny). Client
1981
- action: refetch the inbox; this pending is gone. SDK → ApprovalStaleError.
1982
- content:
1983
- application/json:
1984
- schema: { $ref: '#/components/schemas/ErrorResponse' }
2150
+ # 🔴 REMOVED 2026-07-31 — the 410 that never was. `grep -rn "\b410\b" src/` across the whole server
2151
+ # is ZERO hits: `approval_stale` ships as a **409** carrying `errorCode: "approval_stale"` (the CAS
2152
+ # 409 stays bare → ConflictError, so the two 409s never collide). The `terminal` enum values
2153
+ # `expired_abort`/`expired_sla` are likewise never assigned — only `"resolved"`, or the key is absent.
2154
+ # A consumer branching on 410 to detect staleness had a branch that could not fire; see the 409 below.
1985
2155
  '413':
1986
2156
  description: >
1987
2157
  errorCode "reason_too_large" — `reason` exceeded 4096 chars (same cap on plan_review). REJECTED,
@@ -2017,6 +2187,41 @@ paths:
2017
2187
  content:
2018
2188
  application/json:
2019
2189
  schema: { $ref: '#/components/schemas/ExemptionList' }
2190
+ '400':
2191
+ description: >-
2192
+ (declared 2026-07-31) errorCode "request.path_malformed" — the sessionId (or a present `:toolName`
2193
+ segment) carried a malformed %-sequence (`decodeURIComponent` threw), shared decode gate in
2194
+ `routes/approvals-assistant.ts` ahead of GET/DELETE.
2195
+ content:
2196
+ application/json:
2197
+ schema: { $ref: '#/components/schemas/ErrorResponse' }
2198
+ '401': { $ref: '#/components/responses/Unauthorized' }
2199
+ '404': { $ref: '#/components/responses/NotFound' }
2200
+ '501':
2201
+ description: Worker has no exemption store backend (DB_BACKEND / LOCAL lane required).
2202
+ content:
2203
+ application/json:
2204
+ schema: { $ref: '#/components/schemas/ErrorResponse' }
2205
+ delete:
2206
+ tags: [approvals]
2207
+ operationId: approvalsExemptionsRouteUnsupported
2208
+ x-status: live # (declared 2026-07-31) same `em` regex as the sibling per-tool DELETE also matches this shorter path (no `:toolName`) — approvals-assistant.ts.
2209
+ summary: 'DELETE without a tool name — always 400 (use /exemptions/{toolName} instead).'
2210
+ description: >
2211
+ The route regex behind `GET /v1/approvals/{sessionId}/exemptions` also accepts `DELETE` on this
2212
+ SAME (shorter) path — `routes/approvals-assistant.ts` runs the identical store/decode/owner gates
2213
+ as the GET above (so a bad store → 501, a malformed %-encoded id → 400, an unknown/non-owned session
2214
+ → 404 all still apply first), then — because this path has no `:toolName` segment to match — it
2215
+ deterministically hits `request.route_unsupported` ("DELETE needs /exemptions/:toolName"). This
2216
+ operation therefore never returns a 2xx; callers should use `DELETE /v1/approvals/{sessionId}/exemptions/{toolName}`.
2217
+ responses:
2218
+ '400':
2219
+ description: >-
2220
+ errorCode "request.route_unsupported" — "DELETE needs /exemptions/:toolName" (deterministic once
2221
+ past the store/decode/owner gates below); OR "request.path_malformed" from the shared decode gate.
2222
+ content:
2223
+ application/json:
2224
+ schema: { $ref: '#/components/schemas/ErrorResponse' }
2020
2225
  '401': { $ref: '#/components/responses/Unauthorized' }
2021
2226
  '404': { $ref: '#/components/responses/NotFound' }
2022
2227
  '501':
@@ -2060,6 +2265,14 @@ paths:
2060
2265
  required: [revoked]
2061
2266
  properties:
2062
2267
  revoked: { const: true }
2268
+ '400':
2269
+ description: >-
2270
+ (declared 2026-07-31) errorCode "request.path_malformed" — sessionId or toolName carried a
2271
+ malformed %-sequence (shared decode gate in `routes/approvals-assistant.ts`, same as the sibling
2272
+ GET .../exemptions).
2273
+ content:
2274
+ application/json:
2275
+ schema: { $ref: '#/components/schemas/ErrorResponse' }
2063
2276
  '401': { $ref: '#/components/responses/Unauthorized' }
2064
2277
  '404': { $ref: '#/components/responses/NotFound' }
2065
2278
  '501':
@@ -2253,6 +2466,7 @@ paths:
2253
2466
  schema: { $ref: '#/components/schemas/TurnList' }
2254
2467
  '401': { $ref: '#/components/responses/Unauthorized' }
2255
2468
  '404': { $ref: '#/components/responses/NotFound' }
2469
+ '501': { $ref: '#/components/responses/NotImplemented' } # (declared 2026-07-31) `routes/trace-usage.ts`'s `!deps.runStore` guard fires before the /turns|/stream|/artifacts sub-dispatch — capability.run_store_required
2256
2470
 
2257
2471
  /v1/tasks/source-summary:
2258
2472
  parameters:
@@ -2348,6 +2562,7 @@ paths:
2348
2562
  schema: { $ref: '#/components/schemas/TraceStreamEvent' }
2349
2563
  '401': { $ref: '#/components/responses/Unauthorized' }
2350
2564
  '404': { $ref: '#/components/responses/NotFound' }
2565
+ '501': { $ref: '#/components/responses/NotImplemented' } # (declared 2026-07-31) `routes/trace-usage.ts`'s `!deps.runStore` guard fires before the /turns|/stream|/artifacts sub-dispatch — capability.run_store_required
2351
2566
 
2352
2567
  /v1/usage:
2353
2568
  parameters:
@@ -2380,7 +2595,10 @@ paths:
2380
2595
  summary: Windowed usage totals (usage-analytics face).
2381
2596
  description: >
2382
2597
  READ-ONLY analytics aggregate over the caller's principal (operator/trace token = fleet-wide). Query:
2383
- `from`/`to` (ISO), `owner` (fleet-wide callers only). Response is the analytics aggregate — an OPEN
2598
+ `from`/`to` (ISO), `principal` (fleet-wide callers only — the handler reads this key; a JWT subject is
2599
+ forced to itself and the param is ignored). (Corrected 2026-07-31: this said `owner`, which the handler
2600
+ never reads — a hand-rolled caller sending `?owner=` got UNFILTERED results with no error. The shipped
2601
+ SDK client has always sent `principal`.) Response is the analytics aggregate — an OPEN
2384
2602
  object (additive fields ride; consumers branch on known keys only).
2385
2603
  responses:
2386
2604
  '200':
@@ -2627,7 +2845,9 @@ paths:
2627
2845
  application/json:
2628
2846
  schema: { type: object, additionalProperties: true }
2629
2847
  '401': { $ref: '#/components/responses/Unauthorized' }
2630
- '501': { description: side-query not enabled on this worker. }
2848
+ # 🔴 REMOVED 2026-07-31 — `routes/side-query.ts` has no 501 branch at all, and
2849
+ # `routes/capabilities.ts` reports `sideQuery: true` as a hardcoded constant (no deployment flag
2850
+ # gates this route). The documented "not enabled on this worker" case is unreachable.
2631
2851
 
2632
2852
  /v1/workflows:
2633
2853
  parameters:
@@ -2653,6 +2873,13 @@ paths:
2653
2873
  required: false
2654
2874
  schema: { type: integer, minimum: 1, maximum: 100, default: 50 }
2655
2875
  description: Max rows (server clamps to 1..100; default 50).
2876
+ - in: query
2877
+ name: session
2878
+ required: false
2879
+ schema: { type: string }
2880
+ description: >-
2881
+ 🔴 Declared 2026-07-31 (always read, never documented). Filters the rows to one session — a
2882
+ consumer following the old spec (status/limit only) silently got the whole scope back.
2656
2883
  responses:
2657
2884
  '200':
2658
2885
  description: Owner's workflow-run summaries.
@@ -2661,6 +2888,7 @@ paths:
2661
2888
  schema:
2662
2889
  type: object
2663
2890
  required: [workflows]
2891
+ additionalProperties: false
2664
2892
  properties:
2665
2893
  workflows:
2666
2894
  type: array
@@ -2737,6 +2965,13 @@ paths:
2737
2965
  application/json:
2738
2966
  schema: { $ref: '#/components/schemas/AttachmentInfo' }
2739
2967
  '400': { description: 'missing/unsafe name, malformed mime' }
2968
+ '401':
2969
+ description: >-
2970
+ (declared 2026-07-31) errorCode "auth.principal_required" — `routes/attachments.ts` requires a
2971
+ principal ahead of the method dispatch when `requirePrincipal` is on.
2972
+ content:
2973
+ application/json:
2974
+ schema: { $ref: '#/components/schemas/ErrorResponse' }
2740
2975
  '413': { description: 'attachment.too_large' }
2741
2976
  '415': { description: 'attachment.mime_not_allowed' }
2742
2977
  '501': { description: 'attachment store not configured' }
@@ -2754,7 +2989,15 @@ paths:
2754
2989
  summary: Download an attachment's bytes (owner-gated, no oracle — unknown and foreign both 404).
2755
2990
  responses:
2756
2991
  '200': { description: 'bytes; content-type = stored mime; content-disposition carries the sanitized name.' }
2992
+ '401':
2993
+ description: >-
2994
+ (declared 2026-07-31) errorCode "auth.principal_required" — `routes/attachments.ts` gates the
2995
+ store-presence check and the principal requirement ahead of the GET/DELETE method dispatch.
2996
+ content:
2997
+ application/json:
2998
+ schema: { $ref: '#/components/schemas/ErrorResponse' }
2757
2999
  '404': { description: 'unknown or not owned' }
3000
+ '501': { description: '(declared 2026-07-31) errorCode "capability.attachment_store_required" — no attachment store configured.' }
2758
3001
  delete:
2759
3002
  tags: [tasks]
2760
3003
  operationId: deleteAttachment
@@ -2762,10 +3005,33 @@ paths:
2762
3005
  summary: Delete an attachment (owner-gated; 204 on success).
2763
3006
  responses:
2764
3007
  '204': { description: 'deleted' }
3008
+ '401':
3009
+ description: >-
3010
+ (declared 2026-07-31) errorCode "auth.principal_required" — same shared gate as the GET (store
3011
+ presence + principal requirement) ahead of the method dispatch.
3012
+ content:
3013
+ application/json:
3014
+ schema: { $ref: '#/components/schemas/ErrorResponse' }
2765
3015
  '404': { description: 'unknown or not owned' }
3016
+ '501': { description: '(declared 2026-07-31) errorCode "capability.attachment_store_required" — no attachment store configured.' }
2766
3017
  /v1/fleet/stream:
2767
3018
  parameters:
2768
3019
  - $ref: '#/components/parameters/PrincipalHeader'
3020
+ - name: session
3021
+ in: query
3022
+ required: false
3023
+ schema: { type: string }
3024
+ description: >-
3025
+ 🔴 Declared 2026-07-31 (always read, never documented). Scopes row visibility / content scrubbing
3026
+ to one session.
3027
+ - name: observe
3028
+ in: query
3029
+ required: false
3030
+ schema: { type: string, enum: ['1'] }
3031
+ description: >-
3032
+ 🔴 Declared 2026-07-31 (always read, never documented). `observe=1` opens the stream WITHOUT the
3033
+ durable notification park-fencing (the workflow-completion inbox is not passed) — an observer view
3034
+ that must not consume another consumer's parked notifications.
2769
3035
  get:
2770
3036
  tags: [fleet]
2771
3037
  operationId: fleetStream
@@ -2804,7 +3070,10 @@ paths:
2804
3070
  `LEADER_ENABLED` (default OFF) + `REMOTE_EXEC=e2b`. Mode is a SERVER deploy flag, not a client choice:
2805
3071
  `LEADER_FANOUT_ENABLED=true` (default) lets the router fan out to N isolated workers (= the value-HOLD
2806
3072
  correctness bet); `=false` pins a single worker. Either way it runs the E2B+git pipeline. Door B's brain
2807
- is the strong single agent via `/v1/tasks`+`/v1/runs`+`scenario`. When disabled → 501.
3073
+ is the strong single agent via `/v1/tasks`+`/v1/runs`+`scenario`. When disabled (`LEADER_ENABLED` off)
3074
+ the route is simply NOT REGISTERED → the generic **404** `not_found.route`, not 501.
3075
+ (Corrected 2026-07-31: "When disabled → 501" was never true — no code path emits 501 here; a consumer
3076
+ branching on 501 to detect "leader unavailable" never fires.)
2808
3077
  requestBody:
2809
3078
  required: true
2810
3079
  content:
@@ -2824,7 +3093,13 @@ paths:
2824
3093
  application/json:
2825
3094
  schema: { $ref: '#/components/schemas/LeaderReceipt' }
2826
3095
  '401': { $ref: '#/components/responses/Unauthorized' }
2827
- '501': { $ref: '#/components/responses/NotImplemented' }
3096
+ '404':
3097
+ description: >-
3098
+ errorCode "not_found.route" — the leader lane is disabled on this worker, so the route is not
3099
+ registered at all. (Was documented as 501 until 2026-07-31; no code path ever emitted that.)
3100
+ content:
3101
+ application/json:
3102
+ schema: { $ref: '#/components/schemas/ErrorResponse' }
2828
3103
 
2829
3104
  /v1/leader/{leaderRunId}:
2830
3105
  parameters:
@@ -2879,7 +3154,7 @@ paths:
2879
3154
  schema:
2880
3155
  type: object
2881
3156
  required: [images]
2882
- additionalProperties: true
3157
+ additionalProperties: false
2883
3158
  properties:
2884
3159
  images:
2885
3160
  type: array
@@ -2897,9 +3172,17 @@ paths:
2897
3172
  summary: 'OPERATOR: register one image-index entry → { id }.'
2898
3173
  requestBody: { required: true, content: { application/json: { schema: { type: object, additionalProperties: true } } } }
2899
3174
  responses:
2900
- '200': { description: '{ id }.', content: { application/json: { schema: { type: object, required: [id], properties: { id: { type: string } }, additionalProperties: true } } } }
3175
+ '200': { description: '{ id }.', content: { application/json: { schema: { type: object, required: [id], properties: { id: { type: string } }, additionalProperties: false } } } }
2901
3176
  '400': { $ref: '#/components/responses/BadRequest' }
2902
3177
  '401': { $ref: '#/components/responses/Unauthorized' }
3178
+ '403':
3179
+ description: >-
3180
+ (declared 2026-07-31) errorCode "auth.operator_only" — EXPLICIT operator gate (not the bare
3181
+ isOperator, whose empty-OPERATOR_PRINCIPALS "true-for-all" would make this index-mutating door
3182
+ world-writable).
3183
+ content:
3184
+ application/json:
3185
+ schema: { $ref: '#/components/schemas/ErrorResponse' }
2903
3186
 
2904
3187
  /v1/images/{profile}:
2905
3188
  parameters:
@@ -2911,7 +3194,9 @@ paths:
2911
3194
  x-status: live
2912
3195
  summary: 'Newest PUBLISHED entry for a profile → { image }.'
2913
3196
  responses:
2914
- '200': { description: '{ image: entry } (open row shape).', content: { application/json: { schema: { type: object, additionalProperties: true } } } }
3197
+ # 🔴 census 批2 四段:this arm was a bare open `{}` (no `image` key even declared) — the handler's real
3198
+ # body is `{ image: entry }` (images.ts:478-484), entry = the SAME ImageIndexEntry as the list route.
3199
+ '200': { description: '{ image: entry }.', content: { application/json: { schema: { type: object, required: [image], additionalProperties: false, properties: { image: { $ref: '#/components/schemas/ImageIndexEntry' } } } } } }
2915
3200
  '404': { $ref: '#/components/responses/NotFound' }
2916
3201
 
2917
3202
  /v1/images/digests/{digest}:
@@ -2924,7 +3209,8 @@ paths:
2924
3209
  x-status: live
2925
3210
  summary: 'Entry by immutable digest → { image }.'
2926
3211
  responses:
2927
- '200': { description: '{ image: entry }.', content: { application/json: { schema: { type: object, additionalProperties: true } } } }
3212
+ # 🔴 census 批2 四段:same open-`{}` gap as imagesByProfile — real body `{ image: entry }` (images.ts:466-472).
3213
+ '200': { description: '{ image: entry }.', content: { application/json: { schema: { type: object, required: [image], additionalProperties: false, properties: { image: { $ref: '#/components/schemas/ImageIndexEntry' } } } } } }
2928
3214
  '404': { $ref: '#/components/responses/NotFound' }
2929
3215
 
2930
3216
  /v1/images/bakes:
@@ -2940,11 +3226,31 @@ paths:
2940
3226
  busy ⇒ 409 {activeBakeId?, eventsUrl?}; submission rate-limit ⇒ 429 + retryAfterSec.
2941
3227
  requestBody: { required: true, content: { application/json: { schema: { type: object, additionalProperties: true } } } }
2942
3228
  responses:
2943
- '202': { description: '{ bakeId, eventsUrl, status, state? }.', content: { application/json: { schema: { type: object, additionalProperties: true } } } }
3229
+ '202': { description: '{ bakeId, eventsUrl, status, state }.', content: { application/json: { schema: { $ref: '#/components/schemas/BakeSubmitAck' } } } }
2944
3230
  '400': { $ref: '#/components/responses/BadRequest' }
2945
3231
  '401': { $ref: '#/components/responses/Unauthorized' }
3232
+ '403':
3233
+ description: (declared 2026-07-31) errorCode "auth.operator_only" — EXPLICIT operator gate (§P2.4a).
3234
+ content:
3235
+ application/json:
3236
+ schema: { $ref: '#/components/schemas/ErrorResponse' }
2946
3237
  '409': { description: 'Busy — { activeBakeId?, eventsUrl? }.' }
2947
3238
  '429': { description: 'Submission rate-limited (retryAfterSec).' }
3239
+ '501':
3240
+ description: >-
3241
+ (declared 2026-07-31) errorCode "capability.unavailable" — the requested bake shape is not open
3242
+ yet (custom/tenant P4), from the input-whitelist validator (`validateBake`).
3243
+ content:
3244
+ application/json:
3245
+ schema: { $ref: '#/components/schemas/ErrorResponse' }
3246
+ '503':
3247
+ description: >-
3248
+ (declared 2026-07-31) errorCode "state.no_sandbox_base" — a real (non-dryRun) build has no
3249
+ concrete `sandbox-base` to build FROM (set `SANDBOX_BASE_REF` or publish one); a `dryRun` request
3250
+ is resolve-only and is exempt.
3251
+ content:
3252
+ application/json:
3253
+ schema: { $ref: '#/components/schemas/ErrorResponse' }
2948
3254
 
2949
3255
  /v1/images/bakes/claim:
2950
3256
  parameters:
@@ -2955,9 +3261,16 @@ paths:
2955
3261
  x-status: live # bake-runner credential face (machine-to-machine).
2956
3262
  summary: 'RUNNER: claim the oldest queued bake (discover+CAS in one step; 204 = queue empty).'
2957
3263
  responses:
2958
- '200': { description: 'BakeClaim { bakeId, argv, push, dryRun, logs, profile, bands, ingestSecret, leaseUntil }.', content: { application/json: { schema: { type: object, additionalProperties: true } } } }
3264
+ '200': { description: 'BakeClaim { bakeId, argv, push, dryRun, logs, profile, bands, ingestSecret, leaseUntil }.', content: { application/json: { schema: { $ref: '#/components/schemas/BakeClaim' } } } }
2959
3265
  '204': { description: 'Queue empty.' }
2960
- '401': { $ref: '#/components/responses/Unauthorized' }
3266
+ '403':
3267
+ description: >-
3268
+ errorCode "auth.bake_runner_only" — the caller is not the bake runner. (Was documented as 401
3269
+ until 2026-07-31; the handler has always sent 403 with this code, so a consumer keyed on 401
3270
+ never matched.)
3271
+ content:
3272
+ application/json:
3273
+ schema: { $ref: '#/components/schemas/ErrorResponse' }
2961
3274
 
2962
3275
  /v1/images/bakes/{bakeId}:
2963
3276
  parameters:
@@ -2967,9 +3280,14 @@ paths:
2967
3280
  tags: [sandbox]
2968
3281
  operationId: bakesGet
2969
3282
  x-status: live
2970
- summary: 'One bake row → { bake } (open view: status/state/profile/bands/logs/digest…).'
3283
+ summary: 'One bake row → { bake } (status/state/profile/bands/logs/digest…).'
2971
3284
  responses:
2972
- '200': { description: '{ bake }.', content: { application/json: { schema: { type: object, additionalProperties: true } } } }
3285
+ '200': { description: '{ bake }.', content: { application/json: { schema: { type: object, required: [bake], additionalProperties: false, properties: { bake: { $ref: '#/components/schemas/BakeRecordView' } } } } } }
3286
+ '403':
3287
+ description: (declared 2026-07-31) errorCode "auth.operator_only" — EXPLICIT operator gate.
3288
+ content:
3289
+ application/json:
3290
+ schema: { $ref: '#/components/schemas/ErrorResponse' }
2973
3291
  '404': { $ref: '#/components/responses/NotFound' }
2974
3292
 
2975
3293
  /v1/images/bakes/{bakeId}/cancel:
@@ -2980,9 +3298,14 @@ paths:
2980
3298
  tags: [sandbox]
2981
3299
  operationId: bakesCancel
2982
3300
  x-status: live
2983
- summary: 'OPERATOR: request cancel → 202 { bakeId, cancelRequested: true, note? } (flag only; the runner kills the build on its next heartbeat; terminal rows = honest no-op note).'
3301
+ summary: 'OPERATOR: request cancel → 202 { bakeId, cancelRequested: true, note } (flag only; the runner kills the build on its next heartbeat; terminal rows = honest no-op note).'
2984
3302
  responses:
2985
- '202': { description: '{ bakeId, cancelRequested, note? }.', content: { application/json: { schema: { type: object, additionalProperties: true } } } }
3303
+ '202': { description: '{ bakeId, cancelRequested, note }.', content: { application/json: { schema: { $ref: '#/components/schemas/BakeCancelAck' } } } }
3304
+ '403':
3305
+ description: (declared 2026-07-31) errorCode "auth.operator_only" — EXPLICIT operator gate.
3306
+ content:
3307
+ application/json:
3308
+ schema: { $ref: '#/components/schemas/ErrorResponse' }
2986
3309
  '404': { $ref: '#/components/responses/NotFound' }
2987
3310
 
2988
3311
  /v1/images/bakes/{bakeId}/claim:
@@ -2995,7 +3318,12 @@ paths:
2995
3318
  x-status: live
2996
3319
  summary: 'RUNNER: claim THIS queued bake (single-flight CAS; losing the race ⇒ 409).'
2997
3320
  responses:
2998
- '200': { description: 'BakeClaim (same shape as claim-next).', content: { application/json: { schema: { type: object, additionalProperties: true } } } }
3321
+ '200': { description: 'BakeClaim (same shape as claim-next).', content: { application/json: { schema: { $ref: '#/components/schemas/BakeClaim' } } } }
3322
+ '403':
3323
+ description: (declared 2026-07-31) errorCode "auth.bake_runner_only" — the caller is not the bake runner (same code as the sibling claim-next).
3324
+ content:
3325
+ application/json:
3326
+ schema: { $ref: '#/components/schemas/ErrorResponse' }
2999
3327
  '409': { description: 'Claim race lost / not queued.' }
3000
3328
 
3001
3329
  /v1/images/bakes/{bakeId}/ingest:
@@ -3007,10 +3335,19 @@ paths:
3007
3335
  operationId: bakesIngest
3008
3336
  x-status: live
3009
3337
  summary: 'RUNNER: push one build.sh event line + lease heartbeat (x-bake-ingest-secret header from the claim).'
3010
- description: 'line = {event, …}; event=heartbeat renews the lease only. Response always carries { accepted, leaseValid, cancelRequested } — 409 = lease lost/stolen; cancelRequested=true ⇒ the runner should kill the build.'
3338
+ description: 'line = {event, …}; event=heartbeat renews the lease only. 409 = lease lost/stolen; cancelRequested=true ⇒ the runner should kill the build.'
3011
3339
  requestBody: { required: true, content: { application/json: { schema: { type: object, additionalProperties: true } } } }
3012
3340
  responses:
3013
- '200': { description: '{ accepted, leaseValid, cancelRequested }.', content: { application/json: { schema: { type: object, additionalProperties: true } } } }
3341
+ '200': { description: 'One of four shapes (heartbeat / non-terminal append / terminal done-success / terminal done-needs-flip) — see BakeIngestAck.', content: { application/json: { schema: { $ref: '#/components/schemas/BakeIngestAck' } } } }
3342
+ '403':
3343
+ description: >-
3344
+ (declared 2026-07-31) two distinct causes share this status: errorCode "auth.bake_runner_only"
3345
+ (the caller's credential isn't the configured bake-runner principal) and
3346
+ "auth.ingest_secret_invalid" (the `x-bake-ingest-secret` header is missing or doesn't match the
3347
+ per-bake secret minted at claim).
3348
+ content:
3349
+ application/json:
3350
+ schema: { $ref: '#/components/schemas/ErrorResponse' }
3014
3351
  '409': { description: 'Lease invalid / claimed by another runner.' }
3015
3352
 
3016
3353
  /v1/images/bakes/{bakeId}/events:
@@ -3021,9 +3358,14 @@ paths:
3021
3358
  tags: [sandbox]
3022
3359
  operationId: bakesEvents
3023
3360
  x-status: live
3024
- summary: 'Bake live SSE (run-trace-SSE framing; RESUMABLE — `id:` lines carry the event seq, Last-Event-ID resumes).'
3361
+ summary: 'OPERATOR: Bake live SSE (run-trace-SSE framing; RESUMABLE — `id:` lines carry the event seq, Last-Event-ID resumes).'
3025
3362
  responses:
3026
3363
  '200': { description: 'SSE stream.', content: { text/event-stream: { schema: { type: string } } } }
3364
+ '403':
3365
+ description: (declared 2026-07-31) errorCode "auth.operator_only" — EXPLICIT operator gate (this summary previously did not mention the restriction at all).
3366
+ content:
3367
+ application/json:
3368
+ schema: { $ref: '#/components/schemas/ErrorResponse' }
3027
3369
  '404': { $ref: '#/components/responses/NotFound' }
3028
3370
 
3029
3371
  /v1/images/select:
@@ -3097,14 +3439,19 @@ paths:
3097
3439
  The approval row carries the FULL tool-call args (commands/paths/contents of a high-risk op), so the
3098
3440
  by-id read enforces the same tenant boundary as the list: operators read across tenants; everyone else
3099
3441
  reads ONLY their own scope (the guard fires at the SQL layer — no cross-tenant args even if an id
3100
- leaks). Non-owner → 404 (never 403, no existence oracle). Present only on deployments with the
3101
- D-lane approval store; the checkpoint-lane decide verb is `/v1/approvals/{sessionId}/decide`.
3442
+ leaks). Non-owner → 404 (never 403, no existence oracle).
3443
+ 🔴 AVAILABILITY (corrected 2026-07-31): present only on deployments that have the D-lane approval store
3444
+ **AND NOT the checkpoint store**. When `DURABLE_APPROVAL` is on, the checkpoint lane takes precedence
3445
+ for the whole `/v1/approvals*` prefix and this by-id route answers 404 `not_found.route` — deliberate,
3446
+ not a bug; decide via `/v1/approvals/{sessionId}/decide` and list via `GET /v1/approvals`.
3447
+ The old wording ("present only on deployments with the D-lane approval store") named one condition of
3448
+ two, so a both-stores worker looked like it should serve this route.
3102
3449
  responses:
3103
3450
  '200':
3104
3451
  description: The approval row (full args projection).
3105
3452
  content:
3106
3453
  application/json:
3107
- schema: { type: object, additionalProperties: true }
3454
+ schema: { $ref: '#/components/schemas/ApprovalRow' }
3108
3455
  '401': { $ref: '#/components/responses/Unauthorized' }
3109
3456
  '404': { $ref: '#/components/responses/NotFound' }
3110
3457
 
@@ -3135,11 +3482,14 @@ paths:
3135
3482
  application/json:
3136
3483
  schema:
3137
3484
  type: object
3485
+ # 🔴 census 批2 五段:CLOSED — exact literal `{ scope, exportedAt, entries }`
3486
+ # (memory-policy.ts:58), no fourth key.
3138
3487
  required: [scope, exportedAt, entries]
3488
+ additionalProperties: false
3139
3489
  properties:
3140
3490
  scope: { type: string }
3141
3491
  exportedAt: { type: string, format: date-time }
3142
- entries: { type: array, items: { type: object, additionalProperties: true } }
3492
+ entries: { type: array, items: { $ref: '#/components/schemas/MemoryEntry' } }
3143
3493
  '400': { $ref: '#/components/responses/BadRequest' }
3144
3494
  '401': { $ref: '#/components/responses/Unauthorized' }
3145
3495
  '404': { $ref: '#/components/responses/NotFound' }
@@ -3175,7 +3525,7 @@ paths:
3175
3525
  description: 'The sync outcome (core reconcile/nextSyncBaseline projection).'
3176
3526
  content:
3177
3527
  application/json:
3178
- schema: { type: object, additionalProperties: true }
3528
+ schema: { $ref: '#/components/schemas/MemorySyncResponse' }
3179
3529
  '400': { $ref: '#/components/responses/BadRequest' }
3180
3530
  '401': { $ref: '#/components/responses/Unauthorized' }
3181
3531
  '404': { $ref: '#/components/responses/NotFound' }
@@ -3211,7 +3561,15 @@ paths:
3211
3561
  description: Aggregation rows.
3212
3562
  content:
3213
3563
  application/json:
3214
- schema: { type: object, additionalProperties: true }
3564
+ schema:
3565
+ type: object
3566
+ # 🔴 census 批2 五段:path↔schema 脱钩修(same class as the earlier BakeClaim/usage-triplet
3567
+ # finds) — `OutcomeRow` already existed named+correct (observability.ts:51's exact
3568
+ # `{ outcomes: rows }`) but this path never `$ref`'d it, declaring a bare open `{}` instead.
3569
+ required: [outcomes]
3570
+ additionalProperties: false
3571
+ properties:
3572
+ outcomes: { type: array, items: { $ref: '#/components/schemas/OutcomeRow' } }
3215
3573
  '403': { description: 'Multi-tenant worker: outcomes view is operator-only.' }
3216
3574
  '501': { $ref: '#/components/responses/NotImplemented' }
3217
3575
 
@@ -3253,9 +3611,13 @@ paths:
3253
3611
  application/json:
3254
3612
  schema:
3255
3613
  type: object
3614
+ # 🔴 census 批2 五段:path↔schema 脱钩修 (same class as /v1/outcomes above) — `SendfileLinkRow`
3615
+ # already existed named+correct but this path's `links` items were a bare open `{}` instead
3616
+ # of `$ref`ing it.
3256
3617
  required: [links]
3618
+ additionalProperties: false
3257
3619
  properties:
3258
- links: { type: array, items: { type: object, additionalProperties: true } }
3620
+ links: { type: array, items: { $ref: '#/components/schemas/SendfileLinkRow' } }
3259
3621
  nextBefore: { type: string }
3260
3622
  '400': { $ref: '#/components/responses/BadRequest' }
3261
3623
  '401': { $ref: '#/components/responses/Unauthorized' }
@@ -3297,9 +3659,18 @@ paths:
3297
3659
  schema:
3298
3660
  type: object
3299
3661
  required: [runId, entries]
3662
+ additionalProperties: false
3300
3663
  properties:
3301
3664
  runId: { type: string }
3302
- entries: { type: array, items: { type: object, additionalProperties: true } }
3665
+ # 🔴 census 批2 四段:CLOSED against server's real `projectResult`/truncated-row shapes
3666
+ # (workflows.ts:127-165) — a row is EITHER the projected result OR an honest truncated stub
3667
+ # (a row whose stored TaskResult exceeded the 64KiB per-row gate never enters process memory).
3668
+ entries:
3669
+ type: array
3670
+ items:
3671
+ oneOf:
3672
+ - $ref: '#/components/schemas/WorkflowJournalEntry'
3673
+ - $ref: '#/components/schemas/WorkflowJournalEntryTruncated'
3303
3674
  nextOffset: { type: integer }
3304
3675
  '401': { $ref: '#/components/responses/Unauthorized' }
3305
3676
  '404': { $ref: '#/components/responses/NotFound' }
@@ -3340,6 +3711,14 @@ paths:
3340
3711
  responses:
3341
3712
  '200':
3342
3713
  description: 'Injected. Body `{ runId, label, status: "running", delivery: "applied", marker }`.'
3714
+ # 🔴 census 批2 五段:spec-谎修——this 200 declared a bare `description` with NO `content`/`schema`
3715
+ # at all (classify() reads it as "no-json", invisible to both the open- and closed-op ledgers)
3716
+ # while the server genuinely sends a JSON body (workflows.ts:279). The named schema
3717
+ # `WorkflowAgentSteerReceipt` already existed (path↔schema decoupling, same class as the earlier
3718
+ # BakeClaim/usage-triplet finds) — wiring it up here, not inventing a new one.
3719
+ content:
3720
+ application/json:
3721
+ schema: { $ref: '#/components/schemas/WorkflowAgentSteerReceipt' }
3343
3722
  '400': { $ref: '#/components/responses/BadRequest' }
3344
3723
  '401': { $ref: '#/components/responses/Unauthorized' }
3345
3724
  '404': { $ref: '#/components/responses/NotFound' }
@@ -3350,6 +3729,14 @@ paths:
3350
3729
  content:
3351
3730
  application/json:
3352
3731
  schema: { $ref: '#/components/schemas/ErrorResponse' }
3732
+ '413':
3733
+ description: >-
3734
+ (declared 2026-07-31) errorCode "steer.content_too_large" — content exceeds the server's
3735
+ per-steer request cap (`STEER_IN_MAX_REQUEST_CHARS`, same family as the run/subagent steer caps);
3736
+ REJECTED, never truncated. Shorten and resend.
3737
+ content:
3738
+ application/json:
3739
+ schema: { $ref: '#/components/schemas/ErrorResponse' }
3353
3740
  '501': { $ref: '#/components/responses/NotImplemented' }
3354
3741
 
3355
3742
  /metrics/summary:
@@ -3369,7 +3756,12 @@ paths:
3369
3756
  description: Metrics summary (shape draft).
3370
3757
  content:
3371
3758
  application/json:
3372
- schema: { type: object, additionalProperties: true }
3759
+ # 🔴 census 批2 五段:CLOSED against the real emitter — `sendJson(res, 200, { model,
3760
+ # ...deps.metrics.summarize() })` (server.ts:794) is a fixed key set (server observability/
3761
+ # metrics.ts `MetricsSummary`), not an ops free-for-all. Left `x-status: draft` / `x-sdk: none`
3762
+ # untouched (still deliberately outside the SDK's wrapped surface) — closing the SHAPE and
3763
+ # keeping it un-wrapped are independent axes.
3764
+ schema: { $ref: '#/components/schemas/MetricsSummaryOps' }
3373
3765
  '401': { $ref: '#/components/responses/Unauthorized' }
3374
3766
 
3375
3767
  # ─────────────────────────────────────────────────────────────────────────────
@@ -3437,6 +3829,15 @@ components:
3437
3829
  content:
3438
3830
  application/json:
3439
3831
  schema: { $ref: '#/components/schemas/ErrorResponse' }
3832
+ Forbidden:
3833
+ description: >
3834
+ Authorization denied on a resource the caller is authenticated for — 403. Distinct from Unauthorized
3835
+ (401/503 = who are you / this worker refuses to serve): here the identity is established and rejected.
3836
+ Today's only producer is session-ownership: reusing a `sessionId` that belongs to another principal.
3837
+ Mapped to AuthError.
3838
+ content:
3839
+ application/json:
3840
+ schema: { $ref: '#/components/schemas/ErrorResponse' }
3440
3841
  NotFound:
3441
3842
  description: >
3442
3843
  No such resource for this principal. Owner-scoped reads/cancel return 404 (not 403) for a non-owned or
@@ -4116,11 +4517,15 @@ components:
4116
4517
  ResumeEvicted:
4117
4518
  type: object
4118
4519
  description: >
4119
- 416 body for an evicted SSE resume point (`GET /v1/runs/:id/events`). There is NO machine `code` field —
4120
- match the 416 STATUS and read `retainedFrom`, then full-sync via `/v1/tasks/:id/turns` (pinned SSE contract).
4121
- required: [error, retainedFrom]
4520
+ 416 body for an evicted SSE resume point (`GET /v1/runs/:id/events`). Carries the machine code
4521
+ `errorCode: "limit.retention_evicted"`; matching the 416 STATUS alone is equally valid. Read
4522
+ `retainedFrom`, then full-sync via `/v1/tasks/:id/turns` (pinned SSE contract).
4523
+ (Corrected 2026-07-31 — see the 416 response note on that path: the "no machine code" claim predates
4524
+ the site's move onto the shared `sendError`.)
4525
+ required: [error, errorCode, retainedFrom]
4122
4526
  properties:
4123
4527
  error: { type: string }
4528
+ errorCode: { type: string, enum: [limit.retention_evicted] }
4124
4529
  retainedFrom: { type: integer, description: Lowest event seq still retained; resume events from here. }
4125
4530
 
4126
4531
  ImageIndexEntry:
@@ -4136,7 +4541,7 @@ components:
4136
4541
  [id, profile, bands, repo, tag, digest, toolchainVersions, capabilities, podContract, sizeBytes, status,
4137
4542
  visibility, tenantId, manifestSha, recipeGitSha, generatorVersion, buildDate, supersedes, signed,
4138
4543
  createdAt, updatedAt]
4139
- additionalProperties: true
4544
+ additionalProperties: false
4140
4545
  properties:
4141
4546
  id: { type: string, description: 'deterministically derived from (repo, digest) — there is no explicit-id upsert path, so an id collision ⟺ a (repo, digest) collision.' }
4142
4547
  profile: { type: string }
@@ -4150,26 +4555,8 @@ components:
4150
4555
  back to bind an image (bind by `profile`; the server re-resolves + re-admits under a trusted
4151
4556
  principal, because trusting a caller-supplied digest would fail OPEN).
4152
4557
  toolchainVersions: { type: object, additionalProperties: { type: string } }
4153
- capabilities:
4154
- type: object
4155
- additionalProperties: true
4156
- properties:
4157
- browser: { type: boolean }
4158
- db: { type: boolean }
4159
- nestedBuild: { type: boolean, description: 'can it build container images (back-compat boolean).' }
4160
- nestedBuildMode:
4161
- type: string
4162
- enum: [none, docker-cli-only, rootless-buildkit, privileged-dind]
4163
- description: 'HOW it nests (the posture). Refines `nestedBuild`; absent ⇒ fall back to the boolean.'
4164
- podContract:
4165
- type: object
4166
- additionalProperties: true
4167
- properties:
4168
- devShmMB: { type: integer }
4169
- minMemMB: { type: integer }
4170
- minCpu: { type: string }
4171
- readyTimeoutSec: { type: integer }
4172
- securityContext: { type: object, additionalProperties: true }
4558
+ capabilities: { $ref: '#/components/schemas/ImageCapabilities' }
4559
+ podContract: { $ref: '#/components/schemas/ImagePodContract' }
4173
4560
  sizeBytes: { type: ['integer', 'null'] }
4174
4561
  status: { type: string, enum: [building, published, deprecated, failed] }
4175
4562
  visibility: { type: string, enum: [public, tenant] }
@@ -4183,14 +4570,47 @@ components:
4183
4570
  createdAt: { type: string, description: ISO date-time. }
4184
4571
  updatedAt: { type: string, description: ISO date-time. }
4185
4572
 
4573
+ ImageCapabilities:
4574
+ # 抽成具名 schema(census 批2 四段)——此前在 ImageIndexEntry/ImageSelectResult 两处内联重复,
4575
+ # ImageSelectResult 那份还漏了 `nestedBuildMode`(server 侧两处读的是同一个 ImageCapabilities,
4576
+ # entry.capabilities 逐字透传;两侧同缺,不是刻意窄化)。server store-contracts.ts 全字段皆 optional。
4577
+ type: object
4578
+ description: What a sandbox image can do (server `ImageCapabilities`, plugins/store-contracts.ts).
4579
+ additionalProperties: false
4580
+ properties:
4581
+ browser: { type: boolean }
4582
+ db: { type: boolean }
4583
+ nestedBuild: { type: boolean, description: 'can it build container images (back-compat boolean).' }
4584
+ nestedBuildMode:
4585
+ type: string
4586
+ enum: [none, docker-cli-only, rootless-buildkit, privileged-dind]
4587
+ description: 'HOW it nests (the posture). Refines `nestedBuild`; absent ⇒ fall back to the boolean.'
4588
+
4589
+ ImagePodContract:
4590
+ # 抽成具名 schema(census 批2 四段)——同上,ImageSelectResult 那份此前连字段都没声明(裸 additionalProperties:true)。
4591
+ type: object
4592
+ description: Pod-shape hints for scheduling a sandbox off this image (server `ImagePodContract`).
4593
+ additionalProperties: false
4594
+ properties:
4595
+ devShmMB: { type: integer }
4596
+ minMemMB: { type: integer }
4597
+ minCpu: { type: string }
4598
+ readyTimeoutSec: { type: integer }
4599
+ securityContext: { type: object, additionalProperties: true, description: 'k8s-shaped, genuinely arbitrary — Record<string,unknown> server-side.' }
4600
+
4186
4601
  ImageSelectResult:
4187
- # 新增(2026-07-25 回填批)。
4602
+ # 新增(2026-07-25 回填批)。🔴 census 批2 四段:closed against the real handler (images.ts:407-415) — ALL
4603
+ # seven keys are unconditionally assigned every 200 (required tightened from none); `manifestSha` can be
4604
+ # null (ImageIndexEntry.manifestSha is `string | null`, this was typed non-nullable — a spec lie); and
4605
+ # `podContract`/`capabilities` now share the SAME closed schemas as ImageIndexEntry (no more drift/no
4606
+ # more missing `nestedBuildMode`).
4188
4607
  type: object
4189
4608
  description: >
4190
4609
  `POST /v1/images/select` resolution result — a PREVIEW of "which digest/capabilities does this profile
4191
4610
  resolve to" for the UI. 🔴 NOT a binding credential: to actually bind, send `sandboxImageProfile` on the
4192
4611
  task request and let the server resolve under a trusted principal.
4193
- additionalProperties: true
4612
+ required: [profile, digest, repo, ref, podContract, capabilities, manifestSha]
4613
+ additionalProperties: false
4194
4614
  properties:
4195
4615
  profile: { type: string }
4196
4616
  digest: { type: string }
@@ -4200,15 +4620,9 @@ components:
4200
4620
  description: >
4201
4621
  `${repo}@${digest}` — always present on a 200. This is the value a caller threads into the kata
4202
4622
  adapter's per-sandbox image (the server handler says so itself), i.e. it is load-bearing, not decorative.
4203
- podContract: { type: object, additionalProperties: true }
4204
- capabilities:
4205
- type: object
4206
- additionalProperties: true
4207
- properties:
4208
- browser: { type: boolean }
4209
- db: { type: boolean }
4210
- nestedBuild: { type: boolean }
4211
- manifestSha: { type: string }
4623
+ podContract: { $ref: '#/components/schemas/ImagePodContract' }
4624
+ capabilities: { $ref: '#/components/schemas/ImageCapabilities' }
4625
+ manifestSha: { type: ['string', 'null'], description: 'the source entry''s manifestSha — nullable (ImageIndexEntry.manifestSha is string|null; was wrongly non-nullable here).' }
4212
4626
 
4213
4627
  QuotaError:
4214
4628
  type: object
@@ -4350,17 +4764,32 @@ components:
4350
4764
 
4351
4765
  LeaderReceipt:
4352
4766
  type: object
4767
+ description: >
4768
+ 202 receipt of POST /v1/leader (server leader/endpoint.ts:130 — exact literal
4769
+ `{ leaderRunId, status: "running" }`; the POST ack is always the "running" value, the other
4770
+ `status` enum members only ever appear on the GET twin below).
4771
+ # 🔴 census 批2 五段:CLOSED — the POST handler's literal never carries a third key.
4353
4772
  required: [leaderRunId, status]
4773
+ additionalProperties: false
4354
4774
  properties:
4355
4775
  leaderRunId: { type: string }
4356
4776
  status: { type: string, enum: [running, completed, failed] }
4357
4777
 
4358
4778
  LeaderRecord:
4359
4779
  type: object
4780
+ description: >
4781
+ 200 body of GET /v1/leader/{leaderRunId} (server leader/endpoint.ts:142-150 — exact literal
4782
+ `{ id, status, ...(result?{result}:{}), ...(error?{error}:{}) }`).
4783
+ # 🔴 census 批2 五段:两处修——① CLOSED(result/error 是 OMIT-when-absent 的可选键,不是额外键);
4784
+ # ② `status` 的第四值 `needs_human`(LEADER-REPAIRLOOP-INTEGRATION §5/§10.6 的第三终态,
4785
+ # `statusForLeaderResult` 在 candidate_only/needs_human_oracle/conflict 三个 repairTerminal 上产出)
4786
+ # 此前 SPEC LIES BY OMISSION——TS 侧 `LeaderRecord.status` 早就带这个第四值(types.ts:1607,亲验
4787
+ # 已在场),只有 spec 的 enum 一直缺它,是个只有场景真触发才会现形的假红洞(一个真实合法帧被判违约)。
4360
4788
  required: [id, status]
4789
+ additionalProperties: false
4361
4790
  properties:
4362
4791
  id: { type: string }
4363
- status: { type: string, enum: [running, completed, failed] }
4792
+ status: { type: string, enum: [running, completed, failed, needs_human] }
4364
4793
  result: {}
4365
4794
  error: { type: string }
4366
4795
 
@@ -4399,6 +4828,30 @@ components:
4399
4828
  Approve-with-edit (design/37 last-wins): operator's rewritten tool args, applied AFTER the binding
4400
4829
  check. `boundInputHash` STILL binds the ORIGINAL input the human saw — never hash updatedInput.
4401
4830
 
4831
+ ApprovalRow:
4832
+ type: object
4833
+ description: >
4834
+ The legacy D-lane approval row of GET /v1/approvals/{approvalId} (server `ApprovalRow`,
4835
+ plugins/approval-store-sql.ts:43 — the row is `sendJson`'d verbatim, approvals-assistant.ts:668).
4836
+ Present only on deployments running the legacy `approvalStore` leg (no `checkpointStore` wired);
4837
+ the durable F4 lane has no by-id GET (only list + decide).
4838
+ # 🔴 census 批2 五段:CLOSED — byte-identical to the server row type, no extra keys.
4839
+ required: [id, taskId, sessionId, owner, scope, toolName, args, status, reason, decidedBy, createdAt, decidedAt]
4840
+ additionalProperties: false
4841
+ properties:
4842
+ id: { type: string }
4843
+ taskId: { type: [string, 'null'] }
4844
+ sessionId: { type: [string, 'null'] }
4845
+ owner: { type: [string, 'null'] }
4846
+ scope: { type: [string, 'null'], description: 'Single-DB fleet scope guard (tenant identity, owner-sourced).' }
4847
+ toolName: { type: string }
4848
+ args: { description: 'The reviewed tool-call args — full fidelity, not redacted (this IS the review surface).' }
4849
+ status: { type: string, enum: [pending, approved, denied, expired] }
4850
+ reason: { type: [string, 'null'] }
4851
+ decidedBy: { type: [string, 'null'] }
4852
+ createdAt: { type: string }
4853
+ decidedAt: { type: [string, 'null'] }
4854
+
4402
4855
  Capabilities:
4403
4856
  type: object
4404
4857
  description: >
@@ -4594,6 +5047,148 @@ components:
4594
5047
  egress: { type: boolean }
4595
5048
  irreversibility: { type: string, enum: [always, never] }
4596
5049
 
5050
+ MemoryEntryFrontmatter:
5051
+ type: object
5052
+ description: >
5053
+ core `MemoryEntryFrontmatter` (@sema-agent/core memory-engine/types.d.ts). All fields optional —
5054
+ an entry with none of them is a bare note.
5055
+ additionalProperties: false
5056
+ properties:
5057
+ name: { type: string }
5058
+ description: { type: string }
5059
+ type: { type: string }
5060
+ deleted: { type: boolean }
5061
+ provenance:
5062
+ type: object
5063
+ required: [kind, path, contentHash, ingestedAt]
5064
+ additionalProperties: false
5065
+ properties:
5066
+ kind: { type: string, enum: [repo_file] }
5067
+ path: { type: string }
5068
+ contentHash: { type: string }
5069
+ ingestedAt: { type: integer }
5070
+ trust: { type: string, enum: [untrusted] }
5071
+ extra: { type: array, items: { type: string } }
5072
+
5073
+ MemoryEntry:
5074
+ type: object
5075
+ description: >
5076
+ core `MemoryEntry` (@sema-agent/core memory-engine/types.d.ts) — one memory-engine (DB twins)
5077
+ note. Used verbatim by both GET /v1/memory/export's `entries` and POST /v1/memory/sync/{scope}'s
5078
+ `serverEntries` (server never re-shapes it — memory-policy.ts:57 / memory-sync.ts:79 pass the core
5079
+ array straight to `sendJson`).
5080
+ required: [id, slug, frontmatter, body, rev, scope]
5081
+ additionalProperties: false
5082
+ properties:
5083
+ id: { type: string }
5084
+ slug: { type: string }
5085
+ frontmatter: { $ref: '#/components/schemas/MemoryEntryFrontmatter' }
5086
+ body: { type: string }
5087
+ rev: { type: string }
5088
+ scope: { type: string }
5089
+
5090
+ MemorySyncResponse:
5091
+ type: object
5092
+ description: >
5093
+ 200 body of POST /v1/memory/sync/{scope} — core `MemorySyncResponse` (server memory-sync.ts:74-86),
5094
+ `sendJson`'d verbatim (memory-policy.ts:107). `pullTruncated` is OMIT-when-absent (present+`true`
5095
+ only when `serverEntries` was cut by `?pull.limit`).
5096
+ required: [applied, conflicts, serverEntries, serverDeletes, cursor]
5097
+ additionalProperties: false
5098
+ properties:
5099
+ applied:
5100
+ type: array
5101
+ description: The center's own-round applied ops (backend `PatchReport.applied` verbatim; empty on an idempotent replay).
5102
+ items:
5103
+ type: object
5104
+ required: [op, id]
5105
+ additionalProperties: false
5106
+ properties:
5107
+ op: { type: string, enum: [add, update, delete] }
5108
+ id: { type: string }
5109
+ slug: { type: string }
5110
+ conflicts:
5111
+ type: array
5112
+ items:
5113
+ type: object
5114
+ required: [id, reason]
5115
+ additionalProperties: false
5116
+ properties:
5117
+ id: { type: string }
5118
+ reason: { type: string }
5119
+ baseRev: { type: string }
5120
+ currentRev: { type: string }
5121
+ serverEntries:
5122
+ type: array
5123
+ description: Pull half — entries new/changed at the center since the caller's baseRevs.
5124
+ items: { $ref: '#/components/schemas/MemoryEntry' }
5125
+ serverDeletes:
5126
+ type: array
5127
+ items:
5128
+ type: object
5129
+ required: [id, baseRev]
5130
+ additionalProperties: false
5131
+ properties:
5132
+ id: { type: string }
5133
+ baseRev: { type: string }
5134
+ cursor:
5135
+ type: object
5136
+ required: [peer, baseRevs, updatedAtMs]
5137
+ additionalProperties: false
5138
+ properties:
5139
+ peer: { type: string }
5140
+ baseRevs: { type: object, additionalProperties: { type: string } }
5141
+ updatedAtMs: { type: integer }
5142
+ pullTruncated: { type: boolean, enum: [true] }
5143
+
5144
+ MetricsSummaryOps:
5145
+ type: object
5146
+ description: >
5147
+ 200 body of GET /metrics/summary — `{ model, ...MetricsSummary }` (server.ts:794; server
5148
+ observability/metrics.ts `MetricsSummary` interface + `summarize()`, all keys unconditional).
5149
+ Ops/observability face (`x-sdk: none` on the path) — closed here for census fidelity, but this
5150
+ does NOT change its SDK-wrapping status.
5151
+ required:
5152
+ [model, runsActive, tasks, tokensTotal, costUsd, costUsdByModel, costUnknown, unpricedCalls,
5153
+ taskDurationAvgSec, brainFirstTokenAvgMs, brainCallAvgMs, cacheHitRateAvg, promptCacheLowHit,
5154
+ rateLimited, costQuotaRejected, budgetExceeded, degraded, degenerate, memoryConsolidationOps,
5155
+ planCacheRecurrence, cascade, verifications, councilRuns, toolErrors]
5156
+ additionalProperties: false
5157
+ properties:
5158
+ model: { type: string }
5159
+ runsActive: { type: integer }
5160
+ tasks: { type: object, additionalProperties: { type: integer } }
5161
+ tokensTotal: { type: integer }
5162
+ costUsd: { type: number }
5163
+ costUsdByModel: { type: object, additionalProperties: { type: number } }
5164
+ # D5 ([2122]): true ⇒ the window saw a cost-unknown brain.call (unpriced deployment) — costUsd/
5165
+ # costUsdByModel are then a LOWER BOUND, not an exact total.
5166
+ costUnknown: { type: boolean }
5167
+ unpricedCalls: { type: integer }
5168
+ taskDurationAvgSec: { type: [number, 'null'] }
5169
+ brainFirstTokenAvgMs: { type: [number, 'null'] }
5170
+ brainCallAvgMs: { type: [number, 'null'] }
5171
+ cacheHitRateAvg: { type: [number, 'null'] }
5172
+ promptCacheLowHit: { type: integer }
5173
+ rateLimited: { type: integer }
5174
+ costQuotaRejected: { type: integer }
5175
+ budgetExceeded: { type: object, additionalProperties: { type: integer } }
5176
+ degraded: { type: object, additionalProperties: { type: integer } }
5177
+ degenerate: { type: object, additionalProperties: { type: integer } }
5178
+ memoryConsolidationOps: { type: object, additionalProperties: { type: integer } }
5179
+ planCacheRecurrence:
5180
+ type: object
5181
+ required: [tasks, hits, hitRate]
5182
+ additionalProperties: false
5183
+ properties:
5184
+ tasks: { type: integer }
5185
+ hits: { type: integer }
5186
+ hitRate: { type: [number, 'null'] }
5187
+ cascade: { type: object, additionalProperties: { type: integer } }
5188
+ verifications: { type: object, additionalProperties: { type: integer } }
5189
+ councilRuns: { type: object, additionalProperties: { type: integer } }
5190
+ toolErrors: { type: object, additionalProperties: { type: integer } }
5191
+
4597
5192
  ModelInfo:
4598
5193
  type: object
4599
5194
  description: >
@@ -4760,14 +5355,19 @@ components:
4760
5355
  type: object
4761
5356
  description: >
4762
5357
  GET /v1/workflows list row = core summarizeWorkflowRun projection (owner-scoped; scope = the creating
4763
- principal). All times are epoch MS. Open set.
5358
+ principal). All times are epoch MS.
5359
+ 🔴 census 批2 四段(2026-07-30):CLOSED against core's real `summarizeWorkflowRun` (sema-core
5360
+ src/core/workflow-run-store.ts:79-97) — `originatingSessionId`/`agentFailures` were emitted on the
5361
+ wire (both conditional-spread, present only when defined) but absent from this schema (two-side same-gap).
4764
5362
  required: [id, scope, status, phaseCount, agentCount, tokens, startedAt, createdAt]
4765
- additionalProperties: true
5363
+ additionalProperties: false
4766
5364
  properties:
4767
5365
  id: { type: string, description: 'Run id (= WorkflowRun.id).' }
4768
5366
  scope: { type: string, description: 'Tenant/group scope (= the creating principal).' }
5367
+ originatingSessionId: { type: string, description: 'γ 批([1510]): the originating session id, so a LIST-face session filter needs no N+1 `get`. Absent for a direct runWorkflow call / sessionless deployment.' }
4769
5368
  name: { type: string, description: "The workflow script's declared `meta.name`." }
4770
5369
  description: { type: string, description: "The workflow script's declared `meta.description` (one-liner)." }
5370
+ agentFailures: { type: integer, description: 'Count of agent-runs that ended failed — present only when > 0 (a "completed, with failures" flag without an N+1 get).' }
4771
5371
  currentPhase: { type: string, description: 'Title of the phase currently executing (absent once terminal).' }
4772
5372
  status: { $ref: '#/components/schemas/WorkflowRunStatus' }
4773
5373
  phaseCount: { type: integer, description: 'Phases recorded.' }
@@ -4787,25 +5387,40 @@ components:
4787
5387
  🔴 `arg` is a SHORT primary-arg SUMMARY (command→name, path→basename, url→origin+path), secret-scrubbed
4788
5388
  and truncated (~80 code points) by core. It exists for the monitor's `Read(path)` / `Bash(grep …)`
4789
5389
  display — it is NOT the full args. RENDER it; NEVER re-feed it to a model.
4790
- additionalProperties: true
5390
+ 🔴 census 批2 四段:CLOSED against core's real `ToolActivity` (sema-core src/core/types.ts:2332-2348) —
5391
+ `at` (the [843]④a epoch-ms stamp, carried verbatim by both the start and end beat) was on the wire
5392
+ but absent from this schema.
5393
+ required: [phase, toolCallId, toolName]
5394
+ additionalProperties: false
4791
5395
  properties:
4792
5396
  phase: { type: string, enum: [start, end] }
4793
5397
  toolCallId: { type: string }
4794
5398
  toolName: { type: string, description: 'the monitor renders `${toolName}(${arg})`.' }
5399
+ at: { type: integer, description: '[843]④a: epoch ms, stamped on both the start and end beat — a consumer derives per-call duration (end.at − start.at) and inter-call idle. Additive/optional: older producers lack it (absence ≠ 0).' }
4795
5400
  arg: { type: string, description: 'SHORT, secret-scrubbed, ~80-code-point summary (set on `phase:"start"`). UNTRUSTED display text.' }
4796
5401
  isError: { type: boolean, description: 'set on `phase:"end"` — whether the tool call errored.' }
4797
5402
 
4798
5403
  WorkflowAgentRow:
4799
5404
  # 新增(2026-07-25 回填批)—— 取代 `WorkflowRun.agents` 的 `items: {}`(零形状)。
5405
+ # 🔴 census 批2 四段:CLOSED against the REAL wire projection — server's `summarizeWorkflowDetail`
5406
+ # (src/http/routes/workflows.ts:358-394), not core's raw `WorkflowAgentRun`. The two differ: the server
5407
+ # ADDS `displayStatus`/`callKey`/`groupId`/`lastActivityAt`/`durationMs`/`queuedAt`/`startedAt`/`endedAt`/
5408
+ # `replayed` (all present on the wire but previously undeclared — same-side gap, no server change needed)
5409
+ # and DROPS core's `errorCode`/`errorMessage`/`attempts`/`lastAttemptReason`/`sessionId` (never projected
5410
+ # by this route — genuinely absent from the wire, not a spec lie).
4800
5411
  type: object
4801
5412
  description: >
4802
- One agent-run row in a workflow's detail. The shape is PERMISSIVE — read the known render fields below,
4803
- tolerate the rest as the projection evolves.
4804
- additionalProperties: true
5413
+ One agent-run row in a workflow's detail (server projection, not the raw core record).
5414
+ required: [label, status, displayStatus, callKey, queuedAt]
5415
+ additionalProperties: false
4805
5416
  properties:
4806
- label: { type: string, description: 'display label.' }
5417
+ label: { type: string, description: 'display label (redacted — LLM-authored).' }
4807
5418
  status: { type: string, description: 'lifecycle status (the input vocabulary the display derivation reads).' }
4808
- phase: { type: string, description: 'the phase title this agent ran under (groups it into a phase bucket).' }
5419
+ displayStatus: { type: string, description: 'core deriveAgentDisplayStatus: running|queued|done|failed|interrupted — the canonical glyph vocabulary, one source of truth shared with the shell.' }
5420
+ taskStatus: { type: string, description: 'the underlying task''s terminal TaskStatus (open vocabulary; core enum verbatim).' }
5421
+ callKey: { type: string, description: 'the stable deterministic identity of this ctx.agent call (resume-journal key).' }
5422
+ groupId: { type: string, description: 'the nesting ctx.workflow sub-group this agent ran under; absent = top-level.' }
5423
+ phase: { type: string, description: 'the phase title this agent ran under (redacted — LLM-authored); absent = unphased.' }
4809
5424
  model: { type: string, description: 'per-agent model DISPLAY label (e.g. "Opus 4.8 (1M context)"), not an id.' }
4810
5425
  tokens: { type: integer }
4811
5426
  turns: { type: integer }
@@ -4814,44 +5429,149 @@ components:
4814
5429
  type: array
4815
5430
  description: 'the "Activity" tail — the LAST-N tool-call beats (bounded).'
4816
5431
  items: { $ref: '#/components/schemas/WorkflowActivityBeat' }
5432
+ lastActivityAt: { type: integer, description: 'the tail activity beat''s `at` timestamp — a historical view, so the shell derives idleness against its own clock rather than a served idleMs.' }
5433
+ durationMs: { type: integer, description: 'endedAt − startedAt; absent while queued/running or if either end is unset (a queued agent has no running duration).' }
5434
+ queuedAt: { type: integer, description: 'when the agent was ENQUEUED (before waiting on a concurrency slot). Always set.' }
5435
+ startedAt: { type: integer, description: 'when the agent actually started running (post-queue). Absent while queued, or if aborted/finalized before it ran.' }
5436
+ endedAt: { type: integer }
5437
+ replayed: { type: boolean, description: 'set when this agent''s result was REPLAYED from a resume journal rather than freshly run.' }
4817
5438
  prompt: { type: string, description: 'what the worker was ASKED (core-redacted + bounded). UNTRUSTED display text.' }
4818
5439
  output: { type: string, description: 'the worker''s final OUTPUT (core-redacted + bounded). UNTRUSTED display text.' }
4819
5440
 
4820
- WorkflowRun:
5441
+ WorkflowPhaseProgress:
5442
+ # 新(census 批2 四段):此前 `WorkflowRun.phases` 是 `items: {}`(零形状)。真形 = server
5443
+ # `summarizeWorkflowDetail`'s phases.map projection (workflows.ts:335-346) — a DERIVED progress view,
5444
+ # not core's raw `WorkflowPhase` (drops `detail`/`model`/(phase-level) `agentFailures`; adds `done`/`total`).
4821
5445
  type: object
4822
- description: >
4823
- GET /v1/workflows/:id full run (core WorkflowRun). Non-owner → 404 (no existence oracle).
4824
- 🔴 phases/agents/stats nested shapes may evolve → permissive (read length/known fields, tolerate the rest).
4825
- required: [id, scope, status, phases, agents, stats, startedAt, createdAt]
4826
- additionalProperties: true
5446
+ description: One phase's progress in a workflow's detail (server-derived — done/total of the agents grouped under it).
5447
+ required: [title, status, startedAt, done, total]
5448
+ additionalProperties: false
4827
5449
  properties:
4828
- id: { type: string }
4829
- scope: { type: string }
4830
- name: { type: string, description: "The workflow script's declared `meta.name`." }
4831
- description: { type: string, description: "The workflow script's declared `meta.description` (one-liner)." }
4832
- status: { $ref: '#/components/schemas/WorkflowRunStatus' }
4833
- phases: { type: array, description: 'Phase records (permissive).', items: {} }
4834
- # `items: {}`(= 任意值,零形状)改为具名 $ref(2026-07-25 回填批):这是 workflow 监视面最吃重的数组,
4835
- # 空 schema 等于对非 TS 消费方完全没描述过它。`WorkflowAgentRow` 仍是 PERMISSIVE(additionalProperties)——
4836
- # 只钉住监视面真正 key 的那些字段,其余随投影演进。
4837
- agents:
4838
- type: array
4839
- description: 'Agent-run records (permissive — read the known render fields, tolerate the rest).'
4840
- items: { $ref: '#/components/schemas/WorkflowAgentRow' }
4841
- stats:
5450
+ title: { type: string, description: 'phase title (redacted — LLM-authored).' }
5451
+ status: { type: string, description: 'core WorkflowItemStatus, plus "pending" for a meta-preregistered phase not yet adopted.' }
5452
+ startedAt: { type: integer, description: 'adoption time for a pre-registered phase (0 while still pending).' }
5453
+ endedAt: { type: integer }
5454
+ durationMs: { type: integer, description: 'endedAt − startedAt; absent until the phase ends.' }
5455
+ done: { type: integer, description: 'agents grouped under this phase whose status is a terminal (completed/failed).' }
5456
+ total: { type: integer, description: 'agents grouped under this phase.' }
5457
+
5458
+ WorkflowGroupNode:
5459
+ # 新(census 批2 四段):`WorkflowRun.groups` was undeclared entirely. Real shape = server's rebuilt nested
5460
+ # tree (workflows.ts:399-441, GroupNode) — recursive, acyclic (a dangling/cyclic parentGroupId is grafted
5461
+ # onto the root by the server, so this shape never actually cycles on the wire).
5462
+ type: object
5463
+ description: One node of the workflow's nested `ctx.workflow` group tree (rebuilt server-side from `parentGroupId`).
5464
+ required: [groupId, status, startedAt, agentCallKeys, children]
5465
+ additionalProperties: false
5466
+ properties:
5467
+ groupId: { type: string }
5468
+ parentGroupId: { type: string, description: 'absent = top-level (a child of the implicit root).' }
5469
+ status: { type: string, description: 'core WorkflowItemStatus (running/completed/failed).' }
5470
+ startedAt: { type: integer }
5471
+ endedAt: { type: integer }
5472
+ durationMs: { type: integer, description: 'endedAt − startedAt; absent until the group ends.' }
5473
+ agentCallKeys: { type: array, items: { type: string }, description: 'callKeys of the agents directly under this group.' }
5474
+ children: { type: array, items: { $ref: '#/components/schemas/WorkflowGroupNode' }, description: 'nested child groups (recursive).' }
5475
+
5476
+ WorkflowRunStats:
5477
+ # 新(census 批2 四段):此前 inline `stats` 只钉了 `tokens`/`nested.tokens` 两键;真形 = core
5478
+ # `WorkflowRunStats`(sema-core src/orchestration/workflow-types.ts:105-110)——own/nested 各自还有
5479
+ # `turns`/`costMicroUsd`,nested 另有 `tasks`。四键(own turns/costMicroUsd + nested 两键)此前两侧同缺。
5480
+ type: object
5481
+ description: 'Cumulative workflow usage. `own` (top-level) and `nested` (delegated sub-agents) are kept SEPARATE (R-5) — total spend = tokens + nested.tokens.'
5482
+ required: [tokens, turns, costMicroUsd, nested]
5483
+ additionalProperties: false
5484
+ properties:
5485
+ tokens: { type: integer }
5486
+ turns: { type: integer }
5487
+ costMicroUsd: { type: integer }
5488
+ nested:
4842
5489
  type: object
4843
- description: 'Token stats — own at stats.tokens, nested at stats.nested.tokens (R-5 own/nested split).'
4844
- additionalProperties: true
5490
+ required: [tokens, turns, tasks, costMicroUsd]
5491
+ additionalProperties: false
4845
5492
  properties:
4846
5493
  tokens: { type: integer }
4847
- nested:
4848
- type: object
4849
- additionalProperties: true
4850
- properties:
4851
- tokens: { type: integer }
5494
+ turns: { type: integer }
5495
+ tasks: { type: integer }
5496
+ costMicroUsd: { type: integer }
5497
+
5498
+ WorkflowRun:
5499
+ # 🔴 census 批2 四段:CLOSED against the REAL wire projection — server's `summarizeWorkflowDetail`
5500
+ # (src/http/routes/workflows.ts:317-475), which is a DERIVED view over core's `WorkflowRun`, not the raw
5501
+ # record. This schema previously mirrored (a stale subset of) the core type; the actual wire adds
5502
+ # `durationMs`/`unphased`/`groups` and drops core's `sourceTaskId`/`originatingSessionId`/`effectiveArgs`/
5503
+ # `resultFull`/`completionId`/`resume`/`journalSkips` (never projected by this route — genuinely absent
5504
+ # from the wire, not a spec lie). `agentFailures`/`rev`/`error`/`result` WERE on the wire but undeclared
5505
+ # (two-side same-gap, this segment's fix).
5506
+ type: object
5507
+ description: 'GET /v1/workflows/:id full run detail (server-derived — Phases/agents/groups panels). Non-owner → 404 (no existence oracle).'
5508
+ required: [id, scope, status, stats, startedAt, createdAt, phases, agents, groups]
5509
+ additionalProperties: false
5510
+ properties:
5511
+ id: { type: string }
5512
+ scope: { type: string }
5513
+ name: { type: string, description: "The workflow script's declared `meta.name` (redacted)." }
5514
+ description: { type: string, description: "The workflow script's declared `meta.description` (redacted, one-liner)." }
5515
+ status: { $ref: '#/components/schemas/WorkflowRunStatus' }
5516
+ agentFailures: { type: integer, description: 'run-level count of agents whose status ended failed — stamped at run end, only when > 0.' }
5517
+ stats: { $ref: '#/components/schemas/WorkflowRunStats' }
4852
5518
  startedAt: { type: integer }
4853
5519
  endedAt: { type: integer }
4854
5520
  createdAt: { type: integer }
5521
+ durationMs: { type: integer, description: 'endedAt − startedAt; absent while the run is still running.' }
5522
+ rev: { type: integer, description: 'store optimistic-concurrency revision; unset for a pure in-memory run.' }
5523
+ error: { type: string, description: 'set when status === "failed": the error the script threw.' }
5524
+ result: { type: string, description: 'the script''s RETURN VALUE (bounded + redacted). Absent on failed/pre-1.234 runs.' }
5525
+ phases:
5526
+ type: array
5527
+ description: 'Phase progress records.'
5528
+ items: { $ref: '#/components/schemas/WorkflowPhaseProgress' }
5529
+ unphased:
5530
+ type: object
5531
+ description: 'done/total of agents with no phase (or a stray phase matching no registered entry). Present only when non-empty.'
5532
+ required: [done, total]
5533
+ additionalProperties: false
5534
+ properties:
5535
+ done: { type: integer }
5536
+ total: { type: integer }
5537
+ agents:
5538
+ type: array
5539
+ description: 'Agent-run records.'
5540
+ items: { $ref: '#/components/schemas/WorkflowAgentRow' }
5541
+ groups:
5542
+ type: array
5543
+ description: 'The nested ctx.workflow group tree (roots only — children nest recursively). Empty when the script used no nesting.'
5544
+ items: { $ref: '#/components/schemas/WorkflowGroupNode' }
5545
+
5546
+ WorkflowJournalEntry:
5547
+ # 新(census 批2 四段):GET /v1/workflows/:id/journal row, the non-truncated arm (server `projectResult`,
5548
+ # workflows.ts:127-134) — one agent()-call's cached TaskResult, bounded + redacted.
5549
+ type: object
5550
+ description: One journal entry — a completed agent() call's projected TaskResult (bounded + redacted).
5551
+ required: [callKey, ordinal, status]
5552
+ additionalProperties: false
5553
+ properties:
5554
+ callKey: { type: string }
5555
+ ordinal: { type: integer, description: 'callKeyOrdinal(callKey) — the stable resume/display order.' }
5556
+ status: { type: string, description: 'the TaskResult''s terminal status (open vocabulary; core enum verbatim).' }
5557
+ error: { type: string, description: 'first line of errorMessage, clipped to 300 chars (redacted). Present only when the result carried an errorMessage.' }
5558
+ result: { type: string, description: 'the result text, clipped to 2000 chars (redacted). Present only when the result carried one.' }
5559
+ tokens: { type: integer, description: 'present only when the result carried stats.' }
5560
+ turns: { type: integer, description: 'present only when the result carried stats.' }
5561
+
5562
+ WorkflowJournalEntryTruncated:
5563
+ # 新(census 批2 四段):the truncated arm (workflows.ts:141-142/159-162) — an honest stub for a row whose
5564
+ # stored TaskResult exceeded the per-row 64KiB gate (SQL stores) or byte length (file/in-memory
5565
+ # fallback): the server never pulls the oversized payload into process memory.
5566
+ type: object
5567
+ description: A journal row too large to project — an honest "too big to show" stub, never a silent 2000-char truncation of the real result.
5568
+ required: [callKey, ordinal, truncated, resultBytes]
5569
+ additionalProperties: false
5570
+ properties:
5571
+ callKey: { type: string }
5572
+ ordinal: { type: integer }
5573
+ truncated: { type: boolean, enum: [true] }
5574
+ resultBytes: { type: integer, description: 'the stored TaskResult''s byte length (so a UI can at least show the size).' }
4855
5575
 
4856
5576
  WorkflowStreamEvent:
4857
5577
  type: object
@@ -4878,8 +5598,11 @@ components:
4878
5598
  AttachmentInfo:
4879
5599
  type: object
4880
5600
  description: 'Upload receipt (POST /v1/attachments 201). `name` is the SANITIZED basename the server will materialize under `attachments/`.'
5601
+ # 🔴 census 批2 五段(2026-07-30):CLOSED against the real emitter — `sendJson(res, 201, { id, name,
5602
+ # mime, sha256, sizeBytes })` (server attachments.ts:90) is an EXACT 5-key object literal, never a
5603
+ # superset.
4881
5604
  required: [id, name, mime, sha256, sizeBytes]
4882
- additionalProperties: true
5605
+ additionalProperties: false
4883
5606
  properties:
4884
5607
  id: { type: string }
4885
5608
  name: { type: string }
@@ -6941,6 +7664,24 @@ components:
6941
7664
  description: 'Machine-branchable cause. CLOSED on the server side (fleet-bus.ts HookNotice.reason) but OPEN ON READ — branch known values, render anything else as the generic "could not evaluate".'
6942
7665
  detail: { type: string, description: 'Redacted supplement (e.g. why skipped). Additive / tolerate-absent.' }
6943
7666
 
7667
+ BakeStatus:
7668
+ # 新(census 批2 四段)——server `BakeStatus`(plugins/store-contracts.ts:83), the COARSE lifecycle
7669
+ # bakeView/claimResponse/bakesCreate all key on.
7670
+ type: string
7671
+ enum: [queued, running, done, failed]
7672
+
7673
+ BakeState:
7674
+ # 新(census 批2 四段)——server `BakeState`(store-contracts.ts:85-93), build.sh's finer 8-value state;
7675
+ # null before the runner's first `state` line lands.
7676
+ type: [string, 'null']
7677
+ enum: [PENDING, BUILDING, PUSHING, VERIFYING, REGISTERING, COMPLETE, FAILED, CANCELLED, null]
7678
+
7679
+ BakeErrorCode:
7680
+ # 新(census 批2 四段)——server `BakeErrorCode`(store-contracts.ts:96), the closed structured terminal
7681
+ # code set (§P2.14 #2); null = success or an uncategorized failure.
7682
+ type: [string, 'null']
7683
+ enum: [band-conflict, duplicate, disk, timeout, cancelled, null]
7684
+
6944
7685
  BakeClaim:
6945
7686
  type: object
6946
7687
  description: >
@@ -6949,8 +7690,11 @@ components:
6949
7690
  secret (minted at claim, echoed on every ingest via the x-bake-ingest-secret header) + the
6950
7691
  profile/bands the runner needs for its profile-aware hard deadline. The runner executes `argv`
6951
7692
  VERBATIM — it never assembles its own command line. 204 (no body) = queue empty.
6952
- required: [bakeId, argv, push, dryRun, profile, ingestSecret, leaseUntil]
6953
- additionalProperties: true
7693
+ 🔴 census 批2 四段:CLOSED + wired to both claim paths (this schema existed but neither path
7694
+ `$ref`'d it — path↔schema 脱钩; both declared a bare open `{}` instead). `logs`/`bands` are
7695
+ unconditionally assigned by claimResponse() but were absent from `required` — same-side gap.
7696
+ required: [bakeId, argv, push, dryRun, logs, profile, bands, ingestSecret, leaseUntil]
7697
+ additionalProperties: false
6954
7698
  properties:
6955
7699
  bakeId: { type: string }
6956
7700
  argv:
@@ -6959,23 +7703,128 @@ components:
6959
7703
  description: 'The server-assembled, whitelist-vetted build command — execute verbatim, never self-assemble.'
6960
7704
  push: { type: boolean }
6961
7705
  dryRun: { type: boolean }
6962
- logs: { description: 'Log routing config, passed through as stored (opaque to the claim contract).' }
7706
+ logs: { type: boolean, description: 'Log routing config, passed through as stored.' }
6963
7707
  profile: { type: string }
6964
- bands: { description: 'Profile bands for the runner''s profile-aware hard deadline (opaque passthrough).' }
7708
+ bands: { type: [array, 'null'], items: { type: string }, description: 'Profile bands for the runner''s profile-aware hard deadline (opaque passthrough); null when the bake carries none.' }
6965
7709
  ingestSecret: { type: string, description: 'Per-bake credential — the ONLY place it leaves image-api; echo it on every ingest.' }
6966
7710
  leaseUntil:
6967
7711
  description: 'Lease expiry (epoch ms or ISO timestamp depending on store backend).'
6968
7712
  oneOf: [{ type: number }, { type: string }]
6969
7713
 
7714
+ BakeRecordView:
7715
+ # 新(census 批2 四段)——GET /v1/images/bakes/{bakeId}'s `{ bake }` envelope. Exact key set = server
7716
+ # `bakeView()` (images.ts:527-551): the durable BakeRecord MINUS the runner-internal secret/lease fields
7717
+ # (ingestSecret/runnerId/leaseUntil/cancelRequested/argv — an ops surface, not the claim credential).
7718
+ type: object
7719
+ description: The operator poll view of one bake (server bakeView projection over the durable BakeRecord).
7720
+ required:
7721
+ [bakeId, status, state, profile, bands, baseRef, push, dryRun, logs, digest, repo, ref, indexId,
7722
+ exitCode, error, errorCode, manifestSha, tag, requestedBy, createdAt, updatedAt]
7723
+ additionalProperties: false
7724
+ properties:
7725
+ bakeId: { type: string }
7726
+ status: { $ref: '#/components/schemas/BakeStatus' }
7727
+ state: { $ref: '#/components/schemas/BakeState' }
7728
+ profile: { type: string }
7729
+ bands: { type: [array, 'null'], items: { type: string } }
7730
+ baseRef: { type: [string, 'null'] }
7731
+ push: { type: boolean }
7732
+ dryRun: { type: boolean }
7733
+ logs: { type: boolean }
7734
+ digest: { type: [string, 'null'] }
7735
+ repo: { type: [string, 'null'] }
7736
+ ref: { type: [string, 'null'] }
7737
+ indexId: { type: [string, 'null'] }
7738
+ exitCode: { type: [integer, 'null'] }
7739
+ error: { type: [string, 'null'] }
7740
+ errorCode: { $ref: '#/components/schemas/BakeErrorCode' }
7741
+ manifestSha: { type: [string, 'null'] }
7742
+ tag: { type: [string, 'null'] }
7743
+ requestedBy: { type: [string, 'null'] }
7744
+ createdAt: { type: string, description: ISO date-time. }
7745
+ updatedAt: { type: string, description: ISO date-time. }
7746
+
7747
+ BakeSubmitAck:
7748
+ # 新(census 批2 四段)——POST /v1/images/bakes 202 body. All three code paths (attach-to-in-flight,
7749
+ # atomic-admission-created, dryRun) produce this SAME shape (images.ts:193/206/213/217).
7750
+ type: object
7751
+ description: Acknowledgement of a submitted/attached-to bake.
7752
+ required: [bakeId, eventsUrl, status, state]
7753
+ additionalProperties: false
7754
+ properties:
7755
+ bakeId: { type: string }
7756
+ eventsUrl: { type: string, description: '`/v1/images/bakes/{bakeId}/events` — the SSE resource to attach to.' }
7757
+ status: { $ref: '#/components/schemas/BakeStatus' }
7758
+ state: { $ref: '#/components/schemas/BakeState' }
7759
+
7760
+ BakeCancelAck:
7761
+ # 新(census 批2 四段)——POST /v1/images/bakes/{bakeId}/cancel 202 body (images.ts:239-241). `note` is
7762
+ # ALWAYS present (a ternary VALUE, not a conditional key) — the spec previously implied it was optional.
7763
+ type: object
7764
+ description: Cooperative-cancel acknowledgement (a durable flag; the runner kills the build at its next heartbeat).
7765
+ required: [bakeId, cancelRequested, note]
7766
+ additionalProperties: false
7767
+ properties:
7768
+ bakeId: { type: string }
7769
+ cancelRequested: { type: boolean, enum: [true] }
7770
+ note: { type: string, description: 'always one of two fixed strings: "cancel requested — …" or "bake already terminal — cancel is a no-op".' }
7771
+
7772
+ BakeIngestAck:
7773
+ # 新(census 批2 四段)——POST /v1/images/bakes/{bakeId}/ingest 200 body. 🔴 FOUR distinct shapes share
7774
+ # this one status code (images.ts:302-334): a heartbeat ack carries no `seq`; a non-terminal frame ack
7775
+ # carries `seq` but no `terminal`; a terminal `done` carries `terminal`+`indexId` (success) OR
7776
+ # `terminal`+`needsFlip` (the bounded-retry-exhausted non-throw path, still 200 — the runner re-POSTs the
7777
+ # idempotent done to re-drive the flip). `cancelRequested`+`leaseValid` ride on every non-heartbeat shape
7778
+ # (merged in at the send site); the heartbeat shape sets them directly instead.
7779
+ description: One ingest acknowledgement (four shapes keyed by which fields are present).
7780
+ oneOf:
7781
+ - type: object
7782
+ description: Heartbeat ack (lease renewed; no event appended).
7783
+ required: [accepted, leaseValid, cancelRequested]
7784
+ additionalProperties: false
7785
+ properties:
7786
+ accepted: { type: boolean, enum: [true] }
7787
+ leaseValid: { type: boolean }
7788
+ cancelRequested: { type: boolean }
7789
+ - type: object
7790
+ description: Non-terminal build.sh line appended (resolved/state/step/image/manifest/log).
7791
+ required: [accepted, seq, cancelRequested, leaseValid]
7792
+ additionalProperties: false
7793
+ properties:
7794
+ accepted: { type: boolean, enum: [true] }
7795
+ seq: { type: integer }
7796
+ cancelRequested: { type: boolean }
7797
+ leaseValid: { type: boolean, enum: [true] }
7798
+ - type: object
7799
+ description: Terminal `done` — the coarse-status CAS was won and the register step succeeded (or the bake never registers, e.g. a non-COMPLETE terminal).
7800
+ required: [accepted, terminal, indexId, cancelRequested, leaseValid]
7801
+ additionalProperties: false
7802
+ properties:
7803
+ accepted: { type: boolean, enum: [true] }
7804
+ terminal: { type: boolean }
7805
+ indexId: { type: [string, 'null'] }
7806
+ cancelRequested: { type: boolean }
7807
+ leaseValid: { type: boolean, enum: [true] }
7808
+ - type: object
7809
+ description: Terminal `done` — the bounded setTerminal retry was exhausted; a non-terminal 200 asks the runner to re-POST the (lineOrd-idempotent) done to re-drive the flip.
7810
+ required: [accepted, terminal, needsFlip, cancelRequested, leaseValid]
7811
+ additionalProperties: false
7812
+ properties:
7813
+ accepted: { type: boolean, enum: [true] }
7814
+ terminal: { type: boolean, enum: [false] }
7815
+ needsFlip: { type: boolean, enum: [true] }
7816
+ cancelRequested: { type: boolean }
7817
+ leaseValid: { type: boolean, enum: [true] }
7818
+
6970
7819
  OutcomeRow:
6971
7820
  type: object
6972
7821
  description: >
6973
7822
  One task-outcome ledger row of GET /v1/outcomes (envelope key `outcomes`; design/73 §7 read-only
6974
7823
  base). Shape = the SQL ledgers' per-(taskSignature, model) summary projection (server
6975
- tidb-outcome-ledger.ts:186 / pg-outcome-ledger.ts:124 — the only queryable sinks; a File sink
6976
- 501s honestly). OPEN set — mechanical-signal read-only surface.
7824
+ outcome-ledger-sql.ts `summary()` — the only queryable sink; a File sink 501s honestly).
7825
+ # 🔴 census 批2 五段:CLOSED — the exact 5-key `.map()` projection, no sixth key.
6977
7826
  required: [taskSignature, model, n, passRate, meanCostMicroUsd]
6978
- additionalProperties: true
7827
+ additionalProperties: false
6979
7828
  properties:
6980
7829
  taskSignature: { type: string }
6981
7830
  model: { type: string }
@@ -6990,8 +7839,10 @@ components:
6990
7839
  nextBefore?}`; server src/http/routes/sessions-list.ts — exact projection). A principal caller is PINNED to its
6991
7840
  own scope (?scope= ignored); a fleet credential may pass ?scope= (default = the single-user
6992
7841
  sentinel "_"). Page with `before` = a previous page's `nextBefore` (uuidv7; malformed → 400).
7842
+ # 🔴 census 批2 五段:CLOSED — exact projection (sessions-list.ts:94-105); taskId/sessionId are the
7843
+ # only OMIT-when-absent keys, no others.
6993
7844
  required: [id, filename, size, ttlSec, track, bucket, key, createdAt]
6994
- additionalProperties: true
7845
+ additionalProperties: false
6995
7846
  properties:
6996
7847
  id: { type: string, description: 'uuidv7 link id — also the `before` pagination cursor.' }
6997
7848
  filename: { type: string }
@@ -7347,13 +8198,16 @@ components:
7347
8198
  The 200 receipt of POST /v1/workflows/{runId}/agents/{label}/steer (server src/http/routes/workflows.ts —
7348
8199
  exact literal: `status:"running"`, `delivery:"applied"`, `marker` = the engine-minted delivery
7349
8200
  marker; NO `note`, unlike the run-subagent verb).
7350
- required: [runId, label]
7351
- additionalProperties: true
8201
+ # 🔴 census 批2 五段:CLOSED — all 5 keys are unconditional in the literal
8202
+ # `{ runId: wfRunId, label, status: "running", delivery: "applied", marker }` (workflows.ts:279);
8203
+ # this schema previously existed but no path `$ref`'d it (see the path fix above).
8204
+ required: [runId, label, status, delivery, marker]
8205
+ additionalProperties: false
7352
8206
  properties:
7353
8207
  runId: { type: string }
7354
8208
  label: { type: string }
7355
- status: { type: string, description: 'Literal "running" on today''s server.' }
7356
- delivery: { type: string, description: 'Literal "applied" on today''s server.' }
8209
+ status: { type: string, const: running, description: 'Literal "running" on today''s server.' }
8210
+ delivery: { type: string, const: applied, description: 'Literal "applied" on today''s server.' }
7357
8211
  marker: { type: string, description: 'Engine-minted delivery marker.' }
7358
8212
 
7359
8213
  WorkspaceSnapshotRow: