@sema-agent/client-core 0.96.0 → 0.97.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 (143) hide show
  1. package/CHANGELOG.md +89 -0
  2. package/README.md +28 -22
  3. package/dist/adapter/activeRunSelfHeal.d.ts +0 -2
  4. package/dist/adapter/activeRunSelfHeal.js +7 -7
  5. package/dist/adapter/downstream/terminalToSdkResult.js +6 -2
  6. package/dist/agentSession/backgroundView.js +2 -15
  7. package/dist/agentsWireCaps.d.ts +1 -1
  8. package/dist/agentsWireCaps.js +16 -10
  9. package/dist/approvalDecidedByCapability.d.ts +2 -0
  10. package/dist/approvalDecidedByCapability.js +0 -1
  11. package/dist/approvalsStreamLiveCapability.d.ts +2 -0
  12. package/dist/approvalsStreamLiveCapability.js +3 -2
  13. package/dist/attachmentSeatCapability.d.ts +2 -0
  14. package/dist/attachmentSeatCapability.js +0 -1
  15. package/dist/callerCwdCapability.d.ts +2 -0
  16. package/dist/callerCwdCapability.js +0 -1
  17. package/dist/centerWiringCapability.d.ts +2 -0
  18. package/dist/centerWiringCapability.js +4 -3
  19. package/dist/cloudConfigWireCaps.d.ts +2 -2
  20. package/dist/cloudConfigWireCaps.js +192 -56
  21. package/dist/declared/supportFloorDeclared.js +3 -3
  22. package/dist/deviceExecutorManagementCapability.d.ts +2 -0
  23. package/dist/deviceExecutorManagementCapability.js +3 -2
  24. package/dist/displayUntrusted.d.ts +3 -0
  25. package/dist/displayUntrusted.js +15 -0
  26. package/dist/engineCapReader.d.ts +8 -2
  27. package/dist/engineCapReader.js +15 -7
  28. package/dist/engineCapsCache.d.ts +11 -3
  29. package/dist/engineCapsCache.js +54 -23
  30. package/dist/engineErrorCodes.d.ts +3 -0
  31. package/dist/engineErrorCodes.js +9 -1
  32. package/dist/engineNoticeCodes.js +2 -3
  33. package/dist/executionLaneCapability.d.ts +2 -0
  34. package/dist/executionLaneCapability.js +3 -2
  35. package/dist/fileHistoryCaptureCapability.d.ts +2 -0
  36. package/dist/fileHistoryCaptureCapability.js +34 -8
  37. package/dist/fileHistoryLegNotice.js +4 -0
  38. package/dist/fleet/fleetLedger.js +129 -105
  39. package/dist/gateOutcome.js +2 -4
  40. package/dist/gateVocabulary.d.ts +1 -6
  41. package/dist/gateVocabulary.js +2 -5
  42. package/dist/generated/sdkFactTables.d.ts +2 -0
  43. package/dist/generated/sdkFactTables.js +7 -0
  44. package/dist/generated/toolNameTables.d.ts +0 -1
  45. package/dist/generated/toolNameTables.js +0 -42
  46. package/dist/hitl/approvalDecisionNoteAudit.js +2 -2
  47. package/dist/hitl/approvalEndingCause.js +2 -7
  48. package/dist/hitl/approvalHolds.d.ts +16 -1
  49. package/dist/hitl/approvalHolds.js +52 -12
  50. package/dist/hitl/askGateWire.js +1 -0
  51. package/dist/hitl/automaticAnswerVerdict.js +5 -7
  52. package/dist/hitl/decisionConflict.js +4 -3
  53. package/dist/hitl/decisionRefusal.js +19 -12
  54. package/dist/hitl/frameRouter.d.ts +3 -0
  55. package/dist/hitl/frameRouter.js +52 -25
  56. package/dist/hitl/gateLedger.d.ts +0 -5
  57. package/dist/hitl/gateLedger.js +0 -21
  58. package/dist/hitl/hitlBridge.js +18 -4
  59. package/dist/hitl/parkResolver.js +7 -3
  60. package/dist/hitl/replayedApproval.d.ts +2 -3
  61. package/dist/hitl/replayedApproval.js +7 -16
  62. package/dist/hitl/sessionPolicyDeliverable.d.ts +0 -1
  63. package/dist/hitl/sessionPolicyDeliverable.js +15 -17
  64. package/dist/hitl/sessionPolicyWire.d.ts +2 -0
  65. package/dist/hitl/sessionPolicyWire.js +15 -3
  66. package/dist/hitl/standingRows.d.ts +1 -0
  67. package/dist/hitl/standingRows.js +38 -17
  68. package/dist/hitl/suspendedReopen.js +2 -1
  69. package/dist/hitl/toolApprovalWire.d.ts +8 -8
  70. package/dist/hitl/toolApprovalWire.js +50 -24
  71. package/dist/index.d.ts +6 -4
  72. package/dist/index.js +4 -3
  73. package/dist/liveModelCatalog.d.ts +13 -0
  74. package/dist/liveModelCatalog.js +56 -0
  75. package/dist/mcpProbeCapability.d.ts +2 -0
  76. package/dist/mcpProbeCapability.js +3 -2
  77. package/dist/mcpWireCaps.d.ts +1 -0
  78. package/dist/mcpWireCaps.js +4 -1
  79. package/dist/memoryComplianceCapability.d.ts +2 -0
  80. package/dist/memoryComplianceCapability.js +3 -2
  81. package/dist/memoryOriginCapability.d.ts +2 -0
  82. package/dist/memoryOriginCapability.js +3 -2
  83. package/dist/modelIdentityResolvability.js +2 -2
  84. package/dist/noticePresentation.js +2 -2
  85. package/dist/outputStyleSeatCapability.d.ts +2 -0
  86. package/dist/outputStyleSeatCapability.js +0 -1
  87. package/dist/peerLaneCapability.d.ts +2 -0
  88. package/dist/peerLaneCapability.js +3 -2
  89. package/dist/permissionModesCapability.d.ts +2 -0
  90. package/dist/permissionModesCapability.js +0 -1
  91. package/dist/permissionRulesWriteCapability.d.ts +2 -0
  92. package/dist/permissionRulesWriteCapability.js +3 -2
  93. package/dist/permissionsAllowGrantCapability.d.ts +2 -0
  94. package/dist/permissionsAllowGrantCapability.js +4 -2
  95. package/dist/permissionsDeclaration.js +19 -21
  96. package/dist/request/taskRequest.d.ts +1 -4
  97. package/dist/request/taskRequest.js +74 -19
  98. package/dist/resumeRefusalCopy.js +4 -4
  99. package/dist/rewindArchiveCapability.js +5 -0
  100. package/dist/rewindArchiveReading.d.ts +2 -0
  101. package/dist/rewindArchiveReading.js +0 -1
  102. package/dist/runTerminal.d.ts +33 -4
  103. package/dist/runTerminal.js +170 -15
  104. package/dist/sdkWireTransit.d.ts +2 -1
  105. package/dist/sdkWireTransit.js +1 -0
  106. package/dist/sealedKeyCapability.d.ts +2 -0
  107. package/dist/sealedKeyCapability.js +0 -1
  108. package/dist/seatContract.d.ts +7 -4
  109. package/dist/seatContract.js +34 -20
  110. package/dist/sessionPolicyCapability.d.ts +2 -0
  111. package/dist/sessionPolicyCapability.js +3 -2
  112. package/dist/sessionReadRedactionCapability.d.ts +2 -0
  113. package/dist/sessionReadRedactionCapability.js +0 -1
  114. package/dist/sessionSyncFileHistoryCapability.d.ts +2 -0
  115. package/dist/sessionSyncFileHistoryCapability.js +3 -2
  116. package/dist/skillToolListsCapability.d.ts +2 -0
  117. package/dist/skillToolListsCapability.js +3 -2
  118. package/dist/sqlEngineCapability.d.ts +2 -0
  119. package/dist/sqlEngineCapability.js +4 -3
  120. package/dist/steerPriorityCapability.d.ts +2 -0
  121. package/dist/steerPriorityCapability.js +3 -2
  122. package/dist/storePostureCapability.d.ts +2 -0
  123. package/dist/storePostureCapability.js +4 -3
  124. package/dist/toolHistoryBadTurn.js +3 -2
  125. package/dist/toolResult.d.ts +11 -0
  126. package/dist/toolResult.js +53 -0
  127. package/dist/userInvokedSkillsCapability.d.ts +2 -0
  128. package/dist/userInvokedSkillsCapability.js +3 -2
  129. package/dist/webSearchBackendCapability.d.ts +2 -0
  130. package/dist/webSearchBackendCapability.js +3 -2
  131. package/dist/wireErrorTriage.js +58 -34
  132. package/dist/wireFailureShape.d.ts +1 -1
  133. package/dist/wireFailureShape.js +76 -25
  134. package/dist/workflowsJournalCapability.d.ts +2 -0
  135. package/dist/workflowsJournalCapability.js +3 -2
  136. package/dist/writeProtectionCapability.d.ts +2 -0
  137. package/dist/writeProtectionCapability.js +3 -2
  138. package/docs/INTEGRATION-CLIENTS.md +533 -14
  139. package/package.json +4 -4
  140. package/dist/engineRefusedToolName.d.ts +0 -1
  141. package/dist/engineRefusedToolName.js +0 -8
  142. package/dist/engineWordRenames.d.ts +0 -8
  143. package/dist/engineWordRenames.js +0 -13
package/README.md CHANGED
@@ -35,7 +35,7 @@ Renamed from **`@sema-agent/wire-cc-adapter`** (0.1.x, deprecated — see *Migra
35
35
 
36
36
  ## Scope
37
37
 
38
- **Version:** 0.96.0
38
+ **Version:** 0.97.0
39
39
 
40
40
  - **Today** — the adapter seam, the whole `adapt()` pipeline (all 14 A-layer arms plus the
41
41
  B/D/E tool-card layers), the notification/caps/model families, the adapter kernel (stream driver
@@ -67,12 +67,12 @@ Renamed from **`@sema-agent/wire-cc-adapter`** (0.1.x, deprecated — see *Migra
67
67
  against — the tables live upstream precisely so this package does not keep a second copy that can
68
68
  fall behind. The browser bundle really bundles the SDK through (the portability guard would
69
69
  exit 3 rather than quietly mark it external).
70
- - The declared floor is `>=20.0.0` (raised from `>=19.0.0` in 0.96.0 with the sdk 20 pick-up, together with the server and engine floors: the stream terminal reads the sdk's five-arm `done.result` union — idempotent replay body and refusal-after-open included — and fleet rows carry `workspaceSnapshot`; before that raised from `>=18.1.0` in 0.95.0 with the sdk 19 pick-up: the ask-row ending-cause table, the run-death verdict codes and the terminal-frame rule now come from the sdk's own tables; before that raised from `>=12.0.1` in 0.94.0: the request-side permission-mode vocabulary, the decision-attribution key type, the cancellation-cause list, the status-derived coarse-code list and the routing-failure denial word are now taken from the SDK itself, all of which it declares from 17.x or 18.x on; before that raised from `>=11.3.0` in 0.84.0: the per-session background listing (`sessions.background`), its capability bit `capabilities.background.listFace` and the closed background-status vocabulary are typed from 12.x on, and the package reads all three, so it no longer compiles against 11.x; before that raised from `>=11.2.1` in 0.80.0: a deny decision may now name its settler — `settledBy: "policy"` is typed on both the durable decide body and the live respond body from 11.3.0 on, and the gate record's settlement vocabulary carries its thirteenth word `policy_refused`, which this package reads to place a refusal in the policy bucket rather than the person's; the package now compiles against 11.3.0 and no consumer ships 11.2.x any more, so the older floor lost its witness; before that raised from `>=11.0.1` in 0.79.0: `capabilities.mcpProbe`, the MCP probe face (`mcpCapabilities` / `probeMcp` with `McpProbeFace`) and the write receipt's third liveness arm (`stillLive: "unknown"`) are typed from 11.2.x on, 11.2.1 adds `TaskRequest.approverPosture` and the `mandated` approval-frame key to the types and the runtime key anchor, the package now compiles against 11.2.1, and no consumer ships 11.0.x / 11.1.x any more, so the older floor lost its witness; before that raised from `>=9.8.1` in 0.78.0: `DeniedBy` carries its tenth word `read_boundary`, `rules.write` answers a `stillLive`-discriminated body, `RemovalLiveness` / `RuleWriteRequest` / `RuleWriteResult` / `RuleWriteBehavior` are exported from the SDK root and `TaskRequest.excludeAllTools` is typed from 11.x on, the package now compiles against 11.0.1, and no consumer ships 9.8.x any more, so the older floor lost its witness; before that raised from `>=9.7.1` in 0.75.0: `Capabilities.deviceExecutor.management` is typed from 9.8.x on, the package now compiles against 9.8.1, and no consumer ships 9.7.x any more, so the older floor lost its witness; before that raised from `>=9.6.0` in 0.74.0: `Capabilities.approvalsStreamLive` / `.executionLane`, `LivePendingRow.frame`, the `live_*` approval-stream events and `gates[].toolCallId` are typed from 9.7.x on, and no consumer ships 9.6.0 any more, so the older floor lost its witness; before that raised from `>=9.4.0` in 0.71.0: the `tool_disclosure` / `tool_progress` frames and `ToolApprovalFrame.readRootCandidate` are typed there; earlier: raised from `>=8.8.0` in 0.69.0: the `reasoning_end` frame and `McpStatusPanel.lastLegMcp` are typed from 9.4.0 on), and it is *witnessed*: the guard checks that an actually
70
+ - The declared floor is `>=21.1.0` (raised from `>=20.0.0` in 0.97.0 with the sdk 21 pick-up, together with the server and engine floors: the two decision doors' gate-kind tables (`APPROVAL_DECIDE_GATE_KINDS` / `PLAN_REVIEW_GATE_KINDS`, 21.1.0 on) are passed through from the sdk and the package's gate-kind judge is derived from them, the run terminal's failure detail reads the sdk's three-arm union and the approval card word comes from the sdk's closed card-word list; before that raised from `>=19.0.0` in 0.96.0 with the sdk 20 pick-up, together with the server and engine floors: the stream terminal reads the sdk's five-arm `done.result` union — idempotent replay body and refusal-after-open included — and fleet rows carry `workspaceSnapshot`; before that raised from `>=18.1.0` in 0.95.0 with the sdk 19 pick-up: the ask-row ending-cause table, the run-death verdict codes and the terminal-frame rule now come from the sdk's own tables; before that raised from `>=12.0.1` in 0.94.0: the request-side permission-mode vocabulary, the decision-attribution key type, the cancellation-cause list, the status-derived coarse-code list and the routing-failure denial word are now taken from the SDK itself, all of which it declares from 17.x or 18.x on; before that raised from `>=11.3.0` in 0.84.0: the per-session background listing (`sessions.background`), its capability bit `capabilities.background.listFace` and the closed background-status vocabulary are typed from 12.x on, and the package reads all three, so it no longer compiles against 11.x; before that raised from `>=11.2.1` in 0.80.0: a deny decision may now name its settler — `settledBy: "policy"` is typed on both the durable decide body and the live respond body from 11.3.0 on, and the gate record's settlement vocabulary carries its thirteenth word `policy_refused`, which this package reads to place a refusal in the policy bucket rather than the person's; the package now compiles against 11.3.0 and no consumer ships 11.2.x any more, so the older floor lost its witness; before that raised from `>=11.0.1` in 0.79.0: `capabilities.mcpProbe`, the MCP probe face (`mcpCapabilities` / `probeMcp` with `McpProbeFace`) and the write receipt's third liveness arm (`stillLive: "unknown"`) are typed from 11.2.x on, 11.2.1 adds `TaskRequest.approverPosture` and the `mandated` approval-frame key to the types and the runtime key anchor, the package now compiles against 11.2.1, and no consumer ships 11.0.x / 11.1.x any more, so the older floor lost its witness; before that raised from `>=9.8.1` in 0.78.0: `DeniedBy` carries its tenth word `read_boundary`, `rules.write` answers a `stillLive`-discriminated body, `RemovalLiveness` / `RuleWriteRequest` / `RuleWriteResult` / `RuleWriteBehavior` are exported from the SDK root and `TaskRequest.excludeAllTools` is typed from 11.x on, the package now compiles against 11.0.1, and no consumer ships 9.8.x any more, so the older floor lost its witness; before that raised from `>=9.7.1` in 0.75.0: `Capabilities.deviceExecutor.management` is typed from 9.8.x on, the package now compiles against 9.8.1, and no consumer ships 9.7.x any more, so the older floor lost its witness; before that raised from `>=9.6.0` in 0.74.0: `Capabilities.approvalsStreamLive` / `.executionLane`, `LivePendingRow.frame`, the `live_*` approval-stream events and `gates[].toolCallId` are typed from 9.7.x on, and no consumer ships 9.6.0 any more, so the older floor lost its witness; before that raised from `>=9.4.0` in 0.71.0: the `tool_disclosure` / `tool_progress` frames and `ToolApprovalFrame.readRootCandidate` are typed there; earlier: raised from `>=8.8.0` in 0.69.0: the `reasoning_end` frame and `McpStatusPanel.lastLegMcp` are typed from 9.4.0 on), and it is *witnessed*: the guard checks that an actually
71
71
  installed SDK at that line still exports every value-level symbol this package imports and still
72
72
  declares `TaskStats.costMicroUsd` (the key `costOrNull` reads). A floor nobody ever ran is a
73
73
  promise, not a contract.
74
- - **Support floor:** the oldest server, engine and SDK this package still supports — server **7.125.0**,
75
- engine **7.48.0**, SDK **20.0.0** (all three raised together in 0.96.0, from 7.115.0 / 7.40.1 / 19.0.0; server / engine raised from 7.106.0 / 7.35.2 in 0.95.0 and from 7.104.0 / 7.33.8 in 0.92.0; SDK raised from 18.1.0 in 0.95.0 and from 12.0.1 in 0.94.0). The values are
74
+ - **Support floor:** the oldest server, engine and SDK this package still supports — server **7.126.0**,
75
+ engine **7.49.5**, SDK **21.1.0** (all three raised together in 0.97.0, from 7.125.0 / 7.48.0 / 20.0.0; raised together in 0.96.0 from 7.115.0 / 7.40.1 / 19.0.0; server / engine raised from 7.106.0 / 7.35.2 in 0.95.0 and from 7.104.0 / 7.33.8 in 0.92.0; SDK raised from 18.1.0 in 0.95.0 and from 12.0.1 in 0.94.0). The values are
76
76
  declared in `scripts/support-floor.json` and exported at runtime as `SUPPORT_FLOOR`; servers below the floor
77
77
  are no longer witnessed, and the readings that existed only for them were retired in the same release.
78
78
  - The direction is constitutional: **sdk (wire) + agent-types (vocabulary) → this package**,
@@ -293,10 +293,10 @@ guard still cross-checks the table by name).
293
293
 
294
294
  | Suite | What it guards |
295
295
  |---|---|
296
- | `scripts/run-client-core-pure-test.mjs` | Consumer-view behaviour of every moved-in module; segment floors (B1–B8 + C) that only move up The non-throwing attachments verdict names a retired key, and gives its reason and way out, from the retirement rule itself; a negative control plants a second retired key in a copy of the build and checks that it is reported by its own name. The configuration refusal registry includes the inline-image refusal code, whose one sentence names no codes or identifiers, recognises only that code, and is checked against both the installed engine (minted in exactly one place) and the receiving server's older engine (not minted at all). The background-agent task-output reader hands the state and error lines that directly follow the status line to optional slots, positionally: lines written inside the agent's own result text never become slots; checked on twelve forms minted by the installed engine's own functions and by the receiving server's older engine. It also hands the result label's parenthetical note and the header bytes not mapped to a named slot to optional slots, with a rebuild invariant (status line, state, error, the unmapped header bytes, the result label and the output rebuild the unwrapped text, up to four named normalizations) checked on those same forms and on a seeded random pool of forged headers. A Bash command that is moved to the background mid-flight (by the user, or by the engine on a timeout or a queued message) is registered for the background panel by the same event and the same record as a model-requested background command: the reader recognises only the structured detached result (never the receipt prose, never the request's background flag), at most one registration is minted per card (a card both readers recognise yields one, taken from the structured result), the package keeps no record of what it has already published (the same task id on a second card is registered again, because only the consumer knows whether it still has the row), the receipt reader and the detached reader both stand down when the structured result says the engine failed to register, and the stream is compared byte for byte against the previous published build on inputs without a detached result (including two receipts for one task id), differing only by that one event on inputs that have one. The attachments env lane's product default now also carries the todo reminder (one default table shared by the absent, empty, unknown-word and `default` paths); `off` removes the key and an explicit list that leaves it out does not get it. The network word table matches terminated and other side closed as whole words: a local Unterminated string in JSON parse error is classified as internal instead of a connection failure, while the sentences undici itself produces, the errno codes and the UND_ERR family still classify as transport. |
296
+ | `scripts/run-client-core-pure-test.mjs` | Consumer-view behaviour of every moved-in module; segment floors (B1–B8 + C) that only move up The non-throwing attachments verdict names a retired key, and gives its reason and way out, from the retirement rule itself; a negative control plants a second retired key in a copy of the build and checks that it is reported by its own name. The configuration refusal registry includes the inline-image refusal code, whose one sentence names no codes or identifiers, recognises only that code, and is checked against both the installed engine (minted in exactly one place) and the receiving server's older engine (not minted at all). The background-agent task-output reader hands the state and error lines that directly follow the status line to optional slots, positionally: lines written inside the agent's own result text never become slots; checked on twelve forms minted by the installed engine's own functions and by the receiving server's older engine. It also hands the result label's parenthetical note and the header bytes not mapped to a named slot to optional slots, with a rebuild invariant (status line, state, error, the unmapped header bytes, the result label and the output rebuild the unwrapped text, up to four named normalizations) checked on those same forms and on a seeded random pool of forged headers. A Bash command that is moved to the background mid-flight (by the user, or by the engine on a timeout or a queued message) is registered for the background panel by the same event and the same record as a model-requested background command: the reader recognises only the structured detached result (never the receipt prose, never the request's background flag), at most one registration is minted per card (a card both readers recognise yields one, taken from the structured result), the package keeps no record of what it has already published (the same task id on a second card is registered again, because only the consumer knows whether it still has the row), the receipt reader and the detached reader both stand down when the structured result says the engine failed to register, and the stream is compared byte for byte against the previous published build on inputs without a detached result (including two receipts for one task id), differing only by that one event on inputs that have one. The attachments env lane's product default now also carries the todo reminder (one default table shared by the absent, empty, unknown-word and `default` paths); `off` removes the key and an explicit list that leaves it out does not get it. The network word table matches terminated and other side closed as whole words: a local Unterminated string in JSON parse error is classified as internal instead of a connection failure, while the sentences undici itself produces, the errno codes and the UND_ERR family still classify as transport. The error-triage readers (`classifyTurnWireError`, `isWireTransportError` and the three sibling readers of the same fields) never throw on an error object whose `status`, `errorCode`, `message`, `cause` or `name` is a throwing getter, a revoked proxy or a throwing string conversion; an unreadable `status` is its own state (neither an HTTP answer nor a transport failure), and ordinary shapes answer exactly as a frozen copy of the previous implementation does. |
297
297
  | `scripts/run-client-core-portability-test.mjs` | Kernel / A-layer / index import closures, the runtime-dependency equality gate, barrel reachability, and a real esbuild `--platform=browser` bundle |
298
298
  | `scripts/run-client-core-diff-test.mjs` | Differential equivalence against the CLI reference bridge + replay-id invariant + ledger round-trip |
299
- | `scripts/run-seat-contract-keys-test.mjs` | The seat IPC contract: verb list ↔ SPEC ↔ types, element-wise. Since 0.83.4 the seat approval request also carries the four card bits that say whether an approval must be asked and where it came from (`mandated`, `mandate`, `ruleOffersAbsence` — the parked-row form of mandated, so the seat and the card give the same `approvalIsMandated` answer — and `origin`, which the resident-posture predicate reads), and they reach the seat through `toolPermissionRequestAskBits`, which reads them exactly as the card request does — a strict own `true`, one of the closed mandate words byte for byte, a non-empty own string — so for a host that builds the seat request through that crossing an off-list word, an empty string or an inherited key never lands on the seat; the seat validator does not become a second judge that could drop a safety ask over one bit, which leaves a host that builds the request by hand to apply the same narrow read itself. Since 0.96.0 the seat chrome list also carries the two segment-end events (`text_segment_end` and `thinking_segment_end`), so the authoritative replacement of a segment (redaction or rewrite) can reach a seat lane, and its type is bound to the discriminants of `ChromeEvent` itself (a word that is not a chrome arm no longer compiles); the approval request also carries `rememberRefused` ("the session-wide allow will not be remembered on this card" — present only when the server said so, absent otherwise, read by the same narrow reader as the card's two legs and never invented as `false`) |
299
+ | `scripts/run-seat-contract-keys-test.mjs` | The seat IPC contract: verb list ↔ SPEC ↔ types, element-wise. Since 0.83.4 the seat approval request also carries the four card bits that say whether an approval must be asked and where it came from (`mandated`, `mandate`, `ruleOffersAbsence` — the parked-row form of mandated, so the seat and the card give the same `approvalIsMandated` answer — and `origin`, which the resident-posture predicate reads), and they reach the seat through `toolPermissionRequestAskBits`, which reads them exactly as the card request does — a strict own `true`, one of the closed mandate words byte for byte, a non-empty own string — so for a host that builds the seat request through that crossing an off-list word, an empty string or an inherited key never lands on the seat; the seat validator does not become a second judge that could drop a safety ask over one bit, which leaves a host that builds the request by hand to apply the same narrow read itself. Since 0.96.0 the seat chrome list also carries the two segment-end events (`text_segment_end` and `thinking_segment_end`), so the authoritative replacement of a segment (redaction or rewrite) can reach a seat lane, and its type is bound to the discriminants of `ChromeEvent` itself (a word that is not a chrome arm no longer compiles); the approval request carries the seat's allow-for-this-session bit; since 0.97.0 that bit is `sessionRow` (replacing `rememberRefused`): present only when the card judge's session-row premise holds and the request id's domain can carry a session answer, absent otherwise, which hides that row — never invented as `false`; the crossing never copies the bit from its input and never mints it for a seat request itself (the seat reads the request's own bit) |
300
300
  | `scripts/run-approval-frame-keys-test.mjs` | The tool-approval frame key mirror, element-wise against the SDK's runtime anchor (one carve-out: AHEAD_OF_ANCHOR entries — keys the server already emits but the SDK anchor has not caught up to — may lead by one generation; the gate turns red the day the SDK catches up, forcing the entry's removal — the register is occupied again — this time by the bit that says a saved allow rule cannot retire a given approval card, carrying both the release that minted it and the byte coordinates that prove it, so the lead is a dated record rather than an exemption; its predecessor left the register the other way, by being retired upstream rather than by the anchor catching up). Beside the key mirror it now guards three further faces of that bit: the closed word table the durable leg reads it through must be **the very array object the SDK exports**, not a same-looking copy — reference identity, because an equal-contents check still permits a second table that diverges the day upstream adds a member; the one predicate a client is meant to call answers over both legs — the live card's bit and the parked row's absence word, which is all the row carries, since the row has no such bit at all — and answers `false` for a malformed value exactly as its presence-only siblings do, a strictness the upstream mint shares; and the one sentence minted for it must never point the reader at writing a rule, since a rule written in answer to a mandated question can never take effect where it was written. The same bit's key also has to reach the card request itself, which the SDK's card anchor does not list — a fact that arrives at the package boundary and stops there is the shape of defect this file's guards exist to catch. Since 0.83.2 the frame also mirrors `mandate` (the word a mandated question stands on) ahead of the SDK anchor, with its own exit condition; the released server carries it from 7.102.0, so the allowance for the server fixture was removed by its own exit condition. The installed engine must declare the member with the six-word closed type, and the key must reach the card request. Since 0.91.0 the frame also mirrors `probeReason` ahead of the development SDK anchor, with the same exit condition, and the probe-tree comparison reads the newest configured SDK first. |
301
301
  | `scripts/run-segment-authority-single-source-test.mjs` | The authoritative-segment replacement verdict, single-sourced. `text_end.content` and the `text_delta` stream stopped being byte-identical the day the engine started redacting the former through the same filter as the result, so every consumer now has to decide six ways what to do with the segment it has half-emitted — and until this release that decision existed **twice**: once here for the transcript lane, once in the shell for the print lane, hot-fixed a version apart. The verdict is now one pure function both lanes call, and the guard pins it on the quantity that actually decides the outcome: whether the authoritative text still *starts with* the bytes that already left, not whether a flush has happened — the latter is a precondition, and anchoring on it withholds a perfectly ordinary answer. Each of the six forms is checked with its counter-case, the prefix length is pinned to UTF-16 code units against a non-ASCII sample whose UTF-8 byte count differs (slicing by bytes leaves the very thing being redacted on screen), and the withheld-segment ledger is compared by normalised equality rather than substring, because a short redaction marker quoted in an unrelated later answer would otherwise suppress that answer entirely. The same file pins the session-level memory-capture declaration to one mint point — the wire value is a single-member closed set, and a consumer that spells it wrong gets a loud refusal rather than a silently dropped privacy request — and pins the SDK URL/health transit to be the **same function reference**, since wrapping it would discard the one guarantee the transit exists for. A last section strips comments with the TypeScript parser and asserts the second expression has not grown back |
302
302
  | `scripts/run-print-bash-iserror-test.mjs` | The print lane's Bash `is_error` authority (structured over regex). A second section pins where the denial classification word lands on this lane: on the message envelope, never inside the tool-result block, because that block is forwarded verbatim to the provider on compaction and a self-minted key there is the shape of an old, real defect. A word outside the upstream table — or an empty string, a non-string, or nothing at all — mints no key rather than a guess, and the word never moves the error flag, because attribution does not decide anything |
