@desplega.ai/agent-swarm 1.165.0 → 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 (188) hide show
  1. package/dist/{acp-adapter-w6zws6me.js → acp-adapter-yzfxn8vj.js} +4 -4
  2. package/dist/{actions-jna0b3wm.js → actions-qz3m692w.js} +20 -24
  3. package/dist/{amp-adapter-0eve81pz.js → amp-adapter-j1xbmsqz.js} +4 -4
  4. package/dist/{app-75hm5cfa.js → app-43vrdmv8.js} +6 -7
  5. package/dist/{assistant-yzd51j33.js → assistant-ertffmka.js} +21 -25
  6. package/dist/{boot-reembed-z158xq32.js → boot-reembed-998jtpdg.js} +9 -12
  7. package/dist/{boot-reembed-2ex2mcsh.js → boot-reembed-s7k7c6v2.js} +10 -12
  8. package/dist/boot-scrub-sweep-nepsk4t1.js +211 -0
  9. package/dist/{boot-seeds-2bmnh18p.js → boot-seeds-vvzs1zhk.js} +8 -8
  10. package/dist/{claude-adapter-jbj1mycz.js → claude-adapter-2zvmekrp.js} +9 -11
  11. package/dist/{claude-managed-adapter-17ggz09t.js → claude-managed-adapter-s49wbk8t.js} +3 -3
  12. package/dist/{claude-managed-setup-ptm1nzm2.js → claude-managed-setup-cvhqq8qn.js} +4 -4
  13. package/dist/{claude-sdk-session-9re1g7r2.js → claude-sdk-session-kkbxtw5q.js} +2618 -2600
  14. package/dist/{cli-k6xx3pve.js → cli-0nhdwxd5.js} +4 -4
  15. package/dist/{cli-n9ehc28t.js → cli-0qjcsxgz.js} +4 -4
  16. package/dist/{cli-zn2v3t8v.js → cli-3ddybbbx.js} +1 -1
  17. package/dist/{cli-5anb0ev3.js → cli-3qn4r334.js} +71 -14
  18. package/dist/{cli-1kzfvd1z.js → cli-4r1sjfc4.js} +9 -5
  19. package/dist/{cli-ba66wyhy.js → cli-66zh26sp.js} +6 -6
  20. package/dist/{cli-9xb89xca.js → cli-68r1j4sg.js} +6 -6
  21. package/dist/{cli-xnzrzets.js → cli-8kgvmkak.js} +2 -2
  22. package/dist/{cli-28smfvbn.js → cli-8t229cmr.js} +5 -5
  23. package/dist/{cli-9866bd07.js → cli-aqcey741.js} +1 -1
  24. package/dist/{cli-90v942q1.js → cli-cpfmqz3n.js} +8 -8
  25. package/dist/{cli-50eg13rj.js → cli-df56tqdw.js} +459 -282
  26. package/dist/{cli-8gpbwy09.js → cli-dw2q9f7g.js} +5 -5
  27. package/dist/{cli-0n4fasz0.js → cli-ecknp7p1.js} +15 -15
  28. package/dist/{cli-xb3c6aat.js → cli-f2n17zhp.js} +1 -1
  29. package/dist/{cli-ftry7975.js → cli-feh3dkah.js} +1 -1
  30. package/dist/{cli-amwhs8h2.js → cli-fn697bnt.js} +2 -2
  31. package/dist/{cli-prt9emct.js → cli-gt8azyet.js} +3 -3
  32. package/dist/{cli-59jn5twa.js → cli-h3x613ze.js} +2 -2
  33. package/dist/{cli-mmzwwptd.js → cli-hf5s9pgf.js} +1 -1
  34. package/dist/cli-j2jbr8af.js +516 -0
  35. package/dist/{cli-wfajtmf7.js → cli-jhm6hzjn.js} +593 -10
  36. package/dist/{cli-xtgtyafv.js → cli-jvj6a0cv.js} +190 -439
  37. package/dist/{cli-hpryr6em.js → cli-mb75wpxf.js} +1 -1
  38. package/dist/{cli-420bdgw0.js → cli-mv2520nm.js} +1 -1
  39. package/dist/{cli-pjxnr1jn.js → cli-mwegs4af.js} +3 -3
  40. package/dist/{cli-5b2fjjzh.js → cli-p4ztb80h.js} +2 -2
  41. package/dist/{cli-e7t4snpa.js → cli-rggdq8wq.js} +4 -4
  42. package/dist/{cli-bdv39kgj.js → cli-rsy933yz.js} +5 -5
  43. package/dist/{cli-gh5yqzpe.js → cli-v2a8x45d.js} +2 -2
  44. package/dist/{cli-bns5c8k9.js → cli-vzc73wet.js} +43 -47
  45. package/dist/cli-wt7ccy4r.js +2654 -0
  46. package/dist/{cli-j0pdrmwg.js → cli-x7kpr72a.js} +3 -3
  47. package/dist/{cli-x7afk5wm.js → cli-xvc3q9br.js} +1 -1
  48. package/dist/{cli-9s3h836g.js → cli-xvdgasgy.js} +1 -1
  49. package/dist/{cli-zvckv750.js → cli-y59h2pk9.js} +2 -2
  50. package/dist/{cli-qdv8kgz7.js → cli-yshaazs1.js} +13884 -3241
  51. package/dist/{cli-nfygw434.js → cli-ysnyhws9.js} +2 -2
  52. package/dist/{cli-fw6vdcq5.js → cli-zp67qg5x.js} +34 -1
  53. package/dist/{cli-ma3zvh39.js → cli-zy7vwsne.js} +6066 -5829
  54. package/dist/cli.js +24 -20
  55. package/dist/{codex-adapter-3qnxshje.js → codex-adapter-dw1wxqt8.js} +7 -7
  56. package/dist/{codex-login-brkxhhdg.js → codex-login-terbdsqq.js} +2 -2
  57. package/dist/{codex-session-runner-612wnapp.js → codex-session-runner-3cgb8y1g.js} +7 -7
  58. package/dist/{commands-2ybfcxkj.js → commands-mhxam6z2.js} +6 -6
  59. package/dist/{cursor-adapter-p3jgdehv.js → cursor-adapter-2qda3st7.js} +3 -3
  60. package/dist/{db-f4ans0jj.js → db-sjks418k.js} +8 -6
  61. package/dist/{devin-adapter-dhzt01ff.js → devin-adapter-bffjj5n4.js} +2 -2
  62. package/dist/{dispatcher-pk182apd.js → dispatcher-1es759rn.js} +18 -22
  63. package/dist/{dsh-adapter-7twaeps1.js → dsh-adapter-bek4cyw9.js} +3 -3
  64. package/dist/{e2b-y8t3jzyz.js → e2b-nd50eccg.js} +1 -1
  65. package/dist/{event-bus-w7ac4n3c.js → event-bus-vnfthafg.js} +2 -2
  66. package/dist/{grok-adapter-yt9pab3k.js → grok-adapter-ar9cw9jg.js} +5 -5
  67. package/dist/{handlers-g2wnr4t9.js → handlers-1gkbhktr.js} +21 -25
  68. package/dist/{hook-f7jt9vnm.js → hook-ds2zxq9v.js} +14 -16
  69. package/dist/{hook-bbdpnkb4.js → hook-vxa3tcsy.js} +8 -10
  70. package/dist/{http-w40q4w94.js → http-3h21z9md.js} +247 -164
  71. package/dist/{index-056j5kzn.js → index-9b65j1m5.js} +8 -8
  72. package/dist/{index-9bfssf54.js → index-bn6cy8ps.js} +18 -22
  73. package/dist/{index-cj1r9q83.js → index-dkfxpkmm.js} +19 -23
  74. package/dist/{index-7n70jwbb.js → index-f8xnqrye.js} +19 -21
  75. package/dist/{index-fdk1t4d6.js → index-n0rxa4ca.js} +11962 -9900
  76. package/dist/{index-k8xc083e.js → index-n99fycq7.js} +18 -22
  77. package/dist/{keepalive-1tj10tf4.js → keepalive-3xspa9jw.js} +11 -8
  78. package/dist/{lead-st0bg7v4.js → lead-6qphc124.js} +34 -38
  79. package/dist/{maintenance-nz71zats.js → maintenance-0teem1bb.js} +7 -9
  80. package/dist/{node-f0ykebbv.js → node-9sg7ajby.js} +25 -7
  81. package/dist/{oauth-refresh-sweep-aefzwvvq.js → oauth-refresh-sweep-sh9q25xp.js} +10 -7
  82. package/dist/{onboard-kgpr9mbn.js → onboard-54qmnqxe.js} +2 -2
  83. package/dist/{opencode-adapter-0c8e70dk.js → opencode-adapter-wjfa2wvd.js} +2 -2
  84. package/dist/{otel-impl-x8zp4y5a.js → otel-impl-ycqzp2a1.js} +2 -2
  85. package/dist/{pi-mono-adapter-t280ah5c.js → pi-mono-adapter-e77e9zst.js} +4 -4
  86. package/dist/{pricing-refresh-9e0q7cwk.js → pricing-refresh-atnkngqs.js} +7 -7
  87. package/dist/{rbac-roles-kcp6zge2.js → rbac-roles-9pjphh9q.js} +6 -6
  88. package/dist/{rbac-roles-790j3ga8.js → rbac-roles-j78wdksm.js} +6 -6
  89. package/dist/{render-v2-tch4nefj.js → render-v2-pxjs49fx.js} +6 -7
  90. package/dist/{setup-rcm303ns.js → setup-ymej0xtx.js} +2 -2
  91. package/dist/{work-objects-ctg7t70w.js → work-objects-3nh4ymzb.js} +22 -26
  92. package/dist/{worker-5dp0xm55.js → worker-qa9fkz5s.js} +34 -38
  93. package/dist/{x-v3yw66zs.js → x-ma4gnfgy.js} +2 -2
  94. package/openapi.json +5 -2
  95. package/package.json +9 -5
  96. package/src/be/automation-preflight.ts +9 -4
  97. package/src/be/boot-scrub-sweep.ts +302 -0
  98. package/src/be/crypto/key-bootstrap.ts +1 -1
  99. package/src/be/db/approvals.ts +15 -1
  100. package/src/be/db/codex-oauth-device-flows.ts +40 -0
  101. package/src/be/db/config-secrets.ts +31 -0
  102. package/src/be/db/runtime.ts +4 -0
  103. package/src/be/db/script-runs.ts +15 -0
  104. package/src/be/db/tasks/write.ts +1 -1
  105. package/src/be/db-queries/mcp-oauth.ts +5 -0
  106. package/src/be/db-queries/oauth.ts +77 -0
  107. package/src/be/db.ts +162 -59
  108. package/src/be/events.ts +17 -1
  109. package/src/be/memory/index-content.ts +2 -2
  110. package/src/be/memory/providers/sqlite-store.ts +63 -11
  111. package/src/be/memory/types.ts +14 -0
  112. package/src/be/migrations/201_workflow_replay_columns.sql +10 -0
  113. package/src/be/migrations/202_codex_oauth_device_flows.sql +24 -0
  114. package/src/be/scripts/db.ts +28 -4
  115. package/src/be/scripts/source-secrets.ts +37 -0
  116. package/src/be/scrub-json.ts +32 -0
  117. package/src/be/sealed-json.ts +59 -0
  118. package/src/be/secret-registry.ts +131 -0
  119. package/src/be/workflow-replay.ts +77 -0
  120. package/src/http/codex-oauth-device.ts +8 -10
  121. package/src/http/db-query-bounded.ts +3 -0
  122. package/src/http/db-query-shared.ts +107 -15
  123. package/src/http/db-query.ts +10 -2
  124. package/src/http/index.ts +23 -6
  125. package/src/http/script-runs.ts +9 -1
  126. package/src/http/scripts.ts +15 -0
  127. package/src/http/workflows.ts +3 -2
  128. package/src/oauth/mcp-wrapper.ts +1 -1
  129. package/src/providers/acp-adapter.ts +25 -11
  130. package/src/providers/acp-target-catalog.ts +35 -1
  131. package/src/providers/acp-targets.ts +69 -3
  132. package/src/providers/amp-adapter.ts +1 -1
  133. package/src/providers/cursor-adapter.ts +1 -1
  134. package/src/script-workflows/supervisor.ts +11 -2
  135. package/src/scripts-runtime/eval-harness.ts +2 -12
  136. package/src/scripts-runtime/executors/quickjs-bundle.ts +152 -0
  137. package/src/scripts-runtime/executors/quickjs-prelude.ts +285 -0
  138. package/src/scripts-runtime/executors/quickjs-runner.ts +580 -0
  139. package/src/scripts-runtime/executors/quickjs-worker.ts +81 -0
  140. package/src/scripts-runtime/executors/quickjs.ts +196 -0
  141. package/src/scripts-runtime/executors/registry.ts +2 -0
  142. package/src/scripts-runtime/executors/sourcemap.ts +79 -0
  143. package/src/scripts-runtime/script-error-hints.ts +14 -0
  144. package/src/server.ts +3 -1
  145. package/src/tests/acp-adapter.test.ts +127 -2
  146. package/src/tests/boot-scrub-sweep.test.ts +390 -0
  147. package/src/tests/check-text-columns.test.ts +145 -0
  148. package/src/tests/claude-sdk-process.fixture.ts +13 -2
  149. package/src/tests/codex-oauth-device.test.ts +23 -37
  150. package/src/tests/db-query-denylist.test.ts +202 -0
  151. package/src/tests/db-task-read-contract.test.ts +1 -0
  152. package/src/tests/db-task-write-contract.test.ts +1 -0
  153. package/src/tests/entrypoint-acp-target-binary.test.ts +31 -1
  154. package/src/tests/events-scrub.test.ts +88 -0
  155. package/src/tests/fixtures/secret-corpus.ts +320 -0
  156. package/src/tests/script-connections.test.ts +5 -0
  157. package/src/tests/script-executor-conformance.test.ts +3 -0
  158. package/src/tests/script-executor-quickjs.test.ts +442 -0
  159. package/src/tests/script-executor-registry.test.ts +6 -1
  160. package/src/tests/script-runs-http.test.ts +88 -1
  161. package/src/tests/script-runs-scrub.test.ts +275 -0
  162. package/src/tests/scripts-http.test.ts +82 -1
  163. package/src/tests/scrubbed-text-12e.test.ts +289 -0
  164. package/src/tests/sealed-key-guard.test.ts +166 -0
  165. package/src/tests/secret-canary-sweep.test.ts +958 -0
  166. package/src/tests/secret-registry.test.ts +355 -0
  167. package/src/tests/secret-rules.test.ts +230 -0
  168. package/src/tests/secret-scrubber.test.ts +3 -2
  169. package/src/tests/workflow-replay.test.ts +518 -0
  170. package/src/tests/workflow-scrub.test.ts +242 -0
  171. package/src/tools/db-query.ts +1 -1
  172. package/src/tools/inject-learning.ts +1 -1
  173. package/src/tools/workflows/get-workflow-run.ts +3 -2
  174. package/src/types.ts +1 -1
  175. package/src/utils/secret-rules.generated.ts +2055 -0
  176. package/src/utils/secret-scrubber.ts +363 -29
  177. package/src/workflows/engine.ts +39 -4
  178. package/src/workflows/recovery.ts +10 -1
  179. package/src/workflows/resume.ts +62 -3
  180. package/src/workflows/retry-poller.ts +3 -0
  181. package/dist/boot-scrub-logs-246yn076.js +0 -99
  182. package/dist/cli-3a4m1aws.js +0 -572
  183. package/dist/cli-a87b04k8.js +0 -951
  184. package/dist/cli-cvk6yzw5.js +0 -40
  185. package/dist/cli-h1spv8v4.js +0 -427
  186. package/dist/cli-jqt9hrnz.js +0 -168
  187. package/dist/cli-yhmwmv0g.js +0 -9281
  188. package/src/be/boot-scrub-logs.ts +0 -132
