@sema-agent/sdk 25.0.0 → 26.1.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 (71) hide show
  1. package/README.md +151 -3
  2. package/dist/client.d.ts +0 -4
  3. package/dist/client.d.ts.map +1 -1
  4. package/dist/client.js +0 -5
  5. package/dist/client.js.map +1 -1
  6. package/dist/errors.d.ts +108 -26
  7. package/dist/errors.d.ts.map +1 -1
  8. package/dist/errors.js +57 -35
  9. package/dist/errors.js.map +1 -1
  10. package/dist/events.d.ts +41 -4
  11. package/dist/events.d.ts.map +1 -1
  12. package/dist/events.js.map +1 -1
  13. package/dist/index.d.ts +28 -17
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +12 -3
  16. package/dist/index.js.map +1 -1
  17. package/dist/resources/approvals.d.ts +19 -3
  18. package/dist/resources/approvals.d.ts.map +1 -1
  19. package/dist/resources/approvals.js +8 -3
  20. package/dist/resources/approvals.js.map +1 -1
  21. package/dist/resources/fleet.d.ts +12 -12
  22. package/dist/resources/fleet.d.ts.map +1 -1
  23. package/dist/resources/fleet.js.map +1 -1
  24. package/dist/resources/rules.d.ts +3 -6
  25. package/dist/resources/rules.d.ts.map +1 -1
  26. package/dist/resources/rules.js.map +1 -1
  27. package/dist/resources/runs.d.ts +4 -1
  28. package/dist/resources/runs.d.ts.map +1 -1
  29. package/dist/resources/runs.js +4 -1
  30. package/dist/resources/runs.js.map +1 -1
  31. package/dist/resources/session-sync.d.ts +36 -34
  32. package/dist/resources/session-sync.d.ts.map +1 -1
  33. package/dist/resources/session-sync.js +34 -41
  34. package/dist/resources/session-sync.js.map +1 -1
  35. package/dist/resources/sessions.d.ts +5 -3
  36. package/dist/resources/sessions.d.ts.map +1 -1
  37. package/dist/resources/sessions.js +5 -3
  38. package/dist/resources/sessions.js.map +1 -1
  39. package/dist/resources/side-query.d.ts +8 -0
  40. package/dist/resources/side-query.d.ts.map +1 -1
  41. package/dist/resources/side-query.js.map +1 -1
  42. package/dist/resources/tasks.d.ts +2 -0
  43. package/dist/resources/tasks.d.ts.map +1 -1
  44. package/dist/resources/tasks.js +2 -0
  45. package/dist/resources/tasks.js.map +1 -1
  46. package/dist/resources/tool-approvals.d.ts +106 -101
  47. package/dist/resources/tool-approvals.d.ts.map +1 -1
  48. package/dist/resources/tool-approvals.js +58 -37
  49. package/dist/resources/tool-approvals.js.map +1 -1
  50. package/dist/settings.d.ts +14 -1
  51. package/dist/settings.d.ts.map +1 -1
  52. package/dist/settings.js +0 -24
  53. package/dist/settings.js.map +1 -1
  54. package/dist/sync.d.ts +5 -5
  55. package/dist/sync.d.ts.map +1 -1
  56. package/dist/sync.js +15 -55
  57. package/dist/sync.js.map +1 -1
  58. package/dist/transport.d.ts +5 -0
  59. package/dist/transport.d.ts.map +1 -1
  60. package/dist/transport.js +7 -0
  61. package/dist/transport.js.map +1 -1
  62. package/dist/types.d.ts +236 -129
  63. package/dist/types.d.ts.map +1 -1
  64. package/dist/types.js +8 -0
  65. package/dist/types.js.map +1 -1
  66. package/openapi.yaml +566 -559
  67. package/package.json +2 -3
  68. package/dist/resources/workspace.d.ts +0 -95
  69. package/dist/resources/workspace.d.ts.map +0 -1
  70. package/dist/resources/workspace.js +0 -60
  71. 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:
@@ -2510,7 +2428,7 @@ paths:
2510
2428
  summary: Write the per-session tool-permission rules (tighten-only).
2511
2429
  description: >
2512
2430
  E6 — write the per-session rules. 🔴 TIGHTEN-ONLY for a normal principal: a loosen (remove a deny / widen
2513
- an allowlist or allowDirs) → 403 `loosen_forbidden` (operator-only). `expectedRev` = optimistic-concurrency
2431
+ an allowlist or allowDirs) → 403 `loosen_forbidden` (operator-only). 🔴 server >= 7.135.0 (engine 7.55.0): the tighten gate is a gate on the row's OPERATOR PART (`operatorRules`), not on the whole row — a non-operator (the session's owner) can take back what NON-operator writes added, but not what an operator wrote (moving anything inside the operator part, even an entry both sides wrote, answers 403 `loosen_forbidden`; the row stays). <= 7.134.x: a non-operator write was subtract-only against the whole row. Read the CODE (the sentence changed: `a non-operator session-rule write may not loosen what an operator wrote; refused: …` vs `… may only tighten …`) — never the sentence. `expectedRev` = optimistic-concurrency
2514
2432
  CAS; a stale rev → 409 `conflict`. Each rule field must be `string[]` and the body carries only the three buckets (`toolAllow` / `toolDeny` / `allowDirs`; any other
2515
2433
  key, including the retired `commandAllow` / `commandDeny`, → 400 `rules.invalid_shape` on server >= 7.129.0) → malformed body 400. ownerless
2516
2434
  session → 409; non-owner → 404. 501 without a session-policy store.
@@ -2538,6 +2456,9 @@ paths:
2538
2456
  # write judge stamps the current grammar). `grammar` newer than the engine's ⇒ `400 rules.invalid_shape`.
2539
2457
  rev: { type: integer, minimum: 0 }
2540
2458
  grammar: { type: integer, minimum: 0 }
2459
+ # server >= 7.135.0 (engine 7.55.0): a stored body echoed back (GET → edit → PUT) may also carry its `operatorRules`; on THIS door the echoed part is ALWAYS ignored — the route reads the value through the write reader
2460
+ # (its product carries no `rev`) and the store judges it again: over an existing row the STORED row's operator part is kept, over no row an empty stamped part is minted. Do not rely on an echo to move the operator line — send the new value without `operatorRules`.
2461
+ operatorRules: { $ref: '#/components/schemas/SessionRulesOperatorPart' }
2541
2462
  expectedRev: { type: integer, minimum: 0, description: CAS — reject if the stored rev differs. }
2542
2463
  responses:
2543
2464
  '200':
@@ -2574,7 +2495,8 @@ paths:
2574
2495
  schema: { $ref: '#/components/schemas/ErrorResponse' }
2575
2496
  '401': { $ref: '#/components/responses/Unauthorized' }
2576
2497
  '403':
2577
- description: 403 `loosen_forbidden` — a non-operator write tried to relax the rules (tighten-only).
2498
+ description: >-
2499
+ 403 `loosen_forbidden` — a non-operator write tried to relax what an OPERATOR wrote (server >= 7.135.0; <= 7.134.x: relax the rules at all, tighten-only). Branch on the code, never the sentence (it changed with 7.135.0).
2578
2500
  content:
2579
2501
  application/json:
2580
2502
  schema: { $ref: '#/components/schemas/ErrorResponse' }
@@ -2663,7 +2585,7 @@ paths:
2663
2585
  narrow it, switch with a default arm; `httpStatus` 401 is a re-authorize door); `reason` is HUMAN TEXT
2664
2586
  (server-redacted, length-bounded) and must NOT be parsed.
2665
2587
  Gates, the same family as steer / capture-optout: 501 `capability.run_store_required` (no durable run
2666
- ledger) · 501 `capability.session_ownership_required` (no `sessionStorage.ownerOf`) · 400
2588
+ ledger) · 501 `capability.session_ownership_required` (no `sessionStorage.headOf`; <= 7.134.x: `ownerOf`) · 400
2667
2589
  `request.path_malformed` (bad percent-encoding in the id) · 404 `not_found.session` — unknown session
2668
2590
  AND someone else's session answer the SAME code and SAME message (deliberate: no existence oracle); an
2669
2591
  EXPLICIT operator may re-dial any tenant's session · 422 `steering.invalid_content` · 409
@@ -2729,7 +2651,7 @@ paths:
2729
2651
  the model's description, falling back to the launch command text (content family: redacted, cut at 500
2730
2652
  chars). At most 500 rows (core `list()`'s own cap). 🔴 IN-PROCESS: only rows on THIS replica.
2731
2653
  Zero model work: not billable, not refused by drain. Gates (same family as `mcp/reconnect`): 501
2732
- `capability.session_ownership_required` (no `sessionStorage.ownerOf`) · 404 `not_found.session` — unknown
2654
+ `capability.session_ownership_required` (no `sessionStorage.headOf`; <= 7.134.x: `ownerOf`) · 404 `not_found.session` — unknown
2733
2655
  session AND someone else's session answer the SAME code and SAME message (no existence oracle); an EXPLICIT
2734
2656
  operator may read any session · 400 `request.path_malformed`. Probe with `capabilities.background.listFace`;
2735
2657
  ABSENT key = an older server, where this path is 404 `not_found.route` (same status, different code).
@@ -3002,6 +2924,14 @@ paths:
3002
2924
  '401': { $ref: '#/components/responses/Unauthorized' }
3003
2925
  '404': { $ref: '#/components/responses/NotFound' }
3004
2926
  '501': { $ref: '#/components/responses/NotImplemented' }
2927
+ '503':
2928
+ description: >-
2929
+ server >= 7.135.0: `503 state.file_history_unreadable` — this session's file history cannot be read WHOLE (a boundary torn / unreadable, the tracked set unreadable); nothing is exported this time. 🔴 It is NOT "no history":
2930
+ with nothing to export the manifest still answers `200` with `fileHistory: null`. May carry `permanence` (`transient` / `terminal`; absent = the store did not judge — see `ErrorResponse.permanence`). Older servers read an
2931
+ unreadable history as "none" (manifest 200 / blob 404).
2932
+ content:
2933
+ application/json:
2934
+ schema: { $ref: '#/components/schemas/ErrorResponse' }
3005
2935
 
3006
2936
  /v1/sessions/{sessionId}/sync/entries:
3007
2937
  parameters:
@@ -3040,50 +2970,6 @@ paths:
3040
2970
  '404': { $ref: '#/components/responses/NotFound' }
3041
2971
  '501': { $ref: '#/components/responses/NotImplemented' }
3042
2972
 
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
2973
  /v1/sessions/{sessionId}/sync/history/blobs/{hash}:
3088
2974
  parameters:
3089
2975
  - $ref: '#/components/parameters/PrincipalHeader'
@@ -3109,7 +2995,7 @@ paths:
3109
2995
  → 404 `not_found.blob`, never a blind read; a server whose file-history store has no blob faces
3110
2996
  (e.g. the local file backend) answers the SAME 404 shape (no oracle — "nothing servable" and "not
3111
2997
  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}`.
2998
+ 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
2999
  responses:
3114
3000
  '200':
3115
3001
  description: The blob bytes.
@@ -3120,6 +3006,14 @@ paths:
3120
3006
  '401': { $ref: '#/components/responses/Unauthorized' }
3121
3007
  '404': { $ref: '#/components/responses/NotFound' }
3122
3008
  '501': { $ref: '#/components/responses/NotImplemented' }
3009
+ '503':
3010
+ description: >-
3011
+ server >= 7.135.0: `503 state.file_history_unreadable` — the history cannot be read WHOLE, so the server cannot tell whether this hash is one the session references — it does NOT answer "no such blob" (that is `404 not_found.blob`) and gives no bytes. 🔴 It is NOT "no history":
3012
+ with nothing to export the manifest still answers `200` with `fileHistory: null`. May carry `permanence` (`transient` / `terminal`; absent = the store did not judge — see `ErrorResponse.permanence`). Older servers read an
3013
+ unreadable history as "none" (manifest 200 / blob 404).
3014
+ content:
3015
+ application/json:
3016
+ schema: { $ref: '#/components/schemas/ErrorResponse' }
3123
3017
 
3124
3018
  /v1/sessions/{sessionId}/sync/blobs/{hash}:
3125
3019
  parameters:
@@ -3142,12 +3036,10 @@ paths:
3142
3036
  body). The cloud verifies `sha256(body) === :hash` (the content-address integrity guard) BEFORE storing — a
3143
3037
  mismatch → 400 `blob hash mismatch`. own-or-FRESH gate. Per-blob cap 64 MiB → 413 mid-stream beyond it. A
3144
3038
  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
3039
+ PUSH path is the BARE `blobs/:hash` (the PULL is `history/blobs/:hash`). The blob faces ride the
3147
3040
  file-HISTORY store — a deployment whose store has no blob faces (the local file backend) answers 501
3148
3041
  `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).
3042
+ rest of the sync surface keeps working (operator disposition: wire a backend with blob faces).
3151
3043
  requestBody:
3152
3044
  required: true
3153
3045
  content:
@@ -3217,16 +3109,17 @@ paths:
3217
3109
  x-status: live
3218
3110
  summary: PUSH Phase A — classify + stage (mint a stagingId).
3219
3111
  description: >
3220
- 2c session-sync PUSH Phase A — a SMALL metadata body (`entryIds, snapshots, policy, anchors, resolution?`); the
3112
+ 2c session-sync PUSH Phase A — a SMALL metadata body (`entryIds, fileHistory?, policy, anchors, resolution?, logDigest?, title?, pinned?`); the
3221
3113
  entries stream in Phase B. The cloud classifies (§7), pre-checks blob presence (a missing referenced hash →
3222
- 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
3228
- push must send `fileHistory` itself, and only to a peer whose `capabilities.sessionSyncFileHistory` is not false (server >= 7.116.0 — else the
3229
- commit 422s `missing_blob`).
3114
+ 422 `missing_blob` — PUT it first; server >= 7.135.0 answers it in this FIRST phase — zero staging, zero commit, ahead of the `identical` replay arm — and
3115
+ a deployment whose `capabilities.sessionSyncFileHistory` is false, e.g. `DB_BACKEND=local`, takes a `fileHistory` that references blobs as a first-phase 422 too: the conversation does not land either; <= 7.134.x answered it after the conversation
3116
+ had committed), guards the import lease + any active run (409), then mints a stagingId. 🔴 server >= 7.135.0: a file-history replay fault (the replay arms of `identical` re-push and of the commit) answers `422 adopt_failed`
3117
+ (+ optional `permanence`, see `ErrorResponse.permanence`; <= 7.134.x: `unsupported_format`) — distinct from `missing_blob`.
3118
+ The file history rides `fileHistory` (a `FileHistoryExport` envelope, `null` / absent = none) and every blob it references must have been
3119
+ `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
3120
+ no longer declares it). The SDK carries `fileHistory` in `pushSessionBundle` / `importSessionBundle`; a hand-rolled
3121
+ push must send `fileHistory` itself, and only to a peer whose `capabilities.sessionSyncFileHistory` is not false (server >= 7.116.0 — else
3122
+ `422 missing_blob` in the first phase from server >= 7.135.0; after the commit on <= 7.134.x).
3230
3123
  `identical` → `{ relation:"identical", basis, payloadVerified }` with NO stagingId (skip Phase B);
3231
3124
  `basis` is `"entry-ids+digest"` when the caller supplied a comparable digest and it matched, else
3232
3125
  `"entry-ids"` (id-set equality only) — see the `ImportStaged` schema, which has carried these two
@@ -3239,15 +3132,14 @@ paths:
3239
3132
  application/json:
3240
3133
  schema:
3241
3134
  type: object
3242
- required: [entryIds, snapshots, policy, anchors]
3135
+ required: [entryIds, policy, anchors]
3243
3136
  properties:
3244
3137
  entryIds: { type: array, items: { type: string } }
3245
- snapshots: { type: array, items: { $ref: '#/components/schemas/SyncSnapshot' } }
3246
3138
  fileHistory:
3247
3139
  oneOf:
3248
3140
  - { $ref: '#/components/schemas/FileHistoryExport' }
3249
3141
  - { type: 'null' }
3250
- description: 'server >= 7.52: the file-history envelope (replaces `snapshots`); `null` / absent = none. Every blob it references must already be PUT.'
3142
+ description: 'the file-history envelope; `null` / absent = none. Every blob it references must already be PUT.'
3251
3143
  policy: { type: array, items: { $ref: '#/components/schemas/SessionRulesRecord' } }
3252
3144
  anchors: { type: array, items: { $ref: '#/components/schemas/SyncAnchor' } }
3253
3145
  title:
@@ -3300,7 +3192,12 @@ paths:
3300
3192
  content:
3301
3193
  application/json:
3302
3194
  schema: { $ref: '#/components/schemas/SyncConflict' }
3303
- '422': { $ref: '#/components/responses/UnprocessableEntity' }
3195
+ '422':
3196
+ description: >-
3197
+ 422 — `missing_blob` (first phase from server >= 7.135.0), `restore_failed`, and (server >= 7.135.0) `adopt_failed` (+ optional `permanence`) for the file-history replay; `invalid_entries` etc. as before. Branch on `errorCode`.
3198
+ content:
3199
+ application/json:
3200
+ schema: { $ref: '#/components/schemas/ErrorResponse' }
3304
3201
  '501': { $ref: '#/components/responses/NotImplemented' }
3305
3202
  '503': { $ref: '#/components/responses/Unauthorized' } # (declared 2026-08-01) E6 兄弟守卫:无 service token + 未开 ALLOW_UNAUTHED_WRITES 时,这条 import 提交腿与 `PUT …/policy` 写的是同一个 policy store ⇒ 同门同码 `auth.service_token_required`(server 3.15.0 起)
3306
3203
 
@@ -3324,7 +3221,7 @@ paths:
3324
3221
  description: >
3325
3222
  2c session-sync PUSH Phase B — stream the entry log as an NDJSON REQUEST body into the staged shadow id; the
3326
3223
  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
3224
+ commits (the atomic swap) → replays fileHistory/policy/anchors. Returns `{ relation }` (the AUTHORITATIVE in-txn
3328
3225
  re-classify). Re-POSTing the same stagingId is idempotent. Typed errors: a per-entry / structural violation →
3329
3226
  422 `invalid_entries`; a line too large → 413; a commit-time TOCTOU flip to fork/stale → 409 `conflict`
3330
3227
  (SyncConflictError); a refused policy loosen → 403 `loosen_forbidden`; a wrong/foreign stagingId → 404.
@@ -3342,7 +3239,8 @@ paths:
3342
3239
  '400': { $ref: '#/components/responses/BadRequest' }
3343
3240
  '401': { $ref: '#/components/responses/Unauthorized' }
3344
3241
  '403':
3345
- description: 403 `loosen_forbidden` — a post-commit policy replay tried to relax the rules (tighten-only).
3242
+ description: >-
3243
+ 403 `loosen_forbidden` — a post-commit policy replay tried to relax what an operator wrote (server >= 7.135.0; <= 7.134.x: tighten-only against the whole row). A non-operator replay keeps each record's own operator part; an operator replay makes the imported row the operator's.
3346
3244
  content:
3347
3245
  application/json:
3348
3246
  schema: { $ref: '#/components/schemas/ErrorResponse' }
@@ -3385,6 +3283,22 @@ paths:
3385
3283
  application/json:
3386
3284
  schema: { $ref: '#/components/schemas/PendingList' }
3387
3285
  '401': { $ref: '#/components/responses/Unauthorized' }
3286
+ '500':
3287
+ description: >-
3288
+ 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
3289
+ or delete that row) answers `500` + `errorCode: "approvals.read_corrupt"` (body `{ error, errorCode }`; the bad row's coordinates stay off the wire) —
3290
+ 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
3291
+ failure (connection / timeout / store hiccup) is `503 approvals.read_unavailable` from server >= 7.135.0 (below); servers <= 7.134.x answered it `500 internal.error`. Older servers answered the corrupt row with `internal.error`.
3292
+ content:
3293
+ application/json:
3294
+ schema: { $ref: '#/components/schemas/ErrorResponse' }
3295
+ '503':
3296
+ description: >-
3297
+ server >= 7.135.0: `503 approvals.read_unavailable` — the approvals queue could not be read RIGHT NOW (the store hiccuped; transient — the SDK retries it by status). Until a read succeeds, an answer here does NOT mean nothing is pending.
3298
+ Body `{ error, errorCode }`. Older servers answered a read that could not complete with the global fallback `500 internal.error`.
3299
+ content:
3300
+ application/json:
3301
+ schema: { $ref: '#/components/schemas/ErrorResponse' }
3388
3302
 
3389
3303
  /v1/approvals/stream:
3390
3304
  parameters:
@@ -3829,6 +3743,20 @@ paths:
3829
3743
  application/json:
3830
3744
  schema: { $ref: '#/components/schemas/InboxList' }
3831
3745
  '401': { $ref: '#/components/responses/Unauthorized' }
3746
+ '500':
3747
+ description: >-
3748
+ server >= 7.135.0: `500 approvals.read_corrupt` — a queue row's stored body cannot be read (the store's refusal contract: a retry fails the same way; an operator has to repair or delete that row) — the SAME judge and the same code as `GET /v1/approvals`
3749
+ and `GET /v1/approvals/stream`. Do NOT retry it as-is (the SDK's retry arm exempts the code). Every other read failure is `503 approvals.read_unavailable` (below). Older servers: `500 internal.error`.
3750
+ content:
3751
+ application/json:
3752
+ schema: { $ref: '#/components/schemas/ErrorResponse' }
3753
+ '503':
3754
+ description: >-
3755
+ server >= 7.135.0: `503 approvals.read_unavailable` — the approvals queue could not be read RIGHT NOW (the store hiccuped; transient — the SDK retries it by status). Until a read succeeds, an answer here does NOT mean nothing is pending.
3756
+ Body `{ error, errorCode }`. Older servers answered a read that could not complete with the global fallback `500 internal.error`.
3757
+ content:
3758
+ application/json:
3759
+ schema: { $ref: '#/components/schemas/ErrorResponse' }
3832
3760
 
3833
3761
  /v1/assistant/tasks:
3834
3762
  parameters:
@@ -3850,6 +3778,20 @@ paths:
3850
3778
  application/json:
3851
3779
  schema: { $ref: '#/components/schemas/AssistantTaskList' }
3852
3780
  '401': { $ref: '#/components/responses/Unauthorized' }
3781
+ '500':
3782
+ description: >-
3783
+ server >= 7.135.0: `500 approvals.read_corrupt` — a queue row's stored body cannot be read (the store's refusal contract: a retry fails the same way; an operator has to repair or delete that row) — the SAME judge and the same code as `GET /v1/approvals`
3784
+ and `GET /v1/approvals/stream`. Do NOT retry it as-is (the SDK's retry arm exempts the code). Every other read failure is `503 approvals.read_unavailable` (below). Older servers: `500 internal.error`.
3785
+ content:
3786
+ application/json:
3787
+ schema: { $ref: '#/components/schemas/ErrorResponse' }
3788
+ '503':
3789
+ description: >-
3790
+ server >= 7.135.0: `503 approvals.read_unavailable` — the approvals queue could not be read RIGHT NOW (the store hiccuped; transient — the SDK retries it by status). Until a read succeeds, an answer here does NOT mean nothing is pending.
3791
+ Body `{ error, errorCode }`. Older servers answered a read that could not complete with the global fallback `500 internal.error`.
3792
+ content:
3793
+ application/json:
3794
+ schema: { $ref: '#/components/schemas/ErrorResponse' }
3853
3795
 
3854
3796
  /v1/assistant/tasks/{taskId}/resume:
3855
3797
  parameters:
@@ -4458,7 +4400,7 @@ paths:
4458
4400
  verbatim 400 texts, the two capability bits to probe first and the `deny` posture are on
4459
4401
  the PersistRuleSelection schema. Allow-family only (on `deny` the whole field is ignored).
4460
4402
 
4461
- ⚠️ If the frame carried neither `ruleOffers` nor `ruleSuggestions`, do not send this key —
4403
+ ⚠️ If the frame carried no `ruleOffers`, do not send this key —
4462
4404
  there is nothing redeemable for THIS card and the answer is necessarily
4463
4405
  `rulePersisted:false` + `rule_lane_unavailable` (the missing material short-circuits BEFORE
4464
4406
  any candidate comparison, so it is never `rule_not_offered`). Absence has several causes —
@@ -5327,39 +5269,6 @@ paths:
5327
5269
  error: { type: string }
5328
5270
  missing: { type: array, items: { type: string } }
5329
5271
 
5330
- /v1/approvals/{approvalId}:
5331
- parameters:
5332
- - $ref: '#/components/parameters/PrincipalHeader'
5333
- - in: path
5334
- name: approvalId
5335
- required: true
5336
- schema: { type: string }
5337
- get:
5338
- tags: [approvals]
5339
- operationId: approvalsGet
5340
- x-status: retired # 5.0.0: legacy approval-store leg deleted — NO deployment serves this route.
5341
- summary: 'RETIRED (server 5.0.0) — by-id approval read; every deployment answers 404. Use GET /v1/approvals.'
5342
- description: >
5343
- 🔴 RETIRED as of `@sema-agent/server` 5.0.0 ([2354]/[2356] A-5): the legacy approval-store polling leg
5344
- that served this route was deleted whole, and the durable checkpoint lane never had a by-id read
5345
- (it can list by scope or look up by capability token — there is no simple-id query). A worker WITH a
5346
- checkpoint store answers 404 `not_found.route`; a worker WITHOUT one never matches the `/v1/approvals*`
5347
- prefix at all. Either way: 404 on every deployment, and that 404 is indistinguishable from "no such
5348
- approval", which is exactly why the SDK stopped calling it (4.0.1 — `approvals.get` now reads
5349
- `GET /v1/approvals` and filters by `sessionId`, one round trip instead of a guaranteed-404 probe plus
5350
- the same list call).
5351
- Kept in the spec as a tombstone rather than deleted: consumers generated against an older spec need to
5352
- see WHY the path went away. Decide via `POST /v1/approvals/{sessionId}/decide`; list via `GET /v1/approvals`.
5353
- Historical shape (below) is left intact for the same reason; it describes no live behavior.
5354
- responses:
5355
- '200':
5356
- description: The approval row (full args projection).
5357
- content:
5358
- application/json:
5359
- schema: { $ref: '#/components/schemas/ApprovalRow' }
5360
- '401': { $ref: '#/components/responses/Unauthorized' }
5361
- '404': { $ref: '#/components/responses/NotFound' }
5362
-
5363
5272
  /v1/memory/export:
5364
5273
  parameters:
5365
5274
  - $ref: '#/components/parameters/PrincipalHeader'
@@ -6076,6 +5985,14 @@ paths:
6076
5985
  schema: { $ref: '#/components/schemas/WorkflowJournalPage' }
6077
5986
  '401': { $ref: '#/components/responses/Unauthorized' }
6078
5987
  '404': { $ref: '#/components/responses/NotFound' }
5988
+ '500':
5989
+ description: >-
5990
+ server >= 7.135.0 (core 7.55.0): `500 workflow.journal_incompatible` — a journal row the engine's judge rejects (a parked row with no checkpoint token, a parked row whose terminal is not paused, a row that does not read as an object — hand-edited / another writer /
5991
+ a newer format). The judgment covers the WHOLE journal, on every backend, on every page (a bad row on another page, or one over 64 KiB, answers the same); the body carries `ordinal` (the first bad row, ascending; `-1` = the refusal names no entry). The run's resume is refused by the engine
5992
+ with the same code. Repair that row from the retained copy — NEVER by deleting it (a missing entry re-runs that step live). A retry changes nothing (the SDK's retry arm exempts the code). <= 7.134.x read such rows back as `200`.
5993
+ content:
5994
+ application/json:
5995
+ schema: { $ref: '#/components/schemas/ErrorResponse' }
6079
5996
  '501': { $ref: '#/components/responses/NotImplemented' }
6080
5997
 
6081
5998
  /v1/workflows/{workflowRunId}/agents/{label}/steer:
@@ -7161,8 +7078,32 @@ components:
7161
7078
  enabled: { type: boolean }
7162
7079
  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).' }
7163
7080
  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.' }
7081
+ skills:
7082
+ type: array
7083
+ items: { $ref: '#/components/schemas/ScenarioSkill' }
7084
+ description: >-
7085
+ server >= 7.135.0, always present there (`[]` when none): the deployment SKILLS this scenario's run mounts (the deployment skills directory UNION what the config source hands over, filtered by scenario; a request's own `skills` are not
7086
+ included) — the same single filter that feeds `TaskSpec`, same group, same order. A client's `/skill-name` completion reads it; naming one goes through `userInvokedSkills`. Entries carry no body and none of a skill's tool lists. On the card
7087
+ but not in the run: `disableSkills: true` (nothing mounted) and a skill whose body exceeds the engine's load cap (naming it answers `config.user_invoked_skill_unknown`). ABSENT = an older server: the list cannot be read —
7088
+ NOT "no skills" (that is `[]`). Bit: `capabilities.scenarioSkills`.
7164
7089
  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".' }
7165
7090
 
7091
+ ScenarioSkill:
7092
+ type: object
7093
+ description: One row of `ScenarioDetail.skills` (server >= 7.135.0). No body, no tool lists.
7094
+ additionalProperties: false
7095
+ required: [name, description, source]
7096
+ properties:
7097
+ name: { type: string }
7098
+ description: { type: string }
7099
+ source: { $ref: '#/components/schemas/ScenarioSkillSource' }
7100
+ ScenarioSkillSource:
7101
+ type: string
7102
+ description: >
7103
+ Where a scenario skill came from (server >= 7.135.0; the server's self-minted word table `scenarioSkillSource`, closed two words): `factory` = the deployment's skills directory (`SKILLS_DIR`);
7104
+ `center` = the config source (the center / the local `config.d` skills list, or a config-declared plugin) — a skill that overrides a same-named factory one by name is also `center`.
7105
+ enum: [factory, center]
7106
+
7166
7107
  RunStatus:
7167
7108
  type: string
7168
7109
  enum: [running, completed, failed, suspended, needs_review, blocked]
@@ -7445,7 +7386,7 @@ components:
7445
7386
  CC Rewind parity (E18). Fork the conversation FROM this user-message SessionTreeEntry.id (non-destructive
7446
7387
  branch via setLeafId). With `restoreFiles: true` ⇒ "both" (also restore the working tree — the retired spelling
7447
7388
  `rewindFiles` still rides the wire but its restore sense is core's typed migration rejection naming `restoreFiles`). Alone ⇒
7448
- "conversation". HONORED only where the deployment wired a resume-anchor store + getLeafId (capabilities.resumeAt);
7389
+ "conversation". HONORED only where the deployment wired a resume-anchor store + the session head read `headOf` (<= 7.134.x: `getLeafId`) (capabilities.resumeAt);
7449
7390
  unknown anchor ⇒ 4xx, capability-off ⇒ 501. Cannot combine with a durable resume.
7450
7391
  resumeAtMode:
7451
7392
  type: string
@@ -7479,7 +7420,7 @@ components:
7479
7420
  kind of handle as `resumeAt`, resolved through the SAME anchor store — typically the `taskId` from that turn's
7480
7421
  202 receipt (the server keys the user-message anchor by the run's taskId). Server `src/boot/resolve-spec.ts:686-697`:
7481
7422
  `resumeAnchorStore.resolve(sessionId, handle, principal)` → entry id; unknown handle ⇒ 404
7482
- `rewind_files_to.unknown_event`; no session ⇒ 422 `rewind_files_to.no_session`; no anchor store / no getLeafId ⇒
7423
+ `rewind_files_to.unknown_event`; no session ⇒ 422 `rewind_files_to.no_session`; no anchor store / no session head read (`headOf`; <= 7.134.x: `getLeafId`) ⇒
7483
7424
  501. ⚠️ The 501 is NOT the capability's whole predicate: `capabilities.rewindFilesTo` also requires the
7484
7425
  file-history store; without it the server does not 501 — core refuses at prepare with
7485
7426
  `rewind.store_unconfigured` (server >= 7.104.0 / S-724: a 409 error body on the synchronous `POST /v1/tasks` when
@@ -7801,7 +7742,7 @@ components:
7801
7742
  `permissions.defaultMode`'s specific fold is documented on `TaskRequest.permissionMode`.
7802
7743
  🔴 ON THIS SUBMIT FACE THE KEY SET IS CLOSED (server >= 7.57.0 for the sub-tree): accepted top-level keys
7803
7744
  are `env` / `hooks` / `model` / `outputStyle` / `permissions` / `skillListingBudgetFraction` /
7804
- `skillListingMaxDescChars` / `ultracode` / `webSearch` (server >= 7.133.0 adds the three TIGHTEN-ONLY seats `complianceAdditionalDenies` / `readDenyPatterns` / `writeProtectedExtra`), and accepted `permissions.*` keys are
7745
+ `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
7805
7746
  `allow` / `ask` / `defaultMode` / `deny` / `disableAutoMode` (server >= 7.133.0 adds `neverAuto`); anything else is 400 `request.body_shape`
7806
7747
  with the names in the refusal's `unknownKeys` / `unsupportedKeys` arrays (REFUSED, not dropped — a dropped
7807
7748
  key reads as "in effect" to the caller). The object stays OPEN here because the same `SemaSettings`
@@ -7812,6 +7753,15 @@ components:
7812
7753
  (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`).
7813
7754
  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.
7814
7755
  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).
7756
+ 🔴 `settings.approvalFrameFamilies` (server >= 7.134.0; send it on sight of `capabilities.taskSettings.approvalFrameFamilies === true` — NOT of the top-level
7757
+ `capabilities.approvalFrameFamilies`, which <= 7.133.x already has while answering 400 `request.body_shape` for this key): the approval opening-frame FAMILIES this
7758
+ 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
7759
+ repeat) => 400 `request.field_invalid` with `field: "settings.approvalFrameFamilies"`, `reason: "bad_entry"` and `index` (absent for a non-array). BEHAVIOR (BREAKING, wire):
7760
+ a run whose declaration does not contain the family this worker presents (`capabilities.approvalFrameFamilies`) has NO live approval channel — no card of either
7761
+ 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
7762
+ policy at once: with a durable store (`capabilities.approvals`) it parks per the operator policy (default; answer through `POST /v1/approvals/{sessionId}/decide`)
7763
+ 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
7764
+ rides the persisted body onto resume legs.
7815
7765
  🔴 `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` /
7816
7766
  `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 =>
7817
7767
  400 `request.field_invalid` with `detail: { reason: "wire_key_invalid", key, path }`; the plugin context has one 8192-character budget.
@@ -8066,6 +8016,15 @@ components:
8066
8016
  combination); `taskWriteFaceOpen` is NOT its reading. Residual (server S-908): the binding is keyed by SESSION and written
