* Treat literal 'null'/'none' strings as absent for optional tool args
Models routinely pass the literal string "null" or "none" instead of
omitting an optional argument. Taken at face value it becomes a filter
that matches nothing, so tools like list_notes / list_reports /
list_requests silently return no results.
Coerce such values to None in the central argument-coercion layer, but
only for parameters the schema allows to be null (or that are absent from
a declared "required" list), so required strings keep the literal value.
The list/filter helpers normalize the same values too, so a direct call
can't regress.
* Limit nullish coercion to query tools and keep literal tags
A literal "null"/"none" is only a mistake where the argument is a filter, so
gate the coercion on read-only query tools; a tool that writes keeps the value,
which stops update_note(content="none") from being read as "leave unchanged".
Stop dropping nullish entries from a notes tag filter too: tags are free-form,
so a literal "none" tag stays filterable and mixed tag queries keep every
branch.
One tool was doing three jobs (wait on the user, wait on other agents, and
- wrongly - wait for a long-running command), so the driver had to guess which
one an agent meant and used parent_id as the proxy: the root waits for a human,
everyone else waits for agents. That proxy is wrong, since the user can message
any agent from the TUI's agent tree.
Tool identity now carries the intent, and the coordinator records it as a
wait_kind that survives snapshot/restore:
respond_to_user -> wait_kind="user", never auto-resumed (root or not)
wait_for_agents -> wait_kind="agents", auto-resumed on a 300s timer
recovery exhaust -> wait_kind="stalled"
respond_to_user fuses the message and the yield into one call, so there is no
way to answer and then forget to stop - the two-step that gpt-4o-mini skipped
2/2 in live testing. Plain text still renders as before.
Auto-resume is also bounded now: an agent that re-parks after every timeout
burned a model turn every 300s for the rest of the scan (and, since parked
children notify their parent, spammed the parent's inbox on the same cycle).
After _MAX_IDLE_AUTO_RESUMES it stays parked until a real message arrives.
Chat-completions mode converts filesystem CustomTools to FunctionTools
(which bounds their result), but the Responses-API path kept them native
and unbounded, so a large read_file could still exhaust the context
window. Always configure the Filesystem capability to head+tail bound
tool output in both modes.
Treat tool_output_max_tokens as a ceiling so an explicit model-supplied
cap can't exceed it, and derive the truncation notice's dropped-line
count from the lines actually kept after the byte-trim pass. Also cast
the pygments fallback lexer so it satisfies the resolve_lexer return
type under the pre-commit mypy hook.
Cap the size of every tool result so a single verbose command (recursive
find, noisy scanner, full page dump) can't pin the conversation near the
model's context window for the rest of a scan.
- New ContextSettings config group with env-tunable caps.
- Default the SDK shell tools' max_output_tokens so exec_command /
write_stdin truncate head+tail instead of returning unbounded output.
- Bound Strix's own FunctionTool/CustomTool results (line + UTF-8 byte
head+tail preview with a truncation notice) and cap error strings.
Drop every hand-rolled provider table and per-model gating that had
accumulated in the model-handling layer:
* normalize_model_name no longer auto-prefixes bare claude-* / gemini-*
names. Users supply the full <provider>/<model> form. The function
became literally model_name.strip(), so callers now inline that and
the function is removed.
* tool_choice="required" is gone everywhere. Thinking-mode endpoints
(Anthropic, DeepSeek /beta) reject it; modern reasoning models don't
need it; non-interactive runs already have
_append_noninteractive_tool_required_message as the convergence
backstop. model_supports_reasoning, model_known_to_registry, and
_model_cost_entry were only used to gate this and follow it out.
* Reasoning(effort=...) is now attached whenever
STRIX_REASONING_EFFORT is non-none. litellm.drop_params=True absorbs
it for non-reasoning models.
* Warm-up's bare-name OpenAI 401 hint is removed (false-positive prone,
relied on substring matching).
* reset_tool_choice on SandboxAgent is no-op now (no tool_choice gets
set) and is removed.
* report/dedupe.py was still routing through stock MultiProvider, so
non-OpenAI configs failed the dedupe LLM pass; switch it to
StrixProvider.
Verified end-to-end against modern provider strings (openai/gpt-5.4,
anthropic/claude-opus-4-7, deepseek/deepseek-reasoner,
gemini/gemini-2.5-pro, groq/, xai/, mistral/, together_ai/, perplexity/,
openrouter/, litellm/ legacy form, and whitespace-padded input): 18/18
cases route correctly, env vars mirror via litellm.validate_environment,
and ModelSettings carries no tool_choice. mypy strict passes.
Five rounds of sweep across the tree. Net ~544 lines removed.
Removed:
- Section-divider banners and one-line section labels (# Display
utilities, # ----- list_requests -----, # CVSS breakdown, etc.).
- Module-level prose docstrings on internal modules. Kept one-line
summaries; trimmed multi-paragraph narration about SDK/Strix
responsibility splits, cache strategies, three-source precedence.
- Internal-helper docstrings that just restate the function name —
caido_api helpers (caido_url, get_client, view_request, etc.),
settings-class one-liners (LLMSettings, RuntimeSettings, ...),
UI helper docstrings.
- Args/Returns blocks on non-LLM-facing internal helpers
(build_strix_agent, render_system_prompt, create_or_reuse,
bootstrap_caido) — kept only the genuinely non-obvious params.
- Internal-history phrasing — "Mirrors main-branch shape",
"pre-SDK harness", "previous lookup matched no attribute".
- Narrative comments inside function bodies that explained what the
next line does, design rationale obvious from the surrounding code,
or "we used to..." asides.
- Trailing periods on every error-string literal across the tool tree.
- Duplicated roundtripTime quirk comment (kept the LLM-facing copy in
tools/proxy/tools.py).
Kept (every one names an upstream bug, vendored-code provenance, or
non-obvious data quirk):
- core/runner.py: SDK replay-with-empty-initial-input + on_agent_end
lifecycle gap.
- runtime/docker_client.py: VERBATIM COPY block of the upstream
_create_container body, pinned to SDK v0.14.6.
- runtime/session_manager.py: NO_PROXY for agent-browser CDP loopback.
- tools/proxy/caido_api.py: generated-pydantic Request.raw quirk,
replay double-history pitfall.
- tools/proxy/tools.py: Caido roundtripTime=0 quirk for proxy
captures.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Four passes of audit-and-patch on the tool surface, condensed.
Tool API shape:
- Todo tools collapse to a single list-based form (one arg per tool,
always a list, no dual-mode validator). Result-field names line up
across the family — created_count / updated_count / marked_count /
deleted_count, and _mark returns a single "marked" key plus the new
status instead of marked_done / marked_pending.
- list_notes splits the overloaded total_count into filtered_count
(matches) and total_count (grand total), matching list_todos. All
three notes mutations now echo total_count and note_id.
- finish_scan drops the machine-code error strings; a single human
"error" key carries the reason on every failure path.
- scope_rules delete echoes a message so the renderer's success
branch has something to surface.
Failure-key unification: every tool now uses {"success": False,
"error": "..."} on failure paths. Touched thinking, web_search,
reporting, and finish. Trailing periods on error strings swept clean
across the whole tool tree.
Tool prompts (docstring re-imports vs main):
- create_vulnerability_report re-imports the CWE reference catalog,
multi-part fix rules, fix_before/fix_after PR-suggestion mechanics,
the COMMON MISTAKES list, the informational-vs-actionable
distinction, and file-path examples.
- web_search re-imports concrete example queries.
- list_sitemap docstring fixed hasDescendants -> has_descendants
(the camelCase reference never matched our snake_case schema).
- create_agent.skills description "Comma-separated" -> "List of".
- factory.py module docstring no longer claims there's no runtime
skill-loading tool. agents_graph module docstring lists stop_agent.
- system_prompt nudges loading the matching skill before guessing
payloads or syntax from memory.
TUI:
- proxy_renderer was reading stale field names from the pre-SDK
schema (requests / total_count / statusCode / matches /
showing_lines); now reads entries / page_info / status_code / hits
/ page+total_lines. Three proxy operations were rendering empty
before this.
- Idle-pane placeholder text trimmed to "Loading...".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Main's load_skill tool was deleted during the SDK migration along
with the prompt-mutation pattern it relied on. Re-add the capability
without the mutation: load_skill(skills=[...]) now returns the skill
markdown bodies as a tool result, so the content lands in conversation
history as in-context reference rather than as patched-in system
prompt content. Same source of truth (load_skills + skill files),
same validation (validate_requested_skills) as create_agent.
Tool result format is plain markdown (## Skill: <name> headers joined
with ---), not the <specialized_knowledge> XML wrapping used at
agent-build time. The XML framing was deliberately reserved for
prompt-level privileged context; tool-loaded skills are honestly
labelled as just-fetched reference material.
Close the discovery loop by surfacing the full skill catalog in the
system prompt. Without it the model could only guess skill names —
discovering them via validation errors on misses. Now every agent
sees a categorised <available_skills> block right after the
<specialized_knowledge> block with a short hint pointing at
create_agent / load_skill.
Skills module: factored _iter_user_skill_files() so get_all_skill_names
(set, for validation) and get_available_skills (dict by category,
for the prompt) share one source of truth on what counts as
user-selectable. Internal categories (scan_modes, coordination) stay
excluded from both.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three concrete wraps on exec_command / write_stdin via the existing
Shell capability configure_tools mechanism, plus one skill-doc fix.
All wraps fire on both Responses and chat-completions paths; the
chat-completions error-as-result wrap still stacks on top when needed.
- write_stdin: decode the common escape forms in `chars` (\uXXXX,
\xXX, \n \t \r \0 \a \b \v \f \\). Models routinely send the
literal six-char string `` intending the ASCII control byte;
the SDK takes chars verbatim so the byte never reaches the PTY and
documented mechanisms like Ctrl-C, arrows, and Escape silently
don't work. Allowlist regex over recognized escapes only —
unrecognized sequences like `\p` pass through untouched.
- exec_command: catch InvalidManifestPathError and rewrite to a
model-actionable message ("workdir must be a path inside
/workspace") using the exception's structured `context["rel"]` so
we don't need to string-match the SDK's wording.
- Both tools: catch pydantic ValidationError once at the wrap and
reformat into a short "{tool}: invalid arguments — {field}: {msg}"
string. Covers empty cmd, missing required fields, ge/min_length
violations on max_output_tokens and yield_time_ms — and any future
schema field the SDK adds.
Updated python.md guidance: the `shell=` parameter is for swapping
POSIX shells (bash/zsh/sh). Interpreters belong in `cmd` —
`cmd="python3 -c '...'"`, not `shell=python3`. The `shell=interpreter`
shortcut breaks in interpreter-specific ways (python needs `-c`,
node/ruby/perl need `-e`) so there's no clean code fix and we don't
try one.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- view_agent_graph status summary now derives buckets from the canonical
Status literal via get_args, so adding a new status in core.agents
auto-flows into the summary. The previous hardcoded five-bucket list
silently omitted "failed" — buckets stopped summing to total whenever
an agent failed.
- stop_agent rejects targets that are already in a terminal status
(completed / stopped / crashed / failed) with a model-readable error
pointing at view_agent_graph and send_message_to_agent. request_stop
unconditionally overwrites status, so without this guard calling
stop_agent on a completed agent erased the "completed" history.
- StopAgentRenderer added — was falling back to the generic key/value
renderer; the rest of the agents_graph tools have purpose-built ones.
- agent_finish root-rejection payload trimmed from
{success, agent_completed, error, parent_notified} to {success, error}.
The lifecycle gate only reads success+agent_completed and they were
always False/False on this branch, so the extra fields were dead weight.
- wait_for_message renames its top-level outcome field from "status" to
"wait_outcome" — "status" overloaded with the coordinator's agent
status literal (which also has "stopped" as a value, different
meaning). Redundant "agent_waiting" boolean dropped (true iff
wait_outcome == "waiting"). Consumer at factory._wait_tool_parked
updated to match.
- send_message_to_agent now refuses self-send with a pointer at think /
agent_finish / finish_scan instead of looping a message into your
own session.
- SendMessageToAgentRenderer read args.get("agent_id") but the tool's
param is target_agent_id, so the TUI silently never showed the target.
Fixed.
- Restored skill validation lost during the SDK migration: skills
module re-exports get_all_skill_names and validate_requested_skills
(excluding internal scan_modes/coordination categories from the
user-selectable set). create_agent now validates skills before
spawning instead of silently accepting unknown names.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Re-add list_sitemap and view_sitemap_entry from main, ported to the
new caido-sdk-client layout via raw GraphQL queries (the typed SDK
doesn't expose sitemap operations, but the Caido server still
supports sitemapRootEntries / sitemapDescendantEntries / sitemapEntry).
Wired through caido_api (sandbox-importable helpers), the host-side
@function_tool wrappers, factory _BASE_TOOLS, the system prompt, the
python skill doc, and the public proxy docs.
While threading these through, lock down the output contract across
every proxy tool so the model sees one consistent shape:
- All tools wrap success/failure in {"success": bool, "error"?: str}
- Canonical field names: status_code, length, roundtrip_ms (omitted
when 0), is_tls, has_descendants. snake_case everywhere on output;
camelCase stays only on the input side where it's the GraphQL
schema.
- repeat_request now returns a structured response that matches
list_requests' response_summary shape (parse_raw_response parses
the raw bytes into status_code / length / headers / body), with
body capped at 8KB and a body_truncated flag so the model knows
when to fetch the full body via view_request.
- RepeatRequestRenderer was reading non-existent top-level keys
(status_code, response_time_ms, body) and silently displaying
nothing useful — now reads the structured response shape.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
send_request was a thin wrapper over the Caido Replay API that the model
could replicate with a one-liner `curl` via exec_command. The sandbox's
HTTP_PROXY env captures all such traffic for free, so the tool was
adding bugs (duplicate dispatch, dropped responses) without adding
capability. Removed across factory, tools module, sandbox-importable
caido_api helper, TUI renderer, prompt template, skill doc, and public
docs. repeat_request stays — it operates on captured request IDs with
structured modifications, which curl can't replicate cleanly.
Three caido-sdk-client workarounds that were hitting us through both
send_request and repeat_request:
- replay_send_raw used to pass CreateReplaySessionFromRaw to
sessions.create(), which seeds a stored entry server-side, then
called send() — producing two history rows per call. Empty-create +
send produces one dispatched request.
- The same helper read result.entry.response_raw, an attribute that
doesn't exist on ReplayEntry, so response bytes were silently
dropped. Fixed to walk result.entry.response.raw with proper None
guards.
- get_request_with_client passed include_request_raw / include_response_raw
based on the requested part, but the SDK's generated pydantic models
declare raw as required even though the GraphQL fragment makes it
conditional via @include. Passing False crashed view_request with a
pydantic validation error. Always request both raw bodies; the caller
picks which to surface.
Also wrapped replay.send() in asyncio.wait_for(30s) so a stalled Caido
dispatch (notably loopback targets that don't route cleanly through the
sandbox proxy) fails fast with a model-readable error instead of
hanging the agent until the function_tool 120s budget expires.
Finally, list_requests now omits the roundtrip_ms field when Caido
reports 0 — proxy-captured unscoped traffic consistently reports 0
while scoped/replay traffic carries real measurements, so the absence
of the field is now informative ("Caido didn't measure this") rather
than misleading ("this request took 0ms").
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>
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>
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 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>
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>