@@ -310,7 +310,7 @@ guard still cross-checks the table by name).
310
310
  | `scripts/run-engine-notice-catalog-test.mjs` | The engine-notice catalog and its audience table. Whether a notice deserves a person's attention is not decided by whether this end happens to have a phrasing for it — that drifts with each client's build order — but by whether the engine minted the code into its own written catalog; the audience row answers the separate question of *who* the fact is for, since an operations fact pushed at an end user is noise and a user-facing fact buried in an operator log is something withheld from the person who could act on it. Both tables are reconciled against the installed engine's own artefacts in both directions and pinned in lockstep with each other, unknown codes fall back to the conservative operator side, and catalog membership is tested on the raw value so a code carrying control characters cannot impersonate a registered one after sanitizing. The reader for a dropped MCP injection keys on its own code alone and treats a missing session, server or reason as absence rather than throwing at a read site. A reverse pin enforces the upstream's single-mint contract: the engine composes those sentences from the host's facts, so a copy of them appearing in this package's source or build is a second source that would drift, and fails. From 0.84.0 it also covers the reader for the two read-directory grant notices: it recognises only those two codes, needs the tool call id to match a card, passes the rejection reason through as written, and treats only the granted notice as evidence that a directory was added; a granted notice without both the directory and the spelling the engine now holds, or with a scope other than `exact`, is not read at all, and the scope word is pinned to the engine's type at compile time. The server also mints a few notices of its own through the same channel; those codes live in a second table with their own audiences, kept apart from the engine mirror (which must stay equal to the engine's catalog) and reconciled against the server's published package when one is supplied, so a user-facing server notice is no longer filed under operations. One dispatcher returns the typed facts for every code that has a reader, discriminated by code and tagged with its audience — only the user-audience codes belong on a user surface — and the guard ties the dispatch table to the module's own exported readers in both directions, so a reader cannot be exported without a row and a row cannot be dropped without the guard failing. From 0.85.0 it also covers a third server-minted notice, the one saying that part of a session's saved history could not be read when the session was reopened: it is a user-audience notice, its two counts are read one by one and anything that is not a non-negative integer reads as `unknown` rather than zero (neither "nothing was skipped" nor "nothing is left" may be invented), and one extra sentence — the context is empty but the turn runs — is given only when the count of entries left is exactly zero. When the server package is supplied, the guard drives the server's own emitter for that notice and reads what it emits back through the dispatcher. From 0.86.0 the catalog grows by six engine notices — a personal rule store that could not be read and was skipped, an auto-mode classification that could not decide and where the call went, a requested permission mode that did not take effect, a permission mode answering a question on the person's behalf, a mode release past a read-deny table, and a legacy checkpoint's mode keys being migrated — each with its own typed reader: branching words (the classifier's cause and destination, the reason a mode did not take effect) are read as closed sets, descriptive words pass through as written, and a malformed notice reads as absent; two of them are checked against notices produced by the engine's own emitters. The unresolvable-ask notice's optional remedy sentence is carried verbatim when it is a non-empty string — never trimmed, rewritten or filtered by cause — and its absence (older engines, other causes, an empty or non-string value) leaves the rest of the view unchanged; a notice without it reads byte-for-byte as before, and the engine's own mint function drives three cases through the dispatcher. A fourth server-minted code, the user-facing notice that a model endpoint could not be connected to, is registered with its audience and has its own reader: the failure code, the endpoint, the remedy sentence and the session are required, the name of the proxy variable is optional, the code and remedy pass through as written, and the server's sentences are never copied into the package. With server fixtures, the server's own decorator mints the notice for a direct and a proxied connection and for every connect-failure code it knows, and stays silent when a response arrived or the call did not end in error; the audience table is reconciled against both fixture generations, a row newer than the older fixture being required to be absent there. From 0.88.0 the catalog grows by one operator notice — a provider request whose tool turn broke the pairing rules was repaired on its way out — with a typed reader: each repair's form word passes through as written (the engine does not publish that word list from its entry point and has announced more words), the call ids are read as a non-empty list, any malformed entry makes the whole notice read as absent, and notices produced by the engine's own repair law and emitter read back field for field. The model-unreachable reader reads only the notice detail's own properties, like the other server-minted readers. From 0.89.0 the engine's own catalog registers those server-minted codes as well — each code carries an audience and a record of who mints it — so the package's separate table for them is retired and both the membership check and the audience lookup answer from the engine's tables alone, the four codes keeping the audiences they had; the guard reconciles the engine's record of server-minted codes against the published server's stream allow-list on the newer server generation and against the older server's own table on the older one, and the generator refuses an engine whose minter table is not in lockstep with its catalog. Two user-facing notices gain typed readers: an incomplete tool turn (cause, count, session and run are required, the call ids are optional and copied, and a cause word the package does not know passes through as written) and user input a run may not have used — one reader for the steer and follow-up codes, the kind decided by the code, the count required, the cause passed through as written and never filled in when absent — plus one user-facing sentence that names only the count, does not claim the input went unused when the server could not tell, and falls back to a cautious sentence for any cause it does not recognise. Notices produced by the engine's and the server's own emitters read back field for field, and every reader answers the same for frozen and unfrozen input. Every reader and the by-code entry point read a notice's code and detail as own properties only: a notice whose code or detail sits on its prototype, or on a polluted global prototype, reads as nothing through all three doors alike. From 0.90.0 the catalog is eighty-five notices: the engine retired the notice for the one-off migration of old parked rows, so the catalog, audience and minter tables lose a row, its fact reader and named type are gone from the public surface, and the code is treated like any code outside the catalog (the dispatcher answers nothing, the catalog check answers false, the audience stays the conservative default). The unused-input reader also reads the child-session field carried by the copy a parent session receives when a sub-agent ends with inputs unused — taken from the engine's real minting site — and the user-facing sentence says so for the two definite shapes, with no identifier in it; every malformed form of the field drops only that field. The register now holds the allow-list notice under its 0.92.0 engine name with the user audience; the earlier engine name is not registered and falls to the operator default, which is the audience it had on the engines that minted it. Since 0.94.0 the user-input-not-used reader takes its lane and count key from the generated lane table rather than from two hand-written codes, and the catalogue expectations follow the installed engine generation. From 0.94.0 the allow-list notice has a fact reader: each entry that reads as nothing comes back with its list source, its machine value exactly as the server sent it (control characters included), a display form produced by the package's single untrusted-text exit, its reason and the optional skill, list, verdict, current-name and value-type positions; a malformed entry list makes the whole reading absent. The model-unreachable reader carries the leg's run id as an optional `taskId` when the server sends one, and drops only that key when it is unusable. Since 0.96.0 the dropped-tool-turn reader also carries the optional outcome word (absent means the engine did not say), and the catalog follows the renamed path-view notice and three new route notices. |
311
311
  | `scripts/run-tool-roster-projection-test.mjs` | The leg's tool roster — what the engine says it actually mounted and what face each tool wears — replacing three word lists that were only ever an estimate taken from one traffic capture against one pinned engine. The reader copies the engine's own all-or-nothing discipline: a roster whose row cannot be read, or whose declared count disagrees with the rows, is dropped whole rather than handed over short, because a consumer reading a short roster concludes the missing tools are not mounted — the upstream says in as many words that this is worse than sending nothing. A malformed *face* on a row (path target, render hints) drops only that face, since a face is not an identity. Shims are built strictly from roster rows and never guessed from a tool's name, and an axis that cannot be read stays absent rather than defaulting to `false` or `never`, which would render "unknown" as "safe". For run-time changes the guard pins the one hard rule in the contract: a digest that does not match is **not** a rejection — the carried roster is the new state regardless and only the summary becomes unusable, because refusing the swap would leave the consumer holding a stale roster forever. One reading here answers a question that the terminal state structurally cannot: whether this run was assembled with any file-and-shell tools at all. The engine's terminal vocabulary says a run finished, not whether the work got done, so an orchestrator that waits for the end and then guesses has nothing to guess from — while the assembly manifest already said it at the start, one row per mounted instance with the single condition that mounted it. The reading is three-state and both folds are refused: a roster that is readable and carries no such row is the engine stating a fact, while no roster at all is not that fact — the static half of a manifest never carries one, and an older engine reports rosters without naming the mount condition at all, where an empty count would be a statement about the reader rather than about the run. Those two are kept apart in the reason the reading carries, and the wording for every unknown case is checked never to claim the run had no tools. The same roster now decides the tool list on the first line of a non-interactive run: the host holds that line until the roster arrives and lists exactly what the engine mounted at the start of the run, in mount order. The guard runs a real assembly frame through the projection into the decision, and pins that the host falls back to the estimate only once the roster is known not to be coming — a manifest without one, an unreadable one, model output or the run's end arriving first — rather than on a timer alone (model activity counts, including a model call that is still waiting or retrying; an error line the stream synthesizes when a run fails before assembly counts as the run ending), that a sub-run's manifest is never mistaken for the run's own, that an empty roster is taken as the engine's answer rather than as silence, and that the wait bound covers both sequential default budgets the engine gives an external tool server to connect and list its tools. The holding logic itself lives in the package as a small per-run gate — buffer, decide once, release the held messages in arrival order, then pass through — and the guard drives real stream output through it to pin that the release happens exactly once, at the manifest, releasing exactly the held prefix. The ordering itself also lives in the package as a stream wrapper, and the guard checks the final output a consumer reads: the first line is always the tool-list line, a message that arrives while that line is still being built comes after it, a timer firing races nothing out of order, a source that ends or fails before the decision still gets its first line and held messages out before the error, and an early exit closes the source. From 0.84.0 the roster-derived sentence source no longer throws on a value it does not recognise, including a reading of the manifest's `hands` section passed by mistake: it answers the same "not stated" sentence as the `hands` reader, from one shared source, and its six known sentences do not change. From 0.86.0 that first line also says where its tool list came from, in two added keys the wrapper writes onto the host's own line object in place: a source of `engine-roster`, `estimate` or `host-static`, and, only for an estimate, which of the four reasons made the roster known not to be coming. The guard checks every decision path end to end, that the host's object keeps its identity and every other key byte for byte, that stale values of the same two keys left by the host are replaced (a stale reason is removed rather than left behind), and that a host line that cannot be written is passed through untouched instead of failing the first line. |
312
312
  | `scripts/run-permission-rule-issue-codes-test.mjs` | The rule-lint refusal codes an engine reports when it will not compile a permission rule. The SDK publishes neither a schema nor a type for them, so the package mints the table from the engine's own bytes and the guard pays the cost of that copy instead of leaving it to somebody remembering: it parses the codes the engine actually mints and reconciles them against the table in both directions, so a code added upstream (the user would see a bare code) and a code only the package believes in (a branch that can never fire) both fail. It also reconciles the table plus a small retired ledger against the engine's declared union, which is deliberately not the same set — one member was renamed and its old name is still declared — so reviving a code the engine will never mint again is impossible and a future stale member shows up immediately. Sentences are pinned one per code, mutually distinct, and split by family: a rule that is wrong and a rule that is legal but unsupported on this lane are different next steps and may not share a sentence. The engine's own message rides along as prose — sanitized and capped after escaping, never matched on The set of codes the engine actually mints now also counts declared codes minted outside the rule compiler (the allow layer reports an entry that is not a string with one). Codes an engine release retired but older engines still mint are found by comparing the receiving server's bundled engine with the installed one rather than by a hand-kept list; each keeps the exact sentence it had before, and the codes that replaced them each get their own sentence with no machine identifiers in it. Since 0.94.0 the base-path refusal sentence is checked to hold on both engine generations (base missing, or declared but not a usable absolute path), with the premise run on the installed engine. |
313
- | `scripts/run-gate-vocabulary-test.mjs` | The two gate vocabularies — who denied a call (`DeniedBy`, the installed engine's ten words) and who asked about it (`AskOrigin`, eleven) — together with the one place their sentences are minted, so the same denial does not read three different ways across three clients. The tables are copies, not opinions: the asker table is reconciled in both directions against the lists the installed SDK and engine packages publish, and the denier table is exactly the installed engine's own list (from 0.92.0 the one word only engines below the support floor still sent is no longer in it), checked so that every word the SDK declares is present except that retired word, and every word ahead of the SDK comes from the engine — a word added upstream (nobody renders it, the user sees a bare code) and a word only the package believes in (a branch that can never fire) both fail. Every word must carry its own literal sentence and no two may collide, including the sibling pairs the upstream deliberately split apart — an organization store and a personal rule store being unreadable send you to different people, and the two tighten origins exist precisely to name which layer of engine logic asked. The two fallbacks are pinned distinct because an unknown word means different things in each: for a denial layer this build does not know, the sentence says only that this build cannot say which layer refused the call or why — it no longer guesses at a newer engine or a damaged record, since the most likely unknown word is the retired one replayed from an old session; the asker vocabulary is genuinely open (the server only checks for a non-empty string, so an unknown word just means the client is older than the engine). Alongside them sits an **uplift anchor** rather than a third table: the reason a call was decided the way it was is a distinct semantic face from who denied it and who asked, one upstream has not mirrored into the SDK at all, and one whose newest member — a shell command allowed because it only reads — has no sentence anywhere yet. Minting the union here would create the second drifting source the day upstream publishes it, so the guard instead asserts the **absence** from both ends: the SDK declarations carry no such union near that word, and the installed engine’s own list does not carry the word either. The engine end fires first, on the batch that raises the dependency, which is exactly when the ownership question should be answered; the SDK end fires when the mirror lands. Either red is the work order to mint the sentence, never a reason to delete the anchor. A fourth mint now sits beside the three tables and is not a table at all: a single presence-only fact — that no saved rule and no standing posture can retire this question — earns one sentence, taking no argument precisely so a caller cannot mistake it for a second kind of mandate, pinned distinct from every sentence the tables mint, pinned never to point at rule-writing, and pinned not to overclaim the stronger neighbouring demand that a person rather than a configuration must answer; it must not say the question is asked every time — an answer for this one call may come from the person, a hook or an automatic check the deployment runs — and its wording is checked against the engine package's own description of the mandate. A fifth table joins them from 0.80.0: the words for **how a wait ended** (thirteen from 0.80.0, fourteen from 0.90.0), mirrored in both directions from the engine's own declarations — the table's owner — with the wire SDK's copy held alongside as a second witness that must match it word for word and in order, so the day the SDK falls a generation behind, that is what turns red rather than the mirror silently following the wrong source. The newest of them says a deployment's own policy answered the card — not a person, and not “nobody could be asked” — so the guard pins it apart from both neighbours by behaviour, feeding every one of the words through all five named predicates and checking which word makes which one speak, rather than what any predicate returns. Two of them also decide how a refusal is filed in the session transcript; that mapping is minted once and reused by both of the package's own entry points, and anything outside those two words yields nothing rather than a guess. Since 0.83.2 a sixth list covers the word a mandated question stands on (`APPROVAL_MANDATE_WORDS`, six words): it must equal the engine's own list word for word and in order, membership is exact, the card reader `readApprovalMandate` answers only for an own key holding one of the six words, and each word has one fixed sentence explaining why the question must be confirmed — six distinct sentences that never point the reader at writing a rule, never promise a question every time, never claim only a person may answer, and repeat no other sentence the package mints. The list is also pinned against the engine's type at compile time in both directions, while the published build references no engine package at all: every `.js` and `.d.ts` file in the build is scanned, and the same scan is first shown to fire on references planted in a scratch directory. From 0.90.0 the settlement table carries `policy_allowed` (a machine allow, listed before `policy_refused`) and the mirror is checked against the installed engine byte for byte, in order; the installed SDK (12.x) predates the word, so its list is compared as "the engine's list minus one registered lagging word", and every SDK witness at 16.0.0 or later must carry exactly the engine's set of words. The party that settled is the engine's five-word set, read open (any word, including an unknown one, comes back as written); a policy allow is no person's approval and no refusal (all five named predicates stay silent on it, and it files no denial class in the transcript), and the refusal word reads the same under both party spellings of the two server generations. The one lagging word that loosens the compile-time check against the installed SDK is itself guarded: it must stay a non-empty union of string literals (widening it to `string` compiles cleanly, so only this check catches it), the check's declared type must stay exactly as written, the word list must equal the list this check registers, and every lagging word must be absent from the SDK at the peer floor yet present in the newest witness SDK and in the installed engine's list. The origin word on a gate record and on a released-by-mode record is read as an own key only, and a structural pin holds that every source file reading an ask origin goes through the one reader. |
313
+ | `scripts/run-gate-vocabulary-test.mjs` | The two gate vocabularies — who denied a call (`DeniedBy`, the installed engine's ten words) and who asked about it (`AskOrigin`, eleven) — together with the one place their sentences are minted, so the same denial does not read three different ways across three clients. The tables are copies, not opinions: the asker table is reconciled in both directions against the lists the installed SDK and engine packages publish, and the denier table is exactly the installed engine's own list (from 0.92.0 the one word only engines below the support floor still sent is no longer in it), checked so that every word the SDK declares is present except that retired word, and every word ahead of the SDK comes from the engine — a word added upstream (nobody renders it, the user sees a bare code) and a word only the package believes in (a branch that can never fire) both fail. Every word must carry its own literal sentence and no two may collide, including the sibling pairs the upstream deliberately split apart — an organization store and a personal rule store being unreadable send you to different people, and the two tighten origins exist precisely to name which layer of engine logic asked. The two fallbacks are pinned distinct because an unknown word means different things in each: for a denial layer this build does not know, the sentence says only that this build cannot say which layer refused the call or why — it no longer guesses at a newer engine or a damaged record, since the most likely unknown word is the retired one replayed from an old session; the asker vocabulary is genuinely open (the server only checks for a non-empty string, so an unknown word just means the client is older than the engine). Alongside them sits an **uplift anchor** rather than a third table: the reason a call was decided the way it was is a distinct semantic face from who denied it and who asked, one upstream has not mirrored into the SDK at all, and one whose newest member — a shell command allowed because it only reads — has no sentence anywhere yet. Minting the union here would create the second drifting source the day upstream publishes it, so the guard instead asserts the **absence** from both ends: the SDK declarations carry no such union near that word, and the installed engine’s own list does not carry the word either. The engine end fires first, on the batch that raises the dependency, which is exactly when the ownership question should be answered; the SDK end fires when the mirror lands. Either red is the work order to mint the sentence, never a reason to delete the anchor. A fourth mint now sits beside the three tables and is not a table at all: a single presence-only fact — that no saved rule and no standing posture can retire this question — earns one sentence, taking no argument precisely so a caller cannot mistake it for a second kind of mandate, pinned distinct from every sentence the tables mint, pinned never to point at rule-writing, and pinned not to overclaim the stronger neighbouring demand that a person rather than a configuration must answer; it must not say the question is asked every time — an answer for this one call may come from the person, a hook or an automatic check the deployment runs — and its wording is checked against the engine package's own description of the mandate. A fifth table joins them from 0.80.0: the words for **how a wait ended** (thirteen from 0.80.0, fourteen from 0.90.0), mirrored in both directions from the engine's own declarations — the table's owner — with the wire SDK's copy held alongside as a second witness that must match it word for word and in order, so the day the SDK falls a generation behind, that is what turns red rather than the mirror silently following the wrong source. The newest of them says a deployment's own policy answered the card — not a person, and not “nobody could be asked” — so the guard pins it apart from both neighbours by behaviour, feeding every one of the words through all five named predicates and checking which word makes which one speak, rather than what any predicate returns. Two of them also decide how a refusal is filed in the session transcript; that mapping is minted once and reused by both of the package's own entry points, and anything outside those two words yields nothing rather than a guess. Since 0.83.2 a sixth list covers the word a mandated question stands on (`APPROVAL_MANDATE_WORDS`, six words): it must equal the engine's own list word for word and in order, membership is exact, the card reader `readApprovalMandate` answers only for an own key holding one of the six words, and each word has one fixed sentence explaining why the question must be confirmed — six distinct sentences that never point the reader at writing a rule, never promise a question every time, never claim only a person may answer, and repeat no other sentence the package mints. The list is also pinned against the engine's type at compile time in both directions, while the published build references no engine package at all: every `.js` and `.d.ts` file in the build is scanned, and the same scan is first shown to fire on references planted in a scratch directory. From 0.90.0 the settlement table carries `policy_allowed` (a machine allow, listed before `policy_refused`) and the mirror is checked against the installed engine byte for byte, in order; the installed SDK (12.x) predates the word, so its list is compared as "the engine's list minus one registered lagging word", and every SDK witness at 16.0.0 or later must carry exactly the engine's set of words. The party that settled is the engine's five-word set, read open (any word, including an unknown one, comes back as written); a policy allow is no person's approval and no refusal (all five named predicates stay silent on it, and it files no denial class in the transcript), and the refusal word reads the same under both party spellings of the two server generations. The one lagging word that loosens the compile-time check against the installed SDK is itself guarded: it must stay a non-empty union of string literals (widening it to `string` compiles cleanly, so only this check catches it), the check's declared type must stay exactly as written, the word list must equal the list this check registers, and every lagging word must be absent from the SDK at the peer floor yet present in the newest witness SDK and in the installed engine's list. The origin word on a gate record and on a released-by-mode record is read as an own key only, and a structural pin holds that every source file reading an ask origin goes through the one reader. A retired mandate word that an older server still sends reads as the sentence of the word that replaced it (one sentence per fact), the sentence table holds the current words only (a leftover sentence for a retired word fails to compile), and the modules that read the rename table are pinned to a named roster. |
314
314
  | `scripts/run-engine-identity-test.mjs` | The engine generation anchors on `/health` (`pid`, `instanceId`, `startedAt`; engine >=7.67.0). `/health` is the one unauthenticated door and its heartbeat is always green, so "another host restarted the shared engine" used to be discoverable only by having some authenticated request hit a 401 first — a path that misreads a restart as a network fault. The reader narrows each anchor independently (one malformed field never hides the other two) and always hands back a reading object rather than an absence, because the caller is asking which anchors answered, not whether there was a response. The comparison is a three-word verdict, not a boolean: `unknown` when the two readings share no comparable anchor at all — an empty intersection means nothing could be compared, never that nothing changed — and the boolean convenience is pinned so that only `true` is an assertion. Any comparable anchor differing decides `changed`, so a reading whose `startedAt` matches while its `instanceId` does not cannot be waved through as the same life; precedence only decides which anchor gets named in the diagnosis |
315
315
  | `scripts/run-posture-knob-projection-test.mjs` | The three deployment knobs on the operator face (`serverGates.durableApproval` / `streamAskWindowMs` / `sessionAutoTitle`, engine >=7.67.0), each read as a value **plus who set it plus one operator-facing pointer** rather than a bare value — a bare boolean cannot answer why this particular machine is on this setting or how to pin it back, and a default that flips with the deployment shape is invisible without that. A worker too old to report readings still sends a bare boolean; the reader folds it into the same shell so consumers keep one branch, but raises a `legacy` bit, answers `undefined` from the machine-readable source accessor, and mints a sentence that contains no source word at all — claiming a source nobody reported is worse than admitting the worker cannot say. The other two knobs are honestly absent on such a worker rather than defaulted, a malformed side knob drops only itself while the anchor knob drops the whole reading, and the four sentences are pinned literally distinct so an operator can tell "not observed" from "not reported" from a real value. The last leg reads the installed SDK's `openapi.yaml` and `types.d.ts` directly, including a pin that exactly one knob on this face is numeric — the premise the millisecond-to-prose rendering rests on |
316
316
  | `scripts/run-terminal-facts-projection-test.mjs` | The four unconsumed terminal-receipt facts: `TaskResult.effectiveReasoning` / `effectiveMemoryScopes` are narrowed into `_sema_effective_reasoning` / `_sema_effective_memory_scopes` on the CC-shaped `result` (success and error envelopes alike; a malformed value mints nothing, never a default tier), the resume **reopen** family (`resume.env_failed` / `tool_unavailable` / `tool_contract_mismatch`) is a frozen closed set with a reader and three-sentence copy that is disjoint from the refusal and retry-later sets, and `routePairingVerdict` reads `ModelInfo.routePairing` as ok / broken / unknown without policing the open set. A fifth section pins the structured-output key on the success result: the CC-spelled `structured_output` is the only home for the value the wire calls `structuredOutput`. The camelCase spelling this package used to mint on its own — a misspelling of the CC field, not an additive field of our own — rode alongside it for exactly one release (0.79.1) and is **absent from 0.80.0 on**, pinned both by own-key and by `in`, so a consumer still reading the old name sees `undefined` rather than a stale copy. The wire position is read exactly once, so a value-changing accessor is only ever asked for its first answer; absence stays absence; a wire key that is present but `undefined` mints nothing, since a key whose value is `undefined` makes a consumer that tests presence read "the engine produced nothing" as "the engine produced an empty result"; falsy-but-present values such as `null`, `0`, `""` and `false` are still minted, and so are shapes that are not records at all — an empty array, a populated array, a string, a number, a boolean — each carried through by the same reference, because the shape of that value is decided by the caller's own schema and the package does not get to filter it; and the error envelope carries no such key, because the CC error arm has no such field. Which spelling CC itself declares is witnessed from the mirror's own syntax tree rather than a constant copied into the guard, so the day that field is renamed upstream the guard says so. A seventh section covers the two keys the memory-scope reading used to leave behind: the mounted arm's `projectKey` (exactly one of `{ scope }` or `{ absent }`; a missing key means the deployment declared nothing, which is not the same as having looked and found none, and the reason word is read as an open set so a word added upstream is never dropped) and each row's `shadowed` (a positive integer only, never on the write home's own row, never a zero or a coerced value). A malformed or unreadable value mints that one key and nothing else, so a throwing accessor never takes the arm down; the other arms never carry either key; and inputs without the two keys read byte for byte as the previous release does, checked against that release's published artifact on a large generated pool and on a pool of live objects (throwing accessors, revoked proxies) where even throwing versus not throwing must match. The upstream facts are pinned on every generation of server fixture the guard is given: the mint sites and declarations in the engine each one bundles, the server's terminal shaping run for real so both keys cross the wire unchanged, and the server's own project-identity minting fed into the reader. The SDK types are pinned as not modelling the face at all, and the two keys that were declined are pinned as having no mint site on the wire, so the day either changes the guard goes red instead of the decision going stale. The key set compared across the success and error envelopes is read from the type declaration of the shared fact parts by the compiler's checker rather than from a hand-kept list or a name prefix: every declared key must have an input fixture and every fixture a declared key, and a planted extra key or a removed key is shown to change the derived set. |
@@ -350,7 +350,7 @@ guard still cross-checks the table by name).
350
350
  | `scripts/run-engine-vocab-floor-test.mjs` | Engine-mirrored vocabularies (structured card whitelist, self-reported tool face, control verbs, recogniser sets) against the *installed* `@sema-agent/core` |
351
351
  | `scripts/run-limits-env-failloud-test.mjs` | `SEMA_HEADLESS_*` env-lane limits reject invalid values as loudly as the flag lane (no silent "no budget" runs) The sentence about whether an attached engine honors an explicit limit flag (`limitsSupportNotice`) and the helper naming the explicitly given limit flags (`explicitLimitFlagNames`) are pinned here: no sentence when the capability probe was not observed or the version is at or above the support floor, a "may ignore" sentence below it, a "cannot be confirmed" sentence when no readable version was reported, and only the three limit flag names are ever echoed. |
352
352
  | `scripts/run-streamjson-timing-honesty-test.mjs` | Stream timing & terminal honesty ([2084]): held errored fs-write results release on model progress; a wall-clock stop maps to `error_during_execution` with a truthful salvage note; the synthetic API-error assistant row carries the `<synthetic>` in-message sentinel. Also ([2489], core 5.8.0): the run-limit `errorCode` -> CC subtype map is pinned code by code (`limits.max_{cost,turns,tokens,walltime}_exceeded`), token/wall-clock stops keep the text the engine already produced, and the `failed` event arm shares the one mapping point. The 5.7 dual-vocabulary legs retired with server 6.0.0 (which bundles core 5.8.0); four **retirement negative controls** stand in their place — the retired `status:'timeout'` and the retired codes must fall to the honest fallback subtype and must never drop back to an empty success, so putting any of them back turns the gate red |
353
- | `scripts/run-background-view-test.mjs` | `createBackgroundView` lifecycle: polling/notify pairing, per-source degrade (`501 → not-configured` vs `unavailable`), the capabilities `scheduler` probe, and dispose really aborting the in-flight fleet snapshot (pure projection lives in the pure suite's W-A segment) |
353
+ | `scripts/run-background-view-test.mjs` | `createBackgroundView` lifecycle: polling/notify pairing, per-source degrade (`501 → not-configured` vs `unavailable`), no capabilities probe of its own (whether the assistant face is configured is read only from that route's own `501`, the same arm as the fleet source; the view makes zero `/v1/capabilities` requests), and dispose really aborting the in-flight fleet snapshot (pure projection lives in the pure suite's W-A segment) |
354
354
  | `scripts/run-fleet-view-keys-test.mjs` | The fleet projection views, **both directions**: `FleetTaskView`/`FleetWorkflowView` ⇄ their key lists (compile-pinned) ⇄ what a maximal/minimal row really projects, plus a wire-key coverage ledger (every `FleetTaskRow` key is either projected or carries a written reason why not) and a drift ledger against the shell's render contract. A one-directional assignability check is blind to optional keys — which is how `startedAt` was silently dropped Two keys that no server ever sends have left the view since 0.95.0 — the queue-depth count on a task row (the SDK still declares it, as a phantom key) and the run-level error code on a workflow row (the fleet workflow row never carries it): whatever the input holds, the view has no such key, and the first is recorded in the not-projected ledger with its reason. Where the shell's render contract still declares them, the drift ledger lists exactly those two as a transitional entry that must equal the measured set, so the day the shell drops its declaration the entry is a zombie and the gate says so. |
355
355
  | `scripts/run-usage-verbatim-channel-test.mjs` | The two complementary usage disciplines (core 3.0.0 metering semantics): the CC `ModelUsage` mirror stays pure (five pinned keys, `totalInputTokens` has no seat), while the sema-owned channel forwards the engine `turn_end.usage` object **verbatim** (six keys, incl. `totalInputTokens`) via `last_turn_usage.engineUsage` / `handle.latestEngineUsage` — honest absence on pre-3.0.0 engines, no fabricated zeros |
356
356
  | `scripts/run-plan-review-decide-verify-test.mjs` | `decidePlanReview`'s post-decide honesty ([2315]/[2316], engine RB-471 family): a 2xx from the decide endpoint is **not** a terminal — the wire re-pulls the task status and words the outcome by the real shape (still-locked / legal new gate / genuinely left park / unverified), never claiming success it hasn't earned; when the engine answers that the session's stored resume context cannot be read, the outcome names the unreadable row and says the decision was not applied. Driven against a real fake-engine HTTP server through the shipped dist. The outcome queue item also carries a machine-readable `_sema_planReviewOutcome` (task id, a package-minted dispatch number, decision, effect) so a host can tell which in-flight decision an outcome belongs to without searching the prose; the prose is byte-identical, the number is minted only for a decision the in-flight latch admits, and a caller that passes no metadata gets no key. A plan-review card answered with free text and no option picked (or on the reject option with a note) is read as a rejection carrying that text as its reason, on both the first card and a reopened card; the reason is narrowed by the same step as an approval-denial reason (trimmed, blank dropped, capped at 4096 characters) and a note on the card wins over any reason the caller supplied. An approval with a note stays an approval and sends no note; an empty answer, a contradictory multi-select, several questions or an option outside the shown card stay dismissed; an answer without a note produces a request body byte-identical to before. |
@@ -402,7 +402,7 @@ guard still cross-checks the table by name).
402
402
  | `scripts/run-assistant-arm-identity-test.mjs` | The identity keys on an assistant row, and an explicit account of the two that are **deliberately not** there. What the renderer received was a bare role-and-content object, so a dozen consumer sites downstream were each estimating what the message envelope should have told them. The id is taken from the engine's own event id rather than minted locally, because it has to be **the same value** on the live leg and on a durable replay — a freshly minted one would make a replayed message look new to a host's dedup and to rewind — and when the wire carries none the key is simply absent rather than filled with a random stand-in wearing an identity it does not have; it is also kept distinct from the envelope's own local render key, which is a different identity. The model name comes from what the host pinned when it opened the stream (the request was the host's to build) and is never guessed, since a wrong model name is worse than none once a billing or capability face looks it up. Usage and stop reason are **not** minted on this arm, and the reason is frame order rather than effort: content arms arrive before the turn's closing frame, so at the moment the arm is emitted the engine has not yet said what the round cost — anything put there would be an estimate, which is the very thing this work exists to remove — and synthesising a follow-up assistant update when the real figure lands is also refused, because that shape does not exist upstream and would place a message in the transcript the engine never sent. Their real values leave through the turn's own neutral arm as two superset keys, the usage one reusing the **same single mint point** the footer rollup already folds so the two faces cannot diverge, and the stop reason passed through verbatim as an open set — the machine signal for *was this turn cut short*, previously blind on both the stream and the trace. The existing behaviours beside them are pinned too: no arm at all when usage is wholly absent, and the sub-flow cut-out that keeps a child's turn from driving the leader's face |
403
403
  | `scripts/run-text-segment-authority-test.mjs` | The **authoritative segment replacement** on `text_end` (server >=7.75.3). `text_end.content` now goes through the same redactor as `result` and the ledger while `text_delta` stays verbatim, so the two **may differ** — an answer that quoted a credential used to be committed to the local transcript in its unredacted form, because the arm only forwarded the boundary signal. Six timing shapes are pinned, two of which an adversarial review reproduced against the installed engine's real bytes and which the first design got wrong in both directions: a second boundary in the same turn (the per-block case on one provider lane) used to make the first segment's prose vanish, and a boundary that arrives *after* the tool card (the other lane emits it at finalize) used to be read as "this package never handled that segment" and reported nothing at all. Three additive keys, all never-false; the two shapes that look alike are told apart by the second one, because the host's action in them is the opposite. The end-to-end legs drive the real pipeline without hand-inserting a segment commit — doing so is exactly what hid the first defect. A second review round then found two combination timings on top of the first fix — a tool card followed by *more* deltas in the same segment, and a byte count that had been documented as a message count — and both are pinned here too. A third round caught a length that the prose called bytes while the code returned UTF-16 units — harmless in ASCII, and on CJK text enough to leave the credential on screen — plus a backfill ledger that had to be kept in step, so the terminal frame does not re-render the segment a second time — kept in step only where the whole stretch sits in one message, because those ledgers are per-message and a fourth round showed that writing across them charges one message's prose to another. A fifth round settled the whole class into one invariant the guard now checks against the previous release's behaviour: this package only rewrites bytes it is still holding in the current message — once a segment has crossed a package-side boundary it emits the three keys and changes nothing else **0.68.2 (CC-01):** the segment identity is now minted here, not by the host: every committed assistant text row carries a top-level `_sema_segment_id` (stamped once at the `adapt()` exit, so the durable whole-message leg and the streamed-segment leg are covered alike; thinking blocks, tool_use-tailed rows and chrome events are left byte-for-byte), `text_segment_end` carries the same value as `segmentId` before rotating, subagent boundaries never rotate, and a replayed stream yields the same identities. Three mutations (no rotation / no stamping / stamping tool_use rows) each turn the guard red **0.69.0 (CC-02):** the same authority replacement now covers the reasoning face (`reasoning_end`, server >=7.77.0): a thinking block still buffered is swapped whole and its live tail recomputed; one already committed at a boundary (the usual timing, since the first text delta commits it) is left untouched and the host is told the row to replace by its uuid, never re-emitted. Subagent boundaries are ignored and the text-segment identity does not rotate **0.69.1 (CC-09):** the run-stream replay guard still drops a frame whose event id was already seen, but it now reports the drop through the host's dropped-frame sink as `duplicate_seq` instead of vanishing silently (server 7.77.0 reuses the first reasoning delta's id for `reasoning_end`, so that authoritative segment is lost on the print lane until 7.78.1); the interactive adapter has no such guard and keeps receiving it **0.69.1 (CC-10/CC-11):** subagent segment-end frames are fenced on all three identity keys (a frame carrying only `sourceTaskId` no longer masquerades as the leader's), and a reasoning segment that spans tool cards now hands the host every committed row it covers (`committedUuids`) so nothing unredacted is left behind. **0.96.0 (CC-645):** the tail of a segment that the authoritative text had not yet streamed is now emitted *before* the segment-end event, so nothing for the same segment follows it; a host that replays the sequence (append deltas, replace with `content` when `diverged`) shows the segment exactly once, on both the text and the reasoning channel |
404
404
  | `scripts/run-gate-negative-controls-test.mjs` | Whether the registry-shaped guards among the suites above actually turn red when the material they check really breaks — a census found the ones clean enough to rehearse safely (closed sets, mirrors, baselines, floors, a type-shape ratchet) without touching any judgement code. Each is exercised by tampering a disk copy of the real material, spawning the guard's own unmodified script, asserting it exits non-zero and names the disease, then restoring the file byte-for-byte. The guards of the same shape that are too costly to rehearse, and the behaviour/projection suites that red on their own assertions, are catalogued rather than rehearsed — see `docs/GATE-NEGATIVE-CONTROLS.md` for the three tables (all generated from `gates-manifest.json`, which carries each suite's classification), the reasons, and a one-minute manual replay recipe for each blind one. The suite reconciles its case list against that classification by name in both directions, so a case quietly dropped from the array without the manifest following is itself an undeclared blind guard. The backup that makes the restore possible is taken by **exclusive create**: checking for it and then copying are otherwise two steps, and two instances can pass the check together — the later one overwrites the only clean copy with material the earlier one has already tampered, and the rehearsal that promises to leave no trace leaves a permanently corrupted file instead. That interleaving is rehearsed too, in a throwaway directory of its own. A run that is interrupted — by the suite runner's time limit, by Ctrl-C or by its terminal closing — restores what it was in the middle of tampering with before it exits: the rehearsed guard's whole process group is stopped first, so nothing that guard started can write the tampered state back afterwards; the file is restored byte-for-byte from its backup and the backup removed only once the bytes match; the build output is regenerated from the restored sources within the grace period the runner allows (a rebuild that would overrun it is cut short, its compiler stopped, and the run says the build output was not regenerated); and the run exits with 128 plus the signal number rather than passing. Before the first rehearsal the suite also looks for backups left behind by an earlier run that could not clean up — in the trees its targets live in, following a symlinked dependency directory — and if it finds one it refuses to start, names the file and how to check and restore it, and leaves the backup untouched rather than overwriting what may be the only clean copy. |
405
- | `scripts/run-engine-cap-reader-factory-test.mjs` | The one shared implementation behind every capability reader's ports, exercised as a table. Since 0.95.0 a reading is a lazily computed, frozen projection of the capabilities cache's landed response for that base URL, memoised per landing; the probe tee port is a retired no-op and the per-reader `forget` port is gone — invalidation is `invalidateEngineCaps` alone. Every reader in the table runs the *same* criteria (the table length is the source of truth, and a roster check fails the gate if any source file calls the factory without having a row): the four states (nothing landed, present, key absent so not reported, malformed so unobserved); a throwing projection or descriptor read treated exactly like an unreadable one and never escaping; a projection that changes the generation mid-flight answering unobserved without being remembered; a getter at the top level or at any nested position never being executed and reading as malformed; a freshly minted "unobserved" reading on every miss (two misses are never the same object, so a consumer that mutates one cannot taint another base URL); one independent memo per reader that never takes another down; invalidating one base URL leaving every other base URL's reading untouched; `invalidateEngineCaps` and the retired tee port being no-ops on an empty or non-string base URL; the retired per-reader `forget*Reading` exports being absent from the root entry; the read anchor being resolved dynamically; and the deliberate split in how presence is judged per reader (own data property only, so a key that exists only on the prototype is not reported). A stored reading is frozen at every level, whether it reports a value or reports that the position was not reported, so a consumer that writes to the object a reader hands out changes nothing for later readers (a strict-mode write throws); the capabilities body passed in is never frozen. A reading follows landings and invalidations rather than any per-reader state: after `invalidateEngineCaps` every reader answers "unobserved" until the next landing, checked against a late response from the previous engine (dropped by the cache), a replacement probe that succeeds, throws, returns a bad shape, hangs or never comes, two readers sharing one landing, two base URLs landing in either order, two in-flight probes landing in either order, and a projection that invalidates midway — and a test reset of the cache never revives an older reading. A host that never invalidates is checked on a seeded random sequence (each read is the projection over the latest landing, one object per landing, getters never executed), and the downstream decisions that depend on these readings are pinned at each moment around a switch. |
405
+ | `scripts/run-engine-cap-reader-factory-test.mjs` | The one shared implementation behind every capability reader's ports, exercised as a table. Since 0.95.0 a reading is a lazily computed, frozen projection of the capabilities cache's landed response for that base URL, memoised per landing; the probe tee port is a retired no-op and the per-reader `forget` port is gone — invalidation is `invalidateEngineCaps` alone. Every reader in the table runs the *same* criteria (the table length is the source of truth, and a roster check fails the gate if any source file calls the factory without having a row): the four states (nothing landed, present, key absent so not reported, malformed or unreadable once a response has landed, so a distinct unreadable state rather than unobserved); a throwing projection or descriptor read treated exactly like an unreadable one and never escaping; a projection that changes the generation mid-flight answering unobserved without being remembered; a getter at the top level or at any nested position never being executed and reading as malformed; a freshly minted "unobserved" reading on every miss (two misses are never the same object, so a consumer that mutates one cannot taint another base URL); one independent memo per reader that never takes another down; invalidating one base URL leaving every other base URL's reading untouched; `invalidateEngineCaps` and the retired tee port being no-ops on an empty or non-string base URL; the retired per-reader `forget*Reading` exports being absent from the root entry; the read anchor being resolved dynamically; and the deliberate split in how presence is judged per reader (own data property only, so a key that exists only on the prototype is not reported). A stored reading is frozen at every level, whether it reports a value or reports that the position was not reported, so a consumer that writes to the object a reader hands out changes nothing for later readers (a strict-mode write throws); the capabilities body passed in is never frozen. A reading follows landings and invalidations rather than any per-reader state: after `invalidateEngineCaps` every reader answers "unobserved" until the next landing, checked against a late response from the previous engine (dropped by the cache), a replacement probe that succeeds, throws, returns a bad shape, hangs or never comes, two readers sharing one landing, two base URLs landing in either order, two in-flight probes landing in either order, and a projection that invalidates midway — and a test reset of the cache never revives an older reading. A host that never invalidates is checked on a seeded random sequence (each read is the projection over the latest landing, one object per landing, getters never executed), and the downstream decisions that depend on these readings are pinned at each moment around a switch. |
406
406
  | `scripts/run-mcp-liveness-test.mjs` | The engine's **liveness observation** about each MCP server it hosts (`wiring_manifest.mcp[].liveness`, engine-side from core 7.24.3 / server 7.91.2): one reader, one word list, one leg-level verdict. The cell answers *can this server still be reached* — it is not the connect-time verdict beside it, which the engine deliberately freezes (a server that died mid-run still reads `connected`), and it is not a re-dial's judgement either, so `status: "failed"` next to `liveness.state: "reachable"` is a **real row**: the server answered the handshake and answered with a protocol error — up, and misconfigured. The three words are read as a closed set (an exchange completed / it was lost in transport or the clock / what came back does not answer the question), and a word outside it is malformed rather than rendered, because a word nobody upstream has defined is not a sentence worth putting on screen. **Absence is the fourth reading and is not one of the words**: it means *no liveness record is available*, which on the wire covers a server this leg never reached, a declaration that could not be dialled, an older engine, and a record the projection ahead of us dropped — all indistinguishable, so it is never read as "we looked and could not tell" (a strictly stronger claim), never as healthy and never as off. The failure-class footnote rides the unreachable word only, and one that turns up anywhere else, or that is malformed, loses **just the footnote** while the word and its timestamp stay: the honest reading is then "cannot be reached, reason not given", not "this record is broken". Malformed never becomes healthy: a cell that is present but unreadable marks its row and pulls the leg-level verdict back to *cannot tell*, since letting it sit beside a reachable row would report a leg as reachable on the strength of a record that may well have said the opposite. The verdict takes the worst fact first rather than a majority or the newest reading, carries no server count — so there is no fabricated zero to be read as "no problems" — and its timestamp belongs to **that leg's** observation, not to now: the engine runs no probe and adds no traffic of its own, so this is a per-leg snapshot rather than a heartbeat. The replayed roster on the session panel and the live leg go through the same reader, and the panel's own deployment-side rows carry no liveness position today, so the verdict does not borrow a word from that face. The word list is bitten in both directions where an installed witness exists and the absence of one is itself asserted against the installed engine's version, so the day it ships the comparison starts on its own; since sdk 11.2.0 the wire types declare the cell, and the word list and the presence test are pinned to those types at compile time in both directions, so the two can never drift apart |
407
407
  | `scripts/run-peer-lane-rules-write-capability-test.mjs` | Two more engine self-descriptions read the same four-state way as their seven sibling capability readers (`capabilities.peerLane`, `capabilities.permissionRulesWrite`): an absent key is not reported (an older engine that predates the position, never folded into `false`), `true` is present, `false` is a positive absent (the cross-session lane not being mounted on this deployment, or this particular call not being able to reach the tightening-direction write entry), and anything else is unreadable and drops the cell. Each carries its own single-source verdict (`peerLaneAvailable` returns `yes`/`no`/`unknown`; `permissionRulesWriteAvailable` collapses to a plain boolean, present being the only `true`). The write-entry position pairs with a boolean convenience port in the persisted-rules module, and this guard pins that port to derive from nothing but this one reader's own reading — never a conjunction with the lane-reachable position, and never a second read of the deployment-level existence signal the revoke surface uses (the two are documented as reading differently on purpose): a deployment where the lane answers true but the write entry's key is simply absent (an older binary) must still come back `false`, a deployment where the write entry answers true while the lane key is entirely unseen must still come back `true` (proving no silent conjunction crept in), seeding only the general capabilities cache — never this reader's own feed — must still come back `false` (proving the convenience port cannot be satisfied by the wrong table), and passing an explicit `undefined` base URL must still come back `false` even while a different, already-installed engine target answers `true` for the same position (an adversarial pass found the naive forward of that parameter falls through to the reader's own convenience default, silently answering for whichever engine happens to be installed rather than the caller's absent target — the fix routes an explicit absence through the same empty-string path the reader treats as unobserved). |
408
408
  | `scripts/run-lane-proof-identity-test.mjs` | The **instance identity of a lane proof**: the main-lane proof is minted fresh on every emission. Previously a single module-level constant object was handed both to `laneOf(an unregistered task id)` and to some fifty main-lane emission points, so two unrelated consumers — across adapter instances, across streams, across turns — held the same object: writing a card id onto one of them was readable on the other, and the four opening main-lane events changed together. Nothing in this package writes to a lane proof and the known consumers only read it, so this is an **aliasing hazard on a published output surface** rather than an observed corruption — a consumer that uses the proof as an identity key, for dedup, or as a view-layer identity would conflate two unrelated rows without writing a single byte, which is precisely the half that freezing the object would not solve. The gate therefore anchors on instance identity: two independent adapter instances, two rows inside one instance, the same id read twice, and two arms in one beat are each distinct references; mutating one leaves the others byte-identical; and the subagent lane, which already minted fresh, is the control that proves the criterion discriminates. The main-lane **value** is unchanged — an unregistered id still answers `{lane:"main"}` with exactly one own key and still emits its events, so absence is not turned into a second kind of absence — with ordering pinned three ways (registered-then-read, read-then-registered with no retroactive edit of an already delivered proof, the same id twice) and the id failure classes pinned four ways (unregistered, empty string, absent, non-string, the last two emitting no panel event at all rather than an ownerless proof). Where one row emits **two** events — the terminal-tick and card-close legs, which each yield a lifecycle stop and a panel end — the attribution is decided once (a consumer binding a card between the two yields must not split one row across two lanes) while each event still gets its own proof, so a host consuming them one at a time cannot poison the second before it is even yielded. The run stream leg is covered as the same shape, and a syntax-tree check forbids reintroducing a module-level lane-proof object literal or a module-level `LaneProof`-annotated binding (judged on the type node, not on text, so a compile-time pin tuple that merely mentions the type is not miscaught), backed by a type-checker pass that also catches an un-annotated module-level cache such as `const x = mainLane()` while letting the callable factory itself through, while the module-private three-state sentinels of the untrusted read are frozen instead — only `Object.freeze` counts, never `Object.seal`, which still permits writes to existing keys — their exposure being confined to one module |
@@ -424,7 +424,7 @@ guard still cross-checks the table by name).
424
424
  | `scripts/run-registry-quota-usage-test.mjs` | The projection of the cloud control plane's quota reading, and the three different things a missing number can mean there. This response says `null` in two places and means something different each time: no token quota is configured for this principal on this instance, and this window has no cap at all. Both are **facts the server is asserting**, not gaps in the reading — while a key that is absent or carries the wrong type is a genuine gap. All three have to survive to the screen separately, because folding them is how a user ends up staring at a confident `0`: an uncapped window rendered as if nothing were left, or a deployment that simply never configured quotas rendered as if the quota were exhausted. The complaint that started this was the opposite direction — a centrally configured quota that the command line could not see at all — so the reading also refuses to let an unreadable response masquerade as "no quota configured". Two fields deliberately do not share one signal: whether a window is exhausted and whether there is a recovery time, since the recovery time is only ever populated in the exhausted case and reading its absence as "not exhausted" would answer a question the response never answered. Counts that the server always provides are narrowed no further than the mint: a used counter has no uncapped state, so a null there is unreadable rather than zero. The wording helper carries the only human-facing phrasing, and the sentences for "no cap" and "unknown" are checked to contain no digits at all |
425
425
  | `scripts/run-file-history-capture-capability-test.mjs` | The engine's file-history-capture self-description (`capabilities.fileHistoryCapture`), read the same four-state way as its sibling capability readers: an absent key is reported as not reported (never folded into `off`), words are taken as an open set so a newer mode is not mistaken for a malformed answer, `fileHistoryCaptureMode` recognises `off` and `on-optional` (captured, and a run may opt out) — any other word, including the older `on-always` that pre-`7.104` engines sent, reads as unknown — and the wording for `off` speaks about capture only — whether code can be rewound is left to the rewind readings; a malformed reading makes neither the verdict nor the wording throw. The request side is covered too: the fragment helper yields the per-run opt-out word only when the reading is `on-optional` and the host asked not to capture — never for an older engine that would refuse the unknown key — and the fragment is driven through the real request constructor on every lane. The recognised words are checked both ways against the capture vocabulary of a `7.104` engine package. |
426
426
  | `scripts/run-model-identity-resolvability-test.mjs` | The model-identity judgement a client makes before letting anyone in: can the engine it is about to use start with a model name? Each end reports what it read from each place that can feed a model name to a local engine (complete, partial — a gateway address or a credential but no model name —, absent, or unreadable), or, for an engine that runs elsewhere, whether that engine has been seen answering; `modelIdentityResolvability` answers resolvable, unresolvable or unknown. Having part of an upstream configuration is not having enough of one, so partial lanes never add up to resolvable; a lane that was not reported or could not be read makes the answer unknown rather than unresolvable; an engine that runs elsewhere is never judged unresolvable and local lanes are never consulted for it (it does not start without a model name, so seeing it answer is enough to call it resolvable). `modelSetupDecision` combines that answer with whether this end can configure a model at all: setup is offered only for unresolvable on an end that can configure one, an end that cannot says so and points at whoever runs the engine, and unknown never opens setup. The detail and notice sentences are checked to be pairwise distinct, unknown sentences neither claim a model is configured nor that it is not, and the module is checked to import no platform I/O. The catalog lane counts as complete only when the host reports that the local engine accepts a catalog default; a report of `false` reads the catalog as partial, and no report (an explicit `undefined` included) or an unusable one leaves that lane undetermined — the same answer as before the catalog could be complete. Every sentence about a complete catalog names the catalog, including the undetermined sentence when the report is `false` and another lane cannot be read, and a 25-cell matrix pins that the report changes nothing when the catalog is not complete. |
427
- | `scripts/run-cloud-effective-projection-test.mjs` | The cloud control plane's effective-configuration response beyond its four configuration domains, and what a locally started engine does with the models document derived from it. Three top-level keys are read with the meaning their producer gives them: `warnings` (degradation warnings from the build that produced the served view — an empty list is a clean build, an absent key is an older server that cannot tell), and `budget` / `runtimeCaps`, where `null` means two different things: nothing resolves for this principal when you view yourself, and values withheld when the response previews another principal. A missing key or a wrong type is a third state, unknown, and none of the three is ever folded into a zero, a `false` or "no budget". A malformed warning row, budget field or cap costs only itself, and a known budget field of the wrong type is named as not shown rather than silently read as "no limit on this axis"; warning kinds are an open set, so a kind this client does not recognise still produces a warning line. The budget and cap readers are reconciled against the installed settings schema. The single wording source puts degradation warnings first and keeps every "not set / withheld / unknown" sentence free of digits, while a zero the server really sent is shown as a zero. On the models side, the document is written in the shape of the installed settings schema: the default model rides `roles.default` and the @-mention policy rides `atMention` (`only` carries its list, `none` is written, and `all` is left to the schema default). Roles are carried with their catalog references checked; when the host injects an entry check the document also carries the tier groups, the active tier group and the default model that a legacy `default` key names (without the check none of those three is written), and every catalog reference the local engine's schema would reject — a role, @-mention entry, tier binding or active group that names something outside the catalog served to this principal — is dropped and recorded, because one dangling reference makes the local engine discard the whole models domain and fall back to its environment catalog; this is proven by reading the produced document with the installed file store. The @-mention policy is narrowed with the settings contract's own narrowing function: an allowlist pruned to empty becomes "nothing may be mentioned" (never "everything") and gets one warning saying so, and a policy this client cannot read is not written (the schema default applies) and gets its own warning. The keys the settings schema retired (`default`, `atModelAllowlist`) are never written; an older control plane that still sends them is read through the same normaliser and written in the current shape, and when both generations name the default model the legacy key wins and the role entry it covers is recorded. Both generations of the same response produce the same document, byte for byte, and the gate compares the result with what the settings contract's own one-time migration makes of the older shape. Warning kinds with their own sentence are reconciled against the kinds the installed schema can produce. A per-model budget in which no field could be read is never described as having no limits. Registry annotation keys on model entries (`origin`, `overridesTeam`) are removed before the models document is written: the local engine's schema does not accept them, and a configuration refresh would otherwise be rejected as a whole. A model entry the host-injected entry check rejects is left out of the document and references to it are dropped: the local engine drops such an entry at startup, but a refresh rejects the whole configuration over it, so the gate requires a clean read of the produced document; when no entry passes the check, the catalog is kept as sent and gets its own warning, which the gate proves by reading the document back. The check receives a copy, so it cannot alter what is written. Tier words outside the local schema's closed set are dropped and recorded as unsupported, and the package's tier word list is reconciled against the installed schema in both directions; an active tier group is judged against group names, never model names. Since 0.96.0 the MCP leg's account of HTTP transport keys is reconciled against the installed settings contract: the connector assertion header is not forwarded while the request shape has no such key (the guard turns red the day it does), and an entry that names both the principal header and the assertion header is left out with the reason `principal-and-assertion-header` instead of having one of the two picked, answering exactly as the contract's own parse does whenever both are well-formed names; the dropped entry's header references are not reported as missing environment names. The request shape is read with the type checker, so a quoted or inherited declaration counts as declared. |
427
+ | `scripts/run-cloud-effective-projection-test.mjs` | The cloud control plane's effective-configuration response beyond its four configuration domains, and what a locally started engine does with the models document derived from it. Three top-level keys are read with the meaning their producer gives them: `warnings` (degradation warnings from the build that produced the served view — an empty list is a clean build, an absent key is an older server that cannot tell), and `budget` / `runtimeCaps`, where `null` means two different things: nothing resolves for this principal when you view yourself, and values withheld when the response previews another principal. A missing key or a wrong type is a third state, unknown, and none of the three is ever folded into a zero, a `false` or "no budget". A malformed warning row, budget field or cap costs only itself, and a known budget field of the wrong type is named as not shown rather than silently read as "no limit on this axis"; warning kinds are an open set, so a kind this client does not recognise still produces a warning line. The budget and cap readers are reconciled against the installed settings schema. The single wording source puts degradation warnings first and keeps every "not set / withheld / unknown" sentence free of digits, while a zero the server really sent is shown as a zero. On the models side, the document is written in the shape of the installed settings schema: the default model rides `roles.default` and the @-mention policy rides `atMention` (`only` carries its list, `none` is written, and `all` is left to the schema default). Every role goes through one gate: a role that names something outside the catalog is dropped and recorded, and a role that names a catalog model is carried only when the host injects an entry check that accepts that entry — one rejected entry costs only the roles that name it — while without the check no such role is written and each one is recorded with the reason `unvouched` (the missing check is the cause, not the role), because the package cannot tell which entries the local engine will discard and a role naming one makes it discard the whole models domain (a role chosen by capability or price names no entry and is always carried). When the host injects an entry check the document also carries the tier groups, the active tier group and the default model that a legacy `default` key names (without the check none of those three is written, so both generations of the same response give the same document with or without the check), and every catalog reference the local engine's schema would reject — a role, @-mention entry, tier binding or active group that names something outside the catalog served to this principal — is dropped and recorded, because one dangling reference makes the local engine discard the whole models domain and fall back to its environment catalog; this is proven by reading the produced document with the installed file store. The @-mention policy is narrowed with the settings contract's own narrowing function: an allowlist pruned to empty becomes "nothing may be mentioned" (never "everything") and gets one warning saying so, and a policy this client cannot read is not written (the schema default applies) and gets its own warning. The keys the settings schema retired (`default`, `atModelAllowlist`) are never written; an older control plane that still sends them is read through the same normaliser and written in the current shape, and when both generations name the default model the legacy key wins and the role entry it covers is recorded. Both generations of the same response produce the same document, byte for byte, and the gate compares the result with what the settings contract's own one-time migration makes of the older shape. Warning kinds with their own sentence are reconciled against the kinds the installed schema can produce. A per-model budget in which no field could be read is never described as having no limits. Registry annotation keys on model entries (`origin`, `overridesTeam`) are removed before the models document is written: the local engine's schema does not accept them, and a configuration refresh would otherwise be rejected as a whole. A model entry the host-injected entry check rejects is left out of the document and references to it are dropped: the local engine drops such an entry at startup, but a refresh rejects the whole configuration over it, so the gate requires a clean read of the produced document; when no entry passes the check, the catalog is kept as sent and gets its own warning, which the gate proves by reading the document back. The check receives a copy, so it cannot alter what is written. Tier words outside the local schema's closed set are dropped and recorded as unsupported, and the package's tier word list is reconciled against the installed schema in both directions; an active tier group is judged against group names, never model names. Since 0.96.0 the MCP leg's account of HTTP transport keys is reconciled against the installed settings contract: the connector assertion header is not forwarded while the request shape has no such key (the guard turns red the day it does), and an entry that names both the principal header and the assertion header is left out with the reason `principal-and-assertion-header` instead of having one of the two picked, answering exactly as the contract's own parse does whenever both are well-formed names; the dropped entry's header references are not reported as missing environment names. The request shape is read with the type checker, so a quoted or inherited declaration counts as declared. Since 0.97.0 a default-model collision is judged on the model a role points at, not on the whole role object: a `roles.default` that points at the same model as the legacy default key is kept as sent (its own extra keys included) instead of being reported as overridden, one that points elsewhere is still reported, and the legacy and current reply shapes yield byte-identical shadow documents for the same-model case. |
428
428
  | `scripts/run-websearch-verdict-test.mjs` | The per-request web-search configuration a host puts on the wire. Newer servers refuse the whole request when that section is malformed — and a missing or misspelled search provider now counts as malformed, because the section names where the searches go and an unknown destination is refused rather than silently swapped for the deployment's own backend. The old readers in this package dropped such a section without a word, which let the server swap destinations after all. The guard pins the new three-way verdict (absent, honoured, malformed with the field that is wrong) against the server's own judge, vector by vector, whenever that judge is available next to this package; it pins that a half-configured environment is malformed rather than ignored, that a malformed environment never falls back to the settings file (that would change the destination too), and that neither the sentence shown to the user nor the recorded reason repeats an endpoint, a key, or a search-parameter name or value — an unrecognised provider is never echoed either (the sentence lists the valid words instead), so a URL or key pasted into the wrong field does not come back out, even when it happens to be all letters. A host's key store is plugged in through a callback the package calls only after the provider has been recognised, so the precedence between environment and settings stays inside the package. Since 0.83.0 an endpoint that carries a user name or password is malformed as well (the server judges the same way from 7.101.0), the reason sentences match the server's own word for word, and the three older readers that dropped a misspelled provider are gone. Since 0.93.0 the search key is checked in the same pass: a key that cannot be carried as an HTTP header value — a line break, a NUL or another control character, or a character above U+00FF anywhere but at the very ends — is malformed, with the server's own sentence (the server refuses such a key from 7.118.0 on, and a deployment's own start-up configuration is refused too), while a trailing or leading newline, as secret files often carry, is accepted just as the runtime accepts it. The rule is a pure re-implementation of the runtime's two-step header check, so the package stays free of Node built-ins; the guard pins it character by character against the real Node header pipeline, sends the whole Latin-1 range through a real loopback request, compares a large corpus with the previous release byte for byte (legal keys read exactly the same; only keys that could never be sent differ), and pins that the server's refusal is read by its code alone — nothing in the package matches its sentence. |
429
429
  | `scripts/run-hooks-merged-disable-projection-test.mjs` | The fourth governance leg of the hooks projection: `disableAllHooks` set in a non-managed settings source. The value that counts is the **merged** one, read from the host through the optional `SettingsPort.mergedDisableAllHooks()`, because a per-source approximation ("any source says true") reads user `true` with local `false` backwards — the merged value there is `false` and every source's hooks ship. When the merged value is `true`, only managed-settings hooks are sent to the engine: non-managed settings can switch off their own hooks, never the managed ones, and a managed `disableAllHooks` is still judged first and sends nothing at all. A full matrix over the four sources, each true, false or absent, is merged with the reference rule (later sources override earlier ones, managed settings last) and every cell's projection is asserted. The reader is the only authority: per-source values never second-guess it, and only a strict `true` counts. A host that does not implement it keeps the previous behaviour and gets exactly one warning per installed settings port, never one per request, and none on paths where the reader would not have been consulted; a reader that throws is treated as `true`, so managed hooks still ship. The session goal's Stop hook and the final-verification yield rule, which reads the projected hooks, follow the same verdict, and the trust gate and the three managed gates are evaluated before the reader is ever called. The last leg pins the member's declared shape in the built declarations: optional, no parameters, returning a boolean or `undefined`. The flag settings source (a settings file or inline settings given at startup) is projected after the local source, and settings hooks stay concatenated managed first (managed → user → project → local → flag): the engine runs hooks in request order, and three of its budgets go to whoever comes first — the per-event wall-clock budget, the per-event cap on model-backed entries, and the total cap on added context — so a managed hook placed after the others could be crowded out and silently not run. The flag source moves with user, project and local under every managed or merged gate; its exec-form entries are dropped and warned about like any other settings source, and the not-run notice names it `Flag settings`. An optional leg feeds the request body to an installed server's hook runner and requires the managed deny, block and context to take effect while non-managed hooks exhaust each of those budgets, and pins the known cost of that order: a non-managed `PreToolUse` hook placed after the managed one can still rewrite the input after the managed check allowed it. Without an installed server the leg reports that it did not run. Safe and bare mode: the host reports its startup mode through the optional `SettingsPort.hooksStartupMode()`, which both the plain and the plan path read and which answers `'safe'` or `'bare'`; the flag already carried by the plugin hooks reading, which only the plan path reads and which cannot tell the two modes apart, counts as safe mode unless the reader named a mode. In either mode only managed-settings hooks are sent from the settings sources: bare mode is handled like safe mode, so managed hooks are still sent, also under the managed-only gates. In both modes plugin hooks are excluded as before and the session goal's Stop hook is still sent. Only the two exact words count; a host that does not implement the reader, returns `undefined`, returns any other value or throws keeps the previous request byte for byte, checked for every such reply, both flag states, both paths and with or without a session goal; any other value and a throw each leave one debug line, and so does a reader written as a property instead of a method, which narrows nothing. The reader is called at most once per request and not at all when hooks are already switched off entirely. The optional server leg requires the left-out sources' hooks not to run while the managed ones still run and still deny, in either mode. A hook written in more than one settings place is sent once: entries on the same event, in groups that are identical apart from their hooks (same matcher), with the same identity — command, shell, arguments and condition for command hooks; type, prompt and condition for prompt and agent hooks; URL for HTTP hooks; server, tool and input for MCP tool hooks — are sent once, at the position of their first occurrence in the concatenation order and with the fields of the copy from the highest-precedence source (managed, then flag, local, project, user; within one source the later copy), so a managed copy keeps both its place and its values and a user copy duplicated in flag settings keeps its place but uses the flag copy's timeout; the other entries keep their order, groups without duplicates are sent as they were, and a group left empty is not sent. Different matchers, events, a one-byte command difference, a different condition, an absent versus explicit shell, different types or extra group keys are not merged; malformed entries and entries whose identity cannot be computed are left alone; plugin groups and the session goal hook do not take part, and hooks removed by safe or bare mode never come back. The optional server leg requires such a duplicate to run once with its added context appearing once, and a user copy with a one-second timeout duplicated by a flag copy with a ten-second timeout to run to completion on a two-second command. |
430
430
  | `scripts/run-memory-saved-projection-test.mjs` | Engine memory writes (a successful `Remember` tool call) moved off the transcript onto the additive `memory_saved` chrome event, driven through the real pipeline: zero transcript rows for the write (the transcript is byte-identical to the same frames with the write reported as not successful), exactly one event whose `notes` carry the note text verbatim (notes, not file paths) and whose key set is exactly kind / laneProof / id / notes; no event for a missing, empty or non-string note, a non-`true` `ok`, a tool error, a missing result or another tool name; two writes give two events in order with distinct ids; a sub-agent write rides the sub-agent lane and an empty parent id emits nothing rather than falling back to the main lane; the `id` is derived from the write's wire key (the tool-end event id, else the tool-start event id, else the call id; empty ids count as absent), so projecting the same wire events twice gives the same id, and it never collides with the tool result row of the same or another call; the event sits right after the tool result row; the arm is registered as required. |
@@ -432,7 +432,7 @@ guard still cross-checks the table by name).
432
432
  | `scripts/run-layering-shadow-export-test.mjs` | Same-name shadows across the first-party clients that consume this package (terminal, desktop, web and the admin console). Each client's product sources are read at the local clone's `origin/main` (or its HEAD when there is no such ref), without fetching, and parsed with the TypeScript parser; every top-level runtime export the client declares itself is compared with this package's public runtime exports. The guard prints which ref, commit and commit date it read for each client, and warns (without failing) when that commit is more than seven days old, because the result then only describes that older snapshot. A client-side declaration carrying the name of a package export means a piece of shared logic now lives in two places and can drift apart. It fails the guard unless it is listed in `scripts/layering-shadow-exemptions.json`, and a listed row must carry one due-by version no more than three minor lines ahead (it fails once the package reaches it; that single field is the only one the guard reads for expiry, and the older retire-by and review-by fields that still sit on the rows for one more version are only checked to agree with it). It also fails once the client has removed the shadow and the row still stands. Every row also names what kind of duplicate it is, and carries the evidence that kind requires. A client copy that is this package's own function object behind a type assertion, or a thin wrapper that only passes the client's own dependencies into this package's function, must be proven so on every run by reading the client's syntax tree; such rows are due for review within two minor versions, and the guard fails as soon as the proof no longer holds. A wrapper whose extra logic the client has confirmed to be host-specific carries that confirmation (the client's own wording and where it was stated) together with a pinned review record that is due within two minor versions. A wrapper that adds its own decisions, or a same-name function that does something else, carries a review record pinned to a hash of the normalized client declaration (comments and formatting do not count); once the client's copy changes, the guard fails until it is reviewed again. A second implementation of the same logic carries a semantic diff: the client's copy is taken from the commit being read, the named declarations and the client modules they import are extracted with the TypeScript parser, transpiled and run in memory against this package's build on the same inputs, and any disagreement outside the classes registered on that row fails the guard (each class is a fixed predicate over both answers). A separate register lists same-meaning copies that carry a different name (a name-based comparison cannot see those): each row names the client declaration and the package export it duplicates, and fails when it is malformed, past its due-by version, or when the client no longer declares that name; the register only records and checks liveness and expiry, it does not scan for such copies. A row can follow a client-side rename to a new name, and can be registered ahead of a client change for a limited number of versions before the client code exists. Re-exports of this package's own exports are the intended form and never count. A client tree that is not present is reported as a skipped section, not as a pass. The ruler proves itself on an in-memory fake client (planted shadows must be caught, legal forms must not), on a throwaway repository (a missing `origin/main` falls back to HEAD, a broken one is a fault rather than a silent fallback), and refuses to report zero on a client whose scan surface is empty. |
433
433
  | `scripts/run-session-policy-deliverable-test.mjs` | Which of a batch of user-written permission rules can be written into a session’s own rule record without changing their meaning, and why each of the others cannot. The record holds whole tool names and command names only, so exactly one class maps across losslessly: a deny rule that names one tool with no qualifier. Everything else is withheld with one word from a closed seven-word list — an ask rule (the record has no ask tier), a deny rule with a parenthesised qualifier (recording just the name could block more), a rule that names a server or agent peer without naming one of its tools (for every protocol namespace the engine knows, checked against the engine package's own table) or contains a wildcard (*) anywhere (an engine that compares exact names would block nothing), an entry that is not a tool name, and a name the engine refuses at the start of every run — a retired tool name, or one containing "__" without a protocol prefix, where the prefix check is case-sensitive (once such a name is in the record, every later run of the session fails at startup until that entry is removed; the retired-name list is checked entry by entry against the engine package's own list, and a withheld retired name carries its current name when the engine says it was renamed), and — only when the caller passes the tool roster of a run — a name that roster does not list as a tool name or alias, compared exactly with letter case counted (recorded as it is it would block nothing on that deployment) — and each word has one sentence, which never echoes the rule itself; asking for the sentence never throws, even with a value that throws when turned into a string. A name with leading or trailing whitespace counts as not a tool name: the record compares exact bytes, so it would block nothing. The guard pins the batch semantics: the deliverable part is either the whole batch or empty, never a subset, so a caller cannot send half a change and report it as saved. An end-to-end check runs the engine package itself: every batch this function would deliver — the recorded vectors and a fixed-seed sample of generated names — is written into an in-memory session rule store and the next run must get past its start-up checks and reach the model, while every name withheld as refused — every retired name included — must indeed make that run fail at start-up, and every string literal in the judgement source that it withholds as refused must be one the engine package's own tables refuse. It also checks that malformed input never throws and never delivers anything (non-arrays, non-string entries, holes, a polluted array prototype, a length or index that throws, a changing index read once), that a batch which cannot be read at all is marked `unreadable: true` while an empty batch is not, so the two stay tellable apart, that each word is produced by some vector and nothing outside the list is produced, and — when a checkout of the previous in-client implementation is present — that this function gives the same answer on every recorded vector and on tens of thousands of generated rules and pairs, except for five deliberately stricter classes (whitespace-padded names; rules with a wildcard anywhere, which the previous implementation sent as exact names unless the wildcard was the whole tool part of a server rule; peer-wide rules outside the MCP namespace, which it did not recognise; names the engine refuses at start-up, which it sent as ordinary names; and names the engine's rule compiler does not accept as one whole tool, such as an escaped parenthesis or a backslash in the name), whose disagreements are counted per class and must match an independent count exactly. The optional roster only ever withholds more: with no roster, or an absent or null one, every answer is byte-identical to the last release before the roster existed, checked against that release's published file over tens of thousands of inputs, apart from the rule-syntax class described below; a name withheld without a roster stays withheld with the same word, and the same current name, when the roster lists it as a name or an alias; a name delivered without a roster is withheld as not in the roster exactly when the roster lacks it; the engine's own namespace-covering forms keep their word; and a roster or options value that cannot be read marks the whole batch unreadable instead of falling back to no roster, while a readable empty roster is a roster. An end-to-end check takes the roster from a real run of the engine package that mounts a tool with an alias: every name delivered with that roster passes the engine's start-up name audit without being reported, and every name withheld as not in the roster is one that audit reports as matching no mounted tool. When the previous in-client implementation carries its own roster check, the two are compared under a given roster and may differ only for an alias (delivered here, withheld there) and for a refused name that the roster lists (withheld here, delivered there). A deny rule is delivered only when the installed engine's rule compiler, asked about that one string the same way the request-body permission assembly asks about whole tool names, accepts it as a rule for one whole tool: a name with an escaped parenthesis, a backslash, a lone closing parenthesis or more characters than a rule may have is withheld as not a tool name, because the record would compare those bytes and match no tool. When the compiler is not installed or cannot answer, a batch that needs it is marked unreadable rather than delivered, while batches that never reach that step answer as before. The guard checks that every delivered name is one the compiler accepts, that nothing delivered now was withheld by the earlier release and the extra withholding is exactly that class, that a roster never makes such a name deliverable, that the compiler of every engine generation in the server fixtures gives the same answers, and that the request-body assembly reaches the same verdict on the same names. |
434
434
  | `scripts/run-plugin-hooks-projection-test.mjs` | Plugin hooks: each command hook an enabled plugin declares is decided one by one as running in the engine, running in this client, or not running at all, and the page of hooks sent with a request is built from the same per-turn plan the client uses to skip its own copies, so one hook never runs in two places. Governance is judged first and always wins — a managed hooks switch-off, an untrusted workspace, safe or bare mode, or a governance read that fails sends no plugin hook and does not list it as a gap; managed-hooks-only (set directly, or through a merged non-managed hooks switch-off) keeps only managed plugins; the plugin-only customization lock does not touch plugin hooks. A hook reaches the engine only when this client started the engine on this machine, the engine reports plugin-hook support, the entry is a command, the plugin declares no sensitive option, and the event still fits the engine's per-event limits; the gate walks that matrix cell by cell, including the limit boundaries and a session goal hook counting toward them. A fact that was never read is reported as not known rather than as a fact: a host that does not say where the engine runs gets a "not known whether this client started the engine" reason, an engine whose capabilities have not been read yet gets a "not known yet whether it supports plugin hooks" reason, and the plan's two engine facts are null in those cases, not false. Events the engine never fires run only if the client says it fires them itself, and hooks the upstream behaviour itself refuses (option references in a shell-form command, an unset option in exec form, malformed entries) run nowhere. Exec-form arguments are passed element by element with only saved non-sensitive option references filled in; path placeholders are left for the executor. Sensitive option values never reach the request: with a host that wrongly supplies one, every string in the plan, the request body, the notice, the labels and the log is searched for it across eight cells. A host without the plugin reader keeps the previous request body and gets exactly one warning per settings port; plugin data that throws while it is being read (a throwing getter, a revoked proxy) is treated like a failing reader — no plugin hooks this turn, settings hooks still sent, nothing thrown; the not-running notice names the plugin and events, never a command or an option value, and escapes control characters in names. Command hooks from settings that carry arguments (a non-empty `args` array, which is the exec form, or any other non-null value) are removed from the request until the engine reports support for arguments, because the engine would otherwise drop the arguments and run the bare command through a shell; an empty `args` array is not treated as carrying arguments when the command is made only of letters, digits and `_ . / : + -` (the shell runs the same executable), so such a guard still reaches the engine, while an empty array on a command with spaces or shell characters is removed; `args` on a prompt or http entry, a null `args`, or an entry with no type is left alone, and those go out unchanged. MCP tool hooks, which the engine cannot parse, are removed only from a request built from a plan, whose not-running notice the host shows; a request built without a plan still carries them on engine-fired events, so the engine rejects the whole request loudly instead of a guard hook silently not running — the gate checks both request bodies against the engine's own hooks schema. Without a plan, every removed hook of that kind on an engine-fired event produces one warning per settings port, event and reason. Malformed entries still pass through for the engine to reject loudly, and passing null where the options object goes behaves like passing nothing; a `plan` option that is not a plan is ignored rather than turning the whole page into nothing, and a plan passed directly in place of the options object is recognised and used. A `plugin` key written by hand on a settings hook is stripped before sending (even when its value is undefined), because only hooks that come from the plugin reader may carry plugin context; the settings document itself is left untouched and a debug line records the count. The two hand-copied tables, the engine-fired event list and the engine limits, are checked against their owners. Since 0.89.0 the same per-turn decision also gives every settings hook on an engine-fired event exactly one executor: a command, HTTP, prompt or agent hook from settings goes to the engine when the engine reports that it runs hooks sent with requests and either this client started the engine on this machine or this client cannot run that hook itself; otherwise this client runs it and the hook is left out of the request. A hook the engine is known to skip (a PowerShell command, or hooks the engine ignores entirely) that this client cannot run either is listed as not running; one the engine may or may not reach (capabilities not read yet, or a prompt or agent hook after the fourth one on an event) is still sent and not listed. The client learns which of its own settings hooks to skip from `settingsHooksKeptLocally`, which matches hooks by content against the request body only, returns the same array when nothing is skipped, and for the two compaction events follows whether the engine actually compacted. When the prompt was not written by the user, prompt-submit hooks from settings and plugins are left out on both sides. A host that reports events must also report which hook types its own executor runs on each event: with that table, a plugin hook it cannot run is reported as not running rather than as running in this client; without it, prompt and agent hooks from settings on engine events are treated as hooks the client cannot run and stay in the request for the engine. A settings hook whose required field is unusable (a command, URL or prompt that is not a non-empty string) is not assigned to either side and is sent unchanged, so the service still rejects it; malformed rows in a plan handed back by the host never make the not-running notice throw and never authorise skipping a hook; and a missing, empty or `*` matcher counts as the same match-all matcher when the client decides what to skip. A matrix over source, event, hook type, capability, engine ownership, host shape, prompt origin and compaction outcome checks that every hook runs at most once and exactly once whenever either side can run it, while the print lane and hosts without a local executor keep the previous request byte for byte. An optional leg runs the request body through an installed server's hook runner, current and previous release, together with the client-side skip, and counts one run per prompt. A plan in the previous release's six-key shape is read the same way by the request side and the skip side, so every hook it sends runs once; the client side of the skip reads groups and entries the way the client's executor does (class instances and inherited getters included); and on the two compaction events a prompt or agent hook past the engine's per-event limit runs in the client when the client can run it. |
