@sema-agent/settings-schema 14.5.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 (77) hide show
  1. package/CHANGELOG.md +531 -0
  2. package/README.md +46 -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/atomic-write.js +10 -7
  16. package/dist/bundle.d.ts +1 -1
  17. package/dist/bundle.js +1 -1
  18. package/dist/config-d-file.js +33 -15
  19. package/dist/config-dir-migration.js +2 -1
  20. package/dist/config-fns.js +142 -78
  21. package/dist/conformance/canonical.js +7 -10
  22. package/dist/conformance/contract-adapter.js +26 -1
  23. package/dist/conformance/corpus.js +5 -5
  24. package/dist/conformance/derive.js +6 -5
  25. package/dist/conformance/run.js +44 -6
  26. package/dist/cross-session.d.ts +4 -4
  27. package/dist/declared-capture.d.ts +41 -0
  28. package/dist/declared-capture.js +344 -0
  29. package/dist/file-edit.js +11 -15
  30. package/dist/file-store.d.ts +1 -31
  31. package/dist/file-store.js +3 -30
  32. package/dist/fleet.d.ts +11 -0
  33. package/dist/fleet.js +60 -26
  34. package/dist/fs-failure.d.ts +12 -7
  35. package/dist/fs-failure.js +53 -29
  36. package/dist/hash.d.ts +0 -9
  37. package/dist/hash.js +64 -7
  38. package/dist/hooks-config.d.ts +2 -2
  39. package/dist/hooks-config.js +129 -33
  40. package/dist/http-hook-allowlists.d.ts +27 -10
  41. package/dist/http-hook-allowlists.js +189 -25
  42. package/dist/index.d.ts +3 -0
  43. package/dist/index.js +4 -1
  44. package/dist/mcp-request-entry.d.ts +13 -8
  45. package/dist/mcp-request-entry.js +246 -107
  46. package/dist/one-time-migrations.js +207 -95
  47. package/dist/own-key-read.d.ts +247 -0
  48. package/dist/own-key-read.js +449 -0
  49. package/dist/plain-json-object.d.ts +38 -3
  50. package/dist/plain-json-object.js +84 -7
  51. package/dist/portable-domains.d.ts +31 -0
  52. package/dist/portable-domains.js +30 -0
  53. package/dist/remote-exec.js +9 -1
  54. package/dist/safety-merge-spec.d.ts +88 -6
  55. package/dist/safety-merge-spec.js +246 -17
  56. package/dist/sandbox-git.d.ts +64 -0
  57. package/dist/sandbox-git.js +89 -0
  58. package/dist/scheduler-store.js +21 -12
  59. package/dist/settings-units.d.ts +85 -0
  60. package/dist/settings-units.js +177 -0
  61. package/dist/types.d.ts +6 -2
  62. package/dist/types.js +65 -56
  63. package/dist/url-shape.d.ts +27 -5
  64. package/dist/url-shape.js +29 -5
  65. package/dist/web-admin-credential.js +5 -2
  66. package/dist/web-admin-session-url.js +2 -2
  67. package/dist/worker-row-verdict.d.ts +11 -0
  68. package/dist/worker-row-verdict.js +51 -1
  69. package/dist/workers-rows.d.ts +2 -1
  70. package/dist/workers-rows.js +41 -12
  71. package/dist/zod-unwrap.d.ts +6 -0
  72. package/dist/zod-unwrap.js +14 -7
  73. package/docs/API-SURFACE.md +50 -5
  74. package/docs/CONSUMER-INTEGRATION.md +178 -0
  75. package/llms-full.txt +34 -5
  76. package/llms.txt +6 -3
  77. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -17,7 +17,8 @@ export * from "./config-fns.js";
17
17
  export * from "./reader.js";
18
18
  export * from "./cross-domain.js"; // pure half only (no fleet placement)
19
19
  export * from "./remote-exec.js";
20
- export * from "./safety-merge-spec.js"; // #14 per-category safety merge-spec (settings resolver authority)
20
+ export * from "./safety-merge-spec.js"; // #14 per-category safety merge-spec (settings resolver authority) + 14.6.0 THE comparator tightenVerdict / TIGHTEN_VERDICTS and the member comparator memberTighten / resolveMemberTighten (derived from a member row's value class)
21
+ export * from "./settings-units.js"; // 14.6.0: SETTINGS_UNITS — the settings-document unit table (path / category / addressing / reading) + unitTighten (a unit's comparator) + readSettingsUnit (a unit's raw-value reading; spellings normalized, everything else unreadable), pure/browser-safe
21
22
  export * from "./scheduler-store.js"; // R7 self-wake scheduler store CONTRACT (pure: type + parse/serialize + SchedulerStore interface; fs binding in ./node)
22
23
  export * from "./cross-session.js"; // CC-parity `settings.json` cross-session keys (crossSessionInbound / dialogExpiry) — pure/browser-safe
23
24
  export * from "./hooks.js"; // CC-parity hooks contract (33-event set + owner map + per-event input/output shapes + the config surface: classifyHooksConfig and its zod faces HooksConfig / HookEntry …) & stop attribution (stoppedBy) — pure/browser-safe
@@ -28,6 +29,8 @@ export * from "./api/auth-bridge.js"; // 鉴权桥 contract: worker verifies web
28
29
  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
