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.
- checksums.yaml +4 -4
- data/.envrc +6 -2
- data/.loki +5 -0
- data/Archspec.rb +44 -0
- data/CHANGELOG.md +70 -1
- data/CLAUDE.md +4 -2
- data/README.md +15 -17
- data/Rakefile +6 -111
- data/_typos.toml +21 -0
- data/docs/api/index.md +2 -2
- data/docs/api/skills.md +53 -149
- data/docs/api/support.md +1 -1
- data/docs/architecture/core-concepts.md +1 -1
- data/docs/architecture/index.md +5 -4
- data/docs/architecture/state-management.md +4 -4
- data/docs/concepts.md +1 -1
- data/docs/getting-started/configuration.md +4 -1
- data/docs/getting-started/installation.md +1 -4
- data/docs/guides/hooks.md +122 -0
- data/docs/guides/memory.md +2 -2
- data/docs/guides/using-tools.md +10 -1
- data/examples/.envrc +2 -0
- data/examples/02_tools.rb +8 -8
- data/examples/03_network.rb +1 -1
- data/examples/04_mcp.rb +7 -5
- data/examples/08_llm_config.rb +5 -5
- data/examples/09_chaining.rb +3 -3
- data/examples/14_rusty_circuit/comic.rb +8 -8
- data/examples/14_rusty_circuit/scout.rb +4 -4
- data/examples/15_memory_network_and_bus/README.md +66 -0
- data/examples/15_memory_network_and_bus/output/combined_article.md +5 -7
- data/examples/15_memory_network_and_bus/output/final_article.md +5 -10
- data/examples/15_memory_network_and_bus/output/linux_draft.md +3 -3
- data/examples/15_memory_network_and_bus/output/mac_draft.md +3 -3
- data/examples/15_memory_network_and_bus/output/memory.json +6 -6
- data/examples/15_memory_network_and_bus/output/revision_1.md +21 -10
- data/examples/15_memory_network_and_bus/output/revision_2.md +43 -6
- data/examples/15_memory_network_and_bus/output/revision_3.md +8 -0
- data/examples/15_memory_network_and_bus/output/windows_draft.md +3 -3
- data/examples/16_writers_room/tools.rb +14 -14
- data/examples/19_token_tracking.rb +2 -2
- data/examples/20_circuit_breaker.rb +3 -3
- data/examples/22_context_compression.rb +1 -1
- data/examples/27_incident_response/README.md +65 -0
- data/examples/28_mcp_discovery.rb +2 -2
- data/examples/29_ractor_tools.rb +4 -4
- data/examples/30_ractor_network.rb +2 -2
- data/examples/33_stock_predictor.rb +8 -8
- data/examples/35_hooks.rb +3 -3
- data/examples/README.md +17 -0
- data/examples/common.rb +55 -23
- data/examples/run_all.rb +60 -0
- data/lib/robot_lab/agent_skill_catalog.rb +1 -0
- data/lib/robot_lab/ask_user.rb +5 -3
- data/lib/robot_lab/bus_poller.rb +2 -0
- data/lib/robot_lab/capabilities.rb +4 -0
- data/lib/robot_lab/config/defaults.yml +5 -5
- data/lib/robot_lab/config.rb +10 -2
- 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 +5 -3
- data/lib/robot_lab/mcp/transports/websocket.rb +3 -0
- data/lib/robot_lab/memory.rb +39 -13
- data/lib/robot_lab/memory_change.rb +1 -0
- data/lib/robot_lab/message.rb +3 -0
- data/lib/robot_lab/names.rb +2 -4
- data/lib/robot_lab/network.rb +13 -6
- data/lib/robot_lab/robot/agent_skill_matching.rb +2 -0
- data/lib/robot_lab/robot/bus_messaging.rb +3 -0
- data/lib/robot_lab/robot/history_search.rb +2 -0
- data/lib/robot_lab/robot/hooking.rb +29 -0
- data/lib/robot_lab/robot/mcp_management.rb +11 -3
- data/lib/robot_lab/robot/result_building.rb +119 -0
- data/lib/robot_lab/robot/template_rendering.rb +13 -4
- data/lib/robot_lab/robot.rb +88 -130
- data/lib/robot_lab/robot_result.rb +3 -0
- data/lib/robot_lab/run_config.rb +57 -20
- 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 +11 -12
- 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 +9 -6
- metadata +23 -32
- 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
|
@@ -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,
|
|
67
|
+
# Run a command, delegating to the installed {.executor} if one is present.
|
|
56
68
|
#
|
|
57
|
-
#
|
|
58
|
-
# capture2e path with no timeout
|
|
59
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
70
|
-
|
|
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"
|
data/lib/robot_lab/task.rb
CHANGED
|
@@ -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)
|
data/lib/robot_lab/tool.rb
CHANGED
|
@@ -9,7 +9,7 @@ module RobotLab
|
|
|
9
9
|
#
|
|
10
10
|
# class GetWeather < RobotLab::Tool
|
|
11
11
|
# description "Get weather for a location"
|
|
12
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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 =
|
|
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
|
data/lib/robot_lab/utils.rb
CHANGED
|
@@ -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
|
data/lib/robot_lab/version.rb
CHANGED
data/lib/robot_lab/waiter.rb
CHANGED
|
@@ -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
|
-
|
|
136
|
-
|
|
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
|
-
|
|
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.
|
|
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: '
|
|
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: '
|
|
53
|
+
version: '2.0'
|
|
68
54
|
- !ruby/object:Gem::Dependency
|
|
69
|
-
name:
|
|
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:
|
|
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:
|
|
67
|
+
version: 0.2.1
|
|
82
68
|
- !ruby/object:Gem::Dependency
|
|
83
|
-
name: ruby_llm-
|
|
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:
|
|
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:
|
|
81
|
+
version: 0.2.1
|
|
96
82
|
- !ruby/object:Gem::Dependency
|
|
97
|
-
name:
|
|
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
|
|
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
|
|
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),
|
|
201
|
-
|
|
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.
|
|
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
|
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
|