@sema-agent/settings-schema 15.2.0 → 16.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 +137 -0
- package/README.md +52 -46
- package/conformance/VERSION +1 -1
- package/conformance/cases/c16-deep-nesting-205/expected.json +2 -2
- package/conformance/cases/c16-deep-nesting-205/meta.json +1 -1
- package/conformance/cases/c50-s784-lane-host-legal/expected.json +6 -2
- package/conformance/cases/c50-s784-lane-host-legal/input.json +3 -1
- package/conformance/cases/c51-s784-lane-e2b-legal/expected.json +6 -2
- package/conformance/cases/c51-s784-lane-e2b-legal/input.json +3 -1
- package/conformance/cases/c52-s784-lane-k8s-legal/expected.json +6 -2
- package/conformance/cases/c52-s784-lane-k8s-legal/input.json +3 -1
- package/conformance/cases/c53-s784-lane-ssh-legal/expected.json +6 -2
- package/conformance/cases/c53-s784-lane-ssh-legal/input.json +3 -1
- package/conformance/cases/c54-s784-lane-adb-legal/expected.json +6 -2
- package/conformance/cases/c54-s784-lane-adb-legal/input.json +3 -1
- package/conformance/cases/c55-s784-lane-local-docker-legal/expected.json +6 -2
- package/conformance/cases/c55-s784-lane-local-docker-legal/input.json +3 -1
- package/conformance/cases/c56-s784-lane-device-legal/expected.json +8 -2
- package/conformance/cases/c56-s784-lane-device-legal/input.json +5 -2
- package/conformance/cases/c57-s784-g1-enabled-no-lane/meta.json +1 -1
- package/conformance/cases/c58-s784-g2-multitenant-host/input.json +3 -1
- package/conformance/cases/c59-s784-g3-k8s-missing-image/input.json +3 -1
- package/conformance/cases/c60-s784-g3-e2b-missing-key/input.json +3 -1
- package/conformance/cases/c61-s784-g3-other-lane-key/input.json +3 -1
- package/conformance/cases/c62-s784-g7a-session-tidb/input.json +3 -1
- package/conformance/cases/c63-s784-host-two-replicas/input.json +3 -1
- package/conformance/cases/c64-s784-multitenant-local-store/input.json +3 -1
- package/conformance/cases/c65-s784-pg-without-db/input.json +3 -1
- package/conformance/cases/c66-s784-memory-backend-mismatch/input.json +3 -1
- package/conformance/cases/c66-s784-memory-backend-mismatch/meta.json +1 -1
- package/conformance/cases/c68-s784-device-local-store/input.json +6 -1
- package/conformance/cases/cli04-env-value-with-newline/expected.json +6 -2
- package/conformance/cases/cli04-env-value-with-newline/input.json +3 -1
- package/dist/bundle.d.ts +0 -18
- package/dist/bundle.js +1 -54
- package/dist/cli.d.ts +1 -1
- package/dist/config-d-file.d.ts +5 -5
- package/dist/config-d-file.js +58 -14
- package/dist/config-document-refusal.d.ts +9 -7
- package/dist/config-document-refusal.js +2 -2
- package/dist/device-relay.d.ts +21 -0
- package/dist/device-relay.js +23 -0
- package/dist/env-model-table.d.ts +2 -9
- package/dist/env-model-table.js +6 -14
- package/dist/execution-lane-index.d.ts +19 -7
- package/dist/execution-lane-index.js +26 -8
- package/dist/execution-lanes.d.ts +9 -4
- package/dist/execution-lanes.js +18 -6
- package/dist/file-store.d.ts +30 -3
- package/dist/file-store.js +74 -8
- package/dist/fleet.d.ts +6 -8
- package/dist/fleet.js +5 -7
- package/dist/hooks.d.ts +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -1
- package/dist/migration-receipt.d.ts +1 -1
- package/dist/migration-receipt.js +1 -1
- package/dist/one-time-migrations.d.ts +1 -1
- package/dist/one-time-migrations.js +166 -45
- package/dist/remote-exec.js +11 -4
- package/dist/types.d.ts +15 -11
- package/dist/types.js +99 -48
- package/dist/url-shape.d.ts +18 -0
- package/dist/url-shape.js +22 -0
- package/docs/API-SURFACE.md +12 -8
- package/docs/CONFORMANCE.md +1 -7
- package/docs/CONSUMER-INTEGRATION.md +3 -3
- package/llms-full.txt +24 -28
- package/llms.txt +9 -8
- package/package.json +1 -1
package/dist/env-model-table.js
CHANGED
|
@@ -7,21 +7,21 @@
|
|
|
7
7
|
* cheap 槽四态({@link EnvCheapSlot})。「env 模型表已声明」({@link envModelTableDeclared})与角色底({@link buildEnvModelBaseline})都由它派生 ——
|
|
8
8
|
* 这道闸的执行方(引擎一侧;今天引擎还不强制)与写者的计划助手调同一份(一次性迁移 SS-M11 只用两槽点名被顶替的 env 模型,铸不铸不看它),不各写一份。env 快照按「读到即算」(下标读;原型分层的快照里继承来的键算在场)。
|
|
9
9
|
* 引擎另有「名册占位 ⇒ env 表算空」一格,那是引擎自己的部署事实,不在本包。
|
|
10
|
-
* · **计划助手判序**({@link planEnvModelTableWrite}):目标腿不读 `config.d`(remote / none)⇒ 照写 → 候选不管模型面 ⇒ 照写 →
|
|
11
|
-
*
|
|
10
|
+
* · **计划助手判序**({@link planEnvModelTableWrite}):目标腿不读 `config.d`(remote / none)⇒ 照写 → 候选不管模型面 ⇒ 照写 →
|
|
11
|
+
* env 表未声明 ⇒ 照写、**不写确认** → 配置根的决定是 `replaced` ⇒ 照写 →
|
|
12
12
|
* 调用方带回的确认键等于这一次的 ⇒ `write_with_decision`(先经 `writeEnvModelsDecision` 写 `replaced`,再写目录;两次写在写者对 models.json 的同一把锁里)→
|
|
13
13
|
* 否则 `confirm`(把回执给用户看)。执法在引擎一侧(本地腿的顶替闸;今天引擎不拒这个组),本助手只是让写者在写之前知道这道闸强制起来会不会拒、会顶掉什么。
|
|
14
14
|
* · **回执按 id**:每个退场的 env 条目带 `servedAs`(候选里同 id 的启用条目名 —— 上游模型仍被服务,形可能不同);每个 env 底角色带写前绑谁
|
|
15
15
|
* (env 表键 + 它解到的 id)与采纳后落到哪个条目(`{name, id}`)。缺省落点经 `resolveCatalogDefaultModel`(与引擎的 stage 同一只解析器);
|
|
16
16
|
* 绑到停用条目或目录外的名 = `dangles`。确认键 = 回执的定序 JSON —— 后果(落点名 / id、env 槽、决定读数)一变,旧键就过期。
|
|
17
|
-
* · 可用性(引擎的 brain 腿、端点、读腿逐条丢)
|
|
17
|
+
* · 可用性(引擎的 brain 腿、端点、读腿逐条丢)计划判不了。悬空的 summarize 不是拒(16.0.0 起;此前计划把它判成「不可确认」,前提不成立,那一臂删了):引擎把点名目录外 / 停用条目的角色席
|
|
18
|
+
* 读成未设、按角色回退链落下一级并说一次 —— 没有哪种读法因它拒组;它照样写在回执里(`dangles`),要确认的写法把它给用户看。
|
|
18
19
|
*/
|
|
19
20
|
import { z } from "zod";
|
|
20
21
|
import { entryEnabledFlag, catalogManagesModelPlane } from "./catalog-manages.js";
|
|
21
22
|
import { parseDomainSalvaged, isDisclosableIdentity, resolveActiveTiers, withheldNameToken } from "./config-fns.js";
|
|
22
23
|
import { ENV_MODELS_DOCUMENT } from "./config-root-documents.js";
|
|
23
24
|
import { resolveCatalogDefaultModel } from "./default-model.js";
|
|
24
|
-
import { SUMMARIZE_MODEL_MISSING_CODE } from "./fleet.js";
|
|
25
25
|
import { landCatalogWord } from "./tier-words.js";
|
|
26
26
|
import { stableStringify } from "./hash.js";
|
|
27
27
|
import { captureJson, dataDescriptor, isolatedObject } from "./own-key-read.js";
|
|
@@ -205,8 +205,6 @@ export function planEnvModelTableWrite(input) {
|
|
|
205
205
|
return { kind: "write", why: "not_managing" };
|
|
206
206
|
const env = snapshotEnvModelTable(input.env); // read once: the receipt and the env-table rule answer from the same reading
|
|
207
207
|
const receipt = buildReceipt(buildEnvModelBaseline(env), candidateView(copy.value), decision);
|
|
208
|
-
if (receipt.roles.find((r) => r.role === "summarize")?.after.kind === "dangles")
|
|
209
|
-
return { kind: "fix_first", code: SUMMARIZE_MODEL_MISSING_CODE, receipt };
|
|
210
208
|
if (!envModelTableDeclared(env))
|
|
211
209
|
return { kind: "write", why: "env_table_empty" };
|
|
212
210
|
if (decision === "replaced")
|
|
@@ -226,7 +224,7 @@ const landingText = (after) => {
|
|
|
226
224
|
return after.kind === "dangles" ? "nothing (no enabled catalog entry)" : "an entry the engine selects ({select})";
|
|
227
225
|
};
|
|
228
226
|
/**
|
|
229
|
-
*
|
|
227
|
+
* 计划停下来的那一种结局(`confirm`)的**一句话**(三端同一句;只用回执里的 env 键名、目录名与模型 id —— 可披露的原样,秘密形的打成记号;凭据与别的 env 值从不进句子)。
|
|
230
228
|
* 写类结局不是拒 ⇒ `TypeError`。
|
|
231
229
|
*/
|
|
232
230
|
export function envModelTableWriteRefusal(plan) {
|
|
@@ -245,13 +243,7 @@ export function envModelTableWriteRefusal(plan) {
|
|
|
245
243
|
return (`writing this models document replaces the env model table on the local config leg: ${models}; after adoption ${roles}.${decision} ` +
|
|
246
244
|
`To let the catalog replace them, confirm — the writer records ${file} = {"envModels":"replaced"} in the config root first, then writes the models document; to keep the env models, do not write it.`);
|
|
247
245
|
}
|
|
248
|
-
case "fix_first": {
|
|
249
|
-
const row = plan.receipt.roles.find((x) => x.role === "summarize");
|
|
250
|
-
const ref = row === undefined ? "" : ` (env binding ${shown(row.before.ref)})`;
|
|
251
|
-
return (`this models document leaves the summarize role unresolved${ref}: it lands on no enabled catalog entry, so every task would be refused after adoption (${SUMMARIZE_MODEL_MISSING_CODE}). ` +
|
|
252
|
-
"Bind roles.summarize to an enabled entry of the document, or add an enabled entry under the name it points at, then plan the write again.");
|
|
253
|
-
}
|
|
254
246
|
default:
|
|
255
|
-
throw new TypeError(`envModelTableWriteRefusal: only a "confirm"
|
|
247
|
+
throw new TypeError(`envModelTableWriteRefusal: only a "confirm" plan is a refusal (got ${JSON.stringify(kind)})`);
|
|
256
248
|
}
|
|
257
249
|
}
|
|
@@ -5,13 +5,16 @@
|
|
|
5
5
|
* (必需坐标算不算「缺」的唯一判官 `isBlankCoordinate` 不在这里:它住在零依赖的叶模块 `./execution-lanes`,包根与 `./execution-lanes` 子路径是同一只。)
|
|
6
6
|
* 索引只从表派生,不另存任何词:表改了,这里跟着变。
|
|
7
7
|
*
|
|
8
|
-
* ## 迁移指路(7.0.1;11.0.0 起指路句住 `retired-form.ts
|
|
9
|
-
* 拒因要不要带迁移指路只有一条规则:**这个键「旧形 ⇒ 迁移能搬」就带,搬不了的保留名(`PORT` / `MODEL_*` …)
|
|
10
|
-
* 「迁移能搬」= 迁移自己识别的键集(车道键 {@link isLaneEnvKey} ∪ 店键 {@link STORE_ENV_KEYS})
|
|
11
|
-
* ({@link
|
|
8
|
+
* ## 迁移指路(7.0.1;11.0.0 起指路句住 `retired-form.ts`;16.0.0 起指路点名**哪一条**迁移)
|
|
9
|
+
* 拒因要不要带迁移指路只有一条规则:**这个键「旧形 ⇒ 迁移能搬」就带,搬不了的保留名(`PORT` / `MODEL_*` …)不带**;带的话点名搬它的那一条迁移。
|
|
10
|
+
* 「迁移能搬」= 迁移自己识别的键集(车道键 {@link isLaneEnvKey} ∪ 店键 {@link STORE_ENV_KEYS}),写闸读的是**同一只函数**
|
|
11
|
+
* ({@link envKeyMigration}),不是第二张手抄的表 —— 迁移多认一个键而写闸没跟,或反过来,不会静默分叉
|
|
12
12
|
* (`test/migration-signpost-11.0.0.test.ts` 用真跑迁移的读数逐个保留名对拍)。
|
|
13
|
+
* 哪一条:车道表登记 7.0.0 之后才进表的车道键({@link LATE_LANE_ENV_KEYS})在 7.0.0 起的存量行里可以**合法地**写在 `env` / `secretEnv` 逃生口里
|
|
14
|
+
* (那时它还不是保留名),它的旧形归登记它那一版的迁移;其余键的旧形只可能出自 6.x 行,归 SS-M4。
|
|
13
15
|
*/
|
|
14
16
|
import { type ExecutionLane, type ExecutionLaneRow } from "./execution-lanes.js";
|
|
17
|
+
import type { OneTimeMigrationId } from "./migration-receipt.js";
|
|
15
18
|
/** 一条车道的表行。词表与表行由 `execution-lanes.ts` 的 `satisfies Record<ExecutionLane, …>` 一一对应 ——
|
|
16
19
|
* 找不到 = 本包自身的不变量坏了(不是输入错),所以抛。 */
|
|
17
20
|
export declare function laneRowOf(lane: ExecutionLane): ExecutionLaneRow;
|
|
@@ -25,11 +28,20 @@ export interface LaneEnvOwner {
|
|
|
25
28
|
export declare const LANE_ENV_OWNER: ReadonlyMap<string, LaneEnvOwner>;
|
|
26
29
|
/** 一个车道 env 名在 WorkerSpec 里的**唯一写位**(保留键的拒因指路用它)。 */
|
|
27
30
|
export declare function laneEnvSeat(name: string, owner: LaneEnvOwner): string;
|
|
28
|
-
/** 车道块的两个特殊键(不在车道表里,但归车道块:迁移把 `REMOTE_EXEC` 搬成 `runtime.execution.
|
|
31
|
+
/** 车道块的两个特殊键(不在车道表里,但归车道块:迁移把 `REMOTE_EXEC` 搬成 `runtime.execution.lanes`、`REQUIRE_EXECUTION_LANE` 丢弃)。 */
|
|
29
32
|
export declare const LANE_WORD_KEYS: readonly ["REMOTE_EXEC", "REQUIRE_EXECUTION_LANE"];
|
|
30
33
|
/** 渲染器以前不渲、运维只能写在 env 逃生口里的店键(7.0.0 起归 `runtime.store`;迁移搬进 `store`)。 */
|
|
31
34
|
export declare const STORE_ENV_KEYS: readonly ["DB_BACKEND", "MEMORY_ENGINE", "MEMORY_ENGINE_BACKEND"];
|
|
32
35
|
/** 一个名字是不是车道块的键:车道词两键,或车道表里的任一车道 env 名。 */
|
|
33
36
|
export declare function isLaneEnvKey(name: string): boolean;
|
|
34
|
-
/**
|
|
35
|
-
export
|
|
37
|
+
/** 搬一个逃生口里的保留名的那一条一次性迁移,与它改写的存量形是哪一版之前存下的(指路句 `migrationSignpost(migration, before)` 的两个参数)。 */
|
|
38
|
+
export interface EnvKeyMigration {
|
|
39
|
+
readonly migration: OneTimeMigrationId;
|
|
40
|
+
readonly before: string;
|
|
41
|
+
}
|
|
42
|
+
/** 7.0.0 之后才登记进车道表的车道 env 名 → 改写它的迁移(只增不减;每加一个,在这里写一行)。16.0.0:`DEVICE_RELAY_URL`(device 行的必需坐标;
|
|
43
|
+
* 此前 device 臂就读它而表漏登记,7.0.0–15.x 的写闸把它当普通 env 收下)→ SS-M12。 */
|
|
44
|
+
export declare const LATE_LANE_ENV_KEYS: ReadonlyMap<string, EnvKeyMigration>;
|
|
45
|
+
/** 一个 env 名在逃生口里 ⇒ 哪一条迁移搬它(见文件头「迁移指路」):晚登记的车道键 ⇒ 登记它那一版的迁移;别的车道块键与店键 ⇒ SS-M4;其余 ⇒ `undefined`(没有迁移搬它)。
|
|
46
|
+
* 写闸拒因带不带指路、指哪一条只看它。 */
|
|
47
|
+
export declare function envKeyMigration(name: string): EnvKeyMigration | undefined;
|
|
@@ -5,11 +5,13 @@
|
|
|
5
5
|
* (必需坐标算不算「缺」的唯一判官 `isBlankCoordinate` 不在这里:它住在零依赖的叶模块 `./execution-lanes`,包根与 `./execution-lanes` 子路径是同一只。)
|
|
6
6
|
* 索引只从表派生,不另存任何词:表改了,这里跟着变。
|
|
7
7
|
*
|
|
8
|
-
* ## 迁移指路(7.0.1;11.0.0 起指路句住 `retired-form.ts
|
|
9
|
-
* 拒因要不要带迁移指路只有一条规则:**这个键「旧形 ⇒ 迁移能搬」就带,搬不了的保留名(`PORT` / `MODEL_*` …)
|
|
10
|
-
* 「迁移能搬」= 迁移自己识别的键集(车道键 {@link isLaneEnvKey} ∪ 店键 {@link STORE_ENV_KEYS})
|
|
11
|
-
* ({@link
|
|
8
|
+
* ## 迁移指路(7.0.1;11.0.0 起指路句住 `retired-form.ts`;16.0.0 起指路点名**哪一条**迁移)
|
|
9
|
+
* 拒因要不要带迁移指路只有一条规则:**这个键「旧形 ⇒ 迁移能搬」就带,搬不了的保留名(`PORT` / `MODEL_*` …)不带**;带的话点名搬它的那一条迁移。
|
|
10
|
+
* 「迁移能搬」= 迁移自己识别的键集(车道键 {@link isLaneEnvKey} ∪ 店键 {@link STORE_ENV_KEYS}),写闸读的是**同一只函数**
|
|
11
|
+
* ({@link envKeyMigration}),不是第二张手抄的表 —— 迁移多认一个键而写闸没跟,或反过来,不会静默分叉
|
|
12
12
|
* (`test/migration-signpost-11.0.0.test.ts` 用真跑迁移的读数逐个保留名对拍)。
|
|
13
|
+
* 哪一条:车道表登记 7.0.0 之后才进表的车道键({@link LATE_LANE_ENV_KEYS})在 7.0.0 起的存量行里可以**合法地**写在 `env` / `secretEnv` 逃生口里
|
|
14
|
+
* (那时它还不是保留名),它的旧形归登记它那一版的迁移;其余键的旧形只可能出自 6.x 行,归 SS-M4。
|
|
13
15
|
*/
|
|
14
16
|
import { EXECUTION_LANES } from "./execution-lanes.js";
|
|
15
17
|
/** 一条车道的表行。词表与表行由 `execution-lanes.ts` 的 `satisfies Record<ExecutionLane, …>` 一一对应 ——
|
|
@@ -29,7 +31,7 @@ export const LANE_ENV_OWNER = new Map(EXECUTION_LANES.flatMap((r) => {
|
|
|
29
31
|
export function laneEnvSeat(name, owner) {
|
|
30
32
|
return `runtime.execution.${owner.secret ? "secretRefs" : "coords"}.${name}`;
|
|
31
33
|
}
|
|
32
|
-
/** 车道块的两个特殊键(不在车道表里,但归车道块:迁移把 `REMOTE_EXEC` 搬成 `runtime.execution.
|
|
34
|
+
/** 车道块的两个特殊键(不在车道表里,但归车道块:迁移把 `REMOTE_EXEC` 搬成 `runtime.execution.lanes`、`REQUIRE_EXECUTION_LANE` 丢弃)。 */
|
|
33
35
|
export const LANE_WORD_KEYS = ["REMOTE_EXEC", "REQUIRE_EXECUTION_LANE"];
|
|
34
36
|
/** 渲染器以前不渲、运维只能写在 env 逃生口里的店键(7.0.0 起归 `runtime.store`;迁移搬进 `store`)。 */
|
|
35
37
|
export const STORE_ENV_KEYS = ["DB_BACKEND", "MEMORY_ENGINE", "MEMORY_ENGINE_BACKEND"];
|
|
@@ -37,7 +39,23 @@ export const STORE_ENV_KEYS = ["DB_BACKEND", "MEMORY_ENGINE", "MEMORY_ENGINE_BAC
|
|
|
37
39
|
export function isLaneEnvKey(name) {
|
|
38
40
|
return LANE_WORD_KEYS.some((k) => k === name) || LANE_ENV_OWNER.has(name);
|
|
39
41
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
42
|
+
const SIX_X_ENV = Object.freeze({ migration: "SS-M4", before: "7.0.0" });
|
|
43
|
+
/** 7.0.0 之后才登记进车道表的车道 env 名 → 改写它的迁移(只增不减;每加一个,在这里写一行)。16.0.0:`DEVICE_RELAY_URL`(device 行的必需坐标;
|
|
44
|
+
* 此前 device 臂就读它而表漏登记,7.0.0–15.x 的写闸把它当普通 env 收下)→ SS-M12。 */
|
|
45
|
+
export const LATE_LANE_ENV_KEYS = lateLaneEnvKeys([["DEVICE_RELAY_URL", Object.freeze({ migration: "SS-M12", before: "16.0.0" })]]);
|
|
46
|
+
/** 建 {@link LATE_LANE_ENV_KEYS}:每一行的名字必须在车道表里(载入时验,名字拼错 ⇒ 包载入即抛,不静默成一条永不命中的行)。 */
|
|
47
|
+
function lateLaneEnvKeys(rows) {
|
|
48
|
+
for (const [name] of rows) {
|
|
49
|
+
if (!LANE_ENV_OWNER.has(name))
|
|
50
|
+
throw new Error(`LATE_LANE_ENV_KEYS names "${name}", which is not in the execution lane table (package invariant broken)`);
|
|
51
|
+
}
|
|
52
|
+
return new Map(rows);
|
|
53
|
+
}
|
|
54
|
+
/** 一个 env 名在逃生口里 ⇒ 哪一条迁移搬它(见文件头「迁移指路」):晚登记的车道键 ⇒ 登记它那一版的迁移;别的车道块键与店键 ⇒ SS-M4;其余 ⇒ `undefined`(没有迁移搬它)。
|
|
55
|
+
* 写闸拒因带不带指路、指哪一条只看它。 */
|
|
56
|
+
export function envKeyMigration(name) {
|
|
57
|
+
const late = LATE_LANE_ENV_KEYS.get(name);
|
|
58
|
+
if (late !== undefined)
|
|
59
|
+
return late;
|
|
60
|
+
return isLaneEnvKey(name) || STORE_ENV_KEYS.some((k) => k === name) ? SIX_X_ENV : undefined;
|
|
43
61
|
}
|
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
* · `tenantSeparation`:同文件 `executionLaneCaps` 每臂的 `tenantSeparation` 位(:209 host / :230 e2b / :249 k8s /
|
|
18
18
|
* :269 ssh / :288 adb / :308 local-docker / :327 device)。`tenantSeparationBasis`(11.0.0):分租户的那几臂凭什么分 ——
|
|
19
19
|
* e2b / k8s / local-docker 每任务一只隔离目标(`isolation`),device 是发起者自己的设备(`principal-binding`)。
|
|
20
|
+
* · `singleReplica`(16.0.0):这条车道的执行目标挂在**一个副本**上 —— host 是副本自己的文件系统,device 是设备连上的那一个副本
|
|
21
|
+
* (设备的连接落在一个副本上,跨副本把会话送到它的设备今天没有)。表里有一条这种车道的 worker 只能一个副本(写闸按这一列判,不点名车道词)。
|
|
20
22
|
* · `requiredEnv ∪ requiredSecretEnv`:这条车道缺了就起不了服的键。最初逐字抄自 `src/config.ts` 的手写表 `REQUIRED_ENVS_BY_LANE`(:1054),
|
|
21
23
|
* 那张手写表已退役 —— 服务端的必需 env 门按本表行的这两栏拒启,部署面的车道清单读同一行,所以今天**本表就是**必需键的唯一来源。按 `src/config-catalog.ts`
|
|
22
24
|
* 该键那一行的值类型分栏 —— 类型 `secret` / `opaque`(目录自己「绝不回显值」的两类)进 `*SecretEnv`,其余进 `*Env`。
|
|
@@ -66,9 +68,11 @@ export type LaneTenancy = {
|
|
|
66
68
|
/** 一条车道的表行。四栏 env 名两两不交、跨车道也不交(`test/execution-lanes-7.0.0.test.ts` 钉)。
|
|
67
69
|
* - `requiredEnv` / `optionalEnv`:非密坐标 —— 写在 `runtime.execution.coords`(字面值);
|
|
68
70
|
* - `requiredSecretEnv` / `optionalSecretEnv`:密钥 —— 写在 `runtime.execution.secretRefs`(服务变量名 → 宿主 env-NAME,绝不写值);
|
|
69
|
-
* - 租户分离位 + 依据:见 {@link LaneTenancy}。
|
|
71
|
+
* - 租户分离位 + 依据:见 {@link LaneTenancy}。
|
|
72
|
+
* - `singleReplica`(16.0.0):执行目标挂在一个副本上 ⇒ 声明了这条车道的 worker 只能一个副本(见文件头)。 */
|
|
70
73
|
export type ExecutionLaneRow = {
|
|
71
74
|
readonly lane: ExecutionLane;
|
|
75
|
+
readonly singleReplica: boolean;
|
|
72
76
|
readonly requiredEnv: readonly string[];
|
|
73
77
|
readonly requiredSecretEnv: readonly string[];
|
|
74
78
|
readonly optionalEnv: readonly string[];
|
|
@@ -79,7 +83,7 @@ export declare const EXECUTION_LANES: readonly ExecutionLaneRow[];
|
|
|
79
83
|
/**
|
|
80
84
|
* 车道坐标的**唯一判官**:一个必需坐标「未设 / 空串 / 只含空白」一律算缺。本函数判其中的后两种,且只看**已经有的字符串值**:空串,或只含空白(`String.prototype.trim` 认的空白:空格 / 制表 / 换行 / 不换行空格 / 字节序标记 …)⇒ `true`;
|
|
81
85
|
* 「这一键根本没设」由调用方先判(写闸对没设的键另有一句 `requires coords.<键>`,两种缺法同样被拒)。
|
|
82
|
-
*
|
|
86
|
+
* 写闸对所有车道、所有必需坐标同判这一只;本地文件契约 `RemoteExecSpec`(`config.d/remote-exec.json`)的每个非空串字段(镜像 / 命名空间 / 主机 / 路径 / …)问的也是这一只(16.0.0 起:同一个部署声明在两处说,「空白」不再有两种读法);它只判「是不是空白」,不 trim 值、不管值的格式(格式不在这一条的范围:`" sandboxes "` 不是空白)。
|
|
83
87
|
* 它只答「这个值是不是空白」,不管这个键是必需还是可选:据它**拒不拒**一个坐标,由调用方按键的必需 / 可选决定 —— 写闸只对必需坐标据它拒;个别可选坐标上空串有语义(如 `K8S_RUNTIME_CLASS` 的空串 = 不用 Kata),可选坐标的空串照收。
|
|
84
88
|
*
|
|
85
89
|
* 住在这个零依赖的叶模块里:包根(`export *`)与 `./execution-lanes` 子路径是**同一个函数对象** —— 只想问这一个问题的消费方走子路径,不必冷载入整只契约包桶;
|
|
@@ -93,6 +97,7 @@ export type StoreBackend = (typeof STORE_BACKENDS)[number];
|
|
|
93
97
|
* (`tidb` 不在集内:引擎见之拒启)。 */
|
|
94
98
|
export declare const SESSION_BACKENDS: readonly ["memory", "mysql", "auto"];
|
|
95
99
|
export type SessionBackend = (typeof SESSION_BACKENDS)[number];
|
|
96
|
-
/** 记忆持久面方言(→ `MEMORY_ENGINE_BACKEND`)
|
|
97
|
-
|
|
100
|
+
/** 记忆持久面方言(→ `MEMORY_ENGINE_BACKEND`)。16.0.0:MySQL 族那一只叫 `mysql`(此前 `tidb` —— 同一只 SQL 店有店词 `mysql` 与方言 `tidb` 两个字;
|
|
101
|
+
* 收成一个字,与 {@link STORE_BACKENDS} 同字;硬破坏,`tidb` 读写两面拒、指路一次性迁移 SS-M12)。次序 file → pg → mysql。 */
|
|
102
|
+
export declare const MEMORY_ENGINE_BACKENDS: readonly ["file", "pg", "mysql"];
|
|
98
103
|
export type MemoryEngineBackend = (typeof MEMORY_ENGINE_BACKENDS)[number];
|
package/dist/execution-lanes.js
CHANGED
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
* · `tenantSeparation`:同文件 `executionLaneCaps` 每臂的 `tenantSeparation` 位(:209 host / :230 e2b / :249 k8s /
|
|
18
18
|
* :269 ssh / :288 adb / :308 local-docker / :327 device)。`tenantSeparationBasis`(11.0.0):分租户的那几臂凭什么分 ——
|
|
19
19
|
* e2b / k8s / local-docker 每任务一只隔离目标(`isolation`),device 是发起者自己的设备(`principal-binding`)。
|
|
20
|
+
* · `singleReplica`(16.0.0):这条车道的执行目标挂在**一个副本**上 —— host 是副本自己的文件系统,device 是设备连上的那一个副本
|
|
21
|
+
* (设备的连接落在一个副本上,跨副本把会话送到它的设备今天没有)。表里有一条这种车道的 worker 只能一个副本(写闸按这一列判,不点名车道词)。
|
|
20
22
|
* · `requiredEnv ∪ requiredSecretEnv`:这条车道缺了就起不了服的键。最初逐字抄自 `src/config.ts` 的手写表 `REQUIRED_ENVS_BY_LANE`(:1054),
|
|
21
23
|
* 那张手写表已退役 —— 服务端的必需 env 门按本表行的这两栏拒启,部署面的车道清单读同一行,所以今天**本表就是**必需键的唯一来源。按 `src/config-catalog.ts`
|
|
22
24
|
* 该键那一行的值类型分栏 —— 类型 `secret` / `opaque`(目录自己「绝不回显值」的两类)进 `*SecretEnv`,其余进 `*Env`。
|
|
@@ -56,6 +58,7 @@ const LANE_ROWS = {
|
|
|
56
58
|
/** server `config.ts` :4014 臂(无必需键);`execution-lane-caps.ts` :209 `single-user-only`(所有任务落 worker 宿主)。 */
|
|
57
59
|
host: {
|
|
58
60
|
tenantSeparation: "single-user-only",
|
|
61
|
+
singleReplica: true,
|
|
59
62
|
requiredEnv: [],
|
|
60
63
|
requiredSecretEnv: [],
|
|
61
64
|
optionalEnv: ["HOST_WORKSPACE_BASE", "HOST_COMMAND_TIMEOUT_MS"],
|
|
@@ -66,6 +69,7 @@ const LANE_ROWS = {
|
|
|
66
69
|
e2b: {
|
|
67
70
|
tenantSeparation: "supported",
|
|
68
71
|
tenantSeparationBasis: "isolation",
|
|
72
|
+
singleReplica: false,
|
|
69
73
|
requiredEnv: [],
|
|
70
74
|
requiredSecretEnv: ["E2B_API_KEY"],
|
|
71
75
|
optionalEnv: ["E2B_TEMPLATE", "E2B_TIMEOUT_MS", "E2B_LIVENESS_MS", "E2B_ALLOW_NET", "E2B_MOUNT_PATH", "E2B_ORPHAN_RECLAIM"],
|
|
@@ -78,6 +82,7 @@ const LANE_ROWS = {
|
|
|
78
82
|
k8s: {
|
|
79
83
|
tenantSeparation: "supported",
|
|
80
84
|
tenantSeparationBasis: "isolation",
|
|
85
|
+
singleReplica: false,
|
|
81
86
|
requiredEnv: ["K8S_SANDBOX_IMAGE", "K8S_NAMESPACE"],
|
|
82
87
|
requiredSecretEnv: [],
|
|
83
88
|
optionalEnv: [
|
|
@@ -98,6 +103,7 @@ const LANE_ROWS = {
|
|
|
98
103
|
* `execution-lane-caps.ts` :269 `single-user-only`(一台跨任务共享的常驻真机)。 */
|
|
99
104
|
ssh: {
|
|
100
105
|
tenantSeparation: "single-user-only",
|
|
106
|
+
singleReplica: false,
|
|
101
107
|
requiredEnv: ["SSH_HOST", "SSH_USER"],
|
|
102
108
|
requiredSecretEnv: ["SSH_PRIVATE_KEY"],
|
|
103
109
|
optionalEnv: ["SSH_PORT", "SSH_MOUNT_PATH", "SSH_HOST_FINGERPRINT", "SSH_KNOWN_HOSTS"],
|
|
@@ -106,6 +112,7 @@ const LANE_ROWS = {
|
|
|
106
112
|
/** server `config.ts` :4006 臂;`execution-lane-caps.ts` :288 `single-user-only`(一台跨任务共享的设备)。 */
|
|
107
113
|
adb: {
|
|
108
114
|
tenantSeparation: "single-user-only",
|
|
115
|
+
singleReplica: false,
|
|
109
116
|
requiredEnv: ["ADB_SERIAL"],
|
|
110
117
|
requiredSecretEnv: [],
|
|
111
118
|
optionalEnv: ["ADB_PATH", "ADB_MOUNT_PATH"],
|
|
@@ -116,6 +123,7 @@ const LANE_ROWS = {
|
|
|
116
123
|
"local-docker": {
|
|
117
124
|
tenantSeparation: "supported",
|
|
118
125
|
tenantSeparationBasis: "isolation",
|
|
126
|
+
singleReplica: false,
|
|
119
127
|
requiredEnv: ["DOCKER_IMAGE"],
|
|
120
128
|
requiredSecretEnv: [],
|
|
121
129
|
optionalEnv: [
|
|
@@ -131,12 +139,14 @@ const LANE_ROWS = {
|
|
|
131
139
|
],
|
|
132
140
|
optionalSecretEnv: ["DOCKER_SANDBOX_ENV"],
|
|
133
141
|
},
|
|
134
|
-
/** server `config.ts` :4055 臂 + `parseDevicePreAuthPerRemoteEnv` :871 / `parseDeviceEnrollTokenTtlEnv` :857
|
|
135
|
-
*
|
|
142
|
+
/** server `config.ts` :4055 臂 + `parseDevicePreAuthPerRemoteEnv` :871 / `parseDeviceEnrollTokenTtlEnv` :857;`execution-lane-caps.ts` :327
|
|
143
|
+
* `supported`(发起者自己的设备)。16.0.0:`DEVICE_RELAY_URL`(设备拨哪里)是**必需**坐标 —— 引擎的 device 臂缺它即拒启
|
|
144
|
+
* (`parseDeviceRelayUrlEnv`;此前它是 device 臂读而本表漏登记的键),值只有两种:ws(s) 绝对 URL 或字面 `none`(值的文法见 `./device-relay`)。 */
|
|
136
145
|
device: {
|
|
137
146
|
tenantSeparation: "supported",
|
|
138
147
|
tenantSeparationBasis: "principal-binding",
|
|
139
|
-
|
|
148
|
+
singleReplica: true,
|
|
149
|
+
requiredEnv: ["DEVICE_RELAY_URL"],
|
|
140
150
|
requiredSecretEnv: [],
|
|
141
151
|
optionalEnv: [
|
|
142
152
|
"DEVICE_DELIVERY_TIMEOUT_MS",
|
|
@@ -157,6 +167,7 @@ export const EXECUTION_LANES = Object.freeze(EXECUTION_LANE_WORDS.map((lane) =>
|
|
|
157
167
|
return Object.freeze({
|
|
158
168
|
lane,
|
|
159
169
|
...tenancy,
|
|
170
|
+
singleReplica: row.singleReplica,
|
|
160
171
|
requiredEnv: Object.freeze([...row.requiredEnv]),
|
|
161
172
|
requiredSecretEnv: Object.freeze([...row.requiredSecretEnv]),
|
|
162
173
|
optionalEnv: Object.freeze([...row.optionalEnv]),
|
|
@@ -166,7 +177,7 @@ export const EXECUTION_LANES = Object.freeze(EXECUTION_LANE_WORDS.map((lane) =>
|
|
|
166
177
|
/**
|
|
167
178
|
* 车道坐标的**唯一判官**:一个必需坐标「未设 / 空串 / 只含空白」一律算缺。本函数判其中的后两种,且只看**已经有的字符串值**:空串,或只含空白(`String.prototype.trim` 认的空白:空格 / 制表 / 换行 / 不换行空格 / 字节序标记 …)⇒ `true`;
|
|
168
179
|
* 「这一键根本没设」由调用方先判(写闸对没设的键另有一句 `requires coords.<键>`,两种缺法同样被拒)。
|
|
169
|
-
*
|
|
180
|
+
* 写闸对所有车道、所有必需坐标同判这一只;本地文件契约 `RemoteExecSpec`(`config.d/remote-exec.json`)的每个非空串字段(镜像 / 命名空间 / 主机 / 路径 / …)问的也是这一只(16.0.0 起:同一个部署声明在两处说,「空白」不再有两种读法);它只判「是不是空白」,不 trim 值、不管值的格式(格式不在这一条的范围:`" sandboxes "` 不是空白)。
|
|
170
181
|
* 它只答「这个值是不是空白」,不管这个键是必需还是可选:据它**拒不拒**一个坐标,由调用方按键的必需 / 可选决定 —— 写闸只对必需坐标据它拒;个别可选坐标上空串有语义(如 `K8S_RUNTIME_CLASS` 的空串 = 不用 Kata),可选坐标的空串照收。
|
|
171
182
|
*
|
|
172
183
|
* 住在这个零依赖的叶模块里:包根(`export *`)与 `./execution-lanes` 子路径是**同一个函数对象** —— 只想问这一个问题的消费方走子路径,不必冷载入整只契约包桶;
|
|
@@ -180,5 +191,6 @@ export const STORE_BACKENDS = ["mysql", "pg", "local", "memory"];
|
|
|
180
191
|
/** session 店(→ `SESSION_BACKEND`)。= server `src/config.ts` :1760 `enumEnv("SESSION_BACKEND", …)` 的词表逐字同序
|
|
181
192
|
* (`tidb` 不在集内:引擎见之拒启)。 */
|
|
182
193
|
export const SESSION_BACKENDS = ["memory", "mysql", "auto"];
|
|
183
|
-
/** 记忆持久面方言(→ `MEMORY_ENGINE_BACKEND`)
|
|
184
|
-
|
|
194
|
+
/** 记忆持久面方言(→ `MEMORY_ENGINE_BACKEND`)。16.0.0:MySQL 族那一只叫 `mysql`(此前 `tidb` —— 同一只 SQL 店有店词 `mysql` 与方言 `tidb` 两个字;
|
|
195
|
+
* 收成一个字,与 {@link STORE_BACKENDS} 同字;硬破坏,`tidb` 读写两面拒、指路一次性迁移 SS-M12)。次序 file → pg → mysql。 */
|
|
196
|
+
export const MEMORY_ENGINE_BACKENDS = ["file", "pg", "mysql"];
|
package/dist/file-store.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type EffectiveReadWarning } from "./config-fns.js";
|
|
2
|
+
import type { ConfigDocumentRefusal } from "./config-document-refusal.js";
|
|
2
3
|
import type { ConfigReader } from "./reader.js";
|
|
3
4
|
import { type DomainConfig, type DomainName, type EffectiveConfig, type RetiredDomainName } from "./types.js";
|
|
4
5
|
export { NON_PORTABLE_DOMAIN_REASONS, PORTABLE_DOMAINS, type PortableDomain } from "./portable-domains.js";
|
|
@@ -21,6 +22,23 @@ export type TolerantEffective = {
|
|
|
21
22
|
effective: EffectiveConfig;
|
|
22
23
|
domainErrors: DomainReadError[];
|
|
23
24
|
};
|
|
25
|
+
/** 单读口 {@link FileConfigStore.read} 的答案(按 `kind` 判别的闭集两臂;消费方 `switch` 穷举)。两臂都带 `warnings`(读侧告警交成数据,不经 `onWarning`)。 */
|
|
26
|
+
export type FileConfigRead =
|
|
27
|
+
/** 每份文档都读得进:`effective` 与严读 `getEffective()` 逐位同(同一份快照、同一个版本);`warnings` = 没人读的文件 + 装配时的读侧告警。 */
|
|
28
|
+
{
|
|
29
|
+
readonly kind: "effective";
|
|
30
|
+
readonly effective: EffectiveConfig;
|
|
31
|
+
readonly warnings: readonly EffectiveReadWarning[];
|
|
32
|
+
}
|
|
33
|
+
/** 至少一份文档在文件层被拒:`refusals` = 整棵目录的拒单(次序确定:retired 域文档在前,按墓碑表的声明次序;便携域按 `PORTABLE_DOMAINS` 的次序);
|
|
34
|
+
* `version` = 这一次读到的整棵目录的内容版本(被拒的文档以坏文档的指纹参与 —— 与宽容读的坏文件哨兵同一只:同样的坏内容同一个数、坏内容变了换数、
|
|
35
|
+
* 绝不与「文件缺席」同数);`warnings` = 没人读的文件(装配没有发生)。 */
|
|
36
|
+
| {
|
|
37
|
+
readonly kind: "refused";
|
|
38
|
+
readonly refusals: readonly ConfigDocumentRefusal[];
|
|
39
|
+
readonly version: number;
|
|
40
|
+
readonly warnings: readonly EffectiveReadWarning[];
|
|
41
|
+
};
|
|
24
42
|
export declare class FileConfigStore implements ConfigReader {
|
|
25
43
|
#private;
|
|
26
44
|
private readonly root;
|
|
@@ -37,9 +55,10 @@ export declare class FileConfigStore implements ConfigReader {
|
|
|
37
55
|
domainPath(domain: DomainName | RetiredDomainName): string;
|
|
38
56
|
/** Read+parse one domain (validated, defaults filled), or `undefined` if the domain is ABSENT — matching
|
|
39
57
|
* the {@link ConfigReader} contract (reader.ts: absent domain → undefined) and the server stores'
|
|
40
|
-
* `has(domain) ? parse : undefined`.
|
|
41
|
-
* with defaults
|
|
42
|
-
*
|
|
58
|
+
* `has(domain) ? parse : undefined`. Only a MISSING file (ENOENT) is absence → undefined. A present `{}` IS a set
|
|
59
|
+
* domain → parsed with defaults. A present file that is empty or whitespace-only (a half-write) throws as a corrupt file,
|
|
60
|
+
* as do bad JSON, a non-object, an unreadable file and one nested too deep (the same reading as getEffective / readBundle).
|
|
61
|
+
* (getEffective still fills defaults for absent domains; getDomain is the per-domain "was this ever set?" probe.) */
|
|
43
62
|
getDomain<K extends DomainName>(domain: K): Promise<DomainConfig[K] | undefined>;
|
|
44
63
|
/** All portable domains → EffectiveConfig. The PAYLOAD and its `version` come from ONE raws snapshot (no
|
|
45
64
|
* re-read between them — else a concurrent atomic rename could pair payload A with version B and a consumer
|
|
@@ -64,4 +83,12 @@ export declare class FileConfigStore implements ConfigReader {
|
|
|
64
83
|
/** Stable version number for the current files: same files ⇒ same number ⇒ a consumer's "skip re-apply if
|
|
65
84
|
* version unchanged" works locally too. */
|
|
66
85
|
getVersion(): Promise<number>;
|
|
86
|
+
/**
|
|
87
|
+
* **单读口**(16.0.0):一次读整棵 `config.d`(全部便携域 + retired 域的墓碑文档,每份只读一次),交一个结局({@link FileConfigRead})——
|
|
88
|
+
* · 每份文档在文件层都读得进 ⇒ `effective`:与严读 `getEffective()` 逐位同;读侧告警交成数据(没人读的文件 + 装配告警,不经 `onWarning`);
|
|
89
|
+
* · 有文档在文件层被拒(空 / 坏 JSON / 不是对象 / 读不出 / 过深;带事实的 retired 域文档是墓碑拒,带迁移编号)⇒ `refused`:整棵目录的拒单与这一次读到的内容版本。
|
|
90
|
+
* 严读与 `readBundle` 读的是同一只文件读法(`./config-d-file` 的读数)—— 同一种坏形同一个词、同一句话;区别只在渲染:严读遇到第一份坏文档就抛,本方法把整个目录读完、逐份交出。
|
|
91
|
+
* 文件层之外的拒(读得出、过不了门控域的 schema)照严读抛 —— 那是判形,不在拒单里。拒单、版本、effective、告警都出自这一次读到的字节:没有「先抛、再重探」的第二次读。
|
|
92
|
+
*/
|
|
93
|
+
read(): Promise<FileConfigRead>;
|
|
67
94
|
}
|
package/dist/file-store.js
CHANGED
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
* - A MISSING file (ENOENT) → the domain is absent → schema defaults fill in (parseDomain tolerates undefined).
|
|
15
15
|
* A present-but-EMPTY/whitespace file is a half-write → THROW (fail-closed, like remote-exec); to mean
|
|
16
16
|
* "absent/defaults" the file must be missing, not empty. A valid `{}` object IS a set domain (→ defaults).
|
|
17
|
+
* - `read()` (16.0.0) is the ONE read that answers both ways: the effective config + its read warnings as data when every
|
|
18
|
+
* document reads, or the typed refusal list of the whole directory + its version when one does not — every document read once.
|
|
17
19
|
* - Non-portable domains are OMITTED on a single machine and resolve to their schema defaults (`{workers:[]}`
|
|
18
20
|
* etc.) — no stubbing needed. 逐域为什么被排除见 {@link NON_PORTABLE_DOMAIN_REASONS}(禁裸排除)。
|
|
19
21
|
* - A RETIRED domain's file (`config.d/runtime.json`, see `RETIRED_DOMAINS`) is read only for its tombstone: carrying facts ⇒
|
|
@@ -26,7 +28,7 @@
|
|
|
26
28
|
*/
|
|
27
29
|
import { readdir, stat } from "node:fs/promises";
|
|
28
30
|
import path from "node:path";
|
|
29
|
-
import { buildEffective, DOMAIN_READ_FALLBACK, parseDomainLoud } from "./config-fns.js";
|
|
31
|
+
import { RetiredDomainError, buildEffective, DOMAIN_READ_FALLBACK, parseDomainLoud, refuseRetiredDomain } from "./config-fns.js";
|
|
30
32
|
import { RETIRED_DOMAIN_NAMES, configDPath, configDVersionOf, corruptDocumentRaw, readConfigDFile, readConfigDFileReading, refuseRetiredDomainFiles } from "./config-d-file.js";
|
|
31
33
|
import { PORTABLE_DOMAINS } from "./portable-domains.js";
|
|
32
34
|
import { REMOTE_EXEC_DOCUMENT } from "./remote-exec.js";
|
|
@@ -84,6 +86,11 @@ export class FileConfigStore {
|
|
|
84
86
|
const onWarning = this.opts.onWarning;
|
|
85
87
|
if (!onWarning)
|
|
86
88
|
return;
|
|
89
|
+
for (const w of await this.#unreadFiles())
|
|
90
|
+
onWarning(w);
|
|
91
|
+
}
|
|
92
|
+
/** {@link #warnUnreadFiles} 的扫描本体:没人读的 `*.json` 逐个一条告警(单读口 `read()` 把它们交成数据,所以它总扫)。 */
|
|
93
|
+
async #unreadFiles() {
|
|
87
94
|
let names;
|
|
88
95
|
try {
|
|
89
96
|
names = await readdir(path.join(this.root, "config.d"));
|
|
@@ -91,8 +98,9 @@ export class FileConfigStore {
|
|
|
91
98
|
catch {
|
|
92
99
|
// 目录读不了(不存在 / 只有 x 没有 r 的目录权限)⇒ 本次扫不了,**不抛**:这条是尽力而为的审计线,
|
|
93
100
|
// 绝不能把一次本来能成功的 getEffective 拖成失败(域文件本身仍按名逐个打开,该失败的自然会失败)。
|
|
94
|
-
return;
|
|
101
|
+
return [];
|
|
95
102
|
}
|
|
103
|
+
const out = [];
|
|
96
104
|
for (const name of names) {
|
|
97
105
|
if (name.startsWith(".") || !name.endsWith(".json") || NON_DOMAIN_CONFIG_FILES.has(name))
|
|
98
106
|
continue;
|
|
@@ -101,10 +109,11 @@ export class FileConfigStore {
|
|
|
101
109
|
continue; // 真被读的域
|
|
102
110
|
if (isRetiredDomainName(base))
|
|
103
111
|
continue; // retired 域:由墓碑判(带事实 ⇒ getEffective 抛),不是「没人读的文件」
|
|
104
|
-
|
|
112
|
+
out.push(isDomainName(base)
|
|
105
113
|
? { kind: "unread-config-file", file: name, domain: base, why: "non-portable-domain" }
|
|
106
114
|
: { kind: "unread-config-file", file: name, why: "not-a-domain" });
|
|
107
115
|
}
|
|
116
|
+
return out;
|
|
108
117
|
}
|
|
109
118
|
/** Raw (unparsed) values for every PORTABLE domain and every retired domain (the latter for the tombstone), keyed by
|
|
110
119
|
* name (absent → undefined). Both feed the version hash: a retired file appearing / changing flips `getEffective`
|
|
@@ -133,21 +142,22 @@ export class FileConfigStore {
|
|
|
133
142
|
}
|
|
134
143
|
/** Read+parse one domain (validated, defaults filled), or `undefined` if the domain is ABSENT — matching
|
|
135
144
|
* the {@link ConfigReader} contract (reader.ts: absent domain → undefined) and the server stores'
|
|
136
|
-
* `has(domain) ? parse : undefined`.
|
|
137
|
-
* with defaults
|
|
138
|
-
*
|
|
145
|
+
* `has(domain) ? parse : undefined`. Only a MISSING file (ENOENT) is absence → undefined. A present `{}` IS a set
|
|
146
|
+
* domain → parsed with defaults. A present file that is empty or whitespace-only (a half-write) throws as a corrupt file,
|
|
147
|
+
* as do bad JSON, a non-object, an unreadable file and one nested too deep (the same reading as getEffective / readBundle).
|
|
148
|
+
* (getEffective still fills defaults for absent domains; getDomain is the per-domain "was this ever set?" probe.) */
|
|
139
149
|
async getDomain(domain) {
|
|
140
150
|
await refuseRetiredDomainFiles(this.root); // every read entry asks the tombstone first (exportBundle reads through here)
|
|
141
151
|
const raw = await this.#readDomainRaw(domain);
|
|
142
152
|
if (raw === undefined)
|
|
143
|
-
return undefined; // missing
|
|
153
|
+
return undefined; // missing file (ENOENT) → domain absent (not schema defaults); a corrupt file threw above
|
|
144
154
|
// 0.19.0(对抗复审 复审 round2 F3):与 buildEffective **同一条** parse-and-warn 判据。此前本方法直接
|
|
145
155
|
// parseDomain,于是 `exportBundle`(它就是逐域走 getDomain 读的)会把一份携带未知键的域**静默剥**
|
|
146
156
|
// 成一份"干净"的 bundle —— 再 import 回去就是一次持久的配置丢失,全程零告警。
|
|
147
157
|
return parseDomainLoud(domain, raw, this.opts.onWarning);
|
|
148
158
|
}
|
|
149
159
|
/** Content hash of a raws snapshot → a stable u53-ish version number (≤ Number.MAX_SAFE_INTEGER). The one reading is `configDVersionOf`
|
|
150
|
-
* (`./config-d-file`) — `
|
|
160
|
+
* (`./config-d-file`) — `read()` asks the same function, so a clean tree has the same version from either. */
|
|
151
161
|
#versionOf(raws) {
|
|
152
162
|
return configDVersionOf(raws);
|
|
153
163
|
}
|
|
@@ -191,4 +201,60 @@ export class FileConfigStore {
|
|
|
191
201
|
async getVersion() {
|
|
192
202
|
return this.#versionOf(await this.#readAllRaw());
|
|
193
203
|
}
|
|
204
|
+
/**
|
|
205
|
+
* **单读口**(16.0.0):一次读整棵 `config.d`(全部便携域 + retired 域的墓碑文档,每份只读一次),交一个结局({@link FileConfigRead})——
|
|
206
|
+
* · 每份文档在文件层都读得进 ⇒ `effective`:与严读 `getEffective()` 逐位同;读侧告警交成数据(没人读的文件 + 装配告警,不经 `onWarning`);
|
|
207
|
+
* · 有文档在文件层被拒(空 / 坏 JSON / 不是对象 / 读不出 / 过深;带事实的 retired 域文档是墓碑拒,带迁移编号)⇒ `refused`:整棵目录的拒单与这一次读到的内容版本。
|
|
208
|
+
* 严读与 `readBundle` 读的是同一只文件读法(`./config-d-file` 的读数)—— 同一种坏形同一个词、同一句话;区别只在渲染:严读遇到第一份坏文档就抛,本方法把整个目录读完、逐份交出。
|
|
209
|
+
* 文件层之外的拒(读得出、过不了门控域的 schema)照严读抛 —— 那是判形,不在拒单里。拒单、版本、effective、告警都出自这一次读到的字节:没有「先抛、再重探」的第二次读。
|
|
210
|
+
*/
|
|
211
|
+
async read() {
|
|
212
|
+
const documents = [...RETIRED_DOMAIN_NAMES, ...PORTABLE_DOMAINS];
|
|
213
|
+
const [readings, unread] = await Promise.all([Promise.all(documents.map((d) => readConfigDFileReading(this.root, d))), this.#unreadFiles()]);
|
|
214
|
+
const raws = {};
|
|
215
|
+
const refusals = [];
|
|
216
|
+
documents.forEach((document, i) => {
|
|
217
|
+
const reading = readings[i];
|
|
218
|
+
switch (reading.status) {
|
|
219
|
+
case "absent":
|
|
220
|
+
raws[document] = undefined;
|
|
221
|
+
return;
|
|
222
|
+
case "refused":
|
|
223
|
+
raws[document] = corruptDocumentRaw(reading);
|
|
224
|
+
refusals.push({ document, reason: reading.reason, error: reading.error });
|
|
225
|
+
return;
|
|
226
|
+
case "present":
|
|
227
|
+
raws[document] = reading.value;
|
|
228
|
+
if (isRetiredDomainName(document)) {
|
|
229
|
+
const tombstone = tombstoneOf(document, reading.value);
|
|
230
|
+
if (tombstone !== undefined)
|
|
231
|
+
refusals.push(tombstone);
|
|
232
|
+
}
|
|
233
|
+
return;
|
|
234
|
+
default: {
|
|
235
|
+
const unhandled = reading;
|
|
236
|
+
throw new Error(`FileConfigStore.read: unhandled reading ${String(unhandled)}`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
const version = this.#versionOf(raws);
|
|
241
|
+
if (refusals.length > 0)
|
|
242
|
+
return { kind: "refused", refusals, version, warnings: unread };
|
|
243
|
+
const warnings = [...unread];
|
|
244
|
+
const updatedAt = await this.#maxMtimeIso();
|
|
245
|
+
const effective = buildEffective(version, (d) => raws[d], updatedAt, (w) => warnings.push(w));
|
|
246
|
+
return { kind: "effective", effective, warnings };
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
/** 墓碑判一次(与严读同一只判官 `refuseRetiredDomain`、同一句):retired 域文档带事实 ⇒ 一条 `retired_domain` 拒;缺席 / `{}` ⇒ `undefined`。 */
|
|
250
|
+
function tombstoneOf(domain, raw) {
|
|
251
|
+
try {
|
|
252
|
+
refuseRetiredDomain(domain, raw);
|
|
253
|
+
return undefined;
|
|
254
|
+
}
|
|
255
|
+
catch (e) {
|
|
256
|
+
if (e instanceof RetiredDomainError)
|
|
257
|
+
return { document: e.domain, reason: "retired_domain", error: e.message, migration: e.migration };
|
|
258
|
+
throw e;
|
|
259
|
+
}
|
|
194
260
|
}
|
package/dist/fleet.d.ts
CHANGED
|
@@ -166,12 +166,13 @@ export type RestartSignal = z.infer<typeof RestartSignal>;
|
|
|
166
166
|
* `config.gateway_fallback_without_primary` — fallback gateways are declared without a primary;
|
|
167
167
|
* `config.catalog_replaces_env_models` ({@link CATALOG_REPLACES_ENV_MODELS_CODE}) — on the LOCAL config leg, a candidate that
|
|
168
168
|
* manages the model plane would replace a declared env model table (`MODEL_ID` …) while the config root's decision document
|
|
169
|
-
* (`config.env-models.json`, read by `readEnvModelsDecision`) does not say `replaced` — the env models keep serving
|
|
170
|
-
* `config.summarize_model_missing` ({@link SUMMARIZE_MODEL_MISSING_CODE}) — the summarize role, which every task resolves,
|
|
171
|
-
* would land on no servable entry (every task would be refused after adoption).
|
|
169
|
+
* (`config.env-models.json`, read by `readEnvModelsDecision`) does not say `replaced` — the env models keep serving.
|
|
172
170
|
* Source (engine): the first two in `src/model-entry-refusal.ts`, the next two in `src/model-route-endpoint.ts:40,42`
|
|
173
171
|
* (invariant `model-route-endpoint`, judged by the model-plane stage in `src/config-center/apply-effective.ts:664-665`); the
|
|
174
|
-
* last
|
|
172
|
+
* last one is named HERE first (the code is a contract word: this package names it and exports the constant for the engine to read; the engine does not mint it today).
|
|
173
|
+
* 16.0.0 retired `config.summarize_model_missing` (and its constant) from this set: a role seat naming an entry the catalog does not serve
|
|
174
|
+
* reads as unset (it falls back down the role chain and the engine says so once) — no reading refuses a model plane for it, so it was never
|
|
175
|
+
* an engine group refusal. A reader still holding it gets it dropped by {@link readModelRefusals} like any code outside the set.
|
|
175
176
|
*/
|
|
176
177
|
export declare const MODEL_ENTRY_REFUSAL_CODES: readonly ["config.model_no_brain", "config.model_entry_invalid"];
|
|
177
178
|
export declare const ModelEntryRefusalCode: z.ZodEnum<{
|
|
@@ -181,12 +182,9 @@ export declare const ModelEntryRefusalCode: z.ZodEnum<{
|
|
|
181
182
|
export type ModelEntryRefusalCode = z.infer<typeof ModelEntryRefusalCode>;
|
|
182
183
|
/** The env-model-table confirmation gate's group-refusal code (see {@link MODEL_PLANE_REFUSAL_CODES}). */
|
|
183
184
|
export declare const CATALOG_REPLACES_ENV_MODELS_CODE: "config.catalog_replaces_env_models";
|
|
184
|
-
|
|
185
|
-
export declare const SUMMARIZE_MODEL_MISSING_CODE: "config.summarize_model_missing";
|
|
186
|
-
export declare const MODEL_PLANE_REFUSAL_CODES: readonly ["config.default_model_missing", "config.model_refused_name_minted", "config.model_route_endpoint_missing", "config.gateway_fallback_without_primary", "config.catalog_replaces_env_models", "config.summarize_model_missing"];
|
|
185
|
+
export declare const MODEL_PLANE_REFUSAL_CODES: readonly ["config.default_model_missing", "config.model_refused_name_minted", "config.model_route_endpoint_missing", "config.gateway_fallback_without_primary", "config.catalog_replaces_env_models"];
|
|
187
186
|
export declare const ModelPlaneRefusalCode: z.ZodEnum<{
|
|
188
187
|
"config.catalog_replaces_env_models": "config.catalog_replaces_env_models";
|
|
189
|
-
"config.summarize_model_missing": "config.summarize_model_missing";
|
|
190
188
|
"config.default_model_missing": "config.default_model_missing";
|
|
191
189
|
"config.model_refused_name_minted": "config.model_refused_name_minted";
|
|
192
190
|
"config.model_route_endpoint_missing": "config.model_route_endpoint_missing";
|
package/dist/fleet.js
CHANGED
|
@@ -234,26 +234,24 @@ function readSignalBody(body, pick, schema) {
|
|
|
234
234
|
* `config.gateway_fallback_without_primary` — fallback gateways are declared without a primary;
|
|
235
235
|
* `config.catalog_replaces_env_models` ({@link CATALOG_REPLACES_ENV_MODELS_CODE}) — on the LOCAL config leg, a candidate that
|
|
236
236
|
* manages the model plane would replace a declared env model table (`MODEL_ID` …) while the config root's decision document
|
|
237
|
-
* (`config.env-models.json`, read by `readEnvModelsDecision`) does not say `replaced` — the env models keep serving
|
|
238
|
-
* `config.summarize_model_missing` ({@link SUMMARIZE_MODEL_MISSING_CODE}) — the summarize role, which every task resolves,
|
|
239
|
-
* would land on no servable entry (every task would be refused after adoption).
|
|
237
|
+
* (`config.env-models.json`, read by `readEnvModelsDecision`) does not say `replaced` — the env models keep serving.
|
|
240
238
|
* Source (engine): the first two in `src/model-entry-refusal.ts`, the next two in `src/model-route-endpoint.ts:40,42`
|
|
241
239
|
* (invariant `model-route-endpoint`, judged by the model-plane stage in `src/config-center/apply-effective.ts:664-665`); the
|
|
242
|
-
* last
|
|
240
|
+
* last one is named HERE first (the code is a contract word: this package names it and exports the constant for the engine to read; the engine does not mint it today).
|
|
241
|
+
* 16.0.0 retired `config.summarize_model_missing` (and its constant) from this set: a role seat naming an entry the catalog does not serve
|
|
242
|
+
* reads as unset (it falls back down the role chain and the engine says so once) — no reading refuses a model plane for it, so it was never
|
|
243
|
+
* an engine group refusal. A reader still holding it gets it dropped by {@link readModelRefusals} like any code outside the set.
|
|
243
244
|
*/
|
|
244
245
|
export const MODEL_ENTRY_REFUSAL_CODES = ["config.model_no_brain", "config.model_entry_invalid"];
|
|
245
246
|
export const ModelEntryRefusalCode = z.enum(MODEL_ENTRY_REFUSAL_CODES);
|
|
246
247
|
/** The env-model-table confirmation gate's group-refusal code (see {@link MODEL_PLANE_REFUSAL_CODES}). */
|
|
247
248
|
export const CATALOG_REPLACES_ENV_MODELS_CODE = "config.catalog_replaces_env_models";
|
|
248
|
-
/** The summarize invariant's group-refusal code (see {@link MODEL_PLANE_REFUSAL_CODES}); a writer's plan answers `fix_first` with it. */
|
|
249
|
-
export const SUMMARIZE_MODEL_MISSING_CODE = "config.summarize_model_missing";
|
|
250
249
|
export const MODEL_PLANE_REFUSAL_CODES = [
|
|
251
250
|
"config.default_model_missing",
|
|
252
251
|
"config.model_refused_name_minted",
|
|
253
252
|
"config.model_route_endpoint_missing",
|
|
254
253
|
"config.gateway_fallback_without_primary",
|
|
255
254
|
CATALOG_REPLACES_ENV_MODELS_CODE,
|
|
256
|
-
SUMMARIZE_MODEL_MISSING_CODE,
|
|
257
255
|
];
|
|
258
256
|
export const ModelPlaneRefusalCode = z.enum(MODEL_PLANE_REFUSAL_CODES);
|
|
259
257
|
/**
|
package/dist/hooks.d.ts
CHANGED
|
@@ -78,8 +78,8 @@ export declare const HookEventName: z.ZodEnum<{
|
|
|
78
78
|
}>;
|
|
79
79
|
export type HookEventName = z.infer<typeof HookEventName>;
|
|
80
80
|
export declare const HookOwner: z.ZodEnum<{
|
|
81
|
-
engine: "engine";
|
|
82
81
|
shell: "shell";
|
|
82
|
+
engine: "engine";
|
|
83
83
|
}>;
|
|
84
84
|
export type HookOwner = z.infer<typeof HookOwner>;
|
|
85
85
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ export * from "./types.js";
|
|
|
6
6
|
export * from "./execution-lanes.js";
|
|
7
7
|
export * from "./config-document-refusal.js";
|
|
8
8
|
export * from "./url-shape.js";
|
|
9
|
+
export * from "./device-relay.js";
|
|
9
10
|
export * from "./url-credential.js";
|
|
10
11
|
export * from "./default-model.js";
|
|
11
12
|
export { catalogManagesModelPlane, enabledEntryCount } from "./catalog-manages.js";
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
export * from "./types.js";
|
|
6
6
|
export * from "./execution-lanes.js"; // 7.0.0 M-R2: the ONE lane / store vocabulary (EXECUTION_LANE_WORDS / EXECUTION_LANES / STORE_BACKENDS / SESSION_BACKENDS / MEMORY_ENGINE_BACKENDS) + THE lane-coordinate blank judge isBlankCoordinate (empty or whitespace-only = absent; the write gate and every consumer ask this one function; whether a key is set, and whether an optional key may be empty, stays the caller's call; the same function object as on the `./execution-lanes` subpath) — a zero-import leaf, pure/browser-safe
|
|
7
7
|
export * from "./config-document-refusal.js"; // 15.2.0: the typed refusal of one config.d document (CONFIG_DOCUMENT_REFUSAL_REASONS = empty / invalid_json / not_object / io / retired_domain; ConfigDocumentRefusal { document, reason, error } + the tombstone arm's migration) — the reading is `inspectBundle` in ./node; pure/browser-safe
|
|
8
|
-
export * from "./url-shape.js"; // 11.0.0: THE URL shape judge (httpUrlFlaws / httpUrlRule / isHierarchicalUrl), pure/browser-safe
|
|
8
|
+
export * from "./url-shape.js"; // 11.0.0: THE URL shape judge (httpUrlFlaws / httpUrlRule / isHierarchicalUrl; 16.0.0 + wsUrlFlaws), pure/browser-safe
|
|
9
|
+
export * from "./device-relay.js"; // 16.0.0: THE value grammar of the device lane's DEVICE_RELAY_URL (DEVICE_RELAY_NONE / deviceRelayFlaws / DEVICE_RELAY_RULE), pure/browser-safe
|
|
9
10
|
export * from "./url-credential.js"; // 14.5.0: THE "does this URL carry a credential" judge (findUrlCredential / findUrlUserinfo / URL_CREDENTIAL_REASONS / CREDENTIAL_PARAM_NAMES …), pure/browser-safe
|
|
10
11
|
export * from "./default-model.js"; // 11.0.0: THE catalog-default resolver (roles.default → active tier ladder → fallback), pure/browser-safe
|
|
11
12
|
export { catalogManagesModelPlane, enabledEntryCount } from "./catalog-manages.js"; // THE "does this models document manage the model plane" judge (raw half: at least one entry not explicitly disabled) + the log count it is derived from, pure/browser-safe
|