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.
@@ -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,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RobotLab
4
+ module Sandbox
5
+ VERSION = "0.2.8"
6
+ end
7
+ 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 &copy; 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: []