@sema-agent/client-core 0.101.0 → 0.102.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/CHANGELOG.md +57 -0
  2. package/README.md +12 -7
  3. package/dist/adapt/toolCards.d.ts +2 -1
  4. package/dist/adapt/toolCards.js +20 -6
  5. package/dist/adapt.d.ts +1 -1
  6. package/dist/adapt.js +1 -1
  7. package/dist/adapter/downstream/eventToSdkMessage.js +6 -68
  8. package/dist/adapter/downstream/wiringManifestView.d.ts +1 -0
  9. package/dist/adapter/downstream/wiringManifestView.js +138 -0
  10. package/dist/compensations.js +1 -1
  11. package/dist/displayUntrusted.d.ts +8 -4
  12. package/dist/displayUntrusted.js +769 -96
  13. package/dist/engineRowRef.js +1 -0
  14. package/dist/fleet/fleetProjection.js +1 -1
  15. package/dist/i18n/messages.en.d.ts +38 -0
  16. package/dist/i18n/messages.en.js +38 -0
  17. package/dist/i18n/messages.zh.js +40 -0
  18. package/dist/index.d.ts +8 -0
  19. package/dist/index.js +5 -0
  20. package/dist/interactiveHalt.d.ts +3 -1
  21. package/dist/interactiveHalt.js +2 -0
  22. package/dist/interactiveInterrupt.d.ts +2 -0
  23. package/dist/interactiveInterrupt.js +15 -0
  24. package/dist/modsFace.d.ts +8 -0
  25. package/dist/modsFace.js +59 -0
  26. package/dist/ownKey.d.ts +1 -0
  27. package/dist/ownKey.js +15 -0
  28. package/dist/printToolResultFrame.js +1 -1
  29. package/dist/request/printNotification.js +1 -1
  30. package/dist/request/taskRequest.d.ts +21 -0
  31. package/dist/request/taskRequest.js +123 -7
  32. package/dist/seam.js +1 -1
  33. package/dist/seatContract.d.ts +22 -6
  34. package/dist/seatContract.js +35 -6
  35. package/dist/sessionBackground.d.ts +165 -0
  36. package/dist/sessionBackground.js +781 -0
  37. package/dist/sessionBackgroundHandoff.d.ts +54 -0
  38. package/dist/sessionBackgroundHandoff.js +321 -0
  39. package/dist/sessionMap.js +4 -1
  40. package/dist/settingsCredentialKeyPaths.js +2 -17
  41. package/dist/thinkingLatch.d.ts +69 -0
  42. package/dist/thinkingLatch.js +255 -0
  43. package/dist/thinkingLatchWords.d.ts +7 -0
  44. package/dist/thinkingLatchWords.js +4 -0
  45. package/dist/toolResult.js +2 -24
  46. package/docs/INTEGRATION-CLIENTS.md +309 -10
  47. package/package.json +1 -1
  48. package/dist/argvFlagValue.d.ts +0 -8
  49. package/dist/argvFlagValue.js +0 -37
package/CHANGELOG.md CHANGED
@@ -49,6 +49,63 @@
49
49
  > 挡住 ⇒ 本批把它机械化——④a0 对 `pending` 行**要求段头已是日期形**(`(未发布)` 直接红),阶段一
50
50
  > commit 漏转在发布前就红,不再靠人记。
51
51
 
