robot_lab 0.2.7 → 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/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/index.md +5 -4
- data/docs/getting-started/configuration.md +4 -1
- data/docs/guides/hooks.md +122 -0
- data/docs/guides/using-tools.md +10 -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 +8 -0
- 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 +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 +3 -0
- data/lib/robot_lab/robot/mcp_management.rb +11 -3
- data/lib/robot_lab/robot/template_rendering.rb +13 -4
- data/lib/robot_lab/robot.rb +58 -17
- 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 +6 -3
- metadata +7 -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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: de87b78206bc424703b465d3a1f236db723ea38fe4235c07413466ab5b469ead
|
|
4
|
+
data.tar.gz: 6a7db14e4530e615e258415491422f297567a2f640983035f1542d88e7d200e5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f6814c9a8b86b8e6d1df2adcc2d9b4ab84c176c67211424e8404e9d83c1e08ffd353587523e4c45efec295d0e47bbd731b0cdc6f3e62754cfd4f1ce2a33d7655
|
|
7
|
+
data.tar.gz: 37682af098203ae333cc83fa6e66a95f1883aae0bed8411524a05d6795bb47b4a99f231353fe3546b2fdae8a5c098d0f45b863e531edaf237aab33e4e1bc3859
|
data/.envrc
CHANGED
data/.loki
CHANGED
|
@@ -18,6 +18,11 @@ class Tasks
|
|
|
18
18
|
sh "ruby -Ilib:test \"#{path}\""
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
+
desc "Check architecture boundaries with ArchSpec"
|
|
22
|
+
def archspec
|
|
23
|
+
sh "bundle exec archspec check"
|
|
24
|
+
end
|
|
25
|
+
|
|
21
26
|
class DocsCommands < Tasks
|
|
22
27
|
desc "Build YARD + MkDocs documentation"
|
|
23
28
|
def build
|
data/Archspec.rb
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
component :robot, in: "lib/robot_lab/robot.rb"
|
|
2
|
+
component :robot_support, in: "lib/robot_lab/robot/**/*.rb"
|
|
3
|
+
component :network, in: ["lib/robot_lab/network.rb", "lib/robot_lab/bus_poller.rb"]
|
|
4
|
+
component :memory, in: ["lib/robot_lab/memory.rb", "lib/robot_lab/memory_change.rb"]
|
|
5
|
+
component :mcp, in: "lib/robot_lab/mcp/**/*.rb"
|
|
6
|
+
component :sandbox, in: ["lib/robot_lab/sandbox.rb", "lib/robot_lab/sandbox/**/*.rb"]
|
|
7
|
+
component :streaming, in: "lib/robot_lab/streaming/**/*.rb"
|
|
8
|
+
component :budget, in: "lib/robot_lab/budget/**/*.rb"
|
|
9
|
+
component :hooks, in: %w[
|
|
10
|
+
lib/robot_lab/hook.rb
|
|
11
|
+
lib/robot_lab/hook_context.rb
|
|
12
|
+
lib/robot_lab/hook_registry.rb
|
|
13
|
+
lib/robot_lab/hooks.rb
|
|
14
|
+
]
|
|
15
|
+
component :tools, in: %w[
|
|
16
|
+
lib/robot_lab/tool.rb
|
|
17
|
+
lib/robot_lab/tool_config.rb
|
|
18
|
+
lib/robot_lab/tool_manifest.rb
|
|
19
|
+
lib/robot_lab/script_tool.rb
|
|
20
|
+
]
|
|
21
|
+
component :config, in: ["lib/robot_lab/config.rb", "lib/robot_lab/run_config.rb"]
|
|
22
|
+
|
|
23
|
+
# NOTE: every file here reopens `module RobotLab`, so a `dependencies.forbid`
|
|
24
|
+
# rule keyed on components (e.g. `memory.cannot_use :robot`) treats a plain
|
|
25
|
+
# `RobotLab.config` call as "depends on every component" — every file
|
|
26
|
+
# "defines" the bare RobotLab constant. Naming the actual target constants
|
|
27
|
+
# instead of components sidesteps that ambiguity.
|
|
28
|
+
|
|
29
|
+
# Memory is a low-level primitive shared by Robot and Network; it must not
|
|
30
|
+
# depend back on the things that depend on it.
|
|
31
|
+
memory.cannot_reference_constants "RobotLab::Robot", "RobotLab::Network"
|
|
32
|
+
|
|
33
|
+
# MCP is an external-tool integration layer that Robot/Network consume; it
|
|
34
|
+
# must not reach back into the objects that use it.
|
|
35
|
+
mcp.cannot_reference_constants "RobotLab::Robot", "RobotLab::Network"
|
|
36
|
+
|
|
37
|
+
# Sandbox runs untrusted/generated code paths and should have the smallest
|
|
38
|
+
# possible surface area — no reaching into orchestration or memory internals.
|
|
39
|
+
sandbox.cannot_reference_constants "RobotLab::Robot", "RobotLab::Network", "RobotLab::Memory"
|
|
40
|
+
|
|
41
|
+
# Config/RunConfig sits at the bottom of the configuration cascade
|
|
42
|
+
# (RobotLab.config -> Network -> Robot -> template -> task -> runtime) and
|
|
43
|
+
# must not depend upward on the objects it configures.
|
|
44
|
+
config.cannot_reference_constants "RobotLab::Robot", "RobotLab::Network"
|
data/CHANGELOG.md
CHANGED
|
@@ -6,7 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
8
|
|
|
9
|
-
## [
|
|
9
|
+
## [0.2.8] - 2026-09-09
|
|
10
10
|
|
|
11
11
|
### Added
|
|
12
12
|
- `BusMessaging#respond_to_tasks` and `#serve` — a member auto-answers inbound
|
|
@@ -16,6 +16,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
16
16
|
without hand-wiring `on_message`.
|
|
17
17
|
- `.loki` Asgard task file: `test`, `rubocop`, `rubocop_fix`, `flog`, `flay`, `quality`, `build`, `install`, `release`, `integration`, `docs`, and `examples` tasks via the Asgard task runner
|
|
18
18
|
|
|
19
|
+
### Changed (BREAKING)
|
|
20
|
+
- **Sandboxing extracted to `robot_lab-sandbox`.** `RobotLab::Sandbox`,
|
|
21
|
+
`RobotLab::Sandbox::Seatbelt`, and `RobotLab::Sandbox::Null` no longer live
|
|
22
|
+
in core; they moved verbatim to the new `robot_lab-sandbox` gem. Core has no
|
|
23
|
+
execution limitations of its own: `ScriptTool.execute` now delegates to an
|
|
24
|
+
optional `ScriptTool.executor` (`nil` by default, in which case it runs the
|
|
25
|
+
unconfined `Open3.capture2e` path with no timeout exactly as before).
|
|
26
|
+
Applications that want confined skill-script execution must add
|
|
27
|
+
`gem "robot_lab-sandbox"` and `require "robot_lab/sandbox"`, which installs
|
|
28
|
+
itself as the executor; `config.sandbox.*` behavior is unchanged from there.
|
|
29
|
+
`RobotLab::Capabilities` (the declarative fs_read/fs_write/network/timeout/
|
|
30
|
+
trust manifest parsed from `SKILL.md`) stays in core since `AgentSkill` needs
|
|
31
|
+
it regardless of whether sandboxing is enforced.
|
|
32
|
+
- `ScriptTool.run_with_timeout` and `ScriptTool.terminate` moved to
|
|
33
|
+
`RobotLab::Sandbox::Executor` in `robot_lab-sandbox`.
|
|
34
|
+
|
|
19
35
|
### Fixed
|
|
20
36
|
- `BusMessaging` now synchronizes its message counter and outbox with a mutex, so
|
|
21
37
|
concurrent sends and poller-thread reply correlation can't clobber each other.
|
data/Rakefile
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
# Quality gates (quality, rubocop_check, flog_check, flay_check,
|
|
4
|
+
# archspec_check, ...), doc *serving*, and the gem lifecycle (build, install,
|
|
5
|
+
# release) live in asgard — see .loki and the shared dev/*.loki files it
|
|
6
|
+
# imports. This Rakefile keeps the tasks asgard delegates to (tests,
|
|
7
|
+
# integration, examples, docs:build).
|
|
5
8
|
|
|
6
|
-
|
|
9
|
+
require "rake/testtask"
|
|
7
10
|
|
|
8
11
|
Rake::TestTask.new(:test) do |t|
|
|
9
12
|
t.libs << "test"
|
|
@@ -36,100 +39,6 @@ Rake::TestTask.new(:integration) do |t|
|
|
|
36
39
|
t.ruby_opts << "-rtest_helper"
|
|
37
40
|
end
|
|
38
41
|
|
|
39
|
-
desc "Check code complexity with Flog (warn ≥20, fail ≥50)"
|
|
40
|
-
task :flog_check do
|
|
41
|
-
require 'flog'
|
|
42
|
-
|
|
43
|
-
# Target to work toward; methods above this are warned but don't fail the gate.
|
|
44
|
-
METHOD_WARN = 20.0
|
|
45
|
-
# Current baseline floor — established from first run. Reduce incrementally.
|
|
46
|
-
METHOD_FAIL = 50.0
|
|
47
|
-
|
|
48
|
-
flogger = Flog.new(all: true)
|
|
49
|
-
flogger.flog(*Dir.glob('lib/**/*.rb'))
|
|
50
|
-
|
|
51
|
-
warnings = []
|
|
52
|
-
failures = []
|
|
53
|
-
|
|
54
|
-
flogger.each_by_score do |method, score|
|
|
55
|
-
next if method.end_with?('#none') # skip file-level non-method code
|
|
56
|
-
if score > METHOD_FAIL
|
|
57
|
-
failures << "#{'%.1f' % score}: #{method}"
|
|
58
|
-
elsif score > METHOD_WARN
|
|
59
|
-
warnings << "#{'%.1f' % score}: #{method}"
|
|
60
|
-
end
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
unless warnings.empty?
|
|
64
|
-
puts "\nFlog warnings (#{METHOD_WARN}–#{METHOD_FAIL}) — target for future refactoring:"
|
|
65
|
-
warnings.each { |v| puts " #{v}" }
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
if failures.empty?
|
|
69
|
-
puts "\nFlog: no methods exceed the failure threshold (≥#{METHOD_FAIL})"
|
|
70
|
-
else
|
|
71
|
-
puts "\nFlog failures (≥#{METHOD_FAIL}) — must be refactored:"
|
|
72
|
-
failures.each { |v| puts " #{v}" }
|
|
73
|
-
abort "\nFlog quality gate failed: #{failures.size} method(s) exceed #{METHOD_FAIL}"
|
|
74
|
-
end
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
desc "Check for structural code duplication with Flay (mass >= 50)"
|
|
78
|
-
task :flay_check do
|
|
79
|
-
require 'flay'
|
|
80
|
-
|
|
81
|
-
mass_threshold = 50
|
|
82
|
-
|
|
83
|
-
flay = Flay.new({ mass: mass_threshold, diff: false, verbose: false, summary: false, timeout: 60 })
|
|
84
|
-
flay.process(*Dir.glob('lib/**/*.rb'))
|
|
85
|
-
flay.analyze
|
|
86
|
-
|
|
87
|
-
if flay.hashes.empty?
|
|
88
|
-
puts "\nFlay: no structural duplication detected (mass >= #{mass_threshold})"
|
|
89
|
-
else
|
|
90
|
-
puts "\nFlay found structural duplication (mass >= #{mass_threshold}):"
|
|
91
|
-
flay.report
|
|
92
|
-
abort "\nFlay quality gate failed: #{flay.hashes.length} pattern(s) detected"
|
|
93
|
-
end
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
desc "Run all quality checks: tests (with coverage), RuboCop, Flog, and Flay"
|
|
97
|
-
task :quality do
|
|
98
|
-
results = {}
|
|
99
|
-
|
|
100
|
-
puts "\n#{'=' * 60}"
|
|
101
|
-
puts "Quality Gate: Tests + Coverage"
|
|
102
|
-
puts '=' * 60
|
|
103
|
-
results[:tests] = system("bundle exec rake test") ? :pass : :fail
|
|
104
|
-
|
|
105
|
-
puts "\n#{'=' * 60}"
|
|
106
|
-
puts "Quality Gate: RuboCop"
|
|
107
|
-
puts '=' * 60
|
|
108
|
-
results[:rubocop] = system(RUBOCOP_ENV, "bundle exec rubocop") ? :pass : :fail
|
|
109
|
-
|
|
110
|
-
puts "\n#{'=' * 60}"
|
|
111
|
-
puts "Quality Gate: Flog Complexity"
|
|
112
|
-
puts '=' * 60
|
|
113
|
-
results[:flog] = system("bundle exec rake flog_check") ? :pass : :fail
|
|
114
|
-
|
|
115
|
-
puts "\n#{'=' * 60}"
|
|
116
|
-
puts "Quality Gate: Flay Duplication"
|
|
117
|
-
puts '=' * 60
|
|
118
|
-
results[:flay] = system("bundle exec rake flay_check") ? :pass : :fail
|
|
119
|
-
|
|
120
|
-
puts "\n#{'=' * 60}"
|
|
121
|
-
puts "Quality Summary"
|
|
122
|
-
puts '=' * 60
|
|
123
|
-
results.each do |gate, status|
|
|
124
|
-
icon = status == :pass ? 'PASS' : 'FAIL'
|
|
125
|
-
puts " [#{icon}] #{gate}"
|
|
126
|
-
end
|
|
127
|
-
puts '=' * 60
|
|
128
|
-
|
|
129
|
-
abort "\nQuality gate failed" if results.values.any?(:fail)
|
|
130
|
-
puts "\nAll quality gates passed."
|
|
131
|
-
end
|
|
132
|
-
|
|
133
42
|
namespace :examples do
|
|
134
43
|
# Map of subdirectory-based demos to their entry point scripts
|
|
135
44
|
SUBDIR_ENTRY_POINTS = {
|
|
@@ -277,22 +186,8 @@ namespace :docs do
|
|
|
277
186
|
sh "yard doc"
|
|
278
187
|
end
|
|
279
188
|
|
|
280
|
-
namespace :yard do
|
|
281
|
-
desc "Serve YARD documentation locally"
|
|
282
|
-
task :serve do
|
|
283
|
-
sh "yard server --reload"
|
|
284
|
-
end
|
|
285
|
-
end
|
|
286
|
-
|
|
287
189
|
desc "Build MkDocs documentation"
|
|
288
190
|
task :mkdocs do
|
|
289
191
|
sh "mkdocs build"
|
|
290
192
|
end
|
|
291
|
-
|
|
292
|
-
namespace :mkdocs do
|
|
293
|
-
desc "Serve MkDocs documentation locally"
|
|
294
|
-
task :serve do
|
|
295
|
-
sh "mkdocs serve"
|
|
296
|
-
end
|
|
297
|
-
end
|
|
298
193
|
end
|
data/_typos.toml
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# False-positive suppressions for the typos quality gate (asgard).
|
|
2
|
+
# Everything here was reviewed by hand — see git history for context.
|
|
3
|
+
|
|
4
|
+
[files]
|
|
5
|
+
extend-exclude = [
|
|
6
|
+
# Example transcripts / articles: prose quotes real names ("Damon Clark")
|
|
7
|
+
# and SQL-ish plurals ("SELECTs") that typos mis-parses.
|
|
8
|
+
"examples/ruboruby.md",
|
|
9
|
+
"examples/25_history_search/conversation.jsonl",
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
[default.extend-identifiers]
|
|
13
|
+
# Robot names in lib/robot_lab/names.rb
|
|
14
|
+
BATs = "BATs"
|
|
15
|
+
Damon = "Damon"
|
|
16
|
+
Mata_Nui = "Mata_Nui"
|
|
17
|
+
Pris = "Pris"
|
|
18
|
+
S1MONE = "S1MONE"
|
|
19
|
+
# Test-local variable names
|
|
20
|
+
ba = "ba" # b→a similarity (convergence_test.rb)
|
|
21
|
+
thr = "thr" # thread handle (connection_poller_test.rb)
|
data/docs/api/index.md
CHANGED
|
@@ -41,8 +41,8 @@ confines them:
|
|
|
41
41
|
| [AgentSkill](skills.md#robotlabagentskill) | One skill bundle: instructions + `scripts/` |
|
|
42
42
|
| [AgentSkillCatalog](skills.md#robotlabagentskillcatalog) | Lazy registry over `~/.prompts/skills/` |
|
|
43
43
|
| [Capabilities](skills.md#robotlabcapabilities) | Declared vs. ceiling filesystem / network / timeout grant |
|
|
44
|
-
| [ScriptTool](skills.md#robotlabscripttool) | Wraps an executable script as a `Tool` |
|
|
45
|
-
| [Sandbox](skills.md#
|
|
44
|
+
| [ScriptTool](skills.md#robotlabscripttool) | Wraps an executable script as a `Tool`; `ScriptTool.executor` is the confinement extension point |
|
|
45
|
+
| [Sandbox](skills.md#confinement-robot_lab-sandbox) | macOS Seatbelt confinement, or a passthrough — ships in the optional `robot_lab-sandbox` gem |
|
|
46
46
|
|
|
47
47
|
## Support Classes
|
|
48
48
|
|
data/docs/api/skills.md
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
# Skills API
|
|
2
2
|
|
|
3
3
|
Class-level reference for the AgentSkills subsystem: skill bundles, the scripts
|
|
4
|
-
they expose as tools, the capabilities those scripts declare
|
|
5
|
-
|
|
4
|
+
they expose as tools, and the capabilities those scripts declare. Core itself
|
|
5
|
+
has **no sandboxing and no execution limitations** — every script runs
|
|
6
|
+
unconfined until the optional
|
|
7
|
+
[`robot_lab-sandbox`](https://github.com/MadBomber/robot_lab-sandbox) gem is
|
|
8
|
+
required, at which point it installs a confinement strategy (see
|
|
9
|
+
[ScriptTool.executor](#scripttoolexecutor) below). For the how-to, see
|
|
6
10
|
[Using Tools: Skill Scripts and Sandboxing](../guides/using-tools.md#skill-scripts-and-sandboxing)
|
|
7
11
|
and [Building Robots: Composable Skills](../guides/building-robots.md#composable-skills).
|
|
8
12
|
|
|
@@ -27,12 +31,16 @@ graph LR
|
|
|
27
31
|
G[effective grant<br/>declared ∩ ceiling]
|
|
28
32
|
end
|
|
29
33
|
|
|
30
|
-
subgraph "Execution"
|
|
34
|
+
subgraph "Execution (core)"
|
|
31
35
|
T[ScriptTool.from_path<br/>-> RobotLab::Tool]
|
|
36
|
+
EX{ScriptTool.executor<br/>set?}
|
|
37
|
+
UN[Open3.capture2e<br/>unconfined, no timeout]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
subgraph "robot_lab-sandbox (optional gem)"
|
|
32
41
|
SB{Sandbox.enabled?}
|
|
33
42
|
SE[Sandbox::Seatbelt<br/>macOS]
|
|
34
43
|
NU[Sandbox::Null<br/>passthrough]
|
|
35
|
-
P[script process]
|
|
36
44
|
end
|
|
37
45
|
|
|
38
46
|
R[Robot<br/>AgentSkillMatching] --> C
|
|
@@ -41,12 +49,12 @@ graph LR
|
|
|
41
49
|
S --> T
|
|
42
50
|
D --> G
|
|
43
51
|
CE --> G
|
|
44
|
-
T -->
|
|
52
|
+
T --> EX
|
|
53
|
+
EX -- "nil (default)" --> UN
|
|
54
|
+
EX -- "installed" --> SB
|
|
45
55
|
SB -- "off, or trust: core,<br/>or non-macOS" --> NU
|
|
46
56
|
SB -- "on + macOS" --> SE
|
|
47
57
|
G --> SE
|
|
48
|
-
SE --> P
|
|
49
|
-
NU --> P
|
|
50
58
|
```
|
|
51
59
|
|
|
52
60
|
---
|
|
@@ -264,9 +272,9 @@ of the ceiling, and `intersect` keeps the declared value.
|
|
|
264
272
|
capabilities.core? # => trust == "core"
|
|
265
273
|
```
|
|
266
274
|
|
|
267
|
-
A `core` skill is exempt from confinement: `
|
|
268
|
-
|
|
269
|
-
Reserve `trust: core` for bundles you wrote and audited.
|
|
275
|
+
A `core` skill is exempt from confinement: when `robot_lab-sandbox` is loaded,
|
|
276
|
+
`Sandbox.for` returns a `Null` strategy for it regardless of platform or
|
|
277
|
+
config. Reserve `trust: core` for bundles you wrote and audited.
|
|
270
278
|
|
|
271
279
|
### intersect
|
|
272
280
|
|
|
@@ -314,43 +322,32 @@ The generated tool takes a single optional `args` string parameter, which is
|
|
|
314
322
|
[`derive_name`](#scripttoolderive_name) and its description from
|
|
315
323
|
[`extract_description`](#scripttoolextract_description).
|
|
316
324
|
|
|
317
|
-
### ScriptTool.
|
|
325
|
+
### ScriptTool.executor
|
|
318
326
|
|
|
319
327
|
```ruby
|
|
320
|
-
RobotLab::ScriptTool.
|
|
328
|
+
RobotLab::ScriptTool.executor # => #call, or nil (the default)
|
|
329
|
+
RobotLab::ScriptTool.executor = obj # any object responding to
|
|
330
|
+
# call(cmd, capabilities:, skill_dir:)
|
|
321
331
|
```
|
|
322
332
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
- **Sandboxing on** — intersects `capabilities` with `Capabilities.ceiling`, wraps
|
|
328
|
-
the command with the strategy from `Sandbox.for`, runs it under the grant's
|
|
329
|
-
timeout, and cleans the strategy up in an `ensure`.
|
|
330
|
-
|
|
331
|
-
The declared `timeout:` therefore only takes effect when sandboxing is enabled.
|
|
333
|
+
The extension point core exposes for confinement. `nil` by default — core has
|
|
334
|
+
no sandboxing of its own. `robot_lab-sandbox`, when required, sets this to
|
|
335
|
+
`RobotLab::Sandbox::Executor`, which handles `capabilities`/timeout/cleanup
|
|
336
|
+
itself. See that gem's docs for what it does when installed.
|
|
332
337
|
|
|
333
|
-
### ScriptTool.
|
|
338
|
+
### ScriptTool.execute
|
|
334
339
|
|
|
335
340
|
```ruby
|
|
336
|
-
RobotLab::ScriptTool.
|
|
337
|
-
# => [String, Process::Status | nil]
|
|
341
|
+
RobotLab::ScriptTool.execute(cmd, capabilities:, skill_dir:) # => String
|
|
338
342
|
```
|
|
339
343
|
|
|
340
|
-
Run
|
|
341
|
-
|
|
342
|
-
`["<partial output>\n[killed: exceeded <N>s]", nil]` — a `nil` status is the
|
|
343
|
-
timeout signal.
|
|
344
|
-
|
|
345
|
-
### ScriptTool.terminate
|
|
346
|
-
|
|
347
|
-
```ruby
|
|
348
|
-
RobotLab::ScriptTool.terminate(pid)
|
|
349
|
-
```
|
|
344
|
+
Run a command array and return its combined stdout+stderr, or an error string.
|
|
345
|
+
Two paths:
|
|
350
346
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
347
|
+
- **`executor` is `nil`** (the default) — `Open3.capture2e`, unconfined, **no timeout**.
|
|
348
|
+
- **`executor` is set** — delegates entirely to `executor.call(cmd, capabilities:, skill_dir:)`.
|
|
349
|
+
Core no longer knows or cares what the executor does with `capabilities` or
|
|
350
|
+
how (or whether) it bounds execution time.
|
|
354
351
|
|
|
355
352
|
### ScriptTool.format_result
|
|
356
353
|
|
|
@@ -393,121 +390,27 @@ cannot be read.
|
|
|
393
390
|
|
|
394
391
|
---
|
|
395
392
|
|
|
396
|
-
##
|
|
393
|
+
## Confinement: robot_lab-sandbox
|
|
397
394
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
RobotLab::Sandbox.enabled? # => Boolean
|
|
404
|
-
RobotLab::Sandbox.enabled?(some_config)
|
|
405
|
-
```
|
|
406
|
-
|
|
407
|
-
`true` only when the config responds to `sandbox`, that section exists, and
|
|
408
|
-
`sandbox.enabled == true`. **Sandboxing is off by default** — see the
|
|
409
|
-
[`sandbox:` config section](../getting-started/configuration.md#skill-script-sandboxing-sandbox-section).
|
|
410
|
-
|
|
411
|
-
### Sandbox.macos?
|
|
412
|
-
|
|
413
|
-
```ruby
|
|
414
|
-
RobotLab::Sandbox.macos? # => RUBY_PLATFORM.include?("darwin")
|
|
415
|
-
```
|
|
416
|
-
|
|
417
|
-
### Sandbox.for
|
|
418
|
-
|
|
419
|
-
```ruby
|
|
420
|
-
strategy = RobotLab::Sandbox.for(grant, skill_dir:, macos: macos?)
|
|
421
|
-
# => Sandbox::Seatbelt or Sandbox::Null
|
|
422
|
-
```
|
|
423
|
-
|
|
424
|
-
| Name | Type | Default | Description |
|
|
425
|
-
|------|------|---------|-------------|
|
|
426
|
-
| `grant` | `Capabilities` | **required** | The **already-intersected** effective grant |
|
|
427
|
-
| `skill_dir` | `String` | **required** | Bundle root, always granted read access |
|
|
428
|
-
| `macos` | `Boolean` | `macos?` | Injectable so both branches are testable on any host |
|
|
395
|
+
`RobotLab::Sandbox`, `RobotLab::Sandbox::Seatbelt`, and `RobotLab::Sandbox::Null`
|
|
396
|
+
used to live here; they now ship in the separate
|
|
397
|
+
[`robot_lab-sandbox`](https://github.com/MadBomber/robot_lab-sandbox) gem, which
|
|
398
|
+
core has no dependency on. Requiring it installs `RobotLab::Sandbox::Executor`
|
|
399
|
+
as [`ScriptTool.executor`](#scripttoolexecutor):
|
|
429
400
|
|
|
430
|
-
|
|
431
|
-
`
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
many scripts does not flood the log.
|
|
442
|
-
|
|
443
|
-
---
|
|
444
|
-
|
|
445
|
-
## RobotLab::Sandbox::Null
|
|
446
|
-
|
|
447
|
-
Passthrough strategy — used off macOS and for `trust: core` skills.
|
|
448
|
-
|
|
449
|
-
| Method | Returns | Description |
|
|
450
|
-
|--------|---------|-------------|
|
|
451
|
-
| `wrap(cmd)` | `cmd` | Unchanged |
|
|
452
|
-
| `cleanup` | `nil` | No-op |
|
|
453
|
-
|
|
454
|
-
---
|
|
455
|
-
|
|
456
|
-
## RobotLab::Sandbox::Seatbelt
|
|
457
|
-
|
|
458
|
-
macOS strategy: generates a deny-by-default `sandbox-exec` profile from the grant
|
|
459
|
-
and wraps the command as `sandbox-exec -f <profile> <cmd...>`.
|
|
460
|
-
|
|
461
|
-
### Constants
|
|
462
|
-
|
|
463
|
-
| Constant | Description |
|
|
464
|
-
|----------|-------------|
|
|
465
|
-
| `SYSTEM_READ` | `/usr /bin /sbin /System /Library /opt /private/etc /dev /var/select` — the locations an interpreter needs to boot |
|
|
466
|
-
| `DEV_WRITE` | `/dev/null /dev/stdout /dev/stderr /dev/dtracehelper /dev/tty` — always writable |
|
|
467
|
-
|
|
468
|
-
### Constructor
|
|
469
|
-
|
|
470
|
-
```ruby
|
|
471
|
-
RobotLab::Sandbox::Seatbelt.new(grant, skill_dir:)
|
|
472
|
-
```
|
|
473
|
-
|
|
474
|
-
### wrap / cleanup
|
|
475
|
-
|
|
476
|
-
```ruby
|
|
477
|
-
cmd = strategy.wrap(["bash", "script.sh"])
|
|
478
|
-
# => ["sandbox-exec", "-f", "/tmp/robot_lab-sandbox-xxxx.sb", "bash", "script.sh"]
|
|
479
|
-
strategy.cleanup # unlinks the generated profile
|
|
480
|
-
```
|
|
481
|
-
|
|
482
|
-
`wrap` writes the profile to a `Tempfile`; `cleanup` unlinks it and swallows any
|
|
483
|
-
error (an already-removed file is fine). `ScriptTool.execute` always calls
|
|
484
|
-
`cleanup` in an `ensure`.
|
|
485
|
-
|
|
486
|
-
### profile_text
|
|
487
|
-
|
|
488
|
-
```ruby
|
|
489
|
-
strategy.profile_text # => String
|
|
490
|
-
```
|
|
401
|
+
- `Sandbox.enabled?` — reads `config.sandbox.enabled` (default `false`).
|
|
402
|
+
- `Sandbox.for(grant, skill_dir:, macos: macos?)` — picks `Sandbox::Seatbelt` on
|
|
403
|
+
macOS, `Sandbox::Null` (passthrough) elsewhere or for `trust: core` grants.
|
|
404
|
+
- `Sandbox::Seatbelt` — generates a deny-by-default `sandbox-exec` profile from
|
|
405
|
+
the effective grant (`fs_read`/`fs_write`/`network`), wrapping the command as
|
|
406
|
+
`sandbox-exec -f <profile> <cmd...>`. `$HOME` is never implicitly readable.
|
|
407
|
+
- `Sandbox::Null` — `wrap(cmd)` returns `cmd` unchanged; `cleanup` is a no-op.
|
|
408
|
+
- `Sandbox::Executor` — the piece that plugs into core: intersects the skill's
|
|
409
|
+
`Capabilities` with `Capabilities.ceiling`, wraps and runs the command under
|
|
410
|
+
the chosen strategy, and bounds it with the grant's `timeout`
|
|
411
|
+
(`Process.kill('-TERM', ...)` on the process group on expiry).
|
|
491
412
|
|
|
492
|
-
|
|
493
|
-
rather than inferred from behavior. It imports `bsd.sb` (without which a
|
|
494
|
-
deny-default profile aborts the binary before it starts), denies by default, then
|
|
495
|
-
allows: `process-fork`, `process-exec`, `sysctl-read`, `mach-lookup`,
|
|
496
|
-
`file-read-metadata` on any path, `file-read*` on `SYSTEM_READ` + the skill
|
|
497
|
-
directory + granted `fs_read` paths, `file-write*` on `DEV_WRITE` + granted
|
|
498
|
-
`fs_write` paths, and `network*` only when the grant allows it.
|
|
499
|
-
|
|
500
|
-
Every path is canonicalized to its symlink-free real path first, because macOS
|
|
501
|
-
symlinks `/tmp` → `/private/tmp` and the kernel matches against the real path. For
|
|
502
|
-
write targets that do not exist yet, the nearest existing ancestor is resolved and
|
|
503
|
-
the remainder re-appended.
|
|
504
|
-
|
|
505
|
-
!!! warning "`$HOME` is never implicitly readable"
|
|
506
|
-
Which is the point — SSH keys and cloud credentials stay out of reach. But it
|
|
507
|
-
also means an interpreter installed under `$HOME` (rbenv, asdf, mise, a
|
|
508
|
-
Homebrew prefix in `~`) is **invisible** to the sandboxed process and the
|
|
509
|
-
script fails to start. Grant that path explicitly in `fs_read`, or mark the
|
|
510
|
-
skill `trust: core`.
|
|
413
|
+
Full reference lives in that gem's own docs.
|
|
511
414
|
|
|
512
415
|
---
|
|
513
416
|
|
|
@@ -515,5 +418,6 @@ the remainder re-appended.
|
|
|
515
418
|
|
|
516
419
|
- [Using Tools: Skill Scripts and Sandboxing](../guides/using-tools.md#skill-scripts-and-sandboxing)
|
|
517
420
|
- [Configuration: `sandbox:` section](../getting-started/configuration.md#skill-script-sandboxing-sandbox-section)
|
|
421
|
+
- [robot_lab-sandbox](https://github.com/MadBomber/robot_lab-sandbox) — the optional confinement gem
|
|
518
422
|
- [Robot: Skills](core/robot.md#skills) — template skills, the other meaning
|
|
519
423
|
- [Tool](core/tool.md) — `Tool.create`, which `ScriptTool` builds on
|
data/docs/api/support.md
CHANGED
|
@@ -842,5 +842,5 @@ Thread-safe monotonic counter for event ordering.
|
|
|
842
842
|
|
|
843
843
|
- [Core Classes](core/index.md) — `Robot`, `Network`, `Memory`, `Tool`, `RobotResult`
|
|
844
844
|
- [Hooks API](hooks.md) — the extension seam these classes are wired into
|
|
845
|
-
- [Skills API](skills.md) — `AgentSkill`, `Capabilities`, `ScriptTool
|
|
845
|
+
- [Skills API](skills.md) — `AgentSkill`, `Capabilities`, `ScriptTool` (confinement lives in the optional `robot_lab-sandbox` gem)
|
|
846
846
|
- [Errors](errors.md) — including `Errors.retryable?` and `Errors.retryable_classes`
|
data/docs/architecture/index.md
CHANGED
|
@@ -62,7 +62,7 @@ graph TB
|
|
|
62
62
|
H[MCP Client]
|
|
63
63
|
I[Tools < RubyLLM::Tool]
|
|
64
64
|
J[Templates / prompt_manager]
|
|
65
|
-
SK[AgentSkills
|
|
65
|
+
SK[AgentSkills<br/>+ optional Sandbox]
|
|
66
66
|
end
|
|
67
67
|
|
|
68
68
|
subgraph "Execution Layer"
|
|
@@ -138,8 +138,9 @@ graph LR
|
|
|
138
138
|
AS --> CAT[AgentSkillCatalog]
|
|
139
139
|
AS --> CAP[Capabilities]
|
|
140
140
|
AS --> ST[ScriptTool]
|
|
141
|
-
|
|
142
|
-
|
|
141
|
+
ST --> EX{ScriptTool.executor}
|
|
142
|
+
EX -. "installed by<br/>robot_lab-sandbox" .-> SB[Sandbox<br/>Seatbelt or Null]
|
|
143
|
+
CAP -. "used if installed" .-> SB
|
|
143
144
|
|
|
144
145
|
HC --> TA[TextAnalysis<br/>TF / TF-IDF]
|
|
145
146
|
HS --> TA
|
|
@@ -165,7 +166,7 @@ dependency.
|
|
|
165
166
|
| **Config** | MywayConfig-based global configuration with env var and file support | [Configuration](#configuration) |
|
|
166
167
|
| **Hook** | Handler base class for the seven hook families — the framework's extension seam | [Hooks API](../api/hooks.md) |
|
|
167
168
|
| **AgentSkill** | A `SKILL.md` bundle whose instructions and `scripts/` become prompt text and tools | [Skills API](../api/skills.md) |
|
|
168
|
-
| **Sandbox** | Opt-in OS-level confinement (macOS Seatbelt) for skill scripts, derived from `Capabilities` | [Skills API](../api/skills.md) |
|
|
169
|
+
| **Sandbox** | Opt-in OS-level confinement (macOS Seatbelt) for skill scripts, derived from `Capabilities`; ships in the optional `robot_lab-sandbox` gem, not core | [Skills API](../api/skills.md) |
|
|
169
170
|
| **RobotMessage** | Immutable envelope for TypedBus inter-robot messaging, serialized per robot by `BusPoller` | [Support API](../api/support.md) |
|
|
170
171
|
| **Budget::Ledger** | Thread-safe reserve/reconcile ledger behind `token_budget` / `cost_budget` | [Support API](../api/support.md) |
|
|
171
172
|
|
|
@@ -278,7 +278,10 @@ robot = RobotLab.build(
|
|
|
278
278
|
|
|
279
279
|
### Skill-Script Sandboxing (`sandbox:` section)
|
|
280
280
|
|
|
281
|
-
|
|
281
|
+
Core declares this config schema but has no sandboxing behavior of its own — enforcement
|
|
282
|
+
lives in the optional [`robot_lab-sandbox`](https://github.com/MadBomber/robot_lab-sandbox)
|
|
283
|
+
gem. Without it, skill scripts always run unconfined regardless of these values; requiring
|
|
284
|
+
it installs a `RobotLab::ScriptTool.executor` that honors them:
|
|
282
285
|
|
|
283
286
|
| Key | Default | Description |
|
|
284
287
|
|-----|---------|-------------|
|