kobako 0.4.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.
Files changed (96) hide show
  1. checksums.yaml +4 -4
  2. data/.release-please-manifest.json +1 -0
  3. data/CHANGELOG.md +29 -0
  4. data/Cargo.lock +1 -1
  5. data/README.md +0 -1
  6. data/data/kobako.wasm +0 -0
  7. data/ext/kobako/Cargo.toml +1 -1
  8. data/ext/kobako/src/lib.rs +4 -2
  9. data/ext/kobako/src/{wasm → runtime}/cache.rs +12 -16
  10. data/ext/kobako/src/runtime/capture.rs +91 -0
  11. data/ext/kobako/src/runtime/config.rs +26 -0
  12. data/ext/kobako/src/runtime/dispatch.rs +211 -0
  13. data/ext/kobako/src/runtime/exports.rs +51 -0
  14. data/ext/kobako/src/runtime/guest_mem.rs +228 -0
  15. data/ext/kobako/src/{wasm/host_state.rs → runtime/invocation.rs} +94 -86
  16. data/ext/kobako/src/runtime/trap.rs +134 -0
  17. data/ext/kobako/src/runtime.rs +782 -0
  18. data/ext/kobako/src/snapshot.rs +110 -0
  19. data/lib/kobako/capture.rb +11 -16
  20. data/lib/kobako/catalog/handles.rb +107 -0
  21. data/lib/kobako/catalog/namespaces.rb +99 -0
  22. data/lib/kobako/{snippet/table.rb → catalog/snippets.rb} +37 -62
  23. data/lib/kobako/catalog.rb +18 -0
  24. data/lib/kobako/codec/decoder.rb +13 -5
  25. data/lib/kobako/codec/factory.rb +12 -12
  26. data/lib/kobako/codec/utils.rb +56 -59
  27. data/lib/kobako/codec.rb +6 -3
  28. data/lib/kobako/errors.rb +45 -28
  29. data/lib/kobako/fault.rb +40 -0
  30. data/lib/kobako/handle.rb +4 -6
  31. data/lib/kobako/namespace.rb +67 -0
  32. data/lib/kobako/outcome.rb +31 -35
  33. data/lib/kobako/runtime.rb +30 -0
  34. data/lib/kobako/sandbox.rb +83 -72
  35. data/lib/kobako/sandbox_options.rb +6 -9
  36. data/lib/kobako/snapshot.rb +40 -0
  37. data/lib/kobako/snippet/binary.rb +6 -7
  38. data/lib/kobako/snippet/source.rb +8 -8
  39. data/lib/kobako/snippet.rb +7 -9
  40. data/lib/kobako/transport/dispatcher.rb +195 -0
  41. data/lib/kobako/{rpc/wire_error.rb → transport/error.rb} +7 -6
  42. data/lib/kobako/transport/request.rb +78 -0
  43. data/lib/kobako/transport/response.rb +69 -0
  44. data/lib/kobako/transport/run.rb +141 -0
  45. data/lib/kobako/transport/yield.rb +91 -0
  46. data/lib/kobako/transport/yielder.rb +89 -0
  47. data/lib/kobako/transport.rb +24 -0
  48. data/lib/kobako/version.rb +1 -1
  49. data/lib/kobako.rb +4 -4
  50. data/release-please-config.json +24 -0
  51. data/sig/kobako/capture.rbs +0 -2
  52. data/sig/kobako/catalog/handles.rbs +19 -0
  53. data/sig/kobako/catalog/namespaces.rbs +17 -0
  54. data/sig/kobako/{snippet/table.rbs → catalog/snippets.rbs} +2 -11
  55. data/sig/kobako/{rpc.rbs → catalog.rbs} +1 -1
  56. data/sig/kobako/codec/decoder.rbs +2 -1
  57. data/sig/kobako/codec/factory.rbs +2 -2
  58. data/sig/kobako/codec/utils.rbs +5 -5
  59. data/sig/kobako/errors.rbs +7 -7
  60. data/sig/kobako/fault.rbs +19 -0
  61. data/sig/kobako/handle.rbs +2 -3
  62. data/sig/kobako/namespace.rbs +19 -0
  63. data/sig/kobako/outcome.rbs +2 -2
  64. data/sig/kobako/runtime.rbs +23 -0
  65. data/sig/kobako/sandbox.rbs +5 -8
  66. data/sig/kobako/snapshot.rbs +15 -0
  67. data/sig/kobako/transport/dispatcher.rbs +34 -0
  68. data/sig/kobako/transport/error.rbs +6 -0
  69. data/sig/kobako/transport/request.rbs +32 -0
  70. data/sig/kobako/transport/response.rbs +30 -0
  71. data/sig/kobako/transport/run.rbs +27 -0
  72. data/sig/kobako/transport/yield.rbs +34 -0
  73. data/sig/kobako/transport/yielder.rbs +21 -0
  74. data/sig/kobako/transport.rbs +4 -0
  75. metadata +48 -30
  76. data/ext/kobako/src/wasm/dispatch.rs +0 -162
  77. data/ext/kobako/src/wasm/instance.rs +0 -873
  78. data/ext/kobako/src/wasm.rs +0 -126
  79. data/lib/kobako/handle_table.rb +0 -119
  80. data/lib/kobako/invocation.rb +0 -143
  81. data/lib/kobako/rpc/dispatcher.rb +0 -171
  82. data/lib/kobako/rpc/envelope.rb +0 -118
  83. data/lib/kobako/rpc/fault.rb +0 -41
  84. data/lib/kobako/rpc/namespace.rb +0 -74
  85. data/lib/kobako/rpc/server.rb +0 -146
  86. data/lib/kobako/rpc.rb +0 -11
  87. data/lib/kobako/wasm.rb +0 -25
  88. data/sig/kobako/handle_table.rbs +0 -23
  89. data/sig/kobako/invocation.rbs +0 -25
  90. data/sig/kobako/rpc/dispatcher.rbs +0 -33
  91. data/sig/kobako/rpc/envelope.rbs +0 -51
  92. data/sig/kobako/rpc/fault.rbs +0 -20
  93. data/sig/kobako/rpc/namespace.rbs +0 -24
  94. data/sig/kobako/rpc/server.rbs +0 -31
  95. data/sig/kobako/rpc/wire_error.rbs +0 -6
  96. data/sig/kobako/wasm.rbs +0 -41
