@sema-agent/sdk 8.3.0 → 8.5.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
@@ -5633,6 +5633,121 @@ components:
5633
5633
  properties:
5634
5634
  clampTolerance: { type: number }
5635
5635
 
5636
+ TerminalCause:
5637
+ description: >
5638
+ 🔴 BREAKING (server >= 7.64.0, core 7.6.0 design/390 D-8) — WHY the run ended, as ONE tagged cause
5639
+ and the ONLY terminal record a `TaskResult` carries. It REPLACES the eight parallel plane keys of
5640
+ 7.63.0 and earlier (`status` / `errorCode` / `errorMessage` / `blockedReason` / `checkpointToken` /
5641
+ `checkpointId` / `checkpointGate` / `workspaceRestoreMode`), all of which are GONE from this schema —
5642
+ no optional compatibility twin ships, because two spellings of one fact on one wire is two writers on
5643
+ one semantic surface.
5644
+ TRIAGE ON `terminal.kind`. The legacy five-word `status` (and the seven flat words) is a DERIVATION:
5645
+ call the engine's `terminalProjection(terminal)` — the ONE cause->plane derivation — never hand-roll it.
5646
+ Which pause reads as `suspended` and which as `needs_review` is decided by the engine's pause registry,
5647
+ not by this service.
5648
+ 🔴 The service's OWN columns and envelopes are UNCHANGED and still speak the five words: the `status` /
5649
+ `errorCode` columns on `GET /v1/runs/:id`, the task-list row `status`, the `/decide` 200 body's
5650
+ `status` / `errorCode` / `errorMessage`, and the fleet row's terminal word. Only their data source moved
5651
+ (projected once from the cause).
5652
+ oneOf:
5653
+ - $ref: '#/components/schemas/TerminalCause_completed'
5654
+ - $ref: '#/components/schemas/TerminalCause_failed'
5655
+ - $ref: '#/components/schemas/TerminalCause_blocked'
5656
+ - $ref: '#/components/schemas/PausedCause'
5657
+ discriminator:
5658
+ propertyName: kind
5659
+ mapping:
5660
+ completed: '#/components/schemas/TerminalCause_completed'
5661
+ failed: '#/components/schemas/TerminalCause_failed'
5662
+ blocked: '#/components/schemas/TerminalCause_blocked'
5663
+ paused: '#/components/schemas/PausedCause'
5664
+
5665
+ TerminalCause_completed:
5666
+ type: object
5667
+ additionalProperties: false
5668
+ description: 'The run finished (a person''s clean halt included — see `TaskResult.haltedByUser`).'
5669
+ required: [kind]
5670
+ properties:
5671
+ kind: { const: completed }
5672
+
5673
+ TerminalCause_failed:
5674
+ type: object
5675
+ additionalProperties: false
5676
+ description: 'A limit, a provider failure, an abort, an invalid output…'
5677
+ required: [kind]
5678
+ properties:
5679
+ kind: { const: failed }
5680
+ code:
5681
+ type: string
5682
+ description: >
5683
+ The machine code this failure carried — the seat the retired flat `errorCode` occupied, now
5684
+ STRUCTURALLY owned by the failure arm ("a code belongs to a failure" is the shape, not a
5685
+ cross-field convention). OPEN SET, dotted namespaces so a caller can prefix-match a whole class:
5686
+ `limits.max_tokens_exceeded` / `limits.max_cost_exceeded` / `limits.max_turns_exceeded` /
5687
+ `limits.max_walltime_exceeded` / `config.*` / `resume_at.*`; brain codes (`auth` / `network` /
5688
+ `rate_limit` / …) and `conflict` stay FLAT. Two EXTERNAL stop causes are deliberately NOT
5689
+ `limits.*` (retrying them needs a new environment / a freed window, not a smaller budget):
5690
+ `env.lifetime_expired` and `usage.window_exhausted`.
5691
+ Governance codes arrive here too, and WHICH leg remaps one to an HTTP status differs per code —
5692
+ do not generalize from one to the other: (a) `memory.admission_denied` /
5693
+ `memory.admission_required` are remapped to 403/503 ONLY on the synchronous `POST /v1/tasks`;
5694
+ on `POST /v1/runs` -> `GET /v1/runs/{taskId}` and on the SSE `done` frame they ride a 200 result.
5695
+ (b) `usage.window_exhausted` is a 429 only when the PRE-ADMISSION gate catches it; caught in the
5696
+ race after admission it is NOT remapped on any leg, including the synchronous 200.
5697
+ A NESTED orchestrator that met a durable pause it cannot drive stamps `unexpected.suspended` /
5698
+ `unexpected.needs_review` and carries that pause on `nestedPause`.
5699
+ Consumers MUST branch with a `default` arm. Absent = the failure never had a code.
5700
+ message: { type: string, description: 'Human-readable failure text. Absent when there was none.' }
5701
+ nestedPause:
5702
+ allOf: [{ $ref: '#/components/schemas/PausedCause' }]
5703
+ description: >
5704
+ The durable pause this failure STANDS IN FOR — set ONLY at a nested hard boundary (verify /
5705
+ cascade): the nested leg paused on a durable gate the orchestrator cannot drive, so at ITS
5706
+ boundary the pause is reported as a failure while the pause's own cause travels here verbatim.
5707
+ 🔴 On the wire it carries NO `token` — the same withholding rule as the top-level `paused` arm
5708
+ (see `PausedCause`); `gate` / `checkpointId` are present as usual.
5709
+ Absent on every failure the engine itself assembles.
5710
+
5711
+ TerminalCause_blocked:
5712
+ type: object
5713
+ additionalProperties: false
5714
+ description: 'The agent could not finish and said why (a governance verdict is NOT a failure).'
5715
+ required: [kind, reason]
5716
+ properties:
5717
+ kind: { const: blocked }
5718
+ reason: { type: string, description: 'Why the agent could not finish.' }
5719
+
5720
+ PausedCause:
5721
+ type: object
5722
+ additionalProperties: false
5723
+ description: >
5724
+ A durable pause committed a checkpoint and the run is resumable.
5725
+ 🔴 THE `token` SEAT IS NEVER ON THE WIRE. It is the resume CAPABILITY (token-as-auth) and never leaves
5726
+ the service — the SAME discipline the retired top-level `checkpointToken` had, only the credential
5727
+ changed address (`src/terminal.ts` `stripResumeToken` removes it on both reachable paths: this arm and
5728
+ `TerminalCause_failed.nestedPause`). To act on this pause call
5729
+ `POST /v1/approvals/{sessionId}/decide` and reconcile by `checkpointId`.
5730
+ required: [kind, gate]
5731
+ properties:
5732
+ kind: { const: paused }
5733
+ gate:
5734
+ allOf: [{ $ref: '#/components/schemas/CheckpointGate' }]
5735
+ description: 'WHICH pause — who/what must resume it. Same word as the park row''s `gate.kind`.'
5736
+ checkpointId:
5737
+ type: string
5738
+ description: >
5739
+ The pause's NON-SECRET stable identity — the display/correlation key to log or render where the
5740
+ token must not travel. Absent on pre-identity checkpoints.
5741
+ restoreMode:
5742
+ type: string
5743
+ enum: [snapshot, park_only]
5744
+ description: >
5745
+ HOW the paused task's remote workspace comes back, present whenever the pause captured one.
5746
+ `snapshot` = the VM was suspended into a snapshot (provider billing typically stops).
5747
+ `park_only` = the env declared itself non-suspendable (an SSH host / an ADB device) so NOTHING was
5748
+ paused: the machine keeps running and keeps costing. ABSENT for a process-local pause — a
5749
+ scheduler that assumes "paused => idle and free" must be able to see the difference.
5750
+
5636
5751
  TaskResult:
5637
5752
  type: object
5638
5753
  # 🔴 **刻意不封闭**(census 轴一裁定,2026-07-30)。此前是「没写」= JSON Schema 默认开放,分不清
@@ -5642,12 +5757,21 @@ components:
5642
5757
  # 这里改成 false 会让 spec 与本仓自己的类型面互相打脸,并且把「引擎加字段」判成 wire 违约(它不是)。
5643
5758
  # 收紧这一面的正解不是 additionalProperties,是把引擎真发的键**逐个登记**(字段级 drift 门的活)。
5644
5759
  additionalProperties: true
5645
- description: Synchronous task result (`POST /v1/tasks`).
5646
- required: [taskId, sessionId, status, stats]
5760
+ description: >
5761
+ Synchronous task result (`POST /v1/tasks`). 🔴 BREAKING at server 7.64.0: the terminal is ONE tagged
5762
+ cause (`terminal`), not the eight parallel plane keys — see `TerminalCause`.
5763
+ required: [taskId, sessionId, terminal, stats]
5647
5764
  properties:
5648
5765
  taskId: { type: string }
5649
5766
  sessionId: { type: string }
5650
- status: { $ref: '#/components/schemas/RunStatus' }
5767
+ terminal:
5768
+ allOf: [{ $ref: '#/components/schemas/TerminalCause' }]
5769
+ description: >
5770
+ 🔴 server >= 7.64.0 — the ONE terminal record. Branch on `terminal.kind`. The eight retired plane
5771
+ keys (`status` / `errorCode` / `errorMessage` / `blockedReason` / `checkpointToken` /
5772
+ `checkpointId` / `checkpointGate` / `workspaceRestoreMode`) are GONE from this schema and this
5773
+ SDK ships NO compatibility projection: a consumer that still needs the five-word `status` calls
5774
+ the engine's `terminalProjection(terminal)` itself.
5651
5775
  result: { type: string }
5652
5776
  activeTaskId:
5653
5777
  type: [string, 'null']
@@ -5678,20 +5802,13 @@ components:
5678
5802
  salvagedOutput:
5679
5803
  type: string
5680
5804
  description: Partial output preserved when the run could not complete normally (best-effort salvage).
5681
- blockedReason:
5682
- type: string
5683
- description: Why a `blocked` status was reached (human-readable).
5684
- checkpointGate:
5685
- description: >
5686
- The gate kind that produced `checkpointToken` when the run suspended. Deliberately UNTYPED here —
5687
- the shape is core's and still evolving; treat it as opaque and branch on `status`/`errorCode` instead.
5688
5805
  toolCallId:
5689
5806
  type: string
5690
5807
  description: >
5691
5808
  [4913] (server 7.41+) — the PENDING tool call's id when this result is a durable park
5692
- (`status` suspended/needs_review with a `tool_approval` pending action); same key and meaning as the
5809
+ (`terminal.kind === "paused"` with a `tool_approval` pending action); same key and meaning as the
5693
5810
  `tool_approval` frame's `toolCallId` (server >= 1.307). ABSENT (key omitted, never null) on a
5694
- tool-less park (resource_limit / plan_review / task_done), on every terminal result, and when the
5811
+ tool-less park (resource_limit / plan_review / task_done), on every non-paused cause, and when the
5695
5812
  server-side checkpoint read failed — consumers test presence.
5696
5813
  degraded:
5697
5814
  type: object
@@ -5716,21 +5833,6 @@ components:
5716
5833
  description: >
5717
5834
  The task's structured (schema-constrained) output when one was requested. Shape is caller-defined,
5718
5835
  so this is intentionally untyped.
5719
- errorCode:
5720
- type: string
5721
- description: >
5722
- Result-level terminal code (NOT an HTTP error). Includes `budget.precall` / `budget.exceeded`
5723
- (cost gate, non-retryable) — branch on this, not on a 4xx (pinned wire contract).
5724
- Governance codes also arrive HERE, and WHICH leg renders them as an HTTP status differs per code —
5725
- do not generalize from one to the other:
5726
- (a) `memory.admission_denied` / `memory.admission_required` (server >=7.0.0) are remapped to 403/503
5727
- ONLY on the synchronous `POST /v1/tasks`; on `POST /v1/runs` -> `GET /v1/runs/{taskId}` and on the
5728
- SSE `done` frame they ride a 200 result (those legs have no second chance at a status line).
5729
- (b) `usage.window_exhausted` is a 429 only when the PRE-ADMISSION gate catches it (all three submit
5730
- endpoints). When the window is exhausted in the race after admission, the engine's refusal is NOT
5731
- remapped on any leg — it lands in the result, including a 200 from the synchronous `POST /v1/tasks`.
5732
- Branch on this field, never on the status alone.
5733
- errorMessage: { type: string }
5734
5836
  retryAfterMs:
5735
5837
  type: integer
5736
5838
  description: >
@@ -5751,6 +5853,51 @@ components:
5751
5853
  rounds: { type: integer }
5752
5854
  findings: { type: array, items: { type: string } }
5753
5855
 
5856
+ LegacyPlaneTaskResult:
5857
+ type: object
5858
+ additionalProperties: true
5859
+ description: >
5860
+ 🔴 THE SHAPE THAT IS ON DISK — a task result written by server 7.63.0 or earlier, in the retired FLAT
5861
+ plane form. It appears ONLY on the faces that REPLAY PERSISTED BYTES, and only on a deployment that was
5862
+ upgraded across 7.64.0: `GET /v1/runs/:id` -> `result` (the service passes the stored blob through
5863
+ verbatim, redaction only — `src/http/routes/runs.ts`) and the durable-ledger replay of the `done` frame.
5864
+ NOTHING WRITES IT ANY MORE: the write path mints exactly one shape (`TaskResult`, with `terminal`).
5865
+ 🔴 THIS IS NOT A COMPATIBILITY TWIN. The rule against "two spellings of one fact on one wire" is about
5866
+ WRITERS: two producers of one semantic surface. Here there is ONE writer and a READ face that admits the
5867
+ bytes a now-retired writer actually left on disk — the same distinction the service draws for its own
5868
+ two-generation reader (`persistedPlaneOf`, whose header records the two reproducible regressions that
5869
+ deleting it caused: a 500 on every historical `GET /v1/runs/:id`, and unredacted credentials replayed
5870
+ from old ledger rows).
5871
+ NARROWING: `"terminal" in result` — present = the current cause form, absent = this shape. Do NOT narrow
5872
+ on `status` alone: the SSE conflict envelope (`ActiveRunConflictDoneResult`) also carries `status`.
5873
+ 🔴 `terminal` IS REFUSED HERE, and that clause is load-bearing rather than decorative: the two
5874
+ generations are MUTUALLY EXCLUSIVE on disk (7.63.0 wrote `status` and never `terminal`; 7.64.0 writes
5875
+ `terminal` and never `status`), and without the refusal a MALFORMED current result — `terminal: null`,
5876
+ an unknown cause kind, a `paused` cause that still carries its `token` — would fail the `TaskResult`
5877
+ arm and then be LAUNDERED through this one, so the read faces would silently lose every negative
5878
+ control `TaskResult` enforces while a consumer narrowing on `"terminal" in result` walks straight into
5879
+ it. The documented discriminant and the enforced one are the same clause.
5880
+ not: { required: [terminal] }
5881
+ required: [taskId, sessionId, status, stats]
5882
+ properties:
5883
+ taskId: { type: string }
5884
+ sessionId: { type: string }
5885
+ status: { $ref: '#/components/schemas/RunStatus' }
5886
+ result: { type: string }
5887
+ stats: { $ref: '#/components/schemas/TaskStats' }
5888
+ errorCode: { type: string, description: 'The retired flat terminal code. Its successor is `TerminalCause.failed.code`.' }
5889
+ errorMessage: { type: string, description: 'The retired flat failure text. Successor: `TerminalCause.failed.message`.' }
5890
+ blockedReason: { type: string, description: 'The retired flat governance reason. Successor: `TerminalCause.blocked.reason`.' }
5891
+ checkpointGate:
5892
+ description: >
5893
+ The retired flat pause gate — UNTYPED here on purpose, because these are bytes an older generation
5894
+ wrote and this contract does not re-litigate their shape. Successor: `TerminalCause.paused.gate`
5895
+ (typed as `CheckpointGate`).
5896
+ salvagedOutput: { type: string }
5897
+ model: { type: string }
5898
+ toolCallId: { type: string }
5899
+ retryAfterMs: { type: integer }
5900
+
5754
5901
  RunReceipt:
5755
5902
  type: object
5756
5903
  # 封闭(census 轴一,2026-07-30):producer 铸造点 routes/runs.ts 与 idem 重放臂各自逐字写死
@@ -5769,7 +5916,12 @@ components:
5769
5916
  description: >
5770
5917
  Run state (`GET /v1/runs/:id`) — live shape per the pinned wire contract Drift 2: `result` is the NESTED TaskResult
5771
5918
  object (output text in result.result, stats in result.stats; NO top-level stats), and the error text
5772
- field is `error` (not errorMessage; errorCode is floated up from result.errorCode).
5919
+ field is `error` (not errorMessage).
5920
+ 🔴 server >= 7.64.0 — this ROW's own `status` / `errorCode` columns are UNCHANGED and still speak the
5921
+ five words; only their SOURCE moved. The nested `result` is the engine result itself and therefore
5922
+ changed shape with it (`result.terminal`, no plane keys): the row's `errorCode` column is now projected
5923
+ from `result.terminal` (`src/http/routes/runs.ts` -> `persistedPlaneOf`, which also reads the flat bytes
5924
+ 7.63.0 and earlier wrote to disk), NOT copied from a `result.errorCode` that no longer exists.
5773
5925
  # 封闭(census 轴一,2026-07-30;A-075.21 回填后 12 键):铸造点 routes/runs.ts 是一个逐键写死的
5774
5926
  # 字面量(taskId/sessionId/status/msSinceLastActivity/crossSliceUsage/result/supervisorCost/
5775
5927
  # suggestions/errorCode/error/jobId/source),与本 properties 一一对上;只有 taskId/sessionId/status
@@ -5806,10 +5958,21 @@ components:
5806
5958
  spentMicroUsd: { type: integer, description: 'Integer micro-USD; absent on unpriced deployments (unknown, not 0).' }
5807
5959
  maxSlices: { type: integer }
5808
5960
  sliceCount: { type: integer }
5809
- result: { $ref: '#/components/schemas/TaskResult' }
5961
+ result:
5962
+ oneOf:
5963
+ - $ref: '#/components/schemas/TaskResult'
5964
+ - $ref: '#/components/schemas/LegacyPlaneTaskResult'
5965
+ description: >
5966
+ 🔴 A READ-OF-DISK face, so it carries TWO generations of bytes (see `LegacyPlaneTaskResult`): the
5967
+ current cause form on anything server >= 7.64.0 wrote, and the retired flat form on rows written
5968
+ before the upgrade. The service passes the stored blob through verbatim (redaction only) and does
5969
+ NOT back-fill `terminal`. Narrow on `"terminal" in result`.
5810
5970
  errorCode:
5811
5971
  type: string
5812
- description: Result-level terminal code (e.g. `budget.exceeded`, `cancelled`); NOT an HTTP error.
5972
+ description: >
5973
+ Result-level terminal code (e.g. `budget.exceeded`, `cancelled`); NOT an HTTP error. This ROW COLUMN
5974
+ is unchanged at 7.64.0 — only its source moved (projected from `result.terminal`, or read straight
5975
+ off a legacy flat blob).
5813
5976
  error: { type: string }
5814
5977
  jobId:
5815
5978
  type: string
@@ -6080,20 +6243,29 @@ components:
6080
6243
  input: { description: tool-call — redacted args. }
6081
6244
  callId: { type: string }
6082
6245
  isError: { type: boolean }
6083
- # 🔴 core >=5.18.1 (#187) —— 与 `Event_tool_end.settledBy` **同一个值**:server 的共享挑键器
6084
- # `toolResultFieldsOf`(src/trace/project.ts)同时喂 turns 面与 trace SSE 面,而写侧
6085
- # `toolEndEventData` 喂 durable 账本 —— 三面一个真源,所以这里的词表与那边逐字相同。
6086
- settledBy:
6246
+ # 🔴 BREAKING (server >=7.64.0, core 7.6.0 S6-A) —— 与 `Event_tool_end.gate` **同一个值**:server 的
6247
+ # 共享挑键器 `toolResultFieldsOf`(src/trace/project.ts)同时喂 turns 面与 trace SSE 面,而写侧
6248
+ # `toolEndEventData` 喂 durable 账本 —— 三面一个真源。退役的 `settledBy` 与它的三兄弟
6249
+ # (`resolution`/`autoDenied`/`approver`)在这三面上一并删除。
6250
+ gate:
6251
+ allOf: [{ $ref: '#/components/schemas/GateOutcome' }]
6252
+ description: >
6253
+ tool-result — the WHOLE record of this call''s gate pass (see `GateOutcome`). The READ leg
6254
+ screens it INDEPENDENTLY of the write leg (a ledger row can come from any engine generation or
6255
+ a third-party producer), so a defective record is withheld here too. Absence is not a fact
6256
+ about the call.
6257
+ delivered:
6258
+ allOf: [{ $ref: '#/components/schemas/McpDelivered' }]
6259
+ description: 'tool-result — MCP delivery verdict; read with `errorCode`. Absent on every non-MCP-failure result.'
6260
+ errorCode:
6087
6261
  type: string
6088
- enum: [human, timeout, aborted]
6089
6262
  description: >
6090
- tool-result — HOW this call was settled when it went through an approval: `human` = somebody
6091
- actually answered; `timeout` = the approval window elapsed; `aborted` = abort / unclonable
6092
- arguments / out-of-contract. Present ONLY on the settled call.
6093
- ABSENCE CARRIES NO SEMANTICS (an older worker, or a posture arm that deliberately leaves it
6094
- unset — the two are indistinguishable), so absent is neither "human" nor "no approval
6095
- happened". The read leg re-validates the closed set independently of the write leg, because a
6096
- ledger row can come from any engine generation.
6263
+ tool-result — the machine code for WHY this call failed, verbatim from any tool''s
6264
+ `ToolResult.details.code` (OPEN set; the MCP family within it is core''s closed
6265
+ `McpFailureKind` word list since 7.6.0). Same key and semantics as `Event_tool_end.errorCode`.
6266
+ gatedCallId:
6267
+ type: string
6268
+ description: 'tool-result — WHICH tool call a durable park is holding (core >= 5.55.0); absent on a tool-less park.'
6097
6269
  tokens:
6098
6270
  type: object
6099
6271
  properties:
@@ -6831,6 +7003,9 @@ components:
6831
7003
  `roots` (copying an upstream default onto the wire becomes a lie the day core changes it).
6832
7004
  Minimal disclosure — the deny table itself is never on the capability face. There is NO
6833
7005
  per-request `readFace` field on TaskRequest.
7006
+ server >= 7.65.0 (S-167): on a SINGLE-USER TURNKEY worker (REQUIRE_PRINCIPAL unset) with no
7007
+ READ_FACE set this bit is `"open"` (posture-derived), where 7.64.0 answered `null`; multi-tenant
7008
+ workers still answer `null`. Provenance lives on the operator face `diagnostics.wiring.readFace`.
6834
7009
  callerCwd:
6835
7010
  type: boolean
6836
7011
  description: >
@@ -6894,6 +7069,105 @@ components:
6894
7069
  denial:
6895
7070
  type: ["string", "null"]
6896
7071
  enum: [entitlement_resolver_absent, null]
7072
+ writeProtection:
7073
+ oneOf:
7074
+ - $ref: '#/components/schemas/WriteProtectionCapability'
7075
+ - type: 'null'
7076
+ description: >
7077
+ server >= 7.63.0 (S-138) — the WRITE-PROTECTION TABLE's posture on this deployment (see
7078
+ `WriteProtectionCapability`). `null` = this process cannot say; an ABSENT key means an OLDER worker,
7079
+ and "no key" is NOT "no table" — an absent seat on the engine side is precisely the DEFAULT TABLE
7080
+ BEING IN PLACE.
7081
+ WriteProtectionCapability:
7082
+ type: object
7083
+ additionalProperties: false
7084
+ description: >
7085
+ server >= 7.63.0 (S-138) — the WRITE-PROTECTION TABLE's posture on this deployment. The engine's
7086
+ literal name table downgrades a locatable write (Write / Edit / NotebookEdit) that lands on a table
7087
+ row from `allow` to `ask`, so a shell can tell in advance that those paths will raise a card.
7088
+ 🔴 THE THREE ARE DELIBERATELY NOT FOLDED INTO ONE BOOLEAN: folded, "the engine's default table is
7089
+ in place" and "an operator swapped in a table of their own" both read `true`, and those two say
7090
+ different things to an operator.
7091
+ 🔴 MINIMUM DISCLOSURE: the per-row names/kinds are NOT here — a deployment-authored row can carry
7092
+ an internal path name, and listing them tells anyone who wants around them which names are NOT
7093
+ protected. Rows live on the operator face `GET /v1/diagnostics/wiring` -> `writeProtection.rows`.
7094
+ 🔴 A PARALLEL MECHANISM, NOT THE SAME ONE: `SENSITIVE_WRITE_PATTERNS` (pattern-shaped write deny)
7095
+ is reported separately and must never be folded into this bit — the two have different fixes.
7096
+ The three-state reading of the SEAT itself lives at the reference point (`Capabilities.writeProtection`):
7097
+ `null` = this process cannot say; key absent = an older worker (and "no key" is NOT "no table").
7098
+ required: [armed, rows, replaced]
7099
+ properties:
7100
+ armed: { type: boolean, description: 'The effective table is non-empty => the engine really will raise a card on a row hit.' }
7101
+ rows: { type: integer, description: 'How many rows the effective table has (contents stay on the operator face).' }
7102
+ replaced: { type: boolean, description: 'The whole-table replacement knob was written => the default table is no longer in place as-is (an explicit EMPTY table counts — "the operator turned it off" is not "nothing was touched").' }
7103
+
7104
+
7105
+ WriteProtectedRow:
7106
+ type: object
7107
+ additionalProperties: false
7108
+ description: >
7109
+ One row of the write-protection table (core `WriteProtectedRow`): a LITERAL name plus how it matches.
7110
+ The `name` is also the row's stable identity — the string a refusal / ask message cites.
7111
+ required: [name, kind]
7112
+ properties:
7113
+ name: { type: string }
7114
+ kind:
7115
+ type: string
7116
+ enum: [basename, segment, segment-run]
7117
+ description: >
7118
+ `basename` = the target's LAST segment equals the row name · `segment` = ANY path segment equals it ·
7119
+ `segment-run` = a CONSECUTIVE run of segments equals the row's `/`-separated segments.
7120
+
7121
+ WriteProtectionPosture:
7122
+ type: object
7123
+ additionalProperties: false
7124
+ description: >
7125
+ server >= 7.63.0 (S-138) — the OPERATOR face of the write-protection table (`GET /v1/diagnostics/wiring`
7126
+ -> `writeProtection`). Same boot artefact as the tenant-facing `capabilities.writeProtection`, so the two
7127
+ faces cannot tell different stories; this one adds the ROW CONTENTS, which the tenant face withholds.
7128
+ required: [rows, source]
7129
+ properties:
7130
+ rows:
7131
+ type: array
7132
+ items: { $ref: '#/components/schemas/WriteProtectedRow' }
7133
+ description: 'The effective table, verbatim from the engine''s own compile (the service recomputes nothing). Empty = an explicitly empty table.'
7134
+ source:
7135
+ type: string
7136
+ enum: [default, extra, replace, off]
7137
+ description: >
7138
+ `default` = neither knob was written (the engine's default table is in place; the service does NOT
7139
+ copy that table — the engine owns it) · `extra` = default plus additions · `replace` = whole-table
7140
+ replacement · `off` = replaced with an EMPTY table (legal, and loud).
7141
+ droppedDefaultRows:
7142
+ type: array
7143
+ items: { type: string }
7144
+ description: >
7145
+ The DEFAULT row names a whole-table replacement dropped. Present only on the replacement family
7146
+ (`extra` / `default` cannot structurally drop a row). Row identity is judged by the ENGINE's own
7147
+ fold rule, so a case-variant spelling is not reported as a loss.
7148
+
7149
+ ReadFacePosture:
7150
+ type: object
7151
+ additionalProperties: false
7152
+ description: >
7153
+ server >= 7.65.0 (S-167) — the READ containment rung of this deployment AND WHO SET IT (operator face
7154
+ `GET /v1/diagnostics/wiring` -> `readFace`). `face` is the same boot artefact as the tenant-facing
7155
+ `capabilities.readFace` (`null` = nothing pinned by this deployment, the engine default is in force).
7156
+ `source` is a CLOSED four-word set on the server (exhaustive switch — adding a word is a compile error
7157
+ there): `env` = this machine's READ_FACE env var (wins over everything) · `center` = the config-center
7158
+ readFace domain (restart-to-apply) · `posture` = derived from the single-user turnkey posture
7159
+ (REQUIRE_PRINCIPAL unset ⇒ `open`; server >= 7.65.0 behaviour change) · `engine-default` = not pinned.
7160
+ `note` is a human-facing pointer for operators. Minimal disclosure: the deny table is never on this face.
7161
+ required: [face, source, note]
7162
+ properties:
7163
+ face:
7164
+ type: ["string", "null"]
7165
+ enum: [open, roots, null]
7166
+ source:
7167
+ type: string
7168
+ enum: [env, center, posture, engine-default]
7169
+ note:
7170
+ type: string
6897
7171
 
6898
7172
  SkillSpec:
6899
7173
  type: object
@@ -9267,6 +9541,214 @@ components:
9267
9541
  parentToolCallId: { type: string }
9268
9542
  sourceTaskId: { type: string }
9269
9543
  bgAgentId: { type: string }
9544
+ DeniedBy:
9545
+ type: string
9546
+ description: >
9547
+ WHO REFUSED a call — the LAYER whose verdict is the deny (core 7.6.0 `DENIED_BY_VALUES`, 8 words,
9548
+ CLOSED). The other question a deny raises — who ASKED — is answered by `AskOrigin`; the two used to
9549
+ share one seven-word list in which `classifier` meant both.
9550
+ `policy` = the deployment `ToolPolicy` denied (directly, or re-checking an approved edit), or the
9551
+ approval-edit chain hit its round cap · `hook` = a PreToolUse hook denied, threw, or never answered ·
9552
+ `org` = an organization policy rule denied · `classifier` = the auto-mode classifier denied directly ·
9553
+ `plan_mode` = plan mode's read-only block on a write tool · `compliance` = the compliance call-time
9554
+ lock · `write_protection` = an approved edit was rewritten onto a write-protected path no approval
9555
+ covers · `ask_resolution` = the ask's own settlement IS the refusal (detail on `GateOutcome.settlement`).
9556
+ 🔴 CLOSED ON THIS WIRE, and deliberately not marked open: the engine's invariant screen
9557
+ (`screenGateOutcome`) treats an out-of-set `deniedBy` as a DEFECT, and the service withholds a defective
9558
+ record WHOLE (report + withhold). A word outside this set therefore cannot reach a consumer — it arrives
9559
+ as the `gate` key being ABSENT, never as an unfamiliar word. Same rule for `Settlement.kind` and for
9560
+ `GateOutcome.origin`.
9561
+ enum: [policy, hook, org, classifier, plan_mode, compliance, write_protection, ask_resolution]
9562
+
9563
+ Settlement:
9564
+ description: >
9565
+ HOW the wait an ask was in ENDED — a DISCRIMINATED union on `kind` (core 7.6.0 `SETTLEMENT_KINDS`,
9566
+ 12 words, CLOSED). It is deliberately NOT a free product of `kind` x `who`: a free product can express
9567
+ "human_allowed, ended by the engine's window" — a structurally complete record that contradicts itself.
9568
+ `who` says WHICH PARTY ended it and its shape is fixed BY the word; `when` is epoch ms written at the
9569
+ settlement site; a person's refusal carries the decider's own `note` when one was attached.
9570
+ 🔴 Single mint: every kind is composed by the ENGINE at the site whose wait ended. A host never mints a
9571
+ word — it reports FACTS (a synchronous approver's `human`/`timeout`, a durable decide's
9572
+ `decidedBy: "person" | "sla_timeout"`) and core turns them into the word.
9573
+ Server-side projection note: `who.approver` and `note` are host-derived / decider-authored text and are
9574
+ secret-redacted (and `note` length-bounded) before they reach this wire.
9575
+ oneOf:
9576
+ - $ref: '#/components/schemas/Settlement_human'
9577
+ - $ref: '#/components/schemas/Settlement_approval_window_expired'
9578
+ - $ref: '#/components/schemas/Settlement_denial_limit_window_expired'
9579
+ - $ref: '#/components/schemas/Settlement_park_sla_expired'
9580
+ - $ref: '#/components/schemas/Settlement_fail_closed'
9581
+
9582
+ Settlement_human:
9583
+ type: object
9584
+ additionalProperties: false
9585
+ description: 'A person (or the approver acting for one) decided. `human_refused` may carry the decider''s own note.'
9586
+ required: [kind, who, when]
9587
+ properties:
9588
+ kind: { type: string, enum: [human_allowed, human_refused] }
9589
+ who:
9590
+ type: object
9591
+ additionalProperties: false
9592
+ required: [party]
9593
+ properties:
9594
+ party: { const: person }
9595
+ approver: { type: string, description: 'The attribution the approval channel reported. Absent = the channel reported no name — NEVER read as "nobody approved".' }
9596
+ when: { type: integer, format: int64, description: 'Epoch ms, written at the settlement site.' }
9597
+ note: { type: string, description: 'The refusing decider''s own words, when one was attached. A refusal WITHOUT a note is a bare "no".' }
9598
+
9599
+ Settlement_approval_window_expired:
9600
+ type: object
9601
+ additionalProperties: false
9602
+ description: 'An approval window elapsed with no answer — the engine''s own approval factory window, or a synchronous host approver reporting its own window elapsed.'
9603
+ required: [kind, who, when]
9604
+ properties:
9605
+ kind: { const: approval_window_expired }
9606
+ who:
9607
+ oneOf:
9608
+ - type: object
9609
+ additionalProperties: false
9610
+ required: [party, window]
9611
+ properties:
9612
+ party: { const: engine }
9613
+ window: { const: approval_factory }
9614
+ - type: object
9615
+ additionalProperties: false
9616
+ required: [party]
9617
+ properties:
9618
+ party: { const: host }
9619
+ when: { type: integer, format: int64 }
9620
+
9621
+ Settlement_denial_limit_window_expired:
9622
+ type: object
9623
+ additionalProperties: false
9624
+ description: 'The classifier denial-limit fallback''s auto-deny window (core''s own timer over the synchronous leg) elapsed with no answer.'
9625
+ required: [kind, who, when]
9626
+ properties:
9627
+ kind: { const: denial_limit_window_expired }
9628
+ who:
9629
+ type: object
9630
+ additionalProperties: false
9631
+ required: [party, window]
9632
+ properties:
9633
+ party: { const: engine }
9634
+ window: { const: denial_limit }
9635
+ when: { type: integer, format: int64 }
9636
+
9637
+ Settlement_park_sla_expired:
9638
+ type: object
9639
+ additionalProperties: false
9640
+ description: >
9641
+ A DURABLE park''s SLA deadline passed and the host''s sweep resolved it as a deny. A store
9642
+ `expire`/`reap` produces no `tool_end` and therefore no settlement.
9643
+ required: [kind, who, when]
9644
+ properties:
9645
+ kind: { const: park_sla_expired }
9646
+ who:
9647
+ type: object
9648
+ additionalProperties: false
9649
+ required: [party]
9650
+ properties:
9651
+ party: { const: host }
9652
+ approver: { type: string }
9653
+ when: { type: integer, format: int64 }
9654
+
9655
+ Settlement_fail_closed:
9656
+ type: object
9657
+ additionalProperties: false
9658
+ description: >
9659
+ The fail-closed family — nobody ended the wait, the situation did. `no_approver` (headless: no approver
9660
+ or question face is wired, or the deny posture string) · `approver_unavailable` (the approver answered
9661
+ the ROUTING question "nobody reachable" and no park took the ask) · `approver_error` (it threw) ·
9662
+ `approver_contract` (it answered outside its contract) · `presentation_failed` (args/edit could not be
9663
+ safely presented or adopted) · `blanket_allow_refused` (a blanket `onAsk:"allow"` met a
9664
+ `requiresRealApproval` ask) · `task_aborted` (the wait''s abort signal ended it).
9665
+ required: [kind, who, when]
9666
+ properties:
9667
+ kind:
9668
+ type: string
9669
+ enum: [no_approver, approver_unavailable, approver_error, approver_contract, presentation_failed, blanket_allow_refused, task_aborted]
9670
+ who:
9671
+ type: object
9672
+ additionalProperties: false
9673
+ required: [party]
9674
+ properties:
9675
+ party: { const: none }
9676
+ when: { type: integer, format: int64 }
9677
+
9678
+ GateDisposition:
9679
+ description: 'The final disposition of one tool-gate pass. A deny NAMES the layer that refused.'
9680
+ oneOf:
9681
+ - type: object
9682
+ additionalProperties: false
9683
+ required: [kind]
9684
+ properties:
9685
+ kind: { const: allowed }
9686
+ - type: object
9687
+ additionalProperties: false
9688
+ required: [kind, deniedBy]
9689
+ properties:
9690
+ kind: { const: denied }
9691
+ deniedBy: { $ref: '#/components/schemas/DeniedBy' }
9692
+
9693
+ GateOutcome:
9694
+ type: object
9695
+ additionalProperties: false
9696
+ description: >
9697
+ 🔴 BREAKING (server >= 7.64.0, core 7.6.0 design/390 S6-A) — the WHOLE record of ONE tool-gate pass.
9698
+ It REPLACES the four orthogonal words of 7.63.0 and earlier (`settledBy` / `resolution` / `autoDenied` /
9699
+ `approver`, all four DELETED with no alias): their pairing rules lived only in comments, and consumers
9700
+ had to infer semantics from ABSENCE ("a `timeout` with no `resolution` means a durable park expired").
9701
+ Minted ONCE by the engine (the gate''s exit; the decide lane for a durable park) and projected without
9702
+ re-derivation onto three faces — the `permissionDenied` observer payload, this `tool_end` frame, and the
9703
+ durable row''s resolved outcome — so the three cannot tell different stories.
9704
+ The three members are orthogonal but bound by four engine invariants: (I1) `settlement` present <=>
9705
+ `origin` present — they describe the SAME ask; (I2) `deniedBy === "ask_resolution"` => `settlement`
9706
+ present AND a refusal kind; (I3) a `human_allowed` settlement beside a `denied` disposition means a
9707
+ LATER re-check vetoed a person''s approval, so `deniedBy` is one of the veto layers (`policy` / `hook` /
9708
+ `org` / `write_protection`) and the approver stays on the settlement; (I4) an `allowed` disposition has
9709
+ `settlement` absent or `human_allowed`.
9710
+ 🔴 NOTHING here is read out of an ABSENCE: an ordinary allow with no ask is
9711
+ `{disposition:{kind:"allowed"}}`, a direct policy deny is `{disposition:{kind:"denied",deniedBy:"policy"}}`,
9712
+ and neither carries a settlement because neither settled one.
9713
+ 🔴 The SERVICE withholds a record that fails the engine''s invariant screen (report + withhold, counter
9714
+ `server.trace.gate-outcome-defective`) — so "a gate should have been here but the key is absent" has a
9715
+ fourth cause besides the three below: that record was defective. The four are indistinguishable on the
9716
+ wire; a consumer falls back to `isError` + the message text.
9717
+ required: [disposition]
9718
+ properties:
9719
+ disposition: { $ref: '#/components/schemas/GateDisposition' }
9720
+ settlement:
9721
+ allOf: [{ $ref: '#/components/schemas/Settlement' }]
9722
+ description: 'Present only when THIS pass settled an ask. Same-present/same-absent as `origin` (I1).'
9723
+ origin:
9724
+ allOf:
9725
+ - { $ref: '#/components/schemas/AskOrigin' }
9726
+ # 🔴 闭集执法**只在这一面**:引擎的 `screenGateOutcome` 对 `origin` 判成员,出集即记录缺陷,而
9727
+ # 服务对缺陷记录是整条不上帧 ⇒ 词表外的值在这条 wire 上到不了消费端。审批帧那一面不判成员,
9728
+ # 所以 `AskOrigin` 本体保持真开(见该 schema 的长注)。这张 enum 是本 spec 里该词表的**唯一**
9729
+ # 执法点;core 加词时改这一处。
9730
+ - enum: [content_question, unresolvable, org_unavailable, org_rule, hook, ask_rule, denial_limit_fallback, shell_gate_tighten, safety_tighten, policy]
9731
+ description: >
9732
+ WHO ASKED. Same-present/same-absent as `settlement` (I1). CLOSED here (and only here) — see the
9733
+ enum note above.
9734
+
9735
+ McpDelivered:
9736
+ type: string
9737
+ description: >
9738
+ Whether a failed MCP request REACHED the server (core 7.6.0 `MCP_DELIVERY_VERDICTS`, CLOSED).
9739
+ `yes` = the server answered (a `protocol` rejection proves the exchange happened) · `no` = provably
9740
+ never sent (a connect-phase errno, a spawn failure, a malformed declaration, a server already known
9741
+ dead) — safe to retry, nothing executed · `unknown` = the client stopped waiting or lost the pipe
9742
+ mid-exchange, so the request MAY have executed and a write-capable tool''s side effects must be
9743
+ verified before a retry.
9744
+ 🔴 READ IT WITH `errorCode`, never alone: the SAME `connection_closed` is directly retryable at `no`
9745
+ and must be investigated at `unknown` — which is exactly why the verdict is its own field and not
9746
+ folded into the class.
9747
+ ⚠️ `no` says the CALLER''S TOOL CALL was not sent; it does NOT promise the server received zero bytes
9748
+ (an elicitation during the dial may already have crossed the connection) — core''s contract, passed
9749
+ through verbatim.
9750
+ enum: [yes, no, unknown]
9751
+
9270
9752
  Event_tool_end:
