@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.
Files changed (226) hide show
  1. package/build/{agent-yby11zaq.js → agent-68t2wzmc.js} +6 -6
  2. package/build/agent-jhekpern.js +13 -0
  3. package/build/{agent-ayreaj6z.js → agent-qvreg8a4.js} +5 -5
  4. package/build/{agent-runs-g37vkrjr.js → agent-runs-30dvbvmm.js} +18 -18
  5. package/build/agent-vybw255f.js +13 -0
  6. package/build/{apps-xn6km9yn.js → apps-na0y0992.js} +13 -13
  7. package/build/{auth-d105wc64.js → auth-dz2c5thx.js} +13 -13
  8. package/build/{blackboxAgent-s56sgx9b.js → blackboxAgent-2mwvaajb.js} +6 -6
  9. package/build/{blackboxPentest-ps78v3w7.js → blackboxPentest-s2mfze9p.js} +17 -12
  10. package/build/{cli-kb21qhvm.js → cli-1pmfnz1a.js} +4 -3
  11. package/build/{cli-ae8jqxqr.js → cli-563yntrb.js} +2 -2
  12. package/build/{cli-7eea4e2y.js → cli-5ab346kd.js} +6 -6
  13. package/build/{cli-1ve18npr.js → cli-72y76bpg.js} +3 -2
  14. package/build/{cli-fy6vh2a7.js → cli-98wmmphf.js} +1 -1
  15. package/build/{cli-c05y640n.js → cli-9jscnhtg.js} +1 -1
  16. package/build/{cli-3sp6nd8d.js → cli-9kk9tm58.js} +2 -2
  17. package/build/{cli-d37a42dc.js → cli-9pz9wjt2.js} +101 -18
  18. package/build/{cli-dpsw2fe6.js → cli-a6x4jm5a.js} +476 -193
  19. package/build/{cli-az3xq70v.js → cli-cfrdr25z.js} +1 -1
  20. package/build/{cli-ngq9dr8n.js → cli-fdrrbb9p.js} +3 -3
  21. package/build/{cli-k2nwxta0.js → cli-t09zj814.js} +2 -2
  22. package/build/{cli-qgz1q9g5.js → cli-t4q4t4dx.js} +223 -76
  23. package/build/{cli-xtc2t6me.js → cli-vdd1s3x9.js} +103 -39
  24. package/build/{cli-gx71jcke.js → cli-vn4cwac3.js} +1 -1
  25. package/build/{cli-65g6rcat.js → cli-w9nw2a4z.js} +1 -1
  26. package/build/{cli-n2en7j5b.js → cli-z7ysyrr4.js} +1 -1
  27. package/build/{cli-709rvekb.js → cli-zbbtr4sb.js} +4 -2
  28. package/build/cli.js +70 -40
  29. package/build/{config-zpqkd5cz.js → config-nkzm9arw.js} +1 -1
  30. package/build/{fastStrike-mcjdvwez.js → fastStrike-16jw1s61.js} +67 -15
  31. package/build/{fixes-kytx5dmj.js → fixes-6wxk22v5.js} +13 -13
  32. package/build/{index-4vqdgb42.js → index-36p56pcf.js} +1473 -1217
  33. package/build/{index-je6hcsqr.js → index-6nejymsc.js} +1 -1
  34. package/build/{index-fv64eb32.js → index-asm4y7e4.js} +2 -2
  35. package/build/{index-n65z6v7b.js → index-bfmn52za.js} +6 -6
  36. package/build/{index-qv4cc65p.js → index-bt4r13q7.js} +13 -13
  37. package/build/{index-dr3tg1w3.js → index-gfq1rp1q.js} +1 -1
  38. package/build/{index-wqetp6gq.js → index-j9pxgrzd.js} +12 -7
  39. package/build/{index-r9yjkfrv.js → index-n9kapn1x.js} +4 -4
  40. package/build/{index-ns4m4s01.js → index-tr6evhyg.js} +4 -4
  41. package/build/{issues-eb3q9d3a.js → issues-7rmf4ker.js} +13 -13
  42. package/build/{knowledge-fqsvtx0f.js → knowledge-s2sxh8nb.js} +13 -13
  43. package/build/lib/{chunk-1azdancq.js → chunk-0dnx79b1.js} +1 -1
  44. package/build/lib/{chunk-4hwzqthr.js → chunk-161ak2am.js} +3 -2
  45. package/build/lib/{chunk-z137x08b.js → chunk-1rqbhx4b.js} +7 -5
  46. package/build/lib/{chunk-1yct6cra.js → chunk-3hjvdf67.js} +1 -1
  47. package/build/lib/{chunk-r2dkp4tb.js → chunk-5j8pp0xf.js} +1 -1
  48. package/build/lib/{chunk-3c00n0cy.js → chunk-77se1d35.js} +8 -3
  49. package/build/lib/{chunk-yrnrbjes.js → chunk-7tft4b8b.js} +4 -2
  50. package/build/lib/{chunk-w66ce82a.js → chunk-8njjwwcw.js} +4 -2
  51. package/build/lib/{chunk-v3dk9j7k.js → chunk-aq9eyxas.js} +80 -21
  52. package/build/lib/{chunk-9tt2021m.js → chunk-d05cvnv8.js} +9 -9
  53. package/build/lib/{chunk-e8yy2k73.js → chunk-f3sq4896.js} +28 -2
  54. package/build/lib/{chunk-1fy8azee.js → chunk-fasqtjq1.js} +1 -1
  55. package/build/lib/{chunk-1jvwqnem.js → chunk-gm3b9j4y.js} +104 -40
  56. package/build/lib/{chunk-r4j8ybn9.js → chunk-hys7nemr.js} +87 -36
  57. package/build/lib/{chunk-7pwa92rn.js → chunk-j9668h16.js} +1 -1
  58. package/build/lib/{chunk-ba41jh41.js → chunk-kcw4cpr3.js} +418 -133
  59. package/build/lib/{chunk-z96pbjhd.js → chunk-m4swxy5r.js} +13 -6
  60. package/build/lib/{chunk-xhdpgpkm.js → chunk-mbsde9d0.js} +2 -2
  61. package/build/lib/{chunk-9yksqgzr.js → chunk-mpvjfvz0.js} +20 -13
  62. package/build/lib/{chunk-2qsq76xq.js → chunk-qkz5sm0d.js} +128 -34
  63. package/build/lib/{chunk-nhfax4gb.js → chunk-rfyjqybp.js} +1 -1
  64. package/build/lib/{chunk-mg03jdmt.js → chunk-vsnwzne5.js} +1 -1
  65. package/build/lib/{chunk-acgbyf5h.js → chunk-ydxhkgjw.js} +3 -2
  66. package/build/lib/{chunk-bb0ekhkw.js → chunk-z8kej0sp.js} +69 -18
  67. package/build/lib/core/agents/agentRuntime.d.ts +2 -1
  68. package/build/lib/core/agents/defineAgent.d.ts +3 -2
  69. package/build/lib/core/agents/offSecAgent/index.d.ts +3 -1
  70. package/build/lib/core/agents/offSecAgent/index.js +10 -8
  71. package/build/lib/core/agents/offSecAgent/prompt.d.ts +3 -1
  72. package/build/lib/core/agents/offSecAgent/subagentSpawner.d.ts +2 -1
  73. package/build/lib/core/agents/offSecAgent/subagentSpawner.js +5 -5
  74. package/build/lib/core/agents/offSecAgent/tools/index.js +4 -4
  75. package/build/lib/core/agents/offSecAgent/tools/listMemories.d.ts +11 -1
  76. package/build/lib/core/agents/offSecAgent/tools/reportError.d.ts +2 -1
  77. package/build/lib/core/agents/offSecAgent/tools/runPentestWorkflow.d.ts +20 -1
  78. package/build/lib/core/agents/offSecAgent/tools/spawnPentestAgent.d.ts +4 -7
  79. package/build/lib/core/agents/offSecAgent/tools/spawnPentestSwarm.d.ts +7 -1
  80. package/build/lib/core/agents/offSecAgent/tools/types.d.ts +2 -1
  81. package/build/lib/core/agents/offSecAgent/types.d.ts +7 -1
  82. package/build/lib/core/agents/specialized/attackSurface/blackboxAgent.js +9 -9
  83. package/build/lib/core/agents/specialized/attackSurface/schemas.js +9 -9
  84. package/build/lib/core/agents/specialized/authenticationAgent/index.js +10 -10
  85. package/build/lib/core/agents/specialized/codeAgent/agent.js +9 -9
  86. package/build/lib/core/agents/specialized/codeAgent/prompts.d.ts +1 -0
  87. package/build/lib/core/agents/specialized/findingJudge/agent.js +8 -8
  88. package/build/lib/core/agents/specialized/findingJudge/index.js +3 -3
  89. package/build/lib/core/agents/specialized/patching/index.js +10 -10
  90. package/build/lib/core/agents/specialized/pentest/agent.d.ts +18 -4
  91. package/build/lib/core/agents/specialized/pentest/agent.js +11 -9
  92. package/build/lib/core/agents/specialized/whiteboxAttackSurface/index.js +9 -9
  93. package/build/lib/core/ai/ai.d.ts +23 -6
  94. package/build/lib/core/ai/ai.js +2 -2
  95. package/build/lib/core/ai/index.js +2 -2
  96. package/build/lib/core/ai/models/claude-capabilities.d.ts +3 -2
  97. package/build/lib/core/ai/models/concentrate.d.ts +1 -0
  98. package/build/lib/core/ai/models/index.d.ts +1 -1
  99. package/build/lib/core/ai/providers/concentrate.d.ts +1 -0
  100. package/build/lib/core/ai/utils.d.ts +2 -0
  101. package/build/lib/core/ai/utils.js +4 -2
  102. package/build/lib/core/api/index.d.ts +3 -0
  103. package/build/lib/core/api/index.js +22 -19
  104. package/build/lib/core/api/targetedPentest.d.ts +1 -0
  105. package/build/lib/core/api/targetedPentest.js +10 -10
  106. package/build/lib/core/config/config.d.ts +3 -0
  107. package/build/lib/core/config/index.js +1 -1
  108. package/build/lib/core/findings/registry.js +3 -3
  109. package/build/lib/core/session/index.d.ts +12 -0
  110. package/build/lib/core/session/index.js +2 -2
  111. package/build/lib/core/session/persistence.d.ts +7 -0
  112. package/build/lib/core/session/persistence.js +7 -3
  113. package/build/lib/core/workflows/fastStrike.d.ts +3 -1
  114. package/build/lib/core/workflows/pentest.d.ts +8 -1
  115. package/build/lib/core/workflows/pentest.js +15 -15
  116. package/build/lib/core/workflows/seams.js +4 -4
  117. package/build/lib/core/workflows/whiteboxAttackSurface.js +11 -11
  118. package/build/lib/index.js +22 -22
  119. package/build/{logs-x62b7hk1.js → logs-70fyr848.js} +13 -13
  120. package/build/{native-rollout-evidence-c9yhfqcs.js → native-rollout-evidence-ntensvzb.js} +3 -3
  121. package/build/{offesecAgent-5whnpeta.js → offesecAgent-kk62tf60.js} +4 -4
  122. package/build/pentest-4jgznn7h.js +22 -0
  123. package/build/{pentests-e0f32aqx.js → pentests-3e41hj08.js} +13 -13
  124. package/build/recordedRunClient-w34kcch1.js +11 -0
  125. package/build/{targetedPentest-rcbs4nvp.js → targetedPentest-t9snxcj2.js} +18 -11
  126. package/build/{targets-xj9g3q0z.js → targets-1pyf21c7.js} +13 -13
  127. package/build/threatModel-48y5qjze.js +21 -0
  128. package/build/{uninstall-7s784q4a.js → uninstall-fbydd9t6.js} +1 -1
  129. package/build/{upload-4gvhwmvr.js → upload-sbhwe2fq.js} +4 -4
  130. package/build/{utils-cjrz64pc.js → utils-23903gcj.js} +3 -3
  131. package/package.json +1 -1
  132. package/src/cli.ts +48 -14
  133. package/src/core/agents/agentRuntime.test.ts +38 -0
  134. package/src/core/agents/agentRuntime.ts +5 -2
  135. package/src/core/agents/defineAgent.ts +3 -1
  136. package/src/core/agents/offSecAgent/index.ts +3 -0
  137. package/src/core/agents/offSecAgent/offensiveSecurityAgent.test.ts +92 -5
  138. package/src/core/agents/offSecAgent/offensiveSecurityAgent.ts +70 -31
  139. package/src/core/agents/offSecAgent/prompt.test.ts +46 -0
  140. package/src/core/agents/offSecAgent/prompt.ts +52 -8
  141. package/src/core/agents/offSecAgent/selectiveConstruction.publicPath.test.ts +38 -0
  142. package/src/core/agents/offSecAgent/selectiveConstruction.test.ts +64 -0
  143. package/src/core/agents/offSecAgent/subagentSpawner.ts +12 -1
  144. package/src/core/agents/offSecAgent/tools/delegateAuth.ts +1 -0
  145. package/src/core/agents/offSecAgent/tools/listMemories.test.ts +230 -0
  146. package/src/core/agents/offSecAgent/tools/listMemories.ts +73 -6
  147. package/src/core/agents/offSecAgent/tools/reportError.test.ts +6 -0
  148. package/src/core/agents/offSecAgent/tools/reportError.ts +10 -2
  149. package/src/core/agents/offSecAgent/tools/runAttackSurface.ts +2 -0
  150. package/src/core/agents/offSecAgent/tools/runPentestWorkflow.test.ts +57 -0
  151. package/src/core/agents/offSecAgent/tools/runPentestWorkflow.ts +19 -1
  152. package/src/core/agents/offSecAgent/tools/runWhiteboxScan.ts +5 -2
  153. package/src/core/agents/offSecAgent/tools/spawnCodingAgent.ts +1 -0
  154. package/src/core/agents/offSecAgent/tools/spawnPentestAgent.test.ts +72 -2
  155. package/src/core/agents/offSecAgent/tools/spawnPentestAgent.ts +32 -11
  156. package/src/core/agents/offSecAgent/tools/spawnPentestSwarm.ts +25 -5
  157. package/src/core/agents/offSecAgent/tools/threatModelGenerator.ts +1 -0
  158. package/src/core/agents/offSecAgent/tools/types.ts +6 -1
  159. package/src/core/agents/offSecAgent/tools/updateTask.ts +5 -1
  160. package/src/core/agents/offSecAgent/tools/whiteboxCandidates.ts +11 -4
  161. package/src/core/agents/offSecAgent/types.ts +9 -0
  162. package/src/core/agents/specialized/codeAgent/agent.test.ts +7 -0
  163. package/src/core/agents/specialized/codeAgent/agent.ts +9 -2
  164. package/src/core/agents/specialized/codeAgent/prompts.ts +6 -0
  165. package/src/core/agents/specialized/pentest/agent.test.ts +116 -0
  166. package/src/core/agents/specialized/pentest/agent.ts +178 -49
  167. package/src/core/agents/specialized/pentest/results.test.ts +59 -4
  168. package/src/core/agents/specialized/whiteboxAttackSurface/endpointDocumentationAgent.ts +1 -0
  169. package/src/core/ai/ai.operator-context.bench.test.ts +1408 -0
  170. package/src/core/ai/ai.recorded-context.test.ts +74 -0
  171. package/src/core/ai/ai.test.ts +54 -0
  172. package/src/core/ai/ai.ts +312 -82
  173. package/src/core/ai/compactionTelemetry.test.ts +2 -2
  174. package/src/core/ai/models/anthropic.ts +6 -0
  175. package/src/core/ai/models/bedrock.ts +18 -0
  176. package/src/core/ai/models/claude-capabilities.ts +29 -8
  177. package/src/core/ai/models/concentrate.ts +52 -0
  178. package/src/core/ai/models/index.ts +25 -4
  179. package/src/core/ai/models/models.test.ts +154 -0
  180. package/src/core/ai/models/openrouter.ts +43 -1
  181. package/src/core/ai/providers/anthropic-models.test.ts +60 -1
  182. package/src/core/ai/providers/bedrock-models.test.ts +81 -0
  183. package/src/core/ai/providers/concentrate.test.ts +31 -0
  184. package/src/core/ai/providers/concentrate.ts +11 -0
  185. package/src/core/ai/providers/gateway-models.test.ts +16 -6
  186. package/src/core/ai/utils.native-rollout-evidence.test.ts +1 -0
  187. package/src/core/ai/utils.ts +189 -143
  188. package/src/core/api/blackboxPentest.ts +9 -1
  189. package/src/core/api/index.ts +9 -0
  190. package/src/core/api/targetedPentest.ts +19 -5
  191. package/src/core/config/config.test.ts +35 -0
  192. package/src/core/config/config.ts +5 -0
  193. package/src/core/observability/telemetry.test.ts +2 -3
  194. package/src/core/session/index.ts +5 -0
  195. package/src/core/session/persistence.test.ts +62 -0
  196. package/src/core/session/persistence.ts +56 -0
  197. package/src/core/workflows/fastStrike.seams.test.ts +149 -0
  198. package/src/core/workflows/fastStrike.test.ts +18 -1
  199. package/src/core/workflows/fastStrike.ts +103 -11
  200. package/src/core/workflows/pentest.test.ts +104 -0
  201. package/src/core/workflows/pentest.ts +110 -7
  202. package/src/core/workflows/seams.test.ts +13 -0
  203. package/src/core/workflows/seams.ts +5 -1
  204. package/src/core/workflows/whiteboxAttackSurface.ts +1 -0
  205. package/src/tui/components/chat/loading-indicator.tsx +20 -4
  206. package/src/tui/components/chat/message-list.tsx +1 -0
  207. package/src/tui/components/commands/__tests__/advanced-settings-lifecycle.tsx +176 -0
  208. package/src/tui/components/commands/advanced-settings.test.ts +28 -0
  209. package/src/tui/components/commands/advanced-settings.tsx +237 -19
  210. package/src/tui/components/operator-dashboard/index.tsx +46 -9
  211. package/src/tui/components/operator-dashboard/logic.test.ts +106 -0
  212. package/src/tui/components/operator-dashboard/logic.ts +42 -2
  213. package/src/tui/components/operator-dashboard/run-events.test.ts +3 -1
  214. package/src/tui/components/shared/__tests__/tool-renderer-lifecycle.tsx +18 -0
  215. package/src/tui/components/shared/action-label.test.ts +9 -0
  216. package/src/tui/components/shared/compacting-icon.tsx +18 -0
  217. package/src/tui/components/shared/index.ts +1 -0
  218. package/src/tui/components/shared/tool-registry.ts +1 -0
  219. package/src/tui/components/shared/tool-renderer.tsx +21 -7
  220. package/src/tui/context/route.tsx +1 -0
  221. package/src/tui/keybindings/registry.ts +2 -0
  222. package/build/agent-6w7egcf1.js +0 -13
  223. package/build/agent-9zx75feh.js +0 -13
  224. package/build/pentest-e61hc38s.js +0 -22
  225. package/build/recordedRunClient-dp032c32.js +0 -11
  226. package/build/threatModel-jkxfa40s.js +0 -21
@@ -573,3 +573,107 @@ describe("runPentestWorkflow — threads seams into the whitebox discovery phase
573
573
  expect(mocks.whiteboxCalls[0]?.seams).toBe(seams);
574
574
  });
575
575
  });
576
+
577
+ describe("external pentest workflow", () => {
578
+ it("returns worker findings without native report generation and retains them on resume", async () => {
579
+ const emit = vi.fn();
580
+ mocks.consumeImpl = async () => ({
581
+ findings: [],
582
+ reportedFindings: ["GET /api reveals another tenant's data"],
583
+ objectiveResults: [
584
+ {
585
+ objective: "Test the target",
586
+ completed: true,
587
+ result: "Extracted FLAG{example} through the confirmed weakness",
588
+ },
589
+ ],
590
+ findingsPath: session.findingsPath,
591
+ pocsPath: session.pocsPath,
592
+ });
593
+ const input: PentestWorkflowInput = {
594
+ target: "https://example.com",
595
+ model: {} as PentestWorkflowInput["model"],
596
+ session,
597
+ findingPolicy: "external",
598
+ eventBus: { emit } as unknown as NonNullable<
599
+ PentestWorkflowInput["eventBus"]
600
+ >,
601
+ hooks: { findingPolicy: undefined },
602
+ };
603
+
604
+ const first = await runPentestWorkflow(input);
605
+ expect(first).toMatchObject({
606
+ findings: [],
607
+ reportedFindings: ["GET /api reveals another tenant's data"],
608
+ objectiveResults: [
609
+ {
610
+ objective: "Test the target",
611
+ completed: true,
612
+ result: "Extracted FLAG{example} through the confirmed weakness",
613
+ },
614
+ ],
615
+ reportPath: null,
616
+ });
617
+ expect(mocks.targetedInputs).toHaveLength(1);
618
+ expect(mocks.targetedInputs[0]?.findingPolicy).toBe("external");
619
+ expect(emit).toHaveBeenCalledWith("workflow-phase-complete", {
620
+ phase: "pentesting",
621
+ summary: { findingsCount: 1 },
622
+ });
623
+
624
+ mocks.consumeImpl = async () => {
625
+ throw new Error("completed worker was rerun");
626
+ };
627
+ const resumed = await runPentestWorkflow(input);
628
+ expect(resumed.reportedFindings).toEqual(first.reportedFindings);
629
+ expect(resumed.objectiveResults).toEqual(first.objectiveResults);
630
+ expect(mocks.targetedInputs).toHaveLength(1);
631
+ });
632
+
633
+ it("does not generate a native report when discovery has no targets", async () => {
634
+ mocks.loadAttackSurfaceResults.mockReturnValue({ targets: [] });
635
+ const result = await runPentestWorkflow({
636
+ target: "https://example.com",
637
+ model: {} as PentestWorkflowInput["model"],
638
+ session,
639
+ findingPolicy: "external",
640
+ });
641
+ expect(result).toMatchObject({
642
+ findings: [],
643
+ reportedFindings: [],
644
+ objectiveResults: [],
645
+ reportPath: null,
646
+ });
647
+ });
648
+
649
+ it("rejects an external worker result without objective outcomes", async () => {
650
+ mocks.consumeImpl = async () => ({
651
+ findings: [],
652
+ reportedFindings: ["A finding"],
653
+ findingsPath: session.findingsPath,
654
+ pocsPath: session.pocsPath,
655
+ });
656
+ await expect(
657
+ runPentestWorkflow({
658
+ target: "https://example.com",
659
+ model: {} as PentestWorkflowInput["model"],
660
+ session,
661
+ findingPolicy: "external",
662
+ }),
663
+ ).rejects.toThrow("incomplete swarm target");
664
+ });
665
+
666
+ it("fails rather than treating an incomplete external worker as no finding", async () => {
667
+ mocks.consumeImpl = async () => {
668
+ throw new Error("worker failed");
669
+ };
670
+ await expect(
671
+ runPentestWorkflow({
672
+ target: "https://example.com",
673
+ model: {} as PentestWorkflowInput["model"],
674
+ session,
675
+ findingPolicy: "external",
676
+ }),
677
+ ).rejects.toThrow("incomplete swarm target");
678
+ });
679
+ });
@@ -7,7 +7,7 @@ import type {
7
7
  ToolSet,
8
8
  } from "ai";
9
9
  import { hasToolCall } from "ai";
10
- import type { AgentHooks, Finding } from "../agents/offSecAgent";
10
+ import type { AgentHooks, Finding, FindingPolicy } from "../agents/offSecAgent";
11
11
  import {
12
12
  OffensiveSecurityAgent,
13
13
  PLAN_MODE_TOOL_NAMES,
@@ -64,6 +64,8 @@ import {
64
64
  finalizeManifest,
65
65
  getCompletedAgentIds,
66
66
  loadSubagentMessages,
67
+ loadSubagentObjectiveResults,
68
+ loadSubagentReportedFindings,
67
69
  readAgentManifest,
68
70
  reconcileManifestOnResume,
69
71
  type SwarmTarget,
@@ -99,6 +101,7 @@ export const DEFAULT_CONCURRENCY = 10;
99
101
  // ---------------------------------------------------------------------------
100
102
 
101
103
  export interface PentestWorkflowInput {
104
+ findingPolicy?: FindingPolicy;
102
105
  /** Live target URL — always required */
103
106
  target: string;
104
107
 
@@ -170,6 +173,11 @@ export interface PentestWorkflowInput {
170
173
 
171
174
  export interface PentestWorkflowResult {
172
175
  findings: Finding[];
176
+ reportedFindings?: string[];
177
+ objectiveResults?: PentestResult["objectiveResults"];
178
+ submissionJson?: string;
179
+ solved?: boolean;
180
+ summary?: string;
173
181
  findingsPath: string;
174
182
  pocsPath: string;
175
183
  reportPath: string | null;
@@ -178,6 +186,7 @@ export interface PentestWorkflowResult {
178
186
  export type { SwarmTarget } from "../session/persistence";
179
187
 
180
188
  export interface PentestSwarmInput {
189
+ findingPolicy?: FindingPolicy;
181
190
  targets: SwarmTarget[];
182
191
  model: AIModel;
183
192
  session: SessionInfo;
@@ -290,7 +299,23 @@ export async function runPentestSwarmTarget(
290
299
  const subagentId = entry.id;
291
300
  const subagentName = target.name ?? entry.name;
292
301
 
293
- if (completedIds.has(subagentId)) return null;
302
+ if (completedIds.has(subagentId)) {
303
+ if (sharedHooks.findingPolicy !== "external") return null;
304
+ const reportedFindings = loadSubagentReportedFindings(session, subagentId);
305
+ const objectiveResults = loadSubagentObjectiveResults(session, subagentId);
306
+ if (!reportedFindings || !objectiveResults) {
307
+ throw new Error(
308
+ `Cannot resume externally graded pentest: findings or objective results for completed agent ${subagentId} are unavailable`,
309
+ );
310
+ }
311
+ return {
312
+ findings: [],
313
+ reportedFindings,
314
+ objectiveResults,
315
+ findingsPath: session.findingsPath,
316
+ pocsPath: session.pocsPath,
317
+ };
318
+ }
294
319
 
295
320
  // Per-target AgentHooks: seams.hooksForItem can give this one target
296
321
  // (e.g. its own lease/backends, its own politeness budget) different
@@ -418,15 +443,37 @@ export async function runPentestSwarmTarget(
418
443
  });
419
444
 
420
445
  const result = await agent.consume();
446
+ if (
447
+ itemHooks.findingPolicy === "external" &&
448
+ (!Array.isArray(result.reportedFindings) ||
449
+ !Array.isArray(result.objectiveResults))
450
+ ) {
451
+ throw new Error(
452
+ `Externally graded pentest worker ${subagentId} ended without structured findings and objective results`,
453
+ );
454
+ }
421
455
 
422
456
  saveSubagentData(session, {
423
457
  agentName: subagentId,
424
458
  target: target.target,
425
459
  objective: objectiveStr,
426
460
  status: "completed",
427
- findingsCount: result.findings.length,
461
+ findingsCount: result.reportedFindings?.length ?? result.findings.length,
462
+ ...(itemHooks.findingPolicy === "external"
463
+ ? {
464
+ reportedFindings: result.reportedFindings,
465
+ objectiveResults: result.objectiveResults,
466
+ }
467
+ : {}),
428
468
  messages: [...previousMessages, ...lastMessages],
429
- systemPrompt: buildPentestSystemPrompt(session),
469
+ systemPrompt: buildPentestSystemPrompt(
470
+ session,
471
+ "orchestrator",
472
+ "default",
473
+ undefined,
474
+ undefined,
475
+ itemHooks.findingPolicy,
476
+ ),
430
477
  userPrompt: agent.userPrompt,
431
478
  });
432
479
  updateManifestEntryStatus(session, subagentId, "completed");
@@ -450,7 +497,14 @@ export async function runPentestSwarmTarget(
450
497
  status: "failed",
451
498
  error: error instanceof Error ? error.message : String(error),
452
499
  messages: [...previousMessages, ...lastMessages],
453
- systemPrompt: buildPentestSystemPrompt(session),
500
+ systemPrompt: buildPentestSystemPrompt(
501
+ session,
502
+ "orchestrator",
503
+ "default",
504
+ undefined,
505
+ undefined,
506
+ itemHooks.findingPolicy,
507
+ ),
454
508
  userPrompt: agent?.userPrompt ?? "",
455
509
  });
456
510
  updateManifestEntryStatus(session, subagentId, "failed");
@@ -522,6 +576,7 @@ export async function runPentestSwarm(
522
576
  usageRecorder,
523
577
  streamIdFactory,
524
578
  ...hooks,
579
+ findingPolicy: hooks?.findingPolicy ?? input.findingPolicy,
525
580
  };
526
581
 
527
582
  // Ops-typed by the seam; TargetedPentestAgent/OffensiveSecurityAgent only
@@ -574,6 +629,15 @@ export async function runPentestSwarm(
574
629
 
575
630
  finalizeManifest(session, manifestEntries, results);
576
631
 
632
+ if (
633
+ sharedHooks.findingPolicy === "external" &&
634
+ results.some((result) => result === null)
635
+ ) {
636
+ throw new Error(
637
+ "Externally graded pentest has an incomplete swarm target; findings cannot be aggregated",
638
+ );
639
+ }
640
+
577
641
  return results;
578
642
  }
579
643
 
@@ -613,6 +677,7 @@ export async function runPentestWorkflow(
613
677
  threatModel,
614
678
  surfaceIntegrationEnabled,
615
679
  } = input;
680
+ const findingPolicy = input.hooks?.findingPolicy ?? input.findingPolicy;
616
681
 
617
682
  // Default registries mirror today's `FindingsRegistry.fromDirectory` resume
618
683
  // behavior (dedup against on-disk findings, model-backed semantic dedup).
@@ -706,6 +771,7 @@ export async function runPentestWorkflow(
706
771
  usageRecorder: input.usageRecorder,
707
772
  streamIdFactory: input.streamIdFactory,
708
773
  ...input.hooks,
774
+ findingPolicy,
709
775
  },
710
776
  seams,
711
777
  });
@@ -726,6 +792,7 @@ export async function runPentestWorkflow(
726
792
  usageRecorder: input.usageRecorder,
727
793
  streamIdFactory: input.streamIdFactory,
728
794
  ...input.hooks,
795
+ findingPolicy,
729
796
  },
730
797
  });
731
798
  }
@@ -744,6 +811,16 @@ export async function runPentestWorkflow(
744
811
  }
745
812
 
746
813
  if (swarmTargets.length === 0) {
814
+ if (findingPolicy === "external") {
815
+ return {
816
+ findings: [],
817
+ reportedFindings: [],
818
+ objectiveResults: [],
819
+ findingsPath: session.findingsPath,
820
+ pocsPath: session.pocsPath,
821
+ reportPath: null,
822
+ };
823
+ }
747
824
  const report = buildPentestReport([], {
748
825
  target,
749
826
  model,
@@ -778,10 +855,15 @@ export async function runPentestWorkflow(
778
855
  const findingsRegistry = seams.registries.findings() as FindingsRegistry;
779
856
 
780
857
  const completedCount = getCompletedAgentIds(session).size;
858
+ let externalReportedFindings: string[] = [];
859
+ let externalObjectiveResults: NonNullable<
860
+ PentestResult["objectiveResults"]
861
+ > = [];
781
862
 
782
- if (completedCount < swarmTargets.length) {
783
- await runPentestSwarm({
863
+ if (findingPolicy === "external" || completedCount < swarmTargets.length) {
864
+ const swarmResults = await runPentestSwarm({
784
865
  targets: swarmTargets,
866
+ findingPolicy,
785
867
  model,
786
868
  session,
787
869
  authConfig,
@@ -800,12 +882,33 @@ export async function runPentestWorkflow(
800
882
  hooks: input.hooks,
801
883
  seams,
802
884
  });
885
+ externalReportedFindings = swarmResults.flatMap(
886
+ (result) => result?.reportedFindings ?? [],
887
+ );
888
+ externalObjectiveResults = swarmResults.flatMap(
889
+ (result) => result?.objectiveResults ?? [],
890
+ );
803
891
  }
804
892
 
805
893
  if (abortSignal?.aborted) {
806
894
  throw new DOMException("Pentest aborted by user", "AbortError");
807
895
  }
808
896
 
897
+ if (findingPolicy === "external") {
898
+ eventBus?.emit("workflow-phase-complete", {
899
+ phase: "pentesting",
900
+ summary: { findingsCount: externalReportedFindings.length },
901
+ });
902
+ return {
903
+ findings: [],
904
+ reportedFindings: externalReportedFindings,
905
+ objectiveResults: externalObjectiveResults,
906
+ findingsPath: session.findingsPath,
907
+ pocsPath: session.pocsPath,
908
+ reportPath: null,
909
+ };
910
+ }
911
+
809
912
  // =========================================================================
810
913
  // Phase 3: Aggregate results
811
914
  // =========================================================================
@@ -312,6 +312,19 @@ describe("resolveItemHooks recorder scope", () => {
312
312
  });
313
313
  });
314
314
 
315
+ it("does not let a per-item hook re-enable native finding judging under an external run", () => {
316
+ const seams = inProcessSeams({
317
+ hooksForItem: () => ({ findingPolicy: "native" }),
318
+ });
319
+ const child = resolveItemHooks(
320
+ { findingPolicy: "external" },
321
+ seams,
322
+ "target",
323
+ 0,
324
+ );
325
+ expect(child.findingPolicy).toBe("external");
326
+ });
327
+
315
328
  it("requires a child inference binding instead of inheriting the parent's journal", () => {
316
329
  const recorder: InferenceRecorder = {
317
330
  runId: "parent",
@@ -266,11 +266,15 @@ export function resolveItemHooks<TItem>(
266
266
  item: TItem,
267
267
  index: number,
268
268
  ): AgentHooks {
269
+ const itemHooks = seams.hooksForItem?.(item, index);
269
270
  return {
270
271
  ...hooks,
271
272
  contextRecorder: undefined,
272
273
  inferenceRecorder: undefined,
273
274
  toolExecutionRecorder: undefined,
274
- ...seams.hooksForItem?.(item, index),
275
+ ...itemHooks,
276
+ ...(hooks?.findingPolicy === "external"
277
+ ? { findingPolicy: "external" as const }
278
+ : {}),
275
279
  };
276
280
  }
@@ -298,6 +298,7 @@ export async function runWhiteboxAttackSurfaceApp(
298
298
  attackSurfaceRegistry,
299
299
  },
300
300
  runtime: {
301
+ findingPolicy: hooksForApp.findingPolicy,
301
302
  model,
302
303
  session,
303
304
  authConfig,
@@ -7,6 +7,7 @@
7
7
  import { useEffect, useRef, useState } from "react";
8
8
  import { INTERNAL_ID_PATTERN } from "../../../core/operator";
9
9
  import { useTheme } from "../../theme";
10
+ import { CompactingIcon } from "../shared";
10
11
 
11
12
  // Braille spinner frames for smooth animation
12
13
  const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
@@ -16,7 +17,12 @@ const SPINNER_INTERVAL = 80;
16
17
  const DOTS_FRAMES = ["", ".", "..", "..."];
17
18
  const DOTS_INTERVAL = 400;
18
19
 
19
- export type LoadingState = "thinking" | "executing" | "streaming" | "waiting";
20
+ export type LoadingState =
21
+ | "thinking"
22
+ | "compacting"
23
+ | "executing"
24
+ | "streaming"
25
+ | "waiting";
20
26
 
21
27
  interface LoadingIndicatorProps {
22
28
  /** Current loading state */
@@ -42,19 +48,21 @@ export function LoadingIndicator({
42
48
 
43
49
  // Spinner animation
44
50
  useEffect(() => {
51
+ if (state === "compacting") return;
45
52
  const interval = setInterval(() => {
46
53
  setSpinnerFrame((f) => (f + 1) % SPINNER_FRAMES.length);
47
54
  }, SPINNER_INTERVAL);
48
55
  return () => clearInterval(interval);
49
- }, []);
56
+ }, [state]);
50
57
 
51
58
  // Dots animation
52
59
  useEffect(() => {
60
+ if (state === "compacting") return;
53
61
  const interval = setInterval(() => {
54
62
  setDotsFrame((f) => (f + 1) % DOTS_FRAMES.length);
55
63
  }, DOTS_INTERVAL);
56
64
  return () => clearInterval(interval);
57
- }, []);
65
+ }, [state]);
58
66
 
59
67
  const spinner = SPINNER_FRAMES[spinnerFrame];
60
68
  const dots = DOTS_FRAMES[dotsFrame];
@@ -64,6 +72,8 @@ export function LoadingIndicator({
64
72
  switch (state) {
65
73
  case "thinking":
66
74
  return `Thinking${dots}`;
75
+ case "compacting":
76
+ return "Compacting";
67
77
  case "streaming":
68
78
  return `Responding${dots}`;
69
79
  case "waiting":
@@ -95,6 +105,8 @@ export function LoadingIndicator({
95
105
  switch (state) {
96
106
  case "thinking":
97
107
  return colors.warning;
108
+ case "compacting":
109
+ return colors.primary;
98
110
  case "streaming":
99
111
  return colors.primary;
100
112
  case "waiting":
@@ -108,7 +120,11 @@ export function LoadingIndicator({
108
120
 
109
121
  return (
110
122
  <box flexDirection="row" marginTop={1} marginLeft={2} gap={1}>
111
- <text fg={getColor()}>{spinner}</text>
123
+ {state === "compacting" ? (
124
+ <CompactingIcon fg={getColor()} />
125
+ ) : (
126
+ <text fg={getColor()}>{spinner}</text>
127
+ )}
112
128
  <text fg={colors.textMuted}>{getMessage()}</text>
113
129
  </box>
114
130
  );
@@ -39,6 +39,7 @@ function getLoadingState(
39
39
  pendingToolName: string | null,
40
40
  ): LoadingState {
41
41
  if (hasPendingTool) {
42
+ if (pendingToolName === "summarize_conversation") return "compacting";
42
43
  if (pendingToolName && SUBAGENT_TOOLS.has(pendingToolName)) {
43
44
  return "waiting";
44
45
  }
@@ -0,0 +1,176 @@
1
+ import { mock } from "bun:test";
2
+ import assert from "node:assert/strict";
3
+ import { join } from "node:path";
4
+ import { testRender } from "@opentui/react/test-utils";
5
+ import { act } from "react";
6
+ import type { Config } from "../../../../core/config/config";
7
+
8
+ const scenario = process.argv[2];
9
+ assert.ok(
10
+ scenario === "native" || scenario === "external" || scenario === "compact",
11
+ );
12
+ const compact = scenario === "compact";
13
+
14
+ const saved: Partial<Config>[] = [];
15
+ let closes = 0;
16
+ mock.module(join(import.meta.dirname, "../../../context/config.tsx"), () => ({
17
+ useConfig: () => ({
18
+ data: { strikeMode: false, findingPolicy: "native" },
19
+ update: async (patch: Partial<Config>) => {
20
+ saved.push(patch);
21
+ },
22
+ }),
23
+ }));
24
+
25
+ const { default: AdvancedSettings } = await import("../advanced-settings.tsx");
26
+ const { TerminalDimensionsProvider } = await import(
27
+ "../../../context/dimensions"
28
+ );
29
+ const { registerBuiltinThemes, ThemeProvider } = await import("../../../theme");
30
+ registerBuiltinThemes();
31
+
32
+ const setup = await testRender(
33
+ <ThemeProvider>
34
+ <TerminalDimensionsProvider>
35
+ <AdvancedSettings onClose={() => (closes += 1)} />
36
+ </TerminalDimensionsProvider>
37
+ </ThemeProvider>,
38
+ {
39
+ width: compact ? 60 : 80,
40
+ height: compact ? 18 : 24,
41
+ exitOnCtrlC: false,
42
+ },
43
+ );
44
+
45
+ const settle = async () => {
46
+ for (let i = 0; i < 3; i++) await act(async () => {});
47
+ await act(setup.renderOnce);
48
+ };
49
+ const frame = async () => {
50
+ await settle();
51
+ return setup.captureCharFrame();
52
+ };
53
+ const press = async (key: string) => {
54
+ await act(async () => setup.mockInput.pressKey(key));
55
+ await settle();
56
+ };
57
+
58
+ try {
59
+ const initialFrame = await frame();
60
+ assert.ok(initialFrame.includes("Strike Mode"), initialFrame);
61
+ assert.ok(initialFrame.includes("❯ Strike Mode"), initialFrame);
62
+ assert.ok(initialFrame.includes("Save settings"), initialFrame);
63
+ assert.ok(!initialFrame.includes("Finding judge"), initialFrame);
64
+ assert.ok(!initialFrame.includes("Judge off"), initialFrame);
65
+ assert.deepEqual(saved, []);
66
+
67
+ await press("TAB");
68
+ const strikeOnFrame = await frame();
69
+ assert.ok(strikeOnFrame.includes("● Enabled"), strikeOnFrame);
70
+ assert.ok(strikeOnFrame.includes("❯ Strike Mode"), strikeOnFrame);
71
+ assert.ok(strikeOnFrame.includes("Finding judge"), strikeOnFrame);
72
+ assert.ok(strikeOnFrame.includes("Apex judge"), strikeOnFrame);
73
+ assert.ok(!strikeOnFrame.includes("[J]"), strikeOnFrame);
74
+ if (compact) {
75
+ assert.ok(strikeOnFrame.includes("Save settings"), strikeOnFrame);
76
+ }
77
+
78
+ await press("j");
79
+ assert.ok(!(await frame()).includes("Off (report findings only)"));
80
+
81
+ await press("ARROW_DOWN");
82
+ const judgeFocusedFrame = await frame();
83
+ assert.ok(judgeFocusedFrame.includes("❯ Finding judge"), judgeFocusedFrame);
84
+ await press("TAB");
85
+ const judgeOffFrame = await frame();
86
+ assert.ok(judgeOffFrame.includes("❯ Finding judge"), judgeOffFrame);
87
+ assert.ok(judgeOffFrame.includes("● Enabled"), judgeOffFrame);
88
+ assert.ok(judgeOffFrame.includes("Disabled ▾"), judgeOffFrame);
89
+ assert.ok(
90
+ !judgeOffFrame.includes("Off (report findings only)"),
91
+ judgeOffFrame,
92
+ );
93
+ await press("TAB");
94
+ const judgeOnFrame = await frame();
95
+ assert.ok(judgeOnFrame.includes("❯ Finding judge"), judgeOnFrame);
96
+ assert.ok(judgeOnFrame.includes("● Enabled"), judgeOnFrame);
97
+ assert.ok(judgeOnFrame.includes("Apex judge ▾"), judgeOnFrame);
98
+ assert.deepEqual(saved, []);
99
+
100
+ await press("RETURN");
101
+ const findingSelectFrame = await frame();
102
+ if (compact) {
103
+ assert.ok(
104
+ findingSelectFrame.includes("Apex judge (default)"),
105
+ findingSelectFrame,
106
+ );
107
+ assert.ok(findingSelectFrame.includes("Save settings"), findingSelectFrame);
108
+ }
109
+ assert.ok(
110
+ findingSelectFrame.includes("Off (report findings only)"),
111
+ findingSelectFrame,
112
+ );
113
+ await press("ARROW_DOWN");
114
+ const offFocusedFrame = await frame();
115
+ assert.ok(
116
+ offFocusedFrame.includes("❯ Off (report findings only)"),
117
+ offFocusedFrame,
118
+ );
119
+ await press("RETURN");
120
+ assert.ok((await frame()).includes("Disabled ▾"));
121
+ assert.deepEqual(saved, []);
122
+
123
+ if (scenario === "native") {
124
+ await press("ARROW_UP");
125
+ const strikeFocusedFrame = await frame();
126
+ assert.ok(strikeFocusedFrame.includes("❯ Strike Mode"), strikeFocusedFrame);
127
+ assert.ok(strikeFocusedFrame.includes("Disabled ▾"), strikeFocusedFrame);
128
+ await press("TAB");
129
+ const strikeOffFrame = await frame();
130
+ assert.ok(strikeOffFrame.includes("○ Disabled"));
131
+ assert.ok(!strikeOffFrame.includes("Finding judge"), strikeOffFrame);
132
+ assert.ok(!strikeOffFrame.includes("Judge off"), strikeOffFrame);
133
+ assert.ok(!strikeOffFrame.includes("Disabled ▾"));
134
+ assert.ok(strikeOffFrame.includes("❯ Strike Mode"), strikeOffFrame);
135
+ }
136
+
137
+ await press("ARROW_DOWN");
138
+ const saveFocusedFrame = await frame();
139
+ assert.ok(saveFocusedFrame.includes("❯ Save settings"), saveFocusedFrame);
140
+ await press("TAB");
141
+ const afterSaveTabFrame = await frame();
142
+ assert.ok(afterSaveTabFrame.includes("❯ Save settings"), afterSaveTabFrame);
143
+ assert.ok(
144
+ afterSaveTabFrame.includes(
145
+ scenario === "native" ? "○ Disabled" : "● Enabled",
146
+ ),
147
+ afterSaveTabFrame,
148
+ );
149
+ assert.ok(
150
+ scenario === "native"
151
+ ? !afterSaveTabFrame.includes("Finding judge")
152
+ : afterSaveTabFrame.includes("Disabled ▾"),
153
+ afterSaveTabFrame,
154
+ );
155
+ assert.deepEqual(saved, []);
156
+ assert.equal(closes, 0);
157
+ await press("ARROW_UP");
158
+ const previousFocusedFrame = await frame();
159
+ assert.ok(
160
+ previousFocusedFrame.includes(
161
+ scenario === "native" ? "❯ Strike Mode" : "❯ Finding judge",
162
+ ),
163
+ previousFocusedFrame,
164
+ );
165
+ await press("ARROW_DOWN");
166
+ assert.ok((await frame()).includes("❯ Save settings"));
167
+ await press("RETURN");
168
+ assert.deepEqual(saved, [
169
+ scenario === "native"
170
+ ? { strikeMode: false, findingPolicy: "native" }
171
+ : { strikeMode: true, findingPolicy: "external" },
172
+ ]);
173
+ assert.equal(closes, 1);
174
+ } finally {
175
+ await act(async () => setup.renderer.destroy());
176
+ }
@@ -0,0 +1,28 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { join } from "node:path";
3
+ import { expect, it } from "vitest";
4
+
5
+ for (const scenario of [
6
+ {
7
+ name: "Tab toggles the focused Judge and resets it when Strike turns off",
8
+ mode: "native",
9
+ },
10
+ {
11
+ name: "Tab on Save does not change Strike or Judge",
12
+ mode: "external",
13
+ },
14
+ { name: "keeps judge choices and Save visible at 60x18", mode: "compact" },
15
+ ] as const) {
16
+ it(scenario.name, () => {
17
+ const result = spawnSync(
18
+ "bun",
19
+ [
20
+ join(import.meta.dirname, "__tests__/advanced-settings-lifecycle.tsx"),
21
+ scenario.mode,
22
+ ],
23
+ { encoding: "utf8", timeout: 30_000 },
24
+ );
25
+ expect(result.error).toBeUndefined();
26
+ expect(result.status, result.stderr).toBe(0);
27
+ });
28
+ }