@desplega.ai/agent-swarm 1.164.1 → 1.166.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 (328) hide show
  1. package/README.md +1 -1
  2. package/dist/acp-adapter-yzfxn8vj.js +23 -0
  3. package/dist/{actions-hp2h32dx.js → actions-qz3m692w.js} +24 -28
  4. package/dist/{amp-adapter-zfd7djyx.js → amp-adapter-j1xbmsqz.js} +7 -7
  5. package/dist/{app-w2d26h4f.js → app-43vrdmv8.js} +9 -10
  6. package/dist/{assistant-45pc4en7.js → assistant-ertffmka.js} +25 -29
  7. package/dist/{boot-reembed-k1q2s4fc.js → boot-reembed-998jtpdg.js} +12 -15
  8. package/dist/{boot-reembed-6rhcpsjf.js → boot-reembed-s7k7c6v2.js} +13 -15
  9. package/dist/boot-scrub-sweep-nepsk4t1.js +211 -0
  10. package/dist/{boot-seeds-ex93edx7.js → boot-seeds-vvzs1zhk.js} +12 -12
  11. package/dist/{claude-adapter-hcxd0szm.js → claude-adapter-2zvmekrp.js} +15 -17
  12. package/dist/{claude-managed-adapter-fz7xps2r.js → claude-managed-adapter-s49wbk8t.js} +5 -5
  13. package/dist/{claude-managed-setup-xx6f15h9.js → claude-managed-setup-cvhqq8qn.js} +4 -4
  14. package/dist/{claude-sdk-session-6nb9raem.js → claude-sdk-session-kkbxtw5q.js} +2624 -2606
  15. package/dist/{cli-etbrt8x1.js → cli-0nhdwxd5.js} +4 -4
  16. package/dist/{cli-jeefgppq.js → cli-0qjcsxgz.js} +7 -7
  17. package/dist/{cli-6behs2bt.js → cli-32bca7nr.js} +1 -1
  18. package/dist/{cli-gt8v96qx.js → cli-3ddybbbx.js} +4 -4
  19. package/dist/{acp-adapter-rxvek4ym.js → cli-3qn4r334.js} +317 -35
  20. package/dist/{cli-1d3w4xm3.js → cli-4r1sjfc4.js} +14 -6
  21. package/dist/{cli-691f2rxm.js → cli-66zh26sp.js} +68 -16
  22. package/dist/{cli-et8wdyqh.js → cli-68r1j4sg.js} +9 -9
  23. package/dist/{cli-xb7kyanp.js → cli-8kgvmkak.js} +3 -3
  24. package/dist/{cli-fdpkw8vk.js → cli-8t229cmr.js} +8 -8
  25. package/dist/{cli-gc54qc0p.js → cli-aqcey741.js} +1 -1
  26. package/dist/{cli-4q4cmr4t.js → cli-cpfmqz3n.js} +8 -8
  27. package/dist/{cli-a9bw0xxt.js → cli-df56tqdw.js} +997 -408
  28. package/dist/{cli-n7h1kqpf.js → cli-dmtrxqhg.js} +6 -1
  29. package/dist/{cli-xfb7333f.js → cli-dw2q9f7g.js} +5 -5
  30. package/dist/{cli-4drstbb9.js → cli-ecknp7p1.js} +81 -23
  31. package/dist/{cli-v0pvvhfq.js → cli-efnd2kce.js} +36 -1
  32. package/dist/{cli-1wetf34q.js → cli-f2n17zhp.js} +1 -1
  33. package/dist/{cli-2c8eky5y.js → cli-feh3dkah.js} +1 -1
  34. package/dist/cli-fn697bnt.js +126 -0
  35. package/dist/{cli-v5pr2fgt.js → cli-gt8azyet.js} +3 -3
  36. package/dist/{cli-dbszf3rw.js → cli-h3x613ze.js} +4 -4
  37. package/dist/{cli-nwhb8y2e.js → cli-hf5s9pgf.js} +1 -1
  38. package/dist/cli-j2jbr8af.js +516 -0
  39. package/dist/{cli-agjzafns.js → cli-jhm6hzjn.js} +593 -10
  40. package/dist/{cli-j0tmc908.js → cli-jvj6a0cv.js} +484 -477
  41. package/dist/{cli-xxz89c2n.js → cli-mb75wpxf.js} +1 -1
  42. package/dist/{cli-07s39g1n.js → cli-mv2520nm.js} +1 -1
  43. package/dist/{cli-v90vfwyb.js → cli-mwegs4af.js} +5 -5
  44. package/dist/{cli-bz1gbac9.js → cli-p4ztb80h.js} +2 -2
  45. package/dist/{cli-1wd1n2ya.js → cli-rfphkzh5.js} +1 -1
  46. package/dist/{cli-976r1zqx.js → cli-rggdq8wq.js} +5 -5
  47. package/dist/{cli-3ajw5v5v.js → cli-rsy933yz.js} +6 -6
  48. package/dist/{cli-yj790e19.js → cli-tadfgd0t.js} +8 -1
  49. package/dist/{cli-cn53e7vc.js → cli-ttbsse3f.js} +6 -1
  50. package/dist/{cli-dyqsrqcf.js → cli-v2a8x45d.js} +4 -4
  51. package/dist/{cli-wbvnn6tk.js → cli-v6vv7dy8.js} +91 -9
  52. package/dist/{cli-z2ap2c0c.js → cli-vzc73wet.js} +90 -55
  53. package/dist/cli-wt7ccy4r.js +2654 -0
  54. package/dist/{cli-4cn19jvn.js → cli-x7kpr72a.js} +3 -3
  55. package/dist/{cli-003785v9.js → cli-xvc3q9br.js} +1 -1
  56. package/dist/{cli-v0e7jpr5.js → cli-xvdgasgy.js} +13 -4
  57. package/dist/{cli-asqeg4ve.js → cli-y59h2pk9.js} +2 -2
  58. package/dist/{cli-0wn3ypqv.js → cli-yshaazs1.js} +14121 -3478
  59. package/dist/{cli-wg8yjqtw.js → cli-ysnyhws9.js} +16 -3
  60. package/dist/cli-zp67qg5x.js +92 -0
  61. package/dist/{cli-g3qgrzck.js → cli-zsh093hk.js} +71 -3
  62. package/dist/{cli-ma3zvh39.js → cli-zy7vwsne.js} +6066 -5829
  63. package/dist/cli.js +79 -26
  64. package/dist/{codex-adapter-pcxdn10v.js → codex-adapter-dw1wxqt8.js} +10 -10
  65. package/dist/{codex-hook-b1fb7c2k.js → codex-hook-d2rkvf39.js} +1 -1
  66. package/dist/{codex-login-059nm145.js → codex-login-terbdsqq.js} +2 -2
  67. package/dist/{codex-session-runner-xzwwddp8.js → codex-session-runner-3cgb8y1g.js} +10 -10
  68. package/dist/{commands-98at8mkn.js → commands-mhxam6z2.js} +9 -9
  69. package/dist/{cursor-adapter-dkg6qnkp.js → cursor-adapter-2qda3st7.js} +6 -6
  70. package/dist/{db-01g8wtvw.js → db-sjks418k.js} +11 -9
  71. package/dist/{devin-adapter-qmttt2fz.js → devin-adapter-bffjj5n4.js} +2 -2
  72. package/dist/{dispatcher-nn2k1wxd.js → dispatcher-1es759rn.js} +22 -26
  73. package/dist/{dsh-adapter-w4d4vwdq.js → dsh-adapter-bek4cyw9.js} +6 -6
  74. package/dist/{e2b-9e4g2hvt.js → e2b-nd50eccg.js} +1 -1
  75. package/dist/{event-bus-z7v2t26e.js → event-bus-vnfthafg.js} +2 -2
  76. package/dist/grok-adapter-ar9cw9jg.js +29 -0
  77. package/dist/{handlers-b0kqsbg8.js → handlers-1gkbhktr.js} +25 -29
  78. package/dist/{hook-sap7v1aj.js → hook-ds2zxq9v.js} +16 -18
  79. package/dist/{hook-pz50gqep.js → hook-vxa3tcsy.js} +10 -12
  80. package/dist/{http-ncr8rmd0.js → http-3h21z9md.js} +679 -227
  81. package/dist/{index-c4aqcyz1.js → index-9b65j1m5.js} +11 -11
  82. package/dist/{index-r46s01nq.js → index-bn6cy8ps.js} +22 -26
  83. package/dist/{index-38kqdt3e.js → index-dkfxpkmm.js} +23 -27
  84. package/dist/{index-630kj5vd.js → index-f8xnqrye.js} +55 -43
  85. package/dist/{index-fdk1t4d6.js → index-n0rxa4ca.js} +11962 -9900
  86. package/dist/{index-9nrj7797.js → index-n99fycq7.js} +22 -26
  87. package/dist/{keepalive-31613r2h.js → keepalive-3xspa9jw.js} +14 -11
  88. package/dist/{lead-mx59tr5s.js → lead-6qphc124.js} +43 -43
  89. package/dist/{maintenance-55ff39ca.js → maintenance-0teem1bb.js} +10 -12
  90. package/dist/{node-f0ykebbv.js → node-9sg7ajby.js} +25 -7
  91. package/dist/{oauth-refresh-sweep-anma954w.js → oauth-refresh-sweep-sh9q25xp.js} +13 -10
  92. package/dist/{onboard-b2fy8ftb.js → onboard-54qmnqxe.js} +4 -3
  93. package/dist/{opencode-adapter-p1s92tmy.js → opencode-adapter-wjfa2wvd.js} +6 -6
  94. package/dist/{otel-impl-9brvt7xv.js → otel-impl-ycqzp2a1.js} +2 -2
  95. package/dist/{pi-mono-adapter-kz4rrwvd.js → pi-mono-adapter-e77e9zst.js} +6 -6
  96. package/dist/{pricing-refresh-7w8mbmva.js → pricing-refresh-atnkngqs.js} +10 -10
  97. package/dist/{rbac-roles-ejp7qwvb.js → rbac-roles-9pjphh9q.js} +10 -10
  98. package/dist/{rbac-roles-nhk4r7bc.js → rbac-roles-j78wdksm.js} +10 -10
  99. package/dist/{render-v2-2gzt8skq.js → render-v2-pxjs49fx.js} +9 -10
  100. package/dist/{setup-5et8t412.js → setup-ymej0xtx.js} +2 -2
  101. package/dist/{work-objects-tb0er7qa.js → work-objects-3nh4ymzb.js} +26 -30
  102. package/dist/{worker-62cv075d.js → worker-qa9fkz5s.js} +43 -43
  103. package/dist/{x-vbxb31jr.js → x-ma4gnfgy.js} +2 -2
  104. package/openapi.json +457 -31
  105. package/package.json +14 -6
  106. package/plugin/commands/create-pr.md +7 -1
  107. package/plugin/pi-skills/create-pr/SKILL.md +7 -1
  108. package/src/agentmail/app.ts +5 -4
  109. package/src/be/automation-preflight.ts +11 -9
  110. package/src/be/boot-scrub-sweep.ts +302 -0
  111. package/src/be/connector-codes.ts +81 -0
  112. package/src/be/crypto/key-bootstrap.ts +1 -1
  113. package/src/be/db/approvals.ts +15 -1
  114. package/src/be/db/codex-oauth-device-flows.ts +40 -0
  115. package/src/be/db/config-secrets.ts +31 -0
  116. package/src/be/db/runtime.ts +4 -0
  117. package/src/be/db/script-runs.ts +15 -0
  118. package/src/be/db/tasks/write.ts +1 -1
  119. package/src/be/db-queries/mcp-oauth.ts +5 -0
  120. package/src/be/db-queries/oauth.ts +77 -0
  121. package/src/be/db.ts +162 -59
  122. package/src/be/events.ts +17 -1
  123. package/src/be/memory/index-content.ts +2 -2
  124. package/src/be/memory/providers/sqlite-store.ts +63 -11
  125. package/src/be/memory/types.ts +14 -0
  126. package/src/be/migrations/197_connector_codes.sql +23 -0
  127. package/src/be/migrations/198_permission_audit_guest.sql +33 -0
  128. package/src/be/migrations/200_backfill_auto_memory_expiry.sql +20 -0
  129. package/src/be/migrations/201_workflow_replay_columns.sql +10 -0
  130. package/src/be/migrations/202_codex_oauth_device_flows.sql +24 -0
  131. package/src/be/model-tier-resolution.ts +2 -1
  132. package/src/be/model-validation.ts +1 -1
  133. package/src/be/pricing-normalize.ts +3 -0
  134. package/src/be/rbac-audit.ts +2 -1
  135. package/src/be/scripts/db.ts +28 -4
  136. package/src/be/scripts/source-secrets.ts +37 -0
  137. package/src/be/scripts/typecheck.ts +23 -2
  138. package/src/be/scrub-json.ts +32 -0
  139. package/src/be/sealed-json.ts +59 -0
  140. package/src/be/secret-registry.ts +131 -0
  141. package/src/be/seed-pricing.ts +14 -0
  142. package/src/be/users.ts +7 -1
  143. package/src/be/workflow-replay.ts +77 -0
  144. package/src/cli.tsx +7 -0
  145. package/src/commands/provider-credentials.ts +46 -8
  146. package/src/commands/runner.ts +2 -10
  147. package/src/extensions/contract-types.generated.ts +1 -1
  148. package/src/github/app.ts +4 -4
  149. package/src/heartbeat/heartbeat.ts +20 -8
  150. package/src/hooks/pr-body-guard.ts +1 -1
  151. package/src/http/agents.ts +1 -0
  152. package/src/http/approval-requests.ts +1 -0
  153. package/src/http/apps.ts +3 -0
  154. package/src/http/auth.ts +4 -1
  155. package/src/http/codex-oauth-device.ts +8 -10
  156. package/src/http/codex-oauth-keep-warm.ts +2 -2
  157. package/src/http/config-values.ts +59 -0
  158. package/src/http/config.ts +5 -0
  159. package/src/http/db-query-bounded.ts +3 -0
  160. package/src/http/db-query-shared.ts +107 -15
  161. package/src/http/db-query.ts +10 -2
  162. package/src/http/extensions.ts +1 -0
  163. package/src/http/index.ts +23 -6
  164. package/src/http/integrations.ts +1 -38
  165. package/src/http/ip-rate-limit.ts +82 -0
  166. package/src/http/kv.ts +38 -4
  167. package/src/http/mcp-servers.ts +1 -0
  168. package/src/http/memory.ts +9 -0
  169. package/src/http/models-catalog.ts +2 -0
  170. package/src/http/operator-actor.ts +4 -0
  171. package/src/http/page-proxy.ts +22 -0
  172. package/src/http/pages.ts +4 -2
  173. package/src/http/realtime.ts +1 -1
  174. package/src/http/request-principal.ts +14 -1
  175. package/src/http/script-connections.ts +8 -0
  176. package/src/http/script-runs.ts +12 -4
  177. package/src/http/scripts.ts +15 -0
  178. package/src/http/session-cost-recompute.ts +3 -2
  179. package/src/http/tasks.ts +9 -0
  180. package/src/http/users.ts +226 -0
  181. package/src/http/workflows.ts +3 -2
  182. package/src/http.ts +1 -0
  183. package/src/integrations/kapso/client.ts +3 -1
  184. package/src/jira/app.ts +2 -2
  185. package/src/kv-view.ts +324 -0
  186. package/src/linear/app.ts +2 -2
  187. package/src/oauth/mcp-wrapper.ts +1 -1
  188. package/src/prompts/memories.ts +42 -1
  189. package/src/providers/acp-adapter.ts +85 -24
  190. package/src/providers/acp-target-catalog.ts +54 -1
  191. package/src/providers/acp-targets.ts +358 -4
  192. package/src/providers/amp-adapter.ts +1 -1
  193. package/src/providers/cursor-adapter.ts +1 -1
  194. package/src/providers/grok-adapter.ts +186 -0
  195. package/src/providers/index.ts +5 -1
  196. package/src/providers/pricing-sources.md +6 -0
  197. package/src/providers/reasoning-effort.ts +17 -1
  198. package/src/providers/types.ts +2 -1
  199. package/src/rbac/can.ts +4 -3
  200. package/src/rbac/legacy-policy.ts +3 -3
  201. package/src/rbac/types.ts +6 -0
  202. package/src/script-workflows/supervisor.ts +11 -2
  203. package/src/scripts-runtime/eval-harness.ts +2 -12
  204. package/src/scripts-runtime/executors/quickjs-bundle.ts +152 -0
  205. package/src/scripts-runtime/executors/quickjs-prelude.ts +285 -0
  206. package/src/scripts-runtime/executors/quickjs-runner.ts +580 -0
  207. package/src/scripts-runtime/executors/quickjs-worker.ts +81 -0
  208. package/src/scripts-runtime/executors/quickjs.ts +196 -0
  209. package/src/scripts-runtime/executors/registry.ts +2 -0
  210. package/src/scripts-runtime/executors/sourcemap.ts +79 -0
  211. package/src/scripts-runtime/script-error-hints.ts +14 -0
  212. package/src/scripts-runtime/swarm-sdk.ts +9 -2
  213. package/src/scripts-runtime/types/stdlib.d.ts +27 -8
  214. package/src/scripts-runtime/types/swarm-extension.d.ts +3 -0
  215. package/src/scripts-runtime/types/swarm-sdk.d.ts +27 -8
  216. package/src/server-user.ts +2 -20
  217. package/src/server.ts +3 -1
  218. package/src/slack/app.ts +4 -0
  219. package/src/slack/config.ts +3 -1
  220. package/src/slack/egress-scrub.ts +87 -0
  221. package/src/tests/acp-adapter.test.ts +191 -2
  222. package/src/tests/boot-scrub-sweep.test.ts +390 -0
  223. package/src/tests/budget-user-scope.test.ts +7 -21
  224. package/src/tests/check-pr-body.test.ts +149 -6
  225. package/src/tests/check-text-columns.test.ts +145 -0
  226. package/src/tests/claude-adapter-binary.test.ts +1 -1
  227. package/src/tests/claude-sdk-process.fixture.ts +13 -2
  228. package/src/tests/codex-oauth-device.test.ts +23 -37
  229. package/src/tests/codex-oauth-keep-warm.test.ts +29 -0
  230. package/src/tests/console-scrub.test.ts +141 -0
  231. package/src/tests/credential-check.test.ts +90 -0
  232. package/src/tests/db-query-denylist.test.ts +202 -0
  233. package/src/tests/db-task-read-contract.test.ts +1 -0
  234. package/src/tests/db-task-write-contract.test.ts +1 -0
  235. package/src/tests/docs-index-footprint.test.js +3 -3
  236. package/src/tests/entrypoint-acp-target-binary.test.ts +44 -1
  237. package/src/tests/events-scrub.test.ts +88 -0
  238. package/src/tests/explicit-model-validation.test.ts +81 -0
  239. package/src/tests/extensions-pre-task.test.ts +33 -1
  240. package/src/tests/fixtures/grok/acp-updates-recorded.jsonl +53 -0
  241. package/src/tests/fixtures/grok/prompt-response-meta.json +36 -0
  242. package/src/tests/fixtures/secret-corpus.ts +320 -0
  243. package/src/tests/grok-adapter.test.ts +647 -0
  244. package/src/tests/heartbeat-checklist.test.ts +44 -0
  245. package/src/tests/integration-disable-flags.test.ts +156 -0
  246. package/src/tests/kv-http.test.ts +34 -0
  247. package/src/tests/kv-tool.test.ts +274 -1
  248. package/src/tests/kv-view.test.ts +147 -0
  249. package/src/tests/mcp-user-route.test.ts +257 -15
  250. package/src/tests/memory-injection-outcome.test.ts +143 -0
  251. package/src/tests/model-tier-resolution.test.ts +76 -3
  252. package/src/tests/page-guest-session.test.ts +424 -0
  253. package/src/tests/page-proxy.test.ts +1 -1
  254. package/src/tests/page-viewer-auth.test.ts +22 -1
  255. package/src/tests/pr-body-leaks.test.ts +157 -2
  256. package/src/tests/preload.ts +10 -0
  257. package/src/tests/provider-registration.test.ts +3 -1
  258. package/src/tests/rbac-audit.test.ts +32 -0
  259. package/src/tests/rbac-mcp-admission-e2e.test.ts +4 -62
  260. package/src/tests/script-connections.test.ts +9 -0
  261. package/src/tests/script-executor-conformance.test.ts +3 -0
  262. package/src/tests/script-executor-quickjs.test.ts +442 -0
  263. package/src/tests/script-executor-registry.test.ts +6 -1
  264. package/src/tests/script-runs-http.test.ts +126 -1
  265. package/src/tests/script-runs-scrub.test.ts +275 -0
  266. package/src/tests/scripts-http.test.ts +82 -1
  267. package/src/tests/scripts-mcp-e2e.test.ts +42 -6
  268. package/src/tests/scripts-runtime.test.ts +37 -0
  269. package/src/tests/scrubbed-text-12e.test.ts +289 -0
  270. package/src/tests/sealed-key-guard.test.ts +166 -0
  271. package/src/tests/secret-canary-sweep.test.ts +958 -0
  272. package/src/tests/secret-registry.test.ts +355 -0
  273. package/src/tests/secret-rules.test.ts +230 -0
  274. package/src/tests/secret-scrubber.test.ts +317 -1
  275. package/src/tests/session-costs-golden.test.ts +17 -0
  276. package/src/tests/slack-egress-scrub.test.ts +216 -0
  277. package/src/tests/swarm-tool-result-gate.test.ts +143 -30
  278. package/src/tests/task-tools-ctx.test.ts +3 -0
  279. package/src/tests/user-token-routes.test.ts +342 -3
  280. package/src/tests/workflow-replay.test.ts +518 -0
  281. package/src/tests/workflow-scrub.test.ts +242 -0
  282. package/src/tests/workflow-system-one-decision-providers.test.ts +737 -0
  283. package/src/tools/db-query.ts +1 -1
  284. package/src/tools/inject-learning.ts +1 -1
  285. package/src/tools/kv/kv-get.ts +136 -6
  286. package/src/tools/send-task.ts +61 -17
  287. package/src/tools/utils.ts +186 -43
  288. package/src/tools/workflows/get-workflow-run.ts +3 -2
  289. package/src/types.ts +16 -1
  290. package/src/utils/console-scrub-install.ts +5 -0
  291. package/src/utils/console-scrub.ts +57 -0
  292. package/src/utils/credentials.ts +8 -0
  293. package/src/utils/page-session.ts +6 -2
  294. package/src/utils/pr-body-leaks.ts +149 -10
  295. package/src/utils/request-auth-context.ts +2 -0
  296. package/src/utils/secret-rules.generated.ts +2055 -0
  297. package/src/utils/secret-scrubber.ts +662 -36
  298. package/src/workflows/engine.ts +39 -4
  299. package/src/workflows/executors/notify.ts +6 -1
  300. package/src/workflows/executors/system-one-decision.ts +31 -12
  301. package/src/workflows/executors/system-one-openai-wire.ts +168 -0
  302. package/src/workflows/executors/system-one-providers.ts +112 -11
  303. package/src/workflows/executors/system-one-wire.ts +79 -0
  304. package/src/workflows/recovery.ts +10 -1
  305. package/src/workflows/resume.ts +62 -3
  306. package/src/workflows/retry-poller.ts +3 -0
  307. package/templates/ai-toolbox.manifest.json +2 -2
  308. package/templates/skills/agent-browser/SKILL.md +5 -5
  309. package/templates/skills/agent-browser/config.json +1 -1
  310. package/templates/skills/agent-browser/content.md +4 -4
  311. package/templates/skills/artifacts/content.md +17 -7
  312. package/templates/skills/jira-interaction/config.json +1 -1
  313. package/templates/skills/jira-interaction/content.md +56 -205
  314. package/templates/skills/linear-interaction/config.json +1 -1
  315. package/templates/skills/linear-interaction/content.md +31 -50
  316. package/templates/skills/pages/content.md +8 -4
  317. package/templates/skills/qa/content.md +2 -2
  318. package/templates/skills/workflow-iterate/content.md +4 -4
  319. package/tsconfig.json +3 -3
  320. package/dist/boot-scrub-logs-t8xg7wd7.js +0 -99
  321. package/dist/cli-0a7r5331.js +0 -168
  322. package/dist/cli-93rxw9yd.js +0 -951
  323. package/dist/cli-cvk6yzw5.js +0 -40
  324. package/dist/cli-dgnj0zcc.js +0 -40
  325. package/dist/cli-qt5wzq6v.js +0 -256
  326. package/dist/cli-v7fqehqd.js +0 -9281
  327. package/dist/cli-vbr859rz.js +0 -572
  328. package/src/be/boot-scrub-logs.ts +0 -132
