@desplega.ai/agent-swarm 2.0.0 → 2.1.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 (238) hide show
  1. package/dist/{acp-adapter-sj2g0m84.js → acp-adapter-kr4abrsk.js} +10 -10
  2. package/dist/{actions-fegk44gr.js → actions-eh4g6w2r.js} +27 -27
  3. package/dist/{amp-adapter-ad8ft89x.js → amp-adapter-7h7wdt8q.js} +5 -5
  4. package/dist/{app-kfvgbvzy.js → app-7k8s9w5p.js} +9 -9
  5. package/dist/{assistant-z7zdy9ja.js → assistant-wswt2y7p.js} +28 -28
  6. package/dist/{boot-reembed-j8xa181e.js → boot-reembed-9apm9qw6.js} +7 -7
  7. package/dist/{boot-reembed-5c2z0mbe.js → boot-reembed-whzbdthj.js} +7 -7
  8. package/dist/{boot-scrub-sweep-hnkn3rgt.js → boot-scrub-sweep-ndzmvmgt.js} +8 -8
  9. package/dist/{boot-seeds-kwjq4d2z.js → boot-seeds-e9sta0yt.js} +13 -11
  10. package/dist/{claude-adapter-9vmvpvgd.js → claude-adapter-4mqd3r6r.js} +11 -11
  11. package/dist/{claude-managed-adapter-k9qae05j.js → claude-managed-adapter-9wswb52m.js} +3 -3
  12. package/dist/{claude-managed-setup-q54zx5q2.js → claude-managed-setup-tpe1effx.js} +4 -4
  13. package/dist/{claude-sdk-session-9jye6cxr.js → claude-sdk-session-q49ghmh9.js} +10340 -10197
  14. package/dist/{cli-ke896fcf.js → cli-01xvj1rq.js} +10 -10
  15. package/dist/{cli-tr4sxp68.js → cli-035vekcw.js} +2 -2
  16. package/dist/{cli-z5jjpaxw.js → cli-0pd8hzhc.js} +3 -3
  17. package/dist/{cli-n5vwg842.js → cli-0veb92a3.js} +6 -1
  18. package/dist/{cli-wayw93nt.js → cli-10jesr25.js} +3 -3
  19. package/dist/{cli-se8w6wny.js → cli-1y7yywmj.js} +1 -1
  20. package/dist/{cli-v1yb0rqb.js → cli-20krdhz5.js} +47 -19
  21. package/dist/{cli-d5f9ed5c.js → cli-29rja109.js} +39 -20
  22. package/dist/{cli-apx87jne.js → cli-2pwa0q4d.js} +15 -4
  23. package/dist/{cli-ee9gtz70.js → cli-4mengw46.js} +348 -25
  24. package/dist/{cli-03gedhng.js → cli-5twdke5x.js} +6 -6
  25. package/dist/{cli-6razj6t4.js → cli-711vwbxn.js} +2 -2
  26. package/dist/{cli-s2sp957d.js → cli-78vv7446.js} +1 -1
  27. package/dist/{cli-3fbmmawm.js → cli-7n6s5man.js} +4 -4
  28. package/dist/{cli-hnhtk6cj.js → cli-8wwxw8a3.js} +1 -1
  29. package/dist/{cli-hnmhtzvh.js → cli-9jzdj90z.js} +2 -0
  30. package/dist/{cli-skxvvpap.js → cli-cvv9k0hh.js} +28 -25
  31. package/dist/{cli-74ahz61x.js → cli-d9286m9h.js} +2 -2
  32. package/dist/{cli-4jxm2qtz.js → cli-gxzkg1de.js} +28 -16
  33. package/dist/{cli-zkpks3r3.js → cli-h1arqn31.js} +1 -1
  34. package/dist/{cli-r62a35ea.js → cli-hqr88ew6.js} +1 -1
  35. package/dist/{cli-ra0xzggj.js → cli-j67mh2vg.js} +2 -2
  36. package/dist/{cli-29jc5q7w.js → cli-ksphgh48.js} +2 -2
  37. package/dist/{cli-eejsgvm4.js → cli-mgz4nbgg.js} +4 -4
  38. package/dist/{cli-vatyhshy.js → cli-mz5as5ha.js} +4 -4
  39. package/dist/{cli-f7kr7svz.js → cli-n8ndw8j7.js} +2 -2
  40. package/dist/{cli-1e4b6bzh.js → cli-naqzr17j.js} +7 -7
  41. package/dist/{cli-q0ke1yww.js → cli-nm1p8jfr.js} +6 -6
  42. package/dist/{cli-m50t4309.js → cli-p3zbc1rw.js} +5 -5
  43. package/dist/{cli-cgtksawx.js → cli-pe05f8yk.js} +23 -3
  44. package/dist/{cli-vdach5a9.js → cli-qmzsbkvy.js} +4 -4
  45. package/dist/{cli-n3hzg89b.js → cli-qnm4e9e4.js} +57 -579
  46. package/dist/{cli-s4setq91.js → cli-qqy4q48b.js} +3 -3
  47. package/dist/{cli-k382tshb.js → cli-r35rw5de.js} +8 -7
  48. package/dist/{cli-k3dp5tpf.js → cli-r610p9bs.js} +2 -2
  49. package/dist/{cli-e6mpjhed.js → cli-rc6pf5kv.js} +1 -1
  50. package/dist/{cli-3dj8k4vy.js → cli-rdxm77k3.js} +1 -1
  51. package/dist/{cli-tqaazemc.js → cli-s69nfyeh.js} +1 -1
  52. package/dist/{cli-6jyj59nb.js → cli-sawv63e8.js} +2 -2
  53. package/dist/{cli-q50942rs.js → cli-sg19gbf0.js} +1 -1
  54. package/dist/{cli-p1x85d2t.js → cli-shk7r5cs.js} +12 -9
  55. package/dist/{cli-przpmdgr.js → cli-sksetb2q.js} +11 -1
  56. package/dist/{cli-d2qr7aj4.js → cli-t02z59f2.js} +1 -1
  57. package/dist/{cli-j0pfptdp.js → cli-v7g551te.js} +39 -43
  58. package/dist/{cli-bzszjf2p.js → cli-vgqzp4em.js} +519 -126
  59. package/dist/cli-wfnemr1t.js +86 -0
  60. package/dist/{cli-ne236afy.js → cli-wms6x9db.js} +8 -8
  61. package/dist/{cli-5hkm7whv.js → cli-x1r545x8.js} +1 -1
  62. package/dist/{cli-kt6zqg0e.js → cli-xczsgshn.js} +2 -2
  63. package/dist/{cli-ef1hg1wv.js → cli-y3167wt1.js} +430 -238
  64. package/dist/{cli-tsde1hdr.js → cli-yjmqpxt3.js} +12 -11
  65. package/dist/{cli-9nfx44eh.js → cli-z780ec7c.js} +1 -1
  66. package/dist/{cli-r2pnvj8g.js → cli-zm9t2vde.js} +2 -2
  67. package/dist/cli.js +24 -23
  68. package/dist/{codex-adapter-y81sssk0.js → codex-adapter-4z16v9n0.js} +8 -8
  69. package/dist/{codex-login-f2rcfpp9.js → codex-login-pkepy2j1.js} +2 -2
  70. package/dist/{codex-session-runner-8vx4j6a9.js → codex-session-runner-rvms9y2x.js} +8 -8
  71. package/dist/{commands-kcpvpcrr.js → commands-2a9rtyat.js} +8 -8
  72. package/dist/{controller-ywrmt6yd.js → controller-3k3madbn.js} +4 -4
  73. package/dist/{controllers-connect-kwpvj3rf.js → controllers-connect-v9yn8zb9.js} +30 -30
  74. package/dist/{cursor-adapter-cehv1qns.js → cursor-adapter-98gbhyzn.js} +4 -4
  75. package/dist/{db-ww5d7jq3.js → db-eywwsnjw.js} +9 -7
  76. package/dist/{db-k8ankj20.js → db-ncypk9np.js} +9 -9
  77. package/dist/{devin-adapter-x1022qb1.js → devin-adapter-zeq8b9gz.js} +2 -2
  78. package/dist/{dispatcher-e8gq5m68.js → dispatcher-w94kc575.js} +26 -26
  79. package/dist/{docker-ff6y1190.js → docker-b1e4cqab.js} +2 -2
  80. package/dist/{dsh-adapter-3epjm3af.js → dsh-adapter-47ztej36.js} +4 -4
  81. package/dist/{e2b-8ygwhg0f.js → e2b-bbt8ey45.js} +3 -3
  82. package/dist/{e2b-swhnpw3n.js → e2b-dp804sen.js} +3 -3
  83. package/dist/{event-bus-6axnngdj.js → event-bus-j2jdg3qb.js} +2 -2
  84. package/dist/{external-channel-h3359cwn.js → external-channel-zrkg8ar9.js} +8 -8
  85. package/dist/{grok-adapter-0d4x7gkx.js → grok-adapter-s9ncp9vy.js} +11 -11
  86. package/dist/{handlers-an1gzv21.js → handlers-kqftvjgw.js} +29 -29
  87. package/dist/{heartbeat-6dakeb61.js → heartbeat-9q96p8v9.js} +31 -31
  88. package/dist/{hook-6v6cv6d5.js → hook-56qe10dp.js} +7 -7
  89. package/dist/{hook-sa5xye4y.js → hook-yy6rw42k.js} +8 -8
  90. package/dist/{http-r7f3eky0.js → http-r2dnb8f8.js} +963 -75
  91. package/dist/{index-7ep4gzdk.js → index-3gtmkhc6.js} +26 -26
  92. package/dist/{index-v1v6n7hg.js → index-3k5hkz7r.js} +31 -31
  93. package/dist/{index-rmpw9a8q.js → index-5e101240.js} +26 -26
  94. package/dist/{index-s2wxgzr0.js → index-5wyfjc2c.js} +392 -15
  95. package/dist/{index-zk96aq1k.js → index-vxvhxxbn.js} +7 -7
  96. package/dist/{keepalive-be9vwy5h.js → keepalive-psj6j54c.js} +9 -9
  97. package/dist/{lead-0861cwrf.js → lead-hajz4688.js} +43 -43
  98. package/dist/{maintenance-mzzgc8bn.js → maintenance-ps02sb5m.js} +7 -7
  99. package/dist/{node-mn1zs7an.js → node-mwb5r697.js} +1 -1
  100. package/dist/{oauth-refresh-sweep-cpdnfwdj.js → oauth-refresh-sweep-0284h93b.js} +8 -8
  101. package/dist/{onboard-fh8qs4r3.js → onboard-rbf6bg01.js} +2 -2
  102. package/dist/{opencode-adapter-nm42wwj8.js → opencode-adapter-13dcatgq.js} +8 -8
  103. package/dist/{otel-impl-x6c1c9g4.js → otel-impl-zvqcbhp7.js} +2 -2
  104. package/dist/{pi-mono-adapter-9n2ya240.js → pi-mono-adapter-xxy47q4v.js} +7 -7
  105. package/dist/{pricing-refresh-qb0rv0ap.js → pricing-refresh-kefx9v3t.js} +9 -9
  106. package/dist/{rbac-roles-1gzdwbh1.js → rbac-roles-3m36rxm2.js} +8 -8
  107. package/dist/{rbac-roles-3mraey52.js → rbac-roles-z76xv0hq.js} +9 -9
  108. package/dist/{render-v2-agpa45zz.js → render-v2-wz73bs25.js} +9 -9
  109. package/dist/{resume-x0ydy9s9.js → resume-k4wzge8w.js} +14 -14
  110. package/dist/{setup-as9ga0d6.js → setup-2fp2pypp.js} +2 -2
  111. package/dist/{work-objects-y5g1y7ev.js → work-objects-6k0ywz3m.js} +29 -29
  112. package/dist/{worker-860c3e38.js → worker-5v3rxsdh.js} +43 -43
  113. package/dist/{x-m477fr0h.js → x-4mtdeym1.js} +2 -2
  114. package/openapi.json +777 -6
  115. package/package.json +8 -7
  116. package/src/agentmail/handlers.ts +21 -11
  117. package/src/apps/definition.ts +16 -0
  118. package/src/azure-devops/api.ts +52 -2
  119. package/src/azure-devops/boards-sync.ts +50 -20
  120. package/src/azure-devops/index.ts +1 -0
  121. package/src/azure-devops/outbound.ts +95 -0
  122. package/src/be/boot-seeds.ts +11 -0
  123. package/src/be/db/model-profiles.ts +315 -0
  124. package/src/be/db/tasks/read.ts +7 -0
  125. package/src/be/db.ts +53 -47
  126. package/src/be/migrations/219_relabel_pi_context_compactions.sql +22 -0
  127. package/src/be/migrations/220_model_profiles.sql +137 -0
  128. package/src/be/model-tier-resolution.ts +145 -11
  129. package/src/be/model-validation.ts +8 -1
  130. package/src/be/scripts/typecheck.ts +12 -0
  131. package/src/be/secret-registry.ts +6 -2
  132. package/src/be/seed-model-profiles.ts +41 -0
  133. package/src/be/seed-scripts/catalog/heartbeat-jev-gate.ts +368 -0
  134. package/src/be/seed-scripts/index.ts +9 -0
  135. package/src/be/swarm-config-guard.ts +1 -0
  136. package/src/commands/runner.ts +3 -17
  137. package/src/extensions/catalog.generated.json +17 -0
  138. package/src/extensions/contract-types.generated.ts +1 -1
  139. package/src/extensions/loader.ts +10 -5
  140. package/src/heartbeat/heartbeat.ts +2 -1
  141. package/src/http/agents.ts +49 -2
  142. package/src/http/all-routes.ts +1 -0
  143. package/src/http/apps.ts +30 -3
  144. package/src/http/config.ts +26 -2
  145. package/src/http/core.ts +4 -0
  146. package/src/http/index.ts +11 -1
  147. package/src/http/model-profiles.ts +303 -0
  148. package/src/http/pages-public.ts +11 -5
  149. package/src/http/poll.ts +16 -5
  150. package/src/http/script-runs.ts +1 -1
  151. package/src/http/scripts.ts +25 -2
  152. package/src/http/tasks.ts +18 -0
  153. package/src/http/webhooks.ts +1 -2
  154. package/src/pages/feedback-overlay.ts +65 -6
  155. package/src/providers/acp-targets.ts +1 -1
  156. package/src/providers/amp-adapter.ts +1 -1
  157. package/src/providers/dsh-adapter.ts +1 -1
  158. package/src/providers/pi-mono-extension.ts +17 -3
  159. package/src/providers/reasoning-effort.ts +10 -0
  160. package/src/rbac/legacy-policy.ts +8 -0
  161. package/src/rbac/permissions.ts +5 -0
  162. package/src/scheduler/deferred-task-waits.ts +18 -5
  163. package/src/scheduler/scheduler.ts +7 -1
  164. package/src/scripts-runtime/executors/quickjs-prelude.ts +75 -6
  165. package/src/scripts-runtime/executors/quickjs-runner.ts +85 -8
  166. package/src/scripts-runtime/executors/quickjs-web-shims.ts +1041 -0
  167. package/src/scripts-runtime/extract-args-schema.ts +9 -8
  168. package/src/scripts-runtime/quickjs-compat.ts +464 -0
  169. package/src/scripts-runtime/sdk-allowlist.ts +4 -0
  170. package/src/scripts-runtime/types/stdlib.d.ts +12 -0
  171. package/src/scripts-runtime/types/swarm-extension.d.ts +3 -0
  172. package/src/scripts-runtime/types/swarm-sdk.d.ts +12 -0
  173. package/src/server.ts +10 -1
  174. package/src/slack/blocks.ts +4 -2
  175. package/src/slack/reaction-shortcode.ts +14 -0
  176. package/src/slack/render-v2.ts +12 -7
  177. package/src/slack/responses.ts +5 -4
  178. package/src/slack/thread-buffer.ts +41 -22
  179. package/src/tasks/worker-follow-up.ts +11 -2
  180. package/src/tests/agentmail-filters.test.ts +47 -0
  181. package/src/tests/apps-spike2.test.ts +35 -0
  182. package/src/tests/azure-devops-outbound.test.ts +420 -0
  183. package/src/tests/db-task-read-contract.test.ts +2 -0
  184. package/src/tests/db-task-write-contract.test.ts +1 -0
  185. package/src/tests/deferred-task-wake.test.ts +78 -1
  186. package/src/tests/docs-index-footprint.test.js +2 -2
  187. package/src/tests/dsh-adapter.test.ts +1 -1
  188. package/src/tests/env-flag.test.ts +1 -0
  189. package/src/tests/extensions-catalog.test.ts +1 -0
  190. package/src/tests/extensions-example-comms-adherence-gate.test.ts +413 -0
  191. package/src/tests/extensions-loader.test.ts +15 -0
  192. package/src/tests/heartbeat-jev-gate-credentials.test.ts +168 -0
  193. package/src/tests/heartbeat-jev-gate.test.ts +385 -0
  194. package/src/tests/model-profile-backcompat.test.ts +327 -0
  195. package/src/tests/model-profile-providers.test.ts +302 -0
  196. package/src/tests/model-profile-runtime-secrets.test.ts +223 -0
  197. package/src/tests/model-profiles-api.test.ts +614 -0
  198. package/src/tests/pages-feedback-keys.test.ts +121 -0
  199. package/src/tests/pages-feedback.test.ts +3 -0
  200. package/src/tests/pi-context-compaction.test.ts +193 -0
  201. package/src/tests/profile-claim.test.ts +269 -0
  202. package/src/tests/quickjs-compat.test.ts +213 -0
  203. package/src/tests/rbac-engine.test.ts +21 -0
  204. package/src/tests/script-connections.test.ts +4 -0
  205. package/src/tests/script-executor-conformance.test.ts +252 -1
  206. package/src/tests/script-executor-quickjs.test.ts +96 -0
  207. package/src/tests/script-runs-http.test.ts +89 -0
  208. package/src/tests/scripts-http.test.ts +16 -0
  209. package/src/tests/secret-scrubber.test.ts +10 -0
  210. package/src/tests/seed-model-profiles.test.ts +174 -0
  211. package/src/tests/seed-scripts.test.ts +2 -2
  212. package/src/tests/skill-invocations.test.ts +4 -2
  213. package/src/tests/slack-blocks.test.ts +36 -0
  214. package/src/tests/slack-inline-output.test.ts +14 -0
  215. package/src/tests/slack-reaction-config.test.ts +20 -0
  216. package/src/tests/slack-render-v2.test.ts +37 -0
  217. package/src/tests/slack-thread-buffer.test.ts +98 -1
  218. package/src/tests/workflow-llm.test.ts +61 -0
  219. package/src/tools/model-profiles/list-model-profiles.ts +54 -0
  220. package/src/tools/request-human-input.ts +11 -3
  221. package/src/tools/script-apis.ts +5 -1
  222. package/src/tools/script-upsert.ts +4 -1
  223. package/src/tools/send-task.ts +24 -1
  224. package/src/tools/system-one-decide.ts +104 -0
  225. package/src/tools/task-action.ts +22 -1
  226. package/src/tools/tool-config.ts +6 -0
  227. package/src/types.ts +36 -1
  228. package/src/utils/secret-scrubber.ts +19 -12
  229. package/src/utils/vcs-repo-match.ts +29 -0
  230. package/src/workflows/executors/raw-llm.ts +8 -3
  231. package/src/workflows/executors/system-one-decision.ts +21 -11
  232. package/src/workflows/executors/workflow-llm.ts +22 -0
  233. package/src/workflows/index.ts +21 -12
  234. package/templates/extensions/README.md +1 -0
  235. package/templates/extensions/comms-adherence-gate/README.md +88 -0
  236. package/templates/extensions/comms-adherence-gate/hooks.ts +489 -0
  237. package/templates/extensions/comms-adherence-gate/manifest.json +8 -0
  238. package/dist/cli-5t4f29ez.js +0 -33
