rails-ai-context 5.7.1 → 5.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +97 -0
- data/README.md +2 -2
- data/docs/CONFIGURATION.md +1 -0
- data/docs/FAQ.md +1 -1
- data/docs/GUIDE.md +4 -0
- data/docs/STANDALONE.md +3 -0
- data/docs/TOOLS.md +1 -1
- data/docs/social-preview.html +1 -1
- data/exe/rails-ai-context +44 -6
- data/lib/generators/rails_ai_context/install/install_generator.rb +4 -0
- data/lib/rails_ai_context/configuration.rb +34 -3
- data/lib/rails_ai_context/fingerprinter.rb +34 -7
- data/lib/rails_ai_context/instrumentation.rb +46 -3
- data/lib/rails_ai_context/introspectors/auth_introspector.rb +62 -4
- data/lib/rails_ai_context/introspectors/convention_introspector.rb +26 -0
- data/lib/rails_ai_context/introspectors/gem_introspector.rb +1 -0
- data/lib/rails_ai_context/introspectors/job_introspector.rb +93 -3
- data/lib/rails_ai_context/introspectors/model_introspector.rb +2 -1
- data/lib/rails_ai_context/introspectors/schema_introspector.rb +15 -1
- data/lib/rails_ai_context/serializers/tool_guide_helper.rb +5 -1
- data/lib/rails_ai_context/tools/analyze_feature.rb +45 -19
- data/lib/rails_ai_context/tools/base_tool.rb +20 -23
- data/lib/rails_ai_context/tools/get_edit_context.rb +18 -3
- data/lib/rails_ai_context/tools/get_job_pattern.rb +69 -12
- data/lib/rails_ai_context/tools/get_partial_interface.rb +16 -5
- data/lib/rails_ai_context/tools/get_view.rb +24 -5
- data/lib/rails_ai_context/tools/migration_advisor.rb +68 -0
- data/lib/rails_ai_context/tools/query.rb +100 -0
- data/lib/rails_ai_context/tools/search_code.rb +1 -1
- data/lib/rails_ai_context/tools/validate.rb +33 -6
- data/lib/rails_ai_context/version.rb +1 -1
- data/lib/rails_ai_context/vfs.rb +24 -5
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8b8c8f1c7133f9f97b35975be1ecf1485e6fd663867c7fb6ae776af131f972ec
|
|
4
|
+
data.tar.gz: 2a4e374b3509f1eca37ddeefa730eb8fc181a7710df616c7a0ab389babce7280
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a6c3bed1bd469fbee71f6395ad2f7ac0fc90347fdf2bf8066f36f8396a99a915f5954aa207207cabf9b073c435e2fbe7849692e28ba2c0ab752ef516cc8b32e0
|
|
7
|
+
data.tar.gz: 16791ed1aa9293f5d87d61bd1a63c3fca990cdf25da6f06639ba514912e097f69a177f9e829b14684fbe2242b8ab53f4828f5b2ad7c15f485a1e5c2493152c4b
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,103 @@ 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.8.1] — 2026-04-15
|
|
9
|
+
|
|
10
|
+
### Fixed — Security Hardening
|
|
11
|
+
|
|
12
|
+
Four exploitable vulnerabilities across `rails_query`, the VFS URI dispatcher, and the instrumentation bridge, plus six defense-in-depth hardening issues. All discovered by security and deep code-review passes conducted during v5.8.1 pre-release verification. None were known at the v5.8.0 release — **users should upgrade immediately**.
|
|
13
|
+
|
|
14
|
+
- **SQL column-aliasing redaction bypass (exploitable).** Post-execution redaction in `rails_query` operated on `result.columns` (the DB-returned column names), which the caller controls via aliases and expressions. `SELECT password_digest AS x FROM users` returned raw bcrypt hashes. Same for `SELECT substring(password_digest, 1, 60) FROM users` (column named `substring`), `SELECT md5(session_data) FROM sessions`, `SELECT CASE WHEN id > 0 THEN password_digest END FROM users`, and subqueries that re-project the sensitive column. **Fix:** moved enforcement to pre-execution in `validate_sql`. Any query that textually references a column name in `config.query_redacted_columns` OR the hard-coded `SENSITIVE_COLUMN_SUFFIXES` list (password_digest, encrypted_password, password_hash, reset_password_token, api_key, refresh_token, otp_secret, session_data, secret_key, private_key, etc.) is now rejected. Users with a legitimately non-sensitive column matching one of these names can subtract from `config.query_redacted_columns` in an initializer. **8 bypass scenarios covered by new specs.**
|
|
15
|
+
|
|
16
|
+
- **Arbitrary filesystem read via database functions (exploitable).** `rails_query` did not block PostgreSQL's `pg_read_file`, `pg_read_binary_file`, `pg_ls_dir`, `pg_stat_file`, `lo_import`/`lo_export`, `dblink`, MySQL's `LOAD_FILE`, `SELECT ... INTO OUTFILE/DUMPFILE`, or SQLite's `load_extension`. These are SELECT-callable (so they pass the `BLOCKED_KEYWORDS` scanner and `SET TRANSACTION READ ONLY`) but give the caller a filesystem and shared-library-load primitive — completely bypassing the gem's `sensitive_patterns` allowlist by pivoting through the database process. PoC: `SELECT pg_read_file('/etc/passwd')`, `SELECT pg_read_file('config/master.key')`. **Fix:** added a `BLOCKED_FUNCTIONS` regex and `BLOCKED_OUTPUT` pattern that reject any query referencing these built-ins. **10 function-specific specs added.**
|
|
17
|
+
|
|
18
|
+
- **`sensitive_patterns` default list expanded.** The v5.8.0 default list covered `.env`, `.env.*`, `config/master.key`, `config/credentials*.yml.enc`, `*.pem`, `*.key` but missed common secret locations. v5.8.1 adds `config/database.yml`, `config/secrets.yml`, `config/cable.yml`, `config/storage.yml`, `config/mongoid.yml`, `config/redis.yml`, `*.p12`, `*.pfx`, `*.jks`, `*.keystore`, `**/id_rsa`, `**/id_ed25519`, `**/id_ecdsa`, `**/id_dsa`, `.ssh/*`, `.aws/credentials`, `.aws/config`, `.netrc`, `.pgpass`, `.my.cnf`.
|
|
19
|
+
|
|
20
|
+
- **`get_edit_context` now re-checks `sensitive_file?` after realpath resolution.** The initial check ran on the caller-supplied string; a symlink inside `app/models/` pointing at `config/master.key` previously passed the basename check and fell through to `File.read`. The post-realpath check blocks this.
|
|
21
|
+
|
|
22
|
+
- **`validate` now enforces `sensitive_file?`.** The validate tool had no sensitive-file check at all. Even though its output is limited to error messages (not raw content), it still leaked file existence/size and ran readers on secret files. Now denied with an `access denied (sensitive file)` error.
|
|
23
|
+
|
|
24
|
+
- **`BaseTool.sensitive_file?` has direct spec coverage for the first time.** The security boundary behind every file-accepting tool had zero direct tests in v5.8.0 — 36 new specs added covering the Rails secret locations, the v5.8.1 expanded pattern list, private keys and certificates, case-insensitivity, basename-only matching, and custom pattern configurations.
|
|
25
|
+
|
|
26
|
+
- **VFS `resolve_view` sibling-directory path traversal (exploitable).** The `rails-ai-context://views/{path}` URI resolver used bare `String#start_with?` on the realpath without a `File::SEPARATOR` suffix check. `/app/views_spec/secret.erb` matched `/app/views` as a prefix, so a symlink inside `app/views/` pointing at a sibling directory escaped containment and returned arbitrary file content. **Fix:** changed the containment check to `real == base || real.start_with?(base + File::SEPARATOR)`. Also added a `sensitive_file?` realpath check mirroring the v5.8.1 `get_edit_context` fix, so `.env`/`.key` symlinks inside `app/views/` are rejected. **2 new regression specs covering both PoCs.**
|
|
27
|
+
|
|
28
|
+
- **Instrumentation bridge leaks raw tool arguments to ActiveSupport::Notifications subscribers (exploitable).** `Instrumentation.callback` forwarded the MCP SDK's full data hash to `ActiveSupport::Notifications.instrument`. The SDK's `add_instrumentation_data(tool_name:, tool_arguments:)` includes raw tool inputs — so every Rails observability subscriber (Datadog, Scout, New Relic, custom loggers) received `rails_query`'s raw SQL, `rails_get_env`'s env var names, and `rails_read_logs`'s search patterns unredacted. The response-side redaction each of those tools carefully implements did nothing for the request side. **Fix:** introduced `Instrumentation::SAFE_KEYS` (`method`, `tool_name`, `duration`, `error`, `resource_uri`, `prompt_name`) — only those fields are forwarded. Users who need arguments in observability can set `config.instrumentation_include_arguments = true` in an initializer (taking on the redaction obligation). **3 new regression specs.**
|
|
29
|
+
|
|
30
|
+
- **Instrumentation subscriber failures could crash tool calls (exploitable).** The MCP SDK's `instrument_call` invokes our callback from an `ensure` block. Any exception raised inside the callback (e.g. a custom subscriber bug, a Datadog client losing connection) would propagate out of `ensure` and overwrite the tool's actual return value — effectively failing every tool call whenever any subscriber was broken. **Fix:** wrapped the `Notifications.instrument` call in a `rescue => e` block. Subscriber failures now log to stderr under `DEBUG=1` instead of corrupting tool responses. **1 new regression spec.**
|
|
31
|
+
|
|
32
|
+
- **`analyze_feature` caps per-directory file scans at 500 files.** `discover_services`, `discover_jobs`, and `discover_views` previously ran unbounded `Dir.glob` + `SafeFile.read` on every match, which on large monorepos could read thousands of files per call. Matches the existing cap used by `discover_tests`. Tool output notes when the cap was hit so the AI agent knows to narrow its feature keyword.
|
|
33
|
+
|
|
34
|
+
### Added — Configuration
|
|
35
|
+
|
|
36
|
+
- **`config.instrumentation_include_arguments`** (default `false`) — controls whether raw tool arguments are forwarded to `ActiveSupport::Notifications` subscribers. See the Security Hardening note above for the opt-in risk.
|
|
37
|
+
|
|
38
|
+
### Performance — Hot-Path Optimization
|
|
39
|
+
|
|
40
|
+
- **`cached_context` TTL short-circuit.** The hot path of every tool call ran `Fingerprinter.changed?` on every hit, which walks every `*.{rb,rake,js,ts,erb,haml,slim,yml}` file in `WATCHED_DIRS` plus (for path:-installed users) every file in the gem's own lib/ tree — doing an `mtime` stat per file. Measured at ~12ms per call in dev-mode path installs, ~0.5ms in production. Since LiveReload fires `reset_all_caches!` on actual file-change events, stale-cache risk during a short TTL window is already covered. **Fix:** skip the fingerprint check entirely when within the TTL window. When TTL expires, re-fingerprint; if unchanged, bump the timestamp and reuse the cached context (avoiding a 31-introspector re-run).
|
|
41
|
+
|
|
42
|
+
- **Fingerprinter gem-lib scan memoized.** For users who install the gem via `path:` (common for gem contributors, monorepos, the standalone dev workflow), the fingerprinter was walking 123 gem-lib files on every tool call. Memoized at class level with a `reset_gem_lib_fingerprint!` hook that `BaseTool.reset_cache!` and LiveReload invoke.
|
|
43
|
+
|
|
44
|
+
- **Measured result:** `cached_context` hot-path benchmark dropped from **11.77ms to 0.199ms** per call — a **~59x speedup** on dev-mode path installs. In-Gemfile / production users see a smaller but still meaningful improvement (0.77ms → 0.199ms).
|
|
45
|
+
|
|
46
|
+
### Fixed — schema.rb empty-file wrinkle
|
|
47
|
+
|
|
48
|
+
- `SchemaIntrospector#static_schema_parse` returned `{ error: "No db/schema.rb, db/structure.sql, or migrations found" }` when `db/schema.rb` existed but contained zero `create_table` calls (common on freshly-created Rails apps between `db:create` and the first migration). Now returns `{ total_tables: 0, tables: {}, note: "Schema file exists but is empty — no migrations have been run yet..." }`.
|
|
49
|
+
|
|
50
|
+
### Changed — CI release matrix synced to PR matrix
|
|
51
|
+
|
|
52
|
+
- `.github/workflows/release.yml` test matrix was still on the old Ruby `3.2/3.3/3.4` × Rails `7.1/7.2/8.0` grid even though `ci.yml` was expanded to cover Ruby 4.0 and Rails 8.1 in v5.8.0. Now synced — release-time testing matches PR-time testing across all 12 combos, including the #69 reporter's environment (Ruby 4.0.2 + Rails 8.1.3).
|
|
53
|
+
|
|
54
|
+
### Fixed — Pre-release review pass (rounds 2–3)
|
|
55
|
+
|
|
56
|
+
Five additional issues found during multi-round cold-eyes security and correctness review after the initial hardening pass.
|
|
57
|
+
|
|
58
|
+
- **`search_code` sibling-directory path traversal.** `rails_search_code`'s `path` parameter used `real_search.start_with?(real_root)` without a `File::SEPARATOR` suffix — the same bypass class as the original VFS C1 bug. A Rails root of `/app/myapp` would accept a search path whose realpath is `/app/myapp_evil`. **Fix:** changed to `real_search == real_root || real_search.start_with?(real_root + File::SEPARATOR)`. Spec added.
|
|
59
|
+
|
|
60
|
+
- **Instrumentation callback: `data[:method]` extraction outside `begin/rescue`.** Two lines before the `begin` block (`method = data[:method]` and `event_name = ...`) were not covered by the rescue. A non-Hash `data` argument from the MCP SDK would raise `NoMethodError` which would propagate into the SDK's `ensure` context and overwrite the tool's return value. **Fix:** moved `begin` to wrap the full lambda body after the early-exit guard.
|
|
61
|
+
|
|
62
|
+
- **`get_partial_interface` TOCTOU gap (residual from initial hardening).** `resolve_partial_path` performed the `File.realpath` security check internally but returned the original glob `found` path to the caller. The caller then called `File.size(found)` and `safe_read(found)` — creating a sub-millisecond race window where a symlink swap could read from a path that bypassed the check. **Fix:** `resolve_partial_path` now returns `real_found`. All file operations in the caller use the pre-checked realpath.
|
|
63
|
+
|
|
64
|
+
- **`validate` tool passed pre-realpath path to validators.** `validate_ruby`, `validate_erb`, `validate_javascript`, and `check_rails_semantics` all received `full_path` (pre-realpath) after the security check resolved `real`. **Fix:** all four now receive `Pathname.new(real)`.
|
|
65
|
+
|
|
66
|
+
- **`rails_query` `LOAD DATA INFILE` not explicitly blocked.** Added `LOAD\s+DATA` to `BLOCKED_FUNCTIONS`. Belt-and-suspenders: `ALLOWED_PREFIX` already blocks it at statement level, but the explicit entry makes intent self-documenting. Two specs added (`LOAD DATA INFILE` and `LOAD DATA LOCAL INFILE`).
|
|
67
|
+
|
|
68
|
+
### Test coverage
|
|
69
|
+
|
|
70
|
+
- **2004 examples, 0 failures** (was 1928 in v5.8.0, +76 new regression tests across the security + hardening + empty-schema + VFS + instrumentation + review-pass fixes).
|
|
71
|
+
|
|
72
|
+
## [5.8.0] — 2026-04-14
|
|
73
|
+
|
|
74
|
+
### Added — Modern Rails Coverage Pass
|
|
75
|
+
|
|
76
|
+
Five targeted gaps in modern Rails introspection, identified by an audit of the introspectors against current Rails 7/8 patterns. Net result: the gem now surfaces what AI agents need to know about Rails 8 built-in auth, Solid Errors, async query usage, strong_migrations safety, and Action Cable channel detail.
|
|
77
|
+
|
|
78
|
+
- **Rails 8 built-in auth depth.** `auth_introspector#detect_authentication` previously detected `bin/rails generate authentication` only as a boolean. Now returns a hash with the Authentication concern path, the Sessions/Passwords controller paths, and a per-controller list of `allow_unauthenticated_access` filters with their `only:`/`except:` scope. Each declaration in a file yields its own entry (a controller with both `only:` and `except:` is captured fully, not collapsed to the first match), and trailing line comments are stripped from the captured scope. AI agents can answer "which controllers are public?" in one tool call.
|
|
79
|
+
- **Solid Errors gem detection.** Added `solid_errors` (Rails 8 database-backed error tracking, by @fractaledmind) to `gem_introspector.rb`'s `NOTABLE_GEMS` map under the `:monitoring` category. Was the only Solid-* gem missing from the list (`solid_queue`, `solid_cache`, `solid_cable` were already covered). `solid_health` is NOT a real published gem — Rails 8 ships a built-in `/up` healthcheck endpoint with no gem needed.
|
|
80
|
+
- **Async query pattern detection.** `convention_introspector#detect_patterns` now adds `async_queries` to the patterns array when it finds `load_async` or any of the `async_count`/`async_sum`/`async_minimum`/`async_maximum`/`async_average`/`async_pluck`/`async_ids`/`async_exists`/`async_find_by`/`async_find`/`async_first`/`async_last`/`async_take` calls in `app/controllers`, `app/services`, `app/jobs`, or `app/models`. Comment-only references (e.g. `# TODO: bring back load_async`) are skipped to avoid false positives. AI agents can recognize the perf optimization is in use without re-scanning.
|
|
81
|
+
- **Strong Migrations integration.** `migration_advisor` now emits a `## Strong Migrations Warnings` section when the `strong_migrations` gem is in `Gemfile.lock`. Catalog covers the most common breaking-change patterns: `remove_column` (needs `safety_assured` + `ignored_columns` first), `rename_column` (unsafe under load, two-step pattern), `change_column` type change (table rewrite), `add_index` without `algorithm: :concurrently` (Postgres write lock), `add_foreign_key` without `validate: false` (lock validation), and `add_column` with `null: false` but no default (table rewrite). Each warning includes the safer pattern. Fires only when the gem is detected — zero noise for projects that don't use it.
|
|
82
|
+
- **Action Cable channel detail.** `job_introspector#extract_channels` was returning `{ name, stream_methods }` only. Enriched to also extract `identified_by` attributes, `stream_from`/`stream_for` targets, `periodically` timers with their full intervals (including lambdas like `every: -> { current_user.interval }`), RPC action methods (excluding subscribed/unsubscribed/stream_*), and the source file path. **`get_job_pattern` now renders an "Action Cable Channels" section with all of these fields**, so AI agents calling the tool actually see the data instead of just the channel name. Also added `eager_load_channels!` to `JobIntrospector` so the channel set is populated in development mode (where `config.eager_load = false` and `ActionCable::Channel::Base.descendants` is otherwise empty until a client subscribes).
|
|
83
|
+
|
|
84
|
+
### Changed — CI matrix expanded to cover Ruby 4.0 + Rails 8.1
|
|
85
|
+
|
|
86
|
+
- Added Ruby `4.0` and Rails `8.1` to the GitHub Actions test matrix. Net jobs: 12 (was 8). Excludes the unsupported combinations: Ruby 3.2 × Rails 8.x (Rails 8 needs 3.3+) and Ruby 4.0 × Rails 7.x (Rails 7 has no Ruby 4 support). Verified locally that the full spec suite passes on Ruby 4.0.2 + Rails 8.1.3 (the environment in #69) — 1925 examples, 0 failures, rubocop clean across all 282 source files.
|
|
87
|
+
|
|
88
|
+
### Fixed — Standalone Install Path Crashed Inside Bundler-Backed Rails Apps
|
|
89
|
+
|
|
90
|
+
- **`rails-ai-context` installed via `gem install` (standalone path) crashed on every tool call** when run inside a Rails app that has its own `Gemfile`. Root cause: `boot_rails!` in `exe/rails-ai-context` calls `require config/environment.rb` which runs `Bundler.setup`, which strips `Gem.loaded_specs` to only the app's Gemfile-resolved gems. The MCP SDK reads `Gem.loaded_specs["json-schema"].full_gem_path` at tool-call time (`mcp/tool/schema.rb:45`) — but `json-schema` is a transitive dep of `mcp`, not in the app's Gemfile, so the lookup nils and crashes with `NoMethodError: undefined method 'full_gem_path' for nil`.
|
|
91
|
+
- **Fix:** added `restore_standalone_gem_specs` to `exe/rails-ai-context` which re-registers `mcp`, `json-schema`, and a couple of their transitive deps in `Gem.loaded_specs` after `Bundler.setup` runs. No-op in in-Gemfile mode (the specs are already registered). This was a pre-existing bug that was discovered during v5.8.0 pre-release E2E verification — affected v5.4.0 onward.
|
|
92
|
+
|
|
93
|
+
### Fixed — MCP Tool Responses Rejected by Strict Clients (#69)
|
|
94
|
+
|
|
95
|
+
- **Removed default `output_schema` from all 38 tools.** Since v5.4.0, `BaseTool.inherited` automatically assigned a `DEFAULT_OUTPUT_SCHEMA` to every tool. The schema described the response wire envelope (`{content: [...]}`) rather than app-level structured data, and tools never returned matching `structured_content`. Per MCP spec, when a tool declares `outputSchema`, it MUST return `structuredContent` matching it. Strict MCP clients (e.g. Copilot CLI) reject responses that don't, with `MCP error -32600: Tool ... has an output schema but did not return structured content`. Lenient clients (Claude Code, Cursor) silently ignored the missing field, which is why the bug went unnoticed since v5.4.0.
|
|
96
|
+
- **Why this happened.** The MCP Ruby SDK does not enforce `output_schema` server-side (no `validate_result` call in `MCP::Server`), so the test suite passed end-to-end. Validation happens client-side, and only strict clients caught it. Reported by @pardeyke.
|
|
97
|
+
- **What changed.** Deleted `DEFAULT_OUTPUT_SCHEMA` constant and the `inherited` hook line that set it (`lib/rails_ai_context/tools/base_tool.rb`). Tools now ship with no `outputSchema` by default — matching what they actually return (text-only). Individual tools can still declare their own `output_schema` via the MCP::Tool DSL, provided they also return matching `structured_content`.
|
|
98
|
+
- **Regression spec added.** `spec/lib/rails_ai_context/tools_spec.rb` now asserts (a) no tool advertises a default `outputSchema`, and (b) any tool that *does* declare one must also have `structured_content:` in its source — preventing the v5.4.0 misuse from sneaking back in.
|
|
99
|
+
- **Future enhancement.** Per-tool structured output (returning parseable JSON alongside the Markdown text via `structured_content:`) is a future feature for tools where it adds value (`get_schema`, `get_routes`, etc.). Out of scope for this patch.
|
|
100
|
+
|
|
101
|
+
### Added — Framework Association Noise Filter
|
|
102
|
+
|
|
103
|
+
- **`excluded_association_names` config option** — filters framework-generated associations (ActiveStorage, ActionText, ActionMailbox, Noticed) from model introspection output. 7 association names excluded by default. Configurable via initializer (`config.excluded_association_names += %w[...]`) or YAML. Closes #57.
|
|
104
|
+
|
|
8
105
|
## [5.7.1] — 2026-04-09
|
|
9
106
|
|
|
10
107
|
### Changed — SLOP Cleanup
|
data/README.md
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
<br>
|
|
22
22
|
[](https://github.com/crisnahine/rails-ai-context)
|
|
23
23
|
[](https://github.com/crisnahine/rails-ai-context)
|
|
24
|
-
[](https://github.com/crisnahine/rails-ai-context/actions)
|
|
25
25
|
[](LICENSE)
|
|
26
26
|
|
|
27
27
|
</div>
|
|
@@ -543,7 +543,7 @@ end
|
|
|
543
543
|
## About
|
|
544
544
|
|
|
545
545
|
Built by a Rails developer with 10+ years of production experience.<br>
|
|
546
|
-
|
|
546
|
+
1989 tests. 38 tools. 5 resource templates. 31 introspectors. Standalone or in-Gemfile.<br>
|
|
547
547
|
MIT licensed. [Contributions welcome.](CONTRIBUTING.md)
|
|
548
548
|
|
|
549
549
|
<br>
|
data/docs/CONFIGURATION.md
CHANGED
|
@@ -94,6 +94,7 @@ preset: full
|
|
|
94
94
|
| `excluded_filters` | Array | 3 framework filters | Controller filters to skip |
|
|
95
95
|
| `excluded_middleware` | Array | 24 framework middleware | Middleware to skip in listing |
|
|
96
96
|
| `excluded_paths` | Array | `["node_modules", "tmp", "log", "vendor", ".git", "doc", "docs"]` | Paths excluded from search |
|
|
97
|
+
| `excluded_association_names` | Array | 7 framework associations | Association names to hide from model output |
|
|
97
98
|
| `excluded_concerns` | Array of Regex | Framework concerns | Concerns to skip (supports regex) |
|
|
98
99
|
|
|
99
100
|
### File Size Limits
|
data/docs/FAQ.md
CHANGED
|
@@ -81,7 +81,7 @@ config.skip_tools = %w[rails_security_scan rails_query]
|
|
|
81
81
|
|
|
82
82
|
### What's the `detail` parameter?
|
|
83
83
|
|
|
84
|
-
|
|
84
|
+
Individual lookup tools accept `detail`: `summary` (compact), `standard` (default), `full` (everything). Start with summary and drill down as needed. This keeps AI context windows lean. Composite tools (`rails_get_context`, `rails_analyze_feature`) do not accept `detail` — they always return their full bundled output.
|
|
85
85
|
|
|
86
86
|
### What are `[VERIFIED]` and `[INFERRED]` tags?
|
|
87
87
|
|
data/docs/GUIDE.md
CHANGED
|
@@ -1243,6 +1243,9 @@ if defined?(RailsAiContext)
|
|
|
1243
1243
|
# Route prefixes hidden with app_only (e.g. admin frameworks)
|
|
1244
1244
|
# config.excluded_route_prefixes += %w[admin/]
|
|
1245
1245
|
|
|
1246
|
+
# Framework association names hidden from model output (ActiveStorage, ActionText, etc.)
|
|
1247
|
+
# config.excluded_association_names += %w[my_custom_framework_assoc]
|
|
1248
|
+
|
|
1246
1249
|
# Regex patterns for concerns to hide from model output
|
|
1247
1250
|
# config.excluded_concerns += [/MyInternal::/]
|
|
1248
1251
|
|
|
@@ -1342,6 +1345,7 @@ end
|
|
|
1342
1345
|
| `max_validate_files` | Integer | `50` | Max files per validate call |
|
|
1343
1346
|
| `excluded_controllers` | Array | `DeviseController`, etc. | Controller classes hidden from listings |
|
|
1344
1347
|
| `excluded_route_prefixes` | Array | `action_mailbox/`, `active_storage/`, etc. | Route controller prefixes hidden with `app_only` |
|
|
1348
|
+
| `excluded_association_names` | Array | 7 framework associations | Framework association names hidden from model output |
|
|
1345
1349
|
| `excluded_concerns` | Array | framework regex patterns | Regex patterns for concerns to hide from model output |
|
|
1346
1350
|
| `excluded_filters` | Array | `verify_authenticity_token`, etc. | Framework filter names hidden from controller output |
|
|
1347
1351
|
| `excluded_middleware` | Array | standard Rails middleware | Default middleware hidden from config output |
|
data/docs/STANDALONE.md
CHANGED
data/docs/TOOLS.md
CHANGED
|
@@ -43,7 +43,7 @@ Tool name resolution is flexible — all of these work:
|
|
|
43
43
|
| `get_schema` | `rails_get_schema` |
|
|
44
44
|
| `rails_get_schema` | `rails_get_schema` |
|
|
45
45
|
|
|
46
|
-
|
|
46
|
+
Individual lookup tools accept a **`detail`** parameter: `summary` (compact), `standard` (default), or `full` (everything). Start with summary, drill down as needed. Composite tools (`rails_get_context`, `rails_analyze_feature`) do not accept `detail`.
|
|
47
47
|
|
|
48
48
|
<p align="right"><a href="#table-of-contents">↑ back to top</a></p>
|
|
49
49
|
|
data/docs/social-preview.html
CHANGED
data/exe/rails-ai-context
CHANGED
|
@@ -414,6 +414,12 @@ class RailsAiContextCLI < Thor
|
|
|
414
414
|
(result[:skipped] || []).each { |f| $stderr.puts " Skipped: #{f} (unchanged)" }
|
|
415
415
|
end
|
|
416
416
|
|
|
417
|
+
# Gems that the MCP SDK reads from `Gem.loaded_specs` at tool-call time
|
|
418
|
+
# (specifically `Gem.loaded_specs["json-schema"].full_gem_path` in
|
|
419
|
+
# mcp/tool/schema.rb). Bundler.setup strips them in standalone mode so we
|
|
420
|
+
# capture references BEFORE Rails boot runs and re-register them afterwards.
|
|
421
|
+
STANDALONE_REQUIRED_GEMS = %w[mcp json-schema addressable public_suffix].freeze
|
|
422
|
+
|
|
417
423
|
def boot_rails!
|
|
418
424
|
config_path = File.join(Dir.pwd, "config", "environment.rb")
|
|
419
425
|
unless File.exist?(config_path)
|
|
@@ -426,22 +432,54 @@ class RailsAiContextCLI < Thor
|
|
|
426
432
|
# Bundler.setup (in config/boot.rb) strips $LOAD_PATH to Gemfile-only gems.
|
|
427
433
|
# In standalone mode (gem not in Gemfile), this removes our gem and mcp paths.
|
|
428
434
|
# We restore them after boot so lazy requires (mcp, etc.) still resolve.
|
|
429
|
-
#
|
|
430
|
-
# We do NOT require mcp or rails_ai_context before boot — that would activate
|
|
431
|
-
# gem versions that conflict with Bundler's resolution (e.g., bigdecimal).
|
|
432
|
-
# Instead, paths are restored and gems load lazily using Bundler-resolved deps.
|
|
433
435
|
pre_boot_paths = $LOAD_PATH.dup
|
|
434
436
|
|
|
437
|
+
# Capture spec references BEFORE Bundler.setup runs. After Bundler.setup,
|
|
438
|
+
# Gem::Specification.find_by_name is filtered to only Gemfile-resolved gems
|
|
439
|
+
# and our transitive deps (mcp, json-schema) become invisible — find_by_name
|
|
440
|
+
# raises Gem::MissingSpecError. Capturing first means we hold real spec
|
|
441
|
+
# objects we can splice back in after boot. No-op in in-Gemfile mode (the
|
|
442
|
+
# captured specs already exist in Gem.loaded_specs and our `||=` skips them).
|
|
443
|
+
pre_boot_specs = capture_standalone_specs
|
|
444
|
+
|
|
435
445
|
require config_path
|
|
436
446
|
|
|
437
|
-
# Restore paths that Bundler.setup stripped (standalone mode).
|
|
438
|
-
# For Gemfile users,
|
|
447
|
+
# Restore paths and specs that Bundler.setup stripped (standalone mode).
|
|
448
|
+
# For Gemfile users, both restorations are no-ops.
|
|
439
449
|
(pre_boot_paths - $LOAD_PATH).each { |p| $LOAD_PATH << p }
|
|
450
|
+
pre_boot_specs.each { |name, spec| Gem.loaded_specs[name] ||= spec }
|
|
451
|
+
|
|
452
|
+
# Loud warning if any required spec couldn't be restored — silent failure
|
|
453
|
+
# here means tools will crash later with `undefined method 'full_gem_path'
|
|
454
|
+
# for nil` from inside the MCP SDK.
|
|
455
|
+
missing = STANDALONE_REQUIRED_GEMS.reject { |g| Gem.loaded_specs.key?(g) }
|
|
456
|
+
if missing.any? && !ENV["BUNDLE_BIN_PATH"]
|
|
457
|
+
$stderr.puts "[rails-ai-context] WARNING: standalone CLI could not restore gemspec(s): #{missing.join(', ')}."
|
|
458
|
+
$stderr.puts "[rails-ai-context] Tool calls may crash with `undefined method 'full_gem_path' for nil`."
|
|
459
|
+
$stderr.puts "[rails-ai-context] Try `gem install rails-ai-context` to ensure all transitive deps are installed."
|
|
460
|
+
end
|
|
440
461
|
|
|
441
462
|
require "rails_ai_context"
|
|
442
463
|
|
|
443
464
|
RailsAiContext::Configuration.auto_load!
|
|
444
465
|
end
|
|
466
|
+
|
|
467
|
+
# Look up each required gem via Gem::Specification.find_by_name BEFORE
|
|
468
|
+
# Bundler.setup runs and filters the spec registry. Returns a hash of
|
|
469
|
+
# {gem_name => Gem::Specification}. In in-Gemfile mode the lookups still
|
|
470
|
+
# succeed but the result is unused (Bundler keeps these specs registered).
|
|
471
|
+
def capture_standalone_specs
|
|
472
|
+
return {} unless defined?(Gem) && Gem.respond_to?(:loaded_specs)
|
|
473
|
+
|
|
474
|
+
STANDALONE_REQUIRED_GEMS.each_with_object({}) do |gem_name, acc|
|
|
475
|
+
spec = Gem::Specification.find_by_name(gem_name)
|
|
476
|
+
acc[gem_name] = spec if spec
|
|
477
|
+
rescue Gem::MissingSpecError, Gem::LoadError
|
|
478
|
+
# The gem is not installed at all (not just Bundler-filtered). Skip
|
|
479
|
+
# silently — the downstream `require` produces a clearer error.
|
|
480
|
+
next
|
|
481
|
+
end
|
|
482
|
+
end
|
|
445
483
|
end
|
|
446
484
|
|
|
447
485
|
RailsAiContextCLI.start(ARGV)
|
|
@@ -203,6 +203,10 @@ module RailsAiContext
|
|
|
203
203
|
# Models to exclude from introspection
|
|
204
204
|
# config.excluded_models += %w[AdminUser InternalThing]
|
|
205
205
|
|
|
206
|
+
# Framework association names hidden from model output
|
|
207
|
+
# (ActiveStorage, ActionText, ActionMailbox, Noticed associations are excluded by default)
|
|
208
|
+
# config.excluded_association_names += %w[my_custom_framework_assoc]
|
|
209
|
+
|
|
206
210
|
# Controllers to exclude from listings
|
|
207
211
|
# config.excluded_controllers += %w[Admin::BaseController]
|
|
208
212
|
|
|
@@ -17,13 +17,14 @@ module RailsAiContext
|
|
|
17
17
|
server_name cache_ttl max_tool_response_chars
|
|
18
18
|
live_reload live_reload_debounce auto_mount http_path http_bind http_port
|
|
19
19
|
output_dir skip_tools excluded_models excluded_controllers
|
|
20
|
-
excluded_route_prefixes excluded_filters excluded_middleware excluded_paths
|
|
20
|
+
excluded_route_prefixes excluded_filters excluded_middleware excluded_association_names excluded_paths
|
|
21
21
|
sensitive_patterns search_extensions concern_paths frontend_paths
|
|
22
22
|
max_file_size max_test_file_size max_schema_file_size max_view_total_size
|
|
23
23
|
max_view_file_size max_search_results max_validate_files
|
|
24
24
|
query_timeout query_row_limit query_redacted_columns allow_query_in_production
|
|
25
25
|
log_lines introspectors
|
|
26
26
|
hydration_enabled hydration_max_hints
|
|
27
|
+
instrumentation_include_arguments
|
|
27
28
|
].freeze
|
|
28
29
|
|
|
29
30
|
# Load configuration from a YAML file, applying values to the current config instance.
|
|
@@ -131,6 +132,19 @@ module RailsAiContext
|
|
|
131
132
|
# Debounce interval in seconds for live reload file watching
|
|
132
133
|
attr_accessor :live_reload_debounce
|
|
133
134
|
|
|
135
|
+
# Whether to include raw tool arguments in ActiveSupport::Notifications
|
|
136
|
+
# instrumentation events. Default: false (v5.8.1+). When false, only
|
|
137
|
+
# metadata (method, tool_name, duration, error) is forwarded to
|
|
138
|
+
# subscribers. When true, `tool_arguments` and `arguments` are forwarded
|
|
139
|
+
# verbatim — including raw SQL from `rails_query`, env var names from
|
|
140
|
+
# `rails_get_env`, and log search patterns from `rails_read_logs`.
|
|
141
|
+
#
|
|
142
|
+
# Setting this to true means the operator takes on the redaction
|
|
143
|
+
# obligation for any downstream observability pipeline (Datadog, Scout,
|
|
144
|
+
# custom loggers) that receives these events. See CHANGELOG.md for v5.8.1
|
|
145
|
+
# for the security review that changed the default.
|
|
146
|
+
attr_accessor :instrumentation_include_arguments
|
|
147
|
+
|
|
134
148
|
# Whether to generate root-level context files (CLAUDE.md, AGENTS.md, etc.)
|
|
135
149
|
# When false, only generates split rule files (.claude/rules/, .cursor/rules/, etc.)
|
|
136
150
|
attr_accessor :generate_root_files
|
|
@@ -189,12 +203,20 @@ module RailsAiContext
|
|
|
189
203
|
/\A(Devise::Models|Devise::Orm|Bullet::|Turbo::|GlobalID::|Rolify::)/
|
|
190
204
|
].freeze
|
|
191
205
|
|
|
206
|
+
DEFAULT_EXCLUDED_ASSOCIATION_NAMES = %w[
|
|
207
|
+
active_storage_attachments active_storage_blobs
|
|
208
|
+
rich_text_body rich_text_content
|
|
209
|
+
action_mailbox_inbound_emails
|
|
210
|
+
noticed_events noticed_notifications
|
|
211
|
+
].freeze
|
|
212
|
+
|
|
192
213
|
# Filtering — customize what's hidden from AI output
|
|
193
214
|
attr_accessor :excluded_controllers # Controller classes hidden from listings (e.g. DeviseController)
|
|
194
215
|
attr_accessor :excluded_route_prefixes # Route controller prefixes hidden with app_only (e.g. action_mailbox/)
|
|
195
216
|
attr_accessor :excluded_concerns # Regex patterns for concerns to hide (e.g. /Devise::Models/)
|
|
196
217
|
attr_accessor :excluded_filters # Framework filter names hidden from controller output
|
|
197
218
|
attr_accessor :excluded_middleware # Default middleware hidden from config output
|
|
219
|
+
attr_accessor :excluded_association_names # Framework association names hidden from model output
|
|
198
220
|
|
|
199
221
|
# Search and file discovery
|
|
200
222
|
attr_accessor :search_extensions # File extensions for Ruby fallback search (default: rb,js,erb,yml,yaml,json)
|
|
@@ -221,8 +243,15 @@ module RailsAiContext
|
|
|
221
243
|
@introspectors = PRESETS[:full].dup
|
|
222
244
|
@excluded_paths = %w[node_modules tmp log vendor .git doc docs]
|
|
223
245
|
@sensitive_patterns = %w[
|
|
224
|
-
.env .env.*
|
|
225
|
-
config/
|
|
246
|
+
.env .env.*
|
|
247
|
+
config/master.key
|
|
248
|
+
config/credentials.yml.enc config/credentials/*.yml.enc
|
|
249
|
+
config/database.yml config/secrets.yml
|
|
250
|
+
config/cable.yml config/storage.yml
|
|
251
|
+
config/mongoid.yml config/redis.yml
|
|
252
|
+
*.pem *.key *.p12 *.pfx *.jks *.keystore
|
|
253
|
+
**/id_rsa **/id_ed25519 **/id_ecdsa **/id_dsa
|
|
254
|
+
.ssh/* .aws/credentials .aws/config .netrc .pgpass .my.cnf
|
|
226
255
|
]
|
|
227
256
|
@auto_mount = false
|
|
228
257
|
@http_path = "/mcp"
|
|
@@ -241,6 +270,7 @@ module RailsAiContext
|
|
|
241
270
|
@max_tool_response_chars = 200_000
|
|
242
271
|
@live_reload = :auto
|
|
243
272
|
@live_reload_debounce = 1.5
|
|
273
|
+
@instrumentation_include_arguments = false
|
|
244
274
|
@generate_root_files = true
|
|
245
275
|
@anti_hallucination_rules = true
|
|
246
276
|
@max_file_size = 5_000_000
|
|
@@ -255,6 +285,7 @@ module RailsAiContext
|
|
|
255
285
|
@excluded_concerns = DEFAULT_EXCLUDED_CONCERNS.dup
|
|
256
286
|
@excluded_filters = DEFAULT_EXCLUDED_FILTERS.dup
|
|
257
287
|
@excluded_middleware = DEFAULT_EXCLUDED_MIDDLEWARE.dup
|
|
288
|
+
@excluded_association_names = DEFAULT_EXCLUDED_ASSOCIATION_NAMES.dup
|
|
258
289
|
@custom_tools = []
|
|
259
290
|
@skip_tools = []
|
|
260
291
|
@ai_tools = nil
|
|
@@ -39,13 +39,13 @@ module RailsAiContext
|
|
|
39
39
|
# Include the gem's own version so cache invalidates during gem development
|
|
40
40
|
digest.update(RailsAiContext::VERSION)
|
|
41
41
|
|
|
42
|
-
# Include gem lib directory
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
42
|
+
# Include gem lib directory fingerprint when using a local/path gem.
|
|
43
|
+
# MEMOIZED — the gem lib contents don't change within a single process
|
|
44
|
+
# lifetime unless a developer is actively editing the gem source (rare
|
|
45
|
+
# audience, they should restart the server to see changes). Previously
|
|
46
|
+
# this walked 123 gem files on every tool call, adding ~12ms to the
|
|
47
|
+
# cached_context hot path for path:-installed users.
|
|
48
|
+
digest.update(gem_lib_fingerprint(root))
|
|
49
49
|
|
|
50
50
|
WATCHED_FILES.each do |file|
|
|
51
51
|
path = File.join(root, file)
|
|
@@ -68,8 +68,35 @@ module RailsAiContext
|
|
|
68
68
|
digest.hexdigest
|
|
69
69
|
end
|
|
70
70
|
|
|
71
|
+
# Clear the memoized gem-lib fingerprint. Called by BaseTool.reset_cache!
|
|
72
|
+
# and LiveReload so active gem development gets a fresh scan on next call
|
|
73
|
+
# without requiring a process restart.
|
|
74
|
+
def reset_gem_lib_fingerprint!
|
|
75
|
+
@gem_lib_fingerprint = nil
|
|
76
|
+
end
|
|
77
|
+
|
|
71
78
|
private
|
|
72
79
|
|
|
80
|
+
# Memoized gem-lib fingerprint. Computed ONCE per process lifetime
|
|
81
|
+
# (or per reset_gem_lib_fingerprint! call) instead of on every
|
|
82
|
+
# tool invocation.
|
|
83
|
+
def gem_lib_fingerprint(root)
|
|
84
|
+
@gem_lib_fingerprint ||= compute_gem_lib_fingerprint(root)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def compute_gem_lib_fingerprint(root)
|
|
88
|
+
gem_lib = File.expand_path("../../..", __FILE__)
|
|
89
|
+
return "" unless gem_lib.start_with?(root) || (defined?(Bundler) && local_gem_path?)
|
|
90
|
+
|
|
91
|
+
sub = Digest::SHA256.new
|
|
92
|
+
Dir.glob(File.join(gem_lib, "**/*.rb")).sort.each do |path|
|
|
93
|
+
sub.update(File.mtime(path).to_f.to_s)
|
|
94
|
+
rescue Errno::ENOENT
|
|
95
|
+
# File deleted between glob and mtime read — skip
|
|
96
|
+
end
|
|
97
|
+
sub.hexdigest
|
|
98
|
+
end
|
|
99
|
+
|
|
73
100
|
# Detect if this gem is loaded via a local path (path: in Gemfile)
|
|
74
101
|
def local_gem_path?
|
|
75
102
|
spec = Bundler.rubygems.find_name("rails-ai-context").first
|
|
@@ -6,17 +6,60 @@ module RailsAiContext
|
|
|
6
6
|
module Instrumentation
|
|
7
7
|
EVENT_PREFIX = "rails_ai_context"
|
|
8
8
|
|
|
9
|
+
# Metadata-only fields forwarded to ActiveSupport::Notifications. We
|
|
10
|
+
# deliberately exclude `tool_arguments`, `params`, `arguments`, and
|
|
11
|
+
# `request` because the MCP SDK includes raw tool inputs in those keys —
|
|
12
|
+
# e.g. `rails_query(sql: "SELECT password_digest...")`, `rails_get_env(name: "SECRET_KEY_BASE")`,
|
|
13
|
+
# `rails_read_logs(search: "api_key=xyz")`. Forwarding them unredacted
|
|
14
|
+
# would leak the request-side data that each tool's response-side
|
|
15
|
+
# redaction was specifically designed to protect. Fixed in v5.8.1.
|
|
16
|
+
#
|
|
17
|
+
# Users who legitimately need tool arguments in observability can set
|
|
18
|
+
# config.instrumentation_include_arguments = true in an initializer
|
|
19
|
+
# (see CONFIGURATION.md for the redaction obligation that comes with it).
|
|
20
|
+
SAFE_KEYS = %i[method tool_name duration error resource_uri prompt_name].freeze
|
|
21
|
+
|
|
9
22
|
# Returns a lambda for MCP::Configuration#instrumentation_callback.
|
|
10
23
|
# Instruments each MCP method call as an ActiveSupport::Notifications event.
|
|
11
24
|
def self.callback
|
|
12
25
|
->(data) {
|
|
13
26
|
return unless defined?(ActiveSupport::Notifications)
|
|
14
27
|
|
|
15
|
-
|
|
16
|
-
|
|
28
|
+
begin
|
|
29
|
+
method = data[:method] || "unknown"
|
|
30
|
+
event_name = "#{EVENT_PREFIX}.#{method.to_s.tr("/", ".")}"
|
|
17
31
|
|
|
18
|
-
|
|
32
|
+
# build_payload reads configuration — wrap it in the rescue so a
|
|
33
|
+
# broken/nil configuration doesn't propagate out of the lambda and
|
|
34
|
+
# crash the MCP SDK's ensure block. v5.8.1-r3 hardening.
|
|
35
|
+
payload = build_payload(data)
|
|
36
|
+
ActiveSupport::Notifications.instrument(event_name, payload)
|
|
37
|
+
rescue => e
|
|
38
|
+
# The MCP SDK's instrument_call invokes this callback from an `ensure`
|
|
39
|
+
# block, which means any exception raised here would overwrite the
|
|
40
|
+
# tool's actual return value with the subscriber's error — effectively
|
|
41
|
+
# crashing every tool call whenever a single subscriber is broken.
|
|
42
|
+
# Swallow the error and log to stderr instead. Fixed in v5.8.1.
|
|
43
|
+
$stderr.puts "[rails-ai-context] instrumentation subscriber failed: #{e.message}" if ENV["DEBUG"]
|
|
44
|
+
end
|
|
19
45
|
}
|
|
20
46
|
end
|
|
47
|
+
|
|
48
|
+
# Build a safe payload from the raw MCP SDK data hash. Strips tool
|
|
49
|
+
# arguments unless the user has explicitly opted in.
|
|
50
|
+
def self.build_payload(data)
|
|
51
|
+
payload = SAFE_KEYS.each_with_object({}) do |key, acc|
|
|
52
|
+
acc[key] = data[key] if data.key?(key)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
if RailsAiContext.configuration.instrumentation_include_arguments
|
|
56
|
+
# User opted in: include arguments verbatim. They take on the
|
|
57
|
+
# redaction obligation for anything downstream consumers log.
|
|
58
|
+
payload[:tool_arguments] = data[:tool_arguments] if data.key?(:tool_arguments)
|
|
59
|
+
payload[:arguments] = data[:arguments] if data.key?(:arguments)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
payload
|
|
63
|
+
end
|
|
21
64
|
end
|
|
22
65
|
end
|
|
@@ -36,10 +36,9 @@ module RailsAiContext
|
|
|
36
36
|
devise_models = scan_models_for(/devise\s+(.+)$/)
|
|
37
37
|
auth[:devise] = devise_models if devise_models.any?
|
|
38
38
|
|
|
39
|
-
# Rails 8 built-in auth
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
end
|
|
39
|
+
# Rails 8 built-in auth (`bin/rails generate authentication`)
|
|
40
|
+
rails_auth = detect_rails_auth
|
|
41
|
+
auth[:rails_auth] = rails_auth if rails_auth
|
|
43
42
|
|
|
44
43
|
# has_secure_password
|
|
45
44
|
secure_pw = scan_models_for(/has_secure_password/)
|
|
@@ -56,6 +55,65 @@ module RailsAiContext
|
|
|
56
55
|
auth
|
|
57
56
|
end
|
|
58
57
|
|
|
58
|
+
# Rails 8's `bin/rails generate authentication` produces a Session model,
|
|
59
|
+
# a Current attributes model, an Authentication concern in app/controllers/concerns,
|
|
60
|
+
# a SessionsController, and a PasswordsController. AI agents need to know:
|
|
61
|
+
#
|
|
62
|
+
# 1. that this app uses the built-in pattern (not Devise / not custom)
|
|
63
|
+
# 2. which controllers opt out via `allow_unauthenticated_access`
|
|
64
|
+
# 3. where the Authentication concern lives so they can find before_actions
|
|
65
|
+
def detect_rails_auth
|
|
66
|
+
return nil unless file_exists?("app/models/session.rb") && file_exists?("app/models/current.rb")
|
|
67
|
+
|
|
68
|
+
result = { detected: true }
|
|
69
|
+
|
|
70
|
+
result[:authentication_concern] = "app/controllers/concerns/authentication.rb" if file_exists?("app/controllers/concerns/authentication.rb")
|
|
71
|
+
result[:sessions_controller] = "app/controllers/sessions_controller.rb" if file_exists?("app/controllers/sessions_controller.rb")
|
|
72
|
+
result[:passwords_controller] = "app/controllers/passwords_controller.rb" if file_exists?("app/controllers/passwords_controller.rb")
|
|
73
|
+
|
|
74
|
+
unauth = scan_allow_unauthenticated_access
|
|
75
|
+
result[:allow_unauthenticated_access] = unauth if unauth.any?
|
|
76
|
+
|
|
77
|
+
result
|
|
78
|
+
rescue => e
|
|
79
|
+
$stderr.puts "[rails-ai-context] detect_rails_auth failed: #{e.message}" if ENV["DEBUG"]
|
|
80
|
+
nil
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def scan_allow_unauthenticated_access
|
|
84
|
+
controllers_dir = File.join(root, "app/controllers")
|
|
85
|
+
return [] unless Dir.exist?(controllers_dir)
|
|
86
|
+
|
|
87
|
+
Dir.glob(File.join(controllers_dir, "**/*.rb")).flat_map do |path|
|
|
88
|
+
content = RailsAiContext::SafeFile.read(path) or next []
|
|
89
|
+
next [] unless content.match?(/\ballow_unauthenticated_access\b/)
|
|
90
|
+
|
|
91
|
+
relative = path.sub("#{root}/", "")
|
|
92
|
+
# Capture every `only:` / `except:` declaration in the file. A single
|
|
93
|
+
# controller can have multiple (e.g. `only:` + `except:` mixed via concerns).
|
|
94
|
+
scoped = content.scan(/allow_unauthenticated_access\s+(only|except):\s*(\[?[^\n]+)/)
|
|
95
|
+
|
|
96
|
+
if scoped.empty?
|
|
97
|
+
[ { file: relative, scope: "all actions" } ]
|
|
98
|
+
else
|
|
99
|
+
scoped.map do |kw, value|
|
|
100
|
+
{ file: relative, scope: "#{kw}: #{strip_trailing_comment(value).strip}" }
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end.compact.sort_by { |h| h[:file] }
|
|
104
|
+
rescue => e
|
|
105
|
+
$stderr.puts "[rails-ai-context] scan_allow_unauthenticated_access failed: #{e.message}" if ENV["DEBUG"]
|
|
106
|
+
[]
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Strip a trailing `# comment` from a captured scope value while preserving
|
|
110
|
+
# `#` characters that appear inside string literals or array element syntax.
|
|
111
|
+
# Conservative: only strips when the `#` is preceded by whitespace, which
|
|
112
|
+
# matches the common style (`only: %i[index] # comment`).
|
|
113
|
+
def strip_trailing_comment(value)
|
|
114
|
+
value.sub(/\s+#.*\z/, "")
|
|
115
|
+
end
|
|
116
|
+
|
|
59
117
|
def detect_authorization
|
|
60
118
|
authz = {}
|
|
61
119
|
|