@pensar/apex 2.6.1 → 2.7.0-canary.21cb83dc
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/build/{agent-yby11zaq.js → agent-68t2wzmc.js} +6 -6
- package/build/agent-jhekpern.js +13 -0
- package/build/{agent-ayreaj6z.js → agent-qvreg8a4.js} +5 -5
- package/build/{agent-runs-g37vkrjr.js → agent-runs-30dvbvmm.js} +18 -18
- package/build/agent-vybw255f.js +13 -0
- package/build/{apps-xn6km9yn.js → apps-na0y0992.js} +13 -13
- package/build/{auth-d105wc64.js → auth-dz2c5thx.js} +13 -13
- package/build/{blackboxAgent-s56sgx9b.js → blackboxAgent-2mwvaajb.js} +6 -6
- package/build/{blackboxPentest-ps78v3w7.js → blackboxPentest-s2mfze9p.js} +17 -12
- package/build/{cli-kb21qhvm.js → cli-1pmfnz1a.js} +4 -3
- package/build/{cli-ae8jqxqr.js → cli-563yntrb.js} +2 -2
- package/build/{cli-7eea4e2y.js → cli-5ab346kd.js} +6 -6
- package/build/{cli-1ve18npr.js → cli-72y76bpg.js} +3 -2
- package/build/{cli-fy6vh2a7.js → cli-98wmmphf.js} +1 -1
- package/build/{cli-c05y640n.js → cli-9jscnhtg.js} +1 -1
- package/build/{cli-3sp6nd8d.js → cli-9kk9tm58.js} +2 -2
- package/build/{cli-d37a42dc.js → cli-9pz9wjt2.js} +101 -18
- package/build/{cli-dpsw2fe6.js → cli-a6x4jm5a.js} +476 -193
- package/build/{cli-az3xq70v.js → cli-cfrdr25z.js} +1 -1
- package/build/{cli-ngq9dr8n.js → cli-fdrrbb9p.js} +3 -3
- package/build/{cli-k2nwxta0.js → cli-t09zj814.js} +2 -2
- package/build/{cli-qgz1q9g5.js → cli-t4q4t4dx.js} +223 -76
- package/build/{cli-xtc2t6me.js → cli-vdd1s3x9.js} +103 -39
- package/build/{cli-gx71jcke.js → cli-vn4cwac3.js} +1 -1
- package/build/{cli-65g6rcat.js → cli-w9nw2a4z.js} +1 -1
- package/build/{cli-n2en7j5b.js → cli-z7ysyrr4.js} +1 -1
- package/build/{cli-709rvekb.js → cli-zbbtr4sb.js} +4 -2
- package/build/cli.js +70 -40
- package/build/{config-zpqkd5cz.js → config-nkzm9arw.js} +1 -1
- package/build/{fastStrike-mcjdvwez.js → fastStrike-16jw1s61.js} +67 -15
- package/build/{fixes-kytx5dmj.js → fixes-6wxk22v5.js} +13 -13
- package/build/{index-4vqdgb42.js → index-36p56pcf.js} +1473 -1217
- package/build/{index-je6hcsqr.js → index-6nejymsc.js} +1 -1
- package/build/{index-fv64eb32.js → index-asm4y7e4.js} +2 -2
- package/build/{index-n65z6v7b.js → index-bfmn52za.js} +6 -6
- package/build/{index-qv4cc65p.js → index-bt4r13q7.js} +13 -13
- package/build/{index-dr3tg1w3.js → index-gfq1rp1q.js} +1 -1
- package/build/{index-wqetp6gq.js → index-j9pxgrzd.js} +12 -7
- package/build/{index-r9yjkfrv.js → index-n9kapn1x.js} +4 -4
- package/build/{index-ns4m4s01.js → index-tr6evhyg.js} +4 -4
- package/build/{issues-eb3q9d3a.js → issues-7rmf4ker.js} +13 -13
- package/build/{knowledge-fqsvtx0f.js → knowledge-s2sxh8nb.js} +13 -13
- package/build/lib/{chunk-1azdancq.js → chunk-0dnx79b1.js} +1 -1
- package/build/lib/{chunk-4hwzqthr.js → chunk-161ak2am.js} +3 -2
- package/build/lib/{chunk-z137x08b.js → chunk-1rqbhx4b.js} +7 -5
- package/build/lib/{chunk-1yct6cra.js → chunk-3hjvdf67.js} +1 -1
- package/build/lib/{chunk-r2dkp4tb.js → chunk-5j8pp0xf.js} +1 -1
- package/build/lib/{chunk-3c00n0cy.js → chunk-77se1d35.js} +8 -3
- package/build/lib/{chunk-yrnrbjes.js → chunk-7tft4b8b.js} +4 -2
- package/build/lib/{chunk-w66ce82a.js → chunk-8njjwwcw.js} +4 -2
- package/build/lib/{chunk-v3dk9j7k.js → chunk-aq9eyxas.js} +80 -21
- package/build/lib/{chunk-9tt2021m.js → chunk-d05cvnv8.js} +9 -9
- package/build/lib/{chunk-e8yy2k73.js → chunk-f3sq4896.js} +28 -2
- package/build/lib/{chunk-1fy8azee.js → chunk-fasqtjq1.js} +1 -1
- package/build/lib/{chunk-1jvwqnem.js → chunk-gm3b9j4y.js} +104 -40
- package/build/lib/{chunk-r4j8ybn9.js → chunk-hys7nemr.js} +87 -36
- package/build/lib/{chunk-7pwa92rn.js → chunk-j9668h16.js} +1 -1
- package/build/lib/{chunk-ba41jh41.js → chunk-kcw4cpr3.js} +418 -133
- package/build/lib/{chunk-z96pbjhd.js → chunk-m4swxy5r.js} +13 -6
- package/build/lib/{chunk-xhdpgpkm.js → chunk-mbsde9d0.js} +2 -2
- package/build/lib/{chunk-9yksqgzr.js → chunk-mpvjfvz0.js} +20 -13
- package/build/lib/{chunk-2qsq76xq.js → chunk-qkz5sm0d.js} +128 -34
- package/build/lib/{chunk-nhfax4gb.js → chunk-rfyjqybp.js} +1 -1
- package/build/lib/{chunk-mg03jdmt.js → chunk-vsnwzne5.js} +1 -1
- package/build/lib/{chunk-acgbyf5h.js → chunk-ydxhkgjw.js} +3 -2
- package/build/lib/{chunk-bb0ekhkw.js → chunk-z8kej0sp.js} +69 -18
- package/build/lib/core/agents/agentRuntime.d.ts +2 -1
- package/build/lib/core/agents/defineAgent.d.ts +3 -2
- package/build/lib/core/agents/offSecAgent/index.d.ts +3 -1
- package/build/lib/core/agents/offSecAgent/index.js +10 -8
- package/build/lib/core/agents/offSecAgent/prompt.d.ts +3 -1
- package/build/lib/core/agents/offSecAgent/subagentSpawner.d.ts +2 -1
- package/build/lib/core/agents/offSecAgent/subagentSpawner.js +5 -5
- package/build/lib/core/agents/offSecAgent/tools/index.js +4 -4
- package/build/lib/core/agents/offSecAgent/tools/listMemories.d.ts +11 -1
- package/build/lib/core/agents/offSecAgent/tools/reportError.d.ts +2 -1
- package/build/lib/core/agents/offSecAgent/tools/runPentestWorkflow.d.ts +20 -1
- package/build/lib/core/agents/offSecAgent/tools/spawnPentestAgent.d.ts +4 -7
- package/build/lib/core/agents/offSecAgent/tools/spawnPentestSwarm.d.ts +7 -1
- package/build/lib/core/agents/offSecAgent/tools/types.d.ts +2 -1
- package/build/lib/core/agents/offSecAgent/types.d.ts +7 -1
- package/build/lib/core/agents/specialized/attackSurface/blackboxAgent.js +9 -9
- package/build/lib/core/agents/specialized/attackSurface/schemas.js +9 -9
- package/build/lib/core/agents/specialized/authenticationAgent/index.js +10 -10
- package/build/lib/core/agents/specialized/codeAgent/agent.js +9 -9
- package/build/lib/core/agents/specialized/codeAgent/prompts.d.ts +1 -0
- package/build/lib/core/agents/specialized/findingJudge/agent.js +8 -8
- package/build/lib/core/agents/specialized/findingJudge/index.js +3 -3
- package/build/lib/core/agents/specialized/patching/index.js +10 -10
- package/build/lib/core/agents/specialized/pentest/agent.d.ts +18 -4
- package/build/lib/core/agents/specialized/pentest/agent.js +11 -9
- package/build/lib/core/agents/specialized/whiteboxAttackSurface/index.js +9 -9
- package/build/lib/core/ai/ai.d.ts +23 -6
- package/build/lib/core/ai/ai.js +2 -2
- package/build/lib/core/ai/index.js +2 -2
- package/build/lib/core/ai/models/claude-capabilities.d.ts +3 -2
- package/build/lib/core/ai/models/concentrate.d.ts +1 -0
- package/build/lib/core/ai/models/index.d.ts +1 -1
- package/build/lib/core/ai/providers/concentrate.d.ts +1 -0
- package/build/lib/core/ai/utils.d.ts +2 -0
- package/build/lib/core/ai/utils.js +4 -2
- package/build/lib/core/api/index.d.ts +3 -0
- package/build/lib/core/api/index.js +22 -19
- package/build/lib/core/api/targetedPentest.d.ts +1 -0
- package/build/lib/core/api/targetedPentest.js +10 -10
- package/build/lib/core/config/config.d.ts +3 -0
- package/build/lib/core/config/index.js +1 -1
- package/build/lib/core/findings/registry.js +3 -3
- package/build/lib/core/session/index.d.ts +12 -0
- package/build/lib/core/session/index.js +2 -2
- package/build/lib/core/session/persistence.d.ts +7 -0
- package/build/lib/core/session/persistence.js +7 -3
- package/build/lib/core/workflows/fastStrike.d.ts +3 -1
- package/build/lib/core/workflows/pentest.d.ts +8 -1
- package/build/lib/core/workflows/pentest.js +15 -15
- package/build/lib/core/workflows/seams.js +4 -4
- package/build/lib/core/workflows/whiteboxAttackSurface.js +11 -11
- package/build/lib/index.js +22 -22
- package/build/{logs-x62b7hk1.js → logs-70fyr848.js} +13 -13
- package/build/{native-rollout-evidence-c9yhfqcs.js → native-rollout-evidence-ntensvzb.js} +3 -3
- package/build/{offesecAgent-5whnpeta.js → offesecAgent-kk62tf60.js} +4 -4
- package/build/pentest-4jgznn7h.js +22 -0
- package/build/{pentests-e0f32aqx.js → pentests-3e41hj08.js} +13 -13
- package/build/recordedRunClient-w34kcch1.js +11 -0
- package/build/{targetedPentest-rcbs4nvp.js → targetedPentest-t9snxcj2.js} +18 -11
- package/build/{targets-xj9g3q0z.js → targets-1pyf21c7.js} +13 -13
- package/build/threatModel-48y5qjze.js +21 -0
- package/build/{uninstall-7s784q4a.js → uninstall-fbydd9t6.js} +1 -1
- package/build/{upload-4gvhwmvr.js → upload-sbhwe2fq.js} +4 -4
- package/build/{utils-cjrz64pc.js → utils-23903gcj.js} +3 -3
- package/package.json +1 -1
- package/src/cli.ts +48 -14
- package/src/core/agents/agentRuntime.test.ts +38 -0
- package/src/core/agents/agentRuntime.ts +5 -2
- package/src/core/agents/defineAgent.ts +3 -1
- package/src/core/agents/offSecAgent/index.ts +3 -0
- package/src/core/agents/offSecAgent/offensiveSecurityAgent.test.ts +92 -5
- package/src/core/agents/offSecAgent/offensiveSecurityAgent.ts +70 -31
- package/src/core/agents/offSecAgent/prompt.test.ts +46 -0
- package/src/core/agents/offSecAgent/prompt.ts +52 -8
- package/src/core/agents/offSecAgent/selectiveConstruction.publicPath.test.ts +38 -0
- package/src/core/agents/offSecAgent/selectiveConstruction.test.ts +64 -0
- package/src/core/agents/offSecAgent/subagentSpawner.ts +12 -1
- package/src/core/agents/offSecAgent/tools/delegateAuth.ts +1 -0
- package/src/core/agents/offSecAgent/tools/listMemories.test.ts +230 -0
- package/src/core/agents/offSecAgent/tools/listMemories.ts +73 -6
- package/src/core/agents/offSecAgent/tools/reportError.test.ts +6 -0
- package/src/core/agents/offSecAgent/tools/reportError.ts +10 -2
- package/src/core/agents/offSecAgent/tools/runAttackSurface.ts +2 -0
- package/src/core/agents/offSecAgent/tools/runPentestWorkflow.test.ts +57 -0
- package/src/core/agents/offSecAgent/tools/runPentestWorkflow.ts +19 -1
- package/src/core/agents/offSecAgent/tools/runWhiteboxScan.ts +5 -2
- package/src/core/agents/offSecAgent/tools/spawnCodingAgent.ts +1 -0
- package/src/core/agents/offSecAgent/tools/spawnPentestAgent.test.ts +72 -2
- package/src/core/agents/offSecAgent/tools/spawnPentestAgent.ts +32 -11
- package/src/core/agents/offSecAgent/tools/spawnPentestSwarm.ts +25 -5
- package/src/core/agents/offSecAgent/tools/threatModelGenerator.ts +1 -0
- package/src/core/agents/offSecAgent/tools/types.ts +6 -1
- package/src/core/agents/offSecAgent/tools/updateTask.ts +5 -1
- package/src/core/agents/offSecAgent/tools/whiteboxCandidates.ts +11 -4
- package/src/core/agents/offSecAgent/types.ts +9 -0
- package/src/core/agents/specialized/codeAgent/agent.test.ts +7 -0
- package/src/core/agents/specialized/codeAgent/agent.ts +9 -2
- package/src/core/agents/specialized/codeAgent/prompts.ts +6 -0
- package/src/core/agents/specialized/pentest/agent.test.ts +116 -0
- package/src/core/agents/specialized/pentest/agent.ts +178 -49
- package/src/core/agents/specialized/pentest/results.test.ts +59 -4
- package/src/core/agents/specialized/whiteboxAttackSurface/endpointDocumentationAgent.ts +1 -0
- package/src/core/ai/ai.operator-context.bench.test.ts +1408 -0
- package/src/core/ai/ai.recorded-context.test.ts +74 -0
- package/src/core/ai/ai.test.ts +54 -0
- package/src/core/ai/ai.ts +312 -82
- package/src/core/ai/compactionTelemetry.test.ts +2 -2
- package/src/core/ai/models/anthropic.ts +6 -0
- package/src/core/ai/models/bedrock.ts +18 -0
- package/src/core/ai/models/claude-capabilities.ts +29 -8
- package/src/core/ai/models/concentrate.ts +52 -0
- package/src/core/ai/models/index.ts +25 -4
- package/src/core/ai/models/models.test.ts +154 -0
- package/src/core/ai/models/openrouter.ts +43 -1
- package/src/core/ai/providers/anthropic-models.test.ts +60 -1
- package/src/core/ai/providers/bedrock-models.test.ts +81 -0
- package/src/core/ai/providers/concentrate.test.ts +31 -0
- package/src/core/ai/providers/concentrate.ts +11 -0
- package/src/core/ai/providers/gateway-models.test.ts +16 -6
- package/src/core/ai/utils.native-rollout-evidence.test.ts +1 -0
- package/src/core/ai/utils.ts +189 -143
- package/src/core/api/blackboxPentest.ts +9 -1
- package/src/core/api/index.ts +9 -0
- package/src/core/api/targetedPentest.ts +19 -5
- package/src/core/config/config.test.ts +35 -0
- package/src/core/config/config.ts +5 -0
- package/src/core/observability/telemetry.test.ts +2 -3
- package/src/core/session/index.ts +5 -0
- package/src/core/session/persistence.test.ts +62 -0
- package/src/core/session/persistence.ts +56 -0
- package/src/core/workflows/fastStrike.seams.test.ts +149 -0
- package/src/core/workflows/fastStrike.test.ts +18 -1
- package/src/core/workflows/fastStrike.ts +103 -11
- package/src/core/workflows/pentest.test.ts +104 -0
- package/src/core/workflows/pentest.ts +110 -7
- package/src/core/workflows/seams.test.ts +13 -0
- package/src/core/workflows/seams.ts +5 -1
- package/src/core/workflows/whiteboxAttackSurface.ts +1 -0
- package/src/tui/components/chat/loading-indicator.tsx +20 -4
- package/src/tui/components/chat/message-list.tsx +1 -0
- package/src/tui/components/commands/__tests__/advanced-settings-lifecycle.tsx +176 -0
- package/src/tui/components/commands/advanced-settings.test.ts +28 -0
- package/src/tui/components/commands/advanced-settings.tsx +237 -19
- package/src/tui/components/operator-dashboard/index.tsx +46 -9
- package/src/tui/components/operator-dashboard/logic.test.ts +106 -0
- package/src/tui/components/operator-dashboard/logic.ts +42 -2
- package/src/tui/components/operator-dashboard/run-events.test.ts +3 -1
- package/src/tui/components/shared/__tests__/tool-renderer-lifecycle.tsx +18 -0
- package/src/tui/components/shared/action-label.test.ts +9 -0
- package/src/tui/components/shared/compacting-icon.tsx +18 -0
- package/src/tui/components/shared/index.ts +1 -0
- package/src/tui/components/shared/tool-registry.ts +1 -0
- package/src/tui/components/shared/tool-renderer.tsx +21 -7
- package/src/tui/context/route.tsx +1 -0
- package/src/tui/keybindings/registry.ts +2 -0
- package/build/agent-6w7egcf1.js +0 -13
- package/build/agent-9zx75feh.js +0 -13
- package/build/pentest-e61hc38s.js +0 -22
- package/build/recordedRunClient-dp032c32.js +0 -11
- package/build/threatModel-jkxfa40s.js +0 -21
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
type Memory,
|
|
4
|
+
type MemoryBackend,
|
|
5
|
+
type MemorySummary,
|
|
6
|
+
withMemoryBackend,
|
|
7
|
+
} from "../../../memory";
|
|
8
|
+
import { type GetMemoryResult, getMemory } from "./getMemory";
|
|
9
|
+
import { type ListMemoriesResult, listMemories } from "./listMemories";
|
|
10
|
+
import type { ToolContext } from "./types";
|
|
11
|
+
|
|
12
|
+
const ctx = { session: { id: "synthetic" } } as ToolContext;
|
|
13
|
+
const options = { toolCallId: "synthetic-call", messages: [] };
|
|
14
|
+
const list = listMemories(ctx).execute;
|
|
15
|
+
const get = getMemory(ctx).execute;
|
|
16
|
+
if (!list || !get) throw new Error("Memory tools must be executable");
|
|
17
|
+
|
|
18
|
+
describe("list_memories pagination", () => {
|
|
19
|
+
it("continues from the first unreturned summary when the character budget fills", async () => {
|
|
20
|
+
const summaries: MemorySummary[] = Array.from({ length: 30 }, (_, i) => ({
|
|
21
|
+
id: `large-${i}`,
|
|
22
|
+
category: "general",
|
|
23
|
+
title: "t".repeat(300),
|
|
24
|
+
tags: Array.from({ length: 8 }, () => "tag".repeat(40)),
|
|
25
|
+
createdAt: new Date(Date.UTC(2026, 0, 1) - i * 1_000).toISOString(),
|
|
26
|
+
}));
|
|
27
|
+
const backend: MemoryBackend = {
|
|
28
|
+
add: vi.fn(async () => {
|
|
29
|
+
throw new Error("unexpected add");
|
|
30
|
+
}),
|
|
31
|
+
list: vi.fn(async () => summaries),
|
|
32
|
+
get: vi.fn(async () => null),
|
|
33
|
+
};
|
|
34
|
+
await withMemoryBackend(backend, async () => {
|
|
35
|
+
const first = (await list(
|
|
36
|
+
{ toolCallDescription: "List large synthetic memories" },
|
|
37
|
+
options,
|
|
38
|
+
)) as ListMemoriesResult;
|
|
39
|
+
expect(first.memories.length).toBeLessThan(20);
|
|
40
|
+
expect(JSON.stringify(first).length).toBeLessThan(12_500);
|
|
41
|
+
expect(first.nextCursor).toEqual({
|
|
42
|
+
id: first.memories.at(-1)?.id,
|
|
43
|
+
category: first.memories.at(-1)?.category,
|
|
44
|
+
createdAt: first.memories.at(-1)?.createdAt,
|
|
45
|
+
});
|
|
46
|
+
const second = (await list(
|
|
47
|
+
{
|
|
48
|
+
cursor: first.nextCursor ?? undefined,
|
|
49
|
+
toolCallDescription: "Continue large synthetic memories",
|
|
50
|
+
},
|
|
51
|
+
options,
|
|
52
|
+
)) as ListMemoriesResult;
|
|
53
|
+
expect(second.memories[0]?.id).toBe(summaries[first.memories.length]?.id);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("continues past the last returned memory when a newer one is added", async () => {
|
|
58
|
+
const original: MemorySummary[] = Array.from({ length: 30 }, (_, i) => ({
|
|
59
|
+
id: `memory-${i}`,
|
|
60
|
+
category: "general",
|
|
61
|
+
title: `Memory ${i}`,
|
|
62
|
+
tags: [],
|
|
63
|
+
createdAt: new Date(Date.UTC(2026, 0, 1) - i * 1_000).toISOString(),
|
|
64
|
+
}));
|
|
65
|
+
let summaries = original;
|
|
66
|
+
const backend: MemoryBackend = {
|
|
67
|
+
add: vi.fn(async () => {
|
|
68
|
+
throw new Error("unexpected add");
|
|
69
|
+
}),
|
|
70
|
+
list: vi.fn(async () => summaries),
|
|
71
|
+
get: vi.fn(async () => null),
|
|
72
|
+
};
|
|
73
|
+
await withMemoryBackend(backend, async () => {
|
|
74
|
+
const first = (await list(
|
|
75
|
+
{ toolCallDescription: "List first memory page" },
|
|
76
|
+
options,
|
|
77
|
+
)) as ListMemoriesResult;
|
|
78
|
+
summaries = [
|
|
79
|
+
{
|
|
80
|
+
...original[0],
|
|
81
|
+
id: "memory-new",
|
|
82
|
+
createdAt: new Date(Date.UTC(2026, 0, 2)).toISOString(),
|
|
83
|
+
},
|
|
84
|
+
...original,
|
|
85
|
+
];
|
|
86
|
+
const second = (await list(
|
|
87
|
+
{
|
|
88
|
+
cursor: first.nextCursor ?? undefined,
|
|
89
|
+
toolCallDescription: "Continue after an insertion",
|
|
90
|
+
},
|
|
91
|
+
options,
|
|
92
|
+
)) as ListMemoriesResult;
|
|
93
|
+
|
|
94
|
+
expect(first.memories.map((memory) => memory.id)).toEqual(
|
|
95
|
+
original.slice(0, 20).map((memory) => memory.id),
|
|
96
|
+
);
|
|
97
|
+
expect(second.memories.map((memory) => memory.id)).toEqual(
|
|
98
|
+
original.slice(20).map((memory) => memory.id),
|
|
99
|
+
);
|
|
100
|
+
expect(second.count).toBe(31);
|
|
101
|
+
expect(second.nextCursor).toBeNull();
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("uses stable tie ordering when the cursor memory is deleted", async () => {
|
|
106
|
+
const original: MemorySummary[] = Array.from({ length: 25 }, (_, i) => ({
|
|
107
|
+
id: `memory-${i.toString().padStart(2, "0")}`,
|
|
108
|
+
category: "general",
|
|
109
|
+
title: `Memory ${i}`,
|
|
110
|
+
tags: [],
|
|
111
|
+
createdAt: "2026-01-01T00:00:00.000Z",
|
|
112
|
+
}));
|
|
113
|
+
let summaries = [...original].reverse();
|
|
114
|
+
const backend: MemoryBackend = {
|
|
115
|
+
add: vi.fn(async () => {
|
|
116
|
+
throw new Error("unexpected add");
|
|
117
|
+
}),
|
|
118
|
+
list: vi.fn(async () => summaries),
|
|
119
|
+
get: vi.fn(async () => null),
|
|
120
|
+
};
|
|
121
|
+
await withMemoryBackend(backend, async () => {
|
|
122
|
+
const first = (await list(
|
|
123
|
+
{ toolCallDescription: "List tied memories" },
|
|
124
|
+
options,
|
|
125
|
+
)) as ListMemoriesResult;
|
|
126
|
+
expect(first.memories.map((memory) => memory.id)).toEqual(
|
|
127
|
+
original.slice(0, 20).map((memory) => memory.id),
|
|
128
|
+
);
|
|
129
|
+
summaries = summaries.filter(
|
|
130
|
+
(memory) => memory.id !== first.nextCursor?.id,
|
|
131
|
+
);
|
|
132
|
+
const second = (await list(
|
|
133
|
+
{
|
|
134
|
+
cursor: first.nextCursor ?? undefined,
|
|
135
|
+
toolCallDescription: "Continue after anchor deletion",
|
|
136
|
+
},
|
|
137
|
+
options,
|
|
138
|
+
)) as ListMemoriesResult;
|
|
139
|
+
expect(second.memories.map((memory) => memory.id)).toEqual(
|
|
140
|
+
original.slice(20).map((memory) => memory.id),
|
|
141
|
+
);
|
|
142
|
+
expect(second.nextCursor).toBeNull();
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("bounds each result while keeping every older memory discoverable", async () => {
|
|
147
|
+
const summaries: MemorySummary[] = Array.from({ length: 200 }, (_, i) => ({
|
|
148
|
+
id: `memory-${i}`,
|
|
149
|
+
category: "general",
|
|
150
|
+
title: `Memory ${i} ${"x".repeat(480)}`,
|
|
151
|
+
tags: ["synthetic", "history"],
|
|
152
|
+
createdAt: new Date(Date.UTC(2026, 0, 1) - i * 1_000).toISOString(),
|
|
153
|
+
}));
|
|
154
|
+
const backend: MemoryBackend = {
|
|
155
|
+
add: vi.fn(async () => {
|
|
156
|
+
throw new Error("unexpected add");
|
|
157
|
+
}),
|
|
158
|
+
list: vi.fn(async () =>
|
|
159
|
+
summaries.map((summary, index) =>
|
|
160
|
+
index === 0 ? { ...summary, content: "full-body-marker" } : summary,
|
|
161
|
+
),
|
|
162
|
+
),
|
|
163
|
+
get: vi.fn(async (_category, id) => {
|
|
164
|
+
const summary = summaries.find((item) => item.id === id);
|
|
165
|
+
return summary
|
|
166
|
+
? ({
|
|
167
|
+
...summary,
|
|
168
|
+
content: `Full ${id}`,
|
|
169
|
+
updatedAt: summary.createdAt,
|
|
170
|
+
} satisfies Memory)
|
|
171
|
+
: null;
|
|
172
|
+
}),
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
await withMemoryBackend(backend, async () => {
|
|
176
|
+
const allIds: string[] = [];
|
|
177
|
+
let cursor: ListMemoriesResult["nextCursor"] = null;
|
|
178
|
+
let firstResultChars = 0;
|
|
179
|
+
let pages = 0;
|
|
180
|
+
for (;;) {
|
|
181
|
+
const result = (await list(
|
|
182
|
+
{
|
|
183
|
+
cursor: cursor ?? undefined,
|
|
184
|
+
toolCallDescription: "List synthetic memories",
|
|
185
|
+
},
|
|
186
|
+
options,
|
|
187
|
+
)) as ListMemoriesResult;
|
|
188
|
+
expect(result.success).toBe(true);
|
|
189
|
+
expect(result.count).toBe(200);
|
|
190
|
+
expect(result.memories.length).toBeGreaterThan(0);
|
|
191
|
+
expect(result.memories.length).toBeLessThanOrEqual(20);
|
|
192
|
+
expect(JSON.stringify(result).length).toBeLessThan(12_500);
|
|
193
|
+
if (pages === 0) {
|
|
194
|
+
firstResultChars = JSON.stringify(result).length;
|
|
195
|
+
expect(JSON.stringify(result)).not.toContain("full-body-marker");
|
|
196
|
+
expect(result.memories[0]?.truncated).toBe(true);
|
|
197
|
+
expect(result.memories[0]?.title.length).toBe(160);
|
|
198
|
+
}
|
|
199
|
+
allIds.push(...result.memories.map((memory) => memory.id));
|
|
200
|
+
pages++;
|
|
201
|
+
if (result.nextCursor === null) break;
|
|
202
|
+
expect(result.nextCursor.id).toBe(result.memories.at(-1)?.id);
|
|
203
|
+
cursor = result.nextCursor;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
expect(allIds).toEqual(summaries.map((memory) => memory.id));
|
|
207
|
+
const oldest = (await get(
|
|
208
|
+
{
|
|
209
|
+
category: "general",
|
|
210
|
+
id: allIds.at(-1) ?? "",
|
|
211
|
+
toolCallDescription: "Read oldest synthetic memory",
|
|
212
|
+
},
|
|
213
|
+
options,
|
|
214
|
+
)) as GetMemoryResult;
|
|
215
|
+
expect(oldest.memory?.content).toBe("Full memory-199");
|
|
216
|
+
if (process.env.APEX_CONTEXT_BENCH === "1") {
|
|
217
|
+
const previousChars = JSON.stringify({
|
|
218
|
+
success: true,
|
|
219
|
+
error: "",
|
|
220
|
+
memories: summaries,
|
|
221
|
+
count: summaries.length,
|
|
222
|
+
}).length;
|
|
223
|
+
console.log(
|
|
224
|
+
"MEMORY_LIST_BENCH",
|
|
225
|
+
JSON.stringify({ previousChars, firstResultChars, pages }),
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
});
|
|
@@ -7,6 +7,9 @@ import {
|
|
|
7
7
|
} from "../../../memory";
|
|
8
8
|
import type { ToolContext } from "./types";
|
|
9
9
|
|
|
10
|
+
const PAGE_SIZE = 20;
|
|
11
|
+
const PAGE_CHAR_BUDGET = 12_000;
|
|
12
|
+
|
|
10
13
|
const listMemoriesInputSchema = z.object({
|
|
11
14
|
category: z
|
|
12
15
|
.enum(MEMORY_CATEGORIES)
|
|
@@ -19,6 +22,14 @@ const listMemoriesInputSchema = z.object({
|
|
|
19
22
|
.string()
|
|
20
23
|
.optional()
|
|
21
24
|
.describe("Optional tag to filter memories by (exact match)"),
|
|
25
|
+
cursor: z
|
|
26
|
+
.object({
|
|
27
|
+
createdAt: z.string(),
|
|
28
|
+
category: z.enum(MEMORY_CATEGORIES),
|
|
29
|
+
id: z.string(),
|
|
30
|
+
})
|
|
31
|
+
.optional()
|
|
32
|
+
.describe("Pass the previous nextCursor to continue a listing"),
|
|
22
33
|
toolCallDescription: z
|
|
23
34
|
.string()
|
|
24
35
|
.describe(
|
|
@@ -27,24 +38,53 @@ const listMemoriesInputSchema = z.object({
|
|
|
27
38
|
});
|
|
28
39
|
|
|
29
40
|
type ListMemoriesInput = z.infer<typeof listMemoriesInputSchema>;
|
|
41
|
+
type MemoryCursor = Pick<MemorySummary, "createdAt" | "category" | "id">;
|
|
30
42
|
|
|
31
43
|
export type ListMemoriesResult = {
|
|
32
44
|
success: boolean;
|
|
33
45
|
error: string;
|
|
34
|
-
memories: MemorySummary
|
|
46
|
+
memories: Array<MemorySummary & { truncated?: true }>;
|
|
35
47
|
count: number;
|
|
48
|
+
nextCursor: MemoryCursor | null;
|
|
36
49
|
};
|
|
37
50
|
|
|
51
|
+
function compareMemories(a: MemoryCursor, b: MemoryCursor): number {
|
|
52
|
+
const byCreatedAt = Date.parse(b.createdAt) - Date.parse(a.createdAt);
|
|
53
|
+
if (byCreatedAt > 0 || byCreatedAt < 0) return byCreatedAt;
|
|
54
|
+
return a.category.localeCompare(b.category) || a.id.localeCompare(b.id);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function previewSummary(memory: MemorySummary) {
|
|
58
|
+
const title = memory.title.slice(0, 160);
|
|
59
|
+
const tags = memory.tags.slice(0, 8).map((tag) => tag.slice(0, 48));
|
|
60
|
+
const truncated =
|
|
61
|
+
title.length < memory.title.length ||
|
|
62
|
+
tags.length < memory.tags.length ||
|
|
63
|
+
tags.some(
|
|
64
|
+
(tag, index) => tag.length < (memory.tags[index]?.length ?? tag.length),
|
|
65
|
+
);
|
|
66
|
+
return {
|
|
67
|
+
id: memory.id,
|
|
68
|
+
category: memory.category,
|
|
69
|
+
title,
|
|
70
|
+
tags,
|
|
71
|
+
createdAt: memory.createdAt,
|
|
72
|
+
...(truncated ? { truncated: true as const } : {}),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
38
76
|
export function listMemories(ctx: ToolContext) {
|
|
39
77
|
return tool({
|
|
40
78
|
description: `List saved memories, optionally filtered by category and/or tag.
|
|
41
79
|
|
|
42
|
-
Returns lightweight summaries (id, category, title, tags, createdAt)
|
|
43
|
-
most recent first. Use
|
|
44
|
-
|
|
80
|
+
Returns up to 20 lightweight summaries (id, category, title, tags, createdAt)
|
|
81
|
+
sorted by most recent first. Use nextCursor to continue until it is null;
|
|
82
|
+
count is the current total matching memories. A fresh listing includes newer
|
|
83
|
+
memories added ahead of a cursor. Titles and tags may be abbreviated in the
|
|
84
|
+
listing. Use category + id with get_memory to retrieve full content.`,
|
|
45
85
|
inputSchema: listMemoriesInputSchema,
|
|
46
86
|
execute: async (
|
|
47
|
-
{ category, tag },
|
|
87
|
+
{ category, tag, cursor },
|
|
48
88
|
{ toolCallId },
|
|
49
89
|
): Promise<ListMemoriesResult> => {
|
|
50
90
|
try {
|
|
@@ -52,11 +92,37 @@ full content.`,
|
|
|
52
92
|
{ category, tag },
|
|
53
93
|
{ sessionId: ctx.session.id, toolCallId },
|
|
54
94
|
);
|
|
95
|
+
const ordered = [...memories].sort(compareMemories);
|
|
96
|
+
let start = 0;
|
|
97
|
+
if (cursor) {
|
|
98
|
+
start = ordered.findIndex(
|
|
99
|
+
(memory) => compareMemories(memory, cursor) > 0,
|
|
100
|
+
);
|
|
101
|
+
if (start < 0) start = ordered.length;
|
|
102
|
+
}
|
|
103
|
+
const page: ListMemoriesResult["memories"] = [];
|
|
104
|
+
let pageChars = 0;
|
|
105
|
+
for (const memory of ordered.slice(start, start + PAGE_SIZE)) {
|
|
106
|
+
const summary = previewSummary(memory);
|
|
107
|
+
const chars = JSON.stringify(summary).length + 1;
|
|
108
|
+
if (page.length > 0 && pageChars + chars > PAGE_CHAR_BUDGET) break;
|
|
109
|
+
page.push(summary);
|
|
110
|
+
pageChars += chars;
|
|
111
|
+
}
|
|
112
|
+
const last = page.at(-1);
|
|
55
113
|
return {
|
|
56
114
|
success: true,
|
|
57
115
|
error: "",
|
|
58
|
-
memories,
|
|
116
|
+
memories: page,
|
|
59
117
|
count: memories.length,
|
|
118
|
+
nextCursor:
|
|
119
|
+
start + page.length < ordered.length && last
|
|
120
|
+
? {
|
|
121
|
+
createdAt: last.createdAt,
|
|
122
|
+
category: last.category,
|
|
123
|
+
id: last.id,
|
|
124
|
+
}
|
|
125
|
+
: null,
|
|
60
126
|
};
|
|
61
127
|
} catch (err: unknown) {
|
|
62
128
|
return {
|
|
@@ -64,6 +130,7 @@ full content.`,
|
|
|
64
130
|
error: err instanceof Error ? err.message : String(err),
|
|
65
131
|
memories: [],
|
|
66
132
|
count: 0,
|
|
133
|
+
nextCursor: null,
|
|
67
134
|
};
|
|
68
135
|
}
|
|
69
136
|
},
|
|
@@ -11,6 +11,12 @@ describe("report_error tool", () => {
|
|
|
11
11
|
expect(REPORT_ERROR_TOOL_NAME).toBe("report_error");
|
|
12
12
|
});
|
|
13
13
|
|
|
14
|
+
it("does not name the unavailable finding tool under external grading", () => {
|
|
15
|
+
const external = createReportErrorTool(vi.fn(), "external");
|
|
16
|
+
expect(external.description).toContain("final response");
|
|
17
|
+
expect(external.description).not.toContain("document_vulnerability");
|
|
18
|
+
});
|
|
19
|
+
|
|
14
20
|
it("forwards the reason + message to the onError callback", async () => {
|
|
15
21
|
const onError = vi.fn<(error: ReportedError) => void>();
|
|
16
22
|
const tool = createReportErrorTool(onError);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { tool } from "ai";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
+
import type { FindingPolicy } from "../types";
|
|
3
4
|
|
|
4
5
|
export const REPORT_ERROR_TOOL_NAME = "report_error" as const;
|
|
5
6
|
|
|
@@ -22,7 +23,14 @@ export type ReportedError = {
|
|
|
22
23
|
* of a successful result it surfaces a blocking error so the caller can turn
|
|
23
24
|
* the run into a failure (e.g. mark a scan endpoint failed with this message).
|
|
24
25
|
*/
|
|
25
|
-
export function createReportErrorTool(
|
|
26
|
+
export function createReportErrorTool(
|
|
27
|
+
onError: (error: ReportedError) => void,
|
|
28
|
+
findingPolicy: FindingPolicy = "native",
|
|
29
|
+
) {
|
|
30
|
+
const vulnerabilityGuidance =
|
|
31
|
+
findingPolicy === "external"
|
|
32
|
+
? "Do NOT use this for vulnerabilities (include them in your final response)"
|
|
33
|
+
: "Do NOT use this for vulnerabilities (use document_vulnerability)";
|
|
26
34
|
return tool({
|
|
27
35
|
description: `Report a blocking error that prevents you from completing the pentest, and END your run.
|
|
28
36
|
|
|
@@ -31,7 +39,7 @@ Call this ONLY when you genuinely cannot continue, for example:
|
|
|
31
39
|
- The target is unreachable or responds in a way that makes testing impossible.
|
|
32
40
|
- Some other runtime condition blocks you from carrying out the objectives.
|
|
33
41
|
|
|
34
|
-
|
|
42
|
+
${vulnerabilityGuidance} or for normal completion (use response). Provide a clear, specific message describing exactly what blocked you — include the failing URL, status codes, or the auth step that failed where relevant — so it can be surfaced to the operator.`,
|
|
35
43
|
inputSchema: z.object({
|
|
36
44
|
reason: ReportErrorReasonSchema.describe(
|
|
37
45
|
"Coarse category of the blocking error.",
|
|
@@ -74,6 +74,7 @@ should be passed directly to spawn_pentest_swarm for deep testing.`,
|
|
|
74
74
|
attackSurfaceRegistry: ctx.attackSurfaceRegistry,
|
|
75
75
|
},
|
|
76
76
|
runtime: {
|
|
77
|
+
findingPolicy: ctx.findingPolicy,
|
|
77
78
|
session: ctx.session,
|
|
78
79
|
model: ctx.model,
|
|
79
80
|
authConfig: ctx.authConfig,
|
|
@@ -144,6 +145,7 @@ should be passed directly to spawn_pentest_swarm for deep testing.`,
|
|
|
144
145
|
attackSurfaceRegistry: ctx.attackSurfaceRegistry,
|
|
145
146
|
},
|
|
146
147
|
runtime: {
|
|
148
|
+
findingPolicy: ctx.findingPolicy,
|
|
147
149
|
session: ctx.session,
|
|
148
150
|
model: ctx.model,
|
|
149
151
|
authConfig: ctx.authConfig,
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
const workflow = vi.hoisted(() => vi.fn());
|
|
4
|
+
|
|
5
|
+
vi.mock("../../../workflows/pentest", () => ({
|
|
6
|
+
runPentestWorkflow: workflow,
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
import { runPentestWorkflow } from "./runPentestWorkflow";
|
|
10
|
+
import type { ToolContext } from "./types";
|
|
11
|
+
|
|
12
|
+
describe("run_pentest_workflow external finding handoff", () => {
|
|
13
|
+
it("returns reported findings to the parent instead of claiming zero were documented", async () => {
|
|
14
|
+
workflow.mockResolvedValue({
|
|
15
|
+
findings: [],
|
|
16
|
+
reportedFindings: ["Cross-tenant read at GET /private"],
|
|
17
|
+
objectiveResults: [
|
|
18
|
+
{
|
|
19
|
+
objective: "Extract the assigned flag",
|
|
20
|
+
completed: true,
|
|
21
|
+
result: "Extracted FLAG{example}",
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
findingsPath: "/session/findings",
|
|
25
|
+
pocsPath: "/session/pocs",
|
|
26
|
+
reportPath: null,
|
|
27
|
+
});
|
|
28
|
+
const ctx = {
|
|
29
|
+
findingPolicy: "external",
|
|
30
|
+
model: "test",
|
|
31
|
+
session: { id: "ses_external", rootPath: "/session" },
|
|
32
|
+
} as ToolContext;
|
|
33
|
+
const agentTool = runPentestWorkflow(ctx);
|
|
34
|
+
expect(agentTool.description).toContain("caller's evaluation");
|
|
35
|
+
const result = await agentTool.execute?.(
|
|
36
|
+
{ target: "https://example.com", toolCallDescription: "test" },
|
|
37
|
+
{ toolCallId: "tc_external", messages: [] },
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
expect(workflow).toHaveBeenCalledWith(
|
|
41
|
+
expect.objectContaining({ findingPolicy: "external" }),
|
|
42
|
+
);
|
|
43
|
+
expect(result).toMatchObject({
|
|
44
|
+
success: true,
|
|
45
|
+
totalFindings: 1,
|
|
46
|
+
reportedFindings: ["Cross-tenant read at GET /private"],
|
|
47
|
+
objectiveResults: [
|
|
48
|
+
{
|
|
49
|
+
objective: "Extract the assigned flag",
|
|
50
|
+
completed: true,
|
|
51
|
+
result: "Extracted FLAG{example}",
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
});
|
|
55
|
+
expect((result as { message: string }).message).not.toContain("documented");
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -11,13 +11,17 @@ import type { ToolContext } from "./types";
|
|
|
11
11
|
* need to orchestrate multiple tool calls manually.
|
|
12
12
|
*/
|
|
13
13
|
export function runPentestWorkflow(ctx: ToolContext) {
|
|
14
|
+
const resultStep =
|
|
15
|
+
ctx.findingPolicy === "external"
|
|
16
|
+
? "3. Return evidence-backed findings for the caller's evaluation"
|
|
17
|
+
: "3. Aggregate findings and generate the final report";
|
|
14
18
|
return tool({
|
|
15
19
|
description: `Run the full autonomous penetration test pipeline against a target.
|
|
16
20
|
|
|
17
21
|
This single tool call executes the entire pentest workflow:
|
|
18
22
|
1. Attack surface discovery (blackbox or whitebox)
|
|
19
23
|
2. Spawn a swarm of targeted pentest agents
|
|
20
|
-
|
|
24
|
+
${resultStep}
|
|
21
25
|
|
|
22
26
|
Use this when you want to run a complete, end-to-end pentest. The workflow
|
|
23
27
|
handles session creation, concurrency, persistence, and reporting automatically.
|
|
@@ -54,6 +58,7 @@ Omit \`cwd\` for blackbox mode (live target probing only).`,
|
|
|
54
58
|
|
|
55
59
|
try {
|
|
56
60
|
const result = await workflow({
|
|
61
|
+
findingPolicy: ctx.findingPolicy,
|
|
57
62
|
target,
|
|
58
63
|
cwd,
|
|
59
64
|
model: ctx.model,
|
|
@@ -66,6 +71,7 @@ Omit \`cwd\` for blackbox mode (live target probing only).`,
|
|
|
66
71
|
openAIReasoningEffort: ctx.openAIReasoningEffort,
|
|
67
72
|
surfaceIntegrationEnabled: ctx.surfaceIntegrationEnabled,
|
|
68
73
|
hooks: {
|
|
74
|
+
findingPolicy: ctx.findingPolicy,
|
|
69
75
|
backends: ctx.backends,
|
|
70
76
|
subagentSpawner: ctx.subagentSpawner,
|
|
71
77
|
languageModelMiddleware: ctx.languageModelMiddleware,
|
|
@@ -81,6 +87,18 @@ Omit \`cwd\` for blackbox mode (live target probing only).`,
|
|
|
81
87
|
const minutes = Math.floor(elapsedSeconds / 60);
|
|
82
88
|
const seconds = elapsedSeconds % 60;
|
|
83
89
|
|
|
90
|
+
if (ctx.findingPolicy === "external") {
|
|
91
|
+
const reportedFindings = result.reportedFindings ?? [];
|
|
92
|
+
return {
|
|
93
|
+
success: true,
|
|
94
|
+
totalFindings: reportedFindings.length,
|
|
95
|
+
reportedFindings,
|
|
96
|
+
objectiveResults: result.objectiveResults ?? [],
|
|
97
|
+
duration: `${minutes}m ${seconds}s`,
|
|
98
|
+
message: `Pentest complete. ${reportedFindings.length} finding(s) returned for external evaluation.`,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
84
102
|
return {
|
|
85
103
|
success: true,
|
|
86
104
|
totalFindings: result.findings.length,
|
|
@@ -22,13 +22,16 @@ const ScanKindSchema = z.enum([
|
|
|
22
22
|
]);
|
|
23
23
|
|
|
24
24
|
export function runWhiteboxScan(ctx: ToolContext) {
|
|
25
|
+
const verificationGuidance =
|
|
26
|
+
ctx.findingPolicy === "external"
|
|
27
|
+
? "as recoverable conditions. Static results are candidates only; assess them against the caller's evidence requirements."
|
|
28
|
+
: "as recoverable conditions. Static results are candidates only; verify before\ncalling document_vulnerability.";
|
|
25
29
|
return tool({
|
|
26
30
|
description: `Run installed whitebox security scanners with bounded output.
|
|
27
31
|
|
|
28
32
|
The tool detects applicable scanner adapters for the repository and normalizes
|
|
29
33
|
results into concise summaries plus artifact paths. Missing tools are reported
|
|
30
|
-
|
|
31
|
-
calling document_vulnerability.`,
|
|
34
|
+
${verificationGuidance}`,
|
|
32
35
|
inputSchema: z.object({
|
|
33
36
|
path: z
|
|
34
37
|
.string()
|
|
@@ -188,6 +188,7 @@ async function runSingleCodingAgent(
|
|
|
188
188
|
const result = await spawner.spawn<CodingChildResult | undefined>({
|
|
189
189
|
spec: { type: "code", codebasePath, objective },
|
|
190
190
|
runtime: {
|
|
191
|
+
findingPolicy: ctx.findingPolicy,
|
|
191
192
|
session: ctx.session,
|
|
192
193
|
model: ctx.model!,
|
|
193
194
|
authConfig: ctx.authConfig,
|
|
@@ -23,6 +23,9 @@ const registryCalls: Array<{
|
|
|
23
23
|
path: string;
|
|
24
24
|
options: Record<string, unknown>;
|
|
25
25
|
}> = [];
|
|
26
|
+
const swarmMock = vi.hoisted(() => ({
|
|
27
|
+
results: [] as Array<Record<string, unknown>>,
|
|
28
|
+
}));
|
|
26
29
|
|
|
27
30
|
vi.mock("../../specialized/pentest/agent", () => ({
|
|
28
31
|
TargetedPentestAgent: class {
|
|
@@ -46,7 +49,7 @@ vi.mock("../../../findings/registry", () => ({
|
|
|
46
49
|
|
|
47
50
|
vi.mock("../../../workflows/pentest", () => ({
|
|
48
51
|
DEFAULT_CONCURRENCY: 10,
|
|
49
|
-
runPentestSwarm: async () =>
|
|
52
|
+
runPentestSwarm: async () => swarmMock.results,
|
|
50
53
|
}));
|
|
51
54
|
|
|
52
55
|
import type { AIModel } from "../../../ai";
|
|
@@ -94,7 +97,12 @@ function makeParentSession(): {
|
|
|
94
97
|
async function runSpawn(
|
|
95
98
|
ctx: ToolContext,
|
|
96
99
|
inputOverrides: Record<string, unknown> = {},
|
|
97
|
-
): Promise<{
|
|
100
|
+
): Promise<{
|
|
101
|
+
success?: boolean;
|
|
102
|
+
message?: string;
|
|
103
|
+
findingsCount?: number;
|
|
104
|
+
reportedFindings?: string[];
|
|
105
|
+
}> {
|
|
98
106
|
const tool = spawnPentestAgent(ctx);
|
|
99
107
|
// biome-ignore lint/suspicious/noExplicitAny: ai-sdk tool execute receives an opaque ExecuteContext we don't need to narrow for this test.
|
|
100
108
|
return await (tool as any).execute(
|
|
@@ -110,6 +118,31 @@ async function runSpawn(
|
|
|
110
118
|
);
|
|
111
119
|
}
|
|
112
120
|
|
|
121
|
+
it("passes external grading to a worker and returns its findings without native documentation", async () => {
|
|
122
|
+
const spawn = vi.fn().mockResolvedValue({
|
|
123
|
+
findings: [],
|
|
124
|
+
reportedFindings: ["Cross-tenant read at GET /private"],
|
|
125
|
+
objectiveResults: [],
|
|
126
|
+
});
|
|
127
|
+
const ctx = buildCtx({
|
|
128
|
+
findingPolicy: "external",
|
|
129
|
+
subagentSpawner: { spawn, spawnMany: vi.fn() },
|
|
130
|
+
});
|
|
131
|
+
const result = await runSpawn(ctx);
|
|
132
|
+
expect(spawn).toHaveBeenCalledWith(
|
|
133
|
+
expect.objectContaining({
|
|
134
|
+
runtime: expect.objectContaining({ findingPolicy: "external" }),
|
|
135
|
+
}),
|
|
136
|
+
);
|
|
137
|
+
expect(result).toMatchObject({
|
|
138
|
+
findingsCount: 1,
|
|
139
|
+
reportedFindings: ["Cross-tenant read at GET /private"],
|
|
140
|
+
});
|
|
141
|
+
const description = spawnPentestAgent(ctx).description;
|
|
142
|
+
expect(description).not.toContain("document_vulnerability");
|
|
143
|
+
expect(description).not.toContain("ONE MORE TIME");
|
|
144
|
+
});
|
|
145
|
+
|
|
113
146
|
function makeEventBus(): {
|
|
114
147
|
bus: ToolContext["eventBus"];
|
|
115
148
|
emit: ReturnType<typeof vi.fn>;
|
|
@@ -121,6 +154,7 @@ function makeEventBus(): {
|
|
|
121
154
|
afterEach(() => {
|
|
122
155
|
constructorCalls.length = 0;
|
|
123
156
|
registryCalls.length = 0;
|
|
157
|
+
swarmMock.results = [];
|
|
124
158
|
vi.restoreAllMocks();
|
|
125
159
|
});
|
|
126
160
|
|
|
@@ -164,6 +198,42 @@ describe("spawn_pentest_agent — usage callbacks", () => {
|
|
|
164
198
|
});
|
|
165
199
|
|
|
166
200
|
describe("spawn tools — findings session attribution", () => {
|
|
201
|
+
it("returns each external swarm objective outcome to the parent", async () => {
|
|
202
|
+
swarmMock.results = [
|
|
203
|
+
{
|
|
204
|
+
findings: [],
|
|
205
|
+
reportedFindings: ["Cross-tenant read"],
|
|
206
|
+
objectiveResults: [
|
|
207
|
+
{
|
|
208
|
+
objective: "Extract the assigned flag",
|
|
209
|
+
completed: true,
|
|
210
|
+
result: "Extracted FLAG{example}",
|
|
211
|
+
},
|
|
212
|
+
],
|
|
213
|
+
},
|
|
214
|
+
];
|
|
215
|
+
const tool = spawnPentestSwarm(buildCtx({ findingPolicy: "external" }));
|
|
216
|
+
// biome-ignore lint/suspicious/noExplicitAny: ai-sdk tool execute receives an opaque ExecuteContext we don't need to narrow for this test.
|
|
217
|
+
const output = await (tool as any).execute(
|
|
218
|
+
{
|
|
219
|
+
targets: [{ target: "https://example.com/api", objectives: ["Test"] }],
|
|
220
|
+
toolCallDescription: "spawn swarm",
|
|
221
|
+
},
|
|
222
|
+
{ toolCallId: "tc_external", messages: [], abortSignal: undefined },
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
expect(output.results[0]).toMatchObject({
|
|
226
|
+
reportedFindings: ["Cross-tenant read"],
|
|
227
|
+
objectiveResults: [
|
|
228
|
+
{
|
|
229
|
+
objective: "Extract the assigned flag",
|
|
230
|
+
completed: true,
|
|
231
|
+
result: "Extracted FLAG{example}",
|
|
232
|
+
},
|
|
233
|
+
],
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
167
237
|
it("attributes the single-agent fallback registry to the root session", async () => {
|
|
168
238
|
await runSpawn(buildCtx());
|
|
169
239
|
|