little_ghost 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +28 -6
  3. data/docs/guides/assemblies.md +143 -17
  4. data/docs/guides/code_mode.md +276 -0
  5. data/docs/guides/core_concepts.md +46 -30
  6. data/docs/guides/getting_started.md +17 -6
  7. data/docs/guides/integrations.md +217 -0
  8. data/docs/guides/models_and_providers.md +125 -0
  9. data/docs/guides/production.md +190 -17
  10. data/docs/guides/prompt_views.md +14 -7
  11. data/docs/guides/sandboxing.md +282 -0
  12. data/docs/guides/skills.md +141 -0
  13. data/docs/guides/structured_outputs_and_content.md +135 -0
  14. data/docs/guides/tools.md +355 -0
  15. data/lib/little_ghost/ag_ui/adapter.rb +2 -2
  16. data/lib/little_ghost/agent/delegation.rb +2 -2
  17. data/lib/little_ghost/agent/skills.rb +6 -1
  18. data/lib/little_ghost/agent/tool_loop.rb +6 -2
  19. data/lib/little_ghost/agent.rb +250 -48
  20. data/lib/little_ghost/agent_builder.rb +61 -22
  21. data/lib/little_ghost/agent_stream_source.rb +262 -0
  22. data/lib/little_ghost/artifact.rb +182 -0
  23. data/lib/little_ghost/artifacts/presentation_budget.rb +44 -0
  24. data/lib/little_ghost/artifacts/workspace_store.rb +370 -0
  25. data/lib/little_ghost/artifacts.rb +11 -0
  26. data/lib/little_ghost/assembly.rb +64 -20
  27. data/lib/little_ghost/assembly_builder.rb +26 -26
  28. data/lib/little_ghost/assembly_execution.rb +23 -11
  29. data/lib/little_ghost/code_mode/broker.rb +168 -0
  30. data/lib/little_ghost/code_mode/catalog.rb +54 -0
  31. data/lib/little_ghost/code_mode/engine.rb +58 -0
  32. data/lib/little_ghost/code_mode/javascript/catalog.rb +118 -0
  33. data/lib/little_ghost/code_mode/javascript/client.rb +550 -0
  34. data/lib/little_ghost/code_mode/javascript/host.rb +347 -0
  35. data/lib/little_ghost/code_mode/javascript/session.rb +578 -0
  36. data/lib/little_ghost/code_mode/javascript_engine.rb +172 -0
  37. data/lib/little_ghost/code_mode/protocol.rb +78 -0
  38. data/lib/little_ghost/code_mode/ruby/catalog.rb +80 -0
  39. data/lib/little_ghost/code_mode/ruby/host.rb +102 -0
  40. data/lib/little_ghost/code_mode/ruby/session.rb +508 -0
  41. data/lib/little_ghost/code_mode/ruby_engine.rb +86 -0
  42. data/lib/little_ghost/code_mode/runtime.rb +335 -0
  43. data/lib/little_ghost/code_mode/session.rb +61 -0
  44. data/lib/little_ghost/code_mode/types.rb +58 -0
  45. data/lib/little_ghost/code_mode.rb +53 -0
  46. data/lib/little_ghost/configuration.rb +208 -21
  47. data/lib/little_ghost/content.rb +19 -8
  48. data/lib/little_ghost/data_map.rb +2 -2
  49. data/lib/little_ghost/errors.rb +26 -7
  50. data/lib/little_ghost/execution.rb +32 -27
  51. data/lib/little_ghost/execution_state.rb +3 -3
  52. data/lib/little_ghost/graph.rb +373 -210
  53. data/lib/little_ghost/instrumentation.rb +2 -1
  54. data/lib/little_ghost/mcp/client.rb +487 -88
  55. data/lib/little_ghost/mcp/toolset.rb +210 -0
  56. data/lib/little_ghost/mcp/types.rb +216 -0
  57. data/lib/little_ghost/mcp.rb +3 -0
  58. data/lib/little_ghost/model_capabilities.rb +9 -6
  59. data/lib/little_ghost/model_request.rb +0 -12
  60. data/lib/little_ghost/model_resolver.rb +13 -3
  61. data/lib/little_ghost/model_response.rb +3 -7
  62. data/lib/little_ghost/network/authorizer_server.rb +162 -0
  63. data/lib/little_ghost/network/certificate_authority.rb +104 -0
  64. data/lib/little_ghost/network/envoy_config.rb +362 -0
  65. data/lib/little_ghost/network/envoy_gateway.rb +418 -0
  66. data/lib/little_ghost/network/external_gateway.rb +68 -0
  67. data/lib/little_ghost/network.rb +96 -0
  68. data/lib/little_ghost/prompt_resolver.rb +7 -7
  69. data/lib/little_ghost/provider_registry.rb +3 -3
  70. data/lib/little_ghost/providers/anthropic.rb +8 -1
  71. data/lib/little_ghost/providers/gemini.rb +10 -1
  72. data/lib/little_ghost/providers/vertex_ai.rb +6 -1
  73. data/lib/little_ghost/run.rb +89 -25
  74. data/lib/little_ghost/run_context.rb +11 -1
  75. data/lib/little_ghost/run_result.rb +0 -7
  76. data/lib/little_ghost/runtime/hook.rb +5 -0
  77. data/lib/little_ghost/runtime/hooks/artifacts.rb +338 -0
  78. data/lib/little_ghost/runtime.rb +111 -34
  79. data/lib/little_ghost/sandbox/capabilities.rb +68 -0
  80. data/lib/little_ghost/sandbox/environment_policy.rb +41 -0
  81. data/lib/little_ghost/sandbox/filesystem.rb +377 -0
  82. data/lib/little_ghost/sandbox/isolated_backend.rb +163 -0
  83. data/lib/little_ghost/sandbox/limits.rb +48 -0
  84. data/lib/little_ghost/sandbox/mount.rb +129 -0
  85. data/lib/little_ghost/sandbox/network_policy.rb +108 -0
  86. data/lib/little_ghost/sandbox/policy.rb +88 -0
  87. data/lib/little_ghost/sandbox/process_runner.rb +103 -0
  88. data/lib/little_ghost/sandbox/process_session.rb +335 -0
  89. data/lib/little_ghost/sandbox/scope.rb +304 -0
  90. data/lib/little_ghost/sandbox.rb +158 -32
  91. data/lib/little_ghost/sandboxes/bubblewrap.rb +351 -0
  92. data/lib/little_ghost/sandboxes/native.rb +51 -0
  93. data/lib/little_ghost/sandboxes/seatbelt.rb +261 -0
  94. data/lib/little_ghost/sandboxes/unrestricted.rb +240 -0
  95. data/lib/little_ghost/session.rb +30 -6
  96. data/lib/little_ghost/session_stores/filesystem.rb +29 -13
  97. data/lib/little_ghost/skills/catalog.rb +94 -14
  98. data/lib/little_ghost/skills/resource_root.rb +42 -0
  99. data/lib/little_ghost/skills/skill.rb +0 -3
  100. data/lib/little_ghost/stream_event.rb +8 -13
  101. data/lib/little_ghost/subagents/control_tool.rb +8 -0
  102. data/lib/little_ghost/subagents/manager.rb +182 -79
  103. data/lib/little_ghost/support/callbacks.rb +3 -1
  104. data/lib/little_ghost/support/cancellation_token.rb +2 -2
  105. data/lib/little_ghost/support/content_capture.rb +3 -3
  106. data/lib/little_ghost/support/executor.rb +84 -15
  107. data/lib/little_ghost/support/http_client.rb +11 -4
  108. data/lib/little_ghost/support/interruptible_stream.rb +4 -2
  109. data/lib/little_ghost/support/pooled_thread_runner.rb +126 -0
  110. data/lib/little_ghost/support/redactor.rb +1 -1
  111. data/lib/little_ghost/support/serialized_dispatcher.rb +84 -0
  112. data/lib/little_ghost/support/task.rb +126 -0
  113. data/lib/little_ghost/support/task_runner.rb +74 -0
  114. data/lib/little_ghost/support.rb +4 -0
  115. data/lib/little_ghost/tool.rb +104 -53
  116. data/lib/little_ghost/tool_registry.rb +1 -1
  117. data/lib/little_ghost/tools/filesystem.rb +1 -1
  118. data/lib/little_ghost/tools/shell.rb +4 -3
  119. data/lib/little_ghost/version.rb +1 -1
  120. data/lib/little_ghost/workflow.rb +3 -3
  121. data/lib/little_ghost/workspace.rb +222 -8
  122. data/lib/little_ghost.rb +38 -12
  123. metadata +116 -2
  124. data/lib/little_ghost/unrestricted_sandbox.rb +0 -306
