@sema-agent/settings-schema 1.4.1 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,54 @@
6
6
  > `@sema-agent/registry-core`(更早还曾是 `@sema-ai/registry-core`)。本档 `0.19.0` 以下各节里的
7
7
  > 包名是**历史记录**,如实保留不追改。
8
8
 
9
+ ## 1.6.0 — 2026-09-05 — `settings.json` 跨会话两键 `crossSessionInbound` / `dialogExpiry`(新模块/新导出=minor)
10
+
11
+ 成案:跨终端会话设计稿(design/385 §4.4/§8)把这两个 settings 键的属主定在 worker 服务侧,而键的**格式契约**
12
+ 按本包定位落这里(与 `hooks` 同姿势:本包出 zod 形,引擎出判定,宿主出装配)。此前 worker 服务的 settings 键闭集门
13
+ 不认这两个键 ⇒ 一份合法的 CC `settings.json` 带上它们会被整请求 400;本节是那条链的上游一格。
14
+
15
+ **新模块 `src/cross-session.ts`(主 barrel 导出,无新 subpath ⇒ `exports` 表 21 项不变)**:
16
+
17
+ - `crossSessionInbound` —— 显式闭集 `CROSS_SESSION_INBOUND_VALUES = ["accept","hold","refuse"]`,**顺序即收紧秩**
18
+ (accept < hold < refuse:层次解析只许沿此序收紧);UI 第四拼写 `CROSS_SESSION_INBOUND_UNSET = "default"`
19
+ = **未设**(不是第四种策略),未设 ⇒ mode-parity。CC describe 逐字锚:「Inbound cross-session peer messages
20
+ (SendMessage from your other sessions)」。
21
+ - `dialogExpiry` —— `DIALOG_EXPIRY_VALUES = ["60s","5m","10m","never"]` + `DIALOG_EXPIRY_DEFAULT = "5m"`。
22
+ CC describe 明写**两个**消费面(远端停靠的审批对话框 / 停审的跨会话消息),本包只声明键形,宿主自陈消费哪一面。
23
+ - zod 受理面 `CrossSessionInboundSetting` / `DialogExpirySetting`;片段 schema `CrossSessionSettings`
24
+ (两键可选 + `.passthrough()` —— 本片段**不是**整份 settings 的闭集门)。
25
+ - 层读取器 `readCrossSessionInbound` / `readDialogExpiry` → `SettingLayerRead<T>` 三态(未设 / 显式值 / 词表外)。
26
+ **坏值方向 = 响亮**:词表外恒 `ok:false` 交调用方拒,与 CC 文件层的 `.catch(undefined)` 静默吞成未设有意分岔
27
+ (一个大小写拼错静默读成「未设」会把用户以为已关的入站车道悄悄打开)。
28
+
29
+ **锁步义务(写进模块头注)**:引擎侧另有一份同词表(判定纯函数的输入域),两份必须逐字相等;等值由同时装了本包
30
+ 与引擎的消费方用一格锁步钉住——本包保持纯度不变量,不 import 引擎。
31
+
32
+ 红先:`test/cross-session-1.6.test.ts` 20 格,落 `src/cross-session.ts` 之前首跑红(模块不存在)。
33
+
34
+ ## 1.5.0 — 2026-09-04 — 扩展命名空间 + 剥键告警截断标志(sema-server S-118 源头修;新键/新导出=minor)
35
+
36
+ 成案:sema-server 修 cli 用户 P1「`/model` 热加载被装饰键整拒」时在下游连打四层补丁(按 kind 放宽→按域→按键路径闭集→
37
+ 64 满额特判),每层都被下一轮复审打穿;clay 裁「源头修完再发」+ 抽象优先四原则(sema-comms [6351])。根因在本包:
38
+ 「未知键」一个信号被要求同时回答「已知键拼错」(该拒)与「别的写者有意放的扩展键」(该放),按键名扫描分不出意图——
39
+ 意图只能在**声明处**说清。
40
+
41
+ **① `EXTENSION_NAMESPACES`(新导出)+ `isReservedExtensionKey(domain, parentPath, key)`(新导出,分段匹配:父路径 + 键名前缀,不在点号路径上猜——对抗复审 [medium] 采:`semaX-v2`/`semaX.foo` 这类合法前缀成员此前会被后缀文法误报)**:域文档里由别的写者按契约
42
+ 放置、本包刻意不认的键。登记的键照旧被 zod strip(行为零变,写者自己回读文件),但 {@link droppedKeys} / `parseDomainLoud`
43
+ **不点名、不计数**。首条登记:`models` 条目级 `sema[A-Z]…`(壳 cli 的自留字段族,cli [5273] design/cli-190c 实键清单
44
+ 六键 `semaChannel`/`semaLabel`/`semaProbe`/`semaProbeAt`/`semaEffortDefault`/`semaVision`,全条目级、顶层零装饰键 ⇒ 顶层
45
+ `sema*` 仍是未知键)。消费方从此一条规则:**未登记的未知键 = 拼错 = 错误,零特例**。新写者/新层级加行 = minor。
46
+
47
+ **② `unknown-keys-dropped` / `unknown-keys-carried` 告警加可选 `truncatedFrom?: number`**(与 `hosts-grandfathered.truncatedFrom`
48
+ 同形):`keys` 仍有界(`DROP_SCAN_MAX_KEYS`=64),超界时带真实总数——扫描本就 O(文档键数),上限只界告警载荷不界扫描。
49
+ 把有界列表当穷举决策输入正是 S-118 第四层被打穿的形(13 模型 × 6 装饰键填满 64 后拼错键不在列表里);①之后扩展键不计数,
50
+ ②让剩下的截断在消费方看得见。
51
+
52
+ 钉 `test/extension-namespace-1.5.test.ts` 七格(六装饰键零告警且仍 strip / 顶层 sema* 照报 / 前缀契约即前缀(`semaGPT_5`/`semaX-v2`/`semaX.foo` 成员,`semax`/`Sema` 不是)/ S-118 真案
53
+ 78 扩展键 + 1 拼错恰点名 1 / 表门:锚定正则+真实域名 / dropped 与 carried 各自 64+truncatedFrom)。`droppedKeys` /
54
+ `carriedUnknownKeys` 导出签名不变(仍返回有界 `string[]`,是同一次扫描的投影)。消费方:sema-server(提货后删下游四层
55
+ 特判与 `domainNotices` 机制)、cli(不消费告警面,零改;实键面若变请改表)。
56
+
9
57
  ## 1.4.1 — 2026-09-02 — 文案 patch(cli [6037]/[6038] 请件)
