pentesting 0.72.12 → 0.73.2

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.
@@ -18,7 +18,7 @@ import {
18
18
  getProcessEventLog,
19
19
  logEvent,
20
20
  setProcess
21
- } from "./chunk-GHJPYI4S.js";
21
+ } from "./chunk-YFDJI3GO.js";
22
22
 
23
23
  // src/shared/constants/time/conversions.ts
24
24
  var MS_PER_MINUTE = 6e4;
@@ -235,7 +235,7 @@ var INPUT_PROMPT_PATTERNS = [
235
235
 
236
236
  // src/shared/constants/agent.ts
237
237
  var APP_NAME = "Pentest AI";
238
- var APP_VERSION = "0.72.12";
238
+ var APP_VERSION = "0.73.2";
239
239
  var APP_DESCRIPTION = "Autonomous Penetration Testing AI Agent";
240
240
  var LLM_ROLES = {
241
241
  SYSTEM: "system",
@@ -304,7 +304,8 @@ var PHASE_TRANSITIONS = {
304
304
  [PHASES.FORENSICS]: [PHASES.EXPLOIT, PHASES.REPORT, PHASES.PWN, PHASES.CRYPTO]
305
305
  };
306
306
  var AGENT_ROLES = {
307
- ORCHESTRATOR: "orchestrator"
307
+ ORCHESTRATOR: "orchestrator",
308
+ AGENT_TOOL: "agent_tool"
308
309
  };
309
310
 
310
311
  // src/shared/constants/protocol/services.ts
@@ -619,7 +620,10 @@ var TOOL_NAMES = {
619
620
  HARVESTER: "harvester",
620
621
  SHODAN_QUERY: "shodan_query",
621
622
  CERT_TRANSPARENCY: "cert_transparency",
622
- GOOGLE_DORK: "google_dork"
623
+ GOOGLE_DORK: "google_dork",
624
+ // Task Delegation (v1)
625
+ RUN_TASK: "run_task",
626
+ TASK_COMPLETE: "task_complete"
623
627
  };
624
628
 
625
629
  // src/shared/constants/attack.ts
@@ -717,7 +721,6 @@ var UI_COMMANDS = {
717
721
  LOGS: "logs",
718
722
  LOGS_SHORT: "l",
719
723
  CTF: "ctf",
720
- TOR: "tor",
721
724
  AUTO: "auto",
722
725
  LAYOUT: "layout",
723
726
  GRAPH: "graph",
@@ -1919,57 +1922,6 @@ async function installTool(toolName, eventEmitter, inputHandler) {
1919
1922
  });
1920
1923
  }
1921
1924
 
1922
- // src/shared/utils/config/env.ts
1923
- var ENV_KEYS = {
1924
- API_KEY: "PENTEST_API_KEY",
1925
- BASE_URL: "PENTEST_BASE_URL",
1926
- MODEL: "PENTEST_MODEL",
1927
- SEARCH_API_KEY: "SEARCH_API_KEY",
1928
- SEARCH_API_URL: "SEARCH_API_URL",
1929
- THINKING: "PENTEST_THINKING",
1930
- THINKING_BUDGET: "PENTEST_THINKING_BUDGET",
1931
- SCOPE_MODE: "PENTEST_SCOPE_MODE",
1932
- APPROVAL_MODE: "PENTEST_APPROVAL_MODE",
1933
- APP_VERSION: "PENTESTING_VERSION"
1934
- };
1935
- var DEFAULT_SEARCH_API_URL = "https://api.search.brave.com/res/v1/web/search";
1936
- var DEFAULT_MODEL = "glm-4.7";
1937
- function getApiKey() {
1938
- return process.env[ENV_KEYS.API_KEY] || "";
1939
- }
1940
- function getBaseUrl() {
1941
- return process.env[ENV_KEYS.BASE_URL] || void 0;
1942
- }
1943
- function getModel() {
1944
- return process.env[ENV_KEYS.MODEL] || "";
1945
- }
1946
- function getSearchApiKey() {
1947
- if (process.env[ENV_KEYS.SEARCH_API_KEY]) {
1948
- return process.env[ENV_KEYS.SEARCH_API_KEY];
1949
- }
1950
- return process.env[ENV_KEYS.API_KEY];
1951
- }
1952
- function getSearchApiUrl() {
1953
- return process.env[ENV_KEYS.SEARCH_API_URL] || DEFAULT_SEARCH_API_URL;
1954
- }
1955
- function isZaiProvider() {
1956
- const baseUrl = getBaseUrl() || "";
1957
- return baseUrl.includes("z.ai");
1958
- }
1959
- function isThinkingEnabled() {
1960
- return process.env[ENV_KEYS.THINKING] !== "false";
1961
- }
1962
- function getThinkingBudget() {
1963
- const val = parseInt(process.env[ENV_KEYS.THINKING_BUDGET] || "", 10);
1964
- return isNaN(val) ? 8e3 : Math.max(1024, val);
1965
- }
1966
- function getScopeMode() {
1967
- return process.env[ENV_KEYS.SCOPE_MODE] === "enforce" ? "enforce" : "advisory";
1968
- }
1969
- function getApprovalMode() {
1970
- return process.env[ENV_KEYS.APPROVAL_MODE] === "require_auto_approve" ? "require_auto_approve" : "advisory";
1971
- }
1972
-
1973
1925
  // src/shared/utils/config/tor/core.ts
1974
1926
  var TOR_PROXY = {
1975
1927
  /** SOCKS5 proxy host (Tor daemon listens here) */
@@ -2068,31 +2020,10 @@ function checkTorLeakRisk(command) {
2068
2020
  return { safe: true };
2069
2021
  }
2070
2022
 
2071
- // src/shared/utils/config/validation.ts
2072
- function validateRequiredConfig() {
2073
- const errors = [];
2074
- if (!getApiKey()) {
2075
- errors.push(
2076
- `[config] PENTEST_API_KEY is required.
2077
- Export it before running:
2078
- export PENTEST_API_KEY=your_api_key
2079
- For z.ai: get your key at https://api.z.ai`
2080
- );
2081
- }
2082
- const budgetRaw = process.env[ENV_KEYS.THINKING_BUDGET];
2083
- if (budgetRaw !== void 0 && budgetRaw !== "") {
2084
- const parsed = parseInt(budgetRaw, 10);
2085
- if (isNaN(parsed) || parsed < 1024) {
2086
- errors.push(
2087
- `[config] PENTEST_THINKING_BUDGET must be an integer \u2265 1024 (got: "${budgetRaw}")`
2088
- );
2089
- }
2090
- }
2091
- return errors;
2092
- }
2093
-
2094
2023
  // src/engine/tools-base/command-executor/internal.ts
2095
2024
  import { spawn as spawn3 } from "child_process";
2025
+
2026
+ // src/engine/tools-base/command-executor/preparer.ts
2096
2027
  function injectCurlMaxTime(command, timeoutSec) {
2097
2028
  if (!/\bcurl\b/.test(command)) return command;
2098
2029
  if (/--max-time\b|-m\s+\d/.test(command)) return command;
@@ -2110,45 +2041,38 @@ Reason: ${torLeak.reason}
2110
2041
  Suggestion: ${torLeak.suggestion}`
2111
2042
  };
2112
2043
  }
2113
- return { safeCommand, execCommand: wrapCommandForTor(safeCommand) };
2114
- }
2115
- function setupTimeout(child, timeoutMs, onTimeout) {
2116
- return setTimeout(() => {
2117
- onTimeout();
2118
- try {
2119
- process.kill(-child.pid, "SIGKILL");
2120
- } catch {
2121
- try {
2122
- child.kill("SIGKILL");
2123
- } catch {
2124
- }
2125
- }
2126
- }, timeoutMs);
2044
+ return {
2045
+ safeCommand,
2046
+ execCommand: wrapCommandForTor(safeCommand)
2047
+ };
2127
2048
  }
2049
+
2050
+ // src/engine/tools-base/command-executor/io-handler.ts
2128
2051
  var ProcessIOHandler = class {
2129
- constructor(child, processState, eventEmitter, inputHandler) {
2130
- this.child = child;
2131
- this.processState = processState;
2132
- this.eventEmitter = eventEmitter;
2133
- this.inputHandler = inputHandler;
2052
+ constructor(ctx) {
2053
+ this.ctx = ctx;
2134
2054
  }
2135
2055
  stdout = "";
2136
2056
  stderr = "";
2137
2057
  inputHandled = false;
2058
+ /**
2059
+ * Checks if the data contains a known input prompt pattern.
2060
+ * If found, it invokes the inputHandler and writes to stdin.
2061
+ */
2138
2062
  async checkForInputPrompt(data) {
2139
- if (this.inputHandled || !this.inputHandler || this.processState.terminated) return;
2063
+ if (this.inputHandled || !this.ctx.inputHandler || this.ctx.processState.terminated) return;
2140
2064
  for (const pattern of INPUT_PROMPT_PATTERNS) {
2141
2065
  if (pattern.test(data)) {
2142
2066
  this.inputHandled = true;
2143
- this.eventEmitter?.({
2067
+ this.ctx.eventEmitter?.({
2144
2068
  type: COMMAND_EVENT_TYPES.INPUT_REQUIRED,
2145
2069
  message: `Input required: ${data.trim().slice(0, SYSTEM_LIMITS.MAX_PROMPT_PREVIEW)}`,
2146
2070
  detail: "Waiting for user input..."
2147
2071
  });
2148
2072
  try {
2149
- const userInput = await this.inputHandler(data.trim());
2150
- if (userInput !== null && !this.processState.terminated && this.child.stdin?.writable) {
2151
- this.child.stdin.write(userInput + "\n");
2073
+ const userInput = await this.ctx.inputHandler(data.trim());
2074
+ if (userInput !== null && !this.ctx.processState.terminated && this.ctx.child.stdin?.writable) {
2075
+ this.ctx.child.stdin.write(userInput + "\n");
2152
2076
  }
2153
2077
  } catch {
2154
2078
  }
@@ -2156,6 +2080,9 @@ var ProcessIOHandler = class {
2156
2080
  }
2157
2081
  }
2158
2082
  }
2083
+ /**
2084
+ * Handles a chunk of stdout data.
2085
+ */
2159
2086
  handleStdout(data) {
2160
2087
  const text = data.toString();
2161
2088
  this.stdout += text;
@@ -2164,13 +2091,16 @@ var ProcessIOHandler = class {
2164
2091
  }
2165
2092
  const lines = text.split("\n").map((l) => l.trim()).filter(Boolean);
2166
2093
  if (lines.length > 0) {
2167
- this.eventEmitter?.({
2094
+ this.ctx.eventEmitter?.({
2168
2095
  type: COMMAND_EVENT_TYPES.COMMAND_STDOUT,
2169
2096
  message: lines[lines.length - 1]
2170
2097
  });
2171
2098
  }
2172
2099
  this.checkForInputPrompt(text);
2173
2100
  }
2101
+ /**
2102
+ * Handles a chunk of stderr data.
2103
+ */
2174
2104
  handleStderr(data) {
2175
2105
  const text = data.toString();
2176
2106
  this.stderr += text;
@@ -2180,6 +2110,23 @@ var ProcessIOHandler = class {
2180
2110
  this.checkForInputPrompt(text);
2181
2111
  }
2182
2112
  };
2113
+
2114
+ // src/engine/tools-base/command-executor/internal.ts
2115
+ function setupTimeout(child, timeoutMs, onTimeout) {
2116
+ return setTimeout(() => {
2117
+ onTimeout();
2118
+ try {
2119
+ if (child.pid) {
2120
+ process.kill(-child.pid, "SIGKILL");
2121
+ }
2122
+ } catch {
2123
+ try {
2124
+ child.kill("SIGKILL");
2125
+ } catch {
2126
+ }
2127
+ }
2128
+ }, timeoutMs);
2129
+ }
2183
2130
  async function executeCommandOnce(command, options = {}) {
2184
2131
  return new Promise((resolve) => {
2185
2132
  const eventEmitter = getEventEmitter();
@@ -2202,7 +2149,12 @@ async function executeCommandOnce(command, options = {}) {
2202
2149
  processState.timedOut = true;
2203
2150
  processState.terminated = true;
2204
2151
  });
2205
- const ioHandler = new ProcessIOHandler(child, processState, eventEmitter, getInputHandler());
2152
+ const ioHandler = new ProcessIOHandler({
2153
+ child,
2154
+ processState,
2155
+ eventEmitter,
2156
+ inputHandler: getInputHandler()
2157
+ });
2206
2158
  child.stdout.on("data", (d) => ioHandler.handleStdout(d));
2207
2159
  child.stderr.on("data", (d) => ioHandler.handleStderr(d));
2208
2160
  child.on("close", (code) => {
@@ -2211,7 +2163,10 @@ async function executeCommandOnce(command, options = {}) {
2211
2163
  const outTrimmed = ioHandler.stdout.trim();
2212
2164
  const errTrimmed = ioHandler.stderr.trim();
2213
2165
  if (processState.timedOut) {
2214
- eventEmitter?.({ type: COMMAND_EVENT_TYPES.COMMAND_FAILED, message: `Command timed out after ${Math.round(timeoutMs / 1e3)}s` });
2166
+ eventEmitter?.({
2167
+ type: COMMAND_EVENT_TYPES.COMMAND_FAILED,
2168
+ message: `Command timed out after ${Math.round(timeoutMs / 1e3)}s`
2169
+ });
2215
2170
  return resolve({
2216
2171
  success: false,
2217
2172
  output: outTrimmed,
@@ -2223,7 +2178,11 @@ ${(ioHandler.stdout + ioHandler.stderr).trim().slice(-500)}`
2223
2178
  eventEmitter?.({ type: COMMAND_EVENT_TYPES.COMMAND_SUCCESS, message: `Command completed` });
2224
2179
  return resolve({ success: true, output: outTrimmed || errTrimmed });
2225
2180
  }
2226
- eventEmitter?.({ type: COMMAND_EVENT_TYPES.COMMAND_FAILED, message: `Command failed (exit ${code})`, detail: errTrimmed.slice(0, SYSTEM_LIMITS.MAX_INPUT_SLICE) });
2181
+ eventEmitter?.({
2182
+ type: COMMAND_EVENT_TYPES.COMMAND_FAILED,
2183
+ message: `Command failed (exit ${code})`,
2184
+ detail: errTrimmed.slice(0, SYSTEM_LIMITS.MAX_INPUT_SLICE)
2185
+ });
2227
2186
  return resolve({ success: false, output: outTrimmed, error: errTrimmed || `Process exited with code ${code}` });
2228
2187
  });
2229
2188
  child.on("error", (err) => {
@@ -2647,7 +2606,7 @@ async function cleanupAllProcesses() {
2647
2606
  cleanupDone = false;
2648
2607
  return;
2649
2608
  }
2650
- const { getBackgroundProcessesMap: getBackgroundProcessesMap2 } = await import("./process-registry-CCAQVJ4Y.js");
2609
+ const { getBackgroundProcessesMap: getBackgroundProcessesMap2 } = await import("./process-registry-GSHEX2LT.js");
2651
2610
  const backgroundProcesses = getBackgroundProcessesMap2();
2652
2611
  terminateAllNatively(backgroundProcesses, "SIGTERM");
2653
2612
  await new Promise((r) => setTimeout(r, SYSTEM_LIMITS.CLEANUP_BATCH_WAIT_MS));
@@ -2830,6 +2789,799 @@ function registerExitHandlers(processMap) {
2830
2789
  // src/engine/process/process-manager.ts
2831
2790
  registerExitHandlers(getBackgroundProcessesMap());
2832
2791
 
2792
+ // src/shared/utils/agent-memory/types.ts
2793
+ var MEMORY_ENTRY_CATEGORIES = {
2794
+ SUCCESS: "success",
2795
+ FAILURE: "failure",
2796
+ DISCOVERY: "discovery",
2797
+ TECHNIQUE: "technique",
2798
+ INSIGHT: "insight"
2799
+ };
2800
+ var EPISODIC_EVENT_TYPES = {
2801
+ TOOL_SUCCESS: "tool_success",
2802
+ TOOL_FAILURE: "tool_failure",
2803
+ PHASE_CHANGE: "phase_change",
2804
+ FLAG_FOUND: "flag_found",
2805
+ ACCESS_GAINED: "access_gained",
2806
+ VECTOR_SWITCH: "vector_switch"
2807
+ };
2808
+
2809
+ // src/shared/utils/agent-memory/fingerprint.ts
2810
+ var WORDLIST_FLAGS = /* @__PURE__ */ new Set(["-w", "-P", "-U", "-L", "--wordlist", "--password", "--passwords", "--username", "--usernames"]);
2811
+ var PORT_FLAGS = /* @__PURE__ */ new Set(["-p", "--port"]);
2812
+ var SCRIPT_RUNNERS = /* @__PURE__ */ new Set(["python", "python3", "bash", "sh", "zsh", "node", "ruby", "perl"]);
2813
+ function stripWrappingQuotes(value) {
2814
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
2815
+ return value.slice(1, -1);
2816
+ }
2817
+ return value;
2818
+ }
2819
+ function tokenizeCommand(command) {
2820
+ const matches = command.match(/"[^"]*"|'[^']*'|\S+/g) ?? [];
2821
+ return matches.map(stripWrappingQuotes);
2822
+ }
2823
+ function parseOptionTokens(tokens) {
2824
+ const options = [];
2825
+ const positionals = [];
2826
+ for (let i = 1; i < tokens.length; i++) {
2827
+ const token = tokens[i];
2828
+ if (token.startsWith("--")) {
2829
+ const eqIndex = token.indexOf("=");
2830
+ if (eqIndex !== -1) {
2831
+ options.push({ flag: token.slice(0, eqIndex), value: token.slice(eqIndex + 1) });
2832
+ continue;
2833
+ }
2834
+ const next = tokens[i + 1];
2835
+ if (next && !next.startsWith("-")) {
2836
+ options.push({ flag: token, value: next });
2837
+ i++;
2838
+ } else {
2839
+ options.push({ flag: token, value: "" });
2840
+ }
2841
+ continue;
2842
+ }
2843
+ if (token.startsWith("-") && token.length > 1) {
2844
+ const next = tokens[i + 1];
2845
+ const expectsValue = /^-[A-Za-z]$/.test(token) || /^-[a-z]{2,}$/.test(token);
2846
+ const canConsumeNext = expectsValue && next && !next.startsWith("-");
2847
+ if (canConsumeNext) {
2848
+ options.push({ flag: token, value: next });
2849
+ i++;
2850
+ } else {
2851
+ options.push({ flag: token, value: "" });
2852
+ }
2853
+ continue;
2854
+ }
2855
+ positionals.push(token);
2856
+ }
2857
+ return { options, positionals };
2858
+ }
2859
+ function findOptionValue(options, flags) {
2860
+ return options.find((option) => flags.has(option.flag))?.value || "";
2861
+ }
2862
+ function buildOptionSignature(options, positionals, effectiveTool) {
2863
+ const entries = options.filter((option) => !WORDLIST_FLAGS.has(option.flag) && !PORT_FLAGS.has(option.flag)).map((option) => option.value ? `${option.flag}=${option.value}` : option.flag);
2864
+ if (SCRIPT_RUNNERS.has(effectiveTool) && positionals.length > 0) {
2865
+ entries.push(`script=${positionals[0]}`);
2866
+ }
2867
+ return Array.from(new Set(entries)).sort().join(" ");
2868
+ }
2869
+ function extractFingerprint(tool, command) {
2870
+ const cmd = command || "";
2871
+ const tokens = tokenizeCommand(cmd);
2872
+ let effectiveTool = tool.toLowerCase();
2873
+ if (effectiveTool === "run_cmd" || effectiveTool === "run_background") {
2874
+ const firstWord = tokens[0];
2875
+ if (firstWord && !firstWord.startsWith("-")) {
2876
+ effectiveTool = firstWord.toLowerCase();
2877
+ }
2878
+ }
2879
+ const { options, positionals } = parseOptionTokens(tokens);
2880
+ const targetMatch = cmd.match(
2881
+ /(?::\/\/|@)([\w.\-]+(?::\d+)?)|\b(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})(?::\d+)?\b/
2882
+ );
2883
+ const target = targetMatch?.[1] || targetMatch?.[2] || "";
2884
+ const wordlist = findOptionValue(options, WORDLIST_FLAGS);
2885
+ const port = findOptionValue(options, PORT_FLAGS);
2886
+ const flags = buildOptionSignature(options, positionals, effectiveTool);
2887
+ return { tool: effectiveTool, target, wordlist, flags, port };
2888
+ }
2889
+ function fingerprintsMatch(a, b) {
2890
+ return a.tool === b.tool && a.target === b.target && a.wordlist === b.wordlist && a.flags === b.flags && a.port === b.port;
2891
+ }
2892
+ function formatFingerprint(fp) {
2893
+ const parts = [fp.tool];
2894
+ if (fp.target) parts.push(`\u2192${fp.target}`);
2895
+ if (fp.wordlist) parts.push(`[wordlist:${fp.wordlist.split("/").pop()}]`);
2896
+ if (fp.port) parts.push(`[port:${fp.port}]`);
2897
+ if (fp.flags) parts.push(`[${fp.flags}]`);
2898
+ return parts.join(" ");
2899
+ }
2900
+
2901
+ // src/shared/utils/agent-memory/working-memory.ts
2902
+ var PRUNE_IMPORTANCE_WEIGHT = 0.7;
2903
+ var PRUNE_RECENCY_WEIGHT = 0.3;
2904
+ var PRUNE_WINDOW_MS = 6e5;
2905
+ var WorkingMemory = class {
2906
+ entries = [];
2907
+ maxEntries = MEMORY_LIMITS.WORKING_MEMORY_MAX_ENTRIES;
2908
+ add(category, content, importance = 0.5, context = {}) {
2909
+ this.entries.push({
2910
+ id: generatePrefixedId("wm"),
2911
+ timestamp: Date.now(),
2912
+ category,
2913
+ content,
2914
+ context,
2915
+ importance
2916
+ });
2917
+ this.pruneIfNeeded();
2918
+ }
2919
+ /**
2920
+ * Record a failed attempt with structured fingerprint.
2921
+ *
2922
+ * WHY: Fingerprint-based recording ensures that "hydra with rockyou.txt"
2923
+ * and "hydra with darkweb2017.txt" are stored as distinct attempts.
2924
+ * The LLM can see what parameter combinations have been tried and pick new ones.
2925
+ */
2926
+ recordFailure(tool, command, error, hypothesizedReason) {
2927
+ const fp = extractFingerprint(tool, command);
2928
+ fp.hypothesizedReason = hypothesizedReason;
2929
+ const fpLabel = formatFingerprint(fp);
2930
+ const entry = {
2931
+ id: generatePrefixedId("wm"),
2932
+ timestamp: Date.now(),
2933
+ category: MEMORY_ENTRY_CATEGORIES.FAILURE,
2934
+ content: `FAILED: ${fpLabel} \u2192 ${error.slice(0, DISPLAY_LIMITS.ERROR_PREVIEW)}`,
2935
+ context: { tool, command },
2936
+ importance: 0.8,
2937
+ fingerprint: fp
2938
+ };
2939
+ this.entries.push(entry);
2940
+ this.pruneIfNeeded();
2941
+ }
2942
+ /**
2943
+ * Record a successful action for reference.
2944
+ */
2945
+ recordSuccess(tool, command, result) {
2946
+ this.add(MEMORY_ENTRY_CATEGORIES.SUCCESS, `SUCCESS: ${tool} \u2192 ${command.slice(0, DISPLAY_LIMITS.COMMAND_PREVIEW)}`, 0.6, { tool, result: result.slice(0, DISPLAY_LIMITS.OUTPUT_SUMMARY) });
2947
+ }
2948
+ /**
2949
+ * Check if the EXACT same attack attempt (all parameters identical) has failed.
2950
+ *
2951
+ * WHY: Only blocks truly duplicate attempts. Different wordlists, flags, or
2952
+ * ports are treated as new attacks and allowed through.
2953
+ */
2954
+ hasFailedBefore(command, tool) {
2955
+ const effectiveTool = tool || command.split(/\s+/)[0] || "";
2956
+ const fp = extractFingerprint(effectiveTool, command);
2957
+ return this.entries.find(
2958
+ (e) => e.category === MEMORY_ENTRY_CATEGORIES.FAILURE && e.fingerprint != null && fingerprintsMatch(e.fingerprint, fp)
2959
+ );
2960
+ }
2961
+ /**
2962
+ * 사후 보완: Analyst가 분류한 hypothesizedReason을 기존 실패 항목에 추가한다.
2963
+ *
2964
+ * WHY: pipeline.ts에서 recordFailure()가 먼저 호출되고, Analyst 분석은
2965
+ * 비동기로 나중에 완료된다. command 매칭으로 최근 실패 fingerprint를 찾아
2966
+ * hypothesizedReason을 보완한다. 기존 reason이 있으면 덮어쓰지 않는다.
2967
+ */
2968
+ updateLastFailureReason(command, hypothesizedReason) {
2969
+ const effectiveTool = command.split(/\s+/)[0] || "";
2970
+ const fp = extractFingerprint(effectiveTool, command);
2971
+ for (let i = this.entries.length - 1; i >= 0; i--) {
2972
+ const entry = this.entries[i];
2973
+ if (entry.category === MEMORY_ENTRY_CATEGORIES.FAILURE && entry.fingerprint && fingerprintsMatch(entry.fingerprint, fp)) {
2974
+ if (!entry.fingerprint.hypothesizedReason) {
2975
+ entry.fingerprint.hypothesizedReason = hypothesizedReason;
2976
+ }
2977
+ break;
2978
+ }
2979
+ }
2980
+ }
2981
+ /** Internal prune helper (used by both add() and recordFailure()) */
2982
+ pruneIfNeeded() {
2983
+ if (this.entries.length > this.maxEntries) {
2984
+ this.entries.sort((a, b) => {
2985
+ const aScore = a.importance * PRUNE_IMPORTANCE_WEIGHT + (1 - (Date.now() - a.timestamp) / PRUNE_WINDOW_MS) * PRUNE_RECENCY_WEIGHT;
2986
+ const bScore = b.importance * PRUNE_IMPORTANCE_WEIGHT + (1 - (Date.now() - b.timestamp) / PRUNE_WINDOW_MS) * PRUNE_RECENCY_WEIGHT;
2987
+ return bScore - aScore;
2988
+ });
2989
+ this.entries = this.entries.slice(0, this.maxEntries);
2990
+ }
2991
+ }
2992
+ /**
2993
+ * Get count of consecutive failures on the SAME attack vector.
2994
+ *
2995
+ * WHY: Switching from "hydra with wordlist A" to "hydra with wordlist B"
2996
+ * is a legitimate new approach. The consecutive-failure counter should
2997
+ * only trigger when every recent attempt is against the same tool+target
2998
+ * with no variation producing success. This prevents premature vector abandonment.
2999
+ */
3000
+ getConsecutiveFailures() {
3001
+ let count = 0;
3002
+ for (let i = this.entries.length - 1; i >= 0; i--) {
3003
+ if (this.entries[i].category === MEMORY_ENTRY_CATEGORIES.FAILURE) count++;
3004
+ else break;
3005
+ }
3006
+ return count;
3007
+ }
3008
+ /**
3009
+ * Format for prompt injection.
3010
+ */
3011
+ toPrompt() {
3012
+ if (this.entries.length === 0) return "";
3013
+ const failures = this.entries.filter((e) => e.category === MEMORY_ENTRY_CATEGORIES.FAILURE);
3014
+ const successes = this.entries.filter((e) => e.category === MEMORY_ENTRY_CATEGORIES.SUCCESS);
3015
+ const insights = this.entries.filter((e) => e.category === MEMORY_ENTRY_CATEGORIES.INSIGHT || e.category === MEMORY_ENTRY_CATEGORIES.DISCOVERY);
3016
+ const lines = ["<working-memory>"];
3017
+ lines.push(...this.buildFailuresSection(failures));
3018
+ lines.push(...this.buildSuccessesSection(successes));
3019
+ lines.push(...this.buildInsightsSection(insights));
3020
+ lines.push("</working-memory>");
3021
+ return lines.join("\n");
3022
+ }
3023
+ buildFailuresSection(failures) {
3024
+ if (failures.length === 0) return [];
3025
+ const lines = [];
3026
+ lines.push(`\u26A0\uFE0F FAILED ATTEMPTS (${failures.length} \u2014 DO NOT REPEAT EXACT SAME PARAMS):`);
3027
+ for (const f of failures.slice(-DISPLAY_LIMITS.RECENT_FAILURES)) {
3028
+ const fp = f.fingerprint;
3029
+ if (fp) {
3030
+ const reason = fp.hypothesizedReason ? ` [${fp.hypothesizedReason}]` : "";
3031
+ lines.push(` \u2717 ${formatFingerprint(fp)}${reason} \u2192 ${f.content.split("\u2192").pop()?.trim() || ""}`);
3032
+ } else {
3033
+ lines.push(` \u2717 ${f.content}`);
3034
+ }
3035
+ }
3036
+ lines.push(...this.buildAttackCoverageLines(failures));
3037
+ const consecutiveFails = this.getConsecutiveFailures();
3038
+ if (consecutiveFails >= MEMORY_LIMITS.CONSECUTIVE_FAIL_THRESHOLD) {
3039
+ lines.push(`\u{1F534} ${consecutiveFails} CONSECUTIVE FAILURES \u2014 consider changing approach or parameters`);
3040
+ }
3041
+ return lines;
3042
+ }
3043
+ buildSuccessesSection(successes) {
3044
+ if (successes.length === 0) return [];
3045
+ const lines = [`\u2705 RECENT SUCCESSES (${successes.length}):`];
3046
+ for (const s of successes.slice(-DISPLAY_LIMITS.RECENT_SUCCESSES)) {
3047
+ lines.push(` \u2713 ${s.content}`);
3048
+ }
3049
+ return lines;
3050
+ }
3051
+ buildInsightsSection(insights) {
3052
+ if (insights.length === 0) return [];
3053
+ const lines = ["\u{1F4A1} INSIGHTS:"];
3054
+ for (const i of insights.slice(-DISPLAY_LIMITS.RECENT_INSIGHTS)) {
3055
+ lines.push(` \u2192 ${i.content}`);
3056
+ }
3057
+ return lines;
3058
+ }
3059
+ /**
3060
+ * Build ATTACK COVERAGE lines grouped by tool+target.
3061
+ * WHY: Shows the LLM which parameter combinations have been tried per vector,
3062
+ * so it can pick genuinely untried variations instead of repeating.
3063
+ */
3064
+ buildAttackCoverageLines(failures) {
3065
+ const vectorMap = /* @__PURE__ */ new Map();
3066
+ for (const f of failures) {
3067
+ if (f.fingerprint) {
3068
+ const key = `${f.fingerprint.tool}\u2192${f.fingerprint.target}`;
3069
+ const detail = f.fingerprint.wordlist ? f.fingerprint.wordlist.split("/").pop() || "" : f.fingerprint.flags || "default";
3070
+ const existing = vectorMap.get(key) || [];
3071
+ if (!existing.includes(detail)) existing.push(detail);
3072
+ vectorMap.set(key, existing);
3073
+ }
3074
+ }
3075
+ if (vectorMap.size === 0) return [];
3076
+ const lines = [` \u{1F4CA} ATTACK COVERAGE (tried variations):`];
3077
+ for (const [vector, variations] of vectorMap) {
3078
+ lines.push(` ${vector}: tried [${variations.join(", ")}] \u2014 try DIFFERENT params`);
3079
+ }
3080
+ return lines;
3081
+ }
3082
+ getEntries() {
3083
+ return [...this.entries];
3084
+ }
3085
+ /** Clear all working memory entries. */
3086
+ clear() {
3087
+ this.entries = [];
3088
+ }
3089
+ };
3090
+
3091
+ // src/shared/utils/agent-memory/episodic-memory.ts
3092
+ var EpisodicMemory = class {
3093
+ events = [];
3094
+ maxEvents = MEMORY_LIMITS.EPISODIC_MEMORY_MAX_EVENTS;
3095
+ record(type, summary, details = {}) {
3096
+ this.events.push({
3097
+ timestamp: Date.now(),
3098
+ type,
3099
+ summary,
3100
+ details
3101
+ });
3102
+ if (this.events.length > this.maxEvents) {
3103
+ this.events = this.events.slice(-this.maxEvents);
3104
+ }
3105
+ }
3106
+ /**
3107
+ * Get timeline of key events (for strategic overview).
3108
+ */
3109
+ getTimeline() {
3110
+ return [...this.events];
3111
+ }
3112
+ /**
3113
+ * Get events of a specific type.
3114
+ */
3115
+ getByType(type) {
3116
+ return this.events.filter((e) => e.type === type);
3117
+ }
3118
+ /**
3119
+ * Count how many times a vector has been switched (indicator of difficulty).
3120
+ */
3121
+ getVectorSwitchCount() {
3122
+ return this.events.filter((e) => e.type === EPISODIC_EVENT_TYPES.VECTOR_SWITCH).length;
3123
+ }
3124
+ /**
3125
+ * Format for prompt (condensed timeline).
3126
+ */
3127
+ toPrompt() {
3128
+ if (this.events.length === 0) return "";
3129
+ const lines = ["<session-timeline>"];
3130
+ const recent = this.events.slice(-DISPLAY_LIMITS.RECENT_MEMORY_EVENTS);
3131
+ for (const e of recent) {
3132
+ const mins = Math.floor((Date.now() - e.timestamp) / MS_PER_MINUTE);
3133
+ const icon = {
3134
+ tool_success: "\u2705",
3135
+ tool_failure: "\u274C",
3136
+ phase_change: "\u{1F504}",
3137
+ flag_found: "\u{1F3C1}",
3138
+ access_gained: "\u{1F513}",
3139
+ vector_switch: "\u21AA\uFE0F"
3140
+ }[e.type] || "\u2022";
3141
+ lines.push(` ${icon} [${mins}min ago] ${e.summary}`);
3142
+ }
3143
+ lines.push("</session-timeline>");
3144
+ return lines.join("\n");
3145
+ }
3146
+ /** Clear all episodic events. */
3147
+ clear() {
3148
+ this.events = [];
3149
+ }
3150
+ };
3151
+
3152
+ // src/shared/utils/agent-memory/persistent-memory.ts
3153
+ import { existsSync as existsSync7, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
3154
+ import { join as join4 } from "path";
3155
+
3156
+ // src/shared/utils/agent-memory/similarity.ts
3157
+ var JACCARD_MATCH_THRESHOLD = 0.15;
3158
+ var MIN_TOKEN_LENGTH = 2;
3159
+ function tokenize(s) {
3160
+ return new Set(
3161
+ s.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((t) => t.length >= MIN_TOKEN_LENGTH)
3162
+ );
3163
+ }
3164
+ function jaccardSimilarity(a, b) {
3165
+ const setA = tokenize(a);
3166
+ const setB = tokenize(b);
3167
+ if (setA.size === 0 || setB.size === 0) return 0;
3168
+ let intersection = 0;
3169
+ for (const token of setA) {
3170
+ if (setB.has(token)) intersection++;
3171
+ }
3172
+ const union = setA.size + setB.size - intersection;
3173
+ return union === 0 ? 0 : intersection / union;
3174
+ }
3175
+ function matchesServiceProfile(serviceProfile, services) {
3176
+ if (services.length === 0) return false;
3177
+ const query = services.join(" ");
3178
+ return jaccardSimilarity(serviceProfile, query) >= JACCARD_MATCH_THRESHOLD;
3179
+ }
3180
+
3181
+ // src/shared/utils/agent-memory/session-snapshot.ts
3182
+ import { existsSync as existsSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync4, unlinkSync as unlinkSync4 } from "fs";
3183
+ import { join as join3 } from "path";
3184
+ var SNAPSHOT_FILE = join3(WORKSPACE.MEMORY, SPECIAL_FILES.SESSION_SNAPSHOT);
3185
+ function saveSessionSnapshot(snapshot) {
3186
+ try {
3187
+ ensureDirExists(WORKSPACE.MEMORY);
3188
+ writeFileSync4(SNAPSHOT_FILE, JSON.stringify(snapshot, null, 2));
3189
+ } catch {
3190
+ }
3191
+ }
3192
+ function loadSessionSnapshot() {
3193
+ try {
3194
+ if (existsSync6(SNAPSHOT_FILE)) {
3195
+ return JSON.parse(readFileSync6(SNAPSHOT_FILE, "utf-8"));
3196
+ }
3197
+ } catch {
3198
+ }
3199
+ return null;
3200
+ }
3201
+ function snapshotToPrompt() {
3202
+ const snap = loadSessionSnapshot();
3203
+ if (!snap) return "";
3204
+ const lines = [
3205
+ "<session-snapshot>",
3206
+ `TARGET: ${snap.target} PHASE: ${snap.phase} SAVED: ${new Date(snap.savedAt).toISOString()}`,
3207
+ "",
3208
+ "ACHIEVED:",
3209
+ ...snap.achieved.map((a) => ` \u2705 ${a}`),
3210
+ "",
3211
+ "NEXT PRIORITIES (resume here):",
3212
+ ...snap.next.map((n, i) => ` ${i + 1}. ${n}`)
3213
+ ];
3214
+ if (snap.credentials.length > 0) {
3215
+ lines.push("", "USABLE CREDENTIALS:");
3216
+ snap.credentials.forEach((c) => lines.push(` \u{1F511} ${c}`));
3217
+ }
3218
+ if (snap.notes) {
3219
+ lines.push("", `NOTES: ${snap.notes}`);
3220
+ }
3221
+ lines.push("</session-snapshot>");
3222
+ return lines.join("\n");
3223
+ }
3224
+ function clearSessionSnapshot() {
3225
+ try {
3226
+ if (existsSync6(SNAPSHOT_FILE)) {
3227
+ unlinkSync4(SNAPSHOT_FILE);
3228
+ }
3229
+ } catch {
3230
+ }
3231
+ }
3232
+
3233
+ // src/shared/utils/agent-memory/persistent-memory.ts
3234
+ var MEMORY_FILE = join4(WORKSPACE.MEMORY, SPECIAL_FILES.PERSISTENT_KNOWLEDGE);
3235
+ var MAX_PLAYBOOK_DISPLAY = 3;
3236
+ var MAX_SUCCESSFUL_TECHNIQUES = 100;
3237
+ var MAX_FAILURE_PATTERNS = 100;
3238
+ var MAX_TECH_FACTS = 200;
3239
+ var MAX_EXPLOIT_CHAINS = 50;
3240
+ var PersistentMemory = class {
3241
+ knowledge;
3242
+ constructor() {
3243
+ this.knowledge = this.load();
3244
+ }
3245
+ /**
3246
+ * Record a successful technique.
3247
+ */
3248
+ recordSuccess(service, version, technique) {
3249
+ const existing = this.knowledge.successfulTechniques.find(
3250
+ (t) => t.service === service && t.technique === technique
3251
+ );
3252
+ if (existing) {
3253
+ existing.successCount++;
3254
+ existing.lastUsed = Date.now();
3255
+ } else {
3256
+ this.knowledge.successfulTechniques.push({
3257
+ service,
3258
+ version,
3259
+ technique,
3260
+ successCount: 1,
3261
+ lastUsed: Date.now()
3262
+ });
3263
+ if (this.knowledge.successfulTechniques.length > MAX_SUCCESSFUL_TECHNIQUES) {
3264
+ this.knowledge.successfulTechniques.sort((a, b) => b.lastUsed - a.lastUsed);
3265
+ this.knowledge.successfulTechniques = this.knowledge.successfulTechniques.slice(0, MAX_SUCCESSFUL_TECHNIQUES);
3266
+ }
3267
+ }
3268
+ this.save();
3269
+ }
3270
+ /**
3271
+ * Record a failure pattern to avoid.
3272
+ */
3273
+ recordFailurePattern(pattern, reason) {
3274
+ const existing = this.knowledge.failurePatterns.find((f) => f.pattern === pattern);
3275
+ if (existing) {
3276
+ existing.avoidCount++;
3277
+ } else {
3278
+ this.knowledge.failurePatterns.push({ pattern, reason, avoidCount: 1 });
3279
+ if (this.knowledge.failurePatterns.length > MAX_FAILURE_PATTERNS) {
3280
+ this.knowledge.failurePatterns.sort((a, b) => b.avoidCount - a.avoidCount);
3281
+ this.knowledge.failurePatterns = this.knowledge.failurePatterns.slice(0, MAX_FAILURE_PATTERNS);
3282
+ }
3283
+ }
3284
+ this.save();
3285
+ }
3286
+ /**
3287
+ * Learn a fact about a technology.
3288
+ */
3289
+ learnFact(technology, fact) {
3290
+ if (!this.knowledge.techFacts.some((f) => f.technology === technology && f.fact === fact)) {
3291
+ this.knowledge.techFacts.push({ technology, fact, learnedAt: Date.now() });
3292
+ if (this.knowledge.techFacts.length > MAX_TECH_FACTS) {
3293
+ this.knowledge.techFacts.sort((a, b) => b.learnedAt - a.learnedAt);
3294
+ this.knowledge.techFacts = this.knowledge.techFacts.slice(0, MAX_TECH_FACTS);
3295
+ }
3296
+ this.save();
3297
+ }
3298
+ }
3299
+ /**
3300
+ * Get known successful techniques for a service.
3301
+ */
3302
+ getSuccessfulTechniques(service) {
3303
+ return this.knowledge.successfulTechniques.filter((t) => t.service.toLowerCase().includes(service.toLowerCase())).sort((a, b) => b.successCount - a.successCount);
3304
+ }
3305
+ /**
3306
+ * Clear all knowledge (for testing isolation).
3307
+ */
3308
+ clear() {
3309
+ this.knowledge = { successfulTechniques: [], failurePatterns: [], techFacts: [], exploitChains: [] };
3310
+ this.save();
3311
+ clearSessionSnapshot();
3312
+ }
3313
+ /**
3314
+ * Record a successful exploit chain for session-level reuse (CurriculumPT §).
3315
+ *
3316
+ * WHY: 단일 기법 저장(recordSuccess)과 달리, 체인 전체를 저장한다.
3317
+ * 다음 세션에서 동일 서비스 스택 조우 시 체인 첫 단계부터 시도할 수 있다.
3318
+ */
3319
+ recordExploitChain(serviceProfile, chainSummary, toolsUsed) {
3320
+ const existing = this.knowledge.exploitChains.find(
3321
+ (c) => c.serviceProfile === serviceProfile && c.chainSummary === chainSummary
3322
+ );
3323
+ if (existing) {
3324
+ existing.successCount++;
3325
+ existing.lastSucceededAt = Date.now();
3326
+ } else {
3327
+ this.knowledge.exploitChains.push({
3328
+ serviceProfile,
3329
+ chainSummary,
3330
+ toolsUsed,
3331
+ successCount: 1,
3332
+ lastSucceededAt: Date.now()
3333
+ });
3334
+ if (this.knowledge.exploitChains.length > MAX_EXPLOIT_CHAINS) {
3335
+ this.knowledge.exploitChains.sort((a, b) => b.successCount - a.successCount);
3336
+ this.knowledge.exploitChains = this.knowledge.exploitChains.slice(0, MAX_EXPLOIT_CHAINS);
3337
+ }
3338
+ }
3339
+ this.save();
3340
+ }
3341
+ /**
3342
+ * Format for prompt injection (most relevant persistent knowledge).
3343
+ */
3344
+ toPrompt(services) {
3345
+ const relevant = [];
3346
+ for (const svc of services) {
3347
+ const techniques = this.getSuccessfulTechniques(svc);
3348
+ if (techniques.length > 0) {
3349
+ relevant.push(`${svc}: ${techniques.map((t) => `${t.technique} (${t.successCount}x)`).join(", ")}`);
3350
+ }
3351
+ }
3352
+ const matchedChains = this.knowledge.exploitChains.filter(
3353
+ (chain) => matchesServiceProfile(chain.serviceProfile, services)
3354
+ ).sort((a, b) => b.successCount - a.successCount).slice(0, MAX_PLAYBOOK_DISPLAY);
3355
+ if (relevant.length === 0 && matchedChains.length === 0) return "";
3356
+ const sections = ["<persistent-memory>"];
3357
+ if (relevant.length > 0) {
3358
+ sections.push(
3359
+ "PREVIOUSLY SUCCESSFUL TECHNIQUES:",
3360
+ ...relevant.map((r) => ` \u{1F4DA} ${r}`)
3361
+ );
3362
+ }
3363
+ if (matchedChains.length > 0) {
3364
+ sections.push(
3365
+ "EXPLOIT CHAIN PLAYBOOKS (matched to current target):",
3366
+ ...matchedChains.map(
3367
+ (c) => ` \u{1F517} [${c.serviceProfile}] ${c.chainSummary} (${c.successCount}x) tools:[${c.toolsUsed.join(",")}]`
3368
+ )
3369
+ );
3370
+ }
3371
+ sections.push("</persistent-memory>");
3372
+ return sections.join("\n");
3373
+ }
3374
+ load() {
3375
+ try {
3376
+ if (existsSync7(MEMORY_FILE)) {
3377
+ const data = JSON.parse(readFileSync7(MEMORY_FILE, "utf-8"));
3378
+ return {
3379
+ ...data,
3380
+ exploitChains: data.exploitChains ?? []
3381
+ };
3382
+ }
3383
+ } catch {
3384
+ }
3385
+ return { successfulTechniques: [], failurePatterns: [], techFacts: [], exploitChains: [] };
3386
+ }
3387
+ save() {
3388
+ try {
3389
+ ensureDirExists(WORKSPACE.MEMORY);
3390
+ writeFileSync5(MEMORY_FILE, JSON.stringify(this.knowledge, null, 2));
3391
+ } catch {
3392
+ }
3393
+ }
3394
+ };
3395
+
3396
+ // src/shared/utils/agent-memory/dynamic/extraction.ts
3397
+ var KNOWN_TECHS = [
3398
+ "apache",
3399
+ "nginx",
3400
+ "iis",
3401
+ "tomcat",
3402
+ "nodejs",
3403
+ "express",
3404
+ "flask",
3405
+ "django",
3406
+ "rails",
3407
+ "php",
3408
+ "wordpress",
3409
+ "joomla",
3410
+ "drupal",
3411
+ "mysql",
3412
+ "postgresql",
3413
+ "mssql",
3414
+ "mongodb",
3415
+ "redis",
3416
+ "memcached",
3417
+ "ssh",
3418
+ "ftp",
3419
+ "smb",
3420
+ "rdp",
3421
+ "ldap",
3422
+ "kerberos",
3423
+ "dns",
3424
+ "docker",
3425
+ "kubernetes",
3426
+ "jenkins",
3427
+ "gitlab",
3428
+ "grafana",
3429
+ "spring",
3430
+ "struts",
3431
+ "log4j",
3432
+ "jackson",
3433
+ "fastjson"
3434
+ ];
3435
+ var TECHNIQUE_PATTERNS = [
3436
+ // "Step 1: ...", "1. Run ..."
3437
+ /(?:step\s*\d+|^\d+\.\s)[:.]?\s*(.{20,150})/gim,
3438
+ // "Exploit: ...", "Payload: ...", "Command: ..."
3439
+ /(?:exploit|payload|command|technique|bypass|trick):\s*(.{10,150})/gi,
3440
+ // Code blocks with commands
3441
+ /(?:```|`)((?:curl|wget|python|nmap|sqlmap|nikto|gobuster|ffuf)\s+.{10,100})(?:```|`)/g
3442
+ ];
3443
+ function extractTechnologies(query) {
3444
+ const techs = [];
3445
+ const lower = query.toLowerCase();
3446
+ for (const tech of KNOWN_TECHS) {
3447
+ if (lower.includes(tech)) techs.push(tech);
3448
+ }
3449
+ const versionMatch = query.match(/(\d+\.\d+(?:\.\d+)?)/);
3450
+ if (versionMatch && techs.length > 0) {
3451
+ techs[0] = `${techs[0]}/${versionMatch[1]}`;
3452
+ }
3453
+ return techs.length > 0 ? techs : ["general"];
3454
+ }
3455
+ function extractTechniquesFromText(resultText) {
3456
+ if (!resultText || resultText.length < 50) return [];
3457
+ const extracted = [];
3458
+ for (const pattern of TECHNIQUE_PATTERNS) {
3459
+ pattern.lastIndex = 0;
3460
+ let match;
3461
+ while ((match = pattern.exec(resultText)) !== null && extracted.length < 5) {
3462
+ const technique = (match[1] || match[0]).trim();
3463
+ if (technique.length > 10 && technique.length < 200) {
3464
+ extracted.push(technique);
3465
+ }
3466
+ }
3467
+ }
3468
+ return extracted;
3469
+ }
3470
+
3471
+ // src/shared/utils/agent-memory/dynamic/formatter.ts
3472
+ function formatTechniquesForPrompt(techniques) {
3473
+ if (techniques.length === 0) return "";
3474
+ const confirmed = techniques.filter((t) => t.confidence >= CONFIDENCE_THRESHOLDS.CONFIRMED);
3475
+ const discovered = techniques.filter((t) => t.confidence < CONFIDENCE_THRESHOLDS.CONFIRMED);
3476
+ const lines = ["<learned-techniques>"];
3477
+ if (confirmed.length > 0) {
3478
+ lines.push("CONFIRMED (worked in this session):");
3479
+ for (const t of confirmed) {
3480
+ lines.push(` \u2705 [${t.applicableTo.join(",")}] ${t.technique}`);
3481
+ }
3482
+ }
3483
+ if (discovered.length > 0) {
3484
+ lines.push(`DISCOVERED (${discovered.length} unverified):`);
3485
+ for (const t of discovered.slice(0, MEMORY_LIMITS.PROMPT_UNVERIFIED_TECHNIQUES)) {
3486
+ lines.push(` \u{1F4A1} [${t.applicableTo.join(",")}] ${t.technique} (from: ${t.source})`);
3487
+ }
3488
+ }
3489
+ lines.push("</learned-techniques>");
3490
+ return lines.join("\n");
3491
+ }
3492
+
3493
+ // src/shared/utils/agent-memory/dynamic/engine.ts
3494
+ var DynamicTechniqueLibrary = class {
3495
+ techniques = [];
3496
+ maxTechniques = MEMORY_LIMITS.DYNAMIC_TECHNIQUES_MAX;
3497
+ /**
3498
+ * Learn a new technique from a search result or successful exploit.
3499
+ */
3500
+ learn(technique) {
3501
+ const exists = this.techniques.some(
3502
+ (t) => t.technique.toLowerCase() === technique.technique.toLowerCase()
3503
+ );
3504
+ if (exists) return;
3505
+ this.techniques.push({
3506
+ ...technique,
3507
+ learnedAt: Date.now()
3508
+ });
3509
+ if (this.techniques.length > this.maxTechniques) {
3510
+ this.techniques.sort((a, b) => {
3511
+ if (a.confidence !== b.confidence) return b.confidence - a.confidence;
3512
+ return b.learnedAt - a.learnedAt;
3513
+ });
3514
+ this.techniques = this.techniques.slice(0, this.maxTechniques);
3515
+ }
3516
+ }
3517
+ /**
3518
+ * Extract and learn techniques from a web search result.
3519
+ */
3520
+ learnFromSearchResult(query, resultText) {
3521
+ const extracted = extractTechniquesFromText(resultText);
3522
+ if (extracted.length === 0) return;
3523
+ const applicableTo = extractTechnologies(query);
3524
+ for (const tech of extracted) {
3525
+ this.learn({
3526
+ source: `Web search: "${query}"`,
3527
+ technique: tech,
3528
+ applicableTo,
3529
+ confidence: CONFIDENCE_THRESHOLDS.POSSIBLE,
3530
+ fromQuery: query
3531
+ });
3532
+ }
3533
+ }
3534
+ /**
3535
+ * Mark a technique as verified (it worked in practice).
3536
+ */
3537
+ verify(techniqueSubstring) {
3538
+ for (const t of this.techniques) {
3539
+ if (t.technique.toLowerCase().includes(techniqueSubstring.toLowerCase())) {
3540
+ t.confidence = CONFIDENCE_THRESHOLDS.CONFIRMED;
3541
+ }
3542
+ }
3543
+ }
3544
+ /**
3545
+ * Record that a technique failed in practice.
3546
+ */
3547
+ recordFailure(techniqueSubstring) {
3548
+ if (!techniqueSubstring || techniqueSubstring.length < 3) return;
3549
+ const lower = techniqueSubstring.toLowerCase();
3550
+ this.techniques = this.techniques.filter((t) => {
3551
+ if (t.technique.toLowerCase().includes(lower)) {
3552
+ t.confidence -= MEMORY_LIMITS.TECHNIQUE_FAILURE_DECAY;
3553
+ return t.confidence > MEMORY_LIMITS.TECHNIQUE_PRUNE_THRESHOLD;
3554
+ }
3555
+ return true;
3556
+ });
3557
+ }
3558
+ /**
3559
+ * Get techniques relevant to a specific service/technology.
3560
+ */
3561
+ getRelevant(service) {
3562
+ const lower = service.toLowerCase();
3563
+ return this.techniques.filter(
3564
+ (t) => t.applicableTo.some((a) => a.toLowerCase().includes(lower) || lower.includes(a.toLowerCase()))
3565
+ );
3566
+ }
3567
+ /**
3568
+ * Get all learned techniques.
3569
+ */
3570
+ getAll() {
3571
+ return [...this.techniques];
3572
+ }
3573
+ /** Clear all learned techniques. */
3574
+ clear() {
3575
+ this.techniques = [];
3576
+ }
3577
+ /**
3578
+ * Format for prompt injection.
3579
+ */
3580
+ toPrompt() {
3581
+ return formatTechniquesForPrompt(this.techniques);
3582
+ }
3583
+ };
3584
+
2833
3585
  // src/engine/state/persistence/serializer.ts
2834
3586
  var UNVERIFIED_CHAIN_DEPTH_THRESHOLD = 2;
2835
3587
  var ACTIONABLE_LOOT_TYPES = /* @__PURE__ */ new Set([
@@ -2857,7 +3609,7 @@ var StateSerializer = class {
2857
3609
  this.formatLoot(state3, lines);
2858
3610
  this.formatTodos(state3, lines);
2859
3611
  this.formatEnvironment(state3, lines);
2860
- const snapshot = state3.persistentMemory.snapshotToPrompt();
3612
+ const snapshot = snapshotToPrompt();
2861
3613
  if (snapshot) lines.push(snapshot);
2862
3614
  lines.push(`Phase: ${state3.getPhase()}`);
2863
3615
  return lines.join("\n");
@@ -2993,8 +3745,8 @@ BLOCKED (leak real IP): ping, traceroute, dig, nslookup, nmap -sU`
2993
3745
  };
2994
3746
 
2995
3747
  // src/engine/state/persistence/saver.ts
2996
- import { writeFileSync as writeFileSync4, readdirSync, statSync as statSync2, unlinkSync as unlinkSync4 } from "fs";
2997
- import { join as join3 } from "path";
3748
+ import { writeFileSync as writeFileSync6, readdirSync, statSync as statSync2, unlinkSync as unlinkSync5 } from "fs";
3749
+ import { join as join5 } from "path";
2998
3750
  function saveState(state3) {
2999
3751
  const sessionsDir = WORKSPACE.SESSIONS;
3000
3752
  ensureDirExists(sessionsDir);
@@ -3011,18 +3763,18 @@ function saveState(state3) {
3011
3763
  missionSummary: state3.getMissionSummary(),
3012
3764
  missionChecklist: state3.getMissionChecklist()
3013
3765
  };
3014
- const sessionFile = join3(sessionsDir, FILE_PATTERNS.session());
3766
+ const sessionFile = join5(sessionsDir, FILE_PATTERNS.session());
3015
3767
  const json = JSON.stringify(snapshot, null, 2);
3016
- writeFileSync4(sessionFile, json, "utf-8");
3017
- const latestFile = join3(sessionsDir, "latest.json");
3018
- writeFileSync4(latestFile, json, "utf-8");
3768
+ writeFileSync6(sessionFile, json, "utf-8");
3769
+ const latestFile = join5(sessionsDir, "latest.json");
3770
+ writeFileSync6(latestFile, json, "utf-8");
3019
3771
  pruneOldSessions(sessionsDir);
3020
3772
  return sessionFile;
3021
3773
  }
3022
3774
  function pruneOldSessions(sessionsDir) {
3023
3775
  try {
3024
3776
  const sessionFiles = readdirSync(sessionsDir).filter((f) => f.endsWith(FILE_EXTENSIONS.JSON) && f !== SPECIAL_FILES.LATEST_STATE).map((f) => {
3025
- const filePath = join3(sessionsDir, f);
3777
+ const filePath = join5(sessionsDir, f);
3026
3778
  return {
3027
3779
  name: f,
3028
3780
  path: filePath,
@@ -3031,22 +3783,22 @@ function pruneOldSessions(sessionsDir) {
3031
3783
  }).sort((a, b) => b.mtime - a.mtime);
3032
3784
  const toDelete = sessionFiles.slice(AGENT_LIMITS.MAX_SESSION_FILES);
3033
3785
  for (const file of toDelete) {
3034
- unlinkSync4(file.path);
3786
+ unlinkSync5(file.path);
3035
3787
  }
3036
3788
  } catch {
3037
3789
  }
3038
3790
  }
3039
3791
 
3040
3792
  // src/engine/state/persistence/loader.ts
3041
- import { readFileSync as readFileSync6, existsSync as existsSync6 } from "fs";
3042
- import { join as join4 } from "path";
3793
+ import { readFileSync as readFileSync8, existsSync as existsSync8 } from "fs";
3794
+ import { join as join6 } from "path";
3043
3795
  function loadState(state3) {
3044
- const latestFile = join4(WORKSPACE.SESSIONS, "latest.json");
3045
- if (!existsSync6(latestFile)) {
3796
+ const latestFile = join6(WORKSPACE.SESSIONS, "latest.json");
3797
+ if (!existsSync8(latestFile)) {
3046
3798
  return false;
3047
3799
  }
3048
3800
  try {
3049
- const raw = readFileSync6(latestFile, "utf-8");
3801
+ const raw = readFileSync8(latestFile, "utf-8");
3050
3802
  const snapshot = JSON.parse(raw);
3051
3803
  if (snapshot.version !== 1) {
3052
3804
  debugLog("general", `Unknown snapshot version: ${snapshot.version}`);
@@ -3088,7 +3840,7 @@ function loadState(state3) {
3088
3840
  }
3089
3841
 
3090
3842
  // src/engine/state/persistence/janitor.ts
3091
- import { existsSync as existsSync7, rmSync } from "fs";
3843
+ import { existsSync as existsSync9, rmSync } from "fs";
3092
3844
  import path2 from "path";
3093
3845
  function clearWorkspace() {
3094
3846
  const cleared = [];
@@ -3106,7 +3858,7 @@ function clearWorkspace() {
3106
3858
  ];
3107
3859
  for (const dir2 of dirsToClean) {
3108
3860
  try {
3109
- if (existsSync7(dir2.path)) {
3861
+ if (existsSync9(dir2.path)) {
3110
3862
  rmSync(dir2.path, { recursive: true, force: true });
3111
3863
  ensureDirExists(dir2.path);
3112
3864
  cleared.push(dir2.label);
@@ -3121,28 +3873,14 @@ function clearWorkspace() {
3121
3873
  export {
3122
3874
  ensureDirExists,
3123
3875
  FILE_EXTENSIONS,
3124
- SPECIAL_FILES,
3125
3876
  FILE_PATTERNS,
3126
3877
  WORK_DIR,
3127
3878
  WORKSPACE,
3128
3879
  initDebugLogger,
3129
3880
  debugLog,
3130
3881
  flowLog,
3131
- DEFAULT_MODEL,
3132
- getApiKey,
3133
- getBaseUrl,
3134
- getModel,
3135
- getSearchApiKey,
3136
- getSearchApiUrl,
3137
- isZaiProvider,
3138
- isThinkingEnabled,
3139
- getThinkingBudget,
3140
- getScopeMode,
3141
- getApprovalMode,
3142
- isTorEnabled,
3143
3882
  setTorEnabled,
3144
3883
  getTorBrowserArgs,
3145
- validateRequiredConfig,
3146
3884
  MS_PER_MINUTE,
3147
3885
  DISPLAY_LIMITS,
3148
3886
  RETRY_CONFIG,
@@ -3184,7 +3922,12 @@ export {
3184
3922
  COMMAND_EVENT_TYPES,
3185
3923
  UI_COMMANDS,
3186
3924
  generateId,
3187
- generatePrefixedId,
3925
+ WorkingMemory,
3926
+ EpisodicMemory,
3927
+ saveSessionSnapshot,
3928
+ snapshotToPrompt,
3929
+ PersistentMemory,
3930
+ DynamicTechniqueLibrary,
3188
3931
  createSessionRuntime,
3189
3932
  setActiveSessionRuntime,
3190
3933
  clearActiveSessionRuntime,