@sema-agent/sdk 2.1.3 → 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:
@@ -558,14 +608,9 @@ paths:
558
608
  description: Accepted — compaction runs at the next turn boundary; watch the run stream for `compacted{trigger:"manual"}`.
559
609
  content:
560
610
  application/json:
561
- schema:
562
- type: object
563
- required: [taskId, status, delivery]
564
- properties:
565
- taskId: { type: string }
566
- status: { type: string }
567
- delivery: { type: string, enum: [accepted] }
568
- note: { type: string }
611
+ # census 批2 第三段(2026-07-30):`CompactAck` 这个命名 schema 本文件早就有,path 却抄了一份
612
+ # 内联形(usage 三兄弟同病)——接上,封闭做在 schema 一处。
613
+ schema: { $ref: '#/components/schemas/CompactAck' }
569
614
  '400': { $ref: '#/components/responses/BadRequest' }
570
615
  '401': { $ref: '#/components/responses/Unauthorized' }
571
616
  '404': { $ref: '#/components/responses/NotFound' }
@@ -574,6 +619,7 @@ paths:
574
619
  content:
575
620
  application/json:
576
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)
577
623
  '501': { $ref: '#/components/responses/NotImplemented' }
578
624
 
579
625
  /v1/runs/{taskId}/detach:
@@ -608,14 +654,8 @@ paths:
608
654
  description: Requested — honest about carrying no confirmation (the run stream carries the outcome).
609
655
  content:
610
656
  application/json:
611
- schema:
612
- type: object
613
- required: [taskId, toolCallId, delivery]
614
- properties:
615
- taskId: { type: string }
616
- toolCallId: { type: string }
617
- delivery: { type: string, enum: [requested] }
618
- note: { type: string }
657
+ # census 批2 第三段(2026-07-30):同 compact——`DetachAck` 在档,path 抄内联形;接上。
658
+ schema: { $ref: '#/components/schemas/DetachAck' }
619
659
  '400': { $ref: '#/components/responses/BadRequest' }
620
660
  '401': { $ref: '#/components/responses/Unauthorized' }
621
661
  '404': { $ref: '#/components/responses/NotFound' }
@@ -624,6 +664,14 @@ paths:
624
664
  content:
625
665
  application/json:
626
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' }
627
675
  '501': { $ref: '#/components/responses/NotImplemented' }
628
676
 
629
677
  /v1/runs/{taskId}/subagents/{target}/output:
@@ -635,6 +683,17 @@ paths:
635
683
  required: true
636
684
  schema: { type: string }
637
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.
638
697
  get:
639
698
  tags: [runs]
640
699
  operationId: runsSubagentOutput
@@ -653,14 +712,9 @@ paths:
653
712
  description: 'The registry projection. `content` = the child''s final assistant body (UNTRUSTED model output).'
654
713
  content:
655
714
  application/json:
656
- schema:
657
- type: object
658
- required: [taskId, target]
659
- properties:
660
- taskId: { type: string }
661
- target: { type: string }
662
- content: { type: string }
663
- output: { type: object, description: 'Registry details projection (status/kind/…), passed through verbatim.' }
715
+ # census 批2 第三段(2026-07-30):`SubagentOutputResult` 在档而 path 抄内联形(且抄漏了
716
+ # required 的 content/output)——接上。cursorSemantics 只有 generic 面铸,本面不铸(schema 注)。
717
+ schema: { $ref: '#/components/schemas/SubagentOutputResult' }
664
718
  '401': { $ref: '#/components/responses/Unauthorized' }
665
719
  '404': { $ref: '#/components/responses/NotFound' }
666
720
  '501': { $ref: '#/components/responses/NotImplemented' }
@@ -715,6 +769,7 @@ paths:
715
769
  content:
716
770
  application/json:
717
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)
718
773
  '501': { $ref: '#/components/responses/NotImplemented' }
719
774
 
720
775
  /v1/runs/{taskId}/subagents/{target}/stream:
@@ -726,6 +781,17 @@ paths:
726
781
  required: true
727
782
  schema: { type: string }
728
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.
729
795
  get:
730
796
  tags: [runs]
731
797
  operationId: runsSubagentStream
@@ -760,6 +826,17 @@ paths:
760
826
  required: true
761
827
  schema: { type: string }
762
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.
763
840
  get:
764
841
  tags: [runs]
765
842
  operationId: runsTaskOutput
@@ -778,15 +855,9 @@ paths:
778
855
  description: 'Registry projection passed through verbatim; `content` is UNTRUSTED tool/model output.'
779
856
  content:
780
857
  application/json:
781
- schema:
782
- type: object
783
- required: [taskId, target]
784
- properties:
785
- taskId: { type: string }
786
- target: { type: string }
787
- content: { type: string }
788
- output: { type: object }
789
- cursorSemantics: { type: string, enum: [cursor, full], description: 'G14: whether THIS read consumed the cursor. Absent on error/not_ready or unknown kinds.' }
858
+ # census 批2 第三段(2026-07-30):内联形收编进已在档的 `SubagentOutputResult`(它本来就
859
+ # 自述覆盖本面,cursorSemantics 也在那边)。
860
+ schema: { $ref: '#/components/schemas/SubagentOutputResult' }
790
861
  '400': { $ref: '#/components/responses/BadRequest' }
791
862
  '401': { $ref: '#/components/responses/Unauthorized' }
792
863
  '404': { $ref: '#/components/responses/NotFound' }
@@ -816,7 +887,18 @@ paths:
816
887
  output verb.
817
888
  responses:
818
889
  '200':
819
- description: 'Stopped — same projection shape as the output verb ({ taskId, target, content, output }).'
890
+ # 🔴 census 批2 第三段(2026-07-30)spec 谎修正:这条 200 此前只有一句 description、**零 content
891
+ # 声明**(机器视角=「无 body」),而 server 真发 JSON(`routes/runs.ts:1155`,与 output 面同一个
892
+ # sendJson,含 g14 的条件 `cursorSemantics`)。补接同一个 schema。
893
+ description: 'Stopped — same projection shape as the output verb.'
894
+ content:
895
+ application/json:
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'
820
902
  '401': { $ref: '#/components/responses/Unauthorized' }
821
903
  '404': { $ref: '#/components/responses/NotFound' }
822
904
  '409':
@@ -824,6 +906,14 @@ paths:
824
906
  content:
825
907
  application/json:
826
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' }
827
917
  '501': { $ref: '#/components/responses/NotImplemented' }
828
918
 
829
919
  /v1/sessions:
@@ -851,6 +941,14 @@ paths:
851
941
  name: owner
852
942
  schema: { type: string }
853
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.
854
952
  responses:
855
953
  '200':
856
954
  description: Keyset page of session summaries.
@@ -966,6 +1064,10 @@ paths:
966
1064
  required: [deleted]
967
1065
  properties:
968
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'
969
1071
  '401': { $ref: '#/components/responses/Unauthorized' }
970
1072
  '404': { $ref: '#/components/responses/NotFound' }
971
1073
  '409': { $ref: '#/components/responses/Conflict' }
@@ -1081,6 +1183,14 @@ paths:
1081
1183
  schema: { type: string }
1082
1184
  '401': { $ref: '#/components/responses/Unauthorized' }
1083
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' }
1084
1194
  '501': { $ref: '#/components/responses/NotImplemented' }
1085
1195
  '503': { description: 'Subscription connection cap exceeded — Retry-After set; fall back to polling /head.' }
1086
1196
 
@@ -1109,11 +1219,16 @@ paths:
1109
1219
  application/json:
1110
1220
  schema:
1111
1221
  type: object
1222
+ # 封闭两层(census 批2 第三段,2026-07-30):铸造点 `routes/sessions.ts:81-85` 是两层字面量,
1223
+ # 唯一条件键 `mode`(`config.autonomy` 缺席即省略)。description 里许诺的 tools/agents/
1224
+ # commands/cwd 至今没铸——封闭把「将来长出来要先过 spec」变成机器事实。
1225
+ additionalProperties: false
1112
1226
  required: [sessionId, model]
1113
1227
  properties:
1114
1228
  sessionId: { type: string }
1115
1229
  model:
1116
1230
  type: object
1231
+ additionalProperties: false
1117
1232
  required: [provider, modelId]
1118
1233
  properties:
1119
1234
  provider: { type: string }
@@ -1226,6 +1341,8 @@ paths:
1226
1341
  application/json:
1227
1342
  schema:
1228
1343
  type: object
1344
+ # 封闭(census 批2 第三段,2026-07-30):live 臂铸造点 `routes/notify-wake.ts:86` 是两键字面量。
1345
+ additionalProperties: false
1229
1346
  required: [sessionId, delivery]
1230
1347
  properties:
1231
1348
  sessionId: { type: string }
@@ -1236,7 +1353,9 @@ paths:
1236
1353
  application/json:
1237
1354
  schema:
1238
1355
  type: object
1239
- required: [sessionId, delivery]
1356
+ # 封闭 + note 补 required:parked 臂铸造点 `routes/notify-wake.ts:104` 是三键字面量,note 无条件。
1357
+ additionalProperties: false
1358
+ required: [sessionId, delivery, note]
1240
1359
  properties:
1241
1360
  sessionId: { type: string }
1242
1361
  delivery: { type: string, enum: [parked] }
@@ -1274,22 +1393,31 @@ paths:
1274
1393
  properties:
1275
1394
  message: { type: string, minLength: 1 }
1276
1395
  responses:
1277
- '202':
1278
- description: Resumed — the woken leg is driving into the durable run log.
1396
+ '200':
1397
+ # 🔴 census 批2 第三段(2026-07-30)spec 谎修正:此前这里写的是 **202** + required [taskId,…]。
1398
+ # 亲读真码:wake 的成功腿是 `driveResumeIntoRunLog` 的返回(`server.ts:2289` 与取消臂 `:2240`),
1399
+ # **恒为 200**(wake 是同步驱动到终局/再挂起,不是 accepted 回执),且 `taskId` 有条件
1400
+ # (无活跃行即省略——ApprovalDecisionResult 同一铸造点的同一注)。旧 202+required taskId 双谎。
1401
+ description: Woken — the resumed leg ran to its next settle (terminal, re-suspended, or needs_review).
1279
1402
  content:
1280
1403
  application/json:
1281
1404
  schema:
1282
1405
  type: object
1283
- required: [taskId, sessionId, status]
1406
+ additionalProperties: false
1407
+ required: [sessionId, status]
1284
1408
  properties:
1285
- taskId: { type: string }
1409
+ taskId: { type: string, description: 'The active run id; OMITTED when no active row.' }
1286
1410
  sessionId: { type: string }
1287
- status: { type: string }
1411
+ status: { type: string, description: 'The run''s resulting status (completed/failed/suspended/needs_review; blocked/timeout also occur — core TaskStatus is passed through verbatim).' }
1412
+ errorCode: { type: string, description: 'Failure code when the resumed leg failed (e.g. `cancelled`).' }
1413
+ errorMessage: { type: string, description: 'Human-readable failure detail (server-side secret-redacted).' }
1414
+ retriable: { type: boolean, description: 'true = the park is STILL pending (re-parked); refetch and retry.' }
1288
1415
  '400': { $ref: '#/components/responses/BadRequest' }
1289
1416
  '401': { $ref: '#/components/responses/Unauthorized' }
1290
1417
  '404': { description: 'No parked checkpoint for this session (nothing to wake), or session not visible to the caller.' }
1291
1418
  '409': { description: 'Pending gate (`wake.gate_pending` — decide it through its own entry) or resume context missing.' }
1292
1419
  '422': { description: '`steering.invalid_content` — the message failed the steering content gate.' }
1420
+ '429': { $ref: '#/components/responses/RateLimited' }
1293
1421
  '501': { $ref: '#/components/responses/NotImplemented' }
1294
1422
 
1295
1423
  /v1/sessions/{sessionId}/workspace:
@@ -1322,6 +1450,9 @@ paths:
1322
1450
  application/json:
1323
1451
  schema:
1324
1452
  type: object
1453
+ # 封闭两层(census 批2 第三段,2026-07-30):铸造点 `routes/sessions.ts:621-626`(信封)与
1454
+ # `:616`(行)。条件键=信封 `latest`(零快照即省略)、行 `bytes`(blobSizes 面缺席=诚实省略)。
1455
+ additionalProperties: false
1325
1456
  required: [sessionId, snapshots, total]
1326
1457
  properties:
1327
1458
  sessionId: { type: string }
@@ -1329,6 +1460,7 @@ paths:
1329
1460
  type: array
1330
1461
  items:
1331
1462
  type: object
1463
+ additionalProperties: false
1332
1464
  required: [key, files]
1333
1465
  properties:
1334
1466
  key: { type: string }
@@ -1378,6 +1510,9 @@ paths:
1378
1510
  application/json:
1379
1511
  schema:
1380
1512
  type: object
1513
+ # 封闭两层(census 批2 第三段,2026-07-30):铸造点 `routes/sessions.ts:648-654`(信封)与
1514
+ # `:651`(行)。条件键=信封 `nextOffset`(尾页省略)、行 `size`(blobSizes 面缺席省略)。
1515
+ additionalProperties: false
1381
1516
  required: [sessionId, key, entries, total]
1382
1517
  properties:
1383
1518
  sessionId: { type: string }
@@ -1386,6 +1521,7 @@ paths:
1386
1521
  type: array
1387
1522
  items:
1388
1523
  type: object
1524
+ additionalProperties: false
1389
1525
  required: [path, hash]
1390
1526
  properties:
1391
1527
  path: { type: string }
@@ -1516,6 +1652,10 @@ paths:
1516
1652
  required: [rules]
1517
1653
  properties:
1518
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'
1519
1659
  '401': { $ref: '#/components/responses/Unauthorized' }
1520
1660
  '404': { $ref: '#/components/responses/NotFound' }
1521
1661
  '409': { $ref: '#/components/responses/Conflict' }
@@ -1617,6 +1757,8 @@ paths:
1617
1757
  application/json:
1618
1758
  schema:
1619
1759
  type: object
1760
+ # 封闭(census 批2 第三段,2026-07-30):铸造点 `routes/session-sync.ts:165` 是单键字面量。
1761
+ additionalProperties: false
1620
1762
  required: [manifest]
1621
1763
  properties:
1622
1764
  manifest: { $ref: '#/components/schemas/SessionManifest' }
@@ -1793,7 +1935,10 @@ paths:
1793
1935
  2c session-sync PUSH Phase A — a SMALL metadata body (`entryIds, snapshots, policy, anchors, resolution?`); the
1794
1936
  entries stream in Phase B. The cloud classifies (§7), pre-checks blob presence (a missing referenced hash →
1795
1937
  422 `missing_blob` — PUT it first), guards the import lease + any active run (409), then mints a stagingId.
1796
- `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` →
1797
1942
  `{ stagingId, relation }`. 🔴 a `fork`/`stale` without `resolution:"overwrite-dst"` → 409
1798
1943
  `conflict` carrying the `relation` (the SDK surfaces SyncConflictError).
1799
1944
  requestBody:
@@ -1828,6 +1973,7 @@ paths:
1828
1973
  schema: { $ref: '#/components/schemas/SyncConflict' }
1829
1974
  '422': { $ref: '#/components/responses/UnprocessableEntity' }
1830
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 起)
1831
1977
 
1832
1978
  /v1/sessions/{sessionId}/sync/import/{stagingId}/entries:
1833
1979
  parameters:
@@ -1884,6 +2030,15 @@ paths:
1884
2030
  /v1/approvals:
1885
2031
  parameters:
1886
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.
1887
2042
  get:
1888
2043
  tags: [approvals]
1889
2044
  operationId: approvalsList
@@ -1905,14 +2060,27 @@ paths:
1905
2060
  /v1/approvals/stream:
1906
2061
  parameters:
1907
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.
1908
2072
  get:
1909
2073
  tags: [approvals]
1910
2074
  operationId: approvalsStream
1911
2075
  x-status: live # spec-path-gate 首批回填 2026-07-27(design/80 native push;SDK approvals.stream 消费)
1912
2076
  summary: SSE — pending-approval deltas (subscribe once instead of polling GET /v1/approvals).
1913
2077
  description: >
1914
- Frames: `meta` ({type,version,mode:"approvals-delta",pollMs}) then `pending`/`resolved` deltas (each
1915
- 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
1916
2084
  construction (polls the SHARED checkpoint table). 15-min cap + heartbeats; a DB blip retries, never
1917
2085
  kills the stream. Scope = the caller's principal (operator/trace token = fleet-wide).
1918
2086
  responses:
@@ -1950,6 +2118,18 @@ paths:
1950
2118
  content:
1951
2119
  application/json:
1952
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' }
1953
2133
  '401': { $ref: '#/components/responses/Unauthorized' }
1954
2134
  '404': { $ref: '#/components/responses/NotFound' }
1955
2135
  '409':
@@ -1959,17 +2139,19 @@ paths:
1959
2139
  NOT match the current pending action (TOCTOU/swap). The SDK splits these by `errorCode` into
1960
2140
  ConflictError vs ApprovalBindingMismatchError. binding_mismatch = SAFETY signal: refetch + re-present
1961
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.
1962
2147
  content:
1963
2148
  application/json:
1964
2149
  schema: { $ref: '#/components/schemas/ErrorResponse' }
1965
- '410':
1966
- description: >
1967
- design/80 D-1 `approval_stale` (`errorCode`, `terminal`: resolved|expired_abort|expired_sla) — the
1968
- checkpoint is already terminal (someone resolved it / abandonment-TTL GC / SLA resolve-deny). Client
1969
- action: refetch the inbox; this pending is gone. SDK → ApprovalStaleError.
1970
- content:
1971
- application/json:
1972
- 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.
1973
2155
  '413':
1974
2156
  description: >
1975
2157
  errorCode "reason_too_large" — `reason` exceeded 4096 chars (same cap on plan_review). REJECTED,
@@ -2005,6 +2187,41 @@ paths:
2005
2187
  content:
2006
2188
  application/json:
2007
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' }
2008
2225
  '401': { $ref: '#/components/responses/Unauthorized' }
2009
2226
  '404': { $ref: '#/components/responses/NotFound' }
2010
2227
  '501':
@@ -2048,6 +2265,14 @@ paths:
2048
2265
  required: [revoked]
2049
2266
  properties:
2050
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' }
2051
2276
  '401': { $ref: '#/components/responses/Unauthorized' }
2052
2277
  '404': { $ref: '#/components/responses/NotFound' }
2053
2278
  '501':
@@ -2241,6 +2466,7 @@ paths:
2241
2466
  schema: { $ref: '#/components/schemas/TurnList' }
2242
2467
  '401': { $ref: '#/components/responses/Unauthorized' }
2243
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
2244
2470
 
2245
2471
  /v1/tasks/source-summary:
2246
2472
  parameters:
@@ -2262,18 +2488,27 @@ paths:
2262
2488
  application/json:
2263
2489
  schema:
2264
2490
  type: object
2265
- required: [rows]
2491
+ # 🔴 封闭 + 两处 spec 谎修正(census 批2 第三段,2026-07-30),铸造点 `routes/trace-usage.ts:61`:
2492
+ # ① 顶层信封 server 真发 `{sinceSec, rows}` —— `sinceSec`(生效的窗口秒数,默认 86400)
2493
+ # 此前整键不在 spec 里;
2494
+ # ② 行 `source` 三个 store 实现(sql/pg/local 的 sourceSummary)签名都是 `string | null`
2495
+ # (无来源凭据的 run 记 null),spec 却写成裸 string。
2496
+ additionalProperties: false
2497
+ required: [sinceSec, rows]
2266
2498
  properties:
2499
+ sinceSec: { type: integer, description: 'The effective window seconds (echo of ?sinceSec, default 86400).' }
2267
2500
  rows:
2268
2501
  type: array
2269
2502
  items:
2270
2503
  type: object
2504
+ additionalProperties: false
2271
2505
  required: [source, status, count]
2272
2506
  properties:
2273
- source: { type: string }
2507
+ source: { type: [string, 'null'], description: 'Credential-derived system; null = runs submitted without a source.' }
2274
2508
  status: { type: string }
2275
2509
  count: { type: integer }
2276
2510
  '401': { $ref: '#/components/responses/Unauthorized' }
2511
+ '501': { $ref: '#/components/responses/NotImplemented' } # 无 durable run store(capability.run_store_required)——`routes/trace-usage.ts:56`,此前漏登记
2277
2512
 
2278
2513
  /v1/tasks/{taskId}/artifacts:
2279
2514
  parameters:
@@ -2327,6 +2562,7 @@ paths:
2327
2562
  schema: { $ref: '#/components/schemas/TraceStreamEvent' }
2328
2563
  '401': { $ref: '#/components/responses/Unauthorized' }
2329
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
2330
2566
 
2331
2567
  /v1/usage:
2332
2568
  parameters:
@@ -2359,7 +2595,10 @@ paths:
2359
2595
  summary: Windowed usage totals (usage-analytics face).
2360
2596
  description: >
2361
2597
  READ-ONLY analytics aggregate over the caller's principal (operator/trace token = fleet-wide). Query:
2362
- `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
2363
2602
  object (additive fields ride; consumers branch on known keys only).
2364
2603
  responses:
2365
2604
  '200':
@@ -2441,8 +2680,9 @@ paths:
2441
2680
  summary: Non-secret model catalog (the `@model` picker / autocomplete data source).
2442
2681
  description: >
2443
2682
  READ-ONLY catalog of `@mention`-pickable models. The user selects a model PER-TASK by typing `@<name>` in
2444
- the objective body (the service allowlist-parses it; the client never re-decides). Envelope `{ models, default }`
2445
- (`default` = the deployment default model id, display-only — the internal `default` alias is hidden).
2683
+ the objective body (the service allowlist-parses it; the client never re-decides). Envelope
2684
+ `{ models, default, defaultModel }` (`default` = the deployment default model id, display-only — the
2685
+ internal `default` alias is hidden; `defaultModel` = the same default as `{id, name?}` in one piece).
2446
2686
  🔴 NON-SECRET: only name/id/provider/reasoning/vision + the E4/E7 capability fields — NEVER baseUrl/apiKey/
2447
2687
  headers (the service strips them). Fail-safe: on error, render an empty picker (never block).
2448
2688
  responses:
@@ -2452,12 +2692,24 @@ paths:
2452
2692
  application/json:
2453
2693
  schema:
2454
2694
  type: object
2455
- required: [models, default]
2695
+ # 🔴 封闭 + `defaultModel` 补登记(census 批2 第三段,2026-07-30):铸造点
2696
+ # `routes/capabilities.ts:322` 三键字面量。`defaultModel`([865]③ id/name 撕裂消解,server
2697
+ # 已发多版)SDK `models.list()` 的返回类型早就带,**spec 是唯一没登记的一侧** —— 生成式
2698
+ # 消费方对它全瞎。`name` 条件(目录无命中即省略),`id` 恒在。
2699
+ additionalProperties: false
2700
+ required: [models, default, defaultModel]
2456
2701
  properties:
2457
2702
  models:
2458
2703
  type: array
2459
2704
  items: { $ref: '#/components/schemas/ModelInfo' }
2460
- default: { type: string, description: "deployment default model id (display-only)" }
2705
+ default: { type: string, description: "deployment default model id (display-only; legacy id-form, byte-stable)" }
2706
+ defaultModel:
2707
+ type: object
2708
+ additionalProperties: false
2709
+ required: [id]
2710
+ properties:
2711
+ id: { type: string }
2712
+ name: { type: string, description: 'the catalog @handle of the default model; omitted when the catalog has no match (env single-model lane).' }
2461
2713
  '401': { $ref: '#/components/responses/Unauthorized' }
2462
2714
 
2463
2715
  /v1/elicitations/{id}/respond:
@@ -2562,7 +2814,9 @@ paths:
2562
2814
  description: Ack (`decision` echoed).
2563
2815
  content:
2564
2816
  application/json:
2565
- schema: { type: object, additionalProperties: true }
2817
+ # census 批2 第三段(2026-07-30):这条 200 原本是裸 `{type: object, additionalProperties: true}`,
2818
+ # 而 `ToolApprovalRespondAck` 本文件里早就有、path 从来没引用(usage 三兄弟同款脱钩)。接上。
2819
+ schema: { $ref: '#/components/schemas/ToolApprovalRespondAck' }
2566
2820
  '400': { description: Malformed body (existence-independent). }
2567
2821
  '401': { $ref: '#/components/responses/Unauthorized' }
2568
2822
  '404': { description: Unknown/settled/expired/foreign approval (no existence oracle). }
@@ -2591,7 +2845,9 @@ paths:
2591
2845
  application/json:
2592
2846
  schema: { type: object, additionalProperties: true }
2593
2847
  '401': { $ref: '#/components/responses/Unauthorized' }
2594
- '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.
2595
2851
 
2596
2852
  /v1/workflows:
2597
2853
  parameters:
@@ -2617,6 +2873,13 @@ paths:
2617
2873
  required: false
2618
2874
  schema: { type: integer, minimum: 1, maximum: 100, default: 50 }
2619
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.
2620
2883
  responses:
2621
2884
  '200':
2622
2885
  description: Owner's workflow-run summaries.
@@ -2625,6 +2888,7 @@ paths:
2625
2888
  schema:
2626
2889
  type: object
2627
2890
  required: [workflows]
2891
+ additionalProperties: false
2628
2892
  properties:
2629
2893
  workflows:
2630
2894
  type: array
@@ -2701,6 +2965,13 @@ paths:
2701
2965
  application/json:
2702
2966
  schema: { $ref: '#/components/schemas/AttachmentInfo' }
2703
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' }
2704
2975
  '413': { description: 'attachment.too_large' }
2705
2976
  '415': { description: 'attachment.mime_not_allowed' }
2706
2977
  '501': { description: 'attachment store not configured' }
@@ -2718,7 +2989,15 @@ paths:
2718
2989
  summary: Download an attachment's bytes (owner-gated, no oracle — unknown and foreign both 404).
2719
2990
  responses:
2720
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' }
2721
2999
  '404': { description: 'unknown or not owned' }
3000
+ '501': { description: '(declared 2026-07-31) errorCode "capability.attachment_store_required" — no attachment store configured.' }
2722
3001
  delete:
2723
3002
  tags: [tasks]
2724
3003
  operationId: deleteAttachment
@@ -2726,10 +3005,33 @@ paths:
2726
3005
  summary: Delete an attachment (owner-gated; 204 on success).
2727
3006
  responses:
2728
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' }
2729
3015
  '404': { description: 'unknown or not owned' }
3016
+ '501': { description: '(declared 2026-07-31) errorCode "capability.attachment_store_required" — no attachment store configured.' }
2730
3017
  /v1/fleet/stream:
2731
3018
  parameters:
2732
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.
2733
3035
  get:
2734
3036
  tags: [fleet]
2735
3037
  operationId: fleetStream
@@ -2768,7 +3070,10 @@ paths:
2768
3070
  `LEADER_ENABLED` (default OFF) + `REMOTE_EXEC=e2b`. Mode is a SERVER deploy flag, not a client choice:
2769
3071
  `LEADER_FANOUT_ENABLED=true` (default) lets the router fan out to N isolated workers (= the value-HOLD
2770
3072
  correctness bet); `=false` pins a single worker. Either way it runs the E2B+git pipeline. Door B's brain
2771
- 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.)
2772
3077
  requestBody:
2773
3078
  required: true
2774
3079
  content:
@@ -2788,7 +3093,13 @@ paths:
2788
3093
  application/json:
2789
3094
  schema: { $ref: '#/components/schemas/LeaderReceipt' }
2790
3095
  '401': { $ref: '#/components/responses/Unauthorized' }
2791
- '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' }
2792
3103
 
2793
3104
  /v1/leader/{leaderRunId}:
2794
3105
  parameters:
@@ -2843,7 +3154,7 @@ paths:
2843
3154
  schema:
2844
3155
  type: object
2845
3156
  required: [images]
2846
- additionalProperties: true
3157
+ additionalProperties: false
2847
3158
  properties:
2848
3159
  images:
2849
3160
  type: array
@@ -2861,9 +3172,17 @@ paths:
2861
3172
  summary: 'OPERATOR: register one image-index entry → { id }.'
2862
3173
  requestBody: { required: true, content: { application/json: { schema: { type: object, additionalProperties: true } } } }
2863
3174
  responses:
2864
- '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 } } } }
2865
3176
  '400': { $ref: '#/components/responses/BadRequest' }
2866
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' }
2867
3186
 
2868
3187
  /v1/images/{profile}:
2869
3188
  parameters:
@@ -2875,7 +3194,9 @@ paths:
2875
3194
  x-status: live
2876
3195
  summary: 'Newest PUBLISHED entry for a profile → { image }.'
2877
3196
  responses:
2878
- '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' } } } } } }
2879
3200
  '404': { $ref: '#/components/responses/NotFound' }
2880
3201
 
2881
3202
  /v1/images/digests/{digest}:
@@ -2888,7 +3209,8 @@ paths:
2888
3209
  x-status: live
2889
3210
  summary: 'Entry by immutable digest → { image }.'
2890
3211
  responses:
2891
- '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' } } } } } }
2892
3214
  '404': { $ref: '#/components/responses/NotFound' }
2893
3215
 
2894
3216
  /v1/images/bakes:
@@ -2904,11 +3226,31 @@ paths:
2904
3226
  busy ⇒ 409 {activeBakeId?, eventsUrl?}; submission rate-limit ⇒ 429 + retryAfterSec.
2905
3227
  requestBody: { required: true, content: { application/json: { schema: { type: object, additionalProperties: true } } } }
2906
3228
  responses:
2907
- '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' } } } }
2908
3230
  '400': { $ref: '#/components/responses/BadRequest' }
2909
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' }
2910
3237
  '409': { description: 'Busy — { activeBakeId?, eventsUrl? }.' }
2911
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' }
2912
3254
 
2913
3255
  /v1/images/bakes/claim:
2914
3256
  parameters:
@@ -2919,9 +3261,16 @@ paths:
2919
3261
  x-status: live # bake-runner credential face (machine-to-machine).
2920
3262
  summary: 'RUNNER: claim the oldest queued bake (discover+CAS in one step; 204 = queue empty).'
2921
3263
  responses:
2922
- '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' } } } }
2923
3265
  '204': { description: 'Queue empty.' }
2924
- '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' }
2925
3274
 
2926
3275
  /v1/images/bakes/{bakeId}:
2927
3276
  parameters:
@@ -2931,9 +3280,14 @@ paths:
2931
3280
  tags: [sandbox]
2932
3281
  operationId: bakesGet
2933
3282
  x-status: live
2934
- 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…).'
2935
3284
  responses:
2936
- '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' }
2937
3291
  '404': { $ref: '#/components/responses/NotFound' }
2938
3292
 
2939
3293
  /v1/images/bakes/{bakeId}/cancel:
@@ -2944,9 +3298,14 @@ paths:
2944
3298
  tags: [sandbox]
2945
3299
  operationId: bakesCancel
2946
3300
  x-status: live
2947
- 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).'
2948
3302
  responses:
2949
- '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' }
2950
3309
  '404': { $ref: '#/components/responses/NotFound' }
2951
3310
 
2952
3311
  /v1/images/bakes/{bakeId}/claim:
@@ -2959,7 +3318,12 @@ paths:
2959
3318
  x-status: live
2960
3319
  summary: 'RUNNER: claim THIS queued bake (single-flight CAS; losing the race ⇒ 409).'
2961
3320
  responses:
2962
- '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' }
2963
3327
  '409': { description: 'Claim race lost / not queued.' }
2964
3328
 
2965
3329
  /v1/images/bakes/{bakeId}/ingest:
@@ -2971,10 +3335,19 @@ paths:
2971
3335
  operationId: bakesIngest
2972
3336
  x-status: live
2973
3337
  summary: 'RUNNER: push one build.sh event line + lease heartbeat (x-bake-ingest-secret header from the claim).'
2974
- 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.'
2975
3339
  requestBody: { required: true, content: { application/json: { schema: { type: object, additionalProperties: true } } } }
2976
3340
  responses:
2977
- '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' }
2978
3351
  '409': { description: 'Lease invalid / claimed by another runner.' }
2979
3352
 
2980
3353
  /v1/images/bakes/{bakeId}/events:
@@ -2985,9 +3358,14 @@ paths:
2985
3358
  tags: [sandbox]
2986
3359
  operationId: bakesEvents
2987
3360
  x-status: live
2988
- 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).'
2989
3362
  responses:
2990
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' }
2991
3369
  '404': { $ref: '#/components/responses/NotFound' }
2992
3370
 
2993
3371
  /v1/images/select:
@@ -3061,14 +3439,19 @@ paths:
3061
3439
  The approval row carries the FULL tool-call args (commands/paths/contents of a high-risk op), so the
3062
3440
  by-id read enforces the same tenant boundary as the list: operators read across tenants; everyone else
3063
3441
  reads ONLY their own scope (the guard fires at the SQL layer — no cross-tenant args even if an id
3064
- leaks). Non-owner → 404 (never 403, no existence oracle). Present only on deployments with the
3065
- 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.
3066
3449
  responses:
3067
3450
  '200':
3068
3451
  description: The approval row (full args projection).
3069
3452
  content:
3070
3453
  application/json:
3071
- schema: { type: object, additionalProperties: true }
3454
+ schema: { $ref: '#/components/schemas/ApprovalRow' }
3072
3455
  '401': { $ref: '#/components/responses/Unauthorized' }
3073
3456
  '404': { $ref: '#/components/responses/NotFound' }
3074
3457
 
@@ -3099,11 +3482,14 @@ paths:
3099
3482
  application/json:
3100
3483
  schema:
3101
3484
  type: object
3485
+ # 🔴 census 批2 五段:CLOSED — exact literal `{ scope, exportedAt, entries }`
3486
+ # (memory-policy.ts:58), no fourth key.
3102
3487
  required: [scope, exportedAt, entries]
3488
+ additionalProperties: false
3103
3489
  properties:
3104
3490
  scope: { type: string }
3105
3491
  exportedAt: { type: string, format: date-time }
3106
- entries: { type: array, items: { type: object, additionalProperties: true } }
3492
+ entries: { type: array, items: { $ref: '#/components/schemas/MemoryEntry' } }
3107
3493
  '400': { $ref: '#/components/responses/BadRequest' }
3108
3494
  '401': { $ref: '#/components/responses/Unauthorized' }
3109
3495
  '404': { $ref: '#/components/responses/NotFound' }
@@ -3139,7 +3525,7 @@ paths:
3139
3525
  description: 'The sync outcome (core reconcile/nextSyncBaseline projection).'
3140
3526
  content:
3141
3527
  application/json:
3142
- schema: { type: object, additionalProperties: true }
3528
+ schema: { $ref: '#/components/schemas/MemorySyncResponse' }
3143
3529
  '400': { $ref: '#/components/responses/BadRequest' }
3144
3530
  '401': { $ref: '#/components/responses/Unauthorized' }
3145
3531
  '404': { $ref: '#/components/responses/NotFound' }
@@ -3175,7 +3561,15 @@ paths:
3175
3561
  description: Aggregation rows.
3176
3562
  content:
3177
3563
  application/json:
3178
- 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' } }
3179
3573
  '403': { description: 'Multi-tenant worker: outcomes view is operator-only.' }
3180
3574
  '501': { $ref: '#/components/responses/NotImplemented' }
3181
3575
 
@@ -3217,9 +3611,13 @@ paths:
3217
3611
  application/json:
3218
3612
  schema:
3219
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.
3220
3617
  required: [links]
3618
+ additionalProperties: false
3221
3619
  properties:
3222
- links: { type: array, items: { type: object, additionalProperties: true } }
3620
+ links: { type: array, items: { $ref: '#/components/schemas/SendfileLinkRow' } }
3223
3621
  nextBefore: { type: string }
3224
3622
  '400': { $ref: '#/components/responses/BadRequest' }
3225
3623
  '401': { $ref: '#/components/responses/Unauthorized' }
@@ -3261,9 +3659,18 @@ paths:
3261
3659
  schema:
3262
3660
  type: object
3263
3661
  required: [runId, entries]
3662
+ additionalProperties: false
3264
3663
  properties:
3265
3664
  runId: { type: string }
3266
- 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'
3267
3674
  nextOffset: { type: integer }
3268
3675
  '401': { $ref: '#/components/responses/Unauthorized' }
3269
3676
  '404': { $ref: '#/components/responses/NotFound' }
@@ -3304,6 +3711,14 @@ paths:
3304
3711
  responses:
3305
3712
  '200':
3306
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' }
3307
3722
  '400': { $ref: '#/components/responses/BadRequest' }
3308
3723
  '401': { $ref: '#/components/responses/Unauthorized' }
3309
3724
  '404': { $ref: '#/components/responses/NotFound' }
@@ -3314,6 +3729,14 @@ paths:
3314
3729
  content:
3315
3730
  application/json:
3316
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' }
3317
3740
  '501': { $ref: '#/components/responses/NotImplemented' }
3318
3741
 
3319
3742
  /metrics/summary:
@@ -3333,7 +3756,12 @@ paths:
3333
3756
  description: Metrics summary (shape draft).
3334
3757
  content:
3335
3758
  application/json:
3336
- 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' }
3337
3765
  '401': { $ref: '#/components/responses/Unauthorized' }
3338
3766
 
3339
3767
  # ─────────────────────────────────────────────────────────────────────────────
@@ -3401,6 +3829,15 @@ components:
3401
3829
  content:
3402
3830
  application/json:
3403
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' }
3404
3841
  NotFound:
3405
3842
  description: >
3406
3843
  No such resource for this principal. Owner-scoped reads/cancel return 404 (not 403) for a non-owned or
@@ -3477,8 +3914,11 @@ components:
3477
3914
  `Capabilities.scenarios` instead. `source`/`builtin` = builtin vs center-config overlay; `toolset` names
3478
3915
  the tool bundle while `tools` are the resolved tool names; `promptSummary` is a human-readable prompt
3479
3916
  DIGEST (never the full prompt); `enabled` = a center scenario can be declared-but-disabled.
3917
+ # 封闭(census 批2 第三段,2026-07-30):路由 `routes/capabilities.ts:38` 原样发 `deps.scenarioDetails[name]`,
3918
+ # 其类型 `ScenarioDetail`(capabilities/scenarios.ts:353)恰是这 8 个必填键;builtin 工厂 `mk`(:379)与
3919
+ # center overlay 都按该接口铸,无第 9 键来源。
3480
3920
  required: [name, source, builtin, summary, toolset, tools, promptSummary, enabled]
3481
- additionalProperties: true
3921
+ additionalProperties: false
3482
3922
  properties:
3483
3923
  name: { type: string }
3484
3924
  source: { type: string, enum: [builtin, center] }
@@ -4077,11 +4517,15 @@ components:
4077
4517
  ResumeEvicted:
4078
4518
  type: object
4079
4519
  description: >
4080
- 416 body for an evicted SSE resume point (`GET /v1/runs/:id/events`). There is NO machine `code` field —
4081
- match the 416 STATUS and read `retainedFrom`, then full-sync via `/v1/tasks/:id/turns` (pinned SSE contract).
4082
- 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]
4083
4526
  properties:
4084
4527
  error: { type: string }
4528
+ errorCode: { type: string, enum: [limit.retention_evicted] }
4085
4529
  retainedFrom: { type: integer, description: Lowest event seq still retained; resume events from here. }
4086
4530
 
4087
4531
  ImageIndexEntry:
@@ -4097,7 +4541,7 @@ components:
4097
4541
  [id, profile, bands, repo, tag, digest, toolchainVersions, capabilities, podContract, sizeBytes, status,
4098
4542
  visibility, tenantId, manifestSha, recipeGitSha, generatorVersion, buildDate, supersedes, signed,
4099
4543
  createdAt, updatedAt]
4100
- additionalProperties: true
4544
+ additionalProperties: false
4101
4545
  properties:
4102
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.' }
4103
4547
  profile: { type: string }
@@ -4111,26 +4555,8 @@ components:
4111
4555
  back to bind an image (bind by `profile`; the server re-resolves + re-admits under a trusted
4112
4556
  principal, because trusting a caller-supplied digest would fail OPEN).
4113
4557
  toolchainVersions: { type: object, additionalProperties: { type: string } }
4114
- capabilities:
4115
- type: object
4116
- additionalProperties: true
4117
- properties:
4118
- browser: { type: boolean }
4119
- db: { type: boolean }
4120
- nestedBuild: { type: boolean, description: 'can it build container images (back-compat boolean).' }
4121
- nestedBuildMode:
4122
- type: string
4123
- enum: [none, docker-cli-only, rootless-buildkit, privileged-dind]
4124
- description: 'HOW it nests (the posture). Refines `nestedBuild`; absent ⇒ fall back to the boolean.'
4125
- podContract:
4126
- type: object
4127
- additionalProperties: true
4128
- properties:
4129
- devShmMB: { type: integer }
4130
- minMemMB: { type: integer }
4131
- minCpu: { type: string }
4132
- readyTimeoutSec: { type: integer }
4133
- securityContext: { type: object, additionalProperties: true }
4558
+ capabilities: { $ref: '#/components/schemas/ImageCapabilities' }
4559
+ podContract: { $ref: '#/components/schemas/ImagePodContract' }
4134
4560
  sizeBytes: { type: ['integer', 'null'] }
4135
4561
  status: { type: string, enum: [building, published, deprecated, failed] }
4136
4562
  visibility: { type: string, enum: [public, tenant] }
@@ -4144,14 +4570,47 @@ components:
4144
4570
  createdAt: { type: string, description: ISO date-time. }
4145
4571
  updatedAt: { type: string, description: ISO date-time. }
4146
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
+
4147
4601
  ImageSelectResult:
4148
- # 新增(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`).
4149
4607
  type: object
