Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
213dcbb
feat(sandbox): implement macOS SandboxExecutor using Seatbelt/SBPL
fohte Feb 22, 2026
670c12c
refactor(sandbox): address PR review feedback
fohte Feb 22, 2026
f900b9e
fix(sandbox): handle relative paths and glob patterns in SBPL deny rules
fohte Feb 22, 2026
0bb40d3
refactor(sandbox): replace hand-written glob-to-regex with globset crate
fohte Feb 22, 2026
94e33a2
fix(sandbox): conditionally import rstest fixture for macOS-only tests
fohte Feb 22, 2026
b58be41
fix(sandbox): error on undefined --sandbox preset name
fohte Feb 22, 2026
51d66fc
fix(sandbox): strip (?-u) prefix from globset regex and detect ? and …
fohte Feb 22, 2026
a7db71d
Revert "fix(sandbox): strip (?-u) prefix from globset regex and detec…
fohte Feb 22, 2026
90e803d
Revert "fix(sandbox): conditionally import rstest fixture for macOS-o…
fohte Feb 22, 2026
016b483
Revert "refactor(sandbox): replace hand-written glob-to-regex with gl…
fohte Feb 22, 2026
5152f4c
fix(sandbox): detect `?` and `[` as glob characters and support them …
fohte Feb 22, 2026
f812857
Merge remote-tracking branch 'origin/main' into fohte/impl-runok-init…
fohte Feb 22, 2026
82832f1
fix(sandbox): add brace expansion support and skip sandbox tests unde…
fohte Feb 22, 2026
a778448
fix(sandbox): add is_supported to LinuxSandboxExecutor and fix fixtur…
fohte Feb 22, 2026
fa966a6
fix(sandbox): conditionally import macOS-only symbols in main.rs
fohte Feb 22, 2026
a2a1561
refactor(sandbox): extract glob pattern logic into separate module
fohte Feb 23, 2026
76e6f47
refactor(sandbox): move glob_pattern under macos_sandbox module
fohte Feb 23, 2026
d95e0d6
test(sandbox): add edge case tests for glob-to-regex conversion
fohte Feb 23, 2026
0451a3e
test(sandbox): add regex match verification to glob-to-regex tests
fohte Feb 23, 2026
12b6d50
fix(sandbox): convert glob `[!...]` negation to regex `[^...]`
fohte Feb 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 49 additions & 14 deletions src/adapter/exec_adapter.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use std::collections::HashMap;

use crate::adapter::{ActionResult, Endpoint, SandboxInfo};
use crate::config::{ActionKind, Defaults};
use crate::exec::command_executor::{CommandExecutor, CommandInput};
use crate::config::{ActionKind, Defaults, MergedSandboxPolicy, SandboxPreset};
use crate::exec::command_executor::{CommandExecutor, CommandInput, SandboxPolicy};
use crate::rules::command_parser::shell_quote_join;
use crate::rules::rule_engine::Action;

Expand All @@ -12,6 +14,7 @@ use crate::rules::rule_engine::Action;
pub struct ExecAdapter {
args: Vec<String>,
sandbox_preset: Option<String>,
sandbox_definitions: HashMap<String, SandboxPreset>,
executor: Box<dyn CommandExecutor>,
}

Expand All @@ -24,17 +27,49 @@ impl ExecAdapter {
Self {
args,
sandbox_preset,
sandbox_definitions: HashMap::new(),
executor,
}
}

/// Set sandbox preset definitions for resolving preset names to policies.
pub fn with_sandbox_definitions(mut self, definitions: HashMap<String, SandboxPreset>) -> Self {
self.sandbox_definitions = definitions;
self
}

fn command_input(&self) -> CommandInput {
if self.args.len() == 1 {
CommandInput::Shell(self.args[0].clone())
} else {
CommandInput::Argv(self.args.clone())
}
}

/// Resolve a sandbox preset name to a `SandboxPolicy`.
fn resolve_sandbox_policy(
&self,
preset_name: &str,
) -> Result<Option<SandboxPolicy>, anyhow::Error> {
let preset = match self.sandbox_definitions.get(preset_name) {
Some(p) => p,
None => return Ok(None),
};

let merged = SandboxPreset::merge_strictest(&[preset]);
match merged {
Some(policy) => {
let sandbox_policy = SandboxPolicy::from_merged(&policy)?;
Ok(Some(sandbox_policy))
}
None => Ok(None),
}
}

/// Build a `SandboxPolicy` from a `MergedSandboxPolicy`.
fn build_sandbox_policy(merged: &MergedSandboxPolicy) -> Result<SandboxPolicy, anyhow::Error> {
Ok(SandboxPolicy::from_merged(merged)?)
}
}

impl Endpoint for ExecAdapter {
Expand All @@ -55,22 +90,22 @@ impl Endpoint for ExecAdapter {
Action::Allow => {
let command_input = self.command_input();

// Determine sandbox policy from the result or from the constructor preset
let sandbox = match result.sandbox {
SandboxInfo::Preset(ref preset) => {
// Use the rule's preset, falling back to the constructor preset
preset.as_ref().or(self.sandbox_preset.as_ref())
// Resolve sandbox policy from the result or constructor preset
let policy = match &result.sandbox {
SandboxInfo::Preset(preset) => {
let preset_name = preset.as_ref().or(self.sandbox_preset.as_ref());
match preset_name {
Some(name) => self.resolve_sandbox_policy(name)?,
None => None,
}
}
SandboxInfo::MergedPolicy(_) => {
// MergedPolicy is for compound commands; sandbox execution
// with merged policies is not yet implemented (Phase 2)
None
SandboxInfo::MergedPolicy(Some(merged)) => {
Some(Self::build_sandbox_policy(merged)?)
}
SandboxInfo::MergedPolicy(None) => None,
};

// Sandbox execution is Phase 2; for now, execute without sandbox
let _ = sandbox;
let exit_code = self.executor.exec(&command_input, None)?;
let exit_code = self.executor.exec(&command_input, policy.as_ref())?;
Ok(exit_code)
}
Action::Deny(deny_response) => {
Expand Down
14 changes: 11 additions & 3 deletions src/exec/command_executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,12 +184,12 @@ fn canonicalize_path(path: &str) -> Result<PathBuf, SandboxError> {
}

/// Trait for executing commands in a sandboxed environment.
///
/// Phase 2 will provide real implementations (macOS seatbelt, Linux landlock/seccomp).
/// For now, only a stub is provided.
pub trait SandboxExecutor {
/// Execute a command within a sandbox, returning the exit code.
fn exec_sandboxed(&self, command: &[String], policy: &SandboxPolicy) -> Result<i32, ExecError>;

/// Returns whether the sandbox mechanism is available on the current system.
fn is_supported(&self) -> bool;
}

/// Stub sandbox executor that returns an error indicating sandbox is not yet supported.
Expand All @@ -206,6 +206,10 @@ impl SandboxExecutor for StubSandboxExecutor {
"sandbox execution is not yet implemented",
)))
}

fn is_supported(&self) -> bool {
false
}
}

impl SandboxPolicy {
Expand Down Expand Up @@ -796,6 +800,10 @@ mod tests {
self.invocations.borrow_mut().push(command.to_vec());
Ok(self.exit_code)
}

fn is_supported(&self) -> bool {
true
}
}

#[test]
Expand Down
Loading
Loading