@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
@@ -92,8 +92,8 @@ export declare const MIN_TRUST_WORDS: readonly ["local", "session", "project", "
92
92
  export declare const MinTrust: z.ZodEnum<{
93
93
  local: "local";
94
94
  session: "session";
95
- project: "project";
96
95
  global: "global";
96
+ project: "project";
97
97
  managed: "managed";
98
98
  task: "task";
99
99
  }>;
@@ -151,8 +151,8 @@ export declare const MemberAdmission: z.ZodObject<{
151
151
  minTrust: z.ZodOptional<z.ZodEnum<{
152
152
  local: "local";
153
153
  session: "session";
154
- project: "project";
155
154
  global: "global";
155
+ project: "project";
156
156
  managed: "managed";
157
157
  task: "task";
158
158
  }>>;
@@ -161,8 +161,8 @@ export declare const MemberAdmission: z.ZodObject<{
161
161
  minTrust: z.ZodEnum<{
162
162
  local: "local";
163
163
  session: "session";
164
- project: "project";
165
164
  global: "global";
165
+ project: "project";
166
166
  managed: "managed";
167
167
  task: "task";
168
168
  }>;
@@ -264,8 +264,8 @@ export declare const MergeCategorySpec: z.ZodObject<{
264
264
  minTrust: z.ZodEnum<{
265
265
  local: "local";
266
266
  session: "session";
267
- project: "project";
268
267
  global: "global";
268
+ project: "project";
269
269
  managed: "managed";
270
270
  task: "task";
271
271
  }>;
@@ -314,8 +314,8 @@ export declare const MergeCategorySpec: z.ZodObject<{
314
314
  minTrust: z.ZodOptional<z.ZodEnum<{
315
315
  local: "local";
316
316
  session: "session";
317
- project: "project";
318
317
  global: "global";
318
+ project: "project";
319
319
  managed: "managed";
320
320
  task: "task";
321
321
  }>>;
@@ -324,8 +324,8 @@ export declare const MergeCategorySpec: z.ZodObject<{
324
324
  minTrust: z.ZodEnum<{
325
325
  local: "local";
326
326
  session: "session";
327
- project: "project";
328
327
  global: "global";
328
+ project: "project";
329
329
  managed: "managed";
330
330
  task: "task";
331
331
  }>;
@@ -66,8 +66,8 @@
66
66
  */
67
67
  import { z } from "zod";
68
68
  import { MAX_HASH_DEPTH, stableStringify } from "./hash.js";
69
- import { captureStringArray, dataDescriptor } from "./definition-copy.js";
70
- import { isJsonArray, isPlainJsonObject } from "./plain-json-object.js";
69
+ import { captureStringArray, dataDescriptor, isAccessorDescriptor, isolatedObject } from "./own-key-read.js";
70
+ import { jsonShapeOf } from "./plain-json-object.js";
71
71
  /** How values from different layers/principals combine for a category. */
72
72
  export const MergeShape = z.enum(["denyFirst", "replace", "runAll", "overrideByName", "concat"]);
73
73
  /** The trust-tier VOCABULARY as a RUNTIME VALUE — the ONE source of the ladder's words, in the same form as
@@ -183,10 +183,11 @@ function orderVerdict(diff) {
183
183
  return diff > 0 ? "stricter" : diff < 0 ? "looser" : "equal";
184
184
  }
185
185
  /** A string list as a set (order and duplicates do not count); `undefined` when the value is not an array of strings
186
- * — a sparse array, an accessor that throws, a proxy that cannot be read (revoked, a trap that throws) is not one: all through the package's one string-array
187
- * reading ({@link captureStringArray}), so the comparator never throws on them and never splits a string. */
186
+ * — a sparse array, an accessor or non-enumerable element (the verdict is about the caller's own value: an accessor is not a stored value, it may answer
187
+ * differently on the next read — the `none` rule's domain says the same; no getter is called), a proxy that cannot be read (revoked, a trap that throws) is not one:
188
+ * all through the package's one string-array reading, strict ({@link captureStringArray}), so the comparator never throws on them and never splits a string. */
188
189
  function stringSetOf(v) {
189
- const list = captureStringArray(v);
190
+ const list = captureStringArray(v, "strict");
190
191
  return list === undefined ? undefined : new Set(list);
191
192
  }
192
193
  /** `superset ⇒ stricter` for setUnion, the mirror for setNarrower. */
@@ -213,45 +214,57 @@ const NOT_STRICT_JSON = { ok: false };
213
214
  function strictJsonCopy(v, depth = 0) {
214
215
  if (depth > MAX_HASH_DEPTH)
215
216
  return NOT_STRICT_JSON;
216
- if (v === null || typeof v === "string" || typeof v === "boolean")
217
- return { ok: true, value: v };
218
- if (typeof v === "number")
219
- return Number.isFinite(v) ? { ok: true, value: v } : NOT_STRICT_JSON;
220
- if (typeof v !== "object")
221
- return NOT_STRICT_JSON; // undefined / function / symbol / bigint
222
- if (isJsonArray(v)) {
223
- const length = v.length;
224
- if (typeof length !== "number" || !Number.isSafeInteger(length) || length < 0)
225
- return NOT_STRICT_JSON;
226
- const out = [];
227
- for (let i = 0; i < length; i++) {
228
- const member = strictJsonStored(Object.getOwnPropertyDescriptor(v, i), depth);
229
- if (!member.ok)
217
+ const shape = jsonShapeOf(v); // the shape is asked ONCE, of the seat (total): no `typeof` split, no second prototype read
218
+ switch (shape) {
219
+ case "null":
220
+ return { ok: true, value: v };
221
+ case "primitive":
222
+ if (typeof v === "string" || typeof v === "boolean")
223
+ return { ok: true, value: v };
224
+ return typeof v === "number" && Number.isFinite(v) ? { ok: true, value: v } : NOT_STRICT_JSON; // undefined / symbol / bigint / NaN / ±Infinity
225
+ case "array": {
226
+ const array = v;
227
+ const length = array.length;
228
+ if (typeof length !== "number" || !Number.isSafeInteger(length) || length < 0)
230
229
  return NOT_STRICT_JSON;
231
- Object.defineProperty(out, i, dataDescriptor(member.value));
230
+ const out = [];
231
+ for (let i = 0; i < length; i++) {
232
+ const member = strictJsonStored(Object.getOwnPropertyDescriptor(array, i), depth);
233
+ if (!member.ok)
234
+ return NOT_STRICT_JSON;
235
+ Object.defineProperty(out, i, dataDescriptor(member.value));
236
+ }
237
+ return { ok: true, value: out };
232
238
  }
233
- return { ok: true, value: out };
234
- }
235
- if (!isPlainJsonObject(v))
236
- return NOT_STRICT_JSON; // Date / Map / class instance
237
- const out = Object.create(null);
238
- for (const key of Reflect.ownKeys(v)) {
239
- if (typeof key === "symbol")
240
- continue; // never serialized
241
- const desc = Object.getOwnPropertyDescriptor(v, key);
242
- if (desc !== undefined && desc.enumerable !== true)
243
- continue; // never serialized (what `Object.keys` leaves out)
244
- const member = strictJsonStored(desc, depth);
245
- if (!member.ok)
239
+ case "plain": {
240
+ const record = v;
241
+ const out = isolatedObject();
242
+ for (const key of Reflect.ownKeys(record)) {
243
+ if (typeof key === "symbol")
244
+ continue; // never serialized
245
+ const desc = Object.getOwnPropertyDescriptor(record, key);
246
+ if (desc !== undefined && desc.enumerable !== true)
247
+ continue; // never serialized (what `Object.keys` leaves out)
248
+ const member = strictJsonStored(desc, depth);
249
+ if (!member.ok)
250
+ return NOT_STRICT_JSON;
251
+ Object.defineProperty(out, key, dataDescriptor(member.value));
252
+ }
253
+ return { ok: true, value: out };
254
+ }
255
+ case "non_plain_object": // function / Date / Map / class instance
256
+ case "uninspectable": // a proxy that cannot be inspected: outside the domain
246
257
  return NOT_STRICT_JSON;
247
- Object.defineProperty(out, key, dataDescriptor(member.value));
258
+ default: {
259
+ const unhandled = shape;
260
+ throw new Error(`strictJsonCopy: unhandled shape ${String(unhandled)}`);
261
+ }
248
262
  }
249
- return { ok: true, value: out };
250
263
  }
251
264
  /** One member, judged by the descriptor read ONCE: a stored value (a hole has no descriptor; an accessor has no `value`) that is itself strict JSON. */
252
265
  function strictJsonStored(desc, depth) {
253
- if (desc === undefined || !Object.hasOwn(desc, "value"))
254
- return NOT_STRICT_JSON; // 描述符本身也只读自有键:`Object.prototype.value` 被污染时访问器描述符不会「有 value」
266
+ if (desc === undefined || isAccessorDescriptor(desc))
267
+ return NOT_STRICT_JSON; // 访问器不是存着的值(描述符的字段只认自有的,见内核)
255
268
  return strictJsonCopy(desc.value, depth + 1);
256
269
  }
257
270
  /** `none`: compares the DESCRIPTOR VIEW, read once ({@link strictJsonCopy}), and serializes only the copies — member values never go through `[[Get]]`. The ONE guard around the
@@ -0,0 +1,64 @@
1
+ /**
2
+ * 沙箱 git 平面的 **origin 形** 与 **有效配置块 `sandboxGit`**(14.7.0)。纯、浏览器安全。
3
+ *
4
+ * ## 律(一条)
5
+ * **一个 origin 就是它自己的 WHATWG origin**:串等于 `new URL(串).origin`,且 scheme ∈ {http, https}。规范形 ——
6
+ * 小写 host、去缺省端口(https 的 443、http 的 80)、无尾斜杠、无 userinfo / 路径 / 查询 / 片段 —— 不是一张手写规则表,而是这一个等式的推论:
7
+ * 凡 WHATWG 会改写的串(大写、缺省端口、IPv4 简写、百分号编码的 host、串内制表符、前后空白、反斜杠……)都不等于自己的 origin,所以都不是规范形。
8
+ * 判官只有 {@link isSandboxGitOrigin} 一只;请求体的 `origins`、应答行的 `origin`、有效配置块的 `origin` 全读它(经 {@link SandboxGitOrigin})。
9
+ * 「这个串是不是 http(s) URL」归包内唯一的 URL 形判官 `httpUrlFlaws`(`./url-shape`),本模块只加「等于自己的 origin」这一格。
10
+ * origin 不表达子路径部署(`https://host/gitea` 不是 origin)—— 规范化器不存在:把带路径的串静默削成 origin 会把凭据送到运维没写的地方。
11
+ *
12
+ * ## 有效配置块 `sandboxGit`
13
+ * `/effective` 载荷上的**非域顶层键**(web-admin 按部署登记册投影,不是存储域):可选、无默认 —— 缺席 = 这个部署没有投影这一块。
14
+ * `readEffectiveWire` 对它走 open-world 逐字承运(从不校验、从不丢,`EffectiveWire.sandboxGit` 是 `unknown`);**判它的座位只有 {@link SandboxGitConfig}**,由消费方在用之前判。
15
+ *
16
+ * · `origins[i].origin` —— 本部署代理哪些 origin(规范形,见上);≤ {@link SANDBOX_GIT_MAX_ORIGINS} 项、两两不同。
17
+ * · `origins[i].connectUrl` —— 控制台「连接 Gitea(git)」页的绝对 https URL(非秘密:{@link SandboxGitConnectUrl})。
18
+ *
19
+ * 🔴 **块里 http origin 可读**:它为什么送不出凭据(传输不加密)是逐 origin 应答里的一个词(`insecure_transport`,`./api/credentials` 的判定序表第 1 行)
20
+ * 与能力读数里的一个词,不是块的校验拒。块若因一个 http origin 整块不可读,同块里的 https origin 一并失效,「逐 origin 说拒因」就无从谈起;
21
+ * http origin 只配路由、永不取值,是消费方的事。
22
+ *
23
+ * 解析剥未知键(与 `SessionMirrorPolicy` 同律:加字段恒为加性)。
24
+ */
25
+ import { z } from "zod";
26
+ /** 一次最多代理的 origin 数(请求体、应答、有效配置块同一个上界)。 */
27
+ export declare const SANDBOX_GIT_MAX_ORIGINS = 16;
28
+ /**
29
+ * 判:`raw` 是不是规范形的 http(s) origin。合格 ⇔ 它是层级式 http(s) URL 且**逐字等于**自己的 WHATWG origin。
30
+ * 纯函数、不抛;非串由 {@link SandboxGitOrigin} 的 zod 形先拒。
31
+ */
32
+ export declare function isSandboxGitOrigin(raw: string): boolean;
33
+ /** 一个规范形的 origin(`scheme://host[:port]`,scheme ∈ {http, https})。判官 {@link isSandboxGitOrigin};拒句是定句,不回显值。 */
34
+ export declare const SandboxGitOrigin: z.ZodString;
35
+ export type SandboxGitOrigin = z.infer<typeof SandboxGitOrigin>;
36
+ /**
37
+ * 一张 origin 表:每项是规范形、两两不同、至多 {@link SANDBOX_GIT_MAX_ORIGINS} 项。**空表合法**(有效配置块可以没有 origin);要求至少一项的座位(申领请求)自己 `.min(1)`。
38
+ * 重复的那一项被点名(坐标 = 第二次出现的下标)。
39
+ */
40
+ export declare const SandboxGitOriginList: z.ZodArray<z.ZodString>;
41
+ export type SandboxGitOriginList = z.infer<typeof SandboxGitOriginList>;
42
+ /**
43
+ * 控制台「连接 Gitea(git)」页的绝对 URL:**https**,且是 {@link NON_SECRET_URL_NO_MIGRATION} 那一类(层级式 URL、无 userinfo、无片段、路径与查询里没有秘密形的值)。
44
+ * 它是 wire 输入,不是存量文档里的值,所以走「无迁移指路」那一只。
45
+ */
46
+ export declare const SandboxGitConnectUrl: z.ZodString;
47
+ export type SandboxGitConnectUrl = z.infer<typeof SandboxGitConnectUrl>;
48
+ /** 有效配置块里的一个 origin 条目。 */
49
+ export declare const SandboxGitOriginEntry: z.ZodObject<{
50
+ origin: z.ZodString;
51
+ connectUrl: z.ZodString;
52
+ }, z.core.$strip>;
53
+ export type SandboxGitOriginEntry = z.infer<typeof SandboxGitOriginEntry>;
54
+ /**
55
+ * 有效配置块 `sandboxGit`(`/effective` 载荷的非域顶层键,可选、无默认)。判它的**唯一座位**:消费方在用之前对 `wire.sandboxGit` 跑 `safeParse`。
56
+ * `origins` ≤ {@link SANDBOX_GIT_MAX_ORIGINS} 项,按 `origin` 两两不同(重复的那一项的 `origin` 字段被点名);空表合法。
57
+ */
58
+ export declare const SandboxGitConfig: z.ZodObject<{
59
+ origins: z.ZodArray<z.ZodObject<{
60
+ origin: z.ZodString;
61
+ connectUrl: z.ZodString;
62
+ }, z.core.$strip>>;
63
+ }, z.core.$strip>;
64
+ export type SandboxGitConfig = z.infer<typeof SandboxGitConfig>;
@@ -0,0 +1,89 @@
1
+ /**
2
+ * 沙箱 git 平面的 **origin 形** 与 **有效配置块 `sandboxGit`**(14.7.0)。纯、浏览器安全。
3
+ *
4
+ * ## 律(一条)
5
+ * **一个 origin 就是它自己的 WHATWG origin**:串等于 `new URL(串).origin`,且 scheme ∈ {http, https}。规范形 ——
6
+ * 小写 host、去缺省端口(https 的 443、http 的 80)、无尾斜杠、无 userinfo / 路径 / 查询 / 片段 —— 不是一张手写规则表,而是这一个等式的推论:
7
+ * 凡 WHATWG 会改写的串(大写、缺省端口、IPv4 简写、百分号编码的 host、串内制表符、前后空白、反斜杠……)都不等于自己的 origin,所以都不是规范形。
8
+ * 判官只有 {@link isSandboxGitOrigin} 一只;请求体的 `origins`、应答行的 `origin`、有效配置块的 `origin` 全读它(经 {@link SandboxGitOrigin})。
9
+ * 「这个串是不是 http(s) URL」归包内唯一的 URL 形判官 `httpUrlFlaws`(`./url-shape`),本模块只加「等于自己的 origin」这一格。
10
+ * origin 不表达子路径部署(`https://host/gitea` 不是 origin)—— 规范化器不存在:把带路径的串静默削成 origin 会把凭据送到运维没写的地方。
11
+ *
12
+ * ## 有效配置块 `sandboxGit`
13
+ * `/effective` 载荷上的**非域顶层键**(web-admin 按部署登记册投影,不是存储域):可选、无默认 —— 缺席 = 这个部署没有投影这一块。
14
+ * `readEffectiveWire` 对它走 open-world 逐字承运(从不校验、从不丢,`EffectiveWire.sandboxGit` 是 `unknown`);**判它的座位只有 {@link SandboxGitConfig}**,由消费方在用之前判。
15
+ *
16
+ * · `origins[i].origin` —— 本部署代理哪些 origin(规范形,见上);≤ {@link SANDBOX_GIT_MAX_ORIGINS} 项、两两不同。
17
+ * · `origins[i].connectUrl` —— 控制台「连接 Gitea(git)」页的绝对 https URL(非秘密:{@link SandboxGitConnectUrl})。
18
+ *
19
+ * 🔴 **块里 http origin 可读**:它为什么送不出凭据(传输不加密)是逐 origin 应答里的一个词(`insecure_transport`,`./api/credentials` 的判定序表第 1 行)
20
+ * 与能力读数里的一个词,不是块的校验拒。块若因一个 http origin 整块不可读,同块里的 https origin 一并失效,「逐 origin 说拒因」就无从谈起;
21
+ * http origin 只配路由、永不取值,是消费方的事。
22
+ *
23
+ * 解析剥未知键(与 `SessionMirrorPolicy` 同律:加字段恒为加性)。
24
+ */
25
+ import { z } from "zod";
26
+ import { buildQuotedIssue } from "./quoted-issue.js";
27
+ import { NON_SECRET_URL_NO_MIGRATION } from "./types.js";
28
+ import { httpUrlFlaws } from "./url-shape.js";
29
+ /** 一次最多代理的 origin 数(请求体、应答、有效配置块同一个上界)。 */
30
+ export const SANDBOX_GIT_MAX_ORIGINS = 16;
31
+ /**
32
+ * 判:`raw` 是不是规范形的 http(s) origin。合格 ⇔ 它是层级式 http(s) URL 且**逐字等于**自己的 WHATWG origin。
33
+ * 纯函数、不抛;非串由 {@link SandboxGitOrigin} 的 zod 形先拒。
34
+ */
35
+ export function isSandboxGitOrigin(raw) {
36
+ if (httpUrlFlaws(raw, []).length > 0)
37
+ return false;
38
+ return new URL(raw).origin === raw;
39
+ }
40
+ /** 一个规范形的 origin(`scheme://host[:port]`,scheme ∈ {http, https})。判官 {@link isSandboxGitOrigin};拒句是定句,不回显值。 */
41
+ export const SandboxGitOrigin = z
42
+ .string()
43
+ .refine(isSandboxGitOrigin, buildQuotedIssue `must be a canonical http(s) origin — scheme://host[:port] equal to its own WHATWG origin: lowercase host, no default port, no userinfo, path, query, fragment or trailing slash`);
44
+ /** 给定下标的 origin 在它之前已经出现过 ⇒ 下标。重复判在**规范形**之上(两个都是规范形的串,相等即同一个 origin)。 */
45
+ function repeatedAt(origins) {
46
+ const seen = new Set();
47
+ const repeats = [];
48
+ origins.forEach((o, i) => {
49
+ if (seen.has(o))
50
+ repeats.push(i);
51
+ seen.add(o);
52
+ });
53
+ return repeats;
54
+ }
55
+ /**
56
+ * 一张 origin 表:每项是规范形、两两不同、至多 {@link SANDBOX_GIT_MAX_ORIGINS} 项。**空表合法**(有效配置块可以没有 origin);要求至少一项的座位(申领请求)自己 `.min(1)`。
57
+ * 重复的那一项被点名(坐标 = 第二次出现的下标)。
58
+ */
59
+ export const SandboxGitOriginList = z
60
+ .array(SandboxGitOrigin)
61
+ .max(SANDBOX_GIT_MAX_ORIGINS)
62
+ .superRefine((origins, ctx) => {
63
+ for (const i of repeatedAt(origins))
64
+ ctx.addIssue({ code: "custom", ...buildQuotedIssue `duplicate origin`, path: [i] });
65
+ });
66
+ /**
67
+ * 控制台「连接 Gitea(git)」页的绝对 URL:**https**,且是 {@link NON_SECRET_URL_NO_MIGRATION} 那一类(层级式 URL、无 userinfo、无片段、路径与查询里没有秘密形的值)。
68
+ * 它是 wire 输入,不是存量文档里的值,所以走「无迁移指路」那一只。
69
+ */
70
+ export const SandboxGitConnectUrl = NON_SECRET_URL_NO_MIGRATION.refine((u) => URL.canParse(u) && new URL(u).protocol === "https:", buildQuotedIssue `must be an https URL — the connect page is opened by a person and carries their session`);
71
+ /** 有效配置块里的一个 origin 条目。 */
72
+ export const SandboxGitOriginEntry = z.object({
73
+ /** 本部署代理的 origin(规范形)。http 在这里可读(见模块头注)。 */
74
+ origin: SandboxGitOrigin,
75
+ /** 该 origin 的「连接」页,绝对 https URL。 */
76
+ connectUrl: SandboxGitConnectUrl,
77
+ });
78
+ /**
79
+ * 有效配置块 `sandboxGit`(`/effective` 载荷的非域顶层键,可选、无默认)。判它的**唯一座位**:消费方在用之前对 `wire.sandboxGit` 跑 `safeParse`。
80
+ * `origins` ≤ {@link SANDBOX_GIT_MAX_ORIGINS} 项,按 `origin` 两两不同(重复的那一项的 `origin` 字段被点名);空表合法。
81
+ */
82
+ export const SandboxGitConfig = z
83
+ .object({
84
+ origins: z.array(SandboxGitOriginEntry).max(SANDBOX_GIT_MAX_ORIGINS),
85
+ })
86
+ .superRefine((block, ctx) => {
87
+ for (const i of repeatedAt(block.origins.map((e) => e.origin)))
88
+ ctx.addIssue({ code: "custom", ...buildQuotedIssue `duplicate origin`, path: ["origins", i, "origin"] });
89
+ });
@@ -1,22 +1,30 @@
1
- import { isPlainJsonObject } from './plain-json-object.js';
1
+ import { ownDeclaredValueOf } from './own-key-read.js';
2
+ import { isJsonArray, isPlainJsonObject } from './plain-json-object.js';
2
3
  import { isSessionLifetime } from './scheduler-lifetime.js';
3
4
  import { migrateSchedulerRecords } from './scheduler-store-migration.js';
4
5
  /** Structural validation of one record (required fields + a well-formed `when`). Unknown fields are allowed. */
5
6
  export function isSchedulerRecord(v) {
6
7
  if (!isPlainJsonObject(v))
7
8
  return false;
8
- const r = v;
9
- if (typeof r.id !== 'string' || typeof r.scope !== 'string' || typeof r.prompt !== 'string' || typeof r.createdMs !== 'number')
9
+ // a type guard, so every field is read STRICTLY from the record's own keys (own, enumerable, data — the kernel's `ownDeclaredValueOf`): an inherited field is absent
10
+ // (a polluted `Object.prototype.prompt` is not a prompt the record carries); an accessor / hidden field, or one that cannot be read ⇒ not a record — no getter is called,
11
+ // so what the caller reads from the record next is what was judged
12
+ const field = (o, key) => {
13
+ const read = ownDeclaredValueOf(o, key);
14
+ return read.ok ? read.value : undefined;
15
+ };
16
+ if (typeof field(v, 'id') !== 'string' || typeof field(v, 'scope') !== 'string' || typeof field(v, 'prompt') !== 'string' || typeof field(v, 'createdMs') !== 'number')
10
17
  return false;
11
- const w = r.when;
18
+ const w = field(v, 'when');
12
19
  if (!isPlainJsonObject(w))
13
20
  return false;
14
- if (w.kind === 'cron')
15
- return typeof w.expr === 'string';
16
- if (w.kind === 'at')
17
- return typeof w.atMs === 'number';
18
- if (w.kind === 'delay')
19
- return typeof w.delaySec === 'number';
21
+ const kind = field(w, 'kind');
22
+ if (kind === 'cron')
23
+ return typeof field(w, 'expr') === 'string';
24
+ if (kind === 'at')
25
+ return typeof field(w, 'atMs') === 'number';
26
+ if (kind === 'delay')
27
+ return typeof field(w, 'delaySec') === 'number';
20
28
  return false;
21
29
  }
22
30
  /** 一条会话链上的记录过期多久算「永驻孤儿」:`lifetime: "session"` ∧ `sessionId` 在场 ∧ `nextRunMs` 已过去这么久 ⇒ 读口丢弃并告警(7 天)。 */
@@ -38,7 +46,7 @@ export function parseSchedulerStore(text, opts) {
38
46
  catch {
39
47
  return { records: [], warnings: [{ kind: 'unreadable-file', why: 'not-json' }] };
40
48
  }
41
- if (!Array.isArray(parsed))
49
+ if (!isJsonArray(parsed))
42
50
  return { records: [], warnings: [{ kind: 'unreadable-file', why: 'not-an-array' }] };
43
51
  const warnings = [];
44
52
  const kept = [];
@@ -42,6 +42,7 @@
42
42
  * the engine's env catalog, whose one implementation is the server's.
43
43
  */
44
44
  import { classifyHooksConfig } from "./hooks-config.js";
45
+ import { captureJson, captureStringArray, ownEntriesOf } from "./own-key-read.js";
45
46
  import { isPlainJsonObject } from "./plain-json-object.js";
46
47
  import { SAFETY_MERGE_SPEC, resolveMemberTighten, tightenVerdict } from "./safety-merge-spec.js";
47
48
  /** The settings-document units (see the file header). `path` is the unit's place in a settings document. */
@@ -98,20 +99,17 @@ const UNREADABLE = { ok: false, reason: "unreadable" };
98
99
  const LAUNCH_ONLY = { ok: false, reason: "launch_only" };
99
100
  /** `stringList`: an array whose every element is a non-empty string (a hole reads as `undefined`, so a sparse array is not one). */
100
101
  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;
102
+ const list = captureStringArray(v, "strict"); // the package's one string-array reading, strict: the value is handed back as is, so an accessor / hidden index ⇒ not a list (no getter is called)
103
+ return list !== undefined && list.every((x) => x.length > 0);
109
104
  }
110
- /** `envMap`: a plain object (prototype `Object.prototype` or `null`), non-empty keys, string values. */
105
+ /** `envMap`: a plain object (prototype `Object.prototype` or `null`), non-empty keys, string values. The value is handed back AS IS, so its members are read strictly —
106
+ * the package's one whole-container read, strict (`ownEntriesOf`: one `ownKeys`, each own string key's descriptor asked once — symbol keys never; never `[[Get]]`, never a
107
+ * proxy's `get` trap): an accessor member or a non-enumerable member ⇒ not an env map (no getter is called — what the caller reads next is what was judged); unreadable ⇒ not one. */
111
108
  function isEnvMap(v) {
112
109
  if (!isPlainJsonObject(v))
113
110
  return false;
114
- return Object.keys(v).every((k) => k.length > 0 && typeof v[k] === "string");
111
+ const members = ownEntriesOf(v);
112
+ return members.ok && members.value.every(([k, x]) => k.length > 0 && typeof x === "string");
115
113
  }
116
114
  /** What a declared member's value class adds on top of its comparator's domain — read through the comparator
117
115
  * {@link resolveMemberTighten} derived from the row, so the seat is interpreted in one place: `list` ⇒ setUnion / setNarrower,
@@ -144,8 +142,12 @@ function shapedReading(unit, row, rule, raw) {
144
142
  case "killSwitch":
145
143
  // CC spells ON "disable"; the boolean itself is the member comparator's domain
146
144
  return { ok: true, value: raw === "disable" ? true : raw };
147
- case "hooks":
148
- return classifyHooksConfig({ hooks: raw }).outcome === "loaded" ? { ok: true, value: raw } : UNREADABLE;
145
+ case "hooks": {
146
+ // the value is handed back AS IS, so it is read strictly (as every unit is): the whole tree is snapshotted by the kernel's strict whole-container read first —
147
+ // an accessor anywhere or a non-enumerable member ⇒ not readable (no getter is called) — and the classifier judges the snapshot, not the caller's object
148
+ const snapshot = captureJson(raw);
149
+ return snapshot.ok && classifyHooksConfig({ hooks: snapshot.value }).outcome === "loaded" ? { ok: true, value: raw } : UNREADABLE;
150
+ }
149
151
  case "envMap":
150
152
  return isEnvMap(raw) ? { ok: true, value: raw } : UNREADABLE;
151
153
  case "nonEmptyString":
@@ -0,0 +1,33 @@
1
+ /**
2
+ * 档位词(纯、零 I/O、浏览器安全;**包内叶子模块,不出包**):引擎的档序与 CC 档位别名、从某档起向低档的梯,与「一个目录词在档位展开之后落到哪条条目」。
3
+ * 本包里档位词的唯一一处:default 角色的档梯(`./default-model`)与写者计划助手的落点(`./env-model-table`)都从这里取。
4
+ * 镜像 core(`DEFAULT_TIER_ORDER` / `CC_MODEL_TIER_ALIASES` / `expandTiers`):`test/tier-words-parity-14.8.0` 对 devDependency 里精确钉住的那一版 core 对拍。
5
+ */
6
+ /** 引擎的档序(高 → 低)。 */
7
+ export declare const TIER_ORDER: readonly ["ultra", "max", "pro", "flash", "lite"];
8
+ /** 一个档位词。 */
9
+ export type TierWord = (typeof TIER_ORDER)[number];
10
+ /** CC 档位词 → 引擎档位。 */
11
+ export declare const CC_TIER_ALIASES: {
12
+ readonly fable: "max";
13
+ readonly mythos: "max";
14
+ readonly opus: "pro";
15
+ readonly sonnet: "flash";
16
+ readonly haiku: "lite";
17
+ };
18
+ /** 档序里从 `S` 起(含)向低档的那一段,字面元组型。 */
19
+ type LadderFrom<S extends TierWord, T extends readonly TierWord[] = typeof TIER_ORDER> = T extends readonly [infer H, ...infer R extends readonly TierWord[]] ? H extends S ? Readonly<T> : LadderFrom<S, R> : readonly [];
20
+ /** 一档的梯:档序里从它起(含)向低档的那一段(没绑的档沿它往下落)。 */
21
+ export declare function tierLadderFrom<S extends TierWord>(tier: S): LadderFrom<S>;
22
+ /**
23
+ * 一个目录词(条目名 / 档位词 / CC 档位别名 / `best`)在**档位展开之后**落到哪条目录条目名 —— 与引擎装配档位表的两步同答:
24
+ * 先摘掉活跃组里绑定目标不可服务的档(引擎的生效装配在交给 core 之前做这一步,并 warn 点名),再按 core `expandTiers` 展开
25
+ * (对拍 devDependency 那一版 core 的「摘后表」)。所以这里只有一条绑定规则:**一档的绑定只在它点名一条可服务条目时算数**,否则当作没绑。展开规则:
26
+ * · 目录自己的键先赢(`has(word)`;一条就叫 `flash` 的条目遮住档位词);
27
+ * · 档位词 ⇒ 它的梯({@link tierLadderFrom})上第一个有绑定的档;摘完没有任何绑定 ⇒ 只认目录键;
28
+ * · CC 档位别名 ⇒ 经它对应的档位;`best` ⇒ 档序里第一个有绑定的档;别的词 ⇒ 落不到。
29
+ * `has` = 调用方的「这个名是目录里一条可服务(启用)的条目」;`tiers` = `resolveActiveTiers` 的读数(原表,未摘)—— 它的键只有五个档位词
30
+ * (档位组的绑定表在 schema 里只收这五个键,别的键被剥掉),所以只按档位词读表。
31
+ */
32
+ export declare function landCatalogWord(word: string, has: (name: string) => boolean, tiers: Readonly<Partial<Record<TierWord, string>>> | undefined): string | undefined;
33
+ export {};
@@ -0,0 +1,43 @@
1
+ /**
2
+ * 档位词(纯、零 I/O、浏览器安全;**包内叶子模块,不出包**):引擎的档序与 CC 档位别名、从某档起向低档的梯,与「一个目录词在档位展开之后落到哪条条目」。
3
+ * 本包里档位词的唯一一处:default 角色的档梯(`./default-model`)与写者计划助手的落点(`./env-model-table`)都从这里取。
4
+ * 镜像 core(`DEFAULT_TIER_ORDER` / `CC_MODEL_TIER_ALIASES` / `expandTiers`):`test/tier-words-parity-14.8.0` 对 devDependency 里精确钉住的那一版 core 对拍。
5
+ */
6
+ /** 引擎的档序(高 → 低)。 */
7
+ export const TIER_ORDER = ["ultra", "max", "pro", "flash", "lite"];
8
+ /** CC 档位词 → 引擎档位。 */
9
+ export const CC_TIER_ALIASES = { fable: "max", mythos: "max", opus: "pro", sonnet: "flash", haiku: "lite" };
10
+ /** 一档的梯:档序里从它起(含)向低档的那一段(没绑的档沿它往下落)。 */
11
+ export function tierLadderFrom(tier) {
12
+ return TIER_ORDER.slice(TIER_ORDER.indexOf(tier));
13
+ }
14
+ const isTierWord = (word) => TIER_ORDER.includes(word);
15
+ /**
16
+ * 一个目录词(条目名 / 档位词 / CC 档位别名 / `best`)在**档位展开之后**落到哪条目录条目名 —— 与引擎装配档位表的两步同答:
17
+ * 先摘掉活跃组里绑定目标不可服务的档(引擎的生效装配在交给 core 之前做这一步,并 warn 点名),再按 core `expandTiers` 展开
18
+ * (对拍 devDependency 那一版 core 的「摘后表」)。所以这里只有一条绑定规则:**一档的绑定只在它点名一条可服务条目时算数**,否则当作没绑。展开规则:
19
+ * · 目录自己的键先赢(`has(word)`;一条就叫 `flash` 的条目遮住档位词);
20
+ * · 档位词 ⇒ 它的梯({@link tierLadderFrom})上第一个有绑定的档;摘完没有任何绑定 ⇒ 只认目录键;
21
+ * · CC 档位别名 ⇒ 经它对应的档位;`best` ⇒ 档序里第一个有绑定的档;别的词 ⇒ 落不到。
22
+ * `has` = 调用方的「这个名是目录里一条可服务(启用)的条目」;`tiers` = `resolveActiveTiers` 的读数(原表,未摘)—— 它的键只有五个档位词
23
+ * (档位组的绑定表在 schema 里只收这五个键,别的键被剥掉),所以只按档位词读表。
24
+ */
25
+ export function landCatalogWord(word, has, tiers) {
26
+ if (has(word))
27
+ return word;
28
+ if (tiers === undefined)
29
+ return undefined;
30
+ /** 一档的绑定:只在它点名一条可服务条目时算数(否则引擎已摘掉它)。 */
31
+ const bound = (tier) => {
32
+ const target = Object.hasOwn(tiers, tier) ? tiers[tier] : undefined;
33
+ return target !== undefined && has(target) ? target : undefined;
34
+ };
35
+ const firstBound = (ladder) => ladder.map(bound).find((landed) => landed !== undefined);
36
+ if (isTierWord(word))
37
+ return firstBound(tierLadderFrom(word));
38
+ if (Object.hasOwn(CC_TIER_ALIASES, word))
39
+ return firstBound(tierLadderFrom(CC_TIER_ALIASES[word]));
40
+ if (word === "best")
41
+ return firstBound(TIER_ORDER);
42
+ return undefined;
43
+ }
package/dist/types.d.ts CHANGED
@@ -1830,11 +1830,11 @@ export interface SkillsManifest {
1830
1830
  * legal hash scheme. */
1831
1831
  export declare const SkillsManifestSchema: z.ZodObject<{
1832
1832
  skills: z.ZodPrefault<z.ZodArray<z.ZodObject<{
1833
+ description: z.ZodDefault<z.ZodString>;
1833
1834
  name: z.ZodString;
1834
1835
  enabled: z.ZodDefault<z.ZodBoolean>;
1835
1836
  notes: z.ZodOptional<z.ZodString>;
1836
1837
  rosters: z.ZodPrefault<z.ZodArray<z.ZodString>>;
1837
- description: z.ZodDefault<z.ZodString>;
1838
1838
  scenarios: z.ZodPrefault<z.ZodArray<z.ZodString>>;
1839
1839
  allowedTools: z.ZodOptional<z.ZodArray<z.ZodString>>;
1840
1840
  disallowedTools: z.ZodOptional<z.ZodArray<z.ZodString>>;
@@ -1885,6 +1885,8 @@ export declare const McpServerSpec: z.ZodObject<{
1885
1885
  local: "local";
1886
1886
  execution: "execution";
1887
1887
  }>>;
1888
+ sessionMetaKey: z.ZodOptional<z.ZodUnknown>;
1889
+ startupTimeoutMs: z.ZodOptional<z.ZodUnknown>;
1888
1890
  source: z.ZodOptional<z.ZodUnknown>;
1889
1891
  toolFaces: z.ZodOptional<z.ZodUnknown>;
1890
1892
  toolAxes: z.ZodOptional<z.ZodUnknown>;
@@ -1918,6 +1920,8 @@ export declare const McpConfig: z.ZodObject<{
1918
1920
  local: "local";
1919
1921
  execution: "execution";
1920
1922
  }>>;
1923
+ sessionMetaKey: z.ZodOptional<z.ZodUnknown>;
1924
+ startupTimeoutMs: z.ZodOptional<z.ZodUnknown>;
1921
1925
  source: z.ZodOptional<z.ZodUnknown>;
1922
1926
  toolFaces: z.ZodOptional<z.ZodUnknown>;
1923
1927
  toolAxes: z.ZodOptional<z.ZodUnknown>;
@@ -2520,6 +2524,8 @@ export declare const DOMAIN_SCHEMAS: {
2520
2524
  local: "local";
2521
2525
  execution: "execution";
2522
2526
  }>>;
2527
+ sessionMetaKey: z.ZodOptional<z.ZodUnknown>;
2528
+ startupTimeoutMs: z.ZodOptional<z.ZodUnknown>;
2523
2529
  source: z.ZodOptional<z.ZodUnknown>;
2524
2530
  toolFaces: z.ZodOptional<z.ZodUnknown>;
2525
2531
  toolAxes: z.ZodOptional<z.ZodUnknown>;
@@ -3219,10 +3225,14 @@ export type EffectiveKey = z.infer<typeof EffectiveKey>;
3219
3225
  * `prompts` is the second wire divergence: on the wire it is {@link EffectivePromptsWire} (`{schemaVersion: 2, artifact}` — the one
3220
3226
  * artifact {@link promptArtifactFor} selected for the pulling worker, stamped with its publish provenance), carried verbatim by the
3221
3227
  * read leg and judged by the consumer (that shape, then the engine's verify) — optional (absent = nothing published, or the publish
3222
- * gate is closed), never the storage shape. */
3228
+ * gate is closed), never the storage shape.
3229
+ * `sandboxGit` (14.7.0) is the third wire-only key and the third carried verbatim: the sandbox git plane's origin block — web-admin projects it from the
3230
+ * deployment's registry, it is NOT a stored domain, so `buildEffective` never produces it and `readEffectiveWire` neither validates nor drops it. Optional with
3231
+ * no default (absent = the deployment projects no such block); the ONE seat that judges it is `SandboxGitConfig` (`./sandbox-git`), run by the consumer before use. */
3223
3232
  export type EffectiveWire = Omit<EffectiveConfig, "skills" | "prompts"> & {
3224
3233
  skills: SkillsManifest;
3225
3234
  prompts?: unknown;
3235
+ sandboxGit?: unknown;
3226
3236
  };
3227
3237
  /** Reserved audit "domain" for RBAC/user-management events (not a config domain → no snapshot, not
3228
3238
  * rollback-able). Lets user changes share the one audit stream the Audit page already renders. */