4150
4608
  description: >
4151
4609
  `POST /v1/images/select` resolution result — a PREVIEW of "which digest/capabilities does this profile
4152
4610
  resolve to" for the UI. 🔴 NOT a binding credential: to actually bind, send `sandboxImageProfile` on the
4153
4611
  task request and let the server resolve under a trusted principal.
4154
- additionalProperties: true
4612
+ required: [profile, digest, repo, ref, podContract, capabilities, manifestSha]
4613
+ additionalProperties: false
4155
4614
  properties:
4156
4615
  profile: { type: string }
4157
4616
  digest: { type: string }
@@ -4161,15 +4620,9 @@ components:
4161
4620
  description: >
4162
4621
  `${repo}@${digest}` — always present on a 200. This is the value a caller threads into the kata
4163
4622
  adapter's per-sandbox image (the server handler says so itself), i.e. it is load-bearing, not decorative.
4164
- podContract: { type: object, additionalProperties: true }
4165
- capabilities:
4166
- type: object
4167
- additionalProperties: true
4168
- properties:
4169
- browser: { type: boolean }
4170
- db: { type: boolean }
4171
- nestedBuild: { type: boolean }
4172
- 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).' }
4173
4626
 
4174
4627
  QuotaError:
4175
4628
  type: object
@@ -4311,17 +4764,32 @@ components:
4311
4764
 
4312
4765
  LeaderReceipt:
4313
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.
4314
4772
  required: [leaderRunId, status]
4773
+ additionalProperties: false
4315
4774
  properties:
4316
4775
  leaderRunId: { type: string }
4317
4776
  status: { type: string, enum: [running, completed, failed] }
4318
4777
 
4319
4778
  LeaderRecord:
4320
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 一直缺它,是个只有场景真触发才会现形的假红洞(一个真实合法帧被判违约)。
4321
4788
  required: [id, status]
4789
+ additionalProperties: false
4322
4790
  properties:
4323
4791
  id: { type: string }
4324
- status: { type: string, enum: [running, completed, failed] }
4792
+ status: { type: string, enum: [running, completed, failed, needs_human] }
4325
4793
  result: {}
4326
4794
  error: { type: string }
4327
4795
 
@@ -4360,6 +4828,30 @@ components:
4360
4828
  Approve-with-edit (design/37 last-wins): operator's rewritten tool args, applied AFTER the binding
4361
4829
  check. `boundInputHash` STILL binds the ORIGINAL input the human saw — never hash updatedInput.
4362
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
+
4363
4855
  Capabilities:
4364
4856
  type: object
4365
4857
  description: >
@@ -4555,14 +5047,160 @@ components:
4555
5047
  egress: { type: boolean }
4556
5048
  irreversibility: { type: string, enum: [always, never] }
4557
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
+
4558
5192
  ModelInfo:
4559
5193
  type: object
4560
5194
  description: >
4561
5195
  A non-secret `@model` catalog entry (GET /v1/models; service src/http/routes/capabilities.ts). `name` = the `@handle` the
4562
5196
  user types in the objective body to pick this model per-task. 🔴 NON-SECRET — name/capabilities only, never
4563
- baseUrl/apiKey/headers (the service strips them). Open set (additive future fields).
4564
- required: [name]
4565
- additionalProperties: true
5197
+ baseUrl/apiKey/headers (the service strips them).
5198
+ # 🔴 封闭 + required 补铸造点(census 批2 第三段,2026-07-30):铸造点 `routes/capabilities.ts:295-313`
5199
+ # 是逐键字面量投影(白名单,不是透传——「open set」旧注不成立,新键要先过这道 spec)。core Model 类型
5200
+ # 里 id/provider/reasoning 都是必填、`vision` 由 `input` 恒算 ⇒ 五键无条件;contextWindow/maxOutputTokens
5201
+ # 0/缺省即省略,supportedEffortLevels 仅 reasoning 模型。
5202
+ required: [name, id, provider, reasoning, vision]
5203
+ additionalProperties: false
4566
5204
  properties:
4567
5205
  name: { type: string, description: 'the @mention handle (user-pickable model name).' }
4568
5206
  id: { type: string, description: 'underlying model id (display, e.g. deepseek-v4-pro).' }
@@ -4598,6 +5236,8 @@ components:
4598
5236
  ElicitRespondAck:
4599
5237
  type: object
4600
5238
  description: The 200 ack from a successful elicitation respond (service elicitation.ts:267).
5239
+ # 封闭(census 批2 第三段,2026-07-30):铸造点是三键字面量,三键全部无条件。
5240
+ additionalProperties: false
4601
5241
  required: [elicitationId, delivery, action]
4602
5242
  properties:
4603
5243
  elicitationId: { type: string }
@@ -4630,6 +5270,8 @@ components:
4630
5270
  QuestionRespondAck:
4631
5271
  type: object
4632
5272
  description: The 200 ack from a successful question respond (service question.ts:226).
5273
+ # 封闭(census 批2 第三段,2026-07-30):铸造点是两键字面量。
5274
+ additionalProperties: false
4633
5275
  required: [questionId, delivery]
4634
5276
  properties:
4635
5277
  questionId: { type: string }
@@ -4713,14 +5355,19 @@ components:
4713
5355
  type: object
4714
5356
  description: >
4715
5357
  GET /v1/workflows list row = core summarizeWorkflowRun projection (owner-scoped; scope = the creating
4716
- 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).
4717
5362
  required: [id, scope, status, phaseCount, agentCount, tokens, startedAt, createdAt]
4718
- additionalProperties: true
5363
+ additionalProperties: false
4719
5364
  properties:
4720
5365
  id: { type: string, description: 'Run id (= WorkflowRun.id).' }
4721
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.' }
4722
5368
  name: { type: string, description: "The workflow script's declared `meta.name`." }
4723
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).' }
4724
5371
  currentPhase: { type: string, description: 'Title of the phase currently executing (absent once terminal).' }
4725
5372
  status: { $ref: '#/components/schemas/WorkflowRunStatus' }
4726
5373
  phaseCount: { type: integer, description: 'Phases recorded.' }
@@ -4740,25 +5387,40 @@ components:
4740
5387
  🔴 `arg` is a SHORT primary-arg SUMMARY (command→name, path→basename, url→origin+path), secret-scrubbed
4741
5388
  and truncated (~80 code points) by core. It exists for the monitor's `Read(path)` / `Bash(grep …)`
4742
5389
  display — it is NOT the full args. RENDER it; NEVER re-feed it to a model.
4743
- 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
4744
5395
  properties:
4745
5396
  phase: { type: string, enum: [start, end] }
4746
5397
  toolCallId: { type: string }
4747
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).' }
4748
5400
  arg: { type: string, description: 'SHORT, secret-scrubbed, ~80-code-point summary (set on `phase:"start"`). UNTRUSTED display text.' }
4749
5401
  isError: { type: boolean, description: 'set on `phase:"end"` — whether the tool call errored.' }
4750
5402
 
4751
5403
  WorkflowAgentRow:
4752
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).
4753
5411
  type: object
4754
5412
  description: >
4755
- One agent-run row in a workflow's detail. The shape is PERMISSIVE — read the known render fields below,
4756
- tolerate the rest as the projection evolves.
4757
- 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
4758
5416
  properties:
4759
- label: { type: string, description: 'display label.' }
5417
+ label: { type: string, description: 'display label (redacted — LLM-authored).' }
4760
5418
  status: { type: string, description: 'lifecycle status (the input vocabulary the display derivation reads).' }
4761
- 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.' }
4762
5424
  model: { type: string, description: 'per-agent model DISPLAY label (e.g. "Opus 4.8 (1M context)"), not an id.' }
4763
5425
  tokens: { type: integer }
4764
5426
  turns: { type: integer }
@@ -4767,44 +5429,149 @@ components:
4767
5429
  type: array
4768
5430
  description: 'the "Activity" tail — the LAST-N tool-call beats (bounded).'
4769
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.' }
4770
5438
  prompt: { type: string, description: 'what the worker was ASKED (core-redacted + bounded). UNTRUSTED display text.' }
4771
5439
  output: { type: string, description: 'the worker''s final OUTPUT (core-redacted + bounded). UNTRUSTED display text.' }
4772
5440
 