@@ -887,6 +887,96 @@ describe("isCredCheckDisabled", () => {
887
887
  });
888
888
 
889
889
  describe("buildCredStatusReport", () => {
890
+ const HOME = "/home/worker";
891
+
892
+ for (const [provider, authPath] of [
893
+ ["pi", ".pi/agent/auth.json"],
894
+ ["opencode", ".local/share/opencode/auth.json"],
895
+ ] as const) {
896
+ test(`${provider}: auth.json gives a successful presence-only live test`, async () => {
897
+ const realFetch = globalThis.fetch;
898
+ const fetchMock = mock(async () => {
899
+ throw new Error("file credentials must not be sent to a provider endpoint");
900
+ });
901
+ globalThis.fetch = fetchMock as typeof fetch;
902
+ try {
903
+ for (const env of [{ HOME }, { HOME, OPENROUTER_API_KEY: "unused-key" }]) {
904
+ const snap = await buildCredStatusReport(
905
+ provider,
906
+ env,
907
+ { homeDir: HOME, fs: fsWith(new Set([`${HOME}/${authPath}`])) },
908
+ "boot",
909
+ );
910
+ expect(snap.ready).toBe(true);
911
+ expect(snap.satisfiedBy).toBe("file");
912
+ expect(snap.liveTest).toMatchObject({ ok: true, error: null, latency_ms: 0 });
913
+ }
914
+ expect(fetchMock).not.toHaveBeenCalled();
915
+ } finally {
916
+ globalThis.fetch = realFetch;
917
+ }
918
+ });
919
+
920
+ test(`${provider}: missing credentials do not become a successful live test`, async () => {
921
+ const snap = await buildCredStatusReport(
922
+ provider,
923
+ {},
924
+ { homeDir: HOME, fs: noFiles },
925
+ "boot",
926
+ );
927
+ expect(snap.ready).toBe(false);
928
+ expect(snap.liveTest).toBeNull();
929
+ });
930
+
931
+ for (const key of ["OPENROUTER_API_KEY", "ANTHROPIC_API_KEY", "OPENAI_API_KEY"] as const) {
932
+ test(`${provider}: ${key} still receives a live probe and reports its result`, async () => {
933
+ const realFetch = globalThis.fetch;
934
+ let responseStatus = 200;
935
+ const fetchMock = mock(async (_url: string | URL | Request, _init?: RequestInit) => {
936
+ return new Response("{}", { status: responseStatus });
937
+ });
938
+ globalThis.fetch = fetchMock as typeof fetch;
939
+ try {
940
+ const env = { [key]: "provider-key" };
941
+ const opts = { homeDir: HOME, fs: noFiles };
942
+ const success = await buildCredStatusReport(provider, env, opts, "boot");
943
+ expect(success.liveTest?.ok).toBe(true);
944
+ responseStatus = 401;
945
+ const failure = await buildCredStatusReport(provider, env, opts, "post_task");
946
+ expect(failure.ready).toBe(true);
947
+ expect(failure.liveTest?.ok).toBe(false);
948
+ expect(failure.liveTest?.error).toContain("HTTP 401");
949
+ expect(fetchMock).toHaveBeenCalledTimes(2);
950
+ expect(String(fetchMock.mock.calls[0]?.[0])).toMatch(/\/models$/);
951
+ } finally {
952
+ globalThis.fetch = realFetch;
953
+ }
954
+ });
955
+ }
956
+ }
957
+
958
+ test("pi: a Google model with only GEMINI_API_KEY gives a successful presence-only live test", async () => {
959
+ const realFetch = globalThis.fetch;
960
+ const fetchMock = mock(async () => {
961
+ throw new Error("a presence-only credential must not issue a live probe");
962
+ });
963
+ globalThis.fetch = fetchMock as typeof fetch;
964
+ try {
965
+ const snap = await buildCredStatusReport(
966
+ "pi",
967
+ { MODEL_OVERRIDE: "google/gemini-3-flash-preview", GEMINI_API_KEY: "google-key" },
968
+ { homeDir: HOME, fs: noFiles },
969
+ "boot",
970
+ );
971
+ expect(snap.ready).toBe(true);
972
+ expect(snap.satisfiedBy).toBe("env");
973
+ expect(snap.liveTest).toMatchObject({ ok: true, error: null, latency_ms: 0 });
974
+ expect(fetchMock).not.toHaveBeenCalled();
975
+ } finally {
976
+ globalThis.fetch = realFetch;
977
+ }
978
+ });
979
+
890
980
  test("not ready → no live test, snapshot mirrors presence check", async () => {
891
981
  const snap = await buildCredStatusReport("claude", {}, {}, "boot");
892
982
  expect(snap.ready).toBe(false);
@@ -0,0 +1,202 @@
1
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
2
+ import { createServer as createHttpServer, type Server } from "node:http";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { closeDb, getDb, initDb, upsertKv } from "../be/db";
6
+ import { upsertCodexDeviceFlow } from "../be/db/codex-oauth-device-flows";
7
+ import {
8
+ DbQueryDeniedTableError,
9
+ executeReadOnlyQueryGated,
10
+ handleDbQuery,
11
+ } from "../http/db-query";
12
+ import { executeReadOnlyQuery } from "../http/db-query-shared";
13
+ import { getPathSegments, parseQueryParams } from "../http/utils";
14
+ import { listenOnFreePort } from "./test-net";
15
+
16
+ // A real on-disk database: the bounded path spawns a child process that opens
17
+ // its own connection, so an in-memory test template would be invisible to it.
18
+ const DB_PATH = join(tmpdir(), `db-query-denylist-${process.pid}-${Date.now()}.sqlite`);
19
+
20
+ const testGlobals = globalThis as typeof globalThis & {
21
+ __testMigrationTemplate?: Uint8Array;
22
+ };
23
+ let savedTemplate: Uint8Array | undefined;
24
+
25
+ // Built at runtime so no secret-shaped literal lands in the repo.
26
+ const syntheticToken = () => `gh${"p"}_${"K".repeat(36)}`;
27
+
28
+ async function removeDb(): Promise<void> {
29
+ for (const suffix of ["", "-wal", "-shm"]) {
30
+ try {
31
+ await Bun.file(`${DB_PATH}${suffix}`).delete();
32
+ } catch {}
33
+ }
34
+ }
35
+
36
+ beforeAll(async () => {
37
+ savedTemplate = testGlobals.__testMigrationTemplate;
38
+ testGlobals.__testMigrationTemplate = undefined;
39
+ closeDb();
40
+ await removeDb();
41
+ initDb(DB_PATH);
42
+ const db = getDb();
43
+ db.run("CREATE VIEW denylist_test_view AS SELECT * FROM oauth_authorizations");
44
+ db.run("CREATE TABLE denylist_test_notes (id INTEGER PRIMARY KEY, body TEXT)");
45
+ db.run("INSERT INTO denylist_test_notes (body) VALUES (?)", [
46
+ `deploy used ${syntheticToken()} from the env dump`,
47
+ ]);
48
+ });
49
+
50
+ afterAll(async () => {
51
+ closeDb();
52
+ testGlobals.__testMigrationTemplate = savedTemplate;
53
+ await removeDb();
54
+ });
55
+
56
+ const DENIED_QUERIES: Array<[string, string, string]> = [
57
+ ["direct read", "SELECT * FROM oauth_authorizations", "oauth_authorizations"],
58
+ ["alias", "SELECT a.accessToken FROM oauth_authorizations AS a", "oauth_authorizations"],
59
+ ["CTE", "WITH t AS (SELECT * FROM oauth_authorizations) SELECT * FROM t", "oauth_authorizations"],
60
+ [
61
+ "join",
62
+ "SELECT n.body, p.codeVerifier FROM denylist_test_notes n JOIN oauth_pending p ON 1 = 1",
63
+ "oauth_pending",
64
+ ],
65
+ ["subquery", "SELECT * FROM (SELECT * FROM user_tokens)", "user_tokens"],
66
+ ["view", "SELECT * FROM denylist_test_view", "oauth_authorizations"],
67
+ ["scalar subquery", "SELECT (SELECT clientSecret FROM oauth_apps LIMIT 1) AS s", "oauth_apps"],
68
+ ["count only", "SELECT COUNT(*) FROM session_tokens", "session_tokens"],
69
+ ["mixed case", "select * from Script_Apis", "script_apis"],
70
+ ];
71
+
72
+ describe("db-query credential-table denylist", () => {
73
+ for (const [label, sql, table] of DENIED_QUERIES) {
74
+ test(`in-process path rejects a ${label}`, () => {
75
+ expect(() => executeReadOnlyQuery(sql)).toThrow(DbQueryDeniedTableError);
76
+ expect(() => executeReadOnlyQuery(sql)).toThrow(table);
77
+ });
78
+ }
79
+
80
+ test("bounded path rejects before spawning a child", async () => {
81
+ for (const [, sql, table] of DENIED_QUERIES) {
82
+ await expect(executeReadOnlyQueryGated(sql, [], 5_000, 10)).rejects.toThrow(table);
83
+ }
84
+ });
85
+
86
+ test("allows swarm_config, EXPLAIN and a table name in a string literal", async () => {
87
+ const config = await executeReadOnlyQueryGated("SELECT key FROM swarm_config", [], 5_000, 10);
88
+ expect(config.columns).toEqual(["key"]);
89
+
90
+ const explained = executeReadOnlyQuery("EXPLAIN QUERY PLAN SELECT * FROM oauth_authorizations");
91
+ expect(explained.columns.length).toBeGreaterThan(0);
92
+
93
+ const literal = executeReadOnlyQuery("SELECT 'oauth_authorizations' AS name");
94
+ expect(literal.rows).toEqual([["oauth_authorizations"]]);
95
+ });
96
+ });
97
+
98
+ describe("POST /api/db-query", () => {
99
+ let server: Server;
100
+ let baseUrl = "";
101
+
102
+ beforeAll(async () => {
103
+ server = createHttpServer(async (req, res) => {
104
+ const handled = await handleDbQuery(
105
+ req,
106
+ res,
107
+ getPathSegments(req.url || ""),
108
+ parseQueryParams(req.url || ""),
109
+ );
110
+ if (!handled) {
111
+ res.writeHead(404);
112
+ res.end();
113
+ }
114
+ });
115
+ baseUrl = `http://localhost:${await listenOnFreePort(server)}`;
116
+ });
117
+
118
+ afterAll(async () => {
119
+ await new Promise<void>((resolve) => server.close(() => resolve()));
120
+ });
121
+
122
+ async function post(sql: string) {
123
+ const res = await fetch(`${baseUrl}/api/db-query`, {
124
+ method: "POST",
125
+ headers: { "Content-Type": "application/json" },
126
+ body: JSON.stringify({ sql }),
127
+ });
128
+ return { status: res.status, body: (await res.json()) as Record<string, unknown> };
129
+ }
130
+
131
+ test("rejects a credential table with a clear 400", async () => {
132
+ const { status, body } = await post("SELECT * FROM oauth_authorizations");
133
+ expect(status).toBe(400);
134
+ expect(String(body.error)).toContain("oauth_authorizations");
135
+ expect(String(body.error)).toContain("credentials");
136
+ });
137
+
138
+ test("redacts a secret stored in an allowed table and keeps the context", async () => {
139
+ const { status, body } = await post("SELECT body FROM denylist_test_notes");
140
+ expect(status).toBe(200);
141
+ const serialized = JSON.stringify(body.rows);
142
+ expect(serialized).not.toContain(syntheticToken());
143
+ expect(serialized).toContain("[REDACTED:");
144
+ expect(serialized).toContain("from the env dump");
145
+ });
146
+
147
+ test("denies Codex device-login state and keeps general KV readable", async () => {
148
+ const marker = `device-state-${crypto.randomUUID()}`;
149
+ await upsertCodexDeviceFlow({
150
+ flowId: crypto.randomUUID(),
151
+ state: marker,
152
+ expiresAt: Date.now() + 60_000,
153
+ });
154
+ await upsertKv({
155
+ namespace: "denylist-test",
156
+ key: "note",
157
+ value: "plain kv value",
158
+ valueType: "string",
159
+ });
160
+
161
+ for (const sql of [
162
+ "SELECT * FROM codex_oauth_device_flows",
163
+ "SELECT f.state FROM codex_oauth_device_flows f JOIN kv_entries k ON 1",
164
+ ]) {
165
+ const { status, body } = await post(sql);
166
+ expect(status).toBe(400);
167
+ expect(String(body.error)).toContain("codex_oauth_device_flows");
168
+ expect(JSON.stringify(body)).not.toContain(marker);
169
+ }
170
+
171
+ const kv = await post("SELECT namespace, value FROM kv_entries");
172
+ expect(kv.status).toBe(200);
173
+ const rows = JSON.stringify(kv.body.rows);
174
+ expect(rows).toContain("plain kv value");
175
+ expect(rows).not.toContain("codex-oauth-device");
176
+ expect(rows).not.toContain(marker);
177
+ });
178
+
179
+ test("migration 202 moves legacy kv device flows out of kv_entries", async () => {
180
+ const flowId = crypto.randomUUID();
181
+ const marker = `legacy-state-${crypto.randomUUID()}`;
182
+ await upsertKv({
183
+ namespace: "codex-oauth-device",
184
+ key: flowId,
185
+ value: marker,
186
+ valueType: "string",
187
+ expiresAt: Date.now() + 60_000,
188
+ });
189
+ const migration = await Bun.file(
190
+ join(import.meta.dir, "../be/migrations/202_codex_oauth_device_flows.sql"),
191
+ ).text();
192
+ getDb().exec(migration);
193
+
194
+ const kv = await post("SELECT key FROM kv_entries WHERE namespace = 'codex-oauth-device'");
195
+ expect(kv.status).toBe(200);
196
+ expect(kv.body.rows).toEqual([]);
197
+ const moved = getDb()
198
+ .query("SELECT state FROM codex_oauth_device_flows WHERE flow_id = ?")
199
+ .get(flowId) as { state: string } | null;
200
+ expect(moved?.state).toBe(marker);
201
+ });
202
+ });
@@ -357,6 +357,7 @@ value:getScheduledTaskByName
357
357
  value:getScheduledTasks
358
358
  value:getScriptRun
359
359
  value:getScriptRunByIdempotencyKey
360
+ value:getScriptRunExecutionArgs
360
361
  value:getScriptRunJournalStep
361
362
  value:getServiceByAgentAndName
362
363
  value:getServiceById
@@ -380,6 +380,7 @@ value:getScheduledTaskByName
380
380
  value:getScheduledTasks
381
381
  value:getScriptRun
382
382
  value:getScriptRunByIdempotencyKey
383
+ value:getScriptRunExecutionArgs
383
384
  value:getScriptRunJournalStep
384
385
  value:getServiceByAgentAndName
385
386
  value:getServiceById
@@ -78,7 +78,7 @@ describe("docs API index footprint", () => {
78
78
  expect(tasks).toContain('"path":"/api/task-templates","method":"get"');
79
79
  });
80
80
 
81
- test("OpenAPI and generated pages keep all 396 operations", async () => {
81
+ test("OpenAPI and generated pages keep all 399 operations", async () => {
82
82
  const spec = await Bun.file(resolve(repoRoot, "openapi.json")).json();
83
83
  const operationKeys = [];
84
84
  const tags = new Set();
@@ -103,8 +103,8 @@ describe("docs API index footprint", () => {
103
103
  expect(spec.paths["/api/agents/{id}/runtime"]?.get).toBeDefined();
104
104
 
105
105
  const generatedKeys = await generatedOperationKeys();
106
- expect(operationKeys).toHaveLength(396);
107
- expect(generatedKeys).toHaveLength(396);
106
+ expect(operationKeys).toHaveLength(399);
107
+ expect(generatedKeys).toHaveLength(399);
108
108
  expect(new Set(generatedKeys)).toEqual(new Set(operationKeys));
109
109
  });
110
110
 
@@ -43,6 +43,8 @@ const STUB_BINARIES = [
43
43
  "claude",
44
44
  "codex",
45
45
  "opencode",
46
+ "gemini",
47
+ "copilot",
46
48
  "dsh",
47
49
  "acp-target-stub",
48
50
  "acp target stub",
@@ -129,6 +131,47 @@ describe("docker-entrypoint.sh: acp target binary verification", () => {
129
131
  CHILD_PROCESS_TEST_BUDGET_MS,
130
132
  );
131
133
 
134
+ test(
135
+ "resolves the gemini catalog target to the gemini binary",
136
+ async () => {
137
+ const result = await runChain({ HARNESS_PROVIDER: "acp", ACP_TARGET: "gemini" });
138
+
139
+ expect(result.exitCode).toBe(0);
140
+ expect(result.stdout).toContain("ACP target: ");
141
+ expect(result.stdout).toContain("ACP_TARGET='gemini'");
142
+ },
143
+ CHILD_PROCESS_TEST_BUDGET_MS,
144
+ );
145
+
146
+ test(
147
+ "resolves the copilot catalog target to the copilot binary",
148
+ async () => {
149
+ const result = await runChain({ HARNESS_PROVIDER: "acp", ACP_TARGET: "copilot" });
150
+
151
+ expect(result.exitCode).toBe(0);
152
+ expect(result.stdout).toContain("ACP target: ");
153
+ expect(result.stdout).toContain("ACP_TARGET='copilot'");
154
+ },
155
+ CHILD_PROCESS_TEST_BUDGET_MS,
156
+ );
157
+
158
+ test(
159
+ "fails boot with an install hint when the copilot binary is missing",
160
+ async () => {
161
+ // No stub dir on PATH, so `copilot` cannot resolve.
162
+ const result = await runChain({
163
+ HARNESS_PROVIDER: "acp",
164
+ ACP_TARGET: "copilot",
165
+ PATH: "/usr/bin:/bin",
166
+ });
167
+
168
+ expect(result.exitCode).not.toBe(0);
169
+ expect(result.stdout).toContain("FATAL: ACP target binary not found: 'copilot'");
170
+ expect(result.stdout).toContain("npm install -g @github/copilot");
171
+ },
172
+ CHILD_PROCESS_TEST_BUDGET_MS,
173
+ );
174
+
132
175
  test(
133
176
  "resolves a custom target supplied as a command string with argv",
134
177
  async () => {
@@ -178,7 +221,7 @@ describe("docker-entrypoint.sh: acp target binary verification", () => {
178
221
 
179
222
  expect(result.exitCode).not.toBe(0);
180
223
  expect(result.stdout).toContain(
181
- "FATAL: unsupported ACP target 'gemini-cli'. Supported targets: opencode, custom.",
224
+ "FATAL: unsupported ACP target 'gemini-cli'. Supported targets: opencode, gemini, copilot, custom.",
182
225
  );
183
226
  },
184
227
  CHILD_PROCESS_TEST_BUDGET_MS,
@@ -0,0 +1,88 @@
1
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
2
+ import { unlink } from "node:fs/promises";
3
+ import { closeDb, getDbClient, initDb } from "../be/db";
4
+ import { createEvent, createEventsBatch } from "../be/events";
5
+ import { randomToken, syntheticSecret } from "./synthetic-secret-helpers";
6
+
7
+ const TEST_DB_PATH = "./test-events-scrub.sqlite";
8
+
9
+ beforeAll(async () => {
10
+ try {
11
+ await unlink(TEST_DB_PATH);
12
+ } catch {}
13
+ initDb(TEST_DB_PATH);
14
+ });
15
+
16
+ afterAll(async () => {
17
+ closeDb();
18
+ for (const suffix of ["", "-wal", "-shm"]) {
19
+ try {
20
+ await unlink(`${TEST_DB_PATH}${suffix}`);
21
+ } catch {}
22
+ }
23
+ });
24
+
25
+ async function readData(sessionId: string): Promise<string[]> {
26
+ const rows = await getDbClient().query<{ data: string | null }>(
27
+ "SELECT data FROM events WHERE sessionId = ? ORDER BY rowid",
28
+ [sessionId],
29
+ );
30
+ return rows.map((r) => r.data ?? "");
31
+ }
32
+
33
+ describe("events.data scrub at write", () => {
34
+ test("createEvent redacts a secret in the payload and keeps valid JSON", async () => {
35
+ const pat = ["ghp", randomToken(36)].join("_");
36
+ const sessionId = crypto.randomUUID();
37
+ await createEvent({
38
+ category: "session",
39
+ event: "session.start",
40
+ source: "api",
41
+ sessionId,
42
+ data: { note: "cloned repo with token", output: `remote: ${pat} ok`, count: 3 },
43
+ });
44
+
45
+ const [raw] = await readData(sessionId);
46
+ expect(raw).toBeDefined();
47
+ expect(raw).not.toContain(pat);
48
+ expect(raw).toContain("[REDACTED:");
49
+ const parsed = JSON.parse(raw as string);
50
+ expect(parsed.note).toBe("cloned repo with token");
51
+ expect(parsed.output).toStartWith("remote: ");
52
+ expect(parsed.output).toEndWith(" ok");
53
+ expect(parsed.count).toBe(3);
54
+ });
55
+
56
+ test("createEventsBatch redacts every row and keeps valid JSON", async () => {
57
+ // Registered per test: the preload clears volatile secrets after each one.
58
+ const known = syntheticSecret("evscrub");
59
+ const sessionId = crypto.randomUUID();
60
+ const count = await createEventsBatch([
61
+ {
62
+ category: "tool",
63
+ event: "tool.start",
64
+ source: "worker",
65
+ sessionId,
66
+ data: { tool: "Bash", input: `echo ${known.value}` },
67
+ },
68
+ {
69
+ category: "tool",
70
+ event: "tool.end",
71
+ source: "worker",
72
+ sessionId,
73
+ data: { tool: "Bash", nested: { lines: ["first", `leaked ${known.value} here`] } },
74
+ },
75
+ ]);
76
+ expect(count).toBe(2);
77
+
78
+ const rows = await readData(sessionId);
79
+ expect(rows).toHaveLength(2);
80
+ for (const raw of rows) {
81
+ expect(raw).not.toContain(known.value);
82
+ expect(raw).toContain(`[REDACTED:${known.name}]`);
83
+ expect(JSON.parse(raw).tool).toBe("Bash");
84
+ }
85
+ expect(JSON.parse(rows[0] as string).input).toStartWith("echo ");
86
+ expect(JSON.parse(rows[1] as string).nested.lines[0]).toBe("first");
87
+ });
88
+ });
@@ -47,6 +47,7 @@ let leadId = "";
47
47
  let workerId = "";
48
48
  let acpId = "";
49
49
  let codexId = "";
50
+ let grokId = "";
50
51
 
51
52
  beforeAll(async () => {
52
53
  await removeDbFiles(TEST_DB_PATH);
@@ -78,6 +79,14 @@ beforeAll(async () => {
78
79
  harnessProvider: "codex",
79
80
  })
80
81
  ).id;
82
+ grokId = (
83
+ await createAgent({
84
+ name: "validation-grok",
85
+ isLead: false,
86
+ status: "idle",
87
+ harnessProvider: "grok",
88
+ })
89
+ ).id;
81
90
 
82
91
  server = createServer((req, res) => {
83
92
  setRequestAuth(req, { kind: "operator", fingerprint: "explicit-model-validation-test" });
@@ -250,6 +259,78 @@ describe("explicitModelError", () => {
250
259
  });
251
260
  });
252
261
 
262
+ describe("grok model namespace", () => {
263
+ test("another vendor's id, namespace or alias is refused, even with allowCustomModel", async () => {
264
+ for (const model of [
265
+ "claude-opus-5-5",
266
+ "anthropic/claude-opus-5-5",
267
+ "gpt-5.6-sol",
268
+ "openai/gpt-5.6-sol",
269
+ "deepseek/deepseek-v4.1-flash",
270
+ "latest:anthropic/opus",
271
+ "latest:openai/gpt-5*",
272
+ ]) {
273
+ for (const allowCustomModel of [false, true]) {
274
+ const error = await explicitModelError({
275
+ model,
276
+ harnessProvider: "grok",
277
+ allowCustomModel,
278
+ });
279
+ expect(error).toContain(`Model "${model}" does not run on the grok harness`);
280
+ expect(error).toContain("openrouter/<vendor>/<id>");
281
+ }
282
+ }
283
+ });
284
+
285
+ test("bare xAI ids and both OpenRouter spellings pass", async () => {
286
+ for (const model of [
287
+ "grok-4.6",
288
+ "openrouter/deepseek/deepseek-v4.1-flash",
289
+ "latest:openrouter/deepseek/deepseek-v4.1-*",
290
+ ]) {
291
+ expect(await explicitModelError({ model, harnessProvider: "grok" })).toBeNull();
292
+ }
293
+ });
294
+
295
+ test("xai/<id> runs on grok; the API catalog has no xai section, so it needs allowCustomModel", async () => {
296
+ expect(
297
+ await explicitModelError({
298
+ model: "xai/grok-4.6",
299
+ harnessProvider: "grok",
300
+ allowCustomModel: true,
301
+ }),
302
+ ).toBeNull();
303
+ expect(await explicitModelError({ model: "xai/grok-4.6", harnessProvider: "grok" })).toContain(
304
+ 'Unknown model "xai/grok-4.6"',
305
+ );
306
+ });
307
+
308
+ test("POST /api/tasks to a grok agent: vendor ids are a 400, xAI and OpenRouter ids a 201", async () => {
309
+ const base = { task: "grok route", agentId: grokId, routingReason: "human_pinned" };
310
+ for (const model of ["claude-opus-5-5", "latest:anthropic/opus"]) {
311
+ const refused = await api("POST", "/api/tasks", { ...base, model });
312
+ expect(refused.status).toBe(400);
313
+ expect(String(refused.body.error)).toContain(
314
+ `does not run on the grok harness of agent "validation-grok" (${grokId})`,
315
+ );
316
+ }
317
+ for (const model of ["grok-4.6", "openrouter/deepseek/deepseek-v4.1-flash"]) {
318
+ const accepted = await api("POST", "/api/tasks", { ...base, model });
319
+ expect(accepted.status).toBe(201);
320
+ }
321
+ });
322
+
323
+ test("MODEL_TIER_GROK_<TIER> refuses another vendor's model", () => {
324
+ expect(validateTierConfigValue("MODEL_TIER_GROK_SMART", "claude-opus-5-5")).toBe(
325
+ 'Invalid MODEL_TIER_GROK_SMART: model "claude-opus-5-5" does not run on the grok harness.',
326
+ );
327
+ expect(validateTierConfigValue("MODEL_TIER_GROK_SMART", "grok-4.6")).toBeNull();
328
+ expect(
329
+ validateTierConfigValue("MODEL_TIER_GROK_SMOL", "openrouter/deepseek/deepseek-v4.1-flash"),
330
+ ).toBeNull();
331
+ });
332
+ });
333
+
253
334
  describe("MODEL_TIER_<PROVIDER>_<TIER> values", () => {
254
335
  test("a model from another harness family is rejected on write", () => {
255
336
  expect(validateTierConfigValue("MODEL_TIER_CODEX_SMART", "claude-opus-5-5")).toBe(
@@ -8,6 +8,7 @@ import {
8
8
  createAgent,
9
9
  createScheduledTask,
10
10
  createTaskExtended,
11
+ createUser,
11
12
  createWorkflow,
12
13
  createWorkflowRun,
13
14
  createWorkflowRunStep,
@@ -26,8 +27,9 @@ import { handleTasks } from "../http/tasks";
26
27
  import { dispatchScheduleTarget } from "../scheduler/scheduler";
27
28
  import { createTaskWithSiblingAwareness } from "../tasks/sibling-awareness";
28
29
  import { createWorkerTaskFollowUp } from "../tasks/worker-follow-up";
29
- import { registerSendTaskTool } from "../tools/send-task";
30
+ import { registerSendTaskTool, sendTaskHandler } from "../tools/send-task";
30
31
  import { registerTaskActionTool } from "../tools/task-action";
32
+ import { userCtx } from "../tools/task-tool-ctx";
31
33
  import { markExtensionRequestOrigin } from "../tools/utils";
32
34
  import { workflowEventBus } from "../workflows/event-bus";
33
35
  import { AgentTaskExecutor } from "../workflows/executors/agent-task";
@@ -139,6 +141,7 @@ describe("extension task boundaries", () => {
139
141
  const client = getDbClient();
140
142
  await client.run("DELETE FROM agent_tasks");
141
143
  await client.run("DELETE FROM extensions");
144
+ await client.run("DELETE FROM users");
142
145
  await client.run("DELETE FROM agents");
143
146
  });
144
147
 
@@ -501,6 +504,35 @@ export default extension;
501
504
  expect(compatible.body.model).toBe("claude-opus-5-5");
502
505
  });
503
506
 
507
+ test("user send-task rejects an extension that retargets the Lead assignment", async () => {
508
+ await createAgent({ name: "user-task-lead", isLead: true, status: "idle" });
509
+ const worker = await createAgent({ name: "user-task-worker", isLead: false, status: "idle" });
510
+ const user = await createUser({ name: "Extension Guard User" });
511
+ await installSource(
512
+ "retarget-user-task",
513
+ `import type { SwarmExtension } from "swarm-extension";
514
+ const extension: SwarmExtension = (api) => {
515
+ api.on("pre.task.create", () => ({ action: "modify", data: { agentId: "${worker.id}" } }));
516
+ };
517
+ export default extension;
518
+ `,
519
+ );
520
+
521
+ const result = await sendTaskHandler(
522
+ userCtx(user),
523
+ sendTaskArgs("extension must not retarget this user task"),
524
+ );
525
+
526
+ expect(result.ok).toBe(false);
527
+ expect(result.message).toBe(
528
+ "A pre.task.create extension cannot change the user task's Lead assignment.",
529
+ );
530
+ expect(
531
+ (await getDbClient().get<{ count: number }>("SELECT COUNT(*) AS count FROM agent_tasks"))
532
+ ?.count,
533
+ ).toBe(0);
534
+ });
535
+
504
536
  test("extension tool calls derive their origin and skip the creating extension", async () => {
505
537
  const creator = await installAndEnable("rewrite-task-priority");
506
538
  await installAndEnable("block-tasks-from-source", {