@desplega.ai/agent-swarm 2.4.3 → 2.4.5

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 (270) hide show
  1. package/dist/{acp-adapter-1885dpr7.js → acp-adapter-2wdkktv6.js} +1 -2
  2. package/dist/{actions-pn76n32q.js → actions-c40ebcm4.js} +18 -18
  3. package/dist/{amp-adapter-af84rx1d.js → amp-adapter-wb67m95z.js} +2 -2
  4. package/dist/{app-6sv8mhzm.js → app-353vre8g.js} +3 -3
  5. package/dist/{assistant-sc2tf825.js → assistant-sj1njh23.js} +21 -21
  6. package/dist/{boot-reembed-y3m0ydzj.js → boot-reembed-b9c3az3c.js} +1 -1
  7. package/dist/{boot-reembed-gvxjs2py.js → boot-reembed-qdkvxeem.js} +1 -1
  8. package/dist/{boot-scrub-sweep-1j9mk05f.js → boot-scrub-sweep-m3wh8eks.js} +2 -2
  9. package/dist/{boot-seeds-37sp626b.js → boot-seeds-j2273s54.js} +4 -4
  10. package/dist/{claude-adapter-rp0jtnwr.js → claude-adapter-m7q0a085.js} +2 -2
  11. package/dist/{claude-managed-adapter-h2y889ge.js → claude-managed-adapter-5ptb6tzs.js} +2 -2
  12. package/dist/{claude-managed-setup-rdt248k7.js → claude-managed-setup-smrqk5j5.js} +3 -3
  13. package/dist/{claude-sdk-session-53rcw5we.js → claude-sdk-session-38xnz21e.js} +8284 -8267
  14. package/dist/{cli-6eyncg0y.js → cli-018r4jq6.js} +34 -6
  15. package/dist/{cli-sds5zzs8.js → cli-1f3qzw6y.js} +78 -12
  16. package/dist/{cli-f0p0f6ja.js → cli-1sp861s4.js} +7 -7
  17. package/dist/{cli-dm8p2dpf.js → cli-1syn2xy1.js} +10 -10
  18. package/dist/{cli-tkf4x6ha.js → cli-37j04k05.js} +11 -11
  19. package/dist/{cli-v87dr6e3.js → cli-3a45zcgm.js} +1 -1
  20. package/dist/{cli-1n9gdg56.js → cli-43x64hv3.js} +4 -4
  21. package/dist/{cli-p0qerd2r.js → cli-4zfqgjc5.js} +2 -2
  22. package/dist/{cli-aa3tb29r.js → cli-7c9qs530.js} +1 -1
  23. package/dist/{cli-9qtjfd60.js → cli-848hxdwj.js} +1 -1
  24. package/dist/{cli-1xsraagq.js → cli-94gwyg7m.js} +8 -8
  25. package/dist/{cli-0dxxwfkt.js → cli-9hstj09h.js} +17 -14
  26. package/dist/{cli-1012xb7j.js → cli-9yqgm9nd.js} +22 -22
  27. package/dist/{cli-pygdsgh8.js → cli-a92r1127.js} +1 -1
  28. package/dist/{cli-8vverzf7.js → cli-ba8ne1es.js} +5 -0
  29. package/dist/{cli-bxrqvpg5.js → cli-be2161h1.js} +7 -7
  30. package/dist/{cli-npfe9t1s.js → cli-bnes4z6b.js} +2 -2
  31. package/dist/{cli-aypbg9r2.js → cli-cxxe61f6.js} +1 -1
  32. package/dist/{cli-vq9gqarq.js → cli-dk44dxec.js} +1 -1
  33. package/dist/{cli-836ehxmt.js → cli-eww120g8.js} +3 -2
  34. package/dist/{cli-a0kcty51.js → cli-f01h25yt.js} +1 -1
  35. package/dist/{cli-mzkwkbdy.js → cli-f0g3pgvf.js} +1 -1
  36. package/dist/{cli-1ma335fz.js → cli-ftc1g5tj.js} +2 -2
  37. package/dist/{cli-4yef8ghv.js → cli-fv5hepyy.js} +3 -3
  38. package/dist/{cli-58vwn100.js → cli-g35m2xwm.js} +4 -4
  39. package/dist/{cli-qje9k2nh.js → cli-g9ypd5en.js} +12 -2
  40. package/dist/{cli-gx0j2khs.js → cli-j99azg3g.js} +3 -3
  41. package/dist/{cli-er83cjbn.js → cli-k8g16gz3.js} +416 -116
  42. package/dist/{cli-qp2m4btv.js → cli-kpw04va0.js} +2 -2
  43. package/dist/{cli-8y99kfey.js → cli-ms85309d.js} +33 -15
  44. package/dist/{cli-kgnkyabk.js → cli-n72rg53p.js} +1555 -645
  45. package/dist/{cli-e4d78xsv.js → cli-p14m2cnp.js} +1 -1
  46. package/dist/{cli-1eszr343.js → cli-qfxc84gh.js} +1 -1
  47. package/dist/{cli-wk28hzqn.js → cli-qwhy2qma.js} +1 -1
  48. package/dist/{cli-73ezfhha.js → cli-st8aw1m6.js} +228 -11
  49. package/dist/{cli-zvbb4n0e.js → cli-tw4p90jb.js} +92 -192
  50. package/dist/{cli-038fga5k.js → cli-v0yrfmdh.js} +148 -183
  51. package/dist/{cli-6aen3ezb.js → cli-v8fjakwf.js} +1 -1
  52. package/dist/{cli-99jez72r.js → cli-w9etnnxk.js} +6 -1
  53. package/dist/{cli-vcgscaan.js → cli-wt0jf9hm.js} +217 -80
  54. package/dist/{cli-s9rr4gr2.js → cli-xpa64rek.js} +1065 -305
  55. package/dist/{cli-h0mgxann.js → cli-z7d29rw9.js} +2 -2
  56. package/dist/cli.js +45 -20
  57. package/dist/{codex-adapter-2zrmab9b.js → codex-adapter-tn6k5mkg.js} +1 -1
  58. package/dist/{codex-session-runner-kbkz62vc.js → codex-session-runner-7vjt7jbw.js} +1 -1
  59. package/dist/{commands-hd574th6.js → commands-t7rz9146.js} +2 -2
  60. package/dist/{controller-pz5mcfg4.js → controller-m2jasma5.js} +2 -2
  61. package/dist/{controllers-connect-5rgq2hfk.js → controllers-connect-9ftpex99.js} +19 -19
  62. package/dist/{cursor-adapter-mgaprq0j.js → cursor-adapter-mjv79e4z.js} +1 -1
  63. package/dist/{db-6nf1207q.js → db-6sfrn9g3.js} +1 -1
  64. package/dist/{db-h06dxp05.js → db-rn673r06.js} +3 -3
  65. package/dist/{dispatcher-zthpy4m1.js → dispatcher-c155xz0p.js} +17 -17
  66. package/dist/{dsh-adapter-64dhw0j7.js → dsh-adapter-3mmszc1x.js} +1 -1
  67. package/dist/{external-channel-x01t7ke6.js → external-channel-35k6tkv8.js} +2 -2
  68. package/dist/{grok-adapter-h21jv594.js → grok-adapter-tfrq7tg1.js} +2 -3
  69. package/dist/handlers-7gw64dxw.js +39 -0
  70. package/dist/{heartbeat-zzfksckz.js → heartbeat-dyg4npkp.js} +20 -20
  71. package/dist/{hook-z4zhzj1f.js → hook-drspbwpe.js} +1 -1
  72. package/dist/{hook-3xfqhh2s.js → hook-x26b31bg.js} +1 -1
  73. package/dist/{http-q813tmya.js → http-pk972xyw.js} +571 -294
  74. package/dist/{index-va3y63es.js → index-6srwvd5b.js} +1 -2
  75. package/dist/{index-x8xbyn1t.js → index-arytgj5q.js} +1 -1
  76. package/dist/{index-wbbs14c5.js → index-bgdyvcnv.js} +17 -17
  77. package/dist/{index-s98037v0.js → index-cbxkmfc5.js} +1 -2
  78. package/dist/{index-c4ykd0f1.js → index-dr94zbnh.js} +13348 -11611
  79. package/dist/{index-m0a4ydqj.js → index-gabbcdn2.js} +17 -17
  80. package/dist/{index-220tk9bw.js → index-jwxkkex2.js} +139 -76
  81. package/dist/index-tfb26zhy.js +41 -0
  82. package/dist/{keepalive-y5t1my7m.js → keepalive-7yrng7px.js} +3 -3
  83. package/dist/{lead-2xwtq66b.js → lead-7e02z839.js} +29 -30
  84. package/dist/{maintenance-ybfsaedv.js → maintenance-e6cemdqt.js} +1 -1
  85. package/dist/{node-3v3f08f7.js → node-de6akczr.js} +10 -9
  86. package/dist/{oauth-refresh-sweep-909jmn12.js → oauth-refresh-sweep-yw3xdbcj.js} +2 -2
  87. package/dist/{onboard-xn6fkkys.js → onboard-0a9y5yrc.js} +2 -2
  88. package/dist/{otel-impl-e6mn0z13.js → otel-impl-trjpkfb7.js} +1 -1
  89. package/dist/{pricing-refresh-ygd362ep.js → pricing-refresh-d2b5my0y.js} +3 -3
  90. package/dist/{rbac-roles-mb470p4e.js → rbac-roles-hey3yv9d.js} +2 -2
  91. package/dist/{rbac-roles-c9axvfnz.js → rbac-roles-nxh5ahwk.js} +2 -2
  92. package/dist/{registry-rxw10xzg.js → registry-ajhexe44.js} +1 -1
  93. package/dist/{render-v2-g0306nh1.js → render-v2-rr3p2atg.js} +3 -3
  94. package/dist/{resume-v315hnq6.js → resume-2efkf1s1.js} +5 -5
  95. package/dist/{session-access-2zg285a1.js → session-access-s2mttxpw.js} +6 -4
  96. package/dist/{setup-a4mtf02k.js → setup-vypma5yt.js} +1 -1
  97. package/dist/{work-objects-9fr0fgtn.js → work-objects-rvhckdkp.js} +20 -20
  98. package/dist/{worker-qae22jeb.js → worker-5snytpbd.js} +29 -30
  99. package/dist/worker-token-xf11yyds.js +136 -0
  100. package/openapi.json +303 -13
  101. package/package.json +8 -8
  102. package/src/apps/action-runner.ts +4 -0
  103. package/src/apps/catalog.generated.json +209 -4
  104. package/src/apps/page-validator.ts +21 -3
  105. package/src/be/chunking.ts +42 -9
  106. package/src/be/db/tasks/draft-text.ts +15 -0
  107. package/src/be/db/tasks/read.ts +14 -1
  108. package/src/be/db.ts +5 -1
  109. package/src/be/memory/index-content.ts +36 -17
  110. package/src/be/memory/providers/openai-embedding.ts +115 -61
  111. package/src/be/memory/providers/sqlite-store.ts +69 -23
  112. package/src/be/memory/reranker.ts +31 -1
  113. package/src/be/memory/search-candidates.ts +47 -0
  114. package/src/be/memory/types.ts +14 -0
  115. package/src/be/migrations/232_agent_session_tokens.sql +41 -0
  116. package/src/be/scripts/typecheck.ts +2 -0
  117. package/src/be/seed-scripts/catalog/heartbeat-jev-gate.ts +116 -55
  118. package/src/be/seed-scripts/index.ts +2 -2
  119. package/src/be/swarm-config-guard.ts +40 -1
  120. package/src/be/task-attachment-store.ts +30 -0
  121. package/src/be/users.ts +169 -12
  122. package/src/cli.tsx +24 -0
  123. package/src/commands/worker-token.ts +164 -0
  124. package/src/extensions/catalog.generated.json +4 -4
  125. package/src/extensions/contract-types.generated.ts +1 -1
  126. package/src/extensions/contract.ts +7 -0
  127. package/src/fs/provider.ts +13 -1
  128. package/src/heartbeat/heartbeat.ts +31 -11
  129. package/src/http/apps.ts +43 -5
  130. package/src/http/codex-oauth-keep-warm.ts +27 -9
  131. package/src/http/config.ts +5 -3
  132. package/src/http/core.ts +2 -0
  133. package/src/http/fs.ts +54 -34
  134. package/src/http/mcp-body.ts +37 -0
  135. package/src/http/mcp-user.ts +3 -5
  136. package/src/http/mcp.ts +6 -7
  137. package/src/http/memory.ts +26 -15
  138. package/src/http/page-proxy.ts +61 -5
  139. package/src/http/pages-public.ts +27 -44
  140. package/src/http/pages.ts +4 -4
  141. package/src/http/script-runs.ts +2 -0
  142. package/src/http/session-access.ts +51 -2
  143. package/src/http/sessions.ts +249 -10
  144. package/src/http/tasks.ts +4 -32
  145. package/src/http/users.ts +12 -1
  146. package/src/integrations/chat-sdk/adapters.ts +7 -3
  147. package/src/integrations/chat-sdk/attachments.ts +90 -12
  148. package/src/integrations/chat-sdk/facts.ts +120 -0
  149. package/src/integrations/chat-sdk/inbound.ts +156 -8
  150. package/src/integrations/chat-sdk/limits.ts +24 -0
  151. package/src/integrations/chat-sdk/outbound-files.ts +186 -0
  152. package/src/integrations/chat-sdk/outbound.ts +234 -4
  153. package/src/integrations/chat-sdk/relay.ts +4 -1
  154. package/src/integrations/chat-sdk/router.ts +28 -2
  155. package/src/integrations/chat-sdk/steering.ts +9 -0
  156. package/src/integrations/chat-sdk/templates.ts +14 -0
  157. package/src/integrations/chat-sdk/thread-buffer.ts +4 -0
  158. package/src/mcp-limits.ts +20 -0
  159. package/src/pages/browser-sdk.ts +8 -3
  160. package/src/pages/feedback-overlay.ts +1 -1
  161. package/src/providers/acp-adapter.ts +33 -13
  162. package/src/providers/acp-swarm-events.ts +1 -1
  163. package/src/providers/amp-adapter.ts +1 -1
  164. package/src/providers/codex-adapter.ts +106 -119
  165. package/src/providers/codex-agents-md.ts +35 -229
  166. package/src/providers/codex-oauth/storage.ts +1 -1
  167. package/src/providers/cursor-adapter.ts +1 -1
  168. package/src/rbac/legacy-policy.ts +3 -1
  169. package/src/rbac/permissions.ts +4 -0
  170. package/src/realtime/transport.ts +1 -2
  171. package/src/script-workflows/workflow-ctx.ts +1 -0
  172. package/src/scripts-runtime/types/stdlib.d.ts +2 -0
  173. package/src/scripts-runtime/types/swarm-extension.d.ts +8 -0
  174. package/src/scripts-runtime/types/swarm-sdk.d.ts +2 -0
  175. package/src/server-user.ts +2 -0
  176. package/src/server.ts +2 -0
  177. package/src/slack/assistant.ts +2 -2
  178. package/src/slack/files.ts +2 -0
  179. package/src/slack/handlers.ts +4 -4
  180. package/src/slack/inbound-files.ts +39 -6
  181. package/src/slack/thread-buffer.ts +2 -1
  182. package/src/tasks/task-terminal-effects.ts +21 -17
  183. package/src/tests/acp-adapter.test.ts +10 -23
  184. package/src/tests/acp-fake-agent.ts +36 -0
  185. package/src/tests/acp-swarm-events.test.ts +1 -1
  186. package/src/tests/agent-session-tokens.test.ts +452 -0
  187. package/src/tests/apps-action-parent-task.test.ts +295 -0
  188. package/src/tests/apps-card-grid.test.ts +197 -0
  189. package/src/tests/apps-catalog-additions.test.ts +120 -0
  190. package/src/tests/apps-tree.test.ts +129 -0
  191. package/src/tests/chat-sdk-attachments-reply.test.ts +52 -0
  192. package/src/tests/chat-sdk-config.test.ts +11 -0
  193. package/src/tests/chat-sdk-facts.test.ts +23 -0
  194. package/src/tests/chat-sdk-inbound.test.ts +558 -4
  195. package/src/tests/chat-sdk-outbound-files.test.ts +652 -0
  196. package/src/tests/codex-adapter.test.ts +167 -242
  197. package/src/tests/codex-oauth-keep-warm.test.ts +85 -1
  198. package/src/tests/config-api-only-keys.test.ts +9 -0
  199. package/src/tests/config-session-auth.test.ts +14 -0
  200. package/src/tests/cors-allowlist.test.ts +1 -1
  201. package/src/tests/docs-index-footprint.test.js +2 -2
  202. package/src/tests/extensions-example-slack-reply-gate.test.ts +262 -91
  203. package/src/tests/grok-adapter.test.ts +3 -3
  204. package/src/tests/heartbeat-checklist.test.ts +33 -0
  205. package/src/tests/heartbeat-jev-gate-credentials.test.ts +44 -0
  206. package/src/tests/heartbeat-jev-gate.test.ts +265 -1
  207. package/src/tests/kv-page-proxy.test.ts +91 -5
  208. package/src/tests/launch-password-rejection.test.ts +2 -2
  209. package/src/tests/mcp-input-caps.test.ts +294 -0
  210. package/src/tests/memory-chunked-search-starvation.test.ts +200 -0
  211. package/src/tests/memory-embedding-input-cap.test.ts +114 -0
  212. package/src/tests/memory.test.ts +14 -0
  213. package/src/tests/migration-232-agent-session-tokens.test.ts +67 -0
  214. package/src/tests/page-guest-session.test.ts +19 -8
  215. package/src/tests/page-proxy-authed.test.ts +1 -1
  216. package/src/tests/page-proxy.test.ts +17 -10
  217. package/src/tests/page-session.test.ts +102 -1
  218. package/src/tests/pages-authed-mode.test.ts +82 -12
  219. package/src/tests/pages-password-mode.test.ts +5 -5
  220. package/src/tests/rbac-engine.test.ts +1 -0
  221. package/src/tests/realtime-transport.test.ts +13 -7
  222. package/src/tests/runtime-child-token-cleanup.test.ts +3 -3
  223. package/src/tests/script-connections.test.ts +2 -0
  224. package/src/tests/script-runs-http.test.ts +84 -0
  225. package/src/tests/scripts-http.test.ts +10 -1
  226. package/src/tests/session-route-posture.test.ts +2 -0
  227. package/src/tests/swarm-config-export-to-env.test.ts +22 -3
  228. package/src/tests/task-audio-transcription.test.ts +299 -0
  229. package/src/tests/task-completion-memory-chunking.test.ts +434 -0
  230. package/src/tests/task-terminal-secret-scrub.test.ts +6 -1
  231. package/src/tests/transcription.test.ts +299 -0
  232. package/src/tests/worker-token-cli.test.ts +76 -0
  233. package/src/tests/workflow-executors.test.ts +4 -2
  234. package/src/tests/workflow-system-one-decision-providers.test.ts +98 -1
  235. package/src/tests/workflow-system-one-decision.test.ts +2 -1
  236. package/src/tests/workflow-system-one-extraction.test.ts +805 -0
  237. package/src/tools/chat-access.ts +10 -0
  238. package/src/tools/chat-post.ts +31 -11
  239. package/src/tools/chat-reply.ts +32 -10
  240. package/src/tools/memory-search.ts +3 -1
  241. package/src/tools/system-one-decide.ts +17 -4
  242. package/src/types.ts +8 -1
  243. package/src/utils/config-env-export.ts +3 -1
  244. package/src/utils/page-session.ts +133 -20
  245. package/src/utils/request-auth-context.ts +2 -1
  246. package/src/utils/transcription.ts +246 -0
  247. package/src/workflows/definition.ts +7 -1
  248. package/src/workflows/engine.ts +18 -12
  249. package/src/workflows/executors/index.ts +1 -0
  250. package/src/workflows/executors/registry.ts +2 -0
  251. package/src/workflows/executors/system-one-decision.ts +84 -262
  252. package/src/workflows/executors/system-one-extract-wire.ts +57 -0
  253. package/src/workflows/executors/system-one-extraction.ts +830 -0
  254. package/src/workflows/executors/system-one-providers.ts +316 -114
  255. package/src/workflows/executors/system-one-transport.ts +264 -0
  256. package/templates/extensions/slack-reply-gate/README.md +24 -22
  257. package/templates/extensions/slack-reply-gate/hooks.ts +146 -95
  258. package/templates/extensions/slack-reply-gate/manifest.json +2 -2
  259. package/templates/official/lead/HEARTBEAT.md +1 -1
  260. package/templates/skills/apps/SKILL.md +31 -7
  261. package/templates/skills/apps/content.md +31 -7
  262. package/templates/skills/chat-sdk/content.md +6 -2
  263. package/templates/skills/code-quality/content.md +2 -1
  264. package/templates/skills/heartbeat-runbook/content.md +8 -8
  265. package/templates/skills/pages/content.md +6 -4
  266. package/templates/skills/swarm-scripts/SKILL.md +2 -2
  267. package/templates/skills/swarm-scripts/content.md +2 -2
  268. package/dist/cli-v1sjhn52.js +0 -7
  269. package/dist/handlers-2qjw434w.js +0 -39
  270. package/dist/index-d0g18bax.js +0 -41
@@ -7,7 +7,16 @@
7
7
  */
8
8
 
9
9
  import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test } from "bun:test";
10
- import { lstatSync, mkdirSync, readFileSync, readlinkSync, rmSync, symlinkSync } from "node:fs";
10
+ import {
11
+ lstatSync,
12
+ mkdirSync,
13
+ mkdtempSync,
14
+ readlinkSync,
15
+ realpathSync,
16
+ rmSync,
17
+ symlinkSync,
18
+ } from "node:fs";
19
+ import { homedir, tmpdir } from "node:os";
11
20
  import { join } from "node:path";
12
21
  import type {
13
22
  AgentMessageItem,
@@ -17,7 +26,6 @@ import type {
17
26
  ThreadItem,
18
27
  } from "../providers/codex-adapter";
19
28
  import { buildCodexConfig, CodexAdapter } from "../providers/codex-adapter";
20
- import { writeCodexAgentsMd } from "../providers/codex-agents-md";
21
29
  import {
22
30
  CODEX_DEFAULT_MODEL,
23
31
  computeCodexCostUsd,
@@ -863,7 +871,7 @@ describe("CodexSession event mapping", () => {
863
871
 
864
872
  const config = testConfig({
865
873
  logFile: join(tmpLogDir, "happy.log"),
866
- cwd: "", // disable AGENTS.md writing
874
+ cwd: "",
867
875
  });
868
876
 
869
877
  const { emitted, result } = await runSessionWithFakeThread(events, config);
@@ -1268,242 +1276,161 @@ describe("CodexAdapter.canResume", () => {
1268
1276
  });
1269
1277
  });
1270
1278
 
1271
- describe("writeCodexAgentsMd round-trip", () => {
1272
- const tmpDir = `/tmp/codex-agents-md-test-${Date.now()}`;
1273
- // Crash-recovery records live outside every per-test repo dir.
1274
- const stateDir = join(tmpDir, ".state");
1275
-
1276
- beforeAll(() => {
1277
- mkdirSync(tmpDir, { recursive: true });
1278
- });
1279
-
1280
- afterAll(() => {
1281
- rmSync(tmpDir, { recursive: true, force: true });
1282
- });
1283
-
1284
- afterEach(async () => {
1285
- // Clean any stray files between tests.
1286
- await Bun.$`rm -f ${tmpDir}/*`.quiet().nothrow();
1287
- });
1288
-
1289
- test("no-op when systemPrompt is empty", async () => {
1290
- const handle = await writeCodexAgentsMd(tmpDir, "");
1291
- expect(await Bun.file(join(tmpDir, "AGENTS.md")).exists()).toBe(false);
1292
- await handle.cleanup();
1293
- expect(await Bun.file(join(tmpDir, "AGENTS.md")).exists()).toBe(false);
1294
- });
1295
-
1296
- test("no-op when cwd is falsy", async () => {
1297
- const handle = await writeCodexAgentsMd(undefined, "test prompt");
1298
- await handle.cleanup();
1299
- // Nothing to assert on fs — just make sure no throw happens.
1300
- expect(true).toBe(true);
1301
- });
1302
-
1303
- test("creates fresh file when AGENTS.md and CLAUDE.md are absent", async () => {
1304
- const dir = join(tmpDir, "fresh-no-claude");
1305
- mkdirSync(dir, { recursive: true });
1306
- const handle = await writeCodexAgentsMd(dir, "my prompt");
1307
- const agentsMd = join(dir, "AGENTS.md");
1308
-
1309
- expect(await Bun.file(agentsMd).exists()).toBe(true);
1310
- const content = await Bun.file(agentsMd).text();
1311
- expect(content).toContain("<swarm_system_prompt>");
1312
- expect(content).toContain("my prompt");
1313
- expect(content).toContain("</swarm_system_prompt>");
1314
-
1315
- await handle.cleanup();
1316
- expect(await Bun.file(agentsMd).exists()).toBe(false);
1317
- });
1318
-
1319
- test("prepends block above CLAUDE.md content when only CLAUDE.md exists", async () => {
1320
- const dir = join(tmpDir, "fresh-with-claude");
1321
- mkdirSync(dir, { recursive: true });
1322
- await Bun.write(join(dir, "CLAUDE.md"), "# My Project\n\nInstructions.");
1323
-
1324
- const handle = await writeCodexAgentsMd(dir, "swarm prompt");
1325
- const agentsMd = join(dir, "AGENTS.md");
1326
-
1327
- const content = await Bun.file(agentsMd).text();
1328
- expect(content.indexOf("<swarm_system_prompt>")).toBeLessThan(content.indexOf("# My Project"));
1329
- expect(content).toContain("swarm prompt");
1330
- expect(content).toContain("# My Project");
1331
-
1332
- await handle.cleanup();
1333
- // Fresh creation → cleanup removes the file entirely (even though CLAUDE.md remains).
1334
- expect(await Bun.file(agentsMd).exists()).toBe(false);
1335
- expect(await Bun.file(join(dir, "CLAUDE.md")).exists()).toBe(true);
1336
- });
1337
-
1338
- test("replaces existing managed block in place", async () => {
1339
- const dir = join(tmpDir, "replace-block");
1340
- mkdirSync(dir, { recursive: true });
1341
- const original = `<swarm_system_prompt>
1342
- stale
1343
- </swarm_system_prompt>
1344
-
1345
- # Keep me`;
1346
- await Bun.write(join(dir, "AGENTS.md"), original);
1347
-
1348
- const handle = await writeCodexAgentsMd(dir, "fresh prompt");
1349
- const agentsMd = join(dir, "AGENTS.md");
1350
-
1351
- const updated = await Bun.file(agentsMd).text();
1352
- expect(updated).toContain("fresh prompt");
1353
- expect(updated).not.toContain("stale");
1354
- expect(updated).toContain("# Keep me");
1355
-
1356
- await handle.cleanup();
1357
- // Not a fresh file — cleanup strips the block but leaves the rest intact.
1358
- expect(await Bun.file(agentsMd).exists()).toBe(true);
1359
- const after = await Bun.file(agentsMd).text();
1360
- expect(after).not.toContain("<swarm_system_prompt>");
1361
- expect(after).toContain("# Keep me");
1362
- });
1363
-
1364
- test("prepends block when existing AGENTS.md has no block", async () => {
1365
- const dir = join(tmpDir, "prepend-block");
1366
- mkdirSync(dir, { recursive: true });
1367
- await Bun.write(join(dir, "AGENTS.md"), "# Project instructions");
1368
-
1369
- const handle = await writeCodexAgentsMd(dir, "swarm prompt");
1370
- const agentsMd = join(dir, "AGENTS.md");
1371
-
1372
- const updated = await Bun.file(agentsMd).text();
1373
- expect(updated).toContain("swarm prompt");
1374
- expect(updated).toContain("# Project instructions");
1375
- expect(updated.indexOf("<swarm_system_prompt>")).toBeLessThan(
1376
- updated.indexOf("# Project instructions"),
1279
+ describe("Codex session instructions", () => {
1280
+ for (const kind of [
1281
+ "file",
1282
+ "in-tree link",
1283
+ "out-of-tree link",
1284
+ "dangling link",
1285
+ "missing link",
1286
+ ]) {
1287
+ // Windows requires symlink privileges; CI exercises the link cases on Linux.
1288
+ test.skipIf(kind !== "file" && process.platform === "win32")(
1289
+ `recovers a crashed legacy session before starting Codex (${kind})`,
1290
+ async () => {
1291
+ const dir = mkdtempSync(join(tmpdir(), "codex-legacy-instructions-"));
1292
+ const cwd = join(dir, "repo");
1293
+ mkdirSync(cwd);
1294
+ const agentsMd = join(cwd, "AGENTS.md");
1295
+ const claudeMd = join(cwd, "CLAUDE.md");
1296
+ const original = "# Repo rules\n";
1297
+ const edited = `${original}- Keep the agent's edit\n`;
1298
+ const outside = join(dir, "outside.md");
1299
+ const linkTarget =
1300
+ kind === "out-of-tree link"
1301
+ ? "../outside.md"
1302
+ : kind === "dangling link"
1303
+ ? "missing.md"
1304
+ : "CLAUDE.md";
1305
+ const realCwd = realpathSync(cwd);
1306
+ const stateDir = join(homedir(), ".cache", "agent-swarm", "codex-agents-md");
1307
+ const key = new Bun.CryptoHasher("sha256").update(realCwd).digest("hex");
1308
+ const recordPath = join(stateDir, `${key}.json`);
1309
+ try {
1310
+ await Bun.write(claudeMd, original);
1311
+ await Bun.write(outside, "OUTSIDE\n");
1312
+ if (kind !== "missing link") {
1313
+ await Bun.write(
1314
+ agentsMd,
1315
+ `<swarm_system_prompt>\nSTALE\n</swarm_system_prompt>\n${edited}`,
1316
+ );
1317
+ }
1318
+ if (kind !== "file") {
1319
+ mkdirSync(stateDir, { recursive: true });
1320
+ await Bun.write(recordPath, JSON.stringify({ cwd: realCwd, linkTarget }));
1321
+ }
1322
+ const fake = makeFakeAppServer(async (method, params, emit) => {
1323
+ if (method === "thread/start") {
1324
+ expect(params).toMatchObject({ developerInstructions: "new session instructions" });
1325
+ expect(await Bun.file(recordPath).exists()).toBe(false);
1326
+ if (kind === "file") {
1327
+ expect(lstatSync(agentsMd).isFile()).toBe(true);
1328
+ expect(await Bun.file(agentsMd).text()).toBe(edited);
1329
+ } else {
1330
+ expect(lstatSync(agentsMd).isSymbolicLink()).toBe(true);
1331
+ expect(readlinkSync(agentsMd)).toBe(linkTarget);
1332
+ }
1333
+ expect(await Bun.file(claudeMd).text()).toBe(
1334
+ kind === "in-tree link" ? edited : original,
1335
+ );
1336
+ expect(await Bun.file(outside).text()).toBe("OUTSIDE\n");
1337
+ return { thread: { id: "legacy-thread" } };
1338
+ }
1339
+ if (method === "turn/start") {
1340
+ queueMicrotask(() => {
1341
+ emit("turn/completed", {
1342
+ threadId: "legacy-thread",
1343
+ turn: { id: "legacy-turn", status: "completed", error: null },
1344
+ });
1345
+ });
1346
+ return { turn: { id: "legacy-turn" } };
1347
+ }
1348
+ throw new Error(`Unexpected method: ${method}`);
1349
+ });
1350
+ const session = await new CodexAdapter({
1351
+ bypassSubprocess: true,
1352
+ appServerFactory: () => fake.client as never,
1353
+ }).createSession(
1354
+ testConfig({
1355
+ cwd,
1356
+ systemPrompt: "new session instructions",
1357
+ logFile: join(dir, "session.log"),
1358
+ taskId: "",
1359
+ }),
1360
+ );
1361
+ await session.waitForCompletion();
1362
+ } finally {
1363
+ rmSync(recordPath, { force: true });
1364
+ rmSync(dir, { recursive: true, force: true });
1365
+ }
1366
+ },
1377
1367
  );
1368
+ }
1378
1369
 
1379
- await handle.cleanup();
1380
- const after = await Bun.file(agentsMd).text();
1381
- expect(after).not.toContain("<swarm_system_prompt>");
1382
- expect(after).toContain("# Project instructions");
1383
- });
1384
-
1385
- test("cleanup restores a plain AGENTS.md byte-identically", async () => {
1386
- const dir = join(tmpDir, "byte-identical");
1387
- mkdirSync(dir, { recursive: true });
1388
- const original = "# Project instructions\n\nDo things.\n";
1389
- await Bun.write(join(dir, "AGENTS.md"), original);
1390
-
1391
- const handle = await writeCodexAgentsMd(dir, "swarm prompt");
1392
- await handle.cleanup();
1393
-
1394
- expect(readFileSync(join(dir, "AGENTS.md"), "utf8")).toBe(original);
1395
- });
1396
-
1397
- test("never writes through an AGENTS.md -> CLAUDE.md symlink", async () => {
1398
- const dir = join(tmpDir, "symlink");
1399
- mkdirSync(dir, { recursive: true });
1400
- const original = "# Repo rules\n\nDo things.\n";
1401
- const claudeMd = join(dir, "CLAUDE.md");
1402
- const agentsMd = join(dir, "AGENTS.md");
1403
- await Bun.write(claudeMd, original);
1404
- symlinkSync("CLAUDE.md", agentsMd);
1405
-
1406
- const handle = await writeCodexAgentsMd(dir, "swarm prompt", { stateDir });
1407
-
1408
- // During the session: AGENTS.md is a real file, CLAUDE.md is untouched.
1409
- expect(lstatSync(agentsMd).isSymbolicLink()).toBe(false);
1410
- const during = readFileSync(agentsMd, "utf8");
1411
- expect(during).toContain("swarm prompt");
1412
- expect(during).toContain("# Repo rules");
1413
- expect(readFileSync(claudeMd, "utf8")).toBe(original);
1414
-
1415
- await handle.cleanup();
1416
-
1417
- expect(lstatSync(agentsMd).isSymbolicLink()).toBe(true);
1418
- expect(readlinkSync(agentsMd)).toBe("CLAUDE.md");
1419
- expect(readFileSync(claudeMd, "utf8")).toBe(original);
1420
- });
1421
-
1422
- test("carries agent edits to a symlinked AGENTS.md over to the link target", async () => {
1423
- const dir = join(tmpDir, "symlink-edit");
1424
- mkdirSync(dir, { recursive: true });
1425
- const claudeMd = join(dir, "CLAUDE.md");
1426
- const agentsMd = join(dir, "AGENTS.md");
1427
- await Bun.write(claudeMd, "# Repo rules\n");
1428
- symlinkSync("CLAUDE.md", agentsMd);
1429
-
1430
- const handle = await writeCodexAgentsMd(dir, "swarm prompt", { stateDir });
1431
- await Bun.write(agentsMd, `${readFileSync(agentsMd, "utf8")}- new rule\n`);
1432
- await handle.cleanup();
1433
-
1434
- expect(lstatSync(agentsMd).isSymbolicLink()).toBe(true);
1435
- expect(readFileSync(claudeMd, "utf8")).toBe("# Repo rules\n- new rule\n");
1436
- });
1437
-
1438
- test("restores a symlink left swapped out by a crashed session", async () => {
1439
- const dir = join(tmpDir, "symlink-crash");
1440
- mkdirSync(dir, { recursive: true });
1441
- const original = "# Repo rules\n";
1442
- const claudeMd = join(dir, "CLAUDE.md");
1443
- const agentsMd = join(dir, "AGENTS.md");
1444
- await Bun.write(claudeMd, original);
1445
- symlinkSync("CLAUDE.md", agentsMd);
1446
-
1447
- // First session never runs cleanup (crash).
1448
- await writeCodexAgentsMd(dir, "first prompt", { stateDir });
1449
- expect(lstatSync(agentsMd).isSymbolicLink()).toBe(false);
1450
-
1451
- const handle = await writeCodexAgentsMd(dir, "second prompt", { stateDir });
1452
- const during = readFileSync(agentsMd, "utf8");
1453
- expect(during).toContain("second prompt");
1454
- expect(during).not.toContain("first prompt");
1455
- expect(readFileSync(claudeMd, "utf8")).toBe(original);
1456
-
1457
- await handle.cleanup();
1458
- expect(lstatSync(agentsMd).isSymbolicLink()).toBe(true);
1459
- expect(readFileSync(claudeMd, "utf8")).toBe(original);
1460
- });
1461
-
1462
- test("ignores a forged symlink marker pointing out of the repo", async () => {
1463
- const dir = join(tmpDir, "forged-marker");
1464
- mkdirSync(dir, { recursive: true });
1465
- const secretPath = join(tmpDir, "outside-secret.txt");
1466
- const secret = "TOP-SECRET-CONTENTS\n";
1467
- await Bun.write(secretPath, secret);
1468
- const agentsMd = join(dir, "AGENTS.md");
1469
-
1470
- for (const forged of ["../outside-secret.txt", secretPath]) {
1471
- const original = `<swarm_system_prompt symlink="${encodeURIComponent(forged)}">\nold\n</swarm_system_prompt>\n# Rules\n`;
1472
- await Bun.write(agentsMd, original);
1473
-
1474
- const handle = await writeCodexAgentsMd(dir, "swarm prompt", { stateDir });
1475
- expect(lstatSync(agentsMd).isSymbolicLink()).toBe(false);
1476
- expect(readFileSync(agentsMd, "utf8")).not.toContain("TOP-SECRET");
1477
- expect(readFileSync(secretPath, "utf8")).toBe(secret);
1478
-
1479
- await handle.cleanup();
1480
- expect(lstatSync(agentsMd).isSymbolicLink()).toBe(false);
1481
- expect(readFileSync(agentsMd, "utf8")).toBe(original);
1482
- expect(readFileSync(secretPath, "utf8")).toBe(secret);
1483
- }
1484
- });
1485
-
1486
- test("never reads or writes an out-of-tree symlink target", async () => {
1487
- const dir = join(tmpDir, "symlink-out-of-tree");
1488
- mkdirSync(dir, { recursive: true });
1489
- const secretPath = join(tmpDir, "outside-target.txt");
1490
- const secret = "OUT-OF-TREE\n";
1491
- await Bun.write(secretPath, secret);
1492
- const agentsMd = join(dir, "AGENTS.md");
1493
- symlinkSync("../outside-target.txt", agentsMd);
1494
-
1495
- const handle = await writeCodexAgentsMd(dir, "swarm prompt", { stateDir });
1496
- expect(lstatSync(agentsMd).isSymbolicLink()).toBe(false);
1497
- const during = readFileSync(agentsMd, "utf8");
1498
- expect(during).toContain("swarm prompt");
1499
- expect(during).not.toContain("OUT-OF-TREE");
1500
-
1501
- await Bun.write(agentsMd, `${during}- agent edit\n`);
1502
- await handle.cleanup();
1503
-
1504
- expect(readlinkSync(agentsMd)).toBe("../outside-target.txt");
1505
- expect(readFileSync(secretPath, "utf8")).toBe(secret);
1506
- });
1370
+ for (const kind of ["file", "missing", "symlink"]) {
1371
+ // Windows requires symlink privileges; CI exercises this case on Linux.
1372
+ test.skipIf(kind === "symlink" && process.platform === "win32")(
1373
+ `passes native instructions without changing ${kind} AGENTS.md during the session`,
1374
+ async () => {
1375
+ const dir = mkdtempSync(join(tmpdir(), "codex-instructions-"));
1376
+ const cwd = join(dir, "repo");
1377
+ mkdirSync(cwd);
1378
+ const agentsMd = join(cwd, "AGENTS.md");
1379
+ const claudeMd = join(cwd, "CLAUDE.md");
1380
+ const original = "# Repo rules\r\n\r\nKeep this file unchanged.\r\n";
1381
+ const systemPrompt = 'Swarm instructions: "quoted"\n第二行';
1382
+ await Bun.write(claudeMd, original);
1383
+ if (kind === "file") await Bun.write(agentsMd, original);
1384
+ if (kind === "symlink") symlinkSync("CLAUDE.md", agentsMd);
1385
+
1386
+ let startParams: unknown;
1387
+ const started = Promise.withResolvers<void>();
1388
+ const fake = makeFakeAppServer((method, params) => {
1389
+ if (method === "thread/start") {
1390
+ startParams = params;
1391
+ return { thread: { id: "instructions-thread" } };
1392
+ }
1393
+ if (method === "turn/start") {
1394
+ started.resolve();
1395
+ return { turn: { id: "instructions-turn" } };
1396
+ }
1397
+ throw new Error(`Unexpected method: ${method}`);
1398
+ });
1399
+ const assertRepoUnchanged = async () => {
1400
+ expect(await Bun.file(claudeMd).text()).toBe(original);
1401
+ if (kind === "missing") {
1402
+ expect(await Bun.file(agentsMd).exists()).toBe(false);
1403
+ } else {
1404
+ expect(await Bun.file(agentsMd).text()).toBe(original);
1405
+ expect(lstatSync(agentsMd).isSymbolicLink()).toBe(kind === "symlink");
1406
+ if (kind === "symlink") expect(readlinkSync(agentsMd)).toBe("CLAUDE.md");
1407
+ }
1408
+ };
1409
+ const session = await new CodexAdapter({
1410
+ bypassSubprocess: true,
1411
+ appServerFactory: () => fake.client as never,
1412
+ }).createSession(
1413
+ testConfig({ cwd, systemPrompt, logFile: join(dir, "session.log"), taskId: "" }),
1414
+ );
1415
+ try {
1416
+ await started.promise;
1417
+ await assertRepoUnchanged();
1418
+ expect(startParams).toMatchObject({ developerInstructions: systemPrompt });
1419
+ } finally {
1420
+ fake.emit("turn/completed", {
1421
+ threadId: "instructions-thread",
1422
+ turn: { id: "instructions-turn", status: "completed", error: null },
1423
+ });
1424
+ await session.waitForCompletion();
1425
+ try {
1426
+ await assertRepoUnchanged();
1427
+ } finally {
1428
+ rmSync(dir, { recursive: true, force: true });
1429
+ }
1430
+ }
1431
+ },
1432
+ );
1433
+ }
1507
1434
  });
1508
1435
 
1509
1436
  // ─── Phase 3: model catalogue ────────────────────────────────────────────────
@@ -2359,9 +2286,7 @@ describe("CodexSession session-end summarization", () => {
2359
2286
  expect(indexCalls.length).toBe(0);
2360
2287
  });
2361
2288
 
2362
- test("runSummarize throws → existing logFileHandle.end() + agentsMdHandle.cleanup() still run", async () => {
2363
- // Use a real temp dir as cwd so we exercise writeCodexAgentsMd, then spy on
2364
- // the resulting AGENTS.md file. After cleanup the file MUST be gone.
2289
+ test("runSummarize throws → logFileHandle.end() still runs", async () => {
2365
2290
  const cwd = `/tmp/codex-summary-cleanup-${Date.now()}-${Math.random().toString(36).slice(2)}`;
2366
2291
  mkdirSync(cwd, { recursive: true });
2367
2292
  const agentsMdPath = join(cwd, "AGENTS.md");
@@ -2377,11 +2302,11 @@ describe("CodexSession session-end summarization", () => {
2377
2302
  const config = testConfig({
2378
2303
  logFile,
2379
2304
  cwd,
2380
- systemPrompt: "test system prompt", // ensures AGENTS.md gets written
2305
+ systemPrompt: "test system prompt",
2381
2306
  });
2382
2307
  await runSessionWithFakeThreadAndDeps(events, config, deps);
2383
2308
 
2384
- // AGENTS.md cleanup must have run despite the summarize throw.
2309
+ // Summarization failure must not create an instruction file.
2385
2310
  expect(await Bun.file(agentsMdPath).exists()).toBe(false);
2386
2311
 
2387
2312
  // Log file flush must have run despite the summarize throw — the log file
@@ -10,7 +10,8 @@ import { afterAll, afterEach, beforeAll, describe, expect, it } from "bun:test";
10
10
  import { unlink } from "node:fs/promises";
11
11
  import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http";
12
12
  import { closeDb, initDb, upsertKv } from "../be/db";
13
- import { handleCodexOAuthKeepWarm } from "../http/codex-oauth-keep-warm";
13
+ import { validateConfigValue } from "../be/swarm-config-guard";
14
+ import { handleCodexOAuthKeepWarm, keepWarmMaxAgeMs } from "../http/codex-oauth-keep-warm";
14
15
  import { handleOAuthLocks } from "../http/oauth-locks";
15
16
  import { resetFetchForTesting, setFetchForTesting } from "../providers/codex-oauth/flow.js";
16
17
  import type { CodexOAuthCredentials } from "../providers/codex-oauth/types.js";
@@ -176,6 +177,70 @@ describe("POST /api/oauth/keep-warm/codex", () => {
176
177
  expect(slots.get(1)?.access).toBe("example-at_keepwarm");
177
178
  });
178
179
 
180
+ it("refreshes a 6-day-old slot by default, before the ~6d20h point where OpenAI ended a session", async () => {
181
+ const slots = new Map<number, CodexOAuthCredentials>([
182
+ [
183
+ 0,
184
+ {
185
+ access: "at_six_days",
186
+ refresh: "rt_six_days",
187
+ expires: Date.now() + 4 * 24 * 60 * 60 * 1000, // issued ~6 days ago (10d TTL)
188
+ accountId: "acc-six",
189
+ },
190
+ ],
191
+ ]);
192
+ installMockTransport(slots);
193
+ setFetchForTesting(
194
+ async () =>
195
+ new Response(
196
+ JSON.stringify({
197
+ access_token: "example-at_six_days_new",
198
+ refresh_token: "example-rt_six_days_new",
199
+ expires_in: 864000,
200
+ }),
201
+ { status: 200, headers: { "Content-Type": "application/json" } },
202
+ ),
203
+ );
204
+
205
+ const { req, res, captured } = fakeReqRes();
206
+ await handleCodexOAuthKeepWarm(req, res, ["api", "oauth", "keep-warm", "codex"]);
207
+
208
+ const body = JSON.parse(captured.body) as { results: Array<Record<string, unknown>> };
209
+ expect(body.results[0]).toMatchObject({ slot: 0, outcome: "refreshed" });
210
+ expect(slots.get(0)?.refresh).toBe("example-rt_six_days_new");
211
+ });
212
+
213
+ it("honors CODEX_OAUTH_KEEP_WARM_MAX_AGE_HOURS read at sweep time", async () => {
214
+ const saved = process.env.CODEX_OAUTH_KEEP_WARM_MAX_AGE_HOURS;
215
+ process.env.CODEX_OAUTH_KEEP_WARM_MAX_AGE_HOURS = "168";
216
+ try {
217
+ const slots = new Map<number, CodexOAuthCredentials>([
218
+ [
219
+ 0,
220
+ {
221
+ access: "at_six_days",
222
+ refresh: "rt_six_days",
223
+ expires: Date.now() + 4 * 24 * 60 * 60 * 1000, // issued ~6 days ago, under 168h
224
+ accountId: "acc-six",
225
+ },
226
+ ],
227
+ ]);
228
+ installMockTransport(slots);
229
+ setFetchForTesting(async () => {
230
+ throw new Error("no /oauth/token exchange should happen under a 168h threshold");
231
+ });
232
+
233
+ const { req, res, captured } = fakeReqRes();
234
+ await handleCodexOAuthKeepWarm(req, res, ["api", "oauth", "keep-warm", "codex"]);
235
+
236
+ const body = JSON.parse(captured.body) as { results: Array<Record<string, unknown>> };
237
+ expect(body.results[0]).toMatchObject({ slot: 0, outcome: "warm" });
238
+ } finally {
239
+ if (saved === undefined) delete process.env.CODEX_OAUTH_KEEP_WARM_MAX_AGE_HOURS;
240
+ else process.env.CODEX_OAUTH_KEEP_WARM_MAX_AGE_HOURS = saved;
241
+ }
242
+ });
243
+
179
244
  it("skips a slot with an active codex-auth-watch bench marker", async () => {
180
245
  const keySuffix = "c-old"; // last 5 of accountId (no chatgpt_user_id in this fixture's access token)
181
246
  await upsertKv({
@@ -284,6 +349,25 @@ describe("POST /api/oauth/keep-warm/codex", () => {
284
349
  }
285
350
  });
286
351
 
352
+ it("keepWarmMaxAgeMs defaults to 120h and ignores invalid or out-of-range values", () => {
353
+ const hour = 60 * 60 * 1000;
354
+ expect(keepWarmMaxAgeMs({})).toBe(120 * hour);
355
+ expect(keepWarmMaxAgeMs({ CODEX_OAUTH_KEEP_WARM_MAX_AGE_HOURS: " 72 " })).toBe(72 * hour);
356
+ expect(keepWarmMaxAgeMs({ CODEX_OAUTH_KEEP_WARM_MAX_AGE_HOURS: "216" })).toBe(216 * hour);
357
+ for (const bad of ["0", "217", "1.5", "-3", "abc", ""]) {
358
+ expect(keepWarmMaxAgeMs({ CODEX_OAUTH_KEEP_WARM_MAX_AGE_HOURS: bad })).toBe(120 * hour);
359
+ }
360
+ });
361
+
362
+ it("the config API rejects the values keepWarmMaxAgeMs would fall back from", () => {
363
+ const key = "CODEX_OAUTH_KEEP_WARM_MAX_AGE_HOURS";
364
+ expect(validateConfigValue(key, "1")).toBeNull();
365
+ expect(validateConfigValue(key, "216")).toBeNull();
366
+ expect(validateConfigValue(key, "0")).toContain("between 1 and 216");
367
+ expect(validateConfigValue(key, "217")).toContain("between 1 and 216");
368
+ expect(validateConfigValue(key, "1.5")).toContain("integer");
369
+ });
370
+
287
371
  it("does not match unrelated paths", async () => {
288
372
  const { req, res } = fakeReqRes();
289
373
  const handled = await handleCodexOAuthKeepWarm(req, res, [
@@ -56,6 +56,12 @@ beforeAll(async () => {
56
56
  value: "synthetic-api-only-signing-secret",
57
57
  isSecret: true,
58
58
  });
59
+ await upsertSwarmConfig({
60
+ scope: "global",
61
+ key: "TRANSCRIPTION_API_KEY",
62
+ value: "synthetic-api-only-transcription-key",
63
+ isSecret: true,
64
+ });
59
65
  await upsertSwarmConfig({
60
66
  scope: "global",
61
67
  key: "SOME_WORKER_SECRET",
@@ -93,6 +99,7 @@ describe("API-only config keys are never served over HTTP", () => {
93
99
  const { configs } = await getJson("/api/config/resolved?includeSecrets=true");
94
100
  expect(configs.some((c) => c.key === BOOTSTRAP_KEY)).toBe(false);
95
101
  expect(configs.some((c) => c.key === "SLACK_SIGNING_SECRET")).toBe(false);
102
+ expect(configs.some((c) => c.key === "TRANSCRIPTION_API_KEY")).toBe(false);
96
103
  // A legitimate worker secret still comes through.
97
104
  expect(configs.some((c) => c.key === "SOME_WORKER_SECRET")).toBe(true);
98
105
  });
@@ -103,12 +110,14 @@ describe("API-only config keys are never served over HTTP", () => {
103
110
  );
104
111
  expect(configs.some((c) => c.key === BOOTSTRAP_KEY)).toBe(false);
105
112
  expect(configs.some((c) => c.key === "SLACK_SIGNING_SECRET")).toBe(false);
113
+ expect(configs.some((c) => c.key === "TRANSCRIPTION_API_KEY")).toBe(false);
106
114
  });
107
115
 
108
116
  test("GET /api/config?scope=global strips the bootstrap key", async () => {
109
117
  const { configs } = await getJson("/api/config?scope=global&includeSecrets=true");
110
118
  expect(configs.some((c) => c.key === BOOTSTRAP_KEY)).toBe(false);
111
119
  expect(configs.some((c) => c.key === "SLACK_SIGNING_SECRET")).toBe(false);
120
+ expect(configs.some((c) => c.key === "TRANSCRIPTION_API_KEY")).toBe(false);
112
121
  expect(configs.some((c) => c.key === "SOME_WORKER_SECRET")).toBe(true);
113
122
  });
114
123
  });
@@ -1,6 +1,7 @@
1
1
  import { afterAll, beforeAll, describe, expect, test } from "bun:test";
2
2
  import { createServer, type Server } from "node:http";
3
3
  import { closeDb, createAgent, initDb, upsertSwarmConfig } from "../be/db";
4
+ import { mintAgentToken, revokeAgentTokens } from "../be/users";
4
5
  import { handleConfig } from "../http/config";
5
6
  import { handleCore } from "../http/core";
6
7
  import { handleSessions } from "../http/sessions";
@@ -116,6 +117,19 @@ describe("config HTTP session-token authorization", () => {
116
117
  expect((await response.json()).configs[0].value).toBe("global");
117
118
  });
118
119
 
120
+ test("agent token resolved config without agentId reads its own agent scope", async () => {
121
+ const token = await mintAgentToken(workerId);
122
+ try {
123
+ const response = await fetch(`${baseUrl}/api/config/resolved?includeSecrets=true`, {
124
+ headers: headers(token.plaintext),
125
+ });
126
+ expect(response.status).toBe(200);
127
+ expect(await response.text()).toContain(SECRET);
128
+ } finally {
129
+ await revokeAgentTokens(workerId);
130
+ }
131
+ });
132
+
119
133
  for (const method of ["PUT", "DELETE"] as const) {
120
134
  test(`${method} cannot escalate a non-lead session by changing only X-Agent-ID`, async () => {
121
135
  const token = await mint(workerId);
@@ -336,7 +336,7 @@ describe("page-session CORS over HTTP", () => {
336
336
  const cookie = response.headers.get("set-cookie")!.split(";")[0]!;
337
337
  for (const path of [`/p/${pageId}.json`, `/@swarm/api/pages/${pageId}`]) {
338
338
  const page = await fetch(`${base}${path}`, {
339
- headers: { Origin: origin, Cookie: cookie },
339
+ headers: { Origin: origin, Cookie: cookie, "X-Swarm-Page-Id": pageId },
340
340
  });
341
341
  expect(page.status).toBe(200);
342
342
  expect(page.headers.get("access-control-allow-origin")).toBe(