@sema-agent/server 7.118.0 → 7.120.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 (139) hide show
  1. package/MIGRATION.md +139 -0
  2. package/README.md +1 -1
  3. package/README.zh-CN.md +1 -1
  4. package/USAGE.md +26 -13
  5. package/deploy/sema-up/README.md +2 -0
  6. package/deploy/sema-up/chart/templates/minio.yaml +2 -1
  7. package/deploy/sema-up/chart/templates/server-v4pro.yaml +1 -0
  8. package/deploy/sema-up/chart/templates/server.yaml +1 -0
  9. package/deploy/sema-up/chart/values.yaml +7 -1
  10. package/deploy/sema-up/cluster-up.sh +20 -8
  11. package/deploy/sema-up/kube-up.sh +29 -16
  12. package/deploy/sema-up/lib/buckets.sh +21 -0
  13. package/deploy/sema-up/lib/health.sh +33 -0
  14. package/deploy/sema-up/sema-up.sh +14 -9
  15. package/deploy/sema-up/smoke.sh +16 -6
  16. package/deploy/sema-up/stack/compose.yaml +1 -1
  17. package/deploy/sema-up/stack/sema-stack-cluster.yaml +4 -1
  18. package/dist/approval-ask-audit-store.js +2 -7
  19. package/dist/approval-card.d.ts +5 -5
  20. package/dist/approval.d.ts +51 -5
  21. package/dist/approval.js +76 -13
  22. package/dist/audit.d.ts +12 -1
  23. package/dist/audit.js +62 -2
  24. package/dist/auto-mode-face.d.ts +2 -3
  25. package/dist/bake-runner/main.js +1 -6
  26. package/dist/bake-runner/protocol.d.ts +18 -0
  27. package/dist/bake-runner/protocol.js +22 -2
  28. package/dist/bake-runner/runner.d.ts +12 -8
  29. package/dist/bake-runner/runner.js +5 -9
  30. package/dist/boot/config-center.js +6 -6
  31. package/dist/boot/coordinators.js +4 -1
  32. package/dist/boot/device-lane.d.ts +3 -3
  33. package/dist/boot/device-lane.js +1 -2
  34. package/dist/boot/execution-env.js +3 -2
  35. package/dist/boot/fatal-exit.d.ts +7 -1
  36. package/dist/boot/fatal-exit.js +2 -1
  37. package/dist/boot/leader.d.ts +4 -4
  38. package/dist/boot/leader.js +1 -2
  39. package/dist/boot/parked-revive-gate.d.ts +5 -1
  40. package/dist/boot/parked-revive-gate.js +8 -2
  41. package/dist/boot/resolve-spec.js +7 -4
  42. package/dist/boot/runner-deps.d.ts +4 -3
  43. package/dist/boot/runner-deps.js +7 -3
  44. package/dist/boot/stage-01-config.js +2 -0
  45. package/dist/boot/stage-07-capability-layer.js +5 -1
  46. package/dist/boot/stage-10-http-server.js +3 -2
  47. package/dist/boot/stores.js +12 -7
  48. package/dist/capabilities/builtin-tools.d.ts +31 -8
  49. package/dist/capabilities/builtin-tools.js +22 -1
  50. package/dist/capabilities/center-plugins.d.ts +2 -0
  51. package/dist/capabilities/center-plugins.js +24 -6
  52. package/dist/capabilities/code-review-council.js +8 -11
  53. package/dist/capabilities/scenarios.d.ts +42 -13
  54. package/dist/capabilities/scenarios.js +36 -9
  55. package/dist/capabilities/send-user-file-tool.d.ts +6 -3
  56. package/dist/capabilities/send-user-file-tool.js +1 -1
  57. package/dist/config-catalog.js +5 -5
  58. package/dist/config-lkg.d.ts +33 -5
  59. package/dist/config-lkg.js +55 -11
  60. package/dist/config-types.d.ts +8 -1
  61. package/dist/config.js +11 -11
  62. package/dist/deployment-governance.d.ts +8 -3
  63. package/dist/deployment-governance.js +7 -4
  64. package/dist/device-store.d.ts +7 -0
  65. package/dist/device-store.js +4 -0
  66. package/dist/device-ws-hub.js +17 -9
  67. package/dist/device-ws-protocol.d.ts +15 -0
  68. package/dist/host-decision.d.ts +52 -27
  69. package/dist/host-decision.js +14 -7
  70. package/dist/http/admission.js +6 -6
  71. package/dist/http/resume-legs.js +6 -1
  72. package/dist/http/routes/approvals-assistant.d.ts +25 -3
  73. package/dist/http/routes/approvals-assistant.js +9 -5
  74. package/dist/http/routes/capabilities.js +5 -3
  75. package/dist/http/routes/devices.js +3 -3
  76. package/dist/http/routes/memory-origin.d.ts +1 -1
  77. package/dist/http/routes/rules.js +13 -6
  78. package/dist/http/routes/runs.js +11 -5
  79. package/dist/http/routes/sessions.js +20 -16
  80. package/dist/http/wire-types.d.ts +12 -13
  81. package/dist/images/manifest.d.ts +7 -1
  82. package/dist/images/manifest.js +18 -16
  83. package/dist/list-entry-display.d.ts +14 -0
  84. package/dist/list-entry-display.js +14 -0
  85. package/dist/memory-store-refusal.d.ts +7 -12
  86. package/dist/memory-store-refusal.js +4 -5
  87. package/dist/namespaced-rule-name.d.ts +4 -3
  88. package/dist/namespaced-rule-name.js +2 -6
  89. package/dist/observability/fail-open.d.ts +9 -1
  90. package/dist/observability/fail-open.js +9 -1
  91. package/dist/operator-ask.d.ts +7 -6
  92. package/dist/plugins/approval-exemption-store.d.ts +4 -0
  93. package/dist/plugins/approval-exemption-store.js +1 -0
  94. package/dist/plugins/blob-backend.d.ts +13 -0
  95. package/dist/plugins/blob-backend.js +26 -0
  96. package/dist/plugins/local-session-store.d.ts +78 -47
  97. package/dist/plugins/local-session-store.js +152 -122
  98. package/dist/plugins/one-time-migrations.d.ts +14 -0
  99. package/dist/plugins/one-time-migrations.js +90 -3
  100. package/dist/plugins/permission-rule-store-file.js +2 -6
  101. package/dist/plugins/permission-rule-store-sql.d.ts +64 -6
  102. package/dist/plugins/permission-rule-store-sql.js +166 -59
  103. package/dist/plugins/remote-env-device.d.ts +12 -1
  104. package/dist/plugins/remote-env-device.js +13 -5
  105. package/dist/plugins/scheduler-support.d.ts +8 -4
  106. package/dist/plugins/scheduler-support.js +5 -5
  107. package/dist/plugins/snapshot-bucket-presence.d.ts +36 -0
  108. package/dist/plugins/snapshot-bucket-presence.js +40 -0
  109. package/dist/plugins/store-backend.d.ts +17 -6
  110. package/dist/plugins/store-backend.js +46 -16
  111. package/dist/plugins/store-open-wait.d.ts +33 -0
  112. package/dist/plugins/store-open-wait.js +73 -0
  113. package/dist/rule-entry-list.d.ts +0 -1
  114. package/dist/rule-entry-list.js +5 -10
  115. package/dist/rules-consent.d.ts +12 -3
  116. package/dist/rules-consent.js +5 -4
  117. package/dist/run-local.js +6 -3
  118. package/dist/security.d.ts +5 -3
  119. package/dist/security.js +4 -0
  120. package/dist/sema-config-dir.d.ts +19 -0
  121. package/dist/sema-config-dir.js +33 -0
  122. package/dist/session-policy-wire.js +10 -14
  123. package/dist/spec-fields.d.ts +7 -2
  124. package/dist/store-posture.d.ts +14 -6
  125. package/dist/store-posture.js +7 -4
  126. package/dist/task-settings.d.ts +30 -58
  127. package/dist/task-settings.js +6 -18
  128. package/dist/tool-approval.d.ts +97 -43
  129. package/dist/tool-approval.js +120 -31
  130. package/dist/trace/engine-notice-wire.d.ts +6 -4
  131. package/dist/trace/engine-notice-wire.js +16 -9
  132. package/dist/trace/redact.d.ts +6 -0
  133. package/dist/trace/redact.js +9 -9
  134. package/dist/trace/self-minted-wire.d.ts +15 -1
  135. package/dist/trace/self-minted-wire.js +2 -0
  136. package/dist/user-input-ledger.d.ts +19 -15
  137. package/dist/user-input-ledger.js +12 -20
  138. package/docs/DEVICE-EXECUTOR-PROTOCOL.md +56 -14
  139. package/package.json +2 -2
package/MIGRATION.md CHANGED
@@ -7,6 +7,145 @@
7
7
  > ⚠️ 完整清单在仓库根 `CHANGELOG.md`——它**不随 npm tarball 出包**(本文件随包)。看完整迁移窗的
8
8
  > 权威姿势是源码 tag diff:`git diff v<旧>..v<新>`(每版都推 `v<版本>` tag);npm 包页也镜像 CHANGELOG。
9
9
 
