Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/advanced/configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,10 @@ When remote vars are set, Strix dual-writes telemetry to both local JSONL and th

## Sandbox Configuration

<ParamField path="STRIX_REQUIRE_MOUNT_FREE" default="0" type="string">
Enforces mount-free (bind-mount-free) transport for local-code Docker scans. Set to `1`, `true`, `yes`, or `on` to ensure no host directories are bind-mounted into the sandbox container. Local project files are transferred as an isolated snapshot payload. The snapshot lives in the container's writable layer and is discarded with it; in-tree directory symlinks are omitted; symlinked projects are read into memory.
</ParamField>

<ParamField path="STRIX_SANDBOX_EXECUTION_TIMEOUT" default="120" type="integer">
Maximum execution time in seconds for sandbox operations.
</ParamField>
Expand Down
4 changes: 4 additions & 0 deletions strix/config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,10 @@ class RuntimeSettings(BaseSettings):
alias="STRIX_IMAGE",
)
backend: str = Field(default="docker", alias="STRIX_RUNTIME_BACKEND")
require_mount_free: bool = Field(
default=False,
alias="STRIX_REQUIRE_MOUNT_FREE",
)
# Max screenshot/image tool outputs kept live per agent context (0 = none).
max_context_images: int = Field(default=3, ge=0, alias="STRIX_MAX_CONTEXT_IMAGES")

Expand Down
36 changes: 23 additions & 13 deletions strix/core/inputs.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from agents.model_settings import ModelSettings
from openai.types.shared import Reasoning

from strix.config.loader import load_settings
from strix.config.models import (
DEFAULT_MODEL_RETRY,
OPENROUTER_ATTRIBUTION_HEADERS,
Expand All @@ -21,6 +22,7 @@
routes_through_litellm,
)
from strix.core.sessions import scrub_images_from_items
from strix.runtime.backends import backend_supports_bind_mounts


if TYPE_CHECKING:
Expand Down Expand Up @@ -105,10 +107,24 @@ def _render_workspace_files(scan_config: dict[str, Any]) -> list[str]:
]


def _describe_directory_mount() -> str:
backend_name = load_settings().runtime.backend
use_bind_mounts = (
backend_supports_bind_mounts(backend_name)
and not load_settings().runtime.require_mount_free
)
if use_bind_mounts:
return (
"this is the user's real directory, mounted live and writable — "
".git/.agents/.codex are read-only"
)
return "this is a bounded snapshot of the user's directory, isolated from the live host system"


def build_root_task(scan_config: dict[str, Any]) -> str:
targets = scan_config.get("targets", []) or []
diff_scope = scan_config.get("diff_scope") or {}
user_instructions = scan_config.get("user_instructions", "") or ""
user_instructions = (scan_config.get("user_instructions") or "").strip()

sections: dict[str, list[str]] = {
"Repositories": [],
Expand All @@ -120,8 +136,8 @@ def build_root_task(scan_config: dict[str, Any]) -> str:

for target in targets:
ttype = target.get("type")
details = target.get("details") or {}
workspace_subdir = details.get("workspace_subdir")
details = target.get("details") or target.get("target_details") or {}
workspace_subdir = details.get("workspace_subdir") or target.get("workspace_subdir") or ""
workspace_path = f"/workspace/{workspace_subdir}" if workspace_subdir else "/workspace"

if ttype == "repository":
Expand All @@ -132,11 +148,8 @@ def build_root_task(scan_config: dict[str, Any]) -> str:
)
elif ttype == "local_code":
path = details.get("target_path", "unknown")
sections["Local Codebases"].append(
f"- {path} (available at: {workspace_path}; "
"this is the user's real directory, mounted live and writable — "
".git/.agents/.codex are read-only)"
)
desc = _describe_directory_mount()
sections["Local Codebases"].append(f"- {path} (available at: {workspace_path}; {desc})")
elif ttype == "web_application":
sections["URLs"].append(f"- {details.get('target_url', '')}")
elif ttype == "ip_address":
Expand All @@ -155,12 +168,9 @@ def build_root_task(scan_config: dict[str, Any]) -> str:
if workspace_mount := scan_config.get("workspace_mount") or "":
subdir = scan_config.get("workspace_subdir") or ""
workspace_path = f"/workspace/{subdir}" if subdir else "/workspace"
desc = _describe_directory_mount()
parts.append("\n\nWorking Directory:")
parts.append(
f"- {workspace_mount} (available at: {workspace_path}; "
"this is the user's real directory, mounted live and writable — "
".git/.agents/.codex are read-only)"
)
parts.append(f"- {workspace_mount} (available at: {workspace_path}; {desc})")
parts.append(
"- No scan target was set. This directory is where you work, not a "
"target to assess: the instructions below are the only source of "
Expand Down
8 changes: 8 additions & 0 deletions strix/report/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -627,9 +627,17 @@ def set_scan_config(self, config: dict[str, Any]) -> None:
"local_sources": config.get("local_sources", []),
"scope_mode": config.get("scope_mode", "auto"),
"diff_base": config.get("diff_base"),
"transport": None,
}
)

def set_observed_transport(self, transport: str | None) -> None:
"""Record the observed sandbox transport (e.g. 'mount-free' or 'bind-mount')."""
if self.run_record.get("transport") == transport:
return
self.run_record["transport"] = transport
self.save_run_data()

def save_run_data(self, mark_complete: bool = False, status: str | None = None) -> None:
if mark_complete:
self.end_time = datetime.now(UTC).isoformat()
Expand Down
23 changes: 19 additions & 4 deletions strix/runtime/backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ async def _docker_backend(
}

_BIND_MOUNT_BACKENDS: set[str] = {"docker"}
_MOUNT_FREE_BACKENDS: set[str] = {"docker"}


def get_backend(name: str) -> SandboxBackend:
Expand All @@ -80,26 +81,40 @@ def register_backend(
backend: SandboxBackend,
*,
supports_bind_mounts: bool = False,
supports_mount_free: bool = False,
) -> None:
"""Register a custom backend under ``name``.

Intended for downstream users who ship their own runtime — register
before any ``session_manager.create_or_reuse`` call. Re-registering
an existing name overwrites the prior entry. ``supports_bind_mounts``
defaults to False: a remote runtime cannot see the caller's filesystem, so
it is handed local sources as manifest entries to upload instead.
an existing name overwrites the prior entry. Custom backends default
to no bind mounts and no mount-free support (fail-closed); callers must
explicitly opt in to capabilities they support.
"""
_BACKENDS[name] = backend
if supports_bind_mounts:
_BIND_MOUNT_BACKENDS.add(name)
else:
_BIND_MOUNT_BACKENDS.discard(name)
logger.info("Registered sandbox backend: %s (bind mounts: %s)", name, supports_bind_mounts)
if supports_mount_free:
_MOUNT_FREE_BACKENDS.add(name)
else:
_MOUNT_FREE_BACKENDS.discard(name)
logger.info(
"Registered sandbox backend: %s (bind mounts: %s, mount-free: %s)",
name,
supports_bind_mounts,
supports_mount_free,
)


def backend_supports_bind_mounts(name: str) -> bool:
return name in _BIND_MOUNT_BACKENDS


def backend_supports_mount_free(name: str) -> bool:
return name in _MOUNT_FREE_BACKENDS


def supported_backends() -> list[str]:
return sorted(_BACKENDS)
Loading