Class: LittleGhost::Sandboxes::Seatbelt
- Inherits:
-
LittleGhost::Sandbox::IsolatedBackend
- Object
- Sandbox
- LittleGhost::Sandbox::IsolatedBackend
- LittleGhost::Sandboxes::Seatbelt
- Defined in:
- lib/little_ghost/sandboxes/seatbelt.rb
Overview
Runs child programs under macOS Seatbelt. Seatbelt grants access to the workspace's existing physical paths; it does not create Linux-style bind mounts or virtual path aliases. Child processes inherit the profile, but macOS cannot provide PID-namespace ownership for detached descendants.
Constant Summary collapse
- DEFAULT_EXECUTABLE =
:nodoc:
"/usr/bin/sandbox-exec"
Instance Attribute Summary
Attributes inherited from LittleGhost::Sandbox::IsolatedBackend
Class Method Summary collapse
-
.backend_capabilities ⇒ Object
Capabilities the Seatbelt backend can enforce before a Policy narrows them.
-
.probe(executable: DEFAULT_EXECUTABLE, platform: RUBY_PLATFORM) ⇒ Object
Reports whether Seatbelt is available and returns its capabilities.
Instance Method Summary collapse
-
#capabilities ⇒ Object
Effective capabilities after the configured root-filesystem policy is applied.
-
#close ⇒ Object
Removes temporary storage owned by this backend.
-
#execute_program(command, timeout:, context: nil, max_output_bytes: nil, environment: {}, inherit_environment: false, scope: nil, cwd: nil) ⇒ Object
Runs
commandto completion under Seatbelt and returns its bounded stdout, stderr, and exit status. -
#initialize(workspace:, policy: nil, profiles: {}, limits: {}, executable: DEFAULT_EXECUTABLE, platform: RUBY_PLATFORM) ⇒ Seatbelt
constructor
Builds a Seatbelt backend around
workspacewithout opening it. -
#open(run: nil) ⇒ Object
Validates Seatbelt and the Workspace, then creates owned temporary storage.
-
#start_program(command, context: nil, environment: {}, inherit_environment: false, scope: nil, cwd: nil, output_bytes: nil, memory_bytes: nil, cpu_seconds: nil, file_bytes: nil, allow_subprocesses: false) ⇒ Object
Starts
commandunder Seatbelt and returns an owned ProcessSession.
Methods inherited from LittleGhost::Sandbox::IsolatedBackend
#list, #read, #replace, #writable?, #write
Constructor Details
#initialize(workspace:, policy: nil, profiles: {}, limits: {}, executable: DEFAULT_EXECUTABLE, platform: RUBY_PLATFORM) ⇒ Seatbelt
Builds a Seatbelt backend around workspace without opening it.
40 41 42 43 44 45 46 |
# File 'lib/little_ghost/sandboxes/seatbelt.rb', line 40 def initialize(workspace:, policy: nil, profiles: {}, limits: {}, executable: DEFAULT_EXECUTABLE, platform: RUBY_PLATFORM) super(workspace:, policy: policy || {}, profiles:, limits:) @executable = File.(executable) @platform = platform @opened = false end |
Class Method Details
.backend_capabilities ⇒ Object
Capabilities the Seatbelt backend can enforce before a Policy narrows them.
28 29 30 31 32 33 34 35 36 37 |
# File 'lib/little_ghost/sandboxes/seatbelt.rb', line 28 def self.backend_capabilities Capabilities.new( features: %i[ filesystem_read filesystem_list filesystem_write filesystem_replace process_execute process_spawn process_spawn_denial ], network_modes: %i[inherit none], isolation: :seatbelt ) end |
.probe(executable: DEFAULT_EXECUTABLE, platform: RUBY_PLATFORM) ⇒ Object
Reports whether Seatbelt is available and returns its capabilities.
17 18 19 20 21 22 23 24 |
# File 'lib/little_ghost/sandboxes/seatbelt.rb', line 17 def self.probe(executable: DEFAULT_EXECUTABLE, platform: RUBY_PLATFORM) available = platform.include?("darwin") && File.executable?(executable) { available:, reason: available ? nil : "Seatbelt sandboxing requires macOS and #{executable}", capabilities: available ? backend_capabilities : Capabilities.new(features: [], network_modes: []) } end |
Instance Method Details
#capabilities ⇒ Object
Effective capabilities after the configured root-filesystem policy is applied.
50 51 52 53 54 55 56 57 58 59 |
# File 'lib/little_ghost/sandboxes/seatbelt.rb', line 50 def capabilities supported = self.class.backend_capabilities return supported unless effective_policy.root_filesystem == :isolated Capabilities.new( features: supported.features - [:process_spawn], network_modes: supported.network_modes, isolation: supported.isolation ) end |
#close ⇒ Object
Removes temporary storage owned by this backend. Safe to call more than once.
82 83 84 85 86 87 |
# File 'lib/little_ghost/sandboxes/seatbelt.rb', line 82 def close FileUtils.remove_entry(@temporary_directory) if @temporary_directory && File.exist?(@temporary_directory) @temporary_directory = nil @opened = false nil end |
#execute_program(command, timeout:, context: nil, max_output_bytes: nil, environment: {}, inherit_environment: false, scope: nil, cwd: nil) ⇒ Object
Runs command to completion under Seatbelt and returns its bounded
stdout, stderr, and exit status.
91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 |
# File 'lib/little_ghost/sandboxes/seatbelt.rb', line 91 def execute_program(command, timeout:, context: nil, max_output_bytes: nil, environment: {}, inherit_environment: false, scope: nil, cwd: nil) selected_scope = scope || self.scope session = start_program( command, context:, environment:, inherit_environment:, scope: selected_scope, cwd:, output_bytes: max_output_bytes, allow_subprocesses: selected_scope.supports?(:process_spawn) ) session.close_write stdout = +"" stderr = +"" deadline = monotonic_time + Float(timeout) while session.alive? context&.check! remaining = deadline - monotonic_time raise ToolError, "Command timed out after #{timeout} seconds" unless remaining.positive? chunk = session.read(timeout: [remaining, 0.05].min) stdout << chunk.stdout stderr << chunk.stderr end chunk = session.read(timeout: 0) stdout << chunk.stdout stderr << chunk.stderr status = session.wait Sandbox::Execution.new(stdout:, stderr:, exit_code: status&.exitstatus) ensure session&.close end |
#open(run: nil) ⇒ Object
Validates Seatbelt and the Workspace, then creates owned temporary
storage. Returns self.
63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 |
# File 'lib/little_ghost/sandboxes/seatbelt.rb', line 63 def open(run: nil) return self if @opened raise UnsupportedPlatformError, "Seatbelt sandboxing is supported only on macOS" unless @platform.include?("darwin") raise DependencyError, "Seatbelt sandboxing requires #{@executable}" unless File.executable?(@executable) if effective_policy.network&.allowlist? raise CapabilityError, "Seatbelt does not implement allowlisted network egress" end workspace.validate! @temporary_directory = Dir.mktmpdir("little-ghost-seatbelt-") @opened = true self rescue close raise end |
#start_program(command, context: nil, environment: {}, inherit_environment: false, scope: nil, cwd: nil, output_bytes: nil, memory_bytes: nil, cpu_seconds: nil, file_bytes: nil, allow_subprocesses: false) ⇒ Object
Starts command under Seatbelt and returns an owned ProcessSession.
The caller must close the returned session.
128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 |
# File 'lib/little_ghost/sandboxes/seatbelt.rb', line 128 def start_program(command, context: nil, environment: {}, inherit_environment: false, scope: nil, cwd: nil, output_bytes: nil, memory_bytes: nil, cpu_seconds: nil, file_bytes: nil, allow_subprocesses: false) open unless @opened selected_scope = scope || self.scope selected_scope.validate! if allow_subprocesses && !selected_scope.supports?(:process_spawn) raise CapabilityError, "sandbox scope does not allow subprocess creation" end configured_environment = workspace.environment .merge(effective_policy.environment.to_h) .merge(environment.transform_keys(&:to_s).transform_values(&:to_s)) .merge("TMPDIR" => @temporary_directory) profile = seatbelt_profile(selected_scope, allow_subprocesses:) Sandbox::ProcessSession.new( command: [@executable, "-p", profile, "--", *Array(command).map(&:to_s)], environment: configured_environment, inherit_environment: effective_policy.environment.inherit? && inherit_environment, chdir: cwd ? workspace.resolve(cwd) : workspace.root, output_bytes: output_bytes || limits.output_bytes, memory_bytes:, cpu_seconds:, file_bytes: ) end |