kobako 0.2.1 → 0.3.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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/Cargo.lock +1 -1
  3. data/README.md +123 -57
  4. data/data/kobako.wasm +0 -0
  5. data/ext/kobako/Cargo.toml +2 -2
  6. data/ext/kobako/src/wasm/cache.rs +3 -3
  7. data/ext/kobako/src/wasm/dispatch.rs +87 -36
  8. data/ext/kobako/src/wasm/host_state.rs +189 -52
  9. data/ext/kobako/src/wasm/instance.rs +367 -152
  10. data/ext/kobako/src/wasm.rs +19 -5
  11. data/lib/kobako/capture.rb +12 -10
  12. data/lib/kobako/codec/decoder.rb +3 -2
  13. data/lib/kobako/codec/encoder.rb +1 -1
  14. data/lib/kobako/codec/error.rb +3 -2
  15. data/lib/kobako/codec/factory.rb +11 -7
  16. data/lib/kobako/codec/utils.rb +3 -2
  17. data/lib/kobako/codec.rb +2 -1
  18. data/lib/kobako/errors.rb +22 -10
  19. data/lib/kobako/invocation.rb +112 -0
  20. data/lib/kobako/outcome/panic.rb +2 -2
  21. data/lib/kobako/outcome.rb +20 -13
  22. data/lib/kobako/rpc/dispatcher.rb +9 -9
  23. data/lib/kobako/rpc/envelope.rb +3 -3
  24. data/lib/kobako/rpc/fault.rb +3 -2
  25. data/lib/kobako/rpc/handle.rb +3 -2
  26. data/lib/kobako/rpc/handle_table.rb +7 -7
  27. data/lib/kobako/rpc/namespace.rb +3 -3
  28. data/lib/kobako/rpc/server.rb +14 -12
  29. data/lib/kobako/sandbox.rb +147 -125
  30. data/lib/kobako/sandbox_options.rb +73 -0
  31. data/lib/kobako/snippet/binary.rb +30 -0
  32. data/lib/kobako/snippet/source.rb +28 -0
  33. data/lib/kobako/snippet/table.rb +174 -0
  34. data/lib/kobako/snippet.rb +20 -0
  35. data/lib/kobako/version.rb +1 -1
  36. data/sig/kobako/errors.rbs +3 -0
  37. data/sig/kobako/invocation.rbs +23 -0
  38. data/sig/kobako/sandbox.rbs +17 -18
  39. data/sig/kobako/sandbox_options.rbs +32 -0
  40. data/sig/kobako/snippet/binary.rbs +12 -0
  41. data/sig/kobako/snippet/source.rbs +13 -0
  42. data/sig/kobako/snippet/table.rbs +36 -0
  43. data/sig/kobako/snippet.rbs +4 -0
  44. data/sig/kobako/wasm.rbs +3 -1
  45. metadata +13 -1
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kobako
4
+ module Snippet
5
+ # Kobako::Snippet::Source — value object representing a single
6
+ # +#preload(code:, name:)+ entry held by +Kobako::Snippet::Table+
7
+ # ({docs/behavior.md B-32}[link:../../../docs/behavior.md]).
8
+ #
9
+ # +name+ is the canonical +Symbol+ identity baked into the loaded
10
+ # IREP's +debug_info+; backtrace frames originating in this snippet
11
+ # surface as +(snippet:Name):line+. +body+ is the UTF-8 mruby source
12
+ # detached from the caller's reference at +Table#register+ time so
13
+ # later mutation of the original String cannot bleed through.
14
+ #
15
+ # The class is a +Data.define+ subclass — frozen, value-equal, and
16
+ # carries no mutation API. Callers (chiefly +Table+) construct
17
+ # instances via keyword form +Source.new(name: ..., body: ...)+.
18
+ # Wire-form construction is the +Table+'s responsibility, mirroring
19
+ # +Kobako::RPC.encode_request+'s pattern of reading attributes off a
20
+ # carrier rather than asking the carrier to self-describe.
21
+ class Source < Data.define(:name, :body)
22
+ # The +kind+ field value carried by source snippets in their Frame
23
+ # 3 wire envelope entry
24
+ # ({docs/wire-codec.md Invocation channels}[link:../../../docs/wire-codec.md]).
25
+ KIND = "source"
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,174 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "msgpack"
4
+
5
+ require_relative "binary"
6
+ require_relative "source"
7
+
8
+ module Kobako
9
+ module Snippet
10
+ # Kobako::Snippet::Table — per-Sandbox insertion-ordered registry of
11
+ # preloaded snippets
12
+ # ({docs/behavior.md B-32 / B-33}[link:../../../docs/behavior.md]).
13
+ #
14
+ # Entries replay against the fresh +mrb_state+ before per-invocation
15
+ # source / entrypoint resolution. Each +Source+ entry's +name+ is its
16
+ # canonical identity — the filename baked into the loaded IREP's
17
+ # +debug_info+ that surfaces in every backtrace frame originating
18
+ # from the snippet as +(snippet:Name):line+. Duplicate names within
19
+ # the +code:+ form would produce ambiguous attribution and are
20
+ # rejected at registration time
21
+ # ({docs/behavior.md E-33}[link:../../../docs/behavior.md]).
22
+ # +Binary+ entries carry no host-side name — their canonical name
23
+ # lives in the bytecode's +debug_info+ and is read by the guest at
24
+ # load time; the host does not extract it.
25
+ #
26
+ # Sealing (B-33) is governed by the owning Sandbox — the table itself
27
+ # is append-only and exposes no mutation API beyond +#register+; the
28
+ # Sandbox guards +#register+ behind the seal check before delegating.
29
+ class Table
30
+ # Ruby constant-name pattern enforced on snippet names
31
+ # ({docs/behavior.md E-34}[link:../../../docs/behavior.md]).
32
+ NAME_PATTERN = /\A[A-Z]\w*\z/
33
+
34
+ def initialize
35
+ @entries = [] # : Array[Kobako::Snippet::Source | Kobako::Snippet::Binary]
36
+ end
37
+
38
+ # Serialize the registered snippets to wire bytes. Each entry
39
+ # contributes a msgpack map shape; the collection rides as a single
40
+ # msgpack array. An empty table serializes to an empty array, never
41
+ # absent. The wire codec is an implementation detail — callers
42
+ # receive a binary +String+ that the +Kobako::Wasm+ layer ships
43
+ # through the invocation channel. Mirrors the
44
+ # +Kobako::RPC.encode_request+ pattern: entry value objects stay
45
+ # pure carriers, this method reads their attributes externally.
46
+ def encode
47
+ MessagePack.pack(@entries.map { |entry| entry_payload(entry) })
48
+ end
49
+
50
+ # Register one preloaded snippet in either of two forms
51
+ # ({docs/behavior.md B-32}[link:../../../docs/behavior.md]).
52
+ #
53
+ # * Source form +register(code: src, name: Name)+ — +src+ is the
54
+ # mruby source as a String; the bytes are re-encoded as UTF-8
55
+ # and detached from the caller's reference. +name+ is a Symbol
56
+ # or String matching +NAME_PATTERN+. Returns the Symbol form
57
+ # of +name+.
58
+ # * Binary form +register(binary: bytes)+ — +bytes+ is
59
+ # precompiled RITE bytecode as a String, duplicated and forced
60
+ # to ASCII-8BIT so msgpack-ruby ships it as +bin+. Returns
61
+ # +nil+ — bytecode entries are anonymous on the host side; any
62
+ # structural validation
63
+ # ({docs/behavior.md E-37 / E-38}[link:../../../docs/behavior.md])
64
+ # is deferred to the guest at first replay.
65
+ #
66
+ # The two forms are mutually exclusive: shape validation lives
67
+ # here so callers (chiefly +Kobako::Sandbox#preload+) collapse to
68
+ # a single delegation. Raises +ArgumentError+ on mixed forms,
69
+ # missing keywords, wrong types, malformed +name+ (E-34), or
70
+ # duplicate +code:+ +name+ (E-33).
71
+ def register(code: nil, name: nil, binary: nil)
72
+ if binary
73
+ raise ArgumentError, "cannot combine binary: with code: / name:" if code || name
74
+
75
+ register_binary!(binary)
76
+ else
77
+ register_source!(code, name)
78
+ end
79
+ end
80
+
81
+ # Iterate over registered entries in insertion order. Yields each
82
+ # entry (a +Kobako::Snippet::Source+ or +Kobako::Snippet::Binary+).
83
+ # Returns an Enumerator when no block is given.
84
+ def each(&)
85
+ @entries.each(&)
86
+ end
87
+
88
+ # Canonical names of every registered +Source+ entry, in insertion
89
+ # order. +Binary+ entries are skipped — their names live in
90
+ # bytecode +debug_info+ on the guest side and are not extracted by
91
+ # the host.
92
+ def names
93
+ @entries.filter_map { |entry| entry.name if entry.is_a?(Source) }
94
+ end
95
+
96
+ # Number of registered snippets.
97
+ def size
98
+ @entries.size
99
+ end
100
+
101
+ # Whether no snippets are registered.
102
+ def empty?
103
+ @entries.empty?
104
+ end
105
+
106
+ private
107
+
108
+ # Source-form register path. Delegates argument-shape checks to
109
+ # +ensure_source_args!+ (which returns the narrowed
110
+ # +[code, name]+ pair), normalises +name+ to a Symbol, rejects
111
+ # duplicates (E-33), and appends the Source entry.
112
+ def register_source!(code, name)
113
+ code, name = ensure_source_args!(code, name)
114
+ name_sym = normalize_name(name)
115
+ raise ArgumentError, "snippet #{name_sym.inspect} already preloaded" if names.include?(name_sym)
116
+
117
+ @entries << Source.new(name: name_sym, body: code.dup.force_encoding(Encoding::UTF_8))
118
+ name_sym
119
+ end
120
+
121
+ # Shape-only validation for the +code:+ + +name:+ pair. Returns
122
+ # the pair with +nil+ narrowed away so callers can treat both as
123
+ # present. The +code:+ type check runs before the +name:+
124
+ # presence check so callers passing +code: nil+ explicitly see
125
+ # the type error rather than the "missing keyword" error.
126
+ def ensure_source_args!(code, name)
127
+ raise ArgumentError, "missing keyword: code: + name:, or binary:" if code.nil? && name.nil?
128
+ raise ArgumentError, "code must be a String, got #{code.class}" unless code.is_a?(String)
129
+ raise ArgumentError, "missing keyword: name:" if name.nil?
130
+
131
+ [code, name]
132
+ end
133
+
134
+ # Binary-form register path. Validates the +binary:+ payload
135
+ # type and appends the Binary entry. The bytes are duplicated and
136
+ # forced to ASCII-8BIT so msgpack-ruby picks the +bin+ family on
137
+ # the wire.
138
+ def register_binary!(bytes)
139
+ raise ArgumentError, "binary must be a String, got #{bytes.class}" unless bytes.is_a?(String)
140
+
141
+ @entries << Binary.new(body: bytes.dup.force_encoding(Encoding::ASCII_8BIT))
142
+ nil
143
+ end
144
+
145
+ # Build the msgpack-ready Hash for one entry. Source entries
146
+ # contribute their host-side +name+; Binary entries omit it
147
+ # because the canonical name lives in the bytecode's embedded
148
+ # +debug_info+ and is read by the guest at load time
149
+ # ({docs/wire-codec.md Invocation channels}[link:../../../docs/wire-codec.md]).
150
+ def entry_payload(entry)
151
+ case entry
152
+ when Source
153
+ { "name" => entry.name.to_s, "kind" => Source::KIND, "body" => entry.body }
154
+ when Binary
155
+ { "kind" => Binary::KIND, "body" => entry.body }
156
+ end
157
+ end
158
+
159
+ def normalize_name(name)
160
+ unless name.is_a?(Symbol) || name.is_a?(String)
161
+ raise ArgumentError, "snippet name must be a Symbol or String, got #{name.class}"
162
+ end
163
+
164
+ name_str = name.to_s
165
+ unless NAME_PATTERN.match?(name_str)
166
+ raise ArgumentError,
167
+ "snippet name must match #{NAME_PATTERN.inspect} (got #{name.inspect})"
168
+ end
169
+
170
+ name_str.to_sym
171
+ end
172
+ end
173
+ end
174
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "snippet/binary"
4
+ require_relative "snippet/source"
5
+ require_relative "snippet/table"
6
+
7
+ module Kobako
8
+ # Kobako::Snippet — namespace for the per-Sandbox preloaded snippet
9
+ # registry and its entry value objects
10
+ # ({docs/behavior.md B-32 / B-33}[link:../../docs/behavior.md]).
11
+ #
12
+ # The +Table+ owns insertion-ordered storage and seal-coordination with
13
+ # the owning Sandbox; +Source+ is the value object representing a single
14
+ # +#preload(code:, name:)+ entry. Entry types live as siblings under
15
+ # this module rather than nested under +Table+ so they remain plain
16
+ # value objects with no implicit dependency on the registry that holds
17
+ # them.
18
+ module Snippet
19
+ end
20
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Kobako
4
- VERSION = "0.2.1"
4
+ VERSION = "0.3.0"
5
5
  end