52
+ ## 0.102.0(2026-10-28)
53
+
54
+ > 主题:0.102.0 会话送后台四读口归包(能力位读数 · 连线句柄与本包亲手开流 · 交接资格一只判官 · 交接动词与后台观看)接入文档 **§130 BG-1 / BG-2 / BG-3 / BG-4 / BG-5 / BG-6** · 会话级思考开关归包(状态机 + `reasoningEffort` 单铸点投影 + 座位动词 / 能力位 / 会话行位)接入文档 **§130 TL-1 / TL-2 / TL-3 / TL-4 / TL-5** · mods 三类用户面座位(注册卡 / 工具结果行旁拒句 / 代答句)与桌面装载座位的读口 / 句口 —— 零新臂、既有公面型零改动、`./mods` 子路径入口零改动;接入文档 **§130 MF-1 / MF-2 / MF-3 / MF-4 / MF-5 / DU-1 / DU-2 / DU-3 / PS-1 / PS-2 / PS-3 / PS-4**(DU / PS:配置地址展示口七臂、无方案 userinfo 与边界判据进通用网、展示口非串入参 · 配置地址路径段与 Slack webhook 锚定形、设置判官读同一只扫描、凭据面规则按版本的阶梯链) · 会话映射的引擎命名空间键含规范化路径(经中心中继的多台引擎同 origin 异路径不再串键)与出包文案卫生(两条出包串中性化、同形存量清理、卫生门词表扩形),接入文档 **§130 NK-1 / HY-1**。
55
+
56
+ ### Added
57
+
58
+ - **能力位读数 `readSessionBackgroundable(baseUrl?)`**(CC-983;**§130 BG-1**;根入口运行期 +2〔读口 `readSessionBackgroundable` + 同读器的投影函数 `projectSessionBackgroundable(caps)`,与能力位读器族同形、读器族门按名取它;宿主读只用读口〕,型 +1):`GET /v1/capabilities.sessionBackgroundable` 的五态读数 —— `advertised`(位为 `true`)/ `declined`(`false`)/ `not_reported`(键缺或自有值 `undefined`)/ `unreadable`(非布尔、`null`、自有访问器 —— 取值器不执行)/ `unobserved`(没有当代落地)。经既有能力缓存读,不另存、不另探;读数冻结、永不抛。只有 `advertised` 放行交接;交互流带不带 detach 头只把 `declined` 当否决。
59
+ - **连线句柄与本包亲手开流:`interactiveEngineWire(init)` / `retireInteractiveEngineWire(wire)` · 姿态预览 `interactiveDetachPosture(wire, input)` · 开流口 `streamInteractiveRun(wire, req, opts, input)`**(CC-983;**§130 BG-2**;根入口运行期 +4,型 +7;型面 additive):一代引擎一只句柄(生命周期属主声明 `self-spawned` / `external`,`tasks` / `runs` 给原样 sdk 口),代结束同步退役。宿主在**调 sdk `tasks.stream` 的那一处**改调开流口,再把交回的生成器喂给自己的变换管线;本包对请求体取一次顶层浅快照、对 `headers` 取一次快照(任何大小写拼法的 `x-detach-on-disconnect` 都被删掉,带头时放唯一规范值 `'true'`),交给传输的是本包新造的对象;带头那一发被精确 durable-off 400 拒且没钉幂等键时去头重开恰一次。姿态判序:句柄认不出 / 入参读不出 ⇒ 不带 · 句柄退役 ⇒ 不带 · `SEMA_HEADLESS_DETACH` 关 ⇒ 不带 · 车道接不回在别处跑的 run ⇒ 不带 · 能力位 `declined` ⇒ 不带 · 本机自起 ∧ 环回 ⇒ 带(不要求 `advertised`)· 外接 ⇒ 不带(候引擎受理回执)。每次调用建一条开流记录:`x-task-id` 到 ⇒ 绑定;首个带账本锚的帧(`id` 非空)且 `meta` 报了会话 ⇒ 开跑 —— 重放 / 会话占用拒收 / 店竞态 / 落点拒这几种「回了 run id 却没为这一发跑起来」的流结构上进不了开跑。
60
+ - **交接资格一只判官 `backgroundHandoffEligibility({ wire, sessionKey })` + 句口 `backgroundHandoffSentence(verdict, lang?)`**(CC-983;**§130 BG-3**;根入口运行期 +2,型 +3):读句柄上该会话最近一次开流的记录、能力位读数与审批持有表,15 行有序判序,三态闭合(`eligible` 带 run id / `ineligible` 带确定原因 / `unknown` 带判不出的原因);永久原因先于暂时原因(用户不会被叫「稍后再试」而重试也没用)。没开跑 ⇒ 「引擎还没开始输出」;流已不在最初的实时连接上(断线、终帧是停驻)⇒ 那一句;刚交出的提问 / 审批帧宿主还没处理完、本开流有未结未到期的提问、持有表有本 run 的持有 ⇒ 「先回答屏上的问题」;归属说不出的持有 ⇒ 不确定句。同步、永不抛、判决冻结。
61
+ - **交接动词 `handoffInteractiveRun(req)` 与后台观看(`BackgroundRun`:`openFrames()` / `settled` / `dispose()`)· 交接表 `handedOffRunsOf(wire)` / `forgetHandedOffRun(wire, taskId)` · 停止事实 `interactiveStopFacts(wire, taskId)` · 常量 `BACKGROUND_HANDOFF_ABORT_REASON`**(CC-983;**§130 BG-4**;根入口运行期 +5,型 +7):判官放行 ⇒ 同一拍撕本包开的那条流(**不发 interrupt、不发 cancel**)、登记交接、起后台观看、通知宿主(宿主回调抛不回滚,结局 `hostNotified: false`);否则留在前台并给一句。后台观看在同一只句柄上、带引擎 `meta` 帧报的会话,从耐久账本从头重读;断连按 300 秒窗与退避梯重开、读 run 行合成终帧;首个执行终局落定一次(`completed` / `failed` / `cancelled` / `parked`),窗尽 / `dispose()` / 句柄退役 ⇒ `observation-stopped`。交接表 `running` / `unconfirmed` / `unrecognized-wire` 都按「可能在飞」处置,删行只经 `forgetHandedOffRun`。交接之后到的帧不再交前台,前台那一拉干净结束。
62
+ - **消息目录 +22 键(域 `session.background.*`)**(CC-983;**§130 BG-6**):交接成功 / 宿主没收尾 · 十四条拒句与一条不确定句 · 五条后台落定句 · 同一对话撞上后台回答时的 `busy` 句(宿主按 409 的 `activeTaskId` 命中交接表时经 `translate(lang, 'session.background.busy')` 取,本包不另立句口)。
63
+ - **会话级思考开关:状态机与存取端口读写口 `readThinkingLatch(port)` / `writeThinkingLatch(port, event)` + 视图口 `thinkingLatchView(reading, ctx?)` + 句口 `thinkingLatchSentence(subject, lang?)`**(CC-971;**§130 TL-1**;公面运行期 +4,型 +8;型面 additive):开关三态 `on` / `off` / `unset`(未设 ≠ 关:未设在请求面不压档位 —— 档位设置说了算,没有档位时才交给引擎默认)与四事件 `on` / `off` / `toggle` / `clear` 的迁移表只在包里一份(未设时切换 ⇒ 关);宿主只实现一只同步存取端口 `ThinkingLatchStorePort { read(), write(stored) }`(存储形 `true` / `false` / 缺席;一只端口 = 一个会话,包内零模块状态)。读口永不抛、`read` 恰调一次:端口缺席 ⇒ `unset/no_port`、读抛 ⇒ `unset/unreadable`、回 `undefined` ⇒ `unset/never_set`、回非布尔 ⇒ `unset/malformed` —— 读不出一律当未设,不猜开 / 关。写口永不抛:回执 `saved` / `unchanged` / `not_saved`(`no_port` / `bad_event` / `write_failed`);写抛之后再读一次,`now` 只是此刻观察值(不证明存成);存储读不出时照迁移写(修复存储)。视图口给冲突:座位会话而座位能力 `thinkingLatch` 不可用 ⇒ `seat_cannot_carry`(任一开关态);开 × 在线目录行明说这只模型不思考 ⇒ `model_cannot_think`(只披露,不改请求)。句口七个目录键 en / zh(状态三句〔未设说「由档位决定、没有档位时交给引擎默认」〕· 「同一条消息同时带 Ultracode 与关闭思考,以哪个为准由引擎决定」· 模型不思考 · 读不出按未设 · 没存上);座位冲突句转调既有禁用句铸点(不另铸)。门:`scripts/run-thinking-latch-projection-test.mjs`(新)T1 – T3 / T8 / T9。
64
+ - **请求投影:装配口两个控制入参 `thinkingLatch?: 'on' | 'off'` / `reasoningFloor?: string`,产物恒在的投影读数 `assembleTaskRequest(...).thinking`,预览口 `thinkingTurnProjection(input)`**(CC-971;**§130 TL-2**;公面运行期 +1,型 +2;型面 additive):请求体上的 `reasoningEffort` 只由装配口一行铸出,值由一只投影核心算 —— 开关关 ⇒ `"off"`(压过档位词;与 `settings.ultracode` 互不干预:两项同在时照上体,先后由引擎定);开 ⇒ 只解除档位词 `"off"`,从不替用户选档;下限词只认六个非 `off` 档、只抬不降(开关关、或开关投影之后档位仍是 `off` 时不动;开关开先解除档位词 `off` 再按下限抬);旁路车道(`utility`)开关与下限都不参与。🔴 开关未设且不交下限词 ⇒ 请求体与上一发布版**逐字节同**(档位词原样透传,含表外词与非串值)。开关表外值 ⇒ 装配期 `TypeError`(「关」被打字错误吞掉 = 收紧声明丢失);下限词坏值不起作用、不拒。`reasoningEffort` 的门推迟到 `settings` 门之后、对要铸的那个值问一次(这一行三车道有座、不受 live 门 ⇒ 去向回执零变化)。投影读数 `{ word?, verdict: 'utility_lane' | 'off' | 'strength', latch, ultracodeStamped, floorApplied?, strengthOverruled? }` 是本地装配诊断,不上任何载体;预览口交与装配口同一组入参时逐格同答,只绑定一条具体的用户直发、是暂定提示。门:同上 T4 – T6 / T10(T6 对上一发布产物逐字节对拍;T10 对构建产物做语法树计数:装配口内写入点恰 1、投影核心定义 1 / 两口各调 1)。
65
+ - **座位:可缺动词 `setThinkingLatch(req)` → `OkAck`、能力位 `thinkingLatch`、会话行位 `LocalSessionRecord.thinkingLatch`(`on` / `off` / `unreadable` / `malformed`,缺席 = 从未设)+ 行位写法 `thinkingLatchRowWordOf(reading)` / 读回 `readSeatThinkingLatch(record, capability)` + 入站判官 `checkSetThinkingLatchRequest` / 守卫 `isSetThinkingLatchRequest`**(CC-971;**§130 TL-3**;公面运行期 +4,型 +2;🔴 型面 BREAKING(窄):实现 `LocalSessionsApi` 的宿主多一只成员 `setThinkingLatch`):座位会话的开关存在桌面宿主(它装配那个会话的全部提交;渲染端不存、不做乐观本地态)。动词回执复用既有 `OkAck`(`true` = 存成或本来就是,`false` = 没存上);「现在是什么」只读会话行(宿主写后推一次 `session_updated`)。行位四词让「桌面读不出」不被读回成「从未设」;能力不可用 ⇒ 读回恒 `unset/no_port`、不看行位。座位登记七处同加(接口 · 规格方法行与回包判官 · 方法名表 · 入站判官登记与守卫 · 能力名 / 管辖 / 能力态 / 禁用句 / 能力表判官 · 会话行键表与行判官 · 编译钉两向)。门:同上 T7 / T11 + 既有座位门扩格。
66
+ - **接入文档 §130**:TL-4 —— CC-474 ① 的 thinking 半张(开关 ⇒ `reasoningEffort` 的逐请求投影、开关存在哪个宿主)由本票吸收,隐身半张仍归 CC-474;TL-5 —— 与 CC 2.1.309 的偏离登记(130d,含「Ultracode × 关思考:语料未对位」一行与「档位需要思考」报错的射程)。
67
+ - **注册卡读口与句口:`modRowsOf(records)` · `readModRegistrationRefusedNotice(ev)` · `modCardSentence(input, lang?)`**(CC-1042;**§130 MF-1**;根入口运行期 +3):一条腿上每一只 mod 注册的受理结果(挂上 / 被拒两形)在三处出现 —— 回执段 `wiring_manifest.mods[]`、注册被拒通告 `mods.registration_refused` 的 `detail`、进程内内核总线的 `records` —— 现在只有一条读法:`modRowsOf` 对进程内记录与回执段数组同读(非数组 / 读不出 / 非空却零行幸存 ⇒ `undefined`,读不出 ≠ 零;`[]` ⇒ `[]`;坏行逐行丢),两形按成员在场判(`'refusedBy' in row`),不派生处置词(引擎记录分不出「只观察」,包不替它说)。`readModRegistrationRefusedNotice` 把注册被拒通告读成被拒行(码不对 / `detail` 读不成被拒形 ⇒ `undefined`,宿主照渲引擎原话);通告臂、回执视图型、`ChromeEvent` 一字不动。`modCardSentence(视图行)` 出卡句:挂上形「{name} is loaded」,被拒形「{name} was not loaded: {refusedBy} refused it — {reason}」(en / zh;外来串过展示出口)。读口全部永不抛、只读自有数据位(访问器不执行、原型链上的位不算)。
68
+ - **工具结果行信封超集键 `_sema_answered_by` 与行旁拒句 / 代答句**(CC-1042;**§130 MF-2**;引擎 7.56.0 `tool_end.answeredBy`;零新臂):扩展在 `tool.call` 站替一次调用作答(拒了它,或替工具给了结果、本体没跑)时,引擎记下那张注册的名。前层 `tool_end` 投影把它(自有非空串)原样上内部臂(不进转录);收卡唯一行铸口(交互收口与 `-p` 帧同一只)按一句判据铸信封键 —— 令 A := `answeredBy` 是自有非空串且不是引擎自身记号 `engine`(`tool.call` 站本身起不来时引擎记它:那是引擎自己闭合失败,不是某一只扩展 ⇒ 零句)∧ `notExecuted === true`:**拒格** = A ∧ 门记录最新处置 `denied` ∧ `deniedBy === 'hook'` ∧ `isError === true`;**代答格** = A ∧ 门记录不是 `denied` ∧ `isError === false`;门视图带 `unreadable` 先判零;其余一律不铸。判据只在铸口算一次,门视图与同一行信封 `_sema_gate` 是同一次读。`modCardSentence(工具结果行)` 按信封门视图分臂出「Refused by {by}」/「Answered by {by} in place of the tool」(en / zh;只说「被谁」,「没执行」照旧由 `readToolCardOutcome` 说);信封无此键 ⇒ 零句(classic 钩子拒 / `tool.check` 拒 / 政策拒 / 正常跑都没有它 —— 成员缺席零句,不改说「钩子或扩展」,不解析结果句)。键同批进 `TRANSCRIPT_ROW_SIDECAR_KEYS`(元组 +1 元素,型不变):按白名单重建行的端展开它即保住新键。🔴 服务端 7.138.0 之前刻意不投 `answeredBy` ⇒ 经服务端车道今天恒缺席、三端用户面零可见变化。
69
+ - **桌面装载座位:动词闭集 `MOD_LOADER_VERBS`(`list` / `load` / `unload` / `status`)与型 `ModLoaderVerb`;装载失败句 `modLoaderFailureSentence(name, error, lang?)`**(CC-1042;**§130 MF-3**;根入口运行期 +2、型 +1):`list` / `status` 读、`load` / `unload` 请求,集外拒(宿主按闭集判,不猜近义词)。失败句吃宿主 `catch` 到的抛出物原样(抛出物读法复用 `wireFailureTextsOf`):读得出话 ⇒「{name} could not be loaded: {reason}」,读不出 ⇒「{name} could not be loaded」。身份 = `(side, name)`(side 取内核 `ModBusSide`,经 `./mods`);去重契约:surface 侧被拒行按 `(总线, side, name)` 各渲一次,engine 侧拒句只由通告出。装载编排(何时重准入)与宿主自身状态(持久化的「想要」、已渲身份、装载过程 UI 词)归宿主,不进包。
70
+ - **消息目录 +9 键(域 `config.mods.*`)**(CC-1042;**§130 MF-4**):卡两形 `card.mounted` / `card.refused` · 行旁两句 `refusal.by` / `rewrite.resultInPlace` · 失败两句 `loader.failed` / `loader.failedNoReason` · 一条腿的注册计数三句 `card.none`(「No mods registered on this leg」)/ `card.counts`(「{n} mounted」,零被拒)/ `card.countsWithRefused`(「{n} mounted, {m} refused」,有被拒)—— 计数三句不另立句口,壳按 `modRowsOf` 结果里 `refusedBy` 在场的行数选键、经 `translate(lang, key, { n, m })` 取句。
71
+
72
+ ### Changed
73
+
74
+ - **配置地址展示口七臂**(CC-848 ①;**§130 DU-1**;公面零新增):`displayConfiguredUrl` 对「userinfo 与 query / 片段的边界判不准」的写法(方案之后、第一个 `/` `\` 之前,任何 `@` 之前先出现裸 `?` / `#` 而其后还有 `@`:`https://alice:<前半>?<后半>@host/x`,能不能解析都一样)与 http / https / ws / wss / ftp 打头、单斜杠 / 零斜杠写法又读不成带主机的地址、串里带 `@ ? #` 的写法整串显示一枚 `«redacted:secret»`(修前口令两段原样上屏);串首就是无方案 `user:pass@host` 的写法 userinfo 换 `«redacted:userinfo»`、主机与路径照示;无方案、无前导斜杠的相对请求地址(`oauth/callback?code=<值>`)路径留、参数值逐值遮;特殊方案 authority 里夹反斜杠的写法(`https://h\x;sid=<值>/sse`)原串读法与按解析出的组件重建的读法按位置对应取舍 —— 原串读法已遮住重建读法要遮的一切 ⇒ 照原串显示、与上一发布版同字节(`https://alice:\<口令>@host` 仍显示 `https://«redacted:userinfo»@host`;`https://h\x/v1` 原样;`https://h\x?sig=<令牌>` 只遮令牌);否则重建读法遮住原串读法遮住的一切 ⇒ 重建后显示(`https://h/x;sid=«redacted:query»/sse`);两头都不成立 ⇒ 整串一枚 `«redacted:secret»`。authority 之外的凭据记号或 `\uXXXX` 字面不改变读法(`https://h\x;sid=<值>/sse#«redacted:fragment»` ⇒ `https://h/x;sid=«redacted:query»/sse#«redacted:fragment»`),它们按原文位置留在所在组件里;authority 里带本出口写出的东西(整枚记号,或本出口会写的转义记号形 `\u202E` 一类)的写法照原串显示(那是展示过的 authority;KL-8484)—— 展示过的串再展示一遍逐字节不变。
75
+ - **无方案 userinfo 与边界判据进通用网**(CC-848 ①;**§130 DU-2**):URL 结构网开着的出口上(这两条归 URL 结构网),无方案 `user:pass@host` 的口令可以夹 `?` / `#` / `@`(多枚 `@` 取主机之前最后一枚)、主机后的端口不要求数字、主机之后可以是 `) , ; ! ]`;带方案的地址里「边界判不准」那一形整只地址词换一枚 `«redacted:secret»`(与配置地址口同一只判据)。放宽读法只多交候选:每枚 `@` 先按上一发布版的读法读、只有它推进扫描起点,同一段文本里不比上一发布版少遮(`Bearer :=@h)<口令>@h` 照旧整段 `Bearer «redacted:userinfo»@h`)。代价:时刻 / 构建标签形的 `12:30@office, agenda`、`build:release@runner:linux, ready` 也被读成 userinfo(KL-8482)。只开词级网与两网全关的出口逐字节不变。
76
+ - **展示口非串入参**(CC-848 ①;**§130 DU-3**):`displayUntrusted` / `displayUntrustedMarks` / `displayConfiguredUrl` 收到非串的对象 / 数组 / 代理 / 函数 / Symbol 时显示一枚 `«redacted:secret»`,不再调用它的 `toString` / `valueOf` / `Symbol.toPrimitive`(修前执行调用方代码:它抛则口抛、它答什么就上屏什么);数 / bigint / 布尔照旧出字面串,`undefined` / `null` 照旧出空串。选项对象的读法本版不变。
77
+ - **配置地址路径段名位 / 值位**(CC-849;**§130 PS-1**):配置地址口里非 `file:` 地址的每一个路径段,名位(`=` 之前)与值位(`=` 之后;没有 `=` 即整段)过 query 值那一只值判官(高熵形 / 已知凭据字面形 / 嵌套凭据结构):名位判中 ⇒ 整截一枚 `«redacted:secret»`,值位判中 ⇒ 只换值位(`PGPASSWORD=«redacted:secret»`)。网关路径里的钥匙、MCP 能力地址段(`/api/mcp/s/<令牌>/mcp`、`/<uuid>/github`)、RPC 地址的密钥段被遮;代价:账号 ID / UUID 资源段(Cloudflare AI Gateway 账号段等)在配置口也显示成记号(KL-8493)。自由文本里的地址路径段不按形判(与 CC 2.1.309 同)。
78
+ - **Slack webhook 锚定形**(CC-849;**§130 PS-2**):凡开着凭据网的出口,`hooks.slack.com/services|workflows|triggers/` 之后的尾段(带不带 `https://`、主机尾点写法、主机百分号编码都算;与 CC 2.1.309 `slack-webhook-url` 同一识别式,尾段 ≥ 40 字)换一枚 `«redacted:secret»`,主机与 kind 段照示;尾段里已经有一枚本包记号时不再核 40 字下限(整尾一枚)。截短的 webhook(尾段不足 40 字)不遮,与 CC 同。
79
+ - **设置判官与上传判据读同一只扫描**(CC-849;**§130 PS-3**):`settingsCredentialKeyPaths` 判「这串设置值装不装凭据」改为配置地址口的同一次扫描 —— 终态里被遮掉的原文落成凭据种类即装着(片段只在内容按 query 再判一次带凭据时算);修前另写一段「要有 `://`、只看 userinfo ∪ query」的读法。多报:无方案 Slack webhook、路径里的钥匙、单斜杠写法、片段里的 `access_token=`、散文串值里的 `token=<值>`;改口:值里只剩已遮记号的地址(`?token=«redacted:query»`)不再报(没有可漏的内容)。`queryCarriesCredential` 读同一条阶梯。
80
+ - **凭据面规则按版本的阶梯链**(CC-849;**§130 PS-4**):本版新增的两只判法(PS-1 / PS-2)只在旧规则(规则表之外的全部规则,含本版对既有规则的改读 DU-1 / DU-2)跑到帽内不动点之后再上,之后「旧规则 + 新规则」交替到不动点 —— 任何输入的凭据面终态遮盖(封长之前)⊇ 旧规则单独跑到不动点的遮盖(按构造:记号落地后不再解开);相对上一发布版本的「不回退」是冻结池上的实测(原文位置与值字节两把尺子皆 0),不是按构造的保证;同一串文本只收集一次候选、各阶共用。帽按阶计(每阶一帽)。射程:带 `max` 的出口,本版遮得更短时截断点后移,上一版截掉的尾巴可能进窗(那几个字节上一版不截也可见;KL-8495)。
81
+ - **Esc 发射口与停止判定**(CC-983;**§130 BG-5**;🔴 型面 BREAKING(窄)):`fireInteractiveInterrupt(runs, taskId, opts)` 的 `opts` 多可选位 `wire` —— 在场时先在句柄上记「停止已发起」(此后送后台答 `stopping`),这条 run 已在交接 / 已交接 ⇒ 不发 interrupt、答新臂 `{ kind: 'withheld' }`;`wire` 缺席 ⇒ 与上一版逐字同。`InteractiveHaltInterruptOutcome` 多一臂 `{ kind: 'withheld' }`、`InteractiveHaltReason` 多一词 `handed-off`,`planInteractiveHalt` 收到 `withheld` ⇒ `none` / `handed-off`(不升级,`parked` / `detachArmed` 在这一格不读)。对这两只型做穷举 `switch` 的端编译红一次(补一臂:不升级、不提示)。
82
+ - **回执段 `mods[]` 行读法升级为自有数据位读 + 逐键隔离**(CC-1042;**§130 MF-5**;🔴 只对非 JSON 输入可见):回执段投影改调 `modRowsOf`:原型链上的位不算在场、自有访问器不执行;必填位 / 判别位读不出 ⇒ 那一行丢,可选位(`observe` / `skipped`)读不出 ⇒ 只丢那一位、行照出(修前整行一个 try:无关位的取值器抛会连坐有效行,原型链上的位会被收)。JSON 帧无原型链位 / 无访问器 ⇒ 真实输入逐字节同。
83
+ - **出包字符串去帖号别形与修补批过程词;卫生门词表扩形**(CC-1056;**§130 HY-1**;文案卫生,无行为变化):两条点名的出包串改中性 —— 散文段边界臂的说明串去掉帖号别形与仓名,座位契约豁免说明串去掉修补批段号与三处测试脚本文件名(说法改为「本包纯测试套的投毒段」)。按新词表扫 dist 全树,同形存量五个源文件里各一条说明串一并删去帖号别形,对外 README 的两个表行同(表行由登记清单生成)。卫生门 `scripts/run-package-hygiene-test.mjs` 的词表本仓本地加三条:`#` 加 2–5 位纯数字且后面不是字母数字(帖号别形;放行 `CC-nnn`、`[nnnn]`、十六进制颜色码、`文件.md#锚`、HTML 实体)· `FIX` 加数字(含圈号数字)· `fix` 加数字再加「段」(修补批过程词);dist 与 README 全文扫,CHANGELOG 自首个版本段起至 0.101.0 段之前、接入文档 §130 起向前执法(已发段冻结)。纯数字的 2–5 位短色码与帖号别形不可分,命中时改写成颜色名或 `rgb()`;dist 内目前零此类。
84
+
85
+ ### Fixed
86
+
87
+ - **会话映射的引擎命名空间键含规范化路径**(CC-1055;**§130 NK-1**;🔴 行为变,只对带路径的连接目标可见;公面零增删):`engineNamespaceKeyFor(baseUrl)` 修前只取 `new URL(baseUrl).origin`,经中心中继的多台引擎(`https://c.example/api/v1/chat/w/<名>/engine`)同 origin 只差路径,算成同一把键,会话映射里 `engines[键]` 的任务号与同步水位互相串键。现在键 = 小写 origin + 规范化路径:去全部尾斜杠;路径大小写与百分号编码逐字保留(同一台引擎的两种百分号拼法是两把键 —— 分开是安全方向);query / fragment / userinfo 不入键;默认端口与点段由 URL 解析器归一。不变:自起引擎恒 `local`;异 origin 异键;坏串回原串小写。同批收的一形:origin 不透明的方案(`foo://a/x`、无方案的 `host:port`、`blob:`)修前全并成同一把 `null` 键,现在回原串小写。旧映射只有一种读法:路径为空的目标(`http://host:8790` 与 `http://host:8790/`)键仍是纯 origin = 旧键,旧条目原样读得到;带路径的目标不读旧 origin 条目(它可能是同 origin 的另一台引擎写的,读它就是串键),没有回落读法 —— 升级后该目标的水位与任务号按首次同步处理。宿主判「live 引擎与同步目标是不是同一台」时,改比包键(`engineNamespaceKeyFor(a) === engineNamespaceKeyFor(b)`)而不是各自取 origin(130y ②)。门:`scripts/run-session-namespace-key-test.mjs`(新)N / S / L / B / U / X。
88
+
89
+ ### Fixed
90
+
91
+ - **路径段首是一枚记号时整段遮盖不再劈开记号**(CC-849;**§130 PS-4**):段首去边停在记号里时整枚补回截内,`…/v1/«redacted:secret»Bearer%20<值>` 一类输出一枚完整记号(修前 `««redacted:secret»`)。
92
+ - **单斜杠 / 零斜杠写法的配置地址不再把口令显示成路径**(CC-848 ①;**§130 DU-1**;🔴 行为变,只多遮):`displayConfiguredUrl` 对 `https:/alice:\<口令>@host`、`https:\\alice:\<口令>@host`、`https:/alice:\<口令>%40host` 一类写法,上一发布版按解析器重建后显示 `https://alice/<口令>@host`(反斜杠把 authority 收短,口令落进路径原样上屏);现在原串读法遮住的每一段须整段、逐字节落在重建串的同一个组件里才重建,否则(解析器改写了那些字节,或把那一段切到主机 / 路径 / query 两个组件里)整串显示一枚 `«redacted:secret»`(KL-8497)。同一段字在地址里写两遍时按位置认来源,不借另一份的「在场」:`https:/alice:\<口令>@host#alice:\<口令>`(片段 / query / 路径里再写一遍)上一发布版显示 `https://alice/<口令>@host#«redacted:fragment»`,现在整串一枚。解析器没有改写它们的写法照旧重建(`https:/h.example/mcp?x=<令牌>` ⇒ `https://h.example/mcp?x=«redacted:query»`)。
93
+ - **设置键路径对口令藏在反斜杠 / 编码 `@` 后面的地址值报「装着凭据」**(CC-848 ① / CC-849;**§130 PS-3**):`https://alice:\<口令>@host`、`https://alice:\<口令>%40host`、`https:/alice:\<口令>@host` 一类值,上一发布版 `settingsCredentialKeyPaths` 不列出(机读口遮、上传判据判带);现在设置键路径、机读口、上传判据同答「装着凭据」—— 设置判官读配置地址的同一种读法,重建不在判官之前丢掉原串读法认出的口令。
94
+
95
+ ### Gates
96
+
97
+ - **`scripts/run-display-untrusted-phase-ladder-test.mjs`**(CC-849;**§130 PS-4**;包侧门):规则表只一张、行只收加性生成器、阶号只经行发射口;生产路径阶 0 与上一发布产物逐字节同(文本 + 来源表;不同的格只许是本版基础规则的改读、与「表置空」变体同答);不回退尺(可见原文下标 ⊆ 上一版、值字节 0);出口幂等七组 ≤ 上一版、六组逐行不回退;干净文本一次收集;折叠对照(本版行折进阶 0 的单链在构造行上露值);两读法格与嵌套判官三行;读法层不回退(本版配置地址读法与上一版不同的格只许整串扣下,或保住上一版原文读法遮住的每一个字节的重建);反例钉(原文读法遮得到东西的反斜杠 / `%40` / 主机形一族四只读法同答且配置口逐字节为期望、全池 userinfo 证据不在判官之前丢、放宽读法加性、两族洗两遍 = 洗一遍、控制串里的锚定形阶 0 原样);判不准 / 读不成的写法整串扣下不因值里出现记号 / 转义记号形而放开、反斜杠写法在原串读法已遮住重建读法要遮的一切时照原串;两读法遮盖对应的独立尺(黑盒位置对应:原文一个字换一个同类字、看重建串哪几位跟着变,不借本包的组件对齐)—— 重复子串一族与记号 / 转义记号形一族逐字节为期望且四面不露、全池每条配置地址的读法两种口径下成立(重建 ⇒ 原串读法遮住的字节在重建读法里仍遮,单斜杠 / 零斜杠写法遮住的段整段落在同一字段;反斜杠写法照原串 ⇒ 原串可见的字节在重建读法里不被遮);读法层不回退的格改成同一把独立尺(修前是本包判法的副本);本包的位置对应表与同一把黑盒扰动一致(代理对 / 非 ASCII / 主机大写 / 端口);控制串里的锚定形原字 / 展示转义 / 百分号编码三形阶 0 原样;门内负控九枚(阶 1 从原文重起 / 丢阶号 / 判官只用末阶答 / 锚定形撤认记号 / 控制串内容嵌套候选丢阶号 / 屏上与解码读面内容嵌套候选丢阶号 / 裸 query 段丢阶号 / 只让解码读面内容的嵌套候选丢阶号 / 反斜杠写法不先问原串读法)。
98
+ - **展示出口投影门 CF848 / CF849 / K6 段 · 基线差分门 B11 / B12 段 · 设置键路径门的地址支格**(CC-848 ① / CC-849;**§130 DU-1 – DU-3 / PS-1 – PS-3**):终端金样 L5 / L6 / L8 换读本包口、N1 / N2 / S-c 逐格、重建不改写原文读法遮住的字节的十一形与放宽读法加性三形(CF848h / CF848i,负控 ②:`scheme://` 写法不再两读法取舍、一律用重建串 ⇒ 红)、重复子串五形与记号 / 转义记号形六形(CF848j / CF848k)、上一版遮住的口令本版十五出口 + 配置口三面不露(B11e,不分大小写)、上一版配置口露口令 / 段参数值的六形本版三面零字节(B11f);路径段与锚定形四十余格;非串入参九形 × 三口;无方案 userinfo 登记分歧与邻接负例对上一发布产物;能力地址池按秘密片段零残留判;遍数读口改读 `collections`(另报 `passes`)。
99
+ - **`scripts/run-mods-face-projection-test.mjs`**(CC-1042;**§130 MF-1 – MF-5**;包侧投影门):K 进程内真内核(开发依赖内核经 `./mods` 真跑 `admitMods`,判官拒候选)→ 两形行 → 句;W-① 夹具服务端真投影的回执段 + 引擎真铸、夹具服务端真投影的被拒通告 → 读器 → 句;W-②③a 一句判据七格每格至少一帧 × 交互收口 / `-p` 帧两出口,铸口与信封 `_sema_gate` 同一次读,判据只在铸口(语法树),合形帧对开发依赖引擎 `tool_end` 型 `satisfies`(`answeredBy` 一员以开发依赖引擎 ≥ 7.56.0 为前提:之前那一段打 `SKIPPED-SECTION … PENDING-DEVDEP-CORE-7.56.0`,换钉当天自动转正);L 同一拒装经 K / W 两路读出相等 + 九键金样 + 展示出口;A 缺席语义;M 冻结的修前读法 × 既有样本 + 生成池逐字节对拍。负控一枚(读器收原型链上的 `name` ⇒ K 段红)。
100
+ - **`scripts/run-session-namespace-key-test.mjs`**(CC-1055;**§130 NK-1**;包侧投影门):N 键形(异路径异键且逐字 = 手写期望、写法差同键、路径大小写保留、自起、异 origin 异键)· S 同一台判据前置(同 origin 异路径按包键比较不是同一台、同一台的不同写法是同一台;两条目写进同一份记录互不覆盖、按另一路径的键读不到对方水位)· L 旧映射单一读法(路径为空键 = 旧键,旧条目读得到;带路径目标不读旧 origin 条目)· B 边界(IPv6 / 默认端口 / 点段与越根 / 百分号编码 / 双斜杠 / 空路径 / 不透明 origin / `blob:`)· U 导出集恰三名(无第二读法)· X 一批畸形串与随机串永不抛、键对 URL 形的键幂等。修前产物 20 格红;变异五枚全杀;负控一枚(键改回只取 origin ⇒ N1 红)。
101
+ - **`scripts/run-package-hygiene-test.mjs` 扩形**(CC-1056;**§130 HY-1**):帖号别形 / 修补批过程词三条;自检 15 条正样本命中 / 26 条反样本不命中(含颜色码、锚、实体、字中命中)+ CHANGELOG 与接入文档两处向前执法切片自证;旧词表对修前 dist 通过(尺子瞎)、新词表红 11 行(dist 九处 + README 两处),改中性后通过;负控 +2 枚(往 README 植帖号别形 / 植修补批过程词 ⇒ 门红且点名同一节)。
102
+
103
+ ### Known limitations
104
+
105
+ - 本机自起引擎上「撕流不杀 run」是有界信任假设(地址按主机名判环回、引擎不回执 detach 受理)(KL-9831)· 重连期间 / 停驻续流期间 / 接管轮上的回答不能送后台(KL-9832)· 交接那一拍还在网线上的提问 / 审批卡不阻交接、前台不渲(KL-9833)· 交接后同一对话在后台回答结束前不能续聊(KL-9834)· 外接引擎车道一律不交接(KL-9835)。
106
+ - 配置地址口:短 / 无数字令牌仍原样(KL-8491)· 自由文本里非 Slack 的 webhook 路径令牌照样可见(KL-8492)· 账号 ID / UUID 资源段被遮(KL-8493)· 路径里低熵的秘密不在射程(KL-8481)· 无方案 `host/路径?k=v` 按相对请求地址读、每个参数值都遮(KL-8483)· 时刻 / 构建标签形被读成 userinfo(KL-8482)· URL 结构槽「记号 + 标点 + 明文」按已洗(路径段名位 / 值位同 query 槽,KL-8494)· 封长出口的单调射程(KL-8495)· 设置告知多报(KL-8496)· 单斜杠 / 零斜杠写法里解析器改写了原串读法遮住的字节、或把它切到两个组件时整串一枚(KL-8497)· authority 里带凭据记号或本出口会写的转义记号形的反斜杠写法照原串显示(手写这种字面 / 记号的地址,解析器读法里的段参数值不遮,KL-8484)· 点形下 authority 只靠不可见字充当主机又夹反斜杠的写法两次显示不同(第二次只多遮,KL-8498)。
107
+ - **KL-971**:座位会话的组合句预览是暂定提示 —— 排队件以排空那一拍的开关为准,入队时的预览可能与实发不同;座位会话的权威投影读数留在桌面,不新增回传载体(§130 包侧缺口)。
108
+
52
109
  ## 0.101.0(2026-10-27)
53
110
 
54
111
  > 主题:0.101.0 提货引擎 7.54.0(开发依赖 W1 换钉 `^7.54.0` 与生成表重生成,后续 mods 内核车换钉 core `^7.55.0` / mods-kernel 7.55.0 同号;通告码册 96 → 98;`AllowDenyPolicyOptionsView.scope` 退出,🔴 型面 BREAKING)· MCP 服务器名独立判口 · 引擎名册按对端分组投影 · 收紧桶对端段 `*` 原样 · 值级端口第 19 件接入文档 **§129 VP-1 / RP-1 / TF-1 / GT-5 / UA-4 / SP-1 / TF-2 / TR-2 / PK-2 / HK-1 / MK-5 / TS-2 / FL-2 / PD-1 / PD-2 / PD-3 / HK-3 / HP-1 / EV-1 / PR-2 / PR-3 / HP-2 / ER-1 / HT-1 / KP-3 / HS-1 / HS-2 / HS-3 / HS-4 / HS-5 / HS-6 / HS-7 / UC-1 / UC-2 / UC-3 / UC-4 / UC-5 / UC-6 / UC-7 / XA-1 / XA-2 / XA-3 / XA-4 / XA-5 / XA-6 / XA-7 / XA-8 / XA-9 / UB-1 / UB-3 / UB-5 / UB-8 / SF-1 / XC-1 / XC-2 / XC-3 / XC-4 / XC-5 / XC-6 / XC-7 / XE-1 / XE-2 / XE-3 / XE-4 / XE-5 / XE-6 / XF-1 / XF-2 / XF-3 / XF-4 / XF-5 / XF-6 / XF-7 / XF-8 / XF-9 / XF-10 / XF-11 / XF-12 / XF-13 / XF-14 / XF-15 / XF-16 / XF-17 / XF-18 / RQ-1 / MI-1 / MI-2 / MC-1 / CZ-5 / CZ-6 / PK-3 / PK-4 / PK-5 / GN-13 / GN-14 / GN-15 / GN-16 / GN-17 / GN-18 / GN-19 / GN-20 / GN-21 / GN-22 / GN-23 / GN-24 / GN-25 / GN-26 / GN-27 / GN-28 / GN-29 / GN-30 / NA-1 / IE-1 / PE-1 / MN-2 / CD-1 / PM-1 / NS-1 / DR-1 / TK-5 / AF-1 / WS-1 / TH-4 / SR-1 / ME-1 / SK-3 / SK-2 / MC-2 / IE-2 / FB-1 / FB-2 / FB-3 / FB-4 / FB-5 / LG-1 / FV-1 / FV-2 / FV-3 / FV-4 / FV-5 / FV-6 / FV-7 / FV-8 / FW-1 / FW-2 / FW-3 / FW-4 / FW-5 / FW-6 / U11-1**(本版首批 四波已并:+ 跨边界污染读口 · mods 内核子路径转口 · toolFaces 两腿同判 · 会话规则尾句 · 成因薄壳之二 · 工具结果块两新臂;本版 提货引擎 7.55.0:通告码册 98 → 100 · 卡词 8 → 10 · 问类句表 `probe` 无行 · 持久规则 `match` 十词 · 发送前拒绝两码 · 规则拒因码 `invalid.name_ambiguous` · 设置契约 14.4.0 换钉)。
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.101.0
38
+ **Version:** 0.102.0
39
39
 
40
40
  - **Today** — the adapter seam, the whole `adapt()` pipeline (all 14 A-layer arms plus the
41
41
  B/D/E tool-card layers), the notification/caps/model families, the adapter kernel (stream driver
@@ -359,8 +359,8 @@ guard still cross-checks the table by name).
359
359
  | `scripts/run-fleet-view-keys-test.mjs` | The fleet projection views, **both directions**: `FleetTaskView`/`FleetWorkflowView` ⇄ their key lists (compile-pinned) ⇄ what a maximal/minimal row really projects, plus a wire-key coverage ledger (every `FleetTaskRow` key is either projected or carries a written reason why not) and a drift ledger against the shell's render contract. A one-directional assignability check is blind to optional keys — which is how `startedAt` was silently dropped Two keys that no server ever sends have left the view since 0.95.0 — the queue-depth count on a task row (the SDK still declares it, as a phantom key) and the run-level error code on a workflow row (the fleet workflow row never carries it): whatever the input holds, the view has no such key, and the first is recorded in the not-projected ledger with its reason. Where the shell's render contract still declares them, the drift ledger lists exactly those two as a transitional entry that must equal the measured set, so the day the shell drops its declaration the entry is a zombie and the gate says so. |
360
360
  | `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 |
361
361
  | `scripts/run-plan-review-decide-verify-test.mjs` | `decidePlanReview`'s post-decide honesty ([2315]/[2316], engine RB-471 family): a 2xx from the decide endpoint is **not** a terminal — the wire re-pulls the task status and words the outcome by the real shape (still-locked / legal new gate / genuinely left park / unverified), never claiming success it hasn't earned; when the engine answers that the session's stored resume context cannot be read, the outcome names the unreadable row and says the decision was not applied. Driven against a real fake-engine HTTP server through the shipped dist. The outcome queue item also carries a machine-readable `_sema_planReviewOutcome` (task id, a package-minted dispatch number, decision, effect) so a host can tell which in-flight decision an outcome belongs to without searching the prose; the prose is byte-identical, the number is minted only for a decision the in-flight latch admits, and a caller that passes no metadata gets no key. A plan-review card answered with free text and no option picked (or on the reject option with a note) is read as a rejection carrying that text as its reason, on both the first card and a reopened card; the reason is narrowed by the same step as an approval-denial reason (trimmed, blank dropped, capped at 4096 characters) and a note on the card wins over any reason the caller supplied. An approval with a note stays an approval and sends no note; an empty answer, a contradictory multi-select, several questions or an option outside the shown card stay dismissed; an answer without a note produces a request body byte-identical to before. |
362
- | `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 |
363
- | `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. A later section pins the split this release introduced on the deny close-out frame. Until now every denied tool call was stamped with the same sentence — the one that says *the user* does not want to proceed — including the calls denied automatically on a lane that has no approval surface at all, where nobody was ever asked. The guard drives all three shapes (a person pressed No, a rule settled it, nobody said which) through both close-out arms and the durable park leg, and pins that the third shape is byte-identical to the previous release: an attribution nobody supplied is not evidence for either answer. The rule-settled shape carries the shell's own reason on a second line when there is one and stands alone when there is not, because a blank line where a reason should be reads worse than no line at all. The attribution is read from own data properties only, so neither a polluted prototype nor a getter can make an automatic denial claim a person made it — and the getter case is pinned to never run at all. The transcript classification word is minted only on the two paths where the upstream transcript format really carries one; the three classifier words and the two abort words are left absent, with the abort words pinned against the strings this package actually normalises interruptions to, which are different strings. A final section pins the decide-operation observer: one `start` in the same tick as the first request and exactly one `end` after the last attempt has settled, across success, retried timeouts, exhausted transient failures, semantic refusal, binding mismatch and both kinds of caller abort, with nothing between retries; an observer that throws or rejects — even when logging that fault fails — never changes what is sent or returned, and the stream-level dependency reaches both durable park legs. A package-internal re-delivery of the same decision (plain approve after an older server rejects the session-scope flag, or a re-send without the attribution key) is reported as one operation with a single start and end. An operation handle only groups sends for the same session and bound call — a send for another gate through the same handle is its own operation — and closing a handle while a send is still in flight defers the end until that send settles. When a person picks "allow for this session" on a parked card and the grant is known not to have been stored — the server answers so, which newer servers do for the gates they can recognise from the parked row as needing a person each time, or the server refuses the session-wide grant with one of the refusal codes that are fixed by the row or the deployment and the package falls back to a plain approval — the parked path now says so with the same line the live path uses, and the receipt carries the server's bit for hosts that call that path directly; an answer without the bit, or any other failure — including a conflict that an internal retry can hit after an earlier send already stored the grant — is treated as unknown and says nothing, a plain approval that never asked for the grant says nothing, and a host logger that throws after a successful decision, on either card path or in the bridge's retry step, can no longer turn it into a second send or a failure. From 0.83.0 it also pins approval cards whose arguments are not the tool's real input: a live frame whose arguments were omitted over the size cap or never sent (the card holds at most a path recovered from the question), and a parked approval whose stored input is missing or replaced by the upstream size marker. On every such card an edited approval sends nothing at all — no respond, no decide, no session-grant attempt — ends as edit-refused, raises the same notice once and flags the card so hosts do not offer editing; both production entry points are driven end to end, and the parked path neither mistakes it for an already-decided gate nor reconnects. The explanatory line is pinned word for word in its four forms: a recovered path is the only thing it claims to have recovered, an unrecoverable one says nothing was recovered, neither claims a reconstructed diff, and the parked forms each say which of the two gaps it is, with near-miss shapes of the size marker still rendered as ordinary input. Real input on the stream, the frame or the record keeps edits flowing, and plain approvals and denials are untouched. Since 0.83.2 the live frame's `mandate` word reaches the card only when it is one of the six known words, read back identically by `readApprovalMandate`; it never adds the separate mandated key to the card, but a known word on its own makes `approvalIsMandated` answer true, because the engine only sends the word as the whole reason for that bit. The frame's `origin` is read from its own keys only, so a value inherited through the prototype chain never reaches the card. The already-resolved verdict a decision leg uses (`isAlreadyResolvedFailure`: the closed failure code first; codes that say the gate is still standing or failed to start never fall through to the wording check; the moved-gate codes by code; then the four-word wording fallback) is exported from the root entry as the same function the package calls, and a malformed or hostile input answers `false` without throwing, since a `true` would let a caller silently re-attach. The same section compares it, input by input, with the copy a client kept of the same combination, and requires every disagreement to come from one of the four extra checks that copy adds. It also replays the documented replacement order (the closed failure code first, answered by this function; then the copy's extra checks; then this function) and requires it to reproduce the copy's answers on every input, while moving the extra checks ahead of the closed code must disagree somewhere. A refusal that says the stored form of a parked gate cannot be continued while the gate is still pending is classified by its code and its `retriable` bit as a gate that is still standing, so failure text that happens to contain words from the wording fallback no longer makes the leg re-attach; both decision legs carry the bit on their failure outcome. The judge of whether a remember request was refused by the server specifically is a root export — it answers the matching code for the five dedicated codes and nothing otherwise, never throws, and agrees with the durable approval leg code for code. |
362
+ | `scripts/run-shell-gate-durable-allow-test.mjs` | 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 |
363
+ | `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 same replay-swallowing disease as on shell gates, 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. A later section pins the split this release introduced on the deny close-out frame. Until now every denied tool call was stamped with the same sentence — the one that says *the user* does not want to proceed — including the calls denied automatically on a lane that has no approval surface at all, where nobody was ever asked. The guard drives all three shapes (a person pressed No, a rule settled it, nobody said which) through both close-out arms and the durable park leg, and pins that the third shape is byte-identical to the previous release: an attribution nobody supplied is not evidence for either answer. The rule-settled shape carries the shell's own reason on a second line when there is one and stands alone when there is not, because a blank line where a reason should be reads worse than no line at all. The attribution is read from own data properties only, so neither a polluted prototype nor a getter can make an automatic denial claim a person made it — and the getter case is pinned to never run at all. The transcript classification word is minted only on the two paths where the upstream transcript format really carries one; the three classifier words and the two abort words are left absent, with the abort words pinned against the strings this package actually normalises interruptions to, which are different strings. A final section pins the decide-operation observer: one `start` in the same tick as the first request and exactly one `end` after the last attempt has settled, across success, retried timeouts, exhausted transient failures, semantic refusal, binding mismatch and both kinds of caller abort, with nothing between retries; an observer that throws or rejects — even when logging that fault fails — never changes what is sent or returned, and the stream-level dependency reaches both durable park legs. A package-internal re-delivery of the same decision (plain approve after an older server rejects the session-scope flag, or a re-send without the attribution key) is reported as one operation with a single start and end. An operation handle only groups sends for the same session and bound call — a send for another gate through the same handle is its own operation — and closing a handle while a send is still in flight defers the end until that send settles. When a person picks "allow for this session" on a parked card and the grant is known not to have been stored — the server answers so, which newer servers do for the gates they can recognise from the parked row as needing a person each time, or the server refuses the session-wide grant with one of the refusal codes that are fixed by the row or the deployment and the package falls back to a plain approval — the parked path now says so with the same line the live path uses, and the receipt carries the server's bit for hosts that call that path directly; an answer without the bit, or any other failure — including a conflict that an internal retry can hit after an earlier send already stored the grant — is treated as unknown and says nothing, a plain approval that never asked for the grant says nothing, and a host logger that throws after a successful decision, on either card path or in the bridge's retry step, can no longer turn it into a second send or a failure. From 0.83.0 it also pins approval cards whose arguments are not the tool's real input: a live frame whose arguments were omitted over the size cap or never sent (the card holds at most a path recovered from the question), and a parked approval whose stored input is missing or replaced by the upstream size marker. On every such card an edited approval sends nothing at all — no respond, no decide, no session-grant attempt — ends as edit-refused, raises the same notice once and flags the card so hosts do not offer editing; both production entry points are driven end to end, and the parked path neither mistakes it for an already-decided gate nor reconnects. The explanatory line is pinned word for word in its four forms: a recovered path is the only thing it claims to have recovered, an unrecoverable one says nothing was recovered, neither claims a reconstructed diff, and the parked forms each say which of the two gaps it is, with near-miss shapes of the size marker still rendered as ordinary input. Real input on the stream, the frame or the record keeps edits flowing, and plain approvals and denials are untouched. Since 0.83.2 the live frame's `mandate` word reaches the card only when it is one of the six known words, read back identically by `readApprovalMandate`; it never adds the separate mandated key to the card, but a known word on its own makes `approvalIsMandated` answer true, because the engine only sends the word as the whole reason for that bit. The frame's `origin` is read from its own keys only, so a value inherited through the prototype chain never reaches the card. The already-resolved verdict a decision leg uses (`isAlreadyResolvedFailure`: the closed failure code first; codes that say the gate is still standing or failed to start never fall through to the wording check; the moved-gate codes by code; then the four-word wording fallback) is exported from the root entry as the same function the package calls, and a malformed or hostile input answers `false` without throwing, since a `true` would let a caller silently re-attach. The same section compares it, input by input, with the copy a client kept of the same combination, and requires every disagreement to come from one of the four extra checks that copy adds. It also replays the documented replacement order (the closed failure code first, answered by this function; then the copy's extra checks; then this function) and requires it to reproduce the copy's answers on every input, while moving the extra checks ahead of the closed code must disagree somewhere. A refusal that says the stored form of a parked gate cannot be continued while the gate is still pending is classified by its code and its `retriable` bit as a gate that is still standing, so failure text that happens to contain words from the wording fallback no longer makes the leg re-attach; both decision legs carry the bit on their failure outcome. The judge of whether a remember request was refused by the server specifically is a root export — it answers the matching code for the five dedicated codes and nothing otherwise, never throws, and agrees with the durable approval leg code for code. |
364
364
  | `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 |
365
365
  | `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`) |
366
366
  | `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. The floor witnesses are a list since 0.90.0 (the installed SDK plus the 15.x and 16.0.0 probe trees); every listed SDK is checked for the value-level symbols and the cost field, and a missing 16.0.0 witness is reported as a skipped section rather than as green. |
@@ -375,7 +375,7 @@ guard still cross-checks the table by name).
375
375
  | `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 |
376
376
  | `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 Every row in the `needsHuman` bucket also carries a closed-set `needsHumanReason` (`approved_then_interrupted` / `pending_unsafe` / `unstable_row` / `contradictory`) derived from the same single read that bucketed it; `resumeSafe` rows never carry it, a same-named key on the supplied row is overwritten or removed, and the bucketing itself is unchanged. The `cause` discriminant is a closed set of two words — a crash, or an orderly shutdown that cut an unfinished step off — and neither the bucket nor the reason ever reads it: every combination buckets identically under both words, a word outside the set still drops the row, and the row reaches the client with its word unchanged and no sentence attached. An exhaustive switch over `cause` that only knows the older word fails to compile, by design. When server fixtures are present, rows minted by the server's own audit store are projected for both generations: the newer one yields the shutdown rows and an untracked step's pending orphan that is no longer marked safe to re-run, the older one yields neither. The one line a person reads about those leftovers is minted here too, by a single speaker: `crashConvergedNotice(projection)` takes the projection as it is (a host that drops rows it has already disclosed passes the two buckets back with `total` recomputed) and produces one English line — how many approvals were still waiting with the recorded cause codes repeated verbatim, then either one sentence per needs-human reason in a fixed order (`NEEDS_HUMAN_REASON_ORDER`, deliberately not the union's declaration order, which is the bucketing priority) with a fallback sentence last and the "check them by hand" closing said exactly once, or the "based on what was recorded" re-run sentence, then "sema has not re-run anything" and a note for unreadable records. It never re-judges a row (bucket membership is taken from the projection as given), it says nothing for an absent, empty or unreadable projection — including one whose buckets do not add up to its `total`, so a host that filtered rows without recounting gets silence rather than self-contradicting numbers — and a row whose reason cannot be read only falls into the fallback sentence. `crashConvergedNeedsHumanSentence(reason, n, total)` gives the per-reason sentence, or `undefined` for anything outside the four words or a bad count. The order constant is pinned at compile time to be a permutation of the reason union, and the pin is proven to sit on the thing it guards: the guard edits the source in memory and requires the line of the matching pin to fail when a member is removed from either side or duplicated. When the terminal's source tree is present, the same 9432 projections also run through the terminal's own sentence table and assembly, extracted by syntax tree, and must match byte for byte. |
377
377
  | `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 |
378
- | `scripts/run-package-hygiene-test.mjs` | Everything `package.json` `files` ships — dist JS/typings and the Markdown docs — is screened line-by-line against a deny-list of strings that must never reach a public tarball (internal hostnames, codenames, person names, collaboration-process words, other repos' ledger ids and repo names; opaque ticket ids `CC-nnn` and post numbers `[nnnn]` are allowed as traceability references). Since 0.77.2 the build strips comments (`removeComments`; enforced by `run-dist-comments-test.mjs`), so what this gate screens in dist is code, string literals and type-level text. Markdown docs are enforced forward-only (CHANGELOG from 0.77.2, the integration doc from §81) because published sections are frozen. From 0.88.0 every shipped file is also scanned in full, frozen sections included, for merge-conflict markers at the start of a line (`<<<<<<< `, a bare `=======`, `>>>>>>> `, and the seven-bar base marker of three-way conflicts). From 0.88.0 a short local list of release-process phrasings (integration-branch and review-round wording) is screened as well, forward-only from the 0.88.0 sections. From 0.101.0 internal finding labels (a single capital letter, a hyphen and a number, such as the forms an internal review uses, and another repository's ledger form) are screened in the Markdown docs, forward-only from the 0.101.0 sections; the integration doc's own row prefixes are two letters and are not matched.
378
+ | `scripts/run-package-hygiene-test.mjs` | Everything `package.json` `files` ships — dist JS/typings and the Markdown docs — is screened line-by-line against a deny-list of strings that must never reach a public tarball (internal hostnames, codenames, person names, collaboration-process words, other repos' ledger ids and repo names; opaque ticket ids `CC-nnn` and post numbers `[nnnn]` are allowed as traceability references). Since 0.77.2 the build strips comments (`removeComments`; enforced by `run-dist-comments-test.mjs`), so what this gate screens in dist is code, string literals and type-level text. Markdown docs are enforced forward-only (CHANGELOG from 0.77.2, the integration doc from §81) because published sections are frozen. From 0.88.0 every shipped file is also scanned in full, frozen sections included, for merge-conflict markers at the start of a line (`<<<<<<< `, a bare `=======`, `>>>>>>> `, and the seven-bar base marker of three-way conflicts). From 0.88.0 a short local list of release-process phrasings (integration-branch and review-round wording) is screened as well, forward-only from the 0.88.0 sections. From 0.101.0 internal finding labels (a single capital letter, a hyphen and a number, such as the forms an internal review uses, and another repository's ledger form) are screened in the Markdown docs, forward-only from the 0.101.0 sections; the integration doc's own row prefixes are two letters and are not matched. From 0.102.0 post-number aliases (a hash sign followed directly by two to five digits, as opposed to the allowed `CC-nnn` and `[nnnn]` forms) and fix-batch labels (`FIX` followed by a number) are screened in the shipped JS, typings and README in full, and forward-only in the CHANGELOG and the integration doc; hex colour codes, anchors such as `file.md#129a` and HTML entities are not matched.
379
379
  | `scripts/run-terminal-coded-detail-projection-test.mjs` | The structured account of a coded failure, carried on the synthesized API error row and on the run result frame under one superset key. When a run ends in a failure whose throw site stated structured detail (for example the seat and arm of a refused model binding, or the code and permanence of an unusable rewind boundary), both the row and the result frame carry that detail under `_sema_coded_detail` (key names and structure unchanged; credential-shaped substrings in string values are replaced by redaction markers, the same net the error list goes through), taken from a single reading of the terminal so the two carriers always agree, and the interactive lane's rebuilt row keeps the key. Nothing is carried when the terminal has no such account, the account is malformed, empty or over budget, the terminal is not a failure, or the event is a bare failure event: absence never reads as having no structured reason, no placeholder word is ever written, and a permanence that was not stated is not guessed. `terminalCodedDetailOf` reads the key back from either carrier and `runTerminalCodedDetail` reads the account from a terminal reading, both returning a frozen copy and neither throwing on hostile shapes. Everything else on the row and the frame is unchanged, the key has one minting site per carrier, and it has a row in the superset ledger |
380
380
  | `scripts/run-sdk-registry-transit-test.mjs` | The package's cloud control-plane surface (`sdkRegistryTransit`), which lives behind its own `./registry` subpath entry point rather than on the root barrel, and this guard holds both halves of that decision. Upstream publishes the same surface behind a subpath of its own, because the subject differs: the engine-wire surface speaks for one engine's service credential, this one for a person's rotating token, and their refresh and error semantics were deliberately never merged. Keeping it behind a second entry point means a client that never touches the control plane neither resolves nor type-checks it. Every value re-export is therefore read from the file the subpath entry actually resolves to, and must be the **same reference** as upstream's own (the control-plane client class, the three config reads, the health probe, the feedback call, the auth-path constant, the content-address helper, the two typed error classes a host recognises with `instanceof`, and — since 0.90.0 — the five sign-in and scope calls: device-code request, polling until approved, token refresh, scope listing and scope switch); every type re-export is compared with the emitted declaration file in both directions; the gate's list and the source file's export lists are likewise compared both ways; and the root entry is checked to carry none of these names, with the root barrel's own source checked to reference the entry file nowhere — a name leaking onto the root would put the cost of this surface back on clients that never asked for it. The subpath is then verified end to end: the installed SDK must really publish its own `./registry` entry and declare every transited name inside it, and this package's own `exports` must point that subpath at exactly the files the gate just judged. Portability is two checks rather than one, done with a parser rather than a text search: the upstream subpath's emitted JavaScript, walked recursively, must contain neither a `node:` specifier (static, side-effect, dynamic, `require` and re-export forms all exercised) nor a Node **global** — because the same package's third entry point is a Node-only surface that imports nothing at all and reaches for the `Buffer` global, so a specifier check alone would pass it as isomorphic, while a byte-level search of it reports two `node:` hits that live entirely inside a documentation example. A text scanner that merely strips comments first gets both directions wrong on ordinary JavaScript — a regular-expression literal containing a slash pair swallows the rest of its line, and the word in a string reads as a reference — so both scanners run off the syntax tree and are checked against fixtures for each failure direction as well as against that real material — including a dynamic import written with a template literal, which a check that accepts only quoted strings misses entirely, and a dynamic import whose target cannot be determined statically, which is refused rather than read as no edge at all. Zero-processing is likewise enforced with a syntax-tree allowlist rather than a keyword search: every top-level statement must be a named re-export carrying that one specifier, so an import followed by an in-place edit of the upstream prototype is refused with a file and line — that shape leaves the name lists untouched and even keeps the same-reference check green, since both sides are then the one object that was damaged. The last section measures, rather than merely notes, one declaration-level gap: two of the upstream subpath's declaration files reference a package the SDK lists only among its own dev dependencies. Moving such a check to a scratch directory is not isolation, because package resolution walks the ancestor directories, so the gate builds a sandbox served by a restricted compiler host and proves the isolation both ways — a decoy copy of the missing package placed one level above the sandbox must silence the errors for an unrestricted host and must not silence them for the restricted one. Then it installs this package into that same sandbox as a real consumer would, and pins the two readings that justify the entry-point split: a consumer that imports only from the root sees no unresolved-module errors at all, while a consumer that imports the subpath sees exactly the two, reported honestly rather than swallowed by this layer. The day upstream ships those declarations, that section turns red; the registered count is then updated and the section stays as a guard against a regression. The separation itself rests on the root closure being computed correctly, so the portability guard that computes it was extended in the same change: a template-literal dynamic import is followed like any other edge, and an edge whose target cannot be resolved statically is refused on every one of the four graphs — without that, a single line in a third file already reachable from the root would put this surface back into the root runtime while every guard stayed green. Since 0.84.0 the upstream subpath ships those declarations itself: the measured count is now zero, the section stays as a guard (a new missing declaration package turns it red again), the isolation proof uses a synthetic probe that imports only the decoy, and the gate also checks that no upstream subpath declaration references the missing package any more. Since 0.90.0 the five sign-in, token-rotation and scope calls are re-exported as well, with the same untouched re-export form, and the declaration file of the re-export is compared with the gate's value list in both directions. The problem that once kept those five out lived in the type surface, and reference equality holds just as well for a function whose result type has silently collapsed to `any`, so the gate reads each of three upstream generations separately (the installed one plus two probe trees named by environment variables): the five names must be functions in the generation's emitted JavaScript and value-level named exports in its declaration entry; the closure of relative edges (imports, re-exports, import types and path references) from the declaration files that declare them, including a hop out of the registry directory, must contain no bare module specifier; the re-export file, loaded in a scratch directory where the package resolves to that generation, must expose the same function objects as upstream and exactly the gate's list of names, with the neighbours upstream really exports beside them (logout, the grant-type constant, the in-memory token store, the admin class and the like) absent; and a typed probe in the clean-install sandbox must see real return and parameter types, using suppression comments that stay valid only if there is a genuine error to suppress. A missing name in any generation fails the gate rather than skipping; only an unset probe tree is reported as a skipped section, and a set one that does not resolve stops the gate as broken material. The three generations must also be three distinct versions, each at least the major its slot is named for: one SDK filled into two slots, or an older one into a newer slot, stops the gate as broken material instead of counting as extra coverage. Synthetic fixtures (a generation missing a name, a bare specifier in the declaring file and one a hop away, a path reference that points at nothing or pulls in a file with a bare specifier, a wrapper function, one name too many, an export entry pointing at nothing, the generation registry itself) prove the reader answers each case. |
381
381
  | `scripts/run-mods-kernel-transit-test.mjs` | The package's `./mods` subpath entry point, which re-exports the mods kernel package unchanged and keeps it out of the root entry entirely: the kernel is an optional peer dependency, so a host that never imports the subpath installs, type-checks and runs without it, while a host that does import it without the kernel installed fails loudly at load time. The 67 re-exported names (26 values and 41 types, a fixed commitment for the pinned kernel generation; a name the kernel adds later is not forwarded until reviewed) are read from the file the subpath entry actually resolves to; every object and function value must be the **same reference** as the kernel's own, and because reference equality cannot see a locally re-minted number or string, the entry file may contain nothing but named `export … from` statements (a syntax-tree allow-list) and the guard also loads the built entry into a sandbox beside a synthetic kernel whose every primitive has been changed, requiring each re-exported value to follow it. The root entry's whole emitted graph (script and declaration) must carry no reference to the kernel or to the subpath's modules; the subpath's own graph is exactly two files with the kernel as its only outward edge and is disjoint from the root graph; the kernel's emitted code and the entry are free of Node built-ins and Node globals and bundle for a browser. Install behaviour is exercised in a sandbox with a restricted compiler host and real Node loads: root-only consumers type-check with no kernel present, subpath consumers without the kernel get errors naming exactly the kernel, and with the kernel installed the subpath re-exports the kernel's own values. The kernel is pinned exactly as a development dependency (no caret range) and each pinned generation has a column of expected readings: the contract number and a SHA-256 of the canonical event table (drift alarms), three behaviour canaries run through the entry (the chain rule, how a pinned key is judged, which events a surface-side bus accepts), and a same-version pairing check against the engine package's embedded copy (byte-identical sources). The entry's one own export, an identity reader returning the kernel's contract number and the event-table digest, is checked against an independent recomputation over a seeded pool and throws on malformed input. |
@@ -457,8 +457,8 @@ guard still cross-checks the table by name).
457
457
  | `scripts/run-memory-saved-projection-test.mjs` | Engine memory writes (a successful `Remember` tool call) moved off the transcript onto the additive `memory_saved` chrome event, driven through the real pipeline: zero transcript rows for the write (the transcript is byte-identical to the same frames with the write reported as not successful), exactly one event whose `notes` carry the note text verbatim (notes, not file paths) and whose key set is exactly kind / laneProof / id / notes; no event for a missing, empty or non-string note, a non-`true` `ok`, a tool error, a missing result or another tool name; two writes give two events in order with distinct ids; a sub-agent write rides the sub-agent lane and an empty parent id emits nothing rather than falling back to the main lane; the `id` is derived from the write's wire key (the tool-end event id, else the tool-start event id, else the call id; empty ids count as absent), so projecting the same wire events twice gives the same id, and it never collides with the tool result row of the same or another call; the event sits right after the tool result row; the arm is registered as required. |
458
458
  | `scripts/run-result-frame-projection-test.mjs` | Result frames and the synthesized terminal rows. The CC key `terminal_reason` is minted on result frames only where it follows from what the engine reported: `completed` on success, `max_turns`, `budget_exhausted` and `structured_output_retry_exhausted` for the three matching engine codes, on both the done-frame path and the failed-event path. Every other outcome leaves the key absent as an own property rather than present with an undefined value: wall-clock and token-budget limits, the classifier denial limit, cancellation, unknown codes, blocked, paused, unreadable or missing terminal records, and the busy-session refusal. The public reader `terminalReasonForResult` shares the minting predicate and is checked to agree with the minted key on every frame the gate produces. Both the minted key and the reader derive the word from the frame's CC subtype (success with `is_error` strictly false, and the three limit subtypes), not from the error code, so a replayed row whose status is paused, blocked or unrecognised never carries a word that contradicts its subtype. The four words are checked against the mirrored CC union, and the minting file is checked to hold no hand-copied code literals. The renamed superset keys (`_sema_error_code`, `_sema_salvaged_result`, `_sema_model_degraded`, `_sema_selected_model`, and the row flag `_sema_api_error_message`) are driven through the real stream pipeline. Each must be present under its new name, the old name must be absent, and every frame the gate saw is swept for old names. The selected model appears on error envelopes whenever the terminal record carries it, and never on a failed event, which has no record. It stays separate from the provider-reported model name. The two in-package readers still work: the interactive result arm reads the salvaged text under its new name, and the print init gate treats the renamed flag as the run having ended. Since 0.86.0 the result arm no longer reads `result` on an error envelope, meaning a frame whose subtype is a string starting with `error_`, including error subtypes it does not recognise yet; a compile-time check pins that every CC error subtype carries that prefix. Such an envelope that carries the salvaged text only under the old `result` key yields no answer row, no appended suffix and no divergence event; when both names are present, or the new one is present but not a string, the old one is never read. Everything else reads `result` exactly as before, in all three producing cases (whole answer, missing suffix, divergence): success frames, a success frame flagged `is_error: true` (checked equal to the same frame with `is_error: false`), and frames whose subtype is a string without that prefix (empty, unrecognised, `Success`, a bare `error`), absent, or not a string (each checked equal to the same frame with subtype `success`). From 0.94.0 four root readers return the result frame's string superset keys (`resultErrorCodeOf`, `resultSelectedModelOf`, `resultSalvagedResultOf`, `resultParkFailedReasonOf`): they read only the frame's own data properties, never fall back to the retired names, run no getter and never throw. A synthesized terminal row that carries `_sema_api_error_message: true` has its text credential-washed on both projection paths (minted by the stream projection, or rebuilt by `adapt()` from an older transcript row), so the flag itself tells a client the text is already washed; model rows are left byte-for-byte alone. |
459
459
  | `scripts/run-layering-shadow-export-test.mjs` | Same-name shadows across the first-party clients that consume this package (terminal, desktop, web and the admin console). Each client's product sources are read at the local clone's `origin/main` (or its HEAD when there is no such ref), without fetching, and parsed with the TypeScript parser; every top-level runtime export the client declares itself is compared with this package's public runtime exports. The guard prints which ref, commit and commit date it read for each client, and warns (without failing) when that commit is more than seven days old, because the result then only describes that older snapshot. A client-side declaration carrying the name of a package export means a piece of shared logic now lives in two places and can drift apart. It fails the guard unless it is listed in `scripts/layering-shadow-exemptions.json`, and a listed row must carry one due-by version no more than three minor lines ahead (it fails once the package reaches it; that single field is the only one the guard reads for expiry, and the older retire-by and review-by fields that still sit on the rows for one more version are only checked to agree with it). It also fails once the client has removed the shadow and the row still stands. Every row also names what kind of duplicate it is, and carries the evidence that kind requires. A client copy that is this package's own function object behind a type assertion, or a thin wrapper that only passes the client's own dependencies into this package's function, must be proven so on every run by reading the client's syntax tree; such rows are due for review within two minor versions, and the guard fails as soon as the proof no longer holds. A wrapper whose extra logic the client has confirmed to be host-specific carries that confirmation (the client's own wording and where it was stated) together with a pinned review record that is due within two minor versions. A wrapper that adds its own decisions, or a same-name function that does something else, carries a review record pinned to a hash of the normalized client declaration (comments and formatting do not count); once the client's copy changes, the guard fails until it is reviewed again. A second implementation of the same logic carries a semantic diff: the client's copy is taken from the commit being read, the named declarations and the client modules they import are extracted with the TypeScript parser, transpiled and run in memory against this package's build on the same inputs, and any disagreement outside the classes registered on that row fails the guard (each class is a fixed predicate over both answers). A separate register lists same-meaning copies that carry a different name (a name-based comparison cannot see those): each row names the client declaration and the package export it duplicates, and fails when it is malformed, past its due-by version, or when the client no longer declares that name; the register only records and checks liveness and expiry, it does not scan for such copies. A row can follow a client-side rename to a new name, and can be registered ahead of a client change for a limited number of versions before the client code exists. Re-exports of this package's own exports are the intended form and never count. A client tree that is not present is reported as a skipped section, not as a pass. The ruler proves itself on an in-memory fake client (planted shadows must be caught, legal forms must not), on a throwaway repository (a missing `origin/main` falls back to HEAD, a broken one is a fault rather than a silent fallback), and refuses to report zero on a client whose scan surface is empty. |
