fix(runtime,interface): mount sources at advertised paths + surface scan failures in TUI
Two fixes that surfaced from a single broken run. (1) Source mounting was double-broken: - ``session_manager.create_or_reuse`` mounted the *parent* of the first local source under a hardcoded ``"sources"`` key, so the host's unrelated content leaked in at ``/workspace/sources/...`` while the agent's task prompt advertised ``/workspace/<workspace_subdir>`` (from ``_build_root_task``). Result: the agent looked at ``/workspace/empty/`` (per the prompt), found nothing, and bailed. - ``backends._docker_backend`` never called ``await session.start()`` after ``client.create()`` — the SDK's manifest application (``LocalDir`` materialization, mount setup) only runs inside ``start()`` (or ``async with session:``). So even with the right ``entries`` the workspace would have been empty anyway. Fix: thread ``args.local_sources`` (already populated by ``collect_local_sources``) all the way through to the session manager, build ``Manifest.entries`` keyed by each source's ``workspace_subdir``, and call ``session.start()`` in the docker backend so the SDK actually materializes the entries. Drop the now-unused ``_resolve_sources_path`` helpers from ``cli.py`` and ``tui.py``. (2) Scan-failure visibility was nonexistent in TUI mode: - The SDK's ``on_agent_end`` hook only fires after the agent reaches its first turn. A failure earlier (model routing, sandbox bring-up, …) left the root agent stuck at ``status=running`` in the bus and tracer, so the TUI animated "Initializing" forever. - ``scan_target`` in ``tui.py`` caught the exception and called ``logging.exception`` but never propagated it. ``run_tui`` returned cleanly when the user finally ctrl-q'd, so ``main.py`` happily printed the success-completion banner over a dead scan. Fix: in ``run_strix_scan``'s ``except BaseException`` block, finalize the root agent as ``"failed"`` in both the bus and the tracer (with the error message attached). Capture the exception on ``StrixTUIApp._scan_error`` from the scan thread; ``run_tui`` re-raises it after ``app.run_async()`` returns so ``main.py``'s existing handler prints the traceback. Add a ``"failed"`` branch to ``_get_status_display_content`` that shows the error message in red, mirroring the existing ``llm_failed`` branch. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -48,6 +48,13 @@ async def _docker_backend(
|
||||
NET_RAW caps + ``host.docker.internal`` host-gateway. Imports
|
||||
``docker`` lazily so deployments that target a non-Docker
|
||||
backend don't need the docker-py library installed.
|
||||
|
||||
``session.start()`` is what materializes the manifest entries
|
||||
(LocalDir copies, mount setup, etc.) into the running container —
|
||||
the SDK's ``client.create()`` only builds the inner session object
|
||||
without applying the manifest. ``async with session:`` would call it
|
||||
too, but Strix manages session lifetime explicitly via
|
||||
``client.delete()`` so we trigger ``start()`` ourselves.
|
||||
"""
|
||||
import docker
|
||||
from agents.sandbox.sandboxes.docker import DockerSandboxClientOptions
|
||||
@@ -57,6 +64,7 @@ async def _docker_backend(
|
||||
client = StrixDockerSandboxClient(docker.from_env())
|
||||
options = DockerSandboxClientOptions(image=image, exposed_ports=exposed_ports)
|
||||
session = await client.create(options=options, manifest=manifest)
|
||||
await session.start()
|
||||
return client, session
|
||||
|
||||
|
||||
|
||||
@@ -16,9 +16,10 @@ next scan from starting.
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from agents.sandbox.entries import LocalDir
|
||||
from agents.sandbox.entries import BaseEntry, LocalDir
|
||||
from agents.sandbox.manifest import Environment, Manifest
|
||||
|
||||
from strix.config import load_settings
|
||||
@@ -26,10 +27,6 @@ from strix.runtime.backends import get_backend
|
||||
from strix.runtime.caido_bootstrap import bootstrap_caido
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -48,15 +45,17 @@ async def create_or_reuse(
|
||||
scan_id: str,
|
||||
*,
|
||||
image: str,
|
||||
sources_path: Path,
|
||||
local_sources: list[dict[str, str]],
|
||||
) -> dict[str, Any]:
|
||||
"""Return the existing bundle for ``scan_id`` or create a new one.
|
||||
|
||||
Args:
|
||||
scan_id: Caller-provided scan identifier (used as cache key).
|
||||
image: Docker image tag (e.g. ``"strix-sandbox:0.2.0"``).
|
||||
sources_path: Host directory mounted into the container's
|
||||
``/workspace/sources`` so the agent can read user code.
|
||||
local_sources: Each entry's ``source_path`` (host) is mounted at
|
||||
``/workspace/<workspace_subdir>`` inside the container — the
|
||||
same path the root-task prompt advertises. Empty list means
|
||||
no host code is mounted (web/IP-only scans).
|
||||
|
||||
Returns the bundle dict containing ``client``, ``session``, and
|
||||
``caido_client``.
|
||||
@@ -66,6 +65,19 @@ async def create_or_reuse(
|
||||
logger.info("Reusing existing sandbox session for scan %s", scan_id)
|
||||
return cached
|
||||
|
||||
# Build Manifest entries keyed by ``workspace_subdir`` — the SDK
|
||||
# mounts each at ``/workspace/<key>``, which is exactly the path
|
||||
# ``_build_root_task`` puts in the agent's task prompt. Mounting
|
||||
# only the listed source dirs (not their parent) avoids leaking
|
||||
# unrelated host content into the sandbox.
|
||||
entries: dict[str | Path, BaseEntry] = {}
|
||||
for src in local_sources:
|
||||
ws_subdir = src.get("workspace_subdir") or ""
|
||||
host_path = src.get("source_path") or ""
|
||||
if not ws_subdir or not host_path:
|
||||
continue
|
||||
entries[ws_subdir] = LocalDir(src=Path(host_path).expanduser().resolve())
|
||||
|
||||
# Caido runs as an in-container sidecar; HTTP(S) traffic from any
|
||||
# process started via ``session.exec`` (the SDK's Shell tool, etc.)
|
||||
# picks up these env vars automatically. ``NO_PROXY`` keeps the
|
||||
@@ -73,7 +85,7 @@ async def create_or_reuse(
|
||||
# through Caido.
|
||||
container_caido_url = f"http://127.0.0.1:{_CONTAINER_CAIDO_PORT}"
|
||||
manifest = Manifest(
|
||||
entries={"sources": LocalDir(src=sources_path)},
|
||||
entries=entries,
|
||||
environment=Environment(
|
||||
value={
|
||||
"PYTHONUNBUFFERED": "1",
|
||||
|
||||
Reference in New Issue
Block a user