class LittleGhost::Sandboxes::Unrestricted
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.
Attributes
Reports the host permissions this backend actually uses. In particular, unrestricted execution cannot make the host root filesystem read-only.
Public Class Methods
# 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
Configures a host sandbox with an explicit policy and resource limits.
LittleGhost::Sandbox::new
Public Instance Methods
Source
# 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
Reports host execution and the bounded filesystem operations exposed by this instance. +isolation: :none+ is deliberate: unrestricted execution is not a security boundary.
# 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
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.
# File lib/little_ghost/sandboxes/unrestricted.rb, line 80 def list(path = ".", context: nil) = default_scope.list(path, context:)
Produces a newline-delimited, sorted directory listing. Directories end in /.
Source
# 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
Opens the sandbox and verifies that the workspace root has not changed.
Source
# File lib/little_ghost/sandboxes/unrestricted.rb, line 76 def read(path, context: nil) = default_scope.read(path, context:)
Reads a bounded UTF-8 file within the workspace.
# 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:)
Replaces exactly one occurrence of old_text in a writable file.
# 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
Starts a bounded host process. This remains unrestricted host execution, not a containment boundary.
Source
# File lib/little_ghost/sandboxes/unrestricted.rb, line 60 def writable? = @writable
Indicates whether this sandbox accepts filesystem mutations.
# File lib/little_ghost/sandboxes/unrestricted.rb, line 83 def write(path, content, context: nil) = default_scope.write(path, content, context:)
Writes a bounded String without following a symbolic-link target.