robot_lab 0.2.7 → 0.3.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 (101) hide show
  1. checksums.yaml +4 -4
  2. data/.envrc +6 -2
  3. data/.loki +5 -0
  4. data/Archspec.rb +44 -0
  5. data/CHANGELOG.md +70 -1
  6. data/CLAUDE.md +4 -2
  7. data/README.md +15 -17
  8. data/Rakefile +6 -111
  9. data/_typos.toml +21 -0
  10. data/docs/api/index.md +2 -2
  11. data/docs/api/skills.md +53 -149
  12. data/docs/api/support.md +1 -1
  13. data/docs/architecture/core-concepts.md +1 -1
  14. data/docs/architecture/index.md +5 -4
  15. data/docs/architecture/state-management.md +4 -4
  16. data/docs/concepts.md +1 -1
  17. data/docs/getting-started/configuration.md +4 -1
  18. data/docs/getting-started/installation.md +1 -4
  19. data/docs/guides/hooks.md +122 -0
  20. data/docs/guides/memory.md +2 -2
  21. data/docs/guides/using-tools.md +10 -1
  22. data/examples/.envrc +2 -0
  23. data/examples/02_tools.rb +8 -8
  24. data/examples/03_network.rb +1 -1
  25. data/examples/04_mcp.rb +7 -5
  26. data/examples/08_llm_config.rb +5 -5
  27. data/examples/09_chaining.rb +3 -3
  28. data/examples/14_rusty_circuit/comic.rb +8 -8
  29. data/examples/14_rusty_circuit/scout.rb +4 -4
  30. data/examples/15_memory_network_and_bus/README.md +66 -0
  31. data/examples/15_memory_network_and_bus/output/combined_article.md +5 -7
  32. data/examples/15_memory_network_and_bus/output/final_article.md +5 -10
  33. data/examples/15_memory_network_and_bus/output/linux_draft.md +3 -3
  34. data/examples/15_memory_network_and_bus/output/mac_draft.md +3 -3
  35. data/examples/15_memory_network_and_bus/output/memory.json +6 -6
  36. data/examples/15_memory_network_and_bus/output/revision_1.md +21 -10
  37. data/examples/15_memory_network_and_bus/output/revision_2.md +43 -6
  38. data/examples/15_memory_network_and_bus/output/revision_3.md +8 -0
  39. data/examples/15_memory_network_and_bus/output/windows_draft.md +3 -3
  40. data/examples/16_writers_room/tools.rb +14 -14
  41. data/examples/19_token_tracking.rb +2 -2
  42. data/examples/20_circuit_breaker.rb +3 -3
  43. data/examples/22_context_compression.rb +1 -1
  44. data/examples/27_incident_response/README.md +65 -0
  45. data/examples/28_mcp_discovery.rb +2 -2
  46. data/examples/29_ractor_tools.rb +4 -4
  47. data/examples/30_ractor_network.rb +2 -2
  48. data/examples/33_stock_predictor.rb +8 -8
  49. data/examples/35_hooks.rb +3 -3
  50. data/examples/README.md +17 -0
  51. data/examples/common.rb +55 -23
  52. data/examples/run_all.rb +60 -0
  53. data/lib/robot_lab/agent_skill_catalog.rb +1 -0
  54. data/lib/robot_lab/ask_user.rb +5 -3
  55. data/lib/robot_lab/bus_poller.rb +2 -0
  56. data/lib/robot_lab/capabilities.rb +4 -0
  57. data/lib/robot_lab/config/defaults.yml +5 -5
  58. data/lib/robot_lab/config.rb +10 -2
  59. data/lib/robot_lab/doom_loop_detector.rb +6 -3
  60. data/lib/robot_lab/history_compressor.rb +5 -0
  61. data/lib/robot_lab/hook.rb +1 -0
  62. data/lib/robot_lab/hook_context.rb +4 -0
  63. data/lib/robot_lab/hook_registry.rb +1 -0
  64. data/lib/robot_lab/hooks.rb +6 -3
  65. data/lib/robot_lab/mcp/client.rb +2 -2
  66. data/lib/robot_lab/mcp/connection_poller.rb +16 -8
  67. data/lib/robot_lab/mcp/server_discovery.rb +1 -0
  68. data/lib/robot_lab/mcp/transports/sse.rb +3 -0
  69. data/lib/robot_lab/mcp/transports/stdio.rb +5 -0
  70. data/lib/robot_lab/mcp/transports/streamable_http.rb +5 -3
  71. data/lib/robot_lab/mcp/transports/websocket.rb +3 -0
  72. data/lib/robot_lab/memory.rb +39 -13
  73. data/lib/robot_lab/memory_change.rb +1 -0
  74. data/lib/robot_lab/message.rb +3 -0
  75. data/lib/robot_lab/names.rb +2 -4
  76. data/lib/robot_lab/network.rb +13 -6
  77. data/lib/robot_lab/robot/agent_skill_matching.rb +2 -0
  78. data/lib/robot_lab/robot/bus_messaging.rb +3 -0
  79. data/lib/robot_lab/robot/history_search.rb +2 -0
  80. data/lib/robot_lab/robot/hooking.rb +29 -0
  81. data/lib/robot_lab/robot/mcp_management.rb +11 -3
  82. data/lib/robot_lab/robot/result_building.rb +119 -0
  83. data/lib/robot_lab/robot/template_rendering.rb +13 -4
  84. data/lib/robot_lab/robot.rb +88 -130
  85. data/lib/robot_lab/robot_result.rb +3 -0
  86. data/lib/robot_lab/run_config.rb +57 -20
  87. data/lib/robot_lab/script_tool.rb +20 -39
  88. data/lib/robot_lab/state_proxy.rb +1 -0
  89. data/lib/robot_lab/streaming/context.rb +1 -0
  90. data/lib/robot_lab/streaming/events.rb +1 -0
  91. data/lib/robot_lab/task.rb +2 -0
  92. data/lib/robot_lab/tool.rb +11 -12
  93. data/lib/robot_lab/user_message.rb +1 -0
  94. data/lib/robot_lab/utils.rb +2 -0
  95. data/lib/robot_lab/version.rb +1 -1
  96. data/lib/robot_lab/waiter.rb +3 -0
  97. data/lib/robot_lab.rb +9 -6
  98. metadata +23 -32
  99. data/lib/robot_lab/sandbox/null.rb +0 -13
  100. data/lib/robot_lab/sandbox/seatbelt.rb +0 -104
  101. data/lib/robot_lab/sandbox.rb +0 -52
