talk_to_your_app 0.1.0.pre.3 → 0.1.0.pre.7

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.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +205 -0
  3. data/LOCAL_DEVELOPMENT.md +9 -9
  4. data/README.md +157 -43
  5. data/SECURITY.md +69 -0
  6. data/docs/plugin_authoring.md +22 -11
  7. data/docs/read_only_connections.md +51 -25
  8. data/lib/generators/talk_to_your_app/custom_tool/custom_tool_generator.rb +4 -3
  9. data/lib/generators/talk_to_your_app/install/templates/initializer.rb.tt +73 -21
  10. data/lib/talk_to_your_app/audit_logger.rb +22 -0
  11. data/lib/talk_to_your_app/auth/middleware.rb +48 -26
  12. data/lib/talk_to_your_app/configuration.rb +66 -15
  13. data/lib/talk_to_your_app/connection_registry.rb +8 -4
  14. data/lib/talk_to_your_app/current.rb +6 -0
  15. data/lib/talk_to_your_app/plugin.rb +27 -10
  16. data/lib/talk_to_your_app/plugin_registry.rb +13 -0
  17. data/lib/talk_to_your_app/plugins/cache/plugin.rb +32 -0
  18. data/lib/talk_to_your_app/plugins/db/plugin.rb +42 -16
  19. data/lib/talk_to_your_app/plugins/db/tools/query.rb +17 -13
  20. data/lib/talk_to_your_app/plugins/db/tools/schema.rb +0 -3
  21. data/lib/talk_to_your_app/plugins/db/tools/tables.rb +4 -6
  22. data/lib/talk_to_your_app/plugins/flipper/plugin.rb +21 -3
  23. data/lib/talk_to_your_app/plugins/flipper/tools/disable_flag.rb +0 -1
  24. data/lib/talk_to_your_app/plugins/flipper/tools/enable_flag.rb +0 -1
  25. data/lib/talk_to_your_app/plugins/flipper/tools/enabled_flags.rb +0 -1
  26. data/lib/talk_to_your_app/plugins/flipper/tools/list_flags.rb +0 -1
  27. data/lib/talk_to_your_app/plugins/flipper/tools/read_flag.rb +0 -1
  28. data/lib/talk_to_your_app/plugins/jobs/plugin.rb +53 -28
  29. data/lib/talk_to_your_app/plugins/jobs/tools/base.rb +28 -0
  30. data/lib/talk_to_your_app/plugins/jobs/tools/failed_jobs.rb +3 -4
  31. data/lib/talk_to_your_app/plugins/jobs/tools/queue_sizes.rb +4 -4
  32. data/lib/talk_to_your_app/plugins/jobs/tools/rate_metrics.rb +3 -4
  33. data/lib/talk_to_your_app/plugins/jobs/tools/recent_jobs.rb +3 -4
  34. data/lib/talk_to_your_app/railtie.rb +24 -2
  35. data/lib/talk_to_your_app/tool.rb +68 -16
  36. data/lib/talk_to_your_app/transport/rails_mount.rb +26 -2
  37. data/lib/talk_to_your_app/version.rb +1 -1
  38. data/lib/talk_to_your_app.rb +27 -5
  39. metadata +22 -21
  40. data/docs/brainstorms/talk-to-your-app-gem-v1-requirements.md +0 -158
  41. data/docs/plans/2026-06-01-001-feat-talk-to-your-app-gem-v1-plan.md +0 -772
  42. data/docs/residual-review-findings/fd33390.md +0 -19
  43. data/docs/solutions/architecture-patterns/mcp-ruby-sdk-rails-integration-2026-06-01.md +0 -180
  44. data/docs/solutions/runtime-errors/sidekiq-adapter-requires-sidekiq-api-2026-06-16.md +0 -76
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 593eb7d10a1e55f810002c8890fe4eb9e650d5001c2f252c17c79c78c70f9b8d
4
- data.tar.gz: 73bfc284f8ce0081c3af5d40a1b30e7ed14a644d0340e1c7603f98d07d76ecb7
3
+ metadata.gz: d542357394a3ba10a68718a8dfb95eecfe61b51db60270afecbf5a5aa1853a48
4
+ data.tar.gz: 05e68c877cf7373494dffe68bf36948c502eba9eb89a412606edef85b446db5c
5
5
  SHA512:
6
- metadata.gz: d537fc75c1a3d5e59591829de61792610089e8fb4c55c3327abe8ceb228d26fe27bd8802895785940f84fec29d33e1677c3af85ce078e4ff160e14185cfa4766
7
- data.tar.gz: '091db0b41b51ea0dfa3862b3fdade99ae30980c7b80d8c03e323e77be8f957fb7cc34129c79e4e2766f39e056d2f6fac3cecd85287df61ebf4c2dc7861c52dad'
6
+ metadata.gz: 8a02972f0943500d9c32261b9ec9b38a7c043212e8e5b880b7e3da2a8be4a0a038f4afa4975d348559d62cd9f9bb295d738ae22c09d15ffda135b44db3dff71f
7
+ data.tar.gz: fc4769d02c766c515f0b27761ad665a2c80be4e0344eb664460bbcf4560dc3704d379a711c4a244e5760c966cff56efb2b334401b9c1ed5a34facd29b73d8557
data/CHANGELOG.md ADDED
@@ -0,0 +1,205 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. This project follows
4
+ [Semantic Versioning](https://semver.org). Pre-1.0 minor releases may include
5
+ breaking changes.
6
+
7
+ ## [Unreleased]
8
+
9
+ ### Added
10
+ - Failed authentication is now logged. Every rejected request emits one `WARN`
11
+ line and a `talk_to_your_app.auth_failure` notification carrying `reason`
12
+ (`missing_credentials`, `unsupported_scheme`, `invalid_credentials`,
13
+ `validator_error`), `scheme`, `ip`, and `error_class` when a validator raised.
14
+ Previously a `401` left no trace at all, so credential guessing and endpoint
15
+ scanning were undetectable while every *successful* call was audit-logged. No
16
+ credential material is logged, and an unrecognized scheme is reported as
17
+ `other` so a crafted `Authorization` header cannot forge log fields. The level
18
+ is fixed at `:warn` rather than following `config.log_level`.
19
+
20
+ ### Changed
21
+ - A raising `basic_auth` callable is now reported through the configured audit
22
+ logger (as `reason=validator_error`) instead of `Kernel#warn`, so it reaches
23
+ the same sink as the rest of the audit trail. The request outcome is unchanged
24
+ (`401`, never a `500`).
25
+
26
+ ### Fixed
27
+ - `require "talk_to_your_app"` raised `NameError: uninitialized constant
28
+ ActiveSupport::CodeGenerator` anywhere ActiveSupport had not already been fully
29
+ loaded — a plain script, a non-Rails Rack process, or any Gemfile that reaches
30
+ this gem before `rails`. `current.rb` required
31
+ `active_support/current_attributes` without `active_support` itself, and every
32
+ Rails boot hid it. Now covered by a test that loads the gem in a fresh
33
+ subprocess, which is the only way to catch a load-order bug the suite's own
34
+ Rails boot papers over.
35
+ - Boot now rejects a **blank API key value**. `config.api_keys = { "x" =>
36
+ ENV["TTYA_KEY"] }` with the variable unset previously booted clean —
37
+ `auth_configured?` counted the entry, but a blank key can never authenticate,
38
+ so the endpoint returned `401` to every request with no signal that anything
39
+ was wrong. The error names the offending key(s) and never echoes a valid one.
40
+
41
+ ### Fixed (docs)
42
+ - `SECURITY.md` now lists rate limiting, alerting on rejected requests,
43
+ and `X-Forwarded-For` IP trust in the operator checklist, and names rate
44
+ limiting plus query-driven resource exhaustion as operator-owned in the threat
45
+ model. The README documents the auth-failure line and notes that `max_rows`
46
+ bounds the response, not process memory — the full result set is fetched
47
+ before truncation.
48
+
49
+ ## [0.1.0.pre.7] - 2026-07-24
50
+
51
+ ### Removed
52
+ - The boot-time `no config.authorize configured` warning, and
53
+ `Configuration#authorize_configured?` along with it. It fired on every boot of
54
+ a deployment that deliberately grants every principal every tool, and could
55
+ not tell that choice apart from an oversight. The behavior it warned about is
56
+ unchanged and still documented in the README security model, the `SECURITY.md`
57
+ operator checklist, and the generated initializer. The writable-DB-connection
58
+ boot warning is unaffected.
59
+
60
+ ### Fixed
61
+ - Docs: `rails g talk_to_your_app:custom_tool`'s next-steps output and the
62
+ README "Writing your own plugin" example both told operators to enable a
63
+ plugin without `connection:`, which boot rejects. Both now show
64
+ `connection: false`.
65
+ - README's demo poster and video use absolute URLs, so they resolve wherever the
66
+ README is rendered (the files ship in the repo, not in the gem).
67
+
68
+ ## [0.1.0.pre.6] - 2026-07-20
69
+
70
+ ### Fixed
71
+ - Boot-time warnings (`no config.authorize configured`, writable DB connection)
72
+ now go to `$stderr` (`Kernel#warn`) instead of the configured logger. The
73
+ configured logger may write to `$stdout`, and boot output on `$stdout` can be
74
+ captured by scripts (e.g. building `DATABASE_URL` from `rails runner`), which
75
+ corrupted the captured value. Warnings never touch `$stdout` at boot now.
76
+
77
+ ## [0.1.0.pre.5] - 2026-07-18
78
+
79
+ Requires Ruby >= 3.3 and Rails >= 7.2 (the tested CI matrix: Ruby 3.3/4.0 ×
80
+ Rails 7.2/8.0/8.1); the `mcp` SDK is pinned `~> 0.25.0`.
81
+
82
+ ### Added
83
+ - Cache plugin: `config.plugin :cache, connection: false` exposes `cache.clear`
84
+ (`Rails.cache.clear`). Destructive (drops every cached entry) — scope it to
85
+ trusted principals with `config.authorize`.
86
+ - `config.authorize` now receives the tool's arguments as a third parameter:
87
+ `{ |principal, tool_name, args| ... }` (symbol keys, before defaults are
88
+ applied). Enables per-flag/per-task/per-table authorization. Two-parameter
89
+ blocks keep working (Ruby blocks ignore extra arguments); an explicit lambda
90
+ passed via `authorize(&my_lambda)` must accept all three.
91
+ - Boot warns (does not fail) when plugins are enabled without `config.authorize`
92
+ — every authenticated principal can otherwise call every enabled tool.
93
+ - `SECURITY.md` with private reporting instructions and an operator misconfiguration
94
+ checklist. Packaged in the gem; linked from the README security model.
95
+ - `config.allowed_hosts` (default `[]`). Extra `Host` header values accepted by
96
+ the transport's DNS-rebinding protection, beyond the loopback defaults
97
+ (`127.0.0.1`, `::1`, `localhost`). Required for any non-loopback deployment
98
+ (endpoint served from a real domain) — without it the transport rejects every
99
+ request with "Forbidden: Invalid Host header". Forwarded to the mcp SDK's
100
+ `StreamableHTTPTransport(allowed_hosts:)`. The generated initializer defaults
101
+ it to `TalkToYourApp.rails_hosts` — the host app's own `config.hosts` (literal
102
+ host strings only; regexp/IPAddr/dotted-wildcard entries are skipped).
103
+ - `config.enabled` (default true). A global on/off switch: when false the
104
+ mounted endpoint serves `503` and boot validation is skipped, so an operator
105
+ can ship the initializer and disable it per-environment without the gem
106
+ refusing to boot on otherwise-incomplete configuration.
107
+ - Opt-in DB writes: wiring a `role: :writing` connection into the DB plugin
108
+ (`config.plugin :db, connection: :writer`) lets `db.query` execute writes. It
109
+ is read-only on a `:reading` connection (the default) and logs a loud warning
110
+ at boot on a writable one. `config.authorize` cannot distinguish reads from
111
+ writes, and full SQL is written to the audit log — scope the DB user and log
112
+ sinks accordingly (see the README).
113
+ - `config.stateless` (default false). When true the Streamable HTTP transport
114
+ runs stateless — every request is self-contained with no per-session state in
115
+ the transport — so any worker or replica can serve any request. Set it when
116
+ the host app runs more than one Puma/Unicorn worker, where a follow-up request
117
+ can otherwise land on a process that never saw `initialize` and fail with
118
+ "Session not found". Stateless mode does not support SSE streaming or
119
+ server-initiated notifications.
120
+ - `app/talk_to_your_app/` convention directory: custom tools live in
121
+ `custom_tools/` (one `TalkToYourApp::Tool` subclass per file, loaded by
122
+ `:custom_tools`). The gem ignores the tree in Zeitwerk and requires the files
123
+ itself. The custom-tool generator writes here.
124
+ - `rails g talk_to_your_app:custom_tool NAME` generator — scaffolds a
125
+ `TalkToYourApp::Tool` subclass in `app/talk_to_your_app/custom_tools/`,
126
+ exposed as `custom.<name>`.
127
+ - Rake plugin per-task timeout: `config.plugin :rake, allowed: [...], timeout: 60`
128
+ (seconds, default 20). A task exceeding it is hard-killed (process group) and
129
+ returned as a tool error, so a hung task can't pin the web thread.
130
+ - `Tool::Context#ip` exposes the request IP (from `Current.ip`), completing
131
+ parity with `#principal` and `#session_id` for custom tools.
132
+ - Per-principal tool authorization: `config.authorize { |principal, tool_name| ... }`.
133
+ - DB plugin statement timeout on MySQL (`max_execution_time`) in addition to
134
+ PostgreSQL. SQLite and MariaDB have no per-statement timeout (the read-only
135
+ role still applies).
136
+ - Flipper plugin: enable/disable across actor, group, and percentage
137
+ (`percentage_of_actors` / `percentage_of_time`) gates, plus a
138
+ `flipper.enabled_flags` tool that lists active flags with their gates and
139
+ last-change timestamps.
140
+
141
+ ### Changed
142
+ - **BREAKING — minimum `mcp` SDK raised to `~> 0.25.0`.** 0.23 added `Host`-header
143
+ DNS-rebinding protection (`allowed_hosts:`); 0.24/0.25 add transport fixes
144
+ (SSE write synchronization, header normalization) with no further breaking
145
+ changes to this gem's SDK touch points. Earlier SDK versions reject
146
+ `allowed_hosts:` at boot.
147
+ - `config.allowed_origins` now works: it is forwarded to the mcp SDK transport,
148
+ which owns Origin validation (same-origin allowed, case-insensitive matching,
149
+ no-Origin non-browser clients allowed). Previously the gem's own middleware
150
+ rejected every cross-origin request regardless of the setting — the gem-side
151
+ check is removed in favor of the SDK's.
152
+ - Connections use `with_connection` inside `connected_to` (with explicit
153
+ `prevent_writes:` from the connection spec) so checkouts return to the pool
154
+ when the tool block ends.
155
+ - `401` `WWW-Authenticate` lists only the configured schemes (`Bearer`, `Basic`,
156
+ or both) instead of always advertising Bearer.
157
+ - Install generator: sets `config.stateless = true` in production; stronger
158
+ production comments for `enabled`, `authorize`, and the security checklist.
159
+ - Gem package includes `CHANGELOG.md` and `SECURITY.md`; ships only operator
160
+ docs (`docs/read_only_connections.md`, `docs/plugin_authoring.md`) rather than
161
+ internal plans/brainstorms.
162
+ - README security model updated for opt-in writable DB and the authorize warning.
163
+ - **BREAKING — the `:jobs` plugin is split into `:sidekiq` and `:solid_queue`.**
164
+ Enable the backend you run (or both, e.g. mid-migration):
165
+ `config.plugin :sidekiq, connection: false`. The `adapter:` option is gone.
166
+ Tools are now adapter-namespaced — `sidekiq.queue_sizes` / `solid_queue.queue_sizes`
167
+ (etc.) instead of `jobs.queue_sizes` — so both backends can be exposed at once.
168
+ - **BREAKING — every plugin must declare `connection:`.** Pass a declared
169
+ connection name, or `connection: false` to opt out (`:sidekiq`, `:solid_queue`,
170
+ `:rake`, and connection-less `:custom_tools` use `false`). Enabling any plugin
171
+ without the option fails at boot. `:db` and `:flipper` require a real
172
+ connection (`connection: false` is rejected).
173
+ - **BREAKING — connections are wired into plugins by name.** Plugins no longer
174
+ hardcode a connection name; you declare connections and wire one into each
175
+ plugin that needs a database: `config.plugin :db, connection: :readonly` and
176
+ `config.plugin :flipper, connection: :writer`. Enabling `:db` or `:flipper`
177
+ without `connection:` now fails at boot with an actionable error. The per-tool
178
+ hardcoded connection names were removed. `config.connection`'s `role:` now
179
+ defaults to `:reading`. **Existing initializers must add `connection:` to
180
+ `config.plugin :db`/`:flipper`.** **Plugin authors:** the `requires_connection`
181
+ DSL and the `Plugin.required_connections` alias are removed entirely — the
182
+ framework now enforces `connection:` universally (see above), so plugins no
183
+ longer mark themselves. `Tool::Context#connection_name` is now a no-arg reader
184
+ (it reports the connection the call ran on); pass an explicit override to
185
+ `ctx.connection(:name)`, not to `connection_name`. Connection resolution is
186
+ most-specific-first: an explicit `ctx.connection(:name)` arg, then the tool's
187
+ own static `connection` DSL, then the plugin-wired `connection:` default. A
188
+ tool that declares its own connection is no longer overridden by the
189
+ plugin-wired one (custom tools), and a tool's declared connection is now
190
+ validated at boot, not at first call.
191
+ - **BREAKING — Flipper enforces `role: :writing` at boot.** A Flipper connection
192
+ declared `role: :reading` previously booted and failed only on the first write;
193
+ it now fails closed at boot.
194
+ - **Jobs adapter contract:** the `required_gem` hash key was renamed from
195
+ `name:` to `gem_name:` (e.g. `{ const: "Sidekiq", gem_name: "sidekiq" }`),
196
+ matching the Plugin DSL's `requires_gem` option. **Third-party jobs adapters
197
+ must update their `REQUIRED_GEM`/`required_gem` to use `gem_name:`** — with the
198
+ old `name:` key the boot-time gem check still runs but its error message shows
199
+ a blank gem name.
200
+ - **Jobs adapter response shape:** `enqueued_at` is now an ISO-8601 string
201
+ across all adapters, and every job hash carries the same keys (`jid`, `class`,
202
+ `queue`, `args`, `enqueued_at`, `error_message`), with `error_message` nil for
203
+ jobs that have not failed.
204
+ - **Flipper enable/disable response shape:** now `{ name, enabled, gate_type,
205
+ gates }` (previously `{ name, enabled, actor }`), consistent with `read_flag`.
data/LOCAL_DEVELOPMENT.md CHANGED
@@ -4,11 +4,11 @@ How to work on the `talk_to_your_app` gem and run its test suite.
4
4
 
