rails-ai-context 5.36.7 → 5.36.8
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/CHANGELOG.md +95 -0
- data/docs/COMPATIBILITY.md +9 -0
- data/docs/CONFIGURATION.md +1 -1
- data/docs/SECURITY.md +37 -2
- data/docs/STANDALONE.md +1 -1
- data/docs/TOOLS.md +5 -5
- data/lib/rails_ai_context/cli/entry_boot.rb +2 -0
- data/lib/rails_ai_context/doctor.rb +4 -1
- data/lib/rails_ai_context/initializer_settings.rb +600 -0
- data/lib/rails_ai_context/introspectors/env_references.rb +1 -1
- data/lib/rails_ai_context/redaction.rb +653 -17
- data/lib/rails_ai_context/safe_path.rb +101 -11
- data/lib/rails_ai_context/sql_gate.rb +5 -0
- data/lib/rails_ai_context/tools/get_env.rb +20 -30
- data/lib/rails_ai_context/tools/get_partial_interface.rb +12 -3
- data/lib/rails_ai_context/tools/get_view.rb +6 -3
- data/lib/rails_ai_context/tools/review_changes.rb +74 -29
- data/lib/rails_ai_context/tools/search_code.rb +24 -8
- data/lib/rails_ai_context/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8cd401837a164e83cf1558e5ff9ed491e0409974c9ffae9d4b648e56b4e4fb68
|
|
4
|
+
data.tar.gz: 1667e185eba134e006450acd3edf19ffe1e590b3a6033608db25ad60e529e87a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ef8df079d4306b933571f8a01f697a0ca8c6e96b663b71894e065760fc72eb5ffca09a0a07471722afb684e805258ad05c60676f28a7f68599f591e1f955ec67
|
|
7
|
+
data.tar.gz: 7edafa2842e04597cbc61786c8ee67c7e8c74ab312ce9c75c6d0bc711bf0f496deff280e45984c5104e08b229ec7ba16b6d7dd822990f133d77e945b923ec174
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,101 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [5.36.8] - 2026-10-23
|
|
9
|
+
|
|
10
|
+
### Security
|
|
11
|
+
|
|
12
|
+
- **The static tier keeps the security settings the app's Ruby config writes.**
|
|
13
|
+
No initializer runs without a boot, so on the static tier (`--no-boot`, or a
|
|
14
|
+
boot that failed) a `sensitive_patterns`, `excluded_paths` or `skip_tools`
|
|
15
|
+
value set in a `RailsAiContext.configure` block was dropped, and a file the
|
|
16
|
+
block made sensitive was searched and read. The static tier now reads every
|
|
17
|
+
Ruby file of the app that names the gem, without running it: a file under
|
|
18
|
+
`config/`, and the top level of any other, whole, and a method's body
|
|
19
|
+
elsewhere (a tool's, a helper's) for what it writes, with tests, installed
|
|
20
|
+
gems and generator templates left out. The literal values those files write
|
|
21
|
+
to the three keys are added to the YAML's; a removal is left out, the
|
|
22
|
+
stricter reading. A value it cannot read, such as one built from a constant
|
|
23
|
+
or the environment, a pattern changed in place, or the configuration reached
|
|
24
|
+
through `send`, stops the static tier with an error that names the file and
|
|
25
|
+
line.
|
|
26
|
+
- **A sensitive pattern covers its file wherever it sits.**
|
|
27
|
+
`config/database.yml` and the other patterns with a `/` matched only at the
|
|
28
|
+
app root, so an engine's, a pack's or a test/dummy app's copy was read by
|
|
29
|
+
every file tool, and `**/id_rsa` missed an `id_rsa` at the root. A pattern
|
|
30
|
+
now reads the way a `.gitignore` line does: one with a `/` names its file
|
|
31
|
+
below the root too, a leading `/` anchors it at the root, a trailing `/`
|
|
32
|
+
names a folder and everything in it, and `**/` stands for any number of
|
|
33
|
+
folders, none included.
|
|
34
|
+
A file a link brings in is also asked by its name in the app's repository, so
|
|
35
|
+
a link to a pack's `config/` folder cannot hide the `config/` a pattern looks
|
|
36
|
+
for. `doctor`'s gitignore check reads the files below the root the same way.
|
|
37
|
+
A path a caller names that is longer than 4096 bytes, which no file system
|
|
38
|
+
holds, is answered as not found before any pattern reads it.
|
|
39
|
+
- **`rails_search_code` reads a path the way the file system does.** A path
|
|
40
|
+
with a `.` segment (`./`, `config/.`) made ripgrep print `./config/...`, and a
|
|
41
|
+
link inside the app to `config/` made it print the link's name, so no
|
|
42
|
+
sensitive pattern matched and a sensitive file's lines came back. The path is
|
|
43
|
+
now cleaned, and one inside the app is searched as the folder it leads to.
|
|
44
|
+
- **`rails_search_code` and `rails_review_changes` keep a private key's body
|
|
45
|
+
hidden on every line they show.** A search row is redacted with its whole
|
|
46
|
+
file, but ripgrep searched files over `max_file_size`, which the redaction
|
|
47
|
+
does not read, so each body line of a PEM key in such a file came back on its
|
|
48
|
+
own, unfiltered. ripgrep now skips those files as the Ruby search does, and a
|
|
49
|
+
file named as the path over the limit answers "File too large". A diff hunk
|
|
50
|
+
that opened inside a key showed its body lines the same way; each diff line
|
|
51
|
+
is now redacted with the whole side of the file it comes from, the hunk
|
|
52
|
+
header's line too. The diff is read with the repository's colour, external
|
|
53
|
+
diff and textconv settings off, and a diff over `max_file_size` or a merge
|
|
54
|
+
conflict's combined diff shows no lines. A submodule's change shows as the
|
|
55
|
+
commits it moves between, where `diff.submodule=diff` printed its files'
|
|
56
|
+
lines unredacted. A changed file that is not UTF-8 is read, where the tool
|
|
57
|
+
stopped with an encoding error.
|
|
58
|
+
- **`rails_read_logs` filters a secret column's value written inline in a
|
|
59
|
+
statement.** MySQL and MariaDB run no prepared statements, so Active Record
|
|
60
|
+
logs every value inline with backquoted names, and `insert_all` and
|
|
61
|
+
`upsert_all` write their rows inline on every adapter. Password hashes,
|
|
62
|
+
tokens and reset keys came back from INSERT, UPDATE and WHERE lines, from the
|
|
63
|
+
`CAST()` and `LOWER()` a uniqueness validation writes, and from the database
|
|
64
|
+
errors that repeat a row's values. A value compared with a secret column
|
|
65
|
+
(through a JSON path too), listed for it with `IN` or `BETWEEN`, inserted
|
|
66
|
+
under it, or bound to its placeholder is now filtered whatever quotes the
|
|
67
|
+
names and the value carry, and so is a value whose column cannot be told in a
|
|
68
|
+
statement that names a secret column, every value of an `INSERT` with no
|
|
69
|
+
column list, and a secret-named key in JSON a value holds. PostgreSQL's
|
|
70
|
+
failing row is filtered whole, as it names no column, MySQL's duplicate entry
|
|
71
|
+
unless Rails named the index after columns that hold no secret, PostgreSQL's
|
|
72
|
+
duplicate key when the key names a secret, and the statement excerpt a MySQL
|
|
73
|
+
syntax error quotes; a message cut off is filtered to the end of its line. A
|
|
74
|
+
JSON log line's strings are read the same way, a value under a secret's key
|
|
75
|
+
and a JSON logger's binds included.
|
|
76
|
+
- **`rails_get_config(section: "env")` hides a DSN or a webhook URL wherever it
|
|
77
|
+
is set.** A Dockerfile `ENV` or `ARG` value, an `.env.example` value and a
|
|
78
|
+
default in code went through the generic redaction only, which reads a URL's
|
|
79
|
+
password but not a key before `@` or a token in the path, so a Sentry DSN or
|
|
80
|
+
a Slack webhook printed in full where the Kamal section hid the same value.
|
|
81
|
+
Those values now read `[FILTERED]` when their shape can carry a secret: a URL
|
|
82
|
+
whose user part is a key or a password, a webhook's address, a token in a
|
|
83
|
+
query, a key-like run of letters and digits, and a `BUNDLE_<HOST>` gem server
|
|
84
|
+
credential. Paths, hosts and local or public URLs still show. An
|
|
85
|
+
`.env.example` value in quotes keeps a `#`, where the part after it printed
|
|
86
|
+
as a comment, and an inline comment is hidden when it holds a secret's shape.
|
|
87
|
+
- **`rails_get_view` and `rails_get_partial_interface` match a path as plain
|
|
88
|
+
text.** A missing view's folder and a partial's name were globbed as written,
|
|
89
|
+
so each `{a,b}` group doubled the work for every view file, and one crafted
|
|
90
|
+
path held a server thread for minutes.
|
|
91
|
+
- **`rails_query` refuses a `PROCEDURE` clause on MySQL and MariaDB.**
|
|
92
|
+
MariaDB's `SELECT ... PROCEDURE ANALYSE()` returns each column's lowest and
|
|
93
|
+
highest values, lengths and null counts, a secret column's included, under
|
|
94
|
+
names of its own that the result redaction does not read. `SHOW PROCEDURE
|
|
95
|
+
STATUS` still runs.
|
|
96
|
+
- **The patterns that find a URL's password, a JWT and an `sk-` key read a
|
|
97
|
+
line in time that grows with its length.** The URL pattern tried a scheme from
|
|
98
|
+
each letter of a run, and the JWT and `sk-` patterns read to a run's end from
|
|
99
|
+
every repeat in it, so a 100 KB log line or env value took 15 to 35 seconds. A
|
|
100
|
+
URL's password is now read from its `://`, after a shell default's `:-` too,
|
|
101
|
+
and a JWT and an `sk-` key are read so that a run of repeats costs its length.
|
|
102
|
+
|
|
8
103
|
## [5.36.7] - 2026-10-22
|
|
9
104
|
|
|
10
105
|
### Security
|
data/docs/COMPATIBILITY.md
CHANGED
|
@@ -366,6 +366,15 @@ Proof sources:
|
|
|
366
366
|
security, gate, redaction and database specs on Rails 7.0 (Ruby 3.1), 7.1,
|
|
367
367
|
7.2 and 8.1 against live PostgreSQL; and the unit suite and the E2E harness
|
|
368
368
|
on Linux, macOS and Windows.
|
|
369
|
+
In the v5.36.8 release QA: each fix's example failing on the code before
|
|
370
|
+
it; `rails_read_logs` against Active Record 8.0's own logs from live
|
|
371
|
+
PostgreSQL 16, MySQL 8.4 and MariaDB 13 for inline values, binds and the
|
|
372
|
+
errors that repeat a row, and against a JSON logger's lines; the static-tier
|
|
373
|
+
settings reader, `rails_search_code`, `rails_get_config` and
|
|
374
|
+
`rails_review_changes` on five open-source apps and two private ones with
|
|
375
|
+
`--no-boot`, compared with v5.36.7 and every changed answer checked; the
|
|
376
|
+
touched specs on Rails 7.0 (Ruby 3.1) and on prism 1.4; and the unit suite
|
|
377
|
+
and the E2E harness on Linux, macOS and Windows.
|
|
369
378
|
2. Non-crash coverage for every built-in tool including `get_view` in
|
|
370
379
|
`spec/e2e/in_gemfile_install_spec.rb`'s full-tool sweep; output correctness
|
|
371
380
|
(ivar cross-check, render-form detection, partial interfaces) verified
|
data/docs/CONFIGURATION.md
CHANGED
|
@@ -39,7 +39,7 @@ preset: full
|
|
|
39
39
|
### Precedence
|
|
40
40
|
|
|
41
41
|
> [!IMPORTANT]
|
|
42
|
-
> `configure` block > YAML > Defaults, merged key by key. The file is read once, at boot, before `config/initializers`, so an initializer may assign a key or edit it in place (`config.skip_tools << "rails_query"`) and both survive. A block that runs *before* the file - in `config/application.rb` or an environment file - must assign a key to keep it (`config.skip_tools = ["rails_query"]`), because an in-place edit there is replaced when the file loads. A key no block assigns keeps the YAML value. Corrupted YAML degrades gracefully with a warning, and a key the gem does not know warns on stderr and is ignored while the rest of the file applies.
|
|
42
|
+
> `configure` block > YAML > Defaults, merged key by key. The file is read once, at boot, before `config/initializers`, so an initializer may assign a key or edit it in place (`config.skip_tools << "rails_query"`) and both survive. A block that runs *before* the file - in `config/application.rb` or an environment file - must assign a key to keep it (`config.skip_tools = ["rails_query"]`), because an in-place edit there is replaced when the file loads. A key no block assigns keeps the YAML value. Corrupted YAML degrades gracefully with a warning, and a key the gem does not know warns on stderr and is ignored while the rest of the file applies. On the static tier no initializer runs: the literal values that the app's Ruby files naming the gem write to `sensitive_patterns`, `excluded_paths` and `skip_tools` are read without running them and added to the YAML's, and one the gem cannot read stops the static tier with an error naming its file and line ([SECURITY.md](SECURITY.md#configuration)).
|
|
43
43
|
|
|
44
44
|
---
|
|
45
45
|
|
data/docs/SECURITY.md
CHANGED
|
@@ -57,6 +57,7 @@ Before any query reaches the database, the SQL gate (`RailsAiContext::SqlGate`)
|
|
|
57
57
|
- **Blocks lock clauses**: FOR UPDATE, FOR SHARE, FOR NO KEY UPDATE, FOR KEY SHARE and LOCK IN SHARE MODE, with whatever follows them (NOWAIT, SKIP LOCKED, OF, WAIT). On PostgreSQL the clause is read on quote-masked text, so `LIKE '%ready for update%'` is data: its read-only transaction refuses a locking read anyway. MySQL's read-only transaction lets `FOR SHARE` and `LOCK IN SHARE MODE` through, so there and on SQLite the raw text decides
|
|
58
58
|
- **Blocks SHOW forms that tell of other sessions, replication and logs, on MySQL and MariaDB**: `SHOW GRANTS`, `SHOW PROCESSLIST` and `SHOW FULL PROCESSLIST`, `SHOW BINLOG`, `SHOW BINARY LOGS` and `SHOW BINARY LOG STATUS`, `SHOW RELAYLOG EVENTS`, the `SHOW SLAVE` and `SHOW SLAVES` forms, `SHOW MASTER`, the `SHOW REPLICA` and `SHOW REPLICAS` forms, `SHOW ALL SLAVES STATUS` and `SHOW ALL REPLICAS STATUS`, `SHOW ENGINE ... STATUS`, `SHOW PROFILE` and `SHOW PROFILES`, `SHOW OPEN TABLES`, `SHOW EXPLAIN FOR` and `SHOW ANALYZE FOR`, `SHOW USER_STATISTICS`, `SHOW CLIENT_STATISTICS` and the `SHOW WSREP_` forms. The answer is "Blocked: sensitive SHOW command". They are matched on the raw text, so the words inside a string literal are refused too
|
|
59
59
|
- **Blocks `EXPLAIN FOR CONNECTION`, on MySQL and MariaDB**: it shows the plan of the statement another session is running. It is refused also as `DESCRIBE` or `DESC`, with a `FORMAT`, and inside a version comment. The answer is "Blocked: EXPLAIN FOR CONNECTION is not run. It shows the plan of the statement another session is running." It is read on quote-masked text, so a string literal that holds the words runs
|
|
60
|
+
- **Blocks a `PROCEDURE` clause, on MySQL and MariaDB**: MariaDB's `SELECT ... PROCEDURE ANALYSE()` returns each column's lowest and highest values, a secret column's too, under names of its own that the result redaction does not read. `PROCEDURE` is a reserved word there, so a bare one outside a `SHOW` statement is refused in any spelling, and `SHOW PROCEDURE STATUS` runs. The answer starts "Blocked: a PROCEDURE clause is not run."
|
|
60
61
|
- **Blocks SHOW with a WHERE clause**, on every family. MySQL's statement timeout holds `SELECT` only, so a `SHOW ... WHERE` filter would run past `query_timeout`. `SHOW ... LIKE` runs. The answer names the query to use: "Blocked: SHOW with a WHERE clause is not run, because MySQL's statement timeout does not hold it. SHOW ... LIKE runs. For any other filter, SELECT from information_schema, for example `SELECT table_name FROM information_schema.tables WHERE table_schema = DATABASE()`."
|
|
61
62
|
- **Blocks SELECT INTO**: prevents table creation via SELECT. After a `WITH` clause an `INTO` can belong to a write, and such a statement is answered as that write: `WITH ... INSERT INTO ...` answers "Blocked: contains INSERT", and `REPLACE` and `MERGE` are named the same way. `INTO OUTFILE` / `INTO DUMPFILE` disk writes, and the file and network functions below, stay matched on the **raw (unmasked) text**. No read-only transaction stops a server-side file write, so `INTO OUTFILE` inside a string literal is refused on every database family, and a file function inside one on the family that has it
|
|
62
63
|
- **Blocks file and network functions**, each on the database family that has it. PostgreSQL: `pg_read_file`, `pg_read_binary_file`, `pg_ls_dir`, `pg_stat_file`, `lo_import`, `lo_export`, `dblink*` and their relatives. MySQL and MariaDB: `LOAD DATA` and `LOAD_FILE`. SQLite: `load_extension`. They are checked before and after comment stripping
|
|
@@ -357,6 +358,16 @@ A placeholder whose name ends in `.example`, `.sample`, `.template` or
|
|
|
357
358
|
characters, still does, and so does a path pattern (one with a `/`, such as
|
|
358
359
|
`.ssh/*`), which covers everything under it.
|
|
359
360
|
|
|
361
|
+
A pattern reads the way a `.gitignore` line does. One with a `/` names its
|
|
362
|
+
file below the root as well, so `config/database.yml` covers an engine's, a
|
|
363
|
+
pack's and a test/dummy app's copy, and a leading `/` anchors it at the root. A
|
|
364
|
+
trailing `/` names a folder and everything in it, and `**/` stands for any
|
|
365
|
+
number of folders, none included, so `**/id_rsa` covers an `id_rsa` at the
|
|
366
|
+
root too. A file a link brings in from the app's repository is also asked by
|
|
367
|
+
its name there, so a link to a pack's `config/` folder cannot hide the
|
|
368
|
+
`config/` a pattern looks for. A path a caller names that is longer than 4096
|
|
369
|
+
bytes, which no file system holds, is answered as not found.
|
|
370
|
+
|
|
360
371
|
### AI context file exclusions
|
|
361
372
|
|
|
362
373
|
Search also excludes generated AI context files to prevent circular references:
|
|
@@ -376,6 +387,28 @@ AGENTS.md, opencode.json
|
|
|
376
387
|
config.sensitive_patterns = %w[.env* *.key *.pem credentials.yml.enc]
|
|
377
388
|
```
|
|
378
389
|
|
|
390
|
+
On the static tier (`--no-boot`, or a boot that failed) no initializer runs.
|
|
391
|
+
The gem reads every Ruby file of the app that names it (`RailsAiContext` or
|
|
392
|
+
`rails_ai_context`), however the boot loads it, without running it: a file
|
|
393
|
+
under a `config/` folder, and the top level and class bodies of any other file,
|
|
394
|
+
which run as the file loads, whole; the body of a method outside `config/`, a
|
|
395
|
+
tool's or a helper's that runs when called, for what it writes. Tests (a `spec/` or `test/` folder at any
|
|
396
|
+
depth, an engine's test app among them), `db/`, `tmp/`, `vendor/bundle/`,
|
|
397
|
+
`vendor/ruby/`, `vendor/cache/`, `vendor/gems/`, an installed copy of the gem, a
|
|
398
|
+
generator's templates, hidden files and links are left out, and
|
|
399
|
+
`config/initializers` and `config/environments` are read through a link as
|
|
400
|
+
Rails globs them. The literal values those files write to
|
|
401
|
+
`sensitive_patterns`, `excluded_paths` and `skip_tools` are added to the YAML's;
|
|
402
|
+
a removal is left out. A value it cannot read, such as one built from a
|
|
403
|
+
constant or the environment, the configuration handed to other code or
|
|
404
|
+
reached through `send`, a pattern changed in place, or a file that does not
|
|
405
|
+
parse, stops the static tier with an error that names the file and line;
|
|
406
|
+
outside a method, so does a list or one of its patterns kept or handed on. A
|
|
407
|
+
file outside the app, a list a method keeps and changes later, and code that reaches
|
|
408
|
+
the settings through a name it builds at run time
|
|
409
|
+
(`const_get("Rails" + "AiContext")`) are not followed.
|
|
410
|
+
`.rails-ai-context.yml` is read on every tier.
|
|
411
|
+
|
|
379
412
|
---
|
|
380
413
|
|
|
381
414
|
## Path traversal protection
|
|
@@ -516,7 +549,7 @@ Complex patterns that would cause catastrophic backtracking raise `RegexpError`
|
|
|
516
549
|
|
|
517
550
|
`SafeFile.read` provides drop-in safety for all file reads:
|
|
518
551
|
|
|
519
|
-
- Size limit enforcement (`config.max_file_size`, default: 5 MB)
|
|
552
|
+
- Size limit enforcement (`config.max_file_size`, default: 5 MB). `rails_search_code` skips a file over it on both backends, so a row is never shown without its whole file's redaction, and a file named as its path answers `File too large`
|
|
520
553
|
- Returns `nil` on any failure (no exceptions leak)
|
|
521
554
|
- UTF-8 encoding with invalid/undefined byte replacement
|
|
522
555
|
- Handles: ENOENT, EACCES, EISDIR, ENAMETOOLONG, SystemCallError
|
|
@@ -532,6 +565,8 @@ What gets redacted:
|
|
|
532
565
|
- Passwords, tokens, secrets and API keys, wherever they are named
|
|
533
566
|
- The same in the app's own source a tool or resource shows: a method body, a template, a test file, a scan's code line, a change's diff (where a file on `sensitive_patterns` shows none of its lines)
|
|
534
567
|
- Credentials embedded in URIs (`redis://user:pass@host`)
|
|
568
|
+
- Values a logged SQL statement holds for a secret column: compared with it (through `LOWER()`, `CAST()` or a JSON path too), listed for it with `IN` or `BETWEEN`, inserted under it, or bound to a `$1` or `?` placeholder for it. MySQL and MariaDB log every value inline, and `insert_all` and `upsert_all` do on every adapter. A value whose column cannot be told is filtered when the statement names a secret column, and every value of an `INSERT` with no column list is filtered. A secret-named key in JSON a value holds is filtered, its quotes escaped or not. A line with `\'` in it that holds no backquoted name outside its strings is read both as MySQL and as standard SQL. Each string of a JSON log line, a header before it or not, is read as a line of its own; a value under a secret's key is filtered, an array's included, and so are a JSON logger's binds when the statement beside them names a secret column. A line is read on its own, so a value with a line break in it is read a line at a time
|
|
569
|
+
- The values a database error repeats: PostgreSQL's failing row whole, since it names no column, and its duplicate key when the key names a secret; MySQL's and MariaDB's duplicate entry, unless Rails named the index after columns that name no secret (`index_users_on_email`); and the excerpt of the statement a MySQL syntax error quotes, whole. A message cut off is filtered to the end of its line. An error in a language other than English is read as any other line
|
|
535
570
|
- Email addresses in log lines, in any script (`jürgen@example.com`, `anna@münchen.de`)
|
|
536
571
|
- Values assigned to secret-named settings in initializers
|
|
537
572
|
|
|
@@ -596,7 +631,7 @@ The engine builds one transport per process, under a mutex, outside the controll
|
|
|
596
631
|
## Credential handling
|
|
597
632
|
|
|
598
633
|
- `rails_get_config(section:"env")` returns credential **keys** and no values
|
|
599
|
-
- The process's environment variable values are never read. A default written in code, and a placeholder from `.env.example`, `.env.sample` or `.env.template`, is shown after redaction
|
|
634
|
+
- The process's environment variable values are never read. A default written in code, and a placeholder from `.env.example`, `.env.sample` or `.env.template`, is shown after redaction. A default in code, an example file's value and a Dockerfile `ENV` or `ARG` value read `[FILTERED]` when their shape can carry a secret: a URL whose user part holds a password or is a key (16 or more letters and digits, or hex in groups, as a DSN's is), a webhook's address, a token in a query, a run of 16 or more letters and digits with both in it, and a hex key split into groups. `/`, `.`, `-` and `_` end a run, so a path, a host and a slug show; so does a key written with them, base64 with a `/` among them, or in letters alone, unless its name says it is one. A class name with a digit in a run of 16 reads `[FILTERED]`. A `BUNDLE_<HOST>` variable, which Bundler reads a gem server's password from, is hidden whatever it holds. An example file's inline comment is hidden when it holds a credential, one of these shapes or a hex key in groups among its words, and a `#` starts a comment only after a space, so a password's `#` stays in its value (dotenv itself cuts an unquoted value at any `#`). A Kamal clear value that holds any URL, or a run of 16 letters and digits with `/` in it, is hidden
|
|
600
635
|
- `config/credentials/*.yml.enc` is in the sensitive patterns list
|
|
601
636
|
|
|
602
637
|
---
|
data/docs/STANDALONE.md
CHANGED
|
@@ -117,7 +117,7 @@ A key the gem does not know warns on stderr and is ignored; the rest of the file
|
|
|
117
117
|
|
|
118
118
|
### Precedence
|
|
119
119
|
|
|
120
|
-
In standalone mode `.rails-ai-context.yml` is the only config source. The gem is not loaded while `config/initializers` runs, so a `config/initializers/rails_ai_context.rb` contributes nothing: the generated file, which guards on `defined?(RailsAiContext) && RailsAiContext.respond_to?(:configure)`, is a silent no-op, and one without that guard raises `NoMethodError` (the binary opens the `RailsAiContext` namespace before the boot, so the constant exists but `configure` is not defined on it) and drops the command into the static tier.
|
|
120
|
+
In standalone mode `.rails-ai-context.yml` is the only config source of a booted run. The gem is not loaded while `config/initializers` runs, so a `config/initializers/rails_ai_context.rb` contributes nothing there: the generated file, which guards on `defined?(RailsAiContext) && RailsAiContext.respond_to?(:configure)`, is a silent no-op, and one without that guard raises `NoMethodError` (the binary opens the `RailsAiContext` namespace before the boot, so the constant exists but `configure` is not defined on it) and drops the command into the static tier. On the static tier the gem reads the literal `sensitive_patterns`, `excluded_paths` and `skip_tools` values such a file writes, without running it ([SECURITY.md](SECURITY.md#configuration)).
|
|
121
121
|
|
|
122
122
|
For in-Gemfile installs both sources apply and merge key by key - see [Precedence](CONFIGURATION.md#precedence).
|
|
123
123
|
|
data/docs/TOOLS.md
CHANGED
|
@@ -77,7 +77,7 @@ Search your codebase with regex, ripgrep acceleration, and sensitive file blocki
|
|
|
77
77
|
| Parameter | Type | Default | Description |
|
|
78
78
|
|:----------|:-----|:--------|:------------|
|
|
79
79
|
| `pattern` | string | *required* | Regex pattern to search for. With `definition` or `class`, a plain method or constant name matches literally (`valid?`), anything else as a regex (`generate_.*`) |
|
|
80
|
-
| `path` | string | - | Subdirectory or file to search (relative to Rails root); a file is searched whatever its type or ignore files say, as ripgrep does, unless it is sensitive |
|
|
80
|
+
| `path` | string | - | Subdirectory or file to search (relative to Rails root); a file is searched whatever its type or ignore files say, as ripgrep does, unless it is sensitive or over `max_file_size` (`File too large`). A path through a link inside the app is searched as the folder it leads to |
|
|
81
81
|
| `match_type` | enum | `any` | `any`, `definition` (def lines whose method name contains the pattern), `class` (class/module lines whose name contains it), `call`, `trace` |
|
|
82
82
|
| `file_type` | string | - | Filter by extension (`rb`, `erb`, `js`, etc.) |
|
|
83
83
|
| `exact_match` | boolean | `false` | Literal, whole-word match. `def reblog?` does not match `def reblog` |
|
|
@@ -566,7 +566,7 @@ ActiveSupport surface: concerns registry, deprecators, MessageVerifier/MessageEn
|
|
|
566
566
|
|
|
567
567
|
#### `env` (was `rails_get_env`)
|
|
568
568
|
|
|
569
|
-
Environment variables + credentials keys (values are never exposed). Scans `.rb`, `.rake`, ERB views and config YAML under `app`, `config` and `lib`, plus `config.ru`, `db/seeds.rb`, `db/seeds/` and the Ruby scripts in `bin/`. A tag on a YAML comment line is not read: ERB runs it, but what it writes goes with the comment. Config YAML matching `sensitive_patterns` (`config/database.yml`, `config/cable.yml`, `config/storage.yml` and the rest) is read only for the ENV names in its ERB tags, so a default there is listed as present and not read; credentials and keys are never read, and the answer says so. A default is read from a second argument or from a block whose one statement is a literal (`ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" }`). The env Kamal's `config/deploy.yml` gives the app container is listed too: secret names only, and clear values, except one holding a URL or a key-like token, or under a secret-named variable, which shows as hidden, and one an ERB tag sets, which says so. So are the setting keys in the config gem's `config/settings.yml` and `config/settings/<env>.yml` when the bundle has the config gem, and the attributes of each `Anyway::Config` class in `config/configs` or `app/configs` with the env name it reads (`PAYMENT_API_KEY`), never their values. A variable whose call sites pass different defaults is labelled as such rather than with one site's default; `detail:"full"` names each site's. A default is redacted by the rule a source literal gets: a credential format under any name, a URL's password, and under a secret-named variable a value that is not clearly something else, so an address,
|
|
569
|
+
Environment variables + credentials keys (values are never exposed). Scans `.rb`, `.rake`, ERB views and config YAML under `app`, `config` and `lib`, plus `config.ru`, `db/seeds.rb`, `db/seeds/` and the Ruby scripts in `bin/`. A tag on a YAML comment line is not read: ERB runs it, but what it writes goes with the comment. Config YAML matching `sensitive_patterns` (`config/database.yml`, `config/cable.yml`, `config/storage.yml` and the rest) is read only for the ENV names in its ERB tags, so a default there is listed as present and not read; credentials and keys are never read, and the answer says so. A default is read from a second argument or from a block whose one statement is a literal (`ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" }`). The env Kamal's `config/deploy.yml` gives the app container is listed too: secret names only, and clear values, except one holding a URL or a key-like token, or under a secret-named variable, which shows as hidden, and one an ERB tag sets, which says so. A Dockerfile `ENV` or `ARG` value, an `.env.example` value and a default in code read `[FILTERED]` when their shape can carry a secret: a URL whose user part is a key or holds a password, a webhook's address, a token in a query, a key-like run of characters, or a `BUNDLE_<HOST>` gem server credential ([Credential handling](SECURITY.md#credential-handling)). An `.env.example` value in quotes keeps a `#`, and an inline comment is hidden when it holds a credential or one of those shapes. So are the setting keys in the config gem's `config/settings.yml` and `config/settings/<env>.yml` when the bundle has the config gem, and the attributes of each `Anyway::Config` class in `config/configs` or `app/configs` with the env name it reads (`PAYMENT_API_KEY`), never their values. A variable whose call sites pass different defaults is labelled as such rather than with one site's default; `detail:"full"` names each site's. A default is redacted by the rule a source literal gets: a credential format under any name, a URL's password, and under a secret-named variable a value that is not clearly something else, so an address, a hostname or a URL of none of the shapes above prints as written.
|
|
570
570
|
|
|
571
571
|
#### `env_config` (was `rails_get_env_config`)
|
|
572
572
|
|
|
@@ -782,13 +782,13 @@ Each database other than the primary is reached through a connection handler of
|
|
|
782
782
|
|
|
783
783
|
> Disabled in production by default.
|
|
784
784
|
|
|
785
|
-
> Name the columns you need. A query is refused, with a message that says why, when it does one of these: it references a secret column (directly, or through an alias or expression; an `encrypts` column also when `query_allowed_columns` lists it); it serialises a whole row; it carries a column-alias list that renames a wildcard (a `VALUES`-body list such as `(VALUES (1,'a')) AS v(id, name)` is allowed, since it renames no table column); it uses UNION; it calls a session-effecting function, a function that returns a whole table as one value (`table_to_xml`), or any function by a quoted name; it reads a statistics, session or credential relation (`pg_stats`, `pg_stat_activity`, `pg_settings`, `performance_schema`, the `mysql` schema) or a PostgreSQL setting that can hold a password (`primary_conninfo`); it is a `SHOW` with a `WHERE` clause, `SHOW ALL` on PostgreSQL or `EXPLAIN FOR CONNECTION` on MySQL and MariaDB; or it is over 1,000,000 bytes long. `SELECT *` is allowed and its secret columns come back `[FILTERED]`. `SHOW CREATE TABLE` / `SHOW CREATE VIEW` are allowed and their definition text is secret-redacted; other `SHOW CREATE` forms are refused. In the default format a definition is shown whole, as the DDL in a fenced code block with its line breaks kept (a view's character set and collation follow as `name: value` lines); `csv` returns it as one quoted field. When the row limit holds rows back - only when the query really returned more than the cap - the answer says so in every format. With `csv`, a query that returns no rows answers the header row alone.
|
|
785
|
+
> Name the columns you need. A query is refused, with a message that says why, when it does one of these: it references a secret column (directly, or through an alias or expression; an `encrypts` column also when `query_allowed_columns` lists it); it serialises a whole row; it carries a column-alias list that renames a wildcard (a `VALUES`-body list such as `(VALUES (1,'a')) AS v(id, name)` is allowed, since it renames no table column); it uses UNION; it calls a session-effecting function, a function that returns a whole table as one value (`table_to_xml`), or any function by a quoted name; it reads a statistics, session or credential relation (`pg_stats`, `pg_stat_activity`, `pg_settings`, `performance_schema`, the `mysql` schema) or a PostgreSQL setting that can hold a password (`primary_conninfo`); it is a `SHOW` with a `WHERE` clause, `SHOW ALL` on PostgreSQL, or `EXPLAIN FOR CONNECTION` or a `PROCEDURE` clause on MySQL and MariaDB; or it is over 1,000,000 bytes long. `SELECT *` is allowed and its secret columns come back `[FILTERED]`. `SHOW CREATE TABLE` / `SHOW CREATE VIEW` are allowed and their definition text is secret-redacted; other `SHOW CREATE` forms are refused. In the default format a definition is shown whole, as the DDL in a fenced code block with its line breaks kept (a view's character set and collation follow as `name: value` lines); `csv` returns it as one quoted field. When the row limit holds rows back - only when the query really returned more than the cap - the answer says so in every format. With `csv`, a query that returns no rows answers the header row alone.
|
|
786
786
|
|
|
787
787
|
> **Cell rendering by format.** A `markdown` table cell shows `NULL` as `_NULL_`, escapes a pipe (`a\|b`) so it cannot end the cell, renders a line break as the two characters `\n` so it cannot end the row, and shortens a value over 100 characters to `…` (a `SHOW CREATE` definition is not a table cell, so it is never shortened). A `csv` cell is the value itself: `NULL` is an empty field, nothing is escaped beyond RFC 4180 quoting (a field holding a comma, a quote or a line break is wrapped in quotes with its quotes doubled), and nothing is truncated - so a line break stays a real line break inside the quoted field. Binary (non-text) data shows as `[BLOB]` in both.
|
|
788
788
|
|
|
789
789
|
### `rails_read_logs`
|
|
790
790
|
|
|
791
|
-
Reverse file tail with level filtering and sensitive data redaction.
|
|
791
|
+
Reverse file tail with level filtering and sensitive data redaction. A value an SQL statement holds inline is filtered when the column it meets is a secret, and so are the values a database error repeats ([Redaction](SECURITY.md#redaction)).
|
|
792
792
|
|
|
793
793
|
| Parameter | Type | Default | Description |
|
|
794
794
|
|:----------|:-----|:--------|:------------|
|
|
@@ -822,7 +822,7 @@ A changed migration lists the tables it affects: the table each schema method ta
|
|
|
822
822
|
|
|
823
823
|
An app in a subfolder of its repository (a monorepo's `backend/`) is reviewed on its own: its changed files, named from the app root as `rails_validate` takes them, and the commits that touched it.
|
|
824
824
|
|
|
825
|
-
Each diff
|
|
825
|
+
Each diff line is shown as the line of its side, redacted with the whole of that side the way every tool's source is, so a secret a change adds or takes out reads `[FILTERED]`, and so does a key's body in a hunk that opens inside the key, in a hunk header's line too. A changed file on `sensitive_patterns` (`config/database.yml`, `.env`, a key) is listed with its line counts and none of its lines, and so is one whose diff is over `max_file_size` and a merge conflict's combined diff. The diff is read with the repository's colour, external diff and textconv settings off, and a submodule's change shows as the commits it moves between.
|
|
826
826
|
|
|
827
827
|
| Parameter | Type | Default | Description |
|
|
828
828
|
|:----------|:-----|:--------|:------------|
|
|
@@ -221,6 +221,8 @@ module RailsAiContext
|
|
|
221
221
|
RailsAiContext.static_kind = kind
|
|
222
222
|
RailsAiContext.configuration.app_root = root
|
|
223
223
|
Configuration.auto_load!(root)
|
|
224
|
+
# No initializer runs here, so the security settings one writes are read from its source.
|
|
225
|
+
InitializerSettings.apply!(root)
|
|
224
226
|
# A failed boot's reason is already the line above.
|
|
225
227
|
messages << "[rails-ai-context] static tier active#{": #{reason}" unless kind == :boot_failed}"
|
|
226
228
|
Outcome.new(tier: :static, reason: reason, kind: kind, messages: messages)
|
|
@@ -1529,8 +1529,11 @@ module RailsAiContext
|
|
|
1529
1529
|
nil
|
|
1530
1530
|
end
|
|
1531
1531
|
|
|
1532
|
+
# Read as SafePath.sensitive? reads a pattern, so a file the tools refuse lands in the list that names it,
|
|
1533
|
+
# an engine's or a test/dummy app's included.
|
|
1532
1534
|
def matches_any?(patterns, file)
|
|
1533
|
-
|
|
1535
|
+
basename = File.basename(file)
|
|
1536
|
+
patterns.any? { |pattern| SafePath.compile_pattern(pattern).match(file, basename, file.downcase, basename.downcase) }
|
|
1534
1537
|
end
|
|
1535
1538
|
|
|
1536
1539
|
# Every file the tools refuse to read (the one sensitive-pattern list), plus the Codex
|