10
+ ## 7.120.0 —— 引擎 7.46.0:子代工具清单回到开跑前拒退役拼法、管到内建工具 · 配置根搬家后同护 · 回显条目一种渲染 · 路径规则锚在装入处
11
+
12
+ **升级前核查(一句话)**:设了 `SEMA_CONFIG_DIR` 的部署确认那个目录能建出来、不是空串;`CONFIG_LKG_PATH` 不是空串;
13
+ 按 `agents[].denyTools` 拼错只告的调用方改正拼写。停驻中的委派子代不必为本版排空(第 5 条)。
14
+
15
+ 1. **勘误(7.119.0 节,已发节不改字节)**:7.119.0 节说 `/health` 多一键 `sessionBackend` —— 「多一键」错:`/health.sessionBackend` 是既有键(≤7.117.0 已在),
16
+ 7.119.0 只把它与 `listening` 日志的读法收成一只、让 sema-up 的冒烟改读这一位。消费方按「已消费 / 不变」读,不要删读点。
17
+ 2. **子代 `agents[].denyTools` 的退役拼法(`bash`)与大小写孪生(`BASH`)回到开跑前拒**(`config.agent.unknown_tool`,句子点名现名 `Bash`;7.119.0 上只告不拒、子代照拿 `Bash`):
18
+ 本服务的委派工具现在声明它逐次注入的工具名册,引擎列得出子代工具池。无人挂的名照旧只告不拒(运维日志 `runner_config_advisory`)。
19
+ `denyTools` / `allowTools` 现在管子代手里的**全部**工具 —— 委派池与内建 hands(`Read` / `Edit` / `Write` / `NotebookEdit` / `Bash` / `Grep` / `Glob` …):
20
+ `denyTools: ["Write"]` ⇒ 子代没有 `Write`;`allowTools` 只列调用方工具 ⇒ 子代一只内建工具都没有(要什么点名什么);内建只读子代(Explore / Plan)没有 `Edit` / `Write` / `NotebookEdit`。
21
+ hands 之外的引擎工具(`Monitor` 等)仍不受这两张表管;要按路径 / 命令拦而保留工具,写调用方权限策略的 deny 规则。
22
+ 3. **配置根搬家(`SEMA_CONFIG_DIR`)后,根下的配置文件与家目录下同护**:根下的 `settings.json` / `settings.local.json` / `engine-data/.env` / `config-lkg-*.json`
23
+ 模型读不到(家目录 `~/.sema/` 下同名文件照旧读不到,两处都拒),写进根下要问一次。起服时建出那个目录(不在就建,权限 `0700`);建不出来或不是目录 ⇒
24
+ 引擎拒启,拒句带码 `config.agent_config_dir_invalid`(建不出来时本服务先打一行 `sema_config_dir_unprovisioned`,点名旋钮与原因)。
25
+ **`SEMA_CONFIG_DIR=`(空串)**:≤7.119.x 空串 = 进程工作目录(LKG / skill 缓存 / scheduler 店落在那里);7.120.0 起空串**拒启** —— 去掉变量用 `~/.sema`,
26
+ 或写出要的目录(要保持旧位置就写工作目录的路径)。
27
+ LKG 覆写到配置根外(`CONFIG_LKG_PATH`)⇒ 派生读拒行:写下的路径与经符号链接目录后真正落的路径各一行(拒句与内建行同一句)。行的射程按路径段串算 ——
28
+ 去掉开头的 `/`、不锚到根:这串段在任意深度出现即拒、大小写不敏感(`CONFIG_LKG_PATH=/app/config-lkg.json` ⇒ 用户仓里的 `…/app/config-lkg.json`、
29
+ `…/APP/Config-LKG.json` 同样读不到,Grep 的结果里也不出现);引擎提供锚定单文件的读拒行之前如此,要少挡就把覆写路径写得更具体。`CONFIG_LKG_PATH=`(空串)拒启。
30
+ 拒启行的形:带串码的异常印成 `<name> [<code>]: <message>`(按码匹配,不按句子)。
31
+ `AGENT_DATA_DIR` 搬到配置根之外的部署:引擎数据根下的 `.env` 不在这条规则的射程里 —— 用部署自己的读拒行(`READ_DENY_PATTERNS`)守它。
32
+ 4. **回显条目:人读句一种渲染,通告里的规则文本是原值**:拒句、起服拒启行里的工具名 / 规则条目一律是引擎的渲染 —— JSON 串形(带引号),看不见 / 印成空白的字符
33
+ 写成 `\uXXXX`,超长截断;「can never take effect」拒句里的名字与「pre-prefix short form」指引里的建议名从不带引号变成带引号。
34
+ 通告里的规则文本(`permission.allowed_tool_not_granted` 的 `detail.entries[].entry`、`permission.rule_row_dropped` 的 `detail.rows[].rule`)是条目的**脱敏原值**:
35
+ 不加引号、不转义、本服务不另截断;认出的凭据换成脱敏记号,双向控制符 / 零宽字符等格式字符剥掉,C0 控制符保留(JSON 里写成转义)。7.119.0 的 `entry` 剥掉了
36
+ 控制 / 双向符、超过 160 字截断并标原长 —— 把 `entry` 直接打到终端的消费方先自己中和控制符,或显示通告文本 `message`(那是渲染好的句子)。
37
+ 5. **停驻中的委派子代:本服务的续跑腿不触发引擎的「装入记录缺席」拒,升级前不必排空**:引擎 7.46.0 对宿主**重供**的父约束链查「每条规则装在哪条腿」的记录,
38
+ 缺了且规则依赖装入腿(相对形、`/x` 形、`~/` 形)⇒ 拒 `resume.parent_constraint_mismatch`(`detail.reason: "installation_record_absent"`)。本服务 `/decide` 一只停驻子代时
39
+ 重供的链只有部署段(审批名单、命令策略、敏感路径守卫集、誊本门、问答门),不带随腿锚定的路径规则,请求的 `settings.permissions` 规则不在链里 ⇒ 没有要拒的条目。
40
+ 实测:7.119.0 停驻(父请求带 deny `Edit(src/**)` / ask `Edit(/src/**)` / deny `Read(~/secret/**)`)→ 7.120.0 `/decide` 三形都照常续跑完。
41
+ 自建续跑腿、自己重供带这几形路径规则的父约束链的嵌入方不在此列(照引擎说明:让旧版 worker 做完,或过期后重提)。
42
+ 6. **请求规则的相对 / `/x` 形路径模式在沙箱 / 设备车道(与还没登记工作目录的 host 腿)上照收**(修前 422):引擎把规则锚在装入它的那条腿的真根;
43
+ 相对模式**不随 `cd` 走**(`Read(a/b/**)` 在 `cd sub` 之后不再够到 `sub/a/b/…`;要任意层写 `**/a/b/**`)。
44
+ 7. **新通告码 `permission.rule_row_dropped`(上会话流)**:持久规则里有项目行的 scope root 不是完整绝对路径 ⇒ 那几行不参与本 run 的检查;`detail` 点名前八行(`rows[].rule` 与第 4 条同一只写法 = 规则文本的脱敏原值)。
45
+ 这种行是 7.119.0 及更早的本服务入口收下的(`/v1/rules` 写口与两条导入面按「root 非空」收 `project:myproj`,当时引擎也收)。本版:
46
+ - **入口按引擎同一只谓词拒**(project root 必须是完整绝对路径 `/…`、`X:\…`、`\\host\…`):`POST` / `DELETE /v1/rules` 与 cc-import 预览答 `400 request.body_shape`、
47
+ `GET /v1/rules?scope=` 答 `400 request.query_invalid`、local-import 预览把那一行判 `scope_unreadable`;修前写口答 `503 state.rule_write_failed`「再发一次」(重试永远不会好)、
48
+ 撤销口答 200 而行留在店里、导入预览收下而兑付时被引擎拒。拒句从「with a non-empty root」改成「with an absolute root」。
49
+ - **SQL 后端(MySQL-protocol / PostgreSQL)起服时一次性删掉存量的这种行**(规则行与墓碑;一行一条运维 warn `permission_rule_row_removed`,带桶键、态(`kind`)、工具名与 root
50
+ (root 已脱敏),不带规则正文;启动汇总行 `one_time_migrations_applied` 的 `undurableScopeRuleRowsRemoved`)。只有 `kind: "rule"`(仍生效的那几行)的 warn 劝重写:
51
+ 还要的规则按项目的完整路径重新写。`kind: "revoked-rule"`(早已撤销:它背后的每一次同意都被同桶的撤销记录删掉了)与 `kind: "tombstone"`(撤销记录本身)随之删掉,
52
+ **不要**重加 —— 撤销照旧成立。
53
+ - **读路径整桶拒的桶一行不动**(形不合、指纹不符、behavior 不认识、撤销记录的规则文本不规范、规则文本与匹配字段不一致 —— 与读规则时整桶拒的判据是同一只):
54
+ 迁移窗内每次起服逐桶一条运维 warn `permission_rule_bucket_unreadable`(桶键 + 拒因,不带规则正文;汇总行 `undurableScopeRuleBucketsUnreadable` 计数),
55
+ 直到**人工处置** —— 处置 = 修正或删除那一行,并按既有指纹算法(`buildRuleBucketChecksum`,算在规则 / 墓碑 / 隔离三列与观测向量上)重算该桶的 `checksum`。
56
+ 这种桶在升级前后都整桶读不出;迁移不替它删行(从一只整桶拒的桶里删行再重算指纹,等于让原本被拒的规则上线)。
57
+ - **M11 回写与正常写同形**:被改动的桶里 `rules_json` / `tombstones_json` / `quarantined_json` 的旧行(写于 `behavior` 字段之前)都会补上显式 `behavior`
58
+ (缺席的本义就是 `allow`,补上的就是 `"allow"`,语义不变);`observed_vector_json` 不动;`checksum` 按回写后的字节重算。
59
+ - **local 后端删不动**:规则在引擎自己的文件店里,引擎的写面对这种 scope 一律拒 —— 那些行照旧每次 run 被丢并告知;撤销口同样删不动(本版的已知边界,候引擎给删除缝)。
60
+ 8. **引擎 7.46.0 的 shell 读法**:喂给 shell 标准输入的脚本(`bash <<'EOF' …`、`curl … | sh`、`cat x.sh | bash`、`bash < x.sh`)按 `-c` 文本判 —— 运行时拼出的一律要人答
61
+ (`dangerous_delete`,`bypassPermissions` 不免;7.119.0 节第 1 条的已知边界由此收掉);读名单里列了写程序(`cp` / `mv` / `mkdir` / `rm` / `rmdir` / `tee` / `touch` / `flock`)的部署,
62
+ 这些调用现在出卡;开放读面且没有 deny 行时,「说不清读哪」不再出强制卡;经链接 / 别名 / 大小写变体 / `cd` 后相对拼写够到受守路径的调用现在出卡或在闸上拒。
63
+
64
+ ## 7.119.0 —— 场景指派悬空不再静默落 `default`:`DEFAULT_SCENARIO` 不在册 ⇒ 拒启 · 中心指派的场景名本部署没有 ⇒ 该主体的 run 503 · `APPROVAL_REQUIRE` 的「本会话放行」恢复生效
65
+
66
+ **升级前核查(一句话)**:把 `DEFAULT_SCENARIO`(缺省 `code`)与**中心里每个主体 / 组 / 全局的场景指派**逐名对本部署的在册场景
67
+ (内建 `default` / `code` / `code-review` / `scan` / `discuss` ∪ 配置中心声明的场景;`GET /v1/capabilities` 的 `scenarios` 就是这张表)。
68
+ 对不上的名字,7.118.x 上是**静默跑在 `default`**(工具、提示词、skills、MCP 整包跟着换,零日志);本版起:
69
+
70
+ 1. **`DEFAULT_SCENARIO` 不在册 ⇒ 起服拒**,拒句点名那个名字与在册词表。改 env(或把那个场景声明回配置中心)再起。
71
+ `DEFAULT_SCENARIO` 点名的是**配置中心声明的**场景时,起服要在启动窗内拿到那份声明(中心不可达 / 超窗、且无 last-known-good 缓存 ⇒ 声明缺席 ⇒ 同样拒启,中心恢复后再起)。
72
+ 2. **中心指派的场景名不在册 ⇒ 该主体不带 `scenario` 的提交答 `503 state.scenario_assignment_unknown`**,体 `detail: { scenario, source: "ruling" }`,
73
+ 句子点名指派名与在册词表;零 run、零计费、不带 `Retry-After`。出路:在中心把指派改到在册的名字(或把那个场景加回本部署),然后原样重试
74
+ (本副本读到新指派要等它自己的缓存窗,缺省一分钟)。续跑腿上同一条拒折成 `409 resume_blocked_by_policy`(`blockedBy: "state.scenario_assignment_unknown"`)。
75
+ **在中心新增场景 X、同一拍把主体指派到 X**:本部署的场景表重启才生效、指派是热读的 ⇒ worker 重启之前那个主体的每一次 run 都是这条 503 —— 先重启 worker 让 X 进表,再指派。
76
+ 3. 不变:请求体显式给的未知名仍是 `400 scenario_unknown`;体里显式给在册名照常服务(不受悬空指派影响);在册的指派 / 缺省照常。
77
+ 4. 嵌入方自己装配 `resolveSpec`:缺省场景悬空的装配在**提交时**拿到同一条 503(`detail.source: "deployment"`),起服那一拍没人替你拒。
78
+ 5. 库面 `selectScenario(scenarios, name)`:`name` 不在册(含 `""`)现在**抛**,不再回落 `default`;不传名(`undefined`)仍取内建 `default`。
79
+ 6. `run-local`(单次本地 CLI):`--scenario <不在册的名>` 与 `DEFAULT_SCENARIO` 悬空都退出码 2、点名在册词表(此前静默落 `default`)。
80
+
81
+ **`SESSION_BACKEND=auto` 收紧(行为破坏性)**:配了坐标(`MYSQL_HOST` / `PG_HOST`)而库起服时连不上,7.118.x 上静默落内存(一行 warn、`/health` 200);本版起在 `STORE_LOCK_WAIT_MS`(缺省 60s)窗内重试、到窗**拒启**,拒句点名后端与 host:port。
82
+ 升级前核查:库晚于 server 就绪的编排(k8s 同时起)把窗调大(`STORE_LOCK_WAIT_MS=300000`);要内存就显式写 `DB_BACKEND=memory`。`/health` 的 `sessionBackend` 读到 `memory` ⇒ 这台没落库。能力位 `store.degradedReason` 的词 `db_unreachable` 随之退役(只剩 `local_unwritable`)。
83
+
84
+ **对象存储桶与 MinIO 镜像(sema-up 部署)**:引擎配了对象存储(`MINIO_*` 三键)而 `SESSION_SNAPSHOT_BUCKET`(缺省 `session-snapshots`)桶不存在,7.118.x 上起服绿、之后每分钟一条 404;本版起起服核一次、缺则在 `STORE_LOCK_WAIT_MS` 窗内重试后**拒启**。
85
+ 升级前:自己建桶的部署确认这只桶在(`mc mb <别名>/session-snapshots`)。sema-up 只有**新装、或重跑本版的 kube-up / cluster-up / sema-up.sh** 才建它;存量 sema-up 部署只做 `helm upgrade`(或只换引擎镜像)的,升级前先重跑本版脚本,或手工 `mc mb <别名>/session-snapshots`。同版 sema-up 的 MinIO 镜像从 `:latest` 钉成 `minio/minio:RELEASE.2025-09-07T16-13-09Z`(`imagePullPolicy: IfNotPresent`):
86
+ 存量部署升级前对一下正在跑的镜像(`kubectl -n sema get pod -l app=sema-minio -o jsonpath='{..imageID}'`),不同就 `--set minio.image=<正在跑的那一版>`。
87
+
88
+ ### 审批名单(`APPROVAL_REQUIRE`)的「本会话放行」恢复生效(订正 7.118.0 节第 13 条)
89
+
90
+ 1. **零操作**。7.118.0 节第 13 条说「升级前已落的『本会话放行』对 `APPROVAL_REQUIRE` 的卡失效」—— 本版起**恢复生效**:名单的问是本服务自己的问,
91
+ 它的「本会话放行」记在会话豁免店里,名单门在出问之前读它;同一会话、同一工具命中 ⇒ 名单门不出卡。豁免店的形没有变:7.117.x 及更早落下、
92
+ 7.118.x 期间留在店里的行原样生效,不迁移。不想让哪一行生效:`DELETE /v1/approvals/:sessionId/exemptions/<工具名>` 撤掉,或删会话。
93
+ 2. 名单卡上点「本会话放行」(活卡的 `allow_session` / `/decide` 的 `remember: "session"`)⇒ 记下:回执 `rememberApplied: true`,卡面不再带 `rememberRefused`。
94
+ 卡上同时还有别的问(强制、治理、读会话目录外 ……)时照旧只放行这一次。
95
+ 3. **不变**:名单的问照样 `permissionMode: "bypassPermissions"` 放不掉、auto 模式的分类器答不了 —— 免问只来自这一会话里人说过的「本会话放行」;
96
+ `APPROVAL_NEVER_AUTO`、耐久预算耗尽、请求的 `settings.permissions.ask` / 存下的 ask 行铸的卡照旧记不住(`rememberApplied: false`)。
97
+ 4. **已知边界**:「本会话放行」只在读得到同一只豁免店的实例上生效(SQL 后端跨副本共享;`DB_BACKEND=local` 只在本机数据根)。
98
+ 续跑落到读不到它的实例上 ⇒ 名单的问再问一次。
99
+ 5. **回滚到 7.118.x**:豁免行留在店里、在 7.118.x 上不生效(那一版的读法),零操作。
100
+
101
+ ### local 后端(`DB_BACKEND=local`):会话属主写进会话边档,跨重启保留
102
+
103
+ 1. **零操作**。`<数据根>/session-incarnations.json` 的每一格从「只有化身(一个串)」扩成「化身 + 属主」(`{ "incarnation": …, "owner": … | null }`);
104
+ 本版只在新建 / 登记 / 认领 / 导入时写新形,7.118.x 留下的旧格原样可读(读作「属主没被记下」)。不要手改这只文件:它是严格读的,形不对整只拒。
105
+ 2. **升级窗内的认领**:7.118.x 及更早建的会话没有记下属主,读作无主(它的会话规则读不了也改不了:`409 conflict.session_ownerless`)。
106
+ 升级之后在窗内**第一次带 principal 提交任务**到这只会话(壳的 `--resume` 就是),它被认领给那个 principal(日志一行 info `session_owner_claimed`),
107
+ 之后它的规则照常读写。窗的日期见 `plugins/one-time-migrations.ts` 登记表 M10(由本版的发布准备提交回填);窗关之后不再认领,没被认领的存量会话一直读作无主 ——
108
+ 要清它的规则就删会话(`DELETE /v1/sessions/:id`,规则随之删)。
109
+ 3. **回滚到 7.118.x**:旧版读不懂新形的格(严格读 ⇒ 会话操作整只拒,拒句点名这只文件)。回滚前先把每一格改回只有化身的串
110
+ (`{ "<id>": { "incarnation": "T", "owner": … } }` → `{ "<id>": "T" }`;属主随之丢失,即 7.118.x 本来的行为)。
111
+
112
+ ### 引擎 7.45.0(随本版提货):审批与删除的五处行为变化
113
+
114
+ 1. **删除受保护位置一律要人答**:`rm` / `rmdir` 的目标是文件系统根、顶层目录、家目录、任务根 / 工作目录 / 附加工作目录或它们的祖先(含 `rm -rf *` 在任务根、`rm -rf ~/*`),
115
+ 或 shell 拿到的是运行时才拼出的脚本(`bash -c "$CMD"`、`sh -c "$(…)"`、`eval "$(…)"`)—— 任何模式(含 `bypassPermissions`)、任何 allow(请求 `settings.permissions.allow` 的
116
+ `Bash` / `Bash(rm:*)`、`APPROVAL_GRANT`、存下的 allow 行、技能清单、PreToolUse hook 的 allow)、`auto` 的分类器都清不掉,出一张要人的卡(`askClass: "dangerous_delete"`);
117
+ 没人答就拒(无人值守腿按部署政策拒,`run-local` 无终端时只指终端)。**升级前核查**:靠 `rm -rf *` / `rm -rf <任务根>` 清场、或在 bypass 下跑运行时拼出的 `bash -c` 的脚本化部署,
118
+ 改成按名删(`rm -rf ./build ./dist`)或安排人答卡。没有旋钮放行这一类。已知边界:喂给 shell 标准输入的脚本(`bash <<'EOF' … EOF`、`echo … | bash`)这一版仍按旧判据
119
+ (`default` 出卡、bypass 放行)。
120
+ 2. **`roots` 读面 × `auto`:根外的 shell 读出卡,不再交分类器**。单用户 turnkey 部署缺省 `READ_FACE=open`,不受影响;多租户部署(设了 `REQUIRE_PRINCIPAL`、没设 `READ_FACE`
121
+ ⇒ 引擎缺省 `roots`)与显式 `READ_FACE=roots` 的部署上,`auto` 档的 `cat <根外文件>`、放不进根的拼法(heredoc 提交说明、变量路径)从分类器自动放行变成一张卡(与 `default`
122
+ 同一张;没人在就拒)。要分类器继续答 ⇒ 显式 `READ_FACE=open`。生效来源见 `GET /v1/diagnostics/wiring` 的 `readFace.source`。
123
+ 3. **子代 `agents[].denyTools` 拼错的名字不再让 run 失败**:没有工具叫这个名字(`Bahs`)⇒ 照跑,运维日志一行 `runner_config_advisory`(计数 `runner_config_advisory_total`)
124
+ 点名它 —— 那一条 deny 没生效。`Tool(content)` 形(`Edit(src/**)`)⇒ 整只工具对该子代藏起并同样告一行。仍开跑前拒(`config.agent.unknown_tool`)的:带空白 / 不可见字符的条目、
125
+ 非字符串条目。**退役拼法(`bash`)与大小写孪生(`BASH`)在本服务的部署形上也只告不拒**(7.118.x 上开跑前拒):本服务的委派工具带逐次注入的 extraTools,引擎开跑前列不全子代工具池,
126
+ 不把像 `Bash` 的名字判成拼错;引擎给出宿主声明名册的座后这两形回到开跑前拒。**升级前核查**:把「拼错就失败」当校验的调用方改看那一行告警;靠子代 `denyTools` 藏 shell / 写工具的请求改用会话策略
127
+ `toolDeny` 或部署 deny 规则 —— `denyTools` / `allowTools` 只约束委派池里的工具,带手的子代自带的 Read / Write / Edit / Bash 不受它约束(写 `"Bash"` 也藏不住;引擎后续版本一张表盖全)。
128
+ 4. **沙箱 / 设备车道上的 `//` 绝对形路径规则对相对路径生效**:这些车道上请求规则编译时没有任务根,此前一条 `Read(//w/proj/secrets/**)` 的 deny / ask 对 `secrets/k`、
129
+ `./secrets/k` 这类相对拼法不生效;现在按引擎在每条调用上带的根 / 工作目录解析(拒就拒、问就问)。调用上也没有根(无文件手的腿)⇒ 一张 `rule_unreadable` 卡
130
+ (`bypassPermissions` 带理由放行)。相对形的规则模式(`Read(src/**)`)在这些车道上仍 422。
131
+ 5. **工具名里带不可见字符(零宽、软连字符、双向控制符 …)的条目读成文法错**:`APPROVAL_GRANT` 里有这种条目 ⇒ 拒启;技能 `disallowedTools` 里有 ⇒ 提交 400、
132
+ 存量停驻体续跑 409(`resume_blocked_by_policy`)—— 升级前照 7.117.0 节跑排空判据 `skill-deny-inert`(它同数这一形);请求 `settings.permissions.allow` 里有 ⇒ 开跑时一帧
133
+ `permission.allowed_tool_not_granted`(`reason: "grammar"`)。拒句里那个字符看不见,指引句点名「invisible or control character」。
134
+
135
+ ### 接口面
136
+
137
+ - **`400 rules.legacy_tool_name` 的体顶层多一键 `guidance`**(`PUT /v1/sessions/:id/policy`、同步导入的 `policy` 面):引擎判官对这一条的改法指引原句(退役名 / 对端段铸不出的
138
+ 协议名带;无协议前缀的 `__` 名不带)。句子字节不变。对拒体做精确键集比对的客户端放宽一键。
139
+ - **运维 warn `memory_store_form_refused` 的 `form`** 只取引擎闭集里的词(`symlink` / `file` / `other`);闭集外的值不再写进这一行。409 码与句子不变。
140
+ - **嵌入方直接调引擎的 `steerPriorityOf` / `injectionLaneOf`**:档位不是 `now` / `next` / `later`(`null`、别的词、数字)现在抛 `steering.invalid_content`(此前原样返回,`null` 读作 `next`);
141
+ 只有缺席读作 `next`。本服务的受理口与停驻 steer 列读回都不会把档外值交给它。
142
+ - **嵌入方自装 `Runner` 只给静态 `executionEnv`(没有 `executionEnvFactory`)**:委派的 `isolation: "worktree"` 现在在建任何东西之前拒(`isolation.unavailable`),
143
+ 不再在调用方自己的根里跑子代。本服务的主车道带工厂,不受影响。
144
+ - **`council_done` 事件与 code-review 工具结果 `details.council`**:`lenses` / `perLens` 只含成功的镜头,失败的镜头移到新键 `failedLenses: [{ lens, reason }]`(键恒在,无失败 ⇒ `[]`);
145
+ 对 `perLens` 键集或 `lenses` 计数做精确比对的消费方更新(此前失败镜头在 `perLens` 里是一条 `tokens: 0` 的行)。
146
+ - **请求面 `permissionMode` 多一词 `dontAsk`**(闭集 = 引擎 `PERMISSION_MODES` 全集,受理后原样投引擎);能力位 `permissionModes` 给全集,壳按位发词;词表外拒句的成员表多 `dontAsk`。
147
+ - **退役键 `settledBy` 的 `400 request.body_shape`**(live `/respond`、耐久 ask 账、`/decide` 三条决定口)体顶层多一键 `field: "settledBy"`,三口同一句;壳按码 + `field` 判,不锚文案。
148
+
10
149
  ## 7.118.0 —— 审批决定体的出处位 `decidedBy`(取代 `settledBy`)· 能力位 `approvalDecidedBy` · 有审批名单却无检查点店的装配,任务提交一律 501
11
150
 
12
151
  **审批决定体用一只键说「谁做的这个决定」**:`decidedBy?: "person" | "policy"`(缺席 = `"person"`)。三条工具审批决定口 ——
package/README.md CHANGED
@@ -189,7 +189,7 @@ The server is configured entirely through environment variables. The most import
189
189
  | `SSH_HOST_FINGERPRINT` / `SSH_KNOWN_HOSTS` | unset | **SSH lane host-key verification** (`REMOTE_EXEC=ssh` only). `SSH_HOST_FINGERPRINT` = the host key's SHA256 base64 fingerprint (`SHA256:` prefix and `=` padding both optional); `SSH_KNOWN_HOSTS` = a path to a `known_hosts` file, matched literally on `host` / `[host]:port`. **Either one present ⇒ strict verification, a mismatch REFUSES the connection** (the error carries both fingerprints plus an `ssh-keyscan` line); both set ⇒ both must pass. **Both unset ⇒ a loud per-connection warning (`ssh_host_key_unverified`, man-in-the-middle risk) and the connection proceeds** — the deliberate posture for the batch-deploy lane, registered as a P-DEBT fail-open (`docs/FAIL-OPEN-CENSUS.md`). A malformed fingerprint or an unreadable `known_hosts` path **refuses to start**. Not supported in `known_hosts`, on purpose: hashed (`\|1\|…`) lines, wildcard patterns, `@cert-authority` — all skipped, so a host whose only entries are those is REFUSED (`@revoked` is honored and wins over any plain line, whatever the order) |
190
190
  | `HOOKS_TIMEOUT_MS` | unset (engine default 600000) | Per-invocation time bound for every hook seat (core `Hooks.timeoutMs`). Unset = the engine's own default (this server does not copy upstream defaults). `0` is honored as written (every seat expires immediately). A non-integer, negative, or above-`setTimeout`-ceiling (2147483647) value **refuses to start** |
191
191
  | `CONFIG_PROVIDER` | unset | Config source: `local` (file-backed `config.d/`, single machine) / `remote` (registry control plane). Closed set, matched exactly (7.104.0+): any other value (wrong case, surrounding spaces) refuses to boot; empty = unset |
192
- | `DEFAULT_SCENARIO` | `code` | Default scenario when the request body names none |
192
+ | `DEFAULT_SCENARIO` | `code` | Default scenario when the request body names none; must be a scenario this deployment has (boot refuses otherwise) |
193
193
  | `SANDBOX_PKG_SOURCE` | `global` | Package sources inside sandboxes: `global` (official upstreams) / `cn` (China mirrors) / `custom` / `none` |
194
194
  | `SENSITIVE_WRITE_PATTERNS` | core's recommended set | Sensitive-path write deny list; comma-separated value replaces the set, `off` **or an empty value** disables. Applied unconditionally at the governance layer (independent of client permission mode, lane or settings presence) — including on the `run-local` leg. A value that cannot compile into a guard set (e.g. `/`, a pattern with no path segment) refuses to start |
195
195
  | `WRITE_PROTECTED_EXTRA` | unset (engine default table) | **Adds** rows to core's write-protection table (the literal-name table whose hit demotes a surviving `allow` to `ask` on Write/Edit/NotebookEdit). Comma-separated bare names (a bare name matches ANY path segment; a name containing `/` matches a consecutive segment run) or a JSON array of `"name"` strings / `{name, kind}` rows (`kind`: `basename` \| `segment` \| `segment-run`). The value is composed as `[...WRITE_PROTECTED_DEFAULT_TABLE, …]`, so **no default row can be lost**. Unset = no seat is wired = the engine's default table is in force (this server never copies that table). Form discrimination is by content, not by first character: a value containing any JSON structural character (`[ ] { } "`) is parsed as JSON and **must** be a top-level array (a missing pair of brackets refuses to start instead of being split into junk bare names). An empty value, a malformed entry (glob metacharacter, unknown kind, kind/name mismatch) or setting this together with `WRITE_PROTECTED_TABLE_REPLACE` **refuses to start**. Read faces: `GET /v1/capabilities` → `writeProtection.{armed,rows,replaced}`; `GET /v1/diagnostics/wiring` → `writeProtection.{rows,source,droppedDefaultRows}` (operator-only) |
package/README.zh-CN.md CHANGED
@@ -162,7 +162,7 @@ curl -s localhost:8090/v1/tasks -H "Authorization: Bearer <SERVICE_AUTH_TOKEN>"
162
162
  | `SSH_HOST_FINGERPRINT` / `SSH_KNOWN_HOSTS` | 未设 | **SSH 通道主机密钥校验**(仅 `REMOTE_EXEC=ssh`)。前者 = 主机公钥的 SHA256 base64 指纹(`SHA256:` 前缀与 `=` 填充都可选);后者 = `known_hosts` 文件路径,按 `host` / `[host]:port` **逐字**匹配行。**任一在场 ⇒ 严格校验,不符即拒连**(拒因带两半指纹 + `ssh-keyscan` 取指纹指路一行);两只同时在场 ⇒ 合取(都要过)。**两只全缺席 ⇒ 每连接一条响亮 warn(`ssh_host_key_unverified`,中间人风险)后照常连** —— 批量部署通道的既定姿态,已登记为 P-DEBT fail-open(`docs/FAIL-OPEN-CENSUS.md`)。指纹拼错 / `known_hosts` 路径读不到 ⇒ **启动即拒**。`known_hosts` 里**刻意不支持**:hashed(`\|1\|…`)行、通配符 pattern、`@cert-authority` —— 一律跳过,于是只有这些条目的主机会被**拒连**(`@revoked` 照常生效,且无论写在哪一行都优先于普通匹配行) |
163
163
  | `HOOKS_TIMEOUT_MS` | 未设(引擎缺省 600000) | 每一只 hook 席位**单次调用**的时间上限(core `Hooks.timeoutMs`)。未设 = 用引擎自己的缺省(本服务不复制上游默认值)。`0` **按字面生效**(每个席位立即到期)。非整数 / 负数 / 超 `setTimeout` 上限(2147483647)⇒ **启动即拒** |
164
164
  | `CONFIG_PROVIDER` | 未设 | 配置来源:`local`(单机文件 `config.d/`)/ `remote`(registry 控制面)。闭集、逐字比较(7.104.0 起):其余任何值(大小写不同、带首尾空白)拒启;空串 = 未设 |
165
- | `DEFAULT_SCENARIO` | `code` | 请求体未指定场景时的缺省场景 |
165
+ | `DEFAULT_SCENARIO` | `code` | 请求体未指定场景时的缺省场景;必须是本部署在册的场景名,否则拒启 |
166
166
  | `SANDBOX_PKG_SOURCE` | `global` | 沙箱内装包源:`global`(官方源)/ `cn`(国内镜像)/ `custom` / `none` |
167
167
  | `SENSITIVE_WRITE_PATTERNS` | core 推荐集 | 敏感路径写拒集;逗号分隔值为整体替换,`off` **或留空**关闭。在治理层无条件施加(与客户端权限模式/lane/settings 在场性无关),`run-local` 腿同样生效;编译不出守卫集的值(如 `/`)启动即拒 |
168
168
  | `WRITE_PROTECTED_EXTRA` | 未设(引擎缺省表) | 给 core 的**写保护名表**加行(字面名表:命中即把幸存的 `allow` 降级成 `ask`,作用于 Write/Edit/NotebookEdit)。逗号分隔裸名(裸名匹配**任意路径段**,含 `/` 的名匹配连续段)或 JSON 数组(`"name"` 串 / `{name, kind}` 行,`kind`:`basename` \| `segment` \| `segment-run`)。值按 `[...core 缺省表, …]` 组合,**丢不掉任何缺省行**。未设 = 不铸座 = 引擎缺省表在岗(本仓从不复制那张表)。两形按**内容**判而不是猜首字符:值里出现 JSON 结构字符(`[ ] { } "`)即按 JSON 解析,且顶层**必须是数组**(少写一对方括号 ⇒ 拒启,而不是被拆成垃圾裸名静默收下)。空值 / 坏值(通配符、未知 kind、kind 与名字段数矛盾)/ 与 `WRITE_PROTECTED_TABLE_REPLACE` 同时设置 ⇒ **启动即拒**。读面:`GET /v1/capabilities` 的 `writeProtection.{armed,rows,replaced}`;`GET /v1/diagnostics/wiring` 的 `writeProtection.{rows,source,droppedDefaultRows}`(operator-only) |
package/USAGE.md CHANGED
@@ -51,9 +51,10 @@ docker compose up --build --scale service=4 # 4 个无状态 worker,nginx
51
51
  > 而 `DB_BACKEND` 缺省 ⇒ 探的是 mysql)。修前这三形**静默跑内存**(什么都不设是 local 文件持久,加一族坐标反而落到内存)。坐标被用到的存量形
