@sema-agent/settings-schema 14.6.0 → 14.7.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 (65) hide show
  1. package/CHANGELOG.md +266 -0
  2. package/README.md +31 -3
  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/config-d-file.js +33 -15
  16. package/dist/config-fns.js +142 -78
  17. package/dist/conformance/canonical.js +4 -4
  18. package/dist/conformance/contract-adapter.js +26 -1
  19. package/dist/conformance/corpus.js +4 -4
  20. package/dist/conformance/derive.js +2 -2
  21. package/dist/conformance/run.js +43 -6
  22. package/dist/cross-session.d.ts +4 -4
  23. package/dist/declared-capture.d.ts +41 -0
  24. package/dist/declared-capture.js +344 -0
  25. package/dist/file-edit.js +11 -15
  26. package/dist/fleet.d.ts +11 -0
  27. package/dist/fleet.js +59 -26
  28. package/dist/hash.d.ts +0 -9
  29. package/dist/hash.js +64 -7
  30. package/dist/hooks-config.js +18 -3
  31. package/dist/http-hook-allowlists.d.ts +22 -9
  32. package/dist/http-hook-allowlists.js +169 -91
  33. package/dist/index.d.ts +2 -0
  34. package/dist/index.js +2 -0
  35. package/dist/mcp-request-entry.d.ts +9 -7
  36. package/dist/mcp-request-entry.js +89 -82
  37. package/dist/one-time-migrations.js +207 -95
  38. package/dist/own-key-read.d.ts +247 -0
  39. package/dist/own-key-read.js +449 -0
  40. package/dist/plain-json-object.d.ts +11 -1
  41. package/dist/plain-json-object.js +37 -1
  42. package/dist/remote-exec.js +9 -1
  43. package/dist/safety-merge-spec.js +50 -37
  44. package/dist/sandbox-git.d.ts +64 -0
  45. package/dist/sandbox-git.js +89 -0
  46. package/dist/scheduler-store.js +19 -11
  47. package/dist/settings-units.js +14 -12
  48. package/dist/types.d.ts +6 -2
  49. package/dist/types.js +11 -6
  50. package/dist/url-shape.d.ts +16 -0
  51. package/dist/url-shape.js +18 -0
  52. package/dist/web-admin-credential.js +5 -2
  53. package/dist/web-admin-session-url.js +2 -2
  54. package/dist/worker-row-verdict.d.ts +11 -0
  55. package/dist/worker-row-verdict.js +51 -1
  56. package/dist/workers-rows.js +38 -10
  57. package/dist/zod-unwrap.d.ts +6 -0
  58. package/dist/zod-unwrap.js +14 -7
  59. package/docs/API-SURFACE.md +35 -4
  60. package/docs/CONSUMER-INTEGRATION.md +178 -0
  61. package/llms-full.txt +23 -4
  62. package/llms.txt +3 -2
  63. package/package.json +1 -1
  64. package/dist/definition-copy.d.ts +0 -23
  65. package/dist/definition-copy.js +0 -52
@@ -24,68 +24,80 @@
24
24
  * (语义逐字搬),是一项已知待收的分歧。
25
25
  */
26
26
  import { echoCallerText } from "./caller-text-echo.js";
27
- import { captureStringArray, dataDescriptor } from "./definition-copy.js";
28
- import { isPlainJsonObject, jsonShapeOf } from "./plain-json-object.js";
27
+ import { UNINSPECTABLE, captureStringArray, dataDescriptor, inspect, ownEntriesOf, ownFieldsOf, ownValueOf, readKey } from "./own-key-read.js";
28
+ import { isJsonArray, isPlainJsonObject, jsonShapeOf } from "./plain-json-object.js";
29
29
  import { MCP_CONTENT_ORIGINS } from "./types.js";
30
30
  import { startsWithHttpScheme } from "./url-shape.js";
31
31
  // ── 值的种类描述(拒句只说「你写的是什么形」,不回显值:值可能带凭据、可能任意长)──────────────────────────────────────
32
- /** 撤销 / 读不出的 Proxy 的拒句(`got ` 之后的名词短语)—— **全包唯一的一句**:`Array.isArray` 对已撤销的 Proxy 抛 `TypeError`,陷阱抛出来也一样,读器与描述器都不能因此被带出去。 */
33
- const UNINSPECTABLE = "an object that could not be inspected (a proxy trap threw)";
34
- /**
35
- * 唯一的**安全检视内核**:读器与描述器要碰不可信的值(`Array.isArray` 对已撤销的 Proxy 抛、`get` / 原型陷阱抛、会抛的自有 getter)都经它 ——
36
- * 「检视不了」的**律与句都单源**:抛了 ⇒ `ok:false` 带 {@link UNINSPECTABLE} 那一句,不再各处各包一个 try、各写一句。
37
- */
38
- function inspect(read) {
39
- try {
40
- return { ok: true, value: read() };
41
- }
42
- catch {
43
- return { ok: false, sentence: UNINSPECTABLE };
32
+ /** 值的**种类词**(不回显值本身):`null` / `array` / `typeof` 词(`object` / `function` / `string` …);读不出的对象 ⇒ 全包唯一的那一句。形状只问座位(总,不抛):两只坏值描述器共用这一只。 */
33
+ function describeKindWord(v) {
34
+ const shape = jsonShapeOf(v);
35
+ switch (shape) {
36
+ case "null":
37
+ return "null";
38
+ case "array":
39
+ return "array";
40
+ case "uninspectable":
41
+ return UNINSPECTABLE;
42
+ case "plain":
43
+ case "non_plain_object":
44
+ case "primitive":
45
+ return typeof v;
46
+ default: {
47
+ const unhandled = shape;
48
+ throw new Error(`describeKindWord: unhandled shape ${String(unhandled)}`);
49
+ }
44
50
  }
45
51
  }
46
- /** 三只描述器的壳:检视抛了就说「检视不了」那一句(经 {@link inspect})。 */
47
- function inspectSafely(describe) {
48
- const read = inspect(describe);
49
- return read.ok ? read.value : read.sentence;
50
- }
51
- /** 值的**种类**(不回显值本身)。总(不抛):经 {@link inspectSafely}。 */
52
+ /** 值的**种类**(不回显值本身)。总(不抛):形状只问座位,读不出的对象说全包唯一的「检视不了」那一句。 */
52
53
  export function describeValueShape(v) {
53
- return inspectSafely(() => {
54
- if (v === undefined)
55
- return "nothing (the key is absent)";
56
- if (v === null)
54
+ if (v === undefined)
55
+ return "nothing (the key is absent)";
56
+ const shape = jsonShapeOf(v);
57
+ switch (shape) {
58
+ case "null":
57
59
  return "null";
58
- if (Array.isArray(v))
60
+ case "array":
59
61
  return "an array";
60
- if (typeof v === "string")
61
- return v.length === 0 ? "an empty string" : `a string of ${v.length} characters`;
62
- if (typeof v === "object")
63
- return "an object";
64
- return `a ${typeof v}`;
65
- });
62
+ case "uninspectable":
63
+ return UNINSPECTABLE;
64
+ case "plain":
65
+ case "non_plain_object":
66
+ return typeof v === "function" ? "a function" : "an object";
67
+ case "primitive":
68
+ return typeof v === "string" ? (v.length === 0 ? "an empty string" : `a string of ${v.length} characters`) : `a ${typeof v}`;
69
+ default: {
70
+ const unhandled = shape;
71
+ throw new Error(`describeValueShape: unhandled shape ${String(unhandled)}`);
72
+ }
73
+ }
66
74
  }
67
- /** 「不是字符串数组」的种类:数组里混了非串元素说那一句,否则回落到 {@link describeValueShape}。总(不抛):经 {@link inspectSafely}。 */
75
+ /** 「不是字符串数组」的种类:数组里混了非串元素说那一句,否则回落到 {@link describeValueShape}。总(不抛):`isJsonArray` 是座位的总版。 */
68
76
  export function describeNotStringArray(v) {
69
- return inspectSafely(() => (Array.isArray(v) ? "an array with a non-string element" : describeValueShape(v)));
77
+ return isJsonArray(v) ? "an array with a non-string element" : describeValueShape(v);
70
78
  }
71
- /** 「不是字符串值的对象」的种类:对象里混了非串值说那一句,否则回落到 {@link describeValueShape}。总(不抛):经 {@link inspectSafely}。 */
79
+ /** 「不是字符串值的对象」的种类:对象里混了非串值说那一句,否则回落到 {@link describeValueShape}。总(不抛)。 */
72
80
  export function describeNotStringRecord(v) {
73
- return inspectSafely(() => (isPlainJsonObject(v) ? "an object with a non-string value" : describeValueShape(v)));
81
+ return isPlainJsonObject(v) ? "an object with a non-string value" : describeValueShape(v);
74
82
  }
75
83
  /**
76
- * 字符串数组(空数组合法)。与传输读器读 `args` 是**同一只读法**(`./definition-copy` 的 `captureStringArray`:按下标、自有下标、一次读、非串即坏、不走迭代协议):
77
- * 稀疏数组(洞)、`length` 读不出安全非负整数的数组、读元素时抛的数组都不是 —— 洞不顺原型链读到 `Object.prototype` / `Array.prototype` 上的数字键。
84
+ * 字符串数组(空数组合法)—— 类型守卫,所以按 `./own-key-read` 的 `captureStringArray` 的**严归类**读(整容器读,自有下标各一份描述符,非串即坏、不走迭代协议):
85
+ * 下标是访问器或不可枚举的数组不是(getter 一次都不调 —— 守卫为真,调用方随后的普通读读到的就是判过的那一份);稀疏数组(洞)、`length` 读不出安全非负整数的数组、
86
+ * 读不出的数组都不是 —— 洞不顺原型链读到 `Object.prototype` / `Array.prototype` 上的数字键。传输读器读 `args` 交出去的是副本,用同一只函数的宽归类。
78
87
  */
79
88
  export function isStringArray(v) {
80
- return captureStringArray(v) !== undefined;
89
+ return captureStringArray(v, "strict") !== undefined;
81
90
  }
82
91
  /**
83
- * 纯数据的「名 → 串」记录(空对象合法;数组、`Map`、类实例、自有 `__proto__` 成员、有非串成员的对象都不是)。与传输读器读 `env` / `headers` 是**同一个**定义
84
- * ({@link readPlainStringRecord}):包内没有第二把更宽的尺。
92
+ * 纯数据的「名 → 串」记录(空对象合法;数组、`Map`、类实例、自有 `__proto__` 成员、有非串成员的对象都不是)—— 类型守卫,所以是**严归类**:成员经内核的
93
+ * `ownEntriesOf` 读(整容器读的严归类:每个自有串键一份描述符;访问器成员或不可枚举的成员 ⇒ 不是,getter 一次都不调 —— 守卫为真,调用方随后的普通读读到的就是判过的那一份)。
94
+ * 传输读器读 `env` / `headers`({@link readPlainStringRecord})交出去的是**副本**,按宽归类读(不可枚举的成员照读、访问器调一次);值与成员名的判据(是串、不叫 `__proto__`)两边同一条。
85
95
  */
86
96
  export function isStringRecord(v) {
87
- const read = readPlainStringRecord(v);
88
- return read.ok && !read.reservedMember;
97
+ if (!isPlainJsonObject(v))
98
+ return false;
99
+ const entries = ownEntriesOf(v);
100
+ return entries.ok && entries.value.every(([name, value]) => name !== RESERVED_MEMBER_NAME && typeof value === "string");
89
101
  }
90
102
  // ── 调用方写的串的回显口径 ──────────────────────────────────────────────────────────────────────────
91
103
  /** 句子里点名一个值的写法:{@link echoCallerText} 外加一对双引号;内部的引号与反斜杠逐字(不转义)。 */
@@ -115,7 +127,7 @@ export function isUsableWireName(v) {
115
127
  */
116
128
  export function describeBadWireName(v) {
117
129
  if (typeof v !== "string")
118
- return v === null ? "null" : Array.isArray(v) ? "array" : typeof v;
130
+ return describeKindWord(v);
119
131
  if (v.length === 0)
120
132
  return "empty";
121
133
  const head = quotedCallerText(v);
@@ -135,7 +147,7 @@ export function isContentOriginWord(v) {
135
147
  }
136
148
  /** 坏内容出处值的**可打印**摘要:串 ⇒ 引号形的回显({@link quotedCallerText}:剥控制与双向字符、截 64 字);非串只说种类。 */
137
149
  export function describeBadContentOrigin(v) {
138
- return typeof v === "string" ? quotedCallerText(v) : v === null ? "null" : Array.isArray(v) ? "array" : typeof v;
150
+ return typeof v === "string" ? quotedCallerText(v) : describeKindWord(v);
139
151
  }
140
152
  // ── 两个身份头 ──────────────────────────────────────────────────────────────────────────────────────
141
153
  /**
@@ -155,35 +167,35 @@ export function readMcpIdentityHeaders(transport) {
155
167
  /**
156
168
  * 两个身份头的**唯一读法**(公开读者与传输快照共用):只有 `principalHeader` **干净地读成缺席**时才读 `assertionHeader`;`principalHeader` 在场(不论值形)⇒ `assertionHeader` 一次不读;
157
169
  * 任何一个读不出 ⇒ 整体读不出(`ok:false`,向上传),不折成 `undefined` —— 不然「principal 读不出」会落到 assertion、读它的 getter。
170
+ * **只读自有键**(身份只认自有键):键只靠继承可见(含 `Object.prototype` 被污染成数据值)⇒ 不读它的值、当缺席,但**记下**(`principalInherited` / `assertionInherited`)—— 传输读器据此点名,
171
+ * 身份轴上不静默;公开读者没有 `lost` 通道,只读值。
158
172
  */
159
173
  function identityFieldsOf(record) {
160
- const principal = ownValueOf(record, "principalHeader");
161
- if (!principal.ok)
162
- return principal;
163
- if (principal.value !== undefined)
164
- return { ok: true, value: { principalHeader: principal.value, assertionHeader: undefined } };
165
- const assertion = ownValueOf(record, "assertionHeader");
166
- if (!assertion.ok)
167
- return assertion;
168
- return { ok: true, value: { principalHeader: undefined, assertionHeader: assertion.value } };
169
- }
170
- /** 一个字段的值,**只读自有属性**(`Object.hasOwn`):原型链上的值 / 被污染的 getter 一律当缺席。**自身就是守护读**(经 `inspect`):`getOwnPropertyDescriptor` 陷阱抛、会抛的自有 getter、
171
- * 已撤销的 Proxy ⇒ `ok:false`(读不出),调用方各自决定怎么办、不再各包一层。 */
172
- function ownValueOf(record, key) {
173
- return inspect(() => (Object.hasOwn(record, key) ? record[key] : undefined));
174
- }
175
- /** 按顺序读若干字段,**第一个读不出的就停**(后面的一个不读),整体读不出向上传。 */
176
- function ownFieldsOf(record, keys) {
177
- const out = Object.create(null);
178
- for (const key of keys) {
179
- const read = ownValueOf(record, key);
180
- if (!read.ok)
181
- return read;
182
- Object.defineProperty(out, key, dataDescriptor(read.value));
174
+ const principal = readKey(record, "principalHeader");
175
+ if (principal.presence === "unreadable")
176
+ return { ok: false, sentence: UNINSPECTABLE };
177
+ if (principal.presence === "own" && principal.value !== undefined) {
178
+ return { ok: true, value: { principalHeader: principal.value, assertionHeader: undefined, principalInherited: false, assertionInherited: false } };
183
179
  }
184
- return { ok: true, value: out };
180
+ const assertion = readKey(record, "assertionHeader");
181
+ if (assertion.presence === "unreadable")
182
+ return { ok: false, sentence: UNINSPECTABLE };
183
+ return {
184
+ ok: true,
185
+ value: {
186
+ principalHeader: undefined,
187
+ assertionHeader: assertion.presence === "own" ? assertion.value : undefined,
188
+ principalInherited: principal.presence === "inherited",
189
+ assertionInherited: assertion.presence === "inherited",
190
+ },
191
+ };
185
192
  }
186
193
  const transportBad = (path, reason) => ({ ok: false, issue: { path, reason } });
194
+ /** 身份头只靠继承可见时的 `lost` 条目:不读它的值,但点名(身份轴不静默;句子不回显值)。机读位不带(`fault` 的闭集不为它加词)。 */
195
+ const inheritedIdentityIssue = (key) => ({
196
+ path: `transport.${key}`,
197
+ reason: "inherited key ignored (the key is visible only through the prototype chain; a transport's identity headers are read from its own keys)",
198
+ });
187
199
  /**
188
200
  * 请求面 `transport` 的**唯一读器**:读器投影 transport 用它,发送前 / 受理时的判形读它报的 `lost` —— 同一只函数两个消费点,「读器丢了什么」与「门拒什么」不可能分岔。
189
201
  * · 硬坏形(`transport` 不是对象 / `kind` 出闭集 / stdio 缺 `command` / http 的 `url` 不是 http(s))⇒ `ok:false`,`path` 是读器点名的键路径;
@@ -214,7 +226,7 @@ export function readRequestMcpTransport(t) {
214
226
  }
215
227
  }
216
228
  const tr = t;
217
- // 字段只读一次:同一个检视里把「这一种传输要读的字段」读成局部快照(`get` 陷阱抛的 Proxy / 会抛的自有 getter ⇒ 整只不交,不抛),之后只读快照;这一种传输用不到的字段一次也不读
229
+ // 字段只读一次:经读内核把「这一种传输要读的字段」读成局部快照(描述符 / `has` 陷阱抛的 Proxy / 会抛的自有 getter ⇒ 整只不交,不抛;`get` 陷阱不被问),之后只读快照;这一种传输用不到的字段一次也不读
218
230
  const snapshotRead = snapshotTransport(tr);
219
231
  if (!snapshotRead.ok)
220
232
  return transportBad("transport", `must be an object with a "kind" of "stdio" or "http" — got ${snapshotRead.sentence}`);
@@ -225,7 +237,7 @@ export function readRequestMcpTransport(t) {
225
237
  if (typeof snap.command !== "string" || snap.command.length === 0)
226
238
  return transportBad("transport.command", `must be a non-empty string (the command to run) — got ${describeValueShape(snap.command)}`);
227
239
  const rawArgs = snap.args; // 交出去的是副本(不是调用方数组的原引用),稀疏 / 读不出的数组整键丢、进 `lost`
228
- const args = rawArgs === undefined ? undefined : captureStringArray(rawArgs);
240
+ const args = rawArgs === undefined ? undefined : captureStringArray(rawArgs, "wide");
229
241
  const argsIssue = rawArgs !== undefined && args === undefined ? { path: "transport.args", reason: `must be an array of strings — got ${describeNotStringArray(rawArgs)}` } : undefined;
230
242
  const env = readTransportRecord("transport.env", snap.env);
231
243
  return {
@@ -256,7 +268,7 @@ export function readRequestMcpTransport(t) {
256
268
  ...(headers.record !== undefined ? { headers: headers.record } : {}),
257
269
  ...ids,
258
270
  },
259
- lost: issuesOf(...headers.issues, principalIssue),
271
+ lost: issuesOf(...headers.issues, principalIssue, snap.principalInherited ? inheritedIdentityIssue("principalHeader") : undefined, snap.assertionInherited ? inheritedIdentityIssue("assertionHeader") : undefined),
260
272
  };
261
273
  }
262
274
  case "other":
@@ -286,7 +298,7 @@ function snapshotTransport(tr) {
286
298
  const ids = identityFieldsOf(tr);
287
299
  if (!ids.ok)
288
300
  return ids;
289
- return { ok: true, value: { kind: "http", url: fields.value.url, headers: fields.value.headers, principalHeader: ids.value.principalHeader, assertionHeader: ids.value.assertionHeader } };
301
+ return { ok: true, value: { kind: "http", url: fields.value.url, headers: fields.value.headers, principalHeader: ids.value.principalHeader, assertionHeader: ids.value.assertionHeader, principalInherited: ids.value.principalInherited, assertionInherited: ids.value.assertionInherited } };
290
302
  }
291
303
  return { ok: true, value: { kind: "other", value: kind.value } };
292
304
  }
@@ -317,7 +329,7 @@ function notPlainDetail(shape, v) {
317
329
  }
318
330
  /**
319
331
  * 读一份「名 → 串」记录。记录交不交:是朴素 JSON 对象(座位的判据,见 `./plain-json-object`:原型是 `null`,或原型自己的原型是 `null`;纯数据 —— `Map`、`Set`、类实例、建在原型上的记录都不是),
320
- * 且每个成员的值都是串。成员 = `Object.getOwnPropertyNames` 的每一个名字(不可枚举的也算 —— 引擎的收据按全部自有属性名读),值按属性描述读:数据属性取值,访问器调一次(访问器或描述 trap 抛了同判为坏)。
332
+ * 且每个成员的值都是串。成员 = `Object.getOwnPropertyNames` 的每一个名字(不可枚举的也算 —— 引擎的收据按全部自有属性名读),值经内核 `readKey` 读(一份描述符:数据属性取值,访问器调一次,不问 `get` 陷阱;访问器或描述 trap 抛了同判为坏)。
321
333
  * 读原型与成员名时代理 trap 抛了 ⇒ 整张不交(读器对调用方递来的任何对象都不抛;`JSON` 存体里不会有代理,这条只落在直接调读器的嵌入方身上)。
322
334
  * 交出去的是副本(原型 `Object.prototype`、成员全可枚举),调用方的对象不被交出去、也不被改;名叫 `__proto__` 的成员不进副本,`reservedMember` 记下它。
323
335
  * 名叫 `__proto__` 而值也坏的成员,句子一次说全两件事(名坏在前、值坏并列):调用方照值改完再撞第二条是两步才改对。
@@ -337,15 +349,10 @@ function readPlainStringRecord(v) {
337
349
  const members = Object.create(null);
338
350
  let reservedMember = false;
339
351
  for (const name of names) {
340
- let value;
341
- let unreadable = false;
342
- try {
343
- const d = Object.getOwnPropertyDescriptor(v, name);
344
- value = d === undefined ? undefined : Object.hasOwn(d, "value") ? d.value : d.get === undefined ? undefined : Reflect.apply(d.get, v, []);
345
- }
346
- catch {
347
- unreadable = true;
348
- }
352
+ // 成员经内核读(契约见函数头注):一份描述符里取值 —— 数据属性取值、访问器调一次、从不问 Proxy 的 `get` 陷阱;成员在两次读之间被删掉 ⇒ 不是串(不读继承来的值)
353
+ const read = readKey(v, name);
354
+ const unreadable = read.presence === "unreadable";
355
+ const value = read.presence === "own" ? read.value : undefined;
349
356
  if (unreadable || typeof value !== "string") {
350
357
  const valueFault = unreadable ? "could not be read (its getter threw)" : `must be a string — got ${describeValueShape(value)}`;
351
358
  const reserved = name === RESERVED_MEMBER_NAME;