@@ -39,7 +39,9 @@ end
39
39
 
40
40
  Provider connections and model roles can also live in YAML files under `config/little_ghost`, or in files you select explicitly. Values set in Ruby take priority. An explicitly selected file comes next, followed by conventional files and environment defaults. See `LittleGhost::Configuration` when you need every supported source and override.
41
41
 
42
- Prompts, caller input and history, tool results, and attachments may leave the application for the selected external provider. Select providers from trusted configuration and account for their retention and data-residency policies.
42
+ Prompts, caller input and history, Tool results, and attachments may go to the
43
+ selected provider. Choose configured providers that are appropriate for that
44
+ data and its retention or residency needs.
43
45
 
44
46
  ## Configure once, call from anywhere
45
47
 
@@ -73,6 +75,103 @@ The controller supplies identity and account access from authenticated applicati
73
75
 
74
76
  Configure LittleGhost before the first Agent or Assembly call. Once application services start successfully, the configuration is locked so every request sees one stable setup.
75
77
 
78
+ ## Use an existing fiber scheduler
79
+
80
+ If your application already runs inside a Ruby Fiber scheduler, LittleGhost
81
+ uses it for parallel Tool calls, Workflow and Graph branches, subagent turns,
82
+ background Executions, and nested code-mode Tool calls. LittleGhost does not
83
+ install or run a scheduler. Your scheduler must support `Fiber.schedule`. The
84
+ optional `async` gem used below does.
85
+
86
+ For example, an application using the optional `async` gem can let several
87
+ requests make progress on one thread while each request waits for network I/O.
88
+ Add `gem "async"` to the application's bundle, then start the calls inside an
89
+ Async task:
90
+
91
+ ```ruby
92
+ require "async"
93
+
94
+ questions = [
95
+ "How long do refunds take?",
96
+ "Can I update my delivery address?"
97
+ ]
98
+
99
+ answers = Async do |task|
100
+ questions.map do |question|
101
+ task.async { CustomerSupportAgent.ask(question).response }
102
+ end.map(&:wait)
103
+ end.wait
104
+ ```
105
+
106
+ The default `:auto` setting uses fibers when a call begins inside a scheduled
107
+ fiber and worker threads everywhere else. Set `concurrency_backend` to `:fiber`
108
+ when calling outside a scheduled fiber should be an application error.
109
+ LittleGhost then raises `LittleGhost::ConfigurationError` instead of quietly
110
+ starting a thread.
111
+
112
+ ### Keep one blocking call from pausing other fibers
113
+
114
+ Fiber scheduling helps while work waits for I/O; it does not make CPU-heavy
115
+ Ruby code run in parallel. Whether an I/O call lets other fibers run depends on
116
+ the Ruby version, scheduler, and library. Call libraries normally at first. If
117
+ documentation or measurement shows that one call pauses the other fibers—and
118
+ the work can continue on another Ruby thread—wrap that call:
119
+
120
+ ```ruby
121
+ article = LittleGhost.offload_blocking do
122
+ HelpCenterSearch.lookup(question)
123
+ end
124
+ ```
125
+
126
+ Outside a scheduled fiber, `offload_blocking` runs the block inline. Inside one,
127
+ it uses LittleGhost's shared pool of reusable threads so the other fibers can
128
+ continue. If the caller is cancelled after the pool accepts the block,
129
+ LittleGhost waits for the block to finish before reporting the cancellation.
130
+ The helper does not add a timeout or cancellation mechanism to the underlying
131
+ call, so use the library's controls when available.
132
+
133
+ If most of your Tool or extension code prevents other fibers from running, use
134
+ worker threads for all LittleGhost concurrency instead:
135
+
136
+ ```ruby
137
+ LittleGhost.configure do |config|
138
+ config.concurrency_backend = :thread
139
+ end
140
+ ```
141
+
142
+ LittleGhost also keeps a few jobs on threads so it can finish or clean them up
143
+ reliably. Provider and subprocess streams may use dedicated threads. Inside a
144
+ scheduled fiber, certificate generation and Filesystem SessionStore
145
+ transactions use the same shared pool as `offload_blocking`. A Run may
146
+ therefore still create or use threads when its concurrency backend is `:fiber`.
147
+
148
+ The blocking pool allows two operations at a time by default. Applications
149
+ that observe calls waiting for a pool worker can increase its process-wide
150
+ capacity during startup, before any call can start the pool:
151
+
152
+ ```ruby
153
+ LittleGhost.configure do |config|
154
+ config.blocking_pool_capacity 4
155
+ end
156
+ ```
157
+
158
+ Increasing this value permits more operating-system threads. It does not
159
+ change Tool, Workflow, Graph, or subagent concurrency. The value is
160
+ process-wide; every `Configuration` reads and writes the same setting.
161
+
162
+ ### Protect state shared by concurrent calls
163
+
164
+ LittleGhost may call a shared Tool, provider, SessionStore, hook, or callback
165
+ from different threads. Fibers can also take turns entering the same object on
166
+ one thread. Protect shared mutable state, keep lock scope narrow, and do not
167
+ call application callbacks while holding a lock.
168
+
169
+ Pass request-specific values through the Invocation context or another explicit
170
+ argument. Do not use `thread_variable_set` for request state because every
171
+ fiber on the thread shares those values. `Thread.current[:key]` is fiber-local,
172
+ but LittleGhost does not copy application-defined entries into each worker
173
+ task.
174
+
76
175
  ## Preserve conversation with Sessions
