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 # CaidoCapability uses agents.tool.Tool at runtime — pydantic Field
# annotations and the cached _CAIDO_TOOLS tuple need it eagerly. # annotations and the cached _CAIDO_TOOLS tuple need it eagerly.
"strix/sandbox/caido_capability.py" = ["TC002"] "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 # Entry point: ``Path`` is used at runtime by the typing of the
# session_manager call; importing under TYPE_CHECKING would defer # session_manager call; importing under TYPE_CHECKING would defer
# resolution past where mypy needs it. ``_build_root_task`` legitimately # resolution past where mypy needs it. ``_build_root_task`` legitimately
# walks every supported target type — splitting it into per-type # walks every supported target type — splitting it into per-type
# helpers would add indirection without simplifying anything. # helpers would add indirection without simplifying anything.
"strix/entry.py" = ["TC003", "PLR0912"] "strix/entry.py" = ["TC003", "PLR0912"]
# Legacy tracer module — pre-existing PLR/E501 patterns; full refactor # Tracer carries a long event surface and a runtime ``Callable``
# is out of scope for the harness migration. ``Callable`` is a runtime
# annotation on ``vulnerability_found_callback``. # annotation on ``vulnerability_found_callback``.
"strix/telemetry/tracer.py" = ["TC003", "PLR0912", "PLR0915", "E501"] "strix/telemetry/tracer.py" = ["TC003", "PLR0912", "PLR0915", "E501"]
# Legacy interface utility with intentionally many branches per supported # Interface utility branches per scope-mode / target-type combination;
# scope-mode / target-type combination; refactor would obscure the # splitting would obscure the decision tree without simplifying it.
# decision tree without simplifying it.
"strix/interface/utils.py" = ["PLR0912", "BLE001", "PLC0415"] "strix/interface/utils.py" = ["PLR0912", "BLE001", "PLC0415"]
# CLI / TUI / main keep extensive lazy imports + broad exception swallows # CLI / TUI / main keep extensive lazy imports + broad exception
# for resilience around terminal-rendering errors. Refactor is out of # swallows for resilience around terminal-rendering errors.
# scope for the harness migration.
"strix/interface/cli.py" = ["BLE001", "PLC0415"] "strix/interface/cli.py" = ["BLE001", "PLC0415"]
"strix/interface/tui.py" = ["BLE001", "PLC0415", "PLR0912", "PLR0915", "SIM105"] "strix/interface/tui.py" = ["BLE001", "PLC0415", "PLR0912", "PLR0915", "SIM105"]
"strix/interface/main.py" = ["BLE001", "PLC0415", "PLR0912", "PLR0915"] "strix/interface/main.py" = ["BLE001", "PLC0415", "PLR0912", "PLR0915"]
"strix/interface/streaming_parser.py" = ["PLC0415"] "strix/interface/streaming_parser.py" = ["PLC0415"]
"strix/interface/tool_components/agent_message_renderer.py" = ["PLC0415"] "strix/interface/tool_components/agent_message_renderer.py" = ["PLC0415"]
# Sandbox dispatch helper has many short-circuit error returns (auth fail, # Each short-circuit error return in the sandbox dispatch is a distinct,
# size cap, decode fail, etc). Each is a distinct, documented failure mode # documented failure mode the model needs to see verbatim.
# the model needs to see verbatim — collapsing them harms readability.
"strix/tools/_sandbox_dispatch.py" = ["PLR0911"] "strix/tools/_sandbox_dispatch.py" = ["PLR0911"]
# StrixSession + StrixTracingProcessor catch broad Exception intentionally: # StrixSession + StrixTracingProcessor catch broad Exception
# the whole point is that compressor / sanitizer / disk failures must not # intentionally: compressor / sanitizer / disk failures must not tear
# tear down the agent run (C10, C16). Calls already log at exception level. # down the run. Calls already log at exception level.
"strix/llm/strix_session.py" = ["BLE001"] "strix/llm/strix_session.py" = ["BLE001"]
"strix/telemetry/strix_processor.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 Wires the SDK function tools, multi-agent graph tools,
graph tools, Phase 4's CaidoCapability, and the rendered Jinja prompt ``CaidoCapability``, and the rendered Jinja prompt into one
from :mod:`strix.agents.prompt` into a single ``agents.Agent`` ``agents.Agent`` ready for ``Runner.run``.
instance ready for ``Runner.run``.
Two flavors: Two flavors:
- **Root** (``is_root=True``): the top-level scan agent. Carries - **Root** (``is_root=True``): top-level scan agent. Carries
``finish_scan`` (terminates the scan), no ``agent_finish`` (that's ``finish_scan`` and stops there.
for subagents). ``tool_use_behavior`` stops on ``finish_scan`` so
the model can't accidentally keep talking after marking the scan
complete.
- **Child** (``is_root=False``): subagents spawned by the - **Child** (``is_root=False``): subagents spawned by the
``create_agent`` graph tool. Carries ``agent_finish``, no ``create_agent`` graph tool. Carries ``agent_finish`` and stops
``finish_scan``. ``tool_use_behavior`` stops on ``agent_finish`` there — without ``stop_at_tool_names`` the SDK loop would keep
(C4 — without this, the SDK loop would keep going to ``max_turns`` running to ``max_turns`` even after the child reported back.
even after the child reported back to its parent).
Caido tools come from ``CaidoCapability.tools()`` automatically via Caido tools come from ``CaidoCapability.tools()`` via the SDK's
the SDK's capability merge — we don't include them here. Skills are capability merge — we don't list them here. Skills are baked into the
injected via the prompt at scan-bring-up time; runtime skill loading system prompt at scan bring-up; there's no runtime skill-loading tool.
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)
""" """
from __future__ import annotations from __future__ import annotations
@@ -196,12 +184,12 @@ def make_child_factory(
) -> Any: ) -> Any:
"""Return a callable suitable for ``ctx.context['agent_factory']``. """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 ``ctx.context['agent_factory']`` and calls it with ``name=`` and
``skills=`` to build a child Agent. We snapshot the run-level ``skills=`` to build a child ``Agent``. Run-level arguments
arguments (scan_mode, is_whitebox, etc.) into a closure so each (``scan_mode``, ``is_whitebox``, etc.) are captured in a closure so
child inherits the right scan-level configuration without the each child inherits the scan-level configuration without
create_agent tool having to know about them. ``create_agent`` having to know about them.
""" """
def _factory(*, name: str, skills: list[str]) -> Agent[Any]: 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 index += 2
continue continue
# Backward-compat fallback if output is tab-delimited unexpectedly. break
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
return entries 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 ``ModelSettings.extra_body`` lands fields at the request top level,
Anthropic ignores. Anthropic only honors ``cache_control`` when it is on the which Anthropic ignores. Anthropic only honors ``cache_control`` when
message itself. We patch the input list before delegating to the parent. it is on the message itself, so 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 *,)
""" """
from __future__ import annotations 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 on the system message). Every other prefix
(``openai/`` / ``gemini/`` / ``openrouter/`` / ``litellm/...``) falls (``openai/`` / ``gemini/`` / ``openrouter/`` / ``litellm/...``) falls
through to the SDK's built-in litellm routing. 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 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 Delegates storage to any underlying session implementation (in-memory,
storage. We delegate the actual storage to any underlying session SQLite, Redis, …) and intercepts ``get_items`` so the
implementation (in-memory, SQLite, Redis, …) and intercept ``get_items`` so ``MemoryCompressor`` runs before the model sees the history.
the ``MemoryCompressor`` runs before the model sees the history.
Why wrap rather than reimplement: Wrapping (rather than reimplementing) keeps the pentest-tuned
- ``MemoryCompressor`` already encodes the pentest-tuned summarization summarization prompt and 90K-token budget intact. ``get_items`` is
prompt and the 90K-token budget that Strix has been tuning for months. also the last hook before ``call_model_input_filter``, so compressing
Reimplementing inside a Session would lose that institutional knowledge. here means the filter sees a compressed history too.
- 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.
References: If compression raises, we fall back to the uncompressed history and
- PLAYBOOK.md §2.8 flip a flag so future attempts skip — a permanently broken compressor
- AUDIT_R2.md §1.5 (C10 — compressor exception → uncompressed fallback) mustn't infinite-loop the agent into context starvation.
- 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.
""" """
from __future__ import annotations 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, A single ``asyncio.Lock``-protected dataclass that owns inboxes,
parent edges, statuses, and per-agent stats for the lifetime of one parent edges, statuses, and per-agent stats for the lifetime of one
Strix scan. 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 from __future__ import annotations
@@ -69,9 +63,8 @@ class AgentMessageBus:
async def send(self, target: str, msg: dict[str, Any]) -> None: async def send(self, target: str, msg: dict[str, Any]) -> None:
"""Append a message to ``target``'s inbox. """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 —
Messages addressed to a finalized agent are dropped silently — the :meth:`finalize` clears the inbox so they can't accumulate.
target's inbox was cleared in :meth:`finalize` (C13).
""" """
async with self._lock: async with self._lock:
if target not in self.statuses: if target not in self.statuses:
@@ -115,9 +108,8 @@ class AgentMessageBus:
async def finalize(self, agent_id: str, status: str) -> None: async def finalize(self, agent_id: str, status: str) -> None:
"""Move an agent from live to completed; clean up routing state. """Move an agent from live to completed; clean up routing state.
C13 (AUDIT_R3): also clears ``inboxes``, ``parent_of``, ``names`` so Also clears ``inboxes``, ``parent_of``, ``names`` so siblings
sibling agents that try to send to a finished agent don't accumulate that send to a finished agent can't accumulate orphan messages.
orphan messages forever.
""" """
async with self._lock: async with self._lock:
self.statuses[agent_id] = status self.statuses[agent_id] = status
@@ -127,7 +119,7 @@ class AgentMessageBus:
self.names.pop(agent_id, None) self.names.pop(agent_id, None)
async def total_stats(self) -> dict[str, Any]: 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: async with self._lock:
agg = {"in": 0, "out": 0, "cached": 0, "cost": 0.0, "calls": 0} agg = {"in": 0, "out": 0, "cached": 0, "cost": 0.0, "calls": 0}
for stats in (*self.stats_live.values(), *self.stats_completed.values()): 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: async def cancel_descendants(self, root_agent_id: str) -> None:
"""Cancel ``root_agent_id`` and every transitive child, leaves first. """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 Wired into the CLI Ctrl+C handler and TUI stop button
actually propagates (C9 — SDK's ``result.cancel`` does not cascade the SDK's ``result.cancel`` doesn't cascade to children spawned
to children spawned via ``asyncio.create_task``). via ``asyncio.create_task``, so we walk the tree ourselves.
""" """
async with self._lock: async with self._lock:
queue = [root_agent_id] 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 SDK runs ``call_model_input_filter`` exactly once per turn before
the LLM call (``run_internal/turn_preparation.py:55-80``) and captures the LLM call and captures the output in a closure for any subsequent
the filter's output in a lambda closure for any subsequent retries retries — so a single drain per turn doesn't lose messages on retry.
(``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)
""" """
from __future__ import annotations 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 Each drained message is wrapped in an ``<inter_agent_message>`` XML envelope
so the system prompt's rules around inter-agent communication apply. so the system prompt's rules around inter-agent communication apply.
Messages from the literal sender ``"user"`` (a real human via TUI) skip Messages from the literal sender ``"user"`` (a real human via TUI)
the XML wrap and are added as plain user messages. skip the XML wrap and are added as plain user messages.
C14: any exception inside the filter — including a malformed message dict Any exception inside the filter — malformed message dict, bug in
or a bug in ``bus.drain`` — is caught and the original ``data.model_data`` ``bus.drain``, etc. — is caught and the original ``data.model_data``
is returned unmodified. A bug in the filter must never tear down the run. is returned unmodified. A bug here must never tear down the run.
""" """
try: try:
if not isinstance(data.context, dict): if not isinstance(data.context, dict):
+13 -30
View File
@@ -1,13 +1,4 @@
"""StrixOrchestrationHooks — RunHooks subclass wiring bus + tracer + warnings. """``StrixOrchestrationHooks`` — RunHooks 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)
"""
from __future__ import annotations from __future__ import annotations
@@ -27,16 +18,17 @@ class StrixOrchestrationHooks(RunHooks[Any]):
Wires four concerns: Wires four concerns:
1. Turn-budget warnings injected into ``input_items`` at 85% and ``N - 3`` 1. Turn-budget warnings injected into ``input_items`` at 85% and
of ``max_turns``. ``N - 3`` of ``max_turns``.
2. LLM usage recording into the bus. 2. LLM usage recording into the bus + tracer.
3. Sandbox readiness: awaits the ``CaidoCapability._healthcheck_task`` 3. Sandbox readiness: awaits the
on first agent start so the agent doesn't fire tools before Caido and ``CaidoCapability._healthcheck_task`` on first agent start so
the tool server are ready. the agent doesn't fire tools before Caido and the tool server
4. Subagent crash detection (C8): if ``on_agent_end`` fires without are ready.
``agent_finish_called`` being set in context, posts a synthetic 4. Subagent crash detection: if ``on_agent_end`` fires without
``<agent_crash>`` message to the parent's inbox so the parent learns ``agent_finish_called`` being set, posts a synthetic
on its next turn instead of polling ``wait_for_message`` forever. ``<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( async def on_llm_start(
@@ -105,7 +97,7 @@ class StrixOrchestrationHooks(RunHooks[Any]):
input_tokens=int(getattr(usage, "input_tokens", 0) or 0), input_tokens=int(getattr(usage, "input_tokens", 0) or 0),
output_tokens=int(getattr(usage, "output_tokens", 0) or 0), output_tokens=int(getattr(usage, "output_tokens", 0) or 0),
cached_tokens=cached, cached_tokens=cached,
cost=0.0, # litellm cost computation lives in the legacy LLM cost=0.0,
requests=1, requests=1,
bucket="live", bucket="live",
) )
@@ -203,12 +195,3 @@ class StrixOrchestrationHooks(RunHooks[Any]):
tracer.log_tool_end(ctx.get("agent_id", "?"), tool.name, result) tracer.log_tool_end(ctx.get("agent_id", "?"), tool.name, result)
except Exception: except Exception:
logger.exception("on_tool_end failed") 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 Every scan goes through here so defaults apply uniformly. Per-call
applied uniformly. Per-call overrides are accepted via ``model_settings_override`` overrides land 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``
""" """
from __future__ import annotations from __future__ import annotations
@@ -39,14 +28,12 @@ if TYPE_CHECKING:
from strix.orchestration.bus import AgentMessageBus from strix.orchestration.bus import AgentMessageBus
# Phase 6 relaxes the tool server's per-agent task-slot serialization # Sequential tool calls per agent — the tool server serializes one task
# (``runtime/tool_server.py:94-97``) and flips this to ``True`` after # per agent at a time, so concurrent calls would queue anyway.
# multi-agent stress tests confirm safety. _PARALLEL_TOOL_CALLS_DEFAULT = False
_PHASE1_PARALLEL_DEFAULT = False
# Default retry policy. Explicitly does NOT include 401/403/400 — those are # Retry policy. 401/403/400 are deliberately excluded — auth and
# auth and validation errors that retrying cannot fix; they should fail fast # validation errors can't be fixed by retrying and should fail fast.
# so the user sees the real error within seconds. 429/5xx is the right set.
_RETRYABLE_HTTP_STATUSES = (429, 500, 502, 503, 504) _RETRYABLE_HTTP_STATUSES = (429, 500, 502, 503, 504)
# Default retry budget: 5 attempts with ``min(90, 2*2^n)`` backoff. # Default retry budget: 5 attempts with ``min(90, 2*2^n)`` backoff.
@@ -82,7 +69,7 @@ def make_run_config(
*, *,
sandbox_session: BaseSandboxSession | None, sandbox_session: BaseSandboxSession | None,
model: str = "anthropic/claude-sonnet-4-6", 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", tool_choice: Literal["auto", "required", "none"] | None = "required",
reasoning_effort: Literal["low", "medium", "high"] | None = None, reasoning_effort: Literal["low", "medium", "high"] | None = None,
model_settings_override: ModelSettings | None = None, model_settings_override: ModelSettings | None = None,
@@ -90,32 +77,27 @@ def make_run_config(
) -> RunConfig: ) -> RunConfig:
"""Build a ``RunConfig`` with Strix defaults. """Build a ``RunConfig`` with Strix defaults.
Note: ``max_turns`` and ``isolate_parallel_failures`` are NOT Note: ``max_turns`` is not a ``RunConfig`` field — pass it directly
``RunConfig`` fields — they are passed directly to ``Runner.run``. to ``Runner.run``. ``STRIX_DEFAULT_MAX_TURNS`` is the budget Strix
Use ``STRIX_DEFAULT_MAX_TURNS`` for the budget; pass uses.
``isolate_parallel_failures=False`` to ``Runner.run`` if Phase 6 has
not yet flipped ``parallel_tool_calls=True``.
Args: Args:
sandbox_session: Live sandbox session shared by every agent in this sandbox_session: Live sandbox session shared by every agent in
scan (one container per scan; see ``strix.sandbox.session_manager``). this scan (one container per scan; see
``None`` is allowed for unit tests and dry runs. :mod:`strix.sandbox.session_manager`). ``None`` is allowed
model: Model alias to pass to ``MultiProvider``. Defaults to the for unit tests and dry runs.
current production-favored Anthropic alias. model: Model alias passed to ``MultiProvider``. Defaults to the
parallel_tool_calls: Default ``False`` to keep behavior sequential production Anthropic alias.
per the tool server's slot serialization (C1). parallel_tool_calls: Default ``False`` — the tool server
serializes one task per agent.
tool_choice: Forces tool use per turn unless explicitly relaxed. tool_choice: Forces tool use per turn unless explicitly relaxed.
Pass ``None`` to omit.
reasoning_effort: ``"low" | "medium" | "high"``; routes to reasoning_effort: ``"low" | "medium" | "high"``; routes to
``ModelSettings.reasoning``. ``None`` defers to provider default. ``ModelSettings.reasoning``.
model_settings_override: Optional ``ModelSettings`` to merge over model_settings_override: Optional per-run ``ModelSettings``
the factory defaults (C21 — per-run override path). merged over factory defaults.
sandbox_client: Optional pre-built sandbox client (e.g., the Strix sandbox_client: Optional pre-built sandbox client (Strix Docker
Docker subclass). Defaults to ``None``; the SDK will instantiate subclass). The SDK instantiates its built-in if a session is
its built-in if a session is supplied without a client. supplied without a client.
Returns:
A ``RunConfig`` ready to pass to ``Runner.run``.
""" """
base_settings = ModelSettings( base_settings = ModelSettings(
parallel_tool_calls=parallel_tool_calls, parallel_tool_calls=parallel_tool_calls,
@@ -175,14 +157,14 @@ def make_agent_context(
) -> dict[str, Any]: ) -> dict[str, Any]:
"""Build the per-agent ``context`` dict passed to ``Runner.run(context=...)``. """Build the per-agent ``context`` dict passed to ``Runner.run(context=...)``.
The dict is the canonical place where bus, sandbox handles, identity, The canonical place where bus, sandbox handles, identity, tracer
tracer reference, and per-agent toggles live. Tools, hooks, and the reference, and per-agent toggles live. Tools, hooks, and
``inject_messages_filter`` all reach in via ``ctx.context.get(...)``. ``inject_messages_filter`` reach in via ``ctx.context.get(...)``.
``agent_factory`` is a callable ``(name, skills) -> agents.Agent`` used by ``agent_factory`` is a callable ``(name, skills) -> agents.Agent``
the ``create_agent`` graph tool to spin up children. ``sandbox_client`` the ``create_agent`` graph tool uses it to spin up children that
is the host-side Docker subclass; ``create_agent`` reuses it across inherit the same wiring. ``sandbox_client`` is the host-side Docker
child runs. subclass, reused across child runs.
""" """
return { return {
"bus": bus, "bus": bus,
+8 -26
View File
@@ -1,31 +1,13 @@
"""Strix runtime package. """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`` - ``tool_server.py`` — FastAPI server that runs inside the sandbox
subclass that injects ``NET_ADMIN`` / ``NET_RAW`` capabilities and container. Sandbox-bound tools (browser, terminal, python, file_edit,
``host.docker.internal`` extra-hosts, used by the per-scan session proxy) POST here from the host via
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
:func:`strix.tools._sandbox_dispatch.post_to_sandbox`. :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 These are required for raw-socket pentest tools (nmap -sS) and for letting
the agent reach host-served apps via ``host.docker.internal``. the agent reach host-served apps via ``host.docker.internal``.
Pinned to ``openai-agents==0.14.6``. Bumping the SDK version requires Pinned to ``openai-agents==0.14.6``. Bumping the SDK requires
re-merging the parent body. Track upstream PR for an injection hook. re-merging the parent body. Track upstream 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``
""" """
from __future__ import annotations from __future__ import annotations
+5 -4
View File
@@ -1,7 +1,8 @@
"""Strix sandbox layer on top of OpenAI Agents SDK SandboxAgent / Manifest. """Strix sandbox layer on top of OpenAI Agents SDK SandboxAgent / Manifest.
Phase 4 deliverables: - :mod:`.caido_capability` — Caido proxy + 7 GraphQL function tools
- CaidoCapability: Caido proxy + 7 GraphQL function tools + system prompt block + system prompt block.
- healthcheck: wait_for_ports_ready - :mod:`.healthcheck` — ``wait_for_ports_ready``.
- session_manager: create_or_reuse / cleanup keyed by scan_id - :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. etc.) now flows through the proxy automatically.
2. **Tool exposure** (``tools``): the seven Caido SDK function-tool 2. **Tool exposure** (``tools``): the seven Caido SDK function-tool
wrappers from Phase 2.5 are returned here. The SDK runtime collects wrappers are returned here. The SDK runtime collects tools from
tools from every capability and merges them with the agent's every capability and merges them with the agent's ``tools=[...]``
``tools=[...]`` declaration, so individual agents don't have to declaration, so agents don't have to redeclare them.
redeclare them.
3. **Healthcheck task** (``bind``): when a session binds, we kick off 3. **Healthcheck task** (``bind``): when a session binds, we kick off
:func:`wait_for_http_ready` against the FastAPI tool server's :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 :class:`StrixOrchestrationHooks.on_agent_start` hook awaits before
the first LLM call so the agent never hits a connection-refused the first LLM call so the agent never hits a connection-refused
on its very first tool invocation. on its very first tool invocation.
References:
- PLAYBOOK.md §3.2
- AUDIT.md §2.5 (C5 — healthcheck wired to RunHooks)
""" """
from __future__ import annotations 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 - :func:`wait_for_tcp_ready` for Caido, which serves an HTTP forward
proxy on its port and does *not* expose ``/health``. A TCP connect proxy on its port and does *not* expose ``/health``. A TCP connect
is the most we can probe without sending real proxy traffic. is the most we can probe without sending real proxy traffic.
References:
- PLAYBOOK.md §3.1
""" """
from __future__ import annotations 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 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 — a leaked container is preferable to a stuck cleanup that prevents the
next scan from starting. next scan from starting.
References:
- PLAYBOOK.md §3.3
""" """
from __future__ import annotations from __future__ import annotations
+38 -149
View File
@@ -1,167 +1,56 @@
import logging
import re import re
from strix.utils.resource_paths import get_strix_resource_path 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) _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]: 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 files live at ``strix/skills/<category>/<name>.md``. Names
skill_content = {} 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") 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: 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: 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: var_name = skill_name.split("/")[-1]
skill_path = f"{skill_name}.md" skill_content[var_name] = _FRONTMATTER_PATTERN.sub("", content).lstrip()
else: logger.info("Loaded skill: %s -> %s", skill_name, var_name)
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}")
return skill_content 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 Hooks into the SDK's tracing pipeline and writes events to
``strix_runs/<run-name>/events.jsonl``. PII scrubbing via the existing ``strix_runs/<run-name>/events.jsonl``. PII scrubbing runs through
``TelemetrySanitizer``. :class:`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)
""" """
from __future__ import annotations from __future__ import annotations
@@ -47,17 +41,11 @@ def _lock_for(path: Path) -> threading.Lock:
class StrixTracingProcessor(TracingProcessor): class StrixTracingProcessor(TracingProcessor):
"""Append trace + span events as JSONL into ``run_dir/events.jsonl``. """Append trace + span events as JSONL into ``run_dir/events.jsonl``.
Every hook is synchronous — required by ``TracingProcessor`` ABC. Every hook is synchronous — required by the ``TracingProcessor``
Every write is protected by a per-path ``threading.Lock`` so concurrent ABC. Each write is protected by a per-path ``threading.Lock`` so
spans (e.g., from parallel agent tasks) cannot interleave bytes concurrent spans can't interleave bytes mid-line. ``OSError`` is
mid-line and corrupt the JSONL (C7). 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.
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.
""" """
def __init__( def __init__(
@@ -80,8 +68,8 @@ class StrixTracingProcessor(TracingProcessor):
def _emit(self, event: dict[str, Any]) -> None: def _emit(self, event: dict[str, Any]) -> None:
"""Sanitize ``event`` and append it as one JSONL line. """Sanitize ``event`` and append it as one JSONL line.
Failures are swallowed — we'd rather lose a trace event than fail Failures are swallowed — we'd rather lose a trace event than
the run. Errors are logged at WARNING (C16). fail the run.
""" """
try: try:
clean = self.sanitizer.sanitize(event) clean = self.sanitizer.sanitize(event)
+1 -8
View File
@@ -17,13 +17,7 @@ from .finish import * # noqa: F403
from .notes import * # noqa: F403 from .notes import * # noqa: F403
from .proxy import * # noqa: F403 from .proxy import * # noqa: F403
from .python import * # noqa: F403 from .python import * # noqa: F403
from .registry import ( from .registry import get_tool_by_name, get_tool_names, register_tool, tools
ImplementedInClientSideOnlyError,
get_tool_by_name,
get_tool_names,
register_tool,
tools,
)
from .reporting import * # noqa: F403 from .reporting import * # noqa: F403
from .terminal import * # noqa: F403 from .terminal import * # noqa: F403
from .thinking import * # noqa: F403 from .thinking import * # noqa: F403
@@ -32,7 +26,6 @@ from .web_search import * # noqa: F403
__all__ = [ __all__ = [
"ImplementedInClientSideOnlyError",
"get_tool_by_name", "get_tool_by_name",
"get_tool_names", "get_tool_names",
"register_tool", "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`` Every tool uses ``@strix_tool`` instead of bare ``@function_tool`` so
instead of bare ``@function_tool`` so the team's defaults stay consistent defaults stay consistent across the suite. Override per call when
without per-tool boilerplate. Override per call when needed. needed.
Defaults: Defaults:
- ``timeout``: 120s (matches the legacy tool server's - ``timeout``: 120s.
``STRIX_SANDBOX_EXECUTION_TIMEOUT``).
- ``timeout_behavior``: ``"error_as_result"`` for idempotent tools. - ``timeout_behavior``: ``"error_as_result"`` for idempotent tools.
Critical sandbox tools (terminal, browser, python) should pass Critical sandbox tools (terminal, browser, python) opt into
``timeout_behavior="raise_exception"`` explicitly so the SDK can fail ``timeout_behavior="raise_exception"`` explicitly so the SDK
the run rather than letting the model retry the same hung call (C20). fails the run rather than letting the model retry a hung call.
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)
""" """
from __future__ import annotations from __future__ import annotations
@@ -44,17 +35,10 @@ def strix_tool(
) -> Callable[[_ToolFn], FunctionTool]: ) -> Callable[[_ToolFn], FunctionTool]:
"""Wrap ``agents.function_tool`` with Strix defaults. """Wrap ``agents.function_tool`` with Strix defaults.
The SDK's ``FunctionTool`` requires ``async def`` for ``timeout_seconds`` Strict mode is on by default (forbids free-form ``dict[str, X]``
to apply (sync handlers cannot be cleanly cancelled). All Strix tools are parameters because the strict JSON schema needs
``async def``; sync libraries (libtmux, IPython) get wrapped in ``additionalProperties: false``). A few tools that take arbitrary
``asyncio.to_thread`` inside the async tool body. header / modification dicts opt out via ``strict_mode=False``.
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.
Usage:: 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, Every Strix tool that runs inside the Kali container (browser,
python, the seven Caido tools) has the same wire shape: POST a JSON body terminal, python, file_edit, the seven Caido tools) has the same wire
to ``http://localhost:{tool_server_host_port}/execute`` with a Bearer shape: POST JSON to ``http://localhost:{tool_server_host_port}/execute``
token header and ``{"agent_id", "tool_name", "kwargs"}`` as the body. with a Bearer token and ``{"agent_id", "tool_name", "kwargs"}`` body.
This helper centralizes that transport so: The helper centralizes timeouts (``connect=10s`` / ``read=150s``), a
50 MB response-size cap so a runaway tool can't OOM the host, and
- Every sandbox tool gets the same timeout policy predictable error-string shaping so transport failures don't tear
(``connect=10s`` / ``read=150s``). down the run.
- 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)
""" """
from __future__ import annotations from __future__ import annotations
@@ -36,14 +26,9 @@ if TYPE_CHECKING:
logger = logging.getLogger(__name__) 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) _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 # Cap so a runaway tool body never blows up the host heap.
#: replaced by an error string so the model sees something coherent and
#: the host doesn't OOM trying to allocate the buffer (C18).
_MAX_RESPONSE_BYTES = 50 * 1024 * 1024 # 50 MB _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, - ``view_agent_graph``: render the parent/child tree.
``strix.orchestration.bus``):
- ``view_agent_graph``: render the parent/child tree from ``bus.parent_of``.
- ``agent_status``: per-agent status + pending message count. - ``agent_status``: per-agent status + pending message count.
- ``send_message_to_agent``: peer-to-peer message into a child/sibling inbox. - ``send_message_to_agent``: queue a message in another agent's inbox.
- ``wait_for_message``: poll our own inbox until a message arrives or the - ``wait_for_message``: pause this agent until a message arrives or
timeout expires (the legacy harness's "I'm idle, wake me on inbox"). ``timeout_seconds`` elapses.
- ``create_agent``: spawn a child via ``asyncio.create_task(Runner.run(...))``; - ``create_agent``: spawn a child via
registers the child with the bus and stores its task handle so root cancels ``asyncio.create_task(Runner.run(...))``; the task handle is stored
cascade (C9, ``bus.cancel_descendants``). so a root-level cancel cascades to descendants.
- ``agent_finish``: subagents only — flips ``agent_finish_called`` so the - ``agent_finish``: subagents only — flips ``agent_finish_called`` so
on_agent_end hook records "completed" rather than "crashed" (C8), and the ``on_agent_end`` hook records "completed" rather than "crashed",
posts a structured completion report to the parent's inbox. 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)
""" """
from __future__ import annotations from __future__ import annotations
@@ -223,8 +210,7 @@ async def send_message_to_agent(
) )
# Polling cadence for ``wait_for_message``. 1s matches the PLAYBOOK # Tighter would burn CPU; slacker would feel laggy when a sibling
# skeleton; tighter would burn CPU, slacker would feel laggy when a sibling
# delivers a message right after the wait starts. # delivers a message right after the wait starts.
_WAIT_POLL_SECONDS = 1.0 _WAIT_POLL_SECONDS = 1.0
+4 -15
View File
@@ -1,14 +1,14 @@
"""Minimal in-container tool registry. """Minimal in-container tool registry.
Used inside the sandbox container by ``strix.runtime.tool_server`` to Used inside the sandbox container by ``strix.runtime.tool_server`` to
look up `@register_tool`-decorated functions by name. Sandbox-bound look up ``@register_tool``-decorated functions by name. Sandbox-bound
tools (browser, terminal, python, file_edit, proxy) live as legacy tools (browser, terminal, python, file_edit, proxy) live as
``*_actions.py`` modules with this decoration; the host POSTs to ``*_actions.py`` modules with this decoration; the host POSTs to
:func:`tool_server.execute_tool` which dispatches via :func:`tool_server.execute_tool` which dispatches via
:func:`get_tool_by_name`. :func:`get_tool_by_name`.
Host-side tools are pure SDK function tools wired through Host-side SDK function tools are wired through
:mod:`strix.agents.factory` and don't touch this registry at all. :mod:`strix.agents.factory` and don't touch this registry.
""" """
import logging import logging
@@ -25,17 +25,6 @@ tools: list[dict[str, Any]] = []
_tools_by_name: dict[str, Callable[..., 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: def _is_sandbox_mode() -> bool:
return os.getenv("STRIX_SANDBOX_MODE", "false").lower() == "true" return os.getenv("STRIX_SANDBOX_MODE", "false").lower() == "true"