Picks up the recent in-image deps (``pip install caido-sdk-client`` for ``python_action`` + Caido CLI bumped to v0.56.0). 0.2.0 is the new minor since this is the first SDK-migration-era image; users pulling the new strix should pull the matching new image. Updated: - ``strix/config/settings.py:64`` — ``RuntimeSettings.image`` default - ``strix/runtime/session_manager.py`` + ``strix/orchestration/scan.py`` — docstring example - ``HARNESS_WIKI.md`` — three references in the runtime + config docs - ``MIGRATION_EVALUATION.md`` — the SDK-bridging note The historical changelog row (``HARNESS_WIKI.md:744`` — "bump to 0.1.13") stays untouched on purpose; it records what commit ``640bd67`` did, not the current pin. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
60 KiB
Strix Harness — Internal Architecture Wiki
Internal deep-dive for the team. Maps every subsystem, every tool, every architectural decision, with
path:linereferences throughout.Generated against
mainat9fb1012(fix: --config flag now fully overrides ~/.strix/cli-config.json). When code drifts, prefergit logand the source over this document.
Table of Contents
- What Strix Is
- Architecture at a Glance
- Repository Layout
- Lifecycle of One Run
- Agent System
- LLM Layer
- Tool System
- Runtime & Sandbox
- Interface (CLI / TUI / Headless)
- Prompts
- Skills
- Config
- Telemetry & Persistence
- Cross-Cutting Design Decisions
- Recent Evolution (Notable Commits)
- 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:
- Host ↔ sandbox: HTTP/JSON over Bearer-token auth. Host owns LLM + telemetry; sandbox owns dangerous tools.
- 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:
- CLI parse (
interface/main.py:267-426): parse args, validate, infer target types viainfer_target_type()(interface/utils.py). Resolve diff scope if--scope-mode=diff. - Config layering (
config/config.py): apply~/.strix/cli-config.json(or--config <path>override) intoos.environ; resolveSTRIX_LLM,LLM_API_KEYviaresolve_llm_config()atconfig/config.py:199-224. - LLM warm: validate model reachable.
- Docker pull if needed; image pin
ghcr.io/usestrix/strix-sandbox:0.2.0(strix/config/settings.py:64). - Run name + run dir:
strix_runs/<run-name>/. Tracer init (telemetry/tracer.py:50+). - Mode dispatch: TUI (
tui.py) or CLI (cli.py). Both end up callingStrixAgent.execute_scan(scan_config). - Sandbox launch (
runtime/docker_runtime.py:111-173): container created with namestrix-scan-{scan_id}, two random host ports mapped to container ports48080(Caido) and48081(tool server), 32-byte bearer token generated,local_sourcestar-copied into/workspace. - Container boot (
containers/docker-entrypoint.sh): start Caido → fetch GraphQL token vialoginAsGuest→ create temp project → install CA cert into NSS + system trust → set system-wide proxy env vars → spawn tool server aspentesteruser → wait for/healthready. - Agent loop (
agents/base_agent.py:152-260): see §5. - Termination: root agent calls
finish_scan(tools/finish/finish_actions.py) when work is done; tracer writes final reportpenetration_test_report.mdand per-finding JSONs undervulnerabilities/. - Cleanup:
docker rm -fasync-spawned (runtime/docker_runtime.py:334-352). - Exit code:
0clean,2if vulns found in headless mode (percli.py).
5. Agent System
5.1 Single-Agent Loop
agents/base_agent.py:152-260 (agent_loop). Each iteration:
_initialize_sandbox_and_state()once at start (base_agent.py:158).- 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. - Iteration counter bump and warning watchdog: at 85% of
max_iterations(default 300) emit warning; atmax-3emit critical "next message MUST be finish" warning (base_agent.py:186-211). _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.
- 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 =delegationormessage._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.
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) setsstate.request_stop()+ callsagent_instance.cancel_current_execution()(base_agent.py:615-623) which cancels the running asyncio task.asyncio.CancelledErroris 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-218 — LLM.generate() is an async generator yielding LLMResponse objects. Pipeline:
- Retry loop (
:162-171): maxSTRIX_LLM_MAX_RETRIES(default 5) attempts. Backoffmin(90, 2 * 2**attempt)seconds._should_retry()(:326-330) is True for network errors (no status_code) orlitellm._should_retry(code)for HTTP statuses. - 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). - Stream (
_stream,:173-218):acompletion(...)wrapped inasyncio.wait_for(timeout=self.config.timeout)— commit60abc09.- Each
await it.__anext__()also wrapped inasyncio.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, setdone_streaming=1and continue 5 more chunks for trailers, then stop.
- Stats (
_update_usage_stats,:287-312): extractresponse.usage(input, completion, cached tokens). Cost via_extract_cost()(:314-324) — prefersresponse.usage.cost, elselitellm.completion_cost(...)with provider stripped. - Tool parsing (
:212-217):normalize_tool_format→fix_incomplete_tool_call→parse_tool_invocations(all inllm/utils.py).
6.2 Provider Routing
strix/llm/utils.py:34-61 defines STRIX_MODEL_MAP — strix/<short> aliases (e.g. strix/claude-sonnet-4.6) resolve to a tuple (api_model, canonical_model):
api_modelis what gets passed toacompletion(typically OpenAI-compatible against the Strix proxyhttps://models.strix.ai/api/v1set atconfig/config.py:8).canonical_modelis what's used for litellm capability checks likesupports_prompt_caching()andsupports_reasoning().
For non-strix/ prefixes, the model string is passed straight through. LLMConfig.__init__ (llm/config.py:8-41) reads LLM_API_BASE → OPENAI_API_BASE → LITELLM_BASE_URL → OLLAMA_API_BASE from env in priority order.
Per-provider quirks:
- Anthropic:
_is_anthropic()(llm.py:338-341) detectsanthropic/orclaudesubstrings and adds an ephemeral cache control block to the system message via_add_cache_control()(:371-387). - OpenAI reasoning: if
supports_reasoning()is true, setreasoning_effort(:265-266) — envSTRIX_REASONING_EFFORT> config > scan-mode default (mediumfor quick,highotherwise). - 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 indocs/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 oflen(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):
- Render system prompt from Jinja template (
agents/StrixAgent/system_prompt.jinja) with:interactiveflag,system_prompt_context(authorized targets),loaded_skill_names, tools prompt. - Insert agent-identity user message (hidden control block) — see §5.3.
- Run
MemoryCompressor.compress_history()over conversation. - If last message is assistant, append a
<meta>Continue the task.</meta>continuation prompt (autonomous mode). - 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, onlysandbox_execution=Truetools register. IfSTRIX_DISABLE_BROWSER=true, browser tools are skipped. IfPERPLEXITY_API_KEYis missing,web_searchis skipped. - Schema: each tool group has
<group>_actions_schema.xmlnext 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 tohttp://{host}:{tool_server_port}/executewith{agent_id, tool_name, kwargs}andAuthorization: Bearer {token}. Connect timeoutSTRIX_SANDBOX_CONNECT_TIMEOUT=10, request timeoutSTRIX_SANDBOX_EXECUTION_TIMEOUT + 30 = 150s. - Local path (
_execute_tool_locally,:101-115): look up function, coerce arg types viaargument_parser.convert_arguments(), injectagent_stateif the function requests it (introspection over signature),awaitif 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 anyscreenshotkey 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, returnshould_agent_finishboolean.
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.56.0 pinned in containers/Dockerfile (override via --build-arg CAIDO_VERSION=...).
Notes (strix/tools/notes/) — create_note, list_notes, get_note, update_note, delete_note
Pure in-memory dict, shared across every agent in the same scan for the lifetime of the process. Not persisted — process exit clears the lot.
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.
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.
Web Search (strix/tools/web_search/) — web_search
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:
- Screenshot extraction (
executor.py:345-353): if a result dict has keyscreenshotwhose value is a base64 string, pull it out into a separate image attachment, replace its dict value with"[Image data extracted - see attached image]". - Length truncation (
:246-249): >10 KB results are split head + truncation marker + tail, 4 KB each side. - Error truncation (
:182-183): error strings capped at 500 chars with[truncated]suffix. - Telemetry sanitization (
telemetry/utils.py:67-150): scrubadub + regex on dict/list/tuple keys/values, redactingscreenshot, sensitive-key patterns (api[-_]?key|token|secret|password|...), and bearer-style tokens (ghp_,ghs_,xox*). - 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
pentesteruser 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.56.0); Go toolshttpx,katana,gospider,interactsh; Pythonarjun,dirsearch,wafw00f,semgrep,bandit,trufflehog; JSretire,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
/workspaceowned by pentester (line 194). - Ports
48080(Caido),48081(tool server) exposed. - Image pin:
ghcr.io/usestrix/strix-sandbox:0.2.0(strix/config/settings.py:64).
8.2 Boot Sequence (containers/docker-entrypoint.sh)
- Caido start (lines 12-17):
caido-cli --listen 0.0.0.0:48080 --allow-guests --no-logging --import-ca-cert /app/certs/ca.p12. - Caido readiness poll (24-38): GET
/graphql/until 200/400, 30 attempts × 1 s. - Login (50-74): GraphQL
loginAsGuestmutation → bearer token, 5 retries with backoff. Exported asCAIDO_API_TOKEN. - Project setup (79-109): create + select temp Caido project for capture.
- System-wide proxy (113-146): write
/etc/profile.d/proxy.shsettinghttp_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. - Tool server start (154-180):
sudo -u pentester python -m strix.runtime.tool_serverwith token, port, timeout. Wait for/health200, 10 retries × 1 s. - Ready (182): emit "✅ Container ready" and
execthe trailing args (typicallysleep 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 byagent_idcontextvar). - 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): namestrix-scan-{scan_id}, labelstrix-scan-id={scan_id}, capabilitiesNET_ADMIN | NET_RAW,extra_hosts={"host.docker.internal": "host-gateway"}, env passthrough includingTOOL_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/healthfor 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 fromdocker inspectenv. - Cleanup (
:322-352):docker stop+docker rmspawned 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). Setscurrent_agent_idcontextvar, looks up registry, calls function viaasyncio.to_thread(). Per-agent task tracking inagent_tasks: dict[agent_id, asyncio.Task](:39) — a new request for the same agent cancels the previous task (:94-97). Hard timeoutasyncio.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, auto→diff. |
--diff-base |
Branch/commit to diff against (e.g. origin/main). Auto-detected if missing. |
--config |
Path to custom cli-config.json — fully 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 + orchestration/scan.py. Contents:
events.jsonl— every span/event in append-only JSONL (thread-safe writes).vulnerabilities/vuln_{id}.md— one file per finding, sorted by severity, dedup-checked.vulnerabilities.csv— index of every finding for spreadsheet consumption.vulnerabilities.json— machine-readable mirror, used byTracer.hydrate_from_run_dirto repopulate vuln state on resume so id allocation doesn't collide.penetration_test_report.md— final markdown report.session.db— SDKSQLiteSessionfor the root agent's conversation history.sessions/{child_id}.db— per-subagentSQLiteSession, one file per spawned child.bus.json— atomic snapshot of the orchestration bus (topology, statuses, inboxes, per-agent stats, per-agent metadata{task, skills, is_whitebox, scan_mode, diff_scope}). Written after everybus.register/finalize/park/mark_llm_failed, plus a final dump at scan teardown..lock— advisoryflock-style file lock; prevents twostrixprocesses from running on the samescan_idconcurrently.strix.log— per-scan log file (DEBUG to file, ERROR to stderr).<target_subdir>/— local source clones, per-target.
9.7 Resume
Resume is always on: presence of bus.json triggers it. Fresh runs simply have no bus.json to begin with. The CLI exposes --resume <run_name> as the canonical way to opt back into an existing run.
What survives a process restart with the same scan_id:
- Root agent's full LLM conversation (replayed by SDK from
session.db). - Every non-terminal subagent's full LLM conversation (replayed from
sessions/{child_id}.db). - Bus topology:
parent_of,statuses,names,stats_live,stats_completed, pendinginboxes,metadata(task + skills per agent). - Vulnerability reports (hydrated from
vulnerabilities.json). - Run log (appended to existing
strix.log).
What does not survive:
- The sandbox container itself — fresh container per process. Files agents wrote under
/workspace/scratch/or scanner outputs to/workspace/.strix-source-aware/are lost./workspace/sourcesre-mounts from the host so source code is unchanged. - Caido proxy state (request log, scopes, replay sessions).
On resume, every subagent in running / waiting / llm_failed status is respawned via _respawn_subagents with initial_input=[]; the SDK's session replay drives them from where they stopped. Terminal-status agents (completed / crashed / stopped) are left alone but their stats remain in stats_completed for the TUI footer. Per-child failure (corrupt session DB, missing skill module) finalizes that child as crashed and continues with the rest.
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:
- Core capabilities (4-9): security assessment, validation, remediation.
- Communication rules (11-44): CLI markdown only; never echo control XML (
inter_agent_message,agent_completion_report,agent_identity). - 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
thinktool is the planning escape hatch. - Autonomous: minimize chatter, never empty messages, use
wait_for_messageor finish when idle.
- 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
- 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.
- 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 getscoordination/source_aware_whitebox+custom/source_aware_sast). system_prompt_context.authorized_targets(only set on root).is_whiteboxflag (selects the whitebox skill stack).
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.2.0" |
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 atConfig.apply_saved().--config <path>(CLI flag) overrides viaapply_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.environfirst, then load the custom file. - Test:
tests/test_config_override.pyvalidates 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_base → openai_api_base → litellm_base_url → ollama_api_base.
12.3 Telemetry Opt-Out
STRIX_TELEMETRY=0kills both streams.STRIX_OTEL_TELEMETRY=0kills OTel only.STRIX_POSTHOG_TELEMETRY=0kills 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 matchingscreenshot→[SCREENSHOT_OMITTED]._SENSITIVE_KEY_PATTERN:api[-_]?key|token|secret|password|...→[REDACTED]._SENSITIVE_TOKEN_PATTERN: bearer tokens, GitHubghp_/ghs_, Slackxox*.- 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/ |
In-memory notes (shared across agents in a run). |
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.