Files
strix/HARNESS_WIKI.md
T
0xallam a35a4a22b1 docs: harness wiki + SDK migration plan + audits + playbook + testing strategy
Seven internal documents that frame the migration to the OpenAI Agents SDK:

- HARNESS_WIKI.md      legacy harness deep-dive (every subsystem, file:line refs)
- MIGRATION_EVALUATION.md  architectural plan (rev 2 — bridges + tradeoffs)
- AUDIT.md             pre-execution audit; 5 plan corrections (C1-C5)
- AUDIT_R2.md          round 1 audit; 7 more corrections (C6-C12)
- AUDIT_R3.md          round 3 audit; 13 more corrections (C13-C25) + 3 type fixes
- PLAYBOOK.md          file-by-file specs, per-tool contracts, day-1 commit list
- TESTING_STRATEGY.md  layered testing strategy + feature inventory matrix

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 23:37:41 -07:00

58 KiB
Raw Blame History

Strix Harness — Internal Architecture Wiki

Internal deep-dive for the team. Maps every subsystem, every tool, every architectural decision, with path:line references throughout.

Generated against main at 9fb1012 (fix: --config flag now fully overrides ~/.strix/cli-config.json). When code drifts, prefer git log and the source over this document.


Table of Contents

  1. What Strix Is
  2. Architecture at a Glance
  3. Repository Layout
  4. Lifecycle of One Run
  5. Agent System
  6. LLM Layer
  7. Tool System
  8. Runtime & Sandbox
  9. Interface (CLI / TUI / Headless)
  10. Prompts
  11. Skills
  12. Config
  13. Telemetry & Persistence
  14. Cross-Cutting Design Decisions
  15. Recent Evolution (Notable Commits)
  16. Quick File Index

1. What Strix Is

strix-agent (PyPI: strix-agent, version 0.8.3 per pyproject.toml:3) is an autonomous AI hacker harness. The agent dynamically pentests apps — runs targets in a sandboxed Kali container, finds vulns, validates them with PoCs, and writes a report.

Mission shape: loop an LLM against a Kali-loaded sandbox until it produces a vulnerability report. Provider-agnostic via litellm. Multi-agent orchestration. Whitebox + blackbox + greybox modes.

Core deps (pyproject.toml:25-39): litellm[proxy], pydantic, rich, docker, textual, tenacity, cvss, traceloop-sdk, opentelemetry-exporter-otlp-proto-http, scrubadub. Optional sandbox extra adds fastapi, uvicorn, ipython, playwright, pyte, libtmux, gql, openhands-aci.

Entry point: strix.interface.main:main (pyproject.toml:43).


2. Architecture at a Glance

                ┌──────────────────────────────────────────────────────┐
                │  HOST PROCESS                                         │
                │                                                       │
   user CLI ──▶ │  interface/main.py                                    │
                │       │                                               │
                │       ▼                                               │
                │  StrixAgent (root) ──spawns──▶ StrixAgent (subagent) │
                │       │                              │ thread        │
                │       │ agent_loop()                 │               │
                │       ▼                              ▼               │
                │  ┌──────────┐  ┌─────────────────────────┐           │
                │  │ LLM      │  │ Tool Executor           │           │
                │  │ litellm  │  │ ──local────▶ in-process │           │
                │  │ stream   │  │ ──sandbox──▶ HTTP POST  │           │
                │  └──────────┘  └─────────────┬───────────┘           │
                │                              │                       │
                │  Tracer ──▶ events.jsonl     │                       │
                │             OTel/Traceloop   │                       │
                │             PostHog          │                       │
                └──────────────────────────────┼───────────────────────┘
                                               │ Bearer-auth HTTP
                                               ▼
                ┌──────────────────────────────────────────────────────┐
                │  SANDBOX CONTAINER (Kali, one per scan)              │
                │                                                       │
                │   FastAPI tool server :48081                          │
                │     POST /execute → registry → tool fn               │
                │                                                       │
                │   Caido HTTP proxy :48080  (CA-injected)             │
                │   Playwright Chromium      (headless, no-sandbox)    │
                │   tmux + IPython           (terminal, python)        │
                │   /workspace               (shared FS)               │
                │   nmap, sqlmap, nuclei, semgrep, trivy, ...           │
                └──────────────────────────────────────────────────────┘

Two key boundaries:

  1. Host ↔ sandbox: HTTP/JSON over Bearer-token auth. Host owns LLM + telemetry; sandbox owns dangerous tools.
  2. Provider-agnostic LLM: everything goes through litellm.acompletion; tool calls are custom XML in text, not native function-calling — provides multi-provider compatibility at the cost of native streaming schemas.

3. Repository Layout

strix/
├── agents/            # Agent loop, state, multi-agent graph orchestration
│   ├── base_agent.py            # Core loop, ~620 lines
│   ├── state.py                 # AgentState pydantic model
│   └── StrixAgent/
│       ├── strix_agent.py       # execute_scan() entry, task assembly
│       └── system_prompt.jinja  # 32 KB Jinja system prompt
├── llm/               # Completion wrapper, provider routing, memory mgmt
│   ├── llm.py                   # acompletion wrapper, streaming, retries
│   ├── config.py                # LLMConfig dataclass
│   ├── memory_compressor.py     # Token-budget pruning + LLM summarization
│   ├── utils.py                 # Tool-format normalization, XML parsing
│   └── dedupe.py                # Vulnerability dedup via LLM similarity
├── tools/             # Every tool the agent can call
│   ├── registry.py              # @register_tool decorator, schema loader
│   ├── executor.py              # Dual local/sandbox dispatch, result fmt
│   ├── context.py               # contextvar agent_id propagation
│   ├── argument_parser.py       # Type coercion of XML string args
│   ├── browser/                 # Playwright Chromium (24 actions)
│   ├── terminal/                # tmux + libtmux interactive shell
│   ├── python/                  # IPython kernel, persistent
│   ├── proxy/                   # Caido GraphQL client
│   ├── notes/                   # Wiki + categorized notes (JSONL persisted)
│   ├── todo/                    # In-memory todo list
│   ├── reporting/               # create_vulnerability_report w/ CVSS
│   ├── web_search/              # Perplexity sonar-reasoning-pro
│   ├── file_edit/               # openhands-aci str_replace_editor + rg
│   ├── finish/                  # finish_scan (root only)
│   ├── thinking/                # think tool (planning escape hatch)
│   ├── load_skill/              # Runtime skill injection
│   └── agents_graph/            # create_agent, send_message_to_agent, ...
├── runtime/           # Docker-side container management
│   ├── docker_runtime.py        # Host-side: launch/healthcheck/cleanup
│   └── tool_server.py           # Sandbox-side FastAPI tool server
├── interface/         # CLI + Textual TUI + headless
│   ├── main.py                  # argparse, validation, mode dispatch
│   ├── cli.py                   # Headless mode (Rich)
│   ├── tui.py                   # Textual app, modal screens
│   └── utils.py                 # Helpers (target inference, run-dir, ...)
├── prompts/           # Vulnerability-specific Jinja prompts (e.g. NoSQLi)
├── skills/            # Markdown playbooks (vulns, frameworks, scan modes)
├── config/            # Config class, env layering, file persistence
├── telemetry/         # Tracer, OTel, Scrubadub PII redaction, PostHog
└── utils/             # resource_paths.py for frozen-vs-dev path resolution

containers/
├── Dockerfile                   # Kali rolling, all the pentest tools
└── docker-entrypoint.sh         # Caido boot, CA install, tool server start

4. Lifecycle of One Run

End-to-end trace of strix --target ./app:

  1. CLI parse (interface/main.py:267-426): parse args, validate, infer target types via infer_target_type() (interface/utils.py). Resolve diff scope if --scope-mode=diff.
  2. Config layering (config/config.py): apply ~/.strix/cli-config.json (or --config <path> override) into os.environ; resolve STRIX_LLM, LLM_API_KEY via resolve_llm_config() at config/config.py:199-224.
  3. LLM warm: validate model reachable.
  4. Docker pull if needed; image pin ghcr.io/usestrix/strix-sandbox:0.1.13 (config/config.py:43).
  5. Run name + run dir: strix_runs/<run-name>/. Tracer init (telemetry/tracer.py:50+).
  6. Mode dispatch: TUI (tui.py) or CLI (cli.py). Both end up calling StrixAgent.execute_scan(scan_config).
  7. Sandbox launch (runtime/docker_runtime.py:111-173): container created with name strix-scan-{scan_id}, two random host ports mapped to container ports 48080 (Caido) and 48081 (tool server), 32-byte bearer token generated, local_sources tar-copied into /workspace.
  8. Container boot (containers/docker-entrypoint.sh): start Caido → fetch GraphQL token via loginAsGuest → create temp project → install CA cert into NSS + system trust → set system-wide proxy env vars → spawn tool server as pentester user → wait for /health ready.
  9. Agent loop (agents/base_agent.py:152-260): see §5.
  10. Termination: root agent calls finish_scan (tools/finish/finish_actions.py) when work is done; tracer writes final report penetration_test_report.md and per-finding JSONs under vulnerabilities/.
  11. Cleanup: docker rm -f async-spawned (runtime/docker_runtime.py:334-352).
  12. Exit code: 0 clean, 2 if vulns found in headless mode (per cli.py).

5. Agent System

5.1 Single-Agent Loop

agents/base_agent.py:152-260 (agent_loop). Each iteration:

  1. _initialize_sandbox_and_state() once at start (base_agent.py:158).
  2. Check messages: _check_agent_messages() (base_agent.py:448-531) drains the inter-agent message queue, wrapping each in <inter_agent_message> XML and appending to history.
  3. Iteration counter bump and warning watchdog: at 85% of max_iterations (default 300) emit warning; at max-3 emit critical "next message MUST be finish" warning (base_agent.py:186-211).
  4. _process_iteration() (base_agent.py:214-216):
    • Compress history (memory compressor, see §6.4).
    • Build messages, call LLM via async generator (llm.py:156-218).
    • Parse tool invocations from streamed response (custom XML parser, see §6.5).
    • _execute_actions()process_tool_invocations() (tools/executor.py:313-342) — sequential per-action dispatch.
    • Append observation XML to history.
  5. Loop until state.should_stop() (state.py): completed | stop_requested | iteration >= max_iterations.

In interactive mode, after completed=True the loop pauses in _enter_waiting_state() (base_agent.py:287-329) instead of exiting — user can send more input. _wait_for_input() resumes on message arrival or waiting_timeout (default 600 s for subagents, 0 = forever for root, base_agent.py:265-266).

5.2 State Model

agents/state.py:12-173 (Pydantic AgentState):

Field Purpose
agent_id, agent_name, parent_id Identity. parent_id is None ⇔ root agent.
task Initial task string.
messages Conversation history list (role/content tuples; multimodal-capable).
iteration, max_iterations Hard budget (default 300).
waiting_for_input, waiting_start_time, waiting_timeout Interactive-mode pause state.
completed, stop_requested, final_result Termination signals.
sandbox_id, sandbox_token, sandbox_info Sandbox handles (port, ID).
actions_taken, observations, errors Local audit trail.
start_time, last_updated ISO timestamps.

Snapshots (state.model_dump()) are stored verbatim in the agent-graph node when the agent is registered (base_agent.py:122-134).

5.3 Multi-Agent Graph

tools/agents_graph/agents_graph_actions.py (839 lines) is the orchestration plane. Globals at :9-37:

  • _agent_graph = {"nodes": {agent_id: node}, "edges": [...]} — node = full agent metadata; edges = delegation or message.
  • _agent_messages: dict[agent_id, list[msg]] — per-agent inbox.
  • _agent_instances: dict[agent_id, agent_obj] — live in-process instances (for stat snapshots).
  • _agent_states: dict[agent_id, AgentState].
  • _running_agents: dict[agent_id, threading.Thread] — daemon threads.
  • _completed_agent_llm_totals + _agent_llm_stats_lock — accumulated stats from finished subagents.

Spawning (create_agent, :383-492): validate skills → build child LLMConfig inheriting parent flags → StrixAgent(config) → optionally copy parent history (inherit_context=True) → spawn daemon thread running _run_agent_in_thread() (:205-298). The thread creates a fresh asyncio event loop and runs agent.agent_loop(state.task). On finish, status flips to completed | stopped | failed, _finalize_agent_llm_stats() is called.

Identity injection: parent task is wrapped in <agent_delegation>...<agent_identity> so the child knows its name/ID and is told to never echo it (:238-266).

Finish from subagent (agent_finish, :567-685): only callable when parent_id != None. Builds <agent_completion_report> XML with summary/findings/recommendations and pushes it onto the parent's inbox. If the agent is whitebox, the wiki note is updated with a delta.

Finish from root (finish_scan, tools/finish/finish_actions.py:86-149): only callable from root (parent_id is None); blocks if any sibling/child agent is still running/stopping. Triggers tracer.update_scan_final_fields() which writes penetration_test_report.md.

Inter-agent messaging (send_message_to_agent, :495-563): synchronous append to _agent_messages[target] with edge metadata. No broker, no durability, single-process only.

Waiting (wait_for_message, :796-839): subagent calls this to pause; _check_agent_messages resumes it on arrival.

Graph view (view_agent_graph, :302-380): traversal printout, root-first.

5.4 Stats Aggregation Across the Tree

Recent fix (15c9571). _finalize_agent_llm_stats() (:54-68) snapshots a finished subagent's llm._total_stats and adds it under lock to _completed_agent_llm_totals. The root's reported totals (via tracer.get_total_llm_stats() at telemetry/tracer.py:801-834) are: sum(_completed_agent_llm_totals) + sum(live agent _total_stats). Before the fix, finalized children were dropped on cleanup — root undercounted.

5.5 Termination, Interrupts, and Cancellation

  • Hard limit: iteration >= max_iterations → loop exits (base_agent.py:174). 85% / N-3 warnings give the model time to wrap up.
  • User Ctrl+C / parent kill: stop_agent(agent_id) (agents_graph_actions.py:688-748) sets state.request_stop() + calls agent_instance.cancel_current_execution() (base_agent.py:615-623) which cancels the running asyncio task. asyncio.CancelledError is caught (base_agent.py:232-243) — interactive mode enters waiting state; non-interactive re-raises.
  • Finish tools: see §5.3.

5.6 Streaming to TUI

The agent loop consumes the LLM async generator and after each chunk calls tracer.update_streaming_content(agent_id, accumulated_text) (base_agent.py:373-375). The TUI polls the tracer at 2 Hz and re-renders. On finalize, tracer.clear_streaming_content() and tracer.log_chat_message() snapshot the full message into the events log.


6. LLM Layer

6.1 Completion Wrapper

strix/llm/llm.py:156-218LLM.generate() is an async generator yielding LLMResponse objects. Pipeline:

  1. Retry loop (:162-171): max STRIX_LLM_MAX_RETRIES (default 5) attempts. Backoff min(90, 2 * 2**attempt) seconds. _should_retry() (:326-330) is True for network errors (no status_code) or litellm._should_retry(code) for HTTP statuses.
  2. Build args (_build_completion_args, :265-274): model resolution, reasoning effort, drop unsupported params (litellm.drop_params = True, litellm.modify_params = True, set at :25-26).
  3. Stream (_stream, :173-218):
    • acompletion(...) wrapped in asyncio.wait_for(timeout=self.config.timeout) — commit 60abc09.
    • Each await it.__anext__() also wrapped in asyncio.wait_for(timeout) — needed because litellm's own timeout doesn't propagate to httpx for Bedrock streaming, which can accept TCP and then send no data forever.
    • Accumulate text; when a closing </function> tag is seen, set done_streaming=1 and continue 5 more chunks for trailers, then stop.
  4. Stats (_update_usage_stats, :287-312): extract response.usage (input, completion, cached tokens). Cost via _extract_cost() (:314-324) — prefers response.usage.cost, else litellm.completion_cost(...) with provider stripped.
  5. Tool parsing (:212-217): normalize_tool_formatfix_incomplete_tool_callparse_tool_invocations (all in llm/utils.py).

6.2 Provider Routing

strix/llm/utils.py:34-61 defines STRIX_MODEL_MAPstrix/<short> aliases (e.g. strix/claude-sonnet-4.6) resolve to a tuple (api_model, canonical_model):

  • api_model is what gets passed to acompletion (typically OpenAI-compatible against the Strix proxy https://models.strix.ai/api/v1 set at config/config.py:8).
  • canonical_model is what's used for litellm capability checks like supports_prompt_caching() and supports_reasoning().

For non-strix/ prefixes, the model string is passed straight through. LLMConfig.__init__ (llm/config.py:8-41) reads LLM_API_BASEOPENAI_API_BASELITELLM_BASE_URLOLLAMA_API_BASE from env in priority order.

Per-provider quirks:

  • Anthropic: _is_anthropic() (llm.py:338-341) detects anthropic/ or claude substrings and adds an ephemeral cache control block to the system message via _add_cache_control() (:371-387).
  • OpenAI reasoning: if supports_reasoning() is true, set reasoning_effort (:265-266) — env STRIX_REASONING_EFFORT > config > scan-mode default (medium for quick, high otherwise).
  • Vision-less models: _strip_images() (:343-369) replaces image content with "[Image removed - model doesn't support vision]".
  • Vertex AI: optional extra in pyproject.toml:48. Documented in docs/llm-providers/vertex.mdx.

6.3 Retries & Timeouts (the 60abc09 story)

Symptom: Bedrock converse-stream calls would TCP-connect, send no chunks, and hang the agent loop indefinitely. faulthandler showed the loop blocked in selectors.select().

Fix: wrap both the initial acompletion call and every per-chunk read in asyncio.wait_for. Timeouts surface as TimeoutError (no status_code attr) which _should_retry() treats as retryable, kicking the backoff loop.

Default LLM_TIMEOUT = 300 (config/config.py:24).

6.4 Memory Compression

strix/llm/memory_compressor.py:152-219. Hard ceiling MAX_TOTAL_TOKENS = 100_000; compression triggers above ~90 K.

  • Image budget: keep last 3 images, replace older ones with [Previously attached image removed] (:134-149).
  • System messages: never compressed.
  • Recent floor: keep last 15 messages intact (MIN_RECENT_MESSAGES = 15).
  • Older messages: chunk in groups of 10, summarize via LLM call (separate acompletion, 120 s timeout). The summary prompt (:15-43) emphasizes preserving exact technical details — URLs, payloads, credentials, failed attempts (so the agent doesn't repeat them) — wrapped as <context_summary message_count='N'>...</context_summary>.
  • Token counting via litellm.token_counter() with a fallback of len(text) / 4.

This runs every iteration. Anthropic prompt cache helps with system-prompt cost but not history (which mutates).

6.5 Tool-Call Format (custom XML, NOT native function-calling)

Tools are injected into the system prompt as XML descriptions via get_tools_prompt() (tools/registry.py:280-300). The model is instructed to emit:

<function=tool_name>
  <parameter=key>value</parameter>
  <parameter=other>value</parameter>
</function>

Parser (llm/utils.py):

  • normalize_tool_format (:12-31): converts Anthropic-style <invoke name="X"> and other variants to the canonical form.
  • fix_incomplete_tool_call (:110-121): auto-closes unclosed tags when streaming is truncated.
  • parse_tool_invocations (:80-133): regex extraction; HTML-entity-decodes values.
  • clean_content (:135-160): strips tool XML and inter-agent control tags before logging to telemetry.

Why XML over native tool use? Multi-provider compatibility (works on any text LLM), graceful streaming truncation (early-exit at </function>), and full client-side control of formatting. Cost: tool descriptions are re-injected as text every call (no native streaming of schemas), and content has to be parsed by hand.

6.6 Prompt Assembly

_prepare_messages (llm.py:220-248):

  1. Render system prompt from Jinja template (agents/StrixAgent/system_prompt.jinja) with: interactive flag, system_prompt_context (authorized targets), loaded_skill_names, tools prompt.
  2. Insert agent-identity user message (hidden control block) — see §5.3.
  3. Run MemoryCompressor.compress_history() over conversation.
  4. If last message is assistant, append a <meta>Continue the task.</meta> continuation prompt (autonomous mode).
  5. Apply Anthropic cache control on the system message if applicable.

6.7 Stats / Cost

Per-call: extracted into RequestStats dataclass and accumulated in LLM._total_stats. Across the tree: see §5.4. Tracer renders these into the live TUI status panel and the final summary text (utils/format_token_count, etc.).

6.8 Vulnerability Deduplication

strix/llm/dedupe.py (~213 lines) — separate LLM call to compare a new finding against existing ones. Wired into tools/reporting/reporting_actions.py so duplicate vuln reports get rejected on submission.


7. Tool System

7.1 Registry & Schema Loading

strix/tools/registry.py:

  • Decorator: @register_tool(sandbox_execution: bool, requires_browser_mode: bool = False, requires_web_search_mode: bool = False) (:190-250).
  • Conditional registration via _should_register_tool() (:175-187): in sandbox mode, only sandbox_execution=True tools register. If STRIX_DISABLE_BROWSER=true, browser tools are skipped. If PERPLEXITY_API_KEY is missing, web_search is skipped.
  • Schema: each tool group has <group>_actions_schema.xml next to the implementation. Parsed via _parse_param_schema() (:131-149) → _tool_param_schemas[name] = {params, required, has_params}.
  • get_tools_prompt() (:280-300) emits the XML descriptions injected into the system prompt. Includes a {{DYNAMIC_SKILLS_DESCRIPTION}} placeholder filled by the load-skill subsystem.

7.2 Executor (Local vs Sandbox Dispatch)

strix/tools/executor.py:

  • execute_tool() is the single entrypoint. should_execute_in_sandbox(tool_name) (:29-37) decides routing.
  • Sandbox path (_execute_tool_in_sandbox, :39-99): POST to http://{host}:{tool_server_port}/execute with {agent_id, tool_name, kwargs} and Authorization: Bearer {token}. Connect timeout STRIX_SANDBOX_CONNECT_TIMEOUT=10, request timeout STRIX_SANDBOX_EXECUTION_TIMEOUT + 30 = 150 s.
  • Local path (_execute_tool_locally, :101-115): look up function, coerce arg types via argument_parser.convert_arguments(), inject agent_state if the function requests it (introspection over signature), await if async.
  • Argument validation (_validate_tool_arguments, :130-186): unknown params and missing required params → formatted error string returned to the LLM (no exception).
  • Result formatting (_format_tool_result, :227-256): truncate >10 KB to 4 KB head + ... [middle content truncated] ... + 4 KB tail. Wrap in <tool_result><tool_name>...</tool_name><result>...</result></tool_result>. Extract any screenshot key into a separate base64 image attachment.
  • Process orchestrator (process_tool_invocations, :313-342): iterate actions sequentially, aggregate result text, attach images as multimodal content blocks, return should_agent_finish boolean.

Sequential, not parallel. No asyncio.gather over tools — could be a future optimization.

7.3 Tool Catalog

Browser (strix/tools/browser/) — browser_action

Playwright Chromium singleton per container, shared event loop in a daemon thread (browser_instance.py:34-48). 24 actions:

  • Navigation: launch, goto, back, forward
  • Interaction: click, double_click, hover, type, press_key, scroll_up, scroll_down
  • Tabs: new_tab, switch_tab, close_tab, list_tabs
  • Misc: wait, execute_js, save_pdf, get_console_logs, view_source, close

Returns {screenshot: base64-png, url, title, tab_id, all_tabs, js_result, console_logs, page_source}. Viewport 1280×720, screenshot is viewport-only (not full-page) by default. Page source truncated to 20 KB; JS result to 5 KB; console logs capped at 200 entries / 30 KB total / 1 KB each. Browser launched with --no-sandbox --disable-web-security — intentional for XSS/CORS pentesting.

State keyed by get_current_agent_id() (contextvar). Tabs persist with sequential IDs (tab_1, tab_2, ...). atexit registered via _register_cleanup_handlers().

Terminal (strix/tools/terminal/) — terminal_execute

libtmux-backed (>=0.46.2). One tmux session per (agent_id, terminal_id), default terminal_id="default". PS1 customized to [STRIX_$?]$ so the exit code can be regex-extracted from the prompt (terminal_session.py:49-54). Pane history limited to 10 K lines.

Params: command, is_input (false=new command, true=feed input to running process), timeout (default 30 s), no_enter, terminal_id. Returns {content, status, exit_code, working_dir} where status is completed | running | error (with sub-states CONTINUE, NO_CHANGE_TIMEOUT, HARD_TIMEOUT).

Special-key sequences supported: C-c, ^X, S-X, M-X, F1-F12, arrow keys, Enter, Tab, BSpace. Output deduplication strips previous output prefix to show only new bytes.

Python (strix/tools/python/) — python_action

Persistent IPython kernel (>=9.3.0) per (agent_id, session_id). Actions: new_session, execute, close, list_sessions. cwd /workspace. Stdout truncated to 10 K, stderr to 5 K, repr to 10 K. Execution timeout enforced via thread join + cancellation flag (default 30 s).

Pre-injects proxy helper functions into the user namespace so the agent can send_request(...) directly (python_instance.py:30-47).

KeyboardInterrupt and SystemExit are caught and returned as errors rather than propagating.

HTTP Proxy (strix/tools/proxy/) — 7 tools

GraphQL client against Caido at http://127.0.0.1:48080/graphql (proxy_manager.py:25). Bearer token from env CAIDO_API_TOKEN. Tools: list_requests, view_request, send_request, repeat_request, scope_rules, list_sitemap, view_sitemap_entry.

Supports HTTPQL filter syntax for request queries. Pagination (offset, limit). view_request supports regex search through captured request/response pairs. Caido all-traffic capture is enabled because /etc/profile.d/proxy.sh sets http_proxy/https_proxy system-wide and the Caido CA cert is installed into the system + NSS trust stores.

Hardcoded port 48080. Caido v0.48.0 pinned in containers/Dockerfile.

Notes (strix/tools/notes/) — create_note, list_notes, get_note, update_note, delete_note (+ internal append_note_content)

In-memory dict + JSONL persistence at {run_dir}/notes/notes.jsonl. Wiki-category notes additionally rendered as Markdown to {run_dir}/wiki/{note_id}-{title}.md so they're human-readable artifacts of the run.

Categories: general | findings | methodology | questions | plan | wiki. IDs are 5-char UUID hex (collision-retry up to 20 attempts). Thread-safe via RLock. List preview defaults to 280 chars per note.

The wiki note in particular is the shared whitebox knowledge base between root and subagents; agent_finish for whitebox subagents auto-appends a delta (see §5.3).

Todos (strix/tools/todo/) — 6 tools

Per-agent in-memory storage; not persisted. Priorities critical | high | normal | low; statuses pending | in_progress | done. Bulk create/update via JSON list. IDs are 6-char UUID hex.

Reporting (strix/tools/reporting/) — create_vulnerability_report

Saves a CVSS-scored vulnerability to the run. Required fields: title, description, impact, target, technical_analysis, poc_description, poc_script_code (mandatory), remediation_steps, cvss_breakdown (XML with AV/AC/PR/UI/S/C/I/A enums).

Optional fields: endpoint, method, cve (CVE-\d{4}-\d{4,}), cwe (CWE-\d+), code_locations (XML with file/start_line/end_line/snippet/label/fix_before/fix_after — relative paths only, no ..).

CVSS computed via the cvss library. Deduplication via LLM similarity check (llm/dedupe.py). Persisted via tracer to {run_dir}/vulnerabilities/vuln_{id}.json.

Perplexity API (sonar-reasoning-pro model). 300 s timeout. System prompt tailored for security professionals — vuln details, CVEs, OWASP, exploit info. Skipped from registry if PERPLEXITY_API_KEY not set.

File Edit (strix/tools/file_edit/) — str_replace_editor, list_files, search_files

Wraps openhands-aci's file_editor (>=0.3.0). Commands create | str_replace | view | insert. Relative paths auto-prefixed with /workspace/. search_files uses ripgrep (rg); recursive listings capped at 500 results.

Finish (strix/tools/finish/) — finish_scan

Root-only. Validates: caller is root, all child agents not running/stopping, all four narrative fields non-empty (executive_summary, methodology, technical_analysis, recommendations). On success: tracer writes the final report.

Thinking (strix/tools/thinking/) — think

Minimal — records a thought string, validates non-empty, returns char count. Acts as the "free turn" escape hatch for planning so the agent can satisfy the per-message tool-call requirement (see §10) without doing real work.

Agents Graph (strix/tools/agents_graph/) — 6 tools

view_agent_graph, create_agent, agent_status, agent_finish, wait_for_message, send_message_to_agent. Mechanics covered in §5.3.

Load Skill (strix/tools/load_skill/) — load_skill

Runtime injection of additional skill content into the agent's context. Validates names against the registry. Replaces the {{DYNAMIC_SKILLS_DESCRIPTION}} placeholder. Max 5 skills per agent context.

7.4 Result Sanitization (commit 4934bb8)

Three layers of defense before tool output reaches the model or telemetry:

  1. Screenshot extraction (executor.py:345-353): if a result dict has key screenshot whose value is a base64 string, pull it out into a separate image attachment, replace its dict value with "[Image data extracted - see attached image]".
  2. Length truncation (:246-249): >10 KB results are split head + truncation marker + tail, 4 KB each side.
  3. Error truncation (:182-183): error strings capped at 500 chars with [truncated] suffix.
  4. Telemetry sanitization (telemetry/utils.py:67-150): scrubadub + regex on dict/list/tuple keys/values, redacting screenshot, sensitive-key patterns (api[-_]?key|token|secret|password|...), and bearer-style tokens (ghp_, ghs_, xox*).
  5. Content cleaning before logging (llm/utils.py:135-160, clean_content): strips tool XML, inter-agent control blocks, agent-identity blocks before they hit the JSONL events log — keeps the audit log readable and prevents log-injection of tool schemas.

7.5 Agent Context Propagation

strix/tools/context.py defines a single ContextVar current_agent_id. Set on every tool invocation in tool_server._run_tool (runtime/tool_server.py:71-83) and used by stateful tools (browser, terminal, python, todos) to silo per-agent state without explicit threading.


8. Runtime & Sandbox

8.1 Image (containers/Dockerfile)

  • Base: kalilinux/kali-rolling:latest (line 1).
  • Non-root pentester user with NOPASSWD sudo (lines 10-13) — needed for raw-socket pentest tools.
  • Pre-installed: nmap, nuclei, subfinder, naabu, ffuf, sqlmap, zaproxy, wapiti, caido-cli (v0.48.0); Go tools httpx, katana, gospider, interactsh; Python arjun, dirsearch, wafw00f, semgrep, bandit, trufflehog; JS retire, eslint, js-beautify, jshint, @ast-grep/cli, tree-sitter-cli; tree-sitter parsers for Java/JS/Python/Go/Bash/JSON/YAML/TS; gitleaks, trivy.
  • Sandbox-extra Python deps: fastapi, uvicorn, ipython, playwright, pyte, libtmux, gql, openhands-aci.
  • Self-signed CA chain at /app/certs/{ca.key,ca.crt,ca.p12}, 3650-day validity (lines 52-71).
  • Workspace /workspace owned by pentester (line 194).
  • Ports 48080 (Caido), 48081 (tool server) exposed.
  • Image pin: ghcr.io/usestrix/strix-sandbox:0.1.13 (strix/config/config.py:43, bumped in 640bd67).

8.2 Boot Sequence (containers/docker-entrypoint.sh)

  1. Caido start (lines 12-17): caido-cli --listen 0.0.0.0:48080 --allow-guests --no-logging --import-ca-cert /app/certs/ca.p12.
  2. Caido readiness poll (24-38): GET /graphql/ until 200/400, 30 attempts × 1 s.
  3. Login (50-74): GraphQL loginAsGuest mutation → bearer token, 5 retries with backoff. Exported as CAIDO_API_TOKEN.
  4. Project setup (79-109): create + select temp Caido project for capture.
  5. System-wide proxy (113-146): write /etc/profile.d/proxy.sh setting http_proxy/https_proxy/HTTP_PROXY/HTTPS_PROXY/ALL_PROXY=127.0.0.1:48080; mirror into /etc/environment, /etc/wgetrc. Import CA into NSS db so Chromium trusts it.
  6. Tool server start (154-180): sudo -u pentester python -m strix.runtime.tool_server with token, port, timeout. Wait for /health 200, 10 retries × 1 s.
  7. Ready (182): emit " Container ready" and exec the trailing args (typically sleep infinity).

8.3 Host-Side Runtime (strix/runtime/docker_runtime.py)

  • One container per scan (not per agent). All agents in a scan share the same container, the same /workspace, the same Caido proxy capture, the same browser/terminal/python sessions (keyed by agent_id contextvar).
  • Port allocation (:43-46): socket.bind(("", 0)) to grab two free host ports, mapped to container 48080 and 48081.
  • Token (:131): secrets.token_urlsafe(32) per container.
  • Container creation (:111-173): name strix-scan-{scan_id}, label strix-scan-id={scan_id}, capabilities NET_ADMIN | NET_RAW, extra_hosts={"host.docker.internal": "host-gateway"}, env passthrough including TOOL_SERVER_PORT / TOOL_SERVER_TOKEN / STRIX_SANDBOX_EXECUTION_TIMEOUT / HOST_GATEWAY. Reuses a running container if one exists for the same scan_id.
  • Healthcheck (:87-109): poll /health for 30 s with backoff before declaring ready.
  • Local source mount (:222-269): tar-pipe local sources into /workspace. (Not a Docker bind mount — copy on init.)
  • Reattach (:72-85): on existing container, re-extract token+ports from docker inspect env.
  • Cleanup (:322-352): docker stop + docker rm spawned as detached subprocess so the host doesn't block on shutdown.

No CPU/memory/network egress limits configured. Container has full host outbound access. Kill switches are: tool server asyncio.wait_for request timeout (default 120 s), and host-driven docker stop. There is no seccomp/AppArmor profile beyond Docker defaults.

8.4 Tool Server (strix/runtime/tool_server.py)

FastAPI app, served via Uvicorn on 0.0.0.0:{TOOL_SERVER_PORT}. Auth via HTTPBearer (:36-37, 42-57); the /health endpoint is unauth.

  • POST /execute (:86-127): JSON {agent_id, tool_name, kwargs}_run_tool (:71-83). Sets current_agent_id contextvar, looks up registry, calls function via asyncio.to_thread(). Per-agent task tracking in agent_tasks: dict[agent_id, asyncio.Task] (:39) — a new request for the same agent cancels the previous task (:94-97). Hard timeout asyncio.wait_for(REQUEST_TIMEOUT) default 120 s.
  • POST /register_agent (:130-135): registers an agent_id (used by host to pre-allocate state).
  • GET /health (:138-147): readiness/liveness.
  • Signal handling (:150-162): SIGTERM/SIGINT cancel all in-flight tasks; SIGPIPE ignored.
  • Returns {"result": ..., "error": ...} shape; HTTP 401 on bad token.

8.5 openhands-aci

Listed as sandbox dep (pyproject.toml:54). Used by strix/tools/file_edit/ to back str_replace_editor (the same primitive as in OpenHands / Claude Code's Edit semantics — view/create/str_replace/insert with strict matching).

8.6 Multi-Agent in One Sandbox

Subagents inherit sandbox_id/sandbox_token/sandbox_info via the parent's state (passed implicitly through the LLMConfig copy in create_agent). They share /workspace, the Caido proxy capture, and stateful tools (each agent gets its own browser tab manager / terminal session / python kernel keyed by agent_id). No per-agent process or container isolation.


9. Interface (CLI / TUI / Headless)

9.1 CLI Args (strix/interface/main.py:267-426)

Flag Purpose
-t / --target (multi) Target — URL / repo / local dir / domain / IP. Type inferred via infer_target_type().
--instruction Inline directive. Mutex with --instruction-file.
--instruction-file File path. Read into args.instruction.
-n / --non-interactive Headless mode (cli.py) instead of TUI.
-m / --scan-mode {quick,standard,deep} Default deep. Controls breadth/depth via prompt-injected skill.
--scope-mode {auto,diff,full} Default auto. In CI/headless, autodiff.
--diff-base Branch/commit to diff against (e.g. origin/main). Auto-detected if missing.
--config Path to custom cli-config.jsonfully overrides ~/.strix/cli-config.json (commit 9fb1012).

localhost targets are rewritten to host.docker.internal so the sandbox can reach host-served apps.

9.2 Scan Modes

Implemented as prompt content, not control-flow branching. LLMConfig.scan_mode flows through to the agent's loaded skill set:

  • quick (strix/skills/scan_modes/quick.md): time-boxed, prioritize high-impact (auth, IDOR, RCE, SQLi, SSRF, secrets), skip exhaustive enumeration, breadth>depth, minimal PoC validation.
  • standard (strix/skills/scan_modes/standard.md): balanced. Whitebox = repo map → semgrep → AST → secrets/deps. Blackbox = crawl, fingerprint, capture proxy traffic. Phase 2 = business logic; Phase 3 = systematic input/auth/access tests.
  • deep (default): exhaustive — every file, every endpoint, every parameter, every edge case, every user role, complete state-machine and trust-boundary modeling, maximum chaining.

Reasoning effort defaults flow off scan mode (llm/llm.py:74-82): quick→medium, else→high.

9.3 Scope Modes

resolve_diff_scope_context() (interface/utils.py:40+): computes DiffScopeResult from git diff <base>...HEAD. The result's instruction_block is injected into the user instruction rather than filtering files on disk — the agent decides prioritization. Used heavily for CI/PR workflows where you only want to test what changed.

9.4 Textual TUI (strix/interface/tui.py)

StrixTUIApp with modal screens: SplashScreen, HelpScreen, StopAgentScreen, VulnerabilityDetailScreen, plus the main agent-tree + log widgets. Multi-line ChatTextArea (Shift+Enter = newline, Enter = send). Keys: F1 help, Ctrl+Q/C quit, ESC stop, Tab cycle panels.

Live updates: an updater thread polls the tracer at 2 Hz and refreshes reactive widgets. Vulnerability discoveries trigger the modal popup via tracer.vulnerability_found_callback.

9.5 Headless / CLI (strix/interface/cli.py)

Async run loop. Rich panels for vuln-found events, live stats panel updated every 2 s. Exit codes: 0 clean, 2 if tracer.vulnerability_reports is non-empty (used to fail CI). Final completion panel includes target, duration, stats, output path.

9.6 Run Directory Layout (strix_runs/<run_name>/)

Created and managed by telemetry/tracer.py. Contents:

  • events.jsonl — every span/event in append-only JSONL (thread-safe writes).
  • vulnerabilities/vuln_{id}.json — one file per finding, sorted by severity, dedup-checked.
  • penetration_test_report.md — final markdown report (executive summary + methodology + technical analysis + recommendations).
  • notes/notes.jsonl — note ops audit log.
  • wiki/{note_id}-{slug}.md — human-readable wiki notes.
  • <target_subdir>/ — local source clones, per-target.

There is no execution checkpointing — if the process crashes mid-run, the agent restarts from scratch on retry. Resumability is limited to the interactive-mode wait/resume on inbound messages.


10. Prompts

10.1 The System Prompt

strix/agents/StrixAgent/system_prompt.jinja (~32 KB). Single template, two behavioral modes via {% if interactive %} ... {% else %} ... {% endif %}. Sections:

  1. Core capabilities (4-9): security assessment, validation, remediation.
  2. Communication rules (11-44): CLI markdown only; never echo control XML (inter_agent_message, agent_completion_report, agent_identity).
  3. Tool-call requirement (24-44, hardened in 4f90a56):
    • Interactive: a message without a tool call immediately stops the entire execution. "Planning..." and "I'll now scan..." both halt. The only exceptions are genuinely done or explicitly asking the user. The think tool is the planning escape hatch.
    • Autonomous: minimize chatter, never empty messages, use wait_for_message or finish when idle.
  4. Execution guidelines (47+):
    • System-verified scope block (injected at runtime with the authorized targets).
    • Authorization status: full authz for in-scope targets, don't question permission.
    • Refusal-avoidance language: treat as internal security engineering, not generic offensive activity.
    • Validation mandate: persist, iterate, assume more issues are hidden.
    • Multi-target coordination: build target map, correlate findings, reuse secrets/endpoints.
    • Testing modes: black-box, white-box, combined.
    • Methodology: scope → recon → automated scanning → targeted validation → continuous iteration → impact documentation.
    • Efficiency tactics: automate via Python, batch operations, parallel scans, fuzzers (ffuf/sqlmap/nuclei/semgrep) before custom payloads. For SQLi/XSS/RCE, spray via python/terminal not manual browser.
  5. Vulnerability methodology (230+): per-class attack surface, detection channels, chaining strategies, WAF bypasses for IDOR / SQLi / SSRF / RCE / XSS / XXE / path-traversal / race conditions / auth bypass / CSRF.

10.2 Vulnerability-Specific Prompts (strix/prompts/)

Currently only vulnerabilities/nosql_injection.jinja (~266 lines) — operator injection ($ne, $gt), boolean/timing/error oracles, auth bypass, regex-based extraction, JS execution, WAF bypass, deduplication. Covers MongoDB, CouchDB, Redis, Cassandra, Neo4j, GraphQL.

10.3 Persona System

There is no separate persona file. All agents are StrixAgent instances using the same Jinja system prompt. Differentiation comes from:

  • parent_id (root agent vs subagent → different finish tools, different prompts injected).
  • Loaded skills (root gets root_agent; whitebox gets coordination/source_aware_whitebox + custom/source_aware_sast).
  • system_prompt_context.authorized_targets (only set on root).
  • is_whitebox flag (toggles wiki-note auto-update on subagent finish).

11. Skills

strix/skills/ — Markdown playbooks loaded into the agent's system prompt. Categories:

Dir Contents
vulnerabilities/ Auth/JWT, IDOR, SQLi, NoSQL, XSS, XXE, SSRF, CSRF, business logic, race conditions, path traversal, RCE, auth bypass, info disclosure, mass assignment, open redirect, insecure uploads, subdomain takeover.
frameworks/ FastAPI, NestJS, Next.js.
technologies/ Firebase/Firestore, Supabase.
protocols/ GraphQL.
tooling/ ffuf, httpx, katana, naabu, nmap, nuclei, semgrep, sqlmap, subfinder.
cloud/ Kubernetes (RBAC, container escapes, etcd, supply chain — added in #394).
reconnaissance/ placeholder.
custom/ source_aware_whitebox (whitebox coordination), source_aware_sast (triage).
scan_modes/ quick, standard, deep.

Loading: skills passed to LLMConfig(skills=[...]) are rendered into the system prompt via the Jinja get_skill(name) macro (llm/llm.py:96). Whitebox automatically pulls in coordination/source_aware_whitebox + custom/source_aware_sast. Max 5 skills per agent (per skills/README.md). Mid-run skill loading via the load_skill tool replaces the {{DYNAMIC_SKILLS_DESCRIPTION}} placeholder.

Recent additions:

  • NoSQL injection guide (#168) — see §10.2.
  • Kubernetes security testing (#394).

12. Config

strix/config/config.py — hand-rolled Config class (no Pydantic). All knobs are class attributes; Config.get(name) resolves os.environ[name.upper()] first, then the default.

Knob Default Purpose
strix_llm None Model string; required.
llm_api_key None Provider API key.
llm_api_base / openai_api_base / litellm_base_url / ollama_api_base None Base URL fallbacks (resolved in priority order).
strix_reasoning_effort "high" low/medium/high.
strix_llm_max_retries "5" LLM retry count.
strix_memory_compressor_timeout "30" Compressor LLM timeout.
llm_timeout "300" Outer LLM timeout.
perplexity_api_key None Web search.
strix_disable_browser "false" Skip browser tool registration.
strix_image "ghcr.io/usestrix/strix-sandbox:0.1.13" Sandbox image pin.
strix_runtime_backend "docker" Only docker supported.
strix_sandbox_execution_timeout "120" Tool exec timeout (s).
strix_sandbox_connect_timeout "10" Tool server connect timeout.
strix_telemetry "1" Master telemetry switch.
strix_otel_telemetry / strix_posthog_telemetry None Per-stream override.
traceloop_base_url / traceloop_api_key / traceloop_headers None OTel/Traceloop endpoint config.

12.1 Layering

Order (highest first): os.environ → class default. Config files apply by writing into os.environ.

Two file locations:

  • ~/.strix/cli-config.json (default) auto-applied at Config.apply_saved().
  • --config <path> (CLI flag) overrides via apply_config_override() (interface/main.py:531-539).

The --config override fix (commit 9fb1012):

  • Track applied vars in Config._applied_from_default: ClassVar[dict] (config.py:59-61).
  • On override, clear those tracked vars from os.environ first, then load the custom file.
  • Test: tests/test_config_override.py validates the leak doesn't happen.

12.2 LLM Config Resolution

resolve_llm_config() (config.py:199-224): if model starts with strix/, force api_base = STRIX_API_BASE; else cascade through llm_api_baseopenai_api_baselitellm_base_urlollama_api_base.

12.3 Telemetry Opt-Out

  • STRIX_TELEMETRY=0 kills both streams.
  • STRIX_OTEL_TELEMETRY=0 kills OTel only.
  • STRIX_POSTHOG_TELEMETRY=0 kills PostHog only.
  • Checked via strix/telemetry/flags.py.

13. Telemetry & Persistence

13.1 Tracer (strix/telemetry/tracer.py)

Holds run_id, start_time, agents map, tool_executions, chat_messages, vulnerability_reports, scan_results, run_metadata. Emits structured events into events.jsonl (thread-safe with _get_events_write_lock) and OTel spans.

Event types include: run.started, run.configured, agent.created, agent.status.updated, tool.execution.started, tool.execution.updated, chat.message, finding.created.

13.2 OpenTelemetry / Traceloop

bootstrap_otel() wires an OTLP HTTP exporter (opentelemetry-exporter-otlp-proto-http). If Traceloop SDK is installed, spans also stream to TRACELOOP_BASE_URL with TRACELOOP_API_KEY headers. TRACELOOP_HEADERS (JSON string) allows custom headers. Graceful degradation if Traceloop SDK is missing.

13.3 Scrubadub PII Redaction (strix/telemetry/utils.py:67-150)

TelemetrySanitizer:

  • Recurses dict/list/tuple structures.
  • _SCREENSHOT_KEY_PATTERN: keys matching screenshot[SCREENSHOT_OMITTED].
  • _SENSITIVE_KEY_PATTERN: api[-_]?key|token|secret|password|...[REDACTED].
  • _SENSITIVE_TOKEN_PATTERN: bearer tokens, GitHub ghp_/ghs_, Slack xox*.
  • Strings additionally run through scrubadub.Scrubber() for emails, IPs, phone numbers, names; placeholders {{...}} replaced with [REDACTED].

Applied on every event payload before write/export (tracer.py:159-160, 223-227).

13.4 PostHog (strix/telemetry/posthog.py)

Anonymous usage telemetry. Per-process _SESSION_ID = uuid4().hex[:16] — no user identifier. Public API key embedded; events go to https://us.i.posthog.com/capture/. Events include: scan start/end, finding reported, error. Payload metadata: OS, arch, Python version, Strix version, scan mode, finding count, LLM tokens.

Disabled by STRIX_POSTHOG_TELEMETRY=0 or STRIX_TELEMETRY=0.


14. Cross-Cutting Design Decisions

Decision Rationale Tradeoff
XML tool calls in text, not native function-calling Multi-provider compatibility, streaming truncation control, client-side parsing, partial-tag recovery. Tool descriptions re-injected as text every call (no schema cache); custom parser to maintain.
Thread-based subagents (daemon threads, own event loops) Simple parent-child coordination, shared sandbox, true asyncio.create_task not enough because some tools are blocking. GIL-bound; no real CPU parallelism; daemon threads die on process exit (acceptable for CLI).
Direct dict messaging, no broker Simple, fast, in-process. Single-process only; no durability; zero distribution.
One container per scan, not per agent Shared /workspace + Caido capture is the common case for security work; less Docker overhead. No per-agent isolation — a buggy/exploited tool can affect other agents in the same scan.
No CPU/memory/network limits on sandbox container Pentest tools (nmap, etc.) need raw socket access and can be heavy. NET_ADMIN+NET_RAW capabilities granted. Container could exhaust host resources or DoS targets if the agent goes wrong. Operator's responsibility.
--disable-web-security on Chromium Required for XSS / CORS testing. Browser isn't a realistic UA mirror — can yield findings that don't repro in a normal browser.
System-wide proxy via /etc/profile.d/proxy.sh + CA installed in NSS + system trust Caido captures all HTTP/HTTPS from the container by default — agents don't need to configure proxies per-tool. Anything in the container talking off-box is observable to Caido; don't run untrusted secrets through there.
Custom PS1 for tmux exit-code extraction Reliable exit code detection across arbitrary shells ([STRIX_$?]$ → regex). Breaks if user-supplied scripts mess with PS1.
Memory compressor summarizes via separate LLM call Preserves operationally-critical findings instead of dropping them. Extra cost per compression cycle; compression timeout 120 s can stall the loop.
Ephemeral Anthropic prompt cache only Simple — system prompt cache resets per request. Lost cache benefit between calls if conversation history mutates (which it always does).
Subagent stats finalized into a global dict on exit Avoids race conditions when the root queries during child execution. Required for accurate root-level totals. Slight complexity in _finalize_agent_llm_stats.
Tool result truncation at 10 KB head/tail Keeps context spend bounded. Loses information; the model sometimes can't see the part it needs.
Vulnerability dedup via LLM similarity Catches semantic duplicates that hash-based dedup would miss. Costs another LLM call per finding submission.
Hard tool-call requirement enforced by prompt Models prone to outputting "Planning..." with no tool call, which the loop interprets as "wait for user" and halts. Strong language in system prompt; need think as escape hatch.
Diff scope as prompt-injected metadata, not filesystem filter Agent can still read related files (imports, helpers) when investigating diffed code. Larger context spend; relies on model self-restraint to actually focus on the diff.
Skills are Markdown files, not Python plugins Lower contributor friction, no import system to maintain, easy to ship. No programmatic logic in skills — they're always pure prompt content.

15. Recent Evolution (Notable Commits)

Commit Subject What Changed
9fb1012 --config flag now fully overrides ~/.strix/cli-config.json (#457) Track default-applied vars in Config._applied_from_default; clear them before loading custom config. New test in tests/test_config_override.py.
60abc09 wrap acompletion in asyncio.wait_for to prevent indefinite hangs (#453) Wrap both acompletion() and per-chunk __anext__() in asyncio.wait_for. Bedrock TCP-accept-then-silence stalls now raise TimeoutError (status_code=None) → retried via _should_retry.
8841294 feat(skills): add Kubernetes security testing skill (#394) New strix/skills/cloud/kubernetes.md. RBAC, container escapes, etcd, supply chain.
5c13348 feat: Add NoSQL injection vulnerability guide (#168) New strix/prompts/vulnerabilities/nosql_injection.jinja covering Mongo/Couch/Redis/Cassandra/Neo4j/GraphQL.
15c9571 fix: ensure LLM stats tracking is accurate by including completed subagents (#441) _finalize_agent_llm_stats() snapshots subagent stats into _completed_agent_llm_totals under lock; tracer aggregates live + completed. Root no longer undercounts.
62e9af3 Add Strix GitHub Actions integration tip README addition only.
38b2700 feat: Migrate from Poetry to uv (#379) Build system now hatchling; deps managed by uv; lockfile uv.lock.
e78c931 feat: Better source-aware testing (#391) Whitebox skill set hardened (coordination/source_aware_whitebox, custom/source_aware_sast).
4934bb8 chore: upgrade litellm and sanitize tool result text Bump litellm to >=1.81.1,<1.82.0; tool result sanitization (screenshot extraction, length truncation, error truncation).
7d5a45d chore: bump version to 0.8.3 PyPI version bump.
dec2c47 fix: use anthropic model in anthropic provider docs example Doc only.
4f90a56 fix: strengthen tool-call requirement in interactive and autonomous modes Hardens system_prompt.jinja:24-44. Explicit "message without tool call IMMEDIATELY STOPS execution" and named exceptions. Adds the think tool escape hatch.
640bd67 chore: bump sandbox image to 0.1.13 strix/config/config.py:43.

16. Quick File Index

Path Role
strix/interface/main.py CLI entrypoint.
strix/interface/cli.py Headless mode.
strix/interface/tui.py Textual TUI app.
strix/interface/utils.py Run-name, target inference, diff-scope helpers.
strix/agents/base_agent.py Core agent loop.
strix/agents/state.py AgentState model.
strix/agents/StrixAgent/strix_agent.py Root agent + execute_scan.
strix/agents/StrixAgent/system_prompt.jinja System prompt.
strix/llm/llm.py LLM wrapper.
strix/llm/config.py LLMConfig.
strix/llm/memory_compressor.py History compaction.
strix/llm/utils.py Tool-format normalization, XML parser.
strix/llm/dedupe.py Vulnerability dedup.
strix/tools/registry.py Tool registry + decorator.
strix/tools/executor.py Local + sandbox dispatcher.
strix/tools/context.py Agent ID contextvar.
strix/tools/argument_parser.py XML arg type coercion.
strix/tools/agents_graph/agents_graph_actions.py Multi-agent orchestration.
strix/tools/finish/finish_actions.py finish_scan (root only).
strix/tools/browser/ Playwright Chromium tool.
strix/tools/terminal/ tmux/libtmux tool.
strix/tools/python/ IPython tool.
strix/tools/proxy/ Caido GraphQL client.
strix/tools/notes/ Notes + wiki.
strix/tools/todo/ In-memory todos.
strix/tools/reporting/ Vulnerability reports + CVSS.
strix/tools/web_search/ Perplexity.
strix/tools/file_edit/ openhands-aci editor.
strix/tools/thinking/ think tool.
strix/tools/load_skill/ Runtime skill injection.
strix/runtime/docker_runtime.py Host-side container orchestration.
strix/runtime/tool_server.py Sandbox-side FastAPI tool server.
containers/Dockerfile Sandbox image.
containers/docker-entrypoint.sh Container boot sequence.
strix/config/config.py Config + env layering.
strix/telemetry/tracer.py Run tracer + JSONL events.
strix/telemetry/utils.py Scrubadub redaction.
strix/telemetry/posthog.py Anonymous usage telemetry.
strix/telemetry/flags.py Telemetry opt-out resolution.
strix/utils/resource_paths.py Frozen-vs-dev path resolution.
strix/skills/**/*.md Vulnerability + tooling + scan-mode playbooks.
strix/prompts/vulnerabilities/nosql_injection.jinja NoSQLi prompt.
pyproject.toml Deps, entry point, lint config.

This wiki captures the harness as of 9fb1012. When in doubt, source wins.