robot_lab-sandbox 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 +7 -0
- data/.envrc +6 -0
- data/.github/workflows/deploy-github-pages.yml +52 -0
- data/.loki +10 -0
- data/.rubocop.yml +1 -0
- data/CHANGELOG.md +21 -0
- data/CLAUDE.md +55 -0
- data/COMMITS.md +196 -0
- data/LICENSE.txt +21 -0
- data/README.md +66 -0
- data/Rakefile +24 -0
- data/docs/configuration.md +112 -0
- data/docs/getting_started.md +121 -0
- data/docs/how_it_works.md +236 -0
- data/docs/index.md +58 -0
- data/docs/troubleshooting.md +108 -0
- data/lib/robot_lab/sandbox/executor.rb +56 -0
- data/lib/robot_lab/sandbox/null.rb +13 -0
- data/lib/robot_lab/sandbox/seatbelt.rb +106 -0
- data/lib/robot_lab/sandbox/version.rb +7 -0
- data/lib/robot_lab/sandbox.rb +74 -0
- data/mkdocs.yml +118 -0
- metadata +82 -0
|
@@ -0,0 +1,106 @@
|
|
|
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
|
+
# :reek:FeatureEnvy -- building and returning a Tempfile inherently
|
|
66
|
+
# calls several methods on it; that's not envy of another object's data.
|
|
67
|
+
def write_profile
|
|
68
|
+
file = Tempfile.create(["robot_lab-sandbox-", ".sb"])
|
|
69
|
+
file.write(profile_text)
|
|
70
|
+
file.close
|
|
71
|
+
file.path
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Resolve to the real (symlink-free) path the kernel matches against. macOS
|
|
75
|
+
# symlinks /tmp -> /private/tmp, /var -> /private/var, etc., so logical
|
|
76
|
+
# paths would never match. For not-yet-existing write targets, resolve the
|
|
77
|
+
# nearest existing ancestor and re-append the remainder.
|
|
78
|
+
def canonicalize(paths)
|
|
79
|
+
Array(paths).map { |p| real_path(File.expand_path(p.to_s)) }.compact.uniq
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def real_path(expanded)
|
|
83
|
+
existing = expanded
|
|
84
|
+
rest = []
|
|
85
|
+
until File.exist?(existing) || existing == "/"
|
|
86
|
+
rest.unshift(File.basename(existing))
|
|
87
|
+
existing = File.dirname(existing)
|
|
88
|
+
end
|
|
89
|
+
real = File.realpath(existing)
|
|
90
|
+
rest.empty? ? real : File.join(real, *rest)
|
|
91
|
+
rescue StandardError
|
|
92
|
+
expanded
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def read_rule(paths)
|
|
96
|
+
subpaths = paths.map { |p| "(subpath #{p.inspect})" }.join(" ")
|
|
97
|
+
"(allow file-read* #{subpaths})"
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def write_rule(entries)
|
|
101
|
+
clauses = entries.map { |kind, p| "(#{kind} #{p.inspect})" }.join(" ")
|
|
102
|
+
"(allow file-write* #{clauses})"
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "sandbox/version"
|
|
4
|
+
|
|
5
|
+
module RobotLab
|
|
6
|
+
# Confines skill-script execution to a granted set of capabilities.
|
|
7
|
+
#
|
|
8
|
+
# Sandboxing is opt-in (config.sandbox.enabled). When off, every script runs
|
|
9
|
+
# unconfined exactly as robot_lab core alone would run it. When on, each
|
|
10
|
+
# script runs under a strategy:
|
|
11
|
+
#
|
|
12
|
+
# - {Sandbox::Seatbelt} on macOS — a generated deny-by-default sandbox-exec
|
|
13
|
+
# profile derived from the effective {Capabilities} grant.
|
|
14
|
+
# - {Sandbox::Null} elsewhere (or for +trust: core+ skills) — a passthrough.
|
|
15
|
+
#
|
|
16
|
+
# OS-level confinement is therefore best-effort and platform-specific; it is
|
|
17
|
+
# never a hard dependency. robot_lab core has no notion of sandboxing at
|
|
18
|
+
# all -- requiring this gem is what installs confinement into
|
|
19
|
+
# RobotLab::ScriptTool via {Executor}.
|
|
20
|
+
module Sandbox
|
|
21
|
+
class Error < StandardError; end
|
|
22
|
+
|
|
23
|
+
module_function
|
|
24
|
+
|
|
25
|
+
# @return [Boolean] whether sandboxing is turned on in config
|
|
26
|
+
def enabled?(config = RobotLab.config)
|
|
27
|
+
config.respond_to?(:sandbox) && config.sandbox && config.sandbox.enabled == true
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def macos?
|
|
31
|
+
RUBY_PLATFORM.include?("darwin")
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Pick a strategy for the given (already-intersected) grant.
|
|
35
|
+
#
|
|
36
|
+
# @param grant [Capabilities] effective grant
|
|
37
|
+
# @param skill_dir [String] skill bundle root, always granted read access
|
|
38
|
+
# @param macos [Boolean] whether to use the macOS strategy; defaults to the
|
|
39
|
+
# real platform check and is injectable so both branches are testable on
|
|
40
|
+
# any host without stubbing.
|
|
41
|
+
# @return [#wrap, #cleanup]
|
|
42
|
+
# :reek:ControlParameter -- macos: is an intentional test seam (see above).
|
|
43
|
+
def for(grant, skill_dir:, macos: macos?)
|
|
44
|
+
return Null.new if grant.core?
|
|
45
|
+
return Seatbelt.new(grant, skill_dir: skill_dir) if macos
|
|
46
|
+
|
|
47
|
+
warn_once_non_macos
|
|
48
|
+
Null.new
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def warn_once_non_macos
|
|
52
|
+
return if @warned_non_macos
|
|
53
|
+
|
|
54
|
+
@warned_non_macos = true
|
|
55
|
+
RobotLab.config.logger.warn(
|
|
56
|
+
"Sandbox: OS-level confinement is only available on macOS; scripts run unconfined here"
|
|
57
|
+
)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
require_relative "sandbox/seatbelt"
|
|
63
|
+
require_relative "sandbox/null"
|
|
64
|
+
require_relative "sandbox/executor"
|
|
65
|
+
|
|
66
|
+
unless defined?(RobotLab::ScriptTool)
|
|
67
|
+
raise RobotLab::Sandbox::Error, "robot_lab must be loaded before robot_lab/sandbox"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
RobotLab::ScriptTool.executor = RobotLab::Sandbox::Executor
|
|
71
|
+
|
|
72
|
+
if RobotLab.respond_to?(:register_extension)
|
|
73
|
+
RobotLab.register_extension(:sandbox, RobotLab::Sandbox)
|
|
74
|
+
end
|
data/mkdocs.yml
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
site_name: robot_lab-sandbox
|
|
2
|
+
site_description: OS-level confinement for RobotLab skill scripts
|
|
3
|
+
site_author: Dewayne VanHoozer
|
|
4
|
+
site_url: https://madbomber.github.io/robot_lab-sandbox
|
|
5
|
+
copyright: Copyright © 2026 Dewayne VanHoozer
|
|
6
|
+
|
|
7
|
+
repo_name: MadBomber/robot_lab-sandbox
|
|
8
|
+
repo_url: https://github.com/MadBomber/robot_lab-sandbox
|
|
9
|
+
edit_uri: edit/main/docs/
|
|
10
|
+
docs_dir: docs
|
|
11
|
+
|
|
12
|
+
theme:
|
|
13
|
+
name: material
|
|
14
|
+
|
|
15
|
+
palette:
|
|
16
|
+
- scheme: default
|
|
17
|
+
primary: teal
|
|
18
|
+
accent: amber
|
|
19
|
+
toggle:
|
|
20
|
+
icon: material/brightness-7
|
|
21
|
+
name: Switch to dark mode
|
|
22
|
+
|
|
23
|
+
- scheme: slate
|
|
24
|
+
primary: teal
|
|
25
|
+
accent: amber
|
|
26
|
+
toggle:
|
|
27
|
+
icon: material/brightness-4
|
|
28
|
+
name: Switch to light mode
|
|
29
|
+
|
|
30
|
+
font:
|
|
31
|
+
text: Roboto
|
|
32
|
+
code: Roboto Mono
|
|
33
|
+
|
|
34
|
+
icon:
|
|
35
|
+
repo: fontawesome/brands/github
|
|
36
|
+
logo: material/shield-lock-outline
|
|
37
|
+
|
|
38
|
+
features:
|
|
39
|
+
- navigation.instant
|
|
40
|
+
- navigation.tracking
|
|
41
|
+
- navigation.tabs
|
|
42
|
+
- navigation.tabs.sticky
|
|
43
|
+
- navigation.path
|
|
44
|
+
- navigation.indexes
|
|
45
|
+
- navigation.top
|
|
46
|
+
- navigation.footer
|
|
47
|
+
- toc.follow
|
|
48
|
+
- search.suggest
|
|
49
|
+
- search.highlight
|
|
50
|
+
- search.share
|
|
51
|
+
- header.autohide
|
|
52
|
+
- content.code.copy
|
|
53
|
+
- content.code.annotate
|
|
54
|
+
- content.tabs.link
|
|
55
|
+
- content.tooltips
|
|
56
|
+
- content.action.edit
|
|
57
|
+
- content.action.view
|
|
58
|
+
|
|
59
|
+
plugins:
|
|
60
|
+
- search:
|
|
61
|
+
separator: '[\s\-,:!=\[\]()"`/]+|\.(?!\d)|&[lg]t;|(?!\b)(?=[A-Z][a-z])'
|
|
62
|
+
|
|
63
|
+
markdown_extensions:
|
|
64
|
+
- abbr
|
|
65
|
+
- admonition
|
|
66
|
+
- attr_list
|
|
67
|
+
- def_list
|
|
68
|
+
- footnotes
|
|
69
|
+
- md_in_html
|
|
70
|
+
- tables
|
|
71
|
+
- toc:
|
|
72
|
+
permalink: true
|
|
73
|
+
title: On this page
|
|
74
|
+
- pymdownx.betterem:
|
|
75
|
+
smart_enable: all
|
|
76
|
+
- pymdownx.caret
|
|
77
|
+
- pymdownx.details
|
|
78
|
+
- pymdownx.emoji:
|
|
79
|
+
emoji_generator: !!python/name:material.extensions.emoji.to_svg
|
|
80
|
+
emoji_index: !!python/name:material.extensions.emoji.twemoji
|
|
81
|
+
- pymdownx.highlight:
|
|
82
|
+
anchor_linenums: true
|
|
83
|
+
line_spans: __span
|
|
84
|
+
pygments_lang_class: true
|
|
85
|
+
- pymdownx.inlinehilite
|
|
86
|
+
- pymdownx.magiclink:
|
|
87
|
+
repo_url_shorthand: true
|
|
88
|
+
user: MadBomber
|
|
89
|
+
repo: robot_lab-sandbox
|
|
90
|
+
normalize_issue_symbols: true
|
|
91
|
+
- pymdownx.mark
|
|
92
|
+
- pymdownx.smartsymbols
|
|
93
|
+
- pymdownx.superfences:
|
|
94
|
+
custom_fences:
|
|
95
|
+
- name: mermaid
|
|
96
|
+
class: mermaid
|
|
97
|
+
format: !!python/name:pymdownx.superfences.fence_code_format
|
|
98
|
+
- pymdownx.tabbed:
|
|
99
|
+
alternate_style: true
|
|
100
|
+
- pymdownx.tasklist:
|
|
101
|
+
custom_checkbox: true
|
|
102
|
+
- pymdownx.tilde
|
|
103
|
+
|
|
104
|
+
extra:
|
|
105
|
+
social:
|
|
106
|
+
- icon: fontawesome/brands/github
|
|
107
|
+
link: https://github.com/MadBomber/robot_lab-sandbox
|
|
108
|
+
name: robot_lab-sandbox on GitHub
|
|
109
|
+
- icon: fontawesome/solid/gem
|
|
110
|
+
link: https://rubygems.org/gems/robot_lab-sandbox
|
|
111
|
+
name: robot_lab-sandbox on RubyGems
|
|
112
|
+
|
|
113
|
+
nav:
|
|
114
|
+
- Home: index.md
|
|
115
|
+
- Getting Started: getting_started.md
|
|
116
|
+
- Configuration: configuration.md
|
|
117
|
+
- How It Works: how_it_works.md
|
|
118
|
+
- Troubleshooting: troubleshooting.md
|
metadata
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: robot_lab-sandbox
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.2.8
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Dewayne VanHoozer
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: robot_lab
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0.1'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0.1'
|
|
26
|
+
description: Deny-by-default, capability-scoped execution for AgentSkills scripts.
|
|
27
|
+
On macOS, wraps scripts in a generated sandbox-exec (Seatbelt) profile derived from
|
|
28
|
+
the skill's declared capabilities intersected with a configured ceiling; robot_lab
|
|
29
|
+
core runs unconfined until this gem is required.
|
|
30
|
+
email:
|
|
31
|
+
- dvanhoozer@gmail.com
|
|
32
|
+
executables: []
|
|
33
|
+
extensions: []
|
|
34
|
+
extra_rdoc_files: []
|
|
35
|
+
files:
|
|
36
|
+
- ".envrc"
|
|
37
|
+
- ".github/workflows/deploy-github-pages.yml"
|
|
38
|
+
- ".loki"
|
|
39
|
+
- ".rubocop.yml"
|
|
40
|
+
- CHANGELOG.md
|
|
41
|
+
- CLAUDE.md
|
|
42
|
+
- COMMITS.md
|
|
43
|
+
- LICENSE.txt
|
|
44
|
+
- README.md
|
|
45
|
+
- Rakefile
|
|
46
|
+
- docs/configuration.md
|
|
47
|
+
- docs/getting_started.md
|
|
48
|
+
- docs/how_it_works.md
|
|
49
|
+
- docs/index.md
|
|
50
|
+
- docs/troubleshooting.md
|
|
51
|
+
- lib/robot_lab/sandbox.rb
|
|
52
|
+
- lib/robot_lab/sandbox/executor.rb
|
|
53
|
+
- lib/robot_lab/sandbox/null.rb
|
|
54
|
+
- lib/robot_lab/sandbox/seatbelt.rb
|
|
55
|
+
- lib/robot_lab/sandbox/version.rb
|
|
56
|
+
- mkdocs.yml
|
|
57
|
+
homepage: https://github.com/MadBomber/robot_lab-sandbox
|
|
58
|
+
licenses:
|
|
59
|
+
- MIT
|
|
60
|
+
metadata:
|
|
61
|
+
homepage_uri: https://github.com/MadBomber/robot_lab-sandbox
|
|
62
|
+
source_code_uri: https://github.com/MadBomber/robot_lab-sandbox
|
|
63
|
+
changelog_uri: https://github.com/MadBomber/robot_lab-sandbox/blob/main/CHANGELOG.md
|
|
64
|
+
rubygems_mfa_required: 'true'
|
|
65
|
+
rdoc_options: []
|
|
66
|
+
require_paths:
|
|
67
|
+
- lib
|
|
68
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
69
|
+
requirements:
|
|
70
|
+
- - ">="
|
|
71
|
+
- !ruby/object:Gem::Version
|
|
72
|
+
version: 3.2.0
|
|
73
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
74
|
+
requirements:
|
|
75
|
+
- - ">="
|
|
76
|
+
- !ruby/object:Gem::Version
|
|
77
|
+
version: '0'
|
|
78
|
+
requirements: []
|
|
79
|
+
rubygems_version: 4.0.20
|
|
80
|
+
specification_version: 4
|
|
81
|
+
summary: OS-level confinement for RobotLab skill scripts.
|
|
82
|
+
test_files: []
|