460
- | `scripts/run-display-untrusted-projection-test.mjs` | The single display-safety outlet (`displayUntrusted`) and the credential wash on the end-of-run rows this package mints. The outlet composes two credential nets (URL structure: userinfo, every query value, the fragment, path parameters and path segments that start with a known secret prefix; key/value words such as `Authorization: Bearer ...`, `Authorization: token ...` or `api_key=...`, plus well-known secret literals that appear without a label, such as `sk-...`, `ghp_...`, `AKIA...`, JWTs and the body of a PEM private key) with three character nets (control characters, bidirectional and format characters, whitespace folding). The credential nets match on a view of the text with ANSI sequences, format characters, control characters and the outlet's own escape tokens stripped, and map the result back onto the original, so colouring or an invisible character wedged between a label, its separator and its value cannot hide the value, and no stray marker is left behind. Whitespace of any length around the separator is accepted. Hosts, ports, paths, query key names and surrounding prose stay byte-for-byte, clean text comes back unchanged, the result is idempotent (also with a length cap), a length cap never splits an escape token or a surrogate pair, an invalid cap means no cap, and every net can be switched off on its own. A few narrow shapes are left alone because they name something rather than carry a value (a plain English word after `bearer` or `basic`, a back-quoted credential variable name, a plain integer after `tokens:`, a list of key names after `keys:`), each with a counter-example that is still washed. Regional flag emoji built from tag characters are kept whole. The existing single-line helpers (`escapeDisplayControlChars`, `collapseLabel`, `capForDisplay`, peer sender names and the hook failure banner) run on the same engine and, since 0.96.0, read the same character table as the outlet: each is checked equal to the outlet with both credential nets off (escape without folding, fold then escape, space mark then fold, and the same length cap) over a seeded pool of more than 600 strings; every character class the old sets left alone (zero-width characters, the word joiner, invisible operators, U+FEFF, the soft hyphen, tag characters and other format characters, plus C1 controls, bidirectional controls and lone surrogates in sender names) is marked class by class, while joiners inside emoji sequences and joining or Brahmic scripts and RGI regional flags stay as written; no character the old sets marked is ever left raw, input without the newly marked characters stays byte-identical to the previous output over every BMP code unit plus random strings, all of them are idempotent, and neither `capForDisplay` nor the hook banner's 512-character cap splits a `\u{…}` escape token or a surrogate pair. The approval decision-note echo, the subagent resume receipt (and its failure debug line) and the startup list of plugin hooks that will not run now also drop bidirectional and format characters (and, for the receipt, C1 controls); a note that is empty after cleaning is treated as absent. The synthetic end-of-run rows (`API Error:`, `Run stopped:`, `Model output error:`, `Outcome unknown:`) and the result frame's `errors[]` pass both credential nets before they leave the package, on the print lane and on the interactive lane (which also keeps the row-class flag); this covers a blocked reason whoever wrote it, while assistant text rows, a successful `result` and salvaged output are never touched, and a non-string `errors[]` entry is passed through unchanged. The known-secret-prefix check is a local copy of the configuration package's detector and is compared with the installed one entry by entry. Since 0.83.5 the outlet has two opt-in switches and a position read-out. `escapeBackslashes` (escape form only) writes every literal backslash as a pair, so each output decodes back to exactly one input (a real invisible character and its literal six-character spelling no longer look alike); a reference decoder round-trips thousands of random strings, the output is byte-identical to the default when the input has no backslash, a length cap is measured on the paired output and keeps the longest fitting prefix, credentials are washed exactly as in the default, and the switch is not idempotent by design (use it only at the final render). Zero-width joiners and non-joiners are kept only inside emoji sequences drawn as emoji (so not between symbols such as © or ™ that display as text) and between letters of scripts where they change the shaping (joining scripts such as Arabic, and the Brahmic family), each listed script checked both ways; the one other place a joiner is kept is right after a virama at the end of a word, the older spelling still found in Malayalam and Bengali text. Next to Latin, Cyrillic, CJK and other letters, next to modifier letters shared across scripts, at the start of a word, at the end of a word without a virama before it, or on their own they are now marked. `blanks` marks characters that look like a space but are not an ASCII space (no-break and other width spaces, the ideographic space, Hangul fillers, the blank Braille pattern) before whitespace folding, for names that must never look alike. `displayUntrustedMarks` returns the same text plus the position of every character mark; its text is compared with the outlet over thousands of inputs. With both credential nets off, the character face stays byte-identical to the previous release for input without joiners. The credential nets read escape sequences the way a terminal would when one is cut short: an unfinished colouring or character-set sequence interrupted by another one is dropped as a whole, a sequence never takes the `@` of an address as its final character, and a final character that starts a well-known secret literal (`sk-`, `ghp_`, `AKIA`, a JWT) is also read as the start of that literal; escape tokens this outlet writes are read as one unit, while look-alike text it never writes (an upper-case `\U`, or a code point it never marks) is read as plain text. A URL is cut before a run of non-ASCII blanks followed by a credential label or scheme word, Hangul fillers and the blank Braille pattern count as spaces around a label's separator, and a value that itself starts with a quoted label (`token= "password":"..."`) is left to that inner label. With `blanks` on, a blank written as an escape token right after a label is read as a blank when the value is judged, so `password:` followed by a no-break space and `missing` stays unmasked and an empty value gets no marker; the credential nets also read the text the way it looks after default whitespace folding and combine what each reading masks, so whatever the default form masks stays masked with `blanks` on (checked over a seeded corpus for the escape form, with paired backslashes and without folding; the exceptions are text that itself contains a literal six-character blank escape, which cannot be told apart from one the outlet wrote, and the dot and space marks, which cannot tell a marked blank from a real dot or space). A lone surrogate wedged between a label, its separator and its value no longer hides the value: the credential nets treat it exactly like a format character, in the machine-readable wash, in a single pass of the display outlet, and on the end-of-run rows and the result frame's `errors[]` on both lanes, while lone surrogates anywhere else are left byte-for-byte. Since 0.84.1 an address-shaped value (`scheme://…`) that sits right after an invisible single character — a format character, a lone surrogate, a non-whitespace control character, or the outlet's own escape token for one — is no longer let through as an address: the scheme stays and the host and path are replaced, with the query and fragment masked as before; `user:<password>@` followed only by stripped units and then a boundary, a port or another `@` is masked as userinfo. A value after real whitespace or after a colour sequence is still treated as an address, and clean text stays unchanged; forms that the previous release masked are checked not to leak on the same outlets. A `user:<password>@` candidate that is the value of a credential label or scheme word — including a label or scheme word split by stripped characters, and a quoted value that goes on past whitespace — is left to the label pass, so the whole value is masked as in the previous release; both reported shapes and frozen samples from the targeted pools are checked on every outlet. A scheme-less `user:password@host` whose password holds a double quote, backtick or angle bracket is still masked when a credential label or scheme word sits right before the user name, or when the user name is a plain word that is not itself a label, the host has a real shape and the address is not wrapped by that quote; quoted JSON values, quoted or bracketed e-mail addresses and quoted arrays stay byte-identical. When a label inside a quoted value ends right at that value's closing quote, the quote and separator stay outside the mask, and an address that begins the rest of a quoted value counts as an address only up to the next blank, so washing the output again never changes it. The credential stage now repeats its single scan on its own output until nothing changes (the cap counts rewrites, at most eight; clean text takes exactly one pass), so masking the output again never changes it for every input that settles within the cap — the random, exhaustive and grammar pools and the two known step-by-step constructions settle in at most three passes counting the confirming one, and the searched constructions kept in the pool in at most six; a deliberately constructed input can need more rewrites as it grows, and at the cap the last rewrite is returned, which a further pass can only mask more of. The checks cover the pass cap and how reaching it is reported, single-pass clean text, a grammar sample of the busiest shapes, constructions that once advanced one step per pass, the mark positions on the final output and how positions compose across rewrites. A username that is itself a credential label with a quote or angle bracket in the password is masked as userinfo (also with only the URL net on) unless the label's own value really reaches the `@`; a URL parameter such as `;token=...` or `,token=...` stays inside the URL piece; a private-key header the outlet cannot recognise does not hide the labels inside its block; the tail glued after the host of a label's address value is masked to the value's end; and a value this package's own transcript wash split at a carriage return is masked when the display joins it back. A bare `Token <value>` / `token <value>` (no label, no separator) is now washed like `Bearer`, with a wider allow-list of ordinary English words after `token` so prose such as `token count: 1234` or `token expired` stays readable; a `/token` URL path tail followed by a status code is not a scheme word and stays as is, and neither is `token` right after `unexpected` in a parser error such as `SyntaxError: Unexpected token '<'`, which stays readable. The `token` scheme word only adds masking of its own value: label values and `bearer` / `basic` values are read exactly as in the previous release, so a value that starts with another character, or a `bearer` value, followed by `token`, a non-ASCII space and an address is still masked whole. When a credential label or scheme word is followed by a colon and a YAML block scalar header (`\|` or `>`, optionally with `-`, `+` or digits, invisible characters allowed between them, and nothing else on that line but blanks, invisible characters or a comment, ending in a line break), the whole indented block becomes a single marker: the block runs until the first line with visible content that is not indented deeper than the label's line, blank lines included, and the line break before that line and the line itself stay as they were on outlets that keep line breaks (outlets that draw line breaks as escapes or dots merge the next word into the marker, as they already do after any value). Raw and JSON-escaped line breaks are both read; blanks inside escape sequences or control strings never count as indentation, and a block line holding only a control string counts as content and is masked whole. Forms where the marker is followed by more text on the same line, `=`-separated forms such as `key = \|` or the `=>` arrow, and a header at the very end of the text read exactly as in the previous release. From 0.94.0 the private-key header has one definition, generated from the settings contract: the whole-string check finds it anywhere in the text (as the contract's own write-side check does from its 10.0.0 release), and the key-block cutter builds its header line from the same definition, so a lowercase header or one without a space after BEGIN has its key body masked like the canonical form, while public keys and certificates are left as they were. Since 0.94.0 the credential words also read through YAML node properties (type tags such as `!!str` and anchors such as `&name`) between the colon and the value: the properties and the value after them get one marker each, block scalars and block-sequence items included, and a property with no value after it on the same line reads exactly as before. A list after a credential label (a block sequence, a flow sequence including JSON arrays, or a same-line comma list after a plural label) is judged item by item with the same exemptions as a single value: a list of plain key names after a bare `keys`, or of integers after a bare `tokens`, is left alone, and any value-like item masks every non-neutral item. A dash right after the colon on the same line is a list bullet, not a value. The retry overlay's error text (`error.formatted` on the four error arms) passes through the same outlet, and the label vocabulary is pinned as it stands (members masked, known out-of-family keys such as `passphrase` left as written). From 0.95.0 the credential words also read a value wrapped in round or square brackets after a scheme word or label (placeholder-shaped contents such as `(API_KEY)` or `(your-token)` and bracketed prose stay as written), a JSON-escaped tab between a scheme word or label and its value, a same-line associative-array arrow with a quoted label and a quoted value, and a `token` scheme word with its value after a credential label as one value; a word with no label at all is masked when it is shaped like a random token (length, three character classes, entropy and run structure), while hex hashes, UUID lists, base64 image headers and data URIs, integrity strings, provider object ids and ordinary identifiers stay as written; a known secret prefix glued after an underscore or hyphen inside an identifier or a URL path segment is masked from the prefix on; and a masked value no longer swallows a closing backquote at its end, or a closing round or square bracket at the end of a URL that has no matching opener, while a backquote inside a value never cuts it short; an address after `Bearer` or `Basic` that is itself the value of a credential label (including `Authorization:`) is masked whole, keeping the scheme word, while an address with no label stays as written. Credential masking also covers three shapes that used to print as written: a name glued onto a credential word (PGPASSWORD=, CUSTOMSECRET=, dbPassword:, secretAccessKey=; singular only, so token-count names such as maxTokens or inputTokens and English words such as monkey stay visible), Bearer or Basic glued to a preceding word character (_Bearer, xBearer), and a known secret prefix followed by one unbroken run of six or more letters and digits (sk-abcdefghijk). The new readings only add masks: a value that was already masked whole stays masked whole when it contains one of these shapes. The configured-address display and the upload check read the same label rule, so a glued name used as a query key counts as a credential in both. A frozen copy of a pattern net that a downstream host used to run locally is replayed against the output as a no-regression check. A length cap never splits a redaction marker: the cut backs off to before it on every capped path (plain, position-tracking, paired backslashes, and the shared cap of the field-escaping helper); strings that are not complete markers are cut as before. Since 0.98.0 a value glued to a semicolon, a value wrapped in braces or angle brackets (template placeholders stay readable), a bare `Basic` / `Bearer` assigned to an environment-style credential name, and an ordinary word right after `Bearer` / `Basic` in a credential header are masked on every outlet, under a 65 KB linear-time bound; the diagnostic-word predicate answers from the same word list the outlet uses. |
461
- | `scripts/run-display-untrusted-baseline-diff-test.mjs` | The credential side of the display outlet (`displayUntrusted` and `washCredentials`), diffed byte for byte against the previous published release: five input pools (seeded random strings, exhaustive shapes around labels, quoted values, userinfo, schemes and invisible units, and URL parameters separated by `;` or `,`) run through fifteen outlets. A secret the previous release masked must stay masked wherever it sits in a credential position; the outlet with only the URL net enabled is judged within that net's own reach. Clean lines from the package's own documents pass through unchanged, and every outlet is idempotent (masking the output again changes nothing). The previous release tarball is supplied through `SEMA_DU_BASELINE_TGZ`; without it the diff sections report as skipped and the run counts as partial, not green. The minimal-shape pool also carries scheme-less userinfo whose password holds a quote or angle bracket after a credential label, and the shapes that once washed differently on a second pass. A full-product grammar pool of 64,512 strings joins the idempotence check, and every pool is also checked for the credential stage reaching its pass cap (it must never do so). The pools now include the bare `token` scheme-word forms and a full `token` grammar product, so the new wash is held to the same idempotence and no-regression bars against the previous release. They also include label and `bearer` / `basic` values that run into a `token` scheme word, a non-ASCII space and an address, with or without a leading character, and quoted values where `token` and an address follow `bearer`. A YAML block-scalar pool (block headers with every chomping and indentation indicator, comments, raw and JSON-escaped line breaks, non-ASCII and tab indentation, invisible characters at line starts, credentials, control strings and bidi characters inside the block, and seven kinds of line after the block) joins all checks: no secret inside a block may show on any outlet that runs the word net, the previous release must show some on the same pool, and the non-block forms in that pool must stay byte for byte as in the previous release. A sixth pool covers lists and YAML node properties after a credential label: block, flow and comma lists plus tag and anchor prefixes, alongside look-alike non-list forms. The secrets in the list pool must stay masked on every outlet, the look-alike forms must read byte for byte as in the previous release, and the three forms whose reading changed on purpose (a lone dash after the colon, a value after a dash, a block sequence of plain key names) are pinned as registered divergences. A seventh pool covers the 0.95.0 credential-net shapes (bracketed values after scheme words and labels, a JSON-escaped tab before the value, unlabelled random-looking tokens built inside the shape rule, associative-array arrows, a label followed by a `token` scheme word and an address, and known prefixes glued after an underscore or hyphen) alongside normal long strings and placeholders: the secrets must stay masked on every outlet while the previous release shows some, the normal strings must read byte for byte as in the previous release, eleven readings that changed on purpose are pinned as registered divergences, and the share of purely random tokens the shape rule catches is read at four lengths with a floor of 0.9. A nested-shape pool pins that the three new credential readings only add masks: a value that the previous release masked whole must stay masked whole when it contains one of the new shapes. At the capped exit, differences against the previous release are accepted only when the old output ended in a truncated redaction marker and the new output is the old one without that fragment. Since 0.101.0 a pool of percent-encoded YAML block scalars and block sequences (166 plain shapes in one to three layers of encoding) is pinned: no secret shows on any outlet while the previous release shows some, benign encoded multi-line text reads byte for byte as in the previous release, six readings that changed on purpose are registered as divergences, and washing twice equals washing once. |
460
+ | `scripts/run-display-untrusted-projection-test.mjs` | The single display-safety outlet (`displayUntrusted`) and the credential wash on the end-of-run rows this package mints. The outlet composes two credential nets (URL structure: userinfo, every query value, the fragment, path parameters and path segments that start with a known secret prefix; key/value words such as `Authorization: Bearer ...`, `Authorization: token ...` or `api_key=...`, plus well-known secret literals that appear without a label, such as `sk-...`, `ghp_...`, `AKIA...`, JWTs and the body of a PEM private key) with three character nets (control characters, bidirectional and format characters, whitespace folding). The credential nets match on a view of the text with ANSI sequences, format characters, control characters and the outlet's own escape tokens stripped, and map the result back onto the original, so colouring or an invisible character wedged between a label, its separator and its value cannot hide the value, and no stray marker is left behind. Whitespace of any length around the separator is accepted. Hosts, ports, paths, query key names and surrounding prose stay byte-for-byte, clean text comes back unchanged, the result is idempotent (also with a length cap), a length cap never splits an escape token or a surrogate pair, an invalid cap means no cap, and every net can be switched off on its own. A few narrow shapes are left alone because they name something rather than carry a value (a plain English word after `bearer` or `basic`, a back-quoted credential variable name, a plain integer after `tokens:`, a list of key names after `keys:`), each with a counter-example that is still washed. Regional flag emoji built from tag characters are kept whole. The existing single-line helpers (`escapeDisplayControlChars`, `collapseLabel`, `capForDisplay`, peer sender names and the hook failure banner) run on the same engine and, since 0.96.0, read the same character table as the outlet: each is checked equal to the outlet with both credential nets off (escape without folding, fold then escape, space mark then fold, and the same length cap) over a seeded pool of more than 600 strings; every character class the old sets left alone (zero-width characters, the word joiner, invisible operators, U+FEFF, the soft hyphen, tag characters and other format characters, plus C1 controls, bidirectional controls and lone surrogates in sender names) is marked class by class, while joiners inside emoji sequences and joining or Brahmic scripts and RGI regional flags stay as written; no character the old sets marked is ever left raw, input without the newly marked characters stays byte-identical to the previous output over every BMP code unit plus random strings, all of them are idempotent, and neither `capForDisplay` nor the hook banner's 512-character cap splits a `\u{…}` escape token or a surrogate pair. The approval decision-note echo, the subagent resume receipt (and its failure debug line) and the startup list of plugin hooks that will not run now also drop bidirectional and format characters (and, for the receipt, C1 controls); a note that is empty after cleaning is treated as absent. The synthetic end-of-run rows (`API Error:`, `Run stopped:`, `Model output error:`, `Outcome unknown:`) and the result frame's `errors[]` pass both credential nets before they leave the package, on the print lane and on the interactive lane (which also keeps the row-class flag); this covers a blocked reason whoever wrote it, while assistant text rows, a successful `result` and salvaged output are never touched, and a non-string `errors[]` entry is passed through unchanged. The known-secret-prefix check is a local copy of the configuration package's detector and is compared with the installed one entry by entry. Since 0.83.5 the outlet has two opt-in switches and a position read-out. `escapeBackslashes` (escape form only) writes every literal backslash as a pair, so each output decodes back to exactly one input (a real invisible character and its literal six-character spelling no longer look alike); a reference decoder round-trips thousands of random strings, the output is byte-identical to the default when the input has no backslash, a length cap is measured on the paired output and keeps the longest fitting prefix, credentials are washed exactly as in the default, and the switch is not idempotent by design (use it only at the final render). Zero-width joiners and non-joiners are kept only inside emoji sequences drawn as emoji (so not between symbols such as © or ™ that display as text) and between letters of scripts where they change the shaping (joining scripts such as Arabic, and the Brahmic family), each listed script checked both ways; the one other place a joiner is kept is right after a virama at the end of a word, the older spelling still found in Malayalam and Bengali text. Next to Latin, Cyrillic, CJK and other letters, next to modifier letters shared across scripts, at the start of a word, at the end of a word without a virama before it, or on their own they are now marked. `blanks` marks characters that look like a space but are not an ASCII space (no-break and other width spaces, the ideographic space, Hangul fillers, the blank Braille pattern) before whitespace folding, for names that must never look alike. `displayUntrustedMarks` returns the same text plus the position of every character mark; its text is compared with the outlet over thousands of inputs. With both credential nets off, the character face stays byte-identical to the previous release for input without joiners. The credential nets read escape sequences the way a terminal would when one is cut short: an unfinished colouring or character-set sequence interrupted by another one is dropped as a whole, a sequence never takes the `@` of an address as its final character, and a final character that starts a well-known secret literal (`sk-`, `ghp_`, `AKIA`, a JWT) is also read as the start of that literal; escape tokens this outlet writes are read as one unit, while look-alike text it never writes (an upper-case `\U`, or a code point it never marks) is read as plain text. A URL is cut before a run of non-ASCII blanks followed by a credential label or scheme word, Hangul fillers and the blank Braille pattern count as spaces around a label's separator, and a value that itself starts with a quoted label (`token= "password":"..."`) is left to that inner label. With `blanks` on, a blank written as an escape token right after a label is read as a blank when the value is judged, so `password:` followed by a no-break space and `missing` stays unmasked and an empty value gets no marker; the credential nets also read the text the way it looks after default whitespace folding and combine what each reading masks, so whatever the default form masks stays masked with `blanks` on (checked over a seeded corpus for the escape form, with paired backslashes and without folding; the exceptions are text that itself contains a literal six-character blank escape, which cannot be told apart from one the outlet wrote, and the dot and space marks, which cannot tell a marked blank from a real dot or space). A lone surrogate wedged between a label, its separator and its value no longer hides the value: the credential nets treat it exactly like a format character, in the machine-readable wash, in a single pass of the display outlet, and on the end-of-run rows and the result frame's `errors[]` on both lanes, while lone surrogates anywhere else are left byte-for-byte. Since 0.84.1 an address-shaped value (`scheme://…`) that sits right after an invisible single character — a format character, a lone surrogate, a non-whitespace control character, or the outlet's own escape token for one — is no longer let through as an address: the scheme stays and the host and path are replaced, with the query and fragment masked as before; `user:<password>@` followed only by stripped units and then a boundary, a port or another `@` is masked as userinfo. A value after real whitespace or after a colour sequence is still treated as an address, and clean text stays unchanged; forms that the previous release masked are checked not to leak on the same outlets. A `user:<password>@` candidate that is the value of a credential label or scheme word — including a label or scheme word split by stripped characters, and a quoted value that goes on past whitespace — is left to the label pass, so the whole value is masked as in the previous release; both reported shapes and frozen samples from the targeted pools are checked on every outlet. A scheme-less `user:password@host` whose password holds a double quote, backtick or angle bracket is still masked when a credential label or scheme word sits right before the user name, or when the user name is a plain word that is not itself a label, the host has a real shape and the address is not wrapped by that quote; quoted JSON values, quoted or bracketed e-mail addresses and quoted arrays stay byte-identical. When a label inside a quoted value ends right at that value's closing quote, the quote and separator stay outside the mask, and an address that begins the rest of a quoted value counts as an address only up to the next blank, so washing the output again never changes it. The credential stage now repeats its single scan on its own output until nothing changes (the cap counts rewrites, at most eight; clean text takes exactly one pass), so masking the output again never changes it for every input that settles within the cap — the random, exhaustive and grammar pools and the two known step-by-step constructions settle in at most three passes counting the confirming one, and the searched constructions kept in the pool in at most six; a deliberately constructed input can need more rewrites as it grows, and at the cap the last rewrite is returned, which a further pass can only mask more of. The checks cover the pass cap and how reaching it is reported, single-pass clean text, a grammar sample of the busiest shapes, constructions that once advanced one step per pass, the mark positions on the final output and how positions compose across rewrites. A username that is itself a credential label with a quote or angle bracket in the password is masked as userinfo (also with only the URL net on) unless the label's own value really reaches the `@`; a URL parameter such as `;token=...` or `,token=...` stays inside the URL piece; a private-key header the outlet cannot recognise does not hide the labels inside its block; the tail glued after the host of a label's address value is masked to the value's end; and a value this package's own transcript wash split at a carriage return is masked when the display joins it back. A bare `Token <value>` / `token <value>` (no label, no separator) is now washed like `Bearer`, with a wider allow-list of ordinary English words after `token` so prose such as `token count: 1234` or `token expired` stays readable; a `/token` URL path tail followed by a status code is not a scheme word and stays as is, and neither is `token` right after `unexpected` in a parser error such as `SyntaxError: Unexpected token '<'`, which stays readable. The `token` scheme word only adds masking of its own value: label values and `bearer` / `basic` values are read exactly as in the previous release, so a value that starts with another character, or a `bearer` value, followed by `token`, a non-ASCII space and an address is still masked whole. When a credential label or scheme word is followed by a colon and a YAML block scalar header (`\|` or `>`, optionally with `-`, `+` or digits, invisible characters allowed between them, and nothing else on that line but blanks, invisible characters or a comment, ending in a line break), the whole indented block becomes a single marker: the block runs until the first line with visible content that is not indented deeper than the label's line, blank lines included, and the line break before that line and the line itself stay as they were on outlets that keep line breaks (outlets that draw line breaks as escapes or dots merge the next word into the marker, as they already do after any value). Raw and JSON-escaped line breaks are both read; blanks inside escape sequences or control strings never count as indentation, and a block line holding only a control string counts as content and is masked whole. Forms where the marker is followed by more text on the same line, `=`-separated forms such as `key = \|` or the `=>` arrow, and a header at the very end of the text read exactly as in the previous release. From 0.94.0 the private-key header has one definition, generated from the settings contract: the whole-string check finds it anywhere in the text (as the contract's own write-side check does from its 10.0.0 release), and the key-block cutter builds its header line from the same definition, so a lowercase header or one without a space after BEGIN has its key body masked like the canonical form, while public keys and certificates are left as they were. Since 0.94.0 the credential words also read through YAML node properties (type tags such as `!!str` and anchors such as `&name`) between the colon and the value: the properties and the value after them get one marker each, block scalars and block-sequence items included, and a property with no value after it on the same line reads exactly as before. A list after a credential label (a block sequence, a flow sequence including JSON arrays, or a same-line comma list after a plural label) is judged item by item with the same exemptions as a single value: a list of plain key names after a bare `keys`, or of integers after a bare `tokens`, is left alone, and any value-like item masks every non-neutral item. A dash right after the colon on the same line is a list bullet, not a value. The retry overlay's error text (`error.formatted` on the four error arms) passes through the same outlet, and the label vocabulary is pinned as it stands (members masked, known out-of-family keys such as `passphrase` left as written). From 0.95.0 the credential words also read a value wrapped in round or square brackets after a scheme word or label (placeholder-shaped contents such as `(API_KEY)` or `(your-token)` and bracketed prose stay as written), a JSON-escaped tab between a scheme word or label and its value, a same-line associative-array arrow with a quoted label and a quoted value, and a `token` scheme word with its value after a credential label as one value; a word with no label at all is masked when it is shaped like a random token (length, three character classes, entropy and run structure), while hex hashes, UUID lists, base64 image headers and data URIs, integrity strings, provider object ids and ordinary identifiers stay as written; a known secret prefix glued after an underscore or hyphen inside an identifier or a URL path segment is masked from the prefix on; and a masked value no longer swallows a closing backquote at its end, or a closing round or square bracket at the end of a URL that has no matching opener, while a backquote inside a value never cuts it short; an address after `Bearer` or `Basic` that is itself the value of a credential label (including `Authorization:`) is masked whole, keeping the scheme word, while an address with no label stays as written. Credential masking also covers three shapes that used to print as written: a name glued onto a credential word (PGPASSWORD=, CUSTOMSECRET=, dbPassword:, secretAccessKey=; singular only, so token-count names such as maxTokens or inputTokens and English words such as monkey stay visible), Bearer or Basic glued to a preceding word character (_Bearer, xBearer), and a known secret prefix followed by one unbroken run of six or more letters and digits (sk-abcdefghijk). The new readings only add masks: a value that was already masked whole stays masked whole when it contains one of these shapes. The configured-address display and the upload check read the same label rule, so a glued name used as a query key counts as a credential in both. A frozen copy of a pattern net that a downstream host used to run locally is replayed against the output as a no-regression check. A length cap never splits a redaction marker: the cut backs off to before it on every capped path (plain, position-tracking, paired backslashes, and the shared cap of the field-escaping helper); strings that are not complete markers are cut as before. Since 0.98.0 a value glued to a semicolon, a value wrapped in braces or angle brackets (template placeholders stay readable), a bare `Basic` / `Bearer` assigned to an environment-style credential name, and an ordinary word right after `Bearer` / `Basic` in a credential header are masked on every outlet, under a 65 KB linear-time bound; the diagnostic-word predicate answers from the same word list the outlet uses. Since 0.102.0 the configured-address outlet's seven arms are pinned cell by cell (a userinfo whose boundary with the query or fragment cannot be told apart, and a single-slash address that does not parse with a host, show one marker; a leading scheme-less `user:pass@host` keeps host and path; a scheme-less relative request keeps its path and masks every parameter value; a backslash inside a special scheme's authority is read through whichever of the text and the parsed-component reading hides everything the other hides, matched by position, else the whole string is withheld), the reference client's golden inputs are checked to be present verbatim when its tree is available, the general nets' wider scheme-less userinfo and the boundary rule on addresses in prose are pinned with neighbouring negatives, path segment names and values in configured addresses and the Slack webhook tail are pinned over forty shapes (free-text path segments stay as written), non-string input to the three outlets shows one marker without running any caller code, and an in-gate negative control on the boundary rule turns those cells red; pass counts are read as `collections`. |
461
+ | `scripts/run-display-untrusted-baseline-diff-test.mjs` | The credential side of the display outlet (`displayUntrusted` and `washCredentials`), diffed byte for byte against the previous published release: five input pools (seeded random strings, exhaustive shapes around labels, quoted values, userinfo, schemes and invisible units, and URL parameters separated by `;` or `,`) run through fifteen outlets. A secret the previous release masked must stay masked wherever it sits in a credential position; the outlet with only the URL net enabled is judged within that net's own reach. Clean lines from the package's own documents pass through unchanged, and every outlet is idempotent (masking the output again changes nothing). The previous release tarball is supplied through `SEMA_DU_BASELINE_TGZ`; without it the diff sections report as skipped and the run counts as partial, not green. The minimal-shape pool also carries scheme-less userinfo whose password holds a quote or angle bracket after a credential label, and the shapes that once washed differently on a second pass. A full-product grammar pool of 64,512 strings joins the idempotence check, and every pool is also checked for the credential stage reaching its pass cap (it must never do so). The pools now include the bare `token` scheme-word forms and a full `token` grammar product, so the new wash is held to the same idempotence and no-regression bars against the previous release. They also include label and `bearer` / `basic` values that run into a `token` scheme word, a non-ASCII space and an address, with or without a leading character, and quoted values where `token` and an address follow `bearer`. A YAML block-scalar pool (block headers with every chomping and indentation indicator, comments, raw and JSON-escaped line breaks, non-ASCII and tab indentation, invisible characters at line starts, credentials, control strings and bidi characters inside the block, and seven kinds of line after the block) joins all checks: no secret inside a block may show on any outlet that runs the word net, the previous release must show some on the same pool, and the non-block forms in that pool must stay byte for byte as in the previous release. A sixth pool covers lists and YAML node properties after a credential label: block, flow and comma lists plus tag and anchor prefixes, alongside look-alike non-list forms. The secrets in the list pool must stay masked on every outlet, the look-alike forms must read byte for byte as in the previous release, and the three forms whose reading changed on purpose (a lone dash after the colon, a value after a dash, a block sequence of plain key names) are pinned as registered divergences. A seventh pool covers the 0.95.0 credential-net shapes (bracketed values after scheme words and labels, a JSON-escaped tab before the value, unlabelled random-looking tokens built inside the shape rule, associative-array arrows, a label followed by a `token` scheme word and an address, and known prefixes glued after an underscore or hyphen) alongside normal long strings and placeholders: the secrets must stay masked on every outlet while the previous release shows some, the normal strings must read byte for byte as in the previous release, eleven readings that changed on purpose are pinned as registered divergences, and the share of purely random tokens the shape rule catches is read at four lengths with a floor of 0.9. A nested-shape pool pins that the three new credential readings only add masks: a value that the previous release masked whole must stay masked whole when it contains one of the new shapes. At the capped exit, differences against the previous release are accepted only when the old output ended in a truncated redaction marker and the new output is the old one without that fragment. Since 0.101.0 a pool of percent-encoded YAML block scalars and block sequences (166 plain shapes in one to three layers of encoding) is pinned: no secret shows on any outlet while the previous release shows some, benign encoded multi-line text reads byte for byte as in the previous release, six readings that changed on purpose are registered as divergences, and washing twice equals washing once. Since 0.102.0 two more sections compare with the previous release: the wider scheme-less userinfo and the boundary rule on addresses in prose (registered rows byte for byte on the machine wash, no password byte on any outlet with a credential net on, unchanged with both nets off, neighbouring negatives unchanged, idempotent), and a pool of capability addresses judged by zero residue of the secret on the configured-address outlet and in prose (Slack and known-prefix rows masked, the other rows in prose unchanged); the pass histogram reads `collections`. |
462
462
  | `scripts/run-automatic-answer-verdict-test.mjs` | The single verdict `automaticAnswerVerdict(card, lane, facts?)` that decides whether an automatic lane (the standing permission mode, or a standing allow rule) may answer an approval card on the user's behalf. The only `may_answer` exit is a well-formed class set whose classes are all known to this build, whose folded row does not block on that lane, with no must-ask evidence on the card and, for the mode lane, a host reading that its own ask rules did not match; every other card is `must_ask` (with the road that decided it) or `unknown` (with why). The guard derives every single-class card and every ordered pair of classes on both lanes from the engine's own arbitration table, walks every row of the decision table on both sides (including positive evidence when the class set is absent or malformed, class sets that change while being read, and class names this build does not know), compares the package's fold against the engine's fold function over all singles, pairs, triples and a random sample, checks the mode lane against the engine's bypass-mode arbitration (including the release disclosure), confirms on a real engine run that a matched ask rule always puts `ask_rule` in the class set, checks that the two older predicates never answer more loosely than this verdict nor than the previous release, and, when a terminal reference tree is available, that the terminal's own decision chain auto-approves nothing new when it is built against this package. Since 0.94.0 it also covers the class-aware form of the mandated-question sentence: the clause naming an automatic check appears only when the card's question classes let the classifier or the sandbox answer, checked class by class against the engine's own table, and any unreadable card falls back to the generic sentence. Since 0.94.0 the mode lane reads the bypass column on the tool face of the card (the face comes only from the generated shell-face name list, matched exactly, and releasable classes are read per face from the generated table): a write whose protection could not be resolved is answerable on the shell face and still asked on file tools, while lookalike tool names (case, whitespace, zero-width or homoglyph characters, protocol prefixes, rule syntax, inherited or accessor names, non-strings, absence) always read as the other face; when the installed engine exposes faces, the fold and the bypass arbitration are compared per face against the engine and every catalog name is checked against its face. A third lane, `permission_request_hook`, reads the same column as the standing-rule lane (whether a stored rule may clear the class) with the same must-ask evidence first, ignores allow-rule shadow evidence and host facts, and `permissionRequestHookScope` turns its verdict into allow-or-deny, deny-only or skip; a real engine run confirms that an automatic answerer's allow is refused exactly on asks that carry the real-approval bit, and that the lane never reads more loosely than the engine does. The terminal-chain comparison accepts exactly the shell-face release as the one intended widening. |
463
463
  | `scripts/run-allow-rule-shadow-projection-test.mjs` | The reader for the fact that a standing allow layer matched a call but could not clear its ask (`readApprovalAllowRuleShadow`, `approvalAllowRuleShadowed`, and the structured `allowRuleShadow` disclosure stamped on the approval card). Servers send a list of members with their matched entries, and that list is the only thing read. Since 0.98.0 the single text that older servers and older parked rows sent is no longer read as a disclosure: a frame or row that carries only that older text has no disclosure and no sentence, while its presence still counts against automatic answers — the card paths record it as unreadable evidence and the automatic-answer check answers "not known" for it, also when a bare frame is handed straight to the check — so retiring the reading never lets a remembered rule answer an ask it could not answer before. The guard covers every member, several members in order, empty entry lists, the count of omitted entries, member names this build does not know, four kinds of malformed values (all of which still count as evidence), absence, the older text on both card paths and the parked-ask path through the real card port (including a row whose risk descriptor is inherited or comes from a getter), a check against the previous release that every older-shaped frame and row it treated as evidence is still not answered automatically, real frames and durable rows minted by the server fixtures when available, and the effect on every automatic lane. The one sentence a card shows for that fact is minted in one place (`allowRuleShadowSentence`): an absent disclosure renders nothing (it never says there is no rule), an unreadable one says so, and entries and member words arrive display-sanitised. |
464
464
  | `scripts/run-engine-agent-absence-projection-test.mjs` | Absent background agents: when the engine stops reporting a background agent and no final state has arrived, the row is marked absent and this package owns every decision about it, so all clients agree. One predicate says whether a row is absent (the mark, not the status, decides). An absent row keeps its last known status, never counts as running, and is never counted as completed, failed or stopped; its elapsed time stops at the last moment it was seen, and its sentence says it may still be running. The end-of-turn sweep never settles an absent row (or a resident one). A row that comes back, or a real final state for the current cycle, clears the mark; a late final state from an earlier cycle does not. Absent rows are never removed at the short grace window. After the hard limit (30 minutes from the last time they were seen) the host is asked for the background-agent registry reading of each row: only a reading that the agent has ended or is not listed lets the row go, and each removal is returned as a fact the host must act on and announce; a reading of running, unknown, missing or unrecognised keeps the row and schedules nothing, so no standing poll is created. Until a registry reading is available every absent row stays. A row someone is viewing is held and reported separately only once the registry confirms it is gone. The row sentence, the removal sentence and the late-result sentence come from one place, never state an outcome or that the agent finished, and escape control characters in names, in the engine's removal word and in the late-result status. An end-to-end cell drives the real fleet projection and the real absence channel through every decision. From 0.84.0 the package also reads the registry itself: a reader lists the session's background registry only when the server advertises the listing, classifies its failures as unavailable (a failed read keeps the HTTP status and error code when the error carries them, and never invents either), and refuses a partly readable listing as a whole; a classifier turns the listing into the per-row reading, treating a registry status it cannot place as unknown and reading "not listed" only for keys shaped like registry handles and only when the host states the engine is a single process and names the row's session, because a key from another identity space proves nothing by being absent and, on a multi-replica deployment, a listing answered by another replica does not contain this session's agents at all (without that statement a missing row reads unknown and the row stays); and a predicate returns the agents the registry still counts as live that the host has no row for. A listing at the server's 500-row cap cannot show that an agent is gone either, so a missing row there reads unknown; the cap is checked against the engine's own list clamp and, when a server build is supplied, against the server's route. Each listing carries its session and a per-client sequence number, and a listing superseded by a later delivered read, or read for another session than the one the host names, counts for nothing. Only the exact listing object the reader returned can authorize removing a row or adding one: a spread copy, a structured clone, a filtered or a hand-built listing reads unknown and adds nothing, the listing, its row array and every row are frozen, the cap check uses the row count recorded when the page was read, and rewriting a listing's sequence number fools nothing. The fill predicate holds back agents first seen within the absence settle window — timed on one monotonic local clock from when this client's reader first saw the id, so a skew between the server's clock and this one, or reconnecting to a long-running agent, cannot skip the window — agents without a readable registration time and agents the host saw end since the read went out; the registry key of a row, progress or absence event is whichever of its two ids is shaped like a registry handle; and none of the reader, the classifier or the predicate throws. |
@@ -624,7 +624,7 @@ guard still cross-checks the table by name).
624
624
  | `scripts/run-seat-session-patch-test.mjs` | The seat's rename and pin verb. The seat gains one optional verb, `patchSession`, that writes the two user-writable fields of a session row — `title` (a string renames; `null` clears back to the automatic title, and an empty string is not a way to clear) and `pinned` (a boolean) — and answers with the row as it is after the write; the host forwards it to the engine, and the seat contract fixes only the shape. The inbound judge `checkSessionRowPatchRequest` accepts exactly the three keys, requires a non-empty `sessionId` and at least one of the two fields that is not `undefined`, and refuses anything else naming the key; accessors are never run; a non-enumerable own key is refused naming the key, whatever its value (a structured clone would drop it, so the gate does not let it pass unread); the request must be a plain record (prototype `Object.prototype` or `null`), so `Date`, `Map`, `Set`, class-instance and custom-prototype carriers are refused; a key found only on the prototype chain does not count as carried. It deliberately does not copy the server's title rules (length and surrounding whitespace): the server judges them and reports a typed error, and the guard pins that choice with two cells so that copying them later is a visible decision. The receipt row goes through the existing outbound gate, which now accepts `pinned` only as a boolean (absent means not pinned or unknown, never an error). Whether the verb can be used is answered only by the seat capability `patchSession`: three readings (reported, not reported, unreadable), three distinct disabled sentences that differ from the other capabilities', and no cross-talk between capabilities. The guard also reads the built declaration file with the TypeScript parser for the verb signature, the request members (only `sessionId` required), `LocalSessionRecord.pinned` and `SeatCapabilitiesState.patchSession`, drives a host stand-in end to end through the public entry points (publish the capability, subscribe, judge, call, outbound gate, structured clone, withdraw), and poisons every key of the new coverage-table row. |
