Repository navigation
Conversation
## Problem On Linux a virtualenv `python` as `command` does not start (strands-agents#30). Its link chain leaves the venv: `bin/python -> python3 -> /usr/local/bin/python3 -> python3.13`. The view bound the spelling as a second file and left out every other hop. When the venv is granted, exec fails with ENOENT at the missing hop. When it is not, the program runs as the spelling, so the loader expands `$ORIGIN/../lib` against the venv and does not find libpython. Box's own closure walk expanded `$ORIGIN` against the identity, so the walk and the loader disagreed. ## Solution The Linux view now plans each link that a grant's lookup traverses, and that has a canonical parent, as a link with the host's text (`MountKind::Symlink`, `MountOrigin::Link`). It binds only the identity. A link that a directory bind brings in is left to the bind. A link under a read-only empty directory is refused. A link whose text no longer leads to a node the grant judged is refused. An entry spelled beneath a link, such as the loader `/lib/ld-linux-*.so.1` where `/lib` is a link, is planned at the host's resolution of it, so no mountpoint is made through a link. A dependency never adds access to an entry there, which keeps W^X. A spelling through a linked ancestor directory keeps its bind; decisions.md records this residual. No interface moves; RECORD_VERSION stays 21. ## Tests - view.rs: a_two_link_chain_plans_each_unenclosed_hop_as_a_link, a_hop_inside_a_bound_tree_plans_nothing, two_grants_sharing_a_hop_plan_it_once, a_hop_under_a_read_only_fresh_mount_is_refused, an_entry_beneath_a_planned_link_is_planned_at_its_resolution, a_refusal_beneath_a_planned_link_refuses_its_resolution, a_linked_loader_directory_and_an_interpreter_spelled_through_it_plan, a_respelled_library_does_not_make_a_writable_grant_executable, a_respelled_writable_grant_replaces_a_library_without_gaining_exec, a_respelled_grant_meeting_a_later_grant_is_one_entry, a_respelled_entry_meeting_a_planned_link_is_refused, a_link_retargeted_after_its_grant_was_built_is_refused, a_spelling_through_a_linked_ancestor_keeps_its_bind, and the changed a_grant_through_a_symlink_is_reachable_under_both_names. - contains_exec_target_linux.rs: a_program_through_a_link_chain_runs_as_its_identity (fails on main: /proc/self/exe is the spelling) and a_link_chain_leaving_a_bound_tree_execs (fails on main: ENOENT). - box_direct_filesystem.rs: a_venv_shaped_command_runs_as_its_identity (fails on main: exit 4). - Ran on Linux aarch64 (finch, rust:1-bookworm): strands-box-containment all pass; strands-box 885 pass, and the 38 failures are the same suites that fail the same way on main in that environment (runtime_mcp_start, native_egress_mcp, runtime_mcp_policy_staging, box_shell signalled binary, hardening, box_direct_filesystem rename EBUSY). Manual: a python.org and a Debian venv `python` start in python:3.13-slim, and numpy and pandas import from the venv with hand-written exec grants. - Not run: x86_64, the other workspace crates (none depends on containment), and Kata. numpy still needs exec grants on site-packages; that is part B of strands-agents#30. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ay be a link Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Bug fix, no interface change. No
box.tomlkey, CLI flag, record field, telemetry name, orpub usechanges, andRECORD_VERSIONstays 21. The change is internal to the Linux namespace view incontainment.On Linux, a virtualenv
pythonas[agent] command(or[tool.<name>] command) does not start.decisions.mdalready says Box execs a program through its link, so that CPython findspyvenv.cfg, and grants the link's target. The Linux view did not keep that promise for a chain of more than one link. A venv'sbin/python -> python3 -> /usr/local/bin/python3 -> python3.13failed in two ways:/usr/local/bin/python3is in no grant and is not in the view.execfails withENOENT./proc/self/exeand the loader's$ORIGINname the venv'sbin. The python.org build carriesRUNPATH $ORIGIN/../lib, solibpython3.13.so.1.0is not found. Box's closure walk had expanded$ORIGINagainst the identity, so the walk and the loader disagreed.The view now reproduces each link that a grant's lookup traverses, with the host's text, and binds only the identity. The kernel then walks the same chain in the view that it walks on the host.
Load-bearing decisions:
/bin/shon merged-/usrlstat's as a link because/usr/bin/shis one, but/binis itself a link, so a copy of it in a real/binwould dangle. Such a spelling, reached through a linked ancestor directory, keeps its bind as today. This is the residual, recorded indecisions.md. Acommandnever reaches it, becausecanonical_routemakes its directory canonical first./usr: a tool's/libread root is a link there, and the loader's program header names/lib/ld-linux-*.so.1. A mountpoint is never made through a link, because that could lead into a bind and onto the host. Refusals are re-spelled too, so a denial stays in force./tmpcan be replaced, but only after the trampoline's exec, and the workload can already make links there to anything in its view.Out of scope (follow-ups):
dlopened extension modules still need hand-writtenexecgrants (site-packages, andlib-dynloadon python.org builds). That needs a design call against thedecisions.mdrule that Box derives no exec literal from a runtime's dependency graph, so I will propose it on [FEATURE] Linux filesystem grants for Python data-science stacks (dlopen'd libraries, venv interpreter) #30 first./proc/self) fail closed when the view is set up, not with a named refusal at plan time. Box never grants these.For maintainers: as an outside contributor, my CI and Auto Strands Review runs wait for approval (
action_required/manual-approval). Could someone approve them? Thanks.Related Issues
Part A of #30 (the issue stays open for part B).
Related: #21 / #40 (x86_64)
Type of Change
Bug fix
Testing
ubuntu-latest(x86_64) andmacos-latest, both Containment / Deterministic legs (ubuntu-24.04-arm,macos-latest), Lint, and Verdict. The first run (38056671462) failed one x86_64 unit test,a_read_root_on_a_loader_directory_is_bound_executable_and_read_only. It expected the loader directory bound at its spelling, but onubuntu-latestthat spelling is the link/lib64 -> usr/lib64, which the view now reproduces as a link.05f3592checks the bind at the identity instead. I reproduced the failure on aarch64 by adding a/lib64link, and the suite passes with and without it.mainand pass here:contains_exec_target_linux.rs::a_program_through_a_link_chain_runs_as_its_identity: onmain,/proc/self/exeis the spelling.contains_exec_target_linux.rs::a_link_chain_leaving_a_bound_tree_execs: onmain,execfails withENOENT.box_direct_filesystem.rs::a_venv_shaped_command_runs_as_its_identity: onmain, the run exits 4.readlink, so CI needs no Python.view.rs): each hop planned as a link, hops inside a bound tree, a shared hop planned once, a hop under a read-only empty directory, entries and refusals beneath a link, the real/libon merged-/usr, the four W^X merge cases, a retargeted link, and the linked-ancestor residual.rust:1-bookworm, privileged):strands-box-containment: all tests pass.strands-box: 885 pass. The 38 failures are in suites that fail the same way onmainin that environment, measured by swappingmain'sview.rsback in:runtime_mcp_start4,native_egress_mcp5,runtime_mcp_policy_staging25,box_shellsignalled binary 1,run::hardening2,box_direct_filesystemrenameEBUSY1.python:3.13-slim(aarch64), which CI cannot show:pythonstart as[agent] command, with and without a read grant on the venv. With the grant,sys.prefixis the venv.execgrants onsite-packagesandlib-dynload(part B removes the need for these).cargo fmt --alland thejust clippygate.containmentand the full build did not fit the local disk (CI runs them). Also not run: Kata.Checklist
By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.
🤖 Generated with Claude Code