{"uuid": "8f32c164-48af-47d0-821c-0f0254ceb2d2", "vulnerability_lookup_origin": "1a89b78e-f703-45f3-bb86-59eb712668bd", "author": "9f56dd64-161d-43a6-b9c3-555944290a09", "vulnerability": "CVE-2024-1234", "type": "seen", "source": "https://gist.github.com/meethune/1b264365b640a7e162f228b8ed74666a", "content": "\n\n\n\n\nArgicida \u2014 Wiki\n\n\n\n\n*{margin:0;padding:0;box-sizing:border-box}\n:root{\n  --bg:#ffffff;--sidebar-bg:#f8f9fb;--border:#e5e7eb;\n  --text:#1e293b;--text-muted:#64748b;--primary:#2563eb;\n  --primary-soft:#eff6ff;--hover:#f1f5f9;--code-bg:#f1f5f9;\n  --radius:8px;--shadow:0 1px 3px rgba(0,0,0,.08);\n}\nbody{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;\n  line-height:1.65;color:var(--text);background:var(--bg)}\n\n.layout{display:flex;min-height:100vh}\n.sidebar{width:280px;background:var(--sidebar-bg);border-right:1px solid var(--border);\n  position:fixed;top:0;left:0;bottom:0;overflow-y:auto;padding:24px 16px;\n  display:flex;flex-direction:column;z-index:10}\n.content{margin-left:280px;flex:1;padding:48px 64px;max-width:960px}\n\n.sidebar-header{margin-bottom:20px;padding-bottom:16px;border-bottom:1px solid var(--border)}\n.sidebar-title{font-size:16px;font-weight:700;color:var(--text);display:flex;align-items:center;gap:8px}\n.sidebar-title svg{flex-shrink:0}\n.sidebar-meta{font-size:11px;color:var(--text-muted);margin-top:6px}\n.nav-section{margin-bottom:2px}\n.nav-item{display:block;padding:7px 12px;border-radius:var(--radius);cursor:pointer;\n  font-size:13px;color:var(--text);text-decoration:none;transition:all .15s;\n  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n.nav-item:hover{background:var(--hover)}\n.nav-item.active{background:var(--primary-soft);color:var(--primary);font-weight:600}\n.nav-item.overview{font-weight:600;margin-bottom:4px}\n.nav-children{padding-left:14px;border-left:1px solid var(--border);margin-left:12px}\n.nav-group-label{font-size:11px;font-weight:600;color:var(--text-muted);\n  text-transform:uppercase;letter-spacing:.5px;padding:12px 12px 4px;user-select:none}\n.sidebar-footer{margin-top:auto;padding-top:16px;border-top:1px solid var(--border);\n  font-size:11px;color:var(--text-muted);text-align:center}\n\n.content h1{font-size:28px;font-weight:700;margin-bottom:8px;line-height:1.3}\n.content h2{font-size:22px;font-weight:600;margin:32px 0 12px;padding-bottom:6px;border-bottom:1px solid var(--border)}\n.content h3{font-size:17px;font-weight:600;margin:24px 0 8px}\n.content h4{font-size:15px;font-weight:600;margin:20px 0 6px}\n.content p{margin:12px 0}\n.content ul,.content ol{margin:12px 0 12px 24px}\n.content li{margin:4px 0}\n.content a{color:var(--primary);text-decoration:none}\n.content a:hover{text-decoration:underline}\n.content blockquote{border-left:3px solid var(--primary);padding:8px 16px;margin:16px 0;\n  background:var(--primary-soft);border-radius:0 var(--radius) var(--radius) 0;\n  color:var(--text-muted);font-size:14px}\n.content code{font-family:'SF Mono',Consolas,'Courier New',monospace;font-size:13px;\n  background:var(--code-bg);padding:2px 6px;border-radius:4px}\n.content pre{background:#1e293b;color:#e2e8f0;border-radius:var(--radius);padding:16px;\n  overflow-x:auto;margin:16px 0}\n.content pre code{background:none;padding:0;font-size:13px;line-height:1.6;color:inherit}\n.content table{border-collapse:collapse;width:100%;margin:16px 0}\n.content th,.content td{border:1px solid var(--border);padding:8px 12px;text-align:left;font-size:14px}\n.content th{background:var(--sidebar-bg);font-weight:600}\n.content img{max-width:100%;border-radius:var(--radius)}\n.content hr{border:none;border-top:1px solid var(--border);margin:32px 0}\n.content .mermaid{margin:20px 0;text-align:center}\n\n.menu-toggle{display:none;position:fixed;top:12px;left:12px;z-index:20;\n  background:var(--bg);border:1px solid var(--border);border-radius:var(--radius);\n  padding:8px 12px;cursor:pointer;font-size:18px;box-shadow:var(--shadow)}\n@media(max-width:768px){\n  .sidebar{transform:translateX(-100%);transition:transform .2s}\n  .sidebar.open{transform:translateX(0);box-shadow:2px 0 12px rgba(0,0,0,.1)}\n  .content{margin-left:0;padding:24px 20px;padding-top:56px}\n  .menu-toggle{display:block}\n}\n.empty-state{text-align:center;padding:80px 20px;color:var(--text-muted)}\n.empty-state h2{font-size:20px;margin-bottom:8px;border:none}\n\n\n\n\n&#9776;\n\n\n\n\n\n\n\n\n\nArgicida\n\n\n\n\n\n\n\nGenerated by GitNexus\n\n\n\n\n\nLoading\u2026\n\n\n\nvar PAGES = {\"container-agent\":\"# Container Agent\\n\\n# Container Agent (`argicida_agent`)\\n\\nThe container agent is a minimal process that runs inside the Argicida tool pod. It listens on a Unix socket, authenticates incoming connections from the host, and executes security tool binaries on demand. It never decides *what* to run \u2014 it only executes commands it receives and streams results back.\\n\\nThis is one of three workspace packages. It depends only on `argicida_common` (for IPC protocol types and constants) and `msgspec` (for serialization). No Podman, no Pydantic, no network libraries \u2014 keeping the attack surface inside the container as small as possible.\\n\\n## How It Fits Together\\n\\n```mermaid\\ngraph LR\\n    Host[\\\"Host (Hermes + argicida plugin)\\\"] --&gt;|Unix socket| Agent[\\\"Container Agent\\\"]\\n    Agent --&gt;|create_subprocess_exec| Nmap[nmap]\\n    Agent --&gt;|create_subprocess_exec| Nuclei[nuclei]\\n    Agent --&gt;|create_subprocess_exec| Tools[\\\"...other tools\\\"]\\n```\\n\\nThe host-side `IpcClient` connects to the agent's Unix socket, authenticates with HMAC, and sends `IpcCommand` messages. The agent resolves the requested tool to a binary on `$PATH`, runs it via `asyncio.create_subprocess_exec`, captures stdout/stderr, and returns an `IpcResponse`. One connection can carry many sequential commands.\\n\\n## Entrypoint (`__main__.py`)\\n\\n`python3 -m argicida_agent [--socket /ipc/agent.sock]`\\n\\nStartup does three things:\\n\\n1. **Reads the session key** from `ARGICIDA_SESSION_KEY` (hex-encoded, 32 bytes). Exits immediately if missing, malformed, or wrong length \u2014 there is no unauthenticated mode.\\n\\n2. **Builds the allowed-tools map** from `argicida_common.constants.TOOL_NAMES`, a dict mapping logical tool names (e.g., `\\\"nmap\\\"`) to binary names (e.g., `\\\"nmap\\\"`). The agent will refuse to execute anything not in this map.\\n\\n3. **Starts `AgentServer`** on the socket path and blocks on `serve_forever()`. Cleanup runs in a `finally` block \u2014 the server stops and the socket file is removed.\\n\\nThe default socket path `/ipc/agent.sock` lives on a bind-mounted directory shared with the host. The container entrypoint script launches this module after other setup (Chromium CDP, config copying) completes.\\n\\n## IPC Server (`ipc/server.py`)\\n\\n`AgentServer` manages the Unix socket listener, connection lifecycle, authentication, and command dispatch.\\n\\n### Connection Lifecycle\\n\\nEach inbound connection goes through:\\n\\n1. **Admission** \u2014 rejected immediately if `_active_connections &gt;= MAX_CONNECTIONS`.\\n2. **Authentication** \u2014 HMAC challenge-response within `AUTH_TIMEOUT_S`.\\n3. **Command loop** \u2014 sequential command execution until the client disconnects or `COMMAND_IDLE_TIMEOUT_S` elapses between commands.\\n\\nConnections are handled as `asyncio.Task`s tracked in `_tasks`, so `stop()` can cancel all in-flight work.\\n\\n### Authentication\\n\\nThe agent generates a random nonce (`IPC_NONCE_SIZE` bytes) and sends it as an `IpcAuthChallenge`. The client must reply with an `IpcAuthResponse` containing a valid HMAC digest computed over the nonce using the shared session key. On failure, the connection is dropped with a `ProtocolError`.\\n\\nThis proves the client holds the same session key that was injected into the container's environment at pod creation \u2014 a connection from an unrelated process (or a different pod) will fail.\\n\\n### Command Loop\\n\\nAfter authentication, the server enters a receive-execute-respond loop. Each iteration:\\n\\n1. Calls `recv_message()` to read and verify the next `IpcCommand` (HMAC-verified, with a monotonically increasing sequence number to prevent replay).\\n2. Passes the command to `execute_command()`.\\n3. Sends the `IpcResponse` back via `send_message()` (also HMAC-signed with the same sequence number).\\n\\nCommands are processed sequentially per connection. The host-side `IpcClient` is per-thread, so parallel tool calls from Hermes each get their own connection and sequence counter.\\n\\n### Socket Permissions\\n\\nThe socket must be accessible across the rootless Podman UID namespace boundary \u2014 the host runs as one UID, the container agent as another. `start()` sets the parent directory to `0o777` and creates the socket with `umask(0o000)`. Both `start()` and `stop()` check for symlinks before touching the socket path.\\n\\n## Command Executor (`executor/runner.py`)\\n\\n`execute_command()` is a pure async function: `IpcCommand` in, `IpcResponse` out. It never raises \u2014 all error conditions produce an `IpcResponse` with `exit_code=-1` and a descriptive `error` field.\\n\\n### Validation Steps\\n\\n1. **Tool allowlist** \u2014 rejects tools not in `allowed_tools`.\\n2. **Environment validation** \u2014 calls `validate_ipc_command_env()` to block dangerous environment variables (prevents the host from injecting `LD_PRELOAD`, `PATH` overrides, etc.).\\n3. **Binary resolution** \u2014 uses `shutil.which()` to find the binary on `$PATH`. Fails explicitly if missing.\\n\\n### Execution\\n\\nThe binary runs via `asyncio.create_subprocess_exec` \u2014 never `shell=True`. If the command specifies environment variables (`cmd.env`), they are merged into the current environment (not replacing it, so tool binaries can still find shared libraries and configs).\\n\\nA `wait_for` wrapper enforces `cmd.timeout_s`. On timeout, the process is killed and an error response is returned.\\n\\n### Output Truncation\\n\\nstdout and stderr are capped at `MAX_IPC_STDOUT_SIZE` and `MAX_IPC_STDERR_SIZE` respectively. Truncation happens at the byte level, then the result is decoded with `errors=\\\"replace\\\"`. A post-decode check handles the edge case where `U+FFFD` replacement characters push the UTF-8 encoded length past the limit \u2014 the last character is trimmed. A defensive `RuntimeError` fires if truncation logic ever fails to maintain the invariant.\\n\\nWhen truncation occurs, the `error` field in the response describes what was truncated. The host stores full output to disk separately via the lossless output storage system, so truncation only affects what travels over IPC.\\n\\n## Key Constants (from `argicida_common`)\\n\\n| Constant | Purpose |\\n|---|---|\\n| `IPC_HMAC_KEY_SIZE` | Required session key length (32 bytes) |\\n| `IPC_NONCE_SIZE` | Authentication challenge nonce size |\\n| `MAX_CONNECTIONS` | Concurrent connection cap |\\n| `AUTH_TIMEOUT_S` | Time limit for HMAC handshake |\\n| `COMMAND_IDLE_TIMEOUT_S` | Inactivity timeout between commands |\\n| `MAX_IPC_STDOUT_SIZE` | Byte cap on stdout in IPC response |\\n| `MAX_IPC_STDERR_SIZE` | Byte cap on stderr in IPC response |\\n| `TOOL_NAMES` | Canonical map of tool name \u2192 binary name |\\n\\n## Security Properties\\n\\n- **No shell execution.** All subprocesses use `create_subprocess_exec` with explicit argument lists.\\n- **Closed tool set.** Only binaries listed in `TOOL_NAMES` can be invoked. The map is compiled into the package, not configurable at runtime.\\n- **Authenticated IPC.** Every connection must pass HMAC challenge-response. Every command and response is individually HMAC-signed with a sequence counter (replay protection).\\n- **Environment sanitization.** `validate_ipc_command_env()` blocks dangerous variable names before they reach the subprocess.\\n- **Symlink rejection.** Socket path is checked for symlinks before creation and removal.\\n- **No decisions.** The agent does not interpret tool output, choose targets, or sequence operations. It is a subprocess executor with an authenticated transport \u2014 nothing more.\",\"container-build-infrastructure\":\"# Container Build Infrastructure\\n\\n# Container Build Infrastructure\\n\\nArgicida runs all scan tools inside rootless Podman containers, isolated from the host by a dual-bridge network topology. Three container images make up the runtime:\\n\\n- **argicida-tools** \u2014 the tool pod where scans execute\\n- **argicida-gateway** \u2014 the routing chokepoint that enforces network scope\\n- **argicida-zap** \u2014 the ZAP passive/active proxy sidecar\\n\\nAll three are built from files under `containers/`. None of these containers run standalone \u2014 they are orchestrated by `pod/launcher.py` at runtime.\\n\\n```mermaid\\ngraph LR\\n    Host[\\\"Host (Hermes + Plugin)\\\"]\\n    Host -- \\\"Unix socket /ipc/agent.sock\\\" --&gt; Tools[\\\"argicida-tools\\\"]\\n    Tools -- \\\"all egress\\\" --&gt; GW[\\\"argicida-gateway\\\"]\\n    GW -- \\\"FORWARD chain\\\" --&gt; Target[\\\"Target Network\\\"]\\n    Tools -. \\\"proxy port 8090\\\" .-&gt; ZAP[\\\"argicida-zap\\\"]\\n    ZAP -- \\\"egress\\\" --&gt; GW\\n```\\n\\n## Build Commands\\n\\n```bash\\n# Tool container (standard build)\\npodman build -f containers/Containerfile -t argicida-tools:dev .\\n\\n# Gateway (lightweight Alpine)\\npodman build -f containers/Containerfile.gateway -t argicida-gateway:dev .\\n\\n# ZAP sidecar (--network=private required: ZAP binds 8080 during add-on install)\\npodman build --network=private -f containers/Containerfile.zap -t argicida-zap:dev .\\n```\\n\\nThe `--network=private` flag on the ZAP build is not optional. ZAP's `-cmd` mode binds port 8080 during add-on installation. Without private networking, the build shares the host network namespace and fails if anything else holds port 8080.\\n\\n---\\n\\n## Tool Container (`Containerfile`)\\n\\n### Two-Stage Build\\n\\n**Stage 1 (`alpine:3.21`, pinned by digest)** downloads and verifies Go binaries, nuclei templates, and SecLists wordlists. Every download has a hardcoded SHA256 checksum \u2014 checksums are NOT fetched from the same origin as the binary (that would be pointless).\\n\\n**Stage 2 (`kalilinux/kali-rolling`, pinned by digest)** assembles the runtime image with system packages, the verified binaries, the Python agent, and tool configs.\\n\\nThe stage split is deliberate: code changes to `argicida_agent` or `argicida_common` only invalidate the later `COPY` + `pip install` layers, not the expensive binary download stage.\\n\\n### Pinned Versions\\n\\nAll tool versions are declared as `ARG` variables at the top of the Containerfile:\\n\\n| Tool | Version | Source |\\n|------|---------|--------|\\n| nuclei | 3.10.0 | ProjectDiscovery |\\n| subfinder | 2.14.0 | ProjectDiscovery |\\n| httpx | 1.9.0 | ProjectDiscovery |\\n| naabu | 2.6.1 | ProjectDiscovery |\\n| dnsx | 1.2.3 | ProjectDiscovery |\\n| katana | 1.1.1 | ProjectDiscovery |\\n| usql | 0.21.4 | xo/usql |\\n| ffuf | 2.1.0 | ffuf |\\n| mongosh | 2.9.2 | mongodb-js |\\n| nuclei-templates | 10.4.5 | ProjectDiscovery |\\n| SecLists | 2026.1 | danielmiessler |\\n\\n**To upgrade a tool:** change the `ARG` version, update the corresponding SHA256 checksum in the `sha256sum -c` block, rebuild. Both values must change together \u2014 a version bump without a checksum update is a build failure, not a security bypass.\\n\\n### System Packages (from Kali)\\n\\nInstalled via `apt-get` in a single layer: `python3`, `python3-pip`, `nmap`, `iputils-tracepath`, `chromium`, `openssh-client`, `smbclient`, `sshpass`, `redis-tools`, `rpcbind`, `curl`.\\n\\n### NSE Script Blocklist\\n\\nThe file `containers/nse-blocklist.txt` lists nmap NSE scripts removed at build time. Scripts are blocked by what they do, not by nmap's category system (which is too coarse \u2014 `\\\"intrusive\\\"` includes both harmless enumeration and remote code execution).\\n\\nFive categories of blocked scripts:\\n\\n- **Denial of Service** \u2014 `broadcast-avahi-dos`, `http-slowloris`, `smb-flood`, etc.\\n- **Remote Code Execution** \u2014 `smb-psexec`, `ssh-run`, `jdwp-exec`, etc.\\n- **Fuzzing** \u2014 `dns-fuzz`, `http-form-fuzzer`\\n- **Data exfiltration** \u2014 `http-exif-spider`\\n- **Brute force** \u2014 all `*-brute` scripts (60+ scripts). Dedicated tools handle credential testing under operator control.\\n- **Dangerous network manipulation** \u2014 `dns-update`, `firewall-bypass`, `sip-call-spoof`, etc.\\n\\nAfter removal, the build runs `nmap --script-updatedb` to regenerate the script database, then verifies no blocked scripts survived. If any remain, the build fails hard.\\n\\nThe blocklist must stay in sync with the `_NSE_BLOCKED_SCRIPTS` set in `tools/normalizers_nmap.py` \u2014 the `NseScriptMatcher` normalizer uses that set to reject blocked script names before they reach the container.\\n\\n### ProjectDiscovery Tool Configs\\n\\nConfig files are baked into `/opt/pd-config/` at build time:\\n\\n| File | Baked to | Purpose |\\n|------|----------|---------|\\n| `nuclei-config.yaml` | `/opt/pd-config/nuclei/config.yaml` | Template paths, interactsh disabled, CDP endpoint, timeouts |\\n| `naabu-config.yaml` | `/opt/pd-config/naabu/config.yaml` | Rate 2000, concurrency 50, full port scan |\\n| `dnsx-config.yaml` | `/opt/pd-config/dnsx/config.yaml` | Retry 2, show responses |\\n| `katana-config.yaml` | `/opt/pd-config/katana/config.yaml` | Disable update check |\\n\\nThese are NOT the runtime config paths. The entrypoint copies them to `$HOME/.config//` at container startup because the rootfs is read-only and `$HOME` is a tmpfs. `XDG_CONFIG_HOME` is intentionally NOT set as a runtime `ENV` \u2014 Chromium inherits it and tries to write crash data there.\\n\\n### Nuclei Templates\\n\\nTemplates are pinned to release tag `v10.4.5` with a hardcoded SHA256. They live at `/opt/nuclei-templates/` (not under `$HOME` \u2014 that path is tmpfs and overlays baked content). The `nuclei-config.yaml` points `templates:` to this path.\\n\\nA custom screenshot template (`containers/screenshot.yaml`) is added at `/opt/nuclei-templates/argicida/screenshot.yaml` for headless page capture via the CDP endpoint.\\n\\n### SecLists Wordlists\\n\\nA curated subset of SecLists is downloaded at build time into `/usr/share/seclists/`:\\n\\n- `Discovery/Web-Content/` \u2014 `common.txt`, `big.txt`, `raft-medium-directories.txt`, `raft-medium-files.txt`, `directory-list-2.3-medium.txt`, `common-api-endpoints-mazen160.txt`, `burp-parameter-names.txt`\\n- `Discovery/DNS/` \u2014 `subdomains-top1million-5000.txt`, `subdomains-top1million-20000.txt`\\n- `Passwords/Common-Credentials/` \u2014 `10k-most-common.txt`\\n\\n### Non-Root Agent User\\n\\nThe container creates a `agent` user (UID 10000) and runs the entrypoint as that user. The IPC directory `/ipc` is owned by this user. At runtime, the host bind-mounts its IPC tempdir over `/ipc` with `U`/`chown` flags so UID 10000 inside the container can write to it.\\n\\n### Healthcheck\\n\\nVerifies the agent is accepting connections by attempting a Unix socket connect to `/ipc/agent.sock`. Runs every 30s with a 5s timeout and 3 retries.\\n\\n---\\n\\n## Tool Container Entrypoint (`entrypoint.sh`)\\n\\nThe entrypoint runs four phases before starting the agent:\\n\\n**1. Config staging** \u2014 copies PD tool configs from `/opt/pd-config/` to `$HOME/.config/`. Also copies `.nuclei-ignore` from the template directory to the nuclei config directory (nuclei expects it in the config dir, not the template dir).\\n\\n**2. ZAP CA trust** \u2014 if `/etc/argicida/zap-ca.pem` exists (mounted by `pod/launcher.py` when ZAP is active), it appends the ZAP CA certificate to the system CA bundle and sets `SSL_CERT_FILE` + `REQUESTS_CA_BUNDLE` so all HTTPS traffic from tools trusts the ZAP intercepting proxy.\\n\\n**3. Chromium CDP** \u2014 launches a persistent headless Chromium instance on port 9222 with `--headless=new`. When ZAP is active, Chromium is configured with `--proxy-server=http://127.0.0.1:8090` so headless crawl traffic (from katana, nuclei) flows through ZAP for passive analysis. The entrypoint waits up to 10 seconds for CDP readiness, then resolves the full WebSocket URL (including the dynamic GUID), patches it into the nuclei config, and writes it to `/ipc/cdp-endpoint` (0o644) for host-side normalizers to read.\\n\\n**4. Agent start** \u2014 `exec python3 -m argicida_agent --socket /ipc/agent.sock`. The `exec` replaces the shell process so signals propagate correctly.\\n\\n---\\n\\n## Gateway Container (`Containerfile.gateway`)\\n\\nA minimal Alpine image with five packages: `nftables`, `dnsmasq-dnssec-nftset`, `wireguard-tools`, `openvpn`, `iproute2`.\\n\\nThree operating modes controlled by `GATEWAY_VPN_MODE` env var:\\n\\n| Mode | Behavior |\\n|------|----------|\\n| `direct` (default) | nftables scope enforcement only, `sleep infinity` |\\n| `wireguard` | WireGuard tunnel + nftables, `sleep infinity` |\\n| `openvpn` | OpenVPN tunnel + nftables, monitor tunnel process |\\n\\n### Gateway Entrypoint (`gateway-entrypoint.sh`)\\n\\n**Phase 1: nftables** \u2014 loads scope rules from `/etc/argicida/scope-rules.nft` (bind-mounted by the host from `scope_dir`). Validates syntax with `nft -c` before applying. Refuses to start if no rules file exists \u2014 this is the fail-closed guarantee.\\n\\n**Phase 2: dnsmasq** \u2014 if `/etc/argicida/dnsmasq-scope.conf` is present, starts dnsmasq as the DNS forwarder on the internal network interface. The tool pod's `resolv.conf` points to the gateway IP (`10.89.1.2`).\\n\\n**Phase 3: VPN** \u2014 starts the configured tunnel or sleeps. OpenVPN mode monitors the tunnel process and exits the gateway if it dies, triggering pod restart (fail-closed on tunnel loss).\\n\\n### Healthcheck\\n\\nChecks that the `inet argicida forward` nftables chain exists and is loadable. If scope enforcement is not running, the container is unhealthy.\\n\\n---\\n\\n## ZAP Sidecar (`Containerfile.zap`)\\n\\nBuilt from `ghcr.io/zaproxy/zaproxy:2.17.0` (the stable full image, not `:bare` which lacks version-pinned tags).\\n\\n### Add-on Installation\\n\\nSix add-ons are installed beyond the base image bundle via `zap.sh -cmd -addoninstall`:\\n\\n- `reports`, `scanpolicies`, `openapi`, `client` \u2014 functional extensions\\n- `ascanrulesBeta` \u2014 adds backup file disclosure, CORS, SSRF, NoSQL injection, session fixation, and other active scan rules\\n- `ascanrulesAlpha` \u2014 adds LDAP injection, SQLite SQLi, web cache deception\\n- `wappalyzer` \u2014 technology fingerprinting (used by `zap_intel` tool)\\n\\n### JVM Heap Sizing (`zap-entrypoint.sh`)\\n\\nZAP's `zap.sh` reads `$HOME/.ZAP/.ZAP_JVM.properties` for JVM flags. Without this file, it falls back to `/proc/meminfo` which reports HOST memory \u2014 wrong inside a container. The ZAP entrypoint reads the cgroup v2 memory limit from `/sys/fs/cgroup/memory.max` (populated by Podman), calculates 75% as `-Xmx`, writes it to `.ZAP_JVM.properties`, then `exec`s `zap.sh`. Falls back to 2048MB if the limit is unreadable or `\\\"max\\\"` (unlimited).\\n\\n---\\n\\n## SSH Key Helper (`ssh-key-wrap`)\\n\\nA Python script installed at `/usr/local/bin/ssh-key-wrap`. Solves a specific cross-namespace permission problem: the IPC directory uses 0o644 for files (required for cross-UID-namespace access), but SSH requires 0o600 on identity files.\\n\\nThe helper copies the key from the IPC path to `/tmp/argicida-ssh-key.pem` with 0o600 permissions, then `os.execvp`s into `ssh` with the corrected key path. The fixed output path means successive calls overwrite rather than accumulate \u2014 cleanup is impossible since `execvp` replaces the process, but the container's tmpfs is destroyed at pod teardown.\\n\\n---\\n\\n## How This Connects to the Runtime\\n\\nThe host-side code that orchestrates these containers lives in `pod/launcher.py`. Key integration points:\\n\\n- `launch_pod()` creates the dual-bridge network topology, starts the gateway first, waits for its FORWARD chain, then creates the tool pod on the internal network only\\n- `scope/netfilter.py` generates the nftables ruleset that gets bind-mounted into the gateway at `/etc/argicida/scope-rules.nft`\\n- `tools/normalizers_nmap.py` maintains `_NSE_BLOCKED_SCRIPTS` which must match `containers/nse-blocklist.txt`\\n- `tools/normalizers_katana.py` reads `/ipc/cdp-endpoint` (written by the tool entrypoint) to inject `-chrome-ws-url` into katana invocations\\n- Tool definitions in `tools/defs/` reference binary names that must exist in the tool container (e.g., `BINARY_NAME = \\\"nuclei\\\"`)\\n- The `PodSession` object tracks all topology resources and drives teardown via `cleanup_topology_by_name()`\",\"engagement-database\":\"# Engagement &amp; Database\\n\\n# Engagement &amp; Database\\n\\nThe engagement database is the persistence backbone for Argicida's penetration testing sessions. It stores everything the LLM and operator produce during an engagement: scope definitions, scan findings, discovered assets, credential fragments, evidence chains, and VPN configuration. All state lives in a single encrypted SQLite file at `$HERMES_HOME/argicida/engagements/engagements.db`, accessed through APSW for thread-safe concurrent use from Hermes's parallel tool dispatch.\\n\\n## Store Architecture\\n\\n`EngagementStore` (`db/store.py`) is the public facade. It owns the APSW connection and delegates domain-specific persistence to five focused repositories, each sharing the same connection:\\n\\n```\\nEngagementStore\\n\u251c\u2500\u2500 .assets:      AssetRepo         # IP-anchored host registry\\n\u251c\u2500\u2500 .findings:    FindingRepo       # Scan results with dedup + triage\\n\u251c\u2500\u2500 .credentials: CredentialRepo    # Validated creds, principals, secrets\\n\u251c\u2500\u2500 .evidence:    EvidenceRepo      # Hash-chained audit trail\\n\u2514\u2500\u2500 .vpn:         VpnRepo           # WireGuard/OpenVPN config references\\n```\\n\\nCallers access sub-repos directly \u2014 `store.assets.upsert_asset(eid, ip)`, `store.findings.add_finding(eid, finding)` \u2014 while engagement-level CRUD (create, get, list, rename, lock/unlock, update phase/scope) stays on the store itself.\\n\\nThe store constructor enforces security invariants before any data touches disk:\\n- Rejects symlinks at the DB path via `O_NOFOLLOW` (TOCTOU-safe)\\n- Sets `0o600` on the file and `0o700` on the parent directory\\n- Enables WAL mode, foreign keys, and a 5-second busy timeout\\n- Runs schema creation followed by `_migrate_schema()` for forward compatibility\\n\\n### Transactions\\n\\nWrap multiple operations in `store.transaction()` for atomicity. Nested calls use SQLite SAVEPOINTs automatically via APSW's connection manager. Pass `immediate=True` when write contention is expected (the locking path uses this internally).\\n\\n```python\\nwith store.transaction():\\n    asset_id = store.assets.upsert_asset(eid, \\\"10.0.0.1\\\")\\n    store.assets.add_port(asset_id, 22, \\\"tcp\\\", service=\\\"ssh\\\")\\n    store.assets.add_hostname(asset_id, \\\"target.local\\\", source=\\\"dnsx\\\")\\n```\\n\\n### Async Bridge\\n\\nTool handlers run in Hermes's async event loop but APSW is synchronous. The store provides `async_get_engagement()`, `async_get_findings()`, and `async_get_chain_entries()` \u2014 thin `asyncio.to_thread()` wrappers. Don't add more unless a hot path demands it; most callers already run on worker threads via Hermes's `_run_async()`.\\n\\n### Encryption\\n\\n`load_or_generate_db_key()` (`db/encryption.py`) creates a 32-byte random key at `$HERMES_HOME/argicida/keys/db.key` on first use, with `0o600` permissions and atomic write (mkstemp \u2192 rename). The key feeds APSW's SQLite3 Multiple Ciphers via `PRAGMA hexkey`. Symlinks at the key path are rejected.\\n\\n## Engagement Lifecycle\\n\\n```mermaid\\nstateDiagram-v2\\n    [*] --&gt; Unbound\\n    Unbound --&gt; Bound[\\\"Bound:\\\"] bind_engagement()\\n    Bound --&gt; Unbound[\\\"Unbound:\\\"] unbind_engagement()\\n    Bound --&gt; Bound[\\\"Bound:\\\"] bind_engagement() [auto-unbinds previous]\\n```\\n\\nEngagements are created via `store.create_engagement(scope, name)` (typically from the `/scope` slash command or `argicida_scope_define` tool). Binding attaches an engagement to the current Hermes session, making it the active target for all tool calls.\\n\\n### bind_engagement()\\n\\n`engagement/lifecycle.py:bind_engagement()` orchestrates the full bind sequence:\\n\\n1. **Auto-unbind** \u2014 if another engagement is already bound, unbinds it first (signs the evidence chain, unlocks, clears state)\\n2. **Lookup** \u2014 resolves by name via `store.get_engagement_by_name()`\\n3. **Delegate to `EngagementSession.bind()`** \u2014 which handles locking, artifact directory creation, key loading, evidence chain seeding, and `last_bound_at` update\\n4. **Replay hostname mappings** \u2014 iterates the asset registry and calls `add_host_entry(hostname, ip)` to repopulate dnsmasq in the gateway container, so DNS resolution from previous sessions is immediately available\\n5. **Backfill assets** \u2014 if the asset registry is empty but findings exist (legacy data), extracts IP addresses from finding targets and creates asset stubs\\n\\nThe `add_host_entry` callback is optional for testability but required in production \u2014 without it, hostname resolution from prior sessions won't work until the tools rediscover them.\\n\\n### EngagementSession\\n\\n`engagement/session.py:EngagementSession` is a dataclass that owns all resources created during binding:\\n\\n| Field | Purpose |\\n|---|---|\\n| `engagement_id` | UUID of the bound engagement |\\n| `signing_key` | Ed25519 private key for evidence chain signatures |\\n| `public_key` | Corresponding public key (stored alongside findings) |\\n| `artifacts_dir` | `$HERMES_HOME/argicida/artifacts//` (0o700) |\\n\\n`EngagementSession.bind()` is the atomic resource-acquisition method. If any step after locking fails (directory creation, key loading, chain seeding), it unlocks the engagement before re-raising \u2014 no leaked locks.\\n\\n### Locking\\n\\n`store.lock_engagement(eid, session_id)` uses an IMMEDIATE transaction to prevent concurrent binds. A lock held by a different session is honored for 12 hours (`_STALE_LOCK_SECONDS`), after which it's considered stale and overwritten. The same session can re-lock freely (idempotent rebind).\\n\\n### unbind_engagement()\\n\\nSigns the evidence chain with the session's Ed25519 key, persists the signature count and blob to `chain_signatures`, unlocks the engagement, clears `state.engagement_session`, and resets the in-memory chain. Each step is independently try/excepted \u2014 unbind must not fail halfway and leave state inconsistent.\\n\\n## Sub-Repositories\\n\\n### AssetRepo (`db/asset_repo.py`)\\n\\nIP-anchored asset model tracking hosts, their ports, hostnames, and enumeration progress. Every asset is identified by its IP address within an engagement (unique index on `(engagement_id, ip)`).\\n\\n**Coverage state machine** \u2014 tracks how thoroughly each host has been enumerated:\\n\\n```mermaid\\nstateDiagram-v2\\n    discovered --&gt; ports_scanned\\n    ports_scanned --&gt; hostnames_probed\\n    hostnames_probed --&gt; vgroups_identified\\n    vgroups_identified --&gt; enumerated\\n```\\n\\nTransitions are forward-only via `update_coverage_state()`. However, discovering new ports or hostnames _resets_ coverage backward via `_reset_coverage_if_needed()` \u2014 if a new port appears on a host that was already at `vgroups_identified`, coverage drops back to `ports_scanned` because the new port hasn't been through the later stages.\\n\\n**Vhost fingerprinting** \u2014 `compute_vhost_groups()` groups hostnames by their structural fingerprint (status code, content-length, headers from httpx), assigning a UUID per distinct group. This identifies which hostnames are aliases for the same application vs. distinct virtual hosts. Groups are only regenerated when the fingerprint-to-hostname mapping actually changes.\\n\\n**Target resolution** \u2014 `resolve_target(eid, target)` accepts an IP or hostname and returns the asset ID. Normalizes the target first (strips URL schemes, ports, paths via `normalize_target()`), tries IP lookup, then falls back to hostname table join. Used by the coverage map builder and finding linker.\\n\\n### FindingRepo (`db/finding_repo.py`)\\n\\nStores scan findings with deduplication and triage-level promotion. Each finding is stored as a JSON blob alongside extracted metadata columns for query performance.\\n\\n**Deduplication** \u2014 `_finding_dedup_key()` generates a key from `target:port:cve_id` (preferred) or `target:port:title`. When a duplicate arrives, the triage level is compared \u2014 `critical &gt; interesting &gt; noise` \u2014 and the stored finding is upgraded if the new one has higher severity. This means re-scanning a host with a better scanner won't create duplicate entries but will promote findings that were previously classified as noise.\\n\\n**Query API** \u2014 `query_findings()` supports filtering by target, severity, triage level, tool, KEV-only, port, and time window. Returns dicts without `raw_output` (which lives on disk in the output store, not in the DB) to keep LLM context lean. `query_hosts()`, `query_ports()`, `query_cves()`, and `query_tools_run()` provide aggregate views for the briefing generator and `/triage` command.\\n\\n### CredentialRepo (`db/credential_repo.py`)\\n\\nThree-table fragment model separating principals (usernames, accounts), secrets (passwords, hashes, keys), and validated credentials (confirmed working combinations).\\n\\n**Candidate ranking engine** \u2014 `get_credential_candidates()` generates prioritized login attempts from the principal and secret fragments. Six tiers, evaluated in order:\\n\\n| Tier | Logic |\\n|---|---|\\n| `same_row` | Secret has `associated_principal` linking it to a specific principal |\\n| `same_target` | Principal and secret discovered on the same IP |\\n| `same_service` | Principal and secret from the same service type, different IPs |\\n| `default_pairing` | Principal + known default password for the service (from `_SERVICE_DEFAULTS`) |\\n| `cross_service_reuse` | Principal from one service + validated credential from another service on same host |\\n| `spray` | Cartesian product (opt-in via `include_spray=True`) |\\n\\nAlready-validated combinations are excluded. The ranking stops as soon as `limit` candidates are generated \u2014 early tiers are cheap, spray is expensive.\\n\\n**Deduplication** \u2014 all three tables use SHA256 fingerprints on key fields with unique indexes. `add_validated_credential()`, `add_principal()`, and `add_secret()` return `True` if new, `False` if duplicate (caught via `apsw.ConstraintError`).\\n\\n### EvidenceRepo (`db/evidence_repo.py`)\\n\\nStores the hash-chained audit trail. Each `ChainEntry` records a finding hash, the previous entry's hash, a sequence number, and the full finding JSON. `get_chain_entries()` returns entries ordered by sequence for chain verification and session-resume seeding.\\n\\n`add_chain_signature()` stores Ed25519 signatures over the chain at unbind time, recording the session ID and entry count at signing. This provides non-repudiation \u2014 the engagement's findings can be verified against the operator's key pair.\\n\\n### VpnRepo (`db/vpn_repo.py`)\\n\\nStores per-engagement VPN mode (`direct`, `wireguard`, `openvpn`) and config directory path. `validate_vpn_config_dir()` checks that the directory exists, isn't a symlink, and contains the expected config file (`wg0.conf` or `client.ovpn` per `VPN_CONFIG_FILES` from `argicida_common.constants`).\\n\\n## Schema &amp; Migrations\\n\\n`db/migrations.py` contains both the initial `_SCHEMA_STATEMENTS` (4 CREATE TABLE statements for the original schema) and `_migrate_schema()` which handles all subsequent evolution. Migrations are idempotent \u2014 they check for column/table/index existence before altering. This means the same migration code runs on fresh databases and on databases from any prior version.\\n\\nNotable migration behaviors:\\n- Drops the removed `engagement_type` column if present\\n- Reshapes phase enum values: `passive_recon`/`active_recon` \u2192 `discovery`, `interactive` \u2192 `human_gate`\\n- Creates asset registry tables (`assets`, `asset_hostnames`, `asset_ports`, `asset_vhost_scans`) and credential fragment tables (`principals`, `secrets`) if absent\\n- Adds dedup infrastructure (unique indexes on fingerprint columns)\\n\\n`ensure_schema()` is exported for test fixtures that need a clean database without going through the full `EngagementStore` constructor.\\n\\n## Finding Storage Helper\\n\\n`db/helpers.py:store_finding()` is the single entry point for persisting findings from hooks and tool result processors. It inserts the finding via `FindingRepo.add_finding()`, and if the finding is new and an evidence chain is provided, appends a `ChainEntry` and persists it via `EvidenceRepo.add_chain_entry()`. This ensures the evidence chain and finding store never diverge.\\n\\n```python\\nstore_finding(store, eid, finding, chain, session_id=\\\"sess-abc\\\")\\n```\\n\\nThe `chain` parameter is optional (`None` skips chain appending) for contexts where evidence tracking isn't relevant (e.g., backfill operations).\\n\\n## Thread Safety\\n\\nAPSW provides thread-safe SQLite access \u2014 multiple Hermes worker threads can call store methods concurrently. The IMMEDIATE transaction mode in `lock_engagement()` serializes lock acquisition. The `IpcClient` is per-thread (via `threading.local()`), but the store itself is shared across threads since APSW handles the locking internally. WAL mode allows concurrent readers alongside a single writer.\",\"evidence-reporting\":\"# Evidence &amp; Reporting\\n\\n# Evidence &amp; Reporting\\n\\nThe evidence module maintains a cryptographically verifiable record of every finding produced during a penetration test. The briefing module generates human-readable reports from that data. Together they solve two problems: proving that scan results haven't been tampered with after collection, and presenting those results to an operator at the HUMAN_GATE.\\n\\n## Evidence Chain\\n\\n`evidence/chain.py` \u2014 `EvidenceChain`\\n\\nA hash-linked sequence of findings. Each `ChainEntry` contains the finding itself, a SHA-256 hash computed over domain-tagged content, and the hash of the previous entry. This makes insertions, deletions, and modifications detectable after the fact.\\n\\n### How entries are hashed\\n\\nThe hash input is constructed with domain separation and length-prefixed fields to prevent concatenation attacks:\\n\\n```\\nb\\\"argicida-chain-v1\\\"          # domain tag\\n+ pack(\\\"&gt;Q\\\", sequence)        # 8-byte big-endian sequence number\\n+ pack(\\\"&gt;H\\\", len(timestamp))  # 2-byte length prefix\\n+ timestamp                   # ISO 8601 UTC\\n+ pack(\\\"&gt;H\\\", len(prev_hash))  # 2-byte length prefix\\n+ previous_hash               # hex string of prior entry's hash\\n+ pack(\\\"&gt;I\\\", len(finding))    # 4-byte length prefix\\n+ finding                     # msgpack-encoded Finding struct\\n```\\n\\nThe first entry chains from a genesis hash of 64 zero characters.\\n\\n### Thread safety\\n\\nAll operations acquire `self._lock`. Hermes dispatches parallel tool calls on separate worker threads, and the `post_tool_call` hook (`hooks/evidence.py` \u2192 `capture_evidence`) appends to the chain from whichever thread completes first.\\n\\n### Lifecycle\\n\\nThe chain lives on `PluginState` and follows the engagement lifecycle:\\n\\n- **Created** at plugin registration (empty)\\n- **Seeded** via `seed()` when resuming an existing engagement \u2014 `EngagementSession.bind()` loads stored entries from the database\\n- **Appended** by `capture_evidence` after every tool call that produces findings\\n- **Signed** at session end by the `on_session_end` hook\\n- **Cleared** via `clear()` on engagement switch\\n\\n`seed()` raises `RuntimeError` if the chain already has entries, preventing accidental double-loading.\\n\\n## Signing and Key Management\\n\\n```mermaid\\ngraph LR\\n    A[Tool call completes] --&gt; B[capture_evidence hook]\\n    B --&gt; C[chain.append]\\n    C --&gt; D[ChainEntry with hash]\\n    E[Session ends] --&gt; F[sign_chain]\\n    F --&gt; G[_chain_digest]\\n    G --&gt; H[Ed25519 signature]\\n    I[Verification] --&gt; J[verify_signature]\\n    J --&gt; G\\n```\\n\\n### Chain signing \u2014 `evidence/signer.py`\\n\\n`sign_chain` computes a digest over the entire chain and signs it with an Ed25519 private key. The digest uses its own domain tag (`b\\\"argicida-sign-v1\\\"`) separate from the per-entry tag, covers the entry count, and includes each entry's hash with length prefixes. This means the signature validates both the content and ordering of all entries.\\n\\n`verify_signature` recomputes the same digest and checks it against the signature using the public key. Returns `False` on `InvalidSignature` rather than raising.\\n\\nSigning an empty chain raises `ValueError` \u2014 there's nothing to attest.\\n\\n### Key persistence \u2014 `evidence/keys.py`\\n\\n`load_or_generate_keys` manages per-engagement Ed25519 key pairs at `keys_dir/.key` and `.pub`. The function is idempotent: if keys exist, it loads them; otherwise it generates and persists new ones.\\n\\nSecurity properties:\\n- Private keys are written with `0o600`, public keys with `0o644`\\n- Atomic writes via `tempfile.mkstemp` + `os.rename` \u2014 no partial key files on crash\\n- Symlink rejection on the private key path (`is_symlink()` check before loading)\\n- Key type validation \u2014 rejects non-Ed25519 keys even if the PEM parses successfully\\n- On write failure, cleanup removes both the temp file and any partially-written key pair\\n\\nKeys are generated during `init_state` at plugin registration (via `generate_signing_key` in `signer.py` for the in-memory pair) and persisted per-engagement through `load_or_generate_keys`.\\n\\n## Briefing Generation\\n\\nThe briefing is a hybrid document: a deterministic Markdown template filled with structured data, plus a one-paragraph executive summary generated by the LLM.\\n\\n### Entry point\\n\\n`generate_briefing(llm, store, engagement_id)` is an async function called by the `/briefing` slash command handler. It:\\n\\n1. Loads the engagement and its findings from the `EngagementStore`\\n2. Renders the deterministic template via `render_briefing`\\n3. Generates (or falls back on) the executive summary\\n4. Replaces the `{executive_summary}` placeholder in the template\\n\\n### Template sections \u2014 `briefing/prompts.py`\\n\\n`render_briefing` builds the full briefing document. Sections are included conditionally based on available data:\\n\\n| Section | Source | Always present |\\n|---------|--------|----------------|\\n| Engagement metadata | `EngagementMeta` | Yes |\\n| Executive Summary | LLM or fallback | Yes (placeholder) |\\n| Assets Discovered | `build_asset_summary` from `hooks/context_sections` | If store provided |\\n| Finding Summary | `count_by_triage` from `triage/classifier` | Yes |\\n| Findings Detail | Sorted actionable findings | If critical or interesting findings exist |\\n| Risk Ranking | Actionable findings ranked | If critical or interesting findings exist |\\n| Noise Findings | Grouped by title with counts | If noise findings exist |\\n| Tool Coverage | `build_coverage_map` from `hooks/coverage` | If coverage data exists |\\n| Recommended Next Steps | `build_deep_testing_gaps` + asset completeness | Yes |\\n\\nFinding sort order: KEV status first (known-exploited vulnerabilities surface to the top), then severity descending, CVSS descending, EPSS descending.\\n\\nNoise findings are grouped by title with counts \u2014 `[medium] SSL Certificate Expired (x3)` \u2014 rather than listed individually. Tool completion sentinels (titles ending in \\\"completed\\\") are filtered out.\\n\\n### Executive summary generation\\n\\n`_generate_summary` sends a small prompt (~50-100 tokens) to the LLM via `llm.acomplete()` with `temperature=0.0` and `purpose=\\\"briefing\\\"`. The prompt contains only: scope CIDRs, current phase, triage counts, and the top 3 findings by severity.\\n\\nOn any failure \u2014 LLM unavailable, empty response, exception \u2014 `_fallback_summary` produces a data-only sentence so the briefing is never incomplete:\\n\\n&gt; Assessment of 10.0.0.0/24 identified 2 critical, 5 interesting findings across 31 total. Phase: exploitation.\\n\\n### Next steps derivation\\n\\n`_build_next_steps` pulls from two sources:\\n\\n- **Deep testing gaps** from `hooks/coverage.py` \u2014 credential testing, ZAP active scanning, sensitive file checks, exploitation opportunities the automated scan didn't pursue\\n- **Asset completeness** from the asset registry \u2014 hosts stuck at `discovered` (no port scan) or `ports_scanned` (no hostname/vhost probing)\\n\\nThis makes the \\\"next steps\\\" section reflect actual coverage gaps rather than generic recommendations.\\n\\n## Integration Points\\n\\nThe evidence chain connects to the rest of Argicida through hooks:\\n\\n- **`post_tool_call`** \u2192 `capture_evidence` appends each finding to the chain\\n- **`on_session_end`** \u2192 signs the chain (fires every turn \u2014 lightweight)\\n- **`on_session_start`** \u2192 `bind` seeds the chain from DB when resuming an engagement\\n- **`on_session_finalize`** \u2192 persists the signed chain before teardown\\n- **`/briefing` slash command** \u2192 `generate_briefing` reads findings from the store (not the chain) to produce the report\\n\\nThe chain and the briefing share the `Finding` schema from `argicida_common` but serve different purposes: the chain is an integrity mechanism (ordered, hashed, signed), while the briefing is a presentation layer (grouped, ranked, summarized).\",\"hermes-profile-skills-profile\":\"# Hermes Profile &amp; Skills \u2014 profile\\n\\nI have enough context now. Let me write the documentation.\\n\\n# Hermes Profile &amp; Skills\\n\\nThe `profile/` directory is a [Hermes profile distribution](https://hermes-agent.nousresearch.com/) \u2014 a self-contained, installable package that turns Hermes into an offensive security operator. It contains the LLM's identity (`SOUL.md`), model configuration (`config.yaml`), visual theme (`skins/argicida.yaml`), and 13 technique skills (`skills/security/`) that teach the LLM how to conduct penetration tests using Argicida's containerized toolset.\\n\\nThis is not application code. Nothing in `profile/` is imported by the Python packages in `src/`. Instead, Hermes reads these files at runtime after profile installation, shaping how the LLM behaves during a pentest session.\\n\\n## Installation and Usage\\n\\n```bash\\n# Install the profile (creates ~/.hermes/profiles/pentest/)\\nhermes profile install /path/to/Argicida/profile --alias\\n\\n# Start an interactive pentest session\\npentest chat\\n\\n# Headless single-query mode\\npentest chat -q \\\"scan 10.10.10.40 for open ports and services\\\" -t argicida\\n```\\n\\nThe `--alias` flag registers `pentest` as a shell command pointing at this profile. All runtime state (sessions, memories, logs) lives under `$HERMES_HOME/profiles/pentest/`, not in the repo.\\n\\n## Profile Structure\\n\\n```\\nprofile/\\n\u251c\u2500\u2500 config.yaml          # Model endpoint, agent parameters, delegation, toolsets\\n\u251c\u2500\u2500 distribution.yaml    # Profile metadata (name, version, hermes_requires)\\n\u251c\u2500\u2500 SOUL.md              # LLM identity, autonomy rules, delegation patterns\\n\u251c\u2500\u2500 .no-bundled-skills   # Marker: skills deploy to runtime, not bundled in profile\\n\u251c\u2500\u2500 .gitignore           # Excludes runtime state (sessions/, memories/, state.db)\\n\u251c\u2500\u2500 skins/\\n\u2502   \u2514\u2500\u2500 argicida.yaml    # Visual theme (colors, spinner, ASCII art, tool emojis)\\n\u2514\u2500\u2500 skills/security/\\n    \u251c\u2500\u2500 DESCRIPTION.md\\n    \u251c\u2500\u2500 pentest-methodology/SKILL.md    # Core methodology (always loaded)\\n    \u251c\u2500\u2500 network-discovery/SKILL.md\\n    \u251c\u2500\u2500 dns-enumeration/SKILL.md\\n    \u251c\u2500\u2500 web-enumeration/               # Hub skill with 8 sub-skills\\n    \u2502   \u251c\u2500\u2500 SKILL.md\\n    \u2502   \u251c\u2500\u2500 fingerprinting/SKILL.md\\n    \u2502   \u251c\u2500\u2500 scanning/SKILL.md\\n    \u2502   \u251c\u2500\u2500 crawling/SKILL.md\\n    \u2502   \u251c\u2500\u2500 fuzzing/SKILL.md\\n    \u2502   \u251c\u2500\u2500 api/SKILL.md\\n    \u2502   \u251c\u2500\u2500 api-testing/SKILL.md\\n    \u2502   \u251c\u2500\u2500 curl-techniques/SKILL.md\\n    \u2502   \u2514\u2500\u2500 services/SKILL.md\\n    \u251c\u2500\u2500 db-enumeration/                # Hub skill with 5 sub-skills\\n    \u2502   \u251c\u2500\u2500 SKILL.md\\n    \u2502   \u251c\u2500\u2500 postgresql/SKILL.md\\n    \u2502   \u251c\u2500\u2500 mysql/SKILL.md\\n    \u2502   \u251c\u2500\u2500 mssql/SKILL.md\\n    \u2502   \u251c\u2500\u2500 oracle/SKILL.md\\n    \u2502   \u2514\u2500\u2500 cassandra/SKILL.md\\n    \u251c\u2500\u2500 redis-enumeration/SKILL.md\\n    \u251c\u2500\u2500 mongo-enumeration/SKILL.md\\n    \u251c\u2500\u2500 smb-enumeration/SKILL.md\\n    \u251c\u2500\u2500 rpc-nfs-enumeration/SKILL.md\\n    \u251c\u2500\u2500 ssh-enumeration/SKILL.md\\n    \u251c\u2500\u2500 ftp-enumeration/SKILL.md\\n    \u251c\u2500\u2500 smtp-enumeration/SKILL.md\\n    \u2514\u2500\u2500 telnet-enumeration/SKILL.md\\n```\\n\\n## SOUL.md \u2014 LLM Identity and Autonomy\\n\\n`SOUL.md` defines who the LLM is and what it is permitted to do without operator approval. Hermes loads it as the system prompt for the `pentest` profile. It is divided into five sections:\\n\\n**Identity and style.** Sets the persona as an offensive security operator: evidence-driven, concise, scope-aware. This is behavioral guidance, not tool instructions \u2014 tool usage is taught by skills.\\n\\n**Autonomy rules.** Uses XML-tagged blocks to create an unambiguous decision boundary:\\n\\n- `` \u2014 tools the LLM runs immediately without asking. All 16 scan tools and all management tools are listed exhaustively. Operational patterns (chaining, scope expansion, artifact persistence, screenshot capture, credential querying) are enumerated as continuous duties.\\n- `` \u2014 actions that require operator approval via `/approve-gate`: exploits, payloads, credential brute-force beyond defaults, and anything that modifies target state irreversibly.\\n- `` \u2014 a three-step test applied before every action. The critical insight is step 3: \\\"Uncertain? Execute.\\\" The scope guard enforces boundaries at the infrastructure level, so the LLM doesn't need to self-censor.\\n\\n**Delegation.** The SOUL defines the LLM as an orchestrator that delegates all tool execution to subagents via `delegate_task`. This keeps raw tool output out of the orchestrator's context window. The `` / `` / `` blocks define what gets delegated (all scan tools, triage, briefings), what doesn't (engagement lifecycle, scope changes, cheap DB reads), and what context each delegation must carry (engagement name, scope, target, objective, skill instruction).\\n\\n**Chaining.** Prescribes multi-tool sequences organized by phase (discovery, enumeration, deep testing). The key design constraint: \\\"Do not stop between steps to ask what to do next.\\\" The LLM chains `delegate_task` calls based on results, not operator prompts.\\n\\n**Operator directives.** Handles the \\\"proceed\\\" problem \u2014 when operators say \\\"continue,\\\" the LLM must respond with tool calls, not text summaries. Includes a context-compaction recovery sequence using `engagement_query`.\\n\\n## config.yaml \u2014 Runtime Configuration\\n\\nThe profile configures Hermes for offline, local-LLM pentesting:\\n\\n| Setting | Value | Purpose |\\n|---------|-------|---------|\\n| `model.default` | Gemma 4 12B (GGUF) | Primary model at `localhost:8080` |\\n| `model_aliases.analyze` | Qwen 35B | Heavier model for analysis tasks |\\n| `agent.max_turns` | 150 | Extended turn budget for long assessments |\\n| `agent.disabled_toolsets` | terminal, file, browser, ... | Disables everything except `argicida` tools \u2014 the LLM has no direct host access |\\n| `delegation.max_concurrent_children` | 4 | Parallel subagent cap |\\n| `delegation.child_timeout_seconds` | 0 | No timeout (scans can run long) |\\n| `compression.threshold` | 0.71 | Context compaction trigger |\\n| `compression.protect_last_n` | 35 | Keep recent turns intact |\\n| `tool_loop_guardrails` | warn 3-4, stop 5-7 | Prevents runaway tool loops |\\n| `security.allow_private_urls` | true | Required for scanning RFC 1918 targets |\\n\\nThe `disabled_toolsets` list is critical: by stripping terminal, file, and browser access, the LLM can only interact with the target through Argicida's scope-guarded, evidence-captured tool pipeline. There is no escape hatch to `bash`.\\n\\n## Skill Architecture\\n\\n### Three-Tier Progressive Disclosure\\n\\nSkills manage the LLM's limited context window using progressive disclosure, implemented by the `skill_loader.py` hook module in `src/argicida/argicida/hooks/`:\\n\\n```mermaid\\ngraph TD\\n    A[\\\"Tier 1: Always Present~900 tokens/turn\\\"] --&gt; B[\\\"Tool syntax table\\\"]\\n    A --&gt; C[\\\"Service\u2192tool matching table\\\"]\\n    A --&gt; D[\\\"Skills menu\\\"]\\n    E[\\\"Tier 2: First Turn Only~3800 tokens\\\"] --&gt; F[\\\"Full pentest-methodology\\\"]\\n    E --&gt; G[\\\"Decision trees\\\"]\\n    E --&gt; H[\\\"Management tool reference\\\"]\\n    I[\\\"Tier 3: On Demand500-3000 tokens each\\\"] --&gt; J[\\\"dns-enumeration\\\"]\\n    I --&gt; K[\\\"web-enumeration\\\"]\\n    I --&gt; L[\\\"db-enumeration\\\"]\\n    I --&gt; M[\\\"...11 more skills\\\"]\\n```\\n\\n**Tier 1** \u2014 `extract_quick_reference()` in `hooks/skill_loader.py` pulls four sections from `pentest-methodology/SKILL.md` by heading name: the tool syntax reference table, enumeration order, service matching table, and engagement database queries. This ~900 token block plus a skills menu (built by `build_skills_menu()`) are injected into every turn via the `pre_llm_call` hook.\\n\\n**Tier 2** \u2014 The full `pentest-methodology` content (~3800 tokens) is injected only on the orchestrator's first turn. Subagents receive only tier 1 plus a `skill_view('pentest-methodology')` instruction. After turn 1, the full methodology is dropped to free context budget.\\n\\n**Tier 3** \u2014 Technique skills are loaded on demand when the LLM calls `skill_view('skill-name')`. Tool schemas in `src/argicida/argicida/tools/schemas.py` contain `skill_view()` references that prompt the LLM to load the relevant skill before using a tool. A consistency test (`tests/argicida/test_consistency.py`, function `_extract_skill_view_refs`) validates that these references point to real skill names.\\n\\n### Hub and Sub-Skill Pattern\\n\\nTwo skills use a hub pattern with nested sub-skills:\\n\\n- **web-enumeration** \u2014 hub defines the 11-phase scan strategy and nuclei tag reference; 8 sub-skills cover fingerprinting, scanning, crawling, fuzzing, API testing, curl techniques, and technology-specific services.\\n- **db-enumeration** \u2014 hub defines triage order and common mistakes; 5 sub-skills cover PostgreSQL, MySQL, MSSQL, Oracle, and Cassandra with database-specific queries.\\n\\n`build_skills_menu()` uses `is_sub_skill()` to detect nesting \u2014 sub-skills whose parent directory also contains a `SKILL.md` are excluded from the top-level menu. The LLM discovers them through the hub skill's internal table.\\n\\n### Skill Naming Contract\\n\\nSkill names are load-bearing. The `name:` field in each skill's YAML frontmatter is the identifier used by `skill_view()` calls. Hermes resolves skills by frontmatter name matching, not directory name. Renaming the `name:` field without updating all `skill_view()` references in `schemas.py` and `SOUL.md` silently breaks the loading chain. The consistency test in `test_consistency.py` catches this drift.\\n\\n### Skill Frontmatter\\n\\nEvery skill's `SKILL.md` starts with YAML frontmatter that declares:\\n\\n```yaml\\n---\\nname: dns-enumeration          # Identity \u2014 must match skill_view() references\\ndescription: DNS reconnaissance \u2014 zone transfers, record enumeration, ...\\nversion: 2.0.0\\nmetadata:\\n  hermes:\\n    tags: [security, pentesting, dns, reconnaissance]\\n    category: security\\n    requires_toolsets: [argicida]\\n    requires_tools: [argicida_dnsx]\\n  covers_services: [domain]     # Maps nmap service names to this skill\\n---\\n```\\n\\nThe `covers_services` metadata is used by `ServiceSkillRegistry` (in the plugin) to detect coverage gaps \u2014 when nmap identifies a service that no loaded skill covers, the context injection flags it as an uncovered gap.\\n\\n## Skin \u2014 argicida.yaml\\n\\nThe visual theme is built around the Argicida mythology (Hermes slaying the hundred-eyed giant Argus Panoptes):\\n\\n- **Palette:** Aged bronze, verdigris patina, warm shadow. The single cool note is \\\"Argus-eye blue\\\" for session tracking labels.\\n- **Spinner:** An eye closing animation (`\u25c9 \u2192 \u25ce \u2192 \u25cb \u2192 \u00b7 \u2192 \u25cb \u2192 \u25ce`) with thinking verbs drawn from the myth (\\\"lulling the watchman,\\\" \\\"counting the sentries,\\\" \\\"drawing the blade\\\").\\n- **Tool emojis:** Each `argicida_*` tool has a thematic emoji (nmap \u2192 `\ud83d\uddfa`, nuclei \u2192 `\ud83d\udc41`, katana \u2192 `\u2694`).\\n- **Banner:** Block-letter ASCII art with a gradient from divine gold to shadowed verdigris, plus a braille-dot Eye of Argus hero graphic.\\n\\n## Syncing Skills to Runtime\\n\\nSkills are version-controlled in `profile/skills/security/` but execute from `$HERMES_HOME/profiles/pentest/skills/security/` after profile installation. The LLM can self-improve skills at runtime (Hermes's `skills.write_approval: true` requires operator confirmation). To sync runtime improvements back to version control:\\n\\n```bash\\nuv run python scripts/sync_skills.py\\n```\\n\\nThe script diffs each skill file between repo and runtime, shows unified diffs, and prompts for sync direction (repo \u2192 runtime or runtime \u2192 repo).\\n\\n## Key Design Decisions\\n\\n**No direct host access.** `config.yaml` disables every toolset except `argicida`. The LLM cannot run shell commands, read files, or browse the web on the host. All interaction with targets flows through scope-guarded, evidence-captured tool handlers. This is the security boundary.\\n\\n**Orchestrator/subagent split.** SOUL.md defines the primary LLM as an orchestrator that delegates tool execution. Raw tool output (which can be megabytes of nmap XML or nuclei JSON) stays in subagent contexts. The orchestrator queries the engagement database for structured results. This prevents context window exhaustion on long assessments.\\n\\n**Exhaustive enumeration in autonomy rules.** The `` block lists every tool and every operational pattern explicitly. Vague categories (\\\"scan tools\\\") were replaced with exhaustive enumeration after discovering that LLMs interpret category boundaries inconsistently. If a tool isn't listed, it isn't autonomous.\\n\\n**Skills as technique reference, not behavior control.** SOUL.md controls *what* the LLM does (identity, autonomy, delegation). Skills control *how* it does it (tool syntax, enumeration procedures, common mistakes). This separation means behavioral changes (e.g., adding a new gated action) require only a SOUL.md edit, while technique changes (e.g., a new nuclei tag pattern) require only a skill edit.\",\"hermes-profile-skills-scripts\":\"# Hermes Profile &amp; Skills \u2014 scripts\\n\\n# `scripts/sync_skills.py` \u2014 Skill Sync Tool\\n\\nBidirectional sync between version-controlled skills in `profile/skills/` and the installed runtime copy at `$HERMES_HOME/profiles/pentest/skills/`. When the LLM self-improves a skill at runtime (Hermes allows this), those changes live only in the runtime directory until explicitly pulled back into the repo. Conversely, repo edits aren't live until pushed to the runtime profile. This script bridges both directions.\\n\\n```\\nuv run python scripts/sync_skills.py\\n```\\n\\n## How It Works\\n\\nThe script walks `profile/skills/` recursively, finds every `SKILL.md`, pairs it with its runtime counterpart by frontmatter `name:` field, and shows a unified diff for any pair that diverges. The operator chooses per-skill: push repo to runtime, pull runtime to repo, or skip.\\n\\n```mermaid\\nflowchart LR\\n    A[\\\"profile/skills/(version-controlled)\\\"] -- \\\"r: repo\u2192runtime\\\" --&gt; B[\\\"$HERMES_HOME/profiles/pentest/skills/(live runtime)\\\"]\\n    B -- \\\"R: runtime\u2192repo\\\" --&gt; A\\n```\\n\\n## Skill Identity Resolution\\n\\nSkills are matched by their frontmatter `name:` field, not by directory name. `parse_skill_name` extracts this from the YAML frontmatter block at the top of each `SKILL.md`:\\n\\n```yaml\\n---\\nname: web-enumeration\\ndescription: ...\\n---\\n```\\n\\nIf the frontmatter is missing or the `name:` field is empty, the parent directory name is used as a fallback. This matters because hub skills contain sub-skills in nested directories \u2014 `web-enumeration/web-curl/SKILL.md` has `name: web-curl`, not `name: web-enumeration`.\\n\\n`find_runtime_skill` searches the runtime tree in two passes:\\n1. Match against `parse_skill_name` output (frontmatter `name:`)\\n2. Fall back to directory-name matching (backward compat for skills predating frontmatter conventions)\\n\\n## Key Functions\\n\\n| Function | Purpose |\\n|---|---|\\n| `parse_skill_name(skill_md)` | Extract `name:` from SKILL.md frontmatter; fall back to directory name |\\n| `collect_repo_skills(skills_dir)` | Recursively discover all `SKILL.md` files under `profile/skills/` |\\n| `find_runtime_skill(name, runtime_skills_dir)` | Locate the runtime counterpart of a repo skill by name |\\n| `diff_skill(repo_dir, runtime_dir)` | Unified diff of two SKILL.md files; returns `None` if identical |\\n| `sync_skill(repo_dir, runtime_dir, direction=)` | Copy all files in a skill directory in the specified direction |\\n| `_sync_dir(src, dst)` | Low-level directory copy (all files, `shutil.copy2` for metadata preservation) |\\n\\n## Sync Directions\\n\\nWhen a skill differs, the interactive prompt offers three choices:\\n\\n- **`r`** \u2014 Repo \u2192 Runtime. You edited skills in the repo and want them live.\\n- **`R`** \u2014 Runtime \u2192 Repo. The LLM improved a skill at runtime and you want to commit the improvement.\\n- **`s`** \u2014 Skip. Leave the divergence for now.\\n\\n`sync_skill` copies the entire skill directory (not just `SKILL.md`) so auxiliary files like sub-skill directories or reference data travel with the sync. It raises `ValueError` on invalid direction strings.\\n\\n## Prerequisites\\n\\n- `HERMES_HOME` environment variable must be set\\n- The `pentest` profile must be installed (`hermes profile install ./profile --alias`)\\n- The runtime skills directory must exist at `$HERMES_HOME/profiles/pentest/skills/`\\n\\nIf any prerequisite is missing, the script prints a diagnostic and exits without error.\\n\\n## Undeployed Skills\\n\\nSkills found in the repo but absent from the runtime tree are reported as \\\"not deployed\\\" with a hint to run `hermes profile update pentest`. The script does not auto-deploy new skills \u2014 that's the profile installer's job.\\n\\n## Test Coverage\\n\\nAll public functions have unit tests in `tests/scripts/test_sync_skills.py` covering: frontmatter extraction, fallback to directory names, nested/category directory discovery, runtime matching by frontmatter vs. directory name, diff generation, bidirectional copy, auxiliary file handling, and invalid-direction rejection. The interactive `main()` loop is not unit-tested (it requires stdin).\",\"hermes-profile-skills-skills\":\"# Hermes Profile &amp; Skills \u2014 skills\\n\\n# Hermes Profile Skills \u2014 Security\\n\\n## Purpose\\n\\nThe `profile/skills/security/` directory contains LLM-facing technique documentation that ships with the Argicida Hermes profile. These are not code \u2014 they are structured reference material the LLM loads at runtime to guide tool usage during penetration testing sessions. Skills teach the LLM _how_ to use Argicida's scan tools correctly: argument syntax, enumeration order, result interpretation, and common mistakes to avoid.\\n\\nSkills deploy to `$HERMES_HOME/skills/security/` when the profile is installed (`hermes profile install ./profile --alias`). Hermes discovers them by scanning for `SKILL.md` files with YAML frontmatter.\\n\\n## Progressive Disclosure Architecture\\n\\nThe LLM has a finite context window. Loading all 13 skills (~25,000 tokens) on every turn would crowd out scan results and engagement state. Instead, skills use a three-tier system that trades context budget for relevance:\\n\\n```mermaid\\ngraph TD\\n    T1[\\\"Tier 1: Always Present~900 tokens/turn\\\"]\\n    T2[\\\"Tier 2: Orchestrator First Turn~3800 tokens, dropped after turn 1\\\"]\\n    T3[\\\"Tier 3: On Demand500-3000 tokens each, loaded via skill_view()\\\"]\\n\\n    T1 --&gt;|\\\"compact quick-referencetool syntax table + service matching\\\"| LLM[\\\"LLM Context\\\"]\\n    T2 --&gt;|\\\"full pentest-methodologydecision tree + interpretation guides\\\"| LLM\\n    T3 --&gt;|\\\"technique skillsloaded when a service type is encountered\\\"| LLM\\n\\n    nmap_result[\\\"nmap finds port 53\\\"] --&gt;|\\\"triggers\\\"| SV[\\\"skill_view('dns-enumeration')\\\"]\\n    SV --&gt; T3\\n```\\n\\n**Tier 1** \u2014 A compressed tool syntax table and service-to-tool matching table extracted from `pentest-methodology`. Present on every turn for every role (orchestrator and subagents). Costs ~900 tokens.\\n\\n**Tier 2** \u2014 The full `pentest-methodology` content including the \\\"What to Do with Results\\\" decision tree, interpretation guides, and common mistakes. Injected on the orchestrator's first turn only, then dropped to free context. Subagents receive only Tier 1 plus a `skill_view('pentest-methodology')` instruction.\\n\\n**Tier 3** \u2014 All other skills. Loaded on demand when the LLM encounters a specific service type. The bridge between Tier 2 disappearing and Tier 3 requiring self-initiated loading is the `skill_view()` prompt embedded in each tool's schema description. When the LLM reads a tool schema before calling it, the schema tells it which skill to load.\\n\\n## Skill Organization\\n\\n### Standalone Skills\\n\\nMost skills are single `SKILL.md` files, one per service type:\\n\\n```\\nprofile/skills/security/\\n\u251c\u2500\u2500 DESCRIPTION.md\\n\u251c\u2500\u2500 pentest-methodology/SKILL.md      # core methodology (Tier 1+2)\\n\u251c\u2500\u2500 network-discovery/SKILL.md        # naabu \u2192 nmap \u2192 subfinder\\n\u251c\u2500\u2500 dns-enumeration/SKILL.md          # dnsx techniques\\n\u251c\u2500\u2500 smb-enumeration/SKILL.md          # smbclient\\n\u251c\u2500\u2500 redis-enumeration/SKILL.md        # redis_query\\n\u251c\u2500\u2500 mongo-enumeration/SKILL.md        # mongo_query\\n\u251c\u2500\u2500 rpc-nfs-enumeration/SKILL.md      # rpcinfo + NFS\\n\u251c\u2500\u2500 ssh-enumeration/SKILL.md          # ssh_exec\\n\u251c\u2500\u2500 ftp-enumeration/SKILL.md          # curl ftp://\\n\u251c\u2500\u2500 smtp-enumeration/SKILL.md         # curl smtp://\\n\u2514\u2500\u2500 telnet-enumeration/SKILL.md       # curl telnet://\\n```\\n\\n### Hub Skills with Sub-Skills\\n\\nTwo skills use a hub-and-spoke pattern for services with deep technique trees:\\n\\n```\\nweb-enumeration/\\n\u251c\u2500\u2500 SKILL.md                          # hub: scan strategy, phase overview, common mistakes\\n\u251c\u2500\u2500 fingerprinting/SKILL.md           # web-fingerprinting (httpx -td)\\n\u251c\u2500\u2500 scanning/SKILL.md                 # web-scanning (nuclei, ZAP)\\n\u251c\u2500\u2500 crawling/SKILL.md                 # web-crawling (katana)\\n\u251c\u2500\u2500 fuzzing/SKILL.md                  # web-fuzzing (ffuf)\\n\u251c\u2500\u2500 api/SKILL.md                      # web-api (OpenAPI, GraphQL, source maps)\\n\u251c\u2500\u2500 api-testing/SKILL.md              # web-api-testing (IDOR, SSRF, CORS)\\n\u251c\u2500\u2500 curl-techniques/SKILL.md          # web-curl (auth, file download, headers)\\n\u2514\u2500\u2500 services/SKILL.md                 # web-services (WordPress, Jenkins, Tomcat)\\n\\ndb-enumeration/\\n\u251c\u2500\u2500 SKILL.md                          # hub: triage order, tool contract, URL schemes\\n\u251c\u2500\u2500 postgresql/SKILL.md               # db-postgresql (pg://)\\n\u251c\u2500\u2500 mysql/SKILL.md                    # db-mysql (my://)\\n\u251c\u2500\u2500 mssql/SKILL.md                    # db-mssql (ms://)\\n\u251c\u2500\u2500 oracle/SKILL.md                   # db-oracle (or://)\\n\u2514\u2500\u2500 cassandra/SKILL.md                # db-cassandra (ca://)\\n```\\n\\nThe hub skill provides strategy and common mistakes. Sub-skills provide technique-specific reference. The LLM loads the hub first, then loads sub-skills as needed for specific technologies or attack phases.\\n\\n## Skill Frontmatter\\n\\nEvery `SKILL.md` begins with YAML frontmatter that Hermes uses for discovery and metadata:\\n\\n```yaml\\n---\\nname: dns-enumeration                     # unique identifier \u2014 load-bearing\\ndescription: DNS reconnaissance \u2014 ...     # shown in skill menu\\nversion: 2.0.0\\nmetadata:\\n  hermes:\\n    tags: [security, pentesting, dns]\\n    category: security\\n    requires_toolsets: [argicida]          # skill only appears when toolset is active\\n    requires_tools: [argicida_dnsx]       # tools this skill documents\\n  covers_services: [domain]               # nmap service names this skill applies to\\n---\\n```\\n\\n### The `name` Field Is Load-Bearing\\n\\nThe `name` field in frontmatter is how `skill_view('name')` resolves the skill. Tool schemas in `src/argicida/argicida/tools/schemas.py` reference skills by this exact name. If you rename a skill's `name` field without updating the corresponding tool schema, the loading chain breaks silently \u2014 the LLM calls `skill_view('old-name')`, Hermes finds nothing, and the LLM proceeds without technique guidance.\\n\\nSub-skills with this constraint are marked with a comment in their frontmatter:\\n\\n```yaml\\n---\\n# name is referenced by schemas.py \u2014 do not rename without updating tool schemas\\nname: web-crawling\\n```\\n\\n## Skill Content Patterns\\n\\n### Enumeration Order\\n\\nService skills prescribe a specific enumeration sequence. The LLM follows this order unless findings warrant deviation:\\n\\n```\\n1. Register context (e.g., scope_define for DNS server)\\n2. Broad discovery (zone transfer, full record enum)\\n3. Targeted follow-up (reverse DNS, subdomain brute)\\n4. Scope expansion (add_host, scope_define for new IPs)\\n```\\n\\n### \\\"When to Stop\\\" Sections\\n\\nEvery skill ends with explicit completion criteria. Without these, the LLM tends to re-run tools hoping for different results. The criteria are stated as positive assertions (\\\"enumeration is complete when you have...\\\") followed by explicit anti-patterns (\\\"do NOT re-run...\\\").\\n\\n### Common Mistakes\\n\\nSkills document tool-specific pitfalls the LLM is likely to hit. These are informed by real session failures \u2014 each mistake entry exists because the LLM actually made that error during testing. Examples: forgetting `-Pn` with nmap, using IP addresses instead of domain names for AXFR, duplicating auto-injected flags.\\n\\n### Tool Argument Guidance\\n\\nThe core `pentest-methodology` skill documents the auto-injection behavior for every tool \u2014 whether `target` is automatically passed to the command, or whether the LLM must include the URL/address in `args`. This is the single most common source of tool call failures, so it appears in both the methodology skill (Tier 1 quick-reference table) and in each technique skill's examples.\\n\\n## How Skills Connect to Tool Schemas\\n\\nTool schemas in `src/argicida/argicida/tools/schemas.py` contain `skill_view()` instructions in their `description` fields. This creates a pull-based loading model:\\n\\n1. Hermes presents the tool schema to the LLM when it considers calling a tool\\n2. The schema description says \\\"Load `skill_view('dns-enumeration')` for techniques\\\"\\n3. The LLM calls `skill_view('dns-enumeration')` before (or alongside) the tool call\\n4. The skill content loads into context for that turn\\n\\nThis is the only mechanism that connects Tier 3 skills to the LLM's decision-making. If a tool schema lacks a `skill_view()` reference, the LLM will use the tool without loading technique guidance.\\n\\n## Adding or Modifying Skills\\n\\n### Adding a New Standalone Skill\\n\\n1. Create `profile/skills/security/-enumeration/SKILL.md` with frontmatter\\n2. Set `name`, `covers_services`, `requires_tools` in frontmatter\\n3. Add a `skill_view('')` reference in the relevant tool schema description (`src/argicida/argicida/tools/schemas.py`)\\n4. Add a routing entry in `pentest-methodology`'s \\\"What to Do with Results\\\" section\\n5. Add the service \u2192 skill mapping in `network-discovery`'s interpretation table\\n6. Run `uv run python scripts/sync_skills.py` to sync back to the profile directory\\n\\n### Adding a Sub-Skill to a Hub\\n\\n1. Create a subdirectory under the hub (e.g., `web-enumeration/new-technique/SKILL.md`)\\n2. Set the `name` field \u2014 this is the `skill_view()` identifier\\n3. Add a row to the hub skill's sub-skill table\\n4. If any tool schema should reference it, add a `skill_view('')` instruction\\n5. Sync with `scripts/sync_skills.py`\\n\\n### Sync Workflow\\n\\nSkills live in two places: the source of truth in `profile/skills/security/` and the deployed copy at `$HERMES_HOME/skills/security/`. The LLM can self-improve deployed skills at runtime (Hermes supports this natively). To capture runtime improvements back into the repo:\\n\\n```bash\\nuv run python scripts/sync_skills.py\\n```\\n\\nThis copies from the deployed location back to `profile/skills/security/`, preserving any changes the LLM made during sessions.\\n\\n## Key Skill: `pentest-methodology`\\n\\nThis is the foundational skill that all others depend on. It defines:\\n\\n- **Available tools** \u2014 what the LLM can and cannot use (no terminal, no file browser)\\n- **Container environment** \u2014 read-only rootfs, writable `/tmp/` and `/artifacts/`\\n- **Engagement lifecycle** \u2014 create engagement before scanning, resume by name\\n- **Argument contract** \u2014 `target` vs `args`, auto-injection rules, flag separation\\n- **Tool syntax table** \u2014 quick-reference for every scan tool's argument pattern\\n- **Enumeration order** \u2014 naabu \u2192 nmap \u2192 service-specific tools \u2192 scope expansion\\n- **Service-to-tool matching** \u2014 which tools apply to which service types\\n- **Result interpretation** \u2014 decision tree for acting on findings\\n- **Credential workflow** \u2014 fragment extraction \u2192 candidate ranking \u2192 testing \u2192 validation\\n- **Error handling** \u2014 scope rejections, empty results, timeouts\\n\\nThe methodology skill is unique in that parts of it are always present (Tier 1 quick-reference) while the full content is Tier 2 (first turn only). No other skill spans two tiers.\\n\\n## Design Constraints\\n\\n**No code execution.** Skills are pure documentation \u2014 they contain no executable logic, no templates, no scripts. All behavior is in the Argicida Python codebase. Skills only influence the LLM's tool call decisions.\\n\\n**Token budget awareness.** Every skill must justify its token cost. Verbose explanations waste context that the LLM needs for scan results and engagement state. Skills are written in compressed reference style, not tutorial style.\\n\\n**Consistency tests.** The codebase includes consistency tests that verify structural invariants between skills and tool schemas. These catch drift where a tool schema references a skill name that no longer exists, or a skill documents a tool that has been removed.\",\"hermes-profile-skills\":\"# Hermes Profile &amp; Skills\\n\\n# Hermes Profile &amp; Skills\\n\\nThe `profile/` directory is a self-contained Hermes profile distribution that transforms Hermes into an offensive security operator. It packages three concerns \u2014 identity, technique knowledge, and synchronization tooling \u2014 into an installable unit that shapes LLM behavior at runtime without touching Argicida's Python source.\\n\\nNothing here is imported by the code in `src/`. Hermes reads these files after installation, and the LLM consults them during pentest sessions to decide how to act.\\n\\n## Sub-modules\\n\\n| Sub-module | Path | Role |\\n|---|---|---|\\n| [Profile](profile.md) | `profile/` | Identity, model config, theme \u2014 the static foundation |\\n| [Skills](skills.md) | `profile/skills/security/` | 13 technique skills the LLM loads on demand |\\n| [Scripts](scripts.md) | `scripts/sync_skills.py` | Bidirectional sync between repo and runtime copies |\\n\\n## How They Fit Together\\n\\n```mermaid\\nflowchart LR\\n    A[profile/] --&gt;|hermes profile install| B[$HERMES_HOME/profiles/pentest/]\\n    B --&gt; C[SOUL.md + config.yaml]\\n    B --&gt; D[skills/security/*.md]\\n    D --&gt;|LLM loads at runtime| E[Pentest Session]\\n    C --&gt;|Shapes identity &amp; params| E\\n    E --&gt;|LLM self-improves skills| D\\n    D --&gt;|drift detected| F[scripts/sync_skills.py]\\n    F --&gt;|push or pull| A\\n```\\n\\nThe [profile](profile.md) defines _who_ the LLM is: its SOUL identity, model endpoint, turn budget, and visual theme. The [skills](skills.md) define _what it knows_: structured technique reference for each service category (web, DNS, SMB, databases, etc.), delivered through a progressive-disclosure architecture that manages context window pressure. Together, they are installed as a unit via `hermes profile install ./profile --alias`, which creates the `pentest` command alias.\\n\\nAt runtime, Hermes can allow the LLM to self-improve skills \u2014 rewriting technique docs based on session experience. Those changes live only in the installed runtime directory. The [sync script](scripts.md) bridges this gap, diffing repo skills against their runtime counterparts by frontmatter `name:` and letting the operator push or pull per-skill.\\n\\n## Key Workflows\\n\\n**Initial setup** \u2014 `hermes profile install ./profile --alias` copies everything to `$HERMES_HOME/profiles/pentest/`, making `pentest chat` available as a CLI command.\\n\\n**During a session** \u2014 The LLM receives the SOUL identity and a compact quick-reference (~900 tokens) every turn. Full methodology loads on the first orchestrator turn only. Technique skills load on demand when the LLM encounters a specific service, triggered by `skill_view()` references in tool schemas.\\n\\n**After sessions** \u2014 `uv run python scripts/sync_skills.py` detects any skills that diverged between repo and runtime, shows unified diffs, and offers per-skill push/pull resolution.\",\"hook-pipeline\":\"# Hook Pipeline\\n\\n# Hook Pipeline\\n\\n`src/argicida/argicida/hooks/`\\n\\nThe hook pipeline is Argicida's central interception layer. Every tool call the LLM makes passes through it, and every LLM turn receives context shaped by it. The pipeline enforces scope, captures evidence, enriches results, populates the asset registry, and injects engagement state into the LLM's context window \u2014 all without the LLM knowing any of this machinery exists.\\n\\nHooks are registered into Hermes via `ctx.register_hook()` during plugin startup (see `registration/hooks.py`). Hermes fires them at defined points in its agent loop. Argicida never calls hooks directly.\\n\\n## Tool Call Lifecycle\\n\\nA single tool invocation passes through up to six hooks in a fixed order determined by Hermes:\\n\\n```mermaid\\nflowchart TD\\n    LLM[\\\"LLM decides to call a tool\\\"]\\n    SG[\\\"pre_tool_call: Scope Guard\\\"]\\n    SG --&gt;|blocked| BLOCK[\\\"Tool call rejectedreason returned to LLM\\\"]\\n    SG --&gt;|allowed| EXEC[\\\"Tool executes in container\\\"]\\n    EXEC --&gt; NORM[\\\"transform_tool_result: Normalizer\\\"]\\n    NORM --&gt; POST1[\\\"post_tool_call: Evidence Capture\\\"]\\n    NORM --&gt; POST2[\\\"post_tool_call: Output Store\\\"]\\n    NORM --&gt; POST3[\\\"post_tool_call: Asset Extractor\\\"]\\n    NORM --&gt; POST4[\\\"post_tool_call: Scope Expansion\\\"]\\n    POST1 &amp; POST2 &amp; POST3 &amp; POST4 --&gt; NEXT[\\\"pre_llm_call: Context Injection\\\"]\\n    NEXT --&gt; LLM2[\\\"LLM sees result + updated context\\\"]\\n\\n    LLM --&gt; SG\\n```\\n\\nThe `pre_llm_call` hook fires before every LLM turn, not just after tool calls. Session lifecycle hooks (`on_session_start`, `on_session_end`, `on_session_finalize`) fire at session boundaries.\\n\\n## The Adapter Pattern\\n\\nHermes hooks have a fixed signature (`def callback(**kwargs) -&gt; ...`). Argicida's domain logic needs access to engagement state, the evidence chain, the store, and other plugin internals. The adapter pattern bridges this gap.\\n\\nEach `make_*_adapter()` function in `adapters.py` is a factory that closes over the required dependencies and returns a plain callback matching the Hermes signature:\\n\\n```python\\ndef make_scope_guard_adapter(\\n    get_scope: Callable[[], ScopeDefinition | None],\\n    *,\\n    store: EngagementStore | None = None,\\n    get_eid: Callable[[], str] | None = None,\\n) -&gt; Callable[..., dict[str, Any] | None]:\\n    def adapter(tool_name: str = \\\"\\\", args: dict = None, **kwargs) -&gt; ...:\\n        # closes over get_scope, store, get_eid\\n        ...\\n    return adapter\\n```\\n\\nDependencies are injected as callables (`get_scope`, `get_eid`, `get_session_id`) rather than values because the underlying state changes during a session \u2014 an engagement might not exist when the hook is registered but will exist by the time it fires.\\n\\nAll adapters follow fail-safe conventions:\\n- **Scope guard** fails closed \u2014 any exception blocks the tool call\\n- **Everything else** fails open \u2014 exceptions are logged but don't interrupt the session\\n\\n## Hook Points\\n\\n### `pre_tool_call` \u2014 Scope Guard\\n\\n**Module:** `scope_guard.py`  \\n**Adapter:** `make_scope_guard_adapter`  \\n**Returns:** `{\\\"action\\\": \\\"block\\\", \\\"message\\\": \\\"...\\\"}` or `None` (allow)\\n\\nTwo-layer scope enforcement. This is the command layer (layer 2); the network layer (nftables on the gateway) is layer 1.\\n\\nThe guard performs these checks in order:\\n\\n1. **Non-Argicida tools** \u2014 allowed unconditionally (Hermes built-ins like `delegate_task`)\\n2. **Exempt tools** \u2014 management tools (`engagement_create`, `scope_define`, etc.) skip scope checks. The exempt set comes from `get_scope_exempt_tools()` in the tool definition registry.\\n3. **No active engagement** \u2014 blocked with a message telling the LLM to create one\\n4. **Target validation** \u2014 the `target` field is checked against scope CIDRs and domains via `_is_target_in_scope()`. Multi-target calls (comma-separated) are rejected. Hostname\u2192IP fallback uses the asset registry when `store` and `engagement_id` are provided.\\n5. **Args validation** \u2014 the full `args` array is checked via `validate_tool_args()` using the tool's scope profile (blocked flags, target flags, value flags, positional handling). Each tool definition declares its own profile.\\n\\nThe `_scope_context()` helper formats the in-scope targets into error messages so the LLM can self-correct. Excluded IPs are deliberately omitted \u2014 operational data the LLM shouldn't see.\\n\\nTool-name-to-binary mapping (`_TOOL_NAME_TO_BINARY`) is populated at startup via `register_tool_aliases()` from discovered tool definitions. This handles cases like `db_query` mapping to `usql`.\\n\\n### `transform_tool_result` \u2014 Normalizer\\n\\n**Module:** `normalizer.py`  \\n**Adapter:** `make_normalizer_adapter`  \\n**Returns:** modified result string, or `None` (unchanged)\\n\\nThe normalizer is the most complex hook. It transforms raw tool output into enriched, triaged results the LLM can act on. The pipeline:\\n\\n1. **Error hint injection** (`enrich_tool_error`) \u2014 matches stderr/error text against known patterns from tool definitions and injects actionable `error_hint` fields. Pattern\u2192hint pairs are registered at startup via `register_error_hints()`.\\n\\n2. **Finding extraction** (`parse_findings_from_result` via `parsers.py`) \u2014 dispatches to per-tool parsers registered from tool definitions. Each parser receives a `ParseContext` and returns a `ParseResult` containing findings, credentials, principals, and secrets.\\n\\n3. **Tool outcome classification** (`build_tool_outcome`) \u2014 produces a canonical `tool_outcome` envelope with:\\n   - `status`: `\\\"success\\\"`, `\\\"error\\\"`, or `\\\"no_results\\\"`\\n   - `connectivity`: `\\\"refused\\\"`, `\\\"timeout\\\"`, `\\\"unreachable\\\"`, `\\\"dns_failure\\\"` (when applicable)\\n   - `auth_result`: `\\\"SUCCESS\\\"` or `\\\"FAILED\\\"` (when auth was attempted)\\n   - `result_count` and `empty_output` for quick triage\\n\\n4. **Result transformation** (`transform_result` via `result_transformers.py`) \u2014 per-tool transformers reshape output for LLM consumption (e.g., summarizing verbose nmap XML). Falls back to truncating `raw_output` via `truncate_field()`.\\n\\n5. **LLM fallback extraction** (`extract_findings_llm` via `perceptor.py`) \u2014 when no deterministic parser produced findings and an LLM is available, sends the raw output through `llm.complete_structured()` with a JSON schema. Skipped for tools in `_PERCEPTOR_SKIP_TOOLS`. This is a last-resort path.\\n\\n6. **Triage pipeline** \u2014 extracted findings go through deduplication (`deduplicate`), EPSS/KEV scoring (`score_findings`), and triage classification (`classify_findings`). The enriched findings are serialized back into the result JSON.\\n\\nThe adapter also supports `post_normalize` callbacks \u2014 a tuple of callables that run after normalization. This is how the scope expansion hook receives the already-normalized result.\\n\\n### `post_tool_call` \u2014 Evidence, Output, Assets, Scope Expansion\\n\\nFour independent hooks fire after every tool call. They all receive the (possibly normalized) result.\\n\\n#### Evidence Capture\\n\\n**Module:** `evidence.py`  \\n**Adapter:** `make_evidence_adapter`\\n\\nExtracts findings from results and appends them to the evidence chain and engagement store. The extraction follows a priority order:\\n\\n1. **Pre-enriched findings** \u2014 if the normalizer already injected a `findings` array into the result JSON, use those directly (they're already triaged)\\n2. **Parser extraction** \u2014 fall back to `parse_findings_from_result()` and run the triage pipeline\\n3. **Sentinel findings** \u2014 if no findings were produced at all, create a minimal `\\\" completed\\\"` sentinel so the coverage gap detector knows the tool ran\\n\\nCredentials, principals, and secrets follow the same pre-enriched \u2192 parser fallback pattern. Each type has a `_*_from_dict` factory and a generic `_extract_enriched()` helper that pulls typed lists from the result JSON.\\n\\nFindings are stored via `store_finding()` (which handles DB insert + chain append). Credentials go to `store.credentials.add_validated_credential()`. All storage calls are individually try/excepted \u2014 a single credential storage failure doesn't lose the findings.\\n\\n#### Lossless Output Store\\n\\n**Module:** `output_store.py`  \\n**Adapter:** `make_output_store_adapter`\\n\\nEvery Argicida tool invocation gets its raw output persisted to disk at `$HERMES_HOME/argicida/output///`. Three files:\\n\\n| File | Content |\\n|------|---------|\\n| `stdout` | `parsed[\\\"raw_output\\\"]` |\\n| `stderr` | `parsed[\\\"stderr\\\"]` |\\n| `meta.json` | Tool name, target, exit code, request ID, session ID, timestamp, tool args |\\n\\nAdditional artifact files (`sarif_report`, `plan_yaml`) are written when present. Unscoped invocations (no engagement) go under `unscoped/`. All files use `O_NOFOLLOW` + `0o600` permissions.\\n\\n#### Asset Extractor\\n\\n**Module:** `asset_extractor.py`  \\n**Adapter:** `make_asset_extraction_adapter`\\n\\nPopulates the asset registry from tool results. Built with an O(1) tool definition lookup at construction time.\\n\\nFor each tool result:\\n1. The tool's `extractor` function (from the tool definition) produces `AssetDiscovery` tuples: `(ip, hostname, port, protocol, service, source)`\\n2. Each discovery upserts an asset, adds hostnames (syncing to dnsmasq), and adds ports\\n3. Coverage state advances (`discovered` \u2192 `ports_scanned`) for port-scanning tools\\n4. For httpx: fingerprints are extracted per hostname, and vhost groups are computed when all hostnames on an asset are fingerprinted\\n5. For web tools (httpx, nuclei, katana, ffuf): vhost scans are recorded and enumeration completeness is checked\\n6. `link_findings_by_target()` connects findings to assets\\n\\nZAP tools get special handling \u2014 their findings contain URLs, so assets are extracted by parsing the `target` URL field.\\n\\n#### Scope Expansion\\n\\n**Adapter:** `make_scope_expansion_adapter` (in `adapters.py`)\\n\\nWhen a tool result contains a `scope_proposal` field with `discovered_hosts`, the scope is automatically expanded:\\n1. New `/32` CIDRs are added for discovered IPs\\n2. Discovered domains are added\\n3. Hostname\u2192IP mappings are synced to dnsmasq via `add_host_entry`\\n4. The expanded scope is validated, persisted to the DB, and propagated to the gateway's nftables rules\\n\\n### `pre_llm_call` \u2014 Context Injection\\n\\n**Module:** `context.py`, `context_sections.py`, `autonomy.py`, `coverage.py`  \\n**Adapter:** `make_context_adapter`  \\n**Returns:** `{\\\"context\\\": \\\"...\\\"}` or `None`\\n\\nAssembles a context string injected into every LLM turn. The content differs dramatically between orchestrator and subagent roles, branching on `platform == \\\"subagent\\\"`.\\n\\n#### Orchestrator Context\\n\\nThe orchestrator delegates tool execution \u2014 it doesn't need tool syntax. Its context includes:\\n\\n- **State block** \u2014 engagement name, phase, scope, finding counts by triage level\\n- **Resume briefing** (first turn only, when findings exist) \u2014 top critical/interesting findings with a directive not to re-scan\\n- **Asset summary** \u2014 per-IP coverage state, ports, hostnames, vhost groups, tech fingerprints, login URLs\\n- **ZAP status** \u2014 passive finding summary when ZAP sidecar is active\\n- **Coverage map** \u2014 per-target list of which tools have produced results, grouped by asset\\n- **Credential summary** \u2014 validated credentials (redacted), known principals, known secrets, untested candidate count\\n- **Management quick-reference** \u2014 `engagement_query` parameter syntax\\n- **Autonomy directive** (positioned last for recency bias) \u2014 phase-aware instructions telling the LLM exactly what to delegate next\\n\\n#### Subagent Context\\n\\nSubagents execute tools directly. Their context includes:\\n\\n- State block and asset summary (same as orchestrator)\\n- **Quick-reference** (~900 tokens) \u2014 tool syntax table and service-to-tool matching table, extracted from pentest-methodology\\n- **Skills menu** \u2014 available technique skills loadable via `skill_view()`\\n- **Skill pointers** \u2014 service-specific skill suggestions based on detected services\\n- Coverage map and credential summary\\n\\n#### Autonomy Directive\\n\\n`build_autonomy_directive()` in `autonomy.py` is the orchestrator's action engine. It:\\n\\n1. Auto-advances the engagement phase when conditions are met (`_maybe_advance_phase`)\\n2. Clears stale in-flight tracking entries that now have findings (`_clear_completed_in_flight`)\\n3. Builds a prioritized gap list from three sources:\\n   - Unscanned assets (no findings at all)\\n   - Enumeration gaps (`build_coverage_gaps` \u2014 services without matching tool runs)\\n   - Deep-testing gaps (ZAP active scans, credential testing, sensitive files, high-EPSS exploitation)\\n4. Emits a structured directive: \\\"Delegate exactly N task(s)\\\" with specific goals\\n\\nThe directive always starts with \\\"ACTION REQUIRED: Do NOT respond with only text\\\" \u2014 a strong steering signal that prevents the LLM from narrating instead of acting.\\n\\n### Session Lifecycle Hooks\\n\\n#### `on_session_start`\\n\\n**Module:** `session_start.py`  \\n**Adapter:** `make_session_start_adapter`\\n\\nThree operational modes based on which dependencies are provided:\\n\\n1. **Session-ID-only** (always) \u2014 stores `session_id` on `PluginState`, checks EPSS/KEV data freshness\\n2. **Enrichment** (with `epss` + `kev`) \u2014 auto-downloads stale EPSS/KEV data and hot-reloads it\\n3. **Auto-resume** (with `store` + `chain` + `hermes_home`) \u2014 binds the last active engagement via `bind_engagement()`, checking `ARGICIDA_ENGAGEMENT` env var first, then falling back to the most recent `last_bound_at` in the database\\n\\n#### `on_session_end`\\n\\n**Adapter:** `make_session_end_adapter`\\n\\nFires every turn (not just at session close \u2014 Hermes naming quirk). Signs the evidence chain with Ed25519 and persists ZAP passive findings if ZAP is active.\\n\\n#### `on_session_finalize`\\n\\n**Adapter:** `make_session_finalize_adapter`\\n\\nDestructive teardown. In order:\\n1. Final evidence chain signature persisted to DB\\n2. Engagement unlocked and session cleared\\n3. Delegation tracking cleared\\n4. ZAP findings persisted, ZAP manager cleared\\n5. Pod session torn down (pod, gateway, networks, temp dirs)\\n6. On shutdown: engagement store closed\\n\\n## Supporting Subsystems\\n\\n### Coverage Gap Detection (`coverage.py`)\\n\\n`build_coverage_gaps()` compares discovered services against the `ServiceSkillRegistry` to find targets with uncovered services. It tracks:\\n\\n- Per-target tool sets and service\u2192port mappings\\n- In-flight delegations (to avoid re-delegating work already dispatched)\\n- Unidentified ports needing `nmap -sV`\\n- Unknown services needing httpx probing\\n- Vhost groups needing web enumeration\\n\\n`build_deep_testing_gaps()` aggregates four specialized gap detectors:\\n- ZAP passive findings without active scans\\n- Services with untested credential candidates\\n- Discovered sensitive files not yet retrieved\\n- High-EPSS critical CVEs without exploitation follow-up\\n\\n### Service Skill Registry (`service_registry.py`)\\n\\nBuilt at startup from SKILL.md frontmatter. Maps nmap service names (e.g., `\\\"ssh\\\"`, `\\\"http\\\"`) to `SkillInfo` objects containing the skill name, required toolsets, and required tools.\\n\\n`skill_exercised()` determines whether a skill's coverage requirement is met by comparing `required_tools` against the set of tools that have run, normalizing both sides to bare names (stripping `argicida_` prefix).\\n\\n### Parser and Transformer Dispatch (`parsers.py`, `result_transformers.py`)\\n\\nBoth use a registry pattern: tool definitions declare parser/transformer functions, which are bulk-registered at startup via `register_parsers()` / `register_transformers()`. At runtime, dispatch is an O(1) dict lookup by tool name.\\n\\n`ParseResult` is a `NamedTuple` carrying `(findings, credentials, principals, secrets)` \u2014 the four types of structured data that can be extracted from a tool result.\\n\\n## Adding a New Hook\\n\\nHooks are wired in `registration/hooks.py`. To add a new hook:\\n\\n1. Write the domain logic in a new module under `hooks/`\\n2. Create a `make_*_adapter()` factory in `adapters.py` that closes over dependencies\\n3. Register it in `registration/hooks.py` via `ctx.register_hook(\\\"hook_point\\\", adapter)`\\n\\nHermes hook points: `pre_tool_call`, `transform_tool_result`, `post_tool_call`, `pre_llm_call`, `on_session_start`, `on_session_end`, `on_session_finalize`, `subagent_start`, `subagent_stop`.\\n\\nAll hooks are synchronous \u2014 Hermes calls them with plain `cb(**kwargs)`. If you need async work inside a hook, use `asyncio.to_thread()`.\",\"ipc-layer\":\"# IPC Layer\\n\\n# IPC Layer\\n\\nThe IPC layer connects the host-side Hermes plugin to the thin agent running inside the Podman tool container. It uses a Unix domain socket with a custom binary protocol built on msgspec/msgpack, authenticated with HMAC-SHA256.\\n\\n## Why Unix Sockets\\n\\nThe tool container runs in a separate user namespace under rootless Podman. HTTP APIs would require port mapping and TLS. Unix sockets avoid both \u2014 the socket file is bind-mounted into the container via the `ipc_dir`, and the `U`/`chown` mount option handles cross-namespace UID mapping. The 108-byte path limit on Unix sockets is why `ipc_dir` uses a short `tempfile.TemporaryDirectory(prefix=\\\"argicida-ipc-\\\")` path rather than embedding engagement IDs.\\n\\n## Architecture\\n\\n```mermaid\\nsequenceDiagram\\n    participant H as Host (IpcClient)\\n    participant S as Container (ipc/server.py)\\n\\n    H-&gt;&gt;S: TCP-style connect via Unix socket\\n    S-&gt;&gt;H: IpcAuthChallenge (nonce)\\n    H-&gt;&gt;S: IpcAuthResponse (HMAC of nonce)\\n    S-&gt;&gt;H: IpcAuthResult (accepted/rejected)\\n\\n    loop Per tool invocation\\n        H-&gt;&gt;S: IpcCommand (tool, args, env, timeout)\\n        S-&gt;&gt;H: IpcResponse (exit_code, stdout, stderr)\\n    end\\n```\\n\\nThe host never sends raw shell commands. Every invocation is a structured `IpcCommand` with a named tool, an argument tuple, and optional environment variables. The agent executes the tool via `create_subprocess_exec` and returns structured output.\\n\\n## Wire Protocol\\n\\nDefined in `argicida_common/ipc_protocol.py`. Shared by both client and server.\\n\\n### Framing\\n\\nEvery message is length-prefixed: a 4-byte big-endian unsigned integer followed by that many bytes of msgpack payload. Maximum payload size is `MAX_IPC_MESSAGE_SIZE` (from `argicida_common.constants`). Empty frames are rejected.\\n\\n```\\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\\n\u2502 4 bytes  \u2502 N bytes              \u2502\\n\u2502 len (BE) \u2502 msgpack payload      \u2502\\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\\n```\\n\\n`write_frame()` and `read_frame()` handle this. `read_frame` accepts a `max_size` parameter \u2014 the handshake phase uses a tighter `MAX_AUTH_FRAME_SIZE` limit.\\n\\n### Authentication\\n\\nThe handshake is challenge-response. The server generates a random nonce and sends it as `IpcAuthChallenge`. The client computes `HMAC-SHA256(session_key, sequence=0 || timestamp=\\\"\\\" || nonce)` and returns the digest in `IpcAuthResponse`. The server verifies with `verify_hmac()` and replies with `IpcAuthResult`.\\n\\nThe `session_key` is a shared secret generated at pod creation time and passed to both sides \u2014 to the container via environment variable, to the host via `PodSession`.\\n\\n### Authenticated Messages\\n\\nAfter handshake, every `IpcCommand` and `IpcResponse` carries three authentication fields:\\n\\n| Field | Purpose |\\n|-------|---------|\\n| `sequence` | Monotonic counter \u2014 prevents replay |\\n| `timestamp` | ISO 8601 UTC \u2014 rejected if older than 30 seconds |\\n| `hmac_tag` | HMAC-SHA256 over `(sequence \\\\|\\\\| timestamp \\\\|\\\\| body)` |\\n\\n`send_message()` computes the HMAC by first encoding the message with `hmac_tag=b\\\"\\\"`, computing the tag over that encoding, then re-encoding with the real tag. `recv_message()` reverses this: decode, extract the tag, re-encode with `hmac_tag=b\\\"\\\"`, and verify. Sequence mismatches and expired timestamps are `ProtocolError`.\\n\\nThe HMAC input is deliberately structured \u2014 sequence and timestamp length are packed as fixed-width integers to prevent length-extension ambiguity:\\n\\n```\\nHMAC_input = pack(\\\"&gt;Q\\\", sequence) + pack(\\\"&gt;I\\\", len(timestamp)) + timestamp + body\\n```\\n\\n## Message Schema\\n\\nDefined in `argicida_common/ipc_schema.py`. All structs use `frozen=True, forbid_unknown_fields=True` per project convention.\\n\\n### `IpcCommand`\\n\\n```python\\ntool: str                    # Tool binary name (e.g., \\\"nmap\\\", \\\"nuclei\\\")\\nargs: tuple[str, ...]        # CLI arguments (max MAX_ARGS_COUNT)\\nrequest_id: str              # Correlation ID for output storage\\nenv: tuple[tuple[str, str], ...] = ()  # Environment variables (max MAX_ENV_COUNT)\\ntimeout_s: int = 300         # Kill the process after this many seconds (1-3600)\\n```\\n\\nEnvironment variables are validated by `validate_ipc_command_env()` against `ENV_ALLOWLIST` and `ENV_BLOCKLIST` before dispatch. Variables not on the allowlist are rejected \u2014 the container never inherits the host environment.\\n\\n### `IpcResponse`\\n\\n```python\\nrequest_id: str     # Matches the command's request_id\\nexit_code: int      # Process exit code\\nstdout: str         # Truncated to MAX_IPC_STDOUT_SIZE\\nstderr: str         # Truncated to MAX_IPC_STDERR_SIZE\\nerror: str | None   # Agent-level error (e.g., tool not found)\\n```\\n\\n## Host-Side Client\\n\\n`IpcClient` in `argicida/ipc/client.py` is the only consumer on the host side. It is instantiated per-thread via `threading.local()` in the tool dispatch layer \u2014 Hermes dispatches parallel tool calls on separate worker threads, each with its own event loop, so sharing a single client's asyncio streams across loops would fail.\\n\\n### Connection Lifecycle\\n\\n`connect()` retries up to 10 times with 500ms delay, handling `FileNotFoundError` (socket not yet created), `ConnectionRefusedError` (agent not ready), and `PermissionError` (namespace issues). Each failed attempt calls `_cleanup()` to release the stream.\\n\\n### Stale Connection Detection\\n\\nThe `connected` property checks whether the event loop that created the connection is still alive via `loop.is_closed()`. It deliberately does **not** compare loop identity (`is`) \u2014 different worker threads legitimately run different event loops, and a reconnect on a new loop is expected.\\n\\nWhen `send_command()` detects a stale connection, it calls `_force_close_stale()` (synchronous \u2014 no `await`, since the old loop may be dead) and then reconnects. Broken pipes during command send also trigger `_force_close_stale()`, but re-raise the exception \u2014 the caller decides whether to retry.\\n\\n### Integration Point\\n\\nThe only production caller is `_send_tool_command()` in `argicida/tools/base.py`. The full path from an LLM tool call to a container process is:\\n\\n```\\ntool handler \u2192 run_tool() \u2192 _send_tool_command() \u2192 IpcClient.send_command()\\n    \u2192 send_message(IpcCommand) / recv_message(IpcResponse)\\n```\\n\\nThe client is accessed through the `PodSession` which holds both the socket path and session key. `ensure_client()` in `tools/base.py` creates the per-thread client instance if one doesn't exist or the current one is stale.\\n\\n## Error Handling\\n\\nAll protocol-level failures raise `ProtocolError`:\\n\\n- Connection closed mid-read (incomplete header)\\n- Frame exceeds size limit\\n- HMAC verification failure\\n- Sequence number mismatch\\n- Message timestamp older than 30 seconds\\n- Authentication rejected during handshake\\n\\nTransport-level failures (`BrokenPipeError`, `ConnectionResetError`) are caught by `send_command()`, which resets the client state but lets the exception propagate. The `scan_handler` decorator in `tools/base.py` catches these at the tool dispatch boundary and converts them to error JSON for the LLM.\",\"management-cli\":\"# Management &amp; CLI\\n\\n# Management &amp; CLI\\n\\nArgicida exposes two parallel control surfaces for the same underlying engagement state: **LLM-facing tool handlers** that the agent calls during autonomous pentest sessions, and **operator-facing interfaces** (CLI subcommands and slash commands) for human oversight. Both surfaces share the same `EngagementStore`, `EvidenceChain`, and `PluginState` \u2014 they are different entry points into a single state machine.\\n\\n```mermaid\\ngraph LR\\n    LLM[\\\"LLM Agent\\\"] --&gt;|tool call| MH[\\\"management_handlers.pyartifact_handlers.py\\\"]\\n    OP[\\\"Operator\\\"] --&gt;|hermes argicida ...| CLI[\\\"commands/cli.py\\\"]\\n    OP --&gt;|/scope, /engage, ...| SLASH[\\\"commands/slash.py\\\"]\\n    MH --&gt; STORE[\\\"EngagementStore\\\"]\\n    CLI --&gt; STORE\\n    SLASH --&gt; STORE\\n    MH --&gt; NFT[\\\"nftables(scope propagation)\\\"]\\n    MH --&gt; ZAP[\\\"ZapManager\\\"]\\n```\\n\\n## Handler Factory Pattern\\n\\nEvery tool handler is built by a `make_*_handler()` factory that captures its dependencies via closure arguments rather than importing global state. The factory returns a `Callable[[dict[str, Any]], str]` (synchronous) or an async equivalent. This pattern exists because Hermes tool registration needs plain callables, and the dependencies (`store`, `get_eid`, `get_scope_updater`, etc.) aren't available until `register()` runs.\\n\\n```python\\n# From registration/management_tools.py \u2014 wiring happens once at plugin load\\nhandler = make_scope_define_handler(\\n    store=store,\\n    get_eid=rctx.get_eid,\\n    get_scope_updater=rctx.get_scope_updater,\\n)\\nctx.register_tool(schema=SCOPE_DEFINE_SCHEMA, handler=handler, toolset=\\\"argicida\\\")\\n```\\n\\nAll handlers return `json.dumps(...)` strings on success or `error_json(\\\"message\\\")` on failure. They never raise exceptions to the caller \u2014 every code path is wrapped in a top-level `try/except` that logs and returns an error JSON payload.\\n\\n## LLM-Facing Tool Handlers\\n\\n### management_handlers.py\\n\\n18 management tools, built by factory functions. The key ones:\\n\\n**Engagement lifecycle** \u2014 `make_engagement_create_handler` handles both creation and resume in a single handler. When the named engagement already exists, it binds to it (via `bind_engagement` from `engagement/lifecycle.py`). New engagements get scope validation, asset seeding for host-width CIDRs (`/32`, `/128`), and nftables propagation. The handler operates in two modes:\\n\\n- **Full mode** (with `state`, `chain`, `hermes_home`): session binding, evidence chain seeding, host replay. Used during live agent sessions.\\n- **Lightweight mode** (store only, or store + `set_eid`): creates/resumes without session binding. Used by tests and the query handler.\\n\\n**Scope management** \u2014 `make_scope_define_handler` merges new CIDRs/domains/exclusions with existing scope (via `_merge_unique` \u2014 union preserving order), validates the combined scope, persists it transactionally, propagates to nftables, and seeds assets. The merge behavior means scope is additive \u2014 calling `scope_define` twice with different CIDRs accumulates them.\\n\\n**Engagement queries** \u2014 `make_engagement_query_handler` is a dispatch hub. Simple queries route through a static table:\\n\\n| `query_type` | Repository | Method |\\n|---|---|---|\\n| `hosts` | `findings` | `query_hosts` |\\n| `ports` | `findings` | `query_ports` |\\n| `cves` | `findings` | `query_cves` |\\n| `tools_run` | `findings` | `query_tools_run` |\\n| `credentials` | `credentials` | `get_validated_credentials` |\\n| `principals` | `credentials` | `get_principals` |\\n| `secrets` | `credentials` | `get_secrets` |\\n\\nComplex queries (`findings`, `credential_candidates`, `assets`, `vhost_groups`, `scope`) have dedicated branches with richer parameter handling.\\n\\n**Host management** \u2014 `make_add_host_handler` and `make_remove_host_handler` manage DNS host entries. Adding a host writes an `address=/hostname/ip` line to the gateway's dnsmasq config and restarts dnsmasq via `restart_dnsmasq()`. With `scope_expand=True`, the host's IP is also added to the engagement scope and propagated to nftables. Both handlers update the asset registry.\\n\\n**Triage** \u2014 `make_triage_handler` returns filtered findings and optionally includes coverage gaps from `ServiceSkillRegistry` \u2014 services discovered by scans that haven't been enumerated yet. This drives the LLM's \\\"what should I enumerate next\\\" decision loop.\\n\\n**Briefing** \u2014 `make_briefing_handler` is async (`is_async=True` in Hermes). Delegates to `briefing/generator.py` which uses the plugin's LLM access (`ctx.llm`) to generate a narrative summary of findings.\\n\\n**ZAP tools** \u2014 Seven handlers wrap `ZapManager` methods: `zap_scan` (active scanning with optional authenticated contexts), `zap_alerts` (finding retrieval), `zap_context` (scan context creation), `zap_auth` (credential configuration including candidate testing via `credential_tester.py`), `zap_import` (OpenAPI/GraphQL spec import), `zap_spider` (authenticated crawling via AF plans), and `zap_intel` (Wappalyzer fingerprinting + login form detection). `zap_scan` and `zap_spider` are async and use `asyncio.to_thread` because ZAP's REST API is synchronous.\\n\\n### artifact_handlers.py\\n\\nFive handlers for managing files transferred from the tool container:\\n\\n- **`artifact_save`** \u2014 Copies a file from the container to the host-side artifacts directory via `transfer_from_container`. Validates the source path (absolute, no traversal, no null bytes) and prefix. Uses `O_CREAT | O_EXCL | O_NOFOLLOW` for collision-safe writes with automatic `-1`, `-2` suffixing.\\n- **`artifact_list`** \u2014 Lists files in the artifacts directory with sizes. Skips dotfiles, symlinks, and directories.\\n- **`artifact_read`** \u2014 Reads small text artifacts (\u226464 KiB) for LLM consumption. Rejects binary files, symlinks, and anything that escapes the artifacts directory.\\n- **`screenshot`** \u2014 Uses a custom nuclei template (`argicida/screenshot.yaml`) to capture a webpage screenshot, transfers the PNG from the container, and optionally runs vision analysis via `_analyze_screenshot`. The analysis uses `llm.acomplete_structured()` with a fixed JSON schema to extract application type, visible technologies, login form details, and security observations from the downscaled image.\\n\\nThe screenshot handler enforces scope on the target URL's hostname before dispatching the nuclei template. Filenames are derived from the URL with `_url_to_safe_filename` \u2014 either `{host}-{label}.png` (with explicit label) or `{host}-{sanitized_path}.png`.\\n\\n## Operator-Facing Interfaces\\n\\n### CLI Subcommands (`hermes argicida ...`)\\n\\n`setup_argicida_cli` registers the argument parser tree; `dispatch_argicida_cli` routes to dispatch functions. Entry point is `registration/commands.py` which calls both during plugin registration.\\n\\n| Subcommand | Purpose |\\n|---|---|\\n| `engagement list` | List all engagements with phase and creation time |\\n| `engagement resume ` | Bind to an existing engagement (creates synthetic CLI session) |\\n| `engagement unlock ` | Force-unlock a stuck engagement |\\n| `engagement create ` | Create engagement from a single IP/CIDR/domain |\\n| `scope ` | Display engagement scope |\\n| `findings  [--level]` | List findings with optional triage filter |\\n| `evidence verify ` | Verify evidence chain integrity |\\n| `pod start [--tool-image] [--gateway-image] [--zap-image] [--vpn-mode]` | Launch tool pod |\\n| `pod stop` | Tear down pod and topology |\\n| `pod status` | Show pod state |\\n| `build-image --tool/--gateway/--all` | Build container images via `pod/builder.py` |\\n| `update-data` | Download EPSS and KEV scoring data |\\n\\n`engagement create` uses `classify_scope_args` from `argicida_common` to auto-detect whether the target is an IP/CIDR or domain, then creates the engagement and binds via `EngagementSession.bind()` with a synthetic `cli-{uuid}` session ID.\\n\\n### Slash Commands (in-session)\\n\\nSix slash commands register via `ctx.register_command()` during plugin registration. These fire during a live agent conversation \u2014 the operator types them at the Hermes prompt.\\n\\n| Command | Handler factory | Behavior |\\n|---|---|---|\\n| `/scope` | `make_scope_handler` | Displays current engagement scope |\\n| `/triage` | `make_triage_handler` | Shows finding counts by triage level |\\n| `/approve-gate` | `make_approve_handler` | Signs evidence chain, approves HUMAN_GATE phase |\\n| `/briefing` | `make_briefing_slash` | Async \u2014 generates LLM narrative briefing |\\n| `/trace ` | `make_trace_handler` | Dispatches `argicida_tracepath` via `ctx.dispatch_tool()` |\\n| `/engage ` | `make_engage_handler` | Full engagement lifecycle: `new`, `resume`, `list`, `close`, `status`, `rename` |\\n\\nThe `/engage` handler is the most complex \u2014 it's a mini CLI within the slash command, handling engagement creation with scope parsing, resume with lock handling, and close with `unbind_engagement`. The `/approve-gate` handler only operates when the engagement is in `HUMAN_GATE` phase, and its primary action is signing the evidence chain with the operator's Ed25519 key.\\n\\n### Formatting\\n\\n`format_engagement_summary` in `commands/formatting.py` is shared between CLI (`_dispatch_scope`) and slash commands (`/scope`, `/engage status`). It renders engagement ID, phase, CIDRs, domains, and exclusions as a plain-text block.\\n\\n## Scope Propagation\\n\\nWhen scope changes (via `engagement_create`, `scope_define`, or `add_host` with `scope_expand`), the handler calls `_propagate_scope`. This invokes the `scope_updater` callback \u2014 set during registration to `PodSession.update_scope()` \u2014 which rebuilds the gateway's nftables FORWARD chain rules. If propagation fails, the scope is still persisted to the database, but the handler returns an `nftables_warning` field in the response. This is a deliberate design choice: scope state in the DB is authoritative, nftables enforcement is best-effort with visible warnings.\\n\\n## Data Downloads\\n\\n`download_epss_kev` in `commands/update_data.py` fetches EPSS scores (gzipped CSV from `epss.empiricalsecurity.com`) and KEV catalog (JSON from `cisa.gov`), validates their structure, and writes them atomically via `_atomic_write` (temp file + rename). Both files land in `$HERMES_HOME/argicida/data/` and are loaded by the triage system at session start for finding enrichment.\\n\\n## Adding a New Management Tool\\n\\n1. Write a `make_*_handler()` factory in `management_handlers.py` (or `artifact_handlers.py` for file-related tools).\\n2. Define the OpenAI-format schema in `tools/schemas.py`.\\n3. Wire the handler in `registration/management_tools.py` via `ctx.register_tool()`.\\n4. If the tool needs an active engagement, use `_require_eid(get_eid)` at the top of the handler.\\n5. If the tool modifies scope, call `_propagate_scope` and `_seed_host_assets`.\",\"other-claude-md\":\"# Other \u2014 CLAUDE.md\\n\\n# CLAUDE.md \u2014 AI-Assisted Development Configuration\\n\\n## Purpose\\n\\n`CLAUDE.md` is the project-level instruction file for [Claude Code](https://claude.ai/code), Anthropic's AI coding assistant. It serves as machine-readable developer documentation that shapes how Claude Code understands, navigates, and modifies the Argicida codebase. Every Claude Code session in this repository loads `CLAUDE.md` into its system context automatically.\\n\\nThis is not documentation for humans to read casually \u2014 it is a **binding contract** between the project and an AI agent. Every statement in it constrains Claude Code's behavior: what commands to run, what patterns to follow, what invariants to preserve, and what mistakes to avoid. Inaccurate or stale content directly causes incorrect code generation.\\n\\n## How It Works\\n\\nWhen Claude Code opens a session rooted at `/mnt/wsl_data/Projects/Argicida/`, it reads `CLAUDE.md` from the project root and injects its full content into the system prompt. This happens before any user interaction. The file's contents become persistent instructions that Claude Code follows for every action in that session \u2014 file edits, tool calls, code generation, test execution, and commit preparation.\\n\\nClaude Code also loads a second layer of instructions from the user's global `~/.claude/CLAUDE.md`, which contains cross-project rules (TDD, PEP 8, conventional commits, `uv` usage). The project-level file supplements these with Argicida-specific constraints.\\n\\n```mermaid\\ngraph TD\\n    A[\\\"~/.claude/CLAUDE.md(global rules)\\\"] --&gt; C[\\\"Claude CodeSystem Prompt\\\"]\\n    B[\\\"./CLAUDE.md(project rules)\\\"] --&gt; C\\n    D[\\\"~/.claude/projects/...memory[\\\"/memory/\\\"](persistent memory)\\\"] --&gt; C\\n    C --&gt; E[\\\"Every Claude Code actionin this session\\\"]\\n```\\n\\n## Structure and Sections\\n\\nThe file is organized into sections that map to different categories of development activity.\\n\\n### Commands\\n\\nA runnable command reference for every development task: dependency management (`uv sync`, `uv run`), linting (`ruff check`, `ruff format`), type checking (`mypy`), security scanning (`bandit`), testing at all three tiers (`pytest`, `pytest -m pod_integration`, `pytest -m e2e`), and container builds (`podman build`). These are the canonical invocations \u2014 Claude Code uses them verbatim rather than guessing at flags or paths.\\n\\n### Architecture\\n\\nThe largest section. Documents the system's structure in enough detail that Claude Code can make informed decisions about where to place new code, how subsystems interact, and what invariants to preserve. Key subsections:\\n\\n- **Three workspace packages** (`argicida`, `argicida_agent`, `argicida_common`) and their dependency relationships\\n- **Flexible-args tool model** with the 6 manual-target exceptions and their specific behaviors\\n- **Tool definition registry** (`tools/defs/.py`) and the discovery-based registration pattern\\n- **Scope guard** value flag classification (value-flag values, target-flag values, positional args)\\n- **Pod lifecycle** with dual-bridge topology, `PodSession` ownership semantics, and `cleanup_topology_by_name` as the single cleanup path\\n- **IPC file permission model** with three filesystem zones (`ipc_dir`, `scope_dir`, host-only dirs) and their distinct ownership rules\\n- **Argument normalizers** per tool, including `NseScriptMatcher`, `FlagValueGuard`, `RedisQueryNormalizer`\\n- **Asset registry** with the coverage state machine\\n- **ZAP subsystem** five-layer architecture\\n\\nEach of these subsections exists because Claude Code has previously made mistakes in that area. The detail level is calibrated to prevent specific classes of errors, not to be exhaustive for its own sake.\\n\\n### Security Rules\\n\\nHard constraints that Claude Code must never violate. These are not guidelines \u2014 they are invariants:\\n\\n| Rule | Prevents |\\n|------|----------|\\n| No `shell=True` or `create_subprocess_shell` | Command injection via shell metacharacters |\\n| No `pydantic.model_construct()` on untrusted data | Validation bypass |\\n| msgspec decode from wire format only | Construction-time validation bypass |\\n| `ipaddress.ip_network()` for all CIDR/IP input | nftables injection, malformed scope rules |\\n| `# nosec` requires explanation | Silent security suppression |\\n| `# type: ignore` requires error code + comment | Silent type-safety erosion |\\n| Restrictive file permissions with `O_NOFOLLOW` | Symlink attacks on SQLite databases |\\n\\n### Validation Rules\\n\\nRules about what constitutes \\\"done\\\" and how to verify work:\\n\\n- Container behavior requires full topology testing, not standalone `podman run`\\n- Features require E2E tests through real infrastructure, not just mocked unit tests\\n- The three-tier test architecture (unit \u2192 pod integration \u2192 E2E) and what each tier proves\\n- Integration tests use real Hermes `PluginManager`/`PluginContext`, not fakes\\n- Hooks don't fire during `registry.dispatch()` \u2014 must be tested via `pm.invoke_hook()`\\n\\n### Hermes Conventions\\n\\nIntegration rules for the host framework. These prevent Claude Code from reimplementing patterns that Hermes already provides:\\n\\n- `is_async=True` for async tool handlers (uses Hermes's `_run_async()`)\\n- Hooks must be synchronous `def`, not `async def`\\n- `ctx.llm` for LLM calls (not direct API clients)\\n- `ctx.dispatch_tool()` for tool invocation from slash commands\\n- `pre_llm_call` for context injection (not system prompt mutation)\\n\\n### Skill Context Architecture\\n\\nDocuments the three-tier progressive-disclosure system that manages what the LLM sees during a pentest session. This section exists because the relationship between tool schemas, `skill_view()` references, and sub-skill frontmatter `name:` fields is fragile \u2014 renaming a skill's `name:` field without updating `schemas.py` silently breaks the loading chain.\\n\\n### GitNexus Section\\n\\nAppended between `` and `` markers. Contains instructions for using the GitNexus code intelligence MCP tools (impact analysis, change detection, code queries). This section is managed by GitNexus tooling and should not be manually edited outside the markers.\\n\\n## Maintenance\\n\\n### When to Update\\n\\nUpdate `CLAUDE.md` when:\\n\\n- A new tool is added (update the tool count in Current Status, add normalizer docs if non-trivial)\\n- A new hook, slash command, or subsystem is introduced\\n- An architectural pattern changes (e.g., the migration from shared-namespace to dual-bridge topology)\\n- Claude Code makes a recurring mistake that a CLAUDE.md rule would prevent\\n- A Hermes convention changes upstream\\n- Test counts change significantly\\n\\n### What Not to Put Here\\n\\n- Transient project status (use `docs/` or git history)\\n- Step-by-step tutorials (the audience is an AI agent, not a human learner)\\n- Aspirational architecture (document what exists, not what's planned)\\n- Duplicate information already in code comments or docstrings\\n\\n### Relationship to Other Instruction Files\\n\\n| File | Scope | Loaded by |\\n|------|-------|-----------|\\n| `~/.claude/CLAUDE.md` | All projects for this user | Claude Code (global) |\\n| `./CLAUDE.md` | This project only | Claude Code (project) |\\n| `~/.claude/projects/.../memory/` | Persistent cross-session memory | Claude Code (memory system) |\\n| `profile/SOUL.md` | LLM persona during pentest sessions | Hermes agent loop |\\n| `profile/skills/security/` | LLM technique skills | Hermes skill system |\\n\\n`CLAUDE.md` instructs the *developer's AI assistant*. `SOUL.md` and skills instruct the *pentest agent LLM*. These are separate systems with separate audiences \u2014 do not conflate them.\\n\\n### The Detail Calibration Principle\\n\\nEvery paragraph in `CLAUDE.md` exists because its absence caused a concrete failure. The IPC file permission model section exists because Claude Code used `0o600` on host-written IPC files, causing `Permission denied` inside containers. The `value_flags` section exists because missing entries caused legitimate flag values to be rejected as out-of-scope targets. The `PodSession` ownership section exists because cleanup code was written that didn't use `cleanup_topology_by_name`, leaving orphaned resources.\\n\\nWhen adding new content, include enough detail to prevent the specific failure mode, and no more.\",\"other-docs-archive\":\"# Other \u2014 docs-archive\\n\\n# docs/archive \u2014 Historical Design &amp; Audit Records\\n\\nThis directory preserves the planning documents, infrastructure audits, and refactor plans that shaped Argicida's current architecture. Everything here is **frozen history** \u2014 the work described has either been completed or explicitly deferred. These documents are not living specifications; the codebase is the source of truth.\\n\\n## Why This Archive Exists\\n\\nArgicida was built at high velocity (607 commits in 14 days) and underwent two major architectural pivots: the deletion of the pipeline engine in favor of LLM-driven tool dispatch (2026-07-02), and a comprehensive source-driven refactor (2026-07-13/14). The archive captures the reasoning behind those decisions so future contributors can understand *why* the code looks the way it does without re-discovering the same failure modes.\\n\\n## Documents\\n\\n### NOTES.md\\n\\nEarly-stage design research from before implementation began. Covers:\\n\\n- **Container strategy** \u2014 Kali Linux base image with layered Go binaries, capability separation between gateway (CAP_NET_ADMIN) and tool container (CAP_NET_RAW), rootless Podman viability\\n- **Pod architecture** \u2014 VPN sidecar design, ZAP proxy integration, dual-layer scope enforcement (nftables + command validation), Unix socket IPC hardening\\n- **HTB as development targets** \u2014 rationale for using HackTheBox labs, networking constraints, blind spots requiring supplemental targets\\n- **Phase 1 retrospective** \u2014 what changed from original design (rootless Podman worked, gateway reframed from VPN sidecar to mandatory scope enforcer, nftables moved from host-side nsenter to gateway container)\\n\\nMost design decisions from NOTES.md were implemented. The pod layout diagram remains a reasonable high-level reference for the dual-bridge topology, though specifics (shared namespace vs. separate networks, ZAP as sidecar vs. standalone container) evolved during implementation.\\n\\n### AUDIT-2026-07-14.md\\n\\nInfrastructure audit that identified why live pentest sessions were failing. Six parallel audit agents covered autonomy loops, schema-behavior alignment, parser service population, SOUL.md contradictions, normalizer error recovery, and methodology-skill consistency.\\n\\n**27 findings total:** 5 critical, 12 high, 10 medium.\\n\\nThe five critical findings explained session failures:\\n1. Stale `_in_flight_delegations` creating permanent deadlocks (C1)\\n2. SOUL.md chaining/delegation contradiction causing model oscillation (C2)\\n3. Opaque \\\"Blocked\\\" error envelopes hiding rejection reasons (C3)\\n4. `_resolve_service_name` returning \\\"unknown\\\" and suppressing coverage gaps (C4)\\n5. NSE blocked-script rejections giving no alternative guidance (C5)\\n\\nAll critical and high findings were addressed in the refactor work that followed. The audit is useful for understanding the failure modes that the current error-handling, normalizer guidance, and coverage gap detection were designed to prevent.\\n\\n### REFACTOR-PLAN.md\\n\\nComprehensive refactor plan synthesizing five audits (git history, architecture, Hermes integration, prompt engineering, code quality). Contains:\\n\\n- **Root cause analysis** \u2014 maps all findings to six root causes: reactive development, documentation drift, god objects, unmanaged module-global state, triplicated classification lists, trial-and-error prompt tuning\\n- **Architecture redesign** \u2014 EngagementStore split into sub-repos, `register()` decomposition into typed phases, normalizer/context module splits, DRY consolidation\\n- **Prompt engineering overhaul** \u2014 SOUL.md restructuring, methodology skill rewrite, schema fixes, context injection changes\\n- **Implementation phases** \u2014 7 phases with completion criteria\\n\\nAll phases through Phase 6 were completed. Phase 7 (offline evaluation harness) was partially completed \u2014 consistency tests were added but session replay fixtures were deferred.\\n\\n### REFACTOR-PLAN-v2.md\\n\\nSource-driven companion to REFACTOR-PLAN.md. Every recommendation traces from an authoritative source (Anthropic tool design docs, Hermes plugin docs, PEP 8) to a specific code location. Contains 19 numbered findings across five categories:\\n\\n1. **Anthropic tool design violations** (10 findings) \u2014 schema enum blocking implemented query types, terse descriptions, methodology contradicting auto-injection\\n2. **Context engineering violations** (6 findings) \u2014 oversized subagent injection, stale delegation state, missing management tool reference\\n3. **Prompt engineering violations** (2 findings) \u2014 aggressive trigger language, content duplication\\n4. **Hermes integration violations** (5 findings) \u2014 internal module imports, kwargs parameter names, defensive getattr\\n5. **Architecture violations** (6 findings) \u2014 god classes, circular dependencies, triplicated lists\\n\\nAll phases marked COMPLETE with commit references. Remaining work items (context.py split, session replay fixtures, scope guard messages, hook ordering tests, normalizer split) were subsequently completed on 2026-07-14.\\n\\n## How to Use These Documents\\n\\n**When debugging session behavior:** Check AUDIT-2026-07-14.md for whether the failure mode was previously identified. The finding IDs (C1\u2013C5, H1\u2013H12, M1\u2013M10) are referenced in commit messages and test docstrings.\\n\\n**When understanding architectural decisions:** The root cause analysis in REFACTOR-PLAN.md section 2 explains why `EngagementStore` was split, why `register()` was decomposed, and why tool classification is derived from definitions rather than maintained as separate frozensets.\\n\\n**When modifying LLM-facing content:** REFACTOR-PLAN-v2.md section 1 documents the Anthropic tool design principles that govern schema descriptions, error messages, and skill references. The anti-pattern checklist in REFACTOR-PLAN.md section 8 lists what to verify on every change.\\n\\n**When adding new tools:** The code review checklist in REFACTOR-PLAN.md section 9 specifies what to verify \u2014 schema/normalizer alignment, tool classification, handler branch coverage, and skill reference consistency.\\n\\nDo **not** treat these documents as current specifications. If a document says \\\"the handler accepts X\\\" or \\\"the schema restricts to Y,\\\" verify against the current code \u2014 the refactor changed many of the interfaces described here.\",\"other-docs-research\":\"# Other \u2014 docs-research\\n\\n# Research Documents \u2014 `docs/research/`\\n\\nResearch artifacts that ground Argicida's architecture in empirical evidence rather than assumption. Each document was produced by multi-agent adversarial research workflows (25\u2013111 agents, 500\u20132200 tool calls) with explicit claim verification \u2014 findings carry vote counts and confidence levels, not opinion.\\n\\nThese are **decision records**, not living documentation. They capture what was true at research time and what architectural choices followed. The codebase may have evolved past some recommendations; when in doubt, the code is authoritative.\\n\\n## Why This Exists\\n\\nArgicida makes non-obvious architectural choices \u2014 no hardcoded scan pipeline, LLM as tool selector not executor, deterministic triage over LLM judgment, dual-bridge pod topology. Every major choice traces back to a verified finding in one of these documents. A developer encountering \\\"why doesn't the LLM pick exploits?\\\" or \\\"why not just use PTES phases as a pipeline?\\\" will find the empirical answer here.\\n\\n## Documents\\n\\n### `2026-07-02-offensive-security-research.md`\\n\\nThe foundational document. Established two principles that shaped every subsequent design decision:\\n\\n1. **Pentesting is not a pipeline.** Seven verified findings (8-0 to 15-0 adversarial votes) demonstrate that methodology standards are incompatible with each other, rarely fully implemented in practice, and explicitly described as guidelines by their own authors. This is why Argicida has no `ReconPhase \u2192 ExploitPhase \u2192 ReportPhase` pipeline \u2014 the LLM selects tools based on what it observes, not a predetermined sequence.\\n\\n2. **LLMs fail at decisions, succeed at perception.** Quantified from 8 academic sources (PentestGPT, CAI, CheckMate, PentestEval, VulnBot): attack decision-making scored 0.25 average, exploit revision scored 0.60, context overflow caused 42% of failures. This directly produced the architecture where the LLM interprets tool output and correlates findings (perception) but deterministic systems handle triage (EPSS+KEV scoring) and the operator handles exploitation decisions (HUMAN_GATE).\\n\\nThe CheckMate pattern (deterministic planning + LLM perception + human gates) validated Argicida's hybrid approach at 88% success rate, 53% lower cost than pure-LLM agents.\\n\\n**Four claims were refuted** and are documented as such \u2014 the document does not hide negative results.\\n\\n### `2026-07-04-hermes-configuration-deep-dive.md`\\n\\nComprehensive audit of 100+ Hermes Agent extension points, classified into four buckets:\\n\\n| Bucket | Count | Purpose |\\n|--------|-------|---------|\\n| Already in use | 30 | Confirms correct integration patterns |\\n| Should adopt | 25 | Actionable improvements with priority and effort |\\n| Not relevant | 35 | Explicitly ruled out with reasoning |\\n| Needs investigation | 12 | Open questions requiring hands-on testing |\\n\\nThe \\\"should adopt\\\" section is a prioritized backlog. Items marked **trivial** are config-only changes to `profile/config.yaml`. Notable high-priority items:\\n\\n- **`tool_loop_guardrails`** \u2014 circuit breaker for autonomous scanning (prevents burning 150-turn budget on a timing-out nmap)\\n- **`skills.write_approval`** \u2014 defense against prompt injection via adversarial scan output modifying methodology skills\\n- **Holographic memory** \u2014 cross-engagement intelligence via local SQLite FTS5 (works air-gapped)\\n\\nThe `check_fn` entry contains a post-hoc addendum marking it **NOT VIABLE** after live testing caused infinite loops \u2014 an example of the document being updated when reality contradicted the research.\\n\\nSection 3 (\\\"Not Relevant\\\") is worth reading when someone proposes adopting a Hermes feature \u2014 it may already be evaluated and rejected with reasoning.\\n\\n### `2026-07-10-c2-integration-strategy.md`\\n\\nArchitecture for exploitation and post-exploitation phases. Key decisions:\\n\\n- **Metasploit for exploitation** (2,300+ exploit modules, `db.analyze` for machine-ranked suggestions from imported Argicida findings)\\n- **Sliver for post-exploitation** (single binary, gRPC API, no Docker dependency, credential database)\\n- **Mythic excluded** from programmatic integration (verified Docker host networking dependency)\\n- **Existing Python clients all abandoned** \u2014 custom thin clients required\\n\\nThe critical architectural insight: **the C2 framework is the shared environment, not Argicida.** Argicida feeds intelligence in and reads state out. The LLM is an intelligence analyst sitting beside the operator, not a C2 operator. This prevents scope creep into reimplementing implant frameworks, shell managers, or SOCKS proxies.\\n\\nThe phased roadmap (Phase 0\u20135, 18\u201325 weeks total) starts with a read-only Metasploit bridge as minimum viable integration. Phase 0 (approval framework) is a prerequisite for all C2 work.\\n\\n## How Research Feeds Into Code\\n\\n```mermaid\\ngraph TD\\n    A[offensive-security-research] --&gt;|no pipeline| B[LLM-driven tool selection]\\n    A --&gt;|LLM fails at decisions| C[deterministic EPSS+KEV triage]\\n    A --&gt;|LLM succeeds at perception| D[OCSF normalization + perceptor]\\n    A --&gt;|human gates| E[HUMAN_GATE + /approve-gate]\\n    F[hermes-config-deep-dive] --&gt;|config wins| G[profile/config.yaml]\\n    F --&gt;|skill frontmatter| H[profile/skills/security/]\\n    I[c2-integration-strategy] --&gt;|future| J[src/argicida/argicida/c2/]\\n    I --&gt;|approval framework| K[ToolRiskLevel + /review]\\n```\\n\\n| Finding | Where it landed in code |\\n|---------|------------------------|\\n| No hardcoded pipeline | `register()` delegates to Hermes agent loop; no phase sequencing |\\n| Deterministic triage over LLM judgment | `triage.py` uses EPSS+KEV+CWE scoring |\\n| LLM as perceptor, not decider | `hooks/ocsf_normalizer.py` with `llm.complete_structured()` fallback |\\n| CheckMate hybrid pattern | HUMAN_GATE in engagement lifecycle |\\n| `tool_loop_guardrails` | `profile/config.yaml` (config-only) |\\n| `skills.write_approval` | `profile/config.yaml` (config-only) |\\n| C2 role separation | Not yet implemented; roadmap in the document |\\n\\n## Research Methodology\\n\\nAll three documents used multi-agent adversarial workflows:\\n\\n1. **Parallel research agents** read source material (academic papers, official docs, API references, industry reports)\\n2. **Claim extraction** \u2014 factual claims isolated from prose\\n3. **Adversarial verification** \u2014 independent agents attempt to refute each claim, voting on confidence\\n4. **Synthesis** \u2014 surviving claims assembled into architectural recommendations\\n\\nVote counts (e.g., \\\"13-1 vote\\\") represent verifier consensus. A claim that fails verification is documented as **refuted** with reasoning, not silently dropped.\\n\\n## Using These Documents\\n\\n**When proposing a new architecture pattern:** Check whether the research already evaluated it. The offensive-security doc covers methodology choices; the Hermes doc covers framework features; the C2 doc covers exploitation integration.\\n\\n**When questioning an existing design choice:** Trace it back to a verified finding. If the finding's source material has been superseded (new paper, new framework version), the choice may warrant revisiting \u2014 but update the research document first.\\n\\n**When the document contradicts current code:** The code wins. Update the document to note the divergence and why. The `check_fn` addendum in the Hermes doc is the model for this.\\n\\n**Do not treat these as implementation specs.** They inform constraints and trade-offs. Implementation details live in `docs/specs/` (active specs) and `CLAUDE.md` (conventions). The C2 roadmap phases are a rough plan, not a committed schedule.\",\"other-docs-rfe\":\"# Other \u2014 docs-rfe\\n\\n# RFE Directory \u2014 Design Decision Records\\n\\n`docs/rfe/` holds Request for Enhancement documents that capture the reasoning, alternatives analysis, and implementation scope for significant architectural changes to Argicida. Each RFE is a self-contained decision record: it states the problem, evaluates options, records what was chosen and why, and tracks implementation status.\\n\\nThese are not meeting notes or feature requests. An RFE is written **before** implementation begins and updated as work progresses. It serves two audiences: the implementer (who needs precise constraints and interface contracts) and the future maintainer (who needs to understand why the system looks the way it does).\\n\\n## Document Lifecycle\\n\\n```\\nDraft \u2192 Implementing \u2192 Implemented \u2192 Verified\\n                                        \u2191\\n                                  status updated when\\n                                  E2E tests confirm behavior\\n```\\n\\nEach document carries a status header:\\n\\n| Status | Meaning |\\n|--------|---------|\\n| **Draft** | Design proposed, not yet approved or started |\\n| **Implementing** | Active development underway |\\n| **Implemented** | Code merged, may lack full verification |\\n| **Verified** | Confirmed working via pod integration or E2E tests, with verification date |\\n\\nThe `Verified` status includes a date (e.g., `Verified: 2026-07-24`) so readers can judge staleness. An RFE verified six months ago against a codebase that has since changed warrants re-checking.\\n\\n## Document Structure\\n\\nRFEs follow a consistent structure, though sections vary by scope:\\n\\n- **Problem** \u2014 what is broken or missing, with concrete evidence (scan results, test failures, architectural gaps). No vague \\\"it would be nice if.\\\"\\n- **Current State** \u2014 what exists today, verified against the live codebase at the time of writing. Includes tables of implemented vs. missing capabilities.\\n- **Proposed Solution** \u2014 one or more options, each with architecture diagrams, code sketches, and compatibility analysis. The recommended option is marked explicitly.\\n- **Podman Compatibility / Design Constraints** \u2014 Argicida runs in rootless Podman pods with specific filesystem permission models, UID namespace shifting, and network topology constraints. Every RFE must address how the proposed change interacts with these.\\n- **Implementation Scope** \u2014 ordered list of work items with file paths. Items are checked off as they land.\\n- **Open Questions** \u2014 numbered, with resolution status. Resolved questions retain their original text plus the answer \u2014 they are not deleted.\\n\\n## Current Documents\\n\\n### `2026-07-11-zap-proxy-web-scanning.md`\\n\\n**Status:** Implemented, verified 2026-07-24\\n\\nDefines the ZAP sidecar architecture \u2014 how ZAP runs as a container within the Argicida pod, intercepts web tool traffic via auto-injected proxy flags, runs passive analysis on every HTTP response, and provides on-demand active scanning through the `zap_scan` management tool.\\n\\nKey decisions recorded here:\\n\\n- **Option A selected** (ZAP as passive MITM proxy + on-demand active scanner) over three alternatives: proxify in front of ZAP (Option B, deferred), proxify standalone (Option C, rejected), mitmproxy (Option D, rejected).\\n- **CA certificate distribution** via `scope_dir` (not `ipc_dir`) because U/chown mount shifting breaks host-side overwrites. The entrypoint installs the cert via `SSL_CERT_FILE` environment variable on a tmpfs copy.\\n- **`ZapProxyInjector` normalizer** auto-injects per-tool proxy flags (`-proxy`, `-http-proxy`, `-x`) when ZAP is active. LLM-provided proxy flags take precedence.\\n- **Active scan scope enforcement** is doubly enforced: nftables FORWARD chain on the gateway (network layer) plus `pre_tool_call` hook validation (command layer). One open item remains \u2014 `zap_scan` lacks a `ToolProfile` entry for command-layer target URL validation.\\n\\nThis RFE is the foundational reference for all ZAP-related code in `zap/`, `tools/normalizers_zap.py`, `tools/parsers_zap.py`, `tools/transformers_zap.py`, and the ZAP-related fields in `pod/config.py`, `pod/launcher.py`, and `pod/session.py`.\\n\\n### `2026-07-25-zap-automation-framework.md`\\n\\n**Status:** Implemented (2026-07-26)\\n\\nRedesigns ZAP usage from individual REST API calls to ZAP's Automation Framework (AF) \u2014 declarative YAML plans that compose spider, passive analysis, authentication, active scanning, and reporting into pipelines.\\n\\nKey decisions recorded here:\\n\\n- **Why the original approach failed** \u2014 against DVWA, 10 minutes of active scanning produced 0 findings because unauthenticated spidering only discovered the login page. This is not DVWA-specific; any app behind a login wall has the same problem.\\n- **Browser-based auth as the default** \u2014 form/JSON auth methods are deprecated upstream (July 2025) because they cannot authenticate browsers launched by the Client Spider or AJAX Spider.\\n- **Complete AF job catalog** \u2014 27 job types documented with full YAML parameter schemas, gotchas, and usage notes. This serves as the primary reference for `zap/plan_generator.py`.\\n- **Passive-to-active rule mapping** \u2014 passive findings (no CSRF tokens, CORS misconfiguration, debug errors) inform which active scan rules to enable, reducing scan time and noise.\\n- **Credential testing via `sendRequest`** \u2014 the Fuzzer add-on has no production API (PR #2222 closed July 2026, Swing dependency). Credential testing uses `core/action/sendRequest` loops for simple forms and browser-based auth attempts for CSRF-protected forms.\\n- **`planProgress` does not return alerts** \u2014 a critical implementation constraint. Findings must always be fetched via `core/view/alerts` after plan completion.\\n\\n### `2026-07-26-test-target-infrastructure.md`\\n\\n**Status:** Draft\\n\\nProposes replacing the single nginx:alpine test target with two additional vulnerable applications \u2014 WebGoat (form-based auth, active scan surface) and VAmPI (JWT auth, OpenAPI import) \u2014 to exercise the AF integration paths that a static file server cannot cover.\\n\\nKey decisions recorded here:\\n\\n- **Why not DVWA, Juice Shop, or Gin and Juice Shop** \u2014 each rejected with specific technical reasons (fragile under scanning, no Docker image, outdated stack).\\n- **nginx:alpine retained** for scope enforcement, basic dispatch, and planted exposure tests. Not replaced.\\n- **Test coverage matrix** mapping each AF integration path to its validating target. This matrix drives which tests use which fixture.\\n\\n## How to Read an RFE\\n\\nStart with the **Problem** section to understand what motivated the change. If you only need the architectural outcome, skip to **Proposed Solution** and read the recommended option. If you are debugging or extending the implementation, the **Implementation Scope** checklist maps decisions to specific files, and **Open Questions** records constraints that shaped the code.\\n\\nThe **Current State** tables (capabilities implemented vs. missing, API methods wired vs. unwired) are snapshots from the authoring date. Cross-reference against the actual codebase before relying on them \u2014 the `Verified` date tells you how fresh the snapshot is.\\n\\n## How to Write an RFE\\n\\nWrite an RFE when a change touches three or more files across different subsystems, introduces a new external dependency (container image, API, protocol), or changes the pod topology or IPC model. Single-file bug fixes and refactors do not need RFEs.\\n\\nNaming convention: `YYYY-MM-DD-.md`. The date is when the document was first drafted, not when implementation started.\\n\\nRequired headers:\\n\\n```markdown\\n# RFE: \\n\\n**Date:** YYYY-MM-DD\\n**Status:** Draft | Implementing | Implemented | Verified\\n**Verified:** YYYY-MM-DD  (only when Status is Verified)\\n**Type:** Enhancement | Redesign | New Subsystem\\n**Depends on:**  (if applicable)\\n**Scope:** comma-separated file paths affected\\n```\\n\\nThe `Depends on` field creates a dependency chain between RFEs. The ZAP AF RFE depends on the ZAP proxy RFE; the test target RFE depends on the ZAP AF RFE. Read dependencies before reading the dependent document.\\n\\nEvery proposed option must address Podman compatibility (rootless, UID namespace, filesystem permissions, network topology). Code sketches should use actual Argicida types and patterns \u2014 `PodManager.exec_sync()`, `scope_dir` vs. `ipc_dir`, `0o644` for container-readable files. An RFE that hand-waves these constraints will produce an implementation that fails at integration time.\\n\\nOpen questions are numbered and never renumbered. When resolved, the original question text stays and the resolution is appended. This preserves the audit trail \u2014 a reader can see what was uncertain and how it was resolved.\\n\\n## Relationship to CLAUDE.md\\n\\nCLAUDE.md captures the **current** architectural truth \u2014 what the codebase looks like right now. RFEs capture the **reasoning** \u2014 why it looks that way, what alternatives were considered, and what constraints drove the design. When CLAUDE.md says \\\"ZAP runs as a sidecar with browser-based auth as the default,\\\" the ZAP AF RFE explains why form/JSON auth was rejected and documents the 10-minute zero-finding DVWA failure that motivated the change.\\n\\nIf an RFE's implementation scope is complete and verified, the architectural decisions should already be reflected in CLAUDE.md. If they are not, update CLAUDE.md \u2014 stale documentation in either location is a bug (per the project's validation rules: \\\"stale gap docs are lies\\\").\",\"other-docs-superpowers\":\"# Other \u2014 docs-superpowers\\n\\n# ZAP Integration Design Archive\\n\\n`docs/superpowers/archive/` contains the design specifications and implementation plans that drove the ZAP subsystem (`src/argicida/argicida/zap/`). These are historical records \u2014 the implemented code is authoritative, but these documents capture the *why* behind architectural decisions.\\n\\n## Documents\\n\\n| Document | Role | Status |\\n|----------|------|--------|\\n| `2026-07-22-zap-deep-integration-design.md` | Architecture spec \u2014 layered subsystem design, container image, pod lifecycle, tool definitions | **Approved**, implemented |\\n| `2026-07-22-zap-deep-integration.md` | Implementation plan \u2014 8 TDD tasks with file maps, interface contracts, test-first steps | **Executed** |\\n| `2026-07-22-zap-integration-gaps-design.md` | Gap closure spec \u2014 12 issues from pentester audit: finding enrichment, AJAX Spider, API import, context injection | **Draft**, partially implemented |\\n\\n## How These Documents Relate\\n\\nThe design spec came first and established the three-layer architecture (ZapClient \u2192 ZapManager \u2192 tools). The implementation plan decomposed that spec into ordered tasks with explicit interface contracts between them \u2014 each task declares what it consumes and produces so parallel work stays compatible. The gaps design followed a real-world audit and extended the original spec without changing the layer boundaries.\\n\\n```mermaid\\ngraph TD\\n    A[ZAP Deep Integration Design] --&gt;|decomposed into| B[Implementation Plan]\\n    A --&gt;|audited, extended by| C[Integration Gaps Design]\\n    B --&gt;|produced| D[zap/ package + tools]\\n    C --&gt;|extends| D\\n```\\n\\n## Key Decisions Recorded Here\\n\\n**Why `exec_sync(curl)` instead of the `zaproxy` PyPI package.** The host cannot reach `127.0.0.1:8090` inside the pod namespace under rootless Podman. All ZAP access must go through `PodManager.exec_sync()` running curl inside the ZAP container. A ~150-line custom client is less code than subclassing `ZAPv2` to override its transport. (Design spec, Section 3.)\\n\\n**Why ZAP Automation Framework was excluded.** The LLM is the orchestrator \u2014 a second orchestration layer inside ZAP is architecturally backwards. The LLM calls `zap_scan`, `zap_context`, `zap_auth` directly and decides sequencing based on what it discovers. (Design spec, Section 8.)\\n\\n**Why CA cert lives in `scope_dir`, not `ipc_dir`.** The `U`/`chown` mount on `ipc_dir` shifts ownership to the container's UID namespace, making host-side overwrites fail with `PermissionError`. `scope_dir` is host-owned throughout pod lifetime. (Design spec, Section 2b; also documented in CLAUDE.md's IPC File Permission Model.)\\n\\n**Why `ZapManager` lives on `PluginState`, not `PodSession`.** Different lifecycle \u2014 `PodSession` owns topology resources (pod, networks, temp dirs), while `ZapManager` owns application-level state (contexts, auth configs, scan policies). `ZapManager` is created after ZAP healthcheck and cleared in `on_session_finalize` after persisting findings. (Design spec, Section 4.)\\n\\n**Why the credential-to-auth mapping uses auto-detection with explicit override.** The `secret_type` field on `ValidatedCredential` (`token`, `api_key`, `password`) maps deterministically to a ZAP auth method, but pentesting often requires overriding this \u2014 a password credential might need JSON-based auth instead of form-based. The `method` parameter on `zap_auth` lets the LLM override when auto-detection is wrong. (Design spec, Section 4; gaps design, Section 9.)\\n\\n## Reading These Documents\\n\\nThe implementation plan (`2026-07-22-zap-deep-integration.md`) is the most useful for understanding how the subsystem was built. Each task lists:\\n\\n- **Files** \u2014 what was created and modified\\n- **Interfaces** \u2014 exact function signatures with types, documenting what each task consumes from prior tasks and produces for later ones\\n- **TDD steps** \u2014 test code written before implementation code, with expected failure modes\\n\\nThe interface contracts between tasks are the critical detail. Task 4 (ZapProxyInjector) declares it consumes `has_flag()` from `argicida.utils` and the `NormalizeResult` protocol from `tools/defs/_protocol.py` \u2014 if those interfaces changed, the plan would need updating before execution.\\n\\nThe gaps design (`2026-07-22-zap-integration-gaps-design.md`) includes verified ZAP API endpoint references with parameter names matching ZAP's Java source. The \\\"API Reference\\\" section at the end (truncated in this view) documents every endpoint's exact parameter names and types, verified against three independent sources. This is the reference to consult when adding new `ZapClient` methods.\\n\\n## Relationship to Live Code\\n\\nThese documents are **archived** \u2014 the `archive/` directory signals they've been executed or superseded. When the implementation diverges from these documents (and it will, as bugs are fixed and features evolve), the code wins. The documents remain valuable for understanding original intent, especially the \\\"deliberately excluded\\\" sections that explain what was considered and rejected.\\n\\nThe active specs directory is `docs/specs/`. If a ZAP design document appears there instead of in `archive/`, it hasn't been implemented yet.\",\"other-docs\":\"# Other \u2014 docs\\n\\n# Project Documentation (`docs/`)\\n\\nThe `docs/` directory holds three reference documents that govern how Argicida is built, what tools it integrates, and how its triage subsystem works. Each document has a different authority level and audience.\\n\\n## Document Index\\n\\n| Document | Authority | Audience | When to Read |\\n|----------|-----------|----------|--------------|\\n| `DEVELOPMENT-STANDARDS.md` | **Mandatory** \u2014 every rule is a defect if violated | All contributors | Start of every development session |\\n| `RESEARCH.md` | **Reference** \u2014 informs tool selection and roadmap | Contributors adding tools or planning features | Before proposing new tool integrations or architecture changes |\\n| `triage-engine.md` | **Technical spec** \u2014 describes the scoring pipeline | Contributors working on finding parsers, hooks, or triage logic | Before modifying anything in `hooks/`, `tools/parsers*.py`, or triage-related code |\\n\\n## Development Standards\\n\\n`DEVELOPMENT-STANDARDS.md` is the project's post-incident corrective action plan. It was written after a forensic analysis of the first 634 commits revealed a 1.21:1 fix-to-feature ratio (2.4x worse than industry baseline), 49% of fixes landing within 3 commits of the feature they repair, and 38 tests with zero assertions.\\n\\nThe document has four parts:\\n\\n**Part 1 (Forensic Analysis)** quantifies the failures with commit hashes, churn metrics, and test suite pathology. It identifies five root causes (RC1\u2013RC5) from an independent audit by 4 specialized agents on 2026-07-14. Three are resolved, one partially. The RC status sections track remediation progress \u2014 update them when gaps close.\\n\\n**Part 2 (Pattern Taxonomy)** catalogs five recurring failure patterns, each linked to persistent feedback memories in the auto-memory system. These patterns were identified because the same mistake was corrected multiple times across sessions before being formalized.\\n\\n**Part 3 (Rules)** contains 28 numbered rules (R-01 through R-28) organized by the failure category they prevent. Every rule cites the evidence that motivated it. The rules cover:\\n\\n- R-01 through R-03: Research before code (prevents guessing)\\n- R-04 through R-07: Resource lifecycle (prevents leaks)\\n- R-08 through R-09: Data integrity (prevents silent corruption)\\n- R-10 through R-14: Testing and verification (prevents false completion claims)\\n- R-15 through R-18: Commit discipline (prevents broken commits)\\n- R-19 through R-22: Debugging protocol (prevents symptom-patching)\\n- R-23 through R-25: Delegation quality (prevents unverified subagent output)\\n- R-26 through R-28: Accountability (prevents blame deflection)\\n\\n**Part 4 (Pre-Commit Checklist)** is the operational checklist that enforces the rules. The checklist includes an infrastructure-specific addendum for changes touching pods, IPC, containers, or scope.\\n\\n### Relationship to CLAUDE.md\\n\\n`DEVELOPMENT-STANDARDS.md` and `CLAUDE.md` overlap intentionally. CLAUDE.md is the machine-readable instruction set loaded into every Claude Code session. Development Standards is the human-readable document with full context, evidence, and rationale. When they conflict, CLAUDE.md wins (it is the operational authority), but the conflict should be resolved by updating whichever document is stale.\\n\\n## Tool Landscape Research\\n\\n`RESEARCH.md` documents a 40-agent adversarial research workflow (~3.2M tokens, ~1,480 tool calls) that evaluated the penetration testing tool landscape on 2026-06-29. It informed every architectural decision in Argicida.\\n\\nThe document evaluates three categories of tools:\\n\\n**Category 1 (LLM Agent Pentest Frameworks)** \u2014 concludes that no production-ready, local-first, scope-enforced LLM pentest agent exists. This finding is the reason Argicida exists as a Hermes plugin rather than a wrapper around an existing framework.\\n\\n**Category 2 (TTP Knowledge Frameworks)** \u2014 identifies Caldera + Atomic Red Team + TTPForge as the mature, interoperable stack for adversary emulation. These are Phase 6 roadmap items.\\n\\n**Category 3 (Traditional Automation Tools)** \u2014 ranks the ProjectDiscovery pipeline (subfinder, httpx, naabu, nuclei) as Tier 1 and identifies the network enumeration gap (SMB, SNMP, LDAP, SSH, RPC) that drove Phase 5 tool expansion.\\n\\n### Superseded Sections\\n\\nThe Integration Strategy section contains both current and superseded content. The original phased roadmap (Phases 1, 2A, 2B, 3, 4) was replaced on 2026-07-02 when the architecture shifted from a pipeline engine to LLM-driven tool dispatch. The current roadmap (Phases 3\u20136) follows the supersession notice. Phases 1 and 2A are marked complete with delivery summaries. The superseded phases are preserved for historical context \u2014 they explain why certain design decisions were made and later reversed.\\n\\n### Verification Claims\\n\\nThe \\\"Claims Killed by Verification\\\" table at the end is a key reference. It documents 20 fabricated or incorrect claims found during the adversarial verification pass. Before citing any tool capability from an external source, check this table \u2014 if the claim appears, the documented reality supersedes it.\\n\\n### Gap Analysis\\n\\nSeven capability gaps (Gap 1\u20137) define what Argicida must build because no existing tool provides it. Current status:\\n\\n| Gap | Description | Status |\\n|-----|-------------|--------|\\n| Gap 1 | Scope Guardian | Implemented (dual-layer: nftables + command validator) |\\n| Gap 2 | Finding Triage | Implemented (see `triage-engine.md`) |\\n| Gap 3 | Engagement Lifecycle | Implemented (LLM-driven, not FSM) |\\n| Gap 4 | Cross-Engagement Learning | Phase 6 roadmap |\\n| Gap 5 | Tool Output Normalization | Implemented (OCSF subset) |\\n| Gap 6 | Exploitation Boundary | Partially addressed (template/flag blocklists) |\\n| Gap 7 | Automated-to-Interactive Transition | Implemented (HUMAN_GATE + `/briefing`) |\\n\\n## Triage Engine\\n\\n`triage-engine.md` is the technical specification for the finding scoring and classification pipeline. It documents the four-stage flow from raw tool output to prioritized findings.\\n\\n```mermaid\\nflowchart LR\\n    A[Tool Output] --&gt; B[Parsers]\\n    B --&gt; C{Findings?}\\n    C --&gt;|Yes| D[Dedup]\\n    C --&gt;|No| E[LLM Perceptor]\\n    E --&gt; D\\n    D --&gt; F[EPSS/KEV Enrichment]\\n    F --&gt; G[Composite Score]\\n    G --&gt; H[Classify]\\n    H --&gt; I[CRITICAL / INTERESTING / NOISE]\\n```\\n\\nThe document covers:\\n\\n- **Seven deterministic parsers** mapped to specific tools, with their CVE/CWE/credential extraction capabilities\\n- **LLM perceptor fallback** \u2014 fires only when deterministic parsers return nothing, uses `ctx.llm.complete_structured()` for structured extraction\\n- **Composite scoring formula** \u2014 weighted combination of severity (0.25), CVSS (0.20), EPSS (0.25), KEV (0.20), and ransomware linkage (0.10)\\n- **Classification rules** \u2014 deterministic priority-ordered rules that assign CRITICAL, INTERESTING, or NOISE triage levels\\n- **Hook integration** \u2014 how the five Hermes hooks (`scope_guard`, `normalize_result`, `capture_evidence`, `output_store`, `build_context`) wire the pipeline into the agent lifecycle\\n\\nThe air-gapped design is load-bearing: EPSS and KEV data are pre-downloaded local files, not runtime API calls. This supports deployments with no internet access during active engagements.\\n\\n## Other Documentation\\n\\nThe `docs/` directory also contains subdirectories not covered by this module's source set:\\n\\n- `docs/specs/` \u2014 Design specifications for specific features. Read the active spec before implementing; it contains dependency versions, interface contracts, and architectural constraints.\\n- `docs/archive/` \u2014 Superseded specs and gap analyses. Preserved for historical context, not operational reference.\\n- `docs/research/` \u2014 Research artifacts beyond the main `RESEARCH.md` (e.g., the 2026-07-02 offensive security research that informed the LLM-driven redesign).\\n\\n## Maintaining These Documents\\n\\nThe Development Standards document states: \\\"Gap analysis docs in `docs/` are living documents. Update them when gaps are fixed \u2014 stale gap docs are lies.\\\" This applies to all three documents:\\n\\n- When an RC status changes in the remediation plan, update Part 1 of `DEVELOPMENT-STANDARDS.md`\\n- When a roadmap phase completes or a gap closes, update `RESEARCH.md`\\n- When a parser is added or the scoring formula changes, update `triage-engine.md`\",\"other-profile-skins\":\"# Other \u2014 profile-skins\\n\\n# Profile Skins \u2014 `argicida.yaml`\\n\\n## Overview\\n\\nThe `profile/skins/` directory contains Hermes skin definitions \u2014 YAML files that theme the CLI's visual presentation. A skin controls colors, spinner animations, branding text, tool iconography, and banner artwork. Argicida ships one skin, `argicida.yaml`, which replaces Hermes's default appearance with a bronze-and-verdigris palette inspired by the mythological Hermes Argeiphontes (slayer of Argus Panoptes).\\n\\nSkins are pure configuration \u2014 no Python code, no call graph, no runtime logic. Hermes loads the YAML and applies it to its Rich-based terminal renderer.\\n\\n## Installation and Activation\\n\\n```bash\\n# Copy to the Hermes skins directory\\ncp profile/skins/argicida.yaml $HERMES_HOME/skins/argicida.yaml\\n\\n# Activate interactively\\n/skin argicida\\n\\n# Or set permanently in config.yaml\\ndisplay:\\n  skin: argicida\\n```\\n\\nThe skin is **not** installed automatically by `hermes profile install`. It must be copied manually or added to a post-install step.\\n\\n## File Structure\\n\\nThe skin YAML has seven top-level sections:\\n\\n| Section | Purpose |\\n|---------|---------|\\n| `name` / `description` | Identity \u2014 Hermes uses `name` for `/skin` lookup |\\n| `colors` | Hex color assignments for every UI surface |\\n| `spinner` | Animated spinner frames and verb lists |\\n| `branding` | Agent name, welcome/goodbye messages, prompt symbol |\\n| `tool_prefix` | Character drawn before tool output lines |\\n| `tool_emojis` | Per-tool icon mapping (tool name \u2192 emoji) |\\n| `banner_logo` / `banner_hero` | Rich-markup ASCII art for the startup banner |\\n\\n## Colors\\n\\nColors are organized into five functional groups, each mapped to a region of the Hermes TUI:\\n\\n**Banner** \u2014 the startup display. `banner_border` (verdigris frame), `banner_title` (gold heading), `banner_accent` (warm bronze highlights), `banner_dim` (muted commentary), `banner_text` (readable body).\\n\\n**UI elements** \u2014 interactive chrome. `ui_accent` (gold, primary highlight), `ui_label` (cool blue \u2014 the \\\"watchful eye\\\" color, used sparingly), `ui_ok`/`ui_error`/`ui_warn` (semantic status).\\n\\n**Prompt** \u2014 the input area. `prompt` (warm text), `input_rule` (separator line), `response_border` (frame around agent output).\\n\\n**Session** \u2014 engagement tracking. `session_label` (blue, matching `ui_label`), `session_border` (dark frame).\\n\\n**Surfaces** \u2014 backgrounds for menus, status bar, and selection. All use near-black values (`#161410`, `#2A2820`) for a dark-temple aesthetic. The completion menu has four background keys covering normal/current states and their meta variants.\\n\\nAll values are hex RGB strings. Hermes passes them directly to Rich's style engine.\\n\\n## Spinner\\n\\nThe spinner section defines animated sequences shown while the agent works:\\n\\n- **`waiting_faces`** \u2014 eye closing: `(\u25c9) \u2192 (\u25ce) \u2192 (\u25cb) \u2192 (\u00b7) \u2192 (\u25cb) \u2192 (\u25ce)`. Cycles while idle.\\n- **`thinking_faces`** \u2014 includes `(\u2298)` (the struck eye) in the cycle. Shown during LLM inference.\\n- **`thinking_verbs`** \u2014 eight myth-flavored status messages (\\\"lulling the watchman\\\", \\\"drawing the blade\\\", etc.) displayed alongside the spinner.\\n- **`wings`** \u2014 bracket pairs flanking the spinner: `\u27e8\u25c9 ... \u25c9\u27e9`, `\u27e8\u263f ... \u263f\u27e9`, `\u27e8\u25cc ... \u25cc\u27e9`, `\u27e8\u2020 ... \u2020\u27e9`. Hermes cycles through these as visual phase markers.\\n\\n## Branding\\n\\n```yaml\\nbranding:\\n  agent_name: \\\"Argicida\\\"\\n  welcome: \\\"Argicida ready. The hundred eyes are watching.\\\"\\n  goodbye: \\\"The watchman sleeps. \u263f\\\"\\n  response_label: \\\" \u263f Argicida \\\"\\n  prompt_symbol: \\\"\u263f\\\"\\n  help_header: \\\"(\u263f) Available Commands\\\"\\n```\\n\\nThe Mercury symbol (\u263f) is the consistent glyph. `response_label` appears in the border of every agent response block. `prompt_symbol` replaces Hermes's default `&gt;` at the input line.\\n\\n## Tool Emojis\\n\\nEvery registered Argicida tool has a unique emoji for visual scanning in output:\\n\\n| Category | Tools | Icons |\\n|----------|-------|-------|\\n| Network recon | nmap, naabu, tracepath | \ud83d\uddfa \ud83d\udeaa \ud83d\udc63 |\\n| Web | httpx, nuclei, katana, ffuf, curl | \ud83c\udfdb \ud83d\udc41 \u2694 \ud83d\udd13 \ud83e\udeb6 |\\n| DNS | dnsx, subfinder | \ud83d\udcdc \ud83d\udd31 |\\n| Services | smbclient, rpcinfo, db_query, redis_query, ssh_exec | \ud83d\udcc1 \ud83d\udd14 \ud83c\udffa \ud83d\udd11 \ud83d\udddd |\\n| Management | engagement_create, scope_define, triage_findings, briefing_generate | \ud83d\udccb \ud83c\udfaf \u2696 \ud83d\udcc4 |\\n| Assets/artifacts | artifact_save/list/read, screenshot, add/remove_host | \ud83d\udcbe \ud83d\udce6 \ud83d\udcd6 \ud83d\udcf8 \u2795 \u2796 |\\n\\nThe mapping key is the full tool name as registered in Hermes (e.g., `argicida_nmap`). Tools not listed here fall back to Hermes's default tool icon.\\n\\n## Banner Art\\n\\nTwo Rich-markup blocks render at startup:\\n\\n- **`banner_logo`** \u2014 block-letter \\\"ARGICIDA\\\" using box-drawing characters. Six lines, each tagged with a progressively darker color from `#D4A574` (bright bronze) at the top to `#4A5A3A` (dark verdigris) at the bottom, simulating an inscription lit from above.\\n\\n- **`banner_hero`** \u2014 a braille-dot eye (Argus Panoptes) with a vertical color gradient: verdigris crown \u2192 bronze iris \u2192 gold center \u2192 rapid fade to shadow below. The Mercury glyph (\u263f) beneath marks the kill. Both blocks use Rich's `[color]...[/]` inline markup syntax.\\n\\n## Adding a New Skin\\n\\nTo create an alternative skin, copy `argicida.yaml`, change `name`, and modify any section. All keys are optional \u2014 Hermes falls back to defaults for missing entries. The minimum viable skin is:\\n\\n```yaml\\nname: my-skin\\ndescription: \\\"one-line description\\\"\\ncolors:\\n  ui_accent: \\\"#FF0000\\\"\\n```\\n\\nPlace the file in `$HERMES_HOME/skins/` and activate with `/skin my-skin`.\",\"other-profile\":\"# Other \u2014 profile\\n\\nI now have a thorough understanding of the profile module. Let me write the documentation.\\n\\n# Pentest Profile (`profile/`)\\n\\nThe `profile/` directory is a [Hermes profile distribution](https://hermes-agent.nousresearch.com/) \u2014 a self-contained package that transforms a generic Hermes agent into a penetration testing operator. When installed, it configures the LLM identity, model routing, tool access, delegation behavior, and technique skills for security assessments.\\n\\nThis is not application code. Nothing here is imported by the Python packages in `src/`. Instead, Hermes reads these files at runtime to configure the agent session. The profile is the interface between Argicida's tooling infrastructure and the LLM that drives it.\\n\\n## How Installation Works\\n\\n```bash\\nhermes profile install /path/to/Argicida/profile --alias\\n```\\n\\nHermes copies the profile contents to `$HERMES_HOME/profiles/pentest/` and creates a `pentest` command alias. After installation, the operator interacts with the system via `pentest chat`, never by running Argicida directly.\\n\\nThe installed copy is the live runtime copy \u2014 the LLM reads SOUL.md and skills from there, not from the repository. Changes made by `hermes profile update pentest` or by the LLM at runtime (skill self-improvement) modify the installed copy. Use `scripts/sync_skills.py` to reconcile diffs between the repo and runtime copies.\\n\\n```mermaid\\nflowchart LR\\n    R[\\\"profile/(repo, version-controlled)\\\"]\\n    I[\\\"$HERMES_HOME/profiles/pentest/(installed, runtime)\\\"]\\n    R -- \\\"hermes profile install\\\" --&gt; I\\n    R &lt;-- \\\"scripts/sync_skills.py\\\" --&gt; I\\n    I -- \\\"read at session start\\\" --&gt; H[\\\"Hermes Agent\\\"]\\n    H -- \\\"skill self-improvement\\\" --&gt; I\\n```\\n\\n## File Inventory\\n\\n| File | Purpose |\\n|------|---------|\\n| `distribution.yaml` | Profile metadata: name (`pentest`), version, `hermes_requires` constraint, optional env vars (Shodan/Censys API keys) |\\n| `config.yaml` | Full Hermes configuration: model, provider routing, agent behavior, delegation, tool access, guardrails |\\n| `SOUL.md` | LLM identity and behavioral directives \u2014 defines *who* the agent is and *how* it operates |\\n| `skins/argicida.yaml` | Terminal skin: colors, spinners, tool emojis, banner art. Activated by `display.skin: argicida` in config |\\n| `.no-bundled-skills` | Empty sentinel file that tells Hermes not to bundle default skills \u2014 only `skills/` ships |\\n| `.gitignore` | Excludes runtime-generated files (state DBs, sessions, memories, caches) from version control |\\n| `skills/security/` | LLM-facing technique skills (13 top-level, 13 sub-skills) |\\n\\n## `config.yaml` \u2014 Agent Configuration\\n\\nThe config controls every aspect of the Hermes session. Key decisions encoded here:\\n\\n**Model routing.** The default model is Gemma 4 12B via a local llama-server at `localhost:8080`. Two model aliases provide task-specific routing: `scan` (Gemma 4 12B for interactive tool execution) and `analyze` (Qwen 35B on a second endpoint at `:8081` for heavier analysis). Qwen 35B is also the fallback provider.\\n\\n**Toolset lockdown.** All non-Argicida toolsets are explicitly disabled \u2014 `terminal`, `file`, `browser`, `web`, `vision`, `code_execution`, and 17 others. The LLM can only use `argicida_*` tools, `skills`, `memory`, `todo`, `clarify`, and `delegate_task`. This prevents the model from attempting shell access or file operations that would bypass the container isolation.\\n\\n**Delegation.** The orchestrator delegates scan execution to subagents (`max_concurrent_children: 4`, `max_iterations: 50`). `child_timeout_seconds: 0` disables subagent timeouts \u2014 scans run as long as they need. Subagents use the same model as the orchestrator.\\n\\n**Guardrails.** Tool loop detection warns after 3 exact-repeat failures and hard-stops after 5. Same-tool failure thresholds are slightly higher (4/7). These prevent the LLM from burning its turn budget retrying broken commands.\\n\\n**Other notable settings:**\\n- `agent.max_turns: 150` \u2014 extended budget for full assessments (default Hermes is 90)\\n- `terminal.timeout: 300` \u2014 5-minute timeout per tool execution\\n- `compression.threshold: 0.71` with `protect_last_n: 35` \u2014 aggressive context compression but preserves recent turns\\n- `security.allow_private_urls: true` \u2014 required for scanning RFC 1918 targets\\n- `skills.write_approval: true` \u2014 operator must approve skill modifications\\n- `tools.tool_search.enabled: 'off'` \u2014 disables dynamic tool search (all tools are pre-registered)\\n\\n**Quick commands.** Two diagnostic shortcuts available at the Hermes prompt:\\n- `/pod-status` \u2014 runs `podman pod ps --format json`\\n- `/engagement-db` \u2014 queries the 5 most recent engagements from SQLite\\n\\n## `SOUL.md` \u2014 LLM Identity and Behavior\\n\\nSOUL.md is the system prompt extension that defines the agent's persona and operational rules. It is injected into every LLM turn. The document uses XML-tagged sections (``, ``, ``, etc.) for structured behavioral control.\\n\\n### Autonomy Tiers\\n\\nThe core design decision: which actions the LLM executes immediately vs. which require operator approval.\\n\\n**Autonomous** \u2014 the LLM runs these without asking:\\n- All 16 scan tools (nmap, nuclei, httpx, naabu, dnsx, subfinder, katana, ffuf, curl, smbclient, rpcinfo, tracepath, db_query, redis_query, mongo_query, ssh_exec)\\n- All management tools (engagement_create, scope_define, triage_findings, briefing_generate, artifact_save/list/read, screenshot, engagement_query)\\n- Infrastructure tools (add_host, remove_host)\\n- Tool chaining based on results\\n\\n**Gated** (requires `/approve-gate`):\\n- Exploits, payloads, shells, implants\\n- Credential bruteforce beyond default-login templates\\n- Modifying target state (writing files, creating accounts)\\n- Irreversible actions\\n\\nThe decision rule defaults to **execute** when uncertain \u2014 the scope guard enforces boundaries at the infrastructure level, so the SOUL doesn't need to be the last line of defense.\\n\\n### Delegation Architecture\\n\\nSOUL.md defines an orchestrator/subagent split. The orchestrator:\\n- Runs lightweight management tools directly (engagement_create, scope_define, engagement_query, add_host)\\n- Delegates all scan execution to subagents via `delegate_task`\\n- Passes toolset filters per task type (e.g., `[\\\"argicida\\\", \\\"argicida-web\\\", \\\"skills\\\"]` for HTTP enumeration)\\n- Queries the engagement database for structured results after delegation returns \u2014 never parses subagent text\\n\\nEach delegation context must include: engagement name, scope, target, objective, and a `skill_view('pentest-methodology')` instruction so the subagent loads the tool syntax reference.\\n\\n### Chaining Rules\\n\\nSOUL.md prescribes tool chains organized by phase:\\n\\n- **Discovery:** naabu \u2192 nmap -sV\\n- **Enumeration:** HTTP \u2192 httpx \u2192 nuclei \u2192 katana \u2192 ffuf; DNS \u2192 scope_define \u2192 dnsx \u2192 add_host \u2192 scan new hosts\\n- **Deep Testing:** ZAP active \u2192 credential testing \u2192 file retrieval \u2192 exploitation follow-up\\n\\nThe key directive: \\\"Do not stop between steps to ask what to do next.\\\"\\n\\n## Skills \u2014 Three-Tier Progressive Disclosure\\n\\nSkills are the LLM's technique knowledge. They live in `profile/skills/security/` and deploy to `$HERMES_HOME/profiles/pentest/skills/security/` on install.\\n\\n### Context Budget Management\\n\\nSkills use a progressive-disclosure design to manage the LLM's limited context window:\\n\\n| Tier | When loaded | Token cost | Content |\\n|------|-------------|------------|---------|\\n| Always present | Every turn | ~900 tokens | Tool syntax table, service-to-tool matching table (from pentest-methodology) |\\n| First turn only | Orchestrator's first turn | ~3800 tokens | Full pentest-methodology (decision trees, interpretation guides, common mistakes) |\\n| On demand | `skill_view('name')` | 500\u20133000 each | Service-specific technique skills |\\n\\nSubagents receive only the ~900-token quick-reference plus a `skill_view('pentest-methodology')` instruction \u2014 never the full methodology content.\\n\\n### Skill Inventory\\n\\n**Root-level skills** (13):\\n\\n| Skill | Covers |\\n|-------|--------|\\n| `pentest-methodology` | Tool syntax reference, argument rules, enumeration order, result interpretation |\\n| `network-discovery` | naabu/nmap recon workflow, port discovery, service detection |\\n| `dns-enumeration` | dnsx: zone transfers, record types, reverse DNS, subdomain bruteforce |\\n| `web-enumeration` | Hub skill: 11-phase scan strategy, links to 8 sub-skills |\\n| `db-enumeration` | Hub skill: triage order, usql contract. Links to 5 DB-specific sub-skills |\\n| `mongo-enumeration` | mongosh via mongo_query |\\n| `smb-enumeration` | smbclient share listing and file access |\\n| `rpc-nfs-enumeration` | rpcinfo, NFS mounts, NIS detection |\\n| `redis-enumeration` | redis-cli: auth, ACL, config, keyspace |\\n| `ssh-enumeration` | Banner analysis, auth methods, credential testing |\\n| `ftp-enumeration` | Anonymous access, directory listing, credential testing |\\n| `smtp-enumeration` | Relay testing, user enumeration |\\n| `telnet-enumeration` | Banner grabbing, authentication testing |\\n\\n**Hub skills** contain sub-skills in subdirectories:\\n\\n- `web-enumeration/` \u2192 8 sub-skills: `web-fingerprinting`, `web-scanning`, `web-crawling`, `web-fuzzing`, `web-api`, `web-curl`, `web-api-testing`, `web-services`\\n- `db-enumeration/` \u2192 5 sub-skills: `db-postgresql`, `db-mysql`, `db-mssql`, `db-oracle`, `db-cassandra`\\n\\n### Load-Bearing Naming Convention\\n\\nTool schemas in `src/argicida/argicida/tools/schemas.py` reference sub-skills by their frontmatter `name:` field (e.g., `skill_view('web-curl')`). Hermes resolves these via frontmatter name matching, not directory path. Renaming a sub-skill's `name:` field without updating `schemas.py` silently breaks the skill-loading chain \u2014 the LLM will use tools without technique guidance.\\n\\n## `skins/argicida.yaml` \u2014 Terminal Theme\\n\\nA custom Hermes terminal skin themed after the Argicida mythology (Hermes slaying Argus Panoptes). Defines:\\n\\n- **Color palette** \u2014 aged bronze/verdigris/warm shadow, not neon terminal\\n- **Spinner animations** \u2014 eye symbols (\u25c9 \u2192 \u25ce \u2192 \u25cb \u2192 \u00b7) representing Argus's eyes closing\\n- **Tool emojis** \u2014 per-tool icons in the progress display (e.g., nmap = \ud83d\uddfa, nuclei = \ud83d\udc41, katana = \u2694)\\n- **Banner art** \u2014 block-letter logo with bronze gradient, braille-dot Eye of Argus hero image\\n- **Branding** \u2014 agent named \\\"Argicida\\\", Mercury glyph (\u263f) as prompt symbol\\n\\nActivated in `config.yaml` via `display.skin: argicida`.\\n\\n## Syncing Skills Between Repo and Runtime\\n\\nThe LLM can self-improve skills at runtime (with operator approval per `skills.write_approval: true`). These changes modify the installed copy, not the repo. To reconcile:\\n\\n```bash\\nuv run python scripts/sync_skills.py\\n```\\n\\nThe script walks every `SKILL.md` in `profile/skills/`, finds its runtime counterpart by frontmatter `name:` match, diffs them, and prompts for sync direction (repo\u2192runtime or runtime\u2192repo). Skills without a runtime counterpart are flagged as undeployed.\\n\\n## Relationship to the Codebase\\n\\nThe profile has no code dependencies \u2014 it is pure configuration consumed by Hermes. But it has implicit contracts with the Argicida plugin:\\n\\n- **Tool names** in SOUL.md and skills must match tool registrations in `src/argicida/argicida/tools/`\\n- **Toolset names** in delegation contexts (`argicida-web`, `argicida-service`, etc.) must match `TOOLSET` values in `tools/defs/.py`\\n- **Skill names** referenced in tool schemas (`schemas.py`) must match frontmatter `name:` fields in skill files\\n- **Slash commands** (`/engage`, `/scope`, `/triage`, `/briefing`, `/approve-gate`, `/trace`) are registered by the plugin, not the profile\\n- **`covers_services`** metadata in skill frontmatter drives `ServiceSkillRegistry` for coverage gap detection in the plugin\\n\\nChanges to tool registration, toolset grouping, or slash command names in the plugin require corresponding updates in the profile \u2014 and vice versa. There is no automated consistency check beyond the existing consistency tests in `tests/`.\",\"other-pyproject-toml\":\"# Other \u2014 pyproject.toml\\n\\n# Workspace Configuration \u2014 `pyproject.toml`\\n\\nThe root `pyproject.toml` defines the Argicida monorepo as a **uv workspace** and centralizes all tool configuration (linting, type checking, security scanning, testing). It is not a publishable package \u2014 it exists to coordinate three workspace members and enforce consistent quality standards across them.\\n\\n## Workspace Structure\\n\\nArgicida uses uv's workspace feature to manage three packages with separate dependency trees but shared tooling configuration.\\n\\n```mermaid\\ngraph TD\\n    ROOT[\\\"pyproject.toml(workspace root)\\\"] --&gt; COMMON[\\\"src/argicida_common/Shared types \u00b7 msgspec only\\\"]\\n    ROOT --&gt; HOST[\\\"src/argicida/Host plugin \u00b7 heavy deps\\\"]\\n    ROOT --&gt; AGENT[\\\"src/argicida_agent/Container agent \u00b7 minimal deps\\\"]\\n    HOST --&gt; COMMON\\n    AGENT --&gt; COMMON\\n    HOST -.-&gt;|dev dependency| HERMES[\\\"hermes-agent(local editable)\\\"]\\n```\\n\\n| Package | Location | Role |\\n|---------|----------|------|\\n| `argicida-common` | `src/argicida_common/` | IPC message types, finding schemas, scope definitions. Single dependency: `msgspec`. |\\n| `argicida` | `src/argicida/` | Host-side Hermes plugin. Registers tools, hooks, CLI commands. Heavy dependency tree. |\\n| `argicida-agent` | `src/argicida_agent/` | Thin container agent that executes tools over Unix socket IPC. Minimal deps. |\\n\\nEach member has its own `pyproject.toml` under its `src/` directory declaring its specific dependencies. The root workspace aggregates them so `uv sync` installs everything in one pass.\\n\\n### Source Resolution\\n\\n```toml\\n[tool.uv.sources]\\nargicida = { workspace = true }\\nargicida-agent = { workspace = true }\\nargicida-common = { workspace = true }\\nhermes-agent = { path = \\\"/mnt/wsl_data/Projects/hermes-agent\\\", editable = true }\\n```\\n\\nThe three workspace members resolve to each other via `workspace = true` \u2014 uv links them as editable installs. `hermes-agent` is an **external editable dependency** pointing to a sibling checkout on disk. This means changes to Hermes are immediately visible without reinstalling.\\n\\n### Optional Dependencies\\n\\n```toml\\n[project.optional-dependencies]\\nvision = [\\\"Pillow&gt;=10.0\\\"]\\n```\\n\\nThe `vision` extra enables screenshot analysis in the host plugin. Install with `uv sync --extra vision`. The plugin degrades gracefully when Pillow is absent.\\n\\n## Dev Dependencies\\n\\nAll quality tooling is grouped under `[dependency-groups] dev`:\\n\\n| Tool | Purpose |\\n|------|---------|\\n| `ruff` | Linting (PEP 8, security rules) and formatting (black-compatible) |\\n| `mypy` | Static type checking in strict mode |\\n| `bandit[toml]` | Security-focused static analysis |\\n| `pytest` | Test runner with asyncio, coverage, and timeout plugins |\\n| `hermes-agent` | Real Hermes `PluginManager`/`PluginContext` for integration tests |\\n| `jsonschema` | Schema validation in tests |\\n| `pydot` | Graph generation (test visualization) |\\n\\n## Tool Configuration\\n\\n### Ruff (Lint + Format)\\n\\nTargets Python 3.13. Line length 88 (black-compatible). Source roots set to `src/` so imports resolve correctly across the nested package layout.\\n\\n**Enabled rule sets:**\\n\\n- `E`/`W` \u2014 pycodestyle errors and warnings\\n- `F` \u2014 pyflakes (unused imports, undefined names)\\n- `I` \u2014 isort (import ordering)\\n- `N` \u2014 PEP 8 naming conventions\\n- `UP` \u2014 pyupgrade (modernize syntax to 3.13)\\n- `B` \u2014 flake8-bugbear (common pitfalls)\\n- `S` \u2014 flake8-bandit (security, inline)\\n- `A` \u2014 flake8-builtins (shadowed builtins)\\n- `C4` \u2014 flake8-comprehensions\\n- `SIM` \u2014 flake8-simplify\\n- `TCH` \u2014 flake8-type-checking (move type-only imports behind `TYPE_CHECKING`)\\n- `RUF` \u2014 Ruff-specific rules\\n\\n**Per-file ignores** relax security rules in test code:\\n\\n| Pattern | Suppressed | Reason |\\n|---------|-----------|--------|\\n| `tests/**` | `S101`, `S108` | `assert` statements and `/tmp` usage are normal in tests |\\n| `tests/pod_integration/**`, `tests/e2e/**` | `S101`, `S108`, `S603`, `S607` | Additionally allows subprocess calls with partial paths (required for Podman invocations) |\\n| `store.py`, `context.py` | `TC003` | Type imports that can't move behind `TYPE_CHECKING` due to runtime use |\\n| `vendor/**` | `ALL` | Third-party vendored code is not linted |\\n\\n### Mypy\\n\\nStrict mode with `explicit_package_bases = true` and `namespace_packages = true` \u2014 required by the nested `src/pkg/pkg/` layout. Without these, mypy misresolves the package hierarchy.\\n\\nThree override blocks suppress missing import errors for untyped dependencies:\\n- `libnmap` \u2014 no `py.typed` marker or stubs\\n- `PIL` (Pillow) \u2014 no `py.typed` marker\\n- `hermes_cli` \u2014 no `py.typed` marker\\n\\nEach override includes a comment explaining *why* it exists. Adding `type: ignore` directives elsewhere requires the specific error code and an explanation.\\n\\n### Bandit\\n\\nScans `src/` only, excludes `tests/` and `vendor/`. Both severity and confidence thresholds are set to `low` \u2014 nothing is filtered out. The `skips` list is empty; suppressing a finding requires a `# nosec` comment with an explanation in the source file.\\n\\n### Pytest\\n\\n```toml\\ntestpaths = [\\\"tests\\\"]\\nasyncio_mode = \\\"auto\\\"\\naddopts = \\\"-m 'not e2e and not pod_integration'\\\"\\n```\\n\\nBy default, `uv run pytest` runs only unit and integration tests. The marker filter excludes tests that require infrastructure:\\n\\n| Marker | Requires | Run with |\\n|--------|----------|----------|\\n| *(none)* | Nothing | `uv run pytest` |\\n| `pod_integration` | Podman + built images + `nginx:alpine` | `uv run pytest -m pod_integration` |\\n| `e2e` | Above + running LLM endpoint | `uv run pytest -m e2e` |\\n\\n### Coverage\\n\\n```toml\\n[tool.coverage.run]\\nsource = [\\\"argicida\\\", \\\"argicida_agent\\\", \\\"argicida_common\\\"]\\nbranch = true\\n\\n[tool.coverage.report]\\nfail_under = 80\\n```\\n\\nBranch coverage is enabled. The 80% threshold is enforced \u2014 `uv run pytest --cov` fails the build if coverage drops below it. All three packages are measured.\\n\\n## Common Operations\\n\\n```bash\\n# Install everything (all workspace members + dev deps)\\nuv sync\\n\\n# Run all fast tests\\nuv run pytest\\n\\n# Full quality gate (what CI runs)\\nuv run ruff check .\\nuv run ruff format --check .\\nuv run mypy -p argicida -p argicida_common -p argicida_agent\\nuv run bandit -c pyproject.toml -r src/\\nuv run pytest --cov\\n\\n# Add a dependency to a specific workspace member\\ncd src/argicida &amp;&amp; uv add \\n\\n# Add a dev dependency to the workspace root\\nuv add --dev \\n```\",\"other-readme-md\":\"# Other \u2014 README.md\\n\\nThe README is ready to write. It covers the project's purpose, prerequisites, install/usage instructions, architecture (with a Mermaid diagram showing the host-container IPC topology), all three workspace packages, the dual-bridge network topology, two-layer scope enforcement, tool model, hook system, ZAP integration, engagement database, development commands, test architecture, the process for adding new tools, pre-engagement data setup, and configuration.\\n\\nEverything references actual code paths and component names from the codebase \u2014 no invented APIs.\",\"other-src-argicida-agent\":\"# Other \u2014 src-argicida_agent\\n\\nNow I have everything needed. Let me write the documentation.\\n\\n# `argicida_agent` \u2014 Container-Side Tool Executor\\n\\nThe thin agent that runs **inside** the Podman tool container. It listens on a Unix socket for HMAC-authenticated commands from the host-side Argicida plugin, executes scan tool binaries via `create_subprocess_exec`, and returns structured results over the same socket. It never decides what to run \u2014 it only does what it's told.\\n\\n## Why It Exists\\n\\nArgicida's security model isolates all offensive tooling inside a container. The host process (Hermes + the `argicida` plugin) cannot exec binaries that live inside the container's filesystem. The agent bridges this gap: a process inside the container that accepts commands, runs the requested binary, and sends back stdout/stderr/exit code. It is deliberately minimal \u2014 two modules, one dependency beyond the shared types (`msgspec`), no decision logic.\\n\\n```mermaid\\ngraph LR\\n    Host[\\\"Host (Hermes + argicida plugin)\\\"] --&gt;|\\\"Unix socket/ipc/agent.sock\\\"| Agent[\\\"argicida_agent(container)\\\"]\\n    Agent --&gt;|create_subprocess_exec| Nmap[nmap]\\n    Agent --&gt;|create_subprocess_exec| Nuclei[nuclei]\\n    Agent --&gt;|create_subprocess_exec| Other[... 16 more tools]\\n```\\n\\n## Package Structure\\n\\n```\\nsrc/argicida_agent/\\n\u251c\u2500\u2500 pyproject.toml\\n\u2514\u2500\u2500 argicida_agent/\\n    \u251c\u2500\u2500 __init__.py\\n    \u251c\u2500\u2500 __main__.py          # Entrypoint: parse args, validate key, start server\\n    \u251c\u2500\u2500 ipc/\\n    \u2502   \u2514\u2500\u2500 server.py        # AgentServer: socket lifecycle, auth, command loop\\n    \u2514\u2500\u2500 executor/\\n        \u2514\u2500\u2500 runner.py        # execute_command: tool allowlist, subprocess, truncation\\n```\\n\\nTwo runtime dependencies: `msgspec` (wire format) and `argicida-common` (shared IPC types and constants). No Podman SDK, no pydantic, no cryptography \u2014 those belong on the host side.\\n\\n## Startup\\n\\nThe container's `entrypoint.sh` launches the agent as PID 1's child:\\n\\n```\\nexec python3 -m argicida_agent --socket /ipc/agent.sock\\n```\\n\\n`__main__.py` does three things before accepting connections:\\n\\n1. **Reads `ARGICIDA_SESSION_KEY`** from the environment \u2014 a hex-encoded 32-byte (256-bit) HMAC key injected by the host when creating the pod. Exits immediately if missing, malformed, or wrong size.\\n\\n2. **Builds the allowed-tools map** from `argicida_common.constants.TOOL_NAMES` \u2014 a hardcoded `dict[str, str]` mapping tool names (`\\\"nmap\\\"`) to absolute binary paths (`\\\"/usr/bin/nmap\\\"`). This is the only allowlist; the agent rejects any tool name not in this dict.\\n\\n3. **Starts `AgentServer`** on the Unix socket path, then calls `serve_forever()`.\\n\\n## IPC Server (`ipc/server.py`)\\n\\n`AgentServer` manages the Unix socket lifecycle and per-connection state.\\n\\n### Socket Setup\\n\\nThe agent must handle rootless Podman's UID namespace mismatch: the host UID (typically 1000) maps to container root (UID 0), not to the agent's UID (10000). On startup, the server:\\n\\n- Rejects symlinked socket paths (prevents symlink attacks)\\n- Removes any stale socket file\\n- Sets the parent directory to `0o777` and creates the socket with `umask(0o000)` so the host process can connect across the namespace boundary\\n\\n### Connection Handling\\n\\nEach incoming connection is dispatched to a task via `asyncio.create_task`. The server enforces a hard cap of `MAX_CONNECTIONS` (10) concurrent connections \u2014 excess connections are closed immediately. This matches Hermes's parallel tool dispatch model (multiple worker threads, each with its own `IpcClient`).\\n\\n### Authentication Handshake\\n\\nEvery connection authenticates before any commands are accepted:\\n\\n```\\nServer                          Client (host)\\n  \u2502                                \u2502\\n  \u251c\u2500\u2500 IpcAuthChallenge(nonce) \u2500\u2500\u2500\u2500&gt;\u2502\\n  \u2502                                \u2502\\n  \u2502&lt;\u2500\u2500\u2500\u2500 IpcAuthResponse(hmac) \u2500\u2500\u2500\u2500\u2524\\n  \u2502                                \u2502\\n  \u251c\u2500\u2500 IpcAuthResult(accepted) \u2500\u2500\u2500\u2500&gt;\u2502\\n```\\n\\n1. Server generates a 32-byte random nonce and sends it as an `IpcAuthChallenge`.\\n2. Client computes `HMAC-SHA256(session_key, nonce)` and sends the digest back.\\n3. Server verifies the HMAC via `verify_hmac()` from `argicida_common.ipc_protocol`. Sends `IpcAuthResult(accepted=True/False)`.\\n\\nThe entire handshake must complete within `AUTH_TIMEOUT_S` (10 seconds) or the connection is dropped. After authentication, every subsequent message is HMAC-tagged with a monotonically increasing sequence number to prevent replay attacks.\\n\\n### Command Loop\\n\\nAfter auth, the server enters a `recv \u2192 execute \u2192 send` loop:\\n\\n1. **Receive**: `recv_message()` deserializes an `IpcCommand` from the wire, verifying its HMAC tag and sequence number.\\n2. **Execute**: Delegates to `execute_command()` in the executor module.\\n3. **Respond**: `send_message()` serializes an `IpcResponse`, computes its HMAC tag, and sends it.\\n\\nEach receive has a `COMMAND_IDLE_TIMEOUT_S` (300s) deadline \u2014 if the host doesn't send a command within 5 minutes, the connection is dropped. This prevents leaked connections from accumulating.\\n\\n## Command Executor (`executor/runner.py`)\\n\\n`execute_command()` is where tool invocations actually happen. It implements a strict validation-then-exec pipeline:\\n\\n### Validation Gates\\n\\nThree checks, in order, before any subprocess is created:\\n\\n1. **Tool allowlist** \u2014 Is `cmd.tool` in the `allowed_tools` dict? If not, returns an error response listing what's allowed.\\n\\n2. **Environment validation** \u2014 `validate_ipc_command_env()` checks every env var against an allowlist (`SHODAN_API_KEY`, `CENSYS_API_SECRET`, etc.) and blocklist (`LD_PRELOAD`, `PATH`, `PYTHONPATH`, etc.). Any violation returns an error before execution.\\n\\n3. **Binary existence** \u2014 `shutil.which()` resolves the binary path. Missing binary \u2192 error response. This catches container build issues early.\\n\\n### Subprocess Execution\\n\\n```python\\nproc = await asyncio.create_subprocess_exec(\\n    binary, *cmd.args,\\n    stdout=asyncio.subprocess.PIPE,\\n    stderr=asyncio.subprocess.PIPE,\\n    env=env_dict,\\n)\\n```\\n\\nKey design choices:\\n\\n- **`create_subprocess_exec`, never `shell=True`** \u2014 arguments are passed as a list, preventing shell injection. This is a hard security requirement.\\n- **Per-command timeout** \u2014 `cmd.timeout_s` (default 300s, max 3600s, validated by msgspec `Meta` constraints). On timeout, the process is killed with `SIGKILL` and an error response is returned.\\n- **Optional environment overlay** \u2014 If `cmd.env` is non-empty, the current environment is cloned and the specified vars are added. Otherwise the container's default environment is inherited.\\n\\n### Output Truncation\\n\\nTool output can be enormous (a full nmap scan of a /16, nuclei against thousands of templates). The executor enforces hard byte limits:\\n\\n| Stream | Limit |\\n|--------|-------|\\n| stdout | 16 MiB (`MAX_IPC_STDOUT_SIZE`) |\\n| stderr | 1 MiB (`MAX_IPC_STDERR_SIZE`) |\\n\\nTruncation is byte-aware: raw bytes are sliced first, then decoded with `errors=\\\"replace\\\"`. A post-decode check trims one character if `U+FFFD` replacement at the truncation boundary pushed the UTF-8 encoding over the limit. A defensive `RuntimeError` assert catches any bug in this logic.\\n\\nWhen truncation occurs, the `error` field on `IpcResponse` carries a note like `\\\"stdout truncated at 16000000 bytes.\\\"` so the host knows the output is incomplete.\\n\\n## What the Agent Does NOT Do\\n\\nUnderstanding what's excluded is as important as what's included:\\n\\n- **No scope enforcement** \u2014 the host-side `pre_tool_call` hook and the gateway's nftables rules handle scope. The agent executes whatever passes the allowlist.\\n- **No output parsing** \u2014 raw stdout/stderr are returned as strings. Parsers live in the host-side `tools/parsers_*.py` modules.\\n- **No tool orchestration** \u2014 the agent doesn't chain tools, decide what to run next, or maintain engagement state. It's a subprocess executor with authentication.\\n- **No filesystem persistence** \u2014 results are sent back over the socket. Lossless output storage happens on the host side.\\n- **No network access** \u2014 the agent shares the tool pod's network namespace, but never initiates connections itself. Only the tools it spawns make network calls.\\n\\n## Testing\\n\\nThe agent's code is tested in `tests/argicida_agent/`. Since the agent has no host-side dependencies, its unit tests are fast and isolated \u2014 they test `execute_command()` with real subprocesses (using `/bin/echo`, `/bin/false`, etc.) and `AgentServer` with real Unix sockets.\\n\\nIntegration testing happens at the pod level (`tests/pod_integration/`), where a real Podman pod runs the agent and the host-side `IpcClient` connects over the bind-mounted socket.\",\"other-src-argicida-common\":\"# Other \u2014 src-argicida_common\\n\\nNow I have all the source. Let me produce the documentation.\\n\\n# `argicida_common` \u2014 Shared Types for Argicida\\n\\n`argicida_common` is the type foundation shared by the two halves of Argicida's split architecture: the host-side Hermes plugin (`argicida`) and the container-side agent (`argicida_agent`). Its only dependency is `msgspec`, and it deliberately has no awareness of Podman, networking, databases, or any host/container runtime concerns.\\n\\nEvery struct in this package crosses either the Unix socket IPC boundary (serialized as MessagePack) or the database boundary (serialized as JSON). Changing a field name, type, or adding a required field is a breaking wire-format change that requires coordinated updates on both sides.\\n\\n```mermaid\\ngraph LR\\n    subgraph Host [\\\"Host (argicida)\\\"]\\n        Plugin[\\\"Hermes Plugin\\\"]\\n        Store[\\\"EngagementStore\\\"]\\n    end\\n    subgraph Container [\\\"Container (argicida_agent)\\\"]\\n        Agent[\\\"IPC Agent\\\"]\\n    end\\n    Common[\\\"argicida_common\\\"]\\n\\n    Plugin -- \\\"IpcCommand / IpcResponse\\\" --&gt; Agent\\n    Plugin -- \\\"Finding / Asset / Credential\\\" --&gt; Store\\n    Agent -- \\\"IpcResponse\\\" --&gt; Plugin\\n    Common -. \\\"types used by\\\" .-&gt; Plugin\\n    Common -. \\\"types used by\\\" .-&gt; Agent\\n    Common -. \\\"types used by\\\" .-&gt; Store\\n```\\n\\n## Struct Conventions\\n\\nAll msgspec Structs enforce two invariants project-wide:\\n\\n- **`frozen=True`** \u2014 instances are immutable after creation.\\n- **`forbid_unknown_fields=True`** \u2014 deserialization rejects payloads with unexpected keys, catching version skew immediately instead of silently dropping data.\\n\\nCollection fields use `tuple[str, ...]` rather than `list` for true immutability. Enum-like fields use `Literal` types so validation happens at decode time, not via runtime checks.\\n\\nDirect construction (`MyStruct(field=value)`) bypasses msgspec validation. In production paths, always decode from wire format via `msgspec.msgpack.decode()` or `msgspec.json.decode()`.\\n\\n## Module Reference\\n\\n### `constants.py` \u2014 Shared Configuration\\n\\nThree categories of constants, all consumed by both host and agent:\\n\\n**Enums** \u2014 `TriageLevel`, `EngagementPhase`, and `CoverageState` are `StrEnum` types used as discriminators throughout the codebase. Use these instead of string literals.\\n\\n| Enum | Values | Used For |\\n|------|--------|----------|\\n| `TriageLevel` | `critical`, `interesting`, `noise` | Finding triage classification |\\n| `EngagementPhase` | `scoping` \u2192 `discovery` \u2192 `enumeration` \u2192 `deep_testing` \u2192 `human_gate` | Engagement lifecycle state machine |\\n| `CoverageState` | `discovered` \u2192 `ports_scanned` \u2192 `hostnames_probed` \u2192 `vgroups_identified` \u2192 `enumerated` | Per-asset enumeration completeness |\\n\\n**`TOOL_NAMES`** \u2014 maps logical tool names (e.g., `\\\"nmap\\\"`) to absolute container paths (e.g., `\\\"/usr/bin/nmap\\\"`). The agent resolves commands through this map; the host uses the keys for tool registration and scope profiles.\\n\\n**Size and security limits** \u2014 bounds enforced via `msgspec.Meta` annotations on struct fields:\\n\\n| Constant | Value | Guards |\\n|----------|-------|--------|\\n| `MAX_IPC_MESSAGE_SIZE` | 64 MiB | Frame-level size limit |\\n| `MAX_IPC_STDOUT_SIZE` | 16 MiB | `IpcResponse.stdout` |\\n| `MAX_IPC_STDERR_SIZE` | 1 MiB | `IpcResponse.stderr` |\\n| `MAX_ARGS_COUNT` | 100 | `IpcCommand.args` tuple length |\\n| `IPC_HMAC_KEY_SIZE` | 32 bytes | Session key width |\\n| `AUTH_TIMEOUT_S` | 10s | Handshake deadline |\\n\\n**`ENV_ALLOWLIST` / `ENV_BLOCKLIST`** \u2014 environment variable filtering for IPC commands. Only allowlisted variables (API keys for Shodan, Censys, etc.) pass through to the container. Blocklisted variables (`LD_PRELOAD`, `PATH`, proxy vars) are rejected regardless.\\n\\n### `ipc_schema.py` \u2014 Wire Types\\n\\nThe five structs that travel over the Unix socket:\\n\\n**Command/response pair:**\\n\\n- `IpcCommand` \u2014 tool name, args tuple, request ID, optional env vars and timeout. Fields `sequence`, `timestamp`, and `hmac_tag` are populated by the protocol layer, not by callers.\\n- `IpcResponse` \u2014 exit code, stdout, stderr, optional error message. Same auth fields.\\n\\n**Authentication handshake** (unauthenticated, no HMAC):\\n\\n- `IpcAuthChallenge` \u2014 server sends a random nonce.\\n- `IpcAuthResponse` \u2014 client returns HMAC(nonce, shared_key).\\n- `IpcAuthResult` \u2014 server sends `accepted: bool`.\\n\\n`validate_ipc_command_env()` checks environment variable tuples against the allowlist/blocklist before dispatch. Returns a tuple of error strings (empty = valid).\\n\\n### `ipc_protocol.py` \u2014 Framing and Authentication\\n\\nThe wire protocol implementation shared by both the host-side `IpcClient` and the container-side `IpcServer`.\\n\\n**Framing** \u2014 length-prefixed binary frames. A 4-byte big-endian header gives the payload size, followed by that many bytes of MessagePack-encoded struct data. `write_frame()` and `read_frame()` handle the framing; both enforce `MAX_IPC_MESSAGE_SIZE`.\\n\\n**HMAC authentication** \u2014 every `IpcCommand` and `IpcResponse` carries three auth fields: a monotonic `sequence` number, an ISO 8601 `timestamp`, and an `hmac_tag` (SHA-256). The protocol:\\n\\n1. Compute the body by encoding the struct with `hmac_tag=b\\\"\\\"`.\\n2. HMAC the concatenation of `sequence` (8 bytes big-endian) + `len(timestamp)` (4 bytes) + `timestamp` (UTF-8) + `body`.\\n3. Replace `hmac_tag` with the computed tag and encode the final message.\\n\\nOn receipt, `recv_message()` verifies the tag (constant-time via `hmac.compare_digest`), checks sequence number monotonicity, and rejects messages older than 30 seconds. This prevents replay attacks even if an attacker can observe the socket.\\n\\n`send_message()` and `recv_message()` are generic over any `msgspec.Struct` that has the auth fields. They are `async` \u2014 both sides use `asyncio` streams.\\n\\n### `scope_schema.py` \u2014 Engagement Boundaries\\n\\n`ScopeDefinition` captures what the penetration test is allowed to touch:\\n\\n- `target_cidrs` / `target_domains` \u2014 what's in scope.\\n- `excluded_ips` \u2014 carved-out exceptions.\\n- `max_intensity` \u2014 operator-set aggressiveness limit.\\n- Nuclei-specific filters: `template_allowlist`, `nuclei_tags`, `nuclei_severity`, `nuclei_exclude_tags` (defaults to `(\\\"dos\\\",)`).\\n- `dns_server` \u2014 custom resolver forwarded to dnsmasq.\\n\\n`ScopeDefinition` is host-only. It drives both nftables rule generation (network-layer enforcement on the gateway) and `pre_tool_call` hook validation (command-layer enforcement). It is never serialized to the container.\\n\\nHelper functions:\\n\\n- `classify_scope_args(args)` \u2014 splits a list of scope arguments into CIDRs (via `ipaddress.ip_network`) and domain names. Used by the `scope_define` tool handler.\\n- `validate_scope(scope)` \u2014 validates all CIDRs, IPs, and domains in a `ScopeDefinition`. Returns error tuples.\\n\\n### `finding_schema.py` \u2014 Vulnerability Findings\\n\\n`Finding` represents a single vulnerability or observation extracted from tool output. Fields align with OCSF where possible:\\n\\n- Identity: `tool`, `target`, `timestamp`\\n- Severity: `severity` (5-level Literal), `triage_level` (3-level from `TriageLevel`)\\n- Vulnerability metadata: `cve_ids`, `cwe_ids`, `cvss`, `epss`, `kev` (Known Exploited Vulnerability flag), `wasc_ids`\\n- Network context: `port`, `protocol`, `service`\\n- Web context: `attack`, `param`, `evidence`\\n- Remediation: `solution`, `reference`\\n- `raw_output` \u2014 full tool output preserved for reanalysis (capped at `MAX_RAW_OUTPUT_SIZE`)\\n\\nFindings are created by tool-specific parsers (`tools/parsers_*.py`) and stored in the `FindingRepo`.\\n\\n### `engagement_schema.py` \u2014 Engagement Metadata\\n\\n`EngagementMeta` is the top-level engagement record: `id`, `name`, `scope` (embedded `ScopeDefinition`), `current_phase` (constrained to the `EngagementPhase` values), and timestamps. Used for LLM context injection and engagement queries.\\n\\n### `asset_schema.py` \u2014 Asset Registry Types\\n\\nThe IP-anchored asset model:\\n\\n- `Asset` \u2014 top-level entity keyed by IP address. Tracks `coverage_state` (from `CoverageState`), associated `hostnames`, `ports`, and optional OS fingerprint.\\n- `AssetHostname` \u2014 a hostname associated with an IP, with `source` tracking which tool discovered it. Optional fields for vhost grouping, visual hashing, tech fingerprinting, and login URL detection (populated by ZAP intel).\\n- `AssetPort` \u2014 port/protocol/service/banner tuple.\\n- `VhostFingerprint` \u2014 structural metadata for virtual host deduplication (status code, content length, title hash, body hash, server header, redirect target).\\n\\n`serialize_fingerprint()` produces deterministic JSON for fingerprint comparison.\\n\\n### `credential_schema.py` \u2014 Credential Lifecycle\\n\\nThree structs model the credential discovery lifecycle from fragments to proven access:\\n\\n1. **`Principal`** \u2014 a username or identity found alone (e.g., from user enumeration). Carries `confidence` level: `confirmed`, `likely`, or `inferred`.\\n2. **`Secret`** \u2014 authentication material found alone. `secret_type` discriminates between `password`, `token`, `api_key`, `private_key`, `hash`, and `passphrase`. Optional `associated_principal` links to a paired identity.\\n3. **`ValidatedCredential`** \u2014 a proven `(target, service, username, secret)` tuple that successfully authenticated. Adds `access_level` (admin/user/read-only/unknown) and `is_default` flag.\\n\\n`Credential` is a deprecated alias for `ValidatedCredential`. The `EMPTY_CREDS` sentinel avoids allocating empty tuples repeatedly.\\n\\n## Adding New Types\\n\\n1. Create the struct in the appropriate `*_schema.py` file (or a new one if it represents a new domain).\\n2. Use `frozen=True, forbid_unknown_fields=True`. Use `tuple` for collections, `Literal` for constrained strings.\\n3. Add size-limit constants to `constants.py` and apply them via `msgspec.Meta` annotations.\\n4. If the struct crosses the IPC boundary, it needs `sequence`, `timestamp`, and `hmac_tag` fields \u2014 the protocol layer populates these automatically.\\n5. Never add runtime dependencies beyond `msgspec`. This package must remain installable in the minimal container environment.\",\"other-src-argicida\":\"# Other \u2014 src-argicida\\n\\n# `argicida` \u2014 Host-Side Hermes Plugin Package\\n\\n## Overview\\n\\n`argicida` is the primary workspace package in the Argicida monorepo. It contains the host-side Hermes plugin that registers security tools, manages Podman pod lifecycles, enforces scope, and coordinates IPC with the in-container agent. This document covers the package metadata and build configuration \u2014 the runtime code lives under `argicida/` (the inner package directory).\\n\\n## Entry Point\\n\\n```toml\\n[project.entry-points.\\\"hermes_agent.plugins\\\"]\\nargicida = \\\"argicida\\\"\\n```\\n\\nHermes discovers Argicida through the `hermes_agent.plugins` entry point group. At startup, Hermes calls `ep.load()` on this entry point, which imports the `argicida` module, then looks for a `register(ctx: PluginContext)` function on the returned object. The entry point value is the **module name** (`\\\"argicida\\\"`), not a callable reference \u2014 Hermes resolves `register()` itself.\\n\\n## Dependencies\\n\\n| Dependency | Purpose |\\n|---|---|\\n| `argicida-common` | Shared msgspec Structs for IPC messages, finding schemas, scope definitions (workspace source) |\\n| `podman` | Podman API client for pod/container lifecycle management |\\n| `python-libnmap` | Nmap XML output parsing for the nmap finding extractor |\\n| `msgspec` | High-performance serialization for Unix socket IPC (msgpack) |\\n| `pydantic` | Validation for engagement configuration, scope definitions, credentials |\\n| `cryptography` | Evidence chain signing (Ed25519) and key management |\\n| `httpx` | HTTP client for ZAP REST API communication |\\n| `apsw-sqlite3mc` | SQLite with encryption support for the engagement database |\\n\\n### Optional Dependencies\\n\\n```toml\\n[project.optional-dependencies]\\nvision = [\\\"Pillow&gt;=10.0\\\"]\\n```\\n\\nThe `vision` extra enables screenshot analysis in the httpx extractor. When installed, `_analyze_screenshot()` can downscale captured PNGs and send them to the LLM for structured visual extraction (app type, visible tech, login forms). The feature degrades gracefully when Pillow is absent.\\n\\nInstall with: `uv sync --extra vision`\\n\\n## `py.typed` Marker\\n\\nThe empty `py.typed` file (PEP 561) signals to type checkers that this package ships inline type annotations. This enables mypy to check downstream code that imports from `argicida` without requiring stubs.\\n\\n## Workspace Layout\\n\\nThis package uses the nested source layout required by the monorepo structure:\\n\\n```\\nsrc/argicida/\\n\u251c\u2500\u2500 pyproject.toml      # Package metadata (this file)\\n\u251c\u2500\u2500 argicida/           # Python package (inner directory)\\n\u2502   \u251c\u2500\u2500 __init__.py     # register() entry point\\n\u2502   \u251c\u2500\u2500 py.typed        # PEP 561 marker\\n\u2502   \u251c\u2500\u2500 registration/   # Phase modules for plugin setup\\n\u2502   \u251c\u2500\u2500 tools/          # Tool handlers, schemas, normalizers, parsers\\n\u2502   \u251c\u2500\u2500 pod/            # Pod lifecycle, session management\\n\u2502   \u251c\u2500\u2500 scope/          # Scope guard, nftables rule generation\\n\u2502   \u251c\u2500\u2500 hooks/          # Hermes hook implementations\\n\u2502   \u251c\u2500\u2500 ipc/            # Unix socket client\\n\u2502   \u251c\u2500\u2500 store/          # Engagement database (APSW)\\n\u2502   \u2514\u2500\u2500 zap/            # ZAP sidecar integration\\n\u2514\u2500\u2500 (no __init__.py)    # Outer dir must NOT have __init__.py\\n```\\n\\nThe outer `src/argicida/` holds `pyproject.toml`. The inner `argicida/` is the importable Python package. Placing an `__init__.py` in the outer directory creates dual-path module resolution that breaks mypy.\\n\\n## Build System\\n\\nBuilt with Hatchling. The `packages = [\\\"argicida\\\"]` directive tells the wheel builder to include only the inner package directory, excluding `pyproject.toml` and any non-package files from the outer directory.\\n\\nThe `argicida-common` dependency uses a workspace source reference (`{ workspace = true }`), resolved by uv during development. In a built wheel, this becomes a standard PyPI dependency.\",\"other-tests-argicida-agent\":\"# Other \u2014 tests-argicida_agent\\n\\n# `argicida_agent` Test Suite\\n\\nTests for the container-side agent \u2014 the thin process that lives inside the Podman tool pod, receives commands over a Unix socket, and executes scan tools via `create_subprocess_exec`. These tests run without Podman or containers; they exercise the agent's Python modules directly.\\n\\n## What the agent does\\n\\nThe agent has three responsibilities:\\n\\n1. **Bootstrap** (`__main__`) \u2014 parse CLI args, read the HMAC session key from the environment, resolve the allowed tool whitelist, start the server.\\n2. **IPC** (`ipc.server`) \u2014 listen on a Unix socket, authenticate clients via HMAC challenge-response, receive `IpcCommand` messages, dispatch them, return `IpcResponse` messages.\\n3. **Execution** (`executor.runner`) \u2014 run a tool binary as a subprocess, capture stdout/stderr, enforce timeouts, filter environment variables, truncate oversized output.\\n\\n```mermaid\\ngraph LR\\n    A[Host IpcClient] --&gt;|Unix socket| B[AgentServer]\\n    B --&gt;|HMAC handshake| A\\n    B --&gt;|dispatch| C[execute_command]\\n    C --&gt;|create_subprocess_exec| D[Tool binary]\\n    D --&gt;|stdout/stderr| C\\n    C --&gt;|IpcResponse| B\\n```\\n\\n## Test files\\n\\n### `test_runner.py` \u2014 Command execution\\n\\nTests `execute_command()` in isolation (no socket, no auth). Each test constructs an `IpcCommand` and asserts properties of the returned `IpcResponse`.\\n\\n**`TestExecuteCommand`** covers the core contract:\\n\\n| Test | Asserts |\\n|------|---------|\\n| `test_runs_simple_command` | `echo hello world` returns exit 0, stdout contains output, `request_id` round-trips |\\n| `test_captures_stderr` | Failed `ls` on a nonexistent path populates `stderr` |\\n| `test_rejects_unknown_tool` | `rm` not in `allowed_tools` \u2192 error with \\\"not allowed\\\", exit -1 |\\n| `test_timeout_returns_error` | `sleep 60` with `timeout_s=1` \u2192 error with \\\"timeout\\\", exit -1 |\\n| `test_rejects_blocked_env_variable` | `LD_PRELOAD` in `env` \u2192 error with \\\"Blocked\\\" |\\n| `test_passes_allowed_env` | `LANG=en_US.UTF-8` appears in `env` output |\\n| `test_env_merges_with_system_env` | Custom `LANG` coexists with inherited `PATH` |\\n| `test_truncates_large_stdout` | Output exceeding `MAX_IPC_STDOUT_SIZE` is truncated with an error note |\\n\\n**`TestTruncationByteConsistency`** is a focused suite for multi-byte truncation edge cases. All tests patch `MAX_IPC_STDOUT_SIZE` or `MAX_IPC_STDERR_SIZE` to 10 bytes:\\n\\n- Multi-byte characters (`\u20ac` = 3 UTF-8 bytes) are truncated at byte boundaries without exceeding the limit.\\n- Splitting mid-character produces `U+FFFD` replacement, not a crash or invalid UTF-8.\\n- stderr truncation follows the same byte-safe logic as stdout.\\n\\nThese tests exist because the IPC channel has a hard byte budget \u2014 the host side allocates fixed buffers, and a truncated multi-byte sequence would corrupt downstream msgpack decoding.\\n\\n### `test_server.py` \u2014 IPC server\\n\\nTests `AgentServer` end-to-end: starts a real Unix socket server, connects a client, performs the HMAC handshake, sends commands, and reads responses. Uses the actual `ipc_protocol` framing functions (`read_frame`, `write_frame`, `send_message`, `recv_message`).\\n\\nThe `_do_handshake` helper implements the client side of authentication:\\n\\n1. Read `IpcAuthChallenge` frame (server-generated nonce).\\n2. Compute `compute_hmac(session_key, sequence=0, request_id=\\\"\\\", nonce)`.\\n3. Send `IpcAuthResponse` with the digest.\\n4. Read `IpcAuthResult` \u2014 raise `ProtocolError` if rejected.\\n\\n**`TestAgentServerAuth`**:\\n- `test_accepts_valid_handshake` \u2014 correct HMAC completes without error.\\n- `test_rejects_invalid_handshake` \u2014 zeroed-out digest gets `accepted=False`, server closes the connection (verified by reading EOF).\\n\\n**`TestAgentServerCommandExecution`**:\\n- `test_executes_command_and_returns_response` \u2014 `echo hello` through the full socket path returns exit 0 with correct stdout and `request_id`.\\n- `test_rejects_disallowed_tool` \u2014 `rm -rf /` sent over an authenticated connection returns an error, not an execution.\\n- `test_multiple_sequential_commands` \u2014 three commands on one connection with incrementing sequence numbers all succeed, verifying the server handles connection reuse correctly.\\n\\n**`TestAgentServerSocketSecurity`**:\\n- `test_socket_file_permissions` \u2014 socket is `0o777`. This is intentional: the host process and container agent run in different UID namespaces (host UID 1000 maps to container UID 0, agent runs as UID 10000), so the socket must be world-accessible for cross-namespace IPC.\\n- `test_rejects_symlink_socket_path` \u2014 `AgentServer.start()` raises `RuntimeError` if the socket path is a symlink, preventing symlink-following attacks in the shared IPC directory.\\n\\n### `test_main.py` \u2014 Entry point\\n\\nTests for the `__main__` module's bootstrap functions.\\n\\n**`TestParseArgs`**: Verifies `parse_args()` defaults the socket path to `/ipc/agent.sock` and accepts `--socket` overrides.\\n\\n**`TestGetSessionKey`**: Validates `get_session_key()` reads `ARGICIDA_SESSION_KEY` from the environment and enforces:\\n- Presence (missing \u2192 `SystemExit`)\\n- Valid hex encoding (garbage \u2192 `SystemExit`)\\n- Correct length matching `IPC_HMAC_KEY_SIZE` (short key \u2192 `SystemExit`)\\n\\n**`TestGetAllowedTools`**: Confirms `get_allowed_tools()` returns the canonical `TOOL_NAMES` dict from `argicida_common.constants` \u2014 the single source of truth for which binaries the agent will execute.\\n\\n## Shared dependencies\\n\\nAll three test files depend on types from `argicida_common`:\\n\\n- **`ipc_schema`** \u2014 `IpcCommand`, `IpcResponse`, `IpcAuthChallenge`, `IpcAuthResponse`, `IpcAuthResult` (msgspec Structs for the wire protocol)\\n- **`ipc_protocol`** \u2014 `read_frame`, `write_frame`, `send_message`, `recv_message`, `compute_hmac` (framing and authentication primitives)\\n- **`constants`** \u2014 `IPC_HMAC_KEY_SIZE`, `TOOL_NAMES`\\n\\nThe server tests use real protocol functions rather than mocks \u2014 they test the actual wire format and HMAC computation, catching serialization regressions that mocked tests would miss.\\n\\n## Running\\n\\n```bash\\nuv run pytest tests/argicida_agent/              # all agent tests\\nuv run pytest tests/argicida_agent/executor/      # executor only\\nuv run pytest tests/argicida_agent/ipc/           # IPC server only\\nuv run pytest tests/argicida_agent/test_main.py   # entry point only\\n```\\n\\nNo markers, no infrastructure requirements. These are fast unit tests that run in the default `uv run pytest` invocation.\",\"other-tests-argicida-common\":\"# Other \u2014 tests-argicida_common\\n\\n# `argicida_common` Test Suite\\n\\nTests for the shared types package (`src/argicida_common/`) \u2014 the thin dependency layer between the host-side Hermes plugin and the container-side agent. Everything in `argicida_common` is a msgspec Struct or a validation function; the tests enforce schema contracts, serialization round-trips, and security invariants that both sides of the IPC boundary depend on.\\n\\n## What These Tests Protect\\n\\n`argicida_common` is the only package imported by both `argicida` (host) and `argicida_agent` (container). A broken schema here breaks the wire protocol. The tests verify three properties across every struct:\\n\\n1. **Round-trip fidelity** \u2014 encode \u2192 decode preserves all fields, including defaults\\n2. **Reject invalid input** \u2014 `forbid_unknown_fields=True` and `Literal` constraints catch malformed data at decode time, not at use time\\n3. **Immutability** \u2014 `frozen=True` structs raise `AttributeError` on mutation\\n\\n## Test Modules\\n\\n### `test_scope_schema.py`\\n\\nCovers `ScopeDefinition` (the engagement's target boundary) and `validate_scope()` (the semantic validator that runs after deserialization).\\n\\n`ScopeDefinition` fields tested: `target_cidrs`, `target_domains`, `excluded_ips`, `max_intensity`, `dns_server`, `nuclei_severity`, `nuclei_tags`, `nuclei_exclude_tags`. The struct carries the entire scope definition through msgpack over IPC \u2014 it's embedded in `EngagementMeta` and drives both the nftables ruleset generator and the command-layer scope guard.\\n\\n`validate_scope()` returns a tuple of error strings (not exceptions) for:\\n\\n- Invalid CIDR notation (IPv4 and IPv6)\\n- Invalid excluded IPs\\n- Domain injection attempts (semicolons, newlines, empty strings, wildcards)\\n- Invalid DNS server addresses\\n\\nThe empty-scope-is-valid test is intentional \u2014 diagnostic pods launch without a scope and get permissive gateway rules until `scope_define` runs.\\n\\n### `test_ipc_schema.py`\\n\\nCovers the wire-format structs: `IpcCommand`, `IpcResponse`, `IpcAuthChallenge`, `IpcAuthResponse`.\\n\\nKey contract points:\\n\\n- `IpcCommand.timeout_s` has msgspec-level bounds: rejects 0 and values above 3600. The 300s default is the common case; tools like nmap override to 600.\\n- `IpcCommand.env` is validated by `validate_ipc_command_env()`, which maintains a two-tier filter: an explicit blocklist (`LD_PRELOAD`, `LD_LIBRARY_PATH`, etc.) and a positive allowlist (`LANG`, `TZ`, API key variables). Anything not in the allowlist is rejected.\\n- Auth fields (`sequence`, `timestamp`, `hmac_tag`) default to zero-values for backward compatibility but are populated by `send_message()` before transmission.\\n\\n### `test_ipc_protocol.py`\\n\\nCovers the framing layer (`read_frame`/`write_frame`) and the authenticated message layer (`send_message`/`recv_message`/`compute_hmac`/`verify_hmac`).\\n\\n```mermaid\\ngraph LR\\n    A[send_message] --&gt; B[compute_hmac]\\n    A --&gt; C[write_frame]\\n    C --&gt; D[\\\"4-byte big-endian length + payload\\\"]\\n    E[recv_message] --&gt; F[read_frame]\\n    E --&gt; G[verify_hmac]\\n    G --&gt;|fail| H[ProtocolError]\\n```\\n\\n**Framing tests** (`TestWriteFrame`, `TestReadFrame`): The wire format is a 4-byte big-endian length prefix followed by the payload. Tests verify:\\n- Oversized payloads rejected at write time (monkeypatched `MAX_IPC_MESSAGE_SIZE`)\\n- Oversized frames rejected at read time (128 MiB &gt; 64 MiB limit)\\n- Zero-length frames rejected\\n- Incomplete headers (EOF mid-read) raise `ProtocolError`\\n\\n**HMAC tests** (`TestHmac`): HMAC-SHA256 keyed authentication binds the key, sequence number, timestamp, and body together. Tests cover:\\n- Valid tag acceptance\\n- Tampered body rejection\\n- Wrong sequence rejection\\n- Wrong key rejection\\n- Different sequences producing different tags (no sequence-independent MAC)\\n\\n**Message-level tests** (`TestSendRecvMessage`, `TestSendMessageFailClose`): `send_message` populates `sequence`, `timestamp`, and `hmac_tag` on the struct before encoding. `recv_message` verifies the HMAC and sequence match. Tests cover:\\n- `IpcCommand` and `IpcResponse` round-trips through the full send/recv path\\n- Sequence mismatch detection (replay protection)\\n- HMAC mismatch detection (tampering protection)\\n- Messages without `hmac_tag` attribute rejected before sending (fail-closed \u2014 prevents accidentally sending unauthenticated `IpcAuthChallenge` through the authenticated channel)\\n\\nThe test infrastructure uses `_LoopbackPipe` (feeds data into an `asyncio.StreamReader`) for read-side tests and `_BufferTransport` with `_make_writer_pair()` for write-then-read round-trips. `flush_to_reader()` bridges the write buffer to the reader, simulating the transport layer without actual sockets.\\n\\n### `test_asset_schema.py`\\n\\nCovers `Asset`, `AssetHostname`, `AssetPort`, `VhostFingerprint`, and `serialize_fingerprint`.\\n\\n`AssetHostname.source` is a `Literal` type restricting to known tool sources: `dns`, `nmap`, `httpx`, `subfinder`, `naabu`, `nuclei`, `manual`, `scope`. The test for each accepted source matters \u2014 when a new extractor is added, its source string must be added to the Literal and a test written.\\n\\n`serialize_fingerprint()` is tested for determinism \u2014 identical fingerprints must produce identical serialized output so vhost grouping comparisons are stable.\\n\\n`Asset` nests `AssetHostname` and `AssetPort` tuples. The defaults test verifies that an asset created with only `id`, `ip`, and `coverage_state` has empty tuples (not `None`) for collection fields.\\n\\n### `test_finding_schema.py`\\n\\nCovers `Finding`, including the extended fields added for ZAP integration.\\n\\nThree generations of the schema are tested:\\n\\n1. **Core fields**: `tool`, `target`, `severity`, `title`, `description`, `raw_output`, `triage_level`, `timestamp`, plus optional `port`, `protocol`, `cvss`, `cve_ids`, `epss`, `kev`\\n2. **Service field**: Added for per-service finding correlation. Tests verify it defaults to `None` and round-trips through msgpack.\\n3. **Extended fields**: `evidence`, `attack`, `param`, `solution`, `reference`, `wasc_ids` \u2014 added for ZAP alert data. Backward compatibility is explicitly tested: data encoded before the extension decodes with `None`/`()` defaults.\\n\\n`severity` is constrained to a Literal set; `triage_level` maps to the `TriageLevel` enum values. Both are validated at decode time.\\n\\n### `test_credential_schema.py`\\n\\nCovers the credential fragment model: `Principal`, `Secret`, `ValidatedCredential`, and the `Credential` type alias.\\n\\n`Principal` represents a discovered identity (username, email). `Secret` represents a discovered secret (password, hash, token). `ValidatedCredential` is the confirmed combination after successful authentication. Each is `frozen=True` with `forbid_unknown_fields=True`.\\n\\nKey constraints tested:\\n- `Principal.confidence` restricted to Literal values (rejects `\\\"certain\\\"`)\\n- `Secret.secret_type` restricted to Literal values (rejects `\\\"certificate\\\"`)\\n- `Credential` is a type alias for `ValidatedCredential` \u2014 the `TestCredentialAlias` class verifies `Credential is ValidatedCredential` and that instances are interchangeable\\n\\n### `test_engagement_schema.py`\\n\\nCovers `EngagementMeta` \u2014 the top-level struct containing engagement ID, nested `ScopeDefinition`, phase, and timestamps. Tests verify msgpack round-trips including nested scope with IPv6 CIDRs.\\n\\n### `test_constants.py`\\n\\nCovers `TriageLevel`, `EngagementPhase`, and `TOOL_NAMES`.\\n\\n`EngagementPhase` ordering is load-bearing \u2014 the progression `scoping \u2192 discovery \u2192 enumeration \u2192 deep_testing \u2192 human_gate` is enforced by testing `list(EngagementPhase)`. The test also explicitly verifies that retired phase values (`passive_recon`, `active_recon`, `interactive`) are absent.\\n\\n`TOOL_NAMES` is checked for the five core scan tools. This catches accidental removal during tool registry refactors.\\n\\n## Adding Tests for New Schemas\\n\\nWhen adding a new msgspec Struct to `argicida_common`:\\n\\n1. Add a test file `tests/argicida_common/test_.py`\\n2. Test msgpack (or JSON) round-trip with all fields populated\\n3. Test defaults \u2014 construct with only required fields, assert all optional fields\\n4. Test `frozen=True` \u2014 attempt mutation, expect `AttributeError`\\n5. Test `forbid_unknown_fields=True` \u2014 decode with an extra field, expect `ValidationError`\\n6. Test every `Literal`-constrained field with an invalid value\\n7. If the struct crosses the IPC boundary, test backward compatibility: encode without new fields, decode with the new schema, verify defaults apply\",\"other-tests-argicida\":\"# Other \u2014 tests-argicida\\n\\n# `tests/argicida/` \u2014 Host Plugin Test Suite\\n\\nUnit and integration tests for the `argicida` host-side Hermes plugin. These tests run without infrastructure (no Podman, no IPC, no LLM endpoint) and validate the plugin's internal logic: database persistence, CLI commands, slash commands, briefing generation, hooks, scope enforcement, tool handlers, and pod configuration.\\n\\nRun with:\\n\\n```bash\\nuv run pytest tests/argicida/    # all host plugin tests\\nuv run pytest tests/argicida/db/ # just database layer\\nuv run pytest -k \\\"test_dedup\\\"    # by name pattern\\n```\\n\\n## Session-Scoped Bootstrap (`conftest.py`)\\n\\nThe root `conftest.py` performs one-time initialization that mirrors the plugin's `register()` startup sequence. Two session-scoped autouse fixtures populate global registries from discovered tool definitions:\\n\\n- **`_skip_readiness_waits`** \u2014 Patches out `_wait_for_agent_socket` and `_wait_for_gateway` so tests never block on IPC or gateway readiness.\\n- **`_populate_tool_profiles`** \u2014 Calls `discover_tool_defs()` and feeds the results into `register_profiles()`, `register_tool_aliases()`, `register_normalizer_aliases()`, `register_error_hints()`, and `register_auth_indicators()`. This is the same data path the real plugin uses, so scope validation, normalizer alias resolution, and error hint lookup all work correctly in tests.\\n\\nCommon per-test fixtures:\\n\\n| Fixture | Scope | Provides |\\n|---|---|---|\\n| `store` | function | `EngagementStore` backed by a `tmp_path` SQLite file; auto-closed |\\n| `sample_scope` | session | `ScopeDefinition(target_cidrs=(\\\"10.10.10.0/24\\\",))` |\\n| `engagement_id` | function | A created engagement ID (depends on `store` + `sample_scope`) |\\n| `chain` | function | Empty `EvidenceChain` |\\n| `state` | function | Empty `PluginState` |\\n\\nThe `store` fixture yields a real `EngagementStore` with schema migrations applied \u2014 no mocking of the database layer.\\n\\n## Test Organization\\n\\nTests mirror the source layout under `src/argicida/argicida/`:\\n\\n```\\ntests/argicida/\\n\u251c\u2500\u2500 conftest.py              # session bootstrap + shared fixtures\\n\u251c\u2500\u2500 briefing/                # briefing generator + prompt templates\\n\u251c\u2500\u2500 commands/                # CLI dispatch, slash commands, update-data\\n\u251c\u2500\u2500 db/                      # persistence layer (store, repos, migrations)\\n\u251c\u2500\u2500 engagement/              # engagement session lifecycle\\n\u251c\u2500\u2500 hooks/                   # all 10+ hook behaviors\\n\u251c\u2500\u2500 pod/                     # pod config, manager, session, launcher\\n\u251c\u2500\u2500 scope/                   # scope validation, netfilter rules\\n\u251c\u2500\u2500 tools/                   # tool handlers, normalizers, parsers, extractors\\n\u2502   \u2514\u2500\u2500 defs/                # tool definition discovery + structural invariants\\n\u2514\u2500\u2500 test_register.py         # plugin registration wiring\\n```\\n\\n## Key Testing Patterns\\n\\n### Real Database, No ORM Mocks\\n\\nDatabase tests use real APSW connections against in-memory or `tmp_path` SQLite files. The `db/` tests set up their own `conn` and `repo` fixtures that apply `_SCHEMA_STATEMENTS` and `_migrate_schema()` directly, giving each test a clean schema. This catches real SQL bugs \u2014 constraint violations, migration ordering, column type mismatches \u2014 that mock-based tests would miss.\\n\\n```python\\n@pytest.fixture\\ndef conn(tmp_path: Path) -&gt; apsw.Connection:\\n    db = apsw.Connection(str(tmp_path / \\\"test.db\\\"))\\n    for stmt in _SCHEMA_STATEMENTS:\\n        db.execute(stmt)\\n    _migrate_schema(db)\\n    return db\\n```\\n\\n### Factory Functions\\n\\nEach test module defines local factory helpers (prefixed `_make_*` or `make_*`) that construct domain objects with sensible defaults. A shared `tests/factories.py` module provides cross-cutting factories like `make_finding()`. Factory functions accept `**overrides` so tests only specify fields relevant to the assertion:\\n\\n```python\\ncred = _make_credential(service=\\\"mysql\\\", port=3306, username=\\\"root\\\")\\n```\\n\\n### Security Property Verification\\n\\nTests explicitly verify security-sensitive behaviors:\\n\\n- **File permissions** \u2014 `0o600` for data files, `0o700` for directories (`test_file_permissions_are_0o600`, `test_directory_permissions_are_0o700`)\\n- **Symlink rejection** \u2014 `test_rejects_symlink` in encryption and VPN config\\n- **Atomic writes** \u2014 `test_atomic_write_no_partial_on_validation_failure` ensures a failed download doesn't corrupt existing data\\n- **Key length validation** \u2014 `test_rejects_wrong_length` for encryption keys\\n- **Encryption isolation** \u2014 `test_encrypted_db_unreadable_without_key` opens the raw file with APSW and expects `NotADBError`\\n\\n### Async Test Support\\n\\nAsync handlers (briefing generator, slash commands) use `async def test_*` methods. The project's pytest configuration handles the async test runner. Mocked LLM calls use `AsyncMock`:\\n\\n```python\\nmock_llm.acomplete = AsyncMock(return_value=mock_result)\\nresult = await generate_briefing(mock_llm, store, eid)\\n```\\n\\n## Module-by-Module Summary\\n\\n### `briefing/`\\n\\n**`test_generator.py`** \u2014 Tests `generate_briefing()` end-to-end with a mocked LLM. Validates: LLM summary appears in output, placeholder replacement works, findings are included, prompt stays small (&lt;500 chars), graceful fallback when LLM fails, error message for missing engagements.\\n\\n**`test_prompts.py`** \u2014 Tests the template layer without LLM involvement. Covers `SEVERITY_RANK` ordering, `SUMMARY_SYSTEM_MESSAGE` constraints (forbids fabrication, stays concise), `format_finding()` output formatting (CVEs, CWEs, protocol, CVSS, EPSS, KEV flag, evidence truncation), `render_briefing()` section structure (metadata, scope, finding counts, risk ranking sorted by severity with KEV priority, noise grouping by title with counts, tool coverage, asset discovery, next steps), and `build_summary_prompt()` size and content constraints.\\n\\n### `commands/`\\n\\n**`test_cli.py`** \u2014 Tests `dispatch_argicida_cli()` for each subcommand: `engagement list/resume/create`, `evidence verify`, `scope`, `findings` (with level filter), `pod start/stop/status`, `build-image` (tool/gateway/all/no-flags), `update-data`, and unknown command handling. Verifies that CLI commands use `EngagementSession.bind()` for proper lifecycle management.\\n\\n**`test_formatting.py`** \u2014 Tests `format_engagement_summary()` output: includes ID, phase, CIDRs; conditionally includes domains and exclusions; shows \\\"none\\\" for empty CIDRs.\\n\\n**`test_slash.py`** \u2014 Tests all six slash command factory functions. `/scope` shows scope or \\\"no active\\\" message. `/triage` shows finding counts. `/approve-gate` returns a message. `/briefing` is async, checks for engagement and LLM availability, delegates to `generate_briefing`. `/trace` validates usage, dispatches via `dispatch_tool`, manages pod lifecycle. `/engage` handles list/resume/status/rename subcommands with proper `EngagementSession` binding.\\n\\n**`test_update_data.py`** \u2014 Tests `download_epss_kev()`: downloads and writes both files, creates directories, enforces file/directory permissions, validates EPSS CSV headers, validates KEV JSON structure, ensures atomic writes preserve existing data on failure.\\n\\n### `db/`\\n\\n**`test_store.py`** \u2014 Core CRUD for `EngagementStore`: create/get/update/list engagements, scope validation (rejects invalid CIDRs), VPN config (direct/wireguard/openvpn with directory and symlink validation), findings with dedup (promotes triage level, never demotes), and evidence chain entries.\\n\\n**`test_asset_repo.py`** \u2014 `AssetRepo` persistence: upsert with UUID generation and dedup, IPv6 support, hostname management with coverage state reset, port upsert with service/banner updates, vhost fingerprinting and group computation (idempotent group UUIDs), coverage state machine (forward-only transitions), target resolution (IP and hostname lookup), and finding linkage.\\n\\n**`test_asset_tech.py`** \u2014 Tech fingerprint and login URL extensions on `AssetHostname`: schema defaults, storage/retrieval, overwrite behavior.\\n\\n**`test_candidate_engine.py`** \u2014 Credential candidate ranking across all six tiers: Tier 1 (same_row \u2014 associated principal+secret), Tier 2 (same_target), Tier 3 (same_service), Tier 5 (cross_service_reuse \u2014 validated credentials from other services), Tier 6 (spray \u2014 excluded by default, opt-in). Verifies ranking order (confirmed before inferred), limit enforcement, and exclusion of already-validated pairs.\\n\\n**`test_credentials.py`** \u2014 CRUD for the three credential types: `ValidatedCredential` (store, dedup, filter by target/service, bool handling, null port), `Principal` (store, dedup, target/service filters, null target), `Secret` (store, dedup, hash_type, associated_principal, target filter).\\n\\n**`test_credential_helpers.py`** \u2014 Internal helpers: `_fingerprint()` determinism and field sensitivity, `_build_where()` clause construction with optional target/service filters.\\n\\n**`test_encryption.py`** \u2014 `load_or_generate_db_key()`: key generation, idempotency, directory creation, file permissions, symlink rejection, wrong-length rejection, partial-write safety, retry-after-failure. `EngagementStore` with encryption: table creation, unreadable without key, readable with correct key, unencrypted fallback.\\n\\n**`test_finding_repo.py`** \u2014 `FindingRepo.add_finding()` transaction atomicity.\\n\\n**`test_helpers.py`** \u2014 `store_finding()` helper: stores in DB, appends to evidence chain when provided, skips chain when `None`.\\n\\n**`test_migrations.py`** \u2014 Verifies `last_bound_at` column exists after migration.\\n\\n## Relationship to Other Test Tiers\\n\\n```mermaid\\ngraph TD\\n    A[\\\"tests/argicida/Unit + Integration~2800 tests\\\"] --&gt;|no infra| B[Mocked IPC/Podman/LLM]\\n    C[\\\"tests/pod_integration/~139 tests\\\"] --&gt;|real pods| D[Podman + IPC + direct dispatch]\\n    E[\\\"tests/e2e/~10 tests\\\"] --&gt;|real everything| F[HermesCLI + LLM + ZAP]\\n\\n    A -.-|\\\"shares\\\"| G[tests/factories.py]\\n    C -.-|\\\"shares\\\"| G\\n```\\n\\nThe `tests/argicida/` suite is the fast feedback loop \u2014 it runs in seconds with no external dependencies. Pod integration tests (`-m pod_integration`) exercise real Podman pods and IPC but skip the agent loop. E2E tests (`-m e2e`) run through the full HermesCLI with a live LLM. All three tiers share factory functions from `tests/factories.py`.\\n\\n## Adding Tests\\n\\n1. **New tool handler** \u2014 Add tests under `tests/argicida/tools/`. The session-scoped `_populate_tool_profiles` fixture auto-discovers tool defs, so new tools get scope profiles registered automatically.\\n2. **New database table/column** \u2014 Add a migration test in `test_migrations.py` and repo tests in a new or existing `test_*_repo.py`. Use the `conn`/`repo`/`eid` fixture pattern.\\n3. **New slash command** \u2014 Add tests in `test_slash.py` using the factory-function pattern (`make_*_handler`). Test both the happy path and the no-engagement/no-LLM error paths.\\n4. **New hook** \u2014 Add tests under `tests/argicida/hooks/`. Call `pm.invoke_hook()` explicitly \u2014 hooks do not fire during `registry.dispatch()`.\",\"other-tests-e2e\":\"# Other \u2014 tests-e2e\\n\\n# E2E Test Suite (`tests/e2e/`)\\n\\nEnd-to-end tests that exercise Argicida through the real Hermes CLI execution path \u2014 real LLM, real Podman pods, real IPC, real tool dispatch. Nothing is mocked. These tests exist because unit and integration tests, no matter how thorough, cannot catch wiring failures between the CLI bootstrap, plugin discovery, agent loop, delegation, and pod lifecycle.\\n\\n## Prerequisites\\n\\n| Requirement | How to satisfy |\\n|---|---|\\n| Rootless Podman | Installed and running (`podman version`) |\\n| `argicida-tools:dev` | `podman build -f containers/Containerfile -t argicida-tools:dev .` |\\n| `argicida-gateway:dev` | `podman build -f containers/Containerfile.gateway -t argicida-gateway:dev .` |\\n| `argicida-zap:dev` | `podman build --network=private -f containers/Containerfile.zap -t argicida-zap:dev .` |\\n| `nginx:alpine` | `podman pull docker.io/library/nginx:alpine` |\\n| LLM endpoint | Configured in `profile/config.yaml` |\\n\\nRun with:\\n\\n```bash\\nuv run pytest -m e2e -v\\n```\\n\\nE2E tests are excluded from the default `pytest` run. The `e2e` marker and a 600-second timeout are applied to every test via `pytestmark`.\\n\\n## Architecture\\n\\nThe harness drives HermesCLI **in-process** \u2014 it calls the same `cli.chat()` and `cli._drain_process_notifications()` methods the interactive CLI uses. This is deliberate: a custom dispatch harness would test a different system than the one operators actually run.\\n\\n```mermaid\\ngraph LR\\n    Test[\\\"test_e2e_smoke\\\"] --&gt; Harness[\\\"E2ESystemHarness\\\"]\\n    Harness --&gt; CLI[\\\"HermesCLI.chat()\\\"]\\n    CLI --&gt; Agent[\\\"Agent Loop\\\"]\\n    Agent --&gt; Plugin[\\\"Argicida Plugin\\\"]\\n    Plugin --&gt; Pod[\\\"Podman Pod + Gateway\\\"]\\n    Pod --&gt; Tool[\\\"Tool Execution\\\"]\\n    Test --&gt; Assert[\\\"SessionAssertions\\\"]\\n    Assert --&gt; DB[\\\"Session DB\\\"]\\n    Test --&gt; FI[\\\"FaultInjector\\\"]\\n    FI --&gt; Pod\\n```\\n\\n## Components\\n\\n### `E2ESystemHarness` (`harness.py`)\\n\\nThe central test driver. `setup()` mirrors what `cli.main()` does before creating the interactive CLI:\\n\\n1. **Creates an ephemeral HERMES_HOME** under `tests/e2e/.runs//` \u2014 symlinks `config.yaml` and `SOUL.md` from the profile, copies skills, creates output/session/engagement directories.\\n2. **Sets environment** \u2014 `HERMES_HOME` and `ARGICIDA_NETWORK` point at the ephemeral home and the E2E-specific Podman network.\\n3. **Reloads CLI config** and **discovers plugins** via `hermes_cli.plugins.discover_plugins(force=True)`, which triggers Argicida's `register()`.\\n4. **Creates `HermesCLI`** in quiet mode with no agent attached (the agent is created lazily on first `chat()`).\\n\\n#### `run_session(prompt)`\\n\\nSends a prompt through `cli.chat()`, then enters a delegation drain loop. The loop mirrors the CLI's `process_loop`:\\n\\n- While delegated tasks are active (`active_count() &gt; 0`), it drains process notifications and feeds completions back as follow-up `chat()` turns via the `_pending_input` queue.\\n- When no active delegations remain, it waits two idle cycles (0.5s each) to catch late completions before returning.\\n- Hard deadline: 480 seconds.\\n\\nNo `time.sleep()` polling \u2014 the loop uses `queue.Queue.get(timeout=...)` condition-variable waits, identical to the production CLI.\\n\\n#### `finalize_session()`\\n\\nMirrors the CLI's session boundary: fires `on_session_finalize` (which triggers pod teardown, evidence signing, engagement unbind), sweeps orphan pods, ends the session in the DB, and creates a new session ID. Used to test lifecycle transitions within a single test.\\n\\n#### `_sweep_pods()`\\n\\nSafety net that removes all `argicida-*` pods after each session. Before removing, it execs into each tool container as root to clear and re-own the IPC directory \u2014 necessary because the `U`/`chown` bind mount shifts ownership to the container's UID namespace, preventing host-side cleanup. Delegates to `PodManager.cleanup_topology_by_name()` for full topology removal (pod + gateway + internal network).\\n\\n#### `teardown()`\\n\\nFull cleanup sequence: interrupts active delegations, closes the agent, fires `on_session_finalize`, deregisters all `argicida_*` tools, restores the plugin manager, closes database connections, restores the original working directory and environment variables, and sweeps pods. Every operation is wrapped in `contextlib.suppress(Exception)` \u2014 teardown must not raise.\\n\\n### `SessionAssertions` (`assertions.py`)\\n\\nBehavioral assertions that query the Hermes session database after a run completes. All queries span the parent session **and** its delegation child sessions, excluding compression children (identified by `end_reason = 'compression'`).\\n\\n**`get_tool_calls()`** \u2014 The core query. Scans the `messages` table for assistant messages with `tool_calls` (the request) and `role='tool'` messages (the result), then joins them by `tool_call_id`. Returns an ordered list of dicts with `tool_name`, `args`, `result`, `session_id`, and `timestamp`.\\n\\n**`get_tool_result(tool_name)`** \u2014 Parses the JSON result of the last call to a given tool. When the in-DB result is truncated by Hermes (detected by `[truncated]` or `[Truncated:` suffixes), falls back to reading the raw `stdout` file from the output store at `$HERMES_HOME/argicida/output///`. This fallback is essential \u2014 Hermes truncates large tool results to fit the LLM context window, but the raw output is always preserved on disk.\\n\\n**Assertion methods:**\\n\\n| Method | Checks |\\n|---|---|\\n| `assert_tool_was_called(name)` | At least one call exists |\\n| `assert_tool_not_called(name)` | Zero calls exist |\\n| `assert_any_argicida_tool_called()` | At least one `argicida_*` call |\\n| `assert_engagement_created()` | `argicida_engagement_create` was called |\\n| `assert_tool_succeeded(name)` | Called, result parses, no `\\\"error\\\"` key |\\n| `assert_findings_stored(min)` | Finding count in engagement DB |\\n| `assert_raw_output_exists(name)` | stdout file exists on disk |\\n\\n### `FaultInjector` (`fault_injector.py`)\\n\\nInjects faults into recoverable surfaces for resilience testing. Each fault method registers its cleanup inverse in a LIFO queue, unwound on `__exit__` or explicit `cleanup()`. Context manager protocol supported.\\n\\n| Fault | What it does | What it tests |\\n|---|---|---|\\n| `kill_pod()` | Removes entire topology via `cleanup_topology_by_name` | Pod re-creation on next tool call |\\n| `kill_gateway()` | Stops/removes only the gateway container | Fail-closed network isolation |\\n| `delete_socket()` | Removes `/ipc/agent.sock` from inside the container | IPC reconnection |\\n| `clear_pod_session()` | Sets `state.pod_session = None` without teardown | Stale state detection |\\n| `clear_engagement_session()` | Sets `state.engagement_session = None` without unbind | Engagement state recovery |\\n| `delete_ipc_dir()` | Removes the IPC directory from the host | IPC dir re-creation |\\n| `chmod_output_dir(mode)` | Makes the output directory unwritable | Graceful write failure handling |\\n\\n`kill_pod()` execs into the tool container as root to clear IPC contents before topology removal \u2014 same cleanup sequence as `PodSession.teardown()`.\\n\\n### Fixtures (`conftest.py`)\\n\\n| Fixture | Scope | Purpose |\\n|---|---|---|\\n| `_require_podman_and_images` | session | Skips the entire suite if Podman or any required image is missing |\\n| `_prune_runs` | session | Deletes `.runs/` entries older than 7 days |\\n| `e2e_network` | session | Creates and tears down a dedicated `argicida-e2e` Podman network |\\n| `nginx_target` | session | Runs an nginx container on the E2E network, yields its IP |\\n| `e2e_harness` | module | Creates, sets up, yields, and tears down the harness |\\n\\nThe `nginx_target` fixture mounts a minimal HTML page and inspects the container's network settings to extract the IP address, which becomes the scan target for tests.\\n\\n## The Smoke Test\\n\\n`TestE2ESmoke.test_full_lifecycle` is a single test method with five assertion phases, all running against one LLM session:\\n\\n1. **Dispatch** \u2014 Prompts the LLM to create an engagement and run `argicida_naabu`. Asserts the engagement was created, naabu succeeded, raw output was stored, and the evidence chain has entries.\\n\\n2. **Delegation** \u2014 Verifies child sessions were created in the session DB (the LLM delegates tool execution to subagents).\\n\\n3. **Finalization** \u2014 Calls `finalize_session()`, then asserts: pod is torn down (`state.pod_session is None`), session ID changed, old session has `ended_at` set, and the evidence chain is cryptographically signed.\\n\\n4. **Session resume** \u2014 Calls `resume_session()` with the old session ID. Asserts conversation history is reloaded and findings remain accessible.\\n\\n5. **Engage resume** \u2014 Finalizes again (new session boundary), then runs `/engage resume `. Asserts the same engagement is re-bound across the session boundary.\\n\\n## Writing New E2E Tests\\n\\nNew tests go in `tests/e2e/` with the `@pytest.mark.e2e` marker. Use the `e2e_harness` and `nginx_target` fixtures. The pattern:\\n\\n```python\\ndef test_something(self, e2e_harness, nginx_target):\\n    e2e_harness.run_session(\\\"prompt for the LLM\\\")\\n    assertions = e2e_harness.session_assertions()\\n    assertions.assert_tool_succeeded(\\\"argicida_\\\")\\n```\\n\\nFor fault injection tests, wrap faults in the context manager:\\n\\n```python\\nfrom tests.e2e.fault_injector import FaultInjector\\n\\nwith FaultInjector(e2e_harness) as fi:\\n    fi.kill_gateway()\\n    e2e_harness.run_session(\\\"try a scan\\\")\\n    # assert fail-closed behavior\\n```\\n\\nCall `e2e_harness.finalize_session()` between independent test phases within a single test method to get clean session boundaries. The harness handles pod sweep and state reset.\\n\\nTest run artifacts persist in `tests/e2e/.runs/` (auto-pruned after 7 days). Each run gets its own timestamped directory containing the full Hermes home \u2014 session DB, engagement DB, raw tool output, and logs. These are the primary debugging artifacts when a test fails.\",\"other-tests-integration\":\"# Other \u2014 tests-integration\\n\\n# Integration Tests (`tests/integration/`)\\n\\n## Purpose\\n\\nThis module validates that Argicida's components work together through real Hermes infrastructure \u2014 real `PluginManager`, real `tools.registry`, real `EngagementStore` \u2014 with only the pod and IPC layers mocked. It occupies the middle tier of a three-tier test architecture:\\n\\n| Tier | Infrastructure | What it proves |\\n|------|---------------|----------------|\\n| Unit (`tests/argicida/`) | Everything mocked | Individual functions behave correctly in isolation |\\n| **Integration** (`tests/integration/`) | **Real Hermes + real DB, mocked pod/IPC** | **Components wire together correctly** |\\n| E2E (`tests/e2e/`) | Real pods, real IPC, real LLM | The system works end-to-end |\\n\\nThe integration tier catches a class of bugs that unit tests cannot: registration wiring gaps, hook ordering violations, scope guard bypass paths, and state management errors that only surface when real closures and real registries are involved.\\n\\n## The Test Harness\\n\\nEverything runs through `ArgicidaTestHarness`, built by the `hermes_ctx` fixture in `conftest.py`. The fixture calls the real `register(ctx)` function \u2014 the same entry point Hermes uses in production \u2014 then captures what was registered.\\n\\n```mermaid\\ngraph TD\\n    F[hermes_ctx fixture] --&gt; PM[Real PluginManager]\\n    F --&gt; REG[\\\"Real tools.registry\\\"]\\n    F --&gt; DB[Real EngagementStoreon tmp_path SQLite]\\n    F --&gt; MOCK_POD[Mocked PodManager]\\n    F --&gt; MOCK_LLM[Mocked LLM client]\\n\\n    PM --&gt; HOOKS[Hook dispatchinvoke_hook]\\n    REG --&gt; TOOLS[Tool dispatchregistry.dispatch]\\n    PM --&gt; CMDS[Command dispatch_plugin_commands]\\n\\n    F --&gt; H[ArgicidaTestHarness]\\n    H --&gt; HOOKS\\n    H --&gt; TOOLS\\n    H --&gt; CMDS\\n```\\n\\nThe harness exposes three dispatch methods that mirror production call paths:\\n\\n- **`invoke_hook(hook_name, **kwargs)`** \u2014 Delegates to `PluginManager.invoke_hook()`. Hooks fire through the same fault-isolation mechanism as production: a crashed hook is logged and skipped, other hooks continue.\\n- **`dispatch_tool(name, args)`** \u2014 Calls `registry.dispatch()` on the real global tool registry. Handlers are the actual closures that `register()` created.\\n- **`dispatch_command(name, raw_args)`** \u2014 Looks up the command in `pm._plugin_commands` and calls its handler, bridging async handlers via `asyncio.run()`.\\n\\nThe fixture tears down cleanly: it closes the store, deregisters all argicida tools from the global registry, and clears all hooks. This prevents test pollution when the registry singleton persists across test modules.\\n\\n### `create_and_activate_engagement`\\n\\nA convenience method that dispatches `argicida_engagement_create` through the real tool registry. This ensures the engagement is created with all the same side effects as production \u2014 hook callbacks fire, state binds, the store writes. Tests that need an active engagement call this rather than writing directly to the store.\\n\\n## Test Modules\\n\\n### `test_registration.py` \u2014 Structural Invariants\\n\\nAsserts exact counts and names for everything `register()` produces: 34 tools, 9 hooks, 6 slash commands, 1 CLI command. These are consistency tests \u2014 if someone adds a tool but forgets to wire its schema, or renames a hook without updating the registration module, these fail.\\n\\nThe tool name set is exhaustive. When a new tool is added via `tools/defs/`, it must appear here or the test breaks. This is intentional \u2014 it forces the contributor to acknowledge the registration.\\n\\n### `test_hook_pipeline.py` \u2014 Hook Invocation Through Real PluginManager\\n\\nTests the hook pipeline as it actually fires in production, not through mocked hook lists.\\n\\n**`TestScopeGuardHook`** \u2014 The scope guard (`pre_tool_call`) is the primary safety mechanism. These tests verify:\\n- Scan tools are blocked when no engagement is active\\n- In-scope targets pass\\n- Out-of-scope targets are blocked\\n- Non-argicida tools are ignored (the guard only intercepts `argicida_*` tools)\\n\\n**`TestPostToolCallHookIsolation`** \u2014 Two `post_tool_call` hooks are registered (evidence chain + output store). These tests verify the Hermes fault-isolation guarantee: if the evidence hook crashes, the output store hook still fires. They also verify registration order (evidence before output).\\n\\n**`TestNormalizerScopeGuardPipeline`** \u2014 Validates a critical ordering invariant: the scope guard sees the *original* args from the LLM, not normalized args. Normalizers run inside `make_scan_handler`, after the scope guard has already approved or blocked the call. If this ordering were reversed, a normalizer could inject arguments that bypass scope validation.\\n\\n### `test_tool_dispatch.py` \u2014 Tool Handlers Through Real Registry\\n\\nDispatches tools through `registry.dispatch()` \u2014 the same path the Hermes agent loop uses. Without a running pod, scan tools return structured error JSON. This is expected and tested explicitly: all six scan tools (`nmap`, `nuclei`, `subfinder`, `httpx`, `naabu`, `tracepath`) must return `{\\\"error\\\": ...}` rather than crashing.\\n\\nManagement tools (`engagement_create`, `engagement_query`, `triage_findings`, `briefing_generate`) work fully since they operate on the database, not the pod. The briefing test verifies that `ctx.llm.acomplete` is called exactly once \u2014 confirming the LLM integration point is wired correctly.\\n\\n### `test_slash_commands.py` \u2014 Command Dispatch\\n\\nTests all six slash commands (`/scope`, `/triage`, `/approve-gate`, `/briefing`, `/trace`, `/engage`) through the real command dispatch path. Each command is tested both with and without an active engagement. The `/engage` subcommands (`list`, `resume`, `new`, `close`, `rename`, `status`) each have dedicated tests verifying state transitions through the real store.\\n\\n### `test_session_lifecycle.py` \u2014 Session Hooks\\n\\nVerifies that `on_session_end` hooks handle both normal completion and interrupted sessions without crashing. Also tests that hooks accept extra keyword arguments (Hermes may add new fields like `telemetry_schema_version` \u2014 hooks must not reject unknown kwargs).\\n\\n### `test_asset_pipeline.py` \u2014 Asset Extraction End-to-End\\n\\nTests the asset subsystem through the real `EngagementStore` and `AssetRepo`:\\n\\n- **Scope seeding**: `/32` CIDRs create assets, `/24` CIDRs don't, `/128` IPv6 CIDRs create assets\\n- **Engagement resume**: Hostname\u2192IP mappings replay into a dnsmasq callback; findings with IP targets backfill missing assets; hostname-only findings are skipped\\n- **Hook wiring**: `make_asset_extraction_adapter` produces a callable with the correct `post_tool_call` signature\\n\\n### `test_phase_lifecycle.py` \u2014 Engagement Phase State Machine\\n\\nDrives the full phase progression (`SCOPING` \u2192 `DISCOVERY` \u2192 `ENUMERATION` \u2192 `DEEP_TESTING` \u2192 `HUMAN_GATE`) through the real store and real `build_autonomy_directive()`. This test verifies that:\\n\\n1. Phase advances are triggered by the right conditions (service identification triggers `DISCOVERY` \u2192 `ENUMERATION`)\\n2. The autonomy directive contains the right signals at each phase (e.g., `UNCOVERED` services during enumeration)\\n3. The `HUMAN_GATE` phase produces a directive that references `briefing_generate`\\n\\nThis is the only test that exercises the autonomy directive logic against a real database \u2014 unit tests for `build_autonomy_directive` use in-memory findings, but this test writes findings to SQLite and reads them back.\\n\\n## What These Tests Do Not Cover\\n\\nIntegration tests mock the pod and IPC layers. They cannot verify:\\n\\n- Network isolation (nftables rules, gateway FORWARD chain)\\n- Container agent behavior (tool execution, output streaming)\\n- IPC protocol correctness (msgpack encoding, socket permissions)\\n- Real LLM-driven tool selection\\n\\nThese are covered by pod integration tests (`tests/pod_integration/`) and E2E tests (`tests/e2e/`).\\n\\n## Adding New Integration Tests\\n\\nWhen adding a new tool, hook, or command, add corresponding integration tests:\\n\\n1. **New tool**: Add its name to the `expected` set in `test_tool_names`. If it's a scan tool, add it to `test_all_scan_tools_return_error_without_pod`.\\n2. **New hook**: Update the count in `test_seven_hooks_registered` (the name is stale \u2014 it checks 9) and add the hook name to `test_hook_names`.\\n3. **New slash command**: Update the count in `test_six_commands_registered` and add the command name to `test_command_names`. Write at least two tests: one without an engagement (should return an error), one with.\\n\\nAll integration tests run with `uv run pytest tests/integration/` \u2014 no Podman, no containers, no LLM endpoint required.\",\"other-tests-pod-integration\":\"# Other \u2014 tests-pod_integration\\n\\n# Pod Integration Tests\\n\\n## Purpose\\n\\nThe pod integration test suite validates Argicida's core subsystems against real Podman infrastructure. Unlike unit tests that mock IPC, pods, and container runtimes, these tests launch actual containers, establish real Unix socket connections, execute real scan tool binaries, and verify that nftables rules, dnsmasq DNS, and the dual-bridge gateway topology behave correctly under production conditions.\\n\\nThe suite occupies the middle tier of Argicida's three-tier test architecture: heavier than unit/integration tests (which use no infrastructure) but lighter than E2E tests (which require a live LLM endpoint and run through the real HermesCLI agent loop). Pod integration tests call tool handlers and hooks directly through the Hermes registry, bypassing the LLM entirely.\\n\\nRun with:\\n\\n```bash\\nuv run pytest -m pod_integration -v\\n```\\n\\n## Prerequisites\\n\\n- **Podman** (rootless) available at `/usr/bin/podman`\\n- **Built images**: `argicida-tools:dev`, `argicida-gateway:dev`\\n- **Pulled image**: `docker.io/library/nginx:alpine`\\n- **ZAP tests additionally require**: `argicida-zap:dev`\\n\\nThe session-scoped `_require_podman_and_images` fixture skips the entire suite if any prerequisite is missing.\\n\\n## Fixture Architecture\\n\\nFixtures are layered by pytest scope to balance isolation against pod startup cost (~5-10s per topology).\\n\\n```mermaid\\ngraph TD\\n    S1[_require_podman_and_imagessession \u00b7 autouse] --&gt; S2[e2e_networksession \u00b7 bridge network]\\n    S2 --&gt; S3[nginx_targetsession \u00b7 vulnerable site]\\n    S2 --&gt; S4[graphql_targetsession \u00b7 introspection endpoint]\\n    S2 --&gt; S5[out_of_scope_targetsession \u00b7 blocked IP]\\n    S2 --&gt; M1[pod_harnessmodule \u00b7 standard topology]\\n    S2 --&gt; M2[zap_harnessmodule \u00b7 ZAP sidecar topology]\\n    M1 --&gt; T[\\\"test functions(dispatch_tool, invoke_hook)\\\"]\\n    M2 --&gt; T\\n```\\n\\n**Session-scoped** (created once, shared across all modules):\\n\\n| Fixture | What it provides |\\n|---------|-----------------|\\n| `e2e_network` | Podman bridge network `argicida-e2e` connecting all containers |\\n| `nginx_target` | IP address of an nginx container serving deliberately vulnerable content (exposed `.env`, `.git/config`, `swagger.json`) |\\n| `graphql_target` | IP address of a Python HTTP server responding to GraphQL introspection queries |\\n| `out_of_scope_target` | IP address of a container that is never added to any engagement scope \u2014 used to verify scope enforcement blocks it |\\n\\n**Module-scoped** (one per test file):\\n\\n| Fixture | What it provides |\\n|---------|-----------------|\\n| `pod_harness` | Full Hermes registration + `PodIntegrationHarness` with real PodManager. Pod launches on first `ensure_pod()` or tool dispatch. |\\n| `zap_harness` | Same as `pod_harness` but sets `ARGICIDA_ZAP_IMAGE` before registration and calls `ensure_pod()` eagerly (ZAP's JVM takes ~30s). Skips the module if `argicida-zap:dev` is not built. |\\n\\nModule scope means each test file gets its own pod topology. This is necessary because some tests are destructive (killing the gateway, clearing `pod_session`, tightening scope rules) and cannot share infrastructure.\\n\\n## PodIntegrationHarness\\n\\n`PodIntegrationHarness` is the central test driver. It wraps a real Hermes `PluginManager` with a real Argicida `register()` call, giving tests access to the full plugin lifecycle without an LLM.\\n\\n### What it owns\\n\\n- `pm` / `ctx` \u2014 Real Hermes `PluginManager` and `PluginContext` (with a mocked `ctx._llm`)\\n- `store` \u2014 Real `EngagementStore` backed by a temporary SQLite database\\n- `state` \u2014 Real `PluginState` holding the live `PodSession` and `EngagementSession`\\n- `hermes_home` \u2014 Temporary directory tree mimicking `$HERMES_HOME`\\n\\n### dispatch_tool\\n\\nThe critical method. It mirrors the Hermes agent loop sequence that runs in production when the LLM calls a tool:\\n\\n1. `pre_tool_call` \u2014 scope guard validates target and args\\n2. `registry.dispatch()` \u2014 handler runs (async handlers bridged automatically)\\n3. `post_tool_call` \u2014 evidence chain, output store, asset extraction\\n4. `transform_tool_result` \u2014 normalizer enriches result, scope expands\\n\\nThis is **not** a simplified reimplementation. It calls the same hook functions and registry dispatch that the production agent loop uses \u2014 the only difference is that a test function calls `dispatch_tool` instead of the LLM.\\n\\n### create_and_activate_engagement\\n\\nCreates an engagement through the real `argicida_engagement_create` handler, which runs `bind_engagement()` \u2192 `EngagementSession.bind()`, setting up signing keys, artifacts directory, evidence chain seeding, and scope rules. Returns the engagement ID.\\n\\n### Teardown\\n\\n`_create_harness` is a generator that yields the harness and cleans up on exit:\\n\\n1. Tears down the pod session (pod + gateway + networks)\\n2. Deregisters tools added during `register()`\\n3. Closes the engagement store\\n4. Restores environment variables (`HERMES_HOME`, `ARGICIDA_NETWORK`)\\n\\n## Test Modules\\n\\n### test_first_light.py \u2014 Core dispatch and lifecycle\\n\\nThe foundational tests. Exercises the production path from engagement creation through tool dispatch to output verification:\\n\\n- **Tool dispatch**: naabu, nmap, tracepath against real nginx \u2014 verifies pod auto-launch, exit codes, raw output\\n- **Finding extraction**: naabu findings lack `service` (port-only), nmap `-sV` findings carry `service=\\\"http\\\"`\\n- **Output storage**: `stdout`, `meta.json` (including `tool_args`) written to `$HERMES_HOME/argicida/output/`\\n- **Evidence chain**: chain entries match findings count, sequences are contiguous, hashes link correctly\\n- **Scope enforcement**: out-of-scope targets blocked by `pre_tool_call` hook, in-scope targets pass\\n- **Host mapping**: `add_host` / `remove_host` write to dnsmasq config, entries visible in gateway, multiple hostnames per IP\\n- **Web tools**: katana crawl, ffuf fuzzing, curl GET/download against nginx\\n- **Context injection**: `pre_llm_call` returns resume briefing (with findings) or methodology (without), phase advances through DISCOVERY \u2192 ENUMERATION\\n- **Error enrichment**: `db_query` without connection URL gets actionable guidance\\n\\n### test_asset_registry.py \u2014 Asset population pipeline\\n\\nVerifies the asset extraction pipeline:  `engagement_create` (scope seeding) \u2192 `naabu`/`nmap`/`httpx` (scan) \u2192 `post_tool_call` hooks (extractor) \u2192 store assertions.\\n\\n- Scope seeding: `/32` CIDR creates asset in `discovered` state\\n- Port population: naabu discovers ports, nmap adds service detection\\n- Hostname population: httpx against a custom hostname populates the asset's hostname list\\n- Resolution: IPs and hostnames resolve to `asset_id` after scan\\n- Coverage state machine: asset advances from `discovered` \u2192 `ports_scanned` after naabu\\n- Engagement resume: hostname\u2192IP mappings replay into pod host entries\\n\\n### test_container_build.py \u2014 Image content verification\\n\\nRuns assertions inside the actual container image (via `podman run --rm` or a module-scoped container with entrypoint). Catches regressions that only manifest at runtime:\\n\\n- **Tool binaries**: all 14 scan binaries present and executable (nuclei, nmap, httpx, katana, ffuf, curl, etc.)\\n- **Nuclei templates**: directory exists with content, `.nuclei-ignore` baked in, `templates:` key points to `/opt/nuclei-templates/`\\n- **PD configs**: baked to `/opt/pd-config/`, entrypoint copies to `$HOME/.config/`\\n- **Nuclei config**: update checks disabled, interactsh disabled, headless enabled, CDP endpoint configured, no `system-chrome`\\n- **Container user**: runs as UID 10000, `/ipc` writable, home writable with tmpfs\\n- **CDP Chromium**: endpoint at `127.0.0.1:9222` responding, process running, nuclei config has resolved browser GUID\\n- **SecLists**: 10 wordlist paths verified present, `common.txt` has 4000+ lines\\n\\nThe `tools_container` fixture starts the container with its real entrypoint (which copies configs and starts Chromium), then waits for CDP readiness before yielding.\\n\\n### test_diagnostic_pod.py \u2014 Pre-engagement pod behavior\\n\\nTests pods launched without an engagement (diagnostic mode):\\n\\n- Gateway is running with FORWARD chain even without engagement\\n- FORWARD chain starts with `policy accept` (permissive)\\n- After `scope_define`, FORWARD tightens to `policy drop` with scope-specific allow rules\\n\\n### test_engagement_management.py \u2014 Mid-session operations\\n\\n- **Engagement switch**: create A, scan, switch to B \u2014 pod survives, findings isolated between engagements\\n- **dnsmasq host entries**: `add_host` writes `address=` entry, dnsmasq restarts, tool container resolves hostname\\n- **Scope expansion**: `scope_expand=True` adds IP to engagement scope and nftables rules\\n- **Scope regen**: host entries persist after `scope_define` triggers dnsmasq regeneration\\n\\n### test_gateway_isolation.py \u2014 Network topology (non-destructive)\\n\\nVerifies the dual-bridge topology without killing containers:\\n\\n- FORWARD chain present with `policy drop` after scope definition\\n- `resolv.conf` in tool container points to `10.89.1.2` (gateway internal IP)\\n- Gateway has NAT masquerade rule\\n- Gateway has INPUT chain (self-protection)\\n\\n### test_gateway_fail_closed.py \u2014 Fail-closed proof (destructive)\\n\\nKills the gateway container and proves the tool pod loses all connectivity. This test gets its own module (own `pod_harness` lifecycle) because recovery is not possible after gateway death.\\n\\n### test_fault_recovery.py \u2014 Stale socket reconnect\\n\\nDeletes the IPC socket inside the container, then dispatches a tool. Verifies that `ensure_client` detects the broken connection and reconnects without pod restart.\\n\\n### test_fault_stale_session.py \u2014 Stale session relaunch\\n\\nClears `state.pod_session` (simulating a crash), then dispatches a tool. Verifies that `ensure_pod` detects the orphaned pod by name convention, cleans it up, and launches a new topology. Gets its own module because the new topology would collide with the original.\\n\\n### test_artifact_transfer.py \u2014 File transfer and screenshots\\n\\n- **Binary integrity**: writes controlled binary content in container, transfers via `artifact_save`, verifies byte-for-byte match on host\\n- **Screenshots**: `argicida_screenshot` captures nginx via headless Chromium, verifies PNG magic bytes, correct filename derivation, IPC cleanup after capture\\n- **Scope enforcement**: screenshot of out-of-scope target blocked\\n\\n### test_ipc_file_write.py \u2014 Host\u2192container file exchange\\n\\nWrites a file into `ipc_dir` using `write_ipc_file` (the `0o644` / `O_NOFOLLOW` pattern), then reads it back via `podman exec cat` inside the container. Catches permission regressions that would break `FileInjector`, `TargetFileInjector`, and `PlanExecutor`.\\n\\n### test_zap.py \u2014 ZAP sidecar integration\\n\\nComprehensive ZAP lifecycle tests using the `zap_harness` fixture:\\n\\n- **Pod lifecycle**: ZAP container starts, `zap_active` flag set, `ZapManager` created, API responds, CA cert extracted as valid PEM\\n- **Engagement gating**: `zap_scan`, `zap_context`, `zap_auth` fail gracefully without an active engagement\\n- **Proxy integration**: katana crawl through ZAP proxy produces passive findings (verifies `ZapProxyInjector` injection)\\n- **Context management**: create, idempotent re-create, create with exclude patterns\\n- **Scan workflow**: quick scan returns findings, findings have correct schema fields\\n- **Alert filtering**: filter by `baseurl`, `min_risk`, nonexistent URL returns zero\\n- **Finding persistence**: `persist_findings` stores to DB, deduplicates on re-persist\\n- **Enriched findings**: solution field, WASC IDs, stage timing in response\\n- **AJAX Spider**: `ajax_spider=true` parameter accepted\\n- **API import**: OpenAPI spec import from nginx's `swagger.json`, GraphQL introspection import from the graphql target\\n- **Adaptive scan policy**: passive harvest from proxy traffic drives active rule selection\\n\\n## Module Isolation Strategy\\n\\nSeveral tests are destructive \u2014 they kill containers, clear state, or tighten firewall rules irreversibly. These are isolated into separate modules so each gets its own `pod_harness` lifecycle:\\n\\n| Module | Why isolated |\\n|--------|-------------|\\n| `test_gateway_fail_closed` | Kills gateway \u2014 no recovery possible |\\n| `test_fault_stale_session` | Clears `pod_session` \u2014 new topology collides with original |\\n| `test_diagnostic_pod` | Depends on pristine permissive FORWARD rules before `scope_define` |\\n\\nNon-destructive modules (`test_first_light`, `test_asset_registry`, `test_gateway_isolation`, etc.) share a `pod_harness` safely because their operations are additive \u2014 they create engagements, dispatch tools, and inspect state without destroying topology components.\\n\\n## Writing New Pod Integration Tests\\n\\n1. **Mark with `@pytest.mark.pod_integration`** \u2014 this excludes them from the default `uv run pytest` run\\n2. **Use `pod_harness` or `zap_harness`** \u2014 never create pods manually\\n3. **Call `create_and_activate_engagement`** before dispatching scan tools \u2014 scope enforcement blocks unscoped targets\\n4. **Call `ensure_pod()`** if you need a running pod without dispatching a tool (e.g., to inspect gateway state)\\n5. **Isolate destructive tests** into their own module \u2014 anything that kills containers, clears `pod_session`, or irreversibly changes nftables rules\\n6. **Assert on parsed JSON** from `dispatch_tool`, not raw strings \u2014 all tool handlers return `json.dumps()` strings\\n7. **Check `\\\"error\\\" not in parsed`** before asserting on result fields \u2014 scope blocks and handler failures both return `{\\\"error\\\": \\\"...\\\"}`\",\"other-tests-scripts\":\"# Other \u2014 tests-scripts\\n\\n# `tests/scripts/` \u2014 Skill Sync Script Tests\\n\\nTests for `scripts/sync_skills.py`, the utility that keeps profile skills (checked into the repo at `profile/skills/`) in sync with their runtime copies (deployed to `$HERMES_HOME/skills/` after `hermes profile install`).\\n\\n## Why This Exists\\n\\nSkills are authored in the repo but executed from the Hermes runtime directory. The LLM can self-improve skills at runtime, creating drift between the two copies. `sync_skills.py` detects and resolves that drift in either direction. These tests validate the five core operations the script exposes.\\n\\n## What's Under Test\\n\\nThe test file imports directly from the script via `importlib.util` (since `scripts/` is not a package):\\n\\n| Function | Purpose |\\n|---|---|\\n| `collect_repo_skills(path)` | Walks a skill tree, returns all directories containing `SKILL.md`. Recurses into nested hub/sub-skill structures. |\\n| `find_runtime_skill(name, path)` | Locates a skill in the runtime tree by name \u2014 checks both directory names and YAML frontmatter `name:` fields. |\\n| `diff_skill(repo, runtime)` | Compares `SKILL.md` content between repo and runtime copies. Returns `None` if identical, a diff string otherwise. |\\n| `sync_skill(repo, runtime, direction)` | Copies skill files in the specified direction (`\\\"repo-to-runtime\\\"` or `\\\"runtime-to-repo\\\"`). Includes auxiliary files, not just `SKILL.md`. |\\n| `parse_skill_name(skill_md)` | Extracts the skill name from YAML frontmatter `name:` field, falling back to the parent directory name. |\\n| `SkillPair` | Data class pairing a skill name with its repo and runtime paths. |\\n\\n## Key Behaviors Verified\\n\\n**Frontmatter name takes priority over directory name.** Sub-skills like `profile/skills/security/db-enumeration/postgresql/SKILL.md` declare `name: db-postgresql` in frontmatter. Both `collect_repo_skills` and `find_runtime_skill` resolve by frontmatter name, not filesystem path. This matters because tool schemas reference skills by frontmatter name (e.g., `skill_view('db-postgresql')`), and a mismatch silently breaks the loading chain.\\n\\n**Nested hub skills are fully discovered.** A hub skill like `db-enumeration/` contains its own `SKILL.md` plus sub-skill directories (`postgresql/`, `mysql/`), each with their own `SKILL.md`. `collect_repo_skills` finds all levels.\\n\\n**Sync copies everything, not just SKILL.md.** Skills can include auxiliary files (reference docs, examples). `sync_skill` copies the full directory contents in the chosen direction.\\n\\n**Graceful handling of missing/malformed data.** Missing directories return empty lists, missing frontmatter falls back to directory name, empty `name:` fields fall back to directory name, invalid sync directions raise `ValueError`.\\n\\n## Running\\n\\n```bash\\nuv run pytest tests/scripts/ -v\\n```\\n\\nThese are pure-filesystem tests with no infrastructure dependencies \u2014 they use `tmp_path` fixtures exclusively.\",\"other-tests\":\"# Other \u2014 tests\\n\\n# Test Infrastructure \u2014 Shared Factories and Configuration\\n\\nThe `tests/` root contains the shared test infrastructure that all Argicida test modules depend on. It provides factory functions for constructing domain objects with sensible defaults, reducing boilerplate across the 2800+ unit and integration tests.\\n\\n## `tests/factories.py`\\n\\nTwo factory functions and one constant that form the foundation of test data construction.\\n\\n### `TEST_CIDR`\\n\\n```python\\nTEST_CIDR = \\\"10.10.10.0/24\\\"\\n```\\n\\nCanonical test network range. Used wherever tests need a scope CIDR \u2014 scope guard tests, nftables rule generation, engagement setup. Using a single constant prevents tests from silently diverging on what \\\"in scope\\\" means.\\n\\n### `make_finding(**overrides) -&gt; Finding`\\n\\nConstructs an `argicida_common.finding_schema.Finding` with defaults that satisfy all required fields:\\n\\n```python\\nfinding = make_finding()                          # all defaults\\nfinding = make_finding(severity=\\\"critical\\\", tool=\\\"nuclei\\\")  # override specific fields\\n```\\n\\nDefault values:\\n| Field | Default |\\n|-------|---------|\\n| `tool` | `\\\"nmap\\\"` |\\n| `target` | `\\\"10.10.10.1\\\"` |\\n| `title` | `\\\"Test finding\\\"` |\\n| `severity` | `\\\"info\\\"` |\\n| `description` | `\\\"Test description\\\"` |\\n| `raw_output` | `\\\"\\\"` |\\n| `triage_level` | `\\\"noise\\\"` |\\n| `timestamp` | `\\\"2026-01-01T00:00:00+00:00\\\"` |\\n\\nThis is the most widely used factory in the suite \u2014 called from evidence chain tests, signer tests, database helpers, briefing generation, triage logic, and engagement lifecycle tests. The `target` default (`10.10.10.1`) falls within `TEST_CIDR`, so findings created with defaults are always in-scope.\\n\\n### `make_tool_def(schema_name, toolset=\\\"argicida\\\") -&gt; dict`\\n\\nConstructs a minimal OpenAI-format tool schema dict for registration tests:\\n\\n```python\\ntool_def = make_tool_def(\\\"argicida_nmap\\\")\\ntool_def = make_tool_def(\\\"argicida_httpx\\\", toolset=\\\"custom\\\")\\n```\\n\\nReturns a dict with `name`, `toolset`, `description`, and an empty `parameters` object. Used where tests need a tool registered in the Hermes `tools.registry` without caring about the actual parameter schema.\\n\\n## `tests/conftest.py`\\n\\nShared pytest fixtures available to all test modules under `tests/`. Fixtures defined here are automatically discovered by pytest without explicit imports.\\n\\n## Usage Pattern\\n\\nThe factory pattern follows a deliberate convention: **defaults produce valid, internally consistent objects**. Tests override only the fields relevant to what they're asserting. This keeps tests readable \u2014 when you see `make_finding(severity=\\\"critical\\\")`, the test is about severity behavior, and every other field is irrelevant noise held constant.\\n\\n```python\\n# Evidence chain test \u2014 only cares about chaining, not finding content\\nchain.append(make_finding())\\nchain.append(make_finding())\\nassert chain.verify()\\n\\n# Triage test \u2014 cares about tool identity\\nmake_finding(tool=\\\"nmap\\\")       # should be processed\\nmake_finding(tool=\\\"subfinder\\\")  # should be skipped\\n```\\n\\nWhen adding new test modules, prefer these factories over hand-constructing `Finding` or tool definition dicts. If a new domain object appears frequently in tests, add a factory here rather than duplicating construction logic across test files.\",\"other\":\"# Other\\n\\n# Other \u2014 Project Infrastructure &amp; Cross-Cutting Concerns\\n\\nEverything outside Argicida's runtime code: build configuration, documentation, test suites, the Hermes profile, and AI-assisted development setup. These modules define how the project is built, tested, documented, deployed as a Hermes agent, and maintained by both human and AI contributors.\\n\\n## Module Groups\\n\\n### Build &amp; Configuration\\n\\nThree files define the project's identity and build system:\\n\\n- [pyproject.toml](pyproject.toml.md) \u2014 uv workspace root coordinating the three packages (`argicida`, `argicida_agent`, `argicida_common`) with centralized linting, type checking, security scanning, and test configuration.\\n- [CLAUDE.md](claude-md.md) \u2014 Machine-readable project instructions that Claude Code loads into every session. Acts as a binding contract: architecture constraints, security rules, command reference, and validation requirements.\\n- [README.md](readme.md) \u2014 Human-facing project overview covering prerequisites, installation, architecture, and development workflow.\\n\\n### Workspace Packages (Build Metadata)\\n\\nPackage-level `pyproject.toml` files and entry points for each workspace member. These define dependencies, entry points, and extras \u2014 not runtime logic:\\n\\n- [src-argicida](src-argicida.md) \u2014 Host plugin entry point (`hermes_agent.plugins`), heavy dependencies (podman, pydantic, cryptography), optional `[vision]` extras.\\n- [src-argicida_agent](src-argicida_agent.md) \u2014 Container agent with minimal dependencies (msgspec only).\\n- [src-argicida_common](src-argicida_common.md) \u2014 Shared types package imported by both sides of the IPC boundary. Single dependency (msgspec).\\n\\n### Documentation\\n\\nFive directories with distinct authority levels:\\n\\n- [docs](docs.md) \u2014 Active project documents: development standards (mandatory), research roadmap (reference), triage engine spec (technical).\\n- [docs/archive](docs-archive.md) \u2014 Frozen history: audit records, refactor plans, and architectural decisions from the two major pivots.\\n- [docs/research](docs-research.md) \u2014 Multi-agent adversarial research artifacts with confidence-scored findings that justified non-obvious architectural choices.\\n- [docs/rfe](docs-rfe.md) \u2014 Request for Enhancement decision records following a Draft \u2192 Implementing \u2192 Implemented \u2192 Verified lifecycle.\\n- [docs/superpowers](docs-superpowers.md) \u2014 ZAP integration design archive: the specs and plans behind the `zap/` subsystem.\\n\\n### Hermes Profile\\n\\nThe runtime configuration that transforms a generic Hermes agent into a penetration testing operator:\\n\\n- [profile](profile.md) \u2014 Profile distribution: LLM identity (SOUL.md), model routing, 13 technique skills with three-tier progressive disclosure, delegation config, and operational parameters.\\n- [profile/skins](profile-skins.md) \u2014 CLI theming via `argicida.yaml` \u2014 colors, spinners, branding. Pure YAML configuration, no runtime logic.\\n\\n### Test Infrastructure\\n\\nSix test modules organized in a three-tier architecture, plus shared factories:\\n\\n| Tier | Module | Infrastructure | Count |\\n|------|--------|---------------|-------|\\n| Shared | [tests](tests.md) | Factories (`make_finding`, `make_engagement`) | \u2014 |\\n| Unit | [tests/argicida](tests-argicida.md) | Everything mocked | ~2800 |\\n| Unit | [tests/argicida_agent](tests-argicida_agent.md) | No containers | \u2014 |\\n| Unit | [tests/argicida_common](tests-argicida_common.md) | Schema contracts only | \u2014 |\\n| Integration | [tests/integration](tests-integration.md) | Real Hermes + real DB, mocked pod | \u2014 |\\n| Pod integration | [tests/pod_integration](tests-pod_integration.md) | Real Podman, real IPC, direct dispatch | 139 |\\n| E2E | [tests/e2e](tests-e2e.md) | Real HermesCLI, real LLM, real ZAP | 10 |\\n| Scripts | [tests/scripts](tests-scripts.md) | Skill sync validation | \u2014 |\\n\\n## How They Fit Together\\n\\n```mermaid\\ngraph TD\\n    subgraph \\\"Build\\\"\\n        PY[\\\"pyproject.tomlworkspace root\\\"]\\n        SA[\\\"src-argicidapyproject.toml\\\"]\\n        SAG[\\\"src-argicida_agentpyproject.toml\\\"]\\n        SAC[\\\"src-argicida_commonpyproject.toml\\\"]\\n        PY --&gt; SA &amp; SAG &amp; SAC\\n    end\\n\\n    subgraph \\\"Runtime Config\\\"\\n        PROF[\\\"profile/skills, SOUL, config\\\"]\\n        SKIN[\\\"profile/skins/CLI theme\\\"]\\n    end\\n\\n    subgraph \\\"Documentation\\\"\\n        DOCS[\\\"docs/standards, specs\\\"]\\n        ARCH[\\\"docs/archive/frozen history\\\"]\\n        RES[\\\"docs/research/evidence\\\"]\\n        RFE[\\\"docs/rfe/decisions\\\"]\\n    end\\n\\n    subgraph \\\"Testing\\\"\\n        FAC[\\\"tests/factories\\\"]\\n        UNIT[\\\"tests/argicida*/unit\\\"]\\n        INT[\\\"tests/integration/\\\"]\\n        POD[\\\"tests/pod_integration/\\\"]\\n        E2E[\\\"tests/e2e/\\\"]\\n        FAC --&gt; UNIT &amp; INT &amp; POD &amp; E2E\\n    end\\n\\n    CLAUDE[\\\"CLAUDE.md\\\"] -.-&gt;|constrains| PY &amp; DOCS\\n    RFE -.-&gt;|drives| SA\\n    RES -.-&gt;|justifies| PROF\\n```\\n\\nThe build system (`pyproject.toml`) defines what gets built; the profile defines how it runs as an agent; the documentation records why it was built that way; the test suites prove it works at every level of abstraction. `CLAUDE.md` sits across all of them, ensuring AI-assisted development respects the same constraints that the documentation and tests enforce.\\n\\n## Key Cross-Module Workflows\\n\\n**E2E test lifecycle** spans the test harness (`tests/e2e/conftest.py`) through pod teardown (`cleanup_topology_by_name`) to container removal \u2014 exercising the full path from Hermes CLI bootstrap through plugin discovery, tool dispatch, and resource cleanup.\\n\\n**Skill authoring and sync** connects the profile (`profile/skills/security/`) to the test scripts (`tests/scripts/`) via `sync_skills.py`, keeping repo-checked skills aligned with runtime-deployed copies that the LLM may have self-improved.\\n\\n**Documentation authority chain**: RFEs in `docs/rfe/` drive implementation, research in `docs/research/` justifies architectural choices recorded in `docs/archive/`, and active standards in `docs/` govern ongoing development \u2014 with `CLAUDE.md` encoding all of these as machine-enforceable rules.\",\"overview\":\"# Argicida \u2014 Wiki\\n\\n# Argicida\\n\\nArgicida is an LLM-driven penetration testing framework that runs as a plugin for [Hermes Agent](https://hermes-agent.nousresearch.com/). It gives the LLM a toolkit of 34 security tools \u2014 port scanners, web fuzzers, database clients, SMB/SSH/DNS utilities \u2014 all running inside isolated Podman containers. The LLM decides what to scan and in what order; Argicida enforces scope, captures evidence, and makes sure nothing leaves the approved target list.\\n\\nThere is no hardcoded scan pipeline. The operator defines a scope (target CIDRs, domains, exclusions), and the LLM drives the engagement from reconnaissance through enumeration, choosing tools based on what it discovers. A human gate lets the operator review findings before the session transitions to interactive exploitation.\\n\\n## Architecture\\n\\nArgicida is split into three Python packages that communicate over a Unix socket:\\n\\n```mermaid\\ngraph TD\\n    Hermes[\\\"Hermes Agent\\\"]\\n    Plugin[\\\"argicida(host plugin)\\\"]\\n    Common[\\\"argicida_common(shared types)\\\"]\\n    Agent[\\\"argicida_agent(container agent)\\\"]\\n    Gateway[\\\"Gateway Container(nftables scope)\\\"]\\n    Tools[\\\"Scan Tools(nmap, nuclei, \u2026)\\\"]\\n    ZAP[\\\"ZAP Sidecar(proxy + active scan)\\\"]\\n    DB[\\\"EngagementStore(SQLite/APSW)\\\"]\\n\\n    Hermes --&gt;|plugin API| Plugin\\n    Plugin --&gt;|Unix socket IPC| Agent\\n    Plugin --&gt; DB\\n    Agent --&gt; Tools\\n    Plugin -.-&gt;|REST API| ZAP\\n    Tools --&gt;|all egress| Gateway\\n    ZAP --&gt;|all egress| Gateway\\n    Common -.-|types| Plugin\\n    Common -.-|types| Agent\\n```\\n\\n**`argicida`** is the host-side plugin \u2014 the largest package. It [registers into Hermes](plugin-registration-and-core.md) at startup, manages [pod lifecycle](pod-management.md), dispatches tool calls over [IPC](ipc-layer.md), and runs a [hook pipeline](hook-pipeline.md) that intercepts every tool invocation.\\n\\n**`argicida_agent`** is a [thin process inside the container](container-agent.md). It receives commands on a Unix socket, runs binaries via `create_subprocess_exec`, and streams output back. It never decides what to run.\\n\\n**`argicida_common`** holds the [shared types and schemas](shared-types-and-schemas.md) \u2014 msgspec Structs for IPC messages, finding schemas, scope definitions \u2014 that both sides depend on. Its only external dependency is `msgspec`.\\n\\n## How a Scan Flows End-to-End\\n\\nWhen the LLM calls a tool like `argicida_nmap`, here is what happens:\\n\\n1. **Pod launch** \u2014 If no pod exists, [pod management](pod-management.md) creates a dual-bridge network topology: an internal-only network for the tool pod, a gateway container bridging internal and external networks, and nftables rules on the gateway that only allow traffic to in-scope targets.\\n\\n2. **Scope check** \u2014 The [hook pipeline](hook-pipeline.md) fires `pre_tool_call`, which runs the [scope guard](scope-enforcement.md). It validates both the `target` field and every argument in the `args` array against the engagement scope. If anything is out of scope, the call is denied before it reaches the container.\\n\\n3. **Normalization** \u2014 The [scan tool framework](scan-tool-framework.md) runs the tool's normalizer chain, which corrects common LLM mistakes (missing flags, typos in NSE script names, absent output format flags) without burning a turn.\\n\\n4. **IPC dispatch** \u2014 The host-side [IPC client](ipc-layer.md) sends a msgpack-encoded command over the Unix socket. The [container agent](container-agent.md) authenticates the connection with HMAC-SHA256, executes the binary, and streams stdout/stderr back.\\n\\n5. **Post-processing** \u2014 Back on the host, hooks capture the raw output to the [lossless output store](evidence-and-reporting.md), parse findings through tool-specific extractors, run them through the [triage engine](triage-engine.md) for severity scoring, update the [asset registry](engagement-and-database.md) with discovered hosts and services, and append entries to the cryptographic evidence chain.\\n\\n6. **Context injection** \u2014 On the next LLM turn, the `pre_llm_call` hook injects a compact summary of engagement state \u2014 scope, assets, findings, coverage gaps \u2014 so the LLM can decide what to scan next.\\n\\nIf [ZAP](zap-integration.md) is enabled, web-facing tool traffic is automatically proxied through the ZAP sidecar for continuous passive analysis, and the LLM can trigger active scans against specific targets.\\n\\n## Scope Enforcement\\n\\nScope is enforced at two independent layers, and both must pass:\\n\\n- **Network layer** \u2014 nftables FORWARD chain rules on the [gateway container](scope-enforcement.md) drop all traffic that doesn't match the engagement's target CIDRs or domains. If the gateway dies, routing fails \u2014 the design is fail-closed.\\n- **Command layer** \u2014 the `pre_tool_call` [hook](hook-pipeline.md) validates every argument the LLM passes to a tool. It checks targets, flag values, and positional arguments against scope. This layer is strictly narrower than the network rules.\\n\\n## Operator Interfaces\\n\\nThe operator interacts with Argicida through two surfaces, both backed by the same [engagement database](engagement-and-database.md):\\n\\n- **CLI subcommands** (`hermes argicida ...`) and **slash commands** (`/scope`, `/triage`, `/briefing`, `/engage`, `/trace`, `/approve-gate`) for direct human control \u2014 documented in [Management &amp; CLI](management-and-cli.md).\\n- **LLM-facing management tools** (`engagement_create`, `engagement_query`, `scope_define`, etc.) that the agent calls autonomously during sessions.\\n\\n## Project Layout\\n\\n```\\nsrc/\\n\u251c\u2500\u2500 argicida/argicida/         # Host-side Hermes plugin\\n\u2502   \u251c\u2500\u2500 registration/          # Five-phase plugin startup\\n\u2502   \u251c\u2500\u2500 hooks/                 # Hook pipeline (scope, evidence, context)\\n\u2502   \u251c\u2500\u2500 tools/                 # Scan tool framework + definitions\\n\u2502   \u251c\u2500\u2500 pod/                   # Pod lifecycle + dual-bridge topology\\n\u2502   \u251c\u2500\u2500 ipc/                   # Unix socket client\\n\u2502   \u251c\u2500\u2500 db/                    # EngagementStore + sub-repos\\n\u2502   \u251c\u2500\u2500 triage/                # Finding scorer + deduplicator\\n\u2502   \u251c\u2500\u2500 evidence/              # Hash-linked evidence chain\\n\u2502   \u2514\u2500\u2500 zap/                   # ZAP sidecar integration\\n\u251c\u2500\u2500 argicida_agent/            # Container agent (thin executor)\\n\u2514\u2500\u2500 argicida_common/           # Shared msgspec types\\ncontainers/                    # Containerfiles for tools, gateway, ZAP\\nprofile/                       # Hermes pentest profile + LLM skills\\ntests/                         # Unit, pod_integration, and E2E suites\\n```\\n\\n## Getting Started\\n\\n**Prerequisites:** Python 3.13+, [uv](https://docs.astral.sh/uv/), rootless Podman, and [Hermes Agent](https://hermes-agent.nousresearch.com/) installed.\\n\\n```bash\\n# Install dependencies\\nuv sync\\n\\n# Build container images\\npodman build -f containers/Containerfile -t argicida-tools:dev .\\npodman build -f containers/Containerfile.gateway -t argicida-gateway:dev .\\npodman build --network=private -f containers/Containerfile.zap -t argicida-zap:dev .\\n\\n# Install the Hermes profile\\nhermes profile install ./profile --alias\\n\\n# Run a pentest session\\npentest chat\\n```\\n\\nRun the test suite with `uv run pytest`. Pod integration tests (`-m pod_integration`) require built images and Podman. E2E tests (`-m e2e`) additionally require a running LLM endpoint. See [Container Build Infrastructure](container-build-infrastructure.md) for image details and the [Hermes Profile &amp; Skills](hermes-profile-and-skills.md) page for profile configuration.\\n\\n## Key Design Decisions\\n\\n- **No shell execution** \u2014 every subprocess uses `create_subprocess_exec` with argument lists. No `shell=True`, anywhere.\\n- **LLM-driven, not pipelined** \u2014 the LLM chooses tools based on what it discovers. [Skills](hermes-profile-and-skills.md) provide technique knowledge on demand, but never force a fixed sequence.\\n- **Lossless evidence** \u2014 every tool invocation's raw output is stored to disk with `O_NOFOLLOW` permissions. The [evidence chain](evidence-and-reporting.md) provides cryptographic integrity verification. Nothing is discarded.\\n- **Fail-closed isolation** \u2014 the tool pod cannot reach the internet without the gateway. If the gateway dies, ARP resolution fails and all egress stops.\\n- **Air-gap capable** \u2014 threat intelligence (EPSS scores, CISA KEV catalog) loads from local files. No runtime calls to external scoring APIs.\",\"plugin-registration-core\":\"# Plugin Registration &amp; Core\\n\\n# Plugin Registration &amp; Core\\n\\nThe entry point for Argicida as a Hermes plugin. This module handles the five-phase registration sequence that wires tools, hooks, and commands into the Hermes agent framework, and provides the shared state layer that all runtime components access throughout a session.\\n\\n## How Registration Works\\n\\nHermes discovers Argicida through the `hermes_agent.plugins` pip entry point, imports the `argicida` module, and calls `register(ctx)`. That function delegates to five phase modules in strict order:\\n\\n```mermaid\\ngraph LR\\n    H[Hermes] --&gt;|calls| R[register]\\n    R --&gt;|Phase 1| S[init_state]\\n    R --&gt;|Phase 2| ST[register_scan_tools]\\n    R --&gt;|Phase 3| MT[register_management_tools]\\n    R --&gt;|Phase 4| HK[register_hooks]\\n    R --&gt;|Phase 5| CM[register_commands]\\n\\n    S --&gt;|creates| RC[RegistrationContext]\\n    RC -.-&gt;|passed to| ST &amp; MT &amp; HK &amp; CM\\n```\\n\\nThe ordering matters. Phase 2 builds the `ServiceSkillRegistry` and populates `reg.tool_defs`, which Phase 3 and Phase 4 depend on. Phase 4 raises `RuntimeError` if `service_registry` is still `None`.\\n\\n### Phase 1 \u2014 `init_state` (`registration/state.py`)\\n\\nCreates every shared service and returns a fully populated `RegistrationContext`:\\n\\n- **Filesystem**: creates `$HERMES_HOME/argicida/{data,keys,engagements,artifacts/unscoped}` with `0o700` permissions\\n- **Database**: loads or generates an encryption key, opens `EngagementStore` (APSW/SQLite)\\n- **Cryptography**: generates an Ed25519 signing keypair for evidence chain signatures\\n- **Triage data**: loads EPSS scores and KEV catalog from disk (degrades gracefully if absent \u2014 used for air-gapped engagements)\\n- **Pod infrastructure**: creates a `PodManager` (Podman API wrapper)\\n- **Container images**: reads `ARGICIDA_TOOL_IMAGE`, `ARGICIDA_GATEWAY_IMAGE`, `ARGICIDA_ZAP_IMAGE`, `ARGICIDA_NETWORK` from environment, with sensible `:dev` defaults\\n\\nAll heavy imports are deferred inside the function body to keep module-level imports fast.\\n\\n### Phase 2 \u2014 `register_scan_tools` (`registration/scan_tools.py`)\\n\\nAuto-discovers tool definitions from `tools/defs/` via `discover_tool_defs()`. For each definition, it:\\n\\n1. Extracts `ToolProfile` scope configs and registers them with the scope validator\\n2. Builds alias mappings (e.g., `nmap` \u2192 `nmap`, `db_query` \u2192 `usql`) for the scope guard and normalizer\\n3. Registers parsers, transformers, error hints, and auth indicators into their respective global registries\\n4. Wires the `NseScriptMatcher` normalizer's lazy script getter to the live `PodSession`\\n5. Creates a generic scan handler via `make_scan_handler()` and registers each tool with `ctx.register_tool()`\\n6. Builds the `ServiceSkillRegistry` from profile skill frontmatter and stores it on `reg`\\n\\nThe `_make_context` closure captures `reg` to provide `NormalizeContext` (IPC dir path, DNS server) on each tool invocation.\\n\\n### Phase 3 \u2014 `register_management_tools` (`registration/management_tools.py`)\\n\\nRegisters 18 non-scan tools: engagement CRUD, scope management, triage, artifacts, screenshots, and 7 ZAP tools. Each tool gets a handler factory that closes over the relevant `RegistrationContext` accessors. Returns the count for the startup banner.\\n\\nAlso wires `set_zap_active_fn()` so the `ZapProxyInjector` normalizer can check whether ZAP is running without importing pod session types.\\n\\n### Phase 4 \u2014 `register_hooks` (`registration/hooks.py`)\\n\\nRegisters 10 hooks across 8 Hermes hook points:\\n\\n| Hook point | Callback | Purpose |\\n|---|---|---|\\n| `on_session_start` | `make_session_start_adapter` | Session ID assignment, engagement auto-resume, data reload |\\n| `pre_tool_call` | `make_scope_guard_adapter` | Scope enforcement on every tool invocation |\\n| `post_tool_call` | `make_evidence_adapter` | Evidence chain append, finding extraction with EPSS/KEV enrichment |\\n| `post_tool_call` | `make_output_store_adapter` | Lossless raw output storage to disk |\\n| `transform_tool_result` | `make_normalizer_adapter` | OCSF normalization, asset extraction, scope expansion |\\n| `subagent_start/stop` | inline lambdas | Delegation counter for concurrent child tracking |\\n| `pre_llm_call` | `make_context_adapter` | Engagement state, skills menu, quick-reference injection into LLM context |\\n| `on_session_end` | `make_session_end_adapter` | Per-turn evidence signing |\\n| `on_session_finalize` | `make_session_finalize_adapter` | Pod teardown, final evidence signing, ZAP finding persistence |\\n\\nThe `pre_llm_call` hook is the most complex \u2014 it loads methodology text and skills menu at registration time (once), then builds dynamic context each turn from engagement state, asset summaries, ZAP status, and delegation tracking.\\n\\n### Phase 5 \u2014 `register_commands` (`registration/commands.py`)\\n\\nRegisters 6 slash commands (`/scope`, `/triage`, `/approve-gate`, `/briefing`, `/engage`, `/trace`) and the `hermes argicida` CLI subcommand tree. The `/trace` command conditionally captures `ctx.dispatch_tool` for tool invocation through the registry with parent agent context.\\n\\n## PluginState and RegistrationContext\\n\\nTwo dataclasses partition mutable runtime state from shared services:\\n\\n**`PluginState`** (`__init__.py`) is a mutable bag of session-scoped values that change throughout a session:\\n\\n- `pod_session` \u2014 owns all topology resources (pod, gateway, networks, temp dirs); set by `ensure_pod()`, cleared on teardown\\n- `engagement_session` \u2014 owns engagement-scoped resources (artifacts dir, signing key); set by engagement create/resume\\n- `zap_manager` \u2014 ZAP coordinator; created when ZAP healthcheck passes, cleared on finalize\\n- `signing_key` / `public_key` \u2014 session-level Ed25519 keypair (engagement sessions may override the signing key)\\n- `active_delegation_count` / `_delegation_tracking` \u2014 subagent concurrency tracking\\n\\n**`RegistrationContext`** (`registration/context.py`) holds all services and provides accessor methods that replace what were previously closures inside `register()`. It is created once in Phase 1 and passed to every subsequent phase.\\n\\nThe accessor methods on `RegistrationContext` encapsulate the indirection between what hooks/handlers need and where the data actually lives:\\n\\n- `get_eid()` \u2014 extracts engagement ID from the current `EngagementSession`\\n- `get_client()` \u2014 returns a per-thread `IpcClient`, creating one if needed, detecting stale connections\\n- `get_scope()` \u2014 fetches the current scope definition from the engagement database\\n- `ensure_pod()` \u2014 thread-safe pod creation with orphan cleanup and stale session recovery\\n- `get_ipc_dir()`, `get_artifacts_dir()`, `get_gateway_name()`, `get_tool_container_name()` \u2014 topology resource accessors\\n\\n### Thread Safety\\n\\nTwo mechanisms handle concurrent access from Hermes's parallel tool dispatch:\\n\\n1. **`_pod_lock`** (`threading.Lock`) \u2014 prevents concurrent pod creation races in `ensure_pod()`. The lock covers the full lifecycle: check if alive \u2192 clean stale \u2192 remove orphans \u2192 launch \u2192 enumerate NSE scripts \u2192 initialize ZAP.\\n\\n2. **`_thread_local`** (`threading.local`) \u2014 gives each worker thread its own `IpcClient` instance. Hermes dispatches parallel tool calls on separate threads with independent event loops; sharing an asyncio-stream-backed client across loops would fail. `get_client()` validates the cached client against the current socket path and session key before reusing it.\\n\\n### Pod Lifecycle in `ensure_pod()`\\n\\nThe method handles five scenarios inside the pod lock:\\n\\n1. **Pod exists and alive** \u2014 returns immediately\\n2. **Pod exists but dead** \u2014 tears down stale session, clears ZAP manager, falls through to relaunch\\n3. **Orphaned topology** \u2014 finds and removes pods matching the expected naming convention before launching\\n4. **Fresh launch** \u2014 calls `launch_pod()`, enumerates NSE scripts from the tool container, initializes `ZapManager` if ZAP is active\\n5. **Launch failure** \u2014 logs warning, returns without setting `pod_session` (tools will get error responses)\\n\\n## Utilities (`utils.py`)\\n\\nSmall pure functions used across the codebase:\\n\\n- `utc_now()` \u2014 ISO 8601 timestamp, used by the evidence chain, output store, and DB operations\\n- `normalize_target(target)` \u2014 reduces URLs, `IP:port`, UNC paths, and IPv6 brackets to bare hostnames/IPs for scope validation\\n- `is_argicida_tool(name)` \u2014 prefix check for hook filtering\\n- `has_flag(args, flags)` \u2014 `--flag=value`-aware presence check (replaces bare `in` checks that miss the `=` form)\\n- `iter_flag_values(args, flags)` \u2014 yields `(index, flag, value)` tuples for flag-value pair scanning\\n\\n## Adding to Registration\\n\\n**New scan tool**: add a definition module to `tools/defs/`. Phase 2 discovers it automatically \u2014 no registration code changes needed.\\n\\n**New management tool**: add the schema to `tools/schemas.py`, the handler factory to `tools/management_handlers.py`, wire it in `register_management_tools()`, and bump `_N_MANAGEMENT_TOOLS`.\\n\\n**New hook**: add the adapter factory, wire it in `register_hooks()`. Hermes hooks are synchronous \u2014 use `def`, not `async def`.\\n\\n**New slash command**: add the handler factory to `commands/slash.py`, wire it in `register_commands()`. Do not reuse Hermes built-in names (`/approve`, `/help`, `/clear`, `/new`, `/reset`, `/plugins`, `/config`).\\n\\n**New accessor on RegistrationContext**: add the method, then pass it (as a bound method reference or lambda) to whatever handler/adapter factory needs it. Avoid storing `reg` itself in handler closures \u2014 the accessor pattern keeps the dependency explicit.\",\"pod-management\":\"# Pod Management\\n\\n# Pod Management (`argicida/pod/`)\\n\\nThis module creates and manages the isolated Podman infrastructure that scan tools run inside. Every tool invocation happens in a container \u2014 never on the host \u2014 and all container-to-internet traffic flows through a gateway that enforces scope via nftables.\\n\\n## Dual-Bridge Topology\\n\\nThe core abstraction is a **dual-bridge network topology**: a tool pod on an internal-only network, a standalone gateway container bridging internal and external networks, and the gateway as the sole route to the internet.\\n\\n```mermaid\\ngraph LR\\n    Host[\\\"Host (Hermes + Argicida)\\\"]\\n    Host --&gt;|Unix socket /ipc| ToolPod\\n    subgraph Internal Network10.89.1.0/24 --internal --disable-dns\\n        ToolPod[\\\"Tool Pod10.89.1.xnmap, nuclei, etc.\\\"]\\n    end\\n    ToolPod --&gt;|\\\"default route10.89.1.2\\\"| Gateway\\n    subgraph Gateway[\\\"Gateway Container10.89.1.2\\\"]\\n        NFT[nftables FORWARD chain]\\n        DNS[dnsmasq]\\n    end\\n    Gateway --&gt;|NAT masquerade| ExtNet[\\\"External Networkor ARGICIDA_NETWORK\\\"]\\n    ExtNet --&gt; Internet[\\\"Target / Internet\\\"]\\n```\\n\\nThe tool pod has no direct external connectivity. Its `resolv.conf` points to the gateway (`10.89.1.2`), and its default route goes through the gateway. If the gateway dies, ARP resolution for `10.89.1.2` fails \u2014 **fail-closed by design**.\\n\\n## Module Layout\\n\\n| File | Responsibility |\\n|------|---------------|\\n| `config.py` | Frozen `msgspec.Struct` specs (`PodSpec`, `GatewaySpec`, `ContainerSpec`) and `build_topology()` |\\n| `manager.py` | `PodManager` \u2014 thread-safe wrapper around rootless Podman SDK + CLI |\\n| `launcher.py` | `launch_pod()` \u2014 orchestrates the 8-step startup sequence |\\n| `session.py` | `PodSession` \u2014 RAII-style owner of all topology resources |\\n| `builder.py` | `build_image()` \u2014 podman build for tool and gateway images |\\n| `vpn.py` | VPN endpoint extraction from WireGuard/OpenVPN configs |\\n\\n## Startup Sequence\\n\\n`launch_pod()` in `launcher.py` is the single entry point. It returns a `PodSession` or raises, cleaning up partial resources on failure.\\n\\n**Steps:**\\n\\n1. **Create internal network** \u2014 `10.89.1.0/24`, `--internal`, `--disable-dns`, default route to `10.89.1.2` (gateway)\\n2. **Determine external network** \u2014 uses `ARGICIDA_NETWORK` env var if set, otherwise creates a managed one (`argicida-ext-{eid}`)\\n3. **Write scope files** \u2014 nftables rules, dnsmasq config, and `resolv.conf` into a host-owned `scope_dir` (not `ipc_dir` \u2014 see [Filesystem Zones](#filesystem-zones))\\n4. **Build topology specs** \u2014 `build_topology()` returns frozen `PodSpec` + `GatewaySpec`\\n5. **Start gateway** \u2014 created and started first; it must be reachable before the tool pod boots\\n6. **Wait for gateway** \u2014 polls `nft list chain inet argicida forward` until the FORWARD chain is active\\n7. **Start tool pod** \u2014 created on the internal network; includes ZAP sidecar if configured\\n8. **Wait for readiness** \u2014 polls for the agent Unix socket; waits for ZAP API if ZAP image is provided\\n\\nIf any step fails, the `except` block tears down everything created so far \u2014 either via `PodSession.teardown()` if one was constructed, or via `PodManager.cleanup_topology_by_name()` for partial launches.\\n\\n## Naming Convention\\n\\nAll resource names derive deterministically from `pod_eid`:\\n\\n| Resource | Name |\\n|----------|------|\\n| Tool pod | `argicida-{pod_eid}` |\\n| Tool container | `argicida-{pod_eid}-tool` |\\n| Gateway container | `argicida-{pod_eid}-gateway` |\\n| ZAP container | `argicida-{pod_eid}-zap` |\\n| Internal network | `argicida-int-{pod_eid[:12]}` |\\n| External network (managed) | `argicida-ext-{pod_eid[:12]}` |\\n\\nFor engagements, `pod_eid` is the engagement ID. For diagnostic pods (no engagement), it's `diag-{random_hex}`.\\n\\nThis convention makes `cleanup_topology_by_name()` possible \u2014 given just a `pod_eid`, every resource can be located and removed without tracking state.\\n\\n## PodManager\\n\\n`PodManager` (`manager.py`) wraps the Podman SDK and CLI behind a thread-safe interface. A single `threading.Lock` serializes all Podman operations.\\n\\n**Why both SDK and CLI?** The Podman Python SDK (`podman-py`) doesn't support `--route` on network creation, so network operations use `subprocess.run([\\\"podman\\\", ...])`. Pod and container lifecycle uses the SDK via `self._client`.\\n\\nKey methods:\\n\\n- **`create_pod_sync(spec, ipc_dir)`** \u2014 creates a pod with all its containers atomically; rolls back (removes the pod) if any container creation fails\\n- **`create_container_sync(spec)`** \u2014 creates standalone containers (used for the gateway)\\n- **`exec_sync(container_name, cmd)`** \u2014 runs a command inside a container, returns `(exit_code, stdout_bytes)`\\n- **`cleanup_topology_by_name(pod_eid)`** \u2014 idempotent removal of all resources for an engagement; silently skips anything already gone\\n- **`find_pod_by_name(name_prefix)`** \u2014 finds a running pod by prefix, used by `ensure_pod()` to reconnect to an existing session\\n\\n## PodSession\\n\\n`PodSession` (`session.py`) is a dataclass that owns every resource created during launch: pod ID, temp directories, socket paths, session keys, container names, and the scope update callback.\\n\\n**Thread-safe host entries** \u2014 `add_host_entry()`, `remove_host_entry()`, and `get_host_entries()` manage hostname-to-IP mappings protected by a lock. These feed into dnsmasq config regeneration when scope changes.\\n\\n**`teardown(pod_manager)`** performs cleanup in order:\\n\\n1. Exec into the tool container as root to clear the IPC mount (resets UID ownership \u2014 see [Filesystem Zones](#filesystem-zones))\\n2. Fall back to `podman unshare rm -rf` if the exec fails (container already dead)\\n3. Call `cleanup_topology_by_name()` to remove pod, gateway, and networks\\n4. Clean up both temp directories (`ipc_tmpdir`, `scope_tmpdir`)\\n\\n## Filesystem Zones\\n\\nThree directories with different ownership rules \u2014 getting this wrong causes `PermissionError` across the UID namespace boundary.\\n\\n| Zone | Mount flags | Owner after pod start | Host can write? | Container (UID 10000) can read? |\\n|------|------------|----------------------|-----------------|-------------------------------|\\n| `ipc_dir` | `U=True, chown=True` | Container UID 10000 | New files only (as UID 0 inside container) | Host files: only via `o+r` (use `0o644`) |\\n| `scope_dir` | No U/chown | Host UID 1000 | Yes | Read-only bind mount |\\n| Output dirs | Not mounted | Host UID 1000 | Yes | N/A |\\n\\n**Why two directories?** The `U/chown` mount shifts ownership of everything in `ipc_dir` to the container's UID namespace. If scope rules lived there, the host couldn't overwrite them when scope changes. `scope_dir` stays host-owned so `update_scope_rules()` can rewrite nftables rules and dnsmasq config at any time.\\n\\n## Live Scope Updates\\n\\n`_make_scope_updater()` returns a closure captured by `PodSession.update_scope_rules`. When the engagement scope changes (e.g., new CIDRs added via `scope_define`), this callback:\\n\\n1. Rewrites the nftables ruleset file in `scope_dir`\\n2. Reloads rules in the gateway via `nft -f /etc/argicida/scope-rules.nft`\\n3. Regenerates the dnsmasq config with current host entries\\n4. Restarts dnsmasq in the gateway (`restart_dnsmasq()` kills all dnsmasq processes, then starts fresh)\\n\\nNo pod restart required \u2014 the gateway reloads rules in place.\\n\\n## Topology Specs (`config.py`)\\n\\n`build_topology()` is a pure function: engagement parameters in, frozen specs out. It returns `(PodSpec, GatewaySpec)` \u2014 no Podman calls, no side effects.\\n\\nThe tool container gets:\\n- `CAP_NET_RAW` (SYN scanning), read-only rootfs, 4GB memory limit, 512 PID limit\\n- `init=True` (catatonit PID 1 for zombie reaping \u2014 matters for Chromium)\\n- tmpfs at `/tmp` and `/home/agent`, IPC bind mount at `/ipc`\\n- ZAP CA cert bind-mounted if ZAP is active\\n\\nThe gateway gets:\\n- `CAP_NET_ADMIN` + `CAP_NET_RAW`, `net.ipv4.ip_forward=1` sysctl\\n- Static IP `10.89.1.2` on the internal network\\n- Scope rules, dnsmasq config as read-only bind mounts\\n\\nThe ZAP sidecar (optional) runs inside the tool pod, sharing its network namespace. It binds to `0.0.0.0:8090` \u2014 only reachable within the pod.\\n\\n## VPN Support\\n\\n`vpn.py` extracts the VPN server endpoint (IP, port, protocol) from WireGuard or OpenVPN config files. The launcher uses this to add nftables allow rules so tunnel establishment traffic can pass through the gateway's FORWARD chain before the tunnel is up.\\n\\nOnly IP-literal endpoints are supported \u2014 hostnames are rejected because DNS may not be available before the tunnel establishes. If extraction fails, the launcher logs a warning but continues (the tunnel may still work if scope rules are permissive enough).\\n\\n## Image Building\\n\\n`build_image()` (`builder.py`) wraps `podman build` for the two image types: `tool` (main Containerfile) and `gateway` (Containerfile.gateway). Called from the CLI via `hermes argicida build`. Returns `(success, output)` \u2014 no exceptions on build failure.\\n\\n## Integration Points\\n\\n`launch_pod()` is called from two places:\\n\\n- **`RegistrationContext.ensure_pod()`** \u2014 on-demand launch when the first tool call arrives; runs via `asyncio.to_thread()` to avoid blocking the event loop\\n- **`_dispatch_pod` (CLI)** \u2014 direct pod launch from `hermes argicida pod` command\\n\\n`PodSession.teardown()` is called from:\\n\\n- **`on_session_finalize` hook** \u2014 when the Hermes session ends\\n- **E2E test harness** \u2014 `_sweep_pods()` uses `cleanup_topology_by_name()` for cleanup\\n- **`launch_pod()` itself** \u2014 on failure, to clean up partial resources\",\"scan-tool-framework\":\"# Scan Tool Framework\\n\\n# Scan Tool Framework\\n\\nThe scan tool framework turns tool definitions into live Hermes-registered tools with a single generic handler. Each tool is a Python module in `tools/defs/` that declares what it wraps (binary name, schema, scope profile, normalizers). The framework discovers these modules at import time, validates them, builds async handlers, and registers them into Hermes \u2014 no manual wiring required.\\n\\n## How a tool call flows\\n\\n```mermaid\\ngraph TD\\n    LLM[\\\"LLM tool call\\\"] --&gt; H[\\\"scan_handler decorator(error boundary)\\\"]\\n    H --&gt; EC[\\\"ensure_client()(pod on demand)\\\"]\\n    EC --&gt; N[\\\"Normalizer pipeline(fix args, inject defaults)\\\"]\\n    N --&gt;|rejection| ERR[\\\"JSON error + guidance\\\"]\\n    N --&gt;|ok| RT[\\\"run_tool()(IPC dispatch)\\\"]\\n    RT --&gt; IPC[\\\"_send_tool_command()(Unix socket)\\\"]\\n    IPC --&gt; AGENT[\\\"Container agent(create_subprocess_exec)\\\"]\\n    AGENT --&gt; RESP[\\\"IPC response\\\"]\\n    RESP --&gt; JSON[\\\"JSON result + corrections\\\"]\\n```\\n\\nThe LLM sends `{\\\"target\\\": \\\"10.10.10.5\\\", \\\"args\\\": [\\\"-p\\\", \\\"80,443\\\"]}`. The handler extracts `target` (for scope validation/metadata) and `args` (the CLI flags), runs normalizers to fix common mistakes and inject defaults, then dispatches the corrected command over Unix socket IPC to the container agent. The agent runs the binary via `create_subprocess_exec` and streams output back.\\n\\n## Tool definitions (`tools/defs/`)\\n\\nEach tool is a single module (e.g., `nmap.py`, `naabu.py`) exporting a fixed set of attributes:\\n\\n| Export | Type | Purpose |\\n|--------|------|---------|\\n| `SCHEMA` | `dict` | OpenAI-format tool schema for Hermes registration |\\n| `BINARY_NAME` | `str` | Container binary to execute (e.g., `\\\"nmap\\\"`, `\\\"usql\\\"`) |\\n| `TOOLSET` | `str` | Hermes toolset grouping (e.g., `\\\"argicida-network\\\"`) |\\n| `SCOPE_PROFILE` | `ToolProfile` | Blocked/target/value flag classification for the scope guard |\\n| `NORMALIZERS` | `tuple` | Ordered normalizer pipeline |\\n| `TIMEOUT_S` | `int` | IPC timeout in seconds (default 300) |\\n| `ERROR_HINTS` | `tuple[tuple[str, str], ...]` | Pattern \u2192 hint pairs for actionable error messages |\\n| `PARSER` | callable or `None` | Extracts structured findings from raw output |\\n| `TRANSFORMER` | callable or `None` | Reshapes tool output for LLM context efficiency |\\n| `EXTRACTOR` | callable or `None` | Discovers assets (IPs, hostnames, ports) from results |\\n| `AUTH_INDICATORS` | `AuthIndicators` or `None` | Classifies auth success/failure in tool output |\\n\\n### Adding a new tool\\n\\nCopy an existing def module. For a simple scanner, start from `naabu.py`. For a tool with connection URLs and query parameters, start from `db_query.py`. Change the values. Registration is automatic \u2014 `discover_tool_defs()` finds all non-underscore modules in the `tools/defs/` package via `pkgutil.iter_modules` and validates each one against the required exports.\\n\\nThe only code you touch:\\n\\n1. `tools/defs/.py` \u2014 the definition\\n2. `tools/parsers_.py` \u2014 finding extraction (if the tool produces parseable output)\\n3. `tools/transformers_.py` \u2014 LLM context shaping (if raw output is too verbose)\\n4. `tools/extractors_.py` \u2014 asset discovery (if the tool reveals IPs/hostnames/ports)\\n5. `tools/normalizers_.py` \u2014 tool-specific normalizers (if the tool has unique arg patterns)\\n\\nYou do NOT touch `__init__.py`, `schemas.py`, `scope/validator.py`, or any registration code.\\n\\n### Discovery and validation\\n\\n`discover_tool_defs()` (`tools/defs/__init__.py`) returns a sorted tuple of `ToolDef` dataclasses. It validates every required export's type and checks that `SCHEMA` contains `name` and `description` keys. A malformed module raises `TypeError` or `ValueError` at import time, not at runtime.\\n\\n```python\\n@dataclasses.dataclass(frozen=True)\\nclass ToolDef:\\n    schema: dict[str, Any]\\n    binary_name: str\\n    toolset: str\\n    scope_profile: ToolProfile\\n    normalizers: tuple[Any, ...]\\n    timeout_s: int = 300\\n    error_hints: tuple[tuple[str, str], ...] = ()\\n    parser: Any | None = None\\n    transformer: Any | None = None\\n    extractor: Any | None = None\\n    auth_indicators: Any | None = None\\n```\\n\\n## Handler factory (`make_scan_handler`)\\n\\n`make_scan_handler()` builds a single generic async handler from any `ToolDef`. Every scan tool shares this handler \u2014 there are no per-tool handler functions. The factory takes four callables that bind the handler to runtime state:\\n\\n- `get_client` \u2014 returns the current `IpcClient` or `None`\\n- `ensure_pod` \u2014 launches a pod if none exists\\n- `get_context` \u2014 returns `NormalizeContext` with current IPC dir and DNS server\\n\\nThe generated handler:\\n\\n1. Calls `ensure_client()` to get an IPC connection, launching the pod on demand if needed\\n2. Extracts `target` and `args` from the tool call dict\\n3. Splits concatenated args (LLMs sometimes leak JSON array syntax into a single string: `'-o\\\", \\\"/tmp/file\\\"'` \u2192 `['-o', '/tmp/file']`)\\n4. Runs the normalizer pipeline\\n5. If normalizers reject: returns a JSON error with guidance\\n6. Calls `run_tool()` to dispatch over IPC\\n7. Cleans up any temporary IPC files created by normalizers\\n8. Appends `corrected_args` to the result if normalizers made corrections\\n\\nThe `scan_handler` decorator wraps the handler with uniform error handling \u2014 `ToolError` and general exceptions both produce JSON error responses instead of crashing.\\n\\n## Base infrastructure (`base.py`)\\n\\n### `ensure_client(get_client, ensure_pod)`\\n\\nGets the IPC client, launching a pod on demand. If `get_client()` returns `None`, calls `ensure_pod()` on a thread (via `asyncio.to_thread` \u2014 the Podman API is synchronous) and retries once. Raises `ToolError` with a diagnostic message if both attempts fail.\\n\\n### `run_tool(client, tool, target, args, timeout_s)`\\n\\nThe generic dispatcher. Sends an `IpcCommand` over the Unix socket and returns raw output as a JSON string containing `tool`, `target`, `request_id`, `raw_output`, `exit_code`, and `stderr`. The `target` field is metadata only \u2014 it is NOT appended to the command args.\\n\\n### `_send_tool_command(client, tool, args, request_id, timeout_s)`\\n\\nConstructs an `IpcCommand`, sends it via the client, and raises `ToolError` with an actionable hint if the response contains an error. Hint matching (`_ipc_error_hint`) pattern-matches against common IPC failures: command not found, timeout, connection refused, permission denied, file not found.\\n\\n### `transfer_from_container(pm, container_name, source_path, ipc_dir)`\\n\\nFile transfer across UID namespace boundaries. Copies a file from the container to `/ipc/` via `podman exec`, sets permissions to 644 (so the host can read across the rootless Podman UID mapping), reads the bytes, and cleans up the transfer file.\\n\\n## Normalizer pipeline\\n\\nNormalizers intercept and correct LLM argument mistakes before IPC dispatch. They implement the `ArgNormalizer` protocol:\\n\\n```python\\nclass ArgNormalizer(Protocol):\\n    def normalize(self, args: tuple[str, ...], ctx: NormalizeContext) -&gt; NormalizeResult: ...\\n```\\n\\n`NormalizeResult` carries the (possibly modified) args, corrections made, rejections with guidance, an optional binary override, and cleanup file paths. `CompositeNormalizer` chains components in order, accumulating corrections and short-circuiting on the first rejection.\\n\\n### Generic normalizer components\\n\\n| Component | Purpose |\\n|-----------|---------|\\n| `TargetInjector` | Places target into args based on mode: `last` (nmap), `flag` (naabu `-host`), `flag_url` (nuclei `-u`), `after_mode_flag` (rpcinfo) |\\n| `FlagInjector` | Injects default flags when absent (e.g., `-json`, `-Pn`, `-n`) |\\n| `BlockedChecker` | Rejects args containing specific flags with guidance |\\n| `FileInjector` | Writes a tool_args parameter to an IPC file and injects a CLI flag pointing to it |\\n| `TargetFileInjector` | Writes `ctx.target` to an IPC file (used by dnsx which reads from `-l`) |\\n| `DnsServerInjector` | Injects engagement DNS server via a flag (e.g., `-r`) |\\n| `RequiredArgsChecker` | Rejects when args are empty |\\n| `ConflictChecker` | Rejects when a parameter conflicts with a CLI flag |\\n| `UriRequiredChecker` | Rejects when no connection URL is present in args |\\n| `FlagValueGuard` | Scans flag-value pairs for blocked content (destructive SQL, etc.) |\\n\\n### Tool-specific normalizers\\n\\n| Module | Key normalizer | What it does |\\n|--------|---------------|--------------|\\n| `normalizers_nmap.py` | `NseScriptMatcher` | Fuzzy-matches `--script` names against the container's 518-script NSE inventory. Auto-corrects close matches (score gap &gt;= 0.1), rejects ambiguous matches with suggestions, blocks exploitation/brute-force scripts |\\n| `normalizers_nuclei.py` | `NucleiDefaultSeverity` | Injects `-severity critical,high,medium` when no template filter is present (prevents running all ~13k templates) |\\n| `normalizers_nuclei.py` | `NucleiProtocolEnforcer` | Auto-injects or strips `-nh` based on whether the URL has an HTTP scheme |\\n| `normalizers_curl.py` | `CurlPathGuard` | Restricts `-o` to `/tmp/` and blocks reads from sensitive paths |\\n| `normalizers_curl.py` | `CurlAuthUrlFixer` | Fixes `-u URL` when the model confuses curl's auth flag with httpx's URL flag |\\n| `normalizers_db.py` | `DbSqlGuard` | Blocks destructive SQL and usql meta-commands |\\n| `normalizers_redis.py` | `RedisCommandGuard` | Blocks destructive Redis commands (FLUSHALL, CONFIG SET, etc.) |\\n| `normalizers_smb.py` | `SmbPathNormalizer` | Auto-prepends `//IP/SHARE` from target when missing |\\n| `normalizers_ssh.py` | `SshHostKeyInjector` | Injects `-o StrictHostKeyChecking=no` for non-interactive auth |\\n| `normalizers_zap.py` | `ZapProxyInjector` | Routes web tool traffic through ZAP for passive analysis |\\n\\n### NormalizeContext\\n\\nCarries environment state through the pipeline:\\n\\n```python\\nclass NormalizeContext(msgspec.Struct, frozen=True):\\n    target: str = \\\"\\\"       # scope-validation target (IP/hostname/URL)\\n    tool_args: dict = {}   # full tool call dict (for named params like `query`, `body`)\\n    ipc_dir: str = \\\"\\\"      # path to IPC bind mount (for file injection)\\n    dns_server: str = \\\"\\\"   # engagement DNS server (for auto-injection)\\n```\\n\\n## Schemas (`schemas.py`)\\n\\n`_scan_schema()` builds the standard flexible-args schema that every scan tool uses:\\n\\n```python\\n{\\n    \\\"name\\\": \\\"argicida_\\\",\\n    \\\"parameters\\\": {\\n        \\\"properties\\\": {\\n            \\\"target\\\": {\\\"type\\\": \\\"string\\\", \\\"description\\\": \\\"...\\\"},\\n            \\\"args\\\": {\\\"type\\\": \\\"array\\\", \\\"items\\\": {\\\"type\\\": \\\"string\\\"}, \\\"description\\\": \\\"...\\\"},\\n            # ... extra_properties for tools with named params (body, query, key, password)\\n        },\\n        \\\"required\\\": [\\\"args\\\"]\\n    }\\n}\\n```\\n\\nThe `target` field is for scope validation and metadata. The `args` array contains CLI flags as separate elements. Tools that need additional named parameters (db_query's `query`, curl's `body`, ssh_exec's `password` and `key`) add them via `extra_properties`.\\n\\nManagement tool schemas (engagement_create, scope_define, engagement_query, ZAP tools, etc.) are hand-written dicts in the same file. They don't go through `make_scan_handler` \u2014 they have dedicated handlers in `management_handlers.py`.\\n\\n## Tool classification sets\\n\\n`tools/defs/__init__.py` maintains canonical sets that hooks use to decide how to handle each tool:\\n\\n- **`get_non_evidence_tools()`** \u2014 management/infrastructure tools that don't produce forensic evidence (skip evidence chain)\\n- **`get_scope_exempt_tools()`** \u2014 tools that don't need scope validation (management tools + tracepath)\\n- **`get_perceptor_skip_tools()`** \u2014 tools whose output doesn't benefit from LLM-based finding extraction (management tools + tracepath, rpcinfo, katana, ffuf, curl)\\n\\nThese are the single source of truth. Hooks import from here instead of maintaining their own frozensets.\\n\\n## ProjectDiscovery tool conventions\\n\\nPD tools (naabu, nuclei, httpx, subfinder, dnsx, katana) share a goflags convention where `-flag` and `--flag` are interchangeable. `_pd_flags()` from `scope/validator.py` auto-expands short forms into both variants. Shared blocked-flag constants live in `tools/defs/_pd_common.py`:\\n\\n```python\\nPD_BLOCKED_CONFIG = _pd_flags(\\\"-config\\\")      # no config file override\\nPD_BLOCKED_OUTPUT = _pd_flags(\\\"-o\\\", \\\"-output\\\") # no file output (IPC only)\\nPD_BLOCKED_LIST   = _pd_flags(\\\"-l\\\", \\\"-list\\\")   # no target list files\\n```\\n\\n## Post-dispatch processing\\n\\nThe framework supports three optional post-dispatch processors declared in tool defs:\\n\\n**Parsers** (`PARSER`) extract structured findings from raw output. Each parser receives a `ParseContext` with the raw output and tool args, and returns findings in the OCSF-normalized schema. Tool-specific parsers live in `tools/parsers_.py` and use shared helpers from `tools/parsers.py` (`make_finding`, `iter_jsonl`, `utc_timestamp`).\\n\\n**Transformers** (`TRANSFORMER`) reshape tool output for LLM context efficiency. Raw nmap XML becomes a service table. Raw nuclei JSONL becomes a severity-grouped summary. Transformers run via the `transform_tool_result` hook, not inside the handler. The `with_raw_fallback` decorator in `tools/transformers.py` catches transformer errors and falls back to truncated raw output.\\n\\n**Extractors** (`EXTRACTOR`) discover assets (IPs, hostnames, ports, services) from tool results and feed them into the asset registry. They run via the `ArgicidaAssetExtractor` hook in `post_tool_call`. Shared helpers live in `tools/extractors_base.py` (`extract_target`, `findings_to_discoveries`, `resolve_hostname`, `collect_ip_candidates`).\\n\\n## IPC file permissions\\n\\nNormalizers that write files to the IPC directory (FileInjector, TargetFileInjector, SshAuthNormalizer) MUST use `write_ipc_file()` from `_protocol.py`, which sets `0o644` permissions. The host UID maps to container root (UID 0), not the tool process UID (10000). The tool process reads host-written files via the \\\"other\\\" permission bits. Using `0o600` causes `Permission denied` inside the container.\",\"scope-enforcement\":\"# Scope Enforcement\\n\\n# Scope Enforcement\\n\\nArgicida enforces engagement scope at two independent layers: **network** (nftables rules on the gateway container) and **command** (argument validation in the `pre_tool_call` hook). Both must pass for a tool invocation to proceed. If either layer blocks it, the action is denied. This defense-in-depth design means a bug in one layer cannot silently expand scope.\\n\\n```mermaid\\ngraph LR\\n    LLM[\\\"LLM tool call\\\"] --&gt; Hook[\\\"pre_tool_call hook(validator.py)\\\"]\\n    Hook --&gt;|blocked| Deny1[\\\"\u274c Denied\\\"]\\n    Hook --&gt;|allowed| Pod[\\\"Tool container\\\"]\\n    Pod --&gt; GW[\\\"Gateway FORWARD chain(netfilter.py)\\\"]\\n    GW --&gt;|no match| Deny2[\\\"\u274c Dropped\\\"]\\n    GW --&gt;|scope match| Target[\\\"Target host\\\"]\\n```\\n\\nThe command layer is strictly narrower than the network layer \u2014 it rejects arguments the network layer would allow (blocked flags, shell metacharacters, tools targeting assets outside scope). The network layer is the backstop: even if a tool constructs an unexpected outbound connection, the gateway drops it unless the destination matches scope CIDRs or resolved domain IPs.\\n\\n## ScopeDefinition\\n\\n`argicida_common.scope_schema.ScopeDefinition` is the canonical scope representation, shared between both layers. It is a frozen msgspec Struct:\\n\\n| Field | Type | Purpose |\\n|---|---|---|\\n| `target_cidrs` | `tuple[str, ...]` | IP networks the engagement may touch |\\n| `target_domains` | `tuple[str, ...]` | Hostnames in scope (subdomains included) |\\n| `excluded_ips` | `tuple[str, ...]` | IPs explicitly blocked even if within a target CIDR |\\n| `dns_server` | `str` | Custom upstream DNS for the engagement |\\n| `max_intensity` | `str` | Scan intensity ceiling (default `\\\"safe\\\"`) |\\n| `template_allowlist` | `tuple[str, ...]` | Nuclei template restrictions |\\n| `nuclei_tags` / `nuclei_severity` / `nuclei_exclude_tags` | `tuple[str, ...]` | Nuclei filtering controls |\\n\\n`validate_scope()` checks structural validity (parseable CIDRs, valid IPs, domain format). `classify_scope_args()` splits a flat list of user-supplied strings into CIDRs vs. domains by attempting `ip_network()` on each.\\n\\n**Security boundary:** `ScopeDefinition` never crosses IPC to the container. The container receives only the specific arguments for each command. Scope enforcement is entirely the host's responsibility.\\n\\n## Network Layer \u2014 `netfilter.py`\\n\\nThis module contains pure functions that produce nftables command strings from a `ScopeDefinition`. It never executes rules itself \u2014 callers write the output to a file in `scope_dir`, and the gateway container loads it via `nft -f`.\\n\\n### Gateway ruleset structure\\n\\n`build_gateway_ruleset()` generates four chains in a single `inet` (dual-stack) table:\\n\\n| Chain | Hook | Default Policy | Role |\\n|---|---|---|---|\\n| **FORWARD** | `forward` | `drop` | Scope enforcement \u2014 only traffic to scope CIDRs/domains passes |\\n| **INPUT** | `input` | `drop` | Gateway self-protection \u2014 only DNS from internal subnet + loopback |\\n| **OUTPUT** | `output` | `accept` | Gateway's own traffic \u2014 unrestricted, optional DNS/VPN pinning |\\n| **postrouting** | `postrouting` (NAT) | \u2014 | Masquerade outbound traffic so return packets route correctly |\\n\\nThe table is created idempotently: `destroy table inet argicida` (no-op if absent) followed by `add table`.\\n\\n### FORWARD chain rule ordering\\n\\nRule order in the FORWARD chain is security-critical:\\n\\n1. **Established/related** \u2014 accept return traffic for connections already allowed\\n2. **Domain sets** (if `target_domains` present) \u2014 declare dynamic nftsets `domain_scope` (IPv4) and `domain_scope6` (IPv6) with 5-minute TTL and garbage collection\\n3. **Excluded IPs** \u2014 drop rules, ordered *before* CIDR accepts\\n4. **Domain set accepts** \u2014 `@domain_scope` / `@domain_scope6` match rules\\n5. **CIDR accepts** \u2014 one rule per `target_cidrs` entry\\n6. **Default drop** \u2014 implicit from chain policy\\n\\nExcluded IPs are ordered before CIDR accepts so an exclusion within a target CIDR takes effect. There is no blanket port 80/443 accept \u2014 all traffic must match a scope entry.\\n\\n### Domain resolution via dnsmasq\\n\\nIP-based nftables rules cannot match hostnames. Domain scope works through a DNS sidecar:\\n\\n1. `build_dnsmasq_conf()` generates a dnsmasq config with `nftset=` directives for each `target_domain`\\n2. When the tool pod resolves a scoped domain, dnsmasq adds the resolved IP to the `domain_scope` / `domain_scope6` nftsets\\n3. The FORWARD chain accepts traffic to IPs in those sets\\n\\nThe sets use `flags dynamic,timeout` with a 5-minute TTL, so stale resolutions expire automatically. Subdomains are included: `nftset=/.example.com/` matches `foo.example.com`.\\n\\n`build_dnsmasq_conf()` also emits:\\n- `listen-address=` bound to the gateway's internal IP (`10.89.1.2`) so the tool pod's `resolv.conf` can point there\\n- `server=` pointing to the engagement's upstream DNS\\n- `address=` lines for static host entries (e.g., known hostname\u2192IP mappings)\\n\\n### Permissive and flush rulesets\\n\\nTwo utility functions handle non-engagement scenarios:\\n\\n- `build_permissive_ruleset()` \u2014 same four-chain structure but FORWARD policy is `accept`. Used for diagnostic pods before an engagement scope is defined. Replaced with real scope when `scope_define` runs.\\n- `build_flush_ruleset()` \u2014 single `destroy table` command for cleanup.\\n\\n### Serialization\\n\\n`serialize_scope_rules()` and `serialize_dnsmasq_conf()` write generated content to files using `os.open()` with `O_NOFOLLOW` (reject symlinks). Scope rules get `0o600` permissions; dnsmasq config gets `0o644`. These files live in `scope_dir` \u2014 a host-owned temp directory that is *not* U/chown-shifted, so the host can always update them during scope changes.\\n\\n### VPN support\\n\\n`_build_output_chain()` optionally pins VPN endpoint traffic in the OUTPUT chain. It accepts a `(ip, port, protocol)` tuple and validates all three components. The `_VPN_INTERFACES` dict maps mode names to interface names (`wireguard` \u2192 `wg0`, `openvpn` \u2192 `tun0`), though the interface name is not currently used in rules.\\n\\n## Command Layer \u2014 `validator.py`\\n\\nThe command layer runs inside the `pre_tool_call` hook on the host. It validates every tool invocation's arguments against the engagement scope *before* the command reaches the container.\\n\\n### Tool profiles\\n\\nEach tool declares a `ToolProfile` that tells the validator how to interpret its arguments:\\n\\n```python\\nclass ToolProfile(msgspec.Struct, frozen=True, forbid_unknown_fields=True):\\n    blocked_flags: tuple[str, ...]     # Always rejected (e.g., -oN, --proxy)\\n    gated_flags: tuple[tuple[str, str], ...] = ()  # Require conditions\\n    target_flags: tuple[str, ...] = () # Values scope-checked as targets (-u, -host)\\n    value_flags: tuple[str, ...] = ()  # Values exempt from scope check (-top-ports, -c)\\n    skip_positional_scope: bool = False # Don't scope-check positional args\\n```\\n\\nProfiles are registered at plugin startup via `register_profiles()`, called from `register_scan_tools()` during Hermes plugin registration. The profiles originate from individual tool definition modules in `tools/defs/`.\\n\\n`_pd_flags()` is a helper for ProjectDiscovery tools that accept both `-flag` and `--flag` forms (goflags convention). It expands single-dash flags longer than 2 characters into both forms.\\n\\n### Argument classification\\n\\n`validate_tool_args()` walks the argument array in a single pass, classifying each element into one of four tiers:\\n\\n| Tier | Check | Rationale |\\n|---|---|---|\\n| **Value-flag values** | Skip all checks | Data values (SQL queries, port numbers) \u2014 safe because `create_subprocess_exec` has no shell |\\n| **Target-flag values** | Metachar + scope | Network destinations the tool will contact |\\n| **Blocked flags** | Reject immediately | Flags that could bypass scope or exfiltrate data |\\n| **Positional args** | Metachar + scope (unless `skip_positional_scope`) | Treated as implicit targets |\\n\\nThe `=` form (`--flag=value`) is handled: the flag name is extracted by splitting on `=`, and the value is checked according to the flag's tier.\\n\\n### Target scope resolution\\n\\n`_is_target_in_scope()` checks a target string against the scope through multiple strategies, in order:\\n\\n1. **Comma-separated targets** \u2014 split and check each independently (recursive)\\n2. **Normalization** \u2014 `normalize_target()` strips URL schemes, ports, and paths to extract the bare IP or hostname\\n3. **Excluded IPs** \u2014 checked first, takes absolute precedence\\n4. **IP match** \u2014 target IP falls within a `target_cidrs` network\\n5. **CIDR match** \u2014 target is itself a CIDR that is a subnet of a scope CIDR\\n6. **Domain match** \u2014 target hostname matches or is a subdomain of a `target_domains` entry\\n7. **Asset registry fallback** \u2014 if `store` and `engagement_id` are provided, looks up the target as a registered hostname on an asset whose IP is in scope\\n\\nThe asset registry fallback handles the case where tool discovery surfaces hostnames (e.g., via subfinder) that resolve to in-scope IPs but aren't explicitly listed in the scope definition.\\n\\n### Shell metacharacter rejection\\n\\nAll arguments except value-flag values are checked against `_SHELL_METACHAR_PATTERN`: `;`, `|`, `&amp;`, `` ` ``, `$`, `$(`. This prevents command injection even though `create_subprocess_exec` already avoids shell interpretation \u2014 defense in depth against any future code path that might inadvertently use a shell.\\n\\n## Lifecycle\\n\\nScope rules are not static. They update during an engagement as the operator expands or narrows scope:\\n\\n1. **Pod launch (no engagement):** `build_permissive_ruleset()` \u2192 FORWARD accepts all. No command-layer scope exists yet.\\n2. **`scope_define` runs:** `build_gateway_ruleset()` generates real rules \u2192 `serialize_scope_rules()` writes to `scope_dir` \u2192 gateway loads via `nft -f`. Command-layer validation begins using the `ScopeDefinition`.\\n3. **Scope update (e.g., new CIDR discovered):** `pod/launcher.py:update()` calls `serialize_scope_rules()` with the updated scope \u2192 gateway reloads. The command layer picks up the new `ScopeDefinition` from the engagement store.\\n4. **Teardown:** `build_flush_ruleset()` removes the table. Pod and gateway are destroyed by `cleanup_topology_by_name()`.\\n\\n## Adding scope support for a new tool\\n\\nNo changes to `validator.py` or `netfilter.py` are needed. Define the tool's scope profile in its `tools/defs/.py` module:\\n\\n```python\\nSCOPE_PROFILE = ToolProfile(\\n    blocked_flags=(\\\"-oN\\\", \\\"--proxy\\\"),\\n    target_flags=(\\\"-u\\\",),\\n    value_flags=(\\\"-top-ports\\\", \\\"-rate\\\"),\\n)\\n```\\n\\nThe profile is automatically discovered and registered via `register_profiles()` at startup. Test with `validate_tool_args()` to verify flag classification before deploying \u2014 a missing `value_flags` entry causes values like `1000` (from `-top-ports 1000`) to be treated as positional targets and rejected as out-of-scope.\",\"shared-types-schemas\":\"# Shared Types &amp; Schemas\\n\\n# `argicida_common` \u2014 Shared Types &amp; Schemas\\n\\nThe `argicida_common` package defines every data structure that crosses a boundary in Argicida: between host and container over Unix socket IPC, between the plugin and its SQLite stores, and between tool output parsers and the LLM context injector. It is the only package that both `argicida` (host plugin) and `argicida_agent` (container agent) depend on, so its sole external dependency is `msgspec`.\\n\\n## Design Invariants\\n\\nEvery struct in this package follows three rules enforced by convention and consistency tests:\\n\\n1. **`frozen=True, forbid_unknown_fields=True`** on all `msgspec.Struct` subclasses. Frozen structs are hashable and prevent accidental mutation after deserialization. Unknown fields cause decode errors rather than silent data loss.\\n\\n2. **`tuple[T, ...]`** for collection fields, never `list`. Tuples are truly immutable \u2014 a frozen struct with a `list` field can still have its contents mutated.\\n\\n3. **`Literal[...]`** for constrained string fields. Values are validated at decode time by msgspec, not at construction time. Direct `Struct()` construction bypasses this \u2014 always decode from wire format via `msgspec.msgpack.decode()` or `msgspec.json.decode()` for untrusted data.\\n\\n## Constants (`constants.py`)\\n\\n### Enums\\n\\nThree `StrEnum` types replace string literals across the codebase:\\n\\n| Enum | Values | Used by |\\n|------|--------|---------|\\n| `TriageLevel` | `critical`, `interesting`, `noise` | Finding triage, briefing generation |\\n| `EngagementPhase` | `scoping` \u2192 `discovery` \u2192 `enumeration` \u2192 `deep_testing` \u2192 `human_gate` | Engagement lifecycle, context injection |\\n| `CoverageState` | `discovered` \u2192 `ports_scanned` \u2192 `hostnames_probed` \u2192 `vgroups_identified` \u2192 `enumerated` | Asset registry coverage tracking |\\n\\n`EngagementPhase` and `CoverageState` are ordered state machines \u2014 assets and engagements progress forward through these states. The ordering is implicit (not enforced by the enum), but the store layer validates transitions.\\n\\n### `TOOL_NAMES`\\n\\nMaps logical tool names to absolute binary paths inside the container. The agent process (`argicida_agent`) looks up binaries here rather than relying on `$PATH`. Adding a new tool binary requires an entry here and a corresponding install in the Containerfile.\\n\\n### Size Limits\\n\\nConstants prefixed `MAX_` define msgspec `Meta` validation bounds and IPC frame limits:\\n\\n- **IPC payload limits**: `MAX_IPC_STDOUT_SIZE` (16 MiB), `MAX_IPC_STDERR_SIZE` (1 MiB) \u2014 cap what the agent streams back per command. `MAX_IPC_MESSAGE_SIZE` (64 MiB) caps the entire framed message.\\n- **Domain limits**: `MAX_ARGS_COUNT` (100), `MAX_ENV_COUNT` (50), `MAX_CIDRS_COUNT` / `MAX_DOMAINS_COUNT` (1000) \u2014 prevent unbounded scope definitions from consuming memory.\\n- **IPC security**: `IPC_HMAC_KEY_SIZE` (32 bytes), `IPC_NONCE_SIZE` (32 bytes), `AUTH_TIMEOUT_S` (10s), `MAX_AUTH_FRAME_SIZE` (1 KiB) \u2014 govern the HMAC handshake between host and agent.\\n\\n### Environment Variable Controls\\n\\n`ENV_ALLOWLIST` \u2014 the only environment variables that can be forwarded to tool commands inside the container (API keys + locale). `ENV_BLOCKLIST` \u2014 variables that are explicitly stripped even if present, because they could alter tool behavior or bypass isolation (`LD_PRELOAD`, `PATH`, proxy vars).\\n\\n## Schema Modules\\n\\n```mermaid\\ngraph LR\\n    SD[ScopeDefinition] --&gt; EM[EngagementMeta]\\n    F[Finding]\\n    A[Asset] --&gt; AH[AssetHostname]\\n    A --&gt; AP[AssetPort]\\n    VF[VhostFingerprint] -.-&gt;|serialized into| AH\\n    P[Principal]\\n    S[Secret]\\n    VC[ValidatedCredential]\\n\\n    style SD fill:#f0f0f0,stroke:#666\\n    style EM fill:#f0f0f0,stroke:#666\\n```\\n\\n### `finding_schema.py` \u2014 `Finding`\\n\\nRepresents a single security finding from any scan tool. Fields map to the OCSF-normalized output that the `transform_tool_result` hook produces.\\n\\nKey fields:\\n- `severity` \u2014 five-level scale (`critical` through `info`), set by the tool parser\\n- `triage_level` \u2014 three-level operator classification (`critical`, `interesting`, `noise`), set by the triage engine or manually via `/triage`\\n- `raw_output` \u2014 capped at `MAX_RAW_OUTPUT_SIZE` (10 MiB) via `Annotated[str, msgspec.Meta(max_length=...)]`\\n- `epss` / `kev` \u2014 EPSS probability score and CISA KEV membership, populated by the enrichment layer from pre-downloaded data files\\n- `cve_ids` / `cwe_ids` / `wasc_ids` \u2014 vulnerability identifiers as tuples, not lists\\n\\nThe `Finding` struct is produced by tool parsers (`tools/parsers_*.py`) via the `make_finding()` helper, stored in the `FindingRepo`, and rendered into LLM context by the briefing generator.\\n\\n### `engagement_schema.py` \u2014 `EngagementMeta`\\n\\nLightweight engagement summary containing the scope definition, current phase, and timestamps. This struct is what gets injected into LLM context via the `pre_llm_call` hook \u2014 it carries enough information for the LLM to understand what it's allowed to target and where it is in the engagement lifecycle.\\n\\n`EngagementMeta.scope` is a `ScopeDefinition` (from `scope_schema.py`), making this the join point between engagement state and scope enforcement.\\n\\n### `asset_schema.py` \u2014 Asset Registry Types\\n\\nFour structs model the IP-anchored asset registry:\\n\\n**`Asset`** \u2014 top-level entity keyed by IP address. `coverage_state` tracks how thoroughly the asset has been enumerated, progressing through the `CoverageState` machine. Contains nested tuples of `AssetHostname` and `AssetPort`.\\n\\n**`AssetHostname`** \u2014 a hostname associated with an IP, with its discovery `source` tool. Optional fields support vhost fingerprinting: `fingerprint` (serialized `VhostFingerprint`), `vhost_group` (UUID grouping structurally identical vhosts), `visual_hash` (Tier 2 screenshot-based dedup), `tech_fingerprint` (Wappalyzer tech stack from ZAP), and `login_url` (login form URL from ZAP).\\n\\n**`AssetPort`** \u2014 an open port with protocol, service name, and banner.\\n\\n**`VhostFingerprint`** \u2014 structural metadata from httpx responses used to group hostname aliases. Contains status code, content length, title hash, redirect target, body hash, and server header. Fingerprints with identical values are assumed to serve the same content.\\n\\n**`serialize_fingerprint()`** \u2014 the only function in the package. Converts a `VhostFingerprint` to a deterministic JSON string via `msgspec.json.encode()`. Used by the `AssetRepo` for storage and by the httpx extractor when processing scan results.\\n\\n### `credential_schema.py` \u2014 Credential Fragment Model\\n\\nModels the credential discovery lifecycle as three separate entities:\\n\\n**`Principal`** \u2014 a username or identity found in isolation (e.g., from an SMTP VRFY response or a web page). `confidence` tracks discovery certainty: `confirmed` (verified to exist), `likely` (strong evidence), `inferred` (pattern-based guess).\\n\\n**`Secret`** \u2014 authentication material found in isolation (e.g., a leaked password hash or an API key in a config file). `secret_type` discriminates between `password`, `token`, `api_key`, `private_key`, `hash`, and `passphrase`. `associated_principal` optionally links to a suspected owner.\\n\\n**`ValidatedCredential`** \u2014 a proven (principal, secret, target, service) tuple that granted access. Created only after successful authentication testing. `access_level` records what the credential provides: `admin`, `user`, `read-only`, or `unknown`. `is_default` flags factory-default credentials.\\n\\nThe `Credential` alias exists for backward compatibility but new code should use `ValidatedCredential`. The `SECRET_TYPE_PASSWORD` constant carries a `# noqa: S105` because bandit flags any variable with \\\"PASSWORD\\\" in the name \u2014 the comment explains it's a type discriminator, not a credential value.\\n\\n## Adding a New Schema\\n\\n1. Create a new `*_schema.py` file following the existing pattern.\\n2. Use `frozen=True, forbid_unknown_fields=True` on all structs.\\n3. Use `tuple[T, ...]` for collections, `Literal[...]` for constrained strings.\\n4. Add size-limit constants to `constants.py` if the struct carries variable-length data.\\n5. Import from `argicida_common` in both `argicida` and `argicida_agent` as needed \u2014 this package is the only shared dependency.\\n\\nDo not add any dependencies beyond `msgspec`. Both the host plugin and the container agent import this package, and the agent's container image is kept minimal.\",\"triage-engine\":\"# Triage Engine\\n\\n# Triage Engine\\n\\n`src/argicida/argicida/triage/`\\n\\nThe triage engine scores, classifies, and deduplicates vulnerability findings produced by scan tools. It assigns every finding one of three levels \u2014 **CRITICAL**, **INTERESTING**, or **NOISE** \u2014 using a deterministic, weighted composite of CVSS severity, EPSS exploitation probability, and CISA KEV catalog membership. All threat intelligence data is loaded from local files provisioned by the operator before the engagement, enabling air-gapped operation.\\n\\n## Pipeline\\n\\nFindings flow through four stages. Deduplication happens before scoring so duplicate findings from overlapping tools don't inflate counts or waste scoring work.\\n\\n```mermaid\\ngraph LR\\n    A[Raw Findings] --&gt; B[Deduplicate]\\n    B --&gt; C[Score]\\n    C --&gt; D[Classify]\\n    D --&gt; E[Triaged Findings]\\n    F[(EPSS CSV)] -.-&gt; C\\n    G[(KEV JSON)] -.-&gt; C\\n```\\n\\nTwo hooks drive this pipeline at runtime:\\n\\n- **`normalize_result`** (`hooks/normalizer.py`) calls `classify_findings` during OCSF normalization of tool output, writing the triage level back onto each `Finding`.\\n- **`capture_evidence`** (`hooks/evidence.py`) calls `classify_findings` when persisting findings to the evidence chain.\\n\\nThe triage data itself (`EpssData`, `KevData`) is loaded once during plugin registration (`init_state` \u2192 `load_epss` / `load_kev`) and held on `PluginState` for the session lifetime. Both classes expose a `reload()` method for hot-swapping data without restarting the session.\\n\\n## Data Loaders \u2014 `data.py`\\n\\n### `EpssData`\\n\\nWraps a `dict[str, float]` mapping CVE IDs to EPSS probability scores (0.0\u20131.0). Loaded from the FIRST/EPSS CSV format, which starts with comment lines (`#`) followed by a header row and `cve,epss,percentile` data rows. The loader strips comments before passing to `csv.DictReader`.\\n\\n```python\\nepss = load_epss(Path(\\\"epss_scores.csv\\\"))\\nscore = epss.lookup(\\\"CVE-2024-1234\\\")  # float | None\\n```\\n\\n### `KevData`\\n\\nWraps two `frozenset[str]` collections parsed from CISA's Known Exploited Vulnerabilities JSON catalog: all KEV CVE IDs and the subset linked to known ransomware campaigns (`knownRansomwareCampaignUse == \\\"Known\\\"`).\\n\\n```python\\nkev = load_kev(Path(\\\"kev.json\\\"))\\nkev.is_kev(\\\"CVE-2024-1234\\\")         # bool\\nkev.is_ransomware(\\\"CVE-2024-1234\\\")  # bool\\n```\\n\\nBoth data files are expected at `$HERMES_HOME/argicida/data/`. See CLAUDE.md's **Pre-Engagement Data** section for download commands.\\n\\n## Deduplication \u2014 `dedup.py`\\n\\n`deduplicate()` collapses findings from multiple tools that describe the same vulnerability on the same target.\\n\\n**Dedup key:** `(target, port, first_cve_id)` when CVE IDs are present; `(target, port, normalized_title)` otherwise. Title normalization is lowercase + strip.\\n\\n**Conflict resolution via `_pick_best`:** when two findings share a key, the one with more CVE IDs wins. On a tie, the higher CVSS score wins.\\n\\nThis runs before scoring \u2014 deduplication is purely structural and doesn't need threat intelligence data.\\n\\n## Composite Scoring \u2014 `scorer.py`\\n\\n`score_finding` computes a weighted composite score for a single finding:\\n\\n| Signal | Weight | Value range | Source |\\n|---|---|---|---|\\n| Severity | 0.25 | 0.0\u20131.0 (mapped from `info`\u2192`critical`) | Finding field |\\n| CVSS | 0.20 | 0.0\u20131.0 (raw CVSS / 10) | Finding field |\\n| EPSS | 0.25 | 0.0\u20131.0 (max across all CVEs) | Local EPSS CSV |\\n| KEV membership | 0.20 | 0 or 1 | Local KEV JSON |\\n| Ransomware link | 0.10 | 0 or 1 | Local KEV JSON |\\n\\nFor findings with multiple CVE IDs, the scorer takes the **maximum** EPSS score across all of them. KEV and ransomware flags are set if **any** CVE matches.\\n\\nThe output is a `ScoredFinding` \u2014 a frozen msgspec Struct that bundles the original `Finding` with the composite score, individual EPSS score, KEV match flag, and ransomware flag.\\n\\n`score_findings` is the batch variant that maps over a tuple of findings.\\n\\n## Classification \u2014 `classifier.py`\\n\\n`classify` applies a deterministic decision tree to a `ScoredFinding`. The rules evaluate in strict order \u2014 first match wins:\\n\\n**CRITICAL** (any of):\\n1. KEV match\\n2. Ransomware-linked CVE\\n3. EPSS score \u2265 0.8\\n4. Severity is `critical` AND has CVE or CWE IDs\\n\\n**INTERESTING** (any of):\\n5. Composite score \u2265 0.4\\n6. Severity is `high` or `medium` AND has CVE or CWE IDs\\n7. Severity is `critical`, `high`, or `medium` (without IDs)\\n8. CVSS \u2265 7.0\\n\\n**NOISE**: everything else.\\n\\n`classify_findings` is the batch entry point. It scores, classifies, and writes the triage level, EPSS score, and KEV flag back onto each `Finding` via `msgspec.structs.replace`.\\n\\n### `count_by_triage`\\n\\nUtility that counts findings by their already-assigned `triage_level` field. Used by:\\n\\n- `/briefing` and `/triage` slash commands (`commands/slash.py`)\\n- Engagement state context injection (`hooks/context_sections.py` \u2014 both `build_state_block` and `build_resume_context`)\\n- Briefing report generation (`briefing/prompts.py` \u2014 both `build_summary_prompt` and `render_briefing`)\\n- Fallback summary (`briefing/generator.py`)\\n\\n## New Asset Discovery \u2014 `discovery.py`\\n\\n`detect_new_assets` identifies targets in findings that are not covered by the current scope definition. Only findings from reconnaissance tools (`nmap`, `naabu`, `subfinder`, `httpx`) are considered \u2014 vulnerability scanners like nuclei only hit targets already in scope.\\n\\nClassification logic:\\n- Targets parseable as `ip_address` or `ip_network` \u2192 checked against `scope.target_cidrs`\\n- Everything else \u2192 treated as a domain and checked against `scope.target_domains` with subdomain matching (`sub.example.com` matches `example.com`)\\n\\nReturns a sorted, deduplicated tuple of out-of-scope target strings. Targets in `scope.excluded_ips` are treated as \\\"known\\\" and excluded from the result.\\n\\n## Trust Model\\n\\nThe classifier's docstring documents the trust assumptions explicitly:\\n\\n- **CVE IDs** are trusted tool output from nuclei's curated template matching, not target-supplied data.\\n- **EPSS/KEV data** comes from operator-provisioned local files, not fetched at runtime.\\n- **Container compromise** (fabricated CVE injection) is a separate threat model addressed by container hardening \u2014 seccomp, read-only rootfs, non-root user, namespace isolation.\\n\\n## Diagram Generation\\n\\n`scripts/generate_triage_diagrams.py` produces two PNG diagrams via pydot:\\n\\n- `docs/images/triage_pipeline.png` \u2014 the linear pipeline from raw findings to triaged output\\n- `docs/images/classifier_tree.png` \u2014 the full classifier decision tree\\n\\nRun with `uv run python scripts/generate_triage_diagrams.py`.\",\"zap-integration\":\"# ZAP Integration\\n\\n# ZAP Integration (`argicida/zap/`)\\n\\nZAP runs as a sidecar container inside the Argicida pod, providing passive proxy analysis and active vulnerability scanning. This module wraps ZAP's REST API and Automation Framework (AF) behind a five-layer stack that the seven management tools (`zap_scan`, `zap_alerts`, `zap_context`, `zap_auth`, `zap_import`, `zap_spider`, `zap_intel`) call into.\\n\\n```mermaid\\ngraph TD\\n    Tools[\\\"Management tools(zap_scan, zap_intel, \u2026)\\\"]\\n    Manager[\\\"ZapManagerorchestration + state\\\"]\\n    PlanExec[\\\"PlanExecutorAF lifecycle\\\"]\\n    PlanGen[\\\"plan_generatorpure YAML generation\\\"]\\n    Policy[\\\"scan_policytech detection + rule selection\\\"]\\n    Client[\\\"ZapClientREST API via curl\\\"]\\n    CredTest[\\\"credential_testerlogin detection + testing\\\"]\\n    ZAP[\\\"ZAP sidecar container\\\"]\\n\\n    Tools --&gt; Manager\\n    Manager --&gt; PlanExec\\n    Manager --&gt; Policy\\n    Manager --&gt; CredTest\\n    Manager --&gt; Client\\n    PlanExec --&gt; Client\\n    PlanGen -.-&gt;|produces GeneratedPlan| PlanExec\\n    Manager --&gt; PlanGen\\n    Client --&gt;|exec_sync curl| ZAP\\n```\\n\\n## ZapClient\\n\\n`client.py` \u2014 Thin REST client that talks to ZAP at `http://127.0.0.1:8090`. Every call runs `curl -sf ` inside the ZAP container via `PodManager.exec_sync()`. There is no HTTP library dependency \u2014 curl is the transport.\\n\\nTwo internal helpers do all the work:\\n\\n- `_raw_get(path, params)` \u2014 builds the URL, runs curl, raises `ZapError` on non-zero exit.\\n- `_call_json(endpoint, params)` \u2014 calls `_raw_get` with a `JSON/` prefix and deserializes the response.\\n\\nThe public surface maps 1:1 to ZAP API endpoints, grouped by subsystem:\\n\\n| Group | Methods |\\n|---|---|\\n| Core | `version`, `root_cert`, `new_session`, `get_alerts`, `delete_all_alerts`, `send_request` |\\n| Context | `new_context`, `include_in_context`, `exclude_from_context`, `get_context`, `get_include_regexes` |\\n| Authentication | `set_auth_method`, `set_logged_in_indicator`, `set_logged_out_indicator`, `set_session_management` |\\n| Users | `create_user`, `set_user_credentials`, `set_forced_user`, `enable_forced_user` |\\n| Spider | `spider_scan`, `spider_status` |\\n| AJAX Spider | `ajax_spider_scan`, `ajax_spider_scan_as_user`, `ajax_spider_status`, `ajax_spider_stop` |\\n| Passive Scan | `pscan_records_to_scan`, `pscan_set_max_alerts_per_rule`, `pscan_set_max_body_size`, `pscan_disable_scanners`, `pscan_set_scanner_alert_threshold` |\\n| Active Scan | `ascan_scan`, `ascan_status`, `get_scanner_ids` |\\n| Replacer | `add_replacer_rule`, `remove_replacer_rule` |\\n| API Import | `import_openapi_url`, `import_graphql_url` |\\n| Intelligence | `wappalyzer_list_sites`, `wappalyzer_list_site`, `search_by_response_regex`, `get_auth_state` |\\n| Automation Framework | `run_plan`, `plan_progress`, `stop_plan` |\\n| Reports | `get_site_stats`, `generate_report`, `generate_report_content` |\\n\\n`generate_report_content` is notable \u2014 it writes a report to `/tmp` inside the ZAP container (writable tmpfs), then reads it back via `exec_sync(cat)` so the host can store it in the output directory.\\n\\nAll error paths raise `ZapError(endpoint, exit_code, output)`.\\n\\n## Plan Executor\\n\\n`plan_executor.py` \u2014 Manages the lifecycle of a single AF plan: write YAML to the IPC directory, tell ZAP to execute it, poll until finished.\\n\\n```python\\nexecutor = PlanExecutor(client)\\nresult = executor.execute(plan, ipc_dir, poll_interval=2.0)\\n```\\n\\nThe `GeneratedPlan` struct (produced by `plan_generator`) carries the YAML content, a name (used as the filename), and a list of expected job types. `PlanExecutor.execute()`:\\n\\n1. Writes the YAML to `ipc_dir/.yaml` via `write_ipc_file` (0o644 permissions \u2014 the container reads it via \\\"other\\\" bits).\\n2. Calls `client.run_plan(\\\"/ipc/.yaml\\\")` to start execution.\\n3. Polls `client.plan_progress(plan_id)` until the `finished` field is set.\\n4. Returns a `PlanResult` with timing, info/warning/error messages, and the original YAML.\\n\\n## Plan Generator\\n\\n`plan_generator.py` \u2014 Pure functions that produce `GeneratedPlan` objects. No side effects, no API calls. Three plan types:\\n\\n### `generate_scan_plan`\\n\\nActive vulnerability scanning. Accepts a target URL, detected technology, policy config, optional credentials, alert filters, exclude patterns, and passive rule recommendations. Produces:\\n\\n- An `env` block with context URLs, exclude patterns, and credential auth config.\\n- An `activeScan` job with a `policyDefinition` containing the merged rule set (policy rules + passive recommendations).\\n- Auth monitor tests when credentials are provided (watches `stats.auth.failure` and `stats.auth.ascan.state.loggedout` counters to detect broken authentication mid-scan).\\n- An `exitStatus` job (always runs).\\n\\n### `generate_spider_plan`\\n\\nContent discovery. Seeds the spider with URLs from the asset registry \u2014 for every hostname associated with the target IP, it constructs a URL using the target's scheme and port. The env block includes credential config for authenticated spidering.\\n\\n### `generate_intel_plan`\\n\\nMinimal crawl (depth 2, 1 minute) followed by `passiveScan-wait`. Used by `zap_intel` for fast passive analysis without active scanning.\\n\\n### Credential Mapping\\n\\n`credential_to_env_block` dispatches on `secret_type` to produce the right AF authentication block:\\n\\n| `secret_type` | AF auth method |\\n|---|---|\\n| `password` | Browser-based auth (`method: browser`) with autodetect session management |\\n| `token` | Replacer rule injecting `Authorization: Bearer ` |\\n| `api_key` | Replacer rule injecting `X-API-Key: ` |\\n| anything else | Replacer rule injecting `Authorization: Basic ` |\\n\\nBrowser-based auth is recommended for password credentials \u2014 ZAP drives a headless browser to the login page, fills the form, and captures the resulting session cookies/tokens automatically.\\n\\n## Scan Policy\\n\\n`scan_policy.py` \u2014 Builds ZAP active scan policies dynamically from detected technology instead of using static policy files.\\n\\n### Technology Detection\\n\\nThree sources feed `DetectedTech`:\\n\\n1. **Passive alerts** (`detect_tech_from_passive`) \u2014 Scans ZAP alert names and evidence fields for web server banners (`Apache`, `nginx`, `IIS`), language markers (`PHP/`, `ASP.NET`, `Express`, `gunicorn`), and framework signatures.\\n\\n2. **Asset registry** (`detect_tech_from_assets`) \u2014 Reads port banners, service names (maps `mysql` \u2192 `mysql`, `ms-sql-s` \u2192 `mssql`, etc.), vhost fingerprint `server_header` fields, Wappalyzer `tech_fingerprint` JSON on hostnames, and nuclei finding titles matching framework keywords.\\n\\n3. **Wappalyzer** (`ZapManager._query_wappalyzer`) \u2014 Queries ZAP's Wappalyzer add-on. Matches the target against ZAP's internal site tree (scheme://authority format), then classifies each fingerprint entry by category (`web server`, `programming language`, `database`, `framework`/`cms`).\\n\\n`merge_tech` combines multiple `DetectedTech` instances \u2014 the first source to identify each field wins, frameworks accumulate from all sources.\\n\\n### Rule Tiers\\n\\nRules are organized into three tiers, selected by scan level:\\n\\n| Tier | When included | Examples |\\n|---|---|---|\\n| **Tier 1 \u2014 Universal** | Always | Path Traversal, XSS, SQLi (error-based), Command Injection, SSRF, Cloud Metadata |\\n| **Tier 2 \u2014 Technology-specific** | `targeted` and `full` levels | DB-specific SQLi (MySQL/Postgres/Oracle/MSSQL/SQLite/MongoDB), Java deserialization, PHP injection, IIS-specific, Node prototype pollution |\\n| **Tier 3 \u2014 Expensive** | `full` level only | Persistent XSS (3 rules), DOM XSS, LDAP injection, Session Fixation, time-based command injection, all DB-specific rules |\\n\\n### Scan Levels\\n\\n| Level | Threshold | Strength | Rule duration | Scan duration | Threads |\\n|---|---|---|---|---|---|\\n| `quick` | MEDIUM | LOW | 2 min | 5 min | 5 |\\n| `targeted` | LOW | MEDIUM | 3 min | 10 min | 3 |\\n| `full` | LOW | HIGH | 5 min | 30 min | 2 |\\n\\nSpider configs follow the same three levels, scaling depth (2/5/10), duration (1/3/10 min), and feature flags (git/SVN parsing, form posting).\\n\\n### Passive-to-Active Mapping\\n\\n`recommend_active_rules` maps passive finding rule IDs to active scan rules that should be enabled. For example, passive rule 10202 (no CSRF tokens) enables active rule 20012 (Anti-CSRF Scanner), and passive CORS misconfiguration (10098) enables the CORS Scanner (40040).\\n\\n## ZapManager\\n\\n`manager.py` \u2014 The orchestration layer. Holds context state, alert caches, and coordinates all scan workflows. Created by `ensure_pod()` when the ZAP healthcheck passes, stored on `PluginState`, cleared in `on_session_finalize`.\\n\\n### Context Management\\n\\n`create_context(name, include_regex, exclude_regex, extra_includes)` creates a ZAP context with include/exclude URL patterns. Thread-safe via `self._lock`. Contexts are cached by name \u2014 calling with the same name returns the existing context ID.\\n\\n### Authentication Configuration\\n\\n`configure_auth` dispatches on `credential.secret_type`:\\n\\n- **token / api_key** \u2192 `_configure_token_auth` \u2192 `configure_header_auth` \u2192 `add_replacer_rule`. Injects the header on every request via ZAP's replacer. No ZAP user created.\\n- **password** \u2192 `_configure_password_auth`. Sets form-based or JSON-based auth (based on whether the service is `api`/`graphql`/`rest`), creates a ZAP user, sets credentials, enables forced-user mode so all requests authenticate.\\n- **anything else** \u2192 Basic auth via replacer rule.\\n\\nReturns `(context_id, user_id)`. For token/api_key auth, `user_id` is `-1` since no ZAP user is created.\\n\\n### run_scan\\n\\nThe full active scan workflow, used by the `zap_scan` tool:\\n\\n1. Harvest passive alerts accumulated from proxy traffic.\\n2. Persist passive findings to the engagement database.\\n3. Detect technology from passive alerts + asset registry + merge.\\n4. Map passive findings to active rule recommendations.\\n5. Build a `ScanPolicyConfig` from detected tech and scan level.\\n6. Look up credentials if a `user_name` was specified.\\n7. Generate an AF plan via `generate_scan_plan`.\\n8. Execute the plan via `PlanExecutor`.\\n9. Collect alerts from ZAP for the target URL.\\n10. Generate a SARIF report (best-effort \u2014 failures are logged, not fatal).\\n11. Return `(findings, stages)` where `stages` contains plan errors/warnings, SARIF content, scan metadata, and the plan YAML.\\n\\n### harvest_intel\\n\\nThe intelligence gathering workflow, used by `zap_intel`:\\n\\n1. Get passive alerts.\\n2. Detect tech from passive alerts + asset registry + Wappalyzer.\\n3. Detect login forms via `detect_login_forms` (Search API regex + passive rule 10111).\\n4. Map persisted ZAP findings to active rule recommendations.\\n5. Return a `WebIntel` struct with tech fingerprints, login URLs, and recommendations.\\n\\n### Alert Caching\\n\\n`get_cached_alerts()` caches parsed findings for 30 seconds (`_alert_cache_ttl`). Used by `zap_alerts` to avoid hammering ZAP's API on repeated queries.\\n\\n### Passive Scanner Tuning\\n\\n`configure_defaults()` reduces noise on first initialization:\\n- Caps alerts per rule to 10.\\n- Limits body scan size to 500KB.\\n- Disables noisy rules: Timestamp Disclosure (10096), Suspicious Comments (10027), Modern Web Application (10109), Cache-control (10015).\\n\\n## Credential Tester\\n\\n`credential_tester.py` \u2014 Login form detection and credential validation via ZAP's `sendRequest` API.\\n\\n### Login Form Detection\\n\\n`detect_login_forms(client, alerts)` finds login pages from two sources:\\n- ZAP's Search API: regex match for `type=\\\"password\\\"` in response bodies.\\n- Passive rule 10111 alerts (Authentication Request Identified).\\n\\nReturns deduplicated, sorted URLs.\\n\\n### Credential Testing\\n\\n`try_credentials(client, login_url, candidates, target)` builds raw HTTP POST requests (`application/x-www-form-urlencoded` with `username`/`password` fields), sends them through ZAP via `sendRequest`, and checks for success indicators in the response header: `302 Found`, `303 See Other`, or `Set-Cookie`. Returns a `ValidatedCredential` on first success, `None` if all candidates fail.\\n\\n## External Dependencies\\n\\n| Module | Depends on |\\n|---|---|\\n| `ZapClient` | `PodManager.exec_sync` (curl execution in container) |\\n| `ZapManager` | `EngagementStore` (findings, credentials, assets), `parse_zap_findings` (alert \u2192 Finding conversion), `normalize_target` (URL \u2192 IP extraction) |\\n| `PlanExecutor` | `write_ipc_file` from `tools/defs/_protocol.py` (0o644 permissions for container readability) |\\n| `scan_policy` | `AssetRepo.get_asset_by_ip`, `FindingRepo.query_findings`, `VhostFingerprint` schema |\\n\\nThe `ZapProxyInjector` normalizer (in `tools/normalizers.py`, outside this module) auto-routes web tool traffic through ZAP for continuous passive analysis \u2014 that is where ZAP's passive findings originate.\"};\nvar TREE = [{\"name\":\"Plugin Registration &amp; Core\",\"slug\":\"plugin-registration-core\",\"files\":[\"src/argicida/argicida/__init__.py\",\"src/argicida/argicida/registration/__init__.py\",\"src/argicida/argicida/registration/context.py\",\"src/argicida/argicida/registration/state.py\",\"src/argicida/argicida/registration/hooks.py\",\"src/argicida/argicida/registration/scan_tools.py\",\"src/argicida/argicida/registration/management_tools.py\",\"src/argicida/argicida/registration/commands.py\",\"src/argicida/argicida/utils.py\",\"src/argicida/argicida/plugin.yaml\"]},{\"name\":\"Engagement &amp; Database\",\"slug\":\"engagement-database\",\"files\":[\"src/argicida/argicida/engagement/__init__.py\",\"src/argicida/argicida/engagement/lifecycle.py\",\"src/argicida/argicida/engagement/session.py\",\"src/argicida/argicida/db/__init__.py\",\"src/argicida/argicida/db/store.py\",\"src/argicida/argicida/db/asset_repo.py\",\"src/argicida/argicida/db/credential_repo.py\",\"src/argicida/argicida/db/finding_repo.py\",\"src/argicida/argicida/db/evidence_repo.py\",\"src/argicida/argicida/db/vpn_repo.py\",\"src/argicida/argicida/db/helpers.py\",\"src/argicida/argicida/db/migrations.py\",\"src/argicida/argicida/db/encryption.py\"]},{\"name\":\"Pod Management\",\"slug\":\"pod-management\",\"files\":[\"src/argicida/argicida/pod/__init__.py\",\"src/argicida/argicida/pod/launcher.py\",\"src/argicida/argicida/pod/manager.py\",\"src/argicida/argicida/pod/session.py\",\"src/argicida/argicida/pod/config.py\",\"src/argicida/argicida/pod/builder.py\",\"src/argicida/argicida/pod/vpn.py\"]},{\"name\":\"Container Agent\",\"slug\":\"container-agent\",\"files\":[\"src/argicida_agent/argicida_agent/__init__.py\",\"src/argicida_agent/argicida_agent/__main__.py\",\"src/argicida_agent/argicida_agent/executor/__init__.py\",\"src/argicida_agent/argicida_agent/executor/runner.py\",\"src/argicida_agent/argicida_agent/ipc/__init__.py\",\"src/argicida_agent/argicida_agent/ipc/server.py\"]},{\"name\":\"IPC Layer\",\"slug\":\"ipc-layer\",\"files\":[\"src/argicida/argicida/ipc/__init__.py\",\"src/argicida/argicida/ipc/client.py\",\"src/argicida_common/argicida_common/ipc_protocol.py\",\"src/argicida_common/argicida_common/ipc_schema.py\"]},{\"name\":\"Scope Enforcement\",\"slug\":\"scope-enforcement\",\"files\":[\"src/argicida/argicida/scope/__init__.py\",\"src/argicida/argicida/scope/netfilter.py\",\"src/argicida/argicida/scope/validator.py\",\"src/argicida_common/argicida_common/scope_schema.py\"]},{\"name\":\"Scan Tool Framework\",\"slug\":\"scan-tool-framework\",\"files\":[\"src/argicida/argicida/tools/__init__.py\",\"src/argicida/argicida/tools/base.py\",\"src/argicida/argicida/tools/schemas.py\",\"src/argicida/argicida/tools/defs/__init__.py\",\"src/argicida/argicida/tools/defs/_protocol.py\",\"src/argicida/argicida/tools/defs/_pd_common.py\",\"src/argicida/argicida/tools/defs/nmap.py\",\"src/argicida/argicida/tools/defs/naabu.py\",\"src/argicida/argicida/tools/defs/nuclei.py\",\"src/argicida/argicida/tools/defs/httpx.py\",\"src/argicida/argicida/tools/defs/subfinder.py\",\"src/argicida/argicida/tools/defs/dnsx.py\",\"src/argicida/argicida/tools/defs/katana.py\",\"src/argicida/argicida/tools/defs/ffuf.py\",\"src/argicida/argicida/tools/defs/curl.py\",\"src/argicida/argicida/tools/defs/tracepath.py\",\"src/argicida/argicida/tools/defs/rpcinfo.py\",\"src/argicida/argicida/tools/defs/smbclient.py\",\"src/argicida/argicida/tools/defs/db_query.py\",\"src/argicida/argicida/tools/defs/redis_query.py\",\"src/argicida/argicida/tools/defs/mongo_query.py\",\"src/argicida/argicida/tools/defs/ssh_exec.py\",\"src/argicida/argicida/tools/normalizers.py\",\"src/argicida/argicida/tools/normalizers_nmap.py\",\"src/argicida/argicida/tools/normalizers_nuclei.py\",\"src/argicida/argicida/tools/normalizers_curl.py\",\"src/argicida/argicida/tools/normalizers_db.py\",\"src/argicida/argicida/tools/normalizers_redis.py\",\"src/argicida/argicida/tools/normalizers_mongo.py\",\"src/argicida/argicida/tools/normalizers_katana.py\",\"src/argicida/argicida/tools/normalizers_smb.py\",\"src/argicida/argicida/tools/normalizers_ssh.py\",\"src/argicida/argicida/tools/normalizers_zap.py\",\"src/argicida/argicida/tools/parsers.py\",\"src/argicida/argicida/tools/parsers_nmap.py\",\"src/argicida/argicida/tools/parsers_naabu.py\",\"src/argicida/argicida/tools/parsers_nuclei.py\",\"src/argicida/argicida/tools/parsers_dnsx.py\",\"src/argicida/argicida/tools/parsers_db.py\",\"src/argicida/argicida/tools/parsers_redis.py\",\"src/argicida/argicida/tools/parsers_mongo.py\",\"src/argicida/argicida/tools/parsers_smbclient.py\",\"src/argicida/argicida/tools/parsers_zap.py\",\"src/argicida/argicida/tools/transformers.py\",\"src/argicida/argicida/tools/transformers_nmap.py\",\"src/argicida/argicida/tools/transformers_naabu.py\",\"src/argicida/argicida/tools/transformers_nuclei.py\",\"src/argicida/argicida/tools/transformers_httpx.py\",\"src/argicida/argicida/tools/transformers_subfinder.py\",\"src/argicida/argicida/tools/transformers_dnsx.py\",\"src/argicida/argicida/tools/transformers_katana.py\",\"src/argicida/argicida/tools/transformers_ffuf.py\",\"src/argicida/argicida/tools/transformers_curl.py\",\"src/argicida/argicida/tools/transformers_db.py\",\"src/argicida/argicida/tools/transformers_redis.py\",\"src/argicida/argicida/tools/transformers_mongo.py\",\"src/argicida/argicida/tools/transformers_smbclient.py\",\"src/argicida/argicida/tools/transformers_zap.py\",\"src/argicida/argicida/tools/extractors.py\",\"src/argicida/argicida/tools/extractors_base.py\",\"src/argicida/argicida/tools/extractors_nmap.py\",\"src/argicida/argicida/tools/extractors_naabu.py\",\"src/argicida/argicida/tools/extractors_httpx.py\",\"src/argicida/argicida/tools/extractors_subfinder.py\",\"src/argicida/argicida/tools/extractors_dnsx.py\",\"src/argicida/argicida/tools/extractors_nuclei.py\"]},{\"name\":\"Management &amp; CLI\",\"slug\":\"management-cli\",\"files\":[\"src/argicida/argicida/tools/management_handlers.py\",\"src/argicida/argicida/tools/artifact_handlers.py\",\"src/argicida/argicida/commands/__init__.py\",\"src/argicida/argicida/commands/cli.py\",\"src/argicida/argicida/commands/slash.py\",\"src/argicida/argicida/commands/formatting.py\",\"src/argicida/argicida/commands/update_data.py\"]},{\"name\":\"Hook Pipeline\",\"slug\":\"hook-pipeline\",\"files\":[\"src/argicida/argicida/hooks/__init__.py\",\"src/argicida/argicida/hooks/adapters.py\",\"src/argicida/argicida/hooks/asset_extractor.py\",\"src/argicida/argicida/hooks/autonomy.py\",\"src/argicida/argicida/hooks/context.py\",\"src/argicida/argicida/hooks/context_sections.py\",\"src/argicida/argicida/hooks/coverage.py\",\"src/argicida/argicida/hooks/evidence.py\",\"src/argicida/argicida/hooks/normalizer.py\",\"src/argicida/argicida/hooks/output_store.py\",\"src/argicida/argicida/hooks/parsers.py\",\"src/argicida/argicida/hooks/perceptor.py\",\"src/argicida/argicida/hooks/result_transformers.py\",\"src/argicida/argicida/hooks/scope_guard.py\",\"src/argicida/argicida/hooks/service_registry.py\",\"src/argicida/argicida/hooks/session_start.py\",\"src/argicida/argicida/hooks/skill_loader.py\"]},{\"name\":\"Triage Engine\",\"slug\":\"triage-engine\",\"files\":[\"src/argicida/argicida/triage/__init__.py\",\"src/argicida/argicida/triage/classifier.py\",\"src/argicida/argicida/triage/scorer.py\",\"src/argicida/argicida/triage/dedup.py\",\"src/argicida/argicida/triage/data.py\",\"src/argicida/argicida/triage/discovery.py\",\"scripts/generate_triage_diagrams.py\",\"scripts/generate_pipeline_diagrams.py\"]},{\"name\":\"ZAP Integration\",\"slug\":\"zap-integration\",\"files\":[\"src/argicida/argicida/zap/__init__.py\",\"src/argicida/argicida/zap/client.py\",\"src/argicida/argicida/zap/manager.py\",\"src/argicida/argicida/zap/plan_executor.py\",\"src/argicida/argicida/zap/plan_generator.py\",\"src/argicida/argicida/zap/scan_policy.py\",\"src/argicida/argicida/zap/credential_tester.py\"]},{\"name\":\"Evidence &amp; Reporting\",\"slug\":\"evidence-reporting\",\"files\":[\"src/argicida/argicida/evidence/__init__.py\",\"src/argicida/argicida/evidence/chain.py\",\"src/argicida/argicida/evidence/keys.py\",\"src/argicida/argicida/evidence/signer.py\",\"src/argicida/argicida/briefing/__init__.py\",\"src/argicida/argicida/briefing/generator.py\",\"src/argicida/argicida/briefing/prompts.py\"]},{\"name\":\"Shared Types &amp; Schemas\",\"slug\":\"shared-types-schemas\",\"files\":[\"src/argicida_common/argicida_common/__init__.py\",\"src/argicida_common/argicida_common/constants.py\",\"src/argicida_common/argicida_common/finding_schema.py\",\"src/argicida_common/argicida_common/engagement_schema.py\",\"src/argicida_common/argicida_common/asset_schema.py\",\"src/argicida_common/argicida_common/credential_schema.py\"]},{\"name\":\"Container Build Infrastructure\",\"slug\":\"container-build-infrastructure\",\"files\":[\"containers/Containerfile\",\"containers/Containerfile.gateway\",\"containers/Containerfile.zap\",\"containers/entrypoint.sh\",\"containers/gateway-entrypoint.sh\",\"containers/zap-entrypoint.sh\",\"containers/dnsx-config.yaml\",\"containers/katana-config.yaml\",\"containers/naabu-config.yaml\",\"containers/nuclei-config.yaml\",\"containers/screenshot.yaml\",\"containers/nse-blocklist.txt\",\"containers/ssh-key-wrap\"]},{\"name\":\"Hermes Profile &amp; Skills\",\"slug\":\"hermes-profile-skills\",\"files\":[],\"children\":[{\"name\":\"Hermes Profile &amp; Skills \u2014 profile\",\"slug\":\"hermes-profile-skills-profile\",\"files\":[\"profile/SOUL.md\"]},{\"name\":\"Hermes Profile &amp; Skills \u2014 skills\",\"slug\":\"hermes-profile-skills-skills\",\"files\":[\"profile/skills/security/DESCRIPTION.md\",\"profile/skills/security/pentest-methodology/SKILL.md\",\"profile/skills/security/network-discovery/SKILL.md\",\"profile/skills/security/dns-enumeration/SKILL.md\",\"profile/skills/security/web-enumeration/SKILL.md\",\"profile/skills/security/web-enumeration/fingerprinting/SKILL.md\",\"profile/skills/security/web-enumeration/scanning/SKILL.md\",\"profile/skills/security/web-enumeration/crawling/SKILL.md\",\"profile/skills/security/web-enumeration/fuzzing/SKILL.md\",\"profile/skills/security/web-enumeration/api/SKILL.md\",\"profile/skills/security/web-enumeration/api-testing/SKILL.md\",\"profile/skills/security/web-enumeration/curl-techniques/SKILL.md\",\"profile/skills/security/web-enumeration/services/SKILL.md\",\"profile/skills/security/db-enumeration/SKILL.md\",\"profile/skills/security/db-enumeration/postgresql/SKILL.md\",\"profile/skills/security/db-enumeration/mysql/SKILL.md\",\"profile/skills/security/db-enumeration/mssql/SKILL.md\",\"profile/skills/security/db-enumeration/oracle/SKILL.md\",\"profile/skills/security/db-enumeration/cassandra/SKILL.md\",\"profile/skills/security/mongo-enumeration/SKILL.md\",\"profile/skills/security/redis-enumeration/SKILL.md\",\"profile/skills/security/smb-enumeration/SKILL.md\",\"profile/skills/security/rpc-nfs-enumeration/SKILL.md\",\"profile/skills/security/ssh-enumeration/SKILL.md\",\"profile/skills/security/ftp-enumeration/SKILL.md\",\"profile/skills/security/smtp-enumeration/SKILL.md\",\"profile/skills/security/telnet-enumeration/SKILL.md\"]},{\"name\":\"Hermes Profile &amp; Skills \u2014 scripts\",\"slug\":\"hermes-profile-skills-scripts\",\"files\":[\"scripts/sync_skills.py\"]}]},{\"name\":\"Other\",\"slug\":\"other\",\"files\":[],\"children\":[{\"name\":\"Other \u2014 CLAUDE.md\",\"slug\":\"other-claude-md\",\"files\":[\"CLAUDE.md\"]},{\"name\":\"Other \u2014 README.md\",\"slug\":\"other-readme-md\",\"files\":[\"README.md\"]},{\"name\":\"Other \u2014 docs\",\"slug\":\"other-docs\",\"files\":[\"docs/DEVELOPMENT-STANDARDS.md\",\"docs/RESEARCH.md\",\"docs/triage-engine.md\"]},{\"name\":\"Other \u2014 docs-archive\",\"slug\":\"other-docs-archive\",\"files\":[\"docs/archive/AUDIT-2026-07-14.md\",\"docs/archive/NOTES.md\",\"docs/archive/REFACTOR-PLAN-v2.md\",\"docs/archive/REFACTOR-PLAN.md\",\"docs/archive/REFACTOR-REFERENCES.md\",\"docs/archive/REMEDIATION-PLAN-2026-07-14.md\",\"docs/archive/audit-confirmed-findings.md\",\"docs/archive/e2e-gap-analysis.md\",\"docs/archive/full-system-audit.md\",\"docs/archive/htb-api-reference.md\",\"docs/archive/ipc-sync-async-boundary.md\",\"docs/archive/pipeline-engine.md\",\"docs/archive/plans/2026-06-30-foundation.md\",\"docs/archive/plans/2026-06-30-ipc-control-plane.md\",\"docs/archive/plans/2026-06-30-pipeline-engine.md\",\"docs/archive/plans/2026-06-30-scope-enforcement.md\",\"docs/archive/plans/2026-06-30-tool-wrappers.md\",\"docs/archive/plans/2026-06-30-triage-and-hooks.md\",\"docs/archive/plans/2026-07-01-briefing-and-trace.md\",\"docs/archive/plans/2026-07-01-container-build.md\",\"docs/archive/plans/2026-07-01-dynamic-pipeline.md\",\"docs/archive/plans/2026-07-01-e2e-gap-fixes.md\",\"docs/archive/plans/2026-07-01-hermes-plugin-shell.md\",\"docs/archive/plans/2026-07-01-integration-test-harness.md\",\"docs/archive/plans/2026-07-01-iterative-reentry.md\",\"docs/archive/plans/2026-07-01-pod-and-cli-commands.md\",\"docs/archive/plans/2026-07-01-session-lifecycle.md\",\"docs/archive/plans/2026-07-02-llm-driven-redesign.md\",\"docs/archive/plans/2026-07-02-phase3-infrastructure-hardening.md\",\"docs/archive/plans/2026-07-02-structured-perceptor.md\",\"docs/archive/plans/2026-07-15-screenshot-vision-analysis.md\",\"docs/archive/rfe/2026-07-07-engagement-query-tool.md\",\"docs/archive/rfe/2026-07-07-screenshot-vision-analysis.md\",\"docs/archive/rfe/2026-07-07-subagent-delegation.md\",\"docs/archive/rfe/2026-07-10-credential-fragment-model.md\",\"docs/archive/rfe/2026-07-11-dns-aware-scope-enforcement.md\",\"docs/archive/rfe/2026-07-13-infrastructure-vs-prompt-boundary.md\",\"docs/archive/rfe/2026-07-15-engagement-management.md\",\"docs/archive/spec-gaps.md\",\"docs/archive/specs/2026-06-29-project-structure-design.md\",\"docs/archive/specs/2026-07-01-phase-2a-hermes-integration.md\",\"docs/archive/specs/2026-07-02-hermes-convention-refactor.md\",\"docs/archive/specs/2026-07-02-llm-driven-redesign.md\",\"docs/archive/specs/2026-07-02-phase3-infrastructure-hardening.md\",\"docs/archive/specs/2026-07-02-structured-perceptor.md\",\"docs/archive/specs/2026-07-08-engagement-lifecycle.md\",\"docs/archive/specs/2026-07-09-tool-definition-registry.md\",\"docs/archive/specs/2026-07-15-screenshot-vision-analysis.md\",\"docs/archive/superpowers/plans/2026-07-01-phase1-first-light.md\",\"docs/archive/superpowers/plans/2026-07-01-scope-enforcement.md\",\"docs/archive/superpowers/plans/2026-07-02-hermes-convention-refactor.md\",\"docs/archive/superpowers/plans/2026-07-03-nse-script-fuzzy-matching.md\",\"docs/archive/superpowers/plans/2026-07-04-artifact-storage.md\",\"docs/archive/superpowers/plans/2026-07-04-db-query-finding-parser.md\",\"docs/archive/superpowers/plans/2026-07-04-db-query-tool.md\",\"docs/archive/superpowers/plans/2026-07-04-redis-query-tool.md\",\"docs/archive/superpowers/plans/2026-07-06-curl-tool.md\",\"docs/archive/superpowers/plans/2026-07-06-db-enumeration-skill-split.md\",\"docs/archive/superpowers/plans/2026-07-06-katana-ffuf-tools.md\",\"docs/archive/superpowers/plans/2026-07-06-screenshot-tool.md\",\"docs/archive/superpowers/plans/2026-07-07-normalizer-refactor.md\",\"docs/archive/superpowers/plans/2026-07-07-result-transformer.md\",\"docs/archive/superpowers/plans/2026-07-07-rpcinfo-tool.md\",\"docs/archive/superpowers/plans/2026-07-07-validation-fixes.md\",\"docs/archive/superpowers/plans/2026-07-08-engagement-lifecycle.md\",\"docs/archive/superpowers/plans/2026-07-08-engagement-query-tool.md\",\"docs/archive/superpowers/plans/2026-07-09-ssh-exec-tool.md\",\"docs/archive/superpowers/plans/2026-07-09-tool-definition-registry.md\",\"docs/archive/superpowers/plans/2026-07-10-mongo-query.md\",\"docs/archive/superpowers/plans/2026-07-10-service-coverage.md\",\"docs/archive/superpowers/plans/2026-07-13-credential-fragment-model.md\",\"docs/archive/superpowers/plans/2026-07-14-structural-ownership.md\",\"docs/archive/superpowers/plans/2026-07-15-dns-aware-scope-enforcement.md\",\"docs/archive/superpowers/plans/2026-07-16-engagement-management.md\",\"docs/archive/superpowers/plans/2026-07-20-asset-registry.md\",\"docs/archive/superpowers/plans/2026-07-21-dry-simplification.md\",\"docs/archive/superpowers/plans/2026-07-21-register-refactor.md\",\"docs/archive/superpowers/specs/2026-07-01-scope-enforcement-design.md\",\"docs/archive/superpowers/specs/2026-07-03-nse-script-fuzzy-matching-design.md\",\"docs/archive/superpowers/specs/2026-07-04-artifact-storage-design.md\",\"docs/archive/superpowers/specs/2026-07-04-db-query-finding-parser-design.md\",\"docs/archive/superpowers/specs/2026-07-04-redis-query-tool-design.md\",\"docs/archive/superpowers/specs/2026-07-06-curl-tool-design.md\",\"docs/archive/superpowers/specs/2026-07-06-db-enumeration-skill-split-design.md\",\"docs/archive/superpowers/specs/2026-07-06-katana-ffuf-tools-design.md\",\"docs/archive/superpowers/specs/2026-07-06-screenshot-tool-design.md\",\"docs/archive/superpowers/specs/2026-07-07-normalizer-refactor-design.md\",\"docs/archive/superpowers/specs/2026-07-07-result-transformer-design.md\",\"docs/archive/superpowers/specs/2026-07-07-rpcinfo-tool-design.md\",\"docs/archive/superpowers/specs/2026-07-07-validation-fixes-design.md\",\"docs/archive/superpowers/specs/2026-07-08-engagement-query-tool-design.md\",\"docs/archive/superpowers/specs/2026-07-08-subagent-delegation-design.md\",\"docs/archive/superpowers/specs/2026-07-09-ssh-exec-tool-design.md\",\"docs/archive/superpowers/specs/2026-07-10-mongo-query-design.md\",\"docs/archive/superpowers/specs/2026-07-10-service-coverage-design.md\",\"docs/archive/superpowers/specs/2026-07-13-credential-fragment-model-design.md\",\"docs/archive/superpowers/specs/2026-07-14-remediation-design.md\",\"docs/archive/superpowers/specs/2026-07-15-dns-aware-scope-enforcement-design.md\",\"docs/archive/superpowers/specs/2026-07-16-engagement-management-design.md\",\"docs/archive/superpowers/specs/2026-07-20-asset-registry-design.md\",\"docs/archive/superpowers/specs/2026-07-21-dry-simplification-design.md\",\"docs/archive/superpowers/specs/2026-07-21-register-refactor-design.md\"]},{\"name\":\"Other \u2014 docs-research\",\"slug\":\"other-docs-research\",\"files\":[\"docs/research/2026-07-02-offensive-security-research.md\",\"docs/research/2026-07-04-hermes-configuration-deep-dive.md\",\"docs/research/2026-07-10-c2-integration-strategy.md\"]},{\"name\":\"Other \u2014 docs-rfe\",\"slug\":\"other-docs-rfe\",\"files\":[\"docs/rfe/2026-07-11-zap-proxy-web-scanning.md\",\"docs/rfe/2026-07-25-zap-automation-framework.md\",\"docs/rfe/2026-07-26-test-target-infrastructure.md\"]},{\"name\":\"Other \u2014 docs-superpowers\",\"slug\":\"other-docs-superpowers\",\"files\":[\"docs/superpowers/archive/2026-07-22-zap-deep-integration-design.md\",\"docs/superpowers/archive/2026-07-22-zap-deep-integration.md\",\"docs/superpowers/archive/2026-07-22-zap-integration-gaps-design.md\",\"docs/superpowers/archive/2026-07-22-zap-integration-gaps.md\",\"docs/superpowers/archive/2026-07-23-e2e-research-findings.md\",\"docs/superpowers/archive/2026-07-23-e2e-test-integrity-design.md\",\"docs/superpowers/archive/2026-07-23-e2e-test-integrity.md\",\"docs/superpowers/plans/2026-07-26-zap-af-pipeline-integration.md\",\"docs/superpowers/plans/2026-07-27-phase-aware-autonomy.md\",\"docs/superpowers/plans/archive/2026-07-29-gateway-network-isolation.md\",\"docs/superpowers/specs/2026-07-26-zap-af-pipeline-integration-design.md\",\"docs/superpowers/specs/2026-07-27-phase-aware-autonomy-design.md\",\"docs/superpowers/specs/archive/2026-07-29-gateway-network-isolation-design.md\"]},{\"name\":\"Other \u2014 profile\",\"slug\":\"other-profile\",\"files\":[\"profile/README.md\",\"profile/config.yaml\",\"profile/distribution.yaml\"]},{\"name\":\"Other \u2014 profile-skins\",\"slug\":\"other-profile-skins\",\"files\":[\"profile/skins/argicida.yaml\"]},{\"name\":\"Other \u2014 pyproject.toml\",\"slug\":\"other-pyproject-toml\",\"files\":[\"pyproject.toml\"]},{\"name\":\"Other \u2014 src-argicida\",\"slug\":\"other-src-argicida\",\"files\":[\"src/argicida/argicida/py.typed\",\"src/argicida/pyproject.toml\"]},{\"name\":\"Other \u2014 src-argicida_agent\",\"slug\":\"other-src-argicida-agent\",\"files\":[\"src/argicida_agent/argicida_agent/py.typed\",\"src/argicida_agent/pyproject.toml\"]},{\"name\":\"Other \u2014 src-argicida_common\",\"slug\":\"other-src-argicida-common\",\"files\":[\"src/argicida_common/argicida_common/py.typed\",\"src/argicida_common/pyproject.toml\"]},{\"name\":\"Other \u2014 tests\",\"slug\":\"other-tests\",\"files\":[\"tests/__init__.py\",\"tests/conftest.py\",\"tests/factories.py\"]},{\"name\":\"Other \u2014 tests-argicida\",\"slug\":\"other-tests-argicida\",\"files\":[\"tests/argicida/briefing/__init__.py\",\"tests/argicida/briefing/test_generator.py\",\"tests/argicida/briefing/test_prompts.py\",\"tests/argicida/commands/__init__.py\",\"tests/argicida/commands/test_cli.py\",\"tests/argicida/commands/test_formatting.py\",\"tests/argicida/commands/test_slash.py\",\"tests/argicida/commands/test_update_data.py\",\"tests/argicida/conftest.py\",\"tests/argicida/db/test_asset_repo.py\",\"tests/argicida/db/test_asset_tech.py\",\"tests/argicida/db/test_candidate_engine.py\",\"tests/argicida/db/test_credential_helpers.py\",\"tests/argicida/db/test_credentials.py\",\"tests/argicida/db/test_encryption.py\",\"tests/argicida/db/test_finding_repo.py\",\"tests/argicida/db/test_helpers.py\",\"tests/argicida/db/test_migrations.py\",\"tests/argicida/db/test_store.py\",\"tests/argicida/db/test_store_async.py\",\"tests/argicida/engagement/__init__.py\",\"tests/argicida/engagement/test_lifecycle.py\",\"tests/argicida/engagement/test_session.py\",\"tests/argicida/engagement/test_unbind.py\",\"tests/argicida/evidence/test_chain.py\",\"tests/argicida/evidence/test_chain_clear.py\",\"tests/argicida/evidence/test_keys.py\",\"tests/argicida/evidence/test_signer.py\",\"tests/argicida/hooks/conftest.py\",\"tests/argicida/hooks/test_adapters.py\",\"tests/argicida/hooks/test_asset_context.py\",\"tests/argicida/hooks/test_asset_coverage.py\",\"tests/argicida/hooks/test_asset_extractor.py\",\"tests/argicida/hooks/test_autonomy.py\",\"tests/argicida/hooks/test_context.py\",\"tests/argicida/hooks/test_context_tech.py\",\"tests/argicida/hooks/test_coverage_deep_testing.py\",\"tests/argicida/hooks/test_evidence.py\",\"tests/argicida/hooks/test_finalize_zap.py\",\"tests/argicida/hooks/test_normalizer.py\",\"tests/argicida/hooks/test_normalizer_errors.py\",\"tests/argicida/hooks/test_normalizer_perceptor.py\",\"tests/argicida/hooks/test_output_store.py\",\"tests/argicida/hooks/test_parse_result.py\",\"tests/argicida/hooks/test_parsers.py\",\"tests/argicida/hooks/test_password_decoding.py\",\"tests/argicida/hooks/test_perceptor.py\",\"tests/argicida/hooks/test_result_transformers.py\",\"tests/argicida/hooks/test_scope_expansion.py\",\"tests/argicida/hooks/test_scope_guard.py\",\"tests/argicida/hooks/test_service_registry.py\",\"tests/argicida/hooks/test_session_lifecycle.py\",\"tests/argicida/hooks/test_session_start.py\",\"tests/argicida/ipc/test_client.py\",\"tests/argicida/pod/test_builder.py\",\"tests/argicida/pod/test_config.py\",\"tests/argicida/pod/test_launcher.py\",\"tests/argicida/pod/test_launcher_zap.py\",\"tests/argicida/pod/test_manager.py\",\"tests/argicida/pod/test_session.py\",\"tests/argicida/pod/test_session_host_entries.py\",\"tests/argicida/pod/test_vpn_endpoint.py\",\"tests/argicida/registration/__init__.py\",\"tests/argicida/registration/test_context.py\",\"tests/argicida/registration/test_state.py\",\"tests/argicida/scope/conftest.py\",\"tests/argicida/scope/test_db_query_scope.py\",\"tests/argicida/scope/test_hostname_resolution.py\",\"tests/argicida/scope/test_netfilter.py\",\"tests/argicida/scope/test_normalize_target.py\",\"tests/argicida/scope/test_redis_cli_profile.py\",\"tests/argicida/scope/test_rpcinfo_profile.py\",\"tests/argicida/scope/test_validator.py\",\"tests/argicida/test_asset_consistency.py\",\"tests/argicida/test_consistency.py\",\"tests/argicida/test_phase_migration.py\",\"tests/argicida/test_register.py\",\"tests/argicida/test_session_consistency.py\",\"tests/argicida/tools/conftest.py\",\"tests/argicida/tools/defs/__init__.py\",\"tests/argicida/tools/defs/test_all_defs.py\",\"tests/argicida/tools/defs/test_discovery.py\",\"tests/argicida/tools/defs/test_mongo_query_def.py\",\"tests/argicida/tools/defs/test_naabu_def.py\",\"tests/argicida/tools/defs/test_protocol.py\",\"tests/argicida/tools/defs/test_ssh_exec_def.py\",\"tests/argicida/tools/test_add_host_asset.py\",\"tests/argicida/tools/test_add_host_dnsmasq.py\",\"tests/argicida/tools/test_artifact_handlers.py\",\"tests/argicida/tools/test_asset_query_handlers.py\",\"tests/argicida/tools/test_base.py\",\"tests/argicida/tools/test_curl_handler.py\",\"tests/argicida/tools/test_curl_normalizer.py\",\"tests/argicida/tools/test_db_query_handler.py\",\"tests/argicida/tools/test_db_query_normalizer.py\",\"tests/argicida/tools/test_engagement_query_fragments.py\",\"tests/argicida/tools/test_extractor_base.py\",\"tests/argicida/tools/test_extractors.py\",\"tests/argicida/tools/test_ffuf_normalizer.py\",\"tests/argicida/tools/test_handlers.py\",\"tests/argicida/tools/test_katana_normalizer.py\",\"tests/argicida/tools/test_management_handlers.py\",\"tests/argicida/tools/test_management_handlers_zap.py\",\"tests/argicida/tools/test_management_helpers.py\",\"tests/argicida/tools/test_normalizer_components.py\",\"tests/argicida/tools/test_normalizer_has_flag.py\",\"tests/argicida/tools/test_normalizer_shared_classes.py\",\"tests/argicida/tools/test_normalizers.py\",\"tests/argicida/tools/test_normalizers_mongo.py\",\"tests/argicida/tools/test_normalizers_nuclei.py\",\"tests/argicida/tools/test_normalizers_smb.py\",\"tests/argicida/tools/test_normalizers_ssh.py\",\"tests/argicida/tools/test_normalizers_zap.py\",\"tests/argicida/tools/test_parsers_make_finding.py\",\"tests/argicida/tools/test_parsers_mongo.py\",\"tests/argicida/tools/test_parsers_zap.py\",\"tests/argicida/tools/test_redis_query_normalizer.py\",\"tests/argicida/tools/test_schemas.py\",\"tests/argicida/tools/test_screenshot_handler.py\",\"tests/argicida/tools/test_screenshot_vision.py\",\"tests/argicida/tools/test_shared_utils.py\",\"tests/argicida/tools/test_tool_def_extractor.py\",\"tests/argicida/tools/test_transformer_infrastructure.py\",\"tests/argicida/tools/test_transformers_dnsx_proposal.py\",\"tests/argicida/tools/test_transformers_mongo.py\",\"tests/argicida/tools/test_transformers_ssh.py\",\"tests/argicida/tools/test_transformers_zap.py\",\"tests/argicida/tools/test_zap_new_tools.py\",\"tests/argicida/triage/test_asset_discovery.py\",\"tests/argicida/triage/test_classifier.py\",\"tests/argicida/triage/test_data.py\",\"tests/argicida/triage/test_dedup.py\",\"tests/argicida/triage/test_scorer.py\",\"tests/argicida/zap/__init__.py\",\"tests/argicida/zap/test_client.py\",\"tests/argicida/zap/test_client_af.py\",\"tests/argicida/zap/test_client_intel.py\",\"tests/argicida/zap/test_credential_tester.py\",\"tests/argicida/zap/test_manager.py\",\"tests/argicida/zap/test_manager_af.py\",\"tests/argicida/zap/test_plan_executor.py\",\"tests/argicida/zap/test_plan_generator.py\",\"tests/argicida/zap/test_scan_policy.py\",\"tests/argicida/zap/test_scan_policy_passive.py\"]},{\"name\":\"Other \u2014 tests-argicida_agent\",\"slug\":\"other-tests-argicida-agent\",\"files\":[\"tests/argicida_agent/executor/test_runner.py\",\"tests/argicida_agent/ipc/test_server.py\",\"tests/argicida_agent/test_main.py\"]},{\"name\":\"Other \u2014 tests-argicida_common\",\"slug\":\"other-tests-argicida-common\",\"files\":[\"tests/argicida_common/test_asset_schema.py\",\"tests/argicida_common/test_constants.py\",\"tests/argicida_common/test_credential_schema.py\",\"tests/argicida_common/test_engagement_schema.py\",\"tests/argicida_common/test_finding_schema.py\",\"tests/argicida_common/test_ipc_protocol.py\",\"tests/argicida_common/test_ipc_schema.py\",\"tests/argicida_common/test_scope_schema.py\"]},{\"name\":\"Other \u2014 tests-e2e\",\"slug\":\"other-tests-e2e\",\"files\":[\"tests/e2e/__init__.py\",\"tests/e2e/assertions.py\",\"tests/e2e/conftest.py\",\"tests/e2e/fault_injector.py\",\"tests/e2e/harness.py\",\"tests/e2e/test_e2e_smoke.py\"]},{\"name\":\"Other \u2014 tests-integration\",\"slug\":\"other-tests-integration\",\"files\":[\"tests/integration/__init__.py\",\"tests/integration/conftest.py\",\"tests/integration/test_asset_pipeline.py\",\"tests/integration/test_hook_pipeline.py\",\"tests/integration/test_phase_lifecycle.py\",\"tests/integration/test_registration.py\",\"tests/integration/test_session_lifecycle.py\",\"tests/integration/test_slash_commands.py\",\"tests/integration/test_tool_dispatch.py\"]},{\"name\":\"Other \u2014 tests-pod_integration\",\"slug\":\"other-tests-pod-integration\",\"files\":[\"tests/pod_integration/__init__.py\",\"tests/pod_integration/conftest.py\",\"tests/pod_integration/test_artifact_transfer.py\",\"tests/pod_integration/test_asset_registry.py\",\"tests/pod_integration/test_container_build.py\",\"tests/pod_integration/test_diagnostic_pod.py\",\"tests/pod_integration/test_engagement_management.py\",\"tests/pod_integration/test_fault_recovery.py\",\"tests/pod_integration/test_fault_stale_session.py\",\"tests/pod_integration/test_first_light.py\",\"tests/pod_integration/test_gateway_fail_closed.py\",\"tests/pod_integration/test_gateway_isolation.py\",\"tests/pod_integration/test_ipc_file_write.py\",\"tests/pod_integration/test_zap.py\",\"tests/pod_integration/test_zap_af.py\"]},{\"name\":\"Other \u2014 tests-scripts\",\"slug\":\"other-tests-scripts\",\"files\":[\"tests/scripts/__init__.py\",\"tests/scripts/test_sync_skills.py\"]}]}];\nvar META = {\"fromCommit\":\"eaf9cefcbeb60a781d7dfcee322c37c87c494cf3\",\"generatedAt\":\"2026-07-30T20:43:19.245Z\",\"model\":\"\",\"lang\":\"\",\"moduleFiles\":{\"Plugin Registration &amp; Core\":[\"src/argicida/argicida/__init__.py\",\"src/argicida/argicida/registration/__init__.py\",\"src/argicida/argicida/registration/context.py\",\"src/argicida/argicida/registration/state.py\",\"src/argicida/argicida/registration/hooks.py\",\"src/argicida/argicida/registration/scan_tools.py\",\"src/argicida/argicida/registration/management_tools.py\",\"src/argicida/argicida/registration/commands.py\",\"src/argicida/argicida/utils.py\",\"src/argicida/argicida/plugin.yaml\"],\"Engagement &amp; Database\":[\"src/argicida/argicida/engagement/__init__.py\",\"src/argicida/argicida/engagement/lifecycle.py\",\"src/argicida/argicida/engagement/session.py\",\"src/argicida/argicida/db/__init__.py\",\"src/argicida/argicida/db/store.py\",\"src/argicida/argicida/db/asset_repo.py\",\"src/argicida/argicida/db/credential_repo.py\",\"src/argicida/argicida/db/finding_repo.py\",\"src/argicida/argicida/db/evidence_repo.py\",\"src/argicida/argicida/db/vpn_repo.py\",\"src/argicida/argicida/db/helpers.py\",\"src/argicida/argicida/db/migrations.py\",\"src/argicida/argicida/db/encryption.py\"],\"Pod Management\":[\"src/argicida/argicida/pod/__init__.py\",\"src/argicida/argicida/pod/launcher.py\",\"src/argicida/argicida/pod/manager.py\",\"src/argicida/argicida/pod/session.py\",\"src/argicida/argicida/pod/config.py\",\"src/argicida/argicida/pod/builder.py\",\"src/argicida/argicida/pod/vpn.py\"],\"Container Agent\":[\"src/argicida_agent/argicida_agent/__init__.py\",\"src/argicida_agent/argicida_agent/__main__.py\",\"src/argicida_agent/argicida_agent/executor/__init__.py\",\"src/argicida_agent/argicida_agent/executor/runner.py\",\"src/argicida_agent/argicida_agent/ipc/__init__.py\",\"src/argicida_agent/argicida_agent/ipc/server.py\"],\"IPC Layer\":[\"src/argicida/argicida/ipc/__init__.py\",\"src/argicida/argicida/ipc/client.py\",\"src/argicida_common/argicida_common/ipc_protocol.py\",\"src/argicida_common/argicida_common/ipc_schema.py\"],\"Scope Enforcement\":[\"src/argicida/argicida/scope/__init__.py\",\"src/argicida/argicida/scope/netfilter.py\",\"src/argicida/argicida/scope/validator.py\",\"src/argicida_common/argicida_common/scope_schema.py\"],\"Scan Tool Framework\":[\"src/argicida/argicida/tools/__init__.py\",\"src/argicida/argicida/tools/base.py\",\"src/argicida/argicida/tools/schemas.py\",\"src/argicida/argicida/tools/defs/__init__.py\",\"src/argicida/argicida/tools/defs/_protocol.py\",\"src/argicida/argicida/tools/defs/_pd_common.py\",\"src/argicida/argicida/tools/defs/nmap.py\",\"src/argicida/argicida/tools/defs/naabu.py\",\"src/argicida/argicida/tools/defs/nuclei.py\",\"src/argicida/argicida/tools/defs/httpx.py\",\"src/argicida/argicida/tools/defs/subfinder.py\",\"src/argicida/argicida/tools/defs/dnsx.py\",\"src/argicida/argicida/tools/defs/katana.py\",\"src/argicida/argicida/tools/defs/ffuf.py\",\"src/argicida/argicida/tools/defs/curl.py\",\"src/argicida/argicida/tools/defs/tracepath.py\",\"src/argicida/argicida/tools/defs/rpcinfo.py\",\"src/argicida/argicida/tools/defs/smbclient.py\",\"src/argicida/argicida/tools/defs/db_query.py\",\"src/argicida/argicida/tools/defs/redis_query.py\",\"src/argicida/argicida/tools/defs/mongo_query.py\",\"src/argicida/argicida/tools/defs/ssh_exec.py\",\"src/argicida/argicida/tools/normalizers.py\",\"src/argicida/argicida/tools/normalizers_nmap.py\",\"src/argicida/argicida/tools/normalizers_nuclei.py\",\"src/argicida/argicida/tools/normalizers_curl.py\",\"src/argicida/argicida/tools/normalizers_db.py\",\"src/argicida/argicida/tools/normalizers_redis.py\",\"src/argicida/argicida/tools/normalizers_mongo.py\",\"src/argicida/argicida/tools/normalizers_katana.py\",\"src/argicida/argicida/tools/normalizers_smb.py\",\"src/argicida/argicida/tools/normalizers_ssh.py\",\"src/argicida/argicida/tools/normalizers_zap.py\",\"src/argicida/argicida/tools/parsers.py\",\"src/argicida/argicida/tools/parsers_nmap.py\",\"src/argicida/argicida/tools/parsers_naabu.py\",\"src/argicida/argicida/tools/parsers_nuclei.py\",\"src/argicida/argicida/tools/parsers_dnsx.py\",\"src/argicida/argicida/tools/parsers_db.py\",\"src/argicida/argicida/tools/parsers_redis.py\",\"src/argicida/argicida/tools/parsers_mongo.py\",\"src/argicida/argicida/tools/parsers_smbclient.py\",\"src/argicida/argicida/tools/parsers_zap.py\",\"src/argicida/argicida/tools/transformers.py\",\"src/argicida/argicida/tools/transformers_nmap.py\",\"src/argicida/argicida/tools/transformers_naabu.py\",\"src/argicida/argicida/tools/transformers_nuclei.py\",\"src/argicida/argicida/tools/transformers_httpx.py\",\"src/argicida/argicida/tools/transformers_subfinder.py\",\"src/argicida/argicida/tools/transformers_dnsx.py\",\"src/argicida/argicida/tools/transformers_katana.py\",\"src/argicida/argicida/tools/transformers_ffuf.py\",\"src/argicida/argicida/tools/transformers_curl.py\",\"src/argicida/argicida/tools/transformers_db.py\",\"src/argicida/argicida/tools/transformers_redis.py\",\"src/argicida/argicida/tools/transformers_mongo.py\",\"src/argicida/argicida/tools/transformers_smbclient.py\",\"src/argicida/argicida/tools/transformers_zap.py\",\"src/argicida/argicida/tools/extractors.py\",\"src/argicida/argicida/tools/extractors_base.py\",\"src/argicida/argicida/tools/extractors_nmap.py\",\"src/argicida/argicida/tools/extractors_naabu.py\",\"src/argicida/argicida/tools/extractors_httpx.py\",\"src/argicida/argicida/tools/extractors_subfinder.py\",\"src/argicida/argicida/tools/extractors_dnsx.py\",\"src/argicida/argicida/tools/extractors_nuclei.py\"],\"Management &amp; CLI\":[\"src/argicida/argicida/tools/management_handlers.py\",\"src/argicida/argicida/tools/artifact_handlers.py\",\"src/argicida/argicida/commands/__init__.py\",\"src/argicida/argicida/commands/cli.py\",\"src/argicida/argicida/commands/slash.py\",\"src/argicida/argicida/commands/formatting.py\",\"src/argicida/argicida/commands/update_data.py\"],\"Hook Pipeline\":[\"src/argicida/argicida/hooks/__init__.py\",\"src/argicida/argicida/hooks/adapters.py\",\"src/argicida/argicida/hooks/asset_extractor.py\",\"src/argicida/argicida/hooks/autonomy.py\",\"src/argicida/argicida/hooks/context.py\",\"src/argicida/argicida/hooks/context_sections.py\",\"src/argicida/argicida/hooks/coverage.py\",\"src/argicida/argicida/hooks/evidence.py\",\"src/argicida/argicida/hooks/normalizer.py\",\"src/argicida/argicida/hooks/output_store.py\",\"src/argicida/argicida/hooks/parsers.py\",\"src/argicida/argicida/hooks/perceptor.py\",\"src/argicida/argicida/hooks/result_transformers.py\",\"src/argicida/argicida/hooks/scope_guard.py\",\"src/argicida/argicida/hooks/service_registry.py\",\"src/argicida/argicida/hooks/session_start.py\",\"src/argicida/argicida/hooks/skill_loader.py\"],\"Triage Engine\":[\"src/argicida/argicida/triage/__init__.py\",\"src/argicida/argicida/triage/classifier.py\",\"src/argicida/argicida/triage/scorer.py\",\"src/argicida/argicida/triage/dedup.py\",\"src/argicida/argicida/triage/data.py\",\"src/argicida/argicida/triage/discovery.py\",\"scripts/generate_triage_diagrams.py\",\"scripts/generate_pipeline_diagrams.py\"],\"ZAP Integration\":[\"src/argicida/argicida/zap/__init__.py\",\"src/argicida/argicida/zap/client.py\",\"src/argicida/argicida/zap/manager.py\",\"src/argicida/argicida/zap/plan_executor.py\",\"src/argicida/argicida/zap/plan_generator.py\",\"src/argicida/argicida/zap/scan_policy.py\",\"src/argicida/argicida/zap/credential_tester.py\"],\"Evidence &amp; Reporting\":[\"src/argicida/argicida/evidence/__init__.py\",\"src/argicida/argicida/evidence/chain.py\",\"src/argicida/argicida/evidence/keys.py\",\"src/argicida/argicida/evidence/signer.py\",\"src/argicida/argicida/briefing/__init__.py\",\"src/argicida/argicida/briefing/generator.py\",\"src/argicida/argicida/briefing/prompts.py\"],\"Shared Types &amp; Schemas\":[\"src/argicida_common/argicida_common/__init__.py\",\"src/argicida_common/argicida_common/constants.py\",\"src/argicida_common/argicida_common/finding_schema.py\",\"src/argicida_common/argicida_common/engagement_schema.py\",\"src/argicida_common/argicida_common/asset_schema.py\",\"src/argicida_common/argicida_common/credential_schema.py\"],\"Container Build Infrastructure\":[\"containers/Containerfile\",\"containers/Containerfile.gateway\",\"containers/Containerfile.zap\",\"containers/entrypoint.sh\",\"containers/gateway-entrypoint.sh\",\"containers/zap-entrypoint.sh\",\"containers/dnsx-config.yaml\",\"containers/katana-config.yaml\",\"containers/naabu-config.yaml\",\"containers/nuclei-config.yaml\",\"containers/screenshot.yaml\",\"containers/nse-blocklist.txt\",\"containers/ssh-key-wrap\"],\"Hermes Profile &amp; Skills\":[\"profile/SOUL.md\",\"profile/skills/security/DESCRIPTION.md\",\"profile/skills/security/pentest-methodology/SKILL.md\",\"profile/skills/security/network-discovery/SKILL.md\",\"profile/skills/security/dns-enumeration/SKILL.md\",\"profile/skills/security/web-enumeration/SKILL.md\",\"profile/skills/security/web-enumeration/fingerprinting/SKILL.md\",\"profile/skills/security/web-enumeration/scanning/SKILL.md\",\"profile/skills/security/web-enumeration/crawling/SKILL.md\",\"profile/skills/security/web-enumeration/fuzzing/SKILL.md\",\"profile/skills/security/web-enumeration/api/SKILL.md\",\"profile/skills/security/web-enumeration/api-testing/SKILL.md\",\"profile/skills/security/web-enumeration/curl-techniques/SKILL.md\",\"profile/skills/security/web-enumeration/services/SKILL.md\",\"profile/skills/security/db-enumeration/SKILL.md\",\"profile/skills/security/db-enumeration/postgresql/SKILL.md\",\"profile/skills/security/db-enumeration/mysql/SKILL.md\",\"profile/skills/security/db-enumeration/mssql/SKILL.md\",\"profile/skills/security/db-enumeration/oracle/SKILL.md\",\"profile/skills/security/db-enumeration/cassandra/SKILL.md\",\"profile/skills/security/mongo-enumeration/SKILL.md\",\"profile/skills/security/redis-enumeration/SKILL.md\",\"profile/skills/security/smb-enumeration/SKILL.md\",\"profile/skills/security/rpc-nfs-enumeration/SKILL.md\",\"profile/skills/security/ssh-enumeration/SKILL.md\",\"profile/skills/security/ftp-enumeration/SKILL.md\",\"profile/skills/security/smtp-enumeration/SKILL.md\",\"profile/skills/security/telnet-enumeration/SKILL.md\",\"scripts/sync_skills.py\"],\"Hermes Profile &amp; Skills \u2014 profile\":[\"profile/SOUL.md\"],\"Hermes Profile &amp; Skills \u2014 skills\":[\"profile/skills/security/DESCRIPTION.md\",\"profile/skills/security/pentest-methodology/SKILL.md\",\"profile/skills/security/network-discovery/SKILL.md\",\"profile/skills/security/dns-enumeration/SKILL.md\",\"profile/skills/security/web-enumeration/SKILL.md\",\"profile/skills/security/web-enumeration/fingerprinting/SKILL.md\",\"profile/skills/security/web-enumeration/scanning/SKILL.md\",\"profile/skills/security/web-enumeration/crawling/SKILL.md\",\"profile/skills/security/web-enumeration/fuzzing/SKILL.md\",\"profile/skills/security/web-enumeration/api/SKILL.md\",\"profile/skills/security/web-enumeration/api-testing/SKILL.md\",\"profile/skills/security/web-enumeration/curl-techniques/SKILL.md\",\"profile/skills/security/web-enumeration/services/SKILL.md\",\"profile/skills/security/db-enumeration/SKILL.md\",\"profile/skills/security/db-enumeration/postgresql/SKILL.md\",\"profile/skills/security/db-enumeration/mysql/SKILL.md\",\"profile/skills/security/db-enumeration/mssql/SKILL.md\",\"profile/skills/security/db-enumeration/oracle/SKILL.md\",\"profile/skills/security/db-enumeration/cassandra/SKILL.md\",\"profile/skills/security/mongo-enumeration/SKILL.md\",\"profile/skills/security/redis-enumeration/SKILL.md\",\"profile/skills/security/smb-enumeration/SKILL.md\",\"profile/skills/security/rpc-nfs-enumeration/SKILL.md\",\"profile/skills/security/ssh-enumeration/SKILL.md\",\"profile/skills/security/ftp-enumeration/SKILL.md\",\"profile/skills/security/smtp-enumeration/SKILL.md\",\"profile/skills/security/telnet-enumeration/SKILL.md\"],\"Hermes Profile &amp; Skills \u2014 scripts\":[\"scripts/sync_skills.py\"],\"Other\":[\"CLAUDE.md\",\"README.md\",\"docs/DEVELOPMENT-STANDARDS.md\",\"docs/RESEARCH.md\",\"docs/triage-engine.md\",\"docs/archive/AUDIT-2026-07-14.md\",\"docs/archive/NOTES.md\",\"docs/archive/REFACTOR-PLAN-v2.md\",\"docs/archive/REFACTOR-PLAN.md\",\"docs/archive/REFACTOR-REFERENCES.md\",\"docs/archive/REMEDIATION-PLAN-2026-07-14.md\",\"docs/archive/audit-confirmed-findings.md\",\"docs/archive/e2e-gap-analysis.md\",\"docs/archive/full-system-audit.md\",\"docs/archive/htb-api-reference.md\",\"docs/archive/ipc-sync-async-boundary.md\",\"docs/archive/pipeline-engine.md\",\"docs/archive/plans/2026-06-30-foundation.md\",\"docs/archive/plans/2026-06-30-ipc-control-plane.md\",\"docs/archive/plans/2026-06-30-pipeline-engine.md\",\"docs/archive/plans/2026-06-30-scope-enforcement.md\",\"docs/archive/plans/2026-06-30-tool-wrappers.md\",\"docs/archive/plans/2026-06-30-triage-and-hooks.md\",\"docs/archive/plans/2026-07-01-briefing-and-trace.md\",\"docs/archive/plans/2026-07-01-container-build.md\",\"docs/archive/plans/2026-07-01-dynamic-pipeline.md\",\"docs/archive/plans/2026-07-01-e2e-gap-fixes.md\",\"docs/archive/plans/2026-07-01-hermes-plugin-shell.md\",\"docs/archive/plans/2026-07-01-integration-test-harness.md\",\"docs/archive/plans/2026-07-01-iterative-reentry.md\",\"docs/archive/plans/2026-07-01-pod-and-cli-commands.md\",\"docs/archive/plans/2026-07-01-session-lifecycle.md\",\"docs/archive/plans/2026-07-02-llm-driven-redesign.md\",\"docs/archive/plans/2026-07-02-phase3-infrastructure-hardening.md\",\"docs/archive/plans/2026-07-02-structured-perceptor.md\",\"docs/archive/plans/2026-07-15-screenshot-vision-analysis.md\",\"docs/archive/rfe/2026-07-07-engagement-query-tool.md\",\"docs/archive/rfe/2026-07-07-screenshot-vision-analysis.md\",\"docs/archive/rfe/2026-07-07-subagent-delegation.md\",\"docs/archive/rfe/2026-07-10-credential-fragment-model.md\",\"docs/archive/rfe/2026-07-11-dns-aware-scope-enforcement.md\",\"docs/archive/rfe/2026-07-13-infrastructure-vs-prompt-boundary.md\",\"docs/archive/rfe/2026-07-15-engagement-management.md\",\"docs/archive/spec-gaps.md\",\"docs/archive/specs/2026-06-29-project-structure-design.md\",\"docs/archive/specs/2026-07-01-phase-2a-hermes-integration.md\",\"docs/archive/specs/2026-07-02-hermes-convention-refactor.md\",\"docs/archive/specs/2026-07-02-llm-driven-redesign.md\",\"docs/archive/specs/2026-07-02-phase3-infrastructure-hardening.md\",\"docs/archive/specs/2026-07-02-structured-perceptor.md\",\"docs/archive/specs/2026-07-08-engagement-lifecycle.md\",\"docs/archive/specs/2026-07-09-tool-definition-registry.md\",\"docs/archive/specs/2026-07-15-screenshot-vision-analysis.md\",\"docs/archive/superpowers/plans/2026-07-01-phase1-first-light.md\",\"docs/archive/superpowers/plans/2026-07-01-scope-enforcement.md\",\"docs/archive/superpowers/plans/2026-07-02-hermes-convention-refactor.md\",\"docs/archive/superpowers/plans/2026-07-03-nse-script-fuzzy-matching.md\",\"docs/archive/superpowers/plans/2026-07-04-artifact-storage.md\",\"docs/archive/superpowers/plans/2026-07-04-db-query-finding-parser.md\",\"docs/archive/superpowers/plans/2026-07-04-db-query-tool.md\",\"docs/archive/superpowers/plans/2026-07-04-redis-query-tool.md\",\"docs/archive/superpowers/plans/2026-07-06-curl-tool.md\",\"docs/archive/superpowers/plans/2026-07-06-db-enumeration-skill-split.md\",\"docs/archive/superpowers/plans/2026-07-06-katana-ffuf-tools.md\",\"docs/archive/superpowers/plans/2026-07-06-screenshot-tool.md\",\"docs/archive/superpowers/plans/2026-07-07-normalizer-refactor.md\",\"docs/archive/superpowers/plans/2026-07-07-result-transformer.md\",\"docs/archive/superpowers/plans/2026-07-07-rpcinfo-tool.md\",\"docs/archive/superpowers/plans/2026-07-07-validation-fixes.md\",\"docs/archive/superpowers/plans/2026-07-08-engagement-lifecycle.md\",\"docs/archive/superpowers/plans/2026-07-08-engagement-query-tool.md\",\"docs/archive/superpowers/plans/2026-07-09-ssh-exec-tool.md\",\"docs/archive/superpowers/plans/2026-07-09-tool-definition-registry.md\",\"docs/archive/superpowers/plans/2026-07-10-mongo-query.md\",\"docs/archive/superpowers/plans/2026-07-10-service-coverage.md\",\"docs/archive/superpowers/plans/2026-07-13-credential-fragment-model.md\",\"docs/archive/superpowers/plans/2026-07-14-structural-ownership.md\",\"docs/archive/superpowers/plans/2026-07-15-dns-aware-scope-enforcement.md\",\"docs/archive/superpowers/plans/2026-07-16-engagement-management.md\",\"docs/archive/superpowers/plans/2026-07-20-asset-registry.md\",\"docs/archive/superpowers/plans/2026-07-21-dry-simplification.md\",\"docs/archive/superpowers/plans/2026-07-21-register-refactor.md\",\"docs/archive/superpowers/specs/2026-07-01-scope-enforcement-design.md\",\"docs/archive/superpowers/specs/2026-07-03-nse-script-fuzzy-matching-design.md\",\"docs/archive/superpowers/specs/2026-07-04-artifact-storage-design.md\",\"docs/archive/superpowers/specs/2026-07-04-db-query-finding-parser-design.md\",\"docs/archive/superpowers/specs/2026-07-04-redis-query-tool-design.md\",\"docs/archive/superpowers/specs/2026-07-06-curl-tool-design.md\",\"docs/archive/superpowers/specs/2026-07-06-db-enumeration-skill-split-design.md\",\"docs/archive/superpowers/specs/2026-07-06-katana-ffuf-tools-design.md\",\"docs/archive/superpowers/specs/2026-07-06-screenshot-tool-design.md\",\"docs/archive/superpowers/specs/2026-07-07-normalizer-refactor-design.md\",\"docs/archive/superpowers/specs/2026-07-07-result-transformer-design.md\",\"docs/archive/superpowers/specs/2026-07-07-rpcinfo-tool-design.md\",\"docs/archive/superpowers/specs/2026-07-07-validation-fixes-design.md\",\"docs/archive/superpowers/specs/2026-07-08-engagement-query-tool-design.md\",\"docs/archive/superpowers/specs/2026-07-08-subagent-delegation-design.md\",\"docs/archive/superpowers/specs/2026-07-09-ssh-exec-tool-design.md\",\"docs/archive/superpowers/specs/2026-07-10-mongo-query-design.md\",\"docs/archive/superpowers/specs/2026-07-10-service-coverage-design.md\",\"docs/archive/superpowers/specs/2026-07-13-credential-fragment-model-design.md\",\"docs/archive/superpowers/specs/2026-07-14-remediation-design.md\",\"docs/archive/superpowers/specs/2026-07-15-dns-aware-scope-enforcement-design.md\",\"docs/archive/superpowers/specs/2026-07-16-engagement-management-design.md\",\"docs/archive/superpowers/specs/2026-07-20-asset-registry-design.md\",\"docs/archive/superpowers/specs/2026-07-21-dry-simplification-design.md\",\"docs/archive/superpowers/specs/2026-07-21-register-refactor-design.md\",\"docs/research/2026-07-02-offensive-security-research.md\",\"docs/research/2026-07-04-hermes-configuration-deep-dive.md\",\"docs/research/2026-07-10-c2-integration-strategy.md\",\"docs/rfe/2026-07-11-zap-proxy-web-scanning.md\",\"docs/rfe/2026-07-25-zap-automation-framework.md\",\"docs/rfe/2026-07-26-test-target-infrastructure.md\",\"docs/superpowers/archive/2026-07-22-zap-deep-integration-design.md\",\"docs/superpowers/archive/2026-07-22-zap-deep-integration.md\",\"docs/superpowers/archive/2026-07-22-zap-integration-gaps-design.md\",\"docs/superpowers/archive/2026-07-22-zap-integration-gaps.md\",\"docs/superpowers/archive/2026-07-23-e2e-research-findings.md\",\"docs/superpowers/archive/2026-07-23-e2e-test-integrity-design.md\",\"docs/superpowers/archive/2026-07-23-e2e-test-integrity.md\",\"docs/superpowers/plans/2026-07-26-zap-af-pipeline-integration.md\",\"docs/superpowers/plans/2026-07-27-phase-aware-autonomy.md\",\"docs/superpowers/plans/archive/2026-07-29-gateway-network-isolation.md\",\"docs/superpowers/specs/2026-07-26-zap-af-pipeline-integration-design.md\",\"docs/superpowers/specs/2026-07-27-phase-aware-autonomy-design.md\",\"docs/superpowers/specs/archive/2026-07-29-gateway-network-isolation-design.md\",\"profile/README.md\",\"profile/config.yaml\",\"profile/distribution.yaml\",\"profile/skins/argicida.yaml\",\"pyproject.toml\",\"src/argicida/argicida/py.typed\",\"src/argicida/pyproject.toml\",\"src/argicida_agent/argicida_agent/py.typed\",\"src/argicida_agent/pyproject.toml\",\"src/argicida_common/argicida_common/py.typed\",\"src/argicida_common/pyproject.toml\",\"tests/__init__.py\",\"tests/conftest.py\",\"tests/factories.py\",\"tests/argicida/briefing/__init__.py\",\"tests/argicida/briefing/test_generator.py\",\"tests/argicida/briefing/test_prompts.py\",\"tests/argicida/commands/__init__.py\",\"tests/argicida/commands/test_cli.py\",\"tests/argicida/commands/test_formatting.py\",\"tests/argicida/commands/test_slash.py\",\"tests/argicida/commands/test_update_data.py\",\"tests/argicida/conftest.py\",\"tests/argicida/db/test_asset_repo.py\",\"tests/argicida/db/test_asset_tech.py\",\"tests/argicida/db/test_candidate_engine.py\",\"tests/argicida/db/test_credential_helpers.py\",\"tests/argicida/db/test_credentials.py\",\"tests/argicida/db/test_encryption.py\",\"tests/argicida/db/test_finding_repo.py\",\"tests/argicida/db/test_helpers.py\",\"tests/argicida/db/test_migrations.py\",\"tests/argicida/db/test_store.py\",\"tests/argicida/db/test_store_async.py\",\"tests/argicida/engagement/__init__.py\",\"tests/argicida/engagement/test_lifecycle.py\",\"tests/argicida/engagement/test_session.py\",\"tests/argicida/engagement/test_unbind.py\",\"tests/argicida/evidence/test_chain.py\",\"tests/argicida/evidence/test_chain_clear.py\",\"tests/argicida/evidence/test_keys.py\",\"tests/argicida/evidence/test_signer.py\",\"tests/argicida/hooks/conftest.py\",\"tests/argicida/hooks/test_adapters.py\",\"tests/argicida/hooks/test_asset_context.py\",\"tests/argicida/hooks/test_asset_coverage.py\",\"tests/argicida/hooks/test_asset_extractor.py\",\"tests/argicida/hooks/test_autonomy.py\",\"tests/argicida/hooks/test_context.py\",\"tests/argicida/hooks/test_context_tech.py\",\"tests/argicida/hooks/test_coverage_deep_testing.py\",\"tests/argicida/hooks/test_evidence.py\",\"tests/argicida/hooks/test_finalize_zap.py\",\"tests/argicida/hooks/test_normalizer.py\",\"tests/argicida/hooks/test_normalizer_errors.py\",\"tests/argicida/hooks/test_normalizer_perceptor.py\",\"tests/argicida/hooks/test_output_store.py\",\"tests/argicida/hooks/test_parse_result.py\",\"tests/argicida/hooks/test_parsers.py\",\"tests/argicida/hooks/test_password_decoding.py\",\"tests/argicida/hooks/test_perceptor.py\",\"tests/argicida/hooks/test_result_transformers.py\",\"tests/argicida/hooks/test_scope_expansion.py\",\"tests/argicida/hooks/test_scope_guard.py\",\"tests/argicida/hooks/test_service_registry.py\",\"tests/argicida/hooks/test_session_lifecycle.py\",\"tests/argicida/hooks/test_session_start.py\",\"tests/argicida/ipc/test_client.py\",\"tests/argicida/pod/test_builder.py\",\"tests/argicida/pod/test_config.py\",\"tests/argicida/pod/test_launcher.py\",\"tests/argicida/pod/test_launcher_zap.py\",\"tests/argicida/pod/test_manager.py\",\"tests/argicida/pod/test_session.py\",\"tests/argicida/pod/test_session_host_entries.py\",\"tests/argicida/pod/test_vpn_endpoint.py\",\"tests/argicida/registration/__init__.py\",\"tests/argicida/registration/test_context.py\",\"tests/argicida/registration/test_state.py\",\"tests/argicida/scope/conftest.py\",\"tests/argicida/scope/test_db_query_scope.py\",\"tests/argicida/scope/test_hostname_resolution.py\",\"tests/argicida/scope/test_netfilter.py\",\"tests/argicida/scope/test_normalize_target.py\",\"tests/argicida/scope/test_redis_cli_profile.py\",\"tests/argicida/scope/test_rpcinfo_profile.py\",\"tests/argicida/scope/test_validator.py\",\"tests/argicida/test_asset_consistency.py\",\"tests/argicida/test_consistency.py\",\"tests/argicida/test_phase_migration.py\",\"tests/argicida/test_register.py\",\"tests/argicida/test_session_consistency.py\",\"tests/argicida/tools/conftest.py\",\"tests/argicida/tools/defs/__init__.py\",\"tests/argicida/tools/defs/test_all_defs.py\",\"tests/argicida/tools/defs/test_discovery.py\",\"tests/argicida/tools/defs/test_mongo_query_def.py\",\"tests/argicida/tools/defs/test_naabu_def.py\",\"tests/argicida/tools/defs/test_protocol.py\",\"tests/argicida/tools/defs/test_ssh_exec_def.py\",\"tests/argicida/tools/test_add_host_asset.py\",\"tests/argicida/tools/test_add_host_dnsmasq.py\",\"tests/argicida/tools/test_artifact_handlers.py\",\"tests/argicida/tools/test_asset_query_handlers.py\",\"tests/argicida/tools/test_base.py\",\"tests/argicida/tools/test_curl_handler.py\",\"tests/argicida/tools/test_curl_normalizer.py\",\"tests/argicida/tools/test_db_query_handler.py\",\"tests/argicida/tools/test_db_query_normalizer.py\",\"tests/argicida/tools/test_engagement_query_fragments.py\",\"tests/argicida/tools/test_extractor_base.py\",\"tests/argicida/tools/test_extractors.py\",\"tests/argicida/tools/test_ffuf_normalizer.py\",\"tests/argicida/tools/test_handlers.py\",\"tests/argicida/tools/test_katana_normalizer.py\",\"tests/argicida/tools/test_management_handlers.py\",\"tests/argicida/tools/test_management_handlers_zap.py\",\"tests/argicida/tools/test_management_helpers.py\",\"tests/argicida/tools/test_normalizer_components.py\",\"tests/argicida/tools/test_normalizer_has_flag.py\",\"tests/argicida/tools/test_normalizer_shared_classes.py\",\"tests/argicida/tools/test_normalizers.py\",\"tests/argicida/tools/test_normalizers_mongo.py\",\"tests/argicida/tools/test_normalizers_nuclei.py\",\"tests/argicida/tools/test_normalizers_smb.py\",\"tests/argicida/tools/test_normalizers_ssh.py\",\"tests/argicida/tools/test_normalizers_zap.py\",\"tests/argicida/tools/test_parsers_make_finding.py\",\"tests/argicida/tools/test_parsers_mongo.py\",\"tests/argicida/tools/test_parsers_zap.py\",\"tests/argicida/tools/test_redis_query_normalizer.py\",\"tests/argicida/tools/test_schemas.py\",\"tests/argicida/tools/test_screenshot_handler.py\",\"tests/argicida/tools/test_screenshot_vision.py\",\"tests/argicida/tools/test_shared_utils.py\",\"tests/argicida/tools/test_tool_def_extractor.py\",\"tests/argicida/tools/test_transformer_infrastructure.py\",\"tests/argicida/tools/test_transformers_dnsx_proposal.py\",\"tests/argicida/tools/test_transformers_mongo.py\",\"tests/argicida/tools/test_transformers_ssh.py\",\"tests/argicida/tools/test_transformers_zap.py\",\"tests/argicida/tools/test_zap_new_tools.py\",\"tests/argicida/triage/test_asset_discovery.py\",\"tests/argicida/triage/test_classifier.py\",\"tests/argicida/triage/test_data.py\",\"tests/argicida/triage/test_dedup.py\",\"tests/argicida/triage/test_scorer.py\",\"tests/argicida/zap/__init__.py\",\"tests/argicida/zap/test_client.py\",\"tests/argicida/zap/test_client_af.py\",\"tests/argicida/zap/test_client_intel.py\",\"tests/argicida/zap/test_credential_tester.py\",\"tests/argicida/zap/test_manager.py\",\"tests/argicida/zap/test_manager_af.py\",\"tests/argicida/zap/test_plan_executor.py\",\"tests/argicida/zap/test_plan_generator.py\",\"tests/argicida/zap/test_scan_policy.py\",\"tests/argicida/zap/test_scan_policy_passive.py\",\"tests/argicida_agent/executor/test_runner.py\",\"tests/argicida_agent/ipc/test_server.py\",\"tests/argicida_agent/test_main.py\",\"tests/argicida_common/test_asset_schema.py\",\"tests/argicida_common/test_constants.py\",\"tests/argicida_common/test_credential_schema.py\",\"tests/argicida_common/test_engagement_schema.py\",\"tests/argicida_common/test_finding_schema.py\",\"tests/argicida_common/test_ipc_protocol.py\",\"tests/argicida_common/test_ipc_schema.py\",\"tests/argicida_common/test_scope_schema.py\",\"tests/e2e/__init__.py\",\"tests/e2e/assertions.py\",\"tests/e2e/conftest.py\",\"tests/e2e/fault_injector.py\",\"tests/e2e/harness.py\",\"tests/e2e/test_e2e_smoke.py\",\"tests/integration/__init__.py\",\"tests/integration/conftest.py\",\"tests/integration/test_asset_pipeline.py\",\"tests/integration/test_hook_pipeline.py\",\"tests/integration/test_phase_lifecycle.py\",\"tests/integration/test_registration.py\",\"tests/integration/test_session_lifecycle.py\",\"tests/integration/test_slash_commands.py\",\"tests/integration/test_tool_dispatch.py\",\"tests/pod_integration/__init__.py\",\"tests/pod_integration/conftest.py\",\"tests/pod_integration/test_artifact_transfer.py\",\"tests/pod_integration/test_asset_registry.py\",\"tests/pod_integration/test_container_build.py\",\"tests/pod_integration/test_diagnostic_pod.py\",\"tests/pod_integration/test_engagement_management.py\",\"tests/pod_integration/test_fault_recovery.py\",\"tests/pod_integration/test_fault_stale_session.py\",\"tests/pod_integration/test_first_light.py\",\"tests/pod_integration/test_gateway_fail_closed.py\",\"tests/pod_integration/test_gateway_isolation.py\",\"tests/pod_integration/test_ipc_file_write.py\",\"tests/pod_integration/test_zap.py\",\"tests/pod_integration/test_zap_af.py\",\"tests/scripts/__init__.py\",\"tests/scripts/test_sync_skills.py\"],\"Other \u2014 CLAUDE.md\":[\"CLAUDE.md\"],\"Other \u2014 README.md\":[\"README.md\"],\"Other \u2014 docs\":[\"docs/DEVELOPMENT-STANDARDS.md\",\"docs/RESEARCH.md\",\"docs/triage-engine.md\"],\"Other \u2014 docs-archive\":[\"docs/archive/AUDIT-2026-07-14.md\",\"docs/archive/NOTES.md\",\"docs/archive/REFACTOR-PLAN-v2.md\",\"docs/archive/REFACTOR-PLAN.md\",\"docs/archive/REFACTOR-REFERENCES.md\",\"docs/archive/REMEDIATION-PLAN-2026-07-14.md\",\"docs/archive/audit-confirmed-findings.md\",\"docs/archive/e2e-gap-analysis.md\",\"docs/archive/full-system-audit.md\",\"docs/archive/htb-api-reference.md\",\"docs/archive/ipc-sync-async-boundary.md\",\"docs/archive/pipeline-engine.md\",\"docs/archive/plans/2026-06-30-foundation.md\",\"docs/archive/plans/2026-06-30-ipc-control-plane.md\",\"docs/archive/plans/2026-06-30-pipeline-engine.md\",\"docs/archive/plans/2026-06-30-scope-enforcement.md\",\"docs/archive/plans/2026-06-30-tool-wrappers.md\",\"docs/archive/plans/2026-06-30-triage-and-hooks.md\",\"docs/archive/plans/2026-07-01-briefing-and-trace.md\",\"docs/archive/plans/2026-07-01-container-build.md\",\"docs/archive/plans/2026-07-01-dynamic-pipeline.md\",\"docs/archive/plans/2026-07-01-e2e-gap-fixes.md\",\"docs/archive/plans/2026-07-01-hermes-plugin-shell.md\",\"docs/archive/plans/2026-07-01-integration-test-harness.md\",\"docs/archive/plans/2026-07-01-iterative-reentry.md\",\"docs/archive/plans/2026-07-01-pod-and-cli-commands.md\",\"docs/archive/plans/2026-07-01-session-lifecycle.md\",\"docs/archive/plans/2026-07-02-llm-driven-redesign.md\",\"docs/archive/plans/2026-07-02-phase3-infrastructure-hardening.md\",\"docs/archive/plans/2026-07-02-structured-perceptor.md\",\"docs/archive/plans/2026-07-15-screenshot-vision-analysis.md\",\"docs/archive/rfe/2026-07-07-engagement-query-tool.md\",\"docs/archive/rfe/2026-07-07-screenshot-vision-analysis.md\",\"docs/archive/rfe/2026-07-07-subagent-delegation.md\",\"docs/archive/rfe/2026-07-10-credential-fragment-model.md\",\"docs/archive/rfe/2026-07-11-dns-aware-scope-enforcement.md\",\"docs/archive/rfe/2026-07-13-infrastructure-vs-prompt-boundary.md\",\"docs/archive/rfe/2026-07-15-engagement-management.md\",\"docs/archive/spec-gaps.md\",\"docs/archive/specs/2026-06-29-project-structure-design.md\",\"docs/archive/specs/2026-07-01-phase-2a-hermes-integration.md\",\"docs/archive/specs/2026-07-02-hermes-convention-refactor.md\",\"docs/archive/specs/2026-07-02-llm-driven-redesign.md\",\"docs/archive/specs/2026-07-02-phase3-infrastructure-hardening.md\",\"docs/archive/specs/2026-07-02-structured-perceptor.md\",\"docs/archive/specs/2026-07-08-engagement-lifecycle.md\",\"docs/archive/specs/2026-07-09-tool-definition-registry.md\",\"docs/archive/specs/2026-07-15-screenshot-vision-analysis.md\",\"docs/archive/superpowers/plans/2026-07-01-phase1-first-light.md\",\"docs/archive/superpowers/plans/2026-07-01-scope-enforcement.md\",\"docs/archive/superpowers/plans/2026-07-02-hermes-convention-refactor.md\",\"docs/archive/superpowers/plans/2026-07-03-nse-script-fuzzy-matching.md\",\"docs/archive/superpowers/plans/2026-07-04-artifact-storage.md\",\"docs/archive/superpowers/plans/2026-07-04-db-query-finding-parser.md\",\"docs/archive/superpowers/plans/2026-07-04-db-query-tool.md\",\"docs/archive/superpowers/plans/2026-07-04-redis-query-tool.md\",\"docs/archive/superpowers/plans/2026-07-06-curl-tool.md\",\"docs/archive/superpowers/plans/2026-07-06-db-enumeration-skill-split.md\",\"docs/archive/superpowers/plans/2026-07-06-katana-ffuf-tools.md\",\"docs/archive/superpowers/plans/2026-07-06-screenshot-tool.md\",\"docs/archive/superpowers/plans/2026-07-07-normalizer-refactor.md\",\"docs/archive/superpowers/plans/2026-07-07-result-transformer.md\",\"docs/archive/superpowers/plans/2026-07-07-rpcinfo-tool.md\",\"docs/archive/superpowers/plans/2026-07-07-validation-fixes.md\",\"docs/archive/superpowers/plans/2026-07-08-engagement-lifecycle.md\",\"docs/archive/superpowers/plans/2026-07-08-engagement-query-tool.md\",\"docs/archive/superpowers/plans/2026-07-09-ssh-exec-tool.md\",\"docs/archive/superpowers/plans/2026-07-09-tool-definition-registry.md\",\"docs/archive/superpowers/plans/2026-07-10-mongo-query.md\",\"docs/archive/superpowers/plans/2026-07-10-service-coverage.md\",\"docs/archive/superpowers/plans/2026-07-13-credential-fragment-model.md\",\"docs/archive/superpowers/plans/2026-07-14-structural-ownership.md\",\"docs/archive/superpowers/plans/2026-07-15-dns-aware-scope-enforcement.md\",\"docs/archive/superpowers/plans/2026-07-16-engagement-management.md\",\"docs/archive/superpowers/plans/2026-07-20-asset-registry.md\",\"docs/archive/superpowers/plans/2026-07-21-dry-simplification.md\",\"docs/archive/superpowers/plans/2026-07-21-register-refactor.md\",\"docs/archive/superpowers/specs/2026-07-01-scope-enforcement-design.md\",\"docs/archive/superpowers/specs/2026-07-03-nse-script-fuzzy-matching-design.md\",\"docs/archive/superpowers/specs/2026-07-04-artifact-storage-design.md\",\"docs/archive/superpowers/specs/2026-07-04-db-query-finding-parser-design.md\",\"docs/archive/superpowers/specs/2026-07-04-redis-query-tool-design.md\",\"docs/archive/superpowers/specs/2026-07-06-curl-tool-design.md\",\"docs/archive/superpowers/specs/2026-07-06-db-enumeration-skill-split-design.md\",\"docs/archive/superpowers/specs/2026-07-06-katana-ffuf-tools-design.md\",\"docs/archive/superpowers/specs/2026-07-06-screenshot-tool-design.md\",\"docs/archive/superpowers/specs/2026-07-07-normalizer-refactor-design.md\",\"docs/archive/superpowers/specs/2026-07-07-result-transformer-design.md\",\"docs/archive/superpowers/specs/2026-07-07-rpcinfo-tool-design.md\",\"docs/archive/superpowers/specs/2026-07-07-validation-fixes-design.md\",\"docs/archive/superpowers/specs/2026-07-08-engagement-query-tool-design.md\",\"docs/archive/superpowers/specs/2026-07-08-subagent-delegation-design.md\",\"docs/archive/superpowers/specs/2026-07-09-ssh-exec-tool-design.md\",\"docs/archive/superpowers/specs/2026-07-10-mongo-query-design.md\",\"docs/archive/superpowers/specs/2026-07-10-service-coverage-design.md\",\"docs/archive/superpowers/specs/2026-07-13-credential-fragment-model-design.md\",\"docs/archive/superpowers/specs/2026-07-14-remediation-design.md\",\"docs/archive/superpowers/specs/2026-07-15-dns-aware-scope-enforcement-design.md\",\"docs/archive/superpowers/specs/2026-07-16-engagement-management-design.md\",\"docs/archive/superpowers/specs/2026-07-20-asset-registry-design.md\",\"docs/archive/superpowers/specs/2026-07-21-dry-simplification-design.md\",\"docs/archive/superpowers/specs/2026-07-21-register-refactor-design.md\"],\"Other \u2014 docs-research\":[\"docs/research/2026-07-02-offensive-security-research.md\",\"docs/research/2026-07-04-hermes-configuration-deep-dive.md\",\"docs/research/2026-07-10-c2-integration-strategy.md\"],\"Other \u2014 docs-rfe\":[\"docs/rfe/2026-07-11-zap-proxy-web-scanning.md\",\"docs/rfe/2026-07-25-zap-automation-framework.md\",\"docs/rfe/2026-07-26-test-target-infrastructure.md\"],\"Other \u2014 docs-superpowers\":[\"docs/superpowers/archive/2026-07-22-zap-deep-integration-design.md\",\"docs/superpowers/archive/2026-07-22-zap-deep-integration.md\",\"docs/superpowers/archive/2026-07-22-zap-integration-gaps-design.md\",\"docs/superpowers/archive/2026-07-22-zap-integration-gaps.md\",\"docs/superpowers/archive/2026-07-23-e2e-research-findings.md\",\"docs/superpowers/archive/2026-07-23-e2e-test-integrity-design.md\",\"docs/superpowers/archive/2026-07-23-e2e-test-integrity.md\",\"docs/superpowers/plans/2026-07-26-zap-af-pipeline-integration.md\",\"docs/superpowers/plans/2026-07-27-phase-aware-autonomy.md\",\"docs/superpowers/plans/archive/2026-07-29-gateway-network-isolation.md\",\"docs/superpowers/specs/2026-07-26-zap-af-pipeline-integration-design.md\",\"docs/superpowers/specs/2026-07-27-phase-aware-autonomy-design.md\",\"docs/superpowers/specs/archive/2026-07-29-gateway-network-isolation-design.md\"],\"Other \u2014 profile\":[\"profile/README.md\",\"profile/config.yaml\",\"profile/distribution.yaml\"],\"Other \u2014 profile-skins\":[\"profile/skins/argicida.yaml\"],\"Other \u2014 pyproject.toml\":[\"pyproject.toml\"],\"Other \u2014 src-argicida\":[\"src/argicida/argicida/py.typed\",\"src/argicida/pyproject.toml\"],\"Other \u2014 src-argicida_agent\":[\"src/argicida_agent/argicida_agent/py.typed\",\"src/argicida_agent/pyproject.toml\"],\"Other \u2014 src-argicida_common\":[\"src/argicida_common/argicida_common/py.typed\",\"src/argicida_common/pyproject.toml\"],\"Other \u2014 tests\":[\"tests/__init__.py\",\"tests/conftest.py\",\"tests/factories.py\"],\"Other \u2014 tests-argicida\":[\"tests/argicida/briefing/__init__.py\",\"tests/argicida/briefing/test_generator.py\",\"tests/argicida/briefing/test_prompts.py\",\"tests/argicida/commands/__init__.py\",\"tests/argicida/commands/test_cli.py\",\"tests/argicida/commands/test_formatting.py\",\"tests/argicida/commands/test_slash.py\",\"tests/argicida/commands/test_update_data.py\",\"tests/argicida/conftest.py\",\"tests/argicida/db/test_asset_repo.py\",\"tests/argicida/db/test_asset_tech.py\",\"tests/argicida/db/test_candidate_engine.py\",\"tests/argicida/db/test_credential_helpers.py\",\"tests/argicida/db/test_credentials.py\",\"tests/argicida/db/test_encryption.py\",\"tests/argicida/db/test_finding_repo.py\",\"tests/argicida/db/test_helpers.py\",\"tests/argicida/db/test_migrations.py\",\"tests/argicida/db/test_store.py\",\"tests/argicida/db/test_store_async.py\",\"tests/argicida/engagement/__init__.py\",\"tests/argicida/engagement/test_lifecycle.py\",\"tests/argicida/engagement/test_session.py\",\"tests/argicida/engagement/test_unbind.py\",\"tests/argicida/evidence/test_chain.py\",\"tests/argicida/evidence/test_chain_clear.py\",\"tests/argicida/evidence/test_keys.py\",\"tests/argicida/evidence/test_signer.py\",\"tests/argicida/hooks/conftest.py\",\"tests/argicida/hooks/test_adapters.py\",\"tests/argicida/hooks/test_asset_context.py\",\"tests/argicida/hooks/test_asset_coverage.py\",\"tests/argicida/hooks/test_asset_extractor.py\",\"tests/argicida/hooks/test_autonomy.py\",\"tests/argicida/hooks/test_context.py\",\"tests/argicida/hooks/test_context_tech.py\",\"tests/argicida/hooks/test_coverage_deep_testing.py\",\"tests/argicida/hooks/test_evidence.py\",\"tests/argicida/hooks/test_finalize_zap.py\",\"tests/argicida/hooks/test_normalizer.py\",\"tests/argicida/hooks/test_normalizer_errors.py\",\"tests/argicida/hooks/test_normalizer_perceptor.py\",\"tests/argicida/hooks/test_output_store.py\",\"tests/argicida/hooks/test_parse_result.py\",\"tests/argicida/hooks/test_parsers.py\",\"tests/argicida/hooks/test_password_decoding.py\",\"tests/argicida/hooks/test_perceptor.py\",\"tests/argicida/hooks/test_result_transformers.py\",\"tests/argicida/hooks/test_scope_expansion.py\",\"tests/argicida/hooks/test_scope_guard.py\",\"tests/argicida/hooks/test_service_registry.py\",\"tests/argicida/hooks/test_session_lifecycle.py\",\"tests/argicida/hooks/test_session_start.py\",\"tests/argicida/ipc/test_client.py\",\"tests/argicida/pod/test_builder.py\",\"tests/argicida/pod/test_config.py\",\"tests/argicida/pod/test_launcher.py\",\"tests/argicida/pod/test_launcher_zap.py\",\"tests/argicida/pod/test_manager.py\",\"tests/argicida/pod/test_session.py\",\"tests/argicida/pod/test_session_host_entries.py\",\"tests/argicida/pod/test_vpn_endpoint.py\",\"tests/argicida/registration/__init__.py\",\"tests/argicida/registration/test_context.py\",\"tests/argicida/registration/test_state.py\",\"tests/argicida/scope/conftest.py\",\"tests/argicida/scope/test_db_query_scope.py\",\"tests/argicida/scope/test_hostname_resolution.py\",\"tests/argicida/scope/test_netfilter.py\",\"tests/argicida/scope/test_normalize_target.py\",\"tests/argicida/scope/test_redis_cli_profile.py\",\"tests/argicida/scope/test_rpcinfo_profile.py\",\"tests/argicida/scope/test_validator.py\",\"tests/argicida/test_asset_consistency.py\",\"tests/argicida/test_consistency.py\",\"tests/argicida/test_phase_migration.py\",\"tests/argicida/test_register.py\",\"tests/argicida/test_session_consistency.py\",\"tests/argicida/tools/conftest.py\",\"tests/argicida/tools/defs/__init__.py\",\"tests/argicida/tools/defs/test_all_defs.py\",\"tests/argicida/tools/defs/test_discovery.py\",\"tests/argicida/tools/defs/test_mongo_query_def.py\",\"tests/argicida/tools/defs/test_naabu_def.py\",\"tests/argicida/tools/defs/test_protocol.py\",\"tests/argicida/tools/defs/test_ssh_exec_def.py\",\"tests/argicida/tools/test_add_host_asset.py\",\"tests/argicida/tools/test_add_host_dnsmasq.py\",\"tests/argicida/tools/test_artifact_handlers.py\",\"tests/argicida/tools/test_asset_query_handlers.py\",\"tests/argicida/tools/test_base.py\",\"tests/argicida/tools/test_curl_handler.py\",\"tests/argicida/tools/test_curl_normalizer.py\",\"tests/argicida/tools/test_db_query_handler.py\",\"tests/argicida/tools/test_db_query_normalizer.py\",\"tests/argicida/tools/test_engagement_query_fragments.py\",\"tests/argicida/tools/test_extractor_base.py\",\"tests/argicida/tools/test_extractors.py\",\"tests/argicida/tools/test_ffuf_normalizer.py\",\"tests/argicida/tools/test_handlers.py\",\"tests/argicida/tools/test_katana_normalizer.py\",\"tests/argicida/tools/test_management_handlers.py\",\"tests/argicida/tools/test_management_handlers_zap.py\",\"tests/argicida/tools/test_management_helpers.py\",\"tests/argicida/tools/test_normalizer_components.py\",\"tests/argicida/tools/test_normalizer_has_flag.py\",\"tests/argicida/tools/test_normalizer_shared_classes.py\",\"tests/argicida/tools/test_normalizers.py\",\"tests/argicida/tools/test_normalizers_mongo.py\",\"tests/argicida/tools/test_normalizers_nuclei.py\",\"tests/argicida/tools/test_normalizers_smb.py\",\"tests/argicida/tools/test_normalizers_ssh.py\",\"tests/argicida/tools/test_normalizers_zap.py\",\"tests/argicida/tools/test_parsers_make_finding.py\",\"tests/argicida/tools/test_parsers_mongo.py\",\"tests/argicida/tools/test_parsers_zap.py\",\"tests/argicida/tools/test_redis_query_normalizer.py\",\"tests/argicida/tools/test_schemas.py\",\"tests/argicida/tools/test_screenshot_handler.py\",\"tests/argicida/tools/test_screenshot_vision.py\",\"tests/argicida/tools/test_shared_utils.py\",\"tests/argicida/tools/test_tool_def_extractor.py\",\"tests/argicida/tools/test_transformer_infrastructure.py\",\"tests/argicida/tools/test_transformers_dnsx_proposal.py\",\"tests/argicida/tools/test_transformers_mongo.py\",\"tests/argicida/tools/test_transformers_ssh.py\",\"tests/argicida/tools/test_transformers_zap.py\",\"tests/argicida/tools/test_zap_new_tools.py\",\"tests/argicida/triage/test_asset_discovery.py\",\"tests/argicida/triage/test_classifier.py\",\"tests/argicida/triage/test_data.py\",\"tests/argicida/triage/test_dedup.py\",\"tests/argicida/triage/test_scorer.py\",\"tests/argicida/zap/__init__.py\",\"tests/argicida/zap/test_client.py\",\"tests/argicida/zap/test_client_af.py\",\"tests/argicida/zap/test_client_intel.py\",\"tests/argicida/zap/test_credential_tester.py\",\"tests/argicida/zap/test_manager.py\",\"tests/argicida/zap/test_manager_af.py\",\"tests/argicida/zap/test_plan_executor.py\",\"tests/argicida/zap/test_plan_generator.py\",\"tests/argicida/zap/test_scan_policy.py\",\"tests/argicida/zap/test_scan_policy_passive.py\"],\"Other \u2014 tests-argicida_agent\":[\"tests/argicida_agent/executor/test_runner.py\",\"tests/argicida_agent/ipc/test_server.py\",\"tests/argicida_agent/test_main.py\"],\"Other \u2014 tests-argicida_common\":[\"tests/argicida_common/test_asset_schema.py\",\"tests/argicida_common/test_constants.py\",\"tests/argicida_common/test_credential_schema.py\",\"tests/argicida_common/test_engagement_schema.py\",\"tests/argicida_common/test_finding_schema.py\",\"tests/argicida_common/test_ipc_protocol.py\",\"tests/argicida_common/test_ipc_schema.py\",\"tests/argicida_common/test_scope_schema.py\"],\"Other \u2014 tests-e2e\":[\"tests/e2e/__init__.py\",\"tests/e2e/assertions.py\",\"tests/e2e/conftest.py\",\"tests/e2e/fault_injector.py\",\"tests/e2e/harness.py\",\"tests/e2e/test_e2e_smoke.py\"],\"Other \u2014 tests-integration\":[\"tests/integration/__init__.py\",\"tests/integration/conftest.py\",\"tests/integration/test_asset_pipeline.py\",\"tests/integration/test_hook_pipeline.py\",\"tests/integration/test_phase_lifecycle.py\",\"tests/integration/test_registration.py\",\"tests/integration/test_session_lifecycle.py\",\"tests/integration/test_slash_commands.py\",\"tests/integration/test_tool_dispatch.py\"],\"Other \u2014 tests-pod_integration\":[\"tests/pod_integration/__init__.py\",\"tests/pod_integration/conftest.py\",\"tests/pod_integration/test_artifact_transfer.py\",\"tests/pod_integration/test_asset_registry.py\",\"tests/pod_integration/test_container_build.py\",\"tests/pod_integration/test_diagnostic_pod.py\",\"tests/pod_integration/test_engagement_management.py\",\"tests/pod_integration/test_fault_recovery.py\",\"tests/pod_integration/test_fault_stale_session.py\",\"tests/pod_integration/test_first_light.py\",\"tests/pod_integration/test_gateway_fail_closed.py\",\"tests/pod_integration/test_gateway_isolation.py\",\"tests/pod_integration/test_ipc_file_write.py\",\"tests/pod_integration/test_zap.py\",\"tests/pod_integration/test_zap_af.py\"],\"Other \u2014 tests-scripts\":[\"tests/scripts/__init__.py\",\"tests/scripts/test_sync_skills.py\"]},\"moduleTree\":[{\"name\":\"Plugin Registration &amp; Core\",\"slug\":\"plugin-registration-core\",\"files\":[\"src/argicida/argicida/__init__.py\",\"src/argicida/argicida/registration/__init__.py\",\"src/argicida/argicida/registration/context.py\",\"src/argicida/argicida/registration/state.py\",\"src/argicida/argicida/registration/hooks.py\",\"src/argicida/argicida/registration/scan_tools.py\",\"src/argicida/argicida/registration/management_tools.py\",\"src/argicida/argicida/registration/commands.py\",\"src/argicida/argicida/utils.py\",\"src/argicida/argicida/plugin.yaml\"]},{\"name\":\"Engagement &amp; Database\",\"slug\":\"engagement-database\",\"files\":[\"src/argicida/argicida/engagement/__init__.py\",\"src/argicida/argicida/engagement/lifecycle.py\",\"src/argicida/argicida/engagement/session.py\",\"src/argicida/argicida/db/__init__.py\",\"src/argicida/argicida/db/store.py\",\"src/argicida/argicida/db/asset_repo.py\",\"src/argicida/argicida/db/credential_repo.py\",\"src/argicida/argicida/db/finding_repo.py\",\"src/argicida/argicida/db/evidence_repo.py\",\"src/argicida/argicida/db/vpn_repo.py\",\"src/argicida/argicida/db/helpers.py\",\"src/argicida/argicida/db/migrations.py\",\"src/argicida/argicida/db/encryption.py\"]},{\"name\":\"Pod Management\",\"slug\":\"pod-management\",\"files\":[\"src/argicida/argicida/pod/__init__.py\",\"src/argicida/argicida/pod/launcher.py\",\"src/argicida/argicida/pod/manager.py\",\"src/argicida/argicida/pod/session.py\",\"src/argicida/argicida/pod/config.py\",\"src/argicida/argicida/pod/builder.py\",\"src/argicida/argicida/pod/vpn.py\"]},{\"name\":\"Container Agent\",\"slug\":\"container-agent\",\"files\":[\"src/argicida_agent/argicida_agent/__init__.py\",\"src/argicida_agent/argicida_agent/__main__.py\",\"src/argicida_agent/argicida_agent/executor/__init__.py\",\"src/argicida_agent/argicida_agent/executor/runner.py\",\"src/argicida_agent/argicida_agent/ipc/__init__.py\",\"src/argicida_agent/argicida_agent/ipc/server.py\"]},{\"name\":\"IPC Layer\",\"slug\":\"ipc-layer\",\"files\":[\"src/argicida/argicida/ipc/__init__.py\",\"src/argicida/argicida/ipc/client.py\",\"src/argicida_common/argicida_common/ipc_protocol.py\",\"src/argicida_common/argicida_common/ipc_schema.py\"]},{\"name\":\"Scope Enforcement\",\"slug\":\"scope-enforcement\",\"files\":[\"src/argicida/argicida/scope/__init__.py\",\"src/argicida/argicida/scope/netfilter.py\",\"src/argicida/argicida/scope/validator.py\",\"src/argicida_common/argicida_common/scope_schema.py\"]},{\"name\":\"Scan Tool Framework\",\"slug\":\"scan-tool-framework\",\"files\":[\"src/argicida/argicida/tools/__init__.py\",\"src/argicida/argicida/tools/base.py\",\"src/argicida/argicida/tools/schemas.py\",\"src/argicida/argicida/tools/defs/__init__.py\",\"src/argicida/argicida/tools/defs/_protocol.py\",\"src/argicida/argicida/tools/defs/_pd_common.py\",\"src/argicida/argicida/tools/defs/nmap.py\",\"src/argicida/argicida/tools/defs/naabu.py\",\"src/argicida/argicida/tools/defs/nuclei.py\",\"src/argicida/argicida/tools/defs/httpx.py\",\"src/argicida/argicida/tools/defs/subfinder.py\",\"src/argicida/argicida/tools/defs/dnsx.py\",\"src/argicida/argicida/tools/defs/katana.py\",\"src/argicida/argicida/tools/defs/ffuf.py\",\"src/argicida/argicida/tools/defs/curl.py\",\"src/argicida/argicida/tools/defs/tracepath.py\",\"src/argicida/argicida/tools/defs/rpcinfo.py\",\"src/argicida/argicida/tools/defs/smbclient.py\",\"src/argicida/argicida/tools/defs/db_query.py\",\"src/argicida/argicida/tools/defs/redis_query.py\",\"src/argicida/argicida/tools/defs/mongo_query.py\",\"src/argicida/argicida/tools/defs/ssh_exec.py\",\"src/argicida/argicida/tools/normalizers.py\",\"src/argicida/argicida/tools/normalizers_nmap.py\",\"src/argicida/argicida/tools/normalizers_nuclei.py\",\"src/argicida/argicida/tools/normalizers_curl.py\",\"src/argicida/argicida/tools/normalizers_db.py\",\"src/argicida/argicida/tools/normalizers_redis.py\",\"src/argicida/argicida/tools/normalizers_mongo.py\",\"src/argicida/argicida/tools/normalizers_katana.py\",\"src/argicida/argicida/tools/normalizers_smb.py\",\"src/argicida/argicida/tools/normalizers_ssh.py\",\"src/argicida/argicida/tools/normalizers_zap.py\",\"src/argicida/argicida/tools/parsers.py\",\"src/argicida/argicida/tools/parsers_nmap.py\",\"src/argicida/argicida/tools/parsers_naabu.py\",\"src/argicida/argicida/tools/parsers_nuclei.py\",\"src/argicida/argicida/tools/parsers_dnsx.py\",\"src/argicida/argicida/tools/parsers_db.py\",\"src/argicida/argicida/tools/parsers_redis.py\",\"src/argicida/argicida/tools/parsers_mongo.py\",\"src/argicida/argicida/tools/parsers_smbclient.py\",\"src/argicida/argicida/tools/parsers_zap.py\",\"src/argicida/argicida/tools/transformers.py\",\"src/argicida/argicida/tools/transformers_nmap.py\",\"src/argicida/argicida/tools/transformers_naabu.py\",\"src/argicida/argicida/tools/transformers_nuclei.py\",\"src/argicida/argicida/tools/transformers_httpx.py\",\"src/argicida/argicida/tools/transformers_subfinder.py\",\"src/argicida/argicida/tools/transformers_dnsx.py\",\"src/argicida/argicida/tools/transformers_katana.py\",\"src/argicida/argicida/tools/transformers_ffuf.py\",\"src/argicida/argicida/tools/transformers_curl.py\",\"src/argicida/argicida/tools/transformers_db.py\",\"src/argicida/argicida/tools/transformers_redis.py\",\"src/argicida/argicida/tools/transformers_mongo.py\",\"src/argicida/argicida/tools/transformers_smbclient.py\",\"src/argicida/argicida/tools/transformers_zap.py\",\"src/argicida/argicida/tools/extractors.py\",\"src/argicida/argicida/tools/extractors_base.py\",\"src/argicida/argicida/tools/extractors_nmap.py\",\"src/argicida/argicida/tools/extractors_naabu.py\",\"src/argicida/argicida/tools/extractors_httpx.py\",\"src/argicida/argicida/tools/extractors_subfinder.py\",\"src/argicida/argicida/tools/extractors_dnsx.py\",\"src/argicida/argicida/tools/extractors_nuclei.py\"]},{\"name\":\"Management &amp; CLI\",\"slug\":\"management-cli\",\"files\":[\"src/argicida/argicida/tools/management_handlers.py\",\"src/argicida/argicida/tools/artifact_handlers.py\",\"src/argicida/argicida/commands/__init__.py\",\"src/argicida/argicida/commands/cli.py\",\"src/argicida/argicida/commands/slash.py\",\"src/argicida/argicida/commands/formatting.py\",\"src/argicida/argicida/commands/update_data.py\"]},{\"name\":\"Hook Pipeline\",\"slug\":\"hook-pipeline\",\"files\":[\"src/argicida/argicida/hooks/__init__.py\",\"src/argicida/argicida/hooks/adapters.py\",\"src/argicida/argicida/hooks/asset_extractor.py\",\"src/argicida/argicida/hooks/autonomy.py\",\"src/argicida/argicida/hooks/context.py\",\"src/argicida/argicida/hooks/context_sections.py\",\"src/argicida/argicida/hooks/coverage.py\",\"src/argicida/argicida/hooks/evidence.py\",\"src/argicida/argicida/hooks/normalizer.py\",\"src/argicida/argicida/hooks/output_store.py\",\"src/argicida/argicida/hooks/parsers.py\",\"src/argicida/argicida/hooks/perceptor.py\",\"src/argicida/argicida/hooks/result_transformers.py\",\"src/argicida/argicida/hooks/scope_guard.py\",\"src/argicida/argicida/hooks/service_registry.py\",\"src/argicida/argicida/hooks/session_start.py\",\"src/argicida/argicida/hooks/skill_loader.py\"]},{\"name\":\"Triage Engine\",\"slug\":\"triage-engine\",\"files\":[\"src/argicida/argicida/triage/__init__.py\",\"src/argicida/argicida/triage/classifier.py\",\"src/argicida/argicida/triage/scorer.py\",\"src/argicida/argicida/triage/dedup.py\",\"src/argicida/argicida/triage/data.py\",\"src/argicida/argicida/triage/discovery.py\",\"scripts/generate_triage_diagrams.py\",\"scripts/generate_pipeline_diagrams.py\"]},{\"name\":\"ZAP Integration\",\"slug\":\"zap-integration\",\"files\":[\"src/argicida/argicida/zap/__init__.py\",\"src/argicida/argicida/zap/client.py\",\"src/argicida/argicida/zap/manager.py\",\"src/argicida/argicida/zap/plan_executor.py\",\"src/argicida/argicida/zap/plan_generator.py\",\"src/argicida/argicida/zap/scan_policy.py\",\"src/argicida/argicida/zap/credential_tester.py\"]},{\"name\":\"Evidence &amp; Reporting\",\"slug\":\"evidence-reporting\",\"files\":[\"src/argicida/argicida/evidence/__init__.py\",\"src/argicida/argicida/evidence/chain.py\",\"src/argicida/argicida/evidence/keys.py\",\"src/argicida/argicida/evidence/signer.py\",\"src/argicida/argicida/briefing/__init__.py\",\"src/argicida/argicida/briefing/generator.py\",\"src/argicida/argicida/briefing/prompts.py\"]},{\"name\":\"Shared Types &amp; Schemas\",\"slug\":\"shared-types-schemas\",\"files\":[\"src/argicida_common/argicida_common/__init__.py\",\"src/argicida_common/argicida_common/constants.py\",\"src/argicida_common/argicida_common/finding_schema.py\",\"src/argicida_common/argicida_common/engagement_schema.py\",\"src/argicida_common/argicida_common/asset_schema.py\",\"src/argicida_common/argicida_common/credential_schema.py\"]},{\"name\":\"Container Build Infrastructure\",\"slug\":\"container-build-infrastructure\",\"files\":[\"containers/Containerfile\",\"containers/Containerfile.gateway\",\"containers/Containerfile.zap\",\"containers/entrypoint.sh\",\"containers/gateway-entrypoint.sh\",\"containers/zap-entrypoint.sh\",\"containers/dnsx-config.yaml\",\"containers/katana-config.yaml\",\"containers/naabu-config.yaml\",\"containers/nuclei-config.yaml\",\"containers/screenshot.yaml\",\"containers/nse-blocklist.txt\",\"containers/ssh-key-wrap\"]},{\"name\":\"Hermes Profile &amp; Skills\",\"slug\":\"hermes-profile-skills\",\"files\":[],\"children\":[{\"name\":\"Hermes Profile &amp; Skills \u2014 profile\",\"slug\":\"hermes-profile-skills-profile\",\"files\":[\"profile/SOUL.md\"]},{\"name\":\"Hermes Profile &amp; Skills \u2014 skills\",\"slug\":\"hermes-profile-skills-skills\",\"files\":[\"profile/skills/security/DESCRIPTION.md\",\"profile/skills/security/pentest-methodology/SKILL.md\",\"profile/skills/security/network-discovery/SKILL.md\",\"profile/skills/security/dns-enumeration/SKILL.md\",\"profile/skills/security/web-enumeration/SKILL.md\",\"profile/skills/security/web-enumeration/fingerprinting/SKILL.md\",\"profile/skills/security/web-enumeration/scanning/SKILL.md\",\"profile/skills/security/web-enumeration/crawling/SKILL.md\",\"profile/skills/security/web-enumeration/fuzzing/SKILL.md\",\"profile/skills/security/web-enumeration/api/SKILL.md\",\"profile/skills/security/web-enumeration/api-testing/SKILL.md\",\"profile/skills/security/web-enumeration/curl-techniques/SKILL.md\",\"profile/skills/security/web-enumeration/services/SKILL.md\",\"profile/skills/security/db-enumeration/SKILL.md\",\"profile/skills/security/db-enumeration/postgresql/SKILL.md\",\"profile/skills/security/db-enumeration/mysql/SKILL.md\",\"profile/skills/security/db-enumeration/mssql/SKILL.md\",\"profile/skills/security/db-enumeration/oracle/SKILL.md\",\"profile/skills/security/db-enumeration/cassandra/SKILL.md\",\"profile/skills/security/mongo-enumeration/SKILL.md\",\"profile/skills/security/redis-enumeration/SKILL.md\",\"profile/skills/security/smb-enumeration/SKILL.md\",\"profile/skills/security/rpc-nfs-enumeration/SKILL.md\",\"profile/skills/security/ssh-enumeration/SKILL.md\",\"profile/skills/security/ftp-enumeration/SKILL.md\",\"profile/skills/security/smtp-enumeration/SKILL.md\",\"profile/skills/security/telnet-enumeration/SKILL.md\"]},{\"name\":\"Hermes Profile &amp; Skills \u2014 scripts\",\"slug\":\"hermes-profile-skills-scripts\",\"files\":[\"scripts/sync_skills.py\"]}]},{\"name\":\"Other\",\"slug\":\"other\",\"files\":[],\"children\":[{\"name\":\"Other \u2014 CLAUDE.md\",\"slug\":\"other-claude-md\",\"files\":[\"CLAUDE.md\"]},{\"name\":\"Other \u2014 README.md\",\"slug\":\"other-readme-md\",\"files\":[\"README.md\"]},{\"name\":\"Other \u2014 docs\",\"slug\":\"other-docs\",\"files\":[\"docs/DEVELOPMENT-STANDARDS.md\",\"docs/RESEARCH.md\",\"docs/triage-engine.md\"]},{\"name\":\"Other \u2014 docs-archive\",\"slug\":\"other-docs-archive\",\"files\":[\"docs/archive/AUDIT-2026-07-14.md\",\"docs/archive/NOTES.md\",\"docs/archive/REFACTOR-PLAN-v2.md\",\"docs/archive/REFACTOR-PLAN.md\",\"docs/archive/REFACTOR-REFERENCES.md\",\"docs/archive/REMEDIATION-PLAN-2026-07-14.md\",\"docs/archive/audit-confirmed-findings.md\",\"docs/archive/e2e-gap-analysis.md\",\"docs/archive/full-system-audit.md\",\"docs/archive/htb-api-reference.md\",\"docs/archive/ipc-sync-async-boundary.md\",\"docs/archive/pipeline-engine.md\",\"docs/archive/plans/2026-06-30-foundation.md\",\"docs/archive/plans/2026-06-30-ipc-control-plane.md\",\"docs/archive/plans/2026-06-30-pipeline-engine.md\",\"docs/archive/plans/2026-06-30-scope-enforcement.md\",\"docs/archive/plans/2026-06-30-tool-wrappers.md\",\"docs/archive/plans/2026-06-30-triage-and-hooks.md\",\"docs/archive/plans/2026-07-01-briefing-and-trace.md\",\"docs/archive/plans/2026-07-01-container-build.md\",\"docs/archive/plans/2026-07-01-dynamic-pipeline.md\",\"docs/archive/plans/2026-07-01-e2e-gap-fixes.md\",\"docs/archive/plans/2026-07-01-hermes-plugin-shell.md\",\"docs/archive/plans/2026-07-01-integration-test-harness.md\",\"docs/archive/plans/2026-07-01-iterative-reentry.md\",\"docs/archive/plans/2026-07-01-pod-and-cli-commands.md\",\"docs/archive/plans/2026-07-01-session-lifecycle.md\",\"docs/archive/plans/2026-07-02-llm-driven-redesign.md\",\"docs/archive/plans/2026-07-02-phase3-infrastructure-hardening.md\",\"docs/archive/plans/2026-07-02-structured-perceptor.md\",\"docs/archive/plans/2026-07-15-screenshot-vision-analysis.md\",\"docs/archive/rfe/2026-07-07-engagement-query-tool.md\",\"docs/archive/rfe/2026-07-07-screenshot-vision-analysis.md\",\"docs/archive/rfe/2026-07-07-subagent-delegation.md\",\"docs/archive/rfe/2026-07-10-credential-fragment-model.md\",\"docs/archive/rfe/2026-07-11-dns-aware-scope-enforcement.md\",\"docs/archive/rfe/2026-07-13-infrastructure-vs-prompt-boundary.md\",\"docs/archive/rfe/2026-07-15-engagement-management.md\",\"docs/archive/spec-gaps.md\",\"docs/archive/specs/2026-06-29-project-structure-design.md\",\"docs/archive/specs/2026-07-01-phase-2a-hermes-integration.md\",\"docs/archive/specs/2026-07-02-hermes-convention-refactor.md\",\"docs/archive/specs/2026-07-02-llm-driven-redesign.md\",\"docs/archive/specs/2026-07-02-phase3-infrastructure-hardening.md\",\"docs/archive/specs/2026-07-02-structured-perceptor.md\",\"docs/archive/specs/2026-07-08-engagement-lifecycle.md\",\"docs/archive/specs/2026-07-09-tool-definition-registry.md\",\"docs/archive/specs/2026-07-15-screenshot-vision-analysis.md\",\"docs/archive/superpowers/plans/2026-07-01-phase1-first-light.md\",\"docs/archive/superpowers/plans/2026-07-01-scope-enforcement.md\",\"docs/archive/superpowers/plans/2026-07-02-hermes-convention-refactor.md\",\"docs/archive/superpowers/plans/2026-07-03-nse-script-fuzzy-matching.md\",\"docs/archive/superpowers/plans/2026-07-04-artifact-storage.md\",\"docs/archive/superpowers/plans/2026-07-04-db-query-finding-parser.md\",\"docs/archive/superpowers/plans/2026-07-04-db-query-tool.md\",\"docs/archive/superpowers/plans/2026-07-04-redis-query-tool.md\",\"docs/archive/superpowers/plans/2026-07-06-curl-tool.md\",\"docs/archive/superpowers/plans/2026-07-06-db-enumeration-skill-split.md\",\"docs/archive/superpowers/plans/2026-07-06-katana-ffuf-tools.md\",\"docs/archive/superpowers/plans/2026-07-06-screenshot-tool.md\",\"docs/archive/superpowers/plans/2026-07-07-normalizer-refactor.md\",\"docs/archive/superpowers/plans/2026-07-07-result-transformer.md\",\"docs/archive/superpowers/plans/2026-07-07-rpcinfo-tool.md\",\"docs/archive/superpowers/plans/2026-07-07-validation-fixes.md\",\"docs/archive/superpowers/plans/2026-07-08-engagement-lifecycle.md\",\"docs/archive/superpowers/plans/2026-07-08-engagement-query-tool.md\",\"docs/archive/superpowers/plans/2026-07-09-ssh-exec-tool.md\",\"docs/archive/superpowers/plans/2026-07-09-tool-definition-registry.md\",\"docs/archive/superpowers/plans/2026-07-10-mongo-query.md\",\"docs/archive/superpowers/plans/2026-07-10-service-coverage.md\",\"docs/archive/superpowers/plans/2026-07-13-credential-fragment-model.md\",\"docs/archive/superpowers/plans/2026-07-14-structural-ownership.md\",\"docs/archive/superpowers/plans/2026-07-15-dns-aware-scope-enforcement.md\",\"docs/archive/superpowers/plans/2026-07-16-engagement-management.md\",\"docs/archive/superpowers/plans/2026-07-20-asset-registry.md\",\"docs/archive/superpowers/plans/2026-07-21-dry-simplification.md\",\"docs/archive/superpowers/plans/2026-07-21-register-refactor.md\",\"docs/archive/superpowers/specs/2026-07-01-scope-enforcement-design.md\",\"docs/archive/superpowers/specs/2026-07-03-nse-script-fuzzy-matching-design.md\",\"docs/archive/superpowers/specs/2026-07-04-artifact-storage-design.md\",\"docs/archive/superpowers/specs/2026-07-04-db-query-finding-parser-design.md\",\"docs/archive/superpowers/specs/2026-07-04-redis-query-tool-design.md\",\"docs/archive/superpowers/specs/2026-07-06-curl-tool-design.md\",\"docs/archive/superpowers/specs/2026-07-06-db-enumeration-skill-split-design.md\",\"docs/archive/superpowers/specs/2026-07-06-katana-ffuf-tools-design.md\",\"docs/archive/superpowers/specs/2026-07-06-screenshot-tool-design.md\",\"docs/archive/superpowers/specs/2026-07-07-normalizer-refactor-design.md\",\"docs/archive/superpowers/specs/2026-07-07-result-transformer-design.md\",\"docs/archive/superpowers/specs/2026-07-07-rpcinfo-tool-design.md\",\"docs/archive/superpowers/specs/2026-07-07-validation-fixes-design.md\",\"docs/archive/superpowers/specs/2026-07-08-engagement-query-tool-design.md\",\"docs/archive/superpowers/specs/2026-07-08-subagent-delegation-design.md\",\"docs/archive/superpowers/specs/2026-07-09-ssh-exec-tool-design.md\",\"docs/archive/superpowers/specs/2026-07-10-mongo-query-design.md\",\"docs/archive/superpowers/specs/2026-07-10-service-coverage-design.md\",\"docs/archive/superpowers/specs/2026-07-13-credential-fragment-model-design.md\",\"docs/archive/superpowers/specs/2026-07-14-remediation-design.md\",\"docs/archive/superpowers/specs/2026-07-15-dns-aware-scope-enforcement-design.md\",\"docs/archive/superpowers/specs/2026-07-16-engagement-management-design.md\",\"docs/archive/superpowers/specs/2026-07-20-asset-registry-design.md\",\"docs/archive/superpowers/specs/2026-07-21-dry-simplification-design.md\",\"docs/archive/superpowers/specs/2026-07-21-register-refactor-design.md\"]},{\"name\":\"Other \u2014 docs-research\",\"slug\":\"other-docs-research\",\"files\":[\"docs/research/2026-07-02-offensive-security-research.md\",\"docs/research/2026-07-04-hermes-configuration-deep-dive.md\",\"docs/research/2026-07-10-c2-integration-strategy.md\"]},{\"name\":\"Other \u2014 docs-rfe\",\"slug\":\"other-docs-rfe\",\"files\":[\"docs/rfe/2026-07-11-zap-proxy-web-scanning.md\",\"docs/rfe/2026-07-25-zap-automation-framework.md\",\"docs/rfe/2026-07-26-test-target-infrastructure.md\"]},{\"name\":\"Other \u2014 docs-superpowers\",\"slug\":\"other-docs-superpowers\",\"files\":[\"docs/superpowers/archive/2026-07-22-zap-deep-integration-design.md\",\"docs/superpowers/archive/2026-07-22-zap-deep-integration.md\",\"docs/superpowers/archive/2026-07-22-zap-integration-gaps-design.md\",\"docs/superpowers/archive/2026-07-22-zap-integration-gaps.md\",\"docs/superpowers/archive/2026-07-23-e2e-research-findings.md\",\"docs/superpowers/archive/2026-07-23-e2e-test-integrity-design.md\",\"docs/superpowers/archive/2026-07-23-e2e-test-integrity.md\",\"docs/superpowers/plans/2026-07-26-zap-af-pipeline-integration.md\",\"docs/superpowers/plans/2026-07-27-phase-aware-autonomy.md\",\"docs/superpowers/plans/archive/2026-07-29-gateway-network-isolation.md\",\"docs/superpowers/specs/2026-07-26-zap-af-pipeline-integration-design.md\",\"docs/superpowers/specs/2026-07-27-phase-aware-autonomy-design.md\",\"docs/superpowers/specs/archive/2026-07-29-gateway-network-isolation-design.md\"]},{\"name\":\"Other \u2014 profile\",\"slug\":\"other-profile\",\"files\":[\"profile/README.md\",\"profile/config.yaml\",\"profile/distribution.yaml\"]},{\"name\":\"Other \u2014 profile-skins\",\"slug\":\"other-profile-skins\",\"files\":[\"profile/skins/argicida.yaml\"]},{\"name\":\"Other \u2014 pyproject.toml\",\"slug\":\"other-pyproject-toml\",\"files\":[\"pyproject.toml\"]},{\"name\":\"Other \u2014 src-argicida\",\"slug\":\"other-src-argicida\",\"files\":[\"src/argicida/argicida/py.typed\",\"src/argicida/pyproject.toml\"]},{\"name\":\"Other \u2014 src-argicida_agent\",\"slug\":\"other-src-argicida-agent\",\"files\":[\"src/argicida_agent/argicida_agent/py.typed\",\"src/argicida_agent/pyproject.toml\"]},{\"name\":\"Other \u2014 src-argicida_common\",\"slug\":\"other-src-argicida-common\",\"files\":[\"src/argicida_common/argicida_common/py.typed\",\"src/argicida_common/pyproject.toml\"]},{\"name\":\"Other \u2014 tests\",\"slug\":\"other-tests\",\"files\":[\"tests/__init__.py\",\"tests/conftest.py\",\"tests/factories.py\"]},{\"name\":\"Other \u2014 tests-argicida\",\"slug\":\"other-tests-argicida\",\"files\":[\"tests/argicida/briefing/__init__.py\",\"tests/argicida/briefing/test_generator.py\",\"tests/argicida/briefing/test_prompts.py\",\"tests/argicida/commands/__init__.py\",\"tests/argicida/commands/test_cli.py\",\"tests/argicida/commands/test_formatting.py\",\"tests/argicida/commands/test_slash.py\",\"tests/argicida/commands/test_update_data.py\",\"tests/argicida/conftest.py\",\"tests/argicida/db/test_asset_repo.py\",\"tests/argicida/db/test_asset_tech.py\",\"tests/argicida/db/test_candidate_engine.py\",\"tests/argicida/db/test_credential_helpers.py\",\"tests/argicida/db/test_credentials.py\",\"tests/argicida/db/test_encryption.py\",\"tests/argicida/db/test_finding_repo.py\",\"tests/argicida/db/test_helpers.py\",\"tests/argicida/db/test_migrations.py\",\"tests/argicida/db/test_store.py\",\"tests/argicida/db/test_store_async.py\",\"tests/argicida/engagement/__init__.py\",\"tests/argicida/engagement/test_lifecycle.py\",\"tests/argicida/engagement/test_session.py\",\"tests/argicida/engagement/test_unbind.py\",\"tests/argicida/evidence/test_chain.py\",\"tests/argicida/evidence/test_chain_clear.py\",\"tests/argicida/evidence/test_keys.py\",\"tests/argicida/evidence/test_signer.py\",\"tests/argicida/hooks/conftest.py\",\"tests/argicida/hooks/test_adapters.py\",\"tests/argicida/hooks/test_asset_context.py\",\"tests/argicida/hooks/test_asset_coverage.py\",\"tests/argicida/hooks/test_asset_extractor.py\",\"tests/argicida/hooks/test_autonomy.py\",\"tests/argicida/hooks/test_context.py\",\"tests/argicida/hooks/test_context_tech.py\",\"tests/argicida/hooks/test_coverage_deep_testing.py\",\"tests/argicida/hooks/test_evidence.py\",\"tests/argicida/hooks/test_finalize_zap.py\",\"tests/argicida/hooks/test_normalizer.py\",\"tests/argicida/hooks/test_normalizer_errors.py\",\"tests/argicida/hooks/test_normalizer_perceptor.py\",\"tests/argicida/hooks/test_output_store.py\",\"tests/argicida/hooks/test_parse_result.py\",\"tests/argicida/hooks/test_parsers.py\",\"tests/argicida/hooks/test_password_decoding.py\",\"tests/argicida/hooks/test_perceptor.py\",\"tests/argicida/hooks/test_result_transformers.py\",\"tests/argicida/hooks/test_scope_expansion.py\",\"tests/argicida/hooks/test_scope_guard.py\",\"tests/argicida/hooks/test_service_registry.py\",\"tests/argicida/hooks/test_session_lifecycle.py\",\"tests/argicida/hooks/test_session_start.py\",\"tests/argicida/ipc/test_client.py\",\"tests/argicida/pod/test_builder.py\",\"tests/argicida/pod/test_config.py\",\"tests/argicida/pod/test_launcher.py\",\"tests/argicida/pod/test_launcher_zap.py\",\"tests/argicida/pod/test_manager.py\",\"tests/argicida/pod/test_session.py\",\"tests/argicida/pod/test_session_host_entries.py\",\"tests/argicida/pod/test_vpn_endpoint.py\",\"tests/argicida/registration/__init__.py\",\"tests/argicida/registration/test_context.py\",\"tests/argicida/registration/test_state.py\",\"tests/argicida/scope/conftest.py\",\"tests/argicida/scope/test_db_query_scope.py\",\"tests/argicida/scope/test_hostname_resolution.py\",\"tests/argicida/scope/test_netfilter.py\",\"tests/argicida/scope/test_normalize_target.py\",\"tests/argicida/scope/test_redis_cli_profile.py\",\"tests/argicida/scope/test_rpcinfo_profile.py\",\"tests/argicida/scope/test_validator.py\",\"tests/argicida/test_asset_consistency.py\",\"tests/argicida/test_consistency.py\",\"tests/argicida/test_phase_migration.py\",\"tests/argicida/test_register.py\",\"tests/argicida/test_session_consistency.py\",\"tests/argicida/tools/conftest.py\",\"tests/argicida/tools/defs/__init__.py\",\"tests/argicida/tools/defs/test_all_defs.py\",\"tests/argicida/tools/defs/test_discovery.py\",\"tests/argicida/tools/defs/test_mongo_query_def.py\",\"tests/argicida/tools/defs/test_naabu_def.py\",\"tests/argicida/tools/defs/test_protocol.py\",\"tests/argicida/tools/defs/test_ssh_exec_def.py\",\"tests/argicida/tools/test_add_host_asset.py\",\"tests/argicida/tools/test_add_host_dnsmasq.py\",\"tests/argicida/tools/test_artifact_handlers.py\",\"tests/argicida/tools/test_asset_query_handlers.py\",\"tests/argicida/tools/test_base.py\",\"tests/argicida/tools/test_curl_handler.py\",\"tests/argicida/tools/test_curl_normalizer.py\",\"tests/argicida/tools/test_db_query_handler.py\",\"tests/argicida/tools/test_db_query_normalizer.py\",\"tests/argicida/tools/test_engagement_query_fragments.py\",\"tests/argicida/tools/test_extractor_base.py\",\"tests/argicida/tools/test_extractors.py\",\"tests/argicida/tools/test_ffuf_normalizer.py\",\"tests/argicida/tools/test_handlers.py\",\"tests/argicida/tools/test_katana_normalizer.py\",\"tests/argicida/tools/test_management_handlers.py\",\"tests/argicida/tools/test_management_handlers_zap.py\",\"tests/argicida/tools/test_management_helpers.py\",\"tests/argicida/tools/test_normalizer_components.py\",\"tests/argicida/tools/test_normalizer_has_flag.py\",\"tests/argicida/tools/test_normalizer_shared_classes.py\",\"tests/argicida/tools/test_normalizers.py\",\"tests/argicida/tools/test_normalizers_mongo.py\",\"tests/argicida/tools/test_normalizers_nuclei.py\",\"tests/argicida/tools/test_normalizers_smb.py\",\"tests/argicida/tools/test_normalizers_ssh.py\",\"tests/argicida/tools/test_normalizers_zap.py\",\"tests/argicida/tools/test_parsers_make_finding.py\",\"tests/argicida/tools/test_parsers_mongo.py\",\"tests/argicida/tools/test_parsers_zap.py\",\"tests/argicida/tools/test_redis_query_normalizer.py\",\"tests/argicida/tools/test_schemas.py\",\"tests/argicida/tools/test_screenshot_handler.py\",\"tests/argicida/tools/test_screenshot_vision.py\",\"tests/argicida/tools/test_shared_utils.py\",\"tests/argicida/tools/test_tool_def_extractor.py\",\"tests/argicida/tools/test_transformer_infrastructure.py\",\"tests/argicida/tools/test_transformers_dnsx_proposal.py\",\"tests/argicida/tools/test_transformers_mongo.py\",\"tests/argicida/tools/test_transformers_ssh.py\",\"tests/argicida/tools/test_transformers_zap.py\",\"tests/argicida/tools/test_zap_new_tools.py\",\"tests/argicida/triage/test_asset_discovery.py\",\"tests/argicida/triage/test_classifier.py\",\"tests/argicida/triage/test_data.py\",\"tests/argicida/triage/test_dedup.py\",\"tests/argicida/triage/test_scorer.py\",\"tests/argicida/zap/__init__.py\",\"tests/argicida/zap/test_client.py\",\"tests/argicida/zap/test_client_af.py\",\"tests/argicida/zap/test_client_intel.py\",\"tests/argicida/zap/test_credential_tester.py\",\"tests/argicida/zap/test_manager.py\",\"tests/argicida/zap/test_manager_af.py\",\"tests/argicida/zap/test_plan_executor.py\",\"tests/argicida/zap/test_plan_generator.py\",\"tests/argicida/zap/test_scan_policy.py\",\"tests/argicida/zap/test_scan_policy_passive.py\"]},{\"name\":\"Other \u2014 tests-argicida_agent\",\"slug\":\"other-tests-argicida-agent\",\"files\":[\"tests/argicida_agent/executor/test_runner.py\",\"tests/argicida_agent/ipc/test_server.py\",\"tests/argicida_agent/test_main.py\"]},{\"name\":\"Other \u2014 tests-argicida_common\",\"slug\":\"other-tests-argicida-common\",\"files\":[\"tests/argicida_common/test_asset_schema.py\",\"tests/argicida_common/test_constants.py\",\"tests/argicida_common/test_credential_schema.py\",\"tests/argicida_common/test_engagement_schema.py\",\"tests/argicida_common/test_finding_schema.py\",\"tests/argicida_common/test_ipc_protocol.py\",\"tests/argicida_common/test_ipc_schema.py\",\"tests/argicida_common/test_scope_schema.py\"]},{\"name\":\"Other \u2014 tests-e2e\",\"slug\":\"other-tests-e2e\",\"files\":[\"tests/e2e/__init__.py\",\"tests/e2e/assertions.py\",\"tests/e2e/conftest.py\",\"tests/e2e/fault_injector.py\",\"tests/e2e/harness.py\",\"tests/e2e/test_e2e_smoke.py\"]},{\"name\":\"Other \u2014 tests-integration\",\"slug\":\"other-tests-integration\",\"files\":[\"tests/integration/__init__.py\",\"tests/integration/conftest.py\",\"tests/integration/test_asset_pipeline.py\",\"tests/integration/test_hook_pipeline.py\",\"tests/integration/test_phase_lifecycle.py\",\"tests/integration/test_registration.py\",\"tests/integration/test_session_lifecycle.py\",\"tests/integration/test_slash_commands.py\",\"tests/integration/test_tool_dispatch.py\"]},{\"name\":\"Other \u2014 tests-pod_integration\",\"slug\":\"other-tests-pod-integration\",\"files\":[\"tests/pod_integration/__init__.py\",\"tests/pod_integration/conftest.py\",\"tests/pod_integration/test_artifact_transfer.py\",\"tests/pod_integration/test_asset_registry.py\",\"tests/pod_integration/test_container_build.py\",\"tests/pod_integration/test_diagnostic_pod.py\",\"tests/pod_integration/test_engagement_management.py\",\"tests/pod_integration/test_fault_recovery.py\",\"tests/pod_integration/test_fault_stale_session.py\",\"tests/pod_integration/test_first_light.py\",\"tests/pod_integration/test_gateway_fail_closed.py\",\"tests/pod_integration/test_gateway_isolation.py\",\"tests/pod_integration/test_ipc_file_write.py\",\"tests/pod_integration/test_zap.py\",\"tests/pod_integration/test_zap_af.py\"]},{\"name\":\"Other \u2014 tests-scripts\",\"slug\":\"other-tests-scripts\",\"files\":[\"tests/scripts/__init__.py\",\"tests/scripts/test_sync_skills.py\"]}]}]};\n\n(function() {\n  var activePage = 'overview';\n\n  document.addEventListener('DOMContentLoaded', function() {\n    mermaid.initialize({ startOnLoad: false, theme: 'neutral', securityLevel: 'loose' });\n    renderMeta();\n    renderNav();\n    document.getElementById('menu-toggle').addEventListener('click', function() {\n      document.getElementById('sidebar').classList.toggle('open');\n    });\n    if (location.hash &amp;&amp; location.hash.length &gt; 1) {\n      activePage = decodeURIComponent(location.hash.slice(1));\n    }\n    navigateTo(activePage);\n  });\n\n  function renderMeta() {\n    if (!META) return;\n    var el = document.getElementById('meta-info');\n    var parts = [];\n    if (META.generatedAt) {\n      parts.push(new Date(META.generatedAt).toLocaleDateString());\n    }\n    if (META.model) parts.push(META.model);\n    if (META.fromCommit) parts.push(META.fromCommit.slice(0, 8));\n    el.textContent = parts.join(' \\u00b7 ');\n  }\n\n  function renderNav() {\n    var container = document.getElementById('nav-tree');\n    var html = '\n';\n    html += 'Overview';\n    html += '';\n    if (TREE.length &gt; 0) {\n      html += '\nModules';\n      html += buildNavTree(TREE);\n    }\n    container.innerHTML = html;\n    container.addEventListener('click', function(e) {\n      var target = e.target;\n      while (target &amp;&amp; !target.dataset.page) { target = target.parentElement; }\n      if (target &amp;&amp; target.dataset.page) {\n        e.preventDefault();\n        navigateTo(target.dataset.page);\n      }\n    });\n  }\n\n  function buildNavTree(nodes) {\n    var html = '';\n    for (var i = 0; i &lt; nodes.length; i++) {\n      var node = nodes[i];\n      html += '\n';\n      html += '' + escH(node.name) + '';\n      if (node.children &amp;&amp; node.children.length &gt; 0) {\n        html += '\n' + buildNavTree(node.children) + '';\n      }\n      html += '';\n    }\n    return html;\n  }\n\n  function escH(s) {\n    var d = document.createElement('div');\n    d.textContent = s;\n    return d.innerHTML;\n  }\n\n  function navigateTo(page) {\n    activePage = page;\n    location.hash = encodeURIComponent(page);\n\n    var items = document.querySelectorAll('.nav-item');\n    for (var i = 0; i &lt; items.length; i++) {\n      if (items[i].dataset.page === page) {\n        items[i].classList.add('active');\n      } else {\n        items[i].classList.remove('active');\n      }\n    }\n\n    var contentEl = document.getElementById('content');\n    var md = PAGES[page];\n\n    if (!md) {\n      contentEl.innerHTML = '\n\nPage not found\n' + escH(page) + '.md does not exist.';\n      return;\n    }\n\n    contentEl.innerHTML = marked.parse(md);\n\n    // Rewrite .md links to hash navigation\n    var links = contentEl.querySelectorAll('a[href]');\n    for (var i = 0; i &lt; links.length; i++) {\n      var href = links[i].getAttribute('href');\n      if (href &amp;&amp; href.endsWith('.md') &amp;&amp; href.indexOf('://') === -1) {\n        var slug = href.replace(/\\.md$/, '');\n        links[i].setAttribute('href', '#' + encodeURIComponent(slug));\n        (function(s) {\n          links[i].addEventListener('click', function(e) {\n            e.preventDefault();\n            navigateTo(s);\n          });\n        })(slug);\n      }\n    }\n\n    // Convert mermaid code blocks into mermaid divs\n    var mermaidBlocks = contentEl.querySelectorAll('pre code.language-mermaid');\n    for (var i = 0; i &lt; mermaidBlocks.length; i++) {\n      var pre = mermaidBlocks[i].parentElement;\n      var div = document.createElement('div');\n      div.className = 'mermaid';\n      div.textContent = mermaidBlocks[i].textContent;\n      pre.parentNode.replaceChild(div, pre);\n    }\n    try { mermaid.run({ querySelector: '.mermaid' }); } catch(e) {}\n\n    window.scrollTo(0, 0);\n    document.getElementById('sidebar').classList.remove('open');\n  }\n})();\n\n\n\n", "creation_timestamp": "2026-07-30T20:45:27.381525Z"}