435
- | `scripts/run-display-untrusted-projection-test.mjs` | The single display-safety outlet (`displayUntrusted`) and the credential wash on the end-of-run rows this package mints. The outlet composes two credential nets (URL structure: userinfo, every query value, the fragment, path parameters and path segments that start with a known secret prefix; key/value words such as `Authorization: Bearer ...`, `Authorization: token ...` or `api_key=...`, plus well-known secret literals that appear without a label, such as `sk-...`, `ghp_...`, `AKIA...`, JWTs and the body of a PEM private key) with three character nets (control characters, bidirectional and format characters, whitespace folding). The credential nets match on a view of the text with ANSI sequences, format characters, control characters and the outlet's own escape tokens stripped, and map the result back onto the original, so colouring or an invisible character wedged between a label, its separator and its value cannot hide the value, and no stray marker is left behind. Whitespace of any length around the separator is accepted. Hosts, ports, paths, query key names and surrounding prose stay byte-for-byte, clean text comes back unchanged, the result is idempotent (also with a length cap), a length cap never splits an escape token or a surrogate pair, an invalid cap means no cap, and every net can be switched off on its own. A few narrow shapes are left alone because they name something rather than carry a value (a plain English word after `bearer` or `basic`, a back-quoted credential variable name, a plain integer after `tokens:`, a list of key names after `keys:`), each with a counter-example that is still washed. Regional flag emoji built from tag characters are kept whole. The existing single-line helpers (`escapeDisplayControlChars`, `collapseLabel`, `capForDisplay`, peer sender names and the hook failure banner) run on the same engine and, since 0.96.0, read the same character table as the outlet: each is checked equal to the outlet with both credential nets off (escape without folding, fold then escape, space mark then fold, and the same length cap) over a seeded pool of more than 600 strings; every character class the old sets left alone (zero-width characters, the word joiner, invisible operators, U+FEFF, the soft hyphen, tag characters and other format characters, plus C1 controls, bidirectional controls and lone surrogates in sender names) is marked class by class, while joiners inside emoji sequences and joining or Brahmic scripts and RGI regional flags stay as written; no character the old sets marked is ever left raw, input without the newly marked characters stays byte-identical to the previous output over every BMP code unit plus random strings, all of them are idempotent, and neither `capForDisplay` nor the hook banner's 512-character cap splits a `\u{…}` escape token or a surrogate pair. The approval decision-note echo, the subagent resume receipt (and its failure debug line) and the startup list of plugin hooks that will not run now also drop bidirectional and format characters (and, for the receipt, C1 controls); a note that is empty after cleaning is treated as absent. The synthetic end-of-run rows (`API Error:`, `Run stopped:`, `Model output error:`, `Outcome unknown:`) and the result frame's `errors[]` pass both credential nets before they leave the package, on the print lane and on the interactive lane (which also keeps the row-class flag); this covers a blocked reason whoever wrote it, while assistant text rows, a successful `result` and salvaged output are never touched, and a non-string `errors[]` entry is passed through unchanged. The known-secret-prefix check is a local copy of the configuration package's detector and is compared with the installed one entry by entry. Since 0.83.5 the outlet has two opt-in switches and a position read-out. `escapeBackslashes` (escape form only) writes every literal backslash as a pair, so each output decodes back to exactly one input (a real invisible character and its literal six-character spelling no longer look alike); a reference decoder round-trips thousands of random strings, the output is byte-identical to the default when the input has no backslash, a length cap is measured on the paired output and keeps the longest fitting prefix, credentials are washed exactly as in the default, and the switch is not idempotent by design (use it only at the final render). Zero-width joiners and non-joiners are kept only inside emoji sequences drawn as emoji (so not between symbols such as © or ™ that display as text) and between letters of scripts where they change the shaping (joining scripts such as Arabic, and the Brahmic family), each listed script checked both ways; the one other place a joiner is kept is right after a virama at the end of a word, the older spelling still found in Malayalam and Bengali text. Next to Latin, Cyrillic, CJK and other letters, next to modifier letters shared across scripts, at the start of a word, at the end of a word without a virama before it, or on their own they are now marked. `blanks` marks characters that look like a space but are not an ASCII space (no-break and other width spaces, the ideographic space, Hangul fillers, the blank Braille pattern) before whitespace folding, for names that must never look alike. `displayUntrustedMarks` returns the same text plus the position of every character mark; its text is compared with the outlet over thousands of inputs. With both credential nets off, the character face stays byte-identical to the previous release for input without joiners. The credential nets read escape sequences the way a terminal would when one is cut short: an unfinished colouring or character-set sequence interrupted by another one is dropped as a whole, a sequence never takes the `@` of an address as its final character, and a final character that starts a well-known secret literal (`sk-`, `ghp_`, `AKIA`, a JWT) is also read as the start of that literal; escape tokens this outlet writes are read as one unit, while look-alike text it never writes (an upper-case `\U`, or a code point it never marks) is read as plain text. A URL is cut before a run of non-ASCII blanks followed by a credential label or scheme word, Hangul fillers and the blank Braille pattern count as spaces around a label's separator, and a value that itself starts with a quoted label (`token= "password":"..."`) is left to that inner label. With `blanks` on, a blank written as an escape token right after a label is read as a blank when the value is judged, so `password:` followed by a no-break space and `missing` stays unmasked and an empty value gets no marker; the credential nets also read the text the way it looks after default whitespace folding and combine what each reading masks, so whatever the default form masks stays masked with `blanks` on (checked over a seeded corpus for the escape form, with paired backslashes and without folding; the exceptions are text that itself contains a literal six-character blank escape, which cannot be told apart from one the outlet wrote, and the dot and space marks, which cannot tell a marked blank from a real dot or space). A lone surrogate wedged between a label, its separator and its value no longer hides the value: the credential nets treat it exactly like a format character, in the machine-readable wash, in a single pass of the display outlet, and on the end-of-run rows and the result frame's `errors[]` on both lanes, while lone surrogates anywhere else are left byte-for-byte. Since 0.84.1 an address-shaped value (`scheme://…`) that sits right after an invisible single character — a format character, a lone surrogate, a non-whitespace control character, or the outlet's own escape token for one — is no longer let through as an address: the scheme stays and the host and path are replaced, with the query and fragment masked as before; `user:<password>@` followed only by stripped units and then a boundary, a port or another `@` is masked as userinfo. A value after real whitespace or after a colour sequence is still treated as an address, and clean text stays unchanged; forms that the previous release masked are checked not to leak on the same outlets. A `user:<password>@` candidate that is the value of a credential label or scheme word — including a label or scheme word split by stripped characters, and a quoted value that goes on past whitespace — is left to the label pass, so the whole value is masked as in the previous release; both reported shapes and frozen samples from the targeted pools are checked on every outlet. A scheme-less `user:password@host` whose password holds a double quote, backtick or angle bracket is still masked when a credential label or scheme word sits right before the user name, or when the user name is a plain word that is not itself a label, the host has a real shape and the address is not wrapped by that quote; quoted JSON values, quoted or bracketed e-mail addresses and quoted arrays stay byte-identical. When a label inside a quoted value ends right at that value's closing quote, the quote and separator stay outside the mask, and an address that begins the rest of a quoted value counts as an address only up to the next blank, so washing the output again never changes it. The credential stage now repeats its single scan on its own output until nothing changes (the cap counts rewrites, at most eight; clean text takes exactly one pass), so masking the output again never changes it for every input that settles within the cap — the random, exhaustive and grammar pools and the two known step-by-step constructions settle in at most three passes counting the confirming one, and the searched constructions kept in the pool in at most six; a deliberately constructed input can need more rewrites as it grows, and at the cap the last rewrite is returned, which a further pass can only mask more of. The checks cover the pass cap and how reaching it is reported, single-pass clean text, a grammar sample of the busiest shapes, constructions that once advanced one step per pass, the mark positions on the final output and how positions compose across rewrites. A username that is itself a credential label with a quote or angle bracket in the password is masked as userinfo (also with only the URL net on) unless the label's own value really reaches the `@`; a URL parameter such as `;token=...` or `,token=...` stays inside the URL piece; a private-key header the outlet cannot recognise does not hide the labels inside its block; the tail glued after the host of a label's address value is masked to the value's end; and a value this package's own transcript wash split at a carriage return is masked when the display joins it back. A bare `Token <value>` / `token <value>` (no label, no separator) is now washed like `Bearer`, with a wider allow-list of ordinary English words after `token` so prose such as `token count: 1234` or `token expired` stays readable; a `/token` URL path tail followed by a status code is not a scheme word and stays as is, and neither is `token` right after `unexpected` in a parser error such as `SyntaxError: Unexpected token '<'`, which stays readable. The `token` scheme word only adds masking of its own value: label values and `bearer` / `basic` values are read exactly as in the previous release, so a value that starts with another character, or a `bearer` value, followed by `token`, a non-ASCII space and an address is still masked whole. When a credential label or scheme word is followed by a colon and a YAML block scalar header (`\|` or `>`, optionally with `-`, `+` or digits, invisible characters allowed between them, and nothing else on that line but blanks, invisible characters or a comment, ending in a line break), the whole indented block becomes a single marker: the block runs until the first line with visible content that is not indented deeper than the label's line, blank lines included, and the line break before that line and the line itself stay as they were on outlets that keep line breaks (outlets that draw line breaks as escapes or dots merge the next word into the marker, as they already do after any value). Raw and JSON-escaped line breaks are both read; blanks inside escape sequences or control strings never count as indentation, and a block line holding only a control string counts as content and is masked whole. Forms where the marker is followed by more text on the same line, `=`-separated forms such as `key = \|` or the `=>` arrow, and a header at the very end of the text read exactly as in the previous release. From 0.94.0 the private-key header has one definition, generated from the settings contract: the whole-string check finds it anywhere in the text (as the contract's own write-side check does from its 10.0.0 release), and the key-block cutter builds its header line from the same definition, so a lowercase header or one without a space after BEGIN has its key body masked like the canonical form, while public keys and certificates are left as they were. Since 0.94.0 the credential words also read through YAML node properties (type tags such as `!!str` and anchors such as `&name`) between the colon and the value: the properties and the value after them get one marker each, block scalars and block-sequence items included, and a property with no value after it on the same line reads exactly as before. A list after a credential label (a block sequence, a flow sequence including JSON arrays, or a same-line comma list after a plural label) is judged item by item with the same exemptions as a single value: a list of plain key names after a bare `keys`, or of integers after a bare `tokens`, is left alone, and any value-like item masks every non-neutral item. A dash right after the colon on the same line is a list bullet, not a value. The retry overlay's error text (`error.formatted` on the four error arms) passes through the same outlet, and the label vocabulary is pinned as it stands (members masked, known out-of-family keys such as `passphrase` left as written). From 0.95.0 the credential words also read a value wrapped in round or square brackets after a scheme word or label (placeholder-shaped contents such as `(API_KEY)` or `(your-token)` and bracketed prose stay as written), a JSON-escaped tab between a scheme word or label and its value, a same-line associative-array arrow with a quoted label and a quoted value, and a `token` scheme word with its value after a credential label as one value; a word with no label at all is masked when it is shaped like a random token (length, three character classes, entropy and run structure), while hex hashes, UUID lists, base64 image headers and data URIs, integrity strings, provider object ids and ordinary identifiers stay as written; a known secret prefix glued after an underscore or hyphen inside an identifier or a URL path segment is masked from the prefix on; and a masked value no longer swallows a closing backquote at its end, or a closing round or square bracket at the end of a URL that has no matching opener, while a backquote inside a value never cuts it short; an address after `Bearer` or `Basic` that is itself the value of a credential label (including `Authorization:`) is masked whole, keeping the scheme word, while an address with no label stays as written. Credential masking also covers three shapes that used to print as written: a name glued onto a credential word (PGPASSWORD=, CUSTOMSECRET=, dbPassword:, secretAccessKey=; singular only, so token-count names such as maxTokens or inputTokens and English words such as monkey stay visible), Bearer or Basic glued to a preceding word character (_Bearer, xBearer), and a known secret prefix followed by one unbroken run of six or more letters and digits (sk-abcdefghijk). The new readings only add masks: a value that was already masked whole stays masked whole when it contains one of these shapes. The configured-address display and the upload check read the same label rule, so a glued name used as a query key counts as a credential in both. A frozen copy of a pattern net that a downstream host used to run locally is replayed against the output as a no-regression check. |
435
+ | `scripts/run-display-untrusted-projection-test.mjs` | The single display-safety outlet (`displayUntrusted`) and the credential wash on the end-of-run rows this package mints. The outlet composes two credential nets (URL structure: userinfo, every query value, the fragment, path parameters and path segments that start with a known secret prefix; key/value words such as `Authorization: Bearer ...`, `Authorization: token ...` or `api_key=...`, plus well-known secret literals that appear without a label, such as `sk-...`, `ghp_...`, `AKIA...`, JWTs and the body of a PEM private key) with three character nets (control characters, bidirectional and format characters, whitespace folding). The credential nets match on a view of the text with ANSI sequences, format characters, control characters and the outlet's own escape tokens stripped, and map the result back onto the original, so colouring or an invisible character wedged between a label, its separator and its value cannot hide the value, and no stray marker is left behind. Whitespace of any length around the separator is accepted. Hosts, ports, paths, query key names and surrounding prose stay byte-for-byte, clean text comes back unchanged, the result is idempotent (also with a length cap), a length cap never splits an escape token or a surrogate pair, an invalid cap means no cap, and every net can be switched off on its own. A few narrow shapes are left alone because they name something rather than carry a value (a plain English word after `bearer` or `basic`, a back-quoted credential variable name, a plain integer after `tokens:`, a list of key names after `keys:`), each with a counter-example that is still washed. Regional flag emoji built from tag characters are kept whole. The existing single-line helpers (`escapeDisplayControlChars`, `collapseLabel`, `capForDisplay`, peer sender names and the hook failure banner) run on the same engine and, since 0.96.0, read the same character table as the outlet: each is checked equal to the outlet with both credential nets off (escape without folding, fold then escape, space mark then fold, and the same length cap) over a seeded pool of more than 600 strings; every character class the old sets left alone (zero-width characters, the word joiner, invisible operators, U+FEFF, the soft hyphen, tag characters and other format characters, plus C1 controls, bidirectional controls and lone surrogates in sender names) is marked class by class, while joiners inside emoji sequences and joining or Brahmic scripts and RGI regional flags stay as written; no character the old sets marked is ever left raw, input without the newly marked characters stays byte-identical to the previous output over every BMP code unit plus random strings, all of them are idempotent, and neither `capForDisplay` nor the hook banner's 512-character cap splits a `\u{…}` escape token or a surrogate pair. The approval decision-note echo, the subagent resume receipt (and its failure debug line) and the startup list of plugin hooks that will not run now also drop bidirectional and format characters (and, for the receipt, C1 controls); a note that is empty after cleaning is treated as absent. The synthetic end-of-run rows (`API Error:`, `Run stopped:`, `Model output error:`, `Outcome unknown:`) and the result frame's `errors[]` pass both credential nets before they leave the package, on the print lane and on the interactive lane (which also keeps the row-class flag); this covers a blocked reason whoever wrote it, while assistant text rows, a successful `result` and salvaged output are never touched, and a non-string `errors[]` entry is passed through unchanged. The known-secret-prefix check is a local copy of the configuration package's detector and is compared with the installed one entry by entry. Since 0.83.5 the outlet has two opt-in switches and a position read-out. `escapeBackslashes` (escape form only) writes every literal backslash as a pair, so each output decodes back to exactly one input (a real invisible character and its literal six-character spelling no longer look alike); a reference decoder round-trips thousands of random strings, the output is byte-identical to the default when the input has no backslash, a length cap is measured on the paired output and keeps the longest fitting prefix, credentials are washed exactly as in the default, and the switch is not idempotent by design (use it only at the final render). Zero-width joiners and non-joiners are kept only inside emoji sequences drawn as emoji (so not between symbols such as © or ™ that display as text) and between letters of scripts where they change the shaping (joining scripts such as Arabic, and the Brahmic family), each listed script checked both ways; the one other place a joiner is kept is right after a virama at the end of a word, the older spelling still found in Malayalam and Bengali text. Next to Latin, Cyrillic, CJK and other letters, next to modifier letters shared across scripts, at the start of a word, at the end of a word without a virama before it, or on their own they are now marked. `blanks` marks characters that look like a space but are not an ASCII space (no-break and other width spaces, the ideographic space, Hangul fillers, the blank Braille pattern) before whitespace folding, for names that must never look alike. `displayUntrustedMarks` returns the same text plus the position of every character mark; its text is compared with the outlet over thousands of inputs. With both credential nets off, the character face stays byte-identical to the previous release for input without joiners. The credential nets read escape sequences the way a terminal would when one is cut short: an unfinished colouring or character-set sequence interrupted by another one is dropped as a whole, a sequence never takes the `@` of an address as its final character, and a final character that starts a well-known secret literal (`sk-`, `ghp_`, `AKIA`, a JWT) is also read as the start of that literal; escape tokens this outlet writes are read as one unit, while look-alike text it never writes (an upper-case `\U`, or a code point it never marks) is read as plain text. A URL is cut before a run of non-ASCII blanks followed by a credential label or scheme word, Hangul fillers and the blank Braille pattern count as spaces around a label's separator, and a value that itself starts with a quoted label (`token= "password":"..."`) is left to that inner label. With `blanks` on, a blank written as an escape token right after a label is read as a blank when the value is judged, so `password:` followed by a no-break space and `missing` stays unmasked and an empty value gets no marker; the credential nets also read the text the way it looks after default whitespace folding and combine what each reading masks, so whatever the default form masks stays masked with `blanks` on (checked over a seeded corpus for the escape form, with paired backslashes and without folding; the exceptions are text that itself contains a literal six-character blank escape, which cannot be told apart from one the outlet wrote, and the dot and space marks, which cannot tell a marked blank from a real dot or space). A lone surrogate wedged between a label, its separator and its value no longer hides the value: the credential nets treat it exactly like a format character, in the machine-readable wash, in a single pass of the display outlet, and on the end-of-run rows and the result frame's `errors[]` on both lanes, while lone surrogates anywhere else are left byte-for-byte. Since 0.84.1 an address-shaped value (`scheme://…`) that sits right after an invisible single character — a format character, a lone surrogate, a non-whitespace control character, or the outlet's own escape token for one — is no longer let through as an address: the scheme stays and the host and path are replaced, with the query and fragment masked as before; `user:<password>@` followed only by stripped units and then a boundary, a port or another `@` is masked as userinfo. A value after real whitespace or after a colour sequence is still treated as an address, and clean text stays unchanged; forms that the previous release masked are checked not to leak on the same outlets. A `user:<password>@` candidate that is the value of a credential label or scheme word — including a label or scheme word split by stripped characters, and a quoted value that goes on past whitespace — is left to the label pass, so the whole value is masked as in the previous release; both reported shapes and frozen samples from the targeted pools are checked on every outlet. A scheme-less `user:password@host` whose password holds a double quote, backtick or angle bracket is still masked when a credential label or scheme word sits right before the user name, or when the user name is a plain word that is not itself a label, the host has a real shape and the address is not wrapped by that quote; quoted JSON values, quoted or bracketed e-mail addresses and quoted arrays stay byte-identical. When a label inside a quoted value ends right at that value's closing quote, the quote and separator stay outside the mask, and an address that begins the rest of a quoted value counts as an address only up to the next blank, so washing the output again never changes it. The credential stage now repeats its single scan on its own output until nothing changes (the cap counts rewrites, at most eight; clean text takes exactly one pass), so masking the output again never changes it for every input that settles within the cap — the random, exhaustive and grammar pools and the two known step-by-step constructions settle in at most three passes counting the confirming one, and the searched constructions kept in the pool in at most six; a deliberately constructed input can need more rewrites as it grows, and at the cap the last rewrite is returned, which a further pass can only mask more of. The checks cover the pass cap and how reaching it is reported, single-pass clean text, a grammar sample of the busiest shapes, constructions that once advanced one step per pass, the mark positions on the final output and how positions compose across rewrites. A username that is itself a credential label with a quote or angle bracket in the password is masked as userinfo (also with only the URL net on) unless the label's own value really reaches the `@`; a URL parameter such as `;token=...` or `,token=...` stays inside the URL piece; a private-key header the outlet cannot recognise does not hide the labels inside its block; the tail glued after the host of a label's address value is masked to the value's end; and a value this package's own transcript wash split at a carriage return is masked when the display joins it back. A bare `Token <value>` / `token <value>` (no label, no separator) is now washed like `Bearer`, with a wider allow-list of ordinary English words after `token` so prose such as `token count: 1234` or `token expired` stays readable; a `/token` URL path tail followed by a status code is not a scheme word and stays as is, and neither is `token` right after `unexpected` in a parser error such as `SyntaxError: Unexpected token '<'`, which stays readable. The `token` scheme word only adds masking of its own value: label values and `bearer` / `basic` values are read exactly as in the previous release, so a value that starts with another character, or a `bearer` value, followed by `token`, a non-ASCII space and an address is still masked whole. When a credential label or scheme word is followed by a colon and a YAML block scalar header (`\|` or `>`, optionally with `-`, `+` or digits, invisible characters allowed between them, and nothing else on that line but blanks, invisible characters or a comment, ending in a line break), the whole indented block becomes a single marker: the block runs until the first line with visible content that is not indented deeper than the label's line, blank lines included, and the line break before that line and the line itself stay as they were on outlets that keep line breaks (outlets that draw line breaks as escapes or dots merge the next word into the marker, as they already do after any value). Raw and JSON-escaped line breaks are both read; blanks inside escape sequences or control strings never count as indentation, and a block line holding only a control string counts as content and is masked whole. Forms where the marker is followed by more text on the same line, `=`-separated forms such as `key = \|` or the `=>` arrow, and a header at the very end of the text read exactly as in the previous release. From 0.94.0 the private-key header has one definition, generated from the settings contract: the whole-string check finds it anywhere in the text (as the contract's own write-side check does from its 10.0.0 release), and the key-block cutter builds its header line from the same definition, so a lowercase header or one without a space after BEGIN has its key body masked like the canonical form, while public keys and certificates are left as they were. Since 0.94.0 the credential words also read through YAML node properties (type tags such as `!!str` and anchors such as `&name`) between the colon and the value: the properties and the value after them get one marker each, block scalars and block-sequence items included, and a property with no value after it on the same line reads exactly as before. A list after a credential label (a block sequence, a flow sequence including JSON arrays, or a same-line comma list after a plural label) is judged item by item with the same exemptions as a single value: a list of plain key names after a bare `keys`, or of integers after a bare `tokens`, is left alone, and any value-like item masks every non-neutral item. A dash right after the colon on the same line is a list bullet, not a value. The retry overlay's error text (`error.formatted` on the four error arms) passes through the same outlet, and the label vocabulary is pinned as it stands (members masked, known out-of-family keys such as `passphrase` left as written). From 0.95.0 the credential words also read a value wrapped in round or square brackets after a scheme word or label (placeholder-shaped contents such as `(API_KEY)` or `(your-token)` and bracketed prose stay as written), a JSON-escaped tab between a scheme word or label and its value, a same-line associative-array arrow with a quoted label and a quoted value, and a `token` scheme word with its value after a credential label as one value; a word with no label at all is masked when it is shaped like a random token (length, three character classes, entropy and run structure), while hex hashes, UUID lists, base64 image headers and data URIs, integrity strings, provider object ids and ordinary identifiers stay as written; a known secret prefix glued after an underscore or hyphen inside an identifier or a URL path segment is masked from the prefix on; and a masked value no longer swallows a closing backquote at its end, or a closing round or square bracket at the end of a URL that has no matching opener, while a backquote inside a value never cuts it short; an address after `Bearer` or `Basic` that is itself the value of a credential label (including `Authorization:`) is masked whole, keeping the scheme word, while an address with no label stays as written. Credential masking also covers three shapes that used to print as written: a name glued onto a credential word (PGPASSWORD=, CUSTOMSECRET=, dbPassword:, secretAccessKey=; singular only, so token-count names such as maxTokens or inputTokens and English words such as monkey stay visible), Bearer or Basic glued to a preceding word character (_Bearer, xBearer), and a known secret prefix followed by one unbroken run of six or more letters and digits (sk-abcdefghijk). The new readings only add masks: a value that was already masked whole stays masked whole when it contains one of these shapes. The configured-address display and the upload check read the same label rule, so a glued name used as a query key counts as a credential in both. A frozen copy of a pattern net that a downstream host used to run locally is replayed against the output as a no-regression check. A length cap never splits a redaction marker: the cut backs off to before it on every capped path (plain, position-tracking, paired backslashes, and the shared cap of the field-escaping helper); strings that are not complete markers are cut as before. |
436
436
  | `scripts/run-engine-agent-absence-projection-test.mjs` | Absent background agents: when the engine stops reporting a background agent and no final state has arrived, the row is marked absent and this package owns every decision about it, so all clients agree. One predicate says whether a row is absent (the mark, not the status, decides). An absent row keeps its last known status, never counts as running, and is never counted as completed, failed or stopped; its elapsed time stops at the last moment it was seen, and its sentence says it may still be running. The end-of-turn sweep never settles an absent row (or a resident one). A row that comes back, or a real final state for the current cycle, clears the mark; a late final state from an earlier cycle does not. Absent rows are never removed at the short grace window. After the hard limit (30 minutes from the last time they were seen) the host is asked for the background-agent registry reading of each row: only a reading that the agent has ended or is not listed lets the row go, and each removal is returned as a fact the host must act on and announce; a reading of running, unknown, missing or unrecognised keeps the row and schedules nothing, so no standing poll is created. Until a registry reading is available every absent row stays. A row someone is viewing is held and reported separately only once the registry confirms it is gone. The row sentence, the removal sentence and the late-result sentence come from one place, never state an outcome or that the agent finished, and escape control characters in names, in the engine's removal word and in the late-result status. An end-to-end cell drives the real fleet projection and the real absence channel through every decision. From 0.84.0 the package also reads the registry itself: a reader lists the session's background registry only when the server advertises the listing, classifies its failures as unavailable (a failed read keeps the HTTP status and error code when the error carries them, and never invents either), and refuses a partly readable listing as a whole; a classifier turns the listing into the per-row reading, treating a registry status it cannot place as unknown and reading "not listed" only for keys shaped like registry handles and only when the host states the engine is a single process and names the row's session, because a key from another identity space proves nothing by being absent and, on a multi-replica deployment, a listing answered by another replica does not contain this session's agents at all (without that statement a missing row reads unknown and the row stays); and a predicate returns the agents the registry still counts as live that the host has no row for. A listing at the server's 500-row cap cannot show that an agent is gone either, so a missing row there reads unknown; the cap is checked against the engine's own list clamp and, when a server build is supplied, against the server's route. Each listing carries its session and a per-client sequence number, and a listing superseded by a later delivered read, or read for another session than the one the host names, counts for nothing. Only the exact listing object the reader returned can authorize removing a row or adding one: a spread copy, a structured clone, a filtered or a hand-built listing reads unknown and adds nothing, the listing, its row array and every row are frozen, the cap check uses the row count recorded when the page was read, and rewriting a listing's sequence number fools nothing. The fill predicate holds back agents first seen within the absence settle window — timed on one monotonic local clock from when this client's reader first saw the id, so a skew between the server's clock and this one, or reconnecting to a long-running agent, cannot skip the window — agents without a readable registration time and agents the host saw end since the read went out; the registry key of a row, progress or absence event is whichever of its two ids is shaped like a registry handle; and none of the reader, the classifier or the predicate throws. |
437
437
  | `scripts/run-plan-review-dismissal-test.mjs` | An automatic reopen does not put back a plan-review card the user closed (the first-presentation path neither checks nor clears that record, so a replayed park frame still presents its card). A plan-review card the user dismissed (Esc, abort, or any answer that is not approve or reject) is recorded per session and run at the moment of dismissal, synchronously, before anything queued behind the card can be released; a reopen marked `trigger: 'automatic'` then refuses with `{ reopened: false, dismissedByUser: true }` instead of minting a new card the user's next keystroke would land on, while the user's own next action (`trigger: 'user'`) reopens it and clears the record. The record is keyed by gate instance when the host supplies an instance reader: a new plan gate on the same run is still surfaced, and anything that cannot prove the gate is new (no reader, a failed, empty, thrown or timed-out read) refuses on the conservative side. The asynchronous form re-checks after its reads and before minting — a decision handed over meanwhile (seen by the package, or reported by the host's optional hand-over predicate) answers as "your answer is on its way"; a record that changed meanwhile makes the stale evaluation mint nothing and answer from the current record: another close refuses as the user's close and keeps the newer record, a card already back on screen (the user's own action or a concurrent automatic reopen, waited for within the receipt window and re-read once the wait is over) answers `reopened: true`, and a record that is gone (session change, ledger overflow) answers a plain refusal without `dismissedByUser`; a hand-over predicate that throws refuses with a plain `{ reopened: false }`. Each run has at most one reopen on its way: a reopen that arrives while an earlier one's card is published but not yet settled joins it instead of minting a second card and retiring the first card's answer path. Instance readers are snapshotted when they resolve, so a host that hands over its own live set still gets a new gate recognised; and a decisive-looking host answer note does not clear the record for the very card the package's own responder already judged non-decisive (the label was not on that card). A successful reopen replaces only the record taken before the card was minted (with an on-screen marker, not a deletion), a decisive answer clears it, the per-session ledger is bounded, and a session change clears its bucket. Without `trigger` the reopen answers exactly as before, apart from joining a reopen already on its way. A reopen for a session with no view subscribed to question frames (`onQuestionFrameFor(sessionKey, handler)`; `onQuestionFrame` for the default session) refuses with `{ reopened: false, _sema_noPresentationSurface: true }` — in the synchronous and the receipt form alike, per session key, and also when the view goes away while the asynchronous check is reading — and that answer comes before the user-closed check, at the entry and after the asynchronous read alike, whether or not the read proved a newer gate (after the read, a decision handed over while the read was in progress, and a dismissal record that was replaced or cleared meanwhile, still take precedence over the missing view); a host log or probe port that throws changes none of the reopen answers (the suite runs the same arrangement with a quiet and with a throwing log port and requires the same answer and the same card frames); every other refusal (receipt window lapsed, decision in flight, user-closed card, non-default slot without a delivery path, record gone) keeps its own keys without the flag. The first-presentation call does not check for a view: without one it still answers `true` and the card frame goes nowhere, which the suite records as today's behaviour, not as a guarantee. A free-text answer on the plan card (no option picked, a non-blank note) counts as a decisive answer: it clears the record and delivers exactly one rejection; a blank note, or a contradictory multi-select that carries a note, does not. |
438
438
  | `scripts/run-gate-interrupt-safety-test.mjs` | The two safety properties of running the suites themselves. The collecting runner no longer kills a suite outright when its time limit expires: it sends a termination signal first, waits a grace period for the suite to clean up, and only then kills it — and it records the suite as timed out however it exits, so a suite that exits cleanly after the signal is still not counted as passing. The limit and the grace period can be widened for a single suite in `gates-manifest.json` (an optional entry with a written reason; a malformed entry, including one with a misspelled field, stops the runner before any suite starts instead of silently falling back to the default, and an entry filed under a misspelled name is reported by this guard rather than ignored), and the negative-control suite is widened there. Every property is exercised on byte-for-byte copies of the real runner and the real negative-control suite in a throwaway directory: a suite that honours the signal finishes within the grace period, one that ignores it is killed when the grace period ends, and the summary lines are unchanged; once a suite has exited the runner waits at most a short drain window for its output pipes, so a child process that inherited them and outlives the suite neither turns a passing suite into a timeout nor holds the runner past the limit, the grace period and that window; the negative-control suite, interrupted in the middle of a rehearsal by any of the three signals or by the runner's own time limit, restores the file byte-for-byte, leaves no backup behind, stops the rehearsed guard together with anything it started, regenerates the build output (checked on a small project in the throwaway directory), and exits with 128 plus the signal number; a rebuild that would overrun the grace period is cut short and its compiler stopped; a backup left behind by an earlier run — next to a later target, loose in the source tree, or inside a symlinked dependency directory — makes it refuse to start without touching anything, naming the file and how to restore it. |
@@ -440,7 +440,7 @@ guard still cross-checks the table by name).
440
440
  | `scripts/run-detach-durable-off-verdict-test.mjs` | The verdict behind the durable-off hint, now a public entry point without the process-level gate. `isDetachDurableOff400(err)` answers whether an error is the engine's refusal of the detach-on-disconnect header on a deployment with no durable run ledger: status 400 and the engine's own refusal sentence in the message, and nothing else — in particular not the error code that refusal carries, because the engine uses the same generic precondition code for unrelated refusals on the same submit path, and treating those as this one would silently resubmit a turn without the header. Browser and desktop hosts, which never arm the terminal's detach ledger, can now ask the same question instead of keeping their own copy. The guard pins the positive case on the real sentence (with or without the code, with surrounding text, and on the error the SDK actually throws when a stubbed engine answers 400), the negative case on the engine's other refusals that share the code (verbatim, and again through the SDK), non-400 statuses, and eighteen malformed inputs that must answer `false` without throwing. It shows the verdict does not read the process ledger and that the hint still returns nothing before the header has been sent. The verdict never throws: an error object whose `status` or `message` getter throws, a proxy whose trap throws and a revoked proxy all answer `false`, each property is read at most once (and `message` not at all unless the status is 400), and the hint therefore answers nothing for those objects instead of throwing as it did before. Against a frozen copy of the previous hint, every other input gives the same answer byte for byte, armed or not. When the engine's source tree is available, it also checks that exactly one refusal site with that code carries the sentence and that every other one is answered `false` |
441
441
  | `scripts/run-parked-resume-startup-test.mjs` | The one reading and the one sentence for a parked resume that fails while starting (`422 parked_resume.startup_failed`). The engine has two unrelated reasons for it — the parked agent's session is gone or the engine rejected the decision itself, or the approval is for a tool the agent inherits from the task that started it and this server cannot hand that tool over — and the response carries **no field that tells them apart**; the reason lives only in the engine's prose, which this package does not branch on. So the reading narrows the cause only by what the caller itself sent: a deny can only be the first reason (the inherited-tool refusal is minted for approvals alone, and the guard pins that upstream premise), while an approval, or a caller that does not say, gets one sentence that lays out both ways forward instead of guessing. Either way the verdict is fixed: sending the same decision again is not a recovery path, and deny stays available. The two sentences are minted in one place, never claim the card is still waiting (in one of the two shapes it is not), and avoid every word the package's own "already decided" scan looks for, since they are appended to failure text that scan reads; all five decision exits append exactly that sentence, and any other failure keeps its text byte for byte. The code also takes precedence over the package's "already decided" word scan: the engine's own text for the gone-session shape contains "not found", which that scan used to read as "someone already settled this card" and answer with a silent reconnect; now a failure carrying this code is classified by the code (no reconnect, the sentence reaches the failure text), while failures with no code or another code are scanned exactly as before. The predicate that says which codes classify themselves is public, so a client with its own "already decided" chain can ask it before scanning words. The engine may also name the reason as a word of its own (an open string; it rides the 422 body's `cause` key, and the SDK reads it as `startupCause` from 13.0.0): the reading carries that word exactly as sent, taking the SDK's named slot first and the generic extra-keys slot second (the only place it exists on SDK 12), only from own data properties and never from the platform's standard error `cause`; an empty, non-string or missing value leaves the key out, and any input without a word reads byte for byte as before. The word changes neither the verdict nor the sentences: the set of words belongs to the engine and has not been published, so nothing here guesses at it (servers up to 7.117.0 do not send it yet). The guard checks every SDK generation it can reach against the real error mapping, and compares a 14,000-input pool without a word byte for byte with the previous release's reading and its two sentences word for word. It also pins the premise that none of the servers and engines it can reach mints a cause word today, so the day one does the guard goes red. |
442
442
  | `scripts/run-plan-review-injected-wire-test.mjs` | The plan-review orchestration accepts a host-supplied engine client, so a browser host behind a same-origin relay — which cannot install a token-bearing engine target — runs the package's own decision path instead of rebuilding it. With a client injected, the decision, its in-flight latch, the resend-once-without-the-key handling of a `request.field_conflict` refusal, the post-decide status re-pull, the six-state effect classification and the outcome text are byte-for-byte what the installed-target path produces, proven against a real relay-form SDK client and a fake engine while the installed target points at a different fake engine that must receive nothing. The three-choice card depends only on the host's declaration: no engine-version evidence is read, so the cache key a host may still pass is accepted and ignored; every request of the relay-form client leaves without an `authorization` header, checked beside a token-bearing client on the same spy so the absence is not the spy's blindness; and a planted token never reaches a log line, an outcome sentence, a queue item or the host callback on any of the failure paths. A decision may carry a `reason` (trimmed, dropped when blank, capped at the server's limit), an `onOutcome` callback hands the outcome back to hosts that have no model-prompt queue (called once per admitted decision, never for a latched duplicate, and its own failure never affects delivery), a reopen with an injected client no longer needs a host delivery function on a non-default session, and the two capability probes take the same injected client with a bounded wait. A decision that the injected client's own time limit cuts off is reported as sent without an answer within the time limit (it may have taken effect), in the same sentence the installed-target path uses when its limit is reached, while a connection that cannot be made is still reported as unreachable and one that drops after the decision went out is reported as possibly applied (the same no-answer judgement the background-agent verbs use); a client built with the documented decision budget answers a slow approval exactly as the installed target does, and a client whose namespaces are callable functions is accepted as before. The package's own reads through an injected client — the two probes and the post-decide re-pull — settle at their deadlines even while the client sleeps through a retry back-off, and abort the request underneath; a malformed injected connection is reported as not sent, never thrown. |
