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