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.
Files changed (153) hide show
  1. checksums.yaml +4 -4
  2. data/.envrc +4 -0
  3. data/.loki +5 -0
  4. data/Archspec.rb +44 -0
  5. data/CHANGELOG.md +17 -1
  6. data/README.md +124 -64
  7. data/Rakefile +6 -111
  8. data/_typos.toml +21 -0
  9. data/docs/api/core/index.md +41 -15
  10. data/docs/api/core/memory.md +247 -29
  11. data/docs/api/core/network.md +255 -33
  12. data/docs/api/core/result.md +120 -32
  13. data/docs/api/core/robot.md +551 -61
  14. data/docs/api/core/state.md +87 -197
  15. data/docs/api/core/tool.md +165 -20
  16. data/docs/api/errors.md +110 -17
  17. data/docs/api/hooks.md +469 -0
  18. data/docs/api/index.md +80 -7
  19. data/docs/api/mcp/client.md +129 -35
  20. data/docs/api/mcp/index.md +164 -23
  21. data/docs/api/mcp/server.md +27 -3
  22. data/docs/api/mcp/transports.md +94 -22
  23. data/docs/api/messages/index.md +26 -3
  24. data/docs/api/messages/text-message.md +33 -11
  25. data/docs/api/messages/tool-call-message.md +27 -4
  26. data/docs/api/messages/tool-result-message.md +23 -4
  27. data/docs/api/messages/user-message.md +45 -8
  28. data/docs/api/skills.md +423 -0
  29. data/docs/api/streaming/context.md +28 -5
  30. data/docs/api/streaming/index.md +57 -11
  31. data/docs/api/support.md +846 -0
  32. data/docs/architecture/core-concepts.md +79 -31
  33. data/docs/architecture/index.md +87 -11
  34. data/docs/architecture/message-flow.md +66 -29
  35. data/docs/architecture/network-orchestration.md +145 -38
  36. data/docs/architecture/robot-execution.md +172 -90
  37. data/docs/architecture/state-management.md +31 -12
  38. data/docs/concepts.md +176 -21
  39. data/docs/examples/basic-chat.md +72 -19
  40. data/docs/examples/index.md +117 -31
  41. data/docs/examples/mcp-server.md +154 -45
  42. data/docs/examples/multi-robot-network.md +91 -21
  43. data/docs/examples/tool-usage.md +104 -37
  44. data/docs/getting-started/configuration.md +288 -98
  45. data/docs/getting-started/installation.md +53 -41
  46. data/docs/getting-started/quick-start.md +51 -6
  47. data/docs/guides/building-robots.md +258 -50
  48. data/docs/guides/creating-networks.md +214 -30
  49. data/docs/guides/hooks.md +263 -54
  50. data/docs/guides/knowledge.md +35 -4
  51. data/docs/guides/mcp-integration.md +211 -44
  52. data/docs/guides/memory.md +103 -12
  53. data/docs/guides/observability.md +95 -47
  54. data/docs/guides/streaming.md +184 -125
  55. data/docs/guides/using-tools.md +247 -18
  56. data/docs/index.md +36 -4
  57. data/examples/01_simple_robot.rb +2 -2
  58. data/examples/02_tools.rb +14 -4
  59. data/examples/03_network.rb +12 -7
  60. data/examples/04_mcp.rb +11 -4
  61. data/examples/05_streaming.rb +8 -5
  62. data/examples/06_prompt_templates.rb +13 -9
  63. data/examples/07_network_memory.rb +5 -5
  64. data/examples/08_llm_config.rb +20 -15
  65. data/examples/09_chaining.rb +4 -4
  66. data/examples/11_network_introspection.rb +4 -4
  67. data/examples/12_message_bus.rb +2 -2
  68. data/examples/13_spawn.rb +2 -2
  69. data/examples/14_rusty_circuit/README.md +1 -0
  70. data/examples/14_rusty_circuit/comic.rb +7 -3
  71. data/examples/14_rusty_circuit/display.rb +14 -0
  72. data/examples/14_rusty_circuit/heckler.rb +8 -6
  73. data/examples/14_rusty_circuit/open_mic.rb +17 -6
  74. data/examples/14_rusty_circuit/scout.rb +17 -10
  75. data/examples/15_memory_network_and_bus/editorial_pipeline.rb +14 -10
  76. data/examples/15_memory_network_and_bus/linux_writer.rb +2 -2
  77. data/examples/15_memory_network_and_bus/os_editor.rb +3 -1
  78. data/examples/15_memory_network_and_bus/os_writer.rb +4 -1
  79. data/examples/16_writers_room/writer.rb +22 -22
  80. data/examples/16_writers_room/writers_room.rb +2 -0
  81. data/examples/17_skills.rb +14 -13
  82. data/examples/18_rails/README.md +20 -1
  83. data/examples/18_rails/app/controllers/chat_controller.rb +5 -1
  84. data/examples/18_rails/app/jobs/robot_run_job.rb +11 -5
  85. data/examples/18_rails/app/robots/chat_robot.rb +11 -0
  86. data/examples/18_rails/config/initializers/robot_lab.rb +8 -0
  87. data/examples/19_token_tracking.rb +25 -9
  88. data/examples/20_circuit_breaker.rb +10 -7
  89. data/examples/21_learning_loop.rb +42 -16
  90. data/examples/22_context_compression.rb +23 -23
  91. data/examples/23_convergence.rb +24 -17
  92. data/examples/24_structured_delegation.rb +13 -8
  93. data/examples/25_history_search.rb +12 -8
  94. data/examples/27_incident_response/incident_response.rb +31 -13
  95. data/examples/28_mcp_discovery.rb +17 -13
  96. data/examples/29_ractor_tools.rb +4 -2
  97. data/examples/30_ractor_network.rb +22 -17
  98. data/examples/31_launch_assessment.rb +20 -9
  99. data/examples/32_newsletter_reader.rb +7 -2
  100. data/examples/33_stock_predictor.rb +34 -13
  101. data/examples/34_agentskills.rb +7 -3
  102. data/examples/35_hooks.rb +18 -8
  103. data/examples/README.md +199 -45
  104. data/examples/common.rb +79 -11
  105. data/examples/xyzzy.rb +8 -1
  106. data/lib/robot_lab/agent_skill_catalog.rb +1 -0
  107. data/lib/robot_lab/ask_user.rb +2 -0
  108. data/lib/robot_lab/bus_poller.rb +2 -0
  109. data/lib/robot_lab/capabilities.rb +4 -0
  110. data/lib/robot_lab/config.rb +18 -5
  111. data/lib/robot_lab/doom_loop_detector.rb +6 -3
  112. data/lib/robot_lab/history_compressor.rb +5 -0
  113. data/lib/robot_lab/hook.rb +1 -0
  114. data/lib/robot_lab/hook_context.rb +4 -0
  115. data/lib/robot_lab/hook_registry.rb +1 -0
  116. data/lib/robot_lab/hooks.rb +6 -3
  117. data/lib/robot_lab/mcp/client.rb +2 -2
  118. data/lib/robot_lab/mcp/connection_poller.rb +16 -8
  119. data/lib/robot_lab/mcp/server_discovery.rb +1 -0
  120. data/lib/robot_lab/mcp/transports/sse.rb +3 -0
  121. data/lib/robot_lab/mcp/transports/stdio.rb +5 -0
  122. data/lib/robot_lab/mcp/transports/streamable_http.rb +4 -0
  123. data/lib/robot_lab/mcp/transports/websocket.rb +3 -0
  124. data/lib/robot_lab/memory.rb +23 -6
  125. data/lib/robot_lab/memory_change.rb +1 -0
  126. data/lib/robot_lab/message.rb +3 -0
  127. data/lib/robot_lab/names.rb +400 -0
  128. data/lib/robot_lab/network.rb +13 -6
  129. data/lib/robot_lab/robot/agent_skill_matching.rb +3 -3
  130. data/lib/robot_lab/robot/bus_messaging.rb +19 -8
  131. data/lib/robot_lab/robot/history_search.rb +2 -0
  132. data/lib/robot_lab/robot/hooking.rb +3 -0
  133. data/lib/robot_lab/robot/mcp_management.rb +11 -3
  134. data/lib/robot_lab/robot/template_rendering.rb +29 -7
  135. data/lib/robot_lab/robot.rb +81 -19
  136. data/lib/robot_lab/robot_result.rb +3 -0
  137. data/lib/robot_lab/run_config.rb +5 -0
  138. data/lib/robot_lab/script_tool.rb +20 -39
  139. data/lib/robot_lab/state_proxy.rb +1 -0
  140. data/lib/robot_lab/streaming/context.rb +1 -0
  141. data/lib/robot_lab/streaming/events.rb +1 -0
  142. data/lib/robot_lab/task.rb +2 -0
  143. data/lib/robot_lab/tool.rb +4 -0
  144. data/lib/robot_lab/user_message.rb +1 -0
  145. data/lib/robot_lab/utils.rb +2 -0
  146. data/lib/robot_lab/version.rb +1 -1
  147. data/lib/robot_lab/waiter.rb +3 -0
  148. data/lib/robot_lab.rb +27 -18
  149. data/mkdocs.yml +6 -1
  150. metadata +12 -7
  151. data/lib/robot_lab/sandbox/null.rb +0 -13
  152. data/lib/robot_lab/sandbox/seatbelt.rb +0 -104
  153. 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
- # step :analyzer, analyzer, depends_on: :none
28
- # step :writer, writer, depends_on: [:analyzer]
29
- # step :reviewer, reviewer, depends_on: [:writer]
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/config.yml or ./config/robot_lab.yml)
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
- previous = Thread.current[:robot_lab_hook_scope]
130
- Thread.current[:robot_lab_hook_scope] = {
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
- Thread.current[:robot_lab_hook_scope] = previous
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 files (~/.config/robot_lab/config.yml)
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 steps
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
- # step :first, robot1, depends_on: :none
255
- # step :second, robot2, depends_on: [:first]
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
- # step :classifier, classifier, depends_on: :none
261
- # step :billing, billing_robot, depends_on: :optional
262
- # step :technical, technical_robot, depends_on: :optional
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
- # step :fetch, fetcher, depends_on: :none
268
- # step :sentiment, sentiment_bot, depends_on: [:fetch]
269
- # step :entities, entity_bot, depends_on: [:fetch]
270
- # step :merge, merger, depends_on: [:sentiment, :entities]
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.6
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), and
201
- document storage (robot_lab-document_store).
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.17
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
@@ -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