443
- | `scripts/run-session-policy-refused-removal-test.mjs` | The way out for a session that already carries a tool name the engine refuses. The engine refuses to start a run when any rule applied to it names a retired tool, or a name containing "__" without a protocol prefix, so a session whose own rule record holds such a name fails at startup on every run. The guard pins three pieces. First, the refusal test itself: it is compared name by name, in both directions, with an independent reading of the installed engine's own tables (retired names and protocol prefixes) over a corpus that includes padded, wildcard and parenthesised forms, and it is shown to be wider than the verdict used before a write — a padded or wildcard form is withheld there for another reason yet still refuses the run, so a removal driven by that verdict would leave the session broken. Every corpus name is then written alone into the deny list and into the allow list of a real engine run: the test says "refused" exactly when the run fails at startup with the refusal code and the model is never called, while the same shapes in the command lists are not audited at all. Second, the removal verb: it takes only the refused entries out of those two lists and leaves every other entry, list and ordering byte-for-byte, keeping an emptied allow list as a present empty list; after it runs, the same session's next real engine run gets past startup, for every refused name in the corpus and in both lists. Against the installed engine as an ordinary caller, taking a deny entry out is refused as a loosening — reported as such, written exactly once, the record unchanged and the next run still refused, never a false success — while an allow-list-only removal, which narrows, succeeds; against a model of the newer contract the engine has announced, the removal succeeds, and the same model still refuses a removal of anything else. A concurrent writer causes exactly one re-read, judged again on the other writer's record, and a second collision is reported rather than retried; read failures, coded write refusals and unconfirmed writes (no verdict, a receipt that cannot be read, or a receipt that does not account for the removal, including one that still holds a removed entry) each land in their own arm, and running the verb again after an unconfirmed write reports nothing left to remove. Third, the wording: one sentence per outcome; the loosening refusal names operators and says the record is unchanged, the unconfirmed one says the change may already be in effect, and none carries a rule string or engine text. The startup-failure sentence answers to that one code only, lists every place the entry can be set without naming who may remove it, and a real run with the name in the caller's own policy rather than in the session record yields the same code — which is why the sentence may not claim the entry is in the session record. The verb also requires the tool roster reported by a run on the deployment: a name listed there, as a tool name or an alias and compared exactly as the engine compares it, is left in place even when it is a retired name, because the engine accepts that rule — with a real engine run that mounts a deployment tool under a retired name (or with that name as an alias), the removal keeps the deny rule and the next run still starts. Without a roster, or with one that cannot be read, nothing is read or written and the outcome says why; a readable empty roster is still a roster. The removal sentence speaks separately about names taken out of the deny list and out of the allow list. |
443
+ | `scripts/run-session-policy-refused-removal-test.mjs` | The way out for a session that already carries a tool name the engine refuses. The engine refuses to start a run when any rule applied to it names a tool its own tool-name check does not call current — a retired tool, a name containing "__" without a protocol prefix, or (engines from 7.48.0) a protocol name whose server or peer segment it never mints — so a session whose own rule record holds such a name fails at startup on every run. The guard pins three pieces. First, the refusal test itself: since 0.97.0 it is the engine's own tool-name check read through `ruleToolNameVerdictOf`, compared name by name, in both directions, with that check called directly over a corpus that includes padded, wildcard and parenthesised forms, and it is shown to be wider than the verdict used before a write — a padded or wildcard form is withheld there for another reason yet still refuses the run, so a removal driven by that verdict would leave the session broken. Every corpus name is then written alone into the deny list and into the allow list of a real engine run: the test says "refused" exactly when the run fails at startup with the refusal code and the model is never called, while the same shapes in the command lists are not audited at all. Second, the removal verb: it takes only the refused entries out of those two lists and leaves every other entry, list and ordering byte-for-byte, keeping an emptied allow list as a present empty list; after it runs, the same session's next real engine run gets past startup, for every refused name in the corpus and in both lists. Against the installed engine as an ordinary caller, taking a deny entry out is refused as a loosening — reported as such, written exactly once, the record unchanged and the next run still refused, never a false success — while an allow-list-only removal, which narrows, succeeds; against a model of the newer contract the engine has announced, the removal succeeds, and the same model still refuses a removal of anything else. A concurrent writer causes exactly one re-read, judged again on the other writer's record, and a second collision is reported rather than retried; read failures, coded write refusals and unconfirmed writes (no verdict, a receipt that cannot be read, or a receipt that does not account for the removal, including one that still holds a removed entry) each land in their own arm, and running the verb again after an unconfirmed write reports nothing left to remove. Third, the wording: one sentence per outcome; the loosening refusal names operators and says the record is unchanged, the unconfirmed one says the change may already be in effect, and none carries a rule string or engine text. The startup-failure sentence answers to that one code only, lists every place the entry can be set without naming who may remove it, and a real run with the name in the caller's own policy rather than in the session record yields the same code — which is why the sentence may not claim the entry is in the session record. The verb also requires the tool roster reported by a run on the deployment: a name listed there, as a tool name or an alias and compared exactly as the engine compares it, is left in place even when it is a retired name, because the engine accepts that rule — with a real engine run that mounts a deployment tool under a retired name (or with that name as an alias), the removal keeps the deny rule and the next run still starts. Without a roster, or with one that cannot be read, nothing is read or written and the outcome says why; a readable empty roster is still a roster. The removal sentence speaks separately about names taken out of the deny list and out of the allow list. |
444
444
  | `scripts/run-fixture-flat-done-ratchet-test.mjs` | Test fixtures that feed the engine's terminal `done` frame are kept in the shape the live engine actually sends. Since the terminal record became a tagged cause (`result.terminal`), the older flat shape (`result.status` plus loose keys) reaches a client only in two ways: a stored row replayed verbatim from before the upgrade, and the server's own refusal envelope — so a fixture written in the flat shape tests the replay path while claiming to test the live one. The suite finds every `{ type: 'done', result }` literal under `scripts/`, follows `result` to the object literal it really comes from (in place, through a variable, through a helper's parameter at each of its call sites, through a spread, or through the rows of an iterated array) and counts the flat ones that carry no one-line note saying they model a replayed row or a refusal envelope. That count may only go down: it is a ceiling kept in the ratchet registry, and a count under the ceiling prints a step-down line instead of passing in silence. A planted corpus with a known number of flat, noted and cause-shaped frames in each of those forms must be counted exactly, and a note that sits inside a string or gives no reason does not count |
445
445
  | `scripts/run-authority-envelope-mirror-test.mjs` | The authority-envelope tags that a peer's message body is defused against before it is written into a transcript line. Tags the engine uses to speak with its own authority (reminders, completion notices and the like) must never survive inside a body a peer wrote, or a forged completion notice could be read back on resume as a real one and poison the dedup ledger. The package's list is generated at build time from the engine package's public entry point, and the guard checks that the source binds that generated list rather than a hand-written copy and that it matches the installed engine package in both directions: a tag the engine treats as authority and the package lacks is red, and so is a tag the package defuses that the engine does not, since that rewrites ordinary text in a peer's message. It also confirms the published list is the engine module's own value and is still derived from the engine's envelope registry, which it reads from the engine's module by path because the registry itself is not published. At the rendered output, every engine authority tag placed in a body (opening, closing, with attributes, upper case) comes out defused on all three peer lanes, and the engine's non-authority envelope tags come out untouched |
446
446
  | `scripts/run-upstream-tables-test.mjs` | Engine facts the package may not import at run time (the notice code register and its audience table, the reasons an injected MCP server is dropped, the protocol namespace prefixes and the retired tool names with their current names) are generated at build time from the installed engine package's own public entry point into plain literal modules, which are committed. The generated files must be byte-identical to what the installed engine produces today, no other file may sit beside them, each names the generator and the engine version it came from, none imports anything, and the package's public names are the generated objects themselves. The generator refuses rather than guesses: a table that is missing or empty, a notice code without an audience row (or the reverse), a third audience value, or a retirement note in a shape it does not know all stop generation instead of producing an empty or partial table, and it reads only the engine package installed in this package's own dependency folder. Two word lists the SDK already exports as values (who asked a question, and which word a mandated approval stands on) are now the SDK's own arrays rather than copies; because those arrays are not frozen, the package's own decisions are shown not to change when they are modified in place. Inside the package, the thinking-level and permission-mode vocabularies, the tier words, the rejection sentence and the cancelled code each exist exactly once in the source. A second batch covers engine fact tables that newer engine packages publish from their entry point (the notice cause and source lists, the run-ending reasons, the reserved control-verb names, the workflow size caps, the authority envelope tags, the denial layers, the reasons a requested permission mode did not take effect, and the structured tool-card types): each is generated the same way and bound where it is used, no hand-written copy of any generated table may remain in the source, and an engine package too old to publish them makes the generator stop and name every missing export rather than fall back to reading private files. A third batch covers the closed set of codes the engine refuses a declared caller tool with: it is generated the same way and folded into the package's configuration-refusal recognition set, which must contain every one of those codes; the set only recognises them (no code path branches on them), an unexpected code shape stops generation, and an engine package that does not yet publish the set makes the generator stop and name it. A fourth batch (from 0.88.0) covers the closed set of reasons a permission mode's release of a question may carry: it is generated the same way, the package's public word list is that generated object, no hand-written copy of its words remains in the source, and an engine package that does not yet publish it makes the generator stop and name it. A sixth batch (from 0.90.0) covers the table of tool-name aliases that hook matchers resolve (the alias spelling of a built-in tool to its canonical name): the engine publishes no ready-made table, so the generator expands the aliases declared on each row of the engine's static tool catalog, and the package reads the result through two public functions (the table itself is not exported). Those functions must agree name by name with the engine's own name resolution, leave every name outside the table untouched (empty strings and inherited object keys included), never fold case or whitespace, and never throw. The generator stops rather than writing an empty table when the catalog declares no alias at all, when a catalog row has an unexpected shape, when an alias collides with another row's canonical name, or when an alias is an integer-like string (object keys of that shape would be reordered), and a run of it against a catalog without aliases exits non-zero and leaves every generated file byte-identical; the generated table is built from key-value pairs and is executed in the check against the engine's own resolver, so an alias spelled like an inherited object key still lands as an own entry The generator also produces five closed sets that the engine exports from its package root — the reasons a rule's evidence can be absent in an approval question, the lanes of the system injection queue, the full list of built-in read-deny tiers, the tiers active by default, and the per-class arbitration switches — as frozen literal tables that the package's own root entry hands out as the very same objects. The check compares them item by item and in order with the engine (the arbitration rows bit by bit, with the key set compared to the engine's own list of ask classes, and the tier lists with the engine's own tier resolver), refuses to generate when a name is missing, a list is empty or has duplicates or non-strings, a default tier is not in the full list, or an arbitration row has a missing, extra or non-boolean switch, and a run of the generator against an engine without one of them exits non-zero and leaves every generated file byte-identical. The reader that answers whether a standing answer may settle an ask reads only the generated arbitration table: its answers follow a table with one switch flipped, a class added or a class removed, the source holds no class name literal, and it treats a throwing getter, a prototype-only field or an unknown class as unknown rather than as permission. The engine code string for a self-contradictory request body appears as a string literal exactly once in the source; the three refusal readers that use it keep their own criteria. From 0.92.0 two more batches come from the engine's package root — four closed sets that stay inside the build, and nine facts the package used to copy by hand (settlement words, classifier refusal causes, the auto-mode unavailable causes, tier order and the familiar tier aliases, the background agent handle shape, the sender permission classes, MCP liveness words and the three rule behaviors) — and a third source is read: the settings contract package, from which the engine-owned hook events, the hook events whose matcher reads a tool name, the known-secret literal shape, the tier group name shape, the budget keys grouped by value type and the model entry interface words are generated into their own file. Each former copy gets a check that the public name is the generated object itself, or, for module-private constants, that the initialiser is the imported generated identifier; regex and alias-table copies are scanned for alongside array copies; and malformed upstream shapes, extra members and missing names each make the generator refuse. Two numeric limits are generated as well (the largest inline image in base64 characters, and the longest follow-up text): the same value as the engine's export, never zero, absent or malformed, not on the root entry, read by the human-submission assembly through the generated constants with no hand-written copy left there. The closed set of causes by which a deployment hook can keep a prompt from being submitted is generated as well: the same words in the same order as the engine's export, frozen, a literal tuple type, exported at the root as the generated object itself, agreed with by the engine's own membership test, and with no copy of its words in the reader that uses it; an extra member changes the generated output, while a malformed shape or a missing name makes the generator refuse. Since 0.94.0 the generator also derives the request-side permission-mode words and the user-input lane table (lane to notice code and count key); the package-side fold of the arbitration table takes an optional question face, reads the releasable classes per face from the generated ask-face tables only, and is diffed against the engine's own fold on every face. From 0.94.0 the settings-contract batch also generates the private-key header definition, which must be unanchored and judges the same samples as the contract's own check; a missing, anchored or stateful definition makes the generator refuse, and the display module's header constant is the generated object itself. Since 0.94.0 it also generates the ask-face tables (the face words, the tool names on the shell face derived from the engine's own face function over its catalog, and the releasable classes per face), checks them against the engine item by item and against the arbitration table, and refuses to generate on every malformed shape or missing export. From 0.95.0 a settings-contract function batch copies three pure functions from the contract package's entry point (the narrow loopback check used before sending no credentials, the hierarchical URL check, and the @-mention narrowing) by their own source text: the generator refuses when a function reaches for any name outside itself except an allowed global, the copies must answer exactly like the originals on independent samples, and the package's readers use the copies instead of their own versions. The configuration-refusal registry has no read point inside the package (the only reading of "is this a configuration refusal" is the prefix predicate), and its own comment says so. Since 0.96.0 the approval mandate word list is generated from the engine package root too, followed by the one word older supported engines still send. Since 0.96.0 the settings contract's hook-matcher reading (`hookMatcherSelects` / `hookMatcherForm`, with the two private declarations they depend on copied verbatim from the installed module) is generated the same way and must answer exactly as the installed contract on an independent sample set; generation is refused when the module touches those private declarations anywhere else (modified at load time, reassigned, or exported), because then the copied text would not be what runs upstream. |
@@ -458,8 +458,8 @@ guard still cross-checks the table by name).
458
458
  | `scripts/run-submit-refusal-projection-test.mjs` | Four kinds of refusal that mean the request itself needs fixing, read by code with one sentence each. Permission rule lists that are too long or hold a bad entry are named precisely (which list, how many entries against the limit, or which entry by position) for a new request and, with different wording, when parked work cannot continue with the request stored for it; the same sentence is appended to a decision that fails for that reason. A tool declaration the engine refuses at the start of a run is read by its code rather than its HTTP status, so a status that looks like a temporary outage is not presented as one: the sentence says the same request will be refused the same way. A deployment refusing a single-user-only setting is attributed from the request the caller sent (the whole-environment shell setting, bypassPermissions, both, or unknown) and always says nothing ran and the local settings are not at fault. A body-shape refusal exposes its two lists of unrecognized and unsupported keys exactly as sent (only well-formed lists are read; a missing list is not treated as empty), with a three-way check of whether any listed key is a settings path. |
459
459
  | `scripts/run-mandated-card-values-test.mjs` | Approval cards that newer servers mark as mandatory for four kinds of question (a deployment command policy, an operator's never-auto list, an exhausted durable budget, and supervisor routing) are built with the server's own code and fed through both card paths: the card carries the mandatory mark and the matching reason word, the shared check reports it as mandatory, no mandate word is invented, and a session-wide allow that the server declines to remember produces the single not-remembered notice. An ordinary approval-list card stays non-mandatory and remembered, and the older card shapes are shown to read the other way. This check needs an installed server package to run and reports itself as skipped otherwise. |
460
460
  | `scripts/run-park-failed-reason-projection-test.mjs` | The fail-soft true cause when a parked approval cannot be decided and the engine's own `done{suspended}` terminal is the one handed back: that terminal is still returned as-is (no second terminal), but it carries `_sema_park_failed_reason` — the same sentence, byte for byte, that the durable leg puts on its synthesized terminal — and the result frame carries it through with credentials washed the same way `errors[]` is (value only; same sentence as the durable leg after washing). The key is left off when this host has no decision surface at all (no approval card port, a port installed under another session key, or no question overlay): that sentence's way out is to decide on the card, which such a host never shows. Pinned with a policy-fold refusal on decide, both stall exits, a host-retracted card, credential samples through the full bridge on both legs, the no-decision-surface forms, and the absent forms (user interrupt, malformed values, ordinary terminals). |
461
- | `scripts/run-tool-history-mismatch-projection-test.mjs` | When a provider rejects every request of a session because a tool call in the conversation history no longer pairs up with its result, the terminal error row carries a machine-readable key naming the kind of mismatch and the first tool call id the provider named; a host that offers the rewind command gets the same recovery sentence CC shows (unless that host names the engine and the engine reports it cannot rewind the conversation), plus a count from the second time in a row, while the print and utility lanes, hosts without that command, and the result frame's error text stay unchanged. Only the run's own failure is read, never assistant text, and only when the provider sentence opens the provider's message: other statuses, other 400s and quoted copies do not match. The count is per session and per run, ignores replays, resets on any other outcome and never overstates when it cannot tell; the row-resolving helper that finds the prompt to go back before is checked for the target, the first-turn case and every undecidable case. The error carrier the package mints for a terminal is the same one the failure line is judged on, so handing it to the edit-and-resend reading makes that reading yield exactly when the line carries the mismatch key, across thirty-one terminal shapes including accessor and inherited fields, governance and busy endings among them; the failure line and the carrier are derived from one reading of the frame, so a field whose answer changes between reads cannot make the line and its key disagree, and an empty error sentence yields neither a line nor a carrier. The key also records whether that row offered the recovery sentence, so a host reads the fact instead of re-deciding it later, and the key reader itself is exported and checked from the package entry. The sentence naming the broken turn, which a host appends once it has resolved that turn, is checked for its two exact forms, cleaning before cutting at sixty code points, linear tag stripping, and byte equality with the shipped terminal wording. On any other provider rejection the same terminal row ends with the edit-and-resend sentence and carries that reading as a key: never together with the mismatch key, never on governance, busy or model-output rows, and every row without one of these changes is byte-identical to the previous release. The check also feeds outcomes built and published by an installed server package when one is available and reports that part as skipped otherwise. The arm of the synthesized terminal row is decided in one function that both the row text and the error carrier read, and its order is pinned: a lock or governance terminal keeps its row even when the error text starts with the model-output sentence. |
462
- | `scripts/run-display-untrusted-baseline-diff-test.mjs` | The credential side of the display outlet (`displayUntrusted` and `washCredentials`), diffed byte for byte against the previous published release: five input pools (seeded random strings, exhaustive shapes around labels, quoted values, userinfo, schemes and invisible units, and URL parameters separated by `;` or `,`) run through fifteen outlets. A secret the previous release masked must stay masked wherever it sits in a credential position; the outlet with only the URL net enabled is judged within that net's own reach. Clean lines from the package's own documents pass through unchanged, and every outlet is idempotent (masking the output again changes nothing). The previous release tarball is supplied through `SEMA_DU_BASELINE_TGZ`; without it the diff sections report as skipped and the run counts as partial, not green. The minimal-shape pool also carries scheme-less userinfo whose password holds a quote or angle bracket after a credential label, and the shapes that once washed differently on a second pass. A full-product grammar pool of 64,512 strings joins the idempotence check, and every pool is also checked for the credential stage reaching its pass cap (it must never do so). The pools now include the bare `token` scheme-word forms and a full `token` grammar product, so the new wash is held to the same idempotence and no-regression bars against the previous release. They also include label and `bearer` / `basic` values that run into a `token` scheme word, a non-ASCII space and an address, with or without a leading character, and quoted values where `token` and an address follow `bearer`. A YAML block-scalar pool (block headers with every chomping and indentation indicator, comments, raw and JSON-escaped line breaks, non-ASCII and tab indentation, invisible characters at line starts, credentials, control strings and bidi characters inside the block, and seven kinds of line after the block) joins all checks: no secret inside a block may show on any outlet that runs the word net, the previous release must show some on the same pool, and the non-block forms in that pool must stay byte for byte as in the previous release. A sixth pool covers lists and YAML node properties after a credential label: block, flow and comma lists plus tag and anchor prefixes, alongside look-alike non-list forms. The secrets in the list pool must stay masked on every outlet, the look-alike forms must read byte for byte as in the previous release, and the three forms whose reading changed on purpose (a lone dash after the colon, a value after a dash, a block sequence of plain key names) are pinned as registered divergences. A seventh pool covers the 0.95.0 credential-net shapes (bracketed values after scheme words and labels, a JSON-escaped tab before the value, unlabelled random-looking tokens built inside the shape rule, associative-array arrows, a label followed by a `token` scheme word and an address, and known prefixes glued after an underscore or hyphen) alongside normal long strings and placeholders: the secrets must stay masked on every outlet while the previous release shows some, the normal strings must read byte for byte as in the previous release, eleven readings that changed on purpose are pinned as registered divergences, and the share of purely random tokens the shape rule catches is read at four lengths with a floor of 0.9. A nested-shape pool pins that the three new credential readings only add masks: a value that the previous release masked whole must stay masked whole when it contains one of the new shapes. |
461
+ | `scripts/run-tool-history-mismatch-projection-test.mjs` | When a provider rejects every request of a session because a tool call in the conversation history no longer pairs up with its result, the terminal error row carries a machine-readable key naming the kind of mismatch and the first tool call id the provider named; a host that offers the rewind command gets the same recovery sentence CC shows (unless that host names the engine and the engine reports it cannot rewind the conversation), plus a count from the second time in a row, while the print and utility lanes, hosts without that command, and the result frame's error text stay unchanged. Only the run's own failure is read, never assistant text, and only when the provider sentence opens the provider's message: other statuses, other 400s and quoted copies do not match. The count is per session and per run, ignores replays, resets on any other outcome and never overstates when it cannot tell; the row-resolving helper that finds the prompt to go back before is checked for the target, the first-turn case and every undecidable case. The error carrier the package mints for a terminal is the same one the failure line is judged on, so handing it to the edit-and-resend reading makes that reading yield exactly when the line carries the mismatch key, across thirty-one terminal shapes including accessor and inherited fields, governance and busy endings among them; the failure line and the carrier are derived from one reading of the frame, so a field whose answer changes between reads cannot make the line and its key disagree, and an empty error sentence yields neither a line nor a carrier. The key also records whether that row offered the recovery sentence, so a host reads the fact instead of re-deciding it later, and the key reader itself is exported and checked from the package entry. The sentence naming the broken turn, which a host appends once it has resolved that turn, is checked for its two exact forms, cleaning before cutting at sixty code points, linear tag stripping, and byte equality with the shipped terminal wording. On any other provider rejection the same terminal row ends with the edit-and-resend sentence and carries that reading as a key: never together with the mismatch key, never on governance, busy or model-output rows, and every row without one of these changes is byte-identical to the previous release. The check also feeds outcomes built and published by an installed server package when one is available and reports that part as skipped otherwise. The arm of the synthesized terminal row is decided in one function that both the row text and the error carrier read, and its order is pinned: a lock or governance terminal keeps its row even when the error text starts with the model-output sentence. The preview's code-point cut never splits a redaction marker. |
462
+ | `scripts/run-display-untrusted-baseline-diff-test.mjs` | The credential side of the display outlet (`displayUntrusted` and `washCredentials`), diffed byte for byte against the previous published release: five input pools (seeded random strings, exhaustive shapes around labels, quoted values, userinfo, schemes and invisible units, and URL parameters separated by `;` or `,`) run through fifteen outlets. A secret the previous release masked must stay masked wherever it sits in a credential position; the outlet with only the URL net enabled is judged within that net's own reach. Clean lines from the package's own documents pass through unchanged, and every outlet is idempotent (masking the output again changes nothing). The previous release tarball is supplied through `SEMA_DU_BASELINE_TGZ`; without it the diff sections report as skipped and the run counts as partial, not green. The minimal-shape pool also carries scheme-less userinfo whose password holds a quote or angle bracket after a credential label, and the shapes that once washed differently on a second pass. A full-product grammar pool of 64,512 strings joins the idempotence check, and every pool is also checked for the credential stage reaching its pass cap (it must never do so). The pools now include the bare `token` scheme-word forms and a full `token` grammar product, so the new wash is held to the same idempotence and no-regression bars against the previous release. They also include label and `bearer` / `basic` values that run into a `token` scheme word, a non-ASCII space and an address, with or without a leading character, and quoted values where `token` and an address follow `bearer`. A YAML block-scalar pool (block headers with every chomping and indentation indicator, comments, raw and JSON-escaped line breaks, non-ASCII and tab indentation, invisible characters at line starts, credentials, control strings and bidi characters inside the block, and seven kinds of line after the block) joins all checks: no secret inside a block may show on any outlet that runs the word net, the previous release must show some on the same pool, and the non-block forms in that pool must stay byte for byte as in the previous release. A sixth pool covers lists and YAML node properties after a credential label: block, flow and comma lists plus tag and anchor prefixes, alongside look-alike non-list forms. The secrets in the list pool must stay masked on every outlet, the look-alike forms must read byte for byte as in the previous release, and the three forms whose reading changed on purpose (a lone dash after the colon, a value after a dash, a block sequence of plain key names) are pinned as registered divergences. A seventh pool covers the 0.95.0 credential-net shapes (bracketed values after scheme words and labels, a JSON-escaped tab before the value, unlabelled random-looking tokens built inside the shape rule, associative-array arrows, a label followed by a `token` scheme word and an address, and known prefixes glued after an underscore or hyphen) alongside normal long strings and placeholders: the secrets must stay masked on every outlet while the previous release shows some, the normal strings must read byte for byte as in the previous release, eleven readings that changed on purpose are pinned as registered divergences, and the share of purely random tokens the shape rule catches is read at four lengths with a floor of 0.9. A nested-shape pool pins that the three new credential readings only add masks: a value that the previous release masked whole must stay masked whole when it contains one of the new shapes. At the capped exit, differences against the previous release are accepted only when the old output ended in a truncated redaction marker and the new output is the old one without that fragment. |
463
463
  | `scripts/run-registry-conflict-projection-test.mjs` | How a client tells the two meanings of a registry write conflict apart. The registry answers the same 409 when a write merely lost an optimistic-lock race (re-read, re-base, write once more) and when the space being written was deleted, or deleted and re-created under the same name, while the write was in flight (nothing was written, and a re-read would return the replacement space, so a retry would put an old draft into a space nobody reviewed). The package ships the same three-word classification the newer SDK uses under the same name, plus a predicate for caught errors, because the SDK version this package supports does not have them yet. The guard checks every status and body combination against the newer SDK's own function when that SDK is available, checks the predicate against the newer SDK's own error classes, reads every 409 the registry service mints from its source when that tree is available and replays the resulting bodies, and checks that unreadable bodies never throw and never classify as the retryable kind. The parts that need the newer SDK or the registry source report as skipped when those are absent. |
464
464
  | `scripts/run-prompt-delivery-projection-test.mjs` | Whether a user message actually reached the engine, judged once for every client. A ledger records, per message id and per submitted batch, that a submission entered the submit path and whether the engine created a run for it: a stream frame from the new run counts, a later accepted steer into the running run counts, while the session-busy refusal does not, and neither do the frames a server writes before creating the run (the stream-head `meta` frame and replays of approval cards still pending from earlier runs), which arrive ahead of that refusal. Both sets only grow, so the answers do not depend on arrival order, replays or repeats; a message never seen on the submit path is reported as unknown rather than undelivered. Two row-level readers filter side-channel payloads (only messages known to be undelivered are removed, everything else is kept in order). Records serialize one per line in the shape hosts already persist. The check also replays the exact frames an installed server package writes for a busy session through the real SDK stream parser when one is available and reports that part as skipped otherwise. The idempotent-replay form of the busy-session refusal (the raw conflict body without a status, sent when a host reuses an idempotency key while the first submission is still in flight) does not count as delivered either. Since 0.90.0 the check also feeds the delivery frames a steer now produces (a queued frame and an applied frame on one input id, which equals the message id on the receipt): the first records delivery, the second changes nothing, replays and reversed order give the same records, and the answer does not depend on whether the input id equals the message id, differs from it, or is absent. The ledger object itself also carries the two row-level readers as methods bound to it, `knownUndeliveredMessage(message)` and `withoutUndelivered(messages)`, which answer exactly what the two free functions answer for that ledger on every input shape: a bare string is not a message row, so it is judged false, and that is why the row-level method does not reuse the name of the id-keyed `knownUndelivered(uuid)`. They keep answering when taken off the ledger, when the ledger is copied, and after it is frozen, they read the ledger as it is now, and a ledger rebuilt from stored records carries them too. The check also compiles the installed declaration file against positive and negative type examples, including a hand-written nine-member ledger stand-in that must still be accepted by the two free functions (their ledger parameter only needs the id-keyed reader). Since 0.91.0 every delivered record the ledger writes names its source (`by`: a frame from the new run, or the accepted steer receipt) and may carry a share (`full` or `partial`, absent meaning full); a delivered record without a source, as written by earlier versions, is read as unknown rather than delivered, a record whose extension fields cannot be read falls back to that unknown form and is stored without them, and keys the reader does not know are dropped and counted. Merging takes the highest of unknown, partial and full for each message, whatever the order: the check replays 2400 random sequences in original, shuffled, duplicated, split-and-merged and persisted forms against an independent model, and feeds records written by this version to the previous release's reader, which must give the same answers. A submission can also carry the engine generation it was sent on; the ledger then lists, once per generation, the messages submitted there that are still known undelivered, without changing the recorded facts, and a malformed generation is ignored while the submission itself is still recorded. Revoked proxies passed as batches, outcomes, record lists or message lists are treated as unreadable instead of throwing. Both write ports also take the two tiers of an encoded submission, `{ full, partial }` as returned by `deliveredUuidsOf`: the partial tier is recorded with `share: 'partial'`, so a message that was only partly sent is never read back as known undelivered, and an unreadable batch records nothing without throwing. On read-back, once a record's `u` and `s` are read, a throwing extension field or key enumeration keeps the record as legacy instead of dropping it. |
465
465
  | `scripts/run-session-memory-erase-projection-test.mjs` | The session owner's memory erase (`eraseSessionMemory`, with the pure judge `sessionMemoryEraseOutcome`): one call sends exactly one request to erase what a session contributed to memory, with a body of exactly the request id plus the unevidenced-erase consent only when the caller gives it as a boolean (never added when absent, and no other key the caller passes reaches the wire); a dot-segment session id is refused locally. The connection takes the same configuration as the engine client factory, and the outbound headers are compared byte for byte with a client that factory builds for every credential form (string, per-request value function, a value function that returns nothing or throws, empty, same-origin relay, loopback without a token, blank or missing principal); a token-bearing connection in a browser host sends nothing. Malformed arguments, options and connections never throw and send nothing. A request that may have reached the engine without a readable answer (time limit, a connection dropped after the request went out or while the body was read, a cancellation after sending, an unreadable response) is reported as no answer, while a connection that never opened, a cancellation before sending or a synchronous fetch failure is reported as not sent. The judge sorts each answer into erased, partly erased, refused (nothing was erased: not sent, or a named engine code issued before any change) or not confirmed (no answer, an unreadable receipt or one for a different request or session, a refusal with two possible meanings, an unrecognised code, or a non-success status without the engine's own error code; a code found only on the prototype chain does not count), by machine code before HTTP status, with one fixed sentence per outcome and a retry posture; a caller who gave the unevidenced consent always gets a person decides on the not confirmed outcomes. Receipts are read with the existing erase receipt reader. The closed lists are frozen, derived from the wording tables and equal to what the judge produces; sentences differ, refusals always say nothing was erased, and none copies a server sentence. The check also boots installed server packages of the current and the previous generation, when available, with their real dispatcher and handler and the engine's real file memory store, and drives the call against them: real receipts and replays, a request id reused on another session, a store without erase evidence with and without consent, unknown and foreign sessions, missing credentials, missing faces, malformed bodies and the coded refusals; the previous generation's untyped internal errors are judged not confirmed. The unevidenced-erase consent counts only when it is the caller's own property (an inherited one is never sent), a session id that cannot be encoded into the path is refused locally without a request and without a rejection, and a signal-shaped object that is not a real abort signal is refused as bad input. |
@@ -490,7 +490,7 @@ guard still cross-checks the table by name).
490
490
  | `scripts/run-approval-decided-by-capability-test.mjs` | The approval decision-attribution capability reader and the way both decision legs choose their key from it. The server's capabilities body carries a constant bit saying its three decision routes accept the `decidedBy` attribution key; the reader keeps three states (the literal `true` is present, a missing key is an older worker, anything else is dropped as unreadable and reads as not observed — a constant version bit has no meaningful `false`), ignores keys that exist only on the prototype, reads only own data properties (an accessor is never run and reads as unreadable) and never throws, and its stored reading is discarded automatically when the engine's capability cache changes generation. One judge, `approvalDecidedByAdmitted`, answers true only for a reading whose own data property `kind` is `present` and whose own data property `value` is `true` (accessors are never run, even if they return the right values). A single exported function, `decisionAttributionKeyOf`, decides the attribution key for both the durable decision leg and the live respond leg (and for any sender outside the package that needs the same answer), and a syntax-tree scan pins that it is declared once, called exactly twice inside the package and that no other place there builds a policy attribution literal (with planted violations proving the scan can fail): a policy refusal goes out as `decidedBy: 'policy'` only when the reading is present, as the old `settledBy: 'policy'` for every other reading (not reported, not observed, damaged, missing, unreadable, or none supplied), and with no attribution key at all for a human refusal, an absent or out-of-set word, an allow card, a failed card port or an abandoned card; the reading is judged once per decision before the first await. The direct-door refusal (its code changed name with the key, both codes read as the same arm) is answered with exactly one resend of the same body without the attribution key, whichever of the two names it carried; a second refusal is not resent, a body that carried no key, a caller that has aborted, the retired-key refusal and every other 400 are not resent, and the classification of a later refusal follows the key actually carried by the last send. The reading supplied through the host lane reaches both legs (the live frame route and the durable park path); only an own data property of the lane counts (a prototype-polluted key, an inherited key or an accessor is treated as not supplied, and the accessor is never run), and a lane that throws when read counts as not supplied, and the bridge's whitelist copy matches each key against `'policy'`, reads only own data properties (a polluted prototype or an accessor never adds a key, including on the resend body) and writes only one of them |
