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>
Notes generated 5-char IDs via a 20-try collision loop while todos
generated 6-char IDs in one shot. Mixed widths across the agent's
view made the two tools look unrelated. Match todo's shape — same
length, same one-shot generation. Collision retry is unnecessary at
scan-scale (a few hundred items vs 16^6 keys).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reject ambiguous calls in todo tools that previously combined the
single-target params and the bulk-array param (e.g. create_todo with
both `title` and `todos` would silently create N+1 items). Each tool
now errors with a mode-specific hint pointing the model at the
appropriate form. Also drop the meaningless char-count from `think`'s
success message — the model already knows what it wrote.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat: add HTTP request smuggling skill
Add a new vulnerability skill covering HTTP request smuggling (HRS)
across CL.TE, TE.CL, H2.CL, and H2.TE desync variants. HRS is absent
from the existing skill set despite being a distinct, high-impact
vulnerability class frequently present in any architecture using a
reverse proxy or CDN in front of an application server.
Coverage:
- CL.TE: front-end uses Content-Length, back-end uses Transfer-Encoding
- TE.CL: front-end uses Transfer-Encoding, back-end uses Content-Length
- H2.CL: HTTP/2 front-end downgrades to HTTP/1.1 with injected Content-Length
- H2.TE: Transfer-Encoding header injection through HTTP/2 desync
- Transfer-Encoding obfuscation techniques (tab, space, duplicate, xchunked)
- Front-end security control bypass via smuggled prefix
- Cross-user request capture for session token theft
- Response queue poisoning and WebSocket handshake hijacking
- Timing-based and differential response detection methodology
- HTTP/2 specific probing techniques
Includes raw HTTP examples for each variant, step-by-step testing
methodology, exploitation PoCs, false-positive conditions, and
infrastructure topology guidance.
* fix: correct TE.CL probe, pseudo-header terminology, PoC Content-Length values, \x20 representation
Four reviewer findings addressed:
P1 — TE.CL timing-probe description inverted: previous text said
'Content-Length set to fewer bytes than the chunk content' which
describes socket-poisoning behavior (differential response), not a
timeout. Corrected to: send a complete chunked body with CL set to MORE
bytes than provided so the back-end waits for data that never arrives.
Also corrected Testing Methodology step 3 to match.
P2 — pseudo-header terminology: 'content-length' is a regular HTTP/2
header, not a pseudo-header (pseudo-headers are exclusively :method,
:path, :authority, :scheme). Fixed the H2.CL explanation (line 75),
HTTP/2-specific detection bullet, and Pro Tip #4 which referred to
':content-length pseudo-header'.
P2 — PoC Content-Length values: outer Content-Length in the bypass PoC
corrected from 116 to 100 (actual byte count of the body shown); capture
PoC corrected from 129 to 120.
P2 — \x20 representation: replaced the \x20 escape sequence in the code
block (which renders as a literal four-character string, not a space byte)
with an explanatory comment and actual whitespace characters so the intent
is unambiguous.
* Update strix/skills/vulnerabilities/http_request_smuggling.md
Two fixes that surfaced from a single broken run.
(1) Source mounting was double-broken:
- ``session_manager.create_or_reuse`` mounted the *parent* of the first
local source under a hardcoded ``"sources"`` key, so the host's
unrelated content leaked in at ``/workspace/sources/...`` while the
agent's task prompt advertised ``/workspace/<workspace_subdir>``
(from ``_build_root_task``). Result: the agent looked at
``/workspace/empty/`` (per the prompt), found nothing, and bailed.
- ``backends._docker_backend`` never called ``await session.start()``
after ``client.create()`` — the SDK's manifest application
(``LocalDir`` materialization, mount setup) only runs inside
``start()`` (or ``async with session:``). So even with the right
``entries`` the workspace would have been empty anyway.
Fix: thread ``args.local_sources`` (already populated by
``collect_local_sources``) all the way through to the session manager,
build ``Manifest.entries`` keyed by each source's ``workspace_subdir``,
and call ``session.start()`` in the docker backend so the SDK actually
materializes the entries. Drop the now-unused ``_resolve_sources_path``
helpers from ``cli.py`` and ``tui.py``.
(2) Scan-failure visibility was nonexistent in TUI mode:
- The SDK's ``on_agent_end`` hook only fires after the agent reaches its
first turn. A failure earlier (model routing, sandbox bring-up, …)
left the root agent stuck at ``status=running`` in the bus and
tracer, so the TUI animated "Initializing" forever.
- ``scan_target`` in ``tui.py`` caught the exception and called
``logging.exception`` but never propagated it. ``run_tui`` returned
cleanly when the user finally ctrl-q'd, so ``main.py`` happily
printed the success-completion banner over a dead scan.
Fix: in ``run_strix_scan``'s ``except BaseException`` block, finalize
the root agent as ``"failed"`` in both the bus and the tracer (with the
error message attached). Capture the exception on
``StrixTUIApp._scan_error`` from the scan thread; ``run_tui`` re-raises
it after ``app.run_async()`` returns so ``main.py``'s existing handler
prints the traceback. Add a ``"failed"`` branch to
``_get_status_display_content`` that shows the error message in red,
mirroring the existing ``llm_failed`` branch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
``MultiProvider`` was constructed with no openai kwargs, so the inner
``OpenAIProvider`` defaulted to reading ``OPENAI_API_KEY`` from the
environment. Strix's contract is that ``LLM_API_KEY`` works for every
provider, so users with ``STRIX_LLM=openai/<model>`` + ``LLM_API_KEY``
hit ``openai.OpenAIError`` at the first turn — the warm-up call worked
because that path goes through ``litellm.completion`` directly with
explicit creds, but the actual scan went through the SDK's MultiProvider
where the key was never plumbed.
Pass ``Settings.llm.api_key`` and ``Settings.llm.api_base`` through to
the underlying ``OpenAIProvider`` via the ``openai_api_key`` /
``openai_base_url`` ctor kwargs. ``openai_use_responses`` flips to
``False`` when ``LLM_API_BASE`` is set — non-default base URLs are the
reliable signal that the user is on an OpenAI-compatible endpoint
that doesn't speak the Responses API. Genuine OpenAI usage keeps the
Responses API as the default transport.
The ``anthropic/`` prefix continues to route through
``AnthropicCachingLitellmModel`` for prompt caching; ``litellm/`` and
other prefixes still fall through to the SDK's stock routing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The SDK's ``DockerSandboxClient._create_container`` overrode both
``entrypoint`` and ``command`` (``tail`` + ``-f /dev/null``), which kept
the container alive but bypassed the image's ``docker-entrypoint.sh``.
That script is what launches ``caido-cli`` and sets up the browser CA
trust. With it skipped, every scan since the harness migration sat in
``bootstrap_caido`` retrying ``loginAsGuest`` for 30 s against a dead
port and then aborted before any agent work happened.
Drop the ``entrypoint`` override and pass ``[tail, -f, /dev/null]`` as
``command``. The image's ENTRYPOINT runs setup, then ``exec \"\$@\"``
swaps PID 1 to ``tail`` for the keep-alive — same long-running
no-op the SDK was after, but with the manifest/init work done first.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drops the ``agent-browser install --with-deps`` step (Chrome for
Testing has no ARM64 build and ships several automation tells)
and uses the apt-installed Chromium across both arches.
``agent-browser`` is wired via three env vars baked into the image:
* ``AGENT_BROWSER_EXECUTABLE_PATH=/usr/bin/chromium`` — every
browser launch picks up the apt binary; no per-call flag needed.
* ``AGENT_BROWSER_USER_AGENT`` — recent stable Chrome 131 Linux UA.
* ``AGENT_BROWSER_ARGS`` — minimal stealth flag set:
``--disable-blink-features=AutomationControlled`` (the most-
checked tell), ``--exclude-switches=enable-automation``,
``--disable-features=IsolateOrigins,site-per-process,Translate,
BlinkGenPropertyTrees``, sane window-size + lang, infobars +
save-password + session-crashed bubbles off.
The ``agent-browser doctor --offline --quick`` step at build time
verifies the binary launches; subsequent runtime calls inherit
the env automatically.
Net: smaller image (no ~150 MB Chrome-for-Testing download),
ARM64-clean, env-driven config so future flag tweaks land without
touching the agent-browser install.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Picks up the recent in-image deps (``pip install caido-sdk-client``
for ``python_action`` + Caido CLI bumped to v0.56.0). 0.2.0 is the
new minor since this is the first SDK-migration-era image; users
pulling the new strix should pull the matching new image.
Updated:
- ``strix/config/settings.py:64`` — ``RuntimeSettings.image`` default
- ``strix/runtime/session_manager.py`` + ``strix/orchestration/scan.py`` — docstring example
- ``HARNESS_WIKI.md`` — three references in the runtime + config docs
- ``MIGRATION_EVALUATION.md`` — the SDK-bridging note
The historical changelog row (``HARNESS_WIKI.md:744`` — "bump to
0.1.13") stays untouched on purpose; it records what commit
``640bd67`` did, not the current pin.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When ``_respawn_subagents`` skipped an agent because it was in
``bus.stopping`` (the user clicked stop before the crash), the bus
state was left untouched — status stayed ``running`` forever, so
``view_agent_graph`` and the TUI tree showed phantom agents that
would never make progress.
Now the skip path collects those agent ids and finalizes each as
``stopped`` outside the lock, which transitions status correctly,
clears the ``stopping`` entry (``finalize`` already discards it),
moves the live stats to ``stats_completed``, and triggers the
post-finalize snapshot. A subsequent ``view_agent_graph`` shows the
truth: the agent is stopped.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two follow-ups from the post-fix audit:
**#1 critical**: ``orchestration/scan.py`` injects the user's new
``--instruction`` into the root's bus inbox via ``bus.send`` on resume,
but ``send`` is one of the deliberately-not-snapshotted high-frequency
mutations. A SIGKILL between that send and the model's first turn
would silently drop the user's new directive. Force a snapshot
immediately after the inject — that's the one specific message we
can't afford to lose, while leaving general ``send`` traffic
unsnapshotted as designed.
**Notes persistence**: ``strix/tools/notes/tools.py`` now mirrors the
todo pattern. ``_notes_storage`` writes through to
``{run_dir}/notes.json`` after every create/update/delete via the
same atomic-tempfile + ``Path.replace`` flow. New
``hydrate_notes_from_disk(run_dir)`` is wired in ``run_strix_scan``
alongside ``hydrate_todos_from_disk`` so a resumed scan recovers the
exact note set the prior process saw, including ``wiki``-category
notes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three critical correctness fixes + six TUI/audit/UX fixes from the
parallel-agent audit. All changes verified by an end-to-end smoke
that builds, persists, and re-hydrates state across two simulated
process boundaries.
Critical (resume integrity):
1. ``bus.cancel_descendants_graceful`` now calls ``_maybe_snapshot``
after mutating the ``stopping`` set. Previously, a process crash
between user-initiated graceful-stop and the next finalize lost
the stop signal — respawned agents would run forever instead of
exiting. ``_respawn_subagents`` also gains a guard that skips
agents in ``stopping`` so a previously-cancelled agent is not
resurrected on resume.
2. ``Tracer.hydrate_from_run_dir`` now **raises** on corrupt
``vulnerabilities.json`` instead of swallowing the exception. The
prior behaviour silently reset ``vulnerability_reports`` to empty,
so the next ``add_vulnerability_report`` would allocate ``vuln-0001``
and overwrite the prior MD on disk — silent data loss.
3. ``--instruction`` passed on resume now reaches the model. The CLI
captures whether the user explicitly passed an instruction
(``args.user_explicit_instruction``) before ``_load_resume_state``
loads the persisted one. ``run_strix_scan`` reads
``scan_config["resume_instruction"]`` and, on resume, sends the
new instruction to root's bus inbox before calling
``run_with_continuation`` (which uses ``initial_input=[]`` for SDK
replay). The inject filter surfaces it on the next turn.
4. ``--resume X`` errors loudly when ``scan_state.json`` exists but
``bus.json`` doesn't. Previously this silently fresh-started in
the same dir, confusing the user who explicitly asked to resume.
TUI / audit / UX:
5. ``Tracer.hydrate_from_run_dir`` now reads ``bus.json`` too and
pre-populates ``tracer.agents`` from the snapshot's ``statuses`` /
``names`` / ``parent_of``. Before this, the TUI tree on resume
showed only currently-running agents; completed/crashed children
from the prior run were invisible.
6. ``Tracer.hydrate_from_run_dir`` also seeds ``self._llm_stats`` from
``bus.stats_live + bus.stats_completed`` so the resume's footer
shows cumulative tokens / requests across the prior run plus the
resume segment, instead of resetting to zero.
7. ``Tracer.save_run_data`` now also writes ``run_metadata.json``
(start_time, run_id, run_name, targets, status), and
``hydrate_from_run_dir`` restores ``start_time`` from it. Prior
behaviour reset start_time to ``now()`` on every Tracer init,
breaking the final report's duration calc on resumed scans.
8. Per-agent todos persist to ``{run_dir}/todos.json`` (atomic write
on every CRUD). ``hydrate_todos_from_disk`` (called from
``run_strix_scan``) reloads them so respawned subagents find
their lists intact. Previously, the module-level
``_todos_storage`` was lost on every process restart.
9. ``_load_resume_state`` validates each ``cloned_repo_path`` from
the persisted ``scan_state.json`` still exists on disk. Previously
a deleted clone dir would let the resume proceed with an empty
source tree, with agents silently scanning nothing.
Bonus: ``bus.finalize`` no longer pops ``parent_of`` and ``names``
for finalized agents. Routing protection (don't accept ``send`` to
finalized agents) comes from the ``statuses[id]`` terminal-state
check in ``send`` itself, so dropping those keys was overzealous and
made completed children invisible in ``view_agent_graph`` and the
TUI tree.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds an explicit ``--resume RUN_NAME`` flag that loads the prior
run's persisted scan state from ``strix_runs/<run_name>/scan_state.json``
and replays it (targets, scan_mode, instruction, local_sources,
diff_scope, scope_mode, diff_base) so the user never has to retype
their original args.
The exit panel now suggests ``strix --resume <run_name>`` instead of
``--run-name``. Same single-line, same dim-label / coloured-value
styling as ``Target`` / ``Output`` rows, gated on
``not scan_completed``.
CLI contract:
* ``--resume X`` cannot be combined with ``--target`` (parser error).
* ``--resume X`` errors with a clear message if
``strix_runs/X/scan_state.json`` is missing.
* Fresh runs persist scan_state.json once at the end of setup —
after target normalization, repo cloning, local-source
collection, diff-scope resolution, and final instruction
composition. So whatever the agent saw on first run is exactly
what the resumed run sees.
Internally the resume path stays implicit (presence of bus.json
triggers it inside ``run_strix_scan``); ``--resume`` is a UX layer
that:
1. Sets ``args.run_name = args.resume``.
2. Pre-populates ``args.targets_info`` and friends from disk.
3. Skips the fresh-only steps (target re-parse, repo clone,
diff-scope re-resolution) — the persisted values were already
finalized on the first run.
HARNESS_WIKI.md: drop the "delete the run dir to force fresh"
instruction.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When a scan ends without calling ``finish_scan`` (Ctrl+C, TUI quit,
crash), ``display_completion_message`` now appends one extra line
inside the existing completion panel:
Resume strix --run-name <run_name>
Same ``dim``-label / coloured-value styling as the panel's ``Target``
and ``Output`` rows. Only rendered when ``scan_completed`` is False —
a finished scan doesn't need a resume nudge.
Triggers ``orchestration/scan.py``'s implicit-resume path on the next
invocation (presence of ``{run_dir}/bus.json`` is the trigger), so
the user gets back exactly where they left off — root + every
non-terminal subagent's full LLM history, bus topology, prior
findings.
Covers both ``run_cli`` and ``run_tui`` paths since
``display_completion_message`` is called from ``main()`` regardless
of which front-end ran.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When the user shuts down a run (Ctrl+C in CLI, Ctrl+Q / quit dialog
in TUI, or an uncaught exception during the scan), print a Rich
panel telling them the exact command to pick up where they left off:
strix --run-name <run_name>
The panel only appears when ``strix_runs/<run_name>/bus.json``
exists — i.e. the scan registered at least the root agent and has
snapshot state worth resuming from. Suppressed when:
* No run-name was assigned (Ctrl+C before sandbox bring-up).
* The run dir doesn't exist or has no bus.json yet.
Implementation:
* ``strix/interface/utils.py`` gains ``format_resume_hint(run_name)
-> Panel | None``.
* ``cli.py`` calls it in the SIGINT/SIGTERM/SIGHUP handler before
``sys.exit(1)``, and in the ``except Exception`` arm before the
re-raise.
* ``tui.py:run_tui`` calls it in a ``finally`` after
``app.run_async()`` so the hint lands on the real terminal once
Textual has restored it (whether the user pressed Ctrl+Q,
confirmed the quit dialog, or the run completed naturally).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A scan that crashes or is stopped can now be resumed by re-invoking
``strix`` with the same ``--run-name``. Resume is implicit — presence
of ``{run_dir}/bus.json`` triggers it. To force a fresh start, delete
the run dir.
What survives a process restart with the same scan_id:
* Root agent's LLM history — already worked (root SDK SQLiteSession).
* Every non-terminal subagent's LLM history — new. ``create_agent``
now opens SQLiteSession(session_id=child_id,
db_path={run_dir}/sessions/{child_id}.db) per child and passes it
to ``run_with_continuation``.
* Bus topology — new. ``AgentMessageBus`` gains snapshot/restore/
_maybe_snapshot async methods plus a ``metadata`` field that holds
per-agent {task, skills, is_whitebox, scan_mode, diff_scope}.
``register``, ``finalize``, ``park``, and ``mark_llm_failed`` each
call ``_maybe_snapshot`` to atomically persist the bus to
{run_dir}/bus.json (tempfile + Path.replace).
* Vulnerability reports — new. ``ScanArtifactWriter._write_
vulnerabilities`` now also writes ``vulnerabilities.json``
(atomic). ``Tracer.hydrate_from_run_dir`` reads it on resume so
new vuln-NNNN ids don't collide with prior on-disk files.
What does not survive: the sandbox container itself (fresh per
process), so ``/workspace/scratch`` and Caido state are lost.
``/workspace/sources`` re-mounts from the host so source code is
unchanged.
``orchestration/scan.py:run_strix_scan`` does the actual resume:
1. Resolve run_dir up front; if bus.json exists it's a resume.
2. Acquire {run_dir}/.lock (fcntl.flock) so a second strix process
can't run concurrently on the same scan_id.
3. ``bus.set_snapshot_path(...)``, ``tracer.hydrate_from_run_dir()``.
4. On resume: load + bus.restore, find root_id from snapshot (the
agent with parent_of[id] is None), spawn the sandbox, skip the
root's bus.register (already in snapshot).
5. ``_respawn_subagents`` walks every agent with status in
running/waiting/llm_failed: reopens its SQLiteSession, rebuilds
the child agent via the captured factory, builds run config /
context, asyncio.create_task the run with initial_input=[] so
the SDK replays from session. Per-child failure (missing/corrupt
DB, factory raises) finalizes that child as crashed and continues.
6. Open root SQLiteSession at the same path, run the root with
initial_input=[] on resume (or the formatted root task on a
fresh run), and let SDK replay drive the next turn.
7. ``finally``: close every per-agent session, take a final
snapshot, tear down sandbox, release the lock.
HARNESS_WIKI.md updated with the new run-dir layout (sessions/,
bus.json, vulnerabilities.json, .lock) and the resume contract.
Net: +500 LoC across 7 files. No new deps.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>