@sema-agent/settings-schema 14.5.0 → 14.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.
Files changed (45) hide show
  1. package/CHANGELOG.md +265 -0
  2. package/README.md +15 -0
  3. package/conformance/VERSION +1 -1
  4. package/dist/atomic-write.js +10 -7
  5. package/dist/bundle.d.ts +1 -1
  6. package/dist/bundle.js +1 -1
  7. package/dist/config-dir-migration.js +2 -1
  8. package/dist/config-fns.js +2 -2
  9. package/dist/conformance/canonical.js +4 -7
  10. package/dist/conformance/corpus.js +3 -3
  11. package/dist/conformance/derive.js +6 -5
  12. package/dist/conformance/run.js +2 -1
  13. package/dist/definition-copy.d.ts +23 -0
  14. package/dist/definition-copy.js +52 -0
  15. package/dist/file-store.d.ts +1 -31
  16. package/dist/file-store.js +3 -30
  17. package/dist/fleet.js +2 -1
  18. package/dist/fs-failure.d.ts +12 -7
  19. package/dist/fs-failure.js +53 -29
  20. package/dist/hooks-config.d.ts +2 -2
  21. package/dist/hooks-config.js +112 -31
  22. package/dist/http-hook-allowlists.d.ts +7 -3
  23. package/dist/http-hook-allowlists.js +102 -16
  24. package/dist/index.d.ts +1 -0
  25. package/dist/index.js +2 -1
  26. package/dist/mcp-request-entry.d.ts +9 -6
  27. package/dist/mcp-request-entry.js +225 -93
  28. package/dist/plain-json-object.d.ts +28 -3
  29. package/dist/plain-json-object.js +48 -7
  30. package/dist/portable-domains.d.ts +31 -0
  31. package/dist/portable-domains.js +30 -0
  32. package/dist/safety-merge-spec.d.ts +88 -6
  33. package/dist/safety-merge-spec.js +233 -17
  34. package/dist/scheduler-store.js +3 -2
  35. package/dist/settings-units.d.ts +85 -0
  36. package/dist/settings-units.js +175 -0
  37. package/dist/types.js +57 -53
  38. package/dist/url-shape.d.ts +11 -5
  39. package/dist/url-shape.js +11 -5
  40. package/dist/workers-rows.d.ts +2 -1
  41. package/dist/workers-rows.js +5 -4
  42. package/docs/API-SURFACE.md +17 -3
  43. package/llms-full.txt +12 -2
  44. package/llms.txt +4 -2
  45. package/package.json +1 -1
