class LittleGhost::Sandbox::Scope
A non-owning, capability-reduced view of a Sandbox for one agent or Tool set. Scopes never open or close their parent and cannot widen it. They constrain only callers that receive and use the Scope; code retaining the parent Sandbox retains its broader authority.
Attributes
Operations exposed through this scope.
Outbound connectivity available to processes launched through this scope.
Sandbox that enforces process execution.
Public Class Methods
Source
# File lib/little_ghost/sandbox/scope.rb, line 12 def initialize(sandbox:, files: nil, runtime_paths: nil, capabilities: nil, network: nil, parent_scope: nil) @sandbox = sandbox @parent_mounts = parent_scope&.process_grants || sandbox.effective_policy.process_grants(sandbox.workspace) @parent_tool_denies = if parent_scope parent_scope.send(:tool_deny_mounts) else @parent_mounts.reject(&:tool_visible?) end requested = scoped_mounts(files, runtime_paths) requested_mounts = requested ? preserve_restrictive_overlays(normalize_mounts(requested)) : @parent_mounts @mounts = validate_mounts(requested_mounts).sort_by { |mount| -mount.target.length }.freeze @tool_deny_mounts = relevant_tool_denies(@mounts).freeze parent_capabilities = parent_scope&.capabilities || sandbox.capabilities requested_capabilities = capabilities || parent_capabilities requested_capabilities = Capabilities.new(features: requested_capabilities) unless requested_capabilities.is_a?(Capabilities) @capabilities = parent_capabilities.intersect(requested_capabilities) @network = narrow_network(parent_scope&.network || sandbox.effective_policy.network, network) if @network unless parent_capabilities.supports?(:network, @network.mode) raise CapabilityError, "sandbox backend cannot enforce scoped network mode :#{@network.mode}" end @capabilities = @capabilities.intersect(Capabilities.new( features: @capabilities.features, network_modes: [@network.mode], isolation: @capabilities.isolation )) end @filesystem = Filesystem.new( mounts: (@mounts + @tool_deny_mounts).uniq, relative_root: sandbox.workspace.root, max_read_bytes: sandbox.limits.read_bytes, max_write_bytes: sandbox.limits.write_bytes, max_list_entries: sandbox.limits.list_entries ) @mount_identities = mount_identities end
Creates a view of sandbox. mounts and capabilities may only narrow the parent scope or sandbox.
Public Instance Methods
# File lib/little_ghost/sandbox/scope.rb, line 71 def allows?(operation, path = nil) operation = Capabilities.normalize(operation) return supports?(operation) unless path @filesystem.allows?(operation, absolute_path(path)) rescue PolicyError, ToolError false end
Indicates whether operation is available at optional virtual path.
Source
# File lib/little_ghost/sandbox/scope.rb, line 121 def close = nil
Scopes own no resources, so closing has no effect.
Source
# File lib/little_ghost/sandbox/scope.rb, line 105 def execute(command, **options) require_capability!(:process_execute) sandbox.execute(command, scope: self, **options) end
Executes a shell command through the parent sandbox and this scope.
# File lib/little_ghost/sandbox/scope.rb, line 111 def execute_program(command, **options) require_capability!(:process_execute) sandbox.execute_program(command, scope: self, **options) end
Executes an argument vector through the parent sandbox and this scope.
# File lib/little_ghost/sandbox/scope.rb, line 87 def list(path = ".", context: nil) require_capability!(:filesystem_list) @filesystem.list(logical_path(path), context:) end
Lists one directory through the scoped filesystem.
Source
# File lib/little_ghost/sandbox/scope.rb, line 119 def open(run: nil) = self
Scopes own no resources; opening returns the same object.
Source
# File lib/little_ghost/sandbox/scope.rb, line 59 def policy = sandbox.effective_policy
Effective policy enforced by the parent sandbox.
Source
# File lib/little_ghost/sandbox/scope.rb, line 81 def read(path, context: nil) require_capability!(:filesystem_read) @filesystem.read(logical_path(path), context:) end
Reads bounded UTF-8 text through the scoped filesystem.
# File lib/little_ghost/sandbox/scope.rb, line 99 def replace(path, old_text, new_text, context: nil) require_capability!(:filesystem_replace) @filesystem.replace(logical_path(path), old_text, new_text, context:) end
Replaces one unique text occurrence through a writable scoped mount.
Source
# File lib/little_ghost/sandbox/scope.rb, line 117 def scope(**options) = self.class.new(sandbox:, parent_scope: self, **options)
Produces another view that can only narrow this scope.
# File lib/little_ghost/sandbox/scope.rb, line 64 def supports?(feature, value = nil) = capabilities.supports?(feature, value)
Indicates whether this scope exposes feature.
Source
# File lib/little_ghost/sandbox/scope.rb, line 124 def validate! current = mount_identities unless current == @mount_identities raise CapabilityError, "sandbox scope mount source changed after initialization" end self end
Fails closed if a selected host mount was replaced after scope creation.
Source
# File lib/little_ghost/sandbox/scope.rb, line 57 def workspace = sandbox.workspace
Workspace owned by the parent sandbox.
Source
# File lib/little_ghost/sandbox/scope.rb, line 66 def writable? = supports?(:filesystem_write) && @mounts.any? { |mount| mount.tool_visible? && mount.writable? }
Indicates whether any visible mount accepts writes.
# File lib/little_ghost/sandbox/scope.rb, line 93 def write(path, content, context: nil) require_capability!(:filesystem_write) @filesystem.write(logical_path(path), content, context:) end
Writes bounded content through a writable scoped mount.