@sema-agent/settings-schema 14.5.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 +265 -0
- package/README.md +15 -0
- package/conformance/VERSION +1 -1
- package/dist/atomic-write.js +10 -7
- package/dist/bundle.d.ts +1 -1
- package/dist/bundle.js +1 -1
- package/dist/config-dir-migration.js +2 -1
- package/dist/config-fns.js +2 -2
- 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 +12 -7
- package/dist/fs-failure.js +53 -29
- 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 +1 -0
- package/dist/index.js +2 -1
- package/dist/mcp-request-entry.d.ts +9 -6
- package/dist/mcp-request-entry.js +225 -93
- package/dist/plain-json-object.d.ts +28 -3
- package/dist/plain-json-object.js +48 -7
- 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.js +57 -53
- 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 +17 -3
- package/llms-full.txt +12 -2
- package/llms.txt +4 -2
- package/package.json +1 -1
|
@@ -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
|
}
|
|
@@ -1,6 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 包内私有(不进主桶、不进 exports 表):**「纯 JSON
|
|
3
|
-
* `Date` / `Map` / 类实例 /
|
|
4
|
-
* `
|
|
2
|
+
* 包内私有(不进主桶、不进 exports 表):**「纯 JSON 对象」的唯一谓词** —— 不是 `null`、不是数组、原型是 `null` 或「原型自己的原型是 `null`」(即 `Object.prototype`:JSON 解析出来的对象恒如此;
|
|
3
|
+
* `Date` / `Map` / 类实例 / 建在别的对象原型上的记录原型链更深,都不是)。**已知的边**:原型是某个 null 原型对象的对象(`Object.create(Object.create(null))`)、`class X extends null` 的实例,
|
|
4
|
+
* 链上第二环也是 `null`,读成朴素 —— 它们只能由程序造出,JSON 解析不出来;读者按它的**自有**成员读,继承来的成员不计(托管层 http hook 名单对只靠继承可见的名单键 fail-closed;请求面 `env` / `headers` 收成 `{}` 不修 —— 读者级遍历继承成员与谓词级再加判据都对 `Object.prototype` 上的可枚举污染敏感,而这一侧丢的是数据、失败可见、不在安全轴)。**不按「原型恒等本 realm 的 `Object.prototype`」判**:另一个 realm(`vm` 新上下文、iframe)里 `JSON.parse` 出来的普通对象,
|
|
5
|
+
* 原型是**那个 realm** 的 `Object.prototype`,按恒等判会被读成非朴素;而任何 realm 的 `Object.prototype` 自身原型都是 `null`,所以链上第二环是 `null` 这一条与 realm 无关,不需要为 realm 加特例。包里凡要问「这个值是不是一个 JSON 对象」的地方都读它,不各写一份:读域文档的键集对比(`./config-fns`)、一次性迁移的文档形判
|
|
6
|
+
* (`./one-time-migrations`)、`config.d` 文档形(`./config-d-file`)、`extraBody` 的形闸与秘密走查(`./types`)、settings 单元读法与严格 JSON 深度判(`./settings-units` / `./safety-merge-spec`)、
|
|
7
|
+
* 请求面传输的记录读法与描述件(`./mcp-request-entry`)、workers 行读口(`./workers-rows`)、有效配置线上载荷读口(`./config-fns` 的 `readEffectiveWire`)、http hook 名单的设置袋(`./http-hook-allowlists`)、
|
|
8
|
+
* fleet 线上载荷的受护判(`./fleet`)、符合性语料的比较器 / 派生 / 语料读口 / 元数据拆分(`./conformance/*`)。零 import。
|
|
9
|
+
* **形状分类核 {@link jsonShapeOf} 是座位的本体**(唯一读原型的地方),`isPlainJsonObject` 由它派生(`plain` 那一格)。**总(不抛)**:检视就抛的对象(已撤销的 Proxy、`getPrototypeOf` 陷阱抛)
|
|
10
|
+
* 核答 `uninspectable`、判官答 `false`,不抛出去 —— 全包经座位的读口因此拒而不抛,律在座位上收一处,不在每个读器各包一个 `try`;读者对 `uninspectable` 一律按最严处置(hooks ⇒ 拒整份文档;名单 ⇒ `malformed`;传输 ⇒ 整只不交)。
|
|
11
|
+
*
|
|
12
|
+
* **唯一的第二处是 `./hooks-config` 的 `hookShapeOf` / `hookIsRecord`,与 {@link jsonShapeOf} / 本函数逐字同文(调用名归一后)**:hooks 分类器的闭包要被消费方生成器整函数抄走(闭包里的自由名只有 `HOOK_EVENTS`),闭包里不能 import 本模块 ——
|
|
13
|
+
* 同文由 `test/plain-json-object-single-seat-14.6.0.test.ts` 逐字证明(漂开即红),闭包契约多注入一个名字时(S-2133)把它并回这里。
|
|
14
|
+
* 同一个门也数全包:返回类型是 `x is Record<string, unknown>` 的函数、读原型的调用点,除这两处之外一处不许有;内联的「typeof 对象 ∧ 非 null ∧ 非数组」合取则**一处都不许有**(座位里它是分开的 `if` 臂,不是合取)。
|
|
15
|
+
*
|
|
16
|
+
* **读者要的答案不只是「是不是朴素对象」时,调本模块的 {@link jsonShapeOf}**(六词形状分类),不在读者里再推一遍形状:例如托管层的 http hook 名单,非朴素的**对象**袋必须 fail-closed(读成空名单并告警),
|
|
17
|
+
* 而 `null` / 原始值 / 数组仍是「整份设置不是对象 ⇒ 两键都缺」—— 两种不同的答案,只能由形状分类给。`isPlainJsonObject` 就是 `jsonShapeOf` 的「朴素」那一格。
|
|
5
18
|
*/
|
|
6
19
|
export declare function isPlainJsonObject(v: unknown): v is Record<string, unknown>;
|
|
20
|
+
/** 一个值的形状(六词,闭集):`plain` = 朴素对象(原型是 `null` 或原型自己的原型是 `null`;{@link isPlainJsonObject} 就是这一格);`non_plain_object` = 是对象但不朴素(类实例 / `Map` / `Date` / 建在别的对象原型上的记录,以及**函数** —— 函数是对象、不是纯数据,
|
|
21
|
+
* JSON 里不会有;读者对「是对象但不朴素」一律 fail-closed,函数不能从缝里漏过去);`array`;`null`;`primitive` = 其余(字符串 / 数 / 布尔 / `undefined` / symbol / bigint);
|
|
22
|
+
* `uninspectable` = **读形状就抛**的对象(已撤销的 Proxy:连 `Array.isArray` 都抛;`getPrototypeOf` 陷阱抛的活 Proxy)—— 不是「不朴素」(那句要读者劝人「拷成纯对象」,对撤销的 Proxy 做不到),读者对它 fail-closed 并说全包唯一的「检视不了」那一句。
|
|
23
|
+
* 读者对这六个词**只许穷举 `switch` 加 `never` 默认臂**(加第七个词时每个读者编译红),不许 `===` 链逐个比(单座门看着)。 */
|
|
24
|
+
export type JsonShape = ReturnType<typeof jsonShapeOf>;
|
|
25
|
+
/** 值的形状分类 —— 本包里「是对象但不朴素」与「根本不是对象」要给不同答案的读者调它(见头注)。读原型的就是这一只(判官 {@link isPlainJsonObject} 由它派生)。 */
|
|
26
|
+
export declare function jsonShapeOf(v: unknown): "plain" | "non_plain_object" | "array" | "null" | "primitive" | "uninspectable";
|
|
27
|
+
/**
|
|
28
|
+
* `Array.isArray` 的**总**版(不抛):已撤销的 Proxy 连「是不是数组」都读不出 ⇒ 不是数组。读者要问「这个值是不是数组」且值可能来自嵌入方(非 JSON)时调它,不直接 `Array.isArray`。
|
|
29
|
+
* 分类器闭包里不能 import,`./hooks-config` 的 `hookIsArray` 与本函数**逐字同文**(同一个门证明,与 `hookIsRecord` 同律)。
|
|
30
|
+
*/
|
|
31
|
+
export declare function isJsonArray(v: unknown): v is unknown[];
|
|
@@ -1,11 +1,52 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 包内私有(不进主桶、不进 exports 表):**「纯 JSON
|
|
3
|
-
* `Date` / `Map` / 类实例 /
|
|
4
|
-
* `
|
|
2
|
+
* 包内私有(不进主桶、不进 exports 表):**「纯 JSON 对象」的唯一谓词** —— 不是 `null`、不是数组、原型是 `null` 或「原型自己的原型是 `null`」(即 `Object.prototype`:JSON 解析出来的对象恒如此;
|
|
3
|
+
* `Date` / `Map` / 类实例 / 建在别的对象原型上的记录原型链更深,都不是)。**已知的边**:原型是某个 null 原型对象的对象(`Object.create(Object.create(null))`)、`class X extends null` 的实例,
|
|
4
|
+
* 链上第二环也是 `null`,读成朴素 —— 它们只能由程序造出,JSON 解析不出来;读者按它的**自有**成员读,继承来的成员不计(托管层 http hook 名单对只靠继承可见的名单键 fail-closed;请求面 `env` / `headers` 收成 `{}` 不修 —— 读者级遍历继承成员与谓词级再加判据都对 `Object.prototype` 上的可枚举污染敏感,而这一侧丢的是数据、失败可见、不在安全轴)。**不按「原型恒等本 realm 的 `Object.prototype`」判**:另一个 realm(`vm` 新上下文、iframe)里 `JSON.parse` 出来的普通对象,
|
|
5
|
+
* 原型是**那个 realm** 的 `Object.prototype`,按恒等判会被读成非朴素;而任何 realm 的 `Object.prototype` 自身原型都是 `null`,所以链上第二环是 `null` 这一条与 realm 无关,不需要为 realm 加特例。包里凡要问「这个值是不是一个 JSON 对象」的地方都读它,不各写一份:读域文档的键集对比(`./config-fns`)、一次性迁移的文档形判
|
|
6
|
+
* (`./one-time-migrations`)、`config.d` 文档形(`./config-d-file`)、`extraBody` 的形闸与秘密走查(`./types`)、settings 单元读法与严格 JSON 深度判(`./settings-units` / `./safety-merge-spec`)、
|
|
7
|
+
* 请求面传输的记录读法与描述件(`./mcp-request-entry`)、workers 行读口(`./workers-rows`)、有效配置线上载荷读口(`./config-fns` 的 `readEffectiveWire`)、http hook 名单的设置袋(`./http-hook-allowlists`)、
|
|
8
|
+
* fleet 线上载荷的受护判(`./fleet`)、符合性语料的比较器 / 派生 / 语料读口 / 元数据拆分(`./conformance/*`)。零 import。
|
|
9
|
+
* **形状分类核 {@link jsonShapeOf} 是座位的本体**(唯一读原型的地方),`isPlainJsonObject` 由它派生(`plain` 那一格)。**总(不抛)**:检视就抛的对象(已撤销的 Proxy、`getPrototypeOf` 陷阱抛)
|
|
10
|
+
* 核答 `uninspectable`、判官答 `false`,不抛出去 —— 全包经座位的读口因此拒而不抛,律在座位上收一处,不在每个读器各包一个 `try`;读者对 `uninspectable` 一律按最严处置(hooks ⇒ 拒整份文档;名单 ⇒ `malformed`;传输 ⇒ 整只不交)。
|
|
11
|
+
*
|
|
12
|
+
* **唯一的第二处是 `./hooks-config` 的 `hookShapeOf` / `hookIsRecord`,与 {@link jsonShapeOf} / 本函数逐字同文(调用名归一后)**:hooks 分类器的闭包要被消费方生成器整函数抄走(闭包里的自由名只有 `HOOK_EVENTS`),闭包里不能 import 本模块 ——
|
|
13
|
+
* 同文由 `test/plain-json-object-single-seat-14.6.0.test.ts` 逐字证明(漂开即红),闭包契约多注入一个名字时(S-2133)把它并回这里。
|
|
14
|
+
* 同一个门也数全包:返回类型是 `x is Record<string, unknown>` 的函数、读原型的调用点,除这两处之外一处不许有;内联的「typeof 对象 ∧ 非 null ∧ 非数组」合取则**一处都不许有**(座位里它是分开的 `if` 臂,不是合取)。
|
|
15
|
+
*
|
|
16
|
+
* **读者要的答案不只是「是不是朴素对象」时,调本模块的 {@link jsonShapeOf}**(六词形状分类),不在读者里再推一遍形状:例如托管层的 http hook 名单,非朴素的**对象**袋必须 fail-closed(读成空名单并告警),
|
|
17
|
+
* 而 `null` / 原始值 / 数组仍是「整份设置不是对象 ⇒ 两键都缺」—— 两种不同的答案,只能由形状分类给。`isPlainJsonObject` 就是 `jsonShapeOf` 的「朴素」那一格。
|
|
5
18
|
*/
|
|
6
19
|
export function isPlainJsonObject(v) {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
20
|
+
const shape = jsonShapeOf(v);
|
|
21
|
+
return shape === "plain";
|
|
22
|
+
}
|
|
23
|
+
/** 值的形状分类 —— 本包里「是对象但不朴素」与「根本不是对象」要给不同答案的读者调它(见头注)。读原型的就是这一只(判官 {@link isPlainJsonObject} 由它派生)。 */
|
|
24
|
+
export function jsonShapeOf(v) {
|
|
25
|
+
try {
|
|
26
|
+
if (v === null)
|
|
27
|
+
return "null";
|
|
28
|
+
if (Array.isArray(v))
|
|
29
|
+
return "array";
|
|
30
|
+
if (typeof v === "function")
|
|
31
|
+
return "non_plain_object";
|
|
32
|
+
if (typeof v !== "object")
|
|
33
|
+
return "primitive";
|
|
34
|
+
const proto = Object.getPrototypeOf(v);
|
|
35
|
+
return proto === null || Object.getPrototypeOf(proto) === null ? "plain" : "non_plain_object";
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return "uninspectable"; // 读形状就抛(已撤销的 Proxy 连 `Array.isArray` 都抛、`getPrototypeOf` 陷阱抛):不是「不朴素」,是读不出 —— 读者按最严处置
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* `Array.isArray` 的**总**版(不抛):已撤销的 Proxy 连「是不是数组」都读不出 ⇒ 不是数组。读者要问「这个值是不是数组」且值可能来自嵌入方(非 JSON)时调它,不直接 `Array.isArray`。
|
|
43
|
+
* 分类器闭包里不能 import,`./hooks-config` 的 `hookIsArray` 与本函数**逐字同文**(同一个门证明,与 `hookIsRecord` 同律)。
|
|
44
|
+
*/
|
|
45
|
+
export function isJsonArray(v) {
|
|
46
|
+
try {
|
|
47
|
+
return Array.isArray(v);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return false; // 已撤销的 Proxy:读不出 ⇒ 不是数组
|
|
51
|
+
}
|
|
11
52
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** The domains a single-machine TOC carries: what `FileConfigStore` reads out of `config.d/`, and the
|
|
2
|
+
* **default**域集 of the bundle helpers (`exportBundle`/`readBundle`). 每个被排除的域必须在
|
|
3
|
+
* {@link NON_PORTABLE_DOMAIN_REASONS} 里有一条成文理由(禁裸排除,#282 件5 附带裁定)。
|
|
4
|
+
*
|
|
5
|
+
* ⚠️ 口径诚实(对抗复审 复审 round1 F4):这是**默认集与本地读取集,不是 API 级硬边界** —— `exportBundle`
|
|
6
|
+
* / `readBundle` 的 `domains` 参数可显式传任意 `DomainName`(bundle.ts 不拒非便携域)。把非便携域搬进
|
|
7
|
+
* bundle 的那条口子归调用方纪律(以及各自域的导出面),不是本常量能挡住的;要不要把 bundle API 收窄到
|
|
8
|
+
* `PortableDomain` 是一个独立的行为面裁定(#282 未决项,需三问),本批不擅自改。
|
|
9
|
+
*
|
|
10
|
+
* 🔴 **开新顶层域时必须同步登记本表**(成案 = 消费方 #322 真事故):域表是**闭集**,不在这里的
|
|
11
|
+
* 域名,`config.d/<域>.json` 不会被读进 effective —— 消费方拿到的是「该域缺席」,和文件不存在**逐字
|
|
12
|
+
* 同形**。当年 `limits` 域就是这样在单机腿静默丢了一整片限额发布(远端腿同批可用,所以更难看出来)。
|
|
13
|
+
* 今日的兜底:漏登记的文件会被 `FileConfigStore`(`./node`)的 `"unread-config-file"` 警告点名(fail-loud,
|
|
14
|
+
* 见 getEffective),但那只在消费方接了 `onWarning` 时才响 —— **登记仍是第一现场的义务**。 */
|
|
15
|
+
export declare const PORTABLE_DOMAINS: readonly ["models", "rosters", "skills", "mcp", "plugins", "scenarios", "collab", "governance", "execution", "prompts", "limits"];
|
|
16
|
+
export type PortableDomain = (typeof PORTABLE_DOMAINS)[number];
|
|
17
|
+
/**
|
|
18
|
+
* 非便携域的**排除理由表**(#282 件5 附带裁定:禁裸排除)。
|
|
19
|
+
*
|
|
20
|
+
* `satisfies Record<Exclude<DomainName, PortableDomain>, string>` 让键集在**编译期**双向咬死:新开一个
|
|
21
|
+
* 域却不放进 PORTABLE_DOMAINS ⇒ 这里缺行 ⇒ 编译错;把某域改成便携却忘了删行 ⇒ 多余键 ⇒ 编译错。
|
|
22
|
+
* (此前是「理由写在注释里」,机器只能做子串匹配——域名出现在文件任何角落都算数,等于没门;对抗复审
|
|
23
|
+
* 复审 round1 F3。)理由要写**为什么它不该被单机/跨部署搬运**,带出处。
|
|
24
|
+
*/
|
|
25
|
+
export declare const NON_PORTABLE_DOMAIN_REASONS: {
|
|
26
|
+
readonly workers: "FLEET-only:单机没有 reconciler,`{workers:[]}` 的 schema 默认就是正确语义(缺席不需要 stub)。";
|
|
27
|
+
readonly hosts: "FLEET-only:同 workers —— 单机无机器注册表/自动排布,空注册表即正确语义。";
|
|
28
|
+
readonly systems: "FLEET-only:系统接入/凭据下发的元数据(nonce+路由)属于 web-admin 侧生命周期,单机 lane 无对应执法点。";
|
|
29
|
+
readonly projects: "TENANT IDENTITY DATA,不是可搬运的配置:登记簿随包复制进另一个部署 = 凭空多出的身份登记(design/142 §1.4 威胁模型①「carried-in foreign projectId」换了身 import 的衣服)。要带走走 142 §5 的导出面(service 半场)。";
|
|
30
|
+
readonly entitlement: "per-PRINCIPAL 商务/治理数据(tier·预算上限·kill-switch·allowlist),与 projects 同威胁模型(#282 件5 附带裁定,此前是唯一裸排除项):(a) 搬走 = 把一个租户的商务档复制进另一部署;(b) 反向也不成立 —— 单机 TOC 无 principal 轴(无鉴权语境、无 `/effective?principal=` 解析面),本地 entitlement.json 只会造出一份**永不被执法**的假管控面(真执法点在 service / web-admin 边界)。缺席语义如实:本地 buildEffective 给它 schema 默认值(空表),这**不等于**把远端 kill-switch/预算上限搬到了本地 —— 单机 lane 本就没有那个执法点。";
|
|
31
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** The domains a single-machine TOC carries: what `FileConfigStore` reads out of `config.d/`, and the
|
|
2
|
+
* **default**域集 of the bundle helpers (`exportBundle`/`readBundle`). 每个被排除的域必须在
|
|
3
|
+
* {@link NON_PORTABLE_DOMAIN_REASONS} 里有一条成文理由(禁裸排除,#282 件5 附带裁定)。
|
|
4
|
+
*
|
|
5
|
+
* ⚠️ 口径诚实(对抗复审 复审 round1 F4):这是**默认集与本地读取集,不是 API 级硬边界** —— `exportBundle`
|
|
6
|
+
* / `readBundle` 的 `domains` 参数可显式传任意 `DomainName`(bundle.ts 不拒非便携域)。把非便携域搬进
|
|
7
|
+
* bundle 的那条口子归调用方纪律(以及各自域的导出面),不是本常量能挡住的;要不要把 bundle API 收窄到
|
|
8
|
+
* `PortableDomain` 是一个独立的行为面裁定(#282 未决项,需三问),本批不擅自改。
|
|
9
|
+
*
|
|
10
|
+
* 🔴 **开新顶层域时必须同步登记本表**(成案 = 消费方 #322 真事故):域表是**闭集**,不在这里的
|
|
11
|
+
* 域名,`config.d/<域>.json` 不会被读进 effective —— 消费方拿到的是「该域缺席」,和文件不存在**逐字
|
|
12
|
+
* 同形**。当年 `limits` 域就是这样在单机腿静默丢了一整片限额发布(远端腿同批可用,所以更难看出来)。
|
|
13
|
+
* 今日的兜底:漏登记的文件会被 `FileConfigStore`(`./node`)的 `"unread-config-file"` 警告点名(fail-loud,
|
|
14
|
+
* 见 getEffective),但那只在消费方接了 `onWarning` 时才响 —— **登记仍是第一现场的义务**。 */
|
|
15
|
+
export const PORTABLE_DOMAINS = ["models", "rosters", "skills", "mcp", "plugins", "scenarios", "collab", "governance", "execution", "prompts", "limits"];
|
|
16
|
+
/**
|
|
17
|
+
* 非便携域的**排除理由表**(#282 件5 附带裁定:禁裸排除)。
|
|
18
|
+
*
|
|
19
|
+
* `satisfies Record<Exclude<DomainName, PortableDomain>, string>` 让键集在**编译期**双向咬死:新开一个
|
|
20
|
+
* 域却不放进 PORTABLE_DOMAINS ⇒ 这里缺行 ⇒ 编译错;把某域改成便携却忘了删行 ⇒ 多余键 ⇒ 编译错。
|
|
21
|
+
* (此前是「理由写在注释里」,机器只能做子串匹配——域名出现在文件任何角落都算数,等于没门;对抗复审
|
|
22
|
+
* 复审 round1 F3。)理由要写**为什么它不该被单机/跨部署搬运**,带出处。
|
|
23
|
+
*/
|
|
24
|
+
export const NON_PORTABLE_DOMAIN_REASONS = {
|
|
25
|
+
workers: "FLEET-only:单机没有 reconciler,`{workers:[]}` 的 schema 默认就是正确语义(缺席不需要 stub)。",
|
|
26
|
+
hosts: "FLEET-only:同 workers —— 单机无机器注册表/自动排布,空注册表即正确语义。",
|
|
27
|
+
systems: "FLEET-only:系统接入/凭据下发的元数据(nonce+路由)属于 web-admin 侧生命周期,单机 lane 无对应执法点。",
|
|
28
|
+
projects: "TENANT IDENTITY DATA,不是可搬运的配置:登记簿随包复制进另一个部署 = 凭空多出的身份登记(design/142 §1.4 威胁模型①「carried-in foreign projectId」换了身 import 的衣服)。要带走走 142 §5 的导出面(service 半场)。",
|
|
29
|
+
entitlement: "per-PRINCIPAL 商务/治理数据(tier·预算上限·kill-switch·allowlist),与 projects 同威胁模型(#282 件5 附带裁定,此前是唯一裸排除项):(a) 搬走 = 把一个租户的商务档复制进另一部署;(b) 反向也不成立 —— 单机 TOC 无 principal 轴(无鉴权语境、无 `/effective?principal=` 解析面),本地 entitlement.json 只会造出一份**永不被执法**的假管控面(真执法点在 service / web-admin 边界)。缺席语义如实:本地 buildEffective 给它 schema 默认值(空表),这**不等于**把远端 kill-switch/预算上限搬到了本地 —— 单机 lane 本就没有那个执法点。",
|
|
30
|
+
};
|