9271
9753
  type: object
9272
9754
  description: >
@@ -9274,7 +9756,19 @@ components:
9274
9756
  (TextContent|ImageContent)[] block array) and degrades to a single TRUNCATED string over the core size cap
9275
9757
  (`truncated: true`). Absent ⇒ the tool produced no body. 🔴 UNTRUSTED RAW + observability-only: the service
9276
9758
  has redactDeep'd + size-bounded it; a consumer MAY further bound, MUST never re-feed it to a model.
9277
- allOf: [{ $ref: '#/components/schemas/EventIdentity' }]
9759
+ allOf:
9760
+ - { $ref: '#/components/schemas/EventIdentity' }
9761
+ # 🔴 现役写口与退役四词**互斥**(S-170 codex r2 [medium] 验真后补):退役四词只出现在 <=7.63.0 写下的
9762
+ # durable 行上,而那种行结构上**没有** `gate`(7.63.0 压根不铸它)。所以「`gate` 与四词同帧」在
9763
+ # 任何一代的真字节里都不存在 —— 它只可能是现役写口回吐退役键,或一条自相矛盾的合成记录。
9764
+ # 不拒它,「四词只在回放里」这句话就没有任何机器在守(标记键 `x-legacy-replay-only` 是给人和
9765
+ # codegen 读的,不执法)。
9766
+ - not:
9767
+ anyOf:
9768
+ - { required: [gate, settledBy] }
9769
+ - { required: [gate, resolution] }
9770
+ - { required: [gate, autoDenied] }
9771
+ - { required: [gate, approver] }
9278
9772
  additionalProperties: false # 封闭 + identity 本地镜像:见 Event_reasoning 处的长注
9279
9773
  required: [type, toolCallId, toolName, isError]
9280
9774
  properties:
@@ -9289,19 +9783,60 @@ components:
9289
9783
  output: {} # NON-UNIFORM: string | (TextContent|ImageContent)[] | (truncated) string. Absent ⇒ no body.
9290
9784
  truncated: { type: boolean }
9291
9785
  totalChars: { type: integer, description: 'core >=1.442 (RB-210): honest ORIGINAL size when truncated — sum of each block''s true size (text=chars, image/document=base64 bytes), no JSON-wrapping overhead. Absent on older cores / untruncated results.' } # true ⇒ output was size-bounded to a truncated string.
9292
- # 🔴 core >=5.18.1 (#187). Closed set, mirrored here as `enum` (the vocabulary's owner is the engine).
9293
- # ABSENCE CARRIES NO SEMANTICS: it is either an older caller, or a posture arm (headless auto-deny /
9294
- # blanket onAsk) that deliberately leaves it unset — the two are indistinguishable, so absent is neither
9295
- # "human" nor "no approval happened". Read the run row / checkpoint face to judge a run's fate.
9786
+ # 🔴 BREAKING (server >=7.64.0, core 7.6.0 S6-A): `settledBy` / `resolution` / `autoDenied` / `approver`
9787
+ # are RETIRED and replaced by the ONE `gate` record below. NO 7.64.0+ writer mints them, and the TS type
9788
+ # does NOT carry them — nobody should code against a dead vocabulary.
9789
+ # 🔴 They are still DECLARED here, and only for one reason: this arm is `additionalProperties: false`, and
9790
+ # the durable-ledger replay leg (`GET /v1/runs/:id/events`) passes a stored `tool_end` row through
9791
+ # VERBATIM (the service's `redactLedgerEventData` returns the row unchanged for this type). Server 7.63.0
9792
+ # and earlier really did write all four into that ledger, so on a deployment upgraded across 7.64.0 those
9793
+ # bytes reach a consumer — and without a declaration that is not "four fields we cannot read", it is the
9794
+ # WHOLE FRAME judged invalid (the same failure family as `permissionRules` and `errorCode` before it).
9795
+ # Same doctrine as `LegacyPlaneTaskResult`: the write face mints ONE shape; a face that replays persisted
9796
+ # bytes admits what the retired writer actually left on disk. `x-legacy-replay-only: true` marks them so a
9797
+ # code generator / a reader can tell them apart from live keys at a glance.
9296
9798
  settledBy:
