CodeDocs Vault

CLI & Textual TUI

strix is a terminal program; the interface layer is everything between "user hits return" and the agent loop starting — plus, in interactive mode, a Textual-based live dashboard that renders agent activity as it happens.


1. Entry Point

Declared in pyproject.toml:52:

[project.scripts]
strix = "strix.interface.main:main"

main() (strix/interface/main.py:540-637) sequence:

  1. Startup: pick Windows event-loop policy if needed, call parse_arguments().
  2. Config override: if --config given, load JSON over the in-process Config class.
  3. Dependency check: check_docker_installed().
  4. Warm-up:
    • pull_docker_image() — pulls the sandbox image if cache-missed.
    • validate_environment() — checks STRIX_LLM is set; API key optional (some providers use IAM).
    • warm_up_llm() — sends a "Reply with just 'OK'" request with a 300s timeout to fail-fast on bad credentials/endpoints.
    • Persists config to ~/.strix/cli-config.json so the next run doesn't re-prompt.
  5. Target processing: generate_run_name(targets) produces strix_runs/<slug>_<hex>; repo targets cloned to /tmp/strix_repos/<run>/...; local sources collected for diff-scope.
  6. Diff-scope resolution: resolve_diff_scope_context() returns an instruction block (merged into user instructions) describing which files changed in the PR.
  7. Dispatch: non-interactive → run_cli(args), interactive → run_tui(args). Both wrapped in signal-handling and exception-logger to PostHog.

2. CLI Arguments

parse_arguments() (main.py:266-425):

Flag Type Default Purpose
--target / -t str (append) REQUIRED Repo URL, domain, URL, IP, or local path. Repeatable for multi-target.
--instruction str None Inline guidance ("focus on IDOR").
--instruction-file str None Read instructions from file. Mutually exclusive with --instruction.
--non-interactive / -n flag False Headless CLI mode (no TUI).
--scan-mode / -m quick / standard / deep deep Loads the matching skill, affects reasoning effort.
--scope-mode auto / diff / full auto PR-diff analysis trigger.
--diff-base str None Git ref baseline for diff mode.
--config path None Override ~/.strix/cli-config.json.
--version / -v flag Print version.

Target Type Inference (interface/utils.py:1085-1150)

Heuristic order:

  1. git@, git://, .git suffix → repository
  2. URL with scheme/query, or a GitHub/GitLab host → web_application
  3. IPv4/IPv6 literal → ip_address
  4. Existing directory path → local_code
  5. Contains . and no path → domain → auto-https://

This means the user doesn't have to specify target type — Strix figures it out.


3. Config Bootstrapping

Three-level precedence (implemented across strix/config/config.py and interface/main.py:52-255):

  1. Environment variables (highest) — STRIX_LLM, LLM_API_KEY, LLM_API_BASE, PERPLEXITY_API_KEY, STRIX_REASONING_EFFORT, etc.
  2. --config file — if passed.
  3. ~/.strix/cli-config.json — persisted across runs.
  4. Defaults from the Config class.

Change detection: _llm_env_changed() (config.py:72-83) clears stale saved config when env LLM creds differ from what's on disk. File is saved with chmod 0600 on Unix.

Validation flow bails out with a clear panel if:


4. Interactive vs Non-Interactive

Different code paths; same agent.

4.1 run_cli — Headless (interface/cli.py:23-206)

Suited for CI, Docker jobs, SSH-less environments.

4.2 run_tui — Textual (interface/tui.py:685-2096)

Full dashboard. Agent runs in a background thread; UI polls tracer every 350ms.

Layout

┌────────────────────────────────────────────────────────────┐
│  Strix                                            v0.8.3   │
├────────────────────────────────┬───────────────────────────┤
│                                │ Agents Tree               │
│                                │ ⚪ RootAgent (2)          │
│  Chat / activity stream        │   🟢 SQLi-validate (1)   │
│                                │   ⏸ XSS-recon            │
│  (renders per-tool widgets,    ├───────────────────────────┤
│   streamed LLM text,           │ Vulnerabilities           │
│   streaming tool calls)        │ 🔴 critical  SQLi-login   │
│                                │ 🟠 high       IDOR-users   │
│                                ├───────────────────────────┤
│                                │ Stats                     │
│                                │ Tokens: 128k  Cost: $2.13 │
├────────────────────────────────┴───────────────────────────┤
│ > chat input (Enter = send, Shift+Enter = newline)         │
└────────────────────────────────────────────────────────────┘

Widgets

Modals


5. Streaming Parser (interface/streaming_parser.py:43-126)

Parses incomplete LLM output in real time so the TUI can render partial tool calls as they arrive.

Input (an incoming chunk while the LLM is still generating):

I'll probe the login form next.
<function=browser_action>
<parameter=action>click

Output — list of StreamSegment:

[
  StreamSegment(type="text", content="I'll probe the login form next."),
  StreamSegment(type="tool", tool_name="browser_action",
                args={"action": "click"}, is_complete=False),
]

Parser walks the string, alternating between text and <function=X> blocks. For each function block:

Caching (tui.py:1122-1159): the TUI keys rendered output by (agent_id, len(content)); when the length grows, it re-parses and re-renders only that region.


6. Tool Renderers (interface/tool_components/)

Per-tool visual rendering. Each subclass of BaseToolRenderer registers with @register_tool_renderer and returns a Textual Static widget.

Tool pattern Renderer What it shows
browser_* BrowserRenderer Tab list, screenshots (embedded), last action
terminal_*, run_command TerminalRenderer Pyte-parsed terminal replay with ANSI colors
proxy_* ProxyRenderer HTTP request/response diffs, headers, body preview
python_* PythonRenderer Code + exec result, with syntax highlighting
file_edit_* FileEditRenderer Before/after diff
load_skill_* LoadSkillRenderer "Loaded: sql_injection, idor"
scan_start_info, subagent_start_info ScanInfoRenderer Scan metadata panels
create_vulnerability_report ReportingRenderer Severity-colored finding card
finish_* FinishRenderer Completion status
web_search_* WebSearchRenderer Search results
user/agent messages UserMessageRenderer, AgentMessageRenderer Plain + markdown

This is where the "watching an agent hack" experience comes from — the TUI doesn't just show JSON, it shows terminal output replays, screenshot previews, and diff views inline.


7. Scan-Mode & Scope-Mode Resolution

7.1 Scan Mode

--scan-mode {quick, standard, deep} maps directly into LLMConfig.scan_mode, which the LLM layer uses to:

7.2 Scope Mode (interface/utils.py:988-1070)

resolve_diff_scope_context() decides whether to focus on a PR diff.

Diff-base resolution order (utils.py:657-684):

  1. Explicit --diff-base.
  2. GITHUB_BASE_REFrefs/remotes/origin/<base_ref>.
  3. GitHub Actions event payload base SHA.
  4. origin/HEAD symbolic ref.
  5. Fallback to origin/main or origin/master.
  6. If still nothing → error with a hint ("use actions/checkout with fetch-depth: 0").

Output is a DiffScopeResult with an instruction_block that gets merged into the user's instructions:

The user is requesting a review of a Pull Request.
Instruction: Direct your analysis primarily at the changes in the listed files…

Repository Scope: my-repo
Base reference: origin/main
Merge base: abc123…
Primary Focus (changed files to analyze):
- src/auth.py
- tests/auth_test.py

This text becomes part of the agent's task description; the agent treats the listed files as the "primary focus" for triage.


8. Output Artifacts

Every run writes to strix_runs/<slug>_<hex>/. The Tracer creates the dir and appends:

File Contents
events.jsonl Canonical event stream — run.started, agent.created, chat.message, tool.execution.*, finding.created, etc. with trace_id/span_id correlation
messages.json Full conversation history per agent
tool_executions.json Structured log of every tool call + result
vulnerability_reports.json Findings with CVSS, PoC, remediation
scan_metadata.json Targets, instructions, diff-scope decision

Screenshots from the browser renderer live inside the sandbox's /home/pentester/output (or wherever the tool wrote them) and are base64-embedded into the JSON artifacts on the host, since nothing is synced back from the container filesystem directly.


9. Signal Handling

CLI mode (interface/cli.py:111-125)

signal.signal(signal.SIGINT, signal_handler)    # Ctrl+C
signal.signal(signal.SIGTERM, signal_handler)   # kill
signal.signal(signal.SIGHUP, signal_handler)    # terminal close
atexit.register(cleanup_on_exit)                # backstop

signal_handler → flush tracer → teardown runtime → sys.exit(1).

TUI mode (interface/tui.py:766-781, 1960-1968)

Ctrl+Q or Ctrl+C → QuitScreen modal → on confirm:

def action_custom_quit():
    if self._scan_thread.is_alive():
        self._scan_stop_event.set()
        self._scan_thread.join(timeout=1.0)
    tracer.cleanup()
    self.exit()

The agent thread polls _scan_stop_event cooperatively. After 1s timeout the TUI exits anyway — a deliberately short fuse so the TUI never hangs.

Exit Codes

The 2 exit code is what lets CI fail the PR build on findings.


10. Design Observations

Good ideas:

Pitfalls: