class MXC::Sandbox
A sandbox policy: what a command may read, write and reach, and how long it may run.
Setters return the sandbox, so they chain, and nil resets a setting to its default:
sandbox = MXC.sandbox .with_readable("app/skills") .with_writable(Rails.root.join("tmp")) .with_network(allow: ["pypi.org", "*.pythonhosted.org"]) .with_timeout(30) sandbox.run("python3 app/skills/build.py") sandbox.run("python3", "app/skills/convert.py", params[:filename])
Subclasses declare the same settings with class macros:
class SkillSandbox < MXC::Sandbox readable "app/skills" writable "tmp/skills" network false timeout 30 end SkillSandbox.new.run("python3 app/skills/build.py")
Public Class Methods
Source
# File lib/mxc/sandbox.rb, line 63 def backend(name) = configure { |sandbox| sandbox.with_backend(name) }
Sets every instance’s backend.
Source
# File lib/mxc/sandbox.rb, line 48 def denied(*paths) = configure { |sandbox| sandbox.with_denied(*paths) }
Adds paths every instance hides, even inside readable or writable paths.
Source
# File lib/mxc/sandbox.rb, line 57 def directory(path) = configure { |sandbox| sandbox.with_directory(path) }
Sets every instance’s working directory.
Source
# File lib/mxc/sandbox.rb, line 54 def env(variables) = configure { |sandbox| sandbox.with_env(variables) }
Adds environment variables for every instance.
Source
# File lib/mxc/sandbox.rb, line 51 def network(...) = configure { |sandbox| sandbox.with_network(...) }
Sets every instance’s network access. Takes the same arguments as with_network.
Source
# File lib/mxc/sandbox.rb, line 101 def initialize(**options) unknown = options.keys - OPTIONS raise ArgumentError, "unknown option#{'s' if unknown.size > 1}: #{unknown.join(', ')}" if unknown.any? inherit(self.class.defaults) options.each { |name, value| apply(name, value) } end
Returns a new sandbox with the class’s defaults, then applies options through the matching setters: readable:, writable:, denied:, network:, env:, directory:, timeout: and backend:.
MXC::Sandbox.new(readable: "app/skills", network: { allow: ["pypi.org"] }, timeout: 30)
Source
# File lib/mxc/sandbox.rb, line 42 def readable(*paths) = configure { |sandbox| sandbox.with_readable(*paths) }
Adds paths every instance can read.
Source
# File lib/mxc/sandbox.rb, line 60 def timeout(seconds) = configure { |sandbox| sandbox.with_timeout(seconds) }
Sets every instance’s timeout in seconds.
Source
# File lib/mxc/sandbox.rb, line 45 def writable(*paths) = configure { |sandbox| sandbox.with_writable(*paths) }
Adds paths every instance can read and write.
Public Instance Methods
Source
# File lib/mxc/sandbox.rb, line 243 def availability problem = Preflight.problem(network:, denied:) return MXC.availability(backend:) unless problem Availability.new(backend:, supported: false, reason: problem.message, remediation: problem.remediation) end
Returns the Availability of this sandbox on this host, with the reason and remediation when it cannot run.
Source
# File lib/mxc/sandbox.rb, line 136 def backend = @settings[:backend] || MXC.config.backend
Returns the backend, falling back to MXC.config.backend.
Source
# File lib/mxc/sandbox.rb, line 121 def denied = @settings[:denied].dup.freeze
Returns the paths hidden from the command, as given.
Source
# File lib/mxc/sandbox.rb, line 130 def directory = @settings[:directory]
Returns the working directory, or nil for the default.
Source
# File lib/mxc/sandbox.rb, line 124 def env = @settings[:env].dup.freeze
Returns the environment variables set for the command.
Source
# File lib/mxc/sandbox.rb, line 127 def network = @settings[:network]
Returns the network access: false, true, or { allow: [...] } for an allow-list.
Source
# File lib/mxc/sandbox.rb, line 115 def readable = @settings[:readable].dup.freeze
Returns the paths the command can read, as given.
Source
# File lib/mxc/sandbox.rb, line 221 def run(*command, &) return stream(*command, &) if block_given? outcome = StartupFailure.check!(Native.run(command_line(command), request)) Result.new(**outcome, backend: resolved_backend) end
Runs command and returns its Result once it finishes. Like Kernel#system, one String runs through the platform shell: sh -c, or cmd.exe /d /s /c on Windows. Two or more Strings are a program and its arguments, and each argument reaches the program unchanged, without a shell.
With a block, yields each Chunk of output as it arrives.
sandbox.run("python3 build.py && ls dist") sandbox.run("python3", "convert.py", params[:filename]) sandbox.run("python3", "build.py") { |chunk| print chunk.content }
Raises ArgumentError without a command or when given an Array. On Windows, it also raises ArgumentError for a program and arguments that start a .bat or .cmd file, because Windows runs those through cmd.exe, which splits the arguments again.
Source
# File lib/mxc/sandbox.rb, line 232 def spawn(*command) Process.new(Native.spawn(command_line(command), request), backend: resolved_backend) end
Source
# File lib/mxc/sandbox.rb, line 237 def supported? availability.supported? end
Returns true when this host can run this sandbox, including its network policy.
Source
# File lib/mxc/sandbox.rb, line 133 def timeout = @settings[:timeout] || MXC.config.timeout
Returns the timeout in seconds, falling back to MXC.config.timeout.
Source
# File lib/mxc/sandbox.rb, line 202 def with_backend(name) @settings[:backend] = name && self.class.backend_name(name) self end
Sets the backend: :auto (Bubblewrap on Linux, Seatbelt on macOS, ProcessContainer on Windows), :bubblewrap, :seatbelt, or :process_container.
Source
# File lib/mxc/sandbox.rb, line 150 def with_denied(*paths) add_paths(:denied, paths) end
Adds paths hidden from the command, even inside readable or writable paths.
Source
# File lib/mxc/sandbox.rb, line 187 def with_directory(path) @settings[:directory] = path && File.path(path) self end
Sets the directory the command starts in. The sandbox must be able to read it. Without one, the command starts in /, or %SystemRoot% on Windows.
Source
# File lib/mxc/sandbox.rb, line 179 def with_env(variables) env = @settings[:env] variables.nil? ? env.clear : variables.each { |name, value| env[name.to_s] = value.to_s } self end
Adds environment variables for the command. The sandbox starts from a minimal environment, not the parent’s. On Windows, that environment keeps the system variables programs need to start, such as SYSTEMROOT and PATH.
sandbox.with_env("LANG" => "C.UTF-8")
# File lib/mxc/sandbox.rb, line 160 def with_network(enabled = true, allow: nil) if allow raise ArgumentError, 'pass allow: or true/false, not both' unless enabled == true @settings[:network] = { allow: Array(allow).map(&:to_s).freeze }.freeze else raise ArgumentError, "network must be true, false or nil, got #{enabled.inspect}" unless [true, false, nil].include?(enabled) @settings[:network] = enabled || false end self end
Sets network access. The default, false, gives the command no network at all. true allows any destination. allow: takes a list of hosts the command may reach over HTTP and HTTPS: "pypi.org", "*.pythonhosted.org" (subdomains only), or "example.com:8443" for another port.
sandbox.with_network(allow: ["pypi.org", "*.pythonhosted.org"])
Source
# File lib/mxc/sandbox.rb, line 140 def with_readable(*paths) add_paths(:readable, paths) end
Adds paths the command can read. Accepts Strings and Pathnames; relative paths are expanded from the current directory when the command runs.
Source
# File lib/mxc/sandbox.rb, line 194 def with_timeout(seconds) @settings[:timeout] = self.class.duration(seconds) self end
Sets the timeout in seconds. Accepts anything that responds to to_f, such as 30, 2.5 or 1.minute.
Source
# File lib/mxc/sandbox.rb, line 145 def with_writable(*paths) add_paths(:writable, paths) end
Adds paths the command can read and write.
Source
# File lib/mxc/sandbox.rb, line 118 def writable = @settings[:writable].dup.freeze
Returns the paths the command can read and write, as given.