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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8498e25402e035ed53ab2c0dff9dff31da4c849e616632069891de80f38ff7f4
4
- data.tar.gz: 92dac2339bbe11cc0486ad449f8b84fd5fe840d15c31e56414b53b9b0d7e9fbc
3
+ metadata.gz: 8cd401837a164e83cf1558e5ff9ed491e0409974c9ffae9d4b648e56b4e4fb68
4
+ data.tar.gz: 1667e185eba134e006450acd3edf19ffe1e590b3a6033608db25ad60e529e87a
5
5
  SHA512:
6
- metadata.gz: d06dccf666aa9a2ca94e8a42f5191a9d7cf98f2a8f9799f6ab0afa941c6d4e397ecefe5aa71979bb91d6f667151345b161795bd81058a4e4810d5f29828e64eb
7
- data.tar.gz: 324d72633467fc133a608d848fa3dce1f3f21cc600ab0a9d637288a5ba05f1f69c356aaf3a2871a5217a5c4498787a279695554d8c08ff80af303ba1a8857984
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
@@ -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
@@ -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, URL or hostname default prints as written.
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 passes through the redaction every tool's source does, read as the file it comes from, so a secret a change adds or takes out reads `[FILTERED]`, 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.
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
- patterns.any? { |pattern| File.fnmatch(pattern, file, File::FNM_PATHNAME | File::FNM_DOTMATCH) }
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