security_box 0.1.0 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 22aa6209552c4b4a4fbc60fa98b71e6f70448fb308e3c8f133515ef663c32d7c
4
- data.tar.gz: a33ce5bc7a235946efbd283eb3a8f5a718dc87a971eeb3eff799c5b6701ed172
3
+ metadata.gz: bdcc33a7689dd5d68b5e9ca6c8cd36a83d8da9aa5a44c9b29a5994bd991c5d9e
4
+ data.tar.gz: a2f4fbf283527b9fcf484a9dc06adb3a5d34d2a1c97f355412a880c5204c9283
5
5
  SHA512:
6
- metadata.gz: aa260831067c04ed120b56d082b068ffd606a060999921e5e4f21f925eb4b721cd37c6e786a646cf024e02a5eba05f4e8e0354311045df76d3d9fb99838aa0fd
7
- data.tar.gz: 1d0f95de215b37537d2ba3ca638120599d0461d50b783e1e5b52c5c56375d5729f4c1978e815082dc8c824a0389c1f765147cdf5d06f29981ad05e0f99c7ebc3
6
+ metadata.gz: 86158b7097c7e53602f80949350d2f2aebaf9f672400f908127fb995fbf829dc5a16928873231d1c2b8539660a0f7c5a0c2f600f40a225558afaf39f83a9ee77
7
+ data.tar.gz: e08ee261ac36c91b3fe6761a01e655410d60c3023094e852c7e1fa9ddc8728efe646366bd840c6863264b1f41826874794b52e6a1125dddbca8f662ae3f76fe4
data/CHANGELOG.md CHANGED
@@ -5,6 +5,32 @@ 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.2.0] - 2026-09-14
9
+
10
+ ### Added
11
+
12
+ - Compiled-module disk cache (`~/.cache/security_box/modules`, override with
13
+ `SECURITY_BOX_CACHE_DIR`): new processes boot the runtime in ~0.5s instead
14
+ of ~15s by deserializing the compiled artifact instead of recompiling.
15
+ Best effort — misses, corruption or an unwritable directory fall back to a
16
+ normal compile.
17
+ - Result-channel integrity: a per-eval random token is passed to the guest via
18
+ `SB_TOKEN`, captured and scrubbed by the hardening prelude, and embedded in
19
+ both the `/work/out.json` envelope and the stdout sentinel line. The host
20
+ validates the envelope schema and token; forged results (e.g. an `at_exit`
21
+ overwrite or a fake sentinel) become `:sandbox_error`.
22
+ - Hardening prelude (`lib/security_box/guest/prelude.rb`): `system`, `exec`,
23
+ `Kernel#spawn`, backticks, `IO.popen`, `Process.spawn` and `Kernel#open` now
24
+ raise `SecurityError` inside the guest (they previously were misleading
25
+ WASI stubs); `ENV` is scrubbed before user code runs; `$stdout.sync = true`.
26
+
27
+ ### Changed
28
+
29
+ - Guest result envelope carries a `token` field; `system(...)` inside guest
30
+ code raises `SecurityError` instead of returning a stub `true`.
31
+ - The sandbox image must be repacked (`rake security_box:build_image`) when
32
+ upgrading: the guest entrypoint protocol changed.
33
+
8
34
  ## [0.1.0] - 2026-09-13
9
35
 
10
36
  ### Added
data/README.md CHANGED
@@ -16,13 +16,16 @@ tries to escape.
16
16
  ## How it works
17
17
 
18
18
  1. A `ruby.wasm` image is packed with the Ruby runtime + stdlib + a small guest
19
- entrypoint (`lib/security_box/guest/main.rb`).
19
+ entrypoint (`lib/security_box/guest/main.rb`) plus a hardening prelude
20
+ (`lib/security_box/guest/prelude.rb`).
20
21
  2. On every `#eval`, the host creates an exclusive tmpdir, writes the user code to it, and
21
- mounts it read-write as `/work` inside the sandbox.
22
- 3. A fresh wasm instance boots, evaluates the code and serializes a JSON envelope to
23
- `/work/out.json`.
24
- 4. The host reads the envelope, maps any wasmtime trap to a `Result` status, closes the
25
- store and discards the tmpdir.
22
+ mounts it read-write as `/work` inside the sandbox. It also generates a per-eval
23
+ random token and passes it to the guest via `SB_TOKEN`.
24
+ 3. A fresh wasm instance boots; the prelude captures the token, scrubs `ENV` and
25
+ neutralizes process-spawn APIs, then the guest evaluates the code and serializes a
26
+ token-signed JSON envelope to `/work/out.json` (stdout sentinel fallback).
27
+ 4. The host validates the envelope (schema + token — forged results are rejected),
28
+ maps any wasmtime trap to a `Result` status, closes the store and discards the tmpdir.
26
29
 
