@sema-agent/settings-schema 14.4.0 → 14.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +303 -0
- package/README.md +22 -1
- package/conformance/VERSION +1 -1
- package/dist/api/scopes.d.ts +1 -1
- package/dist/api/scopes.js +1 -1
- package/dist/atomic-write.d.ts +11 -0
- package/dist/atomic-write.js +38 -5
- package/dist/bundle.d.ts +1 -1
- package/dist/bundle.js +1 -1
- package/dist/config-d-file.d.ts +4 -0
- package/dist/config-d-file.js +11 -4
- package/dist/config-dir-io.js +12 -2
- package/dist/config-dir-migration.d.ts +44 -0
- package/dist/config-dir-migration.js +91 -36
- package/dist/config-fns.js +3 -9
- package/dist/conformance/canonical.js +4 -7
- package/dist/conformance/corpus.js +3 -3
- package/dist/conformance/derive.js +6 -5
- package/dist/conformance/run.js +2 -1
- package/dist/definition-copy.d.ts +23 -0
- package/dist/definition-copy.js +52 -0
- package/dist/file-store.d.ts +1 -31
- package/dist/file-store.js +3 -30
- package/dist/fleet.js +2 -1
- package/dist/fs-failure.d.ts +17 -2
- package/dist/fs-failure.js +55 -15
- package/dist/hooks-config.d.ts +2 -2
- package/dist/hooks-config.js +112 -31
- package/dist/http-hook-allowlists.d.ts +7 -3
- package/dist/http-hook-allowlists.js +102 -16
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -1
- package/dist/mcp-request-entry.d.ts +9 -6
- package/dist/mcp-request-entry.js +225 -93
- package/dist/node.d.ts +2 -1
- package/dist/node.js +2 -1
- package/dist/one-time-migrations.js +36 -41
- package/dist/plain-json-object.d.ts +31 -0
- package/dist/plain-json-object.js +52 -0
- package/dist/portable-domains.d.ts +31 -0
- package/dist/portable-domains.js +30 -0
- package/dist/safety-merge-spec.d.ts +88 -6
- package/dist/safety-merge-spec.js +233 -17
- package/dist/scheduler-store.js +3 -2
- package/dist/settings-units.d.ts +85 -0
- package/dist/settings-units.js +175 -0
- package/dist/types.d.ts +4 -0
- package/dist/types.js +61 -53
- package/dist/url-credential.d.ts +41 -0
- package/dist/url-credential.js +95 -0
- package/dist/url-shape.d.ts +11 -5
- package/dist/url-shape.js +11 -5
- package/dist/workers-rows.d.ts +2 -1
- package/dist/workers-rows.js +5 -4
- package/docs/API-SURFACE.md +30 -4
- package/docs/CONSUMER-INTEGRATION.md +2 -0
- package/llms-full.txt +24 -2
- package/llms.txt +6 -2
- package/package.json +2 -3
package/dist/config-d-file.js
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
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 { isPlainJsonObject } from "./plain-json-object.js";
|
|
15
16
|
import { REMOTE_EXEC_DOCUMENT, RemoteExecSpec } from "./remote-exec.js";
|
|
16
17
|
import { RETIRED_DOMAINS, isDomainName, isRetiredDomainName } from "./types.js";
|
|
17
18
|
/** 一份 `config.d` 文档写盘的**文本形** —— 两格缩进的 JSON 加一个换行。写域文件(`writeDomainFile`)、写 `remote-exec.json`(`writeRemoteExec`)与目录迁移写回
|
|
@@ -57,11 +58,17 @@ export function judgeConfigDText(name, text, file) {
|
|
|
57
58
|
catch {
|
|
58
59
|
throw new Error(`invalid JSON in ${file} — ${documentWayOut(name)}`); // sanitized: never echo the raw file text into the error
|
|
59
60
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
return judgeConfigDValue(name, parsed, file);
|
|
62
|
+
}
|
|
63
|
+
/** 一份**已解析的**存量文档在场时的值(同步口 `migrateConfigDocuments` 收的是各层的文档原文,不是文件文本)→ 它自己:一个 JSON 对象 ⇒ 它;`null` / 数组 / 数 / 串 / 布尔 /
|
|
64
|
+
* 非朴素对象 ⇒ 抛。{@link judgeConfigDText} 的最后一道判就是这只(文本解析出来之后过它),所以「不是对象」一种坏形一句话,不论文档是从文件文本来的还是调用方直接给的;
|
|
65
|
+
* `label` = 句首的称呼(文本路径给文件的真实路径,值路径给 `the <名> document`)。 */
|
|
66
|
+
export function judgeConfigDValue(name, value, label) {
|
|
67
|
+
if (!isPlainJsonObject(value)) {
|
|
68
|
+
const kind = value === null ? "null" : Array.isArray(value) ? "array" : typeof value === "object" ? "non-plain object" : typeof value;
|
|
69
|
+
throw new Error(`${label} must be a JSON object (got ${kind}) — ${documentWayOut(name)}`);
|
|
63
70
|
}
|
|
64
|
-
return
|
|
71
|
+
return value;
|
|
65
72
|
}
|
|
66
73
|
/** 读一个 `config.d/<名>.json`:不存在(ENOENT)⇒ `undefined`;在场 ⇒ {@link judgeConfigDText}。 */
|
|
67
74
|
export async function readConfigDFile(root, name) {
|
package/dist/config-dir-io.js
CHANGED
|
@@ -5,10 +5,20 @@
|
|
|
5
5
|
* 命令行入口与引擎起服的目录迁移注入同一份,写法与文件模式因此只在原子写器那一处定。
|
|
6
6
|
*/
|
|
7
7
|
import { readdir, readFile, unlink } from "node:fs/promises";
|
|
8
|
-
import {
|
|
8
|
+
import { dirname } from "node:path";
|
|
9
|
+
import { directoryAdmitsWrite, resolveSymlinkTarget, writeFileAtomic } from "./atomic-write.js";
|
|
10
|
+
import { FsFailure } from "./fs-failure.js";
|
|
9
11
|
export const nodeConfigDirectoryIo = {
|
|
10
12
|
readDir: async (path) => readdir(resolveSymlinkTarget(path)),
|
|
11
13
|
readFile: async (path) => readFile(resolveSymlinkTarget(path), "utf8"),
|
|
12
14
|
writeFileAtomic: async (path, text) => writeFileAtomic(path, text, "config-document"), // the files this migration writes are config.d domain documents
|
|
13
|
-
|
|
15
|
+
// a failed removal is minted here, where the directory's mode can be read (`./fs-failure` is pure): the same `FsFailure` the atomic writer throws, so the command line prints it as is
|
|
16
|
+
removeFile: async (file) => {
|
|
17
|
+
try {
|
|
18
|
+
await unlink(file);
|
|
19
|
+
}
|
|
20
|
+
catch (e) {
|
|
21
|
+
throw new FsFailure({ landing: "remove", path: file, directoryWritable: directoryAdmitsWrite(dirname(file)) }, e);
|
|
22
|
+
}
|
|
23
|
+
},
|
|
14
24
|
};
|
|
@@ -49,6 +49,45 @@ export interface MigrateConfigDirectoryOptions {
|
|
|
49
49
|
/** 只判、不写:有改动且整域判得过 ⇒ `pending`。默认 `false`。 */
|
|
50
50
|
readonly check?: boolean;
|
|
51
51
|
}
|
|
52
|
+
export interface MigrateConfigDocumentsOptions {
|
|
53
|
+
/** 覆盖面:调用方读的文档名集 —— 与 {@link MigrateConfigDirectoryOptions.documents} 同一个词、同一个律(覆盖面内没给的文档 = 从未设过;覆盖面外的键不读、不判、不进结果;拼错的名字 ⇒ 抛 `TypeError`)。 */
|
|
54
|
+
readonly documents: readonly ConfigDirectoryDocument[];
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* 同步口的结果 —— {@link ConfigDirectoryMigration} 的无 io 形,**同一个闭集的词**,调用方穷举:
|
|
58
|
+
* · `unreadable` —— 覆盖面内某份文档不是 JSON 对象(`null` / 数组 / 数 / 串 / 布尔):`document` 点名是哪一份,`reason` 与坏文件同一句式。不抛、不迁(其余文档是别的调用的事:一层一次调用)。
|
|
59
|
+
* · `no-documents`—— 覆盖面内一份文档都没给(缺席与 `undefined` 同)。
|
|
60
|
+
* · `refused` —— 迁移步拒(收据 `refused`)**或**改过的文档整域读不进(整域终判)。`docs` 仍交回迁移后的那一份(迁得动的坐标改新形、迁不动的原样 ——
|
|
61
|
+
* 写手要的是这一份;落不落盘、引擎认不认是 `kind` 的事:`refused` ⇒ 引擎同样不写回、整域落缺省;反向不成立 —— 引擎对整个目录一判,别的文档被拒也会让整目录不写)。
|
|
62
|
+
* · `noop` —— 零改动:`docs` 里每份文档都是入参里的**同一引用**;没改的文档不终判(读不读得进是读者的事,与目录形同一律)。
|
|
63
|
+
* · `migrated` —— 有改动、整域判得过:`docs` 是迁移后的覆盖面内文档全集,`changed` = 改过 / 新建的文档名,`removed` = 被移除的(retired 域),`applied` = 逐条收据。
|
|
64
|
+
* `read` = 覆盖面内在场的文档名(升序)。入参从不被改;重跑 `docs` ⇒ `noop`。
|
|
65
|
+
*/
|
|
66
|
+
export type ConfigDocumentsMigration = {
|
|
67
|
+
readonly kind: "unreadable";
|
|
68
|
+
readonly document: ConfigDirectoryDocument;
|
|
69
|
+
readonly reason: string;
|
|
70
|
+
} | {
|
|
71
|
+
readonly kind: "no-documents";
|
|
72
|
+
} | {
|
|
73
|
+
readonly kind: "refused";
|
|
74
|
+
readonly read: readonly string[];
|
|
75
|
+
readonly docs: Readonly<Record<string, unknown>>;
|
|
76
|
+
readonly applied: readonly MigrationReceipt[];
|
|
77
|
+
readonly refused: readonly ConfigDirectoryRefusal[];
|
|
78
|
+
readonly why: string;
|
|
79
|
+
} | {
|
|
80
|
+
readonly kind: "noop";
|
|
81
|
+
readonly read: readonly string[];
|
|
82
|
+
readonly docs: Readonly<Record<string, unknown>>;
|
|
83
|
+
} | {
|
|
84
|
+
readonly kind: "migrated";
|
|
85
|
+
readonly read: readonly string[];
|
|
86
|
+
readonly docs: Readonly<Record<string, unknown>>;
|
|
87
|
+
readonly applied: readonly MigrationReceipt[];
|
|
88
|
+
readonly changed: readonly string[];
|
|
89
|
+
readonly removed: readonly string[];
|
|
90
|
+
};
|
|
52
91
|
/** 目录迁移能读写的文档名:配置域、retired 域、`remote-exec`(`config.d/remote-exec.json`)—— 就是 `config.d` 里能放的文档名。 */
|
|
53
92
|
export type ConfigDirectoryDocument = ConfigDDocumentName;
|
|
54
93
|
/** 目录加文件名(目录尾部的分隔符折掉)。包内私有(不进 `./node`)。 */
|
|
@@ -59,6 +98,11 @@ type RefusedReceipt = Extract<MigrationReceipt, {
|
|
|
59
98
|
/** 一张收据的出门形(坐标只点名、不带值):`refused` 带 `reason`(= {@link ConfigDirectoryRefusal} 的形),`moved` 带 `to`。包内私有 —— 命令行报告用它渲染 `applied`。 */
|
|
60
99
|
export declare function reportReceipt(r: RefusedReceipt): ConfigDirectoryRefusal;
|
|
61
100
|
export declare function reportReceipt(r: MigrationReceipt): Record<string, unknown>;
|
|
101
|
+
/**
|
|
102
|
+
* 对一组**已有的**存量文档(`docs`:文档名 → 文档,例:一层 `settings` 里的 `models` 域原文)跑一次一次性迁移 + 整域终判 —— 同步、无 io;语义与结果见文件头「同步口」与 {@link ConfigDocumentsMigration}。
|
|
103
|
+
* 覆盖面外的键不读、不判、不进结果;覆盖面内在场(非 `undefined`)的文档必须是 JSON 对象,否则回 `unreadable`(不抛)。`docs` 不是对象 / 覆盖面拼错 ⇒ `TypeError`(调用方的错,不是数据的错)。
|
|
104
|
+
*/
|
|
105
|
+
export declare function migrateConfigDocuments(docs: Readonly<Partial<Record<ConfigDirectoryDocument, unknown>>>, opts: MigrateConfigDocumentsOptions): ConfigDocumentsMigration;
|
|
62
106
|
/**
|
|
63
107
|
* 对 `dir`(一个 `config.d` 目录)的覆盖面内文档跑一次一次性迁移;语义与结果见文件头。`dir` 与目录内文件名的拼接用 `/`。
|
|
64
108
|
*/
|
|
@@ -20,11 +20,18 @@
|
|
|
20
20
|
* · `pending` —— `check: true` 且有改动、整域判得过:一字不写。
|
|
21
21
|
* · `applied` —— 已写回:先写改过 / 新建的文档,最后删被移除的 retired 域文件 —— 中途断掉再跑一次,迁移照样恒等收尾(已搬过去的值在新位上,旧文件里同名键按「新位赢」丢弃)。
|
|
22
22
|
* 写盘阶段的 I/O 错误**原样抛出**(不吞、不改写):已写完的文档留在新位上,再跑一次幂等收尾。
|
|
23
|
+
*
|
|
24
|
+
* ## 同步口 {@link migrateConfigDocuments}(14.5.0)
|
|
25
|
+
* 「迁 → 整域终判」那一段就是一只同步函数:入参是调用方手里**已有**的存量文档原文(域名 → 文档),出参是同一个闭集的无 io 形 —— 迁移后的文档、逐条收据、
|
|
26
|
+
* 整域终判的拒。目录形读完目录后调的就是它的内核(语义只此一处);不经目录的调用方(例:命令行壳按层读 `settings` 文档)调它,不再自己把
|
|
27
|
+
* `migrateStoredDocuments`(纯改写)与 `parseDomain`(整域终判)按「迁移步拒 → 无改动 ⇒ noop → 改过的文档整域终判」的序再组装一遍。
|
|
28
|
+
* 覆盖面、`coverage` 的判、「没改的文档不判」「迁移不许越过覆盖面去写」与目录形逐字同;层的合并(层序 / 覆盖 / 托管锁)不在这里 —— 一层一次调用。
|
|
23
29
|
*/
|
|
24
30
|
import { z } from "zod";
|
|
25
31
|
import { formatConfigPath, parseDomain } from "./config-fns.js";
|
|
26
|
-
import { configDDocumentText, judgeConfigDText, parseRemoteExecDocument } from "./config-d-file.js";
|
|
32
|
+
import { configDDocumentText, judgeConfigDText, judgeConfigDValue, parseRemoteExecDocument } from "./config-d-file.js";
|
|
27
33
|
import { fsFailureMessage } from "./fs-failure.js";
|
|
34
|
+
import { isPlainJsonObject } from "./plain-json-object.js";
|
|
28
35
|
import { migrateStoredDocuments } from "./one-time-migrations.js";
|
|
29
36
|
import { REMOTE_EXEC_DOCUMENT } from "./remote-exec.js";
|
|
30
37
|
import { RUNTIME_LIFT_TARGETS } from "./runtime-lift-targets.js";
|
|
@@ -94,16 +101,78 @@ function verdictOf(input, migrated, receipts) {
|
|
|
94
101
|
return { kind: "changes" };
|
|
95
102
|
}
|
|
96
103
|
const messageOf = (err) => (err instanceof Error ? err.message : String(err));
|
|
97
|
-
/**
|
|
98
|
-
|
|
99
|
-
*/
|
|
100
|
-
export async function migrateConfigDirectory(dir, opts) {
|
|
104
|
+
/** 覆盖面的名字集(校验在这一处):拼错的名字 ⇒ `TypeError`,不让它悄悄变成「什么都没覆盖」。 */
|
|
105
|
+
function coveredDocuments(caller, documents) {
|
|
101
106
|
const covered = new Set();
|
|
102
|
-
for (const name of
|
|
107
|
+
for (const name of documents) {
|
|
103
108
|
if (!isConfigDirectoryDocument(name))
|
|
104
|
-
throw new TypeError(
|
|
109
|
+
throw new TypeError(`${caller}: "${String(name)}" is not a config domain, a retired domain or remote-exec — the coverage names the documents the caller reads`);
|
|
105
110
|
covered.add(name);
|
|
106
111
|
}
|
|
112
|
+
return covered;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* 「迁 → 整域终判」的内核(语义只此一处):`docs` = 覆盖面内**在场且已判过形**的文档(都是 JSON 对象),`covered` = 覆盖面。目录形读完目录后调它,同步口 {@link migrateConfigDocuments} 判完形后调它。
|
|
116
|
+
*/
|
|
117
|
+
function migrateCovered(caller, docs, covered) {
|
|
118
|
+
const read = Object.keys(docs);
|
|
119
|
+
if (read.length === 0)
|
|
120
|
+
return { kind: "no-documents" };
|
|
121
|
+
// 目录/文档集是不是「一个 scope 的全集」只看一件事:SS-M1 要写的目标域是不是都在覆盖面里(见文件头);不是 ⇒ partial,runtime 的迁移被它自己拒掉。
|
|
122
|
+
const coverage = RUNTIME_LIFT_TARGETS.every((target) => covered.has(target)) ? "scope" : "partial";
|
|
123
|
+
const m = migrateStoredDocuments(docs, { coverage });
|
|
124
|
+
const outside = Object.keys(m.docs).filter((name) => !Object.prototype.hasOwnProperty.call(docs, name) && !covered.has(name));
|
|
125
|
+
if (outside.length > 0) {
|
|
126
|
+
// 守卫:迁移新建了覆盖面外的文档。那份文档调用方没读过,「从未设过」只对覆盖面内成立 —— 写下去会覆盖它。今天不会发生(见文件头);发生了就是迁移步的写入域变了,这里先红。
|
|
127
|
+
throw new Error(`${caller}: the migration produced document(s) outside the coverage (${outside.join(", ")}) — refusing to write files the caller did not read`);
|
|
128
|
+
}
|
|
129
|
+
const applied = m.receipts.filter((r) => r.appliedAs !== "refused");
|
|
130
|
+
const verdict = verdictOf(docs, m.docs, m.receipts);
|
|
131
|
+
switch (verdict.kind) {
|
|
132
|
+
case "refused":
|
|
133
|
+
return { kind: "refused", read, docs: m.docs, applied, refused: verdict.refused, why: verdict.why };
|
|
134
|
+
case "noop":
|
|
135
|
+
return { kind: "noop", read, docs: m.docs };
|
|
136
|
+
case "changes":
|
|
137
|
+
break;
|
|
138
|
+
default: {
|
|
139
|
+
const unhandled = verdict;
|
|
140
|
+
throw new Error(`settings-schema: unhandled verdict ${JSON.stringify(unhandled)}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
const changed = Object.entries(m.docs)
|
|
144
|
+
.filter(([name, doc]) => !(Object.prototype.hasOwnProperty.call(docs, name) && docs[name] === doc))
|
|
145
|
+
.map(([name]) => name);
|
|
146
|
+
const removed = Object.keys(docs).filter((name) => !Object.prototype.hasOwnProperty.call(m.docs, name));
|
|
147
|
+
return { kind: "migrated", read, docs: m.docs, applied, changed, removed };
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* 对一组**已有的**存量文档(`docs`:文档名 → 文档,例:一层 `settings` 里的 `models` 域原文)跑一次一次性迁移 + 整域终判 —— 同步、无 io;语义与结果见文件头「同步口」与 {@link ConfigDocumentsMigration}。
|
|
151
|
+
* 覆盖面外的键不读、不判、不进结果;覆盖面内在场(非 `undefined`)的文档必须是 JSON 对象,否则回 `unreadable`(不抛)。`docs` 不是对象 / 覆盖面拼错 ⇒ `TypeError`(调用方的错,不是数据的错)。
|
|
152
|
+
*/
|
|
153
|
+
export function migrateConfigDocuments(docs, opts) {
|
|
154
|
+
if (!isPlainJsonObject(docs))
|
|
155
|
+
throw new TypeError("migrateConfigDocuments: `docs` must be a { document: value } object");
|
|
156
|
+
const covered = coveredDocuments("migrateConfigDocuments", opts.documents);
|
|
157
|
+
const present = {};
|
|
158
|
+
for (const name of [...covered].sort()) {
|
|
159
|
+
const value = Object.prototype.hasOwnProperty.call(docs, name) ? docs[name] : undefined;
|
|
160
|
+
if (value === undefined)
|
|
161
|
+
continue;
|
|
162
|
+
try {
|
|
163
|
+
present[name] = judgeConfigDValue(name, value, `the ${name} document`); // the one config.d "is it an object" judge (the file-text path ends in the same one)
|
|
164
|
+
}
|
|
165
|
+
catch (err) {
|
|
166
|
+
return { kind: "unreadable", document: name, reason: messageOf(err) };
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return migrateCovered("migrateConfigDocuments", present, covered);
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* 对 `dir`(一个 `config.d` 目录)的覆盖面内文档跑一次一次性迁移;语义与结果见文件头。`dir` 与目录内文件名的拼接用 `/`。
|
|
173
|
+
*/
|
|
174
|
+
export async function migrateConfigDirectory(dir, opts) {
|
|
175
|
+
const covered = coveredDocuments("migrateConfigDirectory", opts.documents);
|
|
107
176
|
const { io } = opts;
|
|
108
177
|
let names;
|
|
109
178
|
try {
|
|
@@ -117,7 +186,7 @@ export async function migrateConfigDirectory(dir, opts) {
|
|
|
117
186
|
if (name.startsWith(".") || !name.endsWith(".json"))
|
|
118
187
|
continue;
|
|
119
188
|
const base = name.slice(0, -".json".length);
|
|
120
|
-
if (!
|
|
189
|
+
if (!isConfigDirectoryDocument(base) || !covered.has(base))
|
|
121
190
|
continue;
|
|
122
191
|
let text;
|
|
123
192
|
const file = joinDirPath(dir, name);
|
|
@@ -134,46 +203,32 @@ export async function migrateConfigDirectory(dir, opts) {
|
|
|
134
203
|
return { kind: "unreadable", reason: messageOf(err) };
|
|
135
204
|
}
|
|
136
205
|
}
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
const coverage = RUNTIME_LIFT_TARGETS.every((target) => covered.has(target)) ? "scope" : "partial";
|
|
142
|
-
const m = migrateStoredDocuments(docs, { coverage });
|
|
143
|
-
const outside = Object.keys(m.docs).filter((name) => !Object.prototype.hasOwnProperty.call(docs, name) && !covered.has(name));
|
|
144
|
-
if (outside.length > 0) {
|
|
145
|
-
// 守卫:迁移新建了覆盖面外的文档。那份文件调用方没读过,「从未设过」只对覆盖面内成立 —— 写下去会覆盖它。今天不会发生(见文件头);发生了就是迁移步的写入域变了,这里先红。
|
|
146
|
-
throw new Error(`migrateConfigDirectory: the migration produced document(s) outside the coverage (${outside.join(", ")}) — refusing to write files the caller did not read`);
|
|
147
|
-
}
|
|
148
|
-
const applied = m.receipts.filter((r) => r.appliedAs !== "refused");
|
|
149
|
-
const verdict = verdictOf(docs, m.docs, m.receipts);
|
|
150
|
-
switch (verdict.kind) {
|
|
206
|
+
const m = migrateCovered("migrateConfigDirectory", docs, covered);
|
|
207
|
+
switch (m.kind) {
|
|
208
|
+
case "no-documents":
|
|
209
|
+
return { kind: "no-documents", entries: names };
|
|
151
210
|
case "refused":
|
|
152
|
-
return { kind: "refused", read, applied, refused:
|
|
211
|
+
return { kind: "refused", read: m.read, applied: m.applied, refused: m.refused, why: m.why };
|
|
153
212
|
case "noop":
|
|
154
|
-
return { kind: "noop", read };
|
|
155
|
-
case "
|
|
213
|
+
return { kind: "noop", read: m.read };
|
|
214
|
+
case "migrated":
|
|
156
215
|
break;
|
|
157
216
|
default: {
|
|
158
|
-
const unhandled =
|
|
159
|
-
throw new Error(`settings-schema: unhandled
|
|
217
|
+
const unhandled = m;
|
|
218
|
+
throw new Error(`settings-schema: unhandled migration ${JSON.stringify(unhandled)}`);
|
|
160
219
|
}
|
|
161
220
|
}
|
|
162
221
|
if (opts.check === true)
|
|
163
|
-
return { kind: "pending", read, applied };
|
|
222
|
+
return { kind: "pending", read: m.read, applied: m.applied };
|
|
164
223
|
const written = [];
|
|
165
224
|
const removed = [];
|
|
166
|
-
for (const
|
|
167
|
-
|
|
168
|
-
continue;
|
|
169
|
-
await io.writeFileAtomic(joinDirPath(dir, `${name}.json`), configDDocumentText(doc)); // the one config.d document text (writeDomainFile / writeRemoteExec write the same)
|
|
225
|
+
for (const name of m.changed) {
|
|
226
|
+
await io.writeFileAtomic(joinDirPath(dir, `${name}.json`), configDDocumentText(m.docs[name])); // the one config.d document text (writeDomainFile / writeRemoteExec write the same)
|
|
170
227
|
written.push(`${name}.json`);
|
|
171
228
|
}
|
|
172
|
-
for (const name of
|
|
173
|
-
if (Object.prototype.hasOwnProperty.call(m.docs, name))
|
|
174
|
-
continue;
|
|
229
|
+
for (const name of m.removed) {
|
|
175
230
|
await io.removeFile(joinDirPath(dir, `${name}.json`));
|
|
176
231
|
removed.push(`${name}.json`);
|
|
177
232
|
}
|
|
178
|
-
return { kind: "applied", read, applied, written, removed };
|
|
233
|
+
return { kind: "applied", read: m.read, applied: m.applied, written, removed };
|
|
179
234
|
}
|
package/dist/config-fns.js
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
// its entry collections. zod is this package's one runtime dependency, so this costs nothing new.
|
|
18
18
|
import { z } from "zod";
|
|
19
19
|
import { MODEL_ENTRY_REFUSAL_CODES, ModelRefusalsWire } from "./fleet.js";
|
|
20
|
+
import { isPlainJsonObject } from "./plain-json-object.js";
|
|
20
21
|
import { carriesUserinfo, secretScanForms } from "./secret-scan.js"; // package-private normalization (both scanners read it) + the userinfo judgement (not in the barrel)
|
|
21
22
|
import { sha256Hex } from "./sha256.js";
|
|
22
23
|
import { unwrapSchema } from "./zod-unwrap.js"; // package-private: the ONE reading of zod's wrapper layers (salvage + the one-time migrations share it)
|
|
@@ -347,7 +348,7 @@ function readEntryIdentity(entry) {
|
|
|
347
348
|
return undefined;
|
|
348
349
|
const own = (k) => {
|
|
349
350
|
const d = Object.getOwnPropertyDescriptor(entry, k);
|
|
350
|
-
return d !== undefined && "value"
|
|
351
|
+
return d !== undefined && Object.hasOwn(d, "value") ? d.value : undefined; // 描述符本身也只读自有键
|
|
351
352
|
};
|
|
352
353
|
const name = own("name");
|
|
353
354
|
const id = own("id");
|
|
@@ -731,13 +732,6 @@ export function isReservedExtensionKey(domain, parent, key) {
|
|
|
731
732
|
const rules = EXTENSION_NAMESPACES[domain];
|
|
732
733
|
return rules !== undefined && rules.some((r) => r.at.length === parent.length && r.at.every((seg, i) => (seg === ANY_INDEX ? typeof parent[i] === "number" : seg === parent[i])) && r.key.test(key));
|
|
733
734
|
}
|
|
734
|
-
/** 纯 JSON 对象(不是数组、不是类实例/带原型的东西)——只对这种做键集对比,别的形状交给 parse 判。 */
|
|
735
|
-
function isPlainJsonObject(v) {
|
|
736
|
-
if (v === null || typeof v !== "object" || Array.isArray(v))
|
|
737
|
-
return false;
|
|
738
|
-
const proto = Object.getPrototypeOf(v);
|
|
739
|
-
return proto === Object.prototype || proto === null;
|
|
740
|
-
}
|
|
741
735
|
function collectDropped(domain, raw, parsed, prefix, depth, acc) {
|
|
742
736
|
if (depth > DROP_SCAN_MAX_DEPTH)
|
|
743
737
|
return;
|
|
@@ -1104,7 +1098,7 @@ export function buildEffective(version, getRaw, updatedAt, onWarning) {
|
|
|
1104
1098
|
* and so does any key no domain names(the 0.6-era `teams` key included — not a domain since 0.7, and no engine reads it). Throws ONLY on garbage no caller can partially trust: a non-object payload, or a missing/
|
|
1105
1099
|
* non-finite `version` — that is not "a bad domain", it is not an effective config at all. */
|
|
1106
1100
|
export function readEffectiveWire(raw, onWarning) {
|
|
1107
|
-
if (
|
|
1101
|
+
if (!isPlainJsonObject(raw))
|
|
1108
1102
|
throw new Error("effective wire payload is not a JSON object");
|
|
1109
1103
|
const r = raw;
|
|
1110
1104
|
if (typeof r.version !== "number" || !Number.isFinite(r.version))
|
|
@@ -10,10 +10,7 @@
|
|
|
10
10
|
* `stableKey` 是唯一一处把值转成串的地方,它只用来给**多重集计数**当哈希键 —— 用的是 JSON 编码,
|
|
11
11
|
* 数组边界与段类型(`"1"` vs `1`)在编码里都还在,所以它不是那种有损的点号串。
|
|
12
12
|
*/
|
|
13
|
-
|
|
14
|
-
function isPlainObject(v) {
|
|
15
|
-
return v !== null && typeof v === "object" && !Array.isArray(v);
|
|
16
|
-
}
|
|
13
|
+
import { isPlainJsonObject } from "../plain-json-object.js";
|
|
17
14
|
/**
|
|
18
15
|
* 「这个对象**在场**的键」—— 本文件里唯一一处判键在不在 / 取键的值的地方。
|
|
19
16
|
*
|
|
@@ -42,7 +39,7 @@ function presentKeys(o) {
|
|
|
42
39
|
export function canonicalize(value) {
|
|
43
40
|
if (Array.isArray(value))
|
|
44
41
|
return value.map(canonicalize);
|
|
45
|
-
if (
|
|
42
|
+
if (isPlainJsonObject(value)) {
|
|
46
43
|
// `Object.fromEntries`,**不是**逐键 `out[k] = …`:后者对 `"__proto__"` 走的是原型 setter(改掉对象的
|
|
47
44
|
// 原型,既不产生自有键、还污染返回值),于是规范化会把一个叫 `__proto__` 的顶层键**吃掉** —— 两边
|
|
48
45
|
// 同样被吃掉 = 比对器对它全盲 = 一格假绿。与 S-332 在 wire 透传处修的是同一条「键就是键」的律。
|
|
@@ -60,7 +57,7 @@ export function deepEqual(a, b) {
|
|
|
60
57
|
return true;
|
|
61
58
|
if (Array.isArray(a) && Array.isArray(b))
|
|
62
59
|
return a.length === b.length && a.every((x, i) => deepEqual(x, b[i]));
|
|
63
|
-
if (
|
|
60
|
+
if (isPlainJsonObject(a) && isPlainJsonObject(b)) {
|
|
64
61
|
const ka = presentKeys(a);
|
|
65
62
|
const kb = presentKeys(b);
|
|
66
63
|
return ka.length === kb.length && ka.every((k, i) => k === kb[i]) && ka.every((k) => deepEqual(ownValue(a, k), ownValue(b, k)));
|
|
@@ -93,7 +90,7 @@ export function diffJson(expected, actual, at = []) {
|
|
|
93
90
|
}
|
|
94
91
|
return out;
|
|
95
92
|
}
|
|
96
|
-
if (
|
|
93
|
+
if (isPlainJsonObject(expected) && isPlainJsonObject(actual)) {
|
|
97
94
|
const out = [];
|
|
98
95
|
const keys = [...new Set([...presentKeys(expected), ...presentKeys(actual)])].sort();
|
|
99
96
|
for (const k of keys) {
|
|
@@ -20,14 +20,14 @@ import { dirname, join } from "node:path";
|
|
|
20
20
|
import { fileURLToPath } from "node:url";
|
|
21
21
|
import { LEG_NAMES, REJECT_STAGES } from "./types.js";
|
|
22
22
|
import { LEG_EXPECTATION } from "./derive.js";
|
|
23
|
+
import { isPlainJsonObject } from "../plain-json-object.js";
|
|
23
24
|
/** 本包的根目录 —— `src/conformance/*.ts` 与 `dist/conformance/*.js` 距根同深,两处求值同一个地方。 */
|
|
24
25
|
export const PACKAGE_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
25
26
|
/** 语料目录(默认 = 本包的 `conformance/`)。 */
|
|
26
27
|
export const DEFAULT_CORPUS_DIR = join(PACKAGE_ROOT, "conformance");
|
|
27
28
|
const readJson = async (p) => JSON.parse(await readFile(p, "utf8"));
|
|
28
|
-
const isRecord = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
29
29
|
function requireRecord(v, where) {
|
|
30
|
-
if (!
|
|
30
|
+
if (!isPlainJsonObject(v))
|
|
31
31
|
throw new Error(`conformance corpus: ${where} must be a JSON object`);
|
|
32
32
|
return v;
|
|
33
33
|
}
|
|
@@ -56,7 +56,7 @@ function checkLegExpectation(v, where) {
|
|
|
56
56
|
if (!REJECT_STAGES.includes(r.stage))
|
|
57
57
|
throw new Error(`conformance corpus: ${where}.stage must be one of ${REJECT_STAGES.join("|")}`);
|
|
58
58
|
if (r.refIssues !== undefined) {
|
|
59
|
-
const ok = Array.isArray(r.refIssues) && r.refIssues.every((x) =>
|
|
59
|
+
const ok = Array.isArray(r.refIssues) && r.refIssues.every((x) => isPlainJsonObject(x) && Array.isArray(x.path) && x.path.every((seg) => typeof seg === "string" || typeof seg === "number"));
|
|
60
60
|
if (!ok)
|
|
61
61
|
throw new Error(`conformance corpus: ${where}.refIssues must be an array of { path: (string|number)[] }`);
|
|
62
62
|
}
|
|
@@ -34,7 +34,9 @@
|
|
|
34
34
|
* 严写的结局不是读结局的一种表示变换,而是另一个判断(读腿抢救的地方严写拒)。把它写成「从 domains 期望派生」
|
|
35
35
|
* 就等于让 domains 金样的一处错同时改写两条腿的期望 —— 两条腿同错、门照绿。它的金样逐条显式入库、各自冻结。
|
|
36
36
|
*/
|
|
37
|
-
import { NON_DOMAIN_CONFIG_FILES
|
|
37
|
+
import { NON_DOMAIN_CONFIG_FILES } from "../file-store.js";
|
|
38
|
+
import { PORTABLE_DOMAINS } from "../portable-domains.js";
|
|
39
|
+
import { isPlainJsonObject } from "../plain-json-object.js";
|
|
38
40
|
import { skillContentHash } from "../skills-manifest.js";
|
|
39
41
|
import { DOMAINS, isDomainName } from "../types.js";
|
|
40
42
|
/** 变换表(本文件头注的机器形;`docs/CONFORMANCE.md` 与 `test/conformance-coverage.test.ts` 都读它)。 */
|
|
@@ -65,18 +67,17 @@ export const LEG_EXPECTATION = {
|
|
|
65
67
|
};
|
|
66
68
|
/** 非便携域 —— 由 {@link PORTABLE_DOMAINS} 派生,零手抄。 */
|
|
67
69
|
export const NON_PORTABLE_DOMAINS = DOMAINS.filter((d) => !PORTABLE_DOMAINS.includes(d));
|
|
68
|
-
const isPlainObject = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
69
70
|
/** 一份 `domains` 文档集里,**不是域名**的那些顶层键(T4 的触发面)。 */
|
|
70
71
|
export const foreignKeysOf = (docs) => Object.keys(docs).filter((k) => !isDomainName(k));
|
|
71
72
|
/** T2 的**入参**半场:域形的 skills 文档 → wire 形的 manifest 文档。结构变换,不解析 —— 坏形原样传下去,
|
|
72
73
|
* 让 wire 腿自己的判据去裁(否则语料就没法喂一份坏 manifest 了)。 */
|
|
73
74
|
export function toManifestDoc(doc) {
|
|
74
|
-
if (!
|
|
75
|
+
if (!isPlainJsonObject(doc) || !Array.isArray(doc.skills))
|
|
75
76
|
return doc;
|
|
76
77
|
return {
|
|
77
78
|
...doc,
|
|
78
79
|
skills: doc.skills.map((entry) => {
|
|
79
|
-
if (!
|
|
80
|
+
if (!isPlainJsonObject(entry) || typeof entry.content !== "string")
|
|
80
81
|
return entry;
|
|
81
82
|
const { content, ...rest } = entry;
|
|
82
83
|
return { ...rest, contentHash: skillContentHash(content) };
|
|
@@ -131,7 +132,7 @@ export const EPOCH_ISO = new Date(0).toISOString();
|
|
|
131
132
|
* `conformance/baseline.json`),非便携域取它的那一格 —— 不现算,免得 schema 默认一改 golden 就跟着动。 */
|
|
132
133
|
export function deriveLocalExpectation(source, docs, baseline, localVersion) {
|
|
133
134
|
// T5:文件层先判 —— 任何一个**便携**域的文档不是 JSON 对象 ⇒ 整条腿在解析层被拒。
|
|
134
|
-
const badFile = PORTABLE_DOMAINS.find((d) => Object.prototype.hasOwnProperty.call(docs, d) && !
|
|
135
|
+
const badFile = PORTABLE_DOMAINS.find((d) => Object.prototype.hasOwnProperty.call(docs, d) && !isPlainJsonObject(docs[d]));
|
|
135
136
|
if (badFile !== undefined)
|
|
136
137
|
return { outcome: "rejected", stage: "parse" };
|
|
137
138
|
const effective = clone(source.effective);
|
package/dist/conformance/run.js
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import { mkdtemp, mkdir, readFile, realpath, rm, stat, writeFile, utimes } from "node:fs/promises";
|
|
15
15
|
import { tmpdir } from "node:os";
|
|
16
16
|
import { basename, extname, isAbsolute, join, resolve, sep } from "node:path";
|
|
17
|
+
import { isPlainJsonObject } from "../plain-json-object.js";
|
|
17
18
|
import { canonicalize, deepEqual, diffJson, stableKey } from "./canonical.js";
|
|
18
19
|
import { diffWarningMultiset, findSecretLeaks, projectWarning } from "./warnings.js";
|
|
19
20
|
import { deriveLocalExpectation, deriveLocalInput, deriveWireExpectation, deriveWireInput, toManifestDoc } from "./derive.js";
|
|
@@ -133,7 +134,7 @@ function sameRefIssueBag(expected, actual) {
|
|
|
133
134
|
/** effective 里的元数据两键(独立断言,不进结构比对)。 */
|
|
134
135
|
const METADATA_KEYS = ["version", "updatedAt"];
|
|
135
136
|
function splitMetadata(effective) {
|
|
136
|
-
if (
|
|
137
|
+
if (!isPlainJsonObject(effective))
|
|
137
138
|
return { body: effective };
|
|
138
139
|
const src = effective;
|
|
139
140
|
// fromEntries(不是逐键赋值):`__proto__` 作自有键时逐键赋值会走原型 setter,把它从比对面上抹掉。
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 包内私有(不进主桶、不进 exports 表):**定义语义的复制**。读不可信的串数组、写读出来的副本,都不许走 `[[Set]]` 与继承的成员 ——
|
|
3
|
+
* `Object.prototype` / `Array.prototype` 上被污染的数字键(getter / setter)、`get` / `set` 键、数组实例上被替换的 `Symbol.iterator`,
|
|
4
|
+
* 在下标赋值、`push`、展开、描述符对象字面量上各有一条路能把条目吞掉、替换或抛出去。本模块是这件事的唯一读法,读者只调它:
|
|
5
|
+
* · {@link captureStringArray}:一份串数组的捕获副本(按下标、`hasOwn`、一次读、非串即坏、`defineProperty` 写入);
|
|
6
|
+
* · {@link dataDescriptor}:写副本用的描述符,零原型(`{ value, writable, enumerable, configurable }` 字面量继承 `Object.prototype`,
|
|
7
|
+
* `Object.prototype.get` / `set` 被污染时它会被读成访问器描述符)。
|
|
8
|
+
* 零 import。
|
|
9
|
+
*/
|
|
10
|
+
/** 数据属性描述符(可写 / 可枚举 / 可配置),**零原型**:不继承 `Object.prototype` 上被污染的 `get` / `set` 等键,也不靠对象字面量。 */
|
|
11
|
+
export declare function dataDescriptor(value: unknown): PropertyDescriptor;
|
|
12
|
+
/**
|
|
13
|
+
* 一份**串数组的捕获副本**;不是串数组 ⇒ `undefined`(调用方据此读成坏形 —— 名单读成 `[]` 并点名,`args` 丢键并进 `lost`)。
|
|
14
|
+
* **取值 + 检视 + 复制整段在一个 try 里**,`length` 与每个元素都只读一次(检视与后续使用不会读到两个答案):
|
|
15
|
+
* · 不是数组,或 `length` 不是安全非负整数(读成字符串的 Proxy 等)⇒ 坏形,不按 `length` 物化;
|
|
16
|
+
* · 逐下标先问 `Object.hasOwn` 再读 —— 洞(不是自有下标)⇒ 坏形,不顺原型链读到 `Object.prototype` / `Array.prototype` 上的数字键(数据属性或访问器);
|
|
17
|
+
* · 条目不是串 ⇒ 坏形;遇到第一个坏处就退出(`length` 很大的稀疏数组立即判坏形);
|
|
18
|
+
* · 副本用 `Object.defineProperty` + {@link dataDescriptor} 写(`push` / 下标赋值是 `[[Set]]`,会命中继承的数字键 setter、吞掉条目);
|
|
19
|
+
* · 不走迭代协议(展开 / `for…of` 会让数组实例上被替换的 `Symbol.iterator` 供出别的条目);
|
|
20
|
+
* · 读 `length` / 元素时抛(getter、Proxy 陷阱)⇒ 坏形,不抛出去。
|
|
21
|
+
* 交出去的是**副本**(不是调用方数组的原引用):调用方之后改原数组,副本不变。
|
|
22
|
+
*/
|
|
23
|
+
export declare function captureStringArray(v: unknown): string[] | undefined;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 包内私有(不进主桶、不进 exports 表):**定义语义的复制**。读不可信的串数组、写读出来的副本,都不许走 `[[Set]]` 与继承的成员 ——
|
|
3
|
+
* `Object.prototype` / `Array.prototype` 上被污染的数字键(getter / setter)、`get` / `set` 键、数组实例上被替换的 `Symbol.iterator`,
|
|
4
|
+
* 在下标赋值、`push`、展开、描述符对象字面量上各有一条路能把条目吞掉、替换或抛出去。本模块是这件事的唯一读法,读者只调它:
|
|
5
|
+
* · {@link captureStringArray}:一份串数组的捕获副本(按下标、`hasOwn`、一次读、非串即坏、`defineProperty` 写入);
|
|
6
|
+
* · {@link dataDescriptor}:写副本用的描述符,零原型(`{ value, writable, enumerable, configurable }` 字面量继承 `Object.prototype`,
|
|
7
|
+
* `Object.prototype.get` / `set` 被污染时它会被读成访问器描述符)。
|
|
8
|
+
* 零 import。
|
|
9
|
+
*/
|
|
10
|
+
/** 数据属性描述符(可写 / 可枚举 / 可配置),**零原型**:不继承 `Object.prototype` 上被污染的 `get` / `set` 等键,也不靠对象字面量。 */
|
|
11
|
+
export function dataDescriptor(value) {
|
|
12
|
+
const descriptor = Object.create(null);
|
|
13
|
+
descriptor.value = value;
|
|
14
|
+
descriptor.writable = true;
|
|
15
|
+
descriptor.enumerable = true;
|
|
16
|
+
descriptor.configurable = true;
|
|
17
|
+
return descriptor;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* 一份**串数组的捕获副本**;不是串数组 ⇒ `undefined`(调用方据此读成坏形 —— 名单读成 `[]` 并点名,`args` 丢键并进 `lost`)。
|
|
21
|
+
* **取值 + 检视 + 复制整段在一个 try 里**,`length` 与每个元素都只读一次(检视与后续使用不会读到两个答案):
|
|
22
|
+
* · 不是数组,或 `length` 不是安全非负整数(读成字符串的 Proxy 等)⇒ 坏形,不按 `length` 物化;
|
|
23
|
+
* · 逐下标先问 `Object.hasOwn` 再读 —— 洞(不是自有下标)⇒ 坏形,不顺原型链读到 `Object.prototype` / `Array.prototype` 上的数字键(数据属性或访问器);
|
|
24
|
+
* · 条目不是串 ⇒ 坏形;遇到第一个坏处就退出(`length` 很大的稀疏数组立即判坏形);
|
|
25
|
+
* · 副本用 `Object.defineProperty` + {@link dataDescriptor} 写(`push` / 下标赋值是 `[[Set]]`,会命中继承的数字键 setter、吞掉条目);
|
|
26
|
+
* · 不走迭代协议(展开 / `for…of` 会让数组实例上被替换的 `Symbol.iterator` 供出别的条目);
|
|
27
|
+
* · 读 `length` / 元素时抛(getter、Proxy 陷阱)⇒ 坏形,不抛出去。
|
|
28
|
+
* 交出去的是**副本**(不是调用方数组的原引用):调用方之后改原数组,副本不变。
|
|
29
|
+
*/
|
|
30
|
+
export function captureStringArray(v) {
|
|
31
|
+
try {
|
|
32
|
+
if (!Array.isArray(v))
|
|
33
|
+
return undefined;
|
|
34
|
+
const source = v;
|
|
35
|
+
const n = source.length;
|
|
36
|
+
if (typeof n !== "number" || !Number.isSafeInteger(n) || n < 0)
|
|
37
|
+
return undefined;
|
|
38
|
+
const copy = [];
|
|
39
|
+
for (let i = 0; i < n; i++) {
|
|
40
|
+
if (!Object.hasOwn(source, i))
|
|
41
|
+
return undefined;
|
|
42
|
+
const entry = source[i];
|
|
43
|
+
if (typeof entry !== "string")
|
|
44
|
+
return undefined;
|
|
45
|
+
Object.defineProperty(copy, i, dataDescriptor(entry));
|
|
46
|
+
}
|
|
47
|
+
return copy;
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
}
|
package/dist/file-store.d.ts
CHANGED
|
@@ -1,22 +1,7 @@
|
|
|
1
1
|
import { type EffectiveReadWarning } from "./config-fns.js";
|
|
2
2
|
import type { ConfigReader } from "./reader.js";
|
|
3
3
|
import { type DomainConfig, type DomainName, type EffectiveConfig, type RetiredDomainName } from "./types.js";
|
|
4
|
-
|
|
5
|
-
* **default**域集 of the bundle helpers (`exportBundle`/`readBundle`). 每个被排除的域必须在
|
|
6
|
-
* {@link NON_PORTABLE_DOMAIN_REASONS} 里有一条成文理由(禁裸排除,#282 件5 附带裁定)。
|
|
7
|
-
*
|
|
8
|
-
* ⚠️ 口径诚实(对抗复审 复审 round1 F4):这是**默认集与本地读取集,不是 API 级硬边界** —— `exportBundle`
|
|
9
|
-
* / `readBundle` 的 `domains` 参数可显式传任意 `DomainName`(bundle.ts 不拒非便携域)。把非便携域搬进
|
|
10
|
-
* bundle 的那条口子归调用方纪律(以及各自域的导出面),不是本常量能挡住的;要不要把 bundle API 收窄到
|
|
11
|
-
* `PortableDomain` 是一个独立的行为面裁定(#282 未决项,需三问),本批不擅自改。
|
|
12
|
-
*
|
|
13
|
-
* 🔴 **开新顶层域时必须同步登记本表**(成案 = 消费方 #322 真事故):域表是**闭集**,不在这里的
|
|
14
|
-
* 域名,`config.d/<域>.json` 不会被读进 effective —— 消费方拿到的是「该域缺席」,和文件不存在**逐字
|
|
15
|
-
* 同形**。当年 `limits` 域就是这样在单机腿静默丢了一整片限额发布(远端腿同批可用,所以更难看出来)。
|
|
16
|
-
* 今日的兜底:漏登记的文件会被 {@link FileConfigStore} 的 `"unread-config-file"` 警告点名(fail-loud,
|
|
17
|
-
* 见 getEffective),但那只在消费方接了 `onWarning` 时才响 —— **登记仍是第一现场的义务**。 */
|
|
18
|
-
export declare const PORTABLE_DOMAINS: readonly ["models", "rosters", "skills", "mcp", "plugins", "scenarios", "collab", "governance", "execution", "prompts", "limits"];
|
|
19
|
-
export type PortableDomain = (typeof PORTABLE_DOMAINS)[number];
|
|
4
|
+
export { NON_PORTABLE_DOMAIN_REASONS, PORTABLE_DOMAINS, type PortableDomain } from "./portable-domains.js";
|
|
20
5
|
/** `config.d/` 里**合法存在但不是域文档**的文件名(不进 `"unread-config-file"` 告警)。`remote-exec`
|
|
21
6
|
* 不是 `DOMAIN_SCHEMAS` 成员(resolver 不读它),按 service 表决住在这个目录,由 loadRemoteExec 读
|
|
22
7
|
* ——**它有自己的读取与校验路径**,所以既不是「未知文件」也不是「域文件」。
|
|
@@ -24,21 +9,6 @@ export type PortableDomain = (typeof PORTABLE_DOMAINS)[number];
|
|
|
24
9
|
* **导出**(3.0.0):一致性门的语料派生要按同一张表给本地腿的文件分类(域文件 / 专用文件 / 忽略),
|
|
25
10
|
* 手抄一份必漂 —— 漂了的那一天,门会把一个合法的 `remote-exec.json` 报成未知文件。 */
|
|
26
11
|
export declare const NON_DOMAIN_CONFIG_FILES: ReadonlySet<string>;
|
|
27
|
-
/**
|
|
28
|
-
* 非便携域的**排除理由表**(#282 件5 附带裁定:禁裸排除)。
|
|
29
|
-
*
|
|
30
|
-
* `satisfies Record<Exclude<DomainName, PortableDomain>, string>` 让键集在**编译期**双向咬死:新开一个
|
|
31
|
-
* 域却不放进 PORTABLE_DOMAINS ⇒ 这里缺行 ⇒ 编译错;把某域改成便携却忘了删行 ⇒ 多余键 ⇒ 编译错。
|
|
32
|
-
* (此前是「理由写在注释里」,机器只能做子串匹配——域名出现在文件任何角落都算数,等于没门;对抗复审
|
|
33
|
-
* 复审 round1 F3。)理由要写**为什么它不该被单机/跨部署搬运**,带出处。
|
|
34
|
-
*/
|
|
35
|
-
export declare const NON_PORTABLE_DOMAIN_REASONS: {
|
|
36
|
-
readonly workers: "FLEET-only:单机没有 reconciler,`{workers:[]}` 的 schema 默认就是正确语义(缺席不需要 stub)。";
|
|
37
|
-
readonly hosts: "FLEET-only:同 workers —— 单机无机器注册表/自动排布,空注册表即正确语义。";
|
|
38
|
-
readonly systems: "FLEET-only:系统接入/凭据下发的元数据(nonce+路由)属于 web-admin 侧生命周期,单机 lane 无对应执法点。";
|
|
39
|
-
readonly projects: "TENANT IDENTITY DATA,不是可搬运的配置:登记簿随包复制进另一个部署 = 凭空多出的身份登记(design/142 §1.4 威胁模型①「carried-in foreign projectId」换了身 import 的衣服)。要带走走 142 §5 的导出面(service 半场)。";
|
|
40
|
-
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 本就没有那个执法点。";
|
|
41
|
-
};
|
|
42
12
|
/** One unreadable/corrupt domain FILE surfaced by `getEffective({tolerant:true})`(0.10.12,[897]②)。
|
|
43
13
|
* `error` 是 #readDomainRaw 的 sanitized 文案(带文件路径语境、绝不回显文件内容)。 */
|
|
44
14
|
export type DomainReadError = {
|