5
5
  ## Prerequisites
6
6
 
7
- - **Ruby** >= 3.2 (CI runs 3.2 and 3.3).
7
+ - **Ruby** >= 3.3 (CI runs 3.3 and 4.0).
8
8
  - **Bundler** (`gem install bundler`).
9
9
  - **SQLite** — the default test database; the `sqlite3` gem is bundled, no server needed.
10
10
  - **PostgreSQL** — _optional_ for the test suite (DB-plugin read-only-role and timeout tests skip without it) but **required to run the dummy app as a local MCP server** (see below). A running server on `localhost:5432` reachable as the `postgres` superuser (DBngin, Postgres.app, Homebrew, or Docker all work).
11
- - **Redis** _(optional)_ — exercises the Jobs plugin's Sidekiq adapter. A running server on `localhost:6379`.
11
+ - **Redis** _(optional)_ — exercises the Sidekiq plugin. A running server on `localhost:6379`.
12
12
 
13
13
  Tests that need PostgreSQL or Redis **skip cleanly** when the service is not reachable, so `bundle exec rake test` always runs; you just get fewer assertions without the services.
14
14
 
@@ -52,7 +52,7 @@ bundle exec rake test TEST=test/talk_to_your_app/configuration_test.rb TESTOPTS=
52
52
 
53
53
  ## Testing against multiple Rails versions
54
54
 
55
- The gem supports Rails 7.1, 7.2, and 8.0 via [Appraisal](https://github.com/thoughtbot/appraisal). The version matrix lives in `Appraisals`.
55
+ The gem supports Rails 7.2, 8.0, and 8.1 via [Appraisal](https://github.com/thoughtbot/appraisal). The version matrix lives in `Appraisals`.
56
56
 
57
57
  ```sh
58
58
  # Generate/install the per-version gemfiles under gemfiles/ (needs network)
@@ -117,11 +117,11 @@ The MCP endpoint is now at **`http://localhost:3000/mcp`**.
117
117
  that user's name. (Restart the server after seeding new users so their
118
118
  tokens are picked up.)
119
119
  - **Root page:** `http://localhost:3000/` shows DB stats and the per-user tokens.
120
- - **Plugins enabled:** DB, **Jobs** (Solid Queue adapter), **Flipper**, and **Rake** (allow-listed tasks `demo:stats`, `demo:echo`).
120
+ - **Plugins enabled:** DB, **Solid Queue**, **Flipper**, and **Rake** (allow-listed tasks `demo:stats`, `demo:echo`).
121
121
  - **Tools available:** `db.query` (read-only SQL over `users` / `posts` /
122
122
  `comments` / `widgets`), `db.tables`, `db.schema` (columns/indexes/FKs);
123
- `jobs.queue_sizes` / `jobs.recent_jobs` /
124
- `jobs.failed_jobs` / `jobs.rate_metrics`; `flipper.list_flags` /
123
+ `solid_queue.queue_sizes` / `solid_queue.recent_jobs` /
124
+ `solid_queue.failed_jobs` / `solid_queue.rate_metrics`; `flipper.list_flags` /
125
125
  `read_flag` / `enable_flag` / `disable_flag` / `enabled_flags`; `rake.run`
126
126
  (allow-listed `demo:stats` and `demo:echo[message]`); and custom tools
127
127
  `custom.make_admin` / `custom.toggle_active` (which write user state).
@@ -142,7 +142,7 @@ cd test/dummy
142
142
  RAILS_ENV=development bundle exec ruby bin/jobs
143
143
  ```
144
144
 
145
- Watch the queue with the `jobs.*` MCP tools (or `bin/rails console`).
145
+ Watch the queue with the `solid_queue.*` MCP tools (or `bin/rails console`).
146
146
 
147
147
  ### 3. Connect Claude Code
148
148
 
@@ -257,7 +257,7 @@ lib/talk_to_your_app/ # the gem
257
257
  auth/ # Bearer/Basic middleware + validators
258
258
  transport/rails_mount.rb # builds the MCP::Server + Rack app
259
259
  audit_logger.rb # one log line per tool call
260
- plugins/ # db, jobs, flipper, rake, custom_tools
260
+ plugins/ # db, jobs (sidekiq + solid_queue), flipper, rake, custom_tools
261
261
  lib/generators/talk_to_your_app/ # install, custom_tool generators
262
262
  test/
263
263
  dummy/ # minimal Rails app for integration tests
@@ -281,4 +281,4 @@ Writing a plugin uses the same public DSL as the bundled ones — see
281
281
  `test/support/mcp_driver.rb` (the `initialize` handshake → `tools/list` →
282
282
  `tools/call`).
283
283
  - Keep SDK touch points isolated to `transport/rails_mount.rb` and tool
284
- compilation; the `mcp` gem is pinned `~> 0.18` (see the README upgrade note).
284
+ compilation; the `mcp` gem is pinned `~> 0.25.0` (see the README upgrade note).