@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
@@ -684,10 +684,9 @@ describe("auxiliary model lifecycle", () => {
684
684
 
685
685
  const synthetic = events.find((e) => e.response.id === "summarization");
686
686
  expect(synthetic).toBeDefined();
687
- // Handler-safe: handlers spread response.messages; cache and provider
688
- // metadata survive the synthetic event.
687
+ // Handler-safe: callbacks can spread response.messages without replaying history.
689
688
  expect(Array.isArray(synthetic?.response.messages)).toBe(true);
690
- expect(synthetic?.response.messages).toEqual(history);
689
+ expect(synthetic?.response.messages).toEqual([]);
691
690
  expect(synthetic?.usage.inputTokenDetails?.cacheReadTokens).toBe(400);
692
691
  expect(synthetic?.usage.inputTokenDetails?.cacheWriteTokens).toBe(7);
693
692
  expect(synthetic?.providerMetadata).toEqual({
@@ -96,6 +96,7 @@ const OperatorSettingsObject = z.object({
96
96
  enableSuggestions: z.boolean().default(true),
97
97
  /** Run the top-level operator as a focused solo researcher without orchestration tools. */
98
98
  strikeMode: z.boolean().optional(),
99
+ findingPolicy: z.enum(["native", "external"]).optional(),
99
100
  });
100
101
 
101
102
  export type OperatorSettings = z.infer<typeof OperatorSettingsObject>;
@@ -989,6 +990,7 @@ export async function updateOperatorSettings(
989
990
  requireApproval: true,
990
991
  enableSuggestions: true,
991
992
  strikeMode: false,
993
+ findingPolicy: "native",
992
994
  };
993
995
  }
994
996
 
@@ -1007,6 +1009,9 @@ export async function updateOperatorSettings(
1007
1009
  if (settings.strikeMode !== undefined) {
1008
1010
  session.config.operatorSettings.strikeMode = settings.strikeMode;
1009
1011
  }
1012
+ if (settings.findingPolicy !== undefined) {
1013
+ session.config.operatorSettings.findingPolicy = settings.findingPolicy;
1014
+ }
1010
1015
  });
1011
1016
  }
1012
1017
 
@@ -18,6 +18,7 @@ import {
18
18
  convertModelMessagesToUI,
19
19
  finalizeManifest,
20
20
  getCompletedAgentIds,
21
+ loadSubagentObjectiveResults,
21
22
  loadSubagents,
22
23
  readAgentManifest,
23
24
  reconcileManifestOnResume,
@@ -239,6 +240,67 @@ describe("saveSubagentData + loadSubagents roundtrip", () => {
239
240
  });
240
241
  });
241
242
 
243
+ describe("loadSubagentObjectiveResults", () => {
244
+ it("roundtrips completed objective coverage, including an empty result", () => {
245
+ const session = makeSession();
246
+ const objectiveResults = [
247
+ { objective: "Test authorization", completed: true, result: "No bypass" },
248
+ { objective: "Test injection", completed: false },
249
+ ];
250
+ saveSubagentData(session, {
251
+ agentName: "pentest-agent-1",
252
+ target: "http://localhost:8080",
253
+ status: "completed",
254
+ reportedFindings: [],
255
+ objectiveResults,
256
+ messages: [],
257
+ });
258
+
259
+ expect(loadSubagentObjectiveResults(session, "pentest-agent-1")).toEqual(
260
+ objectiveResults,
261
+ );
262
+
263
+ saveSubagentData(session, {
264
+ agentName: "pentest-agent-1",
265
+ target: "http://localhost:8080",
266
+ status: "completed",
267
+ reportedFindings: [],
268
+ objectiveResults: [],
269
+ messages: [],
270
+ });
271
+ expect(loadSubagentObjectiveResults(session, "pentest-agent-1")).toEqual(
272
+ [],
273
+ );
274
+ });
275
+
276
+ it("rejects missing, failed, and malformed objective coverage", () => {
277
+ const session = makeSession();
278
+ const key = "pentest-agent-1";
279
+ const snapshotPath = join(tmpDir, "subagents", `${key}.json`);
280
+ expect(loadSubagentObjectiveResults(session, key)).toBeNull();
281
+ mkdirSync(join(tmpDir, "subagents"));
282
+
283
+ const invalidSnapshots = [
284
+ { status: "failed", objectiveResults: [] },
285
+ { status: "completed" },
286
+ { status: "completed", objectiveResults: null },
287
+ { status: "completed", objectiveResults: [{ objective: "Test" }] },
288
+ {
289
+ status: "completed",
290
+ objectiveResults: [{ objective: "Test", completed: "true" }],
291
+ },
292
+ {
293
+ status: "completed",
294
+ objectiveResults: [{ objective: "Test", completed: true, result: 42 }],
295
+ },
296
+ ];
297
+ for (const snapshot of invalidSnapshots) {
298
+ writeFileSync(snapshotPath, JSON.stringify(snapshot));
299
+ expect(loadSubagentObjectiveResults(session, key)).toBeNull();
300
+ }
301
+ });
302
+ });
303
+
242
304
  // ---------------------------------------------------------------------------
243
305
  // Manifest roundtrip
244
306
  // ---------------------------------------------------------------------------
@@ -28,6 +28,7 @@ export type { SessionInfo };
28
28
 
29
29
  import type { ModelMessage } from "ai";
30
30
  import type { GrpcPentestContext } from "../agents/specialized/attackSurface/grpcSchema";
31
+ import type { ObjectiveResult } from "../agents/specialized/pentest/agent";
31
32
  import { newSessionId, type SessionID } from "../id/id";
32
33
  import type { AuthenticationInfo } from "./types";
33
34
 
@@ -56,6 +57,8 @@ interface SavedSubagentData {
56
57
  toolCallCount?: number;
57
58
  stepCount?: number;
58
59
  findingsCount?: number;
60
+ reportedFindings?: string[];
61
+ objectiveResults?: ObjectiveResult[];
59
62
  status?: string;
60
63
  error?: string;
61
64
  messages?: ModelMessage[];
@@ -115,6 +118,8 @@ export interface SubagentSaveInput {
115
118
  status: string;
116
119
  error?: string;
117
120
  findingsCount?: number;
121
+ reportedFindings?: string[];
122
+ objectiveResults?: ObjectiveResult[];
118
123
  /** Raw AI SDK messages from onStepFinish (e.response.messages) */
119
124
  messages: ModelMessage[];
120
125
  /** System prompt used for this agent (for training data export) */
@@ -150,6 +155,55 @@ export function loadSubagentMessages(
150
155
  }
151
156
  }
152
157
 
158
+ /** Read external findings from a completed swarm target on resume. */
159
+ export function loadSubagentReportedFindings(
160
+ session: SessionInfo,
161
+ key: string,
162
+ ): string[] | null {
163
+ const snapshotPath = join(session.rootPath, SUBAGENTS_DIR, `${key}.json`);
164
+ if (!existsSync(snapshotPath)) return null;
165
+ try {
166
+ const data = JSON.parse(
167
+ readFileSync(snapshotPath, "utf-8"),
168
+ ) as SavedSubagentData;
169
+ return data.status === "completed" &&
170
+ Array.isArray(data.reportedFindings) &&
171
+ data.reportedFindings.every((finding) => typeof finding === "string")
172
+ ? data.reportedFindings
173
+ : null;
174
+ } catch {
175
+ return null;
176
+ }
177
+ }
178
+
179
+ /** Read completed objective coverage from a swarm target on resume. */
180
+ export function loadSubagentObjectiveResults(
181
+ session: SessionInfo,
182
+ key: string,
183
+ ): ObjectiveResult[] | null {
184
+ const snapshotPath = join(session.rootPath, SUBAGENTS_DIR, `${key}.json`);
185
+ if (!existsSync(snapshotPath)) return null;
186
+ try {
187
+ const data = JSON.parse(
188
+ readFileSync(snapshotPath, "utf-8"),
189
+ ) as SavedSubagentData;
190
+ return data.status === "completed" &&
191
+ Array.isArray(data.objectiveResults) &&
192
+ data.objectiveResults.every(
193
+ (result) =>
194
+ result !== null &&
195
+ typeof result === "object" &&
196
+ typeof result.objective === "string" &&
197
+ typeof result.completed === "boolean" &&
198
+ (result.result === undefined || typeof result.result === "string"),
199
+ )
200
+ ? data.objectiveResults
201
+ : null;
202
+ } catch {
203
+ return null;
204
+ }
205
+ }
206
+
153
207
  /** Writes to `{session.rootPath}/{SUBAGENTS_DIR}/{name}.json`. */
154
208
  export function saveSubagentData(
155
209
  session: SessionInfo,
@@ -181,6 +235,8 @@ export function saveSubagentData(
181
235
  toolCallCount,
182
236
  stepCount,
183
237
  findingsCount: data.findingsCount ?? 0,
238
+ reportedFindings: data.reportedFindings,
239
+ objectiveResults: data.objectiveResults,
184
240
  status: data.status,
185
241
  error: data.error,
186
242
  messages: data.messages,
@@ -61,6 +61,7 @@ let rootPath: string;
61
61
  let session: SessionInfo;
62
62
 
63
63
  beforeEach(() => {
64
+ mocks.registry.groupByRootCause.mockClear();
64
65
  rootPath = mkdtempSync(join(tmpdir(), "faststrike-seams-"));
65
66
  session = {
66
67
  id: "ses_faststrike_seams",
@@ -215,3 +216,151 @@ it("forwards canonical hooks and legacy fields to the fast-strike operator", asy
215
216
  });
216
217
  expect(mocks.fastStrikeInputs[0]).toMatchObject({ ...hooks, usageRecorder });
217
218
  });
219
+
220
+ describe("external Fast Strike", () => {
221
+ it("keeps exfiltration mode explicit and returns the extracted result", async () => {
222
+ session.config = { exfilMode: true };
223
+ mocks.consumeImpl = async () => ({
224
+ solved: true,
225
+ summary: "Extracted FLAG{example}",
226
+ reportedFindings: ["Confirmed scoped weakness"],
227
+ submissionJson: "",
228
+ });
229
+
230
+ const result = await runFastStrike({
231
+ target: "https://example.com",
232
+ model: {} as PentestWorkflowInput["model"],
233
+ session,
234
+ findingPolicy: "external",
235
+ });
236
+
237
+ expect(String(mocks.fastStrikeInputs.at(-1)?.prompt)).toContain(
238
+ "Exfiltration goal: locate and extract FLAG{...}",
239
+ );
240
+ expect(result).toMatchObject({
241
+ solved: true,
242
+ summary: "Extracted FLAG{example}",
243
+ });
244
+ });
245
+
246
+ it("returns exact submission JSON and skips native finding aggregation", async () => {
247
+ const submissionJson = JSON.stringify({
248
+ schema_version: "penbench-finding/v1",
249
+ status: "finding",
250
+ surfaces: ["GET /private"],
251
+ root_cause: "Tenant scope is not enforced",
252
+ impact: "Cross-tenant read",
253
+ });
254
+ mocks.consumeImpl = async () => ({
255
+ solved: true,
256
+ summary: "Cross-tenant read found",
257
+ reportedFindings: ["GET /private reads another tenant's data"],
258
+ submissionJson,
259
+ });
260
+
261
+ const result = await runFastStrike({
262
+ target: "https://example.com",
263
+ model: {} as PentestWorkflowInput["model"],
264
+ session,
265
+ findingPolicy: "external",
266
+ enableThinking: true,
267
+ thinkingEffort: "high",
268
+ });
269
+
270
+ expect(result).toMatchObject({
271
+ findings: [],
272
+ reportedFindings: ["GET /private reads another tenant's data"],
273
+ submissionJson,
274
+ solved: true,
275
+ summary: "Cross-tenant read found",
276
+ reportPath: null,
277
+ });
278
+ expect(mocks.registry.groupByRootCause).not.toHaveBeenCalled();
279
+ const input = mocks.fastStrikeInputs.at(-1);
280
+ expect(input).toMatchObject({
281
+ findingPolicy: "external",
282
+ enableThinking: true,
283
+ thinkingEffort: "high",
284
+ });
285
+ expect(String(input?.system)).not.toContain("document_vulnerability");
286
+ const responseSchema = input?.responseSchema as {
287
+ safeParse: (value: unknown) => { success: boolean };
288
+ };
289
+ expect(
290
+ responseSchema.safeParse({
291
+ solved: true,
292
+ summary: "Found it",
293
+ reportedFindings: [],
294
+ submissionJson,
295
+ }).success,
296
+ ).toBe(true);
297
+ expect(
298
+ responseSchema.safeParse({ solved: true, summary: "Found it" }).success,
299
+ ).toBe(false);
300
+ });
301
+
302
+ it("fails closed when the external response omits the submission", async () => {
303
+ mocks.consumeImpl = async () => ({
304
+ solved: true,
305
+ summary: "Found it",
306
+ reportedFindings: ["finding"],
307
+ });
308
+ await expect(
309
+ runFastStrike({
310
+ target: "https://example.com",
311
+ model: {} as PentestWorkflowInput["model"],
312
+ session,
313
+ findingPolicy: "external",
314
+ }),
315
+ ).rejects.toThrow("without a structured findings response");
316
+ });
317
+
318
+ it.each([
319
+ ["undefined", undefined],
320
+ ["null", null],
321
+ [
322
+ "wrong field types",
323
+ {
324
+ solved: true,
325
+ summary: "claimed",
326
+ reportedFindings: "claimed",
327
+ submissionJson: "{}",
328
+ },
329
+ ],
330
+ ])("marks a %s external response as failed before completion hooks", async (_label, outcome) => {
331
+ mocks.consumeImpl = async () => outcome;
332
+ const emit = vi.fn();
333
+ const onEndpointDone = vi.fn();
334
+ const onEndpointFailed = vi.fn();
335
+
336
+ await expect(
337
+ runFastStrike({
338
+ target: "https://example.com",
339
+ model: {} as PentestWorkflowInput["model"],
340
+ session,
341
+ findingPolicy: "external",
342
+ eventBus: { emit } as unknown as NonNullable<
343
+ PentestWorkflowInput["eventBus"]
344
+ >,
345
+ seams: inProcessSeams({
346
+ hooks: { onEndpointDone, onEndpointFailed },
347
+ registries: {
348
+ findings: () => mocks.registry,
349
+ attackSurface: () => ({}) as never,
350
+ },
351
+ }),
352
+ }),
353
+ ).rejects.toThrow("without a structured findings response");
354
+
355
+ expect(onEndpointDone).not.toHaveBeenCalled();
356
+ expect(onEndpointFailed).toHaveBeenCalledOnce();
357
+ expect(emit).toHaveBeenCalledWith("subagent-complete", {
358
+ subagentId: "fast-strike",
359
+ status: "failed",
360
+ });
361
+ expect(emit).not.toHaveBeenCalledWith("subagent-complete", {
362
+ subagentId: "fast-strike",
363
+ status: "completed",
364
+ });
365
+ });
366
+ });
@@ -1,5 +1,8 @@
1
1
  import { describe, expect, it } from "vitest";
2
- import { FAST_STRIKE_SYSTEM_PROMPT } from "./fastStrike";
2
+ import {
3
+ buildFastStrikeSystemPrompt,
4
+ FAST_STRIKE_SYSTEM_PROMPT,
5
+ } from "./fastStrike";
3
6
 
4
7
  describe("Fast Strike system prompt", () => {
5
8
  it("externalizes complex exploit chains into editable scripts", () => {
@@ -26,4 +29,18 @@ describe("Fast Strike system prompt", () => {
26
29
  "call the response tool with the correct solved value",
27
30
  );
28
31
  });
32
+
33
+ it("keeps the native prompt as the default policy", () => {
34
+ expect(buildFastStrikeSystemPrompt()).toBe(FAST_STRIKE_SYSTEM_PROMPT);
35
+ });
36
+
37
+ it("keeps the Strike loop but hands finding acceptance to the caller", () => {
38
+ const prompt = buildFastStrikeSystemPrompt("external");
39
+ expect(prompt).toContain("OBSERVE");
40
+ expect(prompt).toContain("HYPOTHESIZE");
41
+ expect(prompt).toContain("editable script");
42
+ expect(prompt).toContain("submissionJson");
43
+ expect(prompt).not.toContain("document_vulnerability");
44
+ expect(prompt).not.toContain("validation judge");
45
+ });
29
46
  });
@@ -1,6 +1,7 @@
1
1
  import { writeFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { z } from "zod";
4
+ import type { FindingPolicy } from "../agents/offSecAgent";
4
5
  import { OffensiveSecurityAgent } from "../agents/offSecAgent";
5
6
  import { FindingsRegistry } from "../findings/registry";
6
7
  import {
@@ -32,9 +33,44 @@ const FastStrikeResult = z.object({
32
33
  ),
33
34
  });
34
35
 
35
- type FastStrikeOutcome = z.infer<typeof FastStrikeResult>;
36
+ const FastStrikeExternalResult = z.object({
37
+ solved: z.boolean().describe("True if the supplied objective was met."),
38
+ summary: z
39
+ .string()
40
+ .describe(
41
+ "Concise account of security findings and the evidence observed, including any flag or sensitive data extracted for an exfiltration objective.",
42
+ ),
43
+ reportedFindings: z
44
+ .array(z.string())
45
+ .describe(
46
+ "One concise evidence-backed description per discovered vulnerability.",
47
+ ),
48
+ submissionJson: z
49
+ .string()
50
+ .describe(
51
+ "The exact task-defined JSON submission as a string, or an empty string when no JSON submission was requested. Do not invent fields.",
52
+ ),
53
+ });
54
+
55
+ type FastStrikeOutcome =
56
+ | z.infer<typeof FastStrikeResult>
57
+ | z.infer<typeof FastStrikeExternalResult>;
36
58
 
37
- export const FAST_STRIKE_SYSTEM_PROMPT = `You are an elite offensive security operator running a fast strike against a single target. You work ALONE and finish the job yourself — there is no team to hand off to, no swarm to spawn, no plan to file. Your edge is discipline and observation, not volume.
59
+ export function buildFastStrikeSystemPrompt(
60
+ findingPolicy: FindingPolicy = "native",
61
+ ): string {
62
+ const external = findingPolicy === "external";
63
+ const objectiveStep = external
64
+ ? "5. FOLLOW THE OBJECTIVE — Determine whether the observed behavior establishes a security weakness. For a detection-only objective, stop once the requested evidence is sufficient; develop an exploit or pivot only when the objective requires it."
65
+ : "5. EXPLOIT — When a vulnerability is confirmed, exploit it: capture the flag, extract the sensitive data, and pivot through it if the objective requires more. Preserve viable primitives and build on them. Prefer extending a working script into an end-to-end exploit over rebuilding the chain with repeated one-off tool calls or stopping at the next network or protocol boundary.";
66
+ const finishStep = external
67
+ ? "6. REPORT & FINISH — Include each credible finding and its supporting observations in the caller's requested output. If the caller requested a JSON submission, include the exact JSON text in the response tool's submissionJson field as well; otherwise use an empty string. The caller evaluates findings after the run. Once the objective is met or credible leads are exhausted, call the response tool with the correct solved value and a concise summary."
68
+ : "6. DOCUMENT & FINISH — Record each confirmed vulnerability with the document_vulnerability tool as you go. Before concluding that every credible lead is exhausted, account for each viable primitive and pursue any concrete path that could still close the objective. After this check, whether the objective was met or every credible lead was genuinely exhausted, call the response tool with the correct solved value and a concise summary.";
69
+ const evidenceRule = external
70
+ ? "- Bias toward the shortest path that satisfies the assigned objective. Distinguish observed evidence from hypotheses, and avoid separate in-run finding review."
71
+ : "- Bias toward the shortest path that proves impact. Speed comes from picking the right lead, not from skipping verification — always confirm a finding before you claim it.";
72
+
73
+ return `You are an elite offensive security operator running a fast strike against a single target. You work ALONE and finish the job yourself — there is no team to hand off to, no swarm to spawn, no plan to file. Your edge is discipline and observation, not volume.
38
74
 
39
75
  Run this loop, tightly:
40
76
 
@@ -42,15 +78,18 @@ Run this loop, tightly:
42
78
  2. HYPOTHESIZE — From that signal and your deep knowledge of web/app vulnerability classes, name the single most likely vulnerability and why the evidence points to it. Rank leads by signal strength and go for the strongest first. A disabled form field, an ID in a URL or JWT, advertised demo creds, an exposed schema — these are tells; read them.
43
79
  3. ACT — Test that one hypothesis with the strongest available technique. Use direct tools for quick probes, but treat the shell as a working environment rather than merely a way to issue one-off commands. When the work becomes stateful, repetitive, concurrent, protocol-heavy, or multi-stage, externalize it into an editable script in the session scratchpad directory identified in the workspace instructions. Never create a scratchpad/ directory inside the target repository. Run the script, inspect the results, modify it, and rerun it as the attack develops. For blind/out-of-band testing (SSRF, blind RCE, XSS callbacks), start_callback_listener gives you a nonce-correlated listener that survives intervening commands: embed the returned callback URL (only an address you know the target can reach — never a guessed one) in the payload, then poll_callback_listener for hits. Listener readiness and the tool's own self-test never count as target evidence; stop_callback_listener when done.
44
80
  4. PRUNE — If the evidence disproves a lead, drop it and move to the next-strongest. Do NOT re-run a disproven approach or grind a dead end. Do NOT enumerate, brute-force, or run wordlists unless a specific signal demands it — choose techniques from evidence, not from a checklist.
45
- 5. EXPLOIT — When a vulnerability is confirmed, exploit it: capture the flag, extract the sensitive data, and pivot through it if the objective requires more. Preserve viable primitives and build on them. Prefer extending a working script into an end-to-end exploit over rebuilding the chain with repeated one-off tool calls or stopping at the next network or protocol boundary.
46
- 6. DOCUMENT & FINISH — Record each confirmed vulnerability with the document_vulnerability tool as you go. Before concluding that every credible lead is exhausted, account for each viable primitive and pursue any concrete path that could still close the objective. After this check, whether the objective was met or every credible lead was genuinely exhausted, call the response tool with the correct solved value and a concise summary.
81
+ ${objectiveStep}
82
+ ${finishStep}
47
83
 
48
84
  Rules:
49
85
  - You are one operator. Plan in your head and in short text notes — never via planning tools, task lists, or sub-agents.
50
86
  - Recon and exploitation are the same loop. Do just enough recon to form the first hypothesis, then start testing. Do not map the entire surface before acting.
51
87
  - Spend your reasoning on choosing techniques and interpreting evidence; use scripts for deterministic mechanics, iteration, and chain execution.
52
- - Bias toward the shortest path that proves impact. Speed comes from picking the right lead, not from skipping verification — always confirm a finding before you claim it.
88
+ ${evidenceRule}
53
89
  - Stay in scope. Only touch the target you were given and hosts it legitimately depends on.`;
90
+ }
91
+
92
+ export const FAST_STRIKE_SYSTEM_PROMPT = buildFastStrikeSystemPrompt();
54
93
 
55
94
  /** Single-operator pentest: skips attack-surface/swarm, returns {@link PentestWorkflowResult}. */
56
95
  export async function runFastStrike(
@@ -67,6 +106,7 @@ export async function runFastStrike(
67
106
  onStepFinish,
68
107
  onCacheMetrics,
69
108
  enableThinking,
109
+ thinkingEffort,
70
110
  openAIReasoningEffort,
71
111
  prompt,
72
112
  threatModel,
@@ -116,6 +156,7 @@ export async function runFastStrike(
116
156
  }
117
157
 
118
158
  const operatorGuidance = session.config?.prompt;
159
+ const findingPolicy = input.hooks?.findingPolicy ?? input.findingPolicy;
119
160
 
120
161
  const mode: "blackbox" | "whitebox" = cwd ? "whitebox" : "blackbox";
121
162
 
@@ -136,8 +177,15 @@ export async function runFastStrike(
136
177
  const promptParts: string[] = [`Target: ${target}`];
137
178
  if (operatorGuidance) promptParts.push(operatorGuidance);
138
179
  promptParts.push(
139
- "Run the fast strike: find and exploit the vulnerability, complete the objective, and report what worked.",
180
+ findingPolicy === "external"
181
+ ? "Run the fast strike: investigate the target, identify security weaknesses, and submit the findings in the requested output. Stop when the supplied objective is met."
182
+ : "Run the fast strike: find and exploit the vulnerability, complete the objective, and report what worked.",
140
183
  );
184
+ if (findingPolicy === "external" && session.config?.exfilMode) {
185
+ promptParts.push(
186
+ "Exfiltration goal: locate and extract FLAG{...} or the assigned sensitive data through a confirmed weakness. Do not stop at discovery; include the extraction result in the final summary.",
187
+ );
188
+ }
141
189
 
142
190
  const agent = new OffensiveSecurityAgent<FastStrikeOutcome>({
143
191
  subagentSpawner: input.subagentSpawner,
@@ -145,14 +193,18 @@ export async function runFastStrike(
145
193
  usageRecorder: input.usageRecorder,
146
194
  streamIdFactory: input.streamIdFactory,
147
195
  ...input.hooks,
148
- system: FAST_STRIKE_SYSTEM_PROMPT,
196
+ system: buildFastStrikeSystemPrompt(findingPolicy),
149
197
  prompt: promptParts.join("\n\n"),
150
198
  model,
151
199
  session,
152
200
  target,
153
201
  mode: "fast-strike",
202
+ findingPolicy,
154
203
  activeTools: [],
155
- responseSchema: FastStrikeResult,
204
+ responseSchema:
205
+ findingPolicy === "external"
206
+ ? FastStrikeExternalResult
207
+ : FastStrikeResult,
156
208
  findingsRegistry,
157
209
  subagentId: "fast-strike",
158
210
  authConfig,
@@ -164,6 +216,7 @@ export async function runFastStrike(
164
216
  onStepFinish,
165
217
  onCacheMetrics,
166
218
  enableThinking,
219
+ thinkingEffort,
167
220
  openAIReasoningEffort,
168
221
  });
169
222
 
@@ -179,16 +232,32 @@ export async function runFastStrike(
179
232
  target,
180
233
  });
181
234
 
182
- let strikeResult: FastStrikeOutcome;
235
+ let completed:
236
+ | { policy: "native"; outcome: FastStrikeOutcome }
237
+ | {
238
+ policy: "external";
239
+ outcome: z.infer<typeof FastStrikeExternalResult>;
240
+ };
183
241
  try {
184
- strikeResult = await agent.consume();
242
+ const strikeResult = await agent.consume();
243
+ if (findingPolicy === "external") {
244
+ const parsed = FastStrikeExternalResult.safeParse(strikeResult);
245
+ if (!parsed.success) {
246
+ throw new Error(
247
+ "Externally graded Fast Strike ended without a structured findings response",
248
+ );
249
+ }
250
+ completed = { policy: "external", outcome: parsed.data };
251
+ } else {
252
+ completed = { policy: "native", outcome: strikeResult };
253
+ }
185
254
  eventBus?.emit("subagent-complete", {
186
255
  subagentId: "fast-strike",
187
256
  status: "completed",
188
257
  });
189
258
  await seams.hooks.onEndpointDone?.(
190
259
  { subagentId: "fast-strike", subagentName: "Fast Strike", target },
191
- strikeResult,
260
+ completed.outcome,
192
261
  );
193
262
  } catch (e) {
194
263
  eventBus?.emit("subagent-complete", {
@@ -206,8 +275,31 @@ export async function runFastStrike(
206
275
  throw new DOMException("Pentest aborted by user", "AbortError");
207
276
  }
208
277
 
278
+ if (completed.policy === "external") {
279
+ const strikeResult = completed.outcome;
280
+ eventBus?.emit("workflow-phase-complete", {
281
+ phase: "pentesting",
282
+ summary: {
283
+ findingsCount: strikeResult.reportedFindings.length,
284
+ solved: strikeResult.solved,
285
+ operatorSummary: strikeResult.summary,
286
+ },
287
+ });
288
+ return {
289
+ findings: [],
290
+ findingsPath: session.findingsPath,
291
+ pocsPath: session.pocsPath,
292
+ reportPath: null,
293
+ reportedFindings: strikeResult.reportedFindings,
294
+ submissionJson: strikeResult.submissionJson,
295
+ solved: strikeResult.solved,
296
+ summary: strikeResult.summary,
297
+ };
298
+ }
299
+
209
300
  await findingsRegistry.groupByRootCause();
210
301
  const findings = [...findingsRegistry.getFindings()];
302
+ const strikeResult = completed.outcome;
211
303
 
212
304
  const report = buildPentestReport(findings, {
213
305
  target,