@sema-agent/server 7.4.0 → 7.6.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 (201) 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 +80 -1
  5. package/dist/approval-card.d.ts +15 -3
  6. package/dist/approval-card.js +41 -7
  7. package/dist/approval-reconciler.d.ts +109 -12
  8. package/dist/approval-reconciler.js +152 -24
  9. package/dist/boot/config-center.js +15 -2
  10. package/dist/boot/coordinators.js +10 -2
  11. package/dist/boot/execution-env.js +1 -1
  12. package/dist/boot/org-memory.d.ts +6 -0
  13. package/dist/boot/org-memory.js +1 -1
  14. package/dist/boot/parked-revive-gate.d.ts +78 -0
  15. package/dist/boot/parked-revive-gate.js +114 -0
  16. package/dist/boot/reapers.d.ts +2 -0
  17. package/dist/boot/reapers.js +11 -4
  18. package/dist/boot/resolve-spec.d.ts +3 -19
  19. package/dist/boot/resolve-spec.js +73 -67
  20. package/dist/boot/runner-deps.d.ts +23 -1
  21. package/dist/boot/runner-deps.js +8 -11
  22. package/dist/boot/workflow-orchestration.d.ts +8 -3
  23. package/dist/boot/workflow-orchestration.js +23 -1
  24. package/dist/budget.d.ts +1 -1
  25. package/dist/budget.js +1 -1
  26. package/dist/capabilities/repo-tools.d.ts +1 -1
  27. package/dist/capabilities/repo-tools.js +8 -2
  28. package/dist/config-center/apply-effective.js +33 -10
  29. package/dist/config-provider.d.ts +1 -0
  30. package/dist/config-provider.js +23 -3
  31. package/dist/config-types.d.ts +27 -9
  32. package/dist/config.d.ts +6 -1
  33. package/dist/config.js +61 -15
  34. package/dist/deployment-governance.d.ts +168 -0
  35. package/dist/deployment-governance.js +206 -0
  36. package/dist/env-facts.d.ts +3 -1
  37. package/dist/env-facts.js +3 -1
  38. package/dist/fleet/fleet-bus.d.ts +17 -2
  39. package/dist/fleet/fleet-bus.js +68 -3
  40. package/dist/fleet/fleet-terminal-window.d.ts +98 -0
  41. package/dist/fleet/fleet-terminal-window.js +316 -0
  42. package/dist/governance-ask-marks.d.ts +31 -0
  43. package/dist/governance-ask-marks.js +122 -0
  44. package/dist/hooks/hook-runner.d.ts +28 -0
  45. package/dist/hooks/hook-runner.js +149 -25
  46. package/dist/http/routes/approvals-assistant.js +6 -7
  47. package/dist/http/routes/diagnostics.js +10 -5
  48. package/dist/http/routes/fleet.js +160 -14
  49. package/dist/http/routes/memory-policy.d.ts +2 -1
  50. package/dist/http/routes/memory-policy.js +77 -13
  51. package/dist/http/routes/runs.js +6 -2
  52. package/dist/http/routes/tasks.js +59 -22
  53. package/dist/http/routes/trace-usage.js +3 -4
  54. package/dist/http/send.d.ts +23 -0
  55. package/dist/http/send.js +23 -0
  56. package/dist/http/server.d.ts +9 -0
  57. package/dist/http/server.js +28 -14
  58. package/dist/http/sse-log.js +3 -4
  59. package/dist/http/wire-types.d.ts +7 -2
  60. package/dist/leader/diffout.d.ts +10 -0
  61. package/dist/leader/diffout.js +14 -2
  62. package/dist/leader/diffup.js +3 -2
  63. package/dist/leader/planner.js +7 -0
  64. package/dist/main.js +39 -31
  65. package/dist/observability/fail-open.d.ts +17 -2
  66. package/dist/observability/fail-open.js +19 -4
  67. package/dist/observability/prompt-manifest.d.ts +5 -1
  68. package/dist/orchestration/workflow-notify-journal.d.ts +58 -2
  69. package/dist/orchestration/workflow-notify-journal.js +130 -45
  70. package/dist/parked-decide.js +9 -4
  71. package/dist/plugins/approval-ask-store-memory.d.ts +2 -2
  72. package/dist/plugins/approval-ask-store-memory.js +3 -2
  73. package/dist/plugins/approval-ask-store-sql.d.ts +60 -5
  74. package/dist/plugins/approval-ask-store-sql.js +75 -35
  75. package/dist/plugins/background-agent-store-sql.js +16 -16
  76. package/dist/plugins/background-shell-support.d.ts +1 -1
  77. package/dist/plugins/background-shell-support.js +2 -2
  78. package/dist/plugins/breaker-state-sql.js +2 -2
  79. package/dist/plugins/checkpoint-store-sql.d.ts +67 -8
  80. package/dist/plugins/checkpoint-store-sql.js +76 -13
  81. package/dist/plugins/image-bake-store-sql.d.ts +1 -1
  82. package/dist/plugins/image-bake-store-sql.js +27 -27
  83. package/dist/plugins/image-index-sql.js +15 -15
  84. package/dist/plugins/local-checkpoint-store.d.ts +20 -1
  85. package/dist/plugins/local-checkpoint-store.js +19 -0
  86. package/dist/plugins/mailbox-store-sql.d.ts +4 -10
  87. package/dist/plugins/mailbox-store-sql.js +59 -6
  88. package/dist/plugins/memory-engine-pg.js +9 -9
  89. package/dist/plugins/memory-engine-tidb.js +7 -7
  90. package/dist/plugins/memory-sync-store-pg.js +13 -13
  91. package/dist/plugins/memory-sync-store-tidb.js +5 -5
  92. package/dist/plugins/outcome-ledger-sql.js +7 -7
  93. package/dist/plugins/pg-cost-quota.js +3 -3
  94. package/dist/plugins/pg-pool.js +84 -75
  95. package/dist/plugins/pg-rate-limiter.js +3 -3
  96. package/dist/plugins/pg-session-storage.d.ts +1 -1
  97. package/dist/plugins/pg-session-storage.js +12 -13
  98. package/dist/plugins/remote-env-host.js +3 -1
  99. package/dist/plugins/remote-env-local-docker.js +6 -3
  100. package/dist/plugins/remote-env-ssh.d.ts +13 -1
  101. package/dist/plugins/roster-store-sql.js +8 -8
  102. package/dist/plugins/store-contracts.d.ts +19 -0
  103. package/dist/plugins/store-contracts.js +42 -0
  104. package/dist/plugins/task-attachment-store.js +5 -5
  105. package/dist/plugins/task-list-store-sql.js +1 -1
  106. package/dist/plugins/tidb-cost-quota.js +1 -1
  107. package/dist/plugins/tidb-pool.js +83 -60
  108. package/dist/plugins/tidb-rate-limiter.js +1 -1
  109. package/dist/plugins/tidb-session-store.js +2 -5
  110. package/dist/plugins/tool-result-store-sql.js +2 -2
  111. package/dist/plugins/usage-window-store-sql.js +13 -13
  112. package/dist/plugins/write-behind-counter.d.ts +10 -2
  113. package/dist/plugins/write-behind-counter.js +13 -3
  114. package/dist/resource-suspend.d.ts +3 -1
  115. package/dist/resource-suspend.js +3 -1
  116. package/dist/run-local.d.ts +73 -1
  117. package/dist/run-local.js +146 -5
  118. package/dist/runs.d.ts +11 -1
  119. package/dist/runs.js +18 -3
  120. package/dist/runtime-governance.d.ts +18 -0
  121. package/dist/runtime-governance.js +90 -3
  122. package/dist/security.d.ts +12 -0
  123. package/dist/security.js +12 -0
  124. package/dist/session-sync-kernel.d.ts +13 -0
  125. package/dist/session-sync-kernel.js +13 -0
  126. package/dist/task-settings.d.ts +3 -9
  127. package/dist/task-settings.js +16 -13
  128. package/dist/tool-approval.d.ts +33 -6
  129. package/dist/tool-approval.js +80 -23
  130. package/dist/trace/core-keyset-guard.d.ts +18 -4
  131. package/dist/trace/project.d.ts +10 -1
  132. package/dist/trace/project.js +31 -0
  133. package/package.json +3 -3
  134. package/dist/boot/lexical-path-env.d.ts +0 -10
  135. package/dist/boot/lexical-path-env.js +0 -88
  136. package/dist/capabilities/oa-tools.d.ts +0 -15
  137. package/dist/capabilities/oa-tools.js +0 -54
  138. package/dist/finance/cost-taxonomy.d.ts +0 -34
  139. package/dist/finance/cost-taxonomy.js +0 -26
  140. package/dist/plugins/approval-store-sql.d.ts +0 -116
  141. package/dist/plugins/approval-store-sql.js +0 -151
  142. package/dist/plugins/file-workflow-journal-store.d.ts +0 -12
  143. package/dist/plugins/file-workflow-journal-store.js +0 -12
  144. package/dist/plugins/pg-approval-store.d.ts +0 -9
  145. package/dist/plugins/pg-approval-store.js +0 -9
  146. package/dist/plugins/pg-breaker-state.d.ts +0 -8
  147. package/dist/plugins/pg-breaker-state.js +0 -8
  148. package/dist/plugins/pg-checkpoint-store.d.ts +0 -10
  149. package/dist/plugins/pg-checkpoint-store.js +0 -10
  150. package/dist/plugins/pg-file-snapshot-store.d.ts +0 -8
  151. package/dist/plugins/pg-file-snapshot-store.js +0 -8
  152. package/dist/plugins/pg-image-bake.d.ts +0 -12
  153. package/dist/plugins/pg-image-bake.js +0 -11
  154. package/dist/plugins/pg-image-index.d.ts +0 -12
  155. package/dist/plugins/pg-image-index.js +0 -11
  156. package/dist/plugins/pg-outcome-ledger.d.ts +0 -12
  157. package/dist/plugins/pg-outcome-ledger.js +0 -11
  158. package/dist/plugins/pg-resume-anchor-store.d.ts +0 -7
  159. package/dist/plugins/pg-resume-anchor-store.js +0 -7
  160. package/dist/plugins/pg-run-store.d.ts +0 -9
  161. package/dist/plugins/pg-run-store.js +0 -9
  162. package/dist/plugins/pg-session-policy-store.d.ts +0 -7
  163. package/dist/plugins/pg-session-policy-store.js +0 -7
  164. package/dist/plugins/pg-session-store.d.ts +0 -12
  165. package/dist/plugins/pg-session-store.js +0 -12
  166. package/dist/plugins/pg-tool-result-store.d.ts +0 -9
  167. package/dist/plugins/pg-tool-result-store.js +0 -9
  168. package/dist/plugins/pg-workflow-journal-store.d.ts +0 -9
  169. package/dist/plugins/pg-workflow-journal-store.js +0 -9
  170. package/dist/plugins/pg-workflow-run-store.d.ts +0 -9
  171. package/dist/plugins/pg-workflow-run-store.js +0 -9
  172. package/dist/plugins/tidb-approval-store.d.ts +0 -8
  173. package/dist/plugins/tidb-approval-store.js +0 -8
  174. package/dist/plugins/tidb-breaker-state.d.ts +0 -7
  175. package/dist/plugins/tidb-breaker-state.js +0 -7
  176. package/dist/plugins/tidb-checkpoint-store.d.ts +0 -9
  177. package/dist/plugins/tidb-checkpoint-store.js +0 -9
  178. package/dist/plugins/tidb-file-snapshot-store.d.ts +0 -8
  179. package/dist/plugins/tidb-file-snapshot-store.js +0 -8
  180. package/dist/plugins/tidb-image-bake.d.ts +0 -12
  181. package/dist/plugins/tidb-image-bake.js +0 -11
  182. package/dist/plugins/tidb-image-index.d.ts +0 -12
  183. package/dist/plugins/tidb-image-index.js +0 -11
  184. package/dist/plugins/tidb-outcome-ledger.d.ts +0 -12
  185. package/dist/plugins/tidb-outcome-ledger.js +0 -12
  186. package/dist/plugins/tidb-resume-anchor-store.d.ts +0 -7
  187. package/dist/plugins/tidb-resume-anchor-store.js +0 -7
  188. package/dist/plugins/tidb-run-store.d.ts +0 -10
  189. package/dist/plugins/tidb-run-store.js +0 -9
  190. package/dist/plugins/tidb-session-policy-store.d.ts +0 -7
  191. package/dist/plugins/tidb-session-policy-store.js +0 -7
  192. package/dist/plugins/tidb-tool-result-store.d.ts +0 -8
  193. package/dist/plugins/tidb-tool-result-store.js +0 -10
  194. package/dist/plugins/tidb-workflow-journal-store.d.ts +0 -9
  195. package/dist/plugins/tidb-workflow-journal-store.js +0 -9
  196. package/dist/plugins/tidb-workflow-run-store.d.ts +0 -10
  197. package/dist/plugins/tidb-workflow-run-store.js +0 -10
  198. package/dist/plugins/workflow-journal-limits.d.ts +0 -12
  199. package/dist/plugins/workflow-journal-limits.js +0 -12
  200. package/dist/sema-registry.d.ts +0 -41
  201. package/dist/sema-registry.js +0 -40
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
@@ -150,6 +150,26 @@ MODEL_CODE_ROLES=default,subagent # 不设=全中立;仅这些角色在「
150
150
  - 经 `RoleSpec.systemPrompt` 挂在**角色**上,非开发角色保持中立、全局默认 `DEFAULT_SYSTEM_PROMPT` 不变;任务自带 `systemPrompt`(或客户端注入)时仍优先。
