fix: cwd:// VFS, macOS 15 sandbox, --yes approval, --session, capability merge, lifecycle home_root - #603
Merged
Merged
Conversation
Contributor
|
Warning You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again! |
- fs.rs: add cwd:// scheme to resolve_path() — was handled in security gate but not runtime path resolution, causing capsules to get security denials when using cwd:// paths - sandbox.rs: skip sandbox-exec (Seatbelt) on macOS 15+ (Darwin >= 24) — sandbox-exec is deprecated and crashes with SIGABRT on macOS Sequoia - main.rs: add -y/--yes/--yolo/--autonomous flag for headless mode auto-approval; fix approval response topic to include request_id (was going to generic topic, kernel never received it)
joshuajbouw
force-pushed
the
fix/cwd-vfs-runtime-resolution
branch
from
March 23, 2026 22:57
b1c9c44 to
0412259
Compare
83 tasks
- main.rs: add --session <name> for multi-turn headless conversations (deterministic UUID v5 from session name). Add --print-session flag. - discovery.rs: merge [[component]].capabilities into root capabilities so the security gate sees fs_read/fs_write/host_process declared at the component level. - mod.rs: add home_root to LifecycleConfig, mount home:// VFS during lifecycle hooks so on_install can write to principal home. - install.rs: resolve and pass principal home_root to lifecycle config. - lifecycle_e2e.rs: add home_root: None to test configs.
joshuajbouw
force-pushed
the
fix/cwd-vfs-runtime-resolution
branch
from
March 24, 2026 01:28
6693eae to
b5fbef1
Compare
…OS 15 - Use #[allow] instead of #[expect] — lint fires on Linux (bwrap borrows inner_cmd) but not on macOS (early return consumes it) - Update wrap_uses_inline_profile test to handle macOS 15+ passthrough
joshuajbouw
force-pushed
the
fix/cwd-vfs-runtime-resolution
branch
from
March 24, 2026 01:32
b5fbef1 to
a7d624e
Compare
This was referenced Jun 5, 2026
joshuajbouw
added a commit
that referenced
this pull request
Jun 6, 2026
…857) ## Linked Issue Closes #855 ## Summary `SandboxCommand::wrap` — the macOS arm of the `host_process` spawn path — disabled the OS-level subprocess sandbox on **every current Mac** (Darwin >= 24 = macOS 15 Sequoia / macOS 26) and returned the command **completely unsandboxed**, behind: ```rust if seatbelt::darwin_major_version() >= 24 { tracing::warn!("macOS 15+ detected: sandbox-exec is deprecated. Running host process unsandboxed."); return Ok(inner_cmd); } ``` On `host_process`-capable capsules (`astrid-capsule-shell`) every native subprocess then inherited the host user's full filesystem reach — it could read `~/.ssh`, dotfiles, and write anywhere the user can — with a single `tracing::warn!` as the only signal. Containment also depended on which spawn path a capsule happened to use: the MCP path (`ProcessSandboxConfig::sandbox_prefix`) has **no** version guard and sandboxed on the same machine. ### Root cause The premise of the guard — that `sandbox-exec` is deprecated and therefore non-enforcing on macOS 15+ — is **false**. `sandbox-exec` is deprecated but still enforces on current macOS. The SIGABRT that motivated the guard (introduced in #603, no reproducer, merged without review) was a fail-closed **profile defect**: `wrap`'s inline profile was a stale duplicate that omitted `(allow file-read* (literal "/"))`. A dynamically-linked binary like `node` stats the filesystem root `/` during `dyld` init; the profile denied it, Seatbelt correctly aborted the process, and the guard converted that fail-closed signal into a silent fail-open passthrough. The correct profile already lives in `build_seatbelt_prefix` (it carries `(literal "/")` + `(allow mach*)`, added for Node.js in #534) and runs on macOS 15+ via the MCP path. ## Changes - Remove the `darwin_major_version() >= 24` guard and the stale duplicate inline profile in `wrap`. - Route `wrap`'s macOS arm through `build_seatbelt_prefix` — a single Seatbelt profile now backs **both** the `wrap` and MCP spawn paths. - Drop the now-dead `darwin_major_version` helper. - Fail-closed by construction: if `sandbox-exec` genuinely can't run, the spawn errors (`ENOENT`) and the subprocess never launches — never a silent unsandboxed launch. (Matches the `SandboxPolicy::Required` posture from #655.) - `wrap` no longer has a panic path (the builder returns `io::Result`); its `# Panics` doc is removed. ## Test Plan ### Automated - [x] `cargo test -p astrid-workspace` green (56 passed), including: - `seatbelt_root_read_is_load_bearing_for_real_binary` — spawns a **real `node`** under the generated profile and asserts it runs (T1), then strips `(literal "/")` and asserts the same profile **fails closed** (T3). Skips cleanly when `node` or a usable `sandbox-exec` is absent (e.g. nested-sandbox CI). - `wrap_always_sandboxes_via_shared_profile` — `wrap` now always returns a `sandbox-exec`-wrapped command carrying the root-read rule (never a passthrough). - [x] No new clippy warnings — `cargo clippy --workspace --all-features -- -D warnings` clean for the changed crate; `astrid-capsule` (the consumer) compiles. ### Manual - [x] Verified on **Darwin 25.2**: T1 exits 0, T3 fails closed — matches the issue's repro table. ## External verification The deprecation claim and SBPL mechanism were independently verified against documentation: - `sandbox-exec` is **deprecated but still enforcing** on macOS 15 / macOS 26 (Apple DTS confirmation that it remains functional with no supported replacement for non-App-Store process sandboxing; third-party reports debugging live Seatbelt denials on Darwin 25 in Dec 2025; Chrome/Firefox/Homebrew/SwiftPM still depend on it). - `(literal "/")` grants only a `stat` on the **root inode**, not the recursive access `(subpath "/")` would — so the fix is minimal-aperture, not a widening. - App Sandbox / entitlements require a signed `.app` bundle and **cannot** wrap arbitrary spawned binaries like `node`, so `sandbox-exec` is the only mechanism for this use case. ## Notes / follow-ups - `(allow mach*)` in the shared profile is a deliberately broad wildcard — pragmatic for running general-purpose binaries (e.g. `node`'s `os.cpus()` needs `mach-host*`), a conscious trade-off. - If Apple ever removes `sandbox-exec`, the correct response is a real replacement, **not** another silent-fallback version guard (fail-secure). - Kernel-internal sandbox wiring: no WIT/IPC/capability-contract change, so no RFC. - Security fix that should land in **0.8.0** — once merged I'll rebase the release PR (#854) so this entry folds into `[0.8.0]`. ## Checklist - [x] Linked to an issue - [x] CHANGELOG.md updated under `[Unreleased]`
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.
Linked Issue
Closes #602
Summary
Six fixes discovered through end-to-end smoke testing.
Changes
fs.rs: addcwd://scheme toresolve_path()— was handled in security gate but not runtime path resolutionsandbox.rs: skipsandbox-exec(Seatbelt) on macOS 15+ (Darwin >= 24) — crashes with SIGABRTmain.rs: add-y/--yes/--yolo/--autonomousflag for headless auto-approval; fix approval response topic to includerequest_id; fix decision string (approvenotallow)main.rs: add--session <name>flag for multi-turn headless conversations (deterministic UUID v5 from name); add--print-sessionflagdiscovery.rs: merge[[component]].capabilitiesinto root capabilities so security gate seesfs_read/fs_write/host_processdeclared at the component levelmod.rs+install.rs: addhome_roottoLifecycleConfig, mounthome://VFS during lifecycle hooks soon_installcan write to principal homelifecycle_e2e.rs: addhome_root: Noneto test configsTest Plan
Automated
cargo test --workspacepassesManual
astrid -y -p "run: echo hello"— tool execution works end-to-endastrid -y --session test -p "my name is Josh"then--session test -p "what's my name?"— context persistsastrid -y -p "list all tool names"— 20 tools discoveredastrid -y -p "call list_capsules"— 17 capsules reportedChecklist
[Unreleased]