feat(sandbox): bubblewrap-based local execution sandbox + path-grant flow#15
Merged
Conversation
…flow
Implements docs/LOCAL_EXECUTION_SANDBOX_DESIGN.md. Every CLAI-controlled
local tool execution on Linux now runs inside a bwrap sandbox with
per-agent filesystem grants, network policy, and session-bus policy.
macOS / Windows continue running through an unsandboxed fallback labeled
as "host shell — sandbox not yet available on this platform".
Sandbox layout
- workspace_root: read-write bind, persistent.
- system baseline: /usr, /etc (with /etc/ssh overlaid by a tmpfs to avoid
the unprivileged-userns OpenSSH "Bad owner or permissions" failure mode),
/bin, /sbin, /lib*, /sys all read-only; /proc via --proc; /dev via --dev;
/tmp via private tmpfs.
- Runtime symlink resolution: /etc/resolv.conf and /etc/localtime are
resolved at sandbox-build time. Targets that point into /run (e.g.
systemd-resolved stub) get private parent dirs + a ro-bind of the
resolved file. /run is never bind-mounted wholesale, so ssh-agent,
dbus, docker, gpg-agent and other host sockets stay out.
- Mount-ordering invariant: binds emitted shallowest-first so a
configured ancestor grant (e.g. $HOME RO) never overlays the deeper
workspace RW bind.
- Env filter: small allowlist (PATH, LANG, LC_*, TZ, TERM) + explicit
denylist for socket / display vars (SSH_AUTH_SOCK, DBUS_*, DOCKER_HOST,
DISPLAY, WAYLAND_DISPLAY, XAUTHORITY, etc.). HOME points at the user's
real $HOME so ~/.foo resolves the same way as in the user's shell.
Path-grant approval flow
- `fs_request_grant({path, access, reason})` tool. Approval modal
(InlinePathGrantCard) parallels the existing command-approval flow.
Validation: modal can narrow paths (descendant only) and downgrade
access (RW→RO only) but never widen or upgrade.
- "Always allow" grants persist to the agent's
`execution.filesystem.extra_paths` in workspace_agents, tagged with
GrantOrigin::Approval{reason, granted_at_unix_ms}. "Allow once"
applies to the current run via a session_grants Arc on
ToolExecutionContext.
- New RunNoticeKind variants: PathGranted, PathGrantDenied,
SandboxUnavailable.
Per-agent execution config
All under execution.sandbox.* (camelCase on the wire):
- network: enabled | disabled (default enabled). Maps to bwrap's
--share-net / --unshare-net.
- session_bus: allow | deny (default allow). When Allow, binds the
host's D-Bus session bus socket (parsed from DBUS_SESSION_BUS_ADDRESS
with fallback to $XDG_RUNTIME_DIR/bus) and passes
DBUS_SESSION_BUS_ADDRESS + XDG_RUNTIME_DIR through the env filter.
Required for gh / git-credential-libsecret / secret-tool. Other
socket env vars stay denied.
$HOME visibility
New agents' extra_paths defaults to one entry: the host's $HOME as
read-only. Frontend resolves the path at form-mount via Tauri's
homeDir(). Visible and removable in agent settings like any other
grant — remove it for a fully-isolated agent. Existing agents are not
auto-migrated.
Bwrap failure classification
"Sandboxed shell is unavailable" errors (bwrap missing, kernel refused
namespaces) surface as RunNoticeKind::SandboxUnavailable on the run.
Detection requires exit non-zero + empty stdout + any stderr line
starting with `bwrap:`, so an inner command that happens to print
`bwrap: ...` to its own stderr isn't misclassified as a setup failure.
Packaging
tauri.conf.json declares `bubblewrap (>= 0.4.0)` as a hard dependency
in the .deb / .rpm bundle configs so install pulls it transitively.
Flatpak still needs bwrap bundled inside the Flatpak runtime — flagged
in the design doc as a known gap.
System prompt updates
- "Filesystem boundary" section rewritten: read access matches the
user's, writes confined to the workspace, fs_request_grant is the
escape valve for everything else.
- Per-agent capability listing now reports network status, session-bus
status, and sandbox availability.
- "Git and SSH conventions" guard: never rewrite commit authorship to
bypass GH007; escalate via workspace_requestUserInput instead.
Frontend
- InlinePathGrantCard.jsx + .module.css: approval card mirroring
InlineApprovalCard. Mounted alongside it in Workspace and Fleet
pages.
- AgentFormModal: removed Credentials Preset toggle (redundant with the
$HOME default); added Desktop integration (D-Bus) toggle; new agents
pre-fill $HOME RO into extraPathGrants via homeDir(); grant chips
show provenance (Manual / Approved <date> with reason on hover).
- pathGrantsClient.js: thin invoke() wrapper for
submit_path_grant_decision + list_pending_path_grant_requests.
Tests
367 lib tests passing (337 baseline + 30 new) across:
- sandbox::profile (env filter + session-bus passthrough)
- sandbox::linux_bwrap (mount ordering, /etc/ssh overlay, bwrap failure
classification, D-Bus address resolution, workspace-writable-under-RO-
ancestor regression — exercised against real bwrap on Linux)
- commands::path_grants (registry round-trip, narrow / downgrade
validation)
- assistant::tools::local (path-already-covered short-circuit,
~ expansion)
Env-mutating tests serialize on a module-local Mutex to avoid races
with the Rust test runner's thread pool.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements docs/LOCAL_EXECUTION_SANDBOX_DESIGN.md. On Linux, every CLAI-controlled local tool execution (
bash_execand friends) now runs inside a bubblewrap sandbox with per-agent filesystem grants, network policy, and session-bus policy. macOS / Windows continue running through an unsandboxed fallback labeled in the UI as "host shell — sandbox not yet available on this platform."bwrapis missing or the kernel refuses unprivileged namespaces,bash_execfails closed withRunNoticeKind::SandboxUnavailablerather than silently degrading.fs_request_granttool gives agents a discoverable way to extend their filesystem grants at runtime, with a user-approval modal that parallels the existing command-approval flow.$HOMEshows up by default as a read-only entry in new agents' Additional Path Grants — visible, removable, no magic insandbox_profile().gh,git-credential-libsecret, andsecret-toolcan reach the host keyring.What's in the sandbox
/usr,/etc(with/etc/sshoverlaid by a tmpfs to avoid the unprivileged-userns OpenSSH "Bad owner or permissions" failure mode),/bin,/sbin,/lib*,/sysall RO;/procvia--proc;/devvia--dev;/tmpvia private tmpfs./etc/resolv.confand/etc/localtimeare resolved at build time. Targets that point into/run(e.g. systemd-resolved stub) get private parent dirs + a RO bind of the resolved file./runis never bind-mounted wholesale — keeps ssh-agent, dbus, docker, gpg-agent and other host sockets out.\$HOMERO) never overlays the deeper workspace RW bind. Regression test covers this against real bwrap.\$HOME.Path-grant approval flow
fs_request_grant({path, access, reason})tool. The agent calls it before attempting work that needs an out-of-grant path (e.g.gh pr createneeding~/.config/gh).InlinePathGrantCard) mirrorsInlineApprovalCard. Validation: modal can narrow paths (descendant only) and downgrade access (RW→RO only) — never widen or upgrade.execution.filesystem.extra_pathsin the workspace_agents DB row, tagged withGrantOrigin::Approval{reason, granted_at_unix_ms}. "Allow once" applies only to the current run via a session_grantsArconToolExecutionContext.Per-agent execution config
New fields under
execution.sandbox.*(camelCase on the wire):network: \"enabled\" | \"disabled\"(defaultenabled). Maps to--share-net/--unshare-net.sessionBus: \"allow\" | \"deny\"(defaultallow). When Allow, binds the host's D-Bus session bus socket (parsed fromDBUS_SESSION_BUS_ADDRESSwith fallback to\$XDG_RUNTIME_DIR/bus) and passesDBUS_SESSION_BUS_ADDRESS+XDG_RUNTIME_DIRthrough the env filter. Required for libsecret-based auth. Other socket env vars (SSH_AUTH_SOCK,DOCKER_HOST,DISPLAY, etc.) stay denied.$HOME visibility
New agents'
extra_pathsdefaults to one entry: the host's\$HOMEas read-only. Frontend resolves the path at form-mount via Tauri'shomeDir(). Visible and removable in agent settings like any other grant — remove it for a fully-isolated agent. Existing agents are not auto-migrated.Bwrap failure classification → run notice
"Sandboxed shell is unavailable" errors (bwrap missing, kernel refused namespaces) surface as
RunNoticeKind::SandboxUnavailableon the run, not just a generic tool failure. Detection requires exit non-zero + empty stdout + any stderr line starting withbwrap:, so an inner command that happens to printbwrap: ...to its own stderr isn't misclassified.Packaging
tauri.conf.jsondeclaresbubblewrap (>= 0.4.0)as a hard dependency in the.deb/.rpmbundle configs. Flatpak still needs bwrap bundled inside the runtime — flagged in the design doc as a known gap (the design's §Flatpak section spells out the bundled-bwrap approach).System prompt
fs_request_grantis the escape valve for everything else.GH007(GitHub email-privacy block); escalate viaworkspace_requestUserInputinstead.Frontend changes
InlinePathGrantCard.jsx+.module.css: approval card mirroringInlineApprovalCard. Mounted alongside it inWorkspaceandFleetpages.AgentFormModal: removedCredentials Presettoggle (redundant with the\$HOMEdefault); addedDesktop integration(D-Bus) toggle; new agents pre-fill\$HOMERO into extraPathGrants viahomeDir().pathGrantsClient.js: thin invoke() wrapper forsubmit_path_grant_decision+list_pending_path_grant_requests.Test plan
cargo test --lib— 367 passing / 1 ignored locally on Linux with bwrap 0.11.2 installed (the real-bwrap integration tests actually exercise namespaces).cargo fmt --checkclean.cargo clippy -- -D warnings(lib-only) clean.npm run lint— 0 errors, 47 pre-existing warnings.npm run build— vite bundle clean.~/.ssh, user approves "Always", agent pushes branch via SSH, opens PR viagh. Path-grant persistence verified in workspace_agents.execution.filesystem.extra_paths with provenance metadata.bubblewrapis pulled in by the.deb/.rpm.Known follow-ups (not in this PR)
bwrapbundled inside the runtime (extraction-from-deb workflow currently doesn't include it).db::canonicalize_legacy_tool_names: SQL path usesREPLACE(replaces-all) vs Rust JSON-blob path usesreplacen(first-only); would diverge on multi-dot tool names. Captured in an agent's SOW for a dedicated bug-fix PR.