491
491
  | `scripts/run-durable-decision-single-holder-test.mjs` | The single holder of a durable approval decision: for one approval identity (the call id of the fetched pending row, or the task id when the row has none), one decision (card, answer, decide including the package's own resends) is in flight per process, and any other caller that arrives while it is in flight, or that started reading the queue before it settled, gets a copy of that same outcome marked `joinedInFlight: true` without presenting a card, sending a decision or resending anything. Two concurrent callers produce exactly one card and one decide in both arrival orders, including the real park resolver racing a direct re-presentation of the same row (the stream finishes and both sides settle); a failed decide or a throwing card port gives both callers the same failure and clears the registration so the next call is a new decision; an abort signal from either caller interrupts that one decision: the holder's card is withdrawn and the holder alone sends exactly one interruption deny that both callers report, with identical readings whichever path holds, while an abort that arrives after the decision settled sends nothing and returns the settled outcome; different identities never join; the joining caller reports the holder's card through `onPresented` only when that card was actually handed to the card port (a throwing callback does not change its outcome) and never reports a decide operation; the copy never alters the holder's object and the flag is never written as `false`; and a syntax-tree scan pins that the joining and registration code paths contain no card, decide or notice call (with a planted violation proving the scan can fail) Since 0.94.0 the identity also carries the row instance (its creation time): when the engine parks the same call a second time for a different check, that second row is a new decision in both arrival orders (while the first card is still on screen, or when a read that started before the first decision settled returns the second row), and two callers fetching the same row instance still meet. |
492
492
  | `scripts/run-approval-respond-parking-test.mjs` | The live-card respond answer when the engine replies `202` with the reason `parking_in_progress`: the card's row is moving to parked, so the answer did not take effect. The outcome is `decision: 'unresolved'` plus a `parking` position carrying the approval id and, when the engine supplies them, the suggested retry interval and the decide path of the parked card; the resolution reader reports it on the existing unsettled arm with the new cause `parking_in_progress` (the union keeps its three arms; a retracted or edit-refused card still wins over it, and it wins over the decision word). The parking fact is carried only by an own data property naming the reason and a non-empty approval id that matches the card; a retry interval or decide path that is unusable drops only itself and the parking still stands. A body that names the parking reason but whose approval id is missing, unusable or belongs to another card is reported as not settled, never as decided and never as parked, with no notice and no resend. The answer is never re-sent, never has its attribution key dropped or renamed afterwards, and the package does not wait for the interval; no accompanying notice (for example the one saying the approval itself went through) and no attribution position is produced; exactly one user-facing sentence is shown (with the engine's suggested wait appended in seconds when given) and it contains no engine text. A 200 acknowledgement keeps its reading unchanged, bodies that are neither acknowledgements nor parking bodies are not read as parking, the durable decision path and its single holder have no parking cause, and a syntax-tree scan pins one reader declaration, one call site and the places where the reason word is minted, while the 202 body shape is pinned against every server generation in the fixture list. From 0.96.0 the base sentence is a public constant (`APPROVAL_PARKING_WARN_TEXT`, exactly the notice shown when the engine gives no wait hint, so a host that words its own parking notice no longer keeps a verbatim copy); the longer form with the wait hint and the notice function itself stay internal |
493
- | `scripts/run-decision-refusal-standing-test.mjs` | Shared decision-refusal reading for every client: `decisionRefusalAskReading` is the one reading of whether the question a refused decision was aimed at is already settled (settled, not settled, or unreadable), and settled comes only from positive evidence (a local no-pending stop, a 409 `conflict.ask_decided`, a `gone.ask` or a 410 other than a parked ask, a 404 whose body names an ending cause, or the matching error class when the code was stripped); a 404 without an ending cause, a not-found error class, a parked ask, the codes that only say the approval you looked at is no longer current, and wording in the failure text are not evidence. When the engine refuses an approval decision, `classifyDecisionRefusal` reports settled elsewhere exactly when that reading is settled, otherwise ambiguous (5xx except 501) or refused while the question may still be pending (every other 4xx and 501), returns nothing when no server verdict can be read, and marks transient refusals (408, 425, 429, 502, 503, 504, a body that says the request is retriable, or a 404 whose body says the id belongs to another replica); `liveAskPendingReading` reads whether the question is still in the engine's live pending list as pending, not pending or unknown (an unreported, malformed or partly dropped list is unknown); `decisionRefusalClosesOut` closes out only a refusal that is neither settled elsewhere nor transient and whose question the read confirms is still pending; `undeliverableDecisionContent` is the one sentence for each way the close-out ended. The guard covers the golden status/code table and near-miss codes, values that are not a verdict, guarded reads (throwing getters, prototype accessors, revoked proxies), the outcome note shape, codes unsafe for display, the full close-out truth table and the exact sentences, and that the code literal and the classifier exist in exactly one place; it also checks that the classifier, the interrupt-deny check and the park-resume check give the same settled answer across 6912 failure shapes (the park-resume check also re-reads the stream when the approval it looked at is no longer current), the two shapes that used to get different answers, and that all three read the one shared reading. |
493
+ | `scripts/run-decision-refusal-standing-test.mjs` | Shared decision-refusal reading for every client: `decisionRefusalAskReading` is the one reading of whether the question a refused decision was aimed at is already settled (settled, not settled, or unreadable), and settled comes only from positive evidence (a local no-pending stop, a 409 `conflict.ask_decided`, a `gone.ask` or a 410 other than a parked ask, a 404 whose body names an ending cause, or the matching error class when the code was stripped); a 404 without an ending cause, a not-found error class, a parked ask, the codes that only say the approval you looked at is no longer current, and wording in the failure text are not evidence. When the engine refuses an approval decision, `classifyDecisionRefusal` reports settled elsewhere exactly when that reading is settled, otherwise ambiguous (5xx except 501) or refused while the question may still be pending (every other 4xx and 501), returns nothing when no server verdict can be read, and marks transient refusals (408, 425, 429, 502, 503, 504, a body that says the request is retriable, or a 404 whose body says the id belongs to another replica); `liveAskPendingReading` reads whether the question is still in the engine's live pending list as pending, not pending or unknown (an unreported, malformed or partly dropped list is unknown); `decisionRefusalClosesOut` closes out only a refusal that is neither settled elsewhere nor transient and whose question the read confirms is still pending; `undeliverableDecisionContent` is the one sentence for each way the close-out ended. The guard covers the golden status/code table and near-miss codes, values that are not a verdict, guarded reads (throwing getters, prototype accessors, revoked proxies; the reading of the question itself takes every machine-readable slot of the failure from own data properties only: a value found only on the prototype chain is absent, and an own accessor makes the reading unreadable and is never run; the outcome note and the flattened outcome that also feed the reading take their status and error code from own data properties too, so a value on the prototype chain cannot be laundered into evidence), the outcome note shape, codes unsafe for display, the full close-out truth table and the exact sentences, and that the code literal and the classifier exist in exactly one place; it also checks that the classifier, the interrupt-deny check and the park-resume check give the same settled answer across 6912 failure shapes (the park-resume check also re-reads the stream when the approval it looked at is no longer current), the two shapes that used to get different answers, and that all three read the one shared reading. |
494
494
  | `scripts/run-no-allow-cause-test.mjs` | Why an automatic lane did not allow a call, and the refusal line for the two automatic-refusal lanes. `noAllowCauseOf(card, ladder)` combines the card's standing-rule verdict with the host's rule-ladder evidence (only an own, strictly `true` property counts): allowed or denied by a rule gives no cause; a local ask rule gives `ask_rule`; an exhausted automatic-approval budget gives `budget`; an ask rule the card names (its class or origin) gives `engine_ask_rule` when the ladder evidence shows that none of the user's own ask rules matched (the end-of-ladder no-match flag, or the dedicated flag for the case where an allow rule matched but the card's question class overruled it), and stays `ask_rule` when that evidence is absent, because the user's own rule cannot be ruled out; complete no-match evidence on a card a rule could clear gives `no_allow_rule`; everything else gives `other`, so no lane suggests adding an allow rule when that would not help. `policyDenyReason` mints the single line for the lane without an approval UI and for the dontAsk mode, one form per cause, naming only this one call and offering host-specific remedies (an allow-rule flag, a budget setting) only when the host supplies them. The guard covers the golden cases, pairwise ordering, strict reads, all ten sentences verbatim (the engine-side ask rule line says the user's settings cannot approve it and offers neither removing a rule nor adding an allow rule), unsafe tool names kept out of the sentence, and the single mint site. |
495
495
  | `scripts/run-approval-card-face-projection-test.mjs` | Three card positions added in 0.94.0. `boundGrants` (what approving a Skill card also grants: the skill name or null, the entries that then run without asking, and the entries refused for the rest of the run) is read once from the live approval frame through a strict reader; any malformed part drops the whole position, the copy is frozen, the digest is not put on the card, and the durable pending-row path never carries it; `approvalBoundGrantsDetail` gives one sentence with counts only, never the deployment text. `classifierNoRuling` (why the auto-mode classifier gave no ruling) is read by word shape as an open set from the live frame only; `classifierNoRulingDetail` has a phrase for each of the engine's words and a generic sentence otherwise. `priorCardsForCall` lists, for the same tool call, the earlier cards that asked about something else (a different class set, origin or mandate word), each with its approval id, origin, mandate word and, when answered here, whether it was allowed or denied; replays and parked twins of the same question do not count, both card paths share one bounded ledger, and `approvalPriorCardsDetail` says this is a separate check that needs its own answer. |
496
496
  | `scripts/run-hitl-words-readout-test.mjs` | The five wording and reader fixes of the HITL surface. (1) Seven user-facing failure sentences (pending-row fetch, approve and deny decisions on approval cards, the question-card decision, the parked-row wait, local rule parsing, background stop) carry the thrown value's own message and never the error class name — a thrown value with no usable message becomes one fixed sentence instead of a bare class name, `[object Object]` or `null`; the original thrown value goes only to the host log. (2) `resumeContextUnavailableContent` is a single English source (three ` · ` segments, with and without a run id, the retention seconds only when the engine gave them) and a plan-review decision that meets a 409 `conflict.resume_context_unavailable` says it. (3) The plan-review `permissionModeAfter` pre-check no longer mirrors the server's decision pairing: only the value set is checked locally, and the engine's `request.field_conflict` speaks for the rest. (4) `requestEngineCompact` reports how the fire settled through `onSettled` (accepted / refused / failed / offline), with a 409 `compact.not_running` also surfaced on the host surface. (5) Every exported `project*Capability` reads only own data descriptors: an accessor is never run and reads as `undefined`, and a throwing descriptor read is swallowed the same way. Pinned with throwing and class-named values at all seven sites, both plan-review entry points, the compact outcomes under 409 / 404-retry / 5xx / the 60 s injected deadline, and a per-projector throwing-proxy row plus a source scan. The decision exits treat hostile thrown values uniformly: a throwing message getter, a throwing toString, a revoked proxy, or throwing status, error-code or name getters all end in the thrown value's own message or the fixed fallback sentence, never in the getter's own error text, and the transient-failure classification reads each slot independently. |
@@ -509,7 +509,7 @@ guard still cross-checks the table by name).
509
509
  | `scripts/run-engine-env-sourcing-test.mjs` | The local-engine environment group sourcing judge (`resolveEngineEnvSourcing`, `engineEnvView`): when a settings source wrote half of an all-or-nothing group and that half is ignored, the group is either skipped (its own switches are dropped) or, for safety groups and groups declared not skippable, left for the engine to refuse with an explanation. Group declarations, the ignore rule and the alias table come from the host; the module holds no retired-name table and reads no platform state. The suite runs a seeded differential test against a frozen transcription of the previous host-side loop, fixed cases for alias precedence, source precedence and case folding, and scans the built module for every retired environment name in the settings contract. |
510
510
  | `scripts/run-models-dropped-entries-projection-test.mjs` | The live model catalog's dropped-entries reader (`droppedModelEntriesOf`): which catalog entries the engine did not accept and why, read from `GET /v1/models` `droppedEntries`. An absent key (an older server) reads as `not_reported` — never as an empty list — while a present empty array is a real "nothing was dropped". Rows keep the server's order; a malformed row (non-integer index, empty reason code, bad identity, accessor fields, sparse holes) is counted as unreadable without dropping its siblings; unknown reason codes and additive keys pass through unchanged. Only own data properties are read, accessors never run, a throwing read never escapes, and the result is frozen. The exported row type is derived from the SDK's `models.list()` return type, so it compiles on the peer floor SDK as well. |
511
511
  | `scripts/run-model-catalog-read-projection-test.mjs` | The single reader of the `GET /v1/models` body (`readLiveModelCatalog`) and the readers built on the catalog it returns. A row is keyed by its `name` (the value a request submits and the `@` handle); a malformed row is counted and dropped without losing its siblings, a later row repeating a name is counted, and the result is a frozen deep copy. The default model is the server's answer read from `defaultModel` only (`liveDefaultModelOf`): two rows sharing an upstream id resolve by `defaultModel.name`, a default missing from the catalog reads as `not_listed`, and the first row is never offered as the default. Per-row readers answer whether a model may be `@`-mentioned (`atMentionOf`: an absent key means the deployment has no mention list, not "cannot be mentioned") and what line-compatibility declaration it carries (`modelCompatOf`); the accepted thinking levels stay with `effortLevelsOf`, never re-derived from the compatibility block. `liveModelRowsOf` gives one frozen view per row with every field taken from those same readers; a context window is carried only when it is a positive finite number, with no lower floor. The desktop seat's `listModels` returns this same catalog and its outbound gate shares the row rule. |
512
- | `scripts/run-seat-carriers-test.mjs` | The desktop seat's request carriers and the park bit. One inbound judge for `sendMessage` (`checkSendMessageRequest` / `isSendMessageRequest`): a closed key set, each key's vocabulary taken from the package's own tuple (permission modes, reasoning efforts, rewind modes, turn settings) and the attachment cap; the two subagent keys are judged (boolean, or `true` / `{ ttlMs?, max? }` with non-negative integers); an unregistered key, an accessor or a throwing read is refused with a key path. The rewind carrier `{ mode, targetMessageUuid }` uses the same tuple as `rewindSpecForMode`, and the request keys it is projected to equal what that projector emits. `SEAT_TURN_SETTINGS_ORIGINS` records, for every settings sub-key of the request builder, whether the renderer carries it or the host supplies it; host-supplied keys are refused on the seat. Optional seat positions — verbs and carrier keys alike — are answered only by the seat capability table (`rewind` added). `tool_permission_resolved` may carry `parked: true` only when its outcome is `expired`, minted from the closing frame through `approvalCompleteParked`, the same reader the frame router uses. |
512
+ | `scripts/run-seat-carriers-test.mjs` | The desktop seat's request carriers and the park bit. One inbound judge for `sendMessage` (`checkSendMessageRequest` / `isSendMessageRequest`): a closed key set, each key's vocabulary taken from the package's own tuple (permission modes, reasoning efforts, rewind modes, turn settings) and the attachment cap; the two subagent keys are judged (boolean, or `true` / `{ ttlMs?, max? }` with non-negative integers); an unregistered key, an accessor or a throwing read is refused with a key path; so is a non-enumerable own key, whatever its value (a structured clone would drop it, so the gate does not let it pass unread). The request and its three nested closed shapes (`rewind`, `settings`, `retainSubagentSessions`) must be plain records, so `Date`, `Map`, `Set`, class-instance and custom-prototype carriers are refused. The rewind carrier `{ mode, targetMessageUuid }` uses the same tuple as `rewindSpecForMode`, and the request keys it is projected to equal what that projector emits. `SEAT_TURN_SETTINGS_ORIGINS` records, for every settings sub-key of the request builder, whether the renderer carries it or the host supplies it; host-supplied keys are refused on the seat. Optional seat positions — verbs and carrier keys alike — are answered only by the seat capability table (`rewind` added). `tool_permission_resolved` may carry `parked: true` only when its outcome is `expired`, minted from the closing frame through `approvalCompleteParked`, the same reader the frame router uses. |
513
513
  | `scripts/run-hooks-managed-policy-reading-test.mjs` | The optional `SettingsPort.managedPolicyReading()` that tells the hooks projection whether the managed policy layer could be read. Before it, only a read that threw counted as an unreadable governance layer, while a host's per-source reader usually does not throw: a managed file that cannot be read comes back as null and the projection treated that as no managed policy, so user, project, local and flag hooks went out to the engine exactly when an administrator's restriction had disappeared. The reading has three words. `absent` (readable, nothing configured) and `in-effect` (readable, has content) project exactly as before — the governance gates in the managed document still decide — while `unreadable` closes the whole leg: no hooks from any source and no session-level goal hook are sent, and enabled plugins are listed as excluded for an unreadable governance layer. The change only tightens: for the three readable answers (reader missing, `absent`, `in-effect`) the projection is byte-identical on the same managed document, and `absent` never overrides a document that says hooks are disabled. A reader that throws, or answers anything that is not one of the three words (including a different letter case, an empty object, or a property where a method is expected), counts as `unreadable`; only a missing member means the host did not wire the port, which keeps today's behaviour and logs nothing. |
514
514
  | `scripts/run-park-expiry-judgements-test.mjs` | The judgements the approval flow needs once a streamed approval's window has closed and the engine has moved the call into its durable queue instead of refusing it. `classifyApprovalCompleteFrame` reads a closing frame as one of three outcomes (moved to the queue, no queue evidence so retract the card, decided by a person) or none for anything it does not recognise; only a literal `true` counts as queue evidence, and a new outcome word is never mistaken for an expiry. `parkRowDeadlineState` reads a durable row's `deadline` into none, pending or past, treating a missing, null or absurd value as no deadline rather than clamping it. `lateDecideRoute` picks the endpoint for a late decision (the durable decide endpoint only when the window had closed and the row was actually found). `runReapedByDeadline` reads the run record and answers true only for the reaped-by-deadline error code; anything unreadable is false, so an unreadable record is never reported as a step that was not run. The guard carries the terminal client's offline cases over one by one and runs a frozen copy of that client's original implementations side by side over more than fifteen hundred inputs; the registered differences, both on the conservative side, are that a host client whose property read throws is answered with false instead of a rejection, and that a park bit sitting on the prototype chain or behind an accessor is not read as moved-to-the-queue (it is read through the same single reader the desktop seat uses, which only accepts own data properties). |
515
515
  | `scripts/run-usage-analytics-projection-test.mjs` | Reads of the engine's usage analytics (`GET /v1/usage/summary`, `/series` and `/breakdown`) and the card view built from them. Each reader never throws and answers ok, unavailable (the engine says it has no such face: no run store, no such route, or the feature is switched off) or failed (any other read failure, which is never reported as the backend lacking the feature). A missing number is simply absent from the view, never zero, and a total counts as exact only when the engine says `estimated: false`: otherwise it is an estimate and the view says so — a lower bound for money, an approximation for tokens, each with a shared prefix for the host to render. The card view has three states: available, estimated, and unavailable with one sentence for a backend that has no analytics and a different one for a read that failed; an unavailable card carries no numbers at all. The heat map and the active-day count exist only when the day series was both read and read completely, so a failed read, a series with unreadable points or invalid dates, or one the engine reports as truncated is never drawn as a grid of zero-activity days; the peak hour and the favourite model follow the same completeness rule, and a failed per-model breakdown is different from a window with no model usage. A point's timestamp is read once for both the day and the hour series: the same date-and-time grammar, a real calendar date, an explicit time zone for date-time forms (offsets are converted to UTC first), and a point that cannot be read is dropped and counted as unreadable in either series. The guard also runs a real SDK client against a stubbed fetch and checks the four requests it makes. |