4773
- 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`).
4774
5445
  type: object
4775
- description: >
4776
- GET /v1/workflows/:id full run (core WorkflowRun). Non-owner → 404 (no existence oracle).
4777
- 🔴 phases/agents/stats nested shapes may evolve → permissive (read length/known fields, tolerate the rest).
4778
- required: [id, scope, status, phases, agents, stats, startedAt, createdAt]
4779
- 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
4780
5449
  properties:
4781
- id: { type: string }
4782
- scope: { type: string }
4783
- name: { type: string, description: "The workflow script's declared `meta.name`." }
4784
- description: { type: string, description: "The workflow script's declared `meta.description` (one-liner)." }
4785
- status: { $ref: '#/components/schemas/WorkflowRunStatus' }
4786
- phases: { type: array, description: 'Phase records (permissive).', items: {} }
4787
- # `items: {}`(= 任意值,零形状)改为具名 $ref(2026-07-25 回填批):这是 workflow 监视面最吃重的数组,
4788
- # 空 schema 等于对非 TS 消费方完全没描述过它。`WorkflowAgentRow` 仍是 PERMISSIVE(additionalProperties)——
4789
- # 只钉住监视面真正 key 的那些字段,其余随投影演进。
4790
- agents:
4791
- type: array
4792
- description: 'Agent-run records (permissive — read the known render fields, tolerate the rest).'
4793
- items: { $ref: '#/components/schemas/WorkflowAgentRow' }
4794
- 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:
4795
5489
  type: object
4796
- description: 'Token stats — own at stats.tokens, nested at stats.nested.tokens (R-5 own/nested split).'
4797
- additionalProperties: true
5490
+ required: [tokens, turns, tasks, costMicroUsd]
5491
+ additionalProperties: false
4798
5492
  properties:
4799
5493
  tokens: { type: integer }
4800
- nested:
4801
- type: object
4802
- additionalProperties: true
4803
- properties:
4804
- 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' }
4805
5518
  startedAt: { type: integer }
4806
5519
  endedAt: { type: integer }
4807
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).' }
4808
5575
 
4809
5576
  WorkflowStreamEvent:
4810
5577
  type: object
@@ -4831,8 +5598,11 @@ components:
4831
5598
  AttachmentInfo:
4832
5599
  type: object
4833
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.
4834
5604
  required: [id, name, mime, sha256, sizeBytes]
4835
- additionalProperties: true
5605
+ additionalProperties: false
4836
5606
  properties:
4837
5607
  id: { type: string }
4838
5608
  name: { type: string }
@@ -5623,13 +6393,17 @@ components:
5623
6393
  E9 (shell-host mcpStatus; core McpServerStatus core/mcp.ts:76) — one MCP server's status at materialization
5624
6394
  time. `status` is connected | failed (core never emits disabled). `error` (failed only) is service-redacted
5625
6395
  + length-bounded.
6396
+ # 封闭三层(census 批2 第三段,2026-07-30):行铸造点 `routes/sessions.ts:157-165` 逐键条件展开这 5 键;
6397
+ # `serverInfo` 不是透传——core 自己投影成两键字面量(core dist mcp.js:986 `{name, version}`)。
5626
6398
  required: [name, status]
5627
- additionalProperties: true
6399
+ additionalProperties: false
5628
6400
  properties:
5629
6401
  name: { type: string }
5630
6402
  status: { type: string, description: "connected | failed (open set)" }
5631
6403
  serverInfo:
5632
6404
  type: object
6405
+ additionalProperties: false
6406
+ required: [name, version]
5633
6407
  properties:
5634
6408
  name: { type: string }
5635
6409
  version: { type: string }
@@ -5641,8 +6415,10 @@ components:
5641
6415
  description: >
5642
6416
  `GET /v1/sessions/:id/mcp` envelope. `asOf` = THIS materialization moment (ISO). `degraded:true` (servers
5643
6417
  empty) ⇒ materialize timed out/failed (NOT "no MCP"). No MCP configured ⇒ `servers:[]` without `degraded`.
6418
+ # 封闭(census 批2 第三段,2026-07-30):三个铸造臂(`routes/sessions.ts:134` 空面板 / `:152` 超时
6419
+ # degraded / `:155-166` 正常)键集并集恰是这 3 键;degraded 只在超时臂铸。
5644
6420
  required: [asOf, servers]
5645
- additionalProperties: true
6421
+ additionalProperties: false
5646
6422
  properties:
5647
6423
  asOf: { type: string }
5648
6424
  servers:
@@ -5669,6 +6445,9 @@ components:
5669
6445
  description: >
5670
6446
  One E19 file snapshot keyed by a `SessionTreeEntry.id`. `manifest` = `[relPath, blobHash]` tuples; the blob
5671
6447
  BYTES are NOT inlined (they ride the content-addressed blob routes).
6448
+ # 封闭(census 批2 第三段,2026-07-30):server 侧类型+铸造点(`session-sync.ts:206` PULL 出 /
6449
+ # `routes/session-sync.ts:53` PUSH 校验形)都恰是这两键。
6450
+ additionalProperties: false
5672
6451
  required: [key, manifest]
5673
6452
  properties:
5674
6453
  key: { type: string }
@@ -5684,6 +6463,8 @@ components:
5684
6463
  SyncAnchor:
5685
6464
  type: object
5686
6465
  description: One E18 resume-at anchor. `owner` is RE-KEYED to the importing principal on a PUSH (§9).
6466
+ # 封闭(census 批2 第三段,2026-07-30):server 侧行类型 `session-sync.ts:52` 恰是这三键(owner 可 null 不可缺)。
6467
+ additionalProperties: false
5687
6468
  required: [eventId, entryId, owner]
5688
6469
  properties:
5689
6470
  eventId: { type: string }
@@ -5693,6 +6474,9 @@ components:
5693
6474
  SessionRulesRecord:
5694
6475
  type: object
5695
6476
  description: A (principal, rules) policy record — one row across ALL principals (E6 `listBySession`). Replayed verbatim on import (the cloud applies the tighten-only gate).
6477
+ # 封闭(census 批2 第三段,2026-07-30):core `SessionRulesRecord`(session-policy-store.d.ts:19)恰是
6478
+ # 这两键;`principal: undefined`(会话默认规则)在 JSON 里=整键省略。
6479
+ additionalProperties: false
5696
6480
  required: [rules]
5697
6481
  properties:
5698
6482
  principal: { type: string, description: Absent = the session-default rules. }
@@ -5703,6 +6487,9 @@ components:
5703
6487
  description: >
5704
6488
  2c session-sync `GET …/sync/manifest` payload (wrapped server-side as `{ manifest }`). The THIN cross-backend
5705
6489
  snapshot: entry IDS (oldest-first, NOT payloads) + per-snapshot relPath→blobHash + policy + anchors + leaf.
6490
+ # 封闭(census 批2 第三段,2026-07-30):铸造点 `session-sync.ts:226`(exportSessionManifest 尾部
6491
+ # 字面量)恰是这 7 键,全部无条件(leafId 缺席位=null 不省略)。
6492
+ additionalProperties: false
5706
6493
  required: [sessionId, entryIds, entryCount, leafId, snapshots, policy, anchors]
5707
6494
  properties:
5708
6495
  sessionId: { type: string }
@@ -5732,24 +6519,31 @@ components:
5732
6519
  §7 — how a SOURCE log relates to a DESTINATION log, decided over the entry-ID SETS. A discriminated union on
5733
6520
  `relation`. `fresh`/`identical` carry nothing else; `fast_forward` the appended tail; `stale` the dst entries
5734
6521
  the src lacks; `fork` the common ancestor + each side's exclusive ids.
6522
+ # 五臂全封闭(census 批2 第三段,2026-07-30):铸造点=分类器 `session-sync-kernel.ts:126-160`,
6523
+ # 五个 return 全是字面量,臂形与 core 导出的 `SyncRelation` 判别联合逐键一致。
5735
6524
  oneOf:
5736
6525
  - type: object
6526
+ additionalProperties: false
5737
6527
  required: [relation]
5738
6528
  properties: { relation: { const: fresh } }
5739
6529
  - type: object
6530
+ additionalProperties: false
5740
6531
  required: [relation]
5741
6532
  properties: { relation: { const: identical } }
5742
6533
  - type: object
6534
+ additionalProperties: false
5743
6535
  required: [relation, newEntryIds]
5744
6536
  properties:
5745
6537
  relation: { const: fast_forward }
5746
6538
  newEntryIds: { type: array, items: { type: string } }
5747
6539
  - type: object
6540
+ additionalProperties: false
5748
6541
  required: [relation, dstAheadBy]
5749
6542
  properties:
5750
6543
  relation: { const: stale }
5751
6544
  dstAheadBy: { type: array, items: { type: string } }
5752
6545
  - type: object
6546
+ additionalProperties: false
5753
6547
  required: [relation, commonAncestor, srcExclusive, dstExclusive]
5754
6548
  properties:
5755
6549
  relation: { const: fork }
@@ -5770,6 +6564,9 @@ components:
5770
6564
  description: >
5771
6565
  `POST …/sync/import` Phase-A result. `identical` → no `stagingId` (skip Phase B); `fresh`/`fast_forward` →
5772
6566
  `{ stagingId, relation }`. `relation` is the BARE classifier tag (not the full object).
6567
+ # 封闭(census 批2 第三段,2026-07-30):两个铸造臂——identical 臂 `routes/session-sync.ts:385-389`
6568
+ # (relation+basis+payloadVerified,无 stagingId)/ staged 臂 `:472`(stagingId+relation)。
6569
+ additionalProperties: false
5773
6570
  required: [relation]
5774
6571
  properties:
5775
6572
  stagingId: { type: string, description: Present iff Phase B is needed (absent on `identical`). }
@@ -5777,24 +6574,32 @@ components:
5777
6574
  # 🔴 server ≥1.277 —— 把分类**判据**写进响应,好让消费方不能把 200 `identical` 读成"内容已同步"。
5778
6575
  basis:
5779
6576
  type: string
5780
- enum: [entry-ids]
5781
- x-open-enum: true # 将来上了内容摘要判据会出现新值(如 entry-ids+digest);按未知值降级,别写死
6577
+ # 🔴 census 批2 第三段(2026-07-30)spec 谎修正:`entry-ids+digest` 不是"将来值"——1.277.0 落
6578
+ # digest 半场的**同一车**里铸造点就是 `basis: payloadVerified ? "entry-ids+digest" : "entry-ids"`
6579
+ # (routes/session-sync.ts:387),旧注却把它写成 x-open-enum 的假想例。补进闭集;x-open-enum 保留
6580
+ # (真正的将来值仍按未知值降级)。
6581
+ enum: [entry-ids, entry-ids+digest]
6582
+ x-open-enum: true
5782
6583
  description: >
5783
- The classification BASIS. Currently always `entry-ids`: §7 compares only the **entry id set**.
5784
- 🔴 Entry ids are uuidv7 — NOT content-addressed — so "same ids, different payload" is structurally
5785
- possible and the classifier is blind to payload AND parent structure. On `identical` the entries are
5786
- skipped ENTIRELY and the caller gets a 200 while the destination keeps its own content.
6584
+ The classification BASIS. `entry-ids`: §7 compared only the **entry id set** — entry ids are uuidv7,
6585
+ NOT content-addressed, so "same ids, different payload" is structurally possible and the classifier is
6586
+ blind to payload AND parent structure; on `identical` the entries are skipped ENTIRELY while the
6587
+ destination keeps its own content. `entry-ids+digest` (server ≥1.277): the caller ALSO supplied a
6588
+ comparable `logDigest` and it matched — payload equality was really verified.
5787
6589
  payloadVerified:
5788
6590
  type: boolean
5789
6591
  description: >
5790
- Whether the server verified PAYLOAD equality. Currently always `false` (see `basis`). A consumer MUST
5791
- distinguish "entry ids match (content NOT verified)" from "content is in sync" in its own wording.
6592
+ Whether the server verified PAYLOAD equality. `true` ⇔ `basis:"entry-ids+digest"` (the caller sent a
6593
+ comparable `logDigest` that matched); `false` = id-sets only — a consumer MUST distinguish "entry ids
6594
+ match (content NOT verified)" from "content is in sync" in its own wording.
5792
6595
  ⚠️ ABSENT ≠ verified: a server &lt;1.277 omits both fields and its basis was id-sets all the same —
5793
6596
  so test `payloadVerified !== true`, never `=== false`.
5794
6597
 
5795
6598
  ImportCommitted:
5796
6599
  type: object
5797
6600
  description: '`POST …/sync/import/:stagingId/entries` (Phase B) commit result — the AUTHORITATIVE in-txn re-classify tag.'
6601
+ # 封闭(census 批2 第三段,2026-07-30):铸造点 `routes/session-sync.ts:608`(`{ relation: committed.relation }`)单键字面量。
6602
+ additionalProperties: false
5798
6603
  required: [relation]
5799
6604
  properties:
5800
6605
  relation: { type: string, enum: [fresh, identical, fast_forward], description: The committed §7 tag. }
@@ -6859,6 +7664,24 @@ components:
6859
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".'
6860
7665
  detail: { type: string, description: 'Redacted supplement (e.g. why skipped). Additive / tolerate-absent.' }
6861
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
+
6862
7685
  BakeClaim:
6863
7686
  type: object
6864
7687
  description: >
@@ -6867,8 +7690,11 @@ components:
6867
7690
  secret (minted at claim, echoed on every ingest via the x-bake-ingest-secret header) + the
6868
7691
  profile/bands the runner needs for its profile-aware hard deadline. The runner executes `argv`
6869
7692
  VERBATIM — it never assembles its own command line. 204 (no body) = queue empty.
6870
- required: [bakeId, argv, push, dryRun, profile, ingestSecret, leaseUntil]
6871
- 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
6872
7698
  properties:
6873
7699
  bakeId: { type: string }
6874
7700
  argv:
@@ -6877,23 +7703,128 @@ components:
6877
7703
  description: 'The server-assembled, whitelist-vetted build command — execute verbatim, never self-assemble.'
6878
7704
  push: { type: boolean }
6879
7705
  dryRun: { type: boolean }
6880
- 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.' }
6881
7707
  profile: { type: string }
6882
- 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.' }
6883
7709
  ingestSecret: { type: string, description: 'Per-bake credential — the ONLY place it leaves image-api; echo it on every ingest.' }
6884
7710
  leaseUntil:
6885
7711
  description: 'Lease expiry (epoch ms or ISO timestamp depending on store backend).'
6886
7712
  oneOf: [{ type: number }, { type: string }]
6887
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
+
6888
7819
  OutcomeRow:
6889
7820
  type: object
6890
7821
  description: >
6891
7822
  One task-outcome ledger row of GET /v1/outcomes (envelope key `outcomes`; design/73 §7 read-only
6892
7823
  base). Shape = the SQL ledgers' per-(taskSignature, model) summary projection (server
6893
- tidb-outcome-ledger.ts:186 / pg-outcome-ledger.ts:124 — the only queryable sinks; a File sink
6894
- 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.
6895
7826
  required: [taskSignature, model, n, passRate, meanCostMicroUsd]
6896
- additionalProperties: true
7827
+ additionalProperties: false
6897
7828
  properties:
6898
7829
  taskSignature: { type: string }
6899
7830
  model: { type: string }
@@ -6908,8 +7839,10 @@ components:
6908
7839
  nextBefore?}`; server src/http/routes/sessions-list.ts — exact projection). A principal caller is PINNED to its