@@ -49,4 +49,7 @@ module Kobako
49
49
 
50
50
  class HandleTableExhausted < HandleTableError
51
51
  end
52
+
53
+ class BytecodeError < SandboxError
54
+ end
52
55
  end
@@ -0,0 +1,23 @@
1
+ module Kobako
2
+ class Invocation < Data
3
+ NAME_PATTERN: Regexp
4
+
5
+ attr_reader entrypoint: Symbol
6
+ attr_reader args: Array[untyped]
7
+ attr_reader kwargs: Hash[Symbol, untyped]
8
+
9
+ def self.new: (entrypoint: Symbol | String, ?args: Array[untyped], ?kwargs: Hash[untyped, untyped]) -> Invocation
10
+
11
+ def initialize: (entrypoint: Symbol | String, ?args: Array[untyped], ?kwargs: Hash[untyped, untyped]) -> void
12
+
13
+ def encode: () -> String
14
+
15
+ private
16
+
17
+ def normalize_entrypoint: (Symbol | String target) -> Symbol
18
+
19
+ def validate_args!: (Array[untyped] args) -> Array[untyped]
20
+
21
+ def validate_kwargs!: (Hash[untyped, untyped] kwargs) -> Hash[Symbol, untyped]
22
+ end
23
+ end
@@ -1,18 +1,18 @@
1
1
  module Kobako
2
2
  class Sandbox
3
- DEFAULT_OUTPUT_LIMIT: Integer
4
-
5
- DEFAULT_TIMEOUT_SECONDS: Float
6
-
7
- DEFAULT_MEMORY_LIMIT: Integer
3
+ extend Forwardable
8
4
 
9
5
  attr_reader wasm_path: String
10
6
  attr_reader instance: Kobako::Wasm::Instance
11
- attr_reader stdout_limit: Integer
12
- attr_reader stderr_limit: Integer
13
- attr_reader timeout: Float?
14
- attr_reader memory_limit: Integer?
7
+ attr_reader options: Kobako::SandboxOptions
15
8
  attr_reader services: Kobako::RPC::Server
9
+ attr_reader snippets: Kobako::Snippet::Table
10
+
11
+ # Forwarded to @options via Forwardable#def_delegators.
12
+ def timeout: () -> Float?
13
+ def memory_limit: () -> Integer?
14
+ def stdout_limit: () -> Integer
15
+ def stderr_limit: () -> Integer
16
16
 
17
17
  def initialize: (
18
18
  ?wasm_path: String?,
@@ -32,22 +32,21 @@ module Kobako
32
32
 
33
33
  def define: (Symbol | String name) -> Kobako::RPC::Namespace
34
34
 
35
- def run: (String source) -> untyped
35
+ def preload: (code: String, name: Symbol | String) -> Kobako::Sandbox
36
+ | (binary: String) -> Kobako::Sandbox
36
37
 
37
- private
38
+ def run: (Symbol | String target, *untyped args, **untyped kwargs) -> untyped
38
39
 
39
- def clear_captures!: () -> void
40
+ def eval: (String code) -> untyped
40
41
 
41
- def normalize_timeout: ((Float | Integer)? timeout) -> Float?
42
+ private
42
43
 
43
- def normalize_memory_limit: (Integer? memory_limit) -> Integer?
44
+ def clear_captures!: () -> void
44
45
 
45
- def reset_run_state!: () -> void
46
+ def begin_invocation!: () -> void
46
47
 
47
48
  def read_captures!: () -> void
48
49
 
49
- def run_guest: (String preamble, String source) -> void
50
-
51
- def take_result!: () -> untyped
50
+ def invoke!: (Symbol verb) { () -> void } -> untyped
52
51
  end
53
52
  end
@@ -0,0 +1,32 @@
1
+ module Kobako
2
+ class SandboxOptions < Data
3
+ DEFAULT_TIMEOUT_SECONDS: Float
4
+ DEFAULT_MEMORY_LIMIT: Integer
5
+ DEFAULT_OUTPUT_LIMIT: Integer
6
+
7
+ attr_reader timeout: Float?
8
+ attr_reader memory_limit: Integer?
9
+ attr_reader stdout_limit: Integer
10
+ attr_reader stderr_limit: Integer
11
+
12
+ def self.new: (
13
+ ?timeout: (Float | Integer)?,
14
+ ?memory_limit: Integer?,
15
+ ?stdout_limit: Integer?,
16
+ ?stderr_limit: Integer?
17
+ ) -> SandboxOptions
18
+
19
+ def initialize: (
20
+ ?timeout: (Float | Integer)?,
21
+ ?memory_limit: Integer?,
22
+ ?stdout_limit: Integer?,
23
+ ?stderr_limit: Integer?
24
+ ) -> void
25
+
26
+ private
27
+
28
+ def normalize_timeout: ((Float | Integer)? timeout) -> Float?
29
+
30
+ def normalize_memory_limit: (Integer? memory_limit) -> Integer?
31
+ end
32
+ end
@@ -0,0 +1,12 @@
1
+ module Kobako
2
+ module Snippet
3
+ class Binary < Data
4
+ KIND: String
5
+
6
+ attr_reader body: String
7
+
8
+ def self.new: (body: String) -> Binary
9
+ | (String body) -> Binary
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,13 @@
1
+ module Kobako
2
+ module Snippet
3
+ class Source < Data
4
+ KIND: String
5
+
6
+ attr_reader name: Symbol
7
+ attr_reader body: String
8
+
9
+ def self.new: (name: Symbol, body: String) -> Source
10
+ | (Symbol name, String body) -> Source
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,36 @@
1
+ module Kobako
2
+ module Snippet
3
+ class Table
4
+ NAME_PATTERN: Regexp
5
+
6
+ type entry = Kobako::Snippet::Source | Kobako::Snippet::Binary
7
+
8
+ def initialize: () -> void
9
+
10
+ def register: (?code: String?, ?name: (Symbol | String)?, ?binary: String?) -> (Symbol | nil)
11
+
12
+ def encode: () -> String
13
+
14
+ def each: () { (entry) -> void } -> Array[entry]
15
+ | () -> Enumerator[entry, Array[entry]]
16
+
17
+ def names: () -> Array[Symbol]
18
+
19
+ def size: () -> Integer
20
+
21
+ def empty?: () -> bool
22
+
23
+ private
24
+
25
+ def register_source!: (String? code, (Symbol | String)? name) -> Symbol
26
+
27
+ def ensure_source_args!: (String? code, (Symbol | String)? name) -> [String, Symbol | String]
28
+
29
+ def register_binary!: (String bytes) -> nil
30
+
31
+ def entry_payload: (entry) -> Hash[String, untyped]
32
+
33
+ def normalize_name: (Symbol | String name) -> Symbol
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,4 @@
1
+ module Kobako
2
+ module Snippet
3
+ end
4
+ end
data/sig/kobako/wasm.rbs CHANGED
@@ -25,7 +25,9 @@ module Kobako
25
25
 
26
26
  def server=: (Kobako::RPC::Server server) -> void
27
27
 
28
- def run: (String preamble, String source) -> void
28
+ def eval: (String preamble, String source, String snippets) -> void
29
+
30
+ def run: (String preamble, String snippets, String envelope) -> void
29
31
 
30
32
  def stdout: () -> [String, bool]
31
33
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kobako
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Aotokitsuruya
@@ -69,6 +69,7 @@ files:
69
69
  - lib/kobako/codec/factory.rb
70
70
  - lib/kobako/codec/utils.rb
71
71
  - lib/kobako/errors.rb
72
+ - lib/kobako/invocation.rb
72
73
  - lib/kobako/outcome.rb
73
74
  - lib/kobako/outcome/panic.rb
74
75
  - lib/kobako/rpc.rb
@@ -80,6 +81,11 @@ files:
80
81
  - lib/kobako/rpc/namespace.rb
81
82
  - lib/kobako/rpc/server.rb
82
83
  - lib/kobako/sandbox.rb
84
+ - lib/kobako/sandbox_options.rb
85
+ - lib/kobako/snippet.rb
86
+ - lib/kobako/snippet/binary.rb
87
+ - lib/kobako/snippet/source.rb
88
+ - lib/kobako/snippet/table.rb
83
89
  - lib/kobako/version.rb
84
90
  - lib/kobako/wasm.rb
85
91
  - sig/kobako.rbs
@@ -90,6 +96,7 @@ files:
90
96
  - sig/kobako/codec/factory.rbs
91
97
  - sig/kobako/codec/utils.rbs
92
98
  - sig/kobako/errors.rbs
99
+ - sig/kobako/invocation.rbs
93
100
  - sig/kobako/outcome.rbs
94
101
  - sig/kobako/outcome/panic.rbs
95
102
  - sig/kobako/rpc.rbs
@@ -101,6 +108,11 @@ files:
101
108
  - sig/kobako/rpc/namespace.rbs
102
109
  - sig/kobako/rpc/server.rbs
103
110
  - sig/kobako/sandbox.rbs
111
+ - sig/kobako/sandbox_options.rbs
112
+ - sig/kobako/snippet.rbs
113
+ - sig/kobako/snippet/binary.rbs
114
+ - sig/kobako/snippet/source.rbs
115
+ - sig/kobako/snippet/table.rbs
104
116
  - sig/kobako/wasm.rbs
105
117
  homepage: https://github.com/elct9620/kobako
106
118
  licenses: