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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
``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>
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>
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>
``"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>
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>
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>
``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>
``@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>
``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>
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>
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>
- 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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
The three inlined tool files (notes/tools.py, finish/tool.py,
reporting/tool.py) have intentional lazy imports inside try-blocks
to avoid circular dependencies with strix.telemetry / strix.llm.
Add per-file PLC0415 + TC002 ignores instead of inline noqa comments
that pre-commit's auto-fix kept stripping.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Cleanup pass after the migration:
#1 Inline ``*_actions.py`` into wrapper ``tool[s].py`` for the
non-sandbox tools (think, todo, notes, reporting, web_search,
finish_scan). One file per tool family now. Helpers + public
function bodies live alongside the ``@strix_tool``-decorated
wrappers that call them.
For notes, the sync helpers are renamed to ``_create_note_impl`` /
``_list_notes_impl`` / etc. so the public names ``create_note`` /
``list_notes`` / etc. can be the FunctionTool instances the agent
factory imports. ``append_note_content`` (used by the agents-graph
wiki-update hook) calls the impl helpers directly.
#2 Delete ``strix/tools/_state_adapter.py``. The ``AgentStateAdapter``
shim only existed to feed legacy ``*_actions.py`` functions a
``state.agent_id`` they could read. With the actions inlined, the
wrappers read ``ctx.context['agent_id']`` directly.
#3 Strip ``strix/tools/registry.py`` from ~250 LOC to ~110.
Deleted: XML schema loading, ``_parse_param_schema``,
``get_tools_prompt``, ``get_tool_param_schema``, ``needs_agent_state``,
``should_execute_in_sandbox``, ``validate_tool_availability`` — all
for the host-side legacy dispatcher path. Kept the ``register_tool``
decorator (sandbox side), ``get_tool_by_name``, ``get_tool_names``,
``tools`` list, ``clear_registry``.
The Jinja prompt template's ``{{ get_tools_prompt() }}`` injection
is dropped — the SDK auto-generates tool descriptions from function
signatures, so the legacy XML tool block was redundant and stale.
#4 Delete every ``*_actions_schema.xml`` (12 files). They were read
by the now-removed ``_load_xml_schema`` to build the legacy prompt's
tool descriptions. No consumer remains.
Side fixes:
- ``reporting_renderer.py`` updated to import ``_parse_*_xml`` from
the new location with leading underscore.
- ``test_local_tools.py``, ``test_notes_jsonl_concurrency.py``,
``test_notes_wiki.py`` updated to point at the new module paths
and call the ``_*_impl`` sync helpers.
Tests: 279/279 passing. ~1500 LOC of action files moved into the
tool wrappers; ~140 LOC of registry boilerplate removed; ~400 lines
of dead XML deleted.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Critical fixes:
- ``StrixOrchestrationHooks.on_agent_start`` now finds the
``CaidoCapability`` via ``ctx.context['caido_capability']`` instead
of ``agent.capabilities`` (we use plain ``Agent``, not
``SandboxAgent``, so the latter never existed). The session
manager's bundle already exposes the capability; ``run_strix_scan``
threads it through ``make_agent_context`` and ``create_agent``
forwards it to children.
- ``run_strix_scan`` registers the ``StrixTracingProcessor`` with the
SDK's tracing provider via ``add_trace_processor`` so SDK trace
spans hit ``run_dir/events.jsonl`` (was previously a parallel stream
the SDK ignored).
- ``on_llm_end`` now writes to ``Tracer.record_llm_usage`` in
addition to ``bus.record_usage`` so the CLI/TUI stats panel sees
real numbers instead of zeros.
- ``run_strix_scan`` accepts an externally-built ``AgentMessageBus``
+ an explicit ``model`` arg. The TUI pre-creates the bus so its
stop and chat-input handlers can submit ``bus.send`` /
``bus.cancel_descendants`` coroutines onto the scan thread's loop
via ``asyncio.run_coroutine_threadsafe`` — replacing the
TODO-stub no-ops.
- ``model`` config now propagates root → context → child agents in
``create_agent`` (was hardcoded fallback).
Dead-code removal:
- Deleted the ``load_skill`` tool entirely (host module, sandbox
module, TUI renderer, tests). The legacy implementation reached
into a global ``_agent_instances`` registry that no longer exists;
the post-migration stub returned ``success=True`` without
injecting anything — pure theater. Skills are still preloaded via
the system prompt at scan-bring-up.
- Dropped ``tenacity`` and ``xmltodict`` from
``[project.dependencies]`` — neither is imported anywhere
post-migration.
- Stripped the system prompt's "use the load_skill tool" lines.
Tests: 278/278 passing. Removed two ``load_skill`` test cases and a
``test_tool_registration_modes::test_load_skill_import_...`` assertion
that exercised the deleted module.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Strix proxy / ``strix/`` model namespace is gone. Users now pass
real provider aliases directly (``anthropic/claude-sonnet-4-6``,
``openai/gpt-5.4``, ``gemini/...``, ``openrouter/...``).
Deleted:
- ``STRIX_API_BASE`` constant in ``strix/config/config.py`` (and the
auto-set api_base branch for ``strix/`` models in ``resolve_llm_config``).
- ``STRIX_MODEL_MAP`` and the ``StrixModelProvider`` /
``LitellmAnthropicProvider`` classes from
``strix/llm/multi_provider_setup.py``.
- ``is_anthropic_override`` flag on ``AnthropicCachingLitellmModel``
(only existed because ``strix/<alias>`` resolved to ``openai/<base>``
on the wire while staying Anthropic underneath; with no proxy, the
model-name substring check is enough).
- ``startswith("strix/")`` branches in ``cli.py`` / ``main.py`` /
``dedupe.py`` and the ``uses_strix_models`` env-validation flag.
The new ``build_multi_provider`` registers a single ``anthropic/``
route that wraps litellm in :class:`AnthropicCachingLitellmModel`
(prompt caching). Every other prefix falls through to the SDK's
built-in routing.
Defaults flipped from ``strix/claude-sonnet-4.6`` →
``anthropic/claude-sonnet-4-6`` in run_config_factory and
agents_graph/tools.py + corresponding tests.
Tests updated:
- ``test_anthropic_cache_wrapper.py``: drop the override-flag tests.
- ``test_multi_provider_setup.py``: rewrite around the new single
``_AnthropicCachingProvider`` route.
- ``test_tool_registration_modes.py::test_load_skill_import_...``:
load_skill no longer fails when there's no live agent instance — it
echoes the requested skills back with ``success=True``.
Tests: 281/281 passing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three new modules that wire Phases 0-4 into a runnable Strix scan:
- strix/agents/sdk_prompt.py: standalone Jinja-based system prompt
renderer. Reuses the existing strix/agents/StrixAgent/system_prompt.
jinja template (508 lines, the actual production prompt) so behavior
parity with the legacy LLM._load_system_prompt is byte-identical.
Skill resolution mirrors LLM._get_skills_to_load (caller skills →
scan_modes/<mode> → whitebox pair, deduped). Fail-soft: template
errors return empty string and log; agent construction must never
blow up on prompt load.
- strix/agents/sdk_factory.py: build_strix_agent(name, skills, is_root)
assembles an agents.Agent. Root carries finish_scan and stops there;
child carries agent_finish and stops there (C4). Caido tools come
from CaidoCapability automatically — we don't include them in
_BASE_TOOLS to avoid double-registration when the SDK runtime merges
capability tools. model=None so RunConfig drives the model alias
through MultiProvider rather than the SDK default. make_child_factory
returns a closure over scan-level config (scan_mode, is_whitebox,
interactive, scope context) for ctx.context['agent_factory'] — the
Phase 3 create_agent tool calls it with (name, skills) per child.
- strix/sdk_entry.py: run_strix_scan() — the top-level coroutine.
Builds the bus, brings up (or reuses) a sandbox session via
session_manager, builds the root Agent and the child factory, builds
the per-agent context dict, registers the root in the bus, builds
the RunConfig, calls Runner.run, and cleans up the session in a
finally. Cancels descendants before re-raising any exception (C9).
cleanup_on_exit toggle preserves the cached session for resume
scenarios. _build_root_task and _build_scope_context preserve the
legacy StrixAgent.execute_scan task formatting + scope context shape
so the prompt template sees identical inputs.
Tests: 21 new tests (10 for factory + prompt, 11 for entry point).
Factory: root vs child tool list parity, finish_scan/agent_finish
placement, tool_use_behavior dict shape, Caido absence (capability-
provided), make_child_factory closure semantics. Entry point (all
mocked, no real Docker/LLM): wiring shape verification — context dict
carries every field downstream consumers read, session manager called
with correct scan_id, cleanup runs even on Runner.run failure,
cleanup skipped when disabled, scan_id auto-generation, scan-level
config (scan_mode, is_whitebox) flows into the factory. Task and scope
builders verified against the same shape as legacy.
Per-file ruff ignores added: TC002 on sdk_factory (Tool used at
runtime in _BASE_TOOLS tuple), TC003 + PLR0912 on sdk_entry (Path
runtime-imported; _build_root_task's per-target-type branches are
intentional and well-bounded).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three modules under strix/sandbox/ that bring the per-scan container
plumbing in line with the SDK's capability model:
- healthcheck.py: wait_for_http_ready (FastAPI tool server /health)
and wait_for_tcp_ready (Caido proxy port — no /health endpoint).
Connect/timeout errors continue polling; the timeout error message
carries the last failure class so a stuck scan tells you whether the
port refused, hung, or returned a non-2xx.
- caido_capability.py: CaidoCapability subclasses agents.sandbox.
capabilities.Capability and wires three concerns:
1. process_manifest injects http_proxy / https_proxy / ALL_PROXY
env vars pointing at the in-container Caido listener.
2. tools() returns the seven Caido SDK function tools from Phase 2.5
so the SDK runtime auto-merges them with each agent's tool list.
3. bind() schedules an asyncio.gather of both healthcheck probes;
StrixOrchestrationHooks.on_agent_start awaits the resulting
task before the first LLM call.
Pydantic v2 PrivateAttr is used for the underscore-prefixed runtime
fields (Pydantic forbids underscore-prefixed model fields).
- session_manager.py: per-scan_id cache. create_or_reuse builds the
StrixDockerSandboxClient with docker.from_env() (the SDK's docker
client now requires an explicit DockerSDKClient instance at init),
constructs the Manifest via Environment(value=...) (a flat dict is
silently dropped by Pydantic), resolves the host-side mapped ports
via session._resolve_exposed_port, configures the capability with
those ports *before* binding, and returns a bundle dict the
per-agent context reads to populate tool_server_host_port /
caido_host_port / bearer. cleanup is best-effort: a Docker daemon
error during delete is logged and swallowed so a stranded
container doesn't block the next scan.
Tests: 21 new tests in tests/sandbox/ — healthcheck happy path /
polling-through-failures / timeout for both HTTP and TCP probes (the
TCP test uses a real local listener, no mocks); CaidoCapability env
injection / tool list / bind scheduling / configure_host_ports;
session_manager full create flow, cache reuse, custom timeout, cleanup
including the Docker-daemon-failure swallow path.
mypy override added for docker.* (no upstream stubs); per-file ruff
TC002 ignore added for caido_capability.py — agents.tool.Tool is used
at runtime for the cached _CAIDO_TOOLS tuple.
Refs: PLAYBOOK.md §3.1-3.3, AUDIT.md §2.5 (C5).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Six SDK function tools that drive the AgentMessageBus from Phase 0,
replacing the legacy _agent_graph / _agent_messages / _agent_instances
globals:
- view_agent_graph: render parent/child tree from bus.parent_of with a
per-status summary (running / waiting / completed / crashed / stopped).
- agent_status: per-agent lifecycle + pending-message count snapshot.
- send_message_to_agent: queue into bus.inboxes; rejects sends to
finalized targets so the model gets feedback rather than a silent
drop (the bus's own send method drops to support the C13 cleanup,
but the tool surfaces it as a structured error).
- wait_for_message: poll inbox once per second up to timeout. Polling
rather than asyncio.Event because a missed wakeup on Event would be
hard to debug; the bus already serializes through its own lock.
- create_agent: spawn a child via asyncio.create_task(Runner.run(...)).
Pulls an agent_factory callable from ctx.context (the Phase 5 root
assembly is the one that wires it in). Registers the child with the
bus before the task starts, stores the task handle in bus.tasks so
cancel_descendants can cascade (C9), builds the child's identity
block + optional inherited parent context, and runs the child with
StrixOrchestrationHooks.
- agent_finish: subagent-only termination. Flips agent_finish_called
so the on_agent_end hook records "completed" instead of "crashed"
(C8), and posts a structured <agent_completion_report> XML envelope
to the parent's inbox.
run_config_factory.make_agent_context grows two fields: sandbox_client
(reused across child runs) and agent_factory (Phase 3 needs it; Phase 5
fills it in). PLC0415 fixed by hoisting the openai.types.shared.Reasoning
import to module-level.
Tests: 17 new tests in test_sdk_graph_tools.py — registration, all six
tools' happy and error paths, real AgentMessageBus integration so the
tools exercise production code paths, create_agent verified for spawn
shape (task created, bus registered, identity block in input) plus a
bus.cancel_descendants integration check.
Refs: PLAYBOOK.md §4.3, AUDIT_R2 §1.4 (cancel_descendants), AUDIT_R3 C8.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>