151
151
  - **验证门**(core 1.44,opt-in):请求体带 `verify:true`(可选 `verifyRounds`,夹到 [1,5]、默认 2)→ 任务跑完后由**独立只读对抗 verifier**(`verifier` 角色,默认=主模型)证据强制地"试图 break 它",FAIL 则把 findings 注回同 session 续跑修复→重验,循环到 PASS 或轮数上限。结果带 `verification:{verdict,rounds,findings,evidence}`(`verdict` 看质量,`result`/`status` 仍是实现的)。**仅 `/v1/tasks`(同步)与 `/v1/runs`(异步)**——`/v1/tasks/stream` 不支持(多轮非单流,请求 verify 会 400)。verifier 工具默认 = 实现任务工具滤掉 `effect:"write"`(只读边界)。`/metrics` 加 `verifications_total{verdict}`。
152
152
  - **记忆(design/138 文件记忆引擎,2026-07-08 起唯一记忆面)**:core 注入式文件引擎——任务开始时 materialize 记忆目录(`MEMORY_ENGINE_DIR`,默认 `~/.ai-agent`),模型用**普通文件技能**读写记忆(CC `# Memory` 指令 + 派生索引;无 remember/recall 工具),任务边界 harvest 门(secret/cap 扫描)提交。单用户默认开,`MEMORY_ENGINE=off` 显式关;多租户恒关(文件基座无租户隔离,fail-closed)。旧 SQL 记忆面(`MEMORY_BACKEND`/`EMBEDDING_*`/`MEMORY_READ_LIMIT`/去重/向量检索、`GET/DELETE /v1/memory` 与 session memory 写 verb)已退役,数据不迁移——升级后对库跑一次 `scripts/drop-memory-tables.sql`。`body.memoryWrite:false` 仍是每请求只读开关(harvest 不提交)。
153
+ - **org 记忆准入(design/170 件A,7.0.0 起 BREAKING)**:`org:*` 记忆 scope 分**两个来源**——部署自证
154
+ (env `MEMORY_SCOPE` 的 org 形 + **单用户部署**的 `projects[].defaultScopes` org 键)直通;**多租户**
155
+ 部署里由调用方 `projectId` 选中的登记簿 org 键算 **request 来源**,必须拿到授权目录的逐 principal
156
+ 授予才准入,拿不到一律 fail-closed 拒(纯读泄露面:projectId 只过形状门不过授权)。目录源**三态单选,
157
+ 授权面不双源合并**:config-center 远程腿(per-principal `orgMemory` 段)> `MEMORY_ORG_DIRECTORY_JSON`
158
+ 静态表 > 缺席(request 来源的 org scope 恒拒)。同一个目录也是 `GET /v1/memory/export` /
159
+ `POST /v1/memory/sync/:scope` 的 `org:` 属主门真源(写面另需条目 `write:true`);拒绝形 = 终局
160
+ `memory.admission_denied`(403)/ 瞬时 `memory.admission_required`(503,带 `retryAfterSec`)。
161
+
162
+ | env | 缺省 | 说明 |
163
+ |---|---|---|
164
+ | `MEMORY_ORG_ADMISSION_MODE` | `enforce` | `enforce`=判决即结果;`audit`=**运维诊断位**——准入面判决照算、拒绝降为日志+metric,零行为变化(不整拒、不窄化写面);`/v1/memory/export`、`/v1/memory/sync/:scope` 的 `org:` 属主门在 audit 下**逐字保持收编前的 operator-only**(未验证的目录不得开数据面)。两模式都要求目录 client 在场 |
165
+ | `MEMORY_ORG_DIRECTORY_JSON` | 缺省 | 单机形静态授权表 `{"<principal>":{"org:acme":{"write":true}}}`。**启动期整表校验,坏表拒启动**。有 config-center 时被忽略(center 胜 + warn) |
166
+ | `MEMORY_ORG_GRANT_TTL_MS` | `60000`(`[1000, 3600000]`) | 授予/负结果的缓存 TTL = 「有界 LKG」:**新任务/新 resume 腿**的准入判决滞后 ≤ 此值;**在跑任务不受吊销影响**(判决点在 prepare 期) |
167
+ | `MEMORY_ORG_UNAVAILABLE_BACKOFF_MS` | `10000`(`[500, 600000]`) | 目录取不到之后的退避窗(只用于取数失败臂,不用于负结果) |
168
+
169
+ ⚠️ **拒启动**:多租户 + 记忆面点亮 + `projects[].defaultScopes` 里有 org 键 + 目录源缺席 ⇒ 启动报错
170
+ 并点名 projectId(该部署的每个此类请求都会在 prepare 期整拒,响亮拒启动比静默全拒服务诚实)。
171
+ ⚠️ **回滚脚枪**:回滚到「无 config-center」模板前先清掉残留的 `MEMORY_ORG_DIRECTORY_JSON` —— 否则
172
+ 它作为 operator 自证通道**复活旧授权**(详见 `docs/DEPLOY-PREREQS.md`)。
153
173
 
154
174
  **可选 — 模型级联 cascade(core 1.45,opt-in)**
155
175
  ```bash
@@ -282,7 +302,7 @@ QUESTION_TTL_MS=300000
282
302
  错误文案直接给出该改成什么。所以不存在"两个名字同时设"的状态,也不存在"旧名还在悄悄生效"的状态。
283
303
 
284
304
  **`LSP_ENABLED` 一分为二**:它原来同时驱动两条腿,而且两腿缺省相反——沙箱腿缺省**关**(要烤好的
285
- `sema-code-lsp` 模板),host 腿缺省**开**(只要 PATH 上有 language server,没有就优雅退回 grep/read)。
305
+ `ai-agent-code-lsp` 模板),host 腿缺省**开**(只要 PATH 上有 language server,没有就优雅退回 grep/read)。
286
306
  现在沙箱腿仍是 `LSP_ENABLED`(opt-in),host 腿归 `LSP_HOST_ENABLED`(opt-out)。
287
307
  🪦 `LSP_ENABLED=false`(拆分前唯一的 host 腿逃生舱)自 3.0.0 起是墓碑:拒启并指路 `LSP_HOST_ENABLED`。
288
308
  `LSP_ENABLED=true` 不受影响——那是沙箱腿自己的 opt-in,语义没变。
@@ -408,6 +428,65 @@ curl -N http://<host>:8090/v1/tasks/stream -H 'content-type: application/json' \
408
428
 
409
429
  > **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`。
410
430
  >
431
+ > **⚠️ 引擎 core 5.19.0 起:hook-wired 部署里,parked 后台子代赎回不了(常态,不是升级窗口)。**
432
+ > 5.19.0 让一个任务的 **PreToolUse screening 面下延管辖它委派出去的子代**,于是 hook-wired 父派出的
433
+ > 子代 park 时,checkpoint 记的祖先约束层数是 **2**(screening 席 + 父自己的策略席);而本服务的赎回腿
434
+ > 重建得出的只有 **1** 层。引擎按**层数**做 pre-CAS 校验 ⇒ 每次赎回都被响亮拒
435
+ > (`resume.parent_constraint_mismatch`,checkpoint **保持 pending 不被消费**,不静默降级成更松的链)。
436
+ >
437
+ > **判断本部署在不在射程内**(任一为真即在):① `TOOL_TRACE=true` —— 它装的诊断 tracer 自带 PreToolUse 面,
438
+ > **哪怕它一条裁决都不出**(纯观察者),引擎的判据是「席位在不在」而不是「它说了什么」;
439
+ > ② 调用方提交里带 `settings.hooks.PreToolUse`(未开 `REQUIRE_PRINCIPAL` 的部署对外开放此面)。
440
+ > 两条都不沾的缺省部署**逐字旧行为**,完全不受影响。
441
+ >
442
+ > **精确的兼容矩阵**(本服务恒供 1 层;引擎只比层数,所以下表就是全部情形。已在 5.18.1 与 5.19.0 两个
443
+ > 引擎上实测过):
444
+ >
445
+ > | 挂起的是谁 | checkpoint 是哪版铸的 | 父是否 hook-wired | 行里记的层数 | 本服务供的层数 | 结果 |
446
+ > | --- | --- | --- | --- | --- | --- |
447
+ > | **第一代**子代 | ≤ 5.18.1 | 否 | 1 | 1 | ✅ 照常赎回 |
448
+ > | **第一代**子代 | ≤ 5.18.1 | **是** | 1 | 1 | ✅ 照常赎回 —— **升级本身不会弄坏存量行** |
449
+ > | **第一代**子代 | 5.19.0 | 否 | 1 | 1 | ✅ 照常赎回 |
450
+ > | **第一代**子代 | 5.19.0 | **是** | 2 | 1 | ❌ 永久拒(见下「没有恢复路径」) |
451
+ > | **嵌套**(孙代及更深) | 任意 | 任意 | **≥2** | 1 | ❌ 永久拒(5.19.0 之前就如此,本版无变化) |
452
+ >
453
+ > ⚠️ 嵌套那一行**不是恒等于 2**:引擎的子代链是「继承来的整条 + (有 hook 就加一席) + 自己那一层」逐层
454
+ > 追加,所以嵌套与 hook 叠加时记的层数会**超过** 2(有钉实测:`test/parked-revive-e2e.test.ts` 的计数锚)。
455
+ > 对本服务而言结论一样(供 1,任何 ≥2 都拒),但**别把错误文案里的那个数字当成层深的可靠读数**。
456
+ >
457
+ > ⇒ **纠正一个容易想当然的说法**:上游 CHANGELOG 写的「升级前后跨版本 drain」对本服务**不是硬要求**——
458
+ > 旧行记的就是 1、我们供的也是 1,升级方向不产生错配(反向回滚同理)。滚版前把 `GET /v1/approvals` 排空
459
+ > 仍是好习惯(减少活过开关切换的行),但它**解决不了**下面这条。
460
+ >
461
+ > **没有恢复路径,只有预防旋钮。** 层数是 park 那一刻**写死进 checkpoint** 的:事后再批一次、事后关掉
462
+ > `TOOL_TRACE`、事后回滚引擎版本,都不改行里记的 2 也不改我们供的 1 ⇒ **已经搁浅的行赎回不回来**
463
+ > (它们保持 pending 直到 TTL/reap;那次操作只能作为**新任务**重跑)。**预防旋钮 = 关掉 `TOOL_TRACE`**
464
+ > (它本就是 default-OFF 的诊断面)**,或在开着 PreToolUse hooks 的部署上不依赖后台子代的 durable 审批**
465
+ > —— 只对**此后**新铸的行生效。
466
+ >
467
+ > **🔧 升级到 7.5.0(引擎 core 5.17.0)前:把待决审批排空。** 5.17.0 起,park 铸行按**后端能承载的
468
+ > 宽度**落——审批人看到的 args / 预览、盘上躺着的行、resume 真正执行的那份参数,以及运维在 `/decide`
469
+ > 上要回显的那个不透明 `boundInputHash`,都从同一份投影铸出。**本服务的两条 checkpoint 后端
470
+ > (SQL 双生 / 本地文件)都是 JSON 序列化**,已按 5.17.0 的新轴显式声明 `fidelity: "json"`,所以对
471
+ > **JSON 值域**(模型产出的 args 恒在此域内)这次折叠规则变更是**逐字节零变化**:同一份 args 在 7.4.0
472
+ > 和 7.5.0 上算出的 `boundInputHash` 相同,已经发出去的哈希不需要重新取。
473
+ > 需要动作的只有一种行:**升级前就已经 pending 的那些**——它们带的是旧版本铸的哈希与(可能已降级的)
474
+ > 参数,新引擎不会追认改写。**滚版前把它们批/否掉**(`GET /v1/approvals` 列出来,逐个 `/decide`),
475
+ > 或明确接受那批老行仍按旧语义结算。新铸的行不受影响。
476
+ > **同一次升级还要重建 mailbox 两表。** `mailbox_messages` 新增 `hop_chain` 列(引擎的 peer 消息守卫),
477
+ > 而建表语句是 `CREATE TABLE IF NOT EXISTS` —— 对已存在的旧表**一字不改**,升级后每一次 teammate 消息
478
+ > 投递都会报 unknown column 并失败。按本服务的 schema 契约(删库重建、不做增量迁移),滚版时
479
+ > **重建 `mailboxes` / `mailbox_messages` 两表**(或整库),基线见 `docs/schema/baseline-*.sql`。
480
+ > 该表是带 TTL 的短命投递队列而非账本,重建只丢排队中的 teammate 消息;介意就先让在飞的 peer 会话收敛。
481
+ >
482
+ > 顺带一提,新引擎会在铸点**直接拒绝 park**(点名后端、退回同步门)的只有两类值,而且都只可能由
483
+ > 部署侧的 hook / policy 改写进 args —— 模型自己给的参数永远是 JSON,碰不到任何一条:
484
+ > ① **拿不住的值**(函数、symbol、活句柄这些 `structuredClone` 复制不了的),以及 **`SharedArrayBuffer`**
485
+ > ——后者的理由不是「编不成 JSON」而是「克隆之后仍与原持有者共享同一块内存」,一行存下去别人还能改它,
486
+ > 所以在捕获性检查那一步就被拒;② **编不成 JSON 的值**(`BigInt`、循环引用)。
487
+ > 至于 `Date` / `Map` / 正则这类**能编码但会被 JSON 投影压扁**的值,不拒绝 —— 它们按投影后的形态入行,
488
+ > 若投影改变了值,引擎会拿投影后的那份**重新过一遍部署策略**再决定 park。
489
+ >
411
490
  > **parked 后台子代的待办分两个 scope 桶**(durable 审批面,core 1.389 起):父任务显式转发审批范围的常规 ask 落在该范围的 scope 下(可预算);无转发时无人值守拦下的敏感操作 ask 落在按 principal 派生的隔离 scope 下(带缺省 deadline、永不自动放行)。operator 全量列表天然两桶全见;**按 `?owner` 过滤时注意两桶可能不同名**,展示面要两个都查。
