-
Notifications
You must be signed in to change notification settings - Fork 0
fix(sandbox): overlay opencode's data dir so headless dispatch survives the read-only bwrap root #479
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix(sandbox): overlay opencode's data dir so headless dispatch survives the read-only bwrap root #479
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -24,6 +24,30 @@ use std::sync::OnceLock; | |
| /// defeating the containment this sandbox exists to provide. | ||
| const HOME_CACHE_DIRS: &[&str] = &[".cargo", ".rustup", ".cache", ".npm"]; | ||
|
|
||
| /// opencode's own data dir, relative to `$HOME` -- unlike claude-code/codex/ | ||
| /// gemini's headless modes, `opencode run` unconditionally writes into this | ||
| /// directory on every invocation (an append-mode log file, plus a SQLite | ||
| /// session DB it checkpoints), which crashes outright under the read-only | ||
| /// root (item #106: `FileSystem.open` on `opencode.log`, then a WAL | ||
| /// checkpoint failure once that's worked around). It also holds | ||
| /// `auth.json`, so it can't just go on `HOME_CACHE_DIRS` read-only -- opencode | ||
| /// needs to both read existing credentials/config *and* write. Mounted via | ||
| /// `--overlay-src`+`--tmp-overlay` in `build_bwrap_args_with_home`: reads | ||
| /// see the real directory, writes land in an invisible tmpfs that's | ||
| /// discarded when the sandboxed process exits, so nothing persists back to | ||
| /// the host -- same "no writable state survives past this one job" guarantee | ||
| /// `HOME_CACHE_DIRS` argues for, just via overlay instead of read-only-try | ||
| /// since this one dir needs both. | ||
| const OPENCODE_DATA_DIR_RELATIVE: &str = ".local/share/opencode"; | ||
|
|
||
| /// Whether `command` (the resolved binary about to be run, e.g. | ||
| /// `/home/user/.opencode/bin/opencode`) is opencode -- matched on the final | ||
| /// path component so it doesn't care whether the caller passed a bare name | ||
| /// or a full resolved path. | ||
| fn is_opencode(command: &str) -> bool { | ||
| Path::new(command).file_name() == Some(std::ffi::OsStr::new("opencode")) | ||
| } | ||
|
|
||
| /// `git_writable` controls whether `cwd/.git` is re-protected read-only | ||
| /// (the default, `false` -- appropriate for an arbitrary job command that | ||
| /// has no business rewriting git history) or left writable under the same | ||
|
|
@@ -146,6 +170,27 @@ fn build_bwrap_args_with_home( | |
| bwrap_args.push(path_str); | ||
| } | ||
| } | ||
|
|
||
| if is_opencode(command) { | ||
| let opencode_dir = Path::new(home).join(OPENCODE_DATA_DIR_RELATIVE); | ||
| let opencode_str = path_to_string(&opencode_dir); | ||
| if opencode_dir.exists() { | ||
| // `--overlay-src` requires its source to already exist; | ||
| // reads pass through to it, writes go to the implicit tmpfs | ||
| // `--tmp-overlay` adds on top -- never touching the host. | ||
| bwrap_args.push("--overlay-src".to_string()); | ||
| bwrap_args.push(opencode_str.clone()); | ||
| bwrap_args.push("--tmp-overlay".to_string()); | ||
| bwrap_args.push(opencode_str); | ||
|
Comment on lines
+181
to
+184
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
bwrap="$(command -v bwrap || true)"
test -n "$bwrap"
help="$("$bwrap" --help 2>&1)"
printf '%s\n' "$help" | grep -q -- '--overlay-src'
printf '%s\n' "$help" | grep -q -- '--tmp-overlay'
"$bwrap" --versionRepository: getappz/agentflare Length of output: 156 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- relevant source files ---'
git ls-files 'crates/agentflare-jobs/src/sandbox/bwrap/mod.rs' 'crates/agentflare-jobs/src/sandbox/mod.rs' '*Cargo.toml' '*Dockerfile*' '*.yml' '*.yaml' | sed -n '1,160p'
printf '%s\n' '--- bwrap option usage and command path ---'
rg -n -C 5 -- '--overlay-src|--tmp-overlay|build_bwrap_args_with_home|OpenCode|opencode' crates Cargo.toml .github 2>/dev/null | sed -n '1,260p'
printf '%s\n' '--- local bubblewrap availability ---'
command -v bwrap || true
if command -v bwrap >/dev/null 2>&1; then
bwrap --version || true
bwrap --help 2>&1 | grep -E -- '--overlay-src|--tmp-overlay' || true
fiRepository: getappz/agentflare Length of output: 22022 🌐 Web query:
💡 Result: In Bubblewrap version 0.11.0, new options were introduced—specifically --overlay, --tmp-overlay, --ro-overlay, and --overlay-src—to enable the creation of overlay filesystem mounts [1][2]. These features are explicitly restricted from functioning when Bubblewrap is installed in setuid mode [1][2]. Attempting to use these options while running a setuid-installed binary will not work as intended [3][4][5]. A critical security vulnerability, CVE-2026-41163, was identified in 2026 concerning setuid mode, where attackers could use ptrace to bypass these restrictions and perform unauthorized overlay mounts [6]. This was patched in version 0.11.2 [6][7]. The maintainers consider setuid support for Bubblewrap deprecated and advise moving to systems that support unprivileged user namespaces [6][7]. Newer versions also provide a build option, -Dsupport_setuid=false, to explicitly disable and safely ignore setuid-related security risks [2][7]. Citations:
🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- deployment references to bubblewrap ---'
rg -n -i -C 5 'bubblewrap|bwrap|setuid|user namespace|unprivileged' Dockerfile .github crates Cargo.toml README.md docs 2>/dev/null | sed -n '1,260p'
printf '%s\n' '--- surrounding implementation and tests ---'
sed -n '1,215p' crates/agentflare-jobs/src/sandbox/bwrap/mod.rs
sed -n '300,430p' crates/agentflare-jobs/src/sandbox/bwrap/mod.rs
sed -n '500,580p' crates/agentflare-jobs/src/sandbox/bwrap/mod.rsRepository: getappz/agentflare Length of output: 23946 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- bwrap installer ---'
sed -n '1,180p' crates/agentflare-jobs/src/sandbox/bwrap/bwrap_install.rs
printf '%s\n' '--- argument construction and tests ---'
sed -n '95,215p' crates/agentflare-jobs/src/sandbox/bwrap/mod.rs
sed -n '300,430p' crates/agentflare-jobs/src/sandbox/bwrap/mod.rs
sed -n '430,580p' crates/agentflare-jobs/src/sandbox/bwrap/mod.rs
printf '%s\n' '--- all bwrap-related files and references ---'
git ls-files | rg -i 'bwrap|bubblewrap|docker|readme|deploy|install' | sed -n '1,220p'
rg -n -i 'bwrap|bubblewrap|setuid|tmp-overlay|overlay-src' --glob '!target/**' --glob '!node_modules/**' . | sed -n '1,320p'Repository: getappz/agentflare Length of output: 31030 Enforce Bubblewrap overlay support before launching OpenCode. When |
||
| } else { | ||
| // Never run before under this `$HOME` -- nothing to read, | ||
| // so a plain writable tmpfs (no read-only source needed) | ||
| // covers the same "opencode can create+write its own data | ||
| // dir" case. | ||
| bwrap_args.push("--tmpfs".to_string()); | ||
| bwrap_args.push(opencode_str); | ||
| } | ||
| } | ||
|
Comment on lines
+174
to
+193
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
# Expect: provider-cache prewarming is guaranteed, or an OpenCode cache overlay exists.
rg -n -C 4 \
'HOME_CACHE_DIRS|XDG_CACHE_HOME|opencode|provider|npm|bun' .Repository: getappz/agentflare Length of output: 50375 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- sandbox implementation ---'
sed -n '1,220p' crates/agentflare-jobs/src/sandbox/bwrap/mod.rs
printf '%s\n' '--- relevant tests ---'
sed -n '480,575p' crates/agentflare-jobs/src/sandbox/bwrap/mod.rs
printf '%s\n' '--- repository OpenCode/cache references ---'
rg -n -S \
--glob '!target/**' \
--glob '!node_modules/**' \
--glob '!*.lock' \
'OPENCODE|opencode|XDG_CACHE_HOME|XDG_DATA_HOME|provider cache|provider-cache|HOME_CACHE_DIRS|BUN_INSTALL|npm_config_cache|bunx' \
README.md .github crates Dockerfile* docker-compose* 2>/dev/null || true
printf '%s\n' '--- relevant tracked files ---'
git ls-files | rg -i '(^|/)(dockerfile|compose|opencode|sandbox|bwrap|image|container)' | head -200Repository: getappz/agentflare Length of output: 19433 🌐 Web query:
💡 Result: OpenCode utilizes the XDG Base Directory Specification for its cache location [1]. By default, the cache directory for OpenCode is located at ~/.cache/opencode/ on Linux and macOS, and %USERPROFILE%.cache\opencode on Windows [2][3]. The XDG_CACHE_HOME environment variable defines the base directory for non-essential, user-specific data [4][5]. When XDG_CACHE_HOME is set, OpenCode respects this path, and the cache will be placed within $XDG_CACHE_HOME/opencode/ [1]. Regarding package and plugin installation: - OpenCode uses this cache directory to store downloaded provider packages and dependencies [6][7]. - Official documentation and current implementations commonly reference ~/.cache/opencode/packages/ or ~/.cache/opencode/node_modules/ [8][7]. - There have been documented issues regarding inconsistencies between the directory structure used by the installation process and the path expected by the plugin loader [8][9]. - Users seeking to relocate the cache, particularly for CI/CD or environment-specific workflows (e.g., Kubernetes), have requested an explicit environment variable (such as OPENCODE_CACHE) to override the default behavior, though setting XDG_CACHE_HOME generally directs the path successfully [1][6][10]. For troubleshooting, if a plugin installation becomes stuck or corrupted, the standard procedure is to quit the application and delete the contents of the cache directory [2][3]. On Linux/macOS, this is typically done via rm -rf ~/.cache/opencode [2][3]. Citations:
🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- Dockerfile ---'
cat -n Dockerfile
printf '%s\n' '--- OpenCode installation and image setup ---'
rg -n -C 5 -S \
--glob '!target/**' \
--glob '!*.lock' \
'(`@opencode-ai/opencode`|opencode|npm install|npm i |bun install|bun add|pnpm|yarn|XDG_CACHE_HOME|XDG_DATA_HOME|provider|prewarm)' \
Dockerfile .github crates 2>/dev/null || true
printf '%s\n' '--- all tracked workflow and container configuration ---'
git ls-files | rg '(^|/)(Dockerfile[^/]*|docker-compose[^/]*|.*\.ya?ml)$' | while read -r f; do
printf '\n--- %s ---\n' "$f"
rg -n -C 3 -S \
'opencode|XDG_CACHE_HOME|XDG_DATA_HOME|npm|bun|pnpm|yarn|provider|cache' \
"$f" || true
doneRepository: getappz/agentflare Length of output: 50377 Keep OpenCode’s provider cache writable in the temporary layer. OpenCode stores packages under 🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| bwrap_args.push("--".to_string()); | ||
|
|
@@ -467,4 +512,51 @@ mod tests { | |
| .expect(".cargo cache dir bound"); | ||
| assert_eq!(args[idx - 1], "--ro-bind-try"); | ||
| } | ||
|
|
||
| #[test] | ||
| fn opencode_data_dir_overlaid_when_present() { | ||
| let dir = tempfile::tempdir().unwrap(); | ||
| let data_dir = dir.path().join(OPENCODE_DATA_DIR_RELATIVE); | ||
| std::fs::create_dir_all(&data_dir).unwrap(); | ||
| let home = std::ffi::OsString::from(dir.path()); | ||
| let args = build_bwrap_args_with_home(None, "/usr/bin/opencode", &[], Some(&home), false); | ||
| let data_str = path_to_string(&data_dir); | ||
| let src_idx = args | ||
| .iter() | ||
| .position(|a| a == "--overlay-src") | ||
| .expect("--overlay-src present"); | ||
| assert_eq!(args[src_idx + 1], data_str); | ||
| let overlay_idx = args | ||
| .iter() | ||
| .position(|a| a == "--tmp-overlay") | ||
| .expect("--tmp-overlay present"); | ||
| assert_eq!(args[overlay_idx + 1], data_str); | ||
| } | ||
|
|
||
| #[test] | ||
| fn opencode_data_dir_uses_tmpfs_when_absent() { | ||
| let dir = tempfile::tempdir().unwrap(); | ||
| let home = std::ffi::OsString::from(dir.path()); | ||
| let args = build_bwrap_args_with_home(None, "/usr/bin/opencode", &[], Some(&home), false); | ||
| let data_str = path_to_string(&dir.path().join(OPENCODE_DATA_DIR_RELATIVE)); | ||
| let idx = args | ||
| .iter() | ||
| .position(|a| a == &data_str) | ||
| .expect("opencode data dir tmpfs-mounted"); | ||
| assert_eq!(args[idx - 1], "--tmpfs"); | ||
| assert!(!args.iter().any(|a| a == "--overlay-src")); | ||
| } | ||
|
|
||
| #[test] | ||
| fn non_opencode_command_gets_no_opencode_specific_mount() { | ||
| let dir = tempfile::tempdir().unwrap(); | ||
| std::fs::create_dir_all(dir.path().join(OPENCODE_DATA_DIR_RELATIVE)).unwrap(); | ||
| let home = std::ffi::OsString::from(dir.path()); | ||
| let args = build_bwrap_args_with_home(None, "/usr/bin/claude", &[], Some(&home), false); | ||
| assert!( | ||
| !args | ||
| .iter() | ||
| .any(|a| a == "--overlay-src" || a == "--tmp-overlay") | ||
| ); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
Repository: getappz/agentflare
Length of output: 38674
🏁 Script executed:
Repository: getappz/agentflare
Length of output: 50374
🌐 Web query:
OpenCode source global.ts xdgData XDG_DATA_HOME opencode data directory💡 Result:
OpenCode manages its data directory locations by strictly following the XDG Base Directory Specification, implemented via the
xdg-basedirlibrary in its core source code [1][2][3]. In the source filepackages/core/src/global.ts(often referenced in the project structure as the Global module), OpenCode defines its base paths using thexdgDatavariable [2][4]. The resolution logic is: 1. XDG_DATA_HOME (if set) [5][6] 2. ~/.local/share (default on Linux/macOS) [2][5] Crucially, OpenCode applies this XDG-compliant logic across all platforms, including Windows [3][7]. It does not branch to native Windows conventions like %LOCALAPPDATA% [3]. Consequently, on Windows, the data directory defaults to %USERPROFILE%.local\share\opencode [1][3]. Key details regarding OpenCode's data directory management: - Implementation: TheGlobalnamespace manages these paths [2][4]. Thexdg-basedirpackage is imported and used to resolvexdgData,xdgCache,xdgConfig, andxdgState[4]. - Data Path:Global.Path.dataresolves toXDG_DATA_HOME/opencode(or the default fallback) [2][5]. - Platform Consistency: Because the application uses thexdg-basedirpackage unconditionally, it maintains a unified directory structure (.local/share/opencode) on Linux, macOS, and Windows, which frequently causes confusion for users expecting OS-specific standard paths (like ~/Library/Application Support on macOS or %LOCALAPPDATA% on Windows) [5][3][7]. - Environment Variable Override: Users can explicitly override the data directory by setting theOPENCODE_DATA_DIRenvironment variable, which takes precedence over all automatic detection [5][6]. This design has been the subject of multiple community discussions regarding adherence to the XDG specification, particularly concerning the separation of configuration, data, state, and cache files [8][9][10].Citations:
🏁 Script executed:
Repository: getappz/agentflare
Length of output: 36835
🏁 Script executed:
Repository: getappz/agentflare
Length of output: 434
🌐 Web query:
site:github.com/anomalyco/opencode "OPENCODE_DATA_DIR" "xdgData"💡 Result:
OPENCODE_DATA_DIR is an environment variable used to override the default location of the OpenCode data directory, allowing users to configure where the application stores its data [1][2]. By default, OpenCode adheres to the XDG Base Directory Specification, typically placing data in ~/.local/share/opencode on Linux [3][4]. Setting OPENCODE_DATA_DIR is part of a suite of environment variables designed to enable portable mode and profile isolation, allowing OpenCode to run in environments such as containers or from removable drives without relying on global system paths [1][5]. The precedence for determining directory paths in OpenCode is as follows [2]: OPENCODE_DATA_DIR (or other specific *_DIR overrides) > OPENCODE_APPNAME > XDG default ("opencode") When used alongside other environment variables—such as OPENCODE_CONFIG_DIR, OPENCODE_CACHE_DIR, OPENCODE_LOG_DIR, and OPENCODE_STATE_DIR—it provides complete control over the application's directory structure, effectively decoupling it from default XDG locations [5][2].
Citations:
🏁 Script executed:
Repository: getappz/agentflare
Length of output: 1816
Resolve the effective OpenCode data directory.
If a job sets
XDG_DATA_HOME, OpenCode uses$XDG_DATA_HOME/opencode, but the sandbox always overlays$HOME/.local/share/opencode. The child then writes to an unmounted read-only path and can fail. Resolve the data path from the child environment, or clear and forbidXDG_DATA_HOMEfor OpenCode jobs.🤖 Prompt for AI Agents