@sema-agent/server 7.103.0 → 7.103.2

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 (39) hide show
  1. package/MIGRATION.md +55 -10
  2. package/USAGE.md +20 -6
  3. package/dist/boot/budget-tracing.d.ts +1 -0
  4. package/dist/boot/budget-tracing.js +3 -2
  5. package/dist/boot/config-center.d.ts +5 -0
  6. package/dist/boot/config-center.js +15 -17
  7. package/dist/boot/stage-04-stores.d.ts +1 -0
  8. package/dist/boot/stage-04-stores.js +2 -2
  9. package/dist/boot/stage-05-execution-env.d.ts +1 -0
  10. package/dist/boot/stage-06-runners.d.ts +1 -0
  11. package/dist/boot/stage-07-capability-layer.d.ts +1 -0
  12. package/dist/boot/stage-08-reapers.d.ts +1 -0
  13. package/dist/boot/stage-08-reapers.js +2 -2
  14. package/dist/boot/stage-09-leader.d.ts +1 -0
  15. package/dist/boot/stage-10-http-server.d.ts +1 -0
  16. package/dist/brain.d.ts +32 -1
  17. package/dist/brain.js +24 -4
  18. package/dist/config-center/apply-effective.d.ts +24 -2
  19. package/dist/config-center/apply-effective.js +9 -1
  20. package/dist/config-center/facade.d.ts +1 -1
  21. package/dist/config-center/facade.js +1 -1
  22. package/dist/config-center/restart-signal.d.ts +40 -3
  23. package/dist/config-center/restart-signal.js +11 -12
  24. package/dist/host-lsp-manager.d.ts +1 -1
  25. package/dist/model-entry-refusal.d.ts +2 -1
  26. package/dist/observability/secret-env-scrub.d.ts +1 -1
  27. package/dist/observability/secret-env-scrub.js +3 -2
  28. package/dist/plugins/remote-env-adb.d.ts +10 -3
  29. package/dist/plugins/remote-env-adb.js +24 -20
  30. package/dist/plugins/remote-env-k8s.d.ts +8 -1
  31. package/dist/plugins/remote-env-k8s.js +31 -24
  32. package/dist/plugins/remote-env-ssh.d.ts +8 -1
  33. package/dist/plugins/remote-env-ssh.js +24 -18
  34. package/dist/plugins/remote-shell.d.ts +109 -9
  35. package/dist/plugins/remote-shell.js +83 -0
  36. package/dist/server-secret-env.d.ts +23 -31
  37. package/dist/server-secret-env.js +3 -19
  38. package/package.json +3 -3
  39. package/skills/update-config.md +23 -5
package/MIGRATION.md CHANGED
@@ -7,6 +7,19 @@
7
7
  > ⚠️ 完整清单在仓库根 `CHANGELOG.md`——它**不随 npm tarball 出包**(本文件随包)。看完整迁移窗的
8
8
  > 权威姿势是源码 tag diff:`git diff v<旧>..v<新>`(每版都推 `v<版本>` tag);npm 包页也镜像 CHANGELOG。
9
9
 
10
+ ## 7.103.2 —— core 7.33.8 提货:`GIT_CONFIG_PARAMETERS` 里的嵌套命令凭据助手不论第几条都整只扣;OTLP 头变量一族不进宿主子进程(S-845)
11
+
12
+ - 宿主子进程(host shell / 三条 git 腿 / 语言服务器 / bake-runner)自本版起:`GIT_CONFIG_PARAMETERS` 里要往里读一层命令的内联凭据助手(`!sh -c '…口令…'`)不论第几条
13
+ ⇒ 整只不交(7.103.1 上第 4 条起放行;鉴权头 / URL userinfo / 口令旗等其它凭据形任何位置早就整只扣),同一只里无害的 `-c` 一起没了。
14
+ **迁移**:凭据别写进 `GIT_CONFIG_PARAMETERS`;无害设置按腿分 —— project-memory / center-plugins 两条 git 腿隔离宿主全局配置(`GIT_CONFIG_GLOBAL` / `GIT_CONFIG_SYSTEM`
15
+ = `/dev/null`),只能留在 env 里:从 `GIT_CONFIG_PARAMETERS` 删掉凭据条目只留无害设置,或改用 `GIT_CONFIG_COUNT` 组(一条一判、被扣的一条原位占位、其余照到);
16
+ host shell / leader 推送 / 语言服务器 / bake-runner 四条腿另可写进宿主 `~/.gitconfig` / `GIT_CONFIG_GLOBAL`(`docs/DEPLOY-PREREQS.md` 同段)。
17
+ - `OTEL_EXPORTER_OTLP_{TRACES,METRICS,LOGS,PROFILES}_HEADERS` 与旧拼法 `_SPAN_` / `_METRIC_HEADERS` 不再交给宿主子进程(`OTEL_EXPORTER_OTLP_HEADERS` 此前已由本服务
18
+ 按名剥)。**迁移**:语言服务器 / 构建里要用 ⇒ `LSP_ENV_ALLOW` / `BAKE_ENV_ALLOW` 点名放回。**观测面**:`OTEL_EXPORTER_OTLP_HEADERS` 被剥时计数改记
19
+ `secret_env_scrubbed_total{kind="exact-name"}`(core 的桶),不再记 `kind="config-catalog"`(本服务目录臂的桶)—— 按 `config-catalog` 桶数它的看板改看 `exact-name`;
20
+ 另外六名新出现在 `exact-name` 桶。
21
+ - 零 wire 键、零 DDL。
22
+
10
23
  ## 7.103.0(车OY 部分)—— server 进程出网认 `HTTP_PROXY` / `HTTPS_PROXY` / `NO_PROXY`(S-776)
11
24
 
12
25
  - **出网跟着代理变量走**:server 进程(HTTP 服务与 `run-local`)的出网 —— 引擎 → 网关、中心拉取 / 公告 / 凭证续签、WebSearch、对象存储、OTLP … ——
@@ -46,27 +59,59 @@
46
59
  `mounted: true` ≠ shell 可达。operator 面的 `configFingerprint` **一次性变值**
47
60
  (同装配各腿仍相等)—— 持久化 / 钉了指纹的读者会见一次跳变。
48
61
  - **`GIT_CONFIG_KEY_<i>` 不再被剥**(core 7.33.1 #1129):宿主子进程(host shell / git 腿 / 语言服务器 / bake-runner)里,普通的 git env
49
- 配置组原样到达;只有某个 KEY 的值带 `<scheme>://<userinfo>@`(令牌 insteadOf)时该组整组扣下(7.103.0 组不变量)。🔴 **安全轴披露(core
62
+ 配置组原样到达;只有某个 KEY 的值带 `<scheme>://<userinfo>@`(令牌 insteadOf)时这一条扣下(7.103.0 是本仓整组扣;7.103.1 起判定全在 core 7.33.6,见本段末条)。🔴 **安全轴披露(core
50
63
  KL-1129a)**:`GIT_CONFIG_VALUE_<i>` **不在** core 的凭据形状判据里 —— 组里 KEY 是 `http.extraHeader`、VALUE 是 `Authorization: Bearer …`
51
64
  这一形会**原样**进宿主子进程(含模型能驱动的 host shell),并在 `isolateConfig` 的 git 腿上作为命令级配置**生效**(`GIT_CONFIG_GLOBAL /
52
65
  SYSTEM=/dev/null` 挡不住 env 这一层)。**迁移**:不要把凭据放进服务进程 env 的 git 配置组;要给 git 带凭据,走 credential helper / ssh key /
53
66
  写在 `durableRemote` 里(见 DEPLOY-PREREQS)。⤷ **7.103.0 已随 core 7.33.2 修掉**(车PA 提货):core 给凭据侧加了**值族表**,
54
67
  `GIT_CONFIG_VALUE_<i>` 的值是 `Authorization:` / `Proxy-Authorization:` / `Cookie:` 头(任一行、含折行续写)、带 userinfo 的 URL
55
68
  (`<scheme>:/+<user>[:<secret>]@…`),或 proxy 地址键(`http.proxy` / `http.<url>.proxy` / `remote.<name>.proxy`)下的 `user[:secret]@host`
56
- ⇒ 按凭据剥(遥测 `secret_env_scrubbed_total{kind="family-rule"}`),本仓组不变量随即把残组**整组**扣下(与 7.32.1 同姿态;该组里别的配置
57
- 一并不到子进程)。值族表之外的形(裸令牌写在不叫头 / URL / proxy 的配置值里)仍会原样到达 —— 迁移句不变:凭据别放进服务进程 env 的 git 配置组。
69
+ ⇒ 按凭据剥(遥测 `secret_env_scrubbed_total{kind="family-rule"}`),被剥的那一条不到子进程(7.103.0 是本仓**整组**扣、该组里别的配置
70
+ 一并不到;7.103.1 起其余条目照到,见本段末条)。值族表之外的形(裸令牌写在不叫头 / URL / proxy 的配置值里)仍会原样到达 —— 迁移句不变:凭据别放进服务进程 env 的 git 配置组。
58
71
  ⤷ **core 7.33.4(7.103.0 提货;经 7.33.3,车PB / 车PC)**:KEY 与 VALUE 共用的那只 URL 判据(7.33.2 起共用)不再解析 authority —— 值里有 `:/`、
59
72
  其后任意位置有 `@` 或 `%40` 即剥;proxy 地址键下值里有 `@` / `%40` 即剥(密码含未转义 `/`、括号形、单斜杠 `https:/u:t@h`、`ssh://t%40h/…` 等此前
60
- 漏过的写法一并剥 ⇒ 整组扣)。**谁受伤**:git env 配置组的键或值里 `:/` 之后带 `@` / `%40` 的(含 `@scope` 包路径写法、`%40` 编码形;不限 URL ——
61
- scp 绝对路径 `host:/repo@v2.git`、`http.extraHeader = Referer: https://…/@scope`、`includeIf.gitdir:/home/a@corp/.path` 同)整组不到宿主子进程
62
- (fail-closed 多剥,core KL-1129b)。**出路**(只对不带凭据的这类配置;凭据照旧走 credential helper / ssh key / `durableRemote`,`~/.gitconfig`
73
+ 漏过的写法一并剥 ⇒ 这一对扣下)。**谁受伤**:git env 配置组的键或值里 `:/` 之后带 `@` / `%40` 的(含 `@scope` 包路径写法、`%40` 编码形;不限 URL ——
74
+ scp 绝对路径 `host:/repo@v2.git`、`http.extraHeader = Referer: https://…/@scope`、`includeIf.gitdir:/home/a@corp/.path` 同)那一对不到宿主子进程
75
+ (多剥,core KL-1129b;7.103.0 是整组不到,7.103.1 起同组其余条目照到)。**出路**(只对不带凭据的这类配置;凭据照旧走 credential helper / ssh key / `durableRemote`,`~/.gitconfig`
63
76
  模型可读):不隔离全局配置的腿(host shell / 语言服务器 / bake-runner / leader 推送)读宿主全局 git 配置,写进 `~/.gitconfig` 即可;center-plugins
64
77
  克隆腿每次在新目录克隆、且隔离全局 / 系统配置,这类配置在该腿上**没有替代通道**,只能改写成 `:/` 之后不带 `@` / `%40` 的形,或候 core 收窄
65
78
  KL-1129b;project-memory 只跑 `git log` / `git status`,不读这类配置。不受影响:值里没有 `:/` 的 scp 相对路径(`git@host:org/repo.git`)与 `:/`
66
- 之后没有 `@` / `%40` 的值。
67
- 🔴 **另一条 git env 通道不在剥密族表里(既有缺口 S-783,本版不修码)**:`GIT_CONFIG_PARAMETERS`(git 的命令级配置 env,形如
68
- `'http.extraheader'='Authorization: Bearer …'`)不被 core 与本仓任何一层剥 —— 值**原样**进宿主子进程(含模型能驱动的 host shell),并在
69
- 隔离了全局 / 系统 git 配置的腿上照样**生效**(它与 `git -c` 同层)。**不要**把凭据放进它;core 族表纳入它之后再收。
79
+ 之后没有 `@` / `%40` 的值;7.103.1(core 7.33.6)起 `ssh://` / `git+ssh://` / `git://` 的 userinfo 只是登录名(`ssh://git@github.com/…`,键的子节或整条值)
80
+ 也不再扣。
81
+ ⤷ **7.103.1(core 7.33.6 精确提货;车PG 退役本层、车PK 提 7.33.6):git env 配置组的判定全在 core,本仓那一层退役**。7.103.0 的「整组扣」让同组的
82
+ `protocol.file.allow=never` 这类安全策略随被剥的那一条静默消失(D-7);7.103.1 起:core 一条配置(KEY + VALUE)一次判定;COUNT 以内被扣的一条**原位**
83
+ 换占位(`GIT_CONFIG_KEY_<i>=sema.withheld` / `GIT_CONFIG_VALUE_<i>=` 空串 —— git 读成一条没人用的配置),其余条目与 `GIT_CONFIG_COUNT` 原样(不重排、
84
+ 不改写);COUNT 之外或没有 COUNT 的被扣条目整条不交。COUNT 按 git 自己的读法读(` 2` / `+2` / `02` / 空串照认;git 拒的写法原样交出,子进程里的 git 与宿主
85
+ 一样拒);POSIX 上只有大写成员名是 git 读的记录,别的拼写(`Git_Config_Key_0`、`git_config_count`)按普通变量另判(Windows 宿主照旧大小写折叠)。core 同时
86
+ 多扣:`http.extraHeader` / `http.<url>.extraHeader` 下**非空**的条目不论头名(`X-Api-Key:` / `Private-Token:` …;空的 `Authorization:` 抑制头也扣 —— 整条空值
87
+ `http.extraHeader=` 是 git 的「清掉继承头」,原样交出)、口令 / `token` / `secret` 键(`imap.pass`、`github.token`、任意节下 `pass` / `password` …)、值能被 core
88
+ 秘密扫描器认出的条目(厂商前缀令牌、JWT、私钥块、`Bearer <token>`、`token=` / `password=` 赋值)、写死口令的命令行(`core.sshCommand` / `core.gitProxy` 里的
89
+ `sshpass -p <口令>` / `plink -pw <口令>`)、选项名带 token / pass / secret / auth 且给了字面值的凭据助手(`mytool --token=<值>`)、非规范后缀(`_00` / `_x` / 空)
90
+ 的散名对(一半被名字规则剥 ⇒ 另一半随之扣)。凭据助手只按 shell 读出的字面判:引用外部口令的写法(`!f() { echo "password=$(pass show …)"; }; f`、
91
+ `!gh auth git-credential`、`store --file …`、Windows 路径)原样交出。**反向变化**(对 7.103.0):① 被扣条目在子进程里留一行 `sema.withheld=`(`git config
92
+ --list` 看得见;对 `status` / `ls-remote` / `fetch` / `includeIf` 无影响 —— core 在 git 2.50 / 2.54 上核过);② 源组本就残缺(某 `i < COUNT` 的成员缺席)且另有
93
+ 凭据条目 ⇒ 凭据条目原位占位、残缺照旧 ⇒ git 与源 env 一样报残缺(7.103.0:整组扣);③ 观测:本仓 `secret_env_scrubbed_total{kind="git-config-group"}`
94
+ 标签值不再出现,被扣的成员改在 core 的 `family-rule` 桶(读该标签的看板改看 `family-rule`),披露行只点名子进程没按原样拿到的名字;7.103.1 开发中间态的
95
+ `secret_env_git_config_pairs_withheld_total` / warn `git_config_pair_withheld` 从未发布、随层删除。
96
+ **点名放回**(`LSP_ENV_ALLOW` / `BAKE_ENV_ALLOW`)按名叠在剥除结果之上:一条被扣的配置把 `GIT_CONFIG_KEY_<i>` 与 `GIT_CONFIG_VALUE_<i>` **两只一并点名** ⇒
97
+ 这一条原样交出(COUNT 本就原样,点不点同;v7.103.0 文档写的出路 —— 点名全部 KEY / VALUE —— 照旧成立);只点一只 = 与另一只的占位半条配成一对。
98
+ **迁移**:凭据照旧不进 env 配置组。KL-1129b 多剥的无凭据配置,core 7.33.5 Pickup 段(git 2.54 真验)给的可用写法:`includeIf.gitdir:` 路径里的 `@` 写成
99
+ glob `?`;`safe.directory` 写进宿主自有的全局配置文件(`GIT_CONFIG_GLOBAL`;仓库级配置对它无效);`@scope` 包 URL 写进仓库级配置。(把 `@` 写成 `%40`
100
+ 同样触发判据 —— 不可用。)**已知边界**(core 7.33.6):① 子进程看得见占位行 `sema.withheld=`(见上);② 运行时才拼出来的口令(`$(…)` / `$VAR` / `read` 取值、
101
+ 由展开给出的程序名)core 读不到、照原样交出;③ `http.extraHeader` 下的**非凭据**头(trace / 路由头)照样按键扣,要它到达写进 git 配置文件。
102
+ **带口令的代理(core 7.33.5 #1150 / 7.33.6 #1165)**:`http_proxy` / `https_proxy` / `all_proxy` 与 `npm_config_https_proxy` / `npm_config_proxy` /
103
+ `ftp_proxy`(大小写两形)值里带 userinfo / `%40` ⇒ 宿主子进程拿不到它(`family-rule`);只有地址的代理与 `NO_PROXY` 照到;写死口令的 `GIT_SSH_COMMAND` /
104
+ `GIT_PROXY_COMMAND`(`sshpass -p <口令>` …)同样不到。出路见 `docs/DEPLOY-PREREQS.md`(服务进程自己的出网不受影响)。
105
+ ⤷ **`GIT_CONFIG_PARAMETERS`(S-783)随 core 7.33.5 #1141 收口**:git 的命令级配置 env(`git -c` 传给子进程的那一份)按 git 自己的引号语法读,
106
+ 任一条带凭据(或 git 会拒的写法)⇒ 整只不到宿主子进程;干净的列表逐字节原样。7.103.0 此处「不在剥密族表里」的披露降级为「core 扣它」。
107
+ ⤷ 7.103.2(core 7.33.8 #1183)起按判、不论第几条(7.103.1 / core 7.33.6 上第 4 条起的嵌套命令凭据助手 `!sh -c '…'` 读不到、整只原样到 —— S-845;其它凭据形任何位置
108
+ 都整只扣);任一条带凭据 ⇒ 整只变量不交,同一只里无害的 `-c` 一起没了。凭据别写进它;无害设置按腿分(隔离配置的 project-memory / center-plugins 两腿只能留在 env:
109
+ 删掉凭据条目或改用 `GIT_CONFIG_COUNT` 组;其余四腿另可写进宿主 git 配置文件)—— 出路见 `docs/DEPLOY-PREREQS.md` 同段。
110
+
111
+ ## 7.103.0(车OW 部分)—— `/health.modelRefusals` 是**免鉴权面**上的新键(S-768;7.103.0 验收 G2 格补披露)
112
+
113
+ - **披露**:`GET /health` 不鉴权(既有),7.103.0 起它多带 `modelRefusals`(与 fleet 心跳同一对象):条目名与拒因码经 settings-schema 的 `isReportableRefusalName` 出口筛过(7.103.1 换钉 6.3.0 后 = 白名单闭形 ∧ 无密钥形 ∧ 无 URL userinfo),但**合规的条目名本身会出现在免鉴权面上**。不想让匿名读到目录条目名的部署:把 `/health` 放在反代 / 网关的鉴权后面,或按 USAGE「健康面」节改用带鉴权的 `/v1/capabilities` 读同一对象。
114
+ - 无操作项;零 wire 键增删之外的变化。
70
115
 
71
116
  ## 7.102.0(车OI 部分)—— core 7.32.0 提货带来的四条(后台 shell 退出硬收 / Esc 不收后台 / 读根授权与 artifact store / parked 继承工具新失败形)
72
117
 
package/USAGE.md CHANGED
@@ -1158,8 +1158,14 @@ invalid_using_default` 警告 + 退回缺省值」这条臂随词表放宽一并
1158
1158
  `CONTEXT_TOKENS`)与两个开关(`DB_FOREIGN_KEYS` `JSON_SORT_KEYS`)不再被剥,三条子进程腿同时生效(7.100.0 上这五个 7.30.1 误剥的名
1159
1159
  `MAX_NEW_TOKENS` / `MAX_COMPLETION_TOKENS` / `MAX_BATCH_PREFILL_TOKENS` / `DB_FOREIGN_KEYS` / `JSON_SORT_KEYS` 需要点名放行,现在不用)。
1160
1160
  **精确名,不是形**:`USERS_MAX_NEW_TOKENS`、`MAX_NEW_TOKENS_2` 这类装饰过的变体照剥。引擎自己的「剥了什么」通告码
1161
- `config.secret_env_scrubbed` 只在引擎**自己**剥的子进程上发;本服务三条腿都交本服务剥好的环境,所以运维面看的是上面这行
1162
- `secret_env_scrubbed`,不是那只通告码。
1161
+ `config.secret_env_scrubbed` 只在引擎**自己**剥的子进程上发;本服务六条宿主腿(host shell / project-memory / center-plugins / leader 三条 git 腿 / 语言服务器 /
1162
+ bake-runner)都交本服务剥好的环境,所以运维面看的是上面这行
1163
+ `secret_env_scrubbed`,不是那只通告码。这只通告码**每进程一次**、引擎里几处剥除共用一闩(工具 shell / 语言服务器 / 7.103.2 起还有 `runSpec` 的
1164
+ oracle 门子进程,core 7.33.8 #1181):oracle 门若先于工具 shell 剥,通告只进 oracle 门那一处(`RunSpecOptions.onNotice`,没接就进 console),接在执行环境上的
1165
+ 结构化通告收不到。本服务不调 `runSpec`,不受影响;嵌入引擎、自接 `runSpec` 的宿主给它交同一只 `onNotice`。
1166
+ **OTLP 头变量一族(7.103.2 起,core 7.33.7 #1180)**:`OTEL_EXPORTER_OTLP_HEADERS` 与按信号分的 `_TRACES_` / `_METRICS_` / `_LOGS_` / `_PROFILES_HEADERS`、
1167
+ 旧拼法 `_SPAN_` / `_METRIC_HEADERS` 按名剥(值盲);`OTEL_EXPORTER_OTLP_HEADERS` 此前已由本服务的配置目录按名剥,另外六名此前原样交出。语言服务器 /
1168
+ bake-runner 要用 ⇒ `LSP_ENV_ALLOW` / `BAKE_ENV_ALLOW` 点名放回。
1163
1169
 
1164
1170
  **「点名放行」只有一种意思**:本服务里凡是「一张名单点到哪些环境变量键」的地方(这里的 `LSP_ENV_ALLOW`、沙箱车道的
1165
1171
  `E2B_SANDBOX_ENV` / `DOCKER_SANDBOX_ENV`(见下表),与嵌入方构造 host shell 时的 `inheritEnv:[names]`)用的是同一只谓词 ——
@@ -1939,10 +1945,18 @@ WebSearch 工具的 `tool_result` 错误文本,回到模型的对话里,**不是
1939
1945
  `secret_env_scrubbed`(warn,site = `server.bake-runner.spawn-env`,带 `allowed` 与出路句;只键名,不带值)。
1940
1946
  `PATH` / `HOME` / `NO_PROXY` / `DOCKER_CONFIG` / `BAKE_IMAGE_REGISTRY` / `RECIPE_GITSHA` 这类非密钥键照常传。
1941
1947
  docker 推送的鉴权走构建宿主上的 docker 凭据存储(`docker login`),不走 env,不受影响。
1942
- git 的 env 配置组(`GIT_CONFIG_COUNT` + `GIT_CONFIG_KEY_<i>` / `GIT_CONFIG_VALUE_<i>`)要么整组到达、要么整组不到:core 剥掉其中任一成员
1943
- (KEY 或 VALUE 的值里 `:/` 之后带 `@` / `%40`、VALUE 是鉴权头等)之后剩下的残组会让 git 起手退 128,所以残组整组扣下(随组扣下的成员在同一行
1944
- `secret_env_scrubbed` 里,`kind` = `git-config-group`)。要这组配置到达 ⇒ 把全部 `GIT_CONFIG_KEY_<i>` 与 `GIT_CONFIG_VALUE_<i>` 写进
1945
- `BAKE_ENV_ALLOW`(两边都可能被剥,只放回 KEY 仍是残组),或改用文件型 git 配置(`~/.gitconfig`,不放凭据)。
1948
+ git 的 env 配置组(`GIT_CONFIG_COUNT` + `GIT_CONFIG_KEY_<i>` / `GIT_CONFIG_VALUE_<i>`)的判定全在 core(7.103.1 起,core 7.33.6;7.103.0 是本仓
1949
+ 整组扣):带凭据的一条配置(令牌 insteadOf、鉴权头 / 任意非空 `http.extraHeader`、`:/` 之后带 `@` / `%40` —— `ssh://` / `git+ssh://` / `git://` 只带登录名的
1950
+ `ssh://git@…` 除外、口令 / `token` / `secret` 键、秘密扫描器认得出的值、写死口令的命令行(`sshpass -p <口令>` …)、选项带字面令牌的凭据助手)不交;扣在
1951
+ COUNT 以内 ⇒ 那一条**原位**换占位(`GIT_CONFIG_KEY_<i>=sema.withheld` / `GIT_CONFIG_VALUE_<i>=` 空串 —— git 读成一条没人用的配置,子进程里
1952
+ `git config --list` 看得见一行 `sema.withheld=`),其余条目与 `GIT_CONFIG_COUNT` 原样(不重排、不改写)。被扣的成员进同一行 `secret_env_scrubbed`
1953
+ (`kind` = `family-rule`,只点名子进程没按原样拿到的名字)。COUNT 按 git 自己的读法读(` 2` / `+2` / `02` 照认;git 拒的写法原样交出,子进程里的 git 照样拒);
1954
+ POSIX 上只有大写成员名是 git 读的记录,别的拼写按普通变量另判。点名放回(`BAKE_ENV_ALLOW`)按名叠在剥除结果之上:一条被扣的配置把
1955
+ `GIT_CONFIG_KEY_<i>` 与 `GIT_CONFIG_VALUE_<i>` **两只一并点名** ⇒ 这一条原样交出(只点一只 = 与另一只的占位半条配成一对)。要被扣的那类配置稳定到达,
1956
+ 改用文件型 git 配置(`~/.gitconfig`,不放凭据)。带口令的 `http_proxy` / `https_proxy` / `all_proxy` 与 `npm_config_https_proxy` / `npm_config_proxy` /
1957
+ `ftp_proxy`(大小写两形)同样不交(core 7.33.5 #1150 / 7.33.6 #1165),写死口令的 `GIT_SSH_COMMAND` / `GIT_PROXY_COMMAND` 也不交;配方真要带口令的代理 ⇒
1958
+ `BAKE_ENV_ALLOW` 点名放回 env 里**实际的拼写**(`HTTPS_PROXY` / `https_proxy`;走 `http://` 目标的工具只读小写 `http_proxy` —— curl(1)),或让代理
1959
+ 按来源免密。运行时才拼出来的口令(`$(…)` / `$VAR` 取值)core 读不到、照原样交出 —— 凭据别放进 env。
1946
1960
 
1947
1961
  **`BAKE_ENV_ALLOW=NAME,NAME`。** 配方真要一枚密钥形 env(例如 build.sh 里自己做 registry 登录要 `REGISTRY_PASSWORD`,或
1948
1962
  `git fetch` 的 askpass 读 `GIT_TOKEN`)⇒ 点名它,runner 把该键从本进程 env **原样**放回给每一只子进程组(部署方显式承担)。
@@ -14,6 +14,7 @@ export interface BudgetTracingCtx {
14
14
  }
15
15
  export declare function createBudgetAndTracing(ctx: BudgetTracingCtx): {
16
16
  brain: import("@sema-agent/core").Brain;
17
+ degradeRouteWatch: import("../brain.js").DegradeRouteWatch;
17
18
  pricing: Record<string, import("@sema-agent/core").ModelPricing>;
18
19
  counterDegradeHook: (info: {
19
20
  table: string;
@@ -1,4 +1,4 @@
1
- import { createBrain } from "../brain.js";
1
+ import { createBrain, createDegradeRouteWatch } from "../brain.js";
2
2
  import { ModelUsageTracker, PromptManifestTracker, buildPricing, createSideQueryAccountant, createTracer } from "../budget.js";
3
3
  import { FleetUsageAccumulator } from "../fleet-client.js";
4
4
  import { createFleetLeaseFromEnv } from "../fleet-lease.js";
@@ -8,6 +8,7 @@ import { assembleFileHistory } from "../file-history-capture.js";
8
8
  export function createBudgetAndTracing(ctx) {
9
9
  const { config, logger, metrics, backend, breakerState } = ctx;
10
10
  const brain = createBrain(config, { breakerState });
11
+ const degradeRouteWatch = createDegradeRouteWatch(config);
11
12
  const pricing = buildPricing(config.models);
12
13
  const counterDegradeHook = (info) => {
13
14
  metrics.setGauge("counter_flush_fail_streak", info.streak, { table: info.table, kind: info.kind });
@@ -32,7 +33,7 @@ export function createBudgetAndTracing(ctx) {
32
33
  const sessionPolicyStore = backend ? backend.sessionPolicy() : undefined;
33
34
  const { store: fileHistoryStore, erasure: fileHistoryErasure } = assembleFileHistory(backend, config.fileHistoryCapture);
34
35
  return {
35
- brain, pricing, counterDegradeHook, costQuota, modelUsageTracker, promptManifestTracker,
36
+ brain, degradeRouteWatch, pricing, counterDegradeHook, costQuota, modelUsageTracker, promptManifestTracker,
36
37
  fleetUsage, fleetLease, tracer, sideQueryAccounting, toolResultStore, sessionPolicyStore, fileHistoryStore, fileHistoryErasure,
37
38
  };
38
39
  }
@@ -11,6 +11,7 @@ import type { Logger } from "../observability/logger.js";
11
11
  import type { Metrics } from "../observability/metrics.js";
12
12
  import type { UnpricedCeilingVerdict } from "./unpriced-ceiling-verdict.js";
13
13
  import type { ConsolidationSeatWatch } from "./memory-consolidation.js";
14
+ import type { DegradeRouteWatch } from "../brain.js";
14
15
  import { type EngineRestartSignal } from "../config-center/facade.js";
15
16
  import { type ConfigApplyLedgerView } from "../config-center/apply-ledger.js";
16
17
  import type { LimitSync } from "./limit-sync.js";
@@ -57,6 +58,9 @@ export interface ConfigCenterRuntime {
57
58
  * CLI 之后不再有刷新拍,它继续用纯判据判一次)。
58
59
  * `consolidationSeat`(S-668 波 0,过渡)= 整理驱动席的两侧读数口,喂 `memory-consolidation` 重启片;
59
60
  * 缺席 = 阀门关着(没有 boot 烤死的席)。
61
+ * `degradeRoute`(S-794,**必填**)= 反应式降级壳的两侧读数口(brain.ts `createDegradeRouteWatch`,与 createBrain 同一时刻
62
+ * 捕获),喂 `degrade-route` 重启片。必填而不是可选:车道没开 / 没焊壳由席自己答「两侧同为 null」,缺席没有合法含义 ——
63
+ * 可选就是一条「接线漏了 ⇒ 片永远不签」的静默路([ref])。
60
64
  * `checkpointStorePresent`(S-668 波 2,**必填**)= 部署事实「本进程建没建出 checkpoint 店」(stage-06 的产物,
61
65
  * 不在 ServiceConfig 里)。审批名单转热之后,热腿要用它判 ④「门意图不可服务」(与 boot 拒启同一只判据);
62
66
  * 必填 = 刷新环不可能在不知道这件事的情况下热应用审批名单。 */
@@ -66,6 +70,7 @@ export interface ConfigCenterRuntime {
66
70
  swapRunnerDeps: (next: SwappableDeps) => void;
67
71
  ceilingVerdict?: UnpricedCeilingVerdict;
68
72
  consolidationSeat?: ConsolidationSeatWatch;
73
+ degradeRoute: DegradeRouteWatch;
69
74
  checkpointStorePresent: boolean;
70
75
  }): void;
71
76
  /** [ref]-2:停刷新环(hardShutdown 收尾链;幂等,未起环时 no-op)。S-698:中心凭证续签器的定时器同停(同一条收尾链)。 */
@@ -21,7 +21,7 @@ import { createConfigProvider, raceBootFetch, BOOT_FETCH_DEFERRED } from "../con
21
21
  import { executionLaneCaps, executionLaneOf } from "../execution-lane-caps.js";
22
22
  import { createKeyResolver } from "../key-resolver.js";
23
23
  import { ensureSealedKeyStore, reportExecutionPublicKey } from "../sealed-key.js";
24
- import { applyEffective, applyCenterReadFace, mutateInPlace, logEffectiveDiff, applyCenterSkills, resolveMcpServers, resolveA2aPeers, restartReasons, modelPlaneChanged, withModelPlaneOf, buildEnvOnlyModelPlane, fetchPromptArtifact } from "../config-center/facade.js";
24
+ import { applyEffective, applyCenterReadFace, mutateInPlace, logEffectiveDiff, applyCenterSkills, resolveMcpServers, resolveA2aPeers, restartReasons, modelPlaneChanged, withModelPlaneOf, bootSnapshotOf, fullyLandedForLkg, fetchPromptArtifact } from "../config-center/facade.js";
25
25
  import { createMcpRevocationLedger, enabledMcpEntries } from "../config-center/mcp-revocation.js";
26
26
  import { SHA256_HEX_RE } from "../digest-form.js";
27
27
  import { createConfigApplyLedger, modelPlaneCommitted, refusedGroups } from "../config-center/apply-ledger.js";
@@ -423,12 +423,14 @@ export async function createConfigCenterRuntime(ctx) {
423
423
  const lkgPlane = modelPlaneCommitted(lkgReport);
424
424
  if (lkgPlane)
425
425
  markRosterLanded(lkgR.effective);
426
- effective = lkgR.effective;
426
+ effective = bootSnapshotOf(lkgR.effective, lkgPlane ? lkgR.effective : undefined);
427
427
  if (lkgPlane)
428
428
  appliedPlaneEff = lkgR.effective;
429
- latestEffective = lkgR.effective;
430
- if (lkgR.etag !== undefined)
431
- ccEtag = lkgR.etag;
429
+ if (fullyLandedForLkg(lkgR, lkgReport)) {
430
+ latestEffective = lkgR.effective;
431
+ if (lkgR.etag !== undefined)
432
+ ccEtag = lkgR.etag;
433
+ }
432
434
  const cfgModels = (lkgR.effective.models?.models ?? []).filter((m) => m.enabled !== false).length;
433
435
  logger.info("config_loaded", { source: "lkg", version: lkgR.effective.version, worker: cc?.worker ?? "(global)", models: cfgModels });
434
436
  };
@@ -463,15 +465,12 @@ export async function createConfigCenterRuntime(ctx) {
463
465
  const r = raced === BOOT_FETCH_DEFERRED ? null : raced;
464
466
  if (r) {
465
467
  bootApplyPhase = true;
466
- const bootPromptsRaw = r.effective.prompts;
467
- const bootPromptsOk = bootPromptsRaw === undefined || validatePromptsDomain(bootPromptsRaw).ok;
468
- let bootClean = (r.domainErrors ?? []).length === 0 && bootPromptsOk;
469
468
  for (const de of r.domainErrors ?? [])
470
469
  logger.warn("config_domain_invalid", { domain: de.domain, error: de.error, note: "tolerated at BOOT (corrupt file → schema defaults; dangling ref → reported as-is); the refresh path rejects such a candidate whole and keeps the LKG (§9.5-5)" });
471
470
  warnDroppedEntries(r, "boot");
472
471
  if (dryRun) {
473
472
  logEffectiveDiff(config, r.effective, logger);
474
- if (bootClean)
473
+ if (fullyLandedForLkg(r, undefined))
475
474
  ccEtag = r.etag;
476
475
  }
477
476
  else {
@@ -488,8 +487,7 @@ export async function createConfigCenterRuntime(ctx) {
488
487
  },
489
488
  });
490
489
  applyLedger.applied(bootGen, r.effective.version);
491
- if (bootReport?.fullyApplied === false)
492
- bootClean = false;
490
+ const bootClean = fullyLandedForLkg(r, bootReport);
493
491
  if (bootClean)
494
492
  ccEtag = r.etag;
495
493
  await adoptCenterPrompts(r.effective, "boot");
@@ -507,7 +505,7 @@ export async function createConfigCenterRuntime(ctx) {
507
505
  }
508
506
  if (bootPlaneEff !== undefined)
509
507
  markRosterLanded(bootPlaneEff);
510
- effective = bootPlaneEff === r.effective ? r.effective : withModelPlaneOf(r.effective, bootPlaneEff ?? buildEnvOnlyModelPlane());
508
+ effective = bootSnapshotOf(r.effective, bootPlaneEff);
511
509
  if (bootPlaneEff !== undefined)
512
510
  appliedPlaneEff = bootPlaneEff;
513
511
  if (bootClean)
@@ -566,8 +564,8 @@ export async function createConfigCenterRuntime(ctx) {
566
564
  let keyResolver;
567
565
  const mcpRevocations = createMcpRevocationLedger((ev) => logger.info("mcp_revocation_ledger", { ...(ev.revoked.length > 0 ? { revoked: ev.revoked } : {}), ...(ev.restored.length > 0 ? { restored: ev.restored } : {}) }));
568
566
  let mcpRevocationWired = false;
569
- const restartCtx = (consolidationSeat) => ({
570
- ...(config.degrade?.reactive ? { reactiveDegradeTo: config.degrade.to } : {}),
567
+ const restartCtx = (consolidationSeat, degradeRoute) => ({
568
+ degradeRoute,
571
569
  pluginsAllowHosts: config.pluginsAllowHosts,
572
570
  ...(consolidationSeat !== undefined ? { consolidationSeat } : {}),
573
571
  ...(readFaceEnvHeld.length > 0 ? { readFaceEnvHeld } : {}),
@@ -738,7 +736,7 @@ export async function createConfigCenterRuntime(ctx) {
738
736
  }
739
737
  },
740
738
  startRefreshLoop(a) {
741
- const { pricing, limitSync, swapRunnerDeps, ceilingVerdict, consolidationSeat, checkpointStorePresent } = a;
739
+ const { pricing, limitSync, swapRunnerDeps, ceilingVerdict, consolidationSeat, degradeRoute, checkpointStorePresent } = a;
742
740
  if (configProvider) {
743
741
  const ccRef = config.configCenter;
744
742
  let refreshInFlight = false;
@@ -847,7 +845,7 @@ export async function createConfigCenterRuntime(ctx) {
847
845
  return "ok";
848
846
  }
849
847
  const lkgPersisted = await persistLkgDurable(r.effective, r.etag);
850
- const reasons = restartReasons(effective, r.effective, restartCtx(consolidationSeat?.read()));
848
+ const reasons = restartReasons(effective, r.effective, restartCtx(consolidationSeat?.read(), degradeRoute.read()));
851
849
  const skillsChangeDetected = reasons.includes("skills");
852
850
  if (bootSkillStale && !skillsChangeDetected)
853
851
  reasons.push("skills");
@@ -981,7 +979,7 @@ export async function createConfigCenterRuntime(ctx) {
981
979
  latestEffective = r.effective;
982
980
  ccEtag = r.etag;
983
981
  const lkgPersistedLate = await persistLkgDurable(r.effective, r.etag);
984
- const reasons = restartReasons(undefined, r.effective, restartCtx(consolidationSeat?.read()));
982
+ const reasons = restartReasons(undefined, r.effective, restartCtx(consolidationSeat?.read(), degradeRoute.read()));
985
983
  if (lkgSurvivesRestart && !lkgPersistedLate) {
986
984
  const blocked = reasons.splice(0, reasons.length);
987
985
  planeDeferredNoHandoff = { version: r.effective.version, since: Date.now(), blocked, candidate: r.effective, ...(r.etag !== undefined ? { candidateEtag: r.etag } : {}) };
@@ -2,6 +2,7 @@ import type { runStage03 } from "./stage-03-store-backend.js";
2
2
  export declare function runStage04(ctx: Awaited<ReturnType<typeof runStage03>>): Promise<{
3
3
  brain: import("@sema-agent/core").Brain;
4
4
  costQuota: import("../observability/cost-quota.js").CostQuota | import("../plugins/pg-cost-quota.js").PgCostQuota | import("../plugins/tidb-cost-quota.js").TiDBCostQuota;
5
+ degradeRouteWatch: import("../brain.js").DegradeRouteWatch;
5
6
  counterDegradeHook: (info: {
6
7
  table: string;
7
8
  kind: string;
@@ -27,7 +27,7 @@ export async function runStage04(ctx) {
27
27
  if (adoptionBoot.scanned > 0 || adoptionBoot.error !== undefined) {
28
28
  logger.info("adoption_boot_scan", { ...adoptionBoot });
29
29
  }
30
- const { brain, pricing, counterDegradeHook, costQuota, modelUsageTracker, promptManifestTracker, fleetUsage, fleetLease, tracer, sideQueryAccounting, toolResultStore, sessionPolicyStore, fileHistoryStore, fileHistoryErasure, } = createBudgetAndTracing({ config, logger, metrics, backend, breakerState });
30
+ const { brain, degradeRouteWatch, pricing, counterDegradeHook, costQuota, modelUsageTracker, promptManifestTracker, fleetUsage, fleetLease, tracer, sideQueryAccounting, toolResultStore, sessionPolicyStore, fileHistoryStore, fileHistoryErasure, } = createBudgetAndTracing({ config, logger, metrics, backend, breakerState });
31
31
  const permissionRuleStores = config.permissionRulesEnabled && backend ? backend.permissionRule() : undefined;
32
32
  const permissionRuleProvider = permissionRuleStores
33
33
  ? createPermissionRuleStoreProvider({ durable: permissionRuleStores.durable })
@@ -44,6 +44,6 @@ export async function runStage04(ctx) {
44
44
  });
45
45
  }
46
46
  await auditDormantPermissionRules({ stores: permissionRuleStores, logger, explicit: config.permissionRulesEnabledExplicit });
47
- return { ...ctx, brain, costQuota, counterDegradeHook, fileHistoryErasure, fileHistoryStore, fleetLease, fleetUsage, memoryBundleFaces, memoryComplianceFaces, memoryOriginFaces, modelUsageTracker, permissionRuleProvider, permissionRuleStores, pricing, promptManifestTracker, ruleConsent, sessionMemoryStatusFace, sessionPolicyStore, sideQueryAccounting, toolResultStore, tracer };
47
+ return { ...ctx, brain, costQuota, degradeRouteWatch, counterDegradeHook, fileHistoryErasure, fileHistoryStore, fleetLease, fleetUsage, memoryBundleFaces, memoryComplianceFaces, memoryOriginFaces, modelUsageTracker, permissionRuleProvider, permissionRuleStores, pricing, promptManifestTracker, ruleConsent, sessionMemoryStatusFace, sessionPolicyStore, sideQueryAccounting, toolResultStore, tracer };
48
48
  }
49
49
  //# sourceMappingURL=stage-04-stores.js.map
@@ -15,6 +15,7 @@ export declare function runStage05(ctx: Awaited<ReturnType<typeof runStage04>>):
15
15
  worktreeReap: (() => Promise<void>) | undefined;
16
16
  brain: import("@sema-agent/core").Brain;
17
17
  costQuota: import("../observability/cost-quota.js").CostQuota | import("../plugins/pg-cost-quota.js").PgCostQuota | import("../plugins/tidb-cost-quota.js").TiDBCostQuota;
18
+ degradeRouteWatch: import("../brain.js").DegradeRouteWatch;
18
19
  counterDegradeHook: (info: {
19
20
  table: string;
20
21
  kind: string;
@@ -76,6 +76,7 @@ export declare function runStage06(ctx: Awaited<ReturnType<typeof runStage05>>):
76
76
  worktreeReap: (() => Promise<void>) | undefined;
77
77
  brain: import("@sema-agent/core").Brain;
78
78
  costQuota: import("../observability/cost-quota.js").CostQuota | import("../plugins/pg-cost-quota.js").PgCostQuota | import("../plugins/tidb-cost-quota.js").TiDBCostQuota;
79
+ degradeRouteWatch: import("../brain.js").DegradeRouteWatch;
79
80
  counterDegradeHook: (info: {
80
81
  table: string;
81
82
  kind: string;
@@ -90,6 +90,7 @@ export declare function runStage07(ctx: Awaited<ReturnType<typeof runStage06>>):
90
90
  worktreeReap: (() => Promise<void>) | undefined;
91
91
  brain: import("@sema-agent/core").Brain;
92
92
  costQuota: import("../observability/cost-quota.js").CostQuota | import("../plugins/pg-cost-quota.js").PgCostQuota | import("../plugins/tidb-cost-quota.js").TiDBCostQuota;
93
+ degradeRouteWatch: import("../brain.js").DegradeRouteWatch;
93
94
  counterDegradeHook: (info: {
94
95
  table: string;
95
96
  kind: string;
@@ -103,6 +103,7 @@ export declare function runStage08(ctx: Awaited<ReturnType<typeof runStage07>>):
103
103
  worktreeReap: (() => Promise<void>) | undefined;
104
104
  brain: import("@sema-agent/core").Brain;
105
105
  costQuota: import("../observability/cost-quota.js").CostQuota | import("../plugins/pg-cost-quota.js").PgCostQuota | import("../plugins/tidb-cost-quota.js").TiDBCostQuota;
106
+ degradeRouteWatch: import("../brain.js").DegradeRouteWatch;
106
107
  counterDegradeHook: (info: {
107
108
  table: string;
108
109
  kind: string;
@@ -5,7 +5,7 @@ import { startRetentionLane, RETENTION_LEASE_TTL_FACTOR } from "./retention-lane
5
5
  import { createLimitSync } from "./limit-sync.js";
6
6
  import { runDenySweep } from "./late-binds.js";
7
7
  export function runStage08(ctx) {
8
- const { approvalNonceStore, backend, backgroundAgentStore, ceilingVerdict, checkpointStore, config, configCenter, consolidationSeatWatch, costQuota, fileHistoryErasure, handslessRunner, handslessSubRunner, hookAgentRunner, imageBakes, instanceId, localRoot, logger, mailboxStore, metrics, permissionRuleStores, pricing, rateLimiter, retentionExecutor, retentionLaneStore, rosterStore, runStore, runner, sqlWorkflowRunStore, subRunner, taskAttachmentStore, toolApproval, toolResultStore, workflowJournalStore, workflowNotifyGate, workflowNotifyJournal, workflowRecoverOpts, worktreeReap } = ctx;
8
+ const { approvalNonceStore, backend, backgroundAgentStore, ceilingVerdict, checkpointStore, config, configCenter, consolidationSeatWatch, costQuota, degradeRouteWatch, fileHistoryErasure, handslessRunner, handslessSubRunner, hookAgentRunner, imageBakes, instanceId, localRoot, logger, mailboxStore, metrics, permissionRuleStores, pricing, rateLimiter, retentionExecutor, retentionLaneStore, rosterStore, runStore, runner, sqlWorkflowRunStore, subRunner, taskAttachmentStore, toolApproval, toolResultStore, workflowJournalStore, workflowNotifyGate, workflowNotifyJournal, workflowRecoverOpts, worktreeReap } = ctx;
9
9
  const reaper = startReapers({
10
10
  config, logger, metrics, localRoot, backend, subRunner, runStore, checkpointStore,
11
11
  rateLimiter, costQuota, toolResultStore, fileHistoryErasure, taskAttachmentStore, imageBakes, worktreeReap,
@@ -71,7 +71,7 @@ export function runStage08(ctx) {
71
71
  }
72
72
  }
73
73
  };
74
- configCenter.startRefreshLoop({ pricing, limitSync, swapRunnerDeps, ceilingVerdict, checkpointStorePresent: checkpointStore !== undefined, ...(consolidationSeatWatch !== undefined ? { consolidationSeat: consolidationSeatWatch } : {}) });
74
+ configCenter.startRefreshLoop({ pricing, limitSync, swapRunnerDeps, ceilingVerdict, checkpointStorePresent: checkpointStore !== undefined, degradeRoute: degradeRouteWatch, ...(consolidationSeatWatch !== undefined ? { consolidationSeat: consolidationSeatWatch } : {}) });
75
75
  configCenter.initKeyResolver();
76
76
  return { ...ctx, otelExporter, reaper, retentionLaneStarted };
77
77
  }
@@ -137,6 +137,7 @@ export declare function runStage09(ctx: Awaited<ReturnType<typeof runStage08>>):
137
137
  worktreeReap: (() => Promise<void>) | undefined;
138
138
  brain: import("@sema-agent/core").Brain;
139
139
  costQuota: import("../observability/cost-quota.js").CostQuota | import("../plugins/pg-cost-quota.js").PgCostQuota | import("../plugins/tidb-cost-quota.js").TiDBCostQuota;
140
+ degradeRouteWatch: import("../brain.js").DegradeRouteWatch;
140
141
  counterDegradeHook: (info: {
141
142
  table: string;
142
143
  kind: string;
@@ -137,6 +137,7 @@ export declare function runStage10(ctx: Awaited<ReturnType<typeof runStage09>>):
137
137
  worktreeReap: (() => Promise<void>) | undefined;
138
138
  brain: import("@sema-agent/core").Brain;
139
139
  costQuota: import("../observability/cost-quota.js").CostQuota | import("../plugins/pg-cost-quota.js").PgCostQuota | import("../plugins/tidb-cost-quota.js").TiDBCostQuota;
140
+ degradeRouteWatch: import("../brain.js").DegradeRouteWatch;
140
141
  counterDegradeHook: (info: {
141
142
  table: string;
142
143
  kind: string;
package/dist/brain.d.ts CHANGED
@@ -1,5 +1,6 @@
1
- import { type Brain, type BreakerState } from "@sema-agent/core";
1
+ import { type Brain, type BreakerState, type Model } from "@sema-agent/core";
2
2
  import type { ServiceConfig } from "./config-types.js";
3
+ import { type RestartSliceCtx } from "./config-center/restart-signal.js";
3
4
  /**
4
5
  * 配了快切层(failover ≥2 路 / 断路器)时,**每路**的重试上限。
5
6
  *
@@ -41,6 +42,36 @@ export declare function createBrain(config: ServiceConfig, deps?: {
41
42
  fetchImpl?: typeof fetch;
42
43
  breakerState?: BreakerState;
43
44
  }): Brain;
45
+ /** {@link degradeRouteFingerprint} 读的那几张表(全是活引用:refresh 就地换代,读数每次现取)。 */
46
+ export type DegradeRoutePlane = Pick<ServiceConfig, "degrade" | "models" | "gatewayBaseUrl" | "modelApiKeyEnv" | "modelApiKeys">;
47
+ /**
48
+ * S-794:反应式降级壳焊的是哪一只目标 —— **唯一判据**(createBrain 组壳、启动摘要、`degrade-route` 片的两侧读数同读这一只)。
49
+ * 车道没开(非 reactive)或表里没有叫 `degrade.to` 的条目 ⇒ `undefined`(createBrain 不装降级壳)。
50
+ */
51
+ export declare function degradeTargetOf(config: Pick<ServiceConfig, "degrade" | "models">): Model | undefined;
52
+ /**
53
+ * S-794:降级壳的**路由指纹**(纯数据)—— `degrade-route` 重启片 `baked` / `current` 两侧读数的唯一读法。
54
+ *
55
+ * 壳在 createBrain 那一刻焊死目标那只 `Model` 对象(env 派生或目录派生):请求打 `model.baseUrl`(缺席回网关根)、body 带
56
+ * `model.id` / `maxTokens` / `compat` / …,走哪条腿看 `provider`;refresh 就地换的是表里的**值**,够不到那只对象。于是指纹 =
57
+ * · 那只 Model 经 `catalogEntryFingerprint`(与 swap 门同一张热字段减法:单价**数值**热、在场性不热;其余字段一律算 —— deny-list,
58
+ * 漂移的失败形是多签一次,绝不漏签);
59
+ * · 有效端点根(S-623 同一只解析器 `openaiRouteBaseUrl(baseUrl, gatewayBaseUrl)`);
60
+ * · 它配的凭据**身份**:`apiKeyEnv`(变量名,不是值)与托管密钥**在场位**(不取值:值是热的,且是秘密)。两次查表的键与
61
+ * `fbOwnApiKey` 同(目标的真名,再 `degrade.to`)。
62
+ * 没有焊壳(车道没开 / 没有目标)⇒ `null`。在场 ↔ 缺席的翻转因此自然是一次变化。
63
+ */
64
+ export declare function degradeRouteFingerprint(config: DegradeRoutePlane): unknown;
65
+ /** `degrade-route` 片的席:boot 焊进去的那一只 vs 活配置此刻的同一只目标(`restartReasons` 的 `degradeRoute` 两侧读数)。 */
66
+ export interface DegradeRouteWatch {
67
+ read(): NonNullable<RestartSliceCtx["degradeRoute"]>;
68
+ }
69
+ /**
70
+ * `create*`:返回带捕获状态(组壳那一刻的指纹)的活对象。**须在 {@link createBrain} 的同一时刻、同一份 config 上调**
71
+ * (生产:boot/budget-tracing.ts 紧随 createBrain)—— 否则 `baked` 就不是壳真焊进去的那一只。与 boot/memory-consolidation.ts
72
+ * `createConsolidationSeatWatch` 同形。
73
+ */
74
+ export declare function createDegradeRouteWatch(config: DegradeRoutePlane): DegradeRouteWatch;
44
75
  /**
45
76
  * S-753 复审(opus F2 + codex C1)—— **failover 拓扑 × 首字节段没有部署窗**的启动期披露(纯数据,`build*`;告警由 boot 出)。
46
77
  *
package/dist/brain.js CHANGED
@@ -4,6 +4,7 @@ import { buildDefaultResilience } from "./config-types.js";
4
4
  import { resolveModelApiKey } from "./key-resolver.js";
5
5
  import { dispatchBrainLane } from "./model-provider.js";
6
6
  import { openaiRouteBaseUrl } from "./model-route-endpoint.js";
7
+ import { catalogEntryFingerprint } from "./config-center/restart-signal.js";
7
8
  import { buildDisplaySafeRefusal, buildDisplaySafeUrl } from "./trace/redact.js";
8
9
  function isOffDeploymentRoot(baseUrl, config) {
9
10
  if (!baseUrl)
@@ -93,7 +94,7 @@ export function createBrain(config, deps = {}) {
93
94
  : gatewayBrain;
94
95
  if (config.degrade?.reactive) {
95
96
  const degradeTo = config.degrade.to;
96
- const fallbackModel = config.models[degradeTo];
97
+ const fallbackModel = degradeTargetOf(config);
97
98
  if (fallbackModel) {
98
99
  const fbOwnApiKey = () => resolveModelApiKey(fallbackModel.name, config.modelApiKeyEnv ?? {}, process.env, config.modelApiKeys ?? {}) ??
99
100
  resolveModelApiKey(degradeTo, config.modelApiKeyEnv ?? {}, process.env, config.modelApiKeys ?? {});
@@ -135,6 +136,25 @@ export function createBrain(config, deps = {}) {
135
136
  }
136
137
  return brain;
137
138
  }
139
+ export function degradeTargetOf(config) {
140
+ return config.degrade?.reactive ? config.models?.[config.degrade.to] : undefined;
141
+ }
142
+ export function degradeRouteFingerprint(config) {
143
+ const target = degradeTargetOf(config);
144
+ if (target === undefined || config.degrade === undefined)
145
+ return null;
146
+ const to = config.degrade.to;
147
+ return {
148
+ model: catalogEntryFingerprint(target),
149
+ endpoint: openaiRouteBaseUrl(target.baseUrl, config.gatewayBaseUrl) ?? null,
150
+ apiKeyEnv: config.modelApiKeyEnv?.[target.name] ?? config.modelApiKeyEnv?.[to] ?? null,
151
+ sealedKey: config.modelApiKeys?.[target.name] !== undefined || config.modelApiKeys?.[to] !== undefined,
152
+ };
153
+ }
154
+ export function createDegradeRouteWatch(config) {
155
+ const baked = degradeRouteFingerprint(config);
156
+ return { read: () => ({ baked, current: degradeRouteFingerprint(config) }) };
157
+ }
138
158
  export function buildFailoverFirstByteNotice(config) {
139
159
  const r = config.resilience ?? DEFAULT_RESILIENCE;
140
160
  const fallbacks = config.gatewayFallbackUrls.length;
@@ -155,7 +175,7 @@ export function brainSummary(config) {
155
175
  const r = config.resilience ?? DEFAULT_RESILIENCE;
156
176
  const hasFailover = config.gatewayFallbackUrls.length > 0;
157
177
  const breakerActive = r.circuitBreaker && hasFailover;
158
- const degradeTarget = config.degrade?.reactive ? config.models?.[config.degrade.to] : undefined;
178
+ const degradeTarget = degradeTargetOf(config);
159
179
  const degradeEndpoint = degradeTarget ? openaiRouteBaseUrl(degradeTarget.baseUrl, config.gatewayBaseUrl) : undefined;
160
180
  return {
161
181
  gateway: buildDisplaySafeUrl(config.gatewayBaseUrl ?? "") || null,
@@ -174,10 +194,10 @@ export function brainSummary(config) {
174
194
  ? `fast-fail:${FAST_FAIL_MAX_RETRIES}`
175
195
  : "engine-default",
176
196
  circuitBreakerNoop: r.circuitBreaker && !hasFailover ? "needs ≥2 gateway routes" : null,
177
- reactiveDegradeTo: config.degrade?.reactive && config.models?.[config.degrade.to] ? config.degrade.to : null,
197
+ reactiveDegradeTo: degradeTarget !== undefined ? (config.degrade?.to ?? null) : null,
178
198
  reactiveDegradeBaseUrl: buildDisplaySafeUrl(degradeEndpoint ?? "") || null,
179
199
  ...((() => {
180
- const target = config.degrade?.reactive ? config.models?.[config.degrade.to] : undefined;
200
+ const target = degradeTarget;
181
201
  if (!target)
182
202
  return { reactiveDegradeRoutePairing: null, reactiveDegradeRouteRefusal: null };
183
203
  if (dispatchBrainLane(target.provider, config.anthropic !== undefined) === "anthropic") {
@@ -95,8 +95,30 @@ export type ModelPlaneOf = Pick<EffectiveConfig, ModelPlaneDomain>;
95
95
  * 手写 `{ ...x, models, collab }`(漏一个域正是 V1 的病形)。纯数据:不改两个入参。
96
96
  */
97
97
  export declare function withModelPlaneOf<E extends EffectiveConfig>(target: E, source: ModelPlaneOf): E;
98
- /** 「没有中心模型面」—— 组拒且无 LKG 时进程真正服务的那一代(env 表:没有目录、没有中心 collab 模板)。纯数据,每次新铸。 */
99
- export declare function buildEnvOnlyModelPlane(): ModelPlaneOf;
98
+ /**
99
+ * **boot 快照**(restart 片的对照基线,`boot/config-center.ts` 的 `effective`)= 本次 boot 读到的那份(`read`:拉到的候选,或读回的盘上 LKG)
100
+ * 换上**真落地**的模型面 —— `appliedPlane` 是模型面组真落地的那一代(`read` 自己 / 按组回落的 LKG),`undefined` = 组被拒且没有可用的上一代
101
+ * (进程服务的是 env 表 ⇒「没有中心模型面」)。其余域恒是 `read` 的。
102
+ *
103
+ * boot 的两条落地路径(候选拉取、整份 LKG 读回)都经这一只,禁在调用点各写一份:S-789 的病形正是这句只在候选路径上成立(2d09c8e6),
104
+ * LKG 读回路径把被拒的 LKG 目录记成基线 ⇒ 修好的目录换掉降级目标时零 `degrade-route`,boot 焊死的降级壳永远指着 env 端点。
105
+ * S-794 起 `degrade-route` 片不再读快照的目录(改读降级壳 boot 焊的那只目标 vs 活配置,brain.ts `createDegradeRouteWatch`)——今天没有
106
+ * 重启片读快照的模型面;本函数保证的是快照本身说真话(进程真在服务的那一代),不让下一个读它的片重演 S-789。
107
+ * 纯数据:不改入参;模型面就是 `read` 自己那一代 ⇒ 原样交回(同一引用)。
108
+ */
109
+ export declare function bootSnapshotOf<E extends EffectiveConfig>(read: E, appliedPlane: ModelPlaneOf | undefined): E;
110
+ /**
111
+ * E-1(7.103.1 修复轮,opus 腿):boot 读到的这一代**整套落地**了吗 —— 只有整套落地的那一代才可以成为 LKG(`latestEffective` /
112
+ * 盘上 LKG 候选)、才让它的 ETag 做条件请求(§9.5-2 + codex R2-F1)。三件同判,缺一不算:读腿没报坏域(`domainErrors`)、
113
+ * prompts 面过候选门同一只判形、apply 报告没有被整拒的组(`fullyApplied`;报告缺席 = 没 apply,dry-run 只判前两件)。
114
+ * boot 的两条落地路径(候选拉取、整份 LKG 读回)都调这一只,禁在调用点各写一份(此前候选路径三件、LKG 路径只抄了一件)。
115
+ * LKG 读回路径上前两件在上游已有门(`loadLkg` 的载荷复判拒坏 prompts;读回不产 `domainErrors`),这里同判是同一条规则的同一只谓词,
116
+ * 不是第二道门。refresh / 迟到 boot 两腿的候选门在 apply **之前**整拒坏域与坏 prompts(拒的是整份候选,不只是 LKG 推进),不走本谓词。
117
+ */
118
+ export declare function fullyLandedForLkg(read: {
119
+ readonly effective: EffectiveConfig;
120
+ readonly domainErrors?: readonly unknown[];
121
+ }, report: ApplyReport | undefined): boolean;
100
122
  /**
101
123
  * S-768:组拒码 → 铸它的那条模型面不变量 —— **以码为键**(组拒码闭集的属主是 settings-schema `MODEL_PLANE_REFUSAL_CODES`;那边加一个码
102
124
  * ⇒ 本表缺键 ⇒ 本仓 tsc 红)。模型面 stage 的组拒按**码**铸(码是判决那刻的数据),不变量 id 由本表派生 —— 一个 id 两个码
@@ -12,6 +12,7 @@ import { DEFAULT_MODEL_MISSING_CODE, REFUSED_NAME_MINTED_CODE, judgeCatalogEntri
12
12
  import { judgeModelRouteEndpoint } from "../model-route-endpoint.js";
13
13
  import { gateIntentUnserviceableReason } from "../approval.js";
14
14
  import { refusedGroups } from "./apply-ledger.js";
15
+ import { validatePromptsDomain } from "../prompts-domain-validate.js";
15
16
  const charsPerTokenOf = (v) => (typeof v === "number" && Number.isFinite(v) && v > 0 ? v : undefined);
16
17
  function envDerivedModel(config) {
17
18
  const m = config.envModel;
@@ -204,9 +205,16 @@ function takeModelPlaneDomain(out, source, d) {
204
205
  else
205
206
  out[d] = v;
206
207
  }
207
- export function buildEnvOnlyModelPlane() {
208
+ function buildEnvOnlyModelPlane() {
208
209
  return { models: { models: [], roles: {} } };
209
210
  }
211
+ export function bootSnapshotOf(read, appliedPlane) {
212
+ return appliedPlane === read ? read : withModelPlaneOf(read, appliedPlane ?? buildEnvOnlyModelPlane());
213
+ }
214
+ export function fullyLandedForLkg(read, report) {
215
+ const prompts = read.effective.prompts;
216
+ return (read.domainErrors ?? []).length === 0 && (prompts === undefined || validatePromptsDomain(prompts).ok) && report?.fullyApplied !== false;
217
+ }
210
218
  function modelRefusalVerdictOf(group) {
211
219
  const entries = group.refused ?? {};
212
220
  return group.rejected !== undefined ? { entries, plane: group.rejected.code } : { entries };
@@ -48,7 +48,7 @@
48
48
  * importers to update, which was done in the same commit.
49
49
  */
50
50
  export { fetchEffective, fetchPrincipalCaps, fetchPrincipalOrgMemory, ConfigCenterHttpError, fetchSkillContent, fetchPromptArtifact, fetchPromptBlob, } from "./http-client.js";
51
- export { mutateInPlace, applyEffective, type ApplyReport, applyRuntimeGates, applyRuntimeHot, resolveDefaultModelName, logEffectiveDiff, MODEL_PLANE_DOMAINS, type ModelPlaneDomain, type ModelPlaneOf, withModelPlaneOf, buildEnvOnlyModelPlane, } from "./apply-effective.js";
51
+ export { mutateInPlace, applyEffective, type ApplyReport, applyRuntimeGates, applyRuntimeHot, resolveDefaultModelName, logEffectiveDiff, MODEL_PLANE_DOMAINS, type ModelPlaneDomain, type ModelPlaneOf, withModelPlaneOf, bootSnapshotOf, fullyLandedForLkg, } from "./apply-effective.js";
52
52
  export { restartReasons, modelPlaneChanged, type RestartSlice, type RestartSliceCtx, type EngineRestartSignal, } from "./restart-signal.js";
53
53
  export { applyCenterSkills, resolveMcpServers, mcpForScenario, resolveA2aPeers, a2aForScenario } from "./skills-mcp.js";
54
54
  export { applyCenterReadFace, CENTER_READ_FACE_LIVE_KEYS, type CenterReadFaceApply, type CenterReadFaceReport, type ReadFaceBaseline } from "./read-face.js";
@@ -1,5 +1,5 @@
1
1
  export { fetchEffective, fetchPrincipalCaps, fetchPrincipalOrgMemory, ConfigCenterHttpError, fetchSkillContent, fetchPromptArtifact, fetchPromptBlob, } from "./http-client.js";
2
- export { mutateInPlace, applyEffective, applyRuntimeGates, applyRuntimeHot, resolveDefaultModelName, logEffectiveDiff, MODEL_PLANE_DOMAINS, withModelPlaneOf, buildEnvOnlyModelPlane, } from "./apply-effective.js";
2
+ export { mutateInPlace, applyEffective, applyRuntimeGates, applyRuntimeHot, resolveDefaultModelName, logEffectiveDiff, MODEL_PLANE_DOMAINS, withModelPlaneOf, bootSnapshotOf, fullyLandedForLkg, } from "./apply-effective.js";
3
3
  export { restartReasons, modelPlaneChanged, } from "./restart-signal.js";
4
4
  export { applyCenterSkills, resolveMcpServers, mcpForScenario, resolveA2aPeers, a2aForScenario } from "./skills-mcp.js";
5
5
  export { applyCenterReadFace, CENTER_READ_FACE_LIVE_KEYS } from "./read-face.js";