@sema-agent/settings-schema 1.0.0 → 1.1.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 +100 -1
- package/README.md +12 -2
- package/dist/safety-merge-spec.d.ts +130 -7
- package/dist/safety-merge-spec.js +111 -4
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,105 @@
|
|
|
6
6
|
> `@sema-agent/registry-core`(更早还曾是 `@sema-ai/registry-core`)。本档 `0.19.0` 以下各节里的
|
|
7
7
|
> 包名是**历史记录**,如实保留不追改。
|
|
8
8
|
|
|
9
|
+
## 1.1.0 — `permissions` 成员级准入(权限双通道终裁四件套之④,clay 终裁 [5797];合流形 [5794])
|
|
10
|
+
|
|
11
|
+
**`SAFETY_MERGE_SPEC` 类目可声明 `members`(成员级 minTrust + 方向);首个也是当前唯一成员化的类目 = `permissions`
|
|
12
|
+
(`allow` / `deny` / `ask`,闭集)。纯 additive:类目级 `minTrust` 一个字不动,老消费方行为逐字节不变。**
|
|
13
|
+
|
|
14
|
+
### 键形定稿(全文)
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
permissions: {
|
|
18
|
+
mergeShape: "denyFirst", minTrust: "global", // 类目级 = 缺省兜底(不动)
|
|
19
|
+
tighten: { rule: "policyDenyWins" }, enforcement: "gate", fail: "closed", …,
|
|
20
|
+
members: {
|
|
21
|
+
allow: { direction: "loosen", minTrust: "global" }, // 放宽 = 特权:global 或托管层(managed 在其上)
|
|
22
|
+
deny: { direction: "tighten", minTrust: "local" }, // 收紧:任何 settings-file 启用层恒准入
|
|
23
|
+
ask: { direction: "tighten", minTrust: "local" }, // 收紧(「ask = 收紧」正式化为数据)
|
|
24
|
+
},
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
- `MergeCategorySpec` 新增可选字段 `members: Record<string, MemberAdmission>`;`MemberAdmission = { direction:
|
|
29
|
+
"loosen" | "tighten", minTrust?: MinTrust }`(zod 闭集:方向词/信任词外的值拒)。
|
|
30
|
+
- 新导出:`MemberDirection` / `MemberAdmission`(zod schema + 同名推导类型)、`ResolvedMemberAdmission`
|
|
31
|
+
(**仅类型**,interface,无运行时值)、`SafetyMergeMember<C>`(成员名闭集**类型**:`permissions` ⇒
|
|
32
|
+
`"allow"|"deny"|"ask"`,其余类目 ⇒ `never`,喊错成员名 = **编译错**)、`membersOf(category)`(成员名单源,
|
|
33
|
+
resolver 迭代它而不是自铸 allow/deny/ask 表)、`memberAdmission(category, member)` /
|
|
34
|
+
`memberMinTrust(category, member)`(准入查询,回落已应用)、`resolveMemberAdmission(specValue, member, label?)`
|
|
35
|
+
(对 spec **值**求解的唯一实现,前两者是它对 `SAFETY_MERGE_SPEC` 的绑定)。三个表查询对**未知类目**
|
|
36
|
+
(含 `__proto__`/`constructor` 等原型链名)与**未知成员**一律 `RangeError`——own-property 查表,不走原型链。
|
|
37
|
+
|
|
38
|
+
### 语义句(成文层,规范性)
|
|
39
|
+
|
|
40
|
+
1. **方向不对称**:`deny`/`ask` 只会**收紧**无人值守面——收窄无须信任,故**任何 settings-file 启用层恒准入**
|
|
41
|
+
(`minTrust:"local"` = settings 文件能承载的最低信任档;v2-design §1:session/task 是运行期 overlay 不是文件,
|
|
42
|
+
运行期授权走显式运行期通道,不在本表);与 `policyDenyWins` 同一方向律。`allow` 会**放宽**——自动放行是特权,
|
|
43
|
+
须 trust ≥ `global`(global 或 managed)。
|
|
44
|
+
2. **未达门槛的 allow 不静默丢**(终裁③):执法层把它降级为「待确认建议」走 `confirmRuleApproval` consent 流——
|
|
45
|
+
那个出口归**执法半场**(core `prepareCcImport` 单门 + `RuleAddOrigin` 出处;cli resolver/壳喂门);本表只说
|
|
46
|
+
**哪些成员需要它**(`direction:"loosen"`)。⚠️ **本包只发布契约元数据,不提供该行为**:本包不做准入判定、
|
|
47
|
+
不做降级。
|
|
48
|
+
**消费半场观察值**(亲读于发包日的邻仓 revision,是**观察**不是本包规范,会随邻仓发版过期;以邻仓自己的
|
|
49
|
+
CHANGELOG 为准):① 成员级查询函数的消费方 = **零**,矩阵有界如下(发包日 `grep SAFETY_MERGE_SPEC|safety-merge-spec|
|
|
50
|
+
memberMinTrust|membersOf(` 于各仓 `src/`):cli `1ef236af` 4 文件命中(`keyContracts.ts` / `resolver.ts` /
|
|
51
|
+
`safety-merge.conformance.test.ts` / `printModeEngine.ts`,全部只读类目级 `minTrust`);client-core `1606fd0` /
|
|
52
|
+
server `88925c5` / web-admin `42ac03d` / web-client `fb8f459` / desktop `9ce3736` / sdk `2e822f6` / test `56d09564`
|
|
53
|
+
零命中(cli 半场排 server 车D 后);② core
|
|
54
|
+
`a305f6a3`:`prepareCcImport`(`src/core/permission-rule-consent.ts:1441`)无成员/门槛入参、只导入 allow 桶,
|
|
55
|
+
且 v1 不读 `flagSettings` / `policySettings`(同文件 `IMPORT_UNCOVERED_LAYERS`);③ cli `1ef236af`:
|
|
56
|
+
`flagSettings`(`--settings`)映射为 `local` 信任(`src/sema/settings/consume88.ts:57-63`),resolver 仍按类目级
|
|
57
|
+
整键 trust-skip(`src/sema/settings/resolver.ts:515`)——即在消费半场接线前,`--settings` 里的 allow 既不准入也
|
|
58
|
+
进不了 consent 道。这是执法半场的已知缺口,随其接线批闭合(codex R1-F1 / R2-F3)。
|
|
59
|
+
3. **「ask = 收紧」正式化**:本表把 `ask` 与 `deny` 同标 `tighten`。观察值(core `a305f6a3`):`SessionPermissionRules`
|
|
60
|
+
无 ask 员(只有 toolAllow/toolDeny/allowDirs/commandAllow/commandDeny,`src/core/session-policy-store.ts:27-40`);
|
|
61
|
+
加员 = core 半场 additive,本仓只成文契约。
|
|
62
|
+
4. **类目级 `minTrust` = 缺省兜底**:成员行缺省 `minTrust` 回落类目级(**省略永远不会降低门槛**);不认识 `members`
|
|
63
|
+
的老消费方仍按类目级整键判 = 1.0.0 行为逐字节不变——不静默重解释类目级语义。
|
|
64
|
+
5. **安全下界**(终裁 [5797] 成文入册的 core 原则,本处**重述**以免读者从本表反推出相反结论;它是 core 的
|
|
65
|
+
保证、不是本包能证的):已准入的 allow 只能消 classifier ask,**消不了 mandated ask**(shellGate `"always"` /
|
|
66
|
+
org 策略 / requiresRealApproval)。
|
|
67
|
+
6. **未知成员拒(闭集)**:类型面编译错;运行时 `RangeError`(fail-closed、响亮),**不**回落类目级——静默回落正是
|
|
68
|
+
让拼错的桶按错规则准入的那条臂。无 `members` 的类目喊成员名同样拒(它没有成员可喊)。
|
|
69
|
+
|
|
70
|
+
### 消费方接线(cli resolver / 壳 / web 面板)
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
import { membersOf, memberMinTrust, memberAdmission } from "@sema-agent/settings-schema";
|
|
74
|
+
for (const m of membersOf("permissions")) { // ["allow","deny","ask"] —— 单源,不自铸
|
|
75
|
+
if (layer.permissions?.[m] === undefined) continue;
|
|
76
|
+
const floor = memberMinTrust("permissions", m); // allow→"global", deny/ask→"local"
|
|
77
|
+
if (rank(layer.sourceTrust) < rank(floor)) {
|
|
78
|
+
// memberAdmission("permissions", m).direction === "loosen" ⇒ 待确认建议(consent),不是静默丢
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
- `rank(...)` 仍是消费方自己的阶梯(v2-design §1 `managed > global > project > local > session > task`);本包
|
|
84
|
+
**尚未**导出排序原语——`MinTrust` 枚举的声明序在 session/local 处与 §1 倒置、且零消费方索引它(非规范),
|
|
85
|
+
排序原语随 [5796] 序题表态另车(本版只把该注释改说真话)。
|
|
86
|
+
|
|
87
|
+
### 门
|
|
88
|
+
|
|
89
|
+
- 红先:12 条新钉先红(运行时 11 条 `is not a function` / `MemberDirection.options` undefined;编译期钉 1 条=tsc
|
|
90
|
+
`@ts-expect-error` 未命中;tsc 共 14 处红)再绿。
|
|
91
|
+
- 变异五刀各红(ask 反向 3 / deny 抬门槛 3 / 未知成员静默回落 1 / 去掉缺席回落 4 / allow 低于类目 floor 3),
|
|
92
|
+
源文件 shasum 复原字节一致。
|
|
93
|
+
- codex adversarial-review R1 四条全采:F1 成文措辞把执法半场的 consent 出口写得像本包已提供 → 改述(上 §2);
|
|
94
|
+
F2 `membersOf("__proto__")` 走原型链返回 `[]` = fail-open → own-property 类目守卫 + 红先钉(六个坏类目名);
|
|
95
|
+
F3 「老消费方行为不变」无冻结钉 → 加 1.0.0 字面量冻结钉 + 冻结 1.0.0 schema 解析全表;F4 `ResolvedMemberAdmission`
|
|
96
|
+
误标 zod 导出 → 改「仅类型」。
|
|
97
|
+
- codex R2 三条全采:F1 [high] `SafetyMergeMember<C>` 非分配式——resolver 手里的宽类型 `SafetyMergeCategory` 代入后
|
|
98
|
+
塌成 `never`(`membersOf(cat)` 变 `readonly never[]`,真接缝处被迫 cast)→ 改分配式条件类型 + 类型**等式**红钉
|
|
99
|
+
(`never[]` 可赋给任何数组,可赋性钉抓不到,故用等式);F2 `MemberDirection` JSDoc 把 consent 降级说成无条件运行时
|
|
100
|
+
行为 → 改「裁定要求消费方如此处理;本包只返回元数据」,README 同步;F3 跨仓事实未钉 revision → 逐条钉亲读 revision
|
|
101
|
+
并标「观察值 ≠ 本包规范」。
|
|
102
|
+
- codex R3 两条全采:F1 头注「行为由 mergeShape+minTrust+tighten+enforcement 完全决定」漏 `members`/`fail` → 补;
|
|
103
|
+
F2 「无任何消费方接线」全称句无边界 → 改逐仓 revision 钉的有界矩阵(上 §2 ①)。
|
|
104
|
+
- 变异再两刀:M6 `permissions.seam` 串漂移 → 只红冻结钉(1/21);M7 拆掉类目 own-property 守卫 → 只红原型链类目钉
|
|
105
|
+
(1/21);复原字节一致。
|
|
106
|
+
- 全量 + tsc + build + e2e 绿(读数见发版帖)。
|
|
107
|
+
|
|
9
108
|
## 1.0.0 — 改名(#361 步①;clay 裁 [5156],开工帖 [5295])
|
|
10
109
|
|
|
11
110
|
**`@sema-agent/registry-core` → `@sema-agent/settings-schema`。纯改名件。**
|
|
@@ -128,7 +227,7 @@ process.exit(tok.length === 0 && erased.length === 0 ? 0 : 1);
|
|
|
128
227
|
|
|
129
228
|
### 仓名不动
|
|
130
229
|
|
|
131
|
-
|
|
230
|
+
本步只改**包名**。(追记 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
231
|
`package.json` 的 `repository.url` **原样保留**;src 注释里指代本仓/本项目的裸词 `registry-core`
|
|
133
232
|
同样保留(它们不是包名)。
|
|
134
233
|
|
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,16 @@ 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.
|
|
61
71
|
- **Auth wire contract** (`api/auth.ts`, subpath `./api/auth`): the `/api/v1/auth/*` face — RFC 8628
|
|
62
72
|
device flow (code / token poll / approve consent) + rotating refresh + logout, the OAuth error envelope,
|
|
63
73
|
and the pure client helpers `normalizeUserCode` / `nextPollInterval`. Shipped M1 shapes frozen.
|
|
@@ -123,7 +133,7 @@ defaults. Each `config.d/<domain>.json` is exactly that domain's zod object.
|
|
|
123
133
|
|
|
124
134
|
## 贡献指引
|
|
125
135
|
|
|
126
|
-
- 真源 = `sema-agent/sema-
|
|
136
|
+
- 真源 = `sema-agent/sema-settings-schema` org 仓 `main`(2026-08-26 由 sema-registry-core 改名),所有变更直接落 org main(或 PR 进 main);
|
|
127
137
|
不再向任何个人位/镜像位 push。
|
|
128
138
|
- 提交门 = `npm run build` + `npx vitest run`;改域/契约后必跑 `npm run e2e`(examples/toc-e2e.mjs)。
|
|
129
139
|
- 契约破坏面动之前先盘 center / server / CLI 三方消费点;0.x 破坏性变更必 bump minor(见上节)。
|
|
@@ -7,16 +7,77 @@
|
|
|
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).
|
|
12
41
|
*/
|
|
13
42
|
import { z } from "zod";
|
|
14
43
|
/** How values from different layers/principals combine for a category. */
|
|
15
44
|
export declare const MergeShape: z.ZodEnum<["denyFirst", "replace", "runAll", "overrideByName", "concat"]>;
|
|
16
45
|
export type MergeShape = z.infer<typeof MergeShape>;
|
|
17
|
-
/** The
|
|
46
|
+
/** The trust-tier VOCABULARY a category — or, since 1.1.0, one of its members — names as its admission floor
|
|
47
|
+
* (the MINIMUM trust a layer/principal must have to set it). The RANK is v2-design §1:
|
|
48
|
+
* `managed > global > project > local > session` (`task` is a layer tier below all of these and is not a floor
|
|
49
|
+
* word). ⚠️ This enum's DECLARATION ORDER is not that rank (session/local are listed the other way round) and no
|
|
50
|
+
* consumer indexes it — a published rank primitive is a separate follow-up (board [5796] question). */
|
|
18
51
|
export declare const MinTrust: z.ZodEnum<["local", "session", "project", "global", "managed"]>;
|
|
19
52
|
export type MinTrust = z.infer<typeof MinTrust>;
|
|
53
|
+
/** Which way a MEMBER moves the engine's unattended surface (1.1.0). `loosen` = admits an action without a human
|
|
54
|
+
* that would otherwise ask/deny — a privilege, so it needs trust; `tighten` = only ever narrows — restricting is
|
|
55
|
+
* safe from anyone, so any settings-file layer may add it. The word is what a consumer branches on for the
|
|
56
|
+
* NOT-admitted outlet: the ruling REQUIRES a consumer to turn a `loosen` member below its floor into a pending
|
|
57
|
+
* suggestion (consent flow) rather than drop it silently. This package returns that metadata only — it performs
|
|
58
|
+
* no admission and no degradation; whether a given consumer version implements the outlet is that consumer's
|
|
59
|
+
* contract. */
|
|
60
|
+
export declare const MemberDirection: z.ZodEnum<["loosen", "tighten"]>;
|
|
61
|
+
export type MemberDirection = z.infer<typeof MemberDirection>;
|
|
62
|
+
/** One member's admission row. `minTrust` ABSENT ⇒ the category's `minTrust` applies: a row can refine the floor
|
|
63
|
+
* only by SAYING so; omission never lowers it (fail-closed by construction). */
|
|
64
|
+
export declare const MemberAdmission: z.ZodObject<{
|
|
65
|
+
direction: z.ZodEnum<["loosen", "tighten"]>;
|
|
66
|
+
minTrust: z.ZodOptional<z.ZodEnum<["local", "session", "project", "global", "managed"]>>;
|
|
67
|
+
}, "strip", z.ZodTypeAny, {
|
|
68
|
+
direction: "loosen" | "tighten";
|
|
69
|
+
minTrust?: "local" | "session" | "global" | "project" | "managed" | undefined;
|
|
70
|
+
}, {
|
|
71
|
+
direction: "loosen" | "tighten";
|
|
72
|
+
minTrust?: "local" | "session" | "global" | "project" | "managed" | undefined;
|
|
73
|
+
}>;
|
|
74
|
+
export type MemberAdmission = z.infer<typeof MemberAdmission>;
|
|
75
|
+
/** The RESOLVED admission a consumer branches on — the category fallback already applied, so `minTrust` is never
|
|
76
|
+
* absent here. Produced by {@link resolveMemberAdmission} / {@link memberAdmission}. */
|
|
77
|
+
export interface ResolvedMemberAdmission {
|
|
78
|
+
readonly direction: MemberDirection;
|
|
79
|
+
readonly minTrust: MinTrust;
|
|
80
|
+
}
|
|
20
81
|
/** WHERE the category is enforced (STRENGTH) — MUST stay in lockstep with `EffectiveKey.enforcement` (types.ts).
|
|
21
82
|
* gate=data-plane engine, server=control-plane/TOB boundary, resolver=merger correctness, client=UI only. */
|
|
22
83
|
export declare const SpecEnforcement: z.ZodEnum<["gate", "server", "resolver", "client"]>;
|
|
@@ -112,10 +173,21 @@ export declare const MergeCategorySpec: z.ZodObject<{
|
|
|
112
173
|
seam: z.ZodString;
|
|
113
174
|
/** When the category's VALUE domain is core's, the cited `SAFETY_AXIS_VOCABULARY` dimension (conformance-checked). */
|
|
114
175
|
coreAxis: z.ZodOptional<z.ZodEnum<["toolEffect", "egress", "irreversibility", "safetyAxis", "severity", "shellGate", "permissionDecision"]>>;
|
|
176
|
+
/** 1.1.0 — member-level admission (see the file header). ABSENT ⇒ the category is a whole key: its `minTrust`
|
|
177
|
+
* admits or rejects the whole value. PRESENT ⇒ each named member is admitted on its own row (fallback to the
|
|
178
|
+
* category `minTrust` when the row omits one); the member set is closed — {@link membersOf} enumerates it. */
|
|
179
|
+
members: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
180
|
+
direction: z.ZodEnum<["loosen", "tighten"]>;
|
|
181
|
+
minTrust: z.ZodOptional<z.ZodEnum<["local", "session", "project", "global", "managed"]>>;
|
|
182
|
+
}, "strip", z.ZodTypeAny, {
|
|
183
|
+
direction: "loosen" | "tighten";
|
|
184
|
+
minTrust?: "local" | "session" | "global" | "project" | "managed" | undefined;
|
|
185
|
+
}, {
|
|
186
|
+
direction: "loosen" | "tighten";
|
|
187
|
+
minTrust?: "local" | "session" | "global" | "project" | "managed" | undefined;
|
|
188
|
+
}>>>;
|
|
115
189
|
}, "strip", z.ZodTypeAny, {
|
|
116
190
|
enforcement: "gate" | "server" | "resolver" | "client";
|
|
117
|
-
mergeShape: "concat" | "replace" | "denyFirst" | "runAll" | "overrideByName";
|
|
118
|
-
minTrust: "local" | "session" | "global" | "project" | "managed";
|
|
119
191
|
tighten: {
|
|
120
192
|
rule: "none";
|
|
121
193
|
} | {
|
|
@@ -129,13 +201,17 @@ export declare const MergeCategorySpec: z.ZodObject<{
|
|
|
129
201
|
rule: "enumRank";
|
|
130
202
|
order: string[];
|
|
131
203
|
};
|
|
204
|
+
minTrust: "local" | "session" | "global" | "project" | "managed";
|
|
205
|
+
mergeShape: "concat" | "replace" | "denyFirst" | "runAll" | "overrideByName";
|
|
132
206
|
fail: "closed" | "open";
|
|
133
207
|
seam: string;
|
|
208
|
+
members?: Record<string, {
|
|
209
|
+
direction: "loosen" | "tighten";
|
|
210
|
+
minTrust?: "local" | "session" | "global" | "project" | "managed" | undefined;
|
|
211
|
+
}> | undefined;
|
|
134
212
|
coreAxis?: "permissionDecision" | "toolEffect" | "egress" | "irreversibility" | "safetyAxis" | "severity" | "shellGate" | undefined;
|
|
135
213
|
}, {
|
|
136
214
|
enforcement: "gate" | "server" | "resolver" | "client";
|
|
137
|
-
mergeShape: "concat" | "replace" | "denyFirst" | "runAll" | "overrideByName";
|
|
138
|
-
minTrust: "local" | "session" | "global" | "project" | "managed";
|
|
139
215
|
tighten: {
|
|
140
216
|
rule: "none";
|
|
141
217
|
} | {
|
|
@@ -149,8 +225,14 @@ export declare const MergeCategorySpec: z.ZodObject<{
|
|
|
149
225
|
rule: "enumRank";
|
|
150
226
|
order: string[];
|
|
151
227
|
};
|
|
228
|
+
minTrust: "local" | "session" | "global" | "project" | "managed";
|
|
229
|
+
mergeShape: "concat" | "replace" | "denyFirst" | "runAll" | "overrideByName";
|
|
152
230
|
fail: "closed" | "open";
|
|
153
231
|
seam: string;
|
|
232
|
+
members?: Record<string, {
|
|
233
|
+
direction: "loosen" | "tighten";
|
|
234
|
+
minTrust?: "local" | "session" | "global" | "project" | "managed" | undefined;
|
|
235
|
+
}> | undefined;
|
|
154
236
|
coreAxis?: "permissionDecision" | "toolEffect" | "egress" | "irreversibility" | "safetyAxis" | "severity" | "shellGate" | undefined;
|
|
155
237
|
}>;
|
|
156
238
|
export type MergeCategorySpec = z.infer<typeof MergeCategorySpec>;
|
|
@@ -167,6 +249,20 @@ export declare const SAFETY_MERGE_SPEC: {
|
|
|
167
249
|
readonly fail: "closed";
|
|
168
250
|
readonly seam: "toolPolicy + deny-narrowing (SessionPolicyStore)";
|
|
169
251
|
readonly coreAxis: "permissionDecision";
|
|
252
|
+
readonly members: {
|
|
253
|
+
readonly allow: {
|
|
254
|
+
readonly direction: "loosen";
|
|
255
|
+
readonly minTrust: "global";
|
|
256
|
+
};
|
|
257
|
+
readonly deny: {
|
|
258
|
+
readonly direction: "tighten";
|
|
259
|
+
readonly minTrust: "local";
|
|
260
|
+
};
|
|
261
|
+
readonly ask: {
|
|
262
|
+
readonly direction: "tighten";
|
|
263
|
+
readonly minTrust: "local";
|
|
264
|
+
};
|
|
265
|
+
};
|
|
170
266
|
};
|
|
171
267
|
readonly permissionMode: {
|
|
172
268
|
readonly mergeShape: "replace";
|
|
@@ -325,3 +421,30 @@ export declare const SAFETY_MERGE_SPEC: {
|
|
|
325
421
|
};
|
|
326
422
|
};
|
|
327
423
|
export type SafetyMergeCategory = keyof typeof SAFETY_MERGE_SPEC;
|
|
424
|
+
/** The members a category declares, as a CLOSED SET TYPE: `"allow" | "deny" | "ask"` for `permissions`, `never` for
|
|
425
|
+
* every whole-key category — so naming a member the contract does not know is a COMPILE error at the call site.
|
|
426
|
+
* DISTRIBUTIVE over `C` on purpose: a resolver holds a WIDENED `SafetyMergeCategory` (out of its key→category
|
|
427
|
+
* map), and a non-distributive form would index the whole union at once, fail the `members` check on the
|
|
428
|
+
* whole-key categories and collapse to `never` — typing `membersOf(category)` as `readonly never[]` at exactly
|
|
429
|
+
* the seam these helpers exist for (codex R2-F1). */
|
|
430
|
+
export type SafetyMergeMember<C extends SafetyMergeCategory> = C extends SafetyMergeCategory ? (typeof SAFETY_MERGE_SPEC)[C] extends {
|
|
431
|
+
readonly members: infer M;
|
|
432
|
+
} ? Extract<keyof M, string> : never : never;
|
|
433
|
+
/** The member names a category declares, in declaration order; `[]` for a whole-key category. A resolver iterates
|
|
434
|
+
* THIS (never its own allow/deny/ask list) so the member set has one source. Unknown category ⇒ RangeError. */
|
|
435
|
+
export declare function membersOf<C extends SafetyMergeCategory>(category: C): readonly SafetyMergeMember<C>[];
|
|
436
|
+
/**
|
|
437
|
+
* Resolve one member's admission off a category spec VALUE (the single implementation; {@link memberAdmission}
|
|
438
|
+
* binds it to {@link SAFETY_MERGE_SPEC}). Fallback law: a member row without `minTrust` inherits the category's.
|
|
439
|
+
* Fail-CLOSED and loud on both misuse arms — a category without `members` has no member to name, and a member
|
|
440
|
+
* outside the declared set is a caller bug, not "use the category floor" (that silent arm is exactly what would
|
|
441
|
+
* let a typo admit a bucket under the wrong rule).
|
|
442
|
+
* @param categoryLabel only decorates the error message (the spec value carries no name of its own).
|
|
443
|
+
*/
|
|
444
|
+
export declare function resolveMemberAdmission(spec: MergeCategorySpec, member: string, categoryLabel?: string): ResolvedMemberAdmission;
|
|
445
|
+
/** A member's resolved admission (direction + floor, category fallback applied) from the published table.
|
|
446
|
+
* Unknown category or member ⇒ RangeError (never a fallback). */
|
|
447
|
+
export declare function memberAdmission<C extends SafetyMergeCategory>(category: C, member: SafetyMergeMember<C>): ResolvedMemberAdmission;
|
|
448
|
+
/** A member's admission FLOOR from the published table — the word a resolver compares the layer's trust against
|
|
449
|
+
* (`rank(layer) ≥ rank(floor)` ⇒ admitted). Convenience over {@link memberAdmission}. */
|
|
450
|
+
export declare function memberMinTrust<C extends SafetyMergeCategory>(category: C, member: SafetyMergeMember<C>): MinTrust;
|
|
@@ -7,14 +7,61 @@
|
|
|
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).
|
|
12
41
|
*/
|
|
13
42
|
import { z } from "zod";
|
|
14
43
|
/** How values from different layers/principals combine for a category. */
|
|
15
44
|
export const MergeShape = z.enum(["denyFirst", "replace", "runAll", "overrideByName", "concat"]);
|
|
16
|
-
/** The
|
|
45
|
+
/** The trust-tier VOCABULARY a category — or, since 1.1.0, one of its members — names as its admission floor
|
|
46
|
+
* (the MINIMUM trust a layer/principal must have to set it). The RANK is v2-design §1:
|
|
47
|
+
* `managed > global > project > local > session` (`task` is a layer tier below all of these and is not a floor
|
|
48
|
+
* word). ⚠️ This enum's DECLARATION ORDER is not that rank (session/local are listed the other way round) and no
|
|
49
|
+
* consumer indexes it — a published rank primitive is a separate follow-up (board [5796] question). */
|
|
17
50
|
export const MinTrust = z.enum(["local", "session", "project", "global", "managed"]);
|
|
51
|
+
/** Which way a MEMBER moves the engine's unattended surface (1.1.0). `loosen` = admits an action without a human
|
|
52
|
+
* that would otherwise ask/deny — a privilege, so it needs trust; `tighten` = only ever narrows — restricting is
|
|
53
|
+
* safe from anyone, so any settings-file layer may add it. The word is what a consumer branches on for the
|
|
54
|
+
* NOT-admitted outlet: the ruling REQUIRES a consumer to turn a `loosen` member below its floor into a pending
|
|
55
|
+
* suggestion (consent flow) rather than drop it silently. This package returns that metadata only — it performs
|
|
56
|
+
* no admission and no degradation; whether a given consumer version implements the outlet is that consumer's
|
|
57
|
+
* contract. */
|
|
58
|
+
export const MemberDirection = z.enum(["loosen", "tighten"]);
|
|
59
|
+
/** One member's admission row. `minTrust` ABSENT ⇒ the category's `minTrust` applies: a row can refine the floor
|
|
60
|
+
* only by SAYING so; omission never lowers it (fail-closed by construction). */
|
|
61
|
+
export const MemberAdmission = z.object({
|
|
62
|
+
direction: MemberDirection,
|
|
63
|
+
minTrust: MinTrust.optional(),
|
|
64
|
+
});
|
|
18
65
|
/** WHERE the category is enforced (STRENGTH) — MUST stay in lockstep with `EffectiveKey.enforcement` (types.ts).
|
|
19
66
|
* gate=data-plane engine, server=control-plane/TOB boundary, resolver=merger correctness, client=UI only. */
|
|
20
67
|
export const SpecEnforcement = z.enum(["gate", "server", "resolver", "client"]);
|
|
@@ -41,11 +88,27 @@ export const MergeCategorySpec = z.object({
|
|
|
41
88
|
seam: z.string(),
|
|
42
89
|
/** When the category's VALUE domain is core's, the cited `SAFETY_AXIS_VOCABULARY` dimension (conformance-checked). */
|
|
43
90
|
coreAxis: z.enum(["toolEffect", "egress", "irreversibility", "safetyAxis", "severity", "shellGate", "permissionDecision"]).optional(),
|
|
91
|
+
/** 1.1.0 — member-level admission (see the file header). ABSENT ⇒ the category is a whole key: its `minTrust`
|
|
92
|
+
* admits or rejects the whole value. PRESENT ⇒ each named member is admitted on its own row (fallback to the
|
|
93
|
+
* category `minTrust` when the row omits one); the member set is closed — {@link membersOf} enumerates it. */
|
|
94
|
+
members: z.record(z.string(), MemberAdmission).optional(),
|
|
44
95
|
});
|
|
45
96
|
/** The category spec. Keys = the unified settings categories both seats map their concrete keys/fields into.
|
|
46
97
|
* Faithful to settings-core v2-design §3 (the semantic source of record). */
|
|
47
98
|
export const SAFETY_MERGE_SPEC = {
|
|
48
|
-
|
|
99
|
+
// 1.1.0 [5797] ④ — member-level admission, direction-asymmetric (header): allow = loosen ⇒ global-or-managed;
|
|
100
|
+
// deny/ask = tighten ⇒ every settings-file layer (local = the lowest file tier). The category minTrust stays
|
|
101
|
+
// "global" = the DEFAULT floor (member rows override it; consumers that predate `members` still enforce it on
|
|
102
|
+
// the whole key = unchanged behaviour).
|
|
103
|
+
permissions: {
|
|
104
|
+
mergeShape: "denyFirst", minTrust: "global", tighten: { rule: "policyDenyWins" }, enforcement: "gate", fail: "closed",
|
|
105
|
+
seam: "toolPolicy + deny-narrowing (SessionPolicyStore)", coreAxis: "permissionDecision",
|
|
106
|
+
members: {
|
|
107
|
+
allow: { direction: "loosen", minTrust: "global" },
|
|
108
|
+
deny: { direction: "tighten", minTrust: "local" },
|
|
109
|
+
ask: { direction: "tighten", minTrust: "local" },
|
|
110
|
+
},
|
|
111
|
+
},
|
|
49
112
|
// [3759] order covers the FULL shell-addressable mode vocabulary — a word missing here is not "unknown", it
|
|
50
113
|
// makes every two-layer merge containing it hit the out-of-vocabulary reject arm (the whole bundle used to be
|
|
51
114
|
// dropped; shells now reject per-unit, but the semantic fix is a complete rank). Rank anchors (server
|
|
@@ -68,3 +131,47 @@ export const SAFETY_MERGE_SPEC = {
|
|
|
68
131
|
ui: { mergeShape: "replace", minTrust: "local", tighten: { rule: "none" }, enforcement: "client", fail: "open", seam: "shell-only (theme/keybindings/statusLine) — never touches the engine" },
|
|
69
132
|
org: { mergeShape: "replace", minTrust: "managed", tighten: { rule: "none" }, enforcement: "server", fail: "closed", seam: "config-center → governanceBaseline (SSO/SCIM/audit/budget, TOB-only)" },
|
|
70
133
|
};
|
|
134
|
+
/** Own-property table lookup. The category union is compile-time only; a JavaScript or deserialized caller can
|
|
135
|
+
* hand in `"__proto__"` / `"constructor"` / `"toString"`, which a bare index resolves through Object.prototype
|
|
136
|
+
* to a non-spec object — `membersOf` would then answer `[]` = "whole key, nothing to check" (fail-OPEN). An
|
|
137
|
+
* unknown category is a loud RangeError instead. */
|
|
138
|
+
function categorySpec(category) {
|
|
139
|
+
if (!Object.hasOwn(SAFETY_MERGE_SPEC, category)) {
|
|
140
|
+
throw new RangeError(`SAFETY_MERGE_SPEC: unknown category '${category}' (categories: ${Object.keys(SAFETY_MERGE_SPEC).join(", ")})`);
|
|
141
|
+
}
|
|
142
|
+
return SAFETY_MERGE_SPEC[category];
|
|
143
|
+
}
|
|
144
|
+
/** The member names a category declares, in declaration order; `[]` for a whole-key category. A resolver iterates
|
|
145
|
+
* THIS (never its own allow/deny/ask list) so the member set has one source. Unknown category ⇒ RangeError. */
|
|
146
|
+
export function membersOf(category) {
|
|
147
|
+
const spec = categorySpec(category);
|
|
148
|
+
return (spec.members === undefined ? [] : Object.keys(spec.members));
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Resolve one member's admission off a category spec VALUE (the single implementation; {@link memberAdmission}
|
|
152
|
+
* binds it to {@link SAFETY_MERGE_SPEC}). Fallback law: a member row without `minTrust` inherits the category's.
|
|
153
|
+
* Fail-CLOSED and loud on both misuse arms — a category without `members` has no member to name, and a member
|
|
154
|
+
* outside the declared set is a caller bug, not "use the category floor" (that silent arm is exactly what would
|
|
155
|
+
* let a typo admit a bucket under the wrong rule).
|
|
156
|
+
* @param categoryLabel only decorates the error message (the spec value carries no name of its own).
|
|
157
|
+
*/
|
|
158
|
+
export function resolveMemberAdmission(spec, member, categoryLabel = "(unnamed category)") {
|
|
159
|
+
if (spec.members === undefined) {
|
|
160
|
+
throw new RangeError(`SAFETY_MERGE_SPEC: category '${categoryLabel}' has no member-level admission — its minTrust applies to the whole key`);
|
|
161
|
+
}
|
|
162
|
+
if (!Object.hasOwn(spec.members, member)) {
|
|
163
|
+
throw new RangeError(`SAFETY_MERGE_SPEC: unknown member '${member}' of category '${categoryLabel}' (members: ${Object.keys(spec.members).join(", ")})`);
|
|
164
|
+
}
|
|
165
|
+
const row = spec.members[member];
|
|
166
|
+
return { direction: row.direction, minTrust: row.minTrust ?? spec.minTrust };
|
|
167
|
+
}
|
|
168
|
+
/** A member's resolved admission (direction + floor, category fallback applied) from the published table.
|
|
169
|
+
* Unknown category or member ⇒ RangeError (never a fallback). */
|
|
170
|
+
export function memberAdmission(category, member) {
|
|
171
|
+
return resolveMemberAdmission(categorySpec(category), member, category);
|
|
172
|
+
}
|
|
173
|
+
/** A member's admission FLOOR from the published table — the word a resolver compares the layer's trust against
|
|
174
|
+
* (`rank(layer) ≥ rank(floor)` ⇒ admitted). Convenience over {@link memberAdmission}. */
|
|
175
|
+
export function memberMinTrust(category, member) {
|
|
176
|
+
return memberAdmission(category, member).minTrust;
|
|
177
|
+
}
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sema-agent/settings-schema",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.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",
|