@sema-agent/settings-schema 14.6.0 → 14.8.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 (95) hide show
  1. package/CHANGELOG.md +475 -0
  2. package/README.md +89 -5
  3. package/conformance/VERSION +1 -1
  4. package/conformance/cases/c84-sandbox-git-block-carried-verbatim/expected.json +153 -0
  5. package/conformance/cases/c84-sandbox-git-block-carried-verbatim/input.json +23 -0
  6. package/conformance/cases/c84-sandbox-git-block-carried-verbatim/meta.json +5 -0
  7. package/conformance/cases/c85-sandbox-git-refused-block-still-carried/expected.json +153 -0
  8. package/conformance/cases/c85-sandbox-git-refused-block-still-carried/input.json +23 -0
  9. package/conformance/cases/c85-sandbox-git-refused-block-still-carried/meta.json +5 -0
  10. package/dist/api/auth-bridge.d.ts +16 -2
  11. package/dist/api/auth-bridge.js +59 -24
  12. package/dist/api/credentials.d.ts +281 -0
  13. package/dist/api/credentials.js +149 -0
  14. package/dist/api/scopes.js +5 -1
  15. package/dist/atomic-write.d.ts +15 -0
  16. package/dist/atomic-write.js +109 -5
  17. package/dist/catalog-manages.d.ts +15 -0
  18. package/dist/catalog-manages.js +60 -0
  19. package/dist/center-refs.d.ts +68 -0
  20. package/dist/center-refs.js +70 -0
  21. package/dist/config-d-file.js +33 -15
  22. package/dist/config-dir-migration.js +1 -0
  23. package/dist/config-fns.js +147 -81
  24. package/dist/config-root-documents.d.ts +106 -0
  25. package/dist/config-root-documents.js +93 -0
  26. package/dist/config-root-node.d.ts +117 -0
  27. package/dist/config-root-node.js +260 -0
  28. package/dist/conformance/canonical.js +4 -4
  29. package/dist/conformance/contract-adapter.js +26 -1
  30. package/dist/conformance/corpus.js +4 -4
  31. package/dist/conformance/derive.js +2 -2
  32. package/dist/conformance/run.d.ts +3 -0
  33. package/dist/conformance/run.js +53 -12
  34. package/dist/conformance/temp-roots.d.ts +31 -0
  35. package/dist/conformance/temp-roots.js +184 -0
  36. package/dist/cross-session.d.ts +4 -4
  37. package/dist/declared-capture.d.ts +41 -0
  38. package/dist/declared-capture.js +344 -0
  39. package/dist/default-model.d.ts +1 -1
  40. package/dist/default-model.js +5 -4
  41. package/dist/env-model-table.d.ts +163 -0
  42. package/dist/env-model-table.js +257 -0
  43. package/dist/file-edit.js +11 -15
  44. package/dist/fleet.d.ts +36 -7
  45. package/dist/fleet.js +83 -32
  46. package/dist/hash.d.ts +0 -9
  47. package/dist/hash.js +64 -7
  48. package/dist/hooks-config.js +18 -3
  49. package/dist/http-hook-allowlists.d.ts +22 -9
  50. package/dist/http-hook-allowlists.js +169 -91
  51. package/dist/index.d.ts +7 -0
  52. package/dist/index.js +7 -0
  53. package/dist/mcp-request-entry.d.ts +67 -13
  54. package/dist/mcp-request-entry.js +132 -93
  55. package/dist/mcp-server-name.d.ts +17 -0
  56. package/dist/mcp-server-name.js +19 -0
  57. package/dist/migration-receipt.d.ts +10 -6
  58. package/dist/migration-receipt.js +5 -3
  59. package/dist/node.d.ts +1 -0
  60. package/dist/node.js +2 -0
  61. package/dist/one-time-migrations.d.ts +4 -2
  62. package/dist/one-time-migrations.js +236 -96
  63. package/dist/own-key-read.d.ts +247 -0
  64. package/dist/own-key-read.js +449 -0
  65. package/dist/plain-json-object.d.ts +11 -1
  66. package/dist/plain-json-object.js +37 -1
  67. package/dist/remote-exec.js +9 -1
  68. package/dist/retired-form.d.ts +19 -0
  69. package/dist/retired-form.js +24 -1
  70. package/dist/safety-merge-spec.d.ts +6 -6
  71. package/dist/safety-merge-spec.js +50 -37
  72. package/dist/sandbox-git.d.ts +64 -0
  73. package/dist/sandbox-git.js +89 -0
  74. package/dist/scheduler-store.js +19 -11
  75. package/dist/settings-units.js +14 -12
  76. package/dist/tier-words.d.ts +33 -0
  77. package/dist/tier-words.js +43 -0
  78. package/dist/types.d.ts +12 -2
  79. package/dist/types.js +35 -6
  80. package/dist/url-shape.d.ts +16 -0
  81. package/dist/url-shape.js +18 -0
  82. package/dist/web-admin-credential.js +5 -2
  83. package/dist/web-admin-session-url.js +2 -2
  84. package/dist/worker-row-verdict.d.ts +11 -0
  85. package/dist/worker-row-verdict.js +58 -2
  86. package/dist/workers-rows.js +38 -10
  87. package/dist/zod-unwrap.d.ts +6 -0
  88. package/dist/zod-unwrap.js +14 -7
  89. package/docs/API-SURFACE.md +100 -5
  90. package/docs/CONSUMER-INTEGRATION.md +250 -2
  91. package/llms-full.txt +26 -4
  92. package/llms.txt +11 -4
  93. package/package.json +2 -2
  94. package/dist/definition-copy.d.ts +0 -23
  95. package/dist/definition-copy.js +0 -52
package/dist/hash.js CHANGED
@@ -7,6 +7,8 @@
7
7
  * container restart (drift) or serve a stale 304 (ETag), so we don't want the 32-bit ~77k boundary here
8
8
  * (council finding). Output length isn't a contract (values are only compared for equality before/after).
9
9
  */
10
+ import { UNINSPECTABLE, containerView, inspect, valueOfDescriptor } from "./own-key-read.js";
11
+ import { jsonShapeOf } from "./plain-json-object.js";
10
12
  /** Max nesting stableStringify will hash. A real EffectiveConfig nests ~10-15 deep; this generous cap turns a
11
13
  * pathologically deep OR cyclic value (e.g. a poisoned `model.extraBody` = z.unknown()) into a DETERMINISTIC
12
14
  * throw instead of a `RangeError: Maximum call stack` that would crash every scoped-worker `/effective` poll
@@ -37,13 +39,68 @@ export function stableStringify(v, depth = 0) {
37
39
  // crash every scoped-worker poll. Treat undefined like null for hashing.
38
40
  if (v === undefined)
39
41
  return "null";
40
- if (v === null || typeof v !== "object")
41
- return JSON.stringify(v);
42
- if (Array.isArray(v))
43
- return `[${v.map((x) => stableStringify(x, depth + 1)).join(",")}]`;
44
- const obj = v;
45
- const keys = Object.keys(obj).sort();
46
- return `{${keys.map((k) => `${JSON.stringify(k)}:${stableStringify(obj[k], depth + 1)}`).join(",")}}`;
42
+ // the shape is asked of the seat (total); a node whose members cannot be read is REFUSED loudly with the package's one sentence — a hash of "what could be read" would be a hash of something else.
43
+ // Every container is read through the package's ONE whole-container read (`containerView`, wide): its members and ONE descriptor each are fixed when they are listed (objects: own
44
+ // enumerable string keys; arrays: own indexes + `length`); a value only ever comes from such a descriptor — not from `[[Get]]` (a proxy's `get` / `has` traps are not asked), not
45
+ // through the prototype chain (a hole stays a hole), not from a method of the array (an own `map` is not called).
46
+ // VALUE ORDER = the hash's own order: object keys sorted, array indexes ascending; each member's value is taken from its listed descriptor right before its subtree is hashed,
47
+ // so getters run in the order the hash emits them (a value-first read would run them in insertion order and change the hash of a successful read).
48
+ // Every raw read happens inside `inspect`, the recursion outside it: a refusal raised by a child (depth, unreadable) is never re-labelled by its parent.
49
+ const shape = jsonShapeOf(v);
50
+ switch (shape) {
51
+ case "null":
52
+ case "primitive":
53
+ return JSON.stringify(v);
54
+ case "uninspectable":
55
+ throw new TypeError(`stableStringify: ${UNINSPECTABLE} — refusing to hash`);
56
+ case "array": {
57
+ // the length is the listed `length` value through `ToLength(ToNumber(·))`; an index that is not an own member is a hole (it hashes as nothing between two commas)
58
+ const view = readOrRefuse(() => containerView(v, true, "wide"));
59
+ const length = readOrRefuse(() => lengthOf(view.length));
60
+ const byIndex = new Map(view.members);
61
+ let out = "";
62
+ for (let i = 0; i < length; i++) {
63
+ const d = byIndex.get(String(i));
64
+ out += `${i === 0 ? "" : ","}${d === undefined ? "" : stableStringify(readOrRefuse(() => valueOfDescriptor(v, d)), depth + 1)}`;
65
+ }
66
+ return `[${out}]`;
67
+ }
68
+ case "plain":
69
+ case "non_plain_object": {
70
+ if (typeof v === "function")
71
+ return JSON.stringify(v); // not data (and never was hashed as an object)
72
+ // the package's one whole-container read (wide): the members — own ENUMERABLE string keys, `Object.keys`' set — and ONE descriptor each are fixed when the keys are listed
73
+ // (a later change to the object by a getter does not change what is hashed); keys sorted; each member's value comes from ITS listed descriptor right before its subtree is hashed
74
+ const members = readOrRefuse(() => containerView(v, false, "wide")).members.slice().sort(([a], [b]) => (a < b ? -1 : 1));
75
+ let out = "";
76
+ for (const [key, d] of members) {
77
+ const value = readOrRefuse(() => valueOfDescriptor(v, d));
78
+ out += `${out === "" ? "" : ","}${JSON.stringify(key)}:${stableStringify(value, depth + 1)}`;
79
+ }
80
+ return `{${out}}`;
81
+ }
82
+ default: {
83
+ const unhandled = shape;
84
+ throw new Error(`stableStringify: unhandled shape ${String(unhandled)}`);
85
+ }
86
+ }
87
+ }
88
+ /** One raw read of a node's member (inside `inspect`); unreadable ⇒ the package's one refusal sentence. */
89
+ function readOrRefuse(read) {
90
+ const r = inspect(read);
91
+ if (!r.ok)
92
+ throw new TypeError(`stableStringify: ${r.sentence} — refusing to hash`);
93
+ return r.value;
94
+ }
95
+ /** An array's length from its listed `length` value: `ToLength(ToNumber(value))` — the unary plus is `ToNumber` (a BigInt / Symbol throws, an object is converted through
96
+ * `valueOf` / `toString`), not a number ⇒ 0, truncated — with the allocation limit of an array: a length beyond the largest array length (2^32 − 1 — only a proxy's descriptor
97
+ * trap can report one) throws before a single index is hashed (the caller refuses with the one sentence). */
98
+ function lengthOf(value) {
99
+ const n = +value;
100
+ const length = Number.isNaN(n) || n <= 0 ? 0 : Math.floor(n);
101
+ if (length > 2 ** 32 - 1)
102
+ throw new RangeError("Invalid array length");
103
+ return length;
47
104
  }
48
105
  const FNV64_OFFSET = 0xcbf29ce484222325n;
49
106
  const FNV64_PRIME = 0x100000001b3n;
@@ -209,6 +209,15 @@ function hookIsArray(v) {
209
209
  return false; // 已撤销的 Proxy:读不出 ⇒ 不是数组
210
210
  }
211
211
  }
212
+ /** `Symbol.iterator` WITHOUT the `Symbol` free name: the classifier closure is copied by consumers' generators whose registered built-ins do not include `Symbol` (a new built-in word
213
+ * in the closure is a change they must follow), so the well-known symbol is taken from the one place every realm keeps it — `Array.prototype` is iterable by it. Read once at load;
214
+ * a process in which `Array.prototype` has lost its iterator cannot say which objects hold content off their own keys, so the module refuses to load rather than answer "none do". */
215
+ const HOOK_ITERATOR_SYMBOL = (() => {
216
+ const found = Object.getOwnPropertySymbols(Array.prototype).find((s) => s.description === "Symbol.iterator");
217
+ if (found === undefined)
218
+ throw new Error("hooks classifier: Array.prototype has no Symbol.iterator");
219
+ return found;
220
+ })();
212
221
  /** The first own key of `node` (in key order) that names a guard event with a value other than null / undefined / `[]` (CC `ei`). */
