@sema-agent/server 7.113.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.
package/MIGRATION.md CHANGED
@@ -7,6 +7,40 @@
7
7
  > ⚠️ 完整清单在仓库根 `CHANGELOG.md`——它**不随 npm tarball 出包**(本文件随包)。看完整迁移窗的
8
8
  > 权威姿势是源码 tag diff:`git diff v<旧>..v<新>`(每版都推 `v<版本>` tag);npm 包页也镜像 CHANGELOG。
9
9
 
10
+ ## 7.114.0(车UW,S-995;车UY)—— `settings.hooks` 工具事件 matcher:别名写成正则形的同一个错误,提交当场 400(7.112.0 只拒字面形) · `@sema-agent/settings-schema` 7.0.1 + `@sema-agent/core` 7.39.2:车道 / 存储词表改读正本 · 写了 `remote-docker` 的 `config.d/remote-exec.json` 拒启 · local 车道坏会话规则文件改拒 · 被拒的 `resumeAt` 不再挪会话叶
11
+
12
+ - **🔴 settings 面硬破坏(响亮拒,无迁移代码、无兼容形)**。① *谁受影响*:提交体 `settings.hooks` 里 PreToolUse / PostToolUse / PostToolUseFailure 的 `matcher` **选中引擎工具别名、却选不中它的正名**的 —— 7.112.0 只拒字面的 `ListPeers` / `Read|ListPeers`,
13
+ `^ListPeers$` / `ListPeers.*` / `(ListPeers)` 这类正则写法当时 200、钩子永不命中(core 按 wire 名匹配,别名不是 wire 名)。判据现在是对 core 工具目录、按 hook-runner 运行期**同一只**匹配函数求值(字面 / 名字表读法:`|` 与 `,` 分隔、逐段 trim,与 CC 2.1.280 同形;区分大小写)。
14
+ ② *怎么改*:把 matcher 改成**正名** `ListAgents`(错误句直接给出);`ListAgents|ListPeers` / `List.*` 这类**也选中正名**的写法不受影响。③ *不改会怎样*:该**新提交** `400 request.field_invalid`(错误句点名别名与正名)。
15
+ 耐久 resume 重放**不判**(会砖 parked 任务;与 7.112.0 同):存量 body 里的别名 matcher 在续跑腿照旧永不命中、同一份 hooks 里其余钩子照常生效。
16
+ **不在本规则内(仍受理)**:大小写错的别名(`"listpeers"` —— CC 同形:区分大小写、静默不跑)与「选不到目录里任何名」另一类(含目录之外的 MCP 工具名,受理面无从区分),后者候设计(见 CHANGELOG `## Unreleased` 7.114.0 线 · 车 UW)。
17
+ **正则只是顺带选中别名也会被拒**:规则问的是「这只 matcher 选不选别名 / 选不选正名」,不问写的人本意 —— 一只写来匹配目录外 MCP 工具、只是顺带也选中 `ListPeers` 的正则(如 `.*Peers`、`mcp__.*|.*Peers`)同样 400,
18
+ 而错误句说的是「改成 `ListAgents`」。出路:在正则里把别名排除掉(如 `(?!ListPeers$).*Peers`),或改成不会选中它的写法。
19
+ - **🔴 运行期 matcher 求值与 CC 2.1.280 同形(行为面;不是配置面硬破坏 —— 是钩子开始触发)**。① *谁受影响*:`settings.hooks` 里 matcher 带**两端空白、分隔符两侧空白或用逗号分隔**的(`" Bash"` / `"Bash "` / `"Read | Bash"` / `"Read,Bash"`)—— CC 容忍,sema 此前把它们当「选不中」(钩子永不触发、无任何声音)。② *怎么改*:无需改 —— 它们现在按作者本意命中(PreToolUse 的 deny 钩子也真拦);**区分大小写、不剥词内空白**(`bash` / `"Ba sh"` 照旧不命中,CC 同形)。含 `.` / `*` / 括号等正则字符的 matcher **一字不动**(整体当锚定正则)。③ *不改会怎样*:升级后,**以前因空白永不触发的钩子会触发** —— 若你依赖「带空白的钩子是死的」(不是合法用法),删掉那条。耐久 resume 的存量 hooks 里带空白的 matcher 在续跑腿同样开始命中。**读法**:只含 `[A-Za-z0-9_-]`、`|`、`,`、空格的 matcher 按 `[|,]` 拆、逐段 trim、大小写敏感、词内空白不剥,对所有带 matcher 的事件(含 StopFailure / PreCompact / PostCompact 与 `if` 条件名部)同一只读法。**已知与 CC 的差异(未做)**:CC 的空白 / 逗号放行按事件分档(sema 不分);CC 正则不锚定。依据:对 CC 2.1.280 的黑盒实测(空白 / 大小写);逗号分隔按参照实现的读法对齐,没有黑盒实测。
20
+ - **多租户车道(`requirePrincipal: true`):字面判保留,另有一处放宽见下条**:该车道 `settings.hooks` 整只被忽略(`task_hooks_ignored`),受理面走**字面档 = 7.112.0 的字面判保留**(裸 `ListPeers` / `Read|ListPeers` 照旧 400;正则写法照旧 200),且**零调用方正则执行**;字面档与运行期共用同一只字面 / 名字表读法,所以**带空白或逗号分隔的别名**(`" ListPeers"` / `"Read | ListPeers"` / `"Read,ListPeers"`)在该车道也同拒 400(只多拒、不放宽)。上面的正则求值只在单用户车道。
21
+ - **受理面判序与资源闸(行为面,零新码;两条车道同一条顺序)**:matcher 超 8 192 字符 ⇒ 两条车道都不读、「未判」、照旧受理;字面 / 名字表读法(只含名字字符、`|`、`,`、空格)两条车道都先走、不占求值预算;
22
+ 只剩正则形时,单用户车道在一次受理共享的 100 ms 墙钟预算里求值(灾难回溯的正则用尽预算 ⇒ 那一只与其后各只正则形「未判」、照旧受理),多租户车道对正则形不判。
23
+ 「未判」过 `recordFailOpen`(F 类 tag `server.hooks.matcher-unjudged`,`reason` ∈ oversize / over_budget / invalid_regex;只含事件 / 原因 / 字符数)。正常 matcher 不会碰到。
24
+ **含空段的别名同拒(两条车道)**:前导 / 尾随 / 连续分隔符的写法(如 `ListPeers|`)7.113.0 受理(且永不命中),自 7.114.0 拒。
25
+ **拒放集合相对 7.113.0 的一处放宽**:**超过 8 192 字符的字面别名 matcher**(如一长串名字交替里夹着 `ListPeers`)7.113.0 拒,自 7.114.0 两条车道都判「未判」、受理。
26
+ - **🔴 `config.d/remote-exec.json` 写了 `provider: "remote-docker"` ⇒ 拒启**(只影响 `CONFIG_PROVIDER=local` 的单机形)。① *谁受影响*:本机配置目录里那份文件点名 `remote-docker` 的部署
27
+ (7.113.x 及以前:启动只告警「未接线」、照用 env 车道跑)。② *怎么改*:远端 docker 守护进程的真形是 `local-docker` 车道 + `DOCKER_HOST`(写进文件的 `local-docker` 臂,或设
28
+ `REMOTE_EXEC=local-docker` + `DOCKER_IMAGE` + `DOCKER_HOST`);不想用文件就删掉它。③ *不改会怎样*:启动报 `config.d/remote-exec.json under <root> is invalid: …` 并退出(settings-schema 7.0.0 起
29
+ 该词不在车道表里,文件读取直接拒,不再静默落到别的车道)。
30
+ - **🔴 local 车道(`DB_BACKEND=local` / run-local)会话规则文件「在但坏」⇒ 拒**(core 7.39.2)。① *谁受影响*:数据根 `session-policy/` 下某个会话的规则文件读不出 / 解析不出 / 形不对的部署
31
+ (位腐、半截写、手改)。7.113.x:该会话按「没有会话规则」照跑(调用方加的收紧被静默丢掉)。② *怎么改*:按运维日志 `file_store_corrupt_read` 一行的 `path` 找到那份文件 —— 修好它,或删掉它
32
+ (删 = 显式「该会话无规则」,原规则不再生效);拒绝句本身**不带**盘上路径(它会经错误体到终端用户)。③ *不改会怎样*:该会话的任务以码 `corrupt` 失败(零轮),`GET` / `PUT /v1/sessions/:id/policy`
33
+ 答 `500 corrupt`;`PUT` 不会覆盖坏文件(覆盖 = 丢证据)。文件**不存在**照旧 = 无规则,不受影响。SQL 车道本来就是拒,不变。
34
+ - **被拒的 `resumeAt` 请求不再动会话**(core 7.39.2):`resumeAt` + `restoreFiles:true` 在没有文件历史边界处被拒(同步口 `422 rewind_snapshot.unresolvable` / 后台口 202 后失败)⇒ 会话叶留在请求前的位置。
35
+ 7.113.x 及以前叶会被挪到目标处,下一次普通请求续在回退点上(之前的回合成了旁枝)。零动作;按日志字节 / 条目数对账的工具会多看到两条 `leaf` 条目(分支 + 写回),请比叶不比条数。
36
+ - **运维面**:`server.session-policy.corrupt-read-fail-open`(7.113.0 起的 P-DEBT 计数)**留着,告警 / 看板不要删**:上一条只覆盖**按键读**(任务装规则、`GET` / `PUT …/policy`);
37
+ 另三条路仍把坏规则行当作「无规则」—— 会话同步导出(导出里没有那位 principal 的规则)、删除**别的**会话时顺带扫掉撕尾的规则行(之后该会话按无规则跑,这一形不出披露行)、数据根重绑;
38
+ 修好之前计数照记。计数里也含已被拒的按键读(座上分不出是哪条路读的),非零就按探针 / `file_store_corrupt_read` 行的 `path` 去修或删那份文件。
39
+ `file_store_corrupt_read` 照旧逐命中一行,`note` 的后果句改写(按键读 = 拒;枚举 / 抹除 / 重绑 = 仍按无规则)。
40
+ - **两句拒启文案改形(只影响按原文 grep 的脚本)**:`REMOTE_EXEC=<未知词>` 的 `valid:` 列表次序改为 `host | e2b | k8s | ssh | adb | local-docker | device`;`MEMORY_ENGINE_BACKEND` 越集值改为
41
+ `env MEMORY_ENGINE_BACKEND="<值>" must be one of: file, pg, tidb`(与 `DB_BACKEND` / `SESSION_BACKEND` 同一句形)。拒 / 放的集合不变。
42
+ - **部署面**:`deploy/sema-up/lib/lanes.json` 自此由 settings-schema 车道表**生成**(字节与 7.113.0 相同,零动作);改车道的必需键 / 租户档去 settings-schema 改正本,本仓重生成。
43
+
10
44
  ## 7.113.0(车UP)—— `@sema-agent/core` 7.38.3 → 7.39.1(core 7.39.0 硬破坏六条 + 7.39.1 清单披露补全):`attachments.planModeReminder` 退役 · 停放行排空前提 · 策略结算记在策略名下 · 受理账本收消费证据
11
45
 
12
46
  - **🔴 请求面硬破坏:`attachments.planModeReminder` 退役(响亮拒,无兼容形)**。① *谁受影响*:提交体 `attachments` 里带 `planModeReminder`(任何值,含 `false`)的调用方
@@ -64,8 +98,9 @@
64
98
  - **保持逐命中响亮(安全轴混座,不退役)**:`file_store_corrupt_read`;其 session-policy 脸的坏读今天仍 fail-open(core #1299 修后改拒),server 7.113.0 起同时计 `server.session-policy.corrupt-read-fail-open`(P-DEBT)。
65
99
  - **run-local 车道**(前台 CLI 进程)的会话 repo 仍走混座逐命中 warn,不降。
66
100
 
67
- ### 会话读三口的非属主 404(车 UQ S-989;wire 值变更)
68
- - `GET /v1/sessions/:id`(detail / head / events)对**非属主**调用方的反枚举 404,码 / 句子从 run 拼法(`not_found.run` + run 长文)改为会话拼法(`not_found.session` + `session not found`),与「不存在」臂同串。读 `not_found.run` 判「会话读被拒」的旧端要改读 `not_found.session`;属主 200 不变;run 面(`/v1/runs/:id`)不变。
101
+ ### 会话读三口的非属主 404(车 UQ S-989;**安全修复:存在性探测口闭合**;wire 值变更)
102
+ - **修前(7.112.0 及更早;7.103.x 的 detail 口读码已是此形,head / events 两口与更早版本未逐版核)**:`GET /v1/sessions/:id`(detail / head / events)对**非属主**调用方答 `not_found.run` + run 长文,对**不存在**的会话答 `not_found.session` + `session not found` —— 同一端点两臂的码与句子都不同,多租户部署里任何持有效凭据的调用方可以据此判断「别人的某个会话 id 存不存在」(读不到内容,只能探存在性;7.112.0 发布字节黑盒实测可区分)。**7.113.0 起两臂逐字节相同**(`not_found.session` + `session not found`)。多租户部署(`REQUIRE_PRINCIPAL=true`)建议升到 7.113.0 及以上;单用户部署没有「非属主」这一臂,不受影响。
103
+ - **兼容面**:读 `not_found.run` 判「会话读被拒」的旧端要改读 `not_found.session`;属主 200 不变;run 面(`/v1/runs/:id`)不变。
69
104
 
70
105
  ## 7.112.0(车UG,S-924)—— `settings.hooks` 里按工具别名写的 matcher 提交当场 400(此前静默不命中)
71
106
 
package/USAGE.md CHANGED
@@ -66,7 +66,7 @@ docker compose up --build --scale service=4 # 4 个无状态 worker,nginx
66
66
  > | `adb`(一台设备) | ✅ | ❌ 拒启(7.104.0 起) |
67
67
  > | `e2b` / `k8s` / `local-docker`(每任务一只沙箱) | ✅ | ✅ |
68
68
  > | `device`(每人自己的设备) | ✅ | ✅ |
69
- > | 未点名(云形进程内 stub,无手) | ✅(`REQUIRE_EXECUTION_LANE=true` 可改成拒启) | ✅(同左) |
69
+ > | 未点名(云形进程内 stub,零手带工具、非手带工具照常执行) | ✅(`REQUIRE_EXECUTION_LANE=true` 可改成拒启) | ✅(同左) |
70
70
 
71
71
  > 🔴 **7.104.0 起 `CONFIG_PROVIDER` 是闭集**:只认 `local` / `remote` / 不设(空串 = 不设),**逐字**比较 —— `Local`、` local`、`remote ` 这类写法**拒启**,拒因句点名旋钮并列出合法词。此前认不出的词照起,却被配置源当成 `local`、被隐式 host 车道当成「不是 local」,local 配置源的根还落回进程 cwd(与车道文件覆写读的本地根分家)。本地配置根只有一条链:`CONFIG_LOCAL_DIR` → `AGENT_DATA_DIR` → `~/.ai-agent`,配置源与 `config.d/remote-exec.json` 读同一个。
72
72
 
@@ -55,6 +55,7 @@
55
55
  * 也**不投**这个读面(硬造一个消费点 = 造给自己看的 wire 键)。哪天真要在诊断面露它,先有消费方需求。
56
56
  */
57
57
  import { type ExecutionLane, type RemoteExecProvider } from "../execution-lane-caps.js";
58
+ import type { MemoryEngineBackend } from "@sema-agent/settings-schema/execution-lanes";
58
59
  /**
59
60
  * [ref] A3 源头修(黑板 [ref]/[ref]/[ref],[ref])—— `memoryPersistenceCapable` 的**装配层有效值**:
60
61
  * operator 显式 env 恒赢;env 缺席时,`REMOTE_EXEC=host` 腿自动补 `true`;其余一律缺席(现行为)。
@@ -113,7 +114,7 @@ export interface MemoryWriteBoundaryFacts {
113
114
  * file 腿读(memory-scope 的 `resolveMemoryEngineRoot`),pg/tidb 腿的根是
114
115
  * `<LOCAL_DATA_ROOT|数据根>/memory-work`(boot/stores.ts 两支),那条腿上指 MEMORY_ENGINE_DIR 是让
115
116
  * operator 去拧一个不接线的旋钮 —— 修不好还以为自己修了。 */
116
- engineBackend: "file" | "pg" | "tidb";
117
+ engineBackend: MemoryEngineBackend;
117
118
  /** boot 期算得出的文件工具围栏根。空数组 = 算不出(每任务临时目录 / 未配 workspace)⇒ 不判。 */
118
119
  containmentRoots: readonly string[];
119
120
  }
@@ -160,7 +160,7 @@ export interface ConsolidationWiringFacts {
160
160
  /** 记忆后端自带控制面归属吗(`typeof backend.controlPlaneRoot === "string"` 且非空)。 */
161
161
  readonly controlPlaneOwned: boolean;
162
162
  /** 只进文案 —— 让「该去拧哪根旋钮」这句话指向该腿上**真的**有效的那一根。 */
163
- readonly memoryEngineBackend: "file" | "pg" | "tidb";
163
+ readonly memoryEngineBackend: ServiceConfig["memoryEngineBackend"];
164
164
  /** 部署声明的 provenance 姿态(`config.memoryProvenance`;缺席 ≡ core 缺省 `carry`)。 */
165
165
  readonly provenance?: "off" | "carry";
166
166
  }
@@ -34,7 +34,7 @@ import { normalizeAttachments, normalizeResilience, normalizeResumeAtMode, norma
34
34
  import { cwdHonored, deviceCwdHonored, effectiveHostWorkspace, inProcessSingleUserLane, isValidCwd, parseAdditionalDirectories, satisfiedByProcessCwd, shellEnvMismatchCount } from "../task-cwd.js";
35
35
  import { assertRequestA2aUnlocked, resolveRequestA2a } from "../task-a2a.js";
36
36
  import { assertRequestMcpContentOrigin, assertRequestMcpUnlocked, resolveRequestMcp } from "../task-mcp.js";
37
- import { MAX_SETTINGS_OUTPUT_STYLE_CHARS, acceptAppendSystemPrompt, applyTaskSettings, effectivePermissionMode, effectiveThinking, hasConstitutionAnchors, parseTaskSettings, providerDropsAppend, settingsPermissionBucketIssue, withPermissionMode } from "../task-settings.js";
37
+ import { MAX_SETTINGS_OUTPUT_STYLE_CHARS, acceptAppendSystemPrompt, applyTaskSettings, effectivePermissionMode, effectiveThinking, hasConstitutionAnchors, parseTaskSettings, taskHooksHonored, providerDropsAppend, settingsPermissionBucketIssue, withPermissionMode } from "../task-settings.js";
38
38
  import { enableForkFromBody, normalizeRetainSubagentSessions, selfOrchestrationFromBody } from "../task-workflow.js";
39
39
  import { redactSecrets } from "../trace/redact.js";
40
40
  import { DeferredSandboxPathEnv, isSandboxPathAdjudicationLane, sandboxPathEnvSlots } from "./deferred-sandbox-path-env.js";
@@ -210,7 +210,7 @@ export function createResolveSpec(ctx) {
210
210
  const hc = parsedSettings.settings?.hooks;
211
211
  if (!hc)
212
212
  return undefined;
213
- if (config.requirePrincipal === true) {
213
+ if (!taskHooksHonored(config)) {
214
214
  logger.warn("task_hooks_ignored", { honored: false, sessionId: auth?.sessionId ?? null });
215
215
  return undefined;
216
216
  }
@@ -2,10 +2,11 @@ import { CROSS_SESSION_INBOUND_SETTINGS, CROSS_SESSION_DIALOG_EXPIRY_VALUES } fr
2
2
  import { CENTER_CREDENTIAL_ENV, DOMAIN_SCHEMAS } from "@sema-agent/settings-schema";
3
3
  import { CENTER_CREDENTIAL_RENEW_LEAD_DEFAULT_MS, CENTER_CREDENTIAL_RENEW_LEAD_MAX_MS, CENTER_CREDENTIAL_RENEW_LEAD_MIN_MS } from "./center-credential-renewer.js";
4
4
  import { THINKING_FORMATS } from "./model-compat.js";
5
- import { AUTO_MODE_CLASSIFIER_TIMEOUT_BOUNDS, CONFIG_PROVIDER_WORDS, DB_BACKEND_WORDS, DRAIN_SECOND_SIGNAL_MIN_GAP_BOUNDS, DRAIN_SECOND_SIGNAL_MIN_GAP_DEFAULT_MS, FIRST_BYTE_TIMEOUT_BOUNDS, RESILIENCE_DEFAULTS, TASK_WRITE_FACE_ESCALATIONS } from "./config-types.js";
5
+ import { AUTO_MODE_CLASSIFIER_TIMEOUT_BOUNDS, CONFIG_PROVIDER_WORDS, DRAIN_SECOND_SIGNAL_MIN_GAP_BOUNDS, DRAIN_SECOND_SIGNAL_MIN_GAP_DEFAULT_MS, FIRST_BYTE_TIMEOUT_BOUNDS, RESILIENCE_DEFAULTS, TASK_WRITE_FACE_ESCALATIONS } from "./config-types.js";
6
6
  import { webSearchProviderFromEnv, WEB_SEARCH_PROVIDERS } from "./plugins/web-search.js";
7
7
  import { AUTONOMY_MODES } from "./runtime-governance.js";
8
8
  import { REMOTE_EXEC_PROVIDER_WORDS } from "./execution-lane-caps.js";
9
+ import { MEMORY_ENGINE_BACKENDS, SESSION_BACKENDS, STORE_BACKENDS } from "@sema-agent/settings-schema/execution-lanes";
9
10
  import { MODEL_PROVIDER_WORDS } from "./model-provider.js";
10
11
  import { buildDisplaySafeUrl } from "./trace/redact.js";
11
12
  import { describeAllowlistPostures } from "./env-name-allowlist-knobs.js";
@@ -99,7 +100,7 @@ export const CONFIG_CATALOG = [
99
100
  r("COST_QUOTA_WINDOW_SEC", "limitsHttp", "number", "per-principal 成本配额窗长(秒);SQL 腿窗长进桶主键 ⇒ center 腿显式 deferred(重启生效)", { staticDefault: "86400", danger: BILL, configKey: "costQuotaWindowSec", center: cLimits("costQuotaWindowSec", { timing: "restart", restartReason: "cost-quota-window" }) }),
100
101
  r("CROSS_SESSION_DIALOG_EXPIRY", "approval", "enum", "held 跨会话消息等待人审的期限(CC settings 键名 dialogExpiry;未设=不铸键,引擎自落缺省 5m;词表外拒启)", { danger: SEC, enumValues: [...CROSS_SESSION_DIALOG_EXPIRY_VALUES], configKey: "crossSessionDialogExpiry", derivedDefaultNote: "缺席 ⇒ 不铸键,core 自缺省 5m", resolve: (c) => c.crossSessionDialogExpiry ?? null }),
101
102
  r("CROSS_SESSION_INBOUND", "approval", "enum", "入站跨会话消息的部署/组织层治理(CC settings 键名 crossSessionInbound 的 managed 层;未设=本层不表态≠ next-turn;词表外拒启)", { danger: SEC, enumValues: [...CROSS_SESSION_INBOUND_SETTINGS], configKey: "crossSessionInbound", derivedDefaultNote: "缺席 ⇒ managed 层不设,由其余层与 mode-parity 决定", resolve: (c) => c.crossSessionInbound ?? null }),
102
- r("DB_BACKEND", "store", "enum", "store 后端引擎选择(mysql 兼容/pg/本机文件店/纯内存)", { enumValues: DB_BACKEND_WORDS, danger: DATA, derivedDefaultNote: "裸 boot ⇒ local(多租户裸 boot ⇒ memory + notice);有 SQL 信号 ⇒ mysql", configKey: "dbBackend", resolve: (c) => c.dbBackend }),
103
+ r("DB_BACKEND", "store", "enum", "store 后端引擎选择(mysql 兼容/pg/本机文件店/纯内存)", { enumValues: STORE_BACKENDS, danger: DATA, derivedDefaultNote: "裸 boot ⇒ local(多租户裸 boot ⇒ memory + notice);有 SQL 信号 ⇒ mysql", configKey: "dbBackend", resolve: (c) => c.dbBackend }),
103
104
  r("DB_QUERY_TIMEOUT_MS", "store", "number", "per-query DB 超时(ms;坏值回默认姿态+warn,绝不 NaN 进 driver)", { derivedDefaultNote: "缺席 ⇒ 池级不设/计数族 30s(按引擎)", configKey: "dbQueryTimeoutMs" }),
104
105
  r("DEFAULT_SCENARIO", "integrations", "string", "出厂缺省场景(编码产品出厂态;通用中性 persona 用 default 显式选回)", { staticDefault: "code", configKey: "defaultScenario" }),
105
106
  r("DELEGATION_MAX_CONCURRENT", "orchestration", "number", "委派入口并发帽", { derivedDefaultNote: "缺席 ⇒ 不铸键,core 自缺省(CC parity 20)" }),
@@ -232,7 +233,7 @@ export const CONFIG_CATALOG = [
232
233
  r("MEMORY_EMBEDDER_MODEL", "memory", "string", "embeddings 模型名"),
233
234
  r("MEMORY_EMBEDDER_TIMEOUT_MS", "memory", "number", "单次 embed 活性上限(ms)", { staticDefault: "30000" }),
234
235
  r("MEMORY_ENGINE", "memory", "enum", "记忆引擎总开关(注入式文件记忆面;双族词表,未知词拒启)", { enumValues: ["on", "off"], staticDefault: "on", configKey: "memoryEngineEnabled", booleanEnum: { true: "on", false: "off" } }),
235
- r("MEMORY_ENGINE_BACKEND", "memory", "enum", "记忆持久面方言(file=现状;pg/tidb=DB 真身,多租户点亮的唯一门)", { enumValues: ["file", "pg", "tidb"], staticDefault: "file", danger: DATA, configKey: "memoryEngineBackend" }),
236
+ r("MEMORY_ENGINE_BACKEND", "memory", "enum", "记忆持久面方言(file=现状;pg/tidb=DB 真身,多租户点亮的唯一门)", { enumValues: MEMORY_ENGINE_BACKENDS, staticDefault: "file", danger: DATA, configKey: "memoryEngineBackend" }),
236
237
  r("MEMORY_ENGINE_DIR", "memory", "path", "记忆引擎根覆写", { derivedDefaultNote: "缺席 ⇒ core 默认链(AGENT_DATA_DIR → ~/.ai-agent)", configKey: "memoryEngineDir" }),
237
238
  r("MEMORY_ENGINE_REMOTE_LANE", "memory", "enum", "远端执行车道上放行 file 记忆引擎的 plane-split 覆写", { enumValues: ["allow"] }),
238
239
  r("MEMORY_ORG_ADMISSION_MODE", "memory", "enum", "org 记忆准入模式(audit=判决照算纯观察)", { enumValues: ["audit", "enforce"], staticDefault: "enforce", danger: SEC, configKey: "memoryOrgAdmissionMode" }),
@@ -358,7 +359,7 @@ export const CONFIG_CATALOG = [
358
359
  r("REAP_RUN_STALE_SEC", "limitsHttp", "number", "run 活性判陈旧阈(秒)", { staticDefault: "120", configKey: "runStaleSec", center: cLimits("runStaleSec") }),
359
360
  r("REMOTE_EXEC", "orchestration", "enum", "执行车道选择(点名而配不齐/认不出 ⇒ 拒启,绝不静默降级到 host/stub)", { enumValues: REMOTE_EXEC_PROVIDER_WORDS, danger: SEC, derivedDefaultNote: "缺席:CONFIG_PROVIDER=local ⇒ host;其余 ⇒ 进程内 stub(手带整族不挂;残余名册看活体 wiring_manifest 里 mountedBy=hands 的条目数,不按工具名点。想让这一形拒启见 REQUIRE_EXECUTION_LANE)", configKey: "remoteExec", resolve: (c) => c.remoteExec?.provider ?? null }),
360
361
  r("REMOTE_EXEC_HOME_DIR", "orchestration", "path", "执行环境的 home 目录(`~/` 形权限规则的基;core ExecutionEnv.homeDir 座)", { danger: SEC, derivedDefaultNote: "缺席:host/in-process ⇒ os.homedir();e2b ⇒ /home/user;k8s/local-docker/ssh/adb/device ⇒ 无(`~/` 规则 422 响亮拒)", configKey: "remoteExecHomeDir", resolve: (c) => c.remoteExecHomeDir ?? null }),
361
- r("REQUIRE_EXECUTION_LANE", "orchestration", "boolean", "开了之后:REMOTE_EXEC 未点名 ∧ CONFIG_PROVIDER≠local ⇒ **拒启**并点名该设什么。默认关 = 照旧启动,但执行环境是进程内 stub、手带整族不挂,任务仍回 status=completed(活体读数:mountedBy=hands 条目 10 → 0,名册 38 → 19)—— 与「模型不肯用工具」在用户面同形。残余名册判据 = 活体 wiring_manifest 里 mountedBy=hands 的条目数,不按工具名点。托管/云部署推荐开(docs/DEPLOY-PREREQS.md)", { staticDefault: "false", danger: SEC, configKey: "requireExecutionLane" }),
362
+ r("REQUIRE_EXECUTION_LANE", "orchestration", "boolean", "开了之后:REMOTE_EXEC 未点名 ∧ CONFIG_PROVIDER≠local ⇒ **拒启**并点名该设什么。默认关 = 照旧启动,但执行环境是进程内 stub、**零手带工具**(手带整族不挂;活体读数:mountedBy=hands 条目 10 → 0),名册里其余工具照常执行,任务仍回 status=completed —— 凡需要手的请求,与「模型不肯用工具」在用户面同形。判据 = 活体 wiring_manifest 里 mountedBy=hands 的条目数,不按工具名点;名册总条数随 core 版本走,不写死。托管/云部署推荐开(docs/DEPLOY-PREREQS.md)", { staticDefault: "false", danger: SEC, configKey: "requireExecutionLane" }),
362
363
  r("REQUIRE_PRINCIPAL", "auth", "boolean", "多租户开关(posture 判别位:true ⇒ CC 能力族默认收紧)", { staticDefault: "false", danger: SEC, configKey: "requirePrincipal" }),
363
364
  r("RESOURCE_SUSPEND", "approval", "boolean", "durable 路径 opt-in core resourceSuspend(预算/preempt ⇒ 可恢复挂起)", { staticDefault: "false", configKey: "resourceSuspend" }),
364
365
  r("RESOURCE_SUSPEND_MAX_SLICES", "approval", "number", "resume 链片数上限(0=不设;零进展循环兜底)", { staticDefault: "0", configKey: "resourceSuspendMaxSlices" }),
@@ -408,7 +409,7 @@ export const CONFIG_CATALOG = [
408
409
  r("SERVICE_AUTH_TOKEN", "auth", "secret", "共享 service token(写面门;缺席且未开 ALLOW_UNAUTHED_WRITES ⇒ 拒提交)", { danger: SEC }),
409
410
  r("SERVICE_AUTH_TOKENS", "auth", "secret", "per-system 凭据表(tok=system CSV;source 从凭据派生,绝不自报)", { danger: SEC }),
410
411
  r("SESSION_AUTO_TITLE", "orchestration", "boolean", "首次提交自动铸 session 标题(一次 cheap 调用)", { staticDefault: "true", danger: BILL, configKey: "sessionAutoTitle" }),
411
- r("SESSION_BACKEND", "store", "enum", "session 店引擎(auto=探到 DB 用 DB,探不到回内存;回显=boot 解析后的真引擎)", { enumValues: ["memory", "mysql", "auto"], danger: DATA, derivedDefaultNote: "缺席:显式 DB_BACKEND=mysql|pg ⇒ mysql,否则 memory", configKey: "sessionBackend", resolve: (c) => (c.sessionBackend === "tidb" ? "mysql" : c.sessionBackend) }),
412
+ r("SESSION_BACKEND", "store", "enum", "session 店引擎(auto=探到 DB 用 DB,探不到回内存;回显=boot 解析后的真引擎)", { enumValues: SESSION_BACKENDS, danger: DATA, derivedDefaultNote: "缺席:显式 DB_BACKEND=mysql|pg ⇒ mysql,否则 memory", configKey: "sessionBackend", resolve: (c) => (c.sessionBackend === "tidb" ? "mysql" : c.sessionBackend) }),
412
413
  r("SESSION_CACHE_TTL_SEC", "store", "number", "session 读缓存 TTL(秒;0=关)", { staticDefault: "300", configKey: "sessionCacheTtlSec" }),
413
414
  r("SESSION_EVENTS_HOT_MS", "limitsHttp", "number", "session SSE 订阅热探针间隔(ms)", { derivedDefaultNote: "缺席 ⇒ 200" }),
414
415
  r("SESSION_EVENTS_MAX_CONNS", "limitsHttp", "number", "session SSE 并发连接帽", { derivedDefaultNote: "缺席 ⇒ 256" }),
@@ -57,6 +57,33 @@ export interface CrossInvariant {
57
57
  /** 返回拒因;`undefined` = 通过。 */
58
58
  check(input: CrossInvariantInput): string | undefined;
59
59
  }
60
+ /**
61
+ * ── S-1008 ① 推广(车 UZ 推广轮):**拒因句单铸点**是一类 —— 下面五只是 `config.ts` 区里五条跨域拒启的**判据 + 句子的唯一属主**
62
+ * (`executionLaneRequiredRefusal` / `modelProviderRefusal` 同形):boot 的 throw(`config.ts`,锚在那儿)与注册表条目的 `check` **都调它**,
63
+ * 没有第二份文本可漂。修前这五条逐条两份不同的句子(boot 手写一份、表里另写一份),boot 那份是运维在 stderr 读到的(`config-validation.test.ts`
64
+ * 钉着它的指纹),表那份只在热路径上被调 —— 而热键集带不上这些键 ⇒ 实际无读者。句子**取 boot 那份**(逐字不变);类钉 =
65
+ * `test/config-hot/batch1-cross-invariants.test.ts` 的「类钉」一格(每条带 `bootAnchor` 的条目,boot 点不手写句子、且来自注册表本人或 check 同调的函数)。
66
+ * 参数取 `CrossInvariantView` 的窄 `Pick`:boot 期手里只有装配中的各域值,不是整只 config,按真字段名铸一个小对象传入。
67
+ */
68
+ /** `direct-door-anchors` 的状态臂:半开的直连门(开着却缺锚点)。转换臂(门姿态翻转)只活在热路径,留在条目里。 */
69
+ export declare function halfOpenDirectDoorRefusal(v: Pick<CrossInvariantView, "directApprovalDoor" | "durableApproval" | "approvalHmacKeys" | "principalJwtPubkeys" | "principalJwtIss" | "principalJwtAud" | "requirePrincipal" | "operatorPrincipals">): string | undefined;
70
+ /**
71
+ * `operator-principals-required`([ref]-① 普查候选 1/2):BFF 多租户 durable 形(DURABLE_APPROVAL ∧ REQUIRE_PRINCIPAL,直连门关)的空 operator 名单 ——
72
+ * approvals-assistant 四个读面按裸 `isOperator` 划 scope,空名单 true-for-all ⇒ 任一租户 principal 可读他租户的待批队列。单租户 legacy(REQUIRE_PRINCIPAL 未设)刻意保留。
73
+ */
74
+ export declare function operatorPrincipalsRequiredRefusal(v: Pick<CrossInvariantView, "durableApproval" | "requirePrincipal" | "operatorPrincipals">): string | undefined;
75
+ /** `bake-operator-principals`(IMAGE-API-DESIGN §P2.4a):bake 门是 build-host 无限制执行面,空 operator 名单让 isOperator true-for-all = 无限制写入面。 */
76
+ export declare function bakeOperatorPrincipalsRefusal(v: Pick<CrossInvariantView, "imageBakes" | "operatorPrincipals">): string | undefined;
77
+ /**
78
+ * `bake-runner-credential`(IMAGE-API-DESIGN §P2.12 / H1):bake-runner 的身份由它的 bearer 凭据**派生**(token-derived `source`),不是自称的头 ——
79
+ * 没有 `system == BAKE_RUNNER_PRINCIPAL` 的 SERVICE_AUTH_TOKENS 条目,runner 永远过不了 isRunner,claim/ingest/heartbeat 整条管线静默全 403。
80
+ */
81
+ export declare function bakeRunnerCredentialRefusal(v: Pick<CrossInvariantView, "imageBakes" | "authTokens">): string | undefined;
82
+ /**
83
+ * `leader-durable-store`([ref] 车4 件2,台账 P1-9 / P1-10):leader run(N worker、最长 24h、以真 git push 收尾)的登记表没有外部 SQL 店就退化成进程内 Map。
84
+ * 「多副本」在 config 层判不了(副本数不是本进程的知识),可判的**代理**就是 durable 前提本身。
85
+ */
86
+ export declare function leaderDurableStoreRefusal(v: Pick<CrossInvariantView, "leaderEnabled" | "dbBackend">): string | undefined;
60
87
  export declare const CONFIG_CROSS_INVARIANTS: readonly CrossInvariant[];
61
88
  /**
62
89
  * 按 id 取**一条**(S-157②:boot 面的单源化出口)。
@@ -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,静默丢=丢数据)。 */
@@ -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;