kobako 0.3.0 → 0.5.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/.release-please-manifest.json +1 -0
- data/CHANGELOG.md +29 -0
- data/Cargo.lock +1 -1
- data/README.md +85 -6
- data/data/kobako.wasm +0 -0
- data/ext/kobako/Cargo.toml +1 -1
- data/ext/kobako/src/lib.rs +4 -2
- data/ext/kobako/src/{wasm → runtime}/cache.rs +22 -18
- data/ext/kobako/src/runtime/capture.rs +91 -0
- data/ext/kobako/src/runtime/config.rs +26 -0
- data/ext/kobako/src/runtime/dispatch.rs +211 -0
- data/ext/kobako/src/runtime/exports.rs +51 -0
- data/ext/kobako/src/runtime/guest_mem.rs +228 -0
- data/ext/kobako/src/{wasm/host_state.rs → runtime/invocation.rs} +195 -81
- data/ext/kobako/src/runtime/trap.rs +134 -0
- data/ext/kobako/src/runtime.rs +782 -0
- data/ext/kobako/src/snapshot.rs +110 -0
- data/lib/kobako/capture.rb +11 -16
- data/lib/kobako/catalog/handles.rb +107 -0
- data/lib/kobako/catalog/namespaces.rb +99 -0
- data/lib/kobako/{snippet/table.rb → catalog/snippets.rb} +37 -62
- data/lib/kobako/catalog.rb +18 -0
- data/lib/kobako/codec/decoder.rb +13 -7
- data/lib/kobako/codec/factory.rb +21 -18
- data/lib/kobako/codec/utils.rb +118 -29
- data/lib/kobako/codec.rb +6 -3
- data/lib/kobako/errors.rb +45 -28
- data/lib/kobako/fault.rb +40 -0
- data/lib/kobako/handle.rb +60 -0
- data/lib/kobako/namespace.rb +67 -0
- data/lib/kobako/outcome.rb +55 -29
- data/lib/kobako/runtime.rb +30 -0
- data/lib/kobako/sandbox.rb +131 -67
- data/lib/kobako/sandbox_options.rb +6 -9
- data/lib/kobako/snapshot.rb +40 -0
- data/lib/kobako/snippet/binary.rb +6 -7
- data/lib/kobako/snippet/source.rb +8 -8
- data/lib/kobako/snippet.rb +7 -9
- data/lib/kobako/transport/dispatcher.rb +195 -0
- data/lib/kobako/transport/error.rb +24 -0
- data/lib/kobako/transport/request.rb +78 -0
- data/lib/kobako/transport/response.rb +69 -0
- data/lib/kobako/transport/run.rb +141 -0
- data/lib/kobako/transport/yield.rb +91 -0
- data/lib/kobako/transport/yielder.rb +89 -0
- data/lib/kobako/transport.rb +24 -0
- data/lib/kobako/usage.rb +41 -0
- data/lib/kobako/version.rb +1 -1
- data/lib/kobako.rb +4 -3
- data/release-please-config.json +24 -0
- data/sig/kobako/capture.rbs +0 -2
- data/sig/kobako/{rpc/handle_table.rbs → catalog/handles.rbs} +3 -9
- data/sig/kobako/catalog/namespaces.rbs +17 -0
- data/sig/kobako/{snippet/table.rbs → catalog/snippets.rbs} +2 -11
- data/sig/kobako/{rpc.rbs → catalog.rbs} +1 -1
- data/sig/kobako/codec/decoder.rbs +2 -1
- data/sig/kobako/codec/factory.rbs +3 -3
- data/sig/kobako/codec/utils.rbs +11 -1
- data/sig/kobako/errors.rbs +7 -7
- data/sig/kobako/fault.rbs +19 -0
- data/sig/kobako/handle.rbs +18 -0
- data/sig/kobako/namespace.rbs +19 -0
- data/sig/kobako/outcome.rbs +2 -2
- data/sig/kobako/runtime.rbs +23 -0
- data/sig/kobako/sandbox.rbs +10 -7
- data/sig/kobako/snapshot.rbs +15 -0
- data/sig/kobako/transport/dispatcher.rbs +34 -0
- data/sig/kobako/transport/error.rbs +6 -0
- data/sig/kobako/transport/request.rbs +32 -0
- data/sig/kobako/transport/response.rbs +30 -0
- data/sig/kobako/transport/run.rbs +27 -0
- data/sig/kobako/transport/yield.rbs +34 -0
- data/sig/kobako/transport/yielder.rbs +21 -0
- data/sig/kobako/transport.rbs +4 -0
- data/sig/kobako/usage.rbs +11 -0
- metadata +52 -30
- data/ext/kobako/src/wasm/dispatch.rs +0 -161
- data/ext/kobako/src/wasm/instance.rs +0 -771
- data/ext/kobako/src/wasm.rs +0 -125
- data/lib/kobako/invocation.rb +0 -112
- data/lib/kobako/rpc/dispatcher.rb +0 -169
- data/lib/kobako/rpc/envelope.rb +0 -118
- data/lib/kobako/rpc/fault.rb +0 -41
- data/lib/kobako/rpc/handle.rb +0 -39
- data/lib/kobako/rpc/handle_table.rb +0 -107
- data/lib/kobako/rpc/namespace.rb +0 -74
- data/lib/kobako/rpc/server.rb +0 -158
- data/lib/kobako/rpc.rb +0 -11
- data/lib/kobako/wasm.rb +0 -25
- data/sig/kobako/invocation.rbs +0 -23
- data/sig/kobako/rpc/dispatcher.rbs +0 -33
- data/sig/kobako/rpc/envelope.rbs +0 -51
- data/sig/kobako/rpc/fault.rbs +0 -20
- data/sig/kobako/rpc/handle.rbs +0 -19
- data/sig/kobako/rpc/namespace.rbs +0 -24
- data/sig/kobako/rpc/server.rbs +0 -37
- data/sig/kobako/wasm.rbs +0 -39
data/lib/kobako/outcome.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "outcome/panic"
|
|
4
|
+
require_relative "transport/error"
|
|
4
5
|
|
|
5
6
|
module Kobako
|
|
6
7
|
# Host-facing boundary for the OUTCOME_BUFFER produced by
|
|
@@ -13,7 +14,7 @@ module Kobako
|
|
|
13
14
|
# body decoding), and the +Panic+ wire record lives at
|
|
14
15
|
# +Kobako::Outcome::Panic+. The byte-level msgpack codec at
|
|
15
16
|
# +Kobako::Codec+ is invoked for the body itself; otherwise
|
|
16
|
-
# nothing in +
|
|
17
|
+
# nothing in +Transport+ participates.
|
|
17
18
|
#
|
|
18
19
|
# * tag 0x01, decode OK → return decoded value
|
|
19
20
|
# * tag 0x01, decode fails → SandboxError (E-09)
|
|
@@ -46,11 +47,19 @@ module Kobako
|
|
|
46
47
|
|
|
47
48
|
# TrapError for unknown or absent tag
|
|
48
49
|
# ({docs/wire-codec.md ABI Signatures}[link:../../docs/wire-codec.md]:
|
|
49
|
-
#
|
|
50
|
+
# zero-length output and unrecognised first byte both walk the trap
|
|
51
|
+
# path). The user-facing message stays in caller vocabulary — the
|
|
52
|
+
# raw tag byte (or absence) belongs in +details+ for operators, not
|
|
53
|
+
# in the message a caller sees.
|
|
50
54
|
def build_trap_error(tag)
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
55
|
+
if tag.nil?
|
|
56
|
+
TrapError.new("Sandbox exited without producing a result")
|
|
57
|
+
else
|
|
58
|
+
TrapError.new(
|
|
59
|
+
"Sandbox produced an unrecognised result; the runtime is corrupted, " \
|
|
60
|
+
"discard this Sandbox before another invocation"
|
|
61
|
+
)
|
|
62
|
+
end
|
|
54
63
|
end
|
|
55
64
|
|
|
56
65
|
def split_tag(bytes)
|
|
@@ -63,11 +72,18 @@ module Kobako
|
|
|
63
72
|
end
|
|
64
73
|
|
|
65
74
|
# Decode failure on the success tag is a SandboxError (E-09): the
|
|
66
|
-
# framing was fine, but the carried value is unrepresentable.
|
|
75
|
+
# framing was fine, but the carried value is unrepresentable. The
|
|
76
|
+
# specific codec fault is stashed in +details+ rather
|
|
77
|
+
# than spliced into the message — callers cannot act on the inner
|
|
78
|
+
# "Symbol payload must be …" wording, but operators triaging a
|
|
79
|
+
# corrupted Sandbox runtime still need it.
|
|
67
80
|
def decode_value(body)
|
|
68
81
|
Kobako::Codec::Decoder.decode(body)
|
|
69
82
|
rescue Kobako::Codec::Error => e
|
|
70
|
-
raise
|
|
83
|
+
raise build_transport_error(
|
|
84
|
+
"Sandbox produced an invalid result value",
|
|
85
|
+
detail: e.message
|
|
86
|
+
)
|
|
71
87
|
end
|
|
72
88
|
|
|
73
89
|
# Decode failure on the panic tag is a SandboxError (E-08). Either
|
|
@@ -75,20 +91,25 @@ module Kobako
|
|
|
75
91
|
# layer exception via +build_panic_error+ and raised; on wire-decode
|
|
76
92
|
# failure the rescue path raises the wire-violation +SandboxError+.
|
|
77
93
|
def decode_panic(body)
|
|
78
|
-
raise build_panic_error(
|
|
94
|
+
raise build_panic_error(build_panic_record(body))
|
|
79
95
|
rescue Kobako::Codec::Error => e
|
|
80
|
-
raise
|
|
96
|
+
raise build_transport_error(
|
|
97
|
+
"Sandbox produced an invalid panic record",
|
|
98
|
+
detail: e.message
|
|
99
|
+
)
|
|
81
100
|
end
|
|
82
101
|
|
|
83
102
|
# Build a +Panic+ value object from the msgpack-decoded body. Raises
|
|
84
|
-
# +Kobako::Codec::InvalidType+ when the body is not a map or
|
|
85
|
-
#
|
|
86
|
-
# +
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
103
|
+
# +Kobako::Codec::InvalidType+ when the body is not a map or when
|
|
104
|
+
# required keys are missing — both routed by +decode_panic+ to
|
|
105
|
+
# +build_transport_error+. The decode runs in block form so
|
|
106
|
+
# +Panic.new+'s +ArgumentError+ invariants surface as +InvalidType+
|
|
107
|
+
# through the decoder boundary; the message itself is never user-
|
|
108
|
+
# facing — it lands in +details+ via the rescue chain above.
|
|
109
|
+
def build_panic_record(body)
|
|
110
|
+
Kobako::Codec::Decoder.decode(body) do |map|
|
|
111
|
+
raise Kobako::Codec::InvalidType, "panic body must be a map, got #{map.class}" unless map.is_a?(Hash)
|
|
90
112
|
|
|
91
|
-
Kobako::Codec::Utils.wire_boundary do
|
|
92
113
|
Panic.new(
|
|
93
114
|
origin: map["origin"], klass: map["class"], message: map["message"],
|
|
94
115
|
backtrace: map["backtrace"] || [], details: map["details"]
|
|
@@ -97,11 +118,8 @@ module Kobako
|
|
|
97
118
|
end
|
|
98
119
|
|
|
99
120
|
# Map a decoded Panic record into the corresponding three-layer
|
|
100
|
-
# Ruby exception. +origin == "service"+ → ServiceError
|
|
101
|
-
#
|
|
102
|
-
# disconnected sentinel —
|
|
103
|
-
# {docs/behavior.md Error Classes}[link:../../docs/behavior.md]); everything else
|
|
104
|
-
# → SandboxError.
|
|
121
|
+
# Ruby exception. +origin == "service"+ → ServiceError; everything
|
|
122
|
+
# else → SandboxError.
|
|
105
123
|
def build_panic_error(panic)
|
|
106
124
|
panic_target_class(panic).new(
|
|
107
125
|
panic.message,
|
|
@@ -112,28 +130,36 @@ module Kobako
|
|
|
112
130
|
)
|
|
113
131
|
end
|
|
114
132
|
|
|
115
|
-
# {docs/behavior.md Error
|
|
133
|
+
# {docs/behavior.md Error Scenarios}[link:../../docs/behavior.md]: map
|
|
116
134
|
# the panic +class+ field to the matching Ruby exception subclass so
|
|
117
|
-
# callers can rescue specific failure paths. +origin="service"+
|
|
118
|
-
# +
|
|
119
|
-
# +Disconnected+ subclass (E-14); +origin="sandbox"+ plus
|
|
135
|
+
# callers can rescue specific failure paths. +origin="service"+ →
|
|
136
|
+
# +ServiceError+; +origin="sandbox"+ plus
|
|
120
137
|
# +class="Kobako::BytecodeError"+ selects the +BytecodeError+
|
|
121
138
|
# subclass (E-37 / E-38). Everything else falls back to the base
|
|
122
139
|
# class for the origin.
|
|
123
140
|
def panic_target_class(panic)
|
|
124
141
|
case panic.origin
|
|
125
142
|
when Panic::ORIGIN_SERVICE
|
|
126
|
-
|
|
143
|
+
ServiceError
|
|
127
144
|
else
|
|
128
145
|
panic.klass == "Kobako::BytecodeError" ? BytecodeError : SandboxError
|
|
129
146
|
end
|
|
130
147
|
end
|
|
131
148
|
|
|
132
|
-
|
|
133
|
-
|
|
149
|
+
# Lift the wire-violation fallback to the real
|
|
150
|
+
# +Kobako::Transport::Error+ class so callers can +rescue+ it
|
|
151
|
+
# specifically instead of pattern-matching on +error.klass+. The
|
|
152
|
+
# +klass+ field is still populated so existing operator-side
|
|
153
|
+
# tooling that greps on the string continues to work.
|
|
154
|
+
# +detail+ carries the inner codec / framing message, stashed
|
|
155
|
+
# directly into +details+ for operator diagnosis without polluting
|
|
156
|
+
# the user-facing +#message+.
|
|
157
|
+
def build_transport_error(message, detail: nil)
|
|
158
|
+
Kobako::Transport::Error.new(
|
|
134
159
|
message,
|
|
135
160
|
origin: Panic::ORIGIN_SANDBOX,
|
|
136
|
-
klass: "Kobako::
|
|
161
|
+
klass: "Kobako::Transport::Error",
|
|
162
|
+
details: detail
|
|
137
163
|
)
|
|
138
164
|
end
|
|
139
165
|
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kobako
|
|
4
|
+
# Host-side wasmtime runtime, surfaced as a Ruby class by the native ext
|
|
5
|
+
# (see ext/kobako/src/runtime.rs). The +Kobako::Runtime+ class wraps the
|
|
6
|
+
# wasmtime engine + compiled module + Store; it is the only Ruby-visible
|
|
7
|
+
# wasmtime type and the foundational binding layer for +Kobako::Sandbox+.
|
|
8
|
+
#
|
|
9
|
+
# This file reopens the magnus-defined class only to add the pure-Ruby
|
|
10
|
+
# +.default_path+ helper. Every other method (+#from_path+ singleton,
|
|
11
|
+
# +#eval+ / +#run+, capture and usage readers) is registered from Rust
|
|
12
|
+
# at ext load time.
|
|
13
|
+
class Runtime
|
|
14
|
+
# Absolute path to the gem-bundled +data/kobako.wasm+ artifact. Computed
|
|
15
|
+
# from this file's location so it works for both +bundle exec+ (running
|
|
16
|
+
# from the repo) and an installed gem (running from the gem dir).
|
|
17
|
+
#
|
|
18
|
+
# Returns a String regardless of whether the file currently exists —
|
|
19
|
+
# call sites that need the file to be present should pass this through
|
|
20
|
+
# +Kobako::Runtime.from_path+, which raises
|
|
21
|
+
# +Kobako::ModuleNotBuiltError+ with a clear remediation message.
|
|
22
|
+
# Raises +Kobako::SetupError+ if +__dir__+ is unavailable so that
|
|
23
|
+
# +rescue Kobako::SetupError+ around +Kobako::Sandbox.new+ catches every
|
|
24
|
+
# construction-layer failure uniformly, including this path-resolution one.
|
|
25
|
+
def self.default_path
|
|
26
|
+
dir = __dir__ or raise Kobako::SetupError, "Kobako::Runtime.default_path requires __dir__"
|
|
27
|
+
File.expand_path("../../data/kobako.wasm", dir)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
data/lib/kobako/sandbox.rb
CHANGED
|
@@ -4,24 +4,27 @@ require "forwardable"
|
|
|
4
4
|
|
|
5
5
|
require_relative "capture"
|
|
6
6
|
require_relative "errors"
|
|
7
|
-
require_relative "invocation"
|
|
8
7
|
require_relative "outcome"
|
|
9
|
-
require_relative "rpc/server"
|
|
10
|
-
require_relative "rpc/envelope"
|
|
11
8
|
require_relative "sandbox_options"
|
|
12
|
-
require_relative "
|
|
9
|
+
require_relative "usage"
|
|
10
|
+
require_relative "transport"
|
|
11
|
+
require_relative "catalog"
|
|
13
12
|
|
|
14
13
|
module Kobako
|
|
15
14
|
# Kobako::Sandbox — the user-facing entry point for executing guest mruby
|
|
16
15
|
# scripts inside a wasmtime-hosted Wasm module
|
|
17
16
|
# ({docs/behavior.md B-01}[link:../../docs/behavior.md]).
|
|
18
17
|
#
|
|
19
|
-
# The Sandbox owns the +Kobako::
|
|
20
|
-
# (
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
18
|
+
# The Sandbox owns the +Kobako::Runtime+, the per-Sandbox
|
|
19
|
+
# +Kobako::Catalog::Handles+ ({docs/behavior.md B-19}[link:../../docs/behavior.md]),
|
|
20
|
+
# the per-instance +Kobako::Catalog::Namespaces+ (which receives the
|
|
21
|
+
# +Catalog::Handles+ by injection so guest→host dispatch and host→guest
|
|
22
|
+
# auto-wrap share one allocator), and the dispatch +Proc+ /
|
|
23
|
+
# +yield_to_guest+ lambda installed on the Runtime via
|
|
24
|
+
# +Runtime#on_dispatch=+ ({docs/behavior.md B-12}[link:../../docs/behavior.md]).
|
|
25
|
+
# The underlying wasmtime Engine and compiled Module are cached at process
|
|
26
|
+
# scope by the native ext and never surface to Ruby — constructing many
|
|
27
|
+
# Sandboxes amortises both costs automatically.
|
|
25
28
|
#
|
|
26
29
|
# Output capture policy ({docs/behavior.md B-04}[link:../../docs/behavior.md]): the
|
|
27
30
|
# per-channel cap (+stdout_limit+ / +stderr_limit+) is enforced inside the
|
|
@@ -34,9 +37,7 @@ module Kobako
|
|
|
34
37
|
class Sandbox
|
|
35
38
|
extend Forwardable
|
|
36
39
|
|
|
37
|
-
attr_reader :wasm_path, :
|
|
38
|
-
:options,
|
|
39
|
-
:services, :snippets
|
|
40
|
+
attr_reader :wasm_path, :options
|
|
40
41
|
|
|
41
42
|
# Per-cap accessors forward to the immutable +SandboxOptions+ Value
|
|
42
43
|
# Object so the Host App still reads them off Sandbox directly.
|
|
@@ -72,6 +73,17 @@ module Kobako
|
|
|
72
73
|
@stderr_capture.truncated?
|
|
73
74
|
end
|
|
74
75
|
|
|
76
|
+
# Returns the +Kobako::Usage+ value object for the most recent
|
|
77
|
+
# invocation ({docs/behavior.md B-35}[link:../../docs/behavior.md]).
|
|
78
|
+
# Carries +wall_time+ (Float seconds the guest export call spent
|
|
79
|
+
# inside wasmtime) and +memory_peak+ (Integer bytes, high-water of
|
|
80
|
+
# the per-invocation +memory.grow+ delta past the entry-time
|
|
81
|
+
# baseline). Returns +Kobako::Usage::EMPTY+ before any invocation;
|
|
82
|
+
# populated on every outcome — including +TrapError+ — so the Host
|
|
83
|
+
# App can read it after rescuing a trap to diagnose budget
|
|
84
|
+
# consumption.
|
|
85
|
+
attr_reader :usage
|
|
86
|
+
|
|
75
87
|
# Build a fresh Sandbox.
|
|
76
88
|
#
|
|
77
89
|
# +wasm_path+ is the absolute path to the Guest Binary; defaults to the
|
|
@@ -84,20 +96,22 @@ module Kobako
|
|
|
84
96
|
def initialize(wasm_path: nil, stdout_limit: nil, stderr_limit: nil,
|
|
85
97
|
timeout: SandboxOptions::DEFAULT_TIMEOUT_SECONDS,
|
|
86
98
|
memory_limit: SandboxOptions::DEFAULT_MEMORY_LIMIT)
|
|
87
|
-
@wasm_path = wasm_path || Kobako::
|
|
99
|
+
@wasm_path = wasm_path || Kobako::Runtime.default_path
|
|
88
100
|
@options = SandboxOptions.new(timeout: timeout, memory_limit: memory_limit, stdout_limit: stdout_limit,
|
|
89
101
|
stderr_limit: stderr_limit)
|
|
90
|
-
@
|
|
91
|
-
@
|
|
92
|
-
@
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
102
|
+
@handler = Catalog::Handles.new
|
|
103
|
+
@services = Kobako::Catalog::Namespaces.new(handler: @handler)
|
|
104
|
+
@snippets = Catalog::Snippets.new
|
|
105
|
+
@runtime = Kobako::Runtime.from_path(@wasm_path, @options.timeout, @options.memory_limit,
|
|
106
|
+
@options.stdout_limit, @options.stderr_limit)
|
|
107
|
+
install_dispatch_proc!
|
|
108
|
+
reset_invocation_state!
|
|
96
109
|
end
|
|
97
110
|
|
|
98
111
|
# Declare or retrieve the Namespace named +name+ on this Sandbox
|
|
99
112
|
# ({docs/behavior.md B-07, B-09, B-10}[link:../../docs/behavior.md]). +name+ must be a
|
|
100
|
-
# Symbol or String in constant form. Returns the
|
|
113
|
+
# Symbol or String in constant form. Returns the
|
|
114
|
+
# +Kobako::Namespace+.
|
|
101
115
|
#
|
|
102
116
|
# Raises +ArgumentError+ when called after the first invocation, or
|
|
103
117
|
# when +name+ does not match the constant-name pattern.
|
|
@@ -144,17 +158,19 @@ module Kobako
|
|
|
144
158
|
|
|
145
159
|
# Dispatch into a preloaded entrypoint constant
|
|
146
160
|
# ({docs/behavior.md B-31}[link:../../docs/behavior.md]). Delegates host
|
|
147
|
-
# pre-flight
|
|
148
|
-
# +Kobako::
|
|
149
|
-
#
|
|
150
|
-
#
|
|
151
|
-
#
|
|
152
|
-
#
|
|
153
|
-
#
|
|
161
|
+
# pre-flight and wire encoding to +Kobako::Transport::Run+ /
|
|
162
|
+
# +Kobako::Transport::Run#encode+: a non-Symbol/String +target+ raises
|
|
163
|
+
# +TypeError+ (E-24), while a +target+ failing the constant pattern
|
|
164
|
+
# (E-25), a forged +Kobako::Handle+ in +args+ / +kwargs+ (E-29), or a
|
|
165
|
+
# non-Symbol +kwargs+ key (E-30) raise +ArgumentError+. The guest
|
|
166
|
+
# resolves +target+ as a top-level constant, calls +#call+ on it with
|
|
167
|
+
# +args+ / +kwargs+, and returns the deserialized result. The first
|
|
168
|
+
# invocation seals the Service registry and snippet table (B-07 /
|
|
169
|
+
# B-33). Runtime errors follow the same three-class taxonomy as +#eval+.
|
|
154
170
|
def run(target, *args, **kwargs)
|
|
155
|
-
|
|
171
|
+
run_envelope = Transport::Run.new(entrypoint: target, args: args, kwargs: kwargs)
|
|
156
172
|
invoke!(:run) do
|
|
157
|
-
@
|
|
173
|
+
@runtime.run(@services.encode, @snippets.encode, run_envelope.encode(@handler))
|
|
158
174
|
end
|
|
159
175
|
end
|
|
160
176
|
|
|
@@ -185,66 +201,114 @@ module Kobako
|
|
|
185
201
|
raise SandboxError, "code must be a String, got #{code.class}" unless code.is_a?(String)
|
|
186
202
|
|
|
187
203
|
invoke!(:eval) do
|
|
188
|
-
@
|
|
204
|
+
@runtime.eval(@services.encode, code.b, @snippets.encode)
|
|
189
205
|
end
|
|
190
206
|
end
|
|
191
207
|
|
|
192
208
|
private
|
|
193
209
|
|
|
210
|
+
# Configure the +Runtime+'s host↔guest dispatch wiring
|
|
211
|
+
# ({docs/behavior.md B-12}[link:../../docs/behavior.md]). Builds a
|
|
212
|
+
# lambda that re-enters the guest via
|
|
213
|
+
# +Runtime#yield_to_active_invocation+ (B-24) and a dispatch +Proc+
|
|
214
|
+
# that routes guest→host calls through the stateless
|
|
215
|
+
# +Transport::Dispatcher+, capturing +@services+ / +@handler+ in the
|
|
216
|
+
# closure. Both are registered on the +Runtime+ once at construction
|
|
217
|
+
# time so the wasm ext callback can fire without further setup.
|
|
218
|
+
def install_dispatch_proc!
|
|
219
|
+
yield_to_guest = ->(args_bytes) { @runtime.yield_to_active_invocation(args_bytes) }
|
|
220
|
+
@runtime.on_dispatch = lambda do |request_bytes|
|
|
221
|
+
Transport::Dispatcher.dispatch(request_bytes, @services, @handler, yield_to_guest)
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
194
225
|
# Per-invocation prologue ({docs/behavior.md B-03 / B-07 /
|
|
195
226
|
# B-33}[link:../../docs/behavior.md]). Seals the Service / snippet
|
|
196
227
|
# registries on first call (idempotent) and zeros the per-invocation
|
|
197
228
|
# capability state — capture buffers, truncation predicates, and the
|
|
198
|
-
#
|
|
229
|
+
# +Catalog::Handles+ counter — before the guest runs. The
|
|
230
|
+
# +Catalog::Handles+ itself is held as +@handler+ and never exposed beyond
|
|
231
|
+
# this class: SPEC.md Terminology pins it as "Not exposed to the
|
|
232
|
+
# Host App" (B-19 / B-20 / E-29).
|
|
199
233
|
def begin_invocation!
|
|
200
234
|
@services.seal!
|
|
201
|
-
@
|
|
202
|
-
|
|
235
|
+
@handler.reset!
|
|
236
|
+
reset_invocation_state!
|
|
203
237
|
end
|
|
204
238
|
|
|
205
|
-
# Reset
|
|
206
|
-
#
|
|
207
|
-
# (
|
|
208
|
-
#
|
|
209
|
-
|
|
239
|
+
# Reset all per-invocation observable state to its pre-invocation
|
|
240
|
+
# sentinels — both per-channel captures
|
|
241
|
+
# ({docs/behavior.md B-05}[link:../../docs/behavior.md]) and the
|
|
242
|
+
# per-last-invocation usage record
|
|
243
|
+
# ({docs/behavior.md B-35}[link:../../docs/behavior.md]). Shared by
|
|
244
|
+
# +#initialize+ (first-time setup) and +#begin_invocation!+
|
|
245
|
+
# (between-invocation reset) so both paths agree on what
|
|
246
|
+
# "pre-invocation state" means.
|
|
247
|
+
def reset_invocation_state!
|
|
210
248
|
@stdout_capture = Capture::EMPTY
|
|
211
249
|
@stderr_capture = Capture::EMPTY
|
|
250
|
+
@usage = Usage::EMPTY
|
|
212
251
|
end
|
|
213
252
|
|
|
214
|
-
# Read the per-
|
|
215
|
-
# ext
|
|
216
|
-
#
|
|
217
|
-
# +
|
|
218
|
-
#
|
|
219
|
-
#
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
253
|
+
# Read the per-last-invocation +wall_time+ and +memory_peak+ from
|
|
254
|
+
# the ext and wrap them as a +Kobako::Usage+ value object
|
|
255
|
+
# ({docs/behavior.md B-35}[link:../../docs/behavior.md]). Runs in
|
|
256
|
+
# the +invoke!+ +ensure+ block so the usage record is populated on
|
|
257
|
+
# every outcome — value return, +Kobako::TrapError+ (including
|
|
258
|
+
# +TimeoutError+ / +MemoryLimitError+), +Kobako::SandboxError+,
|
|
259
|
+
# and +Kobako::ServiceError+. On the success path the same figures
|
|
260
|
+
# already arrive via +Snapshot#usage+; on the trap path the Snapshot
|
|
261
|
+
# never reaches Ruby so the ext readout here is the only source.
|
|
262
|
+
#
|
|
263
|
+
# The ext returns a positional 2-tuple +[wall_time, memory_peak]+
|
|
264
|
+
# whose order matches the +Kobako::Usage+ field order; the
|
|
265
|
+
# destructure-then-kwargs handoff below is the explicit
|
|
266
|
+
# positional→keyword conversion point.
|
|
267
|
+
def read_usage!
|
|
268
|
+
wall_time, memory_peak = @runtime.usage
|
|
269
|
+
@usage = Usage.new(wall_time: wall_time, memory_peak: memory_peak)
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# Pick the +TrapError+ subclass to re-raise based on +err+'s actual
|
|
273
|
+
# class. Cap-trap subclasses
|
|
274
|
+
# ({docs/behavior.md E-19 / E-20}[link:../../docs/behavior.md])
|
|
275
|
+
# preserve their named identity; everything else collapses to the
|
|
276
|
+
# base +Kobako::TrapError+. The ext already raises the right subclass
|
|
277
|
+
# directly, so this is a pure re-attribution that lets +#invoke!+
|
|
278
|
+
# add the verb prefix without erasing +TimeoutError+ /
|
|
279
|
+
# +MemoryLimitError+.
|
|
280
|
+
def trap_class_for(err)
|
|
281
|
+
case err
|
|
282
|
+
when TimeoutError then TimeoutError
|
|
283
|
+
when MemoryLimitError then MemoryLimitError
|
|
284
|
+
else TrapError
|
|
285
|
+
end
|
|
225
286
|
end
|
|
226
287
|
|
|
227
288
|
# Shared prologue / epilogue + trap-class translator for both
|
|
228
289
|
# invocation verbs. +verb+ is +:eval+ or +:run+; it tags the
|
|
229
|
-
# TrapError message so the failing export is identifiable.
|
|
230
|
-
#
|
|
231
|
-
#
|
|
232
|
-
# +
|
|
233
|
-
#
|
|
234
|
-
#
|
|
235
|
-
#
|
|
236
|
-
#
|
|
290
|
+
# TrapError message so the failing export is identifiable.
|
|
291
|
+
#
|
|
292
|
+
# The yielded block must return a +Kobako::Snapshot+ — i.e. the
|
|
293
|
+
# value of +Runtime#eval+ / +#run+ (SPEC.md Internal Concepts →
|
|
294
|
+
# Snapshot). The success path unpacks every observable from the
|
|
295
|
+
# Snapshot in one go: +#stdout+ / +#stderr+ pack into +Capture+,
|
|
296
|
+
# +#usage+ packs into +Usage+, +#return_bytes+ feeds +Outcome.decode+.
|
|
297
|
+
# The rescue chain is the single trap-translation boundary —
|
|
298
|
+
# configured-cap paths
|
|
299
|
+
# ({docs/behavior.md E-19 / E-20}[link:../../docs/behavior.md])
|
|
300
|
+
# surface as named TrapError subclasses; everything else surfaces as
|
|
301
|
+
# the base +TrapError+.
|
|
237
302
|
def invoke!(verb)
|
|
238
303
|
begin_invocation!
|
|
239
|
-
yield
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
raise TrapError, "guest __kobako_#{verb} trapped: #{e.message}"
|
|
304
|
+
snapshot = yield
|
|
305
|
+
@stdout_capture = snapshot.stdout
|
|
306
|
+
@stderr_capture = snapshot.stderr
|
|
307
|
+
Outcome.decode(snapshot.return_bytes)
|
|
308
|
+
rescue Kobako::TrapError => e
|
|
309
|
+
raise trap_class_for(e), "Sandbox##{verb} failed: #{e.message}"
|
|
310
|
+
ensure
|
|
311
|
+
read_usage!
|
|
248
312
|
end
|
|
249
313
|
end
|
|
250
314
|
end
|
|
@@ -11,7 +11,7 @@ module Kobako
|
|
|
11
11
|
# for absent values and normalises (timeout to Float seconds,
|
|
12
12
|
# memory_limit to positive Integer bytes) before delegating to Data's
|
|
13
13
|
# +super+. Anything that survives +SandboxOptions.new+ is a wire-ready
|
|
14
|
-
# cap bundle the +Kobako::
|
|
14
|
+
# cap bundle the +Kobako::Runtime+ constructor consumes as-is.
|
|
15
15
|
class SandboxOptions < Data.define(:timeout, :memory_limit, :stdout_limit, :stderr_limit)
|
|
16
16
|
# Default wall-clock timeout for a single invocation: 60 seconds
|
|
17
17
|
# ({docs/behavior.md B-01}[link:../../docs/behavior.md]).
|
|
@@ -27,17 +27,15 @@ module Kobako
|
|
|
27
27
|
# ({docs/behavior.md B-01}[link:../../docs/behavior.md]).
|
|
28
28
|
DEFAULT_OUTPUT_LIMIT = 1 << 20
|
|
29
29
|
|
|
30
|
-
# steep:ignore:start
|
|
31
30
|
def initialize(timeout: DEFAULT_TIMEOUT_SECONDS,
|
|
32
31
|
memory_limit: DEFAULT_MEMORY_LIMIT,
|
|
33
32
|
stdout_limit: nil,
|
|
34
33
|
stderr_limit: nil)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
)
|
|
34
|
+
timeout = normalize_timeout(timeout)
|
|
35
|
+
memory_limit = normalize_memory_limit(memory_limit)
|
|
36
|
+
stdout_limit ||= DEFAULT_OUTPUT_LIMIT
|
|
37
|
+
stderr_limit ||= DEFAULT_OUTPUT_LIMIT
|
|
38
|
+
super
|
|
41
39
|
end
|
|
42
40
|
|
|
43
41
|
private
|
|
@@ -68,6 +66,5 @@ module Kobako
|
|
|
68
66
|
|
|
69
67
|
memory_limit
|
|
70
68
|
end
|
|
71
|
-
# steep:ignore:end
|
|
72
69
|
end
|
|
73
70
|
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "capture"
|
|
4
|
+
require_relative "usage"
|
|
5
|
+
|
|
6
|
+
module Kobako
|
|
7
|
+
# Kobako::Snapshot — per-invocation observable bundle returned from
|
|
8
|
+
# +Kobako::Runtime#eval+ and +#run+.
|
|
9
|
+
#
|
|
10
|
+
# The magnus class (see ext/kobako/src/snapshot.rs) carries seven raw
|
|
11
|
+
# readers: +return_bytes+, +stdout_bytes+, +stdout_truncated+,
|
|
12
|
+
# +stderr_bytes+, +stderr_truncated+, +wall_time+, +memory_peak+. This
|
|
13
|
+
# file reopens the class to add the Ruby-side helpers that pack those
|
|
14
|
+
# raw fields into the user-facing value objects +Kobako::Capture+ and
|
|
15
|
+
# +Kobako::Usage+ — the same shape +Kobako::Sandbox+ exposes to the
|
|
16
|
+
# Host App.
|
|
17
|
+
class Snapshot
|
|
18
|
+
# Wrap the stdout capture pair (+stdout_bytes+, +stdout_truncated+)
|
|
19
|
+
# as a +Kobako::Capture+ value object. {docs/behavior.md
|
|
20
|
+
# B-04}[link:../../docs/behavior.md] — the byte content never carries
|
|
21
|
+
# a truncation sentinel; +#truncated?+ is the only way to observe
|
|
22
|
+
# that the cap was hit.
|
|
23
|
+
def stdout
|
|
24
|
+
Capture.new(bytes: stdout_bytes, truncated: stdout_truncated)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Wrap the stderr capture pair as a +Kobako::Capture+ value object.
|
|
28
|
+
# Mirror of +#stdout+.
|
|
29
|
+
def stderr
|
|
30
|
+
Capture.new(bytes: stderr_bytes, truncated: stderr_truncated)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Wrap the per-last-invocation usage pair (+wall_time+,
|
|
34
|
+
# +memory_peak+) as a +Kobako::Usage+ value object
|
|
35
|
+
# ({docs/behavior.md B-35}[link:../../docs/behavior.md]).
|
|
36
|
+
def usage
|
|
37
|
+
Usage.new(wall_time: wall_time, memory_peak: memory_peak)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -3,23 +3,22 @@
|
|
|
3
3
|
module Kobako
|
|
4
4
|
module Snippet
|
|
5
5
|
# Kobako::Snippet::Binary — value object representing a single
|
|
6
|
-
# +#preload(binary:)+ entry held by +Kobako::
|
|
6
|
+
# +#preload(binary:)+ entry held by +Kobako::Catalog::Snippets+
|
|
7
7
|
# ({docs/behavior.md B-32}[link:../../../docs/behavior.md]).
|
|
8
8
|
#
|
|
9
9
|
# The +body+ is RITE bytecode (as emitted by +mrbc+) carried as an
|
|
10
10
|
# +ASCII_8BIT+ String so msgpack-ruby encodes it as +bin+ family on
|
|
11
11
|
# the wire ({docs/wire-codec.md Invocation channels}[link:../../../docs/wire-codec.md]).
|
|
12
12
|
# The host treats the bytes as opaque — the snippet's canonical
|
|
13
|
-
# name, when present, lives in the bytecode's embedded
|
|
14
|
-
#
|
|
15
|
-
# structural validation
|
|
13
|
+
# name, when present, lives in the bytecode's embedded +debug_info+
|
|
14
|
+
# and is resolved by the guest at load time; structural validation
|
|
16
15
|
# ({docs/behavior.md E-37 / E-38}[link:../../../docs/behavior.md])
|
|
17
16
|
# is deferred to the first invocation's guest replay.
|
|
18
17
|
#
|
|
19
18
|
# The class is a +Data.define+ subclass — frozen and value-equal.
|
|
20
|
-
# Callers (chiefly +
|
|
21
|
-
# +Binary.new(body: ...)+. Wire-form construction is the
|
|
22
|
-
# responsibility.
|
|
19
|
+
# Callers (chiefly +Catalog::Snippets+) construct instances via keyword
|
|
20
|
+
# form +Binary.new(body: ...)+. Wire-form construction is the
|
|
21
|
+
# registry's responsibility.
|
|
23
22
|
class Binary < Data.define(:body)
|
|
24
23
|
# The +kind+ field value carried by bytecode snippets in their
|
|
25
24
|
# Frame 3 wire envelope entry
|
|
@@ -3,21 +3,21 @@
|
|
|
3
3
|
module Kobako
|
|
4
4
|
module Snippet
|
|
5
5
|
# Kobako::Snippet::Source — value object representing a single
|
|
6
|
-
# +#preload(code:, name:)+ entry held by +Kobako::
|
|
6
|
+
# +#preload(code:, name:)+ entry held by +Kobako::Catalog::Snippets+
|
|
7
7
|
# ({docs/behavior.md B-32}[link:../../../docs/behavior.md]).
|
|
8
8
|
#
|
|
9
9
|
# +name+ is the canonical +Symbol+ identity baked into the loaded
|
|
10
10
|
# IREP's +debug_info+; backtrace frames originating in this snippet
|
|
11
11
|
# surface as +(snippet:Name):line+. +body+ is the UTF-8 mruby source
|
|
12
|
-
# detached from the caller's reference at +
|
|
13
|
-
# later mutation of the original String cannot bleed through.
|
|
12
|
+
# detached from the caller's reference at +Catalog::Snippets#register+
|
|
13
|
+
# time so later mutation of the original String cannot bleed through.
|
|
14
14
|
#
|
|
15
15
|
# The class is a +Data.define+ subclass — frozen, value-equal, and
|
|
16
|
-
# carries no mutation API. Callers (chiefly +
|
|
17
|
-
# instances via keyword form +Source.new(name: ..., body: ...)+.
|
|
18
|
-
# Wire-form construction is the
|
|
19
|
-
#
|
|
20
|
-
#
|
|
16
|
+
# carries no mutation API. Callers (chiefly +Catalog::Snippets+)
|
|
17
|
+
# construct instances via keyword form +Source.new(name: ..., body: ...)+.
|
|
18
|
+
# Wire-form construction is the registry's responsibility: as a leaf
|
|
19
|
+
# carrier this Source stays pure and +Catalog::Snippets#encode+ reads
|
|
20
|
+
# its attributes off the outside rather than asking it to self-encode.
|
|
21
21
|
class Source < Data.define(:name, :body)
|
|
22
22
|
# The +kind+ field value carried by source snippets in their Frame
|
|
23
23
|
# 3 wire envelope entry
|
data/lib/kobako/snippet.rb
CHANGED
|
@@ -2,19 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "snippet/binary"
|
|
4
4
|
require_relative "snippet/source"
|
|
5
|
-
require_relative "snippet/table"
|
|
6
5
|
|
|
7
6
|
module Kobako
|
|
8
|
-
# Kobako::Snippet —
|
|
9
|
-
#
|
|
7
|
+
# Kobako::Snippet — value-object family for preloaded snippet entries
|
|
8
|
+
# held by +Kobako::Catalog::Snippets+
|
|
10
9
|
# ({docs/behavior.md B-32 / B-33}[link:../../docs/behavior.md]).
|
|
11
10
|
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
# them.
|
|
11
|
+
# +Source+ represents a single +#preload(code:, name:)+ entry; +Binary+
|
|
12
|
+
# represents a single +#preload(binary:)+ entry. Both are plain value
|
|
13
|
+
# objects with no dependency on the +Catalog::Snippets+ registry that
|
|
14
|
+
# holds them — the registry reads their attributes externally when
|
|
15
|
+
# encoding the wire envelope.
|
|
18
16
|
module Snippet
|
|
19
17
|
end
|
|
20
18
|
end
|