@sema-agent/server 7.43.0 → 7.44.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 (345) hide show
  1. package/USAGE.md +18 -0
  2. package/dist/adoption/plan.js +31 -192
  3. package/dist/adoption/quiesce.js +3 -86
  4. package/dist/adoption/runner.js +3 -140
  5. package/dist/adoption/sql.js +0 -74
  6. package/dist/adoption/wire.js +0 -80
  7. package/dist/approval-ask-machine.js +0 -75
  8. package/dist/approval-card.js +0 -323
  9. package/dist/approval-deny-reasons.js +0 -42
  10. package/dist/approval-hmac.js +0 -35
  11. package/dist/approval-reconciler.js +8 -166
  12. package/dist/approval.js +4 -67
  13. package/dist/audit.js +1 -44
  14. package/dist/auth-bridge.js +4 -47
  15. package/dist/auth-keys.js +0 -23
  16. package/dist/bake-runner/main.js +4 -65
  17. package/dist/bake-runner/protocol.js +3 -58
  18. package/dist/bake-runner/runner.js +5 -91
  19. package/dist/bench/l8/artifact.js +2 -61
  20. package/dist/bench/l8/escape.js +0 -25
  21. package/dist/bench/l8/index.js +0 -14
  22. package/dist/bench/l8/probes.js +0 -60
  23. package/dist/bench/l8/run-probes.js +2 -45
  24. package/dist/bench/s1/arms.js +14 -189
  25. package/dist/bench/s1/live-deps.js +11 -253
  26. package/dist/bench/s1/oracle.js +1 -36
  27. package/dist/bench/s1/repair-oracle-adapter.js +0 -30
  28. package/dist/bench/s1/reviewer.js +0 -34
  29. package/dist/bench/s1/row.js +0 -62
  30. package/dist/bench/s1/run-firm.js +4 -70
  31. package/dist/bench/s1/runner-ctx.js +0 -40
  32. package/dist/bench/s1/tasks.js +0 -115
  33. package/dist/boot/adoption.js +0 -20
  34. package/dist/boot/budget-tracing.js +3 -55
  35. package/dist/boot/config-center.js +43 -551
  36. package/dist/boot/coordinators.js +1 -69
  37. package/dist/boot/crash-last.js +0 -18
  38. package/dist/boot/deferred-sandbox-path-env.js +3 -119
  39. package/dist/boot/execution-env.js +5 -171
  40. package/dist/boot/governance-seams.js +0 -92
  41. package/dist/boot/leader.js +0 -69
  42. package/dist/boot/limit-sync.js +0 -2
  43. package/dist/boot/memory-boundary.js +3 -91
  44. package/dist/boot/org-memory.js +1 -25
  45. package/dist/boot/parked-revive-gate.js +0 -150
  46. package/dist/boot/permission-rules-audit.js +1 -56
  47. package/dist/boot/reapers.js +15 -273
  48. package/dist/boot/resolve-spec.js +9 -768
  49. package/dist/boot/retention-lane.js +4 -87
  50. package/dist/boot/runner-deps.js +3 -297
  51. package/dist/boot/runtime-caps.js +0 -40
  52. package/dist/boot/session-faces.js +2 -133
  53. package/dist/boot/shutdown.js +9 -83
  54. package/dist/boot/side-query-lane.js +2 -137
  55. package/dist/boot/stores.js +15 -215
  56. package/dist/boot/task-list-lane.js +0 -18
  57. package/dist/boot/webfetch-summarize-lane.js +0 -47
  58. package/dist/boot/workflow-orchestration.js +3 -99
  59. package/dist/boot-reclaim.js +0 -20
  60. package/dist/bounded-session-map.js +0 -19
  61. package/dist/brain.js +2 -139
  62. package/dist/budget.js +9 -244
  63. package/dist/capabilities/builtin-tools.js +0 -3
  64. package/dist/capabilities/center-plugins.js +6 -51
  65. package/dist/capabilities/center-prompts.js +6 -71
  66. package/dist/capabilities/code-review-council.js +7 -40
  67. package/dist/capabilities/collab-workflows.js +1 -44
  68. package/dist/capabilities/hands-lane.js +0 -65
  69. package/dist/capabilities/memory-notice.js +0 -70
  70. package/dist/capabilities/prompt.js +0 -14
  71. package/dist/capabilities/prompts/code-review.js +0 -14
  72. package/dist/capabilities/prompts/identity.js +0 -6
  73. package/dist/capabilities/prompts/team.js +0 -4
  74. package/dist/capabilities/repo-tools.js +0 -34
  75. package/dist/capabilities/sandbox-file-send.js +3 -92
  76. package/dist/capabilities/scenarios.d.ts +0 -1
  77. package/dist/capabilities/scenarios.js +18 -279
  78. package/dist/capabilities/select-environment-tool.js +0 -32
  79. package/dist/capabilities/send-user-file-tool.js +2 -66
  80. package/dist/capabilities/skills.d.ts +1 -2
  81. package/dist/capabilities/skills.js +3 -21
  82. package/dist/capabilities/team.d.ts +3 -11
  83. package/dist/capabilities/team.js +3 -51
  84. package/dist/capabilities/tool-defer.js +0 -3
  85. package/dist/config-center/apply-effective.js +15 -330
  86. package/dist/config-center/apply-ledger.js +5 -38
  87. package/dist/config-center/facade.js +0 -41
  88. package/dist/config-center/hot-keys-registry.js +0 -20
  89. package/dist/config-center/http-client.js +2 -125
  90. package/dist/config-center/mcp-revocation.js +2 -34
  91. package/dist/config-center/read-face.js +0 -59
  92. package/dist/config-center/restart-signal.js +1 -85
  93. package/dist/config-center/skills-mcp.d.ts +1 -1
  94. package/dist/config-center/skills-mcp.js +9 -76
  95. package/dist/config-center/stage-limits.js +8 -35
  96. package/dist/config-invariants.js +0 -16
  97. package/dist/config-lkg.js +0 -42
  98. package/dist/config-provider.js +3 -186
  99. package/dist/config-types.js +0 -5
  100. package/dist/config.js +107 -1145
  101. package/dist/degenerate-instrument.js +3 -67
  102. package/dist/deployment-governance.js +0 -124
  103. package/dist/digest-form.js +0 -12
  104. package/dist/elicitation.js +3 -86
  105. package/dist/env-facts.js +7 -75
  106. package/dist/fleet/fleet-bus.js +34 -507
  107. package/dist/fleet/fleet-reconciler.js +9 -149
  108. package/dist/fleet/fleet-terminal-window.js +10 -178
  109. package/dist/fleet/subagent-tail-bus.js +3 -72
  110. package/dist/fleet-client.js +10 -70
  111. package/dist/fleet-lease.js +5 -79
  112. package/dist/git-api-kind.js +0 -3
  113. package/dist/governance-ask-marks.js +2 -78
  114. package/dist/hooks/branch-transcript.js +0 -74
  115. package/dist/hooks/cc-agent-hook-prompt.js +0 -29
  116. package/dist/hooks/cc-stop-prompt.js +1 -46
  117. package/dist/hooks/hook-llm.js +1 -53
  118. package/dist/hooks/hook-runner.js +20 -414
  119. package/dist/http/active-run-conflict.js +4 -129
  120. package/dist/http/cursor-fingerprint.d.ts +5 -0
  121. package/dist/http/cursor-fingerprint.js +5 -0
  122. package/dist/http/idempotency.js +0 -37
  123. package/dist/http/principal-gate.js +3 -40
  124. package/dist/http/route-ctx.js +0 -9
  125. package/dist/http/routes/a2a-serve.js +7 -319
  126. package/dist/http/routes/admin-config-refresh.js +0 -4
  127. package/dist/http/routes/admin-drain.js +0 -10
  128. package/dist/http/routes/adoption.js +1 -29
  129. package/dist/http/routes/agents-roster.js +1 -47
  130. package/dist/http/routes/approvals-assistant.js +32 -395
  131. package/dist/http/routes/attachments.js +4 -22
  132. package/dist/http/routes/capabilities.js +3 -420
  133. package/dist/http/routes/diagnostics.js +1 -81
  134. package/dist/http/routes/fleet.js +9 -185
  135. package/dist/http/routes/images.js +14 -239
  136. package/dist/http/routes/leader.js +0 -13
  137. package/dist/http/routes/memory-bundle.js +1 -61
  138. package/dist/http/routes/memory-policy.js +9 -99
  139. package/dist/http/routes/notify-wake.js +3 -37
  140. package/dist/http/routes/observability.js +2 -19
  141. package/dist/http/routes/retention-ops.js +2 -34
  142. package/dist/http/routes/rules.js +1 -93
  143. package/dist/http/routes/runs.js +64 -873
  144. package/dist/http/routes/session-sync.js +19 -258
  145. package/dist/http/routes/sessions-list.js +17 -43
  146. package/dist/http/routes/sessions.js +34 -226
  147. package/dist/http/routes/shared-memory.js +5 -36
  148. package/dist/http/routes/side-query.js +1 -87
  149. package/dist/http/routes/tasks.js +52 -722
  150. package/dist/http/routes/trace-usage.js +32 -195
  151. package/dist/http/routes/workflows.js +19 -195
  152. package/dist/http/run-meta.js +0 -6
  153. package/dist/http/send.js +0 -32
  154. package/dist/http/server.js +92 -1597
  155. package/dist/http/sse-lifecycle.js +2 -13
  156. package/dist/http/sse-log.js +3 -48
  157. package/dist/http/tar.js +5 -21
  158. package/dist/http/verify-rounds.js +0 -5
  159. package/dist/http/wire-gate.js +0 -9
  160. package/dist/http/workspace-content.js +0 -10
  161. package/dist/images/bake-validate.js +1 -70
  162. package/dist/images/manifest.js +1 -6
  163. package/dist/index.js +0 -21
  164. package/dist/key-resolver.js +2 -17
  165. package/dist/leader/diffout.js +1 -20
  166. package/dist/leader/diffup.js +0 -47
  167. package/dist/leader/endpoint.js +2 -57
  168. package/dist/leader/fanout.js +3 -45
  169. package/dist/leader/grader-env-factory.js +3 -72
  170. package/dist/leader/leader.js +5 -156
  171. package/dist/leader/merge.js +8 -115
  172. package/dist/leader/planner.js +3 -54
  173. package/dist/leader/repair-oracle.js +1 -60
  174. package/dist/leader/repair-wire.js +2 -79
  175. package/dist/leader/wire.js +8 -307
  176. package/dist/lsp/e2b-bridge.js +4 -64
  177. package/dist/lsp/e2b-manager.js +6 -94
  178. package/dist/lsp/lsp-frames.js +0 -12
  179. package/dist/lsp/manager.js +4 -96
  180. package/dist/lsp/ws-transport.js +5 -55
  181. package/dist/lsp-evict.js +1 -15
  182. package/dist/main.js +57 -765
  183. package/dist/memory-bundle-engine.js +0 -55
  184. package/dist/memory-export.js +0 -4
  185. package/dist/memory-posture.js +1 -15
  186. package/dist/memory-scope.js +11 -148
  187. package/dist/memory-sync-client.js +2 -44
  188. package/dist/memory-sync.js +1 -80
  189. package/dist/model-select.js +3 -80
  190. package/dist/observability/cost-quota.js +1 -17
  191. package/dist/observability/cost-taxonomy.js +0 -34
  192. package/dist/observability/fail-open.js +7 -86
  193. package/dist/observability/logger.js +0 -6
  194. package/dist/observability/metrics.js +0 -94
  195. package/dist/observability/otel-exporter.js +3 -13
  196. package/dist/observability/principal-context.js +0 -9
  197. package/dist/observability/prompt-manifest.js +1 -37
  198. package/dist/observability/rate-limit.js +0 -4
  199. package/dist/observability/secret-env-scrub.js +2 -56
  200. package/dist/observability/tool-trace.js +1 -70
  201. package/dist/orchestration/hardened-vm-runner.js +4 -118
  202. package/dist/orchestration/hardened-vm-worker-runner.js +1 -26
  203. package/dist/orchestration/hardened-vm-worker.js +0 -27
  204. package/dist/orchestration/subagent-steer.js +1 -45
  205. package/dist/orchestration/workflow-agent-steer.js +1 -80
  206. package/dist/orchestration/workflow-completion-inbox.js +32 -285
  207. package/dist/orchestration/workflow-notify-journal.js +16 -259
  208. package/dist/org-memory-admission.js +3 -47
  209. package/dist/parent-watch.js +2 -48
  210. package/dist/parked-decide.js +1 -109
  211. package/dist/per-task-image.js +0 -57
  212. package/dist/plan-cache-probe.js +3 -27
  213. package/dist/plugins/adoption-log-sql.js +2 -119
  214. package/dist/plugins/approval-ask-store-memory.js +3 -38
  215. package/dist/plugins/approval-ask-store-sql.js +6 -188
  216. package/dist/plugins/approval-exemption-store.js +2 -28
  217. package/dist/plugins/background-agent-store-sql.js +4 -105
  218. package/dist/plugins/background-shell-support.js +14 -122
  219. package/dist/plugins/blob-backend.js +6 -169
  220. package/dist/plugins/breaker-state-sql.js +8 -46
  221. package/dist/plugins/caching-session-store.js +4 -106
  222. package/dist/plugins/checkpoint-store-sql.js +18 -505
  223. package/dist/plugins/e2b-orphan-reclaim.js +0 -45
  224. package/dist/plugins/file-outcome-sink.js +0 -9
  225. package/dist/plugins/file-resume-anchor-store.js +4 -43
  226. package/dist/plugins/file-run-store.js +26 -364
  227. package/dist/plugins/file-snapshot-store-sql.js +10 -181
  228. package/dist/plugins/fork-routing-session-store.js +8 -111
  229. package/dist/plugins/host-platform.js +2 -91
  230. package/dist/plugins/image-bake-store-sql.js +7 -250
  231. package/dist/plugins/image-index-sql.js +4 -123
  232. package/dist/plugins/k8s-bg-scripts.js +4 -88
  233. package/dist/plugins/k8s-exec-protocol.js +0 -41
  234. package/dist/plugins/leader-run-store-sql.js +0 -101
  235. package/dist/plugins/local-checkpoint-store.js +8 -128
  236. package/dist/plugins/local-session-store.js +34 -296
  237. package/dist/plugins/local-task-attachment-store.js +2 -16
  238. package/dist/plugins/mailbox-store-sql.js +8 -65
  239. package/dist/plugins/memory-embedder-fingerprint.js +5 -166
  240. package/dist/plugins/memory-embedder.js +4 -70
  241. package/dist/plugins/memory-engine-pg.js +6 -170
  242. package/dist/plugins/memory-engine-tidb.js +7 -155
  243. package/dist/plugins/memory-engine-vector-util.js +0 -10
  244. package/dist/plugins/memory-key-guards.js +0 -34
  245. package/dist/plugins/memory-origin-law.js +0 -187
  246. package/dist/plugins/memory-resume-anchor-store.js +0 -17
  247. package/dist/plugins/memory-run-store.js +12 -89
  248. package/dist/plugins/memory-session-policy-store.js +0 -17
  249. package/dist/plugins/memory-sync-store-pg.js +4 -49
  250. package/dist/plugins/memory-sync-store-tidb.js +3 -35
  251. package/dist/plugins/outcome-ledger-sql.js +3 -97
  252. package/dist/plugins/permission-rule-store-file.js +5 -133
  253. package/dist/plugins/permission-rule-store-sql.d.ts +3 -0
  254. package/dist/plugins/permission-rule-store-sql.js +50 -307
  255. package/dist/plugins/pg-cost-quota.js +0 -7
  256. package/dist/plugins/pg-pool.js +0 -92
  257. package/dist/plugins/pg-rate-limiter.js +2 -13
  258. package/dist/plugins/pg-safe-json.js +4 -40
  259. package/dist/plugins/pg-session-storage.js +25 -189
  260. package/dist/plugins/posix-shell-fs.js +1 -31
  261. package/dist/plugins/remote-env-adb.js +12 -101
  262. package/dist/plugins/remote-env-e2b.js +40 -371
  263. package/dist/plugins/remote-env-file-error.js +0 -37
  264. package/dist/plugins/remote-env-host.js +63 -483
  265. package/dist/plugins/remote-env-k8s.js +32 -326
  266. package/dist/plugins/remote-env-local-docker.js +24 -157
  267. package/dist/plugins/remote-env-ssh.js +20 -128
  268. package/dist/plugins/remote-scratchpad.js +2 -32
  269. package/dist/plugins/remote-shell.js +1 -32
  270. package/dist/plugins/resume-anchor-store-sql.js +0 -11
  271. package/dist/plugins/retention-lane-store-sql.js +0 -108
  272. package/dist/plugins/retention-store-sql.js +4 -383
  273. package/dist/plugins/roster-store-sql.js +0 -55
  274. package/dist/plugins/run-store-sql.js +14 -251
  275. package/dist/plugins/s3-presign.js +2 -49
  276. package/dist/plugins/scheduler-support.js +3 -80
  277. package/dist/plugins/send-file-ledger.js +4 -53
  278. package/dist/plugins/send-user-file.js +4 -94
  279. package/dist/plugins/session-placement.js +1 -89
  280. package/dist/plugins/session-policy-store-sql.js +3 -81
  281. package/dist/plugins/session-store.js +0 -57
  282. package/dist/plugins/shared-memory-store-sql.js +4 -167
  283. package/dist/plugins/sql-driver.js +0 -17
  284. package/dist/plugins/sql-errors.js +0 -7
  285. package/dist/plugins/sql-escape.js +0 -8
  286. package/dist/plugins/sql-row-helpers.js +0 -25
  287. package/dist/plugins/store-backend.js +42 -222
  288. package/dist/plugins/store-contracts.js +2 -46
  289. package/dist/plugins/task-attachment-store.js +3 -49
  290. package/dist/plugins/task-list-store-sql.js +0 -79
  291. package/dist/plugins/tidb-cost-quota.js +1 -4
  292. package/dist/plugins/tidb-pool.js +1 -207
  293. package/dist/plugins/tidb-rate-limiter.js +3 -9
  294. package/dist/plugins/tidb-session-storage.js +4 -70
  295. package/dist/plugins/tidb-session-store.js +19 -341
  296. package/dist/plugins/tool-result-store-sql.js +4 -179
  297. package/dist/plugins/usage-window-store-sql.js +0 -8
  298. package/dist/plugins/web-search.js +10 -132
  299. package/dist/plugins/workflow-journal-store-sql.js +2 -58
  300. package/dist/plugins/workflow-run-store-sql.js +6 -91
  301. package/dist/plugins/worktree-isolation.js +6 -126
  302. package/dist/plugins/write-behind-counter.js +16 -75
  303. package/dist/principal-jwt.js +5 -60
  304. package/dist/project-memory.js +15 -146
  305. package/dist/prompts-domain-validate.js +1 -51
  306. package/dist/question.js +2 -111
  307. package/dist/resource-suspend.js +0 -18
  308. package/dist/router/route-orchestration.js +0 -77
  309. package/dist/rules-consent.d.ts +44 -5
  310. package/dist/rules-consent.js +75 -215
  311. package/dist/run-local.js +14 -381
  312. package/dist/runs.js +29 -617
  313. package/dist/runtime-caps-resolver.js +6 -132
  314. package/dist/runtime-governance.js +1 -232
  315. package/dist/sandbox-pkg-source.js +0 -37
  316. package/dist/sealed-key.js +3 -68
  317. package/dist/security.js +6 -291
  318. package/dist/session-leaf-bus.js +0 -32
  319. package/dist/session-sync-content.js +1 -69
  320. package/dist/session-sync-kernel.js +3 -60
  321. package/dist/session-sync.js +3 -66
  322. package/dist/session-titler.js +7 -41
  323. package/dist/session-watch.js +9 -73
  324. package/dist/shared-memory-scope-authorizer.js +0 -18
  325. package/dist/sighup-idle.js +1 -12
  326. package/dist/spec-fields.js +4 -143
  327. package/dist/store-live-probe.js +3 -49
  328. package/dist/task-a2a.js +1 -126
  329. package/dist/task-cwd.js +1 -103
  330. package/dist/task-mcp.js +1 -89
  331. package/dist/task-settings.js +10 -300
  332. package/dist/task-workflow.js +8 -75
  333. package/dist/tool-approval.d.ts +28 -1
  334. package/dist/tool-approval.js +70 -1430
  335. package/dist/trace/artifacts.js +5 -20
  336. package/dist/trace/engine-notice-wire.js +6 -138
  337. package/dist/trace/ledger-sink.js +7 -96
  338. package/dist/trace/project.js +4 -448
  339. package/dist/trace/redact.js +9 -77
  340. package/dist/turn-activity.js +1 -27
  341. package/dist/usage-analytics.js +4 -34
  342. package/dist/wall-clock-jump-guard.js +1 -68
  343. package/package.json +3 -3
  344. package/dist/capabilities/scenario-alias.d.ts +0 -27
  345. package/dist/capabilities/scenario-alias.js +0 -61
