kobako 0.19.0-aarch64-linux → 0.20.0-aarch64-linux
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +20 -0
- data/crates/kobako-runtime/CHANGELOG.md +7 -0
- data/crates/kobako-runtime/README.md +1 -1
- data/crates/kobako-wasmtime/CHANGELOG.md +7 -0
- data/crates/kobako-wasmtime/README.md +1 -1
- data/data/kobako.wasm +0 -0
- data/lib/kobako/3.3/kobako.so +0 -0
- data/lib/kobako/3.4/kobako.so +0 -0
- data/lib/kobako/4.0/kobako.so +0 -0
- data/lib/kobako/catalog/extensions.rb +50 -43
- data/lib/kobako/catalog/handles.rb +7 -16
- data/lib/kobako/catalog/services.rb +22 -36
- data/lib/kobako/catalog/snippets.rb +11 -10
- data/lib/kobako/context.rb +202 -0
- data/lib/kobako/errors.rb +26 -4
- data/lib/kobako/execution.rb +52 -0
- data/lib/kobako/extension.rb +24 -10
- data/lib/kobako/pool.rb +1 -7
- data/lib/kobako/sandbox.rb +43 -182
- data/lib/kobako/sandbox_options.rb +23 -2
- data/lib/kobako/transport/dispatcher.rb +16 -15
- data/lib/kobako/unresolved.rb +17 -0
- data/lib/kobako/usage.rb +6 -6
- data/lib/kobako/version.rb +1 -1
- data/lib/kobako.rb +1 -0
- data/sig/kobako/catalog/extensions.rbs +4 -6
- data/sig/kobako/catalog/handles.rbs +0 -2
- data/sig/kobako/catalog/services.rbs +3 -3
- data/sig/kobako/catalog/snippets.rbs +2 -0
- data/sig/kobako/context.rbs +36 -0
- data/sig/kobako/errors.rbs +12 -3
- data/sig/kobako/execution.rbs +24 -0
- data/sig/kobako/extension.rbs +4 -2
- data/sig/kobako/runtime.rbs +37 -9
- data/sig/kobako/sandbox.rbs +5 -24
- data/sig/kobako/sandbox_options.rbs +9 -2
- data/sig/kobako/transport/dispatcher.rbs +3 -3
- data/sig/kobako/unresolved.rbs +4 -0
- metadata +8 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b09a439fa6c5bfc4598a565bc0ec6cfd026c65c48a39e9c9c0ae73c53ffb00d9
|
|
4
|
+
data.tar.gz: 0b1a09a6af7b5eabf918667c9efcc434952d693322fda2f71cf0195df174d21a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b5475e836c30518290c9b94eab8c0fe92c222efea64c93d909a099d50612e674a75733777fd3c37e884b7564dbc40fa46a3f90a04a26c9be40aa5d54dc548f1f
|
|
7
|
+
data.tar.gz: d76951bea7e5eae63d2c79052dcd3d2e03204afa738020f958139a7b293ae55624d94b8e11a6aa1a124f5b9094b281852e4fca6042c6dbf5d797e29dac874fc5
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{".":"0.
|
|
1
|
+
{".":"0.20.0","wasm/kobako-core":"0.12.0","wasm/kobako-mruby":"0.12.0","wasm/kobako-io":"0.12.0","wasm/kobako-json":"0.12.0","wasm/kobako-regexp":"0.12.0","wasm/kobako-baker":"0.12.0","crates/kobako-codec":"0.12.0","crates/kobako-runtime":"0.12.0","crates/kobako-wasmtime":"0.12.0","crates/kobako":"0.12.0"}
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.20.0](https://github.com/elct9620/kobako/compare/v0.19.0...v0.20.0) (2026-07-24)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **bench:** mark a run in progress with a pid lock file ([3ca85fb](https://github.com/elct9620/kobako/commit/3ca85fbe9860ce32d58c1a5bf4ff85cbdc210780))
|
|
9
|
+
* **execution:** tell a nil-value success from a failure with #failed? ([2f3d382](https://github.com/elct9620/kobako/commit/2f3d382b24d65fabda7771429cd3adfc90a8c7e8))
|
|
10
|
+
* **extension:** declare a backend's kind by explicit keyword ([3f6c628](https://github.com/elct9620/kobako/commit/3f6c6283080ba4346ac8ef878e469dab8039e650))
|
|
11
|
+
* **sandbox:** add the gvl: hold/release scheduling option ([ca2a884](https://github.com/elct9620/kobako/commit/ca2a884415b3271c44b20d131536b3fe7c5a3077))
|
|
12
|
+
* **sandbox:** declare fillable Service paths defaulting to Kobako::Unresolved ([1d60f76](https://github.com/elct9620/kobako/commit/1d60f764ddff3b5b10206993e2b281c838ec0022))
|
|
13
|
+
* **sandbox:** fill and override declared paths per invocation with ctx.bind ([629ef7d](https://github.com/elct9620/kobako/commit/629ef7dfa35273c952685fefb4c5793b11886aee))
|
|
14
|
+
* **sandbox:** return a frozen Kobako::Invocation from #eval and #run ([e9f4634](https://github.com/elct9620/kobako/commit/e9f463403bea26fad32e52d560b979684f555ed1))
|
|
15
|
+
* **tasks:** gate the wasmtime driver against a magnus dependency ([268c3f6](https://github.com/elct9620/kobako/commit/268c3f65f53d64b465458bca5c0c9b0d0467df44))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Bug Fixes
|
|
19
|
+
|
|
20
|
+
* **context:** raise ArgumentError for a spent ctx.bind ([2783bba](https://github.com/elct9620/kobako/commit/2783bba20f139554178389d4c860e0c280cab550))
|
|
21
|
+
* **extension:** re-assert dependencies after a failed seal ([72839ba](https://github.com/elct9620/kobako/commit/72839bac6186e81a1d4323bd3f1eb4f10abf3d64))
|
|
22
|
+
|
|
3
23
|
## [0.19.0](https://github.com/elct9620/kobako/compare/v0.18.0...v0.19.0) (2026-07-19)
|
|
4
24
|
|
|
5
25
|
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.12.0](https://github.com/elct9620/kobako/compare/kobako-runtime-v0.11.0...kobako-runtime-v0.12.0) (2026-07-24)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Miscellaneous Chores
|
|
7
|
+
|
|
8
|
+
* **kobako-runtime:** Synchronize kobako crates versions
|
|
9
|
+
|
|
3
10
|
## [0.11.0](https://github.com/elct9620/kobako/compare/kobako-runtime-v0.10.2...kobako-runtime-v0.11.0) (2026-07-19)
|
|
4
11
|
|
|
5
12
|
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.12.0](https://github.com/elct9620/kobako/compare/kobako-wasmtime-v0.11.0...kobako-wasmtime-v0.12.0) (2026-07-24)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Miscellaneous Chores
|
|
7
|
+
|
|
8
|
+
* **kobako-wasmtime:** Synchronize kobako crates versions
|
|
9
|
+
|
|
3
10
|
## [0.11.0](https://github.com/elct9620/kobako/compare/kobako-wasmtime-v0.10.2...kobako-wasmtime-v0.11.0) (2026-07-19)
|
|
4
11
|
|
|
5
12
|
|
data/data/kobako.wasm
CHANGED
|
Binary file
|
data/lib/kobako/3.3/kobako.so
CHANGED
|
Binary file
|
data/lib/kobako/3.4/kobako.so
CHANGED
|
Binary file
|
data/lib/kobako/4.0/kobako.so
CHANGED
|
Binary file
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../unresolved"
|
|
4
|
+
|
|
3
5
|
module Kobako
|
|
4
6
|
module Catalog
|
|
5
7
|
# Kobako::Catalog::Extensions — per-Sandbox registry of installed
|
|
@@ -7,7 +9,9 @@ module Kobako
|
|
|
7
9
|
# install time (its +source+ into +Catalog::Snippets+, its +backend+
|
|
8
10
|
# path into +Catalog::Services+), asserts declared dependencies are
|
|
9
11
|
# present when the Sandbox seals, and resolves each callable-backed
|
|
10
|
-
# path to a fresh object
|
|
12
|
+
# path to a fresh object for every invocation — returning that map to the
|
|
13
|
+
# driving +Context+ rather than mutating the shared registry, so
|
|
14
|
+
# concurrent invocations stay shared-nothing.
|
|
11
15
|
#
|
|
12
16
|
# Sealing and the reject-after-install guard are governed by the owning
|
|
13
17
|
# Sandbox through +Catalog::Services#sealed?+, the shared seal signal;
|
|
@@ -21,77 +25,87 @@ module Kobako
|
|
|
21
25
|
|
|
22
26
|
# Install +extension+: validate its shape, register its +source+ as a
|
|
23
27
|
# preloaded snippet, and — when it carries a +backend+ — reserve the
|
|
24
|
-
# backend path in +services+. A
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
# +
|
|
29
|
-
#
|
|
30
|
-
# +
|
|
31
|
-
#
|
|
28
|
+
# backend path in +services+. A static +object:+ binds directly; a
|
|
29
|
+
# per-invocation +provider:+ or a fillable backend reserves the path with
|
|
30
|
+
# +Kobako::Unresolved+ (the per-invocation result is layered onto the
|
|
31
|
+
# Context, a fillable stays Unresolved until filled). Raises
|
|
32
|
+
# +ArgumentError+ for the two shapes +#validate!+ owns — +source+ not a
|
|
33
|
+
# String, or a present +backend+ that omits +path+ / +object+ /
|
|
34
|
+
# +provider+. The Extension readers are duck-typed, so an object missing
|
|
35
|
+
# them surfaces the underlying +NoMethodError+, and a malformed +name+ or
|
|
36
|
+
# +backend.path+ surfaces through the +snippets+ / +services+
|
|
37
|
+
# registration it routes to.
|
|
32
38
|
def install(extension, snippets:, services:)
|
|
33
39
|
validate!(extension)
|
|
34
40
|
snippets.register(code: extension.source, name: extension.name)
|
|
35
41
|
backend = extension.backend
|
|
36
|
-
services.bind(backend.path,
|
|
42
|
+
services.bind(backend.path, install_object(backend)) if backend
|
|
37
43
|
@entries << extension
|
|
38
44
|
self
|
|
39
45
|
end
|
|
40
46
|
|
|
41
47
|
# Assert every installed Extension's +depends_on+ names a fellow
|
|
42
|
-
# installed Extension. Runs once, when the Sandbox first seals
|
|
43
|
-
# unmet dependency raises +ArgumentError+ naming the
|
|
44
|
-
# guest runs.
|
|
48
|
+
# installed Extension. Runs once, when the Sandbox first seals
|
|
49
|
+
# successfully; an unmet dependency raises +ArgumentError+ naming the
|
|
50
|
+
# gap, before the guest runs. The asserted flag flips only on success,
|
|
51
|
+
# so a seal that failed re-checks on the next attempt rather than
|
|
52
|
+
# silently passing a broken Sandbox. Idempotent across later invocations.
|
|
45
53
|
def seal!
|
|
46
54
|
return self if @asserted
|
|
47
55
|
|
|
48
|
-
@asserted = true
|
|
49
56
|
assert_dependencies!
|
|
57
|
+
@asserted = true
|
|
50
58
|
self
|
|
51
59
|
end
|
|
52
60
|
|
|
53
|
-
# Resolve each
|
|
54
|
-
#
|
|
55
|
-
# providers yield distinct objects; one
|
|
56
|
-
# Extensions is invoked once and its result
|
|
57
|
-
# identity is resource identity.
|
|
58
|
-
# they stay the object bound at install
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
resolved
|
|
62
|
-
|
|
63
|
-
|
|
61
|
+
# Resolve each +provider:+-backed path to this invocation's object and
|
|
62
|
+
# return the +path => object+ map the driving +Context+ layers over the
|
|
63
|
+
# static base bindings. Distinct providers yield distinct objects; one
|
|
64
|
+
# provider shared by several Extensions is invoked once and its result
|
|
65
|
+
# shared, so provider identity is resource identity. Static and fillable
|
|
66
|
+
# backends are omitted — they stay the object bound at install in the
|
|
67
|
+
# base registry (+Kobako::Unresolved+ for a fillable).
|
|
68
|
+
def resolve
|
|
69
|
+
resolved = {} # : Hash[String, untyped]
|
|
70
|
+
by_provider = {} # : Hash[untyped, untyped]
|
|
71
|
+
by_provider.compare_by_identity
|
|
72
|
+
@entries.each { |extension| resolve_backend(extension, resolved, by_provider) }
|
|
73
|
+
resolved
|
|
64
74
|
end
|
|
65
75
|
|
|
66
76
|
private
|
|
67
77
|
|
|
68
|
-
#
|
|
69
|
-
#
|
|
70
|
-
#
|
|
71
|
-
|
|
72
|
-
|
|
78
|
+
# The object a backend binds at install: a static +object:+ directly, or
|
|
79
|
+
# +Kobako::Unresolved+ as the placeholder for a per-invocation +provider:+
|
|
80
|
+
# (layered onto the Context) or a fillable (neither keyword).
|
|
81
|
+
def install_object(backend) = backend.object.nil? ? Kobako::Unresolved : backend.object
|
|
82
|
+
|
|
83
|
+
# Resolve one Extension's +provider:+-backed path against the shared
|
|
84
|
+
# per-invocation identity cache (keyed by provider identity) and record
|
|
85
|
+
# it in +resolved+. A static or fillable backend is skipped — its object
|
|
86
|
+
# stays as bound at install in the base registry.
|
|
87
|
+
def resolve_backend(extension, resolved, by_provider)
|
|
73
88
|
backend = extension.backend
|
|
74
89
|
return unless backend
|
|
75
90
|
|
|
76
91
|
provider = backend.provider
|
|
77
|
-
return
|
|
92
|
+
return if provider.nil?
|
|
78
93
|
|
|
79
|
-
|
|
80
|
-
services.refresh(backend.path, object)
|
|
94
|
+
resolved[backend.path.to_s] = by_provider.fetch(provider) { by_provider[provider] = provider.call }
|
|
81
95
|
end
|
|
82
96
|
|
|
83
97
|
# Enforce the Extension-shape checks +#preload+ / +#bind+ do not: a
|
|
84
98
|
# mandatory String +source+ (the install/bind boundary) and a
|
|
85
|
-
# +backend+ that, when present, exposes +path+
|
|
99
|
+
# +backend+ that, when present, exposes +path+ / +object+ / +provider+.
|
|
86
100
|
def validate!(extension)
|
|
87
101
|
source = extension.source
|
|
88
102
|
raise ArgumentError, "Extension #source must be a String, got #{source.class}" unless source.is_a?(String)
|
|
89
103
|
|
|
90
104
|
backend = extension.backend
|
|
91
105
|
return if backend.nil?
|
|
92
|
-
return if backend.respond_to?(:path) && backend.respond_to?(:provider)
|
|
106
|
+
return if backend.respond_to?(:path) && backend.respond_to?(:object) && backend.respond_to?(:provider)
|
|
93
107
|
|
|
94
|
-
raise ArgumentError, "Extension #backend must expose #path and #provider"
|
|
108
|
+
raise ArgumentError, "Extension #backend must expose #path, #object, and #provider"
|
|
95
109
|
end
|
|
96
110
|
|
|
97
111
|
def assert_dependencies!
|
|
@@ -110,13 +124,6 @@ module Kobako
|
|
|
110
124
|
# of a constant token are interchangeable; a value that is neither
|
|
111
125
|
# falls through unchanged to the not-installed path.
|
|
112
126
|
def symbolize(name) = name.is_a?(String) ? name.to_sym : name
|
|
113
|
-
|
|
114
|
-
def callable?(provider) = provider.respond_to?(:call)
|
|
115
|
-
|
|
116
|
-
# A callable provider reserves its path with a placeholder that the
|
|
117
|
-
# per-invocation refresh replaces before any dispatch; a fixed
|
|
118
|
-
# provider is the bound object itself.
|
|
119
|
-
def initial_object(provider) = callable?(provider) ? nil : provider
|
|
120
127
|
end
|
|
121
128
|
end
|
|
122
129
|
end
|
|
@@ -5,10 +5,10 @@ require_relative "../handle"
|
|
|
5
5
|
module Kobako
|
|
6
6
|
module Catalog
|
|
7
7
|
# Host-side mapping from opaque integer Handle IDs to Ruby objects.
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
8
|
+
# Each invocation's +Kobako::Context+ mints its own table and hands it to
|
|
9
|
+
# the dispatcher, so guest→host dispatch resolves Handle targets and
|
|
10
|
+
# arguments against the same table that host→guest wire encoding allocates
|
|
11
|
+
# into for that run.
|
|
12
12
|
#
|
|
13
13
|
# Lifecycle invariants:
|
|
14
14
|
#
|
|
@@ -17,10 +17,9 @@ module Kobako
|
|
|
17
17
|
# invocation is 1; ID 0 is reserved as the invalid sentinel and is
|
|
18
18
|
# never returned by +#alloc+.
|
|
19
19
|
#
|
|
20
|
-
# -
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
# argument).
|
|
20
|
+
# - A fresh table backs each invocation, so a Handle issued in one
|
|
21
|
+
# invocation resolves in no other — uniformly regardless of
|
|
22
|
+
# allocation source (Service return or host-injected argument).
|
|
24
23
|
#
|
|
25
24
|
# - The cap is +0x7fff_ffff+ (2³¹ − 1). Allocation beyond the cap
|
|
26
25
|
# raises immediately — no silent truncation, no wrap, no ID reuse.
|
|
@@ -63,14 +62,6 @@ module Kobako
|
|
|
63
62
|
@entries[id]
|
|
64
63
|
end
|
|
65
64
|
|
|
66
|
-
# Clear all entries AND reset the counter to 1. Called at the
|
|
67
|
-
# per-invocation boundary by +Kobako::Sandbox+. Returns +self+.
|
|
68
|
-
def reset!
|
|
69
|
-
@entries.clear
|
|
70
|
-
@next_id = 1
|
|
71
|
-
self
|
|
72
|
-
end
|
|
73
|
-
|
|
74
65
|
# Number of currently-bound entries. Used by tests of the Dispatcher
|
|
75
66
|
# and Codec::HandleWalk#deep_wrap to observe whether each path allocates
|
|
76
67
|
# exactly the Handle entries it should — the +Handles+ table itself never
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "handles"
|
|
4
3
|
require_relative "../codec"
|
|
5
4
|
require_relative "../errors"
|
|
6
5
|
|
|
@@ -18,23 +17,19 @@ module Kobako
|
|
|
18
17
|
# services.lookup("MyService::KV") # => kv_object
|
|
19
18
|
#
|
|
20
19
|
# Per-dispatch routing is +Kobako::Transport::Dispatcher+'s
|
|
21
|
-
# responsibility — the Dispatcher
|
|
22
|
-
# +
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
20
|
+
# responsibility — the Dispatcher resolves a path against the invocation's
|
|
21
|
+
# +Kobako::Context+ (which layers per-invocation providers and +ctx.bind+
|
|
22
|
+
# overrides over these base bindings) and the +Catalog::Handles+, both
|
|
23
|
+
# passed in the per-invocation dispatch Proc the Context hands to
|
|
24
|
+
# +Runtime#eval+ / +#run+.
|
|
26
25
|
class Services
|
|
27
26
|
# Ruby constant-name pattern each +::+-separated bind-path segment
|
|
28
27
|
# must match.
|
|
29
28
|
NAME_PATTERN = /\A[A-Z]\w*\z/
|
|
30
29
|
|
|
31
|
-
# Build a fresh registry.
|
|
32
|
-
|
|
33
|
-
# is pinned near +MAX_ID+ to exercise the cap-exhaustion path
|
|
34
|
-
# without 2³¹ allocations. Production callers leave it at the default.
|
|
35
|
-
def initialize(handler: Catalog::Handles.new)
|
|
30
|
+
# Build a fresh registry.
|
|
31
|
+
def initialize
|
|
36
32
|
@bindings = {} # : Hash[String, untyped]
|
|
37
|
-
@handler = handler
|
|
38
33
|
@sealed = false
|
|
39
34
|
@encoded = nil # : String?
|
|
40
35
|
end
|
|
@@ -56,6 +51,12 @@ module Kobako
|
|
|
56
51
|
self
|
|
57
52
|
end
|
|
58
53
|
|
|
54
|
+
# Returns +true+ iff a Service is bound at +path+ — the declared (Frame 1)
|
|
55
|
+
# key set a per-invocation +ctx.bind+ override may target.
|
|
56
|
+
def bound?(path)
|
|
57
|
+
@bindings.key?(path.to_s)
|
|
58
|
+
end
|
|
59
|
+
|
|
59
60
|
# Resolve a +target+ constant path to the bound Service. Raises
|
|
60
61
|
# +KeyError+ when no Service is bound at +target+.
|
|
61
62
|
def lookup(target)
|
|
@@ -65,18 +66,6 @@ module Kobako
|
|
|
65
66
|
@bindings[target_str]
|
|
66
67
|
end
|
|
67
68
|
|
|
68
|
-
# Replace the object bound at an already-registered +path+ without the
|
|
69
|
-
# collision or seal checks +#bind+ applies. +Catalog::Extensions+ calls
|
|
70
|
-
# this during +begin_invocation!+ to swap a callable backend provider's
|
|
71
|
-
# per-invocation result behind a path whose Frame 1 membership was
|
|
72
|
-
# fixed at the seal. A path that was never registered is left untouched,
|
|
73
|
-
# so the sealed key set can never grow here.
|
|
74
|
-
def refresh(path, object)
|
|
75
|
-
key = path.to_s
|
|
76
|
-
@bindings[key] = object if @bindings.key?(key)
|
|
77
|
-
self
|
|
78
|
-
end
|
|
79
|
-
|
|
80
69
|
# Encode the preamble as msgpack bytes for stdin Frame 1 delivery —
|
|
81
70
|
# a flat array of the bound constant paths, in bind order:
|
|
82
71
|
# +["MyService::KV", "File"]+. Routes through Kobako::Codec::Encoder
|
|
@@ -84,23 +73,20 @@ module Kobako
|
|
|
84
73
|
# Strings, so none of the kobako ext types fire. Returns a binary
|
|
85
74
|
# +String+ of msgpack bytes.
|
|
86
75
|
#
|
|
87
|
-
#
|
|
88
|
-
# subsequent
|
|
89
|
-
#
|
|
90
|
-
# raises +ArgumentError+ and never alters Frame 1.
|
|
76
|
+
# The bytes are pinned eagerly by #seal! at the first invocation, so
|
|
77
|
+
# every subsequent call is a pure read of the frozen preamble — a bind
|
|
78
|
+
# can never reach the registry after the seal to alter Frame 1.
|
|
91
79
|
def encode
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
bytes = Codec::Encoder.encode(@bindings.keys).freeze
|
|
95
|
-
@encoded = bytes if @sealed
|
|
96
|
-
bytes
|
|
80
|
+
@encoded || Codec::Encoder.encode(@bindings.keys).freeze
|
|
97
81
|
end
|
|
98
82
|
|
|
99
|
-
# Mark the registry as sealed
|
|
100
|
-
#
|
|
101
|
-
#
|
|
83
|
+
# Mark the registry as sealed and pin the Frame 1 preamble bytes so
|
|
84
|
+
# registration can never alter them afterwards. Called by +Sandbox+ on
|
|
85
|
+
# the first invocation; afterwards #bind raises ArgumentError.
|
|
86
|
+
# Idempotent; returns +self+.
|
|
102
87
|
def seal!
|
|
103
88
|
@sealed = true
|
|
89
|
+
@encoded ||= Codec::Encoder.encode(@bindings.keys).freeze
|
|
104
90
|
self
|
|
105
91
|
end
|
|
106
92
|
|
|
@@ -41,17 +41,19 @@ module Kobako
|
|
|
41
41
|
# externally via +entry_payload+ rather than asking each entry to
|
|
42
42
|
# self-encode.
|
|
43
43
|
#
|
|
44
|
-
#
|
|
45
|
-
#
|
|
46
|
-
#
|
|
47
|
-
# Unlike +Catalog::Services#encode+, which gates its memo on the
|
|
48
|
-
# seal, this one can fill eagerly and invalidate in +#register+
|
|
49
|
-
# because every mutation funnels through that single method — there is
|
|
50
|
-
# no out-of-sight child object to change the result behind its back.
|
|
44
|
+
# Before the first invocation this recomputes from the still-open
|
|
45
|
+
# table; #seal! then pins the bytes, so every later call is a pure
|
|
46
|
+
# read of the frozen memo.
|
|
51
47
|
def encode
|
|
52
|
-
|
|
48
|
+
@encoded || Codec::Encoder.encode(@entries.map { |entry| entry_payload(entry) }).freeze
|
|
49
|
+
end
|
|
53
50
|
|
|
54
|
-
|
|
51
|
+
# Freeze the Frame 3 table bytes at the first invocation so the
|
|
52
|
+
# replayed snippet set can never change afterwards. Called by +Sandbox+
|
|
53
|
+
# alongside the Service / Extension seals. Idempotent; returns +self+.
|
|
54
|
+
def seal!
|
|
55
|
+
@encoded ||= Codec::Encoder.encode(@entries.map { |entry| entry_payload(entry) }).freeze
|
|
56
|
+
self
|
|
55
57
|
end
|
|
56
58
|
|
|
57
59
|
# Register one preloaded snippet in either of two forms.
|
|
@@ -73,7 +75,6 @@ module Kobako
|
|
|
73
75
|
# missing keywords, wrong types, malformed +name+, or
|
|
74
76
|
# duplicate +code:+ +name+.
|
|
75
77
|
def register(code: nil, name: nil, binary: nil)
|
|
76
|
-
@encoded = nil
|
|
77
78
|
if binary
|
|
78
79
|
raise ArgumentError, "cannot combine binary: with code: / name:" if code || name
|
|
79
80
|
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "capture"
|
|
4
|
+
require_relative "codec"
|
|
5
|
+
require_relative "errors"
|
|
6
|
+
require_relative "execution"
|
|
7
|
+
require_relative "unresolved"
|
|
8
|
+
require_relative "outcome"
|
|
9
|
+
require_relative "usage"
|
|
10
|
+
require_relative "transport"
|
|
11
|
+
require_relative "catalog"
|
|
12
|
+
|
|
13
|
+
module Kobako
|
|
14
|
+
# Kobako::Context — the per-invocation driver behind a single +Sandbox#eval+
|
|
15
|
+
# / +#run+. Each Context owns a fresh +Catalog::Handles+ table and drives one
|
|
16
|
+
# guest invocation: it builds the dispatch handler, runs the guest, records
|
|
17
|
+
# the run's captures and usage, and decodes the outcome. One Context per
|
|
18
|
+
# invocation owns all per-invocation state, so the reusable +Sandbox+ holds
|
|
19
|
+
# none and overlapping evals never touch each other's state.
|
|
20
|
+
class Context
|
|
21
|
+
# Build a Context over the Sandbox-owned config — the +Runtime+, the
|
|
22
|
+
# sealed +Catalog::Services+ / +Catalog::Snippets+ registries, and the
|
|
23
|
+
# +Catalog::Extensions+ whose callable backends this Context resolves for
|
|
24
|
+
# its own run. The +Catalog::Handles+ table is this invocation's own, so
|
|
25
|
+
# guest→host dispatch and host→guest auto-wrap share one allocator scoped
|
|
26
|
+
# to the run; the resolved provider map is likewise per-invocation, so
|
|
27
|
+
# concurrent invocations never share mutable state.
|
|
28
|
+
def initialize(runtime:, services:, snippets:, extensions:)
|
|
29
|
+
@runtime = runtime
|
|
30
|
+
@services = services
|
|
31
|
+
@snippets = snippets
|
|
32
|
+
@extensions = extensions
|
|
33
|
+
@resolved = {} # : Hash[String, untyped]
|
|
34
|
+
@overrides = {} # : Hash[String, untyped]
|
|
35
|
+
@spent = false
|
|
36
|
+
@handler = Catalog::Handles.new
|
|
37
|
+
@stdout_capture = @stderr_capture = Capture::EMPTY
|
|
38
|
+
@usage = Usage::EMPTY
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Override the object bound at an already-declared +path+ for this
|
|
42
|
+
# invocation only — the per-eval hook the +#eval+ / +#run+ block uses to
|
|
43
|
+
# fill a fillable or shadow any static / per-invocation binding. +path+
|
|
44
|
+
# must name a declared (Frame 1) binding; overriding an undeclared path
|
|
45
|
+
# raises +ArgumentError+ so the Frame 1 key set stays fixed. Valid only
|
|
46
|
+
# while the block runs — the Context is spent once the block returns, so a
|
|
47
|
+
# captured +ctx+ used afterward raises +ArgumentError+ too, putting both
|
|
48
|
+
# misuses on one rescuable channel. Returns +self+.
|
|
49
|
+
def bind(path, object)
|
|
50
|
+
raise ArgumentError, "Kobako::Context is spent; ctx.bind is only valid inside the #eval / #run block" if @spent
|
|
51
|
+
|
|
52
|
+
key = path.to_s
|
|
53
|
+
raise ArgumentError, "cannot override undeclared path #{key.inspect}" unless @services.bound?(key)
|
|
54
|
+
|
|
55
|
+
@overrides[key] = object
|
|
56
|
+
self
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Resolve a Service +path+ to the object backing it this invocation,
|
|
60
|
+
# layering the per-eval +ctx.bind+ overrides over this Context's
|
|
61
|
+
# per-invocation provider results over the Sandbox's static base bindings.
|
|
62
|
+
# An unbound path raises +KeyError+; a fillable left unfilled resolves to
|
|
63
|
+
# +Kobako::Unresolved+ and is reported the same way, so an unresolved
|
|
64
|
+
# capability fails closed as an undefined target rather than dispatching to
|
|
65
|
+
# the sentinel. The Dispatcher maps either +KeyError+ to an
|
|
66
|
+
# undefined-target wire fault. Internal — the per-invocation dispatch
|
|
67
|
+
# handler is the sole caller.
|
|
68
|
+
def lookup(path)
|
|
69
|
+
key = path.to_s
|
|
70
|
+
object = @overrides.fetch(key) { @resolved.fetch(key) { @services.lookup(path) } }
|
|
71
|
+
raise KeyError, "service #{path} is declared but unresolved this invocation" if Unresolved.equal?(object)
|
|
72
|
+
|
|
73
|
+
object
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Execute a guest mruby source string in a fresh +mrb_state+ and return the
|
|
77
|
+
# decoded last expression. A given +block+ runs first, receiving this
|
|
78
|
+
# Context to collect +ctx.bind+ overrides before the guest drives.
|
|
79
|
+
def eval(code, &block)
|
|
80
|
+
collect_overrides(&block) if block
|
|
81
|
+
invoke!(:eval) do
|
|
82
|
+
@runtime.eval(dispatch_handler, @services.encode, code.b, @snippets.encode)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Dispatch a +Transport::Run+ envelope into a preloaded entrypoint and
|
|
87
|
+
# return the decoded result. A given +block+ runs first, receiving this
|
|
88
|
+
# Context to collect +ctx.bind+ overrides before the guest drives.
|
|
89
|
+
def run(run_envelope, &block)
|
|
90
|
+
collect_overrides(&block) if block
|
|
91
|
+
invoke!(:run) do
|
|
92
|
+
@runtime.run(dispatch_handler, @services.encode, @snippets.encode, run_envelope.encode(@handler))
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
# Run the per-eval override +block+, handing it this Context so it can call
|
|
99
|
+
# +ctx.bind+, then spend the Context so a captured +ctx+ used after the
|
|
100
|
+
# block raises. A block that raises propagates before the guest drives, so
|
|
101
|
+
# the guest never runs and no Execution is produced.
|
|
102
|
+
def collect_overrides
|
|
103
|
+
yield self
|
|
104
|
+
ensure
|
|
105
|
+
@spent = true
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Build this invocation's guest→host dispatch handler — a +Proc+ routing
|
|
109
|
+
# each guest→host call through the stateless +Transport::Dispatcher+,
|
|
110
|
+
# capturing this Context as the path resolver (its +#lookup+ layers the
|
|
111
|
+
# per-invocation providers over the static bindings) plus +@handler+. Handed to
|
|
112
|
+
# +Runtime#eval+ / +#run+ as a call argument, so the Runtime holds no
|
|
113
|
+
# dispatch state and the +Proc+ stays GC-rooted as a live argument for the
|
|
114
|
+
# synchronous call. The ext hands the +Proc+ a per-dispatch +guest_yielder+
|
|
115
|
+
# — a +String → String+ callable that re-enters the in-flight guest to run
|
|
116
|
+
# a yielded block — which the +Dispatcher+ forwards to the
|
|
117
|
+
# +Transport::Yielder+ it builds for the call.
|
|
118
|
+
def dispatch_handler
|
|
119
|
+
lambda do |request_bytes, guest_yielder|
|
|
120
|
+
Transport::Dispatcher.dispatch(request_bytes, self, @handler, guest_yielder)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Record this invocation's usage and both output captures from the ext
|
|
125
|
+
# +Snapshot+. Every Snapshot carries them — value return or trap alike — so
|
|
126
|
+
# +#usage+ / +#stdout+ / +#stderr+ stay readable after a rescued trap.
|
|
127
|
+
def populate_observability!(snapshot)
|
|
128
|
+
@usage = Usage.new(wall_time: snapshot.wall_time, memory_peak: snapshot.memory_peak)
|
|
129
|
+
@stdout_capture = Capture.new(bytes: snapshot.stdout, truncated: snapshot.stdout_truncated?)
|
|
130
|
+
@stderr_capture = Capture.new(bytes: snapshot.stderr, truncated: snapshot.stderr_truncated?)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Pick the +TrapError+ subclass to re-raise based on +err+'s actual class.
|
|
134
|
+
# Cap-trap subclasses (+TimeoutError+ / +MemoryLimitError+) preserve their
|
|
135
|
+
# named identity; everything else collapses to the base +Kobako::TrapError+,
|
|
136
|
+
# so #invoke! can add the verb prefix without erasing the named subclass.
|
|
137
|
+
def trap_class_for(err)
|
|
138
|
+
case err
|
|
139
|
+
when TimeoutError then TimeoutError
|
|
140
|
+
when MemoryLimitError then MemoryLimitError
|
|
141
|
+
else TrapError
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Build the +TrapError+-family exception for a trapped +Snapshot+ from its
|
|
146
|
+
# neutral trap kind, tagged with the verb — the cap subclasses
|
|
147
|
+
# (+TimeoutError+ / +MemoryLimitError+) keep their identity, every other
|
|
148
|
+
# engine fault is the base +TrapError+.
|
|
149
|
+
def trap_error_for(snapshot, verb)
|
|
150
|
+
klass = case snapshot.trap_kind
|
|
151
|
+
when :timeout then TimeoutError
|
|
152
|
+
when :memory_limit then MemoryLimitError
|
|
153
|
+
else TrapError
|
|
154
|
+
end
|
|
155
|
+
klass.new("Sandbox##{verb} failed: #{snapshot.trap_message}")
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Freeze this run's observables plus +value+ (+nil+ on a failed run) into
|
|
159
|
+
# the read-only +Execution+ the caller receives or the error carries.
|
|
160
|
+
# +failed+ records the two apart so a +nil+ +value+ from a successful run
|
|
161
|
+
# stays distinct from a failed one.
|
|
162
|
+
def build_execution(value, failed:)
|
|
163
|
+
Execution.new(value: value, usage: @usage, stdout: @stdout_capture, stderr: @stderr_capture, failed: failed)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Decode a completed run's outcome into its +Execution+. A Capability
|
|
167
|
+
# Handle in the result is restored to its host object first. Decode sits in
|
|
168
|
+
# the rescue so a wire-violation trap or a Panic envelope both attach this
|
|
169
|
+
# run's Execution, just like a guest-call trap does.
|
|
170
|
+
def settle_outcome(snapshot, verb)
|
|
171
|
+
value, carried = Codec.track_handles { Outcome.decode(snapshot.outcome) }
|
|
172
|
+
value = Codec::HandleWalk.deep_restore(value, @handler) if carried
|
|
173
|
+
build_execution(value, failed: false)
|
|
174
|
+
rescue Kobako::TrapError => e
|
|
175
|
+
raise trap_class_for(e).new("Sandbox##{verb} failed: #{e.message}").with_execution(build_execution(nil,
|
|
176
|
+
failed: true))
|
|
177
|
+
rescue Kobako::SandboxError, Kobako::ServiceError => e
|
|
178
|
+
raise e.with_execution(build_execution(nil, failed: true))
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Drive one invocation and settle it into a frozen +Execution+. +verb+
|
|
182
|
+
# tags the TrapError message so the failing export is identifiable. This
|
|
183
|
+
# invocation's callable Extension backends are resolved first — before the
|
|
184
|
+
# guest runs — so a provider that raises propagates unwrapped and leaves
|
|
185
|
+
# the guest unrun. Usage and captures are recorded before the trap check,
|
|
186
|
+
# so a trapped Snapshot's error carries them just like a completed run's
|
|
187
|
+
# return value. A could-not-start fault ran no invocation at all, so it
|
|
188
|
+
# carries no Execution and gains only the verb prefix.
|
|
189
|
+
def invoke!(verb)
|
|
190
|
+
@resolved = @extensions.resolve
|
|
191
|
+
begin
|
|
192
|
+
snapshot = yield
|
|
193
|
+
rescue Kobako::TrapError => e
|
|
194
|
+
raise trap_class_for(e), "Sandbox##{verb} failed: #{e.message}"
|
|
195
|
+
end
|
|
196
|
+
populate_observability!(snapshot)
|
|
197
|
+
return settle_outcome(snapshot, verb) unless snapshot.trapped?
|
|
198
|
+
|
|
199
|
+
raise trap_error_for(snapshot, verb).with_execution(build_execution(nil, failed: true))
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|