27
30
  One wasm process per `#eval` means zero residual state between executions.
28
31
 
@@ -87,15 +90,18 @@ result.duration_ms # => 257.0 (approx; dominated by the ruby.wasm boot)
87
90
 
88
91
  ### Warming up
89
92
 
90
- The first `eval` in a process pays the cold WebAssembly compilation of the image
91
- (~15s) plus the engine setup. Call `SecurityBox.warmup` ahead of time (e.g., at
92
- boot) to pay that cost up front — every subsequent `eval` then only pays the
93
- ~240ms guest boot:
93
+ The first `eval` in a process pays either the cold WebAssembly compilation of the image
94
+ (~15s) or a fast deserialize from the compiled-module disk cache (~0.5s) plus the engine
95
+ setup. The cache lives in `~/.cache/security_box/modules` (override with
96
+ `SECURITY_BOX_CACHE_DIR`), is keyed by the image content, and is fully best effort —
97
+ on any miss or corruption the module is simply recompiled. Call `SecurityBox.warmup`
98
+ ahead of time (e.g., at boot) to pay that cost up front — every subsequent `eval` then
99
+ only pays the ~260ms guest boot:
94
100
 
95
101
  ```ruby
96
102
  SecurityBox.warmup
97
103
 
98
- SecurityBox.eval("40 + 2").value # => 42 (~250ms, no cold compile)
104
+ SecurityBox.eval("40 + 2").value # => 42 (~260ms, no cold compile)
99
105
  ```
100
106
 
101
107
  `#eval` warms the same caches lazily on first use, so calling `warmup` is a pure
@@ -184,11 +190,13 @@ string.
184
190
  |---|---|
185
191
  | `File.write("/etc/passwd", ...)` | `Errno::ENOENT` (guest does not see the host FS) |
186
192
  | `Dir["/*"]` | `[]` (empty root) |
187
- | `system("ls")` / backticks / `IO.popen` | no-op stubs / `ArgumentError` (nothing executes) |
193
+ | `system("ls")` / backticks / `IO.popen` / `Process.spawn` | `SecurityError` (hardening prelude) |
194
+ | `Kernel#open(...)` | `SecurityError` (pipe form; use `File.open`) |
188
195
  | `fork` | `NotImplementedError` |
189
196
  | `Thread.new` | `NotImplementedError` (WASI p1 has no threads) |
190
197
  | `require "socket"` | `LoadError` (no network) |
191
- | `ENV` | `{}` (sanitized) |
198
+ | `ENV` | `{}` (token read and scrubbed by the prelude) |
199
+ | forge `out.json` via `at_exit` / fake sentinel | rejected (token mismatch → `:sandbox_error`) |
192
200
  | infinite loop | killed by epoch deadline or fuel budget |
193
201
 
194
202
  Notes:
@@ -230,3 +238,5 @@ bundle exec ruby bin/spike.rb
230
238
 
231
239
  - `docs/PLAN.md` — architecture, roadmap and threat model
232
240
  - `docs/plan/stages/stage_1.md` — stage 1 findings and measured numbers
241
+ - `docs/plan/stages/stage_2.md` — stage 2 findings (hardening prelude, result-channel
242
+ integrity, compiled-module disk cache)
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module SecurityBox
6
+ # Host-side validation of the guest result envelope.
7
+ #
8
+ # The guest reports its result either via /work/out.json or, when /work is
9
+ # unavailable, via a sentinel line on stdout (format:
10
+ # `__SECURITY_BOX_RESULT__:<token>:<json>`). Both embed the per-eval random
11
+ # token the host generated. An envelope is trusted only when it parses, the
12
+ # token matches and every field has the expected type; otherwise nil is
13
+ # returned and the execution is reported as a sandbox failure.
14
+ module Envelope
15
+ # Keep in sync with lib/security_box/guest/main.rb.
16
+ SENTINEL = "__SECURITY_BOX_RESULT__"
17
+
18
+ class << self
19
+ # Parses `raw` (JSON text) and validates it against `token`.
20
+ # Returns the envelope Hash or nil when invalid/untrusted.
21
+ def parse(raw, token)
22
+ return nil unless raw.is_a?(String) && token.is_a?(String) && !token.empty?
23
+
24
+ envelope = JSON.parse(raw)
25
+ return nil unless valid?(envelope, token)
26
+
27
+ envelope
28
+ rescue JSON::ParserError, TypeError, ArgumentError
29
+ nil
30
+ end
31
+
32
+ # Scans captured stdout for exactly one sentinel line carrying `token`.
33
+ # Zero or multiple sentinel lines (e.g. a guest forging an extra one)
34
+ # yield nil.
35
+ def from_stdout(stdout, token)
36
+ prefix = "#{SENTINEL}:"
37
+ lines = stdout.to_s.split("\n").select { |line| line.start_with?(prefix) }
38
+ return nil unless lines.size == 1
39
+
40
+ _sentinel, _embedded_token, json = lines.first.split(":", 3)
41
+ parse(json, token)
42
+ end
43
+
44
+ private
45
+
46
+ def valid?(envelope, token)
47
+ return false unless envelope.is_a?(Hash)
48
+ return false unless [true, false].include?(envelope["ok"])
49
+ return false unless envelope["token"] == token
50
+
51
+ if envelope["ok"]
52
+ return false unless envelope.key?("value")
53
+ else
54
+ error = envelope["error"]
55
+ return false unless error.is_a?(Hash)
56
+ return false unless error["class"].is_a?(String)
57
+ return false unless error["message"].is_a?(String)
58
+ end
59
+
60
+ duration = envelope["duration_ms"]
61
+ duration.nil? || duration.is_a?(Numeric)
62
+ end
63
+ end
64
+ end
65
+ end
@@ -3,11 +3,16 @@
3
3
  # Input: /work/code.rb (preferred) or ARGV[0].
4
4
  # Result: /work/out.json (JSON envelope); fallback: sentinel line on stdout.
5
5
  #
6
- # NOTE (stage 1): the result channel is reliable against accidents, not against
7
- # an active adversary (guest code can forge the envelope). Token mitigations and
8
- # schema validation land in Stage 2.
6
+ # Integrity: the host generates a per-eval random token and passes it via ENV;
7
+ # the prelude captures it and scrubs ENV before user code runs. Both the
8
+ # envelope and the sentinel line embed the token, and the host rejects any
9
+ # result that does not carry the expected token (see lib/security_box/envelope.rb).
10
+ # This hardens the channel against forged results (e.g. an at_exit handler
11
+ # overwriting out.json); it is defense in depth, not a cryptographic guarantee.
9
12
  require "json"
10
13
 
14
+ require_relative "prelude"
15
+
11
16
  module SB
12
17
  SENTINEL = "__SECURITY_BOX_RESULT__"
13
18
  WORK_DIR = "/work"
@@ -15,13 +20,15 @@ module SB
15
20
  module_function
16
21
 
17
22
  def run
23
+ token = SBPrelude.apply!
24
+
18
25
  code = fetch_code
19
- return fatal("no code provided") unless code
26
+ return fatal("no code provided", token) unless code
20
27
 
21
28
  t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
22
29
  out = evaluate(code)
23
30
  out[:duration_ms] = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0) * 1000).round(2)
24
- emit(out)
31
+ emit(out, token)
25
32
  end
26
33
 
27
34
  def fetch_code
@@ -47,15 +54,16 @@ module SB
47
54
  end
48
55
 
49
56
  # Preferred: /work/out.json. Fallback (no /work): sentinel on stdout.
50
- def emit(out)
51
- json = JSON.generate(out)
57
+ # Both carry the sandbox token so the host can reject forged results.
58
+ def emit(out, token)
59
+ json = JSON.generate(out.merge(token: token))
52
60
  File.write(File.join(WORK_DIR, "out.json"), json)
53
61
  rescue StandardError, SystemCallError
54
- $stdout.puts "#{SENTINEL}:#{json}"
62
+ $stdout.puts "#{SENTINEL}:#{token}:#{json}"
55
63
  end
56
64
 
57
- def fatal(message)
58
- emit({ ok: false, value: nil, error: { "class" => "SecurityBox::Guest", "message" => message } })
65
+ def fatal(message, token)
66
+ emit({ ok: false, value: nil, error: { "class" => "SecurityBox::Guest", "message" => message } }, token)
59
67
  end
60
68
  end
61
69
 
@@ -0,0 +1,71 @@
1
+ # Hardening prelude — loaded before user code (defense in depth; WASI is the
2
+ # primary isolation boundary).
3
+ #
4
+ # Responsibilities (in order):
5
+ # 1. capture the per-eval sandbox token from ENV and hand it to the guest
6
+ # protocol exactly once (main.rb), erasing every other reference;
7
+ # 2. scrub ENV so user code sees an empty environment;
8
+ # 3. neutralize the process-spawn APIs that WASI leaves as misleading stubs
9
+ # (e.g. `system` returns true without running anything) by raising
10
+ # SecurityError instead;
11
+ # 4. disable Kernel#open entirely (its pipe form executes processes; user
12
+ # code must use File.open/IO.read for plain files);
13
+ # 5. set $stdout.sync so captured output is not lost on a hard kill.
14
+ module SBPrelude
15
+ DENIED_MESSAGE = "process execution is not allowed inside the sandbox"
16
+
17
+ class << self
18
+ # Applies all hardening and returns the sandbox token (or nil). The token
19
+ # is removed from the prelude state; afterwards it exists only in the
20
+ # caller's local scope.
21
+ def apply!
22
+ capture_token
23
+ scrub_env
24
+ neutralize_process_apis
25
+ sync_stdout
26
+ take_token
27
+ end
28
+
29
+ # Returns the token captured by apply! and erases the stored reference.
30
+ def take_token
31
+ token = @token
32
+ remove_instance_variable(:@token) if instance_variable_defined?(:@token)
33
+ token
34
+ end
35
+
36
+ private
37
+
38
+ def capture_token
39
+ @token = ENV["SB_TOKEN"]
40
+ end
41
+
42
+ def scrub_env
43
+ ENV.each_key { |key| ENV.delete(key) }
44
+ end
45
+
46
+ def neutralize_process_apis
47
+ %w[system exec spawn `].each do |name|
48
+ neutralize_instance_method(Kernel, name)
49
+ end
50
+ neutralize_instance_method(Kernel, "open")
51
+ neutralize_singleton_method(IO, :popen)
52
+ neutralize_singleton_method(Process, :spawn)
53
+ end
54
+
55
+ def neutralize_instance_method(owner, name)
56
+ owner.module_eval do
57
+ define_method(name) { |*args, **kwargs| raise SecurityError, DENIED_MESSAGE }
58
+ end
59
+ end
60
+
61
+ def neutralize_singleton_method(owner, name)
62
+ owner.define_singleton_method(name) do |*args, **kwargs|
63
+ raise SecurityError, DENIED_MESSAGE
64
+ end
65
+ end
66
+
67
+ def sync_stdout
68
+ $stdout.sync = true
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+ require "fileutils"
5
+ require "wasmtime"
6
+
7
+ module SecurityBox
8
+ # Content-addressed disk cache for compiled wasm modules.
9
+ #
10
+ # Compiling the ruby.wasm image costs ~15s per process. This cache stores the
11
+ # compiled artifact (Module#serialize) so later processes pay only a fast
12
+ # deserialize. Files live at <cache_dir>/modules/<key>.cwasm, where the key
13
+ # combines the image content digest with the wasmtime precompile compatibility
14
+ # key — a stored module is only reused when the running engine can consume it.
15
+ #
16
+ # Everything here is best effort: on any I/O or deserialization failure the
17
+ # caller falls back to a regular compile and the cache self-heals on the next
18
+ # successful store. Set SECURITY_BOX_CACHE_DIR to relocate the cache; the
19
+ # default is ~/.cache/security_box.
20
+ module ModuleCache
21
+ CACHE_DIR_ENV_VAR = "SECURITY_BOX_CACHE_DIR"
22
+
23
+ class << self
24
+ # Returns the cached module for this engine+image, or nil on cache miss,
25
+ # corrupted cache file or any cache error.
26
+ def load_module(engine, image_path)
27
+ path = cache_path(engine, image_path)
28
+ return nil unless path && File.file?(path)
29
+
30
+ Wasmtime::Module.deserialize_file(engine, path)
31
+ rescue Wasmtime::Error, TypeError, SystemCallError
32
+ nil
33
+ end
34
+
35
+ # Compiles the image and stores the serialized artifact in the cache
36
+ # (best effort). Returns the compiled module in all cases.
37
+ def compile_and_store(engine, image_path)
38
+ module_ = Wasmtime::Module.from_file(engine, image_path)
39
+ store(engine, image_path, module_)
40
+ module_
41
+ end
42
+
43
+ def store(engine, image_path, module_)
44
+ path = cache_path(engine, image_path)
45
+ return if path.nil?
46
+
47
+ dir = File.dirname(path)
48
+ FileUtils.mkdir_p(dir)
49
+ tmp = File.join(dir, ".#{File.basename(path)}.#{Process.pid}.tmp")
50
+ File.binwrite(tmp, module_.serialize)
51
+ File.rename(tmp, path)
52
+ rescue Wasmtime::Error, SystemCallError
53
+ File.delete(tmp) if defined?(tmp) && tmp && File.exist?(tmp)
54
+ nil
55
+ end
56
+
57
+ # Nil when no usable cache directory exists (cache disabled).
58
+ def cache_path(engine, image_path)
59
+ dir = modules_dir
60
+ return nil unless dir
61
+
62
+ File.join(dir, "#{cache_key(engine, image_path)}.cwasm")
63
+ end
64
+
65
+ def modules_dir
66
+ base = ENV[CACHE_DIR_ENV_VAR] || default_base_dir
67
+ return nil unless base
68
+
69
+ File.expand_path("modules", base)
70
+ end
71
+
72
+ private
73
+
74
+ def default_base_dir
75
+ home = ENV["HOME"]
76
+ home && File.join(home, ".cache", "security_box")
77
+ end
78
+
79
+ # Digest cost scales with the image size (~0.7s for the 110MB ruby.wasm
80
+ # image); memoized per file identity so it is paid once per process
81
+ # (and recomputed only if the file changes on disk).
82
+ def cache_key(engine, image_path)
83
+ @keys ||= {}
84
+ stat = File.stat(image_path)
85
+ key = @keys[[image_path, stat.size, stat.mtime]]
86
+ return key if key
87
+
88
+ image_digest = Digest::SHA256.file(image_path).hexdigest
89
+ @keys[[image_path, stat.size, stat.mtime]] =
90
+ Digest::SHA256.hexdigest("#{image_digest}:#{engine.precompile_compatibility_key}")
91
+ end
92
+ end
93
+ end
94
+ end
@@ -2,13 +2,16 @@
2
2
 
3
3
  require "wasmtime"
4
4
 
5
+ require_relative "module_cache"
6
+
5
7
  module SecurityBox
6
8
  # Registry of heavyweight artifacts shared across sandboxes:
7
9
  # - Engine (one per runtime configuration; owns the epoch timer)
8
10
  # - Compiled Module (one per engine+image)
9
11
  #
10
- # Module compilation is slow (~15s the first time per process); the disk cache
11
- # (Module#serialize) lands in Stage 2.
12
+ # Module compilation is slow (~15s the first time per process); the compiled
13
+ # artifact is persisted via ModuleCache so later processes deserialize it
14
+ # from disk instead of recompiling.
12
15
  module Runtime
13
16
  MUTEX = Mutex.new
14
17
 
@@ -24,8 +27,10 @@ module SecurityBox
24
27
 
25
28
  def module_for(engine, image_path)
26
29
  MUTEX.synchronize do
27
- @modules[[engine.object_id, image_path]] ||=
28
- Wasmtime::Module.from_file(engine, image_path)
30
+ @modules[[engine.object_id, image_path]] ||= begin
31
+ ModuleCache.load_module(engine, image_path) ||
32
+ ModuleCache.compile_and_store(engine, image_path)
33
+ end
29
34
  end
30
35
  end
31
36
 
@@ -10,12 +10,17 @@ module SecurityBox
10
10
  # Flow:
11
11
  # 1. exclusive tmpdir mounted as /work (read-write)
12
12
  # 2. user code written to /work/code.rb
13
- # 3. guest (/src/main.rb) evaluates it and writes /work/out.json
14
- # 4. host reads the envelope and returns a Result
13
+ # 3. a per-eval random token is passed via ENV (SB_TOKEN); the guest
14
+ # prelude captures it and scrubs ENV before user code runs
15
+ # 4. guest (/src/main.rb) evaluates the code and writes /work/out.json,
16
+ # embedding the token (stdout sentinel fallback does the same)
17
+ # 5. host reads the envelope, validates schema + token (untrusted results
18
+ # become a sandbox failure) and returns a Result
15
19
  class Sandbox
16
20
  GUEST_ENTRYPOINT = "/src/main.rb"
17
21
  RESULT_FILE = "out.json"
18
22
  CODE_FILE = "code.rb"
23
+ TOKEN_ENV_VAR = "SB_TOKEN"
19
24
 
20
25
  def initialize(configuration = Configuration.build)
21
26
  @config = configuration
@@ -32,6 +37,7 @@ module SecurityBox
32
37
  config = overrides.empty? ? @config : @config.with(**overrides)
33
38
  stdout = +""
34
39
  stderr = +""
40
+ token = SecureRandom.hex(16)
35
41
  t0 = monotonic_ms
36
42
 
37
43
  Dir.mktmpdir("security_box") do |workdir|
@@ -42,7 +48,7 @@ module SecurityBox
42
48
 
43
49
  store = Wasmtime::Store.new(
44
50
  @engine,
45
- wasi_p1_config: build_wasi(workdir, stdout, stderr, config),
51
+ wasi_p1_config: build_wasi(workdir, stdout, stderr, config, token),
46
52
  limits: { memory_size: config.memory_size }
47
53
  )
48
54
  begin
@@ -51,7 +57,7 @@ module SecurityBox
51
57
  store.set_epoch_deadline(epoch_ticks(config))
52
58
  status = invoke_guest(instance, store)
53
59
  fuel_used = config.fuel - store.get_fuel
54
- envelope = read_envelope(workdir)
60
+ envelope = read_envelope(workdir, stdout, token)
55
61
  ensure
56
62
  store.close
57
63
  end
@@ -62,13 +68,13 @@ module SecurityBox
62
68
 
63
69
  private
64
70
 
65
- def build_wasi(workdir, stdout, stderr, config)
71
+ def build_wasi(workdir, stdout, stderr, config, token)
66
72
  Wasmtime::WasiConfig.new
67
73
  .set_stdin_string("")
68
74
  .set_stdout_buffer(stdout, config.stdout_limit)
69
75
  .set_stderr_buffer(stderr, config.stderr_limit)
70
76
  .set_argv(["ruby", GUEST_ENTRYPOINT])
71
- .set_env(config.env)
77
+ .set_env(config.env.merge(TOKEN_ENV_VAR => token))
72
78
  .set_mapped_directory(workdir, "/work", :read_write)
73
79
  end
74
80
 
@@ -110,12 +116,16 @@ module SecurityBox
110
116
  end
111
117
  end
112
118
 
113
- def read_envelope(workdir)
114
- path = File.join(workdir, RESULT_FILE)
115
- return nil unless File.exist?(path)
119
+ def read_envelope(workdir, stdout, token)
120
+ # Preferred channel: /work/out.json. Fallback: sentinel line on stdout.
121
+ # Both go through the same strict validation (schema + token).
122
+ Envelope.parse(read_result_file(workdir), token) ||
123
+ Envelope.from_stdout(stdout, token)
124
+ end
116
125
 
117
- JSON.parse(File.read(path))
118
- rescue JSON::ParserError
126
+ def read_result_file(workdir)
127
+ File.read(File.join(workdir, RESULT_FILE))
128
+ rescue SystemCallError
119
129
  nil
120
130
  end
121
131
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SecurityBox
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
data/lib/security_box.rb CHANGED
@@ -4,7 +4,9 @@ require_relative "security_box/version"
4
4
  require_relative "security_box/errors"
5
5
  require_relative "security_box/configuration"
6
6
  require_relative "security_box/result"
7
+ require_relative "security_box/module_cache"
7
8
  require_relative "security_box/runtime"
9
+ require_relative "security_box/envelope"
8
10
  require_relative "security_box/sandbox"
9
11
 
10
12
  module SecurityBox
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.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Marcelo Junior
@@ -39,8 +39,11 @@ files:
39
39
  - lib/security_box.rb
40
40
  - lib/security_box/assets/security_box.wasm
41
41
  - lib/security_box/configuration.rb
42
+ - lib/security_box/envelope.rb
42
43
  - lib/security_box/errors.rb
43
44
  - lib/security_box/guest/main.rb
45
+ - lib/security_box/guest/prelude.rb
46
+ - lib/security_box/module_cache.rb
44
47
  - lib/security_box/result.rb
45
48
  - lib/security_box/runtime.rb
46
49
  - lib/security_box/sandbox.rb