625
625
  | `scripts/run-catalog-routing-test.mjs` | A gate for the message-catalog migration. Sentences that move into the catalog must read byte for byte the same in English as before the move, and must exist in Chinese. Each corpus entry (one sentence producer called with one set of inputs) is compared in English, with and without an explicit language argument, against digests taken from the pre-migration build; a sentence the changelog names as reworded is pinned to its new digest instead of being waived. In Chinese every entry must contain Chinese characters, differ from the English text, and contain no run of three English words outside registered protocol terms, quoted foreign values and code spans; no placeholder may be left over and the message ledger must stay empty after the whole corpus has run. A syntax-tree check requires every function that takes a language argument to pass it on to every other language-taking function it calls, and every exported language-taking function must be covered by a corpus (or be listed as pinned by another gate or as a not-yet-migrated placeholder, which the gate verifies). The gate also proves its own detectors on planted cases. Read-only modes print the digests of a build and compare two builds entry by entry. |
626
626
  | `scripts/run-host-turn-settings-test.mjs` | The single assembly entry for host-supplied request settings (`hostTurnSettings`): hooks, the resolved permission snapshot, the web-search setup and the output-style name reach the request body only through its product, which always carries one row per key (supplied, explicitly absent, or not wired at all, the last one with a session notice and a self-check that names it), a turn verdict, and notices. The request builder takes the product and refuses these keys when they are written straight into the input settings. The suite checks that the request bodies match the previous release byte for byte on inputs whose projection did not change, the missing-source and explicit-none rows, the refusal when managed hooks cannot be loaded (thrown only on a lane that carries the refused key behind an open live gate: the one-shot side-channel lane never carries hooks, so it is sent as before), the final check of the hooks plan against the settings-contract classifier before sending, partial layers, and, against the pinned server fixture, that a configuration the classifier accepts but the server's own limits refuse still fails through the generic invalid-request path. A permission-rule projection failure (for example missing core value ports) now refuses the turn instead of sending without any permission rules, and the thrown refusal carries the underlying assembly refusal as its `cause`. |
