boxcars 0.10.3 → 0.10.4
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/.rubocop_todo.yml +2 -1
- data/CHANGELOG.md +5 -0
- data/Gemfile.lock +1 -1
- data/README.md +39 -2
- data/UPGRADING.md +16 -0
- data/boxcars-0.10.3.gem +0 -0
- data/boxcars.gemspec +38 -0
- data/lib/boxcars/boxcar/active_record.rb +9 -5
- data/lib/boxcars/boxcar/sql_base.rb +52 -5
- data/lib/boxcars/version.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 91d8243a75d34977c339725329556f657706266926f307f86fb0051ed1ab13fb
|
|
4
|
+
data.tar.gz: da944b8f37d3d0a2f51f8ff2a42486b6ec7efbf8204f27dc0c323f3bbe28746e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5c0eaac090cdb3ae28856000fa3b57b850d942e9fb61fe3b86fbe2c2e6702c82dcc9016c09f577132ef997d328eaf1b9aab1a9808a195f3a99444f3709e47eeb
|
|
7
|
+
data.tar.gz: 725ca632b6281c7c76b8c83382bb0f0bf13dd0cecd39a575fd7ea5ba8d8f093faecb1a2e0facfeadbb63db89412361a84b6982a978af087e5a9de55428ad8d9f
|
data/.rubocop_todo.yml
CHANGED
|
@@ -51,11 +51,12 @@ Metrics/MethodLength:
|
|
|
51
51
|
- 'lib/boxcars/engines.rb'
|
|
52
52
|
- 'lib/boxcars/train/tool_train.rb'
|
|
53
53
|
|
|
54
|
-
# Offense count:
|
|
54
|
+
# Offense count: 5
|
|
55
55
|
# Configuration parameters: Max, CountKeywordArgs, MaxOptionalParameters.
|
|
56
56
|
Metrics/ParameterLists:
|
|
57
57
|
Exclude:
|
|
58
58
|
- 'lib/boxcars/boxcar/json_engine_boxcar.rb'
|
|
59
|
+
- 'lib/boxcars/boxcar/sql_base.rb'
|
|
59
60
|
- 'lib/boxcars/engine.rb'
|
|
60
61
|
- 'lib/boxcars/engine/openai_compatible_chat_helpers.rb'
|
|
61
62
|
- 'lib/boxcars/mcp/stdio_client.rb'
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **`context:` parameter for ActiveRecord and SQL boxcars** — Pass runtime context (current user, tenant, permissions) into prompts so the LLM generates properly scoped queries. Available as a constructor keyword and an `attr_accessor` for per-request updates.
|
|
8
|
+
- **Read-only mode for SQL boxcars** — `SQLBase` (and subclasses `SQLActiveRecord`, `SQLSequel`) now default to read-only, rejecting write SQL (`INSERT`, `UPDATE`, `DELETE`, `DROP`, etc.) with `Boxcars::SecurityError`. Pass `read_only: false` to allow writes, or provide an `approval_callback:` proc that receives the SQL string for custom approval logic.
|
|
9
|
+
|
|
5
10
|
## [0.10.3] - 2026-03-02
|
|
6
11
|
|
|
7
12
|
### Added
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
|
@@ -68,6 +68,25 @@ All of these concepts are in a module named Boxcars:
|
|
|
68
68
|
## Security
|
|
69
69
|
Currently, our system is designed for individuals who already possess administrative privileges for their project. It is likely possible to manipulate the system's prompts to carry out malicious actions, but if you already have administrative access, you can perform such actions without requiring boxcars in the first place.
|
|
70
70
|
|
|
71
|
+
### Read-Only Defaults for Data Boxcars
|
|
72
|
+
|
|
73
|
+
Both `ActiveRecord` and the SQL boxcars (`SQLActiveRecord`, `SQLSequel`) default to **read-only mode**, rejecting write operations (INSERT, UPDATE, DELETE, DROP, etc.) with a `Boxcars::SecurityError`. This prevents LLM-generated code or SQL from accidentally modifying your database.
|
|
74
|
+
|
|
75
|
+
To allow writes, either disable read-only mode or provide an approval callback:
|
|
76
|
+
|
|
77
|
+
```ruby
|
|
78
|
+
# Option 1: disable read-only (use with caution)
|
|
79
|
+
sql = Boxcars::SQLActiveRecord.new(read_only: false)
|
|
80
|
+
|
|
81
|
+
# Option 2: approval callback for write SQL
|
|
82
|
+
sql = Boxcars::SQLActiveRecord.new(approval_callback: ->(sql) { puts "Approve? #{sql}"; true })
|
|
83
|
+
|
|
84
|
+
# Option 3: approval callback for ActiveRecord (receives change count and code)
|
|
85
|
+
ar = Boxcars::ActiveRecord.new(approval_callback: ->(changes, code) { changes < 5 })
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
When an `approval_callback` is provided, read-only defaults to `false` so the callback can decide. You can combine `read_only: true` with a callback to enforce read-only regardless.
|
|
89
|
+
|
|
71
90
|
*Note:* We are actively seeking ways to improve our system's ability to identify and prevent any nefarious attempts from occurring. If you have any suggestions or recommendations, please feel free to share them with us by either finding an existing issue or creating a new one and providing us with your feedback.
|
|
72
91
|
|
|
73
92
|
## Installation
|
|
@@ -196,11 +215,29 @@ Boxcars ships with high-leverage tools you can compose immediately, and you can
|
|
|
196
215
|
- `GoogleSearch`: uses SERP API for live web lookup.
|
|
197
216
|
- `WikipediaSearch`: uses Wikipedia API for fast factual retrieval.
|
|
198
217
|
- `Calculator`: uses an engine to produce/execute Ruby math logic.
|
|
199
|
-
- `SQL`: generates and executes SQL from prompts using your ActiveRecord connection.
|
|
200
|
-
- `ActiveRecord`: generates and executes ActiveRecord code from prompts.
|
|
218
|
+
- `SQL`: generates and executes SQL from prompts using your ActiveRecord connection. Read-only by default.
|
|
219
|
+
- `ActiveRecord`: generates and executes ActiveRecord code from prompts. Read-only by default.
|
|
201
220
|
- `Swagger`: consumes OpenAPI (YAML/JSON) to answer questions about and run against API endpoints. See [Swagger notebook examples](https://github.com/BoxcarsAI/boxcars/blob/main/notebooks/swagger_examples.ipynb).
|
|
202
221
|
- `VectorStore` workflows: embed, persist, and retrieve context for RAG-like retrieval flows (see vector notebooks).
|
|
203
222
|
|
|
223
|
+
#### Scoping ActiveRecord and SQL Queries with `context:`
|
|
224
|
+
|
|
225
|
+
The `ActiveRecord` and `SQL` boxcars accept an optional `context:` parameter that injects runtime information (current user, tenant, permissions) into the LLM prompt so generated queries are properly scoped:
|
|
226
|
+
|
|
227
|
+
```ruby
|
|
228
|
+
ar = Boxcars::ActiveRecord.new(
|
|
229
|
+
models: [Ticket, Comment],
|
|
230
|
+
context: "The current user is User#42 (admin). Only return this user's records."
|
|
231
|
+
)
|
|
232
|
+
ar.run("How many open tickets do I have?")
|
|
233
|
+
|
|
234
|
+
# Update context per-request
|
|
235
|
+
ar.context = "The current user is User#99 (viewer)."
|
|
236
|
+
ar.run("Show my recent comments")
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
When `context` is `nil` or blank, nothing extra is added to the prompt.
|
|
240
|
+
|
|
204
241
|
### Run a list of Boxcars
|
|
205
242
|
```ruby
|
|
206
243
|
# run a Train for a calculator, and search using default Engine
|
data/UPGRADING.md
CHANGED
|
@@ -16,6 +16,22 @@ v1.0 is expected to:
|
|
|
16
16
|
- Remove deprecated model aliases
|
|
17
17
|
- Prefer explicit model names (with a small curated alias set)
|
|
18
18
|
|
|
19
|
+
## SQL Boxcars Now Default to Read-Only (v0.10.x)
|
|
20
|
+
|
|
21
|
+
`SQLBase`, `SQLActiveRecord`, and `SQLSequel` now default to **read-only mode**, matching the existing `ActiveRecord` boxcar behavior. LLM-generated write SQL (`INSERT`, `UPDATE`, `DELETE`, `DROP`, etc.) raises `Boxcars::SecurityError` unless explicitly allowed.
|
|
22
|
+
|
|
23
|
+
If your app relies on SQL boxcars executing write statements, update your initialization:
|
|
24
|
+
|
|
25
|
+
```ruby
|
|
26
|
+
# Allow all writes (no approval gate)
|
|
27
|
+
sql = Boxcars::SQLActiveRecord.new(read_only: false)
|
|
28
|
+
|
|
29
|
+
# Or gate writes through a callback (read_only defaults to false when a callback is provided)
|
|
30
|
+
sql = Boxcars::SQLActiveRecord.new(approval_callback: ->(sql) { your_approval_logic(sql) })
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
**Note:** The SQL approval callback receives a single `(sql)` argument (the SQL string), unlike the ActiveRecord callback which receives `(changes, code)`.
|
|
34
|
+
|
|
19
35
|
## Provider Model Refresh Notes (v0.10.x)
|
|
20
36
|
|
|
21
37
|
- Cohere retired legacy `command-r*` model IDs. Boxcars now defaults Cohere to `command-a-03-2025`.
|
data/boxcars-0.10.3.gem
ADDED
|
Binary file
|
data/boxcars.gemspec
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "lib/boxcars/version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = "boxcars"
|
|
7
|
+
spec.version = Boxcars::VERSION
|
|
8
|
+
spec.authors = ["Francis Sullivan", "Tabrez Syed"]
|
|
9
|
+
spec.email = ["hi@boxcars.ai"]
|
|
10
|
+
|
|
11
|
+
spec.summary = "Boxcars is a gem that enables you to create new systems with AI composability. Inspired by python langchain."
|
|
12
|
+
spec.description = "You simply set an OpenAI key, give a number of Boxcars to a Train, and magic ensues when you run it."
|
|
13
|
+
spec.homepage = "https://github.com/BoxcarsAI/boxcars"
|
|
14
|
+
spec.license = "MIT"
|
|
15
|
+
spec.required_ruby_version = ">= 3.2.0"
|
|
16
|
+
|
|
17
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
|
18
|
+
spec.metadata["source_code_uri"] = spec.homepage
|
|
19
|
+
spec.metadata["changelog_uri"] = "https://github.com/BoxcarsAI/boxcars/blob/main/CHANGELOG.md"
|
|
20
|
+
spec.metadata["rubygems_mfa_required"] = "true"
|
|
21
|
+
|
|
22
|
+
# Specify which files should be added to the gem when it is released.
|
|
23
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
|
24
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
|
25
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
|
26
|
+
(f == __FILE__) || f.match(%r{\A(?:(?:test|spec|features|notebooks)/|\.(?:git|travis|circleci)|appveyor)})
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
spec.bindir = "exe"
|
|
30
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
|
31
|
+
spec.require_paths = ["lib"]
|
|
32
|
+
|
|
33
|
+
# runtime dependencies
|
|
34
|
+
# Provider/tooling gems are optional and loaded on use.
|
|
35
|
+
|
|
36
|
+
# For more information and examples about making a new gem, checkout our
|
|
37
|
+
# guide at: https://bundler.io/guides/creating_gem.html
|
|
38
|
+
end
|
|
@@ -8,7 +8,7 @@ module Boxcars
|
|
|
8
8
|
# Default description for this boxcar.
|
|
9
9
|
ARDESC = "useful for when you need to query a database for an application named %<name>s."
|
|
10
10
|
LOCKED_OUT_MODELS = %w[ActiveRecord::SchemaMigration ActiveRecord::InternalMetadata ApplicationRecord].freeze
|
|
11
|
-
attr_accessor :connection, :requested_models, :read_only, :approval_callback, :code_only
|
|
11
|
+
attr_accessor :connection, :requested_models, :read_only, :approval_callback, :code_only, :context
|
|
12
12
|
attr_reader :except_models
|
|
13
13
|
|
|
14
14
|
# @param models [Array<ActiveRecord::Model>] The models to use for this boxcar. Will use all if nil.
|
|
@@ -17,7 +17,8 @@ module Boxcars
|
|
|
17
17
|
# @param approval_callback [Proc] A function to call to approve changes. Defaults to nil.
|
|
18
18
|
# @param kwargs [Hash] Any other keyword arguments. These can include:
|
|
19
19
|
# :name, :description, :prompt, :except_models, :top_k, :stop, :code_only and :engine
|
|
20
|
-
def initialize(models: nil, except_models: nil, read_only: nil, approval_callback: nil, **kwargs)
|
|
20
|
+
def initialize(models: nil, except_models: nil, read_only: nil, approval_callback: nil, context: nil, **kwargs)
|
|
21
|
+
@context = context
|
|
21
22
|
Boxcars::OptionalDependency.require!(
|
|
22
23
|
"activerecord",
|
|
23
24
|
feature: "Boxcars::ActiveRecord",
|
|
@@ -35,7 +36,9 @@ module Boxcars
|
|
|
35
36
|
|
|
36
37
|
# @return [Hash] The additional variables for this boxcar.
|
|
37
38
|
def prediction_additional(_inputs)
|
|
38
|
-
|
|
39
|
+
ctx = @context.to_s.strip
|
|
40
|
+
context_str = ctx.empty? ? "" : "\n\nAdditional context:\n#{ctx}"
|
|
41
|
+
{ model_info:, context: context_str }.merge super
|
|
39
42
|
end
|
|
40
43
|
|
|
41
44
|
CTEMPLATE = [
|
|
@@ -57,7 +60,8 @@ module Boxcars
|
|
|
57
60
|
"Pay attention to use only the attribute names that you can see in the model description.\n",
|
|
58
61
|
"Do not make up variable or attribute names, and do not share variables between the code in ARChanges and ARCode\n",
|
|
59
62
|
"Be careful to not query for attributes that do not exist, and to use the format specified above.\n",
|
|
60
|
-
"Finally, try not to use print or puts in your code"
|
|
63
|
+
"Finally, try not to use print or puts in your code",
|
|
64
|
+
"%<context>s"
|
|
61
65
|
),
|
|
62
66
|
user("Question: %<question>s")
|
|
63
67
|
].freeze
|
|
@@ -280,7 +284,7 @@ module Boxcars
|
|
|
280
284
|
@my_prompt ||= ConversationPrompt.new(
|
|
281
285
|
conversation: @conversation,
|
|
282
286
|
input_variables: [:question],
|
|
283
|
-
other_inputs: [:top_k, :model_info],
|
|
287
|
+
other_inputs: [:top_k, :model_info, :context],
|
|
284
288
|
output_variables: [:answer])
|
|
285
289
|
end
|
|
286
290
|
end
|
|
@@ -9,15 +9,25 @@ module Boxcars
|
|
|
9
9
|
# Default description for this boxcar.
|
|
10
10
|
SQLDESC = "useful for when you need to query a database for %<name>s."
|
|
11
11
|
LOCKED_OUT_TABLES = %w[schema_migrations ar_internal_metadata].freeze
|
|
12
|
-
|
|
12
|
+
# SQL keywords that indicate a write operation.
|
|
13
|
+
WRITE_SQL_KEYWORDS = %w[INSERT UPDATE DELETE DROP ALTER CREATE TRUNCATE REPLACE MERGE UPSERT
|
|
14
|
+
GRANT REVOKE LOCK CALL EXEC EXECUTE].freeze
|
|
15
|
+
|
|
16
|
+
attr_accessor :connection, :the_tables, :context, :read_only, :approval_callback
|
|
13
17
|
|
|
14
18
|
# @param connection [ActiveRecord::Connection] or [Sequel Object] The SQL connection to use for this boxcar.
|
|
15
19
|
# @param tables [Array<String>] The tables to use for this boxcar. Will use all if nil.
|
|
16
20
|
# @param except_tables [Array<String>] The tables to exclude from this boxcar. Will exclude none if nil.
|
|
21
|
+
# @param read_only [Boolean] Whether to restrict to read-only SQL. Defaults to true unless approval_callback is given.
|
|
22
|
+
# @param approval_callback [Proc] A function to call to approve write SQL. Receives the SQL string. Defaults to nil.
|
|
17
23
|
# @param kwargs [Hash] Any other keyword arguments to pass to the parent class. This can include
|
|
18
24
|
# :name, :description, :prompt, :top_k, :stop, and :engine
|
|
19
|
-
def initialize(connection: nil, tables: nil, except_tables: nil,
|
|
25
|
+
def initialize(connection: nil, tables: nil, except_tables: nil, context: nil, read_only: nil,
|
|
26
|
+
approval_callback: nil, **kwargs)
|
|
27
|
+
@context = context
|
|
20
28
|
@connection = connection
|
|
29
|
+
@approval_callback = approval_callback
|
|
30
|
+
@read_only = read_only.nil? ? !approval_callback : read_only
|
|
21
31
|
check_tables(tables, except_tables)
|
|
22
32
|
kwargs[:name] ||= "Database"
|
|
23
33
|
kwargs[:description] ||= format(SQLDESC, name:)
|
|
@@ -29,7 +39,9 @@ module Boxcars
|
|
|
29
39
|
|
|
30
40
|
# @return [Hash] The additional variables for this boxcar.
|
|
31
41
|
def prediction_additional(_inputs)
|
|
32
|
-
|
|
42
|
+
ctx = @context.to_s.strip
|
|
43
|
+
context_str = ctx.empty? ? "" : "\n\nAdditional context:\n#{ctx}"
|
|
44
|
+
{ schema:, dialect:, context: context_str }.merge super
|
|
33
45
|
end
|
|
34
46
|
|
|
35
47
|
CTEMPLATE = [
|
|
@@ -46,12 +58,43 @@ module Boxcars
|
|
|
46
58
|
"SQLQuery: 'SQL Query to run'\n",
|
|
47
59
|
"SQLResult: 'Result of the SQLQuery'\n",
|
|
48
60
|
"Answer: 'Final answer here'"),
|
|
49
|
-
syst("Only use the following tables:\n%<schema>s"),
|
|
61
|
+
syst("Only use the following tables:\n%<schema>s%<context>s"),
|
|
50
62
|
user("Question: %<question>s")
|
|
51
63
|
].freeze
|
|
52
64
|
|
|
65
|
+
# @return [Boolean] Whether this boxcar is in read-only mode.
|
|
66
|
+
def read_only?
|
|
67
|
+
read_only
|
|
68
|
+
end
|
|
69
|
+
|
|
53
70
|
private
|
|
54
71
|
|
|
72
|
+
# Check if a SQL statement is safe (read-only) to run.
|
|
73
|
+
# Strips string literals first to avoid false positives on values like 'DELETE ME'.
|
|
74
|
+
# @param sql [String] The SQL statement to check.
|
|
75
|
+
# @return [Boolean] true if the SQL appears to be a read-only statement.
|
|
76
|
+
def sql_safe_to_run?(sql)
|
|
77
|
+
without_strings = sql.gsub(/'([^'\\]*(\\.[^'\\]*)*)'/, "''")
|
|
78
|
+
upper = without_strings.upcase
|
|
79
|
+
WRITE_SQL_KEYWORDS.none? { |kw| upper.match?(/\b#{kw}\b/) }
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Check if the SQL is approved for execution.
|
|
83
|
+
# @param sql [String] The SQL statement to check.
|
|
84
|
+
# @return [Boolean] true if approved.
|
|
85
|
+
def approved?(sql)
|
|
86
|
+
return true if sql_safe_to_run?(sql)
|
|
87
|
+
|
|
88
|
+
if read_only?
|
|
89
|
+
Boxcars.error("Cannot execute write SQL in read-only mode: #{sql}", :red)
|
|
90
|
+
return false
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
return approval_callback.call(sql) if approval_callback.is_a?(Proc)
|
|
94
|
+
|
|
95
|
+
true
|
|
96
|
+
end
|
|
97
|
+
|
|
55
98
|
def check_tables(rtables, exceptions)
|
|
56
99
|
requested_tables = nil
|
|
57
100
|
if rtables.is_a?(Array)
|
|
@@ -106,8 +149,12 @@ module Boxcars
|
|
|
106
149
|
code = text[/^SQLQuery: (.*)/, 1]
|
|
107
150
|
code = extract_code text.split('SQLQuery:').last.strip
|
|
108
151
|
Boxcars.debug code, :yellow
|
|
152
|
+
raise Boxcars::SecurityError, "Permission to execute write SQL denied" unless approved?(code)
|
|
153
|
+
|
|
109
154
|
output = clean_up_output(code)
|
|
110
155
|
Result.new(status: :ok, answer: output, explanation: "Answer: #{output.to_json}", code:)
|
|
156
|
+
rescue Boxcars::SecurityError => e
|
|
157
|
+
raise e
|
|
111
158
|
rescue StandardError => e
|
|
112
159
|
Result.new(status: :error, answer: nil, explanation: "Error: #{e.message}", code:)
|
|
113
160
|
end
|
|
@@ -130,7 +177,7 @@ module Boxcars
|
|
|
130
177
|
@my_prompt ||= ConversationPrompt.new(
|
|
131
178
|
conversation: @conversation,
|
|
132
179
|
input_variables: [:question],
|
|
133
|
-
other_inputs: [:top_k, :dialect, :schema],
|
|
180
|
+
other_inputs: [:top_k, :dialect, :schema, :context],
|
|
134
181
|
output_variables: [:answer])
|
|
135
182
|
end
|
|
136
183
|
end
|
data/lib/boxcars/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: boxcars
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.10.
|
|
4
|
+
version: 0.10.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Francis Sullivan
|
|
@@ -36,6 +36,8 @@ files:
|
|
|
36
36
|
- USER_CONTEXT_GUIDE.md
|
|
37
37
|
- bin/console
|
|
38
38
|
- bin/setup
|
|
39
|
+
- boxcars-0.10.3.gem
|
|
40
|
+
- boxcars.gemspec
|
|
39
41
|
- lib/boxcars.rb
|
|
40
42
|
- lib/boxcars/boxcar.rb
|
|
41
43
|
- lib/boxcars/boxcar/active_record.rb
|