@sema-agent/server 7.112.0 → 7.114.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 (88) hide show
  1. package/MIGRATION.md +95 -0
  2. package/USAGE.md +4 -4
  3. package/dist/approval-ask-audit-store.d.ts +3 -4
  4. package/dist/approval-ask-audit-store.js +3 -3
  5. package/dist/approval-card.d.ts +4 -4
  6. package/dist/boot/coordinators.js +1 -1
  7. package/dist/boot/memory-boundary.d.ts +2 -1
  8. package/dist/boot/memory-consolidation.d.ts +1 -1
  9. package/dist/boot/resolve-spec.js +2 -2
  10. package/dist/boot/stores.js +4 -6
  11. package/dist/boot/workflow-orchestration.js +4 -4
  12. package/dist/config-catalog.js +6 -5
  13. package/dist/config-invariants.d.ts +27 -0
  14. package/dist/config-invariants.js +54 -39
  15. package/dist/config-types.d.ts +5 -11
  16. package/dist/config-types.js +0 -1
  17. package/dist/config.d.ts +0 -16
  18. package/dist/config.js +44 -69
  19. package/dist/env-facts.d.ts +3 -1
  20. package/dist/env-facts.js +5 -1
  21. package/dist/execution-lane-caps.d.ts +54 -30
  22. package/dist/execution-lane-caps.js +48 -20
  23. package/dist/hooks/hook-runner.d.ts +87 -7
  24. package/dist/hooks/hook-runner.js +113 -20
  25. package/dist/host-decision.d.ts +5 -3
  26. package/dist/hosted-posture.d.ts +2 -2
  27. package/dist/http/admission.js +8 -4
  28. package/dist/http/request-guards.js +2 -2
  29. package/dist/http/resume-legs.d.ts +9 -10
  30. package/dist/http/resume-legs.js +64 -18
  31. package/dist/http/route-ctx.d.ts +38 -8
  32. package/dist/http/route-ctx.js +4 -0
  33. package/dist/http/routes/background.js +2 -2
  34. package/dist/http/routes/capabilities.js +2 -2
  35. package/dist/http/routes/mcp-reconnect.js +2 -2
  36. package/dist/http/routes/notify-wake.js +3 -3
  37. package/dist/http/routes/runs.js +19 -13
  38. package/dist/http/routes/session-sync.js +6 -6
  39. package/dist/http/routes/sessions.js +20 -20
  40. package/dist/http/routes/tasks.js +1 -0
  41. package/dist/http/send.d.ts +1 -1
  42. package/dist/http/send.js +1 -1
  43. package/dist/http/wire-types.d.ts +1 -1
  44. package/dist/local-data-root.d.ts +14 -0
  45. package/dist/local-data-root.js +5 -0
  46. package/dist/observability/corrupt-read-seat.d.ts +46 -66
  47. package/dist/observability/corrupt-read-seat.js +79 -63
  48. package/dist/observability/fail-open.d.ts +28 -0
  49. package/dist/observability/fail-open.js +28 -0
  50. package/dist/observability/run-terminal.js +39 -0
  51. package/dist/orchestration/workflow-agent-session-index.d.ts +1 -5
  52. package/dist/orchestration/workflow-agent-session-index.js +2 -2
  53. package/dist/orchestration/workflow-notify-journal.d.ts +1 -3
  54. package/dist/orchestration/workflow-notify-journal.js +2 -2
  55. package/dist/plugins/approval-ask-store-file.d.ts +1 -4
  56. package/dist/plugins/approval-ask-store-file.js +2 -2
  57. package/dist/plugins/file-resume-anchor-store.d.ts +1 -6
  58. package/dist/plugins/file-resume-anchor-store.js +2 -4
  59. package/dist/plugins/file-run-store.d.ts +0 -4
  60. package/dist/plugins/file-run-store.js +1 -3
  61. package/dist/plugins/remote-env-host.d.ts +0 -35
  62. package/dist/plugins/remote-env-host.js +0 -8
  63. package/dist/plugins/remote-env-local-docker.d.ts +5 -13
  64. package/dist/plugins/remote-env-local-docker.js +2 -4
  65. package/dist/plugins/send-file-ledger.d.ts +1 -5
  66. package/dist/plugins/send-file-ledger.js +2 -2
  67. package/dist/plugins/store-backend.js +8 -9
  68. package/dist/plugins/user-input-ledger-file.d.ts +1 -5
  69. package/dist/plugins/user-input-ledger-file.js +2 -2
  70. package/dist/run-local.d.ts +1 -1
  71. package/dist/run-local.js +2 -2
  72. package/dist/runs.js +1 -0
  73. package/dist/spec-fields.js +0 -1
  74. package/dist/store-posture.d.ts +7 -7
  75. package/dist/store-posture.js +0 -1
  76. package/dist/task-settings.d.ts +13 -0
  77. package/dist/task-settings.js +3 -0
  78. package/dist/tool-approval.d.ts +1 -1
  79. package/dist/trace/core-keyset-guard.d.ts +1 -1
  80. package/dist/trace/project.js +2 -3
  81. package/dist/trace/redact.d.ts +18 -0
  82. package/dist/trace/redact.js +4 -0
  83. package/dist/unattended-permission-mode.d.ts +9 -2
  84. package/dist/unattended-permission-mode.js +5 -1
  85. package/dist/user-input-ledger.d.ts +24 -5
  86. package/dist/user-input-ledger.js +46 -11
  87. package/package.json +4 -3
  88. package/scripts/retired-mode-seed-drain-local.mjs +317 -0
@@ -3,7 +3,7 @@ import { modelProviderRefusal } from "./model-provider.js";
3
3
  import { modelRouteUrlRefusal } from "./model-route-url.js";
4
4
  import { modelRouteEndpointRefusal } from "./model-route-endpoint.js";
5
5
  import { defaultModelRefusal, refusedNameMintedRefusal } from "./model-entry-refusal.js";
6
- import { executionLaneCaps, executionLaneOf, laneCapabilityHolds, REMOTE_EXEC_PROVIDER_WORDS } from "./execution-lane-caps.js";
6
+ import { executionLaneCaps, executionLaneOf, executionLaneRequiredRefusal, laneCapabilityHolds, REMOTE_EXEC_PROVIDER_WORDS } from "./execution-lane-caps.js";
7
7
  export const LIVENESS_HEARTBEAT_MS = 30_000;
8
8
  const anyServiceAuth = (v) => Boolean(v.authToken) || Object.keys(v.authTokens ?? {}).length > 0;
9
9
  const mustNarrowBind = (v) => Boolean(v.allowUnauthedWrites) && !anyServiceAuth(v);
@@ -16,6 +16,51 @@ function multiTenantSharedLaneRefusal(v) {
16
16
  return (`REQUIRE_PRINCIPAL=true (multi-tenant posture) with the "${lane}" execution lane runs every tenant's Bash on one shared target — ` +
17
17
  `this lane does not separate tenants; set REMOTE_EXEC to ${outlets.join(" / ")}, or unset REQUIRE_PRINCIPAL for a single-user box`);
18
18
  }
