@sema-agent/settings-schema 1.4.0 → 1.5.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 +27 -0
- package/dist/config-fns.d.ts +33 -3
- package/dist/config-fns.js +68 -32
- package/dist/file-store.d.ts +2 -2
- package/dist/file-store.js +4 -4
- package/dist/safety-merge-spec.d.ts +3 -3
- package/dist/safety-merge-spec.js +3 -3
- package/dist/types.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,33 @@
|
|
|
6
6
|
> `@sema-agent/registry-core`(更早还曾是 `@sema-ai/registry-core`)。本档 `0.19.0` 以下各节里的
|
|
7
7
|
> 包名是**历史记录**,如实保留不追改。
|
|
8
8
|
|
|
9
|
+
## 1.5.0 — 2026-09-04 — 扩展命名空间 + 剥键告警截断标志(sema-server S-118 源头修;新键/新导出=minor)
|
|
10
|
+
|
|
11
|
+
成案:sema-server 修 cli 用户 P1「`/model` 热加载被装饰键整拒」时在下游连打四层补丁(按 kind 放宽→按域→按键路径闭集→
|
|
12
|
+
64 满额特判),每层都被下一轮复审打穿;clay 裁「源头修完再发」+ 抽象优先四原则(sema-comms [6351])。根因在本包:
|
|
13
|
+
「未知键」一个信号被要求同时回答「已知键拼错」(该拒)与「别的写者有意放的扩展键」(该放),按键名扫描分不出意图——
|
|
14
|
+
意图只能在**声明处**说清。
|
|
15
|
+
|
|
16
|
+
**① `EXTENSION_NAMESPACES`(新导出)+ `isReservedExtensionKey(domain, parentPath, key)`(新导出,分段匹配:父路径 + 键名前缀,不在点号路径上猜——对抗复审 [medium] 采:`semaX-v2`/`semaX.foo` 这类合法前缀成员此前会被后缀文法误报)**:域文档里由别的写者按契约
|
|
17
|
+
放置、本包刻意不认的键。登记的键照旧被 zod strip(行为零变,写者自己回读文件),但 {@link droppedKeys} / `parseDomainLoud`
|
|
18
|
+
**不点名、不计数**。首条登记:`models` 条目级 `sema[A-Z]…`(壳 cli 的自留字段族,cli [5273] design/cli-190c 实键清单
|
|
19
|
+
六键 `semaChannel`/`semaLabel`/`semaProbe`/`semaProbeAt`/`semaEffortDefault`/`semaVision`,全条目级、顶层零装饰键 ⇒ 顶层
|
|
20
|
+
`sema*` 仍是未知键)。消费方从此一条规则:**未登记的未知键 = 拼错 = 错误,零特例**。新写者/新层级加行 = minor。
|
|
21
|
+
|
|
22
|
+
**② `unknown-keys-dropped` / `unknown-keys-carried` 告警加可选 `truncatedFrom?: number`**(与 `hosts-grandfathered.truncatedFrom`
|
|
23
|
+
同形):`keys` 仍有界(`DROP_SCAN_MAX_KEYS`=64),超界时带真实总数——扫描本就 O(文档键数),上限只界告警载荷不界扫描。
|
|
24
|
+
把有界列表当穷举决策输入正是 S-118 第四层被打穿的形(13 模型 × 6 装饰键填满 64 后拼错键不在列表里);①之后扩展键不计数,
|
|
25
|
+
②让剩下的截断在消费方看得见。
|
|
26
|
+
|
|
27
|
+
钉 `test/extension-namespace-1.5.test.ts` 七格(六装饰键零告警且仍 strip / 顶层 sema* 照报 / 前缀契约即前缀(`semaGPT_5`/`semaX-v2`/`semaX.foo` 成员,`semax`/`Sema` 不是)/ S-118 真案
|
|
28
|
+
78 扩展键 + 1 拼错恰点名 1 / 表门:锚定正则+真实域名 / dropped 与 carried 各自 64+truncatedFrom)。`droppedKeys` /
|
|
29
|
+
`carriedUnknownKeys` 导出签名不变(仍返回有界 `string[]`,是同一次扫描的投影)。消费方:sema-server(提货后删下游四层
|
|
30
|
+
特判与 `domainNotices` 机制)、cli(不消费告警面,零改;实键面若变请改表)。
|
|
31
|
+
|
|
32
|
+
## 1.4.1 — 2026-09-02 — 文案 patch(cli [6037]/[6038] 请件)
|
|
33
|
+
|
|
34
|
+
- 复审出处注词族全清:src 12 处+d.ts 同族(`codex …`→`对抗复审 …`,语义零损)——1.4.0 件⑦「52→1」的三族 regex 未含此词族=漏计根因;npm 包注释=对外文案面,壳 bundle hygiene 禁表命中即发布腿红。
|
|
35
|
+
|
|
9
36
|
## 1.4.0 — 2026-09-02 — settings-schema 批八件(DEBTS-server S-59;新键=minor 按本批成文政策)
|
|
10
37
|
|
|
11
38
|
七件 = 两把新可选键(①⑥,additive)+ 三件文案/论证面(②③④)+ 一件文档重写(⑤)+ 一件发布物中性化(⑦)。
|
package/dist/config-fns.d.ts
CHANGED
|
@@ -175,11 +175,15 @@ export type EffectiveReadWarning = {
|
|
|
175
175
|
}
|
|
176
176
|
/** 域文档里那些**不属于该域 schema** 的键:行为照旧(zod 非 strict = 剥掉,前向兼容不动),但点名
|
|
177
177
|
* 说出来。`keys` = 点号路径(`infraCostRates.toolCalMicroUsd` / `commandPolicy.0.typo`),见
|
|
178
|
-
* {@link droppedKeys}
|
|
178
|
+
* {@link droppedKeys} 的扫描边界。**1.5.0**:①{@link EXTENSION_NAMESPACES} 登记的扩展键(别的写者按契约
|
|
179
|
+
* 放在文档里、本包刻意不认的键,如壳写在 models 条目上的 `sema*`)**不算未知键**——既不点名也不计数;
|
|
180
|
+
* ②`keys` 有界(`DROP_SCAN_MAX_KEYS`),超界时带 `truncatedFrom`(= 真实未知键总数,与 `hosts-grandfathered`
|
|
181
|
+
* 同形),消费方据此知道列表**不是穷举**——把有界诊断当穷举决策输入,是服务端消费方在配置门上三次被复审打穿的根因。 */
|
|
179
182
|
| {
|
|
180
183
|
domain: DomainName;
|
|
181
184
|
kind: "unknown-keys-dropped";
|
|
182
185
|
keys: string[];
|
|
186
|
+
truncatedFrom?: number;
|
|
183
187
|
}
|
|
184
188
|
/** open-world 域(`.passthrough()`)里**已送达但本包不认识**的键:值原样到了消费方手里(承运),
|
|
185
189
|
* 但拼错的键名同样该有人说 —— 认不认归持有键表的消费方,`keys` 是点号路径。 */
|
|
@@ -187,6 +191,7 @@ export type EffectiveReadWarning = {
|
|
|
187
191
|
domain: DomainName;
|
|
188
192
|
kind: "unknown-keys-carried";
|
|
189
193
|
keys: string[];
|
|
194
|
+
truncatedFrom?: number;
|
|
190
195
|
}
|
|
191
196
|
/** `config.d/` 里存在、但本地读取集**永远不会读**的文件(名字不是域,或是域但不便携)。 */
|
|
192
197
|
| {
|
|
@@ -195,6 +200,31 @@ export type EffectiveReadWarning = {
|
|
|
195
200
|
domain?: DomainName;
|
|
196
201
|
why: "not-a-domain" | "non-portable-domain";
|
|
197
202
|
};
|
|
203
|
+
/**
|
|
204
|
+
* **扩展命名空间**(1.5.0):域文档里**由别的写者按契约放置、本包刻意不认**的键。它们照旧被 zod strip
|
|
205
|
+
* (本包与引擎消费方看不见——这正是契约:壳自己回读文件取值),但 {@link droppedKeys} **不点名、不计数**。
|
|
206
|
+
* 没有这张表,「未知键」一个信号要同时回答「已知键拼错了」(该拒)与「别的写者有意放的扩展键」(该放)
|
|
207
|
+
* 两个问题——按键名扫描永远分不出意图,消费方只能在下游猜(服务端消费方的真案:按 kind→按域→按键路径闭集→
|
|
208
|
+
* 满额特判,四层各被下一轮复审打穿)。意图只能在**声明处**说清:写者在这里登记自己的命名空间,之后
|
|
209
|
+
* 所有未被登记的未知键都是拼错,消费方一条规则:未知键 = 错误,零特例。
|
|
210
|
+
*
|
|
211
|
+
* 登记形(分段):`at` = 父路径正则(点号形含尾点,顶层 `""`,首尾锚定,test 有门)+ `key` = 键名前缀正则;`owner` = 写者;
|
|
212
|
+
* `since` = 契约出处。新写者/新层级来这里加行,并在 CHANGELOG 点名——这是 additive(minor)。
|
|
213
|
+
*
|
|
214
|
+
* · `models` 条目级 `sema[A-Z]…`:壳(cli)的自留字段族(`semaChannel`/`semaLabel`/`semaProbe`/`semaProbeAt`/
|
|
215
|
+
* `semaEffortDefault`/`semaVision`,壳侧 models.json 实键清单 design/cli-190c:「`sema` 前缀是壳侧自留字段的既定
|
|
216
|
+
* 约定」,**全条目级、顶层零装饰键**)。顶层 `sema*` 不在此列 ⇒ 仍是未知键(拼错或越界)。
|
|
217
|
+
*/
|
|
218
|
+
export declare const EXTENSION_NAMESPACES: Partial<Record<DomainName, ReadonlyArray<{
|
|
219
|
+
readonly at: RegExp;
|
|
220
|
+
readonly key: RegExp;
|
|
221
|
+
readonly owner: string;
|
|
222
|
+
readonly since: string;
|
|
223
|
+
}>>>;
|
|
224
|
+
/** 该键是否落在 {@link EXTENSION_NAMESPACES} 登记的扩展命名空间里(⇒ 不是未知键)。**分段匹配**:`at` 对父路径(点号形,含尾点;
|
|
225
|
+
* 顶层 = `""`),`key` 对键名本身——不在拼好的点号路径上猜(键名里可以含 `.`,`semaX.foo` 是一个键不是两层),前缀契约就是
|
|
226
|
+
* 前缀契约(`semaGPT_5` / `semaX-v2` 都是成员,后缀不设未声明的文法)。 */
|
|
227
|
+
export declare function isReservedExtensionKey(domain: DomainName, parentPath: string, key: string): boolean;
|
|
198
228
|
/**
|
|
199
229
|
* 一份域文档里被域 schema **丢掉的键**(zod 对象默认 `"strip"`),点号路径形。
|
|
200
230
|
*
|
|
@@ -203,7 +233,7 @@ export type EffectiveReadWarning = {
|
|
|
203
233
|
* (新 center 的新键不该炸老消费方),所以修法不是收紧成 `.strict()`,而是让它**响亮**:
|
|
204
234
|
* {@link buildEffective} 每域解析成功后调用本函数,非空即发 `"unknown-keys-dropped"` 警告。
|
|
205
235
|
*
|
|
206
|
-
* **嵌套也扫**(
|
|
236
|
+
* **嵌套也扫**(对抗复审 复审 F2 真 finding):`{infraCostRates:{toolCalMicroUsd:10}}` 这种**拼错的嵌套
|
|
207
237
|
* 键**在顶层看不见(`infraCostRates` 键还在),只看顶层等于对计费面的错键完全失明。故按 raw↔parsed
|
|
208
238
|
* 的键集逐层对比(对象逐键、数组同长时逐下标),不依赖任何 zod 内省。
|
|
209
239
|
*
|
|
@@ -227,7 +257,7 @@ export declare function carriedUnknownKeys(domain: DomainName, parsed: unknown):
|
|
|
227
257
|
/**
|
|
228
258
|
* 一域的 parse + **静默剥键/静默承运两条告警**(0.19.0)。`buildEffective` 与 `FileConfigStore.getDomain`
|
|
229
259
|
* 共用这一个判据 —— 此前只有 effective 那条路径有门,而 `getDomain` 是 `exportBundle` 的读法
|
|
230
|
-
* (
|
|
260
|
+
* (对抗复审 复审 round2 F3:导出一份被剥了键的域再导入 = 持久的配置丢失,而且全程无声)。
|
|
231
261
|
*/
|
|
232
262
|
export declare function parseDomainLoud<K extends DomainName>(domain: K, raw: unknown, onWarning?: (w: EffectiveReadWarning) => void): DomainConfig[K];
|
|
233
263
|
/** Per-domain READ-corruption policy (0.8.1) — what buildEffective does when a stored domain no longer parses.
|
package/dist/config-fns.js
CHANGED
|
@@ -140,8 +140,33 @@ export function legacyGovernanceFromRuntime(rawRuntime) {
|
|
|
140
140
|
}
|
|
141
141
|
/** 扫描深度上限(嵌套再深的配置文档不存在;有上限才不会被恶意深链拖住)。 */
|
|
142
142
|
const DROP_SCAN_MAX_DEPTH = 8;
|
|
143
|
-
/** 一条警告最多点名多少个键(
|
|
143
|
+
/** 一条警告最多点名多少个键(告警载荷有界;超界时警告带 `truncatedFrom` = 真实总数,见 {@link droppedKeys})。 */
|
|
144
144
|
const DROP_SCAN_MAX_KEYS = 64;
|
|
145
|
+
/**
|
|
146
|
+
* **扩展命名空间**(1.5.0):域文档里**由别的写者按契约放置、本包刻意不认**的键。它们照旧被 zod strip
|
|
147
|
+
* (本包与引擎消费方看不见——这正是契约:壳自己回读文件取值),但 {@link droppedKeys} **不点名、不计数**。
|
|
148
|
+
* 没有这张表,「未知键」一个信号要同时回答「已知键拼错了」(该拒)与「别的写者有意放的扩展键」(该放)
|
|
149
|
+
* 两个问题——按键名扫描永远分不出意图,消费方只能在下游猜(服务端消费方的真案:按 kind→按域→按键路径闭集→
|
|
150
|
+
* 满额特判,四层各被下一轮复审打穿)。意图只能在**声明处**说清:写者在这里登记自己的命名空间,之后
|
|
151
|
+
* 所有未被登记的未知键都是拼错,消费方一条规则:未知键 = 错误,零特例。
|
|
152
|
+
*
|
|
153
|
+
* 登记形(分段):`at` = 父路径正则(点号形含尾点,顶层 `""`,首尾锚定,test 有门)+ `key` = 键名前缀正则;`owner` = 写者;
|
|
154
|
+
* `since` = 契约出处。新写者/新层级来这里加行,并在 CHANGELOG 点名——这是 additive(minor)。
|
|
155
|
+
*
|
|
156
|
+
* · `models` 条目级 `sema[A-Z]…`:壳(cli)的自留字段族(`semaChannel`/`semaLabel`/`semaProbe`/`semaProbeAt`/
|
|
157
|
+
* `semaEffortDefault`/`semaVision`,壳侧 models.json 实键清单 design/cli-190c:「`sema` 前缀是壳侧自留字段的既定
|
|
158
|
+
* 约定」,**全条目级、顶层零装饰键**)。顶层 `sema*` 不在此列 ⇒ 仍是未知键(拼错或越界)。
|
|
159
|
+
*/
|
|
160
|
+
export const EXTENSION_NAMESPACES = {
|
|
161
|
+
models: [{ at: /^models\.\d+\.$/, key: /^sema[A-Z]/, owner: "cli", since: "shell key census design/cli-190c (2026-08-26)" }],
|
|
162
|
+
};
|
|
163
|
+
/** 该键是否落在 {@link EXTENSION_NAMESPACES} 登记的扩展命名空间里(⇒ 不是未知键)。**分段匹配**:`at` 对父路径(点号形,含尾点;
|
|
164
|
+
* 顶层 = `""`),`key` 对键名本身——不在拼好的点号路径上猜(键名里可以含 `.`,`semaX.foo` 是一个键不是两层),前缀契约就是
|
|
165
|
+
* 前缀契约(`semaGPT_5` / `semaX-v2` 都是成员,后缀不设未声明的文法)。 */
|
|
166
|
+
export function isReservedExtensionKey(domain, parentPath, key) {
|
|
167
|
+
const rules = EXTENSION_NAMESPACES[domain];
|
|
168
|
+
return rules !== undefined && rules.some((r) => r.at.test(parentPath) && r.key.test(key));
|
|
169
|
+
}
|
|
145
170
|
/** 纯 JSON 对象(不是数组、不是类实例/带原型的东西)——只对这种做键集对比,别的形状交给 parse 判。 */
|
|
146
171
|
function isPlainJsonObject(v) {
|
|
147
172
|
if (v === null || typeof v !== "object" || Array.isArray(v))
|
|
@@ -149,30 +174,42 @@ function isPlainJsonObject(v) {
|
|
|
149
174
|
const proto = Object.getPrototypeOf(v);
|
|
150
175
|
return proto === Object.prototype || proto === null;
|
|
151
176
|
}
|
|
152
|
-
function collectDropped(raw, parsed, prefix, depth, spared,
|
|
153
|
-
if (depth > DROP_SCAN_MAX_DEPTH
|
|
177
|
+
function collectDropped(domain, raw, parsed, prefix, depth, spared, acc) {
|
|
178
|
+
if (depth > DROP_SCAN_MAX_DEPTH)
|
|
154
179
|
return;
|
|
155
180
|
if (Array.isArray(raw) && Array.isArray(parsed)) {
|
|
156
181
|
// 长度不同 = 整条元素被丢/被裁(hosts 的 grandfather clamp 有自己的警告),不在本判据内,别造重复噪声。
|
|
157
182
|
if (raw.length !== parsed.length)
|
|
158
183
|
return;
|
|
159
184
|
for (let i = 0; i < raw.length; i++)
|
|
160
|
-
collectDropped(raw[i], parsed[i], `${prefix}${i}.`, depth + 1, [],
|
|
185
|
+
collectDropped(domain, raw[i], parsed[i], `${prefix}${i}.`, depth + 1, [], acc);
|
|
161
186
|
return;
|
|
162
187
|
}
|
|
163
188
|
if (!isPlainJsonObject(raw) || !isPlainJsonObject(parsed))
|
|
164
189
|
return;
|
|
165
190
|
for (const k of Object.keys(raw)) {
|
|
166
|
-
if (out.length >= DROP_SCAN_MAX_KEYS)
|
|
167
|
-
return;
|
|
168
191
|
if (depth === 0 && spared.includes(k))
|
|
169
192
|
continue;
|
|
170
193
|
if (!Object.prototype.hasOwnProperty.call(parsed, k)) {
|
|
171
|
-
|
|
194
|
+
if (isReservedExtensionKey(domain, prefix, k))
|
|
195
|
+
continue; // 扩展命名空间:按契约放的键,不是未知键
|
|
196
|
+
acc.total += 1;
|
|
197
|
+
if (acc.keys.length < DROP_SCAN_MAX_KEYS)
|
|
198
|
+
acc.keys.push(prefix + k);
|
|
172
199
|
continue;
|
|
173
200
|
}
|
|
174
|
-
collectDropped(raw[k], parsed[k], `${prefix}${k}.`, depth + 1, spared,
|
|
201
|
+
collectDropped(domain, raw[k], parsed[k], `${prefix}${k}.`, depth + 1, spared, acc);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
function scanDropped(domain, raw, parsed) {
|
|
205
|
+
const acc = { keys: [], total: 0 };
|
|
206
|
+
try {
|
|
207
|
+
collectDropped(domain, raw, parsed, "", 0, domain === "runtime" ? GOVERNANCE_MIRROR_KEYS : [], acc);
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
return { keys: [], total: 0 }; // 反射/访问器抛错:告警是尽力而为的审计线,绝不能把解析拖下水,更不能回显其错误文案
|
|
175
211
|
}
|
|
212
|
+
return acc;
|
|
176
213
|
}
|
|
177
214
|
/**
|
|
178
215
|
* 一份域文档里被域 schema **丢掉的键**(zod 对象默认 `"strip"`),点号路径形。
|
|
@@ -182,7 +219,7 @@ function collectDropped(raw, parsed, prefix, depth, spared, out) {
|
|
|
182
219
|
* (新 center 的新键不该炸老消费方),所以修法不是收紧成 `.strict()`,而是让它**响亮**:
|
|
183
220
|
* {@link buildEffective} 每域解析成功后调用本函数,非空即发 `"unknown-keys-dropped"` 警告。
|
|
184
221
|
*
|
|
185
|
-
* **嵌套也扫**(
|
|
222
|
+
* **嵌套也扫**(对抗复审 复审 F2 真 finding):`{infraCostRates:{toolCalMicroUsd:10}}` 这种**拼错的嵌套
|
|
186
223
|
* 键**在顶层看不见(`infraCostRates` 键还在),只看顶层等于对计费面的错键完全失明。故按 raw↔parsed
|
|
187
224
|
* 的键集逐层对比(对象逐键、数组同长时逐下标),不依赖任何 zod 内省。
|
|
188
225
|
*
|
|
@@ -195,20 +232,13 @@ function collectDropped(raw, parsed, prefix, depth, spared, out) {
|
|
|
195
232
|
* 让其错误外泄(错误文案可能带 secret,与 parseDomain 的 R13 同纪律),扫描失败即当作"没看见",不影响解析。
|
|
196
233
|
*/
|
|
197
234
|
export function droppedKeys(domain, raw, parsed) {
|
|
198
|
-
|
|
199
|
-
try {
|
|
200
|
-
collectDropped(raw, parsed, "", 0, domain === "runtime" ? GOVERNANCE_MIRROR_KEYS : [], out);
|
|
201
|
-
}
|
|
202
|
-
catch {
|
|
203
|
-
return []; // 反射/访问器抛错:告警是尽力而为的审计线,绝不能把解析拖下水,更不能回显其错误文案
|
|
204
|
-
}
|
|
205
|
-
return out;
|
|
235
|
+
return scanDropped(domain, raw, parsed).keys;
|
|
206
236
|
}
|
|
207
237
|
/**
|
|
208
238
|
* open-world 域(`.passthrough()`)的**已知键表**:路径 → 该层已知键(`""` = 顶层)。键集从 schema 的
|
|
209
239
|
* `shape` **单源派生**(手抄一份必然漂),路径表本身是显式的——嵌套只在真有 open-world 子对象的地方登记。
|
|
210
240
|
*
|
|
211
|
-
* 为什么需要它(
|
|
241
|
+
* 为什么需要它(对抗复审 复审 round2 F2):承运解决了「值到不了」,但没解决「键名拼错了没人说」——
|
|
212
242
|
* `toolResultsTtlSec`(多一个 s)会完好穿过本包,而消费方只遍历自己的键表,于是那条天花板悄悄没生效。
|
|
213
243
|
* 承运 + 点名两件一起做:值照送(消费方有权认新键),名照报(拼错的有人说)。
|
|
214
244
|
*
|
|
@@ -226,38 +256,44 @@ const OPEN_WORLD_KNOWN_KEYS = {
|
|
|
226
256
|
* 子树)不再下探,那本就是消费方自定义的内容,逐层猜没有意义。
|
|
227
257
|
*/
|
|
228
258
|
export function carriedUnknownKeys(domain, parsed) {
|
|
259
|
+
return scanCarried(domain, parsed).keys;
|
|
260
|
+
}
|
|
261
|
+
function scanCarried(domain, parsed) {
|
|
229
262
|
const table = OPEN_WORLD_KNOWN_KEYS[domain];
|
|
263
|
+
const acc = { keys: [], total: 0 };
|
|
230
264
|
if (!table || !isPlainJsonObject(parsed))
|
|
231
|
-
return
|
|
232
|
-
const out = [];
|
|
265
|
+
return acc;
|
|
233
266
|
for (const [path, known] of Object.entries(table)) {
|
|
234
267
|
const node = path === "" ? parsed : parsed[path];
|
|
235
268
|
if (!isPlainJsonObject(node))
|
|
236
269
|
continue;
|
|
237
270
|
const prefix = path === "" ? "" : `${path}.`;
|
|
238
271
|
for (const k of Object.keys(node)) {
|
|
239
|
-
if (
|
|
240
|
-
|
|
241
|
-
if (
|
|
242
|
-
|
|
272
|
+
if (known.includes(k))
|
|
273
|
+
continue;
|
|
274
|
+
if (isReservedExtensionKey(domain, prefix, k))
|
|
275
|
+
continue;
|
|
276
|
+
acc.total += 1;
|
|
277
|
+
if (acc.keys.length < DROP_SCAN_MAX_KEYS)
|
|
278
|
+
acc.keys.push(prefix + k);
|
|
243
279
|
}
|
|
244
280
|
}
|
|
245
|
-
return
|
|
281
|
+
return acc;
|
|
246
282
|
}
|
|
247
283
|
/**
|
|
248
284
|
* 一域的 parse + **静默剥键/静默承运两条告警**(0.19.0)。`buildEffective` 与 `FileConfigStore.getDomain`
|
|
249
285
|
* 共用这一个判据 —— 此前只有 effective 那条路径有门,而 `getDomain` 是 `exportBundle` 的读法
|
|
250
|
-
* (
|
|
286
|
+
* (对抗复审 复审 round2 F3:导出一份被剥了键的域再导入 = 持久的配置丢失,而且全程无声)。
|
|
251
287
|
*/
|
|
252
288
|
export function parseDomainLoud(domain, raw, onWarning) {
|
|
253
289
|
const parsed = parseDomain(domain, raw);
|
|
254
290
|
if (onWarning) {
|
|
255
|
-
const dropped =
|
|
256
|
-
if (dropped.length > 0)
|
|
257
|
-
onWarning({ domain, kind: "unknown-keys-dropped", keys: dropped });
|
|
258
|
-
const carried =
|
|
259
|
-
if (carried.length > 0)
|
|
260
|
-
onWarning({ domain, kind: "unknown-keys-carried", keys: carried });
|
|
291
|
+
const dropped = scanDropped(domain, raw, parsed);
|
|
292
|
+
if (dropped.keys.length > 0)
|
|
293
|
+
onWarning({ domain, kind: "unknown-keys-dropped", keys: dropped.keys, ...(dropped.total > dropped.keys.length ? { truncatedFrom: dropped.total } : {}) });
|
|
294
|
+
const carried = scanCarried(domain, parsed);
|
|
295
|
+
if (carried.keys.length > 0)
|
|
296
|
+
onWarning({ domain, kind: "unknown-keys-carried", keys: carried.keys, ...(carried.total > carried.keys.length ? { truncatedFrom: carried.total } : {}) });
|
|
261
297
|
}
|
|
262
298
|
return parsed;
|
|
263
299
|
}
|
package/dist/file-store.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { type DomainConfig, type DomainName, type EffectiveConfig } from "./type
|
|
|
5
5
|
* **default**域集 of the bundle helpers (`exportBundle`/`readBundle`). 每个被排除的域必须在
|
|
6
6
|
* {@link NON_PORTABLE_DOMAIN_REASONS} 里有一条成文理由(禁裸排除,#282 件5 附带裁定)。
|
|
7
7
|
*
|
|
8
|
-
* ⚠️ 口径诚实(
|
|
8
|
+
* ⚠️ 口径诚实(对抗复审 复审 round1 F4):这是**默认集与本地读取集,不是 API 级硬边界** —— `exportBundle`
|
|
9
9
|
* / `readBundle` 的 `domains` 参数可显式传任意 `DomainName`(bundle.ts 不拒非便携域)。把非便携域搬进
|
|
10
10
|
* bundle 的那条口子归调用方纪律(以及各自域的导出面),不是本常量能挡住的;要不要把 bundle API 收窄到
|
|
11
11
|
* `PortableDomain` 是一个独立的行为面裁定(#282 未决项,需三问),本批不擅自改。
|
|
@@ -22,7 +22,7 @@ export type PortableDomain = (typeof PORTABLE_DOMAINS)[number];
|
|
|
22
22
|
*
|
|
23
23
|
* `satisfies Record<Exclude<DomainName, PortableDomain>, string>` 让键集在**编译期**双向咬死:新开一个
|
|
24
24
|
* 域却不放进 PORTABLE_DOMAINS ⇒ 这里缺行 ⇒ 编译错;把某域改成便携却忘了删行 ⇒ 多余键 ⇒ 编译错。
|
|
25
|
-
* (
|
|
25
|
+
* (此前是「理由写在注释里」,机器只能做子串匹配——域名出现在文件任何角落都算数,等于没门;对抗复审
|
|
26
26
|
* 复审 round1 F3。)理由要写**为什么它不该被单机/跨部署搬运**,带出处。
|
|
27
27
|
*/
|
|
28
28
|
export declare const NON_PORTABLE_DOMAIN_REASONS: {
|
package/dist/file-store.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* LOCAL FILE LAYOUT (the TOC owns the root, e.g. `~/.ai-agent/` or a project-local `.ai-agent/`):
|
|
8
8
|
* <root>/config.d/<domain>.json for every domain in {@link PORTABLE_DOMAINS} — that constant is the
|
|
9
9
|
* AUTHORITY; do NOT hand-copy the list here (the hand-written enumeration that used to sit
|
|
10
|
-
* on this line silently missed `prompts` for three releases —
|
|
10
|
+
* on this line silently missed `prompts` for three releases — 对抗复审 复审 round2 F5).
|
|
11
11
|
* <root>/.env secret VALUES (KEY=value) — the ONLY file holding secrets; gitignored, never read here.
|
|
12
12
|
*
|
|
13
13
|
* Rules:
|
|
@@ -30,7 +30,7 @@ import { isDomainName } from "./types.js";
|
|
|
30
30
|
* **default**域集 of the bundle helpers (`exportBundle`/`readBundle`). 每个被排除的域必须在
|
|
31
31
|
* {@link NON_PORTABLE_DOMAIN_REASONS} 里有一条成文理由(禁裸排除,#282 件5 附带裁定)。
|
|
32
32
|
*
|
|
33
|
-
* ⚠️ 口径诚实(
|
|
33
|
+
* ⚠️ 口径诚实(对抗复审 复审 round1 F4):这是**默认集与本地读取集,不是 API 级硬边界** —— `exportBundle`
|
|
34
34
|
* / `readBundle` 的 `domains` 参数可显式传任意 `DomainName`(bundle.ts 不拒非便携域)。把非便携域搬进
|
|
35
35
|
* bundle 的那条口子归调用方纪律(以及各自域的导出面),不是本常量能挡住的;要不要把 bundle API 收窄到
|
|
36
36
|
* `PortableDomain` 是一个独立的行为面裁定(#282 未决项,需三问),本批不擅自改。
|
|
@@ -49,7 +49,7 @@ const NON_DOMAIN_CONFIG_FILES = new Set(["remote-exec.json"]);
|
|
|
49
49
|
*
|
|
50
50
|
* `satisfies Record<Exclude<DomainName, PortableDomain>, string>` 让键集在**编译期**双向咬死:新开一个
|
|
51
51
|
* 域却不放进 PORTABLE_DOMAINS ⇒ 这里缺行 ⇒ 编译错;把某域改成便携却忘了删行 ⇒ 多余键 ⇒ 编译错。
|
|
52
|
-
* (
|
|
52
|
+
* (此前是「理由写在注释里」,机器只能做子串匹配——域名出现在文件任何角落都算数,等于没门;对抗复审
|
|
53
53
|
* 复审 round1 F3。)理由要写**为什么它不该被单机/跨部署搬运**,带出处。
|
|
54
54
|
*/
|
|
55
55
|
export const NON_PORTABLE_DOMAIN_REASONS = {
|
|
@@ -185,7 +185,7 @@ export class FileConfigStore {
|
|
|
185
185
|
const raw = await this.#readDomainRaw(domain);
|
|
186
186
|
if (raw === undefined)
|
|
187
187
|
return undefined; // missing/empty file → domain absent (not schema defaults)
|
|
188
|
-
// 0.19.0(
|
|
188
|
+
// 0.19.0(对抗复审 复审 round2 F3):与 buildEffective **同一条** parse-and-warn 判据。此前本方法直接
|
|
189
189
|
// parseDomain,于是 `exportBundle`(它就是逐域走 getDomain 读的)会把一份携带未知键的域**静默剥**
|
|
190
190
|
// 成一份"干净"的 bundle —— 再 import 回去就是一次持久的配置丢失,全程零告警。
|
|
191
191
|
return parseDomainLoud(domain, raw, this.opts.onWarning);
|
|
@@ -85,7 +85,7 @@ export type MemberDirection = z.infer<typeof MemberDirection>;
|
|
|
85
85
|
/** One member's admission row. `minTrust` ABSENT ⇒ the category's `minTrust` applies: a row can refine the floor
|
|
86
86
|
* only by SAYING so; omission never lowers it (fail-closed by construction).
|
|
87
87
|
*
|
|
88
|
-
* `boolPole` (S-59 件八,
|
|
88
|
+
* `boolPole` (S-59 件八, 对抗复审 r3 [high]) — VALUE-DIRECTIONAL admission for a boolean member whose two poles
|
|
89
89
|
* point opposite ways (`enabled:true` narrows the sandbox, `enabled:false` opens it): a write EQUAL to
|
|
90
90
|
* `boolPole.strict` (strict equality, never truthiness — a string "true" is not the pole) is the tightening
|
|
91
91
|
* write and is admitted at `boolPole.minTrust`; ANY other write — the loose pole, a non-boolean, or a
|
|
@@ -591,7 +591,7 @@ export type SafetyMergeCategory = keyof typeof SAFETY_MERGE_SPEC;
|
|
|
591
591
|
* DISTRIBUTIVE over `C` on purpose: a resolver holds a WIDENED `SafetyMergeCategory` (out of its key→category
|
|
592
592
|
* map), and a non-distributive form would index the whole union at once, fail the `members` check on the
|
|
593
593
|
* whole-key categories and collapse to `never` — typing `membersOf(category)` as `readonly never[]` at exactly
|
|
594
|
-
* the seam these helpers exist for (
|
|
594
|
+
* the seam these helpers exist for (对抗复审 R2-F1). */
|
|
595
595
|
export type SafetyMergeMember<C extends SafetyMergeCategory> = C extends SafetyMergeCategory ? (typeof SAFETY_MERGE_SPEC)[C] extends {
|
|
596
596
|
readonly members: infer M;
|
|
597
597
|
} ? Extract<keyof M, string> : never : never;
|
|
@@ -604,7 +604,7 @@ export declare function membersOf<C extends SafetyMergeCategory>(category: C): r
|
|
|
604
604
|
* Fail-CLOSED and loud on both misuse arms — a category without `members` has no member to name, and a member
|
|
605
605
|
* outside the declared set is a caller bug, not "use the category floor" (that silent arm is exactly what would
|
|
606
606
|
* let a typo admit a bucket under the wrong rule).
|
|
607
|
-
* Value-aware (
|
|
607
|
+
* Value-aware (对抗复审 r3): pass the CANDIDATE VALUE being written for a value-directional (`boolPole`) member —
|
|
608
608
|
* the strict pole resolves to `boolPole.minTrust`, everything else (loose pole / non-boolean / no value) to the
|
|
609
609
|
* row floor. Rows without `boolPole` ignore the value entirely.
|
|
610
610
|
* @param categoryLabel only decorates the error message (the spec value carries no name of its own).
|
|
@@ -97,7 +97,7 @@ export const MemberDirection = z.enum(["loosen", "tighten"]);
|
|
|
97
97
|
/** One member's admission row. `minTrust` ABSENT ⇒ the category's `minTrust` applies: a row can refine the floor
|
|
98
98
|
* only by SAYING so; omission never lowers it (fail-closed by construction).
|
|
99
99
|
*
|
|
100
|
-
* `boolPole` (S-59 件八,
|
|
100
|
+
* `boolPole` (S-59 件八, 对抗复审 r3 [high]) — VALUE-DIRECTIONAL admission for a boolean member whose two poles
|
|
101
101
|
* point opposite ways (`enabled:true` narrows the sandbox, `enabled:false` opens it): a write EQUAL to
|
|
102
102
|
* `boolPole.strict` (strict equality, never truthiness — a string "true" is not the pole) is the tightening
|
|
103
103
|
* write and is admitted at `boolPole.minTrust`; ANY other write — the loose pole, a non-boolean, or a
|
|
@@ -183,7 +183,7 @@ export const SAFETY_MERGE_SPEC = {
|
|
|
183
183
|
// · network.allowAllUnixSockets / network.allowLocalBinding `false` = the opening stays shut ⇒ tighten;
|
|
184
184
|
// · filesystem.allowManagedReadPathsOnly `true` ⇒ tighten (same managed-only consumer caveat);
|
|
185
185
|
// · filesystem.denyWrite / filesystem.denyRead — deny arrays only ever narrow ⇒ tighten in every write.
|
|
186
|
-
// A boolean member's admission is VALUE-directional and the pole is MODELED (
|
|
186
|
+
// A boolean member's admission is VALUE-directional and the pole is MODELED (对抗复审 r3 [high]): each boolean
|
|
187
187
|
// row carries `boolPole {strict, minTrust:"local"}` and keeps its ROW floor at the loosen floor "global" —
|
|
188
188
|
// value-aware consumers admit the strict pole at "local"; pole-blind consumers (whose schema strips boolPole)
|
|
189
189
|
// read tighten@global and withhold BOTH poles from low-trust layers (fail-closed: convenience lost, never a
|
|
@@ -251,7 +251,7 @@ export function membersOf(category) {
|
|
|
251
251
|
* Fail-CLOSED and loud on both misuse arms — a category without `members` has no member to name, and a member
|
|
252
252
|
* outside the declared set is a caller bug, not "use the category floor" (that silent arm is exactly what would
|
|
253
253
|
* let a typo admit a bucket under the wrong rule).
|
|
254
|
-
* Value-aware (
|
|
254
|
+
* Value-aware (对抗复审 r3): pass the CANDIDATE VALUE being written for a value-directional (`boolPole`) member —
|
|
255
255
|
* the strict pole resolves to `boolPole.minTrust`, everything else (loose pole / non-boolean / no value) to the
|
|
256
256
|
* row floor. Rows without `boolPole` ignore the value entirely.
|
|
257
257
|
* @param categoryLabel only decorates the error message (the spec value carries no name of its own).
|
package/dist/types.js
CHANGED
|
@@ -746,7 +746,7 @@ export const LimitsConfig = z.object({
|
|
|
746
746
|
// 在上一份好配置);未知键原样承运,由消费方(键表在它手里)决定认或不认。
|
|
747
747
|
// · 附带收益:老编辑器(TOC)写回新 center 的 limits 文档时不会把自己不认识的键写没了(strip 语义下
|
|
748
748
|
// writeDomainFile 会真的丢键)。
|
|
749
|
-
// · **承运 ≠ 沉默**(
|
|
749
|
+
// · **承运 ≠ 沉默**(对抗复审 复审 round2 F2):未知成员会被 `"unknown-keys-carried"` 警告逐个点名
|
|
750
750
|
// (config-fns 的已知键表由本 schema 的 shape 单源派生)——拼错的键名于是有人说,而值仍然到得了
|
|
751
751
|
// 消费方手里,两头都不吃亏。
|
|
752
752
|
// · 失效条件(如实):若限额面将来收敛为「契约先行、消费方只读已声明键」,本论证不再成立;届时改回
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sema-agent/settings-schema",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.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",
|