Skip to content

feat(sandbox): implement macOS SandboxExecutor using Seatbelt/SBPL#67

Merged
fohte merged 20 commits intomainfrom
fohte/impl-runok-init-macos-sandbox
Feb 23, 2026
Merged

feat(sandbox): implement macOS SandboxExecutor using Seatbelt/SBPL#67
fohte merged 20 commits intomainfrom
fohte/impl-runok-init-macos-sandbox

Conversation

@fohte
Copy link
Owner

@fohte fohte commented Feb 22, 2026

Why

  • OS-level sandbox execution requires a macOS implementation using Seatbelt (sandbox-exec)

What

  • Dynamically generate SBPL profiles from SandboxPolicy and execute commands under sandbox-exec with policy-based sandboxing
  • Add is_supported() method to the SandboxExecutor trait for runtime platform detection
  • Resolve sandbox preset names to actual policies in ExecAdapter and pass them to the executor

Open with Devin

The sandbox execution layer had only a stub implementation that
returned "not yet implemented" errors. This prevented sandbox-enforced
command execution on macOS.

Add `MacOsSandboxExecutor` that generates SBPL profiles dynamically
from `SandboxPolicy` and executes commands under `/usr/bin/sandbox-exec`.

- SBPL generation: deny all file-write by default, selectively allow
  writable_roots, deny read_only_subpaths (last-match-wins ordering),
  control network access, always allow /dev/null writes
- `is_supported()` method added to `SandboxExecutor` trait to check
  for `/usr/bin/sandbox-exec` availability at runtime
- `ExecAdapter` now resolves sandbox preset names to `SandboxPolicy`
  and passes them to the executor instead of ignoring them
- `main.rs` uses `MacOsSandboxExecutor` when available, falls back to
  `StubSandboxExecutor` on other platforms

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@gemini-code-assist
Copy link

Summary of Changes

Hello @fohte, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces robust OS-level sandboxing for macOS by integrating with the native sandbox-exec utility. It allows for dynamic generation of sandboxing policies based on user-defined presets, enhancing security and control over command execution. The changes include a new macOS-specific executor, updates to the core sandboxing trait, and modifications to the command execution adapter to intelligently apply these policies.

Highlights

  • macOS Sandbox Implementation: Implemented a new MacOsSandboxExecutor that leverages macOS's sandbox-exec (Seatbelt/SBPL) to provide OS-level sandboxing capabilities. This executor dynamically generates SBPL profiles based on the defined SandboxPolicy.
  • Dynamic SBPL Profile Generation: The MacOsSandboxExecutor includes logic to convert a generic SandboxPolicy into a specific Seatbelt Policy Language (SBPL) string, controlling file write access (writable roots, read-only subpaths) and network access.
  • SandboxExecutor Trait Enhancement: The SandboxExecutor trait was extended with an is_supported() method, allowing runtime detection of whether a specific sandbox mechanism is available on the current system.
  • Sandbox Policy Resolution in ExecAdapter: The ExecAdapter now includes functionality to resolve sandbox preset names from the configuration into concrete SandboxPolicy objects, which are then passed to the appropriate SandboxExecutor.
  • Conditional Sandbox Activation: The main application logic (main.rs) was updated to conditionally use the MacOsSandboxExecutor if it is supported on the current platform, otherwise falling back to the default non-sandboxed executor.
Changelog
  • src/adapter/exec_adapter.rs
    • Added HashMap import for sandbox definitions.
    • Updated imports to include MergedSandboxPolicy, SandboxPreset, and SandboxPolicy.
    • Added sandbox_definitions field to ExecAdapter struct.
    • Implemented with_sandbox_definitions method to set sandbox presets.
    • Implemented resolve_sandbox_policy method to convert preset names to SandboxPolicy.
    • Implemented build_sandbox_policy method to create SandboxPolicy from MergedSandboxPolicy.
    • Modified Endpoint::handle_action to resolve and pass the appropriate SandboxPolicy to the executor.
  • src/exec/command_executor.rs
    • Added is_supported method to the SandboxExecutor trait.
    • Implemented is_supported for StubSandboxExecutor to return false.
    • Implemented is_supported for MockCommandExecutor (in tests) to return true.
  • src/exec/macos_sandbox.rs
    • Introduced MacOsSandboxExecutor struct for macOS-specific sandboxing.
    • Implemented build_command to construct sandbox-exec commands.
    • Implemented generate_sbpl to dynamically create Seatbelt Policy Language (SBPL) profiles from SandboxPolicy.
    • Added sbpl_escape_string helper for proper SBPL string formatting.
    • Implemented SandboxExecutor trait for MacOsSandboxExecutor, including exec_sandboxed and is_supported.
    • Added exit_code_from_status helper for consistent exit code handling.
    • Included comprehensive unit and integration tests for SBPL generation, command building, and sandbox execution behavior on macOS.
  • src/exec/mod.rs
    • Exported the new macos_sandbox module.
  • src/main.rs
    • Imported SandboxExecutor trait and MacOsSandboxExecutor struct.
    • Modified the run_command function to conditionally initialize ProcessCommandExecutor with MacOsSandboxExecutor if macOS sandboxing is supported.
    • Retrieved sandbox definitions from the configuration and passed them to ExecAdapter.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

devin-ai-integration[bot]

This comment was marked as resolved.

gemini-code-assist[bot]

This comment was marked as resolved.

- Replace unreachable `None` branch in `resolve_sandbox_policy` with
  `let-else` + `unreachable!` since `merge_strictest` with a non-empty
  slice always returns `Some`
- Deduplicate `exit_code_from_status` by making the version in
  `command_executor.rs` `pub(crate)` and importing it in
  `macos_sandbox.rs`
- Consolidate five separate SBPL generation tests into a single
  parameterized `#[rstest]` test with named `#[case]` variants
- Extract `macos_executor` and `default_policy` fixtures for macOS
  integration tests to reduce setup duplication
@codecov
Copy link

codecov bot commented Feb 22, 2026

Codecov Report

❌ Patch coverage is 83.33333% with 39 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.39%. Comparing base (545ae31) to head (12b6d50).
⚠️ Report is 4 commits behind head on main.

Files with missing lines Patch % Lines
src/exec/macos_sandbox/mod.rs 81.81% 18 Missing ⚠️
src/adapter/exec_adapter.rs 67.74% 10 Missing ⚠️
src/exec/command_executor.rs 10.00% 9 Missing ⚠️
src/exec/macos_sandbox/glob_pattern.rs 97.72% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #67      +/-   ##
==========================================
- Coverage   87.59%   87.39%   -0.20%     
==========================================
  Files          27       29       +2     
  Lines        5344     5571     +227     
==========================================
+ Hits         4681     4869     +188     
- Misses        663      702      +39     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

devin-ai-integration[bot]

This comment was marked as resolved.

SBPL's `(subpath ...)` filter requires absolute paths and does not
support glob patterns. When `read_only_subpaths` contained relative
paths (e.g., `.git`) or globs (e.g., `.env*`, `/etc/**`), the deny
rules were silently ineffective.

Classify each deny path and use the appropriate SBPL filter:
- Absolute literal paths use `(subpath ...)` as before
- Relative literal paths are resolved against each `writable_root`
- Glob patterns are converted to `(regex ...)` filters via
  `glob_to_sbpl_regex`
Hand-written glob-to-regex conversion is fragile and prone to edge-case
bugs. The globset crate (by BurntSushi, 139M+ downloads) provides
battle-tested glob-to-regex conversion with correct handling of `*`,
`**`, and metacharacter escaping.

Use `GlobBuilder::literal_separator(true)` so `*` does not match `/`,
matching the expected filesystem glob semantics. Remove the custom
`glob_to_sbpl_regex` and `is_regex_metachar` functions entirely.
On Linux CI, the `fixture` import was unused because all fixtures are
gated behind `#[cfg(target_os = "macos")]`. This caused a build failure
due to `-D unused-imports`.
When the CLI `--sandbox` argument specified a preset name not found in
`definitions.sandbox`, `resolve_sandbox_policy` returned `Ok(None)`,
causing the command to execute without any sandbox silently. This is a
security issue since users expect sandboxing to be applied.

Return an error instead so the user gets clear feedback that the
preset name is invalid.
devin-ai-integration[bot]

This comment was marked as resolved.

fohte and others added 13 commits February 22, 2026 15:13
…[ globs

globset emits Rust-specific (?-u) (disable Unicode) flag prefix in its
regex output, but SBPL's regex engine uses POSIX ERE and rejects this
syntax with "unexpected ^ operator" (exit code 65), preventing the
sandboxed process from starting at all.

Also extend classify_deny_path to detect ? and [ as glob metacharacters,
not just *, so patterns like file?.txt and log[0-9].txt are correctly
routed to regex-based SBPL filters.
…in regex conversion

`classify_deny_path` only checked for `*`, causing `?` and `[...]`
patterns to be misclassified as literal paths. `glob_to_sbpl_regex`
also lacked support for these glob characters.

Add `?` → `[^/]` and `[...]` passthrough to the POSIX ERE conversion,
and update the classifier to detect all three glob metacharacters.
…-macos-sandbox

# Conflicts:
#	src/exec/command_executor.rs
#	src/main.rs
…r srt

The glob-to-regex converter did not handle `{a,b}` brace expansion,
causing patterns like `*.{js,ts}` to be treated as literal text.
Also, `classify_deny_path` did not recognize `{` as a glob character.

The macOS integration tests call `sandbox-exec`, which cannot nest
inside another sandbox (srt sets `SANDBOX_RUNTIME=1`). Skip these
tests when running under srt to avoid spurious failures.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
…e import

The merge introduced the `is_supported` method on `SandboxExecutor`
trait but the `LinuxSandboxExecutor` impl was missing it, causing
compilation failure on Linux CI. Also conditionally import `fixture`
for macOS-only tests to avoid unused import warning on Linux.
`SandboxExecutor` and `MacOsSandboxExecutor` are only used in the
`#[cfg(target_os = "macos")]` block of `create_executor`, so they
must be conditionally imported to avoid unused import errors on Linux.
`macos_sandbox.rs` was growing large with mixed concerns: SBPL
generation, sandbox execution, and glob-to-regex conversion.

Move `DenyPathKind`, `classify_deny_path`, `glob_to_sbpl_regex`, and
`is_regex_metachar` along with their tests into `glob_pattern.rs` to
keep each module focused on a single responsibility.
`glob_pattern` is specific to macOS SBPL regex generation, not a
general-purpose utility. Move it from `exec/glob_pattern.rs` to
`exec/macos_sandbox/glob_pattern.rs` by converting `macos_sandbox`
from a single file to a directory module. Narrow visibility from
`pub(crate)` to `pub(super)`.
The existing tests only covered basic patterns. Add cases for:
- brace expansion edge cases: empty alternative (`ts{x,}`), nested
  braces (`{a,{b,c}}`), dots inside alternatives (`*.{tar.gz,zip}`),
  globs mixed with braces (`{src,lib}/**/*.rs`)
- double star edge cases: bare `**`, prefix `**/*.log`, trailing
  `a/**`, multiple `a/**/b/**/c`
- character class: negation `[!a-z]`, consecutive `[abc][0-9]`
- metacharacter escaping: `.`, `+`, `()`, `|`
- misc: empty pattern, multiple `?`, combined `*` and `?`
String comparison alone cannot catch semantic bugs like `*` incorrectly
matching across directory separators. Add `should_match` / `should_not_match`
path lists to each existing test case so the generated regex is validated
against actual paths using the `regex` crate.

Add `regex` as a dev-dependency for match verification.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
devin-ai-integration[bot]

This comment was marked as resolved.

Glob syntax uses `!` for character class negation (e.g., `[!a-z]`)
while POSIX ERE uses `^` (e.g., `[^a-z]`). Without this conversion,
a deny pattern like `[!a-z]file` would generate `[!a-z]file` which
matches `!` or `a-z` — the opposite of the intended semantics.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@fohte fohte merged commit 32d7e96 into main Feb 23, 2026
5 checks passed
@fohte fohte deleted the fohte/impl-runok-init-macos-sandbox branch February 23, 2026 06:56
@fohte-bot fohte-bot bot mentioned this pull request Feb 22, 2026
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