* fix: resolve pre-commit check failures
- Change RuntimeError to TypeError for type validation in report/writer.py
- Update pyupgrade to v3.21.2 for Python 3.14 compatibility
* chore: add pytest test infrastructure
Mirror the layout introduced on feature/438-token_budget: pytest +
pytest-asyncio dev deps, asyncio_mode auto, a tests.* mypy override, and
pytest in the mypy pre-commit hook deps so the tests/ package type-checks.
* feat: add --mount and large-target pre-flight for local repos (#492)
Large local targets were copied into the sandbox file-by-file via the SDK
LocalDir entry, which stalls on big repos and could leave /workspace empty.
- --mount <path> bind-mounts a host directory read-only at /workspace/<subdir>
instead of copying it, bypassing the per-file stream.
- A size pre-flight (STRIX_MAX_LOCAL_COPY_MB, default 1024) fails fast with a
clear message suggesting --mount when a non-mounted local target is too big.
* fix: reject empty --mount paths
An empty or whitespace-only --mount value resolves to the current working
directory and would silently bind-mount it into the sandbox. Reject it.
* fix: dedupe local targets so a dir is never both copied and mounted
If the same directory is passed via --target and --mount (or as duplicate
values), it previously produced two targets — copied AND bind-mounted, and
the copied one could trip the size pre-flight. Dedupe by resolved path,
preferring the bind mount.
* fix: treat non-positive STRIX_MAX_LOCAL_COPY_MB as disabled
Previously a value of 0 (or negative) made every local target count as
oversized, aborting all local scans. Now <= 0 disables the pre-flight.
* fix: log unreadable subtrees during size pre-flight
os.walk silently swallowed directory-listing errors, so a permission-denied
subtree could make a large repo under-count and slip past the pre-flight.
Surface such omissions via an onerror warning.
* docs: document --mount and STRIX_MAX_LOCAL_COPY_MB
Add CLI reference + example for --mount, document the size pre-flight env var,
note the read-only-is-not-a-hard-boundary caveat and that remote repos are not
size-checked, and clarify the backends docstring on when bind mounts apply.
* Update strix/interface/main.py
* Update strix/runtime/docker_client.py
---------
Five rounds of sweep across the tree. Net ~544 lines removed.
Removed:
- Section-divider banners and one-line section labels (# Display
utilities, # ----- list_requests -----, # CVSS breakdown, etc.).
- Module-level prose docstrings on internal modules. Kept one-line
summaries; trimmed multi-paragraph narration about SDK/Strix
responsibility splits, cache strategies, three-source precedence.
- Internal-helper docstrings that just restate the function name —
caido_api helpers (caido_url, get_client, view_request, etc.),
settings-class one-liners (LLMSettings, RuntimeSettings, ...),
UI helper docstrings.
- Args/Returns blocks on non-LLM-facing internal helpers
(build_strix_agent, render_system_prompt, create_or_reuse,
bootstrap_caido) — kept only the genuinely non-obvious params.
- Internal-history phrasing — "Mirrors main-branch shape",
"pre-SDK harness", "previous lookup matched no attribute".
- Narrative comments inside function bodies that explained what the
next line does, design rationale obvious from the surrounding code,
or "we used to..." asides.
- Trailing periods on every error-string literal across the tool tree.
- Duplicated roundtripTime quirk comment (kept the LLM-facing copy in
tools/proxy/tools.py).
Kept (every one names an upstream bug, vendored-code provenance, or
non-obvious data quirk):
- core/runner.py: SDK replay-with-empty-initial-input + on_agent_end
lifecycle gap.
- runtime/docker_client.py: VERBATIM COPY block of the upstream
_create_container body, pinned to SDK v0.14.6.
- runtime/session_manager.py: NO_PROXY for agent-browser CDP loopback.
- tools/proxy/caido_api.py: generated-pydantic Request.raw quirk,
replay double-history pitfall.
- tools/proxy/tools.py: Caido roundtripTime=0 quirk for proxy
captures.
When a scan ends without calling ``finish_scan`` (Ctrl+C, TUI quit,
crash), ``display_completion_message`` now appends one extra line
inside the existing completion panel:
Resume strix --run-name <run_name>
Same ``dim``-label / coloured-value styling as the panel's ``Target``
and ``Output`` rows. Only rendered when ``scan_completed`` is False —
a finished scan doesn't need a resume nudge.
Triggers ``orchestration/scan.py``'s implicit-resume path on the next
invocation (presence of ``{run_dir}/bus.json`` is the trigger), so
the user gets back exactly where they left off — root + every
non-terminal subagent's full LLM history, bus topology, prior
findings.
Covers both ``run_cli`` and ``run_tui`` paths since
``display_completion_message`` is called from ``main()`` regardless
of which front-end ran.
When the user shuts down a run (Ctrl+C in CLI, Ctrl+Q / quit dialog
in TUI, or an uncaught exception during the scan), print a Rich
panel telling them the exact command to pick up where they left off:
strix --run-name <run_name>
The panel only appears when ``strix_runs/<run_name>/bus.json``
exists — i.e. the scan registered at least the root agent and has
snapshot state worth resuming from. Suppressed when:
* No run-name was assigned (Ctrl+C before sandbox bring-up).
* The run dir doesn't exist or has no bus.json yet.
Implementation:
* ``strix/interface/utils.py`` gains ``format_resume_hint(run_name)
-> Panel | None``.
* ``cli.py`` calls it in the SIGINT/SIGTERM/SIGHUP handler before
``sys.exit(1)``, and in the ``except Exception`` arm before the
re-raise.
* ``tui.py:run_tui`` calls it in a ``finally`` after
``app.run_async()`` so the hint lands on the real terminal once
Textual has restored it (whether the user pressed Ctrl+Q,
confirmed the quit dialog, or the run completed naturally).
Replaces 200+ lines of bespoke env-loader / persist / change-detection
machinery with ``pydantic_settings.BaseSettings`` (already a transitive
of ``openai-agents → mcp``, no new direct dep).
What was wrong with ``Config``:
- 14 knobs flat in one namespace, weak grouping by comment-block.
- ``Config._applied_from_default`` and ``Config._config_file_override``
were externally mutated from ``interface/main.py:532-534``. Private
members were part of the public contract.
- Stringly-typed values: every caller had to coerce
(``int(Config.get("llm_timeout") or "300")``,
``... not in {"0", "false", "no", "off"}``).
- Dead knob: ``strix_llm_max_retries`` declared, persisted, listed in
``_LLM_CANONICAL_NAMES`` — zero readers (``DEFAULT_RETRY``
hardcodes ``max_retries=5``). Dropped.
- ``_LLM_CANONICAL_NAMES`` tuple maintained alongside class vars —
duplicate source of truth.
- ``_tracked_names()`` introspected ``vars(cls).items()`` filtered on
``(v is None or isinstance(v, str))`` — fragile.
- Awkward path: ``strix/config/config.py`` inside ``strix/config/``
with ``__init__.py`` just re-exporting.
- Dual access for the same fact: ``web_search`` read
``os.getenv("PERPLEXITY_API_KEY")`` while ``main.py`` read
``Config.get("perplexity_api_key")``.
New shape:
- ``strix/config/settings.py`` — typed dataclass tree:
``Settings.{llm,runtime,telemetry,integrations}``. Each sub-model is
its own ``BaseSettings`` so it reads env independently. Field-level
``alias=`` and ``validation_alias=AliasChoices(...)`` mirror the
existing flat env-var names — user-facing env contract is unchanged.
Bool fields auto-parse ``"0"``/``"false"``/``"no"``/``"off"``;
int fields auto-coerce.
- ``strix/config/loader.py`` — thin ``load_settings()``,
``apply_config_override(path)``, ``persist_current()`` with module
cache. JSON file reader walks aliases to populate sub-models, dropping
entries already covered by env (so env still wins).
- 13 callsites migrated from ``Config.get("...")`` to
``load_settings().<group>.<field>``.
- ``posthog._is_enabled()`` collapses to one line.
- ``--config <path>`` flow simplified: one
``apply_config_override(...)`` call replaces three lines of
class-private mutation.
Drive-by — drop ``is_whitebox`` from ``scan_config`` dict:
- It was being derived as ``bool(args.local_sources)`` in three places
(``cli.py``, ``tui.py``, ``main.py``) and stuffed into the dict for
``entry.py`` to read back. The fact is fully derivable from
``scan_config["targets"]`` — any target with ``type == "local_code"``.
- New helper ``is_whitebox_scan(targets)`` in ``interface/utils.py``
alongside the other target-classification utilities.
- ``entry.py`` computes once; ``main.py``'s posthog start uses the same
helper. Triplicate derivation gone.
Verified: ruff at baseline (3), mypy at baseline (69). Six smoke tests
pass — defaults / JSON-only / env-wins-over-JSON / alias-chain
fallback / bool parsing / ``is_whitebox_scan``.
The SDK harness is the only path now; legacy host-side code is gone.
File names no longer carry the ``sdk_`` distinction.
Deleted legacy host-side modules:
- strix/agents/StrixAgent/ (template moved to strix/agents/prompts/)
- strix/agents/base_agent.py, state.py
- strix/llm/llm.py, config.py
- strix/runtime/docker_runtime.py, runtime.py
- strix/tools/executor.py, agents_graph/agents_graph_actions.py
- strix/interface/sdk_dispatch.py + the env-flag dispatch in cli.py
Renamed (drop ``sdk_`` prefix):
- strix/sdk_entry.py → strix/entry.py
- strix/agents/sdk_factory.py → strix/agents/factory.py
- strix/agents/sdk_prompt.py → strix/agents/prompt.py
- strix/tools/<x>/<x>_sdk_tool[s].py → strix/tools/<x>/tool[s].py
- strix/tools/_legacy_adapter.py → strix/tools/_state_adapter.py
- ``_legacy`` aliases inside the wrappers → ``_impl``
CLI + TUI now call ``run_strix_scan`` directly — they build the
sandbox image / sources_path locally and rely on
``session_manager.cleanup`` (called inside ``run_strix_scan``'s finally)
for teardown. Three TUI handlers that reached into legacy multi-agent
globals (``_agent_instances``, ``send_user_message_to_agent``,
``stop_agent``) are now no-ops with a TODO; reconnecting them to the
``AgentMessageBus`` is a follow-up.
Tracer.get_total_llm_stats no longer reaches into the deleted
``agents_graph_actions`` globals — the orchestration hooks now feed the
tracer via ``Tracer.record_llm_usage`` (live + completed buckets).
finish_scan's ``_check_active_agents`` and load_skill's runtime
``_agent_instances`` reach-in are no-op stubs; the
``AgentMessageBus`` is the source of truth post-migration.
llm/utils.py rewritten to keep only the streaming-parser helpers
(``normalize_tool_format``, ``parse_tool_invocations``,
``fix_incomplete_tool_call``, ``format_tool_call``, ``clean_content``).
``STRIX_MODEL_MAP`` moved to ``llm/multi_provider_setup.py`` (its only
remaining caller).
Per-file ruff ignores added for legacy interface modules (TUI / main /
CLI / utils / streaming_parser / tool_components) and tracer.py —
pre-existing PLC0415/BLE001/PLR0915 patterns are out of scope.
Tests: 287/287 passing. Renamed test files to drop ``sdk_`` prefix.
``test_tracer.py::test_get_total_llm_stats_aggregates_live_and_completed``
rewritten to feed ``Tracer.record_llm_usage`` instead of legacy globals.
Test file annotations added so pre-commit's strict mypy passes.
Adds the env-var gate that lets users opt into the SDK harness without
disturbing the legacy default. Per PLAYBOOK §7.1, this is the cutover
mechanism: STRIX_USE_SDK_HARNESS=1 routes scans through run_strix_scan
(the Phase 5 entry point); anything else continues to use
StrixAgent.execute_scan.
- strix/interface/sdk_dispatch.py:
- should_use_sdk_harness(): truthy-string parse of the env var.
- _resolve_sandbox_image(): reads strix_image from Config; falls
back to "strix-sandbox:latest" with a warning if unset.
- _resolve_sources_path(): when --local-sources is given, mounts
its parent so the agent walks down to the source tree; otherwise
creates a per-run scratch dir under XDG_CACHE_HOME/strix/sources/.
Phase 6 will replace this with the legacy clone-into-container
flow once we port that.
- run_scan_via_sdk(): the adapter — translates the legacy CLI
(scan_config dict + argparse Namespace + Tracer) into the keyword
arguments run_strix_scan expects. Returns the SDK RunResult; lets
failures bubble up.
- strix/interface/cli.py: adds the dispatch branch inside the existing
Live/status loop. Legacy default unchanged; SDK path is reached only
when STRIX_USE_SDK_HARNESS is truthy. Two pre-existing lazy imports
hoisted to module level (cleanup_runtime + sdk_dispatch helpers) so
ruff is happy.
Pre-existing legacy lint/type issues surfaced when pre-commit checked
the edited cli.py and chased imports — fixed or ignored in passing:
- utils.py:1052 duplicate ``metadata`` annotation removed.
- utils.py:1251 unused ``# type: ignore[import-not-found]`` for yarl.
- main.py:456 ``panel_parts`` inferred type rejected later string
entries — explicit ``list[Text | str]`` annotation.
- utils.py:resolve_diff_scope_context PLR0912 (16 branches) per-file
ignore — branches map 1:1 to scope-mode × target-type combinations.
Tests: 18 new tests in tests/interface/test_sdk_dispatch.py — env
flag parsing parametrized over truthy/falsy variants, image lookup
with config hit + miss-with-warning, sources path resolution for
local_sources / alternative key names / scratch-dir creation, and
the adapter's kwarg handoff verified against a patched
run_strix_scan (run_name from args + run_name from scan_config
fallback + failure propagation).
Refs: PLAYBOOK.md §7.1 (cutover), §7.2 (rollback).
Users can now access the Caido web UI from their browser to inspect traffic,
replay requests, and perform manual testing alongside the automated scan.
- Map Caido port (48080) to a random host port in DockerRuntime
- Add caido_port to SandboxInfo and track across container lifecycle
- Display Caido URL in TUI sidebar stats panel with selectable text
- Bind Caido to 0.0.0.0 in entrypoint (requires image rebuild)
- Bump sandbox image to 0.1.12
- Restore discord link in exit screen
Replace 12 flat parameters (code_file, code_before, code_after, code_diff,
and 8 CVSS fields) with structured nested XML fields: code_locations with
co-located fix_before/fix_after per location, cvss_breakdown, and cwe.
This enables multi-file vulnerability locations, per-location fixes with
precise line numbers, data flow representation (source/sink), CWE
classification, and compatibility with GitHub/GitLab PR review APIs.
Remove hardcoded github.com/gitlab.com/bitbucket.org host lists from
infer_target_type. Instead, detect git repositories on any host by
querying the standard /info/refs?service=git-upload-pack endpoint.
Works for any self-hosted git instance.
- Reuse apply_saved() instead of custom override logic
- Add force parameter to override existing env vars
- Move validation to utils.py
- Prevent saving when using custom config (one-time override)
- Fix: don't modify ~/.strix/cli-config.json when --config is used
Co-Authored-By: FeedClogger <feedclogger@users.noreply.github.com>
Rewrite localhost/127.x.x.x/0.0.0.0 target URLs to use host.docker.internal,
allowing the container to reach services running on the host machine.
- Add extra_hosts mapping for host.docker.internal on Linux
- Add HOST_GATEWAY env var to container
- Add rewrite_localhost_targets() to transform localhost URLs
- Support full 127.0.0.0/8 loopback range and IPv6 ::1
- Reduced animation timer for splash screen to improve responsiveness.
- Added URL display to the splash screen.
- Improved start line animation with dynamic character styling.
- Updated agent status display to show "Initializing Agent" when no real activity is detected.
- Enhanced waiting and animated verb text with dynamic styling.
- Implemented sidebar visibility toggle based on window size.
- Updated live stats to include model information from agent configuration.
- Refined TUI styles for better visual consistency.