Skip to content

feat(bwrap): tell sandboxed agents what is mounted - #31

Open
Stuart B. Wilkins (stuwilkins) wants to merge 8 commits into
NSLS2:mainfrom
stuwilkins:feat/bwrap-sandbox-context
Open

Stuart B. Wilkins (stuwilkins) wants to merge 8 commits into
NSLS2:mainfrom
stuwilkins:feat/bwrap-sandbox-context

Conversation

@stuwilkins

Copy link
Copy Markdown
Contributor

Summary

The bwrap launchers (bwopencode, bwopencode2, bwclaude, bwcodex,
bwcopilot) now generate a short description of the sandbox at start-up and
make the agent load it, so the agent knows it runs in a sandbox and what is
and is not mounted. Without it, agents guessed wrong, for example taking
read-only bind mounts of resolved symlinks for hard links.

Description

  • build_sandbox_context in lib/bwrap_sandbox_lib.sh builds the file from
    the final BWRAP_ARGS, so it always matches the real mounts: $HOME is a
    tmpfs; same-inode paths are bind mounts, not hard links; a table of the
    mounts under $HOME (rw/ro/tmpfs) with system mounts collapsed; what
    is not mounted; disabled tools; and the names of passed-in variables
    (never values).
  • It is mounted read-only at ~/.sandbox/AGENTS.md with --ro-bind-data on a
    file descriptor (nothing is written to the host). BWRAP_SANDBOX and
    BWRAP_SANDBOX_CONTEXT are set inside the sandbox.
  • Each tool loads it in addition to the user's own instructions; no user file
    is overwritten:
    • bwopencode, bwopencode2: OPENCODE_CONFIG_CONTENT (its instructions
      list is added to the user's; a host value is still dropped)
    • bwclaude: --append-system-prompt-file (skipped with a warning if the
      user passes --append-system-prompt[-file])
    • bwcodex: -c developer_instructions=... (skipped with a warning if a
      config.toml or -c already sets it)
    • bwcopilot: COPILOT_CUSTOM_INSTRUCTIONS_DIRS=~/.sandbox
  • --no-sandbox-context turns it off; --dry-run prints the generated file;
    --exec mounts the file but adds no loader.
  • The masked binary list moves to file scope so the file can name it.
  • Docs: docs/bw-wrappers.md (new "Sandbox context" section) and README.md.

Testing

Run from a sandbox on a managed host, with real model runs (throwaway HOME,
env -i, OpenCode 2 only with --standalone):

  • OpenCode 1.19.4 and 2.1.4: OPENCODE_CONFIG_CONTENT adds to a global and a
    project instructions list rather than replacing them (global, project and
    sandbox markers all reported); debug config lists the same files for v1.

  • bwopencode2, bwclaude (2.1.310) and bwcodex (0.164.0) real runs: the
    agent names the launcher, says same-inode files are bind mounts and that
    ~/.ssh is not available, from the file alone.

  • bwcopilot instruction list shows the source with the feature on and none
    with --no-sandbox-context.

  • Warning paths for a user-set --append-system-prompt and
    developer_instructions.

  • pre-commit on the changed files (shellcheck, shfmt, codespell,
    markdownlint, gitleaks).

  • Not tested: bwopencode (OpenCode 1) model run (fails when nested inside a
    v2 sandbox, with or without this change).

  • --dry-run for each launcher shows the generated file

  • bwopencode (OpenCode 1) session on a host answers a question about its mounts

  • pixi run -e dev lint

Checklist

  • Edit the sections above with relevant information
  • Update in-repo documentation, if applicable (docs/bw-wrappers.md, README.md)
  • Run pixi run -e dev lint locally (or wait for CI) — see .github/workflows/lint.yml
  • Test the changes on a representative managed host (or runansible deployment)

@stuwilkins
Stuart B. Wilkins (stuwilkins) marked this pull request as ready for review October 27, 2026 17:58
Copilot AI balanced review requested due to automatic review settings October 27, 2026 17:58

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Context generation can abort valid launches, misrepresent filesystem access, and override existing Codex instructions.

7 open issues
What changed in this PR

Adds shared sandbox context generation so agents launched through the bwrap wrappers receive information about filesystem access, persistence, and restrictions.

Changes:

  • Generates and mounts a read-only context file without writing it to the host.
  • Integrates tool-specific instruction loaders, an opt-out flag, and dry-run output.
  • Documents the context contents and loading behavior.
File Description
README.md Adds the context opt-out option.
lib/​bwrap_sandbox_lib.sh Generates, mounts, and displays sandbox context.
docs/​bw-wrappers.md Documents context contents and loader behavior.
bin/​bwopencode2 Loads context through OpenCode configuration with v2-specific notes.
bin/​bwopencode Loads context through OpenCode configuration.
bin/​bwcopilot Registers the context directory as an instruction source.
bin/​bwcodex Injects context as developer instructions with conflict checks.
bin/​bwclaude Appends context to the system prompt with conflict checks.

Review effort: Balanced

Comment thread lib/bwrap_sandbox_lib.sh
Comment thread lib/bwrap_sandbox_lib.sh
Comment thread lib/bwrap_sandbox_lib.sh
Comment thread lib/bwrap_sandbox_lib.sh Outdated
Comment thread lib/bwrap_sandbox_lib.sh Outdated
Comment thread lib/bwrap_sandbox_lib.sh Outdated
Comment thread lib/bwrap_sandbox_lib.sh Outdated
Copilot AI balanced review requested due to automatic review settings October 27, 2026 18:14

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Mount descriptions can misrepresent effective access and persistence, and Codex conflict detection can allow existing instructions to be overwritten.

1 open issue
1 previously missed issue

In code that hasn't changed since last review

Medium severity Describe /tmp isolation based on its effective mount mode

lib/​bwrap_sandbox_lib.sh:2976

With --rw-path /tmp, the final mount is a read-write bind of the host's /tmp, but this line still tells the agent that writes are private and discarded at exit (the /tmp row is also filtered out above). This can lead the agent to leave persistent or sensitive files on the host under a false isolation assumption. Derive this description from the effective /tmp mode, as is already done for the working directory.

7 resolved since last review

Review effort: Balanced

Comment thread lib/bwrap_sandbox_lib.sh Outdated
Copilot AI balanced review requested due to automatic review settings October 27, 2026 18:41

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Codex conflict detection can overwrite user instructions, and OpenCode 2 can misstate plan-storage persistence.

No open issues 🎉

1 resolved since last review

Review effort: Balanced

Agents guessed wrong about the sandbox, for example taking a read-only
bind mount of a resolved symlink for a hard link.

build_sandbox_context writes a short markdown description of the session
from the final BWRAP_ARGS (mounts under $HOME with rw/ro/tmpfs, system
mounts collapsed, what is not mounted, disabled tools, names of passed-in
variables) and mounts it read-only at ~/.sandbox/AGENTS.md with
--ro-bind-data on a file descriptor, so nothing is written to the host.
BWRAP_SANDBOX and BWRAP_SANDBOX_CONTEXT identify the sandbox. --dry-run
prints the file and --no-sandbox-context turns it off.

bwopencode2 loads it through OPENCODE_CONFIG_CONTENT, whose instructions
list is added to the user's own (tested with 2.1.4), so no user file is
replaced. The masked binary list moves to file scope so the file can
name it.

Assisted-by: opencode:claude-sonnet-5-5
Same mechanism as bwopencode2: OPENCODE_CONFIG_CONTENT adds the
context file to the user's instructions (tested with 1.19.4).

Assisted-by: opencode:claude-sonnet-5-5
Claude Code gets the context file through --append-system-prompt-file,
which adds to the default system prompt and leaves CLAUDE.md alone
(tested with 2.1.310). A user-supplied --append-system-prompt[-file]
wins; the file is then only mounted and a warning is printed.

Assisted-by: opencode:claude-sonnet-5-5
Codex gets the context through -c developer_instructions, which is a
separate message and leaves AGENTS.md alone (tested with 0.164.0). It is
one value, so it is skipped with a warning when a config.toml (system,
user or project) or the command line already sets it.

Assisted-by: opencode:claude-sonnet-5-5
Copilot reads the AGENTS.md in ~/.sandbox through
COPILOT_CUSTOM_INSTRUCTIONS_DIRS, in addition to its usual instruction
files. A host value of the variable was never forwarded, so nothing is
replaced. Checked with `copilot instruction list`.

Assisted-by: opencode:claude-sonnet-5-5
Assisted-by: opencode:claude-sonnet-5-5
Review feedback on the context file, each case checked with --dry-run:

- Record --file and --bind-data destinations as writable copies (mode
  copy), so config files copied into an otherwise empty tmpfs are listed.
- A later mount over a parent now hides the child mounts recorded earlier
  (for example --ro-path on the working directory covers .opencode).
- Clean control characters, | and backticks from every path once, so a
  newline in a mount path no longer splits keys and aborts the launch
  under set -u, and the table cannot break.
- Keep mount modes under /usr and /etc: a writable exception such as
  --rw-path /usr/local or /etc/hosts is listed on its own, not folded
  into the read-only summary.
- Take the working directory's access from the effective mounts.
- List every name in the home table; only system mounts are collapsed.
- Name credential and rc files as not mounted only when no mount exposes
  them, so bwcopilot's gh hosts.yml and --ro-path ~/.zshrc are not denied.

Assisted-by: opencode:claude-sonnet-5-5
Second round of review feedback, each case checked with --dry-run:

- A read-only mount below a writable system mount (--rw-path /usr/local
  with --ro-path /usr/local/bin) is listed as an exception instead of
  being dropped with the other read-only entries under /usr and /etc.
- Describe /tmp from its effective mount: --rw-path /tmp now says it is
  not private. Binds over /dev, /proc and /sys are listed too.
- bwcodex: detect developer_instructions in any form (spaces around =,
  quoted, dotted or profile keys, inline tables, any project config from
  the repository root down, any argument mentioning it), so a user's
  instructions are not replaced by the context.

Assisted-by: opencode:claude-sonnet-5-5
Copilot AI balanced review requested due to automatic review settings October 27, 2026 19:58

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Config detection can overwrite user-defined developer instructions, while generated guidance can incorrectly describe explicitly mounted directories as inaccessible and persistent plans as temporary.

1 open issue

Review effort: Balanced

Comment thread lib/bwrap_sandbox_lib.sh
Comment on lines +2944 to +2946
_t+=" sibling directories of the repository are not mounted."
else
_t+=" It is not a git repository, so only this directory is mounted."
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