@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
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { unlink } from "node:fs/promises";
|
|
3
|
+
import {
|
|
4
|
+
closeDb,
|
|
5
|
+
createScriptRun,
|
|
6
|
+
getDbClient,
|
|
7
|
+
getRunningScriptRuns,
|
|
8
|
+
getScriptRunJournalStep,
|
|
9
|
+
initDb,
|
|
10
|
+
listScriptRunJournalSteps,
|
|
11
|
+
recordInlineScriptRun,
|
|
12
|
+
updateScriptRun,
|
|
13
|
+
updateScriptRunIfNotTerminal,
|
|
14
|
+
updateScriptRunIfRunning,
|
|
15
|
+
upsertScriptRunJournalStep,
|
|
16
|
+
} from "../be/db";
|
|
17
|
+
import { breaksJsonValidity, scrubJsonValue } from "../be/scrub-json";
|
|
18
|
+
import { isSealedJson } from "../be/sealed-json";
|
|
19
|
+
import {
|
|
20
|
+
localProcessScriptExecutor,
|
|
21
|
+
type ScriptExecutionResult,
|
|
22
|
+
type StartScriptExecutionInput,
|
|
23
|
+
} from "../script-workflows/executor";
|
|
24
|
+
import { setScriptRunExecutor, startScriptRunProcess } from "../script-workflows/supervisor";
|
|
25
|
+
import { refreshSecretScrubberCache, scrubSecrets } from "../utils/secret-scrubber";
|
|
26
|
+
import { randomToken, type SyntheticSecret, syntheticSecret } from "./synthetic-secret-helpers";
|
|
27
|
+
|
|
28
|
+
const TEST_DB_PATH = "./test-script-runs-scrub.sqlite";
|
|
29
|
+
const AGENT_ID = "5c2b0000-0000-4000-8000-0000000012c0";
|
|
30
|
+
// Distinctive non-secret context that must survive redaction.
|
|
31
|
+
const CONTEXT = "wombatlantern";
|
|
32
|
+
|
|
33
|
+
let known: SyntheticSecret;
|
|
34
|
+
let ghToken: string;
|
|
35
|
+
|
|
36
|
+
async function removeDbFiles(): Promise<void> {
|
|
37
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
38
|
+
try {
|
|
39
|
+
await unlink(TEST_DB_PATH + suffix);
|
|
40
|
+
} catch {}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Free text carrying a registered known value and a structural GitHub token. */
|
|
45
|
+
function secretText(label: string): string {
|
|
46
|
+
return `${label} ${CONTEXT} known=${known.value} gh=${ghToken} end`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function expectRedacted(value: string | null | undefined, where: string): void {
|
|
50
|
+
expect(value, where).toBeString();
|
|
51
|
+
expect(value!.includes(known.value), `${where} still holds the known value`).toBe(false);
|
|
52
|
+
expect(value!.includes(ghToken), `${where} still holds the token`).toBe(false);
|
|
53
|
+
expect(value!, where).toContain("[REDACTED:");
|
|
54
|
+
expect(value!, where).toContain(CONTEXT);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function runRow(id: string) {
|
|
58
|
+
return getDbClient().get<{ args: string; output: string | null; error: string | null }>(
|
|
59
|
+
"SELECT args, output, error FROM script_runs WHERE id = ?",
|
|
60
|
+
[id],
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function seedRun(): Promise<string> {
|
|
65
|
+
const id = crypto.randomUUID();
|
|
66
|
+
await createScriptRun({ id, agentId: AGENT_ID, source: "export default () => 1", args: null });
|
|
67
|
+
await updateScriptRun(id, { status: "running" });
|
|
68
|
+
return id;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
describe("script run writers scrub at write", () => {
|
|
72
|
+
beforeAll(async () => {
|
|
73
|
+
await removeDbFiles();
|
|
74
|
+
initDb(TEST_DB_PATH);
|
|
75
|
+
ghToken = ["ghp", randomToken(36)].join("_");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
afterAll(async () => {
|
|
79
|
+
known.cleanup();
|
|
80
|
+
refreshSecretScrubberCache();
|
|
81
|
+
closeDb();
|
|
82
|
+
await removeDbFiles();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
beforeEach(async () => {
|
|
86
|
+
// The test preload clears volatile secrets after every test.
|
|
87
|
+
known = syntheticSecret("scriptrun");
|
|
88
|
+
refreshSecretScrubberCache();
|
|
89
|
+
await getDbClient().run("DELETE FROM script_run_journal");
|
|
90
|
+
await getDbClient().run("DELETE FROM script_runs");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("recordInlineScriptRun redacts args, output and error", async () => {
|
|
94
|
+
const id = crypto.randomUUID();
|
|
95
|
+
const now = new Date().toISOString();
|
|
96
|
+
await recordInlineScriptRun({
|
|
97
|
+
id,
|
|
98
|
+
agentId: AGENT_ID,
|
|
99
|
+
source: "export default () => 1",
|
|
100
|
+
args: { note: secretText("args") },
|
|
101
|
+
status: "failed",
|
|
102
|
+
output: { lines: [secretText("output")] },
|
|
103
|
+
error: scrubSecrets(secretText("error")),
|
|
104
|
+
startedAt: now,
|
|
105
|
+
finishedAt: now,
|
|
106
|
+
});
|
|
107
|
+
const row = await runRow(id);
|
|
108
|
+
expectRedacted(row?.args, "args");
|
|
109
|
+
expectRedacted(row?.output, "output");
|
|
110
|
+
expectRedacted(row?.error, "error");
|
|
111
|
+
// JSON columns still parse for readers.
|
|
112
|
+
expect((JSON.parse(row!.args) as { note: string }).note).toContain(CONTEXT);
|
|
113
|
+
expect((JSON.parse(row!.output!) as { lines: string[] }).lines[0]).toContain(CONTEXT);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("createScriptRun seals args: nothing readable at rest, redacted for callers", async () => {
|
|
117
|
+
const id = crypto.randomUUID();
|
|
118
|
+
const args = { note: secretText("launch") };
|
|
119
|
+
const { run } = await createScriptRun({
|
|
120
|
+
id,
|
|
121
|
+
agentId: AGENT_ID,
|
|
122
|
+
source: "export default () => 1",
|
|
123
|
+
args,
|
|
124
|
+
});
|
|
125
|
+
const stored = (await runRow(id))?.args ?? "";
|
|
126
|
+
expect(isSealedJson(stored)).toBe(true);
|
|
127
|
+
expect(stored.includes(known.value)).toBe(false);
|
|
128
|
+
expect(stored.includes(ghToken)).toBe(false);
|
|
129
|
+
expect(stored.includes(CONTEXT)).toBe(false);
|
|
130
|
+
expectRedacted((run.args as { note: string }).note, "run.args");
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("a relaunch after restart executes with the exact sealed args", async () => {
|
|
134
|
+
const id = crypto.randomUUID();
|
|
135
|
+
const args = { note: secretText("relaunch"), n: 3 };
|
|
136
|
+
await createScriptRun({ id, agentId: AGENT_ID, source: "export default () => 1", args });
|
|
137
|
+
await updateScriptRun(id, { status: "running" });
|
|
138
|
+
|
|
139
|
+
// A restarted API sees the run as running with no live process and
|
|
140
|
+
// relaunches it from the row, as reconcileScriptRuns does.
|
|
141
|
+
const [recovered] = await getRunningScriptRuns();
|
|
142
|
+
expect(recovered?.id).toBe(id);
|
|
143
|
+
expectRedacted((recovered!.args as { note: string }).note, "recovered.args");
|
|
144
|
+
|
|
145
|
+
let launched: StartScriptExecutionInput | undefined;
|
|
146
|
+
let exit!: (result: ScriptExecutionResult) => void;
|
|
147
|
+
setScriptRunExecutor({
|
|
148
|
+
async start(input) {
|
|
149
|
+
launched = input;
|
|
150
|
+
return {
|
|
151
|
+
pid: null,
|
|
152
|
+
tmpdir: "/tmp",
|
|
153
|
+
startedAtMs: Date.now(),
|
|
154
|
+
exited: new Promise((resolve) => {
|
|
155
|
+
exit = resolve;
|
|
156
|
+
}),
|
|
157
|
+
async terminate() {},
|
|
158
|
+
async cleanup() {},
|
|
159
|
+
};
|
|
160
|
+
},
|
|
161
|
+
isRunning: () => false,
|
|
162
|
+
async terminatePid() {},
|
|
163
|
+
});
|
|
164
|
+
try {
|
|
165
|
+
await startScriptRunProcess(recovered!, "http://127.0.0.1:1", "test-key");
|
|
166
|
+
expect(launched?.run.args).toEqual(args);
|
|
167
|
+
} finally {
|
|
168
|
+
exit({ exitCode: 0, stderr: "" });
|
|
169
|
+
setScriptRunExecutor(localProcessScriptExecutor);
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("updateScriptRun redacts output and error", async () => {
|
|
174
|
+
const id = await seedRun();
|
|
175
|
+
await updateScriptRun(id, {
|
|
176
|
+
status: "failed",
|
|
177
|
+
output: { summary: secretText("output") },
|
|
178
|
+
error: secretText("error"),
|
|
179
|
+
});
|
|
180
|
+
const row = await runRow(id);
|
|
181
|
+
expectRedacted(row?.output, "output");
|
|
182
|
+
expectRedacted(row?.error, "error");
|
|
183
|
+
expect((JSON.parse(row!.output!) as { summary: string }).summary).toContain(CONTEXT);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test("updateScriptRunIfRunning redacts the supervisor's stderr error", async () => {
|
|
187
|
+
const id = await seedRun();
|
|
188
|
+
const claimed = await updateScriptRunIfRunning(id, {
|
|
189
|
+
status: "failed",
|
|
190
|
+
pid: null,
|
|
191
|
+
finishedAt: new Date().toISOString(),
|
|
192
|
+
error: `Traceback\n${secretText("stderr")}\n`,
|
|
193
|
+
});
|
|
194
|
+
expect(claimed).toBe(true);
|
|
195
|
+
expectRedacted((await runRow(id))?.error, "error");
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test("updateScriptRunIfNotTerminal redacts output", async () => {
|
|
199
|
+
const id = await seedRun();
|
|
200
|
+
const claimed = await updateScriptRunIfNotTerminal(id, {
|
|
201
|
+
status: "completed",
|
|
202
|
+
output: secretText("output"),
|
|
203
|
+
});
|
|
204
|
+
expect(claimed).toBe(true);
|
|
205
|
+
const output = (await runRow(id))?.output;
|
|
206
|
+
expectRedacted(output, "output");
|
|
207
|
+
expect(JSON.parse(output!)).toContain(CONTEXT);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test("null error and output still clear the columns", async () => {
|
|
211
|
+
const id = await seedRun();
|
|
212
|
+
await updateScriptRun(id, { output: { a: 1 }, error: "boom" });
|
|
213
|
+
await updateScriptRun(id, { output: undefined, error: null });
|
|
214
|
+
const row = await runRow(id);
|
|
215
|
+
expect(row?.output).toBeNull();
|
|
216
|
+
expect(row?.error).toBeNull();
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test("upsertScriptRunJournalStep redacts config and error, seals result for replay", async () => {
|
|
220
|
+
const id = await seedRun();
|
|
221
|
+
const result = { text: secretText("result") };
|
|
222
|
+
await upsertScriptRunJournalStep({
|
|
223
|
+
runId: id,
|
|
224
|
+
stepKey: "fetch",
|
|
225
|
+
stepType: "raw-llm",
|
|
226
|
+
config: { prompt: secretText("config") },
|
|
227
|
+
status: "failed",
|
|
228
|
+
result,
|
|
229
|
+
error: scrubSecrets(secretText("error")),
|
|
230
|
+
});
|
|
231
|
+
const row = await getDbClient().get<{
|
|
232
|
+
config: string;
|
|
233
|
+
result: string | null;
|
|
234
|
+
error: string | null;
|
|
235
|
+
}>("SELECT config, result, error FROM script_run_journal WHERE runId = ?", [id]);
|
|
236
|
+
expectRedacted(row?.config, "config");
|
|
237
|
+
expectRedacted(row?.error, "error");
|
|
238
|
+
expect((JSON.parse(row!.config) as { prompt: string }).prompt).toContain(CONTEXT);
|
|
239
|
+
// Replay contract: the harness returns this verbatim as the step result,
|
|
240
|
+
// so it is sealed at rest and opened exactly only on the replay route.
|
|
241
|
+
expect(isSealedJson(row?.result ?? "")).toBe(true);
|
|
242
|
+
expect(row!.result!.includes(known.value)).toBe(false);
|
|
243
|
+
expect(row!.result!.includes(ghToken)).toBe(false);
|
|
244
|
+
expect((await getScriptRunJournalStep(id, "fetch"))?.result).toEqual(result);
|
|
245
|
+
const [listed] = await listScriptRunJournalSteps(id);
|
|
246
|
+
expectRedacted((listed?.result as { text: string }).text, "listed result");
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
describe("scrubJsonValue", () => {
|
|
251
|
+
test("output always parses and keeps non-secret structure", () => {
|
|
252
|
+
known = syntheticSecret("jsonguard");
|
|
253
|
+
refreshSecretScrubberCache();
|
|
254
|
+
try {
|
|
255
|
+
const value = { a: [`k=${known.value}`, 2], b: { c: CONTEXT }, d: null };
|
|
256
|
+
const out = scrubJsonValue(value);
|
|
257
|
+
expect(out.includes(known.value)).toBe(false);
|
|
258
|
+
expect(out).toContain("[REDACTED:");
|
|
259
|
+
expect(JSON.parse(out)).toEqual({
|
|
260
|
+
a: [expect.stringContaining("[REDACTED:"), 2],
|
|
261
|
+
b: { c: CONTEXT },
|
|
262
|
+
d: null,
|
|
263
|
+
});
|
|
264
|
+
} finally {
|
|
265
|
+
known.cleanup();
|
|
266
|
+
refreshSecretScrubberCache();
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test("breaksJsonValidity flags only a valid-to-invalid change", () => {
|
|
271
|
+
expect(breaksJsonValidity('{"a":"x"}', '{"a":"[REDACTED:x]')).toBe(true);
|
|
272
|
+
expect(breaksJsonValidity('{"a":"x"}', '{"a":"[REDACTED:x]"}')).toBe(false);
|
|
273
|
+
expect(breaksJsonValidity("not json", "still not")).toBe(false);
|
|
274
|
+
});
|
|
275
|
+
});
|
|
@@ -6,11 +6,16 @@ import { createApp } from "../apps/store";
|
|
|
6
6
|
import { closeDb, createAgent, getDbClient, initDb } from "../be/db";
|
|
7
7
|
import { getScript, listScripts } from "../be/scripts/db";
|
|
8
8
|
import { setScriptEmbeddingProviderForTests } from "../be/scripts/embeddings";
|
|
9
|
+
import { configSecretName, registerStoredSecret } from "../be/secret-registry";
|
|
9
10
|
import { handleCore } from "../http/core";
|
|
10
11
|
import { handleScripts } from "../http/scripts";
|
|
11
12
|
import { getPathSegments, parseQueryParams } from "../http/utils";
|
|
12
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
clearVolatileSecretsForTesting,
|
|
15
|
+
refreshSecretScrubberCache,
|
|
16
|
+
} from "../utils/secret-scrubber";
|
|
13
17
|
import { SKIP_SANDBOX_SPAWN_TESTS } from "./sandbox-spawn-test-helpers";
|
|
18
|
+
import { randomToken } from "./synthetic-secret-helpers";
|
|
14
19
|
|
|
15
20
|
const skip = test.skipIf(SKIP_SANDBOX_SPAWN_TESTS);
|
|
16
21
|
|
|
@@ -648,3 +653,79 @@ describe("/api/scripts HTTP", () => {
|
|
|
648
653
|
expect(result?.argsJsonSchema).not.toBeNull();
|
|
649
654
|
});
|
|
650
655
|
});
|
|
656
|
+
|
|
657
|
+
describe("script source with an embedded secret is refused at write", () => {
|
|
658
|
+
const sourceWith = (literal: string) =>
|
|
659
|
+
`export default async () => ({ auth: ${JSON.stringify(literal)} });`;
|
|
660
|
+
|
|
661
|
+
async function storedSourceRows(): Promise<number> {
|
|
662
|
+
const row = await getDbClient().get<{ n: number }>(
|
|
663
|
+
`SELECT (SELECT COUNT(*) FROM scripts) + (SELECT COUNT(*) FROM script_versions)
|
|
664
|
+
+ (SELECT COUNT(*) FROM script_runs) AS n`,
|
|
665
|
+
);
|
|
666
|
+
return row?.n ?? 0;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
type Refusal = { error: string; message: string; findings: { kind: string; id: string }[] };
|
|
670
|
+
|
|
671
|
+
async function expectRefused(
|
|
672
|
+
response: TestResponse,
|
|
673
|
+
secretValue: string,
|
|
674
|
+
finding: { kind: string; id: string },
|
|
675
|
+
): Promise<void> {
|
|
676
|
+
expect(response.status).toBe(400);
|
|
677
|
+
expect(response.text).not.toContain(secretValue);
|
|
678
|
+
const body = (await response.json()) as Refusal;
|
|
679
|
+
expect(body.error).toBe("source_contains_secret");
|
|
680
|
+
expect(body.findings).toContainEqual(finding);
|
|
681
|
+
expect(body.message).toContain(finding.id);
|
|
682
|
+
expect(body.message).toContain("ctx.swarm.config.get");
|
|
683
|
+
expect(body.message).toContain("credential binding");
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
afterAll(() => clearVolatileSecretsForTesting());
|
|
687
|
+
|
|
688
|
+
test("script-upsert refuses a registered secret in every encoded form, and persists nothing", async () => {
|
|
689
|
+
const value = `upsert-${randomToken(32)}`;
|
|
690
|
+
const name = configSecretName("UPSTREAM_TOKEN");
|
|
691
|
+
registerStoredSecret(value, name);
|
|
692
|
+
const before = await storedSourceRows();
|
|
693
|
+
for (const form of [
|
|
694
|
+
value,
|
|
695
|
+
Buffer.from(value).toString("base64"),
|
|
696
|
+
Buffer.from(value).toString("base64url"),
|
|
697
|
+
]) {
|
|
698
|
+
const response = await upsert({ name: "leaky-upsert", source: sourceWith(form) });
|
|
699
|
+
await expectRefused(response, value, { kind: "registered-secret", id: name });
|
|
700
|
+
expect(response.text).not.toContain(form);
|
|
701
|
+
}
|
|
702
|
+
expect(await storedSourceRows()).toBe(before);
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
test("script-upsert refuses a gitleaks token shape and names the rule", async () => {
|
|
706
|
+
const token = `ghp_${randomToken(36)}`;
|
|
707
|
+
const before = await storedSourceRows();
|
|
708
|
+
const response = await upsert({ name: "leaky-gitleaks", source: sourceWith(token) });
|
|
709
|
+
await expectRefused(response, token, { kind: "gitleaks-rule", id: "github-pat" });
|
|
710
|
+
expect(await storedSourceRows()).toBe(before);
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
test("inline script-run refuses before running, saving a scratch script or recording the run", async () => {
|
|
714
|
+
const value = `inline-${randomToken(32)}`;
|
|
715
|
+
const name = configSecretName("INLINE_TOKEN");
|
|
716
|
+
registerStoredSecret(value, name);
|
|
717
|
+
const before = await storedSourceRows();
|
|
718
|
+
const response = await dispatch("/api/scripts/run", {
|
|
719
|
+
method: "POST",
|
|
720
|
+
agentId: workerId,
|
|
721
|
+
body: JSON.stringify({ source: sourceWith(value), intent: "leaky inline" }),
|
|
722
|
+
});
|
|
723
|
+
await expectRefused(response, value, { kind: "registered-secret", id: name });
|
|
724
|
+
expect(await storedSourceRows()).toBe(before);
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
test("control: clean source still upserts", async () => {
|
|
728
|
+
const response = await upsert({ name: "clean-upsert", source: validSource(2) });
|
|
729
|
+
expect(response.status).toBe(200);
|
|
730
|
+
});
|
|
731
|
+
});
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Write-time scrub for memory, channel and inbox messages, approval requests,
|
|
3
|
+
* schedules, task text and agent_log.oldValue. Each test writes a synthetic
|
|
4
|
+
* secret through the real writer, reads the row back from SQLite, and checks
|
|
5
|
+
* that the secret is gone while a redaction marker and the surrounding
|
|
6
|
+
* non-secret text survive.
|
|
7
|
+
*/
|
|
8
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
9
|
+
import { unlink } from "node:fs/promises";
|
|
10
|
+
import { recordSchedulePreflightFailure } from "../be/automation-preflight";
|
|
11
|
+
import {
|
|
12
|
+
cancelApprovalRequestById,
|
|
13
|
+
cancelPendingApprovalRequestsForRun,
|
|
14
|
+
closeDb,
|
|
15
|
+
createAgent,
|
|
16
|
+
createApprovalRequest,
|
|
17
|
+
createChannel,
|
|
18
|
+
createInboxMessage,
|
|
19
|
+
createLogEntry,
|
|
20
|
+
createScheduledTask,
|
|
21
|
+
createTask,
|
|
22
|
+
createTaskExtended,
|
|
23
|
+
createWorkflow,
|
|
24
|
+
createWorkflowRun,
|
|
25
|
+
getApprovalRequestById,
|
|
26
|
+
getDbClient,
|
|
27
|
+
initDb,
|
|
28
|
+
markInboxMessageResponded,
|
|
29
|
+
postMessage,
|
|
30
|
+
recordApprovalVotes,
|
|
31
|
+
resolveApprovalRequest,
|
|
32
|
+
updateScheduledTask,
|
|
33
|
+
} from "../be/db";
|
|
34
|
+
import { SqliteMemoryStore } from "../be/memory/providers/sqlite-store";
|
|
35
|
+
import { isSealedJson } from "../be/sealed-json";
|
|
36
|
+
import { registerVolatileSecret } from "../utils/secret-scrubber";
|
|
37
|
+
import { type SyntheticSecret, syntheticSecret } from "./synthetic-secret-helpers";
|
|
38
|
+
|
|
39
|
+
const TEST_DB_PATH = "./test-scrubbed-text-12e.sqlite";
|
|
40
|
+
const agentId = "aaaaaaaa-0000-4000-8000-000000000e12";
|
|
41
|
+
|
|
42
|
+
let secret: SyntheticSecret;
|
|
43
|
+
|
|
44
|
+
async function removeDbFiles(): Promise<void> {
|
|
45
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
46
|
+
try {
|
|
47
|
+
await unlink(TEST_DB_PATH + suffix);
|
|
48
|
+
} catch {}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Secret absent, a redaction marker present, and the context kept. */
|
|
53
|
+
function expectScrubbed(value: string | null | undefined, context: string): void {
|
|
54
|
+
expect(value).toBeTruthy();
|
|
55
|
+
expect(value).not.toContain(secret.value);
|
|
56
|
+
expect(value).toContain("[REDACTED:");
|
|
57
|
+
expect(value).toContain(context);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function column(table: string, col: string, id: string): Promise<string | null> {
|
|
61
|
+
const row = await getDbClient().get<Record<string, string | null>>(
|
|
62
|
+
`SELECT "${col}" AS v FROM ${table} WHERE id = ?`,
|
|
63
|
+
[id],
|
|
64
|
+
);
|
|
65
|
+
return row?.v ?? null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
beforeAll(async () => {
|
|
69
|
+
await removeDbFiles();
|
|
70
|
+
initDb(TEST_DB_PATH);
|
|
71
|
+
await createAgent({ id: agentId, name: "Scrub 12e Agent", isLead: false, status: "idle" });
|
|
72
|
+
secret = syntheticSecret("scrub12e");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// The test preload clears volatile secrets after every test, so register per test.
|
|
76
|
+
beforeEach(() => registerVolatileSecret(secret.value, secret.name));
|
|
77
|
+
|
|
78
|
+
afterAll(async () => {
|
|
79
|
+
secret.cleanup();
|
|
80
|
+
closeDb();
|
|
81
|
+
await removeDbFiles();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe("agent_memory", () => {
|
|
85
|
+
test("store scrubs name, content, summary, tags, version row and FTS row", async () => {
|
|
86
|
+
const store = new SqliteMemoryStore();
|
|
87
|
+
const memory = await store.store({
|
|
88
|
+
agentId,
|
|
89
|
+
scope: "agent",
|
|
90
|
+
name: `deploy note ${secret.value}`,
|
|
91
|
+
content: `the deploy token is ${secret.value} for staging`,
|
|
92
|
+
summary: `summary ${secret.value}`,
|
|
93
|
+
tags: [`tag ${secret.value}`, "plain"],
|
|
94
|
+
source: "manual",
|
|
95
|
+
});
|
|
96
|
+
// Callers embed the returned content, so it must be the scrubbed text.
|
|
97
|
+
expectScrubbed(memory.content, "for staging");
|
|
98
|
+
expectScrubbed(await column("agent_memory", "name", memory.id), "deploy note");
|
|
99
|
+
expectScrubbed(await column("agent_memory", "content", memory.id), "for staging");
|
|
100
|
+
expectScrubbed(await column("agent_memory", "summary", memory.id), "summary");
|
|
101
|
+
const tags = await column("agent_memory", "tags", memory.id);
|
|
102
|
+
expectScrubbed(tags, "plain");
|
|
103
|
+
expect(JSON.parse(tags!)[0]).toContain("tag ");
|
|
104
|
+
const version = await getDbClient().get<{ content: string }>(
|
|
105
|
+
"SELECT content FROM agent_memory_version WHERE memory_id = ?",
|
|
106
|
+
[memory.id],
|
|
107
|
+
);
|
|
108
|
+
expectScrubbed(version?.content, "for staging");
|
|
109
|
+
const fts = await getDbClient().get<{ content: string }>(
|
|
110
|
+
"SELECT content FROM memory_fts WHERE memory_id = ?",
|
|
111
|
+
[memory.id],
|
|
112
|
+
);
|
|
113
|
+
if (fts) expectScrubbed(fts.content, "for staging");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("edit scrubs the new content", async () => {
|
|
117
|
+
const store = new SqliteMemoryStore();
|
|
118
|
+
const memory = await store.store({
|
|
119
|
+
agentId,
|
|
120
|
+
scope: "agent",
|
|
121
|
+
name: "editable",
|
|
122
|
+
content: "clean body",
|
|
123
|
+
source: "manual",
|
|
124
|
+
});
|
|
125
|
+
const result = await store.edit({
|
|
126
|
+
id: memory.id,
|
|
127
|
+
mode: "replace",
|
|
128
|
+
content: `edited body ${secret.value} end`,
|
|
129
|
+
intent: "edit with secret",
|
|
130
|
+
changedByAgentId: agentId,
|
|
131
|
+
});
|
|
132
|
+
expect(result.changed).toBe(true);
|
|
133
|
+
expectScrubbed(result.memory.content, "edited body");
|
|
134
|
+
expectScrubbed(await column("agent_memory", "content", memory.id), "edited body");
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe("agent_tasks.task and agent_log.oldValue", () => {
|
|
139
|
+
test("createTaskExtended scrubs the task text", async () => {
|
|
140
|
+
const task = await createTaskExtended(`run the job with ${secret.value} please`, { agentId });
|
|
141
|
+
expectScrubbed(await column("agent_tasks", "task", task.id), "run the job");
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("createTask scrubs the task text", async () => {
|
|
145
|
+
const task = await createTask(agentId, `legacy create ${secret.value} please`);
|
|
146
|
+
expectScrubbed(await column("agent_tasks", "task", task.id), "legacy create");
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("createLogEntry scrubs oldValue", async () => {
|
|
150
|
+
const log = await createLogEntry({
|
|
151
|
+
eventType: "task_status_change",
|
|
152
|
+
agentId,
|
|
153
|
+
oldValue: `previous ${secret.value}`,
|
|
154
|
+
newValue: "next",
|
|
155
|
+
});
|
|
156
|
+
expectScrubbed(await column("agent_log", "oldValue", log.id), "previous");
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
describe("channel and inbox messages", () => {
|
|
161
|
+
test("postMessage scrubs content", async () => {
|
|
162
|
+
const channel = await createChannel(`scrub-12e-${crypto.randomUUID().slice(0, 8)}`);
|
|
163
|
+
const message = await postMessage(channel.id, agentId, `hello ${secret.value} channel`);
|
|
164
|
+
expectScrubbed(await column("channel_messages", "content", message.id), "channel");
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("createInboxMessage and markInboxMessageResponded scrub free text", async () => {
|
|
168
|
+
const inbox = await createInboxMessage(agentId, `inbox body ${secret.value}`, {
|
|
169
|
+
matchedText: `matched ${secret.value}`,
|
|
170
|
+
});
|
|
171
|
+
expectScrubbed(await column("inbox_messages", "content", inbox.id), "inbox body");
|
|
172
|
+
expectScrubbed(await column("inbox_messages", "matchedText", inbox.id), "matched");
|
|
173
|
+
await markInboxMessageResponded(inbox.id, `reply ${secret.value}`);
|
|
174
|
+
expectScrubbed(await column("inbox_messages", "responseText", inbox.id), "reply");
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
describe("scheduled_tasks", () => {
|
|
179
|
+
test("create and update scrub description, taskTemplate and lastErrorMessage", async () => {
|
|
180
|
+
const schedule = await createScheduledTask({
|
|
181
|
+
name: `scrub-12e-${crypto.randomUUID().slice(0, 8)}`,
|
|
182
|
+
intervalMs: 60_000,
|
|
183
|
+
description: `desc ${secret.value}`,
|
|
184
|
+
taskTemplate: `call the api with ${secret.value}`,
|
|
185
|
+
});
|
|
186
|
+
expectScrubbed(await column("scheduled_tasks", "description", schedule.id), "desc");
|
|
187
|
+
expectScrubbed(await column("scheduled_tasks", "taskTemplate", schedule.id), "call the api");
|
|
188
|
+
|
|
189
|
+
await updateScheduledTask(schedule.id, {
|
|
190
|
+
description: `new desc ${secret.value}`,
|
|
191
|
+
taskTemplate: `new template ${secret.value}`,
|
|
192
|
+
lastErrorMessage: `boom ${secret.value}`,
|
|
193
|
+
});
|
|
194
|
+
expectScrubbed(await column("scheduled_tasks", "description", schedule.id), "new desc");
|
|
195
|
+
expectScrubbed(await column("scheduled_tasks", "taskTemplate", schedule.id), "new template");
|
|
196
|
+
expectScrubbed(await column("scheduled_tasks", "lastErrorMessage", schedule.id), "boom");
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("preflight failure scrubs the message and still dedups the same day", async () => {
|
|
200
|
+
const schedule = await createScheduledTask({
|
|
201
|
+
name: `scrub-12e-${crypto.randomUUID().slice(0, 8)}`,
|
|
202
|
+
intervalMs: 60_000,
|
|
203
|
+
taskTemplate: "work",
|
|
204
|
+
});
|
|
205
|
+
const now = new Date();
|
|
206
|
+
expect(await recordSchedulePreflightFailure(schedule.id, `missing ${secret.value}`, now)).toBe(
|
|
207
|
+
true,
|
|
208
|
+
);
|
|
209
|
+
expectScrubbed(await column("scheduled_tasks", "lastErrorMessage", schedule.id), "missing");
|
|
210
|
+
// Same raw reason the same day: the scrubbed compare still matches.
|
|
211
|
+
expect(await recordSchedulePreflightFailure(schedule.id, `missing ${secret.value}`, now)).toBe(
|
|
212
|
+
false,
|
|
213
|
+
);
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
describe("approval_requests", () => {
|
|
218
|
+
test("create, vote and resolve scrub title, questions, approvals and resolutionReason, seal responses", async () => {
|
|
219
|
+
const id = crypto.randomUUID();
|
|
220
|
+
await createApprovalRequest({
|
|
221
|
+
id,
|
|
222
|
+
title: `approve ${secret.value} deploy`,
|
|
223
|
+
questions: [{ id: "q1", type: "text", label: `paste ${secret.value} here` }],
|
|
224
|
+
approvers: { users: [], policy: "any" },
|
|
225
|
+
});
|
|
226
|
+
expectScrubbed(await column("approval_requests", "title", id), "deploy");
|
|
227
|
+
const questions = await column("approval_requests", "questions", id);
|
|
228
|
+
expectScrubbed(questions, "paste");
|
|
229
|
+
expect(() => JSON.parse(questions!)).not.toThrow();
|
|
230
|
+
|
|
231
|
+
const vote = {
|
|
232
|
+
responder: "operator",
|
|
233
|
+
approved: true,
|
|
234
|
+
responses: { q1: `note ${secret.value}` },
|
|
235
|
+
respondedAt: new Date().toISOString(),
|
|
236
|
+
};
|
|
237
|
+
expect(await recordApprovalVotes(id, [vote])).toBe(true);
|
|
238
|
+
const votes = await column("approval_requests", "approvals", id);
|
|
239
|
+
expectScrubbed(votes, "note");
|
|
240
|
+
expect(() => JSON.parse(votes!)).not.toThrow();
|
|
241
|
+
|
|
242
|
+
const responses = { q1: `note ${secret.value}` };
|
|
243
|
+
await resolveApprovalRequest(id, {
|
|
244
|
+
status: "approved",
|
|
245
|
+
responses,
|
|
246
|
+
approvals: [vote],
|
|
247
|
+
resolutionReason: `because ${secret.value}`,
|
|
248
|
+
});
|
|
249
|
+
expectScrubbed(await column("approval_requests", "resolutionReason", id), "because");
|
|
250
|
+
expectScrubbed(await column("approval_requests", "approvals", id), "note");
|
|
251
|
+
|
|
252
|
+
// Responses resume the workflow on recovery, so they stay byte-exact for
|
|
253
|
+
// the reader and hold no plaintext at rest.
|
|
254
|
+
const stored = await column("approval_requests", "responses", id);
|
|
255
|
+
expect(isSealedJson(stored ?? "")).toBe(true);
|
|
256
|
+
expect(stored).not.toContain(secret.value);
|
|
257
|
+
expect(stored).not.toContain("note");
|
|
258
|
+
expect((await getApprovalRequestById(id))?.responses).toEqual(responses);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test("cancel paths scrub the reason", async () => {
|
|
262
|
+
const single = crypto.randomUUID();
|
|
263
|
+
await createApprovalRequest({
|
|
264
|
+
id: single,
|
|
265
|
+
title: "single",
|
|
266
|
+
questions: [],
|
|
267
|
+
approvers: { users: [], policy: "any" },
|
|
268
|
+
});
|
|
269
|
+
await cancelApprovalRequestById(single, { reason: `stop ${secret.value}`, resolvedBy: null });
|
|
270
|
+
expectScrubbed(await column("approval_requests", "resolutionReason", single), "stop");
|
|
271
|
+
|
|
272
|
+
const workflow = await createWorkflow({
|
|
273
|
+
name: `scrub-12e-${crypto.randomUUID().slice(0, 8)}`,
|
|
274
|
+
definition: { nodes: [] },
|
|
275
|
+
});
|
|
276
|
+
const runId = crypto.randomUUID();
|
|
277
|
+
await createWorkflowRun({ id: runId, workflowId: workflow.id });
|
|
278
|
+
const inRun = crypto.randomUUID();
|
|
279
|
+
await createApprovalRequest({
|
|
280
|
+
id: inRun,
|
|
281
|
+
title: "in run",
|
|
282
|
+
questions: [],
|
|
283
|
+
approvers: { users: [], policy: "any" },
|
|
284
|
+
workflowRunId: runId,
|
|
285
|
+
});
|
|
286
|
+
await cancelPendingApprovalRequestsForRun(runId, `run cancelled ${secret.value}`);
|
|
287
|
+
expectScrubbed(await column("approval_requests", "resolutionReason", inRun), "run cancelled");
|
|
288
|
+
});
|
|
289
|
+
});
|