Skip to content
Merged
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
22 changes: 22 additions & 0 deletions bin/bwclaude
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,27 @@ build_claude_cache_mount() {
BWRAP_ARGS+=(--bind "${CLAUDE_CACHE_DIR}" "${CLAUDE_CACHE_DIR}")
}

# ── Managed Claude skills and settings (read-only) ────────────────
# Claude's Linux managed-settings discovery uses /etc/claude-code. Expose
# only its managed settings file and managed skills alias, never all of /etc
# or /etc/claude-code. The alias is resolved before binding so its deployed
# target remains reachable in the sandbox.
build_managed_claude_paths() {
if [[ -e /etc/claude-code/managed-settings.json ]]; then
BWRAP_ARGS+=(--dir /etc/claude-code)
build_managed_ro_bind \
/etc/claude-code/managed-settings.json \
/etc/claude-code/managed-settings.json
fi

if [[ -d /etc/claude-code/.claude/skills && -r /etc/claude-code/.claude/skills ]]; then
BWRAP_ARGS+=(--dir /etc/claude-code --dir /etc/claude-code/.claude)
build_managed_ro_bind \
/etc/claude-code/.claude/skills \
/etc/claude-code/.claude/skills
fi
}

# ── Claude env vars ──────────────────────────────────────────────
build_claude_env() {
# NOTE: bwclaude is hard-wired for the NSLS-II AIFAPIM/foundry gateway.
Expand Down Expand Up @@ -447,6 +468,7 @@ main() {
build_proc_dev_tmp
build_sys_mounts
build_etc_mounts
build_managed_claude_paths
build_home_tmpfs
build_workdir_mount
build_claude_home_mount
Expand Down
10 changes: 10 additions & 0 deletions bin/bwcodex
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,15 @@ check_codex_config() {
fi
}

# ── Managed Codex skills (read-only) ──────────────────────────────
# Resolve the fixed administrator-managed alias before binding it at the
# location Codex discovers. This is deliberately not a --ro-path mount.
build_managed_skills() {
[[ -d /etc/codex/skills && -r /etc/codex/skills ]] || return 0
BWRAP_ARGS+=(--dir /etc/codex)
build_managed_ro_bind /etc/codex/skills /etc/codex/skills
}

# ── Codex home (ephemeral tmpfs + selective persistence) ─────────
# Mount the home dir as ephemeral tmpfs, then selectively bind-mount
# the files and directories that must persist across sessions.
Expand Down Expand Up @@ -460,6 +469,7 @@ main() {
build_proc_dev_tmp
build_sys_mounts
build_etc_mounts
build_managed_skills
build_home_tmpfs
build_workdir_mount
build_codex_home_mount
Expand Down
33 changes: 33 additions & 0 deletions bin/bwcopilot
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,30 @@ build_copilot_home_mount() {
fi
}

# Mount an existing host settings file read-only after all user-supplied
# mounts. Resolve the source for validation and reading, but retain the
# logical destination because Copilot always discovers settings there; this
# also makes a dotfiles symlink available at COPILOT_HOME/settings.json.
build_copilot_settings_mount() {
local _settings_file="${COPILOT_HOME_DIR}/settings.json"
local _settings_source

# -f follows valid symlinks while rejecting dangling links and directories.
[[ -f "${_settings_file}" ]] || return 0
_settings_source="$(readlink -f "${_settings_file}")" || return 0
[[ -f "${_settings_source}" ]] || return 0

# The source may be outside COPILOT_HOME through a user-controlled
# symlink, so check the resolved file rather than using the managed-path
# helper intended only for fixed administrator-provided locations.
_check_path_safe "${_settings_source}" "COPILOT settings.json (resolved)"

# COPILOT_HOME_DIR already exists as the tmpfs mount point, so no --dir is
# needed for this file bind. This also works when source and destination
# are the same path.
BWRAP_ARGS+=(--ro-bind "${_settings_source}" "${_settings_file}")
}

# ── Copilot cache (read-write) ──────────────────────────────────
build_copilot_cache_mount() {
BWRAP_ARGS+=(--bind "${COPILOT_CACHE_DIR}" "${COPILOT_CACHE_DIR}")
Expand Down Expand Up @@ -409,6 +433,15 @@ main() {
build_nsls2_mount
build_dynamic_tool_mounts
build_extra_path_mounts
# Add settings after extra mounts so a user-supplied rw bind cannot shadow
# the read-only logical settings path.
build_copilot_settings_mount
# Deployment-managed skills are optional and never replace user-created
# skills under COPILOT_HOME. Bind the deployment alias read-only after
# user extra mounts so this narrow final mount cannot be shadowed writable.
if [[ -d /opt/nsls2/skills/skills && -r /opt/nsls2/skills/skills ]]; then
build_managed_ro_bind /opt/nsls2/skills/skills /opt/nsls2/skills/skills
fi

# Environment variables
build_env_vars
Expand Down
12 changes: 11 additions & 1 deletion bin/bwopencode
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ resolve_config_symlinks() {
done
}

# ── Managed opencode config (read-only, highest priority) ────────
# ── Managed OpenCode config and skills (read-only, highest priority) ─
# If a managed config exists at /etc/opencode/ on the host (deployed
# by ansible), bind-mount it RO into the sandbox. This is the Linux
# managed-settings path and takes precedence over all other config
Expand Down Expand Up @@ -283,6 +283,15 @@ build_managed_config() {
fi
}

# The host-managed skills alias may point into /opt/nsls2/skills. Bind
# its resolved source at OpenCode's documented managed-skills location so the
# alias target and every supporting file remain available inside the sandbox.
build_managed_skills() {
[[ -d /etc/opencode/skills && -r /etc/opencode/skills ]] || return 0
BWRAP_ARGS+=(--dir /etc/opencode)
build_managed_ro_bind /etc/opencode/skills /etc/opencode/skills
}

# ── Persistent .opencode/ directory ──────────────────────────────
# OpenCode creates .opencode/ inside the repo root for project-level
# config, rules, and plugins. We store this in a persistent location
Expand Down Expand Up @@ -409,6 +418,7 @@ main() {
build_sys_mounts
build_etc_mounts
build_managed_config
build_managed_skills
build_home_tmpfs
build_workdir_mount
build_persistent_dotdir
Expand Down
43 changes: 43 additions & 0 deletions docs/bw-wrappers.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,31 @@ option list; the tables below summarise the common surface.
- 0.5.0+ enables `--clearenv`
- 0.6.3+ enables bind-over-ro-bind binary masking

## Managed system paths

When present, the wrappers expose administrator-managed paths as optional,
read-only mounts. They do not expose the rest of the corresponding `/etc`
configuration directories.

- OpenCode: `/etc/opencode/skills`
- Codex: `/etc/codex/skills`
- Claude: `/etc/claude-code/.claude/skills` and
`/etc/claude-code/managed-settings.json`
- Copilot: its deployment-defined shared-skill directory, when configured by
the wrapper

## Shared agent skills

Shared agent skills are a deployment-managed skill collection. The deployment
defines the source repository or checkout and manages read-only aliases at the
paths above. When those aliases are present, `bwopencode`, `bwcodex`, and
`bwclaude` mount them read-only into their sandboxes. A wrapper configured for
Copilot likewise mounts its deployment-defined shared-skill directory
read-only.

Contribute or update skills through the deployment's documented contribution
workflow; do not edit its deployed checkout or managed aliases directly.

## Common options

Every `bw*` wrapper accepts these options:
Expand All @@ -30,6 +55,8 @@ Every `bw*` wrapper accepts these options:
| `--exec CMD` | Run `CMD` inside the sandbox instead of the tool |
| `--init-auth` | Persist auth credentials to the host (first-time setup) |
| `--new-session` | Force `bwrap --new-session` (stricter isolation; breaks SIGWINCH) |
| `--ro-path PATH` | Read-only mount an existing path; its canonical target is mounted at the same canonical destination. Use only narrow paths. |
| `--rw-path PATH` | Read-write mount an existing path; its canonical target is mounted at the same canonical destination. Use only narrow paths. |

Tool-specific options are listed in each wrapper's section below.

Expand Down Expand Up @@ -96,6 +123,22 @@ No options beyond the common set.
On shared accounts, auth tokens are ephemeral. On a personal machine,
run `bwcopilot --init-auth` once to persist tokens.

### Shared skills

When configured by the wrapper, `bwcopilot` automatically exposes the
deployment-managed shared-skill directory read-only. Configure Copilot CLI's
documented `skillDirectories` in `$COPILOT_HOME/settings.json` (default:
`~/.copilot/settings.json`) to name that same directory.

When `$COPILOT_HOME/settings.json` exists as a regular file, `bwcopilot`
automatically mounts it read-only. This also supports a valid symlink: the
wrapper resolves its source and mounts that source at the logical
`$COPILOT_HOME/settings.json` destination. Dangling symlinks and directories
are skipped.

The read-only mount prevents Copilot's `/settings` slash command from
persisting changes in the sandbox. Edit settings on the host between sessions.

## `bwcodex`

Launches the [OpenAI Codex CLI](https://github.com/openai/codex) inside
Expand Down
14 changes: 14 additions & 0 deletions lib/bwrap_sandbox_lib.sh
Original file line number Diff line number Diff line change
Expand Up @@ -926,6 +926,20 @@ build_etc_mounts() {
)
}

# ── Managed system paths (read-only) ──────────────────────────────
# Bind a fixed, administrator-provided path only when it exists. Resolve
# aliases before binding: the source can be an /etc symlink into the managed
# skills deployment, while the destination must remain the location the CLI
# discovers. Callers provide literal paths; do not use this for user input.
build_managed_ro_bind() {
local source="$1" target="$2" resolved
[[ -e "${source}" ]] || return 0

resolved="$(readlink -f "${source}")"
[[ -e "${resolved}" ]] || return 0
BWRAP_ARGS+=(--ro-bind "${resolved}" "${target}")
}

# ── Home directory (empty tmpfs, then selective mounts) ──────────
build_home_tmpfs() {
BWRAP_ARGS+=(
Expand Down