Commit Graph

420 Commits

Author SHA1 Message Date
0xallam 8bbb31e075 chore(image): chromium-from-apt + anti-detection flags via agent-browser env
Drops the ``agent-browser install --with-deps`` step (Chrome for
Testing has no ARM64 build and ships several automation tells)
and uses the apt-installed Chromium across both arches.

``agent-browser`` is wired via three env vars baked into the image:

  * ``AGENT_BROWSER_EXECUTABLE_PATH=/usr/bin/chromium`` — every
    browser launch picks up the apt binary; no per-call flag needed.
  * ``AGENT_BROWSER_USER_AGENT`` — recent stable Chrome 131 Linux UA.
  * ``AGENT_BROWSER_ARGS`` — minimal stealth flag set:
    ``--disable-blink-features=AutomationControlled`` (the most-
    checked tell), ``--exclude-switches=enable-automation``,
    ``--disable-features=IsolateOrigins,site-per-process,Translate,
    BlinkGenPropertyTrees``, sane window-size + lang, infobars +
    save-password + session-crashed bubbles off.

The ``agent-browser doctor --offline --quick`` step at build time
verifies the binary launches; subsequent runtime calls inherit
the env automatically.

Net: smaller image (no ~150 MB Chrome-for-Testing download),
ARM64-clean, env-driven config so future flag tweaks land without
touching the agent-browser install.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 01:42:20 -07:00
0xallam c011c66889 chore(image): bump sandbox tag 0.1.13 → 0.2.0
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>
2026-04-26 01:19:56 -07:00
0xallam e83522cec5 fix(scan): respawn-skip finalizes cancelled agents as `stopped`
When ``_respawn_subagents`` skipped an agent because it was in
``bus.stopping`` (the user clicked stop before the crash), the bus
state was left untouched — status stayed ``running`` forever, so
``view_agent_graph`` and the TUI tree showed phantom agents that
would never make progress.

Now the skip path collects those agent ids and finalizes each as
``stopped`` outside the lock, which transitions status correctly,
clears the ``stopping`` entry (``finalize`` already discards it),
moves the live stats to ``stats_completed``, and triggers the
post-finalize snapshot. A subsequent ``view_agent_graph`` shows the
truth: the agent is stopped.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 01:16:26 -07:00
0xallam 671c69327b fix(persistence): snapshot resume-instruction + persist notes to disk
Two follow-ups from the post-fix audit:

**#1 critical**: ``orchestration/scan.py`` injects the user's new
``--instruction`` into the root's bus inbox via ``bus.send`` on resume,
but ``send`` is one of the deliberately-not-snapshotted high-frequency
mutations. A SIGKILL between that send and the model's first turn
would silently drop the user's new directive. Force a snapshot
immediately after the inject — that's the one specific message we
can't afford to lose, while leaving general ``send`` traffic
unsnapshotted as designed.

**Notes persistence**: ``strix/tools/notes/tools.py`` now mirrors the
todo pattern. ``_notes_storage`` writes through to
``{run_dir}/notes.json`` after every create/update/delete via the
same atomic-tempfile + ``Path.replace`` flow. New
``hydrate_notes_from_disk(run_dir)`` is wired in ``run_strix_scan``
alongside ``hydrate_todos_from_disk`` so a resumed scan recovers the
exact note set the prior process saw, including ``wiki``-category
notes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 01:09:56 -07:00
0xallam 5fd2a64562 fix(persistence): close all 9 gaps from the resume audit
Three critical correctness fixes + six TUI/audit/UX fixes from the
parallel-agent audit. All changes verified by an end-to-end smoke
that builds, persists, and re-hydrates state across two simulated
process boundaries.

Critical (resume integrity):

1. ``bus.cancel_descendants_graceful`` now calls ``_maybe_snapshot``
   after mutating the ``stopping`` set. Previously, a process crash
   between user-initiated graceful-stop and the next finalize lost
   the stop signal — respawned agents would run forever instead of
   exiting. ``_respawn_subagents`` also gains a guard that skips
   agents in ``stopping`` so a previously-cancelled agent is not
   resurrected on resume.

2. ``Tracer.hydrate_from_run_dir`` now **raises** on corrupt
   ``vulnerabilities.json`` instead of swallowing the exception. The
   prior behaviour silently reset ``vulnerability_reports`` to empty,
   so the next ``add_vulnerability_report`` would allocate ``vuln-0001``
   and overwrite the prior MD on disk — silent data loss.

3. ``--instruction`` passed on resume now reaches the model. The CLI
   captures whether the user explicitly passed an instruction
   (``args.user_explicit_instruction``) before ``_load_resume_state``
   loads the persisted one. ``run_strix_scan`` reads
   ``scan_config["resume_instruction"]`` and, on resume, sends the
   new instruction to root's bus inbox before calling
   ``run_with_continuation`` (which uses ``initial_input=[]`` for SDK
   replay). The inject filter surfaces it on the next turn.

4. ``--resume X`` errors loudly when ``scan_state.json`` exists but
   ``bus.json`` doesn't. Previously this silently fresh-started in
   the same dir, confusing the user who explicitly asked to resume.

TUI / audit / UX:

5. ``Tracer.hydrate_from_run_dir`` now reads ``bus.json`` too and
   pre-populates ``tracer.agents`` from the snapshot's ``statuses`` /
   ``names`` / ``parent_of``. Before this, the TUI tree on resume
   showed only currently-running agents; completed/crashed children
   from the prior run were invisible.

6. ``Tracer.hydrate_from_run_dir`` also seeds ``self._llm_stats`` from
   ``bus.stats_live + bus.stats_completed`` so the resume's footer
   shows cumulative tokens / requests across the prior run plus the
   resume segment, instead of resetting to zero.

7. ``Tracer.save_run_data`` now also writes ``run_metadata.json``
   (start_time, run_id, run_name, targets, status), and
   ``hydrate_from_run_dir`` restores ``start_time`` from it. Prior
   behaviour reset start_time to ``now()`` on every Tracer init,
   breaking the final report's duration calc on resumed scans.

8. Per-agent todos persist to ``{run_dir}/todos.json`` (atomic write
   on every CRUD). ``hydrate_todos_from_disk`` (called from
   ``run_strix_scan``) reloads them so respawned subagents find
   their lists intact. Previously, the module-level
   ``_todos_storage`` was lost on every process restart.

9. ``_load_resume_state`` validates each ``cloned_repo_path`` from
   the persisted ``scan_state.json`` still exists on disk. Previously
   a deleted clone dir would let the resume proceed with an empty
   source tree, with agents silently scanning nothing.

Bonus: ``bus.finalize`` no longer pops ``parent_of`` and ``names``
for finalized agents. Routing protection (don't accept ``send`` to
finalized agents) comes from the ``statuses[id]`` terminal-state
check in ``send`` itself, so dropping those keys was overzealous and
made completed children invisible in ``view_agent_graph`` and the
TUI tree.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 00:57:52 -07:00
0xallam fb6fdffb40 feat(cli): --resume <run_name> as the canonical resume command
Adds an explicit ``--resume RUN_NAME`` flag that loads the prior
run's persisted scan state from ``strix_runs/<run_name>/scan_state.json``
and replays it (targets, scan_mode, instruction, local_sources,
diff_scope, scope_mode, diff_base) so the user never has to retype
their original args.

The exit panel now suggests ``strix --resume <run_name>`` instead of
``--run-name``. Same single-line, same dim-label / coloured-value
styling as ``Target`` / ``Output`` rows, gated on
``not scan_completed``.

CLI contract:
  * ``--resume X`` cannot be combined with ``--target`` (parser error).
  * ``--resume X`` errors with a clear message if
    ``strix_runs/X/scan_state.json`` is missing.
  * Fresh runs persist scan_state.json once at the end of setup —
    after target normalization, repo cloning, local-source
    collection, diff-scope resolution, and final instruction
    composition. So whatever the agent saw on first run is exactly
    what the resumed run sees.

Internally the resume path stays implicit (presence of bus.json
triggers it inside ``run_strix_scan``); ``--resume`` is a UX layer
that:
  1. Sets ``args.run_name = args.resume``.
  2. Pre-populates ``args.targets_info`` and friends from disk.
  3. Skips the fresh-only steps (target re-parse, repo clone,
     diff-scope re-resolution) — the persisted values were already
     finalized on the first run.

HARNESS_WIKI.md: drop the "delete the run dir to force fresh"
instruction.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 00:43:22 -07:00
0xallam b5ee0c283c feat(interface): show resume hint on the existing exit panel
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.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 00:38:17 -07:00
0xallam 1c4cb4dc8a feat(interface): show resume hint on user-initiated exit
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).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 00:34:44 -07:00
0xallam d538acf66b feat(orchestration): always-on resume across the agent graph
A scan that crashes or is stopped can now be resumed by re-invoking
``strix`` with the same ``--run-name``. Resume is implicit — presence
of ``{run_dir}/bus.json`` triggers it. To force a fresh start, delete
the run dir.

What survives a process restart with the same scan_id:

  * Root agent's LLM history — already worked (root SDK SQLiteSession).
  * Every non-terminal subagent's LLM history — new. ``create_agent``
    now opens SQLiteSession(session_id=child_id,
    db_path={run_dir}/sessions/{child_id}.db) per child and passes it
    to ``run_with_continuation``.
  * Bus topology — new. ``AgentMessageBus`` gains snapshot/restore/
    _maybe_snapshot async methods plus a ``metadata`` field that holds
    per-agent {task, skills, is_whitebox, scan_mode, diff_scope}.
    ``register``, ``finalize``, ``park``, and ``mark_llm_failed`` each
    call ``_maybe_snapshot`` to atomically persist the bus to
    {run_dir}/bus.json (tempfile + Path.replace).
  * Vulnerability reports — new. ``ScanArtifactWriter._write_
    vulnerabilities`` now also writes ``vulnerabilities.json``
    (atomic). ``Tracer.hydrate_from_run_dir`` reads it on resume so
    new vuln-NNNN ids don't collide with prior on-disk files.

What does not survive: the sandbox container itself (fresh per
process), so ``/workspace/scratch`` and Caido state are lost.
``/workspace/sources`` re-mounts from the host so source code is
unchanged.

``orchestration/scan.py:run_strix_scan`` does the actual resume:
  1. Resolve run_dir up front; if bus.json exists it's a resume.
  2. Acquire {run_dir}/.lock (fcntl.flock) so a second strix process
     can't run concurrently on the same scan_id.
  3. ``bus.set_snapshot_path(...)``, ``tracer.hydrate_from_run_dir()``.
  4. On resume: load + bus.restore, find root_id from snapshot (the
     agent with parent_of[id] is None), spawn the sandbox, skip the
     root's bus.register (already in snapshot).
  5. ``_respawn_subagents`` walks every agent with status in
     running/waiting/llm_failed: reopens its SQLiteSession, rebuilds
     the child agent via the captured factory, builds run config /
     context, asyncio.create_task the run with initial_input=[] so
     the SDK replays from session. Per-child failure (missing/corrupt
     DB, factory raises) finalizes that child as crashed and continues.
  6. Open root SQLiteSession at the same path, run the root with
     initial_input=[] on resume (or the formatted root task on a
     fresh run), and let SDK replay drive the next turn.
  7. ``finally``: close every per-agent session, take a final
     snapshot, tear down sandbox, release the lock.

HARNESS_WIKI.md updated with the new run-dir layout (sessions/,
bus.json, vulnerabilities.json, .lock) and the resume contract.

Net: +500 LoC across 7 files. No new deps.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 00:29:37 -07:00
0xallam 81703e286f refactor(notes): drop disk persistence + shared-wiki prose
The notes tool no longer touches disk. ``_notes_storage`` lives in
memory for the lifetime of one scan process, shared across every
agent in that process via the existing RLock. Process exit clears
the lot — no notes.jsonl event log, no wiki/<slug>.md Markdown
rendering, no replay-on-startup hydration.

Removed ~10 internal helpers (``_get_run_dir``,
``_get_notes_jsonl_path``, ``_append_note_event``,
``_load_notes_from_jsonl``, ``_ensure_notes_loaded``,
``_persist_wiki_note``, ``_remove_wiki_note``,
``_get_wiki_directory``, ``_get_wiki_note_path``,
``_sanitize_wiki_title``) plus the ``_loaded_notes_run_dir`` module
state, ``wiki_filename`` per-note field, and the ``OSError`` branches
that only existed for the wiki write path.

The ``wiki`` category is preserved as a free-form long-form bucket;
it just no longer has any special persistence behaviour.

Skill prompts scrubbed of every "shared wiki memory" / "repo wiki" /
"append a delta before agent_finish" instruction:
``coordination/source_aware_whitebox.md``,
``custom/source_aware_sast.md``,
``scan_modes/{quick,standard,deep}.md``, plus the WHITE-BOX TESTING
block in ``agents/prompts/system_prompt.jinja``.

HARNESS_WIKI.md updated to drop the wiki-as-shared-knowledge-base
description, the per-run output-tree references to ``notes/notes.jsonl``
and ``wiki/{note_id}-{slug}.md``, and the ``is_whitebox`` toggle prose.

Net: -178 LoC in notes/tools.py, -45 LoC across skills/system_prompt
and the wiki doc. The notes tool surface (5 ``@function_tool``s) is
unchanged for the agent.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 23:56:22 -07:00
0xallam f8213452ea feat(logging): close audit gaps — SDK records, proxy tracebacks, CLI/docker/posthog
Five gaps from the post-implementation audit, closed:

1. **SDK logger captured.** The openai-agents SDK uses
   ``logging.getLogger("openai.agents")`` for its own lifecycle events
   (Runner.run starts, tool dispatch, model retries, exceptions).
   Previous setup only attached handlers to the ``strix`` root, so
   SDK-internal events were dropped. Tracked-roots tuple now covers
   both, with the same FileHandler/StreamHandler/Filter chain.

2. **Proxy tool exception tracebacks.** Every ``@function_tool`` in
   ``strix/tools/proxy/tools.py`` returns a JSON error to the LLM via
   the ``_err(name, exc)`` helper. The tracebacks were silently
   formatted away — the LLM saw the message, the human reading the
   log saw nothing. ``_err`` now emits ``logger.exception(...)``
   covering all five tools at once.

3. **CLI bootstrap.** ``strix/interface/main.py`` had its module
   ``logger`` removed by the previous commit and was emitting nothing.
   Restored, plus log lines for env validation, docker check, LLM
   warm-up, and image pull (debug for already-present, info for
   pull, exception for failures).

4. **Docker client.** ``strix/runtime/docker_client.py`` had no
   logger. Container creation now logs caps + exposed ports at DEBUG
   and the resulting container id at INFO.

5. **PostHog telemetry.** ``strix/telemetry/posthog.py`` had no
   logger. Now logs send success/failure at DEBUG, version-detection
   failures at DEBUG, and disabled-skip at DEBUG (so the log shows
   when telemetry is off, instead of being silent about it).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 23:43:19 -07:00
0xallam 46ff025209 feat(logging): per-scan `{run_dir}/strix.log` with scan/agent context tagging
Every scan now writes a complete log file at ``{run_dir}/strix.log``
captured from the moment ``run_dir`` is resolved through teardown.
Stdlib ``logging`` only — no parallel framework.

New ``strix/telemetry/logging.py``:
  * ``setup_scan_logging(run_dir, debug=)`` attaches a ``FileHandler``
    (DEBUG, all ``strix.*``) plus a ``StreamHandler`` (ERROR by
    default; DEBUG via ``STRIX_DEBUG=1``).
  * ``ContextVar``-backed ``scan_id`` and ``agent_id`` injected by a
    ``Filter`` so every line is auto-tagged across asyncio tasks
    without callers passing them explicitly.
  * Third-party noise (``httpx``, ``litellm``, ``openai``,
    ``anthropic``, ``urllib3``, ``httpcore``) capped at WARNING.
  * Returns a teardown handle for ``finally`` cleanup.

Wiring:
  * ``orchestration/scan.py`` calls ``setup_scan_logging`` once per
    scan after ``run_dir`` resolves; sets scan_id; tears down in
    ``finally``. Adds INFO logs for sandbox bring-up + scan
    start/end.
  * ``orchestration/hooks.py`` sets/clears ``agent_id`` ContextVar in
    ``on_agent_start`` / ``on_agent_end`` and emits INFO for agent
    lifecycle, DEBUG for every tool start/end and LLM call.
  * ``interface/main.py`` drops the ``setLevel(ERROR)`` silencer.

Coverage expanded across ~20 files (orchestration, agents, runtime,
llm, tools, interface, config, skills) with INFO for lifecycle and
DEBUG for verbose detail. Per the system instructions in
``logger.warning(f"…{e}")`` were converted to module logger calls.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 23:35:01 -07:00
0xallam 9d7f754b59 feat(tools): python_action — stateless Python execution with proxy helpers
Restores the legacy persistent-IPython tool's *ergonomics* (proxy
helpers pre-bound, structured stdout/stderr/error returns) without the
in-container daemon: each call ships ``strix.tools.proxy._calls`` source
into ``/tmp`` alongside a per-call driver, runs ``python3 -u`` against
it, and parses a sentinel-delimited JSON payload back from stdout. The
driver fetches its own guest token from Caido at ``localhost:48080``
and binds ``list_requests`` / ``view_request`` / ``send_request`` /
``repeat_request`` / ``scope_rules`` to that client; user code runs
inside an ``async def`` wrapper so top-level ``await`` works.

The proxy SDK call sequences live in one file —
``strix/tools/proxy/_calls.py`` — and are reused by both the host-side
``@function_tool`` wrappers (which add JSON serialization for the LLM)
and the in-container kernel (which exposes the bare async functions).
No code duplication; the helper logic itself is host-shipped, so
tweaking the proxy helpers does not require an image rebuild.

Image: a single ``pip install caido-sdk-client`` line so the driver's
``import caido_sdk_client`` resolves. Skill ``tooling/python`` is
always-loaded alongside ``tooling/agent_browser``.

Trade-off accepted: state does not persist across calls (no kernel).
For multi-step workflows the agent combines into one ``code`` block or
writes a script to ``/workspace/scratch/`` and runs via
``exec_command``. If a workflow surfaces that genuinely needs
persistence, the same tool surface migrates to a kernel-backed
executor without changing the LLM contract.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 22:58:53 -07:00
0xallam 767dc83581 chore(image): bump caido-cli v0.48.0 → v0.56.0; parametrize via CAIDO_VERSION
The pinned URL pattern (https://caido.download/releases/v<X>/caido-cli-v<X>-linux-<arch>.tar.gz)
is canonical — it's published by api.caido.io/releases/latest. HEAD requests
return 404 because the upstream R2 bucket only honors GET-with-redirect, but
the wget call in the Dockerfile uses GET so the original URL was never
actually broken — it was just stale.

Switch to an ARG so future bumps are a single --build-arg override.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 19:03:58 -07:00
0xallam 72d932f6c4 refactor: collapse strix/io/, strix/run_config_factory.py, strix/entry.py
Three top-level files that didn't earn their place:

- ``strix/io/scan_artifacts.py`` had a single consumer (the Tracer);
  collapsing it into ``strix/telemetry/`` puts it next to that consumer.
  ``strix/io/`` is gone.

- ``strix/run_config_factory.py`` held two helpers that didn't earn the
  factoring. ``make_agent_context`` was a 17-line dict-spelling function
  whose argument names were identical to its dict keys — replaced with
  inline dict literals at the two call sites. ``make_run_config`` had
  enough RunConfig assembly logic to justify a helper, but with only
  two callers (root scan + ``create_agent``) inlining is cleaner than
  keeping a top-level file. ``DEFAULT_RETRY`` moves to
  ``strix/llm/retry.py`` next to its other LLM-policy peers; the dead
  ``STRIX_DEFAULT_MAX_TURNS`` constant is dropped.

- ``strix/entry.py`` is a misnomer — it isn't *the* entry point (that's
  ``strix/interface/main.py`` for the CLI), it's the per-scan bring-up
  driver: build the bus, bring up the sandbox, build the root agent +
  child factory, format the scope-context block, register root in bus,
  open SQLiteSession, hand off to ``run_with_continuation``. That all
  lives next to its peers in ``strix/orchestration/`` now, renamed to
  ``scan.py`` so the role is obvious.

No behavior change. Net -125 LoC.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 18:54:46 -07:00
0xallam 5253332906 fix(telemetry): capture tool args in tool_executions for TUI renderers
The 19 tool renderers under strix/interface/tool_components/ all read
tool_data.get("args", {}) to render meaningful previews (URLs, methods,
note titles, vuln severities, etc.). After the SDK migration,
tracer.log_tool_start was only recording tool_name — every renderer
silently fell back to its empty-args path and the TUI lost its
per-call context.

Pull args from the SDK-native ToolContext (tool_input when parsed,
otherwise json-decode tool_arguments) and stash them on the
tool_executions entry. log_tool_start now takes an optional args dict;
existing callers pass nothing and get the empty-dict default.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 18:08:36 -07:00
0xallam 6bdaa843d9 docs(finish_scan): elevate the active-agent check to a mandatory pre-flight
Audit flagged that legacy ``finish_scan`` had a code-level guard
(``_check_active_agents``) that refused completion if any subagent was
still running or stopping. Restoring it as code would be defensive
mid-stream cancellation we don't actually want — the agent should
choose whether to wait, message, or stop each child.

Lift the responsibility to the prompt instead: docstring now opens
with a numbered pre-flight checklist that requires the agent to
``view_agent_graph`` first and refuses self-permission to call
``finish_scan`` while any peer is in ``running`` / ``waiting`` /
``llm_failed``. The model sees this as part of the tool's schema and
treats it as a hard rule (matches our pattern for similar
constraints).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 17:56:20 -07:00
0xallam 25decb0685 chore(orchestration): drop XML wrappers + close remaining audit gaps
Final pass after re-audit. Three sub-specs landed:

**XML simplification** — the legacy XML envelopes were prompt-engineering
ceremony, not parser primitives (the SDK uses native tool-calling). Drop
the verbose wrappers in favor of one-liner labeled headers. Side benefit:
fixes the unescaped-content XML-injection bug the audit caught (peer
content containing ``</content>`` no longer breaks the wrapper).

- ``_format_inter_agent_message``: ``<inter_agent_message><sender>...
  <content>...`` 9-line XML → ``[Message from {name} ({id}) | type=... |
  priority=...]\n{content}``.
- ``_render_completion_report``: ``<agent_completion_report><agent_info>
  ...<results>...`` XML → human-readable structured text with section
  headers and bulleted lists.
- ``inherited_context``: ``<inherited_context_from_parent>...`` →
  ``== Inherited context from parent (background only) ==``.

**MG1: TUI stop-agent uses graceful cancel.** ``tui.py`` was calling
``bus.cancel_descendants`` (hard, ``task.cancel()`` mid-stream) for the
stop-agent button. Switched to ``bus.cancel_descendants_graceful``, which
uses ``RunResultStreaming.cancel(mode="after_turn")`` to let each agent
finish its current turn (and save to session) before honoring the cancel.
The hard path remains in ``entry.py`` for KeyboardInterrupt where
graceful isn't possible.

**MG2: Document hook lock-free stats mutation.** Added a comment in
``hooks.on_llm_start`` explaining why ``warned_85`` / ``warned_final``
are mutated lock-free: SDK serializes ``on_llm_start`` per agent, so this
hook is the sole writer to those keys; ``record_usage`` only writes
disjoint keys (in/out/cached/calls).

**AG3: Auto-load ``coordination/root_agent`` skill for the root.**
Legacy auto-loaded the orchestration-guidance skill for root agents
only. Threaded ``is_root`` through ``render_system_prompt`` →
``_resolve_skills``; root agents now get the skill, children don't.

Skipped (per user direction): whitebox-wiki integration (CG2-4) — the
auto-injection / auto-update of the shared repo wiki was a pre-migration
feature; user opted not to restore it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 17:48:55 -07:00
0xallam f4834cd6f7 feat(orchestration): full parity with legacy harness — 8 gaps closed via SDK natives
Audit found 8 behavioral gaps between post-migration and the legacy
``BaseAgent.agent_loop``. All 8 are now closed using SDK-native
primitives — no custom workarounds, no shadow state machines.

What was broken / different:

- G1: ``inherit_context`` was dead code; children always started fresh.
- G2: TUI user message couldn't interrupt an in-flight LLM/tool turn.
- G3: ``llm_failed`` state never set; hard failures propagated as crashes.
- G4: No graceful ``stop_agent`` tool.
- G5: Parked subagents waited forever (no auto-resume timeout).
- G6: Inter-agent messages used a plain header instead of legacy XML.
- G7: Completion reports used JSON instead of legacy XML.
- G11/G12: Turn counter reset per cycle; budget warnings could re-fire.

What we did:

Bus extensions (``orchestration/bus.py``):
- ``streams`` registry + ``attach_stream`` ctx manager + ``request_interrupt``
  for SDK-native ``RunResultStreaming.cancel(mode="after_turn")``.
- ``mark_llm_failed`` + ``wait_for_user_message`` (filtered: only ``from="user"``
  satisfies; peer messages don't unstick a stuck model).
- ``stopping: set[str]`` for graceful programmatic exit.
- ``cancel_descendants_graceful`` — leaves-first via ``request_interrupt``.
- ``record_usage`` increments ``calls`` unconditionally so it doubles as the
  per-agent-lifetime turn counter (legacy ``state.iteration`` parity).
- ``warned_85`` / ``warned_final`` flags on ``stats_live`` for once-fire
  budget warnings.

Run loop rewrite (``orchestration/run_loop.py``):
- ``Runner.run`` → ``Runner.run_streamed`` with ``bus.attach_stream`` so
  cancel has a target. Catch ``(AgentsException, APIError)`` after retries
  exhaust; in interactive mode call ``mark_llm_failed`` + wait for user.
- ``UserError`` / ``MaxTurnsExceeded`` / ``CancelledError`` propagate.
- Outer loop: ``asyncio.wait_for(bus.wait_for_message, timeout=300)`` for
  interactive subagents (root waits forever). ``TimeoutError`` injects
  ``"Waiting timeout reached. Resuming execution."``.
- Honors ``bus.stopping`` at top of each iteration.

Hooks (``orchestration/hooks.py``):
- Counter source moved from per-cycle ``ctx["turn_count"]`` to
  per-lifetime ``bus.stats_live[agent_id]["calls"]``.
- Warnings guarded by once-flags — exactly-once across all cycles.

Filter (``orchestration/filter.py``):
- Restored legacy ``<inter_agent_message>`` XML envelope with the
  ``<delivery_notice>DO NOT echo back</delivery_notice>`` instruction.

Agents-graph (``tools/agents_graph/tools.py``):
- G1: ``create_agent`` reads ``ctx.turn_input`` (SDK populates it before
  tool execution at ``run_internal/turn_resolution.py:806``). Wraps as
  one ``<inherited_context_from_parent>`` block.
- G7: ``agent_finish`` emits the legacy ``<agent_completion_report>``
  XML. ``child_ctx["task"] = task`` threaded so the report echoes the
  original task.
- G4: New ``stop_agent`` tool — refuses self-stop, refuses already-
  finalized targets, ``cascade=True`` uses ``cancel_descendants_graceful``.

TUI (``interface/tui.py``):
- ``_send_user_message`` schedules ``bus.send`` AND
  ``bus.request_interrupt(target, mode="after_turn")`` — SDK finishes
  current turn cleanly, next cycle picks up the user's message.

Factory (``agents/factory.py``):
- Registered ``stop_agent`` in ``_BASE_TOOLS``.

Out of scope:
- G8 (``[ABORTED BY USER]`` marker) is auto-resolved by G2 — the SDK
  saves the full assistant message before honoring
  ``cancel(mode="after_turn")``, so partial content is preserved in the
  session.

Verified all bus behaviors with a smoke test. Lint at baseline.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 17:30:29 -07:00
0xallam 5896f25cec refactor: move `run_loop into strix/orchestration/`
Top-level ``strix/run_loop.py`` was an orphan — it owns the multi-agent
continuation loop, which is exactly the orchestration layer's job.
Moves it into ``strix/orchestration/run_loop.py`` next to the bus,
hooks, and filter — they all glue ``Runner.run`` to bus state.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 17:06:17 -07:00
0xallam 1afd1766cb feat(run-loop): lift the interactive continuation loop — applies to all agents
The previous commit only kept the root agent alive across cycles. But
``interactive`` propagates to children via ``make_child_factory``, and
the legacy harness's continuation loop applied to every interactive
agent in the tree — children also stayed alive after ``agent_finish``,
ready to receive follow-up messages from the parent or siblings.

Lift the demo-loop pattern out of ``entry.run_strix_scan`` into a
shared helper :func:`strix.run_loop.run_with_continuation` and use it
at both call sites:

- ``entry.run_strix_scan`` for the root agent.
- ``tools.agents_graph.tools.create_agent`` for child agents — the
  ``asyncio.create_task(Runner.run(...))`` becomes
  ``asyncio.create_task(run_with_continuation(...))``.

``StrixOrchestrationHooks.on_agent_end`` drops the ``parent_id is None``
constraint — any interactive agent parks instead of finalizing.
Children that crash still finalize so parents stop waiting on them.

Cancellation propagates correctly: ``bus.cancel_descendants`` cancels
the task; ``run_with_continuation``'s ``await bus.wait_for_message``
catches ``CancelledError`` and returns the last result.

Lint at baseline.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 17:02:44 -07:00
0xallam 00f5ab33d6 feat(entry): interactive mode keeps the root agent alive across cycles
Pre-migration ``BaseAgent.agent_loop`` ran forever in interactive mode,
re-entering a "waiting state" after each finish-tool call so user
follow-ups could keep the conversation going. Post-migration our
``Runner.run`` returned on ``StopAtTools(finish_scan)`` and the user's
next chat message had no listener — silent dead-end.

Restore the legacy "agent never dies" semantics using the SDK's
canonical demo-loop pattern (``agents/repl.py:run_demo_loop``):

- Add ``AgentMessageBus.wait_for_message(agent_id)`` — blocks until
  an inbox is non-empty. Backed by a per-agent ``asyncio.Event``
  fired from ``send``.
- Add ``AgentMessageBus.park(agent_id)`` — sets status to ``waiting``
  without finalizing (inbox + tree edges + name preserved). Lets
  ``send`` keep accepting messages between cycles.
- Plumb ``interactive`` through ``make_agent_context`` and the
  ``create_agent`` graph tool (children inherit).
- ``StrixOrchestrationHooks.on_agent_end`` parks the root agent
  instead of finalizing when ``interactive=True`` and the run
  completed cleanly. Resets ``agent_finish_called`` /
  ``turn_count`` for the next cycle.
- ``entry.run_strix_scan`` adds an outer loop in interactive mode:
  after ``Runner.run`` returns, ``await bus.wait_for_message(root_id)``,
  drain pending user messages, and re-invoke ``Runner.run``. SQLite
  session preserves prior conversation across cycles.

For non-interactive (CLI) mode: unchanged — single ``Runner.run``,
return.

Verified bus behaviors: wait returns immediately on pre-existing
message, blocks then wakes on send, ``park`` keeps agent send-able,
``finalize`` evicts. Lint at baseline (3 ruff / 69 mypy).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 16:54:36 -07:00
0xallam fc96716956 refactor(agents-graph): drop redundant `agent_finish_called` set
``agent_finish`` was setting ``inner[\"agent_finish_called\"] = True``
at the top of its body, but ``StrixOrchestrationHooks.on_tool_end``
already does this for ``agent_finish`` and ``finish_scan`` after the
tool returns. Doing it twice was harmless but suggested the flag's
ownership was ambiguous; the hook is the single source of truth.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 16:29:51 -07:00
0xallam 8f1f473eb8 refactor(telemetry): extract scan artifact I/O into `strix.io.scan_artifacts`
The 150-line ``Tracer.save_run_data`` mashed three concerns together:
opening file handles, formatting Markdown for vulnerabilities, and
writing the executive penetration-test report. None of that is
telemetry — it's pure on-disk artifact emission.

Extract to :class:`ScanArtifactWriter` in ``strix/io/scan_artifacts.py``:

- One writer per ``run_dir``, owns its own ``_saved_vuln_ids`` dedupe
  set so re-saves only emit new files.
- ``writer.save(vulnerability_reports=, final_scan_result=)`` is the
  only public entry point.
- ``_render_vulnerability_md`` is module-private and unit-testable in
  isolation.

``Tracer`` now lazily creates a single ``ScanArtifactWriter`` per
``run_dir`` and delegates ``save_run_data`` to it (~150 LoC body
collapses to ~10).

Net: tracer.py 422 → 327 LoC; new scan_artifacts.py 196 LoC. About
−95 LoC of mixed concerns, plus telemetry no longer carries file-I/O
responsibilities.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 16:28:07 -07:00
0xallam 494e6fab0d fix(telemetry): restore broken `log_tool_start / log_tool_end` interface
Audit found ``hooks.on_tool_start`` / ``on_tool_end`` were calling
``tracer.log_tool_start`` / ``log_tool_end`` via ``hasattr()`` checks —
but those methods didn't exist on ``Tracer``. The ``hasattr()`` always
returned False, so the calls were silently no-ops, leaving
``tracer.tool_executions`` permanently empty.

Four TUI render paths consume that dict and were therefore broken:

- ``_get_agent_name_for_vulnerability`` always returned ``None`` (vuln
  panel couldn't show which agent reported the finding).
- ``_agent_has_real_activity`` always returned ``False`` (animation
  logic stopped immediately).
- ``_agent_vulnerability_count`` always returned ``0``.
- ``_gather_agent_events`` only showed chat events, never tool events.

Fix: add ``Tracer.log_tool_start(agent_id, tool_name) → exec_id`` and
``Tracer.log_tool_end(agent_id, tool_name, result)``. Hook bodies now
call them directly (no ``hasattr`` guard). The exec-id counter ensures
nested / overlapping tool calls within an agent don't clobber each
other.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 16:24:45 -07:00
0xallam 95865401ae refactor: lift hardcoded model default + fix stale `is_whitebox` docstring
``"anthropic/claude-sonnet-4-6"`` was duplicated as a kwarg default in
5 places (``run_strix_scan``, ``make_run_config``, ``make_agent_context``,
and twice in ``agents_graph.create_agent``'s ``inner.get(..., default)``
calls). The default was actually dead code: ``validate_environment``
requires ``STRIX_LLM`` to be set before any scan starts, and the CLI/TUI
callers don't pass ``model=`` themselves.

Replaced with a single resolution in ``run_strix_scan``:

    resolved_model = model or load_settings().llm.model
    if not resolved_model:
        raise RuntimeError("No LLM model configured. ...")

then propagated explicitly to ``make_agent_context`` and
``make_run_config``. Both lose their string defaults — ``model`` is now
a required kwarg. The graph tool's ``inner.get("model", "...")`` is
``inner["model"]``: the parent context guarantees it's set.

Drive-by: ``run_strix_scan`` docstring still listed ``is_whitebox`` as
a ``scan_config`` key — stale since ``1e641e5`` derived it from
``targets`` instead. Updated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 16:10:54 -07:00
0xallam 1e641e56ce refactor(config): pydantic-settings revamp + drop `is_whitebox` plumbing
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``.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 16:05:40 -07:00
0xallam 346cc477a7 chore(image): drop sidecar/Playwright legacy + plug NO_PROXY hole
Dockerfile carried forward three pieces of dead state from the
pre-migration era:

- ``/app/runtime`` and ``/app/tools`` mkdir entries — the FastAPI
  sidecar + in-container tool registry that those dirs hosted are
  gone.
- ``/home/pentester/{configs,wordlists,output,scripts}`` — empty
  placeholders never populated by anything; greps for them in the
  whole repo come back empty.
- ~20 explicit Chrome/Playwright runtime libs (``libnss3``,
  ``libnspr4``, ``libatk*``, ``libxcomposite1``, …) plus emoji /
  freefont packages. These were Playwright deps; the migration to
  ``agent-browser`` runs ``agent-browser install --with-deps`` which
  owns this list authoritatively. Keep ``libnss3-tools`` for
  ``certutil`` in the entrypoint's CA-trust step.

Drive-by bug fix: ``NO_PROXY=localhost,127.0.0.1`` was set in the
entrypoint (``/etc/profile.d/proxy.sh`` + ``/etc/environment``) but
NOT in the SDK manifest's environment. ``docker exec``-spawned
processes (which ``session.exec`` and the Shell capability use)
inherit only manifest env, so ``agent-browser``'s CDP-localhost
traffic was being looped back through Caido. Add it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 15:28:48 -07:00
0xallam 8a11f9dab5 refactor(dedupe): route through MultiProvider + cache wrapper + retry policy
``check_duplicate`` was calling ``litellm.completion(...)`` directly
via ``resolve_llm_config()``, bypassing every layer the main agent
loop runs through:

- :class:`MultiProvider` (so ``anthropic/...`` aliases never went
  through :class:`AnthropicCachingLitellmModel` and missed the
  ``cache_control`` patching on the system prompt — 4x cost on
  repeated dedupe calls within the same scan).
- :data:`DEFAULT_RETRY` (no retry on 429s / network blips — the
  caller's broad except-and-fallback was hiding this).

Switch to the SDK's :meth:`Model.get_response` directly: same model
selection, same retry policy, same cache wrapper. Extract assistant
text from ``ModelResponse.output`` via the canonical
``ResponseOutputMessage`` walk.

``check_duplicate`` is now async — drops the ``asyncio.to_thread``
indirection in ``_do_create``. Validation logic is fast-sync; running
it on the event loop is fine.

Drive-by: rename ``_DEFAULT_RETRY`` → ``DEFAULT_RETRY`` in
``run_config_factory`` so the dedupe path can reuse the same constant
without reaching into a private name.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 15:25:44 -07:00
0xallam b3f7cfd040 refactor: nuke `strix_tool` shim + dead package re-exports
``@strix_tool`` was passing through every kwarg to ``@function_tool``
with the same defaults — zero Strix-specific value-add. The docstring
also still claimed terminal/browser/python tools opted into
``timeout_behavior="raise_exception"``, but those tools were all
deleted in the recent migrations.

- Replace 30 ``@strix_tool(...)`` callsites with ``@function_tool(...)``.
- Inline ``dump_tool_result(x)`` as ``json.dumps(x, ensure_ascii=False,
  default=str)`` at all 64 callsites — no helper.
- Delete ``strix/tools/_decorator.py``.

Drive-by: gut dead package re-exports.

- ``strix/{agents,orchestration,tools}/__init__.py`` re-exported
  symbols nobody imports via the package — every consumer uses deep
  paths (``from strix.agents.factory import build_strix_agent``).
- The 8 ``strix/tools/<sub>/__init__.py`` re-exports only fed the
  splat ``from .agents_graph import *`` etc. in the parent package
  init, which is also gone now.
- Reduced to docstrings (or empty) so ``import strix.tools`` doesn't
  drag every tool's transitive deps in eagerly.

Drive-by: drop dead helpers in ``runtime.session_manager``
(``cached_scan_ids``, ``_reset_cache_for_tests``) — zero callers since
``tests/`` was nuked in ``a6d578c``.

Verified all tool timeouts preserved (think=10, list_requests=120,
finish_scan=60, web_search=330) and ruff/mypy at baseline.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 15:17:46 -07:00
0xallam 6990fd4ef1 feat(runtime): pluggable sandbox backend registry
``STRIX_RUNTIME_BACKEND`` was already declared on ``Config`` but never
read — ``session_manager`` hard-coded ``StrixDockerSandboxClient`` plus
``DockerSandboxClientOptions`` plus ``docker.from_env()`` directly into
the call site. Adding a second backend would have meant retrofitting
every Docker-specific import.

Move all of that behind a registry:

- ``strix/runtime/backends.py``: maps backend names to async factories
  ``(image, manifest, exposed_ports) -> (client, session)``. Ships with
  ``"docker"``; ``register_backend`` lets downstream users plug in
  Daytona / K8s / Modal / etc. without forking.
- Each backend's deps are imported lazily inside its factory, so a
  K8s-only deployment doesn't need ``docker-py`` installed (and
  vice-versa).
- ``session_manager`` reads the config name, looks up the backend,
  calls it. Zero Docker imports remain.
- Unknown backend name raises ``ValueError`` with the supported list,
  so ``STRIX_RUNTIME_BACKEND=docke`` typos surface immediately.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 15:02:51 -07:00
0xallam fe5f749e13 refactor: rename `strix_docker_client.pydocker_client.py`
The ``strix`` prefix on a file inside ``strix/runtime/`` was pure
redundancy. Class name ``StrixDockerSandboxClient`` keeps the prefix
since it disambiguates from the upstream SDK class it subclasses.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 14:59:00 -07:00
0xallam 295d43b3ab refactor: collapse strix/sandbox into strix/runtime; in-sandbox Caido bootstrap
The split between ``strix/sandbox/`` and ``strix/runtime/`` was
artificial — both were managing the same backend. ``strix/sandbox/``
also collided uncomfortably with the SDK's ``agents.sandbox.*``
namespace. ``runtime/`` (which matches ``STRIX_RUNTIME_BACKEND``) is
the canonical home for everything Docker / Daytona / K8s lifecycle.

While merging, also rip out two pieces of Docker-specific coupling:

- ``caido_bootstrap`` was POSTing ``loginAsGuest`` from the host via
  ``aiohttp`` to ``http://127.0.0.1:{forwarded_port}``. That assumed
  Docker port forwarding; Daytona / K8s expose ports differently.
  Now we ``session.exec`` curl from *inside* the container — the
  SDK's runtime-agnostic exec primitive — so any backend works as
  long as it implements ``exec``. The host-side Caido ``Client``
  still uses the runtime's exposed-port URL for post-bootstrap calls,
  but that goes through the SDK's own ``resolve_exposed_port``
  abstraction (also runtime-agnostic).

- The bootstrap retry loop now doubles as the readiness probe, so
  ``healthcheck.wait_for_tcp_ready`` (and the entire
  ``healthcheck.py`` module) goes away.

Drive-by simplification: drop ``caido_host_port`` plumbing entirely.
It was only piped through ``make_agent_context`` → child contexts
without ever being read; only ``caido_client`` is consumed.

Drops ``aiohttp`` runtime dep (it stays only as a transitive of the
Caido SDK).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 14:55:44 -07:00
0xallam 5d8436cbbb chore: nuke post-migration dead code, deps, and broken Dockerfile fallback
- Drop ``wait_for_http_ready`` (FastAPI sidecar healthcheck) — only Caido
  TCP probe survives now. Removes the ``httpx`` import.
- Delete ``ListSitemapRenderer`` / ``ViewSitemapEntryRenderer`` — render
  UI for tools that disappeared with the Caido SDK migration.
- Drop ``scrubadub`` runtime dep — PII sanitizer was nuked previously
  but the dep stayed; resolve strips 18 transitives (numpy, scipy,
  scikit-learn, nltk, faker, …).
- Drop empty ``[project.optional-dependencies] sandbox`` section — last
  in-container Python dep migrated out.
- Drop unused mypy overrides (``pydantic_settings``, ``jwt``, ``gql``,
  ``scrubadub``, ``httpx``) and the stale ``fastapi`` isort group.
- Collapse Dockerfile's ``pipx install -r ... 2>/dev/null || venv``
  fallback into a direct venv install — pipx never accepted ``-r`` so
  the fallback was always firing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 14:46:33 -07:00
0xallam ab3da5c0b0 docs(skill): document the agent-browser → view_image chain for screenshots
The vendored agent-browser skill described the ``screenshot``
subcommand but didn't tell the model how to actually look at the
resulting PNG. ``agent-browser screenshot`` writes to disk; the
SDK's ``view_image`` (from the ``Filesystem`` capability we already
enable on the agent) is what loads the bytes back as multimodal
content.

Add the explicit two-step pattern:

  exec_command:  agent-browser screenshot /workspace/page.png
  view_image:    {"path": "/workspace/page.png"}

Plus a guidance note that ``snapshot -i`` (text accessibility tree at
~200-400 tokens) is the cheap default and screenshots are for cases
where pixels actually matter — visual layout, captchas, custom
widgets where the a11y tree is incomplete.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 14:38:13 -07:00
0xallam cd1bb46d50 chore: final cleanup — drop `STRIX_SANDBOX_MODE / strix_disable_browser` / runtime docstring
Tail end of the sandbox-tools migration:
- Drop ``ENV STRIX_SANDBOX_MODE=true`` and ``ENV PYTHONPATH=/app`` from
  the Dockerfile — both only mattered for the now-deleted in-container
  tool server (the legacy ``register_tool`` registry gated on the env
  var, and the entrypoint set ``PYTHONPATH`` so it could ``-m
  strix.runtime.tool_server``).
- Drop ``strix_disable_browser`` from the Config defaults — the legacy
  registry used it to skip ``browser_action`` registration; agent-browser
  is unconditional now.
- Strip the ``tool_server.py`` blurb from ``strix/runtime/__init__.py``.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 14:35:55 -07:00
0xallam 2c2ab13c8f refactor: SandboxAgent + SDK Shell/Filesystem; agent-browser CLI; nuke FastAPI sidecar
Combined commits 2+3 of the migration plan because the FastAPI sidecar
removal in commit 2 broke ``browser_action`` (which lived in the
sidecar); they have to land together.

Sandbox tool layer (commit 2 piece):
- ``build_strix_agent`` now returns a ``SandboxAgent`` with
  ``capabilities=[Filesystem(), Shell()]``. The SDK runtime binds the
  capabilities to the live sandbox session per-run; agents get
  ``exec_command``, ``write_stdin``, ``apply_patch``, ``view_image``
  function tools auto-merged into their tool list. Plain ``Agent``
  short-circuits capability binding (``agents/sandbox/runtime.py:190``).
- Drop ``Compaction`` from the default capability set — it's
  OpenAI-Responses-API-only and useless for our litellm-routed
  Anthropic setup.
- Delete the entire custom in-container tool layer:
  - ``strix/tools/terminal/`` (5 files, 748 LoC libtmux)
  - ``strix/tools/file_edit/`` (3 files, 276 LoC)
  - ``strix/tools/python/`` (5 files, 459 LoC)
  - ``strix/runtime/tool_server.py`` (163 LoC FastAPI sidecar)
  - ``strix/tools/_sandbox_dispatch.py`` (117 LoC)
  - ``strix/tools/registry.py`` (109 LoC)
  - ``strix/tools/context.py`` (12 LoC)
- Drop the corresponding TUI renderers (``terminal_renderer.py``,
  ``file_edit_renderer.py``, ``python_renderer.py``) and update
  ``interface/tool_components/__init__.py``.

Browser → agent-browser CLI (commit 3 piece):
- Install ``agent-browser@0.26.0`` globally in the Dockerfile right
  after the existing ``npm install -g`` block. Run
  ``agent-browser install --with-deps`` (apt, root) and
  ``agent-browser install`` (Chrome download, pentester) +
  ``agent-browser doctor --offline --quick`` smoke test.
- Drop the explicit Playwright system-deps apt list (replaced by
  ``--with-deps``) and ``RUN .venv/bin/python -m playwright install
  chromium``.
- Vendor ``agent-browser/skill-data/core/SKILL.md`` →
  ``strix/skills/tooling/agent_browser.md`` (476 lines). Adapt
  frontmatter to Strix format; strip the install/Quickstart and the
  ``agent-browser skills get electron|slack|...`` specialized-skills
  block; add the "Caido proxy is wired via env vars; do not pass
  ``--proxy``" note.
- ``_resolve_skills`` now eagerly loads ``tooling/agent_browser`` for
  every agent (matches the previous unconditional ``browser_action``
  in ``_BASE_TOOLS``).
- Delete ``strix/tools/browser/`` (5 files, 1338 LoC) and the
  ``browser_renderer.py`` TUI render.

Sandbox plumbing:
- Drop ``bearer`` token, ``tool_server_host_port`` resolution + bundle
  keys, ``TOOL_SERVER_TOKEN``/``TOOL_SERVER_PORT``/
  ``STRIX_SANDBOX_EXECUTION_TIMEOUT`` from the manifest env in
  ``session_manager.create_or_reuse``. Caido proxy env vars
  (``http_proxy``, ``https_proxy``, ``ALL_PROXY``) stay; manifest
  applies them to every ``docker exec``-spawned process.
- Drop ``sandbox_token`` and ``tool_server_host_port`` params from
  ``make_agent_context`` and the ``create_agent`` graph tool.
- Drop the tool-server health-check from ``entry.py`` (only Caido's
  ``wait_for_tcp_ready`` remains).
- ``docker-entrypoint.sh``: delete the ~30 line
  ``Starting tool server...`` block (sudo + uvicorn launch + curl
  /health poll). Add ``NO_PROXY=localhost,127.0.0.1`` to
  ``/etc/profile.d/proxy.sh`` and ``/etc/environment`` so the
  agent-browser daemon's CDP traffic on localhost isn't routed
  through Caido.

pyproject.toml:
- ``[project.optional-dependencies] sandbox = []`` (every member of
  the previous list — fastapi, uvicorn, ipython, openhands-aci,
  playwright, libtmux — is gone with the sidecar).
- Drop ``numpydoc.*``, ``IPython.*``, ``openhands_aci.*``,
  ``playwright.*``, ``uvicorn.*``, ``pyte.*``, ``libtmux.*`` from
  the missing-imports module list.
- Drop the per-file ruff ignores for the deleted modules.

Net delta: −5512 LoC. ruff drops to 3 errors (was 21 baseline). mypy
falls to 69 errors over 3 files (was 84 over 8 — the drop comes from
deleting the modules with the worst untyped-import problems).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 14:33:38 -07:00
0xallam 5449af2456 refactor: Caido — replace ProxyManager with caido-sdk-client (host-side)
Drop our 797-LoC manual GraphQL ``ProxyManager`` and the in-container
sandbox dispatch. Caido goes host-side via the official async Python
SDK. The Caido CLI still runs as a sidecar in the container — only the
control-plane moves.

Bootstrap moves host-side:
- New ``strix/sandbox/caido_bootstrap.py``: ``loginAsGuest`` via
  aiohttp (5 retries), then ``client.project.create(temporary=True)``
  + ``client.project.select(...)``, then return the connected
  ``caido_sdk_client.Client``. Drop the equivalent bash from
  ``docker-entrypoint.sh`` (~60 lines of curl + jq).
- ``entry.py`` calls ``bootstrap_caido_client`` after the
  ``wait_for_tcp_ready`` healthcheck, stashes the client in the bundle
  and threads it through ``make_agent_context(caido_client=...)``.
  ``agents_graph.create_agent`` propagates the same client to children.
- ``session_manager.cleanup`` ``await``s ``client.aclose()`` before
  tearing down the container.
- Drop ``CAIDO_PORT`` from the manifest env (only the in-container
  ProxyManager read it) and ``CAIDO_API_TOKEN`` from the entrypoint's
  ``/etc/profile.d/proxy.sh`` + ``/etc/environment`` heredocs.

Tools (``strix/tools/proxy/tools.py``):
- ``list_requests`` → ``client.request.list().filter().first().after()``
  with ascending/descending order. **Pagination changes from
  start_page/end_page (1-indexed) to first/after cursors** matching the
  SDK's native shape; response includes ``page_info.end_cursor`` for
  the model to thread.
- ``view_request`` → ``client.request.get(id, RequestGetOptions(...))``;
  decode raw bytes locally; existing regex-search and line-pagination
  modes preserved.
- ``send_request`` → synthesize raw HTTP bytes, parse URL into
  ``ConnectionInfoInput(host, port, is_tls)``, create a replay session
  via ``client.replay.sessions.create(CreateReplaySessionFromRaw(...))``,
  then ``client.replay.send(session_id, ReplaySendOptions(...))``.
- ``repeat_request`` → ``client.request.get(id, request_raw=True)`` →
  port the existing parse/_apply_modifications/build helpers verbatim →
  send via the same replay flow as ``send_request``.
- ``scope_rules`` → direct mapping to ``client.scope.{list, get, create,
  update, delete}``.
- **Drop ``list_sitemap`` + ``view_sitemap_entry``** — the official SDK
  has no sitemap module. The model uses HTTPQL filters
  (``req.host.eq:"X" AND req.path.cont:"/api/"``) for the same
  drill-down workflow.

Deletions:
- ``strix/tools/proxy/proxy_manager.py`` (797 LoC)
- ``strix/tools/proxy/proxy_actions.py`` (113 LoC)
- The 6-line proxy_actions pre-import in ``python_instance.py``
  (broken once proxy_actions is gone; that file is queued for deletion
  in commit 2 anyway).

Deps:
- Add ``caido-sdk-client>=0.2.0`` and ``aiohttp>=3.10.0`` to runtime
  ``[project] dependencies``.
- Drop ``gql[requests]>=3.5.3`` from ``[project.optional-dependencies]
  sandbox`` — only the in-container ProxyManager used the sync transport
  variant; the SDK pulls in ``gql[aiohttp]`` transitively for us.
- ``[[tool.mypy.overrides]]``: add ``caido_sdk_client.*`` and
  ``aiohttp.*`` to the missing-imports list with
  ``disable_error_code=["import-untyped"]`` (neither ships ``py.typed``).
- ``[tool.ruff.lint.per-file-ignores]``: bump the proxy/tools.py
  ignore to also include ``PLR0911`` (the scope_rules action dispatcher
  has many short-circuit returns).

ruff drops from 21 → 12 errors; mypy moves from 82 → 84 (the +2 are in
already-flaky files unrelated to this change). All touched files mypy
clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 14:23:56 -07:00
0xallam 9b31e9fd29 refactor: nuke `events.jsonl` pipeline and the unused PII sanitizer
The JSONL trace sink was never read — TUI consumes ``Tracer`` state
directly (chat_messages, agents, tool_executions, vulnerability_reports,
LLM stats), and SQLiteSession owns the conversation history. The whole
``StrixTracingProcessor`` → ``_emit_event`` → ``append_jsonl_record``
pipeline was producing files nothing opens.

Deleted:
- ``strix/telemetry/strix_processor.py`` (the SDK ``TracingProcessor``).
- ``strix/telemetry/utils.py`` — ``TelemetrySanitizer`` (no remaining
  callers), ``append_jsonl_record``, ``get_events_write_lock``,
  ``reset_events_write_locks``.
- ``strix/telemetry/flags.py`` — ``is_telemetry_enabled`` /
  ``is_posthog_enabled`` collapsed into a 4-line check inside
  ``posthog._is_enabled`` (its only caller).
- ``Tracer._emit_event`` and every event-emit call inside the tracer
  (``run.started``, ``run.configured``, ``run.completed``,
  ``finding.created``, ``finding.reviewed``, ``chat.message``).
- ``Tracer._enrich_actor`` (only used by ``_emit_event``).
- ``Tracer._sanitize_data`` + ``_sanitizer`` field (PII scrub only ran
  on JSONL events).
- ``Tracer.events_file_path`` property and the ``_events_file_path`` /
  ``_telemetry_enabled`` / ``_run_completed_emitted`` /
  ``_next_execution_id`` fields.
- ``Tracer._calculate_duration`` (one caller in posthog — inlined).
- ``add_trace_processor(StrixTracingProcessor(run_dir))`` from
  ``entry.py``.

The ``Tracer`` class is now ~275 LoC of pure runtime state for the TUI
+ vulnerability artifact writer (markdown / CSV / pentest report).
Conversation history goes to ``SQLiteSession``; SDK trace events are
not persisted.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 13:47:37 -07:00
0xallam df51eeedd0 refactor: flatten CaidoCapability into direct wiring
The custom ``Capability`` subclass was 207 LoC bundling four tiny
concerns (env-var injection, tool exposure, system-prompt block,
healthcheck) — and three of them were dead code: the SDK's
``SandboxRunConfig`` doesn't accept capabilities, so
``process_manifest``, ``tools()``, and ``instructions()`` were never
called. Only ``bind()`` ran, because we invoked it manually.

Replace each piece with the obvious direct equivalent:

- **Env vars**: inject ``http_proxy`` / ``https_proxy`` / ``ALL_PROXY``
  directly into the manifest in ``session_manager.create_or_reuse``.
  This *also fixes a latent bug* — the proxy env vars in
  ``CaidoCapability.process_manifest`` weren't being applied to live
  containers, so shelled-out HTTP traffic from terminal/python tools
  wasn't actually flowing through Caido.
- **Tool exposure**: add the seven Caido tools (``list_requests``,
  ``view_request``, ``send_request``, ``repeat_request``,
  ``scope_rules``, ``list_sitemap``, ``view_sitemap_entry``) to
  ``_BASE_TOOLS`` in ``agents/factory.py`` like every other sandbox
  tool. They were already defined in ``tools/proxy/tools.py``.
- **Healthcheck**: ``entry.py`` now ``await``s
  ``wait_for_http_ready`` + ``wait_for_tcp_ready`` inline after
  ``session_manager.create_or_reuse`` returns, before any agent runs.
  No more capability state, ``configure_host_ports`` plumbing, or
  ``on_agent_start`` await-the-task indirection.
- **Instructions block**: dropped. The seven proxy tools' docstrings
  cover the HTTPQL syntax and usage already; the duplicate prompt
  fragment was overhead.

Cascade cleanups:
- Drop ``caido_capability`` from the agent context (was passed to
  every ``make_agent_context`` call but only used by the now-deleted
  ``on_agent_start`` await).
- Strip the capability await branch from
  ``StrixOrchestrationHooks.on_agent_start``; that hook now does only
  the ``tracer.agents`` mirroring it always should have.
- Drop the ``capability`` key from the session bundle.
- Drop ``strix/sandbox/caido_capability.py`` — entire file (207 LoC).
- Drop the per-file ruff ignore for the deleted file.

mypy clean on every touched file. Net -217 LoC.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 13:32:11 -07:00
0xallam 12baf2d792 refactor: lean on SDK for tracing + native session resume; nuke OTEL/Traceloop
The SDK ships its own tracing pipeline (``agents.tracing``) plus
``SQLiteSession`` for native conversation persistence. Strix's custom
OTEL bootstrap + Traceloop integration was dead weight — the SDK does
not bridge to OpenTelemetry, so all of our adapter code was solving a
problem we didn't actually need solved.

Telemetry purge:
- Drop the ``traceloop-sdk`` and
  ``opentelemetry-exporter-otlp-proto-http`` runtime deps. ``uv sync``
  uninstalls ~30 transitive packages (the OTEL family,
  ``traceloop-sdk``, ``protobuf``, ``opentelemetry-exporter-otlp-*``,
  ``deprecated``, ``wrapt``, ``backoff``, etc.) — about 1000 lines off
  ``uv.lock``.
- Delete ``bootstrap_otel`` and ``JsonlSpanExporter`` from
  ``telemetry/utils.py``; strip the OTEL pruning helpers,
  ``parse_traceloop_headers``, ``default_resource_attributes``,
  ``format_trace_id`` / ``format_span_id`` / ``iso_from_unix_ns``.
  Keep only the sanitizer + JSONL writer + write-lock registry.
- Strip ``Tracer._setup_telemetry``, ``_otel_tracer``,
  ``_remote_export_enabled``, ``_active_events_file_path``,
  ``_active_run_metadata``, ``_get_events_write_lock``,
  ``_set_association_properties``. ``_emit_event`` now generates
  trace/span ids from ``uuid4`` directly.
- Drop the ``traceloop_base_url`` / ``traceloop_api_key`` /
  ``traceloop_headers`` / ``strix_otel_telemetry`` config knobs.
- Rename ``is_otel_enabled`` → ``is_telemetry_enabled`` (the gate now
  controls JSONL emission only).

Native session resume:
- ``entry.py`` now constructs an ``agents.memory.SQLiteSession`` keyed
  by ``scan_id`` and persists conversation history at
  ``strix_runs/<scan_id>/session.db``. A second call to
  ``run_strix_scan`` with the same ``scan_id`` resumes from where the
  prior run left off — no manual state plumbing needed.

Tracer.agents fix (TUI agent tree was silently empty):
- ``StrixOrchestrationHooks.on_agent_start`` now mirrors bus state
  into ``tracer.agents`` (id / name / parent_id / status), and
  ``on_agent_end`` flips the entry to ``completed`` / ``crashed``.
  The TUI now actually shows the agent tree during scans.

Tooling:
- Drop ``pylint`` from dev deps; ``ruff`` covers everything we used
  it for. Strip the ``make lint`` pylint step.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 13:18:21 -07:00
0xallam 28416c5ae9 chore: drop unused pydantic[email] extra
No imports of EmailStr or pydantic.networks; dropping the
extra removes email-validator, dnspython, and idna as
transitives.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 13:07:02 -07:00
0xallam b65e4ebd52 chore: drop unused dependencies
Runtime deps (``[project] dependencies``):
- ``litellm[proxy]>=1.83.0`` — ``openai-agents[litellm]==0.14.6``
  already pulls litellm as a transitive (currently 1.83.7), and we
  only use ``litellm.completion()``, not the proxy server extras.
- ``defusedxml>=0.7.1`` — leftover from the XML tool-call era; zero
  imports remain.

Sandbox deps (``[project.optional-dependencies] sandbox``):
- ``pyte>=0.8.1`` — zero imports.
- ``numpydoc>=1.8.0`` — zero imports.

Optional groups:
- Drop the entire ``vertex`` group (``google-cloud-aiplatform``);
  routing goes through litellm/MultiProvider, no direct Google Cloud
  usage.

Dev deps (``[dependency-groups] dev``):
- ``black>=25.1.0`` — never invoked; ruff format does it and is what
  pre-commit + Makefile actually call.
- ``isort>=6.0.1`` — never invoked; ruff's ``I`` lint set handles
  imports. (pylint pulls isort transitively, so functionality is
  preserved.)

ruff (27) and mypy (82) baselines unchanged; ``uv sync`` uninstalls
~15 packages.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 13:06:41 -07:00
0xallam a6d578c4a8 chore: nuke tests/ and the entire test toolchain
The test suite was carrying migration scars and a long tail of
low-density assertions over SDK-derived behavior. Drop it wholesale.

- Delete ``tests/`` (42 files, ~4900 LoC).
- Drop ``pytest`` / ``pytest-asyncio`` / ``pytest-cov`` /
  ``pytest-mock`` from the dev dependency group; ``uv sync``
  uninstalls the matching wheels.
- Strip the pytest + coverage config blocks, the
  ``flake8-pytest-style`` ruff selector, the ``tests/**`` per-file
  ignores, the ``[tool.mypy.overrides] tests.*`` block, and the
  ``"tests"`` entry from bandit's ``exclude_dirs``.
- Drop the ``test`` / ``test-cov`` Makefile targets; ``dev`` no
  longer depends on tests.
- Strip the ``# Testing`` block from ``.gitignore`` (``.coverage``,
  ``.pytest_cache/``, ``htmlcov/``, ``coverage.xml``, ``nosetests.xml``,
  ``.tox/``, ``.hypothesis/``).

ruff (27) and mypy (82) baselines unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 13:01:20 -07:00
0xallam 49c38de3b2 refactor: dedupe `_dump` helper, collapse retry-policy plumbing, scrub test scars
Tools:
- Add a single ``dump_tool_result`` helper in ``tools/_decorator.py``
  and remove the eight identical ``_dump`` definitions from
  ``proxy/tools.py``, ``file_edit/tools.py``, ``python/tool.py``,
  ``terminal/tool.py``, ``todo/tools.py``, ``browser/tool.py``,
  ``notes/tools.py``, ``agents_graph/tools.py``. Imports trimmed.
  Net -50 LoC across the tool modules.

run_config_factory:
- Inline the four retry-policy plumbing pieces
  (``_RETRYABLE_HTTP_STATUSES``, ``_DEFAULT_MAX_RETRIES``,
  ``_DEFAULT_BACKOFF``, ``_default_retry_policy()``) into a single
  module-level ``_DEFAULT_RETRY`` ``ModelRetrySettings`` literal. The
  inputs were never overridden and the helper had one caller.

Tests:
- Drop migration scars from ``tests/test_run_config_factory.py``
  (``Phase 1`` / ``C1`` / ``C11`` / ``C21`` / ``HARNESS_WIKI`` / ``AUDIT``
  references). Replace the ``_RETRYABLE_HTTP_STATUSES``-touching test
  with a ``retry.policy is not None`` smoke check now that the constant
  has been inlined.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 12:54:44 -07:00
0xallam d959fe2163 refactor: collapse dual stat buckets, prune unused params, kill dead helpers
Tracer:
- Collapse the ``live`` / ``completed`` LLM stat buckets into one
  flat dict. The ``completed`` bucket was only ever written by tests
  — production never moved stats across, and ``get_total_llm_stats``
  always summed both for display.
- Drop ``record_llm_usage(agent_id=...)``: argument was unused, and
  the per-call ``bucket=`` knob is gone with the buckets.

run_config_factory:
- Drop unused ``parallel_tool_calls``, ``tool_choice`` parameters
  from ``make_run_config`` — no caller ever overrode them.
- Drop ``agent_name`` from ``make_agent_context`` — set into the
  context dict but no consumer ever read it; the bus's ``names`` map
  is the source of truth.

Wire reasoning_effort through:
- ``Config.get("strix_reasoning_effort")`` is now actually plumbed
  to ``make_run_config`` from ``entry.py``. Previously the env var
  was advertised but never consumed.

Multi-agent graph tools:
- Replace six copies of
  ``inner = ctx.context if isinstance(ctx.context, dict) else {}``
  with a single ``_ctx(ctx)`` helper.

Todo tools:
- Lift the duplicated ``priority_order`` / ``status_order`` dicts
  to module-level ``_PRIORITY_RANK`` / ``_STATUS_RANK`` and replace
  both inline sort lambdas with ``_todo_sort_key``.

Notes tools:
- Delete ``append_note_content`` (and its test): docstring claimed
  it was for an "agents-graph wiki-update hook on agent_finish" that
  was never wired up. Pure dead public API.

Style:
- Drop the ``del ctx`` no-ops from notes / reporting / web_search
  tools. ``ARG001`` is already silenced project-wide for tool
  modules; the ``del`` was cargo-culted.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 12:44:48 -07:00
0xallam f08ad2a634 refactor: nuke gratuitous XML serialization + delete argument_parser
Argument parser:
- Delete ``strix/tools/argument_parser.py`` and its tests. The SDK
  validates and types tool arguments via Pydantic before they hit our
  wrappers, and the in-container tool server receives JSON-typed
  kwargs over the wire. The string-coercion belt-and-suspenders is no
  longer pulling its weight.

XML → JSON / typed structures:
- ``create_vulnerability_report``: ``cvss_breakdown`` is now a
  ``dict[str, str]`` of the 8 metrics; ``code_locations`` is a
  ``list[dict]``. No more XML parsing in the tool or the renderer.
- ``check_duplicate``: the dedup judge now emits a single JSON object
  instead of an ``<dedupe_result>`` block. Strict JSON parser handles
  optional code-fence wrappers.
- ``agent_finish``: completion report posted to the parent inbox is a
  JSON object (``kind``, ``from``, ``agent_id``, ``success``,
  ``summary``, ``findings``, ``recommendations``) rather than a
  hand-rolled ``<agent_completion_report>`` XML envelope.
- ``create_agent``: identity preamble + inherited-context markers are
  plain bracketed labels rather than ``<agent_delegation>`` /
  ``<inherited_context_from_parent>`` envelopes.
- ``inject_messages_filter``: peer messages get a
  ``[Message from agent <id> | type=... | priority=...]`` header line
  instead of an ``<inter_agent_message>`` envelope.
- Crash + system-warning messages: bracketed labels, no XML.
- System prompt: the inter-agent block now describes the new header
  format and drops the "never echo XML envelope" rule.
- ``strix/llm/utils.py``: deleted. ``clean_content`` collapsed into a
  one-line blank-line normalizer in the agent-message renderer (the
  XML envelope scrub had nothing left to scrub).

Tests updated to match the new shapes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 12:31:07 -07:00
0xallam 369fa56148 refactor: delete orphaned dirs, dead streaming infra, unused session/compressor
Orphaned files/dirs:
- ``strix/agents/StrixAgent/`` — empty, only ``__pycache__``.
- ``strix/tools/browser/litellm/`` — empty, only ``__pycache__``.
- ``strix/strix_runs/`` — runtime output left in the working tree.
- ``strix/prompts/`` — single Jinja template that nothing renders.

Dead streaming pipeline (was never wired in the SDK migration):
- Delete ``strix/interface/streaming_parser.py`` (XML tool-call parser
  for an output format the SDK doesn't produce).
- Strip ``streaming_content`` / ``interrupted_content`` dicts and
  five unused methods from ``Tracer``.
- Strip the streaming-render path + ``interrupted`` branch from TUI.
- Trim ``strix/llm/utils.py``: drop ``normalize_tool_format``,
  ``parse_tool_invocations``, ``format_tool_call``,
  ``fix_incomplete_tool_call`` and the XML-stripping in
  ``clean_content``. Keep only the inter-agent-XML scrub.

Unwired session compression:
- Delete ``strix/llm/strix_session.py`` and
  ``strix/llm/memory_compressor.py``. ``Runner.run`` was never called
  with a ``session=``, so the compressor never ran. Drop the matching
  test file and the ``strix_memory_compressor_timeout`` config knob.

Tracer cleanup:
- Remove ``log_agent_creation``, ``log_tool_execution_start``,
  ``update_tool_execution``, ``update_agent_status``,
  ``get_agent_tools`` — none had production callers.
- Rewrite the redaction + correlation tests against
  ``log_chat_message`` (which still emits events).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 12:21:59 -07:00
0xallam 4146174503 refactor: scrub migration scars, dead code, and unused helpers
- Strip PLAYBOOK / AUDIT / Phase-N / C-numbered references from
  module docstrings across 16 files; rename
  ``_PHASE1_PARALLEL_DEFAULT`` → ``_PARALLEL_TOOL_CALLS_DEFAULT``.
- Delete unused exception classes: ``SandboxInitializationError``,
  ``ImplementedInClientSideOnlyError``.
- Delete the no-op ``on_handoff`` hook (we don't use SDK handoffs).
- Delete the unreachable backward-compat tab-delimited fallback in
  ``_parse_git_diff_output``.
- Delete orphaned ``strix/tools/load_skill/`` (dir contained only a
  pycache) and stale pycache files.
- Rewrite ``strix/skills/__init__.py``: 168 → 56 LoC. Drop seven
  helper functions (``get_available_skills``, ``get_all_skill_names``,
  ``validate_skill_names``, ``parse_skill_list``,
  ``validate_requested_skills``, ``generate_skills_description``,
  ``_get_all_categories``) — none had external callers; only
  ``load_skills`` is used.
- Drop the stale ``strix/agents/sdk_factory.py`` per-file ruff ignore
  (file no longer exists).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 12:05:24 -07:00
0xallam e4be5f9588 docs: restore tool guidance into docstrings, drop prompt tool-format boilerplate
Port the prose guidance that previously lived in the deleted
*_actions_schema.xml files into per-tool docstrings, so the SDK's
auto-generated function schema carries the same domain knowledge
(HTTPQL syntax, Caido sitemap kinds, browser persistence/JS rules,
agent specialization caps, customer-facing report rules, CVSS/CWE
guidance, etc.) without any custom prompt scaffolding.

Strip the <tool_usage> block from system_prompt.jinja — XML format
guidance, the "CRITICAL RULES" 0-8 list, and the </function>
closing-tag reminder all contradicted the SDK's native JSON
function-calling protocol.

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