@@ -5,22 +5,21 @@ require_relative "../handle"
5
5
 
6
6
  module Kobako
7
7
  module Codec
8
- # Wire-codec helpers shared by the host-side encoders and decoders.
8
+ # Codec helpers shared by the host-side encoders and decoders.
9
9
  # Three concerns live here today:
10
10
  #
11
- # - UTF-8 assertion at the wire boundary
11
+ # - UTF-8 assertion at the codec boundary
12
12
  # ({docs/wire-codec.md}[link:../../../docs/wire-codec.md]
13
13
  # § str/bin Encoding Rules and § Ext Types → ext 0x00). Used by
14
14
  # {Decoder} when walking +str+ family payloads and by {Factory}
15
15
  # when validating the +ext 0x00+ Symbol payload.
16
- # - Wire-boundary +ArgumentError+ translation
17
- # ({wire_boundary}) so the public taxonomy stays
16
+ # - +ArgumentError+ translation at the codec boundary
17
+ # ({with_boundary}) so the public taxonomy stays
18
18
  # {Kobako::Codec::Error}.
19
- # - Wire-representability predicate ({wire_representable?}) and
20
- # the symmetric host→guest +#run+ argument walk
21
- # ({deep_wrap}) used by +Kobako::Invocation#encode+ to route
22
- # non-wire-representable leaves through the Sandbox's
23
- # +Kobako::HandleTable+
19
+ # - Representability predicate ({representable?}) and the symmetric
20
+ # host→guest +#run+ argument walk ({deep_wrap}) used by
21
+ # +Kobako::Transport::Run#encode+ to route non-representable leaves
22
+ # through the Sandbox's +Kobako::Catalog::Handles+
24
23
  # ({docs/behavior.md B-34}[link:../../../docs/behavior.md]).
25
24
  #
26
25
  # All helpers are pure — they only inspect inputs, never mutate
@@ -38,22 +37,21 @@ module Kobako
38
37
  raise InvalidEncoding, "#{label} is not valid UTF-8"
39
38
  end
40
39
 
41
- # Run +block+ at the wire boundary: every wire Value Object
42
- # (Handle / Fault / Request / Response / Panic) raises
43
- # +ArgumentError+ when an invariant is violated at construction,
44
- # and the wire boundary surfaces those violations to callers as
45
- # {InvalidType} so the public taxonomy stays
46
- # {Kobako::Codec::Error} and never leaks +ArgumentError+ from the
47
- # Ruby standard library.
40
+ # Run +block+ at the codec boundary: a value object raises
41
+ # +ArgumentError+ when an invariant is violated at construction, and
42
+ # this helper surfaces that as {InvalidType} so the public taxonomy
43
+ # stays {Kobako::Codec::Error} and never leaks +ArgumentError+ from
44
+ # the Ruby standard library.
48
45
  #