627
- | `scripts/run-settings-credential-key-paths-test.mjs` | The one judge of which keys in a settings document hold credentials. It returns key paths only, never values, as findings that carry an optional `numericByNameOnly` flag for count-style names whose value is a plain integer (the flag is present only when true, so a restrictive file-mode decision can still count them while a user-facing notice leaves them out). Key names are judged on two deliberately wide tables, one for environment-style names and one for structured-config names; values are judged only through the display layer's known-secret shapes, URL userinfo and credential-bearing query strings, and an all-string array is read as a command line (`--password <value>`, `--token=<value>`, `NAME=<value>`, `Authorization: …`). Empty strings, null and false do not count. The golden cases, the residual boundaries (a single-letter short option is not judged, credentials embedded in command strings match only by known shape, an unprefixed random value under an ordinary name does not match) and the never-throws / never-leaks behaviour on cycles, getters, throwing proxies and very deep nesting are pinned, together with a differential against the settings package's own secret-token check and the reference client's name table when they are installed Since 0.101.0 it also pins that percent-encoded YAML block scalars and block sequences inside a string value answer exactly as their plain form does: a known credential shape inside an encoded block is found at every layer of encoding, malformed byte sequences next to it do not hide it, a label with a low-entropy value inside a string value is not found in either form, a block inside a URL query string is found through the query check, a whole address that is itself percent-encoded (credentials in its userinfo or query) is found as the plain address is while an encoded address without credentials is not, and three in-gate negative controls (without the known-shape check, without the query check, reading the address only unencoded) turn the cells red. |
627
+ | `scripts/run-settings-credential-key-paths-test.mjs` | The one judge of which keys in a settings document hold credentials. It returns key paths only, never values, as findings that carry an optional `numericByNameOnly` flag for count-style names whose value is a plain integer (the flag is present only when true, so a restrictive file-mode decision can still count them while a user-facing notice leaves them out). Key names are judged on two deliberately wide tables, one for environment-style names and one for structured-config names; values are judged only through the display layer's known-secret shapes, URL userinfo and credential-bearing query strings, and an all-string array is read as a command line (`--password <value>`, `--token=<value>`, `NAME=<value>`, `Authorization: …`). Empty strings, null and false do not count. The golden cases, the residual boundaries (a single-letter short option is not judged, credentials embedded in command strings match only by known shape, an unprefixed random value under an ordinary name does not match) and the never-throws / never-leaks behaviour on cycles, getters, throwing proxies and very deep nesting are pinned, together with a differential against the settings package's own secret-token check and the reference client's name table when they are installed Since 0.101.0 it also pins that percent-encoded YAML block scalars and block sequences inside a string value answer exactly as their plain form does: a known credential shape inside an encoded block is found at every layer of encoding, malformed byte sequences next to it do not hide it, a label with a low-entropy value inside a string value is not found in either form, a block inside a URL query string is found through the query check, a whole address that is itself percent-encoded (credentials in its userinfo or query) is found as the plain address is while an encoded address without credentials is not, and three in-gate negative controls (without the known-shape check, without the query check, reading the address only unencoded) turn the cells red. Since 0.102.0 a string value is judged against the configured-address display outlet's own scan: whatever that outlet would mask as a credential (a scheme-less Slack webhook, a key in an address path, a single-slash address, `access_token=` in a fragment, a labelled value inside prose or a command string) is found, and an address whose only value is already a redaction marker is no longer found; the cells for those shapes, the labelled-value changes to the residual boundary and to the low-entropy block cells, and an in-gate negative control without that scan are pinned. The differential against the settings package's own secret-token check now pins one direction only: every value that check finds is found here too, and the values found only here (shapes the display outlet masks, such as a short `Bearer` value after a prefix) are listed one by one, so a new difference in either direction turns the cell red. |
628
628
  | `scripts/run-mcp-wire-caps-test.mjs` | The local MCP config projection. A server name goes on the wire byte for byte: a name with leading or trailing whitespace, an empty name, or a name that would collide with another entry once the whitespace is stripped is left out (not silently trimmed, which let ` blocked ` reach the wire as `blocked` and slip past a disabled-server list) and is reported by `projectMcpConfigs`, whose `dropped` rows carry a sentence for the user. `elicitation` is judged by one reader for both the local and the cloud-configuration leg (`mcpElicitationVerdict`): a boolean passes through as written (`true` and `false` alike), absence adds no key, and any other value, `null` included, leaves that one server out with a sentence naming the server and the key. A `toolAxes` declaration is judged by the engine's own reader through the core value port (`mcpToolAxesVerdict`): a bad shape, `null` included, leaves that one server out with a sentence naming the server, the key and the fix, the other servers are unaffected, and with no judge installed the declaration passes through unchanged. The suite feeds the products to the live server fixture's request-entry check: the 400 for a bad `toolAxes` is gone, and a non-boolean `elicitation` sent as written would still be refused by the server, naming the key, which is why neither leg sends it. Since 0.100.0 the name, transport and identity-header readers are the settings contract's own judges, copied at build time and read by both the local and the cloud-configuration leg, so the two legs answer every input the same way. A name that is empty, longer than 128 characters or has leading or trailing whitespace leaves its entry out with a sentence built on a bad-name summary that never echoes the whole name; a transport that cannot be used at all (no command, an address that is not an http(s) URL) leaves the entry out and names the key path; and a transport whose optional parts cannot be used (an argument list with a non-string member, an environment or header record that is not plain string data, a member literally named `__proto__`, a non-string principal header) keeps the entry on the wire as the judge's independent copy without those parts and reports what was left out — key paths and member names only, never a value. Setting both identity headers leaves the entry out with a sentence (it used to be skipped silently). The suite feeds these products to the live server fixture's request-entry check on both generations: every product is accepted, while the same declarations sent as written are refused. Nothing is capped at 32 servers any more: forty servers and a skill body above one mebibyte go on the wire as written, servers from 7.132.0 refuse the whole list or body with a field-invalid answer that names the limit, and older servers keep the first 32 on submission as before. A server start timeout passes through as written and a null server list is never produced. A standalone server-name verdict, `mcpServerNameVerdict(name, siblings?)`, answers whether one name can go on the wire byte for byte (usable, or one of four reasons: blank, edge whitespace, collides after trim, too long) with the same judge as the whole-table projection: on one table the two agree name by name. A collision needs the sibling names; without them only the other three reasons are judged. Non-string names count as blank, a non-iterable sibling list throws a type error, and the result is a fresh frozen object. The `toolFaces` declaration of a server entry is judged the same way: its shape is asked of the engine's own reader through the core value port, a malformed declaration keeps that one server off the wire (the other servers go as before) and is reported with the engine's own sentence, a well-formed one goes out as the engine's plain copy, and with no judge installed the value passes through untouched. A pool of three thousand seeded declarations is compared with the engine's reader sentence by sentence, and the published server's request-acceptance door shows which malformed shapes it refuses itself and which only the engine's own door would. |
