From 246d42e17f40d0453335730884d2ff43b91cda70 Mon Sep 17 00:00:00 2001 From: "Stuart B. Wilkins" Date: Thu, 24 Sep 2026 11:06:49 -0400 Subject: [PATCH 1/8] feat(bwrap): mount managed agent skills Assisted-by: opencode:gpt-5.6-terra --- bin/bwclaude | 22 ++++++++++++++++++++++ bin/bwcodex | 10 ++++++++++ bin/bwopencode | 12 +++++++++++- docs/bw-wrappers.md | 11 +++++++++++ lib/bwrap_sandbox_lib.sh | 14 ++++++++++++++ 5 files changed, 68 insertions(+), 1 deletion(-) diff --git a/bin/bwclaude b/bin/bwclaude index a22a085..8e5e52c 100755 --- a/bin/bwclaude +++ b/bin/bwclaude @@ -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 [[ -e /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. @@ -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 diff --git a/bin/bwcodex b/bin/bwcodex index 7f3b346..6f8cb9a 100755 --- a/bin/bwcodex +++ b/bin/bwcodex @@ -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() { + [[ -e /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. @@ -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 diff --git a/bin/bwopencode b/bin/bwopencode index fbd76f4..584f0a7 100755 --- a/bin/bwopencode +++ b/bin/bwopencode @@ -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 @@ -283,6 +283,15 @@ build_managed_config() { fi } +# The host-managed skills alias may point into /opt/nsls2/n2snskills. 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() { + [[ -e /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 @@ -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 diff --git a/docs/bw-wrappers.md b/docs/bw-wrappers.md index 82b4ec1..acc0713 100644 --- a/docs/bw-wrappers.md +++ b/docs/bw-wrappers.md @@ -19,6 +19,17 @@ 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` + ## Common options Every `bw*` wrapper accepts these options: diff --git a/lib/bwrap_sandbox_lib.sh b/lib/bwrap_sandbox_lib.sh index 024c510..9ae682c 100644 --- a/lib/bwrap_sandbox_lib.sh +++ b/lib/bwrap_sandbox_lib.sh @@ -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+=( From 0c612e810b51e64ff138f84a9515247f07c129ef Mon Sep 17 00:00:00 2001 From: "Stuart B. Wilkins" Date: Fri, 25 Sep 2026 03:58:02 -0400 Subject: [PATCH 2/8] docs(bwrap): clarify shared skill setup Assisted-by: opencode:gpt-5.6-terra --- docs/bw-wrappers.md | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/docs/bw-wrappers.md b/docs/bw-wrappers.md index acc0713..72aca73 100644 --- a/docs/bw-wrappers.md +++ b/docs/bw-wrappers.md @@ -30,6 +30,16 @@ configuration directories. - Claude: `/etc/claude-code/.claude/skills` and `/etc/claude-code/managed-settings.json` +## 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. + +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: @@ -41,6 +51,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. @@ -107,6 +119,31 @@ 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 + +`bwcopilot` does not automatically mount the shared skills. Configure +Copilot CLI's documented `skillDirectories` in `$COPILOT_HOME/settings.json` +(default: `~/.copilot/settings.json`) to point at the deployment's shared +skills directory. In the example below, replace +`/path/to/deployment/shared-skills` with that directory. + +For `bwcopilot`, mount both the shared skills directory and the settings file +read-only. This command is appropriate when the default settings file is a +regular (non-symlink) file: + +```console +bwcopilot --ro-path /path/to/deployment/shared-skills \ + --ro-path "$HOME/.copilot/settings.json" +``` + +The read-only settings mount prevents Copilot from writing `/settings`; edit +the host settings file between sessions to change its configuration. + +`--ro-path` canonicalizes symlinks and binds the canonical source at that +same canonical destination. Therefore, a symlinked dotfiles settings alias is +not available as `$COPILOT_HOME/settings.json` in the sandbox. The example +requires a regular (non-symlink) settings file at that location. + ## `bwcodex` Launches the [OpenAI Codex CLI](https://github.com/openai/codex) inside From 796f710d7ce2186c61dd4f78375dcd834a853a9b Mon Sep 17 00:00:00 2001 From: "Stuart B. Wilkins" Date: Fri, 25 Sep 2026 08:15:43 -0400 Subject: [PATCH 3/8] feat(bwrap): mount managed Copilot skills Assisted-by: opencode:gpt-5.6-terra --- bin/bwcopilot | 4 ++++ docs/bw-wrappers.md | 26 +++++++++++++++----------- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/bin/bwcopilot b/bin/bwcopilot index 437b65d..cce22b3 100755 --- a/bin/bwcopilot +++ b/bin/bwcopilot @@ -409,6 +409,10 @@ main() { build_nsls2_mount build_dynamic_tool_mounts build_extra_path_mounts + # 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. + build_managed_ro_bind /opt/nsls2/n2snskills/skills /opt/nsls2/n2snskills/skills # Environment variables build_env_vars diff --git a/docs/bw-wrappers.md b/docs/bw-wrappers.md index 72aca73..2fd35c8 100644 --- a/docs/bw-wrappers.md +++ b/docs/bw-wrappers.md @@ -29,13 +29,17 @@ configuration directories. - 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. +`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. @@ -121,19 +125,19 @@ run `bwcopilot --init-auth` once to persist tokens. ### Shared skills -`bwcopilot` does not automatically mount the shared skills. Configure -Copilot CLI's documented `skillDirectories` in `$COPILOT_HOME/settings.json` -(default: `~/.copilot/settings.json`) to point at the deployment's shared -skills directory. In the example below, replace -`/path/to/deployment/shared-skills` with that directory. +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. -For `bwcopilot`, mount both the shared skills directory and the settings file -read-only. This command is appropriate when the default settings file is a -regular (non-symlink) file: +The automatic mount supplies the skills directory, but `bwcopilot` does not +automatically mount `settings.json`; making settings visible remains the +user's responsibility. To make a host settings file available read-only, use +`--ro-path`. This is appropriate when the default settings file is a regular +(non-symlink) file: ```console -bwcopilot --ro-path /path/to/deployment/shared-skills \ - --ro-path "$HOME/.copilot/settings.json" +bwcopilot --ro-path "$HOME/.copilot/settings.json" ``` The read-only settings mount prevents Copilot from writing `/settings`; edit From 59ceb5df2dbdb494505b6d9ac787402f2645357b Mon Sep 17 00:00:00 2001 From: "Stuart B. Wilkins" Date: Fri, 25 Sep 2026 08:29:58 -0400 Subject: [PATCH 4/8] docs(bwrap): clarify read-only Copilot settings Assisted-by: opencode:gpt-6-astra Assisted-by: opencode:gpt-5.6-terra --- docs/bw-wrappers.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/docs/bw-wrappers.md b/docs/bw-wrappers.md index 2fd35c8..81fda88 100644 --- a/docs/bw-wrappers.md +++ b/docs/bw-wrappers.md @@ -131,17 +131,18 @@ documented `skillDirectories` in `$COPILOT_HOME/settings.json` (default: `~/.copilot/settings.json`) to name that same directory. The automatic mount supplies the skills directory, but `bwcopilot` does not -automatically mount `settings.json`; making settings visible remains the -user's responsibility. To make a host settings file available read-only, use -`--ro-path`. This is appropriate when the default settings file is a regular -(non-symlink) file: +automatically mount `$COPILOT_HOME/settings.json`; making settings visible +remains the user's responsibility. To mount the host settings file read-only, +use `--ro-path`. This is appropriate when the default settings file is a +regular (non-symlink) file: ```console bwcopilot --ro-path "$HOME/.copilot/settings.json" ``` -The read-only settings mount prevents Copilot from writing `/settings`; edit -the host settings file between sessions to change its configuration. +The read-only `$COPILOT_HOME/settings.json` mount prevents Copilot's +`/settings` slash command from persisting changes in the sandbox. Edit the +host settings file between sessions to change its configuration. `--ro-path` canonicalizes symlinks and binds the canonical source at that same canonical destination. Therefore, a symlinked dotfiles settings alias is From 2562baaffed95ee6143e532c6c88f55c14b78ea1 Mon Sep 17 00:00:00 2001 From: "Stuart B. Wilkins" Date: Fri, 25 Sep 2026 08:55:01 -0400 Subject: [PATCH 5/8] feat(bwcopilot): mount skills and settings Assisted-by: opencode:gpt-5.6-terra --- bin/bwcopilot | 27 +++++++++++++++++++++++++++ docs/bw-wrappers.md | 26 ++++++++------------------ 2 files changed, 35 insertions(+), 18 deletions(-) diff --git a/bin/bwcopilot b/bin/bwcopilot index cce22b3..cc52864 100755 --- a/bin/bwcopilot +++ b/bin/bwcopilot @@ -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}") @@ -409,6 +433,9 @@ 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. diff --git a/docs/bw-wrappers.md b/docs/bw-wrappers.md index 81fda88..4d17032 100644 --- a/docs/bw-wrappers.md +++ b/docs/bw-wrappers.md @@ -130,24 +130,14 @@ 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. -The automatic mount supplies the skills directory, but `bwcopilot` does not -automatically mount `$COPILOT_HOME/settings.json`; making settings visible -remains the user's responsibility. To mount the host settings file read-only, -use `--ro-path`. This is appropriate when the default settings file is a -regular (non-symlink) file: - -```console -bwcopilot --ro-path "$HOME/.copilot/settings.json" -``` - -The read-only `$COPILOT_HOME/settings.json` mount prevents Copilot's -`/settings` slash command from persisting changes in the sandbox. Edit the -host settings file between sessions to change its configuration. - -`--ro-path` canonicalizes symlinks and binds the canonical source at that -same canonical destination. Therefore, a symlinked dotfiles settings alias is -not available as `$COPILOT_HOME/settings.json` in the sandbox. The example -requires a regular (non-symlink) settings file at that location. +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` From 8f408dc1809a0bac3cfecb79f51c9477bc48c4bc Mon Sep 17 00:00:00 2001 From: "Stuart B. Wilkins" Date: Sat, 26 Sep 2026 01:39:21 -0400 Subject: [PATCH 6/8] fix(bwrap): update managed skills path Assisted-by: opencode:gpt-5.6-terra --- bin/bwcopilot | 2 +- bin/bwopencode | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/bwcopilot b/bin/bwcopilot index cc52864..6cf89b1 100755 --- a/bin/bwcopilot +++ b/bin/bwcopilot @@ -439,7 +439,7 @@ main() { # 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. - build_managed_ro_bind /opt/nsls2/n2snskills/skills /opt/nsls2/n2snskills/skills + build_managed_ro_bind /opt/nsls2/skills/skills /opt/nsls2/skills/skills # Environment variables build_env_vars diff --git a/bin/bwopencode b/bin/bwopencode index 584f0a7..dfd6385 100755 --- a/bin/bwopencode +++ b/bin/bwopencode @@ -283,7 +283,7 @@ build_managed_config() { fi } -# The host-managed skills alias may point into /opt/nsls2/n2snskills. Bind +# 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() { From 65f611eee94725219a91ac854b663b9bf15e623d Mon Sep 17 00:00:00 2001 From: "Stuart B. Wilkins" Date: Sun, 27 Sep 2026 07:34:12 -0400 Subject: [PATCH 7/8] Apply batched suggestions from code review Co-authored-by: Padraic Shafer <76011594+padraic-shafer@users.noreply.github.com> --- bin/bwcodex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/bwcodex b/bin/bwcodex index 6f8cb9a..5d6260c 100755 --- a/bin/bwcodex +++ b/bin/bwcodex @@ -300,7 +300,7 @@ check_codex_config() { # 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() { - [[ -e /etc/codex/skills ]] || return 0 + [[ -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 } From b120de911837ee51c613cdcbb8f9c842366dfb6a Mon Sep 17 00:00:00 2001 From: "Stuart B. Wilkins" Date: Sun, 27 Sep 2026 07:49:05 -0400 Subject: [PATCH 8/8] fix(bwrap): guard managed skill directories Assisted-by: opencode:gpt-5.6-terra --- bin/bwclaude | 2 +- bin/bwcopilot | 4 +++- bin/bwopencode | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/bin/bwclaude b/bin/bwclaude index 8e5e52c..6c48d1e 100755 --- a/bin/bwclaude +++ b/bin/bwclaude @@ -342,7 +342,7 @@ build_managed_claude_paths() { /etc/claude-code/managed-settings.json fi - if [[ -e /etc/claude-code/.claude/skills ]]; then + 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 \ diff --git a/bin/bwcopilot b/bin/bwcopilot index 6cf89b1..c3e9966 100755 --- a/bin/bwcopilot +++ b/bin/bwcopilot @@ -439,7 +439,9 @@ main() { # 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. - build_managed_ro_bind /opt/nsls2/skills/skills /opt/nsls2/skills/skills + 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 diff --git a/bin/bwopencode b/bin/bwopencode index dfd6385..5aa64fe 100755 --- a/bin/bwopencode +++ b/bin/bwopencode @@ -287,7 +287,7 @@ build_managed_config() { # 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() { - [[ -e /etc/opencode/skills ]] || return 0 + [[ -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 }