@@ -0,0 +1,175 @@
1
+ /**
2
+ * SETTINGS UNITS (14.6.0) — the settings-document unit table: which keys of a settings file carry a merge contract, where each
3
+ * sits in the document, which {@link SAFETY_MERGE_SPEC} category it maps into, and how its raw value READS. Every resolver of a
4
+ * settings file (the shell reads it for each of its sources; a profile loader is a second projection) reads THIS table instead of
5
+ * keeping a key→category map of its own for these ten units. Unit names are the shell's established ten; the two permission-mode
6
+ * kill-switches map to their own `killSwitches` member rows (the table has no managed-only floor for them).
7
+ *
8
+ * NOT in this table: the two http-hook allowlist keys (`allowedHttpHookUrls` / `httpHookAllowedEnvVars`). They carry a merge contract
9
+ * too (the `httpHookAllowlists` category of {@link SAFETY_MERGE_SPEC}) but are keyed and read by `./http-hook-allowlists` on its own
10
+ * (`HTTP_HOOK_ALLOWLIST_KEYS`, `readHttpHookAllowlistLayer`), and read a malformed value differently: there it is `[]` plus a
11
+ * `malformed` mark (the failure direction is "no hook may run"), while a unit reading answers `unreadable`. Folding the two tables
12
+ * into one is a design question (which reading is right), not a drive-by — until it is settled a consumer that wants the allowlist
13
+ * layers reads that module, not this table.
14
+ *
15
+ * How a unit is addressed (`reads`):
16
+ * · `whole` — the unit's value is one value under its category: compared by the category's `tighten`
17
+ * (the only thing a category's `tighten` answers — safety-merge-spec header);
18
+ * · `member` — the unit IS one declared member of its category (the two kill-switches): compared by that member's comparator;
19
+ * · `members` — the unit is an object of declared members (`permissions`, `sandbox`): every question names ONE declared member,
20
+ * compared and read by that member's row ({@link memberTighten}; its value class `boolPole` / `list`). The unit's whole
21
+ * value and its undeclared members have no comparator and no reading here — asking is a RangeError.
22
+ *
23
+ * How a unit READS (`reading`, on every `whole` / `member` row; a `members` unit reads each member by the member row's value class):
24
+ * · `stringList` — an array whose every element is a non-empty string (`[]` reads); a bare string is NOT wrapped into an array;
25
+ * · `permissionMode` — a word of the `permissionMode` category's order other than `bypassPermissions`; `"manual"` reads as
26
+ * `"default"` (the alias); `"bypassPermissions"` is `launch_only` — a launch flag, never a settings-file value;
27
+ * · `killSwitch` — a boolean; `"disable"` (CC's spelling of ON) reads as `true`; `false` is the loose pole — a value, not absence;
28
+ * · `hooks` — what the one hooks judge (`classifyHooksConfig`) classifies as `loaded`;
29
+ * · `envMap` — a plain object whose keys are non-empty and whose values are all strings;
30
+ * · `nonEmptyString` — a non-empty string;
31
+ * and a member's value class: `boolPole` ⇒ a boolean, `list` ⇒ the `stringList` reading.
32
+ * Everything else is `unreadable` — no default arm. Two laws for every unit: `undefined` and JSON `null` are `unreadable` (absence
33
+ * is not a value; "an absent landing spot takes the write" is the loader's business, before it calls the reader), and the reader
34
+ * only recognizes a shape, never rewrites it — an accepted value comes back as the same reference; the only rewrites are the two
35
+ * spellings above (`"manual"` ⇒ `"default"`, `"disable"` ⇒ `true`).
36
+ * WHATEVER READS, COMPARES: a reading has two halves — its shape / spelling half (the table above) and, as its last step, the
37
+ * unit comparator's OWN domain (`tightenVerdict(unitTighten(unit, member), value, value)` must be `equal`). The domain has one
38
+ * owner, the comparator; a reading only adds what it needs on top (non-empty list elements, the hooks judge, …). So a value the
39
+ * shape half accepts but the comparator cannot place — a hooks document the hooks judge loads while a key it strips hides data
40
+ * nested deeper than `MAX_HASH_DEPTH`, or an `undefined` — is `unreadable`, never "read but not comparable".
41
+ * The env DOCUMENT's literal grammar (boolean words, csv + empty-set sentinel, a "≤ 0 means off" cap) is not here: it belongs to
42
+ * the engine's env catalog, whose one implementation is the server's.
43
+ */
44
+ import { classifyHooksConfig } from "./hooks-config.js";
45
+ import { isPlainJsonObject } from "./plain-json-object.js";
46
+ import { SAFETY_MERGE_SPEC, resolveMemberTighten, tightenVerdict } from "./safety-merge-spec.js";
47
+ /** The settings-document units (see the file header). `path` is the unit's place in a settings document. */
48
+ export const SETTINGS_UNITS = {
49
+ permissions: { path: ["permissions"], category: "permissions", reads: "members" },
50
+ permissionMode: { path: ["permissions", "defaultMode"], category: "permissionMode", reads: "whole", reading: "permissionMode" },
51
+ additionalDirectories: { path: ["permissions", "additionalDirectories"], category: "sandboxNetwork", reads: "whole", reading: "stringList" },
52
+ sandbox: { path: ["sandbox"], category: "sandboxNetwork", reads: "members" },
53
+ disableBypassPermissionsMode: { path: ["permissions", "disableBypassPermissionsMode"], category: "killSwitches", reads: "member", member: "disableBypassPermissionsMode", reading: "killSwitch" },
54
+ disableAutoMode: { path: ["permissions", "disableAutoMode"], category: "killSwitches", reads: "member", member: "disableAutoMode", reading: "killSwitch" },
55
+ hooks: { path: ["hooks"], category: "hooks", reads: "whole", reading: "hooks" },
56
+ env: { path: ["env"], category: "env", reads: "whole", reading: "envMap" },
57
+ model: { path: ["model"], category: "model", reads: "whole", reading: "nonEmptyString" },
58
+ outputStyle: { path: ["outputStyle"], category: "ui", reads: "whole", reading: "nonEmptyString" },
59
+ };
60
+ /** The one place a unit + optional member resolves to what is being asked about — the row and its comparator. Fail-closed
61
+ * and loud on every misaddressing: an unknown unit (own-property lookup — `"__proto__"` is unknown, not Object.prototype),
62
+ * a `members` unit without a member, a `whole` unit with one, a `member` unit given another one, an undeclared member
63
+ * (refused by the member row lookup in {@link resolveMemberTighten}). */
64
+ function unitTarget(unit, member) {
65
+ if (!Object.hasOwn(SETTINGS_UNITS, unit)) {
66
+ throw new RangeError(`SETTINGS_UNITS: unknown unit '${unit}' (units: ${Object.keys(SETTINGS_UNITS).join(", ")})`);
67
+ }
68
+ const row = SETTINGS_UNITS[unit];
69
+ const spec = SAFETY_MERGE_SPEC[row.category];
70
+ switch (row.reads) {
71
+ case "whole":
72
+ if (member !== undefined)
73
+ throw new RangeError(`SETTINGS_UNITS: unit '${unit}' is read whole — it has no member '${member}'`);
74
+ return { row, rule: spec.tighten };
75
+ case "member":
76
+ if (member !== undefined)
77
+ throw new RangeError(`SETTINGS_UNITS: unit '${unit}' already names its member ('${row.member}' of '${row.category}') — pass no member`);
78
+ return { row, rule: resolveMemberTighten(spec, row.member, row.category) };
79
+ case "members":
80
+ if (member === undefined) {
81
+ throw new RangeError(`SETTINGS_UNITS: unit '${unit}' is read per declared member of '${row.category}' (members: ${Object.keys(spec.members ?? {}).join(", ")}) — its whole value and its undeclared members have no comparator`);
82
+ }
83
+ return { row, rule: resolveMemberTighten(spec, member, row.category) };
84
+ default: {
85
+ const unknown = row;
86
+ throw new RangeError(`SETTINGS_UNITS: unit '${unit}' has an unknown addressing '${String(unknown.reads)}'`);
87
+ }
88
+ }
89
+ }
90
+ /** The comparator of one unit — or, for a `members` unit, of one DECLARED member of it: `whole` ⇒ the category's `tighten`;
91
+ * `member` ⇒ that member's comparator; `members` ⇒ the named member's comparator ({@link resolveMemberTighten}). Compare two values
92
+ * with `tightenVerdict`. A `members` unit without a member (its whole value), an undeclared member (e.g. `sandbox`'s
93
+ * `failIfUnavailable`), a `whole` unit given a member, a `member` unit given another one, an unknown unit ⇒ RangeError. */
94
+ export function unitTighten(unit, member) {
95
+ return unitTarget(unit, member).rule;
96
+ }
97
+ const UNREADABLE = { ok: false, reason: "unreadable" };
98
+ const LAUNCH_ONLY = { ok: false, reason: "launch_only" };
99
+ /** `stringList`: an array whose every element is a non-empty string (a hole reads as `undefined`, so a sparse array is not one). */
100
+ function isNonEmptyStringList(v) {
101
+ if (!Array.isArray(v))
102
+ return false;
103
+ for (let i = 0; i < v.length; i++) {
104
+ const x = v[i];
105
+ if (typeof x !== "string" || x.length === 0)
106
+ return false;
107
+ }
108
+ return true;
109
+ }
110
+ /** `envMap`: a plain object (prototype `Object.prototype` or `null`), non-empty keys, string values. */
111
+ function isEnvMap(v) {
112
+ if (!isPlainJsonObject(v))
113
+ return false;
114
+ return Object.keys(v).every((k) => k.length > 0 && typeof v[k] === "string");
115
+ }
116
+ /** What a declared member's value class adds on top of its comparator's domain — read through the comparator
117
+ * {@link resolveMemberTighten} derived from the row, so the seat is interpreted in one place: `list` ⇒ setUnion / setNarrower,
118
+ * whose domain takes any strings, so the `stringList` reading adds "every element non-empty"; `boolPole` ⇒ boolStricter, whose
119
+ * domain IS the boolean — nothing to add. A member comparator is only ever one of those three. */
120
+ function memberShapeReads(unit, rule, value) {
121
+ switch (rule.rule) {
122
+ case "boolStricter":
123
+ return true;
124
+ case "setUnion":
125
+ case "setNarrower":
126
+ return isNonEmptyStringList(value);
127
+ default:
128
+ throw new RangeError(`SETTINGS_UNITS: unit '${unit}' resolved to the comparator '${rule.rule}', which no member value class derives`);
129
+ }
130
+ }
131
+ /** A reading's shape / spelling half (the file header's table): the value the raw input reads as, or a refusal. The domain half
132
+ * is the unit comparator's, applied by {@link readSettingsUnit} after this. */
133
+ function shapedReading(unit, row, rule, raw) {
134
+ if (row.reads === "members")
135
+ return memberShapeReads(unit, rule, raw) ? { ok: true, value: raw } : UNREADABLE;
136
+ switch (row.reading) {
137
+ case "stringList":
138
+ return isNonEmptyStringList(raw) ? { ok: true, value: raw } : UNREADABLE;
139
+ case "permissionMode":
140
+ // the words are the category's order (the comparator's enumRank domain); the reading owns the two spellings around it
141
+ if (raw === "bypassPermissions")
142
+ return LAUNCH_ONLY;
143
+ return { ok: true, value: raw === "manual" ? "default" : raw };
144
+ case "killSwitch":
145
+ // CC spells ON "disable"; the boolean itself is the member comparator's domain
146
+ return { ok: true, value: raw === "disable" ? true : raw };
147
+ case "hooks":
148
+ return classifyHooksConfig({ hooks: raw }).outcome === "loaded" ? { ok: true, value: raw } : UNREADABLE;
149
+ case "envMap":
150
+ return isEnvMap(raw) ? { ok: true, value: raw } : UNREADABLE;
151
+ case "nonEmptyString":
152
+ return typeof raw === "string" && raw.length > 0 ? { ok: true, value: raw } : UNREADABLE;
153
+ default: {
154
+ const unknown = row.reading;
155
+ throw new RangeError(`SETTINGS_UNITS: unit '${unit}' has an unknown reading '${String(unknown)}'`);
156
+ }
157
+ }
158
+ }
159
+ /**
160
+ * Read one unit's raw value out of a settings document (14.6.0) — the settings half of "how an existing value is normalized
161
+ * before it is compared". The reading of every unit is the file header's table; anything outside it is `unreadable`, and
162
+ * `undefined` / `null` are `unreadable` for every unit. An accepted value is returned as is (same reference) — the only rewrites
163
+ * are `"manual"` ⇒ `"default"` (permissionMode) and `"disable"` ⇒ `true` (kill-switches); `"bypassPermissions"` is `launch_only`.
164
+ * The last step of every reading is the unit comparator's own domain: whatever reads, compares (file header).
165
+ * Addressing is {@link unitTighten}'s (same RangeErrors: a `members` unit needs a declared member, …).
166
+ */
167
+ export function readSettingsUnit(unit, raw, member) {
168
+ const t = unitTarget(unit, member);
169
+ if (raw === undefined || raw === null)
170
+ return UNREADABLE;
171
+ const shaped = shapedReading(unit, t.row, t.rule, raw);
172
+ if (!shaped.ok)
173
+ return shaped;
174
+ return tightenVerdict(t.rule, shaped.value, shaped.value) === "equal" ? shaped : UNREADABLE;
175
+ }
package/dist/types.js CHANGED
@@ -21,6 +21,7 @@ import { INTEGER } from "./integer.js";
21
21
  import { MAX_EXTRA_BODY_DEPTH } from "./hash.js"; // reconcile the extraBody depth cap with the hash cap (R18)
22
22
  import { DispatchPolicy, FLEET_GENERATION_ENV, FLEET_SCOPE_ENV } from "./fleet.js"; // WorkerSpec.fleet.dispatch (scheduling framework D3)
23
23
  import { isLoopbackAuthHost } from "./loopback.js";
24
+ import { isJsonArray, isPlainJsonObject } from "./plain-json-object.js"; // THE "is this a JSON object" predicate + the total array test
24
25
  import { httpUrlFlaws, httpUrlRule } from "./url-shape.js"; // 11.0.0: THE URL shape judge // 11.0.0: the one narrow「本机」predicate (auth relaxation)
25
26
  import { secretScanForms } from "./secret-scan.js"; // package-private normalization — the one set of forms both scanners read (not in the barrel)
26
27
  import { WEB_ADMIN_ENV } from "./web-admin-env.js"; // the three connection keys the config renderer writes are reserved worker env
@@ -216,62 +217,66 @@ export const NON_SECRET_URL_NO_MIGRATION = createNonSecretUrl(false);
216
217
  * is proven plain JSON (no toJSON/getters), the stored original == what JSON.stringify persists — closing the
217
218
  * non-idempotent-toJSON / accessor-leak / non-plain-container bypasses. Round 2-5 hardening. */
218
219
  export function extraBodyUnsafe(v) {
219
- const stack = [[v, 0]];
220
- const seen = new WeakSet();
221
- let budget = 100_000;
222
- while (stack.length) {
223
- if (budget-- <= 0)
224
- return true; // too large to vet → fail-closed
225
- const [cur, depth] = stack.pop();
226
- if (depth > MAX_EXTRA_BODY_DEPTH)
227
- return true; // too deep to hash later — cap = hash cap MINUS the wrapper reserve, since extraBody is always hashed NESTED (R18: a value passing at the raw hash cap would still overflow once wrapped → write fail-open + read hard-DoS)
228
- if (cur === null)
229
- continue;
230
- const t = typeof cur;
231
- if (t === "string") {
232
- if (containsSecretToken(cur))
233
- return true; // tokenized (mid-string secret too) — sibling of URL/coords/mcp. R8
234
- continue;
235
- }
236
- if (t === "number") {
237
- if (!Number.isFinite(cur))
238
- return true; // Infinity/NaN → JSON null on persist
239
- continue;
240
- }
241
- if (t === "boolean")
242
- continue;
243
- if (t !== "object")
244
- return true; // undefined / function / symbol / bigint
245
- if (seen.has(cur))
246
- return true; // cycle → reject (would also break stableStringify)
247
- seen.add(cur);
248
- if (Array.isArray(cur)) {
249
- if (cur.length !== Object.keys(cur).length)
250
- return true; // sparse array (holes → null on persist)
251
- // descriptor walk for arrays too (NOT `for…of`/index access — that would INVOKE an accessor index's
252
- // getter, which could throw a secret-bearing message). Reject accessor elements. R11.
220
+ try {
221
+ const stack = [[v, 0]];
222
+ const seen = new WeakSet();
223
+ let budget = 100_000;
224
+ while (stack.length) {
225
+ if (budget-- <= 0)
226
+ return true; // too large to vet → fail-closed
227
+ const [cur, depth] = stack.pop();
228
+ if (depth > MAX_EXTRA_BODY_DEPTH)
229
+ return true; // too deep to hash later — cap = hash cap MINUS the wrapper reserve, since extraBody is always hashed NESTED (R18: a value passing at the raw hash cap would still overflow once wrapped → write fail-open + read hard-DoS)
230
+ if (cur === null)
231
+ continue;
232
+ const t = typeof cur;
233
+ if (t === "string") {
234
+ if (containsSecretToken(cur))
235
+ return true; // tokenized (mid-string secret too) — sibling of URL/coords/mcp. R8
236
+ continue;
237
+ }
238
+ if (t === "number") {
239
+ if (!Number.isFinite(cur))
240
+ return true; // Infinity/NaN → JSON null on persist
241
+ continue;
242
+ }
243
+ if (t === "boolean")
244
+ continue;
245
+ if (t !== "object")
246
+ return true; // undefined / function / symbol / bigint
247
+ if (seen.has(cur))
248
+ return true; // cycle → reject (would also break stableStringify)
249
+ seen.add(cur);
250
+ if (isJsonArray(cur)) {
251
+ if (cur.length !== Object.keys(cur).length)
252
+ return true; // sparse array (holes → null on persist)
253
+ // descriptor walk for arrays too (NOT `for…of`/index access — that would INVOKE an accessor index's
254
+ // getter, which could throw a secret-bearing message). Reject accessor elements. R11.
255
+ for (const [k, desc] of Object.entries(Object.getOwnPropertyDescriptors(cur))) {
256
+ if (k === "length")
257
+ continue; // the array's own `length` data prop — not an element
258
+ if (!Object.hasOwn(desc, "value"))
259
+ return true; // accessor element — do NOT invoke
260
+ stack.push([desc.value, depth + 1]);
261
+ }
262
+ continue;
263
+ }
264
+ if (!isPlainJsonObject(cur))
265
+ return true; // Map/Set/Date/class/toJSON-bearing → not plain
266
+ // descriptor walk: never read a value via [] (would invoke a getter); reject accessors outright.
253
267
  for (const [k, desc] of Object.entries(Object.getOwnPropertyDescriptors(cur))) {
254
- if (k === "length")
255
- continue; // the array's own `length` data prop — not an element
256
- if (!("value" in desc))
257
- return true; // accessor element — do NOT invoke
268
+ if (!Object.hasOwn(desc, "value"))
269
+ return true; // accessor (get/set) — do NOT invoke
270
+ if (containsSecretToken(k))
271
+ return true; // secret-shaped KEY (tokenized). R8
258
272
  stack.push([desc.value, depth + 1]);
259
273
  }
260
- continue;
261
- }
262
- const proto = Object.getPrototypeOf(cur);
263
- if (proto !== Object.prototype && proto !== null)
264
- return true; // Map/Set/Date/class/toJSON-bearing → not plain
265
- // descriptor walk: never read a value via [] (would invoke a getter); reject accessors outright.
266
- for (const [k, desc] of Object.entries(Object.getOwnPropertyDescriptors(cur))) {
267
- if (!("value" in desc))
268
- return true; // accessor (get/set) — do NOT invoke
269
- if (containsSecretToken(k))
270
- return true; // secret-shaped KEY (tokenized). R8
271
- stack.push([desc.value, depth + 1]);
272
274
  }
275
+ return false;
276
+ }
277
+ catch {
278
+ return true; // a value the walk cannot read (a proxy that has been revoked, a trap that throws) cannot be vetted ⇒ UNSAFE (fail-closed), never a throw
273
279
  }
274
- return false;
275
280
  }
276
281
  /** THE vetting pipeline for every FREE-FORM JSON value that lands in the persisted config — `ModelEntry.extraBody`,
277
282
  * `ModelEntry.compat` and `ModelEntry.headers` share this ONE function (1.10.0; before that it was inline in `extraBody`
@@ -518,8 +523,7 @@ export const ModelEntry = z.object({
518
523
  .transform((v, ctx) => {
519
524
  if (v === undefined)
520
525
  return undefined;
521
- const proto = typeof v === "object" && v !== null ? Object.getPrototypeOf(v) : undefined;
522
- if (typeof v !== "object" || v === null || Array.isArray(v) || (proto !== Object.prototype && proto !== null)) {
526
+ if (!isPlainJsonObject(v)) {
523
527
  ctx.addIssue({ code: "custom", ...buildQuotedIssue `extraBody must be a plain JSON object` });
524
528
  return z.NEVER;
525
529
  }
@@ -13,10 +13,16 @@
13
13
  *
14
14
  * 拒句里值的样子(展示铸点)不在本模块:那是各宿主自己的展示律(server `buildRefusalShownUrl`)。本模块零 import,纯函数,浏览器安全。
15
15
  *
16
- * ## 本模块里的第二个答案(如实,14.0.0)
16
+ * ## 本模块里的第二个答案(如实,14.0.0 起;14.6.0 把分歧写成读数)
17
17
  * 请求面 MCP 传输的 url(任务提交体 `mcpServers[i].transport.url`,`./mcp-request-entry`)今天不走上面的形判官,而是只看前缀
18
- * ({@link startsWithHttpScheme}):那是消费方请求腿既有的读法,入包时语义逐字冻结。两者对同一个值可以答得不同(空 host、带 userinfo 的
19
- * url 前缀判收、形判官拒)—— 一项已知待收的分歧;放在这里,是为了让「本包里判 URL 的正则」只有这一个模块。
18
+ * ({@link startsWithHttpScheme}):那是消费方请求腿既有的读法,入包时语义逐字冻结。两者对同一个串的答案是两张表(读数逐串钉在 `test/request-url-two-answers-14.6.0.test.ts`):
19
+ * · **读器收、判官拒** —— 前缀对、WHATWG 解析不了的串:裸 scheme `https://`、host 里有空格、未闭合的 IPv6 字面量、非法端口……读器收下,判官答 `not-http-url`;
20
+ * · **判官收、读器拒** —— 不是照字面写全 `http(s)://` 的串:`https:host`、前导空白。WHATWG 会把它们修成合法 URL,读器按字面规则拒;
21
+ * · **部件** —— 读器的座位今天不禁 userinfo / query / fragment(全收),判官在禁这些部件的座位上点名它们。
22
+ * 这是一项**已知待收、收口是收紧所以不走 minor** 的分歧(S-2106):读器收下的是此前能解析出传输的提交体(含重放存量提交体时读它的那一路),把读器收严 = 同一份配置解析出不同结果,发版纪律里是 major,
23
+ * 不是 minor;而「读器直接换成判官」还会放宽第二张表,更不行。S-2106 的收口形与 `PLUGIN_ORIGIN` 座位(`./types`)同构 —— 读器收 ⟺ `startsWithHttpScheme(url)`(座位自己的「照字面写全」规则)∧
24
+ * `httpUrlFlaws(url, [座位禁的部件]).length === 0`(形与部件归本判官)—— 只收紧,不放宽;座位禁不禁 userinfo 是同一次 major 里要定的另一格(读器的拒句已写着凭据别放 URL)。
25
+ * 这个前缀守卫放在这里,是为了让「本包里判 URL 的正则」只有这一个模块。
20
26
  */
21
27
  /** 座位可以禁掉的部件(闭集;判序 = 本数组序)。 */
22
28
  export declare const HTTP_URL_PARTS: readonly ["userinfo", "query", "fragment"];
@@ -35,8 +41,8 @@ export declare function isHierarchicalUrl(raw: string): boolean;
35
41
  * 层级式非 http(s) ⇒ `not-http-url` 之后照判部件。返回类型按 `forbid` 收窄:座位没禁的部件不会出现在答案里。
36
42
  */
37
43
  export declare function httpUrlFlaws<P extends HttpUrlPart>(raw: string, forbid: readonly P[]): ReadonlyArray<"not-http-url" | P>;
38
- /** 以 `http://` / `https://` 起头(大小写不敏感)—— 一道**语法前缀**守卫,不是形判官:不解析、不看 host / 部件(见头注「第二个答案」)。
39
- * 唯一读者是请求面 MCP 传输读器(`readRequestMcpTransport`)。 */
44
+ /** 以 `http://` / `https://` 起头(大小写不敏感)—— 一道**语法前缀**守卫,不是形判官:不解析、不看 host / 部件,所以 `https://` 这样的裸 scheme 它也收(见头注「第二个答案」)。
45
+ * 唯一读者是请求面 MCP 传输读器(`readRequestMcpTransport`);S-2106 收口时它退成那个读器座位的「照字面写全」规则,与形判官合取。 */
40
46
  export declare function startsWithHttpScheme(raw: string): boolean;
41
47
  /** 一个拒因词的规则句(穷举;新词不补 = 编译红)。句子只说规则,不含值。 */
42
48
  export declare function httpUrlRule(flaw: HttpUrlFlaw): string;
package/dist/url-shape.js CHANGED
@@ -13,10 +13,16 @@
13
13
  *
14
14
  * 拒句里值的样子(展示铸点)不在本模块:那是各宿主自己的展示律(server `buildRefusalShownUrl`)。本模块零 import,纯函数,浏览器安全。
15
15
  *
16
- * ## 本模块里的第二个答案(如实,14.0.0)
16
+ * ## 本模块里的第二个答案(如实,14.0.0 起;14.6.0 把分歧写成读数)
17
17
  * 请求面 MCP 传输的 url(任务提交体 `mcpServers[i].transport.url`,`./mcp-request-entry`)今天不走上面的形判官,而是只看前缀
18
- * ({@link startsWithHttpScheme}):那是消费方请求腿既有的读法,入包时语义逐字冻结。两者对同一个值可以答得不同(空 host、带 userinfo 的
19
- * url 前缀判收、形判官拒)—— 一项已知待收的分歧;放在这里,是为了让「本包里判 URL 的正则」只有这一个模块。
18
+ * ({@link startsWithHttpScheme}):那是消费方请求腿既有的读法,入包时语义逐字冻结。两者对同一个串的答案是两张表(读数逐串钉在 `test/request-url-two-answers-14.6.0.test.ts`):
19
+ * · **读器收、判官拒** —— 前缀对、WHATWG 解析不了的串:裸 scheme `https://`、host 里有空格、未闭合的 IPv6 字面量、非法端口……读器收下,判官答 `not-http-url`;
20
+ * · **判官收、读器拒** —— 不是照字面写全 `http(s)://` 的串:`https:host`、前导空白。WHATWG 会把它们修成合法 URL,读器按字面规则拒;
21
+ * · **部件** —— 读器的座位今天不禁 userinfo / query / fragment(全收),判官在禁这些部件的座位上点名它们。
22
+ * 这是一项**已知待收、收口是收紧所以不走 minor** 的分歧(S-2106):读器收下的是此前能解析出传输的提交体(含重放存量提交体时读它的那一路),把读器收严 = 同一份配置解析出不同结果,发版纪律里是 major,
23
+ * 不是 minor;而「读器直接换成判官」还会放宽第二张表,更不行。S-2106 的收口形与 `PLUGIN_ORIGIN` 座位(`./types`)同构 —— 读器收 ⟺ `startsWithHttpScheme(url)`(座位自己的「照字面写全」规则)∧
24
+ * `httpUrlFlaws(url, [座位禁的部件]).length === 0`(形与部件归本判官)—— 只收紧,不放宽;座位禁不禁 userinfo 是同一次 major 里要定的另一格(读器的拒句已写着凭据别放 URL)。
25
+ * 这个前缀守卫放在这里,是为了让「本包里判 URL 的正则」只有这一个模块。
20
26
  */
21
27
  /** 座位可以禁掉的部件(闭集;判序 = 本数组序)。 */
22
28
  export const HTTP_URL_PARTS = ["userinfo", "query", "fragment"];
@@ -51,8 +57,8 @@ export function httpUrlFlaws(raw, forbid) {
51
57
  const shape = u.protocol === "http:" || u.protocol === "https:" ? [] : ["not-http-url"];
52
58
  return [...shape, ...HTTP_URL_PARTS.filter((p) => forbid.includes(p) && present[p])];
53
59
  }
54
- /** 以 `http://` / `https://` 起头(大小写不敏感)—— 一道**语法前缀**守卫,不是形判官:不解析、不看 host / 部件(见头注「第二个答案」)。
55
- * 唯一读者是请求面 MCP 传输读器(`readRequestMcpTransport`)。 */
60
+ /** 以 `http://` / `https://` 起头(大小写不敏感)—— 一道**语法前缀**守卫,不是形判官:不解析、不看 host / 部件,所以 `https://` 这样的裸 scheme 它也收(见头注「第二个答案」)。
61
+ * 唯一读者是请求面 MCP 传输读器(`readRequestMcpTransport`);S-2106 收口时它退成那个读器座位的「照字面写全」规则,与形判官合取。 */
56
62
  export function startsWithHttpScheme(raw) {
57
63
  return /^https?:\/\//i.test(raw);
58
64
  }
@@ -7,7 +7,8 @@
7
7
  * web-admin 的 workers 读口先过本函数:每一只**认得出名字**的 worker 单独过读侧自己的判据(只含这一行的 workers 域读不读得进,
8
8
  * `WorkersConfig` —— 与 `parseDomain("workers")` 同一只 schema,不另抄任何规则;判据与拒句拼装在包内私有的 `./worker-row-verdict`,
9
9
  * 一次性迁移 SS-M4 判改写后的行调同一只);读不进的那一行扣下(`held`,带原文与拒因),
10
- * 其余照常进 `doc`。整域读侧仍是最后围栏,只剩两类归它:认不出名字的条目(不是对象 / 没有字符串名字 —— 原样留在 `doc`)与跨行规则
10
+ * 其余照常进 `doc`。整域读侧仍是最后围栏,只剩两类归它:认不出名字的条目(不是朴素 JSON 对象〔类实例 / `Map` / `Date` 一类,JSON 解析不出来,走到这里只能是程序拼的〕/ 没有字符串名字 —— 原样留在 `doc`;
11
+ * 非朴素的**坏**行因此不在这里单行扣下,而是交整域 `parseDomain("workers")` 判,整张舰队清单读不出 —— 响亮、fail-closed,只落在非 JSON 值上)与跨行规则
11
12
  * (名字 / 路由前缀唯一 —— 两行单独都读得进、合起来才不行,这里不替运维挑留哪一行)。
12
13
  *
13
14
  * 被扣下的行**不进** `doc`,但随 `held[].original` 逐字带回:`doc` ∪ `held[].original` 覆盖输入的全部 worker。读口不写回 —— 原文留在盘上,
@@ -7,27 +7,28 @@
7
7
  * web-admin 的 workers 读口先过本函数:每一只**认得出名字**的 worker 单独过读侧自己的判据(只含这一行的 workers 域读不读得进,
8
8
  * `WorkersConfig` —— 与 `parseDomain("workers")` 同一只 schema,不另抄任何规则;判据与拒句拼装在包内私有的 `./worker-row-verdict`,
9
9
  * 一次性迁移 SS-M4 判改写后的行调同一只);读不进的那一行扣下(`held`,带原文与拒因),
10
- * 其余照常进 `doc`。整域读侧仍是最后围栏,只剩两类归它:认不出名字的条目(不是对象 / 没有字符串名字 —— 原样留在 `doc`)与跨行规则
10
+ * 其余照常进 `doc`。整域读侧仍是最后围栏,只剩两类归它:认不出名字的条目(不是朴素 JSON 对象〔类实例 / `Map` / `Date` 一类,JSON 解析不出来,走到这里只能是程序拼的〕/ 没有字符串名字 —— 原样留在 `doc`;
11
+ * 非朴素的**坏**行因此不在这里单行扣下,而是交整域 `parseDomain("workers")` 判,整张舰队清单读不出 —— 响亮、fail-closed,只落在非 JSON 值上)与跨行规则
11
12
  * (名字 / 路由前缀唯一 —— 两行单独都读得进、合起来才不行,这里不替运维挑留哪一行)。
12
13
  *
13
14
  * 被扣下的行**不进** `doc`,但随 `held[].original` 逐字带回:`doc` ∪ `held[].original` 覆盖输入的全部 worker。读口不写回 —— 原文留在盘上,
14
15
  * web-admin 在列表里标红点名 `field` / `reason`。拒因只点名字段、不回显值。
15
16
  */
16
17
  import { outgoingPath, redactSecrets } from "./config-fns.js";
18
+ import { isJsonArray, isPlainJsonObject } from "./plain-json-object.js";
17
19
  import { workerRowVerdict } from "./worker-row-verdict.js";
18
- const isRec = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
19
20
  /**
20
21
  * 逐行读一份 workers 域**原始**文档(`{ workers: [...] }`)。不是这个形 ⇒ 原样返回、零扣下(判形是 `parseDomain` 的事)。
21
22
  * 规则见文件头。
22
23
  */
23
24
  export function readWorkersRows(raw) {
24
- if (!isRec(raw) || !Array.isArray(raw.workers))
25
+ if (!isPlainJsonObject(raw) || !isJsonArray(raw.workers))
25
26
  return { doc: raw, held: [] };
26
27
  const rows = raw.workers;
27
28
  const kept = [];
28
29
  const held = [];
29
30
  rows.forEach((row, index) => {
30
- if (!isRec(row) || typeof row.name !== "string") {
31
+ if (!isPlainJsonObject(row) || typeof row.name !== "string") {
31
32
  kept.push(row); // 认不出名字:整域读侧判
32
33
  return;
33
34
  }
@@ -4,9 +4,9 @@
4
4
  > 每次全量重算并逐名比对 —— 加 / 删 / 改形一个导出而不重生成本档 ⇒ 红。导出面的每次变动因此以本档的 diff 出现在评审里。
5
5
  > 形:`value` = 运行期拿得到的值;`type` = 只在类型层;`value+type` = 同名的值与类型(zod schema 与它推出的类型)。
6
6
 
7
- 子路径 24 条(资产出口 `./package.json` / `./fleet-paths.json` 不是模块,不列),导出名合计 1351 个(同名在多条子路径上各计一次)。
7
+ 子路径 24 条(资产出口 `./package.json` / `./fleet-paths.json` 不是模块,不列),导出名合计 1365 个(同名在多条子路径上各计一次)。
8
8
 
9
- ## `.`(671)
9
+ ## `.`(680)
10
10
 
11
11
  - `ABSOLUTE_HOST_PATH_MAX` — value
12
12
  - `ACCESS_TOKEN_TTL_SECONDS` — value
@@ -395,6 +395,7 @@
395
395
  - `SESSION_MIRROR_ENGINE_URL` — value
396
396
  - `SETTINGS_SOURCES` — value
397
397
  - `SETTINGS_SOURCE_CONTRACT` — value
398
+ - `SETTINGS_UNITS` — value
398
399
  - `SHA256_DIGEST_RE` — value
399
400
  - `STALE_HEARTBEAT_MULTIPLIER` — value
400
401
  - `STALE_SESSION_RECORD_MS` — value
@@ -442,6 +443,7 @@
442
443
  - `SettingSource` — value+type
443
444
  - `SettingsSource` — type
444
445
  - `SettingsSourceContract` — type
446
+ - `SettingsUnitName` — type
445
447
  - `SetupHookInput` — value
446
448
  - `Sha256Digest` — value
447
449
  - `SiblingResolver` — type
@@ -465,6 +467,7 @@
465
467
  - `TCP_PORT` — value
466
468
  - `TENANT_SEPARATION_BASES` — value
467
469
  - `TENANT_SEPARATION_WORDS` — value
470
+ - `TIGHTEN_VERDICTS` — value
468
471
  - `TOKEN_SCOPE_REFUSALS` — value
469
472
  - `TOOL_WIRE_NAME_MAX_CHARS` — value
470
473
  - `TaskCompletedHookInput` — value
@@ -475,6 +478,7 @@
475
478
  - `TierBindings` — value+type
476
479
  - `TierGroup` — value+type
477
480
  - `TightenRule` — value+type
481
+ - `TightenVerdict` — type
478
482
  - `TokenGrantResponse` — value+type
479
483
  - `TokenQuota` — value+type
480
484
  - `TokenRefreshRequest` — value+type
@@ -601,6 +605,7 @@
601
605
  - `mcpServerPolicyVerdict` — value
602
606
  - `memberAdmission` — value
603
607
  - `memberMinTrust` — value
608
+ - `memberTighten` — value
604
609
  - `membersOf` — value
605
610
  - `mintScopedToken` — value
606
611
  - `narrowAtMention` — value
@@ -631,6 +636,7 @@
631
636
  - `readScopedTokenBinding` — value
632
637
  - `readServingLeaseRequest` — value
633
638
  - `readServingLeaseResponse` — value
639
+ - `readSettingsUnit` — value
634
640
  - `readTokenScope` — value
635
641
  - `readUiLanguage` — value
636
642
  - `readWorkersRows` — value
@@ -645,6 +651,7 @@
645
651
  - `resolveCatalogDefaultModel` — value
646
652
  - `resolveEffectiveForWorker` — value
647
653
  - `resolveMemberAdmission` — value
654
+ - `resolveMemberTighten` — value
648
655
  - `resolveTokenScope` — value
649
656
  - `retiredDomainSentence` — value
650
657
  - `retiredEnvRefusal` — value
@@ -667,9 +674,11 @@
667
674
  - `statusRowsToPrune` — value
668
675
  - `stopAttributionText` — value
669
676
  - `stripControlAndBidi` — value
677
+ - `tightenVerdict` — value
670
678
  - `toSkillsManifest` — value
671
679
  - `toVerifiedIdentity` — value
672
680
  - `trustRank` — value
681
+ - `unitTighten` — value
673
682
  - `urlAllowPatternMatches` — value
674
683
  - `urlCredentialRule` — value
675
684
  - `urlDenyPatternMatches` — value
@@ -1047,7 +1056,7 @@
1047
1056
  - `REMOTE_EXEC_DOCUMENT` — value
1048
1057
  - `RemoteExecSpec` — value+type
1049
1058
 
1050
- ## `./safety-merge-spec`(19)
1059
+ ## `./safety-merge-spec`(24)
1051
1060
 
1052
1061
  - `FailMode` — value+type
1053
1062
  - `MIN_TRUST_WORDS` — value
@@ -1061,12 +1070,17 @@
1061
1070
  - `SafetyMergeCategory` — type
1062
1071
  - `SafetyMergeMember` — type
1063
1072
  - `SpecEnforcement` — value+type
1073
+ - `TIGHTEN_VERDICTS` — value
1064
1074
  - `TightenRule` — value+type
1075
+ - `TightenVerdict` — type
1065
1076
  - `isAtLeast` — value
1066
1077
  - `memberAdmission` — value
1067
1078
  - `memberMinTrust` — value
1079
+ - `memberTighten` — value
1068
1080
  - `membersOf` — value
1069
1081
  - `resolveMemberAdmission` — value
1082
+ - `resolveMemberTighten` — value
1083
+ - `tightenVerdict` — value
1070
1084
  - `trustRank` — value
1071
1085
 
1072
1086
  ## `./node`(39)
package/llms-full.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  <!--
2
2
  llms-full.txt — 自包含全文 · 消费方(使用者)视角 · @sema-agent/settings-schema
3
- 重写日期: 2026-10-23 | 包版本: 14.5.0
3
+ 重写日期: 2026-10-24 | 包版本: 14.6.0
4
4
  生成方式: 无生成脚本——按当前树 src/*.ts 导出面 + package.json + README/CHANGELOG 手写事实版。
5
5
  证据约定: 每条"它怎么工作"的断言附 (文件: 符号) 坐标,刻意不用行号(行号逐版腐烂;符号名由
6
6
  typecheck 与 test/package-surface.test.ts、test/docs-hygiene.test.ts 等常驻门钉住)。无证据推断显式标【推断】。
@@ -345,6 +345,15 @@ entitlement/execution/limits = `none`;`projects` = `salvage`,整域档位仍 `"t
345
345
  `boolPole{strict,minTrust:"local"}`,严格极写=local、松极/非布尔/无值=global;放宽面/failIfUnavailable/
346
346
  未声明新子键回落类目 floor global = fail-closed);读法 `membersOf`/`memberAdmission`(可带候写值)/
347
347
  `memberMinTrust`(禁自铸表)。
348
+ - 比较(14.6.0):唯一比较器 `tightenVerdict(rule, existing, candidate)` → `TIGHTEN_VERDICTS`(equal / stricter / looser /
349
+ unordered / unreadable);只认序不认拼写:enumRank 序内词、boolStricter 严格布尔(不真值化)、setUnion / setNarrower 字符串
350
+ 数组按集合比、numericLower 有限数或 +Infinity(无上限最松)、none 严格 JSON(深度 ≤ MAX_HASH_DEPTH);域外 / 缺席 ⇒
351
+ unreadable;policyDenyWins ⇒ RangeError。`TightenRule` 尾插无参 `setUnion` / `numericLower`。类目 `tighten` 只答整值单元;
352
+ 声明成员的比较器 `memberTighten` / `resolveMemberTighten` 由行的值类座推出(`list` / `boolPole` 恰一;皆无 / 皆有 ⇒
353
+ RangeError);memberized 类目整值与未声明成员无比较器。
354
+ - settings 单元表(14.6.0,`src/settings-units.ts`,主 barrel):`SETTINGS_UNITS` 十单元(path / category / reads / reading)、
355
+ `unitTighten(unit, member?)`、`readSettingsUnit(unit, raw, member?)`(manual ⇒ default、"disable" ⇒ true、false 仍 false、
356
+ bypassPermissions ⇒ launch_only;表外与 undefined / null ⇒ unreadable;收下原样返回)。
348
357
  - 信任阶梯(1.2.0):`trustRank`(managed 6 > global 5 > project 4 > local 3 > session 2 > task 1)、
349
358
  `isAtLeast`;`MinTrust` 含 `task`(声明序非阶梯,阶梯唯一真源是 `trustRank`)。
350
359
  词表是一只纯常量 `MIN_TRUST_WORDS`(`local, session, project, global, managed, task`;`MinTrust` zod 枚举由它派生):`trustRank` 的闭包自由名 = `MIN_TRUST_WORDS` + JS 内建
@@ -650,7 +659,8 @@ R5. fleet 配置种到本地/反向:`exportBundle`/`writeBundle`/`readBundle`(re
650
659
  R6. 复用 secret 边界:直接用 `ENV_NAME`/`NON_SECRET_URL`/`containsSecretToken`,别自写正则。
651
660
  R7. ETag/漂移检测:`stableHash`;skills 内容寻址:`skillContentHash`/`sha256Hex`(prompts 的身份是引擎铸的 `artifactDigest`,本包不铸)。
652
661
  R8. 消费端读 wire:`readEffectiveWire(raw, onWarning)`(容错极性是裁定过的语义,别自己 parse 全量域)。
653
- R9. settings resolver:类别合并律读 `SAFETY_MERGE_SPEC` + `trustRank`/`isAtLeast`/`memberAdmission`,层准入读 `layerAdmission` +
662
+ R9. settings resolver:类别合并律读 `SAFETY_MERGE_SPEC` + `trustRank`/`isAtLeast`/`memberAdmission`,「谁更严」读 `tightenVerdict` +
663
+ `unitTighten` / `memberTighten`,单元与原值读法读 `SETTINGS_UNITS` / `readSettingsUnit`(14.6.0),层准入读 `layerAdmission` +
654
664
  `SETTINGS_SOURCE_CONTRACT`(查口 `settingsSourceContract`),禁自铸 rank/成员/层档表;hooks 坏形读 `classifyHooksConfig`,禁自铸判官。
655
665
 
656
666
  ================================================================================
package/llms.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  <!--
2
2
  llms.txt — 消费方(使用者)视角索引 · @sema-agent/settings-schema
3
- 重写日期: 2026-10-23 | 包版本: 14.5.0
3
+ 重写日期: 2026-10-24 | 包版本: 14.6.0
4
4
  生成方式: 无生成脚本——按当前树的 src/*.ts 导出面 + package.json + README/CHANGELOG 手写事实版;
5
5
  每条断言附证据坐标(文件:符号),刻意不用行号(行号随每版腐烂,符号名由 typecheck/test 钉住)。
6
6
  刷新触发: package.json 版号变更(test/docs-hygiene.test.ts D 组钉「包版本:」与 package.json 相等、
@@ -97,7 +97,9 @@ llms.txt — 消费方(使用者)视角索引 · @sema-agent/settings-schema
97
97
  (src/cross-session.ts: `CrossSessionSettings` / `readCrossSessionInbound` / `readDialogExpiry`)
98
98
  - **安全合并表** `SAFETY_MERGE_SPEC` + 信任阶梯 `trustRank` / `isAtLeast`(词表来源 = 纯常量 `MIN_TRUST_WORDS`,闭包零 zod)+ 成员级准入 `memberAdmission`;关停位 `disableBypassPermissionsMode` / `disableAutoMode`
99
99
  = 类目 `killSwitches`(尾插):任一层可收紧、不放宽 —— 开 = tighten@local(任一设置文件层),关 / 非布尔 / 不带值 = 行地板 global,合并 boolStricter;没有「只许托管层」的地板。
100
- (src/safety-merge-spec.ts)
100
+ 14.6.0:唯一比较器 `tightenVerdict(rule, existing, candidate)`(判词闭集 `TIGHTEN_VERDICTS`;只认序不认拼写,域外 / 缺席 ⇒ `unreadable`;`TightenRule` 尾插无参 `setUnion` / `numericLower`);
101
+ 成员比较器 `memberTighten` 由成员行值类座推出(`MemberAdmission.list` / `boolPole`,每行恰一);settings 单元表 `SETTINGS_UNITS` + `unitTighten` + `readSettingsUnit`(单元读法,表外一切 `unreadable`)。
102
+ (src/safety-merge-spec.ts · src/settings-units.ts)
101
103
  - **设置源表 + 层准入**:`SETTINGS_SOURCES`(合并序)/ `SETTINGS_SOURCE_CONTRACT`(信任档 + 是否仓库可控;本地层与项目层同 `project` 档)/
102
104
  `WORKSPACE_TRUST_STATES` / `layerAdmission(source, workspace, category, member?, value?)`(地板先判;仓库可控层未受信只收 tighten)/
103
105
  `settingsSourceContract(source)`(源表唯一查口:本包要层档的函数都经它,不收调用方给的档;层只带源名)。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/settings-schema",
3
- "version": "14.5.0",
3
+ "version": "14.6.0",
4
4
  "description": "Sema settings schema — pure config contract (zod domains + effective-config + roster resolution + ref-integrity + remoteExec) shared by sema-web-admin, sema-server, and the TOC desktop/CLI. One source of truth. (Renamed at 1.0.0 from @sema-agent/registry-core — name and docs only, no behavior change; note the range moves 0.19.0 -> 1.0.0, so a dependency must become ^1.0.0. The old names @sema-agent/registry-core <=0.19.0 and @sema-ai/registry-core remain published, frozen, for migration.)",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",