@@ -528,10 +528,10 @@ guard still cross-checks the table by name).
528
528
  | `scripts/run-tool-result-content-projection-test.mjs` | How a tool's result body becomes the content of the tool result in the transcript. A result that carries images keeps them: each image whose data is a string and whose type is supported (JPEG, PNG, GIF or WebP) appears as an image block with exactly the bytes the engine sent (the data is not re-validated, matching the reference client), in order, in both the interactive and the print output, while the structured result next to it stays the same text as before. A block that cannot be shown (an image without string data or with an unsupported type, or a block of an unknown kind) is left out and the content ends with one notice saying that unsupported content was omitted, so nothing disappears silently. Plain text and text-only results are unchanged strings, a body the engine already shortened stays a string with the truncation note, and an error result's content is always text. The text readings of a result body (the flattening helpers and the subagent viewer's result slot) come from the same projection and agree with each other. That projection is also the one text reading behind the public `toolEndOutputText` (an empty block list reads as an empty string, an image as `[image]`, an object that is not a string or a block list as absent) and behind the classifier-denial and aborted-call checks, so a frame that carries an image or an unreadable block is never mistaken for one of those engine-minted bodies, and a classifier denial is recognised only when it arrives as a bare string or a single block (a signature split across blocks is not a denial). Whether a result is an error is decided once for both the interactive and the print output: a structured result reporting a failing exit code is an error in both, whatever the tool's name and even when the result carries no body. The task-output tool's content is the model-facing text in both outputs, with the structured task record kept beside it. |
529
529
  | `scripts/run-user-facing-sentence-readers-test.mjs` | Three user-facing sentences that used to follow a blunt rule and now follow a machine-readable field, pinned end to end. (A) A provider's refusal is classified into a closed set (`schema_invalid`, `auth`, `rate_limited`, `resend_eligible`, `unknown`) by HTTP status first and, on the 400 and 422 statuses only, by an exact-match table of the provider's own error words that name a malformed request body; such a refusal no longer receives the "change it and send it again" sentence, every other refusal keeps its previous sentence byte for byte, the provider's word never enters a sentence, and the class rides on the synthetic terminal row's key. The key is relayed only when it re-derives from its own status and sentence: a key from an earlier release that carries no class is still relayed with the class derived from its status alone, so replaying an old transcript keeps its sentence, while a class that is present but not in the closed set, does not fit the status, or does not match the sentence drops the key whole. The classifier stays inside the package; only the closed set is exported, and the gate also checks that the word table exists in exactly one source file. (B) The sandbox degradation hint recognises the engine's 501 `capability.image_index_required` by status plus exact error code, never by the sentence, and does so for the SDK's real error classes too. (C) The model-setup decision takes an optional third host flag saying whether the client can configure the route: a route gap then reads as offer-route-setup, explain-not-configurable or, with the flag absent, a neutral explanation, and the old claim that setup cannot help is gone from every sentence. Each cell asserts the rendered row text and key, the hint string or the decision sentence, and sentences for the unchanged cases are compared with the previous release byte for byte. |
530
530
  | `scripts/run-prompt-blocked-sentence-projection-test.mjs` | The sentence for a run whose first prompt was held back by a deployment's `userPromptSubmit` hook. The engine frames its own explanation for the model in an envelope that carries the session's reminder mark, and the terminal row used to print that envelope verbatim (`Run blocked: <system-reminder mark="…">…`) while the machine-readable cause beside it went unread. The package now has one sentence-maker for the `blocked` terminal, shared by the terminal row and the result frame's first error: a hook that refused the prompt reads `UserPromptSubmit operation blocked by hook:` followed by the hook's own words (treated as untrusted text — control and bidirectional characters shown as visible escapes, credentials washed, folded to one line, envelope tags removed even when spelled with zero-width characters); a hook that crashed, timed out, or was cancelled mid-check gets one fixed sentence each, saying the prompt was not submitted; an unrecognised cause word prints only the lead and never echoes the word. When the machine-readable cause is missing or malformed, the old `Run blocked: <reason>` line stays, with a whole engine envelope unwrapped so no tag or mark reaches the screen, and an agent's own free-text reason is byte-for-byte what it was. The new line counts as a governance stop (it is not an `API Error:`) and is recognised by the same row predicate as `Run stopped` / `Run blocked`. The suite also pins the single mint site in source, covers the interactive lane, and checks the tag remover is linear on near-miss input. |
531
- | `scripts/run-fleet-terminal-retain-test.mjs` | The fleet ledger's rule for terminal rows has one home and one function. A terminal row (completed, failed, killed) lives only in the retention pool and never in the active set, whichever way it arrives — a task frame or a reconnect snapshot — and both arms go through the same admission function. A row brought back by a reconnect snapshot therefore expires with the retention window exactly like one that arrived as a frame; before, the snapshot arm left it in the active set for good, and a lost removal frame did the same to a frame-delivered row, and clearing the pool could not clear it. The suite refutes the change on three axes: timing (snapshot before or after the terminal frame, interleaved with removal, repeated delivery, resurrection, a later snapshot that does not carry the row, a row being viewed, pool clearing, and every terminal word through both paths with identical answers), concurrency (twin rows of one task in either order, a notification-retired entry meeting a snapshot row, notifications against a snapshot-carried row, two sessions' ledgers), and failure class (a status outside the terminal vocabulary, absent, or null is never turned into a terminal row; rows without a usable id, with a non-string id, or null entries are dropped and counted, never thrown on). It also pins that the removal hook still reports a known row for a settled terminal row, because downstream bookkeeping credits a removal only when neither the stale mark nor the unknown-row mark is present. Finally it pins the terminal evidence the ledger keeps past the window (status word plus cycle, per task tail, bounded): after a row has aged out or the pool was cleared, a same-cycle completed notification cannot turn a known failure or kill into a success, while a newer cycle, a revival, and the upgrade direction still pass; the evidence itself is cycle-monotonic, so an older generation's terminal row, or one arriving late after a revival, neither overwrites nor re-plants it. |
531
+ | `scripts/run-fleet-terminal-retain-test.mjs` | The fleet ledger's rule for terminal rows has one home and one function. A terminal row (completed, failed, killed) lives only in the retention pool and never in the active set, whichever way it arrives — a task frame or a reconnect snapshot — and both arms go through the same admission function. A row brought back by a reconnect snapshot therefore expires with the retention window exactly like one that arrived as a frame; before, the snapshot arm left it in the active set for good, and a lost removal frame did the same to a frame-delivered row, and clearing the pool could not clear it. The suite refutes the change on three axes: timing (snapshot before or after the terminal frame, interleaved with removal, repeated delivery, resurrection, a later snapshot that does not carry the row, a row being viewed, pool clearing, and every terminal word through both paths with identical answers), concurrency (twin rows of one task in either order, a notification-retired entry meeting a snapshot row, notifications against a snapshot-carried row, two sessions' ledgers), and failure class (a status outside the terminal vocabulary, absent, or null is never turned into a terminal row; rows without a usable id, with a non-string id, or null entries are dropped and counted, never thrown on). It also pins that the removal hook still reports a known row for a settled terminal row, because downstream bookkeeping credits a removal only when neither the stale mark nor the unknown-row mark is present. Finally it pins the terminal evidence the ledger keeps past the window (status word plus cycle, per task tail, bounded): after a row has aged out or the pool was cleared, a same-cycle completed notification cannot turn a known failure or kill into a success, while a newer cycle, a revival, and the upgrade direction still pass; the evidence itself is cycle-monotonic, so an older generation's terminal row, or one arriving late after a revival, neither overwrites nor re-plants it. Since 0.97.0 it also pins the ledger's single per-task rule, applied in the one admission function to task frames and snapshot rows alike: a reading from an older cycle than the task has already shown is ignored (it neither revives the row, nor withdraws the terminal evidence, nor lets a snapshot drop the task's newer in-flight row); a newer cycle retires older in-flight aliases of the same task; within one cycle, or when the order cannot be told, an in-flight alias wins over a terminal row; otherwise the terminal row settles the task and twin rows collapse into one. Every arrival order of a set of readings, through either arm, gives the same projection and the same in-flight reading. A notification from an older cycle than the task has shown leaves the rows alone even when the running row's latest frame carried no cycle, and the bounded per-task cycle record never evicts a task that still has rows. |
532
532
  | `scripts/run-bg-subagent-row-test.mjs` | The background-subagent identity predicate every end shares. The fleet read surface lists every non-terminal task row — and since server 7.125.0 a session-scoped stream also brings back the session's own run row and its foreground delegation rows — so "is this row a background subagent" needs exactly one answer. `isBackgroundSubagentRow` applies four rules in order: a row with no id, a composite id, or a `run-leg` lane mark is never a subagent (the last two are foreground delegation rows); a non-agent handle (background shell or monitor) never is; a single-segment row with a readable parent is; a row without a readable parent is only when its id is an agent-family handle. `isBackgroundWorkRow` adds the non-agent handles for the "N background tasks" counts, and `isOwnBackgroundSubagentRow` adds ownership — a session-scoped stream replaces the ownership half only, never the row-kind half. The handle shapes are not retyped here: the durable agent-handle pattern is the installed engine's own, and the non-agent prefix letters are read from the engine's task registry when the tables are generated. The read-surface row now carries `sourceLane` (only when the wire row has a non-empty string), and task frames and reconnect snapshots reach the predicate through the same row shape. The suite checks each rule with a positive and a negative example, the absence cases (no id, unreadable parent, unusable lane mark), the 7.125.0 stream shape against the older shape, and that the lane word and the prefix table each have exactly one home in the source. |
533
533
  | `scripts/run-display-credential-reach-test.mjs` | The check for how far the display credential masking reaches. Percent-encoded text is read through decoded views inside the same single scan (up to three layers, on the raw text and on the painted screen text), so an encoded separator such as `token%3D<key>`, an encoded label pair, an encoded separator followed by whitespace before the value, or a whole URL that was percent-encoded once or twice is masked exactly like its plain form, with the mask placed on the original escaped bytes and the surrounding diagnostic words kept byte for byte. A known key prefix right after a slash (`GET /sk-…`, a multi-segment request path, or its encoded form) is now masked too, while segments inside a `file:` address that starts its own token (a `file:` buried inside a request path or query value does not count), short directory names that merely start with a prefix, and random-looking path segments are left alone. The configured-address display and the upload check read one value judge: a query value is masked or reported when its key is a credential label, its value is credential-shaped or carries a nested credential (including encoded layers and `+` as a space), and readable word chains under non-secret keys such as `deployment` stay visible on screen but still count as credentials for upload. A sixty-eight address pool (including single-slash spellings, which are rebuilt from the parsed components first, also when a query value holds a nested address), a seventeen query pool, the rule that anything the configured-address display would mask is also reported as a credential for upload, idempotence on dot, space and unfolded exits and the absent-input rules are pinned, and when a terminal reference tree is present both functions are compared with the terminal's own implementation byte for byte. |
534
- | `scripts/run-notice-presentation-test.mjs` | Presentation facts for engine notices, shared by every client. The cut primitive (`cutForDisplay`) shortens a sentence to a budget at a sentence boundary, then a clause, then a word, then a character, keeping at least half of the usable length at each step, counting the mark inside the budget, never splitting a surrogate pair or an escape token, and leaving the text untouched when the budget is not a finite number. The facts reader (`noticePresentationFacts`) answers who a notice is for (user and operator from the generated audience table; unknown for any code outside the catalogue), whether it carries something the person can do (present only when `detail.remedy` is an own non-empty string, otherwise unknown; absent is never invented), the remedy text, whether repeats are the same occurrence (only for the seven codes whose contract names an identity key, checked against the installed engine contract; every other case is an event), and a seat class (persistent with a hint, persistent, transient or diagnostic; `NOTICE_SEAT_CLASS`, `noticeSeatClassOf`). `outletReachable` and `planOutletRendering` decide which ways out a sentence may name in a given lane and deployment: reachable ones are written, unreachable ones are dropped, unreadable ones are not asserted. `noticePrintLine` builds the one-line print-mode form `[sema] <sentence> [<code>]` with a per-audience budget, sentence-boundary cut, a full-text pointer only when a session transcript exists, no cut when there is nowhere to point to, a single line, and the shared credential and control-character wash. The suite runs the real built package on each reader, probes hostile inputs and getters, checks the identity table against the installed engine contract, and reads the source to confirm there is one implementation of the surrogate and escape boundary. |
534
+ | `scripts/run-notice-presentation-test.mjs` | Presentation facts for engine notices, shared by every client. The cut primitive (`cutForDisplay`) shortens a sentence to a budget at a sentence boundary, then a clause, then a word, then a character, keeping at least half of the usable length at each step, counting the mark inside the budget, never splitting a surrogate pair or an escape token, and leaving the text untouched when the budget is not a finite number. The facts reader (`noticePresentationFacts`) answers who a notice is for (user and operator from the generated audience table; unknown for any code outside the catalogue), whether it carries something the person can do (present only when `detail.remedy` is an own non-empty string, otherwise unknown; absent is never invented), the remedy text, whether repeats are the same occurrence (only for the seven codes whose contract names an identity key, checked against the installed engine contract; every other case is an event), and a seat class (persistent with a hint, persistent, transient or diagnostic; `NOTICE_SEAT_CLASS`, `noticeSeatClassOf`). `outletReachable` and `planOutletRendering` decide which ways out a sentence may name in a given lane and deployment: reachable ones are written, unreachable ones are dropped, unreadable ones are not asserted. `noticePrintLine` builds the one-line print-mode form `[sema] <sentence> [<code>]` with a per-audience budget, sentence-boundary cut, a full-text pointer only when a session transcript exists, no cut when there is nowhere to point to, a single line, and the shared credential and control-character wash. The suite runs the real built package on each reader, probes hostile inputs and getters, checks the identity table against the installed engine contract, and reads the source to confirm there is one implementation of the surrogate and escape boundary. A cut never lands inside a redaction marker (the marker is removed whole), including on the 8000-character defence bound. |
535
535
  | `scripts/run-print-permission-outcome-test.mjs` | The one-line permission outcome for print mode (`printPermissionOutcomeOf`). It reads only the engine's own outcome for a tool call (the gate disposition and its settlement) and answers one of three forms from a single sentence table: the permission mode denied the call (the dontAsk sentence names the mode and offers an allow rule; the bypassPermissions sentence says it could not tell whether the write lands on a protected path; an unknown or missing mode word gets a generic sentence that never echoes the word), nobody could be asked (print mode has no approval channel; the sentence no longer claims a mode), or the mode let a call run without asking (the sentence for the recorded reason, shared with `modeReleaseReasonSentence`). Every other outcome, a missing record and hostile input answer nothing. The notice is declared for the user with or without a way out, and its seat class is read from the shared seat-class table; combined with `noticePrintLine` it forms the line `[sema] <sentence>`. |
536
536
  | `scripts/run-engine-directory-test.mjs` | The read faces for the cloud control plane's two engine endpoints: the directory of engines visible in a scope, and the "my engine" resolution a client asks for right after login. Four things are guarded because each one is a way to be confidently wrong. An unreadable response is not an empty directory: only an explicit empty list means "no engines here", and a body that is missing the list, or is not an object at all, voids the reading rather than reporting zero. A row that cannot be read is dropped without taking its neighbours with it, but the roster then says how many rows it could not read, so nobody treats it as complete. The relay path on a row is the one string the client appends to the center's origin and sends the user's token to, so it is accepted only as a same-origin relative path — an absolute URL, a protocol-relative form, one that only becomes protocol-relative once the URL parser has eaten a tab or newline, or one with a dot segment (plain or percent-encoded) that the parser would fold into a different path makes the whole row unreadable instead of being cleaned up and let through. And the words the server may add later are read as an open set: an unlisted kind, posture or resolution rule is passed through verbatim rather than thrown on or folded into a known word, while a missing online flag stays "not reported" instead of becoming "offline". The decision on top of the reading never guesses: a target the server named is used only when the candidate roster is present and agrees with it, a target the client could not read is never turned into "no engine", a resolution rule this version does not know with no target neither claims "no engine" nor "ambiguous", and a body whose rule contradicts its target or roster is not acted on. The explicit engine override is checked against the candidate roster only, by exact bytes; a name that also appears on a row that could not be read counts as a duplicate rather than disappearing with the filtered row; and when the roster itself cannot be read the override is refused rather than falling back to connecting by name |
537
537
  | `scripts/run-server-7125-pick-test.mjs` | Checks for the 7.125.0 server and SDK 20 pickup. An idempotent replay of a submission that already created a run is read from the nested engine result (falling back to the run row's own status and message) — both for the terminal reading, which is marked as a replay, and for the final result message's text, turn count and model; refusals sent after the stream opened, and the in-flight merge's follower frames, are read the same way as the first request's; a fleet row's workspace-snapshot failure is projected onto the fleet view only for the one documented value and never read as a run failure; the display outlet keeps joiners in place and turns directional controls and zero-width spaces into visible escapes on server output that now carries them back; the session-id refusal is classified by its code, not its sentence; and fields this package does not read yet stay unread until there is a consumer for them. Against the published 7.125.0 server package it checks where the stream's final frames are minted and that its real conflict projection reads as a busy session. |
@@ -539,16 +539,22 @@ guard still cross-checks the table by name).
539
539
  | `scripts/run-seat-settings-default-model-test.mjs` | The seat settings state no longer carries the deployment's default model. That fact has one reader, `liveDefaultModelOf`, applied to the model catalog the seat's `listModels()` returns; a second copy published through the settings store (a different wire field, taken at a different moment) could only disagree with it. The settings state now holds just the engine version and the data root, and a host that still publishes a `defaultModel` key is refused at the outbound gate, with the reason pointing at the catalog. The guard also reads the built declaration for the exact member list, and pushes six catalog bodies (default found by name, by id, absent from the catalog, key missing, key malformed, two rows sharing an id) through the body reader, the seat gate and both default-model readers to show they agree byte for byte. |
540
540
  | `scripts/run-seat-recents-test.mjs` | The seat's recent folders. The recent-folders list is read in exactly one way: the folders of the session records (derived, never registered by the host) joined with the folders the host registered (the ones the user picked, whether or not a session ever opened there), de-duplicated by exact path, most recent first, and a path present on both sides counts once at its later time. `recentFoldersOf` is the pure reader; `readRecentFolders` is the orchestrating reader that calls `listRecentFolders` only when the seat capability table says `recents` is available, and that keeps the session-record half when the host half is not offered or cannot be read. The seat gets two optional verbs, `listRecentFolders` and `noteRecentFolder`, and one capability bit `recents` that governs both (an absent bit means the host did not say, never "none"). The guard checks the registration, the outbound gates row by row (plain objects only, so what passes the gate is what survives structured cloning), the capability reading and its three distinct sentences, the reader's absence and ordering rules (a reply whose rows are all unreadable is reported as unreadable, never as an empty list), and an end-to-end chain from the host's capability publication through the renderer's reading to the host's verb double. |
541
541
  | `scripts/run-approval-card-v1-projection-test.mjs` | Projection of the v1 approval card (the `card` of an `approval_request` frame) into approval-card request facts through `approvalCardFactsOfV1`. For every key the v1 card shares with the live approval frame, the same value, well-formed or malformed, fed to both paths yields the same card request key and value; rule offers land in the read-only slot because the ask-decision route carries no rule; the real-approval bit is read from `risk`; every key in the SDK card key list has a recorded disposition, so a new SDK card key fails the gate; keys outside the card contract are not read, host positions are not minted and the session-answer route fact is not stamped; the grant-binding state matches the live path; refusal evidence that is present on the card but unreadable (malformed, inherited or throwing) is recorded as `unreadableEvidence` identically on both paths; non-objects yield undefined, one throwing key drops only that key and records it, and the result is frozen. |
542
- | `scripts/run-standing-rows-test.mjs` | Single judge `standingRowsOf(card, host?)` for the three standing rows on an approval card: allow for this session, don't ask again, and the deployment-list session row. Every row widens what runs without asking, so each one fails closed. An unreadable card, `rememberRefused` from the engine (own or inherited) or a present-but-unreadable value closes all three rows, even when rule offers are present. The session row needs the route fact `allowSessionCarried`, which the live-frame and pending-row paths stamp and the v1 path does not. The deployment-list row needs every ask class to be the deployment-list class, checked against the installed engine's `classifyAsk`. Engine rule offers in either slot give `engine_offer`; an unreadable offer key gives none. A local whole-tool rule is offered only when no engine lane closed the door, the arguments are available, the grant binding reads `none`, a standing allow rule could clear the ask, and the host reports a generic card face and that the rule would be sent; each precondition is flipped on its own. The live-frame, pending-row and v1 paths are exercised end to end, the grant-binding reading is compared form by form with the server's own reading when a server fixture is installed, malformed, inherited or throwing refusal evidence on the live-frame, pending-row and v1 carriers is recorded on the card as `unreadableEvidence` and closes the rows instead of reading as absent (checked end to end through the real paths); results are frozen, and hostile host facts never throw. |
542
+ | `scripts/run-standing-rows-test.mjs` | Single judge `standingRowsOf(card, host?)` for the three standing rows on an approval card: allow for this session, don't ask again, and the deployment-list session row. Every row widens what runs without asking, so each one fails closed. An unreadable card, `rememberRefused` from the engine (own or inherited) or a present-but-unreadable value closes all three rows, even when rule offers are present. The session row needs the route fact `allowSessionCarried`, which the live-frame and pending-row paths stamp and the v1 path does not. The deployment-list row needs every ask class to be the deployment-list class, checked against the installed engine's `classifyAsk`. Engine rule offers in either slot give `engine_offer`; an unreadable offer key gives none. A local whole-tool rule is offered only when no engine lane closed the door, the arguments are available, the grant binding reads `none`, a standing allow rule could clear the ask, and the host reports a generic card face and that the rule would be sent; each precondition is flipped on its own. The live-frame, pending-row and v1 paths are exercised end to end, the grant-binding reading is compared form by form with the server's own reading when a server fixture is installed, malformed, inherited or throwing refusal evidence on the live-frame, pending-row and v1 carriers is recorded on the card as `unreadableEvidence` and closes the rows instead of reading as absent (checked end to end through the real paths); results are frozen, and hostile host facts never throw. Since 0.97.0 the seat request's allow-for-this-session bit `sessionRow` is minted by `toolPermissionRequestAskBits(card, requestId)` from the same premise as the judge's session row and the request id's domain (`live:` and `durable:` carry a session answer; `question:`, `elicit:` and `plan:` do not); an unreadable or refused remember flag never lets the seat show that row, and the crossing never copies the bit from its input — a seat request, or any input carrying a request id, passed back through it gets no bit. |
543
543
  | `scripts/run-refusal-machine-reads-test.mjs` | Machine-readable causes on refusal outcomes, shared by every client. A rule the engine's rule compiler refuses is reported in `permissionsDeclarationReport().kept[]` with the compiler's own `code` and `message` for that rule (present only when readable as non-empty text; other reasons never carry them), so a host does not ask the compiler a second time. The refusal note on a live approval outcome (`respondRefusal`) keeps every machine-readable part the shared refusal readers use: the server's `retriable: true`, the not-pending cause word and the refused field from the refusal body (`extra`), and an `unreadable` mark when the thrown error itself cannot be read, so `classifyDecisionRefusal`, `decisionRefusalAskReading` and the close-out check answer the same for the thrown error and for the note. A binding safety stop says where it came from in its code: `binding_missing` when the pending row lacks the binding fields and nothing was sent, `binding_mismatch` only when the engine answered with a binding conflict; the closed set of safety stop codes is frozen, and the shared readers treat the new code as a local stop (not settled, never read as already resolved). |
544
544
  | `scripts/run-fail-sentence-test.mjs` | The wording of failure sentences that reach the user, in two parts. (1) A scenario refusal now has one sentence owned by the package: `scenarioNotAllowedContent` renders the subject, the allowed list (names escaped but never truncated, empty list omitted) and, for a fresh request only, the way out of omitting the scenario; the resumed-work form leaves that way out off, since a decision has no scenario to omit. The four decision exits (approve, deny, question card, plan review) append the sentence after the resume-policy sentence when the engine folded the refusal into `resume_blocked_by_policy`, and the failed outcomes of the two approval legs carry the folded original code as `blockedBy` next to the outer `errorCode` (present only when the outer code is the fold code and the body names a non-empty original). (2) The remaining user-facing failure sentences read a thrown value the same single way as the earlier seven: the four write and read exits (persisted rules, session policy, memory governance, MCP probe) no longer fall back to the class name or `[object Object]`; the subagent steer exit and the plan-review outcome sentence use the same reading and send the original thrown value's log form to the host log; the parked-row wait and the local rule parse take an optional `onFailure(log)` so their pure modules can hand the original to the host log; and the retry row's stalled `detail` goes through the same display exit as the four error arms. |
545
545
  | `scripts/run-engine-account-keys-projection-test.mjs` | The projection of five engine keys that older packages dropped on the way to the screen: a tool call's `recovered` start frame and `notExecuted` end frame (literal-true marks, read from own data properties only — never from a getter — and never turned into a claim about whether the call ran: absence means nothing), the completed run's machine-readable detail (the provider ended the last turn with a stop reason the engine does not read), the provider's account-condition word on a refused run, and the two notices that report an unknown stop reason and a stream that kept talking after its first ending. The suite drives the built package through the projection, the transcript adapter and the print-mode frame, and checks that absent, falsy, malformed and hostile values produce no claim and never throw, that the terminal reader keeps the run's kind whatever its detail looks like and isolates each key, and that the single reader for the marks is the only way the sources read them. |
546
- | `scripts/run-seat-session-patch-test.mjs` | The seat's rename and pin verb. The seat gains one optional verb, `patchSession`, that writes the two user-writable fields of a session row — `title` (a string renames; `null` clears back to the automatic title, and an empty string is not a way to clear) and `pinned` (a boolean) — and answers with the row as it is after the write; the host forwards it to the engine, and the seat contract fixes only the shape. The inbound judge `checkSessionRowPatchRequest` accepts exactly the three keys, requires a non-empty `sessionId` and at least one of the two fields that is not `undefined`, and refuses anything else naming the key; accessors are never run, and a non-enumerable key or one found only on the prototype chain does not count as carried. It deliberately does not copy the server's title rules (length and surrounding whitespace): the server judges them and reports a typed error, and the guard pins that choice with two cells so that copying them later is a visible decision. The receipt row goes through the existing outbound gate, which now accepts `pinned` only as a boolean (absent means not pinned or unknown, never an error). Whether the verb can be used is answered only by the seat capability `patchSession`: three readings (reported, not reported, unreadable), three distinct disabled sentences that differ from the other capabilities', and no cross-talk between capabilities. The guard also reads the built declaration file with the TypeScript parser for the verb signature, the request members (only `sessionId` required), `LocalSessionRecord.pinned` and `SeatCapabilitiesState.patchSession`, drives a host stand-in end to end through the public entry points (publish the capability, subscribe, judge, call, outbound gate, structured clone, withdraw), and poisons every key of the new coverage-table row. |
546
+ | `scripts/run-seat-session-patch-test.mjs` | The seat's rename and pin verb. The seat gains one optional verb, `patchSession`, that writes the two user-writable fields of a session row — `title` (a string renames; `null` clears back to the automatic title, and an empty string is not a way to clear) and `pinned` (a boolean) — and answers with the row as it is after the write; the host forwards it to the engine, and the seat contract fixes only the shape. The inbound judge `checkSessionRowPatchRequest` accepts exactly the three keys, requires a non-empty `sessionId` and at least one of the two fields that is not `undefined`, and refuses anything else naming the key; accessors are never run; a non-enumerable own key is refused naming the key, whatever its value (a structured clone would drop it, so the gate does not let it pass unread); the request must be a plain record (prototype `Object.prototype` or `null`), so `Date`, `Map`, `Set`, class-instance and custom-prototype carriers are refused; a key found only on the prototype chain does not count as carried. It deliberately does not copy the server's title rules (length and surrounding whitespace): the server judges them and reports a typed error, and the guard pins that choice with two cells so that copying them later is a visible decision. The receipt row goes through the existing outbound gate, which now accepts `pinned` only as a boolean (absent means not pinned or unknown, never an error). Whether the verb can be used is answered only by the seat capability `patchSession`: three readings (reported, not reported, unreadable), three distinct disabled sentences that differ from the other capabilities', and no cross-talk between capabilities. The guard also reads the built declaration file with the TypeScript parser for the verb signature, the request members (only `sessionId` required), `LocalSessionRecord.pinned` and `SeatCapabilitiesState.patchSession`, drives a host stand-in end to end through the public entry points (publish the capability, subscribe, judge, call, outbound gate, structured clone, withdraw), and poisons every key of the new coverage-table row. |
547
547
  | `scripts/run-session-sync-file-history-test.mjs` | The user-facing facts for a session sync that moved the conversation but not its file history. One sentence says it (only the conversation was synced, why, what it means for restoring files with /rewind, and that syncing again will not add it), and a reader decides whether it is said at all: only when the sync result's own file-history outcome is "skipped", and not when the source manifest states outright that the file-history envelope is empty (no tracked files and no rewind boundaries); anything unreadable is said rather than swallowed. A second reader gives the occurrence identity (the session and the source's last entry) so a client can avoid repeating the sentence when nothing new was synced, and a judge tells the 404 for a missing file-history blob (status 404 and exactly the code not_found.blob) apart from every other 404. The guard pins the sentence byte for byte, walks each outcome of the real SDK push, pull and import calls against fake peers, and checks that hostile input (accessors, revoked proxies, inherited properties) never throws. |
548
- | `scripts/run-file-history-tighten-test.mjs` | What a client does when the user has turned file checkpointing off but the engine's file-history capture capability cannot be judged. A four-way verdict reads the capability the same way the request fragment does: no restriction requested, declare the per-run opt-out (the engine says it can be switched off), already off (the engine does not capture), or unjudged (not observed, not reported, a word this client does not know, or a malformed reading), and only the last refuses the send with one sentence that says nothing was sent, why, and the way out. A single gate answers before a submission: anything judged costs nothing, while an unjudged reading triggers exactly one single-flight re-probe of the engine's capabilities (a body that already landed but is unreadable for this one field is fixed in place; a failed or off-generation re-probe leaves the previous reading untouched for every other reader) and the verdict is taken again. The guard pins the verdict table, the refusal sentence, the one-probe and single-flight counts against a real capability cache, and the failure and ordering cases. |
548
+ | `scripts/run-file-history-tighten-test.mjs` | What a client does when the user has turned file checkpointing off but the engine's file-history capture capability cannot be judged. A four-way verdict reads the capability the same way the request fragment does: no restriction requested, declare the per-run opt-out (the engine says it can be switched off), already off (the engine does not capture), or unjudged (not observed, not reported, a word this client does not know, or a malformed reading), and only the last refuses the send, with a sentence chosen by cause (not read yet, so try again in a moment; read but not readable, or not reported, so trying again will not change it) that says nothing was sent, why, and the way out. A single gate answers before a submission: anything judged costs nothing, while an unjudged reading triggers exactly one single-flight re-probe of the engine's capabilities (a body that already landed but is unreadable for this one field is fixed in place; a failed or off-generation re-probe leaves the previous reading untouched for every other reader) and the verdict is taken again. The guard pins the verdict table, the three refusal sentences, the one-probe and single-flight counts against a real capability cache, and the failure and ordering cases. |
549
549
  | `scripts/run-active-run-busy-read-projection-test.mjs` | How the package reads a rejected submission (the session already has a run in flight) in its terminal row, in its result frame and in the public discriminator `activeRunBusySignal`: one discriminator body and one carrier rule, shared by all three. The server's rejected and conflict terminal frames carry `status: 'failed'` since 7.125.0; the older idempotent-replay form — a cached copy of the original 409 conflict body wrapped as `done{ result, replay: true }`, with neither `status` nor a terminal word — is no longer recognised, since the package's support floor moved past the servers that sent it. The suite pins what such a body reads as today (the discriminator answers null, the row is the single `Outcome unknown` line, the result frame carries the unknown-outcome flag, the public projector agrees, with or without the replay flag, code only, id only), checks over a pool of sixteen frames that the discriminator and the row agree in both directions (including a cause-form frame that carries the busy code), pins which objects count as the carrier, pins that the retired folding function and its flag are gone from the source and the build, and pins in source that the discriminator and the carrier rule are each defined once. |
550
550
  | `scripts/run-plan-review-outcome-verdict-test.mjs` | The plan-review decision outcome has one reading rule: each outcome word is given only on positive evidence, otherwise `unconfirmed`. A decision reply that says the review is still pending (`retriable: true`) reads `still_parked` whatever the follow-up status read says; only `completed` reads `took_effect`; `suspended` reads `advanced` only when the follow-up read shows the new pause and that pause has a decision card (an approval or a plan review); `needs_review` reads `still_parked` when the review's instance identity before and after the decision can both be read and match, `advanced` when they differ and the new pause has a decision card, and `unconfirmed` otherwise; a malformed status on the follow-up read falls back to the decision reply like a failed read; every other status (`failed`, `blocked`, `timeout`, `running`, unknown words) reads `unconfirmed` and the sentence states the reading instead of claiming completion. `decidePlanReview` takes an optional `parkedResult` (the paused result the card answers) to supply the before-identity. Pinned with a structural injected client: failed approvals and rejections, the still-pending reply with and without a follow-up read, changed / same / missing / malformed identity, completed with and without the follow-up read, the other status words, the callback and the queue item carrying the same outcome word, and the new option on the shipped declaration. |
551
- | `scripts/run-durable-decide-leg-test.mjs` | Which durable decision leg a pending row belongs to has one table and one reading. The approval-card gate kinds are `human` and `irreversible_ask` (the only kinds the approval decide route accepts), the plan-review gate kind is `plan_review`, and the review wording set is `plan_review` and `needs_review`; each table is pinned at compile time in both directions against the engine's pause registry and checked again at run time against the installed engine. `durableDecideLegOf(row)` answers one of five words: `approval`, `plan_review`, `other_gate` (a known gate neither decide route accepts), `unreadable` (a gate word outside the registry) and `absent` (no readable gate word on the row: missing, null, empty, not a string, or a throwing read); it never throws. `askParkForeignGateKind` derives from it. Pinned with the twenty-shape verdict matrix, the derived foreign-kind answer, the row classifier, the pending-row lookup (including refusing to fall back to another row when the row a park names is not an approval), the busy-session recovery on non-gate words, the five-word union on the shipped declaration, and no engine-package reference in the shipped output. |
551
+ | `scripts/run-durable-decide-leg-test.mjs` | Which durable decision leg a pending row belongs to has one table and one reading. The approval-card gate kinds are `human` and `irreversible_ask` (the only kinds the approval decide route accepts), the plan-review gate kind is `plan_review`, and the review wording set is `plan_review` and `needs_review`; each table is pinned at compile time in both directions against the engine's pause registry and checked again at run time against the installed engine. `durableDecideLegOf(row)` answers one of five words: `approval`, `plan_review`, `other_gate` (a known gate neither decide route accepts), `unreadable` (a gate word outside the registry) and `absent` (no readable gate word on the row: missing, null, empty, not a string, or a throwing read); it never throws. `askParkForeignGateKind` derives from it. Pinned with the twenty-shape verdict matrix, the derived foreign-kind answer, the row classifier, the pending-row lookup (including refusing to fall back to another row when the row a park names is not an approval), the busy-session recovery on non-gate words, the five-word union on the shipped declaration, and no engine-package reference in the shipped output. Since 0.97.0 the same table also decides which approval arm a parked frame goes to: `isToolApprovalGate` agrees cell by cell with `durableDecideLegOf` (an approval gate kind takes any tool that is not the question tool, an unreadable kind falls back to the file-write and shell name families, any other kind takes neither arm); a real-wire park on an approval gate kind for a tool outside those families raises the approval card and sends the decision end to end, while the non-gate word `tool_approval`, a plan-review kind with a write tool, and a non-gate word with the question tool raise nothing. |
552
+ | `scripts/run-engine-caps-landing-projection-test.mjs` | How a client reads the whole capabilities answer of an engine, and how it swaps the probe used for later re-reads. The whole-answer reader has three states: landed (carrying the frozen snapshot that every narrow capability reader and every per-key reader also read), not landed (no probe yet, still in flight, failed, or invalidated), and unreadable (the probe returned a body whose root cannot be read). A key the engine did not report is unknown and never counts as on: the per-key readers are pinned to a written rule applied to the whole answer, and every narrow reader gives its own not-observed reading on an unreadable root. A rotation entry for the same engine with a new probe (for example a client rebuilt after a credential refresh) starts that probe when nothing is landed or in flight, and otherwise only replaces the probe used by later single-flight re-reads without invalidating anything; the guard pins that the next re-read calls the new probe and never the old one, together with the in-flight, re-entrant and invalidation orderings. |
553
+ | `scripts/run-rule-tool-name-judge-test.mjs` | One reading for "does the engine accept this rule tool name": the engine's own tool-name check, installed as a value port and read through `ruleToolNameVerdictOf` (current / retired with its current name / unprefixed namespace / unmintable segment / unknown). The session-rule deliverability check asks it once for every name that survives the plain-text steps (an ordinary name the installed engine has newly retired is withheld as `legacy_tool_name`; a check that cannot answer makes the batch unreadable), the refused-entry removal removes every entry it does not call current and answers `judge_failed` without writing when it cannot answer, the sub-agent tool-list pre-check reports retired, unprefixed and unmintable names from it, reports an entry it cannot check as unjudged (held back like any other deny-list problem when withholding is on) and judges case twins only against a supplied roster, and the request-body assembly rewrites a retired name to the current name it gives only in the deny and ask buckets (allow keeps it). No build-time retired-name table remains in the source. A real engine run that mounts no tool of that name, reading a stored session record, refuses to start exactly for the names this reading does not call current. |
554
+ | `scripts/run-cloud-mcp-dropped-notice-test.mjs` | The sentences shown to a person when the cloud configuration leaves MCP server entries out. Each of the five reasons an entry is left out (switched off, a required setting missing, an unsupported transport, more entries than this client passes to the engine, both identity-header settings set) has its own sentence that names the entries; the switched-off sentence is informational and the other four are warnings. The sentences are part of the single wording source `cloudEffectiveNotices`, whose input now also carries the list of left-out entries. A reason outside the known set and an unreadable row (not an object, a reason that is not a string, a throwing getter, a reason or name that only exists on the prototype chain, an index that is an accessor or a hole) get a plain warning that names what can be read instead of being skipped, a list that is not an array or cannot be read (an accessor that is never run, a value of undefined, a proxy that throws) gets one warning saying it could not be read, a missing key adds nothing, and an empty list adds nothing. Names are capped at five with a `+N more` tail, repeated names are shown once, rows without a readable name are counted, and the order is degradation warnings, catalog references, left-out MCP entries (warnings first), budget, runtime caps. Pinned on the shipped declaration (the reason union is exactly the five words) and on the source (the sentence table is typed on that union and looks reasons up by own key). |
555
+ | `scripts/run-tool-card-outcome-test.mjs` | One reading, one sentence, for what a tool result card in the transcript actually says. Three marks decide it: the placeholder mark on the record (the engine sent no body, so the content is an empty placeholder), the engine's positive evidence that the call never ran, and the block's error flag. `readToolCardOutcome(record)` reads them as `normal` (a real result; success or failure still comes from the error flag and is never rewritten), `degraded` with a cause and one sentence (`not-executed` — takes precedence and is independent of the error flag, so a card that looks successful but never ran is caught; `error-without-detail`; `no-result`, where the error flag false is a placeholder and not a success; `unrecognised`, a placeholder value from another version) or `unreadable` (not a tool result record, or the placeholder position cannot be read — never read as normal). The placeholder has two minting points that cannot be told apart on the record (a call closed at the end of the turn with no end frame, and a real end frame without a body), so the no-result sentence states only what both share. Pinned on records produced by the real pipeline in both lanes (the transcript row and the print-lane frame answer the same), a read-failure matrix that never throws and never reads as normal (accessors are not run, prototype-chain keys do not count), four distinct sentences with no internal names or tool-family names, and the source (the sentence table is typed on the cause union, the evidence mark goes through the existing strict-true reader, the placeholder constant is imported not re-written, and each sentence has exactly one minting site in the shipped output). |
556
+ | `scripts/run-approval-settled-single-source-test.mjs` | Whether an approval already has an outcome is recorded once per process, in the package's approval-holder registry, at two points only: when the live card's holder settles (the decision was delivered and acknowledged, or the interruption denial it sent was accepted) and when the engine's close-out frame for that approval arrives. Every path that could put up a card for the same approval reads that one record first: the in-stream frame path, the suspended-ask path (same entry point), and a host's own durable route through `joinApprovalHolder`, which now answers `joined` for an approval that was already answered or closed by the engine even after the holder let go (an approval the engine moved to the parked queue still answers `free`, since it remains open there). Cells cover the host's durable route after a delivered answer and after a close-out with no card ever shown, the parked negative control, the suspended-ask path writing and reading the same record, a new bridge still recognising a settled approval, the replay verdict reading only this record (a host-supplied map is ignored), the outcomes that are not recorded (a delivery failure including 404, a withdrawn card, a refused edit, an interruption denial that failed, threw or was answered with the parking reply, or that a parked close-out overtook), the write happening at the acknowledgement rather than when the card is answered, and the 4096-entry bound |
557
+ | `scripts/run-server-7126-pick-test.mjs` | Checks for the 7.126.0 server and SDK 21.1 pickup. A submission refused because its idempotency key was already bound to a different request body is classified by one code check on every leg (thrown errors and the refusal in the stream's final frame), and the result frame carries the package's own sentence. The engine's approval card word is read through one closed-set reader and reaches the approval card from both the live frame and the pending row; a missing or unknown word stays missing. The run's failure detail is read as the server's three-arm union plus an unrecognized arm. The two decision doors' gate-kind tables are the SDK's own objects, and the retired hand-written copy is gone from the root entry. The retired team-review request keys are refused at assembly, and allowTools is sent as given. The models list's rejected-candidate record has a three-state reader. Against the 7.126.0 server package it checks the request key closure, the 409 mint site, the engine's card words and the failure-detail registry. |
552
558
 
553
559
  Each suite carries a floor that only moves up — a refactor that stops executing a group of
554
560
  assertions is a failure, not a quieter pass. Guards anchor on the **installed artefact's content**