8067
8017
  after the run is claimed (a refused request writes nothing); on deployments without a run store two concurrently accepted
8068
8018
  runs on one session are last-writer-wins. Delegated children's shells keep the default scrub.
8019
+ projectMemory:
8020
+ type: string
8021
+ enum: [disabled]
8022
+ description: >
8023
+ server >= 7.135.0 (engine 7.55.0 `TaskSpec.projectMemory`, passed through verbatim): this ONE run does not read the deployment's project context — the engine does not call the deployment's project-context seat, so the project layer,
8024
+ the declared instruction files and the few things they feed are all absent (the engine's own git-status lane, the memory engine, skills / hooks / MCP are unaffected; delegated / background / fork / workflow children inherit it from
8025
+ the parent). The only word is `disabled`; the key ABSENT = call the seat. The leg's `wiring_manifest.projectMemory` reads `disabled` and the server-minted `instructions` section is absent. A deployment without the seat still accepts it
8026
+ (the manifest answers `absent`). Anything else (`off`, a boolean, `null`) ⇒ a fresh submit answers 400 `request.field_invalid`; a resume replay judges the same and folds into 409 `resume_blocked_by_policy`
8027
+ (`blockedBy: request.field_invalid`). Read `capabilities.projectMemory` beforehand: older servers answer 400 `request.body_shape` (`unknownKeys: ["projectMemory"]`).
8069
8028
  attachments:
8070
8029
  type: object
8071
8030
  description: >
@@ -9659,6 +9618,14 @@ components:
9659
9618
  redispatched: { type: array, items: { type: string } }
9660
9619
  collapsed: { type: string }
9661
9620
 