77
176
 
78
177
  A **Session** lets one request continue an earlier conversation. Pass the same session ID and trusted actor ID with each related call:
@@ -104,7 +203,8 @@ Every Run has a session ID so LittleGhost can checkpoint its progress. If you do
104
203
 
105
204
  ## Stream or supervise long-running work
106
205
 
107
- `.stream_ask` runs on the caller's thread and yields `StreamEvent` values as the answer arrives:
206
+ `.stream_ask` runs on the caller's fiber or thread and yields `StreamEvent`
207
+ values as the answer arrives:
108
208
 
109
209
  ```ruby
110
210
  stream = CustomerSupportAgent.stream_ask(question)
@@ -116,6 +216,14 @@ end
116
216
  record_outcome(run.outcome, error_type: run.error&.class&.name)
117
217
  ```
118
218
 
219
+ Composite assembly streams include intermediate and nested Agent work as `:agent_stream` events. This default also applies to the event consumer passed to `start_execution`. Pass `include_agent_events: false` when only the ordinary public stream is needed.
220
+
221
+ > **Safety note:** Contextual events can include inputs, reasoning, Tool
222
+ > arguments and results, errors, and output from every participant. Check that
223
+ > the destination may see the complete Run, or filter the events before sending
224
+ > or storing them. LittleGhost's AG-UI adapter ignores these events unless the
225
+ > application translates them explicitly.
226
+
119
227
  Use `start_execution` when the caller must stay free for other work, or when you want to deliver an interjection to an active response:
120
228
 
121
229
  ```ruby
@@ -128,13 +236,24 @@ execution.wait(deadline: Time.now + 30)
128
236
  execution.run.completed?
129
237
  ```
130
238
 
131
- The event block runs on the worker thread, so keep it quick. Cancellation, deadlines, and `close` ask the work to stop; they cannot forcibly end arbitrary provider or tool code. They also cannot undo actions that already happened.
239
+ The event block runs on the same fiber or thread as the Execution. With `:auto`,
240
+ `start_execution` uses a fiber when its caller is already in a scheduled fiber;
241
+ otherwise, it uses a worker thread. Keep the block quick because it slows event
242
+ delivery while it runs. Cancellation, deadlines, and `close` ask the work to
243
+ stop; they cannot forcibly end arbitrary provider or Tool code or undo actions
244
+ that already happened. Keep the application's scheduler running until an
245
+ Execution using it finishes or closes.
132
246
 
133
- ## Treat tools as application boundaries
247
+ ## Keep Tool permission checks in application code
134
248
 
135
- A tool schema checks the shape of model-supplied input. Your application still owns permission checks, safe retries, rate limits, tenant boundaries, and auditing.
249
+ A Tool schema checks the shape of model-supplied input. Your application still
250
+ owns permission checks, safe retries, rate limits, and auditing. An ordinary
251
+ Tool runs in the Ruby process; a Sandbox contains only file or child-process
252
+ work that deliberately passes through it.
136
253
 
137
- Use the Tool binding's `run` to read current, application-established values from `run.invocation.context`. Do not make permission decisions from model arguments.
254
+ Use the Tool binding's `run` to read current, application-established values
255
+ from `run.invocation.context`. Don't rely on model arguments for identity or
256
+ account membership.
138
257
 
139
258
  Treat `RunContext#state` as mutable working and Session state. Revalidate anything restored from an earlier request. Synchronize access when parallel Tools share mutable state, or mark every Tool that reads or changes it as `exclusive true`.
140
259
 
@@ -142,27 +261,72 @@ A `ToolError` message is visible to the model, so keep it safe to share. LittleG
142
261
 
143
262
  When a step retries, its tool calls may happen again too. Prefer read-only work, idempotency keys, or operations that are safe to repeat.
144
263
 
264
+ [Tools](tools.md) develops this pattern from the first application Tool through
265
+ bindings, concurrency, sandbox delegation, and code mode.
266
+
145
267
  ## Choose workspace and sandbox behavior explicitly
146
268
 
147
- A Workspace gives one Run a place for files. A Sandbox decides how filesystem and process operations happen there.
269
+ A **Workspace** names the host paths associated with a Run. A **Sandbox**
270
+ controls filesystem operations and child processes deliberately sent through
271
+ it. A custom Ruby Tool stays in the application process unless it delegates
272
+ work to the bound Sandbox.
273
+
274
+ The dependency-free default is an application-root Workspace with `LittleGhost::Sandboxes::Unrestricted`. It is not process or network isolation. Select an enforcing backend explicitly when a model can influence commands; LittleGhost raises when that backend is unavailable instead of silently falling back:
275
+
276
+ ```ruby
277
+ require "fileutils"
278
+ require "tmpdir"
279
+
280
+ LittleGhost.configure do |config|
281
+ config.workspace = lambda do |**|
282
+ root = Dir.mktmpdir("little-ghost-support-")
283
+ LittleGhost::Workspace.new(
284
+ root:,
285
+ teardown: lambda do |workspace:, **|
286
+ FileUtils.remove_entry_secure(workspace.root) if File.exist?(workspace.root)
287
+ end
288
+ )
289
+ end
290
+ config.sandbox = {
291
+ provider: :native,
292
+ files: {root: :read_write},
293
+ root_filesystem: :isolated,
294
+ environment: {inherit: false, set: {"LANG" => "C.UTF-8"}},
295
+ network: :none
296
+ }
297
+ end
298
+ ```
299
+
300
+ The temporary Workspace above is useful when files should live for one Run. Use application-managed storage, with tenant isolation and concurrency control, when files must persist or several Runs share a root.
301
+
302
+ The Run opens a Runtime-created Workspace before its Sandbox and closes them in reverse order after every outcome. Closing a Workspace invokes its configured teardown; it does not delete files by default. Cleanup failures raise because LittleGhost cannot confirm a clean shutdown of every owned resource. Existing instances passed by the application remain caller-owned.
148
303
 
149
- `LittleGhost::UnrestrictedSandbox` uses the host machine with the Ruby process's permissions. It does not contain untrusted code. Expose only the tools the model needs, and use a real isolation boundary when untrusted code must run.
304
+ [Workspaces and Sandboxes](sandboxing.md) explains backend selection, logical
305
+ Workspace paths, files, process-only runtime paths, Scopes, filtered
306
+ networking, and deployment validation in depth.
150
307
 
151
- The Run closes workspaces and sandboxes that LittleGhost creates for it. If your application passes an existing instance instead, your application keeps ownership and must close it when its own lifecycle ends.
308
+ [Code Mode](code_mode.md) applies that setup to model-authored Ruby or
309
+ optional JavaScript that composes the Agent's existing Tools.
152
310
 
153
- ## Instrument without leaking the application
311
+ ## Protect data in telemetry
154
312
 
155
313
  LittleGhost emits events as a request starts, calls a model or tool, moves between assembly steps, retries, and finishes. Instrumentation subscribers and OpenTelemetry exporters can send those events to your monitoring system.
156
314
 
157
- An external telemetry service may receive application identifiers and event data. Redact sensitive values before they leave your boundary. Avoid attributes with many unique values, such as raw order or request IDs. Replacing one identifier does not make the rest of the data anonymous.
315
+ An external telemetry service may receive application identifiers and event
316
+ data. Redact sensitive values before exporting them. Avoid attributes with many
317
+ unique values, such as raw order or request IDs.
158
318
 
159
- A composite `RunResult` includes short step summaries and trajectory queries. Keep detailed provider errors and sensitive diagnostics in trusted monitoring channels, not in model or user responses.
319
+ A composite `RunResult` includes short step summaries and trajectory queries.
320
+ Keep detailed provider errors and sensitive diagnostics in your monitoring
321
+ system, not in model or user responses.
160
322
 
161
- ## Keep ownership and failure visible
323
+ ## Know what the Run closes and raises
162
324
 
163
325
  One top-level Run owns the workspace and sandbox that LittleGhost creates for it, plus application resources registered with `run.register`. It closes those resources after success, failure, a partial response, or cancellation. Existing workspace or sandbox instances passed by the application remain caller-owned.
164
326
 
165
- Ordinary execution failures appear on the Run and its final event. Cleanup, event delivery, or instrumentation can still raise an exception: once those boundaries fail, LittleGhost cannot promise a clean ending.
327
+ Ordinary execution failures appear on the Run and its final event. Cleanup,
328
+ event delivery, or instrumentation can still raise an exception when
329
+ LittleGhost cannot promise a clean ending.
166
330
 
167
331
  ## Advanced: work with Runtime directly
168
332
 
@@ -176,11 +340,20 @@ runtime = LittleGhost::Runtime.new(configuration: configuration)
176
340
  agent = CustomerSupportAgent.new(runtime: runtime)
177
341
  ```
178
342
 
179
- An explicit Runtime is an independent configuration snapshot. It does not replace LittleGhost's shared default.
343
+ An explicit Runtime has its own independent configuration. It does not replace
344
+ LittleGhost's shared default.
180
345
 
181
- One Runtime can serve independent calls from several threads. Each call gets its own Run, participants, Tools, and Runtime-created workspace and sandbox. An Agent or Assembly already bound to an active Run must stay with that Run.
346
+ One Runtime can serve independent calls from several threads and fibers. Each
347
+ call gets its own Run, participants, Tools, and Runtime-created Workspace and
348
+ Sandbox. An Agent or Assembly already bound to an active Run must stay with
349
+ that Run.
182
350
 
183
- Within one SessionStore instance, LittleGhost serializes calls sharing a Session. Multi-process deployments need coordination from their store. Custom stores, identity and credential resolvers, model resolvers, hooks, instrumentation subscribers, providers, and resource factories may receive concurrent calls and must be thread-safe.
351
+ Within one SessionStore instance, LittleGhost serializes calls sharing a
352
+ Session. Multi-process deployments need coordination from their store. Custom
353
+ stores and other shared extension objects may receive concurrent calls. Calls
354
+ can overlap on different threads, or fibers can take turns entering the same
355
+ object on one thread. Protect shared mutable state without relying on thread
356
+ identity.
184
357
 
185
358
  Runtime has no shutdown step. Shared services supplied by the application keep their own lifecycle. Shut those services down with the rest of your application. If you installed process-wide instrumentation subscribers, flush or shut down `LittleGhost::Instrumentation` during application shutdown.
186
359
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  A short prompt fits nicely inside an Agent class. As the instructions grow, move them into a **prompt view**: an ERB file that LittleGhost finds and renders for the Agent.
4
4
 
5
- This keeps the Agent easy to scan. It also gives shared instructions and application values a natural home.
5
+ This keeps the Agent definition focused. It also gives shared instructions and application values a natural home.
6
6
 
7
7
  ## Start with the inline prompt
8
8
 
@@ -90,7 +90,7 @@ You are a customer support agent for <%= company_name %>.
90
90
 
91
91
  The underscore marks a partial. Its locals are explicit, so it does not quietly inherit everything available to the parent view.
92
92
 
93
- ## Override the convention when it helps
93
+ ## Choose a different template path
94
94
 
95
95
  Most named Agents can rely on their conventional path. Use `system_template` when a class should read a differently named view:
96
96
 
@@ -106,15 +106,20 @@ LittleGhost chooses one prompt source in this order:
106
106
  2. An explicit `system_template`
107
107
  3. The Agent's conventional `system.erb` view
108
108
 
109
- Applications can add prompt lookup roots through `Configuration#prompt_paths`. Earlier roots win, which is useful when one trusted application layer overrides a shared prompt package.
109
+ Applications can add prompt lookup roots through `Configuration#prompt_paths`.
110
+ Earlier roots win, which lets application code override a shared prompt package.
110
111
 
111
112
  ## Treat views as application code
112
113
 
113
- Prompt views run as ERB inside the Ruby process. They can call Ruby, so keep every prompt directory application-controlled and non-user-writable. Never turn a request or model-supplied path into a prompt root.
114
+ Prompt views run as ERB inside the Ruby process and can call Ruby. Keep prompt
115
+ directories with the rest of your application code rather than letting a
116
+ request choose one.
114
117
 
115
- `TrustedPath` exists for the uncommon case where trusted application code selects a request-specific root. It records a trust decision; it does not make an untrusted directory safe.
118
+ `TrustedPath` is available for the uncommon case where application code selects
119
+ a request-specific root. It marks that choice explicitly; it does not inspect
120
+ or restrict the directory.
116
121
 
117
- ## Keep request composition separate
122
+ ## Build request-specific input in a Workflow
118
123
 
119
124
  A prompt view defines reusable instructions for one Agent. A Workflow may still build request-specific input for that Agent:
120
125
 
@@ -129,4 +134,6 @@ MESSAGE
129
134
 
130
135
  The Workflow is composing this request. `CustomerSupportAgent` still receives its own system prompt view when it runs.
131
136
 
132
- Continue with [Running in Production](production.md) to configure model roles, preserve sessions, supervise execution, and connect observability.
137
+ Continue with [Tools](tools.md) to give those Agents application capabilities
138
+ while keeping model input, trusted context, and delegated sandbox operations
139
+ distinct.
@@ -0,0 +1,282 @@
1
+ # Workspaces and Sandboxes
2
+
3
+ A Workspace gives one Run—one top-level Agent or Assembly execution—a stable
4
+ set of host paths. A Sandbox decides which operations may reach those paths and
5
+ how child processes are contained.
6
+
7
+ Use them together when an Agent can read files, change files, or run programs:
8
+
9
+ ```ruby
10
+ require "fileutils"
11
+ require "tmpdir"
12
+
13
+ LittleGhost.configure do |config|
14
+ config.workspace = lambda do |**|
15
+ root = Dir.mktmpdir("little-ghost-support-")
16
+ LittleGhost::Workspace.new(
17
+ root:,
18
+ teardown: lambda do |workspace:, **|
19
+ FileUtils.remove_entry_secure(workspace.root) if File.exist?(workspace.root)
20
+ end
21
+ )
22
+ end
23
+
24
+ config.sandbox = {
25
+ provider: :native,
26
+ files: {root: :read_write},
27
+ root_filesystem: :isolated,
28
+ environment: {inherit: false, set: {"LANG" => "C.UTF-8"}},
29
+ network: :none
30
+ }
31
+ end
32
+ ```
33
+
34
+ This example gives each Run a temporary writable root and removes it during
35
+ teardown. The `:native` Sandbox backend selects Seatbelt on macOS or Bubblewrap on
36
+ Linux. It raises instead of running without isolation when the native backend
37
+ is unavailable.
38
+
39
+ `LittleGhost::Tools::Filesystem` and `LittleGhost::Tools::Shell` use the Sandbox
40
+ assigned to the current Run. Code-mode interpreters also run inside their own
41
+ Sandbox. Ordinary application Tools, callbacks, and provider requests stay in
42
+ the Ruby process unless they deliberately delegate an operation.
43
+
44
+ The split looks like this:
45
+
46
+ ```text
47
+ Ruby application process
48
+ ├── provider requests
49
+ ├── application Tool#call
50
+ └── Sandbox
51
+ ├── bounded filesystem operations
52
+ ├── Shell child processes
53
+ └── code-mode interpreter processes
54
+ ```
55
+
56
+ ## Give files a stable home
57
+
58
+ A Workspace names the files available during a Run and controls their setup and
59
+ cleanup. Its `root` is the default working directory. Named paths give important
60
+ directories logical identities:
61
+
62
+ ```ruby
63
+ workspace = LittleGhost::Workspace.new(
64
+ root: "/var/lib/support/run-481",
65
+ paths: {
66
+ source: "/srv/support/source",
67
+ skills: "skills",
68
+ home: "runtime-home"
69
+ }
70
+ )
71
+ ```
72
+
73
+ Relative named paths live beneath the root and are created when the Workspace
74
+ opens. Absolute named paths are references deliberately supplied by the
75
+ application and must already exist. A setup callback can provision them when
76
+ needed.
77
+
78
+ Opening resolves real paths and records which physical directories they refer
79
+ to. LittleGhost rejects two names that point to the same directory and later
80
+ fails closed if a configured directory is replaced. Nested paths are allowed,
81
+ and the most restrictive matching access wins.
82
+
83
+ Workspace object lifetime and file lifetime are separate. Closing invokes the
84
+ configured teardown callback, but does not delete files by default. Use a
85
+ run-scoped temporary root for disposable work. Use application-managed storage,
86
+ tenant isolation, and concurrency control when several Runs share files.
87
+
88
+ ## Give artifacts logical references
89
+
90
+ Images and documents normally remain provider content. When filesystem Tools
91
+ or code mode should read the same bytes, configure the conventional artifact
92
+ path and enable artifact handling:
93
+
94
+ ```ruby
95
+ LittleGhost.configure do |config|
96
+ config.workspace = {
97
+ provider: :directory,
98
+ root: "tmp/agent-runs",
99
+ paths: {artifacts: "artifacts"}
100
+ }
101
+ config.artifacts
102
+ end
103
+ ```
104
+
105
+ LittleGhost stores input images and documents after the Workspace and Sandbox
106
+ open. The model receives the image or document in its normal input without a
107
+ second text reference. Filesystem Tools can list `workspace://artifacts` when a
108
+ later operation needs the stored copy. Messages added while the Run is active
109
+ receive the same treatment.
110
+
111
+ LittleGhost limits the number and size of files stored from each message and
112
+ across the complete Run. Those limits also include Tool artifacts and oversized
113
+ Tool results. A successful operation stores all of its files. If storage fails
114
+ partway through, LittleGhost attempts to remove that operation's files and
115
+ reports a cleanup error when it cannot. Stored files use private permissions,
116
+ and the Workspace provider still owns final cleanup.
117
+
118
+ Declaring the path does not grant model access. When the Agent should read it,
119
+ grant the Sandbox access to `:artifacts` and include a filesystem Tool.
120
+
121
+ ## Pass logical paths, not host layout
122
+
123
+ The Filesystem Tool accepts root-relative paths such as `notes/today.md`.
124
+ Named paths use references such as
125
+ `workspace://skills/refunds/SKILL.md`.
126
+
127
+ Those references remain meaningful without exposing or translating the host
128
+ layout. The Filesystem Tool rejects physical absolute paths. Child processes
129
+ use the Workspace's real paths directly, start in its root, and receive
130
+ `LITTLE_GHOST_WORKSPACE_ROOT` plus one
131
+ `LITTLE_GHOST_WORKSPACE_<NAME>` variable for each named path.
132
+
133
+ Workspace references give application code one stable path format. Each
134
+ Sandbox backend applies the configured access using the filesystem controls
135
+ available on its host.
136
+
137
+ ## Separate Tool-visible files from process support
138
+
139
+ Sandbox configuration divides named paths into two groups:
140
+
141
+ ```ruby
142
+ config.sandbox = {
143
+ provider: :native,
144
+ files: {
145
+ root: :read_write,
146
+ source: :read_only,
147
+ skills: :read_only
148
+ },
149
+ runtime_paths: {
150
+ home: :read_write
151
+ },
152
+ network: :none
153
+ }
154
+ ```
155
+
156
+ `files` are visible to both the Filesystem Tool and sandboxed processes.
157
+ `runtime_paths` are process-only. Use runtime paths for interpreter libraries,
158
+ homes, sockets, and service state that a model should not browse through a
159
+ filesystem Tool.
160
+
161
+ This is visibility, not secrecy from the process. A child with a runtime-path
162
+ grant can use that path according to its access mode. The distinction prevents
163
+ the Filesystem Tool from offering it as a model-visible file
164
+ tree.
165
+
166
+ ## Narrow access with a Scope
167
+
168
+ A `Sandbox::Scope` is a non-owning, reduced view of a Sandbox. It can remove
169
+ paths, change writable access to read-only, remove capabilities (categories of
170
+ allowed operations), or narrow networking. It cannot widen its parent:
171
+
172
+ ```ruby
173
+ reviewer = run.sandbox.scope(
174
+ files: {source: :read_only},
175
+ runtime_paths: [],
176
+ capabilities: %i[filesystem_read filesystem_list],
177
+ network: false
178
+ )
179
+ ```
180
+
181
+ Scopes constrain only code that receives and uses the Scope. Code that keeps a
182
+ reference to the parent Sandbox keeps the parent's authority. A Scope does not
183
+ open or close its parent and owns no resources.
184
+
185
+ ## Choose how much of the host exists
186
+
187
+ `root_filesystem` controls what a sandboxed process can see beyond declared
188
+ Workspace paths:
189
+
190
+ - `:isolated` exposes only declared paths and required runtime support. It is
191
+ the default and the right starting point for generated interpreters.
192
+ - `:read_only` exposes the host filesystem for development commands while
193
+ confining writes to declared writable paths. It makes installed compilers,
194
+ package managers, profiles, and toolchains available, but the child can also
195
+ read host files unless the Ruby process already runs inside a container or VM
196
+ that prevents those reads.
197
+ - `:read_write` grants the host filesystem directly. Treat it as unrestricted
198
+ host authority.
199
+
200
+ On Seatbelt, host-visible modes permit subprocesses because development tools
201
+ often need them. A Scope can remove `process_spawn` for a command that does not.
202
+
203
+ ## Choose an enforcement backend
204
+
205
+ | Sandbox backend | Host | What it enforces |
206
+ | --- | --- | --- |
207
+ | `:native` | macOS or Linux | Selects Seatbelt on macOS and Bubblewrap on Linux; fails closed elsewhere |
208
+ | `:seatbelt` | macOS | Deny-default Seatbelt profile over the configured physical paths |
209
+ | `:bubblewrap` | Linux | Fresh user, PID, mount, IPC, UTS, and optional network namespaces |
210
+ | `:unrestricted` | Ruby platforms | No containment; commands have the application process's host authority |
211
+
212
+ `LittleGhost::Sandbox.probe(:native)` reports whether the platform backend is
213
+ available. Selecting an enforcing backend never falls back to unrestricted
214
+ execution.
215
+
216
+ Bubblewrap owns descendants with a PID namespace and ends them when the
217
+ supervising process dies. It cannot selectively deny fork inside that
218
+ namespace, so a request for
219
+ `allow_subprocesses: false` fails closed rather than claiming an unenforced
220
+ restriction. Bubblewrap does not impose a task-count limit by itself. Use an
221
+ outer cgroup or container supervisor when generated code needs a hard cap on
222
+ the processes and threads it can create.
223
+
224
+ Seatbelt can constrain spawned children, but macOS has no PID namespace. It
225
+ terminates the command process group during cleanup; a child that deliberately
226
+ detaches from that group may survive. Use an outer process or container
227
+ supervisor when complete descendant ownership is required on macOS.
228
+
229
+ > **Safety note:** `LittleGhost::Sandboxes::Unrestricted` is suitable for
230
+ > application commands and tests that you would already run directly. It
231
+ > validates paths and bounds output, but it does not isolate the command from
232
+ > the host. Use `:native` for generated commands in production.
233
+
234
+ ## Keep process ownership explicit
235
+
236
+ `Sandbox#start_program` returns a `Sandbox::ProcessSession` with bounded input
237
+ and output, `alive?`, bounded `wait(timeout:)`, `terminate`, and `close`. The
238
+ session starts the command in a process group so cleanup can stop it and its
239
+ ordinary descendants together. It applies available CPU, memory, file, and
240
+ output limits, requests termination, and then forces termination when needed.
241
+
242
+ Callers that open a ProcessSession own it and must close it. LittleGhost fails
243
+ closed when it cannot supervise a configured memory limit. The parent samples
244
+ the visible process tree every 100 milliseconds, so it reacts only to memory
245
+ present at a sample and may miss peaks between samples. On Linux, three
246
+ consecutive failures to read the root process or the `/proc` snapshot end the
247
+ process. Use an outer cgroup or container when memory must be enforced as a hard
248
+ limit by the operating system.
249
+
250
+ A Run closes the Workspace and Sandbox it creates after success, failure, a
251
+ partial response, or cancellation. Application-supplied instances remain
252
+ caller-owned.
253
+
254
+ ## Configure child-process networking separately
255
+
256
+ Sandbox networking has three modes:
257
+
258
+ - `:none` removes child networking.
259
+ - `:inherit` permits the selected Sandbox backend's ordinary network access.
260
+ - `:allowlist` requires an enforcing gateway: a supervised proxy that permits
261
+ only configured destinations.
262
+
263
+ Proxy environment variables alone are not an allowlist. An external gateway
264
+ uses named, process-only Workspace paths and verifies that they still point to
265
+ the configured directories; it does not create a virtual path mapping.
266
+ LittleGhost does not attest that an external proxy is ready or enforcing the
267
+ declared destinations. The application must supervise and verify that
268
+ gateway before giving a child network access.
269
+
270
+ These settings reach only processes launched through the Sandbox. Providers,
271
+ callbacks, and application Tool code still use the Ruby process's network
272
+ access.
273
+
274
+ Test the deployed kernel and filesystem, not only the configuration object.
275
+ Exercise denied reads and writes, runtime paths, child creation, direct sockets,
276
+ cancellation, limits, and cleanup before depending on the Sandbox to isolate
277
+ generated code.
278
+
279
+ Continue with [Code Mode](code_mode.md) to see how a model-authored interpreter
280
+ uses this setup while every Tool call stays in the parent Ruby process. For
281
+ exact setup, access, and cleanup behavior, see `LittleGhost::Workspace`,
282
+ `LittleGhost::Sandbox`, and `LittleGhost::Sandbox::Scope`.