@sema-agent/settings-schema 14.4.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 +303 -0
- package/README.md +22 -1
- package/conformance/VERSION +1 -1
- package/dist/api/scopes.d.ts +1 -1
- package/dist/api/scopes.js +1 -1
- package/dist/atomic-write.d.ts +11 -0
- package/dist/atomic-write.js +38 -5
- package/dist/bundle.d.ts +1 -1
- package/dist/bundle.js +1 -1
- package/dist/config-d-file.d.ts +4 -0
- package/dist/config-d-file.js +11 -4
- package/dist/config-dir-io.js +12 -2
- package/dist/config-dir-migration.d.ts +44 -0
- package/dist/config-dir-migration.js +91 -36
- package/dist/config-fns.js +3 -9
- 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 +17 -2
- package/dist/fs-failure.js +55 -15
- 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 +2 -0
- package/dist/index.js +3 -1
- package/dist/mcp-request-entry.d.ts +9 -6
- package/dist/mcp-request-entry.js +225 -93
- package/dist/node.d.ts +2 -1
- package/dist/node.js +2 -1
- package/dist/one-time-migrations.js +36 -41
- package/dist/plain-json-object.d.ts +31 -0
- package/dist/plain-json-object.js +52 -0
- 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.d.ts +4 -0
- package/dist/types.js +61 -53
- package/dist/url-credential.d.ts +41 -0
- package/dist/url-credential.js +95 -0
- 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 +30 -4
- package/docs/CONSUMER-INTEGRATION.md +2 -0
- package/llms-full.txt +24 -2
- package/llms.txt +6 -2
- package/package.json +2 -3
|
@@ -33,6 +33,7 @@ import { outgoingPath, redactSecrets } from "./config-fns.js";
|
|
|
33
33
|
import { LANE_ENV_OWNER, STORE_ENV_KEYS, isLaneEnvKey } from "./execution-lane-index.js";
|
|
34
34
|
import { EXECUTION_LANE_WORDS, MEMORY_ENGINE_BACKENDS, STORE_BACKENDS } from "./execution-lanes.js";
|
|
35
35
|
import { INTEGER } from "./integer.js";
|
|
36
|
+
import { isPlainJsonObject } from "./plain-json-object.js";
|
|
36
37
|
import { RUNTIME_LIFT_TARGETS } from "./runtime-lift-targets.js";
|
|
37
38
|
import { RETIRED_ROLE_WORDS } from "./retired-role-words.js";
|
|
38
39
|
import { REMOTE_EXEC_DOCUMENT, RemoteExecSpec } from "./remote-exec.js";
|
|
@@ -197,7 +198,7 @@ export const ONE_TIME_MIGRATIONS = [
|
|
|
197
198
|
* 重跑恒等:第二次跑零改动,只剩第一次就 `refused` 的那几张收据(那些坐标原样留着,读侧照旧拒)。
|
|
198
199
|
*/
|
|
199
200
|
export function migrateStoredDocuments(docs, opts) {
|
|
200
|
-
if (!
|
|
201
|
+
if (!isPlainJsonObject(docs))
|
|
201
202
|
throw new TypeError("migrateStoredDocuments: `docs` must be a { domain: document } object");
|
|
202
203
|
switch (opts.coverage) {
|
|
203
204
|
case "scope":
|
|
@@ -229,7 +230,7 @@ export function migrateStoredDocuments(docs, opts) {
|
|
|
229
230
|
* 不是对象 ⇒ 原样返回(判形是读者的事)。
|
|
230
231
|
*/
|
|
231
232
|
export function migrateEffectiveSnapshot(effective) {
|
|
232
|
-
if (!
|
|
233
|
+
if (!isPlainJsonObject(effective))
|
|
233
234
|
return { effective, receipts: [] };
|
|
234
235
|
const docs = {};
|
|
235
236
|
for (const [k, v] of Object.entries(effective))
|
|
@@ -251,13 +252,7 @@ export function migrateEffectiveSnapshot(effective) {
|
|
|
251
252
|
next[k] = v;
|
|
252
253
|
return { effective: next, receipts: m.receipts };
|
|
253
254
|
}
|
|
254
|
-
|
|
255
|
-
if (v === null || typeof v !== "object" || Array.isArray(v))
|
|
256
|
-
return false;
|
|
257
|
-
const proto = Object.getPrototypeOf(v);
|
|
258
|
-
return proto === Object.prototype || proto === null;
|
|
259
|
-
}
|
|
260
|
-
const has = (o, k) => isPlainObject(o) && Object.prototype.hasOwnProperty.call(o, k);
|
|
255
|
+
const has = (o, k) => isPlainJsonObject(o) && Object.prototype.hasOwnProperty.call(o, k);
|
|
261
256
|
const without = (o, k) => {
|
|
262
257
|
const { [k]: _gone, ...rest } = o;
|
|
263
258
|
return rest;
|
|
@@ -291,12 +286,12 @@ function stepRuntimeDomain(out, coverage, receipts) {
|
|
|
291
286
|
if (raw === undefined)
|
|
292
287
|
return;
|
|
293
288
|
const refuse = (reason) => void receipts.push(refused("SS-M1", "runtime", [], reason));
|
|
294
|
-
if (
|
|
289
|
+
if (isPlainJsonObject(raw) && Object.keys(raw).length === 0) {
|
|
295
290
|
delete out.runtime;
|
|
296
291
|
receipts.push(receipt("SS-M1", "runtime", [], "dropped-inert"));
|
|
297
292
|
return;
|
|
298
293
|
}
|
|
299
|
-
if (!
|
|
294
|
+
if (!isPlainJsonObject(raw))
|
|
300
295
|
return refuse("the stored runtime document is not a JSON object — the scope does not read today either; fix it by hand");
|
|
301
296
|
if (coverage === "partial" && !RUNTIME_LIFT_TARGETS.every((target) => has(out, target))) {
|
|
302
297
|
return refuse("a runtime document migrates only together with its scope's governance and limits documents (coverage \"scope\") — on its own there is no telling whether governance was ever set");
|
|
@@ -305,7 +300,7 @@ function stepRuntimeDomain(out, coverage, receipts) {
|
|
|
305
300
|
if (!parsed.success)
|
|
306
301
|
return refuse("the stored runtime document does not read under the 10.x runtime schema — the scope does not read today either; fix it by hand");
|
|
307
302
|
const limitsRaw = out.limits;
|
|
308
|
-
if (limitsRaw !== undefined && !
|
|
303
|
+
if (limitsRaw !== undefined && !isPlainJsonObject(limitsRaw))
|
|
309
304
|
return refuse("the scope's limits document is not a JSON object — the scope does not read today either; fix limits first");
|
|
310
305
|
const trio = GOVERNANCE_TRIO.filter((k) => has(raw, k) && raw[k] !== undefined);
|
|
311
306
|
if (trio.length > 0) {
|
|
@@ -323,7 +318,7 @@ function stepRuntimeDomain(out, coverage, receipts) {
|
|
|
323
318
|
const limitKeys = LIMIT_KEYS.filter((k) => parsed.data[k] !== undefined);
|
|
324
319
|
if (limitKeys.length > 0) {
|
|
325
320
|
// 迁移前的读法(消费方):`limits` 有这一键 ⇒ limits 赢;没有 ⇒ 读 runtime 上的值。
|
|
326
|
-
const limits =
|
|
321
|
+
const limits = isPlainJsonObject(limitsRaw) ? { ...limitsRaw } : {};
|
|
327
322
|
let added = false;
|
|
328
323
|
for (const k of limitKeys) {
|
|
329
324
|
if (Object.prototype.hasOwnProperty.call(limits, k))
|
|
@@ -346,13 +341,13 @@ function stepRuntimeDomain(out, coverage, receipts) {
|
|
|
346
341
|
// ── SS-M2:hosts 超界 ───────────────────────────────────────────────────────────────────────────────────
|
|
347
342
|
function stepHostsBounds(out, receipts) {
|
|
348
343
|
const doc = out.hosts;
|
|
349
|
-
if (!
|
|
344
|
+
if (!isPlainJsonObject(doc) || !Array.isArray(doc.hosts))
|
|
350
345
|
return;
|
|
351
346
|
const rows = doc.hosts;
|
|
352
347
|
let kept = [];
|
|
353
348
|
const dropped = [];
|
|
354
349
|
rows.forEach((row, index) => {
|
|
355
|
-
const name =
|
|
350
|
+
const name = isPlainJsonObject(row) ? row.name : undefined;
|
|
356
351
|
if (typeof name === "string" && name.length > HOST_NAME_MAX)
|
|
357
352
|
dropped.push(index);
|
|
358
353
|
else
|
|
@@ -402,8 +397,8 @@ const UNCHANGED = { kind: "unchanged" };
|
|
|
402
397
|
function migrateWorkerRow(raw) {
|
|
403
398
|
const refuse = (field, reason) => ({ kind: "refused", field, reason });
|
|
404
399
|
const enabled = raw.enabled !== false;
|
|
405
|
-
const laneKeysOf = (rec) => (
|
|
406
|
-
const storeKeysOf = (rec) => (
|
|
400
|
+
const laneKeysOf = (rec) => (isPlainJsonObject(rec) ? Object.keys(rec).filter(isLaneEnvKey) : []);
|
|
401
|
+
const storeKeysOf = (rec) => (isPlainJsonObject(rec) ? STORE_ENV_KEYS.filter((k) => Object.hasOwn(rec, k)) : []);
|
|
407
402
|
const envLaneKeys = laneKeysOf(raw.env);
|
|
408
403
|
const secretLaneKeys = laneKeysOf(raw.secretEnv);
|
|
409
404
|
const envStoreKeys = storeKeysOf(raw.env);
|
|
@@ -432,12 +427,12 @@ function migrateWorkerRow(raw) {
|
|
|
432
427
|
const needLane = !hasExecution && (enabled || laneTouched);
|
|
433
428
|
const needStore = !hasStore && (enabled || storeTouched);
|
|
434
429
|
const w = structuredClone(raw);
|
|
435
|
-
const runtime =
|
|
430
|
+
const runtime = isPlainJsonObject(w.runtime) ? w.runtime : {};
|
|
436
431
|
w.runtime = runtime;
|
|
437
|
-
const env =
|
|
438
|
-
const secretEnv =
|
|
439
|
-
const coords =
|
|
440
|
-
const secretRefs =
|
|
432
|
+
const env = isPlainJsonObject(w.env) ? w.env : {};
|
|
433
|
+
const secretEnv = isPlainJsonObject(w.secretEnv) ? w.secretEnv : {};
|
|
434
|
+
const coords = isPlainJsonObject(w.coords) ? w.coords : {};
|
|
435
|
+
const secretRefs = isPlainJsonObject(w.secretRefs) ? w.secretRefs : {};
|
|
441
436
|
const dropped = [];
|
|
442
437
|
const drop = (container, slot, key) => {
|
|
443
438
|
delete container[key];
|
|
@@ -601,9 +596,9 @@ function migrateWorkerRow(raw) {
|
|
|
601
596
|
}
|
|
602
597
|
}
|
|
603
598
|
// 被搬空的 env / secretEnv 整键去掉(它们是可选字段;留一个 `{}` 只是噪声)
|
|
604
|
-
if (
|
|
599
|
+
if (isPlainJsonObject(w.env) && Object.keys(env).length === 0 && isPlainJsonObject(raw.env) && Object.keys(raw.env).length > 0)
|
|
605
600
|
delete w.env;
|
|
606
|
-
if (
|
|
601
|
+
if (isPlainJsonObject(w.secretEnv) && Object.keys(secretEnv).length === 0 && isPlainJsonObject(raw.secretEnv) && Object.keys(raw.secretEnv).length > 0)
|
|
607
602
|
delete w.secretEnv;
|
|
608
603
|
// 改写后的行仍读不进 7.x 形 ⇒ 不写回半成品:拒,原文留盘(读口 `readWorkersRows` 扣下它)。判据与拒句是读口那一只(`workerRowVerdict`)。
|
|
609
604
|
const verdict = workerRowVerdict(w, "migrated");
|
|
@@ -613,12 +608,12 @@ function migrateWorkerRow(raw) {
|
|
|
613
608
|
}
|
|
614
609
|
function stepWorkers6x(out, receipts) {
|
|
615
610
|
const doc = out.workers;
|
|
616
|
-
if (!
|
|
611
|
+
if (!isPlainJsonObject(doc) || !Array.isArray(doc.workers))
|
|
617
612
|
return;
|
|
618
613
|
const rows = doc.workers;
|
|
619
614
|
let next;
|
|
620
615
|
rows.forEach((row, index) => {
|
|
621
|
-
if (!
|
|
616
|
+
if (!isPlainJsonObject(row) || typeof row.name !== "string")
|
|
622
617
|
return; // 认不出名字:整域读侧判
|
|
623
618
|
const r = migrateWorkerRow(row);
|
|
624
619
|
switch (r.kind) {
|
|
@@ -646,7 +641,7 @@ function stepWorkers6x(out, receipts) {
|
|
|
646
641
|
// ── SS-M5:模型钥匙二选一 ──────────────────────────────────────────────────────────────────────────────
|
|
647
642
|
function stepModelKeyOneOf(out, receipts) {
|
|
648
643
|
const doc = out.models;
|
|
649
|
-
if (!
|
|
644
|
+
if (!isPlainJsonObject(doc) || !Array.isArray(doc.models))
|
|
650
645
|
return;
|
|
651
646
|
const rows = doc.models;
|
|
652
647
|
let next;
|
|
@@ -682,17 +677,17 @@ function rewriteAtMention(holder, document, at, receipts) {
|
|
|
682
677
|
return { ...without(holder, "atModelAllowlist"), atMention };
|
|
683
678
|
}
|
|
684
679
|
function stepAtMention(out, receipts) {
|
|
685
|
-
if (
|
|
680
|
+
if (isPlainJsonObject(out.models)) {
|
|
686
681
|
const next = rewriteAtMention(out.models, "models", [], receipts);
|
|
687
682
|
if (next !== undefined)
|
|
688
683
|
out.models = next;
|
|
689
684
|
}
|
|
690
685
|
const rosters = out.rosters;
|
|
691
|
-
if (
|
|
686
|
+
if (isPlainJsonObject(rosters) && Array.isArray(rosters.rosters)) {
|
|
692
687
|
const rows = rosters.rosters;
|
|
693
688
|
let next;
|
|
694
689
|
rows.forEach((row, index) => {
|
|
695
|
-
if (!
|
|
690
|
+
if (!isPlainJsonObject(row))
|
|
696
691
|
return;
|
|
697
692
|
const r = rewriteAtMention(row, "rosters", ["rosters", index], receipts);
|
|
698
693
|
if (r === undefined)
|
|
@@ -707,7 +702,7 @@ function stepAtMention(out, receipts) {
|
|
|
707
702
|
// ── SS-M8:退役的角色词 ────────────────────────────────────────────────────────────────────────────────
|
|
708
703
|
/** 一个 `roles` 图上的退役角色词键删掉(值不论 —— 没有角色读它);逐键一张 `dropped-inert` 收据。返回改后的图或 `undefined`(没改 / 不是对象:判形是读者的事)。 */
|
|
709
704
|
function withoutRetiredRoles(roles, document, at, receipts) {
|
|
710
|
-
if (!
|
|
705
|
+
if (!isPlainJsonObject(roles))
|
|
711
706
|
return undefined;
|
|
712
707
|
const present = RETIRED_ROLE_WORDS.filter((w) => Object.prototype.hasOwnProperty.call(roles, w));
|
|
713
708
|
if (present.length === 0)
|
|
@@ -721,17 +716,17 @@ function withoutRetiredRoles(roles, document, at, receipts) {
|
|
|
721
716
|
}
|
|
722
717
|
function stepRetiredRoleWords(out, receipts) {
|
|
723
718
|
const models = out.models;
|
|
724
|
-
if (
|
|
719
|
+
if (isPlainJsonObject(models)) {
|
|
725
720
|
const roles = withoutRetiredRoles(models.roles, "models", [], receipts);
|
|
726
721
|
if (roles !== undefined)
|
|
727
722
|
out.models = { ...models, roles };
|
|
728
723
|
}
|
|
729
724
|
const rosters = out.rosters;
|
|
730
|
-
if (
|
|
725
|
+
if (isPlainJsonObject(rosters) && Array.isArray(rosters.rosters)) {
|
|
731
726
|
const rows = rosters.rosters;
|
|
732
727
|
let next;
|
|
733
728
|
rows.forEach((row, index) => {
|
|
734
|
-
if (!
|
|
729
|
+
if (!isPlainJsonObject(row))
|
|
735
730
|
return;
|
|
736
731
|
const roles = withoutRetiredRoles(row.roles, "rosters", ["rosters", index], receipts);
|
|
737
732
|
if (roles === undefined)
|
|
@@ -749,16 +744,16 @@ function stepRetiredRoleWords(out, receipts) {
|
|
|
749
744
|
* 消失);异名 ⇒ 原 `roles.default` 作「被压过」删掉、`default` 搬入。`default` 不是非空串 ⇒ 拒(今天它让整个 models 域读不进,不替运维猜)。 */
|
|
750
745
|
function stepModelsDefault(out, receipts) {
|
|
751
746
|
const doc = out.models;
|
|
752
|
-
if (!
|
|
747
|
+
if (!isPlainJsonObject(doc) || !Object.prototype.hasOwnProperty.call(doc, "default"))
|
|
753
748
|
return;
|
|
754
749
|
const name = doc.default;
|
|
755
750
|
if (typeof name !== "string" || name === "") {
|
|
756
751
|
receipts.push(refused("SS-M9", "models", ["default"], "models.default is not a catalog name — it does not read today either; fix it by hand"));
|
|
757
752
|
return;
|
|
758
753
|
}
|
|
759
|
-
const roles =
|
|
754
|
+
const roles = isPlainJsonObject(doc.roles) ? doc.roles : {};
|
|
760
755
|
const prior = roles.default;
|
|
761
|
-
if (prior !== undefined && !(
|
|
756
|
+
if (prior !== undefined && !(isPlainJsonObject(prior) && prior.model === name))
|
|
762
757
|
receipts.push(receipt("SS-M9", "models", ["roles", "default"], "dropped-shadowed"));
|
|
763
758
|
receipts.push(moved("SS-M9", "models", ["default"], { document: "models", path: ["roles", "default"] }));
|
|
764
759
|
out.models = { ...without(doc, "default"), roles: { ...roles, default: { model: name } } };
|
|
@@ -769,7 +764,7 @@ function stepModelsDefault(out, receipts) {
|
|
|
769
764
|
* overlay,catalog 的 digest 是引擎铸的,替运维机械转就是第二个编译器 / 第二个铸点。`prompts` 不是对象 ⇒ 不动(判形是读者的事)。 */
|
|
770
765
|
function stepPromptsAxes(out, receipts) {
|
|
771
766
|
const doc = out.prompts;
|
|
772
|
-
if (!
|
|
767
|
+
if (!isPlainJsonObject(doc))
|
|
773
768
|
return;
|
|
774
769
|
let next = doc;
|
|
775
770
|
if (has(doc, "bindings") && doc.bindings !== undefined) {
|
|
@@ -820,7 +815,7 @@ function stripFragments(schema, value, path, onStrip, depth = 0) {
|
|
|
820
815
|
return value.slice(0, hash);
|
|
821
816
|
}
|
|
822
817
|
if (s instanceof z.ZodDiscriminatedUnion) {
|
|
823
|
-
if (!
|
|
818
|
+
if (!isPlainJsonObject(value))
|
|
824
819
|
return value;
|
|
825
820
|
const disc = s._zod.def.discriminator;
|
|
826
821
|
for (const option of s.options) {
|
|
@@ -842,7 +837,7 @@ function stripFragments(schema, value, path, onStrip, depth = 0) {
|
|
|
842
837
|
return value;
|
|
843
838
|
}
|
|
844
839
|
if (s instanceof z.ZodObject) {
|
|
845
|
-
if (!
|
|
840
|
+
if (!isPlainJsonObject(value))
|
|
846
841
|
return value;
|
|
847
842
|
let next;
|
|
848
843
|
for (const [k, fieldSchema] of Object.entries(s.shape)) {
|
|
@@ -866,7 +861,7 @@ function stripFragments(schema, value, path, onStrip, depth = 0) {
|
|
|
866
861
|
return next ?? value;
|
|
867
862
|
}
|
|
868
863
|
if (s instanceof z.ZodRecord) {
|
|
869
|
-
if (!
|
|
864
|
+
if (!isPlainJsonObject(value))
|
|
870
865
|
return value;
|
|
871
866
|
let next;
|
|
872
867
|
for (const [k, el] of Object.entries(value)) {
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 包内私有(不进主桶、不进 exports 表):**「纯 JSON 对象」的唯一谓词** —— 不是 `null`、不是数组、原型是 `null` 或「原型自己的原型是 `null`」(即 `Object.prototype`:JSON 解析出来的对象恒如此;
|
|
3
|
+
* `Date` / `Map` / 类实例 / 建在别的对象原型上的记录原型链更深,都不是)。**已知的边**:原型是某个 null 原型对象的对象(`Object.create(Object.create(null))`)、`class X extends null` 的实例,
|
|
4
|
+
* 链上第二环也是 `null`,读成朴素 —— 它们只能由程序造出,JSON 解析不出来;读者按它的**自有**成员读,继承来的成员不计(托管层 http hook 名单对只靠继承可见的名单键 fail-closed;请求面 `env` / `headers` 收成 `{}` 不修 —— 读者级遍历继承成员与谓词级再加判据都对 `Object.prototype` 上的可枚举污染敏感,而这一侧丢的是数据、失败可见、不在安全轴)。**不按「原型恒等本 realm 的 `Object.prototype`」判**:另一个 realm(`vm` 新上下文、iframe)里 `JSON.parse` 出来的普通对象,
|
|
5
|
+
* 原型是**那个 realm** 的 `Object.prototype`,按恒等判会被读成非朴素;而任何 realm 的 `Object.prototype` 自身原型都是 `null`,所以链上第二环是 `null` 这一条与 realm 无关,不需要为 realm 加特例。包里凡要问「这个值是不是一个 JSON 对象」的地方都读它,不各写一份:读域文档的键集对比(`./config-fns`)、一次性迁移的文档形判
|
|
6
|
+
* (`./one-time-migrations`)、`config.d` 文档形(`./config-d-file`)、`extraBody` 的形闸与秘密走查(`./types`)、settings 单元读法与严格 JSON 深度判(`./settings-units` / `./safety-merge-spec`)、
|
|
7
|
+
* 请求面传输的记录读法与描述件(`./mcp-request-entry`)、workers 行读口(`./workers-rows`)、有效配置线上载荷读口(`./config-fns` 的 `readEffectiveWire`)、http hook 名单的设置袋(`./http-hook-allowlists`)、
|
|
8
|
+
* fleet 线上载荷的受护判(`./fleet`)、符合性语料的比较器 / 派生 / 语料读口 / 元数据拆分(`./conformance/*`)。零 import。
|
|
9
|
+
* **形状分类核 {@link jsonShapeOf} 是座位的本体**(唯一读原型的地方),`isPlainJsonObject` 由它派生(`plain` 那一格)。**总(不抛)**:检视就抛的对象(已撤销的 Proxy、`getPrototypeOf` 陷阱抛)
|
|
10
|
+
* 核答 `uninspectable`、判官答 `false`,不抛出去 —— 全包经座位的读口因此拒而不抛,律在座位上收一处,不在每个读器各包一个 `try`;读者对 `uninspectable` 一律按最严处置(hooks ⇒ 拒整份文档;名单 ⇒ `malformed`;传输 ⇒ 整只不交)。
|
|
11
|
+
*
|
|
12
|
+
* **唯一的第二处是 `./hooks-config` 的 `hookShapeOf` / `hookIsRecord`,与 {@link jsonShapeOf} / 本函数逐字同文(调用名归一后)**:hooks 分类器的闭包要被消费方生成器整函数抄走(闭包里的自由名只有 `HOOK_EVENTS`),闭包里不能 import 本模块 ——
|
|
13
|
+
* 同文由 `test/plain-json-object-single-seat-14.6.0.test.ts` 逐字证明(漂开即红),闭包契约多注入一个名字时(S-2133)把它并回这里。
|
|
14
|
+
* 同一个门也数全包:返回类型是 `x is Record<string, unknown>` 的函数、读原型的调用点,除这两处之外一处不许有;内联的「typeof 对象 ∧ 非 null ∧ 非数组」合取则**一处都不许有**(座位里它是分开的 `if` 臂,不是合取)。
|
|
15
|
+
*
|
|
16
|
+
* **读者要的答案不只是「是不是朴素对象」时,调本模块的 {@link jsonShapeOf}**(六词形状分类),不在读者里再推一遍形状:例如托管层的 http hook 名单,非朴素的**对象**袋必须 fail-closed(读成空名单并告警),
|
|
17
|
+
* 而 `null` / 原始值 / 数组仍是「整份设置不是对象 ⇒ 两键都缺」—— 两种不同的答案,只能由形状分类给。`isPlainJsonObject` 就是 `jsonShapeOf` 的「朴素」那一格。
|
|
18
|
+
*/
|
|
19
|
+
export declare function isPlainJsonObject(v: unknown): v is Record<string, unknown>;
|
|
20
|
+
/** 一个值的形状(六词,闭集):`plain` = 朴素对象(原型是 `null` 或原型自己的原型是 `null`;{@link isPlainJsonObject} 就是这一格);`non_plain_object` = 是对象但不朴素(类实例 / `Map` / `Date` / 建在别的对象原型上的记录,以及**函数** —— 函数是对象、不是纯数据,
|
|
21
|
+
* JSON 里不会有;读者对「是对象但不朴素」一律 fail-closed,函数不能从缝里漏过去);`array`;`null`;`primitive` = 其余(字符串 / 数 / 布尔 / `undefined` / symbol / bigint);
|
|
22
|
+
* `uninspectable` = **读形状就抛**的对象(已撤销的 Proxy:连 `Array.isArray` 都抛;`getPrototypeOf` 陷阱抛的活 Proxy)—— 不是「不朴素」(那句要读者劝人「拷成纯对象」,对撤销的 Proxy 做不到),读者对它 fail-closed 并说全包唯一的「检视不了」那一句。
|
|
23
|
+
* 读者对这六个词**只许穷举 `switch` 加 `never` 默认臂**(加第七个词时每个读者编译红),不许 `===` 链逐个比(单座门看着)。 */
|
|
24
|
+
export type JsonShape = ReturnType<typeof jsonShapeOf>;
|
|
25
|
+
/** 值的形状分类 —— 本包里「是对象但不朴素」与「根本不是对象」要给不同答案的读者调它(见头注)。读原型的就是这一只(判官 {@link isPlainJsonObject} 由它派生)。 */
|
|
26
|
+
export declare function jsonShapeOf(v: unknown): "plain" | "non_plain_object" | "array" | "null" | "primitive" | "uninspectable";
|
|
27
|
+
/**
|
|
28
|
+
* `Array.isArray` 的**总**版(不抛):已撤销的 Proxy 连「是不是数组」都读不出 ⇒ 不是数组。读者要问「这个值是不是数组」且值可能来自嵌入方(非 JSON)时调它,不直接 `Array.isArray`。
|
|
29
|
+
* 分类器闭包里不能 import,`./hooks-config` 的 `hookIsArray` 与本函数**逐字同文**(同一个门证明,与 `hookIsRecord` 同律)。
|
|
30
|
+
*/
|
|
31
|
+
export declare function isJsonArray(v: unknown): v is unknown[];
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 包内私有(不进主桶、不进 exports 表):**「纯 JSON 对象」的唯一谓词** —— 不是 `null`、不是数组、原型是 `null` 或「原型自己的原型是 `null`」(即 `Object.prototype`:JSON 解析出来的对象恒如此;
|
|
3
|
+
* `Date` / `Map` / 类实例 / 建在别的对象原型上的记录原型链更深,都不是)。**已知的边**:原型是某个 null 原型对象的对象(`Object.create(Object.create(null))`)、`class X extends null` 的实例,
|
|
4
|
+
* 链上第二环也是 `null`,读成朴素 —— 它们只能由程序造出,JSON 解析不出来;读者按它的**自有**成员读,继承来的成员不计(托管层 http hook 名单对只靠继承可见的名单键 fail-closed;请求面 `env` / `headers` 收成 `{}` 不修 —— 读者级遍历继承成员与谓词级再加判据都对 `Object.prototype` 上的可枚举污染敏感,而这一侧丢的是数据、失败可见、不在安全轴)。**不按「原型恒等本 realm 的 `Object.prototype`」判**:另一个 realm(`vm` 新上下文、iframe)里 `JSON.parse` 出来的普通对象,
|
|
5
|
+
* 原型是**那个 realm** 的 `Object.prototype`,按恒等判会被读成非朴素;而任何 realm 的 `Object.prototype` 自身原型都是 `null`,所以链上第二环是 `null` 这一条与 realm 无关,不需要为 realm 加特例。包里凡要问「这个值是不是一个 JSON 对象」的地方都读它,不各写一份:读域文档的键集对比(`./config-fns`)、一次性迁移的文档形判
|
|
6
|
+
* (`./one-time-migrations`)、`config.d` 文档形(`./config-d-file`)、`extraBody` 的形闸与秘密走查(`./types`)、settings 单元读法与严格 JSON 深度判(`./settings-units` / `./safety-merge-spec`)、
|
|
7
|
+
* 请求面传输的记录读法与描述件(`./mcp-request-entry`)、workers 行读口(`./workers-rows`)、有效配置线上载荷读口(`./config-fns` 的 `readEffectiveWire`)、http hook 名单的设置袋(`./http-hook-allowlists`)、
|
|
8
|
+
* fleet 线上载荷的受护判(`./fleet`)、符合性语料的比较器 / 派生 / 语料读口 / 元数据拆分(`./conformance/*`)。零 import。
|
|
9
|
+
* **形状分类核 {@link jsonShapeOf} 是座位的本体**(唯一读原型的地方),`isPlainJsonObject` 由它派生(`plain` 那一格)。**总(不抛)**:检视就抛的对象(已撤销的 Proxy、`getPrototypeOf` 陷阱抛)
|
|
10
|
+
* 核答 `uninspectable`、判官答 `false`,不抛出去 —— 全包经座位的读口因此拒而不抛,律在座位上收一处,不在每个读器各包一个 `try`;读者对 `uninspectable` 一律按最严处置(hooks ⇒ 拒整份文档;名单 ⇒ `malformed`;传输 ⇒ 整只不交)。
|
|
11
|
+
*
|
|
12
|
+
* **唯一的第二处是 `./hooks-config` 的 `hookShapeOf` / `hookIsRecord`,与 {@link jsonShapeOf} / 本函数逐字同文(调用名归一后)**:hooks 分类器的闭包要被消费方生成器整函数抄走(闭包里的自由名只有 `HOOK_EVENTS`),闭包里不能 import 本模块 ——
|
|
13
|
+
* 同文由 `test/plain-json-object-single-seat-14.6.0.test.ts` 逐字证明(漂开即红),闭包契约多注入一个名字时(S-2133)把它并回这里。
|
|
14
|
+
* 同一个门也数全包:返回类型是 `x is Record<string, unknown>` 的函数、读原型的调用点,除这两处之外一处不许有;内联的「typeof 对象 ∧ 非 null ∧ 非数组」合取则**一处都不许有**(座位里它是分开的 `if` 臂,不是合取)。
|
|
15
|
+
*
|
|
16
|
+
* **读者要的答案不只是「是不是朴素对象」时,调本模块的 {@link jsonShapeOf}**(六词形状分类),不在读者里再推一遍形状:例如托管层的 http hook 名单,非朴素的**对象**袋必须 fail-closed(读成空名单并告警),
|
|
17
|
+
* 而 `null` / 原始值 / 数组仍是「整份设置不是对象 ⇒ 两键都缺」—— 两种不同的答案,只能由形状分类给。`isPlainJsonObject` 就是 `jsonShapeOf` 的「朴素」那一格。
|
|
18
|
+
*/
|
|
19
|
+
export function isPlainJsonObject(v) {
|
|
20
|
+
const shape = jsonShapeOf(v);
|
|
21
|
+
return shape === "plain";
|
|
22
|
+
}
|
|
23
|
+
/** 值的形状分类 —— 本包里「是对象但不朴素」与「根本不是对象」要给不同答案的读者调它(见头注)。读原型的就是这一只(判官 {@link isPlainJsonObject} 由它派生)。 */
|
|
24
|
+
export function jsonShapeOf(v) {
|
|
25
|
+
try {
|
|
26
|
+
if (v === null)
|
|
27
|
+
return "null";
|
|
28
|
+
if (Array.isArray(v))
|
|
29
|
+
return "array";
|
|
30
|
+
if (typeof v === "function")
|
|
31
|
+
return "non_plain_object";
|
|
32
|
+
if (typeof v !== "object")
|
|
33
|
+
return "primitive";
|
|
34
|
+
const proto = Object.getPrototypeOf(v);
|
|
35
|
+
return proto === null || Object.getPrototypeOf(proto) === null ? "plain" : "non_plain_object";
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return "uninspectable"; // 读形状就抛(已撤销的 Proxy 连 `Array.isArray` 都抛、`getPrototypeOf` 陷阱抛):不是「不朴素」,是读不出 —— 读者按最严处置
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* `Array.isArray` 的**总**版(不抛):已撤销的 Proxy 连「是不是数组」都读不出 ⇒ 不是数组。读者要问「这个值是不是数组」且值可能来自嵌入方(非 JSON)时调它,不直接 `Array.isArray`。
|
|
43
|
+
* 分类器闭包里不能 import,`./hooks-config` 的 `hookIsArray` 与本函数**逐字同文**(同一个门证明,与 `hookIsRecord` 同律)。
|
|
44
|
+
*/
|
|
45
|
+
export function isJsonArray(v) {
|
|
46
|
+
try {
|
|
47
|
+
return Array.isArray(v);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return false; // 已撤销的 Proxy:读不出 ⇒ 不是数组
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** The domains a single-machine TOC carries: what `FileConfigStore` reads out of `config.d/`, and the
|
|
2
|
+
* **default**域集 of the bundle helpers (`exportBundle`/`readBundle`). 每个被排除的域必须在
|
|
3
|
+
* {@link NON_PORTABLE_DOMAIN_REASONS} 里有一条成文理由(禁裸排除,#282 件5 附带裁定)。
|
|
4
|
+
*
|
|
5
|
+
* ⚠️ 口径诚实(对抗复审 复审 round1 F4):这是**默认集与本地读取集,不是 API 级硬边界** —— `exportBundle`
|
|
6
|
+
* / `readBundle` 的 `domains` 参数可显式传任意 `DomainName`(bundle.ts 不拒非便携域)。把非便携域搬进
|
|
7
|
+
* bundle 的那条口子归调用方纪律(以及各自域的导出面),不是本常量能挡住的;要不要把 bundle API 收窄到
|
|
8
|
+
* `PortableDomain` 是一个独立的行为面裁定(#282 未决项,需三问),本批不擅自改。
|
|
9
|
+
*
|
|
10
|
+
* 🔴 **开新顶层域时必须同步登记本表**(成案 = 消费方 #322 真事故):域表是**闭集**,不在这里的
|
|
11
|
+
* 域名,`config.d/<域>.json` 不会被读进 effective —— 消费方拿到的是「该域缺席」,和文件不存在**逐字
|
|
12
|
+
* 同形**。当年 `limits` 域就是这样在单机腿静默丢了一整片限额发布(远端腿同批可用,所以更难看出来)。
|
|
13
|
+
* 今日的兜底:漏登记的文件会被 `FileConfigStore`(`./node`)的 `"unread-config-file"` 警告点名(fail-loud,
|
|
14
|
+
* 见 getEffective),但那只在消费方接了 `onWarning` 时才响 —— **登记仍是第一现场的义务**。 */
|
|
15
|
+
export declare const PORTABLE_DOMAINS: readonly ["models", "rosters", "skills", "mcp", "plugins", "scenarios", "collab", "governance", "execution", "prompts", "limits"];
|
|
16
|
+
export type PortableDomain = (typeof PORTABLE_DOMAINS)[number];
|
|
17
|
+
/**
|
|
18
|
+
* 非便携域的**排除理由表**(#282 件5 附带裁定:禁裸排除)。
|
|
19
|
+
*
|
|
20
|
+
* `satisfies Record<Exclude<DomainName, PortableDomain>, string>` 让键集在**编译期**双向咬死:新开一个
|
|
21
|
+
* 域却不放进 PORTABLE_DOMAINS ⇒ 这里缺行 ⇒ 编译错;把某域改成便携却忘了删行 ⇒ 多余键 ⇒ 编译错。
|
|
22
|
+
* (此前是「理由写在注释里」,机器只能做子串匹配——域名出现在文件任何角落都算数,等于没门;对抗复审
|
|
23
|
+
* 复审 round1 F3。)理由要写**为什么它不该被单机/跨部署搬运**,带出处。
|
|
24
|
+
*/
|
|
25
|
+
export declare const NON_PORTABLE_DOMAIN_REASONS: {
|
|
26
|
+
readonly workers: "FLEET-only:单机没有 reconciler,`{workers:[]}` 的 schema 默认就是正确语义(缺席不需要 stub)。";
|
|
27
|
+
readonly hosts: "FLEET-only:同 workers —— 单机无机器注册表/自动排布,空注册表即正确语义。";
|
|
28
|
+
readonly systems: "FLEET-only:系统接入/凭据下发的元数据(nonce+路由)属于 web-admin 侧生命周期,单机 lane 无对应执法点。";
|
|
29
|
+
readonly projects: "TENANT IDENTITY DATA,不是可搬运的配置:登记簿随包复制进另一个部署 = 凭空多出的身份登记(design/142 §1.4 威胁模型①「carried-in foreign projectId」换了身 import 的衣服)。要带走走 142 §5 的导出面(service 半场)。";
|
|
30
|
+
readonly entitlement: "per-PRINCIPAL 商务/治理数据(tier·预算上限·kill-switch·allowlist),与 projects 同威胁模型(#282 件5 附带裁定,此前是唯一裸排除项):(a) 搬走 = 把一个租户的商务档复制进另一部署;(b) 反向也不成立 —— 单机 TOC 无 principal 轴(无鉴权语境、无 `/effective?principal=` 解析面),本地 entitlement.json 只会造出一份**永不被执法**的假管控面(真执法点在 service / web-admin 边界)。缺席语义如实:本地 buildEffective 给它 schema 默认值(空表),这**不等于**把远端 kill-switch/预算上限搬到了本地 —— 单机 lane 本就没有那个执法点。";
|
|
31
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** The domains a single-machine TOC carries: what `FileConfigStore` reads out of `config.d/`, and the
|
|
2
|
+
* **default**域集 of the bundle helpers (`exportBundle`/`readBundle`). 每个被排除的域必须在
|
|
3
|
+
* {@link NON_PORTABLE_DOMAIN_REASONS} 里有一条成文理由(禁裸排除,#282 件5 附带裁定)。
|
|
4
|
+
*
|
|
5
|
+
* ⚠️ 口径诚实(对抗复审 复审 round1 F4):这是**默认集与本地读取集,不是 API 级硬边界** —— `exportBundle`
|
|
6
|
+
* / `readBundle` 的 `domains` 参数可显式传任意 `DomainName`(bundle.ts 不拒非便携域)。把非便携域搬进
|
|
7
|
+
* bundle 的那条口子归调用方纪律(以及各自域的导出面),不是本常量能挡住的;要不要把 bundle API 收窄到
|
|
8
|
+
* `PortableDomain` 是一个独立的行为面裁定(#282 未决项,需三问),本批不擅自改。
|
|
9
|
+
*
|
|
10
|
+
* 🔴 **开新顶层域时必须同步登记本表**(成案 = 消费方 #322 真事故):域表是**闭集**,不在这里的
|
|
11
|
+
* 域名,`config.d/<域>.json` 不会被读进 effective —— 消费方拿到的是「该域缺席」,和文件不存在**逐字
|
|
12
|
+
* 同形**。当年 `limits` 域就是这样在单机腿静默丢了一整片限额发布(远端腿同批可用,所以更难看出来)。
|
|
13
|
+
* 今日的兜底:漏登记的文件会被 `FileConfigStore`(`./node`)的 `"unread-config-file"` 警告点名(fail-loud,
|
|
14
|
+
* 见 getEffective),但那只在消费方接了 `onWarning` 时才响 —— **登记仍是第一现场的义务**。 */
|
|
15
|
+
export const PORTABLE_DOMAINS = ["models", "rosters", "skills", "mcp", "plugins", "scenarios", "collab", "governance", "execution", "prompts", "limits"];
|
|
16
|
+
/**
|
|
17
|
+
* 非便携域的**排除理由表**(#282 件5 附带裁定:禁裸排除)。
|
|
18
|
+
*
|
|
19
|
+
* `satisfies Record<Exclude<DomainName, PortableDomain>, string>` 让键集在**编译期**双向咬死:新开一个
|
|
20
|
+
* 域却不放进 PORTABLE_DOMAINS ⇒ 这里缺行 ⇒ 编译错;把某域改成便携却忘了删行 ⇒ 多余键 ⇒ 编译错。
|
|
21
|
+
* (此前是「理由写在注释里」,机器只能做子串匹配——域名出现在文件任何角落都算数,等于没门;对抗复审
|
|
22
|
+
* 复审 round1 F3。)理由要写**为什么它不该被单机/跨部署搬运**,带出处。
|
|
23
|
+
*/
|
|
24
|
+
export const NON_PORTABLE_DOMAIN_REASONS = {
|
|
25
|
+
workers: "FLEET-only:单机没有 reconciler,`{workers:[]}` 的 schema 默认就是正确语义(缺席不需要 stub)。",
|
|
26
|
+
hosts: "FLEET-only:同 workers —— 单机无机器注册表/自动排布,空注册表即正确语义。",
|
|
27
|
+
systems: "FLEET-only:系统接入/凭据下发的元数据(nonce+路由)属于 web-admin 侧生命周期,单机 lane 无对应执法点。",
|
|
28
|
+
projects: "TENANT IDENTITY DATA,不是可搬运的配置:登记簿随包复制进另一个部署 = 凭空多出的身份登记(design/142 §1.4 威胁模型①「carried-in foreign projectId」换了身 import 的衣服)。要带走走 142 §5 的导出面(service 半场)。",
|
|
29
|
+
entitlement: "per-PRINCIPAL 商务/治理数据(tier·预算上限·kill-switch·allowlist),与 projects 同威胁模型(#282 件5 附带裁定,此前是唯一裸排除项):(a) 搬走 = 把一个租户的商务档复制进另一部署;(b) 反向也不成立 —— 单机 TOC 无 principal 轴(无鉴权语境、无 `/effective?principal=` 解析面),本地 entitlement.json 只会造出一份**永不被执法**的假管控面(真执法点在 service / web-admin 边界)。缺席语义如实:本地 buildEffective 给它 schema 默认值(空表),这**不等于**把远端 kill-switch/预算上限搬到了本地 —— 单机 lane 本就没有那个执法点。",
|
|
30
|
+
};
|
|
@@ -4,8 +4,12 @@
|
|
|
4
4
|
* law. This formalizes settings-core's v2-design §3 merge-spec into a published-data contract; the SEMANTIC
|
|
5
5
|
* author is settings-core, web-admin maintains the formalized artifact here (this package), the axis VALUE domains
|
|
6
6
|
* are core's (@sema-agent/core SAFETY_AXIS_VOCABULARY — validated by a dev-only conformance test, never a runtime
|
|
7
|
-
* import).
|
|
8
|
-
*
|
|
7
|
+
* import). The settings-document UNIT table — which keys of a settings file carry a merge contract, where each sits,
|
|
8
|
+
* which category below it maps into, and how its raw value reads — is published by this package too, as
|
|
9
|
+
* `SETTINGS_UNITS` (`./settings-units.ts`, 14.6.0): a consumer reads it instead of keeping a key→category map of its
|
|
10
|
+
* own for those ten units (the http-hook allowlist keys are the one exception: `./http-hook-allowlists` keys and reads them
|
|
11
|
+
* itself, with its own malformed-value reading — see the `./settings-units` header). THIS file is the category spec every unit
|
|
12
|
+
* cites — no second copy of the law.
|
|
9
13
|
*
|
|
10
14
|
* Per-category behavior is fully determined by `mergeShape` + `minTrust` + `tighten` + `enforcement` + `fail` — and,
|
|
11
15
|
* for a MEMBERIZED category (one that declares `members`, 1.1.0), by its member rows: each member's own floor and
|
|
@@ -14,6 +18,16 @@
|
|
|
14
18
|
* admitted or rejected as one value under its `minTrust`. A resolver reads this table and has NO per-key
|
|
15
19
|
* special-casing.
|
|
16
20
|
*
|
|
21
|
+
* COMPARISON (14.6.0) — "which of two values is stricter" has ONE implementation, {@link tightenVerdict}, and it reads
|
|
22
|
+
* ORDER only, never spelling (an empty-set sentinel, a "≤ 0 means off" cap, CC's `"disable"`, a mode alias are the value
|
|
23
|
+
* owner's reading — `readSettingsUnit` for a settings unit).
|
|
24
|
+
* A category's `tighten` answers ONLY the value of a `whole` unit mapped into it (today: `permissionMode`'s enumRank,
|
|
25
|
+
* `additionalDirectories`' setNarrower, `none` for the rest).
|
|
26
|
+
* A DECLARED member's comparator is derived from its row's value class ({@link memberTighten}: `boolPole` ⇒ boolStricter
|
|
27
|
+
* at that pole; `list` ⇒ setUnion for a tighten member, setNarrower for a loosen one). A memberized category's whole value
|
|
28
|
+
* and its UNDECLARED members have NO comparator — they only have the category floor's admission (the paragraph above);
|
|
29
|
+
* asking for one is a RangeError, never a fallback to the category rule.
|
|
30
|
+
*
|
|
17
31
|
* MEMBER-LEVEL ADMISSION (1.1.0 — the WRITTEN layer of the permission-rule dual-channel ruling, [ref] ④;
|
|
18
32
|
* the executing layer is core's `prepareCcImport` door + `RuleAddOrigin`, the resolver/shell halves are cli's):
|
|
19
33
|
* A category may declare `members`, each with its own admission floor and DIRECTION. Today only `permissions`
|
|
@@ -53,8 +67,8 @@
|
|
|
53
67
|
import { z } from "zod";
|
|
54
68
|
/** How values from different layers/principals combine for a category. */
|
|
55
69
|
export declare const MergeShape: z.ZodEnum<{
|
|
56
|
-
concat: "concat";
|
|
57
70
|
replace: "replace";
|
|
71
|
+
concat: "concat";
|
|
58
72
|
denyFirst: "denyFirst";
|
|
59
73
|
runAll: "runAll";
|
|
60
74
|
overrideByName: "overrideByName";
|
|
@@ -121,7 +135,14 @@ export type MemberDirection = z.infer<typeof MemberDirection>;
|
|
|
121
135
|
* must be the LOOSEN floor (the category's), so a consumer that predates this field (its schema strips
|
|
122
136
|
* `boolPole`) reads the row as tighten@<loosen-floor> and withholds BOTH poles from low-trust layers —
|
|
123
137
|
* fail-closed (it loses only the convenience of low-trust tightening, it can never admit a loosening write).
|
|
124
|
-
* Resolution is value-aware: pass the candidate value to {@link memberAdmission} / {@link memberMinTrust}.
|
|
138
|
+
* Resolution is value-aware: pass the candidate value to {@link memberAdmission} / {@link memberMinTrust}.
|
|
139
|
+
*
|
|
140
|
+
* `list` (14.6.0) — the VALUE-CLASS seat of a member whose value is a string list (`permissions.allow` / `deny` / `ask`,
|
|
141
|
+
* `sandboxNetwork.filesystem.denyWrite` / `denyRead`). A row declares exactly ONE value class — `boolPole` (a boolean)
|
|
142
|
+
* or `list` — and the member's comparator is derived from it ({@link resolveMemberTighten}); neither or both is a
|
|
143
|
+
* RangeError there, never a guess (a future numeric member is neither, so it can never be silently read as a list —
|
|
144
|
+
* its value class has to be designed first). The seat carries no floor, so admission reads exactly as before, and a
|
|
145
|
+
* consumer whose schema predates it strips it. */
|
|
125
146
|
export declare const MemberAdmission: z.ZodObject<{
|
|
126
147
|
direction: z.ZodEnum<{
|
|
127
148
|
loosen: "loosen";
|
|
@@ -146,6 +167,7 @@ export declare const MemberAdmission: z.ZodObject<{
|
|
|
146
167
|
task: "task";
|
|
147
168
|
}>;
|
|
148
169
|
}, z.core.$strip>>;
|
|
170
|
+
list: z.ZodOptional<z.ZodLiteral<true>>;
|
|
149
171
|
}, z.core.$strip>;
|
|
150
172
|
export type MemberAdmission = z.infer<typeof MemberAdmission>;
|
|
151
173
|
/** The RESOLVED admission a consumer branches on — the category fallback already applied, so `minTrust` is never
|
|
@@ -180,7 +202,12 @@ export type FailMode = z.infer<typeof FailMode>;
|
|
|
180
202
|
* from LOOSEST→STRICTEST — the category's whole vocabulary: a word outside it is refused, and a ONE-word order (11.0.0:
|
|
181
203
|
* `shellGate`, whose engine vocabulary is the single floor word) is the degenerate rank where the only merge is identity;
|
|
182
204
|
* `boolStricter.strict` is the bool value that is the tighter one; `setNarrower` = a subset is tighter; `policyDenyWins` =
|
|
183
|
-
* deny beats allow; `none` = not rank-tightenable
|
|
205
|
+
* deny beats allow (a fold law BETWEEN a category's members, not an order on one value); `none` = not rank-tightenable
|
|
206
|
+
* (replace / conflict-reject); `setUnion` (14.6.0) = a superset is tighter; `numericLower` (14.6.0) = a smaller number is
|
|
207
|
+
* tighter, `+Infinity` (no limit) the loosest. A parameter may only change the ORDER (`strict`, `order`), so the two
|
|
208
|
+
* appended words take none: a literal that reads as "empty set" or "no limit" is a spelling, normalized by the value's
|
|
209
|
+
* owner before it reaches the comparator. Executed by {@link tightenVerdict}. The two words are appended at the tail
|
|
210
|
+
* (README 发版纪律: a consumer holding an exhaustive table over `rule` goes compile-red at pickup — the pickup cost). */
|
|
184
211
|
export declare const TightenRule: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
185
212
|
rule: z.ZodLiteral<"none">;
|
|
186
213
|
}, z.core.$strip>, z.ZodObject<{
|
|
@@ -193,12 +220,43 @@ export declare const TightenRule: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
193
220
|
}, z.core.$strip>, z.ZodObject<{
|
|
194
221
|
rule: z.ZodLiteral<"enumRank">;
|
|
195
222
|
order: z.ZodArray<z.ZodString>;
|
|
223
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
224
|
+
rule: z.ZodLiteral<"setUnion">;
|
|
225
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
226
|
+
rule: z.ZodLiteral<"numericLower">;
|
|
196
227
|
}, z.core.$strip>], "rule">;
|
|
197
228
|
export type TightenRule = z.infer<typeof TightenRule>;
|
|
229
|
+
/** Where `candidate` lands relative to `existing` under a {@link TightenRule} (14.6.0; closed, in this order):
|
|
230
|
+
* `equal` · `stricter` · `looser` · `unordered` (comparable values, neither side contains the other — two different sets,
|
|
231
|
+
* two different `none` values) · `unreadable` (a side is outside the rule's normalized domain — absent, misspelled,
|
|
232
|
+
* the wrong type). */
|
|
233
|
+
export declare const TIGHTEN_VERDICTS: readonly ["equal", "stricter", "looser", "unordered", "unreadable"];
|
|
234
|
+
export type TightenVerdict = (typeof TIGHTEN_VERDICTS)[number];
|
|
235
|
+
/**
|
|
236
|
+
* THE comparator (14.6.0): where `candidate` lands relative to `existing` under `rule`. One implementation for every
|
|
237
|
+
* consumer — the shell's resolver, the server's catalog column, the profile loader — so "stricter" has one reading.
|
|
238
|
+
* It reads ORDER only: each rule has a normalized domain, and a side outside it is `unreadable` (spellings are normalized
|
|
239
|
+
* by the value's owner first — `readSettingsUnit` for a settings unit):
|
|
240
|
+
* · `enumRank` — a word of `order`; the higher rank is stricter, the same word `equal`;
|
|
241
|
+
* · `boolStricter` — a boolean (strict equality, never truthiness: the string `"false"` is unreadable, not `true`);
|
|
242
|
+
* the `strict` pole is stricter;
|
|
243
|
+
* · `setUnion` — an array of strings, as a SET (order and duplicates do not count): a proper superset is stricter,
|
|
244
|
+
* a proper subset looser, the same set `equal`, neither containing the other `unordered`;
|
|
245
|
+
* · `setNarrower` — the same domain, the opposite direction;
|
|
246
|
+
* · `numericLower` — a finite number or `+Infinity` (no limit, the loosest): the smaller is stricter;
|
|
247
|
+
* · `none` — a strict JSON value no deeper than {@link MAX_HASH_DEPTH}: `equal` when `stableStringify` agrees
|
|
248
|
+
* (key order does not count), else `unordered`;
|
|
249
|
+
* · `policyDenyWins` — a RangeError: it is the fold law BETWEEN a category's members, not an order on one value —
|
|
250
|
+
* compare a member through {@link memberTighten}.
|
|
251
|
+
* Both sides must be present: absence is not a value (`undefined` ⇒ `unreadable`); "an absent landing spot takes the
|
|
252
|
+
* write" is the loading law's business, not the comparator's. A rule word outside the closed set (a JavaScript /
|
|
253
|
+
* deserialized caller) is a RangeError, never a silent verdict.
|
|
254
|
+
*/
|
|
255
|
+
export declare function tightenVerdict(rule: TightenRule, existing: unknown, candidate: unknown): TightenVerdict;
|
|
198
256
|
export declare const MergeCategorySpec: z.ZodObject<{
|
|
199
257
|
mergeShape: z.ZodEnum<{
|
|
200
|
-
concat: "concat";
|
|
201
258
|
replace: "replace";
|
|
259
|
+
concat: "concat";
|
|
202
260
|
denyFirst: "denyFirst";
|
|
203
261
|
runAll: "runAll";
|
|
204
262
|
overrideByName: "overrideByName";
|
|
@@ -223,6 +281,10 @@ export declare const MergeCategorySpec: z.ZodObject<{
|
|
|
223
281
|
}, z.core.$strip>, z.ZodObject<{
|
|
224
282
|
rule: z.ZodLiteral<"enumRank">;
|
|
225
283
|
order: z.ZodArray<z.ZodString>;
|
|
284
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
285
|
+
rule: z.ZodLiteral<"setUnion">;
|
|
286
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
287
|
+
rule: z.ZodLiteral<"numericLower">;
|
|
226
288
|
}, z.core.$strip>], "rule">;
|
|
227
289
|
enforcement: z.ZodEnum<{
|
|
228
290
|
gate: "gate";
|
|
@@ -268,6 +330,7 @@ export declare const MergeCategorySpec: z.ZodObject<{
|
|
|
268
330
|
task: "task";
|
|
269
331
|
}>;
|
|
270
332
|
}, z.core.$strip>>;
|
|
333
|
+
list: z.ZodOptional<z.ZodLiteral<true>>;
|
|
271
334
|
}, z.core.$strip>>>;
|
|
272
335
|
}, z.core.$strip>;
|
|
273
336
|
export type MergeCategorySpec = z.infer<typeof MergeCategorySpec>;
|
|
@@ -288,14 +351,17 @@ export declare const SAFETY_MERGE_SPEC: {
|
|
|
288
351
|
readonly allow: {
|
|
289
352
|
readonly direction: "loosen";
|
|
290
353
|
readonly minTrust: "global";
|
|
354
|
+
readonly list: true;
|
|
291
355
|
};
|
|
292
356
|
readonly deny: {
|
|
293
357
|
readonly direction: "tighten";
|
|
294
358
|
readonly minTrust: "local";
|
|
359
|
+
readonly list: true;
|
|
295
360
|
};
|
|
296
361
|
readonly ask: {
|
|
297
362
|
readonly direction: "tighten";
|
|
298
363
|
readonly minTrust: "local";
|
|
364
|
+
readonly list: true;
|
|
299
365
|
};
|
|
300
366
|
};
|
|
301
367
|
};
|
|
@@ -429,10 +495,12 @@ export declare const SAFETY_MERGE_SPEC: {
|
|
|
429
495
|
readonly "filesystem.denyWrite": {
|
|
430
496
|
readonly direction: "tighten";
|
|
431
497
|
readonly minTrust: "local";
|
|
498
|
+
readonly list: true;
|
|
432
499
|
};
|
|
433
500
|
readonly "filesystem.denyRead": {
|
|
434
501
|
readonly direction: "tighten";
|
|
435
502
|
readonly minTrust: "local";
|
|
503
|
+
readonly list: true;
|
|
436
504
|
};
|
|
437
505
|
};
|
|
438
506
|
};
|
|
@@ -608,3 +676,17 @@ export declare function memberAdmission<C extends SafetyMergeCategory>(category:
|
|
|
608
676
|
/** A member's admission FLOOR from the published table — the word a resolver compares the layer's trust against
|
|
609
677
|
* (`isAtLeast(layer, floor)` ⇒ admitted; the ladder is {@link trustRank}). Convenience over {@link memberAdmission}. */
|
|
610
678
|
export declare function memberMinTrust<C extends SafetyMergeCategory>(category: C, member: SafetyMergeMember<C>, value?: unknown): MinTrust;
|
|
679
|
+
/**
|
|
680
|
+
* Derive one member's comparator off a category spec VALUE (14.6.0; the single implementation — {@link memberTighten} binds
|
|
681
|
+
* it to {@link SAFETY_MERGE_SPEC}). The comparator follows from the row's VALUE CLASS and direction, never from the category's
|
|
682
|
+
* `tighten` (that answers only a `whole` unit's value — the file header):
|
|
683
|
+
* · `boolPole` ⇒ `{ rule: "boolStricter", strict: boolPole.strict }`;
|
|
684
|
+
* · `list` ⇒ `setUnion` for a `tighten` member (a superset narrows more), `setNarrower` for a `loosen` member (a subset grants less).
|
|
685
|
+
* A row declaring NEITHER value class or BOTH is a RangeError (nothing to derive / two readings) — never a default; so are the
|
|
686
|
+
* misuse arms {@link resolveMemberAdmission} has (a whole-key category, an undeclared member).
|
|
687
|
+
* @param categoryLabel only decorates the error message.
|
|
688
|
+
*/
|
|
689
|
+
export declare function resolveMemberTighten(spec: MergeCategorySpec, member: string, categoryLabel?: string): TightenRule;
|
|
690
|
+
/** A declared member's comparator from the published table (see {@link resolveMemberTighten}); compare two of its values with
|
|
691
|
+
* {@link tightenVerdict}. Unknown category or member ⇒ RangeError (never the category rule as a fallback). */
|
|
692
|
+
export declare function memberTighten<C extends SafetyMergeCategory>(category: C, member: SafetyMergeMember<C>): TightenRule;
|