213
222
  function hookGuardKeyOf(node) {
214
223
  for (const key of Object.keys(node)) {
@@ -227,7 +236,7 @@ function hookLooksLikeMatcher(value) {
227
236
  const inner = Object.hasOwn(value, "hooks") ? value.hooks : undefined;
228
237
  if (hookIsArray(inner))
229
238
  return inner.length > 0;
230
- return hookIsRecord(inner) && (Object.hasOwn(value, "matcher") || typeof inner.type === "string");
239
+ return hookIsRecord(inner) && (Object.hasOwn(value, "matcher") || (Object.hasOwn(inner, "type") && typeof inner.type === "string"));
231
240
  }
232
241
  /**
233
242
  * The coordinate of the first guard sign in `root`'s subtree, or undefined: each object answers for itself before its members are searched (its own
@@ -263,8 +272,14 @@ function hookGuardSignIn(root, rootPath, place) {
263
272
  isList = true;
264
273
  break;
265
274
  case "plain":
266
- case "non_plain_object":
267
275
  break; // searched as an object below
276
+ case "non_plain_object":
277
+ // an object whose content is supplied by the iteration protocol (a Map, a Set, a generator …) holds that content OFF its own keys: the own-key search below
278
+ // would find nothing and a guard held inside would vanish with the dropped piece. It cannot be shown free of guard signs ⇒ a sign at its own coordinate (same arm as
279
+ // "uninspectable"). Every other non-plain object (a class instance, a Date, a function) keeps what it holds on its own keys, searched below.
280
+ if (HOOK_ITERATOR_SYMBOL in node)
281
+ return pathTo(frame, []);
282
+ break;
268
283
  default: {
269
284
  const unhandled = shape;
270
285
  throw new Error(`hookGuardSignIn: unhandled shape ${String(unhandled)}`);
@@ -418,7 +433,7 @@ function hookBlockRead(hooks, reject, drop) {
418
433
  drop("hooks-not-object", ["hooks"]);
419
434
  return undefined;
420
435
  }
421
- if (hookLooksLikeMatcher(hooks) || typeof hooks.type === "string") {
436
+ if (hookLooksLikeMatcher(hooks) || (Object.hasOwn(hooks, "type") && typeof hooks.type === "string")) {
422
437
  reject("hooks-is-matcher", ["hooks"]);
423
438
  return undefined;
424
439
  }
@@ -29,21 +29,25 @@ export declare const HttpHookAllowlistSettings: z.ZodObject<{
29
29
  }, z.core.$loose>;
30
30
  export type HttpHookAllowlistSettings = z.infer<typeof HttpHookAllowlistSettings>;
31
31
  /**
32
- * 一个设置层对两键的读数。键缺席 = 这一层不设限;`[]` = 这一层一个都不许。`source` = 这一层是哪个设置源(闭集 `SETTINGS_SOURCES`,原样回显进
33
- * `sources`);准入由判官按源经 `layerAdmission` 判,调用方不预先筛层、不给档。
32
+ * 一个设置层对两键的读数(读器交出的读数里的 `layer`)。键缺席 = 这一层不设限;`[]` = 这一层一个都不许。`source` = 这一层是哪个设置源(闭集 `SETTINGS_SOURCES`,原样回显进
33
+ * `sources`);准入由判官按源经 `layerAdmission` 判,调用方不预先筛层、不给档。读器交出的是寻常对象(不冻结)。
34
+ * **判官经读器读每一层**(与读器同一只读法,见 `httpHookUrlVerdict`):读器交出的层、调用方手造的寻常层、本 realm 的 `JSON.parse` 出来的层都按内容判,同一份内容同一个答案(另一个 realm 的数组按坏形,见 CHANGELOG 例外 ⑤)。读器交出的层里坏形名单已读成 `[]`(点名在读器的 `malformed`),所以同一份坏形设置直接交判官答 `malformed`、先过读器再交判官答 `empty` —— 坏形的告警看读器那一份读数。
34
35
  */
35
36
  export interface HttpHookAllowlistLayer {
36
37
  readonly source: SettingsSource;
37
38
  readonly allowedHttpHookUrls?: readonly string[];
38
39
  readonly httpHookAllowedEnvVars?: readonly string[];
39
40
  }
40
- /** 一层的读法结果:层读数 + 读成空名单的坏形键(调用方据此告警;读法本身不出句)。 */
41
+ /**
42
+ * 一层的读法结果:层读数 + 读成空名单的坏形键(调用方据此告警;读法本身不出句)。由 {@link readHttpHookAllowlistLayer} 交出:普通对象、不冻结(与 v14.6.0 同形)。
43
+ * 读器对自己交出的 `layer` 再读,名单逐位同,`malformed` 只在第一次读出现(坏键第一次已读成 `[]`,再读是空名单);交判官与交它的副本同答。
44
+ */
41
45
  export interface HttpHookAllowlistLayerRead {
42
46
  readonly layer: HttpHookAllowlistLayer;
43
47
  readonly malformed: readonly HttpHookAllowlistKey[];
44
48
  }
45
49
  /**
46
- * 从一层的原始设置对象读出两键(唯一读法)。键缺席(或值为 `undefined`)⇒ 不设;字符串数组 ⇒ 原样(过 `captureAllowlist`〔包内私有〕,与判官同一只读法);其余一切(非数组、含非串条目或洞、取值或读元素时抛、`null`)
50
+ * 从一层的原始设置对象读出两键,过 `allowlistsOf`〔包内私有〕—— 与判官同一只读法:键缺席(或值为 `undefined`)⇒ 不设;字符串数组 ⇒ 原样的捕获副本;其余一切(非数组、含非串条目或洞、取值或读元素时抛、`null`)
47
51
  * ⇒ **读成 `[]`**(CC 同向:坏形名单 = 一个都不许)并在 `malformed` 点名。整份设置不是对象(`null` / 原始值 / 数组)⇒ 两键都缺。
48
52
  * **是对象但不朴素**(类实例 / `Map` / 建在别的对象原型上的记录 / 函数 —— JSON 解析不出来,只能是嵌入方程序拼的)⇒ 同样 fail-closed:两键都读成 `[]` 并点名 `malformed`。
49
53
  * 把它当「没有设置」读,等于把一层带名单的托管设置静默丢掉、方向放行(与本模块「坏形 = 一个都不许」的失败方向相反)。
@@ -51,17 +55,26 @@ export interface HttpHookAllowlistLayerRead {
51
55
  * 原型是带成员的 null 原型对象的对象(`Object.create(<null 原型的名单袋>)`)是一例:它按座位的判据读成朴素、按**自有**键读,名单键只在原型上 ⇒ 坏形。它只能由程序造出,JSON 解析不出来(座位头注 `./plain-json-object` 同写)。
52
56
  */
53
57
  export declare function readHttpHookAllowlistLayer(source: SettingsSource, settings: unknown): HttpHookAllowlistLayerRead;
54
- /** 拦下一只 http hook 的原因。`rule`:`empty` = 有一张准入名单是空的(那一层改对之前一只都不许;**名单形坏 —— 非串数组、含非串条目或洞、读它或读它的元素时 getter 抛 —— 也读成空**,所以 `empty` 不一定是写成了 `[]`);`url` = 没命中那几张名单里的任何模式。
55
- * `sources` = 不放行它的那几张名单所在的层(入参序)。零 URL 回显 —— URL 调用方自己有,按受众给显示形。 */
58
+ /** 拦下一只 http hook 的原因。`rule`(闭集,先到先得:`malformed` 先于 `empty` 先于 `url`):
59
+ * `malformed` = 有一层判官读不出 —— ① 源读不出:`layers` 本身不是朴素数组 / 读不出 / 两份观测对不上,或某层的 `source` 缺席 / 只靠继承可见 / 读不出 / 两份不一致 / 不是串、层不是对象
60
+ * ⇒ 那一层没有可信的源,不点名;② 源读得出、名单读不出(读器对同一份设置报 `malformed` 的那些形:不是朴素串数组 —— 另一个 realm 的数组、带框架挂件的数组、含非串条目或洞 ——、
61
+ * 读它或读它的元素时 getter 抛、两份观测不一致、名单键只靠继承可见、层对象不朴素)⇒ 点名那一层;
62
+ * `empty` = 有一张准入名单真是空的 `[]`(那一层改对之前一只都不许;读器交出的层里坏形名单已是 `[]`,交判官也答 `empty` —— 坏形点名看读器的 `malformed`);
63
+ * `url` = 没命中那几张名单里的任何模式。
64
+ * `sources` = 不放行它的、读得出源的那几层(入参序)—— 所以 `rule: "malformed"` 时 `sources` 可以是空的(只有源读不出的层在拦)。零 URL 回显 —— URL 调用方自己有,按受众给显示形。 */
56
65
  export interface HttpHookUrlBlock {
57
66
  readonly list: "allowedHttpHookUrls";
58
- readonly rule: "empty" | "url";
67
+ readonly rule: "malformed" | "empty" | "url";
59
68
  readonly sources: readonly SettingsSource[];
60
69
  }
61
70
  /**
62
71
  * 一只 http hook 的 URL 过不过名单:每一张准入的 `allowedHttpHookUrls` 都要有一条模式按放行出口({@link urlAllowPatternMatches})命中它。
63
- * 没有准入层写这张名单 ⇒ 不限(`undefined`)。`workspace` = 宿主对工作区信任的读数(读口抛或闭集外按 `declined` 交),与 `layerAdmission` 同一个入参。
72
+ * 没有准入层写这张名单 ⇒ 不限(`undefined`)。`layers` 的每一层经读器读({@link readHttpHookAllowlistLayer} 同一只读法;读不出源 ⇒ 坏形层,全拦、`rule: "malformed"`)。
73
+ * `workspace` = 宿主对工作区信任的读数(读口抛或闭集外按 `declined` 交),与 `layerAdmission` 同一个入参。
64
74
  */
65
75
  export declare function httpHookUrlVerdict(url: string, layers: readonly HttpHookAllowlistLayer[], workspace: WorkspaceTrustState): HttpHookUrlBlock | undefined;
66
- /** 一只 http hook 实际可插值进请求头的环境变量:它自己的 `allowedEnvVars` 与每一张准入的 `httpHookAllowedEnvVars` 取交(保持 hook 自己的序)。 */
76
+ /**
77
+ * 一只 http hook 实际可插值进请求头的环境变量:它自己的 `allowedEnvVars` 与每一张准入的 `httpHookAllowedEnvVars` 取交(保持 hook 自己的序)。
78
+ * `layers` 同 {@link httpHookUrlVerdict}(坏形层 ⇒ 一个都不放);`hookVars` 只许普通的串数组({@link plainArrayOf};不是 ⇒ 当空,一个都不放)。
79
+ */
67
80
  export declare function httpHookEnvVarsAllowed(hookVars: readonly string[], layers: readonly HttpHookAllowlistLayer[], workspace: WorkspaceTrustState): string[];
@@ -15,8 +15,8 @@
15
15
  * 判官出结构不出句:放行 ⇒ `undefined`;拦下 ⇒ {@link HttpHookUrlBlock}(哪张名单 / 为什么 / 出自哪几层),用户面那一句由各宿主按它铸。
16
16
  */
17
17
  import { z } from "zod";
18
- import { captureStringArray } from "./definition-copy.js";
19
- import { jsonShapeOf } from "./plain-json-object.js";
18
+ import { captureDenseArray, inspect, isStringEntry, readKey } from "./own-key-read.js";
19
+ import { isJsonArray, isPlainJsonArray, jsonShapeOf } from "./plain-json-object.js";
20
20
  import { layerAdmission } from "./settings-sources.js";
21
21
  import { urlAllowPatternMatches } from "./url-pattern.js";
22
22
  /** 两键(闭集,声明序)。 */
@@ -31,129 +31,207 @@ export const HttpHookAllowlistSettings = z
31
31
  httpHookAllowedEnvVars: z.array(z.string()).optional(),
32
32
  })