19
+ export function halfOpenDirectDoorRefusal(v) {
20
+ if (!v.directApprovalDoor)
21
+ return undefined;
22
+ const missing = [];
23
+ if (!v.durableApproval)
24
+ missing.push("DURABLE_APPROVAL=true (the direct door's crypto gate only exists on the durable /decide path)");
25
+ if (v.approvalHmacKeys.length === 0)
26
+ missing.push("APPROVAL_HMAC_KEYS");
27
+ if (v.principalJwtPubkeys.length === 0)
28
+ missing.push("PRINCIPAL_JWT_PUBKEYS");
29
+ if (!v.principalJwtIss)
30
+ missing.push("PRINCIPAL_JWT_ISS");
31
+ if (!v.principalJwtAud)
32
+ missing.push("PRINCIPAL_JWT_AUD");
33
+ if (v.operatorPrincipals.length === 0)
34
+ missing.push("OPERATOR_PRINCIPALS (else isOperator is true-for-all)");
35
+ if (!v.requirePrincipal)
36
+ missing.push("REQUIRE_PRINCIPAL=true (a direct door is multi-tenant; without it an absent principal is anonymous and the self-asserted header governs owner/quota checks)");
37
+ return missing.length > 0 ? `DIRECT_APPROVAL_DOOR=true but D-G anchors are missing: ${missing.join(", ")} — refusing to start a half-open direct door (design/80 D-G boot invariant)` : undefined;
38
+ }
39
+ export function operatorPrincipalsRequiredRefusal(v) {
40
+ return v.durableApproval === true && v.requirePrincipal && v.operatorPrincipals.length === 0
41
+ ? "DURABLE_APPROVAL=true + REQUIRE_PRINCIPAL=true but OPERATOR_PRINCIPALS is empty — on a multi-tenant worker the empty operator set makes isOperator() true-for-all, so every authenticated tenant could read every other tenant's pending-approval queue (GET /v1/approvals & siblings). Set OPERATOR_PRINCIPALS, or unset REQUIRE_PRINCIPAL if this is a single-tenant deployment (#157-① boot invariant)"
42
+ : undefined;
43
+ }
44
+ export function bakeOperatorPrincipalsRefusal(v) {
45
+ return v.imageBakes.enabled && v.operatorPrincipals.length === 0
46
+ ? "IMAGE_BAKES_ENABLED=true but OPERATOR_PRINCIPALS is empty — the bake door runs build.sh on the privileged build host; an empty operator set makes isOperator() true-for-all (world-writable RCE). Set OPERATOR_PRINCIPALS or disable bakes (IMAGE-API-DESIGN.md §P2.4a boot invariant)"
47
+ : undefined;
48
+ }
49
+ export function bakeRunnerCredentialRefusal(v) {
50
+ const runner = v.imageBakes.runnerPrincipal;
51
+ return v.imageBakes.enabled && !Object.values(v.authTokens ?? {}).includes(runner)
52
+ ? `IMAGE_BAKES_ENABLED=true but no SERVICE_AUTH_TOKENS entry maps to the runner principal '${runner}' — the bake-runner authenticates by its credential (token-derived source), so a '<BAKE_RUNNER_TOKEN>=${runner}' SERVICE_AUTH_TOKENS entry is required or claim/ingest/heartbeat all 403 (IMAGE-API-DESIGN.md §P2.12 boot invariant)`
53
+ : undefined;
54
+ }
55
+ export function leaderDurableStoreRefusal(v) {
56
+ return v.leaderEnabled && v.dbBackend !== "mysql" && v.dbBackend !== "pg"
57
+ ? `LEADER_ENABLED=true but DB_BACKEND=${v.dbBackend} (no external SQL store) is refused: a leader run is an ` +
58
+ "N-worker fan-out lasting up to 24h that ends in a REAL git push, and without a durable store its registry is " +
59
+ "a per-process Map — a cross-replica GET 404s, a restart forgets the run, and a retried POST starts a SECOND " +
60
+ "push. Point the service at a durable store (DB_BACKEND=mysql|pg), or turn the leader off (unset LEADER_ENABLED) " +
61
+ "(#193 车4 件2 / staleness ledger P1-9+P1-10 boot invariant)"
62
+ : undefined;
63
+ }
19
64
  export const CONFIG_CROSS_INVARIANTS = [
20
65
  {
21
66
  id: "run-stale-heartbeat",
@@ -39,57 +84,31 @@ export const CONFIG_CROSS_INVARIANTS = [
39
84
  keys: ["imageBakes", "authTokens"],
40
85
  restartReason: "bake-runner-credential",
41
86
  bootAnchor: { file: "src/config.ts", marker: "CROSS-INVARIANT:bake-runner-credential" },
42
- check: ({ next }) => next.imageBakes.enabled && !Object.values(next.authTokens ?? {}).includes(next.imageBakes.runnerPrincipal)
43
- ? `IMAGE_BAKES_ENABLED is on but no SERVICE_AUTH_TOKENS entry maps to the runner principal '${next.imageBakes.runnerPrincipal}' — claim/ingest/heartbeat would all 403`
44
- : undefined,
87
+ check: ({ next }) => bakeRunnerCredentialRefusal(next),
45
88
  },
46
89
  {
47
90
  id: "direct-door-anchors",
48
91
  keys: ["directApprovalDoor", "durableApproval", "approvalHmacKeys", "principalJwtPubkeys", "principalJwtIss", "principalJwtAud", "requirePrincipal", "operatorPrincipals"],
49
92
  restartReason: "direct-door",
50
93
  bootAnchor: { file: "src/config.ts", marker: "CROSS-INVARIANT:direct-door-anchors" },
51
- check: ({ live, next }) => {
52
- if (next.directApprovalDoor) {
53
- const missing = [];
54
- if (!next.durableApproval)
55
- missing.push("DURABLE_APPROVAL");
56
- if (next.approvalHmacKeys.length === 0)
57
- missing.push("APPROVAL_HMAC_KEYS");
58
- if (next.principalJwtPubkeys.length === 0)
59
- missing.push("PRINCIPAL_JWT_PUBKEYS");
60
- if (!next.principalJwtIss)
61
- missing.push("PRINCIPAL_JWT_ISS");
62
- if (!next.principalJwtAud)
63
- missing.push("PRINCIPAL_JWT_AUD");
64
- if (next.operatorPrincipals.length === 0)
65
- missing.push("OPERATOR_PRINCIPALS");
66
- if (!next.requirePrincipal)
67
- missing.push("REQUIRE_PRINCIPAL");
68
- if (missing.length > 0)
69
- return `a half-open direct approval door is refused: missing ${missing.join(", ")}`;
70
- }
71
- return directDoorConjuncts(live) !== directDoorConjuncts(next)
94
+ check: ({ live, next }) => halfOpenDirectDoorRefusal(next) ??
95
+ (directDoorConjuncts(live) !== directDoorConjuncts(next)
72
96
  ? "the direct-door posture (its conjunction of approval/JWT anchors) is decided at boot — refusing to hot-flip it (restart to change the door)"
73
- : undefined;
74
- },
97
+ : undefined),
75
98
  },
76
99
  {
77
100
  id: "operator-principals-required",
78
101
  keys: ["durableApproval", "requirePrincipal", "operatorPrincipals"],
79
102
  restartReason: "operator-principals",
80
103
  bootAnchor: { file: "src/config.ts", marker: "CROSS-INVARIANT:operator-principals-required" },
81
- check: ({ next }) => next.durableApproval === true && next.requirePrincipal && next.operatorPrincipals.length === 0
82
- ? "DURABLE_APPROVAL + REQUIRE_PRINCIPAL with an EMPTY OPERATOR_PRINCIPALS makes isOperator() true-for-all — every authenticated tenant could read every other tenant's pending-approval queue"
83
- : undefined,
104
+ check: ({ next }) => operatorPrincipalsRequiredRefusal(next),
84
105
  },
85
106
  {
86
107
  id: "bake-operator-principals",
87
108
  keys: ["imageBakes", "operatorPrincipals"],
88
109
  restartReason: "operator-principals",
89
110
  bootAnchor: { file: "src/config.ts", marker: "CROSS-INVARIANT:bake-operator-principals" },
90
- check: ({ next }) => next.imageBakes.enabled && next.operatorPrincipals.length === 0
91
- ? "IMAGE_BAKES_ENABLED with an EMPTY OPERATOR_PRINCIPALS is a world-writable bake door (the runner runs build.sh on the privileged build host)"
92
- : undefined,
111
+ check: ({ next }) => bakeOperatorPrincipalsRefusal(next),
93
112
  },
94
113
  {
95
114
  id: "sensitive-write-patterns",
@@ -111,9 +130,7 @@ export const CONFIG_CROSS_INVARIANTS = [
111
130
  keys: ["requireExecutionLane", "remoteExec"],
112
131
  restartReason: "execution-lane",
113
132
  bootAnchor: { file: "src/config.ts", marker: "CROSS-INVARIANT:require-execution-lane" },
114
- check: ({ next }) => next.requireExecutionLane && next.remoteExec === undefined
115
- ? "REQUIRE_EXECUTION_LANE=true with no execution lane named (REMOTE_EXEC unset/empty and CONFIG_PROVIDER not \"local\") — the deployment asked to refuse this state"
116
- : undefined,
133
+ check: ({ next }) => executionLaneRequiredRefusal(next),
117
134
  },
118
135
  {
119
136
  id: "leader-host-lane",
@@ -142,9 +159,7 @@ export const CONFIG_CROSS_INVARIANTS = [
142
159
  keys: ["leaderEnabled", "dbBackend"],
143
160
  restartReason: "leader-lane",
144
161
  bootAnchor: { file: "src/config.ts", marker: "CROSS-INVARIANT:leader-durable-store" },
145
- check: ({ next }) => next.leaderEnabled && next.dbBackend !== "mysql" && next.dbBackend !== "pg"
146
- ? `LEADER_ENABLED with DB_BACKEND=${next.dbBackend} (no external SQL store) degrades the leader-run registry to a per-process Map`
147
- : undefined,
162
+ check: ({ next }) => leaderDurableStoreRefusal(next),
148
163
  },
149
164
  {
150
165
  id: "device-durable-store",
@@ -18,6 +18,7 @@ import type { InfraCostRates } from "./observability/cost-taxonomy.js";
18
18
  import type { Autonomy, CommandRule } from "./runtime-governance.js";
19
19
  import type { CenterCredentialSource } from "./center-credential.js";
20
20
  import type { GitApiKind } from "./git-api-kind.js";
21
+ import type { MemoryEngineBackend, StoreBackend as StoreBackendWord } from "@sema-agent/settings-schema/execution-lanes";
21
22
  import { DRAIN_GRACE_FLOOR_MS } from "@sema-agent/settings-schema/fleet";
22
23
  /** [ref]:记忆向量面的 embedder 坐标(env `MEMORY_EMBEDDER_*` 解析结果;单一属主 = 本类型,
23
24
  * `plugins/memory-embedder.ts` 的工厂直接消费它,不复制形状)。语义与三问见
@@ -54,13 +55,6 @@ export type RetentionMode = (typeof RETENTION_MODES)[number];
54
55
  */
55
56
  export declare const CONFIG_PROVIDER_WORDS: readonly ["local", "remote"];
56
57
  export type ConfigProviderWord = (typeof CONFIG_PROVIDER_WORDS)[number];
57
- /**
58
- * S-866 ① —— `DB_BACKEND` 的闭集词表(**一处**:`config.ts` 的枚举解析、配置目录的 enum 行、caps `store.declared` / `store.backend`
59
- * 三处共读;S-2 设计稿 v1 写的契约包 `STORE_ENGINES` 不存在,搬进契约包是 [ref] A-5 的另票)。`mysql` = 任一 MySQL 协议服务器
60
- * (MySQL / TiDB / MariaDB);`local` = 本机文件店;`memory` = 纯内存(显式 opt-out,或多租裸 boot 的缺省)。
61
- */
62
- export declare const DB_BACKEND_WORDS: readonly ["mysql", "pg", "local", "memory"];
63
- export type DbBackendWord = (typeof DB_BACKEND_WORDS)[number];
64
58
  /** [ref] §3/§4 —— sweep lane 的节律与灰度档(语义与三问见 {@link ServiceConfigFlat.retentionSweep})。 */
65
59
  export interface RetentionSweepConfig {
66
60
  /** `audit-only` = 照跑照判、只记 `audit_only` 行,**不调任何破坏性方法**;`enforce` = 真删。默认前者。 */
@@ -702,7 +696,7 @@ export interface ServiceConfigFlat {
702
696
  * pg 部署的检索**恒 lexical**(词面 jaccard),向量列空转。配上这几键 = 该部署真正拿到语义检索。
703
697
  * ② **半配为何拒启**:三键里少任何一件,「向量面」都无从组装。静默忽略的后果不是功能缺席,而是
704
698
  * **operator 以为开了**——检索照常返回结果(词面档),质量差异只在最终答案里,任何日志都不报。
705
- * 所以照 `MEMORY_SYNC_*` / `REQUIRED_ENVS_BY_LANE` 的同款姿势整拒并点名缺哪几键。
699
+ * 所以照 `MEMORY_SYNC_*` / [ref] 车道必需键(`laneRequiredEnv`)的同款姿势整拒并点名缺哪几键。
706
700
  * ③ **维度守卫为何是拒启不是 warn**:向量列写错维度是**静默毒库**形([ref] 裁③)——错长度的向量
707
701
  * 既不会让 DB 报错(本仓 pg 记忆表的 embedding 是 `jsonb`,无维度约束),也不会让检索报错(维度
708
702
  * 不匹配的行只是悄悄退回词面档),只会让「向量面开着」这句话变成谎。所以坏 DIM(非正整数)在
@@ -1003,7 +997,7 @@ export interface ServiceConfigFlat {
1003
997
  * MYSQL_* replacement named (see RETIRED_ENVS in config.ts). `mysql` reads MYSQL_* coords only.
1004
998
  * TiDB itself stays fully supported (any MySQL-protocol engine works — memory no longer needs a VECTOR
1005
999
  * column since [ref] moved it onto the file engine). Env: DB_BACKEND. */
1006
- dbBackend: DbBackendWord;
1000
+ dbBackend: StoreBackendWord;
1007
1001
  /** clay 拍(2026-07-27):operator 是否显式设了 DB_BACKEND。main.ts 的降级判据——**默认推导**的
1008
1002
  * local(裸 boot)在 mkdir/ensure 失败时降级 memory + warn + gauge(auto-DB 不可达先例);
1009
1003
  * **显式** DB_BACKEND=local 失败仍 fail-loud(operator 要了 durable,静默丢=丢数据)。 */
@@ -1884,7 +1878,7 @@ export interface ServiceConfigFlat {
1884
1878
  * 结束等待的确实是本仓的窗;其余三臂结束等待的是容量/连接/装配,自报恒缺席。
1885
1879
  *
1886
1880
  * 🔴 **`deny` 交给引擎的形(S-689)**:部署政策拒对象臂 `{allow:false, settledBy:"policy", reason}` ⇒ 引擎结算
1887
- * `policy_refused`(`who.party:"none"`)、模型读策略句(「部署政策拒了、此处没有交互审批,别重试、做不需审批的
1881
+ * `policy_refused`(`who.party:"policy"` —— core 7.39.0 #1265 起;此前 `"none"`)、模型读策略句(「部署政策拒了、此处没有交互审批,别重试、做不需审批的
1888
1882
  * 部分」)、run 不以「用户拒绝」收束而是继续。修前是裸 `false` —— 引擎契约里那就是「一个人拒了」
1889
1883
  * (`human_refused` / `haltedOnUserRejection`),与本键「没有人会来」的自声明正好相反。(b)(c) 两臂照旧带
1890
1884
  * `"timeout"` 自报(`approval_window_expired`)。
@@ -2053,7 +2047,7 @@ export interface ServiceConfigFlat {
2053
2047
  * 404 there (P1-2: our own repos live on GitHub and were unreviewable). Unknown word = refuse boot. */
2054
2048
  gitApiKind: GitApiKind;
2055
2049
  /** S3-TOB 设计 §1.3:memory 持久面方言(env MEMORY_ENGINE_BACKEND,缺省 file)。boot-only。 */
2056
- memoryEngineBackend: "file" | "pg" | "tidb";
2050
+ memoryEngineBackend: MemoryEngineBackend;
2057
2051
  /** CC `workflowSizeGuideline` (advisory, injected into the Workflow tool card via
2058
2052
  * core `RunnerDeps.workflowLimits.sizeGuideline`). env WORKFLOW_SIZE_GUIDELINE; unset = absent =
2059
2053
  * byte-compat (core treats missing/unrestricted identically). boot-only (per-task card snapshot). */
@@ -2,7 +2,6 @@ import { DRAIN_GRACE_FLOOR_MS } from "@sema-agent/settings-schema/fleet";
2
2
  export const TASK_WRITE_FACE_ESCALATIONS = ["allow", "refuse"];
3
3
  export const RETENTION_MODES = ["audit-only", "enforce"];
4
4
  export const CONFIG_PROVIDER_WORDS = ["local", "remote"];
5
- export const DB_BACKEND_WORDS = ["mysql", "pg", "local", "memory"];
6
5
  export const MEMORY_CONSOLIDATION_MIN_INTERVAL_SEC = 300;
7
6
  export const MAX_TIMER_DELAY_MS = 2_147_483_647;
8
7
  export const AUTO_MODE_CLASSIFIER_TIMEOUT_BOUNDS = { minMs: 1_000, maxMs: MAX_TIMER_DELAY_MS };
package/dist/config.d.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import { type Model, type PathRuleBases, type RuleBehavior } from "@sema-agent/core";
2
- import { type RemoteExecProvider } from "./execution-lane-caps.js";
3
2
  import { type Autonomy } from "./runtime-governance.js";
4
3
  import { type RunnerEnvNameAllowlistKnob, type ServerEnvNameAllowlistKnob } from "./env-name-allowlist-knobs.js";
5
4
  import type { ServiceConfig, ServiceConfigFlat, ServiceConfigGroups } from "./config-types.js";
@@ -252,21 +251,6 @@ export declare const configLkgEnabled: () => boolean;
252
251
  export declare const hostBackgroundShellEnabled: () => boolean;
253
252
  /** The `host` exec lane's spool-file stdio form; false ⇒ the pre-1.226 pipe fallback (read per exec). */
254
253
  export declare const hostExecSpoolEnabled: () => boolean;
255
- /**
256
- * 每条执行车道**必需的 env 键名**([ref] 的 fail-closed 拒启门按它点名缺件;见 `parseOrchestrationDomain`
257
- * 里那处 `REMOTE_EXEC` 校验)。
258
- *
259
- * 键集 = `RemoteExecProvider`(判别联合派生,`execution-lane-caps.ts`):配置面加一条 provider 臂而
260
- * 这里不加行 = **编译红**(codex R1-F2)。此前是 `Record<string, …>`,漏一行只表现为该词在 boot 期被判
261
- * 「不是执行车道」—— fail-closed 但把「新车道」讲成「拼错了」,运维照着报文永远查不到成因。
262
- *
263
- * 🔴 **S-599:提到模块顶层导出**(此前是 `parseOrchestrationDomain` 的函数内局部)。零语义变,动机是
264
- * 部署面需要**同一张表**:`deploy/sema-up/lib/lanes.json` 的每行必须登记它那条车道的必需键名,根 /
265
- * review compose 的 environment 白名单必须把**全部**车道的必需键透传出去 —— 那两处若各手抄一份名单,
266
- * 加一条车道就会静默分叉(选得到、起不来,而 config 的拒启报文里那个键在 compose 里根本不存在)。
267
- * 门 = `test/deploy-lanes-contract.test.ts`,它读的是本表而不是任何副本。
268
- */
269
- export declare const REQUIRED_ENVS_BY_LANE: Record<RemoteExecProvider, readonly string[]>;
270
254
  /** 名单型 env 旋钮的**唯一**解析器(规则全文见上方 `ENV_NAME_ALLOWLIST_KNOBS` 头注)。`undefined` = 没配(未设 / 空串)。
271
255
  * 服务侧旋钮要交本进程的姿态(③);runner 旋钮没有姿态,签名上就不收 —— 读者与行形由叶表的类型一起定,传错是编译红。 */
272
256
  export declare function parseEnvNameAllowlist(knob: ServerEnvNameAllowlistKnob, singleUserTurnkey: boolean): readonly string[] | undefined;
package/dist/config.js CHANGED
@@ -10,14 +10,17 @@ import { DEFAULT_ELICITATION_THROTTLE } from "./elicitation.js";
10
10
  import { isV2ScopeKey, memoryReplicationCoverage, replicationCovers } from "./memory-scope.js";
11
11
  import { DEFAULT_QUESTION_THROTTLE } from "./question.js";
12
12
  import { GIT_API_KINDS, isGitApiKind } from "./git-api-kind.js";
13
- import { CONFIG_PROVIDER_WORDS, DB_BACKEND_WORDS, RETENTION_MODES, MEMORY_CONSOLIDATION_MIN_INTERVAL_SEC, RESILIENCE_DEFAULTS, TASK_WRITE_FACE_ESCALATIONS, MAX_TIMER_DELAY_MS, AUTO_MODE_CLASSIFIER_TIMEOUT_BOUNDS, DRAIN_GRACE_FLOOR_MS, DRAIN_SECOND_SIGNAL_MIN_GAP_BOUNDS, DRAIN_SECOND_SIGNAL_MIN_GAP_DEFAULT_MS, FIRST_BYTE_TIMEOUT_BOUNDS } from "./config-types.js";
13
+ import { CONFIG_PROVIDER_WORDS, RETENTION_MODES, MEMORY_CONSOLIDATION_MIN_INTERVAL_SEC, RESILIENCE_DEFAULTS, TASK_WRITE_FACE_ESCALATIONS, MAX_TIMER_DELAY_MS, AUTO_MODE_CLASSIFIER_TIMEOUT_BOUNDS, DRAIN_GRACE_FLOOR_MS, DRAIN_SECOND_SIGNAL_MIN_GAP_BOUNDS, DRAIN_SECOND_SIGNAL_MIN_GAP_DEFAULT_MS, FIRST_BYTE_TIMEOUT_BOUNDS } from "./config-types.js";
14
+ import { resolveLocalDataRoot } from "./local-data-root.js";
14
15
  import { buildWriteProtectionPosture } from "./write-protection.js";
15
16
  import { normalizeSshFingerprint } from "./ssh-host-key.js";
16
17
  import { buildReadFacePostureNotice, readFaceForPosture } from "./read-face-posture.js";
17
18
  import { THINKING_LEVELS, THINKING_FORMATS, buildModelCompat, isThinkingFormat, isThinkingTier } from "./model-compat.js";
18
19
  import {} from "./posture-source.js";
19
20
  import { CONTROL_AND_BIDI_STRIP_RE } from "./text-bidi.js";
20
- import { REMOTE_EXEC_PROVIDER_WORDS, executionLaneOf, isSingleMachineDefaultLane } from "./execution-lane-caps.js";
21
+ import { bakeOperatorPrincipalsRefusal, bakeRunnerCredentialRefusal, halfOpenDirectDoorRefusal, leaderDurableStoreRefusal, operatorPrincipalsRequiredRefusal } from "./config-invariants.js";
22
+ import { REMOTE_EXEC_PROVIDER_WORDS, executionLaneOf, executionLaneRequiredRefusal, isSingleMachineDefaultLane, laneRequiredEnv } from "./execution-lane-caps.js";
23
+ import { MEMORY_ENGINE_BACKENDS, SESSION_BACKENDS, STORE_BACKENDS } from "@sema-agent/settings-schema/execution-lanes";
21
24
  import { AUTONOMY_MODES } from "./runtime-governance.js";
22
25
  import { createCenterCredentialSource, readCenterCredentialDeclaration } from "./center-credential.js";
23
26
  import { parseCenterCredentialRenewLeadMs } from "./center-credential-renewer.js";
@@ -483,15 +486,6 @@ function boolEnv(name, def) {
483
486
  export const configLkgEnabled = () => boolEnv("CONFIG_LKG_ENABLED", true);
484
487
  export const hostBackgroundShellEnabled = () => boolEnv("HOST_BG_ENABLED", true);
485
488
  export const hostExecSpoolEnabled = () => boolEnv("HOST_EXEC_SPOOL_ENABLED", true);
486
- export const REQUIRED_ENVS_BY_LANE = {
487
- e2b: ["E2B_API_KEY"],
488
- k8s: ["K8S_SANDBOX_IMAGE"],
489
- ssh: ["SSH_HOST", "SSH_USER", "SSH_PRIVATE_KEY"],
490
- adb: ["ADB_SERIAL"],
491
- host: [],
492
- "local-docker": ["DOCKER_IMAGE"],
493
- device: [],
494
- };
495
489
  function parseReadDenyPatternsEnv(raw) {
496
490
  let arr;
497
491
  try {
@@ -861,7 +855,7 @@ function parseStoreDomain(ctx) {
861
855
  !!(process.env.MYSQL_HOST || process.env.PG_HOST);
862
856
  const bareBoot = !dbBackendSet && !sqlSignal;
863
857
  const multiTenantBareBoot = bareBoot && requirePrincipal;
864
- const dbBackend = enumEnv("DB_BACKEND", bareBoot ? (multiTenantBareBoot ? "memory" : "local") : "mysql", DB_BACKEND_WORDS);
858
+ const dbBackend = enumEnv("DB_BACKEND", bareBoot ? (multiTenantBareBoot ? "memory" : "local") : "mysql", STORE_BACKENDS);
865
859
  if (multiTenantBareBoot) {
866
860
  CONFIG_NOTICES.push({
867
861
  level: "warn",
@@ -872,9 +866,9 @@ function parseStoreDomain(ctx) {
872
866
  });
873
867
  }
874
868
  const sqlEngineExplicit = !!process.env.DB_BACKEND && dbBackend !== "local" && dbBackend !== "memory";
875
- const sessionBackendRaw = enumEnv("SESSION_BACKEND", sqlEngineExplicit ? "mysql" : "memory", ["memory", "mysql", "auto"]);
869
+ const sessionBackendRaw = enumEnv("SESSION_BACKEND", sqlEngineExplicit ? "mysql" : "memory", SESSION_BACKENDS);
876
870
  const sessionBackend = sessionBackendRaw === "mysql" ? "tidb" : sessionBackendRaw;
877
- const localDataRoot = process.env.LOCAL_DATA_ROOT || process.env.CONFIG_LOCAL_DIR || process.env.AGENT_DATA_DIR || join(homedir(), ".ai-agent");
871
+ const localDataRoot = resolveLocalDataRoot(process.env, homedir());
878
872
  const dbHostSet = dbBackend === "pg" ? !!process.env.PG_HOST : !!process.env.MYSQL_HOST;
879
873
  const needsTidb = sessionBackend === "tidb" || (sessionBackend === "auto" && dbHostSet);
880
874
  const needsDb = needsTidb;
@@ -1644,11 +1638,7 @@ function parseMemoryConsolidationDriver() {
1644
1638
  function parseMemoryDomain(ctx) {
1645
1639
  const { requirePrincipal } = ctx;
1646
1640
  const memoryEngineEnabled = parseMemoryEngineWord(env("MEMORY_ENGINE", "on"));
1647
- const memoryEngineBackendRaw = process.env.MEMORY_ENGINE_BACKEND ?? "file";
1648
- if (!["file", "pg", "tidb"].includes(memoryEngineBackendRaw)) {
1649
- throw new Error(`MEMORY_ENGINE_BACKEND must be file|pg|tidb, got "${memoryEngineBackendRaw}"`);
1650
- }
1651
- const memoryEngineBackend = memoryEngineBackendRaw;
1641
+ const memoryEngineBackend = enumEnv("MEMORY_ENGINE_BACKEND", "file", MEMORY_ENGINE_BACKENDS);
1652
1642
  const memoryScope = process.env.MEMORY_SCOPE ?? (memoryEngineEnabled && !requirePrincipal ? "local" : undefined);
1653
1643
  const memorySyncUrl = process.env.MEMORY_SYNC_URL || undefined;
1654
1644
  const memorySyncToken = process.env.MEMORY_SYNC_TOKEN || undefined;
@@ -1864,12 +1854,12 @@ function parseOrchestrationDomain(ctx) {
1864
1854
  {
1865
1855
  const lane = process.env.REMOTE_EXEC;
1866
1856
  if (lane !== undefined && lane !== "") {
1867
- const required = Object.entries(REQUIRED_ENVS_BY_LANE).find(([word]) => word === lane)?.[1];
1868
- if (required === undefined) {
1869
- throw new Error(`REMOTE_EXEC="${lane}" is not an execution lane — valid: ${Object.keys(REQUIRED_ENVS_BY_LANE).join(" | ")}. ` +
1857
+ const word = REMOTE_EXEC_PROVIDER_WORDS.find((w) => w === lane);
1858
+ if (word === undefined) {
1859
+ throw new Error(`REMOTE_EXEC="${lane}" is not an execution lane — valid: ${REMOTE_EXEC_PROVIDER_WORDS.join(" | ")}. ` +
1870
1860
  `An unrecognized value used to fall through to the host/in-process lane silently; refusing to start instead (#157).`);
1871
1861
  }
1872
- const missing = required.filter((name) => !process.env[name]);
1862
+ const missing = laneRequiredEnv(word).filter((name) => !process.env[name]);
1873
1863
  if (missing.length > 0) {
1874
1864
  throw new Error(`REMOTE_EXEC=${lane} but required env ${missing.join(", ")} ${missing.length === 1 ? "is" : "are"} missing/empty — ` +
1875
1865
  `refusing to start rather than silently downgrading to the host/in-process lane (#157; unset REMOTE_EXEC if you meant no sandbox lane).`);
@@ -2406,54 +2396,39 @@ export function loadConfig() {
2406
2396
  const integrations = parseIntegrationsDomain({ requirePrincipal, configProvider });
2407
2397
  const { directApprovalDoor, durableApproval: dgDurable, approvalHmacKeys: dgHmacKeys } = approval;
2408
2398
  const { principalJwtPubkeys, principalJwtIss, principalJwtAud, operatorPrincipals: dgOperators, authTokens } = auth;
2409
- const { enabled: imageBakesEnabled, runnerPrincipal: bakeRunnerPrincipal } = orchestration.imageBakes;
2410
2399
  const directDoorActive = directApprovalDoor && dgDurable && dgHmacKeys.length > 0 && principalJwtPubkeys.length > 0 && !!principalJwtIss && !!principalJwtAud;
2411
- if (directApprovalDoor) {
2412
- const missing = [];
2413
- if (!dgDurable)
2414
- missing.push("DURABLE_APPROVAL=true (the direct door's crypto gate only exists on the durable /decide path)");
2415
- if (dgHmacKeys.length === 0)
2416
- missing.push("APPROVAL_HMAC_KEYS");
2417
- if (principalJwtPubkeys.length === 0)
2418
- missing.push("PRINCIPAL_JWT_PUBKEYS");
2419
- if (!principalJwtIss)
2420
- missing.push("PRINCIPAL_JWT_ISS");
2421
- if (!principalJwtAud)
2422
- missing.push("PRINCIPAL_JWT_AUD");
2423
- if (dgOperators.length === 0)
2424
- missing.push("OPERATOR_PRINCIPALS (else isOperator is true-for-all)");
2425
- if (!requirePrincipal)
2426
- missing.push("REQUIRE_PRINCIPAL=true (a direct door is multi-tenant; without it an absent principal is anonymous and the self-asserted header governs owner/quota checks)");
2427
- if (missing.length) {
2428
- throw new Error(`DIRECT_APPROVAL_DOOR=true but D-G anchors are missing: ${missing.join(", ")} — refusing to start a half-open direct door (design/80 D-G boot invariant)`);
2429
- }
2400
+ const doorRefusal = halfOpenDirectDoorRefusal({
2401
+ directApprovalDoor,
2402
+ durableApproval: dgDurable,
2403
+ approvalHmacKeys: dgHmacKeys,
2404
+ principalJwtPubkeys,
2405
+ principalJwtIss,
2406
+ principalJwtAud,
2407
+ requirePrincipal,
2408
+ operatorPrincipals: dgOperators,
2409
+ });
2410
+ if (doorRefusal !== undefined) {
2411
+ throw new Error(doorRefusal);
2412
+ }
2413
+ const operatorsRefusal = operatorPrincipalsRequiredRefusal({ durableApproval: dgDurable, requirePrincipal, operatorPrincipals: dgOperators });
2414
+ if (operatorsRefusal !== undefined) {
2415
+ throw new Error(operatorsRefusal);
2416
+ }
2417
+ const bakeOperatorsRefusal = bakeOperatorPrincipalsRefusal({ imageBakes: orchestration.imageBakes, operatorPrincipals: dgOperators });
2418
+ if (bakeOperatorsRefusal !== undefined) {
2419
+ throw new Error(bakeOperatorsRefusal);
2420
+ }
2421
+ const bakeRunnerRefusal = bakeRunnerCredentialRefusal({ imageBakes: orchestration.imageBakes, authTokens });
2422
+ if (bakeRunnerRefusal !== undefined) {
2423
+ throw new Error(bakeRunnerRefusal);
2424
+ }
2425
+ const leaderStoreRefusal = leaderDurableStoreRefusal({ leaderEnabled: orchestration.leaderEnabled, dbBackend: store.dbBackend });
2426
+ if (leaderStoreRefusal !== undefined) {
2427
+ throw new Error(leaderStoreRefusal);
2430
2428
  }
2431
- if (dgDurable && requirePrincipal && dgOperators.length === 0) {
2432
- throw new Error("DURABLE_APPROVAL=true + REQUIRE_PRINCIPAL=true but OPERATOR_PRINCIPALS is empty — on a multi-tenant worker the empty operator set makes isOperator() true-for-all, so every authenticated tenant could read every other tenant's pending-approval queue (GET /v1/approvals & siblings). Set OPERATOR_PRINCIPALS, or unset REQUIRE_PRINCIPAL if this is a single-tenant deployment (#157-① boot invariant)");
2433
- }
2434
- if (imageBakesEnabled && dgOperators.length === 0) {
2435
- throw new Error("IMAGE_BAKES_ENABLED=true but OPERATOR_PRINCIPALS is empty — the bake door runs build.sh on the privileged build host; an empty operator set makes isOperator() true-for-all (world-writable RCE). Set OPERATOR_PRINCIPALS or disable bakes (IMAGE-API-DESIGN.md §P2.4a boot invariant)");
2436
- }
2437
- if (imageBakesEnabled && !Object.values(authTokens).includes(bakeRunnerPrincipal)) {
2438
- throw new Error(`IMAGE_BAKES_ENABLED=true but no SERVICE_AUTH_TOKENS entry maps to the runner principal '${bakeRunnerPrincipal}' — the bake-runner authenticates by its credential (token-derived source), so a '<BAKE_RUNNER_TOKEN>=${bakeRunnerPrincipal}' SERVICE_AUTH_TOKENS entry is required or claim/ingest/heartbeat all 403 (IMAGE-API-DESIGN.md §P2.12 boot invariant)`);
2439
- }
2440
- if (orchestration.leaderEnabled && store.dbBackend !== "mysql" && store.dbBackend !== "pg") {
2441
- throw new Error(`LEADER_ENABLED=true but DB_BACKEND=${store.dbBackend} (no external SQL store) is refused: a leader run is an ` +
2442
- "N-worker fan-out lasting up to 24h that ends in a REAL git push, and without a durable store its registry is " +
2443
- "a per-process Map — a cross-replica GET 404s, a restart forgets the run, and a retried POST starts a SECOND " +
2444
- "push. Point the service at a durable store (DB_BACKEND=mysql|pg), or turn the leader off (unset LEADER_ENABLED) " +
2445
- "(#193 车4 件2 / staleness ledger P1-9+P1-10 boot invariant)");
2446
- }
2447
- if (orchestration.requireExecutionLane && orchestration.remoteExec === undefined) {
2448
- throw new Error(`REQUIRE_EXECUTION_LANE=true but NO execution lane is named on this deployment — refusing to start. ` +
2449
- `With no lane the engine falls back to its in-process stub environment, where at least the Bash tool is ` +
2450
- `not mounted, yet every task still returns status=completed — indistinguishable from "the model chose ` +
2451
- `not to use its tools". (What the roster actually keeps is a per-run fact: read the wiring_manifest ` +
2452
- `event and count entries whose mountedBy is "hands".) ` +
2453
- `Cause: REMOTE_EXEC is unset/empty AND CONFIG_PROVIDER is not "local". ` +
2454
- `Fix: set REMOTE_EXEC to one of ${REMOTE_EXEC_PROVIDER_WORDS.join(" | ")} (and that lane's required env), ` +
2455
- `or set CONFIG_PROVIDER=local to take the single-machine \`host\` default, ` +
2456
- `or unset REQUIRE_EXECUTION_LANE to keep running without hands on purpose.`);
2429
+ const laneRequiredRefusal = executionLaneRequiredRefusal(orchestration);
2430
+ if (laneRequiredRefusal !== undefined) {
2431
+ throw new Error(laneRequiredRefusal);
2457
2432
  }
2458
2433
  {
2459
2434
  const envLaneDispatchable = !isRosterGateArmed(integrations.configCenter);
@@ -1,4 +1,5 @@
1
1
  import type { ImageCapabilities } from "./plugins/store-contracts.js";
2
+ import type { RemoteExecProvider } from "./execution-lane-caps.js";
2
3
  export interface SandboxEnvFacts {
3
4
  profile?: string;
4
5
  capabilities?: string[];
@@ -53,7 +54,8 @@ export declare function buildEnvFacts(opts: {
53
54
  * codex 复审 medium:k8s 必须按**有效快照能力**分叉,不能只看车道名——s3Snapshot 未配=park-only,
54
55
  * resume 是全新 pod 且**工作区不恢复**,「文件已从快照恢复」那句会让模型信赖不存在的文件(比缺席
55
56
  * 更糟的谎)。 */
56
- export declare function resumeFactsForLane(provider: "e2b" | "k8s" | "ssh" | "adb" | "local-docker" | "host" | "device" | undefined, caps?: {
57
+ export declare function resumeFactsForLane(provider: RemoteExecProvider | undefined, // 车道词 = settings-schema 词表正本(经本仓读口取型,不手抄;表加词 ⇒ 下方穷举 switch 编译红)
58
+ caps?: {
57
59
  k8sSnapshot?: boolean;
58
60
  }): SandboxEnvFacts["resumeFacts"] | undefined;
59
61
  /** Path-safe session segment: the session id can be CALLER-SUPPLIED (body.sessionId), so it must not be able to
package/dist/env-facts.js CHANGED
@@ -42,8 +42,12 @@ export function resumeFactsForLane(provider, caps) {
42
42
  case "host":
43
43
  case undefined:
44
44
  return { scratch: "preserved" };
45
- default:
45
+ case "local-docker":
46
46
  return undefined;
47
+ default: {
48
+ const unreachable = provider;
49
+ return unreachable;
50
+ }
47
51
  }
48
52
  }
49
53
  export function scratchpadSessionSegment(sessionId) {
@@ -1,16 +1,13 @@
1
+ import { type ExecutionLane as DeclarableLane, type ExecutionLaneRow } from "@sema-agent/settings-schema/execution-lanes";
1
2
  import type { ServiceConfigFlat } from "./config-types.js";
2
3
  /** 一位能力的三态。`single-user-only` = 车道支持,但只在单用户部署(`requirePrincipal !== true`)开 ——
3
4
  * 多租户下这一位必须关(host 家族的 cwd/scheduler/backgroundShell/LSP 都是这一档,理由同源:
4
5
  * 一个租户的输入不许把 agent 指向 worker 宿主上另一个租户/运维的文件与进程)。 */
5
6
  export type LaneCapability = "supported" | "unsupported" | "single-user-only";
6
- /** `REMOTE_EXEC` 判别联合里的 provider 词全集 —— **派生自真配置型**,不是手抄一份。
7
- * 手抄的那份挡不住本模块要挡的病:给 `ServiceConfigFlat["remoteExec"]` 加一条臂(`remote-docker` 就是
8
- * config-types.ts 里写着的下一条)可以完全不碰本文件而编译通过,穷举 switch 于是只对**自己的**副本穷举,
9
- * 能力表与普查数组一起静默漏行(codex R1-F2)。派生之后:配置加臂 ⇒ 本文件的 switch/词表/夹具同时编译红。 */
10
- export type RemoteExecProvider = NonNullable<ServiceConfigFlat["remoteExec"]>["provider"];
11
- /** 闭集:执行车道全集 = provider 词 + `in-process`(`REMOTE_EXEC` 未设的进程内 stub 形,配置面无 provider
12
- * 可派生 —— 它是「没有 remoteExec」这个状态的名字)。加词 ⇒ {@link executionLaneCaps} 的穷举 switch 与
13
- * {@link EXECUTION_LANE_WORDS} 两处都编译红,这正是要的(「加词必须逐位表态」)。 */
7
+ /** `REMOTE_EXEC` 能写下去的车道词全集 = settings-schema 词表正本的 `ExecutionLane`(**派生**,本仓零手抄)。 */
8
+ export type RemoteExecProvider = DeclarableLane;
9
+ /** 闭集:执行车道全集 = 表的可声明车道 + `in-process`(`REMOTE_EXEC` 未设的进程内 stub 形 —— 它是「没有 remoteExec」
10
+ * 这个状态的名字,不在表里)。表加词 ⇒ {@link executionLaneCaps} 的穷举 switch 编译红,这正是要的(「加词必须逐位表态」)。 */
14
11
  export type ExecutionLane = "in-process" | RemoteExecProvider;
15
12
  /**
16
13
  * per-lane 能力位。每一位的注里点名**今天真正做判决的那处代码**——对账测试按这些坐标跑真函数,
@@ -88,12 +85,13 @@ export interface ExecutionLaneCaps {
88
85
  * · `single-user-only` = 所有任务落在**同一个共享目标**上(worker 宿主:host;一台常驻 SSH 对端:ssh;一台 ADB 设备:adb)
89
86
  * ⇒ 只许单用户部署用。
90
87
  * 本位**没有** `unsupported` 这一档(那会读成「谁都不许用」,不是车道事实)—— 型上收窄掉,每条车道必须在两档里表态。
91
- * 🔴 **本位即执法源**(车PI 验修轮 F8,如实登记):唯一消费点是 `config-invariants.ts` 的 `multi-tenant-shared-lane`(经
92
- * {@link laneCapabilityHolds} 落成布尔),能力表对账格里本位的「判别式」读的就是这张表 —— 自指,只证明消费点读的是本位,不证明本位
93
- * 的值对。本位值的**独立作者** = 部署面 `deploy/sema-up/lib/lanes.json` 的 users 列(只有 host / e2b / k8s 三行)+ 行为格
94
- * (`test/multi-tenant-shared-lane.test.ts`:ssh / adb 的工厂给两个租户的任务铸出的 env 报同一个 `workspaceHandle().sandboxId`)。
95
- * local-docker / device / in-process 的 `supported` 今天没有独立预言机(e2b / local-docker 的句柄要连上才有,k8s 连前恒 `(unconnected)`,
96
- * 都不是零成本格)—— 改这几行时按适配器事实自查。
88
+ * 🔴 **7.114.0 起本位的值不在本仓写**:七条可声明车道读 settings-schema 词表正本的表行(`executionLaneRow(lane).tenantSeparation`;
89
+ * 中心写闸的 `requirePrincipal ∧ single-user-only` 交叉判与本仓 `multi-tenant-shared-lane` 拒启读的是同一个值,不会分叉);
90
+ * 只有 `in-process` 一格是本仓自己的事实(它不在表里)。
91
+ * 唯一消费点是 `config-invariants.ts` 的 `multi-tenant-shared-lane`(经 {@link laneCapabilityHolds} 落成布尔)。本位值与本仓**运行期事实**
92
+ * 不矛盾的对账 = `test/execution-lanes-schema-conformance.test.ts`(隔离位 / 共享文件平面 ⇒ 租户档的蕴含 + 唯一一条「不隔离却分租户」
93
+ * 的车道 = device 点名);ssh / adb 「两租户同一目标」的行为预言机 = `test/multi-tenant-shared-lane.test.ts`
94
+ * (两个租户的任务铸出的 env 报同一个 `workspaceHandle().sandboxId`)。
97
95
  */
98
96
  readonly tenantSeparation: Exclude<LaneCapability, "unsupported">;
99
97
  }
@@ -117,23 +115,49 @@ export interface ExecutionLaneCaps {
117
115
  * 恰是诚实形(§4.3.4)。
118
116
  */
119
117
  export declare function executionLaneCaps(lane: ExecutionLane): ExecutionLaneCaps;
120
- /** 一个开放字符串是不是闭集里的词。判据 = 词表键在不在,故与联合天然同源。 */
121
- export declare function isExecutionLane(word: string): word is ExecutionLane;
122
- /** 车道普查全集(顺序 = 词表声明序;对账测试按它逐 lane 跑真判别式)。 */
118
+ /**
119
+ * 车道普查全集 = 本仓内部状态名 `in-process` + 词表正本的可声明车道(次序 = 正本声明序;对账测试按它逐 lane 跑真判别式)。
120
+ * 由正本派生、不手维护(手维护的「全表」已被代码反证漏过行)。
121
+ */
123
122
  export declare const ALL_EXECUTION_LANES: readonly ExecutionLane[];
123
+ /** 一个开放字符串是不是闭集里的词(不做前缀 / 大小写宽容)。 */
124
+ export declare function isExecutionLane(word: string): word is ExecutionLane;
124
125
  /**
125
- * `REMOTE_EXEC` **真能写下去的**那几个词(= {@link ALL_EXECUTION_LANES} 去掉 `in-process`:后者是
126
- * 「没有 remoteExec」这个状态的名字,env 里写它没有意义)。
127
- *
128
- * 🔴 S-426 以点改面:此前 `config-catalog.ts` 的 `REMOTE_EXEC` 行把这张表**手抄**成一个
129
- * `enumValues: ["e2b","k8s",…]` 字面量(字段静态型是 `readonly string[]`,与 {@link RemoteExecProvider}
130
- * 零类型联系)。病形与 S-382 逮到的 `WEB_SEARCH_PROVIDER` 逐字同族:给
131
- * `ServiceConfigFlat["remoteExec"]` 加一条 provider 臂(config-types.ts 里写着的下一条就是
132
- * `remote-docker`),本文件的穷举 switch 与词表**会**编译红,而目录行不会 —— 于是运维在
133
- * `GET /v1/config/catalog` 上读到的可写词集静默少一个,新车道成了「配了但目录说不认识」。
134
- * 收成一只派生数组之后,目录行**就用它本人**(身份相等有钉),全仓只剩这一张表。
126
+ * `REMOTE_EXEC` **真能写下去的**那几个词 = settings-schema 词表正本的 `EXECUTION_LANE_WORDS`(冻结副本,次序逐字同)。
127
+ * 读者:配置目录 `REMOTE_EXEC` 行的 `enumValues`(**就用它本人**,身份相等有钉 —— S-426 修过「目录手抄一份词表」的病)、
128
+ * [ref] 拒启句的 `valid:` 列表、`REQUIRE_EXECUTION_LANE` 拒启句的出路、`multi-tenant-shared-lane` 拒启句的出路车道。
129
+ * 7.114.0 起全仓唯一的车道词来源是正本;本常量只是它在本仓的那一个读口。
135
130
  */
136
131
  export declare const REMOTE_EXEC_PROVIDER_WORDS: readonly RemoteExecProvider[];
132
+ /** 一条可声明车道在词表正本里的那一行(必需 / 可选 env 四栏 + 租户档)。加载期自检过完备性,所以对任何 {@link RemoteExecProvider} 都有行。 */
133
+ export declare function executionLaneRow(lane: RemoteExecProvider): ExecutionLaneRow;
134
+ /**
135
+ * 一条车道**必需**的 env 键名 = 正本表行的 `requiredEnv ∪ requiredSecretEnv`(次序:非密坐标在前、密钥在后 —— 与 7.113.0 本仓手写表逐字同)。
136
+ * 读者:`config.ts` 的 [ref] 拒启门(点了名却缺件 ⇒ 点名缺哪几键)、部署面门(`test/deploy-lanes-contract.test.ts`:`lanes.json` 每行登记
137
+ * 本车道必需键、根 / review compose 透传全部车道必需键之并)、`scripts/gen-lanes-json.mjs`(从同一张正本生成 `lanes.json` 的必需键列)。
138
+ * device 没有必需 env(设备是运行期连进来的;它的 boot 前提是跨键的,在跨域不变量区拒启)。
139
+ */
140
+ export declare function laneRequiredEnv(lane: RemoteExecProvider): readonly string[];
141
+ /**
142
+ * S-598 件③ / S-1008 —— **「`REQUIRE_EXECUTION_LANE=true` ∧ 没有任何车道被点名 ⇒ 拒启」的判据与拒因句的唯一属主**(`modelProviderRefusal` /
143
+ * `modelRouteEndpointRefusal` 同形:属主在叶模块,执法点各自调它,登记在 `config-invariants.ts`)。
144
+ *
145
+ * 🔴 病(S-1008,test d327-g6-engine-7112 OBS-4 / OBS-5;2026-10-03 亲核):修前**两份句子**。运行期真拒启的是 `config.ts` 手写 throw
146
+ * (点名 `REQUIRE_EXECUTION_LANE` / `REMOTE_EXEC` + 修法,用户在 stderr 读到的就是它);注册表条目另写了一句短的
147
+ * 「the deployment asked to refuse this state」,而它只在 `config-center/stage-limits.ts` 的全表校验里被调 —— 候选视图只装
148
+ * `BATCH1_LIMIT_KEYS` 的限额键,带不上这两键 ⇒ 对本条恒 `next === live`,**那一句没有任何读者**(test 六个拒启格 stderr 里零命中)。
149
+ * 改一份另一份不跟。现形:boot 的 throw 与注册表条目的 `check` **都**调本函数 —— 一只判据、一句话,没有第二份文本可漂。
150
+ *
151
+ * 判据读**装配结果**(`executionLaneOf` = 全仓唯一的缺席归一读口,缺席 ⇒ `in-process`),不重判 env:`remoteExec` 缺席**就是**
152
+ * 「`REMOTE_EXEC` 未设/空串 ∧ `CONFIG_PROVIDER ≠ local`」这条三元链尾臂的落点;点了名却配不齐 / 认不出归 [ref] 那条既有拒启(两句话各说各的问题)。
153
+ *
154
+ * 句子的量词纪律(S-1008 ②):stub 车道上**手带族**(`wiring_manifest.tools.entries[].mountedBy === "hands"`)整族不挂,名册里其余工具
155
+ * 照常执行(test R1n:`Now` 真执行成功)—— 不写「零工具」;名册**总条数**随 core 版本走,不写进句子(S-1008 ③)。
156
+ */
157
+ export declare function executionLaneRequiredRefusal(deployment: {
158
+ readonly requireExecutionLane: boolean;
159
+ readonly remoteExec?: ServiceConfigFlat["remoteExec"];
160
+ }): string | undefined;
137
161
  /**
138
162
  * `config.remoteExec?.provider` → 车道词。**全仓唯一**的缺席归一读口(S-602 门 D:`?? "in-process"` 只许住在这里,
139
163
  * `?? "host"` 与 `lane === undefined || lane === "host"` 同义折零容忍)。
@@ -144,9 +168,9 @@ export declare const REMOTE_EXEC_PROVIDER_WORDS: readonly RemoteExecProvider[];
144
168
  *
145
169
  * 两个重载 = 类型收窄([ref] §2.2 A):喂**已装配的** config 形(`RemoteExecProvider | undefined`)⇒
146
170
  * 返回型里没有 `undefined`,调用点不必为一条不可达的臂写兜底(兜底写多了就是下一处折词);喂开放字符串
147
- * (测试、外部输入)⇒ 返回含 `undefined`,逼调用点表态。第一个重载的成立依据是**编译期**的:
148
- * `RemoteExecProvider ⊂ ExecutionLane`(`ExecutionLane` 由它派生),而 {@link EXECUTION_LANE_WORDS} 是
149
- * `Record<ExecutionLane, true>` —— 少一个词编译红,所以对 `RemoteExecProvider` 的每一个值 `isExecutionLane` 恒真。
171
+ * (测试、外部输入)⇒ 返回含 `undefined`,逼调用点表态。第一个重载的成立依据是**构造**的:
172
+ * `RemoteExecProvider` 是正本元组 `EXECUTION_LANE_WORDS` 的元素型,而 {@link isExecutionLane} 的集合正是由那只元组
173
+ * (加 `in-process`)建成 ⇒ 对 `RemoteExecProvider` 的每一个值 `isExecutionLane` 恒真(对账格逐词跑过)。
150
174
  */
151
175
  export declare function executionLaneOf(provider: RemoteExecProvider | undefined): ExecutionLane;
152
176
  export declare function executionLaneOf(provider: string | undefined): ExecutionLane | undefined;