9621
+ ApprovalFrameFamily:
9622
+ type: string
9623
+ enum: [tool_approval, approval_request]
9624
+ description: >-
9625
+ The approval opening-frame FAMILY words (closed; the server's `APPROVAL_FRAME_FAMILIES` verbatim and in order): `tool_approval` = the legacy family
9626
+ (live card `tool_approval` / close `tool_approval_complete`); `approval_request` = the card family (`approval_request` / `approval_complete` /
9627
+ batch revoke `approval_revoke`). ONE vocabulary for two faces: the capability `approvalFrameFamilies` (which family this worker presents) and, server >= 7.134.0,
9628
+ the request key `settings.approvalFrameFamilies` (which families this run's client renders).
9662
9629
  ApprovalDecision:
9663
9630
  type: object
9664
9631
  description: HITL decision (`POST /v1/approvals/:sessionId/decide`).
@@ -9726,30 +9693,17 @@ components:
9726
9693
  refused by the ENGINE before the CAS (409 `resume_outcome_invalid` + `field: "hostDecision"`, the card stays
9727
9694
  pending); outside the closed set ⇒ 400 `request.field_invalid`; any value on a direct-door worker ⇒ 400
9728
9695
  `decided_by_not_in_proof`. An older server (no `approvalDecidedBy` bit) refuses the key (400 `request.body_shape`).
9729
-
9730
- ApprovalRow:
9731
- type: object
9732
- description: >
9733
- The legacy D-lane approval row of GET /v1/approvals/{approvalId} (server `ApprovalRow`,
9734
- plugins/approval-store-sql.ts — the row is `sendJson`'d verbatim, approvals-assistant.ts).
9735
- Present only on deployments running the legacy `approvalStore` leg (no `checkpointStore` wired);
9736
- the durable F4 lane has no by-id GET (only list + decide).
9737
- # 🔴 census 批2 五段:CLOSED — byte-identical to the server row type, no extra keys.
9738
- required: [id, taskId, sessionId, owner, scope, toolName, args, status, reason, decidedBy, createdAt, decidedAt]
9739
- additionalProperties: false
9740
- properties:
9741
- id: { type: string }
9742
- taskId: { type: [string, 'null'] }
9743
- sessionId: { type: [string, 'null'] }
9744
- owner: { type: [string, 'null'] }
9745
- scope: { type: [string, 'null'], description: 'Single-DB fleet scope guard (tenant identity, owner-sourced).' }
9746
- toolName: { type: string }
9747
- args: { description: 'The reviewed tool-call args — full fidelity, not redacted (this IS the review surface).' }
9748
- status: { type: string, enum: [pending, approved, denied, expired] }
9749
- reason: { type: [string, 'null'] }
9750
- decidedBy: { type: [string, 'null'] }
9751
- createdAt: { type: string }
9752
- decidedAt: { type: [string, 'null'] }
9696
+ feedback:
9697
+ type: string
9698
+ description: >-
9699
+ 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
9700
+ 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
9701
+ (`decision: "approve"`, `decidedBy` absent or `person`): on a deny or a `decidedBy: "policy"` decision ⇒ 400 `request.field_conflict` with
9702
+ `detail.kind: "approval_feedback"` and `detail.fields` `["feedback", "decision"]` / `["feedback", "decidedBy"]` (a denial's words go in `reason`);
9703
+ 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
9704
+ carrying `<system-reminder>` ⇒ server >= 7.135.0 (engine 7.55.0): 409 `resume_outcome_invalid` + `field: "feedback"` + `retriable: true` (the engine's sentence naming `feedback`; refused before the CAS, the card stays pending — ask the person to change
9705
+ or drop the words, do not auto-refetch and re-decide on the code); server 7.134.x: 409 `approval_binding_mismatch` (no `field`). The same door serves parked background-agent cards and workflow child cards; the live
9706
+ `POST /v1/tool-approvals/{id}/respond` does NOT take it. Servers <= 7.133.x refuse the key (400 `request.body_shape`, `unknownKeys: ["feedback"]`).
9753
9707
 
9754
9708
  Capabilities:
9755
9709
  type: object
@@ -9958,7 +9912,7 @@ components:
9958
9912
  type: boolean
9959
9913
  description: >-
9960
9914
  Code-only rewind (`TaskRequest.rewindFilesTo`) is wired — mint `routes/capabilities.ts:839` =
9961
- `Boolean(deps.fileHistoryStore && deps.resumeAnchorStore && deps.sessionStorage?.getLeafId)`: it needs the
9915
+ `Boolean(deps.fileHistoryStore && deps.resumeAnchorStore && headReadOf(deps.sessionStorage))` (<= 7.134.x last conjunct: `deps.sessionStorage?.getLeafId`): it needs the
9962
9916
  file-history store (to restore) AND the anchor map (to resolve the target HANDLE). Corrected in sdk 11.4.0
9963
9917
  (board [8437] U3): the store is the FILE-HISTORY store (not the pre-S-15 whole-tree snapshot store).
9964
9918
  🔴 K-24: on server 7.97.0–7.103.x that seat was the CAPTURE face (backend AND the `FILE_HISTORY_CAPTURE` knob), so a
@@ -10029,32 +9983,20 @@ components:
10029
9983
  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.' }
10030
9984
  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.' }
10031
9985
  workspace:
9986
+ type: boolean
9987
+ enum: [false]
10032
9988
  description: >
10033
- #3 (server 1.299–7.51): workspace-browse capability family — an OBJECT when the E19 snapshot-store
10034
- read faces + a durable session store are wired ({browse, archive, maxFileBytes}); `false`/absent
10035
- otherwise (same predicate as the routes' 501). `maxFileBytes` = the single-file read cap (over-cap
10036
- ⇒ 413 workspace_file_too_large envelope; the archive endpoint is the escape hatch).
10037
- 🔴 RETIRED (server ≥7.52, S-15/design/381): the whole-tree snapshot epoch this face projected was
10038
- replaced by per-edited-file rewind history, so this key is now ALWAYS false and the four
10039
- /v1/sessions/{sessionId}/workspace* routes answer 501 `capability.workspace_retired` (route shape
10040
- kept as a tombstone — a named refusal, not a 404 that reads like a typo). Consumption posture
10041
- unchanged: probe this bit, false ⇒ don't render the panel.
10042
- oneOf:
10043
- - type: boolean
10044
- enum: [false]
10045
- - type: object
10046
- additionalProperties: true
10047
- properties:
10048
- browse: { type: boolean }
10049
- archive: { type: boolean }
10050
- maxFileBytes: { type: integer }
9989
+ The workspace-browse bit — ALWAYS `false` on every supported server (>= 7.52.0: the whole-tree snapshot epoch was replaced by
9990
+ per-edited-file rewind history; the four /v1/sessions/{sessionId}/workspace* routes answer 501 `capability.workspace_retired`, route shape kept as a
9991
+ tombstone). sdk 26.0.0 deleted the object arm `{ browse, archive, maxFileBytes }` (servers 1.299–7.51, below the support floor).
10051
9992
  sessionEvents:
10052
9993
  type: boolean
10053
9994
  description: >
10054
9995
  🔴 契约调和车1 H② (2026-08-02): server HAS been emitting this key (the `sessionEvents` bit in `src/http/routes/capabilities.ts`)
10055
9996
  while both this spec and the SDK type were missing it. Gates `GET /v1/sessions/{sessionId}/events`
10056
- (the session-level SSE head subscription). Predicate is a THREE-way AND: a durable session-watch seam
10057
- plus a session store exposing BOTH `getLeafId` and `ownerOf` — the route's 501 uses the same
9997
+ (the session-level SSE head subscription). Predicate (server >= 7.135.0) is a TWO-way AND: a durable session-watch seam
9998
+ plus a session store exposing the session head read `headOf` (<= 7.134.x it was a THREE-way AND with BOTH `getLeafId` and `ownerOf`; those two
9999
+ methods left the store contract in 7.135.0, so `DB_BACKEND=memory` now answers `true`, was `false`) — the route's 501 uses the same
10058
10000
  predicate, so "says yes <=> route works" holds here. False/absent => fall back to polling
10059
10001
  `GET /v1/sessions/{sessionId}/head`. NOTE (server [2373]A-5): no consumer is wired to it yet — it is
10060
10002
  a prepared slot, correct to gate on, wrong to read as "widely consumed".
@@ -10140,7 +10082,7 @@ components:
10140
10082
  approvalFrameFamilies:
10141
10083
  type: array
10142
10084
  maxItems: 1
10143
- items: { type: string, enum: [tool_approval, approval_request] }
10085
+ items: { $ref: '#/components/schemas/ApprovalFrameFamily' }
10144
10086
  description: >-
10145
10087
  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:
10146
10088
  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`
@@ -10148,6 +10090,9 @@ components:
10148
10090
  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.
10149
10091
  `[]` = per-call approvals are off altogether (`TOOL_APPROVAL_ENABLED=false`): no card is presented. Dispatch frame families off THIS array, never off `streamApproval`.
10150
10092
  KEY ABSENT (not `[]`) = server <= 7.132.x — there a `streamApproval: true` leg sends BOTH families for one ask.
10093
+ 🔴 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
10094
+ its submit body declared that family in `settings.approvalFrameFamilies` (send it on sight of `taskSettings.approvalFrameFamilies`, not of this bit — <= 7.133.x
10095
+ 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.
10151
10096
  streamApproval:
10152
10097
  type: boolean
10153
10098
  description: >
@@ -10324,6 +10269,7 @@ components:
10324
10269
  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.' }
10325
10270
  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).' }
10326
10271
  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.' }
10272
+ 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.' }
10327
10273
  settings:
10328
10274
  type: object
10329
10275
  description: >
@@ -10382,11 +10328,29 @@ components:
10382
10328
  — SQL backends only; `DB_BACKEND=local` is always false). Advertises exactly the two file-history blob faces
10383
10329
  `PUT /v1/sessions/{sessionId}/sync/blobs/{hash}` and `GET /v1/sessions/{sessionId}/sync/history/blobs/{hash}`.
10384
10330
  READ IT ON BOTH ENDS of a sync: if either is false, sync the conversation only — do not push `fileHistory`, do
10385
- not PUT blobs (a `fileHistory` pushed to a peer without the blob faces 422s `missing_blob` after commit). When
10331
+ not PUT blobs (a `fileHistory` pushed to a peer without the blob faces 422s `missing_blob` in the FIRST phase — zero staging, zero commit, the conversation does not land either — from server >= 7.135.0; <= 7.134.x answered it in the second phase, after the conversation committed). When
10386
10332
  false the two routes answer 501 `capability.snapshot_store_required` (any session id, ahead of the owner check).
10387
10333
  A blob that the session history graph does not reference / whose bytes are gone stays a 404 `not_found.blob`.
10388
10334
  ABSENT = an older server (<= 7.115.x): the bit cannot be read there (a `DB_BACKEND=local` deployment with
10389
10335
  `sessionSync: true` answered PUT blob 501 on every call), so try as before.
10336
+ sessionRulesNamespaceForms:
10337
+ type: boolean
10338
+ description: >
10339
+ server >= 7.135.0 (predicate class "always true", read per request from the engine's protocol table): the session-rule write faces (`PUT /v1/sessions/{id}/policy`, the `policy` face of `POST /v1/sessions/{id}/sync/import`)
10340
+ accept — in `toolAllow` / `toolDeny` — the three COVERING FORMS of every protocol namespace the engine knows (today `mcp__` and `a2a__`): the whole peer (`mcp__srv`), a tool-segment wildcard (`mcp__srv__*`)
10341
+ and a tool-segment glob (`mcp__srv__get_*`); the engine's matcher reads each as ONE GROUP of tools. A client may stop refusing those three forms losslessly on this bit alone (never on a version guess).
10342
+ The bit says NOTHING about other wildcards: `*`, partial peer-segment wildcards (`mcp__git*`) and a trailing-separator `mcp__srv__` are still refused by the write face (`400 rules.inert_entry`); the whole-protocol
10343
+ name `mcp__*` is accepted only in `toolDeny`. Orthogonal to `sessionPolicy` (does the deployment have a session-rules write face at all): read `sessionPolicy` AND this bit. ABSENT (older server) and `false` ⇒ read as "do not widen".
10344
+ projectMemory:
10345
+ type: boolean
10346
+ description: >
10347
+ server >= 7.135.0 (always true): the top-level request key `projectMemory: "disabled"` is accepted. The read face hangs on no facility — a deployment without a project-context seat still accepts it
10348
+ (the manifest answers `"absent"`). ABSENT = an older server, whose key-closure gate answers `400 request.body_shape` (`unknownKeys` names it): send the key on sight of the bit only.
10349
+ scenarioSkills:
10350
+ type: boolean
10351
+ description: >
10352
+ server >= 7.135.0 (always true): `GET /v1/capabilities/scenarios/{name}` carries `skills[]` (`ScenarioDetail.skills`). ABSENT = an older server: the card has no list — do NOT read that as "this scenario has no skills"
10353
+ (that is `skills: []`).
10390
10354
 
10391
10355
  # ─── S-139(sdk 8.3.0):对 server 7.60.0 的整体重对账补齐的 19 位 ───────────────────────
10392
10356
  # 行号 = server 7.60.0 树的 `src/http/routes/capabilities.ts`。语义与谓词的全文在
@@ -10673,7 +10637,7 @@ components:
10673
10637
  type: boolean
10674
10638
  description: >
10675
10639
  server >= 7.85.0 (S-381 / core 7.22.0 #857; mint `routes/capabilities.ts:264` =
10676
- `Boolean(deps.runStore) && Boolean(deps.sessionStorage?.ownerOf)`) — presence of
10640
+ `Boolean(deps.runStore) && Boolean(deps.sessionStorage?.ownerOf)`; server >= 7.135.0 the last conjunct is `headReadOf(deps.sessionStorage) !== undefined`) — presence of
10677
10641
  `POST /v1/sessions/{sessionId}/mcp/reconnect`. FACILITY DEPENDENCY, two conjuncts: a durable run
10678
10642
  ledger (the verb reaches the live stream through the run row) AND the session-ownership face (the
10679
10643
  inbound owner gate is fail-closed; without it the whole door is 501). Both 501s derive from the SAME
@@ -10794,7 +10758,7 @@ components:
10794
10758
  sessionBackgroundFacesAvailable(deps), exitFaces: sessionBackgroundFacesAvailable(deps) }`) — presence of the
10795
10759
  session background-task doors: `listFace` = `GET /v1/sessions/{sessionId}/background`, `exitFaces` =
10796
10760
  `POST /v1/sessions/{sessionId}/background/stop` (contract §16). Both bits use the SAME predicate as the two
10797
- doors' 501 (the session-ownership face `sessionStorage.ownerOf`), so "says yes but 501s" is structurally
10761
+ doors' 501 (the session-ownership face: `sessionStorage.headOf` on server >= 7.135.0, `ownerOf` on <= 7.134.x), so "says yes but 501s" is structurally
10798
10762
  impossible; without it both bits are explicitly `false` and both doors answer 501
10799
10763
  `capability.session_ownership_required`. ENDPOINT-GATE FORM: `false` ⇒ do not call. True only promises the
10800
10764
  endpoint — not that this replica holds any background row of that session (the registry is in-process).
@@ -11845,7 +11809,7 @@ components:
11845
11809
  modelPlane:
11846
11810
  allOf: [{ $ref: '#/components/schemas/ModelPlane' }]
11847
11811
  description: >-
11848
- 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
11812
+ server >= 7.133.0, ALWAYS present there (appended last on 7.133.0–7.134.x; from server >= 7.135.0 the last key is `aliases` and this one precedes it — key order is not a contract, read by name): which model table this engine SERVES, whether the catalog was adopted, and what the start-up route judge says — read this instead of predicting
11849
11813
  (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.
11850
11814
  candidateRejected:
11851
11815
  allOf: [{ $ref: '#/components/schemas/CandidateRejected' }]
@@ -11857,6 +11821,25 @@ components:
11857
11821
  configuration is clean" (it is also an earlier server): render the real cause only when the key is present; keep the old behaviour when absent.
11858
11822
  Same read as `models`: key present => this `models` is the fallback generation (none of the candidate's models are in it). No capability bit
11859
11823
  (the endpoint is always there; the presence of the key is the presence rule).
11824
+ aliases:
11825
+ type: array
11826
+ items: { $ref: '#/components/schemas/ModelAlias' }
11827
+ description: >
11828
+ server >= 7.135.0, ALWAYS present there (appended LAST; `[]` when none): the model WORDS besides `models[].name` that the server's acceptance gate takes (tier words / `default` / a catalog entry's id spelling, ...).
11829
+ ACCEPTANCE RULE (one rule; a model-word judge reads this key, never infers from a row's id): a model word is submittable IFF its name is in `models[].name` UNION `aliases[].name` AND its row's `atMentionable !== false`
11830
+ (`atMentionable` absent = no allowlist verdict, not "not mentionable"). `aliases` deliberately does not say which row a word lands on. ABSENT = an earlier server (<= 7.134.x): tier words / `default` stay "cannot tell" —
11831
+ do not read the absence as "no aliases". Known boundary (not introduced by this version): a catalog entry literally named `default` does not appear in `models[]`.
11832
+
11833
+ ModelAlias:
11834
+ type: object
11835
+ additionalProperties: false
11836
+ required: [name]
11837
+ description: >
11838
+ One row of `ModelsResponse.aliases` (server >= 7.135.0): a submittable model word that is not a `models[]` row. `name` = the word (never equal to a `models[].name`); `atMentionable` = the SAME projection as on a `models[]`
11839
+ row, present ONLY when the deployment configured an at-mention allowlist (the allowlist verdict for the catalog entry the word resolves to); absent = no allowlist verdict, NOT "not mentionable".
11840
+ properties:
11841
+ name: { type: string }
11842
+ atMentionable: { type: boolean }
11860
11843
 
11861
11844
  CandidateRejected:
11862
11845
  type: object
@@ -11931,7 +11914,8 @@ components:
11931
11914
  # 没声明 ⇒ 键缺席 ⇒ 不再是必有键(此前由 `input` 恒算成布尔,对没声明的条目报 false)。
11932
11915
  # 🔴 server 7.131.0 起 `reasoning` 同样三态:env 车道 `MODEL_REASONING` / `MODEL_VISION` 缺省 true 退役、中心名册条目不写即不声明
11933
11916
  # ⇒ 两键都「未声明 = 键缺席」,`required` 只剩 name / id / provider(sdk 23.1.0;TS 型 `ModelInfo.reasoning?` 早已可选)。
11934
- required: [name, id, provider]
11917
+ # sdk 26.0.0(支持底线 7.52.0):`routePairing` 进 required(≥7.45 恒铸,此前因地板 3.0.0 不进)。
11918
+ required: [name, id, provider, routePairing]
11935
11919
  additionalProperties: false
11936
11920
  properties:
11937
11921
  name: { type: string, description: 'the @mention handle (user-pickable model name).' }
@@ -11955,8 +11939,8 @@ components:
11955
11939
  adjudicated" must stay distinguishable on the wire). Declared here 2026-09-10 alongside `compat`:
11956
11940
  this schema is CLOSED, and the key has been minted unconditionally since 7.45 — so every real
11957
11941
  /v1/models response has been structurally rejected by a strict validator for the whole time (same
11958
- class as `hasBidiControls` on InboxRow). Optional here because the SDK's supported server floor is
11959
- 3.0.0; the discriminant is the PEER'S VERSION, not this key's presence.
11942
+ class as `hasBidiControls` on InboxRow). REQUIRED since sdk 26.0.0 (the support floor 7.52.0 is above 7.45; it
11943
+ was optional only while the floor was 3.0.0).
11960
11944
  compat:
11961
11945
  $ref: '#/components/schemas/ModelCompat'
11962
11946
  description: >-
@@ -12236,6 +12220,8 @@ components:
12236
12220
  name: { type: string, description: "The workflow script's declared `meta.name`." }
12237
12221
  description: { type: string, description: "The workflow script's declared `meta.description` (one-liner)." }
12238
12222
  agentFailures: { type: integer, description: 'Count of agent-runs that ended failed — present only when > 0 (a "completed, with failures" flag without an N+1 get).' }
12223
+ parked: { type: boolean, enum: [true], description: 'server >= 7.135.0 (core 7.55.0 `runParkedOf`) — present, always the literal `true`, ONLY while the run is `running` AND at least one of its agents is parked at an approval gate; otherwise the key is ABSENT (never `false`). Read it as "the run is waiting on a person", not "failed". Older servers never send it.' }
12224
+ errorCode: { type: string, description: 'The run row''s machine code (open set; e.g. `workflow.agent_parked` for a drained parked terminal) — present only when the row has one. (Declared here in 26.1.0: the engine summary has emitted it since before 7.134.0.)' }
12239
12225
  currentPhase: { type: string, description: 'Title of the phase currently executing (absent once terminal).' }
12240
12226
  status: { $ref: '#/components/schemas/WorkflowRunStatus' }
12241
12227
  phaseCount: { type: integer, description: 'Phases recorded.' }
@@ -12414,6 +12400,8 @@ components:
12414
12400
  description: { type: string, description: "The workflow script's declared `meta.description` (redacted, one-liner)." }
12415
12401
  status: { $ref: '#/components/schemas/WorkflowRunStatus' }
12416
12402
  agentFailures: { type: integer, description: 'run-level count of agents whose status ended failed — stamped at run end, only when > 0.' }
12403
+ parked: { type: boolean, enum: [true], description: 'server >= 7.135.0 (core 7.55.0 `runParkedOf`) — present, always the literal `true`, ONLY while the run is `running` AND at least one of its agents is parked at an approval gate; otherwise the key is ABSENT (never `false`). Read it as "the run is waiting on a person", not "failed". Older servers never send it.' }
12404
+ errorCode: { type: string, description: 'The run row''s machine code (open set; the same words as the same-named slot on the fleet row, e.g. `workflow.agent_parked` for a drained parked terminal) — present only when the row has one. (Declared here in 26.1.0: the detail projection has forwarded it since before 7.134.0.)' }
12417
12405
  stats: { $ref: '#/components/schemas/WorkflowRunStats' }
12418
12406
  startedAt: { type: integer }
12419
12407
  endedAt: { type: integer }
@@ -12766,19 +12754,6 @@ components:
12766
12754
  startedAt: { type: integer }
12767
12755
  elapsedMs: { type: integer }
12768
12756
  tokens: { type: integer }
12769
- # tokenDir REMOVED (engine ruling 2026-06-28): dead field, 0 producer / 0 consumer — spec caught up 2026-07-26.
12770
- queuedCount:
12771
- type: integer
12772
- description: >-
12773
- 🔴 PHANTOM KEY — NO server version ever emits it (verified against the published 7.87.0 bytes:
12774
- the row type declares it and the fleet route's per-key table forwards it, but NO producer writes it
12775
- — not `publishTask`'s call sites, not core's bg-sink tick, not the durable reconcile leg — and the
12776
- wire contract does not mention it either). The server deleted it as a DEAD KEY (S-459, zero
12777
- behavioural impact since `!== undefined` was always false). It stays declared here only because
12778
- removing an exported field is BREAKING = major-only; it is registered with its deletion condition
12779
- in `test/_keyset-divergence.ts` (`FLEET_TASK_ROW_PHANTOM_KEYS`) and goes with the next major.
12780
- DO NOT read it as "queue depth is already reported" — a permanently absent optional key is a FALSE
12781
- LEAD. Real queue depth needs a NEW key WITH a producer, not this one resurrected.
12782
12757
  awaitingPlanApproval: { type: boolean }
12783
12758
  currentAction: { type: string, description: "The child's most recent tool intent as one human-readable line (UNTRUSTED argument head, redacted server-side)." }
12784
12759
  currentTool:
@@ -12892,6 +12867,18 @@ components:
12892
12867
  failedCount: { type: integer }
12893
12868
  elapsedMs: { type: integer }
12894
12869
  tokens: { type: integer }
12870
+ errorCode:
12871
+ type: string
12872
+ description: >-
12873
+ 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
12874
+ run row has one, the key ABSENT otherwise (never an empty string). A parked workflow run reads `status: "failed"` + `errorCode: "workflow.agent_parked"`
12875
+ (parked, not failed; the `status` vocabulary gains no word). Snapshot rows, `workflow` upsert frames and rows merged from the terminal window share one mint
12876
+ point; a `?session=` subscriber's demoted rows carry it too. ABSENT = an older server, or no code on this row — fall back to `status`.
12877
+ parked:
12878
+ type: boolean
12879
+ enum: [true]
12880
+ description: >-
12881
+ server >= 7.135.0 (core 7.55.0 `runParkedOf`) — present, always the literal `true`, ONLY while the run is `running` AND at least one of its agents is parked at an approval gate; otherwise the key is ABSENT (never `false`). Read it as "the run is waiting on a person", not "failed". Older servers never send it. On a fleet row the workflow-bus row is now replaced WHOLE on every `workflow` upsert (server >= 7.135.0; no longer merged by `id` into the old row), so a key that left is gone from the row.
12895
12882
  FleetFrame:
12896
12883
  description: >
12897
12884
  One decoded frame off GET /v1/fleet/stream (service streamFleet emit set, src/http/routes/fleet.ts). On the
@@ -13105,7 +13092,8 @@ components:
13105
13092
  DISTINCT vocabulary from AgentEvent (the durable log is BLOCK-grained, each frame is a delta to append).
13106
13093
  Resumable (each `id:` = the durable task_event.seq; on 416 the server returns `{retainedFrom}` and the SDK
13107
13094
  resumes from there after a turns full-sync). Known events: meta / block-thinking-delta / block-content-delta
13108
- / tool-call / tool-result / turn / done / error (+ heartbeat keep-alives the SDK swallows). Open set.
13095
+ / tool-call / tool-result / prompt-assembled / cross-boundary-taint (server >= 7.134.0: the device lane's taint row + `seq`, see
13096
+ `Event_cross_boundary_taint`) / turn / done / error (+ heartbeat keep-alives the SDK swallows). Open set.
13109
13097
  NOTE: SSE framing on the wire (event name + JSON data + optional id), not a JSON body.
13110
13098
  required: [event, data]
13111
13099
  additionalProperties: true
@@ -13122,10 +13110,11 @@ components:
13122
13110
  description: >
13123
13111
  契约调和车1 G(2026-08-02;server [2373]C-12, `src/trace/project.ts` + the meta frame at
13124
13112
  `routes/trace-usage.ts`): the frame's own event name, DOUBLE-EMITTED inside `data`. Always
13125
- present on server >= 5.0.0; OPTIONAL here because the SDK's supported floor is server 3.0.0
13126
- (where only the `error` frame carried it). Equal to the SSE `event:` line verbatim — its reason
13127
- to exist is the proxy/relay case that strips the `event:` line and forwards only the data JSON,
13128
- where this is the only key left to dispatch on. Heartbeats (`data: {}`) carry none.
13113
+ present on every named frame of a supported server (>= 7.52.0; minted since server 5.0.0) — the SDK's
13114
+ typed arms declare it REQUIRED since 26.0.0; it is not in this schema's `required` only because the one
13115
+ schema also covers heartbeats (`data: {}`, which carry none and which the SDK swallows). Equal to the SSE
13116
+ `event:` line verbatim — its reason to exist is the proxy/relay case that strips the `event:` line and
13117
+ forwards only the data JSON, where this is the only key left to dispatch on.
13129
13118
  output:
13130
13119
  allOf: [{ $ref: '#/components/schemas/ToolOutput' }]
13131
13120
  description: >
@@ -13528,26 +13517,13 @@ components:
13528
13517
  is RETIRED: the SQL backend rewrote its parked rows once at the 7.117.0 boot to `[{ member: "persisted", entries: [<old value>] }]`, but the
13529
13518
  file backend's pre-upgrade parked rows and run results already on disk (`checkpointGate.riskDescriptor`) keep the old key verbatim
13530
13519
  (text still redacted, no structured disclosure) — a consumer reading those must still tolerate the old string.
13531
- # server 7.16.0 [3684]② — durable 队列行归因两键(与活卡 tool_approval 帧的同名键语义同族、判据不同源;
13532
- # 两条腿对同一只 ask 的在场性可以不一致,两侧都只认在场、都禁把缺席读成否定)。
13533
- ruleSuggestions:
13534
- type: array
13535
- minItems: 1
13536
- items: { $ref: '#/components/schemas/RuleSuggestion' }
13537
- description: >
13538
- server 7.16.0 ([3684]②), ADDITIVE: the rule candidates minted for this ask, replayed off the durable
13539
- row (checkpoint.rule_suggestions). DISPLAY-ONLY on this leg — the durable queue has no persistRule
13540
- redemption; the only redemption path is the LIVE card leg (respond + persistRule). Array order =
13541
- display order; an EXACT entry, when present, is always first. core contract caps candidates at 2;
13542
- the server enforces a tolerance cap of 4 on read-back — lay out for 4. Absent = pre-column row
13543
- (parked before the column existed) OR no candidates; render both as "no candidates", never an error.
13544
13520
  ruleOffers:
13545
13521
  type: array
13546
13522
  minItems: 1
13547
13523
  items: { $ref: '#/components/schemas/RuleOffer' }
13548
13524
  description: >
13549
13525
  server >= 7.46.0 (core 5.58.0, design/375) — the discriminated-union successor of the retired
13550
- `ruleSuggestions` above on the DURABLE queue row (core `PendingAction.ruleOffers`, the same
13526
+ `ruleSuggestions` (servers <= 7.45; sdk 26.0.0 deleted it) on the DURABLE queue row (core `PendingAction.ruleOffers`, the same
13551
13527
  contract as the sync leg's `AskRequest.ruleOffers`). A >= 7.46.0 row carries only the new key;
13552
13528
  pre-5.58 rows fail the new narrow read entry by entry and land as "no supply" (no compatibility
13553
13529
  read — a stale schema means re-ask).
@@ -13772,25 +13748,20 @@ components:
13772
13748
  one-off question. The value comes WHOLE from core (`summarizeCheckpoint` echoes it only when the
13773
13749
  cause is a member of the engine's set), so the server neither recomputes nor re-screens it. See
13774
13750
  ClassifierUnavailable for the open-`cause` rule and the three shapes absence covers.
13775
- RETIRED: server >= 7.72.0 (core 7.14.0 #688) never mints this key on any of the three faces; its
13751
+ RETIRED: server >= 7.72.0 (core 7.14.0) never mints this key on any of the three faces; its
13776
13752
  successor is `disposition.cause` on the gate record (`unavailable` / `parse_error` — a different
13777
- question, so an old switch does not port). Kept for servers in [7.69.0, 7.72.0) only (sdk 9.2.0).
13753
+ 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
13754
+ together with the frame key (floor ≥ 7.106.0, next major); why the frame still declares it: `ToolApprovalFrame.classifierUnavailable`.
13778
13755
  ruleStoreUnreadable:
13779
13756
  type: string
13780
13757
  enum: [store, call]
13781
13758
  description: >-
13782
- server >= 7.72.0 (core 7.14.0 #688 C3), ADDITIVE — WHICH HALF of the rule lane could not be read.
13783
- Present iff `origin === "rule_store_unavailable"`: `store` = the wired rule store could not be read
13784
- (look at the store / network); `call` = the store was read but THIS command could not be read against
13785
- the person's deny/ask rows (look at the command spelling). One `origin` word covers both facts, and
13786
- their recovery verbs live in different places (ops vs prompt). Carried on the live `tool_approval`
13787
- frame and the inbox row; the `card_json` card deliberately does NOT carry it (it travels with `origin`,
13788
- which is not on the card). Absence = `origin` is not that word — NOT an assertion that the rule store
13789
- is healthy.
13790
- RETIRED: server >= 7.106.0 (core 7.34.0 C-R172, BREAKING — wire) never mints this key on either face — an
13791
- unreadable rule store no longer raises a card: that read's rows are SKIPPED by partition and the session
13792
- stream gets the user-facing `engine_notice` `permission.rule_store_unavailable` (`detail.at` = `prepare` /
13793
- `call`). Kept for servers in [7.72.0, 7.106.0) only (sdk K-32); `origin: rule_store_unavailable` itself stays.
13759
+ server >= 7.72.0 (core 7.14.0 #688 C3), ADDITIVE — WHICH HALF of the rule lane could not be read (`store` / `call`).
13760
+ 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
13761
+ rule, what the two words mean and the retirement wording is on `ToolApprovalFrame.ruleStoreUnreadable`, and this description points to it.
13762
+ RETIRED: server >= 7.106.0 never mints this key on either face (the frame's description names the successor). Kept for
13763
+ 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);
13764
+ `origin: rule_store_unavailable` itself stays.
13794
13765
 
13795
13766
  InboxList:
13796
13767
  type: object
@@ -14329,12 +14300,20 @@ components:
14329
14300
  server >= 7.92.0 (engine 7.25.0): the session's rule LINEAGE generation. An erase
14330
14301
  (`deleteBySession`) ends the lineage — the next write carries gen+1 — so a reader treats a
14331
14302
  different gen as a new lineage, never a continuation, regardless of rev. Absent on older servers.
14303
+ operatorRules:
14304
+ allOf: [{ $ref: '#/components/schemas/SessionRulesOperatorPart' }]
14305
+ description: >
14306
+ server >= 7.135.0 (engine 7.55.0): the row's OPERATOR PART — the buckets (and their own `grammar` stamp) the last OPERATOR write set; the line a non-operator write may not loosen (`403 loosen_forbidden` on a move inside it).
14307
+ ABSENT = the whole row is the operator's (an operator write; every row stored before 7.135.0, read as before the upgrade: the owner can take nothing back). Minted by the engine's write judge alone, stored whole and read back verbatim; also on the 2c export `policy[].rules`.
14308
+ A non-operator write over an EMPTY row stamps an empty part (stamp only) — the owner can then take back what the owner added. A part ECHOED back is read per door: on `PUT /v1/sessions/{sessionId}/policy` it is ALWAYS ignored (the stored row's part is kept over an existing row; an empty stamped part is minted over none).
14309
+ On the sync-import replay (`POST …/sync/import`, `policy[]` records, which carry `rev`): over an existing row the judge keeps the STORED row's part and ignores the echo; over NO row, a non-operator record that looks like a stored row (carries `rev` >= 1)
14310
+ has its own part adopted (from then on that part IS the row's operator line: taking it back answers 403 `loosen_forbidden`), otherwise an empty stamped part is minted; an operator write never carries a part.
14332
14311
  grammar:
14333
14312
  type: integer
14334
14313
  minimum: 0
14335
14314
  description: >
14336
14315
  server >= 7.129.0 (engine 7.51.2): the rule GRAMMAR the row's entries are read in, stamped by the
14337
- engine's write judge on every row written through it (every row this server writes carries `1`).
14316
+ engine's write judge on every row written through it (server >= 7.135.0 writes `4` — the engine's `SESSION_RULES_GRAMMAR`; 7.129.0 wrote `1`).
14338
14317
  Absent = a row stored before the stamp (grammar 0: an allow entry spelled as a rule, `Bash(rm:*)`,
14339
14318
  named no tool and admitted nothing — the engine keeps reading such a row that way and announces it
14340
14319
  once; the one-time migration rewrites stored rows and stamps them). Echoing a stored body back into
@@ -14355,6 +14334,18 @@ components:
14355
14334
  rev: { type: integer }
14356
14335
  gen: { type: integer }
14357
14336
  grammar: { type: integer }
14337
+ operatorRules: { $ref: '#/components/schemas/SessionRulesOperatorPart' }
14338
+
14339
+ SessionRulesOperatorPart:
14340
+ type: object
14341
+ description: '`StoredSessionRules.operatorRules` (server >= 7.135.0): the three buckets plus their own grammar stamp. The `grammar` is always present (the write judge stamps it).'
14342
+ additionalProperties: false
14343
+ required: [grammar]
14344
+ properties:
14345
+ toolAllow: { type: array, items: { type: string } }
14346
+ toolDeny: { type: array, items: { type: string } }
14347
+ allowDirs: { type: array, items: { type: string } }
14348
+ grammar: { type: integer, minimum: 0 }
14358
14349
 
14359
14350
  McpServerStatus:
14360
14351
  type: object
@@ -14685,26 +14676,6 @@ components:
14685
14676
  parentId: { type: [string, 'null'] }
14686
14677
  type: { type: string, description: Entry discriminator (message / leaf / compaction / …) — opaque to the SDK. }
14687
14678
 
14688
- SyncSnapshot:
14689
- type: object
14690
- description: >
14691
- One E19 file snapshot keyed by a `SessionTreeEntry.id`. `manifest` = `[relPath, blobHash]` tuples; the blob
14692
- BYTES are NOT inlined (they ride the content-addressed blob routes).
14693
- # 封闭(census 批2 第三段,2026-07-30):server 侧类型+铸造点(`session-sync.ts` PULL 出 /
14694
- # `routes/session-sync.ts` PUSH 校验形)都恰是这两键。
14695
- additionalProperties: false
14696
- required: [key, manifest]
14697
- properties:
14698
- key: { type: string }
14699
- manifest:
14700
- type: array
14701
- items:
14702
- type: array
14703
- minItems: 2
14704
- maxItems: 2
14705
- items: { type: string }
14706
- description: A `[relPath, blobHash]` tuple.
14707
-
14708
14679
  SyncAnchor:
14709
14680
  type: object
14710
14681
  description: One E18 resume-at anchor. `owner` is RE-KEYED to the importing principal on a PUSH (§9).
@@ -14732,31 +14703,28 @@ components:
14732
14703
  description: >
14733
14704
  2c session-sync `GET …/sync/manifest` payload (wrapped server-side as `{ manifest }`). The THIN cross-backend
14734
14705
  snapshot: entry IDS (oldest-first, NOT payloads) + the file-history envelope + policy + anchors + leaf.
14735
- 🔴 server >= 7.52 (S-15, design/381; re-read byte for byte against 7.116.0): the key `snapshots` (per-snapshot
14736
- relPath→blobHash, the retired E19 whole-tree snapshot epoch) is NO LONGER SENT — the server's `exportSessionManifest` mints
14737
- `{ sessionId, entryIds, entryCount, leafId, fileHistory, policy, anchors }` (server >= 7.125.0 adds the session row's two writable bits `title` / `pinned`,
14738
- present-iff the session is a row of the list face — see the two properties) and `fileHistory` (core's versioned
14739
- `FileHistoryExport` envelope; `null` = no history / the source store cannot export) replaces it. <= 7.51 servers still send
14740
- `snapshots` and no `fileHistory`. The blob BYTES never ride the manifest: content-addressed, via the history blob faces
14741
- (`GET …/sync/history/blobs/{hash}` / `PUT …/sync/blobs/{hash}`; probe `capabilities.sessionSyncFileHistory` on BOTH ends).
14742
- The SDK's `manifest()` fills `snapshots: []` when the wire omits it.
14706
+ The server's `exportSessionManifest` mints `{ sessionId, entryIds, entryCount, leafId, fileHistory, policy, anchors }` (server >= 7.125.0 adds the
14707
+ session row's two writable bits `title` / `pinned`, present-iff the session is a row of the list face — see the two properties). `fileHistory`
14708
+ (core's versioned `FileHistoryExport` envelope; `null` = no history / the source store cannot export) is ALWAYS present on a supported server
14709
+ (>= 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
14710
+ never ride the manifest: content-addressed, via the history blob faces (`GET …/sync/history/blobs/{hash}` / `PUT …/sync/blobs/{hash}`; probe
14711
+ `capabilities.sessionSyncFileHistory` on BOTH ends). The SDK returns the wire verbatim.
14743
14712
  # 封闭(census 批2 第三段,2026-07-30;16.2.0 按 7.116.0 发布字节重核):铸造点 `session-sync.ts`(exportSessionManifest 尾部
14744
14713
  # 字面量)恰是 `{ sessionId, entryIds, entryCount, leafId, fileHistory, policy, anchors }` 7 键(≥7.52),无 `snapshots`;
14745
14714
  # server ≥7.125.0 另有 `title` / `pinned` 两键(会话行可写位,present-iff 会话是列表面上的行;条件展开,不进 required);
14746
- # ≤7.51 老 server 发 `snapshots`、无 `fileHistory` ⇒ 两键都不进 required(支持底线 3.0.0)。
14715
+ # sdk 26.0.0(支持底线 7.52.0):`fileHistory` 进 required(支持面内恒铸),`snapshots` 删(≤7.51 才发)。
14747
14716
  additionalProperties: false
14748
- required: [sessionId, entryIds, entryCount, leafId, policy, anchors]
14717
+ required: [sessionId, entryIds, entryCount, leafId, fileHistory, policy, anchors]
14749
14718
  properties:
14750
14719
  sessionId: { type: string }
14751
14720
  entryIds: { type: array, items: { type: string }, description: Oldest-first, verbatim cross-backend. }
14752
14721
  entryCount: { type: integer, description: '= entryIds.length (the `/sync/entries` trailer count).' }
14753
14722
  leafId: { type: [string, 'null'] }
14754
- 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`.' }
14755
14723
  fileHistory:
14756
14724
  oneOf:
14757
14725
  - { $ref: '#/components/schemas/FileHistoryExport' }
14758
14726
  - { type: 'null' }
14759
- 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`).'
14727
+ description: 'the per-edited-file rewind history envelope; `null` = no history / the source store cannot export. Always present on a supported server.'
14760
14728
  policy: { type: array, items: { $ref: '#/components/schemas/SessionRulesRecord' } }
14761
14729
  anchors: { type: array, items: { $ref: '#/components/schemas/SyncAnchor' } }
14762
14730
  title:
@@ -14819,20 +14787,18 @@ components:
14819
14787
  SessionBundle:
14820
14788
  type: object
14821
14789
  description: >
14822
- The portable state of ONE session, ready to replay into another backend. Blobs are NOT inlined (each
14823
- snapshot carries only its `manifest`; the bytes stream through the content-addressed blob routes). The SDK
14824
- assembles this from a manifest + the streamed entries for a PUSH. server >= 7.52: the file history rides `fileHistory`
14825
- (the `FileHistoryExport` envelope) instead of `snapshots`.
14826
- required: [sessionId, entries, snapshots, policy, anchors]
14790
+ The portable state of ONE session, ready to replay into another backend. Blobs are NOT inlined (the file-history
14791
+ envelope carries only blob hashes; the bytes stream through the content-addressed blob routes). The SDK
14792
+ assembles this from a manifest + the streamed entries for a PUSH.
14793
+ required: [sessionId, entries, fileHistory, policy, anchors]
14827
14794
  properties:
14828
14795
  sessionId: { type: string }
14829
14796
  entries: { type: array, items: { $ref: '#/components/schemas/SyncEntry' } }
14830
- snapshots: { type: array, items: { $ref: '#/components/schemas/SyncSnapshot' } }
14831
14797
  fileHistory:
14832
14798
  oneOf:
14833
14799
  - { $ref: '#/components/schemas/FileHistoryExport' }
14834
14800
  - { type: 'null' }
14835
- description: 'server >= 7.52 file-history envelope (same as `SessionManifest.fileHistory`); key absent = the source is a <= 7.51 server.'
14801
+ 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).'
14836
14802
  policy: { type: array, items: { $ref: '#/components/schemas/SessionRulesRecord' } }
14837
14803
  anchors: { type: array, items: { $ref: '#/components/schemas/SyncAnchor' } }
14838
14804
 
@@ -15020,8 +14986,20 @@ components:
15020
14986
  <= 7.105.0 answered the synchronous leg 200 + a failed body), `corrupt` (500 — the session rule store
15021
14987
  cannot read its own bytes), `internal.memory_control_plane_corrupt` (500 — the memory control plane refuses
15022
14988
  fail-closed) and `state.scenario_assignment_unknown` (503, server >= 7.119.0 — the center's scenario assignment for the
15023
- principal is dangling; only re-pointing the assignment, or adding the scenario back to the deployment, helps). Every other 5xx
15024
- code keeps the status-based retry.
14989
+ principal is dangling; only re-pointing the assignment, or adding the scenario back to the deployment, helps) and
14990
+ `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
14991
+ delete the row; server >= 7.135.0 the same judge answers `GET /v1/assistant/inbox` and `GET /v1/assistant/tasks` too) and `workflow.journal_incompatible`
14992
+ (500, server >= 7.135.0 on `GET /v1/workflows/{id}/journal` — a journal row the engine's judge rejects; the body carries `ordinal`; repair the row from the retained copy, never by deleting it).
14993
+ Every other 5xx code keeps the status-based retry.
14994
+ 🔴 server >= 7.135.0 read-failure and store-fault codes (all additive; branch on the code, never the sentence): `503 approvals.read_unavailable` on `GET /v1/approvals`, `/v1/assistant/inbox` and
14995
+ `/v1/assistant/tasks` (the queue cannot be read RIGHT NOW — transient, retried by status; until a read succeeds an answer does NOT mean nothing is pending; it moved from the SSE-only table into the status-bearing catalog, status `-` → 503) and, on
14996
+ the same three faces, `500 approvals.read_corrupt` (above); `503 state.file_history_unreadable` on the session-sync pull faces (`GET /v1/sessions/{id}/sync/manifest`, `GET /v1/sessions/{id}/sync/history/blobs/{hash}` — the file history
14997
+ cannot be read WHOLE; it is NOT "no history", which stays `200` with `fileHistory: null`); `422 adopt_failed` on `POST /v1/sessions/{id}/sync/import` (the file-history replay hit a store fault; 7.134.x answered `unsupported_format`;
14998
+ NOT a missing blob — that is `422 missing_blob`, now answered in the FIRST phase). The two store-fault codes may carry `permanence` (`transient` = nothing changed, retry the same shape later / `terminal` = repair the store first; absent = the store did not judge).
14999
+ `422 request.unprocessable` now also answers a deny / ask rule whose PARAMETER does not belong to the tool (the sentence carries the engine code `unsupported.param_unknown`, the same word in `detail.problems[].reason`; a stored body that
15000
+ resumes ⇒ `409 resume_blocked_by_policy` with `blockedBy: request.unprocessable`; 7.134.x accepted such a rule, and it never matched). `400 request.body_shape` also names an unread key inside a `settings.hooks` group / entry in
15001
+ `unknownKeys` as a FULL coordinate (`settings.hooks.PreToolUse.0.hooks.0.onFailure`) on a fresh submit.
15002
+ `409 resume_outcome_invalid` + `field: "feedback"` (`retriable: true`) now answers a release `feedback` that carries an engine `<system-reminder>` tag (refused before the CAS, the card stays pending; fix or drop the words and decide again — it was `409 approval_binding_mismatch` without `field` on 7.134.x). A `feedback` on a deny / policy decision stays `400 request.field_conflict` (`detail.kind: approval_feedback`).
15025
15003
  🔴 `request.host_posture_refused` (422) has TWO triggers (same code, same status — "you wrote it right, this
15026
15004
  deployment will not grant that host posture"): (1) the write-containment face (`permissionMode: bypassPermissions` ⇒
15027
15005
  `writeFace: open`; read `capabilities.taskWriteFaceOpen` first); (2) server >= 7.106.0 / S-895: `inheritEnv: "all"` on a
@@ -15085,17 +15063,30 @@ components:
15085
15063
  the ids of the background children rooted at this session that are still alive (count = length) — the SAME
15086
15064
  strings as the `handle` on `GET /v1/agents/roster` rows. Nothing was deleted; wait for them to finish (or stop
15087
15065
  them where they run) and retry the delete verbatim. SDK: `err.extra.remaining` on the ConflictError.
15066
+ permanence:
15067
+ type: string
15068
+ enum: [transient, terminal]
15069
+ description: >-
15070
+ server >= 7.135.0, on `503 state.file_history_unreadable` and `422 adopt_failed` only: the file-history store's own judgment of the fault (the engine's `GovernanceRetryClass`) —
15071
+ `transient` = nothing was changed, retrying the same shape later may succeed; `terminal` = repair the store first. ABSENT = the store did not judge (retry later once; still red ⇒ look at the store). SDK: `err.extra.permanence`.
15072
+ ordinal:
15073
+ type: integer
15074
+ description: >-
15075
+ server >= 7.135.0, on `500 workflow.journal_incompatible`: the journal ordinal of the FIRST bad row (ascending); `-1` = the refusal names no entry. SDK: `err.extra.ordinal`.
15088
15076
  reason:
15089
15077
  type: string
15090
15078
  description: >-
15091
- A per-code closed word — read it by `errorCode`, never across codes. On 503 `fleet.lease_unavailable` (server >=
15079
+ A per-code closed word — read it by `errorCode`, never across codes. On 400 `request.field_invalid` for a URL that carries a credential (server >= 7.135.0): `url_userinfo` (`user:pass@host`) / `url_credential_param` (a credential-named
15080
+ query / fragment parameter — the names the service's log redactor treats as credentials), beside `field` = the body coordinate (`mcpServers[i].transport.url`, `a2aPeers[i].url`, `a2aPeers[i].cardUrl`,
15081
+ `settings.hooks.<event>[g].hooks[h].url` — the FULL coordinate, not `field: "settings.hooks"` + `detail` — and `settings.webSearch.endpoint`; a `settings.webSearch.endpoint` with userinfo keeps its sentence and gains `field` / `reason`);
15082
+ `a2aPeers` over 32 entries answers `over_cap` + `count` + `cap` on `field: "a2aPeers"`. On 503 `fleet.lease_unavailable` (server >=
15092
15083
  7.106.0 / S-866 ③): `auth-rejected` (the center refused this engine's credential, or the credential file is
15093
15084
  unreadable) / `unreachable` (unreachable / 5xx / timeout / bad body / any status other than 200 / 409 / 400 / 401 / 403 —
15094
15085
  center 404 / 429 / 3xx included). On 400 `request.field_invalid` for a `settings.permissions` bucket (server >= 7.106.0 /
15095
15086
  S-904): `over_cap` (more than `cap` entries; beside `count` + `cap`) / `bad_entry` (a non-string or empty item; beside
15096
15087
  `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
15097
15088
  `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).
15098
- 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.)
15089
+ 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.)
15099
15090
  retryAfterSec:
15100
15091
  type: number
15101
15092
  description: >-
@@ -15122,6 +15113,10 @@ components:
15122
15113
  `null` and `""` count), the refusal landed before any decision, and `field` is the literal `settledBy`; the three doors now answer
15123
15114
  the SAME sentence. Every such 400 carries it, even when another defect in the same body was hit first (the sentence then names that
15124
15115
  defect). Branch on the code plus `field`, never on the sentence; a bad body WITHOUT that key carries no `field`. SDK: `err.extra.field`.
15116
+ 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
15117
+ `[i]`, record members `.key`: `mcpServers[0].transport.headers.Authorization`, `settings.hooks.PreToolUse[0].hooks[0].headers.Authorization`; the key
15118
+ name is not a secret, the value never enters the refusal); on 400 `request.field_invalid` for the request key `settings.approvalFrameFamilies` it is
15119
+ that literal (with `reason: "bad_entry"` and the bad `index`; no `index` for a non-array).
15125
15120
  detail:
15126
15121
  description: >
15127
15122
  🔴 SHAPES differ by code — branch on `field` (or the `errorCode`), never guess from the type of `detail`. On 503
@@ -15256,8 +15251,9 @@ components:
15256
15251
  callerTrust: { type: string }
15257
15252
  - type: object
15258
15253
  description: >-
15259
- server >= 7.133.0 — 400 `request.field_conflict`: the request body contradicts itself. `kind` = which constraint (a CLOSED fourteen-word set, read open: cascade_verify | image_profile | resume_anchor |
15260
- 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);
15254
+ 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 |
15255
+ 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 |
15256
+ approval_feedback (server >= 7.134.0: `feedback` on a deny — fields `["feedback", "decision"]` — or on a policy decision — `["feedback", "decidedBy"]`));
15261
15257
  `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).
15262
15258
  required: [kind, fields]
15263
15259
  additionalProperties: false
@@ -15303,6 +15299,8 @@ components:
15303
15299
  the engine's `detail.code` passes through as `blockedBy` (today only `checkpoint.config_refused` carries it — the closed set is core's
15304
15300
  `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"),
15305
15301
  next to `field` and `reason`, absent keys not minted (SDK `ResumeRefusalExtra`).
15302
+ 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
15303
+ 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).
15306
15304
 
15307
15305
  ResumeLegConflictError:
15308
15306
  description: >
@@ -15386,6 +15384,9 @@ components:
15386
15384
  # 🔴 census 轴一(2026-07-30)补的 4 臂:server 今天就真发,spec/SDK 两侧此前同缺(详见各 schema 处的注)。
15387
15385
  - $ref: '#/components/schemas/Event_context_usage'
15388
15386
  - $ref: '#/components/schemas/Event_config_assembled'
15387
+ # 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
15388
+ # on the live stream and never on the `GET /v1/runs/{taskId}` poll object; the trace SSE twin is the `cross-boundary-taint` frame.
15389
+ - $ref: '#/components/schemas/Event_cross_boundary_taint'
15389
15390
  - $ref: '#/components/schemas/Event_needs_review'
15390
15391
  - $ref: '#/components/schemas/Event_message_committed'
15391
15392
  # [2373]A-2(2026-08-03,server 5.0.0):compaction_outcome 三腿同源批的 spec 半场(SDK 臂 20867e1 同批)。
@@ -15464,6 +15465,7 @@ components:
15464
15465
  failed: '#/components/schemas/Event_failed'
15465
15466
  context_usage: '#/components/schemas/Event_context_usage'
15466
15467
  config_assembled: '#/components/schemas/Event_config_assembled'
15468
+ cross_boundary_taint: '#/components/schemas/Event_cross_boundary_taint'
15467
15469
  needs_review: '#/components/schemas/Event_needs_review'
15468
15470
  message_committed: '#/components/schemas/Event_message_committed'
15469
15471
  compaction_outcome: '#/components/schemas/Event_compaction_outcome'
@@ -15690,10 +15692,10 @@ components:
15690
15692
  # 每只闭集 = 引擎今天的词 ∪ 引擎退役词表里那一集。这五张 enum 与引擎 `RETIRED_MEMBERS` 逐集逐词同(由 `server-7129-wire` 对拍);卡面 / 队列行 / 请求面的同名闭集只取今天的词。
15691
15693
  GateRecordAskClass:
15692
15694
  type: string
15693
- 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]
15695
+ 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]
15694
15696
  description: >-
15695
15697
  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:
15696
- 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`,
15698
+ 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`,
15697
15699
  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`);
15698
15700
  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`.
15699
15701
  Render a known archive word normally and an unknown one generically; never drop the record because the word is not in today's set.
@@ -15711,9 +15713,9 @@ components:
15711
15713
  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`.
15712
15714
  GateRecordPersistedRuleMandate:
15713
15715
  type: string
15714
- enum: [operator_always, tool_marks, probe_mandate, probe_unanswered, write_protection, target_unresolved, write_protection_unresolved]
15716
+ enum: [operator_always, tool_marks, probe_mandate, probe_unanswered, write_protection, target_unresolved, cross_boundary_taint, write_protection_unresolved]
15715
15717
  description: >-
15716
- `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`.
15718
+ `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`.
15717
15719
  GateRecordAskOrigin:
15718
15720
  type: string
15719
15721
  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]
@@ -15893,6 +15895,12 @@ components:
15893
15895
  K-32 (server >= 7.106.0 / core 7.34.0 design/1111) — `bypassPermissions` RELEASED an ask nobody answered (see
15894
15896
  `ModeRelease`). Absent = this allow was not a mode release (a plain allow / a person's yes / a rule), or an older
15895
15897
  server (<= 7.105.0 stripped it).
15898
+ clearedBy:
15899
+ allOf: [{ $ref: '#/components/schemas/AllowRuleClearance' }]
15900
+ description: >
15901
+ server >= 7.135.0 (core 7.55.0) — the allow layer's account of a call it let run WITHOUT asking: which member's allow rule cleared a standing ask that nobody answered
15902
+ (`AllowRuleClearance`). Present exactly when an allow member cleared the ask; absent on a call nothing asked about, a mode's release or acceptance, the read-only arm's,
15903
+ the classifier's or the sandbox's allow, and a person's yes. Never present together with `released`. Older servers stripped it.
15896
15904
  - type: object
15897
15905
  additionalProperties: false
15898
15906
  required: [kind, deniedBy]
@@ -15944,9 +15952,10 @@ components:
15944
15952
  `UNATTENDED_PERMISSION_MODE=dontAsk`).
15945
15953
  AskClass:
15946
15954
  type: string
15947
- 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]
15955
+ 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]
15948
15956
  description: >
15949
- 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
15957
+ 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
15958
+ 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
15950
15959
  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
15951
15960
  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
15952
15961
  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
@@ -16349,6 +16358,10 @@ components:
16349
16358
  `MCP_FAILURE_CODES` spellings — including `network` and the `http_<status>` encoding — are gone,
16350
16359
  and an HTTP status now rides the structured `httpStatus` seat on `wiring_manifest.mcp[]` instead of
16351
16360
  being baked into the code. The set as a WHOLE stays open (a tool owns its own codes).
16361
+ 🔴 core 7.55.0 / server >= 7.135.0 — two MCP words minted BEFORE the call is sent (`delivered: "no"`; the server has no mint point, they pass through verbatim): after the engine redials a Streamable HTTP
16362
+ server that LOST its session, `mcp.tool_declaration_changed` (the server now declares the tool differently — safety axis / read-only reading / the "a person must answer" mark / task support — or no longer lists it) and
16363
+ `mcp.tool_listing_unreached` (the post-redial listing walk stopped before reaching the tool, so its new declaration cannot be proved). Every later call to that tool is refused the same way until a listing of the connection
16364
+ reaches it — the way out is `POST /v1/sessions/{id}/mcp/reconnect` (or the engine's refresh); do not resend as-is. The lost-session failure itself stays `http_status` + `httpStatus: 404` + `delivered: "unknown"`.
16352
16365
  Consumers MUST branch with a `default` arm and MUST NOT exhaustively switch on known values.
16353
16366
  The service enforces only a length bound (>128 chars = malformed, the whole key is DROPPED rather
16354
16367
  than truncated — truncating would mint a code core never sent). NOTE: this is a DIFFERENT
@@ -16698,6 +16711,21 @@ components:
16698
16711
  type: object
16699
16712
  additionalProperties: false
16700
16713
  properties: { kind: { type: string, description: 'root | child | resume. Open on read.' } }
16714
+ sessionRules:
16715
+ type: object
16716
+ additionalProperties: false
16717
+ required: [childrenReadLive]
16718
+ description: >
16719
+ server >= 7.135.0 (engine 7.55.0; effective half only, both faces): whether the session-rules layer is handed to the delegated legs. `childrenReadLive: true` = this leg wired the session-rules layer (the deployment has a session-rules
16720
+ store and the task carries a session id) and handed the session's key to every leg it delegates (sync / background / fork / workflow / resume / observer side-car, any depth) — a rule written into the session NOW governs an
16721
+ already-running child's NEXT call; `false` = no rules layer, nothing handed out. The section ABSENT = an older engine (say nothing; do not read it as `false`). An unreadable section (not `{ childrenReadLive: <boolean> }`) is dropped.
16722
+ properties:
16723
+ childrenReadLive: { type: boolean }
16724
+ projectMemory:
16725
+ type: string
16726
+ description: >
16727
+ server >= 7.135.0 (engine 7.55.0; effective half only, both faces): did this leg call the deployment's project-context seat — `on` = it did; `disabled` = the seat is wired but the request's `projectMemory: "disabled"` turned it off;
16728
+ `absent` = the deployment wired no seat (whatever the request said). On `disabled` / `absent` legs the server-minted `instructions` section is ABSENT. Open on read (a new engine word passes through); ABSENT = an older engine.
16701
16729
  ask:
16702
16730
  type: object
16703
16731
  additionalProperties: false
@@ -17164,6 +17192,16 @@ components:
17164
17192
  pathTarget: { $ref: '#/components/schemas/McpToolPathTarget' }
17165
17193
  ruleFace: { $ref: '#/components/schemas/McpToolRuleFace' }
17166
17194
  renderHints: { $ref: '#/components/schemas/McpToolRenderHints' }
17195
+ boundaryLegs:
17196
+ description: >-
17197
+ 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
17198
+ `delta.roster.entries[]` carries it) — the LEGS a call of this tool travels (core `BOUNDARY_LEGS`, in that order: `env` = inside the execution
17199
+ 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
17200
+ 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
17201
+ it. ABSENT = an older server (<= 7.133.x).
17202
+ oneOf:
17203
+ - { type: string, const: per_call }
17204
+ - { type: array, items: { type: string, enum: [env, outside_read, outside_write] } }
17167
17205
  McpToolPathTarget:
17168
17206
  type: object
17169
17207
  required: [param, access]
@@ -17279,6 +17317,10 @@ components:
17279
17317
  own failure carries the same code. `{ modelId, boundOrigin, requestOrigin, sessionId, runId }` — both origins in display spelling (scheme + host +
17280
17318
  port, no userinfo / path), never a credential or a digest of one. Once per run per (modelId, boundOrigin, requestOrigin). It replaces the retired
17281
17319
  operator code `route.base_url_changed_key_unchanged` (never on this whitelist). `config.credential_not_withheld` is operator-audience — ops log only.
17320
+ `advisor.not_attached` (server >= 7.135.0 / core 7.55.0; D.3 is now 42 codes) — the request's advisor model did NOT attach (the engine's pairing judge refuses the (advisor, main) pair).
17321
+ `{ advisorModel, mainModel, reason, sessionId, runId? }` — the two model names are the catalog id / the reference the request wrote (display spelling); `reason` is an engine word (`unknown_model` / `advisor_unranked` /
17322
+ `weakest_tier` / `main_unranked` / `pairing` / `hold_unknown`; the server echoes it — read OPEN). Unreachable on this server today: it does not accept `advisorModel` (the request body does not take it, and the
17323
+ `advisor` section is not projected on either `wiring_manifest` face).
17282
17324
  Value change: `permission.rule_row_dropped`'s `detail.rows[].rule` is, since core 7.53.0, the rule's original text (the same reading as
17283
17325
  `allowed_tool_not_granted`'s `entries[].entry`: at most 1 024 code points, longer cut to 1 024 + a truncation mark, CONTROL CHARACTERS RIDE AS DATA —
17284
17326
  neutralize at render); before it was the engine-neutralized single-line form (at most 201 code points). Same keys.
@@ -17865,44 +17907,6 @@ components:
17865
17907
  governance shell gate sits at the "classify" tier, a shell ask may come from the classifier or from
17866
17908
  another gate; the server leaves the key absent rather than guessing). Never render absence as
17867
17909
  "this gate can be bypassed by a posture".
17868
- ruleSuggestions:
17869
- type: array
17870
- minItems: 1 # 空数组不可能出现:server 的合取项②要求 `req.ruleSuggestions` **非空**,否则整键省略(缺席 = 本卡无此选项)
17871
- # 刻意**不**声明 maxItems:server 的 `.max(4)` 是**卡** schema(`ApprovalCardSchema`)上的约束,这条
17872
- # live 帧腿没有它。在这里抄一个上界 = 声明一条 server 并不执行的约束(照 spec 生成的严格客户端会
17873
- # 拒收一条合法帧)。数量上界的真源是引擎:core 5.22.0 的 `suggestRulesForCommand` **至多铸一条,且恒
17874
- # `match: "exact"`**(不写死上界是给将来的 miner 留位,不是暗示今天会有更多)。
17875
- items: { $ref: '#/components/schemas/RuleSuggestion' }
17876
- description: >
17877
- server #154 车二 (core 5.18.0, design/179), ADDITIVE, "tool_approval" only: the rule candidates
17878
- the ENGINE minted for this ask. Render them as the "don't ask again" options and echo the chosen
17879
- one back as `persistRule.rule` on POST /v1/tool-approvals/{id}/respond.
17880
-
17881
- 🔴 PRESENCE IS THE PROMISE: the key is present ONLY on a deployment that actually has a
17882
- permission-rule store wired (the same object behind the engine's `permissionRules.storeWired`
17883
- manifest bit). A deployment without one OMITS it rather than offering an option with nowhere to
17884
- redeem — an unredeemable "don't ask again" is a wire lie, worse than absence.
17885
-
17886
- 🔴 THE CONVERSE DOES NOT HOLD: absence means "THIS CARD has no redeemable candidate", and three
17887
- causes produce it — no rule store wired, the engine minted no candidate for this command (compound /
17888
- redirect / substitution), or the command bytes were unreadable. One frame cannot tell them apart, so
17889
- never diagnose the deployment's rule lane from a single absence, and never synthesize a candidate.
17890
-
17891
- 📏 HOW MANY, TODAY: core 5.22.0's miner (`suggestRulesForCommand`) emits AT MOST ONE candidate and
17892
- always `match: "exact"` — no shipped producer mints the broader prefix option. `prefix` stays in the
17893
- closed vocabulary because the wire validator accepts it and a later miner may emit it; render what
17894
- arrives, but do NOT build a UI that assumes a narrow/broad PAIR is on offer.
17895
-
17896
- 🔴 The values are the engine's VERBATIM (closed `match` vocabulary + engine-minted text); the
17897
- server neither re-mints nor re-orders them. A hand-built rule string is refused at redemption
17898
- (`ruleRefusal: "rule_not_offered"`) — by construction, so a click on a harmless command cannot be
17899
- turned into a rule for something else.
17900
-
17901
- ⚠️ REDEMPTION SCOPE: the key also rides the durable card replay, but its only redemption channel is
17902
- the LIVE respond leg keyed by this frame's `approvalId` — an in-process map on ONE replica. Same
17903
- replica ⇒ redeemable; after a restart / on another replica that `approvalId` 404s and the person
17904
- must use the durable decide leg, which carries NO rule field in v1 (its body is strict — an extra
17905
- `persistRule` is a loud 400, not a silent drop).
17906
17910
  ruleOffers:
17907
17911
  type: array
17908
17912
  minItems: 1
@@ -17910,8 +17914,8 @@ components:
17910
17914
  description: >
17911
17915
  server >= 7.46.0 (core 5.58.0, design/375), "tool_approval" only — the ENGINE-minted "don't ask
17912
17916
  again" OPTIONS for this ask, as a DISCRIMINATED UNION (`single` | `batch`). It is the BREAKING
17913
- REPLACEMENT of `ruleSuggestions` above: a >= 7.46.0 engine mints the old key nowhere, there is no
17914
- alias, and the two keys are declared side by side only so one type surface covers both generations.
17917
+ REPLACEMENT of the triple-shaped `ruleSuggestions` of servers <= 7.45: a >= 7.46.0 engine mints the old key nowhere and there is no
17918
+ alias (sdk 26.0.0 deleted the old key with the support floor 7.52.0).
17915
17919
 
17916
17920
  🔴 READ `kind` TO TELL THE ARMS APART, never a positional assumption. ORDER IS CONTRACT: a
17917
17921
  whole-string exact `single` is index 0 when present and a `batch` is always last; core's own
@@ -17926,23 +17930,15 @@ components:
17926
17930
  suppress its persistence actions entirely (core's normative clause) — a compacted index means the
17927
17931
  k-th option the person clicked is not the k-th rule the server redeems.
17928
17932
 
17929
- 🔴 PRESENCE IS THE PROMISE (same as `ruleSuggestions`): present only where a permission-rule store
17933
+ 🔴 PRESENCE IS THE PROMISE: present only where a permission-rule store
17930
17934
  is really wired. The converse does not hold — a wired deployment still omits it for mandated /
17931
17935
  requiresRealApproval / allow_outranked / ask_rule_matched / hook-produced / inherited-unresolved / ancestor-resolved /
17932
17936
  anonymous asks. Absence reads as "THIS CARD has no such option", never as "this deployment has no
17933
17937
  rule lane" (that is `capabilities.permissionRules`) and never as "your rule broke" (that is
17934
- `allowRuleShadowed`; `persistedRuleShadowed` on a server <= 7.116.x).
17938
+ `allowRuleShadowed`).
17935
17939
 
17936
17940
  ⚠️ REDEMPTION SCOPE is the live respond leg keyed by this frame's `approvalId` only; a replayed
17937
17941
  offer on another replica is not a promise that it is still redeemable.
17938
- persistedRuleShadowed:
17939
- type: string
17940
- description: >-
17941
- server >= 7.13.0 (core 5.25.0 #144), ADDITIVE, "tool_approval" only — this ask MATCHED one of the caller's persisted allow
17942
- rules and that rule COULD NOT silence it; the value is the rule's VERBATIM text (secret-redacted + length-bounded; UNTRUSTED
17943
- for display, like `message`). Absent != "you have no rule"; deliberately separate from `governanceForced`.
17944
- RETIRED: server >= 7.117.0 (core 7.42.0, BREAKING rename, no alias) never mints this key on any face; its successor is
17945
- `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).
17946
17942
  allowRuleShadowed:
17947
17943
  type: array
17948
17944
  items: { $ref: '#/components/schemas/AllowRuleShadow' }
@@ -18220,17 +18216,18 @@ components:
18220
18216
  classifierUnavailable:
18221
18217
  $ref: '#/components/schemas/ClassifierUnavailable'
18222
18218
  description: >-
18223
- server >= 7.69.0 (core 7.10.0 #616), ADDITIVE, present only when the classifier was consulted and
18219
+ server >= 7.69.0 (core 7.10.0), ADDITIVE, present only when the classifier was consulted and
18224
18220
  could not run — WHY this ask reached a human. See ClassifierUnavailable for the open-`cause` rule and
18225
18221
  the three shapes absence covers.
18226
- RETIRED: server >= 7.72.0 (core 7.14.0 #688) never mints this key on any of the three faces; its
18222
+ RETIRED: server >= 7.72.0 (core 7.14.0) never mints this key on any of the three faces; its
18227
18223
  successor is `disposition.cause` on the gate record (`unavailable` / `parse_error` — a different
18228
- question, so an old switch does not port). Kept for servers in [7.69.0, 7.72.0) only (sdk 9.2.0).
18224
+ 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
18225
+ 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.
18229
18226
  ruleStoreUnreadable:
18230
18227
  type: string
18231
18228
  enum: [store, call]
18232
18229
  description: >-
18233
- server >= 7.72.0 (core 7.14.0 #688 C3), ADDITIVE — WHICH HALF of the rule lane could not be read.
18230
+ server >= 7.72.0 (core 7.14.0), ADDITIVE — WHICH HALF of the rule lane could not be read.
18234
18231
  Present iff `origin === "rule_store_unavailable"`: `store` = the wired rule store could not be read
18235
18232
  (look at the store / network); `call` = the store was read but THIS command could not be read against
18236
18233
  the person's deny/ask rows (look at the command spelling). One `origin` word covers both facts, and
@@ -18238,10 +18235,11 @@ components:
18238
18235
  frame and the inbox row; the `card_json` card deliberately does NOT carry it (it travels with `origin`,
18239
18236
  which is not on the card). Absence = `origin` is not that word — NOT an assertion that the rule store
18240
18237
  is healthy.
18241
- RETIRED: server >= 7.106.0 (core 7.34.0 C-R172, BREAKING — wire) never mints this key on either face — an
18242
- unreadable rule store no longer raises a card: that read's rows are SKIPPED by partition and the session
18243
- stream gets the user-facing `engine_notice` `permission.rule_store_unavailable` (`detail.at` = `prepare` /
18244
- `call`). Kept for servers in [7.72.0, 7.106.0) only (sdk K-32); `origin: rule_store_unavailable` itself stays.
18238
+ RETIRED: server >= 7.106.0 (core 7.34.0, BREAKING — wire) never mints this key on either face — an
18239
+ unreadable rule store no longer raises a card; the signal moved to the session stream's user-facing
18240
+ `engine_notice` `permission.rule_store_unavailable` (`detail.at` = `prepare` / `call`; what the run does while the rules cannot be read is
18241
+ 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
18242
+ declares the same key; it goes with the next major once the support floor is >= 7.106.0 (`origin: rule_store_unavailable` itself stays).
18245
18243
  readRootCandidate:
18246
18244
  type: object
18247
18245
  required: [dir, clearsThisAsk]
@@ -18395,33 +18393,15 @@ components:
18395
18393
  posture. Semantics, the ABSENT != false clause and the discrimination boundary are stated verbatim on
18396
18394
  ToolApprovalFrame.governanceForced. It sits at the card's top level rather than inside `risk` because
18397
18395
  `risk` describes the ENGINE's judgement of the call, while this key says WHO imposed the gate.
18398
- ruleSuggestions:
18399
- type: array
18400
- minItems: 1 # 同帧腿:空数组不可能出现(素材同源),缺席才是「本卡无此选项」
18401
- maxItems: 4 # 卡形上**真有**这条上界(server `ApprovalCardSchema` 的 `.max(4)`);live 帧腿没有,故只写在这里
18402
- items: { $ref: '#/components/schemas/RuleSuggestion' }
18403
- description: >
18404
- ADDITIVE (#154 车二 / core 5.18.0 design/179): the rule candidates the ENGINE minted for this ask —
18405
- the card's "don't ask again" options; echo the chosen one back as `persistRule.rule`. Semantics,
18406
- the PRESENCE-IS-THE-PROMISE clause (present only where a rule store is wired) and the redemption
18407
- scope are stated verbatim on ToolApprovalFrame.ruleSuggestions — the live frame, the stored
18408
- `card_json` and the replayed frame carry the SAME material.
18409
18396
  ruleOffers:
18410
18397
  type: array
18411
18398
  minItems: 1
18412
18399
  items: { $ref: '#/components/schemas/RuleOffer' }
18413
18400
  description: >
18414
- server >= 7.46.0 (core 5.58.0, design/375) — the discriminated-union replacement of
18415
- `ruleSuggestions` above. Same material as the live frame and the stored `card_json` (the server
18401
+ server >= 7.46.0 (core 5.58.0, design/375) — the discriminated-union replacement of the retired
18402
+ `ruleSuggestions` (servers <= 7.45; sdk 26.0.0 deleted it). Same material as the live frame and the stored `card_json` (the server
18416
18403
  projects all three through one `copyRuleOffer`), so semantics, order-is-contract, presence-is-the-
18417
18404
  promise and the per-leg index rules are stated verbatim on ToolApprovalFrame.ruleOffers.
18418
- persistedRuleShadowed:
18419
- type: string
18420
- description: >-
18421
- server >= 7.13.0 (core 5.25.0 #144), ADDITIVE — the persisted rule this ask matched but which could not silence it; frame and
18422
- card carry the SAME material (see ToolApprovalFrame.persistedRuleShadowed).
18423
- 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
18424
- carrying it); its successor is `allowRuleShadowed`. Kept for servers in [7.13.0, 7.117.0) only (sdk 16.3.0).
18425
18405
  allowRuleShadowed:
18426
18406
  type: array
18427
18407
  items: { $ref: '#/components/schemas/AllowRuleShadow' }
@@ -18456,35 +18436,13 @@ components:
18456
18436
  classifierUnavailable:
18457
18437
  $ref: '#/components/schemas/ClassifierUnavailable'
18458
18438
  description: >-
18459
- server >= 7.69.0 (core 7.10.0 #616), ADDITIVE — WHY this ask reached a human. Frame and card carry
18460
- the SAME value (one narrow-read function server-side). See ClassifierUnavailable for the open-`cause`
18461
- rule and the three shapes absence covers.
18462
- RETIRED: server >= 7.72.0 (core 7.14.0 #688) never mints this key on any of the three faces; its
18439
+ (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
18440
+ shapes absence covers.
18441
+ RETIRED: server >= 7.72.0 (core 7.14.0) never mints this key on any of the three faces; its
18463
18442
  successor is `disposition.cause` on the gate record (`unavailable` / `parse_error` — a different
18464
- question, so an old switch does not port). Kept for servers in [7.69.0, 7.72.0) only (sdk 9.2.0).
18465
-
18466
- RuleSuggestion:
18467
- type: object
18468
- description: >
18469
- One ENGINE-minted rule candidate (#154 车二 / core 5.18.0 `RuleSuggestion`) — what a single click on
18470
- "don't ask again" would persist.
18471
-
18472
- 🔴 THE TEXT COMES FROM THE ENGINE, NOT THE SHELL: the server derives it from the adjudicated command's
18473
- own bytes; a client may only echo one of these back verbatim (`persistRule.rule`). Reporting text that
18474
- is not among the candidates is refused (`ruleRefusal: "rule_not_offered"`) rather than guessed at, so
18475
- "click on a harmless command, get a rule for something else" is not spellable on this path.
18476
- # 封闭:core `RuleSuggestion` 恰三键(rule/match/command),server 逐字透传不重铸。
18477
- additionalProperties: false
18478
- required: [rule, match, command]
18479
- properties:
18480
- rule: { type: string, maxLength: 512, description: 'The canonical rule text to echo back on redemption (server MAX_RULE_TEXT_CHARS = 512).' }
18481
- match:
18482
- type: string
18483
- enum: [exact, prefix]
18484
- 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.'
18485
- command: { type: string, maxLength: 512, description: 'The command pattern inside the rule, for a shell that would rather not re-parse `rule`.' }
18443
+ 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
18444
+ 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`.)
18486
18445
 
18487
- # ─── S-139(sdk 8.3.0):对 server 7.60.0 整体重对账带进来的四张型面 ───────────────────────
18488
18446
  ClassifierUnavailable:
18489
18447
  # 8.9.0:三面(tool_approval 帧 / ApprovalCard / InboxRow)同形同值 ⇒ 具名单源,内联三份会各自漂。
18490
18448
  type: object
@@ -18567,6 +18525,27 @@ components:
18567
18525
  The closed five-word vocabulary of `AllowRuleShadow.member` (server >= 7.117.0 / core 7.42.0 `ALLOW_RULE_MEMBERS`, verbatim and in the order the engine reads the members). The SDK exports
18568
18526
  it at runtime as `ALLOW_RULE_MEMBERS`. Owner = core; read it as an open string with a default arm (core may add a member and the server follows).
18569
18527
 
18528
+ AllowRuleClearance:
18529
+ type: object
18530
+ additionalProperties: false
18531
+ required: [member, entries]
18532
+ description: >
18533
+ `tool_end.gate.disposition.clearedBy` (server >= 7.135.0 / core 7.55.0 `AllowRuleClearance`): the allow-layer member whose rule CLEARED a standing ask (the pair of `AllowRuleShadow`, which is
18534
+ "matched and could NOT clear"). Same `member` / `entries` / `omitted` shape (minted and capped by the same seat), plus `positions` for a LIST member (`request`, `deployment`): the index of every element of the
18535
+ list the run received that is one of those entries (a string listed twice has both indices), so a host reads which line of its own declaration spoke even where the shown text is capped.
18536
+ properties:
18537
+ member: { $ref: '#/components/schemas/AllowRuleMember' }
18538
+ entries:
18539
+ type: array
18540
+ maxItems: 32
18541
+ items: { type: string, maxLength: 512 }
18542
+ description: 'The entries that member stood on (UNTRUSTED for display, secret-redacted, <= 512 chars each, <= 32 listed).'
18543
+ omitted: { type: integer, minimum: 1, description: 'How many entries were not listed (beyond the cap). Absent = none omitted.' }
18544
+ positions:
18545
+ type: array
18546
+ items: { type: integer, minimum: 0 }
18547
+ description: 'LIST members only: the indices, in the list this run received, of the elements that are those entries. Absent for a non-list member.'
18548
+
18570
18549
  AllowRuleShadow:
18571
18550
  type: object
18572
18551
  additionalProperties: false
@@ -18599,7 +18578,7 @@ components:
18599
18578
  `ApprovalCard.ruleOffersAbsence`; server >= 7.55.0 / core #490 fix ②, S-15). The engine names which
18600
18579
  door is shut: `mandated` = this ask must be confirmed every time (🔴 this arm must NOT point the user
18601
18580
  at writing a rule — a rule would not silence it, and saying so sends them into a loop); `allow_outranked` = an
18602
- 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);
18581
+ 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`);
18603
18582
  `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
18604
18583
  `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).
18605
18584
  🔴 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
@@ -18634,10 +18613,12 @@ components:
18634
18613
  `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.
18635
18614
  PersistedRuleMandate:
18636
18615
  type: string
18637
- enum: [operator_always, tool_marks, probe_mandate, probe_unanswered, write_protection, target_unresolved]
18616
+ enum: [operator_always, tool_marks, probe_mandate, probe_unanswered, write_protection, target_unresolved, cross_boundary_taint]
18638
18617
  description: >
18639
18618
  WHICH WORD a mandated ask stands on (server >= 7.102.0 / S-721 / core 7.32.0 #1092) — core's
18640
- `PERSISTED_RULE_MANDATES` six words, transcribed verbatim and in order (core 7.32.1
18619
+ `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
18620
+ 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
18621
+ 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
18641
18622
  `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 —
18642
18623
  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
18643
18624
  `tool_approval` frame, the `livePending` row top level, the operator queue row (`GET /v1/approvals`
@@ -18691,12 +18672,12 @@ components:
18691
18672
  RuleOfferMatch:
18692
18673
  type: string
18693
18674
  description: >
18694
- The MATCH FORM of a persisted rule — the server's `PERSISTED_RULE_MATCHES`, verbatim and in order (the vocabulary
18695
- is OWNED BY CORE; the server only transcribes it and pins set equality, so core adding a member turns
18696
- the server red at compile time). ONE vocabulary for two faces: a card's rule offer (`RuleOffer.match`, the
18675
+ 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
18676
+ 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
18677
+ equality). ONE vocabulary for two faces: a card's rule offer (`RuleOffer.match`, the
18697
18678
  server's card schema validates it with this very enum) and a persisted rule row (`GET /v1/rules`).
18698
18679
 
18699
- `exact` = this one command only (`Bash(git status)`). `prefix` = the historical colon-star
18680
+ `exact` = this one command only (`Bash(git status)`). `pattern` (server >= 7.135.0 / core 7.55.0) = the MID-STAR command form (`Bash(git * main)`) — the fourth Bash command form a card edit may persist; the persisted `command` is the written pattern itself. `prefix` = the historical colon-star
18700
18681
  word-boundary prefix (`Bash(git status:*)`) and the ONLY spelling for a compound prefix form; it stays
18701
18682
  a permanent compatibility read. `wildcard` = the trailing space-star form (`Bash(npm run *)`), the
18702
18683
  same predicate as `prefix` on a single command body. `subpath` = the `Read(//abs-dir/**)` directory form,
@@ -18710,11 +18691,11 @@ components:
18710
18691
  always refuses these two words (no network grant is minted through a card). A replica rolled back to 7.125.x reads such a
18711
18692
  row as a damaged row.
18712
18693
 
18713
- 🔴 NINE MEMBERS, closed: the server validates the member on both faces. A consumer type narrower than the
18694
+ 🔴 TEN MEMBERS, closed (server >= 7.135.0 / core 7.55.0 added `pattern`, between `wildcard` and `subpath` — the engine's order): the server validates the member on both faces. A consumer type narrower than the
18714
18695
  mint point is a lie — it lets a `switch` claim exhaustiveness the wire does not honour (SDK <= 16 declared
18715
18696
  four here, so a `path` offer and every 7.118.0 skill-card offer were dropped as malformed; SDK <= 21 declared
18716
18697
  seven, so every domain grant row on `GET /v1/rules` was dropped the same way).
18717
- enum: [exact, prefix, wildcard, subpath, path, name, name_prefix, host, subdomain]
18698
+ enum: [exact, prefix, wildcard, pattern, subpath, path, name, name_prefix, host, subdomain]
18718
18699
 
18719
18700
  RuleOfferUncoveredReason:
18720
18701
  type: string
@@ -18896,8 +18877,7 @@ components:
18896
18877
  maxLength: 512
18897
18878
  description: >
18898
18879
  With `edited` absent or false this is the CANDIDATE ARM: echo back, VERBATIM, the `rule` of the
18899
- chosen `kind: "single"` entry of the frame's `ruleOffers` (or, against a <= 7.45 server, of
18900
- `ruleSuggestions`). Do not hand-build it, re-case it or append `:*` — the server locates it by
18880
+ chosen `kind: "single"` entry of the frame's `ruleOffers`. Do not hand-build it, re-case it or append `:*` — the server locates it by
18901
18881
  text and refuses otherwise (`rule_not_offered`). With `edited: true` this is the FREE-TEXT ARM
18902
18882
  and the string is whatever the person typed.
18903
18883
  edited:
@@ -19835,6 +19815,38 @@ components:
19835
19815
  fields: {}
19836
19816
  overrideReasons: {}
19837
19817
  ts: {}
19818
+ CrossBoundaryTaintRow:
19819
+ type: object
19820
+ description: >
19821
+ 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
19822
+ (`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
19823
+ 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);
19824
+ 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`)
19825
+ 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
19826
+ such key.
19827
+ # 不封闭(`additionalProperties: true`):本 schema 是 `Event_cross_boundary_taint` 的 allOf 目标(帧上多一个 `type`),封闭会把合法帧判成违约(同 PromptManifestRow)。
19828
+ additionalProperties: true
19829
+ required: [runId, phase, taint, governed]
19830
+ properties:
19831
+ runId: { type: string, description: 'the leg''s run id.' }
19832
+ phase: { type: string, enum: [prepare, resume, observed], description: 'core `permission.cross_boundary_taint` `phase` (engine closed set).' }
19833
+ taint:
19834
+ type: array
19835
+ 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).'
19836
+ items: { type: string, enum: [targetExecutionArmed, offTargetReadObserved] }
19837
+ governed: { type: boolean, description: 'the tree has run under the `multi_tenant` posture since it armed (derived from `REQUIRE_PRINCIPAL`).' }
19838
+ toolName: { type: string, description: '`observed` only: the call that let the off-target read in.' }
19839
+ toolCallId: { type: string, description: '`observed` only: that call''s id.' }
19840
+ Event_cross_boundary_taint:
19841
+ description: >
19842
+ 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
19843
+ seq). Never on the live stream, never on the `GET /v1/runs/{taskId}` poll object; an older server drops the frame on the spot.
19844
+ allOf:
19845
+ - $ref: '#/components/schemas/CrossBoundaryTaintRow'
19846
+ - type: object
19847
+ required: [type]
19848
+ properties:
19849
+ type: { const: cross_boundary_taint }
19838
19850
  Event_needs_review:
19839
19851
  type: object
19840
19852
  description: >
@@ -20019,6 +20031,12 @@ components:
20019
20031
  runId: { type: string }
20020
20032
  status: { type: string }
20021
20033
  summary: { type: string }
20034
+ completionId:
20035
+ type: string
20036
+ description: >
20037
+ server >= 7.135.0 — the idempotency anchor of this workflow completion: the SAME id rides every notification path of one completion (this frame / `task_notification` / fleet `bg_notification` / inbox replay) and
20038
+ is the dedup key ACROSS paths (the same value domain as `task_notification.completionId`). ABSENT when the engine gave none (an orphan the service terminated itself) — dedup on `runId` as before. The read faces
20039
+ (`GET /v1/workflows/{id}` etc.) deliberately never carry it. Older servers' re-delivered frames have no such key.
20022
20040
 
20023
20041
  # ── 🔴 #185a (2026-08-08): the DURABLE leg's three APPROVAL arms ────────────────────────────────
20024
20042
  # Shape rule for all three: `allOf: [<frame schema>, {the narrowed discriminant}]`. The keys are NOT
@@ -20718,7 +20736,7 @@ components:
20718
20736
  🔴 ONE RULE FOR EVERY "EFFECTIVE HALF ONLY" KEY (sdk 9.7.0, after a codex adversarial-review [high]):
20719
20737
  this schema's only consumer today is `GET /v1/diagnostics/wiring`'s `static` half, and every key marked
20720
20738
  "effective half only" (`leg`, `ask.effective`, `configFingerprint`, `tools`, `mods`, `lsp`,
20721
- `modelGate`, `autoMode`, `writeProtection`, `readDeny`, `autoConsolidation`, `fileHistory`, `instructions`) is ALWAYS ABSENT on that
20739
+ `modelGate`, `autoMode`, `writeProtection`, `readDeny`, `autoConsolidation`, `fileHistory`, `instructions`, `sessionRules`, `projectMemory`) is ALWAYS ABSENT on that
20722
20740
  wire ⇒ THEIR ABSENCE HERE CARRIES NO INFORMATION. The per-key absence readings ("absent = not armed",
20723
20741
  "absent = a third state", …) hold for the LIVE FRAME (`Event_wiring_manifest`) and must not be carried
20724
20742
  over to this response. For the facts behind those keys: a tenant reads
@@ -20731,7 +20749,7 @@ components:
20731
20749
  are EFFECTIVE-half only (the live `wiring_manifest` event) — as are the four per-leg sections
20732
20750
  `modelGate`, `autoMode`, `mcp` and `tools`, which describe what THIS leg did and have no static
20733
20751
  counterpart — and the STATIC half
20734
- (GET /v1/diagnostics/wiring) NEVER mints them; every other section, plus `governance`, is always
20752
+ (GET /v1/diagnostics/wiring) NEVER mints them; every other section NOT in the effective-half-only list above, plus `governance`, is always
20735
20753
  present on the static half. Fields stay optional because core may add or drop sections and that
20736
20754
  must not hard-break a client — read by half, and never wait on a key the half never sends.
20737
20755
  🔴 EVERY section is declared HERE, each saying which half mints it (sdk 8.9.0). Before that, `tools`
@@ -20752,6 +20770,18 @@ components:
20752
20770
  description: 'Which leg emitted it (effective half only; the static half has no leg identity).'
20753
20771
  properties:
20754
20772
  kind: { type: string, enum: [root, child, resume] }
20773
+ sessionRules:
20774
+ type: object
20775
+ required: [childrenReadLive]
20776
+ description: >
20777
+ server >= 7.135.0 (engine 7.55.0; effective half only — the static half never mints it): whether the session-rules layer is handed to the delegated legs. Same reading as on the live frame (`Event_wiring_manifest.sessionRules`):
20778
+ `childrenReadLive: true` = a rule written into the session NOW governs an already-running child's NEXT call; `false` = no rules layer; the section ABSENT = an older engine, or this half.
20779
+ properties:
20780
+ childrenReadLive: { type: boolean }
20781
+ projectMemory:
20782
+ type: string
20783
+ description: >
20784
+ server >= 7.135.0 (engine 7.55.0; effective half only): did the leg call the deployment's project-context seat — `on` / `disabled` (the request's `projectMemory: "disabled"`) / `absent` (no seat wired). Open on read; ABSENT = an older engine, or this half.
20755
20785
  ask:
20756
20786
  type: object
20757
20787
  description: 'Approval seam: wired form, which seat it came from, and (effective half) what it adjudicates to.'
@@ -21519,6 +21549,19 @@ components:
21519
21549
  usageMissing: { type: boolean, enum: [true] }
21520
21550
  stopReason: { type: string }
21521
21551
  errorMessage: { type: string, description: 'A post-stream error (contract: not thrown as HTTP).' }
21552
+ errorCode:
21553
+ type: string
21554
+ description: >-
21555
+ server >= 7.135.0 (core 7.55.0, passed through verbatim; failure only): the engine's brain error code of the failed reply (open on read; today `auth` / `invalid_request` / `rate_limit` / `server` / `network` /
21556
+ `http` / `stream_torn` / `refusal` / `length_empty`). ABSENT on success and on older servers.
21557
+ errorKind:
21558
+ type: string
21559
+ enum: [input_too_long, degenerate, length_empty]
21560
+ description: 'server >= 7.135.0 (failure only): the STRUCTURAL failure kind the reply carried, forwarded as is — `input_too_long` (the provider refused the request as over its window), `degenerate`, `length_empty`. Absent when the reply carried none.'
21561
+ inputTooLongRuledOut:
21562
+ type: boolean
21563
+ enum: [true]
21564
+ description: 'server >= 7.135.0 (failure only): present (always `true`) when the failure was a provider refusal whose minter RULED OUT input-too-long as the explanation (core `AssistantMessage.inputTooLongRuledOut`, passed through). It speaks ONLY to that negative: it does not say the status is 400 / 422 and does not say a verbatim resend repeats the failure (the engine also rules input length out for statuses such as 401 / 429 / 500) — triage on `errorCode`. Absent = no such judgment.'
21522
21565
 
21523
21566
  ToolApprovalDecision:
21524
21567
  type: string
@@ -21690,6 +21733,15 @@ components:
21690
21733
  (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
21691
21734
  (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
21692
21735
  late-answer 404's, see ToolApprovalNotPendingCause).
21736
+ 🔴 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`
21737
+ (+ `decidePath`; ONLY this state renders "go decide"); the gate settled by a person / policy ⇒ `decided` (no `decidePath`); the gate expired (a cancel expires it
21738
+ 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
21739
+ readable direction / time ⇒ 404 `tool_approval.not_pending` WITHOUT `cause`; the gate unreadable (store failure) ⇒ falls back to `parked`. Known boundary (re-read
21740
+ 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
21741
+ a cancelled resume reads `void` once the server collects the reopened gate (`parked` in between, or `void` for an instant during the reopen).
21742
+ Older servers answered all of these with `parked` + `decidePath`.
21743
+ 🔴 server >= 7.135.0: a gate that was DECIDED and then REOPENED by the engine reads `parked` WITH `reopened` (`ToolApprovalReopened`: the engine's reopen reason + the earlier decision's
21744
+ `decision` / `decidedAtMs` / `decisionNote?`). `reopened` ABSENT = the read cannot say it was reopened (not "confirmed never reopened"). Older servers: always absent.
21693
21745
  oneOf:
21694
21746
  - $ref: '#/components/schemas/ToolApprovalReadPending'
21695
21747
  - $ref: '#/components/schemas/ToolApprovalReadDecided'
@@ -21727,6 +21779,29 @@ components:
21727
21779
  properties:
21728
21780
  state: { type: string, enum: [parked] }
21729
21781
  decidePath: { type: string, description: 'The durable decision path for this ask when it can be built.' }
21782
+ reopened:
21783
+ allOf: [{ $ref: '#/components/schemas/ToolApprovalReopened' }]
21784
+ description: 'server >= 7.135.0: present when the gate was decided and then reopened by the engine (the resume leg failed before the approved action started). Absent = the read cannot say it was reopened.'
21785
+ ToolApprovalReopenReason:
21786
+ type: string
21787
+ description: >
21788
+ `ToolApprovalReopened.reason` (server >= 7.135.0) — the engine's `REOPEN_REASONS`, verbatim and in order (engine-owned; the server echoes a word only when it is in this
21789
+ set, otherwise `reopened` is omitted entirely): `env_failed` (the environment / session side failed) · `tool_unavailable` (the approved tool is not there now — decide again) ·
21790
+ `constraint_rejected` (the decision input was rejected — decide again) · `config_refused` (the configuration was refused). Read open on the consumer side: an unknown word is "reopened, reason unknown".
21791
+ enum: [env_failed, tool_unavailable, constraint_rejected, config_refused]
21792
+ ToolApprovalReopened:
21793
+ type: object
21794
+ description: >
21795
+ `ToolApprovalReadParked.reopened` (server >= 7.135.0): evidence that a DECIDED gate was reopened by the engine. `reason` = the engine's reopen reason; `decision` / `decidedAtMs` / `decisionNote?` = the
21796
+ decision made BEFORE the reopen (the same three keys as a `decided` body). An unreadable earlier decision ⇒ the three keys are absent TOGETHER (only `reason` stays; `decisionNote` may always be absent).
21797
+ Single-instant read: if the row is decided and reopened between the two reads the server re-judges (three rounds in a row ⇒ falls back to plain `parked`).
21798
+ additionalProperties: false
21799
+ required: [reason]
21800
+ properties:
21801
+ reason: { $ref: '#/components/schemas/ToolApprovalReopenReason' }
21802
+ decision: { type: string, enum: [allow, deny] }
21803
+ decidedAtMs: { type: integer }
21804
+ decisionNote: { type: string }
21730
21805
  ToolApprovalReadVoid:
21731
21806
  allOf:
21732
21807
  - $ref: '#/components/schemas/ToolApprovalReadProjection'
@@ -22083,74 +22158,6 @@ components:
22083
22158
  delivery: { type: string, const: applied, description: 'Literal "applied" on today''s server.' }
22084
22159
  marker: { type: string, description: 'Engine-minted delivery marker.' }
22085
22160
 
22086
- WorkspaceSnapshotPage:
22087
- # 4.3.0(CAPS-OPS-8):把 GET …/workspace 的 200 信封提成具名 schema。SDK 侧此前是 `list()` 返回位上的
22088
- # **内联闭合字面量**,两道 spec 门(field-drift 只扫 types.ts / resource-schema 只扫 resources 的 exported
22089
- # type)对方法返回位的内联形都是盲的 —— 于是它漏了这里一直标为 required 的 `sessionId`,CI 全绿。
22090
- type: object
22091
- description: >
22092
- GET /v1/sessions/{sessionId}/workspace 的 200 信封(server src/http/routes/sessions.ts)。
22093
- ⚠️ `total` = **本页行数**(`snapshots.length`),不是快照总数,且这条腿没有任何分页游标 —— 与同族
22094
- `WorkspaceTreePage.total`(真·全量)**同名反义**。`latest` 与展示行同源(防 ghost key)。
22095
- additionalProperties: false
22096
- required: [sessionId, snapshots, total]
22097
- properties:
22098
- sessionId: { type: string }
22099
- snapshots:
22100
- type: array
22101
- items: { $ref: '#/components/schemas/WorkspaceSnapshotRow' }
22102
- latest: { type: string, description: 'Newest snapshot key; absent when the session has none.' }
22103
- total: { type: integer, description: 'ROWS ON THIS PAGE — not the snapshot count.' }
22104
-
22105
- WorkspaceTreePage:
22106
- # 4.3.0(CAPS-OPS-8):同上,tree 腿的 200 信封。
22107
- type: object
22108
- description: >
22109
- GET /v1/sessions/{sessionId}/workspace/{key}/tree 的 200 信封(server src/http/routes/sessions.ts)。
22110
- `total` 在这条腿上是**全量**(`all.length`)—— 与 `WorkspaceSnapshotPage.total` 语义相反。
22111
- `key` 回的是解析后的真实键(请求可传 `latest` 别名)。
22112
- additionalProperties: false
22113
- required: [sessionId, key, entries, total]
22114
- properties:
22115
- sessionId: { type: string }
22116
- key: { type: string, description: 'The RESOLVED snapshot key (a `latest` alias in the request comes back resolved).' }
22117
- entries:
22118
- type: array
22119
- items: { $ref: '#/components/schemas/WorkspaceTreeEntry' }
22120
- total: { type: integer, description: 'FULL entry count (not the page size).' }
22121
- nextOffset: { type: integer, description: 'Present ⇒ more pages; send it back as ?offset=.' }
22122
-
22123
- WorkspaceSnapshotRow:
22124
- type: object
22125
- description: >
22126
- One snapshot row of GET /v1/sessions/{sessionId}/workspace (envelope `{sessionId, snapshots,
22127
- latest?, total}`; server src/http/routes/sessions.ts — newest first, uuidv7 keys). E19 snapshot store's
22128
- read-only projection: each row is the freeze at a turn boundary, NOT a live workspace.
22129
- required: [key, files]
22130
- additionalProperties: true
22131
- properties:
22132
- key: { type: string, description: 'Snapshot key (uuidv7 entry id; "latest" is an accepted alias on the sub-routes).' }
22133
- files: { type: integer, description: 'Manifest row count.' }
22134
- 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).' }
22135
-
22136
- WorkspaceTreeEntry:
22137
- type: object
22138
- description: >
22139
- One file entry of GET /v1/sessions/{sessionId}/workspace/{key}/tree (envelope `{sessionId, key,
22140
- entries, total, nextOffset?}`; server src/http/routes/sessions.ts — stable path order, offset paging).
22141
- required: [path, hash]
22142
- additionalProperties: true
22143
- properties:
22144
- path: { type: string, description: 'The relPath — pass VERBATIM as ?path= on the file sub-route.' }
22145
- hash: { type: string, description: 'Content address (sha256) — enables cross-snapshot change highlighting ([1894]②).' }
22146
- size: { type: integer, description: 'Present only when the store has a sizes face (SQL backends).' }
22147
-
22148
- # ═══════════════════════════════════════════════════════════════════════════════════════════════
22149
- # #154 车二(design/179 §7)—— 持久权限规则的 CC settings 导入面。形按 server 真码 + core 原形亲读
22150
- # (`src/http/routes/rules.ts` / `src/rules-consent.ts`;preview/result 两形是 core `ImportPreview` /
22151
- # `ImportResult` 的原样透传)。
22152
- # ═══════════════════════════════════════════════════════════════════════════════════════════════
22153
-
22154
22161
  CcImportSettingsLayer:
22155
22162
  type: string
22156
22163
  enum: [userSettings, projectSettings, localSettings]
@@ -22512,8 +22519,8 @@ components:
22512
22519
  match:
22513
22520
  allOf: [{ $ref: '#/components/schemas/RuleOfferMatch' }]
22514
22521
  description: >
22515
- The match form — the same seven-member vocabulary as a card's rule offer (`RuleOfferMatch`, the server's
22516
- `PERSISTED_RULE_MATCHES`): `exact` / `prefix` / `wildcard` / `subpath` / `path`, and from server >= 7.118.0
22522
+ The match form — the same ten-member vocabulary as a card's rule offer (`RuleOfferMatch`, the server's
22523
+ `PERSISTED_RULE_MATCHES`): `exact` / `prefix` / `wildcard` / `pattern` (server >= 7.135.0: the mid-star command form) / `subpath` / `path`, and from server >= 7.118.0
22517
22524
  `name` / `name_prefix` (a `Skill(<name>)` / `Skill(<prefix>:*)` rule).
22518
22525
  command: { type: string, description: 'What the matcher actually compares against, parsed out of `rule` at mint time and stored so no consumer re-parses. Trust this + `match`, not your own parse of `rule`.' }
22519
22526
  adds: