Skip to content
BreachPilot

Purpose

Deterministic, non-LLM CLI for Flow B's SQLite-backed research loop. Operates directly on research_workspace/research.db via DatabaseManager / MissionController / TaskQueue / ScopeGate / RiskController / ExecutorAgent without the autonomous AgentLoop. Intended for headless/CI, manual triage, and replay of individual tasks.

Source Files

FileLinesRole
cli.py599Arg parsing, 11 subcommands, DB + mission helpers

Flow B file (per AGENTS.md §2, do not add new features here — it is frozen as legacy namespace).

Responsibilities

  • Resolve workspace root from RESEARCH_WORKSPACE env or research_workspace/ (cli.py:42 _workspace_root).
  • Open the research DB (cli.py:46 _load_dbDatabaseManager(workspace/research.db)).
  • Resolve the active mission: load by --mission-id if given (status active or paused), else latest active (cli.py:51 _load_mission, cli.py:74 _require_mission).
  • Implement 11 subcommands (see Public Interfaces) that read/write the same schema AgentLoop uses, preserving atomicity and scope/risk gates.

Public Interfaces

Workspace / DB helpers

FunctionLocationSignatureDescription
_workspace_rootcli.py:42() -> PathEnv-aware workspace root
_load_dbcli.py:46() -> DatabaseManagerOpens research.db
_load_missioncli.py:51(db, mission_id?) -> dict|NoneSELECT missions by id or latest active
_require_missioncli.py:74(args) -> (db, mission)Loads DB + mission; prints context-accurate error if missing
_get_mission_ctrlcli.py:92(db) -> MissionControllerFactory

Subcommands (each is func(args) -> int wired in build_parser)

CommandFunctionLocationDescription
init-missioncmd_init_missioncli.py:100yaml.safe_loadMissionController.create_from_config → prints mission summary
add-scopecmd_add_scopecli.py:137Insert allow/deny scope rule (_classify_asset + db.add_scope_rule)
list-scopecmd_list_scopecli.py:165ScopeGate.list_scope() render of allow/deny + forbidden actions
next-taskcmd_next_taskcli.py:205TaskQueue.get_next_task() — highest-priority pending
list-taskscmd_list_taskscli.py:231list_open_tasks + list_blocked_tasks + count_by_status
run-taskcmd_run_taskcli.py:264Scope→risk→human-approval gates → ExecutorAgentqueue.complete_task/failedObserverAgent + summarize_observation
summarize-targetcmd_summarize_targetcli.py:378MemoryManager.summarize_target + TargetGraph.summarize_graph
list-findingscmd_list_findingscli.py:400FindingVerifier.list_all()
validate-findingcmd_validate_findingcli.py:424FindingVerifier.validate_finding with ScopeGate + EvidenceStore
generate-reportcmd_generate_reportcli.py:449ReportGenerator.generate_report
statuscmd_statuscli.py:469TaskQueue.count_by_status + FindingVerifier.list_all rollup

Parser / Entrypoint

FunctionLocationSignature
build_parsercli.py:504() -> ArgumentParser (shared --mission-id parent parser for 10 subcommands; init-mission has --config required)
maincli.py:578(argv=None) -> int (dispatches args.func, handles KeyboardInterrupt/traceback)

Inputs/Outputs

InputNotes
mission.yamlYAML config for init-mission (yaml.safe_load)
--mission-id M-...Optional on 10 commands; selects specific mission (resume/attach)
--allow/--deny + --notesadd-scope pattern
task_id positionalrun-task [task_id]; empty means next pending
finding_id positionalvalidate-finding / generate-report
OutputNotes
stdout textHuman-readable summaries, task/finding listings, reports
Exit code0 success, 1 error, 130 on KeyboardInterrupt
DB mutationsresearch.db + workspace dirs
Evidence filesresearch_workspace/<mission_id>/evidence/ via EvidenceStore

State/Persistence

  • All reads/writes go through DatabaseManager against research_workspace/research.db (single DB, many missions).
  • research_workspace/<mission_id>/ dirs (evidence/raw_output, http_responses, screenshots, notes, artifacts, reports, logs, tasks) created by MissionController._init_workspace.
  • run-task writes: tasks.status, observations, memories, graph_nodes/edges, findings, audit_logs, evidence rows + files.

Configuration

  • Workspace root from RESEARCH_WORKSPACE env; default research_workspace/.
  • Mission risk profile, budgets (max_commands_per_session, max_tasks_active), and scope rules come from the persisted missions row and scope_rules table, not config.yaml (Flow B uses mission.yaml only at creation).
  • No config.yaml keys are read here.

Dependencies

  • db.DatabaseManager — connection, schema, add_scope_rule, get_scope_rules, log_audit
  • mission.Mission (from_dict, _classify_asset), mission.MissionController
  • scope_gate.ScopeGateload_from_db, check_scope, list_scope
  • risk_controller.RiskControllerassess_action
  • tool_router.ToolRouter — only in cmd_run_task via a stub executor (lambda name,args: "[tool] ...")
  • executor.ExecutorAgent, observer.ObserverAgent, summarizer.summarize_observation
  • evidence.EvidenceStore, memory.MemoryManager, target_graph.TargetGraph, finding_verifier.FindingVerifier, report_generator.ReportGenerator, task_queue.TaskQueue
  • PyYAML

Used By

  • Operator directly: python cli.py <command> [--mission-id M-...]
  • Tests exercising Flow B deterministically without LLM.
  • Not used by Flow A (main.py/app.py).

Control Flow

flowchart TD
    A[main -> build_parser -> parse_args] --> B{has func?}
    B -->|no| C[print_help -> 1]
    B -->|yes| D[args.func]
    D --> E[_require_mission -> _load_db + _load_mission]
    E --> F{mission?}
    F -->|no| G[return 1 after error]
    F -->|yes| H[specific cmd]
    H --> I{run-task?}
    I -->|yes| J[queue.get_task/get_next_task -> ScopeGate.check_scope -> RiskController.assess_action -> human_approval gate -> ExecutorAgent.execute -> ObserverAgent.observe -> queue.complete/failed]
    I -->|no| K[read-only render]

run-task scope/risk/approval flow mirrors agent_loop.py:607-642 and cli.py:293-341:

  1. ScopeGate.check_scope(asset=target, action_type=phase, tool=first_allowed, risk_level)
  2. RiskController.assess_action(phase, first_tool, json(task)[:300], target, risk_level)
  3. If either sets requires_human_approval, mark needs_approval and bail (H16).

Failure Modes

FailureHandling
No active missionERROR: No active mission found. Run init-mission first. → 1
--mission-id not foundERROR: No mission with id ... → 1
Bad mission.yamlyaml.safe_load may throw; Mission.validate raises ValueError → prints and 1
run-task scope blockedqueue.block_task(task_id, reason) → 1
run-task risk blockedqueue.block_task → 1
run-task needs human approvalqueue.update_task_status(needs_approval) → 1 (stub executor has no handler)
Unknown task/finding idERROR: Task ... not found. → 1

Invariants

  • _require_mission is the single chokepoint for mission resolution; all 10 mission-operating commands use it.
  • init-mission never takes --mission-id (it mints a new M-...).
  • list-scope's ScopeGate is constructed from the DB mission's allowed_assets/disallowed_assets/forbidden_actions/risk_profile — not from CLI args.
  • run-task never executes without going through ScopeGate then RiskController.

Security Boundaries

  • Every run-task execution is scope-gated (ScopeGate) and risk-gated (RiskController).
  • ScopeGate enforces: deny-rules first, allow-rules required, forbidden-action exact + substring hard-blocks, third-party flag, sliding-window rate limit (_RateBucket), and high-risk human-approval flag.
  • RiskController enforces: task/command budgets, action-category permission (exploit/pivot/credential), destructive-pattern deny, dangerous-tool deny, high-risk profile gate.

Tests

Test fileCovers
tests/test_cli_mission_id.py--mission-id parent parser, resume by id vs latest active, init-mission exclusion
tests/test_mission.pyMission.validate, _classify_asset, normalization, controller creation
tests/test_task_queue.pycreate_task/get_next_task/block/reset_stale used by next-task/list-tasks/run-task
tests/test_agent_loop.pyLoop-parity: task lifecycle + scope/risk gates

Run: python -m pytest tests/test_cli_mission_id.py tests/test_agent_loop.py -v

Common Changes

ChangeWhere
Add a subcommandcli.py:504 build_parser subparser + new cmd_* function + wire set_defaults(func=...)
Change mission resolutioncli.py:51 _load_mission / cli.py:74 _require_mission
Adjust scope renderingcli.py:165 cmd_list_scope
Extend run-task pipelinecli.py:264 cmd_run_task (keep scope→risk→approval ordering)

Update This Document When

  • A subcommand is added, removed, or changes its arguments/DB writes.
  • --mission-id handling or active-mission selection changes.
  • run-task pipeline (gates, executor/observer wiring) is altered.
  • docs/architecture.md §Entry Points — cli.py commands
  • docs/runtime-flows.md §Database-Backed Research Loop
  • agent_loop.py (docs/components/flow-b/agent-loop.md) — autonomous version of the same loop
  • docs/database-mission.md — DB layout + mission persistence
source: repo docs (build sync)Edit this page on GitHub →