@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,19 +1,4 @@
1
1
  import { redactSecrets } from "../trace/redact.js";
2
- /**
3
- * The gate sources core's `PermissionDeniedPayload.source` names. Treated as an OPEN enum on the WIRE
4
- * (a core newer than this build may send a word this table has never heard of — that must not explode
5
- * the Prometheus label set), but as a CLOSED SET against the core we compile with.
6
- *
7
- * 🔴 core 5.23.0([3372] 提货批件②):这张表**曾是手抄的**(`new Set([...五个字面量])`,与 core 的类型
8
- * 零编译期联系),于是 core 先加 `classifier`、又在 5.23.0(design/182 §7)加 `org` 时,两个**真词**都
9
- * 被静默折进了 `other`。那不是「少一个标签」——`permission_denied_total{source="other"}` 的告警语义是
10
- * 「引擎发了本 build 不认识的词,去对表」,真词混进来就把这个信号读废了,而 `org`(组织治理层的拒)
11
- * 恰恰是运维最该单独看见的一类。
12
- *
13
- * ⇒ 表改为 `Record<PermissionDeniedSource, true>` 穷举:**core 增删一个词,本表编译期先红**(闭集词表
14
- * 禁手抄、必须从 core 类型推导的成文纪律)。运行期配对锚在 `test/tool-trace.test.ts`(从装树 core 的
15
- * `.d.ts` 解析联合词表逐词断言),因为 vitest 走 esbuild 不查类型 —— 两道锚缺一不可。
16
- */
17
2
  const KNOWN_DENY_SOURCE_TABLE = {
18
3
  policy: true,
19
4
  hook: true,
@@ -23,21 +8,9 @@ const KNOWN_DENY_SOURCE_TABLE = {
23
8
  classifier: true,
24
9
  org: true,
25
10
  };
26
- /** 表内 ⇒ 收窄成 `PermissionDeniedSource`(类型守卫)。计量点用它判「这个词是不是本 build 认得的真词」。
27
- * 导出是为了让运行期锚(`test/tool-trace.test.ts`,从装树 core 的 `.d.ts` 解析真词表)**复用同一个谓词**
28
- * —— 测试自己另写一份判据 = 又一张手抄表,正是本文件刚消灭的那个病。 */
29
11
  export function isKnownDenySource(v) {
30
12
  return typeof v === "string" && Object.hasOwn(KNOWN_DENY_SOURCE_TABLE, v);
31
13
  }
32
- /**
33
- * ALWAYS-ON gate-deny meter (goal B4, 2026-07-08): `permission_denied_total{source}` — how often the
34
- * adjudicate chain DENIES a tool call, by gate source. Deliberately SEPARATE from {@link createToolTracer}:
35
- * the tracer is a default-OFF diagnostic (TOOL_TRACE=true), but deny frequency is an operational/security
36
- * signal that must reach /metrics on every deployment — a spike in `source="policy"` means a task keeps
37
- * asking for something the operator forbids; `source="safety"`/`"shellGate"` spikes flag risky-command
38
- * pressure. main.ts composes this UNDER the tracer via composeHooks (both are void observers on the
39
- * permissionDenied slot, which composeHooks double-runs deployment-first).
40
- */
41
14
  export function createPermissionDeniedMeter(metrics) {
42
15
  return {
43
16
  permissionDenied(payload) {
@@ -45,34 +18,6 @@ export function createPermissionDeniedMeter(metrics) {
45
18
  },
46
19
  };
47
20
  }
48
- /**
49
- * prove-before-scale stopgap (design/50): a no-op observer hook that logs every tool call (pre + post)
50
- * to the worker log — INCLUDING resumed legs that `GET /v1/runs/:id/events` does NOT surface (the
51
- * streaming-resume observability gap, service[15] API request #2). Gated by `TOOL_TRACE=true`; OFF by
52
- * default (no prod noise).
53
- *
54
- * Why: a durable-HITL-gated coding task that keeps re-suspending is ambiguous from /events alone —
55
- * could be a model restart-on-resume loop (the SAME tool re-issued each resume) or slow legit iteration.
56
- * The pre-hook logs `{tool, id, arg:{path,contentLen,…}}` per call, so grepping `tool_trace` reconstructs
57
- * the full sequence: a repeated `write_file path=X contentLen=Y` across resumes ⇒ restart-loop;
58
- * write X → write Y → bash ⇒ iteration.
59
- *
60
- * Pure observation: every hook returns `undefined`, so it never rewrites args, never gates, never alters
61
- * the result (and per core's hooks invariant a hook's `allow` can't bypass the policy gate anyway).
62
- *
63
- * 🔴 core 5.20.0 起这条「纯观察」性质必须**声明**出来(`preToolUseObservational`),不能只写在注释里:
64
- * core 5.19.0 的委派折叠按「PreToolUse 席位在不在」把父的 screening 面折进子代的祖先约束链——它分不出
65
- * 诊断 tracer 与真裁决者。后果是**这个默认关的诊断旋钮**一开,该部署所有 hook-wired 父的 parked 子代
66
- * checkpoint 就记 `parentConstraintCount=2`,而跨副本赎回腿(`boot/parked-revive-gate.ts`)恒供 1 ⇒
67
- * core pre-CAS 响亮拒、行永久 pending(层数 park 时写死,事后关旋钮/重批/回滚都不改)。
68
- * 声明之后该面不铸链条目,count 回到 1,病路消灭。
69
- * ⛔ 打这个标的前提是**真的一条裁决都不出**:core 对声明了却返回判词的面是「拒绝采纳 + onError 告警」
70
- * (classification `observational-hook-verdict-ignored`),即判词被静默丢弃——把标打到会裁决的面上比原
71
- * 缺陷更坏。本文件三个 pre/post 回调恒返 `undefined`,`permissionDenied` 本就是 void 观察槽。
72
- * 📎 连带的**可观测**变化(不是回归):core 的「可写工具面无 effect-aware 门」启动告警此前把本 tracer
73
- * 算作一道门而被抑制;声明之后它不再算,于是「没配任何 policy 层 + 开着 TOOL_TRACE」的部署升级后会
74
- * 多出那行告警。方向正确——tracer 从来没有门住任何东西,原先的抑制本身就是一次静默 fail-open。
75
- */
76
21
  export function createToolTracer(logger) {
77
22
  return {
78
23
  preToolUseObservational: true,
@@ -90,10 +35,6 @@ export function createToolTracer(logger) {
90
35
  });
91
36
  return undefined;
92
37
  },
93
- // core 1.252 (design/134) BREAKING: postToolUse and postToolUseFailure are MUTUALLY EXCLUSIVE — an
94
- // isError result now fires ONLY this callback. Without it, every failed tool call silently vanished
95
- // from the trace (the exact silent-fallback class this log exists to catch). Same line shape as the
96
- // success path (phase "post", isError true) so existing `tool_trace` grep workflows keep working.
97
38
  postToolUseFailure(toolName, _input, failure, ctx) {
98
39
  logger.info("tool_trace", {
99
40
  phase: "post",
@@ -106,12 +47,6 @@ export function createToolTracer(logger) {
106
47
  });
107
48
  return undefined;
108
49
  },
109
- // core 1.257: the gate's DENY short-circuit never reaches postToolUse* (the call
110
- // doesn't execute), so denied calls were INVISIBLE to this trace — the exact observability hole
111
- // asked about. `source` = core's adjudicate-chain enum (the closed set is `KNOWN_DENY_SOURCE_TABLE`
112
- // above — deliberately NOT re-listed here, a second hand-written copy is how the meter drifted); the
113
- // deny `reason` can quote the adjudicated args (an approver's text, a policy message), so redact+clip
114
- // like every other line. Same `tool_trace` key so existing grep workflows see denies in sequence.
115
50
  permissionDenied(payload) {
116
51
  logger.info("tool_trace", {
117
52
  phase: "denied",
@@ -124,15 +59,11 @@ export function createToolTracer(logger) {
124
59
  },
125
60
  };
126
61
  }
127
- /** Compact, restart-diagnosable summary: key path/command fields + content length (not full content). */
128
62
  function summarize(input) {
129
63
  if (!input || typeof input !== "object")
130
64
  return { v: clip(String(input), 120) };
131
65
  const o = input;
132
66
  const s = {};
133
- // Redact secrets BEFORE clipping: a traced `command`/`old_string` (e.g. `curl -H "Authorization: Bearer …"`,
134
- // an edited .env line) can carry credentials into the worker log → its aggregation pipeline. TOOL_TRACE is a
135
- // default-off diagnostic, but 120 chars is enough to hold a full token, so mask it.
136
67
  for (const k of ["path", "file_path", "command", "cmd", "pattern", "glob", "old_string", "new_string"]) {
137
68
  if (typeof o[k] === "string")
138
69
  s[k] = clip(redactSecrets(o[k]), 120);
@@ -146,7 +77,7 @@ function textOf(content) {
146
77
  .map((c) => (c && typeof c === "object" && typeof c.text === "string" ? c.text : ""))
147
78
  .join("")
148
79
  .trim();
149
- return clip(redactSecrets(text), 200); // tool output can echo a cat'd secret/credential — mask before logging
80
+ return clip(redactSecrets(text), 200);
150
81
  }
151
82
  function clip(s, n) {
152
83
  return s.length > n ? `${s.slice(0, n)}…(${s.length})` : s;
@@ -7,17 +7,7 @@ const DEFAULTS = {
7
7
  concurrency: 12,
8
8
  maxHeapMb: 128,
9
9
  };
10
- /** Own keys that carry a prototype-tampering gadget across the JSON membrane. */
11
10
  const PROTO_TAMPER_KEYS = ["__proto__", "constructor", "prototype"];
12
- /**
13
- * Recursively delete own `__proto__` / `constructor` / `prototype` keys from JSON-parsed data. Closes the
14
- * tampering gadget a script passes across the membrane: `Reflect.defineProperty(o, "__proto__",
15
- * {enumerable:true, value:{...}})` (Reflect is reachable; `codeGeneration:{strings:false}` does NOT block it)
16
- * makes `__proto__` an OWN ENUMERABLE key, which in-context `JSON.stringify` emits and host `JSON.parse`
17
- * re-materializes as a real own data property — feeding a host fn (or the returned result) a crafted
18
- * prototype a downstream deep-merge / key-assignment sink splats onto the HOST realm's `Object.prototype`
19
- * (process-wide, cross-tenant). Caught by the s8-sandbox-robustness marshal-bridge lens (core's battery + the
20
- * result-path probes both miss it). Apply ONLY to JSON.parse output (plain, acyclic, getter-free). */
21
11
  function scrubProtoDeep(value) {
22
12
  if (value === null || typeof value !== "object")
23
13
  return;
@@ -34,19 +24,6 @@ function scrubProtoDeep(value) {
34
24
  scrubProtoDeep(value[key]);
35
25
  }
36
26
  }
37
- /**
38
- * [1818]§二邻位 — script-realm rejection guard. The vm context shares the HOST isolate's unhandledRejection
39
- * machinery, and Node's default posture is CRASH: an untrusted script's un-awaited rejection (a bare
40
- * `(async () => { throw ... })()`, an un-awaited `agent()`/`phase()` whose body fails) would take the whole
41
- * server process down — a resource/availability hole, not a data-boundary one (clay's live incident hit the
42
- * sibling shape that HANGS instead of rejecting, which is the only reason it didn't crash). There is no
43
- * per-context hook in Node, so the guard is a process listener that tells realms apart by intrinsic:
44
- * a promise minted INSIDE the context fails `instanceof` against the HOST `Promise`. Context-realm rejections
45
- * are contained (logged, never fatal); host-realm rejections keep Node's fail-loud default (rethrown as an
46
- * uncaughtException) UNLESS the deployment installed its own handler — then we defer to it entirely.
47
- * Installed lazily on the first hardened run: only processes that actually execute untrusted scripts change
48
- * their global posture, and only then is the hazard live. Idempotent.
49
- */
50
27
  let rejectionGuardInstalled = false;
51
28
  export function installScriptRealmRejectionGuard() {
52
29
  if (rejectionGuardInstalled)
@@ -54,32 +31,24 @@ export function installScriptRealmRejectionGuard() {
54
31
  rejectionGuardInstalled = true;
55
32
  process.on("unhandledRejection", (reason, promise) => {
56
33
  if (!(promise instanceof Promise)) {
57
- // cross-realm ⇒ minted in a vm context ⇒ an un-awaited rejection inside an untrusted script. Contain.
58
34
  const summary = reason instanceof Error ? `${reason.name}: ${reason.message}` : String(reason);
59
35
  try {
60
36
  console.warn(`hardened-vm: un-awaited script rejection contained: ${summary}`);
61
37
  }
62
38
  catch {
63
- /* the guard must never become a crash source itself */
64
39
  }
65
40
  return;
66
41
  }
67
- // Host-realm: preserve fail-loud unless another handler exists (a deployment that installed one chose
68
- // its own posture — a rethrow here would override that choice).
69
42
  if (process.listenerCount("unhandledRejection") > 1)
70
43
  return;
71
44
  throw reason;
72
45
  });
73
46
  }
74
- /** Host-side parse that strips the tampering gadget from data crossing INTO a host fn. */
75
47
  function sterilizingParse(json) {
76
48
  const parsed = JSON.parse(json);
77
49
  scrubProtoDeep(parsed);
78
50
  return parsed;
79
51
  }
80
- /** Marshal a (possibly live cross-realm) script value to PLAIN, sterilized host data: JSON round-trips it
81
- * (firing in-context getters/toJSON harmlessly in-context → plain data; drops functions; no live context
82
- * object with traps reaches the host) then strips the tampering gadget. Throws on a non-serializable result. */
83
52
  function marshalOut(value) {
84
53
  let json;
85
54
  try {
@@ -90,21 +59,14 @@ function marshalOut(value) {
90
59
  }
91
60
  return sterilizingParse(json === undefined ? "null" : json);
92
61
  }
93
- /**
94
- * The shared hardened-vm core. Builds the membrane, lets `buildGlobals` construct the context-local globals,
95
- * runs the untrusted `body` as an async function under the caps, and returns the MARSHALLED (plain, sterilized)
96
- * result. Both public surfaces delegate here.
97
- */
98
62
  async function runHardened(opts) {
99
63
  const cfg = { ...DEFAULTS, ...opts.limits };
100
64
  const { signal } = opts;
101
65
  if (signal?.aborted)
102
66
  throw new WorkflowScriptError("hardened-vm aborted before start");
103
67
  installScriptRealmRejectionGuard();
104
- // 1. hardened context
105
68
  const sandbox = { __proto__: null };
106
69
  const ctx = vm.createContext(sandbox, { codeGeneration: { strings: false, wasm: false } });
107
- // 2. determinism guard (resume-safety; matches CC): nondeterministic clocks/RNG throw in-context.
108
70
  vm.runInContext(`(() => {
109
71
  const block = (l) => { throw new Error(l + " is unavailable in workflow scripts (nondeterministic — breaks resume). Pass timestamps via args; vary by index for randomness."); };
110
72
  Date.now = () => block("Date.now()");
@@ -121,30 +83,17 @@ async function runHardened(opts) {
121
83
  globalThis[n] = () => noTimer(n + "()");
122
84
  }
123
85
  })()`, ctx);
124
- // 3. membrane helpers (factory bodies COMPILED IN-CONTEXT → context-rooted prototypes)
125
86
  const ctxJsonParse = vm.runInContext("(s => JSON.parse(s))", ctx);
126
87
  const dataIn = (v) => {
127
- // sterilize the gadget host-side BEFORE it re-materializes in-context (a __proto__ own key would otherwise
128
- // pollute the CONTEXT's Object.prototype — sandboxed, but cheap to close).
129
88
  const clean = JSON.parse(JSON.stringify(v === undefined ? null : v));
130
89
  scrubProtoDeep(clean);
131
90
  return ctxJsonParse(JSON.stringify(clean));
132
91
  };
133
- // a {ok,v} | {ok:false,err,name} JSON round-trip; the IN-CONTEXT wrapper throws a CONTEXT-LOCAL Error on
134
- // failure so a host error never reaches the script (beyond-battery fix).
135
92
  const mkBridge = vm.runInContext(`(hostCall => async function (...a) {
136
93
  const r = JSON.parse(await hostCall(JSON.stringify(a)));
137
94
  if (r.ok) return r.v;
138
95
  const e = new Error(r.err); e.name = r.name || "Error"; throw e;
139
96
  })`, ctx);
140
- // [1818]§二 — quiescence deadlock detector. Inside the membrane the ONLY way a suspended script can ever be
141
- // resumed is a bridged host call settling: timers are blocked, there is no I/O, and every other await is a
142
- // microtask chain that drains on its own. So "zero bridged calls in flight + the microtask AND macrotask
143
- // queues have both drained + scriptPromise still pending" is not a heuristic — it PROVES the script can never
144
- // progress again (clay's live shape: an un-awaited `const r = phase(...)` whose body returns `r` → a promise
145
- // cycle that neither rejects nor resolves; pre-fix it burned the full totalTimeoutMs blind and then blamed
146
- // the timeout). setImmediate (a macrotask) is the drain fence: any continuation the settling host call woke
147
- // runs first, and a continuation that starts a new bridged call increments the counter synchronously.
148
97
  let inFlightHostCalls = 0;
149
98
  let scriptSettled = false;
150
99
  let fireDeadlock;
@@ -160,8 +109,6 @@ async function runHardened(opts) {
160
109
  const bridge = (hostFn) => mkBridge(async (argsJson) => {
161
110
  inFlightHostCalls++;
162
111
  try {
163
- // sterilizingParse (not bare JSON.parse): strip a slipped-in __proto__/constructor/prototype gadget
164
- // BEFORE the args reach the host fn — the dangerous (script→host) direction.
165
112
  const out = await hostFn(...sterilizingParse(argsJson));
166
113
  return JSON.stringify({ ok: true, v: out ?? null });
167
114
  }
@@ -178,7 +125,6 @@ async function runHardened(opts) {
178
125
  }
179
126
  });
180
127
  const membrane = { bridge, dataIn, ctxEval: (src) => vm.runInContext(src, ctx) };
181
- // 4. expose the surface's context-local globals + run the body under the sync vm timeout
182
128
  for (const [name, value] of Object.entries(opts.buildGlobals(membrane)))
183
129
  sandbox[name] = value;
184
130
  let invocation;
@@ -188,14 +134,9 @@ async function runHardened(opts) {
188
134
  catch (err) {
189
135
  throw new WorkflowScriptError(`script failed to compile: ${err instanceof Error ? err.message : String(err)}`);
190
136
  }
191
- // `timeout` bounds the SYNCHRONOUS run up to the first await (a sync infinite loop is killed here); an async
192
- // loop is bounded by the wall-clock race below.
193
137
  const scriptPromise = invocation.runInContext(ctx, { timeout: cfg.syncTimeoutMs });
194
138
  scriptPromise.then(() => (scriptSettled = true), () => (scriptSettled = true));
195
- // Initial probe: a script that suspends without EVER touching the bridge (`await new Promise(() => {})`)
196
- // never decrements the counter, so the settle-side probe alone would miss it.
197
139
  probeQuiescence();
198
- // 5. wall-clock + external-abort + quiescence-deadlock race
199
140
  let timer;
200
141
  let onAbort;
201
142
  try {
@@ -208,24 +149,14 @@ async function runHardened(opts) {
208
149
  onAbort = () => reject(new WorkflowScriptError("hardened-vm aborted"));
209
150
  signal.addEventListener("abort", onAbort, { once: true });
210
151
  }
211
- // Diagnosis-first message: it reaches the model via failureSummary (bounded), so the self-correction
212
- // hint has to be up front.
213
152
  fireDeadlock = () => reject(new WorkflowScriptError("script deadlocked: no host call (agent/phase) in flight and nothing can resume the script — usually an un-awaited promise cycle, e.g. `const r = phase(...)` without await whose body reads `r`."));
214
153
  }),
215
154
  ]);
216
- // hand the host PLAIN sterilized data, never a live cross-realm object (getter/toJSON/then traps) and
217
- // never a slipped-in __proto__ (return-path tampering).
218
155
  return marshalOut(result);
219
156
  }
220
157
  catch (err) {
221
- // Live dogfood catch 2026-07-03 ( `setTimeout` blew up as a bare "workflow failed"): a RUNTIME script-realm
222
- // rejection (ReferenceError, an uncaught script throw, a bridged agent() error the script didn't catch) used
223
- // to propagate UN-typed — `failureSummary` only surfaces the message for `workflow.*`-coded errors, so the model/
224
- // operator got zero diagnosis. Wrap it as `workflow.script_error`; core's failureSummary already bounds (200
225
- // chars) + `redactHostLeaks`-redacts the message before it reaches the notification, so a bridged host error
226
- // message is safe to carry here.
227
158
  if (err instanceof WorkflowScriptError)
228
- throw err; // compile/timeout/abort/marshal — already typed
159
+ throw err;
229
160
  throw new WorkflowScriptError(`script failed at runtime: ${err instanceof Error ? err.message : String(err)}`);
230
161
  }
231
162
  finally {
@@ -235,23 +166,6 @@ async function runHardened(opts) {
235
166
  signal.removeEventListener("abort", onAbort);
236
167
  }
237
168
  }
238
- /**
239
- * phases=[] ROOT CAUSE + fix — the SCOPED-PHASE GATE. The old in-context `phase` was pure sugar
240
- * (log a "◆ phase" line, run the body) that NEVER called the host `primitives.phase`, so core recorded ZERO
241
- * phase state: terminal rows had `phases: []` and agents carried no `phase` key (exactly the storage-row
242
- * autopsy — agents fine, phase accounting gone). The membrane's JSON bridge cannot carry the script's BODY
243
- * function across, so the bridge is split into three JSON-safe host calls, and the body stays IN-CONTEXT:
244
- * - `mark(title)` → `primitives.phase(title, undefined)` (core's bare CC-marker path);
245
- * - `enter(title)` → opens core's scoped phase with a DEFERRED body (`await gate`), returns a numeric token.
246
- * core pushes the phase record + sets `currentPhase` SYNCHRONOUSLY before its first await, so by the time
247
- * `enter` returns, `agent()` calls made inside the VM body land with the right phase label;
248
- * - `exit(token, ok, err?)` → releases the gate (ok → core closes the phase `completed`; !ok → the deferred
249
- * body throws and core closes it `failed`), then awaits core's settle so the close is persisted before the
250
- * VM continues.
251
- * A finalized-run `phase()` (core throws) surfaces on `enter` via the settled-race; an unknown/double `exit`
252
- * is idempotent. The VM wrapper rethrows the body's own error — the gate's synthetic error never reaches the
253
- * script.
254
- */
255
169
  export function scopedPhaseGate(phase) {
256
170
  let seq = 0;
257
171
  const open = new Map();
@@ -264,9 +178,7 @@ export function scopedPhaseGate(phase) {
264
178
  if (!r.ok)
265
179
  throw new Error(r.err ?? "phase body failed");
266
180
  }));
267
- settled.catch(() => undefined); // the VM path rethrows the body's own error — never an unhandled rejection here
268
- // Surface an IMMEDIATE rejection (core's finalized guard rejects before its first await settles) while a
269
- // healthy still-pending phase resolves the race via the resolved sentinel.
181
+ settled.catch(() => undefined);
270
182
  await Promise.race([settled, Promise.resolve()]);
271
183
  const token = ++seq;
272
184
  open.set(token, { release, settled });
@@ -275,19 +187,13 @@ export function scopedPhaseGate(phase) {
275
187
  async exit(token, ok, err) {
276
188
  const o = open.get(token);
277
189
  if (!o)
278
- return; // unknown/double exit — idempotent
190
+ return;
279
191
  open.delete(token);
280
192
  o.release({ ok, ...(err !== undefined ? { err } : {}) });
281
- await o.settled.catch(() => undefined); // core closed + persisted the phase before the VM continues
193
+ await o.settled.catch(() => undefined);
282
194
  },
283
195
  };
284
196
  }
285
- /** Build the orchestration context-local globals (agent/parallel/pipeline/phase/log/budget/args) from the
286
- * host {@link WorkflowPrimitives}. agent/log/phase are host-bridged (phase via the three-verb
287
- * {@link scopedPhaseGate} — the body function itself never crosses the membrane); parallel/pipeline are
288
- * in-context sugar; budget is a frozen null-proto object over host accessors.
289
- * Exported so the worker-isolated runner (hardened-vm-worker.ts) reuses the EXACT same surface with
290
- * IPC-backed primitives — one definition of the orchestration globals, no drift. */
291
197
  export function orchestrationGlobals(primitives, scriptArgs, limits = {}) {
292
198
  const cfg = { ...DEFAULTS, ...limits };
293
199
  return (m) => {
@@ -312,8 +218,6 @@ export function orchestrationGlobals(primitives, scriptArgs, limits = {}) {
312
218
  await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
313
219
  return out;
314
220
  })`)(cfg.concurrency);
315
- // Phase-gate fix: REAL phase bridging via the three-verb gate (see scopedPhaseGate) — the old in-context sugar
316
- // never told core a phase existed (phases:[] / label-less agents in every hardened-runner deployment).
317
221
  const gate = scopedPhaseGate((t, b) => primitives.phase(t, b));
318
222
  const phaseMark = m.bridge(async (title) => void (await primitives.phase(String(title), undefined)));
319
223
  const phaseEnter = m.bridge(async (title) => gate.enter(String(title)));
@@ -327,9 +231,6 @@ export function orchestrationGlobals(primitives, scriptArgs, limits = {}) {
327
231
  return { agent, parallel, pipeline, phase, log, budget, args: m.dataIn(scriptArgs) };
328
232
  };
329
233
  }
330
- /** Build the hardened-vm S8 `WorkflowScriptRunner` (TOC+TOB workflow orchestration). `safeForUntrustedScripts`
331
- * is `true` — it has passed the conformance battery + robustness suite; the S8 gate mounts `run_workflow` only for
332
- * a runner that asserts this. */
333
234
  export function createHardenedVmRunner(limits = {}) {
334
235
  const cfg = { ...DEFAULTS, ...limits };
335
236
  return {
@@ -342,25 +243,10 @@ export function createHardenedVmRunner(limits = {}) {
342
243
  ...(signal ? { signal } : {}),
343
244
  buildGlobals: orchestrationGlobals(primitives, scriptArgs, cfg),
344
245
  });
345
- // `meta` is core-parsed (AST, not eval) but its values are LLM-controlled — sterilize it too.
346
246
  return { result, meta: marshalOut(meta) };
347
247
  },
348
248
  };
349
249
  }
350
- /**
351
- * General untrusted-code exec on the SAME hardened membrane — the TOC ephemeral execution sandbox. Runs
352
- * `code` (an async function body that may `return`) with ONLY the safe built-ins plus whatever host
353
- * capabilities the caller bridges via `bindings` (each MUST go through `m.bridge` / `m.dataIn` — never a raw
354
- * host reference). Returns the marshalled (plain, sterilized) result. With no `bindings`, the code is pure
355
- * computation (no host reach at all).
356
- *
357
- * @example
358
- * const out = await runInHardenedVm({
359
- * code: "return await fetchJson(args.url)",
360
- * bindings: (m) => ({ fetchJson: m.bridge((u) => safeFetch(String(u))), args: m.dataIn({ url }) }),
361
- * limits: { totalTimeoutMs: 5000 },
362
- * });
363
- */
364
250
  export async function runInHardenedVm(opts) {
365
251
  return runHardened({
366
252
  body: opts.code,
@@ -2,26 +2,9 @@ import { Worker } from "node:worker_threads";
2
2
  import { WorkflowScriptError } from "@sema-agent/core";
3
3
  import { scopedPhaseGate } from "./hardened-vm-runner.js";
4
4
  const DEFAULTS = { totalTimeoutMs: 600_000, syncTimeoutMs: 5_000, maxAgents: 1000, concurrency: 12, maxHeapMb: 128 };
5
- // Resolve the worker bootstrap next to this module. In a tsc build it's the sibling `.js`; under vitest/tsx
6
- // (this module's URL ends in `.ts`) point at the `.ts` and register tsx as the worker's loader so the worker
7
- // can import TypeScript. Prod (node dist/) never needs tsx.
8
5
  const IS_TS = import.meta.url.endsWith(".ts");
9
6
  const WORKER_URL = new URL(IS_TS ? "./hardened-vm-worker.ts" : "./hardened-vm-worker.js", import.meta.url);
10
7
  const WORKER_EXEC_ARGV = IS_TS ? ["--import", "tsx/esm"] : undefined;
11
- /**
12
- * Worker-isolated S8 hard runner (design/98 v2). Runs the hardened-vm membrane inside a `worker_thread`, adding
13
- * RESOURCE isolation on top of the in-process {@link import("./hardened-vm-runner.js").createHardenedVmRunner}'s
14
- * BOUNDARY isolation:
15
- * - HEAP cap (`resourceLimits.maxOldGenerationSizeMb`): a runaway-allocation script OOMs the WORKER (an `error`
16
- * event), never the shared main process — closes the in-process resource-exhaustion gap.
17
- * - `terminate()` on wall-clock timeout / abort: kills a MICROTASK-starvation loop (`while(true){await null}`)
18
- * the in-process wall-clock race cannot pre-empt (it starves its own event loop).
19
- * - credential isolation: the worker gets only the script + args; `agent`/`log` IPC to the main thread (which
20
- * holds the real Runner/DB/keys); a (validated-impossible) boundary crossing lands in a credential-free isolate.
21
- *
22
- * `safeForUntrustedScripts: true` — the membrane it runs is the conformance-validated + robustness-tested one. Use
23
- * this (over the in-process runner) for genuinely-untrusted multi-tenant scripts where resource isolation matters.
24
- */
25
8
  export function createWorkerHardenedVmRunner(limits = {}) {
26
9
  const cfg = { ...DEFAULTS, ...limits };
27
10
  return {
@@ -31,8 +14,6 @@ export function createWorkerHardenedVmRunner(limits = {}) {
31
14
  return Promise.reject(new WorkflowScriptError("worker hardened-vm aborted before start"));
32
15
  return new Promise((resolve, reject) => {
33
16
  const worker = new Worker(WORKER_URL, {
34
- // budgetSpent seeds the worker's cache with the CURRENT spend (core 1.238 wiring battery caught the
35
- // cold-start gap: a cache seeded 0 made remaining() read total until the first agent reply refreshed it).
36
17
  workerData: { scriptSource, scriptArgs, limits: cfg, budgetTotal: primitives.budget.total, budgetSpent: primitives.budget.spent() },
37
18
  resourceLimits: { maxOldGenerationSizeMb: cfg.maxHeapMb },
38
19
  ...(WORKER_EXEC_ARGV ? { execArgv: WORKER_EXEC_ARGV } : {}),
@@ -45,7 +26,7 @@ export function createWorkerHardenedVmRunner(limits = {}) {
45
26
  clearTimeout(timer);
46
27
  if (signal)
47
28
  signal.removeEventListener("abort", onAbort);
48
- void worker.terminate(); // hard-stops a starved/runaway worker; no-op if it already exited
29
+ void worker.terminate();
49
30
  };
50
31
  const finish = (settle) => {
51
32
  if (settled)
@@ -54,15 +35,10 @@ export function createWorkerHardenedVmRunner(limits = {}) {
54
35
  cleanup();
55
36
  settle();
56
37
  };
57
- // Wall-clock backstop (terminate). Grace over the in-worker totalTimeoutMs so the worker's CLEAN error
58
- // (stuck host primitive) wins normally; terminate is the backstop for microtask-starvation.
59
38
  timer = setTimeout(() => finish(() => reject(new WorkflowScriptError(`worker hardened-vm exceeded totalTimeoutMs (${cfg.totalTimeoutMs})`))), cfg.totalTimeoutMs + 1500);
60
39
  timer.unref?.();
61
40
  if (signal)
62
41
  signal.addEventListener("abort", onAbort, { once: true });
63
- // Phase-gate fix: the worker's phase IPC verbs land on the REAL primitives.phase via the same three-verb gate
64
- // the in-process runner uses (the body function stays in the worker; only mark/enter/exit cross). Replies
65
- // reuse the worker's id-keyed "agentResult" resolver (same shape — no second pending map in the worker).
66
42
  const phaseGate = scopedPhaseGate((t, b) => primitives.phase(t, b));
67
43
  const reply = (id, p) => void p.then((value) => worker.postMessage({ type: "agentResult", id, ok: true, value: value ?? null, budgetSpent: primitives.budget.spent() }), (e) => worker.postMessage({ type: "agentResult", id, ok: false, error: e instanceof Error ? e.message : String(e), name: e instanceof Error ? e.name : "Error", budgetSpent: primitives.budget.spent() }));
68
44
  worker.on("message", (msg) => {
@@ -91,7 +67,6 @@ export function createWorkerHardenedVmRunner(limits = {}) {
91
67
  finish(() => reject(new WorkflowScriptError(msg.message ?? "worker hardened-vm script failed")));
92
68
  }
93
69
  });
94
- // An `error` event = an UNCAUGHT worker exception, most often the heap-cap OOM ("Reached heap limit").
95
70
  worker.on("error", (err) => finish(() => reject(new WorkflowScriptError(`worker hardened-vm crashed (likely heap/OOM): ${err.message}`))));
96
71
  worker.on("exit", (code) => {
97
72
  if (!settled)
@@ -1,30 +1,13 @@
1
- /**
2
- * S8 hardened-vm WORKER bootstrap (design/98 v2 resource isolation). Runs inside a `worker_thread` spawned by
3
- * {@link import("./hardened-vm-worker-runner.js").createWorkerHardenedVmRunner}. The SAME hardened-vm membrane
4
- * ({@link runInHardenedVm} + {@link orchestrationGlobals}) executes the untrusted script HERE — so on top of
5
- * the membrane's BOUNDARY isolation we get the worker's RESOURCE isolation: a separate V8 isolate + heap (capped
6
- * by `resourceLimits.maxOldGenerationSizeMb` → a memory bomb OOMs the WORKER, not the shared main process) and
7
- * `terminate()` (the main thread kills a microtask-starvation / runaway loop the in-process wall-clock cannot).
8
- *
9
- * The worker holds NO credentials: `agent`/`log` are STUBS that IPC to the main thread (which runs the real
10
- * primitives with the real Runner/DB/model keys); `budget` reads a worker-local cache the main thread refreshes
11
- * on each agent reply. So even a (validated-impossible) membrane boundary crossing lands in a credential-free isolate.
12
- */
13
1
  import { parentPort, workerData } from "node:worker_threads";
14
2
  import { splitWorkflowMeta } from "@sema-agent/core";
15
- // Resolve the sibling membrane module with an EXPLICIT extension so it loads under both a tsc build (`.js`) and
16
- // tsx (`.ts`) — tsx does not map a `.js` specifier to `.ts` for a worker's static import. The template literal
17
- // keeps tsc from rejecting a literal `.ts` specifier; the cast restores the types.
18
3
  const ext = import.meta.url.endsWith(".ts") ? "ts" : "js";
19
4
  const { runInHardenedVm, orchestrationGlobals } = (await import(`./hardened-vm-runner.${ext}`));
20
5
  const port = parentPort;
21
6
  if (!port)
22
7
  throw new Error("hardened-vm-worker must run inside a worker_thread");
23
8
  const input = workerData;
24
- // agent: async IPC (request/response keyed by id) to the main thread's real primitive.
25
9
  let nextId = 0;
26
10
  const pending = new Map();
27
- // budget: worker-local cache the main thread refreshes (piggybacked on every agentResult).
28
11
  const budgetCache = { total: input.budgetTotal, spent: input.budgetSpent ?? 0 };
29
12
  port.on("message", (msg) => {
30
13
  if (msg.type !== "agentResult" || msg.id === undefined)
@@ -43,8 +26,6 @@ port.on("message", (msg) => {
43
26
  p.rej(e);
44
27
  }
45
28
  });
46
- // IPC-backed WorkflowPrimitives handed to orchestrationGlobals — agent/log/phase cross to the main thread;
47
- // budget is the live cache; parallel/pipeline are built in-context by orchestrationGlobals.
48
29
  const ipcCall = (type, extra) => new Promise((res, rej) => {
49
30
  const id = nextId++;
50
31
  pending.set(id, { res, rej });
@@ -58,14 +39,8 @@ const ipcPrimitives = {
58
39
  spent: () => budgetCache.spent,
59
40
  remaining: () => (budgetCache.total === null ? Infinity : budgetCache.total - budgetCache.spent),
60
41
  },
61
- // never invoked (orchestrationGlobals builds parallel/pipeline in-context), but required by the type.
62
42
  parallel: async () => [],
63
43
  pipeline: async () => [],
64
- // Phase-gate fix: phase accounting must reach the MAIN thread's real primitives (the old stub ran the body and told
65
- // core nothing — phases:[] on every worker-isolated run). The body function cannot cross the IPC boundary, so
66
- // the same three-verb protocol as the in-process gate: mark (bare) / enter→token / exit(token, ok, err).
67
- // orchestrationGlobals' own gate wraps THIS phase — its deferred body resolves when the in-VM body settles,
68
- // which chains 1:1 onto these IPC verbs (enter here ⇒ the main-side gate's enter; exit ⇒ its exit).
69
44
  phase: async (t, b) => {
70
45
  if (b === undefined) {
71
46
  await ipcCall("phaseMark", { title: String(t) });
@@ -82,7 +57,6 @@ const ipcPrimitives = {
82
57
  await ipcCall("phaseExit", { token, ok: false, err: e instanceof Error ? e.message : String(e) });
83
58
  }
84
59
  catch {
85
- /* exit is best-effort — the body's own error wins */
86
60
  }
87
61
  throw e;
88
62
  }
@@ -96,7 +70,6 @@ void (async () => {
96
70
  bindings: orchestrationGlobals(ipcPrimitives, input.scriptArgs, input.limits),
97
71
  limits: input.limits,
98
72
  });
99
- // meta is core-parsed; JSON round-trip strips any slipped-in gadget before it crosses back via structuredClone.
100
73
  const safeMeta = JSON.parse(JSON.stringify(meta));
101
74
  port.postMessage({ type: "done", result, meta: safeMeta });
102
75
  }
@@ -1,53 +1,13 @@
1
- /**
2
- * C2 (core 1.219) + design/122 (core 1.225) — the SERVICE-side registry for
3
- * STEERING and RESUMING a Task SUB-AGENT (a sync delegation spawned by the `Agent` tool), the sibling of
4
- * {@link import("./workflow-agent-steer.js").WorkflowAgentRegistry} for workflow agents.
5
- *
6
- * core emits a {@link SubagentSteerHandle} to the deployment's `RunInternals.onSubagentSpawn` sink the
7
- * moment a SYNC child spawns (the handle NEVER reaches the model). The service registers it under the
8
- * PARENT RUN's taskId so `POST /v1/runs/:runId/subagents/:target/{steer,resume}` can route into it.
9
- * Delivery is replica-local (the handle is an in-memory bridge, like `steerableRuns`); a cross-replica
10
- * steer/resume is an honest 409, never a silent drop.
11
- *
12
- * 🔴 LIFECYCLE (design/122 item ③ — the "不改必踩" change): handles are NO LONGER evicted on child settle.
13
- * `resume` is only legal AFTER the child settled (core rejects `steering.still_running` before), so a
14
- * settle-evicted registry would make resume structurally unreachable. Instead:
15
- * - one handle PER parentToolCallId, NEWEST WINS (core D2: each resume re-emits a FRESH handle under the
16
- * same id whose `settled` tracks the revived run — `Map.set` replace is exactly that contract);
17
- * - handles live until the OWNING LEG ends (each run leg collects its `register()` unregister thunks and
18
- * runs them in its finally — identity-guarded, so a suspend→fast-resume sibling leg's fresh handles are
19
- * never clobbered by the old leg's teardown, mirroring the `steerableRuns` identity guard);
20
- * - a per-run cap (oldest-evicted) bounds the map for a delegation-heavy parent (handles now outlive
21
- * settle, so an unbounded list would grow for the whole run).
22
- * After the parent run ends core disposes the retain ledger anyway (abort in-flight resumes + release all),
23
- * so a post-teardown resume would only ever 409 — the eviction just keeps the map honest.
24
- *
25
- * Addressing: `target` matches `parentToolCallId` FIRST (spawn-time identity, unique per delegation), then
26
- * falls back to `agentName` (display name; two children of the same agent type collide → the route 409s
27
- * rather than steering/resuming an arbitrary sibling). Steer-in redaction is shared with the workflow route
28
- * (`redactSteerIn`) — one owner of the trust gate. 🔐 `handle.childSessionId` is a continuation
29
- * CAPABILITY: control-plane only — no route response may include it.
30
- *
31
- * SCOPE (core r1-M3): only SYNC delegations emit a handle — `run_in_background` children have neither steer
32
- * nor resume (poll/stop via TaskOutput/TaskStop; durable bg revival is a separate core item).
33
- */
34
- /** Per-run handle cap — handles now live until the leg ends (not settle), so bound the map. Oldest-evicted;
35
- * 256 mirrors the ToolDetachHub bound (a run with >256 live-or-resumable delegations is pathological). */
36
1
  export const SUBAGENT_HANDLES_PER_RUN_CAP = 256;
37
2
  export class SubagentSteerRegistry {
38
- /** parent run taskId → (parentToolCallId → NEWEST handle). Map iteration order = insertion = age. */
39
3
  byRun = new Map();
40
- /** Register a child handle under its parent run. SAME parentToolCallId replaces (core D2: a resume
41
- * re-emits a fresh handle for the revived run — newest wins). Returns an IDENTITY-GUARDED unregister
42
- * thunk: it removes the entry only while it still holds THIS handle (a fresh sibling-leg registration
43
- * under the same id survives the old leg's teardown). Call it at LEG END, not on settle (design/122 ③). */
44
4
  register(runId, handle) {
45
5
  let handles = this.byRun.get(runId);
46
6
  if (!handles) {
47
7
  handles = new Map();
48
8
  this.byRun.set(runId, handles);
49
9
  }
50
- handles.delete(handle.parentToolCallId); // re-insert → moves to newest position (age = last registration)
10
+ handles.delete(handle.parentToolCallId);
51
11
  handles.set(handle.parentToolCallId, handle);
52
12
  if (handles.size > SUBAGENT_HANDLES_PER_RUN_CAP) {
53
13
  const oldest = handles.keys().next().value;
@@ -65,9 +25,6 @@ export class SubagentSteerRegistry {
65
25
  }
66
26
  };
67
27
  }
68
- /** Resolve `runId` + `target` to its handle on this replica. Exact `parentToolCallId` match wins (unique —
69
- * newest-per-id by construction → count is 0|1); otherwise match by `agentName` (count may be >1 →
70
- * ambiguous, `handle` undefined → the route 409s). count=0 → no such child here. */
71
28
  resolve(runId, target) {
72
29
  const handles = this.byRun.get(runId);
73
30
  if (!handles || handles.size === 0)
@@ -78,7 +35,6 @@ export class SubagentSteerRegistry {
78
35
  const byName = [...handles.values()].filter((h) => h.agentName === target);
79
36
  return byName.length === 1 ? { handle: byName[0], count: 1 } : { count: byName.length };
80
37
  }
81
- /** Total registered handles (test/observability). */
82
38
  get size() {
83
39
  let n = 0;
84
40
  for (const handles of this.byRun.values())