@sema-agent/settings-schema 1.0.0 → 1.2.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 +182 -1
- package/README.md +21 -2
- package/dist/safety-merge-spec.d.ts +155 -9
- package/dist/safety-merge-spec.js +150 -5
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,187 @@
|
|
|
6
6
|
> `@sema-agent/registry-core`(更早还曾是 `@sema-ai/registry-core`)。本档 `0.19.0` 以下各节里的
|
|
7
7
|
> 包名是**历史记录**,如实保留不追改。
|
|
8
8
|
|
|
9
|
+
## 1.2.0 — `MinTrust` 排序原语 + `task` 员([5796] 序题 → [5805] schema 认领 / [5807] cli 表态三点)
|
|
10
|
+
|
|
11
|
+
**新导出 `trustRank(layer)` / `isAtLeast(layer, minTrust)`——规范信任阶梯的**单源**(v2-design §1:29 逐字);
|
|
12
|
+
`MinTrust` **尾插** `task`。发布表 `SAFETY_MERGE_SPEC` **一字不动**(1.1.0 全表字面量冻结钉),枚举声明序不重排
|
|
13
|
+
(重排 = BREAKING,免裁);版号 minor = 加词([5805]/[5807] 双方口径「1.2.0 小批 additive」;codex R2 持严格 semver
|
|
14
|
+
异议,见门段——候裁项,非代码缺陷)。**
|
|
15
|
+
|
|
16
|
+
> ⚠️ **词表加宽在消费方是可见的**(codex R1-F1,措辞订正):「表不动」≠「消费方零感知」。① 类型面:持
|
|
17
|
+
> `Record<MinTrust, …>` 字面量或对 `MinTrust` 穷举 `switch` 的消费方,升级即**编译错**(响亮——这正是要的:那张表
|
|
18
|
+
> 该退役改经 `trustRank`);② 运行时:用本包 schema(`MinTrust` / `MemberAdmission` / `MergeCategorySpec`)校验
|
|
19
|
+
> **外部** spec 值的消费方,自本版起会**收下** `task`,必须能给它排位——用 `trustRank`,勿自铸五词表(五词表对
|
|
20
|
+
> `task` 取值 `undefined`,`<`/`>=` 比较静默为 `false`,方向随消费方写法而定、且不响)。发包日观察值(亲 grep,
|
|
21
|
+
> 观察 ≠ 规范):cli `src/` 对 `MinTrust` **类型/值零 import**——仅注释提及(`src/sema/settings/keyContracts.ts:14-15,36`),
|
|
22
|
+
> 其 `TrustName` / `TRUST_RANK` 自铸且已含 `task`(按 [5807] ③ 删表改 import 时才首次成为类型消费方);1.1.0 段矩阵
|
|
23
|
+
> 其余邻仓零命中。故①②今日无真实受害者,但契约句必须先说真话。
|
|
24
|
+
> 「另铸六词 schema 保旧 `MinTrust`」不采:等于第二张词表,正是 1.1.0「一枚举无第二词表」钉所禁,且 cli ① 点名要
|
|
25
|
+
> 在 `MinTrust` 加员。
|
|
26
|
+
|
|
27
|
+
### 阶梯序判定(成文理由,亲读坐标)
|
|
28
|
+
|
|
29
|
+
- 规范序 = v2-design §1:29(`sema-internal/core/research/settings-product/04-v2-design.md`)逐字:
|
|
30
|
+
`MANAGED 6 > GLOBAL 5 > PROJECT 4 > LOCAL 3 > SESSION 2 > TASK 1`。
|
|
31
|
+
- `task` 位**不是本仓臆定**:§1:29 本就含 `TASK 1`(最低信任端);§4:74 `KeyContract.minTrust` 词表本就列 `'task'`
|
|
32
|
+
——它**是** floor 词(1.1.0 JSDoc「`task` 不是 floor 词」为失真句,本版改述);§2:36 SESSION/TASK = 运行期 overlay
|
|
33
|
+
非文件、TASK 单 run 清、resume 不重放;§1:31 TASK 最高 precedence 但最低 trust(两序在用户端发散)。§5:96 resolver
|
|
34
|
+
伪码本就写作 `trustRank(L) < trustRank(contract.minTrust) → throw`——原语**名**亦有真源。
|
|
35
|
+
- **数值本身入契约**(不只是序):与 cli 自铸 `TRUST_RANK`(`src/sema/settings/keyContracts.ts:37-44`,managed 6 … task 1)
|
|
36
|
+
同值,壳删表改 import 后每次比较结果逐字节不变。
|
|
37
|
+
- 声明序仍非规范:`MinTrust.options` = 1.1.0 五词原序 + `task` 尾插;阶梯**只**经 `trustRank`,不索引枚举。
|
|
38
|
+
- **不建模** v2-design §6.4:138 的 `launch-only`(比 session/task 更窄的「仅本进程显式标志」):它是 `bypassPermissions`
|
|
39
|
+
的入口限制,不是阶梯词;将来需成词另案。
|
|
40
|
+
|
|
41
|
+
### API 形(全文)
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
export const MinTrust = z.enum(["local", "session", "project", "global", "managed", "task"]);
|
|
45
|
+
export function trustRank(layer: MinTrust): number; // 6/5/4/3/2/1,穷举 switch
|
|
46
|
+
export function isAtLeast(layer: MinTrust, minTrust: MinTrust): boolean; // trustRank(layer) >= trustRank(minTrust)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
- **fail-closed**:`trustRank` 是对 `MinTrust` 的穷举 `switch`——枚举加词漏 rank = **编译错**(`never` 臂);运行时未知词
|
|
50
|
+
(JS / 反序列化调用方喂 `__proto__` / `constructor` / 大写 provenance 层词 / `launch-only` …)= **`RangeError`**,永不
|
|
51
|
+
`undefined` / `NaN`(NaN 会让每次比较静默 `false`——方向恰好安全但不响,仍属静默臂)。`isAtLeast` 两参都过 `trustRank`。
|
|
52
|
+
- 既有成员级查询族(`membersOf` / `memberAdmission` / `memberMinTrust` / `resolveMemberAdmission`)**不含比较逻辑**,
|
|
53
|
+
无需改经;测试侧原本地 `LADDER` 表删除,改用 `isAtLeast`(单源两用);`memberMinTrust` JSDoc 的
|
|
54
|
+
`rank(layer) ≥ rank(floor)` 改指 `isAtLeast`。
|
|
55
|
+
- `task` 今日**零**类目 / 成员行使用(冻结 1.1.0 schema 全表逐字节解析钉证——1.1.0 消费方永不遇到未知词);
|
|
56
|
+
`minTrust:"task"` = 最宽 floor(任何层含 task overlay 皆可 set),对 spec **值**合法(`MemberAdmission` / `MergeCategorySpec` 收)。
|
|
57
|
+
|
|
58
|
+
### 消费方接线
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
import { isAtLeast, memberMinTrust, membersOf } from "@sema-agent/settings-schema";
|
|
62
|
+
for (const m of membersOf("permissions")) {
|
|
63
|
+
if (!isAtLeast(layer.sourceTrust, memberMinTrust("permissions", m))) { /* loosen ⇒ 待确认建议;tighten 不会走到这 */ }
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
- cli:删自铸 `TRUST_RANK` 改 import(随 L-29 上收批同落,[5807] ③);四件套成员级门吃同一原语。1.1.0 段「本包尚未导出
|
|
68
|
+
排序原语」自本版起过期。
|
|
69
|
+
|
|
70
|
+
### 门
|
|
71
|
+
|
|
72
|
+
- 红先:11 条新钉 + 1 条改经原语的旧钉先红——运行时 10 红(`trustRank`/`isAtLeast is not a function` ×6、`task` 缺席 ×2、
|
|
73
|
+
声明序文本钉 ×1、单调钉 ×1),tsc 11 处红(缺导出 2 / `"task"` 不可赋 4 / `@ts-expect-error` 未命中 2 / 连带 3)→ 全绿。
|
|
74
|
+
- 变异六刀各红(主题文件 33 钉):M1 session/local 互换 → 4 红(逐字表 / 单调 / 真值表 / 准入集);M2 `isAtLeast` 用 `>` →
|
|
75
|
+
5 红(含 1.1.0 方向不变量钉——单源两用之证);M3 枚举去 `task`(switch 仍留)→ 5 红;M4 枚举重排(`task` 置首)→ 2 红
|
|
76
|
+
(尾插钉 + 文本钉,其余全绿=声明序确非阶梯);M5 未知词返 `NaN` 不抛 → 只红未知词钉(1/33,定点);M6 `task` 排 7 高于
|
|
77
|
+
managed → 5 红。源文件 sha256 复原逐字节一致。
|
|
78
|
+
- codex adversarial-review R1 三条:F1 [high]「加 `task` 非纯向后兼容」→ **部分采**:头句 / README 措辞订正为「表不动 ≠
|
|
79
|
+
消费方零感知」+ 消费方迁移句(上 ⚠️ 段);「另铸六词 schema 保旧 `MinTrust`」**驳回**(第二张词表);F2 [medium] 冻结
|
|
80
|
+
1.1.0 钉拿 `r.data` 对当前值 = 同义反复 → **采**:补独立 1.1.0 全表字面量夹具(commit `9e41eaf` 逐字),第七刀 M7
|
|
81
|
+
`hooks.minTrust` project→local **只红该钉**(1/33;修前此刀全绿);F3 [medium] README「无消费方自铸表」写成既成事实 →
|
|
82
|
+
**采**改述为迁移目标(cli 半场未落)。R1 自报 vitest 在其只读沙箱起不来(环境,非发现)。
|
|
83
|
+
- codex R2:F2 / F3 判**已解**;1.1.0 字面量判**独立且完整**(对 `9e41eaf` 逐值,16 类目);`trustRank` / `isAtLeast` /
|
|
84
|
+
JSDoc / 新钉**无新发现**。余 F1 一条 [high]:「加宽公共枚举按 minor 发,`^1.1.0` 消费方自动收到」→ 建议发 **2.0.0**
|
|
85
|
+
或另铸六词 schema。处置:另铸 schema **再驳**(第二张词表,理由同上);**major vs minor 是版号政策、不是代码缺陷**
|
|
86
|
+
——本批按 [5805]/[5807] 双方口径与任务书走 1.2.0,**候主会话 / clay 裁**;若裁 major,只需改 `package.json` /
|
|
87
|
+
`package-lock.json` 根版本 / 本段标题三处,代码零动。
|
|
88
|
+
- 全量 39 文件 / 621 用例绿(1.1.0 基线 610 + 11);tsc 0;build 绿(dist `.d.ts` 含两原语);dist 运行时冒烟
|
|
89
|
+
(`node -e import('./dist/index.js')`)六词/六序/未知词 RangeError 如述。
|
|
90
|
+
|
|
91
|
+
## 1.1.0 — `permissions` 成员级准入(权限双通道终裁四件套之④,clay 终裁 [5797];合流形 [5794])
|
|
92
|
+
|
|
93
|
+
**`SAFETY_MERGE_SPEC` 类目可声明 `members`(成员级 minTrust + 方向);首个也是当前唯一成员化的类目 = `permissions`
|
|
94
|
+
(`allow` / `deny` / `ask`,闭集)。纯 additive:类目级 `minTrust` 一个字不动,老消费方行为逐字节不变。**
|
|
95
|
+
|
|
96
|
+
### 键形定稿(全文)
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
permissions: {
|
|
100
|
+
mergeShape: "denyFirst", minTrust: "global", // 类目级 = 缺省兜底(不动)
|
|
101
|
+
tighten: { rule: "policyDenyWins" }, enforcement: "gate", fail: "closed", …,
|
|
102
|
+
members: {
|
|
103
|
+
allow: { direction: "loosen", minTrust: "global" }, // 放宽 = 特权:global 或托管层(managed 在其上)
|
|
104
|
+
deny: { direction: "tighten", minTrust: "local" }, // 收紧:任何 settings-file 启用层恒准入
|
|
105
|
+
ask: { direction: "tighten", minTrust: "local" }, // 收紧(「ask = 收紧」正式化为数据)
|
|
106
|
+
},
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
- `MergeCategorySpec` 新增可选字段 `members: Record<string, MemberAdmission>`;`MemberAdmission = { direction:
|
|
111
|
+
"loosen" | "tighten", minTrust?: MinTrust }`(zod 闭集:方向词/信任词外的值拒)。
|
|
112
|
+
- 新导出:`MemberDirection` / `MemberAdmission`(zod schema + 同名推导类型)、`ResolvedMemberAdmission`
|
|
113
|
+
(**仅类型**,interface,无运行时值)、`SafetyMergeMember<C>`(成员名闭集**类型**:`permissions` ⇒
|
|
114
|
+
`"allow"|"deny"|"ask"`,其余类目 ⇒ `never`,喊错成员名 = **编译错**)、`membersOf(category)`(成员名单源,
|
|
115
|
+
resolver 迭代它而不是自铸 allow/deny/ask 表)、`memberAdmission(category, member)` /
|
|
116
|
+
`memberMinTrust(category, member)`(准入查询,回落已应用)、`resolveMemberAdmission(specValue, member, label?)`
|
|
117
|
+
(对 spec **值**求解的唯一实现,前两者是它对 `SAFETY_MERGE_SPEC` 的绑定)。三个表查询对**未知类目**
|
|
118
|
+
(含 `__proto__`/`constructor` 等原型链名)与**未知成员**一律 `RangeError`——own-property 查表,不走原型链。
|
|
119
|
+
|
|
120
|
+
### 语义句(成文层,规范性)
|
|
121
|
+
|
|
122
|
+
1. **方向不对称**:`deny`/`ask` 只会**收紧**无人值守面——收窄无须信任,故**任何 settings-file 启用层恒准入**
|
|
123
|
+
(`minTrust:"local"` = settings 文件能承载的最低信任档;v2-design §1:session/task 是运行期 overlay 不是文件,
|
|
124
|
+
运行期授权走显式运行期通道,不在本表);与 `policyDenyWins` 同一方向律。`allow` 会**放宽**——自动放行是特权,
|
|
125
|
+
须 trust ≥ `global`(global 或 managed)。
|
|
126
|
+
2. **未达门槛的 allow 不静默丢**(终裁③):执法层把它降级为「待确认建议」走 `confirmRuleApproval` consent 流——
|
|
127
|
+
那个出口归**执法半场**(core `prepareCcImport` 单门 + `RuleAddOrigin` 出处;cli resolver/壳喂门);本表只说
|
|
128
|
+
**哪些成员需要它**(`direction:"loosen"`)。⚠️ **本包只发布契约元数据,不提供该行为**:本包不做准入判定、
|
|
129
|
+
不做降级。
|
|
130
|
+
**消费半场观察值**(亲读于发包日的邻仓 revision,是**观察**不是本包规范,会随邻仓发版过期;以邻仓自己的
|
|
131
|
+
CHANGELOG 为准):① 成员级查询函数的消费方 = **零**,矩阵有界如下(发包日 `grep SAFETY_MERGE_SPEC|safety-merge-spec|
|
|
132
|
+
memberMinTrust|membersOf(` 于各仓 `src/`):cli `1ef236af` 4 文件命中(`keyContracts.ts` / `resolver.ts` /
|
|
133
|
+
`safety-merge.conformance.test.ts` / `printModeEngine.ts`,全部只读类目级 `minTrust`);client-core `1606fd0` /
|
|
134
|
+
server `88925c5` / web-admin `42ac03d` / web-client `fb8f459` / desktop `9ce3736` / sdk `2e822f6` / test `56d09564`
|
|
135
|
+
零命中(cli 半场排 server 车D 后);② core
|
|
136
|
+
`a305f6a3`:`prepareCcImport`(`src/core/permission-rule-consent.ts:1441`)无成员/门槛入参、只导入 allow 桶,
|
|
137
|
+
且 v1 不读 `flagSettings` / `policySettings`(同文件 `IMPORT_UNCOVERED_LAYERS`);③ cli `1ef236af`:
|
|
138
|
+
`flagSettings`(`--settings`)映射为 `local` 信任(`src/sema/settings/consume88.ts:57-63`),resolver 仍按类目级
|
|
139
|
+
整键 trust-skip(`src/sema/settings/resolver.ts:515`)——即在消费半场接线前,`--settings` 里的 allow 既不准入也
|
|
140
|
+
进不了 consent 道。这是执法半场的已知缺口,随其接线批闭合(codex R1-F1 / R2-F3)。
|
|
141
|
+
3. **「ask = 收紧」正式化**:本表把 `ask` 与 `deny` 同标 `tighten`。观察值(core `a305f6a3`):`SessionPermissionRules`
|
|
142
|
+
无 ask 员(只有 toolAllow/toolDeny/allowDirs/commandAllow/commandDeny,`src/core/session-policy-store.ts:27-40`);
|
|
143
|
+
加员 = core 半场 additive,本仓只成文契约。
|
|
144
|
+
4. **类目级 `minTrust` = 缺省兜底**:成员行缺省 `minTrust` 回落类目级(**省略永远不会降低门槛**);不认识 `members`
|
|
145
|
+
的老消费方仍按类目级整键判 = 1.0.0 行为逐字节不变——不静默重解释类目级语义。
|
|
146
|
+
5. **安全下界**(终裁 [5797] 成文入册的 core 原则,本处**重述**以免读者从本表反推出相反结论;它是 core 的
|
|
147
|
+
保证、不是本包能证的):已准入的 allow 只能消 classifier ask,**消不了 mandated ask**(shellGate `"always"` /
|
|
148
|
+
org 策略 / requiresRealApproval)。
|
|
149
|
+
6. **未知成员拒(闭集)**:类型面编译错;运行时 `RangeError`(fail-closed、响亮),**不**回落类目级——静默回落正是
|
|
150
|
+
让拼错的桶按错规则准入的那条臂。无 `members` 的类目喊成员名同样拒(它没有成员可喊)。
|
|
151
|
+
|
|
152
|
+
### 消费方接线(cli resolver / 壳 / web 面板)
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
import { membersOf, memberMinTrust, memberAdmission } from "@sema-agent/settings-schema";
|
|
156
|
+
for (const m of membersOf("permissions")) { // ["allow","deny","ask"] —— 单源,不自铸
|
|
157
|
+
if (layer.permissions?.[m] === undefined) continue;
|
|
158
|
+
const floor = memberMinTrust("permissions", m); // allow→"global", deny/ask→"local"
|
|
159
|
+
if (rank(layer.sourceTrust) < rank(floor)) {
|
|
160
|
+
// memberAdmission("permissions", m).direction === "loosen" ⇒ 待确认建议(consent),不是静默丢
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
- `rank(...)` 仍是消费方自己的阶梯(v2-design §1 `managed > global > project > local > session > task`);本包
|
|
166
|
+
**尚未**导出排序原语——`MinTrust` 枚举的声明序在 session/local 处与 §1 倒置、且零消费方索引它(非规范),
|
|
167
|
+
排序原语随 [5796] 序题表态另车(本版只把该注释改说真话)。(→ 1.2.0 已导出 `trustRank` / `isAtLeast`。)
|
|
168
|
+
|
|
169
|
+
### 门
|
|
170
|
+
|
|
171
|
+
- 红先:12 条新钉先红(运行时 11 条 `is not a function` / `MemberDirection.options` undefined;编译期钉 1 条=tsc
|
|
172
|
+
`@ts-expect-error` 未命中;tsc 共 14 处红)再绿。
|
|
173
|
+
- 变异五刀各红(ask 反向 3 / deny 抬门槛 3 / 未知成员静默回落 1 / 去掉缺席回落 4 / allow 低于类目 floor 3),
|
|
174
|
+
源文件 shasum 复原字节一致。
|
|
175
|
+
- codex adversarial-review R1 四条全采:F1 成文措辞把执法半场的 consent 出口写得像本包已提供 → 改述(上 §2);
|
|
176
|
+
F2 `membersOf("__proto__")` 走原型链返回 `[]` = fail-open → own-property 类目守卫 + 红先钉(六个坏类目名);
|
|
177
|
+
F3 「老消费方行为不变」无冻结钉 → 加 1.0.0 字面量冻结钉 + 冻结 1.0.0 schema 解析全表;F4 `ResolvedMemberAdmission`
|
|
178
|
+
误标 zod 导出 → 改「仅类型」。
|
|
179
|
+
- codex R2 三条全采:F1 [high] `SafetyMergeMember<C>` 非分配式——resolver 手里的宽类型 `SafetyMergeCategory` 代入后
|
|
180
|
+
塌成 `never`(`membersOf(cat)` 变 `readonly never[]`,真接缝处被迫 cast)→ 改分配式条件类型 + 类型**等式**红钉
|
|
181
|
+
(`never[]` 可赋给任何数组,可赋性钉抓不到,故用等式);F2 `MemberDirection` JSDoc 把 consent 降级说成无条件运行时
|
|
182
|
+
行为 → 改「裁定要求消费方如此处理;本包只返回元数据」,README 同步;F3 跨仓事实未钉 revision → 逐条钉亲读 revision
|
|
183
|
+
并标「观察值 ≠ 本包规范」。
|
|
184
|
+
- codex R3 两条全采:F1 头注「行为由 mergeShape+minTrust+tighten+enforcement 完全决定」漏 `members`/`fail` → 补;
|
|
185
|
+
F2 「无任何消费方接线」全称句无边界 → 改逐仓 revision 钉的有界矩阵(上 §2 ①)。
|
|
186
|
+
- 变异再两刀:M6 `permissions.seam` 串漂移 → 只红冻结钉(1/21);M7 拆掉类目 own-property 守卫 → 只红原型链类目钉
|
|
187
|
+
(1/21);复原字节一致。
|
|
188
|
+
- 全量 + tsc + build + e2e 绿(读数见发版帖)。
|
|
189
|
+
|
|
9
190
|
## 1.0.0 — 改名(#361 步①;clay 裁 [5156],开工帖 [5295])
|
|
10
191
|
|
|
11
192
|
**`@sema-agent/registry-core` → `@sema-agent/settings-schema`。纯改名件。**
|
|
@@ -128,7 +309,7 @@ process.exit(tok.length === 0 && erased.length === 0 ? 0 : 1);
|
|
|
128
309
|
|
|
129
310
|
### 仓名不动
|
|
130
311
|
|
|
131
|
-
|
|
312
|
+
本步只改**包名**。(追记 2026-08-26:仓名亦已改 `sema-settings-schema`,旧 `sema-registry-core` URL 由 GitHub 自动重定向,`repository.url` 已同步——clay 令当日兑现。)仓名 `sema-registry-core` / `github.com/sema-agent/sema-registry-core` 以及
|
|
132
313
|
`package.json` 的 `repository.url` **原样保留**;src 注释里指代本仓/本项目的裸词 `registry-core`
|
|
133
314
|
同样保留(它们不是包名)。
|
|
134
315
|
|
package/README.md
CHANGED
|
@@ -28,7 +28,7 @@ server (the workers), and the client (desktop/CLI) seams. No two copies of the s
|
|
|
28
28
|
> 本**包**名与产品名解耦,**不随产品改名而动**(1.0.0 这次改名是本包自身的正名,与产品名无关)——
|
|
29
29
|
> 下文凡提「center / config-center 产品」即指该产品的当期实现。
|
|
30
30
|
|
|
31
|
-
- **Source of truth**: `github.com/sema-agent/sema-
|
|
31
|
+
- **Source of truth**: `github.com/sema-agent/sema-settings-schema`, branch `main` (private org repo; renamed from `sema-registry-core` on 2026-08-26, old URLs redirect).
|
|
32
32
|
仓名本步不动(只改包名)。
|
|
33
33
|
- **npm**: published as `@sema-agent/settings-schema` on the public npm registry
|
|
34
34
|
(≤`0.19.0` 发在旧名 `@sema-agent/registry-core` 下,冻结)。
|
|
@@ -58,6 +58,25 @@ sound). `npm ls --prod` therefore shows only `zod`. Node built-ins only (`node:c
|
|
|
58
58
|
`extends` it; a local file source / test fixture implements it without stubbing ~32 methods.
|
|
59
59
|
- **`remoteExec` contract** (`remote-exec.ts`): the execution-substrate schema mirroring the service's
|
|
60
60
|
actual providers (`e2b` / `k8s` / `ssh` / `adb`), discriminated on `provider`; every secret an env-NAME ref.
|
|
61
|
+
- **Safety merge-spec** (`safety-merge-spec.ts`, subpath `./safety-merge-spec`): `SAFETY_MERGE_SPEC`, the ONE
|
|
62
|
+
per-category merge/enforcement table every settings resolver reads (`mergeShape` / `minTrust` / `tighten` /
|
|
63
|
+
`enforcement` / `fail`) — no consumer re-implements the merge law. 1.1.0 adds **member-level admission**:
|
|
64
|
+
a category may declare `members`, each with its own `minTrust` floor and `direction` (`loosen` = a privilege,
|
|
65
|
+
needs trust; `tighten` = only narrows, any settings-file layer may add it). Today only `permissions` does —
|
|
66
|
+
`allow` (loosen @ `global`) / `deny` / `ask` (tighten @ `local`), a closed set; read it via `membersOf` /
|
|
67
|
+
`memberMinTrust` / `memberAdmission` (never a hardcoded allow/deny/ask table). The category-level `minTrust`
|
|
68
|
+
stays the default floor, so consumers that predate `members` behave exactly as before. This package publishes
|
|
69
|
+
the contract only: it performs no admission check and no "degrade a below-floor allow to a pending suggestion"
|
|
70
|
+
step — those live in the consumers (engine import door, shell resolver), each on its own release schedule.
|
|
71
|
+
1.2.0 adds the **trust ordering primitives**: `trustRank(layer)` (v2-design §1 verbatim, `managed 6 > global 5 >
|
|
72
|
+
project 4 > local 3 > session 2 > task 1`) and `isAtLeast(layer, minTrust)` — the ladder every resolver is to
|
|
73
|
+
compare floors against; consumers retire their own rank tables onto it (the CLI's `TRUST_RANK` migration is
|
|
74
|
+
pending on its side, board [5807] ③ — exporting the primitive does not by itself make it the single source).
|
|
75
|
+
`MinTrust` gains the `task` word (the runtime task overlay, the loosest floor; appended, declaration order
|
|
76
|
+
otherwise unchanged — the declaration order is not the ladder, `trustRank` is). The published table does not
|
|
77
|
+
use `task`; the widened vocabulary is visible to consumers that hold a `Record<MinTrust, …>` / exhaustive
|
|
78
|
+
switch (compile error on upgrade — migrate to `trustRank`) or that validate external spec values with these
|
|
79
|
+
schemas (they now accept `task` and must rank it — again `trustRank`, never a five-word table of their own).
|
|
61
80
|
- **Auth wire contract** (`api/auth.ts`, subpath `./api/auth`): the `/api/v1/auth/*` face — RFC 8628
|
|
62
81
|
device flow (code / token poll / approve consent) + rotating refresh + logout, the OAuth error envelope,
|
|
63
82
|
and the pure client helpers `normalizeUserCode` / `nextPollInterval`. Shipped M1 shapes frozen.
|
|
@@ -123,7 +142,7 @@ defaults. Each `config.d/<domain>.json` is exactly that domain's zod object.
|
|
|
123
142
|
|
|
124
143
|
## 贡献指引
|
|
125
144
|
|
|
126
|
-
- 真源 = `sema-agent/sema-
|
|
145
|
+
- 真源 = `sema-agent/sema-settings-schema` org 仓 `main`(2026-08-26 由 sema-registry-core 改名),所有变更直接落 org main(或 PR 进 main);
|
|
127
146
|
不再向任何个人位/镜像位 push。
|
|
128
147
|
- 提交门 = `npm run build` + `npx vitest run`;改域/契约后必跑 `npm run e2e`(examples/toc-e2e.mjs)。
|
|
129
148
|
- 契约破坏面动之前先盘 center / server / CLI 三方消费点;0.x 破坏性变更必 bump minor(见上节)。
|
|
@@ -7,16 +7,100 @@
|
|
|
7
7
|
* import). Each consumer keeps its OWN key→category map (client's SemaSettings KEY_CONTRACTS, center's
|
|
8
8
|
* config-field→category map); THIS file is only the shared category spec they all cite — no third copy of the law.
|
|
9
9
|
*
|
|
10
|
-
* Per-category behavior is fully determined by `mergeShape` + `minTrust` + `tighten` + `enforcement
|
|
11
|
-
*
|
|
10
|
+
* Per-category behavior is fully determined by `mergeShape` + `minTrust` + `tighten` + `enforcement` + `fail` — and,
|
|
11
|
+
* for a MEMBERIZED category (one that declares `members`, 1.1.0), by its member rows: each member's own floor and
|
|
12
|
+
* direction override the category `minTrust` for that member; anything a consumer maps into the category that is
|
|
13
|
+
* NOT a declared member stays under the category `minTrust`, whole-key. A WHOLE-KEY category (no `members`) is
|
|
14
|
+
* admitted or rejected as one value under its `minTrust`. A resolver reads this table and has NO per-key
|
|
15
|
+
* special-casing.
|
|
16
|
+
*
|
|
17
|
+
* MEMBER-LEVEL ADMISSION (1.1.0 — the WRITTEN layer of the permission-rule dual-channel ruling, board [5797] ④;
|
|
18
|
+
* the executing layer is core's `prepareCcImport` door + `RuleAddOrigin`, the resolver/shell halves are cli's):
|
|
19
|
+
* A category may declare `members`, each with its own admission floor and DIRECTION. Today only `permissions`
|
|
20
|
+
* does — `allow` / `deny` / `ask`, a CLOSED set ({@link SafetyMergeMember}; an unknown member is a compile error
|
|
21
|
+
* and a loud runtime error, never a fallback). The law is direction-asymmetric:
|
|
22
|
+
* - `tighten` members (`deny`, `ask`) only ever NARROW the unattended surface. Restricting needs no trust, so
|
|
23
|
+
* they are admitted from EVERY settings-file layer: `minTrust:"local"` is the lowest trust tier a settings
|
|
24
|
+
* file can carry (v2-design §1: session/task are runtime overlays, never files — a runtime grant rides the
|
|
25
|
+
* explicit runtime channels, outside this table). This is the same direction law as `policyDenyWins`.
|
|
26
|
+
* `ask` is formally a tightening word HERE even though core's `SessionPermissionRules` has no ask bucket yet
|
|
27
|
+
* (toolAllow/toolDeny/allowDirs/commandAllow/commandDeny only) — adding one is core's additive half.
|
|
28
|
+
* - `loosen` members (`allow`) WIDEN it. Automatic approval is a privilege, so they need trust ≥ `global`
|
|
29
|
+
* (global or managed). A layer below the floor does NOT get its allow rules admitted. The ruling's outlet
|
|
30
|
+
* for such rules — degrade to a pending suggestion behind the consent flow (`confirmRuleApproval`) rather
|
|
31
|
+
* than drop silently — is the EXECUTING layers' contract (core's import door + cli's resolver/shell), not
|
|
32
|
+
* this package's: this table only says WHICH members need that outlet (`direction:"loosen"`). Whether a
|
|
33
|
+
* given consumer version routes them there is that consumer's changelog, never implied by this table.
|
|
34
|
+
* The category-level `minTrust` stays as the DEFAULT floor: a member row without its own `minTrust` inherits it
|
|
35
|
+
* (omission can never lower a floor), and a consumer that predates `members` keeps enforcing it on the whole key
|
|
36
|
+
* — byte-for-byte the pre-1.1.0 behaviour, so the addition is additive and never silently reinterprets the
|
|
37
|
+
* category-level word. Safety lower bound (core's, restated so no reader infers otherwise): an admitted allow
|
|
38
|
+
* silences classifier asks only; it can never silence a MANDATED ask (shellGate "always", org policy,
|
|
39
|
+
* requiresRealApproval). Consumers read admission through {@link memberAdmission} / {@link memberMinTrust} /
|
|
40
|
+
* {@link membersOf} — never through a hardcoded allow/deny/ask table of their own (single source).
|
|
41
|
+
*
|
|
42
|
+
* TRUST ORDERING PRIMITIVES (1.2.0 — board [5796] ordering question → [5805] claim / [5807] consumer statement):
|
|
43
|
+
* The floor words are only meaningful against a LADDER, and that ladder is v2-design §1:29, verbatim:
|
|
44
|
+
* `MANAGED 6 > GLOBAL 5 > PROJECT 4 > LOCAL 3 > SESSION 2 > TASK 1`. {@link trustRank} publishes exactly those
|
|
45
|
+
* numbers and {@link isAtLeast} the comparison a resolver makes (`rank(layer) ≥ rank(floor)` ⇒ admitted), so no
|
|
46
|
+
* consumer keeps a rank table of its own (cli's `TRUST_RANK` retires onto these). `task` joins {@link MinTrust}
|
|
47
|
+
* (v2-design §4:74 lists it in `KeyContract.minTrust`; §2:36 — session/task are runtime overlays, never files):
|
|
48
|
+
* the loosest floor, "any layer including the task overlay may set this". No row of {@link SAFETY_MERGE_SPEC}
|
|
49
|
+
* uses it today (the frozen-1.1.0 conformance pin holds that). The enum's DECLARATION ORDER stays what 1.1.0
|
|
50
|
+
* published, `task` appended — reordering would be breaking ([5807] ②) and the declaration order is NOT the
|
|
51
|
+
* ladder anyway; only {@link trustRank} is.
|
|
12
52
|
*/
|
|
13
53
|
import { z } from "zod";
|
|
14
54
|
/** How values from different layers/principals combine for a category. */
|
|
15
55
|
export declare const MergeShape: z.ZodEnum<["denyFirst", "replace", "runAll", "overrideByName", "concat"]>;
|
|
16
56
|
export type MergeShape = z.infer<typeof MergeShape>;
|
|
17
|
-
/** The
|
|
18
|
-
|
|
57
|
+
/** The trust-tier VOCABULARY a category — or, since 1.1.0, one of its members — names as its admission floor
|
|
58
|
+
* (the MINIMUM trust a layer/principal must have to set it). The RANK is v2-design §1:29,
|
|
59
|
+
* `managed 6 > global 5 > project 4 > local 3 > session 2 > task 1`, published as {@link trustRank}; compare with
|
|
60
|
+
* {@link isAtLeast}. `task` (1.2.0) is the runtime task overlay — the lowest trust, the loosest floor.
|
|
61
|
+
* ⚠️ This enum's DECLARATION ORDER is NOT the rank (it is the 1.1.0 order with `task` appended — kept byte-stable
|
|
62
|
+
* because reordering is breaking); never index it as a ladder — use {@link trustRank}. */
|
|
63
|
+
export declare const MinTrust: z.ZodEnum<["local", "session", "project", "global", "managed", "task"]>;
|
|
19
64
|
export type MinTrust = z.infer<typeof MinTrust>;
|
|
65
|
+
/** The trust RANK of a floor / layer word — v2-design §1:29 verbatim (`MANAGED 6 > GLOBAL 5 > PROJECT 4 > LOCAL 3 >
|
|
66
|
+
* SESSION 2 > TASK 1`), the ONE published ladder every resolver compares against (1.2.0). Higher = more trusted.
|
|
67
|
+
* The numbers themselves are contract (a consumer may persist or log them), not merely their order.
|
|
68
|
+
* Exhaustive over {@link MinTrust}: a word added to the enum without a rank is a COMPILE error here; a word that
|
|
69
|
+
* is not in the enum at runtime (a JavaScript / deserialized caller) is a loud RangeError — never `undefined` or
|
|
70
|
+
* `NaN`, which would turn every comparison into a silent `false`. */
|
|
71
|
+
export declare function trustRank(layer: MinTrust): number;
|
|
72
|
+
/** `trustRank(layer) ≥ trustRank(minTrust)` — "is a source at `layer` trusted enough to set a key / member whose
|
|
73
|
+
* floor is `minTrust`?" (1.2.0). The comparison a resolver's admission check is; reflexive (a floor admits its
|
|
74
|
+
* own tier). Both arguments are validated by {@link trustRank} (unknown word ⇒ RangeError). */
|
|
75
|
+
export declare function isAtLeast(layer: MinTrust, minTrust: MinTrust): boolean;
|
|
76
|
+
/** Which way a MEMBER moves the engine's unattended surface (1.1.0). `loosen` = admits an action without a human
|
|
77
|
+
* that would otherwise ask/deny — a privilege, so it needs trust; `tighten` = only ever narrows — restricting is
|
|
78
|
+
* safe from anyone, so any settings-file layer may add it. The word is what a consumer branches on for the
|
|
79
|
+
* NOT-admitted outlet: the ruling REQUIRES a consumer to turn a `loosen` member below its floor into a pending
|
|
80
|
+
* suggestion (consent flow) rather than drop it silently. This package returns that metadata only — it performs
|
|
81
|
+
* no admission and no degradation; whether a given consumer version implements the outlet is that consumer's
|
|
82
|
+
* contract. */
|
|
83
|
+
export declare const MemberDirection: z.ZodEnum<["loosen", "tighten"]>;
|
|
84
|
+
export type MemberDirection = z.infer<typeof MemberDirection>;
|
|
85
|
+
/** One member's admission row. `minTrust` ABSENT ⇒ the category's `minTrust` applies: a row can refine the floor
|
|
86
|
+
* only by SAYING so; omission never lowers it (fail-closed by construction). */
|
|
87
|
+
export declare const MemberAdmission: z.ZodObject<{
|
|
88
|
+
direction: z.ZodEnum<["loosen", "tighten"]>;
|
|
89
|
+
minTrust: z.ZodOptional<z.ZodEnum<["local", "session", "project", "global", "managed", "task"]>>;
|
|
90
|
+
}, "strip", z.ZodTypeAny, {
|
|
91
|
+
direction: "loosen" | "tighten";
|
|
92
|
+
minTrust?: "local" | "session" | "global" | "project" | "managed" | "task" | undefined;
|
|
93
|
+
}, {
|
|
94
|
+
direction: "loosen" | "tighten";
|
|
95
|
+
minTrust?: "local" | "session" | "global" | "project" | "managed" | "task" | undefined;
|
|
96
|
+
}>;
|
|
97
|
+
export type MemberAdmission = z.infer<typeof MemberAdmission>;
|
|
98
|
+
/** The RESOLVED admission a consumer branches on — the category fallback already applied, so `minTrust` is never
|
|
99
|
+
* absent here. Produced by {@link resolveMemberAdmission} / {@link memberAdmission}. */
|
|
100
|
+
export interface ResolvedMemberAdmission {
|
|
101
|
+
readonly direction: MemberDirection;
|
|
102
|
+
readonly minTrust: MinTrust;
|
|
103
|
+
}
|
|
20
104
|
/** WHERE the category is enforced (STRENGTH) — MUST stay in lockstep with `EffectiveKey.enforcement` (types.ts).
|
|
21
105
|
* gate=data-plane engine, server=control-plane/TOB boundary, resolver=merger correctness, client=UI only. */
|
|
22
106
|
export declare const SpecEnforcement: z.ZodEnum<["gate", "server", "resolver", "client"]>;
|
|
@@ -68,7 +152,7 @@ export declare const TightenRule: z.ZodDiscriminatedUnion<"rule", [z.ZodObject<{
|
|
|
68
152
|
export type TightenRule = z.infer<typeof TightenRule>;
|
|
69
153
|
export declare const MergeCategorySpec: z.ZodObject<{
|
|
70
154
|
mergeShape: z.ZodEnum<["denyFirst", "replace", "runAll", "overrideByName", "concat"]>;
|
|
71
|
-
minTrust: z.ZodEnum<["local", "session", "project", "global", "managed"]>;
|
|
155
|
+
minTrust: z.ZodEnum<["local", "session", "project", "global", "managed", "task"]>;
|
|
72
156
|
tighten: z.ZodDiscriminatedUnion<"rule", [z.ZodObject<{
|
|
73
157
|
rule: z.ZodLiteral<"none">;
|
|
74
158
|
}, "strip", z.ZodTypeAny, {
|
|
@@ -112,10 +196,21 @@ export declare const MergeCategorySpec: z.ZodObject<{
|
|
|
112
196
|
seam: z.ZodString;
|
|
113
197
|
/** When the category's VALUE domain is core's, the cited `SAFETY_AXIS_VOCABULARY` dimension (conformance-checked). */
|
|
114
198
|
coreAxis: z.ZodOptional<z.ZodEnum<["toolEffect", "egress", "irreversibility", "safetyAxis", "severity", "shellGate", "permissionDecision"]>>;
|
|
199
|
+
/** 1.1.0 — member-level admission (see the file header). ABSENT ⇒ the category is a whole key: its `minTrust`
|
|
200
|
+
* admits or rejects the whole value. PRESENT ⇒ each named member is admitted on its own row (fallback to the
|
|
201
|
+
* category `minTrust` when the row omits one); the member set is closed — {@link membersOf} enumerates it. */
|
|
202
|
+
members: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
203
|
+
direction: z.ZodEnum<["loosen", "tighten"]>;
|
|
204
|
+
minTrust: z.ZodOptional<z.ZodEnum<["local", "session", "project", "global", "managed", "task"]>>;
|
|
205
|
+
}, "strip", z.ZodTypeAny, {
|
|
206
|
+
direction: "loosen" | "tighten";
|
|
207
|
+
minTrust?: "local" | "session" | "global" | "project" | "managed" | "task" | undefined;
|
|
208
|
+
}, {
|
|
209
|
+
direction: "loosen" | "tighten";
|
|
210
|
+
minTrust?: "local" | "session" | "global" | "project" | "managed" | "task" | undefined;
|
|
211
|
+
}>>>;
|
|
115
212
|
}, "strip", z.ZodTypeAny, {
|
|
116
213
|
enforcement: "gate" | "server" | "resolver" | "client";
|
|
117
|
-
mergeShape: "concat" | "replace" | "denyFirst" | "runAll" | "overrideByName";
|
|
118
|
-
minTrust: "local" | "session" | "global" | "project" | "managed";
|
|
119
214
|
tighten: {
|
|
120
215
|
rule: "none";
|
|
121
216
|
} | {
|
|
@@ -129,13 +224,17 @@ export declare const MergeCategorySpec: z.ZodObject<{
|
|
|
129
224
|
rule: "enumRank";
|
|
130
225
|
order: string[];
|
|
131
226
|
};
|
|
227
|
+
minTrust: "local" | "session" | "global" | "project" | "managed" | "task";
|
|
228
|
+
mergeShape: "concat" | "replace" | "denyFirst" | "runAll" | "overrideByName";
|
|
132
229
|
fail: "closed" | "open";
|
|
133
230
|
seam: string;
|
|
231
|
+
members?: Record<string, {
|
|
232
|
+
direction: "loosen" | "tighten";
|
|
233
|
+
minTrust?: "local" | "session" | "global" | "project" | "managed" | "task" | undefined;
|
|
234
|
+
}> | undefined;
|
|
134
235
|
coreAxis?: "permissionDecision" | "toolEffect" | "egress" | "irreversibility" | "safetyAxis" | "severity" | "shellGate" | undefined;
|
|
135
236
|
}, {
|
|
136
237
|
enforcement: "gate" | "server" | "resolver" | "client";
|
|
137
|
-
mergeShape: "concat" | "replace" | "denyFirst" | "runAll" | "overrideByName";
|
|
138
|
-
minTrust: "local" | "session" | "global" | "project" | "managed";
|
|
139
238
|
tighten: {
|
|
140
239
|
rule: "none";
|
|
141
240
|
} | {
|
|
@@ -149,8 +248,14 @@ export declare const MergeCategorySpec: z.ZodObject<{
|
|
|
149
248
|
rule: "enumRank";
|
|
150
249
|
order: string[];
|
|
151
250
|
};
|
|
251
|
+
minTrust: "local" | "session" | "global" | "project" | "managed" | "task";
|
|
252
|
+
mergeShape: "concat" | "replace" | "denyFirst" | "runAll" | "overrideByName";
|
|
152
253
|
fail: "closed" | "open";
|
|
153
254
|
seam: string;
|
|
255
|
+
members?: Record<string, {
|
|
256
|
+
direction: "loosen" | "tighten";
|
|
257
|
+
minTrust?: "local" | "session" | "global" | "project" | "managed" | "task" | undefined;
|
|
258
|
+
}> | undefined;
|
|
154
259
|
coreAxis?: "permissionDecision" | "toolEffect" | "egress" | "irreversibility" | "safetyAxis" | "severity" | "shellGate" | undefined;
|
|
155
260
|
}>;
|
|
156
261
|
export type MergeCategorySpec = z.infer<typeof MergeCategorySpec>;
|
|
@@ -167,6 +272,20 @@ export declare const SAFETY_MERGE_SPEC: {
|
|
|
167
272
|
readonly fail: "closed";
|
|
168
273
|
readonly seam: "toolPolicy + deny-narrowing (SessionPolicyStore)";
|
|
169
274
|
readonly coreAxis: "permissionDecision";
|
|
275
|
+
readonly members: {
|
|
276
|
+
readonly allow: {
|
|
277
|
+
readonly direction: "loosen";
|
|
278
|
+
readonly minTrust: "global";
|
|
279
|
+
};
|
|
280
|
+
readonly deny: {
|
|
281
|
+
readonly direction: "tighten";
|
|
282
|
+
readonly minTrust: "local";
|
|
283
|
+
};
|
|
284
|
+
readonly ask: {
|
|
285
|
+
readonly direction: "tighten";
|
|
286
|
+
readonly minTrust: "local";
|
|
287
|
+
};
|
|
288
|
+
};
|
|
170
289
|
};
|
|
171
290
|
readonly permissionMode: {
|
|
172
291
|
readonly mergeShape: "replace";
|
|
@@ -325,3 +444,30 @@ export declare const SAFETY_MERGE_SPEC: {
|
|
|
325
444
|
};
|
|
326
445
|
};
|
|
327
446
|
export type SafetyMergeCategory = keyof typeof SAFETY_MERGE_SPEC;
|
|
447
|
+
/** The members a category declares, as a CLOSED SET TYPE: `"allow" | "deny" | "ask"` for `permissions`, `never` for
|
|
448
|
+
* every whole-key category — so naming a member the contract does not know is a COMPILE error at the call site.
|
|
449
|
+
* DISTRIBUTIVE over `C` on purpose: a resolver holds a WIDENED `SafetyMergeCategory` (out of its key→category
|
|
450
|
+
* map), and a non-distributive form would index the whole union at once, fail the `members` check on the
|
|
451
|
+
* whole-key categories and collapse to `never` — typing `membersOf(category)` as `readonly never[]` at exactly
|
|
452
|
+
* the seam these helpers exist for (codex R2-F1). */
|
|
453
|
+
export type SafetyMergeMember<C extends SafetyMergeCategory> = C extends SafetyMergeCategory ? (typeof SAFETY_MERGE_SPEC)[C] extends {
|
|
454
|
+
readonly members: infer M;
|
|
455
|
+
} ? Extract<keyof M, string> : never : never;
|
|
456
|
+
/** The member names a category declares, in declaration order; `[]` for a whole-key category. A resolver iterates
|
|
457
|
+
* THIS (never its own allow/deny/ask list) so the member set has one source. Unknown category ⇒ RangeError. */
|
|
458
|
+
export declare function membersOf<C extends SafetyMergeCategory>(category: C): readonly SafetyMergeMember<C>[];
|
|
459
|
+
/**
|
|
460
|
+
* Resolve one member's admission off a category spec VALUE (the single implementation; {@link memberAdmission}
|
|
461
|
+
* binds it to {@link SAFETY_MERGE_SPEC}). Fallback law: a member row without `minTrust` inherits the category's.
|
|
462
|
+
* Fail-CLOSED and loud on both misuse arms — a category without `members` has no member to name, and a member
|
|
463
|
+
* outside the declared set is a caller bug, not "use the category floor" (that silent arm is exactly what would
|
|
464
|
+
* let a typo admit a bucket under the wrong rule).
|
|
465
|
+
* @param categoryLabel only decorates the error message (the spec value carries no name of its own).
|
|
466
|
+
*/
|
|
467
|
+
export declare function resolveMemberAdmission(spec: MergeCategorySpec, member: string, categoryLabel?: string): ResolvedMemberAdmission;
|
|
468
|
+
/** A member's resolved admission (direction + floor, category fallback applied) from the published table.
|
|
469
|
+
* Unknown category or member ⇒ RangeError (never a fallback). */
|
|
470
|
+
export declare function memberAdmission<C extends SafetyMergeCategory>(category: C, member: SafetyMergeMember<C>): ResolvedMemberAdmission;
|
|
471
|
+
/** A member's admission FLOOR from the published table — the word a resolver compares the layer's trust against
|
|
472
|
+
* (`isAtLeast(layer, floor)` ⇒ admitted; the ladder is {@link trustRank}). Convenience over {@link memberAdmission}. */
|
|
473
|
+
export declare function memberMinTrust<C extends SafetyMergeCategory>(category: C, member: SafetyMergeMember<C>): MinTrust;
|
|
@@ -7,14 +7,99 @@
|
|
|
7
7
|
* import). Each consumer keeps its OWN key→category map (client's SemaSettings KEY_CONTRACTS, center's
|
|
8
8
|
* config-field→category map); THIS file is only the shared category spec they all cite — no third copy of the law.
|
|
9
9
|
*
|
|
10
|
-
* Per-category behavior is fully determined by `mergeShape` + `minTrust` + `tighten` + `enforcement
|
|
11
|
-
*
|
|
10
|
+
* Per-category behavior is fully determined by `mergeShape` + `minTrust` + `tighten` + `enforcement` + `fail` — and,
|
|
11
|
+
* for a MEMBERIZED category (one that declares `members`, 1.1.0), by its member rows: each member's own floor and
|
|
12
|
+
* direction override the category `minTrust` for that member; anything a consumer maps into the category that is
|
|
13
|
+
* NOT a declared member stays under the category `minTrust`, whole-key. A WHOLE-KEY category (no `members`) is
|
|
14
|
+
* admitted or rejected as one value under its `minTrust`. A resolver reads this table and has NO per-key
|
|
15
|
+
* special-casing.
|
|
16
|
+
*
|
|
17
|
+
* MEMBER-LEVEL ADMISSION (1.1.0 — the WRITTEN layer of the permission-rule dual-channel ruling, board [5797] ④;
|
|
18
|
+
* the executing layer is core's `prepareCcImport` door + `RuleAddOrigin`, the resolver/shell halves are cli's):
|
|
19
|
+
* A category may declare `members`, each with its own admission floor and DIRECTION. Today only `permissions`
|
|
20
|
+
* does — `allow` / `deny` / `ask`, a CLOSED set ({@link SafetyMergeMember}; an unknown member is a compile error
|
|
21
|
+
* and a loud runtime error, never a fallback). The law is direction-asymmetric:
|
|
22
|
+
* - `tighten` members (`deny`, `ask`) only ever NARROW the unattended surface. Restricting needs no trust, so
|
|
23
|
+
* they are admitted from EVERY settings-file layer: `minTrust:"local"` is the lowest trust tier a settings
|
|
24
|
+
* file can carry (v2-design §1: session/task are runtime overlays, never files — a runtime grant rides the
|
|
25
|
+
* explicit runtime channels, outside this table). This is the same direction law as `policyDenyWins`.
|
|
26
|
+
* `ask` is formally a tightening word HERE even though core's `SessionPermissionRules` has no ask bucket yet
|
|
27
|
+
* (toolAllow/toolDeny/allowDirs/commandAllow/commandDeny only) — adding one is core's additive half.
|
|
28
|
+
* - `loosen` members (`allow`) WIDEN it. Automatic approval is a privilege, so they need trust ≥ `global`
|
|
29
|
+
* (global or managed). A layer below the floor does NOT get its allow rules admitted. The ruling's outlet
|
|
30
|
+
* for such rules — degrade to a pending suggestion behind the consent flow (`confirmRuleApproval`) rather
|
|
31
|
+
* than drop silently — is the EXECUTING layers' contract (core's import door + cli's resolver/shell), not
|
|
32
|
+
* this package's: this table only says WHICH members need that outlet (`direction:"loosen"`). Whether a
|
|
33
|
+
* given consumer version routes them there is that consumer's changelog, never implied by this table.
|
|
34
|
+
* The category-level `minTrust` stays as the DEFAULT floor: a member row without its own `minTrust` inherits it
|
|
35
|
+
* (omission can never lower a floor), and a consumer that predates `members` keeps enforcing it on the whole key
|
|
36
|
+
* — byte-for-byte the pre-1.1.0 behaviour, so the addition is additive and never silently reinterprets the
|
|
37
|
+
* category-level word. Safety lower bound (core's, restated so no reader infers otherwise): an admitted allow
|
|
38
|
+
* silences classifier asks only; it can never silence a MANDATED ask (shellGate "always", org policy,
|
|
39
|
+
* requiresRealApproval). Consumers read admission through {@link memberAdmission} / {@link memberMinTrust} /
|
|
40
|
+
* {@link membersOf} — never through a hardcoded allow/deny/ask table of their own (single source).
|
|
41
|
+
*
|
|
42
|
+
* TRUST ORDERING PRIMITIVES (1.2.0 — board [5796] ordering question → [5805] claim / [5807] consumer statement):
|
|
43
|
+
* The floor words are only meaningful against a LADDER, and that ladder is v2-design §1:29, verbatim:
|
|
44
|
+
* `MANAGED 6 > GLOBAL 5 > PROJECT 4 > LOCAL 3 > SESSION 2 > TASK 1`. {@link trustRank} publishes exactly those
|
|
45
|
+
* numbers and {@link isAtLeast} the comparison a resolver makes (`rank(layer) ≥ rank(floor)` ⇒ admitted), so no
|
|
46
|
+
* consumer keeps a rank table of its own (cli's `TRUST_RANK` retires onto these). `task` joins {@link MinTrust}
|
|
47
|
+
* (v2-design §4:74 lists it in `KeyContract.minTrust`; §2:36 — session/task are runtime overlays, never files):
|
|
48
|
+
* the loosest floor, "any layer including the task overlay may set this". No row of {@link SAFETY_MERGE_SPEC}
|
|
49
|
+
* uses it today (the frozen-1.1.0 conformance pin holds that). The enum's DECLARATION ORDER stays what 1.1.0
|
|
50
|
+
* published, `task` appended — reordering would be breaking ([5807] ②) and the declaration order is NOT the
|
|
51
|
+
* ladder anyway; only {@link trustRank} is.
|
|
12
52
|
*/
|
|
13
53
|
import { z } from "zod";
|
|
14
54
|
/** How values from different layers/principals combine for a category. */
|
|
15
55
|
export const MergeShape = z.enum(["denyFirst", "replace", "runAll", "overrideByName", "concat"]);
|
|
16
|
-
/** The
|
|
17
|
-
|
|
56
|
+
/** The trust-tier VOCABULARY a category — or, since 1.1.0, one of its members — names as its admission floor
|
|
57
|
+
* (the MINIMUM trust a layer/principal must have to set it). The RANK is v2-design §1:29,
|
|
58
|
+
* `managed 6 > global 5 > project 4 > local 3 > session 2 > task 1`, published as {@link trustRank}; compare with
|
|
59
|
+
* {@link isAtLeast}. `task` (1.2.0) is the runtime task overlay — the lowest trust, the loosest floor.
|
|
60
|
+
* ⚠️ This enum's DECLARATION ORDER is NOT the rank (it is the 1.1.0 order with `task` appended — kept byte-stable
|
|
61
|
+
* 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"]);
|
|
63
|
+
/** The trust RANK of a floor / layer word — v2-design §1:29 verbatim (`MANAGED 6 > GLOBAL 5 > PROJECT 4 > LOCAL 3 >
|
|
64
|
+
* SESSION 2 > TASK 1`), the ONE published ladder every resolver compares against (1.2.0). Higher = more trusted.
|
|
65
|
+
* The numbers themselves are contract (a consumer may persist or log them), not merely their order.
|
|
66
|
+
* Exhaustive over {@link MinTrust}: a word added to the enum without a rank is a COMPILE error here; a word that
|
|
67
|
+
* 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`. */
|
|
69
|
+
export function trustRank(layer) {
|
|
70
|
+
switch (layer) {
|
|
71
|
+
case "managed": return 6;
|
|
72
|
+
case "global": return 5;
|
|
73
|
+
case "project": return 4;
|
|
74
|
+
case "local": return 3;
|
|
75
|
+
case "session": return 2;
|
|
76
|
+
case "task": return 1;
|
|
77
|
+
default: {
|
|
78
|
+
const unknown = layer;
|
|
79
|
+
throw new RangeError(`MinTrust: unknown trust word '${String(unknown)}' (words: ${MinTrust.options.join(", ")})`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/** `trustRank(layer) ≥ trustRank(minTrust)` — "is a source at `layer` trusted enough to set a key / member whose
|
|
84
|
+
* 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). */
|
|
86
|
+
export function isAtLeast(layer, minTrust) {
|
|
87
|
+
return trustRank(layer) >= trustRank(minTrust);
|
|
88
|
+
}
|
|
89
|
+
/** Which way a MEMBER moves the engine's unattended surface (1.1.0). `loosen` = admits an action without a human
|
|
90
|
+
* that would otherwise ask/deny — a privilege, so it needs trust; `tighten` = only ever narrows — restricting is
|
|
91
|
+
* safe from anyone, so any settings-file layer may add it. The word is what a consumer branches on for the
|
|
92
|
+
* NOT-admitted outlet: the ruling REQUIRES a consumer to turn a `loosen` member below its floor into a pending
|
|
93
|
+
* suggestion (consent flow) rather than drop it silently. This package returns that metadata only — it performs
|
|
94
|
+
* no admission and no degradation; whether a given consumer version implements the outlet is that consumer's
|
|
95
|
+
* contract. */
|
|
96
|
+
export const MemberDirection = z.enum(["loosen", "tighten"]);
|
|
97
|
+
/** One member's admission row. `minTrust` ABSENT ⇒ the category's `minTrust` applies: a row can refine the floor
|
|
98
|
+
* only by SAYING so; omission never lowers it (fail-closed by construction). */
|
|
99
|
+
export const MemberAdmission = z.object({
|
|
100
|
+
direction: MemberDirection,
|
|
101
|
+
minTrust: MinTrust.optional(),
|
|
102
|
+
});
|
|
18
103
|
/** WHERE the category is enforced (STRENGTH) — MUST stay in lockstep with `EffectiveKey.enforcement` (types.ts).
|
|
19
104
|
* gate=data-plane engine, server=control-plane/TOB boundary, resolver=merger correctness, client=UI only. */
|
|
20
105
|
export const SpecEnforcement = z.enum(["gate", "server", "resolver", "client"]);
|
|
@@ -41,11 +126,27 @@ export const MergeCategorySpec = z.object({
|
|
|
41
126
|
seam: z.string(),
|
|
42
127
|
/** When the category's VALUE domain is core's, the cited `SAFETY_AXIS_VOCABULARY` dimension (conformance-checked). */
|
|
43
128
|
coreAxis: z.enum(["toolEffect", "egress", "irreversibility", "safetyAxis", "severity", "shellGate", "permissionDecision"]).optional(),
|
|
129
|
+
/** 1.1.0 — member-level admission (see the file header). ABSENT ⇒ the category is a whole key: its `minTrust`
|
|
130
|
+
* admits or rejects the whole value. PRESENT ⇒ each named member is admitted on its own row (fallback to the
|
|
131
|
+
* category `minTrust` when the row omits one); the member set is closed — {@link membersOf} enumerates it. */
|
|
132
|
+
members: z.record(z.string(), MemberAdmission).optional(),
|
|
44
133
|
});
|
|
45
134
|
/** The category spec. Keys = the unified settings categories both seats map their concrete keys/fields into.
|
|
46
135
|
* Faithful to settings-core v2-design §3 (the semantic source of record). */
|
|
47
136
|
export const SAFETY_MERGE_SPEC = {
|
|
48
|
-
|
|
137
|
+
// 1.1.0 [5797] ④ — member-level admission, direction-asymmetric (header): allow = loosen ⇒ global-or-managed;
|
|
138
|
+
// deny/ask = tighten ⇒ every settings-file layer (local = the lowest file tier). The category minTrust stays
|
|
139
|
+
// "global" = the DEFAULT floor (member rows override it; consumers that predate `members` still enforce it on
|
|
140
|
+
// the whole key = unchanged behaviour).
|
|
141
|
+
permissions: {
|
|
142
|
+
mergeShape: "denyFirst", minTrust: "global", tighten: { rule: "policyDenyWins" }, enforcement: "gate", fail: "closed",
|
|
143
|
+
seam: "toolPolicy + deny-narrowing (SessionPolicyStore)", coreAxis: "permissionDecision",
|
|
144
|
+
members: {
|
|
145
|
+
allow: { direction: "loosen", minTrust: "global" },
|
|
146
|
+
deny: { direction: "tighten", minTrust: "local" },
|
|
147
|
+
ask: { direction: "tighten", minTrust: "local" },
|
|
148
|
+
},
|
|
149
|
+
},
|
|
49
150
|
// [3759] order covers the FULL shell-addressable mode vocabulary — a word missing here is not "unknown", it
|
|
50
151
|
// makes every two-layer merge containing it hit the out-of-vocabulary reject arm (the whole bundle used to be
|
|
51
152
|
// dropped; shells now reject per-unit, but the semantic fix is a complete rank). Rank anchors (server
|
|
@@ -68,3 +169,47 @@ export const SAFETY_MERGE_SPEC = {
|
|
|
68
169
|
ui: { mergeShape: "replace", minTrust: "local", tighten: { rule: "none" }, enforcement: "client", fail: "open", seam: "shell-only (theme/keybindings/statusLine) — never touches the engine" },
|
|
69
170
|
org: { mergeShape: "replace", minTrust: "managed", tighten: { rule: "none" }, enforcement: "server", fail: "closed", seam: "config-center → governanceBaseline (SSO/SCIM/audit/budget, TOB-only)" },
|
|
70
171
|
};
|
|
172
|
+
/** Own-property table lookup. The category union is compile-time only; a JavaScript or deserialized caller can
|
|
173
|
+
* hand in `"__proto__"` / `"constructor"` / `"toString"`, which a bare index resolves through Object.prototype
|
|
174
|
+
* to a non-spec object — `membersOf` would then answer `[]` = "whole key, nothing to check" (fail-OPEN). An
|
|
175
|
+
* unknown category is a loud RangeError instead. */
|
|
176
|
+
function categorySpec(category) {
|
|
177
|
+
if (!Object.hasOwn(SAFETY_MERGE_SPEC, category)) {
|
|
178
|
+
throw new RangeError(`SAFETY_MERGE_SPEC: unknown category '${category}' (categories: ${Object.keys(SAFETY_MERGE_SPEC).join(", ")})`);
|
|
179
|
+
}
|
|
180
|
+
return SAFETY_MERGE_SPEC[category];
|
|
181
|
+
}
|
|
182
|
+
/** The member names a category declares, in declaration order; `[]` for a whole-key category. A resolver iterates
|
|
183
|
+
* THIS (never its own allow/deny/ask list) so the member set has one source. Unknown category ⇒ RangeError. */
|
|
184
|
+
export function membersOf(category) {
|
|
185
|
+
const spec = categorySpec(category);
|
|
186
|
+
return (spec.members === undefined ? [] : Object.keys(spec.members));
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Resolve one member's admission off a category spec VALUE (the single implementation; {@link memberAdmission}
|
|
190
|
+
* binds it to {@link SAFETY_MERGE_SPEC}). Fallback law: a member row without `minTrust` inherits the category's.
|
|
191
|
+
* Fail-CLOSED and loud on both misuse arms — a category without `members` has no member to name, and a member
|
|
192
|
+
* outside the declared set is a caller bug, not "use the category floor" (that silent arm is exactly what would
|
|
193
|
+
* let a typo admit a bucket under the wrong rule).
|
|
194
|
+
* @param categoryLabel only decorates the error message (the spec value carries no name of its own).
|
|
195
|
+
*/
|
|
196
|
+
export function resolveMemberAdmission(spec, member, categoryLabel = "(unnamed category)") {
|
|
197
|
+
if (spec.members === undefined) {
|
|
198
|
+
throw new RangeError(`SAFETY_MERGE_SPEC: category '${categoryLabel}' has no member-level admission — its minTrust applies to the whole key`);
|
|
199
|
+
}
|
|
200
|
+
if (!Object.hasOwn(spec.members, member)) {
|
|
201
|
+
throw new RangeError(`SAFETY_MERGE_SPEC: unknown member '${member}' of category '${categoryLabel}' (members: ${Object.keys(spec.members).join(", ")})`);
|
|
202
|
+
}
|
|
203
|
+
const row = spec.members[member];
|
|
204
|
+
return { direction: row.direction, minTrust: row.minTrust ?? spec.minTrust };
|
|
205
|
+
}
|
|
206
|
+
/** A member's resolved admission (direction + floor, category fallback applied) from the published table.
|
|
207
|
+
* Unknown category or member ⇒ RangeError (never a fallback). */
|
|
208
|
+
export function memberAdmission(category, member) {
|
|
209
|
+
return resolveMemberAdmission(categorySpec(category), member, category);
|
|
210
|
+
}
|
|
211
|
+
/** A member's admission FLOOR from the published table — the word a resolver compares the layer's trust against
|
|
212
|
+
* (`isAtLeast(layer, floor)` ⇒ admitted; the ladder is {@link trustRank}). Convenience over {@link memberAdmission}. */
|
|
213
|
+
export function memberMinTrust(category, member) {
|
|
214
|
+
return memberAdmission(category, member).minTrust;
|
|
215
|
+
}
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sema-agent/settings-schema",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Sema settings schema — pure config contract (zod domains + effective-config + roster resolution + ref-integrity + remoteExec) shared by sema-registry, 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",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
|
-
"url": "https://github.com/sema-agent/sema-
|
|
9
|
+
"url": "https://github.com/sema-agent/sema-settings-schema.git"
|
|
10
10
|
},
|
|
11
11
|
"keywords": [
|
|
12
12
|
"ai",
|