@atlascrew/apparatus 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (291) hide show
  1. package/bin/apparatus.mjs +2 -0
  2. package/certs/server.crt +17 -0
  3. package/certs/server.key +28 -0
  4. package/dist/ai/client.js +104 -0
  5. package/dist/ai/client.js.map +1 -0
  6. package/dist/ai/personas.js +104 -0
  7. package/dist/ai/personas.js.map +1 -0
  8. package/dist/ai/redteam.js +1404 -0
  9. package/dist/ai/redteam.js.map +1 -0
  10. package/dist/ai/report-store.js +309 -0
  11. package/dist/ai/report-store.js.map +1 -0
  12. package/dist/app.js +525 -0
  13. package/dist/app.js.map +1 -0
  14. package/dist/attack-sim.js +69 -0
  15. package/dist/attack-sim.js.map +1 -0
  16. package/dist/attacker-tracker.js +276 -0
  17. package/dist/attacker-tracker.js.map +1 -0
  18. package/dist/blackhole.js +95 -0
  19. package/dist/blackhole.js.map +1 -0
  20. package/dist/chaos.js +88 -0
  21. package/dist/chaos.js.map +1 -0
  22. package/dist/cluster.js +462 -0
  23. package/dist/cluster.js.map +1 -0
  24. package/dist/config.js +61 -0
  25. package/dist/config.js.map +1 -0
  26. package/dist/deception.js +205 -0
  27. package/dist/deception.js.map +1 -0
  28. package/dist/demo-mode.js +109 -0
  29. package/dist/demo-mode.js.map +1 -0
  30. package/dist/dist-dashboard/assets/index-BsMhEnGu.js +648 -0
  31. package/dist/dist-dashboard/assets/index-CNOkYC_Q.css +10 -0
  32. package/dist/dist-dashboard/assets/index-CW2grvPC.js +648 -0
  33. package/dist/dist-dashboard/assets/logo/apparatus-favicon.svg +15 -0
  34. package/dist/dist-dashboard/assets/logo/apparatus-icon-dark.svg +24 -0
  35. package/dist/dist-dashboard/assets/logo/apparatus-icon-light.svg +24 -0
  36. package/dist/dist-dashboard/assets/logo/apparatus-logo-512.png +0 -0
  37. package/dist/dist-dashboard/assets/logo/apparatus-logo-dark.svg +18 -0
  38. package/dist/dist-dashboard/assets/logo/apparatus-logo.svg +17 -0
  39. package/dist/dist-dashboard/assets/logo/apple-touch-icon.png +0 -0
  40. package/dist/dist-dashboard/assets/logo/favicon-192.png +0 -0
  41. package/dist/dist-dashboard/assets/logo/favicon-32.png +0 -0
  42. package/dist/dist-dashboard/assets/logo/favicon.ico +0 -0
  43. package/dist/dist-dashboard/assets/logo/icon-192.png +0 -0
  44. package/dist/dist-dashboard/assets/logo/icon-512.png +0 -0
  45. package/dist/dist-dashboard/assets/logo/icon-light-512.png +0 -0
  46. package/dist/dist-dashboard/assets/react-vendor-DpRMSntD.js +1 -0
  47. package/dist/dist-dashboard/assets/router-DSc5pRwN.js +59 -0
  48. package/dist/dist-dashboard/docs-index.json +1577 -0
  49. package/dist/dist-dashboard/index.html +21 -0
  50. package/dist/dlp.js +40 -0
  51. package/dist/dlp.js.map +1 -0
  52. package/dist/drills.js +770 -0
  53. package/dist/drills.js.map +1 -0
  54. package/dist/echoHandler.js +113 -0
  55. package/dist/echoHandler.js.map +1 -0
  56. package/dist/escape/index.js +225 -0
  57. package/dist/escape/index.js.map +1 -0
  58. package/dist/escape/methods/dns.js +74 -0
  59. package/dist/escape/methods/dns.js.map +1 -0
  60. package/dist/escape/methods/http.js +81 -0
  61. package/dist/escape/methods/http.js.map +1 -0
  62. package/dist/escape/methods/icmp.js +36 -0
  63. package/dist/escape/methods/icmp.js.map +1 -0
  64. package/dist/escape/methods/tcp.js +38 -0
  65. package/dist/escape/methods/tcp.js.map +1 -0
  66. package/dist/escape/methods/udp.js +27 -0
  67. package/dist/escape/methods/udp.js.map +1 -0
  68. package/dist/escape/methods/websocket.js +37 -0
  69. package/dist/escape/methods/websocket.js.map +1 -0
  70. package/dist/forensics.js +111 -0
  71. package/dist/forensics.js.map +1 -0
  72. package/dist/generator.js +67 -0
  73. package/dist/generator.js.map +1 -0
  74. package/dist/ghosting.js +414 -0
  75. package/dist/ghosting.js.map +1 -0
  76. package/dist/graphql.js +44 -0
  77. package/dist/graphql.js.map +1 -0
  78. package/dist/history.js +40 -0
  79. package/dist/history.js.map +1 -0
  80. package/dist/imposter/creds.js +16 -0
  81. package/dist/imposter/creds.js.map +1 -0
  82. package/dist/imposter/index.js +44 -0
  83. package/dist/imposter/index.js.map +1 -0
  84. package/dist/imposter/providers/aws.js +103 -0
  85. package/dist/imposter/providers/aws.js.map +1 -0
  86. package/dist/imposter/providers/gcp.js +26 -0
  87. package/dist/imposter/providers/gcp.js.map +1 -0
  88. package/dist/index.js +53 -0
  89. package/dist/index.js.map +1 -0
  90. package/dist/infra-debug.js +68 -0
  91. package/dist/infra-debug.js.map +1 -0
  92. package/dist/jwt-debug.js +272 -0
  93. package/dist/jwt-debug.js.map +1 -0
  94. package/dist/kv.js +22 -0
  95. package/dist/kv.js.map +1 -0
  96. package/dist/lib/generators.js +43 -0
  97. package/dist/lib/generators.js.map +1 -0
  98. package/dist/lib/json.js +26 -0
  99. package/dist/lib/json.js.map +1 -0
  100. package/dist/logger.js +9 -0
  101. package/dist/logger.js.map +1 -0
  102. package/dist/metrics.js +20 -0
  103. package/dist/metrics.js.map +1 -0
  104. package/dist/mtd.js +30 -0
  105. package/dist/mtd.js.map +1 -0
  106. package/dist/oidc.js +69 -0
  107. package/dist/oidc.js.map +1 -0
  108. package/dist/persistence/cluster-state.js +47 -0
  109. package/dist/persistence/cluster-state.js.map +1 -0
  110. package/dist/persistence/deception-history.js +65 -0
  111. package/dist/persistence/deception-history.js.map +1 -0
  112. package/dist/persistence/drill-runs.js +138 -0
  113. package/dist/persistence/drill-runs.js.map +1 -0
  114. package/dist/persistence/request-history.js +41 -0
  115. package/dist/persistence/request-history.js.map +1 -0
  116. package/dist/persistence/scenario-catalog.js +73 -0
  117. package/dist/persistence/scenario-catalog.js.map +1 -0
  118. package/dist/persistence/status.js +51 -0
  119. package/dist/persistence/status.js.map +1 -0
  120. package/dist/persistence/tarpit-state.js +47 -0
  121. package/dist/persistence/tarpit-state.js.map +1 -0
  122. package/dist/persistence/webhook-store.js +69 -0
  123. package/dist/persistence/webhook-store.js.map +1 -0
  124. package/dist/proxy.js +28 -0
  125. package/dist/proxy.js.map +1 -0
  126. package/dist/ratelimit.js +32 -0
  127. package/dist/ratelimit.js.map +1 -0
  128. package/dist/redteam.js +442 -0
  129. package/dist/redteam.js.map +1 -0
  130. package/dist/scenarios.js +229 -0
  131. package/dist/scenarios.js.map +1 -0
  132. package/dist/scripting.js +30 -0
  133. package/dist/scripting.js.map +1 -0
  134. package/dist/self-healing.js +42 -0
  135. package/dist/self-healing.js.map +1 -0
  136. package/dist/sentinel.js +50 -0
  137. package/dist/sentinel.js.map +1 -0
  138. package/dist/server-bad-ssl.js +47 -0
  139. package/dist/server-bad-ssl.js.map +1 -0
  140. package/dist/server-grpc.js +66 -0
  141. package/dist/server-grpc.js.map +1 -0
  142. package/dist/server-http1.js +5 -0
  143. package/dist/server-http1.js.map +1 -0
  144. package/dist/server-http2.js +27 -0
  145. package/dist/server-http2.js.map +1 -0
  146. package/dist/server-icap.js +46 -0
  147. package/dist/server-icap.js.map +1 -0
  148. package/dist/server-l4.js +30 -0
  149. package/dist/server-l4.js.map +1 -0
  150. package/dist/server-mqtt.js +29 -0
  151. package/dist/server-mqtt.js.map +1 -0
  152. package/dist/server-protocols.js +18 -0
  153. package/dist/server-protocols.js.map +1 -0
  154. package/dist/server-redis.js +112 -0
  155. package/dist/server-redis.js.map +1 -0
  156. package/dist/server-smtp.js +66 -0
  157. package/dist/server-smtp.js.map +1 -0
  158. package/dist/server-syslog.js +23 -0
  159. package/dist/server-syslog.js.map +1 -0
  160. package/dist/server-ws.js +18 -0
  161. package/dist/server-ws.js.map +1 -0
  162. package/dist/sidecar/chaos/engine.js +41 -0
  163. package/dist/sidecar/chaos/engine.js.map +1 -0
  164. package/dist/sidecar/index.js +98 -0
  165. package/dist/sidecar/index.js.map +1 -0
  166. package/dist/simulator/dependency-graph.js +102 -0
  167. package/dist/simulator/dependency-graph.js.map +1 -0
  168. package/dist/simulator/supply-chain.js +67 -0
  169. package/dist/simulator/supply-chain.js.map +1 -0
  170. package/dist/sink.js +24 -0
  171. package/dist/sink.js.map +1 -0
  172. package/dist/sse-broadcast.js +105 -0
  173. package/dist/sse-broadcast.js.map +1 -0
  174. package/dist/swagger.js +309 -0
  175. package/dist/swagger.js.map +1 -0
  176. package/dist/sysinfo.js +36 -0
  177. package/dist/sysinfo.js.map +1 -0
  178. package/dist/tarpit.js +126 -0
  179. package/dist/tarpit.js.map +1 -0
  180. package/dist/tool-executor.js +315 -0
  181. package/dist/tool-executor.js.map +1 -0
  182. package/dist/tui/api-client.js +341 -0
  183. package/dist/tui/api-client.js.map +1 -0
  184. package/dist/tui/core/action-handler.js +302 -0
  185. package/dist/tui/core/action-handler.js.map +1 -0
  186. package/dist/tui/core/index.js +18 -0
  187. package/dist/tui/core/index.js.map +1 -0
  188. package/dist/tui/core/keyboard.js +329 -0
  189. package/dist/tui/core/keyboard.js.map +1 -0
  190. package/dist/tui/core/modal.js +397 -0
  191. package/dist/tui/core/modal.js.map +1 -0
  192. package/dist/tui/core/screen-manager.js +262 -0
  193. package/dist/tui/core/screen-manager.js.map +1 -0
  194. package/dist/tui/core/store.js +254 -0
  195. package/dist/tui/core/store.js.map +1 -0
  196. package/dist/tui/core/widget.js +167 -0
  197. package/dist/tui/core/widget.js.map +1 -0
  198. package/dist/tui/dashboard.js +649 -0
  199. package/dist/tui/dashboard.js.map +1 -0
  200. package/dist/tui/index.js +118 -0
  201. package/dist/tui/index.js.map +1 -0
  202. package/dist/tui/modals/add-rule-modal.js +190 -0
  203. package/dist/tui/modals/add-rule-modal.js.map +1 -0
  204. package/dist/tui/modals/dlp-output-modal.js +102 -0
  205. package/dist/tui/modals/dlp-output-modal.js.map +1 -0
  206. package/dist/tui/modals/dns-form-modal.js +26 -0
  207. package/dist/tui/modals/dns-form-modal.js.map +1 -0
  208. package/dist/tui/modals/ghost-config-modal.js +35 -0
  209. package/dist/tui/modals/ghost-config-modal.js.map +1 -0
  210. package/dist/tui/modals/har-results-modal.js +41 -0
  211. package/dist/tui/modals/har-results-modal.js.map +1 -0
  212. package/dist/tui/modals/index.js +15 -0
  213. package/dist/tui/modals/index.js.map +1 -0
  214. package/dist/tui/modals/jwt-decode-modal.js +45 -0
  215. package/dist/tui/modals/jwt-decode-modal.js.map +1 -0
  216. package/dist/tui/modals/jwt-mint-modal.js +70 -0
  217. package/dist/tui/modals/jwt-mint-modal.js.map +1 -0
  218. package/dist/tui/modals/ping-form-modal.js +19 -0
  219. package/dist/tui/modals/ping-form-modal.js.map +1 -0
  220. package/dist/tui/modals/redteam-results-modal.js +43 -0
  221. package/dist/tui/modals/redteam-results-modal.js.map +1 -0
  222. package/dist/tui/modals/scan-form-modal.js +26 -0
  223. package/dist/tui/modals/scan-form-modal.js.map +1 -0
  224. package/dist/tui/screens/defense-screen.js +281 -0
  225. package/dist/tui/screens/defense-screen.js.map +1 -0
  226. package/dist/tui/screens/forensics-screen.js +81 -0
  227. package/dist/tui/screens/forensics-screen.js.map +1 -0
  228. package/dist/tui/screens/index.js +140 -0
  229. package/dist/tui/screens/index.js.map +1 -0
  230. package/dist/tui/screens/system-screen.js +81 -0
  231. package/dist/tui/screens/system-screen.js.map +1 -0
  232. package/dist/tui/screens/testing-screen.js +429 -0
  233. package/dist/tui/screens/testing-screen.js.map +1 -0
  234. package/dist/tui/screens/traffic-screen.js +76 -0
  235. package/dist/tui/screens/traffic-screen.js.map +1 -0
  236. package/dist/tui/sse-client.js +130 -0
  237. package/dist/tui/sse-client.js.map +1 -0
  238. package/dist/tui/state/metrics-buffer.js +195 -0
  239. package/dist/tui/state/metrics-buffer.js.map +1 -0
  240. package/dist/tui/state/metrics-buffer.test.js +102 -0
  241. package/dist/tui/state/metrics-buffer.test.js.map +1 -0
  242. package/dist/tui/theme.js +136 -0
  243. package/dist/tui/theme.js.map +1 -0
  244. package/dist/tui/types.js +6 -0
  245. package/dist/tui/types.js.map +1 -0
  246. package/dist/tui/widgets/chaos-widget.js +152 -0
  247. package/dist/tui/widgets/chaos-widget.js.map +1 -0
  248. package/dist/tui/widgets/cluster-widget.js +156 -0
  249. package/dist/tui/widgets/cluster-widget.js.map +1 -0
  250. package/dist/tui/widgets/dlp-widget.js +161 -0
  251. package/dist/tui/widgets/dlp-widget.js.map +1 -0
  252. package/dist/tui/widgets/ghost-widget.js +169 -0
  253. package/dist/tui/widgets/ghost-widget.js.map +1 -0
  254. package/dist/tui/widgets/har-widget.js +173 -0
  255. package/dist/tui/widgets/har-widget.js.map +1 -0
  256. package/dist/tui/widgets/index.js +122 -0
  257. package/dist/tui/widgets/index.js.map +1 -0
  258. package/dist/tui/widgets/jwt-widget.js +177 -0
  259. package/dist/tui/widgets/jwt-widget.js.map +1 -0
  260. package/dist/tui/widgets/kv-widget.js +261 -0
  261. package/dist/tui/widgets/kv-widget.js.map +1 -0
  262. package/dist/tui/widgets/mtd-widget.js +181 -0
  263. package/dist/tui/widgets/mtd-widget.js.map +1 -0
  264. package/dist/tui/widgets/netdiag-widget.js +155 -0
  265. package/dist/tui/widgets/netdiag-widget.js.map +1 -0
  266. package/dist/tui/widgets/oidc-widget.js +162 -0
  267. package/dist/tui/widgets/oidc-widget.js.map +1 -0
  268. package/dist/tui/widgets/pcap-widget.js +239 -0
  269. package/dist/tui/widgets/pcap-widget.js.map +1 -0
  270. package/dist/tui/widgets/redteam-widget.js +155 -0
  271. package/dist/tui/widgets/redteam-widget.js.map +1 -0
  272. package/dist/tui/widgets/rps-gauge-widget.js +124 -0
  273. package/dist/tui/widgets/rps-gauge-widget.js.map +1 -0
  274. package/dist/tui/widgets/sentinel-widget.js +171 -0
  275. package/dist/tui/widgets/sentinel-widget.js.map +1 -0
  276. package/dist/tui/widgets/sparklines-widget.js +127 -0
  277. package/dist/tui/widgets/sparklines-widget.js.map +1 -0
  278. package/dist/tui/widgets/sysinfo-widget.js +197 -0
  279. package/dist/tui/widgets/sysinfo-widget.js.map +1 -0
  280. package/dist/tui/widgets/traffic-chart-widget.js +170 -0
  281. package/dist/tui/widgets/traffic-chart-widget.js.map +1 -0
  282. package/dist/tui/widgets/webhook-widget.js +259 -0
  283. package/dist/tui/widgets/webhook-widget.js.map +1 -0
  284. package/dist/utils/ip.js +18 -0
  285. package/dist/utils/ip.js.map +1 -0
  286. package/dist/victim/index.js +71 -0
  287. package/dist/victim/index.js.map +1 -0
  288. package/dist/webhook.js +88 -0
  289. package/dist/webhook.js.map +1 -0
  290. package/package.json +90 -0
  291. package/proto/echo.proto +19 -0
@@ -0,0 +1,1404 @@
1
+ import { request } from "undici";
2
+ import { chat } from "./client.js";
3
+ import { cfg } from "../config.js";
4
+ import { addObjectiveProgressSignal, addAction, addFinding, addThought, createSession, getSessionContext, getLatestSession, getSession, listReports, resetRedTeamStoreForTests, setSessionState, upsertSessionAsset, upsertSessionObservation, upsertSessionRelation, updateSession, } from "./report-store.js";
5
+ import { executeToolStep, resetToolExecutorForTests, stopAllActiveExperiments } from "../tool-executor.js";
6
+ import { logger } from "../logger.js";
7
+ import { DEFAULT_AUTOPILOT_PERSONA_ID, getAutopilotPersonaId, getAutopilotPersonaProfile, listAutopilotPersonaProfiles, } from "./personas.js";
8
+ const ALL_TOOLS = ["cluster.attack", "chaos.cpu", "chaos.memory", "mtd.rotate", "delay", "chaos.crash"];
9
+ const DEFAULT_ALLOWED_TOOLS = ["cluster.attack", "chaos.cpu", "chaos.memory", "mtd.rotate", "delay"];
10
+ const BLOCKED_ATTACK_PREFIXES = ["/api/redteam", "/api/simulator", "/api/attackers", "/cluster", "/chaos", "/scenarios", "/proxy", "/tarpit", "/blackhole", "/deception"];
11
+ const SNAPSHOT_CAPTURE_MAX_ATTEMPTS = 3;
12
+ const SNAPSHOT_CAPTURE_RETRY_DELAY_MS = 200;
13
+ const MEMORY_PROMPT_LIMITS = {
14
+ assets: 8,
15
+ observations: 8,
16
+ relations: 8,
17
+ progressSignals: 8,
18
+ textLength: 160,
19
+ };
20
+ const DEFENSE_BODY_SNIPPET_MAX = 220;
21
+ const DEFENSE_BODY_READ_MAX_BYTES = 4096;
22
+ const TARPIT_LATENCY_THRESHOLD_MS = 1200;
23
+ const UNDICI_TIMEOUT_ERROR_CODES = new Set([
24
+ "UND_ERR_HEADERS_TIMEOUT",
25
+ "UND_ERR_BODY_TIMEOUT",
26
+ "UND_ERR_CONNECT_TIMEOUT",
27
+ ]);
28
+ let activeControl = null;
29
+ let startingSession = false;
30
+ function clampNumber(value, fallback, min, max) {
31
+ const num = Number(value);
32
+ if (!Number.isFinite(num))
33
+ return fallback;
34
+ return Math.max(min, Math.min(max, Math.trunc(num)));
35
+ }
36
+ function normalizeBaseUrl(value, fallback) {
37
+ const fallbackUrl = new URL(fallback);
38
+ const normalizedFallback = `${fallbackUrl.protocol}//${fallbackUrl.host}`;
39
+ if (typeof value !== "string" || !value.trim())
40
+ return fallback;
41
+ try {
42
+ const parsed = new URL(value);
43
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:")
44
+ return fallback;
45
+ // Guardrail: autopilot may only target the same origin as the current server.
46
+ if (parsed.origin !== fallbackUrl.origin)
47
+ return normalizedFallback;
48
+ return `${parsed.protocol}//${parsed.host}`;
49
+ }
50
+ catch {
51
+ return normalizedFallback;
52
+ }
53
+ }
54
+ function getServerOrigin(req) {
55
+ const protocol = req.protocol === "https" ? "https" : "http";
56
+ const port = req.socket.localPort || cfg.portHttp1;
57
+ return `${protocol}://127.0.0.1:${port}`;
58
+ }
59
+ function extractJsonObject(text) {
60
+ const trimmed = text.trim();
61
+ if (trimmed.startsWith("{"))
62
+ return trimmed;
63
+ const fencedMatch = trimmed.match(/```json\s*([\s\S]*?)```/i);
64
+ if (fencedMatch && fencedMatch[1])
65
+ return fencedMatch[1].trim();
66
+ const objectMatch = trimmed.match(/\{[\s\S]*\}/);
67
+ return objectMatch ? objectMatch[0] : "";
68
+ }
69
+ function buildUrl(baseUrl, path) {
70
+ return new URL(path, `${baseUrl}/`).toString();
71
+ }
72
+ function parsePrometheusMetrics(raw) {
73
+ let requestCount = 0;
74
+ let errorCount = 0;
75
+ let latencySum = 0;
76
+ let latencyCount = 0;
77
+ const lines = raw.split(/\r?\n/);
78
+ for (const line of lines) {
79
+ const trimmed = line.trim();
80
+ if (!trimmed || trimmed.startsWith("#"))
81
+ continue;
82
+ if (trimmed.startsWith("echo_http_requests_total")) {
83
+ const value = Number(trimmed.split(" ").pop() || 0);
84
+ if (Number.isFinite(value))
85
+ requestCount += value;
86
+ const statusMatch = trimmed.match(/status_code="(\d{3})"/);
87
+ if (statusMatch && statusMatch[1]?.startsWith("5") && Number.isFinite(value)) {
88
+ errorCount += value;
89
+ }
90
+ }
91
+ if (trimmed.startsWith("echo_http_request_duration_seconds_sum")) {
92
+ const value = Number(trimmed.split(" ").pop() || 0);
93
+ if (Number.isFinite(value))
94
+ latencySum += value;
95
+ }
96
+ if (trimmed.startsWith("echo_http_request_duration_seconds_count")) {
97
+ const value = Number(trimmed.split(" ").pop() || 0);
98
+ if (Number.isFinite(value))
99
+ latencyCount += value;
100
+ }
101
+ }
102
+ return {
103
+ requestCount,
104
+ errorCount,
105
+ avgLatencyMs: latencyCount > 0 ? (latencySum / latencyCount) * 1000 : 0,
106
+ };
107
+ }
108
+ async function getHealthStatus(baseUrl) {
109
+ try {
110
+ const { statusCode } = await request(buildUrl(baseUrl, "/healthz"), {
111
+ method: "GET",
112
+ headersTimeout: 2500,
113
+ bodyTimeout: 2500,
114
+ });
115
+ return statusCode === 200;
116
+ }
117
+ catch {
118
+ return false;
119
+ }
120
+ }
121
+ async function captureRuntimeSnapshot(baseUrl, previous) {
122
+ const metricsResponse = await request(buildUrl(baseUrl, "/metrics"), {
123
+ method: "GET",
124
+ headersTimeout: 2500,
125
+ bodyTimeout: 2500,
126
+ });
127
+ const metricsRaw = await metricsResponse.body.text();
128
+ if (metricsResponse.statusCode !== 200) {
129
+ throw new Error(`Metrics endpoint returned ${metricsResponse.statusCode}`);
130
+ }
131
+ const sysInfoResponse = await request(buildUrl(baseUrl, "/sysinfo"), {
132
+ method: "GET",
133
+ headersTimeout: 2500,
134
+ bodyTimeout: 2500,
135
+ });
136
+ const sysInfoRaw = await sysInfoResponse.body.text();
137
+ if (sysInfoResponse.statusCode !== 200) {
138
+ throw new Error(`Sysinfo endpoint returned ${sysInfoResponse.statusCode}`);
139
+ }
140
+ const healthy = await getHealthStatus(baseUrl);
141
+ let sysInfo;
142
+ try {
143
+ sysInfo = JSON.parse(sysInfoRaw);
144
+ }
145
+ catch {
146
+ throw new Error("Sysinfo endpoint returned invalid JSON");
147
+ }
148
+ const parsedMetrics = parsePrometheusMetrics(metricsRaw);
149
+ const now = Date.now();
150
+ const previousAt = previous ? Date.parse(previous.capturedAt) : 0;
151
+ const elapsedSeconds = previousAt > 0 ? Math.max(0.001, (now - previousAt) / 1000) : 0;
152
+ const requestDelta = previous ? Math.max(0, parsedMetrics.requestCount - previous.requestCount) : 0;
153
+ const totalMemory = Number(sysInfo.memory?.total || 0);
154
+ const freeMemory = Number(sysInfo.memory?.free || 0);
155
+ const cpuCount = Math.max(1, Number(sysInfo.cpus || 1));
156
+ const loadAverage = Array.isArray(sysInfo.loadavg) ? Number(sysInfo.loadavg[0] || 0) : 0;
157
+ const cpuPercent = Math.max(0, Math.min(100, (loadAverage / cpuCount) * 100));
158
+ const memPercent = totalMemory > 0
159
+ ? Math.max(0, Math.min(100, ((totalMemory - freeMemory) / totalMemory) * 100))
160
+ : 0;
161
+ return {
162
+ capturedAt: new Date(now).toISOString(),
163
+ rps: elapsedSeconds > 0 ? requestDelta / elapsedSeconds : 0,
164
+ requestCount: parsedMetrics.requestCount,
165
+ errorCount: parsedMetrics.errorCount,
166
+ errorRate: parsedMetrics.requestCount > 0 ? parsedMetrics.errorCount / parsedMetrics.requestCount : 0,
167
+ avgLatencyMs: parsedMetrics.avgLatencyMs,
168
+ cpuPercent,
169
+ memPercent,
170
+ healthy,
171
+ };
172
+ }
173
+ function asError(value) {
174
+ if (value instanceof Error)
175
+ return value;
176
+ if (typeof value === "string")
177
+ return new Error(value);
178
+ return new Error("Unknown error");
179
+ }
180
+ function sleep(ms) {
181
+ return new Promise((resolve) => setTimeout(resolve, ms));
182
+ }
183
+ async function captureRuntimeSnapshotWithRetry(baseUrl, previous, options = {}) {
184
+ const maxAttempts = Math.max(1, options.maxAttempts ?? SNAPSHOT_CAPTURE_MAX_ATTEMPTS);
185
+ const retryDelayMs = Math.max(0, options.retryDelayMs ?? SNAPSHOT_CAPTURE_RETRY_DELAY_MS);
186
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
187
+ try {
188
+ const snapshot = await captureRuntimeSnapshot(baseUrl, previous);
189
+ if (attempt > 1) {
190
+ options.onRecovered?.(attempt, maxAttempts);
191
+ }
192
+ return snapshot;
193
+ }
194
+ catch (error) {
195
+ const err = asError(error);
196
+ if (attempt >= maxAttempts) {
197
+ throw new Error(`Telemetry capture failed after ${maxAttempts} attempts: ${err.message}`);
198
+ }
199
+ options.onRetry?.(err, attempt, maxAttempts);
200
+ const backoff = retryDelayMs * attempt;
201
+ if (backoff > 0) {
202
+ await sleep(backoff);
203
+ }
204
+ }
205
+ }
206
+ throw new Error("Telemetry capture failed unexpectedly");
207
+ }
208
+ function pickTargetPath(objective) {
209
+ const pathMatch = objective.match(/\/[a-zA-Z0-9/_-]*/);
210
+ if (pathMatch && pathMatch[0] && isSafeAttackPath(pathMatch[0]))
211
+ return pathMatch[0];
212
+ return "/echo";
213
+ }
214
+ function isSafeAttackPath(pathname) {
215
+ if (!pathname.startsWith("/"))
216
+ return false;
217
+ return !BLOCKED_ATTACK_PREFIXES.some((prefix) => pathname === prefix || pathname.startsWith(`${prefix}/`));
218
+ }
219
+ function parseTargetPath(value) {
220
+ if (typeof value !== "string" || !value.trim())
221
+ return null;
222
+ try {
223
+ const parsed = new URL(value);
224
+ if (!isSafeAttackPath(parsed.pathname))
225
+ return null;
226
+ return parsed.pathname;
227
+ }
228
+ catch {
229
+ return null;
230
+ }
231
+ }
232
+ function truncateText(value, maxLength) {
233
+ if (value.length <= maxLength)
234
+ return value;
235
+ return `${value.slice(0, maxLength - 3)}...`;
236
+ }
237
+ async function readBodySnippet(body, maxBytes) {
238
+ const chunks = [];
239
+ let totalBytes = 0;
240
+ try {
241
+ for await (const chunk of body) {
242
+ const asBuffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
243
+ const remaining = maxBytes - totalBytes;
244
+ if (remaining <= 0)
245
+ break;
246
+ if (asBuffer.length > remaining) {
247
+ chunks.push(asBuffer.subarray(0, remaining));
248
+ totalBytes += remaining;
249
+ break;
250
+ }
251
+ chunks.push(asBuffer);
252
+ totalBytes += asBuffer.length;
253
+ if (totalBytes >= maxBytes)
254
+ break;
255
+ }
256
+ }
257
+ finally {
258
+ if (typeof body.destroy === "function") {
259
+ body.destroy();
260
+ }
261
+ }
262
+ return Buffer.concat(chunks).toString("utf8");
263
+ }
264
+ function lastItems(items, max) {
265
+ if (items.length <= max)
266
+ return [...items];
267
+ return items.slice(items.length - max);
268
+ }
269
+ function buildPlannerMemorySummary(sessionId) {
270
+ const context = getSessionContext(sessionId);
271
+ if (!context)
272
+ return null;
273
+ return {
274
+ totals: {
275
+ assets: context.assets.length,
276
+ observations: context.observations.length,
277
+ relations: context.relations.length,
278
+ },
279
+ recentAssets: lastItems(context.assets, MEMORY_PROMPT_LIMITS.assets).map((asset) => ({
280
+ type: asset.type,
281
+ value: truncateText(asset.value, MEMORY_PROMPT_LIMITS.textLength),
282
+ source: asset.source,
283
+ confidence: Number(asset.confidence.toFixed(2)),
284
+ })),
285
+ recentObservations: lastItems(context.observations, MEMORY_PROMPT_LIMITS.observations).map((item) => ({
286
+ kind: item.kind,
287
+ source: item.source,
288
+ summary: truncateText(item.summary, MEMORY_PROMPT_LIMITS.textLength),
289
+ })),
290
+ recentRelations: lastItems(context.relations, MEMORY_PROMPT_LIMITS.relations).map((relation) => ({
291
+ type: relation.type,
292
+ from: truncateText(relation.fromAssetId, MEMORY_PROMPT_LIMITS.textLength),
293
+ to: truncateText(relation.toAssetId, MEMORY_PROMPT_LIMITS.textLength),
294
+ })),
295
+ objectiveProgress: {
296
+ preconditionsMet: lastItems(context.objectiveProgress.preconditionsMet, MEMORY_PROMPT_LIMITS.progressSignals),
297
+ openedPaths: lastItems(context.objectiveProgress.openedPaths, MEMORY_PROMPT_LIMITS.progressSignals),
298
+ breakSignals: lastItems(context.objectiveProgress.breakSignals, MEMORY_PROMPT_LIMITS.progressSignals),
299
+ },
300
+ };
301
+ }
302
+ function composePlannerPayload(control, snapshot, iteration, memory, recentDefenseFeedback) {
303
+ const persona = getAutopilotPersonaProfile(control.persona);
304
+ return {
305
+ objective: control.objective,
306
+ iteration,
307
+ telemetry: snapshot,
308
+ // Keep persona metadata prompt-safe in case registry content becomes externally configurable later.
309
+ persona: {
310
+ id: persona.id,
311
+ label: sanitizePromptFragment(persona.label, 80),
312
+ tags: persona.tags.map((tag) => sanitizePromptFragment(tag, 48)),
313
+ },
314
+ guardrails: {
315
+ allowedTools: control.allowedTools,
316
+ forbidCrashByDefault: !control.allowedTools.includes("chaos.crash"),
317
+ },
318
+ memory,
319
+ recentDefenseFeedback,
320
+ };
321
+ }
322
+ function stableHash(input) {
323
+ let hash = 2166136261;
324
+ for (let i = 0; i < input.length; i++) {
325
+ hash ^= input.charCodeAt(i);
326
+ hash = Math.imul(hash, 16777619);
327
+ }
328
+ return hash >>> 0;
329
+ }
330
+ function deterministicRoll(sessionId, iteration, salt) {
331
+ const hash = stableHash(`${sessionId}:${iteration}:${salt}`);
332
+ return (hash % 10000) / 10000;
333
+ }
334
+ function pickPersonaWeightedTool(control, iteration) {
335
+ if (control.allowedTools.length === 0)
336
+ return "none";
337
+ const persona = getAutopilotPersonaProfile(control.persona);
338
+ const weighted = control.allowedTools
339
+ .map((tool) => ({
340
+ tool,
341
+ weight: Math.max(0, Number(persona.toolWeights[tool] ?? 1)),
342
+ }))
343
+ .filter((item) => item.weight > 0);
344
+ const totalWeight = weighted.reduce((sum, item) => sum + item.weight, 0);
345
+ if (totalWeight <= 0) {
346
+ return control.allowedTools[0] || "none";
347
+ }
348
+ const roll = deterministicRoll(control.sessionId, iteration, `persona-weight:${persona.id}`) * totalWeight;
349
+ let cursor = 0;
350
+ for (const item of weighted) {
351
+ cursor += item.weight;
352
+ if (roll < cursor) {
353
+ return item.tool;
354
+ }
355
+ }
356
+ return weighted[weighted.length - 1]?.tool || "none";
357
+ }
358
+ function applyPersonaBias(decision, control, iteration) {
359
+ if (decision.tool === "none")
360
+ return decision;
361
+ const persona = getAutopilotPersonaProfile(control.persona);
362
+ const shouldBias = deterministicRoll(control.sessionId, iteration, `persona-bias:${persona.id}`) < persona.biasProbability;
363
+ if (!shouldBias)
364
+ return decision;
365
+ const weightedTool = pickPersonaWeightedTool(control, iteration);
366
+ if (weightedTool === "none" || weightedTool === decision.tool)
367
+ return decision;
368
+ const biased = sanitizeDecision({
369
+ thought: `${decision.thought} Persona bias(${persona.label}) pivoted tool choice toward ${weightedTool}.`,
370
+ reason: `${decision.reason} Persona bias weighting applied for ${persona.id}.`,
371
+ tool: weightedTool,
372
+ // Reset params when the tool changes so sanitizer can apply safe defaults for the new tool.
373
+ params: {},
374
+ rawModelOutput: decision.rawModelOutput,
375
+ maneuver: decision.maneuver,
376
+ }, control);
377
+ return {
378
+ ...biased,
379
+ rawModelOutput: decision.rawModelOutput,
380
+ };
381
+ }
382
+ function buildSystemPrompt(control) {
383
+ const persona = getAutopilotPersonaProfile(control.persona);
384
+ // Defense-in-depth: persona content is currently registry-owned, but sanitize prompt fragments if this becomes configurable later.
385
+ const safePersonaLabel = sanitizePromptFragment(persona.label, 80);
386
+ const safePersonaTags = persona.tags.map((tag) => sanitizePromptFragment(tag, 48));
387
+ const personaDirectives = persona.promptDirectives.map((directive) => `Persona directive: ${sanitizePromptFragment(directive, 200)}`);
388
+ return [
389
+ "You are an autonomous reliability red-team strategist.",
390
+ `Active persona: ${safePersonaLabel} (${persona.id}).`,
391
+ `Persona tags: ${safePersonaTags.join(", ") || "none"}.`,
392
+ ...personaDirectives,
393
+ "Choose one tool action based on telemetry and objective.",
394
+ "Output only strict JSON with keys: thought, reason, tool, params.",
395
+ `Allowed tools: ${control.allowedTools.join(", ")}`,
396
+ "tool must be one of allowed tools or 'none'.",
397
+ ].join(" ");
398
+ }
399
+ function sanitizePromptFragment(value, maxLength) {
400
+ if (typeof value !== "string")
401
+ return "";
402
+ return value
403
+ .replace(/[\u0000-\u001F\u007F]/g, " ")
404
+ .replace(/[\u200B-\u200F\u202A-\u202E\u2066-\u2069\uFEFF]/g, "")
405
+ .replace(/[\u{E0000}-\u{E007F}]/gu, "")
406
+ .replace(/\s+/g, " ")
407
+ .trim()
408
+ .slice(0, Math.max(1, maxLength));
409
+ }
410
+ function shouldPauseForBreakSignals(memory) {
411
+ return (memory?.objectiveProgress.breakSignals.length || 0) > 0;
412
+ }
413
+ function safeCaptureMemory(sessionId, phase, callback) {
414
+ try {
415
+ callback();
416
+ }
417
+ catch (error) {
418
+ logger.warn({ sessionId, phase, error: error?.message || "unknown error" }, "Autopilot memory capture failed");
419
+ }
420
+ }
421
+ function captureActionMemory(args) {
422
+ const objectivePath = pickTargetPath(args.objective);
423
+ const objectiveAsset = upsertSessionAsset(args.sessionId, {
424
+ type: "endpoint",
425
+ value: objectivePath,
426
+ source: "objective",
427
+ confidence: 0.7,
428
+ metadata: { iteration: args.iteration },
429
+ });
430
+ addObjectiveProgressSignal(args.sessionId, "openedPaths", objectivePath);
431
+ if (args.decision.tool === "none") {
432
+ upsertSessionObservation(args.sessionId, {
433
+ kind: "system",
434
+ source: "planner",
435
+ summary: `Iteration ${args.iteration}: no tool selected`,
436
+ details: {
437
+ iteration: args.iteration,
438
+ reason: args.decision.reason,
439
+ },
440
+ });
441
+ return;
442
+ }
443
+ const summary = args.execution
444
+ ? (args.execution.ok ? args.execution.message : `Tool failed: ${args.execution.message}`)
445
+ : `Tool selected: ${args.decision.tool}`;
446
+ upsertSessionObservation(args.sessionId, {
447
+ kind: "tool-output",
448
+ source: args.decision.tool,
449
+ summary,
450
+ details: {
451
+ iteration: args.iteration,
452
+ ok: args.execution?.ok ?? true,
453
+ reason: args.decision.reason,
454
+ params: args.decision.params,
455
+ error: args.execution?.error,
456
+ maneuver: args.decision.maneuver,
457
+ },
458
+ });
459
+ if (args.decision.tool === "cluster.attack") {
460
+ const targetPath = parseTargetPath(args.decision.params.target);
461
+ if (targetPath) {
462
+ const targetAsset = upsertSessionAsset(args.sessionId, {
463
+ type: "endpoint",
464
+ value: targetPath,
465
+ source: "cluster.attack",
466
+ confidence: 0.8,
467
+ metadata: {
468
+ iteration: args.iteration,
469
+ target: args.decision.params.target,
470
+ },
471
+ });
472
+ if (objectiveAsset?.id && targetAsset?.id) {
473
+ upsertSessionRelation(args.sessionId, {
474
+ type: "targets",
475
+ fromAssetId: objectiveAsset.id,
476
+ toAssetId: targetAsset.id,
477
+ source: "cluster.attack",
478
+ confidence: 0.7,
479
+ metadata: { iteration: args.iteration },
480
+ });
481
+ }
482
+ }
483
+ }
484
+ if (args.execution && !args.execution.ok) {
485
+ addObjectiveProgressSignal(args.sessionId, "breakSignals", `tool-failure:${args.decision.tool}`);
486
+ upsertSessionAsset(args.sessionId, {
487
+ type: "indicator",
488
+ value: `tool-failure:${args.decision.tool}`,
489
+ source: args.decision.tool,
490
+ confidence: 0.8,
491
+ metadata: {
492
+ iteration: args.iteration,
493
+ message: args.execution.message,
494
+ },
495
+ });
496
+ }
497
+ }
498
+ function captureVerificationMemory(args) {
499
+ const objectivePath = pickTargetPath(args.objective);
500
+ const objectiveAsset = upsertSessionAsset(args.sessionId, {
501
+ type: "endpoint",
502
+ value: objectivePath,
503
+ source: "objective",
504
+ confidence: 0.7,
505
+ metadata: { iteration: args.iteration },
506
+ });
507
+ upsertSessionObservation(args.sessionId, {
508
+ kind: "verification",
509
+ source: "verification",
510
+ summary: args.verification.notes,
511
+ details: {
512
+ iteration: args.iteration,
513
+ broken: args.verification.broken,
514
+ crashDetected: args.verification.crashDetected,
515
+ newServerErrors: args.verification.newServerErrors,
516
+ telemetry: {
517
+ rps: args.after.rps,
518
+ errorRate: args.after.errorRate,
519
+ avgLatencyMs: args.after.avgLatencyMs,
520
+ },
521
+ },
522
+ });
523
+ if (args.verification.crashDetected) {
524
+ const crashAsset = upsertSessionAsset(args.sessionId, {
525
+ type: "indicator",
526
+ value: "service-health-check-failed",
527
+ source: "verification",
528
+ confidence: 0.95,
529
+ metadata: { iteration: args.iteration },
530
+ });
531
+ addObjectiveProgressSignal(args.sessionId, "breakSignals", "service-health-check-failed");
532
+ if (objectiveAsset?.id && crashAsset?.id) {
533
+ upsertSessionRelation(args.sessionId, {
534
+ type: "confirms",
535
+ fromAssetId: objectiveAsset.id,
536
+ toAssetId: crashAsset.id,
537
+ source: "verification",
538
+ confidence: 0.9,
539
+ metadata: { iteration: args.iteration },
540
+ });
541
+ }
542
+ }
543
+ if (args.verification.newServerErrors > 0) {
544
+ const errorAsset = upsertSessionAsset(args.sessionId, {
545
+ type: "vuln",
546
+ value: `new-5xx-errors:${args.verification.newServerErrors}`,
547
+ source: "verification",
548
+ confidence: 0.85,
549
+ metadata: {
550
+ iteration: args.iteration,
551
+ newServerErrors: args.verification.newServerErrors,
552
+ },
553
+ });
554
+ addObjectiveProgressSignal(args.sessionId, "breakSignals", `new-5xx-errors:${args.verification.newServerErrors}`);
555
+ if (objectiveAsset?.id && errorAsset?.id) {
556
+ upsertSessionRelation(args.sessionId, {
557
+ type: "escalates_to",
558
+ fromAssetId: objectiveAsset.id,
559
+ toAssetId: errorAsset.id,
560
+ source: "verification",
561
+ confidence: 0.85,
562
+ metadata: { iteration: args.iteration },
563
+ });
564
+ }
565
+ }
566
+ if (args.verification.broken) {
567
+ addObjectiveProgressSignal(args.sessionId, "breakSignals", args.verification.notes);
568
+ }
569
+ else {
570
+ addObjectiveProgressSignal(args.sessionId, "preconditionsMet", "no-break-detected");
571
+ }
572
+ if (args.defenseFeedback) {
573
+ upsertSessionObservation(args.sessionId, {
574
+ kind: "objective-progress",
575
+ source: "defense-feedback",
576
+ summary: `Defense signal: ${args.defenseFeedback.signal}`,
577
+ details: {
578
+ iteration: args.iteration,
579
+ targetPath: args.defenseFeedback.targetPath,
580
+ signal: args.defenseFeedback.signal,
581
+ reason: args.defenseFeedback.reason,
582
+ statusCode: args.defenseFeedback.statusCode,
583
+ latencyMs: args.defenseFeedback.latencyMs,
584
+ basedOnTool: args.defenseFeedback.basedOnTool,
585
+ toolFailed: args.defenseFeedback.toolFailed,
586
+ probeError: args.defenseFeedback.probeError,
587
+ },
588
+ });
589
+ if (args.defenseFeedback.signal !== "none") {
590
+ const signalValue = `defense-signal:${args.defenseFeedback.signal}`;
591
+ addObjectiveProgressSignal(args.sessionId, "breakSignals", signalValue);
592
+ const defenseSignalAsset = upsertSessionAsset(args.sessionId, {
593
+ type: "indicator",
594
+ value: signalValue,
595
+ source: "defense-feedback",
596
+ confidence: 0.75,
597
+ metadata: {
598
+ iteration: args.iteration,
599
+ targetPath: args.defenseFeedback.targetPath,
600
+ statusCode: args.defenseFeedback.statusCode,
601
+ latencyMs: args.defenseFeedback.latencyMs,
602
+ },
603
+ });
604
+ if (objectiveAsset?.id && defenseSignalAsset?.id) {
605
+ upsertSessionRelation(args.sessionId, {
606
+ type: "related_to",
607
+ fromAssetId: objectiveAsset.id,
608
+ toAssetId: defenseSignalAsset.id,
609
+ source: "defense-feedback",
610
+ confidence: 0.7,
611
+ metadata: { iteration: args.iteration },
612
+ });
613
+ }
614
+ }
615
+ }
616
+ }
617
+ function buildPolicyPrefix(iteration) {
618
+ return `rt${iteration.toString(36)}${Date.now().toString(36).slice(-4)}`;
619
+ }
620
+ function selectPolicyDecision(control, recentDefenseFeedback, iteration) {
621
+ if (!recentDefenseFeedback || recentDefenseFeedback.signal === "none") {
622
+ return null;
623
+ }
624
+ const blockSignal = recentDefenseFeedback.signal === "waf_blocked" || recentDefenseFeedback.signal === "mtd_hidden_route";
625
+ const canDelay = control.allowedTools.includes("delay");
626
+ const canRotateMtd = control.allowedTools.includes("mtd.rotate") && recentDefenseFeedback.basedOnTool !== "mtd.rotate";
627
+ if (recentDefenseFeedback.signal === "rate_limited") {
628
+ if (canDelay) {
629
+ const duration = clampNumber(Math.max(control.intervalMs, 500) + 500, 1500, 1500, 120000);
630
+ return {
631
+ thought: "Recent probe was rate-limited (429). Applying anti-rate-limit backoff before next maneuver.",
632
+ reason: "Rate limiting detected; slow down to evade throttling and preserve probe fidelity.",
633
+ tool: "delay",
634
+ params: { duration },
635
+ rawModelOutput: "policy:rate_limited",
636
+ maneuver: {
637
+ triggerSignal: recentDefenseFeedback.signal,
638
+ countermeasure: "delay",
639
+ rationale: "Rate limiting detected; slow down to evade throttling and preserve probe fidelity.",
640
+ },
641
+ };
642
+ }
643
+ return {
644
+ thought: "Rate limiting detected but delay is unavailable in allowed tools.",
645
+ reason: "No safe backoff tool available for anti-rate-limit policy.",
646
+ tool: "none",
647
+ params: {},
648
+ rawModelOutput: "policy:rate_limited:none",
649
+ maneuver: {
650
+ triggerSignal: recentDefenseFeedback.signal,
651
+ countermeasure: "none",
652
+ rationale: "No safe backoff tool available for anti-rate-limit policy.",
653
+ },
654
+ };
655
+ }
656
+ if (blockSignal) {
657
+ if (canRotateMtd) {
658
+ return {
659
+ thought: "Defensive block detected on the current path. Rotating MTD prefix to pivot route strategy.",
660
+ reason: `Defense signal ${recentDefenseFeedback.signal} suggests route-targeted blocking.`,
661
+ tool: "mtd.rotate",
662
+ params: { prefix: buildPolicyPrefix(iteration) },
663
+ rawModelOutput: `policy:${recentDefenseFeedback.signal}:mtd_rotate`,
664
+ maneuver: {
665
+ triggerSignal: recentDefenseFeedback.signal,
666
+ countermeasure: "mtd.rotate",
667
+ rationale: `Defense signal ${recentDefenseFeedback.signal} suggests route-targeted blocking.`,
668
+ },
669
+ };
670
+ }
671
+ if (canDelay) {
672
+ return {
673
+ thought: "Defensive block detected, but MTD rotation is unavailable. Cooling down before the next pivot.",
674
+ reason: `Defense signal ${recentDefenseFeedback.signal} detected with no rotate capability.`,
675
+ tool: "delay",
676
+ params: { duration: clampNumber(Math.max(control.intervalMs, 250), 900, 250, 120000) },
677
+ rawModelOutput: `policy:${recentDefenseFeedback.signal}:delay`,
678
+ maneuver: {
679
+ triggerSignal: recentDefenseFeedback.signal,
680
+ countermeasure: "delay",
681
+ rationale: `Defense signal ${recentDefenseFeedback.signal} detected with no rotate capability.`,
682
+ },
683
+ };
684
+ }
685
+ }
686
+ if (recentDefenseFeedback.signal === "tarpit_suspected") {
687
+ if (canDelay) {
688
+ return {
689
+ thought: "Latency pattern suggests tarpit behavior. Reducing action tempo to avoid repeated stall traps.",
690
+ reason: "Tarpit suspected from elevated probe latency.",
691
+ tool: "delay",
692
+ params: { duration: clampNumber(Math.max(control.intervalMs, 500) + 250, 1200, 250, 120000) },
693
+ rawModelOutput: "policy:tarpit_suspected",
694
+ maneuver: {
695
+ triggerSignal: recentDefenseFeedback.signal,
696
+ countermeasure: "delay",
697
+ rationale: "Tarpit suspected from elevated probe latency.",
698
+ },
699
+ };
700
+ }
701
+ return {
702
+ thought: "Tarpit suspected, but no delay tool is allowed. Skipping action this cycle.",
703
+ reason: "No safe anti-tarpit tool available.",
704
+ tool: "none",
705
+ params: {},
706
+ rawModelOutput: "policy:tarpit_suspected:none",
707
+ maneuver: {
708
+ triggerSignal: recentDefenseFeedback.signal,
709
+ countermeasure: "none",
710
+ rationale: "No safe anti-tarpit tool available.",
711
+ },
712
+ };
713
+ }
714
+ if (recentDefenseFeedback.signal === "probe_failed" && canDelay) {
715
+ return {
716
+ thought: "Defense probe failed in the previous cycle. Pausing briefly before retrying.",
717
+ reason: "Probe instability detected; short backoff to stabilize signal collection.",
718
+ tool: "delay",
719
+ params: { duration: clampNumber(Math.max(control.intervalMs, 250), 750, 250, 120000) },
720
+ rawModelOutput: "policy:probe_failed",
721
+ maneuver: {
722
+ triggerSignal: recentDefenseFeedback.signal,
723
+ countermeasure: "delay",
724
+ rationale: "Probe instability detected; short backoff to stabilize signal collection.",
725
+ },
726
+ };
727
+ }
728
+ return null;
729
+ }
730
+ function fallbackDecision(snapshot, control, iteration) {
731
+ const attackTarget = buildUrl(control.baseUrl, pickTargetPath(control.objective));
732
+ if (!snapshot.healthy || snapshot.errorRate > 0.05) {
733
+ return {
734
+ thought: "System health is degraded or errors are rising. Stabilize briefly before the next move.",
735
+ reason: "Safety pause after instability",
736
+ tool: control.allowedTools.includes("delay") ? "delay" : "none",
737
+ params: { duration: 1200 },
738
+ rawModelOutput: "fallback:degraded",
739
+ };
740
+ }
741
+ if (snapshot.avgLatencyMs < 400 && control.allowedTools.includes("cluster.attack")) {
742
+ return {
743
+ thought: "Latency is still low. Increase pressure with a larger request flood.",
744
+ reason: "Escalate traffic until latency or errors rise",
745
+ tool: "cluster.attack",
746
+ params: {
747
+ target: attackTarget,
748
+ rate: Math.min(1200, 150 + iteration * 120),
749
+ },
750
+ rawModelOutput: "fallback:cluster_attack",
751
+ };
752
+ }
753
+ if (snapshot.cpuPercent < 85 && control.allowedTools.includes("chaos.cpu")) {
754
+ return {
755
+ thought: "CPU headroom remains. Inject a focused CPU spike.",
756
+ reason: "Probe compute resilience",
757
+ tool: "chaos.cpu",
758
+ params: { duration: 4000 },
759
+ rawModelOutput: "fallback:cpu",
760
+ };
761
+ }
762
+ if (snapshot.memPercent < 85 && control.allowedTools.includes("chaos.memory")) {
763
+ return {
764
+ thought: "Memory usage is still moderate. Allocate additional memory pressure.",
765
+ reason: "Probe memory resilience",
766
+ tool: "chaos.memory",
767
+ params: { action: "allocate", amount: 128 },
768
+ rawModelOutput: "fallback:memory",
769
+ };
770
+ }
771
+ return {
772
+ thought: "No strong escalation signal available. Waiting for more telemetry.",
773
+ reason: "No-op wait",
774
+ tool: control.allowedTools.includes("delay") ? "delay" : "none",
775
+ params: { duration: 1000 },
776
+ rawModelOutput: "fallback:delay",
777
+ };
778
+ }
779
+ function sanitizeDecision(candidate, control) {
780
+ let nextTool = candidate.tool;
781
+ if (nextTool !== "none" && !control.allowedTools.includes(nextTool)) {
782
+ nextTool = control.allowedTools.includes("delay") ? "delay" : control.allowedTools[0] || "none";
783
+ }
784
+ const nextParams = { ...candidate.params };
785
+ const nextManeuver = candidate.maneuver
786
+ ? {
787
+ ...candidate.maneuver,
788
+ countermeasure: nextTool,
789
+ }
790
+ : undefined;
791
+ if (nextTool === "cluster.attack") {
792
+ const fallbackTarget = buildUrl(control.baseUrl, pickTargetPath(control.objective));
793
+ let target = fallbackTarget;
794
+ if (typeof nextParams.target === "string") {
795
+ try {
796
+ const parsedTarget = new URL(nextParams.target);
797
+ const parsedBase = new URL(control.baseUrl);
798
+ if (parsedTarget.origin === parsedBase.origin && isSafeAttackPath(parsedTarget.pathname)) {
799
+ target = parsedTarget.toString();
800
+ }
801
+ }
802
+ catch {
803
+ target = fallbackTarget;
804
+ }
805
+ }
806
+ const rate = clampNumber(nextParams.rate, 150, 1, 2000);
807
+ return { ...candidate, tool: nextTool, params: { target, rate }, maneuver: nextManeuver };
808
+ }
809
+ if (nextTool === "chaos.cpu") {
810
+ const duration = clampNumber(nextParams.duration, 5000, 250, 120000);
811
+ return { ...candidate, tool: nextTool, params: { duration }, maneuver: nextManeuver };
812
+ }
813
+ if (nextTool === "chaos.memory") {
814
+ const action = nextParams.action === "clear" ? "clear" : "allocate";
815
+ const amount = clampNumber(nextParams.amount, 128, 1, 8192);
816
+ return { ...candidate, tool: nextTool, params: { action, amount }, maneuver: nextManeuver };
817
+ }
818
+ if (nextTool === "mtd.rotate") {
819
+ const prefix = typeof nextParams.prefix === "string" ? nextParams.prefix : `rt${Date.now().toString(36).slice(-4)}`;
820
+ return { ...candidate, tool: nextTool, params: { prefix }, maneuver: nextManeuver };
821
+ }
822
+ if (nextTool === "delay") {
823
+ const duration = clampNumber(nextParams.duration, 1000, 10, 120000);
824
+ return { ...candidate, tool: nextTool, params: { duration }, maneuver: nextManeuver };
825
+ }
826
+ if (nextTool === "chaos.crash") {
827
+ const delayMs = clampNumber(nextParams.delayMs, 1000, 100, 30000);
828
+ return { ...candidate, tool: nextTool, params: { delayMs }, maneuver: nextManeuver };
829
+ }
830
+ return { ...candidate, tool: "none", params: {}, maneuver: nextManeuver };
831
+ }
832
+ async function decideNextAction(control, snapshot, iteration, memory, recentDefenseFeedback) {
833
+ const fallback = applyPersonaBias(fallbackDecision(snapshot, control, iteration), control, iteration);
834
+ const policyDecision = selectPolicyDecision(control, recentDefenseFeedback, iteration);
835
+ if (policyDecision) {
836
+ // Policy maneuvers intentionally bypass persona bias to preserve deterministic safety/evasion rules.
837
+ return sanitizeDecision(policyDecision, control);
838
+ }
839
+ if (shouldPauseForBreakSignals(memory)) {
840
+ // Break-signal pauses intentionally force a no-op (or safe delay fallback) and are not persona-biased.
841
+ return sanitizeDecision({
842
+ thought: "Prior break signals were already detected. Pause briefly before further escalation.",
843
+ reason: "Session memory indicates break conditions",
844
+ tool: control.allowedTools.includes("delay") ? "delay" : "none",
845
+ params: { duration: 750 },
846
+ rawModelOutput: "memory:break-signals",
847
+ }, control);
848
+ }
849
+ const systemPrompt = buildSystemPrompt(control);
850
+ const userPrompt = JSON.stringify(composePlannerPayload(control, snapshot, iteration, memory, recentDefenseFeedback));
851
+ try {
852
+ const response = await chat(`autopilot-${control.sessionId}`, systemPrompt, userPrompt);
853
+ const jsonText = extractJsonObject(response);
854
+ if (!jsonText) {
855
+ return fallback;
856
+ }
857
+ const parsed = JSON.parse(jsonText);
858
+ const candidate = {
859
+ thought: typeof parsed.thought === "string" ? parsed.thought : fallback.thought,
860
+ reason: typeof parsed.reason === "string" ? parsed.reason : fallback.reason,
861
+ tool: (typeof parsed.tool === "string" ? parsed.tool : fallback.tool),
862
+ params: typeof parsed.params === "object" && parsed.params !== null ? parsed.params : fallback.params,
863
+ rawModelOutput: response,
864
+ };
865
+ return applyPersonaBias(sanitizeDecision(candidate, control), control, iteration);
866
+ }
867
+ catch {
868
+ return fallback;
869
+ }
870
+ }
871
+ function summarizeVerification(base) {
872
+ const newErrors = Math.max(0, base.after.errorCount - base.before.errorCount);
873
+ const crashDetected = !base.healthAfter;
874
+ const broken = crashDetected || newErrors > 0;
875
+ const notes = crashDetected
876
+ ? "Service health check failed after action."
877
+ : newErrors > 0
878
+ ? `Observed ${newErrors} new 5xx errors after action.`
879
+ : "No crash or new 5xx errors observed.";
880
+ return {
881
+ broken,
882
+ crashDetected,
883
+ newServerErrors: newErrors,
884
+ notes,
885
+ };
886
+ }
887
+ function classifyDefenseSignal(input) {
888
+ if (input.probeError) {
889
+ return {
890
+ signal: "probe_failed",
891
+ reason: `Defense probe failed: ${input.probeError}`,
892
+ };
893
+ }
894
+ if (input.statusCode === 429) {
895
+ return {
896
+ signal: "rate_limited",
897
+ reason: "Received HTTP 429 from objective endpoint.",
898
+ };
899
+ }
900
+ if (input.statusCode === 404) {
901
+ return {
902
+ signal: "mtd_hidden_route",
903
+ reason: "Received HTTP 404 from objective endpoint (possible MTD route hiding; verify prior reachability).",
904
+ };
905
+ }
906
+ if (input.statusCode === 403 || input.statusCode === 406) {
907
+ return {
908
+ signal: "waf_blocked",
909
+ reason: `Received HTTP ${input.statusCode} from objective endpoint.`,
910
+ };
911
+ }
912
+ if (typeof input.statusCode === "number" && input.statusCode >= 500) {
913
+ return {
914
+ signal: "server_error",
915
+ reason: `Received HTTP ${input.statusCode} from objective endpoint.`,
916
+ };
917
+ }
918
+ if (typeof input.latencyMs === "number" && input.latencyMs >= TARPIT_LATENCY_THRESHOLD_MS) {
919
+ return {
920
+ signal: "tarpit_suspected",
921
+ reason: `Observed elevated latency (${input.latencyMs}ms).`,
922
+ };
923
+ }
924
+ return {
925
+ signal: "none",
926
+ reason: "No explicit defense signal detected in objective probe.",
927
+ };
928
+ }
929
+ async function captureDefenseFeedback(control, decision, execution) {
930
+ const targetPath = pickTargetPath(control.objective);
931
+ const targetUrl = buildUrl(control.baseUrl, targetPath);
932
+ const probeStartedAt = Date.now();
933
+ try {
934
+ const response = await request(targetUrl, {
935
+ method: "GET",
936
+ headersTimeout: 2500,
937
+ bodyTimeout: 2500,
938
+ });
939
+ const latencyMs = Math.max(0, Date.now() - probeStartedAt);
940
+ const body = await readBodySnippet(response.body, DEFENSE_BODY_READ_MAX_BYTES);
941
+ const bodySnippet = truncateText(body.replace(/\s+/g, " ").trim(), DEFENSE_BODY_SNIPPET_MAX);
942
+ const classified = classifyDefenseSignal({
943
+ statusCode: response.statusCode,
944
+ latencyMs,
945
+ });
946
+ return {
947
+ capturedAt: new Date().toISOString(),
948
+ targetPath,
949
+ statusCode: response.statusCode,
950
+ bodySnippet: bodySnippet || undefined,
951
+ latencyMs,
952
+ signal: classified.signal,
953
+ reason: classified.reason,
954
+ basedOnTool: decision.tool,
955
+ toolFailed: Boolean(execution && !execution.ok),
956
+ };
957
+ }
958
+ catch (error) {
959
+ const message = error?.message || "unknown error";
960
+ const elapsedMs = Math.max(0, Date.now() - probeStartedAt);
961
+ const lowerMessage = String(message).toLowerCase();
962
+ const errorCode = typeof error?.code === "string" ? error.code : "";
963
+ const isTimeout = UNDICI_TIMEOUT_ERROR_CODES.has(errorCode)
964
+ || lowerMessage.includes("timeout")
965
+ || lowerMessage.includes("timed out")
966
+ || lowerMessage.includes("abort");
967
+ const classified = isTimeout
968
+ ? (elapsedMs >= TARPIT_LATENCY_THRESHOLD_MS
969
+ ? classifyDefenseSignal({ latencyMs: elapsedMs })
970
+ : {
971
+ signal: "probe_failed",
972
+ reason: `Defense probe aborted before tarpit threshold (${elapsedMs}ms).`,
973
+ })
974
+ : classifyDefenseSignal({ probeError: message });
975
+ return {
976
+ capturedAt: new Date().toISOString(),
977
+ targetPath,
978
+ probeError: message,
979
+ latencyMs: elapsedMs,
980
+ signal: classified.signal,
981
+ reason: classified.reason,
982
+ basedOnTool: decision.tool,
983
+ toolFailed: Boolean(execution && !execution.ok),
984
+ };
985
+ }
986
+ }
987
+ function parseAllowedTools(input, forbidCrash = true) {
988
+ const candidate = Array.isArray(input)
989
+ ? input.filter((tool) => typeof tool === "string" && ALL_TOOLS.includes(tool))
990
+ : [...DEFAULT_ALLOWED_TOOLS];
991
+ const deduped = Array.from(new Set(candidate));
992
+ const filtered = forbidCrash ? deduped.filter((tool) => tool !== "chaos.crash") : deduped;
993
+ if (filtered.length > 0)
994
+ return filtered;
995
+ return ["delay"];
996
+ }
997
+ function parsePersona(input) {
998
+ return getAutopilotPersonaId(input);
999
+ }
1000
+ async function runMission(control) {
1001
+ setSessionState(control.sessionId, "running", {
1002
+ startedAt: new Date().toISOString(),
1003
+ iteration: 0,
1004
+ });
1005
+ addThought(control.sessionId, "system", `Objective locked: ${control.objective}`);
1006
+ addThought(control.sessionId, "system", `Tool scope: ${control.allowedTools.join(", ")}`);
1007
+ addThought(control.sessionId, "system", `Persona: ${getAutopilotPersonaProfile(control.persona).label}`);
1008
+ let previousSnapshot;
1009
+ let previousDefenseFeedback = null;
1010
+ try {
1011
+ for (let iteration = 1; iteration <= control.maxIterations; iteration++) {
1012
+ if (control.stopRequested || control.killRequested)
1013
+ break;
1014
+ updateSession(control.sessionId, { iteration });
1015
+ addThought(control.sessionId, "analyze", "Scanning /metrics and /sysinfo for current pressure and stability.");
1016
+ const before = await captureRuntimeSnapshotWithRetry(control.baseUrl, previousSnapshot, {
1017
+ onRetry: (error, attempt, maxAttempts) => {
1018
+ addThought(control.sessionId, "analyze", `Telemetry capture failed (${error.message}). Retrying telemetry capture (${attempt + 1}/${maxAttempts}).`);
1019
+ },
1020
+ onRecovered: (attempt, maxAttempts) => {
1021
+ addThought(control.sessionId, "analyze", `Telemetry capture recovered on attempt ${attempt}/${maxAttempts}.`);
1022
+ },
1023
+ });
1024
+ previousSnapshot = before;
1025
+ if (control.stopRequested || control.killRequested)
1026
+ break;
1027
+ addThought(control.sessionId, "decide", `Telemetry: ${before.rps.toFixed(1)} RPS, ${before.avgLatencyMs.toFixed(1)}ms latency, ${(before.errorRate * 100).toFixed(2)}% errors.`);
1028
+ const memorySummary = buildPlannerMemorySummary(control.sessionId);
1029
+ const decision = await decideNextAction(control, before, iteration, memorySummary, previousDefenseFeedback);
1030
+ addThought(control.sessionId, "decide", decision.thought);
1031
+ if (decision.maneuver) {
1032
+ addThought(control.sessionId, "decide", `Evasion policy maneuver selected: signal=${decision.maneuver.triggerSignal}, countermeasure=${decision.maneuver.countermeasure}, rationale=${decision.maneuver.rationale}`);
1033
+ if (decision.maneuver.triggerSignal === "rate_limited") {
1034
+ const previousIntervalMs = control.intervalMs;
1035
+ control.intervalMs = clampNumber(Math.max(control.intervalMs, 500) + 500, control.intervalMs, 250, 30000);
1036
+ if (control.intervalMs !== previousIntervalMs) {
1037
+ addThought(control.sessionId, "decide", `Anti-rate-limit backoff applied: interval increased from ${previousIntervalMs}ms to ${control.intervalMs}ms.`);
1038
+ }
1039
+ }
1040
+ }
1041
+ if (control.stopRequested || control.killRequested)
1042
+ break;
1043
+ let execution;
1044
+ if (decision.tool !== "none") {
1045
+ addThought(control.sessionId, "act", `Executing ${decision.tool}.`);
1046
+ execution = await executeToolStep({
1047
+ id: `rt-${control.sessionId}-${iteration}`,
1048
+ action: decision.tool,
1049
+ params: decision.params,
1050
+ }, {
1051
+ shouldCancel: () => control.stopRequested || control.killRequested,
1052
+ });
1053
+ addAction(control.sessionId, {
1054
+ tool: decision.tool,
1055
+ params: decision.params,
1056
+ ok: execution.ok,
1057
+ message: execution.message,
1058
+ maneuver: decision.maneuver,
1059
+ });
1060
+ if (!execution.ok) {
1061
+ addThought(control.sessionId, "act", `Tool failed: ${execution.message}`);
1062
+ }
1063
+ }
1064
+ else {
1065
+ addThought(control.sessionId, "act", "Skipping tool execution for this iteration.");
1066
+ }
1067
+ safeCaptureMemory(control.sessionId, "act", () => {
1068
+ captureActionMemory({
1069
+ sessionId: control.sessionId,
1070
+ iteration,
1071
+ objective: control.objective,
1072
+ decision,
1073
+ execution,
1074
+ });
1075
+ });
1076
+ if (control.stopRequested || control.killRequested)
1077
+ break;
1078
+ addThought(control.sessionId, "verify", "Checking for crash and 5xx error movement after action.");
1079
+ const after = await captureRuntimeSnapshotWithRetry(control.baseUrl, before, {
1080
+ onRetry: (error, attempt, maxAttempts) => {
1081
+ addThought(control.sessionId, "verify", `Post-action telemetry capture failed (${error.message}). Retrying telemetry capture (${attempt + 1}/${maxAttempts}).`);
1082
+ },
1083
+ onRecovered: (attempt, maxAttempts) => {
1084
+ addThought(control.sessionId, "verify", `Post-action telemetry capture recovered on attempt ${attempt}/${maxAttempts}.`);
1085
+ },
1086
+ });
1087
+ previousSnapshot = after;
1088
+ const [healthAfter, defenseFeedback] = await Promise.all([
1089
+ getHealthStatus(control.baseUrl),
1090
+ captureDefenseFeedback(control, decision, execution),
1091
+ ]);
1092
+ const verification = summarizeVerification({ before, after, healthAfter });
1093
+ addFinding(control.sessionId, {
1094
+ iteration,
1095
+ objective: control.objective,
1096
+ before,
1097
+ after,
1098
+ decision: {
1099
+ tool: decision.tool,
1100
+ params: decision.params,
1101
+ reason: decision.reason,
1102
+ rawModelOutput: decision.rawModelOutput,
1103
+ maneuver: decision.maneuver,
1104
+ },
1105
+ verification,
1106
+ });
1107
+ addThought(control.sessionId, "report", `${verification.notes} Defense signal: ${defenseFeedback.signal}.`);
1108
+ safeCaptureMemory(control.sessionId, "verify", () => {
1109
+ captureVerificationMemory({
1110
+ sessionId: control.sessionId,
1111
+ iteration,
1112
+ objective: control.objective,
1113
+ verification,
1114
+ after,
1115
+ defenseFeedback,
1116
+ });
1117
+ });
1118
+ previousDefenseFeedback = defenseFeedback;
1119
+ if (verification.broken) {
1120
+ setSessionState(control.sessionId, "completed", {
1121
+ endedAt: new Date().toISOString(),
1122
+ summary: {
1123
+ completedAt: new Date().toISOString(),
1124
+ totalIterations: iteration,
1125
+ breakingPointRps: after.rps,
1126
+ failureReason: verification.notes,
1127
+ }
1128
+ });
1129
+ addThought(control.sessionId, "system", "Breaking point found. Mission complete.");
1130
+ return;
1131
+ }
1132
+ if (iteration < control.maxIterations && control.intervalMs > 0) {
1133
+ await new Promise((resolve) => setTimeout(resolve, control.intervalMs));
1134
+ }
1135
+ }
1136
+ if (control.killRequested) {
1137
+ setSessionState(control.sessionId, "stopped", {
1138
+ endedAt: new Date().toISOString(),
1139
+ summary: {
1140
+ completedAt: new Date().toISOString(),
1141
+ totalIterations: getSession(control.sessionId)?.iteration || 0,
1142
+ failureReason: "Kill switch activated",
1143
+ }
1144
+ });
1145
+ addThought(control.sessionId, "system", "Kill switch engaged. Mission halted.");
1146
+ return;
1147
+ }
1148
+ if (control.stopRequested) {
1149
+ setSessionState(control.sessionId, "stopped", {
1150
+ endedAt: new Date().toISOString(),
1151
+ summary: {
1152
+ completedAt: new Date().toISOString(),
1153
+ totalIterations: getSession(control.sessionId)?.iteration || 0,
1154
+ failureReason: "Stopped by operator",
1155
+ }
1156
+ });
1157
+ addThought(control.sessionId, "system", "Operator stop received. Mission halted.");
1158
+ return;
1159
+ }
1160
+ const session = getSession(control.sessionId);
1161
+ setSessionState(control.sessionId, "completed", {
1162
+ endedAt: new Date().toISOString(),
1163
+ summary: {
1164
+ completedAt: new Date().toISOString(),
1165
+ totalIterations: session?.iteration || control.maxIterations,
1166
+ failureReason: "No system break detected before max iterations",
1167
+ }
1168
+ });
1169
+ addThought(control.sessionId, "system", "Mission ended at max iterations without a break.");
1170
+ }
1171
+ catch (error) {
1172
+ logger.error({ error: error?.message, sessionId: control.sessionId }, "Autopilot mission failed");
1173
+ setSessionState(control.sessionId, "failed", {
1174
+ endedAt: new Date().toISOString(),
1175
+ error: error?.message || "Mission failed unexpectedly",
1176
+ });
1177
+ addThought(control.sessionId, "system", `Mission failed: ${error?.message || "unknown error"}`);
1178
+ }
1179
+ finally {
1180
+ if (activeControl?.sessionId === control.sessionId) {
1181
+ activeControl = null;
1182
+ }
1183
+ }
1184
+ }
1185
+ function buildStartResponse(sessionId) {
1186
+ const session = getSession(sessionId);
1187
+ const reports = listReports(sessionId);
1188
+ return {
1189
+ active: session?.state === "running" || session?.state === "stopping",
1190
+ session,
1191
+ latestReport: reports[reports.length - 1] || null,
1192
+ };
1193
+ }
1194
+ export async function autopilotStartHandler(req, res) {
1195
+ if (activeControl || startingSession) {
1196
+ return res.status(409).json({ error: "Autopilot already running", sessionId: activeControl?.sessionId });
1197
+ }
1198
+ const objective = typeof req.body?.objective === "string" ? req.body.objective.trim() : "";
1199
+ if (!objective) {
1200
+ return res.status(400).json({ error: "Missing objective" });
1201
+ }
1202
+ startingSession = true;
1203
+ try {
1204
+ const defaultBaseUrl = getServerOrigin(req);
1205
+ const targetBaseUrl = defaultBaseUrl;
1206
+ const maxIterations = clampNumber(req.body?.maxIterations, 12, 1, 30);
1207
+ const intervalMs = clampNumber(req.body?.intervalMs, 1500, 0, 30000);
1208
+ const forbidCrash = req.body?.scope?.forbidCrash !== false;
1209
+ const allowedTools = parseAllowedTools(req.body?.scope?.allowedTools, forbidCrash);
1210
+ const persona = parsePersona(req.body?.persona);
1211
+ const session = createSession({
1212
+ objective,
1213
+ targetBaseUrl,
1214
+ maxIterations,
1215
+ persona,
1216
+ allowedTools,
1217
+ });
1218
+ activeControl = {
1219
+ sessionId: session.id,
1220
+ stopRequested: false,
1221
+ killRequested: false,
1222
+ baseUrl: targetBaseUrl,
1223
+ objective,
1224
+ intervalMs,
1225
+ maxIterations,
1226
+ persona,
1227
+ allowedTools,
1228
+ };
1229
+ void runMission(activeControl);
1230
+ return res.json({
1231
+ success: true,
1232
+ sessionId: session.id,
1233
+ session,
1234
+ });
1235
+ }
1236
+ finally {
1237
+ startingSession = false;
1238
+ }
1239
+ }
1240
+ export async function autopilotStopHandler(_req, res) {
1241
+ if (!activeControl) {
1242
+ const latest = getLatestSession();
1243
+ if (latest) {
1244
+ return res.status(409).json({ error: `No active autopilot session (latest is ${latest.state})`, sessionId: latest.id });
1245
+ }
1246
+ return res.status(404).json({ error: "No active autopilot session" });
1247
+ }
1248
+ activeControl.stopRequested = true;
1249
+ setSessionState(activeControl.sessionId, "stopping");
1250
+ addThought(activeControl.sessionId, "system", "Stop requested. Finishing current cycle.");
1251
+ return res.json({ success: true, sessionId: activeControl.sessionId });
1252
+ }
1253
+ export async function autopilotKillHandler(_req, res) {
1254
+ const killResult = await stopAllActiveExperiments();
1255
+ if (activeControl) {
1256
+ activeControl.killRequested = true;
1257
+ activeControl.stopRequested = true;
1258
+ setSessionState(activeControl.sessionId, "stopping");
1259
+ addThought(activeControl.sessionId, "system", "Kill switch engaged. Cancelling active chaos and attacks.");
1260
+ }
1261
+ return res.json({ success: true, killResult });
1262
+ }
1263
+ export function autopilotStatusHandler(req, res) {
1264
+ const sessionId = typeof req.query.sessionId === "string" ? req.query.sessionId : activeControl?.sessionId;
1265
+ if (sessionId) {
1266
+ return res.json(buildStartResponse(sessionId));
1267
+ }
1268
+ const latest = getLatestSession();
1269
+ if (!latest) {
1270
+ return res.json({ active: false, session: null, latestReport: null });
1271
+ }
1272
+ const reports = listReports(latest.id);
1273
+ return res.json({
1274
+ active: activeControl?.sessionId === latest.id,
1275
+ session: latest,
1276
+ latestReport: reports[reports.length - 1] || null,
1277
+ });
1278
+ }
1279
+ export function autopilotReportsHandler(req, res) {
1280
+ const sessionId = typeof req.query.sessionId === "string" ? req.query.sessionId : undefined;
1281
+ res.json({ reports: listReports(sessionId) });
1282
+ }
1283
+ export function autopilotConfigHandler(_req, res) {
1284
+ const personas = listAutopilotPersonaProfiles().map((persona) => ({
1285
+ id: persona.id,
1286
+ label: persona.label,
1287
+ description: persona.description,
1288
+ tags: persona.tags,
1289
+ }));
1290
+ res.json({
1291
+ availableTools: ALL_TOOLS,
1292
+ defaultAllowedTools: DEFAULT_ALLOWED_TOOLS,
1293
+ personas,
1294
+ defaultPersona: DEFAULT_AUTOPILOT_PERSONA_ID,
1295
+ safetyDefaults: {
1296
+ forbidCrash: true,
1297
+ }
1298
+ });
1299
+ }
1300
+ export function resetAutopilotStateForTests() {
1301
+ startingSession = false;
1302
+ activeControl = null;
1303
+ resetToolExecutorForTests();
1304
+ resetRedTeamStoreForTests();
1305
+ }
1306
+ function buildTestSessionControl(overrides) {
1307
+ return {
1308
+ sessionId: "test-session",
1309
+ stopRequested: false,
1310
+ killRequested: false,
1311
+ baseUrl: "http://127.0.0.1:8090",
1312
+ objective: "Find break on /checkout",
1313
+ intervalMs: 1000,
1314
+ maxIterations: 1,
1315
+ persona: DEFAULT_AUTOPILOT_PERSONA_ID,
1316
+ ...overrides,
1317
+ };
1318
+ }
1319
+ export function sanitizeDecisionForTests(candidate, context) {
1320
+ return sanitizeDecision({
1321
+ ...candidate,
1322
+ rawModelOutput: "test",
1323
+ }, buildTestSessionControl({
1324
+ intervalMs: 0,
1325
+ ...context,
1326
+ persona: context.persona || DEFAULT_AUTOPILOT_PERSONA_ID,
1327
+ }));
1328
+ }
1329
+ export function captureActionMemoryForTests(input) {
1330
+ captureActionMemory({
1331
+ sessionId: input.sessionId,
1332
+ iteration: input.iteration,
1333
+ objective: input.objective,
1334
+ decision: {
1335
+ ...input.decision,
1336
+ rawModelOutput: "test",
1337
+ },
1338
+ execution: input.execution,
1339
+ });
1340
+ }
1341
+ export function captureVerificationMemoryForTests(input) {
1342
+ captureVerificationMemory(input);
1343
+ }
1344
+ export function buildPlannerMemorySummaryForTests(sessionId) {
1345
+ return buildPlannerMemorySummary(sessionId);
1346
+ }
1347
+ export function composePlannerPayloadForTests(input) {
1348
+ const control = {
1349
+ ...input.control,
1350
+ persona: input.control.persona || DEFAULT_AUTOPILOT_PERSONA_ID,
1351
+ };
1352
+ return composePlannerPayload(control, input.snapshot, input.iteration, input.memory, input.recentDefenseFeedback || null);
1353
+ }
1354
+ export function buildSystemPromptForTests(input) {
1355
+ const control = buildTestSessionControl({
1356
+ persona: input.persona || DEFAULT_AUTOPILOT_PERSONA_ID,
1357
+ allowedTools: input.allowedTools,
1358
+ });
1359
+ return buildSystemPrompt(control);
1360
+ }
1361
+ export function classifyDefenseSignalForTests(input) {
1362
+ return classifyDefenseSignal(input);
1363
+ }
1364
+ export function selectPolicyDecisionForTests(input) {
1365
+ const control = buildTestSessionControl({
1366
+ baseUrl: input.context.baseUrl,
1367
+ objective: input.context.objective,
1368
+ intervalMs: typeof input.context.intervalMs === "number" ? input.context.intervalMs : 1000,
1369
+ persona: input.context.persona || DEFAULT_AUTOPILOT_PERSONA_ID,
1370
+ allowedTools: input.context.allowedTools,
1371
+ });
1372
+ return selectPolicyDecision(control, input.recentDefenseFeedback, input.iteration);
1373
+ }
1374
+ export function pickPersonaWeightedToolForTests(input) {
1375
+ const control = buildTestSessionControl({
1376
+ sessionId: input.sessionId || "test-session",
1377
+ persona: input.persona || DEFAULT_AUTOPILOT_PERSONA_ID,
1378
+ allowedTools: input.allowedTools,
1379
+ });
1380
+ return pickPersonaWeightedTool(control, input.iteration);
1381
+ }
1382
+ export function applyPersonaBiasForTests(input) {
1383
+ const control = buildTestSessionControl({
1384
+ sessionId: input.sessionId || "test-session",
1385
+ baseUrl: input.baseUrl,
1386
+ objective: input.objective,
1387
+ persona: input.persona || DEFAULT_AUTOPILOT_PERSONA_ID,
1388
+ allowedTools: input.allowedTools,
1389
+ });
1390
+ return applyPersonaBias({
1391
+ ...input.candidate,
1392
+ rawModelOutput: "test",
1393
+ }, control, input.iteration);
1394
+ }
1395
+ export function parsePersonaForTests(input) {
1396
+ return parsePersona(input);
1397
+ }
1398
+ export function sanitizePromptFragmentForTests(input, maxLength) {
1399
+ return sanitizePromptFragment(input, maxLength);
1400
+ }
1401
+ export function shouldPauseForBreakSignalsForTests(memory) {
1402
+ return shouldPauseForBreakSignals(memory);
1403
+ }
1404
+ //# sourceMappingURL=redteam.js.map