robot_lab 0.2.6 → 0.2.8
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.
- checksums.yaml +4 -4
- data/.envrc +4 -0
- data/.loki +5 -0
- data/Archspec.rb +44 -0
- data/CHANGELOG.md +17 -1
- data/README.md +124 -64
- data/Rakefile +6 -111
- data/_typos.toml +21 -0
- data/docs/api/core/index.md +41 -15
- data/docs/api/core/memory.md +247 -29
- data/docs/api/core/network.md +255 -33
- data/docs/api/core/result.md +120 -32
- data/docs/api/core/robot.md +551 -61
- data/docs/api/core/state.md +87 -197
- data/docs/api/core/tool.md +165 -20
- data/docs/api/errors.md +110 -17
- data/docs/api/hooks.md +469 -0
- data/docs/api/index.md +80 -7
- data/docs/api/mcp/client.md +129 -35
- data/docs/api/mcp/index.md +164 -23
- data/docs/api/mcp/server.md +27 -3
- data/docs/api/mcp/transports.md +94 -22
- data/docs/api/messages/index.md +26 -3
- data/docs/api/messages/text-message.md +33 -11
- data/docs/api/messages/tool-call-message.md +27 -4
- data/docs/api/messages/tool-result-message.md +23 -4
- data/docs/api/messages/user-message.md +45 -8
- data/docs/api/skills.md +423 -0
- data/docs/api/streaming/context.md +28 -5
- data/docs/api/streaming/index.md +57 -11
- data/docs/api/support.md +846 -0
- data/docs/architecture/core-concepts.md +79 -31
- data/docs/architecture/index.md +87 -11
- data/docs/architecture/message-flow.md +66 -29
- data/docs/architecture/network-orchestration.md +145 -38
- data/docs/architecture/robot-execution.md +172 -90
- data/docs/architecture/state-management.md +31 -12
- data/docs/concepts.md +176 -21
- data/docs/examples/basic-chat.md +72 -19
- data/docs/examples/index.md +117 -31
- data/docs/examples/mcp-server.md +154 -45
- data/docs/examples/multi-robot-network.md +91 -21
- data/docs/examples/tool-usage.md +104 -37
- data/docs/getting-started/configuration.md +288 -98
- data/docs/getting-started/installation.md +53 -41
- data/docs/getting-started/quick-start.md +51 -6
- data/docs/guides/building-robots.md +258 -50
- data/docs/guides/creating-networks.md +214 -30
- data/docs/guides/hooks.md +263 -54
- data/docs/guides/knowledge.md +35 -4
- data/docs/guides/mcp-integration.md +211 -44
- data/docs/guides/memory.md +103 -12
- data/docs/guides/observability.md +95 -47
- data/docs/guides/streaming.md +184 -125
- data/docs/guides/using-tools.md +247 -18
- data/docs/index.md +36 -4
- data/examples/01_simple_robot.rb +2 -2
- data/examples/02_tools.rb +14 -4
- data/examples/03_network.rb +12 -7
- data/examples/04_mcp.rb +11 -4
- data/examples/05_streaming.rb +8 -5
- data/examples/06_prompt_templates.rb +13 -9
- data/examples/07_network_memory.rb +5 -5
- data/examples/08_llm_config.rb +20 -15
- data/examples/09_chaining.rb +4 -4
- data/examples/11_network_introspection.rb +4 -4
- data/examples/12_message_bus.rb +2 -2
- data/examples/13_spawn.rb +2 -2
- data/examples/14_rusty_circuit/README.md +1 -0
- data/examples/14_rusty_circuit/comic.rb +7 -3
- data/examples/14_rusty_circuit/display.rb +14 -0
- data/examples/14_rusty_circuit/heckler.rb +8 -6
- data/examples/14_rusty_circuit/open_mic.rb +17 -6
- data/examples/14_rusty_circuit/scout.rb +17 -10
- data/examples/15_memory_network_and_bus/editorial_pipeline.rb +14 -10
- data/examples/15_memory_network_and_bus/linux_writer.rb +2 -2
- data/examples/15_memory_network_and_bus/os_editor.rb +3 -1
- data/examples/15_memory_network_and_bus/os_writer.rb +4 -1
- data/examples/16_writers_room/writer.rb +22 -22
- data/examples/16_writers_room/writers_room.rb +2 -0
- data/examples/17_skills.rb +14 -13
- data/examples/18_rails/README.md +20 -1
- data/examples/18_rails/app/controllers/chat_controller.rb +5 -1
- data/examples/18_rails/app/jobs/robot_run_job.rb +11 -5
- data/examples/18_rails/app/robots/chat_robot.rb +11 -0
- data/examples/18_rails/config/initializers/robot_lab.rb +8 -0
- data/examples/19_token_tracking.rb +25 -9
- data/examples/20_circuit_breaker.rb +10 -7
- data/examples/21_learning_loop.rb +42 -16
- data/examples/22_context_compression.rb +23 -23
- data/examples/23_convergence.rb +24 -17
- data/examples/24_structured_delegation.rb +13 -8
- data/examples/25_history_search.rb +12 -8
- data/examples/27_incident_response/incident_response.rb +31 -13
- data/examples/28_mcp_discovery.rb +17 -13
- data/examples/29_ractor_tools.rb +4 -2
- data/examples/30_ractor_network.rb +22 -17
- data/examples/31_launch_assessment.rb +20 -9
- data/examples/32_newsletter_reader.rb +7 -2
- data/examples/33_stock_predictor.rb +34 -13
- data/examples/34_agentskills.rb +7 -3
- data/examples/35_hooks.rb +18 -8
- data/examples/README.md +199 -45
- data/examples/common.rb +79 -11
- data/examples/xyzzy.rb +8 -1
- data/lib/robot_lab/agent_skill_catalog.rb +1 -0
- data/lib/robot_lab/ask_user.rb +2 -0
- data/lib/robot_lab/bus_poller.rb +2 -0
- data/lib/robot_lab/capabilities.rb +4 -0
- data/lib/robot_lab/config.rb +18 -5
- data/lib/robot_lab/doom_loop_detector.rb +6 -3
- data/lib/robot_lab/history_compressor.rb +5 -0
- data/lib/robot_lab/hook.rb +1 -0
- data/lib/robot_lab/hook_context.rb +4 -0
- data/lib/robot_lab/hook_registry.rb +1 -0
- data/lib/robot_lab/hooks.rb +6 -3
- data/lib/robot_lab/mcp/client.rb +2 -2
- data/lib/robot_lab/mcp/connection_poller.rb +16 -8
- data/lib/robot_lab/mcp/server_discovery.rb +1 -0
- data/lib/robot_lab/mcp/transports/sse.rb +3 -0
- data/lib/robot_lab/mcp/transports/stdio.rb +5 -0
- data/lib/robot_lab/mcp/transports/streamable_http.rb +4 -0
- data/lib/robot_lab/mcp/transports/websocket.rb +3 -0
- data/lib/robot_lab/memory.rb +23 -6
- data/lib/robot_lab/memory_change.rb +1 -0
- data/lib/robot_lab/message.rb +3 -0
- data/lib/robot_lab/names.rb +400 -0
- data/lib/robot_lab/network.rb +13 -6
- data/lib/robot_lab/robot/agent_skill_matching.rb +3 -3
- data/lib/robot_lab/robot/bus_messaging.rb +19 -8
- data/lib/robot_lab/robot/history_search.rb +2 -0
- data/lib/robot_lab/robot/hooking.rb +3 -0
- data/lib/robot_lab/robot/mcp_management.rb +11 -3
- data/lib/robot_lab/robot/template_rendering.rb +29 -7
- data/lib/robot_lab/robot.rb +81 -19
- data/lib/robot_lab/robot_result.rb +3 -0
- data/lib/robot_lab/run_config.rb +5 -0
- data/lib/robot_lab/script_tool.rb +20 -39
- data/lib/robot_lab/state_proxy.rb +1 -0
- data/lib/robot_lab/streaming/context.rb +1 -0
- data/lib/robot_lab/streaming/events.rb +1 -0
- data/lib/robot_lab/task.rb +2 -0
- data/lib/robot_lab/tool.rb +4 -0
- data/lib/robot_lab/user_message.rb +1 -0
- data/lib/robot_lab/utils.rb +2 -0
- data/lib/robot_lab/version.rb +1 -1
- data/lib/robot_lab/waiter.rb +3 -0
- data/lib/robot_lab.rb +27 -18
- data/mkdocs.yml +6 -1
- metadata +12 -7
- data/lib/robot_lab/sandbox/null.rb +0 -13
- data/lib/robot_lab/sandbox/seatbelt.rb +0 -104
- data/lib/robot_lab/sandbox.rb +0 -52
data/lib/robot_lab.rb
CHANGED
|
@@ -24,9 +24,9 @@ require 'typed_bus'
|
|
|
24
24
|
#
|
|
25
25
|
# @example Creating a network of robots
|
|
26
26
|
# network = RobotLab.create_network(name: "pipeline") do
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
#
|
|
27
|
+
# task :analyzer, analyzer, depends_on: :none
|
|
28
|
+
# task :writer, writer, depends_on: [:analyzer]
|
|
29
|
+
# task :reviewer, reviewer, depends_on: [:writer]
|
|
30
30
|
# end
|
|
31
31
|
# result = network.run(message: "Process this document")
|
|
32
32
|
#
|
|
@@ -35,7 +35,7 @@ require 'typed_bus'
|
|
|
35
35
|
# # ROBOT_LAB_DEFAULT_MODEL=gpt-4
|
|
36
36
|
# # ROBOT_LAB_RUBY_LLM__ANTHROPIC_API_KEY=sk-ant-...
|
|
37
37
|
#
|
|
38
|
-
# # Or via config files (~/.config/robot_lab/
|
|
38
|
+
# # Or via config files (~/.config/robot_lab/robot_lab.yml or ./config/robot_lab.yml)
|
|
39
39
|
# # See lib/robot_lab/config/defaults.yml for all options
|
|
40
40
|
#
|
|
41
41
|
# # Access configuration values:
|
|
@@ -47,6 +47,7 @@ end
|
|
|
47
47
|
|
|
48
48
|
loader = Zeitwerk::Loader.for_gem(warn_on_extra_files: false)
|
|
49
49
|
loader.ignore("#{__dir__}/robot_lab/robot")
|
|
50
|
+
loader.ignore("#{__dir__}/robot_lab/names.rb")
|
|
50
51
|
|
|
51
52
|
# Custom inflections for classes that don't follow Zeitwerk naming conventions
|
|
52
53
|
loader.inflector.inflect(
|
|
@@ -71,6 +72,11 @@ require_relative 'robot_lab/hooks'
|
|
|
71
72
|
require_relative 'robot_lab/message'
|
|
72
73
|
require_relative 'robot_lab/memory'
|
|
73
74
|
|
|
75
|
+
# names.rb defines the NAMES data constant plus helper methods directly on
|
|
76
|
+
# RobotLab, not a RobotLab::Names class/module, so Zeitwerk can't autoload it
|
|
77
|
+
# by convention — ignored above and required explicitly instead.
|
|
78
|
+
require_relative 'robot_lab/names'
|
|
79
|
+
|
|
74
80
|
# Eager load everything in Rails or when explicitly requested.
|
|
75
81
|
# Otherwise Zeitwerk's lazy autoloading keeps boot fast.
|
|
76
82
|
loader.eager_load if defined?(Rails::Engine) || ENV["ROBOT_LAB_EAGER_LOAD"]
|
|
@@ -126,14 +132,15 @@ module RobotLab
|
|
|
126
132
|
end
|
|
127
133
|
|
|
128
134
|
def with_hook_scope(registries, per_run_hooks)
|
|
129
|
-
|
|
130
|
-
|
|
135
|
+
thread = Thread.current
|
|
136
|
+
previous = thread[:robot_lab_hook_scope]
|
|
137
|
+
thread[:robot_lab_hook_scope] = {
|
|
131
138
|
registries: registries,
|
|
132
139
|
per_run_hooks: per_run_hooks
|
|
133
140
|
}
|
|
134
141
|
yield
|
|
135
142
|
ensure
|
|
136
|
-
|
|
143
|
+
thread[:robot_lab_hook_scope] = previous
|
|
137
144
|
end
|
|
138
145
|
|
|
139
146
|
def current_hook_scope
|
|
@@ -145,7 +152,7 @@ module RobotLab
|
|
|
145
152
|
# Configuration is automatically loaded from:
|
|
146
153
|
# - Bundled defaults (lib/robot_lab/config/defaults.yml)
|
|
147
154
|
# - Environment-specific overrides (development, test, production)
|
|
148
|
-
# - XDG config
|
|
155
|
+
# - XDG config file (~/.config/robot_lab/robot_lab.yml)
|
|
149
156
|
# - Project config (./config/robot_lab.yml)
|
|
150
157
|
# - Environment variables (ROBOT_LAB_*)
|
|
151
158
|
#
|
|
@@ -227,6 +234,7 @@ module RobotLab
|
|
|
227
234
|
# name: "helper",
|
|
228
235
|
# system_prompt: "You are a helpful assistant."
|
|
229
236
|
# )
|
|
237
|
+
# :reek:LongParameterList :reek:BooleanParameter -- documented factory API mirroring Robot#initialize; enable_cache is a feature toggle.
|
|
230
238
|
def build(name: "robot", template: nil, system_prompt: nil, context: {}, enable_cache: true, bus: nil, skills: nil,
|
|
231
239
|
config: nil, **)
|
|
232
240
|
Robot.new(
|
|
@@ -246,28 +254,28 @@ module RobotLab
|
|
|
246
254
|
#
|
|
247
255
|
# @param name [String] the unique identifier for the network
|
|
248
256
|
# @param concurrency [Symbol] concurrency model (:auto, :threads, :async)
|
|
249
|
-
# @yield Block for defining pipeline
|
|
257
|
+
# @yield Block for defining pipeline tasks (the DSL method is `task`)
|
|
250
258
|
# @return [Network] a new Network instance
|
|
251
259
|
#
|
|
252
260
|
# @example Sequential pipeline
|
|
253
261
|
# network = RobotLab.create_network(name: "pipeline") do
|
|
254
|
-
#
|
|
255
|
-
#
|
|
262
|
+
# task :first, robot1, depends_on: :none
|
|
263
|
+
# task :second, robot2, depends_on: [:first]
|
|
256
264
|
# end
|
|
257
265
|
#
|
|
258
266
|
# @example With optional routing
|
|
259
267
|
# network = RobotLab.create_network(name: "support") do
|
|
260
|
-
#
|
|
261
|
-
#
|
|
262
|
-
#
|
|
268
|
+
# task :classifier, classifier, depends_on: :none
|
|
269
|
+
# task :billing, billing_robot, depends_on: :optional
|
|
270
|
+
# task :technical, technical_robot, depends_on: :optional
|
|
263
271
|
# end
|
|
264
272
|
#
|
|
265
273
|
# @example Parallel execution
|
|
266
274
|
# network = RobotLab.create_network(name: "analysis") do
|
|
267
|
-
#
|
|
268
|
-
#
|
|
269
|
-
#
|
|
270
|
-
#
|
|
275
|
+
# task :fetch, fetcher, depends_on: :none
|
|
276
|
+
# task :sentiment, sentiment_bot, depends_on: [:fetch]
|
|
277
|
+
# task :entities, entity_bot, depends_on: [:fetch]
|
|
278
|
+
# task :merge, merger, depends_on: [:sentiment, :entities]
|
|
271
279
|
# end
|
|
272
280
|
def create_network(name:, concurrency: :auto, config: nil, &)
|
|
273
281
|
Network.new(name: name, concurrency: concurrency, config: config, &)
|
|
@@ -289,6 +297,7 @@ module RobotLab
|
|
|
289
297
|
#
|
|
290
298
|
# @example Memory with caching disabled
|
|
291
299
|
# memory = RobotLab.create_memory(data: {}, enable_cache: false)
|
|
300
|
+
# :reek:BooleanParameter -- enable_cache is a documented feature toggle forwarded to Memory.new.
|
|
292
301
|
def create_memory(data: {}, enable_cache: true, **)
|
|
293
302
|
Memory.new(data: data, enable_cache: enable_cache, **)
|
|
294
303
|
end
|
data/mkdocs.yml
CHANGED
|
@@ -177,10 +177,15 @@ nav:
|
|
|
177
177
|
- Core Classes:
|
|
178
178
|
- api/core/index.md
|
|
179
179
|
- Robot: api/core/robot.md
|
|
180
|
+
- RobotResult: api/core/result.md
|
|
180
181
|
- Network: api/core/network.md
|
|
181
|
-
- State: api/core/state.md
|
|
182
182
|
- Tool: api/core/tool.md
|
|
183
183
|
- Memory: api/core/memory.md
|
|
184
|
+
- StateProxy: api/core/state.md
|
|
185
|
+
- Hooks: api/hooks.md
|
|
186
|
+
- Skills: api/skills.md
|
|
187
|
+
- Support Classes: api/support.md
|
|
188
|
+
- Errors: api/errors.md
|
|
184
189
|
- Messages:
|
|
185
190
|
- api/messages/index.md
|
|
186
191
|
- UserMessage: api/messages/user-message.md
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: robot_lab
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.8
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dewayne VanHoozer
|
|
@@ -197,8 +197,9 @@ description: |
|
|
|
197
197
|
memory; networks for coordinating multiple robots with intelligent routing; MCP (Model
|
|
198
198
|
Context Protocol) integration for external tool servers; and a memory system with Redis
|
|
199
199
|
backend and semantic caching. Optional gems add Rails integration (robot_lab-rails),
|
|
200
|
-
durable learning (robot_lab-durable), Ractor concurrency (robot_lab-ractor),
|
|
201
|
-
|
|
200
|
+
durable learning (robot_lab-durable), Ractor concurrency (robot_lab-ractor), document
|
|
201
|
+
storage (robot_lab-document_store), and OS-level skill-script confinement
|
|
202
|
+
(robot_lab-sandbox). Core itself imposes no execution limitations.
|
|
202
203
|
email:
|
|
203
204
|
- dvanhoozer@gmail.com
|
|
204
205
|
executables: []
|
|
@@ -218,12 +219,14 @@ files:
|
|
|
218
219
|
- ".loki"
|
|
219
220
|
- ".quality/reek_baseline.txt"
|
|
220
221
|
- ".rubocop.yml"
|
|
222
|
+
- Archspec.rb
|
|
221
223
|
- CHANGELOG.md
|
|
222
224
|
- CLAUDE.md
|
|
223
225
|
- COMMITS.md
|
|
224
226
|
- LICENSE.txt
|
|
225
227
|
- README.md
|
|
226
228
|
- Rakefile
|
|
229
|
+
- _typos.toml
|
|
227
230
|
- agent2agent_review.md
|
|
228
231
|
- agentf_improvements.md
|
|
229
232
|
- agents.md
|
|
@@ -235,6 +238,7 @@ files:
|
|
|
235
238
|
- docs/api/core/state.md
|
|
236
239
|
- docs/api/core/tool.md
|
|
237
240
|
- docs/api/errors.md
|
|
241
|
+
- docs/api/hooks.md
|
|
238
242
|
- docs/api/index.md
|
|
239
243
|
- docs/api/mcp/client.md
|
|
240
244
|
- docs/api/mcp/index.md
|
|
@@ -245,9 +249,11 @@ files:
|
|
|
245
249
|
- docs/api/messages/tool-call-message.md
|
|
246
250
|
- docs/api/messages/tool-result-message.md
|
|
247
251
|
- docs/api/messages/user-message.md
|
|
252
|
+
- docs/api/skills.md
|
|
248
253
|
- docs/api/streaming/context.md
|
|
249
254
|
- docs/api/streaming/events.md
|
|
250
255
|
- docs/api/streaming/index.md
|
|
256
|
+
- docs/api/support.md
|
|
251
257
|
- docs/architecture/core-concepts.md
|
|
252
258
|
- docs/architecture/index.md
|
|
253
259
|
- docs/architecture/message-flow.md
|
|
@@ -295,6 +301,7 @@ files:
|
|
|
295
301
|
- examples/13_spawn.rb
|
|
296
302
|
- examples/14_rusty_circuit/.envrc
|
|
297
303
|
- examples/14_rusty_circuit/.gitignore
|
|
304
|
+
- examples/14_rusty_circuit/README.md
|
|
298
305
|
- examples/14_rusty_circuit/comic.rb
|
|
299
306
|
- examples/14_rusty_circuit/display.rb
|
|
300
307
|
- examples/14_rusty_circuit/heckler.rb
|
|
@@ -473,6 +480,7 @@ files:
|
|
|
473
480
|
- lib/robot_lab/memory.rb
|
|
474
481
|
- lib/robot_lab/memory_change.rb
|
|
475
482
|
- lib/robot_lab/message.rb
|
|
483
|
+
- lib/robot_lab/names.rb
|
|
476
484
|
- lib/robot_lab/narrator.rb
|
|
477
485
|
- lib/robot_lab/network.rb
|
|
478
486
|
- lib/robot_lab/robot.rb
|
|
@@ -487,9 +495,6 @@ files:
|
|
|
487
495
|
- lib/robot_lab/robot_result.rb
|
|
488
496
|
- lib/robot_lab/run_config.rb
|
|
489
497
|
- lib/robot_lab/runnable.rb
|
|
490
|
-
- lib/robot_lab/sandbox.rb
|
|
491
|
-
- lib/robot_lab/sandbox/null.rb
|
|
492
|
-
- lib/robot_lab/sandbox/seatbelt.rb
|
|
493
498
|
- lib/robot_lab/script_tool.rb
|
|
494
499
|
- lib/robot_lab/state_proxy.rb
|
|
495
500
|
- lib/robot_lab/streaming/context.rb
|
|
@@ -536,7 +541,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
536
541
|
- !ruby/object:Gem::Version
|
|
537
542
|
version: '0'
|
|
538
543
|
requirements: []
|
|
539
|
-
rubygems_version: 4.0.
|
|
544
|
+
rubygems_version: 4.0.20
|
|
540
545
|
specification_version: 4
|
|
541
546
|
summary: Ruby framework for building and orchestrating multi-robot LLM workflows
|
|
542
547
|
test_files: []
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module RobotLab
|
|
4
|
-
module Sandbox
|
|
5
|
-
# Passthrough strategy: runs the command with no confinement. Used when
|
|
6
|
-
# sandboxing is unavailable (non-macOS) or unnecessary (trust: core).
|
|
7
|
-
class Null
|
|
8
|
-
def wrap(cmd) = cmd
|
|
9
|
-
|
|
10
|
-
def cleanup; end
|
|
11
|
-
end
|
|
12
|
-
end
|
|
13
|
-
end
|
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "tempfile"
|
|
4
|
-
|
|
5
|
-
module RobotLab
|
|
6
|
-
module Sandbox
|
|
7
|
-
# macOS strategy: generates a deny-by-default sandbox-exec profile from the
|
|
8
|
-
# effective grant and wraps the command as
|
|
9
|
-
# sandbox-exec -f <profile> <cmd...>
|
|
10
|
-
#
|
|
11
|
-
# The profile allows process exec plus reads of system locations (so the
|
|
12
|
-
# interpreter can load), reads of the skill bundle and granted paths, writes
|
|
13
|
-
# only to granted paths (and the standard /dev sinks), and network only when
|
|
14
|
-
# granted. Everything else -- notably $HOME, so SSH/cloud credentials --
|
|
15
|
-
# is denied. Interpreters installed under $HOME (e.g. rbenv) are not visible;
|
|
16
|
-
# declare them in fs_read or mark the skill trust: core.
|
|
17
|
-
class Seatbelt
|
|
18
|
-
# System locations a typical interpreter needs to read to start up.
|
|
19
|
-
SYSTEM_READ = %w[/usr /bin /sbin /System /Library /opt /private/etc /dev /var/select].freeze
|
|
20
|
-
DEV_WRITE = %w[/dev/null /dev/stdout /dev/stderr /dev/dtracehelper /dev/tty].freeze
|
|
21
|
-
|
|
22
|
-
def initialize(grant, skill_dir:)
|
|
23
|
-
@grant = grant
|
|
24
|
-
@skill_dir = File.expand_path(skill_dir.to_s)
|
|
25
|
-
@profile = nil
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def wrap(cmd)
|
|
29
|
-
@profile = write_profile
|
|
30
|
-
["sandbox-exec", "-f", @profile, *cmd]
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def cleanup
|
|
34
|
-
File.unlink(@profile) if @profile && File.exist?(@profile)
|
|
35
|
-
rescue StandardError
|
|
36
|
-
nil
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
# The generated Seatbelt profile text (public for testing).
|
|
40
|
-
def profile_text
|
|
41
|
-
reads = canonicalize(SYSTEM_READ + [@skill_dir] + @grant.fs_read)
|
|
42
|
-
writes = canonicalize(@grant.fs_write)
|
|
43
|
-
lines = [
|
|
44
|
-
"(version 1)",
|
|
45
|
-
# bsd.sb supplies the base rules a process needs to start (dyld, mach
|
|
46
|
-
# bootstrap, etc.); without it a deny-default profile aborts the binary.
|
|
47
|
-
'(import "bsd.sb")',
|
|
48
|
-
"(deny default)",
|
|
49
|
-
"(allow process-fork)",
|
|
50
|
-
"(allow process-exec)",
|
|
51
|
-
"(allow sysctl-read)",
|
|
52
|
-
"(allow mach-lookup)",
|
|
53
|
-
# Metadata (stat/lookup) on any path so the interpreter can traverse to
|
|
54
|
-
# granted files; reading file *contents* stays restricted below.
|
|
55
|
-
"(allow file-read-metadata)",
|
|
56
|
-
read_rule(reads),
|
|
57
|
-
write_rule(DEV_WRITE.map { |p| [:literal, p] } + writes.map { |p| [:subpath, p] })
|
|
58
|
-
]
|
|
59
|
-
lines << "(allow network*)" if @grant.network
|
|
60
|
-
"#{lines.compact.join("\n")}\n"
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
private
|
|
64
|
-
|
|
65
|
-
def write_profile
|
|
66
|
-
file = Tempfile.create(["robot_lab-sandbox-", ".sb"])
|
|
67
|
-
file.write(profile_text)
|
|
68
|
-
file.close
|
|
69
|
-
file.path
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
# Resolve to the real (symlink-free) path the kernel matches against. macOS
|
|
73
|
-
# symlinks /tmp -> /private/tmp, /var -> /private/var, etc., so logical
|
|
74
|
-
# paths would never match. For not-yet-existing write targets, resolve the
|
|
75
|
-
# nearest existing ancestor and re-append the remainder.
|
|
76
|
-
def canonicalize(paths)
|
|
77
|
-
Array(paths).map { |p| real_path(File.expand_path(p.to_s)) }.compact.uniq
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
def real_path(expanded)
|
|
81
|
-
existing = expanded
|
|
82
|
-
rest = []
|
|
83
|
-
until File.exist?(existing) || existing == "/"
|
|
84
|
-
rest.unshift(File.basename(existing))
|
|
85
|
-
existing = File.dirname(existing)
|
|
86
|
-
end
|
|
87
|
-
real = File.realpath(existing)
|
|
88
|
-
rest.empty? ? real : File.join(real, *rest)
|
|
89
|
-
rescue StandardError
|
|
90
|
-
expanded
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
def read_rule(paths)
|
|
94
|
-
subpaths = paths.map { |p| "(subpath #{p.inspect})" }.join(" ")
|
|
95
|
-
"(allow file-read* #{subpaths})"
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
def write_rule(entries)
|
|
99
|
-
clauses = entries.map { |kind, p| "(#{kind} #{p.inspect})" }.join(" ")
|
|
100
|
-
"(allow file-write* #{clauses})"
|
|
101
|
-
end
|
|
102
|
-
end
|
|
103
|
-
end
|
|
104
|
-
end
|
data/lib/robot_lab/sandbox.rb
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module RobotLab
|
|
4
|
-
# Confines skill-script execution to a granted set of capabilities.
|
|
5
|
-
#
|
|
6
|
-
# Sandboxing is opt-in (config.sandbox.enabled). When off, every script runs
|
|
7
|
-
# unconfined exactly as before. When on, each script runs under a strategy:
|
|
8
|
-
#
|
|
9
|
-
# - {Sandbox::Seatbelt} on macOS — a generated deny-by-default sandbox-exec
|
|
10
|
-
# profile derived from the effective {Capabilities} grant.
|
|
11
|
-
# - {Sandbox::Null} elsewhere (or for +trust: core+ skills) — a passthrough.
|
|
12
|
-
#
|
|
13
|
-
# OS-level confinement is therefore best-effort and platform-specific; it is
|
|
14
|
-
# never a hard dependency.
|
|
15
|
-
module Sandbox
|
|
16
|
-
module_function
|
|
17
|
-
|
|
18
|
-
# @return [Boolean] whether sandboxing is turned on in config
|
|
19
|
-
def enabled?(config = RobotLab.config)
|
|
20
|
-
config.respond_to?(:sandbox) && config.sandbox && config.sandbox.enabled == true
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def macos?
|
|
24
|
-
RUBY_PLATFORM.include?("darwin")
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
# Pick a strategy for the given (already-intersected) grant.
|
|
28
|
-
#
|
|
29
|
-
# @param grant [Capabilities] effective grant
|
|
30
|
-
# @param skill_dir [String] skill bundle root, always granted read access
|
|
31
|
-
# @param macos [Boolean] whether to use the macOS strategy; defaults to the
|
|
32
|
-
# real platform check and is injectable so both branches are testable on
|
|
33
|
-
# any host without stubbing.
|
|
34
|
-
# @return [#wrap, #cleanup]
|
|
35
|
-
def for(grant, skill_dir:, macos: macos?)
|
|
36
|
-
return Null.new if grant.core?
|
|
37
|
-
return Seatbelt.new(grant, skill_dir: skill_dir) if macos
|
|
38
|
-
|
|
39
|
-
warn_once_non_macos
|
|
40
|
-
Null.new
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
def warn_once_non_macos
|
|
44
|
-
return if @warned_non_macos
|
|
45
|
-
|
|
46
|
-
@warned_non_macos = true
|
|
47
|
-
RobotLab.config.logger.warn(
|
|
48
|
-
"Sandbox: OS-level confinement is only available on macOS; scripts run unconfined here"
|
|
49
|
-
)
|
|
50
|
-
end
|
|
51
|
-
end
|
|
52
|
-
end
|