class LittleGhost::Workspace
A Workspace names the host paths associated with a Run. Pair it with a Sandbox to decide how those paths may be read, changed, or used by commands.
workspace = LittleGhost::Workspace.new(root: "./tmp/support-run") workspace.root # => an absolute path ending in "/tmp/support-run"
Workspaces participate in the Run resource lifecycle, but object lifetime and file lifetime are separate. Opening creates root and relative named paths, but does not delete them by default. Absolute named paths are trusted references that must already exist. Setup and teardown callbacks let trusted application configuration provision run-scoped resources without a Workspace subclass. Applications that share a writable root between Runs must provide their own concurrency and tenant isolation.
See the Workspaces and Sandboxes guide for logical paths, Sandbox policy, process ownership, and networking.
Attributes
Immutable named absolute paths owned by this workspace declaration.
Absolute filesystem root assigned to this workspace.
Public Class Methods
# File lib/little_ghost/workspace.rb, line 52 def initialize(root:, paths: {}, setup: nil, teardown: nil) @root = File.expand_path(root) @paths = normalize_paths(paths) @setup = validate_callback(setup, :setup) @teardown = validate_callback(teardown, :teardown) @opened = false @active = false @run = nil @identities = nil end
Expands root and every named path to absolute paths. Relative named paths must remain beneath root; absolute named paths deliberately refer outside it. setup receives workspace: and run: when the Run opens. teardown receives the same values when it closes, including after partial setup.
# File lib/little_ghost/workspace.rb, line 28 def register_provider(name, implementation) unless implementation.is_a?(Class) && implementation <= Workspace raise ArgumentError, "workspace provider must be a Workspace class" end Workspace.providers[name.to_sym] = implementation end
Registers a trusted workspace provider under a configuration symbol.
Source
# File lib/little_ghost/workspace.rb, line 37 def resolve_provider(name) Workspace.providers.fetch(name.to_sym) do raise DependencyError, "workspace provider :#{name} is not available" end end
Resolves an explicitly selected provider without changing its meaning.
Public Instance Methods
Source
# File lib/little_ghost/workspace.rb, line 156 def close return nil unless @active @teardown&.call(workspace: self, run: @run) nil ensure @opened = false @active = false @run = nil @identities = nil end
Calls the application teardown callback once. The default does not remove files or directories.
Source
# File lib/little_ghost/workspace.rb, line 115 def environment {"LITTLE_GHOST_WORKSPACE_ROOT" => root}.merge(paths.to_h do |name, path| ["LITTLE_GHOST_WORKSPACE_#{name.to_s.upcase.gsub(/[^A-Z0-9]/, "_")}", path] end).freeze end
Environment variables supplied to sandboxed programs. These values are trusted process configuration and are never returned by filesystem tools.
Source
# File lib/little_ghost/workspace.rb, line 139 def open(run: nil) return self if @opened @run = run @active = true @setup&.call(workspace: self, run:) materialize! capture_identities! @opened = true self rescue close raise end
Calls the application setup callback once and returns this workspace.
Source
# File lib/little_ghost/workspace.rb, line 70 def path(name) paths.fetch(name.to_sym) end
Returns a configured named path, raising KeyError when it is absent.
Source
# File lib/little_ghost/workspace.rb, line 100 def reference(physical_path) candidate = File.expand_path(physical_path) named = paths.sort_by { |_, path| -path.length }.find { |_, path| beneath?(candidate, path) } if named name, base = named relative = candidate.delete_prefix(base).delete_prefix(File::SEPARATOR) return relative.empty? ? "workspace://#{name}" : "workspace://#{name}/#{relative}" end raise ArgumentError, "path is outside the workspace" unless beneath?(candidate, root) candidate.delete_prefix(root).delete_prefix(File::SEPARATOR).then { |value| value.empty? ? "." : value } end
Returns the stable logical reference for a physical workspace path.
Source
# File lib/little_ghost/workspace.rb, line 82 def resolve(reference) validate! if @identities value = String(reference) raise ArgumentError, "workspace paths must be relative or use workspace://" if Pathname.new(value).absolute? base, relative = if value.start_with?("workspace://") logical = value.delete_prefix("workspace://") name, separator, child = logical.partition("/") raise ArgumentError, "workspace path must name a configured path" if name.empty? [path(name), separator.empty? ? "." : child] else [root, value] end resolve_beneath(base, relative) end
Converts a logical path to its physical workspace path. This method checks lexical containment but does not make direct filesystem access safe for untrusted input. Pass model-selected paths through Sandbox file operations, which reject symlinks while opening each path component.
Relative paths belong to root; named paths use workspace://name/path. Physical absolute paths are deliberately rejected so brokered tools do not teach callers host filesystem layout.
Source
# File lib/little_ghost/workspace.rb, line 122 def validate! return self unless @identities {root: root}.merge(paths).each do |name, path| realpath = File.realpath(path) stat = File.stat(realpath) expected = @identities.fetch(name) unless [realpath, stat.dev, stat.ino] == expected raise ToolError, "workspace path changed after opening: #{name}" end end self rescue Errno::ENOENT raise ToolError, "workspace path changed after opening: #{name}" end
Verifies that no configured directory was replaced after open.