@sema-agent/settings-schema 6.3.0 → 7.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.
Files changed (103) hide show
  1. package/CHANGELOG.md +207 -0
  2. package/README.md +40 -5
  3. package/bin/settings-schema.mjs +22 -0
  4. package/conformance/VERSION +1 -1
  5. package/conformance/cases/c48-s935-model-headers-transported/expected.json +115 -0
  6. package/conformance/cases/c48-s935-model-headers-transported/input.json +37 -0
  7. package/conformance/cases/c48-s935-model-headers-transported/meta.json +6 -0
  8. package/conformance/cases/c49-s935-model-headers-secret-entry-dropped/expected.json +98 -0
  9. package/conformance/cases/c49-s935-model-headers-secret-entry-dropped/input.json +28 -0
  10. package/conformance/cases/c49-s935-model-headers-secret-entry-dropped/meta.json +9 -0
  11. package/conformance/cases/c50-s784-lane-host-legal/expected.json +102 -0
  12. package/conformance/cases/c50-s784-lane-host-legal/input.json +29 -0
  13. package/conformance/cases/c50-s784-lane-host-legal/meta.json +5 -0
  14. package/conformance/cases/c51-s784-lane-e2b-legal/expected.json +104 -0
  15. package/conformance/cases/c51-s784-lane-e2b-legal/input.json +32 -0
  16. package/conformance/cases/c51-s784-lane-e2b-legal/meta.json +5 -0
  17. package/conformance/cases/c52-s784-lane-k8s-legal/expected.json +106 -0
  18. package/conformance/cases/c52-s784-lane-k8s-legal/input.json +34 -0
  19. package/conformance/cases/c52-s784-lane-k8s-legal/meta.json +5 -0
  20. package/conformance/cases/c53-s784-lane-ssh-legal/expected.json +106 -0
  21. package/conformance/cases/c53-s784-lane-ssh-legal/input.json +34 -0
  22. package/conformance/cases/c53-s784-lane-ssh-legal/meta.json +5 -0
  23. package/conformance/cases/c54-s784-lane-adb-legal/expected.json +102 -0
  24. package/conformance/cases/c54-s784-lane-adb-legal/input.json +29 -0
  25. package/conformance/cases/c54-s784-lane-adb-legal/meta.json +5 -0
  26. package/conformance/cases/c55-s784-lane-local-docker-legal/expected.json +103 -0
  27. package/conformance/cases/c55-s784-lane-local-docker-legal/input.json +30 -0
  28. package/conformance/cases/c55-s784-lane-local-docker-legal/meta.json +5 -0
  29. package/conformance/cases/c56-s784-lane-device-legal/expected.json +109 -0
  30. package/conformance/cases/c56-s784-lane-device-legal/input.json +36 -0
  31. package/conformance/cases/c56-s784-lane-device-legal/meta.json +5 -0
  32. package/conformance/cases/c57-s784-g1-enabled-no-lane/expected.json +83 -0
  33. package/conformance/cases/c57-s784-g1-enabled-no-lane/input.json +23 -0
  34. package/conformance/cases/c57-s784-g1-enabled-no-lane/meta.json +5 -0
  35. package/conformance/cases/c58-s784-g2-multitenant-host/expected.json +83 -0
  36. package/conformance/cases/c58-s784-g2-multitenant-host/input.json +34 -0
  37. package/conformance/cases/c58-s784-g2-multitenant-host/meta.json +5 -0
  38. package/conformance/cases/c59-s784-g3-k8s-missing-image/expected.json +83 -0
  39. package/conformance/cases/c59-s784-g3-k8s-missing-image/input.json +26 -0
  40. package/conformance/cases/c59-s784-g3-k8s-missing-image/meta.json +5 -0
  41. package/conformance/cases/c60-s784-g3-e2b-missing-key/expected.json +83 -0
  42. package/conformance/cases/c60-s784-g3-e2b-missing-key/input.json +26 -0
  43. package/conformance/cases/c60-s784-g3-e2b-missing-key/meta.json +5 -0
  44. package/conformance/cases/c61-s784-g3-other-lane-key/expected.json +83 -0
  45. package/conformance/cases/c61-s784-g3-other-lane-key/input.json +30 -0
  46. package/conformance/cases/c61-s784-g3-other-lane-key/meta.json +5 -0
  47. package/conformance/cases/c62-s784-g7a-session-tidb/expected.json +83 -0
  48. package/conformance/cases/c62-s784-g7a-session-tidb/input.json +27 -0
  49. package/conformance/cases/c62-s784-g7a-session-tidb/meta.json +5 -0
  50. package/conformance/cases/c63-s784-host-two-replicas/expected.json +83 -0
  51. package/conformance/cases/c63-s784-host-two-replicas/input.json +27 -0
  52. package/conformance/cases/c63-s784-host-two-replicas/meta.json +5 -0
  53. package/conformance/cases/c64-s784-multitenant-local-store/expected.json +83 -0
  54. package/conformance/cases/c64-s784-multitenant-local-store/input.json +30 -0
  55. package/conformance/cases/c64-s784-multitenant-local-store/meta.json +5 -0
  56. package/conformance/cases/c65-s784-pg-without-db/expected.json +83 -0
  57. package/conformance/cases/c65-s784-pg-without-db/input.json +27 -0
  58. package/conformance/cases/c65-s784-pg-without-db/meta.json +5 -0
  59. package/conformance/cases/c66-s784-memory-backend-mismatch/expected.json +83 -0
  60. package/conformance/cases/c66-s784-memory-backend-mismatch/input.json +36 -0
  61. package/conformance/cases/c66-s784-memory-backend-mismatch/meta.json +5 -0
  62. package/conformance/cases/c67-s784-retired-field-loud/expected.json +83 -0
  63. package/conformance/cases/c67-s784-retired-field-loud/input.json +22 -0
  64. package/conformance/cases/c67-s784-retired-field-loud/meta.json +5 -0
  65. package/conformance/cases/c68-s784-device-local-store/expected.json +83 -0
  66. package/conformance/cases/c68-s784-device-local-store/input.json +26 -0
  67. package/conformance/cases/c68-s784-device-local-store/meta.json +5 -0
  68. package/conformance/cases/cli04-env-value-with-newline/expected.json +13 -2
  69. package/conformance/cases/cli04-env-value-with-newline/input.json +8 -1
  70. package/conformance/cases/cli04-env-value-with-newline/meta.json +1 -1
  71. package/conformance/migrate-workers/m01-g5-k8s-env-lifted/expected.json +49 -0
  72. package/conformance/migrate-workers/m01-g5-k8s-env-lifted/input.json +26 -0
  73. package/conformance/migrate-workers/m01-g5-k8s-env-lifted/meta.json +4 -0
  74. package/conformance/migrate-workers/m02-g5-idempotent/expected.json +38 -0
  75. package/conformance/migrate-workers/m02-g5-idempotent/input.json +33 -0
  76. package/conformance/migrate-workers/m02-g5-idempotent/meta.json +5 -0
  77. package/conformance/migrate-workers/m03-g5-refused-no-lane/expected.json +71 -0
  78. package/conformance/migrate-workers/m03-g5-refused-no-lane/input.json +42 -0
  79. package/conformance/migrate-workers/m03-g5-refused-no-lane/meta.json +4 -0
  80. package/conformance/migrate-workers/m04-g7b-session-tidb-lifted/expected.json +52 -0
  81. package/conformance/migrate-workers/m04-g7b-session-tidb-lifted/input.json +30 -0
  82. package/conformance/migrate-workers/m04-g7b-session-tidb-lifted/meta.json +4 -0
  83. package/dist/cli.d.ts +8 -0
  84. package/dist/cli.js +70 -0
  85. package/dist/execution-lane-index.d.ts +19 -0
  86. package/dist/execution-lane-index.js +23 -0
  87. package/dist/execution-lanes.d.ts +64 -0
  88. package/dist/execution-lanes.js +152 -0
  89. package/dist/file-edit.js +26 -21
  90. package/dist/hash.d.ts +3 -3
  91. package/dist/hash.js +3 -3
  92. package/dist/hooks.d.ts +12 -12
  93. package/dist/index.d.ts +2 -0
  94. package/dist/index.js +2 -0
  95. package/dist/migrate-workers.d.ts +63 -0
  96. package/dist/migrate-workers.js +331 -0
  97. package/dist/remote-exec.d.ts +23 -64
  98. package/dist/remote-exec.js +20 -71
  99. package/dist/secret-refs.d.ts +7 -1
  100. package/dist/secret-refs.js +9 -3
  101. package/dist/types.d.ts +1382 -430
  102. package/dist/types.js +298 -76
  103. package/package.json +9 -1