9297
9799
  type: string
9298
9800
  enum: [human, timeout, aborted]
9801
+ x-legacy-replay-only: true
9299
9802
  description: >
9300
- core >=5.18.1 (#187) — HOW this call was settled when it went through an approval:
9301
- `human` = somebody actually answered; `timeout` = the approval window elapsed;
9302
- `aborted` = abort / unclonable arguments / out-of-contract. Present ONLY on the settled call.
9303
- Absent = an older caller OR a posture arm that does not fill it — do NOT infer a semantic
9304
- from the missing key.
9803
+ RETIRED at server 7.64.0 (core 7.6.0 S6-A) — superseded by `gate.settlement`. Appears ONLY when a
9804
+ durable ledger row written by server <= 7.63.0 is replayed. Never minted by a current writer; not on
9805
+ the SDK type. Do not branch on it.
9806
+ resolution:
9807
+ type: string
9808
+ x-legacy-replay-only: true
9809
+ description: 'RETIRED at 7.64.0 — superseded by `gate.settlement.kind`. Replay-only, see `settledBy`.'
9810
+ autoDenied:
9811
+ const: true
9812
+ x-legacy-replay-only: true
9813
+ description: 'RETIRED at 7.64.0 — superseded by `gate.disposition` + `gate.settlement`. Replay-only, see `settledBy`.'
9814
+ approver:
9815
+ type: string
9816
+ x-legacy-replay-only: true
9817
+ description: 'RETIRED at 7.64.0 — superseded by `gate.settlement.who.approver`. Replay-only, see `settledBy`.'
9818
+ gate:
9819
+ allOf: [{ $ref: '#/components/schemas/GateOutcome' }]
9820
+ description: >
9821
+ 🔴 server >= 7.64.0 — the WHOLE record of the gate pass this call went through: who allowed it or
9822
+ which LAYER refused it (`disposition`), how the wait it consumed ended (`settlement`, carrying the
9823
+ refusing person and their own note), and who ASKED (`origin`).
9824
+ 🔴 ABSENCE IS NOT A FACT ABOUT THIS CALL: the gate never saw it (an unarmed gate, a deferred
9825
+ re-send, an orphan picked up by reconcile) — or the record was defective and the service withheld
9826
+ it (see `GateOutcome`). Never read a semantic out of the missing key.
9827
+ delivered:
9828
+ allOf: [{ $ref: '#/components/schemas/McpDelivered' }]
9829
+ description: >
9830
+ core >= 7.6.0 (S6-B) — whether THIS failed MCP request reached the server. Present only on an MCP
9831
+ failure: every successful call and every non-MCP failure omits it, and the absence carries NO
9832
+ semantics. Read it WITH `errorCode` (see `McpDelivered`).
9833
+ gatedCallId:
9834
+ type: string
9835
+ description: >
9836
+ core >= 5.55.0 (#333) — WHICH tool call a durable park is holding, minted by the engine from the
9837
+ committed checkpoint''s pending action (a tool may not self-report it). Present only on a park
9838
+ frame; absent on a park that holds no tool call (resource_limit / plan_review / task_done) —
9839
+ ABSENT, never guessed.
9305
9840
  # 🔴 core >=5.9.0 (W3 / [2535]). OPEN SET — deliberately a bare `type: string`, NOT an enum.
9306
9841
  # This arm is `additionalProperties: false`, and the server's `toolEndEventData` has been emitting
9307
9842
  # `errorCode` since 5.9.0, so omitting it made every legitimate frame carrying one fail strict
@@ -9316,6 +9851,12 @@ components:
9316
9851
  when `isError` is true. OPEN SET: a verbatim passthrough of any tool's `ToolResult.details.code`.
9317
9852
  The engine's own codes (`gate.parked`, `tool.not_found`) are only two examples; the fs tools
9318
9853
  already emit `path_not_in_root` / `readonly_out_of_root`, and self-registered tools may add more.
9854
+ 🔴 core >= 7.6.0 — the MCP family within this open set is now the CLOSED `McpFailureKind` word list
9855
+ (`connect_refused` / `connection_failed` / `connection_closed` / `http_status` / `not_mcp_response` /
9856
+ `spawn_failed` / `timeout` / `protocol` / `invalid_config` / `unknown`); the retired
9857
+ `MCP_FAILURE_CODES` spellings — including `network` and the `http_<status>` encoding — are gone,
9858
+ and an HTTP status now rides the structured `httpStatus` seat on `wiring_manifest.mcp[]` instead of
9859
+ being baked into the code. The set as a WHOLE stays open (a tool owns its own codes).
9319
9860
  Consumers MUST branch with a `default` arm and MUST NOT exhaustively switch on known values.
9320
9861
  The service enforces only a length bound (>128 chars = malformed, the whole key is DROPPED rather
9321
9862
  than truncated — truncating would mint a code core never sent). NOTE: this is a DIFFERENT
@@ -9680,6 +10221,80 @@ components:
9680
10221
  renders "don't ask again" without reading this bit is advertising an outcome it cannot promise.
9681
10222
  🔴 Same three-way reading as `syncWired`: present-and-false = concept known, not armed here;
9682
10223
  absent = the worker never sent the key. Honestly `false` on server 7.12.0.
10224
+ modelGate:
10225
+ type: object
10226
+ additionalProperties: false
10227
+ description: >
10228
+ core >= 7.3.x (design/385 片2b), server >= 7.58 — TENANT-visible: which tools the tool-MODEL gate
10229
+ removed from THIS session and how to get them back. All three keys are engine-derived (`class` = the
10230
+ merged gate table''s class row key, `removed` = wire tool names, `restore` = the engine''s own
10231
+ recovery line), so the service passes them verbatim.
10232
+ 🔴 ALL-OR-NOTHING: the service mints the section only when all three keys are present and
10233
+ well-formed — half a section would read as "these are the only tools that were removed".
10234
+ ABSENT = this run had nothing gated (the service never mints an empty section or `removed: []`).
10235
+ required: [class, removed, restore]
10236
+ properties:
10237
+ class: { type: string }
10238
+ removed: { type: array, items: { type: string } }
10239
+ restore: { type: string }
10240
+ autoMode:
10241
+ type: object
10242
+ additionalProperties: false
10243
+ description: >
10244
+ core >= 7.3.1 (#529), server >= 7.59 — TENANT-visible: did AUTO mode actually arm on THIS leg, and
10245
+ if not, which arm did it stop at. Sibling of `GET /v1/capabilities.permissionModeAuto`, which
10246
+ answers the BEFORE question ("would a submit right now arm?"); this section answers the AFTER one
10247
+ ("did the leg that really ran arm?").
10248
+ 🔴 `reason` is core''s closed word list passed through VERBATIM and is deliberately NOT normalised
10249
+ onto the service''s own six-word `PermissionModeAutoUnarmedReason` set: the two tables answer
10250
+ different questions at different granularity (core''s single `denied` covers the service''s
10251
+ `org_denied` + `local_denied`, and a settings-level kill-switch folds into core''s `no_intent`,
10252
+ not `denied`, because it rewrites the MODE before the intent seat is ever written). To tell those
10253
+ apart, read `/v1/capabilities`.
10254
+ 🔴 ABSENCE IS NOT "not applicable" (core''s own words: it means an older mint or an external
10255
+ derivation) — never fold it to `armed: false`. A self-contradicting pair (`armed` disagreeing with
10256
+ `reason === "armed"`, core''s own in-section invariant) makes the service withhold the WHOLE section.
10257
+ required: [armed, reason]
10258
+ properties:
10259
+ armed: { type: boolean }
10260
+ reason: { type: string, description: 'core''s word (`armed` / `no_intent` / `no_face` / `denied` / `resolver_fault` / `latch_open` today). Open on read — branch known, keep a default arm.' }
10261
+ mcp:
10262
+ type: array
10263
+ description: >
10264
+ core >= 7.5.0 (#562), server >= 7.60 — TENANT-visible: one row per MCP server THIS leg declared —
10265
+ did it connect, if not which failure class, and how many tools it mounted.
10266
+ 🔴 AN EMPTY ARRAY IS NOT ABSENCE: `[]` means "this leg declared no servers"; a missing key means an
10267
+ older mint or an external derivation. Never fold one into the other.
10268
+ 🔴 The engine''s `error` free text is DELIBERATELY NOT PROJECTED (it is remote-author text, and core
10269
+ already owns the single redaction mint point for it). The actionable cause is `errorCode`.
10270
+ A row missing `name` or `status` is dropped individually — the other servers'' rows still ship.
10271
+ items:
10272
+ type: object
10273
+ additionalProperties: false
10274
+ required: [name, status]
10275
+ properties:
10276
+ name: { type: string, description: 'The declared server name.' }
10277
+ status: { type: string, description: 'Three-word engine set (connected / failed / …). Open on read.' }
10278
+ source: { type: string, description: 'The declarer''s own layer label, echoed verbatim by core (single-line, length-capped).' }
10279
+ toolCount: { type: integer, description: 'Tools mounted. ABSENT rather than 0 — a minted 0 would read as "connected with no tools".' }
10280
+ errorCode:
10281
+ type: string
10282
+ description: >
10283
+ core >= 7.6.0 — the `McpFailureKind` word (`connect_refused` / `connection_failed` /
10284
+ `connection_closed` / `http_status` / `not_mcp_response` / `spawn_failed` / `timeout` /
10285
+ `protocol` / `invalid_config` / `unknown`). The retired 7.5.0 spellings — `network` and the
10286
+ `http_<status>` encoding — are GONE; an HTTP status now rides `httpStatus`.
10287
+ Deliberately NOT enumerated here: the vocabulary''s single owner is the engine, and mirroring
10288
+ it would swallow a newly minted word as a violation. Switch with a `default` arm.
10289
+ delivered:
10290
+ allOf: [{ $ref: '#/components/schemas/McpDelivered' }]
10291
+ description: 'core >= 7.6.0 (S6-B) — did the request reach that server. Read WITH `errorCode`.'
10292
+ httpStatus:
10293
+ type: integer
10294
+ description: >
10295
+ core >= 7.6.0 (S6-B) — the status the endpoint answered with; rides ONLY with
10296
+ `errorCode === "http_status"` (401/403 = re-authorize, 5xx = that end is down). ABSENT rather
10297
+ than 0.
9683
10298
  governance:
9684
10299
  type: object
9685
10300
  additionalProperties: false
@@ -9712,6 +10327,16 @@ components:
9712
10327
  `memory.harvest_quarantined` `{count, moved, escalated, reason?}` — `moved` and `escalated` MUST NOT be
9713
10328
  subtracted from each other (an in-place tombstone counts as both) · `memory.delegation_static_mark_waived`
9714
10329
  `{reason, subagentType?, sessionId?}`.
10330
+ `config.durable_gate_unavailable` — a per-principal `forceDurableGate` entitlement took effect on a leg
10331
+ with NO checkpoint store, so every ask on that leg settles on the LIVE chain (answered in-stream where a
10332
+ live approval / question face exists, fail-closed denied where none does) and NO durable approval record
10333
+ is written. `{sessionId, runId, principal?, cause, liveApprover, liveQuestionFace}`.
10334
+ 🔴 `cause` is a CLOSED two-word set: `no_deployment_store` (this deployment wired no store) and
10335
+ `task_store_disabled` (`TaskSpec.checkpointStore: "disabled"` — the per-run off switch).
10336
+ ⚠️ server >= 7.64.0 / core 7.6.0 (D-9) RENAMED that second word from `task_store_null`, and the off
10337
+ switch itself became the WORD `"disabled"` instead of `null` (a string is not nullish, so the ordinary
10338
+ coalesce is the whole resolution rule and no reader has to remember a null test). A consumer still
10339
+ matching `task_store_null` matches nothing.
9715
10340
  A notice whose `detail.sessionId` is absent is HONESTLY NOT DELIVERED (the server never guesses a session),
9716
10341
  so the frame's absence does NOT mean "did not happen" — the operator-facing structured log always has the
9717
10342
  full family. `detail` is already redacted + size-bounded by the server; still treat it as external text.
@@ -9764,14 +10389,28 @@ components:
9764
10389
  kind:
9765
10390
  type: string
9766
10391
  description: >
10392
+ The SIX known kinds (`server src/http/wire-gate.ts` `WIRE_GATE_KINDS`), listed for codegen and
10393
+ autocomplete — NOT a closed set, and deliberately NOT an `enum` (see the note below):
9767
10394
  `human` — plain HITL approval (budget-auto-approvable). `irreversible_ask` — design/37
9768
10395
  irreversibility / design/70 egress tighten; NOT budgetable (load-bearing safety gate).
9769
10396
  `resource_limit` — design/74 cost/token/time slice boundary; resolve `{decision:"continue"}`.
9770
10397
  `needs_review` — design/76 dry-run/shadow → durable `needs_review` TERMINAL (post-prediction
9771
10398
  review, disjoint from the approval family). `task_done` — design/38 Path A background sub-task
9772
- handle (door B never sees it).
9773
- enum: [human, irreversible_ask, resource_limit, needs_review, task_done]
10399
+ handle (door B never sees it). `plan_review` — [2400] TR-8 / design/80 D-B PRE-ACTION plan
10400
+ review, resolved at `POST /v1/assistant/tasks/:id/plan_review` (3-state), never at `/decide`.
10401
+ An unknown future kind is VALID on the wire: branch on `kind` and render it generically.
9774
10402
  x-open-enum: true # do not close this union — an unknown future kind is VALID on the wire
10403
+ # 🔴 顶层 `enum` **已删**(S-170,codex r1 [high] 验真后修)—— 它与本 schema 自己声明的开集语义
10404
+ # 直接矛盾,而矛盾的那一头在执法:
10405
+ # ① 那张表停在**五**词,漏了 `plan_review` —— 它自 [2400] TR-8 起就是**现役**门(SDK 的
10406
+ # `CheckpointGate.kind` 与 server 的 `WIRE_GATE_KINDS` 六词都有它),下面 catch-all 臂的注
10407
+ # 甚至还把它当成「未来的未知 kind」举例。一次 plan_review park 的整条响应因此被判违约。
10408
+ # ② 就算补齐第六词,顶层 `enum` 仍是**合取**的:`x-open-enum` 只是意图标记,标准 JSON Schema
10409
+ # 的 `enum` 照样执法 ⇒ core 加第七个门的当天,每一条那类 park 响应全线违约。
10410
+ # ⇒ **kind 的合法性只由下面的 `oneOf` 一处决定**(具名臂管已知词的形状,catch-all 管未知词),
10411
+ # 规则从两处变一处。已知词表留在本描述里给 codegen / 自动补全读,不再是第二道执法点。
10412
+ # (本车把 `terminal.paused.gate` 指向本 schema 之后,这条既存缺陷的射程从 `suspended` 帧扩到了
10413
+ # 每一条 park 终局 —— 所以在本车里修,而不是留给下一车。)
9775
10414
  reason: { type: string, description: 'present on human / irreversible_ask / resource_limit / needs_review (not task_done).' }
9776
10415
  toolName: { type: string, description: 'present on human / irreversible_ask.' }
9777
10416
  oneOf:
@@ -9790,12 +10429,16 @@ components:
9790
10429
  - type: object # design/38 Path A background sub-task handle (door B never sees it)
9791
10430
  required: [kind]
9792
10431
  properties: { kind: { const: task_done } }
9793
- - type: object # OPEN SET — unknown future kind (e.g. design/80 D-B plan_review); render generically, never crash
10432
+ - type: object # [2400] TR-8 / design/80 D-B — PRE-ACTION plan review; resolved at POST /v1/assistant/tasks/:id/plan_review
10433
+ required: [kind, reason]
10434
+ properties: { kind: { const: plan_review } }
10435
+ - type: object # OPEN SET — a future kind this contract predates; render generically, never crash
9794
10436
  required: [kind]
9795
- # 🔴 the pinned wire contract: exclude the 5 KNOWN kinds so a known kind matches ONLY its const member above
10437
+ # 🔴 the pinned wire contract: exclude the KNOWN kinds so a known kind matches ONLY its const member above
9796
10438
  # (not also this fallback) → keeps `oneOf` unambiguous AND preserves each kind's shape validation.
9797
- # This fallback matches ONLY a future kind this contract predates. Add new known kinds to the enum.
9798
- properties: { kind: { type: string, not: { enum: [human, irreversible_ask, resource_limit, needs_review, task_done] } } }
10439
+ # This fallback matches ONLY a future kind this contract predates. Add new known kinds to the enum
10440
+ # AND to this exclusion list (S-170: `plan_review` was a KNOWN kind sitting in neither).
10441
+ properties: { kind: { type: string, not: { enum: [human, irreversible_ask, resource_limit, needs_review, task_done, plan_review] } } }
9799
10442
 
9800
10443
  Event_suspended:
9801
10444
  type: object
@@ -10513,7 +11156,14 @@ components:
10513
11156
  # ─── S-139(sdk 8.3.0):对 server 7.60.0 整体重对账带进来的四张型面 ───────────────────────
10514
11157
  AskOrigin:
10515
11158
  type: string
10516
- enum: [content_question, unresolvable, org_unavailable, org_rule, hook, ask_rule, denial_limit_fallback, policy]
11159
+ x-open-enum: true
11160
+ # 🔴 顶层 `enum` **不放这里**(S-170 codex r2 [medium] 验真后修,与本车对 `CheckpointGate` 的处置同款):
11161
+ # 词表属主是引擎、而且它**真的长过**(core 7.6.0 从 8 词加到 10 词),而 `tool_approval` 帧这一面
11162
+ # server 只判非空串、不判成员(`src/tool-approval.ts` 帧铸点)⇒ 留一个 `enum` 在这里,core 加词的
11163
+ # 当天,一个**合法**的审批卡就会被严格消费端拒收。`x-open-enum` 只是意图标记,不解除 enum 的执法 ——
11164
+ # 这正是本车刚从 `CheckpointGate` 拆掉的那个假开集,不在这里换个地方重犯。
11165
+ # 闭集**执法**只属于**筛过的**那一面(`GateOutcome.origin`:引擎的 `screenGateOutcome` 判成员,
11166
+ # 出集即整条不上帧),所以那张 enum 落在**那个引用点**,恰一处。已知十词见下面的描述。
10517
11167
  description: >
10518
11168
  WHO raised an ask (`ToolApprovalFrame.origin`; server >= 7.57.0 / core 7.5.0, S-125③/#564) — core's
10519
11169
  `ASK_ORIGINS`, ENGINE-STAMPED at the gate (core's words: "engine-stamped at the gate, never a policy's
@@ -10526,6 +11176,17 @@ components:
10526
11176
  whether THIS DEPLOYMENT's ops governance layer is the gate — `origin: policy` covers any deployment
10527
11177
  ToolPolicy's ask, so a governance-produced ask and an ordinary one look identical in this one word.
10528
11178
 
11179
+ 🔴 core 7.6.0 ADDED TWO WORDS (the set was eight through core 7.5.x): `shell_gate_tighten` = the gate's
11180
+ own irreversibility tighten raised the ask over a tier the COARSE `shellGate` doctrine installed (a
11181
+ shell command under `shellGate: "classify"` / `"always"`), and `safety_tighten` = the gate's own
11182
+ post-fold tightens over an EXPLICIT fact of the call (an egress mark, an irreversibility mark, a
11183
+ peer-message referral in auto mode, a write onto the write-protected table). They are separate words
11184
+ precisely so a deny observer or an approval card can say WHICH engine layer raised the question — one
11185
+ word could not. Both are classifier-eligible (arming auto mode is the deployment's explicit choice to
11186
+ let the classifier resolve those classes).
11187
+
11188
+ The SAME type is the `origin` member of `GateOutcome` on `tool_end` — one vocabulary, two faces.
11189
+
10529
11190
  RuleOffersAbsence:
10530
11191
  type: string
10531
11192
  enum: [mandated, shadowed, lane_cannot_speak]
@@ -11253,8 +11914,15 @@ components:
11253
11914
  · `ActiveRunConflictDoneResult` — the SSE lane's REJECTION terminal: when the session already has an
11254
11915
  active run, the submit's 409 is encoded as this done frame (`src/http/routes/tasks.ts`), and it
11255
11916
  carries NO `taskId` / `sessionId` / `stats`.
11256
- Narrow on the presence of `stats` (required on TaskResult, absent from the conflict shape), or on
11257
- `errorCode === "conflict.session_active_run"`. `status` does NOT discriminate — both shapes carry it.
11917
+ Narrow in TWO steps: `stats` present (an engine result) vs absent (the conflict envelope) — or match
11918
+ `errorCode === "conflict.session_active_run"` — and then, among the engine results, `"terminal" in result`
11919
+ (the current cause form) vs absent (`LegacyPlaneTaskResult`, the flat bytes an older generation left in
11920
+ the durable ledger; a REPLAYED frame on an upgraded deployment can be that shape).
11921
+ 🔴 server >= 7.64.0 — the two arms are now told apart by their KEYS, not by a shared word: the normal
11922
+ terminal carries `terminal` (the tagged cause) and NO `status`; the conflict shape carries the flat
11923
+ `status: "failed"` + `errorCode` because it is the SERVICE's own rejection envelope, not an engine
11924
+ result, and it did not change. `status` therefore appears on exactly one arm — but keep narrowing on
11925
+ `stats` / `errorCode`, which are the discriminants the service pins.
11258
11926
  additionalProperties: false # 封闭:帧本身只有 {type,result,replay?};`result` 内部按臂各自裁定(TaskResult 开集,冲突形封闭)
11259
11927
  required: [type, result]
11260
11928
  properties:
@@ -11263,6 +11931,7 @@ components:
11263
11931
  oneOf:
11264
11932
  - $ref: '#/components/schemas/TaskResult'
11265
11933
  - $ref: '#/components/schemas/ActiveRunConflictDoneResult'
11934
+ - $ref: '#/components/schemas/LegacyPlaneTaskResult'
11266
11935
  replay:
11267
11936
  type: boolean
11268
11937
  description: >
@@ -12144,7 +12813,9 @@ components:
12144
12813
  core's assembly self-evidence manifest (core >=5.14.0 `WiringManifest`). TWO HALVES, ONE shape (a
12145
12814
  consumer must line the two faces up, so they are not split into separate types) — but which keys
12146
12815
  belong to which half is DETERMINATE, not "usually": `leg`, `ask.effective` and `configFingerprint`
12147
- are EFFECTIVE-half only (the live `wiring_manifest` event) and the STATIC half
12816
+ are EFFECTIVE-half only (the live `wiring_manifest` event) — as are the three per-leg sections the
12817
+ event schema declares (`modelGate`, `autoMode`, `mcp`), which describe what THIS leg did and have no
12818
+ static counterpart — and the STATIC half
12148
12819
  (GET /v1/diagnostics/wiring) NEVER mints them; every other section, plus `governance`, is always
12149
12820
  present on the static half. Fields stay optional because core may add or drop sections and that
12150
12821
  must not hard-break a client — read by half, and never wait on a key the half never sends.
@@ -12232,12 +12903,58 @@ components:
12232
12903
 
12233
12904
  WiringDiagnostics:
12234
12905
  type: object
12235
- description: 'GET /v1/diagnostics/wiring response (operator-only). No per-leg history by design.'
12906
+ description: >
12907
+ GET /v1/diagnostics/wiring response (operator-only). No per-leg history by design.
12908
+ 🔴 Every posture key here answers "what can THIS PROCESS say", so its absent form is an explicit
12909
+ `null` — never an empty shell that would read as a real answer. `null` = this process cannot say
12910
+ (it was not assembled by the composition root, or the facility is not wired).
12236
12911
  required: [static, serverGates]
12237
12912
  additionalProperties: false
12238
12913
  properties:
12239
12914
  static: { $ref: '#/components/schemas/WiringManifest' }
12240
12915
  serverGates: { $ref: '#/components/schemas/ServerWiringGates' }
12916
+ writeProtection:
12917
+ oneOf:
12918
+ - $ref: '#/components/schemas/WriteProtectionPosture'
12919
+ - type: 'null'
12920
+ description: >
12921
+ server >= 7.63.0 (S-138) — the write-protection table, WITH row contents (operator face only; the
12922
+ tenant face `capabilities.writeProtection` carries counts alone). Same boot artefact as that bit.
12923
+ readFace:
12924
+ $ref: '#/components/schemas/ReadFacePosture'
12925
+ description: >
12926
+ server >= 7.65.0 (S-167) — the READ containment rung plus its provenance (`env` / `center` /
12927
+ `posture` / `engine-default`). Absent on older servers; never `null` on 7.65.0+ (the server always
12928
+ has an answer — `face: null` is how "nothing pinned" is spelled, the SOURCE is still known).
12929
+ memoryPosture:
12930
+ type: ["object", "null"]
12931
+ additionalProperties: true
12932
+ description: >
12933
+ server >= 7.30 (#252) — the memory plane's posture (backend / vector mode / lit-or-dark and why /
12934
+ embedder identity). Facts that used to live only in the startup log, which is neither queryable nor
12935
+ aggregatable across replicas. NOT YET MIRRORED KEY-BY-KEY in this SDK generation: declared here so a
12936
+ strict consumer stops rejecting a real operator response, read it as opaque.
12937
+ sqlEngine:
12938
+ type: ["object", "null"]
12939
+ additionalProperties: true
12940
+ description: >
12941
+ server >= 7.60 (S-131) — this deployment's SQL engine posture (engine / version / session isolation
12942
+ level / TiDB transaction mode), READ BACK off a live connection rather than echoed from env.
12943
+ `null` = no SQL backend. NOT YET MIRRORED KEY-BY-KEY in this SDK generation — read as opaque.
12944
+ configApply:
12945
+ type: ["object", "null"]
12946
+ additionalProperties: true
12947
+ description: >
12948
+ server >= 7.5x (#322) — the config generation ledger's operator face (per-group appliedVersion,
12949
+ deferredKeys with their reason, and a SANITISED lastRejected: key names and verdict text only,
12950
+ never values). The three keys on `/health` are a narrow projection of this same reading.
12951
+ NOT YET MIRRORED KEY-BY-KEY in this SDK generation — read as opaque.
12952
+ mcpRevocations:
12953
+ type: ["object", "null"]
12954
+ additionalProperties: true
12955
+ description: >
12956
+ server >= 7.5x (#324, design/338) — the mid-turn MCP revocation list. `null` = this process has no
12957
+ config pipeline (it cannot say). NOT YET MIRRORED KEY-BY-KEY in this SDK generation — read as opaque.
12241
12958
 
12242
12959
  SendfileLinkRow:
12243
12960
  type: object