@sema-agent/settings-schema 15.2.0 → 17.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 +183 -0
- package/README.md +71 -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/atomic-write.d.ts +4 -0
- package/dist/atomic-write.js +20 -0
- 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 +13 -12
- package/dist/config-d-file.js +72 -27
- 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 +31 -4
- package/dist/file-store.js +78 -11
- 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/local-load.d.ts +12 -4
- package/dist/local-load.js +20 -31
- 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 +25 -29
- package/llms.txt +9 -8
- package/package.json +1 -1
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,10 +1,11 @@
|
|
|
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";
|
|
5
6
|
/** `config.d/` 里**合法存在但不是域文档**的文件名(不进 `"unread-config-file"` 告警)。`remote-exec`
|
|
6
7
|
* 不是 `DOMAIN_SCHEMAS` 成员(resolver 不读它),按 service 表决住在这个目录,由 loadRemoteExec 读
|
|
7
|
-
*
|
|
8
|
+
* ——**它有自己的校验路径**(读盘与域文档同一只 `config.d` 文档读法,值判是 `RemoteExecSpec`),所以既不是「未知文件」也不是「域文件」。
|
|
8
9
|
*
|
|
9
10
|
* **导出**(3.0.0):一致性门的语料派生要按同一张表给本地腿的文件分类(域文件 / 专用文件 / 忽略),
|
|
10
11
|
* 手抄一份必漂 —— 漂了的那一天,门会把一个合法的 `remote-exec.json` 报成未知文件。 */
|
|
@@ -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 an ABSENT file (ENOENT with nothing at the path; a dangling symlink is not absence) 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
|
@@ -11,9 +11,12 @@
|
|
|
11
11
|
* <root>/.env secret VALUES (KEY=value) — the ONLY file holding secrets; gitignored, never read here.
|
|
12
12
|
*
|
|
13
13
|
* Rules:
|
|
14
|
-
* -
|
|
14
|
+
* - An ABSENT file (nothing at the path: ENOENT, and neither the file nor `config.d` is a dangling symlink) → the domain is absent → schema defaults
|
|
15
|
+
* fill in (parseDomain tolerates undefined). A DANGLING symlink is present-but-unreadable (a refusal, `io`), never absence.
|
|
15
16
|
* A present-but-EMPTY/whitespace file is a half-write → THROW (fail-closed, like remote-exec); to mean
|
|
16
17
|
* "absent/defaults" the file must be missing, not empty. A valid `{}` object IS a set domain (→ defaults).
|
|
18
|
+
* - `read()` (16.0.0) is the ONE read that answers both ways: the effective config + its read warnings as data when every
|
|
19
|
+
* document reads, or the typed refusal list of the whole directory + its version when one does not — every document read once.
|
|
17
20
|
* - Non-portable domains are OMITTED on a single machine and resolve to their schema defaults (`{workers:[]}`
|
|
18
21
|
* etc.) — no stubbing needed. 逐域为什么被排除见 {@link NON_PORTABLE_DOMAIN_REASONS}(禁裸排除)。
|
|
19
22
|
* - A RETIRED domain's file (`config.d/runtime.json`, see `RETIRED_DOMAINS`) is read only for its tombstone: carrying facts ⇒
|
|
@@ -26,7 +29,7 @@
|
|
|
26
29
|
*/
|
|
27
30
|
import { readdir, stat } from "node:fs/promises";
|
|
28
31
|
import path from "node:path";
|
|
29
|
-
import { buildEffective, DOMAIN_READ_FALLBACK, parseDomainLoud } from "./config-fns.js";
|
|
32
|
+
import { RetiredDomainError, buildEffective, DOMAIN_READ_FALLBACK, parseDomainLoud, refuseRetiredDomain } from "./config-fns.js";
|
|
30
33
|
import { RETIRED_DOMAIN_NAMES, configDPath, configDVersionOf, corruptDocumentRaw, readConfigDFile, readConfigDFileReading, refuseRetiredDomainFiles } from "./config-d-file.js";
|
|
31
34
|
import { PORTABLE_DOMAINS } from "./portable-domains.js";
|
|
32
35
|
import { REMOTE_EXEC_DOCUMENT } from "./remote-exec.js";
|
|
@@ -35,7 +38,7 @@ import { isDomainName, isRetiredDomainName } from "./types.js";
|
|
|
35
38
|
export { NON_PORTABLE_DOMAIN_REASONS, PORTABLE_DOMAINS } from "./portable-domains.js";
|
|
36
39
|
/** `config.d/` 里**合法存在但不是域文档**的文件名(不进 `"unread-config-file"` 告警)。`remote-exec`
|
|
37
40
|
* 不是 `DOMAIN_SCHEMAS` 成员(resolver 不读它),按 service 表决住在这个目录,由 loadRemoteExec 读
|
|
38
|
-
*
|
|
41
|
+
* ——**它有自己的校验路径**(读盘与域文档同一只 `config.d` 文档读法,值判是 `RemoteExecSpec`),所以既不是「未知文件」也不是「域文件」。
|
|
39
42
|
*
|
|
40
43
|
* **导出**(3.0.0):一致性门的语料派生要按同一张表给本地腿的文件分类(域文件 / 专用文件 / 忽略),
|
|
41
44
|
* 手抄一份必漂 —— 漂了的那一天,门会把一个合法的 `remote-exec.json` 报成未知文件。 */
|
|
@@ -57,7 +60,7 @@ export class FileConfigStore {
|
|
|
57
60
|
return configDPath(this.root, domain); // the one name guard (no traversal) lives there
|
|
58
61
|
}
|
|
59
62
|
/** Read + JSON.parse one domain file —— the ONE `config.d/` file reading (`./config-d-file`, shared with `readBundle`): a MISSING
|
|
60
|
-
* file (ENOENT) → undefined = the domain is ABSENT (the ONLY way to mean "use defaults"); a present file that is EMPTY/whitespace-only,
|
|
63
|
+
* file (nothing at the path — ENOENT and not a dangling symlink) → undefined = the domain is ABSENT (the ONLY way to mean "use defaults"); a present file that is EMPTY/whitespace-only,
|
|
61
64
|
* or parses to anything but a JSON OBJECT, is CORRUPT → throw, FAIL CLOSED (an empty present file is a half-write/truncation, NOT
|
|
62
65
|
* "absent": treating it as absent would silently DROP a published domain — e.g. a truncated governance.json drops its
|
|
63
66
|
* commandPolicy/autonomy guardrails). This also keeps getVersion in lock-step with getEffective/getDomain: stableStringify collapses
|
|
@@ -84,6 +87,11 @@ export class FileConfigStore {
|
|
|
84
87
|
const onWarning = this.opts.onWarning;
|
|
85
88
|
if (!onWarning)
|
|
86
89
|
return;
|
|
90
|
+
for (const w of await this.#unreadFiles())
|
|
91
|
+
onWarning(w);
|
|
92
|
+
}
|
|
93
|
+
/** {@link #warnUnreadFiles} 的扫描本体:没人读的 `*.json` 逐个一条告警(单读口 `read()` 把它们交成数据,所以它总扫)。 */
|
|
94
|
+
async #unreadFiles() {
|
|
87
95
|
let names;
|
|
88
96
|
try {
|
|
89
97
|
names = await readdir(path.join(this.root, "config.d"));
|
|
@@ -91,8 +99,9 @@ export class FileConfigStore {
|
|
|
91
99
|
catch {
|
|
92
100
|
// 目录读不了(不存在 / 只有 x 没有 r 的目录权限)⇒ 本次扫不了,**不抛**:这条是尽力而为的审计线,
|
|
93
101
|
// 绝不能把一次本来能成功的 getEffective 拖成失败(域文件本身仍按名逐个打开,该失败的自然会失败)。
|
|
94
|
-
return;
|
|
102
|
+
return [];
|
|
95
103
|
}
|
|
104
|
+
const out = [];
|
|
96
105
|
for (const name of names) {
|
|
97
106
|
if (name.startsWith(".") || !name.endsWith(".json") || NON_DOMAIN_CONFIG_FILES.has(name))
|
|
98
107
|
continue;
|
|
@@ -101,10 +110,11 @@ export class FileConfigStore {
|
|
|
101
110
|
continue; // 真被读的域
|
|
102
111
|
if (isRetiredDomainName(base))
|
|
103
112
|
continue; // retired 域:由墓碑判(带事实 ⇒ getEffective 抛),不是「没人读的文件」
|
|
104
|
-
|
|
113
|
+
out.push(isDomainName(base)
|
|
105
114
|
? { kind: "unread-config-file", file: name, domain: base, why: "non-portable-domain" }
|
|
106
115
|
: { kind: "unread-config-file", file: name, why: "not-a-domain" });
|
|
107
116
|
}
|
|
117
|
+
return out;
|
|
108
118
|
}
|
|
109
119
|
/** Raw (unparsed) values for every PORTABLE domain and every retired domain (the latter for the tombstone), keyed by
|
|
110
120
|
* name (absent → undefined). Both feed the version hash: a retired file appearing / changing flips `getEffective`
|
|
@@ -133,21 +143,22 @@ export class FileConfigStore {
|
|
|
133
143
|
}
|
|
134
144
|
/** Read+parse one domain (validated, defaults filled), or `undefined` if the domain is ABSENT — matching
|
|
135
145
|
* the {@link ConfigReader} contract (reader.ts: absent domain → undefined) and the server stores'
|
|
136
|
-
* `has(domain) ? parse : undefined`.
|
|
137
|
-
* with defaults
|
|
138
|
-
*
|
|
146
|
+
* `has(domain) ? parse : undefined`. Only an ABSENT file (ENOENT with nothing at the path; a dangling symlink is not absence) is absence → undefined. A present `{}` IS a set
|
|
147
|
+
* domain → parsed with defaults. A present file that is empty or whitespace-only (a half-write) throws as a corrupt file,
|
|
148
|
+
* as do bad JSON, a non-object, an unreadable file and one nested too deep (the same reading as getEffective / readBundle).
|
|
149
|
+
* (getEffective still fills defaults for absent domains; getDomain is the per-domain "was this ever set?" probe.) */
|
|
139
150
|
async getDomain(domain) {
|
|
140
151
|
await refuseRetiredDomainFiles(this.root); // every read entry asks the tombstone first (exportBundle reads through here)
|
|
141
152
|
const raw = await this.#readDomainRaw(domain);
|
|
142
153
|
if (raw === undefined)
|
|
143
|
-
return undefined; //
|
|
154
|
+
return undefined; // absent file (nothing at the path) → domain absent (not schema defaults); a corrupt or unreadable file (empty, bad JSON, dangling symlink …) threw above
|
|
144
155
|
// 0.19.0(对抗复审 复审 round2 F3):与 buildEffective **同一条** parse-and-warn 判据。此前本方法直接
|
|
145
156
|
// parseDomain,于是 `exportBundle`(它就是逐域走 getDomain 读的)会把一份携带未知键的域**静默剥**
|
|
146
157
|
// 成一份"干净"的 bundle —— 再 import 回去就是一次持久的配置丢失,全程零告警。
|
|
147
158
|
return parseDomainLoud(domain, raw, this.opts.onWarning);
|
|
148
159
|
}
|
|
149
160
|
/** 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`) — `
|
|
161
|
+
* (`./config-d-file`) — `read()` asks the same function, so a clean tree has the same version from either. */
|
|
151
162
|
#versionOf(raws) {
|
|
152
163
|
return configDVersionOf(raws);
|
|
153
164
|
}
|
|
@@ -191,4 +202,60 @@ export class FileConfigStore {
|
|
|
191
202
|
async getVersion() {
|
|
192
203
|
return this.#versionOf(await this.#readAllRaw());
|
|
193
204
|
}
|
|
205
|
+
/**
|
|
206
|
+
* **单读口**(16.0.0):一次读整棵 `config.d`(全部便携域 + retired 域的墓碑文档,每份只读一次),交一个结局({@link FileConfigRead})——
|
|
207
|
+
* · 每份文档在文件层都读得进 ⇒ `effective`:与严读 `getEffective()` 逐位同;读侧告警交成数据(没人读的文件 + 装配告警,不经 `onWarning`);
|
|
208
|
+
* · 有文档在文件层被拒(空 / 坏 JSON / 不是对象 / 读不出 / 过深;带事实的 retired 域文档是墓碑拒,带迁移编号)⇒ `refused`:整棵目录的拒单与这一次读到的内容版本。
|
|
209
|
+
* 严读与 `readBundle` 读的是同一只文件读法(`./config-d-file` 的读数)—— 同一种坏形同一个词、同一句话;区别只在渲染:严读遇到第一份坏文档就抛,本方法把整个目录读完、逐份交出。
|
|
210
|
+
* 文件层之外的拒(读得出、过不了门控域的 schema)照严读抛 —— 那是判形,不在拒单里。拒单、版本、effective、告警都出自这一次读到的字节:没有「先抛、再重探」的第二次读。
|
|
211
|
+
*/
|
|
212
|
+
async read() {
|
|
213
|
+
const documents = [...RETIRED_DOMAIN_NAMES, ...PORTABLE_DOMAINS];
|
|
214
|
+
const [readings, unread] = await Promise.all([Promise.all(documents.map((d) => readConfigDFileReading(this.root, d))), this.#unreadFiles()]);
|
|
215
|
+
const raws = {};
|
|
216
|
+
const refusals = [];
|
|
217
|
+
documents.forEach((document, i) => {
|
|
218
|
+
const reading = readings[i];
|
|
219
|
+
switch (reading.status) {
|
|
220
|
+
case "absent":
|
|
221
|
+
raws[document] = undefined;
|
|
222
|
+
return;
|
|
223
|
+
case "refused":
|
|
224
|
+
raws[document] = corruptDocumentRaw(reading);
|
|
225
|
+
refusals.push({ document, reason: reading.reason, error: reading.error });
|
|
226
|
+
return;
|
|
227
|
+
case "present":
|
|
228
|
+
raws[document] = reading.value;
|
|
229
|
+
if (isRetiredDomainName(document)) {
|
|
230
|
+
const tombstone = tombstoneOf(document, reading.value);
|
|
231
|
+
if (tombstone !== undefined)
|
|
232
|
+
refusals.push(tombstone);
|
|
233
|
+
}
|
|
234
|
+
return;
|
|
235
|
+
default: {
|
|
236
|
+
const unhandled = reading;
|
|
237
|
+
throw new Error(`FileConfigStore.read: unhandled reading ${String(unhandled)}`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
const version = this.#versionOf(raws);
|
|
242
|
+
if (refusals.length > 0)
|
|
243
|
+
return { kind: "refused", refusals, version, warnings: unread };
|
|
244
|
+
const warnings = [...unread];
|
|
245
|
+
const updatedAt = await this.#maxMtimeIso();
|
|
246
|
+
const effective = buildEffective(version, (d) => raws[d], updatedAt, (w) => warnings.push(w));
|
|
247
|
+
return { kind: "effective", effective, warnings };
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
/** 墓碑判一次(与严读同一只判官 `refuseRetiredDomain`、同一句):retired 域文档带事实 ⇒ 一条 `retired_domain` 拒;缺席 / `{}` ⇒ `undefined`。 */
|
|
251
|
+
function tombstoneOf(domain, raw) {
|
|
252
|
+
try {
|
|
253
|
+
refuseRetiredDomain(domain, raw);
|
|
254
|
+
return undefined;
|
|
255
|
+
}
|
|
256
|
+
catch (e) {
|
|
257
|
+
if (e instanceof RetiredDomainError)
|
|
258
|
+
return { document: e.domain, reason: "retired_domain", error: e.message, migration: e.migration };
|
|
259
|
+
throw e;
|
|
260
|
+
}
|
|
194
261
|
}
|
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
|
package/dist/local-load.d.ts
CHANGED
|
@@ -2,17 +2,25 @@ import { type RequiredEnvName } from "./file-edit.js";
|
|
|
2
2
|
import { type EffectiveReadWarning } from "./config-fns.js";
|
|
3
3
|
import { type RemoteExecSpec } from "./remote-exec.js";
|
|
4
4
|
import type { EffectiveConfig } from "./types.js";
|
|
5
|
-
/** The remoteExec file path under a config root (under config.d/, beside the per-domain files). */
|
|
5
|
+
/** The remoteExec file path under a config root (under config.d/, beside the per-domain files) — the ONE config.d path builder (`configDPath`) answers it. */
|
|
6
6
|
export declare function remoteExecPath(root: string): string;
|
|
7
|
-
/** Read `<root>/config.d/remote-exec.json` → a VALIDATED RemoteExecSpec, or undefined if
|
|
8
|
-
* (→ the engine defaults to `host` under CONFIG_PROVIDER=local). Throws (zod) on a present-but-invalid file.
|
|
7
|
+
/** Read `<root>/config.d/remote-exec.json` → a VALIDATED RemoteExecSpec, or undefined if there is NOTHING at that path
|
|
8
|
+
* (→ the engine defaults to `host` under CONFIG_PROVIDER=local). Throws (zod) on a present-but-invalid file.
|
|
9
|
+
*
|
|
10
|
+
* ABSENT means no entry on the path — nothing else. A present-but-empty file is a corruption/half-write, NOT "use the default" — FAIL CLOSED (an empty file
|
|
11
|
+
* silently falling back to the host lane would be an isolation DOWNGRADE if the user meant local-docker); so is a DANGLING symbolic link, at the file or at
|
|
12
|
+
* `config.d` itself (the kernel follows the link and answers ENOENT, but the entry is there: present and unreadable — it throws the same one sentence the directory
|
|
13
|
+
* migration prints, with its way out: repair or remove the link). To get the host default, the file must be ABSENT, not empty and not a broken link.
|
|
14
|
+
* This function has no read of its own: it is THE `config.d` document reading (`readConfigDFile`, one absence rule, one text judge — empty / bad JSON / not an object, one sentence
|
|
15
|
+
* each — shared with FileConfigStore, readBundle and the directory migration); the sanitized value judge is the migration's too (`./config-d-file`). */
|
|
9
16
|
export declare function loadRemoteExec(root: string): Promise<RemoteExecSpec | undefined>;
|
|
10
17
|
/** Validate + ATOMICALLY write `<root>/config.d/remote-exec.json` (the package's one atomic writer, `./atomic-write`; never a half file). The
|
|
11
18
|
* client's `config` editor writes the exec backend through this; the engine reads it via loadRemoteExec. */
|
|
12
19
|
export declare function writeRemoteExec(root: string, spec: unknown): Promise<RemoteExecSpec>;
|
|
13
20
|
/** Parse `<root>/.env` (KEY=VALUE, `#` comments, optional surrounding quotes) MERGED OVER process.env (the
|
|
14
21
|
* file wins). The ONLY place secret VALUES live — the contract files carry env-NAMEs only. A `KEY=` with an
|
|
15
|
-
* empty value IS present (the caller may treat empty as a weak warning). Absent .env → just process.env
|
|
22
|
+
* empty value IS present (the caller may treat empty as a weak warning). Absent .env → just process.env — absent
|
|
23
|
+
* means no entry on the path (the one absence rule, `readTextIfPresent`): a DANGLING symbolic link at `.env` is present and unreadable, so it throws.
|
|
16
24
|
* A `.env` value of `file:<path>` resolves to that file's trimmed content (file-NAME ref — key stays out of
|
|
17
25
|
* env, contract stays env-NAME-only). Schema still rejects pasted plaintext (the contract field is a NAME). */
|
|
18
26
|
export declare function loadEnvFile(root: string): Promise<Record<string, string>>;
|
package/dist/local-load.js
CHANGED
|
@@ -10,34 +10,28 @@
|
|
|
10
10
|
import { mkdir, readFile } from "node:fs/promises";
|
|
11
11
|
import { homedir } from "node:os";
|
|
12
12
|
import path from "node:path";
|
|
13
|
-
import { writeFileAtomic } from "./atomic-write.js";
|
|
13
|
+
import { readTextIfPresent, writeFileAtomic } from "./atomic-write.js";
|
|
14
14
|
import { FileConfigStore } from "./file-store.js";
|
|
15
15
|
import { listRequiredEnvNames } from "./file-edit.js";
|
|
16
16
|
import {} from "./config-fns.js";
|
|
17
|
-
import { configDDocumentText,
|
|
17
|
+
import { configDDocumentText, configDPath, parseRemoteExecDocument, readConfigDFile } from "./config-d-file.js";
|
|
18
18
|
import { REMOTE_EXEC_DOCUMENT } from "./remote-exec.js";
|
|
19
|
-
/** The remoteExec file path under a config root (under config.d/, beside the per-domain files). */
|
|
19
|
+
/** The remoteExec file path under a config root (under config.d/, beside the per-domain files) — the ONE config.d path builder (`configDPath`) answers it. */
|
|
20
20
|
export function remoteExecPath(root) {
|
|
21
|
-
return
|
|
21
|
+
return configDPath(root, REMOTE_EXEC_DOCUMENT);
|
|
22
22
|
}
|
|
23
|
-
/** Read `<root>/config.d/remote-exec.json` → a VALIDATED RemoteExecSpec, or undefined if
|
|
24
|
-
* (→ the engine defaults to `host` under CONFIG_PROVIDER=local). Throws (zod) on a present-but-invalid file.
|
|
23
|
+
/** Read `<root>/config.d/remote-exec.json` → a VALIDATED RemoteExecSpec, or undefined if there is NOTHING at that path
|
|
24
|
+
* (→ the engine defaults to `host` under CONFIG_PROVIDER=local). Throws (zod) on a present-but-invalid file.
|
|
25
|
+
*
|
|
26
|
+
* ABSENT means no entry on the path — nothing else. A present-but-empty file is a corruption/half-write, NOT "use the default" — FAIL CLOSED (an empty file
|
|
27
|
+
* silently falling back to the host lane would be an isolation DOWNGRADE if the user meant local-docker); so is a DANGLING symbolic link, at the file or at
|
|
28
|
+
* `config.d` itself (the kernel follows the link and answers ENOENT, but the entry is there: present and unreadable — it throws the same one sentence the directory
|
|
29
|
+
* migration prints, with its way out: repair or remove the link). To get the host default, the file must be ABSENT, not empty and not a broken link.
|
|
30
|
+
* This function has no read of its own: it is THE `config.d` document reading (`readConfigDFile`, one absence rule, one text judge — empty / bad JSON / not an object, one sentence
|
|
31
|
+
* each — shared with FileConfigStore, readBundle and the directory migration); the sanitized value judge is the migration's too (`./config-d-file`). */
|
|
25
32
|
export async function loadRemoteExec(root) {
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
try {
|
|
29
|
-
text = await readFile(file, "utf8");
|
|
30
|
-
}
|
|
31
|
-
catch (e) {
|
|
32
|
-
if (e.code === "ENOENT")
|
|
33
|
-
return undefined;
|
|
34
|
-
throw e;
|
|
35
|
-
}
|
|
36
|
-
// A present-but-empty file is a corruption/half-write, NOT "use the default" — FAIL CLOSED (an empty file
|
|
37
|
-
// silently falling back to the host lane would be an isolation DOWNGRADE if the user meant local-docker).
|
|
38
|
-
// 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
|
|
39
|
-
// each — the same judge FileConfigStore and the directory migration read with); the sanitized value judge is the migration's too (`./config-d-file`).
|
|
40
|
-
return parseRemoteExecDocument(judgeConfigDText(REMOTE_EXEC_DOCUMENT, text, file));
|
|
33
|
+
const doc = await readConfigDFile(root, REMOTE_EXEC_DOCUMENT);
|
|
34
|
+
return doc === undefined ? undefined : parseRemoteExecDocument(doc);
|
|
41
35
|
}
|
|
42
36
|
/** Validate + ATOMICALLY write `<root>/config.d/remote-exec.json` (the package's one atomic writer, `./atomic-write`; never a half file). The
|
|
43
37
|
* client's `config` editor writes the exec backend through this; the engine reads it via loadRemoteExec. */
|
|
@@ -77,7 +71,8 @@ async function resolveFileRef(spec, root) {
|
|
|
77
71
|
}
|
|
78
72
|
/** Parse `<root>/.env` (KEY=VALUE, `#` comments, optional surrounding quotes) MERGED OVER process.env (the
|
|
79
73
|
* file wins). The ONLY place secret VALUES live — the contract files carry env-NAMEs only. A `KEY=` with an
|
|
80
|
-
* empty value IS present (the caller may treat empty as a weak warning). Absent .env → just process.env
|
|
74
|
+
* empty value IS present (the caller may treat empty as a weak warning). Absent .env → just process.env — absent
|
|
75
|
+
* means no entry on the path (the one absence rule, `readTextIfPresent`): a DANGLING symbolic link at `.env` is present and unreadable, so it throws.
|
|
81
76
|
* A `.env` value of `file:<path>` resolves to that file's trimmed content (file-NAME ref — key stays out of
|
|
82
77
|
* env, contract stays env-NAME-only). Schema still rejects pasted plaintext (the contract field is a NAME). */
|
|
83
78
|
export async function loadEnvFile(root) {
|
|
@@ -85,15 +80,9 @@ export async function loadEnvFile(root) {
|
|
|
85
80
|
for (const [k, v] of Object.entries(process.env))
|
|
86
81
|
if (v !== undefined)
|
|
87
82
|
merged[k] = v;
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
92
|
-
catch (e) {
|
|
93
|
-
if (e.code === "ENOENT")
|
|
94
|
-
return merged;
|
|
95
|
-
throw e;
|
|
96
|
-
}
|
|
83
|
+
const text = await readTextIfPresent(path.join(root, ".env"));
|
|
84
|
+
if (text === undefined)
|
|
85
|
+
return merged;
|
|
97
86
|
for (const raw of text.split("\n")) {
|
|
98
87
|
const line = raw.trim();
|
|
99
88
|
if (!line || line.startsWith("#"))
|
|
@@ -7,7 +7,7 @@ import type { REMOTE_EXEC_DOCUMENT } from "./remote-exec.js";
|
|
|
7
7
|
import type { DomainName, RetiredDomainName } from "./types.js";
|
|
8
8
|
/** 本版登记的一次性**数据**迁移 id(闭集;每个 id 在 `ONE_TIME_MIGRATIONS` 登记表里恰一行)。收据只许这些 id —— 同表登记的 wire 删码位
|
|
9
9
|
* (`WIRE_RETIREMENT_IDS`,`./one-time-migrations`)不改任何存储形,铸不出收据,不在本集。 */
|
|
10
|
-
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", "SS-M10", "SS-M11"];
|
|
10
|
+
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", "SS-M10", "SS-M11", "SS-M12"];
|
|
11
11
|
export type OneTimeMigrationId = (typeof ONE_TIME_MIGRATION_IDS)[number];
|
|
12
12
|
/**
|
|
13
13
|
* 一条收据说迁移**对一个坐标做了什么**(闭集,每词一义):
|
|
@@ -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-M8", "SS-M9", "SS-M10", "SS-M11"];
|
|
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", "SS-M10", "SS-M11", "SS-M12"];
|
|
4
4
|
/**
|
|
5
5
|
* 一条收据说迁移**对一个坐标做了什么**(闭集,每词一义):
|
|
6
6
|
* - `moved` — 值搬到新位(`to`),义不变;
|
|
@@ -41,7 +41,7 @@ export type StoredDocuments = Readonly<Partial<Record<DomainName | RetiredDomain
|
|
|
41
41
|
export interface StoredDocumentsMigration {
|
|
42
42
|
/** 迁移后的文档集:没改的文档是**同一引用**;被移除的(retired 域)不在;新建的(例:迁出的 `governance` / `limits`)在。 */
|
|
43
43
|
readonly docs: Readonly<Record<string, unknown>>;
|
|
44
|
-
/** 逐条收据(步序 SS-M1 → SS-M2 → SS-M5 → SS-M6 → SS-M8 → SS-M9 → SS-M10 → SS-M7 → SS-
|
|
44
|
+
/** 逐条收据(步序 SS-M1 → SS-M2 → SS-M5 → SS-M6 → SS-M8 → SS-M9 → SS-M10 → SS-M7 → workers 行步〔SS-M12 改写 → SS-M4 改写 → 一次终判〕:纯改写步在前,用新 schema 终判的 workers 行步最后;步内按坐标)。
|
|
45
45
|
* 只有 `refused` 的收据 = 什么都没改。 */
|
|
46
46
|
readonly receipts: readonly MigrationReceipt[];
|
|
47
47
|
}
|