Class: RobotLab::Sandbox::Seatbelt

Inherits:
Object
  • Object
show all
Defined in:
lib/robot_lab/sandbox/seatbelt.rb

Overview

macOS strategy: generates a deny-by-default sandbox-exec profile from the effective grant and wraps the command as sandbox-exec -f <cmd...>

The profile allows process exec plus reads of system locations (so the interpreter can load), reads of the skill bundle and granted paths, writes only to granted paths (and the standard /dev sinks), and network only when granted. Everything else -- notably $HOME, so SSH/cloud credentials -- is denied. Interpreters installed under $HOME (e.g. rbenv) are not visible; declare them in fs_read or mark the skill trust: core.

Constant Summary collapse

SYSTEM_READ =

System locations a typical interpreter needs to read to start up.

%w[/usr /bin /sbin /System /Library /opt /private/etc /dev /var/select].freeze
DEV_WRITE =
%w[/dev/null /dev/stdout /dev/stderr /dev/dtracehelper /dev/tty].freeze

Instance Method Summary collapse

Constructor Details

#initialize(grant, skill_dir:) ⇒ Seatbelt

Returns a new instance of Seatbelt.



22
23
24
25
26
# File 'lib/robot_lab/sandbox/seatbelt.rb', line 22

def initialize(grant, skill_dir:)
  @grant     = grant
  @skill_dir = File.expand_path(skill_dir.to_s)
  @profile   = nil
end

Instance Method Details

#cleanupObject



33
34
35
36
37
# File 'lib/robot_lab/sandbox/seatbelt.rb', line 33

def cleanup
  File.unlink(@profile) if @profile && File.exist?(@profile)
rescue StandardError
  nil
end

#profile_textObject

The generated Seatbelt profile text (public for testing).



40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# File 'lib/robot_lab/sandbox/seatbelt.rb', line 40

def profile_text
  reads  = canonicalize(SYSTEM_READ + [@skill_dir] + @grant.fs_read)
  writes = canonicalize(@grant.fs_write)
  lines = [
    "(version 1)",
    # bsd.sb supplies the base rules a process needs to start (dyld, mach
    # bootstrap, etc.); without it a deny-default profile aborts the binary.
    '(import "bsd.sb")',
    "(deny default)",
    "(allow process-fork)",
    "(allow process-exec)",
    "(allow sysctl-read)",
    "(allow mach-lookup)",
    # Metadata (stat/lookup) on any path so the interpreter can traverse to
    # granted files; reading file *contents* stays restricted below.
    "(allow file-read-metadata)",
    read_rule(reads),
    write_rule(DEV_WRITE.map { |p| [:literal, p] } + writes.map { |p| [:subpath, p] })
  ]
  lines << "(allow network*)" if @grant.network
  "#{lines.compact.join("\n")}\n"
end

#wrap(cmd) ⇒ Object



28
29
30
31
# File 'lib/robot_lab/sandbox/seatbelt.rb', line 28

def wrap(cmd)
  @profile = write_profile
  ["sandbox-exec", "-f", @profile, *cmd]
end