kobako 0.14.0-aarch64-linux → 0.16.0-aarch64-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 +4 -4
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +67 -0
- data/README.md +104 -29
- data/ROADMAP.md +2 -3
- data/SECURITY.md +1 -1
- data/crates/kobako-runtime/CHANGELOG.md +14 -0
- data/crates/kobako-runtime/README.md +1 -1
- data/crates/kobako-wasmtime/CHANGELOG.md +14 -0
- data/crates/kobako-wasmtime/README.md +1 -1
- 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/catalog/extensions.rb +114 -0
- data/lib/kobako/catalog/handles.rb +1 -1
- data/lib/kobako/catalog/services.rb +135 -0
- data/lib/kobako/catalog/snippets.rb +1 -1
- data/lib/kobako/catalog.rb +7 -5
- data/lib/kobako/codec/decoder.rb +3 -3
- data/lib/kobako/codec/encoder.rb +4 -4
- data/lib/kobako/codec/ext_types.rb +169 -0
- data/lib/kobako/codec/state.rb +98 -0
- data/lib/kobako/codec/utils.rb +2 -2
- data/lib/kobako/codec.rb +23 -6
- data/lib/kobako/extension.rb +47 -0
- data/lib/kobako/fault.rb +1 -1
- data/lib/kobako/handle.rb +1 -1
- data/lib/kobako/outcome.rb +13 -7
- data/lib/kobako/pool.rb +1 -1
- data/lib/kobako/sandbox.rb +47 -27
- data/lib/kobako/transport/dispatcher.rb +25 -18
- data/lib/kobako/transport/request.rb +14 -9
- data/lib/kobako/transport/response.rb +9 -2
- data/lib/kobako/transport/yield.rb +4 -1
- data/lib/kobako/transport/yielder.rb +14 -6
- data/lib/kobako/version.rb +1 -1
- data/lib/kobako.rb +1 -0
- data/sig/kobako/catalog/extensions.rbs +25 -0
- data/sig/kobako/catalog/services.rbs +27 -0
- data/sig/kobako/codec/ext_types.rbs +31 -0
- data/sig/kobako/codec/state.rbs +20 -0
- data/sig/kobako/codec.rbs +3 -0
- data/sig/kobako/extension.rbs +20 -0
- data/sig/kobako/sandbox.rbs +3 -1
- data/sig/kobako/transport/dispatcher.rbs +4 -4
- data/sig/kobako/transport/yielder.rbs +1 -1
- data/sig/kobako/transport.rbs +2 -2
- metadata +12 -8
- data/lib/kobako/catalog/namespaces.rb +0 -115
- data/lib/kobako/codec/factory.rb +0 -187
- data/lib/kobako/namespace.rb +0 -78
- data/sig/kobako/catalog/namespaces.rbs +0 -17
- data/sig/kobako/codec/factory.rbs +0 -34
- data/sig/kobako/namespace.rbs +0 -21
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "handles"
|
|
4
|
+
require_relative "../codec"
|
|
5
|
+
require_relative "../errors"
|
|
6
|
+
|
|
7
|
+
module Kobako
|
|
8
|
+
module Catalog
|
|
9
|
+
# Kobako::Catalog::Services — per-Sandbox registry of Service
|
|
10
|
+
# bindings keyed by their constant-path name. Holds the flat
|
|
11
|
+
# path→object table and the preamble emitted on Frame 1.
|
|
12
|
+
#
|
|
13
|
+
# Public API:
|
|
14
|
+
#
|
|
15
|
+
# services = Kobako::Catalog::Services.new
|
|
16
|
+
# services.bind("MyService::KV", kv_object) # => services (chainable)
|
|
17
|
+
# services.encode # => msgpack bytes for Frame 1
|
|
18
|
+
# services.lookup("MyService::KV") # => kv_object
|
|
19
|
+
#
|
|
20
|
+
# Per-dispatch routing is +Kobako::Transport::Dispatcher+'s
|
|
21
|
+
# responsibility — the Dispatcher receives this registry and the
|
|
22
|
+
# +Catalog::Handles+ as arguments from the +Runtime#on_dispatch+ Proc
|
|
23
|
+
# that +Kobako::Sandbox#initialize+ installs. The registry holds an
|
|
24
|
+
# injected +Catalog::Handles+ reference so dispatch target resolution
|
|
25
|
+
# and host→guest auto-wrap share the same Sandbox-owned allocator.
|
|
26
|
+
class Services
|
|
27
|
+
# Ruby constant-name pattern each +::+-separated bind-path segment
|
|
28
|
+
# must match.
|
|
29
|
+
NAME_PATTERN = /\A[A-Z]\w*\z/
|
|
30
|
+
|
|
31
|
+
# Build a fresh registry. +handler+ is an internal seam that injects
|
|
32
|
+
# a pre-configured +Catalog::Handles+; tests pass one whose +next_id+
|
|
33
|
+
# is pinned near +MAX_ID+ to exercise the cap-exhaustion path
|
|
34
|
+
# without 2³¹ allocations. Production callers leave it at the default.
|
|
35
|
+
def initialize(handler: Catalog::Handles.new)
|
|
36
|
+
@bindings = {} # : Hash[String, untyped]
|
|
37
|
+
@handler = handler
|
|
38
|
+
@sealed = false
|
|
39
|
+
@encoded = nil # : String?
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Bind +object+ as the Service reachable at +path+ — a +Symbol+ or
|
|
43
|
+
# +String+ of one or more +::+-separated constant-form segments
|
|
44
|
+
# (+"MyService::KV"+ or a top-level +"File"+). Returns +self+ for
|
|
45
|
+
# chaining. Raises +ArgumentError+ when a segment is malformed, when
|
|
46
|
+
# +path+ collides with an existing binding (a name is a bound Service
|
|
47
|
+
# or a grouping prefix, never both), or when the owning Sandbox has
|
|
48
|
+
# been sealed by its first invocation.
|
|
49
|
+
def bind(path, object)
|
|
50
|
+
raise ArgumentError, "cannot bind after first Sandbox invocation" if @sealed
|
|
51
|
+
|
|
52
|
+
path_str = validate_path!(path)
|
|
53
|
+
raise ArgumentError, "Service path #{path_str} conflicts with an existing binding" if collision?(path_str)
|
|
54
|
+
|
|
55
|
+
@bindings[path_str] = object
|
|
56
|
+
self
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Resolve a +target+ constant path to the bound Service. Raises
|
|
60
|
+
# +KeyError+ when no Service is bound at +target+.
|
|
61
|
+
def lookup(target)
|
|
62
|
+
target_str = target.to_s
|
|
63
|
+
raise KeyError, "no service bound at #{target_str.inspect}" unless @bindings.key?(target_str)
|
|
64
|
+
|
|
65
|
+
@bindings[target_str]
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Replace the object bound at an already-registered +path+ without the
|
|
69
|
+
# collision or seal checks +#bind+ applies. +Catalog::Extensions+ calls
|
|
70
|
+
# this during +begin_invocation!+ to swap a callable backend provider's
|
|
71
|
+
# per-invocation result behind a path whose Frame 1 membership was
|
|
72
|
+
# fixed at the seal. A path that was never registered is left untouched,
|
|
73
|
+
# so the sealed key set can never grow here.
|
|
74
|
+
def refresh(path, object)
|
|
75
|
+
key = path.to_s
|
|
76
|
+
@bindings[key] = object if @bindings.key?(key)
|
|
77
|
+
self
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Encode the preamble as msgpack bytes for stdin Frame 1 delivery —
|
|
81
|
+
# a flat array of the bound constant paths, in bind order:
|
|
82
|
+
# +["MyService::KV", "File"]+. Routes through Kobako::Codec::Encoder
|
|
83
|
+
# like every other host-side wire encode; the preamble carries only
|
|
84
|
+
# Strings, so none of the kobako ext types fire. Returns a binary
|
|
85
|
+
# +String+ of msgpack bytes.
|
|
86
|
+
#
|
|
87
|
+
# Once sealed, the bytes are computed once and reused for every
|
|
88
|
+
# subsequent invocation: sealing freezes Service registration at the
|
|
89
|
+
# first invocation, so a bind reaching the registry after the seal
|
|
90
|
+
# raises +ArgumentError+ and never alters Frame 1.
|
|
91
|
+
def encode
|
|
92
|
+
return @encoded if @encoded
|
|
93
|
+
|
|
94
|
+
bytes = Codec::Encoder.encode(@bindings.keys).freeze
|
|
95
|
+
@encoded = bytes if @sealed
|
|
96
|
+
bytes
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Mark the registry as sealed. Called by +Sandbox+ on the first
|
|
100
|
+
# invocation; afterwards #bind raises ArgumentError. Idempotent;
|
|
101
|
+
# returns +self+.
|
|
102
|
+
def seal!
|
|
103
|
+
@sealed = true
|
|
104
|
+
self
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Returns +true+ when #seal! has been called, +false+ otherwise.
|
|
108
|
+
def sealed?
|
|
109
|
+
@sealed
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
private
|
|
113
|
+
|
|
114
|
+
def validate_path!(path)
|
|
115
|
+
path_str = path.to_s
|
|
116
|
+
segments = path_str.split("::", -1)
|
|
117
|
+
return path_str if !segments.empty? && segments.all? { |seg| NAME_PATTERN.match?(seg) }
|
|
118
|
+
|
|
119
|
+
raise ArgumentError,
|
|
120
|
+
"bind path must be constant-form segments joined by '::' (got #{path.inspect})"
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# A path collides when it equals, is a prefix of, or extends an
|
|
124
|
+
# existing binding on the +::+ segment boundary — the guardrail that
|
|
125
|
+
# keeps a name from being both a bound Service and a grouping prefix.
|
|
126
|
+
def collision?(path)
|
|
127
|
+
@bindings.each_key.any? do |existing|
|
|
128
|
+
existing == path ||
|
|
129
|
+
existing.start_with?("#{path}::") ||
|
|
130
|
+
path.start_with?("#{existing}::")
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
@@ -44,7 +44,7 @@ module Kobako
|
|
|
44
44
|
# The bytes are memoized — the table is replayed verbatim on every
|
|
45
45
|
# invocation after sealing, so Frame 3 never changes between
|
|
46
46
|
# encodes; #register drops the memo while the table is still open.
|
|
47
|
-
# Unlike +Catalog::
|
|
47
|
+
# Unlike +Catalog::Services#encode+, which gates its memo on the
|
|
48
48
|
# seal, this one can fill eagerly and invalidate in +#register+
|
|
49
49
|
# because every mutation funnels through that single method — there is
|
|
50
50
|
# no out-of-sight child object to change the result behind its back.
|
data/lib/kobako/catalog.rb
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "catalog/handles"
|
|
4
|
-
require_relative "catalog/
|
|
4
|
+
require_relative "catalog/services"
|
|
5
5
|
require_relative "catalog/snippets"
|
|
6
|
+
require_relative "catalog/extensions"
|
|
6
7
|
|
|
7
8
|
module Kobako
|
|
8
9
|
# Kobako::Catalog — Sandbox-level configuration and per-invocation
|
|
9
|
-
# allocation tables. Houses the
|
|
10
|
-
#
|
|
11
|
-
# +Catalog::Snippets+ (preloaded source / bytecode entries),
|
|
12
|
-
# +Catalog::
|
|
10
|
+
# allocation tables. Houses the host-side registries the Sandbox owns:
|
|
11
|
+
# +Catalog::Services+ (path→Service binding registry),
|
|
12
|
+
# +Catalog::Snippets+ (preloaded source / bytecode entries),
|
|
13
|
+
# +Catalog::Extensions+ (installed Extensions, composed onto Services /
|
|
14
|
+
# Snippets), and +Catalog::Handles+ (per-invocation Handle ID allocator).
|
|
13
15
|
#
|
|
14
16
|
# See {SPEC.md Refinement → Internal Concepts}[link:../../SPEC.md] for
|
|
15
17
|
# how Catalog fits alongside Transport and Runtime.
|
data/lib/kobako/codec/decoder.rb
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
require "msgpack"
|
|
4
4
|
|
|
5
5
|
require_relative "error"
|
|
6
|
-
require_relative "
|
|
6
|
+
require_relative "ext_types"
|
|
7
7
|
require_relative "utils"
|
|
8
8
|
|
|
9
9
|
module Kobako
|
|
@@ -31,7 +31,7 @@ module Kobako
|
|
|
31
31
|
# surfaces as InvalidType without a separate Utils.with_boundary
|
|
32
32
|
# wrapper at the call site.
|
|
33
33
|
def self.decode(bytes)
|
|
34
|
-
value =
|
|
34
|
+
value = FACTORY.load(bytes.b)
|
|
35
35
|
validate_utf8!(value)
|
|
36
36
|
block_given? ? yield(value) : value
|
|
37
37
|
# msgpack gem raises the format/type errors below; +ArgumentError+
|
|
@@ -54,7 +54,7 @@ module Kobako
|
|
|
54
54
|
# str family but does not validate the bytes; +bin+ family decodes
|
|
55
55
|
# to ASCII-8BIT. Walk the tree once and reject invalid UTF-8 in any
|
|
56
56
|
# str-typed leaf via Utils.assert_utf8!. Kobako::Fault
|
|
57
|
-
# payloads are validated transitively: +
|
|
57
|
+
# payloads are validated transitively: +ExtTypes#unpack_fault+
|
|
58
58
|
# feeds the inner ext-0x02 bytes back through this Decoder, so their
|
|
59
59
|
# +str+ fields are already covered by the time control returns here.
|
|
60
60
|
class << self
|
data/lib/kobako/codec/encoder.rb
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
require "msgpack"
|
|
4
4
|
|
|
5
5
|
require_relative "error"
|
|
6
|
-
require_relative "
|
|
6
|
+
require_relative "ext_types"
|
|
7
7
|
|
|
8
8
|
module Kobako
|
|
9
9
|
module Codec
|
|
@@ -13,8 +13,8 @@ module Kobako
|
|
|
13
13
|
# The codec backbone is the official +msgpack+ gem: integers, floats,
|
|
14
14
|
# strings, arrays, and maps go through the gem's narrowest-encoding
|
|
15
15
|
# logic; the three kobako-specific ext types (0x00 Symbol, 0x01
|
|
16
|
-
# Capability Handle, 0x02 Exception envelope) are registered
|
|
17
|
-
# the
|
|
16
|
+
# Capability Handle, 0x02 Exception envelope) are registered by
|
|
17
|
+
# ExtTypes on the process-wide factory.
|
|
18
18
|
#
|
|
19
19
|
# Public API is a single function — +.encode+. The codec is stateless;
|
|
20
20
|
# there is no buffer accumulator and no streaming write API. Callers
|
|
@@ -32,7 +32,7 @@ module Kobako
|
|
|
32
32
|
# +NoMethodError+ is likewise reported as +UnsupportedType+ rather than
|
|
33
33
|
# propagating.
|
|
34
34
|
def self.encode(value)
|
|
35
|
-
|
|
35
|
+
FACTORY.dump(value)
|
|
36
36
|
rescue ::RangeError, ::NoMethodError => e
|
|
37
37
|
raise UnsupportedType, e.message
|
|
38
38
|
end
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "msgpack"
|
|
4
|
+
|
|
5
|
+
require_relative "error"
|
|
6
|
+
require_relative "utils"
|
|
7
|
+
require_relative "state"
|
|
8
|
+
require_relative "../handle"
|
|
9
|
+
require_relative "../fault"
|
|
10
|
+
|
|
11
|
+
module Kobako
|
|
12
|
+
module Codec
|
|
13
|
+
# The kobako wire ext-type conversions
|
|
14
|
+
# ({docs/wire-codec.md}[link:../../../docs/wire-codec.md] § Ext Types)
|
|
15
|
+
# as pure functions: per-operation decode state is threaded in as an
|
|
16
|
+
# argument, so the module itself holds nothing. #build_factory assembles
|
|
17
|
+
# the one +MessagePack::Factory+ these conversions are registered on.
|
|
18
|
+
module ExtTypes
|
|
19
|
+
# MessagePack ext type code reserved for Symbol
|
|
20
|
+
# ({docs/wire-codec.md}[link:../../../docs/wire-codec.md] § Ext Types
|
|
21
|
+
# → ext 0x00). Module-private — mirrors +codec::EXT_SYMBOL+ on the
|
|
22
|
+
# Rust side.
|
|
23
|
+
EXT_SYMBOL = 0x00
|
|
24
|
+
# MessagePack ext type code reserved for Capability Handle
|
|
25
|
+
# ({docs/wire-codec.md}[link:../../../docs/wire-codec.md] § Ext Types
|
|
26
|
+
# → ext 0x01). Module-private — mirrors +codec::EXT_HANDLE+ on the
|
|
27
|
+
# Rust side.
|
|
28
|
+
EXT_HANDLE = 0x01
|
|
29
|
+
# MessagePack ext type code reserved for Exception envelope
|
|
30
|
+
# ({docs/wire-codec.md}[link:../../../docs/wire-codec.md] § Ext Types
|
|
31
|
+
# → ext 0x02). Module-private — mirrors +codec::EXT_ERRENV+ on the
|
|
32
|
+
# Rust side.
|
|
33
|
+
EXT_ERRENV = 0x02
|
|
34
|
+
private_constant :EXT_SYMBOL, :EXT_HANDLE, :EXT_ERRENV
|
|
35
|
+
|
|
36
|
+
module_function
|
|
37
|
+
|
|
38
|
+
# Assemble a +MessagePack::Factory+ with the three kobako ext types
|
|
39
|
+
# registered, frozen because registration is its only mutation and
|
|
40
|
+
# happens exactly once. The stateful conversions resolve their
|
|
41
|
+
# per-operation state at call time, so one registered factory serves
|
|
42
|
+
# every thread.
|
|
43
|
+
def build_factory
|
|
44
|
+
factory = MessagePack::Factory.new
|
|
45
|
+
register_symbol(factory)
|
|
46
|
+
register_handle(factory)
|
|
47
|
+
register_fault(factory)
|
|
48
|
+
factory.freeze
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Symbol-to-name packer for the ext-0x00 registration.
|
|
52
|
+
def pack_symbol(symbol)
|
|
53
|
+
symbol.name
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Validate the ext-0x00 payload as UTF-8 and intern. Raises
|
|
57
|
+
# InvalidEncoding on invalid bytes — SPEC forbids the
|
|
58
|
+
# binary-encoding fallback that msgpack-gem's default unpacker
|
|
59
|
+
# would otherwise apply. The re-tag step lives here because the
|
|
60
|
+
# msgpack ext-type unpacker hands us binary bytes; the assertion
|
|
61
|
+
# itself is shared with Decoder via Utils.assert_utf8!. The
|
|
62
|
+
# +"Symbol"+ label keeps the error message in Ruby vocabulary
|
|
63
|
+
# rather than wire-ext-code vocabulary.
|
|
64
|
+
def unpack_symbol(payload)
|
|
65
|
+
name = payload.b.force_encoding(Encoding::UTF_8)
|
|
66
|
+
Utils.assert_utf8!(name, "Symbol payload")
|
|
67
|
+
name.to_sym
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Handle-id packer for the ext-0x01 registration: the fixext-4
|
|
71
|
+
# big-endian id frame.
|
|
72
|
+
def pack_handle(handle)
|
|
73
|
+
[handle.id].pack("N")
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Peel off the fixext-4 frame, hand the bytes to the
|
|
77
|
+
# Host-Gem-internal +Kobako::Handle.restore+ factory, and
|
|
78
|
+
# translate the +ArgumentError+ raised by Handle's invariants
|
|
79
|
+
# into a wire-layer +InvalidType+ via Codec::Utils.with_boundary.
|
|
80
|
+
# The Value Object owns the id-range contract; this method only
|
|
81
|
+
# owns the frame shape. Records the Handle sighting on +state+ so a
|
|
82
|
+
# Handle-free decode can skip the downstream resolution walk.
|
|
83
|
+
def unpack_handle(payload, state)
|
|
84
|
+
state.record_handle!
|
|
85
|
+
bytes = payload.b
|
|
86
|
+
raise InvalidType, "Handle payload must be 4 bytes, got #{bytes.bytesize}" unless bytes.bytesize == 4
|
|
87
|
+
|
|
88
|
+
id = bytes.unpack1("N") # : Integer
|
|
89
|
+
Codec::Utils.with_boundary { Kobako::Handle.restore(id) }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Encode the inner ext-0x02 map via Encoder (not the raw factory) so
|
|
93
|
+
# the embedded payload flows through the same boundary as a top-level
|
|
94
|
+
# encode — nested kobako values (Handle, nested Fault) reach the
|
|
95
|
+
# registered ext-type packers. A +details+ chain nested past the
|
|
96
|
+
# +state+ depth cap has no wire representation and surfaces as
|
|
97
|
+
# +UnsupportedType+. In a payload position (+state+ inside a
|
|
98
|
+
# forbid_faults bracket) the envelope has no wire representation at
|
|
99
|
+
# all, so the refusal routes the value into the position's
|
|
100
|
+
# non-representable handling — the Dispatcher's auto-wrap rescue,
|
|
101
|
+
# or a raise at the yield site.
|
|
102
|
+
def pack_fault(fault, state)
|
|
103
|
+
if state.faults_forbidden?
|
|
104
|
+
raise UnsupportedType, "Kobako::Fault has no wire representation in a payload position"
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
state.within_ext_frame(UnsupportedType) do
|
|
108
|
+
Encoder.encode("type" => fault.type, "message" => fault.message, "details" => fault.details)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Peel the embedded msgpack map and hand it to +Kobako::Fault.new+
|
|
113
|
+
# inside Decoder.decode's block form, so the value-object's
|
|
114
|
+
# +ArgumentError+ invariants surface as +InvalidType+ through the
|
|
115
|
+
# decoder boundary. Inner decode goes through Decoder (not the raw
|
|
116
|
+
# factory) so the embedded +str+ payloads flow through the same
|
|
117
|
+
# UTF-8 validation as a top-level decode. A nested ext 0x02 in
|
|
118
|
+
# +details+ re-enters this method, so the +state+ ext-frame guard
|
|
119
|
+
# bounds the chain depth to keep it from exhausting the native stack.
|
|
120
|
+
# In a payload position (+state+ inside a forbid_faults bracket) the
|
|
121
|
+
# envelope is a wire violation outright — its sole legal position is
|
|
122
|
+
# the Response fault field.
|
|
123
|
+
def unpack_fault(payload, state)
|
|
124
|
+
if state.faults_forbidden?
|
|
125
|
+
raise InvalidType, "Fault envelope (ext 0x02) is not a legal value in a payload position"
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
state.within_ext_frame(InvalidType) do
|
|
129
|
+
Decoder.decode(payload) do |map|
|
|
130
|
+
raise InvalidType, "Fault payload must be a map" unless map.is_a?(Hash)
|
|
131
|
+
|
|
132
|
+
Kobako::Fault.new(type: map["type"], message: map["message"], details: map["details"])
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def register_symbol(factory)
|
|
138
|
+
factory.register_type(
|
|
139
|
+
EXT_SYMBOL, Symbol,
|
|
140
|
+
packer: ->(symbol) { ExtTypes.pack_symbol(symbol) },
|
|
141
|
+
unpacker: ->(payload) { ExtTypes.unpack_symbol(payload) }
|
|
142
|
+
)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def register_handle(factory)
|
|
146
|
+
factory.register_type(
|
|
147
|
+
EXT_HANDLE, Kobako::Handle,
|
|
148
|
+
packer: ->(handle) { ExtTypes.pack_handle(handle) },
|
|
149
|
+
unpacker: ->(payload) { ExtTypes.unpack_handle(payload, State.current) }
|
|
150
|
+
)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def register_fault(factory)
|
|
154
|
+
factory.register_type(
|
|
155
|
+
EXT_ERRENV, Kobako::Fault,
|
|
156
|
+
packer: ->(fault) { ExtTypes.pack_fault(fault, State.current) },
|
|
157
|
+
unpacker: ->(payload) { ExtTypes.unpack_fault(payload, State.current) }
|
|
158
|
+
)
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# The process-wide registered factory: ext registration is paid once at
|
|
163
|
+
# load, and a registered +MessagePack::Factory+ only reads its type
|
|
164
|
+
# registry afterwards, so every thread shares this instance for byte
|
|
165
|
+
# work.
|
|
166
|
+
FACTORY = ExtTypes.build_factory
|
|
167
|
+
private_constant :FACTORY
|
|
168
|
+
end
|
|
169
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kobako
|
|
4
|
+
module Codec
|
|
5
|
+
# Codec-internal, per-thread state of the operation in flight: the
|
|
6
|
+
# ext-envelope nesting depth and whether a Capability Handle crossed
|
|
7
|
+
# the current decode. Thread scoping is what makes plain instance
|
|
8
|
+
# variables sound — host codec calls run synchronously on their
|
|
9
|
+
# owning thread, and a nested decode (an ext 0x02 Fault re-entering
|
|
10
|
+
# through its +details+) reuses the same thread instance, so the
|
|
11
|
+
# depth counter accumulates across the re-entry instead of resetting.
|
|
12
|
+
class State
|
|
13
|
+
# An ext 0x02 (Fault) envelope nests through its +details+ field, and
|
|
14
|
+
# each level re-enters the codec with a fresh +MessagePack+ unpacker
|
|
15
|
+
# whose built-in stack guard resets — so ext-envelope depth is tracked
|
|
16
|
+
# here instead. The cap matches the wire's overall nesting bound and
|
|
17
|
+
# keeps a nested chain from exhausting the native stack: an over-deep
|
|
18
|
+
# chain fails as a clean wire error, never a stack-level trap.
|
|
19
|
+
MAX_EXT_DEPTH = 128
|
|
20
|
+
private_constant :MAX_EXT_DEPTH
|
|
21
|
+
|
|
22
|
+
# Thread-local slot holding the calling thread's State.
|
|
23
|
+
STATE_KEY = :__kobako_codec_state__
|
|
24
|
+
private_constant :STATE_KEY
|
|
25
|
+
|
|
26
|
+
# The calling thread's State, built on first use so the mutable
|
|
27
|
+
# state stays isolated to the thread that runs the codec call.
|
|
28
|
+
def self.current
|
|
29
|
+
Thread.current[STATE_KEY] ||= new
|
|
30
|
+
end
|
|
31
|
+
private_class_method :new
|
|
32
|
+
|
|
33
|
+
def initialize
|
|
34
|
+
@ext_depth = 0
|
|
35
|
+
@carried_handle = false
|
|
36
|
+
@faults_forbidden = false
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Bracket a decode and return the block's result together with
|
|
40
|
+
# whether the decoded tree carried an ext 0x01 Capability Handle.
|
|
41
|
+
# ExtTypes#unpack_handle is the sole chokepoint every Handle passes
|
|
42
|
+
# through, so one decode pass records the whole tree and a caller
|
|
43
|
+
# can skip an all-identity Handle-resolution walk when none was
|
|
44
|
+
# present.
|
|
45
|
+
def track_handles
|
|
46
|
+
@carried_handle = false
|
|
47
|
+
result = yield
|
|
48
|
+
[result, @carried_handle]
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Record that an ext 0x01 Capability Handle crossed the current
|
|
52
|
+
# decode; #track_handles reports it to the bracketing caller.
|
|
53
|
+
def record_handle!
|
|
54
|
+
@carried_handle = true
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Bracket a codec operation in a payload position, where an ext 0x02
|
|
58
|
+
# Fault envelope has no legal wire representation: the fault field of
|
|
59
|
+
# an error Response is its only home. The ext-type conversions
|
|
60
|
+
# consult #faults_forbidden? and refuse the envelope in both
|
|
61
|
+
# directions while the bracket is open. Save/restore keeps a nested
|
|
62
|
+
# legal operation on the same thread unaffected.
|
|
63
|
+
def forbid_faults
|
|
64
|
+
previous = @faults_forbidden
|
|
65
|
+
@faults_forbidden = true
|
|
66
|
+
yield
|
|
67
|
+
ensure
|
|
68
|
+
@faults_forbidden = previous
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Whether the operation in flight sits inside a #forbid_faults
|
|
72
|
+
# bracket — i.e. in a payload position where ext 0x02 is a wire
|
|
73
|
+
# violation.
|
|
74
|
+
def faults_forbidden?
|
|
75
|
+
@faults_forbidden
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Track ext-envelope re-entry depth and refuse a chain past
|
|
79
|
+
# MAX_EXT_DEPTH, raising +over_limit+ so the failure lands in the
|
|
80
|
+
# caller's existing wire-error class. The next depth is checked before
|
|
81
|
+
# it is committed, so an over-deep rejection leaves the counter
|
|
82
|
+
# untouched, and the +ensure+ restores the entry value on the way out.
|
|
83
|
+
def within_ext_frame(over_limit)
|
|
84
|
+
depth = @ext_depth + 1
|
|
85
|
+
raise over_limit, "ext envelope nesting exceeds #{MAX_EXT_DEPTH} levels" if depth > MAX_EXT_DEPTH
|
|
86
|
+
|
|
87
|
+
@ext_depth = depth
|
|
88
|
+
begin
|
|
89
|
+
yield
|
|
90
|
+
ensure
|
|
91
|
+
@ext_depth = depth - 1
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private_constant :State
|
|
97
|
+
end
|
|
98
|
+
end
|
data/lib/kobako/codec/utils.rb
CHANGED
|
@@ -10,7 +10,7 @@ module Kobako
|
|
|
10
10
|
# - UTF-8 assertion at the codec boundary
|
|
11
11
|
# ({docs/wire-codec.md}[link:../../../docs/wire-codec.md]
|
|
12
12
|
# § str/bin Encoding Rules and § Ext Types → ext 0x00). Used by
|
|
13
|
-
# Decoder when walking +str+ family payloads and by
|
|
13
|
+
# Decoder when walking +str+ family payloads and by ExtTypes
|
|
14
14
|
# when validating the +ext 0x00+ Symbol payload.
|
|
15
15
|
# - +ArgumentError+ translation at the codec boundary
|
|
16
16
|
# (#with_boundary) so the public taxonomy stays
|
|
@@ -38,7 +38,7 @@ module Kobako
|
|
|
38
38
|
#
|
|
39
39
|
# Reach for this only where a value object is constructed outside a
|
|
40
40
|
# Decoder.decode block, whose rescue already performs the same
|
|
41
|
-
# mapping (worked example:
|
|
41
|
+
# mapping (worked example: ExtTypes#unpack_handle building
|
|
42
42
|
# +Handle.restore+ from a raw fixext payload). Do not use it for
|
|
43
43
|
# general-purpose validation outside the codec boundary —
|
|
44
44
|
# host-layer +ArgumentError+ values should propagate unchanged.
|
data/lib/kobako/codec.rb
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
require_relative "codec/error"
|
|
4
4
|
require_relative "codec/utils"
|
|
5
5
|
require_relative "codec/handle_walk"
|
|
6
|
-
require_relative "codec/
|
|
6
|
+
require_relative "codec/state"
|
|
7
|
+
require_relative "codec/ext_types"
|
|
7
8
|
require_relative "codec/encoder"
|
|
8
9
|
require_relative "codec/decoder"
|
|
9
10
|
|
|
@@ -18,14 +19,30 @@ module Kobako
|
|
|
18
19
|
# the kobako root so the codec can register them without depending
|
|
19
20
|
# upward on Transport.
|
|
20
21
|
#
|
|
21
|
-
# Backed by the official +msgpack+ gem
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
22
|
+
# Backed by the official +msgpack+ gem: ExtTypes registers the three
|
|
23
|
+
# kobako-specific ext types (0x00 Symbol, 0x01 Capability Handle,
|
|
24
|
+
# 0x02 Exception envelope) on one process-wide +MessagePack::Factory+,
|
|
25
|
+
# and Encoder / Decoder are thin wrappers over it. The Rust side
|
|
25
26
|
# mirrors this layer as the +codec+ module in the +kobako-codec+ crate;
|
|
26
|
-
# the ext-code constants live as module-private values on
|
|
27
|
+
# the ext-code constants live as module-private values on ExtTypes
|
|
27
28
|
# alongside +codec::EXT_SYMBOL+ / +codec::EXT_HANDLE+ /
|
|
28
29
|
# +codec::EXT_ERRENV+ on that side.
|
|
29
30
|
module Codec
|
|
31
|
+
# Bracket a decode and return the block's result together with whether
|
|
32
|
+
# the decoded tree carried an ext 0x01 Capability Handle — the signal a
|
|
33
|
+
# dispatch path uses to skip an all-identity Handle-resolution walk.
|
|
34
|
+
# The tracking state is codec-internal; this is its only readout.
|
|
35
|
+
def self.track_handles(&block)
|
|
36
|
+
State.current.track_handles(&block)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Bracket a codec operation in a payload position: an ext 0x02 Fault
|
|
40
|
+
# envelope is only legal in the Response fault field, so the envelope
|
|
41
|
+
# layers open this bracket around every other encode / decode and the
|
|
42
|
+
# ext-type conversions refuse the envelope while it is open — a wire
|
|
43
|
+
# violation on decode, no wire representation on encode.
|
|
44
|
+
def self.forbid_faults(&block)
|
|
45
|
+
State.current.forbid_faults(&block)
|
|
46
|
+
end
|
|
30
47
|
end
|
|
31
48
|
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kobako
|
|
4
|
+
# Kobako::Extension — a guest idiom paired with an optional host backend,
|
|
5
|
+
# installed on a Sandbox via +Sandbox#install+. It composes the existing
|
|
6
|
+
# +#preload+ (the guest +source+) and +#bind+ (the +backend+) verbs into
|
|
7
|
+
# one setup unit, so guest code sees a native-style constant whose pure
|
|
8
|
+
# methods run in-guest and whose privileged methods dispatch to the
|
|
9
|
+
# backend.
|
|
10
|
+
#
|
|
11
|
+
# The four readers form the contract +#install+ duck-types on:
|
|
12
|
+
#
|
|
13
|
+
# * +name+ — a Symbol matching +/\A[A-Z]\w*\z/+, the preloaded snippet's
|
|
14
|
+
# canonical backtrace name and the +depends_on+ match key. Independent
|
|
15
|
+
# of any bound path.
|
|
16
|
+
# * +source+ — the mruby idiom as a String; always present, since an
|
|
17
|
+
# Extension always carries a guest idiom. A host object with no idiom
|
|
18
|
+
# is bound with +#bind+ directly.
|
|
19
|
+
# * +backend+ — an +Extension::Backend+ or +nil+ for a pure-guest
|
|
20
|
+
# Extension.
|
|
21
|
+
# * +depends_on+ — Symbol names of Extensions that must also be
|
|
22
|
+
# installed, checked for presence at the first invocation.
|
|
23
|
+
#
|
|
24
|
+
# +Kobako::Extension+ is the bundled value type; any object exposing the
|
|
25
|
+
# four readers is equally valid, so a Host App or gem may supply its own.
|
|
26
|
+
class Extension < Data.define(:name, :source, :backend, :depends_on)
|
|
27
|
+
# Kobako::Extension::Backend — the host attachment of an Extension,
|
|
28
|
+
# pairing +path+ (the constant path the backend binds at, single-segment
|
|
29
|
+
# +"File"+ or nested +"MyApp::Store"+, spelling the guest constant the
|
|
30
|
+
# idiom routes to) with +provider+ (the source of the bound object).
|
|
31
|
+
#
|
|
32
|
+
# A +provider+ that is not itself callable is the bound object, resolved
|
|
33
|
+
# once for the Sandbox's life; a callable provider is invoked once per
|
|
34
|
+
# invocation to yield that invocation's object, so a fresh object backs
|
|
35
|
+
# the path every invocation. Callability is the sole discriminator — a
|
|
36
|
+
# fixed backend that is itself callable is supplied through a
|
|
37
|
+
# non-callable wrapper.
|
|
38
|
+
class Backend < Data.define(:path, :provider)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# +backend+ and +depends_on+ default to absent so the common
|
|
42
|
+
# pure-idiom and single-backend shapes stay terse.
|
|
43
|
+
def initialize(name:, source:, backend: nil, depends_on: [])
|
|
44
|
+
super
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
data/lib/kobako/fault.rb
CHANGED
|
@@ -5,7 +5,7 @@ module Kobako
|
|
|
5
5
|
#
|
|
6
6
|
# Top-level shared wire primitive: like +Kobako::Handle+ (ext 0x01),
|
|
7
7
|
# +Fault+ is a MessagePack ext-type leaf registered by
|
|
8
|
-
# +Kobako::Codec::
|
|
8
|
+
# +Kobako::Codec::ExtTypes+ and rides nested inside other envelopes (a
|
|
9
9
|
# +Kobako::Transport::Response+ error payload, or another Fault's
|
|
10
10
|
# +details+). It lives at the kobako root rather than under +Transport+
|
|
11
11
|
# because the Codec layer must register it, and Codec must not depend
|
data/lib/kobako/handle.rb
CHANGED
|
@@ -18,7 +18,7 @@ module Kobako
|
|
|
18
18
|
# constructor — is removed for the same reason: a legitimate Handle
|
|
19
19
|
# must not derive a sibling with a caller-chosen id. The Host Gem itself constructs
|
|
20
20
|
# Handles through +.restore+, which exists at exactly two call
|
|
21
|
-
# sites: +Kobako::Codec::
|
|
21
|
+
# sites: +Kobako::Codec::ExtTypes#unpack_handle+ (wire decode) and
|
|
22
22
|
# +Kobako::Codec::HandleWalk.deep_wrap+ / +Kobako::Transport::Dispatcher#wrap_as_handle+
|
|
23
23
|
# (allocator paths). Both live inside +lib/kobako/+ and are not part
|
|
24
24
|
# of any public surface.
|
data/lib/kobako/outcome.rb
CHANGED
|
@@ -70,7 +70,9 @@ module Kobako
|
|
|
70
70
|
# "Symbol payload must be …" wording, but operators triaging a
|
|
71
71
|
# corrupted Sandbox runtime still need it.
|
|
72
72
|
def decode_value(body)
|
|
73
|
-
|
|
73
|
+
# The Result envelope is a payload position: an ext 0x02 Fault in it
|
|
74
|
+
# is a wire violation routed into the invalid-result rescue below.
|
|
75
|
+
Kobako::Codec.forbid_faults { Kobako::Codec::Decoder.decode(body) }
|
|
74
76
|
rescue Kobako::Codec::Error => e
|
|
75
77
|
raise build_transport_error(
|
|
76
78
|
"Sandbox produced an invalid result value",
|
|
@@ -99,13 +101,17 @@ module Kobako
|
|
|
99
101
|
# through the decoder boundary; the message itself is never user-
|
|
100
102
|
# facing — it lands in +details+ via the rescue chain above.
|
|
101
103
|
def build_panic_record(body)
|
|
102
|
-
|
|
103
|
-
|
|
104
|
+
# The Panic envelope is a payload position: an ext 0x02 Fault in its
|
|
105
|
+
# +details+ is a wire violation routed into the invalid-panic rescue.
|
|
106
|
+
Kobako::Codec.forbid_faults do
|
|
107
|
+
Kobako::Codec::Decoder.decode(body) do |map|
|
|
108
|
+
raise Kobako::Codec::InvalidType, "panic body must be a map, got #{map.class}" unless map.is_a?(Hash)
|
|
104
109
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
110
|
+
Panic.new(
|
|
111
|
+
origin: map["origin"], klass: map["class"], message: map["message"],
|
|
112
|
+
backtrace: map["backtrace"] || [], details: map["details"]
|
|
113
|
+
)
|
|
114
|
+
end
|
|
109
115
|
end
|
|
110
116
|
end
|
|
111
117
|
|