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:
+10
-17
@@ -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
@@ -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]:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
"""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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::
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user