33
33
  .passthrough();
34
+ /** 坏形的那一份读数(名单按 `[]` 读、点名 malformed)。每次一份新数组。 */
35
+ const malformedList = () => ({ list: [], malformed: true });
36
+ const UNSET = { kind: "unset" };
37
+ const BAD = { kind: "bad" };
34
38
  /**
35
- * 一张名单的**唯一读法**(读器与判官共用,两边永远同答):**取值 + 复制 + 检视整段在一个 try 里**,名单键只读一次;名单本身走包内的串数组捕获读法
36
- * `captureStringArray`(`./definition-copy`:按下标 / `hasOwn` / 一次读 / 非串即坏 / 定义语义写入,不走迭代协议,交副本)—— 与请求面 `args` 同一只读法。
37
- * 键没写(取到 `undefined`)⇒ `undefined`(不设);是串数组 ⇒ 一份已校验的捕获副本;其余一切 ⇒ 读成 `[]` 并标 `malformed`(fail-closed:一个都不许)——
38
- * 取值抛(键本身是会抛的 getter)、不是串数组(含稀疏数组的洞 —— 不顺原型链读;`length` 不是安全非负整数;读元素时抛),一个也不抛出去。
39
+ * 安全轴的数组(`layers` / `hookVars` / 两张名单)的描述符观测 D —— **只许朴素数组**,一条规则:座位的朴素数组判据({@link isPlainJsonArray}:是数组、原型恒等于本 realm 的 `Array.prototype`、
40
+ * `constructor` / `filter` / `flatMap` / `Symbol.iterator` 与本 realm 的内建恒等 —— 子类、假原型、`null` / 换过的原型、谎报原型或对这几只成员说谎的 Proxy、另一个 realm 的数组都不是);整容器读({@link denseArrayView},`wide` 归类)一次列自有键 ——
41
+ * 稠密(`length` 是安全非负整数、自有下标正好 `0 … length-1`),且**自有键只有下标与 `length`**:任何别的自有键(方法覆盖、`constructor`、任意串键、符号键 —— 含 `Symbol.species`)
42
+ * ⇒ 不是朴素数组,整个输入按坏形、不读元素(v14.6.0 的判官调过调用方数组自己的 `filter`,子类 / 自有方法 / species 构造器让它拦下或少放;本版一律拦,拦集不缩)。
43
+ * 元素值取自那一份描述符(访问器下标调它的 getter 一次),逐个过 `isEntry`(不过即停、后面的不读);交出一份定义语义造的副本。不是 / 读不出 ⇒ `undefined`。
39
44
  */
40
- function captureAllowlist(holder, key) {
41
- try {
42
- const v = holder[key];
43
- if (v === undefined)
44
- return undefined;
45
- const list = captureStringArray(v); // 串数组的唯一读法(`./definition-copy`):按下标 / hasOwn / 一次读 / 非串即坏 / 定义语义写入,交副本
46
- return list === undefined ? { list: [], malformed: true } : { list, malformed: false };
47
- }
48
- catch {
49
- return { list: [], malformed: true };
50
- }
45
+ function plainArrayOf(v, isEntry) {
46
+ return isPlainJsonArray(v) ? captureDenseArray(v, "wide", isEntry, "refuse") : undefined; // 拷贝循环是内核那一份(与串数组读法同一只)
51
47
  }
52
- function presenceOf(bag, key) {
53
- try {
54
- return Object.hasOwn(bag, key) ? "own" : key in bag ? "inherited" : "absent";
55
- }
56
- catch {
57
- return "unreadable";
48
+ const isAny = (_entry) => true;
49
+ /** 判官直接拿到的数组(`layers` / `hookVars`):描述符观测({@link plainArrayOf})与普通读({@link agreesByOrdinaryReading})两份一致才算读到;否则 `undefined`。设置里的名单不走这一只 —— 两键的描述符观测要先于任何普通读(见 {@link allowlistsOf})。 */
50
+ function bothObservations(v, isEntry) {
51
+ const seen = plainArrayOf(v, isEntry);
52
+ return seen !== undefined && agreesByOrdinaryReading(v, seen) ? seen : undefined;
53
+ }
54
+ /**
55
+ * 观测 D(描述符通道,本包的读内核):名单键经 `readKey` 读一次 —— 缺席 / 自有 `undefined` ⇒ 不设;自有 ⇒ {@link plainArrayOf}(只许普通的串数组;不是 ⇒ 坏形);
56
+ * 只靠继承可见 / 读不出 ⇒ 坏形。
57
+ */
58
+ function describedList(holder, key) {
59
+ const read = readKey(holder, key);
60
+ switch (read.presence) {
61
+ case "absent":
62
+ return UNSET;
63
+ case "inherited":
64
+ case "unreadable":
65
+ return BAD;
66
+ case "own": {
67
+ if (read.value === undefined)
68
+ return UNSET;
69
+ const list = plainArrayOf(read.value, isStringEntry);
70
+ return list === undefined ? BAD : { kind: "list", list };
71
+ }
72
+ default: {
73
+ const unhandled = read;
74
+ throw new Error(`describedList: unhandled presence ${String(unhandled.presence)}`);
75
+ }
58
76
  }
59
77
  }
60
78
  /**
61
- * 从一层的原始设置对象读出两键(唯一读法)。键缺席(或值为 `undefined`)⇒ 不设;字符串数组 ⇒ 原样(过 `captureAllowlist`〔包内私有〕,与判官同一只读法);其余一切(非数组、含非串条目或洞、取值或读元素时抛、`null`)
62
- * ⇒ **读成 `[]`**(CC 同向:坏形名单 = 一个都不许)并在 `malformed` 点名。整份设置不是对象(`null` / 原始值 / 数组)⇒ 两键都缺。
63
- * **是对象但不朴素**(类实例 / `Map` / 建在别的对象原型上的记录 / 函数 —— JSON 解析不出来,只能是嵌入方程序拼的)⇒ 同样 fail-closed:两键都读成 `[]` 并点名 `malformed`。
64
- * 把它当「没有设置」读,等于把一层带名单的托管设置静默丢掉、方向放行(与本模块「坏形 = 一个都不许」的失败方向相反)。
65
- * 名单键**只靠继承可见**(不是自有键)⇒ 同样 fail-closed:该键读成 `[]` 并点名 `malformed`(自有键盖过继承键;自有键值为 `undefined` 仍是「不设」)。
66
- * 原型是带成员的 null 原型对象的对象(`Object.create(<null 原型的名单袋>)`)是一例:它按座位的判据读成朴素、按**自有**键读,名单键只在原型上 ⇒ 坏形。它只能由程序造出,JSON 解析不出来(座位头注 `./plain-json-object` 同写)。
79
+ * 观测 G(普通读通道)与描述符那份 `seen` 是否一致:座位判数组、`length` 经 `[[Get]]` 等于 `seen.length`、逐下标「是不是自有」(`Object.hasOwn` —— 14.6.0 读器读名单的读法)、
80
+ * 「在不在」(`i in v`,HasProperty —— v14.6.0 判官对 `layers` / `hookVars` 的 `filter` 按它跳过下标)且 `[[Get]]` 的值与 `seen` 同一(`Object.is`)。
81
+ * 读量以 `seen` 的长度为界(长度不同即不一致,不逐位扫);读不出 ⇒ 不一致。名单、判官的 `layers` / `hookVars` 都用这一只。
67
82
  */
68
- export function readHttpHookAllowlistLayer(source, settings) {
69
- const shape = jsonShapeOf(settings);
70
- let bag = Object.create(null); // 空袋没有原型:不存在 / 非对象的层看不见 `Object.prototype` 上的污染键,仍「两键都缺」
71
- let refuseBoth = false; // 是对象但不朴素 ⇒ 两键都读成 [](fail-closed)
83
+ function agreesByOrdinaryReading(v, seen) {
84
+ const same = inspect(() => {
85
+ if (!isJsonArray(v) || v.length !== seen.length)
86
+ return false;
87
+ for (let i = 0; i < seen.length; i++)
88
+ if (!Object.hasOwn(v, i) || !(i in v) || !Object.is(v[i], seen[i]))
89
+ return false;
90
+ return true;
91
+ });
92
+ return same.ok && same.value;
93
+ }
94
+ /**
95
+ * 名单在一个对象上的**唯一读法**(读器读设置对象、判官读层对象,都调这一只 —— 两边对同一个对象永远同答):先问座位它是什么形(`jsonShapeOf`,一次)——
96
+ * · 朴素对象 ⇒ **两份观测一致才算读到**:先对每个要的键做描述符观测 D({@link describedList}),再逐键做普通读 G({@link agreesByOrdinaryReading},v14.6.0 读器与判官普通读的合取)对拍 ——
97
+ * 两份都「不设」⇒ 不设;都是同一张串名单 ⇒ D 的捕获副本;其余一切(只一份读到、内容不同、任一份读不出 / 坏形、名单键只靠继承可见)⇒ `[]` + malformed(fail-closed:对无状态构造,拦集包含
98
+ * 「只看 G」的 14.6.0 与「只看 D」的读法各自的拦集,两份不一致也拦;普通对象上两份观测恒同,JSON 面零变化)。挡不住的(如实):有状态陷阱 —— 两次都答同一份假名单,
99
+ * 或第一次观测改了第二次观测的答案;G 是本模块读名单时唯一的 `[[Get]]`,只作对拍 —— 交出去的值取自 D;
100
+ * · 是对象但不朴素(类实例 / `Map` / 建在别的对象原型上的记录 / 函数 —— JSON 解析不出来,只能是嵌入方程序拼的)或读形状就抛(已撤销的 Proxy)⇒ 要的键都读成 `[]` + malformed
101
+ * (fail-closed:把它当「没有名单」读,等于把一层带名单的设置静默丢掉、方向放行);
102
+ * · 不是对象(`null` / 原始值 / 数组)⇒ 都缺。
103
+ * 交出零原型的袋(只含读到的键)。
104
+ */
105
+ function allowlistsOf(holder, keys) {
106
+ const out = Object.create(null); // 零原型:原型上被污染成只读的同名键不让赋值抛
107
+ const shape = jsonShapeOf(holder);
72
108
  switch (shape) {
73
- case "plain":
74
- bag = settings;
109
+ case "plain": {
110
+ // 安全轴的名单要**两份观测一致才算读到**:先对所有要的键做描述符观测 D(在任何普通读之前 —— 一个键的普通读改不动另一个键已经拿到的观测),再逐键做普通读 G 对拍
111
+ const described = keys.map((key) => describedList(holder, key));
112
+ keys.forEach((key, i) => {
113
+ const d = described[i];
114
+ if (d.kind === "bad") {
115
+ out[key] = malformedList();
116
+ return;
117
+ }
118
+ const got = inspect(() => holder[key]);
119
+ const agrees = got.ok && (d.kind === "unset" ? got.value === undefined : agreesByOrdinaryReading(got.value, d.list));
120
+ if (!agrees)
121
+ out[key] = malformedList();
122
+ else if (d.kind === "list")
123
+ out[key] = { list: d.list, malformed: false };
124
+ });
75
125
  break;
126
+ }
76
127
  case "non_plain_object":
77
- case "uninspectable": // 读形状就抛的对象(已撤销的 Proxy)同样 fail-closed:读不出就不能当「没有设置」放行
78
- refuseBoth = true;
128
+ case "uninspectable":
129
+ for (const key of keys)
130
+ out[key] = malformedList();
79
131
  break;
80
132
  case "array":
81
133
  case "null":
82
134
  case "primitive":
83
- break; // 整份设置不是对象 ⇒ 两键都缺
135
+ break;
84
136
  default: {
85
137
  const unhandled = shape;
86
- throw new Error(`readHttpHookAllowlistLayer: unhandled shape ${String(unhandled)}`);
138
+ throw new Error(`allowlistsOf: unhandled shape ${String(unhandled)}`);
87
139
  }
88
140
  }
89
- const bad = Object.create(null); // 读成 [] 的坏形键(零原型的标记袋;`malformed` 数组出口只用定义语义造,见下)
90
- // 累加器也零原型:`{}` 继承 `Object.prototype`,原型上被污染成只读(只有 getter)的同名键会让下面的赋值抛(读器该 fail-closed,不是抛)
141
+ return out;
142
+ }
143
+ /**
144
+ * 从一层的原始设置对象读出两键,过 `allowlistsOf`〔包内私有〕—— 与判官同一只读法:键缺席(或值为 `undefined`)⇒ 不设;字符串数组 ⇒ 原样的捕获副本;其余一切(非数组、含非串条目或洞、取值或读元素时抛、`null`)
145
+ * ⇒ **读成 `[]`**(CC 同向:坏形名单 = 一个都不许)并在 `malformed` 点名。整份设置不是对象(`null` / 原始值 / 数组)⇒ 两键都缺。
146
+ * **是对象但不朴素**(类实例 / `Map` / 建在别的对象原型上的记录 / 函数 —— JSON 解析不出来,只能是嵌入方程序拼的)⇒ 同样 fail-closed:两键都读成 `[]` 并点名 `malformed`。
147
+ * 把它当「没有设置」读,等于把一层带名单的托管设置静默丢掉、方向放行(与本模块「坏形 = 一个都不许」的失败方向相反)。
148
+ * 名单键**只靠继承可见**(不是自有键)⇒ 同样 fail-closed:该键读成 `[]` 并点名 `malformed`(自有键盖过继承键;自有键值为 `undefined` 仍是「不设」)。
149
+ * 原型是带成员的 null 原型对象的对象(`Object.create(<null 原型的名单袋>)`)是一例:它按座位的判据读成朴素、按**自有**键读,名单键只在原型上 ⇒ 坏形。它只能由程序造出,JSON 解析不出来(座位头注 `./plain-json-object` 同写)。
150
+ */
151
+ export function readHttpHookAllowlistLayer(source, settings) {
152
+ const read = allowlistsOf(settings, HTTP_HOOK_ALLOWLIST_KEYS);
153
+ // 累加器零原型:`{}` 继承 `Object.prototype`,原型上被污染成只读(只有 getter)的同名键会让赋值抛(读器该 fail-closed,不是抛)
91
154
  const lists = Object.create(null);
92
155
  for (const key of HTTP_HOOK_ALLOWLIST_KEYS) {
93
- if (refuseBoth) {
94
- lists[key] = [];
95
- bad[key] = true;
96
- continue;
97
- }
98
- // 对袋的每一次触碰(`in` / `hasOwn` / 取值)都经守护:先问这个键在袋里怎么在场(一只守护的探针),再取值(`captureAllowlist`,自带守护)
99
- const presence = presenceOf(bag, key);
100
- switch (presence) {
101
- case "absent":
102
- continue;
103
- case "inherited": // 名单键只靠继承可见 ⇒ 不能当「没有名单」放行
104
- case "unreadable": // 袋读不出(`has` / `getOwnPropertyDescriptor` 陷阱抛)⇒ 同样 fail-closed
105
- lists[key] = [];
106
- bad[key] = true;
107
- continue;
108
- case "own": {
109
- const read = captureAllowlist(bag, key); // 取值也在 try 里、只读一次:键是会抛的自有 getter ⇒ 坏形,不是抛出读器
110
- if (read === undefined)
111
- continue;
112
- lists[key] = read.list;
113
- if (read.malformed)
114
- bad[key] = true;
115
- break;
116
- }
117
- default: {
118
- const unhandled = presence;
119
- throw new Error(`readHttpHookAllowlistLayer: unhandled presence ${String(unhandled)}`);
120
- }
121
- }
156
+ const captured = read[key];
157
+ if (captured !== undefined)
158
+ lists[key] = captured.list;
122
159
  }
123
- // `malformed` 用 filter 造(定义语义)—— `push` 会命中 `Object.prototype` 上继承的数字键 setter、吞掉点名;层是普通对象(返回值的运行期形状与 v14.5.0 同),`...lists` 是定义语义拷贝,污染下不抛
124
- const malformed = HTTP_HOOK_ALLOWLIST_KEYS.filter((key) => bad[key] === true);
160
+ // `malformed` 用 filter 造(定义语义)—— `push` 会命中 `Object.prototype` 上继承的数字键 setter、吞掉点名;返回形与 v14.6.0 同(普通对象、不冻结),字面量与 `...lists` 都是定义语义,污染下不抛
161
+ const malformed = HTTP_HOOK_ALLOWLIST_KEYS.filter((key) => read[key]?.malformed === true);
125
162
  return { layer: { source, ...lists }, malformed };
126
163
  }
164
+ const MALFORMED_LAYER = { source: undefined, list: [], malformed: true };
127
165
  /**
128
- * 一层的某张名单:按 `[[Get]]` 读(名单放在原型上的手造层 —— 类 getter、`Object.create(defaults)` —— 照旧生效,与 v14.5.0 同答),
129
- * 过 `captureAllowlist`〔包内私有〕—— 与读器**同一只读法**,产出一份已校验的捕获副本,判官只读这份副本(元素只读一次)。
130
- * 读到 `undefined` ⇒ 没写(`undefined`);取值时 getter 抛、或读到的不是串数组(污染出非数组、含非串条目或洞、读 `length` / 元素的 getter 或 Proxy 陷阱抛)⇒ 读成 `[]`(fail-closed:一个都不许),不抛。
131
- * 原型污染出来的名单只会让准入名单取交后更窄 —— 方向是拦,不是放;读器造的缺席层也是普通对象,污染下同样被收紧(fail-closed,判决的 `sources` 点名该层)。
166
+ * 一层的 `source`,与名单同一条双读一致律:层是对象(朴素与否都读)⇒ 自有描述符那份(`readKey`)与普通读那份(`[[Get]]`)是同一个值、且是串 ⇒ 那个词
167
+ * (闭集不在这里判:读得出的词交 `layerAdmission`,闭集外的词在那里响亮抛 —— 那是宿主代码写错了词,不是数据坏形);
168
+ * 其余一切(不是对象 / 读形状就抛、`source` 缺席 / 只靠继承可见 / 读不出、两份不一致、不是串)⇒ `undefined`(判官把这一层当坏形层,判决词 `malformed`)。
132
169
  */
133
- function listOf(layer, key) {
134
- return captureAllowlist(layer, key)?.list;
170
+ function sourceOf(element) {
171
+ const shape = jsonShapeOf(element);
172
+ switch (shape) {
173
+ case "plain":
174
+ case "non_plain_object": {
175
+ const described = readKey(element, "source");
176
+ if (described.presence !== "own")
177
+ return undefined;
178
+ const got = inspect(() => element.source);
179
+ return got.ok && Object.is(got.value, described.value) && typeof described.value === "string" ? described.value : undefined;
180
+ }
181
+ case "array":
182
+ case "null":
183
+ case "primitive":
184
+ case "uninspectable":
185
+ return undefined;
186
+ default: {
187
+ const unhandled = shape;
188
+ throw new Error(`sourceOf: unhandled shape ${String(unhandled)}`);
189
+ }
190
+ }
135
191
  }
136
- /** 进判决的那几层(入参序):`layerAdmission` 答 `admitted` 的层。每一层都问 —— 闭集外的源 / 工作区读数在这里响亮抛,哪怕那一层没写名单。 */
137
- function admittedLayers(layers, workspace) {
138
- return layers.filter((l) => layerAdmission(l.source, workspace, "httpHookAllowlists") === "admitted");
192
+ /**
193
+ * 进判决的那几层(入参序)与它们对 `key` 那张名单的读数(`list` 缺 = 这一层不设)。**判官经读器读每一层**,与读器同一只读法,没有「是不是读器交出的」判:
194
+ * `layers` 数组见 {@link bothObservations}(不是朴素数组 / 读不出 / 两份观测对不上 ⇒ 整个当一张坏形层);每个元素先读源({@link sourceOf})——
195
+ * 读不出 ⇒ **坏形层**:名单按 `[]`,不问准入(它没有可信的源),进判决、全拦(判决词 `malformed`);
196
+ * 读得出 ⇒ 问 `layerAdmission`(闭集外的源 / 工作区读数在这里响亮抛 RangeError,哪怕那一层没写名单),准入的才进判决,名单与「读不出」= 读器读这个元素的读数({@link allowlistsOf},两键、双读一致律原样;读不出 ⇒ 判决词 `malformed`、点名这一层)。
197
+ * 于是读器交出的层、它的副本、手造的寻常层、本 realm 的 `JSON.parse` 出来的层同一份内容同一个答案(另一个 realm 的数组按坏形);读器对自己交出的 `layer` 再读,名单逐位同(`malformed` 只在第一次读出现)。
198
+ */
199
+ function admittedLists(layers, workspace, key) {
200
+ const elements = bothObservations(layers, isAny);
201
+ if (elements === undefined)
202
+ return [MALFORMED_LAYER];
203
+ return elements.flatMap((element) => {
204
+ const source = sourceOf(element);
205
+ if (source === undefined)
206
+ return [MALFORMED_LAYER];
207
+ // 闭集外的词:`layerAdmission` 经源表那一只判官抛 RangeError(与 14.6.0 同句同类)—— 判官不另写闭集表
208
+ if (layerAdmission(source, workspace, "httpHookAllowlists") !== "admitted")
209
+ return [];
210
+ const captured = allowlistsOf(element, HTTP_HOOK_ALLOWLIST_KEYS)[key];
211
+ return [{ source: source, list: captured?.list, malformed: captured?.malformed === true }];
212
+ });
139
213
  }
140
214
  /**
141
215
  * 一只 http hook 的 URL 过不过名单:每一张准入的 `allowedHttpHookUrls` 都要有一条模式按放行出口({@link urlAllowPatternMatches})命中它。
142
- * 没有准入层写这张名单 ⇒ 不限(`undefined`)。`workspace` = 宿主对工作区信任的读数(读口抛或闭集外按 `declined` 交),与 `layerAdmission` 同一个入参。
216
+ * 没有准入层写这张名单 ⇒ 不限(`undefined`)。`layers` 的每一层经读器读({@link readHttpHookAllowlistLayer} 同一只读法;读不出源 ⇒ 坏形层,全拦、`rule: "malformed"`)。
217
+ * `workspace` = 宿主对工作区信任的读数(读口抛或闭集外按 `declined` 交),与 `layerAdmission` 同一个入参。
143
218
  */
144
219
  export function httpHookUrlVerdict(url, layers, workspace) {
145
- const refusing = admittedLayers(layers, workspace).flatMap((l) => {
146
- const list = listOf(l, "allowedHttpHookUrls");
147
- return list !== undefined && !list.some((p) => urlAllowPatternMatches(url, p)) ? [{ source: l.source, empty: list.length === 0 }] : [];
148
- });
220
+ const refusing = admittedLists(layers, workspace, "allowedHttpHookUrls").flatMap(({ source, list, malformed }) => list !== undefined && !list.some((p) => urlAllowPatternMatches(url, p)) ? [{ source, malformed, empty: list.length === 0 }] : []);
149
221
  if (refusing.length === 0)
150
222
  return undefined;
151
- return { list: "allowedHttpHookUrls", rule: refusing.some((r) => r.empty) ? "empty" : "url", sources: refusing.map((r) => r.source) };
223
+ const sources = refusing.flatMap(({ source }) => (source === undefined ? [] : [source]));
224
+ const rule = refusing.some((r) => r.malformed) ? "malformed" : refusing.some((r) => r.empty) ? "empty" : "url";
225
+ return { list: "allowedHttpHookUrls", rule, sources };
152
226
  }
153
- /** 一只 http hook 实际可插值进请求头的环境变量:它自己的 `allowedEnvVars` 与每一张准入的 `httpHookAllowedEnvVars` 取交(保持 hook 自己的序)。 */
227
+ /**
228
+ * 一只 http hook 实际可插值进请求头的环境变量:它自己的 `allowedEnvVars` 与每一张准入的 `httpHookAllowedEnvVars` 取交(保持 hook 自己的序)。
229
+ * `layers` 同 {@link httpHookUrlVerdict}(坏形层 ⇒ 一个都不放);`hookVars` 只许普通的串数组({@link plainArrayOf};不是 ⇒ 当空,一个都不放)。
230
+ */
154
231
  export function httpHookEnvVarsAllowed(hookVars, layers, workspace) {
155
- const lists = admittedLayers(layers, workspace)
156
- .map((l) => listOf(l, "httpHookAllowedEnvVars"))
232
+ const lists = admittedLists(layers, workspace, "httpHookAllowedEnvVars")
233
+ .map(({ list }) => list)
157
234
  .filter((list) => list !== undefined);
158
- return hookVars.filter((v) => lists.every((list) => list.includes(v)));
235
+ const vars = bothObservations(hookVars, isStringEntry) ?? []; // 只许朴素的串数组,且两份观测一致才算读到;否则一个都不放
236
+ return vars.filter((v) => lists.every((list) => list.includes(v)));
159
237
  }
package/dist/index.d.ts CHANGED
@@ -7,6 +7,9 @@ export * from "./execution-lanes.js";
7
7
  export * from "./url-shape.js";
8
8
  export * from "./url-credential.js";
9
9
  export * from "./default-model.js";
10
+ export { catalogManagesModelPlane, enabledEntryCount } from "./catalog-manages.js";
11
+ export { DEFAULT_MODEL_KEY, ENV_MODEL_TABLE_KEYS, buildEnvModelBaseline, envModelTableDeclared, envModelTableWriteRefusal, mintedCheapId, planEnvModelTableWrite, readEnvModelSlots, type EnvCheapSlot, type EnvModelBaseline, type EnvModelTableEntry, type EnvModelTableKey, type EnvModelTableLanding, type EnvModelTableReceipt, type EnvModelTableRoleRow, type EnvModelTableTarget, type EnvModelTableWriteInput, type EnvModelTableWriteIntent, type EnvModelTableWritePlan, type EnvRoleBinding, type EnvSnapshot, } from "./env-model-table.js";
12
+ export { CONFIG_ROOT_DOCUMENTS, ENV_MODELS_DECISIONS, ENV_MODELS_DOCUMENT, ROOT_DOCUMENT_UNREADABLE_REASONS, envModelsDecisionTag, type ConfigRootDocument, type EnvModelsDecision, type EnvModelsDecisionWord, type MarkedMigrationId, type MigrationMarker, type RootDocumentUnreadableReason } from "./config-root-documents.js";
10
13
  export * from "./loopback.js";
11
14
  export * from "./hash.js";
12
15
  export * from "./sha256.js";
@@ -29,6 +32,8 @@ export * from "./api/auth-bridge.js";
29
32
  export * from "./api/scopes.js";
30
33
  export * from "./api/pull-token.js";
31
34
  export * from "./api/feedback.js";
35
+ export * from "./api/credentials.js";
36
+ export * from "./sandbox-git.js";
32
37
  export * from "./web-admin-env.js";
33
38
  export * from "./web-admin-credential.js";
34
39
  export * from "./web-admin-session-url.js";
@@ -40,3 +45,5 @@ export * from "./mcp-server-policy.js";
40
45
  export * from "./ui-language.js";
41
46
  export * from "./control-and-bidi.js";
42
47
  export * from "./mcp-request-entry.js";
48
+ export { REMOTE_MCP_SERVER_NAME_PREFIX } from "./mcp-server-name.js";
49
+ export * from "./center-refs.js";
package/dist/index.js CHANGED
@@ -7,6 +7,9 @@ export * from "./execution-lanes.js"; // 7.0.0 M-R2: the ONE lane / store vocabu
7
7
  export * from "./url-shape.js"; // 11.0.0: THE URL shape judge (httpUrlFlaws / httpUrlRule / isHierarchicalUrl), pure/browser-safe
8
8
  export * from "./url-credential.js"; // 14.5.0: THE "does this URL carry a credential" judge (findUrlCredential / findUrlUserinfo / URL_CREDENTIAL_REASONS / CREDENTIAL_PARAM_NAMES …), pure/browser-safe
9
9
  export * from "./default-model.js"; // 11.0.0: THE catalog-default resolver (roles.default → active tier ladder → fallback), pure/browser-safe
10
+ export { catalogManagesModelPlane, enabledEntryCount } from "./catalog-manages.js"; // THE "does this models document manage the model plane" judge (raw half: at least one entry not explicitly disabled) + the log count it is derived from, pure/browser-safe
11
+ export { DEFAULT_MODEL_KEY, ENV_MODEL_TABLE_KEYS, buildEnvModelBaseline, envModelTableDeclared, envModelTableWriteRefusal, mintedCheapId, planEnvModelTableWrite, readEnvModelSlots, } from "./env-model-table.js"; // the env model table (MODEL_ID / MODEL_CHEAP_ID) read once: readEnvModelSlots / envModelTableDeclared / buildEnvModelBaseline + the writer's plan planEnvModelTableWrite (receipt by id, confirmKey) + envModelTableWriteRefusal, pure/browser-safe (named: the env snapshot helper stays package-private)
12
+ export { CONFIG_ROOT_DOCUMENTS, ENV_MODELS_DECISIONS, ENV_MODELS_DOCUMENT, ROOT_DOCUMENT_UNREADABLE_REASONS, envModelsDecisionTag } from "./config-root-documents.js"; // the config-root documents (config.env-models.json + the per-id migration marker config.migration.<id>.json): names + closed answer tables + the one tagged identity of a decision reading (envModelsDecisionTag); the readers / writer are in ./node
10
13
  export * from "./loopback.js"; // 11.0.0: the two「本机」predicates (auth relaxation — narrow / connection routing — wide), pure/browser-safe
11
14
  export * from "./hash.js";
12
15
  export * from "./sha256.js";
@@ -29,6 +32,8 @@ export * from "./api/auth-bridge.js"; // 鉴权桥 contract: worker verifies web
29
32
  export * from "./api/scopes.js"; // multi-tenant scope contract: /api/v1/scopes* + /api/v1/auth/scope wire shapes + THE minting/switch decision functions (resolveTokenScope / canSwitchScope / effectiveScopeRole / readTokenScope) — web-admin imports these, never re-implements
30
33
  export * from "./api/pull-token.js"; // scope-bound token FORM + minting rule (one rule, token families are data: WORKER_PULL_TOKEN) + claimed-binding reader + verifier (re-mint + constant-time compare), pure-JS HMAC-SHA256, browser-safe
31
34
  export * from "./api/feedback.js"; // /bug report wire contract: FeedbackSubmitRequest (POST body) + FeedbackWire / FeedbackListResponse + the two caps, pure/browser-safe
35
+ export * from "./api/credentials.js"; // 14.7.0: the credential port's `sandbox_git` wire (barrel only, no subpath): SandboxGitCredentialRequest / CredentialHandleAnswer / CredentialResolveRequest / SandboxGitCredentialAnswer (per-origin SandboxGitOriginAnswer) / SandboxGitCredentialRefusal + the three word tables (SANDBOX_GIT_ORIGIN_VERDICTS → _STATES / _REASONS, SANDBOX_GIT_REFUSALS → _ERRORS, SANDBOX_GIT_PERMANENCE), pure/browser-safe
36
+ export * from "./sandbox-git.js"; // 14.7.0: the sandbox git plane's origin form (isSandboxGitOrigin — a string equal to its own WHATWG origin) + the `sandboxGit` effective-config block (SandboxGitConfig: origins ≤ 16, each { origin, connectUrl }), pure/browser-safe
32
37
  export * from "./web-admin-env.js"; // WEB_ADMIN_ENV — every env name an engine reads to reach web-admin (one table; a leaf module), pure/browser-safe
33
38
  export * from "./web-admin-credential.js"; // S-698 web-admin credential FILE contract: WebAdminCredentialEnvelope (user-session envelope, strict) + parseWebAdminCredentialFile (bare / envelope / unreadable — the one discrimination law) + judgeWebAdminCredentialDeclaration(env, credential) (pick-exactly-one over the credential's WEB_ADMIN_ENV pair; WEB_ADMIN_CREDENTIALS = access | assertion), pure/browser-safe
34
39
  export * from "./web-admin-session-url.js"; // webAdminSessionUrl(webBaseUrl, { worker, sessionId }) — THE web-admin session link template (<webBaseUrl>/chat?agent=<worker>&session=<sessionId>) + parseWebAdminSessionUrl(link) its paired reader, pure/browser-safe
@@ -40,6 +45,8 @@ export * from "./mcp-server-policy.js"; // 13.0.0 (U6): mcpServerPolicyVerdict
40
45
  export * from "./ui-language.js"; // 13.0.0 (S-1592): the UI-language settings fragment (semaUiLanguage, en | zh, category ui, user settings) + readUiLanguage, pure/browser-safe
41
46
  export * from "./control-and-bidi.js"; // the one-line echo strip set (C0 + DEL + the 12 Bidi_Control code points) — CONTROL_AND_BIDI_CHARS (the class, as regex source text) + stripControlAndBidi / hasControlOrBidi, pure/browser-safe
42
47
  export * from "./mcp-request-entry.js"; // 14.0.0: the request-plane MCP entry judges (isUsableWireName / isContentOriginWord / readMcpIdentityHeaders / readRequestMcpTransport + the value-shape describers) — one reading for the service legs and the shell pre-check, pure/browser-safe
48
+ export { REMOTE_MCP_SERVER_NAME_PREFIX } from "./mcp-server-name.js"; // the MCP server-name prefix reserved for the remote MCP servers a device executor offers (mount name = prefix + the executor's server name); the predicate and its sentence stay package-private — the request plane's MCP name judges (isUsableMcpServerName / describeBadMcpServerName) read them; the config plane's McpServerSpec.name accepts the prefix (refusing it would be a validation tightening — a major), pure/browser-safe
49
+ export * from "./center-refs.js"; // resolveCenterRefs(refs, lookup) — THE center-roster reference resolver: one verdict word per member (missing_name / not_a_string / read_threw, CENTER_REF_FAULTS), one whole verdict (resolved / unresolved / refs_unreadable, CENTER_REFS_VERDICTS); the refs record is read by the public readPlainStringRecord, the lookup is the caller's, it reads no process.env; pure/browser-safe
43
50
  // NOTE: the Node-fs-backed helpers (FileConfigStore + the file editor) are NOT in this barrel — they pull
44
51
  // `node:fs`/`node:path` and would break a browser/client bundle that only wants the contract (e.g.
45
52
  // web-admin's client components import the zod types through here). Import them from the Node-only