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.
Files changed (5) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +124 -0
  4. data/lib/ruby_llm/code_mode.rb +156 -0
  5. 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: []