49
- # Wrap any block that constructs a wire Value Object from decoded
50
- # bytes with this helper to keep the five decode sites uniform
51
- # Request / Response in +Kobako::RPC+, Panic map in
52
- # +Kobako::Outcome+, and the Handle / Fault ext-type unpackers in
53
- # {Factory}. Do not use it for general-purpose validation outside
54
- # the wire boundary host-layer +ArgumentError+ values should
55
- # propagate unchanged.
56
- def wire_boundary
46
+ # Most construction sites no longer reach for this directly: a value
47
+ # object built inside a {Decoder.decode} block has its
48
+ # +ArgumentError+ mapped to {InvalidType} by the decoder's own
49
+ # rescue. The lone remaining caller is {Factory#unpack_handle}, which
50
+ # builds +Handle.restore+ from a raw 4-byte fixext payload without a
51
+ # {Decoder.decode} call. Do not use it for general-purpose validation
52
+ # outside the codec boundary — host-layer +ArgumentError+ values
53
+ # should propagate unchanged.
54
+ def with_boundary
57
55
  yield
58
56
  rescue ::ArgumentError => e
59
57
  raise InvalidType, e.message
@@ -64,43 +62,42 @@ module Kobako
64
62
  # unsigned +uint 64+ maximum
65
63
  # ({docs/wire-codec.md}[link:../../../docs/wire-codec.md] § Type
66
64
  # Mapping #3, the +fixint+ / +int 8..64+ / +uint 8..64+ union).
67
- # Anchored as a +Range+ so {primitive_wire_type?} stays a single
68
- # dispatch line. This is the codec's wire-encode domain — not to
65
+ # Anchored as a +Range+ so {primitive_type?} stays a single
66
+ # dispatch line. This is the codec's encode domain — not to
69
67
  # be confused with the Handle id range, which lives on
70
68
  # +Kobako::Handle+ as +MIN_ID+ / +MAX_ID+ (1..2^31 − 1) and
71
69
  # represents a different concept entirely.
72
70
  MSGPACK_INT_RANGE = (-(2**63)..((2**64) - 1))
73
71
 
74
- # Wire-type predicate
72
+ # Codec-type predicate
75
73
  # ({docs/wire-codec.md}[link:../../../docs/wire-codec.md] § Type
76
74
  # Mapping). Returns +true+ when +value+ belongs to the closed
77
- # 12-entry wire set — +nil+, +TrueClass+, +FalseClass+, +Integer+
78
- # (in the +i64..u64+ value domain), +Float+, +String+, +Symbol+,
79
- # +Kobako::Handle+, +Array+ whose every element is itself
80
- # wire-representable, or +Hash+ whose every key and value are
81
- # wire-representable. Integers outside the codec's signed-64 /
75
+ # 12-entry codec type set — +nil+, +TrueClass+, +FalseClass+,
76
+ # +Integer+ (in the +i64..u64+ value domain), +Float+, +String+,
77
+ # +Symbol+, +Kobako::Handle+, +Array+ whose every element is itself
78
+ # representable, or +Hash+ whose every key and value are
79
+ # representable. Integers outside the codec's signed-64 /
82
80
  # unsigned-64 union are rejected so the predicate agrees with the
83
81
  # msgpack gem's encode-time +RangeError+ behaviour the codec
84
82
  # already surfaces as {UnsupportedType}.
85
- def wire_representable?(value)
86
- primitive_wire_type?(value) || container_wire_representable?(value)
83
+ def representable?(value)
84
+ primitive_type?(value) || container_representable?(value)
87
85
  end
88
86
 
89
87
  # Deep-walk Array / Hash containers in +value+ and replace every
90
- # leaf that fails {wire_representable?} with a +Kobako::Handle+
91
- # allocated from +handle_table+
88
+ # leaf that fails {representable?} with a +Kobako::Handle+
89
+ # allocated from +handler+
92
90
  # ({docs/behavior.md B-34}[link:../../../docs/behavior.md]). The
93
- # walk only descends through wire-representable container shapes
94
- # (Array, Hash) one structural level at a time; a non-
95
- # wire-representable leaf is wrapped as-is without inspecting its
96
- # internal structure. An existing +Kobako::Handle+ is wire-
97
- # representable and passes through unchanged — auto-wrap never
98
- # re-wraps a Handle.
91
+ # walk only descends through representable container shapes
92
+ # (Array, Hash) one structural level at a time; a non-representable
93
+ # leaf is wrapped as-is without inspecting its internal structure.
94
+ # An existing +Kobako::Handle+ is representable and passes through
95
+ # unchanged — auto-wrap never re-wraps a Handle.
99
96
  #
100
- # +value+ may be any Ruby value; +handle_table+ must respond to
97
+ # +value+ may be any Ruby value; +handler+ must respond to
101
98
  # +#alloc(object) -> Kobako::Handle+ (a host-side
102
- # +Kobako::HandleTable+). Returns a structurally equivalent value
103
- # whose leaves are either wire-representable or +Kobako::Handle+
99
+ # +Kobako::Catalog::Handles+). Returns a structurally equivalent value
100
+ # whose leaves are either representable or +Kobako::Handle+
104
101
  # tokens.
105
102
  #
106
103
  # The block bodies spell +Utils.deep_wrap+ explicitly rather than
@@ -110,20 +107,20 @@ module Kobako
110
107
  # (still +Utils+ at definition time, but the qualified form keeps
111
108
  # the dispatch readable when the recursive call sits inside a
112
109
  # Proc captured from elsewhere).
113
- def deep_wrap(value, handle_table)
110
+ def deep_wrap(value, handler)
114
111
  case value
115
- when ::Array then value.map { |element| Utils.deep_wrap(element, handle_table) }
116
- when ::Hash then value.transform_values { |val| Utils.deep_wrap(val, handle_table) }
112
+ when ::Array then value.map { |element| Utils.deep_wrap(element, handler) }
113
+ when ::Hash then value.transform_values { |val| Utils.deep_wrap(val, handler) }
117
114
  else
118
- wire_representable?(value) ? value : handle_table.alloc(value)
115
+ representable?(value) ? value : handler.alloc(value)
119
116
  end
120
117
  end
121
118
 
122
- # Predicate split out of {wire_representable?} for cyclomatic
119
+ # Predicate split out of {representable?} for cyclomatic
123
120
  # budget — the closed-set non-container branch. Returns +true+ for
124
- # the wire scalar leaves and an existing Handle. Not part of the
125
- # public surface; reach for {wire_representable?} instead.
126
- def primitive_wire_type?(value)
121
+ # the scalar leaves and an existing Handle. Not part of the
122
+ # public surface; reach for {representable?} instead.
123
+ def primitive_type?(value)
127
124
  case value
128
125
  when ::NilClass, ::TrueClass, ::FalseClass, ::Float, ::String, ::Symbol, Kobako::Handle then true
129
126
  when ::Integer then MSGPACK_INT_RANGE.cover?(value)
@@ -131,15 +128,15 @@ module Kobako
131
128
  end
132
129
  end
133
130
 
134
- # Predicate split out of {wire_representable?} for cyclomatic
131
+ # Predicate split out of {representable?} for cyclomatic
135
132
  # budget — the container branch. Recurses into Array elements and
136
- # Hash key+value pairs through the public {wire_representable?}.
137
- # Not part of the public surface; reach for {wire_representable?}
133
+ # Hash key+value pairs through the public {representable?}.
134
+ # Not part of the public surface; reach for {representable?}
138
135
  # instead.
139
- def container_wire_representable?(value)
136
+ def container_representable?(value)
140
137
  case value
141
- when ::Array then value.all? { |element| Utils.wire_representable?(element) }
142
- when ::Hash then value.all? { |key, val| Utils.wire_representable?(key) && Utils.wire_representable?(val) }
138
+ when ::Array then value.all? { |element| Utils.representable?(element) }
139
+ when ::Hash then value.all? { |key, val| Utils.representable?(key) && Utils.representable?(val) }
143
140
  else false
144
141
  end
145
142
  end
data/lib/kobako/codec.rb CHANGED
@@ -6,9 +6,12 @@ module Kobako
6
6
  # Host-side MessagePack codec for the kobako wire contract — the
7
7
  # byte-level layer ({docs/wire-codec.md}[link:../../docs/wire-codec.md]).
8
8
  # Two consumers sit on top:
9
- # +Kobako::RPC+ pins the RPC framing (Request / Response)
10
- # and +Kobako::Outcome+ owns the per-+#run+ outcome envelope (Result
11
- # body / Panic map).
9
+ # +Kobako::Transport+ pins the host↔guest framing (Request / Response /
10
+ # Run / Yield) and +Kobako::Outcome+ owns the per-+#run+ outcome
11
+ # envelope (Result body / Panic map). The ext-type leaves this layer
12
+ # carries — +Kobako::Handle+ (0x01) and +Kobako::Fault+ (0x02) — live at
13
+ # the kobako root so the codec can register them without depending
14
+ # upward on Transport.
12
15
  #
13
16
  # Backed by the official +msgpack+ gem via {Factory}; {Encoder} and
14
17
  # {Decoder} are thin wrappers that register the three kobako-specific
data/lib/kobako/errors.rb CHANGED
@@ -2,29 +2,39 @@
2
2
 
3
3
  # Top-level Kobako namespace.
4
4
  module Kobako
5
- # Three-class error taxonomy (docs/behavior.md § Error Scenarios).
5
+ # Error taxonomy (docs/behavior.md § Error Scenarios).
6
6
  #
7
7
  # Every `Kobako::Sandbox` invocation (`#eval` or `#run`) either returns a value or raises
8
- # exactly one of these three classes. Attribution is decided after the
8
+ # exactly one of three invocation-outcome classes. Attribution is decided after the
9
9
  # guest binary returns control to the host (docs/behavior.md