52
52
  > (`SESSION_BACKEND=mysql|auto` + MYSQL 坐标、不写 `DB_BACKEND`)照旧起服;「不用坐标」是显式表态的(`DB_BACKEND=memory|local`、
53
53
  > `SESSION_BACKEND=memory`)也不拒。拒句点名在场的坐标键、为什么没人用、三条出路:要持久 `DB_BACKEND=pg|mysql`(或 `SESSION_BACKEND=mysql`);
54
- > 要内存 去掉坐标或 `DB_BACKEND=memory`;要尽力而为 `SESSION_BACKEND=auto` 并把引擎指对(`DB_BACKEND=pg` 配 `PG_HOST`)。本仓两份 compose
55
- > 显式带 `DB_BACKEND=mysql`。同版:`DB_BACKEND=mysql|pg` 显式而 `SESSION_BACKEND` 不许降级时店打不开,拒句同样点名 `DB_BACKEND=<引擎>` 与
56
- > `SESSION_BACKEND=auto` 出路(修前只有驱动的一句 `connect ECONNREFUSED …`)。持久化姿态读面:caps `store`(附录 B)。
54
+ > 要内存 去掉坐标或 `DB_BACKEND=memory`。本仓两份 compose
55
+ > 显式带 `DB_BACKEND=mysql`。同版:`DB_BACKEND=mysql|pg` 显式而店打不开,拒句点名 `DB_BACKEND=<引擎>`(修前只有驱动的一句 `connect ECONNREFUSED …`)。
56
+ > 🔴 **7.119.0 起 `SESSION_BACKEND=auto` 配了坐标而库连不上同样拒启,不再有「auto 内存起服」这条出路**:`auto` = 「配了该引擎的坐标就是那只库(boot 期窗内重试、到窗拒启),没配就是内存」,
57
+ > 由配置决定、不由起库成败决定;内存部署写 `DB_BACKEND=memory` 或 `SESSION_BACKEND=memory`。`GET /health` 的 `sessionBackend` 如实说装好的后端(`durable(pg)` / `memory`)。持久化姿态读面:caps `store`(附录 B)。
57
58
 
58
59
  > 🔴 7.96.0 起根 `docker-compose.yml` 的 `REMOTE_EXEC`(执行车道)是**必填**:不设 ⇒ `docker compose config` 当场失败(此前静默起成一个没有手的 worker)。单人自用 `REMOTE_EXEC=host`(手落在 worker 容器里);多人 / 生产选隔离车道 `e2b`(配 `E2B_API_KEY`)或 `k8s`(配 `K8S_SANDBOX_IMAGE`)。
59
60
 
@@ -537,6 +538,8 @@ MODEL_CASCADE_LADDER=deepseek-flash,deepseek-pro # 目录里的模型名,cheap
537
538
  🔴 **两条取数据根的路是同一条腿、同一只旋钮**:HTTP 服务的 local 臂与 `run-local`(一次性 CLI)共用
538
539
  同一只等待腿 —— **不为前台形留第二套语义**。前台形要「立刻告诉我谁占着」就设 `STORE_LOCK_WAIT_MS=0`,
539
540
  那是文档化的通道,而不是两条路各有一套默认。
541
+ 🔴 **同一只窗 7.119.0 起也管「已配置的 SQL 库此刻连不上」**:`DB_BACKEND=mysql|pg`(含 `SESSION_BACKEND=auto`)boot 期开不了店 ⇒ 窗内每 250ms 重试、到窗拒启(点名后端与 host:port;
542
+ 永久错如凭据 / DDL 权限也拖到窗尾才拒,要当场拒设 `0`)。k8s 清单里库与 server 同时起,所以清单把窗给到 `300000`。
540
543
  ⚠️ **裸 boot(没有显式 `DB_BACKEND`)那一形的时序也跟着变**:它的锁冲突走的不是拒启而是既有的
541
544
  「退回内存并 warn」降级臂(`store_db_unreachable_fallback_memory`),所以第二只实例此前**当场**降级、
542
545
  现在要**等满窗**才降级 —— 方向仍是好的(窗内拿到就用上真数据根,而不是静默丢掉持久面),但它是一次
@@ -859,12 +862,18 @@ UNATTENDED_APPROVAL_POLICY=park
859
862
 
860
863
  **「本会话不再询问」对引擎的问也生效(7.106.0 起)**
861
864
 
862
- - 审批面 `POST /v1/approvals/:sessionId/decide {decision:"approve", remember:"session"}` 与活卡的 `allow_session` 记下的放行,两处兑现:
863
- 本服务自己的 `ordinary` 类问(`run_workflow` 问句、acceptEdits 的域外写)在它们自己的层上不再问;**引擎自己的问**(`default`
864
- 模式下的每一次写、证不了只读的 shell 命令)由服务的**审批席**(工具审批协调器)在下一次到达时代答。后一半要审批席在场:
865
+ - 审批面 `POST /v1/approvals/:sessionId/decide {decision:"approve", remember:"session"}` 与活卡的 `allow_session` 记下的放行,两处兑现 —— **谁问的,谁读它的答案**:
866
+ 本服务自己的问在它们自己的层上、出问之前读,命中就不问:`APPROVAL_REQUIRE` 名单的问(7.119.0 起;卡面带「本会话放行」,回执 `rememberApplied:true`,
867
+ 同一会话下一次调用名单门不出问,引擎看到的只是「这次调用没出问」)、`run_workflow` 问句、acceptEdits 的域外写;**引擎自己的问**(`default`
868
+ 模式下的每一次写、证不了只读的 shell 命令、没人表态的服务器工具)由服务的**审批席**(工具审批协调器)在下一次到达时代答。后一半要审批席在场:
865
869
  `TOOL_APPROVAL_ENABLED` 开(多租户缺省关);不想要活卡(纯 park 部署)再配 `STREAM_ASK_WINDOW_MS=0`。
866
- - 代答不到的:`APPROVAL_DENY` / 敏感路径守卫(是拒,不是问);`APPROVAL_REQUIRE` 名单的问(`ask_rule` 类 —— 引擎的类表说 allow 层清不掉它,站着的答案同读;每次照问,回执 `rememberApplied:false`;要免问就把工具移出名单或开自动批额度);`APPROVAL_NEVER_AUTO` 与耐久预算耗尽的问(「恒问人」,回执 `rememberApplied:false`);
870
+ 名单的问在引擎眼里仍是 `ask_rule`:`permissionMode: "bypassPermissions"` 放不掉、auto 模式的分类器答不了 —— 免问只来自这一会话里人亲口说过的「本会话放行」。
871
+ - 代答不到的:`APPROVAL_DENY` / 敏感路径守卫(是拒,不是问);`APPROVAL_NEVER_AUTO` 与耐久预算耗尽的问(「恒问人」,回执 `rememberApplied:false`);
872
+ 人写的 ask 规则(请求的 `settings.permissions.ask`、存下的 ask 行 —— 引擎的问,类表说站着的答案清不掉,回执 `rememberApplied:false`);
867
873
  委派子代经父链继承到祖先席上的引擎问(引擎盖强制位,席依约不代答)。
874
+ - 已知边界:「本会话放行」记在本部署的会话豁免店(SQL 后端跨副本共享;`DB_BACKEND=local` 只在这台机器的数据根里)。续跑落到另一台**读不到这只店**的
875
+ 实例上(local 车道换机 / 换数据根)⇒ 名单的问再问一次 —— 方向是多问一次,不是静默放行。名单靠别名命中的调用(嵌入方给工具挂了 `Bash` 别名)
876
+ 按调用的 wire 名记、按 wire 名读;一张卡上同时有名单的问与别人的问(人写的 ask 规则、强制位、读会话目录外 ……)⇒ 那张卡的「本会话放行」不记。
868
877
 
869
878
  **无人值守腿:`default` 模式 + 按类作答的审批席(7.109.0 起;S-934 / S-906)—— 没有旋钮**
870
879
 
@@ -1392,7 +1401,7 @@ bake-runner 要用 ⇒ `LSP_ENV_ALLOW` / `BAKE_ENV_ALLOW` 点名放回。
1392
1401
  | `sessionId` | — | 续聊:带上次返回的 `sessionId`,服务端自动 wake 历史 |
1393
1402
  | `images` | — | 图文输入 `[{data,mimeType}|{url}]`(模型需支持 vision) |
1394
1403
  | `attachmentIds` | — | D-1 通用文件上传(1.289+):先 `POST /v1/attachments?name=…`(raw body,content-type=mime)拿句柄,提交时引用 ≤16 个;文件物化到执行环境工作目录 `attachments/` 下,objective 尾部自动追加文件清单(内容不进会话流)。单文件缺省 ≤32 MiB(`ATTACHMENT_MAX_BYTES`);可配 mime 白名单(`ATTACHMENT_MIME_ALLOWLIST` CSV,缺省不限);上传后未引用的按 `ATTACHMENT_UNBOUND_TTL_MS`(缺省 24h)回收。**云形态(tidb/pg)字节本体存对象存储——MinIO 必配**(`MINIO_ENDPOINT/MINIO_ACCESS_KEY/MINIO_SECRET_KEY`,与快照 lane 同一组变量),未配则附件面 501;local 形走本地文件店。 |
1395
- | `scenario` | — | 省略时取本部署的 `DEFAULT_SCENARIO`(工厂缺省场景名 = **`code`**——CC 编码 persona 蒸馏版,default 全量工具面,终验旋钮独立;`default` 场景**已非出厂缺省**,要用它须显式传 `scenario:"default"`,或部署方显式设 `DEFAULT_SCENARIO=default`)。可选内建名:`default`(通用/助理场景,域中性 persona,CC agent loop 全量工具面)/ `code-review`(见 §5)/ `scan`(同 §5 的 repo 只读工具但**中性无框架提示词**——objective+中心下发 skill 全权主导输出,OA 扫描类用;与 `code-review`/`discuss` 同属 clone-free 无执行环境场景,见 §5 末)/ **配置控制面可声明任意新场景**(`{name, toolset: none\|repo-readonly, prompt?}`,组合即配置、能力写死在部署;restart-to-apply;center 可覆盖内建名,boot 日志 `config_center_scenarios.shadowsBuiltin` 可审计) |
1404
+ | `scenario` | — | 省略时取本部署的 `DEFAULT_SCENARIO`(工厂缺省场景名 = **`code`**——CC 编码 persona 蒸馏版,default 全量工具面,终验旋钮独立;`default` 场景**已非出厂缺省**,要用它须显式传 `scenario:"default"`,或部署方显式设 `DEFAULT_SCENARIO=default`)。可选内建名:`default`(通用/助理场景,域中性 persona,CC agent loop 全量工具面)/ `code-review`(见 §5)/ `scan`(同 §5 的 repo 只读工具但**中性无框架提示词**——objective+中心下发 skill 全权主导输出,OA 扫描类用;与 `code-review`/`discuss` 同属 clone-free 无执行环境场景,见 §5 末)/ **配置控制面可声明任意新场景**(`{name, toolset: none\|repo-readonly, prompt?}`,组合即配置、能力写死在部署;restart-to-apply;center 可覆盖内建名,boot 日志 `config_center_scenarios.shadowsBuiltin` 可审计)。**缺省与指派都必须是本部署在册的场景名,悬空不回落 `default`**:`DEFAULT_SCENARIO` 不在册 ⇒ **起服拒**(拒句点名那个名字与在册词表);中心按主体指派的场景名本部署没有 ⇒ 该主体的 run 答 **503 `state.scenario_assignment_unknown`**(体 `detail: { scenario, source: "ruling" }`,不带 `Retry-After`;修法 = 中心改指派,或把那个场景加回本部署,然后原样重试);请求体显式给的未知名仍是 400 `scenario_unknown` |
1396
1405
  | `repo` / `council` / `debate` | — | `repo` 为 `code-review`/`scan` 必填;`council`/`debate` 仅 `code-review`,见 §5 |
1397
1406
  | `systemPrompt` / `appendSystemPrompt` | — | 调用方自带的系统提示 / 追加段(各 ≤16384 字符,超限 400 `request.field_invalid`;`appendSystemPrompt` 必须非空串)—— 任务自带时优先于角色 / 全局缺省(见 §模型角色) |
1398
1407
  | `model` | — | 本次用哪个模型:只能点服务端**已配置目录**里的名字 / 档位词 / 模型 id(目录见 `GET /v1/models`;请求体带不进新模型)。未知 ⇒ 400 `request.unknown_reference`(带 `available` 列表);目录里有、但本部署服务不了 ⇒ 400 `request.model_unavailable`;空串 / 非字符串 ⇒ 400 `request.field_invalid` |
@@ -1516,7 +1525,7 @@ curl -N http://<host>:8090/v1/tasks/stream -H 'content-type: application/json' \
1516
1525
  ## 7. 其它接口(按需)
1517
1526
  | 端点 | 用途 |
1518
1527
  |---|---|
1519
- | `GET /v1/sessions/<id>` | 审计回溯:当前上下文 + 摘要(owner 校验) |
1528
+ | `GET /v1/sessions/<id>` | 审计回溯:当前上下文 + 摘要(owner 校验);消息里模型 / 工具产出的文本与 live 帧、账本帧同一只脱敏(密钥形换占位,其余字节不变) |
1520
1529
  | `GET /v1/approvals?owner=user:42` | 高危写审批(durable,需 `DURABLE_APPROVAL=true`):**operator** 看待办队列(可按 owner 过滤);非 operator 只看自己的 |
1521
1530
  | `POST /v1/approvals/<sessionId>/decide` `{"decision":"approve"|"deny","reason":"…"}` | 批/否并恢复挂起任务(CAS,重复决议 409);**仅 operator**(非 operator → 403)。5.0.0 起旧轮询腿 `POST /v1/approvals/<id>` 已退役 |
1522
1531
  | `GET /v1/agents/roster` | 后台 agent **名册**(7.29 起,需 durable background-agent store,否则 501 `capability.background_agent_store_required`):调用方自己 scope 的 `a…` 行,新→旧。行只带 `handle`/`name`/`agentType`/`status`/`spawnedAt`/`updatedAt`/`settledAt`(content-free;子代产物正文走 `GET /v1/runs/<id>/subagents/<handle>/output`)。查询:`?limit=`(1..100,默认 20)、`?before=`(上一页的 `nextBefore` 游标)、`?status=`(running\|parked\|completed\|failed\|killed)、`?session=`(按会话树窄化)、`?scope=`(**仅** service-token/单用户形;带 principal 的调用方锁定自己)。回体 `{agents,total,nextBefore?,truncated?}`;`truncated:true` = 租户行数超过名册窗(500),`total` 此时是窗内数。非属主的行**不出现也不计数**(无存在性 oracle) |
@@ -1866,7 +1875,11 @@ tag 词表与每条的最坏后果见 `docs/FAIL-OPEN-CENSUS.md`。引擎装配
1866
1875
  (core 给宿主定级的判别词;词表外原样透传并标 `classificationKnown:false`),指标 `runner_config_advisory_total`
1867
1876
  按 `code` / `classification` / `level` 打标;其中 `shell-gate-off` 在本 session 显式声明 `permissionMode:
1868
1877
  "bypassPermissions"` 时是调用方要的姿态,降为 **info** 并标 `expected`——只有没表态落到引擎默认 off 的无壳直连
1869
- 形才 warn。
1878
+ 形才 warn。子代 `agents[].denyTools` / `allowTools` 的**名字**也归引擎开跑前一只判、同落这一行:无人挂的名只告不拒;退役拼法 / 大小写孪生(`bash` / `BASH`)
1879
+ **开跑前拒**(`config.agent.unknown_tool`;委派工具声明了逐次注入工具的名册,引擎列得出子代工具池),名字按工具名原拼写 `Bash`。这两张表管子代手里的**全部**
1880
+ 工具 —— 委派池与内建 hands(`Read` / `Edit` / `Write` / `NotebookEdit` / `Bash` / `Grep` / `Glob` …):`denyTools:["Write"]` ⇒ 子代没有 `Write`;`allowTools` 只列
1881
+ 调用方工具 ⇒ 子代一只内建 hands 工具都没有(要什么就点名什么)。内建只读子代(Explore / Plan)没有 `Edit` / `Write` / `NotebookEdit`。hands 之外的引擎工具(`Monitor`
1882
+ 等)仍不受这两张表管;要按路径 / 命令拦而保留工具,写调用方权限策略的 deny 规则。
1870
1883
 
1871
1884
  🔴 **`shell-gate-off` 这条注的语义自 7.73.0(core 7.15.0)起收窄,文案由引擎改写**:`off` 档**不再**意味着
1872
1885
  「这条会话的 shell 完全没有门」。**读边界**(内建读拒表 + 工作区容纳面)现在**每一档都判**,`shellGate` 只管
@@ -1986,7 +1999,7 @@ GET /v1/capabilities?permissionMode=default
1986
1999
  ```
1987
2000
 
1988
2001
  - `armed`:按上式此刻会不会武装(`?permissionMode=` 折进本次意图;查询串缺席 = 按 auto 意图答「若请求会不会武装」;
1989
- 五词之外 400 `request.field_invalid`)。
2002
+ core 闭集之外 400 `request.field_invalid`)。
1990
2003
  - `reason`(未武装时,闭集**六词**,判序即武装式合取序):`mode_not_auto`(意图不是 auto)/ `deployment_incapable`
1991
2004
  (装配面没挂分类器席位——生产装配恒挂,桩与一次性 CLI 不挂)/ `org_denied`(`caps.autoMode === false`:center 显式 deny、
1992
2005
  center 硬失败 fail-closed 拒项)/ `local_denied`(本地旋钮 `PERMISSIONS_DISABLE_AUTO_MODE=true`,改本机 env)/ `settings_denied`
@@ -2258,10 +2271,10 @@ server 进程(HTTP 服务与 `run-local`)**所有走 undici 的出网跳**都认
2258
2271
  `tag`(本地 tag 在、tag 树的版本对)→ `precheck`(registry 上本版在不在;读不出 ⇒ 红,不盲发)→ `token` → `clone`(干净 tag 树 =
2259
2272
  `git clone --depth 1 --branch v<v> file://<仓>`)→ `npm-ci` → `prepublish`(`npm run prepublishOnly`)→ `pack` → `publish`(**在 tag 树目录里**
2260
2273
  `npm publish --ignore-scripts`)→ `readback-wait`(轮询 registry 至多 20 min)→ `readback-shasum`(dist.shasum = 本地 tarball 的 sha1)→
2261
- `readback-bytes`(curl 回读 tarball,与本地 `cmp` 逐字节同)→ `githead`(registry 的 gitHead = tag commit)。脚本钉 `umask 022`(文件权限进 tar 头,
2274
+ `readback-bytes`(curl 回读 tarball,与本地 `cmp` 逐字节同;**传输失败**在 `readback-wait` 用的同一只窗里重试、窗满才红,**字节差**不重试、立即红,两种红句分开说)→ `githead`(registry 的 gitHead = tag commit)。脚本钉 `umask 022`(文件权限进 tar 头,
2262
2275
  换 umask 重跑会 pack 出另一份字节);每次 registry 请求有硬上限(60 s),回读等待另有 20 min 墙钟截止,registry 挂住不会挂死链。成功尾行
2263
2276
  `PUBLISH v=<v> shasum=<sha1> gitHead=<sha> bytes=<n> readback=identical`(发车帖 / CHANGELOG 回填 shasum 与 gitHead 从这里取);任一步红 ⇒
2264
- `PUBLISH-FAIL v=<v> step=<步骤>` + 非零退出 + 一句点名步骤的红因。
2277
+ `PUBLISH-FAIL v=<v> step=<步骤>` + 非零退出 + 一句点名步骤的红因(红因与步骤行同走 stdout,链日志里步骤行之后紧跟)。
2265
2278
  - **幂等**:registry 上已有本版 ⇒ 不重发,只做回读比对 —— 与本地 pack 逐字节同且 gitHead 对 ⇒ 绿并说「已在」;不同 ⇒ 红(别人发的字节,版本不可覆盖)。
2266
2279
  门 3.5 红了(tag 已推):修好后单独重跑 `bash scripts/publish-local.sh <v>`,再前台跑 `bash scripts/post-publish-gates.sh <v>`。
2267
2280
  **手跑的前置**:两只脚本开头都封口,封口要 import `@sema-agent/core`(封口的判据 = core `scrubSecretEnv`)⇒ 在本树装着依赖的前提下跑
@@ -133,6 +133,8 @@ data-center:2c/2G/20G)以下 BLOCK——确实无法稳定部署,提前警告不
133
133
  - **registry**:`SEMA_REGISTRY_IMAGE`(GHCR/Hub 的 `sema-registry` 镜像,迁移中,公开仓匿名可拉;
134
134
  SWR 已弃用;仅换 private 仓时才需 k8s 路径凭据=env `IMAGE_PULL_USER`/`IMAGE_PULL_TOKEN`)。
135
135
  - 公共镜像(postgres/minio/gitea)直接拉;国内网络不通时配 dockerd `registry-mirrors`。
136
+ - **MinIO**:钉死的 tag(`minio/minio:RELEASE.…`,chart / cluster 静态清单 / compose 三份同值;k8s `imagePullPolicy: IfNotPresent`,不再 `:latest` + `Always`)。换源 = 换整条引用:
137
+ compose 与 kube-up 读环境变量 `MINIO_IMAGE`(kube-up 给了才递给 chart),chart 直接 `--set minio.image=<源>/minio/minio:<tag>`(预灌镜像的节点 `--set minio.pullPolicy=Never`)。
136
138
 
137
139
  ## 凭据纪律
138
140
 
@@ -37,7 +37,8 @@ spec:
37
37
  spec:
38
38
  containers:
39
39
  - name: minio
40
- image: {{ .Values.minio.image }}
40
+ image: {{ .Values.minio.image | quote }}
41
+ imagePullPolicy: {{ .Values.minio.pullPolicy }}
41
42
  args: ["server", "/data", "--console-address", ":9001"]
42
43
  env:
43
44
  - name: MINIO_ROOT_USER
@@ -32,6 +32,7 @@ spec:
32
32
  - { name: PORT, value: "8090" }
33
33
  - { name: DB_BACKEND, value: pg }
34
34
  - { name: SESSION_BACKEND, value: auto }
35
+ - { name: STORE_LOCK_WAIT_MS, value: {{ .Values.server.storeWaitMs | quote }} }
35
36
  - { name: PG_HOST, value: sema-pg-rw.{{ .Release.Namespace }}.svc }
36
37
  - { name: PG_PORT, value: "5432" }
37
38
  - { name: PG_USER, value: sema }
@@ -43,6 +43,7 @@ spec:
43
43
  - { name: PORT, value: "8090" }
44
44
  - { name: DB_BACKEND, value: pg }
45
45
  - { name: SESSION_BACKEND, value: auto }
46
+ - { name: STORE_LOCK_WAIT_MS, value: {{ .Values.server.storeWaitMs | quote }} }
46
47
  - { name: PG_HOST, value: sema-pg-rw.{{ .Release.Namespace }}.svc }
47
48
  - { name: PG_PORT, value: "5432" }
48
49
  - { name: PG_USER, value: sema }
@@ -26,7 +26,10 @@ pg:
26
26
  bucket: s3://sema-pg-backup/
27
27
 
28
28
  minio:
29
- image: minio/minio:latest
29
+ # 钉死的 tag(不用 `:latest`:k8s 对它的缺省拉取策略是 Always,每次 Pod 重建都拉「今天恰好是什么」)。换国内源 = 换整条引用
30
+ # (`--set minio.image=<源>/minio/minio:<tag>`,同 server.image / gitea.image 的形);cluster 静态清单与 compose 默认写同一个 tag(测试咬合)。
31
+ image: minio/minio:RELEASE.2025-09-07T16-13-09Z
32
+ pullPolicy: IfNotPresent # tar 预灌=Never(同 server.pullPolicy)
30
33
  storageSize: 200Gi
31
34
  storageClass: longhorn
32
35
  rootUser: sema
@@ -37,6 +40,9 @@ server:
37
40
  pullPolicy: IfNotPresent # tar 预灌=Never
38
41
  replicas: 3
39
42
  nodePort: 30890
43
+ # 引擎 boot 期等库的窗(ms,env STORE_LOCK_WAIT_MS):CNPG 与 server Pod 同时起、库初始化常慢于 server 就绪 ——
44
+ # 窗内重试、到窗仍连不上才拒启(拒启后 kubelet 重拉;引擎不会再退成内存起服)。0 = 第一次失败就拒。
45
+ storeWaitMs: 300000
40
46
  serviceAuthToken: "" # REQUIRED
41
47
  modelGatewayBaseurl: "" # REQUIRED
42
48
  modelId: "" # REQUIRED
@@ -31,6 +31,10 @@ unset _self _hops _dir
31
31
  . "$HERE/lib/shell-access.sh"
32
32
  # 取值旗的形(S-621):判据与词表同 sema-up.sh 一处定义。
33
33
  . "$HERE/lib/flagshape.sh"
34
+ # 引擎 /health 体里「落在哪个后端」的读法与期望值(smoke.sh / kube-up.sh 同一只)。
35
+ . "$HERE/lib/health.sh"
36
+ # 对象存储桶的唯一表(sema-up.sh / kube-up.sh / smoke.sh 同读这一张)。
37
+ . "$HERE/lib/buckets.sh"
34
38
  log() { printf '[cluster-up] %s\n' "$*" >&2; }
35
39
  die() { log "⛔ $*"; exit 1; }
36
40
 
@@ -419,8 +423,9 @@ PY
419
423
  # 凭据清单经 stdin 直接 apply,不落远端盘(落 /tmp 失败时会残留)
420
424
  rrun 0 k3s kubectl apply -f - < "$RENDERED" >/dev/null
421
425
  fi
422
- rsudo 0 <<'EOF'
426
+ rsudo 0 $(buckets_prefixed local/ $(buckets_k8s)) <<'EOF'
423
427
  set -e
428
+ BUCKETS=("$@") # 要建的桶(local/<桶>):清单只在 lib/buckets.sh 一处,经位置参数递给远端
424
429
  # runner token 跨 ns 复制要赶在 server 起 pod 前(晚了=CreateContainerConfigError 干等——真踩);
425
430
  # SA token secret 由控制器异步填充,先等它有值
426
431
  TOK=""
@@ -431,6 +436,12 @@ done
431
436
  [ -n "$TOK" ] || { echo "sema-runner-token 未被控制器填充" >&2; exit 1; }
432
437
  k3s kubectl create secret generic sema-runner-token-copy -n sema --from-literal=token="$(printf '%s' "$TOK" | base64 -d)" \
433
438
  --dry-run=client -o yaml | k3s kubectl apply -f - >/dev/null
439
+ # MinIO 建桶(备份桶+引擎桶)—— 排在等 PG healthy 与等 server rollout **之前**:引擎起服会核快照桶在不在,桶缺时 server 不就绪、rollout 永远等不到,而建桶又排在它后面 ⇒ 互等;
440
+ # MinIO 不依赖 PG,建桶也就不必陪着等 PG(冷启拉镜像 + init 可要几分钟,会把引擎的等桶窗耗掉)
441
+ k3s kubectl rollout status deployment/sema-minio -n sema --timeout=300s >/dev/null
442
+ PW=$(k3s kubectl get secret sema-minio -n sema -o jsonpath='{.data.rootPassword}' | base64 -d)
443
+ k3s kubectl exec deploy/sema-minio -n sema -- sh -c \
444
+ "mc alias set local http://localhost:9000 sema '$PW' >/dev/null && mc mb --ignore-existing ${BUCKETS[*]}" >/dev/null
434
445
  # PG healthy(冷启拉镜像+init 可要几分钟)
435
446
  for t in $(seq 1 120); do
436
447
  s=$(k3s kubectl get cluster sema-pg -n sema -o jsonpath='{.status.phase}' 2>/dev/null || true)
@@ -438,24 +449,25 @@ for t in $(seq 1 120); do
438
449
  done
439
450
  [ "$s" = "Cluster in healthy state" ] || { k3s kubectl get cluster -n sema; echo "PG 未达 healthy" >&2; exit 1; }
440
451
  k3s kubectl rollout status deployment/sema-server -n sema --timeout=300s >/dev/null
441
- # MinIO 建桶(备份桶+对象桶)
442
- PW=$(k3s kubectl get secret sema-minio -n sema -o jsonpath='{.data.rootPassword}' | base64 -d)
443
- k3s kubectl exec deploy/sema-minio -n sema -- sh -c \
444
- "mc alias set local http://localhost:9000 sema '$PW' >/dev/null && mc mb --ignore-existing local/sema-pg-backup local/workspaces local/blobs" >/dev/null
445
452
  echo STACK_OK
446
453
  EOF
447
454
 
448
455
  # ── 8. smoke(节点内经 NodePort;外网入口=LB 在云商侧配,打印指引)────────────────
449
456
  log "8/8 smoke"
450
457
  # NodePort 被 nodeport-addresses 钉在节点网段——loopback 不在其内,必须打 node-ip(真踩 curl exit 7)
451
- SMOKE=$(rsudo 0 <<EOF
452
- H=\$(curl -m5 -s -o /dev/null -w '%{http_code}' "http://$K3S0_IP:$NODEPORT/health" || echo 000)
458
+ # 第一行 = 两个 HTTP 码,其后 = /health 体(后端位在里面:200 + 401 测不出「服务在内存里跑」,验收必须读那一位)
459
+ SMOKE_OUT=$(rsudo 0 <<EOF
460
+ HB=\$(curl -m5 -s -w '\n%{http_code}' "http://$K3S0_IP:$NODEPORT/health" || printf '\n000')
461
+ H="\${HB##*\$'\n'}"
453
462
  A=\$(curl -m5 -s -o /dev/null -w '%{http_code}' "http://$K3S0_IP:$NODEPORT/v1/runs" || echo 000)
454
463
  echo "health=\$H auth=\$A"
464
+ printf '%s' "\${HB%\$'\n'*}"
455
465
  EOF
456
466
  )
467
+ SMOKE="${SMOKE_OUT%%$'\n'*}"
457
468
  case "$SMOKE" in *"health=200 auth=401"*) : ;; *) die "smoke 失败:$SMOKE(期望 health=200 auth=401)" ;; esac
458
- log "smoke ✓ $SMOKE"
469
+ BK_WHY=$(health_backend_check "${SMOKE_OUT#*$'\n'}") || die "smoke 失败:$BK_WHY。下一步:kubectl -n sema logs deploy/sema-server(引擎在库没连上时不会再退内存起服 —— 它会拒启;读到 memory 多半是旧镜像)"
470
+ log "smoke ✓ $SMOKE backend=$SEMA_UP_EXPECT_SESSION_BACKEND"
459
471
  cat >&2 <<EOF
460
472
 
461
473
  ══════════ cluster-up 完成($MACHINE_COUNT 节点)══════════
@@ -9,7 +9,7 @@
9
9
  # 输入(sema-up.sh 注入;也可单独跑):
10
10
  # WORK(状态目录)PROJECT REGION KUBE_MODE [KUBECONFIG_PATH] [DRY_RUN]
11
11
  # USERS REGISTRY_ENABLED GIT_MODE [GIT_URL GIT_TOKEN]
12
- # SEMA_SERVER_IMAGE(必需)[SEMA_REGISTRY_IMAGE][GITEA_IMAGE]
12
+ # SEMA_SERVER_IMAGE(必需)[SEMA_REGISTRY_IMAGE][GITEA_IMAGE][MINIO_IMAGE(整条引用,换国内源用;缺省 = chart 钉死的 tag)]
13
13
  # MODEL_GATEWAY_BASEURL MODEL_ID MODEL_API_KEY
14
14
  # [NAMESPACE=sema][STORAGE_CLASS=集群默认][CNPG_VERSION]
15
15
  set -euo pipefail
@@ -33,6 +33,10 @@ unset _self _hops _dir
33
33
  . "$HERE/lib/shell-access.sh"
34
34
  # 取值旗的形(S-621):判据与词表同 sema-up.sh 一处定义。
35
35
  . "$HERE/lib/flagshape.sh"
36
+ # 引擎 /health 体里「落在哪个后端」的读法与期望值(smoke.sh / cluster-up.sh 同一只)。
37
+ . "$HERE/lib/health.sh"
38
+ # 对象存储桶的唯一表(sema-up.sh / cluster-up.sh / smoke.sh 同读这一张)。
39
+ . "$HERE/lib/buckets.sh"
36
40
  log() { printf '[kube-up] %s\n' "$*" >&2; }
37
41
  die() { log "⛔ $*"; exit 1; }
38
42
 
@@ -214,6 +218,7 @@ helm_values() { # helm_values <giteaBaseUrl> <oauthId> <oauthSecret>
214
218
  V_REG_PW="${REGISTRY_ADMIN_PASSWORD:-}" V_REG_SS="${REGISTRY_SESSION_SECRET:-}" V_REG_PT="${REGISTRY_PULL_TOKEN:-}" \
215
219
  V_GITEA_ENABLED="$([ "$GIT_MODE" = provision ] && echo true || echo false)" \
216
220
  V_GITEA_IMAGE="${GITEA_IMAGE:-gitea/gitea:1.24}" V_NODE_IP="$NODE_IP" \
221
+ V_MINIO_IMAGE="${MINIO_IMAGE:-}" \
217
222
  python3 - <<'PY'
218
223
  import json, os
219
224
  e = os.environ
@@ -244,6 +249,8 @@ v = {
244
249
  "gitea": {"enabled": e["V_GITEA_ENABLED"] == "true", "image": e["V_GITEA_IMAGE"],
245
250
  "storageClass": e["V_SC"], "domain": e["V_NODE_IP"]},
246
251
  }
252
+ # MinIO 镜像:给了才递(默认只在 chart values.yaml 写一处,不在脚本里再抄一份);国内源 = 整条引用
253
+ if e["V_MINIO_IMAGE"]: v["minio"]["image"] = e["V_MINIO_IMAGE"]
247
254
  print(json.dumps(v))
248
255
  PY
249
256
  }
@@ -317,6 +324,12 @@ done
317
324
  [ -n "$TOK" ] || die "sema-runner-token 未被控制器填充。下一步:kubectl -n sema-sandbox get secret,sa"
318
325
  printf '%s' "$TOK" | base64 -d | $KUBECTL create secret generic sema-runner-token-copy -n "$NAMESPACE" \
319
326
  --from-file=token=/dev/stdin --dry-run=client -o yaml | $KUBECTL apply -f - >/dev/null
327
+ $KUBECTL rollout status deployment/sema-minio -n "$NAMESPACE" --timeout=300s >/dev/null
328
+ # MinIO 建桶(幂等;备份桶+引擎桶,清单只在 lib/buckets.sh 一处)。引擎起服会核快照桶在不在,所以建桶排在等 PG healthy 与等 server rollout 之前
329
+ # (MinIO 不依赖 PG;PG 冷启可要几分钟,不该把引擎的等桶窗耗掉)
330
+ $KUBECTL exec deploy/sema-minio -n "$NAMESPACE" -- sh -c \
331
+ "mc alias set local http://localhost:9000 sema \"\$MINIO_ROOT_PASSWORD\" >/dev/null && mc mb --ignore-existing $(buckets_prefixed local/ $(buckets_k8s))" >/dev/null \
332
+ || die "MinIO 建桶失败。下一步:kubectl -n $NAMESPACE logs deploy/sema-minio"
320
333
  # PG healthy(冷启拉镜像+init 可要几分钟)
321
334
  s=""
322
335
  for t in $(seq 1 120); do
@@ -324,11 +337,6 @@ for t in $(seq 1 120); do
324
337
  [ "$s" = "Cluster in healthy state" ] && break; sleep 5
325
338
  done
326
339
  [ "$s" = "Cluster in healthy state" ] || { $KUBECTL get cluster -n "$NAMESPACE" >&2 || true; die "PG 未达 healthy。下一步:kubectl -n $NAMESPACE describe cluster sema-pg;PVC Pending 时查 StorageClass"; }
327
- $KUBECTL rollout status deployment/sema-minio -n "$NAMESPACE" --timeout=300s >/dev/null
328
- # MinIO 建桶(幂等;备份桶+对象桶)
329
- $KUBECTL exec deploy/sema-minio -n "$NAMESPACE" -- sh -c \
330
- "mc alias set local http://localhost:9000 sema \"\$MINIO_ROOT_PASSWORD\" >/dev/null && mc mb --ignore-existing local/sema-pg-backup local/workspaces local/blobs local/backups" >/dev/null \
331
- || die "MinIO 建桶失败。下一步:kubectl -n $NAMESPACE logs deploy/sema-minio"
332
340
  $KUBECTL rollout status deployment/sema-server -n "$NAMESPACE" --timeout=600s >/dev/null || die "sema-server 未就绪。下一步:kubectl -n $NAMESPACE logs deploy/sema-server(常见:镜像拉取失败→imagePullSecrets/私仓凭据)"
333
341
 
334
342
  # ── Gitea 引导(provision:管理员/token/OAuth app;全部 in-pod 动作,不依赖 NodePort 可达)──
@@ -379,30 +387,35 @@ fi
379
387
 
380
388
  # ── smoke(port-forward 直验——kind/远程 kubeconfig 下 NodePort 对本机不可达,别靠它)──
381
389
  log "5/6 smoke"
382
- smoke_pf() { # smoke_pf <svc> <port> <path> → http code
383
- local pf_pid code
390
+ smoke_pf() { # smoke_pf <svc> <port> <path> → 第一行 http code,其后是响应体(调用方按需取)
391
+ local pf_pid out code body
384
392
  $KUBECTL port-forward "svc/$1" -n "$NAMESPACE" "1$2:$2" >/dev/null 2>&1 &
385
393
  pf_pid=$!
386
394
  sleep 2
387
- code=$(curl -m5 -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:1$2$3" || echo 000)
395
+ out=$(curl -m5 -s -w '\n%{http_code}' "http://127.0.0.1:1$2$3" || printf '\n000')
388
396
  kill $pf_pid 2>/dev/null; wait $pf_pid 2>/dev/null || true
389
- printf '%s' "$code"
397
+ code="${out##*$'\n'}"; body="${out%$'\n'*}"
398
+ [ "$out" = "$code" ] && body="" # 没有换行 = 只有码(假 curl / 空体)
399
+ printf '%s\n%s' "$code" "$body"
390
400
  }
391
- H=$(smoke_pf sema-server 8090 /health)
401
+ HR=$(smoke_pf sema-server 8090 /health)
402
+ H="${HR%%$'\n'*}"
392
403
  [ "$H" = "200" ] || die "smoke 失败:server /health=$H(期望 200)。下一步:kubectl -n $NAMESPACE logs deploy/sema-server"
393
- A=$(smoke_pf sema-server 8090 /v1/runs)
404
+ # /health 200 ≠ 数据落了库:后端位必须是期望的持久后端(durable(pg));读到 memory = 服务在内存里跑、重启即丢,不许判绿
405
+ BK_WHY=$(health_backend_check "${HR#*$'\n'}") || die "smoke 失败:$BK_WHY。下一步:kubectl -n $NAMESPACE logs deploy/sema-server(引擎在库没连上时不会再退内存起服 —— 它会拒启;读到 memory 多半是旧镜像)"
406
+ A=$(smoke_pf sema-server 8090 /v1/runs); A="${A%%$'\n'*}"
394
407
  [ "$A" = "401" ] || die "smoke 失败:无 token /v1/runs=$A(期望 401=auth 门在)。SERVICE_AUTH_TOKEN 未生效=裸奔,立刻查"
395
408
  R="n/a"
396
409
  if [ "$REGISTRY_ENABLED" = "true" ]; then
397
- R=$(smoke_pf sema-registry 3000 /api/healthz)
410
+ R=$(smoke_pf sema-registry 3000 /api/healthz); R="${R%%$'\n'*}"
398
411
  [ "$R" = "200" ] || die "smoke 失败:registry healthz=$R。下一步:kubectl -n $NAMESPACE logs deploy/sema-registry"
399
412
  fi
400
413
  G="n/a"
401
414
  if [ "$GIT_MODE" = "provision" ]; then
402
- G=$(smoke_pf sema-gitea 3000 /api/healthz)
415
+ G=$(smoke_pf sema-gitea 3000 /api/healthz); G="${G%%$'\n'*}"
403
416
  [ "$G" = "200" ] || die "smoke 失败:gitea healthz=$G"
404
417
  fi
405
- log "smoke ✓ health=$H auth=$A registry=$R gitea=$G"
418
+ log "smoke ✓ health=$H backend=$(health_session_backend "${HR#*$'\n'}") auth=$A registry=$R gitea=$G"
406
419
 
407
420
  # ── summary(一屏;凭据一次性显示)────────────────────────────────────────────────
408
421
  log "6/6 完成"
@@ -414,7 +427,7 @@ log "6/6 完成"
414
427
  echo " 本机不可达 NodePort 时(kind/远程):kubectl -n $NAMESPACE port-forward svc/sema-server 8090:8090"
415
428
  # 壳对 NodePort 的明文 http 按设计拒连(非回环要 https)⇒ 经 port-forward 带到本机回环再 connect。
416
429
  shell_access_text "$ENVF" "装了 kubectl、能连这个集群的机器上:" "kubectl -n $NAMESPACE port-forward svc/sema-server 8090:8090" "http://127.0.0.1:8090"
417
- echo " 数据 CNPG sema-pg×$PG_INSTANCES($PG_AA)+ MinIO(桶:workspaces/blobs/backups/sema-pg-backup)"
430
+ echo " 数据 CNPG sema-pg×$PG_INSTANCES($PG_AA)+ MinIO(桶:$(buckets_k8s | tr ' ' '/'))"
418
431
  if [ "$REGISTRY_ENABLED" = "true" ]; then
419
432
  echo " 配置中心 NodePort 30300 → http://$NODE_IP:30300"
420
433
  if $NEW_REG_PW; then echo " 首登:${REGISTRY_ADMIN_USER:-zhaoheran} / ${REGISTRY_ADMIN_PASSWORD}(⚠ 只显示这一次,之后看 .env)"
@@ -0,0 +1,21 @@
1
+ # lib/buckets.sh — 部署侧「要预置哪些对象存储桶」的唯一表。sema-up.sh(compose 建桶 / 外接 S3 建桶 / 汇总)、kube-up.sh、cluster-up.sh(k8s 建桶)与
2
+ # smoke.sh(验桶)都读这一张,不各写一份串:各写一份的下场是清单漏一只桶,而漏桶的症状是起服后每一次对象写 / 每一拍 sweep 都 404。
3
+ #
4
+ # server **不建桶**(docs/DEPLOY-PREREQS.md「对象存储桶」),引擎起服只核一次快照桶在不在、不在就拒启 —— 所以这张表必须包含引擎要用的每一只:
5
+ # · `session-snapshots` = 引擎的 `SESSION_SNAPSHOT_BUCKET` 缺省名(snapshot-BLOB offload + SendUserFile 私有 presigned 轨 / 沙箱 staging);
6
+ # 由测试把这个名与引擎的缺省逐字咬合(引擎改缺省而这里没跟 ⇒ 红);
7
+ # · `sema-public`(SendUserFile 永久公链轨)**不在表里**:它要匿名 GetObject-only 策略,是运维另配的桶,脚本不替人开匿名读。
8
+ SEMA_UP_ENGINE_BUCKETS="workspaces blobs backups session-snapshots"
9
+ # CNPG 的 WAL 归档 / 每日基备桶(只有 k8s 两条路径有 PG 备份;名与 chart values 的 `pg.backup.bucket: s3://<名>/` 同名,由测试咬合)。
10
+ SEMA_UP_PG_BACKUP_BUCKET="sema-pg-backup"
11
+
12
+ # buckets_k8s → 打印 k8s 路径(kube-up / cluster-up)要建的全部桶,空格分隔,每只一次。
13
+ buckets_k8s() { printf '%s %s' "$SEMA_UP_PG_BACKUP_BUCKET" "$SEMA_UP_ENGINE_BUCKETS"; }
14
+
15
+ # buckets_prefixed <前缀> <桶…> → 每个桶名前加前缀(mc 的 `<别名>/<桶>` 写法),空格分隔。桶名是 S3 闭字符集,不含空白,按空白拆是安全的。
16
+ buckets_prefixed() {
17
+ local p="$1" b out=""
18
+ shift
19
+ for b in "$@"; do out="$out$p$b "; done
20
+ printf '%s' "${out% }"
21
+ }