@@ -0,0 +1,303 @@
1
+ // Model profiles CRUD (runbooks/model-tiers.md § Model profiles).
2
+ //
3
+ // A profile is a named harness + model + optional effort. Lead picks one per
4
+ // task (`profile` on send-task / POST /api/tasks) or sets it as an agent's
5
+ // default (PATCH /api/agents/{id}/runtime { profile }); claim resolves it.
6
+ // Reads are open to every caller; writes are operator-only (`models.profiles.write`).
7
+
8
+ import type { IncomingMessage, ServerResponse } from "node:http";
9
+ import { isAlias } from "@desplega/model-catalog";
10
+ import { z } from "zod";
11
+ import { resolveHttpAuditUserId } from "../be/audit-user";
12
+ import {
13
+ createModelProfile,
14
+ deleteModelProfile,
15
+ getModelProfileByName,
16
+ listModelProfiles,
17
+ modelProfileReferences,
18
+ updateModelProfile,
19
+ } from "../be/db/model-profiles";
20
+ import { explicitModelError } from "../be/model-validation";
21
+ import { supportedReasoningLevels } from "../providers/reasoning-effort";
22
+ import { can } from "../rbac";
23
+ import {
24
+ ModelProfileNameSchema,
25
+ ModelProfileSchema,
26
+ type ProviderName,
27
+ ProviderNameSchema,
28
+ type ReasoningEffort,
29
+ ReasoningEffortSchema,
30
+ } from "../types";
31
+ import { agentFirstPrincipal } from "./request-principal";
32
+ import { route } from "./route-def";
33
+ import { jsonError } from "./utils";
34
+
35
+ const listProfiles = route({
36
+ method: "get",
37
+ path: "/api/model-profiles",
38
+ pattern: ["api", "model-profiles"],
39
+ summary: "List model profiles",
40
+ description:
41
+ "Every model profile, ordered by harness then name. Filter by `harness` to see what an agent on that harness can run.",
42
+ tags: ["Models"],
43
+ query: z.object({ harness: ProviderNameSchema.optional() }),
44
+ responses: {
45
+ 200: {
46
+ description: "Model profiles",
47
+ schema: z.object({ profiles: z.array(ModelProfileSchema) }),
48
+ },
49
+ },
50
+ });
51
+
52
+ const getProfile = route({
53
+ method: "get",
54
+ path: "/api/model-profiles/{name}",
55
+ pattern: ["api", "model-profiles", null],
56
+ summary: "Get one model profile",
57
+ tags: ["Models"],
58
+ params: z.object({ name: z.string() }),
59
+ responses: {
60
+ 200: { description: "The model profile", schema: ModelProfileSchema },
61
+ 404: { description: "No profile with that name" },
62
+ },
63
+ });
64
+
65
+ const createProfile = route({
66
+ method: "post",
67
+ path: "/api/model-profiles",
68
+ pattern: ["api", "model-profiles"],
69
+ summary: "Create a model profile",
70
+ description:
71
+ "A profile runs on one harness. `model` must run on that harness and be in the model catalog unless `allowCustomModel` is true (a gateway id, a fresh launch); a `latest:` alias is accepted and resolves at claim. `effort` must be a level the harness and model support. Operator only (the shared API key).",
72
+ tags: ["Models"],
73
+ rbac: { permission: "models.profiles.write" },
74
+ body: z.object({
75
+ name: ModelProfileNameSchema,
76
+ harness: ProviderNameSchema,
77
+ model: z.string().trim().min(1),
78
+ effort: ReasoningEffortSchema.nullable().optional(),
79
+ allowCustomModel: z.boolean().optional().default(false),
80
+ description: z.string().trim().max(500).nullable().optional(),
81
+ }),
82
+ responses: {
83
+ 201: { description: "Profile created", schema: ModelProfileSchema },
84
+ 400: { description: "Validation error" },
85
+ 403: { description: "Forbidden" },
86
+ 409: { description: "A profile with that name exists" },
87
+ },
88
+ });
89
+
90
+ const updateProfile = route({
91
+ method: "patch",
92
+ path: "/api/model-profiles/{name}",
93
+ pattern: ["api", "model-profiles", null],
94
+ summary: "Update a model profile",
95
+ description:
96
+ "Changes the model, effort, custom-model flag or description; the name and harness are fixed. Omit a field to keep it; `effort: null` clears it. Applies to tasks claimed after the change. Operator only (the shared API key).",
97
+ tags: ["Models"],
98
+ rbac: { permission: "models.profiles.write" },
99
+ params: z.object({ name: z.string() }),
100
+ body: z.object({
101
+ model: z.string().trim().min(1).optional(),
102
+ effort: ReasoningEffortSchema.nullable().optional(),
103
+ allowCustomModel: z.boolean().optional(),
104
+ description: z.string().trim().max(500).nullable().optional(),
105
+ }),
106
+ responses: {
107
+ 200: { description: "Profile updated", schema: ModelProfileSchema },
108
+ 400: { description: "Validation error" },
109
+ 403: { description: "Forbidden" },
110
+ 404: { description: "No profile with that name" },
111
+ },
112
+ });
113
+
114
+ const deleteProfile = route({
115
+ method: "delete",
116
+ path: "/api/model-profiles/{name}",
117
+ pattern: ["api", "model-profiles", null],
118
+ summary: "Delete a model profile",
119
+ description:
120
+ "Refused (409) for a shipped `<harness>-<tier>` seed (edit it instead; it would come back at the next boot), and while an agent has the profile as its default or a task holds it (unfinished, or superseded and still owed its resume). Operator only (the shared API key).",
121
+ tags: ["Models"],
122
+ rbac: { permission: "models.profiles.write" },
123
+ params: z.object({ name: z.string() }),
124
+ responses: {
125
+ 200: { description: "Profile deleted", schema: z.object({ deleted: z.literal(true) }) },
126
+ 403: { description: "Forbidden" },
127
+ 404: { description: "No profile with that name" },
128
+ 409: { description: "The profile is a seed or still in use" },
129
+ },
130
+ });
131
+
132
+ /** Why `harness` + `model` + `effort` is not a valid profile, or null. */
133
+ async function profileValueError(input: {
134
+ harness: ProviderName;
135
+ model: string;
136
+ effort: ReasoningEffort | null | undefined;
137
+ allowCustomModel: boolean;
138
+ }): Promise<string | null> {
139
+ const modelError = await explicitModelError({
140
+ model: input.model,
141
+ allowCustomModel: input.allowCustomModel,
142
+ harnessProvider: input.harness,
143
+ });
144
+ if (modelError) return modelError;
145
+ // The levels the harness and model accept (as PATCH /runtime checks them); a
146
+ // supported level may still be a transport no-op, like Opencode `off`. An
147
+ // alias is judged once it resolves.
148
+ if (
149
+ input.effort &&
150
+ !isAlias(input.model) &&
151
+ !supportedReasoningLevels(input.harness, input.model).includes(input.effort)
152
+ ) {
153
+ return `Effort "${input.effort}" is not supported by ${input.model} on ${input.harness}.`;
154
+ }
155
+ return null;
156
+ }
157
+
158
+ /** Gate a write on `models.profiles.write`; writes a 403 and returns false on denial. */
159
+ async function canWriteProfiles(
160
+ req: IncomingMessage,
161
+ res: ServerResponse,
162
+ myAgentId: string | undefined,
163
+ ): Promise<boolean> {
164
+ const decision = can({
165
+ principal: await agentFirstPrincipal(req, myAgentId),
166
+ verb: "models.profiles.write",
167
+ resource: { kind: "none" },
168
+ source: "http",
169
+ });
170
+ if (!decision.allow) {
171
+ jsonError(res, "Writing model profiles requires the operator", 403);
172
+ return false;
173
+ }
174
+ return true;
175
+ }
176
+
177
+ export async function handleModelProfiles(
178
+ req: IncomingMessage,
179
+ res: ServerResponse,
180
+ pathSegments: string[],
181
+ queryParams: URLSearchParams,
182
+ myAgentId: string | undefined,
183
+ ): Promise<boolean> {
184
+ if (listProfiles.match(req.method, pathSegments)) {
185
+ const parsed = await listProfiles.parse(req, res, pathSegments, queryParams);
186
+ if (!parsed) return true;
187
+ listProfiles.respond(res, 200, {
188
+ profiles: await listModelProfiles({ harness: parsed.query.harness }),
189
+ });
190
+ return true;
191
+ }
192
+
193
+ if (getProfile.match(req.method, pathSegments)) {
194
+ const parsed = await getProfile.parse(req, res, pathSegments, queryParams);
195
+ if (!parsed) return true;
196
+ const profile = await getModelProfileByName(parsed.params.name);
197
+ if (!profile) {
198
+ jsonError(res, "Model profile not found", 404);
199
+ return true;
200
+ }
201
+ getProfile.respond(res, 200, profile);
202
+ return true;
203
+ }
204
+
205
+ if (createProfile.match(req.method, pathSegments)) {
206
+ const parsed = await createProfile.parse(req, res, pathSegments, queryParams);
207
+ if (!parsed) return true;
208
+ if (!(await canWriteProfiles(req, res, myAgentId))) return true;
209
+ const body = parsed.body;
210
+ const error = await profileValueError({ ...body, effort: body.effort ?? null });
211
+ if (error) {
212
+ jsonError(res, error, 400);
213
+ return true;
214
+ }
215
+ const created = await createModelProfile(body, {
216
+ userId: await resolveHttpAuditUserId(req, myAgentId),
217
+ });
218
+ if (!created) {
219
+ jsonError(res, `Model profile "${body.name}" already exists`, 409);
220
+ return true;
221
+ }
222
+ createProfile.respond(res, 201, created);
223
+ return true;
224
+ }
225
+
226
+ if (updateProfile.match(req.method, pathSegments)) {
227
+ const parsed = await updateProfile.parse(req, res, pathSegments, queryParams);
228
+ if (!parsed) return true;
229
+ if (!(await canWriteProfiles(req, res, myAgentId))) return true;
230
+ const current = await getModelProfileByName(parsed.params.name);
231
+ if (!current) {
232
+ jsonError(res, "Model profile not found", 404);
233
+ return true;
234
+ }
235
+ const body = parsed.body;
236
+ const error = await profileValueError({
237
+ harness: current.harness,
238
+ model: body.model ?? current.model,
239
+ effort: body.effort === undefined ? current.effort : body.effort,
240
+ allowCustomModel: body.allowCustomModel ?? current.allowCustomModel,
241
+ });
242
+ if (error) {
243
+ jsonError(res, error, 400);
244
+ return true;
245
+ }
246
+ const updated = await updateModelProfile(current.name, body, {
247
+ userId: await resolveHttpAuditUserId(req, myAgentId),
248
+ });
249
+ if (!updated) {
250
+ jsonError(res, "Model profile not found", 404);
251
+ return true;
252
+ }
253
+ updateProfile.respond(res, 200, updated);
254
+ return true;
255
+ }
256
+
257
+ if (deleteProfile.match(req.method, pathSegments)) {
258
+ const parsed = await deleteProfile.parse(req, res, pathSegments, queryParams);
259
+ if (!parsed) return true;
260
+ if (!(await canWriteProfiles(req, res, myAgentId))) return true;
261
+ const current = await getModelProfileByName(parsed.params.name);
262
+ if (!current) {
263
+ jsonError(res, "Model profile not found", 404);
264
+ return true;
265
+ }
266
+ if (current.seededFrom?.startsWith("tier:")) {
267
+ jsonError(
268
+ res,
269
+ `"${current.name}" is a shipped tier profile and comes back at the next boot; edit it instead.`,
270
+ 409,
271
+ );
272
+ return true;
273
+ }
274
+ const refs = await modelProfileReferences(current.name);
275
+ if (refs.agentIds.length > 0 || refs.openTaskCount > 0) {
276
+ jsonError(
277
+ res,
278
+ `"${current.name}" is in use: default of ${refs.agentIds.length} agent(s), held by ${refs.openTaskCount} unfinished or awaiting-resume task(s).`,
279
+ 409,
280
+ );
281
+ return true;
282
+ }
283
+ // The count above is for the message; the delete re-checks in the same
284
+ // statement (migration 220 trigger), so a reference written since is a 409.
285
+ const outcome = await deleteModelProfile(current.name);
286
+ if (outcome === "in-use") {
287
+ jsonError(
288
+ res,
289
+ `"${current.name}" is in use by a task or an agent default created just now.`,
290
+ 409,
291
+ );
292
+ return true;
293
+ }
294
+ if (outcome === "missing") {
295
+ jsonError(res, "Model profile not found", 404);
296
+ return true;
297
+ }
298
+ deleteProfile.respond(res, 200, { deleted: true });
299
+ return true;
300
+ }
301
+
302
+ return false;
303
+ }
@@ -25,7 +25,11 @@ import type { IncomingMessage, ServerResponse } from "node:http";
25
25
  import { z } from "zod";
