@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.
- package/CHANGELOG.md +265 -0
- package/README.md +15 -0
- package/conformance/VERSION +1 -1
- package/dist/atomic-write.js +10 -7
- package/dist/bundle.d.ts +1 -1
- package/dist/bundle.js +1 -1
- package/dist/config-dir-migration.js +2 -1
- package/dist/config-fns.js +2 -2
- package/dist/conformance/canonical.js +4 -7
- package/dist/conformance/corpus.js +3 -3
- package/dist/conformance/derive.js +6 -5
- package/dist/conformance/run.js +2 -1
- package/dist/definition-copy.d.ts +23 -0
- package/dist/definition-copy.js +52 -0
- package/dist/file-store.d.ts +1 -31
- package/dist/file-store.js +3 -30
- package/dist/fleet.js +2 -1
- package/dist/fs-failure.d.ts +12 -7
- package/dist/fs-failure.js +53 -29
- package/dist/hooks-config.d.ts +2 -2
- package/dist/hooks-config.js +112 -31
- package/dist/http-hook-allowlists.d.ts +7 -3
- package/dist/http-hook-allowlists.js +102 -16
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -1
- package/dist/mcp-request-entry.d.ts +9 -6
- package/dist/mcp-request-entry.js +225 -93
- package/dist/plain-json-object.d.ts +28 -3
- package/dist/plain-json-object.js +48 -7
- package/dist/portable-domains.d.ts +31 -0
- package/dist/portable-domains.js +30 -0
- package/dist/safety-merge-spec.d.ts +88 -6
- package/dist/safety-merge-spec.js +233 -17
- package/dist/scheduler-store.js +3 -2
- package/dist/settings-units.d.ts +85 -0
- package/dist/settings-units.js +175 -0
- package/dist/types.js +57 -53
- package/dist/url-shape.d.ts +11 -5
- package/dist/url-shape.js +11 -5
- package/dist/workers-rows.d.ts +2 -1
- package/dist/workers-rows.js +5 -4
- package/docs/API-SURFACE.md +17 -3
- package/llms-full.txt +12 -2
- package/llms.txt +4 -2
- 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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
if (
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
if (
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
if (
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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 (
|
|
255
|
-
|
|
256
|
-
if (
|
|
257
|
-
return true; //
|
|
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
|
-
|
|
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
|
}
|
package/dist/url-shape.d.ts
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})
|
|
19
|
-
*
|
|
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})
|
|
19
|
-
*
|
|
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
|
}
|
package/dist/workers-rows.d.ts
CHANGED
|
@@ -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`。整域读侧仍是最后围栏,只剩两类归它:认不出名字的条目(
|
|
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。读口不写回 —— 原文留在盘上,
|
package/dist/workers-rows.js
CHANGED
|
@@ -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`。整域读侧仍是最后围栏,只剩两类归它:认不出名字的条目(
|
|
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 (!
|
|
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 (!
|
|
31
|
+
if (!isPlainJsonObject(row) || typeof row.name !== "string") {
|
|
31
32
|
kept.push(row); // 认不出名字:整域读侧判
|
|
32
33
|
return;
|
|
33
34
|
}
|
package/docs/API-SURFACE.md
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
> 每次全量重算并逐名比对 —— 加 / 删 / 改形一个导出而不重生成本档 ⇒ 红。导出面的每次变动因此以本档的 diff 出现在评审里。
|
|
5
5
|
> 形:`value` = 运行期拿得到的值;`type` = 只在类型层;`value+type` = 同名的值与类型(zod schema 与它推出的类型)。
|
|
6
6
|
|
|
7
|
-
子路径 24 条(资产出口 `./package.json` / `./fleet-paths.json` 不是模块,不列),导出名合计
|
|
7
|
+
子路径 24 条(资产出口 `./package.json` / `./fleet-paths.json` 不是模块,不列),导出名合计 1365 个(同名在多条子路径上各计一次)。
|
|
8
8
|
|
|
9
|
-
## `.`(
|
|
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`(
|
|
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-
|
|
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
|
|
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-
|
|
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
|
-
(
|
|
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.
|
|
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",
|