Module: Rigor::Plugin::Box

Defined in:
lib/rigor/plugin/box.rb

Overview

ADR-39 slice 5 — the isolation boundary for target-library invocation. When Rigor is launched with RUBY_BOX=1 (opt-in; the launcher re-execs only when a project opts in), a target library is loaded and called inside a Ruby::Box so its core-class monkey-patches and gem version cannot contaminate Rigor's main space. Ruby::Box is not a security sandbox (the gem's code still runs in-process) — it is the contamination boundary the rule needs, which is sufficient because the rule only ever invokes a declared, trusted, pure target library.

Disabled by default. Box.enabled? is false unless the experimental Ruby::Box feature is active, so every consumer keeps a non-box path and the default behaviour (loading the trusted library into the main space, per slices 2/4) is unchanged. The box only ever holds the trusted, project-agnostic target libraries (inflection rules, the Rack status table); per-project / exact-version boxes are a later slice.

Class Method Summary collapse

Class Method Details

.enabled?Boolean

True only when the experimental Ruby::Box feature is present and active (the process was started with RUBY_BOX=1). Any error answers false so a consumer silently keeps its non-box path.

Returns:

  • (Boolean)


21
22
23
24
25
# File 'lib/rigor/plugin/box.rb', line 21

def enabled?
  defined?(::Ruby::Box) && ::Ruby::Box.respond_to?(:enabled?) && ::Ruby::Box.enabled?
rescue StandardError
  false
end

.eval(code) ⇒ Object

Evaluates code inside the shared box and returns the result across the box boundary. The caller MUST build code safely — interpolate value arguments through String#inspect (which round-trips as a safe Ruby literal) and only ever interpolate method names from a fixed allow-list, never free user input.



64
65
66
# File 'lib/rigor/plugin/box.rb', line 64

def eval(code)
  shared.eval(code)
end

.eval_require(feature) ⇒ Object

The gem-resolving fallback: Ruby::Box#require resolves against the raw $LOAD_PATH only (it calls the C-level require directly), so a target library installed as a gem is reachable only through the box's own RubyGems — the Kernel#require inside the box. feature is a fixed, plugin-declared name rendered via String#inspect (a safe Ruby literal), so the eval carries no free input. Returns false when the box cannot load the feature either way (the caller declines).



54
55
56
57
58
59
# File 'lib/rigor/plugin/box.rb', line 54

def eval_require(feature)
  shared.eval("require #{feature.inspect}") # require "active_support/inflector"
  true
rescue ::StandardError, ::ScriptError
  false
end

.require_feature(feature) ⇒ Object

Requires feature into the shared box exactly once. Returns true when the feature is available in the box, false when it could not be loaded (the caller then declines — never falls back to loading into the main space, which would defeat the boundary).

The rescue is ::-qualified and covers ::ScriptError, matching Isolation::Process.run_worker_loop: a failed box require raises ::LoadError — a ScriptError, not a StandardError — and a plain rescue StandardError lets it escape and abort the whole run instead of the clean decline.



39
40
41
42
43
44
45
46
47
# File 'lib/rigor/plugin/box.rb', line 39

def require_feature(feature)
  @required ||= {}
  return @required[feature] if @required.key?(feature)

  shared.require(feature)
  @required[feature] = true
rescue ::StandardError, ::ScriptError
  @required[feature] = eval_require(feature)
end

.sharedObject

The process-shared box for trusted, project-agnostic target libraries. Built lazily on first use.



28
29
30
# File 'lib/rigor/plugin/box.rb', line 28

def shared
  @shared ||= ::Ruby::Box.new
end