kobako 0.5.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.
Files changed (79) hide show
  1. checksums.yaml +7 -0
  2. data/.release-please-manifest.json +1 -0
  3. data/CHANGELOG.md +29 -0
  4. data/LICENSE +201 -0
  5. data/README.md +408 -0
  6. data/data/kobako.wasm +0 -0
  7. data/lib/kobako/3.3/kobako.so +0 -0
  8. data/lib/kobako/3.4/kobako.so +0 -0
  9. data/lib/kobako/4.0/kobako.so +0 -0
  10. data/lib/kobako/capture.rb +43 -0
  11. data/lib/kobako/catalog/handles.rb +107 -0
  12. data/lib/kobako/catalog/namespaces.rb +99 -0
  13. data/lib/kobako/catalog/snippets.rb +149 -0
  14. data/lib/kobako/catalog.rb +18 -0
  15. data/lib/kobako/codec/decoder.rb +73 -0
  16. data/lib/kobako/codec/encoder.rb +37 -0
  17. data/lib/kobako/codec/error.rb +34 -0
  18. data/lib/kobako/codec/factory.rb +162 -0
  19. data/lib/kobako/codec/utils.rb +145 -0
  20. data/lib/kobako/codec.rb +31 -0
  21. data/lib/kobako/errors.rb +140 -0
  22. data/lib/kobako/fault.rb +40 -0
  23. data/lib/kobako/handle.rb +60 -0
  24. data/lib/kobako/namespace.rb +67 -0
  25. data/lib/kobako/outcome/panic.rb +42 -0
  26. data/lib/kobako/outcome.rb +166 -0
  27. data/lib/kobako/runtime.rb +30 -0
  28. data/lib/kobako/sandbox.rb +314 -0
  29. data/lib/kobako/sandbox_options.rb +70 -0
  30. data/lib/kobako/snapshot.rb +40 -0
  31. data/lib/kobako/snippet/binary.rb +29 -0
  32. data/lib/kobako/snippet/source.rb +28 -0
  33. data/lib/kobako/snippet.rb +18 -0
  34. data/lib/kobako/transport/dispatcher.rb +195 -0
  35. data/lib/kobako/transport/error.rb +24 -0
  36. data/lib/kobako/transport/request.rb +78 -0
  37. data/lib/kobako/transport/response.rb +69 -0
  38. data/lib/kobako/transport/run.rb +141 -0
  39. data/lib/kobako/transport/yield.rb +91 -0
  40. data/lib/kobako/transport/yielder.rb +89 -0
  41. data/lib/kobako/transport.rb +24 -0
  42. data/lib/kobako/usage.rb +41 -0
  43. data/lib/kobako/version.rb +5 -0
  44. data/lib/kobako.rb +10 -0
  45. data/release-please-config.json +24 -0
  46. data/sig/kobako/capture.rbs +11 -0
  47. data/sig/kobako/catalog/handles.rbs +19 -0
  48. data/sig/kobako/catalog/namespaces.rbs +17 -0
  49. data/sig/kobako/catalog/snippets.rbs +27 -0
  50. data/sig/kobako/catalog.rbs +4 -0
  51. data/sig/kobako/codec/decoder.rbs +12 -0
  52. data/sig/kobako/codec/encoder.rbs +7 -0
  53. data/sig/kobako/codec/error.rbs +18 -0
  54. data/sig/kobako/codec/factory.rbs +31 -0
  55. data/sig/kobako/codec/utils.rbs +19 -0
  56. data/sig/kobako/errors.rbs +55 -0
  57. data/sig/kobako/fault.rbs +19 -0
  58. data/sig/kobako/handle.rbs +18 -0
  59. data/sig/kobako/namespace.rbs +19 -0
  60. data/sig/kobako/outcome/panic.rbs +34 -0
  61. data/sig/kobako/outcome.rbs +24 -0
  62. data/sig/kobako/runtime.rbs +23 -0
  63. data/sig/kobako/sandbox.rbs +55 -0
  64. data/sig/kobako/sandbox_options.rbs +32 -0
  65. data/sig/kobako/snapshot.rbs +15 -0
  66. data/sig/kobako/snippet/binary.rbs +12 -0
  67. data/sig/kobako/snippet/source.rbs +13 -0
  68. data/sig/kobako/snippet.rbs +4 -0
  69. data/sig/kobako/transport/dispatcher.rbs +34 -0
  70. data/sig/kobako/transport/error.rbs +6 -0
  71. data/sig/kobako/transport/request.rbs +32 -0
  72. data/sig/kobako/transport/response.rbs +30 -0
  73. data/sig/kobako/transport/run.rbs +27 -0
  74. data/sig/kobako/transport/yield.rbs +34 -0
  75. data/sig/kobako/transport/yielder.rbs +21 -0
  76. data/sig/kobako/transport.rbs +4 -0
  77. data/sig/kobako/usage.rbs +11 -0
  78. data/sig/kobako.rbs +3 -0
  79. metadata +145 -0
@@ -0,0 +1,162 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "singleton"
4
+ require "forwardable"
5
+ require "msgpack"
6
+
7
+ require_relative "error"
8
+ require_relative "utils"
9
+ require_relative "../handle"
10
+ require_relative "../fault"
11
+
12
+ module Kobako
13
+ module Codec
14
+ # Cached +MessagePack::Factory+ that owns the kobako wire ext-type
15
+ # registration ({docs/wire-codec.md}[link:../../../docs/wire-codec.md]
16
+ # § Ext Types).
17
+ #
18
+ # The factory is the single place in the host gem that touches the
19
+ # msgpack API — both {Encoder} and {Decoder} delegate through it, so
20
+ # the three kobako ext codes (0x00 Symbol, 0x01 Capability Handle,
21
+ # 0x02 Exception envelope) are configured exactly once at first use.
22
+ #
23
+ # Lifecycle is owned by +Singleton+ from the Ruby standard library:
24
+ # +Factory.instance+ is lazy, thread-safe, and process-wide. Class-level
25
+ # +Factory.dump+ / +Factory.load+ shortcuts are exposed via
26
+ # +SingleForwardable+ so callers do not have to spell the +.instance+
27
+ # hop at every call site; the instance-level +#dump+ / +#load+ are in
28
+ # turn delegated to the wrapped +MessagePack::Factory+ via +Forwardable+.
29
+ class Factory
30
+ include Singleton
31
+ extend Forwardable
32
+ extend SingleForwardable
33
+
34
+ # MessagePack ext type code reserved for Symbol
35
+ # ({docs/wire-codec.md}[link:../../../docs/wire-codec.md] § Ext Types
36
+ # → ext 0x00). Class-private — mirrors +codec::EXT_SYMBOL+ on the
37
+ # Rust side.
38
+ EXT_SYMBOL = 0x00
39
+ # MessagePack ext type code reserved for Capability Handle
40
+ # ({docs/wire-codec.md}[link:../../../docs/wire-codec.md] § Ext Types
41
+ # → ext 0x01). Class-private — mirrors +codec::EXT_HANDLE+ on the
42
+ # Rust side.
43
+ EXT_HANDLE = 0x01
44
+ # MessagePack ext type code reserved for Exception envelope
45
+ # ({docs/wire-codec.md}[link:../../../docs/wire-codec.md] § Ext Types
46
+ # → ext 0x02). Class-private — mirrors +codec::EXT_ERRENV+ on the
47
+ # Rust side.
48
+ EXT_ERRENV = 0x02
49
+ private_constant :EXT_SYMBOL, :EXT_HANDLE, :EXT_ERRENV
50
+
51
+ # Instance-level pass-through onto the wrapped +MessagePack::Factory+.
52
+ # Spelled +def_instance_delegators+ rather than +def_delegators+ because
53
+ # the class also extends +SingleForwardable+ (see the +extend+ block
54
+ # above), which defines its own +def_delegators+ that shadows
55
+ # +Forwardable+'s — the unambiguous forms keep both delegation tiers
56
+ # wired to the right scope.
57
+ def_instance_delegators :@factory, :dump, :load
58
+
59
+ # Class-level shortcuts so callers can write +Factory.dump(v)+ instead
60
+ # of +Factory.instance.dump(v)+; both resolve to the same singleton.
61
+ def_single_delegators :instance, :dump, :load
62
+
63
+ def initialize
64
+ @factory = MessagePack::Factory.new
65
+ register_symbol
66
+ register_handle
67
+ register_fault
68
+ end
69
+
70
+ private
71
+
72
+ def register_symbol
73
+ @factory.register_type(
74
+ EXT_SYMBOL, Symbol,
75
+ packer: method(:pack_symbol),
76
+ unpacker: method(:unpack_symbol)
77
+ )
78
+ end
79
+
80
+ # Symbol-to-name packer — extracted to a real method so Steep can
81
+ # resolve the proc shape without tripping on +lambda(&:name)+'s
82
+ # +Symbol#to_proc+ inference path.
83
+ def pack_symbol(symbol)
84
+ symbol.name
85
+ end
86
+
87
+ # Validate the ext-0x00 payload as UTF-8 and intern. Raises
88
+ # {InvalidEncoding} on invalid bytes — SPEC forbids the
89
+ # binary-encoding fallback that msgpack-gem's default unpacker
90
+ # would otherwise apply. The re-tag step lives here because the
91
+ # msgpack ext-type unpacker hands us binary bytes; the assertion
92
+ # itself is shared with {Decoder} via {Utils.assert_utf8!}. The
93
+ # +"Symbol"+ label keeps the error message in Ruby vocabulary
94
+ # rather than wire-ext-code vocabulary.
95
+ def unpack_symbol(payload)
96
+ name = payload.b.force_encoding(Encoding::UTF_8)
97
+ Utils.assert_utf8!(name, "Symbol payload")
98
+ name.to_sym
99
+ end
100
+
101
+ def register_handle
102
+ @factory.register_type(
103
+ EXT_HANDLE, Kobako::Handle,
104
+ packer: ->(handle) { [handle.id].pack("N") },
105
+ unpacker: ->(payload) { unpack_handle(payload) }
106
+ )
107
+ end
108
+
109
+ def register_fault
110
+ @factory.register_type(
111
+ EXT_ERRENV, Kobako::Fault,
112
+ packer: ->(fault) { pack_fault(fault) },
113
+ unpacker: ->(payload) { unpack_fault(payload) }
114
+ )
115
+ end
116
+
117
+ # Peel off the fixext-4 frame, hand the bytes to the
118
+ # Host-Gem-internal +Kobako::Handle.restore+ factory, and
119
+ # translate the +ArgumentError+ raised by Handle's invariants
120
+ # into a wire-layer +InvalidType+ via {Codec::Utils.with_boundary}.
121
+ # The Value Object owns the id-range contract; this method only
122
+ # owns the frame shape.
123
+ def unpack_handle(payload)
124
+ bytes = payload.b
125
+ raise InvalidType, "Handle payload must be 4 bytes, got #{bytes.bytesize}" unless bytes.bytesize == 4
126
+
127
+ id = bytes.unpack1("N") # : Integer
128
+ Codec::Utils.with_boundary { Kobako::Handle.restore(id) }
129
+ end
130
+
131
+ # Encode the inner ext-0x02 map via {Encoder} (not +factory.dump+) so
132
+ # the embedded payload flows through the same boundary as a top-level
133
+ # encode — nested kobako values (Handle, nested Fault) reach the
134
+ # registered ext-type packers via the cached singleton.
135
+ def pack_fault(fault)
136
+ Encoder.encode("type" => fault.type, "message" => fault.message, "details" => fault.details)
137
+ end
138
+
139
+ # Peel the embedded msgpack map and hand it to +Kobako::Fault.new+
140
+ # inside {Decoder.decode}'s block form, so the value-object's
141
+ # +ArgumentError+ invariants surface as +InvalidType+ through the
142
+ # decoder boundary. Inner decode goes through {Decoder} (not
143
+ # +factory.load+) so the embedded +str+ payloads flow through the
144
+ # same UTF-8 validation as a top-level decode.
145
+ #
146
+ # This establishes a runtime cycle Factory → Decoder → Factory: the
147
+ # singleton instance feeds +Decoder.decode+, which re-enters this
148
+ # method when a nested ext 0x02 appears inside +details+. The recursion
149
+ # is bounded by msgpack nesting depth — identical to nested Array /
150
+ # Hash payloads — so no extra guard is needed. Do not switch back to
151
+ # +factory.load+ to "simplify": that path bypasses UTF-8 validation
152
+ # and re-opens the Decoder's special case for Fault (removed in M5).
153
+ def unpack_fault(payload)
154
+ Decoder.decode(payload) do |map|
155
+ raise InvalidType, "Fault payload must be a map" unless map.is_a?(Hash)
156
+
157
+ Kobako::Fault.new(type: map["type"], message: map["message"], details: map["details"])
158
+ end
159
+ end
160
+ end
161
+ end
162
+ end
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "error"
4
+ require_relative "../handle"
5
+
6
+ module Kobako
7
+ module Codec
8
+ # Codec helpers shared by the host-side encoders and decoders.
9
+ # Three concerns live here today:
10
+ #
11
+ # - UTF-8 assertion at the codec boundary
12
+ # ({docs/wire-codec.md}[link:../../../docs/wire-codec.md]
13
+ # § str/bin Encoding Rules and § Ext Types → ext 0x00). Used by
14
+ # {Decoder} when walking +str+ family payloads and by {Factory}
15
+ # when validating the +ext 0x00+ Symbol payload.
16
+ # - +ArgumentError+ translation at the codec boundary
17
+ # ({with_boundary}) so the public taxonomy stays
18
+ # {Kobako::Codec::Error}.
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+
23
+ # ({docs/behavior.md B-34}[link:../../../docs/behavior.md]).
24
+ #
25
+ # All helpers are pure — they only inspect inputs, never mutate
26
+ # them — except {deep_wrap}, whose only side effect is allocating
27
+ # new Handle ids into the supplied table.
28
+ module Utils
29
+ module_function
30
+
31
+ # Raise {InvalidEncoding} unless +string+'s bytes are valid under
32
+ # its current encoding tag. +label+ is the caller-supplied prefix
33
+ # for the error message (e.g. +"str payload"+, +"Symbol payload"+).
34
+ def assert_utf8!(string, label)
35
+ return if string.valid_encoding?
36
+
37
+ raise InvalidEncoding, "#{label} is not valid UTF-8"
38
+ end
39
+
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.
45
+ #
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
55
+ yield
56
+ rescue ::ArgumentError => e
57
+ raise InvalidType, e.message
58
+ end
59
+
60
+ # Inclusive Integer range the msgpack gem encodes without raising
61
+ # +RangeError+ at encode time — signed +int 64+ minimum through
62
+ # unsigned +uint 64+ maximum
63
+ # ({docs/wire-codec.md}[link:../../../docs/wire-codec.md] § Type
64
+ # Mapping #3, the +fixint+ / +int 8..64+ / +uint 8..64+ union).
65
+ # Anchored as a +Range+ so {primitive_type?} stays a single
66
+ # dispatch line. This is the codec's encode domain — not to
67
+ # be confused with the Handle id range, which lives on
68
+ # +Kobako::Handle+ as +MIN_ID+ / +MAX_ID+ (1..2^31 − 1) and
69
+ # represents a different concept entirely.
70
+ MSGPACK_INT_RANGE = (-(2**63)..((2**64) - 1))
71
+
72
+ # Codec-type predicate
73
+ # ({docs/wire-codec.md}[link:../../../docs/wire-codec.md] § Type
74
+ # Mapping). Returns +true+ when +value+ belongs to the closed
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 /
80
+ # unsigned-64 union are rejected so the predicate agrees with the
81
+ # msgpack gem's encode-time +RangeError+ behaviour the codec
82
+ # already surfaces as {UnsupportedType}.
83
+ def representable?(value)
84
+ primitive_type?(value) || container_representable?(value)
85
+ end
86
+
87
+ # Deep-walk Array / Hash containers in +value+ and replace every
88
+ # leaf that fails {representable?} with a +Kobako::Handle+
89
+ # allocated from +handler+
90
+ # ({docs/behavior.md B-34}[link:../../../docs/behavior.md]). The
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.
96
+ #
97
+ # +value+ may be any Ruby value; +handler+ must respond to
98
+ # +#alloc(object) -> Kobako::Handle+ (a host-side
99
+ # +Kobako::Catalog::Handles+). Returns a structurally equivalent value
100
+ # whose leaves are either representable or +Kobako::Handle+
101
+ # tokens.
102
+ #
103
+ # The block bodies spell +Utils.deep_wrap+ explicitly rather than
104
+ # the unqualified +deep_wrap+ because +module_function+ makes the
105
+ # instance copy of these helpers private; an implicit receiver
106
+ # inside a block would resolve against the enclosing +self+
107
+ # (still +Utils+ at definition time, but the qualified form keeps
108
+ # the dispatch readable when the recursive call sits inside a
109
+ # Proc captured from elsewhere).
110
+ def deep_wrap(value, handler)
111
+ case value
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) }
114
+ else
115
+ representable?(value) ? value : handler.alloc(value)
116
+ end
117
+ end
118
+
119
+ # Predicate split out of {representable?} for cyclomatic
120
+ # budget — the closed-set non-container branch. Returns +true+ for
121
+ # the scalar leaves and an existing Handle. Not part of the
122
+ # public surface; reach for {representable?} instead.
123
+ def primitive_type?(value)
124
+ case value
125
+ when ::NilClass, ::TrueClass, ::FalseClass, ::Float, ::String, ::Symbol, Kobako::Handle then true
126
+ when ::Integer then MSGPACK_INT_RANGE.cover?(value)
127
+ else false
128
+ end
129
+ end
130
+
131
+ # Predicate split out of {representable?} for cyclomatic
132
+ # budget — the container branch. Recurses into Array elements and
133
+ # Hash key+value pairs through the public {representable?}.
134
+ # Not part of the public surface; reach for {representable?}
135
+ # instead.
136
+ def container_representable?(value)
137
+ case value
138
+ when ::Array then value.all? { |element| Utils.representable?(element) }
139
+ when ::Hash then value.all? { |key, val| Utils.representable?(key) && Utils.representable?(val) }
140
+ else false
141
+ end
142
+ end
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "codec/error"
4
+
5
+ module Kobako
6
+ # Host-side MessagePack codec for the kobako wire contract — the
7
+ # byte-level layer ({docs/wire-codec.md}[link:../../docs/wire-codec.md]).
8
+ # Two consumers sit on top:
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.
15
+ #
16
+ # Backed by the official +msgpack+ gem via {Factory}; {Encoder} and
17
+ # {Decoder} are thin wrappers that register the three kobako-specific
18
+ # ext types (0x00 Symbol, 0x01 Capability Handle, 0x02 Exception
19
+ # envelope) on a single +MessagePack::Factory+ instance. The Rust side
20
+ # mirrors this layer as the +codec+ module in the +kobako-wasm+ crate;
21
+ # the ext-code constants live as module-private values on {Factory}
22
+ # alongside +codec::EXT_SYMBOL+ / +codec::EXT_HANDLE+ /
23
+ # +codec::EXT_ERRENV+ on that side.
24
+ module Codec
25
+ end
26
+ end
27
+
28
+ require_relative "codec/utils"
29
+ require_relative "codec/factory"
30
+ require_relative "codec/encoder"
31
+ require_relative "codec/decoder"
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Top-level Kobako namespace.
4
+ module Kobako
5
+ # Error taxonomy (docs/behavior.md § Error Scenarios).
6
+ #
7
+ # Every `Kobako::Sandbox` invocation (`#eval` or `#run`) either returns a value or raises
8
+ # exactly one of three invocation-outcome classes. Attribution is decided after the
9
+ # guest binary returns control to the host (docs/behavior.md
10
+ # "Step 1 — Wasm trap" then "Step 2 — Outcome envelope tag").
11
+ #
12
+ # Three invocation-outcome branches:
13
+ #
14
+ # * {TrapError} — Wasm engine layer (trap, OOM, unreachable, or a
15
+ # wire-violation fallback signalling a corrupted
16
+ # guest runtime).
17
+ # * {SandboxError} — sandbox / wire layer (mruby script error,
18
+ # wire-decode failure on an otherwise valid tag,
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.
32
+ #
33
+ # Subclasses pinned by docs/behavior.md Error Classes:
34
+ #
35
+ # * {ModuleNotBuiltError} < {SetupError} — Guest Binary artifact absent
36
+ # at +wasm_path+ (E-40).
37
+ # * {HandlerExhaustedError} < {SandboxError} — Handle id cap hit (B-21).
38
+
39
+ # Base for all kobako-raised errors so callers that want to ignore the
40
+ # taxonomy can rescue a single class.
41
+ class Error < StandardError; end
42
+
43
+ # Wasm engine layer. Raised when the Wasm execution engine crashed
44
+ # (trap, OOM, unreachable) or when the wire layer detected a structural
45
+ # violation that signals a corrupted guest execution environment
46
+ # (zero-length OUTCOME_BUFFER, unknown outcome tag — SPEC E-02 / E-03).
47
+ #
48
+ # Two named subclasses cover the configured per-invocation caps from B-01:
49
+ #
50
+ # * {TimeoutError} — wall-clock +timeout+ exceeded (E-19).
51
+ # * {MemoryLimitError} — guest +memory.grow+ would exceed
52
+ # +memory_limit+ (E-20).
53
+ #
54
+ # Host Apps that only care about "guest is unrecoverable, discard the
55
+ # Sandbox" can rescue +TrapError+ and ignore the subclass; Host Apps that
56
+ # want to surface a specific reason to operators can rescue the subclass
57
+ # first.
58
+ class TrapError < Error; end
59
+
60
+ # Wall-clock timeout cap exhausted. {docs/behavior.md E-19}[link:../../docs/behavior.md]:
61
+ # the absolute deadline +entry_time + timeout+ passed and the next guest
62
+ # wasm safepoint trapped. The Sandbox is unrecoverable after this point;
63
+ # discard and recreate before another execution.
64
+ class TimeoutError < TrapError; end
65
+
66
+ # Linear-memory cap exhausted. {docs/behavior.md E-20}[link:../../docs/behavior.md]:
67
+ # a guest +memory.grow+ would have pushed linear memory past the
68
+ # configured +memory_limit+. The Sandbox is unrecoverable after this
69
+ # point; discard and recreate before another execution.
70
+ class MemoryLimitError < TrapError; end
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
+
93
+ # Sandbox / wire layer. Raised when the guest ran to completion but
94
+ # execution failed due to a mruby script error, a protocol fault, or a
95
+ # host-side wire decode failure on an otherwise valid outcome tag.
96
+ class SandboxError < Error
97
+ attr_reader :origin, :klass, :backtrace_lines, :details
98
+
99
+ def initialize(message, origin: nil, klass: nil, backtrace_lines: nil, details: nil)
100
+ super(message)
101
+ @origin = origin
102
+ @klass = klass
103
+ @backtrace_lines = backtrace_lines
104
+ @details = details
105
+ end
106
+ end
107
+
108
+ # Service layer. Raised when a Service capability call inside a mruby
109
+ # script reported an application-level failure that the script did not
110
+ # rescue.
111
+ class ServiceError < Error
112
+ attr_reader :origin, :klass, :backtrace_lines, :details
113
+
114
+ def initialize(message, origin: nil, klass: nil, backtrace_lines: nil, details: nil)
115
+ super(message)
116
+ @origin = origin
117
+ @klass = klass
118
+ @backtrace_lines = backtrace_lines
119
+ @details = details
120
+ end
121
+ end
122
+
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
128
+
129
+ # docs/behavior.md Error Classes: BytecodeError is the SandboxError
130
+ # subclass raised when a `#preload(binary:)` snippet fails structural
131
+ # validation during the first invocation's snippet replay against a
132
+ # fresh `mrb_state` (E-37 RITE version mismatch, E-38 corrupt body).
133
+ # Bytecode that loads cleanly and then raises at top level is E-36
134
+ # and surfaces as plain `SandboxError` with the natural mruby class
135
+ # preserved. Inherits from SandboxError so a single
136
+ # `rescue Kobako::SandboxError` covers both source and bytecode
137
+ # snippet failures while callers wanting bytecode-specific handling
138
+ # can `rescue Kobako::BytecodeError` directly.
139
+ class BytecodeError < SandboxError; end
140
+ end
@@ -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
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kobako
4
+ # Wire-level value object for an ext-0x01 Capability Handle, used in both
5
+ # directions across the Sandbox boundary: as a Service method's return
6
+ # value (guest→host return path; {docs/behavior.md B-14}[link:../../docs/behavior.md])
7
+ # and as a +#run+ argument auto-wrapped by the host
8
+ # ({docs/behavior.md B-34}[link:../../docs/behavior.md]).
9
+ #
10
+ # SPEC pins the binary layout to fixext 4 with a 4-byte big-endian u32
11
+ # payload ({docs/wire-codec.md}[link:../../docs/wire-codec.md]
12
+ # § Ext Types → ext 0x01). ID 0 is reserved as the invalid sentinel;
13
+ # the maximum valid ID is 0x7fff_ffff (2^31 - 1).
14
+ #
15
+ # The constructor is internal to the Host Gem. +Kobako::Handle.new+ is
16
+ # privatised so Host App code cannot fabricate a Handle from a bare
17
+ # integer; legitimate Handle instances enter Host App code only as
18
+ # fields on raised error objects. The Host Gem itself constructs
19
+ # Handles through {.restore}, which exists at exactly two call
20
+ # sites: +Kobako::Codec::Factory#unpack_handle+ (wire decode) and
21
+ # +Kobako::Codec::Utils.deep_wrap+ / +Kobako::Transport::Dispatcher#wrap_as_handle+
22
+ # (allocator paths). Both live inside +lib/kobako/+ and are not part
23
+ # of any public surface.
24
+ #
25
+ # The mruby counterpart +Kobako::Handle+ lives inside the Wasm guest
26
+ # under the same canonical name and shares neither code nor instances
27
+ # with this host-side class.
28
+ class Handle < Data.define(:id)
29
+ # Inclusive lower bound on the wire Handle ID. ID 0 is reserved as
30
+ # the invalid sentinel and is never allocated.
31
+ MIN_ID = 1
32
+ # Inclusive upper bound on the wire Handle ID. The cap matches the
33
+ # u32 signed-positive range so Handle IDs fit in a signed integer
34
+ # on either side of the wire without re-encoding.
35
+ MAX_ID = 0x7fff_ffff
36
+
37
+ def initialize(id:)
38
+ raise ArgumentError, "Handle id must be Integer" unless id.is_a?(Integer)
39
+ raise ArgumentError, "Handle id #{id} out of range [#{MIN_ID}, #{MAX_ID}]" unless id.between?(MIN_ID, MAX_ID)
40
+
41
+ super
42
+ end
43
+
44
+ private_class_method :new
45
+
46
+ # Host Gem–internal factory. Allocates the Data instance through
47
+ # +Class#allocate+ and dispatches +#initialize+ explicitly so the
48
+ # invariant checks still run, while keeping the public +.new+
49
+ # privatised against Host App callers.
50
+ #
51
+ # Two collaborators call this: the codec when an ext 0x01 frame is
52
+ # decoded off the wire, and the allocator paths when a host-side
53
+ # Ruby object is registered into the Sandbox's +Catalog::Handles+. Both
54
+ # paths live inside +lib/kobako/+ and treat this method as a
55
+ # package-private constructor.
56
+ def self.restore(id)
57
+ allocate.tap { |handle| handle.send(:initialize, id: id) }
58
+ end
59
+ end
60
+ 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