refactor: scrub migration scars, dead code, and unused helpers

- Strip PLAYBOOK / AUDIT / Phase-N / C-numbered references from
  module docstrings across 16 files; rename
  ``_PHASE1_PARALLEL_DEFAULT`` → ``_PARALLEL_TOOL_CALLS_DEFAULT``.
- Delete unused exception classes: ``SandboxInitializationError``,
  ``ImplementedInClientSideOnlyError``.
- Delete the no-op ``on_handoff`` hook (we don't use SDK handoffs).
- Delete the unreachable backward-compat tab-delimited fallback in
  ``_parse_git_diff_output``.
- Delete orphaned ``strix/tools/load_skill/`` (dir contained only a
  pycache) and stale pycache files.
- Rewrite ``strix/skills/__init__.py``: 168 → 56 LoC. Drop seven
  helper functions (``get_available_skills``, ``get_all_skill_names``,
  ``validate_skill_names``, ``parse_skill_list``,
  ``validate_requested_skills``, ``generate_skills_description``,
  ``_get_all_categories``) — none had external callers; only
  ``load_skills`` is used.
- Drop the stale ``strix/agents/sdk_factory.py`` per-file ruff ignore
  (file no longer exists).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
0xallam
2026-04-25 12:05:24 -07:00
parent e4be5f9588
commit 4146174503
23 changed files with 212 additions and 525 deletions
+10 -17
View File
@@ -299,38 +299,31 @@ ignore = [
# CaidoCapability uses agents.tool.Tool at runtime — pydantic Field
# annotations and the cached _CAIDO_TOOLS tuple need it eagerly.
"strix/sandbox/caido_capability.py" = ["TC002"]
# Agent factory: agents.tool.Tool is used at runtime in the _BASE_TOOLS
# tuple type, not just for annotations.
"strix/agents/sdk_factory.py" = ["TC002"]
# Entry point: ``Path`` is used at runtime by the typing of the
# session_manager call; importing under TYPE_CHECKING would defer
# resolution past where mypy needs it. ``_build_root_task`` legitimately
# walks every supported target type — splitting it into per-type
# helpers would add indirection without simplifying anything.
"strix/entry.py" = ["TC003", "PLR0912"]
# Legacy tracer module — pre-existing PLR/E501 patterns; full refactor
# is out of scope for the harness migration. ``Callable`` is a runtime
# Tracer carries a long event surface and a runtime ``Callable``
# annotation on ``vulnerability_found_callback``.
"strix/telemetry/tracer.py" = ["TC003", "PLR0912", "PLR0915", "E501"]
# Legacy interface utility with intentionally many branches per supported
# scope-mode / target-type combination; refactor would obscure the
# decision tree without simplifying it.
# Interface utility branches per scope-mode / target-type combination;
# splitting would obscure the decision tree without simplifying it.
"strix/interface/utils.py" = ["PLR0912", "BLE001", "PLC0415"]
# CLI / TUI / main keep extensive lazy imports + broad exception swallows
# for resilience around terminal-rendering errors. Refactor is out of
# scope for the harness migration.
# CLI / TUI / main keep extensive lazy imports + broad exception
# swallows for resilience around terminal-rendering errors.
"strix/interface/cli.py" = ["BLE001", "PLC0415"]
"strix/interface/tui.py" = ["BLE001", "PLC0415", "PLR0912", "PLR0915", "SIM105"]
"strix/interface/main.py" = ["BLE001", "PLC0415", "PLR0912", "PLR0915"]
"strix/interface/streaming_parser.py" = ["PLC0415"]
"strix/interface/tool_components/agent_message_renderer.py" = ["PLC0415"]
# Sandbox dispatch helper has many short-circuit error returns (auth fail,
# size cap, decode fail, etc). Each is a distinct, documented failure mode
# the model needs to see verbatim — collapsing them harms readability.
# Each short-circuit error return in the sandbox dispatch is a distinct,
# documented failure mode the model needs to see verbatim.
"strix/tools/_sandbox_dispatch.py" = ["PLR0911"]
# StrixSession + StrixTracingProcessor catch broad Exception intentionally:
# the whole point is that compressor / sanitizer / disk failures must not
# tear down the agent run (C10, C16). Calls already log at exception level.
# StrixSession + StrixTracingProcessor catch broad Exception
# intentionally: compressor / sanitizer / disk failures must not tear
# down the run. Calls already log at exception level.
"strix/llm/strix_session.py" = ["BLE001"]
"strix/telemetry/strix_processor.py" = ["BLE001"]
+17 -29
View File
@@ -1,33 +1,21 @@
"""build_strix_agent — assemble an ``agents.Agent`` for root or child runs.
"""``build_strix_agent`` — assemble an ``agents.Agent`` for root or child.
This is the keystone that links Phase 2's SDK function tools, Phase 3's
graph tools, Phase 4's CaidoCapability, and the rendered Jinja prompt
from :mod:`strix.agents.prompt` into a single ``agents.Agent``
instance ready for ``Runner.run``.
Wires the SDK function tools, multi-agent graph tools,
``CaidoCapability``, and the rendered Jinja prompt into one
``agents.Agent`` ready for ``Runner.run``.
Two flavors:
- **Root** (``is_root=True``): the top-level scan agent. Carries
``finish_scan`` (terminates the scan), no ``agent_finish`` (that's
for subagents). ``tool_use_behavior`` stops on ``finish_scan`` so
the model can't accidentally keep talking after marking the scan
complete.
- **Root** (``is_root=True``): top-level scan agent. Carries
``finish_scan`` and stops there.
- **Child** (``is_root=False``): subagents spawned by the
``create_agent`` graph tool. Carries ``agent_finish``, no
``finish_scan``. ``tool_use_behavior`` stops on ``agent_finish``
(C4 — without this, the SDK loop would keep going to ``max_turns``
even after the child reported back to its parent).
``create_agent`` graph tool. Carries ``agent_finish`` and stops
there — without ``stop_at_tool_names`` the SDK loop would keep
running to ``max_turns`` even after the child reported back.
Caido tools come from ``CaidoCapability.tools()`` automatically via
the SDK's capability merge — we don't include them here. Skills are
injected via the prompt at scan-bring-up time; runtime skill loading
isn't exposed as a tool any more (the legacy implementation reached
into a global agent registry that no longer exists).
References:
- PLAYBOOK.md §4.3 (graph tool wiring)
- AUDIT.md §2.4 (C4 — stop_at_tool_names is required for subagents)
Caido tools come from ``CaidoCapability.tools()`` via the SDK's
capability merge — we don't list them here. Skills are baked into the
system prompt at scan bring-up; there's no runtime skill-loading tool.
"""
from __future__ import annotations
@@ -196,12 +184,12 @@ def make_child_factory(
) -> Any:
"""Return a callable suitable for ``ctx.context['agent_factory']``.
The Phase 3 ``create_agent`` graph tool reads
The ``create_agent`` graph tool reads
``ctx.context['agent_factory']`` and calls it with ``name=`` and
``skills=`` to build a child Agent. We snapshot the run-level
arguments (scan_mode, is_whitebox, etc.) into a closure so each
child inherits the right scan-level configuration without the
create_agent tool having to know about them.
``skills=`` to build a child ``Agent``. Run-level arguments
(``scan_mode``, ``is_whitebox``, etc.) are captured in a closure so
each child inherits the scan-level configuration without
``create_agent`` having to know about them.
"""
def _factory(*, name: str, skills: list[str]) -> Agent[Any]:
+1 -12
View File
@@ -733,18 +733,7 @@ def _parse_name_status_z(raw_output: bytes) -> list[DiffEntry]:
index += 2
continue
# Backward-compat fallback if output is tab-delimited unexpectedly.
status_fallback, has_tab, first_path = token.partition("\t")
if not has_tab:
break
fallback_code = status_fallback[:1]
fallback_similarity: int | None = None
if len(status_fallback) > 1 and status_fallback[1:].isdigit():
fallback_similarity = int(status_fallback[1:])
entries.append(
DiffEntry(status=fallback_code, path=first_path, similarity=fallback_similarity)
)
index += 1
break
return entries
+5 -9
View File
@@ -1,13 +1,9 @@
"""AnthropicCachingLitellmModel — inject cache_control on the system message.
"""``AnthropicCachingLitellmModel`` — inject ``cache_control`` on the system message.
ModelSettings.extra_body lands the field at the request top level, which
Anthropic ignores. Anthropic only honors ``cache_control`` when it is on the
message itself. We patch the input list before delegating to the parent.
References:
- PLAYBOOK.md §2.1
- AUDIT.md §2.2 (C2 — original blocker)
- AUDIT_R3.md F1 (signature: first 7 params positional, then *,)
``ModelSettings.extra_body`` lands fields at the request top level,
which Anthropic ignores. Anthropic only honors ``cache_control`` when
it is on the message itself, so we patch the input list before
delegating to the parent.
"""
from __future__ import annotations
-4
View File
@@ -6,10 +6,6 @@ route so models named ``anthropic/<model>`` go through
on the system message). Every other prefix
(``openai/`` / ``gemini/`` / ``openrouter/`` / ``litellm/...``) falls
through to the SDK's built-in litellm routing.
References:
- PLAYBOOK.md §2.7
- AUDIT_R3.md C17 (model alias validation; raise UserError on bad alias)
"""
from __future__ import annotations
+11 -18
View File
@@ -1,24 +1,17 @@
"""StrixSession — Session wrapper that runs the MemoryCompressor.
"""``StrixSession`` — Session wrapper that runs the MemoryCompressor.
The SDK's `Session` (and ``SessionABC``) protocol owns conversation history
storage. We delegate the actual storage to any underlying session
implementation (in-memory, SQLite, Redis, …) and intercept ``get_items`` so
the ``MemoryCompressor`` runs before the model sees the history.
Delegates storage to any underlying session implementation (in-memory,
SQLite, Redis, …) and intercepts ``get_items`` so the
``MemoryCompressor`` runs before the model sees the history.
Why wrap rather than reimplement:
- ``MemoryCompressor`` already encodes the pentest-tuned summarization
prompt and the 90K-token budget that Strix has been tuning for months.
Reimplementing inside a Session would lose that institutional knowledge.
- The SDK gives us a clean seam in ``get_items``: it's the last call before
``call_model_input_filter`` runs, so compressing here means the filter
sees a compressed history too.
Wrapping (rather than reimplementing) keeps the pentest-tuned
summarization prompt and 90K-token budget intact. ``get_items`` is
also the last hook before ``call_model_input_filter``, so compressing
here means the filter sees a compressed history too.
References:
- PLAYBOOK.md §2.8
- AUDIT_R2.md §1.5 (C10 — compressor exception → uncompressed fallback)
- AUDIT_R3.md §3 row W5/E2 — once compression has failed, set a flag and
skip future attempts so we don't infinite-loop on a permanently broken
compressor while the agent loop slowly drowns in context.
If compression raises, we fall back to the uncompressed history and
flip a flag so future attempts skip — a permanently broken compressor
mustn't infinite-loop the agent into context starvation.
"""
from __future__ import annotations
+9 -17
View File
@@ -1,14 +1,8 @@
"""AgentMessageBus — peer-to-peer multi-agent state owned by Strix.
"""``AgentMessageBus`` — peer-to-peer multi-agent state for one scan.
A single ``asyncio.Lock``-protected dataclass that owns inboxes,
parent edges, statuses, and per-agent stats for the lifetime of one
Strix scan.
References:
- PLAYBOOK.md §2.3
- AUDIT_R2.md §1.4 (cancel_descendants)
- AUDIT_R2.md §1.7 (stats snapshot under lock)
- AUDIT_R3.md C13 (finalize cleans up state to avoid orphaned-message leak)
"""
from __future__ import annotations
@@ -69,9 +63,8 @@ class AgentMessageBus:
async def send(self, target: str, msg: dict[str, Any]) -> None:
"""Append a message to ``target``'s inbox.
Idempotent if target was never registered: creates an empty inbox.
Messages addressed to a finalized agent are dropped silently — the
target's inbox was cleared in :meth:`finalize` (C13).
Messages addressed to a finalized agent are dropped silently —
:meth:`finalize` clears the inbox so they can't accumulate.
"""
async with self._lock:
if target not in self.statuses:
@@ -115,9 +108,8 @@ class AgentMessageBus:
async def finalize(self, agent_id: str, status: str) -> None:
"""Move an agent from live to completed; clean up routing state.
C13 (AUDIT_R3): also clears ``inboxes``, ``parent_of``, ``names`` so
sibling agents that try to send to a finished agent don't accumulate
orphan messages forever.
Also clears ``inboxes``, ``parent_of``, ``names`` so siblings
that send to a finished agent can't accumulate orphan messages.
"""
async with self._lock:
self.statuses[agent_id] = status
@@ -127,7 +119,7 @@ class AgentMessageBus:
self.names.pop(agent_id, None)
async def total_stats(self) -> dict[str, Any]:
"""Snapshot of live + completed stats. Lock-protected (C12)."""
"""Snapshot of live + completed stats."""
async with self._lock:
agg = {"in": 0, "out": 0, "cached": 0, "cost": 0.0, "calls": 0}
for stats in (*self.stats_live.values(), *self.stats_completed.values()):
@@ -138,9 +130,9 @@ class AgentMessageBus:
async def cancel_descendants(self, root_agent_id: str) -> None:
"""Cancel ``root_agent_id`` and every transitive child, leaves first.
Wired into the CLI Ctrl+C handler and TUI stop button so a root cancel
actually propagates (C9 — SDK's ``result.cancel`` does not cascade
to children spawned via ``asyncio.create_task``).
Wired into the CLI Ctrl+C handler and TUI stop button
the SDK's ``result.cancel`` doesn't cascade to children spawned
via ``asyncio.create_task``, so we walk the tree ourselves.
"""
async with self._lock:
queue = [root_agent_id]
+8 -14
View File
@@ -1,14 +1,8 @@
"""inject_messages_filter — SDK ``call_model_input_filter`` for the message bus.
"""``inject_messages_filter`` — SDK ``call_model_input_filter`` for the bus.
The SDK runs ``call_model_input_filter`` exactly once per turn before
the LLM call (``run_internal/turn_preparation.py:55-80``) and captures
the filter's output in a lambda closure for any subsequent retries
(``run_internal/model_retry.py:34-35``) — so a single drain per turn
does not lose messages on retry.
References:
- PLAYBOOK.md §2.4
- AUDIT_R3.md C14 (filter must be defensive — exception → unmodified data)
the LLM call and captures the output in a closure for any subsequent
retries — so a single drain per turn doesn't lose messages on retry.
"""
from __future__ import annotations
@@ -32,12 +26,12 @@ async def inject_messages_filter(data: CallModelData) -> ModelInputData:
Each drained message is wrapped in an ``<inter_agent_message>`` XML envelope
so the system prompt's rules around inter-agent communication apply.
Messages from the literal sender ``"user"`` (a real human via TUI) skip
the XML wrap and are added as plain user messages.
Messages from the literal sender ``"user"`` (a real human via TUI)
skip the XML wrap and are added as plain user messages.
C14: any exception inside the filter — including a malformed message dict
or a bug in ``bus.drain`` — is caught and the original ``data.model_data``
is returned unmodified. A bug in the filter must never tear down the run.
Any exception inside the filter — malformed message dict, bug in
``bus.drain``, etc. — is caught and the original ``data.model_data``
is returned unmodified. A bug here must never tear down the run.
"""
try:
if not isinstance(data.context, dict):
+13 -30
View File
@@ -1,13 +1,4 @@
"""StrixOrchestrationHooks — RunHooks subclass wiring bus + tracer + warnings.
References:
- PLAYBOOK.md §2.5
- AUDIT.md §2.5 (C5 — streaming/hook bridge)
- AUDIT_R2.md §1.3 (C8 — subagent crash detection)
- AUDIT_R3.md F2 (context types: AgentHookContext for agent_*,
RunContextWrapper otherwise; on_tool_end result is str)
- AUDIT_R3.md C15 (every hook body try/except so a hook bug never tears down the run)
"""
"""``StrixOrchestrationHooks`` — RunHooks wiring bus + tracer + warnings."""
from __future__ import annotations
@@ -27,16 +18,17 @@ class StrixOrchestrationHooks(RunHooks[Any]):
Wires four concerns:
1. Turn-budget warnings injected into ``input_items`` at 85% and ``N - 3``
of ``max_turns``.
2. LLM usage recording into the bus.
3. Sandbox readiness: awaits the ``CaidoCapability._healthcheck_task``
on first agent start so the agent doesn't fire tools before Caido and
the tool server are ready.
4. Subagent crash detection (C8): if ``on_agent_end`` fires without
``agent_finish_called`` being set in context, posts a synthetic
``<agent_crash>`` message to the parent's inbox so the parent learns
on its next turn instead of polling ``wait_for_message`` forever.
1. Turn-budget warnings injected into ``input_items`` at 85% and
``N - 3`` of ``max_turns``.
2. LLM usage recording into the bus + tracer.
3. Sandbox readiness: awaits the
``CaidoCapability._healthcheck_task`` on first agent start so
the agent doesn't fire tools before Caido and the tool server
are ready.
4. Subagent crash detection: if ``on_agent_end`` fires without
``agent_finish_called`` being set, posts a synthetic
``<agent_crash>`` message to the parent's inbox so the parent
learns on its next turn instead of waiting forever.
"""
async def on_llm_start(
@@ -105,7 +97,7 @@ class StrixOrchestrationHooks(RunHooks[Any]):
input_tokens=int(getattr(usage, "input_tokens", 0) or 0),
output_tokens=int(getattr(usage, "output_tokens", 0) or 0),
cached_tokens=cached,
cost=0.0, # litellm cost computation lives in the legacy LLM
cost=0.0,
requests=1,
bucket="live",
)
@@ -203,12 +195,3 @@ class StrixOrchestrationHooks(RunHooks[Any]):
tracer.log_tool_end(ctx.get("agent_id", "?"), tool.name, result)
except Exception:
logger.exception("on_tool_end failed")
async def on_handoff(
self,
context: RunContextWrapper[Any],
from_agent: Any,
to_agent: Any,
) -> None:
# Strix multi-agent goes through the bus; SDK handoffs are unused.
pass
+33 -51
View File
@@ -1,18 +1,7 @@
"""make_run_config — assemble a Strix-flavored ``RunConfig`` for ``Runner.run``.
"""``make_run_config`` — assemble a Strix-flavored ``RunConfig`` for ``Runner.run``.
Factory pattern: every Strix scan goes through here so the defaults are
applied uniformly. Per-call overrides are accepted via ``model_settings_override``
for the rare case a single run wants different reasoning effort or
``tool_choice`` (C21).
References:
- PLAYBOOK.md §2.10
- AUDIT.md §2.1 (C1 — parallel_tool_calls=False until Phase 6 relaxes the
tool server's per-agent task slot serialization)
- AUDIT_R2.md §1.6 (C11 — retry policy explicitly excludes 401/403/400;
auth and validation errors must fail fast, not waste retries)
- AUDIT_R3.md C21 — RunConfig override + context fields including
``is_whitebox``, ``diff_scope``, ``run_id``
Every scan goes through here so defaults apply uniformly. Per-call
overrides land via ``model_settings_override``.
"""
from __future__ import annotations
@@ -39,14 +28,12 @@ if TYPE_CHECKING:
from strix.orchestration.bus import AgentMessageBus
# Phase 6 relaxes the tool server's per-agent task-slot serialization
# (``runtime/tool_server.py:94-97``) and flips this to ``True`` after
# multi-agent stress tests confirm safety.
_PHASE1_PARALLEL_DEFAULT = False
# Sequential tool calls per agent — the tool server serializes one task
# per agent at a time, so concurrent calls would queue anyway.
_PARALLEL_TOOL_CALLS_DEFAULT = False
# Default retry policy. Explicitly does NOT include 401/403/400 — those are
# auth and validation errors that retrying cannot fix; they should fail fast
# so the user sees the real error within seconds. 429/5xx is the right set.
# Retry policy. 401/403/400 are deliberately excluded — auth and
# validation errors can't be fixed by retrying and should fail fast.
_RETRYABLE_HTTP_STATUSES = (429, 500, 502, 503, 504)
# Default retry budget: 5 attempts with ``min(90, 2*2^n)`` backoff.
@@ -82,7 +69,7 @@ def make_run_config(
*,
sandbox_session: BaseSandboxSession | None,
model: str = "anthropic/claude-sonnet-4-6",
parallel_tool_calls: bool = _PHASE1_PARALLEL_DEFAULT,
parallel_tool_calls: bool = _PARALLEL_TOOL_CALLS_DEFAULT,
tool_choice: Literal["auto", "required", "none"] | None = "required",
reasoning_effort: Literal["low", "medium", "high"] | None = None,
model_settings_override: ModelSettings | None = None,
@@ -90,32 +77,27 @@ def make_run_config(
) -> RunConfig:
"""Build a ``RunConfig`` with Strix defaults.
Note: ``max_turns`` and ``isolate_parallel_failures`` are NOT
``RunConfig`` fields — they are passed directly to ``Runner.run``.
Use ``STRIX_DEFAULT_MAX_TURNS`` for the budget; pass
``isolate_parallel_failures=False`` to ``Runner.run`` if Phase 6 has
not yet flipped ``parallel_tool_calls=True``.
Note: ``max_turns`` is not a ``RunConfig`` field — pass it directly
to ``Runner.run``. ``STRIX_DEFAULT_MAX_TURNS`` is the budget Strix
uses.
Args:
sandbox_session: Live sandbox session shared by every agent in this
scan (one container per scan; see ``strix.sandbox.session_manager``).
``None`` is allowed for unit tests and dry runs.
model: Model alias to pass to ``MultiProvider``. Defaults to the
current production-favored Anthropic alias.
parallel_tool_calls: Default ``False`` to keep behavior sequential
per the tool server's slot serialization (C1).
sandbox_session: Live sandbox session shared by every agent in
this scan (one container per scan; see
:mod:`strix.sandbox.session_manager`). ``None`` is allowed
for unit tests and dry runs.
model: Model alias passed to ``MultiProvider``. Defaults to the
production Anthropic alias.
parallel_tool_calls: Default ``False`` — the tool server
serializes one task per agent.
tool_choice: Forces tool use per turn unless explicitly relaxed.
Pass ``None`` to omit.
reasoning_effort: ``"low" | "medium" | "high"``; routes to
``ModelSettings.reasoning``. ``None`` defers to provider default.
model_settings_override: Optional ``ModelSettings`` to merge over
the factory defaults (C21 — per-run override path).
sandbox_client: Optional pre-built sandbox client (e.g., the Strix
Docker subclass). Defaults to ``None``; the SDK will instantiate
its built-in if a session is supplied without a client.
Returns:
A ``RunConfig`` ready to pass to ``Runner.run``.
``ModelSettings.reasoning``.
model_settings_override: Optional per-run ``ModelSettings``
merged over factory defaults.
sandbox_client: Optional pre-built sandbox client (Strix Docker
subclass). The SDK instantiates its built-in if a session is
supplied without a client.
"""
base_settings = ModelSettings(
parallel_tool_calls=parallel_tool_calls,
@@ -175,14 +157,14 @@ def make_agent_context(
) -> dict[str, Any]:
"""Build the per-agent ``context`` dict passed to ``Runner.run(context=...)``.
The dict is the canonical place where bus, sandbox handles, identity,
tracer reference, and per-agent toggles live. Tools, hooks, and the
``inject_messages_filter`` all reach in via ``ctx.context.get(...)``.
The canonical place where bus, sandbox handles, identity, tracer
reference, and per-agent toggles live. Tools, hooks, and
``inject_messages_filter`` reach in via ``ctx.context.get(...)``.
``agent_factory`` is a callable ``(name, skills) -> agents.Agent`` used by
the ``create_agent`` graph tool to spin up children. ``sandbox_client``
is the host-side Docker subclass; ``create_agent`` reuses it across
child runs.
``agent_factory`` is a callable ``(name, skills) -> agents.Agent``
the ``create_agent`` graph tool uses it to spin up children that
inherit the same wiring. ``sandbox_client`` is the host-side Docker
subclass, reused across child runs.
"""
return {
"bus": bus,
+8 -26
View File
@@ -1,31 +1,13 @@
"""Strix runtime package.
What lives here:
- :class:`strix.runtime.strix_docker_client.StrixDockerSandboxClient` —
host-side ``DockerSandboxClient`` subclass that injects
``NET_ADMIN`` / ``NET_RAW`` capabilities and ``host.docker.internal``
extra-hosts, used by the per-scan session manager
(:mod:`strix.sandbox.session_manager`).
- :class:`StrixDockerSandboxClient` — host-side ``DockerSandboxClient``
subclass that injects ``NET_ADMIN`` / ``NET_RAW`` capabilities and
``host.docker.internal`` extra-hosts, used by the per-scan session
manager (:mod:`strix.sandbox.session_manager`).
- ``tool_server.py`` — the FastAPI server that runs *inside* the
sandbox container; sandbox-bound tools (browser, terminal, python,
file_edit, proxy) POST here from the host via
- ``tool_server.py`` — FastAPI server that runs inside the sandbox
container. Sandbox-bound tools (browser, terminal, python, file_edit,
proxy) POST here from the host via
:func:`strix.tools._sandbox_dispatch.post_to_sandbox`.
The legacy DockerRuntime / AbstractRuntime + ``get_runtime`` /
``cleanup_runtime`` globals were removed when the SDK harness took
over scan lifecycle; sandbox sessions are now per-scan and managed by
:func:`strix.sandbox.session_manager.create_or_reuse`.
"""
class SandboxInitializationError(Exception):
"""Raised when sandbox initialization fails (e.g., Docker issues)."""
def __init__(self, message: str, details: str | None = None):
super().__init__(message)
self.message = message
self.details = details
__all__ = ["SandboxInitializationError"]
+2 -7
View File
@@ -11,13 +11,8 @@ additions before the final create call:
These are required for raw-socket pentest tools (nmap -sS) and for letting
the agent reach host-served apps via ``host.docker.internal``.
Pinned to ``openai-agents==0.14.6``. Bumping the SDK version requires
re-merging the parent body. Track upstream PR for an injection hook.
References:
- PLAYBOOK.md §2.2
- AUDIT.md §2.3 (C3 — original blocker)
- SDK source: ``/tmp/openai-agents/src/agents/sandbox/sandboxes/docker.py:1434-1477``
Pinned to ``openai-agents==0.14.6``. Bumping the SDK requires
re-merging the parent body. Track upstream for an injection hook.
"""
from __future__ import annotations
+5 -4
View File
@@ -1,7 +1,8 @@
"""Strix sandbox layer on top of OpenAI Agents SDK SandboxAgent / Manifest.
Phase 4 deliverables:
- CaidoCapability: Caido proxy + 7 GraphQL function tools + system prompt block
- healthcheck: wait_for_ports_ready
- session_manager: create_or_reuse / cleanup keyed by scan_id
- :mod:`.caido_capability` — Caido proxy + 7 GraphQL function tools
+ system prompt block.
- :mod:`.healthcheck` — ``wait_for_ports_ready``.
- :mod:`.session_manager` — ``create_or_reuse`` / ``cleanup`` keyed
by scan id.
"""
+3 -8
View File
@@ -8,10 +8,9 @@ Three concerns wired into the SDK's capability lifecycle:
etc.) now flows through the proxy automatically.
2. **Tool exposure** (``tools``): the seven Caido SDK function-tool
wrappers from Phase 2.5 are returned here. The SDK runtime collects
tools from every capability and merges them with the agent's
``tools=[...]`` declaration, so individual agents don't have to
redeclare them.
wrappers are returned here. The SDK runtime collects tools from
every capability and merges them with the agent's ``tools=[...]``
declaration, so agents don't have to redeclare them.
3. **Healthcheck task** (``bind``): when a session binds, we kick off
:func:`wait_for_http_ready` against the FastAPI tool server's
@@ -21,10 +20,6 @@ Three concerns wired into the SDK's capability lifecycle:
:class:`StrixOrchestrationHooks.on_agent_start` hook awaits before
the first LLM call so the agent never hits a connection-refused
on its very first tool invocation.
References:
- PLAYBOOK.md §3.2
- AUDIT.md §2.5 (C5 — healthcheck wired to RunHooks)
"""
from __future__ import annotations
-3
View File
@@ -16,9 +16,6 @@ Two helpers are exposed:
- :func:`wait_for_tcp_ready` for Caido, which serves an HTTP forward
proxy on its port and does *not* expose ``/health``. A TCP connect
is the most we can probe without sending real proxy traffic.
References:
- PLAYBOOK.md §3.1
"""
from __future__ import annotations
-3
View File
@@ -12,9 +12,6 @@ issuing multiple ``create_or_reuse`` calls (e.g., resume after a crash
on the host side) gets the same bundle back. ``cleanup`` is best-effort
— a leaked container is preferable to a stuck cleanup that prevents the
next scan from starting.
References:
- PLAYBOOK.md §3.3
"""
from __future__ import annotations
+38 -149
View File
@@ -1,167 +1,56 @@
import logging
import re
from strix.utils.resource_paths import get_strix_resource_path
_EXCLUDED_CATEGORIES = {"scan_modes", "coordination"}
logger = logging.getLogger(__name__)
_FRONTMATTER_PATTERN = re.compile(r"^---\s*\n.*?\n---\s*\n", re.DOTALL)
def get_available_skills() -> dict[str, list[str]]:
skills_dir = get_strix_resource_path("skills")
available_skills: dict[str, list[str]] = {}
if not skills_dir.exists():
return available_skills
for category_dir in skills_dir.iterdir():
if category_dir.is_dir() and not category_dir.name.startswith("__"):
category_name = category_dir.name
if category_name in _EXCLUDED_CATEGORIES:
continue
skills = []
for file_path in category_dir.glob("*.md"):
skill_name = file_path.stem
skills.append(skill_name)
if skills:
available_skills[category_name] = sorted(skills)
return available_skills
def get_all_skill_names() -> set[str]:
all_skills = set()
for category_skills in get_available_skills().values():
all_skills.update(category_skills)
return all_skills
def validate_skill_names(skill_names: list[str]) -> dict[str, list[str]]:
available_skills = get_all_skill_names()
valid_skills = []
invalid_skills = []
for skill_name in skill_names:
if skill_name in available_skills:
valid_skills.append(skill_name)
else:
invalid_skills.append(skill_name)
return {"valid": valid_skills, "invalid": invalid_skills}
def parse_skill_list(skills: str | None) -> list[str]:
if not skills:
return []
return [s.strip() for s in skills.split(",") if s.strip()]
def validate_requested_skills(skill_list: list[str], max_skills: int = 5) -> str | None:
if len(skill_list) > max_skills:
return "Cannot specify more than 5 skills for an agent (use comma-separated format)"
if not skill_list:
return None
validation = validate_skill_names(skill_list)
if validation["invalid"]:
available_skills = list(get_all_skill_names())
return (
f"Invalid skills: {validation['invalid']}. "
f"Available skills: {', '.join(available_skills)}"
)
return None
def generate_skills_description() -> str:
available_skills = get_available_skills()
if not available_skills:
return "No skills available"
all_skill_names = get_all_skill_names()
if not all_skill_names:
return "No skills available"
sorted_skills = sorted(all_skill_names)
skills_str = ", ".join(sorted_skills)
description = f"List of skills to load for this agent (max 5). Available skills: {skills_str}. "
example_skills = sorted_skills[:2]
if example_skills:
example = f"Example: {', '.join(example_skills)} for specialized agent"
description += example
return description
def _get_all_categories() -> dict[str, list[str]]:
"""Get all skill categories including internal ones (scan_modes, coordination)."""
skills_dir = get_strix_resource_path("skills")
all_categories: dict[str, list[str]] = {}
if not skills_dir.exists():
return all_categories
for category_dir in skills_dir.iterdir():
if category_dir.is_dir() and not category_dir.name.startswith("__"):
category_name = category_dir.name
skills = []
for file_path in category_dir.glob("*.md"):
skill_name = file_path.stem
skills.append(skill_name)
if skills:
all_categories[category_name] = sorted(skills)
return all_categories
def load_skills(skill_names: list[str]) -> dict[str, str]:
import logging
"""Load skill markdown bodies (frontmatter stripped) by name.
logger = logging.getLogger(__name__)
skill_content = {}
Skill files live at ``strix/skills/<category>/<name>.md``. Names
can be ``"name"`` (any category), ``"category/name"``, or a bare
file at the skills root. Missing skills are logged and skipped.
"""
skills_dir = get_strix_resource_path("skills")
if not skills_dir.exists():
return {}
all_categories = _get_all_categories()
by_category: dict[str, str] = {}
for category_dir in skills_dir.iterdir():
if not category_dir.is_dir() or category_dir.name.startswith("__"):
continue
for file_path in category_dir.glob("*.md"):
by_category[file_path.stem] = f"{category_dir.name}/{file_path.stem}.md"
skill_content: dict[str, str] = {}
for skill_name in skill_names:
rel_path: str | None
if "/" in skill_name:
rel_path = f"{skill_name}.md"
elif skill_name in by_category:
rel_path = by_category[skill_name]
elif (skills_dir / f"{skill_name}.md").exists():
rel_path = f"{skill_name}.md"
else:
rel_path = None
if rel_path is None or not (skills_dir / rel_path).exists():
logger.warning("Skill not found: %s", skill_name)
continue
try:
skill_path = None
content = (skills_dir / rel_path).read_text(encoding="utf-8")
except (OSError, ValueError) as e:
logger.warning("Failed to load skill %s: %s", skill_name, e)
continue
if "/" in skill_name:
skill_path = f"{skill_name}.md"
else:
for category, skills in all_categories.items():
if skill_name in skills:
skill_path = f"{category}/{skill_name}.md"
break
if not skill_path:
root_candidate = f"{skill_name}.md"
if (skills_dir / root_candidate).exists():
skill_path = root_candidate
if skill_path and (skills_dir / skill_path).exists():
full_path = skills_dir / skill_path
var_name = skill_name.split("/")[-1]
content = full_path.read_text(encoding="utf-8")
content = _FRONTMATTER_PATTERN.sub("", content).lstrip()
skill_content[var_name] = content
logger.info(f"Loaded skill: {skill_name} -> {var_name}")
else:
logger.warning(f"Skill not found: {skill_name}")
except (FileNotFoundError, OSError, ValueError) as e:
logger.warning(f"Failed to load skill {skill_name}: {e}")
var_name = skill_name.split("/")[-1]
skill_content[var_name] = _FRONTMATTER_PATTERN.sub("", content).lstrip()
logger.info("Loaded skill: %s -> %s", skill_name, var_name)
return skill_content
+10 -22
View File
@@ -1,14 +1,8 @@
"""StrixTracingProcessor — SDK trace processor that writes events.jsonl.
"""``StrixTracingProcessor`` — SDK trace processor that writes ``events.jsonl``.
Hooks into the SDK's tracing pipeline and writes events to
``strix_runs/<run-name>/events.jsonl``. PII scrubbing via the existing
``TelemetrySanitizer``.
References:
- PLAYBOOK.md §2.9
- AUDIT_R2.md §1.2 (C7 — JSONL writes must be lock-protected)
- AUDIT_R3.md C16 (writes must catch OSError; never tear down the run)
- AUDIT_R3.md F3 (every TracingProcessor hook is SYNC, not async)
``strix_runs/<run-name>/events.jsonl``. PII scrubbing runs through
:class:`TelemetrySanitizer`.
"""
from __future__ import annotations
@@ -47,17 +41,11 @@ def _lock_for(path: Path) -> threading.Lock:
class StrixTracingProcessor(TracingProcessor):
"""Append trace + span events as JSONL into ``run_dir/events.jsonl``.
Every hook is synchronous — required by ``TracingProcessor`` ABC.
Every write is protected by a per-path ``threading.Lock`` so concurrent
spans (e.g., from parallel agent tasks) cannot interleave bytes
mid-line and corrupt the JSONL (C7).
Every write is wrapped in ``try/except OSError`` so a full disk or a
permission error during the run does NOT propagate up the hook chain
and tear down the agent (C16).
PII scrubbing via :class:`TelemetrySanitizer` runs on every event
before it hits the file.
Every hook is synchronous — required by the ``TracingProcessor``
ABC. Each write is protected by a per-path ``threading.Lock`` so
concurrent spans can't interleave bytes mid-line. ``OSError`` is
swallowed so a full disk or permission error doesn't tear the run
down. PII scrubbing runs on every event before it hits the file.
"""
def __init__(
@@ -80,8 +68,8 @@ class StrixTracingProcessor(TracingProcessor):
def _emit(self, event: dict[str, Any]) -> None:
"""Sanitize ``event`` and append it as one JSONL line.
Failures are swallowed — we'd rather lose a trace event than fail
the run. Errors are logged at WARNING (C16).
Failures are swallowed — we'd rather lose a trace event than
fail the run.
"""
try:
clean = self.sanitizer.sanitize(event)
+1 -8
View File
@@ -17,13 +17,7 @@ from .finish import * # noqa: F403
from .notes import * # noqa: F403
from .proxy import * # noqa: F403
from .python import * # noqa: F403
from .registry import (
ImplementedInClientSideOnlyError,
get_tool_by_name,
get_tool_names,
register_tool,
tools,
)
from .registry import get_tool_by_name, get_tool_names, register_tool, tools
from .reporting import * # noqa: F403
from .terminal import * # noqa: F403
from .thinking import * # noqa: F403
@@ -32,7 +26,6 @@ from .web_search import * # noqa: F403
__all__ = [
"ImplementedInClientSideOnlyError",
"get_tool_by_name",
"get_tool_names",
"register_tool",
+12 -28
View File
@@ -1,24 +1,15 @@
"""strix_tool — function_tool factory with Strix defaults.
"""``strix_tool````function_tool`` factory with Strix defaults.
Every tool in the migrated harness should be decorated with ``@strix_tool``
instead of bare ``@function_tool`` so the team's defaults stay consistent
without per-tool boilerplate. Override per call when needed.
Every tool uses ``@strix_tool`` instead of bare ``@function_tool`` so
defaults stay consistent across the suite. Override per call when
needed.
Defaults:
- ``timeout``: 120s (matches the legacy tool server's
``STRIX_SANDBOX_EXECUTION_TIMEOUT``).
- ``timeout``: 120s.
- ``timeout_behavior``: ``"error_as_result"`` for idempotent tools.
Critical sandbox tools (terminal, browser, python) should pass
``timeout_behavior="raise_exception"`` explicitly so the SDK can fail
the run rather than letting the model retry the same hung call (C20).
The SDK auto-threads sync function bodies via ``asyncio.to_thread``
(``tool.py:1820-1829``), so libtmux / IPython / blocking httpx code can be
written as plain ``def`` and the decorator will not block the event loop.
References:
- PLAYBOOK.md §2.6
- AUDIT_R3.md C20 (per-tool timeout_behavior discrimination)
Critical sandbox tools (terminal, browser, python) opt into
``timeout_behavior="raise_exception"`` explicitly so the SDK
fails the run rather than letting the model retry a hung call.
"""
from __future__ import annotations
@@ -44,17 +35,10 @@ def strix_tool(
) -> Callable[[_ToolFn], FunctionTool]:
"""Wrap ``agents.function_tool`` with Strix defaults.
The SDK's ``FunctionTool`` requires ``async def`` for ``timeout_seconds``
to apply (sync handlers cannot be cleanly cancelled). All Strix tools are
``async def``; sync libraries (libtmux, IPython) get wrapped in
``asyncio.to_thread`` inside the async tool body.
The SDK enforces ``strict_mode=True`` by default, which forbids
free-form ``dict[str, X]`` parameters (the strict JSON schema needs
``additionalProperties: false``). A handful of legacy tools
(``send_request``, ``repeat_request``) take arbitrary header /
modification dicts whose keys can't be enumerated, so they must
opt out of strict mode to preserve parity with the XML schema.
Strict mode is on by default (forbids free-form ``dict[str, X]``
parameters because the strict JSON schema needs
``additionalProperties: false``). A few tools that take arbitrary
header / modification dicts opt out via ``strict_mode=False``.
Usage::
+10 -25
View File
@@ -1,24 +1,14 @@
"""post_to_sandbox — host-to-container HTTP transport for sandbox tools.
"""``post_to_sandbox`` — host-to-container HTTP transport for sandbox tools.
Every Strix tool that runs inside the Kali container (browser, terminal,
python, the seven Caido tools) has the same wire shape: POST a JSON body
to ``http://localhost:{tool_server_host_port}/execute`` with a Bearer
token header and ``{"agent_id", "tool_name", "kwargs"}`` as the body.
Every Strix tool that runs inside the Kali container (browser,
terminal, python, file_edit, the seven Caido tools) has the same wire
shape: POST JSON to ``http://localhost:{tool_server_host_port}/execute``
with a Bearer token and ``{"agent_id", "tool_name", "kwargs"}`` body.
This helper centralizes that transport so:
- Every sandbox tool gets the same timeout policy
(``connect=10s`` / ``read=150s``).
- Every sandbox tool inherits the same response-size cap (50 MB) so a
runaway tool body cannot OOM the host (C18).
- Auth/transport errors surface as predictable error strings instead of
exceptions, so the model can retry / pick a different tool without the
run dying.
References:
- PLAYBOOK.md §3.4
- AUDIT_R3.md C18 (sandbox response size cap)
- HARNESS_WIKI.md §7.2 (legacy executor.py wire format we mirror)
The helper centralizes timeouts (``connect=10s`` / ``read=150s``), a
50 MB response-size cap so a runaway tool can't OOM the host, and
predictable error-string shaping so transport failures don't tear
down the run.
"""
from __future__ import annotations
@@ -36,14 +26,9 @@ if TYPE_CHECKING:
logger = logging.getLogger(__name__)
# Connect: how long to wait for the TCP handshake to complete.
# Read: how long the tool may spend executing before we abandon the call.
# Mirrors the legacy executor.py (``SANDBOX_EXECUTION_TIMEOUT = 120 + 30``).
_SANDBOX_TIMEOUT = httpx.Timeout(connect=10.0, read=150.0, write=150.0, pool=150.0)
#: Cap on response body size from the tool server. Anything bigger is
#: replaced by an error string so the model sees something coherent and
#: the host doesn't OOM trying to allocate the buffer (C18).
# Cap so a runaway tool body never blows up the host heap.
_MAX_RESPONSE_BYTES = 50 * 1024 * 1024 # 50 MB
+12 -26
View File
@@ -1,29 +1,16 @@
"""SDK function-tool wrappers for the multi-agent graph tools.
"""Multi-agent graph tools — read/write the :class:`AgentMessageBus`.
Six tools that read/write the :class:`AgentMessageBus` (built in Phase 0,
``strix.orchestration.bus``):
- ``view_agent_graph``: render the parent/child tree from ``bus.parent_of``.
- ``view_agent_graph``: render the parent/child tree.
- ``agent_status``: per-agent status + pending message count.
- ``send_message_to_agent``: peer-to-peer message into a child/sibling inbox.
- ``wait_for_message``: poll our own inbox until a message arrives or the
timeout expires (the legacy harness's "I'm idle, wake me on inbox").
- ``create_agent``: spawn a child via ``asyncio.create_task(Runner.run(...))``;
registers the child with the bus and stores its task handle so root cancels
cascade (C9, ``bus.cancel_descendants``).
- ``agent_finish``: subagents only — flips ``agent_finish_called`` so the
on_agent_end hook records "completed" rather than "crashed" (C8), and
posts a structured completion report to the parent's inbox.
The legacy ``strix.tools.agents_graph.agents_graph_actions`` is left
untouched — it still drives the legacy harness. These wrappers only
target the bus and don't touch the legacy ``_agent_graph`` dict.
References:
- PLAYBOOK.md §4.3
- AUDIT_R2.md §1.4 (cancel_descendants — Runner.run task handle stored
in bus.tasks so a root cancel walks the tree)
- AUDIT_R3.md C8 (crash detection via on_agent_end + agent_finish_called)
- ``send_message_to_agent``: queue a message in another agent's inbox.
- ``wait_for_message``: pause this agent until a message arrives or
``timeout_seconds`` elapses.
- ``create_agent``: spawn a child via
``asyncio.create_task(Runner.run(...))``; the task handle is stored
so a root-level cancel cascades to descendants.
- ``agent_finish``: subagents only — flips ``agent_finish_called`` so
the ``on_agent_end`` hook records "completed" rather than "crashed",
and posts a structured completion report to the parent's inbox.
"""
from __future__ import annotations
@@ -223,8 +210,7 @@ async def send_message_to_agent(
)
# Polling cadence for ``wait_for_message``. 1s matches the PLAYBOOK
# skeleton; tighter would burn CPU, slacker would feel laggy when a sibling
# Tighter would burn CPU; slacker would feel laggy when a sibling
# delivers a message right after the wait starts.
_WAIT_POLL_SECONDS = 1.0
+4 -15
View File
@@ -1,14 +1,14 @@
"""Minimal in-container tool registry.
Used inside the sandbox container by ``strix.runtime.tool_server`` to
look up `@register_tool`-decorated functions by name. Sandbox-bound
tools (browser, terminal, python, file_edit, proxy) live as legacy
look up ``@register_tool``-decorated functions by name. Sandbox-bound
tools (browser, terminal, python, file_edit, proxy) live as
``*_actions.py`` modules with this decoration; the host POSTs to
:func:`tool_server.execute_tool` which dispatches via
:func:`get_tool_by_name`.
Host-side tools are pure SDK function tools wired through
:mod:`strix.agents.factory` and don't touch this registry at all.
Host-side SDK function tools are wired through
:mod:`strix.agents.factory` and don't touch this registry.
"""
import logging
@@ -25,17 +25,6 @@ tools: list[dict[str, Any]] = []
_tools_by_name: dict[str, Callable[..., Any]] = {}
class ImplementedInClientSideOnlyError(Exception):
"""Raised by sandbox-side stubs whose real implementation lives host-side."""
def __init__(
self,
message: str = "This tool is implemented in the client side only",
) -> None:
self.message = message
super().__init__(self.message)
def _is_sandbox_mode() -> bool:
return os.getenv("STRIX_SANDBOX_MODE", "false").lower() == "true"