Skip to content

feat: add fail-closed mount-free transport for local-code Docker scans - #1099

Open
manideep-malyala wants to merge 12 commits into
usestrix:mainfrom
manideep-malyala:feat/mount-free-transport
Open

feat: add fail-closed mount-free transport for local-code Docker scans#1099
manideep-malyala wants to merge 12 commits into
usestrix:mainfrom
manideep-malyala:feat/mount-free-transport

Conversation

@manideep-malyala

@manideep-malyala manideep-malyala commented Aug 18, 2026

Copy link
Copy Markdown

Summary

Implements #1080 — adds a require_mount_free configuration flag (env: STRIX_REQUIRE_MOUNT_FREE) that enforces mount-free (bind-mount-free) transport for local-code Docker scans.

When enabled, Strix guarantees that no host directories are bind-mounted into the scanning container. Local sources are transferred via the Manifest upload path, keeping the container fully isolated from the host filesystem. A symlink-safe tree walker ensures projects containing symlinks (e.g. node_modules, venv, tool wrappers) are transferred without aborting sandbox startup.

Changes

  • strix/config/settings.py: Added require_mount_free boolean field to RuntimeSettings (env: STRIX_REQUIRE_MOUNT_FREE, default False).
  • strix/runtime/backends.py: Added _MOUNT_FREE_BACKENDS registry and backend_supports_mount_free() helper. Extended register_backend() with supports_mount_free kwarg (default True).
  • strix/runtime/session_manager.py: Fail-closed enforcement in create_or_reuse — raises RuntimeError if the backend does not support mount-free transport. Added _symlink_safe_dir_entry() which walks the source tree, resolving symlinks into File/Dir entries so the SDK LocalDir symlink rejection is bypassed entirely. Dangling symlinks are skipped with a warning rather than aborting.
  • strix/core/inputs.py: Agent system prompt dynamically adapts — describes sources as a "bounded snapshot, isolated from the live host system" when mount-free is active.
  • strix/report/state.py: Records a transport field ("bind-mount" or "mount-free") in run.json for downstream security auditing.
  • tests/test_mount_free.py: New test validating the fail-closed guarantee when a backend without mount-free support is used with the flag enabled.

Testing

uv run --all-extras pytest
931 passed, 2 warnings in 151.66s

…ls directly into LitellmModel instead of mutating global module state
@greptile-apps

greptile-apps Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds an opt-in mount-free transport policy for local-code Docker scans, backend capability registration, transport-aware prompts and run metadata, and per-model LiteLLM endpoint configuration.

  • Adds STRIX_REQUIRE_MOUNT_FREE and rejects incompatible backends.
  • Routes local sources through manifest upload when bind mounts are forbidden.
  • Records and describes the selected transport in run artifacts and agent instructions.
  • Adds a refusal-path test for backends without mount-free support.

Confidence Score: 4/5

The mount-free feature should not merge until symlink-containing local projects can be transferred without aborting sandbox startup.

Requiring mount-free transport sends unmodified source trees through the SDK LocalDir materializer, which rejects symlinks and prevents common local-code scans from starting.

Files Needing Attention: strix/runtime/session_manager.py, tests/test_mount_free.py

Important Files Changed

Filename Overview
strix/runtime/session_manager.py Enforces the mount-free policy and selects manifest upload, but raw LocalDir uploads make symlink-containing local projects fail during session startup.
strix/runtime/backends.py Adds mount-free capability tracking and registration metadata with fail-closed support checks.
strix/config/settings.py Adds the boolean STRIX_REQUIRE_MOUNT_FREE runtime setting with a safe default.
strix/core/inputs.py Adjusts local-source descriptions according to the configured backend and mount-free policy.
strix/report/state.py Persists the selected transport label in run.json for auditing.
strix/config/models.py Moves LiteLLM key and base URL configuration from module globals to resolved model instances.
tests/test_mount_free.py Covers incompatible-backend refusal but not successful mount-free transfer of realistic source trees such as projects containing symlinks.

Comments Outside Diff (1)

  1. strix/runtime/session_manager.py, line 305 (link)

    P1 Mount-free uploads reject symlinks

    When STRIX_REQUIRE_MOUNT_FREE is enabled for a local-code target or workspace containing a symlink, this path passes the unmodified tree to the SDK's LocalDir materializer, which raises LocalDirReadError(reason="symlink_not_supported") and aborts sandbox startup before the scan can run.

    Knowledge Base Used: Runtime and Docker Sandbox

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: strix/runtime/session_manager.py
    Line: 305
    
    Comment:
    **Mount-free uploads reject symlinks**
    
    When `STRIX_REQUIRE_MOUNT_FREE` is enabled for a local-code target or workspace containing a symlink, this path passes the unmodified tree to the SDK's `LocalDir` materializer, which raises `LocalDirReadError(reason="symlink_not_supported")` and aborts sandbox startup before the scan can run.
    
    **Knowledge Base Used:** [Runtime and Docker Sandbox](https://app.greptile.com/strix-org-3/-/custom-context/knowledge-base/usestrix/strix/-/docs/runtime-and-docker.md)
    
    ---
    
    For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

    Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix All With AI
### Issue 1
strix/runtime/session_manager.py:305
**Mount-free uploads reject symlinks**

When `STRIX_REQUIRE_MOUNT_FREE` is enabled for a local-code target or workspace containing a symlink, this path passes the unmodified tree to the SDK's `LocalDir` materializer, which raises `LocalDirReadError(reason="symlink_not_supported")` and aborts sandbox startup before the scan can run.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Reviews (1): Last reviewed commit: "feat: add fail-closed mount-free transpo..." | Re-trigger Greptile

- Add require_mount_free config field (env: STRIX_REQUIRE_MOUNT_FREE)
  to RuntimeSettings to enforce mount-free transport.
- Enforce fail-closed in session_manager.create_or_reuse: raises
  RuntimeError if the selected backend does not support mount-free transport.
- Add backend_supports_mount_free() to backends.py and
  _MOUNT_FREE_BACKENDS registry; extend register_backend() with
  supports_mount_free kwarg (default True).
- Update AI agent prompt in inputs.py to accurately describe the
  transport mode (bounded snapshot vs live mounted directory).
- Record transport field (bind-mount or mount-free) in run.json
  via state.py for downstream security auditing.
- Add test_mount_free.py with fail-closed coverage (931 tests pass).

Closes usestrix#1080
@samartomar

Copy link
Copy Markdown

Thanks for picking this up — the overall shape (opt-in flag, backend capability registry, fail-closed check in create_or_reuse, transport in run.json, transport-aware prompt) is what #1080 asked for. I reviewed the head commit (0c69014) against the acceptance list in the issue and against the pinned openai-agents==0.19.0 sandbox code. I don't think it's mergeable yet: three things are blocking, the rest are should-fix / nits.

Blocking

1. _symlink_safe_dir_entry copies host files from outside the source tree, and crashes on symlink cycles

def _symlink_safe_dir_entry(root: Path) -> Dir:
"""Walk *root* recursively and build a ``Dir`` entry tree.
Symlinks are resolved to their real targets and copied as regular ``File``
entries so that the SDK ``LocalDir`` symlink rejection is bypassed entirely.
Dangling symlinks are silently skipped with a warning.
"""
children: dict[str | Path, BaseEntry] = {}
for item in sorted(root.iterdir()):
if item.is_symlink():
target = item.resolve()
if not target.exists():
logger.warning("mount-free: skipping dangling symlink %s", item)
continue
if target.is_dir():
children[item.name] = _symlink_safe_dir_entry(target)
elif target.is_file():
try:
children[item.name] = File(content=target.read_bytes())
except OSError:
logger.warning("mount-free: could not read symlink target %s -> %s", item, target)
elif item.is_dir():
children[item.name] = _symlink_safe_dir_entry(item)
elif item.is_file():
try:
children[item.name] = File(content=item.read_bytes())
except OSError:
logger.warning("mount-free: could not read file %s", item)
return Dir(children=children)
follows item.resolve() with no containment check and reads whatever it points at. That is the opposite of a "bounded snapshot": venv/bin/python -> /usr/bin/python3 (the example from the PR description) copies the host binary into the container, and a planted link -> ~/.ssh/id_rsa copies the key. The SDK's LocalDir rejects symlinks deliberately (O_NOFOLLOW, lstat, path grants) — bypassing it removes a hardening layer. The bind-mount path is actually stricter today: a symlink inside a bind mount resolves in the container's namespace, and _metadata_mounts already refuses out-of-tree symlinks (tests/test_session_entries.py::test_metadata_symlinked_outside_the_tree_is_not_mounted).

Repro with a verbatim copy of the function on Python 3.12.10:

file symlink -> outside tree : COPIED host file outside tree
dir symlink  -> outside tree : COPIED
dir symlink  -> ancestor dir : RecursionError (sandbox startup crashes)
symlink loop a->b->a         : RuntimeError: Symlink loop ... (uncaught — resolve() is outside the try)
repro script
import os, tempfile
from pathlib import Path

class File:
    def __init__(self, content): self.content = content
class Dir:
    def __init__(self, children): self.children = children

# paste _symlink_safe_dir_entry from session_manager.py here (logger -> print)

base = Path(tempfile.mkdtemp())
outside = base / "outside"; outside.mkdir()
(outside / "id_rsa").write_bytes(b"SECRET")
repo = base / "repo"; repo.mkdir()
(repo / "app.py").write_text("x")

os.symlink(outside / "id_rsa", repo / "leak")
d = _symlink_safe_dir_entry(repo)
print("escape:", d.children["leak"].content)            # b'SECRET'
(repo / "leak").unlink()

(repo / "sub").mkdir()
os.symlink(repo, repo / "sub" / "up", target_is_directory=True)
_symlink_safe_dir_entry(repo)                          # RecursionError

Two more things about this function: build_manifest_entries is used by every manifest backend, not only when STRIX_REQUIRE_MOUNT_FREE is set, so this also changes behaviour for existing custom-backend users; and File(content=read_bytes()) loads the whole tree into memory — node_modules/venv are exactly the large cases.

Suggested fix: skip symlinks (or follow one only when its lstat-based target stays inside the source root), never recurse through a directory symlink, log what was skipped, and keep LocalDir for everything else. Please add tests for the escape, ancestor and loop cases.

2. The mount-free success path is untested and likely fails for targets outside the process cwd

The SDK applies manifest entries with base_dir = Path.cwd() (https://github.com/openai/openai-agents-python/blob/v0.19.0/src/agents/sandbox/session/base_sandbox_session.py#L1298-L1299), and LocalDir raises LocalDirReadError(reason="outside_base_dir") unless the manifest carries a matching extra_path_grants entry (https://github.com/openai/openai-agents-python/blob/v0.19.0/src/agents/sandbox/entries/artifacts.py#L253-L271). create_or_reuse builds Manifest(entries=..., environment=...) with no grants (

manifest = Manifest(
entries=entries,
environment=Environment(
value={
"PYTHONUNBUFFERED": "1",
"HOST_GATEWAY": "host.docker.internal",
**_host_identity_env(),
"http_proxy": container_caido_url,
"https_proxy": container_caido_url,
"ALL_PROXY": container_caido_url,
"NO_PROXY": "localhost,127.0.0.1",
},
),
)
). So strix --target ./repo works, but STRIX_REQUIRE_MOUNT_FREE=1 strix --target /abs/path/elsewhere — how an orchestrator would run it — should fail at session.start().

#1080 asks for automated coverage of the success path. A test that brings up a mount-free session and asserts the container's HostConfig.Mounts contains no type: bind entries would catch this (and would be the evidence for the first acceptance checkbox). Passing extra_path_grants=(SandboxPathGrant(path=resolved), ...) per local source is probably the fix.

3. Unrelated LiteLLM credential change bundled in

d39735b (

else:
model = super().get_model(model_name)
if type(model).__name__ == "LitellmModel":
if llm.api_key:
model.api_key = llm.api_key
if llm.api_base:
model.base_url = llm.api_base
) moves the LiteLLM key/base URL from module defaults to per-instance attributes. It isn't in the PR's change list, has no test, leaves _configure_litellm_default (
def _configure_litellm_default(name: str, value: str) -> None:
"""Set LiteLLM's module-level defaults without adding a provider wrapper."""
import litellm
setattr(litellm, name, value)
) with no callers, and model is typed Model, so model.api_key = ... should fail strict mypy. Could you split it into its own PR so it can be reviewed on its own merits?

Should fix before merge

  • transport in run.json is a config projection, not what happened (

    strix/strix/report/state.py

    Lines 400 to 402 in 0c69014

    backend_name = load_settings().runtime.backend
    use_bind_mounts = backend_supports_bind_mounts(backend_name) and not load_settings().runtime.require_mount_free
    transport = "bind-mount" if use_bind_mounts else "mount-free"
    ). It is written in set_scan_config before the sandbox exists, so a run that fails closed still records "transport": "mount-free", and a URL-only scan with no local sources records "bind-mount". Suggest create_or_reuse decides the transport once, returns it in the bundle, and state records that value (null when there are no local sources). That also removes the four copies of the backend_supports_bind_mounts(...) and not require_mount_free logic (session_manager, inputs.py x2, state.py) and the new core -> runtime import.
  • Docs: STRIX_REQUIRE_MOUNT_FREE isn't in docs/advanced/configuration.mdx next to STRIX_RUNTIME_BACKEND, and the issue asked for a note on snapshot mutability, cleanup (the snapshot lives in the container's writable layer and goes away with it) and the trust boundary. A --require-mount-free CLI flag would be nice; the doc entry is the minimum.
  • Prompt text in mount-free mode still says .git/.agents/.codex are read-only (

    strix/strix/core/inputs.py

    Lines 136 to 143 in 0c69014

    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:
    desc = "this is the user's real directory, mounted live and writable"
    else:
    desc = "this is a bounded snapshot of the user's directory, isolated from the live host system"
    ,

    strix/strix/core/inputs.py

    Lines 167 to 174 in 0c69014

    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:
    desc = "this is the user's real directory, mounted live and writable"
    else:
    desc = "this is a bounded snapshot of the user's directory, isolated from the live host system"
    ); nothing enforces that on the manifest path.
  • Failure UX: the RuntimeError surfaces as an unhandled traceback. The exit code is non-zero, which satisfies the issue, but other startup failures get _print_error_panel — worth the same treatment.
  • supports_mount_free=True default (
    def register_backend(
    name: str,
    backend: SandboxBackend,
    *,
    supports_bind_mounts: bool = False,
    supports_mount_free: bool = True,
    ) -> 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.
    """
    _BACKENDS[name] = backend
    if supports_bind_mounts:
    _BIND_MOUNT_BACKENDS.add(name)
    else:
    _BIND_MOUNT_BACKENDS.discard(name)
    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)
    ): for a fail-closed feature this is optimistic — a custom bind-mount backend that ignores the manifest would silently run against an empty workspace. Defaulting to not supports_bind_mounts (remote backends are trivially mount-free) and requiring bind-mount-capable backends to opt in seems safer.
  • Test hygiene (
    def test_mount_free_transport_refusal_raises_error(monkeypatch: pytest.MonkeyPatch) -> None:
    # Register a backend that ONLY supports bind mounts and does NOT support mount-free
    register_backend(
    "stub_legacy",
    _dummy_backend,
    supports_bind_mounts=True,
    supports_mount_free=False,
    )
    monkeypatch.setenv("STRIX_RUNTIME_BACKEND", "stub_legacy")
    monkeypatch.setenv("STRIX_REQUIRE_MOUNT_FREE", "1")
    # Clear the settings cache so env vars are picked up
    import strix.config.loader
    strix.config.loader._cached = None
    with pytest.raises(RuntimeError, match="Sandbox backend 'stub_legacy' does not support mount-free transport"):
    import asyncio
    asyncio.run(create_or_reuse("scan_123", image="dummy_image", local_sources=[]))
    ): the test registers stub_legacy and sets loader._cached = None without restoring either, so later tests in the same process inherit backend="stub_legacy", require_mount_free=True. The existing tests use monkeypatch.setattr(loader, "_cached", None) and pop the backend in a finally. With asyncio_mode = "auto" it can also just be an async def test instead of asyncio.run.

Nits (pre-commit will flag these)

Unused import stat (also placed after the third-party imports), trailing whitespace on 8 lines, 9 lines over 100 chars, unsorted imports in inputs.py/state.py; has_symlinks walks the tree with rglob and then LocalDir walks it again.

Happy to re-review once the walker and the success-path test are in.

@manideep-malyala

Copy link
Copy Markdown
Author

Hi @samartomar, thank you for taking the time to write such a detailed security review. I really appreciate your guidance.

I have updated the PR to address all blocking findings, should-fix items, and nits in commits 64956f45, ad5d2195, 3774c2ee, and 4206df8d:

1. Symlink Containment & Loop Protection

  • Out-of-Tree Containment: _symlink_safe_dir_entry now calculates _source_root = root.resolve() and enforces target.relative_to(_source_root). Any out-of-tree symlink attempt (e.g. link -> ~/.ssh/id_rsa or host binaries) is safely caught and skipped with a warning log.
  • Cycle & Loop Protection: Added _visited_dirs set tracking to trap directory loops (a -> b -> a) without raising RecursionError. Directory symlinks are skipped with a warning log rather than recursed.
  • Tests Added: Added test_symlink_safe_dir_entry_skips_out_of_tree_symlinks and test_symlink_safe_dir_entry_prevents_directory_symlink_loops in tests/test_mount_free.py.

2. Path Grants for Non-CWD Targets

  • Added build_manifest_grants helper which passes extra_path_grants=(SandboxPathGrant(path=resolved), ...) to Manifest(...), ensuring target directories outside Path.cwd() don't trigger LocalDirReadError("outside_base_dir").

3. Reverted Unrelated Commits

  • Reverted commit d39735b in strix/config/models.py to keep this PR strictly scoped to Mount-Free Transport as requested.

4. Should-Fix & Nits

  • Fail-Closed Default: Updated register_backend so supports_mount_free defaults to not supports_bind_mounts. Custom bind-mount backends must explicitly opt in.
  • State Audit Log: Updated strix/report/state.py to record transport: null when no local sources are present (e.g. URL scans).
  • Documentation: Documented STRIX_REQUIRE_MOUNT_FREE in docs/advanced/configuration.mdx.
  • Imports & Test Hygiene: Removed unused import stat in session_manager.py and restored settings cache & backend registrations via finally blocks and pytest.mark.asyncio.

Verification

  • Verified live on real filesystem with Docker active — confirmed out-of-tree symlinks are trapped while target files and grants are safely packaged into the sandbox manifest.
  • Ran the entire test suite: 933 passed in 160.20s (100% PASS).

Whenever convenient, I welcome your re-review on this PR. Thank you again for your time and feedback.

@samartomar

Copy link
Copy Markdown

Thanks for the quick turnaround — re-reviewed at 4206df8.

Resolved

  • Symlink containment — verified with the same repro: out-of-tree file and directory symlinks are skipped, an ancestor directory symlink and an a <-> b loop both complete, dangling links are skipped, in-tree file symlinks are kept.
  • extra_path_grants for non-cwd targets — correct against the SDK API (SandboxPathGrant.path coerces a Path). One suggestion: pass read_only=True — Strix only reads host sources on this path, and the SDK enforces read-only grants for write operations, so it's free least-privilege.
  • supports_mount_free default, transport: null for URL-only scans, the docs entry, the settings-cache restore in the test, and the unused import.

Still blocking

  1. strix/config/models.py must have no diff in this PR. d39735b is gone, but the file now carries _TextTagDispatchModel (
    class _TextTagDispatchModel(Model):
    """Fallback dispatch mode for models that fail to emit structured tool_calls.
    Extracts [TOOL: name] ... [/TOOL] text tags from the response output and
    synthesizes ResponseFunctionToolCall instances so the SDK can execute them.
    """
    def __init__(self, inner: Model) -> None:
    self._inner = inner
    async def close(self) -> None:
    await self._inner.close()
    def get_retry_advice(self, request: ModelRetryAdviceRequest) -> ModelRetryAdvice | None:
    return self._inner.get_retry_advice(request)
    async def get_response(
    self,
    system_instructions: str | None,
    input: str | list[TResponseInputItem], # noqa: A002
    model_settings: ModelSettings,
    tools: list[Tool],
    output_schema: AgentOutputSchemaBase | None,
    handoffs: list[Handoff],
    tracing: ModelTracing,
    *,
    previous_response_id: str | None,
    conversation_id: str | None,
    prompt: ResponsePromptParam | None,
    ) -> ModelResponse:
    import re
    import uuid
    import json
    from agents.items import ResponseFunctionToolCall
    # We need the inner get_response first
    response = await self._inner.get_response(
    system_instructions,
    input,
    model_settings,
    tools,
    output_schema,
    handoffs,
    tracing,
    previous_response_id=previous_response_id,
    conversation_id=conversation_id,
    prompt=prompt,
    )
    TEXT_TAG_PATTERN = re.compile(r"\[TOOL:\s*([^\]]+)\](.*?)\[/TOOL\]", re.DOTALL | re.IGNORECASE)
    new_output = []
    for item in response.output:
    if getattr(item, "type", None) == "message":
    raw_content = getattr(item, "content", "")
    if isinstance(raw_content, list):
    content = ""
    for part in raw_content:
    if isinstance(part, str):
    content += part
    elif isinstance(part, dict) and "text" in part:
    content += part["text"]
    elif hasattr(part, "text"):
    content += part.text
    else:
    content = str(raw_content) if raw_content else ""
    if content and "[TOOL:" in content:
    matches = list(TEXT_TAG_PATTERN.finditer(content))
    if matches:
    clean_content = TEXT_TAG_PATTERN.sub("", content).strip()
    if clean_content:
    try:
    item.content = clean_content
    except AttributeError:
    if hasattr(item, "raw_item") and hasattr(item.raw_item, "content"):
    item.raw_item.content = clean_content
    new_output.append(item)
    for match in matches:
    tool_name = match.group(1).strip()
    tool_args = match.group(2).strip()
    # Check if the args are valid JSON, otherwise it will fail gracefully later
    try:
    json.loads(tool_args)
    except ValueError:
    pass
    tool_call = ResponseFunctionToolCall(
    id=uuid.uuid4().hex[:8],
    name=tool_name,
    arguments=tool_args,
    caller="agent",
    )
    new_output.append(tool_call)
    continue
    new_output.append(item)
    response.output = new_output
    return response
    async def stream_response(
    self,
    system_instructions: str | None,
    input: str | list[TResponseInputItem], # noqa: A002
    model_settings: ModelSettings,
    tools: list[Tool],
    output_schema: AgentOutputSchemaBase | None,
    handoffs: list[Handoff],
    tracing: ModelTracing,
    *,
    previous_response_id: str | None,
    conversation_id: str | None,
    prompt: ResponsePromptParam | None,
    ) -> AsyncIterator[TResponseStreamEvent]:
    # Text-tag parsing over a stream is complex; delegate to get_response like _NonStreamingModel
    response = await self.get_response(
    system_instructions,
    input,
    model_settings,
    tools,
    output_schema,
    handoffs,
    tracing,
    previous_response_id=previous_response_id,
    conversation_id=conversation_id,
    prompt=prompt,
    )
    yield _completed_stream_event(response, getattr(self._inner, "model", None))
    ), a tool_mode gate (
    if getattr(llm, "tool_mode", "native") == "text-tags":
    model = _TextTagDispatchModel(model)
    if llm.disable_streaming or getattr(llm, "tool_mode", "native") == "text-tags":
    ) on a setting that does not exist in LlmSettings, and supports_strict_tool_schemas / _ANTHROPIC_MODEL_MARKERS (
    def supports_strict_tool_schemas(model_name: str) -> bool:
    ,
    _ANTHROPIC_MODEL_MARKERS = ("anthropic", "claude", "sonnet", "opus", "haiku")
    ) that nothing calls — ~130 lines of unrelated, unreachable, untested code that synthesizes tool calls from free text. It looks like local WIP got swept into 64956f4. Please rebase on upstream main and confirm git diff main --stat lists only the seven intended files.
  2. Success-path test is still missing. Support a fail-closed mount-free transport for local-code Docker scans #1080's acceptance list says "automated coverage verifies both the mount-free success path and fail-closed refusal"; the two new walker tests are good, but nothing exercises create_or_reuse with the flag on. A Docker-free version is enough: register a stub backend that captures its bind_mounts= / manifest= kwargs, call create_or_reuse with STRIX_REQUIRE_MOUNT_FREE=1 and one local source, and assert bind_mounts == [], the source is present in manifest.entries, and manifest.extra_path_grants covers it. A real-Docker variant (skipped when the daemon is unavailable) asserting HostConfig.Mounts has no type: bind entries would be the ideal evidence for the first checkbox in the issue.

Should still fix before merge

  • pre-commit will still fail: trailing whitespace at inputs.py L138/L143/L166/L169/L174 and backends.py L100/L105; lines over 100 chars at backends.py L106, session_manager.py L118, inputs.py L137/L142/L168/L173, state.py L405. pre-commit run --all-files fixes most of it.
  • The transport decision is still duplicated four times and computed from config before the sandbox exists. A single helper in session_manager that returns the chosen transport in the bundle would let state.py record what actually ran. Not blocking for me.
  • The prompt still says .git/.agents/.codex are read-only in mount-free mode (inputs.py L146/L178); nothing enforces that on the manifest path.
  • Test cleanup: also _BIND_MOUNT_BACKENDS.discard("stub_legacy") in the finally; the import strix.config.loader inside the test will trip PLC0415 — import it at module level.
  • Memory: any project containing a symlink (every Node project with node_modules/.bin) now takes the in-memory Dir/File path, so the whole tree is read into RAM. Fine to ship as-is if documented; a staged copy (shutil.copytree(symlinks=False) into a temp dir, then LocalDir + grant) would keep memory flat if you want to go further.
  • Docs: one more sentence covering cleanup/mutability (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) closes the documentation item in Support a fail-closed mount-free transport for local-code Docker scans #1080.

Once (1) and (2) are in, this satisfies #1080 from my side.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants