Skip to content
BreachPilot

Exploit Agent — Runner (tools/exploit_agent/runner/)

Round loop, outcome pipeline, advisory hooks, and context assembly for the single-target loop. Canonical implementation: tools/exploit_agent/runner/_impl.py (~2.6K lines); runner/loop.py is a plain from ._impl import ... re-export, and runner/context.py / runner/phase.py are re-export shims over the package-level context.py / phase_tracker.py. (Implementation note: the task brief named runner/tool_catalog.py, but the file lives at tools/exploit_agent/tool_catalog.py — there is no runner/tool_catalog.py.)

Entry point run_exploit_agent (_impl.py:391); companionship doc loop.md covers init sequence, phase tracking, and budgets in the same file.

Architecture

run_exploit_agent (_impl.py:391)
 ├─ init: target lock → attacker OS → context profile → stores/mutator →
 │        planner/session → memory → briefings → system prompt → seed messages
 ├─ per round: refresh advisories → budgets → compaction → model call →
 │             validate → dispatch tools → outcome pipeline → hooks → phase/summary
 └─ return final_result dict (target_ip, total_actions, audit_path, workspace,
    records, messages, outcome_summary, plan, session_state, ...)
ModuleSymbolKindLineRole
runner/_impl.pyrun_exploit_agentasync def391Round loop entry; all args keyword-only
runner/_impl.pyCheckpointContextdataclass188Evidence snapshot for the operator hook
runner/_impl.pyCheckpointOutcomedataclass207Operator decision (continue/change_goal/privesc/finish/cancel)
runner/_impl.pyCheckpointHookProtocol228async (ctx) -> CheckpointOutcome | None
runner/_impl.py_InMemoryExperienceStoreclass286No-DB fallback for the adaptive loop
runner/_impl.py_resolve_attacker_osdef238autoWindows/Darwin/Linux
runner/_impl.py_resolve_allowed_targetsdef342Target + exploit.allowed_targets union
runner/_impl.py_load_attack_memory_settingsdef372(enabled, max_chars) from memory.*
runner/_impl.py_emit / _EVENT_TYPEdef/const76/67Best-effort WebUI event emit
runner/_impl.py_build_killchain_machinedef95Opt-in kill-chain machine or None
runner/_impl.py_should_snapshot_for_actiondef139Fail-open snapshot gate
runner/_impl.py_build_snapshot_managerdef154Fail-open SnapshotManager
runner/_impl.py_counterfactual_enableddef166replay_simulator.counterfactual toggle
runner/loop.pyre-export1from ._impl import ... (wheel-importable)
runner/context.pyre-export1tools.exploit_agent.context surface
runner/phase.pyre-export1_PhaseTracker surface
tool_catalog.pyselect_tools_for_phasedef207Phase-narrowed tool schemas
tool_catalog.pyvalidate_tool_calldef267Pre-dispatch schema check
tool_catalog.pyPHASE_TOOL_FAMILIESdict198Phase → tool-name families
async def run_exploit_agent(
    *,
    client: object,
    model: str,
    session: object,
    exploit_tools: list[dict[str, object]],
    policy: ExploitPolicy,
    target_ip: str,
    target_cve: str = "",
    target_os: str | None = None,
    known_cves: list[str] | None = None,
    service_context: str = "",
    reports_dir: Path = Path("reports"),
    experience_store: object | None = None,
    semantic_memory: object | None = None,
    scope_gate: "ScopeGate | None" = None,
    skill_registry: SkillRegistry | None = None,
    skills_cfg: dict[str, object] | None = None,
    skill_embedder: object | None = None,
    config: dict[str, object] | None = None,
    heartbeat: object = None,
    original_target: str | None = None,
    resolved_ip: str | None = None,
    event_sink: object = None,
    checkpoint_hook: CheckpointHook | None = None,
    clock: Callable[[], float] | None = None,
    sleep: Any | None = None,
) -> dict[str, object]:

Round loop lifecycle (_impl.py:1084)

for _round in range(max_rounds) (max_rounds = policy.settings.effective_max_rounds):

  1. Heartbeat update + round header (ui.round_header, skipped round 0).
  2. Refresh advisory messages: _refresh_attack_memory_message, _refresh_reasoning_advisory_message (single in-flight each).
  3. Budget gates: time (_budget_exceeded) and policy.can_proceed — either streams a final summary via _stream_model and breaks.
  4. Compaction: _should_compact_context_build_compacted_messages + _checkpoint_messages (long-session resume).
  5. response = await _call_model_with_retry(client, model, messages, _round_tools(), ...) where _round_tools() narrows schemas to the current phase (_impl.py:1065).
  6. tool_calls, invalid = _filter_and_validate_tool_calls(raw, all_tools); malformed calls get a RECOVERABLE_ERROR tool message (+ replan prompt); non-recoverable ones count toward the terminal-constraint limit.
  7. No-tool turn: phase-minima enforcement (can_terminate, finish-early push-back), goal-complete break on verified compromise/cred-dump, no_path checkpoint hook, else break.
  8. Per tool call: availability check → research-assistant branch → repeated policy.approve_action → optional pre-destructive snapshot → heartbeat + tool_start emit → session.call_tool (one auto-retry via _attempt_retry_correction) → parse content blocks → blocked/success handling → outcome pipeline → policy.record + session_mgr.record_actiontool_result emit (secret-masked) → hooks → banner parsing/phase/plan.
# Example: minimal standalone invocation (all args keyword-only)
result = await run_exploit_agent(
    client=model_client,
    model="glm",
    session=mcp_session,
    exploit_tools=tool_schemas,
    policy=policy,
    target_ip="10.0.0.50",
)
assert result["total_actions"] >= 0

Outcome pipeline: normalize → classify → judge

One ActionResult per tool result, threaded through every downstream consumer (loop, swarm, report, eval). Authority flows strictly downward: the tracker reads the normalized verdict, never the judge alone.

StageSymbolLocationContract
Normalizenormalize_action_result(*, tool_name, result_text, mcp_result=None, exit_code_hint=None)outcome_truth.py:347ActionResult: operational_status (completed/failed/blocked/retryable/execution_unknown) × exploit_outcome (compromise/cred_dump/partial/failure/unknown/none); exit code defaults to None, never fabricated 0
Classifyclassify_exploit_outcome(result_text)outcome_truth.py:195Tightened markers: ^COMPROMISE:, meterpreter session N, uid=0(, NT AUTHORITY\SYSTEM, root@host:# + command; creds require colon/dumped/hash-hex. Legacy loose classify_exploit_result (outcome_classify.py:96) kept for the adapter fallback only
Judgejudge_flow_a(*, config, policy, result_text, tool_name, detail, exit_code, target_ip, plan, action_result=None)outcome_adapter.py:352Builds/caches OutcomeJudge on policy._flow_a_judge, synthesizes the observation via build_observation (outcome_adapter.py:72), returns (HypothesisStatus, confidence, classification) or None

In-loop wiring (_impl.py:1844-2057):

_action_result = _normalize_result(tool_name=name, result_text=result_text, mcp_result=result)
exit_code = _action_result.exit_code  # None preserved; never fabricated as 0
success = (_action_result.operational_status == _OpStatus.COMPLETED
           and not _action_result.is_error
           and exit_code in (0, None)
           and _action_result.exploit_outcome != _ExploitOutcome.FAILURE)
if _action_result.is_compromise:
    outcome_tracker.record_compromise(shell_type=..., privilege_level=...)
elif _action_result.is_cred_dump:
    outcome_tracker.record_cred_dump()
_flowa = await _judge_flow_a(..., action_result=_action_result)

_ToolOutcomeTracker (tool_calls.py:95): threshold=3 consecutive blocked/unavailable → terminal_constraint_reached; record_compromise / record_cred_dump / record_partial monotonic counters; record_exploit_success / record_exploit_failure drive the peer-consult trigger; should_escalate / should_pivot signals for the orchestrator. Only _EXPLOIT_VALIDATION_TOOLS (outcome_truth.py:67) can yield a compromise — recon/install/workspace output classifies as none.

Tool catalog (tool_catalog.py)

_round_tools() intersects the full schema list with the current phase family (_impl.py:1065-1070), always keeping _UNIVERSAL (shell, workspace, skills, advisory consults, state/capability tools) and hiding _HIDDEN (nested control-plane + package/clone noise). Unknown phase or empty selection falls back to the full list.

def select_tools_for_phase(
    all_tools: list[dict[str, Any]],
    phase: str,
    *,
    available_mcp_names: set[str] | None = None,
) -> list[dict[str, Any]]: ...

def validate_tool_call(
    name: str,
    args: dict[str, Any],
    all_tools: list[dict[str, Any]],
) -> str | None: ...  # None = valid; else short error fed back to the model
PhaseFamily (PHASE_TOOL_FAMILIES, :198)
recon_UNIVERSAL + recon/domain + browser-read
service_enumeration_UNIVERSAL + recon + exploit + browser read/mutate
vulnerability_research_UNIVERSAL + CVE/research + browser-read
validation_UNIVERSAL + exploit + browser read/mutate
reporting_UNIVERSAL + research + browser-read

Reflection / peer-consult / reselect hooks

All three are advisory-only user-role injections — they never touch permission, scope, workspace, or audit.

HookSymbolLocationTrigger
LLM reflection_llm_reflect_inline(client, model, messages, plan, action_count, *, semantic_memory, policy, target_ip, experience_store=None, verdict_signal=None)reflection.py:91Every reflection_every_n_actions (ultrathink tightens to ultrathink_reflection_interval); gated by agent.reflection_enabled; heuristic _generate_reflection fallback; writes one reflection:exploit_loop semantic lesson + optional reflection:verdict Bayesian row on terminal verdicts
Peer consult_consult_peers_inline(config, question, context, *, policy, target_ip, action_count)reflection.py:280outcome_tracker.should_consult_peers(peer_consult_on_failure_threshold) + multi_model_enabled; in-process (tools=None), shares the max_consultations budget with the consult_peer_models MCP tool
Skill reselect_maybe_reselect_skills(*, policy, state, action_count, new_cves, registry, skills_cfg, messages, recent_tool="", experience_store=None)skills.py:44New services/CVEs from banner parsing; rate-guarded by reselect_max_per_run (default 3) + reselect_min_interval_actions (default 5); _SkillReselectState (skills.py:21) dedupes repeats

Checkpoints (CheckpointContext/CheckpointOutcome/CheckpointHook, _impl.py:188-232): access fires right after the authoritative classifier confirms compromise/cred-dump (decision deferred until after audit + bookkeeping); no_path fires at the safe natural-termination boundary (minima met, no foothold). Both guarded against decision loops via _last_access_action / _last_no_path_action. finish/cancel outcomes set the break path; cancel sets final_result["cancelled_by_operator"].

Context assembly (context.py)

SymbolLineRole
_build_context_profile(model)80Per-alias window/compact-at/keep-full/output-cap (glm 976K→340K … default 128K→65%); models.info.<alias>.context_window override
_estimate_context_tokens(messages)152tiktoken cl100k when installed, else len//3; +4/msg overhead
_should_compact_context(policy, round_index, current_tokens)614Attack-mode only; current >= compact_at + min-gap (context_summarize_every, default 10 — Implementation note: policy.py:53 default is 10, loop doc cites 5; trust policy.py)
_build_compacted_messages(*, messages, system_prompt, plan, policy, ...)556System + fresh memory message + one deterministic summary + last keep_full verbatim turns
_refresh_attack_memory_message(messages, policy)300Single durable-memory user message after system prompt
_refresh_reasoning_advisory_message(messages, recent, ultrathink)243Single ULTRATHINK [REASONING] advisory (last 3, ≤400 chars each)
_parse_reasoning_block(content)197Pure string parse, sanitized one-liner or None
_checkpoint_messages(session_mgr, messages)622Persist when persist_messages (long-session crash resume)
sanitize_output(text, max_length=12000)645Strip ANSI/control chars; cp1252 round-trip on Windows only

Config keys

KeyEffect
exploit.attacker_osauto/Windows/Linux/Darwin system-prompt branch
exploit.allowed_targetsUnion with locked target (_resolve_allowed_targets)
memory.attack_memory_enabled / attack_memory_max_context_charsAttack-memory store (default on / 6000)
memory.semantic_enabled / experience_min_samples / experience_time_decay_daysSemantic lessons + Bayesian gating (defaults 3 / 90.0)
long_session.enabled / persist_messages / request_timeout_secondsnum_ctx threading + checkpointing + timeout
agent.decision_log_enabledPer-action decision_log.jsonl (default true)
agent.reflection_enabledIn-loop reflection hook (default true)
agent.capability_discovery_enabled / state_tools_enabled / task_graph_enabled / planner_hints_enabledCapability-guidance briefing blocks
outcome_judgment.flow_a + outcome_judgment.*judge_flow_a + OutcomeJudge thresholds
multi_model.enabled / max_consultationsPeer-consult availability + budget
skills.reselect_mid_run / reselect_max_per_run / reselect_min_interval_actions / reselect_sticky_defaultsMid-run skill updates
reasoning.llm_reflection / peer_consult_on_failure_threshold / ultrathink*Reflection depth + consult trigger
research.assistant.*ResearchAssistantSettings.from_config
killchain.enabledKill-chain machine + briefing (default off)
snapshots.enabled + replay_simulator.counterfactualPre-destructive snapshot + revert-and-retry rows

Source map

  • tools/exploit_agent/runner/_impl.py
  • tools/exploit_agent/runner/loop.py
  • tools/exploit_agent/runner/context.py
  • tools/exploit_agent/runner/phase.py
  • tools/exploit_agent/runner/__init__.py
  • tools/exploit_agent/tool_catalog.py
  • tools/exploit_agent/context.py
  • tools/exploit_agent/outcome_truth.py
  • tools/exploit_agent/outcome_adapter.py
  • tools/exploit_agent/outcome_classify.py
  • tools/exploit_agent/reflection.py
  • tools/exploit_agent/skills.py
  • tools/exploit_agent/phase_tracker.py
  • tools/exploit_agent/tool_calls.py
source: repo docs (build sync)Edit this page on GitHub →