412
491
 
413
492
  ---
@@ -32,7 +32,11 @@ export declare const MAX_AGENT_NAME = 200;
32
32
  * `risk` 的三态形(设计稿 §14.1,core [2794] 回帖后定):`AskRequest.riskAxes?.{irreversible,egress}`
33
33
  * 是 **additive optional**,**缺席 = 引擎未判,不是「安全」**。所以两轴在这里是 `optional()`:
34
34
  * `true` / `false` / **缺席(未标注)** 三态各自可分,投影层**禁把缺席折算成 false** —— 那等于替引擎
35
- * 打包票。`requiresRealApproval` core 今天唯一在场的粗粒度安全类标记(必填,车2 已有真值来源)。
35
+ * 打包票。`requiresRealApproval` 是**粗粒度**安全类标记,两侧的在场契约**不同、别混**:core 侧是
36
+ * `AskRequest.requiresRealApproval?: boolean`(**可选、只在为真时带**,缺席 = 这不是一次安全类 ask,是
37
+ * 正常的否定形而不是坏形);本卡面这一格是**必填 boolean**,由车2 的入参归一化(`=== true`)而来。
38
+ * 它与两轴是两件事而不是新旧替代——原注写的「core 今天唯一在场的标记」是 `riskAxes` 上树之前的现势话,
39
+ * 自 core **5.14.0**(其 CHANGELOG 的 `AskRequest.riskAxes` additive 条)起两者并存。
36
40
  */
37
41
  export declare const ApprovalCardSchema: z.ZodObject<{
38
42
  toolName: z.ZodString;
@@ -45,6 +49,7 @@ export declare const ApprovalCardSchema: z.ZodObject<{
45
49
  egress: z.ZodOptional<z.ZodBoolean>;
46
50
  requiresRealApproval: z.ZodBoolean;
47
51
  }, z.core.$strict>;
52
+ governanceForced: z.ZodOptional<z.ZodLiteral<true>>;
48
53
  fromSubagent: z.ZodOptional<z.ZodLiteral<true>>;
49
54
  sourceTaskId: z.ZodOptional<z.ZodString>;
50
55
  sourceAgentName: z.ZodOptional<z.ZodString>;
@@ -75,6 +80,7 @@ export declare const ApprovalCardEnvelopeSchema: z.ZodObject<{
75
80
  egress: z.ZodOptional<z.ZodBoolean>;
76
81
  requiresRealApproval: z.ZodBoolean;
77
82
  }, z.core.$strict>;
83
+ governanceForced: z.ZodOptional<z.ZodLiteral<true>>;
78
84
  fromSubagent: z.ZodOptional<z.ZodLiteral<true>>;
79
85
  sourceTaskId: z.ZodOptional<z.ZodString>;
80
86
  sourceAgentName: z.ZodOptional<z.ZodString>;
@@ -107,6 +113,9 @@ export interface ApprovalCardSource {
107
113
  args?: unknown;
108
114
  argsOmitted?: boolean;
109
115
  toolCallId?: string;
116
+ /** [2942]/[2943]:治理来源标 —— 与 wire 帧**同一份素材**(`ToolApprovalFrame` 结构上满足本接口),
117
+ * 于是 live 帧 / `card_json` / 重放帧三面同源,不是三处各判一遍。 */
118
+ governanceForced?: true;
110
119
  fromSubagent?: true;
111
120
  sourceTaskId?: string;
112
121
  /** 已 redactSecrets。 */
@@ -121,8 +130,11 @@ export interface ApprovalCardSource {
121
130
  * design/172 §3.1 的**中性投影**(设计稿 §6.2)—— 写侧的唯一铸造点。
122
131
  *
123
132
  * `risk` 三态(§14.1):两轴 `optional`,`true`/`false`/**缺席(未标注)** 各自可分。`req` 是 core 交来的
124
- * `AskRequest`(可能带、也可能不带 `riskAxes`),按 `unknown` 窄读;`requiresRealApproval` 是 core 今天
125
- * 唯一在场的粗粒度安全类标记,缺席 = 这不是一次安全类 ask(core 的铸造点语义,不是我们的折算)。
133
+ * `AskRequest`(可能带、也可能不带 `riskAxes`),按 `unknown` 窄读;`requiresRealApproval` 走**独立入参**
134
+ * (车2 已归一化的 boolean),不从 `req` 里读。core 侧它是可选、只在为真时带,缺席 = 这不是一次安全类
135
+ * ask(core 的铸造点语义,不是我们的折算);卡面这一格恒在,`false` 就是那个否定形的如实投影。
136
+ * (原注写的「core 今天唯一在场的粗粒度标记」自 core 5.14.0 的 `riskAxes` 起过期,见 `ApprovalCardSchema`
137
+ * 头注。)
126
138
  */
127
139
  export declare function buildApprovalCard(source: ApprovalCardSource, req: unknown, requiresRealApproval: boolean): ApprovalCard;
128
140
  /** 落库信封的纯构造(写侧;读侧 = `ApprovalCardEnvelopeSchema.safeParse`,**同一个 schema**)。 */
@@ -35,7 +35,11 @@ const MAX_MESSAGE = 8192;
35
35
  * `risk` 的三态形(设计稿 §14.1,core [2794] 回帖后定):`AskRequest.riskAxes?.{irreversible,egress}`
36
36
  * 是 **additive optional**,**缺席 = 引擎未判,不是「安全」**。所以两轴在这里是 `optional()`:
37
37
  * `true` / `false` / **缺席(未标注)** 三态各自可分,投影层**禁把缺席折算成 false** —— 那等于替引擎
38
- * 打包票。`requiresRealApproval` core 今天唯一在场的粗粒度安全类标记(必填,车2 已有真值来源)。
38
+ * 打包票。`requiresRealApproval` 是**粗粒度**安全类标记,两侧的在场契约**不同、别混**:core 侧是
39
+ * `AskRequest.requiresRealApproval?: boolean`(**可选、只在为真时带**,缺席 = 这不是一次安全类 ask,是
40
+ * 正常的否定形而不是坏形);本卡面这一格是**必填 boolean**,由车2 的入参归一化(`=== true`)而来。
41
+ * 它与两轴是两件事而不是新旧替代——原注写的「core 今天唯一在场的标记」是 `riskAxes` 上树之前的现势话,
42
+ * 自 core **5.14.0**(其 CHANGELOG 的 `AskRequest.riskAxes` additive 条)起两者并存。
39
43
  */
40
44
  export const ApprovalCardSchema = z
41
45
  .object({
@@ -55,6 +59,25 @@ export const ApprovalCardSchema = z
55
59
  requiresRealApproval: z.boolean(),
56
60
  })
57
61
  .strict(),
62
+ /**
63
+ * [2942]/[2943] **ADDITIVE**:`true` ⇔ 这只 ask 的门来自运维治理层(语义、判定缝与「缺席 ≠ false」
64
+ * 的硬条款逐字见 `tool-approval.ts` 的 `ToolApprovalFrame.governanceForced` 与
65
+ * `governance-ask-marks.ts` 顶注)。放在**卡的顶层**而不是 `risk` 里:`risk` 讲的是引擎对这次操作的
66
+ * 风险判定(不可逆/出网/安全类),本键讲的是**门是谁下的**,两件事。
67
+ *
68
+ * ⚠️ **回滚窗的行为(codex 交叉复审 round1 [high],验真后按「真实但内生」收下)**。形版本闩
69
+ * (`schemaVersion`)**不动**:additive optional 键在 `.strict()` 下对**旧行**无碍(缺席合法),代价
70
+ * 全在**回滚方向** —— 一个降级回旧二进制的副本读到带本键的新行,`safeParse` 会失败,两条读面各自:
71
+ * · 重放腿(`buildReplayFrame`)⇒ 该行**跳过** + 一次 warn(§5.3),不炸流;
72
+ * · `askBroadcast` 幂等命中该行 ⇒ 走「持久行不可用」臂 ⇒ `"unavailable"` = **park 路由**
73
+ * (tool-approval.ts 的 `persisted-row-unusable`)。park 是本协议的 fail-safe 出口:人仍可经
74
+ * durable gate 补批 —— 不是「批准凭空消失」,更不是把它折算成 deny。
75
+ * 这个代价是**任何** additive 键在一个 `.strict()` schema 下的内生代价,不是本键独有:bump 到
76
+ * `schemaVersion: 2` 只会更糟(旧读面 `z.literal(1)` 直接全量拒收**所有**新行,连缺席本键的行都收不了)。
77
+ * ⇒ 处置 = 不改形版本;**发车条款**写进 CHANGELOG:整队滚前(不留长期混版窗),回滚窗内新卡降级为
78
+ * park(fail-safe),回滚窗结束即自愈。
79
+ */
80
+ governanceForced: z.literal(true).optional(),
58
81
  /** 委派出处(子代 ask 才在场;判别键 = `fromSubagent`,core RB-39②)。 */
59
82
  fromSubagent: z.literal(true).optional(),
60
83
  sourceTaskId: z.string().max(MAX_IDENT).optional(),
@@ -86,10 +109,16 @@ export const APPROVAL_CARD_SCHEMA_VERSION = 1;
86
109
  /**
87
110
  * core 的风险轴(`AskRequest.riskAxes`,[2829]/§14.1)的**边界窄读**。
88
111
  *
89
- * 🔴 为什么是 `safeParse` 而不是读 `req.riskAxes`:树上的 core d.ts(5.13.0)**还没有这个键**,
90
- * 而字段形已由 core 认领(5.14.0-pre)。窄读让**编译与行为解耦**——今天编译得过、缺席=未标注;
91
- * 终版到货后同一行代码自然点亮,不需要回来改一个字。裸 `as` 转型会在两个方向上都出错:既绕过了
92
- * 宪法 [2704] 的「边界必 schema」,也会在 core 真发出一个形状不同的键时静默把垃圾投上卡面。
112
+ * **已点亮**:`riskAxes` 现在既在类型面也在真码面(树上 core 5.16.0,`core/tool-policy.d.ts` 的
113
+ * `readonly riskAxes?: {…}`;package.json 的 floor 已抬到 `^5.16.0`)。窄读当初写下时的现势前提是
114
+ * 「树上 core d.ts(5.13.0)还没有这个键、字段形只由 core 认领」——该键自 core **5.14.0** 起就在
115
+ * (其 CHANGELOG `AskRequest.riskAxes` additive 条),那句现势话早已过期,**别再据它判断「core 还没
116
+ * 供值 ⇒ 卡面 risk 恒缺席」**。判在不在场以**装树的 d.ts 为准**,不要读注里的版本号
117
+ * (同族销账见 `tool-approval.ts` 的 `readBoundInputHash` 头注,#164)。
118
+ *
119
+ * 🔴 保留 `safeParse` 而不改成直读 `req.riskAxes` 的理由不变(它从来不只是等字段):窄读让**编译与行为
120
+ * 解耦**,缺席=未标注;裸 `as` 转型会在两个方向上都出错——既绕过宪法 [2704] 的「边界必 schema」,也会在
121
+ * core 发出一个形状不同的键(或加轴)时静默把垃圾投上卡面。
93
122
  *
94
123
  * 未知键被 zod 默认 strip(此处**故意不 `.strict()`**:core additive 加轴时不该让整只 ask 的卡面塌掉);
95
124
  * 形不合(如 `irreversible: "yes"`)⇒ 整个 safeParse 失败 ⇒ 按**缺席**处置 = 「未标注」,
@@ -115,8 +144,11 @@ function clip(s, max) {
115
144
  * design/172 §3.1 的**中性投影**(设计稿 §6.2)—— 写侧的唯一铸造点。
116
145
  *
117
146
  * `risk` 三态(§14.1):两轴 `optional`,`true`/`false`/**缺席(未标注)** 各自可分。`req` 是 core 交来的
118
- * `AskRequest`(可能带、也可能不带 `riskAxes`),按 `unknown` 窄读;`requiresRealApproval` 是 core 今天
119
- * 唯一在场的粗粒度安全类标记,缺席 = 这不是一次安全类 ask(core 的铸造点语义,不是我们的折算)。
147
+ * `AskRequest`(可能带、也可能不带 `riskAxes`),按 `unknown` 窄读;`requiresRealApproval` 走**独立入参**
148
+ * (车2 已归一化的 boolean),不从 `req` 里读。core 侧它是可选、只在为真时带,缺席 = 这不是一次安全类
149
+ * ask(core 的铸造点语义,不是我们的折算);卡面这一格恒在,`false` 就是那个否定形的如实投影。
150
+ * (原注写的「core 今天唯一在场的粗粒度标记」自 core 5.14.0 的 `riskAxes` 起过期,见 `ApprovalCardSchema`
151
+ * 头注。)
120
152
  */
121
153
  export function buildApprovalCard(source, req, requiresRealApproval) {
122
154
  const axes = RiskAxesEnvelopeSchema.safeParse(req);
@@ -134,6 +166,8 @@ export function buildApprovalCard(source, req, requiresRealApproval) {
134
166
  ...(riskAxes?.egress !== undefined ? { egress: riskAxes.egress } : {}),
135
167
  requiresRealApproval,
136
168
  },
169
+ // [2942]/[2943]:只在为真时投影(`=== true` 严判:非布尔真值不得把一张普通卡染成治理卡)。
170
+ ...(source.governanceForced === true ? { governanceForced: true } : {}),
137
171
  ...(source.fromSubagent === true ? { fromSubagent: true } : {}),
138
172
  ...(sourceTaskId !== undefined ? { sourceTaskId } : {}),
139
173
  ...(sourceAgentName !== undefined ? { sourceAgentName } : {}),
@@ -5,12 +5,26 @@
5
5
  * `PARKED | DENIED | VOID`,并在崩溃后补位那些没人打 expire 的孤儿 `STREAM_PENDING` 行。
6
6
  *
7
7
  * ── 判据表 v2(设计稿 §9 尾的五臂汇总,逐字落地;每臂注读口)────────────────────────────────────────
8
- * ① **identity ∧ hash 双等** ⇒ `bindBatch`(判别式返回;`ok:false` ⇒ 降级续判)
9
- * identity = (scope=owner, sessionId, toolCallId, 因果下界 `cp.createdAtMs ≥ ask.createdAtMs`)
10
- * ∧ `cp.boundInputHash === ask.boundInputHash`,**任一侧 hash 缺席 = 不命中**(§9 C2:同 session 内
11
- * `toolCallId` 会被网关重用,只靠 identity 会把旧 ask PARK 到别人的 resume 坐标上,而 `PARKED` 是
12
- * 不可回滚的终态)。读口 = `findCheckpointCandidatesForAsk`(§9 C4 窄谓词精确查,无分页假阴性);
13
- * `unparseable` 候选**视同不匹配**(单行读不出不许打断整段扫描,§8 C-6)。
8
+ * ① **身份三元组 ∧ hash 双等** ⇒ `bindBatch`(判别式返回;`ok:false` ⇒ 降级续判)
9
+ * 身份 = `sourceTaskId` 相等 ∧ `toolCallId` 相等 ∧ **因果下界**(不是等式)`cp.createdAtMs ≥
10
+ * ask.createdAtMs`;再 ∧ `cp.boundInputHash === ask.boundInputHash`。三维里只有前两维是等式,把时间
11
+ * 那一维读成等式会让合法 park 几乎命不中。逐字实现在 {@link classifyGateMatch};判据的**唯一
12
+ * 属主**是那个函数的头注,这里只列纲要,细则(祖先层 fold 否决、多候选取舍)不在此复述。
13
+ * 🔴 两处易错,写在这里免得下一个人照旧口径改码:
14
+ * · 承重的第一维是 **`sourceTaskId`**(#168 件1,黑板 [2897]③①)——`sessionId` **不进身份等式**
15
+ * (委派子代的 ask 落行记的是投递上下文的根会话,park 却发生在子代自己的 sessionId 上,只按
16
+ * session 等值会张冠李戴),它在这一层只是读口 `findCheckpointCandidatesForAsk` 的入参。
17
+ * ⚠️ 但别据此把它当无用键:{@link isAncestorFoldMint} 的祖先层否决判的正是
18
+ * `sourceTaskId !== sessionId` —— 那是内存里的承重用法,只是不属于身份等式;
19
+ * · hash **任一侧缺席一律不 bind**(硬相等不放宽,理由同下)。缺席的**归因**分两级,别写成一句:
20
+ * 身份先判 —— 同身份候选一条都没有且候选集非空 ⇒ `identity_miss`(有对家但不是这一只,或读不出);
21
+ * 只有在身份这一层没被判掉时,hash 缺席才落 `single_mint` 三形
22
+ * (`ask_only` / `checkpoint_only` / `neither`)。`single_mint` 是可观测分类、不是放宽的命中;
23
+ * 把「只有一侧铸过」这格结构事实混进 `identity_miss` 的噪声底,运维就读不出两者的区别。
24
+ * 两道等式都不许放宽的原因不变(§9 C2:同 session 内 `toolCallId` 会被网关重用,身份不严会把旧 ask
25
+ * PARK 到别人的 resume 坐标上,而 `PARKED` 是不可回滚的终态)。读口 = `findCheckpointCandidatesForAsk`
26
+ * (§9 C4 窄谓词精确查,无分页假阴性);`unparseable` 候选**视同不匹配**(单行读不出不许打断整段
27
+ * 扫描,§8 C-6)。
14
28
  * ② run 终局分臂(读口 `runStore.getRun`):`status ∈ {completed, failed, blocked}`(§8 A-1 词表修正 ——
15
29
  * `cancelled` 不是 run 状态,取消 = `failed` + `errorCode`)——
16
30
  * - `failed ∧ errorCode === "cancelled"`,或批行已 `ABORTED` ⇒ `VOID`(取消不是路由失败,§9 C3);
@@ -41,7 +55,7 @@
41
55
  import type { AskRow, ApprovalAskStore } from "./plugins/approval-ask-store-sql.js";
42
56
  import type { BatchState } from "./approval-ask-machine.js";
43
57
  import type { CheckpointAskCandidate } from "./plugins/checkpoint-store-sql.js";
44
- import type { RunRecord } from "./plugins/store-contracts.js";
58
+ import { type RunRecord } from "./plugins/store-contracts.js";
45
59
  import type { Logger } from "./observability/logger.js";
46
60
  import type { Metrics } from "./observability/metrics.js";
47
61
  import { type DenyReason, type VoidReason } from "./approval-deny-reasons.js";
@@ -82,18 +96,82 @@ export interface ReconcileInput {
82
96
  allowBind?: boolean;
83
97
  }
84
98
  /**
85
- * 判据 1 的**硬谓词**(纯函数,§9 C2 + §8 D-2)。
99
+ * 判据 1 的一次**分类**结果(#168 件1;纯数据)。
86
100
  *
87
- * 返回选中的候选,或 `undefined` = 不命中。逐条:
101
+ * 为什么不只回「命中 / 不命中」:黑板 [2897]/[2898] 两帖把「两侧摘要不等」拆成了**四种成因**,处置各不
102
+ * 相同 —— 只有一种是真的「两个值不一样」,其余三种根本只有一侧(或零侧)铸过摘要。压成同一个
103
+ * `undefined` 会让运维看着一条「不匹配」去查一件从没发生过的事。
104
+ */
105
+ export type GateMatchOutcome =
106
+ /** 三元组身份 ∧ 摘要双等 —— 判据 1 命中。 */
107
+ {
108
+ kind: "match";
109
+ candidate: CheckpointAskCandidate;
110
+ }
111
+ /** 祖先冻结 approver 层 fold 中途的那一次铸造:其后还可能有 rewrite,两侧**本不该等**([2912]③)。 */
112
+ | {
113
+ kind: "ancestor_fold_mint";
114
+ }
115
+ /**
116
+ * 单铸路径 —— **不是** mismatch([2897]③②):
117
+ * - `ask_only`:纯 sync 腿(同轮结算,永无 checkpoint 行)/ 再审批链(2 次 ask 铸造、0 次 checkpoint);
118
+ * - `checkpoint_only`:durable-first 干净 args(ask 侧 0 次铸造,checkpoint 铸出);
119
+ * - `neither`:字符串模式 `onAsk` + durableApproval(两侧都没铸)。
120
+ */
121
+ | {
122
+ kind: "single_mint";
123
+ side: "ask_only" | "checkpoint_only" | "neither";
124
+ }
125
+ /**
126
+ * 三元组身份对得上、两侧摘要都在场却**不等** —— 唯一的真 mismatch。成因是**部署自伤**而非攻击
127
+ * ([2897]②:hook/policy 把自己仍持引用的活对象在两铸点之间改了,或每读返回新值的有状态 getter)。
128
+ * 处置 = 留痕不拒:计数 + 一条 warn,行照 ②③④⑤ 走(判据 1 不命中的既有 fail-safe 路径不变)。
129
+ */
130
+ | {
131
+ kind: "hash_mismatch";
132
+ candidates: number;
133
+ }
134
+ /** 三元组身份就对不上(不是摘要的事):别的 sourceTaskId / 别的 callId / 早于本 ask 的 park。 */
135
+ | {
136
+ kind: "identity_miss";
137
+ };
138
+ /**
139
+ * 这只 ask 是不是**祖先冻结 approver 层**在委派 fold 中途铸的那一份(#168 件1④,黑板 [2912]③)。
140
+ *
141
+ * 判别位是**产品自己发的**,不是外部约定:core 的 `withDelegationProvenance` 只包**子代自己那条缝**,
142
+ * 祖先层那次走未包装的原函数 ⇒ `AskRequest.delegation` 在不在,就是「这一份是哪一层发的」。落到行上,
143
+ * `delegation.parentToolCallId` 是必填字段,铸行时逐字透传进 `parent_tool_call_id` 列 ⇒ **列非 NULL
144
+ * ⇔ delegation 在场**(不需要新列,也不需要回 `req` 里再读一次)。
145
+ *
146
+ * 第二维 `sourceTaskId !== sessionId` 把「根腿」摘出去:根腿的 `sourceTaskId` 恒等于会话锚(core 给
147
+ * `AskRequest.sourceTaskId` 填的就是该腿 sessionId),它根本不在任何 fold 里,`parent_tool_call_id`
148
+ * 为 NULL 是它的常态而不是信号。
149
+ *
150
+ * 命中 ⇒ 退出硬相等(判据 1 结构上不命中,行落 ②③④⑤)。实测(test AI 围栏)checkpoint 存的摘要**逐字
151
+ * 等于子代自己那条缝**、只不等于祖先层那一次 —— 所以收窄到这一层,子代自己的缝照常参与。
152
+ */
153
+ export declare function isAncestorFoldMint(ask: AskRow): boolean;
154
+ /**
155
+ * 判据 1 的**硬谓词**(纯函数,§9 C2 + §8 D-2;#168 件1 起返回分类而不是布尔)。
156
+ *
157
+ * 逐条:
158
+ * - 祖先层 fold 中途铸点 ⇒ 直接退出(见 {@link isAncestorFoldMint});
88
159
  * - `unparseable` 候选直接出局(读不出 ⇒ 不确定 ⇒ 不命中);
89
- * - `boundCallId` 必须逐字等于 `ask.toolCallId`(读口已按它查,这里是纵深防御);
90
- * - **因果下界**:`cp.createdAtMs >= ask.createdAtMs`(park 不可能早于它要 park 的那次 ask);
91
- * - **hash 双等**:两侧都必须在场且相等 —— 任一侧缺席即不命中(禁「能取到时才比」的可选谓词)。
160
+ * - **身份三元组**:`sourceTaskId` **相等** `toolCallId` **相等** ∧ 时间维的**因果下界**
161
+ * `candidate.createdAtMs >= ask.createdAtMs`(⚠️ 第三维**不是等式** —— 把它读成等式会让合法的 park
162
+ * 几乎命不中,行随后落 ②③⑤ 被误判)—— 摘要**不是**身份([2897]③①:
163
+ * 同 args 的两次调用摘要天然相同,只靠摘要硬相等会把第二次投递并进第一次的票);任一维在候选侧
164
+ * 缺席(读不出的 blob 没有 `sourceTaskId`)即身份不成立;
165
+ * - **hash 双等**:两侧都必须在场且相等,任一侧缺席一律不 bind;禁「能取到时才比」的可选谓词。
166
+ * 归因分两级(顺序即代码顺序):身份这一层先判 —— 同身份候选为空**且**候选集非空 ⇒ `identity_miss`;
167
+ * 走到 hash 这一层才把缺席记成**单铸** `single_mint`(见 {@link GateMatchOutcome})。
92
168
  *
93
169
  * 多候选时的取舍:优先 `status === "pending"`(活着的那张 gate),否则取最早的一条(读口按
94
170
  * `created_at ASC` 返回)。两者都满足全部硬谓词,选谁都不会错配;取 pending 只是让 `PARKED` 行落到
95
171
  * 一个还能被 resume 的坐标上,对壳更有用。
96
172
  */
173
+ export declare function classifyGateMatch(ask: AskRow, candidates: readonly CheckpointAskCandidate[]): GateMatchOutcome;
174
+ /** {@link classifyGateMatch} 的布尔面(判据 1 命中即返回那条候选)。分类信息由调用方按需另取。 */
97
175
  export declare function selectGateCandidate(ask: AskRow, candidates: readonly CheckpointAskCandidate[]): CheckpointAskCandidate | undefined;
98
176
  /** 判据表 v2 的判定(纯函数;顺序 = 设计稿 §9 尾的五臂汇总,注见文件头)。 */
99
177
  export declare function decideReconcileAction(input: ReconcileInput): ReconcileAction;
@@ -119,6 +197,25 @@ export interface ReconcileStats {
119
197
  * 那正是运维需要当场看见的东西。恒零才是健康态,不是这条计数没用了。
120
198
  */
121
199
  unmatchableNoHash: number;
200
+ /**
201
+ * 🔴 判据 1 的**真** mismatch 行数(#168 件1③,黑板 [2897]②):三元组身份对得上、两侧摘要都在场却不等。
202
+ *
203
+ * 成因裁定 = **部署自伤,不是攻击**:core 已证在真正产生两次铸造的弧上摘要恒等,两条分歧路径都要求
204
+ * 部署自己交出活对象(hook/policy 返回己持引用后在两铸点之间突变;或每读返回新值的有状态 getter)。
205
+ * 所以处置是**留痕不拒**:计一笔 + 一条 warn,行照 ②③④⑤ 的既有 fail-safe 走 —— 判据 1 不命中本来
206
+ * 就不会产生终态 denial(约束②),这里不新增任何拒绝语义。
207
+ */
208
+ hashMismatch: number;
209
+ /**
210
+ * 单铸路径行数(#168 件1②)——**不计入** {@link hashMismatch}。三形分列:
211
+ * `askOnly` 纯 sync / 再审批链;`checkpointOnly` durable-first 干净 args;`neither` 两侧都没铸。
212
+ * 它们都是**结构上只有一侧(或零侧)有摘要**,把它们读成「比过了、不匹配」是把没发生的事记成异常。
213
+ */
214
+ singleMintAskOnly: number;
215
+ singleMintCheckpointOnly: number;
216
+ singleMintNeither: number;
217
+ /** 祖先冻结 approver 层 fold 中途铸点(#168 件1④):退出硬相等,不是 mismatch 也不是单铸。 */
218
+ ancestorFoldMint: number;
122
219
  parked: number;
123
220
  denied: number;
124
221
  voided: number;