@sema-agent/settings-schema 14.1.0 → 14.3.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.
package/CHANGELOG.md CHANGED
@@ -8,6 +8,72 @@
8
8
 
9
9
  ## Unreleased
10
10
 
11
+ (空)
12
+
13
+ ## 14.3.0 — 2026-10-21 — minor:信任阶梯词表做纯常量 `MIN_TRUST_WORDS`(`MinTrust` 由它派生;`trustRank` / `isAtLeast` 闭包零 zod,整函数抄走的消费方只登记一只全局)
14
+
15
+ > 档位(按发版纪律):**minor** —— 加性:新导出 `MIN_TRUST_WORDS`(包根与 `./safety-merge-spec`);型 `MinTrust`、枚举的词与声明序、`trustRank` 的数值与全部答案、未知词的 RangeError 句逐字节不变。
16
+
17
+ - **信任阶梯的词表是一只纯常量 `MIN_TRUST_WORDS`;`trustRank` / `isAtLeast` 的闭包只引这一只纯数据,不碰 zod**。此前 `trustRank` 的 `switch` 本身零 zod,
18
+ 唯一的 zod 引用是 `default` 臂 RangeError 句里的 `MinTrust.options.join(", ")` —— 于是按「闭包零 import、只登记全局」整函数抄走判官的消费方抄不了
19
+ `isAtLeast → trustRank` 这条链(`layerAdmission` 经它判层档);手写一份层表就是第二份层表。现在词表来源是数据:
20
+ `export const MIN_TRUST_WORDS = ["local", "session", "project", "global", "managed", "task"] as const`(声明序 = 既有枚举序,逐字节不变;声明序不是阶梯,阶梯只经 `trustRank`),
21
+ `MinTrust = z.enum(MIN_TRUST_WORDS)` 由它派生(与 `AUTONOMY_MODES` → `AutonomyMode` 同形),`trustRank` 的默认臂句读 `MIN_TRUST_WORDS.join(", ")`(句字节不变)。
22
+ 结果:`trustRank` 的自由标识符 = `MIN_TRUST_WORDS` + JS 内建 `RangeError` / `String`,`isAtLeast` 的 = `trustRank`;抄走这一对的消费方把 `MIN_TRUST_WORDS` 登记成全局就够了。
23
+ 不另出第二只函数、不出生成物(一只判官一份源)。
24
+ 三问 —— **谁需要**:按闭包抄判官的消费方(构建期生成器),以及任何抄 `layerAdmission` 链的包;**谁受伤**:无(新导出,既有导出对既有输入的答案逐格不变);**补偿**:无需。
25
+ 钉:`test/trust-ladder-zod-free.test.ts`(① 常量逐位 ≡ `MinTrust.options` 且 = 既有声明序 · ② 两只函数的自由标识符与代码标识符 · ③ 裸 `node:vm` 上下文只注入 `MIN_TRUST_WORDS`,六词 rank / 6×6 真值表 / 未知词的 RangeError 句与包内直接调用逐字节同);
26
+ `test/safety-merge-spec.test.ts` 的声明序文本钉改读新形(词表字面量恰一处、`MinTrust` 由它派生恰一处)。
27
+
28
+ ## 14.2.0 — 2026-10-18 — minor:守卫迹象报告序改句 · 关停位任一层 tighten-only(删 managed-only 告知)· 项目层 `.claude/settings*.json` 只经 layerAdmission 读 tighten 方向并点名未读部分 · 控制 / 双向字符剥除集根导出(server / cli 同读,漂移门 ⑨ 退役)
29
+
30
+ > 档位(按发版纪律):**minor** —— 全部加性:新导出(`CONTROL_AND_BIDI_CHARS` · `stripControlAndBidi` · `hasControlOrBidi` · `layerReadsTightenOnly` · `LAYER_PARTS_UNREAD_CODE` ·
31
+ > `buildLayerPartsUnreadNotice` · 型 `LayerPartsUnreadNotice`)、三张闭集各尾插一词(`SafetyMergeCategory` + `killSwitches` · `SETTINGS_SOURCES` + `foreignProjectSettings` ·
32
+ > `LAYER_ADMISSION_VERDICTS` + `tighten_only`)、`SettingsSourceContract` 多一个可选格 `tightenOnly`;既有导出对既有输入的答案逐格不变(钉见各条);
33
+ > 一处展示串改动(`SAFETY_MERGE_SPEC.permissionMode.seam`,规则字段逐字节不变)与一处文字改真(hooks 分类器报告序)。持穷举表的消费方提货时编译红 = 通知。
34
+
35
+ - **新源 + 新判词 + 披露(写给另一个工具的项目层设置文件只读收紧方向)**。克隆来的仓库常带着写给另一个工具的 `.claude/settings.json` / `.claude/settings.local.json`;
36
+ 此前本包没有它们的源,消费方不读 —— 仓库里写好的 deny 规则在这里**静默不生效,也没有一句提示**。现在源表尾插 `foreignProjectSettings`(项目档、仓库可控、`tightenOnly: true`),
37
+ 判官仍是 `layerAdmission` 一只,律仍是一条:**只读收紧方向的层只准入方向 tighten 的成员** —— 这种层要么是本工具自己的仓库可控层在工作区未受信时(既有,判词 `trust_withheld`),
38
+ 要么是只收紧的源、不论工作区读数(新,尾插判词 `tighten_only`:接受工作区是同意本工具的文件,不是同意另一个工具的 hooks、环境变量与 allow 规则)。
39
+ 于是那两份文件里 deny / ask 规则、关停位的开、沙箱的收紧极照读;allow 规则、hooks、env、MCP 服务器与其余一律不读。MCP 名单判决
40
+ (`mcpServerPolicyVerdict`)同律:只收紧的源只贡献拒绝名单,它的允许名单不读(各层允许名单是拼接,拼进一张就是放宽别层的名单)。`layerReadsTightenOnly(source, workspace)` 是
41
+ 「这一层是不是只读收紧方向」的唯一答案(`layerAdmission` 与披露同读);`SettingsSourceContract` 多一个可选格 `tightenOnly`,既有五个源的行与答案逐格不变。
42
+ **披露**:一只事件码 `settings.layer_parts_unread`(`LAYER_PARTS_UNREAD_CODE`)+ 一行 `buildLayerPartsUnreadNotice({ source, workspace, file, parts })` →
43
+ `<file>: read for restrictions only (<起因>) — not read: <parts>`(起因 `it is written for another tool` / `this workspace is not trusted`,两种起因同一句形;
44
+ 文件与部分名剥控制与双向字符、部分名截 64 字、最多列 16 个再计数);对不是只读收紧方向的层调用 ⇒ RangeError(那句话对它不成立)。
45
+ `SETTINGS_SOURCES` / `LAYER_ADMISSION_VERDICTS` 尾插各一词:持穷举表的消费方提货时编译红 = 通知;旧消费方不传新源就收不到新判词。
46
+ 三问 —— **谁需要**:在克隆来的仓库里干活、指望那个仓库写好的 deny 规则也在这里生效的人;**谁受伤**:那份文件里的 deny / ask 现在会收紧本工具的会话(此前被忽略)
47
+ —— 一个仓库能借此拒掉一些工具调用(收紧方向,与本工具自己的项目层在未受信工作区的待遇相同);**补偿**:一行披露写明读了什么、没读什么,要放开就改那个仓库的文件
48
+ (allow 永远不从这里读,所以它放不宽任何东西)。升级说明:消费方的读法到货前,可以先只出这一行、列出文件里的全部键(只提示不读)作过渡 —— 那是消费方自己的发版说明,本包不设旋钮。
49
+ 钉:`test/foreign-project-settings-14.2.0.test.ts`(三表尾插 · 三读数 × 全类目逐格 · 既有五源与上一版律逐格对拍 · 唯一答案真值表 · 披露两种起因 / 回显 / 上限 / 响亮错 · 其余判官认新源)。
50
+
51
+ - **契约表加一行(关停位任一层可收紧,类目尾插)**。`SAFETY_MERGE_SPEC.killSwitches`:权限模式的两只关停位 `disableBypassPermissionsMode` / `disableAutoMode` 是它的成员
52
+ (方向 tighten、值向):**开**(收紧极)任一设置文件层都可写(`boolPole { strict: true, minTrust: "local" }`,与 `permissions.deny` / `ask` 同一条「收紧不需要信任」律);
53
+ **关** / 非布尔 / 不带值的问法落在行地板 `global`;合并 `replace` + `boolStricter(strict: true)` ⇒ 任何一层的开都不会被别层的关覆盖;`gate` / `closed`。
54
+ 此前表上没有这一行,`permissionMode` 行的 seam 文字写着关停位「只许托管层」,消费方据此在本地盖了一个托管层地板 —— 于是同一个消费方里判官读全部来源
55
+ (任一层写了开都算,与 CC 2.1.296 的限制性键表同形),resolver 却对非托管层告知「只能由托管层配置设置、已拒」,说的与做的相反。现在两边读同一行:
56
+ `permissionMode` 的 seam 改成指向这一行(展示文字;它的合并 / 地板 / 收紧 / 执法 / 失败方向逐字节不变),「只许托管层」的标记从表上删掉。
57
+ `SafetyMergeCategory` 随尾插加宽:持 `Record<SafetyMergeCategory, …>` / 穷举 `switch` 的消费方提货时编译红 = 通知(新词尾插按发版纪律是 minor)。
58
+ 三问 —— **谁需要**:想在用户层 / 项目层关掉 bypass 或 auto 模式的人(此前写了判官认、resolver 告知被拒);**谁受伤**:一个克隆来的仓库可以在自己的设置文件里关掉
59
+ 这两种模式(CC 同此),在那个仓库里要用它们的人看见的是被关掉;**补偿**:关停位只能收紧不能放宽是这条律本身 —— 要用就从那个仓库的设置文件里删掉这一行,没有从更低层重新打开的旋钮。
60
+ 不认识 `boolPole` 的旧消费方经它自己的 schema 读这一行时两员都是 tighten@global(仓库可控层两极都不收,fail-closed)。
61
+ 钉:`test/kill-switches-14.2.0.test.ts`(行形 · 尾插 · 值向准入 · 五源 × 三读数 · 无托管层标记)· `test/safety-merge-spec.test.ts`(1.1.0 冻结钉第四个展示串豁免 · 经 1.1.0 schema 的视图)。
62
+
63
+ - **文字改真(hooks 分类器拒绝项 `field` 的报告序;实现不变)**。13.0.1 那一节写「迹象按文档序取第一个」,与实现不符:一块里有几个迹象时,实现**先报该块自身的迹象**
64
+ (它自有的守卫事件键;条目自己就是匹配器形时取 `hooks`),**再按文档序找嵌套迹象**(匹配器下合形的条目列表不在匹配器这一层找:那里的迹象归各条目,各是一块)。所以同一块里嵌套迹象写在前、自有守卫键写在后时(`{ n: { PreToolUse: … }, PermissionRequest: …, hooks: [ … ] }`),
65
+ `field` 是 `PermissionRequest`,不是文档序第一个的 `n`。改的是句子(d.ts 头注、`llms.txt` / `llms-full.txt` 同句),不是判法:结局、码、坐标、剔点、`field` 逐字节不变。
66
+ 句子按实现改而不是反过来:`field` 先指向块自己的键,用户照它找改哪一行时先看到的就是让这一块被拒的那个键。三问 —— **谁需要**:按 `field` 告诉用户改哪个键、并依文档读报告序的消费方;
67
+ **谁受伤**:无;**补偿**:不需要。钉:`test/hooks-classify-13.0.0.test.ts` ⑥(报告序一格:自有键在后仍先报自有键 · 条目自己是匹配器形 ⇒ `hooks` · 块自身无迹象时才按文档序;随包句子一格)。
68
+
69
+ - **新导出(一行回显的剥除集,单源在本包)**。`CONTROL_AND_BIDI_CHARS`(C0 控制符 + DEL + Unicode `Bidi_Control=Yes` 全集 12 个码点的正则字符类体,`\uXXXX` 转义的源码文本)·
70
+ `stripControlAndBidi(text)`(删掉集内每一个码点,别的字符一个不动)· `hasControlOrBidi(text)`(有 ⇔ 剥了会变),主 barrel。调用方 / 仓库写的串进拒句、告警、日志、屏上一行之前剥掉的
71
+ 那一族码点,此前是请求面两只回显描述器里的私有正则,标成引擎双向码点表的镜像、由上游漂移门逐码点对拍;服务端另存一份 —— 两份表一只门。这张集合是**回显策略**,不是引擎判官词表的镜像
72
+ (引擎的审批文本拒集另含 C1 与行段分隔符,用途是拒不是剥),所以单源归本包:两只描述器改读它,对拍引擎的那一组漂移门退役。集合与此前逐码点相同(全扫钉),C1 与 U+2028 / U+2029 仍不在集里;
73
+ 今后增删码点是本包的行为变更,按发版纪律判档,不跟随任何别处的词表。
74
+ 三问 —— **谁需要**:服务端(退掉自己那份,剥与检都读这里)与任何要把外来串落成一行的消费方;**谁受伤**:无(新导出,行为零变化);**补偿**:不需要。
75
+ 钉:`test/control-and-bidi-14.2.0.test.ts`(导出面 · 逐码点全扫真值表 · 检出与剥除同一集合 · 纯函数 · 两只描述器同读)。
76
+
11
77
  ## 14.1.0 — 2026-10-18 — minor:请求面 MCP 判官与服务端请求腿同严(申报名首尾空白 · `env` / `headers` 只收纯数据记录 · 两只回显描述器的回显口径)
12
78
 
13
79
  > 档位:**minor**(型面加性:`McpEntryKeyIssue` 新增两个可选格 `member?` / `fault?`;三处两层两答的判官变严是 bug 修复);导出名单不变(请求面判官与描述器自 14.0.0 起都在主 barrel:`isUsableWireName` · `describeBadWireName` · `isContentOriginWord` ·
package/README.md CHANGED
@@ -292,6 +292,15 @@ therefore shows only `zod`. Node built-ins only (`node:crypto`, `node:fs`).
292
292
  (`src/sema/settings/keyContracts.ts` self-documents the retirement) and its resolver now really imports
293
293
  `isAtLeast` from this package (`src/sema/settings/resolver.ts`). Exporting the primitive DID make it the
294
294
  single source, for this consumer.
295
+ The permission-mode **kill-switches** (`disableBypassPermissionsMode` / `disableAutoMode`) have their own appended, memberized row,
296
+ `killSwitches`: tighten-only from every layer — switching one ON is admitted at `local` (any settings file), OFF / a non-boolean / a valueless
297
+ query sits at the row floor `global`, and the merge is boolStricter (one layer's ON beats every layer's OFF). The table has no managed-only floor
298
+ for them (`permissionMode`'s seam no longer says so): a consumer that stamped one locally retires it onto this row, so its judge and its
299
+ resolver read the same row.
300
+ The ladder's words are a runtime value, the pure-data table `MIN_TRUST_WORDS` (`["local", "session", "project", "global", "managed", "task"]`): `MinTrust` (the
301
+ zod enum) is derived from it, and `trustRank` / `isAtLeast` read nothing else — apart from the JavaScript built-ins `RangeError` / `String`, the body of
302
+ `trustRank` names `MIN_TRUST_WORDS` and `isAtLeast` names `trustRank`, so a consumer that copies the pair whole (a closure with no import, only globals
303
+ registered) registers that one table instead of writing a ladder of its own. The table's order is the enum's declaration order, not the ladder.
295
304
  `MinTrust` gains the `task` word (the runtime task overlay, the loosest floor; appended, declaration order
296
305
  otherwise unchanged — the declaration order is not the ladder, `trustRank` is). The published table does not
297
306
  use `task`; the widened vocabulary is visible to consumers that hold a `Record<MinTrust, …>` / exhaustive
@@ -307,6 +316,15 @@ therefore shows only `zod`. Node built-ins only (`node:crypto`, `node:fs`).
307
316
  is the one lookup of the table (unknown source ⇒ RangeError): every function in this package that needs a layer's tier reads it
308
317
  there and none takes a tier from its caller — a layer is named by its source only. The hooks execution gate
309
318
  (`declined` ⇒ no hook from any layer runs) is contract text in `./hooks`, not layer admission.
319
+ The repository's settings files written for **another tool** (a cloned repository's `.claude/settings.json` / `.claude/settings.local.json`) are
320
+ the appended source `foreignProjectSettings` (project tier, repository-controllable, `tightenOnly`): read in the tighten direction only whatever
321
+ the workspace reading — its deny / ask rules, kill-switches switched ON and sandbox restrictions are admitted, everything else answers
322
+ `tighten_only` (appended verdict; accepting a workspace never admits it) or `below_trust_floor`. `layerReadsTightenOnly(source, workspace)` is the
323
+ one answer to "is this layer read for restrictions only" (that source always; this product's repository files while the workspace is not
324
+ `accepted`), and such a layer names what it did not read in one line: `buildLayerPartsUnreadNotice({ source, workspace, file, parts })` → code
325
+ `settings.layer_parts_unread` (`LAYER_PARTS_UNREAD_CODE`) + `<file>: read for restrictions only (<cause>) — not read: <parts>`. Upgrading a
326
+ consumer that does not read these files yet: until its reading lands it may emit that line naming every key the file holds (disclose, read
327
+ nothing) — that interim is the consumer's own release note, not a switch in this package.
310
328
  - **Auth wire contract** (`api/auth.ts`, subpath `./api/auth`): the `/api/v1/auth/*` face — RFC 8628
311
329
  device flow (code / token poll / approve consent) + rotating refresh + logout, the OAuth error envelope,
312
330
  and the pure client helpers `normalizeUserCode` / `nextPollInterval`. Shipped M1 shapes frozen.
@@ -461,11 +479,16 @@ therefore shows only `zod`. Node built-ins only (`node:crypto`, `node:fs`).
461
479
  a layer is admitted ⇔ `layerAdmission(source, workspace, "httpHookAllowlists") === "admitted"` (user / flag / managed; project / local are below the floor).
462
480
  - **MCP server list verdict** (`mcp-server-policy.ts`, 13.0.0): `mcpServerPolicyVerdict` → `McpPolicyBlock{name, list, rule, target,
463
481
  written, sources}` — judged on the expanded target, echoed only as written; the managed layer (whose `allowManagedMcpServersOnly` counts)
464
- is the source whose tier in `SETTINGS_SOURCE_CONTRACT` is `managed`.
482
+ is the source whose tier in `SETTINGS_SOURCE_CONTRACT` is `managed`; a tighten-only source contributes its deny list only (allow lists
483
+ concatenate across layers, so reading its allow list would widen the others').
465
484
  - **Request-plane MCP entry judges** (`mcp-request-entry.ts`, 14.0.0): `isUsableWireName` / `describeBadWireName` (the engine's wire-name bound
466
485
  `TOOL_WIRE_NAME_MAX_CHARS`, mirrored; a name with leading or trailing whitespace is a bad name too — never trimmed, never normalised; inner whitespace stays legal), `isContentOriginWord` / `describeBadContentOrigin`, `readMcpIdentityHeaders`, `readRequestMcpTransport`
467
486
  (hard faults `ok:false` naming the key path; soft faults dropped by the reader and listed in `lost`; `env` / `headers` are read as plain-data string records — a `Map`, a class instance or an own `__proto__` member is not one, the delivered record is a copy, and a `lost` entry for a record carries machine-readable `member` / `fault`; the two describers strip control and bidi characters and cut at 64 characters, the service's echo form) and the value-shape describers — one reading for
468
487
  the service's request legs and the shell's pre-send check. A `toolFaces` declaration stays parse-transparent here: its shape is the engine ingest side's to judge, and none of the tool-face vocabularies is mirrored in this package.
488
+ - **One-line echo strip set** (`control-and-bidi.ts`, main barrel): `CONTROL_AND_BIDI_CHARS` (C0 + DEL + the 12 `Bidi_Control=Yes` code points,
489
+ as regex character-class source text), `stripControlAndBidi(text)` and `hasControlOrBidi(text)` — the one set a caller- or repository-written string loses
490
+ before it lands in a refusal, a warning, a log line or one screen line (C1 and U+2028 / U+2029 deliberately outside it). It is an echo policy owned here,
491
+ not a mirror of the engine's bidi table: the request-plane describers read it, and so do consumers instead of keeping a class of their own.
469
492
  - **UI language** (`ui-language.ts`, 13.0.0): `semaUiLanguage` (`en` / `zh`, category `ui`, user settings) + `readUiLanguage`.
470
493
  - **Cross-session settings** (`cross-session.ts`): the two `settings.json` keys `crossSessionInbound`
471
494
  (wake/next-turn/hold/refuse — the core 7.30.0 vocabulary, `accept` retired in 6.0.0; `default` = unset) and `dialogExpiry` (60s/5m/10m/never), each with a reader that
@@ -1 +1 @@
1
- 14.1.0
1
+ 14.3.0
@@ -0,0 +1,4 @@
1
+ /** 回显的字符上限(剥完控制与双向字符之后数)。 */
2
+ export declare const CALLER_TEXT_ECHO_MAX_CHARS = 64;
3
+ /** 外来串的回显形(不带引号):剥控制与双向字符 → 超 {@link CALLER_TEXT_ECHO_MAX_CHARS} 字截断加 `…`。 */
4
+ export declare function echoCallerText(text: string): string;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * 包内私有叶子模块(不进任何导出子路径;运行期零依赖,浏览器安全):**调用方 / 仓库写的串的有界回显形** —— 全包一只。
3
+ * 剥一行回显的剥除集(`./control-and-bidi`)→ 剥完超 {@link CALLER_TEXT_ECHO_MAX_CHARS} 字截断加 `…`。拒句、告警元数据、披露行里点名一个外来串都走这里
4
+ * (串可能任意长、可能带换行与双向控制符);引号形与句子是各铸点自己的。
5
+ */
6
+ import { stripControlAndBidi } from "./control-and-bidi.js";
7
+ /** 回显的字符上限(剥完控制与双向字符之后数)。 */
8
+ export const CALLER_TEXT_ECHO_MAX_CHARS = 64;
9
+ /** 外来串的回显形(不带引号):剥控制与双向字符 → 超 {@link CALLER_TEXT_ECHO_MAX_CHARS} 字截断加 `…`。 */
10
+ export function echoCallerText(text) {
11
+ const clean = stripControlAndBidi(text);
12
+ return clean.length > CALLER_TEXT_ECHO_MAX_CHARS ? `${clean.slice(0, CALLER_TEXT_ECHO_MAX_CHARS)}…` : clean;
13
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * **一行回显的剥除集** —— 调用方 / 仓库写的串进拒句、告警、日志、屏上一行之前剥掉的那一族码点,唯一一份(住本包、主 barrel 导出)。
3
+ * 纯数据 + 纯函数,零 import,浏览器安全。
4
+ *
5
+ * ## 集合(一条)
6
+ * C0 控制符(U+0000–U+001F)+ DEL(U+007F)+ Unicode `Bidi_Control=Yes` 全集 12 个码点(U+061C、U+200E、U+200F、U+202A–U+202E、U+2066–U+2069)。
7
+ * 一个换行能在日志里伪造一行,一个 RLO 能把一行字读反 —— 回显只留可打印的一行。刻意**不含** C1(U+0080–U+009F)与 U+2028 / U+2029:
8
+ * 加宽是行为变更,要各自的判据。这张集合是**回显策略**,属主是本包:增删一个码点是本包的行为变更(走发版纪律),不跟随任何别处的词表
9
+ * (引擎的双向码点闭集、审批文本的拒集各为各的判官服务,集合与用途都不同)。
10
+ *
11
+ * ## 两只读法,同一个集合
12
+ * · {@link stripControlAndBidi} —— 剥:删掉集内每一个码点,别的字符一个不动(不截断、不 trim、不转义)。截断与引号形是各回显口自己的事。
13
+ * · {@link hasControlOrBidi} —— 检:这串里有没有集内码点。⇔ `stripControlAndBidi(s) !== s`。
14
+ * {@link CONTROL_AND_BIDI_CHARS} 是同一集合的正则字符类体(源码文本),给要拼自己更宽字符类的消费方;拼的是**更宽**的类时自己负责多出来的那部分。
15
+ */
16
+ /** 剥除集的正则字符类体(源码文本,`\uXXXX` 转义;放进 `[` `]` 即一只字符类):C0 + DEL + 12 个双向控制码点。 */
17
+ export declare const CONTROL_AND_BIDI_CHARS = "\\u0000-\\u001f\\u007f\\u061c\\u200e\\u200f\\u202a-\\u202e\\u2066-\\u2069";
18
+ /** 删掉 `text` 里剥除集({@link CONTROL_AND_BIDI_CHARS})的每一个码点;别的字符原样。 */
19
+ export declare function stripControlAndBidi(text: string): string;
20
+ /** `text` 里有没有剥除集({@link CONTROL_AND_BIDI_CHARS})的码点 —— 与 {@link stripControlAndBidi} 同一个集合:有 ⇔ 剥了会变。 */
21
+ export declare function hasControlOrBidi(text: string): boolean;
@@ -0,0 +1,29 @@
1
+ /**
2
+ * **一行回显的剥除集** —— 调用方 / 仓库写的串进拒句、告警、日志、屏上一行之前剥掉的那一族码点,唯一一份(住本包、主 barrel 导出)。
3
+ * 纯数据 + 纯函数,零 import,浏览器安全。
4
+ *
5
+ * ## 集合(一条)
6
+ * C0 控制符(U+0000–U+001F)+ DEL(U+007F)+ Unicode `Bidi_Control=Yes` 全集 12 个码点(U+061C、U+200E、U+200F、U+202A–U+202E、U+2066–U+2069)。
7
+ * 一个换行能在日志里伪造一行,一个 RLO 能把一行字读反 —— 回显只留可打印的一行。刻意**不含** C1(U+0080–U+009F)与 U+2028 / U+2029:
8
+ * 加宽是行为变更,要各自的判据。这张集合是**回显策略**,属主是本包:增删一个码点是本包的行为变更(走发版纪律),不跟随任何别处的词表
9
+ * (引擎的双向码点闭集、审批文本的拒集各为各的判官服务,集合与用途都不同)。
10
+ *
11
+ * ## 两只读法,同一个集合
12
+ * · {@link stripControlAndBidi} —— 剥:删掉集内每一个码点,别的字符一个不动(不截断、不 trim、不转义)。截断与引号形是各回显口自己的事。
13
+ * · {@link hasControlOrBidi} —— 检:这串里有没有集内码点。⇔ `stripControlAndBidi(s) !== s`。
14
+ * {@link CONTROL_AND_BIDI_CHARS} 是同一集合的正则字符类体(源码文本),给要拼自己更宽字符类的消费方;拼的是**更宽**的类时自己负责多出来的那部分。
15
+ */
16
+ /** 剥除集的正则字符类体(源码文本,`\uXXXX` 转义;放进 `[` `]` 即一只字符类):C0 + DEL + 12 个双向控制码点。 */
17
+ export const CONTROL_AND_BIDI_CHARS = "\\u0000-\\u001f\\u007f\\u061c\\u200e\\u200f\\u202a-\\u202e\\u2066-\\u2069";
18
+ /** 剥用(全局)。只在本模块里用 `String.prototype.replace` 读:`replace` 返回前把 `lastIndex` 归零,模块级常量可安全复用;不导出(全局正则的游标是共享状态)。 */
19
+ const STRIP_RE = new RegExp(`[${CONTROL_AND_BIDI_CHARS}]`, "g");
20
+ /** 检用(非全局,`test` 无游标)。 */
21
+ const DETECT_RE = new RegExp(`[${CONTROL_AND_BIDI_CHARS}]`);
22
+ /** 删掉 `text` 里剥除集({@link CONTROL_AND_BIDI_CHARS})的每一个码点;别的字符原样。 */
23
+ export function stripControlAndBidi(text) {
24
+ return text.replace(STRIP_RE, "");
25
+ }
26
+ /** `text` 里有没有剥除集({@link CONTROL_AND_BIDI_CHARS})的码点 —— 与 {@link stripControlAndBidi} 同一个集合:有 ⇔ 剥了会变。 */
27
+ export function hasControlOrBidi(text) {
28
+ return DETECT_RE.test(text);
29
+ }
@@ -24,8 +24,12 @@
24
24
  * bad — a contract field whose value is wrong, or, for a piece rejected because it holds a guard sign, the member that holds the sign (an unknown
25
25
  * field the contract would strip; a contract field carrying more than its kind, such as `headers` or `input`; `hooks` for an entry that is itself
26
26
  * matcher-shaped). Absent only when the piece is not an object — and for the pieces above matchers, which have no fields to name. One piece, one
27
- * `field`: the first wrong contract field, or the first sign in document order; a sign on the piece itself decides before a wrong contract field
28
- * of the same piece does (it is why the piece is rejected rather than dropped).
27
+ * `field`: the first wrong contract field, or — when the piece holds signs — the piece's own sign first (its own guard-event key; for an entry,
28
+ * the entry itself being matcher-shaped, which names `hooks`), then the first nested sign in document order (the first member whose subtree holds
29
+ * one). So an own guard key written AFTER a member that nests a sign is still the one reported. A matcher's well-formed entry list (`hooks` holding
30
+ * an array) is not searched at the matcher level: the signs in it belong to the entries, each its own piece — so `{ matcher: 5, hooks: [<entry
31
+ * with a sign>] }` reports the matcher's wrong `matcher` field. A sign of the piece decides before a wrong contract field of the same piece does
32
+ * (it is why the piece is rejected rather than dropped).
29
33
  *
30
34
  * CC 2.1.296 DIVERGENCES (each in the stricter direction — a document CC would load may be rejected here, never the reverse): listed once,
31
35
  * in `docs/CC-DIVERGENCE.md` (section "hooks classifier", H1–H7) — the whole-subtree guard-sign scan is the first of them.
@@ -24,8 +24,12 @@
24
24
  * bad — a contract field whose value is wrong, or, for a piece rejected because it holds a guard sign, the member that holds the sign (an unknown
25
25
  * field the contract would strip; a contract field carrying more than its kind, such as `headers` or `input`; `hooks` for an entry that is itself
26
26
  * matcher-shaped). Absent only when the piece is not an object — and for the pieces above matchers, which have no fields to name. One piece, one
27
- * `field`: the first wrong contract field, or the first sign in document order; a sign on the piece itself decides before a wrong contract field
28
- * of the same piece does (it is why the piece is rejected rather than dropped).
27
+ * `field`: the first wrong contract field, or — when the piece holds signs — the piece's own sign first (its own guard-event key; for an entry,
28
+ * the entry itself being matcher-shaped, which names `hooks`), then the first nested sign in document order (the first member whose subtree holds
29
+ * one). So an own guard key written AFTER a member that nests a sign is still the one reported. A matcher's well-formed entry list (`hooks` holding
30
+ * an array) is not searched at the matcher level: the signs in it belong to the entries, each its own piece — so `{ matcher: 5, hooks: [<entry
31
+ * with a sign>] }` reports the matcher's wrong `matcher` field. A sign of the piece decides before a wrong contract field of the same piece does
32
+ * (it is why the piece is rejected rather than dropped).
29
33
  *
30
34
  * CC 2.1.296 DIVERGENCES (each in the stricter direction — a document CC would load may be rejected here, never the reverse): listed once,
31
35
  * in `docs/CC-DIVERGENCE.md` (section "hooks classifier", H1–H7) — the whole-subtree guard-sign scan is the first of them.
@@ -180,7 +184,9 @@ function hookLooksLikeMatcher(value) {
180
184
  return hookIsRecord(inner) && (Object.hasOwn(value, "matcher") || typeof inner.type === "string");
181
185
  }
182
186
  /**
183
- * The coordinate of the first guard sign (document order) in `root`'s subtree, or undefined. `place` is where `root` sits: `matcherList` =
187
+ * The coordinate of the first guard sign in `root`'s subtree, or undefined: each object answers for itself before its members are searched (its own
188
+ * guard-event key, then — outside a matcher's legal place — itself being matcher-shaped), and its members are searched in document order, depth first.
189
+ * `place` is where `root` sits: `matcherList` =
184
190
  * the array under an event name (its elements are in a matcher's legal place), `matcher` = one such element, `other` = anywhere else.
185
191
  * A guard-event member's coordinate ends with its key; a matcher-shaped object's is the object's own. Iterative, whole subtree, each object
186
192
  * visited once per place (cycles terminate).
@@ -292,7 +298,8 @@ function hookMatcherRead(matcher, at, guardEvent, reject, drop) {
292
298
  }
293
299
  const entries = Object.hasOwn(matcher, "hooks") ? matcher.hooks : undefined;
294
300
  // a sign at the matcher level: a guard-event key on the matcher itself, or anywhere in its fields other than a well-formed entry list;
295
- // the member that holds the first one (document order — the order hookGuardSignIn reports) is the rejection's `field`
301
+ // the rejection's `field` is the matcher's own guard-event key if it has one, else the first member (document order) whose subtree holds a sign —
302
+ // the order hookGuardSignIn reports
296
303
  const signField = hookGuardKeyOf(matcher) ??
297
304
  Object.keys(matcher).find((key) => !(key === "hooks" && Array.isArray(entries)) && hookGuardSignIn(matcher[key], [...at, key], "other") !== undefined);
298
305
  if (signField !== undefined) {
package/dist/index.d.ts CHANGED
@@ -36,4 +36,5 @@ export * from "./url-pattern.js";
36
36
  export * from "./http-hook-allowlists.js";
37
37
  export * from "./mcp-server-policy.js";
38
38
  export * from "./ui-language.js";
39
+ export * from "./control-and-bidi.js";
39
40
  export * from "./mcp-request-entry.js";
package/dist/index.js CHANGED
@@ -36,6 +36,7 @@ export * from "./url-pattern.js"; // 13.0.0 (S-1597): THE URL-list pattern judge
36
36
  export * from "./http-hook-allowlists.js"; // 13.0.0 (U3): allowedHttpHookUrls / httpHookAllowedEnvVars — one layer reader + httpHookUrlVerdict / httpHookEnvVarsAllowed (admitted layers INTERSECT; SAFETY_MERGE_SPEC.httpHookAllowlists), pure/browser-safe
37
37
  export * from "./mcp-server-policy.js"; // 13.0.0 (U6): mcpServerPolicyVerdict → McpPolicyBlock{name, list, rule, target, written, sources} (structure, no sentence; target judged expanded, echoed only as written), pure/browser-safe
38
38
  export * from "./ui-language.js"; // 13.0.0 (S-1592): the UI-language settings fragment (semaUiLanguage, en | zh, category ui, user settings) + readUiLanguage, pure/browser-safe
39
+ export * from "./control-and-bidi.js"; // the one-line echo strip set (C0 + DEL + the 12 Bidi_Control code points) — CONTROL_AND_BIDI_CHARS (the class, as regex source text) + stripControlAndBidi / hasControlOrBidi, pure/browser-safe
39
40
  export * from "./mcp-request-entry.js"; // 14.0.0: the request-plane MCP entry judges (isUsableWireName / isContentOriginWord / readMcpIdentityHeaders / readRequestMcpTransport + the value-shape describers) — one reading for the service legs and the shell pre-check, pure/browser-safe
40
41
  // NOTE: the Node-fs-backed helpers (FileConfigStore + the file editor) are NOT in this barrel — they pull
41
42
  // `node:fs`/`node:path` and would break a browser/client bundle that only wants the contract (e.g.
@@ -1,29 +1,3 @@
1
- /**
2
- * 请求面 MCP 条目(一条任务提交体 `mcpServers[i]`)的形判官 —— 14.0.0 起住本包:一只读法,消费方的请求腿 / 中心腿 / 壳的发送前预检都 import 这一份。
3
- * 纯函数、浏览器安全(唯一的值依赖是本包自己的内容出处词表)。
4
- *
5
- * ## 律(一条)
6
- * **一条请求面 MCP 条目的每一格「能不能用」只有一只判官。** 此前这些判官是消费方私有的:壳拿不到,只能在发送前披露「判不准、交给服务端」,
7
- * 或者自己抄一份 —— 两份判据就是将来漂开的那一份。现在判官在契约包,服务端与壳读同一份,同一条条目在两处同受理 / 同拒、同一个键路径、同一句原因。
8
- *
9
- * ## 语义是搬来的,不是重写的
10
- * 判据与原因句逐字等于消费方请求腿的那一份(钉:`test/mcp-request-entry-14.0.0.test.ts` 逐例搬了消费方既有钉的夹具与期望)。
11
- * 本模块只答「这一格的形」;拒绝的整句(条目下标、条目名的回显清洗、收尾句)与「拒还是降级」的后果仍是消费方按自己的腿定的 —— 判据一份,后果按腿分。
12
- * 与最初搬来时相比有三处收严,都是收到与消费方请求腿同严、不是新语义,消费方改 import 这一份之后不再各留一份旧判据:申报名判官另判**首尾空白**;
13
- * `env` / `headers` 只收**纯数据**的「名 → 串」记录(Map / 类实例 / 自有 `__proto__` 成员不收,交出的是副本);两只回显描述器用消费方的回显口径(剥控制与双向字符、截 64 字、引号形)。
14
- *
15
- * ## 依赖的引擎事实(镜像 + 漂移门)
16
- * · 申报名的线长界 {@link TOOL_WIRE_NAME_MAX_CHARS} = 引擎同名常量(值级对拍:上游漂移门 ⑧);
17
- * · 请求面传输形 {@link RequestMcpTransport} = 引擎 MCP 条目的传输形,http 形多一个本包承载的 `assertionHeader`(编译期双向对拍:门 ⑧);
18
- * · 内容出处词表 = {@link MCP_CONTENT_ORIGINS}(已有的镜像,门 ⑥);
19
- * · 回显清洗类的双向半场 = 引擎 `BIDI_CONTROL_CHARS`(镜像,逐码点全扫对拍:门 ⑨;本包运行期不依赖引擎);
20
- * · url 前缀守卫 = `./url-shape` 的 `startsWithHttpScheme`(本包判 URL 的正则只在那一个模块)。
21
- *
22
- * ## 与配置面的关系(如实)
23
- * 配置面的 MCP 条目(`McpServerSpec`,部署声明:凭据只许 env-NAME 引用)与请求面(提交体:可带字面凭据 `env` / `headers`)是两份契约,本模块只管后者。
24
- * 两份契约对「名」与「url」今天给的判据不同(配置面:名按字符集、url 按 `NON_SECRET_URL`;请求面:名按长度界、url 按 http(s) 前缀),本版刻意不动
25
- * (语义逐字搬),是一项已知待收的分歧。
26
- */
27
1
  import { type McpContentOrigin } from "./types.js";
28
2
  /** 值的**种类**(不回显值本身)。 */
29
3
  export declare function describeValueShape(v: unknown): string;
@@ -16,7 +16,6 @@
16
16
  * · 申报名的线长界 {@link TOOL_WIRE_NAME_MAX_CHARS} = 引擎同名常量(值级对拍:上游漂移门 ⑧);
17
17
  * · 请求面传输形 {@link RequestMcpTransport} = 引擎 MCP 条目的传输形,http 形多一个本包承载的 `assertionHeader`(编译期双向对拍:门 ⑧);
18
18
  * · 内容出处词表 = {@link MCP_CONTENT_ORIGINS}(已有的镜像,门 ⑥);
19
- * · 回显清洗类的双向半场 = 引擎 `BIDI_CONTROL_CHARS`(镜像,逐码点全扫对拍:门 ⑨;本包运行期不依赖引擎);
20
19
  * · url 前缀守卫 = `./url-shape` 的 `startsWithHttpScheme`(本包判 URL 的正则只在那一个模块)。
21
20
  *
22
21
  * ## 与配置面的关系(如实)
@@ -24,6 +23,7 @@
24
23
  * 两份契约对「名」与「url」今天给的判据不同(配置面:名按字符集、url 按 `NON_SECRET_URL`;请求面:名按长度界、url 按 http(s) 前缀),本版刻意不动
25
24
  * (语义逐字搬),是一项已知待收的分歧。
26
25
  */
26
+ import { echoCallerText } from "./caller-text-echo.js";
27
27
  import { MCP_CONTENT_ORIGINS } from "./types.js";
28
28
  import { startsWithHttpScheme } from "./url-shape.js";
29
29
  // ── 值的种类描述(拒句只说「你写的是什么形」,不回显值:值可能带凭据、可能任意长)──────────────────────────────────────
@@ -65,18 +65,6 @@ function isRecord(v) {
65
65
  return v !== null && typeof v === "object" && !Array.isArray(v);
66
66
  }
67
67
  // ── 调用方写的串的回显口径 ──────────────────────────────────────────────────────────────────────────
68
- /**
69
- * 回显清洗类:C0 控制符 + DEL + 双向控制字符(Unicode `Bidi_Control=Yes` 全集 12 个码点 —— 一个 RLO 能把一行字读反,一个换行能伪造一行日志)。清洗面刻意不含 C1 与
70
- * U+2028 / U+2029:加宽是行为变更,要各自的判据。双向这一半的属主是引擎(`BIDI_CONTROL_CHARS`),这里是镜像,由上游漂移门逐码点全扫对拍。
71
- */
72
- const CONTROL_AND_BIDI_STRIP_RE = /[\u0000-\u001f\u007f\u061c\u200e\u200f\u202a-\u202e\u2066-\u2069]/g;
73
- /** 回显的字符上限(剥完控制与双向字符之后数)。 */
74
- const CALLER_TEXT_ECHO_MAX_CHARS = 64;
75
- /** 调用方写的串的回显形(不带引号):剥控制与双向字符 → 超 {@link CALLER_TEXT_ECHO_MAX_CHARS} 字截断加 `…`。拒句 / 告警元数据里的有界回显,串可能任意长、可能带换行。 */
76
- function echoCallerText(text) {
77
- const clean = text.replace(CONTROL_AND_BIDI_STRIP_RE, "");
78
- return clean.length > CALLER_TEXT_ECHO_MAX_CHARS ? `${clean.slice(0, CALLER_TEXT_ECHO_MAX_CHARS)}…` : clean;
79
- }
80
68
  /** 句子里点名一个值的写法:{@link echoCallerText} 外加一对双引号;内部的引号与反斜杠逐字(不转义)。 */
81
69
  function quotedCallerText(text) {
82
70
  return `"${echoCallerText(text)}"`;
@@ -5,7 +5,8 @@
5
5
  *
6
6
  * 判法(锚 = cli main ceff8bc6 `mcpServerPolicyBlock`,它与 CC 2.1.296 名单判法逐支一致;URL 条目按本包 `url-pattern` 两只出口判):
7
7
  * - 拒绝名单**绝对优先**,各层都算(用户可以替自己拒):名字条目 → 命令条目(只对 stdio,整数组逐项相等)→ URL 条目(只对远端,拒绝出口)。
8
- * - 允许名单:各层写的拼成一张(CC「Arrays merge across settings sources」);托管层(源表 `SETTINGS_SOURCE_CONTRACT` 档 = `managed` 的源,
8
+ * - 允许名单:各层写的拼成一张(CC「Arrays merge across settings sources」);拼接会**放宽**别层的名单,所以只收紧的源(源表 `tightenOnly`,
9
+ * 仓库里写给另一个工具的设置文件)的允许名单不读 —— 它只贡献拒绝名单;托管层(源表 `SETTINGS_SOURCE_CONTRACT` 档 = `managed` 的源,
9
10
  * 经 `settingsSourceContract` 查 —— 调用方不给档)写了 `allowManagedMcpServersOnly: true` ⇒ 只读托管层那一张(别层写的这个开关不生效)。没有一层写 ⇒ 不限;空 ⇒ 一台都不许;
10
11
  * stdio 且名单里有命令条目 ⇒ 必须命中其一;远端且名单里有 URL 条目 ⇒ 必须命中其一(放行出口);否则按名字。
11
12
  * - SDK 型等「不由宿主连接」的条目是否豁免,归调用方(cli 的过滤口豁免、盘上加载器不豁免 —— 那是接线,不是判法)。
@@ -5,7 +5,8 @@
5
5
  *
6
6
  * 判法(锚 = cli main ceff8bc6 `mcpServerPolicyBlock`,它与 CC 2.1.296 名单判法逐支一致;URL 条目按本包 `url-pattern` 两只出口判):
7
7
  * - 拒绝名单**绝对优先**,各层都算(用户可以替自己拒):名字条目 → 命令条目(只对 stdio,整数组逐项相等)→ URL 条目(只对远端,拒绝出口)。
8
- * - 允许名单:各层写的拼成一张(CC「Arrays merge across settings sources」);托管层(源表 `SETTINGS_SOURCE_CONTRACT` 档 = `managed` 的源,
8
+ * - 允许名单:各层写的拼成一张(CC「Arrays merge across settings sources」);拼接会**放宽**别层的名单,所以只收紧的源(源表 `tightenOnly`,
9
+ * 仓库里写给另一个工具的设置文件)的允许名单不读 —— 它只贡献拒绝名单;托管层(源表 `SETTINGS_SOURCE_CONTRACT` 档 = `managed` 的源,
9
10
  * 经 `settingsSourceContract` 查 —— 调用方不给档)写了 `allowManagedMcpServersOnly: true` ⇒ 只读托管层那一张(别层写的这个开关不生效)。没有一层写 ⇒ 不限;空 ⇒ 一台都不许;
10
11
  * stdio 且名单里有命令条目 ⇒ 必须命中其一;远端且名单里有 URL 条目 ⇒ 必须命中其一(放行出口);否则按名字。
11
12
  * - SDK 型等「不由宿主连接」的条目是否豁免,归调用方(cli 的过滤口豁免、盘上加载器不豁免 —— 那是接线,不是判法)。
@@ -60,8 +61,9 @@ function writtenOf(server) {
60
61
  }
61
62
  /** 名单对这一台的判决:放行 ⇒ `undefined`;拦下 ⇒ 原因(判法见文件头)。 */
62
63
  export function mcpServerPolicyVerdict(server, layers) {
63
- // 每一层先过源表(闭集外的源在这里响亮抛,哪怕那一层什么都没写);托管 ⇔ 源表档 = managed。
64
+ // 每一层先过源表(闭集外的源在这里响亮抛,哪怕那一层什么都没写);托管 ⇔ 源表档 = managed;只收紧的源不进允许名单(拼接是放宽)。
64
65
  const managed = new Set(layers.filter((l) => settingsSourceContract(l.source).trust === "managed"));
66
+ const tightenOnly = new Set(layers.filter((l) => settingsSourceContract(l.source).tightenOnly === true));
65
67
  const block = (list, rule, sources) => ({
66
68
  name: server.name,
67
69
  list,
@@ -76,7 +78,7 @@ export function mcpServerPolicyVerdict(server, layers) {
76
78
  return block("deniedMcpServers", rule, sources);
77
79
  }
78
80
  const managedOnly = layers.some((l) => managed.has(l) && l.allowManagedMcpServersOnly === true);
79
- const allowLayers = layers.filter((l) => l.allowedMcpServers !== undefined && (!managedOnly || managed.has(l)));
81
+ const allowLayers = layers.filter((l) => l.allowedMcpServers !== undefined && !tightenOnly.has(l) && (!managedOnly || managed.has(l)));
80
82
  if (allowLayers.length === 0)
81
83
  return undefined;
82
84
  const miss = allowMiss(allowLayers.flatMap((l) => l.allowedMcpServers), server);
@@ -60,10 +60,19 @@ export declare const MergeShape: z.ZodEnum<{
60
60
  overrideByName: "overrideByName";
61
61
  }>;
62
62
  export type MergeShape = z.infer<typeof MergeShape>;
63
+ /** The trust-tier VOCABULARY as a RUNTIME VALUE — the ONE source of the ladder's words, in the same form as
64
+ * `AUTONOMY_MODES`: {@link MinTrust} (the zod enum) and its type are DERIVED from this table, and {@link trustRank} /
65
+ * {@link isAtLeast} read the words from it (their RangeError sentence lists them). Pure data, no zod: a consumer that
66
+ * copies the ladder's judge functions whole — a closure with no import, only globals registered — registers this
67
+ * table as the one global the closure needs, instead of writing a ladder of its own.
68
+ * ⚠️ The DECLARATION ORDER is NOT the rank (it is the 1.1.0 order with `task` appended — kept byte-stable because
69
+ * reordering is breaking); never index it as a ladder — use {@link trustRank}. */
70
+ export declare const MIN_TRUST_WORDS: readonly ["local", "session", "project", "global", "managed", "task"];
63
71
  /** The trust-tier VOCABULARY a category — or, since 1.1.0, one of its members — names as its admission floor
64
72
  * (the MINIMUM trust a layer/principal must have to set it). The RANK is v2-design §1:29,
65
73
  * `managed 6 > global 5 > project 4 > local 3 > session 2 > task 1`, published as {@link trustRank}; compare with
66
74
  * {@link isAtLeast}. `task` (1.2.0) is the runtime task overlay — the lowest trust, the loosest floor.
75
+ * Derived from {@link MIN_TRUST_WORDS} (the words and their declaration order are unchanged).
67
76
  * ⚠️ This enum's DECLARATION ORDER is NOT the rank (it is the 1.1.0 order with `task` appended — kept byte-stable
68
77
  * because reordering is breaking); never index it as a ladder — use {@link trustRank}. */
69
78
  export declare const MinTrust: z.ZodEnum<{
@@ -80,11 +89,14 @@ export type MinTrust = z.infer<typeof MinTrust>;
80
89
  * The numbers themselves are contract (a consumer may persist or log them), not merely their order.
81
90
  * Exhaustive over {@link MinTrust}: a word added to the enum without a rank is a COMPILE error here; a word that
82
91
  * is not in the enum at runtime (a JavaScript / deserialized caller) is a loud RangeError — never `undefined` or
83
- * `NaN`, which would turn every comparison into a silent `false`. */
92
+ * `NaN`, which would turn every comparison into a silent `false`.
93
+ * Self-contained: apart from the JavaScript built-ins `RangeError` / `String`, the body names exactly one
94
+ * free identifier, the pure-data {@link MIN_TRUST_WORDS} — no zod — so a consumer may copy it whole. */
84
95
  export declare function trustRank(layer: MinTrust): number;
85
96
  /** `trustRank(layer) ≥ trustRank(minTrust)` — "is a source at `layer` trusted enough to set a key / member whose
86
97
  * floor is `minTrust`?" (1.2.0). The comparison a resolver's admission check is; reflexive (a floor admits its
87
- * own tier). Both arguments are validated by {@link trustRank} (unknown word ⇒ RangeError). */
98
+ * own tier). Both arguments are validated by {@link trustRank} (unknown word ⇒ RangeError). Its body names one free
99
+ * identifier, {@link trustRank} — so the pair copies whole with {@link MIN_TRUST_WORDS} as the only registered global. */
88
100
  export declare function isAtLeast(layer: MinTrust, minTrust: MinTrust): boolean;
89
101
  /** Which way a MEMBER moves the engine's unattended surface (1.1.0). `loosen` = admits an action without a human
90
102
  * that would otherwise ask/deny — a privilege, so it needs trust; `tighten` = only ever narrows — restricting is
@@ -296,7 +308,7 @@ export declare const SAFETY_MERGE_SPEC: {
296
308
  };
297
309
  readonly enforcement: "gate";
298
310
  readonly fail: "closed";
299
- readonly seam: "derive → toolPolicy / onAsk / handsReadOnly (§6.4); bypass = launch-flag only + MANAGED kill-switch";
311
+ readonly seam: "derive → toolPolicy / onAsk / handsReadOnly (§6.4); bypass = launch-flag only; the kill-switches are the killSwitches row";
300
312
  };
301
313
  readonly handsReadOnly: {
302
314
  readonly mergeShape: "replace";
@@ -534,6 +546,35 @@ export declare const SAFETY_MERGE_SPEC: {
534
546
  readonly fail: "closed";
535
547
  readonly seam: "http hook URL / header-env allowlists (httpHookUrlVerdict / httpHookEnvVarsAllowed)";
536
548
  };
549
+ readonly killSwitches: {
550
+ readonly mergeShape: "replace";
551
+ readonly minTrust: "global";
552
+ readonly tighten: {
553
+ readonly rule: "boolStricter";
554
+ readonly strict: true;
555
+ };
556
+ readonly enforcement: "gate";
557
+ readonly fail: "closed";
558
+ readonly seam: "permission-mode kill-switches (a switch ON removes bypassPermissions / auto from every layer's mode choice)";
559
+ readonly members: {
560
+ readonly disableBypassPermissionsMode: {
561
+ readonly direction: "tighten";
562
+ readonly minTrust: "global";
563
+ readonly boolPole: {
564
+ readonly strict: true;
565
+ readonly minTrust: "local";
566
+ };
567
+ };
568
+ readonly disableAutoMode: {
569
+ readonly direction: "tighten";
570
+ readonly minTrust: "global";
571
+ readonly boolPole: {
572
+ readonly strict: true;
573
+ readonly minTrust: "local";
574
+ };
575
+ };
576
+ };
577
+ };
537
578
  };
538
579
  export type SafetyMergeCategory = keyof typeof SAFETY_MERGE_SPEC;
539
580
  /** The members a category declares, as a CLOSED SET TYPE: `"allow" | "deny" | "ask"` for `permissions`, `never` for
@@ -53,19 +53,30 @@
53
53
  import { z } from "zod";
54
54
  /** How values from different layers/principals combine for a category. */
55
55
  export const MergeShape = z.enum(["denyFirst", "replace", "runAll", "overrideByName", "concat"]);
56
+ /** The trust-tier VOCABULARY as a RUNTIME VALUE — the ONE source of the ladder's words, in the same form as
57
+ * `AUTONOMY_MODES`: {@link MinTrust} (the zod enum) and its type are DERIVED from this table, and {@link trustRank} /
58
+ * {@link isAtLeast} read the words from it (their RangeError sentence lists them). Pure data, no zod: a consumer that
59
+ * copies the ladder's judge functions whole — a closure with no import, only globals registered — registers this
60
+ * table as the one global the closure needs, instead of writing a ladder of its own.
61
+ * ⚠️ The DECLARATION ORDER is NOT the rank (it is the 1.1.0 order with `task` appended — kept byte-stable because
62
+ * reordering is breaking); never index it as a ladder — use {@link trustRank}. */
63
+ export const MIN_TRUST_WORDS = ["local", "session", "project", "global", "managed", "task"];
56
64
  /** The trust-tier VOCABULARY a category — or, since 1.1.0, one of its members — names as its admission floor
57
65
  * (the MINIMUM trust a layer/principal must have to set it). The RANK is v2-design §1:29,
58
66
  * `managed 6 > global 5 > project 4 > local 3 > session 2 > task 1`, published as {@link trustRank}; compare with
59
67
  * {@link isAtLeast}. `task` (1.2.0) is the runtime task overlay — the lowest trust, the loosest floor.
68
+ * Derived from {@link MIN_TRUST_WORDS} (the words and their declaration order are unchanged).
60
69
  * ⚠️ This enum's DECLARATION ORDER is NOT the rank (it is the 1.1.0 order with `task` appended — kept byte-stable
61
70
  * because reordering is breaking); never index it as a ladder — use {@link trustRank}. */
62
- export const MinTrust = z.enum(["local", "session", "project", "global", "managed", "task"]);
71
+ export const MinTrust = z.enum(MIN_TRUST_WORDS);
63
72
  /** The trust RANK of a floor / layer word — v2-design §1:29 verbatim (`MANAGED 6 > GLOBAL 5 > PROJECT 4 > LOCAL 3 >
64
73
  * SESSION 2 > TASK 1`), the ONE published ladder every resolver compares against (1.2.0). Higher = more trusted.
65
74
  * The numbers themselves are contract (a consumer may persist or log them), not merely their order.
66
75
  * Exhaustive over {@link MinTrust}: a word added to the enum without a rank is a COMPILE error here; a word that
67
76
  * is not in the enum at runtime (a JavaScript / deserialized caller) is a loud RangeError — never `undefined` or
68
- * `NaN`, which would turn every comparison into a silent `false`. */
77
+ * `NaN`, which would turn every comparison into a silent `false`.
78
+ * Self-contained: apart from the JavaScript built-ins `RangeError` / `String`, the body names exactly one
79
+ * free identifier, the pure-data {@link MIN_TRUST_WORDS} — no zod — so a consumer may copy it whole. */
69
80
  export function trustRank(layer) {
70
81
  switch (layer) {
71
82
  case "managed": return 6;
@@ -76,13 +87,14 @@ export function trustRank(layer) {
76
87
  case "task": return 1;
77
88
  default: {
78
89
  const unknown = layer;
79
- throw new RangeError(`MinTrust: unknown trust word '${String(unknown)}' (words: ${MinTrust.options.join(", ")})`);
90
+ throw new RangeError(`MinTrust: unknown trust word '${String(unknown)}' (words: ${MIN_TRUST_WORDS.join(", ")})`);
80
91
  }
81
92
  }
82
93
  }
83
94
  /** `trustRank(layer) ≥ trustRank(minTrust)` — "is a source at `layer` trusted enough to set a key / member whose
84
95
  * floor is `minTrust`?" (1.2.0). The comparison a resolver's admission check is; reflexive (a floor admits its
85
- * own tier). Both arguments are validated by {@link trustRank} (unknown word ⇒ RangeError). */
96
+ * own tier). Both arguments are validated by {@link trustRank} (unknown word ⇒ RangeError). Its body names one free
97
+ * identifier, {@link trustRank} — so the pair copies whole with {@link MIN_TRUST_WORDS} as the only registered global. */
86
98
  export function isAtLeast(layer, minTrust) {
87
99
  return trustRank(layer) >= trustRank(minTrust);
88
100
  }
@@ -166,7 +178,7 @@ export const SAFETY_MERGE_SPEC = {
166
178
  // releases every releasable ask; auto hands them to the classifier, which may auto-release ANY benign ask (⊇
167
179
  // acceptEdits' edits-only auto-allow); dontAsk = every would-ask → deny (allow rules still pass, tighter than default,
168
180
  // looser than plan's handsReadOnly).
169
- permissionMode: { mergeShape: "replace", minTrust: "global", tighten: { rule: "enumRank", order: ["bypassPermissions", "auto", "acceptEdits", "default", "dontAsk", "plan"] }, enforcement: "gate", fail: "closed", seam: "derive → toolPolicy / onAsk / handsReadOnly (§6.4); bypass = launch-flag only + MANAGED kill-switch" },
181
+ permissionMode: { mergeShape: "replace", minTrust: "global", tighten: { rule: "enumRank", order: ["bypassPermissions", "auto", "acceptEdits", "default", "dontAsk", "plan"] }, enforcement: "gate", fail: "closed", seam: "derive → toolPolicy / onAsk / handsReadOnly (§6.4); bypass = launch-flag only; the kill-switches are the killSwitches row" },
170
182
  handsReadOnly: { mergeShape: "replace", minTrust: "global", tighten: { rule: "boolStricter", strict: true }, enforcement: "gate", fail: "closed", seam: "handsReadOnly" },
171
183
  // The engine's shell floor has ONE word, `"always"` (the operator mandates a per-call confirmation for every shell command);
172
184
  // the earlier `off` / `classify` are refused at the engine's door, so a layer carrying either is out of vocabulary here too.
@@ -239,6 +251,24 @@ export const SAFETY_MERGE_SPEC = {
239
251
  // tightening word with a loosening consequence, the same boundary as sandboxNetwork.failIfUnavailable. Read and judged by
240
252
  // ./http-hook-allowlists (`httpHookUrlVerdict` / `httpHookEnvVarsAllowed`); not `sandboxNetwork` (that row is enforcement:"gate").
241
253
  httpHookAllowlists: { mergeShape: "denyFirst", minTrust: "global", tighten: { rule: "setNarrower" }, enforcement: "resolver", fail: "closed", seam: "http hook URL / header-env allowlists (httpHookUrlVerdict / httpHookEnvVarsAllowed)" },
254
+ // The permission-mode KILL-SWITCHES — `disableBypassPermissionsMode` / `disableAutoMode` (the switch ON removes that mode from every layer's
255
+ // choice). TIGHTEN-ONLY FROM EVERY LAYER: turning a switch on only narrows, so any settings-file layer may do it (the strict pole is
256
+ // tighten@"local", the lowest settings-FILE tier — the permissions.deny / ask law); nothing can turn it back off, because the merge is
257
+ // boolStricter(strict: true) — one layer's ON beats every layer's OFF. CC 2.1.296's restrictive-key table has the same shape
258
+ // ({path: ["permissions", "disableBypassPermissionsMode"], restrictive: "disable"}; CC spells ON "disable", consumers read it as `true`).
259
+ // There is no managed-only floor for them: before this row the table had none, a consumer stamped "managed" on them locally, and its judge
260
+ // (every source counts) and its resolver ("may only be set by managed-level configuration") said opposite things.
261
+ // VALUE-directional rows (the sandboxNetwork boolean law): the ON pole at "local"; OFF, a non-boolean or a valueless query at the row floor
262
+ // "global" = the category floor (a pre-members / pre-boolPole consumer withholds BOTH poles from the repository layers — fail-closed).
263
+ // A switch this row does not declare stays under the category floor "global", whole-key (the 1.1.0 header law).
264
+ killSwitches: {
265
+ mergeShape: "replace", minTrust: "global", tighten: { rule: "boolStricter", strict: true }, enforcement: "gate", fail: "closed",
266
+ seam: "permission-mode kill-switches (a switch ON removes bypassPermissions / auto from every layer's mode choice)",
267
+ members: {
268
+ disableBypassPermissionsMode: { direction: "tighten", minTrust: "global", boolPole: { strict: true, minTrust: "local" } },
269
+ disableAutoMode: { direction: "tighten", minTrust: "global", boolPole: { strict: true, minTrust: "local" } },
270
+ },
271
+ },
242
272
  };
243
273
  /** Own-property table lookup. The category union is compile-time only; a JavaScript or deserialized caller can
244
274
  * hand in `"__proto__"` / `"constructor"` / `"toString"`, which a bare index resolves through Object.prototype
@@ -1,41 +1,18 @@
1
- /**
2
- * Settings SOURCES and the ONE layer-admission function.
3
- *
4
- * `SAFETY_MERGE_SPEC` answers "how trusted must a layer be to set this category / member, and which way does it move the unattended
5
- * surface" (floor + direction); `trustRank` / `isAtLeast` are the ladder. What was missing is the LAYER side: which settings sources
6
- * there are, which trust tier each one carries, which of them a repository controls, and what an untrusted workspace does to them —
7
- * every host kept its own table for that. This file is that table, plus the one function a resolver asks per layer × key.
8
- *
9
- * THE RULE (one, no per-key special case): a repository-controllable source (`projectSettings` / `localSettings` — a clone brings or the
10
- * repository's tooling writes them) in a workspace whose trust reading is not `accepted` admits only the members whose published
11
- * direction is `"tighten"`; everything it may otherwise set is `trust_withheld`. In every case the source's tier must reach the floor
12
- * (`isAtLeast(tier, floor)`), checked first — a write below its floor is `below_trust_floor` whether or not the workspace is trusted, which
13
- * also keeps the loose pole of a value-directional (`boolPole`) member out of an untrusted layer. The `direction` data already says which
14
- * keys an untrusted repository may still set (restricting needs no trust); there is no per-row workspace column (that would copy a
15
- * layer fact into every category row).
16
- *
17
- * CC 2.1.296 (the reference): trust is one per-session boolean (a non-interactive session counts as trusted, `cli296.js:238351`); keys split
18
- * into the operator tier `policy / flag / user` and the repository-controllable tier `project / local` ("projectSettings and
19
- * localSettings are repo-controllable", `:237757`); merge order user < project < local < flag < policy (`:237339`).
20
- * The local layer sits in the project tier here too: a `.local` settings file is gitignored, so a clone cannot bring one, and whoever can
21
- * write it can write the project file next to it — "local below project" was never a written rule (it arrived with the first import of the
22
- * table) and holds no threat model. The category rows (`hooks` / `env` / `mcpServers` / `rulesInstructions` at `minTrust: "project"`) and
23
- * the ladder numbers are untouched: the change is on the source's tier, not on any row.
24
- *
25
- * NOT here (each has its own place): the hooks EXECUTION gate (`declined` ⇒ no hook from any layer runs) is contract text in `./hooks`'
26
- * header and executes in the client core; the workspace trust RECORD (where "this folder was accepted" is stored) is host state.
27
- */
28
1
  import { type MinTrust, type SafetyMergeCategory, type SafetyMergeMember } from "./safety-merge-spec.js";
29
2
  /** The settings sources, in MERGE order (a later source overrides an earlier one where a category's merge shape lets it):
30
3
  * user < project < local < flag < policy. `flagSettings` = settings handed in at launch (`--settings <file|json>`, an SDK embedder's inline
31
- * settings); `policySettings` = managed. */
32
- export declare const SETTINGS_SOURCES: readonly ["userSettings", "projectSettings", "localSettings", "flagSettings", "policySettings"];
4
+ * settings); `policySettings` = managed. `foreignProjectSettings` (appended) = the repository's settings files written for another tool; it
5
+ * contributes tightening writes only, and every tighten rule merges by "the stricter wins" (order-independent), so its place in the order
6
+ * changes no merged value. */
7
+ export declare const SETTINGS_SOURCES: readonly ["userSettings", "projectSettings", "localSettings", "flagSettings", "policySettings", "foreignProjectSettings"];
33
8
  export type SettingsSource = (typeof SETTINGS_SOURCES)[number];
34
- /** One source's admission facts: its trust tier (a {@link MinTrust} word, compared with `isAtLeast` against a floor) and whether a
35
- * repository controls it (⇒ subject to the workspace trust reading). */
9
+ /** One source's admission facts: its trust tier (a {@link MinTrust} word, compared with `isAtLeast` against a floor), whether a
10
+ * repository controls it (⇒ subject to the workspace trust reading), and whether it is read in the tighten direction only whatever that
11
+ * reading is (`tightenOnly`; absent = false). */
36
12
  export interface SettingsSourceContract {
37
13
  readonly trust: MinTrust;
38
14
  readonly repoControllable: boolean;
15
+ readonly tightenOnly?: boolean;
39
16
  }
40
17
  /**
41
18
  * The source table. `flagSettings` is in the operator tier (`global`, not repository-controllable) like CC's: settings handed in at launch are
@@ -63,6 +40,11 @@ export declare const SETTINGS_SOURCE_CONTRACT: {
63
40
  readonly trust: "managed";
64
41
  readonly repoControllable: false;
65
42
  };
43
+ readonly foreignProjectSettings: {
44
+ readonly trust: "project";
45
+ readonly repoControllable: true;
46
+ readonly tightenOnly: true;
47
+ };
66
48
  };
67
49
  /**
68
50
  * The ONE lookup of {@link SETTINGS_SOURCE_CONTRACT}: a source's tier and whether a repository controls it. Every function in this package
@@ -78,8 +60,9 @@ export declare function settingsSourceContract(source: SettingsSource): Settings
78
60
  export declare const WORKSPACE_TRUST_STATES: readonly ["accepted", "declined", "no_trust_step"];
79
61
  export type WorkspaceTrustState = (typeof WORKSPACE_TRUST_STATES)[number];
80
62
  /** What {@link layerAdmission} answers (closed): `admitted`; `trust_withheld` = would be admitted once the workspace is `accepted`;
81
- * `below_trust_floor` = this source can never set it (its tier is below the floor). */
82
- export declare const LAYER_ADMISSION_VERDICTS: readonly ["admitted", "trust_withheld", "below_trust_floor"];
63
+ * `below_trust_floor` = this source can never set it (its tier is below the floor); `tighten_only` = this source is read in the tighten direction
64
+ * only, so nothing — no workspace decision — admits this write from it. */
65
+ export declare const LAYER_ADMISSION_VERDICTS: readonly ["admitted", "trust_withheld", "below_trust_floor", "tighten_only"];
83
66
  export type LayerAdmissionVerdict = (typeof LAYER_ADMISSION_VERDICTS)[number];
84
67
  /**
85
68
  * Does a value from `source` for `category` (or one of its declared `member`s; `value` = the candidate value, for a value-directional member)
@@ -89,3 +72,36 @@ export type LayerAdmissionVerdict = (typeof LAYER_ADMISSION_VERDICTS)[number];
89
72
  * Unknown source / trust reading / category, a member named on a whole-key category, or an undeclared member ⇒ RangeError (never a fallback).
90
73
  */
91
74
  export declare function layerAdmission<C extends SafetyMergeCategory>(source: SettingsSource, workspace: WorkspaceTrustState, category: C, member?: SafetyMergeMember<C>, value?: unknown): LayerAdmissionVerdict;
75
+ /**
76
+ * Is a layer from `source` read in the tighten direction only, given the workspace trust reading? — the ONE answer {@link layerAdmission} and the
77
+ * disclosure ({@link buildLayerPartsUnreadNotice}) read: a tighten-only source always; a repository-controllable source while the reading is not
78
+ * `accepted`; an operator source never. Unknown source / reading ⇒ RangeError.
79
+ */
80
+ export declare function layerReadsTightenOnly(source: SettingsSource, workspace: WorkspaceTrustState): boolean;
81
+ /** The event code of the one-line disclosure of what a tighten-direction-only layer did not read ({@link buildLayerPartsUnreadNotice}). */
82
+ export declare const LAYER_PARTS_UNREAD_CODE = "settings.layer_parts_unread";
83
+ /** The disclosure: plain data — the code, the source, the parts as handed in (machine face, unaltered) and the one line (display face). */
84
+ export interface LayerPartsUnreadNotice {
85
+ readonly code: typeof LAYER_PARTS_UNREAD_CODE;
86
+ readonly source: SettingsSource;
87
+ readonly parts: readonly string[];
88
+ readonly line: string;
89
+ }
90
+ /**
91
+ * The ONE line that names what a layer read in the tighten direction only did not read — the consumer emits it under {@link LAYER_PARTS_UNREAD_CODE}.
92
+ * `file` = how the consumer shows the path of the layer's file it opened (its own display string, bounded by the platform's path limit — stripped of
93
+ * control and bidi characters but not cut, so the file name at its end survives); `parts` = the setting paths it did not read, as written in the file
94
+ * (repository-written text: echoed in the bounded caller-text form; dot-joined, document order):
95
+ * every write `layerAdmission` did not admit, AND every key the consumer does not map to a category at all (not read either). Line form —
96
+ * `<file>: read for restrictions only (<cause>) — not read: <parts>`, the cause `it is written for another tool` for a tighten-only source and
97
+ * `this workspace is not trusted` for a repository-controllable one; each part cut at 64 characters after stripping, at most 16 parts named and the rest
98
+ * counted (`+N more`). No unread part ⇒ `undefined` (nothing to disclose).
99
+ * A layer that is NOT read in the tighten direction only (an operator source; this product's repository files in an accepted workspace) ⇒ RangeError:
100
+ * this sentence would be false for it — a caller bug, loud. Unknown source / reading ⇒ RangeError.
101
+ */
102
+ export declare function buildLayerPartsUnreadNotice(input: {
103
+ readonly source: SettingsSource;
104
+ readonly workspace: WorkspaceTrustState;
105
+ readonly file: string;
106
+ readonly parts: readonly string[];
107
+ }): LayerPartsUnreadNotice | undefined;
@@ -6,13 +6,20 @@
6
6
  * there are, which trust tier each one carries, which of them a repository controls, and what an untrusted workspace does to them —
7
7
  * every host kept its own table for that. This file is that table, plus the one function a resolver asks per layer × key.
8
8
  *
9
- * THE RULE (one, no per-key special case): a repository-controllable source (`projectSettings` / `localSettings` — a clone brings or the
10
- * repository's tooling writes them) in a workspace whose trust reading is not `accepted` admits only the members whose published
11
- * direction is `"tighten"`; everything it may otherwise set is `trust_withheld`. In every case the source's tier must reach the floor
12
- * (`isAtLeast(tier, floor)`), checked first — a write below its floor is `below_trust_floor` whether or not the workspace is trusted, which
13
- * also keeps the loose pole of a value-directional (`boolPole`) member out of an untrusted layer. The `direction` data already says which
14
- * keys an untrusted repository may still set (restricting needs no trust); there is no per-row workspace column (that would copy a
15
- * layer fact into every category row).
9
+ * THE RULE (one, no per-key special case): a layer that is READ IN THE TIGHTEN DIRECTION ONLY ({@link layerReadsTightenOnly}) admits only the
10
+ * members whose published direction is `"tighten"`. Which layers those are is one answer, from the source table and the workspace reading:
11
+ * · a repository-controllable source of this product (`projectSettings` / `localSettings` — a clone brings or the repository's tooling writes
12
+ * them) while the workspace trust reading is not `accepted` — everything else it may set is `trust_withheld` (accepting the workspace admits it);
13
+ * · a tighten-only source (`foreignProjectSettings` — the repository's settings files written for ANOTHER tool, today the `.claude/settings.json` /
14
+ * `.claude/settings.local.json` a cloned repository carries) whatever the reading — everything else is `tighten_only` (nothing admits it:
15
+ * accepting a workspace is consent to this product's files, not to another tool's hooks, env and allow rules).
16
+ * In every case the source's tier must reach the floor (`isAtLeast(tier, floor)`), checked first — a write below its floor is `below_trust_floor`
17
+ * whatever the reading, which also keeps the loose pole of a value-directional (`boolPole`) member out of such a layer. The `direction` data
18
+ * already says which keys such a layer may still set (restricting needs no trust); there is no per-row workspace or source column (that would
19
+ * copy a layer fact into every category row).
20
+ *
21
+ * DISCLOSURE (one event code, {@link LAYER_PARTS_UNREAD_CODE}): a layer read in the tighten direction only names what it did not read in ONE
22
+ * line ({@link buildLayerPartsUnreadNotice}) — the same line form for both causes above, the cause in parentheses.
16
23
  *
17
24
  * CC 2.1.296 (the reference): trust is one per-session boolean (a non-interactive session counts as trusted, `cli296.js:238351`); keys split
18
25
  * into the operator tier `policy / flag / user` and the repository-controllable tier `project / local` ("projectSettings and
@@ -25,11 +32,15 @@
25
32
  * NOT here (each has its own place): the hooks EXECUTION gate (`declined` ⇒ no hook from any layer runs) is contract text in `./hooks`'
26
33
  * header and executes in the client core; the workspace trust RECORD (where "this folder was accepted" is stored) is host state.
27
34
  */
35
+ import { echoCallerText } from "./caller-text-echo.js";
36
+ import { stripControlAndBidi } from "./control-and-bidi.js";
28
37
  import { SAFETY_MERGE_SPEC, isAtLeast, memberAdmission } from "./safety-merge-spec.js";
29
38
  /** The settings sources, in MERGE order (a later source overrides an earlier one where a category's merge shape lets it):
30
39
  * user < project < local < flag < policy. `flagSettings` = settings handed in at launch (`--settings <file|json>`, an SDK embedder's inline
31
- * settings); `policySettings` = managed. */
32
- export const SETTINGS_SOURCES = ["userSettings", "projectSettings", "localSettings", "flagSettings", "policySettings"];
40
+ * settings); `policySettings` = managed. `foreignProjectSettings` (appended) = the repository's settings files written for another tool; it
41
+ * contributes tightening writes only, and every tighten rule merges by "the stricter wins" (order-independent), so its place in the order
42
+ * changes no merged value. */
43
+ export const SETTINGS_SOURCES = ["userSettings", "projectSettings", "localSettings", "flagSettings", "policySettings", "foreignProjectSettings"];
33
44
  /**
34
45
  * The source table. `flagSettings` is in the operator tier (`global`, not repository-controllable) like CC's: settings handed in at launch are
35
46
  * the launcher's own act — an SDK embedder or a wrapper script — not something a repository brings (a wrapper that turns repository content
@@ -41,6 +52,10 @@ export const SETTINGS_SOURCE_CONTRACT = {
41
52
  localSettings: { trust: "project", repoControllable: true },
42
53
  flagSettings: { trust: "global", repoControllable: false },
43
54
  policySettings: { trust: "managed", repoControllable: false },
55
+ // The repository's settings files written for another tool (a cloned repository's `.claude/settings.json` / `.claude/settings.local.json`):
56
+ // project tier and repository-controlled like this product's own project files, and tighten-only — its deny / ask rules, kill-switches ON and
57
+ // sandbox restrictions are read, its allow rules, hooks, env, MCP servers and the rest never are (see the file header).
58
+ foreignProjectSettings: { trust: "project", repoControllable: true, tightenOnly: true },
44
59
  };
45
60
  /**
46
61
  * The ONE lookup of {@link SETTINGS_SOURCE_CONTRACT}: a source's tier and whether a repository controls it. Every function in this package
@@ -60,8 +75,9 @@ export function settingsSourceContract(source) {
60
75
  */
61
76
  export const WORKSPACE_TRUST_STATES = ["accepted", "declined", "no_trust_step"];
62
77
  /** What {@link layerAdmission} answers (closed): `admitted`; `trust_withheld` = would be admitted once the workspace is `accepted`;
63
- * `below_trust_floor` = this source can never set it (its tier is below the floor). */
64
- export const LAYER_ADMISSION_VERDICTS = ["admitted", "trust_withheld", "below_trust_floor"];
78
+ * `below_trust_floor` = this source can never set it (its tier is below the floor); `tighten_only` = this source is read in the tighten direction
79
+ * only, so nothing — no workspace decision — admits this write from it. */
80
+ export const LAYER_ADMISSION_VERDICTS = ["admitted", "trust_withheld", "below_trust_floor", "tighten_only"];
65
81
  /**
66
82
  * Does a value from `source` for `category` (or one of its declared `member`s; `value` = the candidate value, for a value-directional member)
67
83
  * enter the effective settings, given the workspace trust reading? The rule is in the file header; the data is {@link SETTINGS_SOURCE_CONTRACT}
@@ -71,7 +87,7 @@ export const LAYER_ADMISSION_VERDICTS = ["admitted", "trust_withheld", "below_tr
71
87
  */
72
88
  export function layerAdmission(source, workspace, category, member, value) {
73
89
  const layer = settingsSourceContract(source);
74
- const trusted = workspaceAccepted(workspace);
90
+ const tightenOnlyLayer = readsTightenOnly(layer, workspace); // an unknown reading throws here, before any category lookup
75
91
  let floor;
76
92
  let direction;
77
93
  if (member === undefined) {
@@ -87,9 +103,55 @@ export function layerAdmission(source, workspace, category, member, value) {
87
103
  }
88
104
  if (!isAtLeast(layer.trust, floor))
89
105
  return "below_trust_floor";
90
- if (layer.repoControllable && !trusted && direction !== "tighten")
91
- return "trust_withheld";
92
- return "admitted";
106
+ if (direction === "tighten" || !tightenOnlyLayer)
107
+ return "admitted";
108
+ return layer.tightenOnly === true ? "tighten_only" : "trust_withheld";
109
+ }
110
+ /**
111
+ * Is a layer from `source` read in the tighten direction only, given the workspace trust reading? — the ONE answer {@link layerAdmission} and the
112
+ * disclosure ({@link buildLayerPartsUnreadNotice}) read: a tighten-only source always; a repository-controllable source while the reading is not
113
+ * `accepted`; an operator source never. Unknown source / reading ⇒ RangeError.
114
+ */
115
+ export function layerReadsTightenOnly(source, workspace) {
116
+ return readsTightenOnly(settingsSourceContract(source), workspace);
117
+ }
118
+ function readsTightenOnly(layer, workspace) {
119
+ const accepted = workspaceAccepted(workspace); // judged first: an unknown reading throws whatever the source
120
+ return layer.tightenOnly === true || (layer.repoControllable && !accepted);
121
+ }
122
+ /** The event code of the one-line disclosure of what a tighten-direction-only layer did not read ({@link buildLayerPartsUnreadNotice}). */
123
+ export const LAYER_PARTS_UNREAD_CODE = "settings.layer_parts_unread";
124
+ /** How many unread parts the line names before it counts the rest (`+N more`): a repository's file can carry any number of keys. */
125
+ const NOTICE_PARTS_SHOWN = 16;
126
+ /**
127
+ * The ONE line that names what a layer read in the tighten direction only did not read — the consumer emits it under {@link LAYER_PARTS_UNREAD_CODE}.
128
+ * `file` = how the consumer shows the path of the layer's file it opened (its own display string, bounded by the platform's path limit — stripped of
129
+ * control and bidi characters but not cut, so the file name at its end survives); `parts` = the setting paths it did not read, as written in the file
130
+ * (repository-written text: echoed in the bounded caller-text form; dot-joined, document order):
131
+ * every write `layerAdmission` did not admit, AND every key the consumer does not map to a category at all (not read either). Line form —
132
+ * `<file>: read for restrictions only (<cause>) — not read: <parts>`, the cause `it is written for another tool` for a tighten-only source and
133
+ * `this workspace is not trusted` for a repository-controllable one; each part cut at 64 characters after stripping, at most 16 parts named and the rest
134
+ * counted (`+N more`). No unread part ⇒ `undefined` (nothing to disclose).
135
+ * A layer that is NOT read in the tighten direction only (an operator source; this product's repository files in an accepted workspace) ⇒ RangeError:
136
+ * this sentence would be false for it — a caller bug, loud. Unknown source / reading ⇒ RangeError.
137
+ */
138
+ export function buildLayerPartsUnreadNotice(input) {
139
+ const layer = settingsSourceContract(input.source);
140
+ if (!readsTightenOnly(layer, input.workspace)) {
141
+ throw new RangeError(`buildLayerPartsUnreadNotice: source '${input.source}' is not read in the tighten direction only under workspace reading '${input.workspace}' — there is nothing "read for restrictions only" to disclose`);
142
+ }
143
+ if (input.parts.length === 0)
144
+ return undefined;
145
+ const cause = layer.tightenOnly === true ? "it is written for another tool" : "this workspace is not trusted";
146
+ const shown = input.parts.slice(0, NOTICE_PARTS_SHOWN).map(echoCallerText);
147
+ const rest = input.parts.length - shown.length;
148
+ const named = rest > 0 ? `${shown.join(", ")}, +${rest} more` : shown.join(", ");
149
+ return {
150
+ code: LAYER_PARTS_UNREAD_CODE,
151
+ source: input.source,
152
+ parts: [...input.parts],
153
+ line: `${stripControlAndBidi(input.file)}: read for restrictions only (${cause}) — not read: ${named}`,
154
+ };
93
155
  }
94
156
  /** `accepted` ⇒ true; `declined` / `no_trust_step` ⇒ false (they differ only at the hooks execution gate, not here); anything else ⇒ RangeError. */
95
157
  function workspaceAccepted(workspace) {
@@ -4,9 +4,9 @@
4
4
  > 每次全量重算并逐名比对 —— 加 / 删 / 改形一个导出而不重生成本档 ⇒ 红。导出面的每次变动因此以本档的 diff 出现在评审里。
5
5
  > 形:`value` = 运行期拿得到的值;`type` = 只在类型层;`value+type` = 同名的值与类型(zod schema 与它推出的类型)。
6
6
 
7
- 子路径 24 条(资产出口 `./package.json` / `./fleet-paths.json` 不是模块,不列),导出名合计 1330 个(同名在多条子路径上各计一次)。
7
+ 子路径 24 条(资产出口 `./package.json` / `./fleet-paths.json` 不是模块,不列),导出名合计 1339 个(同名在多条子路径上各计一次)。
8
8
 
9
- ## `.`(654)
9
+ ## `.`(662)
10
10
 
11
11
  - `ABSOLUTE_HOST_PATH_MAX` — value
12
12
  - `ACCESS_TOKEN_TTL_SECONDS` — value
@@ -35,6 +35,7 @@
35
35
  - `BudgetGroupMember` — value
36
36
  - `COMMAND_DECISIONS` — value
37
37
  - `CONFIG_CHANGE_SOURCES` — value
38
+ - `CONTROL_AND_BIDI_CHARS` — value
38
39
  - `CROSS_SESSION_INBOUND_UNSET` — value
39
40
  - `CROSS_SESSION_INBOUND_VALUES` — value
40
41
  - `CancelSource` — value+type
@@ -219,9 +220,11 @@
219
220
  - `JwksCacheSnapshot` — type
220
221
  - `K8S_QUANTITY` — value
221
222
  - `LAYER_ADMISSION_VERDICTS` — value
223
+ - `LAYER_PARTS_UNREAD_CODE` — value
222
224
  - `LEASE_STATES` — value
223
225
  - `LaneTenancy` — type
224
226
  - `LayerAdmissionVerdict` — type
227
+ - `LayerPartsUnreadNotice` — type
225
228
  - `LeaseState` — value+type
226
229
  - `LimitsConfig` — value+type
227
230
  - `LogoutRequest` — value+type
@@ -233,6 +236,7 @@
233
236
  - `MCP_POLICY_RULES` — value
234
237
  - `MCP_SERVER_LIST_KEYS` — value
235
238
  - `MEMORY_ENGINE_BACKENDS` — value
239
+ - `MIN_TRUST_WORDS` — value
236
240
  - `MODEL_ENTRY_REFUSAL_CODES` — value
237
241
  - `MODEL_PLANE_REFUSAL_CODES` — value
238
242
  - `MODEL_SWITCH_SOURCES` — value
@@ -522,6 +526,7 @@
522
526
  - `aggregateUsage` — value
523
527
  - `applyProjectPatch` — value
524
528
  - `buildEffective` — value
529
+ - `buildLayerPartsUnreadNotice` — value
525
530
  - `buildRetiredEnvWire` — value
526
531
  - `canSwitchScope` — value
527
532
  - `clampServingLeaseTtlMs` — value
@@ -545,6 +550,7 @@
545
550
  - `findSkillContent` — value
546
551
  - `fnv1a64` — value
547
552
  - `formatConfigPath` — value
553
+ - `hasControlOrBidi` — value
548
554
  - `hasNul` — value
549
555
  - `hookMatcherForm` — value
550
556
  - `hookMatcherSelects` — value
@@ -580,6 +586,7 @@
580
586
  - `judgeWebAdminCredentialDeclaration` — value
581
587
  - `jwksWithinOfflineGrace` — value
582
588
  - `layerAdmission` — value
589
+ - `layerReadsTightenOnly` — value
583
590
  - `leaseExhausted` — value
584
591
  - `leaseRemaining` — value
585
592
  - `looksLikeJwt` — value
@@ -651,6 +658,7 @@
651
658
  - `startsWithHttpScheme` — value
652
659
  - `statusRowsToPrune` — value
653
660
  - `stopAttributionText` — value
661
+ - `stripControlAndBidi` — value
654
662
  - `toSkillsManifest` — value
655
663
  - `toVerifiedIdentity` — value
656
664
  - `trustRank` — value
@@ -1030,9 +1038,10 @@
1030
1038
  - `REMOTE_EXEC_DOCUMENT` — value
1031
1039
  - `RemoteExecSpec` — value+type
1032
1040
 
1033
- ## `./safety-merge-spec`(18)
1041
+ ## `./safety-merge-spec`(19)
1034
1042
 
1035
1043
  - `FailMode` — value+type
1044
+ - `MIN_TRUST_WORDS` — value
1036
1045
  - `MemberAdmission` — value+type
1037
1046
  - `MemberDirection` — value+type
1038
1047
  - `MergeCategorySpec` — value+type
@@ -40,3 +40,9 @@
40
40
  ## settings sources(`SETTINGS_SOURCE_CONTRACT`,`src/settings-sources.ts`)
41
41
 
42
42
  无偏差:本地层与项目层同为「仓库可控」档(`:237757`);`flagSettings` 归运维档 `global`(与 user / policy 并列)。
43
+
44
+ | 号 | 面 | CC 原形 | 本包 | 方向 | 理由 | 读它的 |
45
+ |---|---|---|---|---|---|---|
46
+ | SS-CCD-08 | 仓库里写给 CC 的 `.claude/settings.json` / `.claude/settings.local.json` | 那就是 CC 自己的项目层 / 本地层:工作区受信时整份读(hooks / env / allow 规则 / MCP 服务器……) | 源 `foreignProjectSettings`(`tightenOnly`):不论工作区读数只读收紧方向(deny / ask 规则、关停位的开、沙箱收紧极),其余答 `tighten_only` 或 `below_trust_floor`,一行披露没读的部分 | 更严 | 对本工具它是另一个工具的配置:用户接受本工具的工作区,不等于同意把另一个工具的 hooks、环境变量与 allow 规则搬进来;收紧方向不需要信任 | 壳与服务端读仓库设置文件的那一腿 |
47
+
48
+ 关停位(`SAFETY_MERGE_SPEC.killSwitches`)无偏差:任一层可写「开」、合并严者胜,与限制性键表 `{path:["permissions","disableBypassPermissionsMode"],restrictive:"disable"}`(`:237449`)同形。
package/llms-full.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  <!--
2
2
  llms-full.txt — 自包含全文 · 消费方(使用者)视角 · @sema-agent/settings-schema
3
- 重写日期: 2026-10-18 | 包版本: 14.1.0
3
+ 重写日期: 2026-10-21 | 包版本: 14.3.0
4
4
  生成方式: 无生成脚本——按当前树 src/*.ts 导出面 + package.json + README/CHANGELOG 手写事实版。
5
5
  证据约定: 每条"它怎么工作"的断言附 (文件: 符号) 坐标,刻意不用行号(行号逐版腐烂;符号名由
6
6
  typecheck 与 test/package-surface.test.ts、test/docs-hygiene.test.ts 等常驻门钉住)。无证据推断显式标【推断】。
@@ -301,7 +301,7 @@ entitlement/execution/limits = `none`;`projects` = `salvage`,整域档位仍 `"t
301
301
  `httpHookEnvVarsAllowed(hookVars, layers, workspace)` = hook 自己的 allowedEnvVars ∩ 每张准入名单。与 CC 差:拼接 → 取交、任意层 → 准入层(SS-CCD-06 / 07)。
302
302
  - `mcpServerPolicyVerdict(server, layers)`(src/mcp-server-policy.ts)→ `undefined` | `McpPolicyBlock{ name, list, rule, target, written, sources }`;闭集 `MCP_SERVER_LIST_KEYS` /
303
303
  `MCP_POLICY_RULES`(name / command / url / empty)。层 `McpPolicyLayer = { source: SettingsSource, … }`(只带源名)。拒绝绝对优先、各层都算(名字 → 命令整数组 → URL 拒绝出口);
304
- 允许名单各层拼接(CC 形),托管层(`settingsSourceContract(源).trust === "managed"`)`allowManagedMcpServersOnly` ⇒ 只读托管层;缺席不限、空一台都不许;stdio 遇命令条目 / 远端遇 URL 条目须命中其一,否则按名字。`server.target` = 展开后连接目标
304
+ 允许名单各层拼接(CC 形;只收紧的源 `settingsSourceContract(源).tightenOnly` 的允许名单不读 —— 拼进来就是放宽,它只贡献拒绝名单),托管层(`settingsSourceContract(源).trust === "managed"`)`allowManagedMcpServersOnly` ⇒ 只读托管层;缺席不限、空一台都不许;stdio 遇命令条目 / 远端遇 URL 条目须命中其一,否则按名字。`server.target` = 展开后连接目标
305
305
  (只用来判),`server.written` = 展开前写法(唯一回显;stdio 只给可执行名 + 参数个数)。SDK 型等是否豁免归调用方。
306
306
 
307
307
  --- 4.5d 界面语言设置片段(src/ui-language.ts,主 barrel;13.0.0) ---
@@ -322,7 +322,9 @@ entitlement/execution/limits = `none`;`projects` = `salvage`,整域档位仍 `"t
322
322
  issue = 码 + 坐标 + 坏字段名)。`classifyHooksConfig(settingsDoc)` = hooks 坏形的唯一读者:结局 `loaded`/`loaded-partial`/`rejected`;剔点五码
323
323
  `HOOKS_DROPPED_PIECE_CODES`(非守卫事件的坏块逐个剔);拒点六码 `HOOKS_REJECTION_CODES`(守卫事件 PreToolUse / PermissionRequest 任何坏处、
324
324
  或守卫迹象出现在 hooks 外 / 未知事件下 / 匹配器或条目字段里 ⇒ 整份拒;守卫事件值 null = 剔);坐标原样(出门前 `outgoingPath`)、零句子;剔点与拒绝项同一条 `field` 律 —— 坏块是匹配器 / 条目(对象)⇒ `field` = 它上面出问题的键(契约字段值错,或装着守卫迹象的成员:
325
- 契约不认识的未知字段 / 装得下更多东西的契约字段 / 条目自己是匹配器形时取 `hooks`),块不是对象才缺席;
325
+ 契约不认识的未知字段 / 装得下更多东西的契约字段 / 条目自己是匹配器形时取 `hooks`),块不是对象才缺席;一块多个迹象时的报告序 = 先该块自身的迹象
326
+ (自有守卫事件键;条目自己是匹配器形取 `hooks`),再按文档序找嵌套迹象 —— 自有守卫键写在嵌套迹象之后仍先报自有键;匹配器下合形条目列表(`hooks` 数组)
327
+ 里的迹象归各条目自己报(各是一块),所以 `{ matcher: 5, hooks: [藏迹象的条目] }` 报匹配器的 `matcher`;
326
328
  自含(只引用 `HOOK_EVENTS` 与 JS 内建;整函数抄走的生成器 —— client-core —— 须把本包运行期导出 `HOOK_EVENTS` 作为上游常量注入抄本作用域,
327
329
  不许在抄本里贴字面事件表:那是第二张事件表,漂移门看不见)。执行门契约(`./hooks` 头注):工作区 `declined` ⇒ 所有层 hooks 不跑,执行在 client-core。`HooksSlotSemantics`(保留词表,
328
330
  未消费);权限词表 `PermissionBehavior`/`PermissionMode`/`PermissionUpdate` 族;停止归因 `StoppedBy`/
@@ -331,6 +333,10 @@ entitlement/execution/limits = `none`;`projects` = `salvage`,整域档位仍 `"t
331
333
  名单内展开(env 无值 ⇒ 空串)、名单外与凭据名一律空串并进返回值 `blanked`(`{ header, name, reason: "not-allowed" | "credential" }`)、结果剥 CR/LF/NUL;
332
334
  纯函数,壳与服务器同调一只(读法照 CC 2.1.296)。结果类型 `HookEnvInterpolation` / `HookEnvBlank`。
333
335
 
336
+ - 一行回显的剥除集(src/control-and-bidi.ts,主 barrel):`CONTROL_AND_BIDI_CHARS`(C0 + DEL + Unicode `Bidi_Control=Yes` 12 个码点,
337
+ 正则字符类体的源码文本)、`stripControlAndBidi(text)`(只删集内码点)、`hasControlOrBidi(text)`(⇔ 剥了会变)。外来串进拒句 / 告警 / 日志 / 屏上一行前剥的唯一一份;
338
+ C1 与 U+2028 / U+2029 刻意不在集里;是本包的回显策略,属主在此(不对拍引擎的双向码点表),请求面两只回显描述器读它,消费方禁自存字符类。
339
+
334
340
  --- 4.7 safety-merge-spec(主 barrel / `./safety-merge-spec`) ---
335
341
  - `SAFETY_MERGE_SPEC`:逐类别合并/执法表(`mergeShape`/`minTrust`/`tighten`/`enforcement`/`fail`);成员级
336
342
  准入(1.1.0 机制):`permissions`(allow=loosen@global / deny·ask=tighten@local)与 `sandboxNetwork`
@@ -340,12 +346,28 @@ entitlement/execution/limits = `none`;`projects` = `salvage`,整域档位仍 `"t
340
346
  `memberMinTrust`(禁自铸表)。
341
347
  - 信任阶梯(1.2.0):`trustRank`(managed 6 > global 5 > project 4 > local 3 > session 2 > task 1)、
342
348
  `isAtLeast`;`MinTrust` 含 `task`(声明序非阶梯,阶梯唯一真源是 `trustRank`)。
349
+ 词表是一只纯常量 `MIN_TRUST_WORDS`(`local, session, project, global, managed, task`;`MinTrust` zod 枚举由它派生):`trustRank` 的闭包自由名 = `MIN_TRUST_WORDS` + JS 内建
350
+ `RangeError` / `String`,`isAtLeast` 的 = `trustRank` —— 整函数抄走这一对的生成器(client-core)把 `MIN_TRUST_WORDS` 登记成全局即可,不许手写层表 / 词表字面量。
343
351
  - 设置源表(src/settings-sources.ts,主 barrel):`SETTINGS_SOURCES`(合并序 user<project<local<flag<policy)、`SETTINGS_SOURCE_CONTRACT`
344
352
  (user global / project project 仓库可控 / local project 仓库可控 / flag global / policy managed)、`WORKSPACE_TRUST_STATES`
345
353
  (accepted/declined/no_trust_step)、`layerAdmission(source, workspace, category, member?, value?)` → admitted / trust_withheld / below_trust_floor
346
354
  (地板先判;仓库可控层 × 非 accepted 只收 tighten 成员);闭集外抛 RangeError。`settingsSourceContract(source)` = 源表唯一查口(同一句 RangeError):
347
355
  本包要层档的函数(`layerAdmission`、http hook 名单判官、MCP 名单判决)都经它,不收调用方给的档。
356
+ 尾插源 `foreignProjectSettings`(仓库里写给另一个工具的项目层设置文件,今天 = 克隆来的 `.claude/settings.json` / `.claude/settings.local.json`):
357
+ `{ trust: project, repoControllable: true, tightenOnly: true }` —— 不论工作区读数只准入方向 tighten 的成员(permissions.deny / ask、killSwitches 的开、
358
+ sandboxNetwork 的收紧极);其余过了地板的写答尾插判词 `tighten_only`(接受工作区也不收:接受的是本工具的文件,不是另一个工具的 hooks / env / allow),
359
+ 地板之下照旧 `below_trust_floor`;它只贡献收紧的写,各 tighten 律按「严者胜」合并,与次序无关。`layerReadsTightenOnly(source, workspace)` = 「这一层是否只读收紧方向」
360
+ 的唯一答案(只收紧的源恒是;本工具仓库可控层在非 accepted 时是;运维档恒否),`layerAdmission` 与披露同读它。
361
+ 披露 `buildLayerPartsUnreadNotice({ source, workspace, file, parts })` → `{ code: "settings.layer_parts_unread"(LAYER_PARTS_UNREAD_CODE), source, parts, line }`:
362
+ 一行 `<file>: read for restrictions only (<起因>) — not read: <parts>`,起因 = `it is written for another tool` / `this workspace is not trusted`;
363
+ 文件与部分名剥控制与双向字符、部分名截 64、最多列 16 个(余者 `+N more`);parts = 判官没准入的写 + 消费方根本不映射到类目的键(document order);
364
+ 无没读部分 ⇒ undefined;层不是只读收紧方向 ⇒ RangeError(这句对它不成立)。升级说明:还没接上读法的消费方可先只出这一行、列出文件里的全部键(只提示不读)——
365
+ 那是消费方自己的过渡,本包不设旋钮。
348
366
  - 13.0.0 尾插整键类目 `httpHookAllowlists`(minTrust global;判决见 4.5c)。`SafetyMergeCategory` 随之加宽 —— 持穷举表的消费方编译红 = 通知。
367
+ - 尾插成员级类目 `killSwitches`(权限模式关停位 `disableBypassPermissionsMode` / `disableAutoMode`):任一层可收紧、不放宽 —— 两员 tighten、值向,
368
+ 开 = `boolPole{strict:true, minTrust:"local"}`(任一设置文件层可写),关 / 非布尔 / 不带值 = 行地板 global(= 类目地板;不认 boolPole 的旧消费方两极都不收 = fail-closed);
369
+ 合并 replace + boolStricter(strict:true)⇒ 一层的开胜过所有层的关;gate / closed。表上没有「关停位只许托管层」的地板(`permissionMode` 的 seam 不再这么写),
370
+ 判官与 resolver 同读这一行。与 CC 2.1.296 限制性键表同形(`permissions.disableBypassPermissionsMode` restrictive `"disable"`;消费方把 `"disable"` 读成开)。
349
371
 
350
372
  --- 4.8 fleet 契约(主 barrel / `./fleet`) ---
351
373
  - drain 协议(`DRAIN_PHASES`/`DrainSnapshot`/`drainVerdict`/`DEFAULT_DRAIN_GRACE_MS`);实例发现
package/llms.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  <!--
2
2
  llms.txt — 消费方(使用者)视角索引 · @sema-agent/settings-schema
3
- 重写日期: 2026-10-18 | 包版本: 14.1.0
3
+ 重写日期: 2026-10-21 | 包版本: 14.3.0
4
4
  生成方式: 无生成脚本——按当前树的 src/*.ts 导出面 + package.json + README/CHANGELOG 手写事实版;
5
5
  每条断言附证据坐标(文件:符号),刻意不用行号(行号随每版腐烂,符号名由 typecheck/test 钉住)。
6
6
  刷新触发: package.json 版号变更(test/docs-hygiene.test.ts D 组钉「包版本:」与 package.json 相等、
@@ -89,17 +89,21 @@ llms.txt — 消费方(使用者)视角索引 · @sema-agent/settings-schema
89
89
  6.x 存量 workers 文档在 `parseDomain` 之前跑一次 `migrateWorkersDomain`(或 bin `npx @sema-agent/settings-schema migrate-workers`;7.0.1 起 stdout = 结论行 `outcome=… written=…` + JSON,没写时 `wouldChange` 取代 `changed`)。
90
90
  (src/execution-lanes.ts;src/types.ts: `WorkerSpec`;src/migrate-workers.ts)
91
91
  - **hooks 契约**:33 个 hook 事件的全集 + owner 表 + 逐事件输入/输出形 + 配置面:hook 条目五种 type(`HOOK_ENTRY_TYPES`,含 `mcp_tool`)与**一只**
92
- 坏形分类器 `classifyHooksConfig(settingsDoc)`(非守卫事件坏块逐个剔、守卫事件 PreToolUse / PermissionRequest 任何坏处或守卫迹象 ⇒ 整份拒;码 + 坐标 + 坏字段名 `field`(坏块是匹配器 / 条目 ⇒ 它上面出问题的键,块不是对象才缺席)、零句子、零 zod 自含;
92
+ 坏形分类器 `classifyHooksConfig(settingsDoc)`(非守卫事件坏块逐个剔、守卫事件 PreToolUse / PermissionRequest 任何坏处或守卫迹象 ⇒ 整份拒;码 + 坐标 + 坏字段名 `field`(坏块是匹配器 / 条目 ⇒ 它上面出问题的键,块不是对象才缺席;一块多个迹象时先该块自身的迹象(自有守卫事件键;条目自己是匹配器形取 `hooks`),再按文档序找嵌套迹象;匹配器下合形条目列表里的迹象归各条目)、零句子、零 zod 自含;
93
93
  闭包里 JS 内建之外只有一个自由名 `HOOK_EVENTS` —— 整函数抄走的生成器(client-core)须把本包运行期导出 `HOOK_EVENTS`(上游常量)注入抄本作用域,不许贴字面事件表),
94
94
  `HooksConfig` / `HookEntry` 等 zod 面是它的投影;事件与 type 两张闭集与上游的漂移由常驻门看住(上游有而本包没有的词 ⇒ 门红并点名)。http 钩子 header 里 `$VAR` / `${VAR}` 的展开也只有一只读法(名单内展开、名单外与凭据名置空并回报)。(src/hooks.ts: `HOOK_EVENTS` / `interpolateHookEnvVars`)
95
95
  - **跨会话两键**(1.6.0 新增,`settings.json` 面;6.0.0 词表随 core 7.30.0 改):`crossSessionInbound`(wake/next-turn/hold/refuse + `default`=未设;`accept` 已退役,文件层响亮拒)
96
96
  与 `dialogExpiry`(60s/5m/10m/never,缺省 5m);另出两只层读取器,坏值 `ok:false` 不静默折缺省。
97
97
  (src/cross-session.ts: `CrossSessionSettings` / `readCrossSessionInbound` / `readDialogExpiry`)
98
- - **安全合并表** `SAFETY_MERGE_SPEC` + 信任阶梯 `trustRank` / `isAtLeast` + 成员级准入 `memberAdmission`。
98
+ - **安全合并表** `SAFETY_MERGE_SPEC` + 信任阶梯 `trustRank` / `isAtLeast`(词表来源 = 纯常量 `MIN_TRUST_WORDS`,闭包零 zod)+ 成员级准入 `memberAdmission`;关停位 `disableBypassPermissionsMode` / `disableAutoMode`
99
+ = 类目 `killSwitches`(尾插):任一层可收紧、不放宽 —— 开 = tighten@local(任一设置文件层),关 / 非布尔 / 不带值 = 行地板 global,合并 boolStricter;没有「只许托管层」的地板。
99
100
  (src/safety-merge-spec.ts)
100
101
  - **设置源表 + 层准入**:`SETTINGS_SOURCES`(合并序)/ `SETTINGS_SOURCE_CONTRACT`(信任档 + 是否仓库可控;本地层与项目层同 `project` 档)/
101
102
  `WORKSPACE_TRUST_STATES` / `layerAdmission(source, workspace, category, member?, value?)`(地板先判;仓库可控层未受信只收 tighten)/
102
- `settingsSourceContract(source)`(源表唯一查口:本包要层档的函数都经它,不收调用方给的档;层只带源名)。(src/settings-sources.ts)
103
+ `settingsSourceContract(source)`(源表唯一查口:本包要层档的函数都经它,不收调用方给的档;层只带源名)。
104
+ 写给另一个工具的项目层设置文件(克隆来的 `.claude/settings*.json`)= 尾插源 `foreignProjectSettings`(项目档、仓库可控、`tightenOnly`):不论工作区读数只准入 tighten 成员
105
+ (deny / ask、关停位的开、沙箱收紧极),其余答尾插判词 `tighten_only`(受信也救不了)或 `below_trust_floor`;`layerReadsTightenOnly(source, workspace)` = 「这一层是否只读收紧方向」唯一答案;
106
+ 没读的部分一行披露 `buildLayerPartsUnreadNotice({ source, workspace, file, parts })`(码 `settings.layer_parts_unread`,两种起因同一句形)。(src/settings-sources.ts)
103
107
  - **URL 名单一条模式的判法**(13.0.0):一条律两只出口 —— `urlAllowPatternMatches`(命中面 ⊆ CC `g9`,补上游四洞:写明缺省端口照判、`*` 协议只通配
104
108
  http/https/ws/wss 且 `ws+unix:` 套接字段逐字、解析不出的模式不放、解码出的字面 `*` 不当通配)/ `urlDenyPatternMatches`(⊇ `g9` ∪ 整串 glob)。
105
109
  偏离登 `docs/CC-DIVERGENCE.md`。(src/url-pattern.ts)
@@ -108,11 +112,14 @@ llms.txt — 消费方(使用者)视角索引 · @sema-agent/settings-schema
108
112
  `httpHookEnvVarsAllowed(hookVars, layers, workspace)`(取交);准入 ⇔ `layerAdmission(源, 工作区读数, "httpHookAllowlists") === "admitted"`。
109
113
  (src/http-hook-allowlists.ts)
110
114
  - **MCP 名单判决**(13.0.0):`mcpServerPolicyVerdict(server, layers)` → `McpPolicyBlock{name, list, rule, target, written, sources}` 或 `undefined`;
111
- 判按展开后目标、回显只按展开前写法(stdio 只给可执行名 + 参数个数);托管层 = 源表档 `managed` 的源(层只带源名)。(src/mcp-server-policy.ts)
115
+ 判按展开后目标、回显只按展开前写法(stdio 只给可执行名 + 参数个数);托管层 = 源表档 `managed` 的源(层只带源名);只收紧的源只贡献拒绝名单(允许名单是拼接 = 放宽,不读)。(src/mcp-server-policy.ts)
112
116
  - **请求面 MCP 条目判官**(14.0.0,`src/mcp-request-entry.ts`):`isUsableWireName` / `describeBadWireName`(引擎线上名长界 `TOOL_WIRE_NAME_MAX_CHARS` 的镜像;首尾空白 = 坏名,不 trim 不规范化,内部空白合法)·
113
117
  `isContentOriginWord` / `describeBadContentOrigin`(回显剥控制与双向字符、截 64 字、引号形)· `readMcpIdentityHeaders` · `readRequestMcpTransport`(硬坏形 `ok:false` 点名键路径;软坏形读器丢键并记进 `lost`;
114
118
  `env` / `headers` 只收纯数据的「名 → 串」记录,`Map` / 类实例 / 自有 `__proto__` 成员不收、交副本,`lost` 条目带 `member` / `fault` 机读位)
115
119
  + 值种类描述件 —— 服务端请求腿与壳的发送前预检同读一份(判据与原因句逐字同)。`toolFaces` 声明在本包解析透明,形归引擎摄入侧判,本包不镜像工具面词表。
120
+ - **一行回显的剥除集**(`src/control-and-bidi.ts`,主 barrel):`CONTROL_AND_BIDI_CHARS`(C0 + DEL + 12 个 `Bidi_Control` 码点,正则字符类体的源码文本)·
121
+ `stripControlAndBidi(text)` · `hasControlOrBidi(text)`;外来串落成一行(拒句 / 告警 / 日志 / 屏上)前剥的唯一一份,C1 与 U+2028 / U+2029 不在集里;
122
+ 是本包的回显策略(不是引擎双向码点表的镜像),请求面描述器与消费方都读它,禁自存字符类。
116
123
  - **prompts 域单轴**(14.0.0):只装中心 artifact(存储 `PromptsConfig = { artifact?, workers? }` / 下发 `EffectivePromptsWire = { schemaVersion: 2, artifact }` /
117
124
  唯一选择读法 `promptArtifactFor`);本包镜像 artifact v3 的结构与上界,语义交引擎 verify;13.x 两轴是墓碑,存量经一次性迁移 SS-M10。
118
125
  - **界面语言设置片段**(13.0.0):`UI_LANGUAGE_SETTING`(`semaUiLanguage`,`en` / `zh`,类 `ui`,用户设置)+ `readUiLanguage`(词表外 `ok:false`,不猜)。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/settings-schema",
3
- "version": "14.1.0",
3
+ "version": "14.3.0",
4
4
  "description": "Sema settings schema — pure config contract (zod domains + effective-config + roster resolution + ref-integrity + remoteExec) shared by sema-web-admin, sema-server, and the TOC desktop/CLI. One source of truth. (Renamed at 1.0.0 from @sema-agent/registry-core — name and docs only, no behavior change; note the range moves 0.19.0 -> 1.0.0, so a dependency must become ^1.0.0. The old names @sema-agent/registry-core <=0.19.0 and @sema-ai/registry-core remain published, frozen, for migration.)",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",