Skip to content

Strix Agent: Technical Implementation Comparison

Read of usestrix/strix@HEAD (Apache-2.0, ~18.7k LOC Python) on 2026-05-23, mapped against 0sec-labs/0sec@research/strix-comparison. Every claim below is anchored to a strix/<path>:<line> or 0sec packages/<path>:<line> reference; sections marked “not observed in public repo” are exactly that.

  1. Strix has no programmatic verification layer. “Validation” is just another agent role spawned via create_agent and bound by prompt to “prove it’s real with PoC” — there is no separate replay runner, no oracle, no signature_matched style verdict. Compare to 0sec’s verify/replay-runner.ts (726 lines, structured expect predicates) and verify/kernel-verify.ts (690 lines, oracle-driven). This is the most surprising finding in the repo: a 25k-star agent ships zero out-of-band verification.
  2. Strix’s “Docker isolation” is a single shared container per scan, not per-agent. Every sub-agent in a scan hits the same strix-scan-<scan_id> container over HTTP (strix/runtime/docker_runtime.py:142, strix/agents/StrixAgent/system_prompt.jinja:233). The system prompt literally tells the model: “All agents run in the same shared Docker container for efficiency.” No gVisor, no microVM, no per-agent kernel. 0sec’s equivalent (packages/core/src/agent/docker-executor.ts:1-50) is the same shape — shared kernel — but 0sec at least scopes one container per scan with cleanup; both projects rely on docker run --rm as the only boundary.
  3. Tool calls are XML, parsed with regex. Strix prompts the model to emit <function=name><parameter=k>v</parameter></function> and parses it by hand in strix/llm/utils.py (parse_tool_invocations, fix_incomplete_tool_call, _truncate_to_first_function). No JSON-schema tool calling, no provider-native tool API. 0sec added an XML dispatch path for the same reason (cheap models emit malformed JSON — packages/core/src/agent/xml-dispatch.ts, agent/loop.ts:59-66) but keeps JSON tool-calling as the default. Strix has committed to string parsing as the only path.
  4. The system prompt is the architecture. strix/agents/StrixAgent/system_prompt.jinja is 509 lines of behavioral law: refusal-avoidance, scope-locking, mandatory phase ordering, hard-coded vulnerability priority list, multi-agent workflow diagrams, agent-spawn rules. There is no planner module, no FSM, no orchestrator graph beyond what the LLM does after reading the prompt. The whole system is “one model + giant prompt + recursive create_agent.”
  5. No CI test suite for the agent loop. .github/workflows/build-release.yml is the only workflow, and it builds the PyInstaller binary — no pytest run, no eval suite, no benchmark in CI. The tests/ tree exists (~1.9k LOC pytest, mostly unit tests of tool registration and Docker runtime mocks) but nothing runs it on PR. 0sec ships 30+ workflows including journal-ablation.yml, npm-bench.yml, xbow slices, and a paid eval gate.

strix/agents/base_agent.BaseAgent.agent_loop (base_agent.py:152-259) is a single while True with these checks per iteration:

  1. _check_agent_messages — drain the inter-agent inbox (agents_graph_actions._agent_messages[agent_id]).
  2. Waiting / stop / llm_failed gates.
  3. state.increment_iteration(); inject “approaching max iterations” warning at 85% (state.is_approaching_max_iterations), inject “CRITICAL: 3 iterations left” warning at max_iterations - 3.
  4. _process_iteration → stream LLM response, accumulate, regex-truncate at the first </function> outside any <thinking> block (llm/llm.py:35-42, 216-223), then parse_tool_invocations (llm/utils.py).
  5. _execute_actionsprocess_tool_invocations (tools/executor.py:313-342) executes every action in sequence, appends a <tool_result> XML observation to the conversation.
  6. finish_scan / agent_finish tools set should_agent_finish = True and exit the loop.

