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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1e1257a3e6a1d95041ec565a3861f037a83b3d97f8854950fcd4d349f75c21d3
4
- data.tar.gz: ca4b9b0e72f10964e221910f530bc5e2aaa361820e4ee817b232613f7af32147
3
+ metadata.gz: 91d8243a75d34977c339725329556f657706266926f307f86fb0051ed1ab13fb
4
+ data.tar.gz: da944b8f37d3d0a2f51f8ff2a42486b6ec7efbf8204f27dc0c323f3bbe28746e
5
5
  SHA512:
6
- metadata.gz: 52667c34002a5a9c533dec1605f4242935bd16f69da9c717c12114d9a76b9b31395ae47ab3b26f4aa9eebe474a5a42487125eaaf774ce8690fc90a3c361ea907
7
- data.tar.gz: fa3e322f2e50a450ea9feb39472c621b4917f38c4f69ca39571345597e0bab7d05decbf78faa82f5de7f0af218d4099eb600d2cf4a2d49392cae4fe4640fa106
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: 4
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
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- boxcars (0.10.3)
4
+ boxcars (0.10.4)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
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`.
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
- { model_info: }.merge super
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
- attr_accessor :connection, :the_tables
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, **kwargs)
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
- { schema:, dialect: }.merge super
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
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Boxcars
4
4
  # The current version of the gem.
5
- VERSION = "0.10.3"
5
+ VERSION = "0.10.4"
6
6
  end
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.3
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