@sema-agent/server 7.5.0 → 7.7.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 (104) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +18 -3
  3. package/README.zh-CN.md +14 -3
  4. package/USAGE.md +100 -2
  5. package/dist/approval-reconciler.d.ts +1 -1
  6. package/dist/approval-reconciler.js +6 -5
  7. package/dist/boot/budget-tracing.js +1 -1
  8. package/dist/boot/config-center.js +17 -3
  9. package/dist/boot/execution-env.js +1 -1
  10. package/dist/boot/parked-revive-gate.d.ts +78 -0
  11. package/dist/boot/parked-revive-gate.js +114 -0
  12. package/dist/boot/resolve-spec.d.ts +8 -20
  13. package/dist/boot/resolve-spec.js +40 -125
  14. package/dist/budget.d.ts +1 -1
  15. package/dist/budget.js +1 -1
  16. package/dist/capabilities/hands-lane.d.ts +91 -0
  17. package/dist/capabilities/hands-lane.js +104 -0
  18. package/dist/capabilities/repo-tools.d.ts +1 -1
  19. package/dist/capabilities/repo-tools.js +8 -2
  20. package/dist/capabilities/scenarios.d.ts +49 -2
  21. package/dist/capabilities/scenarios.js +84 -16
  22. package/dist/config-provider.d.ts +1 -0
  23. package/dist/config-provider.js +23 -3
  24. package/dist/config-types.d.ts +5 -3
  25. package/dist/config.js +5 -4
  26. package/dist/deployment-governance.d.ts +168 -0
  27. package/dist/deployment-governance.js +206 -0
  28. package/dist/fleet/fleet-bus.d.ts +11 -1
  29. package/dist/fleet/fleet-bus.js +43 -0
  30. package/dist/fleet/fleet-terminal-window.d.ts +98 -0
  31. package/dist/fleet/fleet-terminal-window.js +319 -0
  32. package/dist/hooks/hook-runner.js +11 -0
  33. package/dist/http/route-ctx.d.ts +7 -1
  34. package/dist/http/routes/approvals-assistant.js +6 -7
  35. package/dist/http/routes/fleet.js +164 -14
  36. package/dist/http/routes/runs.js +7 -2
  37. package/dist/http/routes/tasks.js +5 -4
  38. package/dist/http/routes/trace-usage.js +3 -4
  39. package/dist/http/send.d.ts +23 -0
  40. package/dist/http/send.js +23 -0
  41. package/dist/http/server.d.ts +10 -0
  42. package/dist/http/server.js +28 -12
  43. package/dist/http/sse-log.js +3 -4
  44. package/dist/leader/diffout.d.ts +10 -0
  45. package/dist/leader/diffout.js +14 -2
  46. package/dist/leader/diffup.js +3 -2
  47. package/dist/leader/planner.js +7 -0
  48. package/dist/main.js +79 -32
  49. package/dist/observability/fail-open.d.ts +4 -0
  50. package/dist/observability/fail-open.js +4 -0
  51. package/dist/observability/tool-trace.d.ts +13 -0
  52. package/dist/observability/tool-trace.js +14 -0
  53. package/dist/orchestration/workflow-notify-journal.d.ts +1 -1
  54. package/dist/orchestration/workflow-notify-journal.js +14 -34
  55. package/dist/plugins/approval-ask-store-sql.d.ts +33 -0
  56. package/dist/plugins/approval-ask-store-sql.js +66 -33
  57. package/dist/plugins/background-agent-store-sql.js +16 -16
  58. package/dist/plugins/breaker-state-sql.js +2 -2
  59. package/dist/plugins/checkpoint-store-sql.d.ts +5 -2
  60. package/dist/plugins/checkpoint-store-sql.js +5 -2
  61. package/dist/plugins/image-bake-store-sql.d.ts +1 -1
  62. package/dist/plugins/image-bake-store-sql.js +27 -27
  63. package/dist/plugins/image-index-sql.js +15 -15
  64. package/dist/plugins/mailbox-store-sql.js +3 -3
  65. package/dist/plugins/memory-engine-pg.js +9 -9
  66. package/dist/plugins/memory-engine-tidb.js +7 -7
  67. package/dist/plugins/memory-sync-store-pg.js +13 -13
  68. package/dist/plugins/memory-sync-store-tidb.js +5 -5
  69. package/dist/plugins/outcome-ledger-sql.js +7 -7
  70. package/dist/plugins/pg-cost-quota.js +3 -3
  71. package/dist/plugins/pg-pool.js +84 -75
  72. package/dist/plugins/pg-rate-limiter.js +3 -3
  73. package/dist/plugins/pg-session-storage.d.ts +1 -1
  74. package/dist/plugins/pg-session-storage.js +12 -13
  75. package/dist/plugins/remote-env-host.js +3 -1
  76. package/dist/plugins/remote-env-local-docker.js +6 -3
  77. package/dist/plugins/remote-env-ssh.d.ts +13 -1
  78. package/dist/plugins/roster-store-sql.js +8 -8
  79. package/dist/plugins/store-contracts.d.ts +19 -0
  80. package/dist/plugins/store-contracts.js +42 -0
  81. package/dist/plugins/task-attachment-store.js +5 -5
  82. package/dist/plugins/task-list-store-sql.js +1 -1
  83. package/dist/plugins/tidb-cost-quota.js +1 -1
  84. package/dist/plugins/tidb-pool.js +83 -60
  85. package/dist/plugins/tidb-rate-limiter.js +1 -1
  86. package/dist/plugins/tidb-session-store.js +2 -5
  87. package/dist/plugins/tool-result-store-sql.js +2 -2
  88. package/dist/plugins/usage-window-store-sql.js +13 -13
  89. package/dist/plugins/write-behind-counter.d.ts +10 -2
  90. package/dist/plugins/write-behind-counter.js +13 -3
  91. package/dist/resource-suspend.d.ts +3 -1
  92. package/dist/resource-suspend.js +3 -1
  93. package/dist/run-local.d.ts +73 -1
  94. package/dist/run-local.js +160 -9
  95. package/dist/runs.d.ts +3 -1
  96. package/dist/runs.js +3 -1
  97. package/dist/security.d.ts +12 -0
  98. package/dist/security.js +12 -0
  99. package/dist/session-sync-kernel.d.ts +13 -0
  100. package/dist/session-sync-kernel.js +13 -0
  101. package/dist/trace/core-keyset-guard.d.ts +1 -1
  102. package/dist/trace/project.d.ts +10 -1
  103. package/dist/trace/project.js +31 -0
  104. package/package.json +3 -3
package/LICENSE CHANGED
@@ -3,7 +3,7 @@ Business Source License 1.1
3
3
  Parameters
4
4
 
5
5
  Licensor: clay (github.com/clayboby)
6
- Licensed Work: @sema-ai/server (sema-server)
6
+ Licensed Work: @sema-agent/server (sema-server)
7
7
  The Licensed Work is (c) 2026 clay.
8
8
  Additional Use Grant: You may make production use of the Licensed Work for
9
9
  personal, educational, research, or other
package/README.md CHANGED
@@ -95,13 +95,13 @@ Requirements: Node ≥ 20 (npm path) and an OpenAI-compatible model gateway.
95
95
  ```bash
96
96
  # A) npm
97
97
  npm install @sema-agent/server
98
- MODEL_GATEWAY_BASEURL=https://api.deepseek.com MODEL_ID=deepseek-chat \
98
+ MODEL_GATEWAY_BASEURL=https://api.deepseek.com MODEL_ID=deepseek-v4-pro \
99
99
  MODEL_API_KEY=<your-key> SERVICE_AUTH_TOKEN=<pick-one> \
100
100
  node node_modules/@sema-agent/server/dist/main.js # → :8090
101
101
 
102
102
  # B) container (zero local deps, anonymous pull)
103
103
  docker run -p 8090:8090 \
104
- -e MODEL_GATEWAY_BASEURL=https://api.deepseek.com -e MODEL_ID=deepseek-chat \
104
+ -e MODEL_GATEWAY_BASEURL=https://api.deepseek.com -e MODEL_ID=deepseek-v4-pro \
105
105
  -e MODEL_API_KEY=<your-key> -e SERVICE_AUTH_TOKEN=<pick-one> \
106
106
  ghcr.io/sema-agent/sema-server:latest # or docker.io/claybobby/sema-server:latest
107
107
 
@@ -116,6 +116,21 @@ curl -s localhost:8090/v1/tasks -H "Authorization: Bearer <SERVICE_AUTH_TOKEN>"
116
116
  (both public; `:latest` rolling, `:<sha>` pinned).
117
117
  - **Bundled binaries**: the package ships two `bin` entries — `run-local` (single-machine local
118
118
  runner) and `sema-up` (deployment bootstrap script).
119
+ - `run-local` boots the same engine in-process, runs one task from the CLI objective, prints the
120
+ result and exits — no HTTP server, no submission auth gate.
121
+ - **The deployment governance knobs apply here too** (since 7.5.0 — before that this leg assembled its
122
+ task with the governance chain absent, so the knobs were silently inert): `AUTONOMY`,
123
+ `runtime.commandPolicy` (from `config.d/governance.json`) and `SENSITIVE_WRITE_PATTERNS` are compiled
124
+ onto the local task exactly as they are on the HTTP leg, tighten-only, anchored at the
125
+ `--workspace` directory.
126
+ - **Off switches**: `SENSITIVE_WRITE_PATTERNS=off` **or** an empty value (`SENSITIVE_WRITE_PATTERNS=`)
127
+ disables the guard set; unset `AUTONOMY` (or set it to `auto`) for no extra tightening. A guard-set
128
+ value that cannot compile (e.g. a pattern with no path segment, `/`) refuses to start with a message
129
+ naming the knob, instead of failing once per task.
130
+ - **Approvals have no durable park on this leg** — a one-shot CLI has no `/v1/approvals/:id/decide` to
131
+ resume from. A gated `ask` (e.g. `AUTONOMY=ask`, which routes every shell command through approval)
132
+ is answered inline: a `y/N` prompt when stdin is a TTY, otherwise a fail-closed **deny** with a
133
+ stderr line naming the knob that produced the gate.
119
134
  - **Full-stack, one command** (DB + object store + registry web + sandbox pool; Docker and k8s
120
135
  paths): [`sema-agent/sema-deploy`](https://github.com/sema-agent/sema-deploy).
121
136
  - **Sandbox package sources**: default = official upstreams (pypi/npmjs/crates.io/…). For
@@ -141,7 +156,7 @@ The server is configured entirely through environment variables. The most import
141
156
  | `CONFIG_PROVIDER` | unset | Config source: `local` (file-backed `config.d/`, single machine) / `remote` (registry control plane) |
142
157
  | `DEFAULT_SCENARIO` | `code` | Default scenario when the request body names none |
143
158
  | `SANDBOX_PKG_SOURCE` | `global` | Package sources inside sandboxes: `global` (official upstreams) / `cn` (China mirrors) / `custom` / `none` |
144
- | `SENSITIVE_WRITE_PATTERNS` | core's recommended set | Sensitive-path write deny list; comma-separated value replaces the set, `off` disables |
159
+ | `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 |
145
160
  | `MANUAL_MODE_SHELL_GATE` | unset (off) | `always`\|`classify` — tighten `Bash` into the approval chain, applied unconditionally at the governance layer (≥7.1.0: independent of client permission mode, lane, or settings presence) |
146
161
  | `SCRATCHPAD_SWEEP_TTL_MS` | 7 days | Idle-reap window for per-session scratchpad dirs (by dir mtime; `0` disables). The scratchpad is **ephemeral by contract**: replica-local disk, NOT part of the durable-suspend persistence set — a resume on a different replica, or after a sweep, starts with an empty dir (same two-track posture as the Agent SDK hosting doc: conversation persists, working-directory artifacts don't). Raise/disable only on single-replica deployments that park approvals for longer than the window |
147
162
  | `MODEL_CONNECT_TIMEOUT_MS` | `30000` | Gateway connect timeout |
package/README.zh-CN.md CHANGED
@@ -89,13 +89,13 @@
89
89
  ```bash
90
90
  # A) npm
91
91
  npm install @sema-agent/server
92
- MODEL_GATEWAY_BASEURL=https://api.deepseek.com MODEL_ID=deepseek-chat \
92
+ MODEL_GATEWAY_BASEURL=https://api.deepseek.com MODEL_ID=deepseek-v4-pro \
93
93
  MODEL_API_KEY=<你的-key> SERVICE_AUTH_TOKEN=<自定> \
94
94
  node node_modules/@sema-agent/server/dist/main.js # → :8090
95
95
 
96
96
  # B) 容器(零依赖,匿名可拉)
97
97
  docker run -p 8090:8090 \
98
- -e MODEL_GATEWAY_BASEURL=https://api.deepseek.com -e MODEL_ID=deepseek-chat \
98
+ -e MODEL_GATEWAY_BASEURL=https://api.deepseek.com -e MODEL_ID=deepseek-v4-pro \
99
99
  -e MODEL_API_KEY=<你的-key> -e SERVICE_AUTH_TOKEN=<自定> \
100
100
  ghcr.io/sema-agent/sema-server:latest # 或 docker.io/claybobby/sema-server:latest
101
101
 
@@ -110,6 +110,17 @@ curl -s localhost:8090/v1/tasks -H "Authorization: Bearer <SERVICE_AUTH_TOKEN>"
110
110
  (均 public,`:latest` 滚动 / `:<sha>` 钉版)。
111
111
  - **随包二进制**:包内带两个 `bin` —— `run-local`(单机本地 runner)与
112
112
  `sema-up`(部署引导脚本)。
113
+ - `run-local` 在进程内启同一套引擎,把命令行给的目标跑完一次、打印结果、退出——没有 HTTP 服务,
114
+ 也没有提交鉴权门。
115
+ - **部署治理旋钮在这条腿上同样生效**(7.5.0 起;此前这条腿自建任务时整条治理链缺席,旋钮静默失效):
116
+ `AUTONOMY`、`runtime.commandPolicy`(来自 `config.d/governance.json`)与 `SENSITIVE_WRITE_PATTERNS`
117
+ 与 HTTP 腿同样地 tighten-only 编译进本地任务,裁决锚在 `--workspace` 目录。
118
+ - **关闸通道**:`SENSITIVE_WRITE_PATTERNS=off` **或**留空(`SENSITIVE_WRITE_PATTERNS=`)都关掉守卫集;
119
+ `AUTONOMY` 不设(或设 `auto`)= 不额外收紧。守卫集若给了**编译不出来**的值(例如 `/` 这种不含任何
120
+ 路径段的模式),启动即拒并指名旋钮,而不是每个任务炸一次。
121
+ - **这条腿没有 durable 审批 park**:一次性 CLI 没有 `/v1/approvals/:id/decide` 可赎回。被门住的 `ask`
122
+ (例如 `AUTONOMY=ask`,它把每条 shell 命令都送进审批链)当场结算:stdin 是 TTY 就 `y/N` 问人,
123
+ 否则 fail-closed **拒绝**并在 stderr 点名是哪个旋钮产的这道门。
113
124
  - **一键全栈部署**(DB + 对象存储 + registry 网站 + 沙箱池,docker/k8s 双路径):
114
125
  [`sema-agent/sema-deploy`](https://github.com/sema-agent/sema-deploy)。
115
126
  - **沙箱装包源**:缺省 = 官方源(pypi/npmjs/crates.io/…)。中国大陆部署配
@@ -134,7 +145,7 @@ curl -s localhost:8090/v1/tasks -H "Authorization: Bearer <SERVICE_AUTH_TOKEN>"
134
145
  | `CONFIG_PROVIDER` | 未设 | 配置来源:`local`(单机文件 `config.d/`)/ `remote`(registry 控制面) |
135
146
  | `DEFAULT_SCENARIO` | `code` | 请求体未指定场景时的缺省场景 |
136
147
  | `SANDBOX_PKG_SOURCE` | `global` | 沙箱内装包源:`global`(官方源)/ `cn`(国内镜像)/ `custom` / `none` |
137
- | `SENSITIVE_WRITE_PATTERNS` | core 推荐集 | 敏感路径写拒集;逗号分隔值为整体替换,`off` 关闭 |
148
+ | `SENSITIVE_WRITE_PATTERNS` | core 推荐集 | 敏感路径写拒集;逗号分隔值为整体替换,`off` **或留空**关闭。在治理层无条件施加(与客户端权限模式/lane/settings 在场性无关),`run-local` 腿同样生效;编译不出守卫集的值(如 `/`)启动即拒 |
138
149
  | `MODEL_CONNECT_TIMEOUT_MS` | `30000` | 网关连接超时 |
139
150
  | `MODEL_FIRST_TOKEN_TIMEOUT_MS` | `120000` | 首 token 超时 |
140
151
  | `MODEL_IDLE_TIMEOUT_MS` | `300000` | 流中 idle 超时(`0` 关) |
package/USAGE.md CHANGED
@@ -302,7 +302,7 @@ QUESTION_TTL_MS=300000
302
302
  错误文案直接给出该改成什么。所以不存在"两个名字同时设"的状态,也不存在"旧名还在悄悄生效"的状态。
303
303
 
304
304
  **`LSP_ENABLED` 一分为二**:它原来同时驱动两条腿,而且两腿缺省相反——沙箱腿缺省**关**(要烤好的
305
- `sema-code-lsp` 模板),host 腿缺省**开**(只要 PATH 上有 language server,没有就优雅退回 grep/read)。
305
+ `ai-agent-code-lsp` 模板),host 腿缺省**开**(只要 PATH 上有 language server,没有就优雅退回 grep/read)。
306
306
  现在沙箱腿仍是 `LSP_ENABLED`(opt-in),host 腿归 `LSP_HOST_ENABLED`(opt-out)。
307
307
  🪦 `LSP_ENABLED=false`(拆分前唯一的 host 腿逃生舱)自 3.0.0 起是墓碑:拒启并指路 `LSP_HOST_ENABLED`。
308
308
  `LSP_ENABLED=true` 不受影响——那是沙箱腿自己的 opt-in,语义没变。
@@ -332,7 +332,7 @@ QUESTION_TTL_MS=300000
332
332
  | `sessionId` | — | 续聊:带上次返回的 `sessionId`,服务端自动 wake 历史 |
333
333
  | `images` | — | 图文输入 `[{data,mimeType}|{url}]`(模型需支持 vision) |
334
334
  | `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 形走本地文件店。 |
335
- | `scenario` | — | `default`(默认)/ `code-review`(见 §5)/ `scan`(同 §5 的 repo 只读工具但**中性无框架提示词**——objective+中心下发 skill 全权主导输出,OA 扫描类用)/ **配置控制面可声明任意新场景**(`{name, toolset: none\|repo-readonly, prompt?}`,组合即配置、能力钉死在部署;restart-to-apply;center 可覆盖内建名,boot 日志 `config_center_scenarios.shadowsBuiltin` 可审计) |
335
+ | `scenario` | — | `default`(默认)/ `code-review`(见 §5)/ `scan`(同 §5 的 repo 只读工具但**中性无框架提示词**——objective+中心下发 skill 全权主导输出,OA 扫描类用;与 `code-review`/`team` 同属 clone-free 无执行环境场景,见 §5 末)/ **配置控制面可声明任意新场景**(`{name, toolset: none\|repo-readonly, prompt?}`,组合即配置、能力钉死在部署;restart-to-apply;center 可覆盖内建名,boot 日志 `config_center_scenarios.shadowsBuiltin` 可审计) |
336
336
  | `repo` / `council` / `debate` | — | `repo` 为 `code-review`/`scan` 必填;`council`/`debate` 仅 `code-review`,见 §5 |
337
337
  | ~~model / tools / prompt~~ | 🚫 | **不接受**——服务端注入 |
338
338
 
@@ -409,6 +409,22 @@ curl -s http://<host>:8090/v1/runs/<taskId> -H 'x-agent-principal: user:42' #
409
409
  ```
410
410
  服务端需配 `GIT_API_BASEURL` + `GIT_API_TOKEN`(只读,服务端持有);缺则 501,缺 `repo` 则 400。建议走异步 `/v1/runs`(council/debate 几分钟级)。
411
411
 
412
+ > **`GIT_API_BASEURL` 写法**:必须是**含 scheme 的完整 baseURL**(如 `https://git.example.com`),
413
+ > 不是裸主机名——服务端直接拼 `${GIT_API_BASEURL}/api/v1/...` 发请求,裸 `git.example.com` 会拼出非法 URL 而 fetch 失败。
414
+ > 末尾不要带 `/`,也不要把 `/api/v1` 写进来。
415
+
416
+ > **升级注记(7.7.0)**:本版起 `code-review` / `scan` / `team` / `toolset: repo-readonly|none` 的场景改跑
417
+ > 无执行环境的引擎。**升级前**若还有这些场景的 durable 挂起任务(需同时满足:配了 `REMOTE_EXEC`、开了
418
+ > `DURABLE_APPROVAL`、任务已 park),它们在新版上续跑会以 409 `checkpoint.unsupported_version` 响亮失败
419
+ > (run 行落 failed 并释放会话,不会占住会话)——重新提交该任务即可。升级前排空这类挂起任务可完全避免。
420
+
421
+ **clone-free 承诺(`code-review` / `scan`)**:这两个场景(含 `council`/`debate` 的镜头与仲裁子任务、以及
422
+ 配置控制面用 `toolset: repo-readonly` / `none` 声明的场景)一律跑在**不带执行环境的引擎**上——
423
+ 模型看到的工具面只有声明的只读仓库工具(`repo_tree` / `repo_read_file` / `repo_pull_diff`)加 `Now`,
424
+ **没有** `Bash` / `Edit` / `Write` / `Read` 等落盘工具,不 clone、不写盘、不起沙箱。
425
+ 即使部署配了 `REMOTE_EXEC`(host/e2b/k8s/ssh/adb/local-docker)也如此:执行环境只属于 `default` / `code`
426
+ 这类需要动手的场景。`team` 场景同理(协调者只调 `run_team`,成员是纯讨论人格)。
427
+
412
428
  ## 6. 逐 token 直播(同步流式,连接挂着)
413
429
  ```bash
414
430
  curl -N http://<host>:8090/v1/tasks/stream -H 'content-type: application/json' \
@@ -423,11 +439,93 @@ curl -N http://<host>:8090/v1/tasks/stream -H 'content-type: application/json' \
423
439
  | `GET /v1/sessions/<id>` | 审计回溯:当前上下文 + 摘要(owner 校验) |
424
440
  | `GET /v1/approvals?owner=user:42` | 高危写审批(durable,需 `DURABLE_APPROVAL=true`):**operator** 看待办队列(可按 owner 过滤);非 operator 只看自己的 |
425
441
  | `POST /v1/approvals/<sessionId>/decide` `{"decision":"approve"|"deny","reason":"…"}` | 批/否并恢复挂起任务(CAS,重复决议 409);**仅 operator**(非 operator → 403)。5.0.0 起旧轮询腿 `POST /v1/approvals/<id>` 已退役 |
442
+ | `GET /v1/capabilities/scenarios/<name>` | 单个场景的只读详情:工具面、提示词概览、**本部署现在跑不跑得动**(见下) |
426
443
  | `GET /health` | 健康(无需鉴权) |
427
444
  | `GET /metrics` | Prometheus 指标(有 token 时需带) |
428
445
 
446
+ > **场景可用性(7.7.0 起)**:场景详情里 `enabled` 与 `available` 是**两件事**。`enabled` = 这条场景
447
+ > **被声明**了(内建恒 true);`available` = **本部署现在真跑得动**——后端依赖到位没有。不可用时另带一个
448
+ > **机读**原因键 `unavailableReason`(当前唯一取值 `git_client_unconfigured` = 没配 `GIT_API_BASEURL`,
449
+ > 命中 `scan` / `code-review` 这类只读仓库场景)。契约:`available:false` **⟺** 真发一次该场景的请求会
450
+ > 吃 **501**;`available:true` 时 `unavailableReason` **整键缺席**(缺席 = 没有理由,别读成空串)。
451
+ > 两面共用同一份判据,所以列表上画得出的场景点下去不会再突然 501。请按 `unavailableReason` 的**键**分支,
452
+ > 不要去匹配英文文案——文案会改,键不会(新增取值只会追加,现役键不改语义)。
453
+
429
454
  > **operator 鉴权(审批队列)**:`OPERATOR_PRINCIPALS=ops:alice,ops:bob`(CSV)= 谁能当 operator——列任意 owner 待办 + 决议(批/否)。**单租户部署不设=旧行为**(握 service token 即 operator,向后兼容);设了之后,非名单 principal 列待办只看自己的、且**不能决议**(403,防"请求方批自己的高危操作"绕过 F4 闸)。⚠️ **多租户形拒启**(#157-①):`DURABLE_APPROVAL=true` + `REQUIRE_PRINCIPAL=true` 而 `OPERATOR_PRINCIPALS` 空 ⇒ 进程启动失败并点名修法——否则空名单会让任一已验证租户读到其他租户的待批队列(读面 true-for-all)。设名单,或确属单租户则不设 `REQUIRE_PRINCIPAL`。
430
455
  >
456
+ > **⚠️ hook-wired 部署里,parked 后台子代可能赎回不了(常态,不是升级窗口)。**
457
+ > 引擎 5.19.0 起,一个任务的 **PreToolUse screening 面下延管辖它委派出去的子代**,于是 hook-wired 父
458
+ > 派出的子代 park 时,checkpoint 记的祖先约束层数是 **2**(screening 席 + 父自己的策略席);而本服务的
459
+ > 赎回腿重建得出的只有 **1** 层。引擎按**层数**做 pre-CAS 校验 ⇒ 每次赎回都被响亮拒
460
+ > (`resume.parent_constraint_mismatch`,checkpoint **保持 pending 不被消费**,不静默降级成更松的链)。
461
+ >
462
+ > **7.7.0(引擎 5.20.0)起 `TOOL_TRACE` 已退出射程。** 引擎 5.20.0 新增「这条 PreToolUse 面只观察、
463
+ > 不裁决」的声明口,本服务的诊断 tracer(`TOOL_TRACE=true` 装的那只,恒不出判词)已按实声明 ⇒ 它**不再
464
+ > 铸筛查席**,该部署的层数回到 1、赎回照常通。**开着 `TOOL_TRACE` 不再需要在「诊断」与「后台子代能不能
465
+ > 赎回」之间二选一。**
466
+ >
467
+ > 📎 连带的一处**日志**变化(不是回归):引擎那行「可写工具面没有 effect-aware 门」的启动告警,此前把
468
+ > 诊断 tracer 当成一道门而被抑制;声明之后不再抑制,所以「没配任何工具策略层 + 开着 `TOOL_TRACE`」的
469
+ > 部署升级后会多出那行告警。tracer 从来没有门住任何东西,原先的抑制本身才是问题。
470
+ >
471
+ > **仍在射程内的只剩一条**:调用方提交里带 `settings.hooks.PreToolUse`(未开 `REQUIRE_PRINCIPAL` 的部署
472
+ > 对外开放此面)。那是**会真裁决**的面(出得来 deny/ask),按契约**不能**打观察标——打了等于让引擎把
473
+ > 调用方的判词静默丢弃,比拒绝本身坏得多。这条面上的席位是每请求闭包、park 时未持久化,跨副本重建不出,
474
+ > 所以拒绝仍是正确行为。不带该字段的部署**完全不受影响**。
475
+ >
476
+ > **精确的兼容矩阵**(本服务恒供 1 层;引擎只比层数,所以下表就是全部情形。5.18.1 / 5.19.0 / 5.20.0 三个
477
+ > 引擎上都实测过):
478
+ >
479
+ > | 挂起的是谁 | checkpoint 是哪版铸的 | 父有没有**会裁决**的 PreToolUse 面 | 行里记的层数 | 本服务供的层数 | 结果 |
480
+ > | --- | --- | --- | --- | --- | --- |
481
+ > | **第一代**子代 | ≤ 5.18.1 | 任意 | 1 | 1 | ✅ 照常赎回 —— **升级本身不会弄坏存量行** |
482
+ > | **第一代**子代 | 5.19.0 | 否 | 1 | 1 | ✅ 照常赎回 |
483
+ > | **第一代**子代 | 5.19.0 | **是**,或**只是开了 `TOOL_TRACE`** | 2 | 1 | ❌ 永久拒(见下「没有恢复路径」) |
484
+ > | **第一代**子代 | ≥ 5.20.0 | 否(含只开 `TOOL_TRACE`) | 1 | 1 | ✅ 照常赎回 |
485
+ > | **第一代**子代 | ≥ 5.20.0 | **是**(调用方 `settings.hooks.PreToolUse`) | 2 | 1 | ❌ 永久拒 |
486
+ > | **嵌套**(孙代及更深) | 任意 | 任意 | **≥2** | 1 | ❌ 永久拒(5.19.0 之前就如此,历版无变化) |
487
+ >
488
+ > ⚠️ 嵌套那一行**不是恒等于 2**:引擎的子代链是「继承来的整条 + (有会裁决的 hook 就加一席) + 自己那一层」
489
+ > 逐层追加,所以嵌套与 hook 叠加时记的层数会**超过** 2(有钉实测:`test/parked-revive-e2e.test.ts` 的计数锚)。
490
+ > 对本服务而言结论一样(供 1,任何 ≥2 都拒),但**别把错误文案里的那个数字当成层深的可靠读数**。
491
+ >
492
+ > ⇒ **纠正一个容易想当然的说法**:上游 CHANGELOG 写的「升级前后跨版本 drain」对本服务**不是硬要求**——
493
+ > 旧行记的就是 1、我们供的也是 1,升级方向不产生错配(反向回滚同理)。滚版前把 `GET /v1/approvals` 排空
494
+ > 仍是好习惯(减少活过开关切换的行),但它**解决不了**下面这条。
495
+ >
496
+ > **没有恢复路径,只有预防旋钮。** 层数是 park 那一刻**写死进 checkpoint** 的:事后再批一次、事后改配置、
497
+ > 事后升级或回滚引擎版本,都不改行里记的 2 也不改我们供的 1 ⇒ **已经搁浅的行赎回不回来**
498
+ > (它们保持 pending 直到 TTL/reap;那次操作只能作为**新任务**重跑)。
499
+ > **这条对 7.6.0(引擎 5.19.0)期间开着 `TOOL_TRACE` 铸下的行同样成立**:升到 7.7.0 只让**此后**新铸的行
500
+ > 回到 1 层,那批老行仍记着 2,批不动——请把它们当作**新任务**重跑,或等 TTL/reap 收走。
501
+ > 今天仍有效的预防旋钮 = **在开放 `settings.hooks.PreToolUse` 的部署上不依赖后台子代的 durable 审批**
502
+ > (或用 `REQUIRE_PRINCIPAL` 关掉该面),同样只对**此后**新铸的行生效。
503
+ >
504
+ > **🔧 升级到 7.7.0(引擎 core 5.20.0)前:检查数值旋钮的写法。** 5.20.0 把「坏数值旋钮被接受、然后
505
+ > 悄悄做**相反**的事」这一类全部改成响亮拒或响亮钳位。两条与运维直接相关:
506
+ >
507
+ > - **写成 `1e9` / `1_800_000` 的毫秒旋钮会跳回字面值。** 引擎自己读的那五个 MCP **毫秒**旋钮
508
+ > (`MCP_TOOL_TIMEOUT`、`MCP_TOOL_TIMEOUT_TOTAL`、`MCP_IDLE_TIMEOUT_STDIO`、`MCP_IDLE_TIMEOUT_HTTP`、
509
+ > `MCP_TIMEOUT`)此前是 `parseInt` 语义:`1e9` 实际生效成 **1 毫秒**,`30s` 生效成 30 毫秒。升级后它们
510
+ > 按**字面值**生效并钳进 `[1000, 2147483647]` **毫秒**。
511
+ > ⛔ **恰恰是这两种迁移写法不会有任何告警**(亲读引擎 `parseEnvMs` 确认):`1e9` / `1_800_000` 升级后
512
+ > 是**合法且在区间内**的值,于是既不钳位也不告警——旧部署上「1 毫秒」会**静默**跳成十亿毫秒 / 三十分钟。
513
+ > 告警只在两种情形打:值**读不成数**(整条忽略、回落内置默认)或**越出区间**(钳位后点名)。而且解析发生在
514
+ > **首次真用到该 MCP 设置**时,不是进程启动时——所以「启动没看到告警」不代表没变。
515
+ > ⇒ **升级前逐个人工核对这五个值,把非纯数字的写法改成纯数字**;别指望日志替你发现。
516
+ > ⚠️ `MAX_MCP_OUTPUT_TOKENS` **不是毫秒旋钮**,别按上面那个区间去改它:它是 **token 数**,本次只是
517
+ > 换用同一套数字文法(此前 `1e5` 被读成 4;换文法后按字面值),**取值范围照旧不设上限**。
518
+ > 上面这组旋钮由**引擎**直接读 `process.env`,本服务不经手。本服务自己解析的数值旋钮走的是另一条
519
+ > 判据(不受本次变更影响):承重旋钮**非数字即启动失败并点名**、越界即启动失败,少数被显式标成
520
+ > fail-safe 的可选旋钮回落默认值并打一行 warn。
521
+ > - **坏的 retention 旋钮升级后是「拒绝」,不是「清洗」。** 引擎的 `reap` 家族
522
+ > (后台代理行 / 信箱 / workflow run / 两个花名册店)对**非有限或负**的界改抛 `config.retention_policy_invalid`。
523
+ > 症状是**每一次 reap 都抛、行只进不出**(此前 `NaN` 会塌成「删掉该 scope 下每一条终态行」,
524
+ > 花名册的 `maxAgeMs` 则让每个 durable 地址都读成已过期)。**先把旋钮改对再升级,引擎不会替你修**。
525
+ > 本服务的 `BG_AGENT_RETENTION_MS` / `WORKFLOW_RUN_RETENTION_MS` / `WORKFLOW_JOURNAL_RETENTION_MS` /
526
+ > `ROSTER_RETENTION_MS` 在 config 层已是「非数字启动即失败、负值钳到 1 分钟下限」,所以经**文档化的
527
+ > env 通道**配置的部署碰不到这条;它是给「自带注入式配置」的集成方与「看到这个错误码时怎么读」准备的。
528
+ >
431
529
  > **🔧 升级到 7.5.0(引擎 core 5.17.0)前:把待决审批排空。** 5.17.0 起,park 铸行按**后端能承载的
432
530
  > 宽度**落——审批人看到的 args / 预览、盘上躺着的行、resume 真正执行的那份参数,以及运维在 `/decide`
433
531
  > 上要回显的那个不透明 `boundInputHash`,都从同一份投影铸出。**本服务的两条 checkpoint 后端
@@ -55,7 +55,7 @@
55
55
  import type { AskRow, ApprovalAskStore } from "./plugins/approval-ask-store-sql.js";
56
56
  import type { BatchState } from "./approval-ask-machine.js";
57
57
  import type { CheckpointAskCandidate } from "./plugins/checkpoint-store-sql.js";
58
- import type { RunRecord } from "./plugins/store-contracts.js";
58
+ import { type RunRecord } from "./plugins/store-contracts.js";
59
59
  import type { Logger } from "./observability/logger.js";
60
60
  import type { Metrics } from "./observability/metrics.js";
61
61
  import { type DenyReason, type VoidReason } from "./approval-deny-reasons.js";
@@ -1,10 +1,11 @@
1
+ import { isTerminalRunStatus } from "./plugins/store-contracts.js";
1
2
  import { DENY_REASONS, VOID_REASONS } from "./approval-deny-reasons.js";
2
3
  import { buildRevokeFrame } from "./approval-card.js";
3
4
  import { encodeCheckpointScope } from "./security.js";
4
- /** 判据 2 的「非 suspended 终局」词表(§8 A-1;`RunRecord["status"]` = `"running" | TaskStatus`,
5
- * TaskStatus = completed|blocked|failed|suspended|needs_review)。`suspended`/`needs_review`/`running`
6
- * 都不是终局 落判据 3 保持。 */
7
- const TERMINAL_RUN_STATUS = new Set(["completed", "failed", "blocked"]);
5
+ /* 判据 2 的「非 suspended 终局」判据 = `store-contracts.ts` {@link isTerminalRunStatus}(词表属主处的
6
+ * 穷举 switch,core 加/删 `TaskStatus` 成员当场 tsc 红)。此处**不再手抄**一份 `ReadonlySet<string>`:
7
+ * 那份的门对象是 `string`,对词表增删两向都无钉,而这张词表真变过(core 5.8.0 `"timeout"`)。
8
+ * `suspended`/`needs_review`/`running` 都不是终局 落判据 3 保持。 */
8
9
  /** core 给「被取消」的 run 打的 `errorCode`(§9 C3 的取消判别键)。 */
9
10
  const CANCELLED_ERROR_CODE = "cancelled";
10
11
  /**
@@ -140,7 +141,7 @@ export function decideReconcileAction(input) {
140
141
  return { kind: "void", reason: VOID_REASONS.BATCH_BOUND_ELSEWHERE };
141
142
  }
142
143
  // ② run 终局分臂
143
- if (run !== null && TERMINAL_RUN_STATUS.has(run.status)) {
144
+ if (run !== null && isTerminalRunStatus(run.status)) {
144
145
  if (run.status === "failed" && run.errorCode === CANCELLED_ERROR_CODE) {
145
146
  return { kind: "void", reason: VOID_REASONS.RUN_CANCELLED };
146
147
  }
@@ -59,7 +59,7 @@ export function createBudgetAndTracing(ctx) {
59
59
  const sideQueryAccounting = createSideQueryAccountant(metrics, costQuota, fleetUsage, fleetLease, (m) => config.modelQuotaWeights[m] ?? 1);
60
60
  // Durable offload store (core 1.47/1.49): large tool results survive a cross-replica wake. Without a
61
61
  // pool, core's task-scoped in-memory default applies (graceful: cross-wake fetch misses → preview stands).
62
- const toolResultStore = backend?.toolResult ? backend.toolResult() : undefined; // tidb/pg = SQL twins; local = core's FileToolResultStore (core 1.219 — restart-durable refs); no backend → core's in-memory default
62
+ const toolResultStore = backend?.toolResult ? backend.toolResult() : undefined; // tidb/pg = SQL twins; local = core's FileToolResultStore (core 1.219 — restart-durable refs); no backend → main.ts 的 runnerOffloadStore(Runner 侧兜底,本键的「present ⇔ durable 后端」语义不动)
63
63
  // E6 durable SessionPolicyStore — operator-tightened per-session tool rules core reads at prepare-time (subtract-only).
64
64
  // Present on every backend (incl local = core's InMemorySessionPolicyStore); undefined only on the env-only/no-backend
65
65
  // worker → feature OFF (core reads no rules). Wired into the PRIMARY Runner below + the PUT/GET policy route + E21 purge.
@@ -450,8 +450,21 @@ export async function createConfigCenterRuntime(ctx) {
450
450
  const cfgModels = (r.effective.models?.models ?? []).filter((m) => m.enabled !== false).length;
451
451
  const cfgDir = configProvider.kind === "local" ? { dir: configProvider.root } : {};
452
452
  logger.info("config_loaded", { source: configProvider.kind, version: r.effective.version, worker: cc?.worker ?? "(global)", models: cfgModels, ...cfgDir });
453
- if (configProvider.kind === "local" && cfgModels === 0)
454
- logger.warn("config_local_no_models", { ...cfgDir, hint: "config.d/models.json 空或 CONFIG_LOCAL_DIR 指错目录(选目录旋钮是 CONFIG_LOCAL_DIR,不是 LOCAL_DATA_ROOT)— 模型继续走 env 兜底" });
453
+ if (configProvider.kind === "local" && cfgModels === 0) {
454
+ // [3073]#204④ hint 真因三臂:域判形失败(文件在场却喊「空/指错」=对排障者说谎,真因在
455
+ // config_domain_invalid 里但 hint 不指路)/ 全 enabled:false / 真空缺,三态分开点名。
456
+ const modelsDomainErr = (r.domainErrors ?? []).find((de) => de.domain === "models");
457
+ const totalModels = (r.effective.models?.models ?? []).length;
458
+ logger.warn("config_local_no_models", {
459
+ ...cfgDir,
460
+ ...(modelsDomainErr ? { domainError: modelsDomainErr.error } : {}),
461
+ hint: modelsDomainErr
462
+ ? "config.d/models.json 在场但 models 域判形失败落 default(真因见本行 domainError)— 模型继续走 env 兜底"
463
+ : totalModels > 0
464
+ ? `config.d/models.json 有 ${totalModels} 条模型但全部 enabled:false — 模型继续走 env 兜底`
465
+ : "config.d/models.json 空或 CONFIG_LOCAL_DIR 指错目录(选目录旋钮是 CONFIG_LOCAL_DIR,不是 LOCAL_DATA_ROOT)— 模型继续走 env 兜底",
466
+ });
467
+ }
455
468
  }
456
469
  }
457
470
  }
@@ -630,7 +643,8 @@ export async function createConfigCenterRuntime(ctx) {
630
643
  if (effective?.scenarios) {
631
644
  const { overlay, shadows } = centerScenarios(scenarioDeps, effective.scenarios.scenarios, Object.keys(scenarios), logger);
632
645
  Object.assign(scenarios, overlay);
633
- Object.assign(scenarioDetails, centerScenarioDetails(effective.scenarios.scenarios, builtinScenarioNames));
646
+ // [C132]:可用性判据同源 —— 详情表与运行工厂读的是**同一只** scenarioDeps(repoClient 在场性)
647
+ Object.assign(scenarioDetails, centerScenarioDetails(effective.scenarios.scenarios, builtinScenarioNames, scenarioDeps));
634
648
  if (Object.keys(overlay).length > 0)
635
649
  logger.info("sema_registry_scenarios", { scenarios: Object.keys(overlay), ...(shadows.length > 0 ? { shadowsBuiltin: shadows } : {}) });
636
650
  }
@@ -365,7 +365,7 @@ export function createExecutionEnv(ctx) {
365
365
  // LSP sidecar (design/64 §13.1, the 1.86.2 seam): ONE stateless manager on RunnerDeps — core passes each
366
366
  // task's env into sessionFor at the tool mount point, so the manager reaches the SAME sandbox the agent edits
367
367
  // (no sessionId registry / per-task construction). Opt-in (LSP_ENABLED — the SANDBOX lane's knob; the host lane
368
- // below has its own, LSP_HOST_ENABLED, because its default is the opposite); with the baked `sema-code-lsp`
368
+ // below has its own, LSP_HOST_ENABLED, because its default is the opposite); with the baked `ai-agent-code-lsp`
369
369
  // template the first call is fast, without it the language server installs on first use (slow once per sandbox).
370
370
  // k8s lane (gate#2 alignment): same manager/bridge over the pod network — `ws://podIP:port` instead of E2B's
371
371
  // public wss proxy, so it needs an IN-CLUSTER worker and a sandbox image with node + the language servers baked
@@ -0,0 +1,78 @@
1
+ import { type BackgroundAgentRecord, type ToolExecuteContext } from "@sema-agent/core";
2
+ import type { ServiceConfig } from "../config.js";
3
+ import { type ApprovalBaselineConfigView, type DeploymentGovernanceConfigView, type LiveQuestionFace } from "../deployment-governance.js";
4
+ import { type SandboxPathEnvSlots } from "./deferred-sandbox-path-env.js";
5
+ /** 本腿读的配置切面(`ServiceConfig` 结构满足)。两个构造口各自的切面 + lane 判别式读的 `remoteExec`。 */
6
+ export interface ParkedReviveGateConfigView extends DeploymentGovernanceConfigView, ApprovalBaselineConfigView {
7
+ readonly remoteExec?: ServiceConfig["remoteExec"];
8
+ /** host 腿裁决 env 的 cwd 占位根(见下方 `hostGateCwd`)。 */
9
+ readonly localDataRoot?: ServiceConfig["localDataRoot"];
10
+ }
11
+ /** 会话豁免探针店的所需切面(canonical toolName 键空间;`StoreBackend.approvalExemption()` 结构满足)。 */
12
+ export interface ApprovalExemptionProbe {
13
+ has(sessionId: string, toolName: string): Promise<boolean>;
14
+ }
15
+ export interface ParkedReviveGateDeps {
16
+ /** ⚠️ **活引用**:热改字段(autonomy/commandPolicy/守卫集/审批四旋钮)每次赎回现读,见下方「禁 memoize」。 */
17
+ readonly config: ParkedReviveGateConfigView;
18
+ /** #152:活体 AskUserQuestion 面。赎回腿今天无 ALS ctx ⇒ 判 ask,与原形同判。 */
19
+ readonly question: LiveQuestionFace | undefined;
20
+ readonly approvalExemptionStore: ApprovalExemptionProbe | undefined;
21
+ readonly logger: {
22
+ info(event: string, fields?: Record<string, unknown>): void;
23
+ warn(event: string, fields?: Record<string, unknown>): void;
24
+ };
25
+ /** host 腿 cwd 占位的落地根(main.ts 的 `localRoot`;`config.localDataRoot` 优先)。 */
26
+ readonly localRoot: string;
27
+ /** 沙箱 lane 的真 env 登记簿。缺省 = 进程内唯一那只(工厂装饰器写的同一个);测试注入自建实例。 */
28
+ readonly slots?: SandboxPathEnvSlots;
29
+ }
30
+ /**
31
+ * core 1.396 的父约束席位形(`parkedResume.inheritedGate`)—— 从**载体自己的签名位推导**,绝不手写。
32
+ *
33
+ * 🔴 手写这个形状会**藏住轴**(codex 对抗复审 R1-高2 的真回归):第一版在这里手抄了一个只有
34
+ * `parentConstraints` 的接口,于是 core 载体上那个 `shellGate` 槽在类型层面根本看不见,「shellGate 轴
35
+ * 也要交回」这件事就无声地漏了一整刀。推导形让「core 又加了一条能收紧的轴」变成本仓的编译期/钉子面
36
+ * 事件(载体键集钉见 test/parked-revive-gate-wiring.test.ts)。
37
+ */
38
+ export type RebuiltInheritedGate = NonNullable<NonNullable<NonNullable<ToolExecuteContext["reviveClaim"]>["parkedResume"]>["inheritedGate"]>;
39
+ /**
40
+ * 赎回腿的父约束链重建(design/181 件二)。
41
+ *
42
+ * 语义:**同/跨副本一致地重建「部署 ⊇ 操作员」两层完整链**——审批基线(durable question 门 + F4 高危写
43
+ * 审批门 + 会话豁免探针)作 base 的 `toolPolicy` 座,经**折叠属主** `applyRuntimeGovernance`
44
+ * (→ core `tightenTaskSpec`)叠上部署治理段(autonomy / commandPolicy / MANUAL_MODE_SHELL_GATE /
45
+ * 守卫集),取其 `toolPolicy` 装进单层 `parentConstraints`。
46
+ *
47
+ * 🔴 **禁 memoize**:构造整体在 per-row lambda **内**。`autonomy`/`commandPolicy`/守卫集/审批四旋钮都是
48
+ * 热改字段(registry 热应用换 config 引用),boot 期铸一次 = 把治理冻在启动那一刻的值上,而 resume 腿的
49
+ * 书面语义是「按**当前** config 重折」(与 resolve-spec 的审批基线读活 config 同一姿势)。
50
+ *
51
+ * 🔴 **单层链是正确的响亮拒**(design/181 §2.5 特征化格):本腿恒建 count=1。嵌套子代(孙代 park,
52
+ * count≥2)的重建本腿做不到——跨副本连「祖先各层分别是什么」都没有持久化——于是 core 的 pre-CAS
53
+ * `resume.parent_constraint_mismatch` 会响亮拒绝,checkpoint 留 pending。**不许**顺手把它补齐成静默单层:
54
+ * 那等于让孙代在一条比它挂起时更松的祖先链下复活。
55
+ *
56
+ * 交回**两条轴**(载体上本仓供得出的全部,见 {@link RebuiltInheritedGate} 的推导注):
57
+ * · `parentConstraints` —— 上面那条重建链;
58
+ * · `shellGate` —— 治理段折出来的 shell 门档位。**必须交**:core 按 rank 取 `max(live, seed)`,不交
59
+ * 等于 `live` 缺席、max 恒等于 park 时的旧种子,于是「挂起期间运维收紧 AUTONOMY/MANUAL_MODE_SHELL_GATE」
60
+ * 在赎回腿上整条失效(codex R1-高2 的真回归,红先复现)。交了之后收紧被兑现、放松不被兑现(fail-safe)。
61
+ *
62
+ * 载体上**故意不供**的三个槽(都不是本腿产得出的):`ancestorRules`(会话权限规则,属会话面)、
63
+ * `admittedOrgScopes` / `orgAdmissionGoverned`(组织记忆准入,属 memory 面)—— 交一个瞎猜的值比不交更坏
64
+ * (core 对这三个都是 seed 兜底)。
65
+ *
66
+ * 时点语义(成文进 docs/ASSISTANT-WIRE-CONTRACT.md,免运维当 bug 报):
67
+ * · toolPolicy 轴 = **当前** config(与 resume 重折一致,收紧与放松都被兑现——放松是一次显式运维动作);
68
+ * · shellGate 轴 = core 取 max(live, park 时 seed)—— 收紧兑现、放松**不**兑现(fail-safe)。
69
+ * · `onAsk` 不供,但**这不等于 auto-deny**(codex R3-中1 验真纠正):带 `durableMandate` 的继承层
70
+ * 判 `ask` 时,core 只要还看得见 checkpoint 店 + durable 审批配置就把它**重新浮回 durable 门**
71
+ * (再 park 一次,人再批一次);deny 只是「durable 设施不在场」的兜底臂。⛔ 但**不是无条件**
72
+ * (codex R4-中1):core 对挂起链计 `suspendCount`,到 `maxSuspends`(默认 5)即终止任务而不再 park。
73
+ * · 客户端每请求 settings 折出来的层(`settings.permissions` 的 allow/deny/ask、模式派生的 fs-write
74
+ * ask 门、scratchpad 豁免……整类)park 时未持久化 ⇒ 重建不出。§2.3 已披露的残余,机制面有特征化钉
75
+ * (test/parked-revive-e2e.test.ts);真 settings→park→赎回的端到端钉未建(候件,随收口件一起)。
76
+ */
77
+ export declare function createParkedReviveInheritedGate(deps: ParkedReviveGateDeps): (row: BackgroundAgentRecord) => RebuiltInheritedGate;
78
+ //# sourceMappingURL=parked-revive-gate.d.ts.map
@@ -0,0 +1,114 @@
1
+ /**
2
+ * [1596]/[1597] 跨副本父约束重供席工厂 —— durable park 的**赎回腿**(design/181 件二 / #179)。
3
+ *
4
+ * 从 main.ts 的装配体里提出来的**唯一理由不是内聚,是可测**:main.ts 顶层 `void main()`,谁 import 它
5
+ * 谁就启动一整个服务,所以那条腿的**运行期**语义在原地一格都钉不住(此前只有源码字符串钉)。提出来之后
6
+ * 「重建出来的链到底裁决成什么样」变成可断言的事实(test/parked-revive-gate-wiring.test.ts 运行期钉 +
7
+ * 治理矩阵的第三维消费腿)。装配次序契约不变:main.ts 仍在同一位置构造,构造条件逐字保持。
8
+ */
9
+ import { join } from "node:path";
10
+ import { NodeExecutionEnv } from "@sema-agent/core";
11
+ import { createApprovalBaselinePolicy, createDeploymentGovernanceInputs, } from "../deployment-governance.js";
12
+ import { applyRuntimeGovernance } from "../runtime-governance.js";
13
+ import { DeferredSandboxPathEnv, isSandboxPathAdjudicationLane, sandboxPathEnvSlots } from "./deferred-sandbox-path-env.js";
14
+ /**
15
+ * host 腿裁决 env 的 cwd:**从不创建**的占位目录(resolve-spec 的 `fs-write-gate-unrooted` 同族)。
16
+ *
17
+ * 赎回腿拿不到子代的工作目录(park 时未持久化,design/181 §2.2 的 v2 候件票),而 `NodeExecutionEnv`
18
+ * 的构造要求一个 cwd。占位形的语义:**绝对形写目标照常按真身裁决**(cwd 与它们无关),相对形则在
19
+ * 一个不存在的根上 canonicalize —— 段匹配照旧命中(守卫段是路径**段**判据),方向 fail-closed。
20
+ * 真正兜住相对形的是构造口内的 `RelativeTargetLexicalEnv` 词法臂(`PathAdjudication.cwd` 缺席即自动补层)。
21
+ * ⚠️ core 5.19.0(#108)后这句话的射程变窄了:守卫改按 `ToolCallRequest.cwd ?? rootPath` 解析写目标,
22
+ * 引擎盖了戳的调用**根本走不到**本占位根(戳优先),词法臂也随之让位给真身裁决(全文见
23
+ * `deployment-governance.ts` 的 `RelativeTargetLexicalEnv` 类注)。本占位根今天只在缺戳形上承重,
24
+ * 语义不变、方向仍 fail-closed,故保留不动。
25
+ * 绝不回落 `process.cwd()`:那是**服务自己的**启动目录,把它当成子代的盘是拿错基准(resolve-spec W4 同案)。
26
+ */
27
+ function hostGateCwd(config, localRoot) {
28
+ return join(config.localDataRoot ?? localRoot, "parked-revive-unrooted");
29
+ }
30
+ /**
31
+ * 赎回腿的父约束链重建(design/181 件二)。
32
+ *
33
+ * 语义:**同/跨副本一致地重建「部署 ⊇ 操作员」两层完整链**——审批基线(durable question 门 + F4 高危写
34
+ * 审批门 + 会话豁免探针)作 base 的 `toolPolicy` 座,经**折叠属主** `applyRuntimeGovernance`
35
+ * (→ core `tightenTaskSpec`)叠上部署治理段(autonomy / commandPolicy / MANUAL_MODE_SHELL_GATE /
36
+ * 守卫集),取其 `toolPolicy` 装进单层 `parentConstraints`。
37
+ *
38
+ * 🔴 **禁 memoize**:构造整体在 per-row lambda **内**。`autonomy`/`commandPolicy`/守卫集/审批四旋钮都是
39
+ * 热改字段(registry 热应用换 config 引用),boot 期铸一次 = 把治理冻在启动那一刻的值上,而 resume 腿的
40
+ * 书面语义是「按**当前** config 重折」(与 resolve-spec 的审批基线读活 config 同一姿势)。
41
+ *
42
+ * 🔴 **单层链是正确的响亮拒**(design/181 §2.5 特征化格):本腿恒建 count=1。嵌套子代(孙代 park,
43
+ * count≥2)的重建本腿做不到——跨副本连「祖先各层分别是什么」都没有持久化——于是 core 的 pre-CAS
44
+ * `resume.parent_constraint_mismatch` 会响亮拒绝,checkpoint 留 pending。**不许**顺手把它补齐成静默单层:
45
+ * 那等于让孙代在一条比它挂起时更松的祖先链下复活。
46
+ *
47
+ * 交回**两条轴**(载体上本仓供得出的全部,见 {@link RebuiltInheritedGate} 的推导注):
48
+ * · `parentConstraints` —— 上面那条重建链;
49
+ * · `shellGate` —— 治理段折出来的 shell 门档位。**必须交**:core 按 rank 取 `max(live, seed)`,不交
50
+ * 等于 `live` 缺席、max 恒等于 park 时的旧种子,于是「挂起期间运维收紧 AUTONOMY/MANUAL_MODE_SHELL_GATE」
51
+ * 在赎回腿上整条失效(codex R1-高2 的真回归,红先复现)。交了之后收紧被兑现、放松不被兑现(fail-safe)。
52
+ *
53
+ * 载体上**故意不供**的三个槽(都不是本腿产得出的):`ancestorRules`(会话权限规则,属会话面)、
54
+ * `admittedOrgScopes` / `orgAdmissionGoverned`(组织记忆准入,属 memory 面)—— 交一个瞎猜的值比不交更坏
55
+ * (core 对这三个都是 seed 兜底)。
56
+ *
57
+ * 时点语义(成文进 docs/ASSISTANT-WIRE-CONTRACT.md,免运维当 bug 报):
58
+ * · toolPolicy 轴 = **当前** config(与 resume 重折一致,收紧与放松都被兑现——放松是一次显式运维动作);
59
+ * · shellGate 轴 = core 取 max(live, park 时 seed)—— 收紧兑现、放松**不**兑现(fail-safe)。
60
+ * · `onAsk` 不供,但**这不等于 auto-deny**(codex R3-中1 验真纠正):带 `durableMandate` 的继承层
61
+ * 判 `ask` 时,core 只要还看得见 checkpoint 店 + durable 审批配置就把它**重新浮回 durable 门**
62
+ * (再 park 一次,人再批一次);deny 只是「durable 设施不在场」的兜底臂。⛔ 但**不是无条件**
63
+ * (codex R4-中1):core 对挂起链计 `suspendCount`,到 `maxSuspends`(默认 5)即终止任务而不再 park。
64
+ * · 客户端每请求 settings 折出来的层(`settings.permissions` 的 allow/deny/ask、模式派生的 fs-write
65
+ * ask 门、scratchpad 豁免……整类)park 时未持久化 ⇒ 重建不出。§2.3 已披露的残余,机制面有特征化钉
66
+ * (test/parked-revive-e2e.test.ts);真 settings→park→赎回的端到端钉未建(候件,随收口件一起)。
67
+ */
68
+ export function createParkedReviveInheritedGate(deps) {
69
+ const { config, question, approvalExemptionStore, logger, localRoot } = deps;
70
+ const slots = deps.slots ?? sandboxPathEnvSlots;
71
+ return (row) => {
72
+ // lane 分形与 resolve-spec 同一判别式(单一属主,取值处只此一个)。
73
+ // 🔑 沙箱腿的 slot 键 = **row.sessionId**(子代自己的会话,design/181 §2.2 裁定):revive 后 core 以
74
+ // 该 sessionId 铸 env 并注册 slot(subagent → prepare-task → 工厂装饰器),这个键真能绑上。
75
+ // 键 rootSessionId 跨副本恒 unbound ⇒ 每一次结构化写 fail-closed ⇒ 整条腿瘫痪。
76
+ // 未绑时转发面报错 → core 读成 unresolvedSymlink → 守卫策略 deny(成文的 fail-closed 行为)。
77
+ const pathAdjudication = isSandboxPathAdjudicationLane(config.remoteExec?.provider)
78
+ ? {
79
+ env: new DeferredSandboxPathEnv({
80
+ sessionId: row.sessionId,
81
+ slots,
82
+ logger: { warn: (event, fields) => logger.warn(event, { ...(fields ?? {}), lane: config.remoteExec?.provider, leg: "parked_revive" }) },
83
+ }),
84
+ }
85
+ : { env: createHostAdjudicationEnv(config, localRoot) };
86
+ // 审批基线:durable 轴(问答门 + F4 审批门 + 会话豁免探针)。exempt 锚 = row.rootSessionId
87
+ // ——与 /decide 的 remember grant 同键([1591] 候裁②),重建链上的豁免语义与原链一致。
88
+ const rootSessionId = row.rootSessionId;
89
+ const baseline = createApprovalBaselinePolicy(config, {
90
+ question,
91
+ ...(approvalExemptionStore && rootSessionId
92
+ ? {
93
+ exempt: (toolName) => approvalExemptionStore.has(rootSessionId, toolName),
94
+ onExempted: (toolName, rawToolName) => logger.info("approval_exempted", { sessionId: rootSessionId, toolName, rawToolName, leg: "parked_revive" }),
95
+ }
96
+ : {}),
97
+ });
98
+ // 折叠属主 = applyRuntimeGovernance → core tightenTaskSpec(本腿一条 policy 都不自己合成)。
99
+ // `objective: ""` 只是折叠所需的最小 TaskSpec 座——本腿只取回 toolPolicy,spec 的其余面不出本函数。
100
+ const governed = applyRuntimeGovernance({ objective: "", toolPolicy: baseline }, createDeploymentGovernanceInputs(config, pathAdjudication));
101
+ // 治理段无可施加时 applyRuntimeGovernance 原样返回 base ⇒ 基线本身即为链(恒非 undefined,无需断言)。
102
+ // shellGate 只在治理段真产出档位时出现在载体上(缺席 = 本次没有 live 值可交,core 用 seed 兜底)。
103
+ return {
104
+ parentConstraints: [{ policy: governed.toolPolicy ?? baseline, durableMandate: true }],
105
+ ...(governed.shellGate !== undefined ? { shellGate: governed.shellGate } : {}),
106
+ };
107
+ };
108
+ }
109
+ /** host 腿的裁决 env(真 fs;构造是纯字段赋值,不 spawn)。**每次赎回现铸**——与「禁 memoize」同因:
110
+ * cwd 占位读的是活 config 的 `localDataRoot`。 */
111
+ function createHostAdjudicationEnv(config, localRoot) {
112
+ return new NodeExecutionEnv({ cwd: hostGateCwd(config, localRoot) });
113
+ }
114
+ //# sourceMappingURL=parked-revive-gate.js.map
@@ -1,9 +1,11 @@
1
- import { type MemoryBackend, type RunnerDeps, type ToolPolicy } from "@sema-agent/core";
1
+ import { type MemoryBackend, type RunnerDeps } from "@sema-agent/core";
2
2
  import type { PromptsDomainFaces } from "../prompts-domain-validate.js";
3
3
  import { SessionEnvironmentSelection, selectEnvironmentTool } from "../capabilities/select-environment-tool.js";
4
4
  import { sendUserFileTool } from "../capabilities/send-user-file-tool.js";
5
5
  import { selectScenario } from "../capabilities/scenarios.js";
6
+ import { type HandsLaneRegistry } from "../capabilities/hands-lane.js";
6
7
  import type { ServiceConfig } from "../config.js";
8
+ import { type LiveQuestionFace } from "../deployment-governance.js";
7
9
  import { FleetEventBus } from "../fleet/fleet-bus.js";
8
10
  import { type HookLlmCall } from "../hooks/hook-runner.js";
9
11
  import type { createHookLlm } from "../hooks/hook-llm.js";
@@ -20,6 +22,10 @@ type PrincipalCaps = ReturnType<typeof createPrincipalEntitlementsClient>;
20
22
  /** `resolveSpec` 原先从 `main()` 闭包里拿到的全部 boot 局部量。 */
21
23
  export interface ResolveSpecCtx {
22
24
  config: ServiceConfig;
25
+ /** #196:hands lane 登记簿。resolveSpec 在产出 TaskSpec 时按场景判别位登记本请求的表态,HTTP 执行点
26
+ * 凭同一只登记簿选 Runner —— **必须与 main.ts 传给 `runnerFor` 的是同一实例**(异实例 = 每个请求都
27
+ * 落回 full,收窄静默失效)。 */
28
+ handsLanes: HandsLaneRegistry;
23
29
  logger: Logger;
24
30
  metrics: Metrics;
25
31
  localRoot: string;
@@ -58,9 +64,7 @@ export interface ResolveSpecCtx {
58
64
  /** #152 ([2703] 案二):活体 AskUserQuestion 面(QuestionCoordinator 的判决探针切面)。在场(=
59
65
  * ASK_QUESTION_ENABLED)时 durable question 门按活流上下文分腿、spec 不再 stamp QUESTION_AWAITS_RESUME
60
66
  * (否则 spec.onQuestion 恒遮蔽 RunnerDeps.onQuestion 的活人腿);缺席时行为与旧形逐字一致。 */
61
- liveQuestionFace: {
62
- hasLiveContext(): boolean;
63
- } | undefined;
67
+ liveQuestionFace: LiveQuestionFace | undefined;
64
68
  approvalExemptionStore: ReturnType<StoreBackend["approvalExemption"]> | undefined;
65
69
  singleUserAutoAcceptBaseline: boolean;
66
70
  checkpointStore: ReturnType<NonNullable<StoreBackend["checkpoint"]>> | undefined;
@@ -69,22 +73,6 @@ export interface ResolveSpecCtx {
69
73
  perTaskImage: PerTaskImageRegistry;
70
74
  sessionEnvSelection: SessionEnvironmentSelection;
71
75
  }
72
- /** #152([2703] 案二):durable 部署上的 AskUserQuestion 门。活体面(QuestionCoordinator)缺席 ⇒ 原形
73
- * `createDurableQuestionPolicy()`(恒 ask ⇒ 恒 durable park)。在场 ⇒ **判决时**按活流上下文分腿:
74
- * 活流腿(bg/SSE,coordinator.runWithContext 包裹且投递面此刻可达,ALS 判)allow——工具执行落到
75
- * RunnerDeps.onQuestion 的 coordinator,问正在 tail 流的活人;无活流腿(sync /v1/tasks、verify/cascade、
76
- * durable resume 驱动、断连后的 detach 腿)ask——durable park 原语义逐字保留(#166 后无活流腿放行执行
77
- * 也不会产出空答:coordinator 无 ALS ctx ⇒ 冻结 `{kind:"unavailable"}`(src/question.ts),永不悬挂;
78
- * park 仍是把问题送到人面前的唯一那条腿,正当性不变、只是反事实前提换了)。
79
- * 工具名字面量与 server.ts 的 pre-CAS 守卫同源("AskUserQuestion",core 未根导出常量)。
80
- * ⚠️ **单一属主**(复审 A2):AskUserQuestion 的 durable 判决只有这一处。任何需要「同参重建」这条判决的
81
- * 地方(main.ts 的 parkedReviveInheritedGate 父约束链)必须调本工厂,不得自折 core 原形——两份拷贝里
82
- * 只改一份正是本条 finding 的成因。**登记豁免一处**:leader worker 腿(src/leader/wire.ts provisionWorker)
83
- * 自折 core 原形——该腿无活体问答面可装且 leader 不 import boot 层(分层),core 原形+sentinel 即其完整
84
- * 语义;豁免注在彼处互指,接活体面之日必须并回本工厂。 */
85
- export declare function createDurableQuestionGate(live: {
86
- hasLiveContext(): boolean;
87
- } | undefined): ToolPolicy;
88
76
  export declare function createResolveSpec(ctx: ResolveSpecCtx): ServiceDeps["resolveSpec"];
89
77
  export {};
90
78
  //# sourceMappingURL=resolve-spec.d.ts.map