26
26
  import { getPage, incrementPageViewCount } from "../be/db";
27
27
  import { BROWSER_SDK_JS, SWARM_UI_JS } from "../pages/browser-sdk";
28
- import { feedbackOverlayScripts, isPageFeedbackRequested } from "../pages/feedback-overlay";
28
+ import {
29
+ feedbackKeyGuardScript,
30
+ feedbackOverlayScripts,
31
+ isPageFeedbackRequested,
32
+ } from "../pages/feedback-overlay";
29
33
  import { type Page, PageSchema } from "../types";
30
34
  import { getAppUrl, getConfiguredAppUrls } from "../utils/constants";
31
35
  import { getNodeEnv } from "../utils/node-env";
@@ -170,8 +174,8 @@ const PAGE_HEAD_DEFAULTS = `<base target="_blank">
170
174
  }
171
175
  </style>`;
172
176
 
173
- function injectBrowserSdk(html: string): string {
174
- const injection = `${PAGE_HEAD_DEFAULTS}<script>${BROWSER_SDK_JS}</script><script>${SWARM_UI_JS}</script>`;
177
+ function injectBrowserSdk(html: string, headFirst = ""): string {
178
+ const injection = `${headFirst}${PAGE_HEAD_DEFAULTS}<script>${BROWSER_SDK_JS}</script><script>${SWARM_UI_JS}</script>`;
175
179
  // Use the first occurrence of `<head>` (case-insensitive). A page that
176
180
  // doesn't have a `<head>` element (raw fragment) still gets the SDK at the
177
181
  // front of the document.
@@ -646,10 +650,12 @@ export async function handlePagesPublic(
646
650
  // text/html — inject SDK + serve. Append the self-print snippet when the
647
651
  // export button requested it, and the feedback overlay when
648
652
  // `?__swarm-feedback` is on (the overlay hides itself in print).
653
+ // The feedback key guard goes first in <head> so it runs before page scripts.
654
+ const feedback = isPageFeedbackRequested(queryParams);
649
655
  const html =
650
- injectBrowserSdk(page.body) +
656
+ injectBrowserSdk(page.body, feedback ? feedbackKeyGuardScript() : "") +
651
657
  (wantsPrint ? PRINT_AUTOTRIGGER_SCRIPT : "") +
652
- (isPageFeedbackRequested(queryParams) ? feedbackOverlayScripts(page.id) : "");
658
+ (feedback ? feedbackOverlayScripts(page.id) : "");
653
659
  const headers: Record<string, string> = {
654
660
  "Content-Type": "text/html; charset=utf-8",
655
661
  "Cache-Control": "no-store",
package/src/http/poll.ts CHANGED
@@ -41,7 +41,9 @@ import { AgentTaskSchema, BudgetRefusedTriggerSchema, TaskAttachmentSchema } fro
41
41
  import { ensure } from "../utils/business-use";
42
42
  import { isLeadChannelMonitoringEnabled } from "../utils/lead-monitor-channels";
43
43
  import { isMultiRuntimeEnabled } from "../utils/multi-runtime";
44
+ import { getRequestAuth, type HttpRequestAuth } from "../utils/request-auth-context";
44
45
  import { isRuntimePoolsEnabled } from "../utils/runtime-pools-flag";
46
+ import { readsRepoSecrets } from "./config";
45
47
  import { buildTaskRequestedBy, TaskRequestedBySchema } from "./requested-by";
46
48
  import { route, runtimeInstanceHeader } from "./route-def";
47
49
  import { jsonError } from "./utils";
@@ -208,8 +210,14 @@ async function attachmentsForTrigger(
208
210
  * previous claim record is overwritten for the same reason.
209
211
  */
210
212
  async function claimModelFields(
211
- task: TaskModelPinFields & { id: string },
212
- agent: Parameters<typeof recordClaimModelResolution>[1],
213
+ task: TaskModelPinFields & {
214
+ id: string;
215
+ profile?: string | null;
216
+ effort?: string | null;
217
+ vcsRepo?: string | null;
218
+ },
219
+ agent: Parameters<typeof recordClaimModelResolution>[1] & { isLead?: boolean },
220
+ auth: HttpRequestAuth | null,
213
221
  ): Promise<Awaited<ReturnType<typeof recordClaimModelResolution>> & { model: string | undefined }> {
214
222
  const pin = taskModelPin(task);
215
223
  const cleared = {
@@ -217,11 +225,14 @@ async function claimModelFields(
217
225
  resolvedModel: undefined,
218
226
  modelSource: undefined,
219
227
  modelAlias: undefined,
228
+ resolvedProfile: undefined,
220
229
  };
221
230
  try {
222
231
  return {
223
232
  ...cleared,
224
- ...(await recordClaimModelResolution({ ...task, model: pin }, agent)),
233
+ ...(await recordClaimModelResolution({ ...task, model: pin }, agent, {
234
+ includeRepoSecrets: readsRepoSecrets(auth, agent),
235
+ })),
225
236
  };
226
237
  } catch (error) {
227
238
  console.warn(`[/api/poll] model resolution failed for task ${task.id}:`, error);
@@ -495,7 +506,7 @@ export async function handlePoll(
495
506
  triggerSurface: assignedTriggerSurface,
496
507
  task: {
497
508
  ...pendingTask,
498
- ...(await claimModelFields(pendingTask, agent)),
509
+ ...(await claimModelFields(pendingTask, agent, getRequestAuth(req))),
499
510
  status: "in_progress",
500
511
  attachments: await attachmentsForTrigger(pendingTask.id),
501
512
  },
@@ -639,7 +650,7 @@ export async function handlePoll(
639
650
  triggerSurface: claimedTriggerSurface,
640
651
  task: {
641
652
  ...claimed,
642
- ...(await claimModelFields(claimed, agent)),
653
+ ...(await claimModelFields(claimed, agent, getRequestAuth(req))),
643
654
  attachments: await attachmentsForTrigger(claimed.id),
644
655
  },
645
656
  ...(claimedRequestedBy && { requestedBy: claimedRequestedBy }),
@@ -611,7 +611,7 @@ export async function handleScriptRuns(
611
611
  if (!parsed) return true;
612
612
  const result = await executeRawLlm(parsed.body);
613
613
  if (result.status === "failed") {
614
- json(res, { error: result.error }, 500);
614
+ jsonError(res, result.error, 500);
615
615
  return true;
616
616
  }
617
617
  rawLlmRoute.respond(res, 200, result.output);
@@ -41,6 +41,7 @@ import {
41
41
  import { can } from "../rbac";
42
42
  import { extractScriptSignature, type ScriptSignature } from "../scripts-runtime/extract-signature";
43
43
  import { runScript } from "../scripts-runtime/loader";
44
+ import { checkQuickJSCompat, quickJSCompatWarnings } from "../scripts-runtime/quickjs-compat";
44
45
  import {
45
46
  ScriptApiAuthModeSchema,
46
47
  ScriptApiRecordSchema,
@@ -236,6 +237,10 @@ const upsertRoute = route({
236
237
  name: z.string(),
237
238
  version: z.number(),
238
239
  contentDeduped: z.boolean(),
240
+ warnings: z
241
+ .array(z.string())
242
+ .optional()
243
+ .describe("Advisory quickjs-executor compatibility findings. Never blocks the save."),
239
244
  }),
240
245
  },
241
246
  400: { description: "Validation or typecheck failure" },
@@ -424,7 +429,15 @@ const createScriptApiRoute = route({
424
429
  params: idParamsSchema,
425
430
  body: createScriptApiBodySchema,
426
431
  responses: {
427
- 201: { description: "Endpoint created", schema: scriptApiWithSecretSchema },
432
+ 201: {
433
+ description: "Endpoint created",
434
+ schema: scriptApiWithSecretSchema.extend({
435
+ warnings: z
436
+ .array(z.string())
437
+ .optional()
438
+ .describe("Advisory quickjs-executor compatibility findings for the script."),
439
+ }),
440
+ },
428
441
  400: { description: "Validation error or script has no owning agent" },
429
442
  404: { description: "Script not found" },
430
443
  },
@@ -702,10 +715,14 @@ export async function handleScripts(
702
715
  });
703
716
  }
704
717
 
718
+ const warnings = quickJSCompatWarnings(
719
+ checkQuickJSCompat(parsed.body.source, { fsMode: parsed.body.fsMode }),
720
+ );
705
721
  upsertRoute.respond(res, 200, {
706
722
  name: result.script.name,
707
723
  version: result.script.version,
708
724
  contentDeduped: result.contentDeduped,
725
+ ...(warnings.length > 0 ? { warnings } : {}),
709
726
  });
710
727
  return true;
711
728
  }
@@ -1092,7 +1109,13 @@ export async function handleScripts(
1092
1109
  label: parsed.body.label ?? null,
1093
1110
  createdBy: await resolveHttpAuditUserId(req, agentId),
1094
1111
  });
1095
- createScriptApiRoute.respond(res, 201, endpoint);
1112
+ const warnings = quickJSCompatWarnings(
1113
+ checkQuickJSCompat(script.source, { fsMode: script.fsMode }),
1114
+ );
1115
+ createScriptApiRoute.respond(res, 201, {
1116
+ ...endpoint,
1117
+ ...(warnings.length > 0 ? { warnings } : {}),
1118
+ });
1096
1119
  return true;
1097
1120
  }
1098
1121
 
package/src/http/tasks.ts CHANGED
@@ -35,6 +35,7 @@ import {
35
35
  updateTaskTitle,
36
36
  updateTaskVcs,
37
37
  } from "../be/db";
38
+ import { modelProfileRefError, taskProfileError } from "../be/db/model-profiles";
38
39
  import { explicitModelErrorForAgent } from "../be/model-validation";
39
40
  import {
40
41
  getTaskSteeringFields,
@@ -287,6 +288,8 @@ const createTask = route({
287
288
  */
288
289
  allowCustomModel: z.boolean().optional(),
289
290
  effort: ReasoningEffortSchema.optional(),
291
+ /** Model profile name, applied at claim. Exclusive with `model` and `modelTier`. */
292
+ profile: z.string().trim().min(1).optional(),
290
293
  /**
291
294
  * Create in `draft` status instead of the normal pending/unassigned/offered
292
295
  * status (#1240) — the task exists and is visible to its owner, but is not
@@ -966,6 +969,15 @@ export async function handleTasks(
966
969
  jsonError(res, modelError, 400);
967
970
  return true;
968
971
  }
972
+ const profileError = await taskProfileError({
973
+ profile: parsed.body.profile,
974
+ ...splitLegacyModelAlias({ model: parsed.body.model, modelTier: parsed.body.modelTier }),
975
+ agentId: defaultAgentId,
976
+ });
977
+ if (profileError) {
978
+ jsonError(res, profileError, 400);
979
+ return true;
980
+ }
969
981
 
970
982
  const parentTask = parsed.body.parentTaskId
971
983
  ? await getTaskById(parsed.body.parentTaskId)
@@ -1057,6 +1069,7 @@ export async function handleTasks(
1057
1069
  modelTier: parsed.body.modelTier,
1058
1070
  }),
1059
1071
  effort: parsed.body.effort,
1072
+ profile: parsed.body.profile,
1060
1073
  },
1061
1074
  { origin: "rest", allowCustomModel: parsed.body.allowCustomModel },
1062
1075
  );
@@ -1087,6 +1100,11 @@ export async function handleTasks(
1087
1100
  jsonError(res, error.message, 400);
1088
1101
  return true;
1089
1102
  }
1103
+ // The profile was deleted between taskProfileError and the insert.
1104
+ if (modelProfileRefError(error) === "missing") {
1105
+ jsonError(res, `Unknown model profile "${parsed.body.profile}"`, 400);
1106
+ return true;
1107
+ }
1090
1108
  console.error("[HTTP] Failed to create task:", error);
1091
1109
  jsonError(res, "Failed to create task", 500);
1092
1110
  }
@@ -757,9 +757,8 @@ export async function handleWebhooks(
757
757
  payload.message &&
758
758
  !isInboxAllowed(payload.message.inbox_id, process.env.AGENTMAIL_INBOX_DOMAIN_FILTER)
759
759
  ) {
760
- const domain = payload.message.inbox_id.split("@")[1] ?? "unknown";
761
760
  console.log(
762
- `[AgentMail] Ignoring event for inbox domain "${domain}" (not in AGENTMAIL_INBOX_DOMAIN_FILTER)`,
761
+ `[AgentMail] Ignoring event for inbox "${payload.message.inbox_id}" (not in AGENTMAIL_INBOX_DOMAIN_FILTER)`,
763
762
  );
764
763
  return true;
765
764
  }
@@ -25,6 +25,43 @@ export function isPageFeedbackRequested(queryParams: URLSearchParams): boolean {
25
25
  return value !== "0" && value !== "false";
26
26
  }
27
27
 
28
+ // Keys typed in the overlay must never reach the page's own key handlers. A
29
+ // listener on the overlay runs too late: page capture listeners on window and
30
+ // document fire first. So this guard is served at the top of <head>, before any
31
+ // page script, and is the first window capture listener. For a key from the
32
+ // overlay it stops dispatch outright and hands the event to the overlay
33
+ // (window.__swarmFeedbackKeys.handle). A press that started in the overlay
34
+ // stays owned until its keyup, so the release is swallowed even after the
35
+ // composer closed and focus moved to the page. Every other key passes through.
36
+ // The overlay script repeats it as a fallback; the second run is a no-op.
37
+ const FEEDBACK_KEY_GUARD_JS = `
38
+ (function () {
39
+ if (window.__swarmFeedbackKeys) return;
40
+ var keys = window.__swarmFeedbackKeys = { host: null, handle: null };
41
+ var owned = {};
42
+ function id(e) { return e.code || e.key; }
43
+ function fromOverlay(e) {
44
+ return !!keys.host && typeof e.composedPath === 'function' && e.composedPath().indexOf(keys.host) !== -1;
45
+ }
46
+ function guard(e) {
47
+ var k = id(e);
48
+ var mine = fromOverlay(e);
49
+ if (!mine && !owned[k]) return;
50
+ if (e.type === 'keydown') {
51
+ if (mine) owned[k] = true;
52
+ else if (!e.repeat) { delete owned[k]; return; }
53
+ }
54
+ if (e.type === 'keyup') delete owned[k];
55
+ e.stopImmediatePropagation();
56
+ if (mine && keys.handle) keys.handle(e);
57
+ }
58
+ ['keydown', 'keypress', 'keyup'].forEach(function (type) {
59
+ window.addEventListener(type, guard, true);
60
+ });
61
+ window.addEventListener('blur', function () { owned = {}; });
62
+ })();
63
+ `;
64
+
28
65
  const FEEDBACK_OVERLAY_JS = `
29
66
  (function () {
30
67
  var cfg = window.__swarmFeedback;
@@ -135,6 +172,7 @@ const FEEDBACK_OVERLAY_JS = `
135
172
  var panel = document.createElement('div');
136
173
  panel.className = 'panel';
137
174
  var composer = null;
175
+ var composerInput = null;
138
176
  root.appendChild(hl);
139
177
  root.appendChild(markers);
140
178
  root.appendChild(panel);
@@ -207,11 +245,25 @@ const FEEDBACK_OVERLAY_JS = `
207
245
  }
208
246
  render();
209
247
  }
210
- document.addEventListener('keydown', function (e) {
211
- if (e.key !== 'Escape') return;
248
+ function onEscape() {
212
249
  if (state.draft) { closeComposer(); return; }
213
250
  if (state.selecting) setSelecting(false);
251
+ }
252
+ // Escape pressed on the page while selecting. Keys typed in the overlay never
253
+ // get here: the key guard stops them and calls onOverlayKey instead.
254
+ document.addEventListener('keydown', function (e) {
255
+ if (e.key === 'Escape') onEscape();
214
256
  }, true);
257
+ function onOverlayKey(e) {
258
+ if (e.type !== 'keydown') return;
259
+ if (e.key === 'Escape') { onEscape(); return; }
260
+ if (e.key === 'Enter' && (e.metaKey || e.ctrlKey) && composerInput && e.composedPath()[0] === composerInput) {
261
+ e.preventDefault();
262
+ commitDraft(composerInput.value);
263
+ }
264
+ }
265
+ window.__swarmFeedbackKeys.host = host;
266
+ window.__swarmFeedbackKeys.handle = onOverlayKey;
215
267
 
216
268
  // ─── Composer ──────────────────────────────────────────────────────────
217
269
  function openComposer(target) {
@@ -230,9 +282,7 @@ const FEEDBACK_OVERLAY_JS = `
230
282
  add.type = 'button';
231
283
  cancel.addEventListener('click', closeComposer);
232
284
  add.addEventListener('click', function () { commitDraft(input.value); });
233
- input.addEventListener('keydown', function (e) {
234
- if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) { e.preventDefault(); commitDraft(input.value); }
235
- });
285
+ composerInput = input;
236
286
  row.appendChild(cancel);
237
287
  row.appendChild(add);
238
288
  composer.appendChild(row);
@@ -253,6 +303,7 @@ const FEEDBACK_OVERLAY_JS = `
253
303
  function closeComposer() {
254
304
  if (composer) composer.remove();
255
305
  composer = null;
306
+ composerInput = null;
256
307
  state.draft = null;
257
308
  hideHighlight();
258
309
  }
@@ -524,5 +575,13 @@ const FEEDBACK_OVERLAY_JS = `
524
575
  */
525
576
  export function feedbackOverlayScripts(pageId: string): string {
526
577
  const config = JSON.stringify({ pageId }).replace(/</g, "\\u003c");
527
- return `<script>window.__swarmFeedback = ${config};</script><script>${FEEDBACK_OVERLAY_JS}</script>`;
578
+ return `<script>window.__swarmFeedback = ${config};</script><script>${FEEDBACK_KEY_GUARD_JS}${FEEDBACK_OVERLAY_JS}</script>`;
579
+ }
580
+
581
+ /**
582
+ * Key guard served at the top of `<head>` when feedback mode is on, so it runs
583
+ * before any page script and its window capture listeners come first.
584
+ */
585
+ export function feedbackKeyGuardScript(): string {
586
+ return `<script>${FEEDBACK_KEY_GUARD_JS}</script>`;
528
587
  }
@@ -359,7 +359,7 @@ const copilotTargetProfile: AcpTargetProfile = {
359
359
  },
360
360
  };
361
361
 
362
- /** Qwen Code's `--auth-type` choices (0.25.1). `qwen-oauth` needs a browser, so it is left out. */
362
+ /** Qwen Code's `--auth-type` choices (0.25.2). `qwen-oauth` needs a browser, so it is left out. */
363
363
  const QWEN_AUTH_TYPES = ["openai", "openai-responses", "anthropic", "gemini", "vertex-ai"] as const;
364
364
 
365
365
  // Qwen Code is a Gemini CLI fork: QWEN_SYSTEM_MD replaces its built-in prompt
@@ -44,7 +44,7 @@ import type {
44
44
  * default; the image installs exactly this version and the per-task settings
45
45
  * set `amp.updates.mode: "disabled"`. Bump it deliberately, with a live run.
46
46
  */
47
- export const AMP_PACKAGE = "@ampcode/cli@0.0.1792065649-g5a30fc";
47
+ export const AMP_PACKAGE = "@ampcode/cli@0.0.1792152053-g57ff9b";
48
48
 
49
49
  /** MCP server name. Amp exposes its tools as `agent_swarm` / `store_progress` (dashes become underscores). */
50
50
  const AMP_MCP_SERVER_NAME = "agent-swarm";