@@ -1,22 +1,3 @@
1
- /**
2
- * design/158 A10:composition root 分段 —— `resolveSpec`(部署方拥有的 body→TaskSpec 映射)。
3
- *
4
- * 纯搬运:函数体逐字来自 `main.ts`(原 3347-4313 行),缩进不变;新增的只有本文件的 import 与
5
- * 下面这层 `createResolveSpec(ctx)` 包壳(捕获原先靠闭包拿到的 boot 局部量)。
6
- *
7
- * ⚠️ **位置即契约**:本函数在 boot 次序里**必须晚于** scenarios 装配(`scenarios` 已含 center overlay)
8
- * 且早于 `createHttpServer`;它读的两个量是 boot 后仍会变的活引用,故经函数取值而非值捕获:
9
- * - `getKeyResolver()` —— registry 热应用会整个换 `keyResolver` 引用(createHookLlm 同款姿势);
10
- * - `getCenterPrompts()` —— `adoptCenterPrompts` 每次采用都换 `centerPrompts` 引用。
11
- * 这两处是本次搬运里**仅有的**两行改写(值 → 取值调用),其余逐字。
12
- *
13
- * design/158 A10 留档件#2(阶段化):原先 967 行的匿名 `resolveSpec` 体按其**真实六阶段**提为具名阶段函数
14
- * (①场景/append 门 → ②settings·cwd·env → ③anchors → ④spec 字面量 → ⑤governance 折叠 → ⑥image·envFacts·router)。
15
- * 这是可读性刀不是行为刀:各阶段体**逐字搬运**(判别顺序/短路语义/传参不变),原先靠闭包拿的 boot 局部量仍由
16
- * 同一层闭包捕获(完全同一批),仅把**请求级**量(body/auth/opts + 前序阶段产出)改成显式形参、在阶段头解构。
17
- * 唯一的非搬运改动:一段游离注释(`[#40 / TOC cwd seam]`,原文夹在附件块之上却描述其**下面**的 cwd 门)随它
18
- * 描述的代码下移进阶段②。
19
- */
20
1
  import { realpathSync } from "node:fs";
21
2
  import { join } from "node:path";
22
3
  import { NodeExecutionEnv, QUESTION_AWAITS_RESUME, expandTiers, tightenTaskSpec } from "@sema-agent/core";
@@ -55,47 +36,14 @@ import { DeferredSandboxPathEnv, isSandboxPathAdjudicationLane, sandboxPathEnvSl
55
36
  import { effectiveMemoryPersistenceCapable } from "./memory-boundary.js";
56
37
  export function createResolveSpec(ctx) {
57
38
  const { config, logger, metrics, localRoot, scenarios, principalCaps, centerRuntimeCapsResolver, handsLanes, getCenterPrompts, getKeyResolver, taskAttachmentStore, perSessionCwd, setSessionCwd, setSessionShellEnv, hookLlm, hookAgent, fleetBus, hookWakeBus, resumeAnchorStore, ownerAware, taskLimitCaps, taskTimeoutSec, selectEnvTool, sendUserFileToolSpec, memoryEngine, durableEnabled, approvalExemptionStore, singleUserAutoAcceptBaseline, checkpointStore, deploymentHooks, imageIndex, perTaskImage, sessionEnvSelection, liveQuestionFace, lockedKeys, } = ctx;
58
- // ── #177 的两件 boot 期收口(把守卫集搬到 governance 拍之后才成立的两条)────────────────────────
59
- //
60
- // ① 守卫集的**编译**从此每个请求都发生(旧家只在 default/auto/acceptEdits 三个模式臂里发生)。
61
- // 非法旋钮值必须当场炸在启动上而不是每任务一条 500 —— 判据与文案随 design/181 件一搬进构造口
62
- // (`assertGuardPatternsUsable`),赎回腿/run-local 装配时调同一只,三腿同得。
63
- // CROSS-INVARIANT:sensitive-write-patterns —— 同一条判据的热路径半场在 config-invariants.ts 的表里
64
- // (#322:stage 期用同一只编译门判候选,禁手抄第二份判据)。
65
39
  assertGuardPatternsUsable(config);
66
- // ② governance 拍的 DENY 基线让 `spec.toolPolicy` 在**任何**客户端表态下都在场,于是 core 的
67
- // 「write-capable hand tools are present but UNGATED」那条 onError 不再触发(core 判据是
68
- // `policyLayers.length > 0`,prepare-task dist 亲读)。它此前是「部署一个门都没接」这个 misconfig
69
- // 的唯一信号,而守卫集只挡那二十来个路径段、其余写目标照旧无裁决 —— 信号不能因为本件静默消失。
70
- // 条件 = 两条产 toolPolicy 的腿都不在场(durable 门关 ∧ 单用户 auto-accept 基线不适用),即修前
71
- // `spec.toolPolicy` 恒缺席的那一形;三个量都是 boot 常量,所以这里说一次而不是每任务说一次。
72
- // 判据与文案随 design/181 件三搬进构造口(`buildOnlySensitiveBaselineWarning`),run-local 腿调同
73
- // 一只 —— 本处零行为变化(条件与字段逐字同前)。
74
40
  const onlySensitiveBaseline = buildOnlySensitiveBaselineWarning(config, { durableEnabled, singleUserAutoAcceptBaseline });
75
41
  if (onlySensitiveBaseline)
76
42
  logger.warn(onlySensitiveBaseline.event, onlySensitiveBaseline.fields);
77
- // Deployment-owned mapping. Tools/prompt/skills come from the selected scenario (assembled once,
78
- // bound per request); identity/session/policy stay server-side. Never taken from the request body.
79
- // design/158 A8:从 createHttpServer 实参里提出来(原地占该字面量 1,093 行中的 967 行)——
80
- // 类型标注 `ServiceDeps["resolveSpec"]` 顶替了原来的上下文推导,签名不变。
81
43
  const resolveSpec = async (body, _req, auth, opts) => {
82
- // design/170 件C §4.3 —— **锁的拒排在最前**(codex 对抗复审 F3,验真后采纳)。
83
- // 判据不是「在哪一阶段读到这个字段」而是「在**任何副作用**之前」:①阶段最先跑的
84
- // `gateScenarioAndAppend` 会把上传的附件行**持久绑定**到 session,②阶段会写 per-session 的 cwd/env 表
85
- // —— 一个最终 400 的请求若已经改掉了这两样,它就改变了**后续被接受的**任务会物化什么、在哪里执行。
86
- // core 对同一把锁的裁定原话是「Runs BEFORE any side effect of task preparation」,server 侧同姿势。
87
44
  assertRequestMcpUnlocked(body.mcpServers, lockedKeys);
88
- // DESIGN-269 车1 件3:A2A 的同一条判据、同一个位置(同一批副作用要防)。锁键 `a2a` 在 core 5.36 的
89
- // `LockedKey` 里尚不存在(分单件 C-1),所以这道门今天恒不触发 —— 它先在场是纵深:词表到货那一拍
90
- // 它自己就活,而不是等着谁记起来补(见 task-a2a.ts 模块头的成文理由)。
91
45
  assertRequestA2aUnlocked(body.a2aPeers, lockedKeys);
92
- // A10 留档件#2:六阶段流水线。顺序=原文顺序(每一阶段的体逐字搬运),阶段之间只传显式产出;
93
- // 六个阶段函数定义在本 `return` 之后(自上而下先读骨架、再读各阶段)。
94
46
  const gated = await gateScenarioAndAppend(body, auth, opts);
95
- // design/201 §6-1:本请求的**生效** permission mode —— 一次算,三处消费(hooks 载荷的
96
- // `permission_mode`、阶段④的 shellGate 翻译写点、阶段⑤的 settings 折叠)。此前前两者各算一次,
97
- // 翻译表要是再开第三个算点,「顶层键 vs bundle defaultMode 谁赢」这条优先序就有三份可以各自漂移的
98
- // 副本,而漂移在这条轴上的形态是「壳发的 bundle bypass 在这一面生效、在那一面没生效」——三审同点。
99
47
  const effMode = effectivePermissionMode(body, gated.parsedSettings.settings);
100
48
  const lane = bindSettingsCwdEnvAndModel(body, auth, gated, effMode, opts);
101
49
  const anchors = await resolveHistoryAnchors(body, auth);
@@ -106,78 +54,21 @@ export function createResolveSpec(ctx) {
106
54
  scratchpadDir: folded.scratchpadDir,
107
55
  hostSemanticsLane: folded.hostSemanticsLane,
108
56
  });
109
- // #196:登记本请求的 hands lane。**必须是最终对象**(阶段⑤/⑥ 会换新对象 —— 登记中间态 = 执行点
110
- // 查不到、静默落回 full)。resume 腿也走同一条 resolveSpec,故续跑与首跑选同一只 Runner。
111
57
  handsLanes.stamp(final, gated.hands);
112
58
  return final;
113
59
  };
114
- /** 阶段①(场景/append 门):吃 请求体 + auth + `opts.leg`,吐 本请求的场景绑定(scenarioName/cap)、center-pack
115
- * 的**单次**快照与 append 门结论(centerDecls/appendLessPack/acceptedAppend)、objective、解析后的 settings 戳,
116
- * 以及 D-1 附件绑定产出的 attachmentNotice;沿途的准入拒绝(scenario 400 / lane 403 / append 400 / 附件 4xx·501)
117
- * 与 warn 原样在此发生。 */
118
60
  const gateScenarioAndAppend = async (body, auth, opts) => {
119
- // per-principal scenario governance (assignment-as-default; an explicit
120
- // body.scenario is bounded to the allowlist — outside ⇒ typed 400 `scenario_not_allowed`). The ruling
121
- // {scenario, allowlist} is center-RESOLVED and rides the caps view (TTL-cached, same fetch as runtimeCaps);
122
- // no center / dry-run / anonymous / older center / caps blip ⇒ undefined ⇒ legacy body-or-default chain.
123
- // #273 alias:`gateScenarioRequest` 是 HTTP lane 的唯一归一点(旧名 team → discuss,响亮 warn);
124
- // 此后 `scenarioName` 只可能是现役主名,下游判据点一律按主名写(禁双名判)。
125
61
  const requested = gateScenarioRequest(await principalCaps?.scenarioRuling(auth?.principal), body.scenario, config.defaultScenario, Object.keys(scenarios), logger);
126
- // per-principal execution-lane policy — this worker's lane is a
127
- // PROCESS identity (REMOTE_EXEC boot wiring, no per-task switch), so admission is the gate: lane ∉
128
- // allowedLanes ⇒ typed 403 `execution_lane_not_allowed`. Same caps car as the scenario ruling (zero extra
129
- // RTT); fail-open + audit on blip. "in-process" = fleet's REMOTE_EXEC-unset core stub lane
130
- // (named distinctly from the explicit `host` lane — same machine, different posture).
131
- // #157 补痕交叉复审真发现(codex,2026-08-05):第三参不传就落 runtime-caps-resolver.ts 自己的私有
132
- // `defaultLogger`(createLogger() 默认 minLevel="info")——与本部署真正按 config.logLevel 建的 `logger`
133
- // 是两个独立实例,即便运维把 LOG_LEVEL 调到 debug 也打不到这条新增的一次性默认落痕行(补痕功能形同虚设)。
134
- // 显式传本装配的真 logger,让该痕迹真的受部署日志级别管辖。
135
62
  gateExecutionLane(await principalCaps?.executionRuling(auth?.principal), config.remoteExec?.provider ?? "in-process", logger);
136
- // Resolve an unknown name to "default" so the WHOLE bundle stays coherent — tools/prompt AND
137
- // skills/mcp fall back together. (selectScenario already falls back for tools/prompt; without
138
- // resolving here, skills/mcp would still be filtered by the raw unknown name → any skill/MCP tagged
139
- // ["default"] silently drops. council finding.)
140
63
  const scenarioName = scenarios[requested] ? requested : "default";
141
- // [891]→[2400]:"autonomous" alias 已退役(clay 裁不留过渡)——body 显式传它走 scenario_unknown 400。
142
- // 定死 finalVerification:true 保存量行为)。warn 不 400——TB harness/存量部署平滑迁移。
143
- // #318 第三参 = {@link ScenarioContext}(服务端事实,与不可信的 `body` 分开的通道)。`sessionId`
144
- // 只能来自 `auth`(authorizer 解析 + 归属校验过的那一只)—— 与下游 assembleSpecLiteral 里
145
- // 「sessionId comes from `auth`, NEVER the body」逐字同纪律:body.sessionId 是调用方自报串,
146
- // 拿它当会话级设施(任务清单)的分区键就是让一个租户读另一个租户的清单。缺席(无 authorizer
147
- // 的裸部署)⇒ 整键省略 ⇒ 场景侧退回 per-call 私店,不发明假 session。
148
64
  const cap = selectScenario(scenarios, scenarioName)(body, auth?.principal, auth?.sessionId !== undefined ? { sessionId: auth.sessionId } : {});
149
- // codex R3/R4 (appendSystemPrompt 案): ONE center-pack snapshot per request — the append-less gate below
150
- // AND the provider pick (promptProvider field) both read THIS capture, never the mutable centerPrompts ref
151
- // again (R4 TOCTOU: a refresh between the two reads could make the final provider assembled while the gate
152
- // judged safe, restoring the in-core silent drop).
153
- const centerDecls = getCenterPrompts()?.declarations; // A10 搬运改写①:活引用取值(原 `centerPrompts?.declarations`)
154
- // TRUE when the EFFECTIVE assembly for this request takes core's pass-through arm (no core/role.append
155
- // section ⇒ any append rider/outputStyle silently discarded in core). codex R9: PROVIDER-AWARE — mirrors
156
- // core's actual triggers (stableBlocks identity decls / stableSystem OUTPUT carrying the anchors), because
157
- // a raw body.systemPrompt anchor heuristic false-positives on default-provider scenarios (unbranded
158
- // default / scan: no provider ⇒ core defaultPromptProvider retains role.append and the rider mounts even
159
- // with an anchored systemPrompt). center-wins mirrors the provider pick below (same snapshot, same
160
- // scenario derivation — principal ruling + defaultScenario already folded into scenarioName above).
65
+ const centerDecls = getCenterPrompts()?.declarations;
161
66
  const appendLessPack = centerDecls
162
67
  ? centerIdentityAssembled(centerDecls, scenarioName, hasConstitutionAnchors)
163
68
  : providerDropsAppend(cap.promptProvider, typeof body.systemPrompt === "string" ? body.systemPrompt : undefined);
164
- // Accepted rider (defensive resume mirror inside; fresh malformed/over-cap shapes already 400'd in
165
- // prepareSpec). Hoisted to a const so the R14 combined-cap check below can see the accepted length.
166
69
  const acceptedAppend = acceptAppendSystemPrompt(body.appendSystemPrompt, (detail) => logger.warn("append_system_prompt_dropped", { detail, sessionId: auth?.sessionId ?? null }), appendLessPack);
167
- // @-model (1.24) + explicit picker: an end user may pick a CONFIGURED model via the CLI
168
- // `/model` picker / `run --model` flag (body.model) OR an inline `@name` in the objective. resolveTaskModel
169
- // gates BOTH against the catalog allow-list (config.models keys) — pick WHICH model, never inject one — with
170
- // body.model winning over @mention winning over `default`. Returns the objective with the @mention stripped.
171
70
  const objective = typeof body.objective === "string" ? body.objective : "";
172
- // TaskRequest.settings (client per-request SemaSettings stamp). Parsed DEFENSIVELY off the untrusted body
173
- // (every field shape-checked; `bypassPermissions`/unknown modes dropped). `env`/`hooks` are recorded as
174
- // received-but-deferred (never silent-dropped). Folded into the spec TIGHTEN-ONLY after applyRuntimeGovernance.
175
71
  const parsedSettings = parseTaskSettings(body.settings);
176
- // codex R4 (appendSystemPrompt 案) fresh-leg fail-loud: a NEW submit whose append payload (top-level rider
177
- // or settings.outputStyle) meets an append-less pack 400s HERE — inside resolveSpec, where the effective
178
- // scenario (principal ruling + defaultScenario) and the center-pack snapshot are the SAME ones the provider
179
- // pick uses (an HTTP-side gate could disagree with the actual assembly). Resume legs (opts.leg absent)
180
- // keep the drop+warn mirror below — a 4xx would brick a pre-1.243 stored task.
181
72
  if (opts?.leg === "fresh" && appendLessPack) {
182
73
  const riderPresent = typeof body.appendSystemPrompt === "string" && body.appendSystemPrompt.length > 0;
183
74
  if (riderPresent || parsedSettings.settings?.outputStyle) {
@@ -185,15 +76,8 @@ export function createResolveSpec(ctx) {
185
76
  }
186
77
  }
187
78
  if (parsedSettings.deferred.length > 0) {
188
- // No-silent-drop: a caller SENT env/hooks but this version does not wire them on remote (need a core seam /
189
- // a remote hook-runner — see task-settings.ts + capabilities). Log so the gap is visible, never silent.
190
79
  logger.warn("task_settings_deferred", { fields: parsedSettings.deferred, sessionId: auth?.sessionId ?? null });
191
80
  }
192
- // ── D-1 附件绑定半场(②):TaskRequest.attachments = 上传句柄数组 ──
193
- // fresh 腿硬验证(未知/他人 id → 400 typed;超数 → 400)——「引用了不存在的附件」必须在提交时
194
- // 拒,不能等 env 建立时才炸。resume 腿降级(acceptedAppend 同姿):缺行 warn+跳过,余下照常
195
- // (4xx 会砖存量 task;记录不可变 ⇒ 在场行的 notice 字节恒稳)。绑定=把行钉到 auth.sessionId
196
- // (authorizer 恒解析出一个;绑定幂等,重复提交/resume 重绑同值)。
197
81
  let attachmentNotice;
198
82
  {
199
83
  const reqAtt = body.attachmentIds;
@@ -228,22 +112,8 @@ export function createResolveSpec(ctx) {
228
112
  }
229
113
  return { scenarioName, cap, hands: cap.hands, centerDecls, appendLessPack, acceptedAppend, objective, parsedSettings, attachmentNotice };
230
114
  };
231
- /** 阶段②(settings·cwd·env):吃 请求体 + auth + 阶段①的 objective/parsedSettings,吐 taskHooks、
232
- * additionalDirectories、档位展开后的 wireCatalog 与选中的 picked;副作用=per-session cwd/shellEnv 注册与
233
- * honored/ignored 日志、未知模型回落与无视觉模型降级的计数/告警。 */
234
115
  const bindSettingsCwdEnvAndModel = (body, auth, gated, effMode, opts) => {
235
116
  const { objective, parsedSettings } = gated;
236
- // [#40 / TOC cwd seam] register the caller's launch dir so the HOST factory runs the agent there (read by
237
- // ctx.sessionId). 🔒 GATED: only the single-user host lane (cwdHonored) honors it; on any other lane / multi-tenant
238
- // a sent cwd is IGNORED (logged once, never silently — the shell learns via capabilities.projectContext). isValidCwd
239
- // re-checked here too (RESUME re-runs resolveSpec WITHOUT the prepareSpec 400 validation). Re-applies on resume.
240
- // [851]P3a 信号真实化:cwdHonored 只认显式 REMOTE_EXEC=host;provider 未设的 in-process 单用户 lane 没有
241
- // shell hands(core prepare-task:无 factory/executionEnv ⇒ StubExecutionEnv,亲读 dist 坐实),但进程级
242
- // 消费者(hooks 命令、C4 projectContext 本地读取)仍看引擎自身的 cwd/env——壳把引擎 spawn 在用户目录/用户
243
- // 环境时,请求的 override 往往「已被进程状态满足」,此前每任务打 "ignored" warn 与实际状态矛盾(飞轮双平台
244
- // 观测)。改法:已满足 ⇒ debug;真失配 ⇒ 保留 warn 且带 lane 字段(值与 run_local_start exec 口径一致:
245
- // provider ?? "in-process")。真 per-session honor 之路仍是 REMOTE_EXEC=host(host factory 消费
246
- // perSessionCwd/perSessionShellEnv)。
247
117
  const execLane = config.remoteExec?.provider ?? "in-process";
248
118
  if (typeof body.cwd === "string" && body.cwd.length > 0 && auth?.sessionId) {
249
119
  if (cwdHonored(config) && isValidCwd(body.cwd))
@@ -253,14 +123,10 @@ export function createResolveSpec(ctx) {
253
123
  else
254
124
  logger.warn("task_cwd_ignored", { honored: cwdHonored(config), lane: execLane, sessionId: auth.sessionId });
255
125
  }
256
- // [R-survey / TOC shellEnv seam] register the caller's `settings.env` so the HOST factory injects it into the
257
- // agent's shell env. 🔒 GATED identically to cwd (single-user host lane only — design/107 env=capability axis);
258
- // on any cloud/multi-tenant lane a sent env is IGNORED (logged once, never silent). Re-applies on resume.
259
126
  if (parsedSettings.settings?.shellEnv && auth?.sessionId) {
260
127
  if (cwdHonored(config))
261
128
  setSessionShellEnv(auth.sessionId, parsedSettings.settings.shellEnv);
262
129
  else if (inProcessSingleUserLane(config)) {
263
- // 只记失配键「数量」,绝不记键值(秘密类)与键名(名字本身可带敏感语义)——shellEnvMismatchCount 文档同款红线。
264
130
  const requested = parsedSettings.settings.shellEnv;
265
131
  const mismatched = shellEnvMismatchCount(requested, process.env);
266
132
  if (mismatched === 0)
@@ -271,11 +137,6 @@ export function createResolveSpec(ctx) {
271
137
  else
272
138
  logger.warn("task_shell_env_ignored", { honored: false, lane: execLane, sessionId: auth.sessionId });
273
139
  }
274
- // hooks hook-runner(阶段一):settings.hooks(契约校验过)→ core TaskSpec.hooks 进程内回调。
275
- // 🔒 hook 命令跑在 WORKER HOST(CC 语义:hooks 跑在引擎所在机,哪怕工具在远端沙箱)= design/107 class ②
276
- // 能力授予 → 单用户闸 `requirePrincipal !== true`,与 scheduler/backgroundShell/lspHost/MCP 注入同一姿势;
277
- // 多租户 lane 收到只警告忽略(`task_hooks_ignored`,capabilities.taskSettings.hooks 同步 advertise),绝不执行。
278
- // Re-applies on resume(从 stored body 重解析,幂等)。
279
140
  const taskHooks = (() => {
280
141
  const hc = parsedSettings.settings?.hooks;
281
142
  if (!hc)
@@ -284,86 +145,41 @@ export function createResolveSpec(ctx) {
284
145
  logger.warn("task_hooks_ignored", { honored: false, sessionId: auth?.sessionId ?? null });
285
146
  return undefined;
286
147
  }
287
- // payload 的 permission_mode 用【生效】模式——body.permissionMode(显式请求)优先于
288
- // settings.permissions.defaultMode(与 spec 的 withPermissionMode 同一优先序),否则钩子读到的模式与
289
- // 实际裁决模式不一致(如 body=plan 时钩子仍看到 default)。design/201 起该优先序由
290
- // `effectivePermissionMode` 单点裁定,本处只消费(形参 `effMode`),不再自算一份。
291
148
  return createTaskHooks(hc, {
292
149
  logger,
293
150
  sessionId: auth?.sessionId ?? "",
294
151
  cwd: (auth?.sessionId ? perSessionCwd.get(auth.sessionId) : undefined) ?? process.cwd(),
295
152
  ...(effMode ? { permissionMode: effMode } : {}),
296
- // 用户变量通道(hook 进程 env + http $NAME 插值源):同请求的 settings.env——hookEnv 绝不读
297
- // 裸 process.env 的非 allowlist 键(服务密钥不可被 allowedEnvVars 点名)。
298
153
  ...(parsedSettings.settings?.shellEnv ? { shellEnv: parsedSettings.settings.shellEnv } : {}),
299
- // 阶段三b:prompt/agent 条目的模型载体(boot 组装,见 hookLlm/hookAgent 定义处的影响范围说明)。
300
154
  hookLlm,
301
155
  hookAgent,
302
- // cli [1786] 点名的观测位:一次 hook 判定**未能完成**时,往常开的 fleet 流推一帧。
303
- // ⚠️ **纯 observe**(不拦、不续跑、不注入)——`additionalContext` 那条通道会让这一轮不结束,
304
- // 所以不能拿它当"提示一下"用(见 hook-runner 折叠处旁注)。
305
- // ⚠️ 带 ownerScope/ownerSessionId 供流侧做 **fail-CLOSED** 过滤(与 bg_notification 同一姿势):
306
- // 一条关于别人会话的通知落到这个壳上,会让用户以为是自己这轮出了问题。
307
156
  onHookNotice: (n) => fleetBus.publishHookNotice({
308
157
  ...n,
309
158
  ...(auth?.principal ? { ownerScope: auth.principal } : {}),
310
159
  ...(auth?.sessionId ? { ownerSessionId: auth.sessionId } : {}),
311
160
  }),
312
- // asyncRewake wake 管道(契约:exit 2 唤醒模型):session→本副本活流 steer(hookWakeBus,server
313
- // 启动装 deliver)。stderr 过 redactSecrets 再注入(hook 进程可能回显 env 密钥;与 trace 出口同纪律)。
314
- // 无 session(匿名一次性任务)不给 wake——没有可稳定寻址的流。
315
161
  ...(auth?.sessionId
316
162
  ? {
317
163
  wake: async (text) => hookWakeBus.deliver
318
- ? // provenance 围栏:注入内容标明来源=asyncRewake hook 的运行时输出,
319
- // 模型按 hook 反馈对待而非操作者指令(trusted:false 在 server 侧同批落定)。
164
+ ?
320
165
  hookWakeBus.deliver(auth.sessionId, `[hook asyncRewake] ${redactSecrets(text)}`)
321
166
  : false,
322
167
  }
323
168
  : {}),
324
169
  });
325
170
  })();
326
- // design/119 (CC --add-dir parity, core 1.215 `TaskSpec.additionalDirectories`): extra host dirs the FILE tools
327
- // may access beyond the containment root. 🔒 GATED identically to cwd/shellEnv (single-user host lane only —
328
- // these are the caller's own-machine paths; a cloud/multi-tenant lane must not let a caller widen the file-tool
329
- // containment onto operator/other-tenant host paths). Unlike cwd/shellEnv (registered per-session for the host
330
- // FACTORY to read at env construction), additionalDirectories is a plain TaskSpec field core reads directly, so
331
- // it rides onto the spec below. Ignored (logged, never silent) off the host lane. Re-applies on resume (from body).
332
- // 键在 `TaskRequestBody` 上显式声明(#205 件1)⇒ 直读,不再裸铸([2704] 边界必 schema)。
333
- // 运行期校验保留:resume 腿重放的是**持久化 body**,不过 HTTP 400 门,类型说得再准也管不住盘上的字节。
334
171
  const rawAddDirs = body.additionalDirectories;
335
172
  const additionalDirectories = cwdHonored(config) ? parseAdditionalDirectories(rawAddDirs) : undefined;
336
173
  if (rawAddDirs !== undefined && !cwdHonored(config)) {
337
174
  logger.warn("task_additional_directories_ignored", { honored: false, sessionId: auth?.sessionId ?? null });
338
175
  }
339
- // core 5.11.0([2625] Added①,[2597] P0 案三决③A):`additionalReadDirectories` —— 只读围栏拓宽。
340
- // 与 additionalDirectories 同门(host 单用户车道)、同形校验(parseAdditionalDirectories:绝对路径
341
- // 数组 fail-loud)、同「忽略必留声」纪律;差异只在语义:只进 READ 面(classify 读边界 auto-allow +
342
- // read_file/grep 等 containment),写面照旧拒——读授权永不静默变写授权(core 侧保证,我们只透传)。
343
- const rawAddReadDirs = body.additionalReadDirectories; // 同上:直读(#205 件1)
176
+ const rawAddReadDirs = body.additionalReadDirectories;
344
177
  const additionalReadDirectories = cwdHonored(config) ? parseAdditionalDirectories(rawAddReadDirs) : undefined;
345
178
  if (rawAddReadDirs !== undefined && !cwdHonored(config)) {
346
179
  logger.warn("task_additional_read_directories_ignored", { honored: false, sessionId: auth?.sessionId ?? null });
347
180
  }
348
- // body.model (explicit /model picker or run --model) WINS over settings.model (the config default); both gate
349
- // against the catalog allow-list via resolveTaskModel (a settings.model not in the catalog is ignored, never injected).
350
- // 🔴 TOB 档位联跑曾 FAIL(坐标级):the gate must see the TIER-EXPANDED catalog — expandTiers
351
- // runs inside core's Runner on a PRIVATE deps copy (runtask.ts), so gating on bare config.models silently
352
- // degraded every tier word (pro/flash/@mention/CC alias) to default BEFORE the Runner could resolve it.
353
- // Expand per request (cheap: empty tiers returns the same reference; non-empty = one small spread) so the
354
- // gate's key set is exactly what a freshly-constructed Runner resolves — tier words, degrade chains, CC
355
- // aliases, catalog-SHADOW all from core's ONE implementation. Window note: tiers are restart-to-apply at
356
- // the Runner; a refresh-time tier change makes this gate briefly AHEAD of the old Runner snapshot (a new
357
- // tier word then fails loud in core instead of silently degrading — honest during the restart window).
358
181
  const wireCatalog = expandTiers(config.models, config.tiers) ?? config.models;
359
- // #233 / A-002.8:`atModelAllowlist` 与门目录同源(增广视图)——名单按 catalog name 写,而用户可以
360
- // 用档位词/别名/id 指同一只模型,归一在 isModelAllowlisted 里做(model-select.ts 顶注)。
361
182
  const picked = resolveTaskModel(body.model ?? parsedSettings.settings?.model, objective, wireCatalog, config.atModelAllowlist);
362
- // #233 拒形分流(clay 终裁 (a)):**fresh 提交** 400 `request.model_not_allowed`(治理拒——模型在目录
363
- // 里但名单未含,与"目录里没有"是两回事,故与 request.unknown_reference 分码);**resume 续跑照旧**
364
- // ——名单管"用户可点什么",resume 不是一次新选择;而本文件的 appendSystemPrompt 与 compactionModel
365
- // 两处 fresh-vs-resume 分流注、加 model-select.ts 的 [865]② 头注,三处都已成文"砖死 resume 比降级
366
- // 糟"。留声(warn+计数)让治理面看得见还有多少存量会话跑在名单外。
367
183
  if (picked.notAllowed !== undefined) {
368
184
  const { requested, resolved, source } = picked.notAllowed;
369
185
  if (opts?.leg === "fresh") {
@@ -372,65 +188,27 @@ export function createResolveSpec(ctx) {
372
188
  metrics.inc("task_model_not_allowlisted_resume_total");
373
189
  logger.warn("task_model_not_allowlisted_resume", { requested: requested.slice(0, 120), resolved, source, sessionId: auth?.sessionId ?? null, note: "resumed leg keeps running the model it was started on — the allowlist gates fresh picks only" });
374
190
  }
375
- // [865]② 降级永远可见:fresh submit 的未知 body.model 已在 HTTP 门 400(不会到这);走到这的未知 ref
376
- // 只剩 RESUME 重放(模型事后被移出目录)与 settings.model(lenient 文档面)——落 default 可跑,但必须
377
- // 有声(session 中途换模型是最恶性的上下文污染路径,静默=病灶本体)。
378
191
  if (picked.unknownExplicit !== undefined) {
379
192
  metrics.inc("task_model_unknown_fallback_total");
380
193
  logger.warn("task_model_unknown_fallback", { requested: picked.unknownExplicit.slice(0, 120), fallback: picked.model, sessionId: auth?.sessionId ?? null });
381
194
  }
382
- // no-vision images = PLACEHOLDER, not 422(会话可继续的 CC 心智). The old
383
- // fail-loud 422 here BLOCKED core's own degradation: since core 1.233.1 a serving model whose `input`
384
- // has no "image" gets image parts degraded to a bounded text placeholder ("[image omitted: …]"), per
385
- // CURRENT model (degrade chains stay correct) and re-applied on every history replay — so a previously
386
- // poisoned session self-heals. We pin ≥1.240.1, so pass the images through and let core do exactly that.
387
- // The GET /v1/models vision flag stays advertised (the shell's composer-warning half). A model
388
- // that never declared `input` keeps sending images verbatim (deliberate default — never wrongly strip
389
- // for a vision-capable deployment that skipped the metadata; same posture).
390
- // S6 (SILENT-FALLBACK P0-c): images headed for an EXPLICITLY text-only model — core (>=1.233.1) degrades
391
- // them to bounded text placeholders per current model (the pass-through posture above). That modality drop
392
- // was invisible fleet-side; count + warn at submit. modelSupportsImages is false-safe (undeclared input →
393
- // true → no signal), matching core's "never wrongly strip" posture — no false positives for verbatim
394
- // pass-through models. ⚠️ config-center lane caveat (review LOW-2): the roster maps `vision?: boolean` to
395
- // an EXPLICIT input list (config-center/facade.ts) — a vision-capable roster model that FORGOT the flag becomes
396
- // declared text-only, so this signal fires AND core really does degrade its images (signal matches
397
- // behavior; the fix for that footgun is the roster entry, not this counter). The per-run durable event rides core's vision.placeholder trace kind once we
398
- // consume >=1.245 (the authoritative replacement-time signal), not this submit-time predictor.
399
195
  if (Array.isArray(body.images) && body.images.length > 0 && !modelSupportsImages(wireCatalog[picked.model])) {
400
196
  metrics.inc("images_omitted_total", { model: picked.model });
401
197
  logger.warn("images_omitted_no_vision_model", { model: picked.model, count: body.images.length });
402
198
  }
403
199
  return { taskHooks, additionalDirectories, additionalReadDirectories, wireCatalog, picked };
404
200
  };
405
- /** 阶段③(anchors):吃 请求体 + auth,吐 两个历史锚解析后的 entryId(resumeAt / rewindFilesTo —— eventId→entryId
406
- * 过 anchor store,未知锚在此 4xx 而不是交给 core)与 142-S4 项目登记簿种子 s4DefaultScopes。 */
407
201
  const resolveHistoryAnchors = async (body, auth) => {
408
- // E18 resume-at: the shell sends body.resumeAt as the E2 message eventId; core's TaskSpec.resumeAt takes the
409
- // PERSISTED SessionTreeEntry.id, NOT the eventId ("a deployment that holds eventIds owns the eventId→entryId
410
- // map" — core never persists eventIds). Resolve eventId→entryId via the anchor store HERE, 4xx-ing BEFORE the
411
- // run on an UNKNOWN anchor (never hand core an unresolvable id). A KNOWN anchor whose entry was later compacted
412
- // away surfaces as a failed run RESULT (resume_at.not_found) — 4xx on the sync path, the run result on async (the
413
- // async contract is poll-the-result). RESUME re-runs resolveSpec from the stored body — the resume paths strip
414
- // resumeAt before re-resolving (else core would reject resumeAt + a durable resume), so this branch only fires on
415
- // a fresh submit. NEVER silent-drop (mirrors the body.model silent-drop bug class).
416
202
  let resumeAtEntryId;
417
203
  if (typeof body.resumeAt === "string" && body.resumeAt.length > 0) {
418
204
  if (!auth?.sessionId)
419
205
  throw new HttpError(422, "resumeAt requires a session to branch (resume_at.no_session)");
420
- // 501 gated on the SAME pair the `resumeAt` capability advertises (anchor store AND getLeafId) — capture is a
421
- // no-op without getLeafId, so a getLeafId-less backend has a permanently-empty map; report "not available"
422
- // (honest 501) rather than a misleading 404. Keeps "capability says yes ⟺ the route resolves" by construction.
423
206
  if (!resumeAnchorStore || !ownerAware.getLeafId)
424
207
  throw new HttpError(501, "resume-at is not available on this worker (no session-store backend for the anchor map)");
425
208
  resumeAtEntryId = await resumeAnchorStore.resolve(auth.sessionId, body.resumeAt, auth.principal ?? null);
426
209
  if (resumeAtEntryId === undefined)
427
210
  throw new HttpError(404, "resumeAt: no such message in this session (resume_at.unknown_event)");
428
211
  }
429
- // R8 code-only rewind (CC "Restore code" mode, core 1.166 `rewindFilesTo`): restore the working tree to a prior
430
- // USER message's snapshot WITHOUT forking the conversation. Resolve the SAME handle (the prompt's taskId) → entryId
431
- // via the SAME anchor store. Honored ONLY when resumeAt is ABSENT (core's contract: the two are mutually exclusive —
432
- // with resumeAt set, that branch's own rewindFiles governs); so we only resolve it on the code-only path. Same
433
- // fail-loud discipline as resumeAt (501 no-store / 404 unknown anchor — never silent-drop).
434
212
  let rewindFilesToEntryId;
435
213
  if (resumeAtEntryId === undefined && typeof body.rewindFilesTo === "string" && body.rewindFilesTo.length > 0) {
436
214
  if (!auth?.sessionId)
@@ -441,241 +219,59 @@ export function createResolveSpec(ctx) {
441
219
  if (rewindFilesToEntryId === undefined)
442
220
  throw new HttpError(404, "rewindFilesTo: no such message in this session (rewind_files_to.unknown_event)");
443
221
  }
444
- // 142-S4 defaultScopes 种子:projectId → center 项目登记簿(config.projects,registry 喂)→ 该项目
445
- // 声明的默认记忆 scope 词表(memory.scopes 额外 READ 层;写路由仍钉在派生 scope — memorySpecForRequest)。
446
- // projectId 优先取 auth.resolvedProjectId(authorizer 的 PROJECT_ID_REGEX 422 门通过后回传);RESUME 腿
447
- // 的 auth 从 checkpoint 重建、无此字段 → 回落持久化 body.projectId(原始提交已过形状门,与 memoryWrite
448
- // 的 rides-the-persisted-body 姿势一致),让种子在 resume 腿等价重放。查表 miss(未登记/已下架)= 无种子,
449
- // 不 fail(登记簿是加性配置面,不是准入门)。
450
- // A-033.1(#256):回落腿同样折小写 —— authorizer 那一格自 core 的 `/i` 词法起收下大写形并归一,
451
- // 若这条回落腿原样传,同一个 projectId 在首提交与 resume 两腿会查两个不同的 `config.projects` 键。
452
222
  const s4ProjectId = auth?.resolvedProjectId ?? (typeof body.projectId === "string" && body.projectId ? body.projectId.toLowerCase() : undefined);
453
223
  const s4DefaultScopes = s4ProjectId ? config.projects[s4ProjectId]?.defaultScopes : undefined;
454
224
  return { resumeAtEntryId, rewindFilesToEntryId, s4DefaultScopes };
455
225
  };
456
- /** 阶段④(spec 字面量):吃 请求体 + auth + `opts.leg` + 前三阶段的全部产出,吐**尚未过治理层**的 TaskSpec
457
- * 字面量 —— 部署方拥有的 body→TaskSpec 映射本体(compactionModel 的 fresh 腿 400 也在这一段)。 */
458
226
  const assembleSpecLiteral = (body, auth, opts, parts, effMode) => {
459
227
  const { scenarioName, cap, hands, centerDecls, acceptedAppend, parsedSettings, attachmentNotice, taskHooks, additionalDirectories, additionalReadDirectories, wireCatalog, picked, resumeAtEntryId, rewindFilesToEntryId, s4DefaultScopes, } = parts;
460
228
  const spec = {
461
- // D-1:附件告知随 objective 进 durable 流(只有名字/mime/尺寸——**内容永不进流**,这正是
462
- // 「拼进 objective 是伪方案」的账要划清的线;文件名已消毒为安全字符集,无注入面)。
463
229
  objective: attachmentNotice ? `${picked.cleanedObjective}\n\n${attachmentNotice}` : picked.cleanedObjective,
464
- // Decoupling seam: a trusted client (token-holder) injects its OWN business/UX system prompt
465
- // (persona, UI markers like ::ISSUE_FORM::, workflow rules). The service stays business-agnostic
466
- // — integrators evolve their UX here, not by changing scenario prompts in this repo. core's
467
- // stableSystem appends it after the scenario base, before <user_memory>.
468
230
  systemPrompt: typeof body.systemPrompt === "string" ? body.systemPrompt : undefined,
469
- // [1476] R1 / [1478] R2: top-level append rider → core TaskSpec.appendSystemPrompt (stable block after the
470
- // scenario base + systemPrompt, before the volatile tail). settings.outputStyle composes AFTER this via
471
- // applyTaskSettings (`append\n\nstyle`) — byte-stable order: knowledge block always first, style always
472
- // second (pinned by test; order flapping would shred the prefix cache). Resume re-enters here off the
473
- // persisted body, so the rider re-applies on resumed legs like systemPrompt does — DEFENSIVELY (codex F2):
474
- // the resume families skip prepareSpec's 400 gate and a pre-1.243 store kept unknown keys verbatim, so an
475
- // over-cap legacy value is dropped+warned here instead of becoming active prompt content.
476
231
  appendSystemPrompt: acceptedAppend,
477
- // sessionId comes from `auth` (ownership-checked / principal-derived), NEVER the body — the
478
- // authorizer always resolves one. (No `?? body.sessionId` fallback: that would re-open the
479
- // body-as-capability hole S6 closed if a deployment ran without an authorizer.)
480
232
  sessionId: auth?.sessionId,
481
- // The authenticated end-user principal (design/62, core 1.78). Read-only on the TaskSpec — core injects
482
- // it into a consumer MCP's `principalHeader` per-task so the MCP can enforce per-user RBAC (oa-mcp). It
483
- // comes from the auth channel (never the body/model), so the model can't substitute another identity.
484
233
  principal: auth?.principal,
485
234
  images: body.images,
486
- // design/112 C1: pass the client/gateway-supplied user context through to core (localizes the env block's date
487
- // to the user's zone + surfaces who the agent acts for). HTTP-validated + capped at submit (prepareSpec); on a
488
- // resume the body was already validated at its original submit. core re-validates timeZone (invalid → UTC) and
489
- // SANITIZES userEmail (inlineUntrusted: folds newlines, neutralizes <system-reminder>, caps 160cp — no injection;
490
- // double-review-confirmed). PICK only the two fields core reads — don't passthrough stray keys (a caller could
491
- // otherwise bloat the persisted run body with an uncapped junk field; core ignores them but the body stays tight).
492
235
  ...(body.clientContext ? { clientContext: { timeZone: body.clientContext.timeZone, userEmail: body.clientContext.userEmail } } : {}),
493
- // Structured output (CC --json-schema): a JSON Schema constraining the model's FINAL
494
- // answer. core injects a built-in `submit_output` tool from it + surfaces the validated object as
495
- // TaskResult.structuredOutput. HTTP layer shape+size-validated it (prepareSpec); core does deep validation.
496
- // RESUME re-runs resolveSpec ⇒ the structured-output constraint re-applies on resumed legs too.
497
- // 历史复审轴B #5(1.254):compactionModel 已接([1479]①)而 design/145 配套旋钮 clampTolerance
498
- // 够不着=不对称。窄收单键(0..1 数值;其余 compaction 键仍是操作方轴,有意不开)。
499
236
  ...(() => {
500
- // 键已在 `TaskRequestBody` 上声明为 `{ clampTolerance?: number }`(#205 件1)⇒ 直读;
501
- // 下面的 typeof/区间校验照旧(resume 重放持久化 body 不过 HTTP 门)。
502
237
  const ct = body.compaction?.clampTolerance;
503
238
  return typeof ct === "number" && Number.isFinite(ct) && ct >= 0 && ct <= 1 ? { compaction: { clampTolerance: ct } } : {};
504
239
  })(),
505
240
  outputSchema: body.outputSchema,
506
- // 历史复审轴A #2(1.254):outputRetries 与 outputSchema 是成对的 caller 面旋钮(retry-on-invalid
507
- // 轮数),此前只接了 schema 半边。窄校验:1..10 整数,schema 在场才有意义(core 无 schema 时忽略)。
508
241
  ...(typeof body.outputRetries === "number" && Number.isFinite(body.outputRetries) && body.outputRetries >= 1
509
242
  ? { outputRetries: Math.min(10, Math.floor(body.outputRetries)) }
510
243
  : {}),
511
- // E18 resume-at: the resolved persistent SessionTreeEntry.id (from the eventId the shell sent). core branches
512
- // the session at this prior entry before running the new objective ("rewind to this message, ask differently").
513
244
  resumeAt: resumeAtEntryId,
514
- // [833] rewind exclusive mode (core 1.292): "before" branches at the target's PARENT (excludes the target —
515
- // "remove this prompt and everything after it"); absent/"at" = core default, zero regression. Defensive
516
- // normalize (RESUME re-enters resolveSpec without the HTTP 400 gate): only the two enum values ride, and only
517
- // WITH a resolved resumeAt (core ignores the field otherwise; keep the persisted spec tight). core's "before"
518
- // edge rejections (resume_at.before_target_not_user / resume_at.before_root_unsupported /
519
- // rewind_snapshot.unresolvable) surface as failed-result errorCodes mapped to 4xx by resumeAtHttpStatus,
520
- // errorCode passed through UNCHANGED for the shell to render.
521
245
  resumeAtMode: normalizeResumeAtMode(body.resumeAtMode, resumeAtEntryId !== undefined),
522
- // design/114 Phase3 (reuse-path warm-resume): require the session to already exist — core's acquire fails loud
523
- // (`resume.session_not_found`) on a genuinely-missing/purged session instead of silently starting a fresh one.
524
- // The service authorizer ALSO stops pre-registering a missing id under this flag (security.ts), so the store's
525
- // fail-loud actually fires (else the authorizer's claim-create would materialize the session first). Rides onto
526
- // resume legs (resolveSpec re-runs); a resumed session exists, so it's satisfied there.
527
246
  requireExistingSession: body.requireExistingSession === true ? true : undefined,
528
- // §4 (Fork): CC `/fork` — core 1.257 (design/136 BREAKING) retired the standalone Fork tool; a fork is now
529
- // Agent(subagent_type:"fork") gated by `enableFork`. 🔴 core FLIPPED enableFork to opt-OUT (undefined = available),
530
- // so enableForkFromBody now returns an EXPLICIT boolean (never undefined) — false where the service denies, else
531
- // fail-OPEN. DEFAULT ON single-user turnkey (clay 2026-07-01); per-task opt-OUT via body.enableFork:false;
532
- // multi-tenant honored ONLY with a per-principal entitlement resolver wired (core enforces `allowFork`), else
533
- // explicit false (fail-closed). core still requires a fork-capable store (`hasSessionFork`; TOC file backend
534
- // satisfies it via LocalSessionStore.fork), else inert.
535
- // NB: `centerRuntimeCapsResolver`(非合成后的 runtimeCapsResolver)—— env observer 基线不是
536
- // center 背书的 entitlement 源,多租户 fail-close 判别只认 center caps client。
537
247
  enableFork: enableForkFromBody(body, config, Boolean(centerRuntimeCapsResolver)),
538
- // E12 (shell-host contract): opt-in post-completion prompt suggestions. core runs ONE extra fire-and-forget LLM pass
539
- // after the task completes; the service surfaces the strings (redacted, UNTRUSTED UI-only) as a `suggestions`
540
- // event. Defensive normalize (RESUME re-runs resolveSpec without the HTTP 400 validation). OFF ⇒ zero extra LLM.
541
248
  suggestNextPrompts: normalizeSuggestNextPrompts(body.suggestNextPrompts),
542
- // E19 (shell-host contract): opt-in working-tree rewind. core snapshots each completed turn + restores the files on a
543
- // resumeAt branch, for any env when fileSnapshotStore is wired (gate-split 1.134.0). OFF ⇒ no snapshot.
544
249
  rewindFiles: body.rewindFiles === true ? true : undefined,
545
- // R8 code-only (core 1.166): the resolved target entry id for "Restore code" — core restores the working tree to
546
- // its snapshot WITHOUT branching the session. Mutually exclusive with resumeAt (only set on the code-only path).
547
250
  rewindFilesTo: rewindFilesToEntryId,
548
- // design/119 (CC --add-dir): extra host dirs the FILE tools may access (core canonicalizes each into the
549
- // containment allowlist + lists them in the `# Environment` block). Single-user host lane only (gated above).
550
251
  additionalDirectories,
551
- // core 5.11.0:只读围栏(见上方 parse 段注)。缺席=字节等价([2625] 自查③)。
552
252
  additionalReadDirectories,
553
- // EnterPlanMode (core 1.167): MODEL-DRIVEN plan — `enablePlanMode:true` alone (writable
554
- // start, NO handsReadOnly) auto-mounts `enter_plan_mode`/`present_plan` so the model can self-enter read-only
555
- // plan AT ANY TIME it judges a task needs planning. 🆕 DEFAULT ON (clay: resident; PLAN_MODE_ENABLED=false opts
556
- // out). USER-driven plan (permissionMode=plan) ADDS handsReadOnly on top via applyTaskSettings (which also sets
557
- // enablePlanMode, idempotent). Additive — widens nothing; the model self-selects when to use it.
558
253
  enablePlanMode: config.planModeEnabled ? true : undefined,
559
- // design/201 §2:**显式** permission mode → shell 门档位的翻译(bypassPermissions ⇒ "off",
560
- // auto/default/acceptEdits/plan ⇒ "classify";表逐字在 `shellGateForMode`)。这一档是 governance 的
561
- // **base** —— 部署旋钮 `MANUAL_MODE_SHELL_GATE` 与 `AUTONOMY=ask` 在其后的 governance 拍只升不降地
562
- // 覆盖它(#153 那条无条件施加的腿一行未动),所以客户端表态**减不掉**任何 operator 面的收紧。
563
- //
564
- // 🔴 **表态缺席 ⇒ 不写这个键**(不是写 "off",也不是写 "classify"):
565
- // · 写 "classify" 会让今天 env 真缺席的**无壳直连**部署(多租户云 / OA / scan 车道 / SDK 直连)
566
- // 在一个 minor 版本里静默收紧,而它们多半没有审批面 ⇒ 失败形是 headless auto-deny;
567
- // · 写 "off" 与不写在 core 的**默认**上等价,但在本进程里不等价 —— 写下去它就成了 governance 的
568
- // base,把 runtime-governance 那条「base 更严则省略」的防御臂喂上一个人造输入。
569
- // 缺席不写 = 今日行为逐字节不变,车道缺省由壳恒 stamp mode 保证(设计 §3 前提③)。
570
254
  ...(effMode ? { shellGate: shellGateForMode(effMode) } : {}),
571
- // Workflow super-set unlock (same shape as the rank-1 roster gap): per-task activation of the
572
- // LLM-authored workflow engine (core `run_workflow` + the workflow.ts agent/parallel/pipeline orchestration).
573
- // The engine is BUILT but the HTTP API never mapped a body field to `spec.selfOrchestration`, so run_workflow
574
- // was per-task unreachable (capabilities advertised `workflows:true` but the LLM could never call it). Gated on
575
- // the DEPLOYMENT enabling it (`selfOrchestrationEnabled` → workflowScriptRunner/stores wired; else a set flag is
576
- // a harmless no-op core fail-closes). 🔒 Multi-tenant entitlement is NOT blanket-closed here (unlike mcp, which
577
- // has no engine entitlement) — core's `runtimeCapsResolver` enforces `allowWorkflows` (the third-stage
578
- // per-principal cap) ON THE ENGINE, so a multi-tenant principal WITHOUT the entitlement gets run_workflow
579
- // fail-closed by core (no resolver ⇒ fail-closed). Single-user honors directly. Three-gate: engine-can ∧
580
- // center-may(allowWorkflows) ∧ shell-show. (Logic + gates documented in `selfOrchestrationFromBody`.) 🔒 The
581
- // multi-tenant floor: pass whether an entitlement RESOLVER is wired — core's allowWorkflows is TIGHTEN-ONLY
582
- // (no resolver ⇒ fail-OPEN per-principal), so the helper fail-closes multi-tenant when none is present.
583
- // L2 ultracode (design/111): OR the preset's selfOrchestration:true into the body intent, passed THROUGH the
584
- // existing gate — it grants nothing a raw body.selfOrchestration:true couldn't (multi-tenant still fail-closes
585
- // without an entitlement resolver). The preset never bypasses the闸.
586
255
  selfOrchestration: selfOrchestrationFromBody({ selfOrchestration: body.selfOrchestration === true || parsedSettings.settings?.ultracode === true }, config, Boolean(centerRuntimeCapsResolver)),
587
- // C1 (core 1.219, subagent viewing pane): opt-in widening of the forward sink from
588
- // task_progress-only to a delegated child's live CONTENT events (text_delta/reasoning_delta/tool_start/
589
- // tool_end, each carrying `parentToolCallId` attribution). Purely a RENDER channel — core never merges the
590
- // child stream into the parent's model context. The service's forward-sink consumers redact per §E1 (shared
591
- // builders); default OFF = prior progress-only behavior. Strict `=== true` (never a truthy coercion).
592
256
  forwardSubagentEvents: body.forwardSubagentEvents === true ? true : undefined,
593
- // design/122 (core 1.225): opt-in retention of SETTLED sub-agent sessions so the operator/
594
- // shell can REVIVE them (POST /v1/runs/:id/subagents/:target/resume). Defensive normalize + clamp
595
- // (RESUME re-runs resolveSpec without HTTP validation): boolean passes; an object's ttlMs/max are clamped
596
- // to sane ceilings (retention pins live sessions in memory — a caller must not turn the knob into a
597
- // resource hold; core's own defaults are 30min/16, the run-scoped ledger releases everything at parent
598
- // end either way). Malformed values fall to undefined = OFF (core default), never a throw.
599
257
  retainSubagentSessions: normalizeRetainSubagentSessions(body.retainSubagentSessions),
600
- // [876] per-task custom subagents (core 1.295 TaskSpec.agents — CC `.claude/agents/` parity): the shell's
601
- // resolved AgentDefinition[] rides the spec verbatim (model already a real name/Model object — no alias
602
- // translation here). 🔒 TOB tenant gate + defensive per-item normalize live in taskAgentsFromBody
603
- // (spec-fields.ts): multi-tenant is fail-closed (warn task_agents_ignored — no per-tenant caps face yet,
604
- // capabilities.taskAgents advertises false); RESUME replays skip the HTTP whitelist 400 → illegal items
605
- // DROP+warn per entry (task_agent_dropped), never a bricked resume. Absent/empty ⇒ no key (byte-compat).
606
- // [1.211 codex L] 内联闭包换 spec-fields 真出口(taskAgentsSpecFragment)——测试直接 import 调真
607
- // fold,SOURCE PIN 收敛为「resolveSpec 内恰一次调用点」单锚(文本重写同构表达式的假绿面就此关)。
608
258
  ...taskAgentsSpecFragment(body.agents, config.requirePrincipal, (event, fields) => logger.warn(event, { ...fields, sessionId: auth?.sessionId ?? null })),
609
- // [854]② (core 1.295): opt-out of task-end session-background reaping ("keep my dev server running").
610
- // Resource-residency grant on the host lane → single-user gate like backgroundShell/hooks (multi-tenant
611
- // warns task_retain_bg_ignored + ignores; capability advertises false). Only literal `true` rides (false =
612
- // core default, key omitted); non-boolean is 400 at submit, defensively dropped on a resume replay.
613
259
  ...(retainBackgroundProcessesFromBody(body.retainBackgroundProcesses, config.requirePrincipal, (event, fields) => logger.warn(event, { ...fields, sessionId: auth?.sessionId ?? null })) === true
614
260
  ? { retainBackgroundProcesses: true }
615
261
  : {}),
616
- // [1052]② (core 1.314): per-run 工具面收窄两位——excludeTools=roster 真卸载,deferTools=延迟披露
617
- // (cli「Workflow 默认开不暴露」承载位)。tighten-only(core 继承 union 不变量),无租户门;defensive
618
- // normalize(resume 重放),提交面 400 在 HTTP 门。
619
262
  ...(() => { const v = toolNameListFromBody(body.excludeTools); return v ? { excludeTools: v } : {}; })(),
620
263
  ...(() => { const v = toolNameListFromBody(body.deferTools); return v ? { deferTools: v } : {}; })(),
621
- // [1144]/[1146] (core 1.328 R2): promptProfile 双形轴直通——纯呈现轴无租户门(deferTools 姿势);
622
- // 缺省不挂键(引擎缺省 simple);非法值由 HTTP 门 400,resume 重放 defensive DROP。
623
264
  ...(() => { const v = promptProfileFromBody(body.promptProfile); return v ? { promptProfile: v } : {}; })(),
624
- // [2856]②(core 5.14.0 引入;5.15.0 BREAKING 末项默认翻 swap):deferred 工具激活后的 schema 供给
625
- // 策略,per-task 位。core 侧读法是 `spec.toolMaterializeStrategy ?? env ?? "swap"`(5.16.0 dist
626
- // prepare-task 亲读)⇒ **缺省不挂键**才能让进程级 env 继续说话;
627
- // 挂一个「缺省值」上去会把 env 车道悄悄夺权。非法值由 HTTP 门 400,resume 重放 defensive DROP。
628
265
  ...(() => { const v = toolMaterializeStrategyFromBody(body.toolMaterializeStrategy); return v ? { toolMaterializeStrategy: v } : {}; })(),
629
- // [922]①/[915]② (core 1.296): interactiveTools 三态旋钮直通——boolean 上 spec(壳 -p 恒 stamp false=
630
- // A3 类「headless 提问 park」根治腿;plan 显式 true 恒赢),undefined=core 自动判据(送达面探测)。
631
- // per-run 工具面选择无跨租户面 ⇒ 无租户门;非 boolean 由 HTTP 门 400,resume 重放 typeof 检查天然 DROP。
632
266
  ...(typeof body.interactiveTools === "boolean"
633
267
  ? { interactiveTools: body.interactiveTools }
634
268
  : {}),
635
- // [1909]⑧(core 5.23.0 `TaskSpec.oneShot`):这次**提交**是不是一次性的 —— 没有后续回合能接住异步
636
- // 后台通知(headless `sema -p` 的进程随本回合退出)。core 只拿它改 run_workflow / 委派回执的**指引
637
- // 文本**(告诉模型主动 block-wait,而不是「结束回合等通知」——后者在一次性进程里必丢结果),不授予
638
- // 任何能力 ⇒ 无租户门,与 interactiveTools 同姿势直通。
639
- // 🔴 **表态在场才写、缺席不写键**(与 shellGate 同形):不写 = core 默认(交互态)逐字节不变;写一个
640
- // 「默认值」上去等于替没表态的调用方做了表态。非 boolean(resume 重放持久化 body / 提交面已 400)
641
- // 同样不写键。四条 rebuild 腿读的是同一份持久化 body ⇒ 首跑与续跑天然同值。
642
269
  ...(typeof body.oneShot === "boolean" ? { oneShot: body.oneShot } : {}),
643
- // design/131 (core 1.246): per-task resilience INTENT flags. allowDegrade/allowFailover are
644
- // caller-facing (a bench/eval run wants true failure shapes); bypassBreaker is operator-only (normalizer
645
- // drops it for non-operators — it punches through a SHARED breaker). All-absent = byte-compat.
646
270
  resilience: normalizeResilience(body.resilience, explicitOperatorOk(auth?.principal, config.operatorPrincipals)),
647
- // design/132 (core 1.249): one-shot end-game verification nudge ("re-run the final artifact through
648
- // its real entrypoint before finishing"). OPT-IN by core's own judgment (default ON lost the evidence case:
649
- // +1 turn on every interactive write task) — the AUTONOMY caller declares it (harness/scheduler lanes).
650
- // [849]→[2400] scenario-declared leg 已无生产者(autonomous 退役后无场景定死终验);保留 OR 折入形防回归 —— (cap.finalVerification —
651
- // picking that scenario IS the autonomy declaration, so this is not "inferring from scenario"); OR-folded
652
- // with the caller's explicit flag — a scenario can only ADD the closing verification turn, never strip a
653
- // caller's request. Children do not inherit (core semantics).
654
271
  finalVerification: cap.finalVerification === true || body.finalVerification === true ? true : undefined,
655
- // design/133 (core 1.251): turn-boundary attachment reminders (todo/changed-files/plan-mode),
656
- // literal-true unions — the normalizer drops `false`/garbage keys instead of forwarding them (core contract:
657
- // "off = delete the key, never send false"). Resume-safe by construction: the persisted body is full JSON
658
- // and this is the single translation point, so this line IS the whitelist inclusion.
659
272
  attachments: normalizeAttachments(body.attachments),
660
- // hooks(阶段一,gate 见上面 taskHooks 装配):core 是整槽覆盖(`spec.hooks ?? deps.hooks`,
661
- // runtask gateBaseline 注释点名),task hooks 一挂会 shadow deps.hooks 上的 TOOL_TRACE 观测 → composeHooks
662
- // 把部署基线折进来(部署槽先跑=观测看到真实执行)。无 task hooks 时不挂字段,deps 路径原样。
663
273
  ...(taskHooks ? { hooks: composeHooks(deploymentHooks, taskHooks) } : {}),
664
274
  model: picked.model,
665
- // [1479]① compactionModel (design/145 cheap compaction gear): catalog-gated to the SAME expanded view as
666
- // body.model (canonical name form). resolveSpec is the AUTHORITY (codex R5 — the prepareSpec 400 is only
667
- // a fast-fail UX layer; the catalog hot-refreshes in place, so a ref valid at the HTTP gate can be gone
668
- // after the authorize/resolve awaits): unknown on a FRESH leg → 400 fail-loud (the documented guarantee —
669
- // never a silent price change); unknown on a RESUME replay → drop + warn (core's resolveModel THROWS on
670
- // an unknown compactionModel, which would brick the resume; the fallback is the summarize role / main
671
- // model, the pre-field behavior).
672
- // Window note (codex R6, requalified): the resolveSpec→Runner handoff shares spec.model's DOCUMENTED
673
- // restart-window class ([865] / the wireCatalog note above) — the Runner resolves the canonical string
674
- // against ITS OWN constructor-time expanded snapshot, so a hot refresh between here and prepare can
675
- // desync the two views. Inside that window the failure mode is core's resolveModel TYPED THROW
676
- // ("Unknown model ref", roles.js dist-read) = a fail-loud task error, never a silent price change —
677
- // the same honest posture the sibling field ships with. Binding a Model OBJECT here instead would
678
- // diverge from spec.model's string-intent contract and freeze catalog bytes into the persisted body.
679
275
  ...((() => {
680
276
  if (typeof body.compactionModel !== "string" || body.compactionModel.length === 0)
681
277
  return {};
@@ -688,10 +284,6 @@ export function createResolveSpec(ctx) {
688
284
  logger.warn("compaction_model_unknown_fallback", { requested: body.compactionModel.slice(0, 120), sessionId: auth?.sessionId ?? null });
689
285
  return {};
690
286
  }
691
- // #233 / A-002.8 第二执法点:`compactionModel` 也是用户逐 turn 选择通道(一次显式的省钱换挡),
692
- // 与 body.model 同门同拒形。归一同源(isModelAllowlisted 收增广目录 + 名单)。resume 照旧续跑
693
- // (终裁 (a)):这条腿上"照旧"= 保留该 compactionModel,只留声——把它丢回 summarize 角色是
694
- // **静默换价**,正是本字段当初为之存在的病灶。
695
287
  if (!isModelAllowlisted(cm, wireCatalog, config.atModelAllowlist)) {
696
288
  const terminal = terminalCatalogName(cm, wireCatalog) ?? cm;
697
289
  if (opts?.leg === "fresh") {
@@ -702,31 +294,10 @@ export function createResolveSpec(ctx) {
702
294
  }
703
295
  return { compactionModel: cm };
704
296
  })()),
705
- // E7 (shell-host contract): reasoning-effort selection threaded to core's ThinkingLevel. Defensive
706
- // isThinkingLevel guard — RESUME re-runs resolveSpec WITHOUT the HTTP-layer 400 validation, so an invalid/
707
- // absent value falls through to the resolved role's default thinking (core's RoleSpec.thinking), never throws.
708
- // L2 ultracode (design/111): the preset FLOORS thinking at the ultra tier (= max, via core resolveReasoningProfile);
709
- // awareness rides for free (core composes it from thinking∈{xhigh,max}). Else the explicit reasoningEffort.
710
297
  thinking: effectiveThinking(body.reasoningEffort, parsedSettings.settings?.ultracode === true),
711
- // Per-model auth (config-center apiKeyEnv): core calls this per brain call / cascade rung so each
712
- // model uses its own upstream key; a model without one falls back to the gateway key. undefined
713
- // when no per-model keys are configured → unchanged single-key behavior.
714
- getApiKeyAndHeaders: getKeyResolver(), // A10 搬运改写②:活引用取值(原 `keyResolver`)
715
- // [854]④ per-request 配速:body.limits.{maxWalltimeMs,maxOutputTokens,maxTurns} 现在被收下(核对上游
716
- // TB2.0 实测诉求;旧姿势「body limits 一律忽略」作废)。合成规则在 resolveTaskLimits(spec-fields.ts):
717
- // - body.maxWalltimeMs 给了就用 body(caller 显式配速,可低于内建墙;已被可选 TASK_TIMEOUT_MAX_SEC
718
- // 封顶——env 保秒义,对毫秒键封顶时 ×1000);
719
- // - body 缺席保持既有姿势 = tenancy 墙钟(单用户 turnkey 无墙 / 多租 2400s、大任务 3600s,
720
- // clay 2026-07-04 make-real 教训①)+ env TASK_TIMEOUT_SEC 只抬不降(taskWallClockSec 内 Math.max);
721
- // - maxOutputTokens/maxTurns 直透传(可选 TASK_MAX_OUTPUT_TOKENS_MAX / TASK_MAX_TURNS_MAX 同款封顶)。
722
- // resume 重放持久化 body 不过 HTTP 400 门 → normalizeLimits defensive(0/负/垃圾按键 DROP,不 throw);
723
- // 退役键(timeoutSec/deadline 族)DROP 必须留痕:task_limits_legacy_key_dropped 带被丢键与最终生效墙钟
724
- // (null=整任务无墙——单用户 turnkey 无 env 墙时的真后果;多租=被延到租户墙)。
298
+ getApiKeyAndHeaders: getKeyResolver(),
725
299
  ...(() => {
726
300
  const limits = resolveTaskLimits(body.limits, taskLimitCaps, taskTimeoutSec, config.requirePrincipal, body.council === true || body.debate === true || scenarioName === "discuss", (event, fields) => logger.warn(event, { ...fields, sessionId: auth?.sessionId ?? null }));
727
- // [1301]③ config catalog server 半场:env 封顶不再是「五层五值互不知情」的暗手——每个真在场
728
- // 的运营方旋钮以 configOverrides 声明进 spec(advisory,core 折进 config.assembled 的
729
- // overrideReasons;「谁设的顶」变成读帧不考古)。只声明 SET 了的键(缺省不设=不污染帧)。
730
301
  const declarations = [];
731
302
  if (taskLimitCaps.timeoutSec !== undefined)
732
303
  declarations.push({ key: "server.limits.timeoutSecCap", value: String(taskLimitCaps.timeoutSec), reason: "env TASK_TIMEOUT_MAX_SEC (seconds; operator ceiling applied to caller limits.maxWalltimeMs as cap×1000)" });
@@ -734,33 +305,18 @@ export function createResolveSpec(ctx) {
734
305
  declarations.push({ key: "server.limits.maxOutputTokensCap", value: String(taskLimitCaps.maxOutputTokens), reason: "env TASK_MAX_OUTPUT_TOKENS_MAX (operator ceiling)" });
735
306
  if (taskLimitCaps.maxTurns !== undefined)
736
307
  declarations.push({ key: "server.limits.maxTurnsCap", value: String(taskLimitCaps.maxTurns), reason: "env TASK_MAX_TURNS_MAX (operator ceiling)" });
737
- // PAIR-REVIEW F-4:budget 族与墙钟基值的声明补齐——[1301]③ 首批只做了 limits 三键,而
738
- // MAX_TASK_COST_USD/MAX_TASK_TOKENS 在 caller 缺席时**直接成为** spec 值(cappedCeiling),core
739
- // 便记 provenance:"spec"=归因谎报(「谁设的 5 刀」重回考古)。同姿势:只声明 SET 了的键,
740
- // server 命名空间,绝不重复 core 目录键。
741
308
  if (config.maxTaskCostUsd > 0)
742
309
  declarations.push({ key: "server.budget.maxCostUsdCap", value: String(config.maxTaskCostUsd), reason: "env MAX_TASK_COST_USD (operator ceiling; becomes the effective value when the caller sends none)" });
743
310
  if (config.maxTaskTokens > 0)
744
311
  declarations.push({ key: "server.budget.maxTokensCap", value: String(config.maxTaskTokens), reason: "env MAX_TASK_TOKENS_MAX (operator ceiling; becomes the effective value when the caller sends none)" });
745
- // F-4 附:tenancy 墙钟基值(taskWallClockSec 在 multi-tenant 时注入 2400/3600 基值——同为
746
- // 「server 设的顶」,声明其来源;single-user 无墙=不声明)。
747
312
  if (config.requirePrincipal === true)
748
313
  declarations.push({ key: "server.limits.wallClockBaseSec", value: "2400/3600", reason: "tenancy wall-clock base (multi-tenant; big tasks 3600) — TASK_TIMEOUT_SEC raises, never shrinks" });
749
- // codex F1:core 目录自有键(limits.maxWalltimeMs 等)**绝不重复声明**——core 已按真 provenance 发
750
- // 该字段,叠一条 host-declared 字符串值=同键双条矛盾帧(by-key 投影抹掉权威来源)。server 只
751
- // 声明自己命名空间的键;来源语义(caller vs 墙钟)由 maxWalltimeMs 的 spec provenance + 上面的
752
- // Cap 声明组合可读。
753
- // core 5.8.0:预算族(maxCostUsd/maxTokens/degrade)从 TaskSpec 顶层迁入 limits——顶层键已删,
754
- // 继续写顶层=经宽类型静默死键(E-HIGH-1 病族)。wire 面不变:body 顶层 maxCostUsd/maxTokens 照收
755
- //(SDK 在发),装配目标改到 limits。budgetStreamCancel 语义照 core 默认(maxCostUsd 在场即 true)。
756
314
  const maxCostUsd = cappedCeiling(body.maxCostUsd, config.maxTaskCostUsd);
757
315
  const maxTokens = cappedCeiling(body.maxTokens, config.maxTaskTokens);
758
316
  const degrade = (() => {
759
317
  if (!config.degrade || maxCostUsd === undefined)
760
318
  return undefined;
761
319
  if (Array.isArray(body.images) && body.images.length > 0 && !config.degrade.toSupportsImages) {
762
- // S14 (SILENT-FALLBACK P0-f): this drop was undetectable — the task keeps its vision-capable main
763
- // model and near-budget it hard-fails on cost instead of degrading. Surface it.
764
320
  metrics.inc("degrade_dropped_total", { reason: "vision_target" });
765
321
  logger.warn("degrade_dropped", { reason: "vision_target", model: picked.model, images: body.images.length });
766
322
  return undefined;
@@ -775,96 +331,21 @@ export function createResolveSpec(ctx) {
775
331
  };
776
332
  return { ...(Object.keys(merged).length > 0 ? { limits: merged } : {}), ...(declarations.length > 0 ? { configOverrides: declarations } : {}) };
777
333
  })(),
778
- // design/129: the TOC/interactive posture (single-user turnkey — the shell's
779
- // session lane) defaults background children to SESSION scope = CC Backgrounded semantics (a bg Agent/
780
- // Fork outlives the turn; completion arrives via the durable-inbox push). Multi-tenant / CI / workflow
781
- // deployments keep core's "task" default (no orphans burning tokens). Caller-trusted spec field
782
- // (systemPrompt tier), NEVER read from the request body. Same tenancy predicate as the wall clock above.
783
334
  ...(config.requirePrincipal !== true ? { backgroundScope: "session" } : {}),
784
- // ⑤ Per-task budget gate + 1.40 near-budget degrade:core 5.8.0 起全部在 limits(上方 IIFE 合成),
785
- // 顶层键已随 core 类型删除。
786
- // Long-term memory (design/138 S1): enabled when the memory ENGINE is wired AND a scope was derived
787
- // (single-user only — multi-tenant derives none, memory dark). MF-30 PAUSE (option B, per-request —
788
- // clay 2026-06-27 confirmed with core): `body.memoryWrite:false` makes THIS run read-only over memory
789
- // (`writeScope:null` — the engine materializes/reads but harvest commits nothing). Per-request (not a
790
- // stored flag) → no new per-session state; the shell carries the toggle. RESUME re-runs resolveSpec
791
- // from the persisted body, so a paused run stays paused across resume legs.
792
- // 件A origin 盖章(#148 件3④):部署形态维随 requirePrincipal——多租户下登记簿种子(projectId
793
- // 选定)按 request 盖章走 core 准入门,单用户一律 deployment(v4 §1 N2;零 org 键=整键缺席零迁移)。
794
335
  memory: memoryEngine ? memorySpecForRequest(auth?.memoryScope, body.memoryWrite, s4DefaultScopes, { multiTenant: config.requirePrincipal === true }) : undefined,
795
- // #217(core 5.26.0 提货):部署对「本机能不能把用户的『记住 X』落到持久处」的自我声明,env
796
- // `MEMORY_PERSISTENCE_CAPABLE` 三态直通。**部署面旋钮,不开请求键**——恢复 remote lane 的
797
- // `# Memory` 写指令是「这条执行车道与记忆根共享挂载」这个部署事实,归 operator 不归调用方表态
798
- // (operator-knob 律:部署级旋钮禁挂客户端表态派生腿)。与请求键 `memoryWrite` 两轴正交:后者是
799
- // per-run 收紧(writeScope:null),前者是部署声明;任一取否方向即只读方向,客户端永远撑不开
800
- // 部署声明的 `false`(core 自己也把 declared false 当 FLOOR:委派转发 tighten-only)。
801
- // 三态的 absent 腿 = **整键缺席**(core:「absent = 各自推断」),所以走条件展开而不是赋 undefined。
802
- // 🔴 core 5.27.0([3612] F2)起,`false` 还让这次会话成为**受限会话**:按已提交账供给,磁盘上
803
- // 无事务背书的分歧不收编不供给(留盘 + `restricted_divergence` 点名)。**受限是会话级的这一个键**,
804
- // 与上面 `memoryWrite:false` 铸出的 `writeScope:null` **只读平面**不是一回事 —— 那条平面照旧
805
- // adopt-on-read(暂停的只是 harvest)。两轴各自的完整口径见 `config-types.ts` 的字段注。
806
- // 🔴 #266 A3 源头修([4082],A-044):有效值经 effectiveMemoryPersistenceCapable 单源——env 显式恒赢;
807
- // env 缺席且 `REMOTE_EXEC=host` ⇒ 自动 true(host 腿文件平面=本机,capable 断言的车道级事实由构造
808
- // 成立;core 按 remote 面 withhold 对这条腿是误伤,裸装默认形的记忆写指令曾因此静默消失)。
809
- // 沙箱腿/in-process 逐位不变。理由全文与「为什么不是围栏合取」见该函数头注。
810
336
  ...((v) => (v !== undefined ? { memoryPersistenceCapable: v } : {}))(effectiveMemoryPersistenceCapable(config)),
811
- // Scenario-provided capabilities (e.g. code-review = repo tools + reviewer subagents + prompt).
812
- // RFC A2: the SelectEnvironment tool rides after the scenario's tools (spec.tools is ADDITIVE to core's
813
- // built-in roster — prepare-task mounts first-party tools separately). Only when the image chain is live.
814
337
  tools: ((base) => {
815
338
  const extra = [...(selectEnvTool ? [selectEnvTool] : []), ...(sendUserFileToolSpec ? [sendUserFileToolSpec] : [])];
816
339
  const merged = extra.length > 0 ? [...(base ?? []), ...extra] : base;
817
- // [803]④ defer face (EXPERIMENTAL, default OFF): long-tail first-party tools get per-tool defer:true
818
- // (assessment + why NOT deferMode:"auto"/SendUserFile: src/capabilities/tool-defer.ts header).
819
340
  return merged && config.toolDeferLongtail ? applyLongtailDefer(merged, true) : merged;
820
341
  })(cap.tools),
821
- // Personalization: the caller's own skills ride after the scenario's; scenario
822
- // wins on a name clash (mergeUserSkills — the security baseline can't be shadowed). HTTP layer
823
- // already validated shape + caps (validateUserSkills).
824
342
  skills: mergeUserSkills(cap.skills, body.skills, logger),
825
- // Sema-registry MCP servers (B1) filtered to this scenario (env-NAME refs already resolved at startup),
826
- // PLUS the caller's gated per-request MCP (R3 — TOC local `.mcp.json`). resolveRequestMcp honors body.mcpServers
827
- // on any SINGLE-USER deployment (`requirePrincipal!==true` — the requester is the super-admin of their own
828
- // worker; the stdio MCP runs on the worker, not the exec env, so the lane is irrelevant → seamless TOC↔cloud);
829
- // a multi-tenant deployment returns the baseline unchanged (gate closed → ignored, advertised via
830
- // capabilities.mcpInjection). Baseline wins on a name clash (a caller can ADD a server, never SHADOW a configured one).
831
- // design/170 件C:锁形下本行不可达 —— `resolveSpec` 腿首已整拒(见那里的 F3 注)。`resolveRequestMcp`
832
- // 收下的 config 带着 `lockedConfigKeys`,所以即使将来有人绕开腿首那道门,honored 谓词也会翻假。
833
343
  mcp: resolveRequestMcp(mcpForScenario(config.mcpServers, scenarioName), body.mcpServers, config, logger),
834
- // DESIGN-269 车1 件3 —— A2A peers,与上一行逐点同构:center 基线按场景过滤 + 调用方自带的 peer
835
- // (单用户闸 ∧ 未锁 ∧ 合规未禁,`resolveRequestA2a` 里那只三票谓词是唯一属主),同名 baseline 赢。
836
- // 🔴 与 MCP 的语义差:一个 peer 是**远端 agent**,core 给它的每条 skill 挂 `egress:true`+
837
- // `effect:"write"`(2026-08-03 裁定)—— 所以这一行接出去的是对外写通道,不是数据源。两侧都缺席时
838
- // 键整个不写(`a2aForScenario` 与 `resolveRequestA2a` 都恒返回 undefined 而非 []):缺席 = 本部署
839
- // 没有这条腿,空表 = 「声明了一个空的 peer 集合」,对 core 与对读面都是两回事。
840
344
  a2a: resolveRequestA2a(a2aForScenario(config.a2aPeers, scenarioName), body.a2aPeers, config, logger),
841
- // center prompts 投影([1057]①f):center 下发的场景终形(overrides[s] ?? 基线)赢过内置 provider
842
- // (center-wins,scenario overlay 同先例);无 pack/该场景空声明 ⇒ 内置照旧。声明数组逐字喂 typed
843
- // 钩子(contentHash 透传,core 1.315 校验入 manifest);packId 归因在采用日志(center_prompts_adopted)。
844
- // codex R4:读请求级快照 centerDecls(与 append-less 门同一份),不再回读活 ref——刷新竞态下门与
845
- // provider 才不会各看一版(TOCTOU:门判安全、pick 到已组装包=core 侧静默丢 rider 复活)。
846
345
  promptProvider: centerDecls ? centerPromptProvider(centerDecls, scenarioName) : cap.promptProvider,
847
- // F4: high-risk write approval gate. DURABLE (design/45, opt-in): the gated `ask` SUSPENDS the task
848
- // (durable checkpoint, resumable on any replica). [2354]/[2356] A-5(5.0.0): the legacy POLL gate
849
- // (createOaApprovalPolicy over ApprovalStore pending rows) is retired — durable is the ONLY gate; an
850
- // operator with approval intent but no checkpointStore falls to `undefined` (core's UNGATED warning).
851
- // Durable ask (TC-5.4, core 1.95): in durable mode an AskUserQuestion call suspends like an F4 gate —
852
- // the question policy adjudicates it `ask`, the operator answers out-of-band, and the resume carries
853
- // the QuestionAnswer (server.ts `body.answer` → onQuestion closure). #152([2703] 案二): the question
854
- // policy is LIVE-AWARE when the QuestionCoordinator is wired — a live-stream leg adjudicates `allow`
855
- // (the live human answers over the stream) instead of unconditionally parking; see durableQuestionPolicy.
856
- // design/181 件一:基线的**构造**归 `createApprovalBaselinePolicy`(三腿单一属主);本处只做部署形
857
- // 判断——哪一形该有 durable 轴、哪一形铺 allow-all、哪一形留 `undefined`(见下两条注)。
858
- // #196 纵深(设计小票 §2b):hands=none 的场景在 spec 上叠一份 band 名单 deny —— **不是**主保证
859
- // (主保证=这类任务跑在不挂 executionEnvFactory 的 Runner 上,band 根本不 mount),而是「将来
860
- // 有人把它误接回有手 Runner」时的第二层。合成走 tighten-only 的 combinePolicies(TRAP #1:
861
- // 直接赋值会静默丢掉下方那条 durable 审批基线 / auto-accept 座),见 capabilities/hands-lane.ts。
862
346
  toolPolicy: toolPolicyForHands(hands, durableEnabled
863
347
  ? createApprovalBaselinePolicy(config, {
864
348
  question: liveQuestionFace,
865
- // The probe key is the CONTINUED session (auth.sessionId — the same id that keys the
866
- // durable checkpoint /decide route). A fresh session (no body.sessionId) has no exemptions by
867
- // construction; fail-closed inside the policy on a store error.
868
349
  ...(approvalExemptionStore && auth?.sessionId
869
350
  ? {
870
351
  exempt: (toolName) => approvalExemptionStore.has(auth.sessionId, toolName),
@@ -873,22 +354,8 @@ export function createResolveSpec(ctx) {
873
354
  : {}),
874
355
  })
875
356
  : singleUserAutoAcceptBaseline
876
- // 🔴 (live finding: single-user host-lane Bash ran UNGATED, no adjudication): a
877
- // single-user turnkey worker with NO expressed gate intent previously left `toolPolicy` UNDEFINED →
878
- // core's write-capable hand tools mount UNADJUDICATED (core error-logs it every task; host lane has NO
879
- // sandbox to fall back on). CC's trust model is auto-accept, but the GATE MECHANISM must be PRESENT
880
- // (core 原则: "机制留、默认可更宽 auto-accept"). So wire an adjudicated auto-accept BASELINE
881
- // (the durable-less arm of `createApprovalBaselinePolicy` = a present, effect-aware policy that allows
882
- // all): satisfies core's `hasEffectAwareGate`, restores observability + a hook/tighten point, and the
883
- // operator STILL tightens irreversible ops via `AUTONOMY` / `commandPolicy` (layered TIGHTEN-ONLY by
884
- // applyRuntimeGovernance).
885
- // Interactive approval routed to the shell HITL = follow-on. 🔴 `singleUserAutoAcceptBaseline` requires
886
- // ZERO gate intent — a single-user op who SET approval flags but wired no store falls to `undefined`
887
- // (core warns = real misconfig, not masked); multi-tenant likewise stays `undefined` (approval required).
888
357
  ? createApprovalBaselinePolicy(config)
889
358
  : undefined),
890
- // Durable suspend needs both the store (here, per-task) and the opt-in scope key (multi-tenant =
891
- // principal; "_" when auth is off). core suspends on a policy `ask` only when these are present.
892
359
  ...(durableEnabled
893
360
  ? {
894
361
  checkpointStore,
@@ -896,11 +363,6 @@ export function createResolveSpec(ctx) {
896
363
  scope: encodeCheckpointScope(auth?.principal),
897
364
  ...(config.approvalTimeoutSec > 0 ? { ttlMs: config.approvalTimeoutSec * 1000 } : {}),
898
365
  },
899
- // design/80 seam #2: opt this task into resource/preempt durable-suspend (design/74 third state)
900
- // when RESOURCE_SUSPEND=true — a budget/turns/walltime limit OR a scheduler preempt durably SUSPENDS
901
- // (resumable) instead of failing. SAME scope key as durableApproval (the principal). The decision
902
- // (incl. the 🔴 verify/cascade EXCLUSION that keeps eligibility aligned with the preemptSignal wiring,
903
- // so an inner suspend can't orphan a VM+checkpoint) lives in the pure, unit-tested `resourceSuspendOptIn`.
904
366
  ...((rs) => (rs ? { resourceSuspend: rs } : {}))(resourceSuspendOptIn({
905
367
  enabled: config.resourceSuspend,
906
368
  ttlSec: config.resourceSuspendTtlSec,
@@ -908,43 +370,14 @@ export function createResolveSpec(ctx) {
908
370
  isCascade: body.cascade === true,
909
371
  scope: encodeCheckpointScope(auth?.principal),
910
372
  })),
911
- // #152([2703] 案二):sentinel 只在活体面缺席时 stamp。core 取 `spec.onQuestion ?? deps.onQuestion`
912
- // (spec 位恒赢),无条件 stamp 会把 ASK_QUESTION_ENABLED 部署的活人腿永久遮蔽成 durable park。
913
- // 活体面在场 ⇒ 键缺席,RunnerDeps.onQuestion 的 coordinator 生效;park 与否由上面的判决门分腿。
914
- // (suspend 侧的「未答 approve」拒绝不靠 sentinel:server.ts decide 路由 pre-CAS 400 守着。)
915
373
  ...(liveQuestionFace === undefined ? { onQuestion: QUESTION_AWAITS_RESUME } : {}),
916
374
  }
917
375
  : {}),
918
376
  };
919
377
  return spec;
920
378
  };
921
- /** 阶段⑤(governance 折叠):吃 阶段④的 spec + 请求体 + auth + 阶段①的 append/settings 三量,吐 折完运营方
922
- * governance 与客户端 settings(tighten-only)后的 governed,外加两个下游还要用的量:本请求的 scratchpadDir
923
- * 与 hostSemanticsLane 判别。 */
924
379
  const foldGovernanceAndSettings = async (body, auth, spec, gated, effMode) => {
925
380
  const { appendLessPack, acceptedAppend, parsedSettings } = gated;
926
- // Runtime governance "second baton" (center §10): compile the operator's autonomy + commandPolicy onto the
927
- // spec TIGHTEN-ONLY. commandPolicy layers via combinePolicies onto the approval/durable baseline above (NOT a
928
- // bare overwrite — TRAP #1), autonomy expands to handsReadOnly/shellGate (TRAP #2). Applied here so it also
929
- // re-applies identically on the resume paths (which rebuild via resolveSpec) — core requires shellGate/
930
- // toolPolicy be re-supplied on resume (omitting shellGate would leave a resumed run's bash UNGATED).
931
- // tightenTaskSpec THROWS on a loosening misconfig (fail-loud).
932
- //
933
- // 🔴 RESUME uses LIVE config (not a per-task frozen snapshot), IDENTICAL to how the approval baseline above
934
- // reads live `config.approvalRequire`. So if an operator LOOSENS the fleet governance while a task is
935
- // suspended, the resumed task picks up the looser policy (governance stays per-call tighten-only, so a
936
- // mid-suspend TIGHTENING is honored; a loosening is an explicit operator action). Freezing deployment policy
937
- // per-task across suspend is a larger, separate decision that must cover the approval baseline too — tracked
938
- // for center (adversarial-review HIGH-1), deliberately NOT a governance-only partial freeze here (that would
939
- // be an inconsistent false-safety: approval would still be live).
940
- // #153([2713] 分单②):MANUAL_MODE_SHELL_GATE 在此无条件施加(与 autonomy 同拍)——部署旋钮不再
941
- // 依赖客户端 settings 在场性/模式臂/host lane(三重条件曾让 print/headless 腿与沙箱 lane 全静默失效)。
942
- //
943
- // #177([2951] issue #29):SENSITIVE_WRITE_PATTERNS 是 #153 漏下的兄弟旋钮,同病同修——operator
944
- // 守卫集预铸成纯 DENY policy 在 governance 拍无条件施加(bypassPermissions/键缺席/settings 整缺席
945
- // 都掀不掉;deny-wins 折叠,普通目标弃权 ⇒ 不给 bypass 加 ask 门,[820] 表第四行逐字保持)。
946
- // 裁决 env/cwd 在此**单点**铸,settings 折叠的 fsWriteGate wiring 复用同一份(两处各铸一份正是
947
- // 漂移的成因)。lane 分形与 cwd 三形/哨兵的完整理由成文在下方 settings 块(修5/W4/#165),此处不复述。
948
381
  const hostSemanticsLane = !isSandboxPathAdjudicationLane(config.remoteExec?.provider);
949
382
  const pathAdjudication = hostSemanticsLane
950
383
  ? (() => {
@@ -952,7 +385,7 @@ export function createResolveSpec(ctx) {
952
385
  const gateCwd = sessionCwd ??
953
386
  (config.remoteExec === undefined
954
387
  ? process.cwd()
955
- : join(config.localDataRoot ?? localRoot, "fs-write-gate-unrooted")); // never created — see the settings block
388
+ : join(config.localDataRoot ?? localRoot, "fs-write-gate-unrooted"));
956
389
  return { env: new NodeExecutionEnv({ cwd: gateCwd }), cwd: gateCwd };
957
390
  })()
958
391
  : {
@@ -962,49 +395,19 @@ export function createResolveSpec(ctx) {
962
395
  logger: { warn: (event, fields) => logger.warn(event, { ...(fields ?? {}), lane: config.remoteExec?.provider }) },
963
396
  }),
964
397
  };
965
- // design/181 件一:governance 实参(含守卫集预铸)归 `createDeploymentGovernanceInputs`(三腿单一
966
- // 属主),**折叠属主不变**——仍是这一句 `applyRuntimeGovernance` → core `tightenTaskSpec`。每请求现调,
967
- // 读的是活 config(autonomy/commandPolicy 热改;resume 腿按当前值重折,与上方审批基线同姿势)。
968
398
  const governedBase = applyRuntimeGovernance(spec, createDeploymentGovernanceInputs(config, pathAdjudication));
969
- // Fold the client's per-request settings stamp onto the governed spec, TIGHTEN-ONLY (deny-wins) — AFTER
970
- // the approval baseline + operator governance, so the order is deployment ⊇ operator ⊇ client (each can only
971
- // narrow). tightenTaskSpec THROWS if the client settings would LOOSEN a stricter baseline → a 422 client error
972
- // (a settings stamp is tighten-only by contract; never a silent weakening). Re-applies idempotently on resume.
973
399
  let governed = governedBase;
974
- // R4: fold a LIGHT top-level `body.permissionMode` onto the parsed settings (the explicit
975
- // per-turn intent WINS over a bundle `defaultMode`) → the SAME tighten-only governance path. Post-[816] all
976
- // five CC modes are honored as gate-SHAPE choices (see coercePermissionMode/deriveSettingsPolicy): `plan` ⇒
977
- // read-only hands + present_plan; `default`/`auto` ⇒ the fs-write ask gate; `acceptEdits` ⇒ the cwd-domain
978
- // variant; `bypassPermissions` ⇒ no mode gate (the deployment baseline is composed above and untouchable).
979
- // design/201 §6-1:折叠也读**同一只** effMode(顶层键赢过 bundle defaultMode 的裁定在
980
- // `effectivePermissionMode` 里说一次)。effMode 来自 bundle 时这里把同值原样写回,是恒等折叠;
981
- // 缺席时整条 settings 原样透传(无表态请求的 pre-[816] 行为不变)。
982
400
  let effectiveSettings = effMode ? withPermissionMode(parsedSettings.settings, effMode) : parsedSettings.settings;
983
- // codex R3 second bypass: settings.outputStyle ALSO lands in spec.appendSystemPrompt (applyTaskSettings
984
- // fold) — on an append-less pack core would discard it silently, exactly like the top-level rider. Same
985
- // disposition: fresh submits 400 at the HTTP gate; here (the resume mirror + any leg the gate didn't see)
986
- // strip + warn. Spread-copy, never mutate parsedSettings (it may be consulted elsewhere).
987
401
  if (appendLessPack && effectiveSettings?.outputStyle) {
988
402
  logger.warn("output_style_dropped", { detail: "the effective prompt pack cannot mount the append slot (already-assembled systemPrompt or center assembled-identity declaration)", sessionId: auth?.sessionId ?? null });
989
403
  const { outputStyle: _dropped, ...rest } = effectiveSettings;
990
404
  effectiveSettings = Object.keys(rest).length > 0 ? rest : undefined;
991
405
  }
992
- // codex R14 resume mirror of the aggregate cap: both carriers fold into ONE spec field — a pre-cap stored
993
- // body can be individually valid but combined over the bound (fresh submits 400'd this in prepareSpec).
994
- // Drop the STYLE (it composes second; the knowledge rider keeps priority) + warn, never silently oversize.
995
406
  if (effectiveSettings?.outputStyle && acceptedAppend && acceptedAppend.length + 2 + effectiveSettings.outputStyle.length > MAX_SETTINGS_OUTPUT_STYLE_CHARS) {
996
407
  logger.warn("output_style_dropped", { detail: `combined append carriers exceed the ${MAX_SETTINGS_OUTPUT_STYLE_CHARS} cap (rider ${acceptedAppend.length} + style ${effectiveSettings.outputStyle.length}) — style dropped (pre-cap stored body on a resume leg?)`, sessionId: auth?.sessionId ?? null });
997
408
  const { outputStyle: _dropped2, ...rest2 } = effectiveSettings;
998
409
  effectiveSettings = Object.keys(rest2).length > 0 ? rest2 : undefined;
999
410
  }
1000
- // [820]③ scratchpadDir, hoisted from the envFacts block below so the SAME per-session dir feeds BOTH the
1001
- // `# Environment` fact and the fs-write gate's exemptDirs (single-source path via scratchpadDirFor inside
1002
- // ensureScratchpadDir). HOST-semantics lanes only + session required — rationale at the envFacts consumer.
1003
- // (hostSemanticsLane 判别式已随 #177 上移到 governance 拍前——单一真源不变,只是搬了家。)
1004
- // G3([816]③/[820]③,[1840]§四):壳供 body.scratchpadDir 优先——壳(CC 形)有自己的 per-session
1005
- // scratchpad 约定路径,接受后事实/豁免/根围栏三处同源指向壳的目录(壳侧提示词与 server 写门不再
1006
- // 两个 scratchpad)。验收门 fail-closed(多租户/远程 lane/浅路径全拒收,warn 后回落自算)——
1007
- // 规则与理由集中在 acceptShellScratchpadDir。
1008
411
  const shellScratchpad = await acceptShellScratchpadDir(body.scratchpadDir, {
1009
412
  requirePrincipal: config.requirePrincipal,
1010
413
  hostSemanticsLane,
@@ -1013,44 +416,7 @@ export function createResolveSpec(ctx) {
1013
416
  const scratchpadDir = shellScratchpad ??
1014
417
  (hostSemanticsLane && auth?.sessionId ? await ensureScratchpadDir(config.localDataRoot ?? localRoot, auth.sessionId) : undefined);
1015
418
  if (effectiveSettings) {
1016
- // [816]/[820]① fs-write gate wiring —— 两形,按 lane 分:
1017
- //
1018
- // ① HOST-semantics lane(REMOTE_EXEC 未设或 "host"):core 的 createFsWriteGatePolicy 把每一个
1019
- // target/dir 交给给定 env 的**真实 fs**(exists/canonicalPath/readLink,dist 亲读),所以 env 必须
1020
- // 就是 hand 工具真正写的那块盘 —— 这里给 worker 本机的 NodeExecutionEnv(构造是纯字段赋值,不 spawn)。
1021
- //
1022
- // ② 沙箱 lane(e2b/k8s/ssh/adb/local-docker,#165):沙箱 env 由 core 在 spec **之后**才铸
1023
- // (executionEnvFactory),spec 期把 worker 本机 env 递进去会拿**别人机器上的 fs 事实**裁决
1024
- // (symlink/exists 全答错),而错误的 allow 比没有门更糟。解法是把「取 env」推迟到裁决时刻:
1025
- // `DeferredSandboxPathEnv` 是个代理,per-session slot 由工厂装饰器在铸 env 时填(boot/execution-env.ts
1026
- // 链尾),写门每次 canonicalize 都转发那台**真沙箱**的 exists/canonicalPath/fileInfo/readLink。
1027
- // 唯一不转发的是 `absolutePath`(相对路径基准之争,理由在下面那条臂与代理模块的文件头)。
1028
- // cwd 仍不铸 ⇒ 无 acceptDirs、相对路径恒 ask;scratchpad 豁免按 host 同形补回(真 canonicalize
1029
- // 在场,软链逃逸形会落回真身裁决)。sensitivePatterns 的 deny 腿与 isExempt 会话豁免探针照常施加。
1030
- //
1031
- // 修5(三路复审 W4,cwd 基准漂移):gate 的 cwd 基准=与 hands 真跑处同源,分三形:
1032
- // · session 注册过 cwd(cwdHonored 已在注册处把关)⇒ 用它——与 host factory 的 effectiveHostWorkspace
1033
- // 第一优先级同源(顶层 HTTP 任务无 parentCwd,resolver 即 session cwd)。
1034
- // · `host` factory lane 且未注册 ⇒ hands 落在 factory 才铸的 EPHEMERAL per-task 目录(spec 期不可知)。
1035
- // 此前回退 process.cwd()(服务自身启动目录)——acceptEdits 会把服务目录当 auto-allow 域、多任务共享
1036
- // 进程时判定漂移(W4)。改为 fail-safe 哨兵:一个从不创建的目录 ⇒ acceptDirs/相对路径 canonicalize
1037
- // 失败即 ask(fs-write-gate-policy dist 亲读:dir.ok=false 跳过、target 解析失败=ask);绝对路径裁决
1038
- // (hand 工具的书面契约形)与 scratchpad exempt 均不受影响。
1039
- // · provider 未设(in-process host / run-local)⇒ hands 就在本进程 cwd 跑(无 factory),process.cwd()
1040
- // 正是真工作目录,保留。
1041
- // 两形**共有**的一件(与 env/cwd 无关,故不随 lane 分叉——单一属主,两臂各抄一份正是漂移的成因):
1042
419
  const gateCommon = {
1043
- // ③ 的 sensitive-path DENY 腿已随 #177([2951] issue #29)搬到 governance 拍(见上方
1044
- // sensitivePathPolicy)——wiring 不再承运守卫集,FsWriteGateWiring.sensitivePatterns 字段一并拆除
1045
- // (#153 shellGate 同形搬家:部署级旋钮不得挂在客户端表态派生腿上)。
1046
- // [1557]§四 的 shellGate 旧家已随 #153 搬到 applyRuntimeGovernance(governance 层无条件施加,
1047
- // 见上方 governedBase)——wiring 不再承运该旋钮,FsWriteGateWiring.shellGate 字段一并拆除。
1048
- // [841]① / core 1.294 exemption seam:gate 产 ask 前查同一只 approval_exemption 店(与 decide
1049
- // remember="session" 的授予、ask 政策层探针同店同 canonical toolName 键空间)——「本会话不再询问」
1050
- // 对 fs-write 门同 turn 即时生效(父+继承子任务,ask 路径不再重入)。has() reject ⇒ core 按
1051
- // 未豁免处理(fail-closed 照 ask),与店契约的调用方纪律一致;命中打 info 审计(对齐
1052
- // approval_exempted 的短路留痕,不记路径,路径留在 core 的 allow message 里)。探针是 name-keyed
1053
- // 的(不做任何 fs 裁决),所以沙箱 lane 照接不误。
1054
420
  ...(approvalExemptionStore && auth?.sessionId
1055
421
  ? {
1056
422
  isExempt: async (toolName) => {
@@ -1064,42 +430,18 @@ export function createResolveSpec(ctx) {
1064
430
  };
1065
431
  const fsWriteGate = hostSemanticsLane
1066
432
  ? {
1067
- // env/cwd 构造已随 #177 上移(governance 拍前的 pathAdjudication 单点),此处复用同一份——
1068
- // 守卫集 DENY 腿与写门 ask 腿在同一 env/同一 cwd 基准上裁决,两腿不可能分家漂移。
1069
433
  env: pathAdjudication.env,
1070
434
  ...(pathAdjudication.cwd !== undefined ? { cwd: pathAdjudication.cwd } : {}),
1071
435
  ...(scratchpadDir ? { scratchpadDir } : {}),
1072
436
  ...gateCommon,
1073
437
  }
1074
- : // 沙箱 lane(#165):裁决 env = `DeferredSandboxPathEnv` 代理 —— 裁决时刻(必在 prepare 之后)
1075
- // 去 per-session slot 里取本 task 的**真沙箱 env**,转发 exists/canonicalPath/fileInfo/readLink。
1076
- // 于是绝对路径的 symlink 真身、存在性都是真沙箱 fs 的事实,域内软链指向守卫段照样 deny;
1077
- // slot 空/歧义 ⇒ 转发面报错 ⇒ core 判 `unresolvedSymlink` ⇒ 写门 ask + sensitive deny(fail-closed)。
1078
- //
1079
- // **`cwd` 仍不给**(显式裁定,非过渡欠款):`cwd` 同时是 core 的 `rootPath`(相对路径基准)与
1080
- // acceptEdits 的 accept 域。沙箱里那个基准是 engine 跟踪的 cwd,不是 env 自己的 cwd,server 侧要
1081
- // 铸它得先建一个 per-task 跟踪面——那是独立一件(与 host 臂哨兵同族的漂移风险)。后果:相对路径
1082
- // 恒 ask,acceptEdits 在沙箱 lane 与 default 同形(全 ask),fail-safe 方向。
1083
- //
1084
- // **scratchpad 豁免按 host 同形补回**:`/tmp/scratchpad/<sessionId>`(remote-scratchpad.ts 单一
1085
- // 真源,e2b/k8s/local-docker/ssh 四 lane;adb 无此约定故无豁免)。#156 当年不豁免是因为词法版
1086
- // 看不见软链——域内一个指向域外的软链会被判成域内;现在 exemptDirs 与写目标两侧都过真
1087
- // canonicalize,逃逸形会解出域外真身而落回真身裁决,host lane 的同一保证在这里成立了。
1088
- // 注:远程 lane 的**根围栏**admit 走的是 core 对 `spec.envFacts.scratchpadDir` 的自建根(prepare 期
1089
- // canonicalize/实体化),而那条事实在 `SANDBOX_ENV_FACTS=false` 下不发——那种配置里根围栏先拒,
1090
- // 本豁免够不着(方向更严,不是放行面);host 臂的 additionalDirectories 折叠不受影响。
438
+ :
1091
439
  (() => {
1092
440
  const sandboxScratchpadDir = isRemoteScratchpadLane(config.remoteExec?.provider) ? remoteScratchpadDirFor(auth?.sessionId) : undefined;
1093
441
  return {
1094
- env: pathAdjudication.env, // #177 单点构造(DeferredSandboxPathEnv 代理),与守卫集 DENY 腿同一份
442
+ env: pathAdjudication.env,
1095
443
  ...gateCommon,
1096
444
  ...(sandboxScratchpadDir ? { scratchpadDir: sandboxScratchpadDir } : {}),
1097
- // codex 复审 B(2026-08-05,dist 亲读+实测):core 的 canonicalizeTarget 把反斜杠 UNC 形在**入口**
1098
- // 短路成 ok:true/key=原样——不经本 env,canon 便「成功」,gate 随后就咨询豁免探针;name-keyed
1099
- // 命中即 allow,「非 POSIX 绝对形一律 ask」就被豁免会话越过(相对形/盘符形没这个洞:它们的
1100
- // canon 失败先于豁免咨询)。沙箱臂因此给探针加 canonical-key 守门:只有 `/` 开头(=真经过本
1101
- // 代理的路径面)的 key 才享豁免,其余落回 defaultWrite 的 ask。host 臂不加
1102
- // (真 fs canonicalize 的 key 可信,[841]① 原契约不动)。
1103
445
  ...(gateCommon.isExempt
1104
446
  ? {
1105
447
  isExempt: (toolName, canonicalPath) => canonicalPath.startsWith("/") ? gateCommon.isExempt(toolName) : false,
@@ -1107,11 +449,6 @@ export function createResolveSpec(ctx) {
1107
449
  : {}),
1108
450
  };
1109
451
  })();
1110
- // [1248]②/codex F2 — the workflow ask leg's session-exemption probe. Name-keyed and never fs-adjudicated,
1111
- // so it was on EVERY lane from day one (the fs gate has been too since #156, by a different route: a
1112
- // lane-specific adjudication env rather than no gate). Same store + canonical toolName key space as the
1113
- // fs-gate probe — one remember="session" grant serves both; without it the durable lane re-parks every
1114
- // later run_workflow call after an operator already granted "don't ask again this session".
1115
452
  const workflowGate = approvalExemptionStore && auth?.sessionId
1116
453
  ? {
1117
454
  isExempt: async (toolName) => {
@@ -1129,61 +466,26 @@ export function createResolveSpec(ctx) {
1129
466
  throw new HttpError(422, `settings are tighten-only and cannot loosen the deployment policy: ${e.message}`);
1130
467
  }
1131
468
  }
1132
- // subagent-hands follow-on (adversarial-review finding): a read-only (permissionMode=plan) parent that mounts
1133
- // the delegation tool used to let the CHILD get WRITABLE hands — reachable once subRunner grew an
1134
- // executionEnvFactory — which defeats plan mode's read-only contract. So a hands-read-only run mounts no
1135
- // delegation tool at all.
1136
- // 🔴 复审 2026-08-05 亲验安装包(core 5.13.x)后修口:当年那句「core 不把 handsReadOnly/shellGate 传给
1137
- // 子任务」**已过期** —— 子 spec 现在两者都继承(subagent.js / prepare-task.js 的 inheritedGateForChildren,
1138
- // 引文在 stripDelegationTools 的注释里)。本行因此是同向第二层(更严不更松),不是唯一防线;要拆得单独决定。
1139
- // `stripDelegationTools`(runtime-governance.ts,过滤器本体在那里被测钉住)按 core 单源常量
1140
- // `DEFAULT_SUBAGENT_TOOL_NAME`(="Agent")RAW 比对——5.0.0 RB-476 退役了折叠面,旧名 "Task"/"Fork" 在
1141
- // roster 层就 miss、不再需要归一(常量引用本身即防当年 1.202 改名时裸字符串失配的那类 fail-open)。
1142
469
  if (governed.handsReadOnly === true && Array.isArray(governed.tools)) {
1143
470
  governed = { ...governed, tools: stripDelegationTools(governed.tools) };
1144
471
  }
1145
- // #217(core 5.26.0 提货,板 [3499]②/[3521]④ 两次点名的 server 接线半场):零记忆面部署形把 core 的
1146
- // `NO_PERSISTENT_MEMORY_NOTICE` 组进 system prompt(core 导出但**不注入** —— 「组装归宿主」,同
1147
- // MEMORY_SAFETY 姿势)。判别式与 provider 分腿装配的全部理由在 capabilities/memory-notice.ts。
1148
- // 🔴 位置即契约:必须在 governance+settings 折完之后 —— `handsReadOnly` 是判别式的一位输入,而它可能
1149
- // 由 autonomy(governance 拍)或 permissionMode:plan(settings 拍)才被点亮;在阶段④判会漏掉这两条腿。
1150
472
  if (shouldDiscloseNoPersistentMemory({
1151
473
  memory: governed.memory,
1152
- declaredCapable: effectiveMemoryPersistenceCapable(config), // #266:同一有效值(host 腿自动 true 后披露判别式同视角)
474
+ declaredCapable: effectiveMemoryPersistenceCapable(config),
1153
475
  hands: gated.hands,
1154
476
  handsReadOnly: governed.handsReadOnly === true,
1155
477
  excludeTools: governed.excludeTools,
1156
- // core 的 `isRemoteExecutionEnv` 是鸭子类型判别,我方**所有** `REMOTE_EXEC` 车道(含 `host` ——
1157
- // remote-env-host.ts 实现了 suspendVM/workspaceHandle/execStream,亲验)都满足它。判别位因此是
1158
- // 「provider 有没有设」;未设 = in-process(core 拿 StubExecutionEnv,非 remote)。
1159
478
  remoteExecutionEnv: config.remoteExec !== undefined,
1160
479
  })) {
1161
480
  governed = { ...governed, promptProvider: withNoPersistentMemoryNotice(governed.promptProvider) };
1162
481
  }
1163
482
  return { governed, scratchpadDir, hostSemanticsLane };
1164
483
  };
1165
- /** 阶段⑥(image·envFacts·router):吃 阶段⑤的 governed + scratchpadDir/hostSemanticsLane + 场景名,吐**最终**
1166
- * TaskSpec —— per-task 镜像 fail-closed 解析并按 session 登记、envFacts 组装、scratchpad 根围栏折叠,以及 A
1167
- * 价值路由(命中 supervisor 时以 supPostureOverrides 收紧后返回)。 */
1168
484
  const applyImageFactsAndRouting = async (body, auth, governed, folded) => {
1169
485
  const { scenarioName, scratchpadDir, hostSemanticsLane } = folded;
1170
- // §7 P0.5 per-task sandbox image(用户按需选像): the caller sends a PROFILE
1171
- // (intent) — NEVER a digest (a sha256 is enumerable + caller-unbound → trusting a caller digest is fail-OPEN;
1172
- // a user could pass another tenant's digest). The TRUSTED control plane resolves profile→digest with the
1173
- // caller's principal, FAIL-CLOSED (same visibility the /v1/images/select admission uses), and registers it by
1174
- // sessionId for the k8s factory to apply per-pod. RESUME re-runs resolveSpec ⇒ re-resolves LIVE (no stale
1175
- // frozen digest), consistent with the governance live-config discipline above.
1176
- // RFC A2: a session-level SelectEnvironment binding (PROFILE intent, tool-written) is the fallback when the
1177
- // request body carries no explicit profile. Body wins (an explicit caller choice for THIS task); either way
1178
- // the profile is re-resolved FAIL-CLOSED right here — a session binding never skips re-admission, and the
1179
- // F3 boundary contract holds structurally (binding is only read at task boundaries, children inherit
1180
- // the registered ref at spawn).
1181
486
  const bodyProfile = typeof body.sandboxImageProfile === "string" && body.sandboxImageProfile.length > 0 ? body.sandboxImageProfile : undefined;
1182
- // cascade/verify mirror the HTTP-layer v1 reject (server.ts): those flows strip/replace the session, so a
1183
- // session-level binding would not reach the rungs/verifier sub-run — silently mixed environments. A body
1184
- // profile on cascade/verify is already 400-rejected up front; the session fallback simply does not apply.
1185
487
  const taskImageProfile = bodyProfile ?? (body.cascade === true || body.verify === true ? undefined : sessionEnvSelection.get(auth?.sessionId));
1186
- let resolvedImageCaps; // RFC A1: rides into envFacts
488
+ let resolvedImageCaps;
1187
489
  if (taskImageProfile !== undefined) {
1188
490
  if (config.remoteExec?.provider !== "k8s") {
1189
491
  throw new HttpError(400, "sandboxImageProfile is only supported on the k8s sandbox backend");
@@ -1200,54 +502,16 @@ export function createResolveSpec(ctx) {
1200
502
  const resolved = await resolveSandboxImageRef({
1201
503
  profile: taskImageProfile,
1202
504
  ...(capsNeeded && capsNeeded.length > 0 ? { capabilitiesNeeded: capsNeeded } : {}),
1203
- // principal comes from the auth channel (trusted header / verified JWT), NEVER the body — so a caller
1204
- // cannot widen its own visibility. explicitOperatorOk (NOT isOperator): an empty OPERATOR_PRINCIPALS must
1205
- // yield operator=false here — isOperator([],p)=true-for-all would let any caller resolve tenant-scoped
1206
- // images on an operator-less deployment (adversarial-review HIGH-1; the bake/direct-door boot guards do
1207
- // NOT cover per-task image selection).
1208
505
  viewer: { operator: explicitOperatorOk(auth.principal, config.operatorPrincipals), tenantId: auth.principal ?? null },
1209
506
  index: imageIndex,
1210
507
  });
1211
508
  if (!resolved.ok)
1212
509
  throw new HttpError(resolved.status, resolved.message);
1213
- // Register by sessionId — the factory reads it by ctx.sessionId (stable on every path; see the factory
1214
- // wrapper). RESUME re-runs resolveSpec ⇒ re-resolves LIVE + re-registers (no stale frozen digest).
1215
- // ⚠️ KNOWN narrow edge (NOT a security breach): two CONCURRENT requests for the SAME session (same owner —
1216
- // session ownership is enforced) race this set(); a request the run-claim later rejects (409) can leave its
1217
- // image bound for the active run. Both images are the SAME principal's own admitted images, so the worst
1218
- // case is the user's run using the user's other selected image (a correctness glitch, not cross-tenant).
1219
- // Hardening (register-after-claim, keyed by the durable run id) needs the registry threaded into the server
1220
- // claim path — tracked as a follow-up; cascade/verify are rejected with a profile (see prepareSpec) because
1221
- // they strip/replace the session and would silently fall back to the default image.
1222
510
  perTaskImage.set(auth.sessionId, resolved.ref);
1223
511
  resolvedImageCaps = resolved.capabilities;
1224
512
  }
1225
- // RFC A1 (core 1.240.0 `TaskSpec.envFacts`): compose the deployment-trusted sandbox facts
1226
- // (profile/capabilities from the fail-closed resolution above, region pkgSource, exec-lane egress posture)
1227
- // and ride them on the spec — core renders the `# Environment` block (sanitized+bounded, egress=none gets
1228
- // the "downloads will fail" caveat). Facts only ride when known; nothing known ⇒ no field ⇒ block unchanged.
1229
513
  if (config.envFactsEnabled) {
1230
- // pkgSource is a FACT only where the derivePkgSourceEnv injection actually lands (the e2b/k8s sandbox
1231
- // lanes — main.ts wiring above). host/ssh/adb/run-local get NO injection, so with the global default
1232
- // (1.180.0) an unconditional read would tell the model "official sources" about an environment we never
1233
- // touched (workflow audit 2026-07-13: honest-facts posture, same as egress's "don't declare what you
1234
- // don't know").
1235
514
  const pkgSourceLane = config.remoteExec?.provider === "e2b" || config.remoteExec?.provider === "k8s";
1236
- // [820]③ scratchpadDir (core field + prompt chain in since 1.257.3; this fill was the missing half).
1237
- // HOST-semantics lanes only (provider unset = in-process host, or explicit "host"): the advertised path
1238
- // must be REAL to the executing hands — on e2b/k8s/ssh/adb/local-docker the tools run off this box, so a
1239
- // worker-local path would be a lie (same honest-facts axis as pkgSource above; the remote lanes get their
1240
- // IN-SANDBOX path via the [848] branch below instead). Session-scoped dir under
1241
- // localDataRoot, mkdir'd at the HOISTED compute above the settings fold (fail → fact omitted) — the SAME
1242
- // dir now feeds createFsWriteGatePolicy's exemptDirs ([820]① landed at core 1.290, wired in the
1243
- // applyTaskSettings gate above). No sessionId (adhoc no-session task) ⇒ no per-session home ⇒ fact
1244
- // omitted. Lifecycle/reaping: see ensureScratchpadDir docs (deferred).
1245
- // [848] remote lanes' counterpart: on e2b/k8s/local-docker/ssh the withRemoteScratchpad-decorated
1246
- // factory `mkdir -p`s exactly this path on the env's first exec, so advertising it is honest — with
1247
- // the one best-effort caveat (a failed mkdir leaves the advertised path absent; see the decorator
1248
- // header). Mutually exclusive with the host-lane fact above by construction (hostSemanticsLane owns
1249
- // scratchpadDir; this branch is remote lanes only), and it applies the decorator's OWN validity rule
1250
- // (remoteScratchpadDirFor: absent/unsafe sessionId ⇒ no decoration ⇒ no fact). adb stays factless.
1251
515
  const remoteScratchpadDir = !hostSemanticsLane && isRemoteScratchpadLane(config.remoteExec?.provider)
1252
516
  ? remoteScratchpadDirFor(auth?.sessionId)
1253
517
  : undefined;
@@ -1258,36 +522,17 @@ export function createResolveSpec(ctx) {
1258
522
  ...(pkgSourceLane ? { pkgSource: config.sandboxPkgSource } : {}),
1259
523
  egress: egressForRemoteExec(config.remoteExec),
1260
524
  ...(factScratchpadDir ? { scratchpadDir: factScratchpadDir } : {}),
1261
- // [1467]②(core 1.360)durable resume 铸句真值:按车道能力面,单真源 resumeFactsForLane
1262
- // (e2b=双 preserved/k8s=按 s3Snapshot 有效能力分叉/ssh·adb·host=scratch preserved/
1263
- // local-docker=不声明)。
1264
525
  resumeFacts: resumeFactsForLane(config.remoteExec?.provider, config.remoteExec?.provider === "k8s" ? { k8sSnapshot: Boolean(config.remoteExec.s3Snapshot) } : undefined),
1265
526
  });
1266
527
  if (facts)
1267
528
  governed = { ...governed, envFacts: facts };
1268
529
  }
1269
- // [1339] scratchpad 写门失效真因:提示词(envFacts.scratchpadDir)和 policy 层豁免(exemptDirs)都
1270
- // 接了,但 core fs 工具的**根围栏**(resolveKey:taskRootPath+additionalDirectories)在 policy 之前
1271
- // 短路——Write/Edit 到 scratchpad 直接 path_not_in_root,exemptDirs 根本没被问到(Bash 无根围栏,
1272
- // 反而能写=口径矛盾还教模型绕门)。修:host 车道把同一 scratchpadDir 折进 spec.additionalDirectories
1273
- // (server 自算的 per-session 路径,非租户输入,不走 cwdHonored 门限;compute 时已 mkdir,core 的
1274
- // canonicalPath 必过)。remote 车道暂不折:in-sandbox 路径由 [848] 装饰器首 exec 才 mkdir,prepare 期
1275
- // canonicalize 会失败被 core 静默跳过——那半场随 core 排序案另行。
1276
- // codex R3(M2):**独立于 envFactsEnabled 门**——SANDBOX_ENV_FACTS=false 只关事实展示,写门围栏
1277
- // 豁免是行为契约,关展示不得顺带关围栏(否则该配置下 [1339] 原病复发)。
1278
530
  if (scratchpadDir !== undefined) {
1279
531
  const dirs = governed.additionalDirectories ?? [];
1280
532
  if (!dirs.includes(scratchpadDir))
1281
533
  governed = { ...governed, additionalDirectories: [...dirs, scratchpadDir] };
1282
534
  }
1283
- // A value router (S1 verdict §6.3 cash-out, src/router/route-orchestration.ts): auto-decide this task's
1284
- // orchestration. CONSERVATIVE + DETERMINISTIC — default single; escalate to the SUP prevention posture only
1285
- // on a capability-danger signal (write/exec on a NON-isolated env — an isolated sandbox CONTAINS the harm,
1286
- // so isolated tasks stay single, matching the verdict's default). caller-explicit team (council/debate/team)
1287
- // is honored, never auto-produced. Flag-gated (ROUTER_ENABLED) — ships dark, enable after live validation.
1288
535
  if (config.routerEnabled) {
1289
- // Resolve isolation from provider AND runtimeClass — a k8s worker on plain runc is NOT VM-isolated
1290
- // (review HIGH); fail-closed for runc/empty/unknown.
1291
536
  const isolatedExecEnv = isIsolatedExecEnv(config.remoteExec?.provider, config.remoteExec?.provider === "k8s" ? config.remoteExec.runtimeClass : undefined);
1292
537
  const decision = routeServiceTask({
1293
538
  isolatedExecEnv,
@@ -1297,10 +542,6 @@ export function createResolveSpec(ctx) {
1297
542
  });
1298
543
  logger.info("orchestration_routed", { mode: decision.mode, reason: decision.reason, sessionId: auth?.sessionId ?? null });
1299
544
  if (decision.mode === "supervisor") {
1300
- // SUP prevention posture: gate every bash (shellGate:"always") AND the direct write hand tools
1301
- // (edit_file/write_file via toolPolicy) — the verdict's gate-deny on capability-danger. tighten-only
1302
- // (composes with the governance/approval baseline; never loosens). The worker's existing approval
1303
- // machinery (durable/poll) enforces the suspend — same precondition as autonomy "ask".
1304
545
  return tightenTaskSpec(governed, supPostureOverrides());
1305
546
  }
1306
547
  }