Class: LittleGhost::Workspace
- Inherits:
-
Object
- Object
- LittleGhost::Workspace
- Defined in:
- lib/little_ghost/workspace.rb
Overview
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.
Instance Attribute Summary collapse
-
#paths ⇒ Object
readonly
Immutable named absolute paths owned by this workspace declaration.
-
#root ⇒ Object
readonly
Absolute filesystem root assigned to this workspace.
Class Method Summary collapse
-
.providers ⇒ Object
:nodoc:.
-
.register_provider(name, implementation) ⇒ Object
Registers a trusted workspace provider under a configuration symbol.
-
.resolve_provider(name) ⇒ Object
Resolves an explicitly selected provider without changing its meaning.
Instance Method Summary collapse
-
#close ⇒ Object
Calls the application teardown callback once.
-
#environment ⇒ Object
Environment variables supplied to sandboxed programs.
-
#initialize(root:, paths: {}, setup: nil, teardown: nil) ⇒ Workspace
constructor
Expands
rootand every named path to absolute paths. -
#open(run: nil) ⇒ Object
Calls the application setup callback once and returns this workspace.
-
#path(name) ⇒ Object
Returns a configured named path, raising KeyError when it is absent.
-
#reference(physical_path) ⇒ Object
Returns the stable logical reference for a physical workspace path.
-
#resolve(reference) ⇒ Object
Converts a logical path to its physical workspace path.
-
#validate! ⇒ Object
Verifies that no configured directory was replaced after #open.
Constructor Details
#initialize(root:, paths: {}, setup: nil, teardown: nil) ⇒ Workspace
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.
52 53 54 55 56 57 58 59 60 61 |
# File 'lib/little_ghost/workspace.rb', line 52 def initialize(root:, paths: {}, setup: nil, teardown: nil) @root = File.(root) @paths = normalize_paths(paths) @setup = validate_callback(setup, :setup) @teardown = validate_callback(teardown, :teardown) @opened = false @active = false @run = nil @identities = nil end |
Instance Attribute Details
#paths ⇒ Object (readonly)
Immutable named absolute paths owned by this workspace declaration.
67 68 69 |
# File 'lib/little_ghost/workspace.rb', line 67 def paths @paths end |
#root ⇒ Object (readonly)
Absolute filesystem root assigned to this workspace.
64 65 66 |
# File 'lib/little_ghost/workspace.rb', line 64 def root @root end |
Class Method Details
.providers ⇒ Object
:nodoc:
43 44 45 |
# File 'lib/little_ghost/workspace.rb', line 43 def providers # :nodoc: @providers ||= {} end |
.register_provider(name, implementation) ⇒ Object
Registers a trusted workspace provider under a configuration symbol.
28 29 30 31 32 33 34 |
# 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 |
.resolve_provider(name) ⇒ Object
Resolves an explicitly selected provider without changing its meaning.
37 38 39 40 41 |
# 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 |
Instance Method Details
#close ⇒ Object
Calls the application teardown callback once. The default does not remove files or directories.
156 157 158 159 160 161 162 163 164 165 166 |
# 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 |
#environment ⇒ Object
Environment variables supplied to sandboxed programs. These values are trusted process configuration and are never returned by filesystem tools.
115 116 117 118 119 |
# 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 |
#open(run: nil) ⇒ Object
Calls the application setup callback once and returns this workspace.
139 140 141 142 143 144 145 146 147 148 149 150 151 152 |
# 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 |
#path(name) ⇒ Object
Returns a configured named path, raising KeyError when it is absent.
70 71 72 |
# File 'lib/little_ghost/workspace.rb', line 70 def path(name) paths.fetch(name.to_sym) end |
#reference(physical_path) ⇒ Object
Returns the stable logical reference for a physical workspace path.
100 101 102 103 104 105 106 107 108 109 110 111 |
# File 'lib/little_ghost/workspace.rb', line 100 def reference(physical_path) candidate = File.(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 |
#resolve(reference) ⇒ Object
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.
82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 |
# 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 |
#validate! ⇒ Object
Verifies that no configured directory was replaced after #open.
122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 |
# 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 |