@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.
- package/dist/{acp-adapter-w6zws6me.js → acp-adapter-yzfxn8vj.js} +4 -4
- package/dist/{actions-jna0b3wm.js → actions-qz3m692w.js} +20 -24
- package/dist/{amp-adapter-0eve81pz.js → amp-adapter-j1xbmsqz.js} +4 -4
- package/dist/{app-75hm5cfa.js → app-43vrdmv8.js} +6 -7
- package/dist/{assistant-yzd51j33.js → assistant-ertffmka.js} +21 -25
- package/dist/{boot-reembed-z158xq32.js → boot-reembed-998jtpdg.js} +9 -12
- package/dist/{boot-reembed-2ex2mcsh.js → boot-reembed-s7k7c6v2.js} +10 -12
- package/dist/boot-scrub-sweep-nepsk4t1.js +211 -0
- package/dist/{boot-seeds-2bmnh18p.js → boot-seeds-vvzs1zhk.js} +8 -8
- package/dist/{claude-adapter-jbj1mycz.js → claude-adapter-2zvmekrp.js} +9 -11
- package/dist/{claude-managed-adapter-17ggz09t.js → claude-managed-adapter-s49wbk8t.js} +3 -3
- package/dist/{claude-managed-setup-ptm1nzm2.js → claude-managed-setup-cvhqq8qn.js} +4 -4
- package/dist/{claude-sdk-session-9re1g7r2.js → claude-sdk-session-kkbxtw5q.js} +2618 -2600
- package/dist/{cli-k6xx3pve.js → cli-0nhdwxd5.js} +4 -4
- package/dist/{cli-n9ehc28t.js → cli-0qjcsxgz.js} +4 -4
- package/dist/{cli-zn2v3t8v.js → cli-3ddybbbx.js} +1 -1
- package/dist/{cli-5anb0ev3.js → cli-3qn4r334.js} +71 -14
- package/dist/{cli-1kzfvd1z.js → cli-4r1sjfc4.js} +9 -5
- package/dist/{cli-ba66wyhy.js → cli-66zh26sp.js} +6 -6
- package/dist/{cli-9xb89xca.js → cli-68r1j4sg.js} +6 -6
- package/dist/{cli-xnzrzets.js → cli-8kgvmkak.js} +2 -2
- package/dist/{cli-28smfvbn.js → cli-8t229cmr.js} +5 -5
- package/dist/{cli-9866bd07.js → cli-aqcey741.js} +1 -1
- package/dist/{cli-90v942q1.js → cli-cpfmqz3n.js} +8 -8
- package/dist/{cli-50eg13rj.js → cli-df56tqdw.js} +459 -282
- package/dist/{cli-8gpbwy09.js → cli-dw2q9f7g.js} +5 -5
- package/dist/{cli-0n4fasz0.js → cli-ecknp7p1.js} +15 -15
- package/dist/{cli-xb3c6aat.js → cli-f2n17zhp.js} +1 -1
- package/dist/{cli-ftry7975.js → cli-feh3dkah.js} +1 -1
- package/dist/{cli-amwhs8h2.js → cli-fn697bnt.js} +2 -2
- package/dist/{cli-prt9emct.js → cli-gt8azyet.js} +3 -3
- package/dist/{cli-59jn5twa.js → cli-h3x613ze.js} +2 -2
- package/dist/{cli-mmzwwptd.js → cli-hf5s9pgf.js} +1 -1
- package/dist/cli-j2jbr8af.js +516 -0
- package/dist/{cli-wfajtmf7.js → cli-jhm6hzjn.js} +593 -10
- package/dist/{cli-xtgtyafv.js → cli-jvj6a0cv.js} +190 -439
- package/dist/{cli-hpryr6em.js → cli-mb75wpxf.js} +1 -1
- package/dist/{cli-420bdgw0.js → cli-mv2520nm.js} +1 -1
- package/dist/{cli-pjxnr1jn.js → cli-mwegs4af.js} +3 -3
- package/dist/{cli-5b2fjjzh.js → cli-p4ztb80h.js} +2 -2
- package/dist/{cli-e7t4snpa.js → cli-rggdq8wq.js} +4 -4
- package/dist/{cli-bdv39kgj.js → cli-rsy933yz.js} +5 -5
- package/dist/{cli-gh5yqzpe.js → cli-v2a8x45d.js} +2 -2
- package/dist/{cli-bns5c8k9.js → cli-vzc73wet.js} +43 -47
- package/dist/cli-wt7ccy4r.js +2654 -0
- package/dist/{cli-j0pdrmwg.js → cli-x7kpr72a.js} +3 -3
- package/dist/{cli-x7afk5wm.js → cli-xvc3q9br.js} +1 -1
- package/dist/{cli-9s3h836g.js → cli-xvdgasgy.js} +1 -1
- package/dist/{cli-zvckv750.js → cli-y59h2pk9.js} +2 -2
- package/dist/{cli-qdv8kgz7.js → cli-yshaazs1.js} +13884 -3241
- package/dist/{cli-nfygw434.js → cli-ysnyhws9.js} +2 -2
- package/dist/{cli-fw6vdcq5.js → cli-zp67qg5x.js} +34 -1
- package/dist/{cli-ma3zvh39.js → cli-zy7vwsne.js} +6066 -5829
- package/dist/cli.js +24 -20
- package/dist/{codex-adapter-3qnxshje.js → codex-adapter-dw1wxqt8.js} +7 -7
- package/dist/{codex-login-brkxhhdg.js → codex-login-terbdsqq.js} +2 -2
- package/dist/{codex-session-runner-612wnapp.js → codex-session-runner-3cgb8y1g.js} +7 -7
- package/dist/{commands-2ybfcxkj.js → commands-mhxam6z2.js} +6 -6
- package/dist/{cursor-adapter-p3jgdehv.js → cursor-adapter-2qda3st7.js} +3 -3
- package/dist/{db-f4ans0jj.js → db-sjks418k.js} +8 -6
- package/dist/{devin-adapter-dhzt01ff.js → devin-adapter-bffjj5n4.js} +2 -2
- package/dist/{dispatcher-pk182apd.js → dispatcher-1es759rn.js} +18 -22
- package/dist/{dsh-adapter-7twaeps1.js → dsh-adapter-bek4cyw9.js} +3 -3
- package/dist/{e2b-y8t3jzyz.js → e2b-nd50eccg.js} +1 -1
- package/dist/{event-bus-w7ac4n3c.js → event-bus-vnfthafg.js} +2 -2
- package/dist/{grok-adapter-yt9pab3k.js → grok-adapter-ar9cw9jg.js} +5 -5
- package/dist/{handlers-g2wnr4t9.js → handlers-1gkbhktr.js} +21 -25
- package/dist/{hook-f7jt9vnm.js → hook-ds2zxq9v.js} +14 -16
- package/dist/{hook-bbdpnkb4.js → hook-vxa3tcsy.js} +8 -10
- package/dist/{http-w40q4w94.js → http-3h21z9md.js} +247 -164
- package/dist/{index-056j5kzn.js → index-9b65j1m5.js} +8 -8
- package/dist/{index-9bfssf54.js → index-bn6cy8ps.js} +18 -22
- package/dist/{index-cj1r9q83.js → index-dkfxpkmm.js} +19 -23
- package/dist/{index-7n70jwbb.js → index-f8xnqrye.js} +19 -21
- package/dist/{index-fdk1t4d6.js → index-n0rxa4ca.js} +11962 -9900
- package/dist/{index-k8xc083e.js → index-n99fycq7.js} +18 -22
- package/dist/{keepalive-1tj10tf4.js → keepalive-3xspa9jw.js} +11 -8
- package/dist/{lead-st0bg7v4.js → lead-6qphc124.js} +34 -38
- package/dist/{maintenance-nz71zats.js → maintenance-0teem1bb.js} +7 -9
- package/dist/{node-f0ykebbv.js → node-9sg7ajby.js} +25 -7
- package/dist/{oauth-refresh-sweep-aefzwvvq.js → oauth-refresh-sweep-sh9q25xp.js} +10 -7
- package/dist/{onboard-kgpr9mbn.js → onboard-54qmnqxe.js} +2 -2
- package/dist/{opencode-adapter-0c8e70dk.js → opencode-adapter-wjfa2wvd.js} +2 -2
- package/dist/{otel-impl-x8zp4y5a.js → otel-impl-ycqzp2a1.js} +2 -2
- package/dist/{pi-mono-adapter-t280ah5c.js → pi-mono-adapter-e77e9zst.js} +4 -4
- package/dist/{pricing-refresh-9e0q7cwk.js → pricing-refresh-atnkngqs.js} +7 -7
- package/dist/{rbac-roles-kcp6zge2.js → rbac-roles-9pjphh9q.js} +6 -6
- package/dist/{rbac-roles-790j3ga8.js → rbac-roles-j78wdksm.js} +6 -6
- package/dist/{render-v2-tch4nefj.js → render-v2-pxjs49fx.js} +6 -7
- package/dist/{setup-rcm303ns.js → setup-ymej0xtx.js} +2 -2
- package/dist/{work-objects-ctg7t70w.js → work-objects-3nh4ymzb.js} +22 -26
- package/dist/{worker-5dp0xm55.js → worker-qa9fkz5s.js} +34 -38
- package/dist/{x-v3yw66zs.js → x-ma4gnfgy.js} +2 -2
- package/openapi.json +5 -2
- package/package.json +9 -5
- package/src/be/automation-preflight.ts +9 -4
- package/src/be/boot-scrub-sweep.ts +302 -0
- package/src/be/crypto/key-bootstrap.ts +1 -1
- package/src/be/db/approvals.ts +15 -1
- package/src/be/db/codex-oauth-device-flows.ts +40 -0
- package/src/be/db/config-secrets.ts +31 -0
- package/src/be/db/runtime.ts +4 -0
- package/src/be/db/script-runs.ts +15 -0
- package/src/be/db/tasks/write.ts +1 -1
- package/src/be/db-queries/mcp-oauth.ts +5 -0
- package/src/be/db-queries/oauth.ts +77 -0
- package/src/be/db.ts +162 -59
- package/src/be/events.ts +17 -1
- package/src/be/memory/index-content.ts +2 -2
- package/src/be/memory/providers/sqlite-store.ts +63 -11
- package/src/be/memory/types.ts +14 -0
- package/src/be/migrations/201_workflow_replay_columns.sql +10 -0
- package/src/be/migrations/202_codex_oauth_device_flows.sql +24 -0
- package/src/be/scripts/db.ts +28 -4
- package/src/be/scripts/source-secrets.ts +37 -0
- package/src/be/scrub-json.ts +32 -0
- package/src/be/sealed-json.ts +59 -0
- package/src/be/secret-registry.ts +131 -0
- package/src/be/workflow-replay.ts +77 -0
- package/src/http/codex-oauth-device.ts +8 -10
- package/src/http/db-query-bounded.ts +3 -0
- package/src/http/db-query-shared.ts +107 -15
- package/src/http/db-query.ts +10 -2
- package/src/http/index.ts +23 -6
- package/src/http/script-runs.ts +9 -1
- package/src/http/scripts.ts +15 -0
- package/src/http/workflows.ts +3 -2
- package/src/oauth/mcp-wrapper.ts +1 -1
- package/src/providers/acp-adapter.ts +25 -11
- package/src/providers/acp-target-catalog.ts +35 -1
- package/src/providers/acp-targets.ts +69 -3
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/cursor-adapter.ts +1 -1
- package/src/script-workflows/supervisor.ts +11 -2
- package/src/scripts-runtime/eval-harness.ts +2 -12
- package/src/scripts-runtime/executors/quickjs-bundle.ts +152 -0
- package/src/scripts-runtime/executors/quickjs-prelude.ts +285 -0
- package/src/scripts-runtime/executors/quickjs-runner.ts +580 -0
- package/src/scripts-runtime/executors/quickjs-worker.ts +81 -0
- package/src/scripts-runtime/executors/quickjs.ts +196 -0
- package/src/scripts-runtime/executors/registry.ts +2 -0
- package/src/scripts-runtime/executors/sourcemap.ts +79 -0
- package/src/scripts-runtime/script-error-hints.ts +14 -0
- package/src/server.ts +3 -1
- package/src/tests/acp-adapter.test.ts +127 -2
- package/src/tests/boot-scrub-sweep.test.ts +390 -0
- package/src/tests/check-text-columns.test.ts +145 -0
- package/src/tests/claude-sdk-process.fixture.ts +13 -2
- package/src/tests/codex-oauth-device.test.ts +23 -37
- package/src/tests/db-query-denylist.test.ts +202 -0
- package/src/tests/db-task-read-contract.test.ts +1 -0
- package/src/tests/db-task-write-contract.test.ts +1 -0
- package/src/tests/entrypoint-acp-target-binary.test.ts +31 -1
- package/src/tests/events-scrub.test.ts +88 -0
- package/src/tests/fixtures/secret-corpus.ts +320 -0
- package/src/tests/script-connections.test.ts +5 -0
- package/src/tests/script-executor-conformance.test.ts +3 -0
- package/src/tests/script-executor-quickjs.test.ts +442 -0
- package/src/tests/script-executor-registry.test.ts +6 -1
- package/src/tests/script-runs-http.test.ts +88 -1
- package/src/tests/script-runs-scrub.test.ts +275 -0
- package/src/tests/scripts-http.test.ts +82 -1
- package/src/tests/scrubbed-text-12e.test.ts +289 -0
- package/src/tests/sealed-key-guard.test.ts +166 -0
- package/src/tests/secret-canary-sweep.test.ts +958 -0
- package/src/tests/secret-registry.test.ts +355 -0
- package/src/tests/secret-rules.test.ts +230 -0
- package/src/tests/secret-scrubber.test.ts +3 -2
- package/src/tests/workflow-replay.test.ts +518 -0
- package/src/tests/workflow-scrub.test.ts +242 -0
- package/src/tools/db-query.ts +1 -1
- package/src/tools/inject-learning.ts +1 -1
- package/src/tools/workflows/get-workflow-run.ts +3 -2
- package/src/types.ts +1 -1
- package/src/utils/secret-rules.generated.ts +2055 -0
- package/src/utils/secret-scrubber.ts +363 -29
- package/src/workflows/engine.ts +39 -4
- package/src/workflows/recovery.ts +10 -1
- package/src/workflows/resume.ts +62 -3
- package/src/workflows/retry-poller.ts +3 -0
- package/dist/boot-scrub-logs-246yn076.js +0 -99
- package/dist/cli-3a4m1aws.js +0 -572
- package/dist/cli-a87b04k8.js +0 -951
- package/dist/cli-cvk6yzw5.js +0 -40
- package/dist/cli-h1spv8v4.js +0 -427
- package/dist/cli-jqt9hrnz.js +0 -168
- package/dist/cli-yhmwmv0g.js +0 -9281
- 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
|
-
|
|
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
|
|
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
|
|
225
|
-
expect(encrypted?.
|
|
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
|
|
322
|
-
const state = JSON.parse(decryptSecret(
|
|
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
|
|
328
|
-
|
|
329
|
-
|
|
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
|
|
363
|
-
const state = JSON.parse(decryptSecret(
|
|
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
|
|
371
|
-
|
|
372
|
-
|
|
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
|
|
372
|
+
const recovered = await getCodexDeviceFlow(started.flowId);
|
|
384
373
|
const recoveredState = JSON.parse(
|
|
385
|
-
decryptSecret(
|
|
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
|
|
407
|
-
const state = JSON.parse(decryptSecret(
|
|
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
|
|
413
|
-
|
|
414
|
-
|
|
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
|
|
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
|
+
});
|
|
@@ -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
|
+
});
|