Class: LittleGhost::Sandboxes::Unrestricted

Inherits:
LittleGhost::Sandbox show all
Defined in:
lib/little_ghost/sandboxes/unrestricted.rb

Overview

A convenient host-backed sandbox for trusted local work. It offers bounded text-file operations and command execution using only Ruby's standard library.

workspace = LittleGhost::Workspace.new(root: Dir.pwd)
sandbox = LittleGhost::Sandboxes::Unrestricted.new(workspace:)
sandbox.read("README.md").lines.first # => "# LittleGhost\n"

Reads return valid UTF-8 text. Writes preserve the supplied String bytes. Paths may be relative to the workspace or absolute within a declared virtual mount. Traversal components are rejected, and every path is checked against its configured mount root.

Security and trust

This sandbox is not a security boundary. Commands run directly on the host with the Ruby process's permissions, and filesystem containment cannot defend against concurrent adversarial mutation. Use an isolated Sandbox implementation for untrusted work.

Instance Attribute Summary collapse

Attributes inherited from LittleGhost::Sandbox

#limits, #policy, #workspace

Instance Method Summary collapse

Methods inherited from LittleGhost::Sandbox

#allows?, #close, #execute, probe, providers, register_provider, resolve_provider, #scope, #supports?

Constructor Details

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

Configures a host sandbox with an explicit policy and resource limits.



29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# File 'lib/little_ghost/sandboxes/unrestricted.rb', line 29

def initialize(workspace:, policy: nil, profiles: {}, limits: {})
  configured_policy = policy || Sandbox::Policy.new(files: {root: :read_only}, root_filesystem: :read_write, network: :inherit)
  super(workspace:, policy: configured_policy, profiles:, limits:)
  policy = self.policy
  unless policy.network.nil? || policy.network.inherit?
    raise CapabilityError, "the unrestricted sandbox cannot enforce network mode :#{policy.network.mode}"
  end
  @effective_policy = Sandbox::Policy.new(
    files: policy.files,
    runtime_paths: policy.runtime_paths,
    root_filesystem: :read_write,
    environment: policy.environment,
    network: :inherit
  )
  @writable = effective_policy.process_grants(workspace).any?(&:writable?)
  @root = File.expand_path(workspace.root)
  capture_root_identity if File.exist?(@root)
end

Instance Attribute Details

#effective_policyObject (readonly)

Reports the host permissions this backend actually uses. In particular, unrestricted execution cannot make the host root filesystem read-only.



64
65
66
# File 'lib/little_ghost/sandboxes/unrestricted.rb', line 64

def effective_policy
  @effective_policy
end

Instance Method Details

#capabilitiesObject

Reports host execution and the bounded filesystem operations exposed by this instance. isolation: :none is deliberate: unrestricted execution is not a security boundary.



69
70
71
72
73
# File 'lib/little_ghost/sandboxes/unrestricted.rb', line 69

def capabilities
  features = %i[filesystem_read filesystem_list process_execute process_spawn]
  features.concat(%i[filesystem_write filesystem_replace]) if writable?
  Sandbox::Capabilities.new(features:, network_modes: [:inherit], isolation: :none)
end

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

Executes an argument vector on the host from the workspace root.

Shell syntax is not interpreted. The child starts with an empty environment unless inherit_environment is true, is terminated when the context is cancelled or the timeout expires, and has each output stream truncated to max_output_bytes.

Raises:



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
# File 'lib/little_ghost/sandboxes/unrestricted.rb', line 94

def execute_program(
  command,
  timeout:,
  context: nil,
  max_output_bytes: nil,
  environment: {},
  inherit_environment: false,
  scope: nil
)
  argv = Array(command).map(&:to_s)
  raise ToolError, "Command must contain an executable" if argv.empty? || argv.first.empty?

  timeout = Float(timeout)
  max_output_bytes = Integer(max_output_bytes || limits.output_bytes)
  raise ArgumentError, "timeout must be positive" unless timeout.positive?
  raise ArgumentError, "max_output_bytes must be positive" unless max_output_bytes.positive?

  stdout, stderr, status = capture(
    argv,
    timeout:,
    context:,
    max_output_bytes:,
    environment: workspace.environment.merge(effective_policy.environment.values).merge(environment),
    inherit_environment: effective_policy.environment.inherit? && inherit_environment
  )
  Sandbox::Execution.new(stdout:, stderr:, exit_code: status.exitstatus)
end

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

Produces a newline-delimited, sorted directory listing. Directories end in /.



80
# File 'lib/little_ghost/sandboxes/unrestricted.rb', line 80

def list(path = ".", context: nil) = default_scope.list(path, context:)

#open(run: nil) ⇒ Object

Opens the sandbox and verifies that the workspace root has not changed.



49
50
51
52
53
54
55
56
57
# File 'lib/little_ghost/sandboxes/unrestricted.rb', line 49

def open(run: nil)
  if @root_identity
    validate_root!
  else
    @root = File.realpath(workspace.root)
    capture_root_identity
  end
  self
end

#read(path, context: nil) ⇒ Object

Reads a bounded UTF-8 file within the workspace.



76
# File 'lib/little_ghost/sandboxes/unrestricted.rb', line 76

def read(path, context: nil) = default_scope.read(path, context:)

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

Replaces exactly one occurrence of old_text in a writable file.



86
# File 'lib/little_ghost/sandboxes/unrestricted.rb', line 86

def replace(path, old_text, new_text, context: nil) = default_scope.replace(path, old_text, new_text, context:)

#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: true) ⇒ Object

Starts a bounded host process. This remains unrestricted host execution, not a containment boundary.



124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
# File 'lib/little_ghost/sandboxes/unrestricted.rb', line 124

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: true)
  unless allow_subprocesses
    raise CapabilityError, "the unrestricted sandbox cannot prohibit subprocess creation"
  end

  Sandbox::ProcessSession.new(
    command:,
    environment: workspace.environment.merge(effective_policy.environment.values).merge(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

#writable?Boolean

Indicates whether this sandbox accepts filesystem mutations.

Returns:

  • (Boolean)


60
# File 'lib/little_ghost/sandboxes/unrestricted.rb', line 60

def writable? = @writable

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

Writes a bounded String without following a symbolic-link target.



83
# File 'lib/little_ghost/sandboxes/unrestricted.rb', line 83

def write(path, content, context: nil) = default_scope.write(path, content, context:)