ruby_llm-code_mode 0.1.1
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 +7 -0
- data/LICENSE +21 -0
- data/README.md +124 -0
- data/lib/ruby_llm/code_mode.rb +156 -0
- metadata +100 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: d933b7782ef9b21eb6c9c704630a456b1aa96ffecc52e4a258aa50fd99e09ebf
|
|
4
|
+
data.tar.gz: d006d9fbac9d85b569a0978f5bf74cca9430ffe8bf7ba47c526a83ecdb52b7ac
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 0ce747225815a3dd6a9aeccc8ccbb74ac64d9a24e7716797941d75afb009e55c7a56a899abafaeac5a490ad7a921ce0c771955ced03d72861f9472c42f3a2c13
|
|
7
|
+
data.tar.gz: e62fdd4dbe81c6dd7034ea4e27d1ff18c0aa2bf22d83ab58a94d35e1b51fc045db31e73fd4a560c13ffc5615d8e800cdb83be2f0c38c704c1a8e9c767fec97cd
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Juneira
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# ruby_llm-code_mode
|
|
2
|
+
|
|
3
|
+
A [RubyLLM](https://rubyllm.com) tool that runs model-generated Ruby code inside a
|
|
4
|
+
secure sandbox, powered by [SecurityBox](https://rubygems.org/gems/security_box).
|
|
5
|
+
|
|
6
|
+
The model writes Ruby; the code runs as a fresh Ruby 4.0 process inside a
|
|
7
|
+
WebAssembly sandbox (ruby.wasm + wasmtime) with no network, no threads, no
|
|
8
|
+
subprocesses and no access to the host filesystem — except the folders you
|
|
9
|
+
explicitly mount, which are read-only by default.
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
gem install ruby_llm-code_mode
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Or add to your Gemfile:
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
gem "ruby_llm-code_mode"
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
Define a tool class, declare the host folders the sandbox may see, and give it to
|
|
26
|
+
your chat:
|
|
27
|
+
|
|
28
|
+
```ruby
|
|
29
|
+
require "ruby_llm"
|
|
30
|
+
require "ruby_llm/code_mode"
|
|
31
|
+
|
|
32
|
+
class Analytics < RubyLLM::CodeMode
|
|
33
|
+
mount source: "data/input", dest: "/data", description: "CSV files with the raw data (read only)"
|
|
34
|
+
mount_rw source: "workspace", dest: "/workspace", description: "Write generated reports and artifacts here"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
chat = RubyLLM.chat
|
|
38
|
+
chat.with_tools(Analytics)
|
|
39
|
+
chat.ask "Compute the total sales per month from the CSVs in /data and save the report to /workspace/report.md"
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### What the model sees
|
|
43
|
+
|
|
44
|
+
The tool is presented to the model as `analytics` (derived from the class name) with
|
|
45
|
+
a fixed description built from the declared mounts:
|
|
46
|
+
|
|
47
|
+
- a **How to use** section: the code runs as a fresh Ruby 4.0 process, isolated
|
|
48
|
+
from the host, with a private `/work` scratch directory wiped between
|
|
49
|
+
executions and no state carried over between runs;
|
|
50
|
+
- a **Read-only folders** section listing each `mount` as `` `/guest/path` — description``;
|
|
51
|
+
- a **Read-write folders** section listing each `mount_rw`.
|
|
52
|
+
|
|
53
|
+
The single parameter is `code` — a complete, self-contained Ruby script.
|
|
54
|
+
|
|
55
|
+
### What the tool returns
|
|
56
|
+
|
|
57
|
+
A JSON object sent back to the model:
|
|
58
|
+
|
|
59
|
+
| status | payload |
|
|
60
|
+
|---|---|
|
|
61
|
+
| `ok` | `{status:, value:, stdout?}` — value of the last expression, captured `puts` output |
|
|
62
|
+
| `error` | `{status:, error: {class, message, backtrace}, stdout?}` — guest exception details |
|
|
63
|
+
| `timeout` / `fuel_exhausted` / `memory_limit` | `{status:, error:}` — resource limit hit |
|
|
64
|
+
| `sandbox_error` | `{status:, error:, stderr?}` — the sandbox itself failed |
|
|
65
|
+
|
|
66
|
+
### DSL
|
|
67
|
+
|
|
68
|
+
```ruby
|
|
69
|
+
mount source: host_path, dest: guest_path, description: "..." # read-only
|
|
70
|
+
mount_rw source: host_path, dest: guest_path, description: "..." # read-write
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
- `source:` — folder on the host machine, relative to the process working
|
|
74
|
+
directory (expanded at class-definition time) or absolute.
|
|
75
|
+
- `dest:` — absolute, normalized path inside the sandbox (may not overlap
|
|
76
|
+
the reserved `/work`, `/usr` or `/src` trees, and must be unique).
|
|
77
|
+
- `description:` — shown to the model in the tool description.
|
|
78
|
+
|
|
79
|
+
Invalid declarations raise at class-definition time, so a misconfigured tool
|
|
80
|
+
never reaches a live chat. Subclasses inherit their parent's mounts.
|
|
81
|
+
|
|
82
|
+
### Sandbox limits
|
|
83
|
+
|
|
84
|
+
Defaults: `timeout_ms: 30_000` (wall clock), `fuel_ms: 10_000` (CPU budget).
|
|
85
|
+
Call `MyTool.warmup` at boot to pay the WebAssembly compilation cost up front —
|
|
86
|
+
every subsequent evaluation then takes a few hundred milliseconds.
|
|
87
|
+
|
|
88
|
+
## Security notes
|
|
89
|
+
|
|
90
|
+
- Mounted folders are fully readable by the executed code — only mount folders
|
|
91
|
+
whose content you are willing to expose.
|
|
92
|
+
- Read-only mounts are enforced by wasmtime: writes fail inside the sandbox and
|
|
93
|
+
never touch the host folder.
|
|
94
|
+
- Read-write mounts are part of the guest's blast radius: the model can create,
|
|
95
|
+
modify and delete files in them.
|
|
96
|
+
- Guest code is sandboxed by [SecurityBox](https://rubygems.org/gems/security_box):
|
|
97
|
+
no network, no threads, no processes, no host filesystem beyond the mounts,
|
|
98
|
+
deterministic CPU/memory/time limits, and forged results are rejected.
|
|
99
|
+
|
|
100
|
+
## Roadmap
|
|
101
|
+
|
|
102
|
+
- v2: a **Tools** section in the description backed by SecurityBox host RPC
|
|
103
|
+
handlers, so guest code can call registered host functions via `SB.call`.
|
|
104
|
+
|
|
105
|
+
## Development
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
bundle install
|
|
109
|
+
bundle exec rspec
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Releasing
|
|
113
|
+
|
|
114
|
+
The version lives in the `VERSION` constant of `lib/ruby_llm/code_mode.rb`
|
|
115
|
+
(the gemspec parses it from the source).
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
rake version:bump[minor] # bump: major, minor or patch (default: patch)
|
|
119
|
+
git commit -am "Bump version to X.Y.Z"
|
|
120
|
+
rake release # specs + git tag vX.Y.Z + push to GitHub + push gem to RubyGems
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
`rake release` refuses a dirty working tree or an existing tag and uses the
|
|
124
|
+
RubyGems credentials from `~/.gem/credentials`.
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ruby_llm"
|
|
4
|
+
require "security_box"
|
|
5
|
+
|
|
6
|
+
module RubyLLM
|
|
7
|
+
class CodeMode < Tool
|
|
8
|
+
VERSION = "0.1.1"
|
|
9
|
+
|
|
10
|
+
DEFAULT_TIMEOUT_MS = 30_000
|
|
11
|
+
DEFAULT_FUEL_MS = 10_000
|
|
12
|
+
|
|
13
|
+
FIXED_DESCRIPTION = <<~DESC.freeze
|
|
14
|
+
Executes Ruby code inside a secure sandbox and returns what it produced.
|
|
15
|
+
|
|
16
|
+
## How to use
|
|
17
|
+
|
|
18
|
+
Pass a complete, self-contained Ruby script as the `code` argument. It runs
|
|
19
|
+
as a fresh Ruby 4.0 process with:
|
|
20
|
+
- no network, no threads, no subprocesses, and no access to the host
|
|
21
|
+
filesystem outside the mounted folders listed below;
|
|
22
|
+
- a private writable scratch directory at `/work`, wiped between executions;
|
|
23
|
+
- no state carried over between executions, except the contents of
|
|
24
|
+
read-write folders.
|
|
25
|
+
|
|
26
|
+
Capture progress with `puts` (returned as `stdout`) and end with a value or
|
|
27
|
+
a JSON-serializable data structure (returned as `value`; non-serializable
|
|
28
|
+
objects come back as their inspect string). If the code raises, the error
|
|
29
|
+
class, message and backtrace are returned so you can fix and retry.
|
|
30
|
+
DESC
|
|
31
|
+
|
|
32
|
+
Mount = Struct.new(:host, :guest, :mode, :description, keyword_init: true) do
|
|
33
|
+
def payload
|
|
34
|
+
{ host: host, guest: guest, mode: mode }
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
class << self
|
|
39
|
+
def tool_name
|
|
40
|
+
leaf = name.to_s.split('::').last
|
|
41
|
+
return '' unless leaf
|
|
42
|
+
|
|
43
|
+
RubyLLM::Support::Utils.underscore(leaf).delete_suffix('_tool')
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def mount(source:, dest:, description: nil)
|
|
47
|
+
add_mount(source, dest, :read_only, description)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def mount_rw(source:, dest:, description: nil)
|
|
51
|
+
add_mount(source, dest, :read_write, description)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def mounts
|
|
55
|
+
@mounts ||= []
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def configuration
|
|
59
|
+
@configuration ||= SecurityBox::Configuration.build(
|
|
60
|
+
timeout_ms: DEFAULT_TIMEOUT_MS,
|
|
61
|
+
fuel_ms: DEFAULT_FUEL_MS,
|
|
62
|
+
mounts: mounts.map(&:payload)
|
|
63
|
+
)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def build_description
|
|
67
|
+
read_only, read_write = mounts.partition { |m| m.mode == :read_only }
|
|
68
|
+
[
|
|
69
|
+
FIXED_DESCRIPTION.rstrip,
|
|
70
|
+
folder_section("## Read-only folders (readable, never writable)", read_only),
|
|
71
|
+
folder_section("## Read-write folders (readable and writable)", read_write)
|
|
72
|
+
].compact.join("\n\n")
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def description(text = nil)
|
|
76
|
+
raise ArgumentError,
|
|
77
|
+
"RubyLLM::CodeMode builds the tool description from the declared mounts; " \
|
|
78
|
+
"the description is fixed and cannot be set by hand" if text
|
|
79
|
+
|
|
80
|
+
build_description
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def warmup
|
|
84
|
+
SecurityBox.warmup(timeout_ms: DEFAULT_TIMEOUT_MS, fuel_ms: DEFAULT_FUEL_MS)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def inherited(subclass)
|
|
88
|
+
super
|
|
89
|
+
subclass.instance_variable_set(:@mounts, mounts.dup)
|
|
90
|
+
subclass.instance_variable_set(:@configuration, nil)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
def add_mount(source, dest, mode, description)
|
|
96
|
+
mounts << Mount.new(
|
|
97
|
+
host: File.expand_path(source.to_s),
|
|
98
|
+
guest: dest.to_s,
|
|
99
|
+
mode: mode,
|
|
100
|
+
description: description.to_s
|
|
101
|
+
)
|
|
102
|
+
SecurityBox::Mounts.normalize(mounts.map(&:payload))
|
|
103
|
+
rescue SecurityBox::InvalidConfiguration => e
|
|
104
|
+
mounts.pop
|
|
105
|
+
raise ArgumentError,
|
|
106
|
+
"Invalid mount #{source.inspect} => #{dest.inspect}: #{e.message}"
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def folder_section(header, entries)
|
|
110
|
+
return nil if entries.empty?
|
|
111
|
+
|
|
112
|
+
lines = entries.map do |entry|
|
|
113
|
+
line = "- `#{entry.guest}`"
|
|
114
|
+
line += " — #{entry.description}" unless entry.description.empty?
|
|
115
|
+
line
|
|
116
|
+
end
|
|
117
|
+
[header, *lines].join("\n")
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
parameter :code, description: "Complete Ruby script to execute in the sandbox"
|
|
122
|
+
|
|
123
|
+
def execute(code:)
|
|
124
|
+
format_result(sandbox.eval(code))
|
|
125
|
+
rescue SecurityBox::Error => e
|
|
126
|
+
{ status: "sandbox_error", error: { class: e.class.name, message: e.message } }
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
private
|
|
130
|
+
|
|
131
|
+
def sandbox
|
|
132
|
+
@sandbox ||= SecurityBox::Sandbox.new(self.class.configuration)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def format_result(result)
|
|
136
|
+
payload = { status: result.status.to_s }
|
|
137
|
+
case result.status
|
|
138
|
+
when :ok
|
|
139
|
+
payload[:value] = result.value
|
|
140
|
+
when :error
|
|
141
|
+
payload[:error] = result.error || { "class" => "Unknown", "message" => "guest code failed" }
|
|
142
|
+
when :timeout
|
|
143
|
+
payload[:error] = "execution timed out before finishing"
|
|
144
|
+
when :fuel_exhausted
|
|
145
|
+
payload[:error] = "execution exceeded its CPU budget before finishing"
|
|
146
|
+
when :memory_limit
|
|
147
|
+
payload[:error] = "execution exceeded the sandbox memory limit"
|
|
148
|
+
when :sandbox_error
|
|
149
|
+
payload[:error] = "the sandbox failed to run the code"
|
|
150
|
+
end
|
|
151
|
+
payload[:stdout] = result.stdout if result.stdout && !result.stdout.empty?
|
|
152
|
+
payload[:stderr] = result.stderr if result.stderr && !result.stderr.empty?
|
|
153
|
+
payload
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: ruby_llm-code_mode
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Juneira
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: ruby_llm
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '2.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '2.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: security_box
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0.6'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0.6'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: rspec
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '3.13'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '3.13'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: rake
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '13.2'
|
|
61
|
+
type: :development
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '13.2'
|
|
68
|
+
description: |
|
|
69
|
+
A RubyLLM tool that runs model-generated Ruby code inside a secure
|
|
70
|
+
WebAssembly sandbox (SecurityBox), with explicit read-only and read-write
|
|
71
|
+
folder mounts so agents can work on host project folders safely.
|
|
72
|
+
executables: []
|
|
73
|
+
extensions: []
|
|
74
|
+
extra_rdoc_files: []
|
|
75
|
+
files:
|
|
76
|
+
- LICENSE
|
|
77
|
+
- README.md
|
|
78
|
+
- lib/ruby_llm/code_mode.rb
|
|
79
|
+
licenses:
|
|
80
|
+
- MIT
|
|
81
|
+
metadata:
|
|
82
|
+
allowed_push_host: https://rubygems.org
|
|
83
|
+
rdoc_options: []
|
|
84
|
+
require_paths:
|
|
85
|
+
- lib
|
|
86
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
87
|
+
requirements:
|
|
88
|
+
- - ">="
|
|
89
|
+
- !ruby/object:Gem::Version
|
|
90
|
+
version: 4.0.0
|
|
91
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - ">="
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: '0'
|
|
96
|
+
requirements: []
|
|
97
|
+
rubygems_version: 4.0.10
|
|
98
|
+
specification_version: 4
|
|
99
|
+
summary: Secure sandboxed Ruby code execution tool for RubyLLM, powered by SecurityBox.
|
|
100
|
+
test_files: []
|