@sema-agent/settings-schema 14.4.0 → 14.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +303 -0
- package/README.md +22 -1
- package/conformance/VERSION +1 -1
- package/dist/api/scopes.d.ts +1 -1
- package/dist/api/scopes.js +1 -1
- package/dist/atomic-write.d.ts +11 -0
- package/dist/atomic-write.js +38 -5
- package/dist/bundle.d.ts +1 -1
- package/dist/bundle.js +1 -1
- package/dist/config-d-file.d.ts +4 -0
- package/dist/config-d-file.js +11 -4
- package/dist/config-dir-io.js +12 -2
- package/dist/config-dir-migration.d.ts +44 -0
- package/dist/config-dir-migration.js +91 -36
- package/dist/config-fns.js +3 -9
- package/dist/conformance/canonical.js +4 -7
- package/dist/conformance/corpus.js +3 -3
- package/dist/conformance/derive.js +6 -5
- package/dist/conformance/run.js +2 -1
- package/dist/definition-copy.d.ts +23 -0
- package/dist/definition-copy.js +52 -0
- package/dist/file-store.d.ts +1 -31
- package/dist/file-store.js +3 -30
- package/dist/fleet.js +2 -1
- package/dist/fs-failure.d.ts +17 -2
- package/dist/fs-failure.js +55 -15
- package/dist/hooks-config.d.ts +2 -2
- package/dist/hooks-config.js +112 -31
- package/dist/http-hook-allowlists.d.ts +7 -3
- package/dist/http-hook-allowlists.js +102 -16
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -1
- package/dist/mcp-request-entry.d.ts +9 -6
- package/dist/mcp-request-entry.js +225 -93
- package/dist/node.d.ts +2 -1
- package/dist/node.js +2 -1
- package/dist/one-time-migrations.js +36 -41
- package/dist/plain-json-object.d.ts +31 -0
- package/dist/plain-json-object.js +52 -0
- package/dist/portable-domains.d.ts +31 -0
- package/dist/portable-domains.js +30 -0
- package/dist/safety-merge-spec.d.ts +88 -6
- package/dist/safety-merge-spec.js +233 -17
- package/dist/scheduler-store.js +3 -2
- package/dist/settings-units.d.ts +85 -0
- package/dist/settings-units.js +175 -0
- package/dist/types.d.ts +4 -0
- package/dist/types.js +61 -53
- package/dist/url-credential.d.ts +41 -0
- package/dist/url-credential.js +95 -0
- package/dist/url-shape.d.ts +11 -5
- package/dist/url-shape.js +11 -5
- package/dist/workers-rows.d.ts +2 -1
- package/dist/workers-rows.js +5 -4
- package/docs/API-SURFACE.md +30 -4
- package/docs/CONSUMER-INTEGRATION.md +2 -0
- package/llms-full.txt +24 -2
- package/llms.txt +6 -2
- package/package.json +2 -3
|
@@ -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,34 +24,60 @@
|
|
|
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
29
|
import { MCP_CONTENT_ORIGINS } from "./types.js";
|
|
28
30
|
import { startsWithHttpScheme } from "./url-shape.js";
|
|
29
31
|
// ── 值的种类描述(拒句只说「你写的是什么形」,不回显值:值可能带凭据、可能任意长)──────────────────────────────────────
|
|
30
|
-
/**
|
|
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 };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/** 三只描述器的壳:检视抛了就说「检视不了」那一句(经 {@link inspect})。 */
|
|
47
|
+
function inspectSafely(describe) {
|
|
48
|
+
const read = inspect(describe);
|
|
49
|
+
return read.ok ? read.value : read.sentence;
|
|
50
|
+
}
|
|
51
|
+
/** 值的**种类**(不回显值本身)。总(不抛):经 {@link inspectSafely}。 */
|
|
31
52
|
export function describeValueShape(v) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
-
|
|
53
|
+
return inspectSafely(() => {
|
|
54
|
+
if (v === undefined)
|
|
55
|
+
return "nothing (the key is absent)";
|
|
56
|
+
if (v === null)
|
|
57
|
+
return "null";
|
|
58
|
+
if (Array.isArray(v))
|
|
59
|
+
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
|
+
});
|
|
66
|
+
}
|
|
67
|
+
/** 「不是字符串数组」的种类:数组里混了非串元素说那一句,否则回落到 {@link describeValueShape}。总(不抛):经 {@link inspectSafely}。 */
|
|
45
68
|
export function describeNotStringArray(v) {
|
|
46
|
-
return Array.isArray(v) ? "an array with a non-string element" : describeValueShape(v);
|
|
69
|
+
return inspectSafely(() => (Array.isArray(v) ? "an array with a non-string element" : describeValueShape(v)));
|
|
47
70
|
}
|
|
48
|
-
/** 「不是字符串值的对象」的种类:对象里混了非串值说那一句,否则回落到 {@link describeValueShape}。 */
|
|
71
|
+
/** 「不是字符串值的对象」的种类:对象里混了非串值说那一句,否则回落到 {@link describeValueShape}。总(不抛):经 {@link inspectSafely}。 */
|
|
49
72
|
export function describeNotStringRecord(v) {
|
|
50
|
-
return
|
|
73
|
+
return inspectSafely(() => (isPlainJsonObject(v) ? "an object with a non-string value" : describeValueShape(v)));
|
|
51
74
|
}
|
|
52
|
-
/**
|
|
75
|
+
/**
|
|
76
|
+
* 字符串数组(空数组合法)。与传输读器读 `args` 是**同一只读法**(`./definition-copy` 的 `captureStringArray`:按下标、自有下标、一次读、非串即坏、不走迭代协议):
|
|
77
|
+
* 稀疏数组(洞)、`length` 读不出安全非负整数的数组、读元素时抛的数组都不是 —— 洞不顺原型链读到 `Object.prototype` / `Array.prototype` 上的数字键。
|
|
78
|
+
*/
|
|
53
79
|
export function isStringArray(v) {
|
|
54
|
-
return
|
|
80
|
+
return captureStringArray(v) !== undefined;
|
|
55
81
|
}
|
|
56
82
|
/**
|
|
57
83
|
* 纯数据的「名 → 串」记录(空对象合法;数组、`Map`、类实例、自有 `__proto__` 成员、有非串成员的对象都不是)。与传输读器读 `env` / `headers` 是**同一个**定义
|
|
@@ -61,9 +87,6 @@ export function isStringRecord(v) {
|
|
|
61
87
|
const read = readPlainStringRecord(v);
|
|
62
88
|
return read.ok && !read.reservedMember;
|
|
63
89
|
}
|
|
64
|
-
function isRecord(v) {
|
|
65
|
-
return v !== null && typeof v === "object" && !Array.isArray(v);
|
|
66
|
-
}
|
|
67
90
|
// ── 调用方写的串的回显口径 ──────────────────────────────────────────────────────────────────────────
|
|
68
91
|
/** 句子里点名一个值的写法:{@link echoCallerText} 外加一对双引号;内部的引号与反斜杠逐字(不转义)。 */
|
|
69
92
|
function quotedCallerText(text) {
|
|
@@ -120,94 +143,198 @@ export function describeBadContentOrigin(v) {
|
|
|
120
143
|
* `assertionHeader` 不收。所以读后的形**结构上不可能同带两键**;同设(一份声明给出两个答案)由调用方在读之前对**原始**传输判(判的是写了什么,不是读后留下什么)。
|
|
121
144
|
*/
|
|
122
145
|
export function readMcpIdentityHeaders(transport) {
|
|
146
|
+
const ids = identityFieldsOf(transport);
|
|
147
|
+
if (!ids.ok)
|
|
148
|
+
return {}; // 读不出 ⇒ 身份头整体读不出(传输读器对同样的输入是整只不交)
|
|
149
|
+
const { principalHeader, assertionHeader } = ids.value;
|
|
123
150
|
return {
|
|
124
|
-
...(typeof
|
|
125
|
-
...(
|
|
151
|
+
...(typeof principalHeader === "string" ? { principalHeader } : {}),
|
|
152
|
+
...(typeof assertionHeader === "string" ? { assertionHeader } : {}),
|
|
126
153
|
};
|
|
127
154
|
}
|
|
155
|
+
/**
|
|
156
|
+
* 两个身份头的**唯一读法**(公开读者与传输快照共用):只有 `principalHeader` **干净地读成缺席**时才读 `assertionHeader`;`principalHeader` 在场(不论值形)⇒ `assertionHeader` 一次不读;
|
|
157
|
+
* 任何一个读不出 ⇒ 整体读不出(`ok:false`,向上传),不折成 `undefined` —— 不然「principal 读不出」会落到 assertion、读它的 getter。
|
|
158
|
+
*/
|
|
159
|
+
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));
|
|
183
|
+
}
|
|
184
|
+
return { ok: true, value: out };
|
|
185
|
+
}
|
|
128
186
|
const transportBad = (path, reason) => ({ ok: false, issue: { path, reason } });
|
|
129
187
|
/**
|
|
130
188
|
* 请求面 `transport` 的**唯一读器**:读器投影 transport 用它,发送前 / 受理时的判形读它报的 `lost` —— 同一只函数两个消费点,「读器丢了什么」与「门拒什么」不可能分岔。
|
|
131
189
|
* · 硬坏形(`transport` 不是对象 / `kind` 出闭集 / stdio 缺 `command` / http 的 `url` 不是 http(s))⇒ `ok:false`,`path` 是读器点名的键路径;
|
|
132
190
|
* · 软坏形(`args` 不是串数组 / `env` `headers` 不是纯数据的串记录 / `principalHeader` 非串)⇒ 读器**丢这个键**(重放存量提交体时的升级前读法),并把它记进 `lost`
|
|
133
191
|
* (受理新提交的门据它响亮拒)。`assertionHeader` 不在 `lost` 里:两个身份头的键组合与 `assertionHeader` 的值形由调用方在读之前对原始传输判。
|
|
134
|
-
* · `env` / `headers` 只收**纯数据**的记录({@link readPlainStringRecord})
|
|
192
|
+
* · `env` / `headers` 只收**纯数据**的记录({@link readPlainStringRecord}):必须是朴素 JSON 对象(`./plain-json-object` 的判据;`Map`、`Set`、类实例整张不收),每个自有成员的值都是串;
|
|
135
193
|
* 交出的是**副本**(不是调用方的对象,改原对象不影响读后的形)。名叫 `__proto__` 的成员(JSON 文本铸得出这个自有键,对象字面量写不出)不进副本、单独记进 `lost`。
|
|
136
194
|
* 坏形的 `lost` 条目带机读位(见 {@link McpEntryKeyIssue})。
|
|
137
195
|
* url 的拒句不回显值:它常规地带凭据(userinfo)。
|
|
138
196
|
*/
|
|
139
197
|
export function readRequestMcpTransport(t) {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
transport:
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
},
|
|
158
|
-
lost,
|
|
159
|
-
};
|
|
198
|
+
const shape = jsonShapeOf(t); // 总:检视就抛的对象(已撤销的 Proxy、原型陷阱抛)座位答 `uninspectable` ⇒ 下面「检视不了」那一臂,整只不交、不抛
|
|
199
|
+
switch (shape) {
|
|
200
|
+
case "plain":
|
|
201
|
+
case "array":
|
|
202
|
+
break; // 数组走下面 `kind` 缺席那一臂(JSON 面的 path / 句子逐字不变)
|
|
203
|
+
case "null":
|
|
204
|
+
case "primitive":
|
|
205
|
+
return transportBad("transport", `must be an object with a "kind" of "stdio" or "http" — got ${describeValueShape(t)}`);
|
|
206
|
+
case "uninspectable":
|
|
207
|
+
return transportBad("transport", `must be an object with a "kind" of "stdio" or "http" — got ${UNINSPECTABLE}`); // 读形状就抛(已撤销的 Proxy):全包唯一的「检视不了」那句
|
|
208
|
+
case "non_plain_object":
|
|
209
|
+
// 是对象但不朴素 ⇒ 硬拒;函数仍说 `a function`(14.5.0 的句子),别的非朴素对象说「不是朴素数据」
|
|
210
|
+
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"}`);
|
|
211
|
+
default: {
|
|
212
|
+
const unhandled = shape;
|
|
213
|
+
throw new Error(`readRequestMcpTransport: unhandled shape ${String(unhandled)}`);
|
|
214
|
+
}
|
|
160
215
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
216
|
+
const tr = t;
|
|
217
|
+
// 字段只读一次:同一个检视里把「这一种传输要读的字段」读成局部快照(`get` 陷阱抛的 Proxy / 会抛的自有 getter ⇒ 整只不交,不抛),之后只读快照;这一种传输用不到的字段一次也不读
|
|
218
|
+
const snapshotRead = snapshotTransport(tr);
|
|
219
|
+
if (!snapshotRead.ok)
|
|
220
|
+
return transportBad("transport", `must be an object with a "kind" of "stdio" or "http" — got ${snapshotRead.sentence}`);
|
|
221
|
+
const snap = snapshotRead.value;
|
|
222
|
+
// `lost` 不用 `push` 累加(`push` 是 [[Set]],会被 `Object.prototype` 上继承的数字键 setter 吞掉 / 读成污染值):每处诊断是一个值或 `undefined`,由 `issuesOf` 一次铸成数组(定义语义)
|
|
223
|
+
switch (snap.kind) {
|
|
224
|
+
case "stdio": {
|
|
225
|
+
if (typeof snap.command !== "string" || snap.command.length === 0)
|
|
226
|
+
return transportBad("transport.command", `must be a non-empty string (the command to run) — got ${describeValueShape(snap.command)}`);
|
|
227
|
+
const rawArgs = snap.args; // 交出去的是副本(不是调用方数组的原引用),稀疏 / 读不出的数组整键丢、进 `lost`
|
|
228
|
+
const args = rawArgs === undefined ? undefined : captureStringArray(rawArgs);
|
|
229
|
+
const argsIssue = rawArgs !== undefined && args === undefined ? { path: "transport.args", reason: `must be an array of strings — got ${describeNotStringArray(rawArgs)}` } : undefined;
|
|
230
|
+
const env = readTransportRecord("transport.env", snap.env);
|
|
231
|
+
return {
|
|
232
|
+
ok: true,
|
|
233
|
+
transport: {
|
|
234
|
+
kind: "stdio",
|
|
235
|
+
command: snap.command,
|
|
236
|
+
...(args !== undefined ? { args } : {}),
|
|
237
|
+
...(env.record !== undefined ? { env: env.record } : {}),
|
|
238
|
+
},
|
|
239
|
+
lost: issuesOf(argsIssue, ...env.issues),
|
|
240
|
+
};
|
|
169
241
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
242
|
+
case "http": {
|
|
243
|
+
// 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).
|
|
244
|
+
if (typeof snap.url !== "string" || !startsWithHttpScheme(snap.url))
|
|
245
|
+
return transportBad("transport.url", "must be an absolute http(s) URL (put credentials in transport.headers, never in the URL)");
|
|
246
|
+
const ids = readMcpIdentityHeaders({ principalHeader: snap.principalHeader, assertionHeader: snap.assertionHeader });
|
|
247
|
+
const headers = readTransportRecord("transport.headers", snap.headers);
|
|
248
|
+
const principalIssue = snap.principalHeader !== undefined && ids.principalHeader === undefined
|
|
249
|
+
? { path: "transport.principalHeader", reason: `must be a string (the HTTP header NAME the end-user principal is injected into) — got ${describeValueShape(snap.principalHeader)}` }
|
|
250
|
+
: undefined;
|
|
251
|
+
return {
|
|
252
|
+
ok: true,
|
|
253
|
+
transport: {
|
|
254
|
+
kind: "http",
|
|
255
|
+
url: snap.url,
|
|
256
|
+
...(headers.record !== undefined ? { headers: headers.record } : {}),
|
|
257
|
+
...ids,
|
|
258
|
+
},
|
|
259
|
+
lost: issuesOf(...headers.issues, principalIssue),
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
case "other":
|
|
263
|
+
return transportBad("transport.kind", `must be "stdio" or "http" — got ${describeValueShape(snap.value)}`);
|
|
264
|
+
default: {
|
|
265
|
+
const unhandled = snap;
|
|
266
|
+
throw new Error(`readRequestMcpTransport: unhandled transport kind ${String(unhandled.kind)}`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
function snapshotTransport(tr) {
|
|
271
|
+
// 九字段一律只读**自有**属性(一条律):JSON 解析只铸自有键,JSON 面不变;原型链上被污染的值 / 会抛的 getter 不触发,注入的值进不来。
|
|
272
|
+
// 任何一个字段读不出 ⇒ 整只读不出(第一个读不出的就停,后面的一个不读);身份头经 `identityFieldsOf`(与公开读者同一只)。
|
|
273
|
+
const kind = ownValueOf(tr, "kind");
|
|
274
|
+
if (!kind.ok)
|
|
275
|
+
return kind;
|
|
276
|
+
if (kind.value === "stdio") {
|
|
277
|
+
const fields = ownFieldsOf(tr, ["command", "args", "env"]);
|
|
278
|
+
if (!fields.ok)
|
|
279
|
+
return fields;
|
|
280
|
+
return { ok: true, value: { kind: "stdio", command: fields.value.command, args: fields.value.args, env: fields.value.env } };
|
|
281
|
+
}
|
|
282
|
+
if (kind.value === "http") {
|
|
283
|
+
const fields = ownFieldsOf(tr, ["url", "headers"]);
|
|
284
|
+
if (!fields.ok)
|
|
285
|
+
return fields;
|
|
286
|
+
const ids = identityFieldsOf(tr);
|
|
287
|
+
if (!ids.ok)
|
|
288
|
+
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 } };
|
|
180
290
|
}
|
|
181
|
-
return
|
|
291
|
+
return { ok: true, value: { kind: "other", value: kind.value } };
|
|
182
292
|
}
|
|
183
293
|
// ── 「名 → 串」记录的读法(`env` / `headers`)────────────────────────────────────────────────────────
|
|
184
294
|
/** 引擎的声明收据不收的成员名(core `mcp-server-spec`:按赋值拷贝它会变成原型):JSON 文本铸得出这个**自有**键,对象字面量写不出。 */
|
|
185
295
|
const RESERVED_MEMBER_NAME = "__proto__";
|
|
186
296
|
const RESERVED_MEMBER_FAULT = `no member of a declaration may be named "${RESERVED_MEMBER_NAME}"`;
|
|
297
|
+
/** 记录不交时 `detail` 的前半句:它是哪一种不朴素(null / 数组 / 原始值各一句,对象里的非朴素一句)。`shape` 穷举 —— 新词不补 = 编译红。 */
|
|
298
|
+
function notPlainDetail(shape, v) {
|
|
299
|
+
switch (shape) {
|
|
300
|
+
case "null":
|
|
301
|
+
return "got null";
|
|
302
|
+
case "array":
|
|
303
|
+
return "got an array";
|
|
304
|
+
case "primitive":
|
|
305
|
+
return `got a ${typeof v}`;
|
|
306
|
+
case "uninspectable":
|
|
307
|
+
return `got ${UNINSPECTABLE}`; // 读形状就抛(已撤销的 Proxy):全包唯一的「检视不了」那句
|
|
308
|
+
case "non_plain_object":
|
|
309
|
+
if (typeof v === "function")
|
|
310
|
+
return "got a function"; // 函数归 non_plain_object,句子仍是 14.5.0 的那一句
|
|
311
|
+
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)";
|
|
312
|
+
default: {
|
|
313
|
+
const unhandled = shape;
|
|
314
|
+
throw new Error(`notPlainDetail: unhandled shape ${String(unhandled)}`);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
187
318
|
/**
|
|
188
|
-
* 读一份「名 →
|
|
319
|
+
* 读一份「名 → 串」记录。记录交不交:是朴素 JSON 对象(座位的判据,见 `./plain-json-object`:原型是 `null`,或原型自己的原型是 `null`;纯数据 —— `Map`、`Set`、类实例、建在原型上的记录都不是),
|
|
189
320
|
* 且每个成员的值都是串。成员 = `Object.getOwnPropertyNames` 的每一个名字(不可枚举的也算 —— 引擎的收据按全部自有属性名读),值按属性描述读:数据属性取值,访问器调一次(访问器或描述 trap 抛了同判为坏)。
|
|
190
321
|
* 读原型与成员名时代理 trap 抛了 ⇒ 整张不交(读器对调用方递来的任何对象都不抛;`JSON` 存体里不会有代理,这条只落在直接调读器的嵌入方身上)。
|
|
191
322
|
* 交出去的是副本(原型 `Object.prototype`、成员全可枚举),调用方的对象不被交出去、也不被改;名叫 `__proto__` 的成员不进副本,`reservedMember` 记下它。
|
|
192
323
|
* 名叫 `__proto__` 而值也坏的成员,句子一次说全两件事(名坏在前、值坏并列):调用方照值改完再撞第二条是两步才改对。
|
|
193
324
|
*/
|
|
194
325
|
function readPlainStringRecord(v) {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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 = {};
|
|
326
|
+
// 形状归座位旁的分类(不在这里再推一遍;座位总 —— 读形状就抛的对象答 `uninspectable`)。成员名的读取(`ownKeys` 陷阱抛)读不出的对象也不是纯数据。
|
|
327
|
+
// 读器不抛出去 —— 重放存量提交体的腿被一次抛错砖死比整键不交糟,而「整键不交」是响亮的(进 `lost`)。检视律与句都在 `inspect`。
|
|
328
|
+
const shape = jsonShapeOf(v);
|
|
329
|
+
if (shape !== "plain")
|
|
330
|
+
return { ok: false, fault: "not_plain_record", detail: notPlainDetail(shape, v) };
|
|
331
|
+
const namesRead = inspect(() => Object.getOwnPropertyNames(v));
|
|
332
|
+
if (!namesRead.ok)
|
|
333
|
+
return { ok: false, fault: "not_plain_record", detail: `got ${namesRead.sentence}` };
|
|
334
|
+
const names = namesRead.value;
|
|
335
|
+
// 累加器是零原型的袋、成员用 `Object.defineProperty` + 零原型描述符写(定义语义,不走 [[Set]]),最后用对象展开(同为定义语义)铸成普通对象:原型上被污染成只读的同名键不让读器抛,
|
|
336
|
+
// 原型上的数字键 setter 吞不掉成员(此前 `entries.push` + `Object.fromEntries` 被继承的数字键 getter / setter 弄抛)
|
|
337
|
+
const members = Object.create(null);
|
|
211
338
|
let reservedMember = false;
|
|
212
339
|
for (const name of names) {
|
|
213
340
|
let value;
|
|
@@ -232,23 +359,28 @@ function readPlainStringRecord(v) {
|
|
|
232
359
|
if (name === RESERVED_MEMBER_NAME)
|
|
233
360
|
reservedMember = true;
|
|
234
361
|
else
|
|
235
|
-
|
|
362
|
+
Object.defineProperty(members, name, dataDescriptor(value));
|
|
236
363
|
}
|
|
237
|
-
return { ok: true, record, reservedMember };
|
|
364
|
+
return { ok: true, record: { ...members }, reservedMember };
|
|
238
365
|
}
|
|
366
|
+
/** 把诊断条目铸成数组:`undefined`(这一处没有诊断)略去。`filter` 的结果走 `CreateDataProperty`,不走 `[[Set]]` —— `lost` 是「门拒什么」的读数,不能被继承的数字键 setter 吞掉。 */
|
|
367
|
+
const issuesOf = (...candidates) => candidates.filter((c) => c !== undefined);
|
|
239
368
|
/**
|
|
240
|
-
* `transport.env` / `transport.headers` 的读法(两键同一只):缺席 ⇒ 缺席;记录不交 ⇒
|
|
241
|
-
* 交 ⇒ 纯数据副本,名叫 `__proto__`
|
|
369
|
+
* `transport.env` / `transport.headers` 的读法(两键同一只):缺席 ⇒ 缺席;记录不交 ⇒ 一处诊断点名**这个键**(不交的是整张记录;坏在某个成员时句子与 `member` 点名它);
|
|
370
|
+
* 交 ⇒ 纯数据副本,名叫 `__proto__` 的成员单记一处诊断、路径 `<键>.__proto__`。
|
|
242
371
|
*/
|
|
243
|
-
function readTransportRecord(path, raw
|
|
372
|
+
function readTransportRecord(path, raw) {
|
|
244
373
|
if (raw === undefined)
|
|
245
|
-
return undefined;
|
|
374
|
+
return { record: undefined, issues: [] };
|
|
246
375
|
const read = readPlainStringRecord(raw);
|
|
247
376
|
if (!read.ok) {
|
|
248
|
-
|
|
249
|
-
|
|
377
|
+
return {
|
|
378
|
+
record: undefined,
|
|
379
|
+
issues: [{ path, reason: `must be a plain object of string values — ${read.detail}`, ...(read.member !== undefined ? { member: read.member } : {}), fault: read.fault }],
|
|
380
|
+
};
|
|
250
381
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
382
|
+
return {
|
|
383
|
+
record: read.record,
|
|
384
|
+
issues: read.reservedMember ? [{ path: `${path}.${RESERVED_MEMBER_NAME}`, reason: RESERVED_MEMBER_FAULT, member: RESERVED_MEMBER_NAME, fault: "member_name_reserved" }] : [],
|
|
385
|
+
};
|
|
254
386
|
}
|
package/dist/node.d.ts
CHANGED
|
@@ -9,5 +9,6 @@ export * from "./file-edit.js";
|
|
|
9
9
|
export * from "./bundle.js";
|
|
10
10
|
export * from "./local-load.js";
|
|
11
11
|
export * from "./scheduler-store-node.js";
|
|
12
|
-
export { migrateConfigDirectory,
|
|
12
|
+
export { migrateConfigDirectory, migrateConfigDocuments, // 14.5.0:同一段「迁 → 整域终判」的同步口(入参是已有的存量文档,无 io)—— 目录形调的就是它的内核
|
|
13
|
+
type ConfigDirectoryDocument, type ConfigDirectoryIo, type ConfigDirectoryMigration, type ConfigDirectoryRefusal, type ConfigDocumentsMigration, type MigrateConfigDirectoryOptions, type MigrateConfigDocumentsOptions, } from "./config-dir-migration.js";
|
|
13
14
|
export { nodeConfigDirectoryIo } from "./config-dir-io.js";
|
package/dist/node.js
CHANGED
|
@@ -10,5 +10,6 @@ export * from "./bundle.js";
|
|
|
10
10
|
export * from "./local-load.js";
|
|
11
11
|
export * from "./scheduler-store-node.js"; // R7 self-wake scheduler store — fs binding of the ./scheduler-store contract
|
|
12
12
|
// 目录形的一次性迁移(读目录 → 迁 → 整域终判 → 原子写 → 删 retired 域文件)—— 语义只此一处,命令行是它的渲染器、引擎起服调同一只。显式列名:该模块的包内私有件(出门形/拼路径)不外泄。
|
|
13
|
-
export { migrateConfigDirectory
|
|
13
|
+
export { migrateConfigDirectory, migrateConfigDocuments, // 14.5.0:同一段「迁 → 整域终判」的同步口(入参是已有的存量文档,无 io)—— 目录形调的就是它的内核
|
|
14
|
+
} from "./config-dir-migration.js";
|
|
14
15
|
export { nodeConfigDirectoryIo } from "./config-dir-io.js"; // 上面那只函数的真 I/O 绑定:node:fs + 本包唯一的原子写器(config-document 模式规则)
|