10
58
 
11
59
  - 复审出处注词族全清:src 12 处+d.ts 同族(`codex …`→`对抗复审 …`,语义零损)——1.4.0 件⑦「52→1」的三族 regex 未含此词族=漏计根因;npm 包注释=对外文案面,壳 bundle hygiene 禁表命中即发布腿红。
@@ -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
  *
@@ -140,8 +140,33 @@ export function legacyGovernanceFromRuntime(rawRuntime) {
140
140
  }
141
141
  /** 扫描深度上限(嵌套再深的配置文档不存在;有上限才不会被恶意深链拖住)。 */
142
142
  const DROP_SCAN_MAX_DEPTH = 8;
143
- /** 一条警告最多点名多少个键(告警载荷有界;截断即说明,见 {@link droppedKeys})。 */
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, out) {
153
- if (depth > DROP_SCAN_MAX_DEPTH || out.length >= DROP_SCAN_MAX_KEYS)
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, [], out);
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
- out.push(prefix + k);
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, out);
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"`),点号路径形。
@@ -195,14 +232,7 @@ function collectDropped(raw, parsed, prefix, depth, spared, out) {
195
232
  * 让其错误外泄(错误文案可能带 secret,与 parseDomain 的 R13 同纪律),扫描失败即当作"没看见",不影响解析。
196
233
  */
197
234
  export function droppedKeys(domain, raw, parsed) {
198
- const out = [];
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 的
@@ -226,23 +256,29 @@ 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 (out.length >= DROP_SCAN_MAX_KEYS)
240
- return out;
241
- if (!known.includes(k))
242
- out.push(prefix + k);
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 out;
281
+ return acc;
246
282
  }
247
283
  /**
248
284
  * 一域的 parse + **静默剥键/静默承运两条告警**(0.19.0)。`buildEffective` 与 `FileConfigStore.getDomain`
@@ -252,12 +288,12 @@ export function carriedUnknownKeys(domain, parsed) {
252
288
  export function parseDomainLoud(domain, raw, onWarning) {
253
289
  const parsed = parseDomain(domain, raw);
254
290
  if (onWarning) {
255
- const dropped = droppedKeys(domain, raw, parsed);
256
- if (dropped.length > 0)
257
- onWarning({ domain, kind: "unknown-keys-dropped", keys: dropped });
258
- const carried = carriedUnknownKeys(domain, parsed);
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
  }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * `settings.json` 的**跨会话两键** —— CC-parity 契约(`crossSessionInbound` / `dialogExpiry`)。
3
+ *
4
+ * 与 `hooks.ts` 同姿势:本包只出**格式契约**(纯 zod、零运行时依赖、浏览器安全),不做任何解释/判定。
5
+ * 判定(层次收紧、mode-parity、hold 因由)归引擎;写行、渲染、下发归各宿主。
6
+ *
7
+ * ## 两键的 CC 锚(逐字核过的语料读数,非转述)
8
+ *
9
+ * - `crossSessionInbound: enum(["accept","hold","refuse"])` —— describe:「Inbound cross-session peer
10
+ * messages (SendMessage from your other sessions)」:`accept` 投递、`hold` 停在收件方的待审队列
11
+ * (模型看不见、不能据此行动)、`refuse` 让本会话整体退出这条车道。**显式值恒赢**;未设 =
12
+ * mode-parity(发送方与收件方的权限档位同类才自动投递,不同类停审)。
13
+ * - `dialogExpiry: enum(["60s","5m","10m","never"])`,缺省 `"5m"` —— describe:「Max time a
14
+ * permission/user dialog forwarded to a remote client stays parked awaiting an answer, and how long a
15
+ * HELD cross-session message awaits approval」。**注意它有两个消费面**:远端停靠的审批对话框,与
16
+ * 停审的跨会话消息;本模块只声明键形,哪个宿主消费哪一面由宿主自陈。
17
+ *
18
+ * ## `"default"` 这第四个拼写
19
+ *
20
+ * 设置 UI 的选择器需要一个「不设」的可写值,CC 用字面量 `"default"`。它**不是第四种策略**:读取器
21
+ * (`readCrossSessionInbound` / `readDialogExpiry`)把它与 `null`/缺席一同折成 `undefined`。所以
22
+ * `CrossSessionInboundSetting` 的受理面是四拼写,而**值域闭集**(`CROSS_SESSION_INBOUND_VALUES`)只有三个。
23
+ *
24
+ * ## 坏值方向 = 响亮,不静默
25
+ *
26
+ * 词表外的值一律 `ok:false`,由调用方拒(HTTP 门 400 / 文件层告警)。CC 在文件层用 `.catch(undefined)`
27
+ * 静默吞成未设;本栈的坏值响亮律与之有意分岔——一个 `crossSessionInbound: "Hold"`(大小写拼错)静默
28
+ * 读成「未设」会把用户以为已经关掉的入站车道悄悄打开,那正是保护型配置最不能有的沉默。
29
+ *
30
+ * ## 与引擎词表的锁步
31
+ *
32
+ * 引擎另有一份同词表(判定纯函数的输入域)。两份**必须逐字相等**;等值由消费方(同时装了本包与引擎
33
+ * 的那一侧)用一格锁步钉住 —— 任一侧加词/改序,那一格先红。本包不 import 引擎(纯度不变量)。
34
+ */
35
+ import { z } from "zod";
36
+ /** 显式三值,**顺序即收紧秩**(accept < hold < refuse):层次解析只许沿此序收紧,永不放宽。 */
37
+ export declare const CROSS_SESSION_INBOUND_VALUES: readonly ["accept", "hold", "refuse"];
38
+ export type CrossSessionInboundValue = (typeof CROSS_SESSION_INBOUND_VALUES)[number];
39
+ /** 设置 UI 的「不设」拼写(见头注)——两键共用同一个字面量。 */
40
+ export declare const CROSS_SESSION_INBOUND_UNSET: "default";
41
+ /** 文件层受理面:三个显式值 + `"default"`(未设)。 */
42
+ export declare const CrossSessionInboundSetting: z.ZodEnum<["accept", "hold", "refuse", "default"]>;
43
+ export type CrossSessionInboundSettingValue = z.infer<typeof CrossSessionInboundSetting>;
44
+ /** 四值闭集(CC 逐字)。`"never"` = 不设期限。 */
45
+ export declare const DIALOG_EXPIRY_VALUES: readonly ["60s", "5m", "10m", "never"];
46
+ export type DialogExpiryValue = (typeof DIALOG_EXPIRY_VALUES)[number];
47
+ /** 未设时生效的值(CC 缺省)。 */
48
+ export declare const DIALOG_EXPIRY_DEFAULT: DialogExpiryValue;
49
+ /** 文件层受理面:四个值 + `"default"`(未设 ⇒ 落 {@link DIALOG_EXPIRY_DEFAULT})。 */
50
+ export declare const DialogExpirySetting: z.ZodEnum<["60s", "5m", "10m", "never", "default"]>;
51
+ export type DialogExpirySettingValue = z.infer<typeof DialogExpirySetting>;
52
+ /**
53
+ * 整份 `settings.json` 里**本片段负责的两键**。刻意 `passthrough`:本片段不是整份 settings 的闭集门
54
+ * (那道门归各宿主的受理面——宿主消费哪些键、拒哪些键,是宿主的事)。它只保证:**这两个键若在场,
55
+ * 值必合法**。
56
+ */
57
+ export declare const CrossSessionSettings: z.ZodObject<{
58
+ crossSessionInbound: z.ZodOptional<z.ZodEnum<["accept", "hold", "refuse", "default"]>>;
59
+ dialogExpiry: z.ZodOptional<z.ZodEnum<["60s", "5m", "10m", "never", "default"]>>;
60
+ }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
61
+ crossSessionInbound: z.ZodOptional<z.ZodEnum<["accept", "hold", "refuse", "default"]>>;
62
+ dialogExpiry: z.ZodOptional<z.ZodEnum<["60s", "5m", "10m", "never", "default"]>>;
63
+ }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
64
+ crossSessionInbound: z.ZodOptional<z.ZodEnum<["accept", "hold", "refuse", "default"]>>;
65
+ dialogExpiry: z.ZodOptional<z.ZodEnum<["60s", "5m", "10m", "never", "default"]>>;
66
+ }, z.ZodTypeAny, "passthrough">>;
67
+ export type CrossSessionSettings = z.infer<typeof CrossSessionSettings>;
68
+ /** 一层读数:`ok:false` = 词表外(调用方响亮拒);`ok:true` 无 `value` = 未设;有 `value` = 显式值。 */
69
+ export type SettingLayerRead<T> = {
70
+ ok: true;
71
+ value?: T;
72
+ } | {
73
+ ok: false;
74
+ };
75
+ /** 读一层的 `crossSessionInbound` 原始值。 */
76
+ export declare function readCrossSessionInbound(raw: unknown): SettingLayerRead<CrossSessionInboundValue>;
77
+ /** 读一层的 `dialogExpiry` 原始值(未设 ⇒ 由消费方落 {@link DIALOG_EXPIRY_DEFAULT},本函数不替它落)。 */
78
+ export declare function readDialogExpiry(raw: unknown): SettingLayerRead<DialogExpiryValue>;
@@ -0,0 +1,74 @@
1
+ /**
2
+ * `settings.json` 的**跨会话两键** —— CC-parity 契约(`crossSessionInbound` / `dialogExpiry`)。
3
+ *
4
+ * 与 `hooks.ts` 同姿势:本包只出**格式契约**(纯 zod、零运行时依赖、浏览器安全),不做任何解释/判定。
5
+ * 判定(层次收紧、mode-parity、hold 因由)归引擎;写行、渲染、下发归各宿主。
6
+ *
7
+ * ## 两键的 CC 锚(逐字核过的语料读数,非转述)
8
+ *
9
+ * - `crossSessionInbound: enum(["accept","hold","refuse"])` —— describe:「Inbound cross-session peer
10
+ * messages (SendMessage from your other sessions)」:`accept` 投递、`hold` 停在收件方的待审队列
11
+ * (模型看不见、不能据此行动)、`refuse` 让本会话整体退出这条车道。**显式值恒赢**;未设 =
12
+ * mode-parity(发送方与收件方的权限档位同类才自动投递,不同类停审)。
13
+ * - `dialogExpiry: enum(["60s","5m","10m","never"])`,缺省 `"5m"` —— describe:「Max time a
14
+ * permission/user dialog forwarded to a remote client stays parked awaiting an answer, and how long a
15
+ * HELD cross-session message awaits approval」。**注意它有两个消费面**:远端停靠的审批对话框,与
16
+ * 停审的跨会话消息;本模块只声明键形,哪个宿主消费哪一面由宿主自陈。
17
+ *
18
+ * ## `"default"` 这第四个拼写
19
+ *
20
+ * 设置 UI 的选择器需要一个「不设」的可写值,CC 用字面量 `"default"`。它**不是第四种策略**:读取器
21
+ * (`readCrossSessionInbound` / `readDialogExpiry`)把它与 `null`/缺席一同折成 `undefined`。所以
22
+ * `CrossSessionInboundSetting` 的受理面是四拼写,而**值域闭集**(`CROSS_SESSION_INBOUND_VALUES`)只有三个。
23
+ *
24
+ * ## 坏值方向 = 响亮,不静默
25
+ *
26
+ * 词表外的值一律 `ok:false`,由调用方拒(HTTP 门 400 / 文件层告警)。CC 在文件层用 `.catch(undefined)`
27
+ * 静默吞成未设;本栈的坏值响亮律与之有意分岔——一个 `crossSessionInbound: "Hold"`(大小写拼错)静默
28
+ * 读成「未设」会把用户以为已经关掉的入站车道悄悄打开,那正是保护型配置最不能有的沉默。
29
+ *
30
+ * ## 与引擎词表的锁步
31
+ *
32
+ * 引擎另有一份同词表(判定纯函数的输入域)。两份**必须逐字相等**;等值由消费方(同时装了本包与引擎
33
+ * 的那一侧)用一格锁步钉住 —— 任一侧加词/改序,那一格先红。本包不 import 引擎(纯度不变量)。
34
+ */
35
+ import { z } from "zod";
36
+ // ── crossSessionInbound ────────────────────────────────────────────────────────────────────────────
37
+ /** 显式三值,**顺序即收紧秩**(accept < hold < refuse):层次解析只许沿此序收紧,永不放宽。 */
38
+ export const CROSS_SESSION_INBOUND_VALUES = ["accept", "hold", "refuse"];
39
+ /** 设置 UI 的「不设」拼写(见头注)——两键共用同一个字面量。 */
40
+ export const CROSS_SESSION_INBOUND_UNSET = "default";
41
+ /** 文件层受理面:三个显式值 + `"default"`(未设)。 */
42
+ export const CrossSessionInboundSetting = z.enum([...CROSS_SESSION_INBOUND_VALUES, CROSS_SESSION_INBOUND_UNSET]);
43
+ // ── dialogExpiry ───────────────────────────────────────────────────────────────────────────────────
44
+ /** 四值闭集(CC 逐字)。`"never"` = 不设期限。 */
45
+ export const DIALOG_EXPIRY_VALUES = ["60s", "5m", "10m", "never"];
46
+ /** 未设时生效的值(CC 缺省)。 */
47
+ export const DIALOG_EXPIRY_DEFAULT = "5m";
48
+ /** 文件层受理面:四个值 + `"default"`(未设 ⇒ 落 {@link DIALOG_EXPIRY_DEFAULT})。 */
49
+ export const DialogExpirySetting = z.enum([...DIALOG_EXPIRY_VALUES, CROSS_SESSION_INBOUND_UNSET]);
50
+ // ── settings 片段 ──────────────────────────────────────────────────────────────────────────────────
51
+ /**
52
+ * 整份 `settings.json` 里**本片段负责的两键**。刻意 `passthrough`:本片段不是整份 settings 的闭集门
53
+ * (那道门归各宿主的受理面——宿主消费哪些键、拒哪些键,是宿主的事)。它只保证:**这两个键若在场,
54
+ * 值必合法**。
55
+ */
56
+ export const CrossSessionSettings = z
57
+ .object({
58
+ crossSessionInbound: CrossSessionInboundSetting.optional(),
59
+ dialogExpiry: DialogExpirySetting.optional(),
60
+ })
61
+ .passthrough();
62
+ function readEnumLayer(raw, values) {
63
+ if (raw === undefined || raw === null || raw === CROSS_SESSION_INBOUND_UNSET)
64
+ return { ok: true };
65
+ return typeof raw === "string" && values.includes(raw) ? { ok: true, value: raw } : { ok: false };
66
+ }
67
+ /** 读一层的 `crossSessionInbound` 原始值。 */
68
+ export function readCrossSessionInbound(raw) {
69
+ return readEnumLayer(raw, CROSS_SESSION_INBOUND_VALUES);
70
+ }
71
+ /** 读一层的 `dialogExpiry` 原始值(未设 ⇒ 由消费方落 {@link DIALOG_EXPIRY_DEFAULT},本函数不替它落)。 */
72
+ export function readDialogExpiry(raw) {
73
+ return readEnumLayer(raw, DIALOG_EXPIRY_VALUES);
74
+ }
package/dist/index.d.ts CHANGED
@@ -14,6 +14,7 @@ export * from "./cross-domain.js";
14
14
  export * from "./remote-exec.js";
15
15
  export * from "./safety-merge-spec.js";
16
16
  export * from "./scheduler-store.js";
17
+ export * from "./cross-session.js";
17
18
  export * from "./hooks.js";
18
19
  export * from "./fleet.js";
19
20
  export * from "./migrate.js";
package/dist/index.js CHANGED
@@ -14,6 +14,7 @@ export * from "./cross-domain.js"; // pure half only (no fleet placement)
14
14
  export * from "./remote-exec.js";
15
15
  export * from "./safety-merge-spec.js"; // #14 per-category safety merge-spec (settings resolver authority)
16
16
  export * from "./scheduler-store.js"; // R7 self-wake scheduler store CONTRACT (pure: type + parse/serialize + SchedulerStore interface; fs binding in ./node)
17
+ export * from "./cross-session.js"; // CC-parity `settings.json` cross-session keys (crossSessionInbound / dialogExpiry) — pure/browser-safe
17
18
  export * from "./hooks.js"; // CC-parity hooks contract (31-event set + owner map + per-event input/output shapes + HooksConfig) & stop attribution (stoppedBy) — pure/browser-safe
18
19
  export * from "./fleet.js"; // scheduling/fleet contract (drain protocol · instance discovery · dispatch policy · quota lease · usage report · quota reset event) — CONTROL-PLANE-SCHEDULING-DESIGN, pure/browser-safe
19
20
  export * from "./migrate.js"; // 0.5→0.6 migration tools (hosts grandfathering + worker.model→catalog migration PLAN — report/draft only, E1; EXPERT-REDESIGN §2-2/§3), pure/browser-safe
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/settings-schema",
3
- "version": "1.4.1",
3
+ "version": "1.6.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",