kobako 0.5.0-x86_64-linux
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 +7 -0
- data/.release-please-manifest.json +1 -0
- data/CHANGELOG.md +29 -0
- data/LICENSE +201 -0
- data/README.md +408 -0
- data/data/kobako.wasm +0 -0
- data/lib/kobako/3.3/kobako.so +0 -0
- data/lib/kobako/3.4/kobako.so +0 -0
- data/lib/kobako/4.0/kobako.so +0 -0
- data/lib/kobako/capture.rb +43 -0
- data/lib/kobako/catalog/handles.rb +107 -0
- data/lib/kobako/catalog/namespaces.rb +99 -0
- data/lib/kobako/catalog/snippets.rb +149 -0
- data/lib/kobako/catalog.rb +18 -0
- data/lib/kobako/codec/decoder.rb +73 -0
- data/lib/kobako/codec/encoder.rb +37 -0
- data/lib/kobako/codec/error.rb +34 -0
- data/lib/kobako/codec/factory.rb +162 -0
- data/lib/kobako/codec/utils.rb +145 -0
- data/lib/kobako/codec.rb +31 -0
- data/lib/kobako/errors.rb +140 -0
- 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/panic.rb +42 -0
- data/lib/kobako/outcome.rb +166 -0
- data/lib/kobako/runtime.rb +30 -0
- data/lib/kobako/sandbox.rb +314 -0
- data/lib/kobako/sandbox_options.rb +70 -0
- data/lib/kobako/snapshot.rb +40 -0
- data/lib/kobako/snippet/binary.rb +29 -0
- data/lib/kobako/snippet/source.rb +28 -0
- data/lib/kobako/snippet.rb +18 -0
- 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 +5 -0
- data/lib/kobako.rb +10 -0
- data/release-please-config.json +24 -0
- data/sig/kobako/capture.rbs +11 -0
- data/sig/kobako/catalog/handles.rbs +19 -0
- data/sig/kobako/catalog/namespaces.rbs +17 -0
- data/sig/kobako/catalog/snippets.rbs +27 -0
- data/sig/kobako/catalog.rbs +4 -0
- data/sig/kobako/codec/decoder.rbs +12 -0
- data/sig/kobako/codec/encoder.rbs +7 -0
- data/sig/kobako/codec/error.rbs +18 -0
- data/sig/kobako/codec/factory.rbs +31 -0
- data/sig/kobako/codec/utils.rbs +19 -0
- data/sig/kobako/errors.rbs +55 -0
- 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/panic.rbs +34 -0
- data/sig/kobako/outcome.rbs +24 -0
- data/sig/kobako/runtime.rbs +23 -0
- data/sig/kobako/sandbox.rbs +55 -0
- data/sig/kobako/sandbox_options.rbs +32 -0
- data/sig/kobako/snapshot.rbs +15 -0
- data/sig/kobako/snippet/binary.rbs +12 -0
- data/sig/kobako/snippet/source.rbs +13 -0
- data/sig/kobako/snippet.rbs +4 -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
- data/sig/kobako.rbs +3 -0
- metadata +145 -0
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kobako
|
|
4
|
+
# Host-side captured prefix of guest stdout / stderr produced during a
|
|
5
|
+
# single +Kobako::Sandbox+ invocation, paired with the truncation flag
|
|
6
|
+
# the WASI pipe sets when the guest wrote past the configured per-channel
|
|
7
|
+
# cap ({docs/behavior.md B-04}[link:../../docs/behavior.md]).
|
|
8
|
+
#
|
|
9
|
+
# Immutable value object: the captured bytes and the truncation flag
|
|
10
|
+
# always travel together and the instance is frozen on construction.
|
|
11
|
+
# Construct via +Capture.new(bytes:, truncated:)+ for the ext-provided
|
|
12
|
+
# binary bytes (the constructor handles the UTF-8 / ASCII-8BIT fallback)
|
|
13
|
+
# or reach +Capture::EMPTY+ for the pre-invocation sentinel that
|
|
14
|
+
# +Sandbox+ uses before any invocation has executed.
|
|
15
|
+
class Capture
|
|
16
|
+
attr_reader :bytes
|
|
17
|
+
|
|
18
|
+
# Build a Capture wrapping +bytes+ (the captured prefix as a String) and
|
|
19
|
+
# +truncated+ (whether the originating WASI pipe reported the cap was
|
|
20
|
+
# hit). Coerces +bytes+ to UTF-8 when they are valid UTF-8, otherwise
|
|
21
|
+
# falls back to ASCII-8BIT so invalid sequences remain inspectable
|
|
22
|
+
# without raising; +bytes+ is duplicated, never mutated. Freezes the
|
|
23
|
+
# instance so callers cannot mutate the pair.
|
|
24
|
+
def initialize(bytes:, truncated:)
|
|
25
|
+
copy = bytes.dup.force_encoding(Encoding::UTF_8)
|
|
26
|
+
copy.force_encoding(Encoding::ASCII_8BIT) unless copy.valid_encoding?
|
|
27
|
+
@bytes = copy
|
|
28
|
+
@truncated = truncated
|
|
29
|
+
freeze
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Returns +true+ iff the underlying capture channel exceeded its
|
|
33
|
+
# configured cap during the originating +Sandbox+ invocation
|
|
34
|
+
# ({docs/behavior.md B-04}[link:../../docs/behavior.md]).
|
|
35
|
+
def truncated? = @truncated
|
|
36
|
+
|
|
37
|
+
# Pre-invocation sentinel ({docs/behavior.md B-05}[link:../../docs/behavior.md]).
|
|
38
|
+
# Empty UTF-8 bytes and +truncated? == false+; reused by every fresh
|
|
39
|
+
# +Sandbox+ and by +Sandbox+ between invocations to denote "no capture
|
|
40
|
+
# yet".
|
|
41
|
+
EMPTY = new(bytes: "", truncated: false)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../handle"
|
|
4
|
+
|
|
5
|
+
module Kobako
|
|
6
|
+
module Catalog
|
|
7
|
+
# Host-side mapping from opaque integer Handle IDs to Ruby objects.
|
|
8
|
+
# The table is owned by +Kobako::Sandbox+
|
|
9
|
+
# ({docs/behavior.md B-19}[link:../../../docs/behavior.md]) and injected
|
|
10
|
+
# into the per-Sandbox +Kobako::Catalog::Namespaces+ so guest→host dispatch
|
|
11
|
+
# resolves Handle targets and arguments against the same table that
|
|
12
|
+
# host→guest wire encoding allocates into
|
|
13
|
+
# ({docs/behavior.md B-14, B-34}[link:../../../docs/behavior.md]).
|
|
14
|
+
#
|
|
15
|
+
# Lifecycle invariants ({docs/behavior.md}[link:../../../docs/behavior.md]):
|
|
16
|
+
#
|
|
17
|
+
# - {docs/behavior.md B-15}[link:../../../docs/behavior.md] — Handle IDs
|
|
18
|
+
# are allocated by a monotonically increasing counter scoped to a
|
|
19
|
+
# single invocation. The first ID issued in an invocation is 1; ID 0
|
|
20
|
+
# is reserved as the invalid sentinel and is never returned by
|
|
21
|
+
# +#alloc+.
|
|
22
|
+
#
|
|
23
|
+
# - {docs/behavior.md B-19}[link:../../../docs/behavior.md] — At every
|
|
24
|
+
# invocation boundary (via +#reset!+), every Handle issued under the
|
|
25
|
+
# old state becomes invalid. Reset applies uniformly regardless of
|
|
26
|
+
# allocation source (B-14 Service return or B-34 host-injected
|
|
27
|
+
# argument).
|
|
28
|
+
#
|
|
29
|
+
# - {docs/behavior.md B-21}[link:../../../docs/behavior.md] — The cap is
|
|
30
|
+
# +0x7fff_ffff+ (2³¹ − 1). Allocation beyond the cap raises
|
|
31
|
+
# immediately — no silent truncation, no wrap, no ID reuse.
|
|
32
|
+
class Handles
|
|
33
|
+
# Build a fresh, empty table. +next_id+ is an internal seam that
|
|
34
|
+
# sets the starting value of the monotonic counter (defaults to 1 per
|
|
35
|
+
# B-15); tests pass a value near +Kobako::Handle::MAX_ID+ to exercise
|
|
36
|
+
# the cap-exhaustion path without 2³¹ allocations.
|
|
37
|
+
def initialize(next_id: 1)
|
|
38
|
+
@entries = {} # : Hash[Integer, untyped]
|
|
39
|
+
@next_id = next_id
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Bind +object+ in the table and return a +Kobako::Handle+ token
|
|
43
|
+
# for it. +object+ is any host-side Ruby object to bind. Returns a
|
|
44
|
+
# freshly-allocated +Kobako::Handle+ whose +#id+ falls in
|
|
45
|
+
# +[Kobako::Handle::MIN_ID, Kobako::Handle::MAX_ID]+. Raises
|
|
46
|
+
# +Kobako::HandlerExhaustedError+ if the next ID would exceed the
|
|
47
|
+
# cap. The cap is anchored on +Kobako::Handle+ — the wire codec
|
|
48
|
+
# and the allocator share the same invariant
|
|
49
|
+
# ({docs/behavior.md B-21}[link:../../../docs/behavior.md]).
|
|
50
|
+
#
|
|
51
|
+
# Returning a Handle (rather than a bare Integer id) keeps the
|
|
52
|
+
# allocator's output a domain entity; +Kobako::Handle.restore+
|
|
53
|
+
# is reserved for the codec's wire-decode path, where the id is
|
|
54
|
+
# the only thing the bytes carry.
|
|
55
|
+
def alloc(object)
|
|
56
|
+
id = @next_id
|
|
57
|
+
cap = Kobako::Handle::MAX_ID
|
|
58
|
+
if id > cap
|
|
59
|
+
raise HandlerExhaustedError,
|
|
60
|
+
"Out of handle allocations: too many host objects were referenced " \
|
|
61
|
+
"in a single invocation (limit #{cap})"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
@entries[id] = object
|
|
65
|
+
@next_id = id + 1
|
|
66
|
+
Kobako::Handle.restore(id)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Resolve a Handle ID to its bound object. +id+ is a Handle ID previously
|
|
70
|
+
# returned by +#alloc+. Returns the bound object. Raises
|
|
71
|
+
# +Kobako::SandboxError+ if +id+ is not currently bound.
|
|
72
|
+
def fetch(id)
|
|
73
|
+
require_bound!(id)
|
|
74
|
+
@entries[id]
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Clear all entries AND reset the counter to 1. Called at the per-invocation
|
|
78
|
+
# boundary by +Kobako::Sandbox+ — see
|
|
79
|
+
# {docs/behavior.md B-19}[link:../../../docs/behavior.md]. Returns +self+.
|
|
80
|
+
def reset!
|
|
81
|
+
@entries.clear
|
|
82
|
+
@next_id = 1
|
|
83
|
+
self
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Number of currently-bound entries. Used by tests of the Dispatcher
|
|
87
|
+
# and Codec::Utils#deep_wrap to observe whether each path allocates
|
|
88
|
+
# exactly the Handle entries it should — the +Handles+ table itself never
|
|
89
|
+
# consults its own size, but the surrounding code's allocation
|
|
90
|
+
# contract is part of the observable boundary.
|
|
91
|
+
def size
|
|
92
|
+
@entries.size
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
private
|
|
96
|
+
|
|
97
|
+
# Single source of truth for the "unknown Handle id" raise used by
|
|
98
|
+
# {#fetch}. Returns +nil+ on success; raises +Kobako::SandboxError+
|
|
99
|
+
# when +id+ is not currently bound.
|
|
100
|
+
def require_bound!(id)
|
|
101
|
+
return if @entries.key?(id)
|
|
102
|
+
|
|
103
|
+
raise SandboxError, "unknown Handle id: #{id.inspect}"
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "handles"
|
|
4
|
+
require_relative "../codec"
|
|
5
|
+
require_relative "../errors"
|
|
6
|
+
require_relative "../transport/request"
|
|
7
|
+
require_relative "../namespace"
|
|
8
|
+
|
|
9
|
+
module Kobako
|
|
10
|
+
module Catalog
|
|
11
|
+
# Kobako::Catalog::Namespaces — per-Sandbox registry of
|
|
12
|
+
# +Kobako::Namespace+ entities. Holds the Namespace / Member bindings
|
|
13
|
+
# and the preamble emitted on Frame 1
|
|
14
|
+
# ({docs/behavior.md B-07..B-11}[link:../../../docs/behavior.md]).
|
|
15
|
+
#
|
|
16
|
+
# Public API:
|
|
17
|
+
#
|
|
18
|
+
# namespaces = Kobako::Catalog::Namespaces.new
|
|
19
|
+
# namespace = namespaces.define(:MyService) # => Kobako::Namespace
|
|
20
|
+
# namespace.bind(:KV, kv_object) # => namespace (chainable)
|
|
21
|
+
# namespaces.encode # => msgpack bytes for Frame 1
|
|
22
|
+
# namespaces.lookup("MyService::KV") # => kv_object
|
|
23
|
+
#
|
|
24
|
+
# Namespaces live at +Kobako::Namespace+. Per-dispatch routing is
|
|
25
|
+
# +Kobako::Transport::Dispatcher+'s responsibility — the Dispatcher
|
|
26
|
+
# receives this registry and the +Catalog::Handles+ as arguments from
|
|
27
|
+
# the +Runtime#on_dispatch+ Proc that +Kobako::Sandbox#initialize+
|
|
28
|
+
# installs ({docs/behavior.md B-12}[link:../../../docs/behavior.md]).
|
|
29
|
+
# The registry holds an injected +Catalog::Handles+ reference so
|
|
30
|
+
# dispatch target resolution and host→guest auto-wrap share the same
|
|
31
|
+
# Sandbox-owned allocator (docs/behavior.md B-19).
|
|
32
|
+
class Namespaces
|
|
33
|
+
# Build a fresh registry. +handler+ is an internal seam that injects
|
|
34
|
+
# a pre-configured +Catalog::Handles+; tests pass one whose +next_id+
|
|
35
|
+
# is pinned near +MAX_ID+ to exercise the B-21 cap-exhaustion path
|
|
36
|
+
# without 2³¹ allocations. Production callers leave it at the default.
|
|
37
|
+
def initialize(handler: Catalog::Handles.new)
|
|
38
|
+
@namespaces = {} # : Hash[String, Kobako::Namespace]
|
|
39
|
+
@handler = handler
|
|
40
|
+
@sealed = false
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Declare or retrieve the Namespace named +name+ (idempotent — docs/behavior.md B-10).
|
|
44
|
+
# +name+ is a constant-form name as a +Symbol+ or +String+ (must satisfy
|
|
45
|
+
# +Namespace::NAME_PATTERN+). Returns the +Kobako::Namespace+ for that
|
|
46
|
+
# name, creating it if it does not exist. Raises +ArgumentError+ when
|
|
47
|
+
# +name+ is malformed, or when called after the owning Sandbox has been
|
|
48
|
+
# sealed by its first invocation
|
|
49
|
+
# ({docs/behavior.md B-07}[link:../../../docs/behavior.md]).
|
|
50
|
+
def define(name)
|
|
51
|
+
raise ArgumentError, "cannot define after first Sandbox invocation" if @sealed
|
|
52
|
+
|
|
53
|
+
name_str = name.to_s
|
|
54
|
+
unless Namespace::NAME_PATTERN.match?(name_str)
|
|
55
|
+
raise ArgumentError,
|
|
56
|
+
"Namespace name must match #{Namespace::NAME_PATTERN.inspect} (got #{name.inspect})"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
@namespaces[name_str] ||= Namespace.new(name_str)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Resolve a +target+ path of the form +"Namespace::Member"+ to the
|
|
63
|
+
# bound Host object. +target+ is a two-level path using the +::+
|
|
64
|
+
# separator. Returns the bound Host object. Raises +KeyError+ when the
|
|
65
|
+
# namespace or the member is not bound.
|
|
66
|
+
def lookup(target)
|
|
67
|
+
namespace_name, member_name = target.to_s.split("::", 2)
|
|
68
|
+
namespace = @namespaces[namespace_name]
|
|
69
|
+
raise KeyError, "no namespace named #{namespace_name.inspect}" if namespace.nil?
|
|
70
|
+
raise KeyError, "no member in target #{target.inspect}" unless member_name
|
|
71
|
+
|
|
72
|
+
namespace.fetch(member_name)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Encode the preamble as msgpack bytes for stdin Frame 1 delivery
|
|
76
|
+
# ({docs/behavior.md B-02}[link:../../../docs/behavior.md]). Routes through
|
|
77
|
+
# {Kobako::Codec::Encoder} like every other host-side wire encode so
|
|
78
|
+
# there is a single codec path; the preamble carries only Strings and
|
|
79
|
+
# Arrays, so none of the kobako ext types actually fire. Structure:
|
|
80
|
+
# +[["Namespace", ["MemberA", "MemberB"]], ...]+. Returns a binary
|
|
81
|
+
# +String+ of msgpack bytes.
|
|
82
|
+
def encode
|
|
83
|
+
Codec::Encoder.encode(@namespaces.values.map(&:to_preamble))
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Mark the registry as sealed. Called by +Sandbox+ on the first
|
|
87
|
+
# invocation. After sealing, #define raises ArgumentError. Idempotent.
|
|
88
|
+
def seal!
|
|
89
|
+
@sealed = true
|
|
90
|
+
self
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Returns +true+ when {#seal!} has been called, +false+ otherwise.
|
|
94
|
+
def sealed?
|
|
95
|
+
@sealed
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../codec"
|
|
4
|
+
require_relative "../snippet"
|
|
5
|
+
|
|
6
|
+
module Kobako
|
|
7
|
+
module Catalog
|
|
8
|
+
# Kobako::Catalog::Snippets — per-Sandbox insertion-ordered registry
|
|
9
|
+
# of preloaded snippets
|
|
10
|
+
# ({docs/behavior.md B-32 / B-33}[link:../../../docs/behavior.md]).
|
|
11
|
+
#
|
|
12
|
+
# Entries replay against the fresh +mrb_state+ before per-invocation
|
|
13
|
+
# source / entrypoint resolution. Each +Snippet::Source+ entry's +name+
|
|
14
|
+
# is its canonical identity — the filename baked into the loaded IREP's
|
|
15
|
+
# +debug_info+ that surfaces in every backtrace frame originating from
|
|
16
|
+
# the snippet as +(snippet:Name):line+. Duplicate names within the
|
|
17
|
+
# +code:+ form would produce ambiguous attribution and are rejected at
|
|
18
|
+
# registration time
|
|
19
|
+
# ({docs/behavior.md E-33}[link:../../../docs/behavior.md]).
|
|
20
|
+
# +Snippet::Binary+ entries carry no host-side name — their canonical
|
|
21
|
+
# name lives in the bytecode's +debug_info+ and is read by the guest at
|
|
22
|
+
# load time; the host does not extract it.
|
|
23
|
+
#
|
|
24
|
+
# Sealing (B-33) is governed by the owning Sandbox — the registry itself
|
|
25
|
+
# is append-only and exposes no mutation API beyond +#register+; the
|
|
26
|
+
# Sandbox guards +#register+ behind the seal check before delegating.
|
|
27
|
+
class Snippets
|
|
28
|
+
# Ruby constant-name pattern enforced on snippet names
|
|
29
|
+
# ({docs/behavior.md E-34}[link:../../../docs/behavior.md]).
|
|
30
|
+
NAME_PATTERN = /\A[A-Z]\w*\z/
|
|
31
|
+
|
|
32
|
+
def initialize
|
|
33
|
+
@entries = [] # : Array[Kobako::Snippet::Source | Kobako::Snippet::Binary]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Serialize the registered snippets to wire bytes. Each entry
|
|
37
|
+
# contributes a msgpack map shape; the collection rides as a single
|
|
38
|
+
# msgpack array. An empty registry serializes to an empty array, never
|
|
39
|
+
# absent. The wire codec is an implementation detail — callers
|
|
40
|
+
# receive a binary +String+ that the +Kobako::Runtime+ layer ships
|
|
41
|
+
# through the invocation channel. The entry value objects stay pure
|
|
42
|
+
# carriers — this collection-tier method reads their attributes
|
|
43
|
+
# externally via +entry_payload+ rather than asking each entry to
|
|
44
|
+
# self-encode.
|
|
45
|
+
def encode
|
|
46
|
+
Codec::Encoder.encode(@entries.map { |entry| entry_payload(entry) })
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Register one preloaded snippet in either of two forms
|
|
50
|
+
# ({docs/behavior.md B-32}[link:../../../docs/behavior.md]).
|
|
51
|
+
#
|
|
52
|
+
# * Source form +register(code: src, name: Name)+ — +src+ is the
|
|
53
|
+
# mruby source as a String; the bytes are re-encoded as UTF-8
|
|
54
|
+
# and detached from the caller's reference. +name+ is a Symbol
|
|
55
|
+
# or String matching +NAME_PATTERN+. Returns the Symbol form
|
|
56
|
+
# of +name+.
|
|
57
|
+
# * Binary form +register(binary: bytes)+ — +bytes+ is
|
|
58
|
+
# precompiled RITE bytecode as a String, duplicated and forced
|
|
59
|
+
# to ASCII-8BIT so msgpack-ruby ships it as +bin+. Returns
|
|
60
|
+
# +nil+ — bytecode entries are anonymous on the host side; any
|
|
61
|
+
# structural validation
|
|
62
|
+
# ({docs/behavior.md E-37 / E-38}[link:../../../docs/behavior.md])
|
|
63
|
+
# is deferred to the guest at first replay.
|
|
64
|
+
#
|
|
65
|
+
# The two forms are mutually exclusive: shape validation lives
|
|
66
|
+
# here so callers (chiefly +Kobako::Sandbox#preload+) collapse to
|
|
67
|
+
# a single delegation. Raises +ArgumentError+ on mixed forms,
|
|
68
|
+
# missing keywords, wrong types, malformed +name+ (E-34), or
|
|
69
|
+
# duplicate +code:+ +name+ (E-33).
|
|
70
|
+
def register(code: nil, name: nil, binary: nil)
|
|
71
|
+
if binary
|
|
72
|
+
raise ArgumentError, "cannot combine binary: with code: / name:" if code || name
|
|
73
|
+
|
|
74
|
+
register_binary!(binary)
|
|
75
|
+
else
|
|
76
|
+
register_source!(code, name)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
# Source-form register path. Delegates argument-shape checks to
|
|
83
|
+
# +ensure_source_args!+ (which returns the narrowed +[code, name]+
|
|
84
|
+
# pair), normalises +name+ to a Symbol, rejects duplicates (E-33),
|
|
85
|
+
# and appends the Source entry.
|
|
86
|
+
def register_source!(code, name)
|
|
87
|
+
code, name = ensure_source_args!(code, name)
|
|
88
|
+
name_sym = normalize_name(name)
|
|
89
|
+
if @entries.any? { |e| e.is_a?(Snippet::Source) && e.name == name_sym }
|
|
90
|
+
raise ArgumentError, "snippet #{name_sym.inspect} already preloaded"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
@entries << Snippet::Source.new(name: name_sym, body: code.dup.force_encoding(Encoding::UTF_8))
|
|
94
|
+
name_sym
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Shape-only validation for the +code:+ + +name:+ pair. Returns
|
|
98
|
+
# the pair with +nil+ narrowed away so callers can treat both as
|
|
99
|
+
# present. The +code:+ type check runs before the +name:+
|
|
100
|
+
# presence check so callers passing +code: nil+ explicitly see
|
|
101
|
+
# the type error rather than the "missing keyword" error.
|
|
102
|
+
def ensure_source_args!(code, name)
|
|
103
|
+
raise ArgumentError, "missing keyword: code: + name:, or binary:" if code.nil? && name.nil?
|
|
104
|
+
raise ArgumentError, "code must be a String, got #{code.class}" unless code.is_a?(String)
|
|
105
|
+
raise ArgumentError, "missing keyword: name:" if name.nil?
|
|
106
|
+
|
|
107
|
+
[code, name]
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Binary-form register path. Validates the +binary:+ payload type
|
|
111
|
+
# and appends the Binary entry. The bytes are duplicated and forced
|
|
112
|
+
# to ASCII-8BIT so msgpack-ruby picks the +bin+ family on the wire.
|
|
113
|
+
def register_binary!(bytes)
|
|
114
|
+
raise ArgumentError, "binary must be a String, got #{bytes.class}" unless bytes.is_a?(String)
|
|
115
|
+
|
|
116
|
+
@entries << Snippet::Binary.new(body: bytes.dup.force_encoding(Encoding::ASCII_8BIT))
|
|
117
|
+
nil
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Build the msgpack-ready Hash for one entry. Source entries
|
|
121
|
+
# contribute their host-side +name+; Binary entries omit it
|
|
122
|
+
# because the canonical name lives in the bytecode's embedded
|
|
123
|
+
# +debug_info+ and is read by the guest at load time
|
|
124
|
+
# ({docs/wire-codec.md Invocation channels}[link:../../../docs/wire-codec.md]).
|
|
125
|
+
def entry_payload(entry)
|
|
126
|
+
case entry
|
|
127
|
+
when Snippet::Source
|
|
128
|
+
{ "name" => entry.name.to_s, "kind" => Snippet::Source::KIND, "body" => entry.body }
|
|
129
|
+
when Snippet::Binary
|
|
130
|
+
{ "kind" => Snippet::Binary::KIND, "body" => entry.body }
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def normalize_name(name)
|
|
135
|
+
unless name.is_a?(Symbol) || name.is_a?(String)
|
|
136
|
+
raise ArgumentError, "snippet name must be a Symbol or String, got #{name.class}"
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
name_str = name.to_s
|
|
140
|
+
unless NAME_PATTERN.match?(name_str)
|
|
141
|
+
raise ArgumentError,
|
|
142
|
+
"snippet name must match #{NAME_PATTERN.inspect} (got #{name.inspect})"
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
name_str.to_sym
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "catalog/handles"
|
|
4
|
+
require_relative "catalog/namespaces"
|
|
5
|
+
require_relative "catalog/snippets"
|
|
6
|
+
|
|
7
|
+
module Kobako
|
|
8
|
+
# Kobako::Catalog — Sandbox-level configuration and per-invocation
|
|
9
|
+
# allocation tables. Houses the three host-side registries the Sandbox
|
|
10
|
+
# owns: +Catalog::Namespaces+ (Namespace / Member registry),
|
|
11
|
+
# +Catalog::Snippets+ (preloaded source / bytecode entries), and
|
|
12
|
+
# +Catalog::Handles+ (per-invocation Handle ID allocator).
|
|
13
|
+
#
|
|
14
|
+
# See {SPEC.md Refinement → Internal Concepts}[link:../../SPEC.md] for
|
|
15
|
+
# how Catalog fits alongside Transport and Runtime.
|
|
16
|
+
module Catalog
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "msgpack"
|
|
4
|
+
|
|
5
|
+
require_relative "error"
|
|
6
|
+
require_relative "factory"
|
|
7
|
+
require_relative "utils"
|
|
8
|
+
|
|
9
|
+
module Kobako
|
|
10
|
+
module Codec
|
|
11
|
+
# Module-level entry point for the host side of the kobako wire
|
|
12
|
+
# ({docs/wire-codec.md}[link:../../../docs/wire-codec.md] § Type Mapping).
|
|
13
|
+
#
|
|
14
|
+
# Translates msgpack gem exceptions into the kobako error taxonomy
|
|
15
|
+
# ({Truncated}, {InvalidType}, {InvalidEncoding}, {UnsupportedType}) so
|
|
16
|
+
# callers can pattern-match on the SPEC's wire-violation categories
|
|
17
|
+
# without leaking the gem's internal exception classes.
|
|
18
|
+
#
|
|
19
|
+
# Public API is a single function — {.decode}. The decoder is
|
|
20
|
+
# stateless; the +MessagePack::Unpacker+ instance is built per call
|
|
21
|
+
# because callers always decode exactly one wire value at a time.
|
|
22
|
+
module Decoder
|
|
23
|
+
# Decode +bytes+ into one Ruby value and validate transitively
|
|
24
|
+
# against the SPEC type mapping. Raises {Truncated}, {InvalidType},
|
|
25
|
+
# or {InvalidEncoding} on wire violations.
|
|
26
|
+
#
|
|
27
|
+
# When a block is given, the decoded value is yielded and the block's
|
|
28
|
+
# result is returned — wire Value Objects use this to build themselves
|
|
29
|
+
# from the decoded payload. The block runs inside this method's
|
|
30
|
+
# rescue, so a Value Object's +ArgumentError+ invariant failure
|
|
31
|
+
# surfaces as {InvalidType} without a separate {Utils.with_boundary}
|
|
32
|
+
# wrapper at the call site.
|
|
33
|
+
def self.decode(bytes)
|
|
34
|
+
value = Factory.load(bytes.b)
|
|
35
|
+
validate_utf8!(value)
|
|
36
|
+
block_given? ? yield(value) : value
|
|
37
|
+
# msgpack gem raises the format/type errors below; +ArgumentError+
|
|
38
|
+
# comes from our ext-type validators (Handle id range, Exception type
|
|
39
|
+
# whitelist) and from a yielded block's Value Object invariants — both
|
|
40
|
+
# are wire violations, so both map to {InvalidType}.
|
|
41
|
+
rescue ::MessagePack::UnknownExtTypeError, ::MessagePack::MalformedFormatError,
|
|
42
|
+
::MessagePack::StackError, ::ArgumentError => e
|
|
43
|
+
raise InvalidType, e.message
|
|
44
|
+
# +UnpackError+ is the gem's umbrella class for short-read /
|
|
45
|
+
# incomplete-buffer faults; +EOFError+ covers underflow at the
|
|
46
|
+
# buffer edge.
|
|
47
|
+
rescue ::MessagePack::UnpackError, ::EOFError => e
|
|
48
|
+
raise Truncated, e.message
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# SPEC pins +str+ family payloads to UTF-8
|
|
52
|
+
# ({docs/wire-codec.md}[link:../../../docs/wire-codec.md] § str/bin
|
|
53
|
+
# Encoding Rules). The msgpack gem returns UTF-8-tagged Strings for
|
|
54
|
+
# str family but does not validate the bytes; +bin+ family decodes
|
|
55
|
+
# to ASCII-8BIT. Walk the tree once and reject invalid UTF-8 in any
|
|
56
|
+
# str-typed leaf via {Utils.assert_utf8!}. {Kobako::Fault}
|
|
57
|
+
# payloads are validated transitively: +Factory.unpack_fault+
|
|
58
|
+
# feeds the inner ext-0x02 bytes back through this Decoder, so their
|
|
59
|
+
# +str+ fields are already covered by the time control returns here.
|
|
60
|
+
class << self
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def validate_utf8!(value)
|
|
64
|
+
case value
|
|
65
|
+
when String then Utils.assert_utf8!(value, "str payload") if value.encoding == Encoding::UTF_8
|
|
66
|
+
when Array then value.each { |v| validate_utf8!(v) }
|
|
67
|
+
when Hash then value.each { |pair| validate_utf8!(pair) }
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "msgpack"
|
|
4
|
+
|
|
5
|
+
require_relative "error"
|
|
6
|
+
require_relative "factory"
|
|
7
|
+
|
|
8
|
+
module Kobako
|
|
9
|
+
module Codec
|
|
10
|
+
# Module-level entry point for the host side of the kobako wire
|
|
11
|
+
# ({docs/wire-codec.md}[link:../../../docs/wire-codec.md] § Type Mapping).
|
|
12
|
+
#
|
|
13
|
+
# The codec backbone is the official +msgpack+ gem: integers, floats,
|
|
14
|
+
# strings, arrays, and maps go through the gem's narrowest-encoding
|
|
15
|
+
# logic; the three kobako-specific ext types (0x00 Symbol, 0x01
|
|
16
|
+
# Capability Handle, 0x02 Exception envelope) are registered on
|
|
17
|
+
# the cached {Kobako::Codec::Factory} singleton.
|
|
18
|
+
#
|
|
19
|
+
# Public API is a single function — {.encode}. The codec is stateless;
|
|
20
|
+
# there is no buffer accumulator and no streaming write API. Callers
|
|
21
|
+
# that need to concatenate multiple encodings build the bytes
|
|
22
|
+
# themselves.
|
|
23
|
+
module Encoder
|
|
24
|
+
# Encode +value+ to wire bytes (binary-encoded String).
|
|
25
|
+
# Wire violations surface as +UnsupportedType+: SPEC's 12-entry type
|
|
26
|
+
# mapping is a closed set, and anything outside it is rejected by
|
|
27
|
+
# the msgpack gem itself (arbitrary objects raise +NoMethodError+
|
|
28
|
+
# from missing +to_msgpack+, integers outside i64..u64 raise
|
|
29
|
+
# +RangeError+).
|
|
30
|
+
def self.encode(value)
|
|
31
|
+
Factory.dump(value)
|
|
32
|
+
rescue ::RangeError, ::NoMethodError => e
|
|
33
|
+
raise UnsupportedType, e.message
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kobako
|
|
4
|
+
module Codec
|
|
5
|
+
# Base class for all wire-codec faults raised by the pure-Ruby host codec.
|
|
6
|
+
#
|
|
7
|
+
# The wire codec implements the binary contract pinned in
|
|
8
|
+
# {docs/wire-codec.md}[link:../../../docs/wire-codec.md] § Type Mapping.
|
|
9
|
+
# Every wire violation surfaces as a
|
|
10
|
+
# subclass of {Error} so callers can pattern-match on the specific
|
|
11
|
+
# fault while still rescuing all codec faults via this base class.
|
|
12
|
+
#
|
|
13
|
+
# Higher layers (e.g. the Sandbox dispatch loop) translate these into
|
|
14
|
+
# the public {Kobako::SandboxError} / {Kobako::TrapError} taxonomy.
|
|
15
|
+
class Error < StandardError; end
|
|
16
|
+
|
|
17
|
+
# Input ended before the type prefix or payload was fully consumed.
|
|
18
|
+
class Truncated < Error; end
|
|
19
|
+
|
|
20
|
+
# The type byte at the current position is not in the 12-entry kobako
|
|
21
|
+
# type mapping (e.g. an unknown ext code, or a reserved msgpack tag).
|
|
22
|
+
class InvalidType < Error; end
|
|
23
|
+
|
|
24
|
+
# A msgpack `str` payload was not valid UTF-8, or an ext 0x00 Symbol
|
|
25
|
+
# payload was not valid UTF-8 — both are wire violations per SPEC.
|
|
26
|
+
class InvalidEncoding < Error; end
|
|
27
|
+
|
|
28
|
+
# The encoder was handed a Ruby object whose type has no wire
|
|
29
|
+
# representation (e.g. Range, Time). Higher layers may catch this
|
|
30
|
+
# and re-route the value through Handle allocation, but at the
|
|
31
|
+
# codec level it is a hard error.
|
|
32
|
+
class UnsupportedType < Error; end
|
|
33
|
+
end
|
|
34
|
+
end
|