Skip to content

fix: cwd:// VFS, macOS 15 sandbox, --yes approval, --session, capability merge, lifecycle home_root - #603

Merged
joshuajbouw merged 3 commits into
mainfrom
fix/cwd-vfs-runtime-resolution
Mar 24, 2026
Merged

fix: cwd:// VFS, macOS 15 sandbox, --yes approval, --session, capability merge, lifecycle home_root#603
joshuajbouw merged 3 commits into
mainfrom
fix/cwd-vfs-runtime-resolution

Conversation

@joshuajbouw

@joshuajbouw joshuajbouw commented Mar 23, 2026

Copy link
Copy Markdown
Member

Linked Issue

Closes #602

Summary

Six fixes discovered through end-to-end smoke testing.

Changes

  • fs.rs: add cwd:// scheme to resolve_path() — was handled in security gate but not runtime path resolution
  • sandbox.rs: skip sandbox-exec (Seatbelt) on macOS 15+ (Darwin >= 24) — crashes with SIGABRT
  • main.rs: add -y/--yes/--yolo/--autonomous flag for headless auto-approval; fix approval response topic to include request_id; fix decision string (approve not allow)
  • main.rs: add --session <name> flag for multi-turn headless conversations (deterministic UUID v5 from name); add --print-session flag
  • discovery.rs: merge [[component]].capabilities into root capabilities so security gate sees fs_read/fs_write/host_process declared at the component level
  • mod.rs + install.rs: add home_root to LifecycleConfig, mount home:// VFS during lifecycle hooks so on_install can write to principal home
  • lifecycle_e2e.rs: add home_root: None to test configs

Test Plan

Automated

  • cargo test --workspace passes
  • No new clippy warnings

Manual

  • astrid -y -p "run: echo hello" — tool execution works end-to-end
  • astrid -y --session test -p "my name is Josh" then --session test -p "what's my name?" — context persists
  • astrid -y -p "list all tool names" — 20 tools discovered
  • astrid -y -p "call list_capsules" — 17 capsules reported

Checklist

  • Linked to an issue
  • CHANGELOG.md updated under [Unreleased]

@gemini-code-assist

Copy link
Copy Markdown
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
joshuajbouw force-pushed the fix/cwd-vfs-runtime-resolution branch from b1c9c44 to 0412259 Compare March 23, 2026 22:57
@joshuajbouw joshuajbouw changed the title fix(fs): add cwd:// scheme to runtime VFS path resolver fix: cwd:// VFS resolver, macOS 15 sandbox, --yes headless approval Mar 23, 2026
- 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 joshuajbouw changed the title fix: cwd:// VFS resolver, macOS 15 sandbox, --yes headless approval fix: cwd:// VFS, macOS 15 sandbox, --yes approval, --session, capability merge, lifecycle home_root Mar 24, 2026
@joshuajbouw
joshuajbouw force-pushed the fix/cwd-vfs-runtime-resolution branch from 6693eae to b5fbef1 Compare March 24, 2026 01:28
…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
joshuajbouw force-pushed the fix/cwd-vfs-runtime-resolution branch from b5fbef1 to a7d624e Compare March 24, 2026 01:32
@joshuajbouw
joshuajbouw merged commit c321868 into main Mar 24, 2026
13 checks passed
@joshuajbouw
joshuajbouw deleted the fix/cwd-vfs-runtime-resolution branch March 24, 2026 01:44
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]`
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.

fix: cwd:// VFS scheme not handled in runtime path resolver

1 participant