29
30
  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
30
31
  export * from "./api/feedback.js"; // /bug report wire contract: FeedbackSubmitRequest (POST body) + FeedbackWire / FeedbackListResponse + the two caps, pure/browser-safe
32
+ 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
33
+ 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
31
34
  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
32
35
  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
33
36
  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
@@ -1,15 +1,20 @@
1
1
  import { type McpContentOrigin } from "./types.js";
2
- /** 值的**种类**(不回显值本身)。 */
2
+ /** 值的**种类**(不回显值本身)。总(不抛):形状只问座位,读不出的对象说全包唯一的「检视不了」那一句。 */
3
3
  export declare function describeValueShape(v: unknown): string;
4
- /** 「不是字符串数组」的种类:数组里混了非串元素说那一句,否则回落到 {@link describeValueShape}。 */
4
+ /** 「不是字符串数组」的种类:数组里混了非串元素说那一句,否则回落到 {@link describeValueShape}。总(不抛):`isJsonArray` 是座位的总版。 */
5
5
  export declare function describeNotStringArray(v: unknown): string;
6
- /** 「不是字符串值的对象」的种类:对象里混了非串值说那一句,否则回落到 {@link describeValueShape}。 */
6
+ /** 「不是字符串值的对象」的种类:对象里混了非串值说那一句,否则回落到 {@link describeValueShape}。总(不抛)。 */
7
7
  export declare function describeNotStringRecord(v: unknown): string;
8
- /** 字符串数组(空数组合法)。 */
8
+ /**
9
+ * 字符串数组(空数组合法)—— 类型守卫,所以按 `./own-key-read` 的 `captureStringArray` 的**严归类**读(整容器读,自有下标各一份描述符,非串即坏、不走迭代协议):
10
+ * 下标是访问器或不可枚举的数组不是(getter 一次都不调 —— 守卫为真,调用方随后的普通读读到的就是判过的那一份);稀疏数组(洞)、`length` 读不出安全非负整数的数组、
11
+ * 读不出的数组都不是 —— 洞不顺原型链读到 `Object.prototype` / `Array.prototype` 上的数字键。传输读器读 `args` 交出去的是副本,用同一只函数的宽归类。
12
+ */
9
13
  export declare function isStringArray(v: unknown): v is string[];
10
14
  /**
11
- * 纯数据的「名 → 串」记录(空对象合法;数组、`Map`、类实例、自有 `__proto__` 成员、有非串成员的对象都不是)。与传输读器读 `env` / `headers` 是**同一个**定义
12
- * ({@link readPlainStringRecord}):包内没有第二把更宽的尺。
15
+ * 纯数据的「名 → 串」记录(空对象合法;数组、`Map`、类实例、自有 `__proto__` 成员、有非串成员的对象都不是)—— 类型守卫,所以是**严归类**:成员经内核的
16
+ * `ownEntriesOf` 读(整容器读的严归类:每个自有串键一份描述符;访问器成员或不可枚举的成员 ⇒ 不是,getter 一次都不调 —— 守卫为真,调用方随后的普通读读到的就是判过的那一份)。
17
+ * 传输读器读 `env` / `headers`({@link readPlainStringRecord})交出去的是**副本**,按宽归类读(不可枚举的成员照读、访问器调一次);值与成员名的判据(是串、不叫 `__proto__`)两边同一条。
13
18
  */
14
19
  export declare function isStringRecord(v: unknown): v is Record<string, string>;
15
20
  /** 引擎 `TOOL_WIRE_NAME_MAX_CHARS` 的镜像:一台 MCP server(或 A2A 对端)的申报名会成为线上工具名的命名空间段,引擎对超界名是**拒**(不截断)。 */
@@ -58,7 +63,7 @@ export type RequestMcpTransport = {
58
63
  * 条目内一处坏形:`path` = 条目内的键路径(`transport.url`),`reason` = 判官的原因句(只说形与词表,不回显值)。
59
64
  *
60
65
  * `env` / `headers` 这两张「名 → 串」记录上的坏形另带两格机读位,调用方分流不必解析句子:`fault` = 闭集词 ——
61
- * · `not_plain_record`:这个键的值不是纯数据记录(不是对象 / 数组 / `null`,或原型不是 `Object.prototype` 与 `null`:`Map`、`Set`、类实例);整张记录不交;
66
+ * · `not_plain_record`:这个键的值不是纯数据记录(不是对象 / 数组 / `null`,或不是朴素 JSON 对象 —— 原型链比座位的判据长:`Map`、`Set`、类实例);整张记录不交;
62
67
  * · `member_not_string`:有一个成员的值不是串(或访问器抛了);整张记录不交,`member` 点名它;
63
68
  * · `member_name_reserved`:有一个成员叫 `__proto__`(引擎的声明收据拒这个名)。成员的值是串 ⇒ 只剔这个成员、其余照交,`path` 是成员级(`transport.env.__proto__`);
64
69
  * 成员的值也不是串 ⇒ 整张记录不交,`path` 是键级,句子一次说全两件事 —— 名坏优先、值坏并列,所以词仍是 `member_name_reserved`。
@@ -84,7 +89,7 @@ export type RequestMcpTransportRead = {
84
89
  * · 硬坏形(`transport` 不是对象 / `kind` 出闭集 / stdio 缺 `command` / http 的 `url` 不是 http(s))⇒ `ok:false`,`path` 是读器点名的键路径;
85
90
  * · 软坏形(`args` 不是串数组 / `env` `headers` 不是纯数据的串记录 / `principalHeader` 非串)⇒ 读器**丢这个键**(重放存量提交体时的升级前读法),并把它记进 `lost`
86
91
  * (受理新提交的门据它响亮拒)。`assertionHeader` 不在 `lost` 里:两个身份头的键组合与 `assertionHeader` 的值形由调用方在读之前对原始传输判。
87
- * · `env` / `headers` 只收**纯数据**的记录({@link readPlainStringRecord}):原型必须是 `Object.prototype` 或 `null`(`Map`、`Set`、类实例整张不收),每个自有成员的值都是串;
92
+ * · `env` / `headers` 只收**纯数据**的记录({@link readPlainStringRecord}):必须是朴素 JSON 对象(`./plain-json-object` 的判据;`Map`、`Set`、类实例整张不收),每个自有成员的值都是串;
88
93
  * 交出的是**副本**(不是调用方的对象,改原对象不影响读后的形)。名叫 `__proto__` 的成员(JSON 文本铸得出这个自有键,对象字面量写不出)不进副本、单独记进 `lost`。
89
94
  * 坏形的 `lost` 条目带机读位(见 {@link McpEntryKeyIssue})。
90
95
  * url 的拒句不回显值:它常规地带凭据(userinfo)。
@@ -16,7 +16,7 @@
16
16
  * · 申报名的线长界 {@link TOOL_WIRE_NAME_MAX_CHARS} = 引擎同名常量(值级对拍:上游漂移门 ⑧);
17
17
  * · 请求面传输形 {@link RequestMcpTransport} = 引擎 MCP 条目的传输形,http 形多一个本包承载的 `assertionHeader`(编译期双向对拍:门 ⑧);
18
18
  * · 内容出处词表 = {@link MCP_CONTENT_ORIGINS}(已有的镜像,门 ⑥);
19
- * · url 前缀守卫 = `./url-shape` 的 `startsWithHttpScheme`(本包判 URL 的正则只在那一个模块)。
19
+ * · url 前缀守卫 = `./url-shape` 的 `startsWithHttpScheme`(本包判 URL 的正则只在那一个模块)。它只看前缀,不是形判官 —— 同一个串两口可以答得不同,两张分歧表与收口形(S-2106)写在 `./url-shape` 头注「第二个答案」。
20
20
  *
21
21
  * ## 与配置面的关系(如实)
22
22
  * 配置面的 MCP 条目(`McpServerSpec`,部署声明:凭据只许 env-NAME 引用)与请求面(提交体:可带字面凭据 `env` / `headers`)是两份契约,本模块只管后者。
@@ -24,45 +24,80 @@
24
24
  * (语义逐字搬),是一项已知待收的分歧。
25
25
  */
26
26
  import { echoCallerText } from "./caller-text-echo.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";
27
29
  import { MCP_CONTENT_ORIGINS } from "./types.js";
28
30
  import { startsWithHttpScheme } from "./url-shape.js";
29
31
  // ── 值的种类描述(拒句只说「你写的是什么形」,不回显值:值可能带凭据、可能任意长)──────────────────────────────────────
30
- /** 值的**种类**(不回显值本身)。 */
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
+ }
50
+ }
51
+ }
52
+ /** 值的**种类**(不回显值本身)。总(不抛):形状只问座位,读不出的对象说全包唯一的「检视不了」那一句。 */
31
53
  export function describeValueShape(v) {
32
54
  if (v === undefined)
33
55
  return "nothing (the key is absent)";
34
- if (v === null)
35
- return "null";
36
- if (Array.isArray(v))
37
- return "an array";
38
- if (typeof v === "string")
39
- return v.length === 0 ? "an empty string" : `a string of ${v.length} characters`;
40
- if (typeof v === "object")
41
- return "an object";
42
- return `a ${typeof v}`;
43
- }
44
- /** 「不是字符串数组」的种类:数组里混了非串元素说那一句,否则回落到 {@link describeValueShape}。 */
56
+ const shape = jsonShapeOf(v);
57
+ switch (shape) {
58
+ case "null":
59
+ return "null";
60
+ case "array":
61
+ return "an array";
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
+ }
74
+ }
75
+ /** 「不是字符串数组」的种类:数组里混了非串元素说那一句,否则回落到 {@link describeValueShape}。总(不抛):`isJsonArray` 是座位的总版。 */
45
76
  export function describeNotStringArray(v) {
46
- return 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);
47
78
  }
48
- /** 「不是字符串值的对象」的种类:对象里混了非串值说那一句,否则回落到 {@link describeValueShape}。 */
79
+ /** 「不是字符串值的对象」的种类:对象里混了非串值说那一句,否则回落到 {@link describeValueShape}。总(不抛)。 */
49
80
  export function describeNotStringRecord(v) {
50
- return isRecord(v) ? "an object with a non-string value" : describeValueShape(v);
81
+ return isPlainJsonObject(v) ? "an object with a non-string value" : describeValueShape(v);
51
82
  }
52
- /** 字符串数组(空数组合法)。 */
83
+ /**
84
+ * 字符串数组(空数组合法)—— 类型守卫,所以按 `./own-key-read` 的 `captureStringArray` 的**严归类**读(整容器读,自有下标各一份描述符,非串即坏、不走迭代协议):
85
+ * 下标是访问器或不可枚举的数组不是(getter 一次都不调 —— 守卫为真,调用方随后的普通读读到的就是判过的那一份);稀疏数组(洞)、`length` 读不出安全非负整数的数组、
86
+ * 读不出的数组都不是 —— 洞不顺原型链读到 `Object.prototype` / `Array.prototype` 上的数字键。传输读器读 `args` 交出去的是副本,用同一只函数的宽归类。
87
+ */
53
88
  export function isStringArray(v) {
54
- return Array.isArray(v) && v.every((a) => typeof a === "string");
89
+ return captureStringArray(v, "strict") !== undefined;
55
90
  }
56
91
  /**
57
- * 纯数据的「名 → 串」记录(空对象合法;数组、`Map`、类实例、自有 `__proto__` 成员、有非串成员的对象都不是)。与传输读器读 `env` / `headers` 是**同一个**定义
58
- * ({@link readPlainStringRecord}):包内没有第二把更宽的尺。
92
+ * 纯数据的「名 → 串」记录(空对象合法;数组、`Map`、类实例、自有 `__proto__` 成员、有非串成员的对象都不是)—— 类型守卫,所以是**严归类**:成员经内核的
93
+ * `ownEntriesOf` 读(整容器读的严归类:每个自有串键一份描述符;访问器成员或不可枚举的成员 ⇒ 不是,getter 一次都不调 —— 守卫为真,调用方随后的普通读读到的就是判过的那一份)。
94
+ * 传输读器读 `env` / `headers`({@link readPlainStringRecord})交出去的是**副本**,按宽归类读(不可枚举的成员照读、访问器调一次);值与成员名的判据(是串、不叫 `__proto__`)两边同一条。
59
95
  */
60
96
  export function isStringRecord(v) {
61
- const read = readPlainStringRecord(v);
62
- return read.ok && !read.reservedMember;
63
- }
64
- function isRecord(v) {
65
- return v !== null && typeof v === "object" && !Array.isArray(v);
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");
66
101
  }
67
102
  // ── 调用方写的串的回显口径 ──────────────────────────────────────────────────────────────────────────
68
103
  /** 句子里点名一个值的写法:{@link echoCallerText} 外加一对双引号;内部的引号与反斜杠逐字(不转义)。 */
@@ -92,7 +127,7 @@ export function isUsableWireName(v) {
92
127
  */
93
128
  export function describeBadWireName(v) {
94
129
  if (typeof v !== "string")
95
- return v === null ? "null" : Array.isArray(v) ? "array" : typeof v;
130
+ return describeKindWord(v);
96
131
  if (v.length === 0)
97
132
  return "empty";
98
133
  const head = quotedCallerText(v);
@@ -112,7 +147,7 @@ export function isContentOriginWord(v) {
112
147
  }
113
148
  /** 坏内容出处值的**可打印**摘要:串 ⇒ 引号形的回显({@link quotedCallerText}:剥控制与双向字符、截 64 字);非串只说种类。 */
114
149
  export function describeBadContentOrigin(v) {
115
- return typeof v === "string" ? quotedCallerText(v) : v === null ? "null" : Array.isArray(v) ? "array" : typeof v;
150
+ return typeof v === "string" ? quotedCallerText(v) : describeKindWord(v);
116
151
  }
117
152
  // ── 两个身份头 ──────────────────────────────────────────────────────────────────────────────────────
118
153
  /**
@@ -120,105 +155,204 @@ export function describeBadContentOrigin(v) {
120
155
  * `assertionHeader` 不收。所以读后的形**结构上不可能同带两键**;同设(一份声明给出两个答案)由调用方在读之前对**原始**传输判(判的是写了什么,不是读后留下什么)。
121
156
  */
122
157
  export function readMcpIdentityHeaders(transport) {
158
+ const ids = identityFieldsOf(transport);
159
+ if (!ids.ok)
160
+ return {}; // 读不出 ⇒ 身份头整体读不出(传输读器对同样的输入是整只不交)
161
+ const { principalHeader, assertionHeader } = ids.value;
162
+ return {
163
+ ...(typeof principalHeader === "string" ? { principalHeader } : {}),
164
+ ...(typeof assertionHeader === "string" ? { assertionHeader } : {}),
165
+ };
166
+ }
167
+ /**
168
+ * 两个身份头的**唯一读法**(公开读者与传输快照共用):只有 `principalHeader` **干净地读成缺席**时才读 `assertionHeader`;`principalHeader` 在场(不论值形)⇒ `assertionHeader` 一次不读;
169
+ * 任何一个读不出 ⇒ 整体读不出(`ok:false`,向上传),不折成 `undefined` —— 不然「principal 读不出」会落到 assertion、读它的 getter。
170
+ * **只读自有键**(身份只认自有键):键只靠继承可见(含 `Object.prototype` 被污染成数据值)⇒ 不读它的值、当缺席,但**记下**(`principalInherited` / `assertionInherited`)—— 传输读器据此点名,
171
+ * 身份轴上不静默;公开读者没有 `lost` 通道,只读值。
172
+ */
173
+ function identityFieldsOf(record) {
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 } };
179
+ }
180
+ const assertion = readKey(record, "assertionHeader");
181
+ if (assertion.presence === "unreadable")
182
+ return { ok: false, sentence: UNINSPECTABLE };
123
183
  return {
124
- ...(typeof transport.principalHeader === "string" ? { principalHeader: transport.principalHeader } : {}),
125
- ...(transport.principalHeader === undefined && typeof transport.assertionHeader === "string" ? { assertionHeader: transport.assertionHeader } : {}),
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
+ },
126
191
  };
127
192
  }
128
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
+ });
129
199
  /**
130
200
  * 请求面 `transport` 的**唯一读器**:读器投影 transport 用它,发送前 / 受理时的判形读它报的 `lost` —— 同一只函数两个消费点,「读器丢了什么」与「门拒什么」不可能分岔。
131
201
  * · 硬坏形(`transport` 不是对象 / `kind` 出闭集 / stdio 缺 `command` / http 的 `url` 不是 http(s))⇒ `ok:false`,`path` 是读器点名的键路径;
132
202
  * · 软坏形(`args` 不是串数组 / `env` `headers` 不是纯数据的串记录 / `principalHeader` 非串)⇒ 读器**丢这个键**(重放存量提交体时的升级前读法),并把它记进 `lost`
133
203
  * (受理新提交的门据它响亮拒)。`assertionHeader` 不在 `lost` 里:两个身份头的键组合与 `assertionHeader` 的值形由调用方在读之前对原始传输判。
134
- * · `env` / `headers` 只收**纯数据**的记录({@link readPlainStringRecord}):原型必须是 `Object.prototype` 或 `null`(`Map`、`Set`、类实例整张不收),每个自有成员的值都是串;
204
+ * · `env` / `headers` 只收**纯数据**的记录({@link readPlainStringRecord}):必须是朴素 JSON 对象(`./plain-json-object` 的判据;`Map`、`Set`、类实例整张不收),每个自有成员的值都是串;
135
205
  * 交出的是**副本**(不是调用方的对象,改原对象不影响读后的形)。名叫 `__proto__` 的成员(JSON 文本铸得出这个自有键,对象字面量写不出)不进副本、单独记进 `lost`。
136
206
  * 坏形的 `lost` 条目带机读位(见 {@link McpEntryKeyIssue})。
137
207
  * url 的拒句不回显值:它常规地带凭据(userinfo)。
138
208
  */
139
209
  export function readRequestMcpTransport(t) {
140
- if (!t || typeof t !== "object")
141
- return transportBad("transport", `must be an object with a "kind" of "stdio" or "http" — got ${describeValueShape(t)}`);
142
- const tr = t;
143
- const lost = [];
144
- if (tr.kind === "stdio") {
145
- if (typeof tr.command !== "string" || tr.command.length === 0)
146
- return transportBad("transport.command", `must be a non-empty string (the command to run) — got ${describeValueShape(tr.command)}`);
147
- if (tr.args !== undefined && !isStringArray(tr.args))
148
- lost.push({ path: "transport.args", reason: `must be an array of strings — got ${describeNotStringArray(tr.args)}` });
149
- const env = readTransportRecord("transport.env", tr.env, lost);
150
- return {
151
- ok: true,
152
- transport: {
153
- kind: "stdio",
154
- command: tr.command,
155
- ...(isStringArray(tr.args) ? { args: tr.args } : {}),
156
- ...(env !== undefined ? { env } : {}),
157
- },
158
- lost,
159
- };
210
+ const shape = jsonShapeOf(t); // 总:检视就抛的对象(已撤销的 Proxy、原型陷阱抛)座位答 `uninspectable` ⇒ 下面「检视不了」那一臂,整只不交、不抛
211
+ switch (shape) {
212
+ case "plain":
213
+ case "array":
214
+ break; // 数组走下面 `kind` 缺席那一臂(JSON 面的 path / 句子逐字不变)
215
+ case "null":
216
+ case "primitive":
217
+ return transportBad("transport", `must be an object with a "kind" of "stdio" or "http" — got ${describeValueShape(t)}`);
218
+ case "uninspectable":
219
+ return transportBad("transport", `must be an object with a "kind" of "stdio" or "http" — got ${UNINSPECTABLE}`); // 读形状就抛(已撤销的 Proxy):全包唯一的「检视不了」那句
220
+ case "non_plain_object":
221
+ // 是对象但不朴素 ⇒ 硬拒;函数仍说 `a function`(14.5.0 的句子),别的非朴素对象说「不是朴素数据」
222
+ return transportBad("transport", `must be an object with a "kind" of "stdio" or "http" — got ${typeof t === "function" ? describeValueShape(t) : "an object that is not plain data"}`);
223
+ default: {
224
+ const unhandled = shape;
225
+ throw new Error(`readRequestMcpTransport: unhandled shape ${String(unhandled)}`);
226
+ }
160
227
  }
161
- if (tr.kind === "http") {
162
- // http(s) only — never file://, never a bare host (a syntactic guard, not the URL shape judge).
163
- if (typeof tr.url !== "string" || !startsWithHttpScheme(tr.url))
164
- return transportBad("transport.url", "must be an absolute http(s) URL (put credentials in transport.headers, never in the URL)");
165
- const ids = readMcpIdentityHeaders(tr);
166
- const headers = readTransportRecord("transport.headers", tr.headers, lost);
167
- if (tr.principalHeader !== undefined && ids.principalHeader === undefined) {
168
- lost.push({ path: "transport.principalHeader", reason: `must be a string (the HTTP header NAME the end-user principal is injected into) — got ${describeValueShape(tr.principalHeader)}` });
228
+ const tr = t;
229
+ // 字段只读一次:经读内核把「这一种传输要读的字段」读成局部快照(描述符 / `has` 陷阱抛的 Proxy / 会抛的自有 getter ⇒ 整只不交,不抛;`get` 陷阱不被问),之后只读快照;这一种传输用不到的字段一次也不读
230
+ const snapshotRead = snapshotTransport(tr);
231
+ if (!snapshotRead.ok)
232
+ return transportBad("transport", `must be an object with a "kind" of "stdio" or "http" — got ${snapshotRead.sentence}`);
233
+ const snap = snapshotRead.value;
234
+ // `lost` 不用 `push` 累加(`push` 是 [[Set]],会被 `Object.prototype` 上继承的数字键 setter 吞掉 / 读成污染值):每处诊断是一个值或 `undefined`,由 `issuesOf` 一次铸成数组(定义语义)
235
+ switch (snap.kind) {
236
+ case "stdio": {
237
+ if (typeof snap.command !== "string" || snap.command.length === 0)
238
+ return transportBad("transport.command", `must be a non-empty string (the command to run) — got ${describeValueShape(snap.command)}`);
239
+ const rawArgs = snap.args; // 交出去的是副本(不是调用方数组的原引用),稀疏 / 读不出的数组整键丢、进 `lost`
240
+ const args = rawArgs === undefined ? undefined : captureStringArray(rawArgs, "wide");
241
+ const argsIssue = rawArgs !== undefined && args === undefined ? { path: "transport.args", reason: `must be an array of strings — got ${describeNotStringArray(rawArgs)}` } : undefined;
242
+ const env = readTransportRecord("transport.env", snap.env);
243
+ return {
244
+ ok: true,
245
+ transport: {
246
+ kind: "stdio",
247
+ command: snap.command,
248
+ ...(args !== undefined ? { args } : {}),
249
+ ...(env.record !== undefined ? { env: env.record } : {}),
250
+ },
251
+ lost: issuesOf(argsIssue, ...env.issues),
252
+ };
253
+ }
254
+ case "http": {
255
+ // http(s) only — never file://, never a bare host (a syntactic guard, not the URL shape judge: it also lets a bare `https://` through, see ./url-shape).
256
+ if (typeof snap.url !== "string" || !startsWithHttpScheme(snap.url))
257
+ return transportBad("transport.url", "must be an absolute http(s) URL (put credentials in transport.headers, never in the URL)");
258
+ const ids = readMcpIdentityHeaders({ principalHeader: snap.principalHeader, assertionHeader: snap.assertionHeader });
259
+ const headers = readTransportRecord("transport.headers", snap.headers);
260
+ const principalIssue = snap.principalHeader !== undefined && ids.principalHeader === undefined
261
+ ? { path: "transport.principalHeader", reason: `must be a string (the HTTP header NAME the end-user principal is injected into) — got ${describeValueShape(snap.principalHeader)}` }
262
+ : undefined;
263
+ return {
264
+ ok: true,
265
+ transport: {
266
+ kind: "http",
267
+ url: snap.url,
268
+ ...(headers.record !== undefined ? { headers: headers.record } : {}),
269
+ ...ids,
270
+ },
271
+ lost: issuesOf(...headers.issues, principalIssue, snap.principalInherited ? inheritedIdentityIssue("principalHeader") : undefined, snap.assertionInherited ? inheritedIdentityIssue("assertionHeader") : undefined),
272
+ };
273
+ }
274
+ case "other":
275
+ return transportBad("transport.kind", `must be "stdio" or "http" — got ${describeValueShape(snap.value)}`);
276
+ default: {
277
+ const unhandled = snap;
278
+ throw new Error(`readRequestMcpTransport: unhandled transport kind ${String(unhandled.kind)}`);
169
279
  }
170
- return {
171
- ok: true,
172
- transport: {
173
- kind: "http",
174
- url: tr.url,
175
- ...(headers !== undefined ? { headers } : {}),
176
- ...ids,
177
- },
178
- lost,
179
- };
180
280
  }
181
- return transportBad("transport.kind", `must be "stdio" or "http" — got ${describeValueShape(tr.kind)}`);
281
+ }
282
+ function snapshotTransport(tr) {
283
+ // 九字段一律只读**自有**属性(一条律):JSON 解析只铸自有键,JSON 面不变;原型链上被污染的值 / 会抛的 getter 不触发,注入的值进不来。
284
+ // 任何一个字段读不出 ⇒ 整只读不出(第一个读不出的就停,后面的一个不读);身份头经 `identityFieldsOf`(与公开读者同一只)。
285
+ const kind = ownValueOf(tr, "kind");
286
+ if (!kind.ok)
287
+ return kind;
288
+ if (kind.value === "stdio") {
289
+ const fields = ownFieldsOf(tr, ["command", "args", "env"]);
290
+ if (!fields.ok)
291
+ return fields;
292
+ return { ok: true, value: { kind: "stdio", command: fields.value.command, args: fields.value.args, env: fields.value.env } };
293
+ }
294
+ if (kind.value === "http") {
295
+ const fields = ownFieldsOf(tr, ["url", "headers"]);
296
+ if (!fields.ok)
297
+ return fields;
298
+ const ids = identityFieldsOf(tr);
299
+ if (!ids.ok)
300
+ return ids;
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 } };
302
+ }
303
+ return { ok: true, value: { kind: "other", value: kind.value } };
182
304
  }
183
305
  // ── 「名 → 串」记录的读法(`env` / `headers`)────────────────────────────────────────────────────────
184
306
  /** 引擎的声明收据不收的成员名(core `mcp-server-spec`:按赋值拷贝它会变成原型):JSON 文本铸得出这个**自有**键,对象字面量写不出。 */
185
307
  const RESERVED_MEMBER_NAME = "__proto__";
186
308
  const RESERVED_MEMBER_FAULT = `no member of a declaration may be named "${RESERVED_MEMBER_NAME}"`;
309
+ /** 记录不交时 `detail` 的前半句:它是哪一种不朴素(null / 数组 / 原始值各一句,对象里的非朴素一句)。`shape` 穷举 —— 新词不补 = 编译红。 */
310
+ function notPlainDetail(shape, v) {
311
+ switch (shape) {
312
+ case "null":
313
+ return "got null";
314
+ case "array":
315
+ return "got an array";
316
+ case "primitive":
317
+ return `got a ${typeof v}`;
318
+ case "uninspectable":
319
+ return `got ${UNINSPECTABLE}`; // 读形状就抛(已撤销的 Proxy):全包唯一的「检视不了」那句
320
+ case "non_plain_object":
321
+ if (typeof v === "function")
322
+ return "got a function"; // 函数归 non_plain_object,句子仍是 14.5.0 的那一句
323
+ return "got an object that is not plain data (a class instance, a Map, process.env, or any object whose prototype has a prototype of its own; copy it into a plain object)";
324
+ default: {
325
+ const unhandled = shape;
326
+ throw new Error(`notPlainDetail: unhandled shape ${String(unhandled)}`);
327
+ }
328
+ }
329
+ }
187
330
  /**
188
- * 读一份「名 → 串」记录。记录交不交:是对象(不是数组 / `null`),原型是 `Object.prototype` 或 `null`(纯数据:`Map`、`Set`、类实例、建在原型上的记录都不是),
189
- * 且每个成员的值都是串。成员 = `Object.getOwnPropertyNames` 的每一个名字(不可枚举的也算 —— 引擎的收据按全部自有属性名读),值按属性描述读:数据属性取值,访问器调一次(访问器或描述 trap 抛了同判为坏)。
331
+ * 读一份「名 → 串」记录。记录交不交:是朴素 JSON 对象(座位的判据,见 `./plain-json-object`:原型是 `null`,或原型自己的原型是 `null`;纯数据 —— `Map`、`Set`、类实例、建在原型上的记录都不是),
332
+ * 且每个成员的值都是串。成员 = `Object.getOwnPropertyNames` 的每一个名字(不可枚举的也算 —— 引擎的收据按全部自有属性名读),值经内核 `readKey` 读(一份描述符:数据属性取值,访问器调一次,不问 `get` 陷阱;访问器或描述 trap 抛了同判为坏)。
190
333
  * 读原型与成员名时代理 trap 抛了 ⇒ 整张不交(读器对调用方递来的任何对象都不抛;`JSON` 存体里不会有代理,这条只落在直接调读器的嵌入方身上)。
191
334
  * 交出去的是副本(原型 `Object.prototype`、成员全可枚举),调用方的对象不被交出去、也不被改;名叫 `__proto__` 的成员不进副本,`reservedMember` 记下它。
192
335
  * 名叫 `__proto__` 而值也坏的成员,句子一次说全两件事(名坏在前、值坏并列):调用方照值改完再撞第二条是两步才改对。
193
336
  */
194
337
  function readPlainStringRecord(v) {
195
- if (v === null || typeof v !== "object" || Array.isArray(v))
196
- return { ok: false, fault: "not_plain_record", detail: v === null ? "got null" : Array.isArray(v) ? "got an array" : `got a ${typeof v}` };
197
- let proto;
198
- let names;
199
- try {
200
- proto = Object.getPrototypeOf(v);
201
- names = Object.getOwnPropertyNames(v);
202
- }
203
- catch {
204
- // 代理的 trap 抛了:读不出原型与成员名的对象不是纯数据。读器不抛出去 —— 重放存量提交体的腿被一次抛错砖死比整键不交糟,而「整键不交」是响亮的(进 `lost`)。
205
- return { ok: false, fault: "not_plain_record", detail: "got an object that could not be inspected (a proxy trap threw)" };
206
- }
207
- if (proto !== Object.prototype && proto !== null) {
208
- return { ok: false, fault: "not_plain_record", detail: "got an object that is not plain data (a class instance, a Map, process.env, or a record built on a prototype; copy it into a plain object)" };
209
- }
210
- const record = {};
338
+ // 形状归座位旁的分类(不在这里再推一遍;座位总 —— 读形状就抛的对象答 `uninspectable`)。成员名的读取(`ownKeys` 陷阱抛)读不出的对象也不是纯数据。
339
+ // 读器不抛出去 —— 重放存量提交体的腿被一次抛错砖死比整键不交糟,而「整键不交」是响亮的(进 `lost`)。检视律与句都在 `inspect`。
340
+ const shape = jsonShapeOf(v);
341
+ if (shape !== "plain")
342
+ return { ok: false, fault: "not_plain_record", detail: notPlainDetail(shape, v) };
343
+ const namesRead = inspect(() => Object.getOwnPropertyNames(v));
344
+ if (!namesRead.ok)
345
+ return { ok: false, fault: "not_plain_record", detail: `got ${namesRead.sentence}` };
346
+ const names = namesRead.value;
347
+ // 累加器是零原型的袋、成员用 `Object.defineProperty` + 零原型描述符写(定义语义,不走 [[Set]]),最后用对象展开(同为定义语义)铸成普通对象:原型上被污染成只读的同名键不让读器抛,
348
+ // 原型上的数字键 setter 吞不掉成员(此前 `entries.push` + `Object.fromEntries` 被继承的数字键 getter / setter 弄抛)
349
+ const members = Object.create(null);
211
350
  let reservedMember = false;
212
351
  for (const name of names) {
213
- let value;
214
- let unreadable = false;
215
- try {
216
- const d = Object.getOwnPropertyDescriptor(v, name);
217
- value = d === undefined ? undefined : Object.hasOwn(d, "value") ? d.value : d.get === undefined ? undefined : Reflect.apply(d.get, v, []);
218
- }
219
- catch {
220
- unreadable = true;
221
- }
352
+ // 成员经内核读(契约见函数头注):一份描述符里取值 —— 数据属性取值、访问器调一次、从不问 Proxy 的 `get` 陷阱;成员在两次读之间被删掉 ⇒ 不是串(不读继承来的值)
353
+ const read = readKey(v, name);
354
+ const unreadable = read.presence === "unreadable";
355
+ const value = read.presence === "own" ? read.value : undefined;
222
356
  if (unreadable || typeof value !== "string") {
223
357
  const valueFault = unreadable ? "could not be read (its getter threw)" : `must be a string — got ${describeValueShape(value)}`;
224
358
  const reserved = name === RESERVED_MEMBER_NAME;
@@ -232,23 +366,28 @@ function readPlainStringRecord(v) {
232
366
  if (name === RESERVED_MEMBER_NAME)
233
367
  reservedMember = true;
234
368
  else
235
- record[name] = value;
369
+ Object.defineProperty(members, name, dataDescriptor(value));
236
370
  }
237
- return { ok: true, record, reservedMember };
371
+ return { ok: true, record: { ...members }, reservedMember };
238
372
  }
373
+ /** 把诊断条目铸成数组:`undefined`(这一处没有诊断)略去。`filter` 的结果走 `CreateDataProperty`,不走 `[[Set]]` —— `lost` 是「门拒什么」的读数,不能被继承的数字键 setter 吞掉。 */
374
+ const issuesOf = (...candidates) => candidates.filter((c) => c !== undefined);
239
375
  /**
240
- * `transport.env` / `transport.headers` 的读法(两键同一只):缺席 ⇒ 缺席;记录不交 ⇒ `lost` 一处点名**这个键**(不交的是整张记录;坏在某个成员时句子与 `member` 点名它);
241
- * 交 ⇒ 纯数据副本,名叫 `__proto__` 的成员在 `lost` 里单记一处、路径 `<键>.__proto__`。
376
+ * `transport.env` / `transport.headers` 的读法(两键同一只):缺席 ⇒ 缺席;记录不交 ⇒ 一处诊断点名**这个键**(不交的是整张记录;坏在某个成员时句子与 `member` 点名它);
377
+ * 交 ⇒ 纯数据副本,名叫 `__proto__` 的成员单记一处诊断、路径 `<键>.__proto__`。
242
378
  */
243
- function readTransportRecord(path, raw, lost) {
379
+ function readTransportRecord(path, raw) {
244
380
  if (raw === undefined)
245
- return undefined;
381
+ return { record: undefined, issues: [] };
246
382
  const read = readPlainStringRecord(raw);
247
383
  if (!read.ok) {
248
- lost.push({ path, reason: `must be a plain object of string values — ${read.detail}`, ...(read.member !== undefined ? { member: read.member } : {}), fault: read.fault });
249
- return undefined;
384
+ return {
385
+ record: undefined,
386
+ issues: [{ path, reason: `must be a plain object of string values — ${read.detail}`, ...(read.member !== undefined ? { member: read.member } : {}), fault: read.fault }],
387
+ };
250
388
  }
251
- if (read.reservedMember)
252
- lost.push({ path: `${path}.${RESERVED_MEMBER_NAME}`, reason: RESERVED_MEMBER_FAULT, member: RESERVED_MEMBER_NAME, fault: "member_name_reserved" });
253
- return read.record;
389
+ return {
390
+ record: read.record,
391
+ issues: read.reservedMember ? [{ path: `${path}.${RESERVED_MEMBER_NAME}`, reason: RESERVED_MEMBER_FAULT, member: RESERVED_MEMBER_NAME, fault: "member_name_reserved" }] : [],
392
+ };
254
393
  }