@sema-agent/settings-schema 11.2.0 → 11.3.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 +58 -0
- package/README.md +2 -1
- package/conformance/VERSION +1 -1
- package/conformance/cases/c73-projects-extended-registration-reads/expected.json +150 -0
- package/conformance/cases/c73-projects-extended-registration-reads/input.json +42 -0
- package/conformance/cases/c73-projects-extended-registration-reads/meta.json +6 -0
- package/conformance/cases/c74-projects-over-bound-refused/expected.json +89 -0
- package/conformance/cases/c74-projects-over-bound-refused/input.json +29 -0
- package/conformance/cases/c74-projects-over-bound-refused/meta.json +5 -0
- package/conformance/cases/c75-projects-patch-remote-uniqueness/expected.json +89 -0
- package/conformance/cases/c75-projects-patch-remote-uniqueness/input.json +35 -0
- package/conformance/cases/c75-projects-patch-remote-uniqueness/meta.json +5 -0
- package/dist/api/feedback.js +2 -3
- package/dist/api/scopes.js +2 -1
- package/dist/atomic-write.d.ts +6 -0
- package/dist/atomic-write.js +12 -4
- package/dist/cli.d.ts +7 -4
- package/dist/cli.js +52 -10
- package/dist/config-d-file.d.ts +11 -0
- package/dist/config-d-file.js +35 -1
- package/dist/config-dir-io.js +4 -3
- package/dist/config-dir-migration.d.ts +5 -2
- package/dist/config-dir-migration.js +23 -9
- package/dist/cross-domain.d.ts +8 -3
- package/dist/cross-domain.js +23 -13
- package/dist/file-edit.js +7 -5
- package/dist/file-store.js +2 -1
- package/dist/fleet.js +3 -4
- package/dist/hooks.d.ts +14 -1
- package/dist/hooks.js +61 -16
- package/dist/host-path.d.ts +17 -0
- package/dist/host-path.js +22 -0
- package/dist/integer.js +2 -1
- package/dist/local-load.d.ts +1 -1
- package/dist/local-load.js +9 -31
- package/dist/migration-receipt.d.ts +3 -2
- package/dist/node.d.ts +1 -1
- package/dist/one-time-migrations.d.ts +5 -3
- package/dist/one-time-migrations.js +19 -6
- package/dist/quoted-issue.d.ts +41 -0
- package/dist/quoted-issue.js +35 -0
- package/dist/remote-exec.d.ts +3 -0
- package/dist/remote-exec.js +12 -7
- package/dist/rfc3339.d.ts +10 -0
- package/dist/rfc3339.js +12 -0
- package/dist/types.d.ts +140 -20
- package/dist/types.js +194 -105
- package/dist/web-admin-credential.d.ts +2 -2
- package/dist/web-admin-credential.js +5 -10
- package/package.json +1 -1
package/dist/cli.d.ts
CHANGED
|
@@ -2,20 +2,21 @@
|
|
|
2
2
|
* `settings-schema` 命令行的核心(11.0.0)—— 子命令只有一个:`migrate <config.d 目录> [--check]`,即一次性迁移
|
|
3
3
|
* `migrateStoredDocuments`(`./one-time-migrations`)的目录形(本地 config.d / 离线文件用;web-admin 在它起服时直接调同一只函数)。
|
|
4
4
|
* 目录形的语义(读目录 → 迁 → 整域终判 → 原子写 → 删 retired 域文件)只有一处实现:`migrateConfigDirectory`(`./config-dir-migration`,`./node` 导出,引擎起服调同一只);
|
|
5
|
-
* 本模块是它的**渲染器**:把它的结果闭集渲染成退出码、报告行与 stderr
|
|
5
|
+
* 本模块是它的**渲染器**:把它的结果闭集渲染成退出码、报告行与 stderr 句子。命令行的覆盖面是全部配置域、retired 域与 `remote-exec.json`。
|
|
6
6
|
* 目录被当作**一个 scope 的全集**(`coverage: "scope"`):目录里没有的域文件就是从未设过。
|
|
7
7
|
*
|
|
8
8
|
* 本模块不碰 `node:fs`:读写文件由入口 `bin/settings-schema.mjs` 注入(真绑定是 `./config-dir-io` 的 `nodeConfigDirectoryIo`,写盘用的是 `./atomic-write` 那一只原子写器 —— 写法与文件模式在那里一处定),所以测试可以直接驱动它。
|
|
9
9
|
* 不进主桶 —— 它是 bin 的实现,不是契约面。
|
|
10
10
|
*
|
|
11
|
-
* 读哪些文件:目录里 `<名>.json`,名是配置域或 retired 域(`runtime`)—— 便携与否不论(离线的 `workers.json` 一样迁)
|
|
12
|
-
* (
|
|
11
|
+
* 读哪些文件:目录里 `<名>.json`,名是配置域或 retired 域(`runtime`)—— 便携与否不论(离线的 `workers.json` 一样迁)—— 以及 `remote-exec.json`
|
|
12
|
+
* (一次性迁移 SS-M7 的座位走查也走它的两个端点);别的文件(隐藏文件、不认识的名、非 `.json`)不读不动。
|
|
13
13
|
*
|
|
14
14
|
* 零域文档分两形:**有条目却没有域文档**(任何非 `.` 开头的条目 —— 子目录、说明文件、不认识的名)⇒ 退出 2;**目录真空**(零条目,或只有以 `.` 开头的条目)⇒ 照常走判定,
|
|
15
15
|
* 零收据 = noop 退出 0 —— 新部署的空 `config.d` 是合法态,「没有东西可迁」不是「指错目录」(搬空之后再跑同一目录也因此幂等)。
|
|
16
16
|
*
|
|
17
17
|
* 退出码:0 = 已写回 / 无事可做(目录里有域文档只是没有要改的,或目录真空);1 = 有迁不了的坐标(收据 `refused`)**或**要写回的域文档整域读不进(整域终判,见 `migrateConfigDirectory`),**一字不写**;
|
|
18
|
-
* 2 =
|
|
18
|
+
* 2 = 用法错,或**环境错**(读目录 / 读文件 / 写盘 / 删文件的 I/O 错:权限、磁盘、悬空符号链接 …;stdout 空、stderr 一行点名路径与错误(errno 码在 Node 的错误句里),不打栈 ——
|
|
19
|
+
* rc 1 是「拒迁」的约定码,不许被一个权限错冒充),或某个域文件是坏文件(空 / 纯空白 / 坏 JSON / 不是 JSON 对象 —— 与 `FileConfigStore` 同一只判、同一句),**或目录里有条目却没有一个是域文档**
|
|
19
20
|
* (多半是填错了目录:要的是 `config.d` 目录本身;`<目录>/config.d` 在场时句子直接点它)(此时 stdout 为空);3 = `--check` 且会有改动、整域判得过(不写)。`--check` 与真跑过同一只判定,只在
|
|
20
21
|
* 「有改动、且整域判得过」那一支上分开(pending vs applied)—— 整域会被拒的目录 `--check` 也报 refused / rc 1。
|
|
21
22
|
*
|
|
@@ -32,4 +33,6 @@ export interface SettingsSchemaCliIo extends ConfigDirectoryIo {
|
|
|
32
33
|
stdout(text: string): void;
|
|
33
34
|
stderr(text: string): void;
|
|
34
35
|
}
|
|
36
|
+
/** 命令行的覆盖面:全部配置域、retired 域(便携与否不论 —— 离线的 `workers.json` 一样迁)与 `remote-exec.json`。 */
|
|
37
|
+
export declare const COVERED_DOCUMENTS: ("models" | "rosters" | "scenarios" | "skills" | "execution" | "plugins" | "systems" | "limits" | "runtime" | "workers" | "hosts" | "projects" | "mcp" | "collab" | "governance" | "entitlement" | "prompts" | "remote-exec")[];
|
|
35
38
|
export declare function runSettingsSchemaCli(argv: readonly string[], io: SettingsSchemaCliIo): Promise<number>;
|
package/dist/cli.js
CHANGED
|
@@ -2,20 +2,21 @@
|
|
|
2
2
|
* `settings-schema` 命令行的核心(11.0.0)—— 子命令只有一个:`migrate <config.d 目录> [--check]`,即一次性迁移
|
|
3
3
|
* `migrateStoredDocuments`(`./one-time-migrations`)的目录形(本地 config.d / 离线文件用;web-admin 在它起服时直接调同一只函数)。
|
|
4
4
|
* 目录形的语义(读目录 → 迁 → 整域终判 → 原子写 → 删 retired 域文件)只有一处实现:`migrateConfigDirectory`(`./config-dir-migration`,`./node` 导出,引擎起服调同一只);
|
|
5
|
-
* 本模块是它的**渲染器**:把它的结果闭集渲染成退出码、报告行与 stderr
|
|
5
|
+
* 本模块是它的**渲染器**:把它的结果闭集渲染成退出码、报告行与 stderr 句子。命令行的覆盖面是全部配置域、retired 域与 `remote-exec.json`。
|
|
6
6
|
* 目录被当作**一个 scope 的全集**(`coverage: "scope"`):目录里没有的域文件就是从未设过。
|
|
7
7
|
*
|
|
8
8
|
* 本模块不碰 `node:fs`:读写文件由入口 `bin/settings-schema.mjs` 注入(真绑定是 `./config-dir-io` 的 `nodeConfigDirectoryIo`,写盘用的是 `./atomic-write` 那一只原子写器 —— 写法与文件模式在那里一处定),所以测试可以直接驱动它。
|
|
9
9
|
* 不进主桶 —— 它是 bin 的实现,不是契约面。
|
|
10
10
|
*
|
|
11
|
-
* 读哪些文件:目录里 `<名>.json`,名是配置域或 retired 域(`runtime`)—— 便携与否不论(离线的 `workers.json` 一样迁)
|
|
12
|
-
* (
|
|
11
|
+
* 读哪些文件:目录里 `<名>.json`,名是配置域或 retired 域(`runtime`)—— 便携与否不论(离线的 `workers.json` 一样迁)—— 以及 `remote-exec.json`
|
|
12
|
+
* (一次性迁移 SS-M7 的座位走查也走它的两个端点);别的文件(隐藏文件、不认识的名、非 `.json`)不读不动。
|
|
13
13
|
*
|
|
14
14
|
* 零域文档分两形:**有条目却没有域文档**(任何非 `.` 开头的条目 —— 子目录、说明文件、不认识的名)⇒ 退出 2;**目录真空**(零条目,或只有以 `.` 开头的条目)⇒ 照常走判定,
|
|
15
15
|
* 零收据 = noop 退出 0 —— 新部署的空 `config.d` 是合法态,「没有东西可迁」不是「指错目录」(搬空之后再跑同一目录也因此幂等)。
|
|
16
16
|
*
|
|
17
17
|
* 退出码:0 = 已写回 / 无事可做(目录里有域文档只是没有要改的,或目录真空);1 = 有迁不了的坐标(收据 `refused`)**或**要写回的域文档整域读不进(整域终判,见 `migrateConfigDirectory`),**一字不写**;
|
|
18
|
-
* 2 =
|
|
18
|
+
* 2 = 用法错,或**环境错**(读目录 / 读文件 / 写盘 / 删文件的 I/O 错:权限、磁盘、悬空符号链接 …;stdout 空、stderr 一行点名路径与错误(errno 码在 Node 的错误句里),不打栈 ——
|
|
19
|
+
* rc 1 是「拒迁」的约定码,不许被一个权限错冒充),或某个域文件是坏文件(空 / 纯空白 / 坏 JSON / 不是 JSON 对象 —— 与 `FileConfigStore` 同一只判、同一句),**或目录里有条目却没有一个是域文档**
|
|
19
20
|
* (多半是填错了目录:要的是 `config.d` 目录本身;`<目录>/config.d` 在场时句子直接点它)(此时 stdout 为空);3 = `--check` 且会有改动、整域判得过(不写)。`--check` 与真跑过同一只判定,只在
|
|
20
21
|
* 「有改动、且整域判得过」那一支上分开(pending vs applied)—— 整域会被拒的目录 `--check` 也报 refused / rc 1。
|
|
21
22
|
*
|
|
@@ -28,16 +29,18 @@
|
|
|
28
29
|
*/
|
|
29
30
|
import { reportReceipt, joinDirPath, migrateConfigDirectory } from "./config-dir-migration.js";
|
|
30
31
|
import { MIGRATE_COMMAND_TEMPLATE, migrateCommandFor } from "./retired-form.js";
|
|
32
|
+
import { REMOTE_EXEC_DOCUMENT } from "./remote-exec.js";
|
|
31
33
|
import { DOMAINS, RETIRED_DOMAINS } from "./types.js";
|
|
32
|
-
/**
|
|
33
|
-
const COVERED_DOCUMENTS = [...DOMAINS, ...Object.keys(RETIRED_DOMAINS)];
|
|
34
|
+
/** 命令行的覆盖面:全部配置域、retired 域(便携与否不论 —— 离线的 `workers.json` 一样迁)与 `remote-exec.json`。 */
|
|
35
|
+
export const COVERED_DOCUMENTS = [...DOMAINS, ...Object.keys(RETIRED_DOMAINS), REMOTE_EXEC_DOCUMENT];
|
|
34
36
|
const USAGE = [
|
|
35
37
|
`usage: ${MIGRATE_COMMAND_TEMPLATE} [--check]`,
|
|
36
|
-
" Runs the one-time migrations (@sema-agent/settings-schema/one-time-migrations) over every <domain>.json in the directory, read as one",
|
|
38
|
+
" Runs the one-time migrations (@sema-agent/settings-schema/one-time-migrations) over every <domain>.json (and remote-exec.json) in the directory, read as one",
|
|
37
39
|
" scope (a missing domain file = never set): writes the rewritten files in place and removes retired-domain files (runtime.json) only when",
|
|
38
40
|
" nothing is refused and every document it would write reads as a whole domain; --check runs the same verdict and only reports (exit 3",
|
|
39
41
|
" if something would change). The argument is the config.d directory itself; a directory that has entries but no <domain>.json among them is",
|
|
40
|
-
" refused (exit 2); an empty directory has nothing to migrate (exit 0).",
|
|
42
|
+
" refused (exit 2); an empty directory has nothing to migrate (exit 0). An I/O failure (reading, writing or removing a file) exits 2 with",
|
|
43
|
+
" one line on stderr naming the path and the error.",
|
|
41
44
|
" stdout (exit 0 / 1 / 3): line 1 = outcome=<refused|applied|noop|pending> written=<true|false>, line 2 = the JSON report",
|
|
42
45
|
" (applied only when written=true; otherwise wouldApply).",
|
|
43
46
|
].join("\n");
|
|
@@ -69,6 +72,35 @@ function reportOf(outcome, dir, applied, refused) {
|
|
|
69
72
|
});
|
|
70
73
|
return `outcome=${outcome} written=${written}\n${json}\n`;
|
|
71
74
|
}
|
|
75
|
+
/** 写回阶段(写盘 / 删文件)的一次 I/O 失败 —— 环境错一臂(rc 2)。只由 {@link withEnvironmentArm} 包的两个写口铸;别的抛错(包内守卫、误用)不是它,照旧抛。 */
|
|
76
|
+
class EnvironmentFailure extends Error {
|
|
77
|
+
}
|
|
78
|
+
const oneLine = (text) => text.replace(/\s*\n\s*/g, " ");
|
|
79
|
+
const messageOf = (err) => oneLine(err instanceof Error ? err.message : String(err));
|
|
80
|
+
/** 把注入的写口包一层:写盘 / 删文件抛 ⇒ {@link EnvironmentFailure},句子点名**要写的那份文件**(原子写器的错误句里是临时文件的路径)与原错误句。
|
|
81
|
+
* 读口不包:读目录 / 读文件错由 `migrateConfigDirectory` 自己收成 `unreadable`(同为 rc 2)。 */
|
|
82
|
+
function withEnvironmentArm(io) {
|
|
83
|
+
return {
|
|
84
|
+
readDir: (path) => io.readDir(path),
|
|
85
|
+
readFile: (path) => io.readFile(path),
|
|
86
|
+
writeFileAtomic: async (path, text) => {
|
|
87
|
+
try {
|
|
88
|
+
await io.writeFileAtomic(path, text);
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
throw new EnvironmentFailure(`cannot write ${path}: ${messageOf(err)}`, { cause: err });
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
removeFile: async (path) => {
|
|
95
|
+
try {
|
|
96
|
+
await io.removeFile(path);
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
throw new EnvironmentFailure(`cannot remove ${path}: ${messageOf(err)}`, { cause: err });
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
}
|
|
72
104
|
/** 「有条目却没有域文档」的拒句:点名这个目录、说要的是 `config.d` 目录本身;`<dir>/config.d` 在场(探一次)就直接点它 —— 最常见的错是填了它的上一层。指路的命令里路径过 shell 引用,可直接粘贴。 */
|
|
73
105
|
async function noDomainDocumentsSentence(dir, io) {
|
|
74
106
|
const nested = joinDirPath(dir, "config.d");
|
|
@@ -85,10 +117,20 @@ export async function runSettingsSchemaCli(argv, io) {
|
|
|
85
117
|
io.stderr(`${USAGE}\n`);
|
|
86
118
|
return 2;
|
|
87
119
|
}
|
|
88
|
-
|
|
120
|
+
let m;
|
|
121
|
+
try {
|
|
122
|
+
m = await migrateConfigDirectory(dir, { documents: COVERED_DOCUMENTS, io: withEnvironmentArm(io), check });
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
if (!(err instanceof EnvironmentFailure))
|
|
126
|
+
throw err;
|
|
127
|
+
// 写回阶段断在这里:之前写完的文档已在新形上,再跑一次幂等收尾(迁移只认形)。stdout 不出结论行 —— 没有 `outcome=applied`。
|
|
128
|
+
io.stderr(`settings-schema: ${err.message} — files written before it keep their migrated form; fix the cause and re-run\n`);
|
|
129
|
+
return 2;
|
|
130
|
+
}
|
|
89
131
|
switch (m.kind) {
|
|
90
132
|
case "unreadable":
|
|
91
|
-
io.stderr(`settings-schema: ${m.reason}\n`);
|
|
133
|
+
io.stderr(`settings-schema: ${oneLine(m.reason)}\n`);
|
|
92
134
|
return 2;
|
|
93
135
|
case "no-documents":
|
|
94
136
|
if (m.entries.some((name) => !name.startsWith("."))) {
|
package/dist/config-d-file.d.ts
CHANGED
|
@@ -1,4 +1,15 @@
|
|
|
1
|
+
import { RemoteExecSpec } from "./remote-exec.js";
|
|
1
2
|
import { type DomainName, type RetiredDomainName } from "./types.js";
|
|
3
|
+
/** 一份 `config.d` 文档写盘的**文本形** —— 两格缩进的 JSON 加一个换行。写域文件(`writeDomainFile`)、写 `remote-exec.json`(`writeRemoteExec`)与目录迁移写回
|
|
4
|
+
* 都用这一只(此前三处各写一遍同一行),于是迁移写回的文件与编辑器写出的同一份文件逐字节同形。 */
|
|
5
|
+
export declare function configDDocumentText(value: unknown): string;
|
|
6
|
+
/** `config.d/remote-exec.json` 在场时的**文本** → 它的 JSON 值(纯判,零 I/O)—— `loadRemoteExec` 与目录迁移读同一只、同一句(从 `loadRemoteExec` 里搬出)。
|
|
7
|
+
* 空 / 纯空白(半写、截断 —— 不是「用默认」:默认车道要的是文件**缺席**)、坏 JSON ⇒ 抛,句子点名 `filePath`、不回显原文。值的形由
|
|
8
|
+
* {@link parseRemoteExecDocument} 判(不是对象也归它拒)。 */
|
|
9
|
+
export declare function judgeRemoteExecText(filePath: string, text: string): unknown;
|
|
10
|
+
/** 判一份 remote-exec 文档的值(`RemoteExecSpec`),错误**脱敏**(zod 会回显出错的值 —— 粘错位置的密钥不许随抛出的错走);反射访问抛(恶意 getter /
|
|
11
|
+
* Proxy)⇒ 定句。`loadRemoteExec` / `writeRemoteExec` 与目录迁移的整文档终判调同一只。 */
|
|
12
|
+
export declare function parseRemoteExecDocument(raw: unknown): RemoteExecSpec;
|
|
2
13
|
/** `<root>/config.d/<名>.json` 的路径 —— 名字的判也在这里(**一处**):不是配置域也不是 retired 域 ⇒ 拒(不拿用户给的名拼路径,防 `../` 穿越)。 */
|
|
3
14
|
export declare function configDPath(root: string, name: string): string;
|
|
4
15
|
/** 一个 `config.d/<名>.json` 在场时的**文本** → 它的值(纯判,零 I/O;见文件头的律):一个 JSON 对象 ⇒ 它;空 / 纯空白、坏 JSON、
|
package/dist/config-d-file.js
CHANGED
|
@@ -9,8 +9,42 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { readFile } from "node:fs/promises";
|
|
11
11
|
import path from "node:path";
|
|
12
|
-
import { refuseRetiredDomain } from "./config-fns.js";
|
|
12
|
+
import { refuseRetiredDomain, sanitizeZodError } from "./config-fns.js";
|
|
13
|
+
import { RemoteExecSpec } from "./remote-exec.js";
|
|
13
14
|
import { RETIRED_DOMAINS, isDomainName, isRetiredDomainName } from "./types.js";
|
|
15
|
+
/** 一份 `config.d` 文档写盘的**文本形** —— 两格缩进的 JSON 加一个换行。写域文件(`writeDomainFile`)、写 `remote-exec.json`(`writeRemoteExec`)与目录迁移写回
|
|
16
|
+
* 都用这一只(此前三处各写一遍同一行),于是迁移写回的文件与编辑器写出的同一份文件逐字节同形。 */
|
|
17
|
+
export function configDDocumentText(value) {
|
|
18
|
+
return `${JSON.stringify(value, null, 2)}\n`;
|
|
19
|
+
}
|
|
20
|
+
/** `config.d/remote-exec.json` 在场时的**文本** → 它的 JSON 值(纯判,零 I/O)—— `loadRemoteExec` 与目录迁移读同一只、同一句(从 `loadRemoteExec` 里搬出)。
|
|
21
|
+
* 空 / 纯空白(半写、截断 —— 不是「用默认」:默认车道要的是文件**缺席**)、坏 JSON ⇒ 抛,句子点名 `filePath`、不回显原文。值的形由
|
|
22
|
+
* {@link parseRemoteExecDocument} 判(不是对象也归它拒)。 */
|
|
23
|
+
export function judgeRemoteExecText(filePath, text) {
|
|
24
|
+
const trimmed = text.trim();
|
|
25
|
+
if (!trimmed)
|
|
26
|
+
throw new Error(`${filePath} is empty/corrupt — delete it for the host default, or write a valid remoteExec spec`);
|
|
27
|
+
try {
|
|
28
|
+
return JSON.parse(trimmed); // sanitized: don't let a SyntaxError echo the raw (possibly secret-bearing) file text
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
throw new Error(`${filePath} — invalid JSON (delete it for the host default, or write a valid remoteExec spec)`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/** 判一份 remote-exec 文档的值(`RemoteExecSpec`),错误**脱敏**(zod 会回显出错的值 —— 粘错位置的密钥不许随抛出的错走);反射访问抛(恶意 getter /
|
|
35
|
+
* Proxy)⇒ 定句。`loadRemoteExec` / `writeRemoteExec` 与目录迁移的整文档终判调同一只。 */
|
|
36
|
+
export function parseRemoteExecDocument(raw) {
|
|
37
|
+
let r;
|
|
38
|
+
try {
|
|
39
|
+
r = RemoteExecSpec.safeParse(raw);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
throw new Error("invalid remoteExec (a value could not be read during validation)");
|
|
43
|
+
}
|
|
44
|
+
if (r.success)
|
|
45
|
+
return r.data;
|
|
46
|
+
throw sanitizeZodError(r.error);
|
|
47
|
+
}
|
|
14
48
|
/** `<root>/config.d/<名>.json` 的路径 —— 名字的判也在这里(**一处**):不是配置域也不是 retired 域 ⇒ 拒(不拿用户给的名拼路径,防 `../` 穿越)。 */
|
|
15
49
|
export function configDPath(root, name) {
|
|
16
50
|
if (!isDomainName(name) && !isRetiredDomainName(name))
|
package/dist/config-dir-io.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `migrateConfigDirectory`(`./config-dir-migration`)的**真 I/O 绑定**(Node 专用):读目录 / 读文件 / 删文件走 `node:fs/promises`,写盘走本包唯一的原子写器
|
|
3
3
|
* `./atomic-write`(同目录临时文件 → rename;`config-document` 类的模式规则:新建 0o644 经 umask、已有的沿用原模式;目标是符号链接则写到它指向的真文件)。
|
|
4
|
+
* 读目录 / 读文件先过写器同一只链接判法 `resolveSymlinkTarget`:链接读它指向的真文件 / 真目录,**悬空链接**抛与写器同一句(改前读阶段先撞 ENOENT,句子不提链接)。
|
|
4
5
|
* 命令行入口与引擎起服的目录迁移注入同一份,写法与文件模式因此只在原子写器那一处定。
|
|
5
6
|
*/
|
|
6
7
|
import { readdir, readFile, unlink } from "node:fs/promises";
|
|
7
|
-
import { writeFileAtomic } from "./atomic-write.js";
|
|
8
|
+
import { resolveSymlinkTarget, writeFileAtomic } from "./atomic-write.js";
|
|
8
9
|
export const nodeConfigDirectoryIo = {
|
|
9
|
-
readDir: (path) => readdir(path),
|
|
10
|
-
readFile: (path) => readFile(path, "utf8"),
|
|
10
|
+
readDir: async (path) => readdir(resolveSymlinkTarget(path)),
|
|
11
|
+
readFile: async (path) => readFile(resolveSymlinkTarget(path), "utf8"),
|
|
11
12
|
writeFileAtomic: async (path, text) => writeFileAtomic(path, text, "config-document"), // the files this migration writes are config.d domain documents
|
|
12
13
|
removeFile: (path) => unlink(path),
|
|
13
14
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type MigrationReceipt } from "./one-time-migrations.js";
|
|
2
|
+
import { REMOTE_EXEC_DOCUMENT } from "./remote-exec.js";
|
|
2
3
|
import { type DomainName, type RetiredDomainName } from "./types.js";
|
|
3
4
|
/** 注入的 I/O(本模块不碰 `node:fs`)。`writeFileAtomic` 必须原子(写临时文件再改名):半写的域文档比旧文档更糟。 */
|
|
4
5
|
export interface ConfigDirectoryIo {
|
|
@@ -43,12 +44,14 @@ export type ConfigDirectoryMigration = {
|
|
|
43
44
|
readonly removed: readonly string[];
|
|
44
45
|
};
|
|
45
46
|
export interface MigrateConfigDirectoryOptions {
|
|
46
|
-
/**
|
|
47
|
-
readonly documents: readonly
|
|
47
|
+
/** 覆盖面:调用方读的文档名集(配置域、retired 域,或 `remote-exec`)。拼错的名字 ⇒ 抛 `TypeError`,不让它悄悄变成「什么都没覆盖」。 */
|
|
48
|
+
readonly documents: readonly ConfigDirectoryDocument[];
|
|
48
49
|
readonly io: ConfigDirectoryIo;
|
|
49
50
|
/** 只判、不写:有改动且整域判得过 ⇒ `pending`。默认 `false`。 */
|
|
50
51
|
readonly check?: boolean;
|
|
51
52
|
}
|
|
53
|
+
/** 目录迁移能读写的文档名:配置域、retired 域、`remote-exec`(`config.d/remote-exec.json`)。 */
|
|
54
|
+
export type ConfigDirectoryDocument = DomainName | RetiredDomainName | typeof REMOTE_EXEC_DOCUMENT;
|
|
52
55
|
/** 目录加文件名(目录尾部的分隔符折掉)。包内私有(不进 `./node`)。 */
|
|
53
56
|
export declare const joinDirPath: (dir: string, name: string) => string;
|
|
54
57
|
type RefusedReceipt = Extract<MigrationReceipt, {
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* **一个 `config.d` 目录的一次性迁移** —— 「读目录 → 迁 → 整域终判 → 原子写 → 删 retired 域文件」这一条语义的唯一实现。命令行(`migrate <config.d 目录> [--check]`,
|
|
3
3
|
* 见 `./cli`)是它的渲染器(退出码 + 报告行);引擎起服时对本地 `config.d` 的迁移调同一只函数,不按同语义自写一份。
|
|
4
4
|
*
|
|
5
|
-
* **覆盖面 =
|
|
5
|
+
* **覆盖面 = 调用方读的文档名集**({@link MigrateConfigDirectoryOptions.documents}:配置域、retired 域,还可以是 `remote-exec` ——
|
|
6
|
+
* `config.d/remote-exec.json`,一次性迁移 SS-M7 的座位走查也读它;它与域文档同一条路:读 → 迁 → 改过的整文档终判 → 同一只写器写回):覆盖面内没有的域文件就是从未设过(一次性迁移的 `coverage: "scope"`);
|
|
6
7
|
* 覆盖面外的文件不读、不判、不写、不删 —— 本调用方没有资格改写它从不消费的数据,它们是坏文件也挡不住覆盖面内的迁移。命令行的覆盖面是全部配置域加 retired 域;
|
|
7
8
|
* 引擎的覆盖面是它本地读腿会读的那几份。**迁移不许越过覆盖面去写**:retired 的 `runtime` 域迁出时要写 `governance` 与 `limits`(`RUNTIME_LIFT_TARGETS`),
|
|
8
9
|
* 覆盖面没把这两个域一并包进来时,目录算不上「一个 scope 的全集」—— 迁移按 `coverage: "partial"` 走(`runtime` 在那一支上以收据 `refused` 拒迁、一字不写),
|
|
@@ -22,10 +23,23 @@
|
|
|
22
23
|
*/
|
|
23
24
|
import { z } from "zod";
|
|
24
25
|
import { formatConfigPath, parseDomain } from "./config-fns.js";
|
|
25
|
-
import { judgeConfigDText } from "./config-d-file.js";
|
|
26
|
+
import { configDDocumentText, judgeConfigDText, judgeRemoteExecText, parseRemoteExecDocument } from "./config-d-file.js";
|
|
26
27
|
import { migrateStoredDocuments } from "./one-time-migrations.js";
|
|
28
|
+
import { REMOTE_EXEC_DOCUMENT } from "./remote-exec.js";
|
|
27
29
|
import { RUNTIME_LIFT_TARGETS } from "./runtime-lift-targets.js";
|
|
28
30
|
import { isDomainName, isRetiredDomainName } from "./types.js";
|
|
31
|
+
const isConfigDirectoryDocument = (name) => isDomainName(name) || isRetiredDomainName(name) || name === REMOTE_EXEC_DOCUMENT;
|
|
32
|
+
/** 一份文档在场时的文本 → 值:域 / retired 域过 `judgeConfigDText`(与 `FileConfigStore` 同一只),`remote-exec` 过 `judgeRemoteExecText`(与 `loadRemoteExec` 同一只)。 */
|
|
33
|
+
function judgeDocumentText(dir, name, text) {
|
|
34
|
+
return name === REMOTE_EXEC_DOCUMENT ? judgeRemoteExecText(joinDirPath(dir, `${name}.json`), text) : judgeConfigDText(name, text);
|
|
35
|
+
}
|
|
36
|
+
/** 改过的文档的整文档终判:域过读侧那一只 `parseDomain`,`remote-exec` 过 `loadRemoteExec` 那一只 `parseRemoteExecDocument`;retired 域不终判(它只会被删)。 */
|
|
37
|
+
function judgeWholeDocument(name, doc) {
|
|
38
|
+
if (isDomainName(name))
|
|
39
|
+
parseDomain(name, doc);
|
|
40
|
+
else if (name === REMOTE_EXEC_DOCUMENT)
|
|
41
|
+
parseRemoteExecDocument(doc);
|
|
42
|
+
}
|
|
29
43
|
/** 目录加文件名(目录尾部的分隔符折掉)。包内私有(不进 `./node`)。 */
|
|
30
44
|
export const joinDirPath = (dir, name) => `${dir.replace(/[\\/]+$/, "")}/${name}`;
|
|
31
45
|
export function reportReceipt(r) {
|
|
@@ -63,10 +77,10 @@ function verdictOf(input, migrated, receipts) {
|
|
|
63
77
|
return { kind: "noop" };
|
|
64
78
|
const refused = [];
|
|
65
79
|
for (const [name, doc] of Object.entries(migrated)) {
|
|
66
|
-
if (
|
|
80
|
+
if (Object.prototype.hasOwnProperty.call(input, name) && input[name] === doc)
|
|
67
81
|
continue;
|
|
68
82
|
try {
|
|
69
|
-
|
|
83
|
+
judgeWholeDocument(name, doc);
|
|
70
84
|
}
|
|
71
85
|
catch (err) {
|
|
72
86
|
if (err instanceof z.ZodError)
|
|
@@ -89,8 +103,8 @@ const messageOf = (err) => (err instanceof Error ? err.message : String(err));
|
|
|
89
103
|
export async function migrateConfigDirectory(dir, opts) {
|
|
90
104
|
const covered = new Set();
|
|
91
105
|
for (const name of opts.documents) {
|
|
92
|
-
if (!
|
|
93
|
-
throw new TypeError(`migrateConfigDirectory: "${String(name)}" is not a config domain
|
|
106
|
+
if (!isConfigDirectoryDocument(name))
|
|
107
|
+
throw new TypeError(`migrateConfigDirectory: "${String(name)}" is not a config domain, a retired domain or remote-exec — the coverage names the documents the caller reads`);
|
|
94
108
|
covered.add(name);
|
|
95
109
|
}
|
|
96
110
|
const { io } = opts;
|
|
@@ -106,7 +120,7 @@ export async function migrateConfigDirectory(dir, opts) {
|
|
|
106
120
|
if (name.startsWith(".") || !name.endsWith(".json"))
|
|
107
121
|
continue;
|
|
108
122
|
const base = name.slice(0, -".json".length);
|
|
109
|
-
if (!covered.has(base))
|
|
123
|
+
if (!covered.has(base) || !isConfigDirectoryDocument(base))
|
|
110
124
|
continue;
|
|
111
125
|
let text;
|
|
112
126
|
try {
|
|
@@ -116,7 +130,7 @@ export async function migrateConfigDirectory(dir, opts) {
|
|
|
116
130
|
return { kind: "unreadable", reason: `cannot read ${name}: ${messageOf(err)}` };
|
|
117
131
|
}
|
|
118
132
|
try {
|
|
119
|
-
docs[base] =
|
|
133
|
+
docs[base] = judgeDocumentText(dir, base, text); // the one judge per document kind: a domain file's is FileConfigStore's, remote-exec.json's is loadRemoteExec's
|
|
120
134
|
}
|
|
121
135
|
catch (err) {
|
|
122
136
|
return { kind: "unreadable", reason: messageOf(err) };
|
|
@@ -154,7 +168,7 @@ export async function migrateConfigDirectory(dir, opts) {
|
|
|
154
168
|
for (const [name, doc] of Object.entries(m.docs)) {
|
|
155
169
|
if (Object.prototype.hasOwnProperty.call(docs, name) && docs[name] === doc)
|
|
156
170
|
continue;
|
|
157
|
-
await io.writeFileAtomic(joinDirPath(dir, `${name}.json`),
|
|
171
|
+
await io.writeFileAtomic(joinDirPath(dir, `${name}.json`), configDDocumentText(doc)); // the one config.d document text (writeDomainFile / writeRemoteExec write the same)
|
|
158
172
|
written.push(`${name}.json`);
|
|
159
173
|
}
|
|
160
174
|
for (const name of Object.keys(docs)) {
|
package/dist/cross-domain.d.ts
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import type { ConfigReader } from "./reader.js";
|
|
14
14
|
import type { DomainConfig } from "./types.js";
|
|
15
|
+
import { type QuotedIssueParams } from "./quoted-issue.js";
|
|
15
16
|
/** What a dangling reference failed to find —— the closed set (S-1075, 11.0.0): one word per referenced domain. */
|
|
16
17
|
export declare const REF_ISSUE_KINDS: readonly ["unknown_model", "unknown_roster", "unknown_worker", "unknown_skill", "unknown_mcp", "unknown_plugin"];
|
|
17
18
|
export type RefIssueKind = (typeof REF_ISSUE_KINDS)[number];
|
|
@@ -37,7 +38,9 @@ export interface RefReferrer {
|
|
|
37
38
|
* holds the reference), `target` (the name that was not found), `path` (where the reference sits; `path[0]` = the referrer's
|
|
38
39
|
* domain key). `message` is {@link renderRefIssue} of them — a reader that must treat the user-written values differently from
|
|
39
40
|
* the sentence's fixed words (a log boundary fingerprinting each value) re-renders with its own `show` instead of parsing the
|
|
40
|
-
* sentence. Before 11.0.0 the values were only inside `message`.
|
|
41
|
+
* sentence. Before 11.0.0 the values were only inside `message`. `params` is the same sentence in the form every custom
|
|
42
|
+
* issue of this package carries (`QuotedIssueParams`: fixed words + the two values as operands) —— a reader that reads custom issues one
|
|
43
|
+
* way reads this one the same way. */
|
|
41
44
|
export interface RefIssue {
|
|
42
45
|
path: (string | number)[];
|
|
43
46
|
code: "custom";
|
|
@@ -45,10 +48,12 @@ export interface RefIssue {
|
|
|
45
48
|
referrer: RefReferrer;
|
|
46
49
|
target: string;
|
|
47
50
|
message: string;
|
|
51
|
+
params: QuotedIssueParams;
|
|
48
52
|
}
|
|
49
53
|
/** The ONE rendering of a {@link RefIssue}'s sentence: `<referrer> → unknown <noun> "<target>" (at <path>)`. `show` maps each
|
|
50
|
-
* user-written value (the referrer's name, the target); the default shows them as written — that is how `message` is minted.
|
|
51
|
-
|
|
54
|
+
* user-written value (the referrer's name, the target); the default shows them as written — that is how `message` is minted.
|
|
55
|
+
* It renders {@link refIssueSentence}'s params through `renderQuotedSentence` (the one renderer of every custom issue's sentence). */
|
|
56
|
+
export declare function renderRefIssue(issue: Omit<RefIssue, "message" | "params">, show?: (value: string) => string): string;
|
|
52
57
|
/** Resolve a sibling domain's effective value for integrity checks. */
|
|
53
58
|
export type SiblingResolver = <K extends keyof DomainConfig>(domain: K) => Promise<DomainConfig[K] | undefined>;
|
|
54
59
|
/** A resolver over the reader, optionally shadowed by pending (same-operation) writes keyed by domain.
|
package/dist/cross-domain.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { formatConfigPath } from "./config-fns.js";
|
|
2
|
+
import { buildQuotedIssue, fixed, renderQuotedSentence } from "./quoted-issue.js";
|
|
2
3
|
/** What a dangling reference failed to find —— the closed set (S-1075, 11.0.0): one word per referenced domain. */
|
|
3
4
|
export const REF_ISSUE_KINDS = ["unknown_model", "unknown_roster", "unknown_worker", "unknown_skill", "unknown_mcp", "unknown_plugin"];
|
|
4
5
|
/** The domain a {@link RefIssueKind}'s target lives in. */
|
|
@@ -29,33 +30,42 @@ function refTargetNoun(kind) {
|
|
|
29
30
|
return "plugin";
|
|
30
31
|
}
|
|
31
32
|
}
|
|
32
|
-
/** How a referrer is named in the sentence (its name
|
|
33
|
-
function
|
|
34
|
-
const named = (noun) => (r.name === undefined ? noun : `${noun} "${show(r.name)}"`);
|
|
33
|
+
/** How a referrer is named in the sentence: the noun for its domain (its own name, when it has one, follows as an operand). */
|
|
34
|
+
function refReferrerNoun(r) {
|
|
35
35
|
switch (r.domain) {
|
|
36
36
|
case "rosters":
|
|
37
|
-
return
|
|
37
|
+
return "roster";
|
|
38
38
|
case "workers":
|
|
39
|
-
return
|
|
39
|
+
return "worker";
|
|
40
40
|
case "models":
|
|
41
|
-
return
|
|
41
|
+
return "models";
|
|
42
42
|
case "collab":
|
|
43
|
-
return
|
|
43
|
+
return "collab template";
|
|
44
44
|
case "systems":
|
|
45
|
-
return
|
|
45
|
+
return "system";
|
|
46
46
|
case "entitlement":
|
|
47
|
-
return
|
|
47
|
+
return "tier";
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
|
+
/** A {@link RefIssue}'s sentence —— THE one sentence builder (`<referrer> → unknown <noun> "<target>" (at <path>)`): the referrer's name and
|
|
51
|
+
* the target are operands, everything else (nouns, the coordinate) is fixed text. `message`, `params` and {@link renderRefIssue} all come from it. */
|
|
52
|
+
function refIssueSentence(issue) {
|
|
53
|
+
const noun = refReferrerNoun(issue.referrer);
|
|
54
|
+
const at = formatConfigPath(issue.path);
|
|
55
|
+
return issue.referrer.name === undefined
|
|
56
|
+
? buildQuotedIssue `${fixed(noun)} → unknown ${fixed(refTargetNoun(issue.kind))} "${issue.target}" (at ${fixed(at)})`
|
|
57
|
+
: buildQuotedIssue `${fixed(noun)} "${issue.referrer.name}" → unknown ${fixed(refTargetNoun(issue.kind))} "${issue.target}" (at ${fixed(at)})`;
|
|
58
|
+
}
|
|
50
59
|
/** The ONE rendering of a {@link RefIssue}'s sentence: `<referrer> → unknown <noun> "<target>" (at <path>)`. `show` maps each
|
|
51
|
-
* user-written value (the referrer's name, the target); the default shows them as written — that is how `message` is minted.
|
|
60
|
+
* user-written value (the referrer's name, the target); the default shows them as written — that is how `message` is minted.
|
|
61
|
+
* It renders {@link refIssueSentence}'s params through `renderQuotedSentence` (the one renderer of every custom issue's sentence). */
|
|
52
62
|
export function renderRefIssue(issue, show = (v) => v) {
|
|
53
|
-
return
|
|
63
|
+
return renderQuotedSentence(refIssueSentence(issue).params, (operand) => show(String(operand)));
|
|
54
64
|
}
|
|
55
65
|
/** The one minting point of a {@link RefIssue}. */
|
|
56
66
|
function danglingRef(path, kind, referrer, target) {
|
|
57
|
-
const
|
|
58
|
-
return {
|
|
67
|
+
const sentence = refIssueSentence({ path, kind, referrer, target });
|
|
68
|
+
return { path, code: "custom", kind, referrer, target, ...sentence };
|
|
59
69
|
}
|
|
60
70
|
/** A resolver over the reader, optionally shadowed by pending (same-operation) writes keyed by domain.
|
|
61
71
|
* Widened from the fat ConfigStore to the narrow {@link ConfigReader} (it only calls `getDomain`) so a TOC
|
package/dist/file-edit.js
CHANGED
|
@@ -9,7 +9,9 @@ import { mkdir } from "node:fs/promises";
|
|
|
9
9
|
import path from "node:path";
|
|
10
10
|
import { writeFileAtomic } from "./atomic-write.js";
|
|
11
11
|
import { collectEnvRefs } from "./secret-refs.js";
|
|
12
|
-
import { RemoteExecSpec } from "./remote-exec.js";
|
|
12
|
+
import { REMOTE_EXEC_DOCUMENT, RemoteExecSpec } from "./remote-exec.js";
|
|
13
|
+
import { configDDocumentText } from "./config-d-file.js";
|
|
14
|
+
import { buildQuotedIssue, fixed } from "./quoted-issue.js";
|
|
13
15
|
import { RetiredDomainError, parseDomain, sanitizeZodError } from "./config-fns.js";
|
|
14
16
|
import { DOMAIN_SCHEMAS, isDomainName, isRetiredDomainName } from "./types.js";
|
|
15
17
|
/** Throw a CLEAR error for a non-domain key instead of the cryptic `undefined.parse(...)` you'd get from
|
|
@@ -21,7 +23,7 @@ function assertDomainName(domain) {
|
|
|
21
23
|
if (isRetiredDomainName(domain))
|
|
22
24
|
throw new RetiredDomainError(domain);
|
|
23
25
|
if (!isDomainName(domain)) {
|
|
24
|
-
const hint = domain ===
|
|
26
|
+
const hint = domain === REMOTE_EXEC_DOCUMENT ? " — the exec backend is NOT a domain; use writeRemoteExec (config.d/remote-exec.json)" : "";
|
|
25
27
|
throw new Error(`"${domain}" is not a config domain (expected one of: ${DOMAINS_LIST})${hint}`);
|
|
26
28
|
}
|
|
27
29
|
}
|
|
@@ -37,7 +39,7 @@ export function validateDomain(domain, raw) {
|
|
|
37
39
|
}
|
|
38
40
|
catch {
|
|
39
41
|
// a hostile getter/Proxy trap threw during traversal — never surface its (secret-bearing) message. R13.
|
|
40
|
-
return { ok: false, issues: [{ code: "custom",
|
|
42
|
+
return { ok: false, issues: [{ code: "custom", ...buildQuotedIssue `invalid "${fixed(domain)}" config (a value could not be read during validation)`, path: [] }] };
|
|
41
43
|
}
|
|
42
44
|
if (r.success)
|
|
43
45
|
return { ok: true, value: r.data };
|
|
@@ -50,7 +52,7 @@ export function validateRemoteExec(raw) {
|
|
|
50
52
|
r = RemoteExecSpec.safeParse(raw);
|
|
51
53
|
}
|
|
52
54
|
catch {
|
|
53
|
-
return { ok: false, issues: [{ code: "custom",
|
|
55
|
+
return { ok: false, issues: [{ code: "custom", ...buildQuotedIssue `invalid remoteExec (a value could not be read during validation)`, path: [] }] };
|
|
54
56
|
}
|
|
55
57
|
return r.success ? { ok: true, value: r.data } : { ok: false, issues: sanitizeZodError(r.error).issues }; // no secret echo (R14)
|
|
56
58
|
}
|
|
@@ -68,7 +70,7 @@ export async function writeDomainFile(root, domain, value) {
|
|
|
68
70
|
const parsed = parseDomain(domain, value); // routes through the SANITIZED parser (explicit-null reject R4 + no secret echo R14)
|
|
69
71
|
const dir = path.join(root, "config.d");
|
|
70
72
|
await mkdir(dir, { recursive: true });
|
|
71
|
-
writeFileAtomic(path.join(dir, `${domain}.json`),
|
|
73
|
+
writeFileAtomic(path.join(dir, `${domain}.json`), configDDocumentText(parsed), "config-document");
|
|
72
74
|
return parsed;
|
|
73
75
|
}
|
|
74
76
|
/** LIST every env-NAME the current local config requires (across models/workers/mcp + the
|
package/dist/file-store.js
CHANGED
|
@@ -29,6 +29,7 @@ import path from "node:path";
|
|
|
29
29
|
import { buildEffective, DOMAIN_READ_FALLBACK, parseDomainLoud } from "./config-fns.js";
|
|
30
30
|
import { configDPath, readConfigDFile, refuseRetiredDomainFiles } from "./config-d-file.js";
|
|
31
31
|
import { stableHash } from "./hash.js";
|
|
32
|
+
import { REMOTE_EXEC_DOCUMENT } from "./remote-exec.js";
|
|
32
33
|
import { RETIRED_DOMAINS, isDomainName, isRetiredDomainName } from "./types.js";
|
|
33
34
|
/** The retired domain names (`RETIRED_DOMAINS` keys) — read from `config.d/` for their tombstone only. */
|
|
34
35
|
const RETIRED_DOMAIN_NAMES = Object.keys(RETIRED_DOMAINS);
|
|
@@ -53,7 +54,7 @@ export const PORTABLE_DOMAINS = ["models", "rosters", "skills", "mcp", "plugins"
|
|
|
53
54
|
*
|
|
54
55
|
* **导出**(3.0.0):一致性门的语料派生要按同一张表给本地腿的文件分类(域文件 / 专用文件 / 忽略),
|
|
55
56
|
* 手抄一份必漂 —— 漂了的那一天,门会把一个合法的 `remote-exec.json` 报成未知文件。 */
|
|
56
|
-
export const NON_DOMAIN_CONFIG_FILES = new Set([
|
|
57
|
+
export const NON_DOMAIN_CONFIG_FILES = new Set([`${REMOTE_EXEC_DOCUMENT}.json`]);
|
|
57
58
|
/**
|
|
58
59
|
* 非便携域的**排除理由表**(#282 件5 附带裁定:禁裸排除)。
|
|
59
60
|
*
|
package/dist/fleet.js
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
import { z } from "zod";
|
|
21
21
|
import { INTEGER } from "./integer.js";
|
|
22
22
|
import { fnv1a64 } from "./hash.js";
|
|
23
|
+
import { buildQuotedIssue } from "./quoted-issue.js";
|
|
23
24
|
// ── Web-admin's fleet endpoints (paths, one spelling) ─────────────────────────────────────────────────────
|
|
24
25
|
/**
|
|
25
26
|
* Web-admin's `/api/fleet/*` endpoints that are addressed from outside web-admin, relative to its origin. ONE spelling: whoever
|
|
@@ -115,7 +116,7 @@ export function compareGeneration(a, b) {
|
|
|
115
116
|
return a === b ? 0 : a < b ? -1 : 1;
|
|
116
117
|
}
|
|
117
118
|
/** The generation field of every wire shape that carries one — a string judged by {@link isCanonicalGeneration}. */
|
|
118
|
-
const Generation = z.string().refine(isCanonicalGeneration,
|
|
119
|
+
const Generation = z.string().refine(isCanonicalGeneration, buildQuotedIssue `a generation is a canonical non-negative decimal string (no sign, no leading zero)`);
|
|
119
120
|
/**
|
|
120
121
|
* The serving lease as the ENGINE holds it — a CLOSED set of four words, owned here (the engine's vocabulary, word for word;
|
|
121
122
|
* a reader that switches on it exhaustively is told by the compiler when a word is added):
|
|
@@ -271,9 +272,7 @@ export const ModelRefusalsWire = z
|
|
|
271
272
|
entries: z.record(z.string().min(1), z.string().min(1)),
|
|
272
273
|
plane: z.string().min(1).optional(),
|
|
273
274
|
})
|
|
274
|
-
.refine((s) => Object.keys(s.entries).length > 0 || s.plane !== undefined,
|
|
275
|
-
message: "an empty model-refusals signal is not a signal — omit the key instead (absent ⇔ no reportable refusals)",
|
|
276
|
-
});
|
|
275
|
+
.refine((s) => Object.keys(s.entries).length > 0 || s.plane !== undefined, buildQuotedIssue `an empty model-refusals signal is not a signal — omit the key instead (absent ⇔ no reportable refusals)`);
|
|
277
276
|
const isModelEntryRefusalCode = (c) => MODEL_ENTRY_REFUSAL_CODES.includes(c);
|
|
278
277
|
const isModelPlaneRefusalCode = (c) => MODEL_PLANE_REFUSAL_CODES.includes(c);
|
|
279
278
|
/**
|
package/dist/hooks.d.ts
CHANGED
|
@@ -2267,9 +2267,22 @@ export type HookMatcherEntry = z.infer<typeof HookMatcherEntry>;
|
|
|
2267
2267
|
* for the bad matcher itself.
|
|
2268
2268
|
* Before 11.0.0 the server anchored the regex form (`^(?:…)$`) while the shell did not, so one regex matcher selected different
|
|
2269
2269
|
* tools in the two places. A deployment whose regex matchers relied on that anchoring selects MORE tools now — add `^…$`.
|
|
2270
|
-
* Pure: no budget / timeout — a caller evaluating untrusted regex matchers under a deadline does so around this call
|
|
2270
|
+
* Pure: no budget / timeout — a caller evaluating untrusted regex matchers under a deadline does so around this call
|
|
2271
|
+
* ({@link hookMatcherForm} tells it beforehand which matchers run a regex at all).
|
|
2271
2272
|
*/
|
|
2272
2273
|
export declare function hookMatcherSelects(matcher: string | undefined, name: string): boolean | "invalid";
|
|
2274
|
+
/** The form of a hooks matcher under {@link hookMatcherSelects}' reading (closed set). */
|
|
2275
|
+
export type HookMatcherForm = "literal" | "regex" | "invalid";
|
|
2276
|
+
/**
|
|
2277
|
+
* Which form `matcher` takes under the one reading —— the same parse step as {@link hookMatcherSelects}, so for every
|
|
2278
|
+
* matcher `hookMatcherForm(m) === "invalid"` ⇔ `hookMatcherSelects(m, name) === "invalid"` (for any `name`):
|
|
2279
|
+
* · `"literal"` —— no regex is compiled or run: absent / `""` / `"*"` (selects everything) and the NAME-LIST form (only name
|
|
2280
|
+
* characters, `|`, `,` and spaces — membership after splitting);
|
|
2281
|
+
* · `"regex"` —— anything else that compiles as a JS regex (tested unanchored);
|
|
2282
|
+
* · `"invalid"` —— a regex that does not compile (the caller does not run the hook).
|
|
2283
|
+
* For a caller that must route matchers before evaluating them (run the literal ones inline, give the regex ones a deadline).
|
|
2284
|
+
*/
|
|
2285
|
+
export declare function hookMatcherForm(matcher: string | undefined): HookMatcherForm;
|
|
2273
2286
|
/**
|
|
2274
2287
|
* The hooks config record — the shape `TaskRequest.settings.hooks` carries on the wire and the shell
|
|
2275
2288
|
* reads from settings JSON: `{ [eventName]: [{ matcher?, hooks: [...] }] }`. Keys limited to the 33
|