629
629
  | `scripts/run-panel-reduce-test.mjs` | The panel row reducer: one record per background agent, found through one identity index of engine ids — the fleet row id tail and the sub-run id (the id ticks and transcript anchors carry) are bound together only when a single frame states both, and two rows later proven to be the same agent are merged, the duplicate leaving with a `remove` that names the surviving row (`supersededBy`). A delegating card id never decides which agent a frame belongs to: it is a display field and an "already seen in this process" hint only. One generation rule, one place that constructs terminal facts and one place that emits a `terminal` row delta. Usage is monotonic within one known generation: a tick moves the numbers when its own cycle number matches the row (for an agent without cycle numbers the sub-run id decides), a row frame that binds a new sub-run id to the same tail starts a new generation, a terminal row freezes the phase but not the usage, and finality is reported per counter (`usage_delta.final`), a value-unchanged change of finality included. `reducePanelFrame(state, frame)` is pure — no I/O, no clock read (the projection frame carries its own `nowMs`), no module state, the input state is never mutated, the same input always yields the same deltas and replaying a frame yields none. The delta set is closed (`upsert` / `usage_delta` / `terminal` / `remove`); `subscribeEnginePanelRows` replays a snapshot on subscribe. Twelve end-to-end sequences cover interleaved lanes, the first-published id, revival, reconnect/absence, late subscription, short-lived agents in both orders, historic rows, absent usage never filled with zero, the turn-end sweep scope, the transitional event channel versus the row channel, bounded eviction and snapshot fidelity. The generation criterion now lives in a dependency-free leaf module that both the panel record and the fleet ledger share; each layer keeps its own state (the panel's per-row watermark, the ledger's per-task watermark) but the comparison and the notion of a valid cycle number exist once. A data-flow scan keeps cycle numbers and watermark readings out of every comparison operand in the ledger (with planted violations to prove the scanner speaks, and no false alarm on presence checks or shape tests), and the numeric view is pinned against an independently written integer comparison over a grid that includes malformed values. A background agent terminal notification may carry the child session id; the reducer treats it as binding evidence, so a host that feeds only task events (no fleet rows) gets one row per child with the engine's final numbers, and a revived child no longer keeps the previous generation's tool count beside the new generation's token count. A tick carries its engine cycle number: a background child usage is driven by ticks again when the tick cycle matches the row, a tick from a previous cycle is dropped whole, a tick from a newer cycle neither raises the watermark nor counts, and a terminal tick marks exactly the counters it really carried as final. |
630
630
  | `scripts/run-provider-preset-baseurl-mirror-test.mjs` | Mirror ledger for the one hand-copied engine default. The provider preset for the first-party hosted API carries a `baseUrl` that is a hand copy of the engine's own exported default root (the constant the engine falls back to when neither a model entry nor the deployment names a root). The guard reads the constant out of every readable copy of the engine package (the one this package is built against, an explicitly pinned one, and the fixture copy when present; an absent fixture is reported as a skipped section, and no readable copy at all skips the gate) with a TypeScript syntax-tree reader that accepts exactly one exported string-literal declaration, checks that the package root re-exports that very name, and requires the preset's `baseUrl` to match it byte for byte (a trailing slash, a changed letter or a different case is red). A census then requires that the only source file in this package that carries the hosted API's host name is the preset data file, so a second hand copy cannot appear unnoticed. Every ruler is proven against planted samples before it is trusted |
@@ -666,6 +666,11 @@ guard still cross-checks the table by name).
666
666
  | `scripts/run-scenario-skills-projection-test.mjs` | Scenario-card skill list reader. From server 7.135.0 each scenario card carries an always-present `skills: [{ name, description, source }]` — the deployment skills handed to every run of that scenario, `source` being `factory` (the deployment's skill directory) or `center` (the configuration source), no bodies — and the capabilities body carries an always-true `scenarioSkills` flag (absent on older servers). `scenarioSkillsOf(card)` reads the list: an absent, non-array or unreadable `skills` answers `undefined` (never an empty list — `[]` means the server said the scenario has none), entries are isolated one by one (a malformed entry drops only itself), a `source` outside the two words is marked `unknown-source` without dropping the entry, nothing is trimmed, de-duplicated or reordered, and the text the engine injects into prompts is never scraped. `observedScenarioSkillsCapability(baseUrl?)` reads the flag from the landed capabilities (confirmed / absent / declined / unreadable / unobserved); it is evidence only and never a precondition for reading a card. The gate checks the shapes against the 7.135.0 server bytes, feeds the package's own list function's real output through the reader, and runs hand-made broken readers that must each turn it red. |
667
667
  | `scripts/run-mcp-allow-tools-fail-closed-test.mjs` | MCP server `allowTools` fail-closed. `allowTools` narrows which tools of one MCP server are mounted (an empty list mounts none, an absent key mounts all), so a malformed value must never be silently dropped. Both projection legs — the local `.mcp.json` leg and the cloud-config leg — now read it through one judge, `mcpAllowToolsVerdict`: a value that is not an array of strings (a non-array, a non-string element, a sparse array, or something that cannot be read) keeps that one server from starting and discloses a row that names the key path (`allowTools` or `allowTools[2]`) without echoing the value; an empty list is still sent, an absent key is still omitted, and what goes on the wire is a copy taken at read time. The gate runs both legs over every malformed shape, checks the two legs agree with the judge value by value, and runs hand-made legacy implementations (silent drop of a non-array, silent filtering of non-string members, holes accepted, shared reference, empty list folded to absent, value echoed, disclosure before the transport check, double read) that must each turn it red. |
668
668
  | `scripts/run-approval-frame-families-declaration-test.mjs` | Request key `settings.approvalFrameFamilies`, the approval-card frame families this client renders. The package mints the declaration itself at assembly time from a reading the host hands in (`approvalFrameFamiliesReading`, taken for the engine the request goes to): the families the server presents (the top-level `approvalFrameFamilies` capability array) intersected with the families the package can render. Without a reading no key is sent, and assembly never reads the default engine target. Two bits must both be well-formed — the acceptance bit `taskSettings.approvalFrameFamilies` (the server takes the key) and the presented-families array — otherwise no key is sent and the request is byte-identical to the one built before this key existed: absent, declined, unreadable and unobserved readings never mint a key, an out-of-table word declines the whole bit, and a server that presents the array but does not accept the key is never sent it. An empty intersection is declared as `[]` rather than skipped. The reader `observedApprovalFrameFamilies(baseUrl?)` and the judge `approvalFrameFamiliesDeclaration(reading)` are pure over the shared capabilities cache and answer five reading states. The gate replays 32 capability body shapes, accessor and throwing-proxy bodies, prototype-chain pollution (of the key and of descriptor values), a throwing engine-target getter and unprobed / failed / invalidated caches, pins the assembled request byte for byte against the pre-key output on the older server shapes and, in a two-session host, for the session whose server does not accept the key, cross-checks the server's key list, tuple and capability mint site in the installed server packages of every generation, and compiles the server's own admission judge and approval-leg resolver verbatim: the declaration the package assembles must keep the leg from falling to the unattended path, while no declaration, or the other family, must make it do so. Hand-made broken readers, judges and assemblers must each turn red. |
669
+ | `scripts/run-session-background-projection-test.mjs` | Session hand-off to the background: the capability reading `readSessionBackgroundable` (five states), one engine wire per engine generation, the detach posture and the package's own opening of the interactive stream `streamInteractiveRun` (request and headers snapshotted once at entry; every spelling of the detach header replaced by a single canonical value; one header-less reopen after the exact durable-off 400 unless the idempotency key is pinned; one opening record per stream), the single eligibility judge `backgroundHandoffEligibility` (fifteen ordered rows, three outcomes) with its sentence, and the hand-off verb `handoffInteractiveRun` (it tears only the stream the package opened, never interrupts or cancels the run, and the background view rereads the durable ledger from the start). Pinned: the five stream shapes that return a run id without running for this submission never become eligible; a parked terminal frame ends the original stream; hand-off racing completion settles exactly once; an approval or question handed to the host but not yet processed, a pending question and an approval hold on the run keep the answer in the foreground; a capability generation change or a retired wire refuses; Esc after hand-off is withheld; every reader never throws; a second copy of the package cannot recognize the wire; every judge row is pinned with all later rows' conditions also true. |
670
+ | `scripts/run-thinking-latch-projection-test.mjs` | Session-level extended-thinking switch. The request's `reasoningEffort` is minted on exactly one line of the assembly entry, from one projection that reads the lane, the switch reading the host passes (`thinkingLatch`), the effort word, an optional per-turn floor (`reasoningFloor`) and whether this request really carries `settings.ultracode: true`. Once set, the switch alone answers on or off: off sends `"off"`, on only lifts an effort word of `"off"` and never picks a level for the user; with Ultracode on the same request both go on the body and the engine decides which applies. Unset with no floor leaves the request body byte-for-byte unchanged against the previous release. The transition table, stored form and read classification exist once in the package; a seat session keeps its switch in the desktop host (verb `setThinkingLatch` with an `OkAck` receipt, capability `thinkingLatch`, a four-word row field read back only through `readSeatThinkingLatch`, so a failed read is never reported as never set). |
671
+ | `scripts/run-mods-face-projection-test.mjs` | The three user-facing mod seats. A registration record is read one way wherever it comes from (the run receipt's `mods` section, the refused-registration notice's detail, the in-process kernel's records): own data properties only, each key isolated, the two shapes told apart by member presence, no derived disposition word. `modRowsOf` answers `undefined` for anything that is not an array or where no row survives (unreadable is not zero) and `[]` for an empty list; `readModRegistrationRefusedNotice` turns the refused-registration notice into the refused row, or answers `undefined` so the host keeps the engine's own line. On tool result rows the package adds `_sema_answered_by`, the registration that answered a call at the `tool.call` station in place of the tool, from one criterion evaluated once at the single row mint against the same gate view it writes to `_sema_gate`: the engine's own fail-closed marker `engine` never counts as an answering registration, a refusal needs a `denied` / `hook` gate record and `isError: true`, an answer in place needs a gate record that is not a denial and `isError: false`, and an unreadable gate record or an `isError` that cannot be read yields nothing. `modCardSentence` renders the registration card in its two shapes and the refused-by / answered-in-place line; `modLoaderFailureSentence` renders a load failure from whatever the host caught. The gate drives the real kernel in process, the installed server package's receipt and notice projections, and frames derived from the engine's `tool_end` type through both the interactive and the print outputs; it compares the receipt projection byte for byte with the previous reading over the existing samples and a generated pool, and checks that every outward string goes through the display sanitizer. |
672
+ | `scripts/run-display-untrusted-phase-ladder-test.mjs` | The ladder behind the credential side of the display outlet. Since 0.102.0 the rules added in a release run only after the older rules have settled on their own, and then alternate with them until nothing changes, so, for every input, the masked text before any length cap covers at least what the older rules (everything outside the table, including this release's changes to them) mask on their own; not masking less than the previous release is measured on frozen pools, not guaranteed by construction. The gate checks that there is one rule table, that rows are only additive candidate generators read through a single emit point, and that the single-pass reading is gone (the upload check and the settings judge read the same ladder). Against the previous published release (a tarball path in `SEMA_DU_BASELINE_TGZ`; any other version reports a skipped section, not a pass) it checks on frozen pools that the first ladder step, taken on the production path, is byte-identical to the previous release in text and position map wherever this release's base rules did not change, and identical to a variant with an empty table everywhere; that no original character becomes visible again, at the scanner and at the address-reading level (where this release reads a configured address differently from the previous one, it either withholds the whole string or every original character the previous release's reading hid stays hidden, checked by flipping one character at a time and watching which positions of the rebuilt address change, not by the package's own alignment); that exit idempotence does not get worse in seven option groups, nor on any single row in six of them; that clean text is collected once; that folding this release's rows into the first step would expose values on constructed rows; and that the nested value judge and the machine-readable wash agree. Pinned counterexamples: for addresses whose own text already has something to hide (a backslash that would shorten the authority, an encoded `@`, a host the rebuild would lower-case), every credential reading (machine-readable wash, upload check, settings judge, settings notice) answers that it carries a credential and the configured-address output is pinned byte for byte; across the pools, a `scheme://` value whose userinfo the wash hides is never judged clean; the widened scheme-less userinfo reading only adds candidates; both families are idempotent; an anchored form inside a control string stays as-is in the first step; whole-string withholding of an ambiguous or unparseable address is not switched off by marker-shaped or escape-shaped text in the value; a backslash address keeps its own text whenever that reading already hides everything the rebuilt reading would. Also: the same text written twice in one address is never taken as the source of the other copy, a marker or a literal escape outside the authority no longer decides how a backslash address is read (one inside the authority marks it as already displayed and keeps its own text), every pool row's chosen reading is checked against the other one by flipping characters (both directions for backslash addresses; for single-slash and no-slash forms every hidden run must land whole and verbatim in one URL field), the package's own position map between the text and the rebuilt address agrees with that flipping test (surrogate pairs, non-ASCII, upper-case hosts, ports), placeholders and the outlet's own escapes in a backslash address stay where they were, and an anchored form inside a control string stays as-is in the first step in its raw, escaped and percent-encoded spellings. Nine in-gate negative controls (restarting the second step from the original text, dropping the step tags, letting the judge read only the last step, no longer accepting a marker inside the anchored tail, dropping the step tags at each of three local copy points: nested control-string content, nested on-screen and decoded content, a bare query part, dropping them for decoded content alone, and rebuilding a backslash address without first asking whether its own text already hides enough) each turn a cell red. |
673
+ | `scripts/run-session-namespace-key-test.mjs` | Engine namespace key of the session map. The key under which a connected engine's continuity record (engine session id, last task id, sync watermark) is stored is its lowercased origin plus its normalized path: trailing slashes are dropped, path case and percent-escapes are kept as written, and the query, fragment and userinfo are not part of it, so two engines behind one relay origin no longer share a record. A target with an empty path keeps the earlier origin-only key and reads its older entry unchanged; an entry stored under the bare origin is never read for a target that has a path (one reading, no fallback). The self-spawned engine stays `local`; a malformed url, and a url whose origin is opaque, falls back to its raw lowercased string. Cells cover the key shape and spelling variants, role and origin separation, the same-engine comparison an auto-sync no-op check builds on, reading of older records, boundaries (IPv6 hosts, default ports, dot segments, percent-escapes, doubled slashes), the single exported reading, and a never-throws sweep. |
669
674
 
670
675
  Each suite carries a floor that only moves up — a refactor that stops executing a group of
671
676
  assertions is a failure, not a quieter pass. Guards anchor on the **installed artefact's content**
@@ -37,8 +37,9 @@ export interface ToolResultRowFlagParts {
37
37
  readonly _sema_interrupted_never_started?: true;
38
38
  readonly _sema_approval_window_expired?: true;
39
39
  readonly _sema_denial_limit_window_expired?: true;
40
+ readonly _sema_answered_by?: string;
40
41
  }
41
- export declare function toolResultRowFlagParts(arm: object | null | undefined): ToolResultRowFlagParts;
42
+ export declare function toolResultRowFlagParts(arm: object | null | undefined, gate: GateOutcomeView | undefined): ToolResultRowFlagParts;
42
43
  interface ToolEndPayload {
43
44
  output?: unknown;
44
45
  structured?: unknown;