@sema-agent/settings-schema 11.2.0 → 12.0.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 +112 -0
- package/README.md +2 -1
- package/conformance/VERSION +1 -1
- package/conformance/cases/c40-s712-identity-only-what-reads-out/expected.json +6 -3
- package/conformance/cases/c41-s792-encoded-pem-identity/expected.json +4 -2
- package/conformance/cases/c45-s792-identity-bounds-and-decode-depth/expected.json +4 -2
- package/conformance/cases/c46-s811-host-name-url-userinfo-withheld/expected.json +2 -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/conformance/cases/cli01-model-name-nfc-vs-nfd/expected.json +4 -2
- package/conformance/cases/cli02-control-and-zero-width-chars/expected.json +4 -2
- 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 +23 -6
- package/dist/cli.d.ts +8 -4
- package/dist/cli.js +55 -10
- package/dist/config-d-file.d.ts +12 -2
- package/dist/config-d-file.js +36 -9
- package/dist/config-dir-io.js +4 -3
- package/dist/config-dir-migration.d.ts +5 -3
- package/dist/config-dir-migration.js +19 -9
- package/dist/config-fns.d.ts +14 -2
- package/dist/config-fns.js +13 -1
- 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 +4 -3
- package/dist/migration-receipt.js +1 -1
- package/dist/node.d.ts +1 -1
- package/dist/one-time-migrations.d.ts +6 -4
- package/dist/one-time-migrations.js +73 -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/resolve-roster.d.ts +1 -1
- package/dist/resolve-roster.js +3 -3
- package/dist/retired-role-words.d.ts +5 -0
- package/dist/retired-role-words.js +25 -0
- package/dist/rfc3339.d.ts +10 -0
- package/dist/rfc3339.js +12 -0
- package/dist/safety-merge-spec.d.ts +2 -2
- package/dist/types.d.ts +140 -32
- package/dist/types.js +215 -114
- package/dist/web-admin-credential.d.ts +2 -2
- package/dist/web-admin-credential.js +5 -10
- package/dist/write-failure.d.ts +16 -0
- package/dist/write-failure.js +43 -0
- package/package.json +1 -1
package/dist/config-fns.d.ts
CHANGED
|
@@ -182,8 +182,10 @@ export declare function outgoingPath(p: ConfigPath): ConfigPath;
|
|
|
182
182
|
* 所以是**不带**而不是打码)。三键都读不出 ⇒ 整个身份缺席(不铸空对象)。
|
|
183
183
|
*
|
|
184
184
|
* **扣名 ≠ 读不出**(11.0.0,N-4 / S-759):`name` / `id` 是字符串但不能原样出门 ⇒ 身份带 `nameWithheld: true` / `idWithheld: true`;
|
|
185
|
-
* 不是字符串 ⇒
|
|
186
|
-
*
|
|
185
|
+
* 不是字符串 ⇒ 什么都不带。标记只说「有一个名、没给出」,不说为什么(秘密形 / 超长 / 字符集外是同一个记号)。扣下的**名**另带一个可比记号
|
|
186
|
+
* `token`(12.0.0,{@link withheldNameToken}):两条扣名行据此可分、消费方拿请求里的名字调同一只就能比对,原名一个字节都不出门。
|
|
187
|
+
* 身份只回答「丢的是谁」,不回答「这条坏行算不算在场」:一条读不出名字的被丢行照样是文档里的一行,在场与否是消费方按原文判的事
|
|
188
|
+
* (例:server 把没写 `enabled: false` 的被丢行一律算在场)。
|
|
187
189
|
*
|
|
188
190
|
* 为什么要带:条目被丢之后它的名字从目录里消失,只给坐标的话,消费方要知道「丢的是谁」只能拿坐标回读
|
|
189
191
|
* 原文档 —— 而它手里那份原文档未必是这一次读到的那份。铸告警的这一层判的就是那条原文,在这里取一次、
|
|
@@ -194,6 +196,9 @@ export interface DroppedEntryIdentity {
|
|
|
194
196
|
enabled?: boolean;
|
|
195
197
|
/** `name` was a string that {@link isDisclosableIdentity} would not let out (11.0.0). Absent with a `name` present or with no string name. */
|
|
196
198
|
nameWithheld?: true;
|
|
199
|
+
/** The withheld name's comparable token ({@link withheldNameToken}; 12.0.0) — present exactly when `nameWithheld` is. Stable (the same name
|
|
200
|
+
* always gives the same token), irreversible, never the name or any part of it. */
|
|
201
|
+
token?: string;
|
|
197
202
|
/** `id` was a string that {@link isDisclosableIdentity} would not let out (11.0.0). */
|
|
198
203
|
idWithheld?: true;
|
|
199
204
|
}
|
|
@@ -251,6 +256,13 @@ export declare function isDisclosableIdentity(v: string): boolean;
|
|
|
251
256
|
* ② {@link isDisclosableIdentity}(不是白名单闭形里的标识符,或是秘密形 ⇒ 不出门)。
|
|
252
257
|
* 非字符串 ⇒ false。住在本文件而不是 `./fleet`:`fleet.ts` 被 `types.ts` 引,它再引本文件会成环。 */
|
|
253
258
|
export declare function isReportableRefusalName(name: string): boolean;
|
|
259
|
+
/** 扣下的名的**可比记号**(12.0.0)—— `withheld:` + SHA-256(域分隔前缀 + 原名的 JSON 串形)的前 12 个十六进制位。**唯一**铸法:被丢条目的身份
|
|
260
|
+
* ({@link DroppedEntryIdentity}`.token`)由它铸,消费方要问「请求点名的这个名是不是那条扣名行」就对请求里的名调它、比 token —— 不另写一份摘要。
|
|
261
|
+
* 稳定(同名恒同记号)、不可逆(48 位摘要,秘密形名字本身是高熵串);域分隔前缀让它与别处对同一串算的 SHA-256 指纹对不上。它只区分名字,
|
|
262
|
+
* 不是 MAC、不防字典猜测:低熵的名(`Basic Chat`)能被猜回来 —— 那类名被扣下是因为字符集外,本就不是秘密。
|
|
263
|
+
* 摘要输入取 `JSON.stringify(name)`:UTF-8 编码会把孤立代理项换成 U+FFFD(`"\ud800"` / `"\ud801"` / `"\ufffd"` 三个不同的名同一串字节),
|
|
264
|
+
* JSON 串形逐个 UTF-16 码元保留(孤立代理项写成转义)—— 不同的名恒是不同的输入。 */
|
|
265
|
+
export declare function withheldNameToken(name: string): string;
|
|
254
266
|
/** 抢救造成的**下标位移表**:数组字段名 → 该字段压缩后每个位置的**原**下标(`kept[j]` = 压缩后第 j 条
|
|
255
267
|
* 在原文档里的下标)。纯数据,无行为。
|
|
256
268
|
*
|
package/dist/config-fns.js
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
import { z } from "zod";
|
|
19
19
|
import { MODEL_ENTRY_REFUSAL_CODES, ModelRefusalsWire } from "./fleet.js";
|
|
20
20
|
import { carriesUserinfo, secretScanForms } from "./secret-scan.js"; // package-private normalization (both scanners read it) + the userinfo judgement (not in the barrel)
|
|
21
|
+
import { sha256Hex } from "./sha256.js";
|
|
21
22
|
import { unwrapSchema } from "./zod-unwrap.js"; // package-private: the ONE reading of zod's wrapper layers (salvage + the one-time migrations share it)
|
|
22
23
|
import { DOMAIN_SCHEMAS, InfraCostRates, LimitsConfig, PRIVATE_KEY_PEM_HEADER, RETIRED_DOMAINS, SkillsManifestSchema, containsSecretToken, isDomainName, isRetiredDomainName, renderQuotedSentence, retiredDomainSentence } from "./types.js";
|
|
23
24
|
/** Redact secret-shaped tokens from a validation MESSAGE — a zod error echoes the offending value (e.g.
|
|
@@ -327,6 +328,17 @@ export function isReportableRefusalName(name) {
|
|
|
327
328
|
const probe = ModelRefusalsWire.safeParse({ entries: { [name]: MODEL_ENTRY_REFUSAL_CODES[0] } });
|
|
328
329
|
return probe.success && Object.hasOwn(probe.data.entries, name);
|
|
329
330
|
}
|
|
331
|
+
/** {@link withheldNameToken} 的域分隔前缀(摘要输入 = 前缀 + 原名的 JSON 串形)。 */
|
|
332
|
+
const WITHHELD_NAME_TOKEN_DOMAIN = "sema-settings-schema/withheld-name\u0000";
|
|
333
|
+
/** 扣下的名的**可比记号**(12.0.0)—— `withheld:` + SHA-256(域分隔前缀 + 原名的 JSON 串形)的前 12 个十六进制位。**唯一**铸法:被丢条目的身份
|
|
334
|
+
* ({@link DroppedEntryIdentity}`.token`)由它铸,消费方要问「请求点名的这个名是不是那条扣名行」就对请求里的名调它、比 token —— 不另写一份摘要。
|
|
335
|
+
* 稳定(同名恒同记号)、不可逆(48 位摘要,秘密形名字本身是高熵串);域分隔前缀让它与别处对同一串算的 SHA-256 指纹对不上。它只区分名字,
|
|
336
|
+
* 不是 MAC、不防字典猜测:低熵的名(`Basic Chat`)能被猜回来 —— 那类名被扣下是因为字符集外,本就不是秘密。
|
|
337
|
+
* 摘要输入取 `JSON.stringify(name)`:UTF-8 编码会把孤立代理项换成 U+FFFD(`"\ud800"` / `"\ud801"` / `"\ufffd"` 三个不同的名同一串字节),
|
|
338
|
+
* JSON 串形逐个 UTF-16 码元保留(孤立代理项写成转义)—— 不同的名恒是不同的输入。 */
|
|
339
|
+
export function withheldNameToken(name) {
|
|
340
|
+
return `withheld:${sha256Hex(`${WITHHELD_NAME_TOKEN_DOMAIN}${JSON.stringify(name)}`).slice(0, 12)}`;
|
|
341
|
+
}
|
|
330
342
|
/** 一条被丢条目原文的身份({@link DroppedEntryIdentity} 的**唯一**读法)。只看纯 JSON 对象的**自有数据
|
|
331
343
|
* 属性**;任何反射/陷阱抛错 ⇒ 当作读不出(身份是诊断面的附件,绝不许把抢救拖下水)。 */
|
|
332
344
|
function readEntryIdentity(entry) {
|
|
@@ -341,7 +353,7 @@ function readEntryIdentity(entry) {
|
|
|
341
353
|
const id = own("id");
|
|
342
354
|
const enabled = own("enabled");
|
|
343
355
|
const identity = {
|
|
344
|
-
...(typeof name === "string" ? (isDisclosableIdentity(name) ? { name } : { nameWithheld: true }) : {}),
|
|
356
|
+
...(typeof name === "string" ? (isDisclosableIdentity(name) ? { name } : { nameWithheld: true, token: withheldNameToken(name) }) : {}),
|
|
345
357
|
...(typeof id === "string" ? (isDisclosableIdentity(id) ? { id } : { idWithheld: true }) : {}),
|
|
346
358
|
...(typeof enabled === "boolean" ? { enabled } : {}),
|
|
347
359
|
};
|
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
|
package/dist/hooks.js
CHANGED
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
*/
|
|
31
31
|
import { z } from "zod";
|
|
32
32
|
import { INTEGER } from "./integer.js";
|
|
33
|
+
import { buildQuotedIssue } from "./quoted-issue.js";
|
|
33
34
|
// ── event set ──────────────────────────────────────────────────────────────────────────────────────────
|
|
34
35
|
// The two halves below are where each word came from (CC 2.1.198's own table, then the later additions) — provenance for the
|
|
35
36
|
// reader of this file, not a contract: they are module-private, and the ONE exported closed set is {@link HOOK_EVENTS}.
|
|
@@ -623,7 +624,7 @@ export const HOOK_INJECTED_ENV = [
|
|
|
623
624
|
/** Default per-hook timeout when an entry does not set one (seconds; CC default). */
|
|
624
625
|
export const DEFAULT_HOOK_TIMEOUT_SECONDS = 60;
|
|
625
626
|
/** A non-empty string with no NUL byte (spawn()/JSON-safe). */
|
|
626
|
-
const hookStr = z.string().min(1).refine((s) => !s.includes("\0"),
|
|
627
|
+
const hookStr = z.string().min(1).refine((s) => !s.includes("\0"), buildQuotedIssue `must not contain a NUL byte`);
|
|
627
628
|
/**
|
|
628
629
|
* `if` condition — permission-rule syntax (e.g. "Bash(git *)") evaluated against tool_name/tool_input
|
|
629
630
|
* BEFORE spawning the hook (only meaningful on tool events). Shell superset field, CC-compatible.
|
|
@@ -684,6 +685,25 @@ export const HookMatcherEntry = z.object({
|
|
|
684
685
|
});
|
|
685
686
|
/** The characters a matcher may consist of to be read as a NAME LIST rather than a regex. */
|
|
686
687
|
const HOOK_MATCHER_NAME_LIST = /^[a-zA-Z0-9_|, -]+$/;
|
|
688
|
+
function readHookMatcher(matcher) {
|
|
689
|
+
if (matcher === undefined || matcher === "" || matcher === "*")
|
|
690
|
+
return { form: "all" };
|
|
691
|
+
if (HOOK_MATCHER_NAME_LIST.test(matcher)) {
|
|
692
|
+
return {
|
|
693
|
+
form: "names",
|
|
694
|
+
names: matcher
|
|
695
|
+
.split(/[|,]/)
|
|
696
|
+
.map((part) => part.trim())
|
|
697
|
+
.filter((part) => part !== ""),
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
try {
|
|
701
|
+
return { form: "regex", pattern: new RegExp(matcher) };
|
|
702
|
+
}
|
|
703
|
+
catch {
|
|
704
|
+
return { form: "invalid" };
|
|
705
|
+
}
|
|
706
|
+
}
|
|
687
707
|
/**
|
|
688
708
|
* Does `matcher` select `name`? —— **the one reading** of a hooks matcher (11.0.0). The shell and the server both ask this
|
|
689
709
|
* function; neither keeps a copy.
|
|
@@ -703,25 +723,50 @@ const HOOK_MATCHER_NAME_LIST = /^[a-zA-Z0-9_|, -]+$/;
|
|
|
703
723
|
* for the bad matcher itself.
|
|
704
724
|
* Before 11.0.0 the server anchored the regex form (`^(?:…)$`) while the shell did not, so one regex matcher selected different
|
|
705
725
|
* tools in the two places. A deployment whose regex matchers relied on that anchoring selects MORE tools now — add `^…$`.
|
|
706
|
-
* Pure: no budget / timeout — a caller evaluating untrusted regex matchers under a deadline does so around this call
|
|
726
|
+
* Pure: no budget / timeout — a caller evaluating untrusted regex matchers under a deadline does so around this call
|
|
727
|
+
* ({@link hookMatcherForm} tells it beforehand which matchers run a regex at all).
|
|
707
728
|
*/
|
|
708
729
|
export function hookMatcherSelects(matcher, name) {
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
730
|
+
const r = readHookMatcher(matcher);
|
|
731
|
+
switch (r.form) {
|
|
732
|
+
case "all":
|
|
733
|
+
return true;
|
|
734
|
+
case "names":
|
|
735
|
+
return r.names.includes(name);
|
|
736
|
+
case "regex":
|
|
737
|
+
return r.pattern.test(name);
|
|
738
|
+
case "invalid":
|
|
739
|
+
return "invalid";
|
|
740
|
+
default: {
|
|
741
|
+
const unhandled = r;
|
|
742
|
+
throw new Error(`hookMatcherSelects: unhandled matcher reading ${JSON.stringify(unhandled)}`);
|
|
743
|
+
}
|
|
720
744
|
}
|
|
721
|
-
|
|
722
|
-
|
|
745
|
+
}
|
|
746
|
+
/**
|
|
747
|
+
* Which form `matcher` takes under the one reading —— the same parse step as {@link hookMatcherSelects}, so for every
|
|
748
|
+
* matcher `hookMatcherForm(m) === "invalid"` ⇔ `hookMatcherSelects(m, name) === "invalid"` (for any `name`):
|
|
749
|
+
* · `"literal"` —— no regex is compiled or run: absent / `""` / `"*"` (selects everything) and the NAME-LIST form (only name
|
|
750
|
+
* characters, `|`, `,` and spaces — membership after splitting);
|
|
751
|
+
* · `"regex"` —— anything else that compiles as a JS regex (tested unanchored);
|
|
752
|
+
* · `"invalid"` —— a regex that does not compile (the caller does not run the hook).
|
|
753
|
+
* For a caller that must route matchers before evaluating them (run the literal ones inline, give the regex ones a deadline).
|
|
754
|
+
*/
|
|
755
|
+
export function hookMatcherForm(matcher) {
|
|
756
|
+
const r = readHookMatcher(matcher);
|
|
757
|
+
switch (r.form) {
|
|
758
|
+
case "all":
|
|
759
|
+
case "names":
|
|
760
|
+
return "literal";
|
|
761
|
+
case "regex":
|
|
762
|
+
return "regex";
|
|
763
|
+
case "invalid":
|
|
764
|
+
return "invalid";
|
|
765
|
+
default: {
|
|
766
|
+
const unhandled = r;
|
|
767
|
+
throw new Error(`hookMatcherForm: unhandled matcher reading ${JSON.stringify(unhandled)}`);
|
|
768
|
+
}
|
|
723
769
|
}
|
|
724
|
-
return pattern.test(name);
|
|
725
770
|
}
|
|
726
771
|
export const HooksConfig = z.partialRecord(HookEventName, z.array(HookMatcherEntry));
|
|
727
772
|
/**
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 叶子模块(纯、浏览器安全):**一只**「主机绝对路径」形判 —— 任务请求的 `cwd`(服务器受理时判)与项目登记里 `folder` / `file`
|
|
3
|
+
* 条目的值读同一只,不各写一份。只判形,不判存在(不碰文件系统)。
|
|
4
|
+
*/
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
/** 一条主机绝对路径的长度上界(字符数)。 */
|
|
7
|
+
export declare const ABSOLUTE_HOST_PATH_MAX = 4096;
|
|
8
|
+
/**
|
|
9
|
+
* `p` 是不是一条形上合法的**主机绝对路径**:字符串、非空、≤ {@link ABSOLUTE_HOST_PATH_MAX}、不含 NUL、以 `/` 开头(posix 绝对)、没有 `..` 段
|
|
10
|
+
* (`/` 与 `\` 都算分隔符 —— `path.isAbsolute` 是语法判,`/a/../../etc` 也「绝对」,而读方 `path.resolve` 会把它解到别处;拒掉它,读到的就是写下的那一条)。
|
|
11
|
+
*
|
|
12
|
+
* posix 口径是有意的:读这些路径的主机车道今天跑 posix 树,而这一律在 posix 宿主上与 Node 的 `path.isAbsolute` 逐字同判(`C:\x`、`\\host\share`
|
|
13
|
+
* 在 posix 上不绝对)。哪天主机车道要接 win 树,换的是这一只(按树的家族判),读方跟着同一只走。
|
|
14
|
+
*/
|
|
15
|
+
export declare function isAbsoluteHostPath(p: unknown): p is string;
|
|
16
|
+
/** {@link isAbsoluteHostPath} 的 zod 座位(一条 issue,定句)。 */
|
|
17
|
+
export declare const AbsoluteHostPath: z.ZodString & z.ZodType<string, string, z.core.$ZodTypeInternals<string, string>>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 叶子模块(纯、浏览器安全):**一只**「主机绝对路径」形判 —— 任务请求的 `cwd`(服务器受理时判)与项目登记里 `folder` / `file`
|
|
3
|
+
* 条目的值读同一只,不各写一份。只判形,不判存在(不碰文件系统)。
|
|
4
|
+
*/
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { buildQuotedIssue, fixed } from "./quoted-issue.js";
|
|
7
|
+
/** 一条主机绝对路径的长度上界(字符数)。 */
|
|
8
|
+
export const ABSOLUTE_HOST_PATH_MAX = 4096;
|
|
9
|
+
/**
|
|
10
|
+
* `p` 是不是一条形上合法的**主机绝对路径**:字符串、非空、≤ {@link ABSOLUTE_HOST_PATH_MAX}、不含 NUL、以 `/` 开头(posix 绝对)、没有 `..` 段
|
|
11
|
+
* (`/` 与 `\` 都算分隔符 —— `path.isAbsolute` 是语法判,`/a/../../etc` 也「绝对」,而读方 `path.resolve` 会把它解到别处;拒掉它,读到的就是写下的那一条)。
|
|
12
|
+
*
|
|
13
|
+
* posix 口径是有意的:读这些路径的主机车道今天跑 posix 树,而这一律在 posix 宿主上与 Node 的 `path.isAbsolute` 逐字同判(`C:\x`、`\\host\share`
|
|
14
|
+
* 在 posix 上不绝对)。哪天主机车道要接 win 树,换的是这一只(按树的家族判),读方跟着同一只走。
|
|
15
|
+
*/
|
|
16
|
+
export function isAbsoluteHostPath(p) {
|
|
17
|
+
return typeof p === "string" && p.length > 0 && p.length <= ABSOLUTE_HOST_PATH_MAX && !p.includes("\0") && p.startsWith("/") && !p.split(/[/\\]/).includes("..");
|
|
18
|
+
}
|
|
19
|
+
/** {@link isAbsoluteHostPath} 的 zod 座位(一条 issue,定句)。 */
|
|
20
|
+
export const AbsoluteHostPath = z
|
|
21
|
+
.string()
|
|
22
|
+
.refine(isAbsoluteHostPath, buildQuotedIssue `must be an absolute host path: starts with "/", no ".." segment, no NUL, at most ${fixed(String(ABSOLUTE_HOST_PATH_MAX))} characters`);
|
package/dist/integer.js
CHANGED
|
@@ -8,4 +8,5 @@
|
|
|
8
8
|
* written down here once, instead of being borrowed from the library and changing whenever the library does.
|
|
9
9
|
*/
|
|
10
10
|
import { z } from "zod";
|
|
11
|
-
|
|
11
|
+
import { buildQuotedIssue } from "./quoted-issue.js";
|
|
12
|
+
export const INTEGER = z.number().refine((n) => Number.isInteger(n), buildQuotedIssue `Expected integer, received float`);
|
package/dist/local-load.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type RequiredEnvName } from "./file-edit.js";
|
|
2
2
|
import { type EffectiveReadWarning } from "./config-fns.js";
|
|
3
|
-
import { RemoteExecSpec } from "./remote-exec.js";
|
|
3
|
+
import { type RemoteExecSpec } from "./remote-exec.js";
|
|
4
4
|
import type { EffectiveConfig } from "./types.js";
|
|
5
5
|
/** The remoteExec file path under a config root (under config.d/, beside the per-domain files). */
|
|
6
6
|
export declare function remoteExecPath(root: string): string;
|
package/dist/local-load.js
CHANGED
|
@@ -13,25 +13,12 @@ import path from "node:path";
|
|
|
13
13
|
import { writeFileAtomic } from "./atomic-write.js";
|
|
14
14
|
import { FileConfigStore } from "./file-store.js";
|
|
15
15
|
import { listRequiredEnvNames } from "./file-edit.js";
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
|
|
19
|
-
* ride the thrown error). A hostile getter/Proxy trap → a fixed message. R14. */
|
|
20
|
-
function parseRemoteExec(raw) {
|
|
21
|
-
let r;
|
|
22
|
-
try {
|
|
23
|
-
r = RemoteExecSpec.safeParse(raw);
|
|
24
|
-
}
|
|
25
|
-
catch {
|
|
26
|
-
throw new Error("invalid remoteExec (a value could not be read during validation)");
|
|
27
|
-
}
|
|
28
|
-
if (r.success)
|
|
29
|
-
return r.data;
|
|
30
|
-
throw sanitizeZodError(r.error);
|
|
31
|
-
}
|
|
16
|
+
import {} from "./config-fns.js";
|
|
17
|
+
import { configDDocumentText, judgeConfigDText, parseRemoteExecDocument } from "./config-d-file.js";
|
|
18
|
+
import { REMOTE_EXEC_DOCUMENT } from "./remote-exec.js";
|
|
32
19
|
/** The remoteExec file path under a config root (under config.d/, beside the per-domain files). */
|
|
33
20
|
export function remoteExecPath(root) {
|
|
34
|
-
return path.join(root, "config.d",
|
|
21
|
+
return path.join(root, "config.d", `${REMOTE_EXEC_DOCUMENT}.json`);
|
|
35
22
|
}
|
|
36
23
|
/** Read `<root>/config.d/remote-exec.json` → a VALIDATED RemoteExecSpec, or undefined if the file is absent
|
|
37
24
|
* (→ the engine defaults to `host` under CONFIG_PROVIDER=local). Throws (zod) on a present-but-invalid file. */
|
|
@@ -45,28 +32,19 @@ export async function loadRemoteExec(root) {
|
|
|
45
32
|
return undefined;
|
|
46
33
|
throw e;
|
|
47
34
|
}
|
|
48
|
-
const trimmed = text.trim();
|
|
49
35
|
// A present-but-empty file is a corruption/half-write, NOT "use the default" — FAIL CLOSED (an empty file
|
|
50
36
|
// silently falling back to the host lane would be an isolation DOWNGRADE if the user meant local-docker).
|
|
51
|
-
// To get the host default, the file must be ABSENT, not empty.
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
let json;
|
|
55
|
-
try {
|
|
56
|
-
json = JSON.parse(trimmed); // sanitized: don't let a SyntaxError echo the raw (possibly secret-bearing) file text
|
|
57
|
-
}
|
|
58
|
-
catch {
|
|
59
|
-
throw new Error(`${remoteExecPath(root)} — invalid JSON (delete it for the host default, or write a valid remoteExec spec)`);
|
|
60
|
-
}
|
|
61
|
-
return parseRemoteExec(json);
|
|
37
|
+
// To get the host default, the file must be ABSENT, not empty. The text judge is THE config.d one (empty / bad JSON / not an object, one sentence
|
|
38
|
+
// each — the same judge FileConfigStore and the directory migration read with); the sanitized value judge is the migration's too (`./config-d-file`).
|
|
39
|
+
return parseRemoteExecDocument(judgeConfigDText(REMOTE_EXEC_DOCUMENT, text));
|
|
62
40
|
}
|
|
63
41
|
/** Validate + ATOMICALLY write `<root>/config.d/remote-exec.json` (the package's one atomic writer, `./atomic-write`; never a half file). The
|
|
64
42
|
* client's `config` editor writes the exec backend through this; the engine reads it via loadRemoteExec. */
|
|
65
43
|
export async function writeRemoteExec(root, spec) {
|
|
66
|
-
const parsed =
|
|
44
|
+
const parsed = parseRemoteExecDocument(spec);
|
|
67
45
|
const dir = path.join(root, "config.d");
|
|
68
46
|
await mkdir(dir, { recursive: true });
|
|
69
|
-
writeFileAtomic(remoteExecPath(root),
|
|
47
|
+
writeFileAtomic(remoteExecPath(root), configDDocumentText(parsed), "config-document");
|
|
70
48
|
return parsed;
|
|
71
49
|
}
|
|
72
50
|
/** Resolve a `file:<path>` .env VALUE → the file's trimmed content (the file-NAME secret ref, config-research
|
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
* 一次性迁移的**收据形**与两张闭集(11.0.0)—— 叶子模块(运行期零依赖;只有型导入),由 `./one-time-migrations` 再导出(不进主桶)。
|
|
3
3
|
* 单拆出来只为一件事:调度店的读口(`./scheduler-store`,轻、浏览器安全)要在告警里带收据,不该为一个型把整套域 schema 拉进来。
|
|
4
4
|
*/
|
|
5
|
+
import type { REMOTE_EXEC_DOCUMENT } from "./remote-exec.js";
|
|
5
6
|
import type { DomainName, RetiredDomainName } from "./types.js";
|
|
6
7
|
/** 本版登记的一次性**数据**迁移 id(闭集;每个 id 在 `ONE_TIME_MIGRATIONS` 登记表里恰一行)。收据只许这些 id —— 同表登记的 wire 删码位
|
|
7
8
|
* (`WIRE_RETIREMENT_IDS`,`./one-time-migrations`)不改任何存储形,铸不出收据,不在本集。 */
|
|
8
|
-
export declare const ONE_TIME_MIGRATION_IDS: readonly ["SS-M1", "SS-M2", "SS-M3", "SS-M4", "SS-M5", "SS-M6", "SS-M7", "SS-M9"];
|
|
9
|
+
export declare const ONE_TIME_MIGRATION_IDS: readonly ["SS-M1", "SS-M2", "SS-M3", "SS-M4", "SS-M5", "SS-M6", "SS-M7", "SS-M8", "SS-M9"];
|
|
9
10
|
export type OneTimeMigrationId = (typeof ONE_TIME_MIGRATION_IDS)[number];
|
|
10
11
|
/**
|
|
11
12
|
* 一条收据说迁移**对一个坐标做了什么**(闭集,每词一义):
|
|
@@ -17,8 +18,8 @@ export type OneTimeMigrationId = (typeof ONE_TIME_MIGRATION_IDS)[number];
|
|
|
17
18
|
*/
|
|
18
19
|
export declare const MIGRATION_APPLIED_AS: readonly ["moved", "rewritten", "dropped-shadowed", "dropped-inert", "refused"];
|
|
19
20
|
export type MigrationAppliedAs = (typeof MIGRATION_APPLIED_AS)[number];
|
|
20
|
-
/** 收据指向的那份文档:配置域名、retired 域名(`runtime`),或调度店(`scheduler-store`)。 */
|
|
21
|
-
export type MigrationDocument = DomainName | RetiredDomainName | "scheduler-store";
|
|
21
|
+
/** 收据指向的那份文档:配置域名、retired 域名(`runtime`)、`config.d/remote-exec.json`(`remote-exec`),或调度店(`scheduler-store`)。 */
|
|
22
|
+
export type MigrationDocument = DomainName | RetiredDomainName | typeof REMOTE_EXEC_DOCUMENT | "scheduler-store";
|
|
22
23
|
/** 文档里的段坐标(字符串段 = 对象键,数字段 = 数组下标;与告警坐标同一形)。 */
|
|
23
24
|
export type MigrationPath = readonly (string | number)[];
|
|
24
25
|
interface ReceiptBase {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** 本版登记的一次性**数据**迁移 id(闭集;每个 id 在 `ONE_TIME_MIGRATIONS` 登记表里恰一行)。收据只许这些 id —— 同表登记的 wire 删码位
|
|
2
2
|
* (`WIRE_RETIREMENT_IDS`,`./one-time-migrations`)不改任何存储形,铸不出收据,不在本集。 */
|
|
3
|
-
export const ONE_TIME_MIGRATION_IDS = ["SS-M1", "SS-M2", "SS-M3", "SS-M4", "SS-M5", "SS-M6", "SS-M7", "SS-M9"];
|
|
3
|
+
export const ONE_TIME_MIGRATION_IDS = ["SS-M1", "SS-M2", "SS-M3", "SS-M4", "SS-M5", "SS-M6", "SS-M7", "SS-M8", "SS-M9"];
|
|
4
4
|
/**
|
|
5
5
|
* 一条收据说迁移**对一个坐标做了什么**(闭集,每词一义):
|
|
6
6
|
* - `moved` — 值搬到新位(`to`),义不变;
|
package/dist/node.d.ts
CHANGED
|
@@ -10,5 +10,5 @@ export * from "./bundle.js";
|
|
|
10
10
|
export * from "./local-load.js";
|
|
11
11
|
export * from "./scheduler-store-node.js";
|
|
12
12
|
export * from "./prompts-digest.js";
|
|
13
|
-
export { migrateConfigDirectory, type ConfigDirectoryIo, type ConfigDirectoryMigration, type ConfigDirectoryRefusal, type MigrateConfigDirectoryOptions } from "./config-dir-migration.js";
|
|
13
|
+
export { migrateConfigDirectory, type ConfigDirectoryDocument, type ConfigDirectoryIo, type ConfigDirectoryMigration, type ConfigDirectoryRefusal, type MigrateConfigDirectoryOptions } from "./config-dir-migration.js";
|
|
14
14
|
export { nodeConfigDirectoryIo } from "./config-dir-io.js";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { MigrationReceipt, OneTimeMigrationId } from "./migration-receipt.js";
|
|
2
|
+
import { REMOTE_EXEC_DOCUMENT } from "./remote-exec.js";
|
|
2
3
|
import { type DomainName, type RetiredDomainName } from "./types.js";
|
|
3
4
|
export { MIGRATION_APPLIED_AS, ONE_TIME_MIGRATION_IDS, type MigrationAppliedAs, type MigrationDocument, type MigrationPath, type MigrationReceipt, type OneTimeMigrationId } from "./migration-receipt.js";
|
|
4
5
|
export { migrateSchedulerRecords } from "./scheduler-store-migration.js";
|
|
@@ -33,12 +34,12 @@ export declare const ONE_TIME_MIGRATIONS: readonly OneTimeMigrationWindow[];
|
|
|
33
34
|
/** 一组存储文档是不是**一个 scope 的全集**:`"scope"` = 缺席的域就是从未设过;`"partial"` = 缺席的域只是没给(principal 层、回滚史的
|
|
34
35
|
* 单域行、任意子集)。只有 SS-M1 读它。 */
|
|
35
36
|
export type DocumentCoverage = "scope" | "partial";
|
|
36
|
-
/** 一组存储文档(域名 / retired 域名 → 文档)。别的键原样带过、不动。 */
|
|
37
|
-
export type StoredDocuments = Readonly<Partial<Record<DomainName | RetiredDomainName, unknown>>>;
|
|
37
|
+
/** 一组存储文档(域名 / retired 域名 / `remote-exec` → 文档)。别的键原样带过、不动。 */
|
|
38
|
+
export type StoredDocuments = Readonly<Partial<Record<DomainName | RetiredDomainName | typeof REMOTE_EXEC_DOCUMENT, unknown>>>;
|
|
38
39
|
export interface StoredDocumentsMigration {
|
|
39
40
|
/** 迁移后的文档集:没改的文档是**同一引用**;被移除的(retired 域)不在;新建的(例:迁出的 `governance` / `limits`)在。 */
|
|
40
41
|
readonly docs: Readonly<Record<string, unknown>>;
|
|
41
|
-
/** 逐条收据(步序 SS-M1 → SS-M2 → SS-M5 → SS-M6 → SS-M9 → SS-M7 → SS-M4:纯改写步在前,用新 schema 终判的 SS-M4 最后;步内按坐标)。
|
|
42
|
+
/** 逐条收据(步序 SS-M1 → SS-M2 → SS-M5 → SS-M6 → SS-M8 → SS-M9 → SS-M7 → SS-M4:纯改写步在前,用新 schema 终判的 SS-M4 最后;步内按坐标)。
|
|
42
43
|
* 只有 `refused` 的收据 = 什么都没改。 */
|
|
43
44
|
readonly receipts: readonly MigrationReceipt[];
|
|
44
45
|
}
|
|
@@ -58,5 +59,6 @@ export declare function migrateEffectiveSnapshot(effective: unknown): {
|
|
|
58
59
|
readonly effective: unknown;
|
|
59
60
|
readonly receipts: readonly MigrationReceipt[];
|
|
60
61
|
};
|
|
61
|
-
/**
|
|
62
|
+
/** SS-M7 走查的全部 URL 座位的坐标模式(派生;数组 / Record 位写 `*`;域文档以域名起头,`remote-exec.json` 以 `remote-exec` 起头)—— 迁移不用它,
|
|
63
|
+
* 它给钉与说明看「今天派生出哪些座位」。 */
|
|
62
64
|
export declare function urlSeatPatterns(): string[];
|