Skip to content

feat(jobs): sandbox job commands with bubblewrap on Linux/WSL2 - #420

Merged
getappz merged 5 commits into
masterfrom
jobs/bwrap-sandbox-linux
Aug 10, 2026
Merged

feat(jobs): sandbox job commands with bubblewrap on Linux/WSL2#420
getappz merged 5 commits into
masterfrom
jobs/bwrap-sandbox-linux

Conversation

@getappz

@getappz getappz commented Aug 9, 2026

Copy link
Copy Markdown
Owner

Summary

  • Supervisor::spawn() now runs job commands inside a bwrap sandbox on native Linux and WSL2: read-only root, writable bind for only the job's cwd, .git re-protected read-only even inside that writable root, private tmpfs (not the host's shared /tmp), PID/user namespace containment. Windows and macOS are unchanged — bubblewrap has no equivalent on either platform.
  • When bwrap isn't on PATH, it's installed on demand via mise install "github:openai/codex[matching_regex=^bwrap-.*\.tar\.gz$]@rust-v0.147.0" — bubblewrap's own GitHub releases ship a source tarball only, so this reuses openai/codex's prebuilt, statically-linked release binaries instead. Falls back to unsandboxed (with a logged warning, not silently) if bwrap/mise aren't available.
  • Fixes a real bug caught while testing: a test mutated the process-wide PATH env var, which raced with a concurrently-running test under cargo's parallel test harness.

Context

Threat model: the GitHub bridge routes external issue content into autonomous work job execution across a mixed fleet (Windows+WSL2, native Linux, macOS). This adds OS-level containment as defense-in-depth under the bridge's existing author_association gate — narrowing blast radius if a routed job is compromised or misbehaves, independent of whether the routing decision itself was right.

Flag set and design adapted from openai/codex's own linux-sandbox crate, scaled down: no split filesystem policy, glob-based path masking, or seccomp network filter, since this job runner operates on worktrees it already controls rather than arbitrary third-party workspaces.

Test plan

  • cargo fmt --check / cargo clippy -- -A unsafe_code -A clippy::pedantic clean on Windows target
  • Full existing test suite (23 tests) passes unmodified on Windows
  • bwrap flag set validated against a real WSL2 kernel: cwd writes succeed, .git/outside-cwd writes denied, descendants reaped on exit with no host leak, private tmpfs isolated from host /tmp and from other concurrent sandboxes
  • mise install/mise where sequence validated end-to-end against a real WSL2 install, disambiguating the right binary out of a release bundling several unrelated tools
  • Built and ran the full suite natively on Linux (WSL2, not cross-compiled) — 28/28 tests pass, including the descendant-reaping test exercising the real /proc-walking implementation and the full supervisor_test.rs suite routing through the real sandbox::wrap() path
  • Concurrency validated: two sandboxes run genuinely overlapped (not serialized), each with isolated /tmp, no cross-visibility

Known gaps (out of scope for this PR)

  • No CPU/memory resource limits per sandbox (cgroups territory, separate mechanism)
  • Windows-native (no WSL2) and macOS remain unsandboxed by design — flagged in this session, not attempted here (see PR discussion for the sizing rationale)

Summary by CodeRabbit

  • New Features
    • Added sandboxed job execution on Linux and WSL2 using Bubblewrap.
    • Isolated filesystem, temporary directory, device, process, and cache access.
    • Automatically locates or installs the required sandbox tool.
    • Supports normalized working directories and writable job folders.
  • Bug Fixes
    • Jobs gracefully fall back to standard execution when sandboxing is unavailable.
    • Added validation for incomplete installations, failed commands, and timed-out setup processes.

Supervisor::spawn now runs commands through a bwrap sandbox on native
Linux and WSL2 (read-only root, writable bind for the job's cwd only,
.git re-protected read-only, private tmpfs, PID/user namespace
containment). Windows and macOS are unchanged -- bubblewrap has no
equivalent on either, so those platforms keep running unsandboxed as
before. Falls back to unsandboxed on Linux too if bwrap isn't on PATH.

Flag set adapted from openai/codex's linux-sandbox crate. Verified
against a real WSL2 kernel: cwd writes succeed, .git/outside-cwd
writes are denied, descendants are reaped on exit with no host leak,
and job scratch space uses a private tmpfs rather than the host's
shared /tmp.

Agentflare-Agent: claude-code_2-1-225_agent
Agentflare-Branch: jobs/bwrap-sandbox-linux
…'s binaries

bwrap.rs -> bwrap/mod.rs (fixes a module-path bug: mod bwrap_install
declared inside bwrap.rs needs the submodule at bwrap/bwrap_install.rs,
not a flat sibling file).

New bwrap/bwrap_install.rs: when bwrap isn't on PATH, installs it via
`mise install "github:openai/codex[matching_regex=^bwrap-.*\.tar\.gz$]@rust-v0.147.0"`.
Bubblewrap's own GitHub releases ship a source tarball only -- no
prebuilt binaries -- so this reuses openai/codex's own prebuilt,
statically-linked (musl) release binaries instead. Requires mise on
PATH, which agentflare's own doctor/setup flow already ensures, so
this is an existing dependency at a new call site rather than a new
one (avoids adding ureq/flate2/tar/sha2 just to reimplement what mise
already does).

find_or_install_bwrap() checks PATH first, installs on miss, and
caches the result for the process lifetime so a box without bwrap (or
without mise) doesn't retry the install on every job. Logs the
outcome either way so a job silently running unsandboxed isn't
indistinguishable from a sandboxed one.

Verified end-to-end against a real WSL2 Ubuntu install: the exact
mise install/where command sequence correctly disambiguates the right
binary out of a release that bundles several unrelated tools, for the
right host arch, and the extracted binary runs.

Agentflare-Agent: claude-code_2-1-225_agent
Agentflare-Branch: jobs/bwrap-sandbox-linux
which_bwrap()'s test called std::env::set_var("PATH", "") to check the
not-found case, but cargo's test harness runs tests in parallel across
threads within one process -- env vars are process-global, not
thread-local, so that mutation raced with any concurrently-running
test that spawns a child process expecting PATH intact.

Caught by actually running the suite natively on Linux (WSL2) for the
first time: spawn_times_out_and_kills_a_descendant_that_escaped_into_
its_own_process_group failed with ENOENT ("sh" not found) because it
happened to run while the PATH-clearing test held PATH empty.

Fixed by factoring the PATH search into a pure find_on_path(path,
name) helper the test calls directly with an injected empty OsStr,
touching no process state. Full suite (28 tests) now passes natively
on Linux, including the descendant-reaping test exercising the real
/proc-walking implementation and the full supervisor_test.rs suite
routing through the real sandbox::wrap() path.

Agentflare-Agent: claude-code_2-1-225_agent
Agentflare-Branch: jobs/bwrap-sandbox-linux
@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The change adds platform-specific Bubblewrap sandboxing for job commands. Linux discovers or installs Bubblewrap, builds isolated execution arguments, and applies the wrapper during supervisor process creation. Other platforms retain direct command execution.

Changes

Job sandboxing

Layer / File(s) Summary
Sandbox API and platform routing
crates/agentflare-jobs/src/lib.rs, crates/agentflare-jobs/src/sandbox/mod.rs
Registers the sandbox module and exposes platform-specific wrapping with unchanged fallback behavior.
Bubblewrap discovery and installation
crates/agentflare-jobs/src/sandbox/bwrap/bwrap_install.rs, crates/agentflare-jobs/src/sandbox/bwrap/mod.rs
Locates Bubblewrap through PATH or installs the pinned artifact through mise. Installation has timeout and validation handling. Availability is cached.
Isolated command execution
crates/agentflare-jobs/src/sandbox/bwrap/mod.rs, crates/agentflare-jobs/src/supervisor.rs
Builds isolated mounts, normalizes the working directory, adds read-only cache bindings, and executes the wrapped command through Supervisor::spawn. Tests cover discovery and path handling.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Supervisor
  participant SandboxWrap
  participant BwrapDiscovery
  participant Bubblewrap
  Supervisor->>SandboxWrap: wrap(command, args, cwd)
  SandboxWrap->>BwrapDiscovery: locate or install bwrap
  BwrapDiscovery-->>SandboxWrap: bwrap path or unavailable
  SandboxWrap->>Bubblewrap: execute isolated command
  Bubblewrap-->>Supervisor: child process result
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely identifies the main change: Bubblewrap sandboxing for job commands on Linux and WSL2.
Description check ✅ Passed The description provides a detailed summary, context, test results, known gaps, and reviewer-relevant risks despite omitting the template's Notes for reviewers heading.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch jobs/bwrap-sandbox-linux

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/agentflare-jobs/src/sandbox/bwrap/bwrap_install.rs`:
- Around line 26-30: Update the installer flow around the mise Command and
Supervisor::spawn so it launches via a child process, uses the bootstrap timeout
while waiting, terminates the child when the timeout expires, and sets stdin to
Stdio::null() alongside the existing stdout suppression. Preserve the current
optional status handling for successful completion and failed process startup.

In `@crates/agentflare-jobs/src/sandbox/bwrap/mod.rs`:
- Around line 140-143: Require executable permissions for every PATH tool
lookup: update find_on_path in crates/agentflare-jobs/src/sandbox/bwrap/mod.rs
(lines 140-143) to skip non-executable bwrap candidates, and update the mise
candidate checks plus installed bwrap validation in
crates/agentflare-jobs/src/sandbox/bwrap/bwrap_install.rs (lines 43-51) to
require executable files while continuing to later PATH entries.
- Around line 71-80: Update the HOME cache mounting logic around HOME_CACHE_DIRS
to prevent writable host mounts: replace the current --bind-try arguments with
read-only bind mounts, or redirect these paths to isolated per-job cache
directories. Preserve conditional mounting only for existing paths and ensure
jobs cannot persist changes to the host HOME directories.
- Around line 53-69: Normalize the optional cwd to an absolute path relative to
the supervisor process directory before constructing any Bubblewrap arguments.
Update the cwd handling around path_to_string, including .git detection and
--chdir, so --bind never receives a relative path; add coverage for cwd="." and
another relative cwd.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: beb0e9c4-61ff-47dd-a207-83e34e898649

📥 Commits

Reviewing files that changed from the base of the PR and between 34ffb29 and 352f10c.

📒 Files selected for processing (5)
  • crates/agentflare-jobs/src/lib.rs
  • crates/agentflare-jobs/src/sandbox/bwrap/bwrap_install.rs
  • crates/agentflare-jobs/src/sandbox/bwrap/mod.rs
  • crates/agentflare-jobs/src/sandbox/mod.rs
  • crates/agentflare-jobs/src/supervisor.rs

Comment thread crates/agentflare-jobs/src/sandbox/bwrap/bwrap_install.rs Outdated
Comment thread crates/agentflare-jobs/src/sandbox/bwrap/mod.rs
Comment thread crates/agentflare-jobs/src/sandbox/bwrap/mod.rs Outdated
Comment thread crates/agentflare-jobs/src/sandbox/bwrap/mod.rs Outdated
- HOME_CACHE_DIRS (.cargo/.rustup/.cache/.npm) now mount read-only
  (--ro-bind-try) instead of writable -- a writable bind let a
  sandboxed job persist changes into host caches, undermining the
  sandbox's own containment.
- bwrap PATH lookup (find_on_path) and mise-install's own which()
  now require the executable bit, not just is_file(), so a stray
  non-executable file with a matching name can't silently break the
  fallback-to-unsandboxed path.
- Relative job cwd is normalized to an absolute, lexically-clean path
  before being used in --bind/--chdir args, since bwrap takes those
  paths literally (unlike Command::current_dir).
- mise install now runs as a killable child with a bounded timeout
  and stdin closed, instead of a blocking .status() call that could
  hang the worker thread forever on a stalled network or a prompt.

Verified natively on WSL2 Linux (real bwrap present): full
agentflare-jobs suite (33 tests) passes, cargo fmt clean, clippy
clean with -A unsafe_code -A clippy::pedantic -D warnings.

Agentflare-Agent: claude-code_2-1-225_agent
Agentflare-Branch: jobs/bwrap-sandbox-linux

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/agentflare-jobs/src/sandbox/bwrap/mod.rs`:
- Around line 112-139: Replace the lexical normalization in absolute_path with
filesystem-aware resolution for cwd, preserving symlink traversal semantics;
have absolute_path return None when current_dir() or canonicalization fails so
sandbox::wrap uses its existing fallback. Update callers as needed to handle the
optional path, and add coverage for a symlink followed by `..`.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 9e068f29-a817-4c76-a4a2-9354587b9459

📥 Commits

Reviewing files that changed from the base of the PR and between 352f10c and 0d7419d.

📒 Files selected for processing (2)
  • crates/agentflare-jobs/src/sandbox/bwrap/bwrap_install.rs
  • crates/agentflare-jobs/src/sandbox/bwrap/mod.rs

Comment thread crates/agentflare-jobs/src/sandbox/bwrap/mod.rs Outdated
CodeRabbit's second pass on 0d7419d flagged the previous absolute_path fix:
stripping '.' / '..' components lexically is wrong the moment a symlink
sits in the path (link/.. is the parent of whatever link points at, not
the lexical parent of link itself), which could bind the wrong directory
into the sandbox.

Switch to std::fs::canonicalize, matching realpath/kernel resolution.
absolute_path now returns None on an unresolvable cwd, and wrap() bails
out to the existing unsandboxed fallback in that case rather than
proceeding with a bind path that doesn't mean what it looks like.

Added a regression test with an actual symlink + '..' to pin the exact
case lexical normalization got wrong, plus tests for the missing-path
and already-absolute cases.

Verified natively on WSL2 Linux (real bwrap present): full
agentflare-jobs suite (35 tests) passes, fmt clean, clippy clean with
-A unsafe_code -A clippy::pedantic -D warnings.

Agentflare-Agent: claude-code_2-1-225_agent
Agentflare-Branch: jobs/bwrap-sandbox-linux
@getappz
getappz merged commit 9c47be7 into master Aug 10, 2026
16 checks passed
@getappz
getappz deleted the jobs/bwrap-sandbox-linux branch August 10, 2026 03:55
getappz added a commit that referenced this pull request Aug 11, 2026
…#445)

run_headless builds its own Command directly and never went through
Supervisor::spawn, so the bwrap sandboxing added in #420 never covered the
headless agent CLI subprocess that autonomous work-item dispatch
(dispatch_item -> in_process -> WorkItemExecutor -> execute_work) actually
launches. Wrap the headless argv through agentflare_jobs::sandbox::wrap
before spawning, same as Supervisor::spawn does, and expose that module
publicly so the root crate can call it.

Agentflare-Agent: claude-code
Agentflare-Branch: task/67
Agentflare-Item: 67

Co-authored-by: shiva <shiva@gosysinfo.tech>
getappz added a commit that referenced this pull request Aug 14, 2026
…eal commits (#482)

Item #109: a real commit landed on the claimed branch and was pushed to
origin, but gh pr create never resulted in a PR (soft-failed silently),
and the item still went straight to completed. push_and_open_pr's
soft-fail-and-return-None contract predates item #420's split of done,
back when the item was already marked completed before this code ran.
Now that the completed transition happens right here, a diverged branch
with should_push set but no resulting PR must hard-error instead of
falling through to mark_completed -- same treatment item #92 already
gives an auto-commit failure on a dirty tree.

Added a regression test using a local bare-repo origin (push succeeds,
gh pr create is unreachable) to reproduce push-without-PR and confirm
done returns a hard error, posts a comment, and leaves completed_at unset.

item.rs crossed the 1500-line LOC gate as a result; allowlisted at the
frozen 2000-line ceiling like the codebase's other over-limit files, since
splitting its per-action handlers into submodules is separate, larger work
than a completion-correctness fix should carry.

Agentflare-Agent: claude-code
Agentflare-Branch: task/109-agentflare-work-marks-an-item-done-even
Agentflare-Item: 109

Co-authored-by: shiva <shiva@gosysinfo.tech>
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.

1 participant