@sema-agent/sdk 24.0.0 → 26.0.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.
Files changed (83) hide show
  1. package/README.md +243 -6
  2. package/dist/client.d.ts +6 -5
  3. package/dist/client.d.ts.map +1 -1
  4. package/dist/client.js +8 -6
  5. package/dist/client.js.map +1 -1
  6. package/dist/device/index.d.ts +76 -12
  7. package/dist/device/index.d.ts.map +1 -1
  8. package/dist/device/index.js +90 -33
  9. package/dist/device/index.js.map +1 -1
  10. package/dist/errors.d.ts +162 -30
  11. package/dist/errors.d.ts.map +1 -1
  12. package/dist/errors.js +71 -35
  13. package/dist/errors.js.map +1 -1
  14. package/dist/events.d.ts +53 -13
  15. package/dist/events.d.ts.map +1 -1
  16. package/dist/events.js +3 -2
  17. package/dist/events.js.map +1 -1
  18. package/dist/health.d.ts +25 -0
  19. package/dist/health.d.ts.map +1 -1
  20. package/dist/health.js.map +1 -1
  21. package/dist/index.d.ts +44 -23
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +22 -6
  24. package/dist/index.js.map +1 -1
  25. package/dist/model-route-env/index.d.ts +129 -0
  26. package/dist/model-route-env/index.d.ts.map +1 -0
  27. package/dist/model-route-env/index.js +131 -0
  28. package/dist/model-route-env/index.js.map +1 -0
  29. package/dist/resources/adoption.d.ts +8 -2
  30. package/dist/resources/adoption.d.ts.map +1 -1
  31. package/dist/resources/adoption.js.map +1 -1
  32. package/dist/resources/approvals.d.ts +38 -4
  33. package/dist/resources/approvals.d.ts.map +1 -1
  34. package/dist/resources/approvals.js +19 -4
  35. package/dist/resources/approvals.js.map +1 -1
  36. package/dist/resources/fleet.d.ts +73 -18
  37. package/dist/resources/fleet.d.ts.map +1 -1
  38. package/dist/resources/fleet.js.map +1 -1
  39. package/dist/resources/ops.d.ts +113 -0
  40. package/dist/resources/ops.d.ts.map +1 -1
  41. package/dist/resources/ops.js +10 -0
  42. package/dist/resources/ops.js.map +1 -1
  43. package/dist/resources/principal-inbox.d.ts +62 -0
  44. package/dist/resources/principal-inbox.d.ts.map +1 -0
  45. package/dist/resources/principal-inbox.js +51 -0
  46. package/dist/resources/principal-inbox.js.map +1 -0
  47. package/dist/resources/rules.d.ts +2 -5
  48. package/dist/resources/rules.d.ts.map +1 -1
  49. package/dist/resources/rules.js.map +1 -1
  50. package/dist/resources/session-sync.d.ts +27 -32
  51. package/dist/resources/session-sync.d.ts.map +1 -1
  52. package/dist/resources/session-sync.js +25 -39
  53. package/dist/resources/session-sync.js.map +1 -1
  54. package/dist/resources/tool-approvals.d.ts +251 -115
  55. package/dist/resources/tool-approvals.d.ts.map +1 -1
  56. package/dist/resources/tool-approvals.js +222 -69
  57. package/dist/resources/tool-approvals.js.map +1 -1
  58. package/dist/settings.d.ts +83 -1
  59. package/dist/settings.d.ts.map +1 -1
  60. package/dist/settings.js +0 -24
  61. package/dist/settings.js.map +1 -1
  62. package/dist/sse.d.ts +5 -1
  63. package/dist/sse.d.ts.map +1 -1
  64. package/dist/sse.js +6 -2
  65. package/dist/sse.js.map +1 -1
  66. package/dist/sync.d.ts +5 -5
  67. package/dist/sync.d.ts.map +1 -1
  68. package/dist/sync.js +13 -55
  69. package/dist/sync.js.map +1 -1
  70. package/dist/transport.d.ts +2 -0
  71. package/dist/transport.d.ts.map +1 -1
  72. package/dist/transport.js +3 -0
  73. package/dist/transport.js.map +1 -1
  74. package/dist/types.d.ts +298 -123
  75. package/dist/types.d.ts.map +1 -1
  76. package/dist/types.js +10 -3
  77. package/dist/types.js.map +1 -1
  78. package/openapi.yaml +1014 -572
  79. package/package.json +6 -2
  80. package/dist/resources/workspace.d.ts +0 -95
  81. package/dist/resources/workspace.d.ts.map +0 -1
  82. package/dist/resources/workspace.js +0 -60
  83. package/dist/resources/workspace.js.map +0 -1
package/openapi.yaml CHANGED
@@ -172,7 +172,7 @@ tags:
172
172
  themself said yes"), never operator. GATED AND OFF BY DEFAULT: the whole lane hangs on
173
173
  PERMISSION_RULES_ENABLED (default false) plus a wired rule store — so on a stock deployment BOTH doors
174
174
  answer 501 `capability.rule_store_required`, and so does the single-card half (the ask frame simply
175
- carries no `ruleSuggestions`). Read the observed frame / an actual response, never a version number.
175
+ carries no `ruleOffers`). Read the observed frame / an actual response, never a version number.
176
176
  The SINGLE-CARD half of the same lane is `persistRule` on POST /v1/tool-approvals/{id}/respond.
177
177
  - name: adoption
178
178
  description: >
@@ -2280,38 +2280,16 @@ paths:
2280
2280
  get:
2281
2281
  tags: [workspace]
2282
2282
  operationId: workspaceList
2283
- x-status: retired # S-15/design/381 (server 7.52.0): whole-tree snapshot epoch retired — every ≥7.52 deployment answers 501 capability.workspace_retired.
2284
- summary: 'RETIRED (server 7.52.0, S-15) — snapshot listing; ≥7.52 answers 501 capability.workspace_retired. Live on 1.299–7.51 only.'
2285
- description: >
2286
- 🔴 RETIRED as of `@sema-agent/server` 7.52.0 (S-15/design/381): the E19 whole-tree snapshot store this
2287
- face projected was replaced by per-edited-file rewind HISTORY, which holds only files the agent edited
2288
- — structurally there is no "whole workspace" manifest to browse anymore. The route SHAPE is kept
2289
- server-side as a tombstone: every ≥7.52 deployment answers 501 `capability.workspace_retired` (a named
2290
- refusal, not a 404 that reads like a typo), and `capabilities.workspace` is always false (same
2291
- predicate). There is NO 1:1 replacement path — a future "rewind coverage" browse would be a NEW face
2292
- with tracked-set semantics, not this name wired back. Related live faces: rewind restore rides
2293
- `TaskRequest.restoreFiles` (probe `capabilities.restoreFiles`); per-file history blobs ride
2294
- `GET /v1/sessions/{sessionId}/sync/history/blobs/{hash}`.
2295
- Historical shape (servers 1.299–7.51): read-only projection over the E19 snapshot store. Rows carry
2296
- `{key, files, bytes?}`; `latest` = the newest snapshot key. Owner-scoped (non-owner → 404, no
2297
- oracle); 501 without the snapshot-store read faces (probe `capabilities.workspace`).
2298
- parameters:
2299
- - in: query
2300
- name: limit
2301
- required: false
2302
- schema: { type: integer, minimum: 1, maximum: 100 }
2283
+ x-status: retired # server 7.52.0: whole-tree snapshot epoch retired; sdk 26.0.0 deleted the verb (support floor 7.52.0).
2284
+ summary: 'RETIRED (server 7.52.0, S-15) — snapshot listing; every supported server (>= 7.52.0) answers 501 capability.workspace_retired.'
2285
+ description: >
2286
+ 🔴 RETIRED as of `@sema-agent/server` 7.52.0 (S-15/design/381): the E19 whole-tree snapshot store this face projected was replaced by per-edited-file
2287
+ rewind HISTORY (only files the agent edited — there is no "whole workspace" manifest to browse). The route SHAPE is kept server-side as a tombstone:
2288
+ every supported server answers 501 `capability.workspace_retired` (a named refusal, not a 404 that reads like a typo), and `capabilities.workspace` is
2289
+ always `false` (same predicate). No 1:1 replacement: rewind restore rides `TaskRequest.restoreFiles` (probe `capabilities.restoreFiles`); per-file
2290
+ history blobs ride `GET /v1/sessions/{sessionId}/sync/history/blobs/{hash}`. sdk 26.0.0 deleted `client.workspace` (its 200 shapes only existed on 1.299–7.51).
2303
2291
  responses:
2304
- '200':
2305
- description: The snapshot listing.
2306
- content:
2307
- application/json:
2308
- # 封闭两层(census 批2 第三段,2026-07-30):铸造点 `routes/sessions.ts`(信封与行两处)。条件键=信封 `latest`(零快照即省略)、行 `bytes`(blobSizes 面缺席=诚实省略)。
2309
- # 4.3.0:形提成具名 `WorkspaceSnapshotPage`(SDK 同批把返回位的内联字面量也提成了 exported type,
2310
- # 让 spec-resource-schema-gate 罩得住 —— CAPS-OPS-8)。
2311
- schema: { $ref: '#/components/schemas/WorkspaceSnapshotPage' }
2312
- '400': { $ref: '#/components/responses/BadRequest' }
2313
2292
  '401': { $ref: '#/components/responses/Unauthorized' }
2314
- '404': { $ref: '#/components/responses/NotFound' }
2315
2293
  '501': { $ref: '#/components/responses/NotImplemented' }
2316
2294
 
2317
2295
  /v1/sessions/{sessionId}/workspace/{key}/tree:
@@ -2325,38 +2303,19 @@ paths:
2325
2303
  name: key
2326
2304
  required: true
2327
2305
  schema: { type: string }
2328
- description: 'Snapshot key (entryId), or the alias `latest` = the newest snapshot.'
2329
2306
  get:
2330
2307
  tags: [workspace]
2331
2308
  operationId: workspaceTree
2332
- x-status: retired # S-15 (server 7.52.0): retired with the family — see workspaceList's retirement note.
2333
- summary: 'RETIRED (server 7.52.0, S-15) — snapshot file tree; ≥7.52 answers 501 capability.workspace_retired.'
2334
- description: >
2335
- 🔴 RETIRED as of server 7.52.0 — same retirement (and same replacement pointers) as `workspaceList`
2336
- above: every ≥7.52 deployment answers 501 `capability.workspace_retired`; `capabilities.workspace` is
2337
- always false. Historical shape (1.299–7.51): entries carry `{path, hash, size?}` — `hash` is the
2338
- sha256 content-address (cross-snapshot change highlighting, [1894]②); `size` only when the SQL size
2339
- index is available. `limit` 1..2000 default 500, `offset` cursor; `nextOffset` present = more pages.
2340
- parameters:
2341
- - in: query
2342
- name: limit
2343
- required: false
2344
- schema: { type: integer, minimum: 1, maximum: 2000 }
2345
- - in: query
2346
- name: offset
2347
- required: false
2348
- schema: { type: integer, minimum: 0 }
2309
+ x-status: retired # server 7.52.0: whole-tree snapshot epoch retired; sdk 26.0.0 deleted the verb (support floor 7.52.0).
2310
+ summary: 'RETIRED (server 7.52.0, S-15) — snapshot file tree; every supported server (>= 7.52.0) answers 501 capability.workspace_retired.'
2311
+ description: >
2312
+ 🔴 RETIRED as of `@sema-agent/server` 7.52.0 (S-15/design/381): the E19 whole-tree snapshot store this face projected was replaced by per-edited-file
2313
+ rewind HISTORY (only files the agent edited — there is no "whole workspace" manifest to browse). The route SHAPE is kept server-side as a tombstone:
2314
+ every supported server answers 501 `capability.workspace_retired` (a named refusal, not a 404 that reads like a typo), and `capabilities.workspace` is
2315
+ always `false` (same predicate). No 1:1 replacement: rewind restore rides `TaskRequest.restoreFiles` (probe `capabilities.restoreFiles`); per-file
2316
+ history blobs ride `GET /v1/sessions/{sessionId}/sync/history/blobs/{hash}`. sdk 26.0.0 deleted `client.workspace` (its 200 shapes only existed on 1.299–7.51).
2349
2317
  responses:
2350
- '200':
2351
- description: One page of the tree.
2352
- content:
2353
- application/json:
2354
- # 封闭两层(census 批2 第三段,2026-07-30):铸造点 `routes/sessions.ts`(信封与行两处)。条件键=信封 `nextOffset`(尾页省略)、行 `size`(blobSizes 面缺席省略)。
2355
- # 4.3.0:形提成具名 `WorkspaceTreePage`(理由同 list 腿 —— CAPS-OPS-8)。
2356
- schema: { $ref: '#/components/schemas/WorkspaceTreePage' }
2357
- '400': { $ref: '#/components/responses/BadRequest' }
2358
2318
  '401': { $ref: '#/components/responses/Unauthorized' }
2359
- '404': { $ref: '#/components/responses/NotFound' }
2360
2319
  '501': { $ref: '#/components/responses/NotImplemented' }
2361
2320
 
2362
2321
  /v1/sessions/{sessionId}/workspace/{key}/file:
@@ -2373,46 +2332,16 @@ paths:
2373
2332
  get:
2374
2333
  tags: [workspace]
2375
2334
  operationId: workspaceFile
2376
- x-status: retired # S-15 (server 7.52.0): retired with the family — ≥7.52 the tombstone rejects BEFORE any size check, so the 413 envelope below is unreachable there.
2377
- summary: 'RETIRED (server 7.52.0, S-15) — single-file read; ≥7.52 answers 501 capability.workspace_retired.'
2378
- description: >
2379
- 🔴 RETIRED as of server 7.52.0 — same retirement (and replacement pointers) as `workspaceList` above.
2380
- On ≥7.52 the tombstone 501 fires BEFORE any size check, so the 413 `workspace_file_too_large`
2381
- envelope below is UNREACHABLE there (the SDK keeps its typed WorkspaceFileTooLargeError for
2382
- 1.299–7.51 servers still inside the support range). Historical shape (1.299–7.51): raw bytes with a
2383
- CONSERVATIVE content-type table (html/svg etc. deliberately serve as octet-stream — agent output is
2384
- untrusted; `x-content-type-options: nosniff` always set) + the `x-sema-content-binary` discriminator
2385
- header (first-8KiB NUL sniff, [1894]③). Capped at `capabilities.workspace.maxFileBytes` (default
2386
- 8MiB, WORKSPACE_FILE_MAX_BYTES) — over-cap ⇒ 413 STRUCTURED envelope
2387
- `{errorCode: "workspace_file_too_large", sizeBytes, limit}` ([1894]①; archive was the escape hatch).
2388
- With the SQL size index the 413 fired BEFORE fetching the bytes.
2389
- parameters:
2390
- - in: query
2391
- name: path
2392
- required: true
2393
- schema: { type: string }
2394
- description: The tree's relPath, verbatim.
2335
+ x-status: retired # server 7.52.0: whole-tree snapshot epoch retired; sdk 26.0.0 deleted the verb (support floor 7.52.0).
2336
+ summary: 'RETIRED (server 7.52.0, S-15) — snapshot single file; every supported server (>= 7.52.0) answers 501 capability.workspace_retired.'
2337
+ description: >
2338
+ 🔴 RETIRED as of `@sema-agent/server` 7.52.0 (S-15/design/381): the E19 whole-tree snapshot store this face projected was replaced by per-edited-file
2339
+ rewind HISTORY (only files the agent edited — there is no "whole workspace" manifest to browse). The route SHAPE is kept server-side as a tombstone:
2340
+ every supported server answers 501 `capability.workspace_retired` (a named refusal, not a 404 that reads like a typo), and `capabilities.workspace` is
2341
+ always `false` (same predicate). No 1:1 replacement: rewind restore rides `TaskRequest.restoreFiles` (probe `capabilities.restoreFiles`); per-file
2342
+ history blobs ride `GET /v1/sessions/{sessionId}/sync/history/blobs/{hash}`. sdk 26.0.0 deleted `client.workspace` (its 200 shapes only existed on 1.299–7.51).
2395
2343
  responses:
2396
- '200':
2397
- description: The file bytes.
2398
- content:
2399
- application/octet-stream:
2400
- schema: { type: string, format: binary }
2401
- '400': { $ref: '#/components/responses/BadRequest' }
2402
2344
  '401': { $ref: '#/components/responses/Unauthorized' }
2403
- '404': { $ref: '#/components/responses/NotFound' }
2404
- '413':
2405
- description: 'Structured over-cap envelope: {error, errorCode: "workspace_file_too_large", sizeBytes, limit}.'
2406
- content:
2407
- application/json:
2408
- schema:
2409
- type: object
2410
- required: [error, errorCode, sizeBytes, limit]
2411
- properties:
2412
- error: { type: string }
2413
- errorCode: { type: string, enum: [workspace_file_too_large] }
2414
- sizeBytes: { type: integer }
2415
- limit: { type: integer }
2416
2345
  '501': { $ref: '#/components/responses/NotImplemented' }
2417
2346
 
2418
2347
  /v1/sessions/{sessionId}/workspace/{key}/archive:
@@ -2429,28 +2358,17 @@ paths:
2429
2358
  get:
2430
2359
  tags: [workspace]
2431
2360
  operationId: workspaceArchive
2432
- x-status: retired # S-15 (server 7.52.0): retired with the family — see workspaceList's retirement note.
2433
- summary: 'RETIRED (server 7.52.0, S-15) — whole-snapshot tar; ≥7.52 answers 501 capability.workspace_retired.'
2434
- description: >
2435
- 🔴 RETIRED as of server 7.52.0 — same retirement (and replacement pointers) as `workspaceList` above:
2436
- every ≥7.52 deployment answers 501 `capability.workspace_retired`. Historical shape (1.299–7.51):
2437
- streamed ustar (`content-disposition: attachment`), entries in stable path order, blob-by-blob with
2438
- backpressure — server memory stays O(1 file). Pre-flight checks BEFORE the headers went out: paths
2439
- not representable in ustar ⇒ 422 naming the first offenders; missing blobs (raced a reap / partial
2440
- import) ⇒ 502 (retryable). A blob vanishing MID-stream destroyed the connection (honest truncation)
2441
- rather than faking a complete archive.
2361
+ x-status: retired # server 7.52.0: whole-tree snapshot epoch retired; sdk 26.0.0 deleted the verb (support floor 7.52.0).
2362
+ summary: 'RETIRED (server 7.52.0, S-15) — snapshot tar archive; every supported server (>= 7.52.0) answers 501 capability.workspace_retired.'
2363
+ description: >
2364
+ 🔴 RETIRED as of `@sema-agent/server` 7.52.0 (S-15/design/381): the E19 whole-tree snapshot store this face projected was replaced by per-edited-file
2365
+ rewind HISTORY (only files the agent edited — there is no "whole workspace" manifest to browse). The route SHAPE is kept server-side as a tombstone:
2366
+ every supported server answers 501 `capability.workspace_retired` (a named refusal, not a 404 that reads like a typo), and `capabilities.workspace` is
2367
+ always `false` (same predicate). No 1:1 replacement: rewind restore rides `TaskRequest.restoreFiles` (probe `capabilities.restoreFiles`); per-file
2368
+ history blobs ride `GET /v1/sessions/{sessionId}/sync/history/blobs/{hash}`. sdk 26.0.0 deleted `client.workspace` (its 200 shapes only existed on 1.299–7.51).
2442
2369
  responses:
2443
- '200':
2444
- description: The tar stream.
2445
- content:
2446
- application/x-tar:
2447
- schema: { type: string, format: binary }
2448
- '400': { $ref: '#/components/responses/BadRequest' }
2449
2370
  '401': { $ref: '#/components/responses/Unauthorized' }
2450
- '404': { $ref: '#/components/responses/NotFound' }
2451
- '422': { description: 'Snapshot contains paths not representable in ustar (names the first 5).' }
2452
2371
  '501': { $ref: '#/components/responses/NotImplemented' }
2453
- '502': { description: 'Snapshot is missing blobs (raced a reap or a partial import) — retry or pick another snapshot.' }
2454
2372
 
2455
2373
  /v1/sessions/{sessionId}/policy:
2456
2374
  parameters:
@@ -3040,50 +2958,6 @@ paths:
3040
2958
  '404': { $ref: '#/components/responses/NotFound' }
3041
2959
  '501': { $ref: '#/components/responses/NotImplemented' }
3042
2960
 
3043
- /v1/sessions/{sessionId}/sync/snapshots/{key}/blobs/{hash}:
3044
- parameters:
3045
- - $ref: '#/components/parameters/PrincipalHeader'
3046
- - in: path
3047
- name: sessionId
3048
- required: true
3049
- schema: { type: string }
3050
- - in: path
3051
- name: key
3052
- required: true
3053
- schema: { type: string }
3054
- description: The snapshot key (= a `SessionTreeEntry.id`).
3055
- - in: path
3056
- name: hash
3057
- required: true
3058
- schema: { type: string, pattern: '^[0-9a-f]{64}$' }
3059
- description: The content-address (SHA-256, 64 lowercase-hex).
3060
- get:
3061
- tags: [sessions]
3062
- operationId: sessionSyncGetBlob
3063
- x-status: retired # S-15 (server 7.52.0): the E19-era snapshot-scoped PULL arm was REPLACED by GET …/sync/history/blobs/{hash} — ≥7.52 this path 404s (not_found.route).
3064
- summary: 'RETIRED (server 7.52.0, S-15) — snapshot-scoped blob PULL; use GET …/sync/history/blobs/{hash}. Live on ≤7.51 only.'
3065
- description: >
3066
- 🔴 RETIRED as of `@sema-agent/server` 7.52.0 (S-15/design/381): a blob is scoped by the session's
3067
- HISTORY GRAPH now, not by a per-snapshot manifest key, so this arm was replaced by
3068
- `GET /v1/sessions/{sessionId}/sync/history/blobs/{hash}` (sessionSyncGetHistoryBlob — same
3069
- octet-stream body, same no-oracle 404 posture, scope check = "referenced by this session's
3070
- exportHistory envelope"). On ≥7.52 this path no longer matches the sync route family and answers 404
3071
- `not_found.route`. SDK: `sessions.sync.getHistoryBlob` (the legacy `getBlob` verb stays, deprecated,
3072
- for ≤7.51 servers inside the support range).
3073
- Historical shape (≤7.51): one content-addressed blob's RAW bytes; the cloud served a hash ONLY if
3074
- THIS session's `:key` manifest referenced it (foreign/unreferenced → 404 `blob not found`). 400 on a
3075
- non-64-hex hash or an undecodable key/hash. Owner-scoped (404).
3076
- responses:
3077
- '200':
3078
- description: The blob bytes.
3079
- content:
3080
- application/octet-stream:
3081
- schema: { type: string, format: binary }
3082
- '400': { $ref: '#/components/responses/BadRequest' }
3083
- '401': { $ref: '#/components/responses/Unauthorized' }
3084
- '404': { $ref: '#/components/responses/NotFound' }
3085
- '501': { $ref: '#/components/responses/NotImplemented' }
3086
-
3087
2961
  /v1/sessions/{sessionId}/sync/history/blobs/{hash}:
3088
2962
  parameters:
3089
2963
  - $ref: '#/components/parameters/PrincipalHeader'
@@ -3109,7 +2983,7 @@ paths:
3109
2983
  → 404 `not_found.blob`, never a blind read; a server whose file-history store has no blob faces
3110
2984
  (e.g. the local file backend) answers the SAME 404 shape (no oracle — "nothing servable" and "not
3111
2985
  referenced" are deliberately indistinguishable). 400 `request.id_invalid` on a non-64-hex hash.
3112
- Owner-scoped (404). Replaces the retired `GET …/sync/snapshots/{key}/blobs/{hash}`.
2986
+ Owner-scoped (404). Replaces the retired snapshot-scoped PULL arm of servers <= 7.51 (below the support floor; sdk 26.0.0 deleted `getBlob` and its path).
3113
2987
  responses:
3114
2988
  '200':
3115
2989
  description: The blob bytes.
@@ -3142,12 +3016,10 @@ paths:
3142
3016
  body). The cloud verifies `sha256(body) === :hash` (the content-address integrity guard) BEFORE storing — a
3143
3017
  mismatch → 400 `blob hash mismatch`. own-or-FRESH gate. Per-blob cap 64 MiB → 413 mid-stream beyond it. A
3144
3018
  backend write failure (e.g. a MinIO non-2xx) → 502 `blob store write failed`. 204 on success (no body). The
3145
- PUSH path is the BARE `blobs/:hash` (the PULL is `history/blobs/:hash` since server 7.52,
3146
- `snapshots/:key/blobs/:hash` before). 🔴 server ≥7.52 (S-15 换店): the blob faces ride the
3019
+ PUSH path is the BARE `blobs/:hash` (the PULL is `history/blobs/:hash`). The blob faces ride the
3147
3020
  file-HISTORY store — a deployment whose store has no blob faces (the local file backend) answers 501
3148
3021
  `capability.snapshot_store_required` ("blob upload requires a durable file-history store") while the
3149
- rest of the sync surface keeps working; on ≤7.51 the same code meant a missing snapshot store. Same
3150
- code, same operator disposition (wire a backend with blob faces).
3022
+ rest of the sync surface keeps working (operator disposition: wire a backend with blob faces).
3151
3023
  requestBody:
3152
3024
  required: true
3153
3025
  content:
@@ -3217,14 +3089,12 @@ paths:
3217
3089
  x-status: live
3218
3090
  summary: PUSH Phase A — classify + stage (mint a stagingId).
3219
3091
  description: >
3220
- 2c session-sync PUSH Phase A — a SMALL metadata body (`entryIds, snapshots, policy, anchors, resolution?`); the
3092
+ 2c session-sync PUSH Phase A — a SMALL metadata body (`entryIds, fileHistory?, policy, anchors, resolution?, logDigest?, title?, pinned?`); the
3221
3093
  entries stream in Phase B. The cloud classifies (§7), pre-checks blob presence (a missing referenced hash →
3222
3094
  422 `missing_blob` — PUT it first), guards the import lease + any active run (409), then mints a stagingId.
3223
- ⚠️ S-15 EPOCH: server ≥7.52's Phase A consumes `{entryIds, fileHistory?, policy, anchors, resolution?,
3224
- logDigest?}` (routes/session-sync.ts) — the `snapshots` key is SILENTLY IGNORED there (an unknown body key); the
3225
- file history rides `fileHistory` (a `FileHistoryExport` envelope, `null` / absent = none) and every blob it references must have been
3226
- `PUT …/sync/blobs/{hash}` first. ≤7.51 servers consume `snapshots` and do not know `fileHistory`. SDK 16.2.0 carries
3227
- `fileHistory` in `pushSessionBundle` / `importSessionBundle` (and fills `snapshots: []` on the pull side when the manifest omits it); a hand-rolled
3095
+ The file history rides `fileHistory` (a `FileHistoryExport` envelope, `null` / absent = none) and every blob it references must have been
3096
+ `PUT …/sync/blobs/{hash}` first (the `snapshots` key of servers <= 7.51 is outside the support range; a supported server ignores it and sdk 26.0.0
3097
+ no longer declares it). The SDK carries `fileHistory` in `pushSessionBundle` / `importSessionBundle`; a hand-rolled
3228
3098
  push must send `fileHistory` itself, and only to a peer whose `capabilities.sessionSyncFileHistory` is not false (server >= 7.116.0 — else the
3229
3099
  commit 422s `missing_blob`).
3230
3100
  `identical` → `{ relation:"identical", basis, payloadVerified }` with NO stagingId (skip Phase B);
@@ -3239,15 +3109,14 @@ paths:
3239
3109
  application/json:
3240
3110
  schema:
3241
3111
  type: object
3242
- required: [entryIds, snapshots, policy, anchors]
3112
+ required: [entryIds, policy, anchors]
3243
3113
  properties:
3244
3114
  entryIds: { type: array, items: { type: string } }
3245
- snapshots: { type: array, items: { $ref: '#/components/schemas/SyncSnapshot' } }
3246
3115
  fileHistory:
3247
3116
  oneOf:
3248
3117
  - { $ref: '#/components/schemas/FileHistoryExport' }
3249
3118
  - { type: 'null' }
3250
- description: 'server >= 7.52: the file-history envelope (replaces `snapshots`); `null` / absent = none. Every blob it references must already be PUT.'
3119
+ description: 'the file-history envelope; `null` / absent = none. Every blob it references must already be PUT.'
3251
3120
  policy: { type: array, items: { $ref: '#/components/schemas/SessionRulesRecord' } }
3252
3121
  anchors: { type: array, items: { $ref: '#/components/schemas/SyncAnchor' } }
3253
3122
  title:
@@ -3324,7 +3193,7 @@ paths:
3324
3193
  description: >
3325
3194
  2c session-sync PUSH Phase B — stream the entry log as an NDJSON REQUEST body into the staged shadow id; the
3326
3195
  cloud validates per-line (the streaming invariant gate) then `finish()` (exactly-one-root + leaf-resolvable) →
3327
- commits (the atomic swap) → replays snapshots/policy/anchors. Returns `{ relation }` (the AUTHORITATIVE in-txn
3196
+ commits (the atomic swap) → replays fileHistory/policy/anchors. Returns `{ relation }` (the AUTHORITATIVE in-txn
3328
3197
  re-classify). Re-POSTing the same stagingId is idempotent. Typed errors: a per-entry / structural violation →
3329
3198
  422 `invalid_entries`; a line too large → 413; a commit-time TOCTOU flip to fork/stale → 409 `conflict`
3330
3199
  (SyncConflictError); a refused policy loosen → 403 `loosen_forbidden`; a wrong/foreign stagingId → 404.
@@ -3385,6 +3254,15 @@ paths:
3385
3254
  application/json:
3386
3255
  schema: { $ref: '#/components/schemas/PendingList' }
3387
3256
  '401': { $ref: '#/components/responses/Unauthorized' }
3257
+ '500':
3258
+ description: >-
3259
+ server >= 7.134.0: a queue row whose stored body cannot be read (the store's refusal contract — a retry fails the same way; an operator has to repair
3260
+ or delete that row) answers `500` + `errorCode: "approvals.read_corrupt"` (body `{ error, errorCode }`; the bad row's coordinates stay off the wire) —
3261
+ the same judge and the same code as `GET /v1/approvals/stream`. Do NOT retry it as-is (the SDK's GET retry arm exempts the code). Every other read
3262
+ failure (connection / timeout / store hiccup) is still `500 internal.error`. Older servers answered the corrupt row with `internal.error`.
3263
+ content:
3264
+ application/json:
3265
+ schema: { $ref: '#/components/schemas/ErrorResponse' }
3388
3266
 
3389
3267
  /v1/approvals/stream:
3390
3268
  parameters:
@@ -3424,7 +3302,7 @@ paths:
3424
3302
  `synced` (and `synced.count` still counts ONLY the durable queue).
3425
3303
  🔴 THE TWO WORDS DELIBERATELY DO NOT REUSE `pending`/`resolved`: those are keyed by
3426
3304
  `(sessionId, toolCallId)` and decided at `POST /v1/approvals/{sessionId}/decide`, while a live row is
3427
- keyed by `approvalId` and decided at `POST /v1/tool-approvals/{approvalId}/respond`. Sharing one word
3305
+ keyed by `approvalId` and decided at `POST /v1/tool-approvals/{id}/respond`. Sharing one word
3428
3306
  would eventually send a live card to the decide door — a 404 with no wire evidence of the mistake.
3429
3307
  ⚠️ Both live words come from an IN-PROCESS window projection, so their cross-replica reach is exactly
3430
3308
  that of `GET /v1/approvals`'s `livePending` — narrower than the durable pair above. The deployment
@@ -4393,6 +4271,38 @@ paths:
4393
4271
  '404': { description: no pending question for this id (non-owner, answered, expired, or wrong replica) — no existence oracle. }
4394
4272
  '501': { description: AskUserQuestion is not enabled on this worker (ASK_QUESTION_ENABLED). }
4395
4273
 
4274
+ /v1/tool-approvals/{id}:
4275
+ parameters:
4276
+ - $ref: '#/components/parameters/PrincipalHeader'
4277
+ - { name: id, in: path, required: true, schema: { type: string }, description: 'The ask''s `approvalId` (server >= 7.133.0 always equal to its `askId`; an opaque string).' }
4278
+ get:
4279
+ tags: [questions]
4280
+ operationId: toolApprovalRead
4281
+ x-status: live # server >= 7.133.0; SDK toolApprovals.get consumes it
4282
+ summary: Read ONE live-card ask — where is it now (pending, decided, parked or void)? Pure read.
4283
+ description: >
4284
+ server >= 7.133.0 — the by-ask read door. Same root and same gate as POST /v1/tool-approvals/{id}/respond (one `gatedPrincipal`; an owner-gate miss answers the SAME 404 as an unknown
4285
+ id — no existence oracle), and the state is read the way the answer door reads it: a live registration on THIS replica waiting for the ask => `pending` (the body is the `livePending[]` row projection +
4286
+ `state`); otherwise the row is read by id: `DECIDED` => `decided`, `PARKING` / `PARKED` => `parked`, `DENIED` / `VOID` => `void`. A row still `STREAM_PENDING` with no local registration lives on
4287
+ another replica => 404 `tool_approval.not_pending` + `cause: "unknown_or_other_replica"` (the same answer as a late respond); a deployment with no ask ledger answers the same 404 WITHOUT `cause`.
4288
+ Pure read: zero writes, no settlement, the local registration is untouched. Use it to ask "is this card still alive / was it decided by anyone" instead of inferring it from a respond 404.
4289
+ An older server (<= 7.132.x) has no such door: routing-level 404 `not_found.route` — tell it apart from this door's 404 by `errorCode`, never by status.
4290
+ responses:
4291
+ '200':
4292
+ description: The ask's current state.
4293
+ content:
4294
+ application/json:
4295
+ schema: { $ref: '#/components/schemas/ToolApprovalReadBody' }
4296
+ '401': { $ref: '#/components/responses/Unauthorized' }
4297
+ '404':
4298
+ description: >
4299
+ `tool_approval.not_pending` — unknown / not yours (byte-identical) / still pending on another replica (`cause: unknown_or_other_replica`). Past the owner gate the body may carry the additive `cause`
4300
+ (see ToolApprovalNotPendingCause); no ask ledger => no `cause`.
4301
+ content:
4302
+ application/json:
4303
+ schema: { $ref: '#/components/schemas/ToolApprovalNotPendingBody' }
4304
+ '501': { description: 'Live tool-approval HITL not enabled (TOOL_APPROVAL_ENABLED) — `feature.tool_approval_disabled`.' }
4305
+
4396
4306
  /v1/tool-approvals/{id}/respond:
4397
4307
  parameters:
4398
4308
  - $ref: '#/components/parameters/PrincipalHeader'
@@ -4426,7 +4336,7 @@ paths:
4426
4336
  verbatim 400 texts, the two capability bits to probe first and the `deny` posture are on
4427
4337
  the PersistRuleSelection schema. Allow-family only (on `deny` the whole field is ignored).
4428
4338
 
4429
- ⚠️ If the frame carried neither `ruleOffers` nor `ruleSuggestions`, do not send this key —
4339
+ ⚠️ If the frame carried no `ruleOffers`, do not send this key —
4430
4340
  there is nothing redeemable for THIS card and the answer is necessarily
4431
4341
  `rulePersisted:false` + `rule_lane_unavailable` (the missing material short-circuits BEFORE
4432
4342
  any candidate comparison, so it is never `rule_not_offered`). Absence has several causes —
@@ -5295,39 +5205,6 @@ paths:
5295
5205
  error: { type: string }
5296
5206
  missing: { type: array, items: { type: string } }
5297
5207
 
5298
- /v1/approvals/{approvalId}:
5299
- parameters:
5300
- - $ref: '#/components/parameters/PrincipalHeader'
5301
- - in: path
5302
- name: approvalId
5303
- required: true
5304
- schema: { type: string }
5305
- get:
5306
- tags: [approvals]
5307
- operationId: approvalsGet
5308
- x-status: retired # 5.0.0: legacy approval-store leg deleted — NO deployment serves this route.
5309
- summary: 'RETIRED (server 5.0.0) — by-id approval read; every deployment answers 404. Use GET /v1/approvals.'
5310
- description: >
5311
- 🔴 RETIRED as of `@sema-agent/server` 5.0.0 ([2354]/[2356] A-5): the legacy approval-store polling leg
5312
- that served this route was deleted whole, and the durable checkpoint lane never had a by-id read
5313
- (it can list by scope or look up by capability token — there is no simple-id query). A worker WITH a
5314
- checkpoint store answers 404 `not_found.route`; a worker WITHOUT one never matches the `/v1/approvals*`
5315
- prefix at all. Either way: 404 on every deployment, and that 404 is indistinguishable from "no such
5316
- approval", which is exactly why the SDK stopped calling it (4.0.1 — `approvals.get` now reads
5317
- `GET /v1/approvals` and filters by `sessionId`, one round trip instead of a guaranteed-404 probe plus
5318
- the same list call).
5319
- Kept in the spec as a tombstone rather than deleted: consumers generated against an older spec need to
5320
- see WHY the path went away. Decide via `POST /v1/approvals/{sessionId}/decide`; list via `GET /v1/approvals`.
5321
- Historical shape (below) is left intact for the same reason; it describes no live behavior.
5322
- responses:
5323
- '200':
5324
- description: The approval row (full args projection).
5325
- content:
5326
- application/json:
5327
- schema: { $ref: '#/components/schemas/ApprovalRow' }
5328
- '401': { $ref: '#/components/responses/Unauthorized' }
5329
- '404': { $ref: '#/components/responses/NotFound' }
5330
-
5331
5208
  /v1/memory/export:
5332
5209
  parameters:
5333
5210
  - $ref: '#/components/parameters/PrincipalHeader'
@@ -5799,6 +5676,70 @@ paths:
5799
5676
  '403': { description: 'Multi-tenant worker: outcomes view is operator-only.' }
5800
5677
  '501': { $ref: '#/components/responses/NotImplemented' }
5801
5678
 
5679
+ /v1/config/catalog:
5680
+ parameters:
5681
+ - $ref: '#/components/parameters/PrincipalHeader'
5682
+ get:
5683
+ tags: [admin]
5684
+ operationId: configCatalog
5685
+ x-status: live # operator lane (capabilities.configCatalog); SDK configCatalog.get consumes it
5686
+ summary: Deployment configuration catalog — every env knob's declaration and this process's effective reading (operator-only).
5687
+ description: >
5688
+ The deployment's configuration catalog (operator lane; probe `capabilities.configCatalog` first). One row per env knob: its declared semantics, which lanes can write it, which lane is in force, the
5689
+ process's effective DISPLAY value and the centre seat (`center`) when the knob is centre-publishable; plus the `retired` tombstone list (env names this process refuses to start on). `Cache-Control: no-store`.
5690
+ Gate: `OPERATOR_PRINCIPALS` — outside the list 403 `auth.operator_only`; empty list on a multi-tenant deployment 501 `capability.operator_principals_required`; `CONFIG_CATALOG_ENABLED=false` 501
5691
+ `feature.config_catalog_disabled`. A server older than 7.53 has no such route (404).
5692
+ `effectiveValue` is a DISPLAY value, never the raw env: `secret` / `opaque` rows report only `{ present, length? }`, a `url` row goes through the display mint (userinfo landed, query values become
5693
+ tokens). Two VERDICT bits on a row are true-only (never `false`; ABSENT = this cell has no such matter): `effectiveValueInvalid` (the effective value falls outside the row's own echo set; `effectiveValue` is then
5694
+ `null`) and, server >= 7.133.0, `effectiveValueWithheld` (a `url` row whose display mint WITHHELD a part — a credential-shaped piece in a query / path segment — so the shown value is not the whole value).
5695
+ responses:
5696
+ '200':
5697
+ description: The catalog.
5698
+ content:
5699
+ application/json:
5700
+ schema: { $ref: '#/components/schemas/ConfigCatalog' }
5701
+ '401': { $ref: '#/components/responses/Unauthorized' }
5702
+ '403': { description: 'The caller is not in OPERATOR_PRINCIPALS (`auth.operator_only`).' }
5703
+ '501': { $ref: '#/components/responses/NotImplemented' }
5704
+
5705
+ /v1/principal-inbox/messages:
5706
+ parameters:
5707
+ - $ref: '#/components/parameters/PrincipalHeader'
5708
+ post:
5709
+ tags: [admin]
5710
+ operationId: principalInboxSend
5711
+ x-status: live # server >= 7.133.0; SDK principalInbox.send consumes it
5712
+ summary: Deliver one letter to another principal's inbox in the same organization (cross-principal delivery; default OFF).
5713
+ description: >
5714
+ server >= 7.133.0 — one letter is appended to the recipient principal's `principal.inbox` ONLY by the organization delivery gate: membership judgement -> mint -> inbox bound -> quota -> append, all inside one gate that answers
5715
+ with a receipt or one of four refusal codes. The SENDER is always your verified identity (`from` is not a body key: sending `from` or any unknown key => 400 `request.body_shape` + `unknownKeys`). DEFAULT OFF: without env
5716
+ `PRINCIPAL_INBOX_ASSISTANT` the route answers 501 `capability.unavailable` (no delivery gate is mounted) — probe `capabilities.principalInboxDelivery` (true <=> this door works). THIS VERSION SHIPS THE DELIVERY HALF ONLY:
5717
+ a delivered letter sits in the recipient's box (at most 50 unconfirmed envelopes) and no read verb exists yet. Judging order: the sender's ATTEMPT bound (every attempt is recorded, succeeded or not, BEFORE membership) ->
5718
+ membership (including the mailbox-key judge) -> mint (a sender that cannot be spelled into the envelope's sender slot answers the same `not_member`, no quota consumed) -> inbox bound -> quota deduction -> append. A refused
5719
+ delivery appends nothing. Not auto-retried by the SDK (every attempt consumes the sender's attempt quota).
5720
+ The four refusal codes: `org_delivery.not_member` 404 (ONE sentence, byte-identical body for: different organization / no such person / directory could not answer / `to` is not a principal shape / the recipient cannot be a
5721
+ mailbox key / the sender cannot be spelled into the envelope — the gate is never an existence probe); `org_delivery.rate_limited` 429 (one sentence for three bounds: 60 attempts per sender per hour — a typo'd recipient costs an
5722
+ attempt —, 10 letters per (sender, recipient) pair per hour, 30 letters per recipient per hour; fixed clock-hour windows, up to double across a boundary; hard-coded, no knob); `org_delivery.inbox_full` 409 (the recipient's box already holds
5723
+ 50 unconfirmed envelopes — judged BEFORE quota so a full box burns none); `org_delivery.recipient_tombstoned` 410 (the recipient's mailbox is in its pre-delete state).
5724
+ requestBody:
5725
+ required: true
5726
+ content:
5727
+ application/json:
5728
+ schema: { $ref: '#/components/schemas/PrincipalInboxMessage' }
5729
+ responses:
5730
+ '200':
5731
+ description: The letter was appended; `receiptId` is the gate's delivery receipt id.
5732
+ content:
5733
+ application/json:
5734
+ schema: { $ref: '#/components/schemas/PrincipalInboxReceipt' }
5735
+ '400': { description: 'Malformed body: `request.field_invalid` (not an object / empty `to` / empty or over-long `content` / non-boolean `urgent`) or `request.body_shape` (unknown key incl. `from`; body carries `unknownKeys`).' }
5736
+ '401': { $ref: '#/components/responses/Unauthorized' }
5737
+ '404': { description: '`org_delivery.not_member` — one byte-identical body for six causes (see the description); the gate is not an existence probe.' }
5738
+ '409': { description: '`org_delivery.inbox_full` — the recipient''s box holds 50 unconfirmed envelopes.' }
5739
+ '410': { description: '`org_delivery.recipient_tombstoned` — the recipient''s mailbox is in its pre-delete state.' }
5740
+ '429': { description: '`org_delivery.rate_limited` — one sentence for the sender-attempt bound, the pair bound and the recipient aggregate bound.' }
5741
+ '501': { description: 'No delivery gate mounted (`PRINCIPAL_INBOX_ASSISTANT` off) — `capability.unavailable`.' }
5742
+
5802
5743
  /v1/diagnostics/wiring:
5803
5744
  parameters:
5804
5745
  - $ref: '#/components/parameters/PrincipalHeader'
@@ -7064,6 +7005,7 @@ components:
7064
7005
  promptSummary: { type: string, description: 'a human-readable prompt DIGEST — NOT the full prompt.' }
7065
7006
  enabled: { type: boolean }
7066
7007
  available: { type: boolean, description: 'whether this deployment can actually RUN the scenario right now — same predicate as the 501 rejection arm on submit; false ⟺ submitting is guaranteed to 501. Independent axis from `enabled` (declared vs runnable).' }
7008
+ attachmentSupplies: { type: array, items: { type: string, enum: [path, content_text, content_image] }, description: 'server >= 7.133.0, always present there: the attachment SUPPLY words this scenario actually carries — a subset of `capabilities.userAttachments.supplies` (`path` only on hands scenarios, where a workspace lives somewhere; a no-hands scenario drops `path`, and a `path` entry is refused by the engine with `config.user_attachment_invalid`). ABSENT = server <= 7.132.x: fall back to `userAttachments.supplies` plus the existing 400 handling.' }
7067
7009
  unavailableReason: { type: string, description: 'machine-readable reason, present ONLY when available:false (absent = no reason). Closed set, currently the single member `git_client_unconfigured`; branch on the key, never match the English prose. Additive-open: treat an unknown word as "unavailable, reason unknown".' }
7068
7010
 
7069
7011
  RunStatus:
@@ -7704,11 +7646,29 @@ components:
7704
7646
  `permissions.defaultMode`'s specific fold is documented on `TaskRequest.permissionMode`.
7705
7647
  🔴 ON THIS SUBMIT FACE THE KEY SET IS CLOSED (server >= 7.57.0 for the sub-tree): accepted top-level keys
7706
7648
  are `env` / `hooks` / `model` / `outputStyle` / `permissions` / `skillListingBudgetFraction` /
7707
- `skillListingMaxDescChars` / `ultracode` / `webSearch`, and accepted `permissions.*` keys are
7708
- `allow` / `ask` / `defaultMode` / `deny` / `disableAutoMode`; anything else is 400 `request.body_shape`
7649
+ `skillListingMaxDescChars` / `ultracode` / `webSearch` (server >= 7.133.0 adds the three TIGHTEN-ONLY seats `complianceAdditionalDenies` / `readDenyPatterns` / `writeProtectedExtra`; server >= 7.134.0 adds `approvalFrameFamilies`), and accepted `permissions.*` keys are
7650
+ `allow` / `ask` / `defaultMode` / `deny` / `disableAutoMode` (server >= 7.133.0 adds `neverAuto`); anything else is 400 `request.body_shape`
7709
7651
  with the names in the refusal's `unknownKeys` / `unsupportedKeys` arrays (REFUSED, not dropped — a dropped
7710
7652
  key reads as "in effect" to the caller). The object stays OPEN here because the same `SemaSettings`
7711
7653
  schema also serves the local `settings.json` wiring, where deferred CC keys are legal.
7654
+ 🔴 TIGHTEN-ONLY SEATS (server >= 7.133.0; probe `capabilities.taskSettings.tightenOnlySeats`, an absent bit = an older server that answers 400 `request.body_shape` for these four keys): `readDenyPatterns` (entries: a bare
7655
+ pattern string, or `{ pattern, caseSensitive? }`), `writeProtectedExtra` (a bare name — no `/` reads as `segment`, a `/` reads as `segment-run` — or `{ name, kind: basename | segment | segment-run }`),
7656
+ `complianceAdditionalDenies` (`mcp_servers` | `workflows` | `web_fetch` | `org_memory_mount`) and `permissions.neverAuto` (tool names that ALWAYS ask a human). Each seat = the deployment table UNION the request list, ONLY EVER GROWING
7657
+ (the request face has no loosening arm); an empty array is the same as not sent. Bad value => 400 `request.field_invalid` with `field` + `reason` (`over_cap` + `count` + `cap` / `bad_entry` + the first bad `index`).
7658
+ Caps: 1000 entries per seat, EXCEPT `readDenyPatterns`: at most 32 entries, at most 2 `*` per path segment and 16 `*` in the whole table (every star counts, nothing is folded). A `neverAuto` name that can never match any tool => 422.
7659
+ A resume replay of a stored body whose seat went bad => 409 `resume_blocked_by_policy`. A compliance deny is a REFUSAL, not a narrowing: a request denying `web_fetch` is refused whole where the default tool surface still mounts `WebFetch` (the single-user default does).
7660
+ 🔴 `settings.approvalFrameFamilies` (server >= 7.134.0; send it on sight of `capabilities.taskSettings.approvalFrameFamilies === true` — NOT of the top-level
7661
+ `capabilities.approvalFrameFamilies`, which <= 7.133.x already has while answering 400 `request.body_shape` for this key): the approval opening-frame FAMILIES this
7662
+ run's client RENDERS — an array of `tool_approval` / `approval_request`, each once (`[]` reads as absent). Bad value (not an array / a word outside the set / a
7663
+ repeat) => 400 `request.field_invalid` with `field: "settings.approvalFrameFamilies"`, `reason: "bad_entry"` and `index` (absent for a non-array). BEHAVIOR (BREAKING, wire):
7664
+ a run whose declaration does not contain the family this worker presents (`capabilities.approvalFrameFamilies`) has NO live approval channel — no card of either
7665
+ family (no fallback, no double send), no replay in the stream preamble of the sync stream or of `GET /v1/runs/{taskId}/events`; every ask goes to the unattended
7666
+ policy at once: with a durable store (`capabilities.approvals`) it parks per the operator policy (default; answer through `POST /v1/approvals/{sessionId}/decide`)
7667
+ or is refused naming the missing family; with NO durable store the gated tool is refused on the spot and the run finishes (no card, no decide). The declaration
7668
+ rides the persisted body onto resume legs.
7669
+ 🔴 `settings.hooks[]` (server >= 7.133.0; probe `capabilities.taskSettings.pluginHooks` / `managedHookGroups` — an absent or false bit = the keys are STRIPPED SILENTLY): group key `source` (closed: `managedPolicy` / `user` / `project` / `local` / `flag` /
7670
+ `managedPlugin:<name>` / `plugin:<name>`; absent = unmanaged; managed groups run in an independent lane after the unmanaged rewrites), command-entry keys `plugin { name, root, dataDir?, options? }` and exec-form `args`; a malformed one =>
7671
+ 400 `request.field_invalid` with `detail: { reason: "wire_key_invalid", key, path }`; the plugin context has one 8192-character budget.
7712
7672
  🔴 `skillListingBudgetFraction` (number in (0,1]) / `skillListingMaxDescChars` (positive integer),
7713
7673
  server >= 7.78.1: the SKILL-LISTING RENDER BUDGET, passed to the engine with ZERO processing
7714
7674
  (no clamping, no conversion, no folding) — ABSENT means the key is not written at all (the server does
@@ -8374,6 +8334,10 @@ components:
8374
8334
  cause (`terminal`), not the eight parallel plane keys — see `TerminalCause`.
8375
8335
  required: [taskId, sessionId, terminal, stats]
8376
8336
  properties:
8337
+ warnings:
8338
+ type: array
8339
+ items: { $ref: '#/components/schemas/EntryProblem' }
8340
+ description: 'server >= 7.133.0, top level of the sync `POST /v1/tasks` 200 body (beside `notice`), present only when non-empty: the accept-time NOTICES for allow-side selector-list entries the engine reads as nothing or that are not in the roster. ABSENT = no notice (older servers too).'
8377
8341
  taskId: { type: string }
8378
8342
  sessionId: { type: string }
8379
8343
  wiringManifest:
@@ -8676,6 +8640,10 @@ components:
8676
8640
  taskId: { type: string }
8677
8641
  sessionId: { type: string }
8678
8642
  status: { $ref: '#/components/schemas/RunStatus' }
8643
+ warnings:
8644
+ type: array
8645
+ items: { $ref: '#/components/schemas/EntryProblem' }
8646
+ description: 'server >= 7.133.0, present only when non-empty: the accept-time NOTICES for allow-side selector-list entries the engine reads as nothing or that are not in the roster (acceptance is unchanged — the engine takes them, only the operator hears afterwards; this makes "this name will not take effect" visible at the call). Same list as the stream `meta` frame''s and the sync 200 body''s. ABSENT = no notice (older servers too).'
8679
8647
 
8680
8648
  SealedKeyCapability:
8681
8649
  type: object
@@ -9545,6 +9513,14 @@ components:
9545
9513
  redispatched: { type: array, items: { type: string } }
9546
9514
  collapsed: { type: string }
9547
9515
 
9516
+ ApprovalFrameFamily:
9517
+ type: string
9518
+ enum: [tool_approval, approval_request]
9519
+ description: >-
9520
+ The approval opening-frame FAMILY words (closed; the server's `APPROVAL_FRAME_FAMILIES` verbatim and in order): `tool_approval` = the legacy family
9521
+ (live card `tool_approval` / close `tool_approval_complete`); `approval_request` = the card family (`approval_request` / `approval_complete` /
9522
+ batch revoke `approval_revoke`). ONE vocabulary for two faces: the capability `approvalFrameFamilies` (which family this worker presents) and, server >= 7.134.0,
9523
+ the request key `settings.approvalFrameFamilies` (which families this run's client renders).
9548
9524
  ApprovalDecision:
9549
9525
  type: object
9550
9526
  description: HITL decision (`POST /v1/approvals/:sessionId/decide`).
@@ -9612,30 +9588,17 @@ components:
9612
9588
  refused by the ENGINE before the CAS (409 `resume_outcome_invalid` + `field: "hostDecision"`, the card stays
9613
9589
  pending); outside the closed set ⇒ 400 `request.field_invalid`; any value on a direct-door worker ⇒ 400
9614
9590
  `decided_by_not_in_proof`. An older server (no `approvalDecidedBy` bit) refuses the key (400 `request.body_shape`).
9615
-
9616
- ApprovalRow:
9617
- type: object
9618
- description: >
9619
- The legacy D-lane approval row of GET /v1/approvals/{approvalId} (server `ApprovalRow`,
9620
- plugins/approval-store-sql.ts — the row is `sendJson`'d verbatim, approvals-assistant.ts).
9621
- Present only on deployments running the legacy `approvalStore` leg (no `checkpointStore` wired);
9622
- the durable F4 lane has no by-id GET (only list + decide).
9623
- # 🔴 census 批2 五段:CLOSED — byte-identical to the server row type, no extra keys.
9624
- required: [id, taskId, sessionId, owner, scope, toolName, args, status, reason, decidedBy, createdAt, decidedAt]
9625
- additionalProperties: false
9626
- properties:
9627
- id: { type: string }
9628
- taskId: { type: [string, 'null'] }
9629
- sessionId: { type: [string, 'null'] }
9630
- owner: { type: [string, 'null'] }
9631
- scope: { type: [string, 'null'], description: 'Single-DB fleet scope guard (tenant identity, owner-sourced).' }
9632
- toolName: { type: string }
9633
- args: { description: 'The reviewed tool-call args — full fidelity, not redacted (this IS the review surface).' }
9634
- status: { type: string, enum: [pending, approved, denied, expired] }
9635
- reason: { type: [string, 'null'] }
9636
- decidedBy: { type: [string, 'null'] }
9637
- createdAt: { type: string }
9638
- decidedAt: { type: [string, 'null'] }
9591
+ feedback:
9592
+ type: string
9593
+ description: >-
9594
+ server >= 7.134.0 (core 7.54.0) — what the approving PERSON writes in the card's "and tell the agent what to do next" input: on the resumed
9595
+ leg it reaches the model as the user's own words (unfenced, unbounded — like the reference's accepted-prompt feedback). ONLY on a person's approve
9596
+ (`decision: "approve"`, `decidedBy` absent or `person`): on a deny or a `decidedBy: "policy"` decision ⇒ 400 `request.field_conflict` with
9597
+ `detail.kind: "approval_feedback"` and `detail.fields` `["feedback", "decision"]` / `["feedback", "decidedBy"]` (a denial's words go in `reason`);
9598
+ a non-string ⇒ 400 `request.field_invalid`; any value on a direct-door worker ⇒ 400 `feedback_not_in_proof` (not inside the signed proof); words
9599
+ carrying `<system-reminder>` ⇒ 409 `approval_binding_mismatch` (the engine's sentence; the card stays pending — ask the person to change the words,
9600
+ do not auto-refetch and re-decide on the code). The same door serves parked background-agent cards and workflow child cards; the live
9601
+ `POST /v1/tool-approvals/{id}/respond` does NOT take it. Servers <= 7.133.x refuse the key (400 `request.body_shape`, `unknownKeys: ["feedback"]`).
9639
9602
 
9640
9603
  Capabilities:
9641
9604
  type: object
@@ -9803,6 +9766,11 @@ components:
9803
9766
  store backend deliberately wires no image index (the pool is cloud/fleet-only) ⇒ false there.
9804
9767
  SCOPE: P1 only — `/v1/images/bakes*` (the P2 bake control plane) is a separate dependency and an
9805
9768
  operator/runner-internal face, NOT promised by this bit.
9769
+ principalInboxDelivery:
9770
+ type: boolean
9771
+ description: >-
9772
+ server >= 7.133.0 — the cross-principal delivery door `POST /v1/principal-inbox/messages` is wired (delivery gate present: env `PRINCIPAL_INBOX_ASSISTANT` on and the gate's dependencies in place).
9773
+ True iff that route works; absent gate => the route answers 501 `capability.unavailable`. ABSENT key = server <= 7.132.x (no such route; routing-level 404). Probe this bit; do not trial-by-501.
9806
9774
  sharedMemory:
9807
9775
  type: boolean
9808
9776
  description: >
@@ -9910,25 +9878,12 @@ components:
9910
9878
  leaseClient: { type: boolean, description: 'server >= 7.121.0, a version bit (always true, rides with the `fleet` object): this binary carries the serving-lease CLIENT — when the engine is a fleet member (`FLEET_GENERATION`) and the center declares in its heartbeat reply that it answers renewals (`servingLease`), the engine renews and is governed by the lease. Deliberately NOT named `servingLease` (that is the center''s bit): this says the client exists, not that a lease is in force — read `/health.leaseState` for that. Absent = an older worker.' }
9911
9879
  generationEcho: { type: boolean, description: 'server >= 7.121.0, a version bit (always true, rides with the `fleet` object): this binary echoes env `FLEET_GENERATION` as `generation` on the heartbeat and on `/health` (with `leaseState` / `leaseUntil`), and the drain endpoint `POST /v1/admin/drain` compares the four-part identity including `expectedGeneration`. Absent = an older worker.' }
9912
9880
  workspace:
9881
+ type: boolean
9882
+ enum: [false]
9913
9883
  description: >
9914
- #3 (server 1.299–7.51): workspace-browse capability family — an OBJECT when the E19 snapshot-store
9915
- read faces + a durable session store are wired ({browse, archive, maxFileBytes}); `false`/absent
9916
- otherwise (same predicate as the routes' 501). `maxFileBytes` = the single-file read cap (over-cap
9917
- ⇒ 413 workspace_file_too_large envelope; the archive endpoint is the escape hatch).
9918
- 🔴 RETIRED (server ≥7.52, S-15/design/381): the whole-tree snapshot epoch this face projected was
9919
- replaced by per-edited-file rewind history, so this key is now ALWAYS false and the four
9920
- /v1/sessions/{sessionId}/workspace* routes answer 501 `capability.workspace_retired` (route shape
9921
- kept as a tombstone — a named refusal, not a 404 that reads like a typo). Consumption posture
9922
- unchanged: probe this bit, false ⇒ don't render the panel.
9923
- oneOf:
9924
- - type: boolean
9925
- enum: [false]
9926
- - type: object
9927
- additionalProperties: true
9928
- properties:
9929
- browse: { type: boolean }
9930
- archive: { type: boolean }
9931
- maxFileBytes: { type: integer }
9884
+ The workspace-browse bit — ALWAYS `false` on every supported server (>= 7.52.0: the whole-tree snapshot epoch was replaced by
9885
+ per-edited-file rewind history; the four /v1/sessions/{sessionId}/workspace* routes answer 501 `capability.workspace_retired`, route shape kept as a
9886
+ tombstone). sdk 26.0.0 deleted the object arm `{ browse, archive, maxFileBytes }` (servers 1.299–7.51, below the support floor).
9932
9887
  sessionEvents:
9933
9888
  type: boolean
9934
9889
  description: >
@@ -9973,6 +9928,12 @@ components:
9973
9928
  lane) or 422 (hosted).
9974
9929
  mcp: { type: boolean, description: "MCP servers can be attached." }
9975
9930
  mcpInjection: { type: boolean, description: "Per-request MCP injection is accepted." }
9931
+ mcpAssertionHeaderSent:
9932
+ type: boolean
9933
+ description: >-
9934
+ server >= 7.133.0 — the SEND seat self-report (a boolean, today ALWAYS `false`): whether `mcpServers[].transport.assertionHeader` (accepted since server 7.128.0, mutually exclusive with `principalHeader`)
9935
+ is actually SENT to the MCP peer. `false` = the declaration is carried but NOT sent (the same switch drives the key stripping, the operator info line and the probe-door refusal; the disclosure sentences leave when it turns true).
9936
+ ABSENT = an older server, which does not send either — treat it as `false`; never rely on the header for peer authentication.
9976
9937
  mcpProbe:
9977
9938
  type: boolean
9978
9939
  description: >
@@ -10012,6 +9973,20 @@ components:
10012
9973
  ⚠️ ABSENT = an older server (<7.87.1) ⇒ cannot tell whether the push face carries the live family;
10013
9974
  do NOT read "no `live_pending` frame arrived yet" as "this deployment never pushes live" — the two
10014
9975
  are indistinguishable on an older server. Fall back to `GET /v1/approvals`'s `livePending` pull face.
9976
+ approvalFrameFamilies:
9977
+ type: array
9978
+ maxItems: 1
9979
+ items: { $ref: '#/components/schemas/ApprovalFrameFamily' }
9980
+ description: >-
9981
+ server >= 7.133.0, ALWAYS present (the projection of the same judgement as `streamApproval`) — which FRAME FAMILY this worker's approval opening frames belong to:
9982
+ a closed-word array with AT MOST ONE member. `["approval_request"]` = the card protocol is on: one ask travels only `approval_request` -> `approval_complete` / `approval_revoke`
9983
+ (the legacy `tool_approval` / `tool_approval_complete` pair is NOT sent — BREAKING vs <= 7.132.x, which sent both families for one ask and relied on `approvalId` de-duplication;
9984
+ remedy on the worker: `STREAM_APPROVAL_ENABLED=false`). `["tool_approval"]` = the card protocol is not on (`STREAM_APPROVAL_ENABLED=false`, no store backend, or no checkpoint store) but the live card leg is.
9985
+ `[]` = per-call approvals are off altogether (`TOOL_APPROVAL_ENABLED=false`): no card is presented. Dispatch frame families off THIS array, never off `streamApproval`.
9986
+ KEY ABSENT (not `[]`) = server <= 7.132.x — there a `streamApproval: true` leg sends BOTH families for one ask.
9987
+ 🔴 server >= 7.134.0: this bit says which family THIS WORKER presents, no longer that you will receive cards — a run has a live approval channel only when
9988
+ its submit body declared that family in `settings.approvalFrameFamilies` (send it on sight of `taskSettings.approvalFrameFamilies`, not of this bit — <= 7.133.x
9989
+ already had this bit yet answers 400 `request.body_shape` for the key). The value does not change with a declaration. The words are the request key's words.
10015
9990
  streamApproval:
10016
9991
  type: boolean
10017
9992
  description: >
@@ -10066,6 +10041,13 @@ components:
10066
10041
  a server <= 7.118.x returns the archive as written; a 7.119.x server already redacts but does not report the bit. A true
10067
10042
  bit does not mean the read face exists (that is `sessions`; without a session audit face that door answers 501 and this
10068
10043
  bit is still true), nor that the archive holds no credential (the redaction is read-side only).
10044
+ approvalReadRootGrant:
10045
+ type: boolean
10046
+ description: >-
10047
+ server >= 7.133.0, ALWAYS present, a FACT that varies by deployment — whether this deployment accepts the reply-body key `readRootGrant` ("Yes, and add directory": the card's
10048
+ `readRootCandidate.dir` is registered as a read root together with this one allow). True iff the single-user host-lane posture (the same predicate as `projectContext` / the cwd face); false elsewhere.
10049
+ A client shows the option only when the card carries `readRootCandidate` AND this bit is true. On a non-accepting posture a reply carrying `readRootGrant` is refused
10050
+ `400 request.body_shape` (the ask stays unsettled — answer again without the key). ABSENT = server <= 7.132.x = not accepted; do not send the key.
10069
10051
  approvalReach:
10070
10052
  type: boolean
10071
10053
  description: >-
@@ -10178,6 +10160,10 @@ components:
10178
10160
  outputStyle: { type: boolean }
10179
10161
  env: { type: boolean, description: 'TRUE on the single-user host lane (server emits `cwdHonored(config)`). It is NOT hard-coded false any more — the old always-false value predated the per-task shellEnv seam and hid a live capability.' }
10180
10162
  hooks: { type: boolean, description: 'Single-user deployments only (`requirePrincipal !== true`).' }
10163
+ pluginHooks: { type: boolean, description: 'server >= 7.133.0 — the request `settings.hooks[]` PLUGIN-CONTEXT keys (command entry `plugin { name, root, dataDir?, options? }` / exec-form `args`) are accepted; same predicate as `hooks` (single-user true, multi-tenant false). ABSENT or false = the keys are stripped silently — send only on sight of the bit.' }
10164
+ managedHookGroups: { type: boolean, description: 'server >= 7.133.0 — the request `settings.hooks[]` GROUP `source` key (managed lanes `managedPolicy` / `managedPlugin:<name>`) is accepted; same predicate as `hooks`. ABSENT or false = stripped silently (the managed-lane semantics do not apply, it is not a refusal).' }
10165
+ tightenOnlySeats: { type: boolean, description: 'server >= 7.133.0, constant true — the four tighten-only request seats `settings.readDenyPatterns` / `settings.writeProtectedExtra` / `settings.complianceAdditionalDenies` / `settings.permissions.neverAuto` are accepted (each seat = deployment table UNION request list, only ever growing; an empty array is the same as not sent). ABSENT = server <= 7.132.x, which answers 400 `request.body_shape` for those keys — send only on sight of the bit.' }
10166
+ approvalFrameFamilies: { type: boolean, description: 'server >= 7.134.0, constant true — the request key `settings.approvalFrameFamilies` (which approval opening-frame families this run''s client renders) is accepted. THIS is the trigger bit for that key, NOT the top-level `approvalFrameFamilies`: servers <= 7.133.x already carry the top-level bit yet answer 400 `request.body_shape` (`unknownKeys: ["settings.approvalFrameFamilies"]`) for the key. ABSENT = do not send it.' }
10181
10167
  settings:
10182
10168
  type: object
10183
10169
  description: >
@@ -10206,6 +10192,7 @@ components:
10206
10192
  properties:
10207
10193
  allowGrant: { type: boolean, description: 'Constant `true` on server >= 7.115.0: `settings.permissions.allow` is read as allow RULES, not as a list. The whole `settings` key being absent = an older server.' }
10208
10194
  allowLayer: { type: boolean, description: 'Constant `true` on server >= 7.117.0 (core 7.42.0): the whole `settings.permissions.allow` list goes to the engine allow layer (listed calls are not asked; content forms accepted; entries read as nothing get an `engine_notice` `permission.allowed_tool_not_granted`). Absent = a server without it (7.115.0–7.116.x: allow is a statement; older: a list).' }
10195
+ callerTrust: { type: string, description: 'server >= 7.133.0 — THIS caller''s trust tier (per caller / per credential, NOT a version bit). Request rule tables that take effect without a person answering (`settings.permissions` buckets; a named-invoked skill''s `allowedTools` / `disallowedTools`) are admitted by it: `allow` loosens the no-ask surface and needs `global`; `deny` / `ask` only tighten and are admitted at any tier. Today `global` (single-user turnkey; multi-tenant with the deployment service credential; multi-tenant sign-in token whose scope role >= the governance write role) or `project` (everything else). Read OPEN: an unknown word is treated as `project` (fail-closed). A request below the floor is refused whole with 400 `request.field_invalid` (`detail.problems[].reason: below_trust_floor`, `detail.callerTrust`). ABSENT = server <= 7.132.x: no per-tier admission.' }
10209
10196
  memoryWrite:
10210
10197
  type: boolean
10211
10198
  description: >
@@ -10268,7 +10255,7 @@ components:
10268
10255
  additionalProperties: true
10269
10256
  properties:
10270
10257
  enabled: { type: boolean, enum: [true] }
10271
- protocolVersion: { type: integer }
10258
+ protocolVersion: { type: integer, description: 'The protocol REVISION this server speaks (the `challenge` frame carries the same value; accepted window [1, 2] on server >= 7.133.0). Server >= 7.133.0 reads 2 (<= 7.132.x read 1). An executor declares the SMALLER of its own highest revision and this value, and takes its outcome-code set from the declared revision (revision 2 added `liveness_no_output`).' }
10272
10259
  maxInflightPerDevice: { type: integer }
10273
10260
  wsPath: { type: string }
10274
10261
  management: { type: boolean, description: 'server >= 7.88.0. `/v1/devices/*` admin verbs present. PRESENT-IFF — absent means a pre-7.88.0 peer (unknown), not false.' }
@@ -10762,9 +10749,9 @@ components:
10762
10749
  server >= 7.65.0 (S-167) — the READ containment rung of this deployment AND WHO SET IT (operator face
10763
10750
  `GET /v1/diagnostics/wiring` -> `readFace`). `face` is the same boot artefact as the tenant-facing
10764
10751
  `capabilities.readFace` (`null` = nothing pinned by this deployment, the engine default is in force).
10765
- `source` is a CLOSED four-word set on the server (exhaustive switch — adding a word is a compile error
10752
+ `source` is a CLOSED five-word set on the server (exhaustive switch — adding a word is a compile error
10766
10753
  there): `env` = this machine's READ_FACE env var (wins over everything) · `center` = the config-center
10767
- readFace domain (restart-to-apply) · `posture` = derived from the single-user turnkey posture
10754
+ readFace domain (restart-to-apply) · `lane` = (server >= 7.133.0) the isolated execution lane's default — with `READ_FACE` absent, e2b / k8s / local-docker mint `open` · `posture` = derived from the single-user turnkey posture
10768
10755
  (REQUIRE_PRINCIPAL unset ⇒ `open`; server >= 7.65.0 behaviour change) · `engine-default` = not pinned.
10769
10756
  `note` is a human-facing pointer for operators. Minimal disclosure: the deny table is never on this face.
10770
10757
  required: [face, source, note]
@@ -10774,7 +10761,7 @@ components:
10774
10761
  enum: [open, roots, null]
10775
10762
  source:
10776
10763
  type: string
10777
- enum: [env, center, posture, engine-default]
10764
+ enum: [env, center, lane, posture, engine-default]
10778
10765
  note:
10779
10766
  type: string
10780
10767
 
@@ -10791,9 +10778,9 @@ components:
10791
10778
  `DURABLE_APPROVAL` ON and a 24h approval window when neither is set, so a default FLIP has to be visible
10792
10779
  on an operator surface or nobody sees it.
10793
10780
 
10794
- `source` is the CLOSED four-word set (exhaustive switch on the server — adding a word is a compile error
10781
+ `source` is the CLOSED five-word set (exhaustive switch on the server — adding a word is a compile error
10795
10782
  there), in PRECEDENCE order: `env` = pinned by this machine's env var (deployment sovereignty; beats any
10796
- published value) · `center` = applied from the config-center (restart-to-apply) · `posture` = derived from
10783
+ published value) · `center` = applied from the config-center (restart-to-apply) · `lane` = (server >= 7.133.0) derived from the isolated execution lane (e2b / k8s / local-docker mint a lane default, e.g. `READ_FACE` -> `open`) · `posture` = derived from
10797
10784
  the deployment shape (single-user turnkey) · `engine-default` = nothing pinned, the built-in default is in
10798
10785
  force. Only `env`/`center` count as "an operator asked for this"; a posture-derived `true` does not.
10799
10786
  `note` is PROSE for a human (per-knob, and on the `posture` arm it is that knob's own fact sentence) —
@@ -10808,7 +10795,7 @@ components:
10808
10795
  description: 'The knob''s effective value. Typed by the referencing site (boolean / integer / …).'
10809
10796
  source:
10810
10797
  type: string
10811
- enum: [env, center, posture, engine-default]
10798
+ enum: [env, center, lane, posture, engine-default]
10812
10799
  note: { type: string }
10813
10800
 
10814
10801
  SkillSpec:
@@ -11630,6 +11617,24 @@ components:
11630
11617
  `"other"`). This is the "where did those 180 404s actually land" cell. Empty object = zero failures;
11631
11618
  key absent = an older server.
11632
11619
 
11620
+ ModelPlane:
11621
+ type: object
11622
+ additionalProperties: false
11623
+ description: >
11624
+ `GET /v1/models.modelPlane` (server >= 7.133.0; always present; conditional keys are absent, never `null` / empty string) — the single read for "which model table is this engine serving".
11625
+ Two axes. SERVED TABLE: `servedTable` = `env` (the env table minted from MODEL_ID / MODEL_CHEAP_ID — no catalog is in charge: none configured / source unreachable with no last-known-good / the candidate was refused /
11626
+ no enabled entry) | `catalog` (a catalog's model plane landed and replaced the whole env table). CANDIDATE OUTCOME: `catalog` = `refused` (the latest candidate was refused whole — by the candidate judge or the model-plane
11627
+ group; with no previous generation the env table serves, with one the catalog table still serves — who serves is `servedTable`'s answer, NOT merged into this field) | `adopted` (no whole refusal and the catalog table
11628
+ serves; NOT "the latest candidate landed" — that question belongs to the /health generation ledger) | `absent` (no whole refusal and the env table serves). START-UP ROUTE VERDICT: `refusalCode` / `gap` (both ABSENT when the
11629
+ judge passes — the normal state of a running process): `refusalCode` = `config.model_route_endpoint_missing` | `config.gateway_fallback_without_primary`; `gap` = `endpoint_missing` (set MODEL_GATEWAY_BASEURL or the entry's
11630
+ own `baseUrl`) | `lane_credential_missing` (an anthropic-lane choice with neither ANTHROPIC_API_KEY nor ANTHROPIC_AUTH_TOKEN fell into the endpoint-less openai leg). No sentence is carried. Words are server-owned — read OPEN
11631
+ (an unknown word is "unknown", never "passed").
11632
+ required: [servedTable, catalog]
11633
+ properties:
11634
+ servedTable: { type: string, description: '`env` | `catalog` (open on read).' }
11635
+ catalog: { type: string, description: '`absent` | `adopted` | `refused` (open on read).' }
11636
+ refusalCode: { type: string, description: '`config.model_route_endpoint_missing` | `config.gateway_fallback_without_primary` (open on read); ABSENT = the judge passed.' }
11637
+ gap: { type: string, description: '`endpoint_missing` | `lane_credential_missing` (open on read); ABSENT = the judge passed.' }
11633
11638
  ModelsResponse:
11634
11639
  type: object
11635
11640
  description: >
@@ -11677,6 +11682,11 @@ components:
11677
11682
  Upgrade window (as it is): an on-disk LKG written by server <= 7.122.x only recorded the dropped entries that had a name AND were
11678
11683
  enabled; while serving from it with the center still unreachable this key lists only those rows (possibly `[]`) and the first pull after the
11679
11684
  center is reachable fills it in. An unknown `reasonCode` word reads as "dropped, reason unknown".
11685
+ modelPlane:
11686
+ allOf: [{ $ref: '#/components/schemas/ModelPlane' }]
11687
+ description: >-
11688
+ server >= 7.133.0, ALWAYS present there (appended last): which model table this engine SERVES, whether the catalog was adopted, and what the start-up route judge says — read this instead of predicting
11689
+ (the judge's inputs are the serving process's facts; mirroring two of its judgements drifts). ABSENT = an earlier server (<= 7.132.x). Field reading: ModelPlane.
11680
11690
  candidateRejected:
11681
11691
  allOf: [{ $ref: '#/components/schemas/CandidateRejected' }]
11682
11692
  description: >
@@ -11761,7 +11771,8 @@ components:
11761
11771
  # 没声明 ⇒ 键缺席 ⇒ 不再是必有键(此前由 `input` 恒算成布尔,对没声明的条目报 false)。
11762
11772
  # 🔴 server 7.131.0 起 `reasoning` 同样三态:env 车道 `MODEL_REASONING` / `MODEL_VISION` 缺省 true 退役、中心名册条目不写即不声明
11763
11773
  # ⇒ 两键都「未声明 = 键缺席」,`required` 只剩 name / id / provider(sdk 23.1.0;TS 型 `ModelInfo.reasoning?` 早已可选)。
11764
- required: [name, id, provider]
11774
+ # sdk 26.0.0(支持底线 7.52.0):`routePairing` 进 required(≥7.45 恒铸,此前因地板 3.0.0 不进)。
11775
+ required: [name, id, provider, routePairing]
11765
11776
  additionalProperties: false
11766
11777
  properties:
11767
11778
  name: { type: string, description: 'the @mention handle (user-pickable model name).' }
@@ -11785,8 +11796,8 @@ components:
11785
11796
  adjudicated" must stay distinguishable on the wire). Declared here 2026-09-10 alongside `compat`:
11786
11797
  this schema is CLOSED, and the key has been minted unconditionally since 7.45 — so every real
11787
11798
  /v1/models response has been structurally rejected by a strict validator for the whole time (same
11788
- class as `hasBidiControls` on InboxRow). Optional here because the SDK's supported server floor is
11789
- 3.0.0; the discriminant is the PEER'S VERSION, not this key's presence.
11799
+ class as `hasBidiControls` on InboxRow). REQUIRED since sdk 26.0.0 (the support floor 7.52.0 is above 7.45; it
11800
+ was optional only while the floor was 3.0.0).
11790
11801
  compat:
11791
11802
  $ref: '#/components/schemas/ModelCompat'
11792
11803
  description: >-
@@ -12596,19 +12607,6 @@ components:
12596
12607
  startedAt: { type: integer }
12597
12608
  elapsedMs: { type: integer }
12598
12609
  tokens: { type: integer }
12599
- # tokenDir REMOVED (engine ruling 2026-06-28): dead field, 0 producer / 0 consumer — spec caught up 2026-07-26.
12600
- queuedCount:
12601
- type: integer
12602
- description: >-
12603
- 🔴 PHANTOM KEY — NO server version ever emits it (verified against the published 7.87.0 bytes:
12604
- the row type declares it and the fleet route's per-key table forwards it, but NO producer writes it
12605
- — not `publishTask`'s call sites, not core's bg-sink tick, not the durable reconcile leg — and the
12606
- wire contract does not mention it either). The server deleted it as a DEAD KEY (S-459, zero
12607
- behavioural impact since `!== undefined` was always false). It stays declared here only because
12608
- removing an exported field is BREAKING = major-only; it is registered with its deletion condition
12609
- in `test/_keyset-divergence.ts` (`FLEET_TASK_ROW_PHANTOM_KEYS`) and goes with the next major.
12610
- DO NOT read it as "queue depth is already reported" — a permanently absent optional key is a FALSE
12611
- LEAD. Real queue depth needs a NEW key WITH a producer, not this one resurrected.
12612
12610
  awaitingPlanApproval: { type: boolean }
12613
12611
  currentAction: { type: string, description: "The child's most recent tool intent as one human-readable line (UNTRUSTED argument head, redacted server-side)." }
12614
12612
  currentTool:
@@ -12722,6 +12720,13 @@ components:
12722
12720
  failedCount: { type: integer }
12723
12721
  elapsedMs: { type: integer }
12724
12722
  tokens: { type: integer }
12723
+ errorCode:
12724
+ type: string
12725
+ description: >-
12726
+ server >= 7.134.0 — the workflow run's MACHINE code (open set; the same words as the same-named slot on `GET /v1/workflows/{id}`): carried verbatim when the
12727
+ run row has one, the key ABSENT otherwise (never an empty string). A parked workflow run reads `status: "failed"` + `errorCode: "workflow.agent_parked"`
12728
+ (parked, not failed; the `status` vocabulary gains no word). Snapshot rows, `workflow` upsert frames and rows merged from the terminal window share one mint
12729
+ point; a `?session=` subscriber's demoted rows carry it too. ABSENT = an older server, or no code on this row — fall back to `status`.
12725
12730
  FleetFrame:
12726
12731
  description: >
12727
12732
  One decoded frame off GET /v1/fleet/stream (service streamFleet emit set, src/http/routes/fleet.ts). On the
@@ -12898,14 +12903,14 @@ components:
12898
12903
  ts: { type: integer }
12899
12904
  FleetFrame_hook_notice:
12900
12905
  description: >
12901
- Hook observability frame, TWO kinds (both pure observe — never block, resume, or inject anything;
12906
+ Hook observability frame, FOUR kinds (server >= 7.133.0 added the third and fourth; all pure observe — never block, resume, or inject anything;
12902
12907
  visibility fail-CLOSED like bg_notification: the owner keys ownerScope/ownerSessionId gate delivery
12903
12908
  and are STRIPPED before send; wire is FLAT — `send("hook_notice", { type, ...wire, ts })`, the
12904
12909
  server-internal `{notice}` nesting never reaches it). codex R1-F4: modeled as the discriminated union
12905
12910
  it really is — the common `{type, ts}` frame base composed with each kind's payload arm — so a
12906
12911
  generated client gets the SAME per-kind narrowing as the TS type (a second-family frame missing
12907
- hookName/entryType is invalid, not silently accepted). `kind` is CLOSED two-member on the server but
12908
- OPEN ON READ: degrade an unknown future kind to a generic observability row.
12912
+ hookName/entryType is invalid, not silently accepted). `kind` is CLOSED four-member on the server but
12913
+ OPEN ON READ: degrade an unknown future kind to a generic observability row. DISPATCH on `kind`; never infer a call's outcome from any hook_notice frame (read the tool / approval frames).
12909
12914
  oneOf:
12910
12915
  - allOf:
12911
12916
  - $ref: '#/components/schemas/FleetHookNoticeFrameBase'
@@ -12913,10 +12918,16 @@ components:
12913
12918
  - allOf:
12914
12919
  - $ref: '#/components/schemas/FleetHookNoticeFrameBase'
12915
12920
  - $ref: '#/components/schemas/FleetHookNonBlockingFailure'
12921
+ - allOf:
12922
+ - $ref: '#/components/schemas/FleetHookNoticeFrameBase'
12923
+ - $ref: '#/components/schemas/FleetHookManagedUnrun'
12924
+ - allOf:
12925
+ - $ref: '#/components/schemas/FleetHookNoticeFrameBase'
12926
+ - $ref: '#/components/schemas/FleetHookVerdictUntrusted'
12916
12927
 
12917
12928
  FleetHookNoticeFrameBase:
12918
12929
  type: object
12919
- description: 'The hook_notice frame envelope shared by both kinds: the SSE frame discriminant and the server timestamp.'
12930
+ description: 'The hook_notice frame envelope shared by all four kinds: the SSE frame discriminant and the server timestamp.'
12920
12931
  required: [type, ts]
12921
12932
  properties:
12922
12933
  type: { const: hook_notice }
@@ -12929,7 +12940,8 @@ components:
12929
12940
  DISTINCT vocabulary from AgentEvent (the durable log is BLOCK-grained, each frame is a delta to append).
12930
12941
  Resumable (each `id:` = the durable task_event.seq; on 416 the server returns `{retainedFrom}` and the SDK
12931
12942
  resumes from there after a turns full-sync). Known events: meta / block-thinking-delta / block-content-delta
12932
- / tool-call / tool-result / turn / done / error (+ heartbeat keep-alives the SDK swallows). Open set.
12943
+ / tool-call / tool-result / prompt-assembled / cross-boundary-taint (server >= 7.134.0: the device lane's taint row + `seq`, see
12944
+ `Event_cross_boundary_taint`) / turn / done / error (+ heartbeat keep-alives the SDK swallows). Open set.
12933
12945
  NOTE: SSE framing on the wire (event name + JSON data + optional id), not a JSON body.
12934
12946
  required: [event, data]
12935
12947
  additionalProperties: true
@@ -12946,10 +12958,11 @@ components:
12946
12958
  description: >
12947
12959
  契约调和车1 G(2026-08-02;server [2373]C-12, `src/trace/project.ts` + the meta frame at
12948
12960
  `routes/trace-usage.ts`): the frame's own event name, DOUBLE-EMITTED inside `data`. Always
12949
- present on server >= 5.0.0; OPTIONAL here because the SDK's supported floor is server 3.0.0
12950
- (where only the `error` frame carried it). Equal to the SSE `event:` line verbatim — its reason
12951
- to exist is the proxy/relay case that strips the `event:` line and forwards only the data JSON,
12952
- where this is the only key left to dispatch on. Heartbeats (`data: {}`) carry none.
12961
+ present on every named frame of a supported server (>= 7.52.0; minted since server 5.0.0) — the SDK's
12962
+ typed arms declare it REQUIRED since 26.0.0; it is not in this schema's `required` only because the one
12963
+ schema also covers heartbeats (`data: {}`, which carry none and which the SDK swallows). Equal to the SSE
12964
+ `event:` line verbatim — its reason to exist is the proxy/relay case that strips the `event:` line and
12965
+ forwards only the data JSON, where this is the only key left to dispatch on.
12953
12966
  output:
12954
12967
  allOf: [{ $ref: '#/components/schemas/ToolOutput' }]
12955
12968
  description: >
@@ -12965,7 +12978,7 @@ components:
12965
12978
  # #315(server ≥7.43):第二顶层键 `livePending` = streamApproval 窗内的流内 ask(此前对轮询消费端
12966
12979
  # 结构性不可见)。ADDITIVE / tolerate-absent:键缺席=「协调器未装配(本部署无 live 审批面)」或
12967
12980
  # 旧 server;空数组=「有面、此刻无待批」——两义可判别,消费端测 presence。**分数组不混编**:
12968
- # live 行的决议口=POST /v1/tool-approvals/{approvalId}/respond,durable 行=decide 口;数组名即路由判据。
12981
+ # live 行的决议口=POST /v1/tool-approvals/{id}/respond,durable 行=decide 口;数组名即路由判据。
12969
12982
  # 22.0.0:第三、四顶层键此前漏声明 —— 封闭 schema 上一个真键 = 合法信封被严格校验器拒收。
12970
12983
  additionalProperties: false
12971
12984
  required: [pending]
@@ -13020,7 +13033,7 @@ components:
13020
13033
  type: object
13021
13034
  description: >-
13022
13035
  A live (in-stream) pending ask inside its streamApproval window (#315, server ≥7.43). Decide it via
13023
- POST /v1/tool-approvals/{approvalId}/respond — NOT the /decide route (that is the durable rows' door).
13036
+ POST /v1/tool-approvals/{id}/respond — NOT the /decide route (that is the durable rows' door).
13024
13037
  Optional flags are only-if-true (absent is NEVER encoded as false).
13025
13038
  🔴 THE "tool input/args are NOT listed" NARROWING IS GONE (server >= 7.87.0 / S-454 P1). The old reason
13026
13039
  was "details ride the stream frame" — but a SUSPENDED ask never has a stream frame (at mint time it had
@@ -13038,7 +13051,15 @@ components:
13038
13051
  additionalProperties: true
13039
13052
  required: [approvalId, toolName, ts, expiresAtMs]
13040
13053
  properties:
13041
- approvalId: { type: string, description: 'The SAME id the respond endpoint takes (wire uuidv7).' }
13054
+ approvalId: { type: string, description: 'The SAME id the respond endpoint takes. server >= 7.133.0: for an ask that has a ledger row it is ALWAYS the row''s key `askId` (a 64-character lowercase hexadecimal string — an OPAQUE string, never a uuid); on <= 7.132.x a wire uuidv7.' }
13055
+ consumerState:
13056
+ type: string
13057
+ enum: [absent]
13058
+ description: >-
13059
+ server >= 7.133.0, TRUE-ONLY (present as `absent`): no engine leg on THIS replica is waiting for this ask. Present <=> the row was ADOPTED from the ask ledger at start-up (minted by a previous
13060
+ process / another replica, the row still undecided — a "zombie card"): it lists and can be answered (the row becomes decided; the answer ack carries the same bit), but that decision resumes
13061
+ nothing running on this replica — read the run surface to learn whether anything continued. The `live_pending` stream frame and the `pending` arm of GET /v1/tool-approvals/{id} carry it too. After a restart an
13062
+ ask still undecided on the ledger RETURNS to `livePending[]` (it used to vanish and answer 404). Absent = this replica has an engine leg waiting, or an older server.
13042
13063
  toolName: { type: string }
13043
13064
  ts: { type: number, description: 'Registration time (ms epoch).' }
13044
13065
  expiresAtMs: { type: number, description: 'Absolute window deadline — the SAME number minted once for the tool_approval frame (never recomputed).' }
@@ -13063,9 +13084,13 @@ components:
13063
13084
  allOf: [{ $ref: '#/components/schemas/ToolApprovalCardWord' }]
13064
13085
  description: 'server >= 7.126.0 (core 7.49.0 #1414): which card this ask renders on — the engine-minted card word, the SAME value as `frame.approvalCard` (the row reads the frame''s own seat, no second read), same layer and shape as the operator queue row''s `approvalCard`. It REPLACES `contentKind` (server <= 7.125.x derived it from the tool name; deleted at 7.126.0, no alias). ABSENT = the engine minted none. Display / triage only; it NEVER takes part in decision routing.'
13065
13086
  frame:
13066
- allOf: [{ $ref: '#/components/schemas/ToolApprovalFrame' }]
13087
+ oneOf:
13088
+ - $ref: '#/components/schemas/ToolApprovalFrame'
13089
+ - $ref: '#/components/schemas/ApprovalRequestFrame'
13067
13090
  description: >-
13068
- server >= 7.87.0 (S-454 P1; cli L-397) — THE CARD FRAME THIS ASK WOULD HAVE EMITTED. It is the SAME
13091
+ server >= 7.87.0 (S-454 P1; cli L-397) — THE CARD FRAME THIS ASK WOULD HAVE EMITTED. TWO SHAPES, by leg (sdk 25.0.0 declares the union; discriminant
13092
+ `frame.type`): on a leg that speaks the card protocol (`capabilities.approvalFrameFamilies` contains `approval_request`, server >= 7.133.0) it is the
13093
+ `approval_request` frame (card facts under `frame.card`, the ten off-card live facts flat at the top); on a legacy-family leg it is the `tool_approval` frame. It is the SAME
13069
13094
  OBJECT as the server's `PendingApproval.frame` (the one construction site in `askBroadcast`, already
13070
13095
  redacted / byte-capped / window-keys filled), shared by the suspended and the reachable path — so
13071
13096
  this is NOT a second card projection computed for the list, and there is no second redaction surface.
@@ -13285,6 +13310,7 @@ components:
13285
13310
  required: [sessionId, scope, createdAt]
13286
13311
  additionalProperties: true
13287
13312
  properties:
13313
+ checkpointId: { type: string, minLength: 1, maxLength: 128, description: 'server >= 7.133.0: the NON-SECRET stable identity of the pause this row parks on — the join key where the token cannot go; the SAME value as `PausedCause.checkpointId`. Carried on three durable read surfaces (GET /v1/approvals `pending[]` rows, the /v1/approvals/stream `pending` frame, GET /v1/assistant/inbox rows). A row whose value is unreadable or untrusted (non-string / empty / over 128 / contains the token / an old row / an older server) is STILL LISTED with the key ABSENT — read absence as "this row has no joinable identity", never as a required key.' }
13288
13314
  sessionId: { type: string, description: 'The decide handle: POST /v1/approvals/:sessionId/decide.' }
13289
13315
  scope: { type: string, description: 'Principal; "_" = submitted without one.' }
13290
13316
  taskId: { type: ['string', 'null'], description: 'Suspended run holding the session claim (service-pinned) — join key to trace tool-call blocks + context link.' }
@@ -13339,26 +13365,13 @@ components:
13339
13365
  is RETIRED: the SQL backend rewrote its parked rows once at the 7.117.0 boot to `[{ member: "persisted", entries: [<old value>] }]`, but the
13340
13366
  file backend's pre-upgrade parked rows and run results already on disk (`checkpointGate.riskDescriptor`) keep the old key verbatim
13341
13367
  (text still redacted, no structured disclosure) — a consumer reading those must still tolerate the old string.
13342
- # server 7.16.0 [3684]② — durable 队列行归因两键(与活卡 tool_approval 帧的同名键语义同族、判据不同源;
13343
- # 两条腿对同一只 ask 的在场性可以不一致,两侧都只认在场、都禁把缺席读成否定)。
13344
- ruleSuggestions:
13345
- type: array
13346
- minItems: 1
13347
- items: { $ref: '#/components/schemas/RuleSuggestion' }
13348
- description: >
13349
- server 7.16.0 ([3684]②), ADDITIVE: the rule candidates minted for this ask, replayed off the durable
13350
- row (checkpoint.rule_suggestions). DISPLAY-ONLY on this leg — the durable queue has no persistRule
13351
- redemption; the only redemption path is the LIVE card leg (respond + persistRule). Array order =
13352
- display order; an EXACT entry, when present, is always first. core contract caps candidates at 2;
13353
- the server enforces a tolerance cap of 4 on read-back — lay out for 4. Absent = pre-column row
13354
- (parked before the column existed) OR no candidates; render both as "no candidates", never an error.
13355
13368
  ruleOffers:
13356
13369
  type: array
13357
13370
  minItems: 1
13358
13371
  items: { $ref: '#/components/schemas/RuleOffer' }
13359
13372
  description: >
13360
13373
  server >= 7.46.0 (core 5.58.0, design/375) — the discriminated-union successor of the retired
13361
- `ruleSuggestions` above on the DURABLE queue row (core `PendingAction.ruleOffers`, the same
13374
+ `ruleSuggestions` (servers <= 7.45; sdk 26.0.0 deleted it) on the DURABLE queue row (core `PendingAction.ruleOffers`, the same
13362
13375
  contract as the sync leg's `AskRequest.ruleOffers`). A >= 7.46.0 row carries only the new key;
13363
13376
  pre-5.58 rows fail the new narrow read entry by entry and land as "no supply" (no compatibility
13364
13377
  read — a stale schema means re-ask).
@@ -13496,6 +13509,7 @@ components:
13496
13509
  additionalProperties: false
13497
13510
  properties:
13498
13511
  sessionId: { type: string, description: 'The suspended task/session id.' }
13512
+ checkpointId: { type: string, minLength: 1, maxLength: 128, description: 'server >= 7.133.0: the NON-SECRET stable identity of the pause this row parks on — the join key where the token cannot go; the SAME value as `PausedCause.checkpointId`. Carried on three durable read surfaces (GET /v1/approvals `pending[]` rows, the /v1/approvals/stream `pending` frame, GET /v1/assistant/inbox rows). A row whose value is unreadable or untrusted (non-string / empty / over 128 / contains the token / an old row / an older server) is STILL LISTED with the key ABSENT — read absence as "this row has no joinable identity", never as a required key.' }
13499
13513
  scope: { type: string, description: 'Multi-tenant owner scope.' }
13500
13514
  createdAt: { type: number, description: 'Ask creation time (epoch ms).' }
13501
13515
  gateKind:
@@ -13582,25 +13596,20 @@ components:
13582
13596
  one-off question. The value comes WHOLE from core (`summarizeCheckpoint` echoes it only when the
13583
13597
  cause is a member of the engine's set), so the server neither recomputes nor re-screens it. See
13584
13598
  ClassifierUnavailable for the open-`cause` rule and the three shapes absence covers.
13585
- RETIRED: server >= 7.72.0 (core 7.14.0 #688) never mints this key on any of the three faces; its
13599
+ RETIRED: server >= 7.72.0 (core 7.14.0) never mints this key on any of the three faces; its
13586
13600
  successor is `disposition.cause` on the gate record (`unavailable` / `parse_error` — a different
13587
- question, so an old switch does not port). Kept for servers in [7.69.0, 7.72.0) only (sdk 9.2.0).
13601
+ question, so an old switch does not port). Kept for servers in [7.69.0, 7.72.0) only (sdk 9.2.0); the row key retires
13602
+ together with the frame key (floor ≥ 7.106.0, next major); why the frame still declares it: `ToolApprovalFrame.classifierUnavailable`.
13588
13603
  ruleStoreUnreadable:
13589
13604
  type: string
13590
13605
  enum: [store, call]
13591
13606
  description: >-
13592
- server >= 7.72.0 (core 7.14.0 #688 C3), ADDITIVE — WHICH HALF of the rule lane could not be read.
13593
- Present iff `origin === "rule_store_unavailable"`: `store` = the wired rule store could not be read
13594
- (look at the store / network); `call` = the store was read but THIS command could not be read against
13595
- the person's deny/ask rows (look at the command spelling). One `origin` word covers both facts, and
13596
- their recovery verbs live in different places (ops vs prompt). Carried on the live `tool_approval`
13597
- frame and the inbox row; the `card_json` card deliberately does NOT carry it (it travels with `origin`,
13598
- which is not on the card). Absence = `origin` is not that word — NOT an assertion that the rule store
13599
- is healthy.
13600
- RETIRED: server >= 7.106.0 (core 7.34.0 C-R172, BREAKING — wire) never mints this key on either face — an
13601
- unreadable rule store no longer raises a card: that read's rows are SKIPPED by partition and the session
13602
- stream gets the user-facing `engine_notice` `permission.rule_store_unavailable` (`detail.at` = `prepare` /
13603
- `call`). Kept for servers in [7.72.0, 7.106.0) only (sdk K-32); `origin: rule_store_unavailable` itself stays.
13607
+ server >= 7.72.0 (core 7.14.0 #688 C3), ADDITIVE — WHICH HALF of the rule lane could not be read (`store` / `call`).
13608
+ The same key, with the same two words and the same reading, as on the live frame; the authoritative statement of the shape, the presence
13609
+ rule, what the two words mean and the retirement wording is on `ToolApprovalFrame.ruleStoreUnreadable`, and this description points to it.
13610
+ RETIRED: server >= 7.106.0 never mints this key on either face (the frame's description names the successor). Kept for
13611
+ servers in [7.72.0, 7.106.0) only (sdk K-32); the row key retires together with the frame key (floor ≥ 7.106.0, next major);
13612
+ `origin: rule_store_unavailable` itself stays.
13604
13613
 
13605
13614
  InboxList:
13606
13615
  type: object
@@ -13819,6 +13828,17 @@ components:
13819
13828
  ⚠️ The key name is final: `permissionModeAfter` (the build brief once said `nextPermissionMode`,
13820
13829
  which never shipped — zero aliases).
13821
13830
 
13831
+ SessionAgent:
13832
+ type: object
13833
+ additionalProperties: false
13834
+ description: >-
13835
+ The placement-tuple projection of a delegated subagent's transcript (`SessionRecord.agent`, server >= 7.133.0). Each member is independently optional (only what was stored is projected); `{}` is a legal value
13836
+ (the tuple held only a `kind`). `id` = the tuple's `handle` (the durable agent row's `a…` key — the SAME string as the fleet row's `id` and `GET /v1/agents/roster`'s `handle`); `parentSessionId` = the host
13837
+ session that spawned it; `rootSessionId` = the delegation tree's root host session (a nested grandchild anchors to the root).
13838
+ properties:
13839
+ id: { type: string }
13840
+ parentSessionId: { type: string }
13841
+ rootSessionId: { type: string }
13822
13842
  SessionRecord:
13823
13843
  type: object
13824
13844
  description: >
@@ -13851,6 +13871,14 @@ components:
13851
13871
  $ref: '#/components/schemas/SessionWindow'
13852
13872
  promptEpoch:
13853
13873
  $ref: '#/components/schemas/PromptEpoch'
13874
+ agent:
13875
+ allOf: [{ $ref: '#/components/schemas/SessionAgent' }]
13876
+ description: >-
13877
+ server >= 7.133.0, top-level additive key — WHICH delegated subagent's transcript this session is (read by terminal side-chain projections that join a fleet frame's `transcriptId`). Only the stored placement
13878
+ tuple is projected (minted by the engine when a BACKGROUND subagent with a durable agent row creates the session on a store that declares placements; a synchronously delegated subagent's transcript is not in
13879
+ the host store and has no tuple); the three members are each optional. 🔴 READ IT AS "WAS IT STORED", NEVER AS "IS IT A SUBAGENT": key PRESENT = a delegated subagent's transcript (even `{}` when the tuple held only a
13880
+ kind); key ABSENT has three causes — an older server (<= 7.132.x) / an ordinary session / a deployment whose store keeps no placement tuple (the local backend: the copy parked from the private short-TTL store is an
13881
+ ordinary session row). Absence is NOT "not a subagent": fall back to the identity the fleet row (`id`) / `GET /v1/agents/roster` (`handle`) already carry.
13854
13882
  userInputUndrained:
13855
13883
  type: array
13856
13884
  description: >-
@@ -14476,26 +14504,6 @@ components:
14476
14504
  parentId: { type: [string, 'null'] }
14477
14505
  type: { type: string, description: Entry discriminator (message / leaf / compaction / …) — opaque to the SDK. }
14478
14506
 
14479
- SyncSnapshot:
14480
- type: object
14481
- description: >
14482
- One E19 file snapshot keyed by a `SessionTreeEntry.id`. `manifest` = `[relPath, blobHash]` tuples; the blob
14483
- BYTES are NOT inlined (they ride the content-addressed blob routes).
14484
- # 封闭(census 批2 第三段,2026-07-30):server 侧类型+铸造点(`session-sync.ts` PULL 出 /
14485
- # `routes/session-sync.ts` PUSH 校验形)都恰是这两键。
14486
- additionalProperties: false
14487
- required: [key, manifest]
14488
- properties:
14489
- key: { type: string }
14490
- manifest:
14491
- type: array
14492
- items:
14493
- type: array
14494
- minItems: 2
14495
- maxItems: 2
14496
- items: { type: string }
14497
- description: A `[relPath, blobHash]` tuple.
14498
-
14499
14507
  SyncAnchor:
14500
14508
  type: object
14501
14509
  description: One E18 resume-at anchor. `owner` is RE-KEYED to the importing principal on a PUSH (§9).
@@ -14523,31 +14531,28 @@ components:
14523
14531
  description: >
14524
14532
  2c session-sync `GET …/sync/manifest` payload (wrapped server-side as `{ manifest }`). The THIN cross-backend
14525
14533
  snapshot: entry IDS (oldest-first, NOT payloads) + the file-history envelope + policy + anchors + leaf.
14526
- 🔴 server >= 7.52 (S-15, design/381; re-read byte for byte against 7.116.0): the key `snapshots` (per-snapshot
14527
- relPath→blobHash, the retired E19 whole-tree snapshot epoch) is NO LONGER SENT — the server's `exportSessionManifest` mints
14528
- `{ sessionId, entryIds, entryCount, leafId, fileHistory, policy, anchors }` (server >= 7.125.0 adds the session row's two writable bits `title` / `pinned`,
14529
- present-iff the session is a row of the list face — see the two properties) and `fileHistory` (core's versioned
14530
- `FileHistoryExport` envelope; `null` = no history / the source store cannot export) replaces it. <= 7.51 servers still send
14531
- `snapshots` and no `fileHistory`. The blob BYTES never ride the manifest: content-addressed, via the history blob faces
14532
- (`GET …/sync/history/blobs/{hash}` / `PUT …/sync/blobs/{hash}`; probe `capabilities.sessionSyncFileHistory` on BOTH ends).
14533
- The SDK's `manifest()` fills `snapshots: []` when the wire omits it.
14534
+ The server's `exportSessionManifest` mints `{ sessionId, entryIds, entryCount, leafId, fileHistory, policy, anchors }` (server >= 7.125.0 adds the
14535
+ session row's two writable bits `title` / `pinned`, present-iff the session is a row of the list face — see the two properties). `fileHistory`
14536
+ (core's versioned `FileHistoryExport` envelope; `null` = no history / the source store cannot export) is ALWAYS present on a supported server
14537
+ (>= 7.52.0); the retired E19 whole-tree snapshot key `snapshots` of servers <= 7.51 is below the support floor (sdk 26.0.0 deleted it). The blob BYTES
14538
+ never ride the manifest: content-addressed, via the history blob faces (`GET …/sync/history/blobs/{hash}` / `PUT …/sync/blobs/{hash}`; probe
14539
+ `capabilities.sessionSyncFileHistory` on BOTH ends). The SDK returns the wire verbatim.
14534
14540
  # 封闭(census 批2 第三段,2026-07-30;16.2.0 按 7.116.0 发布字节重核):铸造点 `session-sync.ts`(exportSessionManifest 尾部
14535
14541
  # 字面量)恰是 `{ sessionId, entryIds, entryCount, leafId, fileHistory, policy, anchors }` 7 键(≥7.52),无 `snapshots`;
14536
14542
  # server ≥7.125.0 另有 `title` / `pinned` 两键(会话行可写位,present-iff 会话是列表面上的行;条件展开,不进 required);
14537
- # ≤7.51 老 server 发 `snapshots`、无 `fileHistory` ⇒ 两键都不进 required(支持底线 3.0.0)。
14543
+ # sdk 26.0.0(支持底线 7.52.0):`fileHistory` 进 required(支持面内恒铸),`snapshots` 删(≤7.51 才发)。
14538
14544
  additionalProperties: false
14539
- required: [sessionId, entryIds, entryCount, leafId, policy, anchors]
14545
+ required: [sessionId, entryIds, entryCount, leafId, fileHistory, policy, anchors]
14540
14546
  properties:
14541
14547
  sessionId: { type: string }
14542
14548
  entryIds: { type: array, items: { type: string }, description: Oldest-first, verbatim cross-backend. }
14543
14549
  entryCount: { type: integer, description: '= entryIds.length (the `/sync/entries` trailer count).' }
14544
14550
  leafId: { type: [string, 'null'] }
14545
- snapshots: { type: array, items: { $ref: '#/components/schemas/SyncSnapshot' }, description: 'server <= 7.51 ONLY (the retired E19 snapshot epoch); server >= 7.52 does not send it — see `fileHistory`.' }
14546
14551
  fileHistory:
14547
14552
  oneOf:
14548
14553
  - { $ref: '#/components/schemas/FileHistoryExport' }
14549
14554
  - { type: 'null' }
14550
- description: 'server >= 7.52: the per-edited-file rewind history envelope; `null` = no history / the source store cannot export; key ABSENT = a <= 7.51 server (that generation sends `snapshots`).'
14555
+ description: 'the per-edited-file rewind history envelope; `null` = no history / the source store cannot export. Always present on a supported server.'
14551
14556
  policy: { type: array, items: { $ref: '#/components/schemas/SessionRulesRecord' } }
14552
14557
  anchors: { type: array, items: { $ref: '#/components/schemas/SyncAnchor' } }
14553
14558
  title:
@@ -14610,20 +14615,18 @@ components:
14610
14615
  SessionBundle:
14611
14616
  type: object
14612
14617
  description: >
14613
- The portable state of ONE session, ready to replay into another backend. Blobs are NOT inlined (each
14614
- snapshot carries only its `manifest`; the bytes stream through the content-addressed blob routes). The SDK
14615
- assembles this from a manifest + the streamed entries for a PUSH. server >= 7.52: the file history rides `fileHistory`
14616
- (the `FileHistoryExport` envelope) instead of `snapshots`.
14617
- required: [sessionId, entries, snapshots, policy, anchors]
14618
+ The portable state of ONE session, ready to replay into another backend. Blobs are NOT inlined (the file-history
14619
+ envelope carries only blob hashes; the bytes stream through the content-addressed blob routes). The SDK
14620
+ assembles this from a manifest + the streamed entries for a PUSH.
14621
+ required: [sessionId, entries, fileHistory, policy, anchors]
14618
14622
  properties:
14619
14623
  sessionId: { type: string }
14620
14624
  entries: { type: array, items: { $ref: '#/components/schemas/SyncEntry' } }
14621
- snapshots: { type: array, items: { $ref: '#/components/schemas/SyncSnapshot' } }
14622
14625
  fileHistory:
14623
14626
  oneOf:
14624
14627
  - { $ref: '#/components/schemas/FileHistoryExport' }
14625
14628
  - { type: 'null' }
14626
- description: 'server >= 7.52 file-history envelope (same as `SessionManifest.fileHistory`); key absent = the source is a <= 7.51 server.'
14629
+ description: 'the file-history envelope (same as `SessionManifest.fileHistory`; `null` = none, or the pull did not carry it because the source has no blob faces).'
14627
14630
  policy: { type: array, items: { $ref: '#/components/schemas/SessionRulesRecord' } }
14628
14631
  anchors: { type: array, items: { $ref: '#/components/schemas/SyncAnchor' } }
14629
14632
 
@@ -14811,8 +14814,9 @@ components:
14811
14814
  <= 7.105.0 answered the synchronous leg 200 + a failed body), `corrupt` (500 — the session rule store
14812
14815
  cannot read its own bytes), `internal.memory_control_plane_corrupt` (500 — the memory control plane refuses
14813
14816
  fail-closed) and `state.scenario_assignment_unknown` (503, server >= 7.119.0 — the center's scenario assignment for the
14814
- principal is dangling; only re-pointing the assignment, or adding the scenario back to the deployment, helps). Every other 5xx
14815
- code keeps the status-based retry.
14817
+ principal is dangling; only re-pointing the assignment, or adding the scenario back to the deployment, helps) and
14818
+ `approvals.read_corrupt` (500, server >= 7.134.0 on `GET /v1/approvals` — a queue row's stored body cannot be read; an operator has to repair or
14819
+ delete the row). Every other 5xx code keeps the status-based retry.
14816
14820
  🔴 `request.host_posture_refused` (422) has TWO triggers (same code, same status — "you wrote it right, this
14817
14821
  deployment will not grant that host posture"): (1) the write-containment face (`permissionMode: bypassPermissions` ⇒
14818
14822
  `writeFace: open`; read `capabilities.taskWriteFaceOpen` first); (2) server >= 7.106.0 / S-895: `inheritEnv: "all"` on a
@@ -14885,8 +14889,8 @@ components:
14885
14889
  center 404 / 429 / 3xx included). On 400 `request.field_invalid` for a `settings.permissions` bucket (server >= 7.106.0 /
14886
14890
  S-904): `over_cap` (more than `cap` entries; beside `count` + `cap`) / `bad_entry` (a non-string or empty item; beside
14887
14891
  `index`); server >= 7.132.0 widens that group to every tool-name list on the body, the agent definitions and the two host-path lists, and adds
14888
- `too_long` (a text over its character cap; beside `count` + `cap`) — closed three words `over_cap` / `too_long` / `bad_entry` (SDK `RequestFieldRefusalExtra`).
14889
- On 409 refusals of a resume (server >= 7.132.0): the engine's `CheckpointErrorReason` arm (`detail.reason` passed through; see `blockedBy`). On 400 `rules.inert_entry` (server >= 7.129.0): the engine's `ALLOW_ENTRY_INERT_REASONS` (`tool_name` / `wildcard_all` / `namespace_wildcard` / `content_unsupported` / `grammar` / `near_miss` / `earlier_grammar`). Out-of-vocabulary word ⇒ treat as unknown. (Other codes carry their own `reason` words.)
14892
+ `too_long` (a text over its character cap; beside `count` + `cap`) — closed words `over_cap` / `too_long` / `bad_entry` (SDK `RequestFieldRefusalExtra`); server >= 7.133.0 adds `too_large` (a payload over its BYTE cap, only on 413 `request.payload_too_large`: `{ field: "body", reason: "too_large", cap }` — `count` ABSENT when the read stopped at the cap and cannot measure; a request body over 8 MiB is now always that 413) and the `request.invalid_json` reasons `not_json` | `not_object` (400, every JSON-body door; formerly `request.rejected`/`request.body_shape` by door).
14893
+ On 409 refusals of a resume (server >= 7.132.0) and on 422 `parked_resume.startup_failed` (server >= 7.134.0 — the parked background agent's pre-claim failure; absent when the engine gave none): the engine's `CheckpointErrorReason` arm (`detail.reason` passed through; see `blockedBy`). On 409 `conflict.checkpoint_ctx_unsealable` (server >= 7.134.0): why the request's credentials cannot be sealed into, or unsealed from, the stored resume context — a closed five-word set (SDK `CHECKPOINT_CTX_UNSEALABLE_REASONS`); which doors can answer which word is written once, in the SDK documentation of `CHECKPOINT_CTX_UNSEALABLE_REASONS`, not here: `no_key_store` (this server has no sealed-key store) / `key_not_held` (sealed with a key this server does not hold — another replica's, or a replaced sealed-key directory) / `unopenable` (the sealed value is damaged or belongs to another session) / `plaintext` (an older server stored it unsealed) / `unrecognized_seal` (a seal format this version does not know) — the way out depends on the door and the `reason`: `key_not_held` (a resume door) — have the operator restore the sealed-key directory that sealed it and restart that replica (it reads its keys only at startup), then resend the same request; `no_key_store` on the submit door — give the service a writable sealed-key directory (`<data root>/sealed-key/`, read at startup), or resubmit WITHOUT those credentials — resending the same submit body is refused again; `no_key_store` on a resume door — the same way out as `key_not_held`: restore the directory that sealed it and restart, then resend (a fresh empty directory does not help: it holds no such key, so the next read says `key_not_held`); `unopenable` / `plaintext` / `unrecognized_seal` — submit the work as a new task (cancel the still-suspended one first); never auto-retry, never match the sentence. A resume door's refusal decides nothing and starts no resume (the card stays pending; the known exception is a terminal run's steer race arm, which first opens a wake card and, when the second read fails for any of the five reasons, withdraws it and then answers 409 — if the withdrawal fails the server only logs it); a submit door's refusal lands before the task runs (nothing runs; a run row already opened for it is closed as `failed` and its idempotency key released, both best-effort — a failed step is only logged by the server and the answer is still the 409). On 400 `rules.inert_entry` (server >= 7.129.0): the engine's `ALLOW_ENTRY_INERT_REASONS` (`tool_name` / `wildcard_all` / `namespace_wildcard` / `content_unsupported` / `grammar` / `near_miss` / `earlier_grammar`). Out-of-vocabulary word ⇒ treat as unknown. (Other codes carry their own `reason` words.)
14890
14894
  retryAfterSec:
14891
14895
  type: number
14892
14896
  description: >-
@@ -14913,6 +14917,10 @@ components:
14913
14917
  `null` and `""` count), the refusal landed before any decision, and `field` is the literal `settledBy`; the three doors now answer
14914
14918
  the SAME sentence. Every such 400 carries it, even when another defect in the same body was hit first (the sentence then names that
14915
14919
  defect). Branch on the code plus `field`, never on the sentence; a bad body WITHOUT that key carries no `field`. SDK: `err.extra.field`.
14920
+ server >= 7.134.0: on 409 `conflict.checkpoint_ctx_unsealable` `field` is the SUBMIT-BODY PATH of the credential slot that cannot be sealed or unsealed (arrays
14921
+ `[i]`, record members `.key`: `mcpServers[0].transport.headers.Authorization`, `settings.hooks.PreToolUse[0].hooks[0].headers.Authorization`; the key
14922
+ name is not a secret, the value never enters the refusal); on 400 `request.field_invalid` for the request key `settings.approvalFrameFamilies` it is
14923
+ that literal (with `reason: "bad_entry"` and the bad `index`; no `index` for a non-array).
14916
14924
  detail:
14917
14925
  description: >
14918
14926
  🔴 SHAPES differ by code — branch on `field` (or the `errorCode`), never guess from the type of `detail`. On 503
@@ -15006,6 +15014,14 @@ components:
15006
15014
  properties:
15007
15015
  reason: { type: string, enum: [unnamed_refusal] }
15008
15016
  path: { $ref: '#/components/schemas/SettingsHooksConfigPath' }
15017
+ - type: object
15018
+ description: 'one of the three wire keys this service reads itself is malformed (server >= 7.133.0): the group `source` / a command entry''s `plugin` / `args`; `key` names it, `path` is its coordinate. The whole request is refused (they used to be stripped silently as unknown keys).'
15019
+ required: [reason, key, path]
15020
+ additionalProperties: false
15021
+ properties:
15022
+ reason: { type: string, enum: [wire_key_invalid] }
15023
+ key: { type: string }
15024
+ path: { $ref: '#/components/schemas/SettingsHooksConfigPath' }
15009
15025
  - type: object
15010
15026
  description: 'server 7.115.0–7.125.x: the dead-alias matcher refusal before `reason` existed.'
15011
15027
  required: [event, alias, canonical]
@@ -15027,13 +15043,42 @@ components:
15027
15043
  properties:
15028
15044
  scenario: { type: string }
15029
15045
  source: { type: string, enum: [ruling, deployment] }
15046
+ - type: object
15047
+ description: >-
15048
+ server >= 7.133.0 — entry problems on a tool-name list / settings-rule refusal (400 `request.field_invalid`, 422): `problems` (at most 1000, each EntryProblem) + `total` (the true count; `total > problems.length` => cut).
15049
+ With `callerTrust` it is the below-trust-floor refusal: the caller's admission tier (`global` | `project`, read open; the same words as `capabilities.settings.permissions.callerTrust`).
15050
+ required: [problems, total]
15051
+ additionalProperties: false
15052
+ properties:
15053
+ problems: { type: array, items: { $ref: '#/components/schemas/EntryProblem' } }
15054
+ total: { type: integer }
15055
+ callerTrust: { type: string }
15056
+ - type: object
15057
+ description: >-
15058
+ server >= 7.133.0 — 400 `request.field_conflict`: the request body contradicts itself. `kind` = which constraint (a CLOSED set — fourteen words, fifteen from server >= 7.134.0 — read open: cascade_verify | image_profile | resume_anchor |
15059
+ system_prompt_fold | file_history_restore | skills_disabled | existing_session | scratchpad_root | ask_answer | policy_person_ask | plan_edit | plan_mode_after | bound_call_door | remember_decision |
15060
+ approval_feedback (server >= 7.134.0: `feedback` on a deny — fields `["feedback", "decision"]` — or on a policy decision — `["feedback", "decidedBy"]`));
15061
+ `fields` = the named body coordinates, subject first (at least one; a dynamic index reads `agents[1].skills`); `count` / `cap` only on the fold-cap kind (the folded total and its cap).
15062
+ required: [kind, fields]
15063
+ additionalProperties: false
15064
+ properties:
15065
+ kind: { type: string }
15066
+ fields: { type: array, items: { type: string } }
15067
+ count: { type: integer }
15068
+ cap: { type: integer }
15069
+ - type: object
15070
+ description: '409 `conflict.session_placed` (server >= 7.133.0): the session sync commit targeted a session that holds a delegated agent''s transcript. `placement_kind` is the stored placement kind (today only `subagent`; read open).'
15071
+ required: [placement_kind]
15072
+ additionalProperties: false
15073
+ properties:
15074
+ placement_kind: { type: string }
15030
15075
  - type: string
15031
15076
  count:
15032
15077
  type: integer
15033
- description: '`reason: over_cap` / `too_long` only — how much was sent (entries / characters; S-904 since server >= 7.106.0, every capped face since >= 7.132.0). SDK: `err.extra.count`.'
15078
+ description: '`reason: over_cap` / `too_long` / `too_large` — how much was sent (entries / characters / bytes; every capped face since >= 7.132.0). ABSENT on the unmeasured form (a body read that stopped at the cap). SDK: `err.extra.count`.'
15034
15079
  cap:
15035
15080
  type: integer
15036
- description: '`reason: over_cap` / `too_long` only — the cap (1000 for a permissions bucket; per face otherwise). SDK: `err.extra.cap`.'
15081
+ description: '`reason: over_cap` / `too_long` / `too_large` — the cap (1000 for a permissions bucket; per face otherwise; bytes on a too_large 413). SDK: `err.extra.cap`.'
15037
15082
  index:
15038
15083
  type: integer
15039
15084
  description: '`reason: bad_entry` only — the index of the first bad item; ABSENT when the list itself is not an array. SDK: `err.extra.index`.'
@@ -15058,6 +15103,8 @@ components:
15058
15103
  the engine's `detail.code` passes through as `blockedBy` (today only `checkpoint.config_refused` carries it — the closed set is core's
15059
15104
  `CHECKPOINT_CONFIG_REFUSAL_CODES`, 40 words since core 7.53.0; read it as "the errorCode a fresh run would report for the same bad configuration"),
15060
15105
  next to `field` and `reason`, absent keys not minted (SDK `ResumeRefusalExtra`).
15106
+ server >= 7.134.0 / core 7.54.0: the SAME three keys on 422 `parked_resume.startup_failed` (a parked background agent's leg refused before the claim — the
15107
+ engine now hands the failure's code / field / reason to the redemption receipt; absent when the engine gave none; <= 7.133.x carried only `cause` there).
15061
15108
 
15062
15109
  ResumeLegConflictError:
15063
15110
  description: >
@@ -15141,6 +15188,9 @@ components:
15141
15188
  # 🔴 census 轴一(2026-07-30)补的 4 臂:server 今天就真发,spec/SDK 两侧此前同缺(详见各 schema 处的注)。
15142
15189
  - $ref: '#/components/schemas/Event_context_usage'
15143
15190
  - $ref: '#/components/schemas/Event_config_assembled'
15191
+ # server >= 7.134.0 (core 7.54.0): the device lane's cross-boundary taint row — DURABLE only (the `GET /v1/runs/{taskId}/events` ledger replay), never
15192
+ # on the live stream and never on the `GET /v1/runs/{taskId}` poll object; the trace SSE twin is the `cross-boundary-taint` frame.
15193
+ - $ref: '#/components/schemas/Event_cross_boundary_taint'
15144
15194
  - $ref: '#/components/schemas/Event_needs_review'
15145
15195
  - $ref: '#/components/schemas/Event_message_committed'
15146
15196
  # [2373]A-2(2026-08-03,server 5.0.0):compaction_outcome 三腿同源批的 spec 半场(SDK 臂 20867e1 同批)。
@@ -15169,6 +15219,11 @@ components:
15169
15219
  - $ref: '#/components/schemas/Event_tool_approval_complete'
15170
15220
  - $ref: '#/components/schemas/Event_approval_request'
15171
15221
  - $ref: '#/components/schemas/Event_approval_revoke'
15222
+ # server >= 7.133.0 (sdk 25.0.0): the card family's single-card CLOSE frame (`approval_complete`; one frame family per leg — it replaces the legacy
15223
+ # `tool_approval_complete` on a leg where `capabilities.approvalFrameFamilies` = ["approval_request"]) and the LIVE-ONLY `session_titled` frame (a session
15224
+ # title was written; never ledgered, no eventId).
15225
+ - $ref: '#/components/schemas/Event_approval_complete'
15226
+ - $ref: '#/components/schemas/Event_session_titled'
15172
15227
  # sdk 9.1.0: two LIVE-leg arms the service has been emitting while neither this union nor the
15173
15228
  # SDK's declared them — `text_end` since server 7.66.0 (segment boundary; retires the idle-flush
15174
15229
  # heuristic) and `tool_roster_delta` since server 7.70.0 (run-time roster change). A generated client
@@ -15214,6 +15269,7 @@ components:
15214
15269
  failed: '#/components/schemas/Event_failed'
15215
15270
  context_usage: '#/components/schemas/Event_context_usage'
15216
15271
  config_assembled: '#/components/schemas/Event_config_assembled'
15272
+ cross_boundary_taint: '#/components/schemas/Event_cross_boundary_taint'
15217
15273
  needs_review: '#/components/schemas/Event_needs_review'
15218
15274
  message_committed: '#/components/schemas/Event_message_committed'
15219
15275
  compaction_outcome: '#/components/schemas/Event_compaction_outcome'
@@ -15230,6 +15286,8 @@ components:
15230
15286
  tool_approval_complete: '#/components/schemas/Event_tool_approval_complete'
15231
15287
  approval_request: '#/components/schemas/Event_approval_request'
15232
15288
  approval_revoke: '#/components/schemas/Event_approval_revoke'
15289
+ approval_complete: '#/components/schemas/Event_approval_complete'
15290
+ session_titled: '#/components/schemas/Event_session_titled'
15233
15291
  text_end: '#/components/schemas/Event_text_end'
15234
15292
  tool_roster_delta: '#/components/schemas/Event_tool_roster_delta'
15235
15293
  reasoning_end: '#/components/schemas/Event_reasoning_end'
@@ -15259,13 +15317,18 @@ components:
15259
15317
  condition: durable-ledger leg only (no ledger => neither header nor frame). sessionId rides along
15260
15318
  (needed to bind subsequent turns when the server minted a fresh session). Always precedes any content
15261
15319
  frame. NOT an engine event — server-minted, no EventIdentity.
15262
- # 封闭:铸造点 routes/tasks.ts 是逐字写死的三键字面量(sessionId 条件在场)。
15320
+ # 封闭:铸造点 routes/tasks.ts 是逐字写死的三键字面量(sessionId 条件在场);server >= 7.133.0 起第四个条件键 `warnings`(有才发)。
15263
15321
  additionalProperties: false
15264
15322
  required: [type, taskId]
15265
15323
  properties:
15266
15324
  type: { const: meta }
15267
15325
  taskId: { type: string }
15268
15326
  sessionId: { type: string }
15327
+ warnings:
15328
+ type: array
15329
+ items: { $ref: '#/components/schemas/EntryProblem' }
15330
+ description: 'server >= 7.133.0, present only when non-empty: the accept-time NOTICES for entries of the allow-side selector lists (root / agent `allowTools`) the engine reads as nothing or that are not in the roster. Same list as the 202 receipt''s and the sync 200 body''s `warnings`. ABSENT = no notice (older servers too).'
15331
+
15269
15332
  Event_reasoning:
15270
15333
  type: object
15271
15334
  description: durable stream only — turn-aggregated thinking.
@@ -15433,10 +15496,10 @@ components:
15433
15496
  # 每只闭集 = 引擎今天的词 ∪ 引擎退役词表里那一集。这五张 enum 与引擎 `RETIRED_MEMBERS` 逐集逐词同(由 `server-7129-wire` 对拍);卡面 / 队列行 / 请求面的同名闭集只取今天的词。
15434
15497
  GateRecordAskClass:
15435
15498
  type: string
15436
- enum: [content_question, hook, ancestor, dangerous_delete, unverifiable_delete, unverifiable_shell_text, transcript_write, org_rule, rule_unreadable, ask_rule, denial_limit_fallback, classifier_no_ruling, requires_user_interaction, operator_forced, shell_floor, target_unresolved, probe, write_protection, tool_marks_mcp, tool_marks_a2a, policy_rra, artifact_shared_read, mode, unstated, ordinary, write_protection_unresolved, governance_unavailable]
15499
+ enum: [content_question, hook, ancestor, dangerous_delete, unverifiable_delete, unverifiable_shell_text, transcript_write, org_rule, rule_unreadable, ask_rule, denial_limit_fallback, classifier_no_ruling, requires_user_interaction, cross_boundary, operator_forced, shell_floor, target_unresolved, probe, write_protection, tool_marks_mcp, tool_marks_a2a, policy_rra, artifact_shared_read, mode, unstated, ordinary, write_protection_unresolved, governance_unavailable]
15437
15500
  description: >-
15438
15501
  The class word on a GATE RECORD (`tool_end.gate.disposition.askClass` / `.classes` on the mode-refusal arm, `ModeRelease.askClass` / `.classes`) — server >= 7.129.0 reads the record as an ARCHIVE:
15439
- today's `AskClass` set (twenty-five words) PLUS the words the engine retired (`write_protection_unresolved`, retired in core 7.49.0 — renamed `target_unresolved`; `governance_unavailable`,
15502
+ today's `AskClass` set (twenty-six words; `cross_boundary` from server >= 7.134.0) PLUS the words the engine retired (`write_protection_unresolved`, retired in core 7.49.0 — renamed `target_unresolved`; `governance_unavailable`,
15440
15503
  retired in core 7.51.0 — merged into `rule_unreadable`). A record the previous engine generation legally wrote is replayed byte for byte (a 7.50.x mode refusal can carry `governance_unavailable`);
15441
15504
  a word NO engine generation ever minted (a case variant, the empty string, a future word) withholds the whole `gate` and counts `server.trace.gate-outcome-defective`.
15442
15505
  Render a known archive word normally and an unknown one generically; never drop the record because the word is not in today's set.
@@ -15454,9 +15517,9 @@ components:
15454
15517
  OMITTED `reason` for the two retired words (`released` was still sent); from 7.129.0 they arrive verbatim on the replay faces. Same reading as `GateRecordAskClass`.
15455
15518
  GateRecordPersistedRuleMandate:
15456
15519
  type: string
15457
- enum: [operator_always, tool_marks, probe_mandate, probe_unanswered, write_protection, target_unresolved, write_protection_unresolved]
15520
+ enum: [operator_always, tool_marks, probe_mandate, probe_unanswered, write_protection, target_unresolved, cross_boundary_taint, write_protection_unresolved]
15458
15521
  description: >-
15459
- `released.mandate` on a GATE RECORD (archive read, server >= 7.129.0): today's six `PersistedRuleMandate` words PLUS the retired `write_protection_unresolved` (core 7.49.0 rename). Same reading as `GateRecordAskClass`.
15522
+ `released.mandate` on a GATE RECORD (archive read, server >= 7.129.0): today's seven `PersistedRuleMandate` words (`cross_boundary_taint` from server >= 7.134.0) PLUS the retired `write_protection_unresolved` (core 7.49.0 rename). Same reading as `GateRecordAskClass`.
15460
15523
  GateRecordAskOrigin:
15461
15524
  type: string
15462
15525
  enum: [content_question, plan_review, ancestor_marked, org_unavailable, org_rule, rule_store_unavailable, hook, ask_rule, denial_limit_fallback, shell_gate_tighten, safety_tighten, policy, unresolvable]
@@ -15687,9 +15750,10 @@ components:
15687
15750
  `UNATTENDED_PERMISSION_MODE=dontAsk`).
15688
15751
  AskClass:
15689
15752
  type: string
15690
- enum: [content_question, hook, ancestor, dangerous_delete, unverifiable_delete, unverifiable_shell_text, transcript_write, org_rule, rule_unreadable, ask_rule, denial_limit_fallback, classifier_no_ruling, requires_user_interaction, operator_forced, shell_floor, target_unresolved, probe, write_protection, tool_marks_mcp, tool_marks_a2a, policy_rra, artifact_shared_read, mode, unstated, ordinary]
15753
+ enum: [content_question, hook, ancestor, dangerous_delete, unverifiable_delete, unverifiable_shell_text, transcript_write, org_rule, rule_unreadable, ask_rule, denial_limit_fallback, classifier_no_ruling, requires_user_interaction, cross_boundary, operator_forced, shell_floor, target_unresolved, probe, write_protection, tool_marks_mcp, tool_marks_a2a, policy_rra, artifact_shared_read, mode, unstated, ordinary]
15691
15754
  description: >
15692
- core `ASK_CLASSES` (TWENTY-FIVE words as of core 7.51.0 / server >= 7.129.0: `governance_unavailable` was merged into `rule_unreadable`, no alias — sdk 23.0.0; a stored card / parked row written by server <= 7.128.x is rewritten once by the server at boot. The gate record reads the ARCHIVE form, see `GateRecordAskClass`. History: twenty-two words as of core 7.40.0, twenty-three as of core 7.41.0, twenty-four as of core 7.43.0, twenty-five as of core 7.45.0, twenty-six as of core 7.49.0 — 7.34.0 minted twenty; `unverifiable_shell_text` joined in 7.49.0 #1505 and rides server >= 7.126.0, where the
15755
+ core `ASK_CLASSES` (TWENTY-SIX words as of core 7.54.0 / server >= 7.134.0: `cross_boundary` joined after `requires_user_interaction` — the device lane's
15756
+ cross-boundary confirmation, see below; TWENTY-FIVE as of core 7.51.0 / server >= 7.129.0: `governance_unavailable` was merged into `rule_unreadable`, no alias — sdk 23.0.0; a stored card / parked row written by server <= 7.128.x is rewritten once by the server at boot. The gate record reads the ARCHIVE form, see `GateRecordAskClass`. History: twenty-two words as of core 7.40.0, twenty-three as of core 7.41.0, twenty-four as of core 7.43.0, twenty-five as of core 7.45.0, twenty-six as of core 7.49.0 — 7.34.0 minted twenty; `unverifiable_shell_text` joined in 7.49.0 #1505 and rides server >= 7.126.0, where the
15693
15757
  unresolved-write-target class was also RENAMED to `target_unresolved` (#955; zero alias — the previous spelling is in the SDK README's 22.0.0 migration table); `shell_floor` joined in 7.39.0 #1260 and rides
15694
15758
  server >= 7.113.0; `unstated` joined in 7.40.0 and rides server >= 7.115.0; `requires_user_interaction` joined in 7.41.0 and rides
15695
15759
  server >= 7.116.0; `classifier_no_ruling` joined in 7.43.0 #1353 and rides server >= 7.118.0; `dangerous_delete` joined in 7.45.0 and rides
@@ -15857,6 +15921,29 @@ components:
15857
15921
  WHO ASKED. Same-present/same-absent as `settlement` (I1). CLOSED here (archive read, `GateRecordAskOrigin`) — see the
15858
15922
  note above.
15859
15923
 
15924
+ McpProbeFaceRow:
15925
+ # server 7.133.0: the face''s row = the wiring-manifest row projection + TWO face-only optional keys. The manifest row is CLOSED (so an `allOf` over it would reject the two extra keys), hence a
15926
+ # standalone closed schema whose per-key shapes are `$ref`s into WiringManifestMcpEntry's own properties (one source per shape); the key SET is pinned by test (manifest row keys + the two).
15927
+ type: object
15928
+ description: >
15929
+ One row of the no-run MCP status face (GET /v1/capabilities/mcp and POST /v1/capabilities/mcp/probe): the SAME projection a prepared leg pushes onto `wiring_manifest.mcp[]`, PLUS two optional keys only this
15930
+ face carries (server >= 7.133.0). `toolNames` = the tool names this server will mount (the same probe reading `servers[i].tools`, engine namespace form `mcp__<server>__<tool>`, after the schema gate and the per-server
15931
+ allow-list; the server neither composes nor renames), at most 200 per server, the reading-order prefix. ABSENT = an older server or no `tools` in that server's reading — NEVER invent `[]` (it would read as "this server
15932
+ has no tools"); a present `[]` is real. `toolNamesTruncated` = TRUE-ONLY: the list was cut (more than 200; exactly 200 is NOT cut and the key stays absent, never `false`) — when true `toolCount` is the true count, not
15933
+ `toolNames.length`. A misaligned reading (not an array / count differs from the row count) answers 500 `internal.error` on both doors instead of shipping a list that could be paired wrongly.
15934
+ additionalProperties: false
15935
+ required: [name, status]
15936
+ properties:
15937
+ name: { $ref: '#/components/schemas/WiringManifestMcpEntry/properties/name' }
15938
+ status: { $ref: '#/components/schemas/WiringManifestMcpEntry/properties/status' }
15939
+ source: { $ref: '#/components/schemas/WiringManifestMcpEntry/properties/source' }
15940
+ toolCount: { $ref: '#/components/schemas/WiringManifestMcpEntry/properties/toolCount' }
15941
+ errorCode: { $ref: '#/components/schemas/WiringManifestMcpEntry/properties/errorCode' }
15942
+ delivered: { $ref: '#/components/schemas/WiringManifestMcpEntry/properties/delivered' }
15943
+ httpStatus: { $ref: '#/components/schemas/WiringManifestMcpEntry/properties/httpStatus' }
15944
+ liveness: { $ref: '#/components/schemas/WiringManifestMcpEntry/properties/liveness' }
15945
+ toolNames: { type: array, items: { type: string }, maxItems: 200 }
15946
+ toolNamesTruncated: { type: boolean, enum: [true] }
15860
15947
  McpProbeFace:
15861
15948
  type: object
15862
15949
  description: >
@@ -15882,8 +15969,8 @@ components:
15882
15969
  description: 'How long this reading may be reused (seconds; 30s per identity + declaration set server-side). A hit inside the window does NOT count against the rate limit.'
15883
15970
  servers:
15884
15971
  type: array
15885
- items: { $ref: '#/components/schemas/WiringManifestMcpEntry' }
15886
- description: 'One row per declaration, SAME ROW SHAPE as the wiring manifest, index-aligned (see this schema''s description).'
15972
+ items: { $ref: '#/components/schemas/McpProbeFaceRow' }
15973
+ description: 'One row per declaration, the wiring-manifest row shape plus (server >= 7.133.0) this face''s two optional keys, index-aligned (see this schema''s description).'
15887
15974
 
15888
15975
  McpLivenessState:
15889
15976
  type: string
@@ -16654,6 +16741,12 @@ components:
16654
16741
  (`capabilities.rewindFiles`); only a run the REQUEST word turned off cannot restore itself.
16655
16742
  OPEN on read: the server does not enumerate the word (a present non-string is dropped), a third word passes
16656
16743
  through — treat it as unknown. ABSENT = an older engine / external derivation, never "capturing".
16744
+ permissionMode:
16745
+ allOf: [{ $ref: '#/components/schemas/WiringManifestPermissionMode' }]
16746
+ description: >-
16747
+ server >= 7.133.0 (core 7.53.0; TENANT and OPERATOR faces alike) — this leg's PERMISSION-MODE receipt `{ requested?, effective, foldedBy? }`; the shell renders its mode indicator from `effective`
16748
+ (`foldedBy` present = the requested mode was folded), never from the word it sent. ABSENT = an older engine / server, not an assertion of the default mode. Effective half only (this frame);
16749
+ the same section rides the non-streaming `wiringManifest` receipt and the ledger row. Section shape: WiringManifestPermissionMode.
16657
16750
  instructions:
16658
16751
  allOf: [{ $ref: '#/components/schemas/WiringManifestInstructions' }]
16659
16752
  description: >
@@ -16878,6 +16971,16 @@ components:
16878
16971
  pathTarget: { $ref: '#/components/schemas/McpToolPathTarget' }
16879
16972
  ruleFace: { $ref: '#/components/schemas/McpToolRuleFace' }
16880
16973
  renderHints: { $ref: '#/components/schemas/McpToolRenderHints' }
16974
+ boundaryLegs:
16975
+ description: >-
16976
+ core 7.54.0 / server >= 7.134.0 (a REQUIRED engine member: every row of `wiring_manifest.tools.entries[]` and of a `tool_roster_delta` frame's
16977
+ `delta.roster.entries[]` carries it) — the LEGS a call of this tool travels (core `BOUNDARY_LEGS`, in that order: `env` = inside the execution
16978
+ environment / on the device, `outside_read` = a read beyond it, `outside_write` = a write beyond it), or `per_call` when its dispatch gives the legs
16979
+ per call (the engine's own definitions and the protocol tools state them; every other row follows its `effect`). The device lane's launch gate reads
16980
+ it. ABSENT = an older server (<= 7.133.x).
16981
+ oneOf:
16982
+ - { type: string, const: per_call }
16983
+ - { type: array, items: { type: string, enum: [env, outside_read, outside_write] } }
16881
16984
  McpToolPathTarget:
16882
16985
  type: object
16883
16986
  required: [param, access]
@@ -17533,7 +17636,7 @@ components:
17533
17636
  payload carried `type` (the read boundary emits `{type, ...data}`) so runtime discrimination worked,
17534
17637
  but a generated client had nothing to switch on and dropped approval cards. That gap is closed.
17535
17638
  The shell renders `tool_approval` as the three-choice card and dismisses it on `tool_approval_complete`.
17536
- Respond via POST /v1/tool-approvals/{approvalId}/respond.
17639
+ Respond via POST /v1/tool-approvals/{id}/respond.
17537
17640
  required: [type, approvalId]
17538
17641
  additionalProperties: true
17539
17642
  properties:
@@ -17579,44 +17682,6 @@ components:
17579
17682
  governance shell gate sits at the "classify" tier, a shell ask may come from the classifier or from
17580
17683
  another gate; the server leaves the key absent rather than guessing). Never render absence as
17581
17684
  "this gate can be bypassed by a posture".
17582
- ruleSuggestions:
17583
- type: array
17584
- minItems: 1 # 空数组不可能出现:server 的合取项②要求 `req.ruleSuggestions` **非空**,否则整键省略(缺席 = 本卡无此选项)
17585
- # 刻意**不**声明 maxItems:server 的 `.max(4)` 是**卡** schema(`ApprovalCardSchema`)上的约束,这条
17586
- # live 帧腿没有它。在这里抄一个上界 = 声明一条 server 并不执行的约束(照 spec 生成的严格客户端会
17587
- # 拒收一条合法帧)。数量上界的真源是引擎:core 5.22.0 的 `suggestRulesForCommand` **至多铸一条,且恒
17588
- # `match: "exact"`**(不写死上界是给将来的 miner 留位,不是暗示今天会有更多)。
17589
- items: { $ref: '#/components/schemas/RuleSuggestion' }
17590
- description: >
17591
- server #154 车二 (core 5.18.0, design/179), ADDITIVE, "tool_approval" only: the rule candidates
17592
- the ENGINE minted for this ask. Render them as the "don't ask again" options and echo the chosen
17593
- one back as `persistRule.rule` on POST /v1/tool-approvals/{id}/respond.
17594
-
17595
- 🔴 PRESENCE IS THE PROMISE: the key is present ONLY on a deployment that actually has a
17596
- permission-rule store wired (the same object behind the engine's `permissionRules.storeWired`
17597
- manifest bit). A deployment without one OMITS it rather than offering an option with nowhere to
17598
- redeem — an unredeemable "don't ask again" is a wire lie, worse than absence.
17599
-
17600
- 🔴 THE CONVERSE DOES NOT HOLD: absence means "THIS CARD has no redeemable candidate", and three
17601
- causes produce it — no rule store wired, the engine minted no candidate for this command (compound /
17602
- redirect / substitution), or the command bytes were unreadable. One frame cannot tell them apart, so
17603
- never diagnose the deployment's rule lane from a single absence, and never synthesize a candidate.
17604
-
17605
- 📏 HOW MANY, TODAY: core 5.22.0's miner (`suggestRulesForCommand`) emits AT MOST ONE candidate and
17606
- always `match: "exact"` — no shipped producer mints the broader prefix option. `prefix` stays in the
17607
- closed vocabulary because the wire validator accepts it and a later miner may emit it; render what
17608
- arrives, but do NOT build a UI that assumes a narrow/broad PAIR is on offer.
17609
-
17610
- 🔴 The values are the engine's VERBATIM (closed `match` vocabulary + engine-minted text); the
17611
- server neither re-mints nor re-orders them. A hand-built rule string is refused at redemption
17612
- (`ruleRefusal: "rule_not_offered"`) — by construction, so a click on a harmless command cannot be
17613
- turned into a rule for something else.
17614
-
17615
- ⚠️ REDEMPTION SCOPE: the key also rides the durable card replay, but its only redemption channel is
17616
- the LIVE respond leg keyed by this frame's `approvalId` — an in-process map on ONE replica. Same
17617
- replica ⇒ redeemable; after a restart / on another replica that `approvalId` 404s and the person
17618
- must use the durable decide leg, which carries NO rule field in v1 (its body is strict — an extra
17619
- `persistRule` is a loud 400, not a silent drop).
17620
17685
  ruleOffers:
17621
17686
  type: array
17622
17687
  minItems: 1
@@ -17624,8 +17689,8 @@ components:
17624
17689
  description: >
17625
17690
  server >= 7.46.0 (core 5.58.0, design/375), "tool_approval" only — the ENGINE-minted "don't ask
17626
17691
  again" OPTIONS for this ask, as a DISCRIMINATED UNION (`single` | `batch`). It is the BREAKING
17627
- REPLACEMENT of `ruleSuggestions` above: a >= 7.46.0 engine mints the old key nowhere, there is no
17628
- alias, and the two keys are declared side by side only so one type surface covers both generations.
17692
+ REPLACEMENT of the triple-shaped `ruleSuggestions` of servers <= 7.45: a >= 7.46.0 engine mints the old key nowhere and there is no
17693
+ alias (sdk 26.0.0 deleted the old key with the support floor 7.52.0).
17629
17694
 
17630
17695
  🔴 READ `kind` TO TELL THE ARMS APART, never a positional assumption. ORDER IS CONTRACT: a
17631
17696
  whole-string exact `single` is index 0 when present and a `batch` is always last; core's own
@@ -17640,23 +17705,15 @@ components:
17640
17705
  suppress its persistence actions entirely (core's normative clause) — a compacted index means the
17641
17706
  k-th option the person clicked is not the k-th rule the server redeems.
17642
17707
 
17643
- 🔴 PRESENCE IS THE PROMISE (same as `ruleSuggestions`): present only where a permission-rule store
17708
+ 🔴 PRESENCE IS THE PROMISE: present only where a permission-rule store
17644
17709
  is really wired. The converse does not hold — a wired deployment still omits it for mandated /
17645
17710
  requiresRealApproval / allow_outranked / ask_rule_matched / hook-produced / inherited-unresolved / ancestor-resolved /
17646
17711
  anonymous asks. Absence reads as "THIS CARD has no such option", never as "this deployment has no
17647
17712
  rule lane" (that is `capabilities.permissionRules`) and never as "your rule broke" (that is
17648
- `allowRuleShadowed`; `persistedRuleShadowed` on a server <= 7.116.x).
17713
+ `allowRuleShadowed`).
17649
17714
 
17650
17715
  ⚠️ REDEMPTION SCOPE is the live respond leg keyed by this frame's `approvalId` only; a replayed
17651
17716
  offer on another replica is not a promise that it is still redeemable.
17652
- persistedRuleShadowed:
17653
- type: string
17654
- description: >-
17655
- server >= 7.13.0 (core 5.25.0 #144), ADDITIVE, "tool_approval" only — this ask MATCHED one of the caller's persisted allow
17656
- rules and that rule COULD NOT silence it; the value is the rule's VERBATIM text (secret-redacted + length-bounded; UNTRUSTED
17657
- for display, like `message`). Absent != "you have no rule"; deliberately separate from `governanceForced`.
17658
- RETIRED: server >= 7.117.0 (core 7.42.0, BREAKING rename, no alias) never mints this key on any face; its successor is
17659
- `allowRuleShadowed` (all five allow-layer members, not just the rule store). Kept for servers in [7.13.0, 7.117.0) only (sdk 16.3.0).
17660
17717
  allowRuleShadowed:
17661
17718
  type: array
17662
17719
  items: { $ref: '#/components/schemas/AllowRuleShadow' }
@@ -17929,22 +17986,23 @@ components:
17929
17986
 
17930
17987
  # ── design/172 流内审批协议(#151)—— 已随 server 7.3.0 上线;开关 STREAM_APPROVAL_ENABLED 的默认值
17931
17988
  # 分两段:server ≤7.4.0 默认 OFF,≥7.5.0 默认 ON(BREAKING,见 server CHANGELOG)。──
17932
- # 与上面的 ToolApprovalFrame 是**并行加帧,不替换**:开关打开时同一只 ask 出两帧(先 tool_approval,
17933
- # 后 approval_request),两帧同带 `approvalId` ⇒ 消费端按它去重、优先渲染新帧。
17989
+ # 与上面的 ToolApprovalFrame 是两个帧族,**一条腿只走一个**(server >= 7.133.0,capabilities.approvalFrameFamilies):呈卡协议上场的腿上一只 ask 只出本族的帧、
17990
+ # 旧族一帧都不发,消费端不再按 `approvalId` 去重;≤7.132.x 的呈卡腿两族并发(先 tool_approval、后 approval_request,同带 `approvalId`)⇒ 按它去重、优先渲染新帧。
17934
17991
  classifierUnavailable:
17935
17992
  $ref: '#/components/schemas/ClassifierUnavailable'
17936
17993
  description: >-
17937
- server >= 7.69.0 (core 7.10.0 #616), ADDITIVE, present only when the classifier was consulted and
17994
+ server >= 7.69.0 (core 7.10.0), ADDITIVE, present only when the classifier was consulted and
17938
17995
  could not run — WHY this ask reached a human. See ClassifierUnavailable for the open-`cause` rule and
17939
17996
  the three shapes absence covers.
17940
- RETIRED: server >= 7.72.0 (core 7.14.0 #688) never mints this key on any of the three faces; its
17997
+ RETIRED: server >= 7.72.0 (core 7.14.0) never mints this key on any of the three faces; its
17941
17998
  successor is `disposition.cause` on the gate record (`unavailable` / `parse_error` — a different
17942
- question, so an old switch does not port). Kept for servers in [7.69.0, 7.72.0) only (sdk 9.2.0).
17999
+ question, so an old switch does not port). Kept on the frame because servers 7.69–7.71 are inside the supported range and still
18000
+ send it, and the closed inbox row declares the same key (frame and row read the same); it goes with the next major once the support floor is >= 7.106.0.
17943
18001
  ruleStoreUnreadable:
17944
18002
  type: string
17945
18003
  enum: [store, call]
17946
18004
  description: >-
17947
- server >= 7.72.0 (core 7.14.0 #688 C3), ADDITIVE — WHICH HALF of the rule lane could not be read.
18005
+ server >= 7.72.0 (core 7.14.0), ADDITIVE — WHICH HALF of the rule lane could not be read.
17948
18006
  Present iff `origin === "rule_store_unavailable"`: `store` = the wired rule store could not be read
17949
18007
  (look at the store / network); `call` = the store was read but THIS command could not be read against
17950
18008
  the person's deny/ask rows (look at the command spelling). One `origin` word covers both facts, and
@@ -17952,10 +18010,11 @@ components:
17952
18010
  frame and the inbox row; the `card_json` card deliberately does NOT carry it (it travels with `origin`,
17953
18011
  which is not on the card). Absence = `origin` is not that word — NOT an assertion that the rule store
17954
18012
  is healthy.
17955
- RETIRED: server >= 7.106.0 (core 7.34.0 C-R172, BREAKING — wire) never mints this key on either face — an
17956
- unreadable rule store no longer raises a card: that read's rows are SKIPPED by partition and the session
17957
- stream gets the user-facing `engine_notice` `permission.rule_store_unavailable` (`detail.at` = `prepare` /
17958
- `call`). Kept for servers in [7.72.0, 7.106.0) only (sdk K-32); `origin: rule_store_unavailable` itself stays.
18013
+ RETIRED: server >= 7.106.0 (core 7.34.0, BREAKING — wire) never mints this key on either face — an
18014
+ unreadable rule store no longer raises a card; the signal moved to the session stream's user-facing
18015
+ `engine_notice` `permission.rule_store_unavailable` (`detail.at` = `prepare` / `call`; what the run does while the rules cannot be read is
18016
+ that notice's own text). Kept on the frame because servers 7.72–7.105 are inside the supported range and still send it, and the closed inbox row
18017
+ declares the same key; it goes with the next major once the support floor is >= 7.106.0 (`origin: rule_store_unavailable` itself stays).
17959
18018
  readRootCandidate:
17960
18019
  type: object
17961
18020
  required: [dir, clearsThisAsk]
@@ -17972,6 +18031,14 @@ components:
17972
18031
  (a malformed shape, or a `dir` over 1024 UTF-16 code units ⇒ the key is ABSENT — never truncated,
17973
18032
  because a truncated directory is a DIFFERENT directory and adding it would not clear this ask;
17974
18033
  see the STRING-LENGTH UNIT convention in `info.description` for this `maxLength`'s counting unit).
18034
+ covers:
18035
+ allOf: [{ $ref: '#/components/schemas/ReadRootCover' }]
18036
+ description: >-
18037
+ Optional (engine core 7.22.0 `ReadRootGrantCandidate.covers`; the server carries it through key by key). ABSENT ⇒ `dir` is a DIRECTORY
18038
+ (adding it to the read roots releases what lies under it). `exact` ⇒ `dir` is the path the command NAMED, offered verbatim
18039
+ (a file or one concrete path, not a directory containing it): the consumer ADDS the string itself — expanding it into a
18040
+ pattern grants nothing — and must not render the offer as "add directory". The domain is the single word `exact`
18041
+ (a word outside it makes the server drop the whole candidate). The `approval_request` frame reads this very definition.
17975
18042
  clearsThisAsk:
17976
18043
  type: boolean
17977
18044
  enum: [true]
@@ -18052,8 +18119,23 @@ components:
18052
18119
  approvalId:
18053
18120
  type: string
18054
18121
  description: >
18055
- Bridge to the legacy frame / dedupe key — same value as the parallel tool_approval frame. During the
18056
- transition window POST /v1/tool-approvals/{approvalId}/respond still works.
18122
+ The id of POST /v1/tool-approvals/{id}/respond and GET /v1/tool-approvals/{id}. server >= 7.133.0: ALWAYS EQUAL TO `askId` (the row's key is the ask's one wire
18123
+ identity; a 64-character lowercase hexadecimal string — treat it as an OPAQUE string, never parse it as a uuid); the matching close frame `approval_complete` carries it too.
18124
+ On server <= 7.132.x it is the legacy bridge / dedupe key (a uuid, the same value as the parallel tool_approval frame).
18125
+ # ── server >= 7.133.0: the TEN off-card LIVE facts on the opening frame's top level (one frame family per leg) ──────────────────────────
18126
+ # Same keys, same shapes, same values as on the legacy `tool_approval` frame (each is a `$ref` into ToolApprovalFrame's own property — one
18127
+ # source per shape). Only the frame minted LIVE carries them (and what the durable ledger recorded of that frame): a disconnect-replay frame
18128
+ # (rebuilt from `card_json`) never does. Absence means "this frame does not carry it", not "there is none". See APPROVAL_REQUEST_LIVE_FACT_KEYS (sdk) / the server's twin.
18129
+ mandated: { $ref: '#/components/schemas/ToolApprovalFrame/properties/mandated' }
18130
+ mandate: { $ref: '#/components/schemas/ToolApprovalFrame/properties/mandate' }
18131
+ origin: { $ref: '#/components/schemas/ToolApprovalFrame/properties/origin' }
18132
+ boundGrantDigest: { $ref: '#/components/schemas/ToolApprovalFrame/properties/boundGrantDigest' }
18133
+ boundGrants: { $ref: '#/components/schemas/ToolApprovalFrame/properties/boundGrants' }
18134
+ classifierNoRuling: { $ref: '#/components/schemas/ToolApprovalFrame/properties/classifierNoRuling' }
18135
+ boundGrantAbsence: { $ref: '#/components/schemas/ToolApprovalFrame/properties/boundGrantAbsence' }
18136
+ approvalCard: { $ref: '#/components/schemas/ToolApprovalFrame/properties/approvalCard' }
18137
+ readRootCandidate: { $ref: '#/components/schemas/ToolApprovalFrame/properties/readRootCandidate' }
18138
+ probeReason: { $ref: '#/components/schemas/ToolApprovalFrame/properties/probeReason' }
18057
18139
 
18058
18140
  ApprovalCard:
18059
18141
  type: object
@@ -18086,33 +18168,15 @@ components:
18086
18168
  posture. Semantics, the ABSENT != false clause and the discrimination boundary are stated verbatim on
18087
18169
  ToolApprovalFrame.governanceForced. It sits at the card's top level rather than inside `risk` because
18088
18170
  `risk` describes the ENGINE's judgement of the call, while this key says WHO imposed the gate.
18089
- ruleSuggestions:
18090
- type: array
18091
- minItems: 1 # 同帧腿:空数组不可能出现(素材同源),缺席才是「本卡无此选项」
18092
- maxItems: 4 # 卡形上**真有**这条上界(server `ApprovalCardSchema` 的 `.max(4)`);live 帧腿没有,故只写在这里
18093
- items: { $ref: '#/components/schemas/RuleSuggestion' }
18094
- description: >
18095
- ADDITIVE (#154 车二 / core 5.18.0 design/179): the rule candidates the ENGINE minted for this ask —
18096
- the card's "don't ask again" options; echo the chosen one back as `persistRule.rule`. Semantics,
18097
- the PRESENCE-IS-THE-PROMISE clause (present only where a rule store is wired) and the redemption
18098
- scope are stated verbatim on ToolApprovalFrame.ruleSuggestions — the live frame, the stored
18099
- `card_json` and the replayed frame carry the SAME material.
18100
18171
  ruleOffers:
18101
18172
  type: array
18102
18173
  minItems: 1
18103
18174
  items: { $ref: '#/components/schemas/RuleOffer' }
18104
18175
  description: >
18105
- server >= 7.46.0 (core 5.58.0, design/375) — the discriminated-union replacement of
18106
- `ruleSuggestions` above. Same material as the live frame and the stored `card_json` (the server
18176
+ server >= 7.46.0 (core 5.58.0, design/375) — the discriminated-union replacement of the retired
18177
+ `ruleSuggestions` (servers <= 7.45; sdk 26.0.0 deleted it). Same material as the live frame and the stored `card_json` (the server
18107
18178
  projects all three through one `copyRuleOffer`), so semantics, order-is-contract, presence-is-the-
18108
18179
  promise and the per-leg index rules are stated verbatim on ToolApprovalFrame.ruleOffers.
18109
- persistedRuleShadowed:
18110
- type: string
18111
- description: >-
18112
- server >= 7.13.0 (core 5.25.0 #144), ADDITIVE — the persisted rule this ask matched but which could not silence it; frame and
18113
- card carry the SAME material (see ToolApprovalFrame.persistedRuleShadowed).
18114
- RETIRED: server >= 7.117.0 (core 7.42.0) never mints this key on any face (its strict card schema even refuses a stored card
18115
- carrying it); its successor is `allowRuleShadowed`. Kept for servers in [7.13.0, 7.117.0) only (sdk 16.3.0).
18116
18180
  allowRuleShadowed:
18117
18181
  type: array
18118
18182
  items: { $ref: '#/components/schemas/AllowRuleShadow' }
@@ -18147,35 +18211,13 @@ components:
18147
18211
  classifierUnavailable:
18148
18212
  $ref: '#/components/schemas/ClassifierUnavailable'
18149
18213
  description: >-
18150
- server >= 7.69.0 (core 7.10.0 #616), ADDITIVE — WHY this ask reached a human. Frame and card carry
18151
- the SAME value (one narrow-read function server-side). See ClassifierUnavailable for the open-`cause`
18152
- rule and the three shapes absence covers.
18153
- RETIRED: server >= 7.72.0 (core 7.14.0 #688) never mints this key on any of the three faces; its
18214
+ (minted by servers [7.69.0, 7.72.0), core 7.10.0 #616) WHY this ask reached a human. See ClassifierUnavailable for the open-`cause` rule and the three
18215
+ shapes absence covers.
18216
+ RETIRED: server >= 7.72.0 (core 7.14.0) never mints this key on any of the three faces; its
18154
18217
  successor is `disposition.cause` on the gate record (`unavailable` / `parse_error` — a different
18155
- question, so an old switch does not port). Kept for servers in [7.69.0, 7.72.0) only (sdk 9.2.0).
18156
-
18157
- RuleSuggestion:
18158
- type: object
18159
- description: >
18160
- One ENGINE-minted rule candidate (#154 车二 / core 5.18.0 `RuleSuggestion`) — what a single click on
18161
- "don't ask again" would persist.
18162
-
18163
- 🔴 THE TEXT COMES FROM THE ENGINE, NOT THE SHELL: the server derives it from the adjudicated command's
18164
- own bytes; a client may only echo one of these back verbatim (`persistRule.rule`). Reporting text that
18165
- is not among the candidates is refused (`ruleRefusal: "rule_not_offered"`) rather than guessed at, so
18166
- "click on a harmless command, get a rule for something else" is not spellable on this path.
18167
- # 封闭:core `RuleSuggestion` 恰三键(rule/match/command),server 逐字透传不重铸。
18168
- additionalProperties: false
18169
- required: [rule, match, command]
18170
- properties:
18171
- rule: { type: string, maxLength: 512, description: 'The canonical rule text to echo back on redemption (server MAX_RULE_TEXT_CHARS = 512).' }
18172
- match:
18173
- type: string
18174
- enum: [exact, prefix]
18175
- description: '`exact` = this one command only (`Bash(git status)`); `prefix` = word-boundary prefix (`Bash(git status:*)`). Closed vocabulary — an unknown value is not a candidate this contract describes; do not render it as redeemable.'
18176
- command: { type: string, maxLength: 512, description: 'The command pattern inside the rule, for a shell that would rather not re-parse `rule`.' }
18218
+ question, so an old switch does not port). It stays declared on the CARD because the pinned server's card schema (`ApprovalCardSchema`, server 7.134.0) still accepts it: a still-pending `card_json` row a
18219
+ 7.69–7.71 server wrote replays verbatim onto `approval_request.card` / the replay frame. (The FRAME's same-named key stays declared too; why, and until when: `ToolApprovalFrame.classifierUnavailable`.)
18177
18220
 
18178
- # ─── S-139(sdk 8.3.0):对 server 7.60.0 整体重对账带进来的四张型面 ───────────────────────
18179
18221
  ClassifierUnavailable:
18180
18222
  # 8.9.0:三面(tool_approval 帧 / ApprovalCard / InboxRow)同形同值 ⇒ 具名单源,内联三份会各自漂。
18181
18223
  type: object
@@ -18290,7 +18332,7 @@ components:
18290
18332
  `ApprovalCard.ruleOffersAbsence`; server >= 7.55.0 / core #490 fix ②, S-15). The engine names which
18291
18333
  door is shut: `mandated` = this ask must be confirmed every time (🔴 this arm must NOT point the user
18292
18334
  at writing a rule — a rule would not silence it, and saying so sends them into a loop); `allow_outranked` = an
18293
- allow-layer member matched this call but the ask sits at a higher tier, so another allow rule cannot clear it (the machine-readable twin of `allowRuleShadowed`; `persistedRuleShadowed` on a server <= 7.116.x);
18335
+ allow-layer member matched this call but the ask sits at a higher tier, so another allow rule cannot clear it (the machine-readable twin of `allowRuleShadowed`);
18294
18336
  `ask_rule_matched` = an explicit `ask` rule matched this call (same frame `origin: ask_rule`): it keeps asking, an allow rule would not silence it either; a card from the deployment name-list gate
18295
18337
  `APPROVAL_REQUIRE` / the supervisor route that reports an explicit ask-rule hit lands here too; `lane_cannot_speak` = a rule COULD clear this ask but the lane cannot speak this time (the engine could mint no coverable form for this command).
18296
18338
  🔴 sdk 23.0.0 BREAKING (server >= 7.129.0 / engine 7.51.0, `RULE_OFFERS_ABSENCES`): the former `shadowed` meant two things and is split into the middle two words, no alias; a card / parked row that a server <= 7.128.x
@@ -18316,12 +18358,21 @@ components:
18316
18358
  and a word outside the set all read as ABSENT — a card without this word must not be treated as "approving it grants nothing else"; whether to render "allow for this session" is `rememberRefused`.
18317
18359
  Display metadata, never an adjudication input. CLOSED: the server narrows with the engine's members, anything else reads as absent.
18318
18360
 
18361
+ ReadRootCover:
18362
+ type: string
18363
+ enum: [exact]
18364
+ description: >
18365
+ The closed vocabulary of `readRootCandidate.covers` (engine core 7.22.0 `ReadRootGrantCandidate.covers`; the server's narrow read `readReadRootCandidate` drops the whole
18366
+ candidate for a word outside it). One word today: `exact` — `dir` is the path the command NAMED, not a directory containing it. The SDK exports the same table as
18367
+ `READ_ROOT_COVERS` and its `isApprovalRequestFrameV1` judges `covers` by membership in it; a source-level anchor reads the word off the server's minting site and the engine's type.
18319
18368
  PersistedRuleMandate:
18320
18369
  type: string
18321
- enum: [operator_always, tool_marks, probe_mandate, probe_unanswered, write_protection, target_unresolved]
18370
+ enum: [operator_always, tool_marks, probe_mandate, probe_unanswered, write_protection, target_unresolved, cross_boundary_taint]
18322
18371
  description: >
18323
18372
  WHICH WORD a mandated ask stands on (server >= 7.102.0 / S-721 / core 7.32.0 #1092) — core's
18324
- `PERSISTED_RULE_MANDATES` six words, transcribed verbatim and in order (core 7.32.1
18373
+ `PERSISTED_RULE_MANDATES` (seven words since core 7.54.0 / server >= 7.134.0: `cross_boundary_taint` — the run executes on a device outside the deployment's
18374
+ trust boundary and the deployment is multi-tenant, so an org-side write, or after an off-target read a call bound for the device, is confirmed by the deployment's
18375
+ approver per call; no standing allow rule and no automatic lane answers it; core puts it at the HEAD of the mandate ladder, the array keeps core's constant order), transcribed verbatim and in order (core 7.32.1
18325
18376
  `dist/core/ask-origin.d.ts:349`; the vocabulary's owner is core; the sixth word is `target_unresolved` from core 7.49.0 #955 / server >= 7.126.0 —
18326
18377
  server 7.122.0–7.125.x sent the previous spelling, which reads as out of set here, see the SDK README's 22.0.0 migration table). Rides FOUR faces with one value: the
18327
18378
  `tool_approval` frame, the `livePending` row top level, the operator queue row (`GET /v1/approvals`
@@ -18375,9 +18426,9 @@ components:
18375
18426
  RuleOfferMatch:
18376
18427
  type: string
18377
18428
  description: >
18378
- The MATCH FORM of a persisted rule — the server's `PERSISTED_RULE_MATCHES`, verbatim and in order (the vocabulary
18379
- is OWNED BY CORE; the server only transcribes it and pins set equality, so core adding a member turns
18380
- the server red at compile time). ONE vocabulary for two faces: a card's rule offer (`RuleOffer.match`, the
18429
+ The MATCH FORM of a persisted rule — core's `PERSISTED_RULE_MATCHES`, verbatim and in order (the vocabulary is OWNED BY CORE: a package-root
18430
+ value since core 7.54.0, which server >= 7.134.0 reads directly instead of keeping its own transcription; earlier servers transcribed it and pinned set
18431
+ equality). ONE vocabulary for two faces: a card's rule offer (`RuleOffer.match`, the
18381
18432
  server's card schema validates it with this very enum) and a persisted rule row (`GET /v1/rules`).
18382
18433
 
18383
18434
  `exact` = this one command only (`Bash(git status)`). `prefix` = the historical colon-star
@@ -18580,8 +18631,7 @@ components:
18580
18631
  maxLength: 512
18581
18632
  description: >
18582
18633
  With `edited` absent or false this is the CANDIDATE ARM: echo back, VERBATIM, the `rule` of the
18583
- chosen `kind: "single"` entry of the frame's `ruleOffers` (or, against a <= 7.45 server, of
18584
- `ruleSuggestions`). Do not hand-build it, re-case it or append `:*` — the server locates it by
18634
+ chosen `kind: "single"` entry of the frame's `ruleOffers`. Do not hand-build it, re-case it or append `:*` — the server locates it by
18585
18635
  text and refuses otherwise (`rule_not_offered`). With `edited: true` this is the FREE-TEXT ARM
18586
18636
  and the string is whatever the person typed.
18587
18637
  edited:
@@ -18729,6 +18779,40 @@ components:
18729
18779
  depth: { type: integer, description: 'Delegation depth (a direct child of the host = 1).' }
18730
18780
  agentName: { type: string, description: 'Child agent name. UNTRUSTED — spawning-model free text (server-redacted and length-bounded).' }
18731
18781
 
18782
+ ApprovalCompleteFrame:
18783
+ type: object
18784
+ description: >
18785
+ The CLOSE frame of the card family (`approval_complete`; server >= 7.133.0; SSE, `type` IS the event name; the durable leg appends the same frame, so it replays through
18786
+ GET /v1/runs/:id/events as the `Event_approval_complete` arm). ONE FRAME FAMILY PER LEG: on a leg where the card protocol is on (`capabilities.approvalFrameFamilies`
18787
+ = `["approval_request"]`) one ask travels only `approval_request` -> `approval_complete` / `approval_revoke`; the legacy `tool_approval` / `tool_approval_complete` pair is NOT sent
18788
+ (BREAKING vs <= 7.132.x, which sent both families for one ask; remedy on the worker: `STREAM_APPROVAL_ENABLED=false`).
18789
+ Same meaning as the legacy `tool_approval_complete` (one settle, one verdict: the `outcome` three words, the non-human `cause`, the park-route `parked`); dismiss the card by the opening frame's
18790
+ `askId`. Division of labour with ApprovalRevokeFrame: the revoke frame is BATCH-level (siblings voided by a winner), this one is the SINGLE card's end (answered / window expired / park route / cancelled);
18791
+ one ask can receive both — clearing by `askId` is enough. The gate is "this connection's opening frame was actually delivered". The frame is a notification, the row is the source of truth: a lost
18792
+ frame is compensated by the open-stream preamble (see ApprovalRequestFrame). THIS SCHEMA DESCRIBES v1 ONLY (read it as ApprovalFrameEnvelope first; the SDK's `isApprovalCompleteFrameV1` narrows).
18793
+ required: [type, schemaVersion, askId, taskId, approvalId, outcome, serverNowMs]
18794
+ additionalProperties: true
18795
+ properties:
18796
+ type: { type: string, enum: [approval_complete] }
18797
+ schemaVersion: { type: integer, enum: [1], description: 'This schema describes v1 only (see ApprovalFrameEnvelope for the unknown-version read).' }
18798
+ askId: { type: string, description: 'The durable identity — dismiss the card by it (the same key as the opening frame''s `askId`).' }
18799
+ taskId: { type: string, description: 'wire run id (same as the opening frame).' }
18800
+ approvalId: { type: string, description: 'Always equal to `askId` (same as the opening frame).' }
18801
+ outcome:
18802
+ type: string
18803
+ enum: [allowed, denied, expired]
18804
+ description: 'How the ask settled — the same three words as the legacy frame: `allowed` / `denied` = a decision; `expired` = a non-human ending (the reason is in `cause`).'
18805
+ cause:
18806
+ allOf: [{ $ref: '#/components/schemas/ApprovalSettleCause' }]
18807
+ description: >-
18808
+ The non-human ending''s cause (present only on `outcome: "expired"`; the fourteen words of the approval seat''s ending-cause set, the same word as on the audit settle row). A human answer carries none.
18809
+ ABSENCE IS NOT EVIDENCE.
18810
+ parked:
18811
+ type: boolean
18812
+ enum: [true]
18813
+ description: 'TRUE-ONLY: this ask itself ended on the park route (row moved to PARKING / PARKED; a late answer is reachable). Absent = no evidence — NEVER read as a real denial.'
18814
+ serverNowMs: { type: integer, description: 'Server clock at mint time.' }
18815
+
18732
18816
  ApprovalRevokeFrame:
18733
18817
  type: object
18734
18818
  description: >
@@ -19028,7 +19112,7 @@ components:
19028
19112
  description: >
19029
19113
  server >= 7.126.0 (sdk 21.1.0 `APPROVAL_DECIDE_GATE_KINDS`, verbatim and in order; single owner = server
19030
19114
  `APPROVAL_GATE_KINDS`) — the parked gate kinds the DURABLE decision door `POST /v1/approvals/{sessionId}/decide`
19031
- accepts (the live `POST /v1/tool-approvals/{approvalId}/respond` answers the same card family). A parked row of
19115
+ accepts (the live `POST /v1/tool-approvals/{id}/respond` answers the same card family). A parked row of
19032
19116
  kind `plan_review` goes to the plan-review door instead (`PlanReviewGateKind`); `resource_limit` / `needs_review` /
19033
19117
  `task_done` have no human decision door (resume / terminal). Clients route a pending row by reading this set,
19034
19118
  never by a hand-written copy of it.
@@ -19043,7 +19127,7 @@ components:
19043
19127
  enum: [person, policy]
19044
19128
  description: >
19045
19129
  server >= 7.118.0 (`APPROVAL_DECIDED_BY`, verbatim and in order) — WHO made an approval decision, one key on all three decision
19046
- doors (live `POST /v1/tool-approvals/{approvalId}/respond`, durable `POST /v1/tasks/{taskId}/asks/{askId}/decision`, durable
19130
+ doors (live `POST /v1/tool-approvals/{id}/respond`, durable `POST /v1/tasks/{taskId}/asks/{askId}/decision`, durable
19047
19131
  `POST /v1/approvals/{sessionId}/decide`). `person` = a person decided through the approval channel (ABSENT means this);
19048
19132
  `policy` = this deployment's own policy decided (a rule / posture answering on someone's behalf, a headless lane) — BOTH
19049
19133
  directions: the engine records `tool_end.gate.settlement.kind` = `policy_allowed` / `policy_refused` (`who.party: "policy"`).
@@ -19135,6 +19219,20 @@ components:
19135
19219
  `updatedInputForwarded` this field IS persisted, so an idempotent replay DOES promise byte-identity.
19136
19220
  The 409 `conflict.ask_decided` body echoes the same field — and there it is always the FIRST
19137
19221
  decision's note, not the one the rejected request carried.
19222
+ noteRecorded:
19223
+ type: boolean
19224
+ description: >
19225
+ server >= 7.133.0, ADDITIVE: did the `note` THIS request carried land as the row's text? Present ONLY when the request carried a `note`. `true` <=> the row's `decisionNote` is
19226
+ verbatim the text this request submitted (text that happens to be identical counts even if someone else wrote it first); `false` = not confirmed (the row holds another reason /
19227
+ the store could not be read back). Same shape and meaning as `noteRecorded` on the live respond ack and on its replayed arm (one judgement for the three doors). Carried by the
19228
+ first-decision and idempotent-replay forms; the PARKED redemption leg does not carry it (read it as unknown). ABSENT is not `false`.
19229
+ consumerState:
19230
+ type: string
19231
+ enum: [absent]
19232
+ description: >
19233
+ TRUE-ONLY disclosure (present as `absent`): no engine leg on THIS replica is waiting for this ask — the decision is on the row (an idempotent replay will read it, the audit surface keeps it)
19234
+ but nothing running here resumes because of it (the holder of the suspended promise may be another replica, or this replica restarted and its local window is empty). Read the RUN surface to
19235
+ learn whether anything continued; NEVER take a 200 without this key as proof the run continued (older servers never send it). Not a 409 / 410: the decision DID take effect.
19138
19236
 
19139
19237
  QuestionFrame:
19140
19238
  type: object
@@ -19471,6 +19569,38 @@ components:
19471
19569
  fields: {}
19472
19570
  overrideReasons: {}
19473
19571
  ts: {}
19572
+ CrossBoundaryTaintRow:
19573
+ type: object
19574
+ description: >
19575
+ server >= 7.134.0 / core 7.54.0 — the device lane's CROSS-BOUNDARY TAINT row: the engine's `permission.cross_boundary_taint` frame minus its envelope
19576
+ (`kind` / `version` / `taskId` / `ts`). Metadata only. Emitted ONLY on a tree that executes on a target device (one row when an armed tree prepares a
19577
+ leg — `prepare`, one on a durable resume leg — `resume`, one when this leg first lets an off-target read into the tree — `observed`, with that call);
19578
+ the host lane never emits it. Two exits, one row: the ledger event `cross_boundary_taint` on `GET /v1/runs/{taskId}/events` (`Event_cross_boundary_taint`)
19579
+ and the trace SSE `cross-boundary-taint` frame on `GET /v1/tasks/{taskId}/stream` (the row + `seq`). The `GET /v1/runs/{taskId}` poll object carries NO
19580
+ such key.
19581
+ # 不封闭(`additionalProperties: true`):本 schema 是 `Event_cross_boundary_taint` 的 allOf 目标(帧上多一个 `type`),封闭会把合法帧判成违约(同 PromptManifestRow)。
19582
+ additionalProperties: true
19583
+ required: [runId, phase, taint, governed]
19584
+ properties:
19585
+ runId: { type: string, description: 'the leg''s run id.' }
19586
+ phase: { type: string, enum: [prepare, resume, observed], description: 'core `permission.cross_boundary_taint` `phase` (engine closed set).' }
19587
+ taint:
19588
+ type: array
19589
+ description: 'The tree''s taint bits after the event — core `CROSS_BOUNDARY_TAINT_BITS` (`targetExecutionArmed` = the tree is armed to execute on the target; `offTargetReadObserved` = off-target read content was let into the tree).'
19590
+ items: { type: string, enum: [targetExecutionArmed, offTargetReadObserved] }
19591
+ governed: { type: boolean, description: 'the tree has run under the `multi_tenant` posture since it armed (derived from `REQUIRE_PRINCIPAL`).' }
19592
+ toolName: { type: string, description: '`observed` only: the call that let the off-target read in.' }
19593
+ toolCallId: { type: string, description: '`observed` only: that call''s id.' }
19594
+ Event_cross_boundary_taint:
19595
+ description: >
19596
+ DURABLE only (server >= 7.134.0 / core 7.54.0) — the ledger event of a `CrossBoundaryTaintRow` on `GET /v1/runs/{taskId}/events` (SSE `id:` = the row's
19597
+ seq). Never on the live stream, never on the `GET /v1/runs/{taskId}` poll object; an older server drops the frame on the spot.
19598
+ allOf:
19599
+ - $ref: '#/components/schemas/CrossBoundaryTaintRow'
19600
+ - type: object
19601
+ required: [type]
19602
+ properties:
19603
+ type: { const: cross_boundary_taint }
19474
19604
  Event_needs_review:
19475
19605
  type: object
19476
19606
  description: >
@@ -19669,7 +19799,7 @@ components:
19669
19799
  Event_tool_approval:
19670
19800
  description: >
19671
19801
  DURABLE-leg replay of the legacy tool-approval OPEN frame. Same payload as ToolApprovalFrame with the
19672
- discriminant narrowed to `tool_approval`. Settle it via POST /v1/tool-approvals/{approvalId}/respond.
19802
+ discriminant narrowed to `tool_approval`. Settle it via POST /v1/tool-approvals/{id}/respond.
19673
19803
  Anchor the card on `toolCallId` (NOT `approvalId`); render `governanceForced` as a non-bypassable
19674
19804
  "forced by governance" badge. UNTRUSTED for display: `message`/`args` are redacted but model-authored.
19675
19805
  allOf:
@@ -19692,9 +19822,11 @@ components:
19692
19822
  type: { type: string, enum: [tool_approval_complete] }
19693
19823
  Event_approval_request:
19694
19824
  description: >
19695
- The design/172 in-stream approval card as an AgentEvent arm. PARALLEL to `tool_approval`, not a
19696
- replacement: with the protocol on, one ask emits BOTH frames (legacy first) carrying the SAME
19697
- `approvalId` — dedupe on it and prefer this arm. The decision key is `askId`; settle via
19825
+ The design/172 in-stream approval card as an AgentEvent arm. ONE FRAME FAMILY PER LEG (server >= 7.133.0,
19826
+ `capabilities.approvalFrameFamilies`): on a leg where the card protocol is on, one ask emits only this family
19827
+ (`approval_request` -> `approval_complete` / `approval_revoke`) and none of the legacy `tool_approval` family,
19828
+ so there is nothing to deduplicate. On server <= 7.132.x the two families ran side by side (legacy first) carrying
19829
+ the SAME `approvalId` — there, dedupe on it and prefer this arm. The decision key is `askId`; settle via
19698
19830
  POST /v1/tasks/{taskId}/asks/{askId}/decision.
19699
19831
 
19700
19832
  🔴 THIS ARM IS THE OPEN ENVELOPE, NOT `ApprovalRequestFrame`, AND THAT IS DELIBERATE. The v1 frame
@@ -19741,6 +19873,37 @@ components:
19741
19873
  properties:
19742
19874
  type: { type: string, enum: [approval_revoke] }
19743
19875
 
19876
+ Event_approval_complete:
19877
+ description: >
19878
+ The card family's single-card CLOSE frame as an AgentEvent arm (server >= 7.133.0). One frame family per leg: with `capabilities.approvalFrameFamilies` = `["approval_request"]` this frame
19879
+ REPLACES the legacy `tool_approval_complete` (one ask no longer travels two families). Live SSE and the durable ledger replay carry the same shape (same settle, same verdict: `outcome`,
19880
+ non-human `cause`, `parked`).
19881
+ 🔴 THIS ARM IS THE OPEN ENVELOPE, NOT `ApprovalCompleteFrame` — same ruling as Event_approval_request / Event_approval_revoke: narrow with `schemaVersion == 1` (SDK:
19882
+ `isApprovalCompleteFrameV1`) before touching `outcome` / `cause` / `parked`; a frame that does not narrow still dismisses the card by its `askId` (when it has one) — never guess the outcome.
19883
+ 🔴 Consumption discipline: (a) dismiss by the opening frame's `askId`; (b) a ledger-replayed frame is for TIMELINE RENDERING ONLY — the card-set baseline stays the open-stream preamble;
19884
+ (c) NEVER derive the run's fate from this frame (read the run row / checkpoint face); (d) it does not conflict with ApprovalRevokeFrame — one ask can receive both.
19885
+ allOf:
19886
+ - $ref: '#/components/schemas/ApprovalFrameEnvelope'
19887
+ - type: object
19888
+ required: [type]
19889
+ properties:
19890
+ type: { type: string, enum: [approval_complete] }
19891
+ Event_session_titled:
19892
+ description: >
19893
+ server >= 7.133.0 — the session title was WRITTEN (contract §4a-ttl): the title generator wrote the title into the session row and minted this frame on THE RUN'S LIVE STREAM afterwards.
19894
+ 🔴 LIVE-ONLY, NEVER STORED: the session row (`GET /v1/sessions` list row `title`) is the single source of the title; this frame only says "the row just changed". It is not ledgered
19895
+ (GET /v1/runs/:id/events replay never carries it), not part of any replay surface, and has NO `eventId` (no SSE `id:` line, no part in the `Last-Event-ID` cursor). Only a leg holding a live stream
19896
+ receives it (today `POST /v1/tasks/stream`); the sync-JSON leg and the durable background leg structurally cannot. NOT sent when the session already has a title, on a user rename / pin
19897
+ (`PATCH /v1/sessions/{id}` does not go through the generator), with `SESSION_AUTO_TITLE=false`, after `done`, or once the connection is gone. THE FRAME MAY NOT ARRIVE — the title is whatever
19898
+ the session row says: refresh on the frame, read the row on cold start / another window / reconnect. `title` has passed the generator's redaction and cleaning and is the SAME value as the row's
19899
+ (still UNTRUSTED for display — model-authored text).
19900
+ type: object
19901
+ required: [type, sessionId, title]
19902
+ additionalProperties: false
19903
+ properties:
19904
+ type: { type: string, enum: [session_titled] }
19905
+ sessionId: { type: string, description: 'The session whose row was just written.' }
19906
+ title: { type: string, description: 'The title that landed in the session row (redacted and cleaned; bounded by the server''s TITLE_MAX_CHARS).' }
19744
19907
  # ── resources/*.ts schema backfill, batch 2 (2026-07-28) — the 28-item WIRE_PENDING ratchet zeroed.
19745
19908
  # Each schema below is cross-checked against the server source (file:line cited in its description
19746
19909
  # or in the SDK resource JSDoc it mirrors). Wire objects are OPEN sets (additionalProperties: true).
@@ -19765,6 +19928,10 @@ components:
19765
19928
  sessionId: { type: string, description: '`resolved` frames: which pending settled.' }
19766
19929
  toolCallId: { type: string, description: '`resolved` frames: the settled pending''s bound tool-call id (absent when the pending carried none).' }
19767
19930
  count: { type: integer, description: '`synced` frames: how many pendings the connect-time snapshot carried.' }
19931
+ checkpointId: { type: string, description: '`pending` frames only (server >= 7.133.0): the spread PendingCheckpoint row''s `checkpointId` (key absent when the row has no trusted value — read as "no joinable identity").' }
19932
+ consumerState: { type: string, enum: [absent], description: '`live_pending` frames only (server >= 7.133.0): the spread LivePendingRow''s true-only `consumerState` (no engine leg on this replica is waiting for the ask).' }
19933
+ errorCode: { type: string, description: '`error` frames: the machine code (`STREAM_MAX_DURATION` / `draining` / server >= 7.133.0 `approvals.read_unavailable` — the stream STAYS open, do NOT reconnect on it — / `approvals.read_corrupt` — a corrupt row was read, the server closes the stream after the frame).' }
19934
+ message: { type: string, description: '`error` frames: the human sentence (redacted); never match on it.' }
19768
19935
  hasBidiControls:
19769
19936
  type: boolean
19770
19937
  enum: [true]
@@ -19885,20 +20052,24 @@ components:
19885
20052
 
19886
20053
  FleetHookNotice:
19887
20054
  description: >
19888
- Hook observability payload, TWO kinds (service fleet-bus.ts `HookNotice` minus the wire-stripped
20055
+ Hook observability payload, FOUR kinds (server >= 7.133.0 added `hook_managed_unrun` and `hook_verdict_untrusted`; service fleet-bus.ts `HookNotice` minus the wire-stripped
19889
20056
  ownerScope/ownerSessionId — the payload view of FleetFrame_hook_notice; a discriminated union on
19890
20057
  `kind`, mirroring the SDK type verbatim). Both kinds are pure observe — never block, resume, or
19891
20058
  inject. Visibility is fail-CLOSED: a ?session= subscriber only receives notices provably owned by
19892
- that session. `kind` is a CLOSED two-member set on the server (exhaustive-switch discipline) but
20059
+ that session. `kind` is a CLOSED four-member set on the server (exhaustive-switch discipline) but
19893
20060
  OPEN ON READ: degrade an unknown future kind to a generic observability row, never crash.
19894
20061
  oneOf:
19895
20062
  - $ref: '#/components/schemas/FleetHookDecisionUnavailable'
19896
20063
  - $ref: '#/components/schemas/FleetHookNonBlockingFailure'
20064
+ - $ref: '#/components/schemas/FleetHookManagedUnrun'
20065
+ - $ref: '#/components/schemas/FleetHookVerdictUntrusted'
19897
20066
  discriminator:
19898
20067
  propertyName: kind
19899
20068
  mapping:
19900
20069
  hook_decision_unavailable: '#/components/schemas/FleetHookDecisionUnavailable'
19901
20070
  hook_non_blocking_failure: '#/components/schemas/FleetHookNonBlockingFailure'
20071
+ hook_managed_unrun: '#/components/schemas/FleetHookManagedUnrun'
20072
+ hook_verdict_untrusted: '#/components/schemas/FleetHookVerdictUntrusted'
19902
20073
 
19903
20074
  FleetHookDecisionUnavailable:
19904
20075
  type: object
@@ -19933,7 +20104,7 @@ components:
19933
20104
  reason:
19934
20105
  # 🔴 NO `enum` — same open-on-read ruling as kind 1 (SDK type is `... | (string & {})`).
19935
20106
  type: string
19936
- description: 'Machine-branchable failure shape. CLOSED server-side, OPEN ON READ — KNOWN WORDS TODAY: exit_nonzero | timeout | spawn_failed | bad_json.'
20107
+ description: 'Machine-branchable failure shape. CLOSED server-side, OPEN ON READ — KNOWN WORDS TODAY: exit_nonzero | timeout | spawn_failed | bad_json | matcher_over_budget (server >= 7.133.0: the group matcher / the entry `if` name part did not finish evaluating within its 1000 ms budget on this name — NOT a broken hook script but an unevaluable selection condition; the server treats it as selected, the entry still runs; ONE frame per group, `if` one per entry, no exitCode / stderr).'
19937
20108
  hookName: { type: string, description: 'Hook identity (statusMessage, else command/URL/prompt; redacted + bounded, http entries stripped of query/fragment). Always sent.' }
19938
20109
  entryType: { type: string, description: 'command | http | prompt | agent — without it, hookName''s nature is a guess. Closed server-side, open on read. Always sent.' }
19939
20110
  exitCode: { type: integer, description: 'Absent for timeout/spawn_failed (no exit code exists — the server never mints a legit-looking 0).' }
@@ -19941,6 +20112,57 @@ components:
19941
20112
  stderr: { type: string, description: 'Redacted + truncated stderr digest (512-char cap incl. marker). Empty stderr => absent.' }
19942
20113
  detail: { type: string, description: 'Redacted supplement (e.g. where an exit-2 had no venue). Additive / tolerate-absent.' }
19943
20114
 
20115
+ FleetHookManagedUnrun:
20116
+ type: object
20117
+ description: >
20118
+ Kind 3 (server >= 7.133.0): a MANAGED hook did not complete — the group carries a `source`; managed groups (`managedPolicy` / `managedPlugin:<name>`) are the organization policy's guard and run in an independent lane
20119
+ after the unmanaged rewrites, on the final input. A synchronous managed entry that gave neither a legal verdict (exit 0 with no verdict / a closed-set verdict) nor a legal veto (exit 2 / `decision:"block"` /
20120
+ `continue:false`) is "unrun": on `PreToolUse` the conclusion folds to `ask` when nothing vetoed (a person in the loop; unmanaged allows are not trusted; any phase's veto is still the last vote), on other events the
20121
+ conclusion is unchanged — but this frame is ALWAYS sent. One frame per unrun managed entry. The frame does NOT assert how the call ended (read the tool / approval frames).
20122
+ required: [kind, event, reason, source, hookName, entryType]
20123
+ additionalProperties: true
20124
+ properties:
20125
+ kind: { type: string, enum: [hook_managed_unrun] }
20126
+ event: { type: string, description: 'Which hook event. On PreToolUse this entry sets the managed lane: with no veto the call folds to `ask`.' }
20127
+ reason:
20128
+ # NO `enum` — same open-on-read ruling as kinds 1 and 2.
20129
+ type: string
20130
+ description: >-
20131
+ Why it did not complete. CLOSED on the server (thirteen words), OPEN ON READ — KNOWN WORDS TODAY: matcher_invalid | llm_carrier_unavailable | llm_cap | powershell_unsupported | if_unevaluable | budget_exhausted |
20132
+ spawn_failed | timeout | exit_nonzero | bad_json | decision_out_of_set | evidence_unavailable | rewrite_unsettled. `input_truncated` / `matcher_over_budget` are NOT in this kind (they belong to kind 4).
20133
+ source: { type: string, description: 'The group''s source word (`managedPolicy` or `managedPlugin:<name>`; the name is trace only, never a criterion).' }
20134
+ hookName: { type: string, description: 'Hook identity (same rule as kind 2); redacted + bounded.' }
20135
+ entryType: { type: string, description: 'command | http | prompt | agent — closed server-side, open on read.' }
20136
+ toolName: { type: string, description: 'Present on tool events.' }
20137
+ detail: { type: string, description: 'Redacted supplement.' }
20138
+
20139
+ FleetHookVerdictUntrusted:
20140
+ type: object
20141
+ description: >
20142
+ Kind 4 (server >= 7.133.0): a hook's VERDICT IS UNTRUSTED — one rule: an entry that did not get the complete payload, or was pulled in BY SELECTION by an over-budget matcher, has its VETO count (deny / exit 2 /
20143
+ `decision:"block"` / `continue:false` / ask) and its ALLOW and `updatedInput` NOT count. The rule holds on both lanes, so there is one frame; reason, lane and identity are machine fields. Emission condition per lane:
20144
+ managed = the entry ran with an untrusted cause (sent whatever it judged; `PreToolUse` then folds to `ask` when nothing vetoed); unmanaged = it gave an allow or a real rewrite (an identity echo does not count) that was
20145
+ dropped. Today only on `PreToolUse` (the event with an allow word). THE FRAME DOES NOT ASSERT HOW THE CALL ENDED: a managed entry that itself vetoes ends in deny with the frame sent; another trusted allow in the group still
20146
+ lets the call through — read the call's own result. NEVER infer a call outcome from any hook_notice frame.
20147
+ required: [kind, event, reason, lane, hookName, entryType]
20148
+ additionalProperties: true
20149
+ properties:
20150
+ kind: { type: string, enum: [hook_verdict_untrusted] }
20151
+ event: { type: string, description: 'Which hook event (today always PreToolUse).' }
20152
+ reason:
20153
+ # NO `enum` — same open-on-read ruling as kinds 1–3.
20154
+ type: string
20155
+ description: >-
20156
+ Why the verdict is untrusted. CLOSED on the server (two words), OPEN ON READ — KNOWN WORDS TODAY: input_truncated (the entry did not get the full payload: `tool_input` over the cap was replaced by a truncation marker, or a prompt /
20157
+ agent entry''s whole payload text was cut at the same cap) | matcher_over_budget (the group matcher / entry `if` name part ran past its budget and the entry ran BY SELECTION; the same moment also sends a kind-2 frame
20158
+ `matcher_over_budget`).
20159
+ lane: { type: string, description: 'The lane the entry sits in, read off the source table: `managedPolicy` / `managedPlugin:*` => `managed`, everything else and absent => `unmanaged`. Closed (two words) server-side, open on read. Read THIS field, never re-derive from `source`.' }
20160
+ hookName: { type: string, description: 'Hook identity (same rule as kind 2); redacted + bounded.' }
20161
+ entryType: { type: string, description: 'command | http | prompt | agent — closed server-side, open on read.' }
20162
+ toolName: { type: string, description: 'Present on tool events.' }
20163
+ source: { type: string, description: 'The source word the group declared (both lanes give it; ABSENT = the group declared none, so only unmanaged). Trace only, never a criterion.' }
20164
+ detail: { type: string, description: 'Redacted supplement (human-facing; machines read `reason` / `lane`).' }
20165
+
19944
20166
  BakeStatus:
19945
20167
  # 新(census 批2 四段)——server `BakeStatus`(plugins/store-contracts.ts), the COARSE lifecycle
19946
20168
  # bakeView/claimResponse/bakesCreate all key on.
@@ -20450,6 +20672,11 @@ components:
20450
20672
  K-24 (server >= 7.104.0 / S-724) — does this leg capture file history (`on` / `disabled`, OPEN word). EFFECTIVE
20451
20673
  HALF ONLY ⇒ ALWAYS ABSENT ON THIS ENDPOINT; the deployment-level reading is `capabilities.fileHistoryCapture`.
20452
20674
  Full per-key contract: see Event_wiring_manifest.fileHistory.
20675
+ permissionMode:
20676
+ allOf: [{ $ref: '#/components/schemas/WiringManifestPermissionMode' }]
20677
+ description: >-
20678
+ core 7.53.0, server >= 7.133.0 — the leg's permission-mode receipt (section shape WiringManifestPermissionMode). EFFECTIVE HALF ONLY ⇒ this (static) face NEVER carries it, and absence here carries no
20679
+ information; the reading holds only on a live `wiring_manifest` frame.
20453
20680
  instructions:
20454
20681
  allOf: [{ $ref: '#/components/schemas/WiringManifestInstructions' }]
20455
20682
  description: >
@@ -20521,6 +20748,24 @@ components:
20521
20748
  type: array
20522
20749
  description: 'The selected chain member(s), spelled as the chain spells them (relative to the cwd); `[]` = the chain was walked, nothing was selected.'
20523
20750
  items: { type: string, enum: ['CLAUDE.md', '.claude/CLAUDE.md', 'AGENTS.md', '.claude/AGENTS.md'] }
20751
+ WiringManifestPermissionMode:
20752
+ # server 7.133.0 / core 7.53.0:同一段形被 live 帧(两张脸)、静态诊断面、`POST /v1/tasks` 的 `wiringManifest` 回执与账本行引用 ⇒ 具名单源(`spec-field-drift-gate` 也按「导出的 interface 必有同名 schema」逼出它)。
20753
+ type: object
20754
+ additionalProperties: false
20755
+ description: >
20756
+ The `wiring_manifest.permissionMode` section (core 7.53.0 `PermissionModeReceipt`; server >= 7.133.0 projects it on BOTH faces — tenant and operator — and the same section rides the `wiringManifest`
20757
+ receipt of the `POST /v1/tasks` 200 body and the `/events` ledger row; the server judges SHAPE only: `effective` a non-empty string, `requested` / `foldedBy` absent or non-empty strings, else the whole section
20758
+ is dropped). The engine gives the same three-member receipt on EVERY mode question: `requested` = the mode word the caller declared (ABSENT = declared nothing — a root inherits `default`, a child inherits its
20759
+ parent); `effective` = the mode the leg actually runs under (the request clamped under a parent's ceiling, then folded to `default` where the deployment denies it or the classifier face is absent) — ALWAYS present,
20760
+ the shell renders its mode indicator from it; `foldedBy` = PRESENT IFF `requested` and `effective` differ — `operator_deny` (the deployment / organization ceiling denies the mode) / `face_absent` (`auto` needs
20761
+ a classifier face this leg lacks) / `inherited_ceiling` (a delegated child clamped under its parent's ceiling); read OPEN (a fourth word passes through as "folded, cause unknown"). ABSENT section = an older
20762
+ engine / server (<= 7.132.x), not an assertion of the default mode. This is ONE added key on a closed object: a consumer validating `wiring_manifest` strictly against sdk <= 24.0.0's spec fails the whole frame
20763
+ on 7.133.0+ — upgrade to sdk 25.0.0 or relax unknown keys on this section.
20764
+ required: [effective]
20765
+ properties:
20766
+ requested: { type: string, description: 'The mode word the caller declared (a PermissionMode word today; read open). ABSENT = declared nothing.' }
20767
+ effective: { type: string, description: 'The mode the leg actually runs under (a PermissionMode word today; read open). Always present.' }
20768
+ foldedBy: { type: string, description: 'Present iff `requested` and `effective` differ: `operator_deny` / `face_absent` / `inherited_ceiling` (engine words; read OPEN — an unknown word means "folded, cause unknown").' }
20524
20769
  WiringManifestModEntry:
20525
20770
  # server 7.132.0 / core 7.52.0:取代钩子席位行形(删,零别名)。同一份行形被 live 帧与静态诊断面两处引用 ⇒ 具名;两形各自具名(⑤bis 名单逐形对账)。
20526
20771
  description: >
@@ -21050,10 +21295,225 @@ components:
21050
21295
  remember for this session's category (fs-write tools share ONE category: approving Write covers
21051
21296
  Edit/NotebookEdit; other tools are per-tool) — the CC "don't ask again this session" semantic.
21052
21297
 
21298
+ ConfigCatalog:
21299
+ type: object
21300
+ description: >
21301
+ The 200 body of GET /v1/config/catalog (server `ConfigCatalogWire`) — a PROJECTION, left open on read (the producer ships independently and adds keys additively between SDK releases; the request side has no body).
21302
+ required: [catalogVersion, envLaneTiming, restartSlices, centerDomains, summary, rows, retired]
21303
+ additionalProperties: true
21304
+ properties:
21305
+ catalogVersion: { type: integer, enum: [1] }
21306
+ envLaneTiming: { type: string, description: 'The env lane''s uniform timing (`deploy`: env is immutable at runtime, so changing env = changing the deployment). Open on read.' }
21307
+ restartSlices: { type: array, items: { type: string }, description: 'The restart-slice names a centre write can land in (open on read).' }
21308
+ centerDomains: { type: array, items: { type: string }, description: 'The contract package''s centre-configuration domains.' }
21309
+ summary:
21310
+ type: object
21311
+ required: [total, envHeld, secret, centerManaged]
21312
+ additionalProperties: true
21313
+ properties:
21314
+ total: { type: integer }
21315
+ envHeld: { type: integer }
21316
+ secret: { type: integer }
21317
+ centerManaged: { type: integer }
21318
+ rows: { type: array, items: { $ref: '#/components/schemas/ConfigCatalogRow' } }
21319
+ retired: { type: array, items: { $ref: '#/components/schemas/ConfigCatalogRetiredEnv' }, description: 'Tombstoned env names this process refuses to start on (always present, may be empty). A name is never in `rows` and here at once.' }
21320
+ ConfigCatalogRow:
21321
+ type: object
21322
+ description: >
21323
+ One catalog row (server `ConfigCatalogRow`): a deployment knob's static declaration plus this process's effective reading. OPEN on read (projection). The two VERDICT bits are the true-only family:
21324
+ `effectiveValueInvalid` (the effective value fell outside the row's echo set; `effectiveValue` is `null`) and `effectiveValueWithheld` (server >= 7.133.0) — see the property.
21325
+ required: [name, domain, type, semantics, writeLanes, effectiveLane, envHeld, envTiming, effectiveValue, valueClass, danger]
21326
+ additionalProperties: true
21327
+ properties:
21328
+ name: { type: string, description: 'The knob (env) name; the catalog is sorted by it.' }
21329
+ domain: { type: string, description: 'One of the nine configuration groups (store / modelPlane / approval / memory / auth / orchestration / limitsHttp / observability / integrations). Open on read.' }
21330
+ type: { type: string, description: 'Value type: boolean | number | string | enum | csv | json | path | url | secret | opaque. Open on read.' }
21331
+ semantics: { type: string, description: 'Human sentence (true to the read site).' }
21332
+ writeLanes: { type: array, items: { type: string, enum: [env, center] }, description: 'Which lanes can write this knob.' }
21333
+ effectiveLane: { type: string, description: 'The lane actually in force: env | center | default. Open on read.' }
21334
+ envHeld: { type: boolean, description: 'This machine''s env sets it explicitly.' }
21335
+ envTiming: { type: string, description: '`deploy` (changing env = changing the deployment) | `inert` (retired / zero-consumer key). Open on read.' }
21336
+ staticDefault: { type: string }
21337
+ derivedDefaultNote: { type: string }
21338
+ effectiveValue:
21339
+ description: 'This process''s effective DISPLAY value: a scalar / a word list / `null` (unset) / `{ present, length? }` (`secret` / `opaque` rows report presence and length only).'
21340
+ oneOf:
21341
+ - { type: [string, number, boolean, 'null'] }
21342
+ - { type: array, items: { type: string } }
21343
+ - { type: object, required: [present], additionalProperties: true, properties: { present: { type: boolean }, length: { type: integer } } }
21344
+ valueClass: { type: string, description: '`plain` (echoable) | `secret` | `opaque` (the value never goes on the wire). Open on read.' }
21345
+ danger: { type: array, items: { type: string }, description: 'The risk axes of a wrong setting: security | billing | data (open on read).' }
21346
+ center: { $ref: '#/components/schemas/ConfigCatalogRowCenter' }
21347
+ centerDomain: { type: string }
21348
+ centerPath: { type: string }
21349
+ scope: { type: string, description: '`bootstrap-only` = read only at start-up.' }
21350
+ enumValues: { type: array, items: { type: string }, description: 'An `enum` row''s writable word set.' }
21351
+ effectiveValueInvalid:
21352
+ type: boolean
21353
+ enum: [true]
21354
+ description: 'TRUE-ONLY verdict bit: the effective value fell OUTSIDE the row''s own echo set (or a boolean without a declared mapping) — `effectiveValue` is then `null` (echoing an out-of-set value would teach a consumer to write back an illegal word). Enum rows only. ABSENT = this cell has no such matter.'
21355
+ effectiveValueWithheld:
21356
+ type: boolean
21357
+ enum: [true]
21358
+ description: >-
21359
+ server >= 7.133.0, TRUE-ONLY verdict bit on `type: url` rows (present only when true, never `false`): the display mint judged this cell (single value) or any item of it (list) WITHHELD — a part the position rule
21360
+ would have shown (the whole query / a path segment) was NOT shown because it held a credential-shaped piece. The mint owns the verdict; the catalog does not judge again. So the shown value is not the whole value:
21361
+ a catalog page / `sema config show` marks the row ("part of this value is withheld"), and must not read the shown value as "the value is exactly that". ABSENT = nothing withheld (older servers never send it).
21362
+ The standard position rule (userinfo landed, query values become tokens) holds for every row and is NOT a withholding; a value that became a placeholder as a whole means the row's display rule cannot show it
21363
+ (not a hierarchical URL; the two outbound-proxy rows show only scheme://host[:port]) — also not this bit.
21364
+ ConfigCatalogRowCenter:
21365
+ type: object
21366
+ description: 'A catalog row''s centre seat: whether the knob is centre-publishable, who wins, when it takes effect. Present only on rows with a centre seat.'
21367
+ required: [domain, path, precedence, timing, managedNow, effectiveNow]
21368
+ additionalProperties: true
21369
+ properties:
21370
+ domain: { type: string }
21371
+ path: { type: string, description: 'Path in the domain (`a.b` into objects; `xs[].c` into array elements).' }
21372
+ precedence: { type: string, description: '`center-wins` | `env-wins` (open on read).' }
21373
+ timing: { type: string, description: '`hot` | `restart` | `inert` (open on read; not `deploy`).' }
21374
+ restartSlice: { type: string }
21375
+ restartReason: { type: string }
21376
+ managedNow: { type: boolean, description: 'The centre''s latest effective config currently MANAGES this key (no config pipeline => always false).' }
21377
+ effectiveNow: { type: boolean, description: 'The centre write is in force right now (managed AND not held down by an env placeholder).' }
21378
+ ConfigCatalogRetiredEnv:
21379
+ type: object
21380
+ description: 'A tombstoned env name (the contract package''s RETIRED_ENVS wire projection): a name this process refuses to start on. `renamed` carries only `replacement`; `replaced` carries `replacement` + `reason` (how the value semantics changed); `removed` carries only `reason`.'
21381
+ required: [name, kind]
21382
+ additionalProperties: true
21383
+ properties:
21384
+ name: { type: string }
21385
+ kind: { type: string, description: '`renamed` | `replaced` | `removed` (open on read).' }
21386
+ replacement: { type: string }
21387
+ reason: { type: string }
21388
+ EntryProblem:
21389
+ type: object
21390
+ description: >
21391
+ One ENTRY PROBLEM (server >= 7.133.0): a refusal body's `detail.problems[]` element and an accept-time `warnings[]` element share this shape. `code` = the refusal code the problem belongs to (in `warnings[]`: the engine's
21392
+ reading code for that entry); `field` = the coordinate of the list holding the entry in the request body (`settings.permissions.allow`, `skills[i].allowedTools` …); `entry` = the entry string (passed through the server''s
21393
+ redaction mouth — not a guarantee of your original bytes); `reason` = the machine reason — mostly the ENGINE''s word verbatim (why it read the entry as nothing); the server mints two of its own, `not_in_roster` (accept-time
21394
+ warnings only: the name in an allow-side selector list is not in the roster and is no protocol namespace name — the engine accepts it anyway) and `home_unresolvable` (settings-rule 422: a `~/` path rule on a deployment that cannot
21395
+ resolve the execution environment''s home); and `below_trust_floor` (the caller''s trust tier is below the member''s admission floor — see ErrorResponse.detail.callerTrust). Read OPEN.
21396
+ required: [code, field, entry, reason]
21397
+ additionalProperties: false
21398
+ properties:
21399
+ code: { type: string }
21400
+ field: { type: string }
21401
+ entry: { type: string }
21402
+ reason: { type: string }
21403
+ PrincipalInboxMessage:
21404
+ type: object
21405
+ description: 'The request body of POST /v1/principal-inbox/messages: a CLOSED three-key shape (`from` is minted by the server from the verified identity and cannot be sent).'
21406
+ required: [to, content]
21407
+ additionalProperties: false
21408
+ properties:
21409
+ to: { type: string, minLength: 1, description: 'The recipient principal string. Must belong to the sender''s organization (the membership judgement reads the organization directory); a failed judgement is 404 `org_delivery.not_member`, byte-identical to "no such person".' }
21410
+ content: { type: string, minLength: 1, maxLength: 32000, description: 'The letter body (characters; over 32000 => 400 `request.field_invalid`).' }
21411
+ urgent: { type: boolean, description: 'The urgent flag on the envelope (absent = ordinary).' }
21412
+ PrincipalInboxReceipt:
21413
+ type: object
21414
+ description: 'The 200 receipt: the letter was appended. `receiptId` = the gate-minted delivery receipt id (the envelope''s `gateReceiptId` carries the same value).'
21415
+ required: [receiptId]
21416
+ additionalProperties: false
21417
+ properties:
21418
+ receiptId: { type: string }
21419
+ OrgDeliveryRefusalCode:
21420
+ type: string
21421
+ enum: [org_delivery.not_member, org_delivery.rate_limited, org_delivery.inbox_full, org_delivery.recipient_tombstoned]
21422
+ description: >
21423
+ The four cross-principal delivery refusal codes (server >= 7.133.0; the engine's closed `ORG_DELIVERY_REFUSAL_CODES`, one frozen sentence each — machines read the CODE): `org_delivery.not_member` (404),
21424
+ `org_delivery.rate_limited` (429), `org_delivery.inbox_full` (409), `org_delivery.recipient_tombstoned` (410). See POST /v1/principal-inbox/messages.
21425
+ ToolApprovalReadState:
21426
+ type: string
21427
+ enum: [pending, decided, parked, void]
21428
+ description: >
21429
+ `state` of GET /v1/tool-approvals/{id} (server >= 7.133.0), a CLOSED four-word set — read it exhaustively: `pending` = a live registration on this replica is waiting (answer door open);
21430
+ `decided` = the row is DECIDED; `parked` = PARKING / PARKED (a late answer redeems the parked card); `void` = DENIED / VOID (nobody decided it, the answer has nowhere to land).
21431
+ ApprovalsStreamReadErrorCode:
21432
+ type: string
21433
+ enum: [approvals.read_unavailable, approvals.read_corrupt]
21434
+ description: >
21435
+ The `errorCode` of the two READ-FAILURE `error` frames on GET /v1/approvals/stream (server >= 7.133.0). `approvals.read_unavailable` = this tick's approval read failed (the store was momentarily
21436
+ unreadable) — the stream STAYS open and the next tick reads again; it is NOT a terminal frame and NOT a reconnect signal (do not reconnect on it, and do not read it as "nothing pending").
21437
+ `approvals.read_corrupt` = a corrupt row was read — the server closes the stream after the frame; deal with that row before reopening. Branch on the code, never match the message.
21438
+ Older servers (<= 7.132.x) answer a read failure with nothing but heartbeats.
21439
+ ToolApprovalReadProjection:
21440
+ type: object
21441
+ description: >
21442
+ The row projection shared by the three non-`pending` arms of GET /v1/tool-approvals/{id} (server >= 7.133.0): a SUBSET of the `livePending[]` row keys — one reader, one key set. `frame` = the disconnect-replay
21443
+ opening frame (an `approval_request` frame; ABSENT when the card cannot be read back — never invented).
21444
+ required: [approvalId, ts, expiresAtMs]
21445
+ additionalProperties: true
21446
+ properties:
21447
+ approvalId: { type: string }
21448
+ ts: { type: number, description: 'Registration time (ms epoch).' }
21449
+ expiresAtMs: { type: number, description: 'Absolute window deadline (cast once, never recomputed).' }
21450
+ sessionId: { type: string }
21451
+ toolName: { type: string }
21452
+ frame: { $ref: '#/components/schemas/ApprovalRequestFrame' }
21453
+ ToolApprovalReadBody:
21454
+ description: >
21455
+ The 200 body of GET /v1/tool-approvals/{id} (server >= 7.133.0) — a four-arm union discriminated by `state` (closed, read it exhaustively): `pending` = a live registration on this replica is waiting
21456
+ (the `livePending[]` row + `state`; answer door open); `decided` = the row is DECIDED (`decision` / `decidedAtMs` / `decisionNote?`, the same three keys as the late-replay body); `parked` = PARKING / PARKED
21457
+ (a late answer redeems the parked card; `decidePath?` points at the durable decision path); `void` = DENIED / VOID (nobody decided it, the answer has nowhere to land; `cause?` is the same cause word as the
21458
+ late-answer 404's, see ToolApprovalNotPendingCause).
21459
+ 🔴 server >= 7.134.0: a PARKED row passes the gate judge (the same one the answer door's 409 `approval_stale` reads) — the gate still open ⇒ `parked`
21460
+ (+ `decidePath`; ONLY this state renders "go decide"); the gate settled by a person / policy ⇒ `decided` (no `decidePath`); the gate expired (a cancel expires it
21461
+ first) / superseded / settled by the host SLA sweep, the window or nobody-to-answer ⇒ `void` WITHOUT `cause`; the checkpoint row gone, or settled without a
21462
+ readable direction / time ⇒ 404 `tool_approval.not_pending` WITHOUT `cause`; the gate unreadable (store failure) ⇒ falls back to `parked`. Known boundary (re-read
21463
+ to settle it): a parked row's `decided` can return to `parked` when the resume leg fails before the approved action starts (the engine reopens the same gate), and
21464
+ a cancelled resume reads `void` once the server collects the reopened gate (`parked` in between, or `void` for an instant during the reopen).
21465
+ Older servers answered all of these with `parked` + `decidePath`.
21466
+ oneOf:
21467
+ - $ref: '#/components/schemas/ToolApprovalReadPending'
21468
+ - $ref: '#/components/schemas/ToolApprovalReadDecided'
21469
+ - $ref: '#/components/schemas/ToolApprovalReadParked'
21470
+ - $ref: '#/components/schemas/ToolApprovalReadVoid'
21471
+ discriminator:
21472
+ propertyName: state
21473
+ mapping:
21474
+ pending: '#/components/schemas/ToolApprovalReadPending'
21475
+ decided: '#/components/schemas/ToolApprovalReadDecided'
21476
+ parked: '#/components/schemas/ToolApprovalReadParked'
21477
+ void: '#/components/schemas/ToolApprovalReadVoid'
21478
+ ToolApprovalReadPending:
21479
+ allOf:
21480
+ - $ref: '#/components/schemas/LivePendingRow'
21481
+ - type: object
21482
+ required: [state]
21483
+ properties:
21484
+ state: { type: string, enum: [pending] }
21485
+ ToolApprovalReadDecided:
21486
+ allOf:
21487
+ - $ref: '#/components/schemas/ToolApprovalReadProjection'
21488
+ - type: object
21489
+ required: [state, decision, decidedAtMs]
21490
+ properties:
21491
+ state: { type: string, enum: [decided] }
21492
+ decision: { type: string, enum: [allow, deny] }
21493
+ decidedAtMs: { type: integer }
21494
+ decisionNote: { type: string }
21495
+ ToolApprovalReadParked:
21496
+ allOf:
21497
+ - $ref: '#/components/schemas/ToolApprovalReadProjection'
21498
+ - type: object
21499
+ required: [state]
21500
+ properties:
21501
+ state: { type: string, enum: [parked] }
21502
+ decidePath: { type: string, description: 'The durable decision path for this ask when it can be built.' }
21503
+ ToolApprovalReadVoid:
21504
+ allOf:
21505
+ - $ref: '#/components/schemas/ToolApprovalReadProjection'
21506
+ - type: object
21507
+ required: [state]
21508
+ properties:
21509
+ state: { type: string, enum: [void] }
21510
+ cause:
21511
+ allOf: [{ $ref: '#/components/schemas/AskRowEndingCause' }]
21512
+ description: 'The row''s recorded ending cause (absent for a legacy row with no cause written — never invented).'
21053
21513
  ToolApprovalRespondApplied:
21054
21514
  type: object
21055
21515
  description: >
21056
- The APPLIED arm of the 200 ack of POST /v1/tool-approvals/{approvalId}/respond (server tool-approval.ts — exact
21516
+ The APPLIED arm of the 200 ack of POST /v1/tool-approvals/{id}/respond (server tool-approval.ts — exact
21057
21517
  shape, `decision` echoed): the decision settled a live gate. Owner-gated with a 404 and NO existence oracle: non-owner and
21058
21518
  unknown/settled/expired/wrong-replica ids are indistinguishable. See ToolApprovalRespondAck for the replayed arm.
21059
21519
  # 封闭(census 批2 第三段,2026-07-30):铸造点 `tool-approval.ts` 三恒在键 + 两条件键
@@ -21069,6 +21529,13 @@ components:
21069
21529
  approvalId: { type: string }
21070
21530
  delivery: { type: string, enum: [applied] }
21071
21531
  decision: { $ref: '#/components/schemas/ToolApprovalDecision' }
21532
+ consumerState:
21533
+ type: string
21534
+ enum: [absent]
21535
+ description: >-
21536
+ server >= 7.133.0, TRUE-ONLY: no engine leg on THIS replica was waiting for the ask this answer settled — the same bit, with the same meaning, as `LivePendingRow.consumerState` and the
21537
+ durable decision ack's. Present <=> the answered entry was ADOPTED from the ask ledger at start-up. The decision lands on the row as usual, but nothing running here resumes because of it — read the run
21538
+ surface; do not read the 200 as "the run continued". Absent = a local engine leg was waiting, or an older server.
21072
21539
  rememberApplied:
21073
21540
  type: boolean
21074
21541
  description: >
@@ -21160,7 +21627,7 @@ components:
21160
21627
  ToolApprovalRespondReplayed:
21161
21628
  type: object
21162
21629
  description: >
21163
- The REPLAYED arm of the 200 ack of POST /v1/tool-approvals/{approvalId}/respond: the ask was already DECIDED
21630
+ The REPLAYED arm of the 200 ack of POST /v1/tool-approvals/{id}/respond: the ask was already DECIDED
21164
21631
  (by anyone) when this answer arrived, so the server replays the FIRST decision — `decision` is the row's two-word
21165
21632
  value (`allow_session` was folded to `allow` when it landed), `decidedAtMs` the first decision's clock,
21166
21633
  `decisionNote` only when the row has one, `rememberApplied: false` only when THIS request asked `allow_session`.
@@ -21176,14 +21643,57 @@ components:
21176
21643
  decidedAtMs: { type: integer }
21177
21644
  decisionNote: { type: string }
21178
21645
  rememberApplied: { type: boolean, enum: [false] }
21646
+ noteRecorded: { type: boolean, description: 'server >= 7.133.0 (two doors, one shape): present ONLY when THIS request carried a `note`; `true` <=> the row''s text is verbatim what this request submitted, `false` = not confirmed (another reason is on the row / no store / read-back failed). A late answer''s note never overwrites the first decision''s reason — that is `decisionNote`.' }
21647
+ rulePersisted: { type: boolean, enum: [false], description: 'server >= 7.133.0: present (as `false`) only when this late answer carried `persistRule`: the rule is redeemed on the FIRST decision only, a late answer never saves one.' }
21648
+ ruleRefusal:
21649
+ type: string
21650
+ enum: [rule_lane_unavailable]
21651
+ description: 'with `rulePersisted: false`; on this arm always `rule_lane_unavailable` (the only word the published late-answer path mints).'
21652
+ ToolApprovalRespondParkedRedeemed:
21653
+ type: object
21654
+ description: >
21655
+ The PARKED-REDEEMED arm of the 200 ack of POST /v1/tool-approvals/{id}/respond (server >= 7.133.0; server tool-approval.ts `redeemParkedAsk` /
21656
+ `replayRedeemReceipt`): the card's window had run out and the ask moved to background waiting (PARKED); this late answer — on another replica or after a restart —
21657
+ goes through the EXISTING durable redemption leg (the same CAS as POST /v1/approvals/{sessionId}/decide) instead of the old 404 `tool_approval.not_pending`
21658
+ (`cause: unknown_or_other_replica`). Body = the redemption leg's receipt (ApprovalDecisionResult's keys — `status` / `taskId` / `sessionId` / ... as they come) plus the three keys
21659
+ this door adds: `approvalId` / `askId` (the same 64-hex value) and the discriminant `accepted`. `decision` is the two-word direction on the REPLAY form only (`allow` / `deny`,
21660
+ `allow_session` folded to `allow`); `rememberApplied: false` appears only when this request asked `allow_session` (a parked redemption cannot remember a session grant);
21661
+ `rulePersisted` / `ruleRefusal` / `persistedRule*` are the `persistRule` outcome on the redemption leg (a rule that fails to store NEVER flips the verdict).
21662
+ `idempotent: true` appears only on the REPLAY (the same already-redeemed card answered again returns the first redemption's receipt).
21663
+ CLOSED like its two sibling arms (the census keeps this op's 200 body closed): the key set is ApprovalDecisionResult's own keys (one source per shape, by `$ref`) plus the added ones.
21664
+ required: [approvalId, askId, accepted]
21665
+ additionalProperties: false
21666
+ properties:
21667
+ approvalId: { type: string }
21668
+ askId: { type: string }
21669
+ accepted: { type: string, enum: [parked_redeemed] }
21670
+ status: { $ref: '#/components/schemas/ApprovalDecisionResult/properties/status' }
21671
+ taskId: { $ref: '#/components/schemas/ApprovalDecisionResult/properties/taskId' }
21672
+ sessionId: { $ref: '#/components/schemas/ApprovalDecisionResult/properties/sessionId' }
21673
+ bindingEnforced: { $ref: '#/components/schemas/ApprovalDecisionResult/properties/bindingEnforced' }
21674
+ decision: { type: string, description: 'REPLAY form: `allow` / `deny` (`allow_session` folded to `allow`). FIRST redemption: whatever the redemption leg''s own receipt says (`approve` / `deny` today) — read it open.' }
21675
+ errorCode: { $ref: '#/components/schemas/ApprovalDecisionResult/properties/errorCode' }
21676
+ errorMessage: { $ref: '#/components/schemas/ApprovalDecisionResult/properties/errorMessage' }
21677
+ retriable: { $ref: '#/components/schemas/ApprovalDecisionResult/properties/retriable' }
21678
+ idempotent: { $ref: '#/components/schemas/ApprovalDecisionResult/properties/idempotent' }
21679
+ executionOutcome: { $ref: '#/components/schemas/ApprovalDecisionResult/properties/executionOutcome' }
21680
+ rememberApplied: { type: boolean, enum: [false] }
21681
+ rulePersisted: { $ref: '#/components/schemas/ToolApprovalRespondApplied/properties/rulePersisted' }
21682
+ ruleRefusal: { $ref: '#/components/schemas/ToolApprovalRespondApplied/properties/ruleRefusal' }
21683
+ persistedRule: { $ref: '#/components/schemas/ToolApprovalRespondApplied/properties/persistedRule' }
21684
+ persistedRules: { $ref: '#/components/schemas/ToolApprovalRespondApplied/properties/persistedRules' }
21685
+ persistedRuleAnchors: { $ref: '#/components/schemas/ToolApprovalRespondApplied/properties/persistedRuleAnchors' }
21179
21686
  ToolApprovalRespondAck:
21180
21687
  description: >
21181
- The 200 ack of POST /v1/tool-approvals/{approvalId}/respond — a discriminated union: the decision was APPLIED to a live
21688
+ The 200 ack of POST /v1/tool-approvals/{id}/respond — a discriminated union: the decision was APPLIED to a live
21182
21689
  gate (ToolApprovalRespondApplied, `delivery: "applied"`) or the ask was already decided and the first decision is
21183
- REPLAYED (ToolApprovalRespondReplayed, `idempotent: true`). The discriminant is `idempotent`.
21690
+ REPLAYED (ToolApprovalRespondReplayed, `idempotent: true`), or (server >= 7.133.0, sdk 25.0.0) the answer REDEEMED a PARKED card through the durable redemption
21691
+ leg (ToolApprovalRespondParkedRedeemed, `accepted: "parked_redeemed"`). The discriminants are `delivery` / `idempotent` / `accepted` (a replayed redemption carries
21692
+ BOTH `accepted: "parked_redeemed"` and `idempotent: true` — read `accepted` first).
21184
21693
  oneOf:
21185
21694
  - $ref: '#/components/schemas/ToolApprovalRespondApplied'
21186
21695
  - $ref: '#/components/schemas/ToolApprovalRespondReplayed'
21696
+ - $ref: '#/components/schemas/ToolApprovalRespondParkedRedeemed'
21187
21697
  UsageMetric:
21188
21698
  type: string
21189
21699
  enum: [tasks, tokensIn, tokensOut, costUsd]
@@ -21346,74 +21856,6 @@ components:
21346
21856
  delivery: { type: string, const: applied, description: 'Literal "applied" on today''s server.' }
21347
21857
  marker: { type: string, description: 'Engine-minted delivery marker.' }
21348
21858
 
21349
- WorkspaceSnapshotPage:
21350
- # 4.3.0(CAPS-OPS-8):把 GET …/workspace 的 200 信封提成具名 schema。SDK 侧此前是 `list()` 返回位上的
21351
- # **内联闭合字面量**,两道 spec 门(field-drift 只扫 types.ts / resource-schema 只扫 resources 的 exported
21352
- # type)对方法返回位的内联形都是盲的 —— 于是它漏了这里一直标为 required 的 `sessionId`,CI 全绿。
21353
- type: object
21354
- description: >
21355
- GET /v1/sessions/{sessionId}/workspace 的 200 信封(server src/http/routes/sessions.ts)。
21356
- ⚠️ `total` = **本页行数**(`snapshots.length`),不是快照总数,且这条腿没有任何分页游标 —— 与同族
21357
- `WorkspaceTreePage.total`(真·全量)**同名反义**。`latest` 与展示行同源(防 ghost key)。
21358
- additionalProperties: false
21359
- required: [sessionId, snapshots, total]
21360
- properties:
21361
- sessionId: { type: string }
21362
- snapshots:
21363
- type: array
21364
- items: { $ref: '#/components/schemas/WorkspaceSnapshotRow' }
21365
- latest: { type: string, description: 'Newest snapshot key; absent when the session has none.' }
21366
- total: { type: integer, description: 'ROWS ON THIS PAGE — not the snapshot count.' }
21367
-
21368
- WorkspaceTreePage:
21369
- # 4.3.0(CAPS-OPS-8):同上,tree 腿的 200 信封。
21370
- type: object
21371
- description: >
21372
- GET /v1/sessions/{sessionId}/workspace/{key}/tree 的 200 信封(server src/http/routes/sessions.ts)。
21373
- `total` 在这条腿上是**全量**(`all.length`)—— 与 `WorkspaceSnapshotPage.total` 语义相反。
21374
- `key` 回的是解析后的真实键(请求可传 `latest` 别名)。
21375
- additionalProperties: false
21376
- required: [sessionId, key, entries, total]
21377
- properties:
21378
- sessionId: { type: string }
21379
- key: { type: string, description: 'The RESOLVED snapshot key (a `latest` alias in the request comes back resolved).' }
21380
- entries:
21381
- type: array
21382
- items: { $ref: '#/components/schemas/WorkspaceTreeEntry' }
21383
- total: { type: integer, description: 'FULL entry count (not the page size).' }
21384
- nextOffset: { type: integer, description: 'Present ⇒ more pages; send it back as ?offset=.' }
21385
-
21386
- WorkspaceSnapshotRow:
21387
- type: object
21388
- description: >
21389
- One snapshot row of GET /v1/sessions/{sessionId}/workspace (envelope `{sessionId, snapshots,
21390
- latest?, total}`; server src/http/routes/sessions.ts — newest first, uuidv7 keys). E19 snapshot store's
21391
- read-only projection: each row is the freeze at a turn boundary, NOT a live workspace.
21392
- required: [key, files]
21393
- additionalProperties: true
21394
- properties:
21395
- key: { type: string, description: 'Snapshot key (uuidv7 entry id; "latest" is an accepted alias on the sub-routes).' }
21396
- files: { type: integer, description: 'Manifest row count.' }
21397
- bytes: { type: integer, description: 'Total snapshot bytes — present only when the store has a sizes face (SQL backends); the local store honestly omits it ([1894]② additive).' }
21398
-
21399
- WorkspaceTreeEntry:
21400
- type: object
21401
- description: >
21402
- One file entry of GET /v1/sessions/{sessionId}/workspace/{key}/tree (envelope `{sessionId, key,
21403
- entries, total, nextOffset?}`; server src/http/routes/sessions.ts — stable path order, offset paging).
21404
- required: [path, hash]
21405
- additionalProperties: true
21406
- properties:
21407
- path: { type: string, description: 'The relPath — pass VERBATIM as ?path= on the file sub-route.' }
21408
- hash: { type: string, description: 'Content address (sha256) — enables cross-snapshot change highlighting ([1894]②).' }
21409
- size: { type: integer, description: 'Present only when the store has a sizes face (SQL backends).' }
21410
-
21411
- # ═══════════════════════════════════════════════════════════════════════════════════════════════
21412
- # #154 车二(design/179 §7)—— 持久权限规则的 CC settings 导入面。形按 server 真码 + core 原形亲读
21413
- # (`src/http/routes/rules.ts` / `src/rules-consent.ts`;preview/result 两形是 core `ImportPreview` /
21414
- # `ImportResult` 的原样透传)。
21415
- # ═══════════════════════════════════════════════════════════════════════════════════════════════
21416
-
21417
21859
  CcImportSettingsLayer:
21418
21860
  type: string
21419
21861
  enum: [userSettings, projectSettings, localSettings]
@@ -22141,7 +22583,7 @@ components:
22141
22583
 
22142
22584
  AdoptionNotMigratedFace:
22143
22585
  type: string
22144
- enum: [usage-window, cost-quota, rate-limit, approval-exemption-grantor, approval-ask-decision-actor, image-bake-requested-by]
22586
+ enum: [usage-window, cost-quota, rate-limit, approval-exemption-grantor, approval-ask-decision-actor, image-bake-requested-by, permission-rule-approval-history, permission-rule-import-ticket, org-delivery-quota]
22145
22587
  description: >
22146
22588
  FROZEN machine-readable vocabulary of the faces the server deliberately does not migrate. Renaming or
22147
22589
  removing a member is a wire break; adding one is a new behaviour face. Consumers assert POSITIVELY on