Class: LittleGhost::Sandbox

Inherits:
Object
  • Object
show all
Defined in:
lib/little_ghost/sandbox.rb,
lib/little_ghost/sandbox/mount.rb,
lib/little_ghost/sandbox/scope.rb,
lib/little_ghost/sandbox/limits.rb,
lib/little_ghost/sandbox/policy.rb,
lib/little_ghost/sandbox/filesystem.rb,
lib/little_ghost/sandbox/capabilities.rb,
lib/little_ghost/sandbox/network_policy.rb,
lib/little_ghost/sandbox/process_runner.rb,
lib/little_ghost/sandbox/process_session.rb,
lib/little_ghost/sandbox/isolated_backend.rb,
lib/little_ghost/sandbox/environment_policy.rb

Overview

A Sandbox governs filesystem operations and child processes that explicitly pass through it. Built-in filesystem and shell Tools use their bound Sandbox. A custom Ruby Tool remains trusted application code unless it delegates work to that Sandbox or one of its Scopes.

LittleGhost.configure do |config|
config.sandbox = {
  provider: :native,
  files: {root: :read_write, source: :read_only},
  runtime_paths: {home: :read_write},
  network: :none
}
end

A backend reports the policy and capabilities it actually enforces. File operations stay within declared Workspace paths. Process operations honor cancellation and configured limits, then return an Execution.

A backend's isolation mechanism still relies on its outer host, kernel or VM, dependencies, trusted configuration, and deliberately exposed paths. Sandbox policy does not apply to provider requests or arbitrary Ruby code in the application process.

See the Workspaces and Sandboxes guide for the path model, built-in backends, Scopes, process ownership, and networking boundaries.

Defined Under Namespace

Classes: Capabilities, EnvironmentPolicy, Execution, Filesystem, IsolatedBackend, Limits, Mount, NetworkPolicy, Policy, ProcessRunner, ProcessSession, Scope

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(workspace:, policy: nil, profiles: {}, limits: {}) ⇒ Sandbox

Binds the sandbox to workspace.



119
120
121
122
123
124
# File 'lib/little_ghost/sandbox.rb', line 119

def initialize(workspace:, policy: nil, profiles: {}, limits: {})
  @workspace = workspace
  @policy = Policy.coerce(policy)
  @limits = Limits.coerce(limits)
  configure_profiles!(profiles)
end

Instance Attribute Details

#limitsObject (readonly)

File and process output bounds enforced by this Sandbox.



132
133
134
# File 'lib/little_ghost/sandbox.rb', line 132

def limits
  @limits
end

#policyObject (readonly)

Normalized policy requested by trusted application configuration.



130
131
132
# File 'lib/little_ghost/sandbox.rb', line 130

def policy
  @policy
end

#workspaceObject (readonly)

Workspace whose files and processes this sandbox governs.



127
128
129
# File 'lib/little_ghost/sandbox.rb', line 127

def workspace
  @workspace
end

Class Method Details

.probe(name, **options) ⇒ Object

Reports whether a registered backend can start in the current environment without creating a Run-owned sandbox.



53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
# File 'lib/little_ghost/sandbox.rb', line 53

def probe(name, **options)
  implementation = resolve_provider(name)
  provider_probe = implementation.method(:probe)
  return provider_probe.call(**options) unless provider_probe.owner == Sandbox.singleton_class
  unless options.empty?
    raise ArgumentError, "sandbox provider :#{name} does not accept probe options"
  end

  {
    available: true,
    reason: nil,
    capabilities: Capabilities.new(features: [], network_modes: [])
  }
rescue DependencyError => error
  {available: false, reason: error.message, capabilities: Capabilities.new(features: [], network_modes: [])}
end

.providersObject

:nodoc:



70
71
72
# File 'lib/little_ghost/sandbox.rb', line 70

def providers # :nodoc:
  @providers ||= {}
end

.register_provider(name, implementation) ⇒ Object

Registers a trusted backend class under a configuration symbol.



36
37
38
39
40
41
42
# File 'lib/little_ghost/sandbox.rb', line 36

def register_provider(name, implementation)
  unless implementation.is_a?(Class) && implementation <= Sandbox
    raise ArgumentError, "sandbox provider must be a Sandbox class"
  end

  Sandbox.providers[name.to_sym] = implementation
end

.resolve_provider(name) ⇒ Object

Resolves a registered backend without silently falling back.



45
46
47
48
49
# File 'lib/little_ghost/sandbox.rb', line 45

def resolve_provider(name)
  Sandbox.providers.fetch(name.to_sym) do
    raise DependencyError, "sandbox provider :#{name} is not available"
  end
end

Instance Method Details

#allows?(operation, path = nil) ⇒ Boolean

Indicates whether an operation is allowed by this sandbox and optional virtual path.

Returns:

  • (Boolean)


149
150
151
152
153
# File 'lib/little_ghost/sandbox.rb', line 149

def allows?(operation, path = nil)
  return supports?(operation) unless path

  scope.allows?(operation, path)
end

#capabilitiesObject

Operations and network modes implemented by this backend.



140
141
142
# File 'lib/little_ghost/sandbox.rb', line 140

def capabilities
  Capabilities.new(features: [], network_modes: [])
end

#closeObject

Releases sandbox resources. Runs close the sandbox before its workspace.



241
242
243
# File 'lib/little_ghost/sandbox.rb', line 241

def close
  nil
end

#effective_policyObject

Policy the backend enforces. Backends may fill a documented default or report an unavoidable effective value, but reject unsupported requested rules instead of silently claiming enforcement.



137
# File 'lib/little_ghost/sandbox.rb', line 137

def effective_policy = policy

#execute(command, timeout:, context: nil, max_output_bytes: nil, **options) ⇒ Object

Executes command through /bin/sh.

Prefer #execute_program for model-controlled arguments so shell syntax is not interpreted.



215
216
217
218
219
220
221
222
223
# File 'lib/little_ghost/sandbox.rb', line 215

def execute(command, timeout:, context: nil, max_output_bytes: nil, **options)
  execute_program(
    ["/bin/sh", "-c", String(command)],
    timeout:,
    context:,
    max_output_bytes:,
    **options
  )
end

#execute_program(command, timeout:, context: nil, max_output_bytes: nil, environment: {}, inherit_environment: false, **options) ⇒ Object

Executes an argument vector without shell interpretation.

Implementations must enforce timeout and max_output_bytes. Environment inheritance is disabled by default to avoid leaking process credentials; both policy and the individual call must opt in before a backend may inherit.



230
231
232
# File 'lib/little_ghost/sandbox.rb', line 230

def execute_program(command, timeout:, context: nil, max_output_bytes: nil, environment: {}, inherit_environment: false, **options)
  raise AbstractMethodError, "#{self.class} does not support program execution"
end

#list(path = ".", context: nil) ⇒ Object

Lists entries at a workspace-relative or absolute virtual directory path.



196
197
198
# File 'lib/little_ghost/sandbox.rb', line 196

def list(path = ".", context: nil)
  raise AbstractMethodError, "#{self.class} does not support filesystem listings"
end

#open(run: nil) ⇒ Object

Opens any run-scoped resources and makes the sandbox ready for tools.



183
184
185
# File 'lib/little_ghost/sandbox.rb', line 183

def open(run: nil)
  self
end

#read(path, context: nil) ⇒ Object

Reads UTF-8 text at a workspace-relative or absolute virtual path.



191
192
193
# File 'lib/little_ghost/sandbox.rb', line 191

def read(path, context: nil)
  raise AbstractMethodError, "#{self.class} does not support filesystem reads"
end

#replace(path, old_text, new_text, context: nil) ⇒ Object

Replaces one exact old_text occurrence at a workspace-relative or absolute virtual path with new_text.



207
208
209
# File 'lib/little_ghost/sandbox.rb', line 207

def replace(path, old_text, new_text, context: nil)
  raise AbstractMethodError, "#{self.class} does not support filesystem edits"
end

#scope(profile = nil, files: nil, runtime_paths: nil, capabilities: nil, network: nil) ⇒ Object

Produces a non-owning capability-reduced view for tools or child agents. The caller must pass and use that Scope; retaining this parent Sandbox retains its broader authority.



158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
# File 'lib/little_ghost/sandbox.rb', line 158

def scope(profile = nil, files: nil, runtime_paths: nil, capabilities: nil, network: nil)
  if profile
    if !files.nil? || !runtime_paths.nil? || !capabilities.nil? || !network.nil?
      raise ArgumentError, "scope profile cannot be combined with explicit options"
    end

    declaration = @profiles.fetch(profile.to_sym) do
      raise PolicyError, "unknown sandbox scope profile: #{profile.inspect}"
    end
    declaration = declaration.call(workspace:, policy: effective_policy) if declaration.respond_to?(:call)
    unless declaration.respond_to?(:transform_keys)
      raise PolicyError, "sandbox scope profile must be a Hash"
    end
    values = declaration.transform_keys(&:to_sym)
    unknown = values.keys - %i[files runtime_paths capabilities network]
    raise PolicyError, "unknown sandbox scope profile options: #{unknown.join(", ")}" unless unknown.empty?
    files = values[:files]
    runtime_paths = values[:runtime_paths]
    capabilities = values[:capabilities]
    network = values[:network]
  end
  Scope.new(sandbox: self, files:, runtime_paths:, capabilities:, network:)
end

#start_program(command, context: nil, environment: {}, inherit_environment: false, **options) ⇒ Object

Starts an owned, duplex child process for framed protocols and other interactive programs. The returned session owns the child process group.



236
237
238
# File 'lib/little_ghost/sandbox.rb', line 236

def start_program(command, context: nil, environment: {}, inherit_environment: false, **options)
  raise AbstractMethodError, "#{self.class} does not support program sessions"
end

#supports?(feature, value = nil) ⇒ Boolean

Indicates whether the backend implements feature.

Returns:

  • (Boolean)


145
# File 'lib/little_ghost/sandbox.rb', line 145

def supports?(feature, value = nil) = capabilities.supports?(feature, value)

#writable?Boolean

Indicates whether filesystem mutation is allowed.

Returns:

  • (Boolean)


188
# File 'lib/little_ghost/sandbox.rb', line 188

def writable? = supports?(:filesystem_write)

#write(path, content, context: nil) ⇒ Object

Writes content to a workspace-relative or absolute virtual path.



201
202
203
# File 'lib/little_ghost/sandbox.rb', line 201

def write(path, content, context: nil)
  raise AbstractMethodError, "#{self.class} does not support filesystem writes"
end