@@ -0,0 +1,42 @@
1
+ {
2
+ "workers": [
3
+ {
4
+ "name": "handless",
5
+ "enabled": true,
6
+ "host": "default",
7
+ "image": "sema-server:latest",
8
+ "secretRefs": {
9
+ "apiKeyEnv": "MODEL_KEY"
10
+ },
11
+ "runtime": {
12
+ "sessionBackend": "auto",
13
+ "memoryBackend": "off",
14
+ "requirePrincipal": false,
15
+ "rateLimitRpm": 0
16
+ },
17
+ "coords": {}
18
+ },
19
+ {
20
+ "name": "canary",
21
+ "enabled": true,
22
+ "host": "default",
23
+ "image": "sema-server:latest",
24
+ "secretRefs": {
25
+ "apiKeyEnv": "MODEL_KEY"
26
+ },
27
+ "runtime": {
28
+ "sessionBackend": "auto",
29
+ "memoryBackend": "off",
30
+ "requirePrincipal": false,
31
+ "rateLimitRpm": 0
32
+ },
33
+ "coords": {},
34
+ "env": {
35
+ "REMOTE_EXEC": "e2b"
36
+ },
37
+ "secretEnv": {
38
+ "E2B_API_KEY": "HOST_E2B_KEY"
39
+ }
40
+ }
41
+ ]
42
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "contributor": "contract",
3
+ "claim": "G5 refused:enabled 且无 env.REMOTE_EXEC 的 worker(WA-49 病例本身)⇒ refused 点名 env.REMOTE_EXEC,不进输出 doc;同域的 canary(e2b,密钥在 secretEnv)照迁进 runtime.execution.secretRefs。"
4
+ }
@@ -0,0 +1,52 @@
1
+ {
2
+ "doc": {
3
+ "workers": [
4
+ {
5
+ "name": "persist",
6
+ "enabled": true,
7
+ "host": "default",
8
+ "image": "sema-server:latest",
9
+ "secretRefs": {
10
+ "apiKeyEnv": "MODEL_KEY",
11
+ "dbPasswordEnv": "HOST_TIDB_PW"
12
+ },
13
+ "runtime": {
14
+ "requirePrincipal": false,
15
+ "rateLimitRpm": 0,
16
+ "execution": {
17
+ "lane": "host",
18
+ "coords": {},
19
+ "secretRefs": {}
20
+ },
21
+ "store": {
22
+ "backend": "mysql",
23
+ "session": "mysql",
24
+ "db": {
25
+ "host": "tidb.internal",
26
+ "port": 4000,
27
+ "user": "svc",
28
+ "database": "sema"
29
+ }
30
+ }
31
+ },
32
+ "coords": {
33
+ "gitApiBaseUrl": "https://git.internal"
34
+ }
35
+ }
36
+ ]
37
+ },
38
+ "changed": [
39
+ "persist"
40
+ ],
41
+ "dropped": [
42
+ {
43
+ "name": "persist",
44
+ "index": 0,
45
+ "field": [
46
+ "runtime",
47
+ "memoryBackend"
48
+ ]
49
+ }
50
+ ],
51
+ "refused": []
52
+ }
@@ -0,0 +1,30 @@
1
+ {
2
+ "workers": [
3
+ {
4
+ "name": "persist",
5
+ "enabled": true,
6
+ "host": "default",
7
+ "image": "sema-server:latest",
8
+ "secretRefs": {
9
+ "apiKeyEnv": "MODEL_KEY",
10
+ "tidbPasswordEnv": "HOST_TIDB_PW"
11
+ },
12
+ "runtime": {
13
+ "sessionBackend": "tidb",
14
+ "memoryBackend": "off",
15
+ "requirePrincipal": false,
16
+ "rateLimitRpm": 0,
17
+ "db": "sema"
18
+ },
19
+ "coords": {
20
+ "gitApiBaseUrl": "https://git.internal",
21
+ "tidbHost": "tidb.internal",
22
+ "tidbPort": 4000,
23
+ "tidbUser": "svc"
24
+ },
25
+ "env": {
26
+ "REMOTE_EXEC": "host"
27
+ }
28
+ }
29
+ ]
30
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "contributor": "contract",
3
+ "claim": "G7b:旧形 runtime.sessionBackend tidb(今天引擎见之拒启)+ tidb 坐标 ⇒ store {backend: mysql, session: mysql, db} + secretRefs.dbPasswordEnv —— 改成能起的形 + 通知行,不是拒。"
4
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,8 @@
1
+ /** 入口注入的 I/O(核心不碰 node:fs)。`writeFileAtomic` 必须原子(写临时文件再改名):半写的 workers 文档比旧文档更糟。 */
2
+ export interface SettingsSchemaCliIo {
3
+ readFile(path: string): Promise<string>;
4
+ writeFileAtomic(path: string, text: string): Promise<void>;
5
+ stdout(text: string): void;
6
+ stderr(text: string): void;
7
+ }
8
+ export declare function runSettingsSchemaCli(argv: readonly string[], io: SettingsSchemaCliIo): Promise<number>;
package/dist/cli.js ADDED
@@ -0,0 +1,70 @@
1
+ /**
2
+ * `settings-schema` 命令行的核心(7.0.0)—— 子命令只有一个:`migrate-workers <workers.json> [--check]`,即
3
+ * {@link migrateWorkersDomain} 的文件形(文件后端 / 离线用;中心在它的 workers 读口直接调同一只函数)。
4
+ *
5
+ * 本模块不碰 `node:fs`:读写文件由入口 `bin/settings-schema.mjs` 注入(那是本包唯一用 fs 写文件的地方),所以测试可以直接驱动它。
6
+ * 不进主桶 —— 它是 bin 的实现,不是契约面。
7
+ *
8
+ * 退出码:0 = 已写回 / 无事可做;1 = 有 worker 迁不了(refused)或迁出的文档整域读不进 7.0.0,**一字不写**;2 = 用法 / 读文件 /
9
+ * JSON 错;3 = `--check` 且会有改动(不写)。stdout 恒打一行 JSON 报告(`event: "workers_domain_migrated"`,只点名字段、不带值)。
10
+ */
11
+ import { formatConfigPath, sanitizeZodError } from "./config-fns.js";
12
+ import { migrateWorkersDomain } from "./migrate-workers.js";
13
+ import { DOMAIN_SCHEMAS } from "./types.js";
14
+ const USAGE = [
15
+ "usage: settings-schema migrate-workers <workers.json> [--check]",
16
+ " One-time 7.0.0 migration of a workers domain document ({ \"workers\": [...] }): env.REMOTE_EXEC + lane keys →",
17
+ " runtime.execution, sessionBackend / coords.tidb* / runtime.db / tidbPasswordEnv → runtime.store. Writes the file in",
18
+ " place only when every worker migrates; --check only reports (exit 3 if the file would change).",
19
+ ].join("\n");
20
+ export async function runSettingsSchemaCli(argv, io) {
21
+ const [command, ...rest] = argv;
22
+ const check = rest.includes("--check");
23
+ const files = rest.filter((a) => a !== "--check");
24
+ const file = files[0];
25
+ if (command !== "migrate-workers" || files.length !== 1 || file === undefined || file.startsWith("-")) {
26
+ io.stderr(`${USAGE}\n`);
27
+ return 2;
28
+ }
29
+ let text;
30
+ try {
31
+ text = await io.readFile(file);
32
+ }
33
+ catch (err) {
34
+ io.stderr(`settings-schema: cannot read ${file}: ${err instanceof Error ? err.message : String(err)}\n`);
35
+ return 2;
36
+ }
37
+ let doc;
38
+ try {
39
+ doc = JSON.parse(text);
40
+ }
41
+ catch {
42
+ io.stderr(`settings-schema: ${file} is not valid JSON\n`);
43
+ return 2;
44
+ }
45
+ const m = migrateWorkersDomain(doc);
46
+ io.stdout(`${JSON.stringify({
47
+ event: "workers_domain_migrated",
48
+ file,
49
+ changed: m.changed,
50
+ dropped: m.dropped.map((d) => ({ name: d.name, field: formatConfigPath(d.field) })),
51
+ refused: m.refused.map((r) => ({ name: r.name, field: formatConfigPath(r.field), reason: r.reason })),
52
+ })}\n`);
53
+ if (m.refused.length > 0) {
54
+ io.stderr(`settings-schema: ${m.refused.length} worker(s) cannot be migrated (see "refused") — fix them by hand and re-run; nothing was written\n`);
55
+ return 1;
56
+ }
57
+ if (m.changed.length === 0)
58
+ return 0;
59
+ if (check)
60
+ return 3;
61
+ // 整域再判一次(域层规则:名字 / 路由唯一 …)—— 读不进 7.0.0 的文档不落盘。
62
+ const whole = DOMAIN_SCHEMAS.workers.safeParse(m.doc);
63
+ if (!whole.success) {
64
+ const issues = sanitizeZodError(whole.error).issues.map((i) => `${formatConfigPath(i.path)}: ${i.message}`);
65
+ io.stderr(`settings-schema: the migrated document does not read as a 7.0.0 workers domain — ${issues.join("; ")}; nothing was written\n`);
66
+ return 1;
67
+ }
68
+ await io.writeFileAtomic(file, `${JSON.stringify(m.doc, null, 2)}\n`);
69
+ return 0;
70
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * 包内私有:车道表({@link EXECUTION_LANES})的两只**派生索引**,供 `types.ts`(写闸)与 `migrate-workers.ts`(一次性迁移)同读。
3
+ * 不进主桶、不进 `package.json` 的 `exports`(与 `secret-scan.ts` 同一姿势)—— 消费方只拿表本身,索引是本包的实现细节。
4
+ * 索引只从表派生,不另存任何词:表改了,这里跟着变。
5
+ */
6
+ import { type ExecutionLane, type ExecutionLaneRow } from "./execution-lanes.js";
7
+ /** 一条车道的表行。词表与表行由 `execution-lanes.ts` 的 `satisfies Record<ExecutionLane, …>` 一一对应 ——
8
+ * 找不到 = 本包自身的不变量坏了(不是输入错),所以抛。 */
9
+ export declare function laneRowOf(lane: ExecutionLane): ExecutionLaneRow;
10
+ /** 一个车道 env 名归谁:哪条车道、密钥栏还是坐标栏、必需还是可选。 */
11
+ export interface LaneEnvOwner {
12
+ readonly lane: ExecutionLane;
13
+ readonly secret: boolean;
14
+ readonly required: boolean;
15
+ }
16
+ /** 车道 env 名 → 归属。四栏两两不交、跨车道不交(`test/execution-lanes-7.0.0.test.ts` 钉),所以每个名恰好一个归属。 */
17
+ export declare const LANE_ENV_OWNER: ReadonlyMap<string, LaneEnvOwner>;
18
+ /** 一个车道 env 名在 WorkerSpec 里的**唯一写位**(保留键的拒因指路用它)。 */
19
+ export declare function laneEnvSeat(name: string, owner: LaneEnvOwner): string;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * 包内私有:车道表({@link EXECUTION_LANES})的两只**派生索引**,供 `types.ts`(写闸)与 `migrate-workers.ts`(一次性迁移)同读。
3
+ * 不进主桶、不进 `package.json` 的 `exports`(与 `secret-scan.ts` 同一姿势)—— 消费方只拿表本身,索引是本包的实现细节。
4
+ * 索引只从表派生,不另存任何词:表改了,这里跟着变。
5
+ */
6
+ import { EXECUTION_LANES } from "./execution-lanes.js";
7
+ /** 一条车道的表行。词表与表行由 `execution-lanes.ts` 的 `satisfies Record<ExecutionLane, …>` 一一对应 ——
8
+ * 找不到 = 本包自身的不变量坏了(不是输入错),所以抛。 */
9
+ export function laneRowOf(lane) {
10
+ const row = EXECUTION_LANES.find((r) => r.lane === lane);
11
+ if (row === undefined)
12
+ throw new Error(`execution lane table has no row for "${lane}" (package invariant broken)`);
13
+ return row;
14
+ }
15
+ /** 车道 env 名 → 归属。四栏两两不交、跨车道不交(`test/execution-lanes-7.0.0.test.ts` 钉),所以每个名恰好一个归属。 */
16
+ export const LANE_ENV_OWNER = new Map(EXECUTION_LANES.flatMap((r) => {
17
+ const own = (names, secret, required) => names.map((k) => [k, { lane: r.lane, secret, required }]);
18
+ return [...own(r.requiredEnv, false, true), ...own(r.optionalEnv, false, false), ...own(r.requiredSecretEnv, true, true), ...own(r.optionalSecretEnv, true, false)];
19
+ }));
20
+ /** 一个车道 env 名在 WorkerSpec 里的**唯一写位**(保留键的拒因指路用它)。 */
21
+ export function laneEnvSeat(name, owner) {
22
+ return `runtime.execution.${owner.secret ? "secretRefs" : "coords"}.${name}`;
23
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * 执行车道 + 存储后端的**词表正本**(7.0.0,M-R2 WorkerSpec 结构块)。纯数据常量,零函数、零依赖(叶模块:
3
+ * 本文件不 import 任何东西,`types.ts` 反过来引它)。
4
+ *
5
+ * ## 为什么在这里
6
+ * 车道词、每条车道读哪些 env、哪条车道分租户、存储后端词 —— 这几张表以前在四处各抄一份(引擎的能力表、
7
+ * 部署面的车道清单、中心表单、本包 `RemoteExecSpec`),任何一处改了别处都不红。从本版起**只有这一份**:
8
+ * · 中心写闸 = `WorkerSpec.safeParse`(本包 schema 读这张表判 `runtime.execution` / `runtime.store`);
9
+ * · 中心渲染器按这张表把结构块渲成 env(唯一写者);
10
+ * · 引擎提货时从这张表派生自己的车道词表 / 必需键表 / 租户分离位(引擎只留运行期事实,按导入联合类型穷举);
11
+ * · 部署面车道清单由这张表生成。
12
+ * 写闸与起服读同一张表 ⇒ 不会分叉。
13
+ *
14
+ * ## 行内容的出处(逐行抄自引擎真码,坐标 = server 7.112.0 树)
15
+ * · 车道词与次序:`src/execution-lane-caps.ts` `EXECUTION_LANE_WORDS`(:335)去掉 `in-process` =
16
+ * `REMOTE_EXEC_PROVIDER_WORDS`(:366)。次序照抄,于是引擎改派生之后,拒启句里的 `valid:` 列表与出路列表逐字不变。
17
+ * · `tenantSeparation`:同文件 `executionLaneCaps` 每臂的 `tenantSeparation` 位(:209 host / :230 e2b / :249 k8s /
18
+ * :269 ssh / :288 adb / :308 local-docker / :327 device)。
19
+ * · `requiredEnv ∪ requiredSecretEnv`:`src/config.ts` `REQUIRED_ENVS_BY_LANE`(:1054)逐字;按 `src/config-catalog.ts`
20
+ * 该键那一行的值类型分栏 —— 类型 `secret` / `opaque`(目录自己「绝不回显值」的两类)进 `*SecretEnv`,其余进 `*Env`。
21
+ * · `optionalEnv ∪ optionalSecretEnv`:`src/config.ts` `parseOrchestrationDomain` 的 `REMOTE_EXEC` 三元链里**该车道那一臂**
22
+ * 读的 env 名(:3925 e2b / :3957 k8s / :3991 ssh / :4006 adb / :4014 host / :4026 local-docker / :4055 device;
23
+ * 臂内调用的解析器 `parseSshHostKeyEnv` :1521、`parseDevicePreAuthPerRemoteEnv` :871、`parseDeviceEnrollTokenTtlEnv` :857
24
+ * 读的名也算),去掉必需键,同样按目录值类型分栏。**不收**别的域也读的名:k8s 臂里的 `MINIO_ENDPOINT` /
25
+ * `MINIO_ACCESS_KEY` / `MINIO_SECRET_KEY` / `MINIO_REGION` 同时是存储域对象存储的键(`src/config.ts` :1848 / :1872),
26
+ * 收进车道栏会把它们保留成「k8s 专属」,别的车道就再也设不了快照对象存储。也**不收**臂外的适配器调参键
27
+ * (`HOST_BG_*` / `E2B_BG_*` / `K8S_BG_*` / `HOST_SHELL_PATH` …):它们不是车道坐标,照旧走 `env` 逃生口。
28
+ *
29
+ * ## 不在表里的词
30
+ * · `in-process`:引擎内部「没有 remoteExec」这个状态的名字,不是可声明的车道 —— 显式「无手」的 worker 在模型要用工具时
31
+ * 仍回 completed + 零工具,就是那个病换了个合法名字。要无手 worker 先另立设计、定用户面行为再给词。
32
+ * · `remote-docker`:引擎有一只没接线的适配器工厂,但没有任何读点能把这个词变成一条在跑的车道
33
+ * (`REMOTE_EXEC=remote-docker` 起服即拒;远端 docker 守护进程今天的真形 = `local-docker` + `DOCKER_HOST`)。
34
+ */
35
+ /** 可声明的执行车道闭集(= 引擎 `REMOTE_EXEC` 收的词,逐字同序)。 */
36
+ export declare const EXECUTION_LANE_WORDS: readonly ["host", "e2b", "k8s", "ssh", "adb", "local-docker", "device"];
37
+ export type ExecutionLane = (typeof EXECUTION_LANE_WORDS)[number];
38
+ /** 车道分不分租户(引擎能力表的 `tenantSeparation` 位,两档,没有「不表态」)。
39
+ * `supported` = 每个租户的命令落在各自的目标上;`single-user-only` = 所有任务落同一个共享目标 ⇒ 只许单用户 worker。 */
40
+ export declare const TENANT_SEPARATION_WORDS: readonly ["supported", "single-user-only"];
41
+ export type TenantSeparation = (typeof TENANT_SEPARATION_WORDS)[number];
42
+ /** 一条车道的表行。四栏 env 名两两不交、跨车道也不交(`test/execution-lanes-7.0.0.test.ts` 钉)。
43
+ * - `requiredEnv` / `optionalEnv`:非密坐标 —— 写在 `runtime.execution.coords`(字面值);
44
+ * - `requiredSecretEnv` / `optionalSecretEnv`:密钥 —— 写在 `runtime.execution.secretRefs`(服务变量名 → 宿主 env-NAME,绝不写值)。 */
45
+ export interface ExecutionLaneRow {
46
+ readonly lane: ExecutionLane;
47
+ readonly tenantSeparation: TenantSeparation;
48
+ readonly requiredEnv: readonly string[];
49
+ readonly requiredSecretEnv: readonly string[];
50
+ readonly optionalEnv: readonly string[];
51
+ readonly optionalSecretEnv: readonly string[];
52
+ }
53
+ /** 车道表(次序 = {@link EXECUTION_LANE_WORDS})。整张表深冻结 —— 它是唯一正本,消费方拿到的不许被改。 */
54
+ export declare const EXECUTION_LANES: readonly ExecutionLaneRow[];
55
+ /** 存储后端(→ `DB_BACKEND`)。= server `src/config-types.ts` `DB_BACKEND_WORDS`(:67)逐字同序。 */
56
+ export declare const STORE_BACKENDS: readonly ["mysql", "pg", "local", "memory"];
57
+ export type StoreBackend = (typeof STORE_BACKENDS)[number];
58
+ /** session 店(→ `SESSION_BACKEND`)。= server `src/config.ts` :1760 `enumEnv("SESSION_BACKEND", …)` 的词表逐字同序
59
+ * (`tidb` 不在集内:引擎见之拒启)。 */
60
+ export declare const SESSION_BACKENDS: readonly ["memory", "mysql", "auto"];
61
+ export type SessionBackend = (typeof SESSION_BACKENDS)[number];
62
+ /** 记忆持久面方言(→ `MEMORY_ENGINE_BACKEND`)。= server `src/config.ts` :3436 闭集 / 目录 :468 `enumValues` 逐字同序。 */
63
+ export declare const MEMORY_ENGINE_BACKENDS: readonly ["file", "pg", "tidb"];
64
+ export type MemoryEngineBackend = (typeof MEMORY_ENGINE_BACKENDS)[number];
@@ -0,0 +1,152 @@
1
+ /**
2
+ * 执行车道 + 存储后端的**词表正本**(7.0.0,M-R2 WorkerSpec 结构块)。纯数据常量,零函数、零依赖(叶模块:
3
+ * 本文件不 import 任何东西,`types.ts` 反过来引它)。
4
+ *
5
+ * ## 为什么在这里
6
+ * 车道词、每条车道读哪些 env、哪条车道分租户、存储后端词 —— 这几张表以前在四处各抄一份(引擎的能力表、
7
+ * 部署面的车道清单、中心表单、本包 `RemoteExecSpec`),任何一处改了别处都不红。从本版起**只有这一份**:
8
+ * · 中心写闸 = `WorkerSpec.safeParse`(本包 schema 读这张表判 `runtime.execution` / `runtime.store`);
9
+ * · 中心渲染器按这张表把结构块渲成 env(唯一写者);
10
+ * · 引擎提货时从这张表派生自己的车道词表 / 必需键表 / 租户分离位(引擎只留运行期事实,按导入联合类型穷举);
11
+ * · 部署面车道清单由这张表生成。
12
+ * 写闸与起服读同一张表 ⇒ 不会分叉。
13
+ *
14
+ * ## 行内容的出处(逐行抄自引擎真码,坐标 = server 7.112.0 树)
15
+ * · 车道词与次序:`src/execution-lane-caps.ts` `EXECUTION_LANE_WORDS`(:335)去掉 `in-process` =
16
+ * `REMOTE_EXEC_PROVIDER_WORDS`(:366)。次序照抄,于是引擎改派生之后,拒启句里的 `valid:` 列表与出路列表逐字不变。
17
+ * · `tenantSeparation`:同文件 `executionLaneCaps` 每臂的 `tenantSeparation` 位(:209 host / :230 e2b / :249 k8s /
18
+ * :269 ssh / :288 adb / :308 local-docker / :327 device)。
19
+ * · `requiredEnv ∪ requiredSecretEnv`:`src/config.ts` `REQUIRED_ENVS_BY_LANE`(:1054)逐字;按 `src/config-catalog.ts`
20
+ * 该键那一行的值类型分栏 —— 类型 `secret` / `opaque`(目录自己「绝不回显值」的两类)进 `*SecretEnv`,其余进 `*Env`。
21
+ * · `optionalEnv ∪ optionalSecretEnv`:`src/config.ts` `parseOrchestrationDomain` 的 `REMOTE_EXEC` 三元链里**该车道那一臂**
22
+ * 读的 env 名(:3925 e2b / :3957 k8s / :3991 ssh / :4006 adb / :4014 host / :4026 local-docker / :4055 device;
23
+ * 臂内调用的解析器 `parseSshHostKeyEnv` :1521、`parseDevicePreAuthPerRemoteEnv` :871、`parseDeviceEnrollTokenTtlEnv` :857
24
+ * 读的名也算),去掉必需键,同样按目录值类型分栏。**不收**别的域也读的名:k8s 臂里的 `MINIO_ENDPOINT` /
25
+ * `MINIO_ACCESS_KEY` / `MINIO_SECRET_KEY` / `MINIO_REGION` 同时是存储域对象存储的键(`src/config.ts` :1848 / :1872),
26
+ * 收进车道栏会把它们保留成「k8s 专属」,别的车道就再也设不了快照对象存储。也**不收**臂外的适配器调参键
27
+ * (`HOST_BG_*` / `E2B_BG_*` / `K8S_BG_*` / `HOST_SHELL_PATH` …):它们不是车道坐标,照旧走 `env` 逃生口。
28
+ *
29
+ * ## 不在表里的词
30
+ * · `in-process`:引擎内部「没有 remoteExec」这个状态的名字,不是可声明的车道 —— 显式「无手」的 worker 在模型要用工具时
31
+ * 仍回 completed + 零工具,就是那个病换了个合法名字。要无手 worker 先另立设计、定用户面行为再给词。
32
+ * · `remote-docker`:引擎有一只没接线的适配器工厂,但没有任何读点能把这个词变成一条在跑的车道
33
+ * (`REMOTE_EXEC=remote-docker` 起服即拒;远端 docker 守护进程今天的真形 = `local-docker` + `DOCKER_HOST`)。
34
+ */
35
+ /** 可声明的执行车道闭集(= 引擎 `REMOTE_EXEC` 收的词,逐字同序)。 */
36
+ export const EXECUTION_LANE_WORDS = ["host", "e2b", "k8s", "ssh", "adb", "local-docker", "device"];
37
+ /** 车道分不分租户(引擎能力表的 `tenantSeparation` 位,两档,没有「不表态」)。
38
+ * `supported` = 每个租户的命令落在各自的目标上;`single-user-only` = 所有任务落同一个共享目标 ⇒ 只许单用户 worker。 */
39
+ export const TENANT_SEPARATION_WORDS = ["supported", "single-user-only"];
40
+ /** 逐车道一行。`satisfies Record<ExecutionLane, …>` = 词表加词而这里不加行 ⇒ 编译红。 */
41
+ const LANE_ROWS = {
42
+ /** server `config.ts` :4014 臂(无必需键);`execution-lane-caps.ts` :209 `single-user-only`(所有任务落 worker 宿主)。 */
43
+ host: {
44
+ tenantSeparation: "single-user-only",
45
+ requiredEnv: [],
46
+ requiredSecretEnv: [],
47
+ optionalEnv: ["HOST_WORKSPACE_BASE", "HOST_COMMAND_TIMEOUT_MS"],
48
+ optionalSecretEnv: [],
49
+ },
50
+ /** server `config.ts` :3925 臂;`E2B_API_KEY` 目录 :351 类型 `secret`、`E2B_SANDBOX_ENV` :359 类型 `secret`;
51
+ * `execution-lane-caps.ts` :230 `supported`(每任务一只 microVM)。 */
52
+ e2b: {
53
+ tenantSeparation: "supported",
54
+ requiredEnv: [],
55
+ requiredSecretEnv: ["E2B_API_KEY"],
56
+ optionalEnv: ["E2B_TEMPLATE", "E2B_TIMEOUT_MS", "E2B_LIVENESS_MS", "E2B_ALLOW_NET", "E2B_MOUNT_PATH", "E2B_ORPHAN_RECLAIM"],
57
+ optionalSecretEnv: ["E2B_SANDBOX_ENV"],
58
+ },
59
+ /** server `config.ts` :3957 臂;`K8S_TOKEN` 目录 :412 类型 `secret`、`K8S_CA_CERT_B64` :403 类型 `opaque`;
60
+ * `execution-lane-caps.ts` :249 `supported`(每任务一只 pod)。`MINIO_*` 刻意不收(见文件头)。 */
61
+ k8s: {
62
+ tenantSeparation: "supported",
63
+ requiredEnv: ["K8S_SANDBOX_IMAGE"],
64
+ requiredSecretEnv: [],
65
+ optionalEnv: [
66
+ "K8S_API_URL",
67
+ "K8S_INSECURE_TLS",
68
+ "K8S_NAMESPACE",
69
+ "K8S_RUNTIME_CLASS",
70
+ "K8S_MOUNT_PATH",
71
+ "K8S_TIMEOUT_MS",
72
+ "K8S_MEMORY",
73
+ "K8S_CPU",
74
+ "WORKSPACE_SNAPSHOT_BUCKET",
75
+ "WORKSPACE_SNAPSHOT_PREFIX",
76
+ "WORKSPACE_SNAPSHOT_TTL_SEC",
77
+ ],
78
+ optionalSecretEnv: ["K8S_TOKEN", "K8S_CA_CERT_B64"],
79
+ },
80
+ /** server `config.ts` :3991 臂 + `parseSshHostKeyEnv` :1521;`SSH_PRIVATE_KEY` 目录 :687 类型 `secret`;
81
+ * `execution-lane-caps.ts` :269 `single-user-only`(一台跨任务共享的常驻真机)。 */
82
+ ssh: {
83
+ tenantSeparation: "single-user-only",
84
+ requiredEnv: ["SSH_HOST", "SSH_USER"],
85
+ requiredSecretEnv: ["SSH_PRIVATE_KEY"],
86
+ optionalEnv: ["SSH_PORT", "SSH_MOUNT_PATH", "SSH_HOST_FINGERPRINT", "SSH_KNOWN_HOSTS"],
87
+ optionalSecretEnv: [],
88
+ },
89
+ /** server `config.ts` :4006 臂;`execution-lane-caps.ts` :288 `single-user-only`(一台跨任务共享的设备)。 */
90
+ adb: {
91
+ tenantSeparation: "single-user-only",
92
+ requiredEnv: ["ADB_SERIAL"],
93
+ requiredSecretEnv: [],
94
+ optionalEnv: ["ADB_PATH", "ADB_MOUNT_PATH"],
95
+ optionalSecretEnv: [],
96
+ },
97
+ /** server `config.ts` :4026 臂;`DOCKER_SANDBOX_ENV` 目录 :345 类型 `secret`;`execution-lane-caps.ts` :308 `supported`
98
+ * (每任务一只容器)。`DOCKER_HOST` 在本行:远端 docker 守护进程 = 本车道 + `coords.DOCKER_HOST`。 */
99
+ "local-docker": {
100
+ tenantSeparation: "supported",
101
+ requiredEnv: ["DOCKER_IMAGE"],
102
+ requiredSecretEnv: [],
103
+ optionalEnv: [
104
+ "DOCKER_MOUNT_PATH",
105
+ "DOCKER_PATH",
106
+ "DOCKER_HOST",
107
+ "DOCKER_MEMORY",
108
+ "DOCKER_CPUS",
109
+ "DOCKER_PIDS_LIMIT",
110
+ "DOCKER_DROP_CAPS",
111
+ "DOCKER_NETWORK",
112
+ "DOCKER_COMMAND_TIMEOUT_MS",
113
+ ],
114
+ optionalSecretEnv: ["DOCKER_SANDBOX_ENV"],
115
+ },
116
+ /** server `config.ts` :4055 臂 + `parseDevicePreAuthPerRemoteEnv` :871 / `parseDeviceEnrollTokenTtlEnv` :857(无必需键:设备是运行期
117
+ * 连进来的);`execution-lane-caps.ts` :327 `supported`(发起者自己的设备)。 */
118
+ device: {
119
+ tenantSeparation: "supported",
120
+ requiredEnv: [],
121
+ requiredSecretEnv: [],
122
+ optionalEnv: [
123
+ "DEVICE_DELIVERY_TIMEOUT_MS",
124
+ "DEVICE_RECONNECT_GRACE_MS",
125
+ "DEVICE_EXEC_TIMEOUT_MS",
126
+ "DEVICE_MAX_INFLIGHT_PER_DEVICE",
127
+ "DEVICE_MAX_PREAUTH_PER_REMOTE",
128
+ "DEVICE_ENROLL_TOKEN_TTL_SEC",
129
+ "DEVICE_SUPERSEDE",
130
+ ],
131
+ optionalSecretEnv: [],
132
+ },
133
+ };
134
+ /** 车道表(次序 = {@link EXECUTION_LANE_WORDS})。整张表深冻结 —— 它是唯一正本,消费方拿到的不许被改。 */
135
+ export const EXECUTION_LANES = Object.freeze(EXECUTION_LANE_WORDS.map((lane) => {
136
+ const row = LANE_ROWS[lane];
137
+ return Object.freeze({
138
+ lane,
139
+ tenantSeparation: row.tenantSeparation,
140
+ requiredEnv: Object.freeze([...row.requiredEnv]),
141
+ requiredSecretEnv: Object.freeze([...row.requiredSecretEnv]),
142
+ optionalEnv: Object.freeze([...row.optionalEnv]),
143
+ optionalSecretEnv: Object.freeze([...row.optionalSecretEnv]),
144
+ });
145
+ }));
146
+ /** 存储后端(→ `DB_BACKEND`)。= server `src/config-types.ts` `DB_BACKEND_WORDS`(:67)逐字同序。 */
147
+ export const STORE_BACKENDS = ["mysql", "pg", "local", "memory"];
148
+ /** session 店(→ `SESSION_BACKEND`)。= server `src/config.ts` :1760 `enumEnv("SESSION_BACKEND", …)` 的词表逐字同序
149
+ * (`tidb` 不在集内:引擎见之拒启)。 */
150
+ export const SESSION_BACKENDS = ["memory", "mysql", "auto"];
151
+ /** 记忆持久面方言(→ `MEMORY_ENGINE_BACKEND`)。= server `src/config.ts` :3436 闭集 / 目录 :468 `enumValues` 逐字同序。 */
152
+ export const MEMORY_ENGINE_BACKENDS = ["file", "pg", "tidb"];
package/dist/file-edit.js CHANGED
@@ -95,29 +95,34 @@ export function listRequiredEnvNames(eff, remoteExec, resolvedEnv) {
95
95
  add(r.envName, use.domain, use.detail);
96
96
  if (remoteExec) {
97
97
  const re = remoteExec;
98
- if (re.provider === "e2b") {
99
- add(re.apiKeyEnv, "remoteExec", "e2b.apiKeyEnv");
100
- for (const e of re.sandboxEnv ?? [])
101
- add(e, "remoteExec", "e2b.sandboxEnv[]");
102
- }
103
- else if (re.provider === "k8s") {
104
- add(re.tokenEnv, "remoteExec", "k8s.tokenEnv");
105
- add(re.caCertEnv, "remoteExec", "k8s.caCertEnv");
106
- if (re.s3Snapshot) {
107
- add(re.s3Snapshot.accessKeyEnv, "remoteExec", "k8s.s3Snapshot.accessKeyEnv");
108
- add(re.s3Snapshot.secretKeyEnv, "remoteExec", "k8s.s3Snapshot.secretKeyEnv");
98
+ // Exhaustive over the provider words (7.0.0: ⊆ EXECUTION_LANE_WORDS) — a new provider that carries an env-NAME
99
+ // ref must say so here, or this switch is a compile error (no silent "carries nothing" default).
100
+ switch (re.provider) {
101
+ case "e2b":
102
+ add(re.apiKeyEnv, "remoteExec", "e2b.apiKeyEnv");
103
+ for (const e of re.sandboxEnv ?? [])
104
+ add(e, "remoteExec", "e2b.sandboxEnv[]");
105
+ break;
106
+ case "k8s":
107
+ add(re.tokenEnv, "remoteExec", "k8s.tokenEnv");
108
+ add(re.caCertEnv, "remoteExec", "k8s.caCertEnv");
109
+ if (re.s3Snapshot) {
110
+ add(re.s3Snapshot.accessKeyEnv, "remoteExec", "k8s.s3Snapshot.accessKeyEnv");
111
+ add(re.s3Snapshot.secretKeyEnv, "remoteExec", "k8s.s3Snapshot.secretKeyEnv");
112
+ }
113
+ break;
114
+ case "ssh":
115
+ add(re.privateKeyEnv, "remoteExec", "ssh.privateKeyEnv");
116
+ break;
117
+ case "adb":
118
+ case "host":
119
+ case "local-docker":
120
+ break; // these carry no secret env-NAME refs
121
+ default: {
122
+ const unhandled = re;
123
+ throw new Error(`listRequiredEnvNames: unhandled remoteExec provider ${JSON.stringify(unhandled)}`);
109
124
  }
110
125
  }
111
- else if (re.provider === "ssh") {
112
- add(re.privateKeyEnv, "remoteExec", "ssh.privateKeyEnv");
113
- }
114
- else if (re.provider === "remote-docker") {
115
- add(re.tlsCertEnv, "remoteExec", "remote-docker.tlsCertEnv");
116
- add(re.tlsKeyEnv, "remoteExec", "remote-docker.tlsKeyEnv");
117
- add(re.tlsCaEnv, "remoteExec", "remote-docker.tlsCaEnv");
118
- add(re.sshKeyEnv, "remoteExec", "remote-docker.sshKeyEnv");
119
- }
120
- // adb / host / local-docker carry no secret env-NAME refs.
121
126
  }
122
127
  return [...map.entries()]
123
128
  .map(([envName, refs]) => ({ envName, refs, present: resolvedEnv[envName] !== undefined }))
package/dist/hash.d.ts CHANGED
@@ -23,9 +23,9 @@ export declare const MAX_HASH_WRAPPER_RESERVE = 32;
23
23
  /** Depth cap for a standalone FREE-FORM config value, reconciled with the hash cap MINUS the wrapper reserve so
24
24
  * that ANYTHING the validator accepts is guaranteed hashable in every context it's later hashed in. 168 ≫ any
25
25
  * real free-form value (OpenAI-style request params nest <10 deep).
26
- * Scope: the ONE cap for EVERY free-form field vetted by `vetFreeFormJson` (`ModelEntry.extraBody` and, since
27
- * 1.10.0, `ModelEntry.compat`) — they sit at the SAME wrapper depth (`{models:{models:[{…}]}}`), so one cap
28
- * serves both. The `EXTRA_BODY` spelling is kept because this is a published export on the `./hash` subpath
26
+ * Scope: the ONE cap for EVERY free-form field vetted by `vetFreeFormJson` (`ModelEntry.extraBody`, since 1.10.0
27
+ * `ModelEntry.compat`, since 6.4.0 `ModelEntry.headers`) — they sit at the SAME wrapper depth (`{models:{models:[{…}]}}`),
28
+ * so one cap serves them all. The `EXTRA_BODY` spelling is kept because this is a published export on the `./hash` subpath
29
29
  * (renaming it would be a BREAKING change for a purely cosmetic gain). */
30
30
  export declare const MAX_EXTRA_BODY_DEPTH: number;
31
31
  /** Deterministic JSON with sorted object keys (so key order never affects the hash). */
package/dist/hash.js CHANGED
@@ -23,9 +23,9 @@ export const MAX_HASH_WRAPPER_RESERVE = 32;
23
23
  /** Depth cap for a standalone FREE-FORM config value, reconciled with the hash cap MINUS the wrapper reserve so
24
24
  * that ANYTHING the validator accepts is guaranteed hashable in every context it's later hashed in. 168 ≫ any
25
25
  * real free-form value (OpenAI-style request params nest <10 deep).
26
- * Scope: the ONE cap for EVERY free-form field vetted by `vetFreeFormJson` (`ModelEntry.extraBody` and, since
27
- * 1.10.0, `ModelEntry.compat`) — they sit at the SAME wrapper depth (`{models:{models:[{…}]}}`), so one cap
28
- * serves both. The `EXTRA_BODY` spelling is kept because this is a published export on the `./hash` subpath
26
+ * Scope: the ONE cap for EVERY free-form field vetted by `vetFreeFormJson` (`ModelEntry.extraBody`, since 1.10.0
27
+ * `ModelEntry.compat`, since 6.4.0 `ModelEntry.headers`) — they sit at the SAME wrapper depth (`{models:{models:[{…}]}}`),
28
+ * so one cap serves them all. The `EXTRA_BODY` spelling is kept because this is a published export on the `./hash` subpath
29
29
  * (renaming it would be a BREAKING change for a purely cosmetic gain). */
30
30
  export const MAX_EXTRA_BODY_DEPTH = MAX_HASH_DEPTH - MAX_HASH_WRAPPER_RESERVE;
31
31
  /** Deterministic JSON with sorted object keys (so key order never affects the hash). */