@@ -2,7 +2,6 @@
2
2
 
3
3
  require 'open3'
4
4
  require 'shellwords'
5
- require 'timeout'
6
5
 
7
6
  module RobotLab
8
7
  # Factory module for wrapping AgentSkills scripts as RobotLab::Tool instances.
@@ -10,7 +9,18 @@ module RobotLab
10
9
  # Given a path to an executable shell script, produces a Tool that shells
11
10
  # out to the script and returns its combined stdout+stderr output.
12
11
  # Non-executable scripts return nil with a logged warning.
12
+ #
13
+ # Core has no sandboxing of its own: by default every script runs unconfined
14
+ # with no timeout. An extension gem (e.g. robot_lab-sandbox) can install a
15
+ # confinement strategy by setting {.executor} to an object responding to
16
+ # +call(cmd, capabilities:, skill_dir:)+; when set, ScriptTool.execute
17
+ # delegates to it instead of running the command directly.
13
18
  module ScriptTool
19
+ class << self
20
+ # @return [#call, nil] optional executor installed by an extension gem
21
+ attr_accessor :executor
22
+ end
23
+
14
24
  # Wrap a script file as a RobotLab::Tool.
15
25
  #
16
26
  # @param script_path [String, Pathname] path to the script file
@@ -19,6 +29,8 @@ module RobotLab
19
29
  # @param capabilities [Capabilities, nil] declared capabilities (from SKILL.md)
20
30
  # @param skill_dir [String, nil] skill bundle root (defaults to the script's dir)
21
31
  # @return [RobotLab::Tool, nil] nil if the script is not executable
32
+ # :reek:ControlParameter -- `capabilities || ...` and `skill_dir || ...` are nil-safe defaults, not behavior selection.
33
+ # :reek:TooManyStatements -- linear derive/validate/build factory; the closure needs every derived local.
22
34
  def self.from_path(script_path, capabilities: nil, skill_dir: nil)
23
35
  path = Pathname.new(script_path)
24
36
 
@@ -52,49 +64,18 @@ module RobotLab
52
64
  end
53
65
  end
54
66
 
55
- # Run a command, optionally confined by the sandbox and a timeout.
67
+ # Run a command, delegating to the installed {.executor} if one is present.
56
68
  #
57
- # When sandboxing is disabled (the default) this is the original, unconfined
58
- # capture2e path with no timeout — behaviour is unchanged. When enabled, the
59
- # command is wrapped by the sandbox strategy for the effective grant and
60
- # bounded by the grant's timeout.
69
+ # With no executor installed (the default core has no sandboxing), this
70
+ # is a plain, unconfined capture2e path with no timeout. An extension gem
71
+ # that sets {.executor} controls confinement and timeout behavior entirely.
61
72
  #
62
73
  # @return [String] combined stdout+stderr, or an error string on failure
63
74
  def self.execute(cmd, capabilities:, skill_dir:)
64
- unless Sandbox.enabled?
65
- output, status = Open3.capture2e(*cmd)
66
- return format_result(output, status)
67
- end
75
+ return executor.call(cmd, capabilities: capabilities, skill_dir: skill_dir) if executor
68
76
 
69
- grant = capabilities.intersect(Capabilities.ceiling)
70
- sandbox = Sandbox.for(grant, skill_dir: skill_dir)
71
- begin
72
- output, status = run_with_timeout(sandbox.wrap(cmd), grant.timeout)
73
- format_result(output, status)
74
- ensure
75
- sandbox.cleanup
76
- end
77
- end
78
-
79
- # @return [Array(String, Process::Status|nil)] output and status (nil = timed out)
80
- def self.run_with_timeout(cmd, timeout)
81
- Open3.popen2e(*cmd, pgroup: true) do |stdin, out, wait|
82
- stdin.close
83
- output = +''
84
- begin
85
- Timeout.timeout(timeout) { output << out.read }
86
- rescue Timeout::Error
87
- terminate(wait.pid)
88
- return ["#{output}\n[killed: exceeded #{timeout}s]", nil]
89
- end
90
- [output, wait.value]
91
- end
92
- end
93
-
94
- def self.terminate(pid)
95
- Process.kill('-TERM', Process.getpgid(pid))
96
- rescue StandardError
97
- nil
77
+ output, status = Open3.capture2e(*cmd)
78
+ format_result(output, status)
98
79
  end
99
80
 
100
81
  # @param status [Process::Status, nil] nil indicates a timeout kill
@@ -147,6 +147,7 @@ module RobotLab
147
147
 
148
148
  # Respond to method calls as hash access
149
149
  #
150
+ # :reek:BooleanParameter -- include_private is Ruby's respond_to_missing? contract signature.
150
151
  def respond_to_missing?(method_name, include_private = false)
151
152
  key = method_name.to_s.chomp("=").to_sym
152
153
  @data.key?(key) || super
@@ -36,6 +36,7 @@ module RobotLab
36
36
  # @param publish [Proc] callback for publishing events
37
37
  # @param parent_run_id [String, nil] parent run identifier
38
38
  # @param sequence_counter [SequenceCounter, nil] shared sequence counter
39
+ # :reek:ControlParameter -- `sequence_counter || SequenceCounter.new` is a nil-safe default.
39
40
  def initialize(run_id:, message_id:, scope:, publish:, parent_run_id: nil, sequence_counter: nil)
40
41
  @run_id = run_id
41
42
  @parent_run_id = parent_run_id
@@ -7,6 +7,7 @@ module RobotLab
7
7
  # Defines the structure and types of events emitted during
8
8
  # robot and network execution.
9
9
  #
10
+ # :reek:TooManyConstants -- deliberately a flat catalog of every streaming event name.
10
11
  module Events
11
12
  # Run lifecycle events
12
13
  RUN_STARTED = "run.started"
@@ -89,6 +89,7 @@ module RobotLab
89
89
 
90
90
  private
91
91
 
92
+ # :reek:TooManyStatements -- one guarded assignment per task-level override injected into run params.
92
93
  def enhanced_result(result)
93
94
  run_params = deep_merge(result.context[:run_params] || {}, @context)
94
95
  run_params[:mcp] = @mcp unless @mcp == :none
@@ -114,6 +115,7 @@ module RobotLab
114
115
  # @param override [Hash] the overriding hash
115
116
  # @return [Hash] the merged result
116
117
  #
118
+ # :reek:FeatureEnvy -- pure recursive hash merge of its two arguments; it has no useful self.
117
119
  def deep_merge(base, override)
118
120
  base = base.transform_keys(&:to_sym)
119
121
  override = override.transform_keys(&:to_sym)
@@ -9,7 +9,7 @@ module RobotLab
9
9
  #
10
10
  # class GetWeather < RobotLab::Tool
11
11
  # description "Get weather for a location"
12
- # param :location, type: "string", desc: "City name"
12
+ # parameter :location, type: "string", description: "City name"
13
13
  #
14
14
  # def execute(location:)
15
15
  # WeatherService.fetch(location)
@@ -27,6 +27,8 @@ module RobotLab
27
27
  # enabling tools that modify their robot's state (temperature,
28
28
  # system prompt, spawning, etc.).
29
29
  #
30
+ # :reek:InstanceVariableAssumption -- @custom_name/@mcp are set by Tool.create via instance_variable_set,
31
+ # @raise_on_error/@ractor_safe are class-level DSL ivars; every read is defined?-guarded.
30
32
  class Tool < RubyLLM::Tool
31
33
  # @!attribute [rw] robot
32
34
  # @return [Robot, nil] the robot that owns this tool
@@ -91,9 +93,11 @@ module RobotLab
91
93
  #
92
94
  # For non-Ractor-safe tools: runs execute directly in the calling thread.
93
95
  #
96
+ # @param tool_call [RubyLLM::ToolCall, nil] the originating tool call (supplied by the chat)
94
97
  # @param args [Hash] the tool arguments from the LLM
95
98
  # @return [Object] the tool result or an error string
96
- def call(args)
99
+ # :reek:TooManyStatements -- hook-wrapped dispatch with per-error-class handling; the rescue clauses are the method.
100
+ def call(tool_call: nil, **args)
97
101
  context = ToolCallHookContext.new(tool: self, tool_args: args, robot: @robot)
98
102
 
99
103
  RobotLab::Hooks.run(:tool_call, context, **tool_hook_options) do
@@ -145,6 +149,7 @@ module RobotLab
145
149
  # mcp: "brave_search"
146
150
  # ) { |args| mcp_client.call_tool("search", args) }
147
151
  #
152
+ # :reek:TooManyStatements -- anonymous-class factory: DSL application and instance wiring form one linear build.
148
153
  def self.create(name:, description: nil, parameters: nil, mcp: nil, robot: nil, &handler)
149
154
  desc_text = description
150
155
  params_hash = parameters
@@ -153,15 +158,9 @@ module RobotLab
153
158
  tool_class = Class.new(self) do
154
159
  description(desc_text) if desc_text
155
160
 
156
- if params_hash.is_a?(Hash) && params_hash[:properties]
157
- required_list = Array(params_hash[:required]).map(&:to_s)
158
- params_hash[:properties].each do |pname, pdef|
159
- param pname.to_sym,
160
- type: pdef[:type] || "string",
161
- desc: pdef[:description],
162
- required: required_list.include?(pname.to_s)
163
- end
164
- end
161
+ # ruby_llm 2.0's parameters() accepts a raw JSON Schema hash, so the
162
+ # full schema (including nested items/objects) survives verbatim.
163
+ parameters(params_hash) if params_hash.is_a?(Hash) && params_hash[:properties]
165
164
 
166
165
  define_method(:execute) do |**args|
167
166
  block.call(args)
@@ -179,7 +178,7 @@ module RobotLab
179
178
  #
180
179
  # @return [Hash] JSON Schema representation
181
180
  def to_json_schema
182
- schema = params_schema || { "type" => "object", "properties" => {}, "required" => [] }
181
+ schema = parameters_schema || { "type" => "object", "properties" => {}, "required" => [] }
183
182
  {
184
183
  name: name,
185
184
  description: description,
@@ -41,6 +41,7 @@ module RobotLab
41
41
  # @param system_prompt [String, nil] additional system prompt
42
42
  # @param metadata [Hash, nil] additional metadata
43
43
  # @param id [String, nil] unique identifier (defaults to UUID)
44
+ # :reek:ControlParameter -- `metadata || {}` and `id || SecureRandom.uuid` are nil-safe defaults.
44
45
  def initialize(content, session_id: nil, system_prompt: nil, metadata: nil, id: nil)
45
46
  @content = content.to_s
46
47
  @session_id = session_id
@@ -13,6 +13,7 @@ module RobotLab
13
13
  # When already inside an Async reactor, creates a child task.
14
14
  # Otherwise, creates a temporary reactor that runs the block
15
15
  # and cleans up automatically.
16
+ # :reek:FeatureEnvy -- formatting the rescued exception for the log line; there is nothing else to reference.
16
17
  def dispatch_async(&block)
17
18
  Async do
18
19
  block.call
@@ -25,6 +26,7 @@ module RobotLab
25
26
  #
26
27
  # @param obj [Object] the object to duplicate
27
28
  # @return [Object] the deep copy
29
+ # :reek:FeatureEnvy -- pure recursive copy of its argument; it has no useful self.
28
30
  def deep_dup(obj)
29
31
  case obj
30
32
  when Hash
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RobotLab
4
- VERSION = "0.2.7"
4
+ VERSION = "0.3.0"
5
5
  end
@@ -28,6 +28,9 @@ module RobotLab
28
28
  # @param timeout [Numeric, nil] maximum seconds to wait (nil = indefinite)
29
29
  # @return [Object, :timeout] the signaled value, or :timeout if timed out
30
30
  #
31
+ # :reek:TooManyStatements -- register/block/drain steps of the wake-pipe protocol are one atomic unit.
32
+ # :reek:DuplicateMethodCall -- the synchronize blocks are deliberately separate: the lock must be released
33
+ # around the blocking wait_readable.
31
34
  def wait(timeout: nil)
32
35
  @mutex.synchronize do
33
36
  return @value if @signaled
data/lib/robot_lab.rb CHANGED
@@ -39,7 +39,7 @@ require 'typed_bus'
39
39
  # # See lib/robot_lab/config/defaults.yml for all options
40
40
  #
41
41
  # # Access configuration values:
42
- # RobotLab.config.ruby_llm.model #=> "claude-sonnet-4"
42
+ # RobotLab.config.ruby_llm.model #=> "claude-sonnet-4-6"
43
43
  # RobotLab.config.ruby_llm.request_timeout #=> 120
44
44
  #
45
45
  module RobotLab
@@ -132,14 +132,15 @@ module RobotLab
132
132
  end
133
133
 
134
134
  def with_hook_scope(registries, per_run_hooks)
135
- previous = Thread.current[:robot_lab_hook_scope]
136
- Thread.current[:robot_lab_hook_scope] = {
135
+ thread = Thread.current
136
+ previous = thread[:robot_lab_hook_scope]
137
+ thread[:robot_lab_hook_scope] = {
137
138
  registries: registries,
138
139
  per_run_hooks: per_run_hooks
139
140
  }
140
141
  yield
141
142
  ensure
142
- Thread.current[:robot_lab_hook_scope] = previous
143
+ thread[:robot_lab_hook_scope] = previous
143
144
  end
144
145
 
145
146
  def current_hook_scope
@@ -158,7 +159,7 @@ module RobotLab
158
159
  # @return [Config] the config instance
159
160
  #
160
161
  # @example
161
- # RobotLab.config.ruby_llm.model #=> "claude-sonnet-4"
162
+ # RobotLab.config.ruby_llm.model #=> "claude-sonnet-4-6"
162
163
  # RobotLab.config.ruby_llm.request_timeout #=> 120
163
164
  # RobotLab.config.development? #=> true
164
165
  def config
@@ -172,7 +173,7 @@ module RobotLab
172
173
  #
173
174
  # @example
174
175
  # RobotLab.configure do |c|
175
- # c.default_model = "claude-sonnet-4"
176
+ # c.default_model = "claude-sonnet-4-6"
176
177
  # end
177
178
  def configure
178
179
  yield config
@@ -233,6 +234,7 @@ module RobotLab
233
234
  # name: "helper",
234
235
  # system_prompt: "You are a helpful assistant."
235
236
  # )
237
+ # :reek:LongParameterList :reek:BooleanParameter -- documented factory API mirroring Robot#initialize; enable_cache is a feature toggle.
236
238
  def build(name: "robot", template: nil, system_prompt: nil, context: {}, enable_cache: true, bus: nil, skills: nil,
237
239
  config: nil, **)
238
240
  Robot.new(
@@ -295,6 +297,7 @@ module RobotLab
295
297
  #
296
298
  # @example Memory with caching disabled
297
299
  # memory = RobotLab.create_memory(data: {}, enable_cache: false)
300
+ # :reek:BooleanParameter -- enable_cache is a documented feature toggle forwarded to Memory.new.
298
301
  def create_memory(data: {}, enable_cache: true, **)
299
302
  Memory.new(data: data, enable_cache: enable_cache, **)
300
303
  end
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.7
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dewayne VanHoozer
@@ -43,70 +43,56 @@ dependencies:
43
43
  requirements:
44
44
  - - "~>"
45
45
  - !ruby/object:Gem::Version
46
- version: '1.12'
47
- type: :runtime
48
- prerelease: false
49
- version_requirements: !ruby/object:Gem::Requirement
50
- requirements:
51
- - - "~>"
52
- - !ruby/object:Gem::Version
53
- version: '1.12'
54
- - !ruby/object:Gem::Dependency
55
- name: ruby_llm-mcp
56
- requirement: !ruby/object:Gem::Requirement
57
- requirements:
58
- - - "~>"
59
- - !ruby/object:Gem::Version
60
- version: '1.0'
46
+ version: '2.0'
61
47
  type: :runtime
62
48
  prerelease: false
63
49
  version_requirements: !ruby/object:Gem::Requirement
64
50
  requirements:
65
51
  - - "~>"
66
52
  - !ruby/object:Gem::Version
67
- version: '1.0'
53
+ version: '2.0'
68
54
  - !ruby/object:Gem::Dependency
69
- name: prompt_manager
55
+ name: ruby_llm-providers-apfel
70
56
  requirement: !ruby/object:Gem::Requirement
71
57
  requirements:
72
58
  - - "~>"
73
59
  - !ruby/object:Gem::Version
74
- version: '1.0'
60
+ version: 0.2.1
75
61
  type: :runtime
76
62
  prerelease: false
77
63
  version_requirements: !ruby/object:Gem::Requirement
78
64
  requirements:
79
65
  - - "~>"
80
66
  - !ruby/object:Gem::Version
81
- version: '1.0'
67
+ version: 0.2.1
82
68
  - !ruby/object:Gem::Dependency
83
- name: ruby_llm-schema
69
+ name: ruby_llm-providers-lms
84
70
  requirement: !ruby/object:Gem::Requirement
85
71
  requirements:
86
72
  - - "~>"
87
73
  - !ruby/object:Gem::Version
88
- version: '0.3'
74
+ version: 0.2.1
89
75
  type: :runtime
90
76
  prerelease: false
91
77
  version_requirements: !ruby/object:Gem::Requirement
92
78
  requirements:
93
79
  - - "~>"
94
80
  - !ruby/object:Gem::Version
95
- version: '0.3'
81
+ version: 0.2.1
96
82
  - !ruby/object:Gem::Dependency
97
- name: ruby_llm-semantic_cache
83
+ name: prompt_manager
98
84
  requirement: !ruby/object:Gem::Requirement
99
85
  requirements:
100
86
  - - "~>"
101
87
  - !ruby/object:Gem::Version
102
- version: '0.1'
88
+ version: '1.0'
103
89
  type: :runtime
104
90
  prerelease: false
105
91
  version_requirements: !ruby/object:Gem::Requirement
106
92
  requirements:
107
93
  - - "~>"
108
94
  - !ruby/object:Gem::Version
109
- version: '0.1'
95
+ version: '1.0'
110
96
  - !ruby/object:Gem::Dependency
111
97
  name: async
112
98
  requirement: !ruby/object:Gem::Requirement
@@ -197,8 +183,9 @@ description: |
197
183
  memory; networks for coordinating multiple robots with intelligent routing; MCP (Model
198
184
  Context Protocol) integration for external tool servers; and a memory system with Redis
199
185
  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).
186
+ durable learning (robot_lab-durable), Ractor concurrency (robot_lab-ractor), document
187
+ storage (robot_lab-document_store), and OS-level skill-script confinement
188
+ (robot_lab-sandbox). Core itself imposes no execution limitations.
202
189
  email:
203
190
  - dvanhoozer@gmail.com
204
191
  executables: []
@@ -218,12 +205,14 @@ files:
218
205
  - ".loki"
219
206
  - ".quality/reek_baseline.txt"
220
207
  - ".rubocop.yml"
208
+ - Archspec.rb
221
209
  - CHANGELOG.md
222
210
  - CLAUDE.md
223
211
  - COMMITS.md
224
212
  - LICENSE.txt
225
213
  - README.md
226
214
  - Rakefile
215
+ - _typos.toml
227
216
  - agent2agent_review.md
228
217
  - agentf_improvements.md
229
218
  - agents.md
@@ -309,6 +298,7 @@ files:
309
298
  - examples/14_rusty_circuit/scout.rb
310
299
  - examples/14_rusty_circuit/show.log
311
300
  - examples/15_memory_network_and_bus/.envrc
301
+ - examples/15_memory_network_and_bus/README.md
312
302
  - examples/15_memory_network_and_bus/editor_in_chief.rb
313
303
  - examples/15_memory_network_and_bus/editorial_pipeline.rb
314
304
  - examples/15_memory_network_and_bus/linux_writer.rb
@@ -321,6 +311,7 @@ files:
321
311
  - examples/15_memory_network_and_bus/output/memory.json
322
312
  - examples/15_memory_network_and_bus/output/revision_1.md
323
313
  - examples/15_memory_network_and_bus/output/revision_2.md
314
+ - examples/15_memory_network_and_bus/output/revision_3.md
324
315
  - examples/15_memory_network_and_bus/output/windows_draft.md
325
316
  - examples/15_memory_network_and_bus/prompts/os_advocate.md
326
317
  - examples/15_memory_network_and_bus/prompts/os_chief.md
@@ -384,6 +375,7 @@ files:
384
375
  - examples/26_document_store/postgres_runbook.md
385
376
  - examples/26_document_store/redis_caching_guide.md
386
377
  - examples/26_document_store/sidekiq_guide.md
378
+ - examples/27_incident_response/README.md
387
379
  - examples/27_incident_response/incident_response.rb
388
380
  - examples/28_mcp_discovery.rb
389
381
  - examples/29_ractor_tools.rb
@@ -445,6 +437,7 @@ files:
445
437
  - examples/prompts/template_with_skills_test.md
446
438
  - examples/prompts/triage.md
447
439
  - examples/ruboruby.md
440
+ - examples/run_all.rb
448
441
  - examples/xyzzy.rb
449
442
  - lib/robot_lab.rb
450
443
  - lib/robot_lab/agent_skill.rb
@@ -487,14 +480,12 @@ files:
487
480
  - lib/robot_lab/robot/history_search.rb
488
481
  - lib/robot_lab/robot/hooking.rb
489
482
  - lib/robot_lab/robot/mcp_management.rb
483
+ - lib/robot_lab/robot/result_building.rb
490
484
  - lib/robot_lab/robot/template_rendering.rb
491
485
  - lib/robot_lab/robot_message.rb
492
486
  - lib/robot_lab/robot_result.rb
493
487
  - lib/robot_lab/run_config.rb
494
488
  - lib/robot_lab/runnable.rb
495
- - lib/robot_lab/sandbox.rb
496
- - lib/robot_lab/sandbox/null.rb
497
- - lib/robot_lab/sandbox/seatbelt.rb
498
489
  - lib/robot_lab/script_tool.rb
499
490
  - lib/robot_lab/state_proxy.rb
500
491
  - lib/robot_lab/streaming/context.rb
@@ -541,7 +532,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
541
532
  - !ruby/object:Gem::Version
542
533
  version: '0'
543
534
  requirements: []
544
- rubygems_version: 4.0.19
535
+ rubygems_version: 4.0.21
545
536
  specification_version: 4
546
537
  summary: Ruby framework for building and orchestrating multi-robot LLM workflows
547
538
  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