@sema-agent/settings-schema 14.6.0 → 14.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +475 -0
- package/README.md +89 -5
- package/conformance/VERSION +1 -1
- package/conformance/cases/c84-sandbox-git-block-carried-verbatim/expected.json +153 -0
- package/conformance/cases/c84-sandbox-git-block-carried-verbatim/input.json +23 -0
- package/conformance/cases/c84-sandbox-git-block-carried-verbatim/meta.json +5 -0
- package/conformance/cases/c85-sandbox-git-refused-block-still-carried/expected.json +153 -0
- package/conformance/cases/c85-sandbox-git-refused-block-still-carried/input.json +23 -0
- package/conformance/cases/c85-sandbox-git-refused-block-still-carried/meta.json +5 -0
- package/dist/api/auth-bridge.d.ts +16 -2
- package/dist/api/auth-bridge.js +59 -24
- package/dist/api/credentials.d.ts +281 -0
- package/dist/api/credentials.js +149 -0
- package/dist/api/scopes.js +5 -1
- package/dist/atomic-write.d.ts +15 -0
- package/dist/atomic-write.js +109 -5
- package/dist/catalog-manages.d.ts +15 -0
- package/dist/catalog-manages.js +60 -0
- package/dist/center-refs.d.ts +68 -0
- package/dist/center-refs.js +70 -0
- package/dist/config-d-file.js +33 -15
- package/dist/config-dir-migration.js +1 -0
- package/dist/config-fns.js +147 -81
- package/dist/config-root-documents.d.ts +106 -0
- package/dist/config-root-documents.js +93 -0
- package/dist/config-root-node.d.ts +117 -0
- package/dist/config-root-node.js +260 -0
- package/dist/conformance/canonical.js +4 -4
- package/dist/conformance/contract-adapter.js +26 -1
- package/dist/conformance/corpus.js +4 -4
- package/dist/conformance/derive.js +2 -2
- package/dist/conformance/run.d.ts +3 -0
- package/dist/conformance/run.js +53 -12
- package/dist/conformance/temp-roots.d.ts +31 -0
- package/dist/conformance/temp-roots.js +184 -0
- package/dist/cross-session.d.ts +4 -4
- package/dist/declared-capture.d.ts +41 -0
- package/dist/declared-capture.js +344 -0
- package/dist/default-model.d.ts +1 -1
- package/dist/default-model.js +5 -4
- package/dist/env-model-table.d.ts +163 -0
- package/dist/env-model-table.js +257 -0
- package/dist/file-edit.js +11 -15
- package/dist/fleet.d.ts +36 -7
- package/dist/fleet.js +83 -32
- package/dist/hash.d.ts +0 -9
- package/dist/hash.js +64 -7
- package/dist/hooks-config.js +18 -3
- package/dist/http-hook-allowlists.d.ts +22 -9
- package/dist/http-hook-allowlists.js +169 -91
- package/dist/index.d.ts +7 -0
- package/dist/index.js +7 -0
- package/dist/mcp-request-entry.d.ts +67 -13
- package/dist/mcp-request-entry.js +132 -93
- package/dist/mcp-server-name.d.ts +17 -0
- package/dist/mcp-server-name.js +19 -0
- package/dist/migration-receipt.d.ts +10 -6
- package/dist/migration-receipt.js +5 -3
- package/dist/node.d.ts +1 -0
- package/dist/node.js +2 -0
- package/dist/one-time-migrations.d.ts +4 -2
- package/dist/one-time-migrations.js +236 -96
- package/dist/own-key-read.d.ts +247 -0
- package/dist/own-key-read.js +449 -0
- package/dist/plain-json-object.d.ts +11 -1
- package/dist/plain-json-object.js +37 -1
- package/dist/remote-exec.js +9 -1
- package/dist/retired-form.d.ts +19 -0
- package/dist/retired-form.js +24 -1
- package/dist/safety-merge-spec.d.ts +6 -6
- package/dist/safety-merge-spec.js +50 -37
- package/dist/sandbox-git.d.ts +64 -0
- package/dist/sandbox-git.js +89 -0
- package/dist/scheduler-store.js +19 -11
- package/dist/settings-units.js +14 -12
- package/dist/tier-words.d.ts +33 -0
- package/dist/tier-words.js +43 -0
- package/dist/types.d.ts +12 -2
- package/dist/types.js +35 -6
- package/dist/url-shape.d.ts +16 -0
- package/dist/url-shape.js +18 -0
- package/dist/web-admin-credential.js +5 -2
- package/dist/web-admin-session-url.js +2 -2
- package/dist/worker-row-verdict.d.ts +11 -0
- package/dist/worker-row-verdict.js +58 -2
- package/dist/workers-rows.js +38 -10
- package/dist/zod-unwrap.d.ts +6 -0
- package/dist/zod-unwrap.js +14 -7
- package/docs/API-SURFACE.md +100 -5
- package/docs/CONSUMER-INTEGRATION.md +250 -2
- package/llms-full.txt +26 -4
- package/llms.txt +11 -4
- package/package.json +2 -2
- package/dist/definition-copy.d.ts +0 -23
- package/dist/definition-copy.js +0 -52
package/dist/atomic-write.js
CHANGED
|
@@ -7,10 +7,10 @@
|
|
|
7
7
|
* `mutateSchedulerStore` 的提交点),异步调用方(`writeDomainFile` 等)直接调它 —— 一只写器,没有第二份同样的写法。
|
|
8
8
|
*
|
|
9
9
|
* ## 文件模式:一张按文档类的表({@link ATOMIC_FILE_CLASSES}),不在任何调用点另写
|
|
10
|
-
* · `config-document` ——
|
|
11
|
-
*
|
|
12
|
-
* · `scheduler-store` —— 调度店耐久记录(可能带 principal / session 标识):新建 = 0o600,且**每次写都回到 0o600**(
|
|
13
|
-
*
|
|
10
|
+
* · `config-document` —— 契约 JSON:`config.d` 下的域文档与 `remote-exec.json`,以及配置根上的两份根级文档(决定文档 `config.env-models.json` / 运行记录 `config.migration.<id>.json`)——
|
|
11
|
+
* 只带 env-NAME 或几个固定词、不带秘密值(README 契约),新建 = 0o644;**已有的文件沿用它原来的模式**(运维 chmod 过的文件不被一次写回覆盖)。
|
|
12
|
+
* · `scheduler-store` —— 调度店耐久记录(可能带 principal / session 标识):新建 = 0o600,且**每次写都回到 0o600**(同样经进程 umask:已有的更宽的文件被收回,更严的 umask 下更严)。
|
|
13
|
+
* 新建**文件**的模式照常经进程 umask 遮一道(`writeFile` 的 `mode` 语义):比表更严的 umask 只会更严,不会被这里放宽;沿用的模式原样恢复(不再过 umask)。这是**文件**的律;目录是另一条律(见下面「仅属主可进的目录」)。
|
|
14
14
|
*
|
|
15
15
|
* ## 目标是符号链接
|
|
16
16
|
* 域文件可以是符号链接 —— 运维用链接把 `config.d` 指到别处管理的部署,那是有意的布局,写器不替他改布局。规则一条:**先把目标解析到真文件(`realpath`),
|
|
@@ -20,6 +20,16 @@
|
|
|
20
20
|
* 「链接 ⇒ 真文件、悬空 ⇒ 响亮抛」这一只判法是 {@link resolveSymlinkTarget}:目录迁移的读口(`./config-dir-io`)读目录 / 读文件前也问它,
|
|
21
21
|
* 悬空链接在读阶段与写阶段是**同一句**({@link danglingSymlinkSentence})。
|
|
22
22
|
*
|
|
23
|
+
* ## 不存在才建({@link createFileExclusive})
|
|
24
|
+
* 另一种写法只有一处不同:最后一步不是 `rename` 盖过目标,而是把(已 fsync 的)临时文件 `link` 成目标名 —— 目标名已在(任何东西:文件、目录、哪怕是悬空的符号链接)⇒
|
|
25
|
+
* 内核拒(`EEXIST`),答 `present`、一字不动。读到「缺席」与写入之间别人写下的东西因此不会被吞(一次性迁移铸配置根的决定文档时用它)。
|
|
26
|
+
* 临时文件与目标同目录、模式按同一张表(目标不在 ⇒ 表的新建模式,经进程 umask,与 {@link writeFileAtomic} 的新建同一条规则);临时名无论成败都删掉。文件系统不支持硬链接(`EPERM` / `ENOTSUP` …)⇒ 照常抛,调用方按写失败处置。
|
|
27
|
+
*
|
|
28
|
+
* ## 仅属主可进的目录({@link ensureOwnerOnlyDirectory})
|
|
29
|
+
* 配置根缺席时一次性迁移要建它:建出来的每一级都是 {@link OWNER_ONLY_DIRECTORY_MODE} 的**精确值** —— 逐级 `mkdir` 之后立刻 `chmod`,所以目录的模式**不经进程 umask**:
|
|
30
|
+
* **不受 umask 放宽,也不受 umask 收窄**(前者:`mkdir` 给的 mode 本身就是 0o700,umask 只能清掉位、加不了位;后者:例如 umask 0o277 下 `mkdir` 先出 0o500,紧跟的 `chmod` 把它定回 0o700)。
|
|
31
|
+
* 这是目录的另一条律,与上面「新建文件经进程 umask」不是同一条律的两个特例;已在的目录一位不动。目录模式与文件模式同在本文件一处定。
|
|
32
|
+
*
|
|
23
33
|
* ## 写失败(12.0.0;句子归文件系统失败句的唯一铸点)
|
|
24
34
|
* 三步各一句({@link FsFailure},句子在 `./fs-failure`):临时文件建不出 ⇒ 「cannot write into directory <dir> (<errno>): …」(点名目录 —— 目录不可写时
|
|
25
35
|
* 文件本身没错);沿用原模式的文件给临时文件设模式失败 ⇒ 「cannot set the mode of <file> (<errno>): …」(点名设模式这一步 —— 文件系统不让改模式,目录没有问题);`rename` 失败 ⇒ 「cannot replace <file> (<errno>): …」(点名真文件)。
|
|
@@ -28,7 +38,7 @@
|
|
|
28
38
|
* 写器在建临时文件那一步读了经落点递给铸点(`EPERM` ∧ 可写 ⇒ 真因是不可变标志,首选出路是清标志;`EACCES` ∧ 可写 ⇒ 真因是 ACL / 安全模块;不可写 ⇒ 先让目录可写);`chmod` / `rename` 那两步不读 —— 临时文件刚在同目录建成,可写是已证明的事实,铸点自己给。
|
|
29
39
|
*/
|
|
30
40
|
import { randomBytes } from "node:crypto";
|
|
31
|
-
import { chmodSync, lstatSync, readlinkSync, realpathSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
41
|
+
import { chmodSync, closeSync, existsSync, fsyncSync, linkSync, lstatSync, mkdirSync, openSync, readlinkSync, realpathSync, renameSync, statSync, unlinkSync, writeFileSync, writeSync } from "node:fs";
|
|
32
42
|
import path from "node:path";
|
|
33
43
|
import { FsFailure } from "./fs-failure.js";
|
|
34
44
|
/** 按文档类的模式表(闭集)。 */
|
|
@@ -138,3 +148,97 @@ export function directoryAdmitsWrite(dir) {
|
|
|
138
148
|
const shift = st.uid === euid ? 6 : group ? 3 : 0;
|
|
139
149
|
return ((st.mode >> shift) & 0o3) === 0o3;
|
|
140
150
|
}
|
|
151
|
+
/** 新建目录的模式:仅属主可读写进(引擎的目录信任判官要求路径上的组件不得组 / 他人可写;0o700 在任何 umask 下都过)。 */
|
|
152
|
+
export const OWNER_ONLY_DIRECTORY_MODE = 0o700;
|
|
153
|
+
/**
|
|
154
|
+
* 把临时文件**独占地**(`wx`)建出来、写到写完为止(`writeSync` 可以少写,按返回值续写)、fsync 到盘上:模式是 `mode` **经进程 umask**(`open` 的 `mode` 语义,与 {@link writeFileAtomic}
|
|
155
|
+
* 新建临时文件同一条规则),建出来之后不再改它。`wx` 只管这个临时名没被占;「目标名已在」是 {@link createFileExclusive} 里 `link` 的事。
|
|
156
|
+
*/
|
|
157
|
+
function writeTempExclusive(tmp, text, mode) {
|
|
158
|
+
const fd = openSync(tmp, "wx", mode);
|
|
159
|
+
try {
|
|
160
|
+
const bytes = Buffer.from(text, "utf8");
|
|
161
|
+
for (let written = 0; written < bytes.length;)
|
|
162
|
+
written += writeSync(fd, bytes, written, bytes.length - written);
|
|
163
|
+
fsyncSync(fd);
|
|
164
|
+
}
|
|
165
|
+
finally {
|
|
166
|
+
closeSync(fd);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* 原子地**不存在才建** `target`(见文件头「不存在才建」):建成 ⇒ `"created"`;目标名已在(只有 `EEXIST`)⇒ `"present"`(一字不动)。
|
|
171
|
+
* 临时文件独占建出、写完、fsync({@link writeTempExclusive})后 `link` 成目标名。模式 = `kind` 在表里的新建模式经进程 umask(目标按定义不在,没有「已有的模式」可沿用)。
|
|
172
|
+
* 临时文件建不出 / 写不完 / fsync 不了 ⇒ 抛 {@link FsFailure}(落点「往目录里写临时文件」,与 {@link writeFileAtomic} 建临时文件失败同落点同句);`link` 的别的失败原样抛
|
|
173
|
+
* (调用方经 `fsFailureMessage` 取句)。父目录须已存在。
|
|
174
|
+
*/
|
|
175
|
+
export function createFileExclusive(target, text, kind) {
|
|
176
|
+
const spec = ATOMIC_FILE_CLASSES[kind];
|
|
177
|
+
const dir = path.dirname(target);
|
|
178
|
+
const tmp = path.join(dir, `.${path.basename(target)}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`);
|
|
179
|
+
try {
|
|
180
|
+
writeTempExclusive(tmp, text, spec.newMode);
|
|
181
|
+
}
|
|
182
|
+
catch (e) {
|
|
183
|
+
try {
|
|
184
|
+
unlinkSync(tmp);
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
/* 临时文件没建成:没什么可清 */
|
|
188
|
+
}
|
|
189
|
+
throw new FsFailure({ landing: "dir-create-temp", path: tmp, directoryWritable: directoryAdmitsWrite(dir) }, e);
|
|
190
|
+
}
|
|
191
|
+
try {
|
|
192
|
+
linkSync(tmp, target);
|
|
193
|
+
return "created";
|
|
194
|
+
}
|
|
195
|
+
catch (e) {
|
|
196
|
+
if (e.code === "EEXIST")
|
|
197
|
+
return "present";
|
|
198
|
+
throw e;
|
|
199
|
+
}
|
|
200
|
+
finally {
|
|
201
|
+
try {
|
|
202
|
+
unlinkSync(tmp);
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
/* 已不在 */
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* 目录不在就建(连同缺的上级):**逐级** mkdir、每建一级立刻 chmod 到 {@link OWNER_ONLY_DIRECTORY_MODE} —— 模式恰是它,不经 umask(umask 0777 下一次性的递归 mkdir 会先建出
|
|
211
|
+
* 一级 0000 的目录、再也进不去建下一级)。本进程建了 `dir` ⇒ `"created"`;`dir` 已在(或恰被别的进程同时建成)⇒ `"present"`(它的模式一位不动)。
|
|
212
|
+
* 路径上有一节不是目录 ⇒ 抛。
|
|
213
|
+
*/
|
|
214
|
+
export function ensureOwnerOnlyDirectory(dir) {
|
|
215
|
+
const target = path.resolve(dir);
|
|
216
|
+
const missing = [];
|
|
217
|
+
for (let at = target; !existsSync(at);) {
|
|
218
|
+
missing.unshift(at);
|
|
219
|
+
const up = path.dirname(at);
|
|
220
|
+
if (up === at)
|
|
221
|
+
break;
|
|
222
|
+
at = up;
|
|
223
|
+
}
|
|
224
|
+
if (missing.length === 0) {
|
|
225
|
+
if (!statSync(target).isDirectory())
|
|
226
|
+
throw Object.assign(new Error(`${target} exists and is not a directory`), { code: "ENOTDIR" });
|
|
227
|
+
return "present";
|
|
228
|
+
}
|
|
229
|
+
let created = false;
|
|
230
|
+
for (const d of missing) {
|
|
231
|
+
try {
|
|
232
|
+
mkdirSync(d, { mode: OWNER_ONLY_DIRECTORY_MODE });
|
|
233
|
+
}
|
|
234
|
+
catch (e) {
|
|
235
|
+
if (e.code === "EEXIST" && statSync(d).isDirectory())
|
|
236
|
+
continue; // a concurrent creator made it: theirs, left as it is
|
|
237
|
+
throw e;
|
|
238
|
+
}
|
|
239
|
+
chmodSync(d, OWNER_ONLY_DIRECTORY_MODE);
|
|
240
|
+
if (d === target)
|
|
241
|
+
created = true;
|
|
242
|
+
}
|
|
243
|
+
return created ? "created" : "present";
|
|
244
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 一个**已核过是纯 JSON 对象**的条目的 `enabled`:自有数据属性且是布尔 ⇒ 它;其余(缺席、只靠继承、访问器、不是布尔)⇒ `undefined`。不调 getter。
|
|
3
|
+
* 反射本身抛(Proxy 的描述符陷阱)⇒ **照抛**,由调用方的失败臂收:被丢条目身份整份按读不出处置(不带半份身份);原文判官读的是捕获出来的快照,不会走到这里。
|
|
4
|
+
* 包内私有(不进包根):这是「`enabled` 怎么读」的**唯一实现**;条目的形由调用方判一次(被丢条目身份在读 `name` / `id` 之前已判过,这里不再问第二次)。
|
|
5
|
+
*/
|
|
6
|
+
export declare function ownEnabledFlag(entry: object): boolean | undefined;
|
|
7
|
+
/** 一条目录条目**原文**的 `enabled`:先判形(不是纯 JSON 对象 ⇒ `undefined`),再读 {@link ownEnabledFlag}。「显式停用」与计划助手的启用过滤都调它。 */
|
|
8
|
+
export declare function entryEnabledFlag(entry: unknown): boolean | undefined;
|
|
9
|
+
/**
|
|
10
|
+
* 文档里**不是显式停用**的条目数(日志计数用;判官 {@link catalogManagesModelPlane} 就是它 > 0)。入参是 models 文档原文或读过的 `ModelsConfig`(两者同形)。
|
|
11
|
+
* 读不出 ⇒ `TypeError`(见文件头)。
|
|
12
|
+
*/
|
|
13
|
+
export declare function enabledEntryCount(doc: unknown): number;
|
|
14
|
+
/** 这份 models 文档管模型面吗(见文件头的律)。读不出 ⇒ `TypeError`。 */
|
|
15
|
+
export declare function catalogManagesModelPlane(doc: unknown): boolean;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 「一份 models 文档管不管模型面」—— **原文半场的唯一判官**(纯、零依赖、浏览器安全)。
|
|
3
|
+
*
|
|
4
|
+
* ## 律(一条)
|
|
5
|
+
* **文档的 `models` 里至少一条不是「显式停用」的条目 ⇔ 管。** 「显式停用」= 纯 JSON 对象、自有数据属性 `enabled` 恰为 `false`
|
|
6
|
+
* ({@link ownEnabledFlag} 是这一读法的**唯一**实现:被丢条目身份的 `enabled`(`./config-fns` 的 `readEntryIdentity`)读的是同一只;{@link entryEnabledFlag} 只是它前面判一次形的入口)。
|
|
7
|
+
* 判官 = 计数 > 0:「管不管」与「几条在管」是同一个事实的两种问法,只有一种读法。
|
|
8
|
+
*
|
|
9
|
+
* · 文档不是纯对象、`models` 缺席或不是数组 ⇒ 0 条(不管);数组里的洞不是条目;
|
|
10
|
+
* · 条目本身不是对象(坏条目)⇒ 不是「显式停用」⇒ 计入 —— 引擎读腿那一半(被丢条目载体:形不对 ⇒ 算管)同答;
|
|
11
|
+
* · 读不出的值(撤销的 Proxy、访问器 / 隐藏的自有键、类实例 —— JSON 解析不出这些)⇒ 抛 `TypeError`,带全包那一句:判官的两个调用方失败方向相反
|
|
12
|
+
* (写者的计划助手:「管」= 要问人;一次性迁移:「管」= 替人铸确认),不替它们猜。
|
|
13
|
+
*
|
|
14
|
+
* 读后表示(引擎的读腿载体半场)不在本包:那一半读的是引擎自己的被丢条目载体,由引擎在本判官之上 ∨ 一格。
|
|
15
|
+
*/
|
|
16
|
+
import { captureJson, isAccessorDescriptor } from "./own-key-read.js";
|
|
17
|
+
import { isJsonArray, isPlainJsonObject } from "./plain-json-object.js";
|
|
18
|
+
/**
|
|
19
|
+
* 一个**已核过是纯 JSON 对象**的条目的 `enabled`:自有数据属性且是布尔 ⇒ 它;其余(缺席、只靠继承、访问器、不是布尔)⇒ `undefined`。不调 getter。
|
|
20
|
+
* 反射本身抛(Proxy 的描述符陷阱)⇒ **照抛**,由调用方的失败臂收:被丢条目身份整份按读不出处置(不带半份身份);原文判官读的是捕获出来的快照,不会走到这里。
|
|
21
|
+
* 包内私有(不进包根):这是「`enabled` 怎么读」的**唯一实现**;条目的形由调用方判一次(被丢条目身份在读 `name` / `id` 之前已判过,这里不再问第二次)。
|
|
22
|
+
*/
|
|
23
|
+
export function ownEnabledFlag(entry) {
|
|
24
|
+
const d = Object.getOwnPropertyDescriptor(entry, "enabled");
|
|
25
|
+
const value = d !== undefined && !isAccessorDescriptor(d) ? d.value : undefined; // 访问器不调(描述符的字段只认自有的,见内核)
|
|
26
|
+
return typeof value === "boolean" ? value : undefined;
|
|
27
|
+
}
|
|
28
|
+
/** 一条目录条目**原文**的 `enabled`:先判形(不是纯 JSON 对象 ⇒ `undefined`),再读 {@link ownEnabledFlag}。「显式停用」与计划助手的启用过滤都调它。 */
|
|
29
|
+
export function entryEnabledFlag(entry) {
|
|
30
|
+
return isPlainJsonObject(entry) ? ownEnabledFlag(entry) : undefined;
|
|
31
|
+
}
|
|
32
|
+
/** 「显式停用」:{@link entryEnabledFlag} 恰为 `false`。 */
|
|
33
|
+
const isExplicitlyDisabled = (entry) => entryEnabledFlag(entry) === false;
|
|
34
|
+
/**
|
|
35
|
+
* 文档里**不是显式停用**的条目数(日志计数用;判官 {@link catalogManagesModelPlane} 就是它 > 0)。入参是 models 文档原文或读过的 `ModelsConfig`(两者同形)。
|
|
36
|
+
* 读不出 ⇒ `TypeError`(见文件头)。
|
|
37
|
+
*/
|
|
38
|
+
export function enabledEntryCount(doc) {
|
|
39
|
+
const copy = captureJson(doc); // 一次自有键快照:之后的读都在副本上(不调 getter、不读继承)
|
|
40
|
+
if (!copy.ok)
|
|
41
|
+
throw new TypeError(`enabledEntryCount: the models document could not be read — ${copy.sentence}`);
|
|
42
|
+
const snapshot = copy.value;
|
|
43
|
+
if (!isPlainJsonObject(snapshot))
|
|
44
|
+
return 0;
|
|
45
|
+
const models = snapshot.models; // 副本是隔离原型的袋:没有继承成员可读
|
|
46
|
+
if (!isJsonArray(models))
|
|
47
|
+
return 0;
|
|
48
|
+
let count = 0;
|
|
49
|
+
for (let i = 0; i < models.length; i++) {
|
|
50
|
+
if (!Object.hasOwn(models, i))
|
|
51
|
+
continue; // 洞不是条目:只认自有下标(`forEach` / `for…of` 会读到被污染的 `Array.prototype[i]`)
|
|
52
|
+
if (!isExplicitlyDisabled(models[i]))
|
|
53
|
+
count++;
|
|
54
|
+
}
|
|
55
|
+
return count;
|
|
56
|
+
}
|
|
57
|
+
/** 这份 models 文档管模型面吗(见文件头的律)。读不出 ⇒ `TypeError`。 */
|
|
58
|
+
export function catalogManagesModelPlane(doc) {
|
|
59
|
+
return enabledEntryCount(doc) > 0;
|
|
60
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 中心引用的**解析口** `resolveCenterRefs(refs, lookup)` —— 纯函数、浏览器安全。
|
|
3
|
+
*
|
|
4
|
+
* 中心名册里 MCP server 的传输带两张**引用表**:stdio 的 `envRefs`(目标 env 键 → 变量名)、http 的 `headerRefs`(目标头名 → 变量名)。名册只存**变量名**、从不存值;
|
|
5
|
+
* 值要在消费方自己的宿主环境里读。此前本包只发引用表的形、不发读它的法:变量没设、设了但读到的不是串、读它的时候抛了,留给每个宿主自己写 —— 各按各的口径处置、各留各的句子。
|
|
6
|
+
* 现在一个读法:**每个成员一句判定,整台一个结论;本口只判,不决定挂不挂**(挂不挂是调用方按自己那条腿的规则定的事)。
|
|
7
|
+
*
|
|
8
|
+
* ## 律(一条)
|
|
9
|
+
* 引用表怎么读、每个变量怎么问、读不出怎么说,只有这一处:
|
|
10
|
+
* · **引用表经公开的记录读器读**({@link readPlainStringRecord}:同一个函数、同一张原因词表 {@link PLAIN_STRING_RECORD_FAULTS}),不另写形状 / 描述符判据。读不出(整张不是纯数据、有成员的变量名不是串、
|
|
11
|
+
* 读成员就抛、叫 `__proto__` 的成员)⇒ 整台结论 `refs_unreadable`,带读器的词,**一个变量都不问** —— 引用表本身不可信时,问了的值也没有可以挂的地方。
|
|
12
|
+
* 读器会把叫 `__proto__` 的串值成员**剔出副本**交其余;本口不许这样悄悄丢一条引用再挂一台少了鉴权的 server,所以同样答 `refs_unreadable`、词 `member_name_reserved`。
|
|
13
|
+
* · **查找函数 `lookup(name)` 由调用方给**,本口**不读 `process.env`**(源里没有 `process` / `globalThis`;宿主怎么读环境、读哪一层环境、要不要缓存,都是调用方的事)。没给 / 不是函数 ⇒ `TypeError`
|
|
14
|
+
* (这是调用方的编程错,不是数据错:不折成成员的 `read_threw`,更不回落任何环境)。
|
|
15
|
+
* · 每个成员恰问一次 `lookup`(按引用表副本的自有键序 —— 整数形的键在前、其余按记录列出的次序;实参只有变量名一个),判成**一个词**({@link CenterRefMemberVerdict}):
|
|
16
|
+
* `resolved`(答了串,空串也是串)· `missing_name`(答 `undefined`:变量没设)· `not_a_string`(答了别的种类 —— 例如对普通对象做 `obj[name]` 读到继承来的函数)· `read_threw`(`lookup` 抛了,不传出)。
|
|
17
|
+
* 一个成员坏了**不早停**:每个成员都有自己的判定,调用方可以一次点名所有缺的变量。同一个变量被两个目标键引用就问两次(缓存是查找函数的事)。
|
|
18
|
+
* · 整台结论只有三个词({@link CenterRefsVerdict}):`resolved`(每个成员都是 `resolved`;零成员也是)· `unresolved`(有成员不是)· `refs_unreadable`(引用表读不出)。
|
|
19
|
+
* **值只在 `resolved` 臂的 `values` 里**;`unresolved` 臂的行只有词,没有任何变量的值(失败读数会进日志)。
|
|
20
|
+
*
|
|
21
|
+
* ## 引用表缺席
|
|
22
|
+
* `refs` 是 `undefined`(这台 server 没声明引用)⇒ `resolved`、零成员、一次都不问 —— 与「空表」同一个答案。别的不是纯数据记录的值(`null`、数组、串……)是 `refs_unreadable`,不当缺席。
|
|
23
|
+
*
|
|
24
|
+
* ## 读数的构造
|
|
25
|
+
* 成员表用 `map`,`values` 用 `Object.defineProperty` + 零原型描述符({@link dataDescriptor})逐键写进一个新对象(定义语义,不走 [[Set]]):`Object.prototype` 上挂了数字键 setter 时目标键 `"0"` 的值也不会被吞;
|
|
26
|
+
* `constructor` / `toString` 这类原型链上的名字作目标键照落成自有数据属性。交出的 `values` 原型是 `Object.prototype`,与读器交出的副本同形。
|
|
27
|
+
*/
|
|
28
|
+
import { type PlainStringRecordFault } from "./mcp-request-entry.js";
|
|
29
|
+
/** 一个成员没解到的**原因词**,闭集,三个:`missing_name`(查找函数答 `undefined` —— 变量没设)· `not_a_string`(答了别的种类)· `read_threw`(查找函数抛了)。消费方按词分派,穷尽 `switch`,不读句子(本口没有句子)。 */
|
|
30
|
+
export declare const CENTER_REF_FAULTS: readonly ["missing_name", "not_a_string", "read_threw"];
|
|
31
|
+
export type CenterRefFault = (typeof CENTER_REF_FAULTS)[number];
|
|
32
|
+
/** 一个成员的判定词:解到了(`resolved`)或{@link CenterRefFault}之一。 */
|
|
33
|
+
export type CenterRefMemberVerdict = "resolved" | CenterRefFault;
|
|
34
|
+
/** 整台结论的词,闭集,三个:`resolved` · `unresolved`(有成员没解到)· `refs_unreadable`(引用表读不出)。 */
|
|
35
|
+
export declare const CENTER_REFS_VERDICTS: readonly ["resolved", "unresolved", "refs_unreadable"];
|
|
36
|
+
export type CenterRefsVerdict = (typeof CENTER_REFS_VERDICTS)[number];
|
|
37
|
+
/** 调用方给的查找函数:变量名 → 它的值(没设 ⇒ `undefined`)。可以抛(本口接住,判 `read_threw`)。本口不替它读任何环境。 */
|
|
38
|
+
export type CenterRefLookup = (name: string) => string | undefined;
|
|
39
|
+
/** 一个成员的判定:`key` = 目标键(env 键 / 头名,按引用表所写)、`ref` = 它引用的变量名(按所写)、`verdict` = 判定词。行里没有值。 */
|
|
40
|
+
export interface CenterRefMember {
|
|
41
|
+
readonly key: string;
|
|
42
|
+
readonly ref: string;
|
|
43
|
+
readonly verdict: CenterRefMemberVerdict;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* {@link resolveCenterRefs} 的读数,按 `verdict` 分三臂(穷尽 `switch`):
|
|
47
|
+
* · `resolved`:`members` 每行都是 `resolved`;`values` = 目标键 → 解到的串(新铸的朴素对象);
|
|
48
|
+
* · `unresolved`:`members` 里至少一行不是 `resolved`(行的次序 = 询问的次序,见文件头注);没有 `values`;
|
|
49
|
+
* · `refs_unreadable`:引用表没读成 —— `fault` = 读器的原因词({@link PlainStringRecordFault}),坏在某个成员时 `member` 点名它的回显形;没问过任何变量。
|
|
50
|
+
*/
|
|
51
|
+
export type CenterRefsResolution = {
|
|
52
|
+
readonly verdict: "resolved";
|
|
53
|
+
readonly members: readonly CenterRefMember[];
|
|
54
|
+
readonly values: Readonly<Record<string, string>>;
|
|
55
|
+
} | {
|
|
56
|
+
readonly verdict: "unresolved";
|
|
57
|
+
readonly members: readonly CenterRefMember[];
|
|
58
|
+
} | {
|
|
59
|
+
readonly verdict: "refs_unreadable";
|
|
60
|
+
readonly fault: PlainStringRecordFault;
|
|
61
|
+
readonly member?: string;
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* 解析一张中心引用表(见文件头注的律)。`refs` 是名册里 `envRefs` / `headerRefs` 的值(`undefined` = 没声明);`lookup` 是调用方给的查找函数(必给)。
|
|
65
|
+
* 判而不决:`resolved` 之外的两个结论,调用方按自己那条腿的规则处置(例如整台不挂、并点名没解到的成员)。
|
|
66
|
+
* `lookup` 不是函数 ⇒ `TypeError`;别的一切输入都得到一个读数,不抛。
|
|
67
|
+
*/
|
|
68
|
+
export declare function resolveCenterRefs(refs: unknown, lookup: CenterRefLookup): CenterRefsResolution;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 中心引用的**解析口** `resolveCenterRefs(refs, lookup)` —— 纯函数、浏览器安全。
|
|
3
|
+
*
|
|
4
|
+
* 中心名册里 MCP server 的传输带两张**引用表**:stdio 的 `envRefs`(目标 env 键 → 变量名)、http 的 `headerRefs`(目标头名 → 变量名)。名册只存**变量名**、从不存值;
|
|
5
|
+
* 值要在消费方自己的宿主环境里读。此前本包只发引用表的形、不发读它的法:变量没设、设了但读到的不是串、读它的时候抛了,留给每个宿主自己写 —— 各按各的口径处置、各留各的句子。
|
|
6
|
+
* 现在一个读法:**每个成员一句判定,整台一个结论;本口只判,不决定挂不挂**(挂不挂是调用方按自己那条腿的规则定的事)。
|
|
7
|
+
*
|
|
8
|
+
* ## 律(一条)
|
|
9
|
+
* 引用表怎么读、每个变量怎么问、读不出怎么说,只有这一处:
|
|
10
|
+
* · **引用表经公开的记录读器读**({@link readPlainStringRecord}:同一个函数、同一张原因词表 {@link PLAIN_STRING_RECORD_FAULTS}),不另写形状 / 描述符判据。读不出(整张不是纯数据、有成员的变量名不是串、
|
|
11
|
+
* 读成员就抛、叫 `__proto__` 的成员)⇒ 整台结论 `refs_unreadable`,带读器的词,**一个变量都不问** —— 引用表本身不可信时,问了的值也没有可以挂的地方。
|
|
12
|
+
* 读器会把叫 `__proto__` 的串值成员**剔出副本**交其余;本口不许这样悄悄丢一条引用再挂一台少了鉴权的 server,所以同样答 `refs_unreadable`、词 `member_name_reserved`。
|
|
13
|
+
* · **查找函数 `lookup(name)` 由调用方给**,本口**不读 `process.env`**(源里没有 `process` / `globalThis`;宿主怎么读环境、读哪一层环境、要不要缓存,都是调用方的事)。没给 / 不是函数 ⇒ `TypeError`
|
|
14
|
+
* (这是调用方的编程错,不是数据错:不折成成员的 `read_threw`,更不回落任何环境)。
|
|
15
|
+
* · 每个成员恰问一次 `lookup`(按引用表副本的自有键序 —— 整数形的键在前、其余按记录列出的次序;实参只有变量名一个),判成**一个词**({@link CenterRefMemberVerdict}):
|
|
16
|
+
* `resolved`(答了串,空串也是串)· `missing_name`(答 `undefined`:变量没设)· `not_a_string`(答了别的种类 —— 例如对普通对象做 `obj[name]` 读到继承来的函数)· `read_threw`(`lookup` 抛了,不传出)。
|
|
17
|
+
* 一个成员坏了**不早停**:每个成员都有自己的判定,调用方可以一次点名所有缺的变量。同一个变量被两个目标键引用就问两次(缓存是查找函数的事)。
|
|
18
|
+
* · 整台结论只有三个词({@link CenterRefsVerdict}):`resolved`(每个成员都是 `resolved`;零成员也是)· `unresolved`(有成员不是)· `refs_unreadable`(引用表读不出)。
|
|
19
|
+
* **值只在 `resolved` 臂的 `values` 里**;`unresolved` 臂的行只有词,没有任何变量的值(失败读数会进日志)。
|
|
20
|
+
*
|
|
21
|
+
* ## 引用表缺席
|
|
22
|
+
* `refs` 是 `undefined`(这台 server 没声明引用)⇒ `resolved`、零成员、一次都不问 —— 与「空表」同一个答案。别的不是纯数据记录的值(`null`、数组、串……)是 `refs_unreadable`,不当缺席。
|
|
23
|
+
*
|
|
24
|
+
* ## 读数的构造
|
|
25
|
+
* 成员表用 `map`,`values` 用 `Object.defineProperty` + 零原型描述符({@link dataDescriptor})逐键写进一个新对象(定义语义,不走 [[Set]]):`Object.prototype` 上挂了数字键 setter 时目标键 `"0"` 的值也不会被吞;
|
|
26
|
+
* `constructor` / `toString` 这类原型链上的名字作目标键照落成自有数据属性。交出的 `values` 原型是 `Object.prototype`,与读器交出的副本同形。
|
|
27
|
+
*/
|
|
28
|
+
import { PLAIN_STRING_RECORD_RESERVED_MEMBER, readPlainStringRecord } from "./mcp-request-entry.js";
|
|
29
|
+
import { dataDescriptor, inspect } from "./own-key-read.js";
|
|
30
|
+
/** 一个成员没解到的**原因词**,闭集,三个:`missing_name`(查找函数答 `undefined` —— 变量没设)· `not_a_string`(答了别的种类)· `read_threw`(查找函数抛了)。消费方按词分派,穷尽 `switch`,不读句子(本口没有句子)。 */
|
|
31
|
+
export const CENTER_REF_FAULTS = ["missing_name", "not_a_string", "read_threw"];
|
|
32
|
+
/** 整台结论的词,闭集,三个:`resolved` · `unresolved`(有成员没解到)· `refs_unreadable`(引用表读不出)。 */
|
|
33
|
+
export const CENTER_REFS_VERDICTS = ["resolved", "unresolved", "refs_unreadable"];
|
|
34
|
+
/** 问一次 `lookup(ref)` 并判成一个词。抛了 ⇒ `read_threw`(经内核的检视口,不另包 `try`);答 `undefined` ⇒ `missing_name`;答串 ⇒ 解到;答别的 ⇒ `not_a_string`。 */
|
|
35
|
+
function answerOf(ref, lookup) {
|
|
36
|
+
const asked = inspect(() => lookup(ref));
|
|
37
|
+
if (!asked.ok)
|
|
38
|
+
return { verdict: "read_threw" };
|
|
39
|
+
const answer = asked.value;
|
|
40
|
+
if (typeof answer === "string")
|
|
41
|
+
return { verdict: "resolved", value: answer };
|
|
42
|
+
return { verdict: answer === undefined ? "missing_name" : "not_a_string" };
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* 解析一张中心引用表(见文件头注的律)。`refs` 是名册里 `envRefs` / `headerRefs` 的值(`undefined` = 没声明);`lookup` 是调用方给的查找函数(必给)。
|
|
46
|
+
* 判而不决:`resolved` 之外的两个结论,调用方按自己那条腿的规则处置(例如整台不挂、并点名没解到的成员)。
|
|
47
|
+
* `lookup` 不是函数 ⇒ `TypeError`;别的一切输入都得到一个读数,不抛。
|
|
48
|
+
*/
|
|
49
|
+
export function resolveCenterRefs(refs, lookup) {
|
|
50
|
+
if (typeof lookup !== "function") {
|
|
51
|
+
throw new TypeError("resolveCenterRefs: `lookup` must be a function (variable name => string | undefined) supplied by the caller — this function reads no environment of its own");
|
|
52
|
+
}
|
|
53
|
+
if (refs === undefined)
|
|
54
|
+
return { verdict: "resolved", members: [], values: {} };
|
|
55
|
+
const read = readPlainStringRecord(refs);
|
|
56
|
+
if (!read.ok)
|
|
57
|
+
return { verdict: "refs_unreadable", fault: read.fault, ...(read.member !== undefined ? { member: read.member } : {}) };
|
|
58
|
+
if (read.reservedMember)
|
|
59
|
+
return { verdict: "refs_unreadable", fault: "member_name_reserved", member: PLAIN_STRING_RECORD_RESERVED_MEMBER };
|
|
60
|
+
const judged = Object.entries(read.record).map((pair) => ({ key: pair[0], ref: pair[1], answer: answerOf(pair[1], lookup) }));
|
|
61
|
+
const members = judged.map((j) => ({ key: j.key, ref: j.ref, verdict: j.answer.verdict }));
|
|
62
|
+
if (judged.some((j) => j.answer.verdict !== "resolved"))
|
|
63
|
+
return { verdict: "unresolved", members };
|
|
64
|
+
const values = {};
|
|
65
|
+
for (const j of judged) {
|
|
66
|
+
if (j.answer.verdict === "resolved")
|
|
67
|
+
Object.defineProperty(values, j.key, dataDescriptor(j.answer.value));
|
|
68
|
+
}
|
|
69
|
+
return { verdict: "resolved", members, values };
|
|
70
|
+
}
|
package/dist/config-d-file.js
CHANGED
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
import { readFile } from "node:fs/promises";
|
|
13
13
|
import path from "node:path";
|
|
14
14
|
import { refuseRetiredDomain, sanitizeZodError } from "./config-fns.js";
|
|
15
|
-
import {
|
|
15
|
+
import { UNINSPECTABLE, guardedSafeParse } from "./own-key-read.js";
|
|
16
|
+
import { jsonShapeOf } from "./plain-json-object.js";
|
|
16
17
|
import { REMOTE_EXEC_DOCUMENT, RemoteExecSpec } from "./remote-exec.js";
|
|
17
18
|
import { RETIRED_DOMAINS, isDomainName, isRetiredDomainName } from "./types.js";
|
|
18
19
|
/** 一份 `config.d` 文档写盘的**文本形** —— 两格缩进的 JSON 加一个换行。写域文件(`writeDomainFile`)、写 `remote-exec.json`(`writeRemoteExec`)与目录迁移写回
|
|
@@ -23,16 +24,12 @@ export function configDDocumentText(value) {
|
|
|
23
24
|
/** 判一份 remote-exec 文档的值(`RemoteExecSpec`),错误**脱敏**(zod 会回显出错的值 —— 粘错位置的密钥不许随抛出的错走);反射访问抛(恶意 getter /
|
|
24
25
|
* Proxy)⇒ 定句。`loadRemoteExec` / `writeRemoteExec` 与目录迁移的整文档终判调同一只。 */
|
|
25
26
|
export function parseRemoteExecDocument(raw) {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
33
|
-
if (r.success)
|
|
34
|
-
return r.data;
|
|
35
|
-
throw sanitizeZodError(r.error);
|
|
27
|
+
const judged = guardedSafeParse(() => RemoteExecSpec.safeParse(raw), sanitizeZodError); // 错误对象的构造与脱敏都在守护里(zod 4.6 首读才拼句子)
|
|
28
|
+
if (!judged.ok)
|
|
29
|
+
throw new TypeError("invalid remoteExec (a value could not be read during validation)");
|
|
30
|
+
if (judged.value.success)
|
|
31
|
+
return judged.value.data;
|
|
32
|
+
throw judged.value.error;
|
|
36
33
|
}
|
|
37
34
|
/** `<root>/config.d/<名>.json` 的路径 —— 名字的判也在这里(**一处**):不是配置域也不是 retired 域 ⇒ 拒(不拿用户给的名拼路径,防 `../` 穿越)。 */
|
|
38
35
|
export function configDPath(root, name) {
|
|
@@ -64,11 +61,32 @@ export function judgeConfigDText(name, text, file) {
|
|
|
64
61
|
* 非朴素对象 ⇒ 抛。{@link judgeConfigDText} 的最后一道判就是这只(文本解析出来之后过它),所以「不是对象」一种坏形一句话,不论文档是从文件文本来的还是调用方直接给的;
|
|
65
62
|
* `label` = 句首的称呼(文本路径给文件的真实路径,值路径给 `the <名> document`)。 */
|
|
66
63
|
export function judgeConfigDValue(name, value, label) {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
64
|
+
const shape = jsonShapeOf(value);
|
|
65
|
+
if (shape === "plain")
|
|
66
|
+
return value;
|
|
67
|
+
// `shape` is narrowed to the five non-plain words: an exhaustive switch (a sixth word is a compile error here)
|
|
68
|
+
let kind;
|
|
69
|
+
switch (shape) {
|
|
70
|
+
case "null":
|
|
71
|
+
kind = "null";
|
|
72
|
+
break;
|
|
73
|
+
case "array":
|
|
74
|
+
kind = "array";
|
|
75
|
+
break;
|
|
76
|
+
case "non_plain_object":
|
|
77
|
+
kind = typeof value === "function" ? "function" : "non-plain object";
|
|
78
|
+
break;
|
|
79
|
+
case "uninspectable": // 读不出(撤销的 Proxy / 陷阱抛)不是「不是对象」:调用方给的对象读不出是调用方的错 ⇒ `TypeError`(v14.6.0:引擎对它抛的就是 TypeError),句子带全包唯一那一句
|
|
80
|
+
throw new TypeError(`${label} must be a JSON object (got ${UNINSPECTABLE}) — ${documentWayOut(name)}`);
|
|
81
|
+
case "primitive":
|
|
82
|
+
kind = typeof value;
|
|
83
|
+
break;
|
|
84
|
+
default: {
|
|
85
|
+
const unhandled = shape;
|
|
86
|
+
throw new Error(`judgeConfigDValue: unhandled shape ${String(unhandled)}`);
|
|
87
|
+
}
|
|
70
88
|
}
|
|
71
|
-
|
|
89
|
+
throw new Error(`${label} must be a JSON object (got ${kind}) — ${documentWayOut(name)}`);
|
|
72
90
|
}
|
|
73
91
|
/** 读一个 `config.d/<名>.json`:不存在(ENOENT)⇒ `undefined`;在场 ⇒ {@link judgeConfigDText}。 */
|
|
74
92
|
export async function readConfigDFile(root, name) {
|