@sema-agent/client-core 0.64.1 → 0.64.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +88 -0
- package/README.md +6 -6
- package/dist/autoModeUnavailable.d.ts +18 -5
- package/dist/autoModeUnavailable.js +31 -7
- package/dist/cloudConfigWireCaps.js +16 -0
- package/dist/hitl/toolApprovalWire.d.ts +82 -5
- package/dist/hitl/toolApprovalWire.js +112 -1
- package/dist/mcpWireCaps.d.ts +31 -0
- package/dist/mcpWireCaps.js +12 -0
- package/docs/INTEGRATION-CLIENTS.md +277 -14
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -49,6 +49,94 @@
|
|
|
49
49
|
> 挡住 ⇒ 本批把它机械化——④a0 对 `pending` 行**要求段头已是日期形**(`(未发布)` 直接红),阶段一
|
|
50
50
|
> commit 漏转在发布前就红,不再靠人记。
|
|
51
51
|
|
|
52
|
+
## 0.64.2(2026-09-10)
|
|
53
|
+
|
|
54
|
+
> server 7.69.0 提货批的包侧六件:**四件 additive**(零删除;只在上游真发那一位在场时多铸一个键)
|
|
55
|
+
> + **一条行为面订正**(件④,异源对抗复审 r1 [medium] 逼出;`parse_error` 的行为一字未变)
|
|
56
|
+
> + **一处同形存量族扫**(件⑤)。接入面详解见 `docs/INTEGRATION-CLIENTS.md` §29。
|
|
57
|
+
|
|
58
|
+
### 新增
|
|
59
|
+
|
|
60
|
+
- **`ApprovalCardRequest.classifierUnavailable?: { cause: string }`**(cli L-174②;server ≥7.69.0 /
|
|
61
|
+
core 7.10.0 #616)——「问你是因为 auto 模式分类器这一轮跑不了」的事实**两腿都到卡口**:活卡帧腿
|
|
62
|
+
(`ToolApprovalFrame.classifierUnavailable`,同批进帧型与键镜像)与 durable park 行腿(行上
|
|
63
|
+
`PendingAction.tool_approval` 的孪生位),经**同一把**窄读器,**键路与 ask / park 行同形** ⇒ 端把卡入参
|
|
64
|
+
直接喂给 0.63.0 就有的 `classifierUnavailableOf` / `classifierUnavailableDetail`,读法零改。
|
|
65
|
+
过境判据与 server 唯一铸点 `readClassifierUnavailable` 同源(`cause` 非空串即收、**开集**、坏形降缺席);
|
|
66
|
+
「渲哪一句」那一层的闭集收窄仍归公面读器 —— **过境按开集、显示按闭集**是设计,不是漂移。
|
|
67
|
+
🔴 显示元数据、echo-only:不参与也不改变任何决断字节,缺席**不许**渲成「分类器跑成了」。
|
|
68
|
+
⚠️ 两条诚实边界:① 本键**退役日期已定**(core 7.12.0 / server 7.70.0 起帧上是死键,事实位随拒绝面走),
|
|
69
|
+
0.65.0 改读那一面而**不删码**;② durable 腿在 engine 7.69.0 上**恒零命中** —— 两只 checkpoint 店的
|
|
70
|
+
`listPending` 今天没把这一位扁平化到 `/v1/approvals` 行上(与 `governanceForced` 同一格),包侧承诺是
|
|
71
|
+
「行上有 ⇒ 卡上有」,server 补投那天零改动生效。
|
|
72
|
+
- **durable park 行 → 卡的两位对偶**(件⑥;cli B-058)——「行 → 卡」重铸此前只投 5 位,补上
|
|
73
|
+
`persistedRuleShadowed`(← 行 **`riskDescriptor.shadowedRule`**;⚠️ **键名与活卡帧顶层不同**,那正是它
|
|
74
|
+
长期被漏掉的原因 —— server `redactPendingDisclosures` 逐字脱敏后随 `projectPendingForWire` 上 wire)与
|
|
75
|
+
`ruleOffersAbsence`(← 行同名键;两只 checkpoint 店今天还没把它扁平化上来,补投那天零改动生效)。
|
|
76
|
+
落位是**活卡腿同一个卡位**,端零改。🔴 **它是壳让位臂的输入,不是一格顺手补上的显示**:活卡腿早就有,
|
|
77
|
+
durable 腿因为不投,一条**被记住的 allow 规则清不掉的 ask** 在耐久路上看起来就是普通 ask ⇒ 被自动
|
|
78
|
+
`decide approve`(cli B-058 离线真跑:default 车道 + `Bash(grep:*)` + 带 `shadowedRule` 的 park 行 ⇒
|
|
79
|
+
零卡 + approve)。判据与活卡腿逐字同族(非空白串才 stamp、原文字节、坏形/缺席不折空串、两位不连坐)。
|
|
80
|
+
同批把帧上 `persistedRuleShadowed` JSDoc 里「耐久对偶仍未消费——那一路是独立的一件」那句 0.28.0 旧话
|
|
81
|
+
改成现状。黑盒判据 **G-41**。
|
|
82
|
+
- **`readDenialLimitFallback` 上公面**(cli L-174③)—— 0.59.0 起就在的限额回落卡窄读器
|
|
83
|
+
(四成员全必填、计数与窗按有限非负数、`limit` 开集)从模块私有转为导出,让端自持的同判据副本整只退役
|
|
84
|
+
(第二个判官会在上游改形那天与包各漂各的)。⚠️ 导出的是**读器,不是许可**:`autoDenyAfterMs` 仍然只许
|
|
85
|
+
渲倒计时,窗的执行全在引擎。公面导出 918 → **919**。
|
|
86
|
+
|
|
87
|
+
### 修复
|
|
88
|
+
|
|
89
|
+
- 🔴 **`classifierUnavailableOf` 把「比本端新的成因词」吞成缺席**(件④;**本批唯一的行为面改动**,
|
|
90
|
+
异源对抗复审 r1 [medium] 采纳)—— 修前这只 0.63.0 的读器按 `AUTO_MODE_UNAVAILABLE_CAUSES` **闭集**
|
|
91
|
+
收窄,于是一台比本端新的引擎发一个**合法的新成因词**时,审批卡上「为什么问我」那一行**整段消失**;
|
|
92
|
+
把同一张卡喂给 `classifierStatusOf({armed:true}, card)` 还会答 **`available`** —— 一条真实的故障事实
|
|
93
|
+
被渲成「一切正常」。自证:`classifierUnavailableDetail` 早就为这一形备了兜底句
|
|
94
|
+
(「a word newer than this client」),而修前那句话**永远不可达**。
|
|
95
|
+
修后判据 = **非空串即收 + 只排除熔断轴独占的词**(`AUTO_MODE_BREAKER_CAUSES` −
|
|
96
|
+
`AUTO_MODE_UNAVAILABLE_CAUSES`,**派生而非手抄**,今天 = `{parse_error}`)——「表外」与「另一条轴上
|
|
97
|
+
的词」本来就是两件事,修前把它们判成了同一件。🔴 **`parse_error` 的行为一字未变**,0.63.0 起的黑盒
|
|
98
|
+
判据 **G-19 照旧成立**;新判据的判据见新黑盒判据 **G-36**。端**零改**,但端若自己维护过一张成因闭集
|
|
99
|
+
表(为了「只渲认得的词」),那张表现在是这次修的病的第二处,该退役。
|
|
100
|
+
- **`cloudMcpToSpecs` 同样丢 `toolFaces`**(件⑤,下一条的**同形存量第二处**;族扫抓出)——
|
|
101
|
+
center `/api/v1/effective` 的 mcp 域 → 请求面的那条投影也是逐键白名单重建。上游对这一位的成文是
|
|
102
|
+
「三条腿(请求 / config-center / 本地 config.d)同一条规则」,只修本地那条 = 用 center 下发配置的
|
|
103
|
+
部署照样丢键。判据与本地腿逐字相同;门 G7 段改成**对两条腿各对一次账**。
|
|
104
|
+
- **`mcpConfigToSpec` 丢 `.mcp.json` 的解析透明键 `toolFaces`**(cli L-167①,包侧缺陷)——
|
|
105
|
+
该函数是逐键白名单重建,而 settings-schema 1.9.0 给 mcp 段加的 `toolFaces` 是**解析透明键**
|
|
106
|
+
(`z.unknown()`,形的校验属引擎摄入侧),白名单没有这一行 ⇒ 运维在 `.mcp.json` 上声明的 per-tool 面
|
|
107
|
+
(写围栏与敏感路径策略读的 `pathTarget` 那一格)在**包边界**被静默剥掉,而两边看着都对。
|
|
108
|
+
修 = **按上游的透明键表逐名透传**(两条 transport 臂 + 整表腿),**不是**改成全展开:配置上的未知键照旧
|
|
109
|
+
不上 wire。🔴 **坏形原样过境,不是包拒** —— 形的判决在引擎(整条 server 不进 + `mcp_injection_dropped`
|
|
110
|
+
点名 / 单只工具拒 + `config.tool_face_invalid`);包在这里判一次形,判严了就把运维声明的保护**静默**吞掉。
|
|
111
|
+
透明键表 1.10.0 实装两员,`source` 刻意不在这条腿上(它是部署面的键,请求面 `McpServerSpec` 上没有它),
|
|
112
|
+
这条不透传是**带退出条件的记账**。
|
|
113
|
+
|
|
114
|
+
### 文档
|
|
115
|
+
|
|
116
|
+
- `docs/INTEGRATION-CLIENTS.md` 新增 **§29**(本批六件:病形、两腿投影表、五条读法、两条诚实边界、
|
|
117
|
+
三端换装清单、黑盒判据 G-33–G-39、常驻门);§0a 版本锚整段刷到 0.64.2;§2/§2b 计数 918 → 919。
|
|
118
|
+
- §23a 的 `denialLimitFallback` 读器纪律段指向已上公面的 `readDenialLimitFallback`,并写明端自持副本
|
|
119
|
+
应当退役的理由与「导出的是读器不是许可」。
|
|
120
|
+
- §27h 单铸律补第二条 `delegation.ask_unresolvable`(core 7.11.2 `ask-unresolvable-notice.d.ts` 的
|
|
121
|
+
`@contract` 逐字引用):audience `user`、`detail` 五格、🔴 `parkLaneExisted` 是判别位禁折叠、
|
|
122
|
+
deny 本身比通告更老且一字不改(披露面补齐,不是裁决面变更)。
|
|
123
|
+
- §27i⑦ audience 分流条款补一条**允许形**:单人本机端(起引擎的人 = 看屏的人)可以**登记在案地**不分流、
|
|
124
|
+
把 `operator` 受众渲通用行 —— 这一端没有第二块屏,分流的净效果是把一批正文里就写着补救办法的治理事实
|
|
125
|
+
整段静音。允许的是渲通用行,**不是不登记**(确实不渲的码要进端自己的明拒表);多用户 / 有独立运维面的端
|
|
126
|
+
照旧分流。§27i⑧ 改成与本批实装一致(改读卡入参;过境/显示两层分工;退役日期)。
|
|
127
|
+
|
|
128
|
+
### 常驻门
|
|
129
|
+
|
|
130
|
+
- `run-hitl-gate-honesty-test.mjs` 新增 **F16 段**(活卡帧腿,含件④ 的**端到端**格:新词一路到最终
|
|
131
|
+
那句话与状态读数,带反向判别力自证),`run-durable-card-display-keys-test.mjs` 新增 **⑫ 段**
|
|
132
|
+
(durable 腿,含**两腿同值反向对照**、端到端与射程边界打印),`run-approval-frame-keys-test.mjs`
|
|
133
|
+
新增 `classifierUnavailable` 的 `AHEAD_OF_ANCHOR` 领先登记(sdk 8.8.0 锚 27 项尚无本键;两条退出条件由门
|
|
134
|
+
自己执行),`run-additive-key-passthrough-test.mjs` 新增 **G7 段**(透明键表 ⇄ **每一条腿**的账逐名相等,
|
|
135
|
+
本地 + center 两条腿各对一次),`run-auto-mode-unavailable-test.mjs` 新增 **B2b 段**(件④ 的红先绿后:
|
|
136
|
+
未知词照收并走到兜底句 / 排除集是派生的 / 两集的交照旧读得出)。
|
|
137
|
+
- 棘轮:公面导出基线 918 → **919**;`unknown` 出境 312 → **314**(逐件写明理由);
|
|
138
|
+
`TOOL_APPROVAL_FRAME_KEYS_MIRROR` 计数 27 → **28**。
|
|
139
|
+
|
|
52
140
|
## 0.64.1(2026-09-09)
|
|
53
141
|
|
|
54
142
|
### 文档
|
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.64.
|
|
38
|
+
**Version:** 0.64.2
|
|
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
|
|
@@ -230,14 +230,14 @@ public-surface guard checks that last one).
|
|
|
230
230
|
| `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 |
|
|
231
231
|
| `scripts/run-client-core-diff-test.mjs` | Differential equivalence against the CLI reference bridge + replay-id invariant + ledger round-trip |
|
|
232
232
|
| `scripts/run-seat-contract-keys-test.mjs` | The seat IPC contract: verb list ↔ SPEC ↔ types, element-wise |
|
|
233
|
-
| `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) |
|
|
233
|
+
| `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 by the classifier-unavailable key the server already emits, 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) |
|
|
234
234
|
| `scripts/run-print-bash-iserror-test.mjs` | The print lane's Bash `is_error` authority (structured over regex) |
|
|
235
235
|
| `scripts/run-bash-benign-exit-interpretation-test.mjs` | Benign non-zero Bash exits (`returnCodeInterpretation`) stay non-errors across all three derivation arms, and the annotation transits to the card |
|
|
236
236
|
| `scripts/run-sdk-floor-test.mjs` | The SDK version floor — and, more to the point, that the *installed* type declarations still carry the keys this package reads |
|
|
237
237
|
| `scripts/run-engine-caps-ledger-test.mjs` | A per-key disposition ledger for `GET /v1/capabilities`. The SDK's `Capabilities` grew from 74 keys to 93 in one release and nothing on the board could see it: this package consumes that table through four synchronous readers, and *nineteen new positions arriving while the package does not move* is exactly the disease shape this repo keeps logging on other axes — the fact is already on the wire, the package boundary is the cell that swallows it, and no client can read it however they write their side. So the ledger is reconciled **element-wise against the SDK interface in both directions**: a key the SDK added with no ledger row is red (someone must classify it), and a row for a key the SDK removed is red too (a registration that no longer does anything). Each row then has to survive its own claim — a `read` row names the source file, and the **code** there (comments stripped) must really mention the key, because prose asserting an alignment is the classic way these guards go hollow; a `not_read` row must have **zero** read sites in the tree, so wiring one up while the ledger still says the package ignores it is red rather than invisible. The census behind those two directions recognises five call shapes, each of which really occurs here — a reader whose base argument carries its own parentheses, a direct `caps.<key>`, a narrowing cast, an own-property read helper, and a `*_CAP` constant — and proves it on fabricated samples first, since a census that recognises one shape reports "nothing here" for the other four. What the guard deliberately does **not** judge is whether a position *ought* to be read: that is a design call, and the ledger only pins that every capability was looked at once by a person and that what they wrote down does not contradict the code |
|
|
238
238
|
| `scripts/run-sql-engine-capability-test.mjs` | The SQL-posture read face and the four-state capability reader underneath it. One capability cell here carries **four different things**, and each one points an operator somewhere else: nothing has been observed yet in this process (a one-shot doctor run is always in that state), the response arrived but carries no such key (an older engine), the engine explicitly answered `null` — *this deployment has no SQL backend*, which is a **positive fact** rather than an absence — and a full reading. Fold any two together and the screen states something flatly, confidently, and wrongly, so every positive control here is paired with a control pointing the opposite way, and the four sentences the doctor row can print are checked to be pairwise distinct and non-implying. The reading itself is narrowed no tighter than the mint: `txnMode: null` is a **legal value** — two of the three engines always report it that way, and the upstream type note names reading it as "optimistic" as the error — so treating it as malformed would throw away the entire reading for ordinary deployments, which is the same disease this repo logged when a consumer's domain was narrower than the producer's. A response that cannot be parsed **clears** the cell rather than leaving the previous engine's answer in place, and a separate invalidation port exists for the case the generation latch cannot catch — a same-port respawn whose new probe never succeeded, where the stale reading would otherwise be answered as current fact. Untrusted values (the isolation string is read back from a database server variable) are sanitised and bounded before display, and the bound is applied **before** escaping so a visible escape never gets cut in half. Finally the export names are themselves a guard: the shell still carries a copy that is meant to go red on the package's same-named export and be swapped out, so renaming anything here would silently disarm that lock |
|
|
239
239
|
| `scripts/run-terminal-cause-projection-test.mjs` | The `7.64.0` wire reshape, projected. A run's ending stopped being eight parallel flat keys and became **one tagged cause** (`completed | failed | blocked | paused`), and a tool call's gate stopped being four orthogonal words and became **one record** (`disposition` / `settlement?` / `origin?`). Both are read in exactly one place in this package, and this guard pins them at **two levels**, because the dangerous seam is "the reader was updated, the consumer was not": each terminal arm is checked on the reader *and* on the `subtype` / `is_error` / `errors[]` the projector actually emits. Two properties carry most of the weight. First, a terminal word this reader does not know is **never** laundered into an empty success — it lands on an `unknown` arm carrying the word verbatim, while a payload with no terminal word at all (the mock lane) keeps the success arm exactly as before, which is the one and only case the reader answers `null`. Second, the three window words (`approval_window_expired`, `denial_limit_window_expired`, `park_sla_expired`) must each be told apart by a different predicate: the previous generation collapsed all three onto one `timeout`, and re-merging them would throw away the discrimination this reshape just restored. Two byte generations are read by one reader, keyed on the discriminator upstream nailed (`"terminal" in result`): the current cause form, and the **flat** form that a current engine still emits on two lanes — replayed persisted bytes, which the service passes through verbatim rather than back-filling, and the service's own rejection envelope. A cause-form payload that also carries stale flat keys must ignore them entirely: keeping one compatibility read is what gives a single fact two sources. The same file also pins the MCP delivery verdict and HTTP status riding the wiring manifest, the four-state write-protection reading (where three of the four states mean *cannot tell*, and none of them may be printed as "there is no table"), and the park-reopen fetch identity: that predicate is asserted through the **real entry point**, since the defect being fixed was precisely a call site wired to a different predicate than the one that routed the row there |
|
|
240
|
-
| `scripts/run-auto-mode-unavailable-test.mjs` | The fact behind "you are being asked because the auto-mode classifier could not run", and the one place its sentence is minted. Two cause axes exist upstream and the guard pins them apart: why one classification round failed, and why the session breaker latched. They overlap on two words and each owns a third, so folding them into one table would throw the distinction away — and one of those third words, `parse_error`, is stamped nowhere near this fact, because the classifier did run and did answer, just outside its contract. The reader therefore narrows on the availability axis and treats that word as absence rather than inventing a failure the engine says did not happen. One reader serves both the live ask and its durable parked twin, since the two carry the same key path and a second copy is how two ledgers drift apart. Absence is pinned as absence — most asks never consulted a classifier at all — and the sentences are checked mutually distinct, prototype-safe, and reconciled against the engine's own bytes |
|
|
240
|
+
| `scripts/run-auto-mode-unavailable-test.mjs` | The fact behind "you are being asked because the auto-mode classifier could not run", and the one place its sentence is minted. Two cause axes exist upstream and the guard pins them apart: why one classification round failed, and why the session breaker latched. They overlap on two words and each owns a third, so folding them into one table would throw the distinction away — and one of those third words, `parse_error`, is stamped nowhere near this fact, because the classifier did run and did answer, just outside its contract. The reader therefore narrows on the availability axis and treats that word as absence rather than inventing a failure the engine says did not happen. One reader serves both the live ask and its durable parked twin, since the two carry the same key path and a second copy is how two ledgers drift apart. Absence is pinned as absence — most asks never consulted a classifier at all — and the sentences are checked mutually distinct, prototype-safe, and reconciled against the engine's own bytes. A cause word this client has never seen is now KEPT rather than dropped: the word table's owner is the engine, so narrowing to a copied list here would discard a legal value the day upstream adds one — and the value discarded is precisely "this outage is a NEW kind". The reader's only exclusion is derived rather than copied — the words that belong exclusively to the breaker axis — so the one word the engine says it never stamps here still reads as absence, and a future breaker-only word follows automatically. The guard walks the unknown word all the way to the sentence a person reads (the fallback that names it verbatim, which was unreachable before) and to the status reading (unavailable for this round, never a fallback to "available"), with a counter-control proving neither assertion is vacuous |
|
|
241
241
|
| `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 |
|
|
242
242
|
| `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 |
|
|
243
243
|
| `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 |
|
|
@@ -259,7 +259,7 @@ public-surface guard checks that last one).
|
|
|
259
259
|
| `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 |
|
|
260
260
|
| `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. Driven against a real fake-engine HTTP server through the shipped dist |
|
|
261
261
|
| `scripts/run-shell-gate-durable-allow-test.mjs` | #110: the durable approval leg for **shell** gates. The tool_end HOLD/REJECT predicate must cover Bash the same way park detection already does (otherwise the park poison frame `Operation aborted` hits the transcript, `endedCalls` swallows the real replayed result, and the user who pressed Yes watches a command that really ran be reported as aborted); a replayed, already-decided park must resume reading the stream instead of being reported as a failed turn; `lastEventId` must track numeric `seq` too. Mutation-proven: each of the three fixes reverted turns the gate red |
|
|
262
|
-
| `scripts/run-hitl-gate-honesty-test.mjs` | [2393] the four HITL disciplines that a passing type-check cannot see. (1) The park predicate and the `tool_end` predicate must cover the **same** set — the park side admits a first-class `kind:'tool_approval'` gate for *any* tool name, and a `tool_end` frame carries no `kind`, so the frame-level judge falls back to the engine's exact abort marker; otherwise the poison frame hits the transcript and `markEnded` swallows the real replayed result (the #110 disease, reopened on kind-only gates). (2) The already-decided identity criterion is **one-shot**: its two inputs are monotonic, so without consumption one successful decide makes every later park failure — including a real `approvals.list` outage — read as "already resolved" until the 24-hop budget runs out and reports a cause that has nothing to do with what happened. (3) A `plan_review` card dismissed without an answer must be re-presentable: the idempotent re-arm short-circuit re-publishes the still-armed card, and a stale armed id (responder gone) re-arms from scratch rather than presenting a card nobody can answer. (4) `HitlSafetyError` is a safety signal — the `remember` fallback arm must re-raise it instead of auto-retrying the decide, while a plain unknown-key 400 still falls back. (5) The polling leg reschedules after an escaping throw and flips `mode()` to `idle` once it consistently fails, so the honesty surface stops reporting a dead feed as live |
|
|
262
|
+
| `scripts/run-hitl-gate-honesty-test.mjs` | [2393] the four HITL disciplines that a passing type-check cannot see. (1) The park predicate and the `tool_end` predicate must cover the **same** set — the park side admits a first-class `kind:'tool_approval'` gate for *any* tool name, and a `tool_end` frame carries no `kind`, so the frame-level judge falls back to the engine's exact abort marker; otherwise the poison frame hits the transcript and `markEnded` swallows the real replayed result (the #110 disease, reopened on kind-only gates). (2) The already-decided identity criterion is **one-shot**: its two inputs are monotonic, so without consumption one successful decide makes every later park failure — including a real `approvals.list` outage — read as "already resolved" until the 24-hop budget runs out and reports a cause that has nothing to do with what happened. (3) A `plan_review` card dismissed without an answer must be re-presentable: the idempotent re-arm short-circuit re-publishes the still-armed card, and a stale armed id (responder gone) re-arms from scratch rather than presenting a card nobody can answer. (4) `HitlSafetyError` is a safety signal — the `remember` fallback arm must re-raise it instead of auto-retrying the decide, while a plain unknown-key 400 still falls back. (5) The polling leg reschedules after an escaping throw and flips `mode()` to `idle` once it consistently fails, so the honesty surface stops reporting a dead feed as live. (6) The live-frame leg carries the fact behind "you are being asked because the auto-mode classifier could not run" all the way to the card port. Transit narrows on SHAPE only — a non-empty cause string is taken verbatim, an open set, because the word table's owner is the engine and re-checking a closed table at the package boundary would drop a legal value the day a new cause word appears, which is exactly the information worth keeping. A malformed carrier degrades to absence rather than half-minting, and absence stays absence: it covers "the classifier answered", "this ask never qualified" and "this deployment has no classifier" at once, so nothing may render it as reassurance. The guard also pins the division of labour that makes the open set safe — the same word that transits is judged again by the public display reader, which narrows to the availability axis, so a word the engine says it never stamps on this fact renders no sentence while still being visible on the card for triage |
|
|
263
263
|
| `scripts/run-park-hop-progress-test.mjs` | L-80: the park re-attach loop budgets **stalled** rounds, not parks. A turn where the model keeps hitting gates and every one of them is really decided (a card was answered, the engine really moved on) must never be cut off by the hop budget — the budget counts consecutive rounds that produced no progress, and "the engine revived and immediately parked again on the same coordinates" is not progress. The three non-progress arms (already-resolved, decide-transport-exhausted, and a re-scan that was adopted but led nowhere) share one same-cause limit instead of one arm having a limit and the others having none, and every non-progress re-attach is announced once through the host callback rather than only to the debug log. When the limit is spent the resolver reads the approval queue once more and puts whatever is decidable in front of the user before it gives up; only when there is genuinely nothing to show does it fail soft, and the terminal message then carries the real cause and a real way out instead of a sentence about a budget. On the self-heal side, a reopen verdict that reports `decidedWithoutCard` — the chain settled the gate by rule, so there was no card to present — is progress, not a reopen failure, and the user is not told their message was NOT sent. Negative control: a genuinely empty queue with a run that never moves still fails soft |
|
|
264
264
|
| `scripts/run-notif-fleet-honesty-test.mjs` | [2393] the five notification/fleet disciplines a green type-check cannot see, each proven by reverting the fix. (1) The workflow-side dedup `return` keeps a count and a trace — without it "suppressed by design" and "a real completion swallowed because the runId minting changed" are the same observation. (2) `seq` normalisation has exactly one mint point, so a 0-based or fractional wire `seq` cannot make the watcher lane and the frame lane key the same completion differently (which would feed the model twice). (3) The TTL sweep defers to a probe arm that is still inside its own deadline — an entry recorded as "abandoned" must not be delivered a moment later — while an arm that has outlived its deadline never blocks the sweep, so the headless exit gate keeps its liveness. (4) The reset hook really clears every ledger it claims to (the sticky `prompt` ledger leaked across cases). (5) The fleet ledger counts all three drop paths (malformed / unknown frame type / isolation drop), and the panel projection's settled recycling is anchored on the settle instant and skips still-present rows, so the dedup token is never carried off with the entry (which would re-emit `end`) |
|
|
265
265
|
| `scripts/run-public-surface-test.mjs` | The outward promises: the npm export surface baseline (an **exact set**, both directions — a new export that never entered the baseline is one nobody watched leave, and deleting it later would not be red), the peer floor witness, and this README's claims |
|
|
@@ -269,7 +269,7 @@ public-surface guard checks that last one).
|
|
|
269
269
|
| `scripts/run-client-core-singleton-test.mjs` | Module-level singletons ⇄ `docs/refactor/p1-scan/singleton-manifest.json`, **both directions**: an unregistered singleton is red (registering it forces someone to answer "what if this got duplicated"), a stale entry is red, and the `dupRisk: high` count only goes down |
|
|
270
270
|
| `scripts/run-catalog-loader-gates-test.mjs` | The model-catalog candidate chain (`loadCatalogWithSources`) and the provider device-code seam: offline ⇒ `bundled` with an honest `online.reason`, a good source ⇒ `online` plus a cache write, a second offline run ⇒ `cacheHit`; the three hostile source shapes (malformed JSON, `schemaVersion: 99`, off-domain `http`) each fall through to the bundled table, and an off-allowlist target is **never dialled** — including a `302` to another host, proven by a real loopback server's hit counter staying at zero; a one-byte edit to `catalog.sha256` drops that source while an unavailable sidecar only warns; and the device-code poller's `pending → ok` / `expired` arms run against a real loopback HTTP server with an injected clock |
|
|
271
271
|
| `scripts/run-abortable-sleep-test.mjs` | The shared `abortableSleep(ms, signal)` leaf (consumed by `workflowClient.ts` and `agentSession/backgroundView.ts`'s poll backoff): normal timeout resolution, immediate wake-up on `abort` mid-wait, `clearTimeout` really firing on that path, and a post-resolve late abort staying a no-op |
|
|
272
|
-
| `scripts/run-durable-card-display-keys-test.mjs` | The durable approval row's two display keys survive the row→card recast in `surfaceFsApprovalAndDecide`: `governanceForced` stamps on strict `true` only (absence is "no evidence", never `false`), `ruleSuggestions` passes through the same shape-narrowing reader as the live-frame leg and lands on the **read-only** card key — plus a standing pin that the durable leg never stamps the redeemable `ruleSuggestions` card position (the `/decide` body has no rule slot; offering a "don't ask again" option there would be an affordance nothing can honour) |
|
|
272
|
+
| `scripts/run-durable-card-display-keys-test.mjs` | The durable approval row's two display keys survive the row→card recast in `surfaceFsApprovalAndDecide`: `governanceForced` stamps on strict `true` only (absence is "no evidence", never `false`), `ruleSuggestions` passes through the same shape-narrowing reader as the live-frame leg and lands on the **read-only** card key — plus a standing pin that the durable leg never stamps the redeemable `ruleSuggestions` card position (the `/decide` body has no rule slot; offering a "don't ask again" option there would be an affordance nothing can honour), and a section for the parked twin of the classifier-unavailable fact: the upstream declares that key on the parked action itself, verbatim and under the same name as the synchronous ask, so this leg reads it rather than guessing a carrier name the way the deliberately unprojected keys must. The guard drives both legs with the same cause and asserts the card ends up byte-identical either way — the observable consequence of one reader serving two key paths, and the thing that silently diverges the day someone writes a second copy. Its own reach is printed rather than implied: what is proven is the package-boundary promise "on the row ⇒ on the card", not that today's engine flattens that key onto the pending row. A further section covers the two display facts the recast had been dropping for far longer. One of them the row has carried all along under a DIFFERENT NAME than the live frame uses — the frame puts it at the top level, the row nests it under the risk descriptor — and that difference in name is exactly why it went unnoticed; unlike the keys this leg deliberately refuses to project, its carrier is witnessed in the engine's own artefact rather than guessed. Neither is decoration: the shell's stand-aside arm reads them, so a call that matched a remembered allow rule which could NOT silence it looked like an ordinary ask on the durable path and was auto-approved with no card at all. Both land on the SAME card slot the live leg uses (one shape for the ends), verbatim bytes, present only when non-blank, never folded into an empty string — and the guard pins the discipline in both directions, including that a top-level key the upstream row does not actually have must still not grow this position |
|
|
273
273
|
| `scripts/run-session-memory-status-test.mjs` | The session **memory-status** read face (S-53): the two judgements three clients would otherwise each get wrong. First, *same status, different code* — this route's 404 carries two unrelated meanings (`not_found.session` = unknown or non-owned session; `not_found.route` = a pre-7.53 server that has no such route at all), so dispatching on the **status** would report "your deployment lacks this surface" as "your session does not exist". The verdict is anchored on `errorCode`, the two 404s are pinned to **different** verdicts, and — the load-bearing negative control — a 404 carrying **no** code falls to `failed` rather than guessing either way, since a wrong guess in either direction is a false statement a user would act on. 501 is allowed a codeless fallback because both of its arms mean the same thing here, and `capability.*` stays split from `feature.*` because those two share a status while their dispositions are opposite. Second, *absence means something different per key*: `optOutSource` and `lastCaptureAt` are legitimately absent on a **healthy** session (a zero-history session really is `{captureOptedOut:false, committedCount:0, foldedCount:0}` with no degradation at all), so reading absence as "off/none/0" asserts something unprovable. Two combined readers are pinned: capture opt-out is read from **both** its keys (a record-store fault yields `indeterminate`, never `active` — the difference between "your conversation is being remembered" and "nobody knows"), and last-capture is a **three-state** read whose discriminator is the *other* key, because `lastCaptureAt`'s absence alone covers both "ledger unreadable" and "genuinely no contributions" and therefore decides nothing; the two shapes are pinned to different verdicts so a single-key read turns red. The thin wrapper is the only IO: it never throws, drops malformed keys to absence rather than trusting them (an unreadable value must answer "don't know", never render as truth), refuses to spend a request on an empty `sessionId`, and passes `signal` through untouched |
|
|
274
274
|
| `scripts/run-crash-converged-projection-test.mjs` | The `crashConverged` read face on `GET /v1/approvals` (L-38): what the *previous life* of a crashed local engine left behind, projected for every client. Three judgements are pinned. First, **absence is not an empty list** — a missing key (an older server, deps not present, or a carrier that is not an array at all) returns `undefined`, and the client renders nothing; an empty array returns a present zero-count object, which is the server actually saying "none". Folding the first into `{total:0}` would have the client assert "nothing was left behind" on a surface a person uses to decide whether it is safe to re-run something — the worst possible direction for a false statement — so the two cases are pinned to different **return shapes** and a test asserts the two verdicts are unequal. Second, bucketing is a **four-term conjunction**: `orphanState === 'pending'` *and* `resumeSafe === true` *and* both approval-evidence keys (`originalDecision`, `decidedAtMs`) absent. A fifth term rejects any row carrying an **accessor**, and accessors are never invoked at all — reading one means synchronously running someone else's code, and `catch` catches throwing, not *never returning*, so a looping getter would pin the startup thread forever (the row cap does nothing against that shape). The same rule covers the three untrusted reads outside the row as well — the envelope's `crashConverged` key, the carrier's `length`, and every numeric index are read as own property *descriptors* and only data descriptors are used, so accessors and prototype entries read as absent and are never invoked. Such a key is treated as absent: if it was a required field the row is counted as dropped, if it was optional or additive the row survives without it. That also closes the ordering attack, since spreading runs getters in property order and an earlier one could `delete` the approval evidence before it is ever copied (measured before the fix: such a row reached the resume-safe bucket), and the check therefore moves ahead of the read, onto the property descriptors — from which the snapshot is then built directly, because checking descriptors and *then* spreading is two independent observations of the same row, and a non-throwing proxy can make the two `ownKeys` calls disagree (first showing `originalDecision: 'approve'` so the row reads as plain data, then omitting that configurable key so the snapshot loses the evidence; measured before the fix: the dangerous row reached the resume-safe bucket after exactly two enumerations, and after it, one). Keys are written with `Object.defineProperty` rather than plain assignment, because `'__proto__'` is a legal own enumerable key and `o['__proto__'] = x` does not store a value — it calls the prototype setter, letting a row whose own properties are all plain data (so the accessor gate never fires) inject a prototype whose `sessionId` getter deletes the approval evidence from the snapshot during validation; `defineProperty` fires no setter, so the key survives as ordinary additive data and the snapshot keeps `Object.prototype`. A row that simply arrives with a custom prototype is treated the same way, since the snapshot only enumerates own properties: approval evidence sitting on the prototype would never reach it, and a perfectly ordinary object with no proxy and no accessors could otherwise be called safe to re-run — real bodies come from `JSON.parse` and always carry `Object.prototype`, so nothing genuine trips it). Validation itself runs on a **null-prototype** dictionary and the bucketing verdict is carried out of that same pass rather than re-read from the delivered row, because every property lookup on an ordinary `{}` reaches `Object.prototype`: a polluted `sessionId` getter there would delete the approval evidence from the snapshot mid-validation and send the row to the safe bucket (measured before the fix). The row handed to the client is still an ordinary object — the null prototype is an implementation detail of the check, not of the value) — real JSON bodies are all data properties, so only a middle-layer-synthesised payload ever trips it, and it too lands in the human bucket rather than being dropped. The `decided` arm means the human had already approved and side effects may be half-landed, so it always goes to the human bucket, as does `resumeSafe === false` and — the last two terms — any row whose own fields contradict each other, since `pending` claims nothing ran while that evidence says somebody pressed approve. Deciding "not safe" costs one extra question (recoverable); deciding "safe" wrongly has somebody re-run work that already partly happened (not). A 2x2 truth table pins that exactly one cell is resume-safe, so reading either key alone turns red, and the contradictory rows are routed to the human bucket rather than dropped — they are real orphans, and the ones most worth showing. Third, unreadable rows are **dropped and counted**, never thrown and never passed through: the product is declared as `CrashConvergedRow`, so letting a row missing a required field — or carrying one of the wrong type — past would be a lie at the type level, and the closed literal discriminators (`decision` / `cause` / `orphanState`) decide family membership rather than being an open vocabulary. The measuring stick stops at the **type** floor, though: degenerate-but-well-typed values (`ts: NaN`, an empty `toolName`) are kept, because swallowing a real orphan over a decorative field is the worse direction, and the one deliberate exception is `approvalId`, which must be non-empty to be a row identity at all. `dropped` is kept separate from `total` so unreadable rows never inflate "N approvals were affected"; each row is a **one-shot snapshot** — every own enumerable key is read exactly once, and validation, bucketing and the handed-back value all read that same snapshot, so additive upstream keys survive while a **non-idempotent** getter (one that never throws, just answers differently on a second read) can no longer erase the approval evidence between the check and the bucketing (measured before the fix: such a row landed in the resume-safe bucket while its checked value was `"approve"`). Hostile carriers are counted rather than allowed to reject: **every** touch of the carrier is guarded — envelope property reads, `Array.isArray` itself (it throws on a revoked proxy), the `length` read, each indexed read and each row's property reads — and a traversal that dies halfway returns absence rather than a half-counted total. A row that cannot be read never takes the batch with it: its own shape check is inside its own guard, so one revoked-proxy row costs a `dropped` tick rather than collapsing the whole projection to absence — which a client would have read as "this deployment does not offer the surface". Traversal goes by **numeric index, never the carrier's own iterator protocol**, because `for...of` hands the carrier the question of which rows exist: an array carrying an overridden `Symbol.iterator` can yield nothing (measured before the fix: a real orphan became `{total:0}`, which a client reads as "the server said there are none") or swap a dangerous `decided` row for a safe-looking one (measured: `fake-safe` was returned in place of `real-danger`). Row count is capped at 100000 and the cap is checked **before** the walk: requiring only a non-negative integer `length` does not stop a proxy trap reporting a billion, and this surface runs on the startup / `--resume` path, where a synchronous spin freezes the thread (measured before the cap: twenty million rows took 18.3 seconds and twenty million index reads; a billion does not come back). The honest boundary is stated rather than overclaimed — a proxy can still lie in its `length` or index traps, which is the same thing as a host injecting a lying transport — and the widening of `ApprovalsResourceLike.list()` is proven **additive** by really running tsc over a legacy `{ pending }` mock *and* over the real `AgentClient` path — the projector takes `unknown` precisely because a parameter shaped as "an object with an optional `crashConverged`" is a TypeScript weak type that the installed SDK's own `list()` return shape shares no property with, which only a real-client compile would have caught — with a known-red control so a clean run means the checker spoke |
|
|
275
275
|
| `scripts/run-self-orchestration-denial-test.mjs` | The three judgements behind a **denied self-orchestration request** (server 7.57.0), each of which all three clients would otherwise get wrong on their own. First, whether to retry at all is a **conjunction that may not be loosened**: HTTP 501 *and* an `errorCode` that is **exactly** `capability.self_orchestration_required`. That code shares its shape with every other `capability.*` 501, so dispatching on the prefix would drag "some other capability is not wired up" into the retry arm — those requests do not become acceptable once the two keys are gone, so the client would spend a request and then tell the user the wrong reason. Negative controls cover all four directions: a sibling `capability.*` code, a truncated or suffixed variant of the right one, a codeless 501 (it decides nothing, so it decides nothing — no guessing), and the right code under 500 / 400 / 503 or a string `"501"`. The classifier reads structurally rather than by `instanceof` (a host may inject its own transport; across realms or duplicate SDK instances an understandable error would read as unreadable), so a class instance, a bare `{status, errorCode}` literal and an error carrying those fields on its **prototype** all reach the same verdict — and a hostile proxy or a throwing getter yields `null` instead of throwing, because this classifier runs inside a `catch` block where anything it throws escapes the caller's own guard. Second, removing the intent is a **structural** operation, not wording: `selfOrchestration` sits at the top level while `ultracode` sits under `settings` — two different stamping legs — and a client hand-writing `delete` will miss the second one, which costs the user the same failure twice. The single stripper is pinned to touch exactly those two: other `settings` sub-keys and their values survive byte for byte, `deferTools` is left alone (pulling `Workflow` out would be a behaviour change, not a removal of intent), additive unknown keys survive at both levels, the input object is never mutated, `settings` is only dropped entirely when `ultracode` was really there and nothing else remains (an already-empty one is left as is), a non-object `settings` is not touched at all, an `ultracode` that only exists on the prototype does not count, and the whole thing is idempotent. The end-to-end leg runs a real `buildTaskRequest` product through it and asserts the stripped body still passes the registration gate key by key. Third, on the capabilities body, **absence is not "switched off"**: a pre-7.57 server has no `workflowsGate` key at all, so reading absence as "the engine says no" asserts something the server never said, and the mirror-image disease is folding an **unrecognised** `denial` into `null`, which would have the client render "nothing was denied" when the truth is "denied, for a reason I do not recognise". Five shapes are pinned — caps unreadable, gate absent, closed-set member, unknown value, accessor — with the unknown arm carrying the raw token (or an empty one when the value is not even a string) and never collapsing to `null`. All four untrusted reads go through own **data descriptors** only, and the guard pins the getter invocation count at zero, since `catch` catches throwing but not *never returning*; a descriptor trap that throws and a revoked proxy both yield honest absence rather than an exception — though *what* absence means differs by field, and the guard pins that split rather than a blanket rule: an accessor on `workflows`, `workflowsGate` or `engineCan` reads as absent, while an accessor on `denial` reads as `{unknown:''}`, because a key that is **not there** is the gate saying "nothing was denied" whereas a key that is there but cannot be read is "denied, and I could not read why" — folding the second into the first is exactly the false statement this face exists to prevent. Two further pins came out of an adversarial review. The exported retry list is **frozen at runtime**, not merely `as const`: the verdict hands out that same reference, so any consumer splicing it once would poison every later verdict in the process — the guard asserts `Object.isFrozen`, that four different mutation attempts leave it byte-identical, and that a verdict issued *after* those attempts still carries the original two entries. And the classifier reads `denial` only **after** both criteria have passed, since it is not a criterion but an extra field on the verdict: the guard pins the getter invocation count at zero for any error that does not match and at most one for an error that does. The scope line is drawn explicitly rather than overclaimed — "no getter ever runs" holds for `projectWorkflowsGate`, which reads **wire JSON** where every field is an own data property by definition, but not for the classifier, which reads a **thrown value** that may well be an SDK `APIError` class instance carrying `status` and `errorCode` on its prototype; insisting on own data descriptors there would report a perfectly readable error as unreadable, so that side promises only that it never throws. A final pin covers the **integration document's own worked example** rather than the library: the shipped SDK's `tasks.stream()` is an `async` generator, so calling it issues no request at all — the POST happens inside `streamRaw` on the first iteration, and a `try` wrapped around the `stream(...)` call itself can never catch the 501. A client following a submit-shaped recipe on the streaming leg would never run the classifier, and the whole strip-and-retry path would silently do nothing. The guard drives the **real** `TasksResource` against a fake transport, offline, and pins both halves: the synchronous leg is in flight the moment it is called, the streaming leg has issued zero requests after the call and raises on the first `next()` — and it does so through the **real** error path, with `openStream` returning an actual 501 `Response` that the SDK's own `errorFromResponse` turns into the typed error, pinning the `openStream`→`errorFrom` call order so a transport that stops minting `errorCode` cannot pass. The documented recipe is then **executed** rather than keyword-counted: exactly one retry, a second body that really lost both keys while every other setting survives byte for byte, the caller's own request object left untouched, one disclosure and only one, a second 501 propagating with the request count still at two, and — after the first 501 — an abort leaving the count at one with nothing disclosed. A last leg is type-level: `stripSelfOrchestrationIntent` carries an SDK `TaskRequest` overload, because the wide `Record<string, unknown>` form erases the caller's type and the document's "strip and resubmit" line would not compile without an unsafe cast; a real tsc run over a virtual file proves both the narrow and the wide path, with a known-red control — and it compiles the document's two recipes **verbatim**, extracted from the section itself, because a recipe that does not compile is a recipe that was never given: `{ transientOk: true, signal }` is a TS2379 under `exactOptionalPropertyTypes`, which no amount of prose review had caught. The last thing pinned is the one that would have been quietest of all: the SDK's `stream()` returns only on a `done` or `failed` frame, so a stream truncated mid-run — or yielding nothing at all — ends the `for await` just as normally as a completed one. The documented `runOnce` therefore tracks whether it ever saw a terminal frame and raises when it did not, the guard's success fixture emits a real terminal and asserts the handler received it, and a truncated-stream control asserts that shape is reported as a failure with no retry and nothing disclosed. That terminal-frame rule then needed one more turn of its own: the underlying reader returns *normally* when the signal is aborted, so the check as first written rewrote a user's cancellation into a generic stream fault — a client keying off `AbortError` to suppress the error would instead have shown a failure, or resubmitted. Cancellation is therefore checked first, a real-SDK case aborts from inside the handler and asserts the original `AbortError` survives with no retry and nothing disclosed, and the document is checked for that ordering. The harness runs the documented `handle` and `transcript.note` as real spies rather than pushing frames itself, the drive loop rethrows exactly as the document does, and the disclosure ledger is proven to be the caller's own array by a positive identity assertion — without which the cancellation leg's "nothing disclosed" would have been vacuously true. Each recipe is compiled **on its own**, with a preamble that declares only what a host supplies and injects no library symbol, since compiling them together let the second one borrow the first one's imports, and the preamble's own types are decoupled from what the recipes import so the "remove the imports and it must fail" control fails for the right reason — which is checked by attribution, not merely by redness. Ordering is the last thing to get right: the cancellation check must come before the truncation error but **both** must sit behind the terminal-frame test, because a cancellation that lands after the run already reported `done` would otherwise overwrite a real outcome — one that may have already had effects — with "cancelled", and a person reading that will run it again. Aborting from inside `handle(done)` and `handle(failed)` are both pinned to still report success, and the ordering assertion is anchored inside the streaming `runOnce` body rather than the section, since the section's first `throwIfAborted` belongs to the synchronous recipe and would have made a reversed streaming recipe pass — and that ordering check is now anchored on the TypeScript AST rather than on text, since a comment reproducing the two statements in the right order let a genuinely reversed body pass. One more timing fact had to be written into the recipe: a single SSE read buffers several frames and the SDK yields them back to back, so checking the signal only after the loop lets a cancelled run keep consuming the rest of the chunk — measured, an abort inside `handle(turn_start)` still swallowed the `done` that followed and reported success. The recipe therefore re-checks after every non-terminal frame. Finally, the behavioural matrix is no longer run against a copy of the recipe: both recipes are extracted from the document, transpiled, and **executed** with injected host objects, so the disclosure assertion really exercises the document's own `transcript.note(disclose(...))` line, and the synchronous leg gets the same full matrix the streaming one does |
|
|
@@ -280,7 +280,7 @@ public-surface guard checks that last one).
|
|
|
280
280
|
| `scripts/run-park-decision-layer-test.mjs` | The decision layer behind the "stuck behind a card" family, shared by every client. A pending row that is **not in the queue** is three states, not one: a bounded, interruptible re-probe loop distinguishes *a decidable row*, *not born yet* (no positive evidence that anything settled — an empty queue proves nothing) and *settled elsewhere*, always probes at least once so a zero budget keeps the pre-fix semantics verbatim, cuts a hung read face off at the window rather than only noticing afterwards, and reports the honest failure when the window is spent instead of inventing a decision. The decision-note reader is likewise three-state: an explicit `noteRecorded: false` outranks an echoed note body, absence renders **no line at all**, and untrusted note text is flattened and bounded before it ever reaches a renderer. Row routing anchors on the deciding quantity — a row carrying `gateKind: "human"` with `toolName: "Write"` is a tool gate, because `human` is the engine's *generic* "someone must decide", not a synonym for a question — and the queue scan refuses to surface a row it cannot positively prove belongs to this session. A chain that fails after the row vanished is split by whether a card was ever presented: decided-elsewhere, or not-its-turn-yet. A row-level single-flight makes "at most one card per pending item" structural rather than incidental. The resume three-way card pins the option **order** (the zero-effect choice sits at index 0, because the frame carries no default-focus field and a stray Enter must not attach or cancel), renders only options the wired verbs can honour, collapses every ambiguous answer to zero action, omits the liveness line entirely when the engine gave no evidence, and — when there is no card lane at all — prints three real routes and exits on a dedicated code rather than reporting success |
|
|
281
281
|
| `scripts/run-selfheal-reopen-test.mjs` | The 409 active-run self-heal decision chain: `governanceForced` narrows on strict `true` only; triage prefers the wire's `pendingGate.kind` and falls back to the status table (an off-table kind is never guessed into a card arm — hands-off plus the honest wording); a first-sight card makes zero closed/reopened claims and a host presentation receipt of `presented: false` demotes the outcome to reopen-failed; park-row ownership is a fail-closed positive proof (own-run ledger or session id — unprovable is not owned); the three gate-identity key literals live in exactly one mint (`hitl/gateIdentity.ts`, AST string-token scan); the armed-gate presentation ledger is per-session; and the `plan_review` reopen arm shares the arm arm's card body, three-state verdict and delivery pipe, consuming the presentation history once a decision is delivered. The same chain also carries the `running` three-way card: both plan-family gate kinds route to the plan arm and all four ask-family kinds to the ask arm (an off-table kind still never gets guessed into either); the card is offered only for verbs that can actually be honoured and a missing presenter means zero action rather than a silent cancel; a steer is sent **exactly once** with its three delivery outcomes worded apart (a `queued` receipt is the wire correcting the triage input, so the named park word decides which card gets reopened, and an unrecognised park word drives neither arm), and a steer failure is split into *provably not delivered* (4xx) and *delivery unknown*, because telling a user to resend a non-idempotent instruction that may already have landed is how duplicates get made. After a user-chosen cancel, "the session is free" is asserted only from a whitelist of terminal states — park states hold the claim, an unrecognised state word is not a release, a failed read is *unknown* rather than a release, and only a 404 counts as one — and the honest timeout line quotes how long it really waited |
|
|
282
282
|
| `scripts/run-terminal-identity-copy-test.mjs` | Terminal-state **identity**, in both lanes where a stop gets a name. A run stopped by this deployment's own governance knobs — the open-set `limits.*` family, `output.invalid`, and the `blocked` contract terminal a ReportBlocked agent produces — is not a provider failure, and labelling it `API Error:` sends the reader to check the network, the key and the quota when the handle is the `--max-turns` they passed themselves. Those terminals now render a neutral row; the reverse direction is guarded just as hard, because asserting "this is *not* an API error" on a code the package does not recognise is the same misfiling pointed the other way — a real `gateway HTTP 502`, a `conflict.session_active_run` and any unknown code all keep the `API Error:` prefix, and the row keeps its `isApiErrorMessage` class flag so brief-mode visibility filtering does not silently drop it. The second half is who the rejected submission belonged to: the self-heal copy told every caller "Your message was NOT sent … send it again", which is three separate untruths for a system injection (a plan-review outcome, a cron wake-up, a task notification) — not the user's message, and not re-sendable, since a host queue marks those non-editable and non-recallable. The injected form says so instead, and the one sentence that promises re-delivery is pinned to the single disposition that earns it: `selfHealSubmissionDisposition` is the same function the host consults before putting the item back on its queue, so the promise and the behaviour cannot drift apart, and the arms where no card could be surfaced state plainly that nothing was delivered and nothing will retry |
|
|
283
|
-
| `scripts/run-additive-key-passthrough-test.mjs` | The one disease shape behind two legs: a **closed whitelist / flattening arm** dropping a fact that is already on the wire, while both sides of the seam look correct. (1) The `task_progress` projection carries a registered **key ledger** — a frame populated with every key the service really projects is pushed through the shipped `eventToSdkMessage`, and the set of wire keys that survive must equal the registered pass-through list **name for name in both directions**, so quietly forwarding one more key is as red as quietly dropping one. `model` (the child run's model id, minted by core as `prepared.model.id` and projected by the server since 7.52.1) is the key this batch adds, with the same conditional the server itself applies: a non-empty string or no key at all — an empty string is neither a model id nor "unknown". The ledger is also checked against the fenced list in `docs/INTEGRATION-CLIENTS.md` §3d, so a doc that still says seven keys while the code forwards eight is red rather than merely stale. (2) The decide-failure arms carry the server's S-02 `currentPending` pointer key from a 409 `approval_stale` refusal onto the outcome the host reads. The reader is structural rather than `instanceof`, because the client is host-injected and the class identity is not this package's to assume; a half triple never mints (half a pointer cannot relocate anything), an empty string is not presence, and `checkpointToken` never transits. Both the allow and the deny leg are driven end to end through the real durable approval path — as is the accept-session leg, where a refusal carrying the pointer key must now re-raise instead of silently re-sending the human's answer for the **old** card as a plain approve (one decide call, pointer preserved), while a legacy 400 still falls back exactly as before — and all three flattening points must call the one shared reader — the same-shape residue check that makes "fixed one arm and left the twin" red instead of invisible |
|
|
283
|
+
| `scripts/run-additive-key-passthrough-test.mjs` | The one disease shape behind two legs: a **closed whitelist / flattening arm** dropping a fact that is already on the wire, while both sides of the seam look correct. (1) The `task_progress` projection carries a registered **key ledger** — a frame populated with every key the service really projects is pushed through the shipped `eventToSdkMessage`, and the set of wire keys that survive must equal the registered pass-through list **name for name in both directions**, so quietly forwarding one more key is as red as quietly dropping one. `model` (the child run's model id, minted by core as `prepared.model.id` and projected by the server since 7.52.1) is the key this batch adds, with the same conditional the server itself applies: a non-empty string or no key at all — an empty string is neither a model id nor "unknown". The ledger is also checked against the fenced list in `docs/INTEGRATION-CLIENTS.md` §3d, so a doc that still says seven keys while the code forwards eight is red rather than merely stale. (2) The decide-failure arms carry the server's S-02 `currentPending` pointer key from a 409 `approval_stale` refusal onto the outcome the host reads. The reader is structural rather than `instanceof`, because the client is host-injected and the class identity is not this package's to assume; a half triple never mints (half a pointer cannot relocate anything), an empty string is not presence, and `checkpointToken` never transits. Both the allow and the deny leg are driven end to end through the real durable approval path — as is the accept-session leg, where a refusal carrying the pointer key must now re-raise instead of silently re-sending the human's answer for the **old** card as a plain approve (one decide call, pointer preserved), while a legacy 400 still falls back exactly as before — and all three flattening points must call the one shared reader — the same-shape residue check that makes "fixed one arm and left the twin" red instead of invisible. (3) The same disease growing on the REQUEST side: the `.mcp.json` → server-spec projection rebuilds each server key by key, and the settings schema deliberately leaves some keys parse-transparent — whatever JSON the file carries reaches the engine untouched, because validating them where the whole domain parses all-or-nothing would let one bad declaration take every server down silently. The whitelist had no row for the newest of them, so an operator's per-tool declarations — the ones the write fence reads — were stripped at the package boundary while both sides looked correct. The criterion is not "is that key handled" but the transparent-key table read out of the INSTALLED schema at runtime, reconciled name-for-name against this leg's ledger, so the day upstream adds a third one this turns red and forces an explicit decision. Behaviour is pinned on both transports, by object identity rather than deep equality (a rebuild would be a second judge), and malformed values must transit UNCHANGED rather than be refused here — the engine refuses them loudly and names the server, whereas a package-side judge can only swallow a declared protection quietly. Absence still mints no key, unknown keys still never reach the wire (the fix is the dropped key, not the gate), and the one transparent key this leg deliberately does not forward is a ledger entry with its own exit condition: it belongs to the deployment plane, and the day the request-plane type declares it the entry's premise is gone and the gate says so |
|
|
284
284
|
| `scripts/run-esc-halt-plan-test.mjs` | The Esc stop decision every client shares: fire the **turn-level** halt first, and escalate to a **run-level** cancel in exactly two cases — the engine itself answered with a 409 from the closed code set (it is saying "there is no in-flight turn here; use cancel for a run-level stop"), or that shot came back with no verdict at all *and* the shell can independently prove a permission card was on screen. Everything else does not escalate. The asymmetry is the whole point and every negative control guards the same direction — deciding *not* to escalate costs the user one more choice on a busy-session card (recoverable), deciding to escalate wrongly tears down a run that was alive and takes every in-flight tool with it (not). So: the closed code set is a **frozen** value, not a `ReadonlySet` — type-level immutability does not stop a consumer's `.add()`, and the guard proves it by really trying to mutate the exported value and then checking the verdict did not drift; the escalation gate is the **conjunction** of that closed set and the 409 status, since honouring the code alone lets a 500 that merely quotes it drive a destructive call; `interrupt.not_held` and `steering.not_running` are deliberately outside the set (the first means *this replica* has no live face — the run may be perfectly alive on another); an unreadable code falls to the no-escalation side; a `parked` flag never overrides a verdict the engine did give, and only strict `true` counts when it did not. The first shot is unconditional by construction — it does not consult `parked`, because the 409 it earns is exactly the verdict the gate wants — and the verdict itself is a closed machine-readable reason word, not display copy. A third escalating case was added once tearing the stream stopped reaping the run: with detach armed, a shot that never lands leaves the run going all the way to the end of the turn, so the Esc the user pressed has no effect at all and nothing on screen says so — the old behaviour had a silent backstop (tearing the stream ended the run) and that backstop is gone. The new fact is held to the same three disciplines as `parked`: it is read only where the engine gave no verdict, it is judged **after** `parked` so an existing host's reason word does not change under it, and only strict `true` counts. Absence is proven to be a no-op rather than asserted — the guard carries its own reference implementation of the previous version's table, runs the full grid through both, requires zero divergence when the new field is omitted, and first shows the comparison really does report a difference on the one cell where the two versions are meant to differ |
|
|
285
285
|
| `scripts/run-peer-frame-projection-test.mjs` | The three engine-injected lanes design/385 puts on the **one** `task_notification` carrier, which are not the same kind of thing at all: a delegated child's uplink (`agentMessage`), another session's message drained from this session's own box (`crossSessionMessage`), and a receipt about one of *this* session's own outbound messages (`crossSessionNotice`). The engine renders none of them inside a `<task-notification>` shell, so a client that projects them as the generic completion card shows "background task finished" while the model read a colleague's sentence — two faces describing different events. The discriminator is pinned to the **typed carrier being present**, never to the `summary` text: those carriers can only be minted by the engine's injection legs (the external `notify()` input is a strict subset of the payload and can wear none of them), while `summary` is filled by every notification there is — so anchoring on text would let any background task impersonate a colleague's message by writing `<agent-message from="…">` into its own summary, and a positive control asserts exactly that payload still projects as the generic card. Fail-closed has two tiers rather than one: a broken **required** field (empty `from`, a non-string `body`, a notice `kind` outside the closed set) returns absence so the caller falls back to the generic card — an honest downgrade where the user still sees the notification — while a broken **optional** field drops only itself, because losing an attribution note and losing a colleague's whole message are not the same magnitude. The provenance side record is **required and must agree on four points** (`kind` matches the lane; `from`/`taskId`/`seq` are present and equal the carrier/payload — each equality is anchored on a core mint site and pinned by the cli wire-anchor A-K24), so a carrier signed with a trusted name but a disagreeing provenance falls back to the generic card; peer bodies pass the same authority-envelope neutralization core applies (`<task-notification>` etc. are defused) so a colleague's text can never seed the resume dedup ledger. Lane precedence copies the engine renderer's own order, because the model already read the frame in that order and a client ordering of its own would put a card on screen that disagrees with the frame the model saw. Rendering and parsing of the transcript line live in the same module and are round-tripped in both directions, including a body carrying a forged closing tag (a parser fooled there hands half a message to the next row) and a quote inside the sender label (which must not forge a second attribute); the notice lane is deliberately kept **out** of the parser, since recognising it would mean anchoring the `[Cross-session …]` prefix and a user typing that same line would be rendered as engine speech. Hostile carriers are read as own **data** descriptors only and accessors are never invoked at all — `catch` catches throwing, not never returning — proven by a counting getter that must stay at zero calls, alongside a revoked proxy and a prototype-only carrier; and four legacy payload shapes assert the no-carrier path is byte-identical to before, which is the executable form of "zero difference for an older host" |
|
|
286
286
|
| `scripts/run-wiring-manifest-projection-test.mjs` | The two end-user facts carried on the engine's `wiring_manifest` frame (`modelGate`: which tools this run's model gate removed and the verbatim restore hint; `autoMode`: whether auto mode is actually armed and the engine's own reason word). Projection: both sections ride as `_sema_`-prefixed superset keys, verbatim, and no SDK-named key is minted; a frame where neither section is well-formed projects to `none/not_in_slice` (no empty arm); `modelGate` needs all three keys and treats `removed: []` as a bad value rather than a reading; `autoMode` needs a boolean plus a non-empty reason that agrees with it, and the reason word is never mapped onto the capabilities vocabulary; the frame is flat (a nested `manifest:{}` wrapper is not a supply); `eventId` rides like every other arm. Adapter: exactly one chrome event on the main lane, a sub-flow frame (any `parentToolCallId`, `null` included) yields nothing, and an absent `eventId` leaves the key absent. Added at receiving time because the shell-side gate could not see this package's behaviour: two mutations (empty `removed` accepted, sub-flow gate removed) had passed the package suite untouched |
|
|
@@ -17,8 +17,12 @@
|
|
|
17
17
|
* 两集交于 `error` / `timeout`,各有一个独占成员。合成一张表就把两条轴的差别扔了。
|
|
18
18
|
* 🔴 **`parse_error` 只在熔断轴上**:core 顶注逐字 ——「`parse_error` stamps nothing」:分类器
|
|
19
19
|
* **跑了并且答了**,只是答在契约之外,那是**另一句话**。所以它**一个字节都不 stamp** 到
|
|
20
|
-
* `classifierUnavailable`
|
|
21
|
-
* 把它读成一个「没跑成」的成因就是替引擎编一件它明说没发生的事。
|
|
20
|
+
* `classifierUnavailable` 上;本读器把**熔断轴独占的词**({@link BREAKER_ONLY},今天 = `parse_error`)
|
|
21
|
+
* 判**缺席** —— 把它读成一个「没跑成」的成因就是替引擎编一件它明说没发生的事。
|
|
22
|
+
* ⚠️ **0.64.2 订正(行为面)**:修前这只读器按 unavailable **闭集**收窄,于是一台比本端新的引擎发一个
|
|
23
|
+
* **合法的新成因词**时,卡上那一行整段消失(而 {@link classifierUnavailableDetail} 的兜底句因此
|
|
24
|
+
* **永远不可达**)。现改为「非空串即收 + 排除熔断轴独占词」——「表外」与「另一条轴上的词」是两件事,
|
|
25
|
+
* 修前把它们判成了同一件。`parse_error` 的行为一字未变(黑盒判据 G-19 照旧成立)。
|
|
22
26
|
*
|
|
23
27
|
* -- 为什么是镜像而不是 import ---------------------------------------------------------------
|
|
24
28
|
* 这两张表与 `classifierUnavailable` 的型面在 sdk 8.8.0 与 agent-types 上**都还没有**(亲验:两棵树
|
|
@@ -79,10 +83,19 @@ export interface ClassifierUnavailableView {
|
|
|
79
83
|
* 🔴 **两处同一只读器**:`AskRequest.classifierUnavailable` 与
|
|
80
84
|
* `PendingAction.tool_approval.classifierUnavailable`(durable park 行的孪生位)**键路同形**,
|
|
81
85
|
* 所以一只读器吃两处 —— 各写一份就是两份台账各漂各的,本包一贯要根治的形。
|
|
82
|
-
* 🔴 **`cause`
|
|
83
|
-
*
|
|
84
|
-
*
|
|
86
|
+
* 🔴 **`cause` 按开集读 + 一条派生的排除**(**0.64.2 订正,行为面**;修前是「unavailable 闭集」):
|
|
87
|
+
* · **非空串即收** —— 词表的属主是 core,而 server 侧除了「非空串」之外**不做词表校验**
|
|
88
|
+
* (engine fixture `approval-card.js:240` 的 `z.string().min(1)`)。在这里抄一份闭集表,只会在
|
|
89
|
+
* core 加词那天把一个**合法**值判没,而丢掉的正是「这次不可用是**新出现的那一类**」这条信息
|
|
90
|
+
* —— 与 `AskRequest.origin` / `ruleOffersAbsence` / `denialLimitFallback.limit` 逐字同一条纪律。
|
|
91
|
+
* 修前那条路是可复现的:一台比本端新的引擎发一个新成因词 ⇒ 卡上那一行**整段消失**,而
|
|
92
|
+
* {@link classifierUnavailableDetail} 早就为这一形备好了兜底句(「a word newer than this client」)
|
|
93
|
+
* —— 那句话在修前**永远不可达**,这本身就是判据写错了的证据。
|
|
94
|
+
* · **{@link BREAKER_ONLY} 里的词仍判缺席** —— 今天只有 `parse_error`。这不是「表外词一律拒」,
|
|
95
|
+
* 而是一条**有出处的排除**:core 顶注逐字「`parse_error` stamps nothing」,读它就是替引擎编一件
|
|
96
|
+
* 它明说没发生的事。0.63.0 起的黑盒判据 G-19 钉的正是这一条,行为**一字未变**。
|
|
85
97
|
* 🔴 **只交 `cause` 一格**:顺手把整只 ask 的别的键带出来会长成第二份 ask 读面。
|
|
98
|
+
* 🔴 **端不许在自己那一侧再补一张闭集表**:未知词的正解是渲兜底句(措辞铸点已经有),不是不渲。
|
|
86
99
|
*/
|
|
87
100
|
export declare function classifierUnavailableOf(ask: unknown): ClassifierUnavailableView | undefined;
|
|
88
101
|
/**
|
|
@@ -17,8 +17,12 @@
|
|
|
17
17
|
* 两集交于 `error` / `timeout`,各有一个独占成员。合成一张表就把两条轴的差别扔了。
|
|
18
18
|
* 🔴 **`parse_error` 只在熔断轴上**:core 顶注逐字 ——「`parse_error` stamps nothing」:分类器
|
|
19
19
|
* **跑了并且答了**,只是答在契约之外,那是**另一句话**。所以它**一个字节都不 stamp** 到
|
|
20
|
-
* `classifierUnavailable`
|
|
21
|
-
* 把它读成一个「没跑成」的成因就是替引擎编一件它明说没发生的事。
|
|
20
|
+
* `classifierUnavailable` 上;本读器把**熔断轴独占的词**({@link BREAKER_ONLY},今天 = `parse_error`)
|
|
21
|
+
* 判**缺席** —— 把它读成一个「没跑成」的成因就是替引擎编一件它明说没发生的事。
|
|
22
|
+
* ⚠️ **0.64.2 订正(行为面)**:修前这只读器按 unavailable **闭集**收窄,于是一台比本端新的引擎发一个
|
|
23
|
+
* **合法的新成因词**时,卡上那一行整段消失(而 {@link classifierUnavailableDetail} 的兜底句因此
|
|
24
|
+
* **永远不可达**)。现改为「非空串即收 + 排除熔断轴独占词」——「表外」与「另一条轴上的词」是两件事,
|
|
25
|
+
* 修前把它们判成了同一件。`parse_error` 的行为一字未变(黑盒判据 G-19 照旧成立)。
|
|
22
26
|
*
|
|
23
27
|
* -- 为什么是镜像而不是 import ---------------------------------------------------------------
|
|
24
28
|
* 这两张表与 `classifierUnavailable` 的型面在 sdk 8.8.0 与 agent-types 上**都还没有**(亲验:两棵树
|
|
@@ -67,7 +71,16 @@ export const AUTO_MODE_UNAVAILABLE_CAUSES = Object.freeze(['error', 'timeout', '
|
|
|
67
71
|
* 就能改,而公面消费者拿到的正是这个实例(本仓已定谳的病形,同 `RESUME_RETRY_LATER_CODES`)。
|
|
68
72
|
*/
|
|
69
73
|
export const AUTO_MODE_BREAKER_CAUSES = Object.freeze(['error', 'timeout', 'parse_error']);
|
|
70
|
-
|
|
74
|
+
/**
|
|
75
|
+
* **熔断轴独占的成因词**(0.64.2 起是本文件唯一的排除集)——
|
|
76
|
+
* `AUTO_MODE_BREAKER_CAUSES` 减去 `AUTO_MODE_UNAVAILABLE_CAUSES`,今天恰是 `{parse_error}`。
|
|
77
|
+
*
|
|
78
|
+
* 🔴 **派生而不是手抄**:排除的理由不是「它不在我的表里」,而是 core 逐字说过**它不 stamp 到这一格**
|
|
79
|
+
* (分类器跑了并且答了,只是答在契约之外)。这条理由天然属于「只在熔断轴上的那些词」,所以判据
|
|
80
|
+
* 就按两张镜像表的**差集**算 —— core 哪天再给熔断轴加一个独占词,这里自动跟上,而不需要有人记得
|
|
81
|
+
* 回来改第三处。
|
|
82
|
+
*/
|
|
83
|
+
const BREAKER_ONLY = new Set(AUTO_MODE_BREAKER_CAUSES.filter((c) => !AUTO_MODE_UNAVAILABLE_CAUSES.includes(c)));
|
|
71
84
|
/**
|
|
72
85
|
* 一只 ask(或一条 durable park 行的 `tool_approval` 载荷)→ 「分类器跑不了」的事实;
|
|
73
86
|
* 没有这条事实 / 形坏 ⇒ `undefined`,绝不抛出。
|
|
@@ -75,10 +88,19 @@ const UNAVAILABLE = new Set(AUTO_MODE_UNAVAILABLE_CAUSES);
|
|
|
75
88
|
* 🔴 **两处同一只读器**:`AskRequest.classifierUnavailable` 与
|
|
76
89
|
* `PendingAction.tool_approval.classifierUnavailable`(durable park 行的孪生位)**键路同形**,
|
|
77
90
|
* 所以一只读器吃两处 —— 各写一份就是两份台账各漂各的,本包一贯要根治的形。
|
|
78
|
-
* 🔴 **`cause`
|
|
79
|
-
*
|
|
80
|
-
*
|
|
91
|
+
* 🔴 **`cause` 按开集读 + 一条派生的排除**(**0.64.2 订正,行为面**;修前是「unavailable 闭集」):
|
|
92
|
+
* · **非空串即收** —— 词表的属主是 core,而 server 侧除了「非空串」之外**不做词表校验**
|
|
93
|
+
* (engine fixture `approval-card.js:240` 的 `z.string().min(1)`)。在这里抄一份闭集表,只会在
|
|
94
|
+
* core 加词那天把一个**合法**值判没,而丢掉的正是「这次不可用是**新出现的那一类**」这条信息
|
|
95
|
+
* —— 与 `AskRequest.origin` / `ruleOffersAbsence` / `denialLimitFallback.limit` 逐字同一条纪律。
|
|
96
|
+
* 修前那条路是可复现的:一台比本端新的引擎发一个新成因词 ⇒ 卡上那一行**整段消失**,而
|
|
97
|
+
* {@link classifierUnavailableDetail} 早就为这一形备好了兜底句(「a word newer than this client」)
|
|
98
|
+
* —— 那句话在修前**永远不可达**,这本身就是判据写错了的证据。
|
|
99
|
+
* · **{@link BREAKER_ONLY} 里的词仍判缺席** —— 今天只有 `parse_error`。这不是「表外词一律拒」,
|
|
100
|
+
* 而是一条**有出处的排除**:core 顶注逐字「`parse_error` stamps nothing」,读它就是替引擎编一件
|
|
101
|
+
* 它明说没发生的事。0.63.0 起的黑盒判据 G-19 钉的正是这一条,行为**一字未变**。
|
|
81
102
|
* 🔴 **只交 `cause` 一格**:顺手把整只 ask 的别的键带出来会长成第二份 ask 读面。
|
|
103
|
+
* 🔴 **端不许在自己那一侧再补一张闭集表**:未知词的正解是渲兜底句(措辞铸点已经有),不是不渲。
|
|
82
104
|
*/
|
|
83
105
|
export function classifierUnavailableOf(ask) {
|
|
84
106
|
if (typeof ask !== 'object' || ask === null || Array.isArray(ask))
|
|
@@ -87,7 +109,9 @@ export function classifierUnavailableOf(ask) {
|
|
|
87
109
|
if (typeof raw !== 'object' || raw === null || Array.isArray(raw))
|
|
88
110
|
return undefined;
|
|
89
111
|
const cause = raw.cause;
|
|
90
|
-
if (typeof cause !== 'string' ||
|
|
112
|
+
if (typeof cause !== 'string' || cause === '')
|
|
113
|
+
return undefined;
|
|
114
|
+
if (BREAKER_ONLY.has(cause))
|
|
91
115
|
return undefined;
|
|
92
116
|
return { cause };
|
|
93
117
|
}
|
|
@@ -98,6 +98,22 @@ export function cloudMcpToSpecs(servers, env) {
|
|
|
98
98
|
transport,
|
|
99
99
|
...(allowTools.length > 0 ? { allowTools } : {}),
|
|
100
100
|
...(s.elicitation === true ? { elicitation: true } : {}),
|
|
101
|
+
// ── 0.64.2 族扫(cli L-167① 的**同形存量第二处**)─────────────────────────────────────
|
|
102
|
+
// 病形与 `mcpWireCaps.mcpConfigToSpec` **逐字同一条**:这也是一次逐键白名单重建,而
|
|
103
|
+
// settings-schema 的 mcp 段上有**解析透明键**(`z.unknown()`;1.10.0 两员 `source` / `toolFaces`)。
|
|
104
|
+
// 上游对 `toolFaces` 的成文是「三条腿(请求 / config-center / 本地 config.d)同一条规则」——
|
|
105
|
+
// 本函数正是 **config-center 那条腿落到请求面**的投影,只修本地那条 = 同形存量留一半:
|
|
106
|
+
// 用 center 下发配置的部署,运维声明的 per-tool 面照样在包边界被静默剥掉。
|
|
107
|
+
// 🔴 **原样过境,不校形**(理由见 `mcpWireCaps.ts` 模块顶注与 `McpConfigLike.toolFaces` 的注):
|
|
108
|
+
// 形的判决属引擎摄入侧;包在这里判一次形,判严了就把运维声明的保护静默吞掉。
|
|
109
|
+
// 🔴 **`source` 同样不在这条腿上**:它是部署面的分组标签,而本函数的产物是**请求面**
|
|
110
|
+
// `McpServerSpec`(sdk 8.8.0 上没有这个键)。两条腿的记账逐字相同。
|
|
111
|
+
// 🔴 **两条腿刻意不共用一个函数,改由门同源**:跨文件共用要么给公面加一个没有外部消费者的
|
|
112
|
+
// 名字,要么把这一行抽成一个新叶子(闭包/单例账各加一笔)。这里的「规则」只有一行
|
|
113
|
+
// (键在场 ⇒ 原样铸),真正会漂的是**哪些键算透明** —— 那张表的属主在上游,而
|
|
114
|
+
// `run-additive-key-passthrough-test.mjs` G7 段是从**实装 schema** 取表、对**两条腿各对一次账**,
|
|
115
|
+
// 任一条漏一个键当天红。⇒ 单一真源在上游 + 机械门,不是两份各自为政的判断。
|
|
116
|
+
...(s.toolFaces !== undefined ? { toolFaces: s.toolFaces } : {}),
|
|
101
117
|
});
|
|
102
118
|
}
|
|
103
119
|
return { specs, missingEnv: [...new Set(missingEnv)], droppedServers };
|
|
@@ -78,6 +78,7 @@
|
|
|
78
78
|
* 🔴 方向纪律:reason/note 只做归因,绝不参与裁决;缺席 ⇒ 现状字节不变。
|
|
79
79
|
*/
|
|
80
80
|
import { type GateCurrentPending, type HitlFailureStage, type HitlSafetyCode, type HitlClientLike } from './hitlBridge.js';
|
|
81
|
+
import type { ClassifierUnavailableView } from '../autoModeUnavailable.js';
|
|
81
82
|
import { RULE_OFFER_MATCHES, RULE_OFFER_BATCH_MEMBER_KINDS, RULE_OFFER_UNCOVERED_REASONS, RULE_OFFERS_ABSENCE_REASONS, DENIAL_LIMIT_KINDS } from '@sema-agent/sdk';
|
|
82
83
|
import type { RuleSuggestion, ToolApprovalRespondAck, PersistedRuleAnchor as SdkPersistedRuleAnchor, RuleOfferMatch as SdkRuleOfferMatch, RuleOfferBatchMember as SdkRuleOfferBatchMember, RuleOfferUncoveredDetail as SdkRuleOfferUncoveredDetail, RuleOfferUncoveredReason as SdkRuleOfferUncoveredReason, AskOrigin as SdkAskOrigin, RuleOffersAbsence as SdkRuleOffersAbsence, DenialLimitKind as SdkDenialLimitKind, DenialLimitFallback as SdkDenialLimitFallback } from '@sema-agent/sdk';
|
|
83
84
|
/** fs 写权限 gate 判定:未来的一等 kind(tool_approval)或按 toolName(server 桥首批=fs 写三件,
|
|
@@ -669,6 +670,30 @@ export interface ApprovalCardRequest {
|
|
|
669
670
|
* 本词是分诊/审计座,单独在场只说明「这类门提的问」,给不出计数。
|
|
670
671
|
*/
|
|
671
672
|
origin?: string;
|
|
673
|
+
/**
|
|
674
|
+
* cli L-174②(0.64.2;server ≥7.69.0 / core 7.10.0 #616)—— 「**问你是因为分类器这次跑不了**」的
|
|
675
|
+
* 事实,**双源合流**({@link governanceForced} 同形,两腿在场性可以不一致):
|
|
676
|
+
* · **活卡帧腿**:{@link ToolApprovalFrame.classifierUnavailable} 的窄读产物;
|
|
677
|
+
* · **durable park 行腿**:`PendingAction.tool_approval.classifierUnavailable` 的孪生位
|
|
678
|
+
* (core `checkpoint-store.d.ts` 逐字「the PARK twin of `AskRequest.classifierUnavailable`」),
|
|
679
|
+
* 经**同一把**窄读器 —— 两处**键路同形**,各写一份就是两份台账各漂各的。
|
|
680
|
+
*
|
|
681
|
+
* 🔴 **键路与 ask / park 行同形是有意的**:端不必为卡再学一个读法 —— `classifierUnavailableOf(req)`
|
|
682
|
+
* 直接吃这张卡(0.63.0 件⑧ 的读器,§27h2),措辞走它的孪生铸点 `classifierUnavailableDetail`。
|
|
683
|
+
* 🔴 **两层都按开集,显示层只多一条有出处的排除**(0.64.2 订正后两层判据几乎逐字相同):
|
|
684
|
+
* · **本层(过境)**只判形 —— `cause` 非空串即收,与 server 唯一铸点 `readClassifierUnavailable`
|
|
685
|
+
* 同判据。词表属主在 core,包在边界再校一次闭集,只会在 core 加词那天把一个**合法**值判没,
|
|
686
|
+
* 而丢的正是「这次不可用是新出现的那一类」这条信息(与 `origin`/`ruleOffersAbsence` 逐字同规);
|
|
687
|
+
* · **显示层**(`classifierUnavailableOf`)同样非空串即收,只排除**熔断轴独占**的词
|
|
688
|
+
* (今天 = `parse_error`;core 明说它不 stamp 到这一格)—— 未知词在那一层渲**兜底句**,
|
|
689
|
+
* 不是不渲。⚠️ 0.64.2 之前那一层是 unavailable **闭集**,于是一个合法的新成因词过得了本层、
|
|
690
|
+
* 却在显示层被判没(异源对抗复审逼出的真病;详见 INTEGRATION §29e)。
|
|
691
|
+
* 🔴 **显示元数据,不是裁决位**:不参与、也不改变本次决断的任何字节;缺席**不许**渲成
|
|
692
|
+
* 「分类器跑成了」(缺席三形不可分,见帧上同名键)。
|
|
693
|
+
* ⚠️ **退役日期已定**(上游发布记录 [6852]/[6853]):core 7.12.0 / server 7.70.0 起帧上这一格是**死键**,
|
|
694
|
+
* 事实位随拒绝面走;本包 0.65.0 改读那一面而**不删本键**——缺席臂当天就是正解。
|
|
695
|
+
*/
|
|
696
|
+
classifierUnavailable?: ClassifierUnavailableView;
|
|
672
697
|
}
|
|
673
698
|
/**
|
|
674
699
|
* 🔴 **拆缝口** —— 弹「三选卡」并等人的决断。壳 = vendored CC `PermissionRequest`;
|
|
@@ -852,6 +877,28 @@ export interface ToolApprovalFrame {
|
|
|
852
877
|
* durable 行 → 卡那条腿不 stamp,与 {@link requiresRealApproval} 同一格记账。
|
|
853
878
|
*/
|
|
854
879
|
denialLimitFallback?: DenialLimitFallback;
|
|
880
|
+
/**
|
|
881
|
+
* **server ≥7.69.0 / core 7.10.0 #616**(cli L-174②;**ADDITIVE**,`"tool_approval"` only。真发直证 =
|
|
882
|
+
* engine 7.69.0 fixture `@sema-agent/server/dist/tool-approval.js:847` 的条件 stamp + 同包
|
|
883
|
+
* `approval-card.js:244` `readClassifierUnavailable`(`z.object({cause: z.string().min(1).max(200)}).strict()`,
|
|
884
|
+
* 与 `card_json` 铸点 `:315` 同一只读器);上游发布记录 [6863] ③ 表逐字「additive;缺席不是断言;`cause`
|
|
885
|
+
* core 闭三词但 **wire 上 string**」)——这只 ask 是因为 **auto 模式分类器这一轮跑不了**才问人的,
|
|
886
|
+
* `cause` 说的是「为什么没跑成」(core `AUTO_MODE_UNAVAILABLE_CAUSES`)。
|
|
887
|
+
*
|
|
888
|
+
* 🔴 **显示元数据,不是裁决位**:引擎在门上已经判完了,没有任何东西读它决定放行与否。
|
|
889
|
+
* 🔴 **缺席不是断言**:缺席同时覆盖「分类器答上了」「这只 ask 没资格走分类器」「本部署没接分类器」
|
|
890
|
+
* 三形(core d.ts 逐字:read presence, never absence)—— 端**禁**读成「分类器好着呢」。
|
|
891
|
+
* 🔴 **echo-only**:server 顶注逐字写明它不据本键做任何裁决;据它在下游自铸第二套熔断/回落判据
|
|
892
|
+
* = 同一语义面两个写者(源头修复纪律)。本包同律 —— 本批只做过境,**不做熔断相关任何事**。
|
|
893
|
+
* ⚠️ **它是一个已定退役日期的键**(上游发布记录 [6852] server 侧落点表逐字:「ask 帧 `classifierUnavailable`
|
|
894
|
+
* **死键**;事实位随 deny 载荷 … 三面投影(tool_approval 帧 / card_json / inbox 行)**退役**,
|
|
895
|
+
* `permissionDenied.classifierUnavailable` 走 `tool_end.gate` 既有透传」)⇒
|
|
896
|
+
* **core 7.11.x 仍写;core 7.12.0 / server 7.70.0 起缺席**。本包 0.65.0 按 [6853] 的排期改从
|
|
897
|
+
* 拒绝面渲,**码不删**(死键保留型):缺席臂那天就是正解,退役一个 additive 键不该逼三端各改一次。
|
|
898
|
+
* ⚠️ **sdk 8.8.0 的运行期锚 `TOOL_APPROVAL_FRAME_KEYS`(27 项)尚无本键** ⇒ 镜像**领先**一代,
|
|
899
|
+
* 对账门里挂着一条带退出条件的 `AHEAD_OF_ANCHOR` 登记(sdk 8.9.x 同拍补上那天它自红逼删)。
|
|
900
|
+
*/
|
|
901
|
+
classifierUnavailable?: ClassifierUnavailableView;
|
|
855
902
|
/**
|
|
856
903
|
* server ≥7.13.0(#144 / core 5.25.0,[3438] 接力契约 / [3443] 主件;**ADDITIVE**,
|
|
857
904
|
* `"tool_approval"` only。来源锚 = engine fixture `@sema-agent/server/dist/tool-approval.d.ts`
|
|
@@ -871,10 +918,13 @@ export interface ToolApprovalFrame {
|
|
|
871
918
|
* 🔴 **UNTRUSTED-for-display,且不是身份通道** —— 详见
|
|
872
919
|
* {@link ApprovalCardRequest.persistedRuleShadowed} 的同名警示(下游删除规则面不许拿它当规则身份)。
|
|
873
920
|
*
|
|
874
|
-
* 耐久路(park 行)
|
|
875
|
-
*
|
|
876
|
-
*
|
|
877
|
-
*
|
|
921
|
+
* 耐久路(park 行)的对偶是行上的 **`riskDescriptor.shadowedRule`**(server `redactPendingDisclosures`
|
|
922
|
+
* 逐字脱敏后随 `projectPendingForWire` 上 wire;⚠️ **键名与本处顶层键不同**,那正是它长期被漏掉的原因)。
|
|
923
|
+
* ✅ **0.64.2 起已消费**(件⑥):`surfaceFsApprovalAndDecide` 把它投到**同一个卡位**
|
|
924
|
+
* {@link ApprovalCardRequest.persistedRuleShadowed} —— 两条腿说的是同一件事,端读一个形。
|
|
925
|
+
* ⚠️ 本段 0.28.0 原文写的是「shadowedRule 的耐久对偶仍未消费——那一路是独立的一件」,那句话到 0.64.2
|
|
926
|
+
* 不再成立:它不是一件「独立的展示件」,而是壳**让位臂的输入** —— 不投的直接后果是耐久路上一条
|
|
927
|
+
* 「规则清不掉」的 ask 被当成普通 ask 自动放行(cli B-058 离线真跑证实)。
|
|
878
928
|
*/
|
|
879
929
|
persistedRuleShadowed?: string;
|
|
880
930
|
/**
|
|
@@ -1038,7 +1088,7 @@ export interface ToolApprovalDelegation {
|
|
|
1038
1088
|
* `TOOL_APPROVAL_FRAME_KEYS` 比对——SDK additive 增键时对账当天红,不再人肉追平。
|
|
1039
1089
|
* 下面两个类型钉保证镜像与 interface 本身不可能漂移(少键/多键都是编译错)。
|
|
1040
1090
|
*/
|
|
1041
|
-
export declare const TOOL_APPROVAL_FRAME_KEYS_MIRROR: readonly ["type", "approvalId", "toolCallId", "toolName", "sourceTaskId", "fromSubagent", "sourceAgentName", "message", "args", "argsOmitted", "governanceForced", "ruleSuggestions", "ruleOffers", "ruleOffersAbsence", "denialLimitFallback", "origin", "inputHasBidi", "parked", "persistedRuleShadowed", "probeCause", "ruleEvidence", "requiresRealApproval", "expiresInMs", "expiresAtMs", "serverNowMs", "delegation", "outcome"];
|
|
1091
|
+
export declare const TOOL_APPROVAL_FRAME_KEYS_MIRROR: readonly ["type", "approvalId", "toolCallId", "toolName", "sourceTaskId", "fromSubagent", "sourceAgentName", "message", "args", "argsOmitted", "governanceForced", "ruleSuggestions", "ruleOffers", "ruleOffersAbsence", "denialLimitFallback", "classifierUnavailable", "origin", "inputHasBidi", "parked", "persistedRuleShadowed", "probeCause", "ruleEvidence", "requiresRealApproval", "expiresInMs", "expiresAtMs", "serverNowMs", "delegation", "outcome"];
|
|
1042
1092
|
/**
|
|
1043
1093
|
* 子代帧判别:显式键 fromSubagent(core 1.378 RB-39②)优先;缺席退 sourceTaskId 在场性权宜式
|
|
1044
1094
|
* (server 1.258 [1549]①3,旧代际兼容)。
|
|
@@ -1264,6 +1314,33 @@ export declare function readToolApprovalRespondRefusal(err: unknown): ToolApprov
|
|
|
1264
1314
|
* `AgentEvent` 的臂了(durable 腿也回放),所以结构识别与 union 收窄两条路都成立;本函数仍按
|
|
1265
1315
|
* 结构读(不依赖类型收窄),因为它同时服务 raw SSE 与 durable 回放两条入口。 */
|
|
1266
1316
|
export declare function isToolApprovalFrame(ev: unknown): ev is ToolApprovalFrame;
|
|
1317
|
+
/**
|
|
1318
|
+
* `denialLimitFallback` 的**窄读器**(S-114,0.59.0)—— wire 是 JSON:注入面 / 旧 server / 比本包新
|
|
1319
|
+
* 一版的 server 都可能给别的形,坏形一律降**缺席**(与 `readToolApprovalDelegation` 同族纪律)。
|
|
1320
|
+
*
|
|
1321
|
+
* 🔴 **四成员全必填,缺一整只丢**(与 server `approval-card.ts` 的 `readDenialLimitFallback`
|
|
1322
|
+
* `.strict()` 同判据,与 core 的形一字不差):半只对象上的 `consecutive` 会被人当成**真实的
|
|
1323
|
+
* 连续拒次数**读,而它可能只是一个恰好在场的键。「一格空白的计数」比不渲这张卡更坏。
|
|
1324
|
+
* 🔴 **两个计数与窗按「有限非负数」判,不折 0**:`Number.isFinite` 单独会放行负数,而三者的定义域
|
|
1325
|
+
* 按上游契约本就非负(计数是次数,窗是 ms 且 `0` = 不武装,是**合法读数**不是缺席)。
|
|
1326
|
+
* NaN / Infinity / 负数 = 坏形降缺席,**绝不猜**(同文件 `delegation.depth` 的旧教训)。
|
|
1327
|
+
* 🔴 **`limit` 按开集读**(非空串即收):闭二词表 `DENIAL_LIMIT_KINDS` 只作再导出给端数,
|
|
1328
|
+
* 包内**不拿它做判定** —— 词表属主是 core,抄一份就是给自己立第二个判官(B-025 的病形);
|
|
1329
|
+
* server 侧已按闭集拒过词表外的值,包再校一遍只会在 core 加员当天把一个合法值判没。
|
|
1330
|
+
* ⚠️ 这条与「四成员全必填」不矛盾:必填说的是**在场性**,开集说的是**取值域**。
|
|
1331
|
+
*
|
|
1332
|
+
* 🆕 **0.64.2 起在公面上**(cli L-174③;此前是模块私有):壳侧此前自持一份同判据的副本
|
|
1333
|
+
* (`src/sema/askFrameNotes.ts`),那是**第二个判官** —— 上游哪天在四成员上加一位、或把某一位的
|
|
1334
|
+
* 定义域改了,两份判据各漂各的,而屏上看到的是哪一份取决于素材走了哪条腿。导出的用途正是让那份
|
|
1335
|
+
* 副本整只退役:端读**包内窄读产物**({@link ApprovalCardRequest.denialLimitFallback})时不必再判
|
|
1336
|
+
* 一遍,端拿到**裸帧/裸行**时也有同一把读器可用。
|
|
1337
|
+
* 🔴 **导出的是读器,不是许可**:`autoDenyAfterMs` 仍然只许渲倒计时(窗的执行全在引擎),
|
|
1338
|
+
* 这一条不因它上了公面而松动。
|
|
1339
|
+
* ⚠️ **与 server `.strict()` 的一格差**(L-120③,如实写在两侧):对象上**多出的成员**本读器剥后
|
|
1340
|
+
* 保留四键,server 侧整只拒收 —— 包比 server 宽一格,方向是「多键不误杀」。端不要把「包读出来了」
|
|
1341
|
+
* 当成「server 也会收」。
|
|
1342
|
+
*/
|
|
1343
|
+
export declare function readDenialLimitFallback(v: unknown): DenialLimitFallback | undefined;
|
|
1267
1344
|
/**
|
|
1268
1345
|
* `ruleOffers`(server ≥7.46.0 的判别联合)的结构读。
|
|
1269
1346
|
*
|