@@ -1,15 +1,8 @@
1
1
  import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test } from "bun:test";
2
2
  import { createServer, type Server } from "node:http";
3
3
  import { decryptSecret, encryptSecret, getEncryptionKey } from "../be/crypto";
4
- import {
5
- closeDb,
6
- getDbClient,
7
- getKv,
8
- getSwarmConfigs,
9
- initDb,
10
- upsertKv,
11
- upsertSwarmConfig,
12
- } from "../be/db";
4
+ import { closeDb, getDbClient, getSwarmConfigs, initDb, upsertSwarmConfig } from "../be/db";
5
+ import { getCodexDeviceFlow, upsertCodexDeviceFlow } from "../be/db/codex-oauth-device-flows";
13
6
  import type { OnboardingState } from "../be/onboarding";
14
7
  import { handleCodexOAuthDevice } from "../http/codex-oauth-device";
15
8
  import { handleCore } from "../http/core";
@@ -97,7 +90,7 @@ afterAll(async () => {
97
90
  });
98
91
 
99
92
  beforeEach(async () => {
100
- await getDbClient().run("DELETE FROM kv_entries");
93
+ await getDbClient().run("DELETE FROM codex_oauth_device_flows");
101
94
  await getDbClient().run("DELETE FROM swarm_config");
102
95
  fetchImpl = async () => {
103
96
  throw new Error("Unexpected fetch");
@@ -221,8 +214,8 @@ describe("Codex device HTTP routes", () => {
221
214
 
222
215
  const started = await startFlow();
223
216
  expect(Number.isNaN(Date.parse(started.expiresAt))).toBe(false);
224
- const encrypted = await getKv("codex-oauth-device", started.flowId);
225
- expect(encrypted?.value).not.toContain("device-auth-full");
217
+ const encrypted = await getCodexDeviceFlow(started.flowId);
218
+ expect(encrypted?.state).not.toContain("device-auth-full");
226
219
 
227
220
  const poll = await request(`/api/codex-oauth/device/${started.flowId}/poll`);
228
221
  expect(await poll.json()).toEqual({ status: "complete", slot: 1 });
@@ -318,17 +311,15 @@ describe("Codex device HTTP routes", () => {
318
311
  const secondResponse = await request(path);
319
312
  const secondBody = await secondResponse.json();
320
313
  const callsBeforeRelease = { pollCalls, exchangeCalls };
321
- const entry = await getKv("codex-oauth-device", started.flowId);
322
- const state = JSON.parse(decryptSecret(String(entry!.value), getEncryptionKey())) as Record<
314
+ const entry = await getCodexDeviceFlow(started.flowId);
315
+ const state = JSON.parse(decryptSecret(entry!.state, getEncryptionKey())) as Record<
323
316
  string,
324
317
  unknown
325
318
  >;
326
319
  state.pollLeaseExpiresAt = Date.now() - 1;
327
- await upsertKv({
328
- namespace: "codex-oauth-device",
329
- key: started.flowId,
330
- value: encryptSecret(JSON.stringify(state), getEncryptionKey()),
331
- valueType: "string",
320
+ await upsertCodexDeviceFlow({
321
+ flowId: started.flowId,
322
+ state: encryptSecret(JSON.stringify(state), getEncryptionKey()),
332
323
  expiresAt: Number(state.expiresAt),
333
324
  });
334
325
  releaseExchange();
@@ -359,19 +350,17 @@ describe("Codex device HTTP routes", () => {
359
350
  }) as typeof fetch;
360
351
 
361
352
  const started = await startFlow();
362
- const entry = await getKv("codex-oauth-device", started.flowId);
363
- const state = JSON.parse(decryptSecret(String(entry!.value), getEncryptionKey())) as Record<
353
+ const entry = await getCodexDeviceFlow(started.flowId);
354
+ const state = JSON.parse(decryptSecret(entry!.state, getEncryptionKey())) as Record<
364
355
  string,
365
356
  unknown
366
357
  >;
367
358
  state.lastPolledAt = Date.now() - 10_000;
368
359
  state.pollLeaseId = crypto.randomUUID();
369
360
  state.pollLeaseExpiresAt = Date.now() - 1;
370
- await upsertKv({
371
- namespace: "codex-oauth-device",
372
- key: started.flowId,
373
- value: encryptSecret(JSON.stringify(state), getEncryptionKey()),
374
- valueType: "string",
361
+ await upsertCodexDeviceFlow({
362
+ flowId: started.flowId,
363
+ state: encryptSecret(JSON.stringify(state), getEncryptionKey()),
375
364
  expiresAt: Number(state.expiresAt),
376
365
  });
377
366
 
@@ -380,9 +369,9 @@ describe("Codex device HTTP routes", () => {
380
369
  });
381
370
  expect(pollCalls).toBe(1);
382
371
 
383
- const recovered = await getKv("codex-oauth-device", started.flowId);
372
+ const recovered = await getCodexDeviceFlow(started.flowId);
384
373
  const recoveredState = JSON.parse(
385
- decryptSecret(String(recovered!.value), getEncryptionKey()),
374
+ decryptSecret(recovered!.state, getEncryptionKey()),
386
375
  ) as Record<string, unknown>;
387
376
  expect(recoveredState.pollLeaseId).toBeNull();
388
377
  expect(recoveredState.pollLeaseExpiresAt).toBeNull();
@@ -403,17 +392,15 @@ describe("Codex device HTTP routes", () => {
403
392
  }) as typeof fetch;
404
393
 
405
394
  const started = await startFlow();
406
- const entry = await getKv("codex-oauth-device", started.flowId);
407
- const state = JSON.parse(decryptSecret(String(entry!.value), getEncryptionKey())) as Record<
395
+ const entry = await getCodexDeviceFlow(started.flowId);
396
+ const state = JSON.parse(decryptSecret(entry!.state, getEncryptionKey())) as Record<
408
397
  string,
409
398
  unknown
410
399
  >;
411
400
  state.expiresAt = Date.now() - 1;
412
- await upsertKv({
413
- namespace: "codex-oauth-device",
414
- key: started.flowId,
415
- value: encryptSecret(JSON.stringify(state), getEncryptionKey()),
416
- valueType: "string",
401
+ await upsertCodexDeviceFlow({
402
+ flowId: started.flowId,
403
+ state: encryptSecret(JSON.stringify(state), getEncryptionKey()),
417
404
  expiresAt: Number(state.expiresAt),
418
405
  });
419
406
 
@@ -482,8 +469,7 @@ describe("Codex device HTTP routes", () => {
482
469
  }) as typeof fetch;
483
470
  await getDbClient().run(
484
471
  `CREATE TRIGGER fail_device_flow_write
485
- BEFORE INSERT ON kv_entries
486
- WHEN NEW.namespace = 'codex-oauth-device'
472
+ BEFORE INSERT ON codex_oauth_device_flows
487
473
  BEGIN
488
474
  SELECT RAISE(ABORT, 'forced local write failure');
489
475
  END`,
@@ -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
@@ -44,6 +44,7 @@ const STUB_BINARIES = [
44
44
  "codex",
45
45
  "opencode",
46
46
  "gemini",
47
+ "copilot",
47
48
  "dsh",
48
49
  "acp-target-stub",
49
50
  "acp target stub",
@@ -142,6 +143,35 @@ describe("docker-entrypoint.sh: acp target binary verification", () => {
142
143
  CHILD_PROCESS_TEST_BUDGET_MS,
143
144
  );
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
+
145
175
  test(
146
176
  "resolves a custom target supplied as a command string with argv",
147
177
  async () => {
@@ -191,7 +221,7 @@ describe("docker-entrypoint.sh: acp target binary verification", () => {
191
221
 
192
222
  expect(result.exitCode).not.toBe(0);
193
223
  expect(result.stdout).toContain(
194
- "FATAL: unsupported ACP target 'gemini-cli'. Supported targets: opencode, gemini, custom.",
224
+ "FATAL: unsupported ACP target 'gemini-cli'. Supported targets: opencode, gemini, copilot, custom.",
195
225
  );
196
226
  },
197
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
+ });