6909
7840
  own scope (?scope= ignored); a fleet credential may pass ?scope= (default = the single-user
6910
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.
6911
7844
  required: [id, filename, size, ttlSec, track, bucket, key, createdAt]
6912
- additionalProperties: true
7845
+ additionalProperties: false
6913
7846
  properties:
6914
7847
  id: { type: string, description: 'uuidv7 link id — also the `before` pagination cursor.' }
6915
7848
  filename: { type: string }
@@ -6946,8 +7879,11 @@ components:
6946
7879
  TaskOutput-tool text (the child's final assistant body once terminal) — UNTRUSTED model output,
6947
7880
  same posture as a run `result`. `output` = the engine registry's UnifiedTaskOutput projection
6948
7881
  passed through VERBATIM.
7882
+ # 封闭(census 批2 第三段,2026-07-30):两个铸造点(`routes/runs.ts:934` 窄面 4 键 / `:1155` generic
7883
+ # 面 4 键 + 条件 `...g14`)顶层键集就是这 5 个。`output` 子对象**保持开**——它是 registry 投影的
7884
+ # verbatim 透传(deps 签名 `details: unknown`),不是 server 铸的形。
6949
7885
  required: [taskId, target, content, output]
6950
- additionalProperties: true
7886
+ additionalProperties: false
6951
7887
  properties:
6952
7888
  taskId: { type: string }
6953
7889
  target: { type: string }
@@ -6983,12 +7919,14 @@ components:
6983
7919
  exact literal: `status:"running"`, `delivery:"accepted"`). ACCEPTANCE, not an outcome: compaction
6984
7920
  runs at the next turn boundary and a `compacted{trigger:"manual"}` event rides the run stream ONLY
6985
7921
  if anything was summarized (mooted/failed → no event).
6986
- required: [taskId]
6987
- additionalProperties: true
7922
+ # 封闭 + required 补铸造点(census 批2 第三段,2026-07-30):唯一铸造点 `routes/runs.ts:793` 是
7923
+ # 四键字面量,四键**全部无条件**——此前 required 只列 taskId,把恒在键写成了可缺。
7924
+ required: [taskId, status, delivery, note]
7925
+ additionalProperties: false
6988
7926
  properties:
6989
7927
  taskId: { type: string }
6990
7928
  status: { type: string, description: 'Literal "running" on today''s server.' }
6991
- delivery: { type: string, description: 'Literal "accepted" on today''s server.' }
7929
+ delivery: { type: string, enum: [accepted] }
6992
7930
  note: { type: string, description: 'The "compaction will run at the next turn boundary…" copy.' }
6993
7931
 
6994
7932
  DetachAck:
@@ -6998,12 +7936,14 @@ components:
6998
7936
  `toolCallId` echoed, `delivery:"requested"`). Fire-and-forget: acceptance only — the
6999
7937
  authoritative outcome is the tool call's own `tool_end.structured {detached:true, task_id}`;
7000
7938
  a non-detachable env makes the request a fail-safe no-op.
7001
- required: [taskId]
7002
- additionalProperties: true
7939
+ # 封闭 + required 补铸造点(census 批2 第三段,2026-07-30):唯一铸造点 `routes/runs.ts:848` 是
7940
+ # 四键字面量,四键全部无条件(toolCallId=请求回显,body 校验过必在)。
7941
+ required: [taskId, toolCallId, delivery, note]
7942
+ additionalProperties: false
7003
7943
  properties:
7004
7944
  taskId: { type: string }
7005
7945
  toolCallId: { type: string, description: 'Echoed from the request.' }
7006
- delivery: { type: string, description: 'Literal "requested" on today''s server.' }
7946
+ delivery: { type: string, enum: [requested] }
7007
7947
  note: { type: string, description: 'The fail-safe no-op copy.' }
7008
7948
 
7009
7949
  SideQueryRequest:
@@ -7087,8 +8027,10 @@ components:
7087
8027
  The 200 ack of POST /v1/tool-approvals/{approvalId}/respond (server tool-approval.ts:521 — exact
7088
8028
  shape, `decision` echoed). Owner-gated with a 404 and NO existence oracle: non-owner and
7089
8029
  unknown/settled/expired/wrong-replica ids are indistinguishable.
8030
+ # 封闭(census 批2 第三段,2026-07-30):铸造点 `tool-approval.ts:521` 三恒在键 + 两条件键
8031
+ # (rememberApplied/updatedInputForwarded 的条件展开就在同一行字面量里),键集恰是这 5 个。
7090
8032
  required: [approvalId, delivery, decision]
7091
- additionalProperties: true
8033
+ additionalProperties: false
7092
8034
  properties:
7093
8035
  approvalId: { type: string }
7094
8036
  delivery: { type: string, enum: [applied] }
@@ -7139,7 +8081,7 @@ components:
7139
8081
  costUsd: { type: number }
7140
8082
  estimated:
7141
8083
  type: boolean
7142
- description: 'true = some rows lacked the per-model echo and fell back to stats.tokens (counted into tokensOut) — the token view is partly estimated. Honest flag, contract-required.'
8084
+ description: 'true = partly-estimated window, two axes: token view (rows lacking the per-model echo fell back to stats.tokens, counted into tokensOut) AND cost view (server >=3.6.0: unpriced rows carry NO costMicroUsd and fold as 0, so costUsd is a lower bound — render as ">= $X", not "~"). Honest flag, contract-required.'
7143
8085
 
7144
8086
  UsageWindowBase:
7145
8087
  type: object
@@ -7256,13 +8198,16 @@ components:
7256
8198
  The 200 receipt of POST /v1/workflows/{runId}/agents/{label}/steer (server src/http/routes/workflows.ts —
7257
8199
  exact literal: `status:"running"`, `delivery:"applied"`, `marker` = the engine-minted delivery
7258
8200
  marker; NO `note`, unlike the run-subagent verb).
7259
- required: [runId, label]
7260
- 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
7261
8206
  properties:
7262
8207
  runId: { type: string }
7263
8208
  label: { type: string }
7264
- status: { type: string, description: 'Literal "running" on today''s server.' }
7265
- 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.' }
7266
8211
  marker: { type: string, description: 'Engine-minted delivery marker.' }
7267
8212
 
7268
8213
  WorkspaceSnapshotRow: