security_box 0.2.0 → 0.3.0
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 +4 -4
- data/CHANGELOG.md +27 -0
- data/README.md +52 -8
- data/lib/security_box/assets/security_box.wasm +0 -0
- data/lib/security_box/configuration.rb +61 -0
- data/lib/security_box/envelope.rb +9 -0
- data/lib/security_box/guest/main.rb +33 -3
- data/lib/security_box/registry.rb +65 -0
- data/lib/security_box/sandbox.rb +19 -1
- data/lib/security_box/version.rb +1 -1
- data/lib/security_box.rb +28 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9949a172b6bf1ddf1eddd9756c8dcbad273d8f2409669304227bb2f981c9fef6
|
|
4
|
+
data.tar.gz: 499c948e3fd57c221f8aef7f8ad6dd63757b36e9d5a4a62d8c8a70823908f86d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1cf51781bb108aa6849ae24bd1db5b812c7e0a7e94f0d1876eb1a860cabc4a6b00241fdc19b4b142b88da8bdf86ed6e9c5dc53009237f36706a4ff637fe96d5f
|
|
7
|
+
data.tar.gz: 39dfd3af95bc8bfd2e3bd79ac57b118ee706b2f7da3aee98854aea832094b29342e751b2a41bf630c593fd07289ecf7ac041b1d416682924f276a29be4a7259b
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,33 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.3.0] - 2026-09-14
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Named, reusable profiles: `SecurityBox.register(:name, from: :base) { |c| c.fuel 100 }`
|
|
13
|
+
and `SecurityBox.spawn(:name, **overrides)` (a `Configuration` or the defaults also
|
|
14
|
+
work). Profiles are immutable; duplicate/unknown names raise
|
|
15
|
+
`SecurityBox::InvalidConfiguration`.
|
|
16
|
+
- `SecurityBox::Configuration#fingerprint`: a stable identity for equal settings
|
|
17
|
+
(SHA-256 of the canonical configuration), ready for artifact caches.
|
|
18
|
+
- Guest error envelopes now carry a `backtrace`: user frames only, capped at 20,
|
|
19
|
+
sandbox-internal locations (no host paths). Surfaced via `Result#error["backtrace"]`;
|
|
20
|
+
malformed backtraces invalidate the envelope (`:sandbox_error`).
|
|
21
|
+
- `Result#fuel_used` is now `nil` for `:timeout`: epoch traps restore fuel to the last
|
|
22
|
+
checkpoint, so the restored value understated consumption by orders of magnitude.
|
|
23
|
+
(Truthful on completion, fuel exhaustion and memory-limit traps.)
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- A guest `NoMemoryError` (interpreter OOM against the store limit) is reported as
|
|
28
|
+
`:memory_limit` instead of `:error`.
|
|
29
|
+
- A `memory_size` below the image's declared minimum (1528 pages ≈ 95.5 MiB) now
|
|
30
|
+
returns a `:sandbox_error` Result with a `security_box:` note on stderr instead of
|
|
31
|
+
raising `Wasmtime::Error` out of `#eval`.
|
|
32
|
+
- The sandbox image must be repacked (`rake security_box:build_image`) when
|
|
33
|
+
upgrading: the guest entrypoint protocol changed.
|
|
34
|
+
|
|
8
35
|
## [0.2.0] - 2026-09-14
|
|
9
36
|
|
|
10
37
|
### Added
|
data/README.md
CHANGED
|
@@ -144,13 +144,44 @@ SecurityBox.eval('1000.times { print "x" * 1000 }', stdout_limit: 4096).stdout.b
|
|
|
144
144
|
Exceptions raised by guest code are a *result*, not a sandbox failure:
|
|
145
145
|
|
|
146
146
|
```ruby
|
|
147
|
-
result = SecurityBox.eval(
|
|
147
|
+
result = SecurityBox.eval("def boom; raise ArgumentError, 'boom'; end; boom")
|
|
148
148
|
|
|
149
|
-
result.status
|
|
150
|
-
result.error["class"]
|
|
151
|
-
result.error["message"]
|
|
149
|
+
result.status # => :error
|
|
150
|
+
result.error["class"] # => "ArgumentError"
|
|
151
|
+
result.error["message"] # => "boom"
|
|
152
|
+
result.error["backtrace"] # => ["sandbox:1:in 'Object#boom'", "sandbox:1:in '<main>'"]
|
|
152
153
|
```
|
|
153
154
|
|
|
155
|
+
The backtrace contains guest frames only (sandbox-internal locations, capped at
|
|
156
|
+
20 frames) — nothing from the host filesystem leaks.
|
|
157
|
+
|
|
158
|
+
### Named profiles
|
|
159
|
+
|
|
160
|
+
Reusable configurations registered once and spawned as often as needed:
|
|
161
|
+
|
|
162
|
+
```ruby
|
|
163
|
+
SecurityBox.register(:default) do |c|
|
|
164
|
+
c.fuel 10_000_000_000
|
|
165
|
+
c.timeout_ms 2_000
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
SecurityBox.register(:lean, from: :default) do |c|
|
|
169
|
+
c.fuel 2_000_000_000 # boot alone costs ~1e9; see the fuel notes below
|
|
170
|
+
c.timeout_ms 500
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
box = SecurityBox.spawn(:lean) # Sandbox from the profile
|
|
174
|
+
box.eval("40 + 2").value # => 42
|
|
175
|
+
|
|
176
|
+
SecurityBox.spawn(:lean, timeout_ms: 100) # per-call override (profile unchanged)
|
|
177
|
+
SecurityBox.spawn # default configuration
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Profiles are immutable: duplicate names and unknown names raise
|
|
181
|
+
`SecurityBox::InvalidConfiguration`; overrides never mutate the profile.
|
|
182
|
+
`SecurityBox::Configuration#fingerprint` gives every configuration a stable
|
|
183
|
+
identity (equal settings → equal fingerprint), used to share runtime artifacts.
|
|
184
|
+
|
|
154
185
|
### Configuration
|
|
155
186
|
|
|
156
187
|
`SecurityBox::Configuration` is immutable; use `.build` to create and `#with` to derive:
|
|
@@ -175,11 +206,11 @@ sandbox = SecurityBox::Sandbox.new(lean)
|
|
|
175
206
|
| Status | Meaning |
|
|
176
207
|
|---|---|
|
|
177
208
|
| `:ok` | guest code ran and returned a value |
|
|
178
|
-
| `:error` | guest code raised an exception (see `#error`) |
|
|
179
|
-
| `:timeout` | interrupted by the epoch deadline (wall clock) |
|
|
209
|
+
| `:error` | guest code raised an exception (see `#error`; includes `backtrace`) |
|
|
210
|
+
| `:timeout` | interrupted by the epoch deadline (wall clock); `fuel_used` is `nil` (epoch traps restore fuel to the checkpoint) |
|
|
180
211
|
| `:fuel_exhausted` | CPU budget exhausted |
|
|
181
|
-
| `:memory_limit` | exceeded the store `memory_size` |
|
|
182
|
-
| `:sandbox_error` | sandbox failure (unexpected trap, missing/invalid envelope) |
|
|
212
|
+
| `:memory_limit` | exceeded the store `memory_size` (wasm trap or guest `NoMemoryError`) |
|
|
213
|
+
| `:sandbox_error` | sandbox failure (unexpected trap, missing/invalid envelope, `memory_size` below the image's minimum) |
|
|
183
214
|
|
|
184
215
|
Values are JSON-serialized; non-serializable objects are returned as their `inspect`
|
|
185
216
|
string.
|
|
@@ -205,6 +236,17 @@ Notes:
|
|
|
205
236
|
- Concurrency: `invoke` holds the GVL, so executions serialize per host process. Scale
|
|
206
237
|
horizontally with multiple processes (e.g., Puma workers); a stuck guest still can't
|
|
207
238
|
hang the process thanks to the epoch deadline.
|
|
239
|
+
- Ractor note: wasmtime `Engine`/`Module` are Ractor-shareable and Ractors run wasm in
|
|
240
|
+
parallel (measured ≈3.6x with 4 Ractors on 6 cores); a supported Ractor pool is on
|
|
241
|
+
the roadmap (see `docs/plan/stages/stage_3.md`).
|
|
242
|
+
- Memory: the packed image declares a 1528-page (~95.5 MiB) minimum; `memory_size`
|
|
243
|
+
below that fails instantiation (reported as `:sandbox_error`). Practical minimum is
|
|
244
|
+
~128–144MB for small workloads; the 512MB default leaves comfortable headroom.
|
|
245
|
+
- Fuel budgeting: compute workloads burn ~4–8e9 fuel/s (tight loops up to ~8.4e9/s)
|
|
246
|
+
and every eval costs ~1e9 fuel for boot — see the calibration table in
|
|
247
|
+
`docs/plan/stages/stage_3.md`. Consequently `fuel` below ~1e9 cannot even boot, and
|
|
248
|
+
`timeout_ms` below ~300ms times out during the guest boot (~500ms is a practical
|
|
249
|
+
floor).
|
|
208
250
|
|
|
209
251
|
## Running the tests
|
|
210
252
|
|
|
@@ -240,3 +282,5 @@ bundle exec ruby bin/spike.rb
|
|
|
240
282
|
- `docs/plan/stages/stage_1.md` — stage 1 findings and measured numbers
|
|
241
283
|
- `docs/plan/stages/stage_2.md` — stage 2 findings (hardening prelude, result-channel
|
|
242
284
|
integrity, compiled-module disk cache)
|
|
285
|
+
- `docs/plan/stages/stage_3.md` — stage 3 findings (named profiles, Ractor
|
|
286
|
+
parallelism, memory floor, fuel calibration, backtrace)
|
|
Binary file
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "digest"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
3
6
|
module SecurityBox
|
|
4
7
|
# Immutable sandbox configuration. Use .build to create and #with to derive.
|
|
5
8
|
class Configuration
|
|
@@ -55,6 +58,64 @@ module SecurityBox
|
|
|
55
58
|
}
|
|
56
59
|
end
|
|
57
60
|
|
|
61
|
+
# Stable identity of the configuration values (SHA-256 of the normalized
|
|
62
|
+
# hash). Two configurations with equal settings — regardless of how they
|
|
63
|
+
# were built — share the same fingerprint; any #with change produces a
|
|
64
|
+
# different one. Used to key profiles and, later, cached artifacts.
|
|
65
|
+
def fingerprint
|
|
66
|
+
Digest::SHA256.hexdigest(JSON.generate(canonical))
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def canonical
|
|
70
|
+
to_h.merge(env: @env.sort.to_h)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Mutable collector for the register DSL. Setter names match the
|
|
74
|
+
# Configuration options (no `=`, e.g. `c.fuel 100`); only changed values
|
|
75
|
+
# are collected and merged over the base profile.
|
|
76
|
+
class Builder
|
|
77
|
+
def initialize
|
|
78
|
+
@changes = {}
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def changes
|
|
82
|
+
@changes
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def image_path(value)
|
|
86
|
+
@changes[:image_path] = value
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def fuel(value)
|
|
90
|
+
@changes[:fuel] = value
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def timeout_ms(value)
|
|
94
|
+
@changes[:timeout_ms] = value
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def memory_size(value)
|
|
98
|
+
@changes[:memory_size] = value
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def stdout_limit(value)
|
|
102
|
+
@changes[:stdout_limit] = value
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def stderr_limit(value)
|
|
106
|
+
@changes[:stderr_limit] = value
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def epoch_interval_ms(value)
|
|
110
|
+
@changes[:epoch_interval_ms] = value
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Replaces the guest environment (it is not merged with the base).
|
|
114
|
+
def env(value)
|
|
115
|
+
@changes[:env] = value
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
58
119
|
private
|
|
59
120
|
|
|
60
121
|
# Resolution order (first existing path wins):
|
|
@@ -55,11 +55,20 @@ module SecurityBox
|
|
|
55
55
|
return false unless error.is_a?(Hash)
|
|
56
56
|
return false unless error["class"].is_a?(String)
|
|
57
57
|
return false unless error["message"].is_a?(String)
|
|
58
|
+
return false unless valid_backtrace?(error["backtrace"])
|
|
58
59
|
end
|
|
59
60
|
|
|
60
61
|
duration = envelope["duration_ms"]
|
|
61
62
|
duration.nil? || duration.is_a?(Numeric)
|
|
62
63
|
end
|
|
64
|
+
|
|
65
|
+
# Optional: an array of backtrace frame strings (guest-side errors).
|
|
66
|
+
def valid_backtrace?(backtrace)
|
|
67
|
+
return true if backtrace.nil?
|
|
68
|
+
return false unless backtrace.is_a?(Array)
|
|
69
|
+
|
|
70
|
+
backtrace.all?(String)
|
|
71
|
+
end
|
|
63
72
|
end
|
|
64
73
|
end
|
|
65
74
|
end
|
|
@@ -43,7 +43,26 @@ module SB
|
|
|
43
43
|
def evaluate(code)
|
|
44
44
|
{ ok: true, value: jsonable(eval(code, TOPLEVEL_BINDING, "sandbox")) } # rubocop:disable Security/Eval,Style/EvalWithLocation
|
|
45
45
|
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
46
|
-
{
|
|
46
|
+
{
|
|
47
|
+
ok: false, value: nil,
|
|
48
|
+
error: {
|
|
49
|
+
"class" => e.class.name,
|
|
50
|
+
"message" => e.message.to_s,
|
|
51
|
+
"backtrace" => guest_backtrace(e)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Frames of the user code only: guest protocol internals (main.rb /
|
|
57
|
+
# prelude.rb) are stripped and the list is capped so a deep recursion
|
|
58
|
+
# cannot flood the result channel.
|
|
59
|
+
def guest_backtrace(exception)
|
|
60
|
+
frames = (exception.backtrace || []).reject do |frame|
|
|
61
|
+
frame.include?("main.rb") || frame.include?("prelude.rb")
|
|
62
|
+
end
|
|
63
|
+
frames.first(20)
|
|
64
|
+
rescue StandardError
|
|
65
|
+
[]
|
|
47
66
|
end
|
|
48
67
|
|
|
49
68
|
# Values must survive JSON; the round-trip normalizes what the host will read.
|
|
@@ -55,11 +74,22 @@ module SB
|
|
|
55
74
|
|
|
56
75
|
# Preferred: /work/out.json. Fallback (no /work): sentinel on stdout.
|
|
57
76
|
# Both carry the sandbox token so the host can reject forged results.
|
|
77
|
+
# The write is verified by reading it back — a silent truncation or
|
|
78
|
+
# partial write falls through to the sentinel instead of losing the
|
|
79
|
+
# result.
|
|
58
80
|
def emit(out, token)
|
|
59
81
|
json = JSON.generate(out.merge(token: token))
|
|
60
|
-
|
|
82
|
+
unless write_verified(json)
|
|
83
|
+
$stdout.puts "#{SENTINEL}:#{token}:#{json}"
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def write_verified(json)
|
|
88
|
+
path = File.join(WORK_DIR, "out.json")
|
|
89
|
+
File.write(path, json)
|
|
90
|
+
File.read(path) == json
|
|
61
91
|
rescue StandardError, SystemCallError
|
|
62
|
-
|
|
92
|
+
false
|
|
63
93
|
end
|
|
64
94
|
|
|
65
95
|
def fatal(message, token)
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SecurityBox
|
|
4
|
+
# Named, reusable profiles: `SecurityBox.register(:name, from: :base) { |c| ... }`
|
|
5
|
+
# stores an immutable Configuration built through the builder DSL; `spawn`
|
|
6
|
+
# resolves a profile and derives per-call overrides with `#with`.
|
|
7
|
+
#
|
|
8
|
+
# Profiles must be registered before they are referenced by `from:`, cannot
|
|
9
|
+
# be redefined (immutability avoids accidental shadowing) and live for the
|
|
10
|
+
# process lifetime; tests can reset the registry with `clear!`.
|
|
11
|
+
module Registry
|
|
12
|
+
MUTEX = Mutex.new
|
|
13
|
+
|
|
14
|
+
@profiles = {}
|
|
15
|
+
|
|
16
|
+
class << self
|
|
17
|
+
def register(name, from: nil, &block)
|
|
18
|
+
key = normalize(name)
|
|
19
|
+
MUTEX.synchronize do
|
|
20
|
+
raise InvalidConfiguration, "profile #{key.inspect} is already registered" if @profiles.key?(key)
|
|
21
|
+
|
|
22
|
+
builder = Configuration::Builder.new
|
|
23
|
+
block&.call(builder)
|
|
24
|
+
@profiles[key] = Configuration.build(**base_of(from), **builder.changes)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def resolve(name)
|
|
29
|
+
key = normalize(name)
|
|
30
|
+
MUTEX.synchronize do
|
|
31
|
+
@profiles.fetch(key) do
|
|
32
|
+
raise InvalidConfiguration, "unknown profile #{key.inspect}; register it first with SecurityBox.register"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def profiles
|
|
38
|
+
MUTEX.synchronize { @profiles.dup.freeze }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def clear!
|
|
42
|
+
MUTEX.synchronize { @profiles.clear }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def normalize(name)
|
|
48
|
+
unless name.is_a?(Symbol) || name.is_a?(String)
|
|
49
|
+
raise InvalidConfiguration, "profile name must be a Symbol or String, got #{name.class}"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
name.to_sym
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def base_of(from)
|
|
56
|
+
return {} if from.nil?
|
|
57
|
+
|
|
58
|
+
key = normalize(from)
|
|
59
|
+
@profiles.fetch(key) do
|
|
60
|
+
raise InvalidConfiguration, "unknown base profile #{key.inspect}; register it before deriving from it"
|
|
61
|
+
end.to_h
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
data/lib/security_box/sandbox.rb
CHANGED
|
@@ -56,8 +56,13 @@ module SecurityBox
|
|
|
56
56
|
instance = build_linker.instantiate(store, @module)
|
|
57
57
|
store.set_epoch_deadline(epoch_ticks(config))
|
|
58
58
|
status = invoke_guest(instance, store)
|
|
59
|
-
fuel_used = config.fuel - store.get_fuel
|
|
59
|
+
fuel_used = config.fuel - store.get_fuel unless status == :timeout
|
|
60
60
|
envelope = read_envelope(workdir, stdout, token)
|
|
61
|
+
rescue Wasmtime::Error => e
|
|
62
|
+
# Instantiation can fail before the guest ever runs (e.g. a
|
|
63
|
+
# memory_size below the module's declared minimum pages).
|
|
64
|
+
status = :sandbox_error
|
|
65
|
+
stderr << "security_box: #{e.class}: #{e.message}\n"
|
|
61
66
|
ensure
|
|
62
67
|
store.close
|
|
63
68
|
end
|
|
@@ -145,6 +150,15 @@ module SecurityBox
|
|
|
145
150
|
fuel_used: fuel_used, duration_ms: duration_ms,
|
|
146
151
|
guest_duration_ms: envelope["duration_ms"]
|
|
147
152
|
)
|
|
153
|
+
elsif guest_memory_error?(guest_error)
|
|
154
|
+
# The guest hit the store memory limit through the Ruby interpreter
|
|
155
|
+
# (NoMemoryError instead of a wasm trap) — report it as a limit, not
|
|
156
|
+
# a user error.
|
|
157
|
+
Result.new(
|
|
158
|
+
status: :memory_limit, error: guest_error, stdout: stdout, stderr: stderr,
|
|
159
|
+
fuel_used: fuel_used, duration_ms: duration_ms,
|
|
160
|
+
guest_duration_ms: envelope["duration_ms"]
|
|
161
|
+
)
|
|
148
162
|
else
|
|
149
163
|
Result.new(
|
|
150
164
|
status: :error, error: guest_error, stdout: stdout, stderr: stderr,
|
|
@@ -154,6 +168,10 @@ module SecurityBox
|
|
|
154
168
|
end
|
|
155
169
|
end
|
|
156
170
|
|
|
171
|
+
def guest_memory_error?(guest_error)
|
|
172
|
+
guest_error.is_a?(Hash) && guest_error["class"] == "NoMemoryError"
|
|
173
|
+
end
|
|
174
|
+
|
|
157
175
|
def monotonic_ms
|
|
158
176
|
Process.clock_gettime(Process::CLOCK_MONOTONIC) * 1000
|
|
159
177
|
end
|
data/lib/security_box/version.rb
CHANGED
data/lib/security_box.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require_relative "security_box/version"
|
|
4
4
|
require_relative "security_box/errors"
|
|
5
5
|
require_relative "security_box/configuration"
|
|
6
|
+
require_relative "security_box/registry"
|
|
6
7
|
require_relative "security_box/result"
|
|
7
8
|
require_relative "security_box/module_cache"
|
|
8
9
|
require_relative "security_box/runtime"
|
|
@@ -17,6 +18,33 @@ module SecurityBox
|
|
|
17
18
|
Sandbox.new.eval(code, **overrides)
|
|
18
19
|
end
|
|
19
20
|
|
|
21
|
+
# Registers a named, reusable profile (see Registry).
|
|
22
|
+
# SecurityBox.register(:lean, from: :default) { |c| c.fuel 5_000_000 }
|
|
23
|
+
def self.register(name, from: nil, &block)
|
|
24
|
+
Registry.register(name, from: from, &block)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Builds a Sandbox from a registered profile, a Configuration, or the
|
|
28
|
+
# defaults:
|
|
29
|
+
# SecurityBox.spawn(:lean) # named profile
|
|
30
|
+
# SecurityBox.spawn(:lean, fuel: 1_000) # profile + per-call overrides
|
|
31
|
+
# SecurityBox.spawn(my_config) # explicit configuration
|
|
32
|
+
# SecurityBox.spawn # default configuration
|
|
33
|
+
def self.spawn(profile = nil, **overrides)
|
|
34
|
+
config = case profile
|
|
35
|
+
when nil then Configuration.build(**overrides)
|
|
36
|
+
when Symbol, String
|
|
37
|
+
resolved = Registry.resolve(profile)
|
|
38
|
+
overrides.empty? ? resolved : resolved.with(**overrides)
|
|
39
|
+
when Configuration
|
|
40
|
+
overrides.empty? ? profile : profile.with(**overrides)
|
|
41
|
+
else
|
|
42
|
+
raise ArgumentError,
|
|
43
|
+
"profile must be a registered name or a Configuration, got #{profile.class}"
|
|
44
|
+
end
|
|
45
|
+
Sandbox.new(config)
|
|
46
|
+
end
|
|
47
|
+
|
|
20
48
|
# Prepares the shared runtime artifacts (Engine + compiled Module) ahead of
|
|
21
49
|
# time so the first #eval skips the cold module compilation (~15s per
|
|
22
50
|
# process). #eval warms these caches lazily on first use, so calling
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: security_box
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Marcelo Junior
|
|
@@ -44,6 +44,7 @@ files:
|
|
|
44
44
|
- lib/security_box/guest/main.rb
|
|
45
45
|
- lib/security_box/guest/prelude.rb
|
|
46
46
|
- lib/security_box/module_cache.rb
|
|
47
|
+
- lib/security_box/registry.rb
|
|
47
48
|
- lib/security_box/result.rb
|
|
48
49
|
- lib/security_box/runtime.rb
|
|
49
50
|
- lib/security_box/sandbox.rb
|