max_iterations = 300 is the default (base_agent.py:50), high relative to 0sec’s 40-turn shellPentest default. There is no early-stop, no oscillation detection, no reflection checkpoint — the only safety net is the budget warning at 85%.

There is none. The “root agent” is just a StrixAgent instance whose system-prompt skill coordination/root_agent.md reminds it: “The root agent’s primary job is orchestration, not hands-on testing” (strix/skills/coordination/root_agent.md). Whether the model obeys is up to the model. There is no enforcement code that prevents the root from calling terminal_execute.

create_agent (tools/agents_graph/agents_graph_actions.py:384) spawns sub-agents as Python threading.Threads (_running_agents: dict[str, threading.Thread], agents_graph_actions.py:18). Sub-agents communicate by appending to a shared _agent_messages dict; the parent loop polls it each iteration via _check_agent_messages. There is no asyncio coordination across agents — asyncio is used inside each agent for I/O, threads for cross-agent. Sub-agent results are surfaced by writing into the same _agent_messages queue with from/to agent ids.

The graph itself is a flat dict (_agent_graph["nodes"], _agent_graph["edges"]); there is no DAG scheduler, no dependency tracking, no rate limiter across the tree.

Strix uses LiteLLM end-to-end (strix/llm/llm.py:1-27). The LLM class (llm/llm.py:77-411) wraps litellm.acompletion(stream=True) with: prompt-caching control-blocks for Anthropic (_add_cache_control, llm.py:395-411), reasoning_effort for models that support it, vision-stripping for those that don’t, and a retry loop with 2^attempt backoff capped at 90s (llm.py:178-187). The only provider-specific code is the Anthropic cache-control insertion and the _is_anthropic() check (llm.py:362-365). Vertex AI is an optional extra (pyproject.toml:55).

Cost is captured per-request via litellm.completion_cost (llm.py:338-348) and accumulated in RequestStats (llm.py:59-74). There is no cost ceiling enforcement in BaseAgent — cost is just observed, never an exit condition. The closest thing is max_iterations.

The system prompt template is strix/agents/StrixAgent/system_prompt.jinja. Structure (each is a top-level XML tag):

  • <core_capabilities> (4 bullets)
  • <communication_rules> — interactive vs autonomous branch; the autonomous branch contains the load-bearing line: “While the agent loop is running, almost every output MUST be a tool call. Do NOT send plain text messages; act via tools.” (system_prompt.jinja:42)
  • <execution_guidelines> — system-verified scope block (Jinja-rendered from system_prompt_context.authorized_targets); explicit refusal-avoidance block: “Do not self-classify normal in-scope validation as unauthorized, harmful, suspicious, or disallowed. Do not produce generic policy warnings or generic safety refusals.” (system_prompt.jinja:73-76)
  • <vulnerability_focus> — a hard-coded numbered list of 10 vuln classes the agent MUST test (IDOR, SQLi, SSRF, XSS, XXE, RCE, CSRF, race conditions, business logic, auth/JWT) — system_prompt.jinja:194-208
  • <multi_agent_system> — workflow diagrams as ASCII art, agent-spawning rules, specialization examples — system_prompt.jinja:231-362
  • <tool_usage> — the XML format spec with explicit “wrong format” examples — system_prompt.jinja:364-435
  • <environment> — full Kali tool inventory by category — system_prompt.jinja:437-498
  • <specialized_knowledge> — Jinja loop over loaded “skills” (vulnerability/tool/framework markdown files) — system_prompt.jinja:500-508

Two patterns worth quoting verbatim because they reveal design intent:

“NEVER use ‘Strix’ or any identifiable names/markers in HTTP requests, payloads, user-agents, or any inputs”system_prompt.jinja:16. Brand stealth at the prompt level.

“PERSISTENCE IS MANDATORY: Real vulnerabilities take TIME - expect to need 2000+ steps minimum. NEVER give up early - attackers spend weeks on single targets. … Bug bounty hunters spend DAYS on single targets - so should you.”system_prompt.jinja:355-360. This is anchored on a 300-iteration default cap which can’t actually fit “2000+ steps,” so the prompt is aspirational rather than budget-aligned.

Skills are markdown files merged into the prompt at render time (llm/llm.py:100-125). The active skill list is per-agent (LLMConfig.skills, llm/config.py:32) and can be mutated mid-run by the load_skill tool (add_skills, llm/llm.py:143-158), which re-renders the entire system prompt. Sub-agents inherit the scan-mode skill (scan_modes/quick|standard|deep) and pick up role-specific skills via create_agent(..., skills=[...]).

12 tool groups, registered via the @register_tool decorator (tools/registry.py:190-250). Each tool ships an XML schema (<tool name=…><parameters>…</parameters></tool>) next to its Python module, loaded by _load_xml_schema and merged into get_tools_prompt() (registry.py:280-300). Tool catalog:

GroupNotable toolsSandbox?
terminalterminal_execute, session managementyes
pythonpython_execute (persistent IPython kernel)yes
file_editstr_replace_editor (OpenHands ACI wrapper)yes
browserfull Playwright surfaceyes
proxyCaido CLI (already running in container)yes
terminal + proxyshared with all sub-agents in the same containeryes
noteslist_notes, get_note, set_note — shared “wiki” memory across agentsno
agents_graphcreate_agent, send_message, wait_for_message, agent_finishno
reportingcreate_vulnerability_report (CVSS-validated, dedupe-checked)no
todoper-agent todo listno
finishfinish_scan (root only)no
thinkingthink — no-op tool that returns the message backno
web_searchPerplexity sonar-reasoning-pro (requires PERPLEXITY_API_KEY)no
load_skillmutates the system prompt to inject more guidanceno

Validation: _validate_tool_arguments (tools/executor.py:130-153) checks required/unknown parameters against the XML schema before dispatch. Errors are returned as "Error: …" strings stuffed back into the next user message — there’s no structured tool_error channel.

Result formatting: all results round-trip as a wrapped XML string <tool_result><tool_name>…</tool_name><result>…</result></tool_result> (executor.py:251-256); results longer than 10kB are middle-truncated (executor.py:246-249). Screenshots are detected by a magic screenshot field on dict results and lifted into an image_url content block (executor.py:227-256, 345-364).

The “in-sandbox” tools are not actually called locally — _execute_tool_in_sandbox (tools/executor.py:39-99) HTTP POSTs to a tool_server running inside the container at port 48081 with a bearer token. The tool server is an independent FastAPI process (runtime/tool_server.py referenced from pyproject.toml:60-67 sandbox extras). Auth is a 256-bit secrets.token_urlsafe(32) minted at container creation (docker_runtime.py:162).

  • Fragile regex on streaming output: _truncate_to_first_function / fix_incomplete_tool_call / normalize_tool_format (llm/utils.py) all depend on the model emitting <function=…></function> correctly. The “wrong formats” section of the system prompt (system_prompt.jinja:393-402) is essentially a list of failure modes they’ve observed; the parser has to forgive <invoke name="…">, code-fenced calls, etc.
  • Global mutable state for the agent graph: _agent_graph, _agent_messages, _running_agents, _agent_instances, _agent_states are all module-level dicts (agents_graph_actions.py:11-30). Only one is lock-guarded (_agent_llm_stats_lock). Concurrent multi-scan in one process would race.
  • except Exception: pass in telemetry/cost paths (llm/llm.py:307, 335, 347, 370, 376, telemetry/posthog.py:32-33, 63-64). Real bugs there will silently degrade cost accounting.

No formal planner. Decomposition is delegated entirely to the LLM via two mechanisms:

  1. The root_agent skill prompt (strix/skills/coordination/root_agent.md) describes a four-bucket taxonomy (Reconnaissance / Vulnerability Assessment / Exploitation and Validation / Reporting) and a “Discovery → Validation → Reporting → Fix” delegation chain.

  2. The system-prompt workflow diagrams for black-box and white-box (system_prompt.jinja:298-318) prescribe exact spawn sequences:

    SQL Injection Agent finds vulnerability in login form
    ↓ create_agent("SQLi Validation Agent (Login Form)")
    ↓ create_agent("SQLi Reporting Agent (Login Form)") on success

There is no machine-checkable representation of “what’s been planned” — the prompt asserts state.context["plan"] should be populated but nothing in state.py reads it as a control signal. The todo tool (tools/todo/todo_actions.py, 568 LOC) is the closest thing to a tracked plan, but it’s still LLM-managed text.

Compare to 0sec’s agent/journal/orchestrator.ts:30-42, which has explicit prioritized routing rules (R1–R7) over a structured OrchestratorBrief, and agent/journal/specialists.ts which defines exactly four specialist roles with bounded scope slices and budgets.

strix/llm/memory_compressor.py is the whole story.

  • Trigger: when total tokens (system + history + reserved) exceed MAX_TOTAL_TOKENS * 0.9 = 90_000 (memory_compressor.py:12, 215).
  • Strategy: always keep the last MIN_RECENT_MESSAGES = 15 messages (memory_compressor.py:13, 205); chunk older messages into groups of 10 and replace each chunk with an LLM-generated summary (memory_compressor.py:218-224).
  • Summary model: the same model as the scan (Config.get("strix_llm")) called via raw litellm.completion — not the streaming LLM class, no retries (memory_compressor.py:107-131). If summarization fails, the chunk is dropped silently except for messages[0] (memory_compressor.py:131).
  • Prompt (memory_compressor.py:15-43): a “compress this for the next security agent” instruction that explicitly lists what to preserve (vulns, creds, tool outputs, dead ends).
  • Image handling: _handle_images keeps only the max_images = 3 most recent images, rewriting older ones to placeholder text (memory_compressor.py:134-149).

Critical gap vs 0sec: no journal. Conversation state lives in AgentState.messages (agents/state.py:33), a plain in-memory list[dict[str, Any]]. There is no append-only fsync’d log, no replay capability, no resume-after-crash. The Tracer (telemetry/tracer.py, 860 LOC) does write JSONL to disk for observability — but that’s a sink for UI rendering, not a source of truth the agent reads back. Compare:

  • 0sec agent/journal/writer.ts:77-100, 90-95loadJournal() rehydrates from an fsync’d journal.jsonl plus sidecar artifacts directory, both sync-by-runId and async-by-path overloads.
  • 0sec agent/journal/orchestrator.ts:122-130runOrchestrator accepts { resume: true } and picks up at the last fsynced entry.
  • Strix _initialize_sandbox_and_state (base_agent.py:331-366) — no resume path; if the process dies, the scan dies.

This is the section where Strix’s design philosophy diverges most sharply from 0sec’s.

Strix has no oracle. A finding is “verified” when:

  1. A discovery sub-agent calls create_agent(name="… Validation Agent").
  2. The validation sub-agent runs whatever it wants (likely terminal_execute / python_execute) and decides, in natural language, that it succeeded.
  3. The validation sub-agent calls create_agent(name="… Reporting Agent").
  4. The reporting sub-agent calls create_vulnerability_report (tools/reporting/reporting_actions.py:201-339).

create_vulnerability_report does enforce structural checks: required fields, CVSS XML parses to valid cvss>=3.2 vector, optional CVE/CWE regex (reporting_actions.py:9-152). But there is no replay: the tool doesn’t re-run the PoC, doesn’t fetch the URL, doesn’t verify the payload. poc_script_code is just text the model writes.

The one programmatic check is LLM-based deduplication: check_duplicate (llm/dedupe.py:142-213) calls the same LLM with an XML-formatted system prompt asking “is this a duplicate?” and parses the <dedupe_result> block back. On error, it falls open (not duplicate) so the report saves. Confidence is whatever the dedup model reports as a float — no calibration, no floor.

0sec equivalents:

Layer0sec
Replay-time oraclepackages/core/src/verify/replay-runner.ts:1-100runDeterministicReplay, assertionFromStepExpect, evaluateAssertion, structured expect predicates (exit-zero, http-status, body-contains, body-matches, file-exists).
Confidence floorspackages/core/src/agent/finding-confidence.ts:1-60 — hybrid: LLM self-report clamped UP by PoC-status floor (0.6 if pocSteps present, 0.8 if any verifiable expect).
Kernel finding verificationpackages/core/src/verify/kernel-verify.ts:1-100 — constrained loop with a single allowlisted tool (kernel_run), oracle-driven promotion (signature_matched → confirmed, confidence=1.0), explicit budget, separate from the main agent loop.
Flag validatorpackages/core/src/agent/flag-validator.ts — for CTF-style flag oracles.

The split is: 0sec treats verification as a separate runtime concern (not another LLM judging); Strix treats verification as another LLM in a different prompt costume.

  • tests/ is ~1.9k LOC pytest, organized by module (tests/agents/, tests/tools/, tests/runtime/, tests/telemetry/).
  • The biggest single test file is tests/telemetry/test_tracer.py (449 LOC).
  • tests/runtime/test_docker_runtime.py (87 LOC) mocks the Docker client; no real container spin-up in test.
  • tests/tools/test_tool_registration_modes.py (94 LOC) exercises the @register_tool decorator’s flag handling.
  • No agent-loop integration test. tests/agents/__init__.py exists but the directory is otherwise empty.
  • No eval / benchmark suite checked into the repo. There is a benchmarks/ directory at repo root but it contains a stub README only.
  • conftest.py (top-level) is empty-init style.

CI: the only workflow is .github/workflows/build-release.yml — matrix-builds the PyInstaller bundle on macOS/Linux/Windows and uploads to a GitHub Release on v* tags. pytest is never invoked in CI. No coverage, no lint gate visible in CI (though ruff/mypy/bandit are in dev deps).

Compare 0sec .github/workflows/: 30+ workflows including journal-ablation.yml, xbow-*, npm-bench.yml, htb-bench.yml, argus-bench.yml, release-smoke.yml, docker-kali-publish.yml, kernel-validator-batch.yml.

Isolation (the HN “Docker containers” claim)

Section titled “Isolation (the HN “Docker containers” claim)”

Confirmed from strix/runtime/docker_runtime.py:

  • One container per scan, named strix-scan-<scan_id> (docker_runtime.py:143).
  • Image is Config.get("strix_image") (default per containers/Dockerfile: kalilinux/kali-rolling:latest).
  • cap_add=["NET_ADMIN", "NET_RAW"] (docker_runtime.py:175) — broader than default. No --security-opt, no seccomp profile, no AppArmor/SELinux profile passed, no user-namespacing config.
  • Container runs sleep infinity; commands enter via container.exec_run for setup and via the in-container tool-server HTTP API for agent tool calls.
  • All sub-agents share this container (system_prompt.jinja:233-236 makes this explicit to the model).

Isolation boundary is the Linux kernel of the host. This is the standard Docker shared-kernel model — fine for “don’t pollute my filesystem,” not fine for “I’m running untrusted RCE payloads against my workstation kernel.” There is no gVisor, no Firecracker-like microVM, no Kata. The HN comment that Strix runs in “Docker containers” is literally true and is the only isolation layer.

For comparison, 0sec’s agent/docker-executor.ts uses the same shared-kernel Docker model for shell-tool execution. Stronger isolation in 0sec shows up only in the kernel-verify path, which delegates to a separate runner — and per the public-copy rule, that’s described as “isolated kernel per scan” without implementation specifics.

  • LLM-call retries: 5 attempts default, 2^attempt backoff capped at 90s (llm/llm.py:178-187). Retry decision delegates to litellm._should_retry(status_code) (llm/llm.py:350-354), so retries fire only on HTTP errors with a retryable status. On final failure, LLMRequestFailedError is raised and the agent enters llm_failed waiting state (base_agent.py:568-601).
  • Tool-call retries: none. If process_tool_invocations raises, the error is added to the conversation as text (executor.py:178-186) and the loop continues — the model gets to decide whether to retry.
  • Sandbox initialization: 2 retries with exponential backoff (docker_runtime.py:151-209).

None enforced. RequestStats.cost accumulates (llm/llm.py:330-333) and is reported in telemetry and the agent graph (agents_graph_actions.py:60-70), but no check ever terminates the run on a $-threshold. The only termination conditions are max_iterations, finish_scan, stop_requested, and llm_failed.

Two layers:

  1. PostHog (strix/telemetry/posthog.py) — opt-in via STRIX_TELEMETRY=1 (telemetry/flags.py). Hard-coded public API key at posthog.py:15. Events: scan_started, finding_reported, scan_ended, error. Properties include model, scan_mode, vulnerability counts by severity, total cost, agent count.
  2. OpenTelemetry / Traceloop (strix/telemetry/tracer.py, telemetry/utils.py) — when STRIX_OTEL_ENABLED=1, exports spans for LLM calls and tool executions via OTLP HTTP. The tracer also writes a local JSONL run log to ~/.strix/runs/<run_id>/events.jsonl (referenced via _events_file_path, tracer.py:81).

A first-run anonymous-id file at ~/.strix/.seen (posthog.py:25-34) seeds a first_run: true event on first scan.

ConcernStrix0sec
Agent loop entrystrix/agents/base_agent.py:152 agent_loop (single while True, max_iterations=300)packages/core/src/agent/loop.ts:38 runAgentLoop (300 LOC, session-restore aware); plus native-loop.ts:1449 for the legacy native path
Loop dispatchimplicit (one agent class)not implemented — there is no loop-dispatch.ts and no journalLoop feature flag; agent/native-loop.ts is the single agent loop today (agent/journal/ holds the journal primitives only)
Plannernone (prompt-only)packages/core/src/agent/journal/orchestrator.ts:30-71 rule-based dispatcher with R1–R7 + pluggable decideNext hook for the FSM upgrade (#225)
Sub-agent modelPython threading.Thread per sub-agent, shared _agent_messages dict (tools/agents_graph/agents_graph_actions.py:18)per-role specialist registry; explicit budget per dispatch (journal/specialists.ts:1-50)
Tool callingXML, parsed via regex (llm/utils.py)JSON tool-calling default; XML dispatch fallback for cheap models (agent/xml-dispatch.ts, agent/loop.ts:59-66)
Tool catalog12 groups, schemas in XML files (tools/*/[name]_schema.xml)similar breadth in agent/tools/; kernel-run.ts is a single-purpose verification tool
Context compressionLLM-summarize chunks of 10 over 90% of 100k token cap (llm/memory_compressor.py:215-226)journal summarizer with hard token cap and OrchestratorWindowExceededError (journal/summarizer.ts:20, orchestrator.ts:104-112) — fails loud, doesn’t silently drop
Persistence / resumenone (in-memory state.messages)journal/writer.ts:90-100 loadJournal() with fsync’d JSONL + sidecar artifacts; orchestrator.ts:122-130 resume: true
Verificationsub-agent role + create_vulnerability_report shape checks (tools/reporting/reporting_actions.py:201)verify/replay-runner.ts:1-100 deterministic replay with structured expect predicates; verify/kernel-verify.ts:1-100 oracle-driven kernel verify; agent/finding-confidence.ts:1-60 PoC-status confidence floors
DeduplicationLLM-judge, XML parse, fail-open (llm/dedupe.py:142-213)not directly equivalent; 0sec uses confidence + finding-table dedupe upstream
Cost ceilingobserved only (llm/llm.py:330)PipelineOptions.costCeilingUsd (unified-pipeline.ts:62) plumbed through to the agent runner
Provider abstractionLiteLLM only, single LLM class (llm/llm.py:77)runtime registry (packages/core/src/runtime/registry.ts) with LlmApiRuntime, native, codex variants
Isolationone shared Docker container per scan; cap_add=NET_ADMIN,NET_RAW (runtime/docker_runtime.py:175)shared-kernel Docker for shell tools (agent/docker-executor.ts:1-50); isolated kernel per scan for kernel-verify path
TelemetryPostHog + optional OTel (telemetry/)structured event emitter + JSONL logs; CI-tied ablation pipelines
CIone workflow (build PyInstaller bundle)30+ workflows including weekly benchmark suites and journal-ablation A/B gate
Tests~1.9k LOC pytest, no CI runthousands of LOC vitest, run on every PR; eval harness in .github/workflows/scripts/

Ordered by ROI. “Borrow” = clear win, ship it. “Verify empirically” = sounds good in their prompt, but we don’t know if it actually moves their numbers, so A/B before adopting. “Avoid” = anti-pattern.

  1. Inject a hard “approaching max iterations” warning at 85% and a “CRITICAL: 3 left” warning at the tail. Strix base_agent.py:186-211 injects a user message into the conversation when the iteration budget gets thin, telling the model to wrap up via the finish tool. 0sec’s agent/loop.ts currently just hits maxTurns and writes state.summary = "Agent reached max turns…" (loop.ts:350). A two-stage warning gives the model a chance to actually call done and persist a partial finding rather than getting cut off mid-thought. Low risk, single-PR.

  2. The “skill” pattern as just-in-time prompt injection. Strix’s load_skill tool (tools/load_skill/load_skill_actions.py, with skills in strix/skills/{vulnerabilities,frameworks,tooling,technologies,protocols}/*.md) lets the model decide at runtime “I need the GraphQL playbook now” and re-renders the system prompt with that markdown appended (llm/llm.py:143-158). 0sec’s dynamic playbooks are injected once after recon (packages/core/src/agent/playbooks.ts); a tool-callable “load this skill” would let mid-run discoveries pull in narrower guidance without bloating every system prompt. Costs prompt cache invalidation; A/B against playbook injection.

  3. The vulnerability-report structural validator. reporting_actions.py:201-339 enforces a strict schema before any report is accepted: required text fields, CVSS XML parses to a real cvss>=3.2 vector, optional CVE/CWE regex shape-checks, file-path validation that rejects .. and absolute paths (_validate_file_path, reporting_actions.py:66-74). 0sec’s Finding validation is laxer; tightening at the agent boundary would prevent garbage from reaching findings-table rendering. Pair with the existing finding-confidence.ts floors.

  1. Their refusal-avoidance block. system_prompt.jinja:71-76 (“Do not self-classify normal in-scope validation as unauthorized…”) plus the system-verified-scope block (:48-63) is a coordinated countermeasure against frontier-model safety reflexes that intermittently break 0sec runs too. Worth A/B testing in our prompts.ts. Don’t borrow blind — Strix’s text is overconfident (“All permission checks have been COMPLETED and APPROVED”) and may trigger different model behavior; test on the same XBOW slice before committing.

  2. The system-prompt-as-scope-lock pattern. Strix injects authorized_targets as Jinja-rendered system-prompt context (strix/agents/StrixAgent/strix_agent.py:22-57) rather than as user-message scope. Mid-conversation user input can’t override system-prompt scope as easily, which matters when sub-agent traffic is appended as "user" role messages. 0sec ships scope mostly via the initial prompt; check whether the same shift reduces scope creep in our runs.

  1. Don’t adopt their context-compression as-is. The “summarize chunks of 10, fall back to messages[0] on failure” (memory_compressor.py:131, 218-224) silently drops work on transient LLM failures. 0sec’s OrchestratorWindowExceededError (agent/journal/orchestrator.ts:104-112) is better — fail loud and surface the bug. Don’t soften that.

  2. Don’t adopt their threading-based sub-agent model. Global module-level dicts (_agent_graph, _agent_messages, _running_agents, _agent_instances, _agent_statesagents_graph_actions.py:11-30) with one lock between them is a recipe for multi-scan races. 0sec’s per-run journal scoping is the correct shape; keep it.

  3. Don’t adopt their “validation is another LLM” approach. This is the central disagreement. Their create_vulnerability_report accepts whatever PoC text the model emits — no replay, no oracle, no calibration. 0sec’s verify/replay-runner.ts + finding-confidence.ts + verify/kernel-verify.ts triangle is the moat. Hold the line.

Strix references (all on usestrix/strix@HEAD, cloned 2026-05-23):

  • Agent loop: strix/agents/base_agent.py:152-259, :331-366, :568-601
  • Agent state: strix/agents/state.py:1-186
  • LLM provider: strix/llm/llm.py:77-411, :178-187, :330-348, :395-411
  • Memory compression: strix/llm/memory_compressor.py:12-13, :15-43, :134-149, :215-226
  • System prompt: strix/agents/StrixAgent/system_prompt.jinja:1-509 (key blocks: :16, :42, :48-76, :194-208, :231-362, :355-360, :393-402)
  • Scope handling: strix/agents/StrixAgent/strix_agent.py:22-152
  • Tool dispatch: strix/tools/executor.py:39-99, :130-186, :227-256, :313-342
  • Tool registry: strix/tools/registry.py:190-300
  • Vulnerability report tool: strix/tools/reporting/reporting_actions.py:9-152, :201-339
  • Deduplication: strix/llm/dedupe.py:14-76, :142-213
  • Agents graph: strix/tools/agents_graph/agents_graph_actions.py:11-30, :384, :797
  • Docker runtime: strix/runtime/docker_runtime.py:142-256, :175
  • Kali container: containers/Dockerfile:1-42
  • Skills: strix/skills/coordination/root_agent.md, strix/skills/scan_modes/deep.md
  • Telemetry: strix/telemetry/posthog.py:15-138, strix/telemetry/tracer.py:50-90
  • CI: .github/workflows/build-release.yml
  • Tests: tests/ (totals from wc -l, no integration tests in tests/agents/)

0sec references (all on 0sec-labs/0sec@research/strix-comparison):

  • Agent loop: packages/core/src/agent/loop.ts:38-120, :350
  • Native loop: packages/core/src/agent/native-loop.ts (1449 LOC)
  • Loop dispatch: planned, not implemented — today packages/core/src/agent/native-loop.ts is the only loop; packages/core/src/agent/journal/ holds the journal primitives
  • Journal writer: packages/core/src/agent/journal/writer.ts:64-100
  • Journal orchestrator: packages/core/src/agent/journal/orchestrator.ts:30-71, :104-112, :122-130
  • Journal summarizer: packages/core/src/agent/journal/summarizer.ts:11-30, :67-82
  • Journal specialists: packages/core/src/agent/journal/specialists.ts:1-50
  • Verification (general): packages/core/src/verify/index.ts:1-69
  • Replay runner: packages/core/src/verify/replay-runner.ts:1-100
  • Kernel verify: packages/core/src/verify/kernel-verify.ts:1-120
  • Kernel verify prompts: packages/core/src/verify/kernel-prompts.ts:1-80
  • Kernel-run tool: packages/core/src/agent/tools/kernel-run.ts:1-185
  • Finding confidence: packages/core/src/agent/finding-confidence.ts:1-60
  • Docker executor: packages/core/src/agent/docker-executor.ts:1-50, :194-228
  • Pipeline: packages/core/src/unified-pipeline.ts:46-112
  • XML dispatch: packages/core/src/agent/xml-dispatch.ts (referenced from agent/loop.ts:59-66)
  • Prompts: packages/core/src/agent/prompts.ts:64-80