10
10
  # "Step 1 — Wasm trap" then "Step 2 — Outcome envelope tag").
11
11
  #
12
- # Three top-level branches:
12
+ # Three invocation-outcome branches:
13
13
  #
14
14
  # * {TrapError} — Wasm engine layer (trap, OOM, unreachable, or a
15
15
  # wire-violation fallback signalling a corrupted
16
16
  # guest runtime).
17
17
  # * {SandboxError} — sandbox / wire layer (mruby script error,
18
18
  # wire-decode failure on an otherwise valid tag,
19
- # HandleTable exhaustion, output buffer overrun).
20
- # * {ServiceError} — service / capability layer (a Service RPC that
21
- # failed and was not rescued inside the script).
19
+ # Catalog::Handles exhaustion, output buffer overrun).
20
+ # * {ServiceError} — service / capability layer (a Service capability
21
+ # call that failed and was not rescued inside the
22
+ # script).
23
+ #
24
+ # A fourth branch sits outside the invocation taxonomy:
25
+ #
26
+ # * {SetupError} — construction layer. Raised by `Kobako::Sandbox.new`
27
+ # when the wasm runtime cannot be built from the
28
+ # configured +wasm_path+ before any invocation runs
29
+ # (docs/behavior.md E-40 / E-41). Not an invocation
30
+ # outcome, so it never passes through the two-step
31
+ # attribution decision.
22
32
  #
23
33
  # Subclasses pinned by docs/behavior.md Error Classes:
24
34
  #
25
- # * {HandleTableExhausted} < {SandboxError} id cap hit (B-21).
26
- # * {ServiceError::Disconnected} < {ServiceError} — `:disconnected`
27
- # sentinel hit on the HandleTable (E-14).
35
+ # * {ModuleNotBuiltError} < {SetupError} Guest Binary artifact absent
36
+ # at +wasm_path+ (E-40).
37
+ # * {HandlerExhaustedError} < {SandboxError} Handle id cap hit (B-21).
28
38
 
29
39
  # Base for all kobako-raised errors so callers that want to ignore the
30
40
  # taxonomy can rescue a single class.
@@ -59,6 +69,27 @@ module Kobako
59
69
  # point; discard and recreate before another execution.
60
70
  class MemoryLimitError < TrapError; end
61
71
 
72
+ # Construction-layer error raised by +Kobako::Sandbox.new+ /
73
+ # +Kobako::Runtime.from_path+ when the wasm runtime cannot be built
74
+ # from the configured +wasm_path+ before any invocation runs —
75
+ # an unreadable artifact, bytes that are not a valid Wasm module, or
76
+ # engine / linker / instantiation setup failure
77
+ # ({docs/behavior.md E-41}[link:../../docs/behavior.md]). Construction
78
+ # is not an invocation, so +SetupError+ sits beside the invocation
79
+ # taxonomy under +Kobako::Error+ rather than under +TrapError+: no
80
+ # Sandbox is produced, so the +TrapError+ "discard and recreate"
81
+ # recovery contract does not apply.
82
+ class SetupError < Error; end
83
+
84
+ # The named +SetupError+ subclass for the common, actionable case:
85
+ # the Guest Binary artifact is absent at +wasm_path+ — the pre-build
86
+ # state on a fresh clone before +bundle exec rake compile+
87
+ # ({docs/behavior.md E-40}[link:../../docs/behavior.md]). Host Apps
88
+ # that only need "the Sandbox could not be set up" rescue +SetupError+;
89
+ # those wanting to special-case the unbuilt-artifact state rescue
90
+ # +ModuleNotBuiltError+ first.
91
+ class ModuleNotBuiltError < SetupError; end
92
+
62
93
  # Sandbox / wire layer. Raised when the guest ran to completion but
63
94
  # execution failed due to a mruby script error, a protocol fault, or a
64
95
  # host-side wire decode failure on an otherwise valid outcome tag.
@@ -87,27 +118,13 @@ module Kobako
87
118
  @backtrace_lines = backtrace_lines
88
119
  @details = details
89
120
  end
90
-
91
- # docs/behavior.md Error Classes: ServiceError::Disconnected is raised
92
- # when the RPC target Handle resolves to the `:disconnected` sentinel
93
- # in the HandleTable (ABA protection rule — id exists but entry was
94
- # invalidated). E-14.
95
- class Disconnected < ServiceError; end
96
121
  end
97
122
 
98
- # HandleTable lookup-failure error (unknown id passed to #fetch /
99
- # #release). A SandboxError subclass: per the wire-layer rule, an
100
- # unknown Handle id surfaces as a `type="undefined"` Response.error
101
- # envelope inside RpcDispatcher and never reaches the Host App
102
- # directly; outside that path (e.g. tests poking the HandleTable
103
- # directly), it surfaces as a SandboxError.
104
- class HandleTableError < SandboxError; end
105
-
106
- # docs/behavior.md Error Classes: HandleTableExhausted is the canonical
107
- # SandboxError subclass for the id-cap-hit path (B-21). Inherits from
108
- # HandleTableError so a single `rescue Kobako::HandleTableError` covers
109
- # both lookup-failure and cap-exhaustion paths.
110
- class HandleTableExhausted < HandleTableError; end
123
+ # docs/behavior.md Error Classes: HandlerExhaustedError is the canonical
124
+ # SandboxError subclass for the id-cap-hit path (B-21). Raised when the
125
+ # per-invocation Handle ID counter in Catalog::Handles reaches
126
+ # +0x7fff_ffff+ (2³¹ 1) and further allocation would exceed the cap.
127
+ class HandlerExhaustedError < SandboxError; end
111
128
 
112
129
  # docs/behavior.md Error Classes: BytecodeError is the SandboxError
113
130
  # subclass raised when a `#preload(binary:)` snippet fails structural
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kobako
4
+ # Wire-level value object for an ext-0x02 Exception envelope.
5
+ #
6
+ # Top-level shared wire primitive: like +Kobako::Handle+ (ext 0x01),
7
+ # +Fault+ is a MessagePack ext-type leaf registered by
8
+ # +Kobako::Codec::Factory+ and rides nested inside other envelopes (a
9
+ # +Kobako::Transport::Response+ error payload, or another Fault's
10
+ # +details+). It lives at the kobako root rather than under +Transport+
11
+ # because the Codec layer must register it, and Codec must not depend
12
+ # upward on Transport.
13
+ #
14
+ # SPEC pins the payload
15
+ # ({docs/wire-codec.md}[link:../../docs/wire-codec.md] § Ext Types
16
+ # → ext 0x02) to a msgpack map with exactly three keys:
17
+ # * "type" — one of "runtime", "argument", "undefined"
18
+ # * "message" — human-readable string
19
+ # * "details" — any wire-legal value, or nil when absent
20
+ #
21
+ # This object holds the *encoded* form. Reifying the corresponding Ruby
22
+ # exception class (RuntimeError, ArgumentError, Kobako::ServiceError, ...)
23
+ # is the responsibility of the dispatch layer, not the codec.
24
+ #
25
+ # Built on the +class X < Data.define(...)+ subclass form so the
26
+ # class body is fully Steep-visible; ruby/rbs upstream documents
27
+ # this as the Steep-friendly shape and the +Style/DataInheritance+
28
+ # cop is disabled on that basis (see +.rubocop.yml+).
29
+ class Fault < Data.define(:type, :message, :details)
30
+ VALID_TYPES = %w[runtime argument undefined].freeze
31
+
32
+ def initialize(type:, message:, details: nil)
33
+ raise ArgumentError, "type must be String" unless type.is_a?(String)
34
+ raise ArgumentError, "message must be String" unless message.is_a?(String)
35
+ raise ArgumentError, "type=#{type.inspect} not one of #{VALID_TYPES.inspect}" unless VALID_TYPES.include?(type)
36
+
37
+ super
38
+ end
39
+ end
40
+ end
data/lib/kobako/handle.rb CHANGED
@@ -16,9 +16,9 @@ module Kobako
16
16
  # privatised so Host App code cannot fabricate a Handle from a bare
17
17
  # integer; legitimate Handle instances enter Host App code only as
18
18
  # fields on raised error objects. The Host Gem itself constructs
19
- # Handles through {.from_wire}, which exists at exactly two call
19
+ # Handles through {.restore}, which exists at exactly two call
20
20
  # sites: +Kobako::Codec::Factory#unpack_handle+ (wire decode) and
21
- # +Kobako::Codec::Utils.deep_wrap+ / +Kobako::RPC::Dispatcher#wrap_as_handle+
21
+ # +Kobako::Codec::Utils.deep_wrap+ / +Kobako::Transport::Dispatcher#wrap_as_handle+
22
22
  # (allocator paths). Both live inside +lib/kobako/+ and are not part
23
23
  # of any public surface.
24
24
  #
@@ -34,14 +34,12 @@ module Kobako
34
34
  # on either side of the wire without re-encoding.
35
35
  MAX_ID = 0x7fff_ffff
36
36
 
37
- # steep:ignore:start
38
37
  def initialize(id:)
39
38
  raise ArgumentError, "Handle id must be Integer" unless id.is_a?(Integer)
40
39
  raise ArgumentError, "Handle id #{id} out of range [#{MIN_ID}, #{MAX_ID}]" unless id.between?(MIN_ID, MAX_ID)
41
40
 
42
41
  super
43
42
  end
44
- # steep:ignore:end
45
43
 
46
44
  private_class_method :new
47
45
 
@@ -52,10 +50,10 @@ module Kobako
52
50
  #
53
51
  # Two collaborators call this: the codec when an ext 0x01 frame is
54
52
  # decoded off the wire, and the allocator paths when a host-side
55
- # Ruby object is registered into the Sandbox's +HandleTable+. Both
53
+ # Ruby object is registered into the Sandbox's +Catalog::Handles+. Both
56
54
  # paths live inside +lib/kobako/+ and treat this method as a
57
55
  # package-private constructor.
58
- def self.from_wire(id)
56
+ def self.restore(id)
59
57
  allocate.tap { |handle| handle.send(:initialize, id: id) }
60
58
  end
61
59
  end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kobako
4
+ # A named grouping of Members for one Sandbox
5
+ # ({docs/behavior.md B-07..B-11}[link:../../docs/behavior.md]).
6
+ # Returned by +Sandbox#define+. Each instance owns a flat name→object
7
+ # table of Members; member binding is validated against {NAME_PATTERN}.
8
+ class Namespace
9
+ # Ruby constant-name pattern shared by Namespace and Member names
10
+ # ({docs/behavior.md B-07/B-08 Notes}[link:../../docs/behavior.md]).
11
+ NAME_PATTERN = /\A[A-Z]\w*\z/
12
+
13
+ attr_reader :name
14
+
15
+ # Build a new Namespace. +name+ is an already-validated Namespace
16
+ # name (must satisfy {NAME_PATTERN}; validation is the caller's
17
+ # responsibility).
18
+ def initialize(name)
19
+ @name = name
20
+ @members = {} # : Hash[String, untyped]
21
+ end
22
+
23
+ # Bind +object+ under +member+ inside this Namespace. +member+ is a
24
+ # constant-form name as a +Symbol+ or +String+. +object+ is any Ruby
25
+ # object that responds to the methods guest code will invoke. Returns
26
+ # +self+ for chaining. Raises +ArgumentError+ when +member+ does not
27
+ # match the constant pattern, or a Member of the same name is already
28
+ # bound ({docs/behavior.md B-11}[link:../../docs/behavior.md]).
29
+ def bind(member, object)
30
+ member_str = validate_member_name!(member)
31
+ raise ArgumentError, "Member #{@name}::#{member_str} is already bound" if @members.key?(member_str)
32
+
33
+ @members[member_str] = object
34
+ self
35
+ end
36
+
37
+ # Member lookup; raises +KeyError+ when no Member is registered
38
+ # under +member+.
39
+ def fetch(member)
40
+ member_str = member.to_s
41
+ unless @members.key?(member_str)
42
+ raise KeyError,
43
+ "no member named #{member_str.inspect} in namespace #{@name.inspect}"
44
+ end
45
+
46
+ @members[member_str]
47
+ end
48
+
49
+ # Structured description for the guest preamble (Frame 1). Returns a
50
+ # two-element array +[name, member_keys]+ suitable for msgpack encoding.
51
+ def to_preamble
52
+ [@name, @members.keys]
53
+ end
54
+
55
+ private
56
+
57
+ def validate_member_name!(member)
58
+ member_str = member.to_s
59
+ unless NAME_PATTERN.match?(member_str)
60
+ raise ArgumentError,
61
+ "MemberName must match #{NAME_PATTERN.inspect} (got #{member.inspect})"
62
+ end
63
+
64
+ member_str
65
+ end
66
+ end
67
+ end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "outcome/panic"
4
- require_relative "rpc/wire_error"
4
+ require_relative "transport/error"
5
5
 
6
6
  module Kobako
7
7
  # Host-facing boundary for the OUTCOME_BUFFER produced by
@@ -14,7 +14,7 @@ module Kobako
14
14
  # body decoding), and the +Panic+ wire record lives at
15
15
  # +Kobako::Outcome::Panic+. The byte-level msgpack codec at
16
16
  # +Kobako::Codec+ is invoked for the body itself; otherwise
17
- # nothing in +RPC+ participates.
17
+ # nothing in +Transport+ participates.
18
18
  #
19
19
  # * tag 0x01, decode OK → return decoded value
20
20
  # * tag 0x01, decode fails → SandboxError (E-09)
@@ -73,16 +73,16 @@ module Kobako
73
73
 
74
74
  # Decode failure on the success tag is a SandboxError (E-09): the
75
75
  # framing was fine, but the carried value is unrepresentable. The
76
- # specific codec fault is stashed in +details[:wire_error]+ rather
76
+ # specific codec fault is stashed in +details+ rather
77
77
  # than spliced into the message — callers cannot act on the inner
78
78
  # "Symbol payload must be …" wording, but operators triaging a
79
79
  # corrupted Sandbox runtime still need it.
80
80
  def decode_value(body)
81
81
  Kobako::Codec::Decoder.decode(body)
82
82
  rescue Kobako::Codec::Error => e
83
- raise build_wire_violation_error(
83
+ raise build_transport_error(
84
84
  "Sandbox produced an invalid result value",
85
- wire_error: e.message
85
+ detail: e.message
86
86
  )
87
87
  end
88
88
 
@@ -91,25 +91,25 @@ module Kobako
91
91
  # layer exception via +build_panic_error+ and raised; on wire-decode
92
92
  # failure the rescue path raises the wire-violation +SandboxError+.
93
93
  def decode_panic(body)
94
- raise build_panic_error(parse_panic(body))
94
+ raise build_panic_error(build_panic_record(body))
95
95
  rescue Kobako::Codec::Error => e
96
- raise build_wire_violation_error(
96
+ raise build_transport_error(
97
97
  "Sandbox produced an invalid panic record",
98
- wire_error: e.message
98
+ detail: e.message
99
99
  )
100
100
  end
101
101
 
102
102
  # Build a +Panic+ value object from the msgpack-decoded body. Raises
103
- # +Kobako::Codec::InvalidType+ when the body is not a map or
104
- # when required keys are missing — both routed by +decode_panic+ to
105
- # +build_wire_violation_error+. The +InvalidType+ message itself is
106
- # never user-facing; it lands in +details[:wire_error]+ via the
107
- # rescue chain above.
108
- def parse_panic(body)
109
- map = Kobako::Codec::Decoder.decode(body)
110
- raise Kobako::Codec::InvalidType, "panic body must be a map, got #{map.class}" unless map.is_a?(Hash)
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)
111
112
 
112
- Kobako::Codec::Utils.wire_boundary do
113
113
  Panic.new(
114
114
  origin: map["origin"], klass: map["class"], message: map["message"],
115
115
  backtrace: map["backtrace"] || [], details: map["details"]
@@ -118,11 +118,8 @@ module Kobako
118
118
  end
119
119
 
120
120
  # Map a decoded Panic record into the corresponding three-layer
121
- # Ruby exception. +origin == "service"+ → ServiceError (with the
122
- # +::Disconnected+ subclass selected when the panic carries the
123
- # disconnected sentinel —
124
- # {docs/behavior.md Error Classes}[link:../../docs/behavior.md]); everything else
125
- # → SandboxError.
121
+ # Ruby exception. +origin == "service"+ → ServiceError; everything
122
+ # else SandboxError.
126
123
  def build_panic_error(panic)
127
124
  panic_target_class(panic).new(
128
125
  panic.message,
@@ -133,37 +130,36 @@ module Kobako
133
130
  )
134
131
  end
135
132
 
136
- # {docs/behavior.md Error Classes}[link:../../docs/behavior.md]: map
133
+ # {docs/behavior.md Error Scenarios}[link:../../docs/behavior.md]: map
137
134
  # the panic +class+ field to the matching Ruby exception subclass so
138
- # callers can rescue specific failure paths. +origin="service"+ plus
139
- # +class="Kobako::ServiceError::Disconnected"+ selects the
140
- # +Disconnected+ subclass (E-14); +origin="sandbox"+ plus
135
+ # callers can rescue specific failure paths. +origin="service"+
136
+ # +ServiceError+; +origin="sandbox"+ plus
141
137
  # +class="Kobako::BytecodeError"+ selects the +BytecodeError+
142
138
  # subclass (E-37 / E-38). Everything else falls back to the base
143
139
  # class for the origin.
144
140
  def panic_target_class(panic)
145
141
  case panic.origin
146
142
  when Panic::ORIGIN_SERVICE
147
- panic.klass == "Kobako::ServiceError::Disconnected" ? ServiceError::Disconnected : ServiceError
143
+ ServiceError
148
144
  else
149
145
  panic.klass == "Kobako::BytecodeError" ? BytecodeError : SandboxError
150
146
  end
151
147
  end
152
148
 
153
149
  # Lift the wire-violation fallback to the real
154
- # +Kobako::RPC::WireError+ class so callers can +rescue+ it
150
+ # +Kobako::Transport::Error+ class so callers can +rescue+ it
155
151
  # specifically instead of pattern-matching on +error.klass+. The
156
152
  # +klass+ field is still populated so existing operator-side
157
153
  # tooling that greps on the string continues to work.
158
- # +wire_error+ carries the inner codec / framing message for
159
- # operator diagnosis without polluting the user-facing
160
- # +#message+.
161
- def build_wire_violation_error(message, wire_error: nil)
162
- Kobako::RPC::WireError.new(
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(
163
159
  message,
164
160
  origin: Panic::ORIGIN_SANDBOX,
165
- klass: "Kobako::RPC::WireError",
166
- details: wire_error.nil? ? nil : { wire_error: wire_error }
161
+ klass: "Kobako::Transport::Error",
162
+ details: detail
167
163
  )
168
164
  end
169
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