@sema-agent/client-core 0.77.2 → 0.78.1
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 +53 -0
- package/README.md +5 -2
- package/dist/adapt/arms.js +4 -33
- package/dist/adapter/downstream/eventToSdkMessage.d.ts +11 -1
- package/dist/adapter/downstream/eventToSdkMessage.js +16 -0
- package/dist/adapter/downstream/wiringManifestView.d.ts +27 -0
- package/dist/adapter/downstream/wiringManifestView.js +23 -0
- package/dist/gateVocabulary.js +2 -0
- package/dist/hitl/persistedRulesWire.d.ts +10 -13
- package/dist/hitl/persistedRulesWire.js +65 -35
- package/dist/hitl/sessionPolicyWire.js +2 -9
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/mcpLiveness.js +3 -2
- package/dist/mcpPanel.js +1 -1
- package/dist/memoryVerbsWire.d.ts +106 -0
- package/dist/memoryVerbsWire.js +423 -0
- package/dist/request/taskRequest.d.ts +1 -0
- package/dist/request/taskRequest.js +14 -0
- package/dist/toolResult.d.ts +9 -0
- package/dist/toolResult.js +9 -1
- package/dist/wireFailureShape.d.ts +7 -0
- package/dist/wireFailureShape.js +32 -0
- package/docs/INTEGRATION-CLIENTS.md +99 -12
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -49,6 +49,59 @@
|
|
|
49
49
|
> 挡住 ⇒ 本批把它机械化——④a0 对 `pending` 行**要求段头已是日期形**(`(未发布)` 直接红),阶段一
|
|
50
50
|
> commit 漏转在发布前就红,不再靠人记。
|
|
51
51
|
|
|
52
|
+
## 0.78.1(2026-09-21)
|
|
53
|
+
|
|
54
|
+
> 主题:**两辆并行车收货 + 三张跟进票**(patch;公面运行期导出 1143 → **1154**;wire 零新键;peer 地板不动 `>=11.0.1`;一处既有导出的行为收紧 + 三处措辞改口)—— 工具结果正文人类面读口 `displayBody` 与围栏解析配对收紧(CC-113)· MCP 面板 / 活性两处「没报」措辞分句(CC-115)· 记忆治理面五动词调用口(CC-97 b)· 非流式提交回执的起手接线回执读口 `readSubmitWiringManifest`(CC-121)· 三份失败判官的抛出物读取收口成一只永不抛的快照 helper(CC-119)。**成文改口段见 §83 83y。**
|
|
55
|
+
|
|
56
|
+
### Added
|
|
57
|
+
|
|
58
|
+
- **`displayBody(text)`**(CC-113):引擎交给模型的文本外面那层不可信围栏由本包铸、本包读,剥壳也归本包 —— `{ fenced: true, label, body } | { fenced: false, body } | undefined`。只剥本包认识的形、只剥最外层;认不出 / 半截 / 头尾标签不同一律 `fenced: false` 且 `body` 逐字 = 入参;**非串答 `undefined`**(本端手上没有可读文本;与空串 `{ fenced: false, body: '' }`、围栏里的空正文 `{ fenced: true, label, body: '' }` 三形互不折叠);不消毒、不封长(呈前清洗归渲染侧)。类型 `DisplayBody` 同批导出。
|
|
59
|
+
- **记忆治理面五动词调用口**(CC-97 b;sdk ≥11.0.1 型面):`readEntryProvenance` / `eraseMemoryEntries` / `listExternalOriginEntries` / `listOriginClearances` / `clearEntryOrigin`(注入缝 `MemoryFacade`,入参 / 回体型全取自 sdk)+ 单一失败判官 `classifyMemoryFailure`(🔴 出处先于状态码:无机读码的 4xx / 501 / 405 一律 `unknown / no_verdict`;分辨位在 `errorCode` 不在回体也不在 status)+ 两张闭集词表 `MEMORY_VERB_REFUSAL_CAUSES`(21 词,「确知什么都没做」)/ `MEMORY_VERB_UNKNOWN_WHYS`(9 词,「不知道做没做成」)+ `externalOriginCoverage`(空答绝不读作本店干净;联合里没有「干净」臂)。能力位 = 明确的否才拦(`refused / face_absent`,零请求),「说不出」照发;两条写腿的 200 体复用 0.76.1 的读口;非幂等动词零重试;三口带身份对账(`identity_mismatch`)。
|
|
60
|
+
- **`readSubmitWiringManifest(result)`**(CC-121):非流式 `POST /v1/tasks` 200 体 `TaskResult.wiringManifest`(server ≥7.92.0 additive)的读口,三态 `not_reported`(键自有缺席 = 老 server / 没产该帧的部署形)/ `unreadable` / `manifest{view}`;🔴 `view` 与流式腿 chrome 臂**同一张**(去掉 `kind` / `laneProof`;两条腿共用同一只拼装口 + 单源窄化器,门以同一份对象两腿逐字节对拍);只读工具面不读活性(`mcp[].liveness` 原样过境);operator 形多出的段只透传;零工具面 `tools.count === 0` 是真读数。类型 `WiringManifestView` / `WiringManifestSections` / `SubmitWiringManifestReading` 同批导出。
|
|
61
|
+
|
|
62
|
+
### Changed(行为 / 措辞;详见 §83 83y)
|
|
63
|
+
|
|
64
|
+
- **`stripUntrustedFence` 围栏解析收紧**(CC-113):结束标签必须与开始标签**逐字相等**才算一只完整围栏。此前结束标签是任意文本,两类文本被误判成完整围栏:开始与结束标签不同的;开始标签在、结束标签被截断而正文里恰有一只完整内层围栏的 —— 后者会把内层那一行结束标记**静默删掉**。收紧依据是上游铸点单点、开闭标记插的是同一个标签值 ⇒ 真引擎产出零影响;把这只口用在非引擎产出文本上的端按 §83 复核。
|
|
65
|
+
- **MCP 面板「最近一条腿」整键缺席那一句**(CC-115):从「保留窗内没有腿 / 最近一条腿没有名册 / 引擎早于这个字段」改成 `last leg mcp not reported (the panel carried no entry for the last leg; this client cannot tell which cause applies)` —— 旧句摆了一份假闭集(上游列的缺席形有七项,含「这条腿有属主而调用方不是属主」)并做了契约明禁的版本反推。
|
|
66
|
+
- **MCP 活性两句**(CC-115):`indeterminate` ⇒ `… (no liveness observation is available to this client, and this client cannot tell which cause applies)`(四源同形,本端连名册都未必看过,不再替名册作证);入参缺席 ⇒ `… (this client holds no liveness reading for this leg)`(与前者拉开逐字距离)。
|
|
67
|
+
- **三份失败判官的抛出物读取收口**(CC-119):持久规则店 / 会话策略店 / 记忆治理面此前各自一份 `errShape`,前两份裸读属性并调用无保护的 `String(e)` —— 注入的 facade 拒绝一个带抛出 getter 的对象时,判官在 catch 块里再抛,整只口以 reject 结束,调用方拿不到原定的 `unknown` / indeterminate 结局(写没写根本判不出)。三份收口成一只永不抛的 `wireFailureShapeOf`(每格恰读一次并快照;读不了 / 渲不出各一句兜底;`retryAfterMs` 同样保护)。可见差别只对注入非常规错误对象的宿主存在:此前 reject,现在按「无码 ⇒ 读不出出处」落 `unknown` 臂。同根同批:出处账读口 `readEntryProvenance` 的**嵌套**判别位(`binding.state` / `custody.state` / `custody.events` / `exclusion.code`)此前在保护外读取,带陷阱的账让整只口 reject ⇒ 各读一次并快照;自有 `exclusion` 键在场但 `undefined` / `null` 不再借 `!== undefined` 豁免溜成「没有扣留」⇒ `unknown / result_unreadable`。
|
|
68
|
+
- 流式帧 `wiring_manifest` chrome 臂改经共用拼装口 `wiringManifestViewOf`(与非流式读口同源);产出逐字节同 0.78.0。
|
|
69
|
+
|
|
70
|
+
### Gates
|
|
71
|
+
|
|
72
|
+
- 新门三只:`run-display-body-test.mjs`(82:单源对拍二十一形 / 只剥最外层 / 头尾配对 + 上游铸点真字节直证 / 非串诚实缺席三形互不折叠 / 不越界洗 / 源码级单源)· `run-memory-verbs-wire-test.mjs`(216:嵌套 getter / revoked Proxy 五形不 reject、扣留位 undefined / null 两形拒认 / 逐动词能力位 / 成功臂 / 逐码一格 / 无码 ⇒ no_verdict / 200 坏体不认成功 / 假件注入 / 账目可达性 / 上游字节锚)· `run-submit-wiring-manifest-test.mjs`(33:三态互不折叠 / 两腿同一张视图逐字节对拍 / operator 形透传 / 零工具面真读数 / eventId / 型面含两道编译期等值钉 —— 双向可赋值 + 键集相等,只钉可赋值时多加一个可选段仍编译得过)。
|
|
73
|
+
- 既有门扩格:`run-mcp-panel-projection-test.mjs` W 段(逐因可达 / 句集互异 / 分辨不了只在那一句 / 不咎版本 / 不摆假闭集 / sdk `McpStatusPanel` 成员签名 ⇄ 已审阅基线漂移钉)+ G4 成文改口 · `run-mcp-liveness-test.mjs` W 段 9 格 · `run-persisted-rule-write-test.mjs` W12(抛出物访问器 / toString / retryAfterMs 各抛 ⇒ 不 reject)· `run-rules-side-test.mjs` G3z · `run-session-policy-wire-test.mjs` R6b / R6c。
|
|
74
|
+
- 登记物:公面 1143 → 1154;typeshape `unknown` 出境 390 → 393(`displayBody(text: unknown)` / `classifyMemoryFailure(e: unknown)` / `wireFailureShapeOf(e: unknown)`,三处都是边界读口入参)+ 负控锚同批;index 闭包 191 → 194(`memoryVerbsWire` / `wireFailureShape` / `wiringManifestView`);singleton 清单 +2;`export-liveness.json` 销掉 `stripUntrustedFence` 那行欠账(它欠的那只包侧门就是 `run-display-body-test.mjs`)。
|
|
75
|
+
|
|
76
|
+
## 0.78.0(2026-09-21)
|
|
77
|
+
|
|
78
|
+
> 主题:**peer sdk 地板抬到 11.x + 单步写口读回按活性判别 + 第十词专句 + 整只工具面卸载进车道表**(🔴 **minor**:peer 地板 `>=9.8.1` → **`>=11.0.1`**;单步写口结局联合**多一臂 `revoked`**、`RulesFacade.write` 转必填、三只自铸型改为 sdk 别名(型面 BREAKING 三处,编译器会报);公面运行期导出 **1143 不变**;wire 新键一枚 `excludeAllTools`(sdk 具名位进车道表))—— 抬地板(CC-106)· 单步写口 200 体按 `stillLive` 判别联合改读(CC-103)· `DeniedBy` 第十词 `read_boundary` 专句(CC-106 ④)· `excludeAllTools` 车道行。**成文改口段见 §82 82y,按表态制点名三端。**
|
|
79
|
+
|
|
80
|
+
### 🔴 BREAKING(型面)
|
|
81
|
+
|
|
82
|
+
- **peer `@sema-agent/sdk` 地板 `>=9.8.1` → `>=11.0.1`**(CC-106):11.x 声明 `DeniedBy` 第十词 `read_boundary`、`rules.write` 200 体按 `stillLive` 判别的联合、`RemovalLiveness` / `RuleWriteRequest` / `RuleWriteResult` / `RuleWriteBehavior` 包根导出、`TaskRequest.excludeAllTools`;本包按 11.0.1 编译,消费端实装已在 10.x / 11.x,9.8.1 失去物料见证。四处同批抬齐(package.json peer + devDep / lockfile 根包两处 / README / 接入文档 §0a),`run-sdk-floor-test.mjs` 的 `FLOOR` 与负控锚同批。装着 <11.0.1 的端:`gateDeniedByDetail('read_boundary')` 仍答专句(镜像表不依赖 sdk 运行期),但 `rules.write` 回体在 sdk 10.x 的型面上看不见 `stillLive`(编译期不报)—— 请同批抬 sdk,别混装。
|
|
83
|
+
- **`writePersistedRule` 结局联合 +1 臂 `{ status: 'revoked', wrote: 'persisted' | 'no_op', rev, message }`**(CC-103;server ≥7.92.2 起可达):写**落了**(`rev` 是真的)而读回它时一条并发撤销已先到 ⇒ 请求的收紧**现在不站着**。🔴 **不要重试**(那会把刚被人撤掉的收紧重新立起来 —— 正是它不能折进 `unknown` 的理由,那一臂的处置是顺序重发);处置只有一个:`listAllPersistedRules` 看现状。键集与其余四臂两两不相交(没有 `rule`:店里没有活行)。穷举式 `switch` 的端在这一臂上编译红,那就是通知。
|
|
84
|
+
- **`RulesFacade.write` 可选 → 必填**:peer 地板 ≥11.0.1 起 sdk 的 `rules` 资源自带这个动词,宿主从真 client 装配一定拼得出;运行期守卫与 `refused / client_too_old` 一格**保留**(注入的假件 / 比这条口老的宿主客户端仍可能没这个动词,「连发都没发」是确知的拒,不折 unknown)。自铸 facade 的测试假件补 `write`。
|
|
85
|
+
- **三只自铸型改为取自 sdk**:`PersistedRuleWriteRequest` = `RuleWriteRequest`、`PersistedRuleWriteWireResult` = `RuleWriteResult`(判别联合:`rule` 只住 `stillLive: "yes"` 支),`PersistedRuleWriteBehavior` 仍由运行期表派生并加编译期等值钉证明与 `RuleWriteBehavior` 是同一张。名字不变;请求体结构不变;回体型变宽(直接读 `facade.write()` 回体的端从此必须先判 `stillLive` 再读 `rule`,不判 = 编译错)。
|
|
86
|
+
|
|
87
|
+
### Added
|
|
88
|
+
|
|
89
|
+
- **`DeniedBy` 第十词 `read_boundary` 专句**(CC-106 ④):`GATE_DENIED_BY_WORDS` 十词(顺序同源 sdk 11.0.1),`gateDeniedByDetail('read_boundary')` = `denied by the read boundary`(读边界站拒:拒表路径命中的读在任何审批之前就被拦,或一次已批准的改写被重判到拒表行)。0.77.2 上这个词走「本版不认识这个层名」兜底句,从此走专句;兜底句仍只给本包不认识的词。
|
|
90
|
+
- **`PersistedRuleLiveness`**(= sdk `RemovalLiveness`,`yes` / `no` / `unknown`):撤销口 `stillLive` 与写口 200 体共用的活性三词以本包的名转口;端从此读包名(此前只能派生 sdk 的 `RuleRevokeResult['stillLive']` 或手抄三词)。
|
|
91
|
+
- **`excludeAllTools` 进请求装配车道表**(sdk 11.x 具名位;server ≥7.90.0 请求面):两条提交车道有座、live 门后;字面 `true` 单成员闭集 —— `true` 才 stamp,`false` 与缺席同义(不发、不进回执:server 只把 literal true 写上任务规格,`false` 是替引擎重申它自己的缺省),坏值(非布尔)构造期 `TypeError` 只报形状不回显值(收紧方向的声明不许被打字错误安静丢掉,与 `memoryCapture` 同律);读序也同律 —— 先于其余键读取并种进快照,排在后面的可执行属性改不掉已读的声明。🔴 与 `excludeTools` 两种语义刻意不折叠:`"*"` 是一个合法的工具名,两键同带各自生效。0.77.2 上这个键会被表外键判据以 `TypeError` 点名拒;从此有座。没接线的 server(≤7.89.x)对它 400 `request.body_shape` 并在 `unknownKeys` 里点名 —— 探测靠键闭集,没有能力位,本包不替它猜。
|
|
92
|
+
|
|
93
|
+
### Changed(判据)
|
|
94
|
+
|
|
95
|
+
- **单步写口 200 体读法**(CC-103):认证字段只凭回包**自有数据属性**一次性快照(`status` / `rev` / `stillLive` / `rule` 任一只在原型链上、或是访问器属性 ⇒ `malformed_result`;getter / Proxy trap 抛错 ⇒ `malformed_result` 而不是 reject;原型链上只有无关键照常读);先读 `stillLive` 再读 `rule` —— `"yes"` 走行窄化 + 身份三元组对账(与 0.77.x 同答);**缺席 = 老服务(≤7.92.1,那一代恒带 `rule`、不带这一格)按行在场读,不补默认值、不合成一个 `yes`**(证据是那一行,不是本包替旧服务说的词);`"no"` 却带 `rule`(两个判别位打架)/ `"unknown"`(本口契约上不可达)/ 表外词 / boolean 时代的 `true` `false` / 键在场读不出 ⇒ `unknown / malformed_result`(读不懂的 200 拒认,不折成 `persisted` 也不折成 `revoked`);`no-op` → `no_op` 的拼法归一收成一处,三臂共用。`unknown / indeterminate`(503 `state.rule_write_failed`)的注解收窄:≥7.92.2 起「写落了而读回时并发撤销先到」走 200 `stillLive: "no"`,不再折在 503 里。
|
|
96
|
+
- 词汇门 `run-gate-vocabulary-test.mjs` 成文改口:A1「上游九词」→ 十词、A5 十句互异、A6 加 `read_boundary` 逐字锚;上游联合解析器改走 TypeScript AST(具名 TypeAliasDeclaration → 字面量成员;逃生口 `(string & {})` 同一棵树判别):sdk 11.0.1 的真形是联合成员之间夹一段带 `;` 与引号串的注释,旧的 `[^;]*` 正则把十词读成八词、A1 假红;行尾注释里的 `;` / 字符串成员里的注释标记 / 注释里的假声明三形各一枚正控。
|
|
97
|
+
- 能力位处置台账门 `run-engine-caps-ledger-test.mjs` 补两行:`peerLane` / `permissionRulesWrite`(0.76.1 起本包已按结构读,sdk 11.x 型面到货 ⇒ 键集对账红,显式登记为 read;读点不变)。
|
|
98
|
+
- `run-sdk-floor-test.mjs` 字符串序正控随地板走:地板 major 进两位数后,原「10.0.0 判绿 ∧ major < 10」自己失去判别力;改为取「数值上高一个位数、字典序却排在地板之前」的版本判绿,并断言字典序确实判反。
|
|
99
|
+
|
|
100
|
+
### Gates
|
|
101
|
+
|
|
102
|
+
- `run-persisted-rule-write-test.mjs` → 319(新 W11 五组:yes 支同答 / no 支 `revoked` 三形 / 打架与表外十三形拒认 / 缺席按行读 / 三型取自 sdk 与 `write` 必填的型面钉)· `run-task-request-omission-receipt-test.mjs` 156 → 174(A6:`excludeAllTools` 两车道座位、非 live 回执、`false` 值级缺席、与 `excludeTools:["*"]` 不折叠、五种坏值响亮拒只报形状、`null` 合法缺席)· `run-client-core-pure-test.mjs` +5(W3b)· `run-gate-vocabulary-test.mjs` → 228(十词 + 解析器两枚正控)· `run-sdk-floor-test.mjs` 地板 11.0.1 · 负控:sdk-floor 锚 11.0.1 → 11.0.2;词汇门负控在 11.0.1 真形上仍于编译期先红。
|
|
103
|
+
- 撤销口假件的 `stillLive` 从 boolean 时代的 `false` 改三词 `'no'`(本包不读这一格,只为假件与 sdk 10.x+ 同形)。
|
|
104
|
+
|
|
52
105
|
## 0.77.2(2026-09-21)
|
|
53
106
|
|
|
54
107
|
> 主题:**出包面结构卫生 + 门词汇兜底句改口**(patch;型面零变、行为零变;制品面:dist 零注释)。
|
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.
|
|
38
|
+
**Version:** 0.78.1
|
|
39
39
|
|
|
40
40
|
- **Today** — the adapter seam, the whole `adapt()` pipeline (all 14 A-layer arms plus the
|
|
41
41
|
B/D/E tool-card layers), the notification/caps/model families, the adapter kernel (stream driver
|
|
@@ -67,7 +67,7 @@ Renamed from **`@sema-agent/wire-cc-adapter`** (0.1.x, deprecated — see *Migra
|
|
|
67
67
|
against — the tables live upstream precisely so this package does not keep a second copy that can
|
|
68
68
|
fall behind. The browser bundle really bundles the SDK through (the portability guard would
|
|
69
69
|
exit 3 rather than quietly mark it external).
|
|
70
|
-
- The declared floor is `>=9.8.1`
|
|
70
|
+
- The declared floor is `>=11.0.1` (raised from `>=9.8.1` in 0.78.0: `DeniedBy` carries its tenth word `read_boundary`, `rules.write` answers a `stillLive`-discriminated body, `RemovalLiveness` / `RuleWriteRequest` / `RuleWriteResult` / `RuleWriteBehavior` are exported from the SDK root and `TaskRequest.excludeAllTools` is typed from 11.x on, the package now compiles against 11.0.1, and no consumer ships 9.8.x any more, so the older floor lost its witness; before that raised from `>=9.7.1` in 0.75.0: `Capabilities.deviceExecutor.management` is typed from 9.8.x on, the package now compiles against 9.8.1, and no consumer ships 9.7.x any more, so the older floor lost its witness; before that raised from `>=9.6.0` in 0.74.0: `Capabilities.approvalsStreamLive` / `.executionLane`, `LivePendingRow.frame`, the `live_*` approval-stream events and `gates[].toolCallId` are typed from 9.7.x on, and no consumer ships 9.6.0 any more, so the older floor lost its witness; before that raised from `>=9.4.0` in 0.71.0: the `tool_disclosure` / `tool_progress` frames and `ToolApprovalFrame.readRootCandidate` are typed there; earlier: raised from `>=8.8.0` in 0.69.0: the `reasoning_end` frame and `McpStatusPanel.lastLegMcp` are typed from 9.4.0 on), and it is *witnessed*: the guard checks that an actually
|
|
71
71
|
installed SDK at that line still exports every value-level symbol this package imports and still
|
|
72
72
|
declares `TaskStats.costMicroUsd` (the key `costOrNull` reads). A floor nobody ever ran is a
|
|
73
73
|
promise, not a contract.
|
|
@@ -333,6 +333,7 @@ public-surface guard checks that last one).
|
|
|
333
333
|
| `scripts/run-mcp-reconnect-test.mjs` | The in-session MCP re-dial verb (`POST /v1/sessions/:id/mcp/reconnect`, engine ≥7.85.0), consumed. The single discriminant is `outcome` and all three answers are HTTP 200, so the reader branches on the word and never on the status; the `unsupported` answer carries exactly five keys and the reader refuses to invent a zero or an empty list for the four fields the engine did not produce, while `accepted` / `refused` treat those four as required and go malformed when one is missing. The tool roster follows the **presence** of `toolNames` (absent = untouched, empty = withdrawn), the connection record passes `errorCode` through as an open set, and every remote-authored string is sanitised and bounded before display. Failures are classified by `errorCode` alone, a missing code is reported as unknown rather than guessed, the verb never throws, and the request-side guard (non-empty name, ≤190 chars) stops a call that the contract would reject anyway. The capability bit reads absent as "cannot tell" rather than "unavailable", and the one sentence the contract insists every UI carries — that re-dialing is a transaction, not a refresh — is minted here once |
|
|
334
334
|
| `scripts/run-core-value-ports-test.mjs` | The port-injection seam for ten **engine value-level** facilities (autonomous-loop prompt assembly, permission-rule loosening, tool-policy composition, protocol/retired-name/grammar lookups, rule compilation, the discussion workflow name). This package cannot re-export them (the engine barrel drags Node built-ins into the browser bundle), so it declares the ports and honest-absence readers; a Node host installs the engine's own functions verbatim. The guard pins: every reader returns `undefined` when nothing is installed (never a fabricated empty array or default policy), arguments and results pass through by reference, engine errors propagate unchanged, partial installs read partially, restore functions unwind to the previous bag, and the module source has zero engine imports |
|
|
335
335
|
| `scripts/run-read-face-posture-projection-test.mjs` | The operator-face `readFace: ReadFacePosture` reader (server >=7.65.0). Three ways of "can't say" are pinned to three different, literal sentences, and none of them may read as "nothing is pinned" — that statement belongs to exactly one case, `face: null`, which is a positive fact reported by the engine, not an absence: not having read an operator response yet, having read one from an engine too old to report the key, and the engine actually saying nothing is pinned are three different next steps for an operator and must not collapse into each other. `source` is read as an open set (the server's closed four words plus an escape hatch) rather than narrowed to an enum, so a new word added upstream is not silently turned into a bad reading. The free-text `note` is sanitized and length-capped before it is ever rendered. A companion pure function flags disagreement between this face and the tenant-facing `capabilities.readFace` — silent only when the two actually agree, honest-absent when either side cannot be read at all, never asserting agreement as a fact. The gate's last leg reads the installed SDK's own `openapi.yaml` directly rather than restating the schema in prose, so the package's leniency cannot quietly drift from the real contract |
|
|
336
|
+
| `scripts/run-display-body-test.mjs` | The engine wraps text it hands a model in a fence — an opening marker naming the payload, the payload itself, and a closing marker — so the model reads it as data and not as instructions. That fence is minted and read in one place here, which makes stripping it for a human reader this package's job rather than each shell's: a shell that renders the envelope verbatim is showing a person a defence that was written for a model. The reader answers with a discriminated union — fenced, with the label and the payload, or not fenced, with the text as it came in — and it reaches that answer through the **same** matcher the mint side registers, never a second copy of it; the guard proves that by walking the syntax tree of every source file and requiring exactly one literal carrying the marker text, and by requiring the reader's own body to contain no matcher of its own. Eighteen shapes are run through both entry points and required to agree line for line. Anything the package does not recognise — a near-miss in the wording, a hyphen where the marker has a dash, a different case, an opening marker with no close, a close before an open, a truncated close, or any non-whitespace byte outside the pair — comes back unfenced with the input returned **verbatim**: no guessing, no trimming, no repair, because a half-stripped envelope puts a sentence on screen that nobody wrote. Only the outermost layer is removed, so a nested fence, or one forged inside the payload, survives byte-for-byte in the body — those bytes are part of what the engine said, not part of this protocol. Nothing else is washed: control characters, leading and trailing whitespace and a twenty-thousand-character payload all pass through untouched, and so does the label, because sanitising and length-capping belong to the mint point that puts a string on a screen and a passage of text must not have two launderers. A value that is not text is answered with **nothing at all** rather than with an empty payload: the reader never stringifies it, never calls its `toString`, and never emits `[object Object]`, and it does not hand back a body of zero length either — an empty payload is a real reading (a fence can legitimately wrap nothing, and an empty string is an empty string), so folding "there was no readable text" into it would leave a caller unable to show a degraded line at all. Those three stay apart: no text yields nothing, an empty string yields an unfenced empty payload, and an empty fenced payload yields a fenced one with its label. The `fenced` discriminator is always present on a reading, and the label key exists only on the fenced arm, so a missing label is never rendered as an empty one |
|
|
336
337
|
| `scripts/run-display-cap-order-test.mjs` | The order in which untrusted text is sanitised and length-capped, across every mint point that puts an engine- or database-supplied string on a screen. The sanitiser rewrites each invisible character as a six-character escape, so capping the **raw** string first and escaping afterwards hands the screen six times the width that was budgeted — a forty-character allowance becomes two hundred and forty. The guard does not hardcode that allowance, because each mint point wraps its field in different fixed prose and the prose moves: it anchors on the deciding quantity instead, feeding one benign and one control-character input of the same length through the same mint and requiring the second not to come out longer. That criterion is immune to wording changes and stays sensitive to the expansion, and it is `<=` rather than `==` on purpose — a correct escape-then-cap backs the cut off a partially-consumed escape token, so the control-character line is legitimately the shorter of the two, and demanding equality would score that avoidance as a regression. Each mint is bracketed by two positive controls (the input really reaches the screen; the cap really engages) and the expansion predicate is shown to turn red against a deliberately cap-then-escape reference, so an all-green run cannot mean the guard simply measured nothing. The shared mint point is checked directly for the two avoidances it owes — never splitting an escape token in half, which would leave something on screen that looks like the beginning of a complete answer, and never splitting a legal surrogate pair, which would manufacture the very lone surrogate the sanitiser exists to catch |
|
|
337
338
|
| `scripts/run-seat-task-request-origin-test.mjs` | Where every field of the seat lane's send-message payload comes from, and whether it actually lands anywhere. The seat payload is a closed interface this package mints itself, and most of its fields are meant to ride verbatim onto the engine's request body — two facts nothing used to connect, so both directions could drift in silence. A seat field could be named after a request position that does not exist, in which case a client writes to it, the wire carries it, the engine ignores the whole key, and the screen shows a switch that does nothing; conversely a new request position could arrive with no seat to sit in, which is **structural** absence — the closed set *is* the carrier, so a decision missing from it has nowhere to be put at all, the same shape logged when the effort dial had no seat. The guard turns each field's origin into data: either it names the request position it forwards to, or it is declared seat-local with a written reason, and the two are mutually exclusive. Forwarding claims are then checked against the **installed** SDK's type declarations, parsed rather than restated — a hand-copied list of position names would only ever prove that two transcriptions agree. The parser is held to reading top-level positions only, since a nested option object's inner keys would otherwise be mistaken for positions of the request itself, and it proves that discrimination on synthetic input before any verdict is given. The two subagent fields carry a standing regression pin, and the retention window's inner keys are read from the declaration the same way, so a seat that offers a tunable window cannot offer one the wire has no room for |
|
|
338
339
|
| `scripts/run-wire-auth-source-test.mjs` | **When** the outbound credential is read. A literal string is consumed at construction — the transport captures it in a closure and every later request reuses that one copy — so once the engine is replaced by another session and the credential rotates, a long-lived client keeps presenting the old one and the only way out is to rebuild the client along with everything hanging off it. The credential position now also accepts a getter that is called **once per outbound request**. The guard anchors on the deciding quantity, which is not "was the getter called" — reading once at construction and reusing the result would satisfy that too, and is exactly the shape being removed — but *which read produced the value on the wire*: it changes the getter's answer between two requests through the same client and requires the second request to carry the new one, and it requires construction to read the getter **zero** times. The three-state credential semantics are replayed per request rather than assumed: on loopback an unavailable credential sends **no** authorization header at all rather than a fabricated one, off loopback it sends the fail-closed anonymous identity so the deployment answers with an honest 401, and the guard shows a single client moving between those states across successive requests. A getter that throws is fail-soft — the request still goes out under the no-credential branch, because a broken credential port should not take the whole wire down, and the exception may itself carry credential material. The same-origin relay form is checked to stay out of the getter path entirely, and every request is checked to keep the credential in the authorization header only — never in the URL, never in another header |
|
|
@@ -373,6 +374,7 @@ public-surface guard checks that last one).
|
|
|
373
374
|
| `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 |
|
|
374
375
|
| `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" |
|
|
375
376
|
| `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 0.71.0 adds sections F–I: the fourth/fifth/sixth manifest sections (`tools` via the roster reader, `hooks[]` rows dropped one by one when malformed, `lsp` absent unless `mounted` is a boolean), the `tool_roster_delta` arm (narrowed `delta`, `malformed` when `fromDigest`/`roster` cannot be read, host applies it against its own digest), the `context_usage` arm (finite-gated scalars plus `sections[]` rows dropped one by one), and the `WiringManifestMcpEntryView` rename with `MAX_AGENT_SKILLS` gone from the surface |
|
|
377
|
+
| `scripts/run-submit-wiring-manifest-test.mjs` | The non-streaming submit receipt can carry the run's opening wiring manifest (`TaskResult.wiringManifest`, additive on newer servers). `readSubmitWiringManifest` answers one of three: the key is absent on the receipt itself (older server, or a deployment whose engine never produced that frame) — not the same as unreadable; the key is present but cannot be read (not an object, or none of the nine sections survive); or a manifest view. The view is the same shape the streaming lane's chrome event carries (minus its two envelope keys) and is assembled by the same code path, so both lanes agree byte for byte on the same object. Liveness fields ride through untouched — this reader never mints a liveness verdict — and an operator-shaped receipt with extra governance sections reads to the same view as a tenant-shaped one. A zero-tool roster is a real reading, not an absence. |
|
|
376
378
|
| `scripts/run-rule-offers-reader-test.mjs` | The narrowing reader behind the "don't ask again" options, now a public entry point rather than a card-port-only one. Hosts that render the frame themselves (a browser has no three-way terminal card) previously had to rebuild this reader on their side, and what it carries is a **redemption-safety** judgement, not a convenience: the batch arm is redeemed by **index**, so a reader that compacts the array after dropping a malformed entry makes the k-th option a person clicked and the k-th rule the server writes two different rules. So: a bad entry is dropped **on its own** (one bad option must not make a real one disappear) while every surviving entry keeps its **original wire index** — pinned from both ends, with the bad entries leading and trailing. A batch's *members* are the opposite: any malformed member drops the whole batch, because a conjunctive batch is one "yes" to all of them and a batch missing a member is a different grant; its honest-remainder count is a reading, not decoration, so a non-integer or negative value drops the batch rather than rendering a fabricated zero. An empty array, a non-array, an over-cap array and an all-bad array all read as **absence** rather than an empty list, because an empty list renders as "there is an option lane with nothing in it". The two wire generations are ordered by a rule, not a preference: the newer key wins outright, a newer key that is **present but unreadable** does not fall back to the retired key (borrowing the older material would pass someone else's options off as this request's), and a `null` newer key reads as absence so a relaying layer that serialises "missing" as null cannot delete the whole lane on older engines. The public entry is finally reconciled against **both** card-port legs on the same material, byte for byte, so the exported reader and the one the card sees can never become two. Two upstream vocabularies used to be **hand-copied** here, and both had fallen behind: a match word outside the copied pair dropped an otherwise valid option outright, and a batch carrying a directory-read member — a member kind the copy did not know — dropped the whole batch. Both tables now come from one place upstream and are re-exported verbatim, pinned in both directions: every word in the table must be accepted (a narrower copy reds on the words it never learned) and a word constructed to be outside it must still be refused (a reader widened to "any string" reds too), with the retired-key normalising leg sharing the same narrowing so the fix cannot land on one leg only. A member whose kind is genuinely unknown still drops **the whole batch and only that batch** — never one member, because a conjunctive batch one member short renders "yes to N" as "yes to N−1", and never the card, because the honest single beside it is intact — while a member from before the discriminant existed normalises to the historical kind rather than being refused. The additive per-segment reasons ride through verbatim, drop only the row that is malformed, and stay **absent rather than empty** when nothing survives, since an empty list would read as "confirmed nothing uncovered" while the count remains the only source of truth |
|
|
377
379
|
| `scripts/run-resume-refusal-copy-test.mjs` | The **words** a client says when a resume is refused, minted once here instead of three times. The facts behind them already lived in this package; the sentences did not, so each client wrote its own — and those sentences answer a safety question (was my decision consumed, can this token still be redeemed), which is exactly the kind of answer that must not vary by client. Two closed sets meet here and the guard pins their relationship in both directions, because it is a premise rather than a coincidence: one set answers *can waiting help* (the codes the server mints a wait on), the other answers *what should a person be told*, they **intersect in exactly one code**, and each keeps a member the other must not have — a placement mismatch is never waitable no matter what arrives on the response, since its remedy is a changed argument rather than elapsed time, and a full governance window needs no prose because "you can wait" is the whole message. The overlapping code delegates its wait and its disposition to the existing reading rather than judging again: nine shapes of input drive both entry points and the two readings must agree byte for byte, the absent case included, because two judges always diverge somewhere. The wait is narrowed to the domain the server mints it in, which is **stricter than the shell's own copy was** — a zero now reads as no window rather than as "retry now", and the wake-up it would retry is an at-most-once action with real side effects. The third sentence is chosen by the disposition, never by the engine's prose: rewriting the message to either upstream branch's exact wording, with the window untouched, must leave all three sentences unchanged, while adding a window must change the third one and only the third one |
|
|
378
380
|
| `scripts/run-resume-retry-later-test.mjs` | The two resume refusals that carry a **wait quantity** — the only members of that refusal family that do, which is the whole reason they form a closed set. Carrying a wait is not the same as being the only ones worth waiting on: a sibling refusal in the same family clears on its own and the engine says so in words, it just cannot put a number on it, so *not recognised here* must never be read as *waiting will not help*. One of the two also has a *terminal* upstream branch that arrives under the same code with the distinguishing detail only in prose, so recognition alone is not permission to say "try again": the disposition is decided by **positive evidence** and pinned from both directions — the quota-window code is evidence in itself, the preflight code counts only when the server really supplied a wait (an upstream fact, not a convention: the terminal branch throws with no detail at all, so a wait value cannot reach the client on that path), and a preflight refusal with no wait reads as *undecidable* (say what is true of both branches — nothing was consumed — and leave redeemability to the engine's own line) rather than being rendered as either a retry or an ending. Every other member means waiting will not help (change a setting, relaunch, the retained session is gone), so the recognition is a **closed set of two codes**: widening it to a family prefix would tell half the users to wait and the other half to keep waiting for something that will never arrive, and the negative controls drive exactly those codes through it, plus a same-named code on a different door (the submission-side quota refusal), the two underscore-form siblings, and a code merely quoted inside a message body. The wait value is narrowed to the same domain the server mints it in (a whole number of seconds, at least one): zero, a negative, a fraction and a non-number all read as **no window given** rather than as zero, because a zero tells the caller to retry immediately and the wake-up it would retry is an at-most-once action with real side effects. Reading is structural rather than `instanceof`, since the client is host-injected and the same class name across two bundles is two classes, and a null-prototype plain object must still be recognised. The failure classifier gains this one disposition without any existing one moving, an unknown code still falls to the honest open-set arm and its wait value is **not** believed, and an end-to-end call proves the disposition and the window reach the host while the call itself is still attempted exactly once. The recognised code set is a **frozen array**, not a type-level readonly set: the latter is a plain mutable collection at runtime and the decision reads the same instance, so one `.add` from any consumer would turn a refusal that waiting cannot fix into one that claims it can — the guard proves it by really trying to mutate the exported value and then checking the verdict did not drift |
|
|
@@ -398,6 +400,7 @@ public-surface guard checks that last one).
|
|
|
398
400
|
| `scripts/run-peer-lane-rules-write-capability-test.mjs` | Two more engine self-descriptions read the same four-state way as their seven sibling capability readers (`capabilities.peerLane`, `capabilities.permissionRulesWrite`): an absent key is not reported (an older engine that predates the position, never folded into `false`), `true` is present, `false` is a positive absent (the cross-session lane not being mounted on this deployment, or this particular call not being able to reach the tightening-direction write entry), and anything else is unreadable and drops the cell. Each carries its own single-source verdict (`peerLaneAvailable` returns `yes`/`no`/`unknown`; `permissionRulesWriteAvailable` collapses to a plain boolean, present being the only `true`). The write-entry position pairs with a boolean convenience port in the persisted-rules module, and this guard pins that port to derive from nothing but this one reader's own reading — never a conjunction with the lane-reachable position, and never a second read of the deployment-level existence signal the revoke surface uses (the two are documented as reading differently on purpose): a deployment where the lane answers true but the write entry's key is simply absent (an older binary) must still come back `false`, a deployment where the write entry answers true while the lane key is entirely unseen must still come back `true` (proving no silent conjunction crept in), seeding only the general capabilities cache — never this reader's own feed — must still come back `false` (proving the convenience port cannot be satisfied by the wrong table), and passing an explicit `undefined` base URL must still come back `false` even while a different, already-installed engine target answers `true` for the same position (an adversarial pass found the naive forward of that parameter falls through to the reader's own convenience default, silently answering for whichever engine happens to be installed rather than the caller's absent target — the fix routes an explicit absence through the same empty-string path the reader treats as unobserved). |
|
|
399
401
|
| `scripts/run-lane-proof-identity-test.mjs` | The **instance identity of a lane proof**: the main-lane proof is minted fresh on every emission. Previously a single module-level constant object was handed both to `laneOf(an unregistered task id)` and to some fifty main-lane emission points, so two unrelated consumers — across adapter instances, across streams, across turns — held the same object: writing a card id onto one of them was readable on the other, and the four opening main-lane events changed together. Nothing in this package writes to a lane proof and the known consumers only read it, so this is an **aliasing hazard on a published output surface** rather than an observed corruption — a consumer that uses the proof as an identity key, for dedup, or as a view-layer identity would conflate two unrelated rows without writing a single byte, which is precisely the half that freezing the object would not solve. The gate therefore anchors on instance identity: two independent adapter instances, two rows inside one instance, the same id read twice, and two arms in one beat are each distinct references; mutating one leaves the others byte-identical; and the subagent lane, which already minted fresh, is the control that proves the criterion discriminates. The main-lane **value** is unchanged — an unregistered id still answers `{lane:"main"}` with exactly one own key and still emits its events, so absence is not turned into a second kind of absence — with ordering pinned three ways (registered-then-read, read-then-registered with no retroactive edit of an already delivered proof, the same id twice) and the id failure classes pinned four ways (unregistered, empty string, absent, non-string, the last two emitting no panel event at all rather than an ownerless proof). Where one row emits **two** events — the terminal-tick and card-close legs, which each yield a lifecycle stop and a panel end — the attribution is decided once (a consumer binding a card between the two yields must not split one row across two lanes) while each event still gets its own proof, so a host consuming them one at a time cannot poison the second before it is even yielded. The run stream leg is covered as the same shape, and a syntax-tree check forbids reintroducing a module-level lane-proof object literal or a module-level `LaneProof`-annotated binding (judged on the type node, not on text, so a compile-time pin tuple that merely mentions the type is not miscaught), backed by a type-checker pass that also catches an un-annotated module-level cache such as `const x = mainLane()` while letting the callable factory itself through, while the module-private three-state sentinels of the untrusted read are frozen instead — only `Object.freeze` counts, never `Object.seal`, which still permits writes to existing keys — their exposure being confined to one module |
|
|
400
402
|
| `scripts/run-memory-entries-wire-test.mjs` | The two memory-governance capability bits and the three memory-entry response readers. Each bit (`capabilities.memoryCompliance`, for the entry-provenance and erasure endpoints; `capabilities.memoryOrigin`, for the external-origin listing and clearance endpoints) is read the same four-state way as its sibling capability readers: an absent key is reported as not reported (never folded into `false` — an older engine simply does not answer, and the right next step is to try the endpoint and read its 501), `true` is the face being mounted, `false` is a positive "not on this deployment" (the wire does not distinguish a backend without control-plane ownership from an empty operator roster, so the wording never guesses which), any non-boolean value is unreadable and drops the cell instead of being folded into "absent", and a capabilities body that is not an object at all is unreadable rather than "not reported". The two bits deliberately stay **two** readers with two separate per-engine tables, because the engine deliberately keeps them two separate product faces even while they happen to carry the same value today: feeding one an unreadable body, or invalidating one, leaves the other's reading untouched, and a body where one is on and the other off is answered one bit at a time. The entry-export reader narrows each row on its own (an empty array really is zero rows, a non-empty array with nothing readable in it is reported as unreadable rather than as "no rows", and partly bad rows are kept with a dropped count), reads the external-origin marker as three states rather than a boolean (the two structural carriers mark a row; a row whose frontmatter cannot be read, or which carries the third, suspended-form carrier, is undecidable, because the judge for that carrier lives in the engine and this package refuses to mint a second copy of it), and treats an unreadable "is this the whole scope" flag as "not the whole scope". Its verdict port implements — in code, not in a comment — the rule that an empty answer is never a clean store: the caller must state whether the request declared origin-awareness, because this endpoint withholds marked entries by default and the two bodies are shaped identically, so without that statement an empty answer is only ever "unknown"; the affirmative answer is scoped to the one named scope and carries that scope with it, and the type has no store-wide arm at all. The erasure receipt reader keeps three things apart that are easy to collapse: "this call erased nothing" (a real receipt whose erased list is empty and whose not-found list explains why, per id), "a 200 with an empty body", and "a body that could not be read" — at the reading, the counting and the verdict layer alike; it refuses a version envelope it does not recognise instead of reinterpreting it, treats the three closed vocabularies as closed (an unknown word is unreadable, never folded into a known arm), keeps an unreadable binding as unknown instead of claiming "unbound", passes the "history cannot be judged" flag through as four states (set, explicitly unset, absent, and present-but-unreadable — an unreadable flag is kept distinct from an absent one, and the history verdict then answers "unknown" rather than the stronger claim), and answers the replay question as three states so that the degraded lane is never retried automatically. The clearance receipt reader carries the cleared marker through verbatim and says separately whether it was reported at all. Every array in every response is snapshotted once — the length is read exactly once and each index exactly once, rather than iterating the caller's own iterator — because an array that reports one length while being walked and another afterwards could otherwise have a marked row quietly dropped while the "was anything unreadable" check saw nothing, which ends in calling the scope clean; an array that reports an absurd length is reported as unreadable rather than silently truncated to its first rows. All three readers never throw. |
|
|
403
|
+
| `scripts/run-memory-verbs-wire-test.mjs` | The five memory-governance verbs as call ports — entry provenance, compliance erasure, the external-origin listing, the clearance ledger and the un-mark valve — on top of the readers above. One failure judge serves all five, and its first question is **provenance, not status**: the engine stamps a machine code on every refusal it mints, so a 501, 405, 409, 404 or 400 that carries **no code** proves nothing about who answered — a proxy or gateway returning the same status may well have passed the request on first — and every such answer is reported as "no verdict" rather than as "nothing happened". Twenty-one coded refusals each get their own arm, branched on the code alone: the status cannot tell them apart (nine different operator actions ride the same 409 here), and conjoining the status would silently demote a refusal the day the engine moved it. A coded 5xx, a coded answer with no status at all, and a coded 4xx this version does not recognise all land in the "cannot tell" arm, because on a non-idempotent verb the default for "could not classify" must be "do not know", never "did not happen". The judge is called from catch blocks, so each of its own property reads is guarded: an error object whose accessors throw is classified, not re-thrown. The capability gate runs before the call and reads the two bits the engine keeps deliberately separate (one for provenance and erasure, one for the origin faces); only an engine that positively says the face is off stops the request, while "this binary does not report that bit" and "this process never saw a capabilities body" both still send — folding "cannot say" into "is not there" is the dishonest-absence shape this package refuses, and these routes answer the capability gate before touching anything. A missing capability reading is a named, explainable error rather than a silent default that would answer for whichever engine happens to be installed. Each verb returns its own discriminated union whose success arm, refusal arm and cannot-tell arm share no keys, so a consumer cannot express "could not read it" as "it worked". The two write legs reuse the erasure and clearance receipt readers rather than minting a second copy, which keeps "this call erased nothing", "a 200 with an empty body" and "a body that could not be read" three separate things here too; the provenance account is narrowed only to its envelope and discriminants and otherwise passes through verbatim, and an account stamped with a newer envelope version is refused rather than reinterpreted. All three receipt-bearing verbs additionally reconcile identity — the account id, the attestation request id and the clearance receipt entry id must be the ones that were sent — because a readable receipt is not yet a receipt about this call. The origin listing carries the server's own echo of the scopes it actually audited, the type has no store-wide arm, and the coverage port takes a mandatory second argument and has no "clean" arm at all: the strongest thing it will say is which scopes were audited. Neither write leg is ever retried, including the refusal whose documented recovery is to send again, because that resend completes whichever clearance row is already open and the audit attribution on it is a person's signature. Request bodies are handed over verbatim — the degraded-erasure authorization is never injected — while the scope list is sent as the snapshot this port validated, so an array that reports one length while being read and another afterwards cannot make "the list I checked" and "the list I sent" two different things. |
|
|
401
404
|
| `scripts/run-dist-orphan-test.mjs` | Every `.js` / `.d.ts` under `dist/` must have a same-named source under `src/`, and every source must have its build output — because the compiler only writes and never deletes, so a module removed from the sources keeps shipping from the previous build (the whole `dist/` directory is on the publish whitelist) while the public-surface gate only looks at what the barrel exports and the hygiene gate only looks at forbidden words. Orphans are named one by one; the pre-publish posture is a clean rebuild, and this gate is the check that the posture was actually followed. |
|
|
402
405
|
| `scripts/run-dist-comments-test.mjs` | **dist ships zero comments.** Since 0.77.2 the build strips comments (`removeComments`); this gate walks every shipped `dist/**/*.js` / `*.d.ts` and counts comment trivia with the TypeScript scanner (string literals containing `//` and generator methods are not comments), failing on the first one (`DIST-COMMENT-FAIL`). Source comments are an internal surface; what still ships is code, string literals and type-level text, which the hygiene gate screens. Negative control: one plain comment appended to `dist/index.js` turns it red. |
|
|
403
406
|
| `scripts/run-task-request-omission-receipt-test.mjs` | Where every key a client hands to the request constructor ends up. The constructor used to answer "not stamped" the same way for four different reasons — value absent, no such row, wrong lane, live gate closed — and a key it had never heard of did not even get that: an unattended run could pass a system prompt, an output schema and a spend cap and receive a body holding the objective and the session id, with nothing anywhere saying what was left out or why. The guard pins the three answers apart. **Seated** keys reach the body verbatim on the unattended lane. Keys the package **knows but did not carry** never throw, never reach the body, and each gets a receipt row with one word from a frozen cause list — every present key is on the body or on the receipt, never both and never neither, checked across both lanes with the live gate open and closed against a key-by-key table written independently of the package's own routing. Keys the package **does not know** are refused loudly and are a separate cell, not a fourth cause: the cause list has no word that could hold them, and the seat reader answers `unknown`, not `none`. The cause list is bitten from both sides (exact, every word producible, nothing produced outside it, the judge table's keys read from source through the TypeScript parser) and no second hand-copied list may exist in `src/`. Upstream claims are read straight off the installed SDK typings: a key seated in this release must be a named request field, a key registered as having no upstream counterpart must not be — the day it appears the guard turns red — and the index signature counts as evidence for nothing. |
|
package/dist/adapt/arms.js
CHANGED
|
@@ -12,6 +12,7 @@ import { isTerminalStatus } from '../runTerminal.js';
|
|
|
12
12
|
import { resolveEnginePanelTaskId } from '../engineAgentPanelStore.js';
|
|
13
13
|
import { fleetRowAgentType } from '../fleet/fleetRowAgentType.js';
|
|
14
14
|
import { chrome, mainLane, transcript, messageIdentityOf } from './ids.js';
|
|
15
|
+
import { wiringManifestViewOf } from '../adapter/downstream/wiringManifestView.js';
|
|
15
16
|
import { CANCEL_MESSAGE, decisionOf, flattenWireOutput, REJECT_MESSAGE, sanitizeToolUseBlock, shortTaskLabel, TASK_TOOL_NAMES, WORKFLOW_TOOL_NAMES, } from './wireShapes.js';
|
|
16
17
|
const assistantArm = function* (m, { ctx, idOf, text, cards, inst }) {
|
|
17
18
|
const message = m.message;
|
|
@@ -267,40 +268,10 @@ const engineNoticeArm = function* (m) {
|
|
|
267
268
|
const wiringManifestArm = function* (m) {
|
|
268
269
|
if (isSubFlowSegmentEnd(m))
|
|
269
270
|
return;
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
const mcp = m._sema_mcp;
|
|
273
|
-
const tools = m._sema_tools;
|
|
274
|
-
const hooks = m._sema_hooks;
|
|
275
|
-
const lsp = m._sema_lsp;
|
|
276
|
-
const writeProtection = m._sema_writeProtection;
|
|
277
|
-
const hasWriteProtection = typeof writeProtection === 'object' && writeProtection !== null;
|
|
278
|
-
const autoConsolidation = m._sema_autoConsolidation;
|
|
279
|
-
const hasAutoConsolidation = typeof autoConsolidation === 'object' && autoConsolidation !== null;
|
|
280
|
-
const readDeny = m._sema_readDeny;
|
|
281
|
-
const hasReadDeny = typeof readDeny === 'object' && readDeny !== null;
|
|
282
|
-
const hasTools = typeof tools === 'object' && tools !== null;
|
|
283
|
-
const hasHooks = Array.isArray(hooks);
|
|
284
|
-
const hasLsp = typeof lsp === 'object' && lsp !== null;
|
|
285
|
-
const hasGate = typeof modelGate === 'object' && modelGate !== null;
|
|
286
|
-
const hasAuto = typeof autoMode === 'object' && autoMode !== null;
|
|
287
|
-
const hasMcp = Array.isArray(mcp);
|
|
288
|
-
if (!hasGate && !hasAuto && !hasMcp && !hasTools && !hasHooks && !hasLsp && !hasWriteProtection && !hasAutoConsolidation && !hasReadDeny)
|
|
271
|
+
const view = wiringManifestViewOf(m);
|
|
272
|
+
if (view === undefined)
|
|
289
273
|
return;
|
|
290
|
-
yield chrome({
|
|
291
|
-
kind: 'wiring_manifest',
|
|
292
|
-
laneProof: mainLane(),
|
|
293
|
-
...(hasGate ? { modelGate: modelGate } : {}),
|
|
294
|
-
...(hasAuto ? { autoMode: autoMode } : {}),
|
|
295
|
-
...(hasMcp ? { mcp: mcp } : {}),
|
|
296
|
-
...(hasTools ? { tools: tools } : {}),
|
|
297
|
-
...(hasHooks ? { hooks: hooks } : {}),
|
|
298
|
-
...(hasLsp ? { lsp: lsp } : {}),
|
|
299
|
-
...(hasWriteProtection ? { writeProtection: writeProtection } : {}),
|
|
300
|
-
...(hasAutoConsolidation ? { autoConsolidation: autoConsolidation } : {}),
|
|
301
|
-
...(hasReadDeny ? { readDeny: readDeny } : {}),
|
|
302
|
-
...(typeof m.eventId === 'string' && m.eventId.length > 0 ? { eventId: m.eventId } : {}),
|
|
303
|
-
});
|
|
274
|
+
yield chrome({ kind: 'wiring_manifest', laneProof: mainLane(), ...view });
|
|
304
275
|
};
|
|
305
276
|
const approvalFrameArm = (kind) => function* (m) {
|
|
306
277
|
const schemaVersion = m.schemaVersion;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { AgentEvent, CheckpointGate } from '@sema-agent/sdk';
|
|
1
|
+
import type { AgentEvent, CheckpointGate, TaskResult } from '@sema-agent/sdk';
|
|
2
|
+
import { type WiringManifestView } from './wiringManifestView.js';
|
|
2
3
|
import { type SDKMessage, type EmitContext, type ModelUsage } from '../types.js';
|
|
3
4
|
import { type McpLivenessView } from '../../mcpLiveness.js';
|
|
4
5
|
export declare const INTERNAL_SDK_ARM_TYPES: ReadonlySet<string>;
|
|
@@ -38,6 +39,15 @@ export interface WiringManifestMcpEntryView {
|
|
|
38
39
|
livenessUnreadable?: true;
|
|
39
40
|
}
|
|
40
41
|
export declare function projectMcpSection(raw: unknown): WiringManifestMcpEntryView[] | undefined;
|
|
42
|
+
export type SubmitWiringManifestReading = {
|
|
43
|
+
kind: 'manifest';
|
|
44
|
+
view: WiringManifestView;
|
|
45
|
+
} | {
|
|
46
|
+
kind: 'not_reported';
|
|
47
|
+
} | {
|
|
48
|
+
kind: 'unreadable';
|
|
49
|
+
};
|
|
50
|
+
export declare function readSubmitWiringManifest(result: TaskResult): SubmitWiringManifestReading;
|
|
41
51
|
export interface WiringManifestWriteProtectionView {
|
|
42
52
|
targetView: 'spelling-only' | 'target';
|
|
43
53
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { wiringManifestViewOf } from './wiringManifestView.js';
|
|
1
2
|
import { stamp, snapshotSegmentIdentity, } from '../types.js';
|
|
2
3
|
import { turnUsageToModelUsage } from './turnUsageToModelUsage.js';
|
|
3
4
|
import { gateOutcomeOf } from '../../gateOutcome.js';
|
|
@@ -515,6 +516,21 @@ function projectAutoModeSection(raw) {
|
|
|
515
516
|
: undefined;
|
|
516
517
|
return { armed, reason, ...(deniedSource !== undefined ? { deniedSource } : {}) };
|
|
517
518
|
}
|
|
519
|
+
export function readSubmitWiringManifest(result) {
|
|
520
|
+
if (typeof result !== 'object' || result === null || Array.isArray(result))
|
|
521
|
+
return { kind: 'unreadable' };
|
|
522
|
+
if (!Object.hasOwn(result, 'wiringManifest'))
|
|
523
|
+
return { kind: 'not_reported' };
|
|
524
|
+
const raw = result.wiringManifest;
|
|
525
|
+
if (typeof raw !== 'object' || raw === null || Array.isArray(raw))
|
|
526
|
+
return { kind: 'unreadable' };
|
|
527
|
+
const body = wiringManifestSupersetBody(raw);
|
|
528
|
+
if (body === undefined)
|
|
529
|
+
return { kind: 'unreadable' };
|
|
530
|
+
const eventId = raw.eventId;
|
|
531
|
+
const view = wiringManifestViewOf({ ...body, ...(typeof eventId === 'string' && eventId.length > 0 ? { eventId } : {}) });
|
|
532
|
+
return view === undefined ? { kind: 'unreadable' } : { kind: 'manifest', view };
|
|
533
|
+
}
|
|
518
534
|
function wiringManifestSupersetBody(ev) {
|
|
519
535
|
const modelGate = projectModelGateSection(ev.modelGate);
|
|
520
536
|
const autoMode = projectAutoModeSection(ev.autoMode);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { WiringManifestModelGate, WiringManifestAutoMode, WiringManifestMcpEntryView, WiringManifestHookEntryView, WiringManifestLspSeamView, WiringManifestWriteProtectionView, WiringManifestAutoConsolidationView, WiringManifestReadDenyView } from './eventToSdkMessage.js';
|
|
2
|
+
import type { ToolRosterView } from '../../toolRoster.js';
|
|
3
|
+
export type WiringManifestSections = {
|
|
4
|
+
_sema_modelGate?: WiringManifestModelGate;
|
|
5
|
+
_sema_autoMode?: WiringManifestAutoMode;
|
|
6
|
+
_sema_mcp?: WiringManifestMcpEntryView[];
|
|
7
|
+
_sema_tools?: ToolRosterView;
|
|
8
|
+
_sema_hooks?: WiringManifestHookEntryView[];
|
|
9
|
+
_sema_lsp?: WiringManifestLspSeamView;
|
|
10
|
+
_sema_writeProtection?: WiringManifestWriteProtectionView;
|
|
11
|
+
_sema_autoConsolidation?: WiringManifestAutoConsolidationView;
|
|
12
|
+
_sema_readDeny?: WiringManifestReadDenyView;
|
|
13
|
+
eventId?: string;
|
|
14
|
+
};
|
|
15
|
+
export interface WiringManifestView {
|
|
16
|
+
modelGate?: WiringManifestModelGate;
|
|
17
|
+
autoMode?: WiringManifestAutoMode;
|
|
18
|
+
mcp?: readonly WiringManifestMcpEntryView[];
|
|
19
|
+
tools?: ToolRosterView;
|
|
20
|
+
hooks?: readonly WiringManifestHookEntryView[];
|
|
21
|
+
lsp?: WiringManifestLspSeamView;
|
|
22
|
+
writeProtection?: WiringManifestWriteProtectionView;
|
|
23
|
+
autoConsolidation?: WiringManifestAutoConsolidationView;
|
|
24
|
+
readDeny?: WiringManifestReadDenyView;
|
|
25
|
+
eventId?: string;
|
|
26
|
+
}
|
|
27
|
+
export declare function wiringManifestViewOf(s: WiringManifestSections): WiringManifestView | undefined;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const _viewPin = true;
|
|
2
|
+
void _viewPin;
|
|
3
|
+
const _viewKeysPin = true;
|
|
4
|
+
void _viewKeysPin;
|
|
5
|
+
const isObj = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
6
|
+
export function wiringManifestViewOf(s) {
|
|
7
|
+
const view = {
|
|
8
|
+
...(isObj(s._sema_modelGate) ? { modelGate: s._sema_modelGate } : {}),
|
|
9
|
+
...(isObj(s._sema_autoMode) ? { autoMode: s._sema_autoMode } : {}),
|
|
10
|
+
...(Array.isArray(s._sema_mcp) ? { mcp: s._sema_mcp } : {}),
|
|
11
|
+
...(isObj(s._sema_tools) ? { tools: s._sema_tools } : {}),
|
|
12
|
+
...(Array.isArray(s._sema_hooks) ? { hooks: s._sema_hooks } : {}),
|
|
13
|
+
...(isObj(s._sema_lsp) ? { lsp: s._sema_lsp } : {}),
|
|
14
|
+
...(isObj(s._sema_writeProtection) ? { writeProtection: s._sema_writeProtection } : {}),
|
|
15
|
+
...(isObj(s._sema_autoConsolidation) ? { autoConsolidation: s._sema_autoConsolidation } : {}),
|
|
16
|
+
...(isObj(s._sema_readDeny) ? { readDeny: s._sema_readDeny } : {}),
|
|
17
|
+
};
|
|
18
|
+
if (Object.keys(view).length === 0)
|
|
19
|
+
return undefined;
|
|
20
|
+
if (typeof s.eventId === 'string' && s.eventId.length > 0)
|
|
21
|
+
view.eventId = s.eventId;
|
|
22
|
+
return view;
|
|
23
|
+
}
|
package/dist/gateVocabulary.js
CHANGED
|
@@ -8,6 +8,7 @@ export const GATE_DENIED_BY_WORDS = Object.freeze([
|
|
|
8
8
|
'compliance',
|
|
9
9
|
'write_protection',
|
|
10
10
|
'ask_resolution',
|
|
11
|
+
'read_boundary',
|
|
11
12
|
]);
|
|
12
13
|
const DENIED_BY_SENTENCES = Object.freeze({
|
|
13
14
|
policy: "denied by this deployment's permission policy",
|
|
@@ -19,6 +20,7 @@ const DENIED_BY_SENTENCES = Object.freeze({
|
|
|
19
20
|
compliance: 'denied by a compliance lock',
|
|
20
21
|
write_protection: 'denied because the write landed on a write-protected path',
|
|
21
22
|
ask_resolution: 'denied when the approval was resolved',
|
|
23
|
+
read_boundary: 'denied by the read boundary',
|
|
22
24
|
});
|
|
23
25
|
export function gateDeniedByDetail(deniedBy) {
|
|
24
26
|
if (typeof deniedBy === 'string' && Object.hasOwn(DENIED_BY_SENTENCES, deniedBy)) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CcImportLayer, CcImportPrepareResult, CcImportRedeemResult, PersistedRule, RuleBehavior, RuleListParams, RuleListResult, RuleRevokeRequest, RuleRevokeResult } from '@sema-agent/sdk';
|
|
1
|
+
import type { CcImportLayer, CcImportPrepareResult, CcImportRedeemResult, PersistedRule, RuleBehavior, RuleListParams, RuleListResult, RuleRevokeRequest, RemovalLiveness, RuleRevokeResult, RuleWriteRequest, RuleWriteResult } from '@sema-agent/sdk';
|
|
2
2
|
export interface RulesFacade {
|
|
3
3
|
list(params?: RuleListParams, opts?: {
|
|
4
4
|
signal?: AbortSignal;
|
|
@@ -6,7 +6,7 @@ export interface RulesFacade {
|
|
|
6
6
|
revoke(input: RuleRevokeRequest, opts?: {
|
|
7
7
|
signal?: AbortSignal;
|
|
8
8
|
}): Promise<RuleRevokeResult>;
|
|
9
|
-
write
|
|
9
|
+
write(input: PersistedRuleWriteRequest, opts?: {
|
|
10
10
|
signal?: AbortSignal;
|
|
11
11
|
}): Promise<PersistedRuleWriteWireResult>;
|
|
12
12
|
ccImportPrepare(layers: CcImportLayer[], opts?: {
|
|
@@ -90,17 +90,9 @@ export declare function listAllPersistedRules(facade: RulesFacade, params?: Omit
|
|
|
90
90
|
declare const DIRECT_WRITE_EXCLUDED_BEHAVIOR = "allow";
|
|
91
91
|
export declare const PERSISTED_RULE_WRITE_BEHAVIORS: readonly Exclude<RuleBehavior, typeof DIRECT_WRITE_EXCLUDED_BEHAVIOR>[];
|
|
92
92
|
export type PersistedRuleWriteBehavior = (typeof PERSISTED_RULE_WRITE_BEHAVIORS)[number];
|
|
93
|
-
export
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
scope: string;
|
|
97
|
-
principal?: string;
|
|
98
|
-
}
|
|
99
|
-
export interface PersistedRuleWriteWireResult {
|
|
100
|
-
status: 'persisted' | 'no-op';
|
|
101
|
-
rev: number;
|
|
102
|
-
rule: PersistedRule;
|
|
103
|
-
}
|
|
93
|
+
export type PersistedRuleWriteRequest = RuleWriteRequest;
|
|
94
|
+
export type PersistedRuleWriteWireResult = RuleWriteResult;
|
|
95
|
+
export type PersistedRuleLiveness = RemovalLiveness;
|
|
104
96
|
export declare const PERSISTED_RULE_WRITE_REFUSAL_CAUSES: readonly ["unwritable_behavior", "unreadable_identity", "unusable_principal", "client_too_old", "field_refused", "body_shape", "operator_only", "store_absent", "lane_too_old", "route_absent"];
|
|
105
97
|
export type PersistedRuleWriteRefusalCause = (typeof PERSISTED_RULE_WRITE_REFUSAL_CAUSES)[number];
|
|
106
98
|
export declare const PERSISTED_RULE_WRITE_UNKNOWN_REASONS: readonly ["indeterminate", "malformed_result", "identity_mismatch", "unattributed_method_refusal", "unclassified"];
|
|
@@ -113,6 +105,11 @@ export type PersistedRuleWriteOutcome = {
|
|
|
113
105
|
status: 'no_op';
|
|
114
106
|
rev: number;
|
|
115
107
|
rule: PersistedRule;
|
|
108
|
+
} | {
|
|
109
|
+
status: 'revoked';
|
|
110
|
+
wrote: 'persisted' | 'no_op';
|
|
111
|
+
rev: number;
|
|
112
|
+
message: string;
|
|
116
113
|
} | {
|
|
117
114
|
status: 'refused';
|
|
118
115
|
cause: PersistedRuleWriteRefusalCause;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { engineCapTrue } from '../engineCapsCache.js';
|
|
2
|
+
import { wireFailureShapeOf } from '../wireFailureShape.js';
|
|
2
3
|
import { hostLog } from '../host.js';
|
|
3
4
|
import { observedPermissionRulesWrite, permissionRulesWriteAvailable } from '../permissionRulesWriteCapability.js';
|
|
4
5
|
import { readToolApprovalRespondAck } from './toolApprovalWire.js';
|
|
@@ -33,22 +34,8 @@ export function readCcImportRedeemCounts(result) {
|
|
|
33
34
|
}
|
|
34
35
|
return null;
|
|
35
36
|
}
|
|
36
|
-
function errShape(e) {
|
|
37
|
-
const o = (e ?? {});
|
|
38
|
-
return {
|
|
39
|
-
...(typeof o.status === 'number' ? { status: o.status } : {}),
|
|
40
|
-
...(typeof o.errorCode === 'string' ? { errorCode: o.errorCode } : {}),
|
|
41
|
-
message: typeof o.message === 'string' && o.message !== '' ? o.message : String(e),
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
function retryAfterSecOf(e) {
|
|
45
|
-
const ms = e.retryAfterMs;
|
|
46
|
-
if (typeof ms !== 'number' || !Number.isFinite(ms) || ms < 0)
|
|
47
|
-
return undefined;
|
|
48
|
-
return Math.ceil(ms / 1000);
|
|
49
|
-
}
|
|
50
37
|
export function classifyRulesFailure(e) {
|
|
51
|
-
const { status, errorCode, message } =
|
|
38
|
+
const { status, errorCode, message, retryAfterSec } = wireFailureShapeOf(e);
|
|
52
39
|
if (errorCode === 'capability.rule_store_required') {
|
|
53
40
|
return { kind: 'lane-unavailable', message };
|
|
54
41
|
}
|
|
@@ -60,7 +47,7 @@ export function classifyRulesFailure(e) {
|
|
|
60
47
|
return { kind: 'error', message };
|
|
61
48
|
}
|
|
62
49
|
if (errorCode === 'state.rule_import_retry') {
|
|
63
|
-
const sec =
|
|
50
|
+
const sec = retryAfterSec;
|
|
64
51
|
return { kind: 'retry-same-ticket', message, ...(sec !== undefined ? { retryAfterSec: sec } : {}) };
|
|
65
52
|
}
|
|
66
53
|
if (errorCode === 'state.rule_remove_failed')
|
|
@@ -218,6 +205,8 @@ export async function listAllPersistedRules(facade, params = {}, opts) {
|
|
|
218
205
|
}
|
|
219
206
|
const DIRECT_WRITE_EXCLUDED_BEHAVIOR = 'allow';
|
|
220
207
|
export const PERSISTED_RULE_WRITE_BEHAVIORS = Object.freeze(PERSISTED_RULE_BEHAVIORS.filter((b) => b !== DIRECT_WRITE_EXCLUDED_BEHAVIOR));
|
|
208
|
+
const _writeBehaviorPin = true;
|
|
209
|
+
void _writeBehaviorPin;
|
|
221
210
|
const WRITE_BEHAVIOR_SET = new Set(PERSISTED_RULE_WRITE_BEHAVIORS);
|
|
222
211
|
function isPersistedRuleWriteBehavior(v) {
|
|
223
212
|
return typeof v === 'string' && WRITE_BEHAVIOR_SET.has(v);
|
|
@@ -264,6 +253,36 @@ function writeOutcomeFromFailure(failure) {
|
|
|
264
253
|
return { status: 'unknown', why: 'unclassified', message };
|
|
265
254
|
}
|
|
266
255
|
}
|
|
256
|
+
function certificationSnapshot(res) {
|
|
257
|
+
const KEYS = ['status', 'rev', 'stillLive', 'rule'];
|
|
258
|
+
const cells = new Map();
|
|
259
|
+
try {
|
|
260
|
+
for (const k of KEYS) {
|
|
261
|
+
const d = Object.getOwnPropertyDescriptor(res, k);
|
|
262
|
+
if (d === undefined) {
|
|
263
|
+
if (k in res)
|
|
264
|
+
return null;
|
|
265
|
+
cells.set(k, { has: false, value: undefined });
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
if (!Object.hasOwn(d, 'value'))
|
|
269
|
+
return null;
|
|
270
|
+
cells.set(k, { has: true, value: d.value });
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
catch {
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
const cell = (k) => cells.get(k) ?? { has: false, value: undefined };
|
|
277
|
+
return {
|
|
278
|
+
status: cell('status').value,
|
|
279
|
+
rev: cell('rev').value,
|
|
280
|
+
hasLiveness: cell('stillLive').has,
|
|
281
|
+
liveness: cell('stillLive').value,
|
|
282
|
+
hasRow: cell('rule').has,
|
|
283
|
+
row: cell('rule').value,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
267
286
|
export async function writePersistedRule(facade, input, opts) {
|
|
268
287
|
const port = facade?.write;
|
|
269
288
|
if (typeof port !== 'function') {
|
|
@@ -317,21 +336,36 @@ export async function writePersistedRule(facade, input, opts) {
|
|
|
317
336
|
catch (e) {
|
|
318
337
|
return writeOutcomeFromFailure(classifyRulesFailure(e));
|
|
319
338
|
}
|
|
320
|
-
const
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
return
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
339
|
+
const malformed = () => ({
|
|
340
|
+
status: 'unknown',
|
|
341
|
+
why: 'malformed_result',
|
|
342
|
+
message: 'the rule write answered 2xx with a body this client cannot read (status, rev, liveness or the written row) — refusing to certify it as a standing rule; re-list the rules to reconcile',
|
|
343
|
+
});
|
|
344
|
+
if (res === null || typeof res !== 'object' || Array.isArray(res))
|
|
345
|
+
return malformed();
|
|
346
|
+
const snap = certificationSnapshot(res);
|
|
347
|
+
if (snap === null)
|
|
348
|
+
return malformed();
|
|
349
|
+
const { status, rev, hasLiveness, liveness, hasRow, row } = snap;
|
|
350
|
+
if ((status !== 'persisted' && status !== 'no-op') || typeof rev !== 'number' || !Number.isFinite(rev))
|
|
351
|
+
return malformed();
|
|
352
|
+
const wrote = status === 'persisted' ? 'persisted' : 'no_op';
|
|
353
|
+
if (hasLiveness) {
|
|
354
|
+
if (liveness === 'no') {
|
|
355
|
+
if (hasRow)
|
|
356
|
+
return malformed();
|
|
357
|
+
return {
|
|
358
|
+
status: 'revoked',
|
|
359
|
+
wrote,
|
|
360
|
+
rev,
|
|
361
|
+
message: 'the rule write landed (the revision is real) but a concurrent revoke arrived before it could be read back — the requested tightening is not standing; do not retry (that would re-create what was just revoked): list the rules to see the current state',
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
if (liveness !== 'yes')
|
|
365
|
+
return malformed();
|
|
334
366
|
}
|
|
367
|
+
if (!persistedRuleIdentityRowReadable(row))
|
|
368
|
+
return malformed();
|
|
335
369
|
const landed = row;
|
|
336
370
|
if (landed.behavior !== behavior || landed.rule !== rule || landed.scope !== scope) {
|
|
337
371
|
return {
|
|
@@ -340,11 +374,7 @@ export async function writePersistedRule(facade, input, opts) {
|
|
|
340
374
|
message: 'the rule write answered 2xx with a row whose identity triple is not the one that was sent — refusing to report the requested tightening as standing; re-list the rules to reconcile',
|
|
341
375
|
};
|
|
342
376
|
}
|
|
343
|
-
return {
|
|
344
|
-
status: status === 'persisted' ? 'persisted' : 'no_op',
|
|
345
|
-
rev,
|
|
346
|
-
rule: row,
|
|
347
|
-
};
|
|
377
|
+
return { status: wrote, rev, rule: row };
|
|
348
378
|
}
|
|
349
379
|
export function classifySkippedReason(reason) {
|
|
350
380
|
const text = typeof reason === 'string' ? reason : String(reason);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { wireFailureShapeOf } from '../wireFailureShape.js';
|
|
1
2
|
import { hostLog } from '../host.js';
|
|
2
3
|
const RULE_FIELD_PRESENCE = {
|
|
3
4
|
toolAllow: true,
|
|
@@ -8,16 +9,8 @@ const RULE_FIELD_PRESENCE = {
|
|
|
8
9
|
};
|
|
9
10
|
export const SESSION_POLICY_RULE_FIELDS = Object.freeze(Object.keys(RULE_FIELD_PRESENCE));
|
|
10
11
|
const MAX_RULE_ENTRIES = 10_000;
|
|
11
|
-
function errShape(e) {
|
|
12
|
-
const o = (e ?? {});
|
|
13
|
-
return {
|
|
14
|
-
...(typeof o.status === 'number' ? { status: o.status } : {}),
|
|
15
|
-
...(typeof o.errorCode === 'string' ? { errorCode: o.errorCode } : {}),
|
|
16
|
-
message: typeof o.message === 'string' && o.message !== '' ? o.message : String(e),
|
|
17
|
-
};
|
|
18
|
-
}
|
|
19
12
|
export function classifySessionPolicyFailure(e) {
|
|
20
|
-
const { status, errorCode, message } =
|
|
13
|
+
const { status, errorCode, message } = wireFailureShapeOf(e);
|
|
21
14
|
if (errorCode === undefined || errorCode === '')
|
|
22
15
|
return { kind: 'no-verdict', message };
|
|
23
16
|
if (status === 501 || errorCode === 'capability.session_store_required') {
|
package/dist/index.d.ts
CHANGED
|
@@ -30,6 +30,7 @@ export * from './sessionPolicyCapability.js';
|
|
|
30
30
|
export * from './memoryComplianceCapability.js';
|
|
31
31
|
export * from './memoryOriginCapability.js';
|
|
32
32
|
export * from './memoryEntriesWire.js';
|
|
33
|
+
export * from './memoryVerbsWire.js';
|
|
33
34
|
export * from './deviceExecutorManagementCapability.js';
|
|
34
35
|
export * from './mcpReconnect.js';
|
|
35
36
|
export * from './leaderConflict.js';
|
|
@@ -85,6 +86,7 @@ export * from './effortWire.js';
|
|
|
85
86
|
export * from './adapter/types.js';
|
|
86
87
|
export * from './adapter/downstream/turnUsageToModelUsage.js';
|
|
87
88
|
export * from './adapter/downstream/eventToSdkMessage.js';
|
|
89
|
+
export type { WiringManifestView, WiringManifestSections } from './adapter/downstream/wiringManifestView.js';
|
|
88
90
|
export * from './adapter/downstream/terminalToSdkResult.js';
|
|
89
91
|
export * from './adapter/runStream.js';
|
|
90
92
|
export * from './adapter/activeRunSelfHeal.js';
|