kobako 0.5.0-x86_64-linux

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