talk_to_your_app 0.1.0.pre.4 → 0.1.0.pre.6
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 +146 -0
- data/LOCAL_DEVELOPMENT.md +9 -9
- data/README.md +58 -12
- data/SECURITY.md +56 -0
- data/docs/plugin_authoring.md +1 -1
- data/docs/read_only_connections.md +39 -19
- data/lib/generators/talk_to_your_app/install/templates/initializer.rb.tt +30 -10
- data/lib/talk_to_your_app/auth/middleware.rb +16 -18
- data/lib/talk_to_your_app/configuration.rb +19 -6
- data/lib/talk_to_your_app/connection_registry.rb +8 -4
- data/lib/talk_to_your_app/plugins/cache/plugin.rb +32 -0
- data/lib/talk_to_your_app/plugins/db/plugin.rb +3 -2
- data/lib/talk_to_your_app/railtie.rb +18 -0
- data/lib/talk_to_your_app/tool.rb +1 -1
- data/lib/talk_to_your_app/transport/rails_mount.rb +1 -0
- data/lib/talk_to_your_app/version.rb +1 -1
- data/lib/talk_to_your_app.rb +1 -0
- metadata +19 -20
- data/docs/brainstorms/talk-to-your-app-gem-v1-requirements.md +0 -158
- data/docs/plans/2026-06-01-001-feat-talk-to-your-app-gem-v1-plan.md +0 -772
- data/docs/plans/2026-06-18-001-feat-connection-wiring-and-enabled-flag-plan.md +0 -363
- data/docs/residual-review-findings/fd33390.md +0 -19
- data/docs/solutions/architecture-patterns/mcp-ruby-sdk-rails-integration-2026-06-01.md +0 -180
- 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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 29d220dca433672cda7a7de7b934ee9fff8d9fd2004a2ae1f8b01066a6f0ec78
|
|
4
|
+
data.tar.gz: 4334526b082a29b0e81a0fc8e36778e76f2ba0363d0e36e2fdc9b875551ed094
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1ad3205b3b15658133ccc9137e5d20ac8b8c96369a304b292a5e90b85f6c0bcec058fb6d3b59e6cc1043026f4f9e37d583c9576247430fbb0e72c4b68b2d3856
|
|
7
|
+
data.tar.gz: bedb84dc9a63d12ef2e80ed09ee8bea2a00a91764f3753be433ace2a1135cb6b6c48eee5b8cc7bf2b239e94c479c913d9f1b710fbfb8b7ba1b135e58e654a558
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
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
|
+
## [0.1.0.pre.6] - 2026-07-20
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
- Boot-time warnings (`no config.authorize configured`, writable DB connection)
|
|
13
|
+
now go to `$stderr` (`Kernel#warn`) instead of the configured logger. The
|
|
14
|
+
configured logger may write to `$stdout`, and boot output on `$stdout` can be
|
|
15
|
+
captured by scripts (e.g. building `DATABASE_URL` from `rails runner`), which
|
|
16
|
+
corrupted the captured value. Warnings never touch `$stdout` at boot now.
|
|
17
|
+
|
|
18
|
+
## [0.1.0.pre.5] - 2026-07-18
|
|
19
|
+
|
|
20
|
+
Requires Ruby >= 3.3 and Rails >= 7.2 (the tested CI matrix: Ruby 3.3/4.0 ×
|
|
21
|
+
Rails 7.2/8.0/8.1); the `mcp` SDK is pinned `~> 0.25.0`.
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
- Cache plugin: `config.plugin :cache, connection: false` exposes `cache.clear`
|
|
25
|
+
(`Rails.cache.clear`). Destructive (drops every cached entry) — scope it to
|
|
26
|
+
trusted principals with `config.authorize`.
|
|
27
|
+
- `config.authorize` now receives the tool's arguments as a third parameter:
|
|
28
|
+
`{ |principal, tool_name, args| ... }` (symbol keys, before defaults are
|
|
29
|
+
applied). Enables per-flag/per-task/per-table authorization. Two-parameter
|
|
30
|
+
blocks keep working (Ruby blocks ignore extra arguments); an explicit lambda
|
|
31
|
+
passed via `authorize(&my_lambda)` must accept all three.
|
|
32
|
+
- Boot warns (does not fail) when plugins are enabled without `config.authorize`
|
|
33
|
+
— every authenticated principal can otherwise call every enabled tool.
|
|
34
|
+
- `SECURITY.md` with private reporting instructions and an operator misconfiguration
|
|
35
|
+
checklist. Packaged in the gem; linked from the README security model.
|
|
36
|
+
- `config.allowed_hosts` (default `[]`). Extra `Host` header values accepted by
|
|
37
|
+
the transport's DNS-rebinding protection, beyond the loopback defaults
|
|
38
|
+
(`127.0.0.1`, `::1`, `localhost`). Required for any non-loopback deployment
|
|
39
|
+
(endpoint served from a real domain) — without it the transport rejects every
|
|
40
|
+
request with "Forbidden: Invalid Host header". Forwarded to the mcp SDK's
|
|
41
|
+
`StreamableHTTPTransport(allowed_hosts:)`. The generated initializer defaults
|
|
42
|
+
it to `TalkToYourApp.rails_hosts` — the host app's own `config.hosts` (literal
|
|
43
|
+
host strings only; regexp/IPAddr/dotted-wildcard entries are skipped).
|
|
44
|
+
- `config.enabled` (default true). A global on/off switch: when false the
|
|
45
|
+
mounted endpoint serves `503` and boot validation is skipped, so an operator
|
|
46
|
+
can ship the initializer and disable it per-environment without the gem
|
|
47
|
+
refusing to boot on otherwise-incomplete configuration.
|
|
48
|
+
- Opt-in DB writes: wiring a `role: :writing` connection into the DB plugin
|
|
49
|
+
(`config.plugin :db, connection: :writer`) lets `db.query` execute writes. It
|
|
50
|
+
is read-only on a `:reading` connection (the default) and logs a loud warning
|
|
51
|
+
at boot on a writable one. `config.authorize` cannot distinguish reads from
|
|
52
|
+
writes, and full SQL is written to the audit log — scope the DB user and log
|
|
53
|
+
sinks accordingly (see the README).
|
|
54
|
+
- `config.stateless` (default false). When true the Streamable HTTP transport
|
|
55
|
+
runs stateless — every request is self-contained with no per-session state in
|
|
56
|
+
the transport — so any worker or replica can serve any request. Set it when
|
|
57
|
+
the host app runs more than one Puma/Unicorn worker, where a follow-up request
|
|
58
|
+
can otherwise land on a process that never saw `initialize` and fail with
|
|
59
|
+
"Session not found". Stateless mode does not support SSE streaming or
|
|
60
|
+
server-initiated notifications.
|
|
61
|
+
- `app/talk_to_your_app/` convention directory: custom tools live in
|
|
62
|
+
`custom_tools/` (one `TalkToYourApp::Tool` subclass per file, loaded by
|
|
63
|
+
`:custom_tools`). The gem ignores the tree in Zeitwerk and requires the files
|
|
64
|
+
itself. The custom-tool generator writes here.
|
|
65
|
+
- `rails g talk_to_your_app:custom_tool NAME` generator — scaffolds a
|
|
66
|
+
`TalkToYourApp::Tool` subclass in `app/talk_to_your_app/custom_tools/`,
|
|
67
|
+
exposed as `custom.<name>`.
|
|
68
|
+
- Rake plugin per-task timeout: `config.plugin :rake, allowed: [...], timeout: 60`
|
|
69
|
+
(seconds, default 20). A task exceeding it is hard-killed (process group) and
|
|
70
|
+
returned as a tool error, so a hung task can't pin the web thread.
|
|
71
|
+
- `Tool::Context#ip` exposes the request IP (from `Current.ip`), completing
|
|
72
|
+
parity with `#principal` and `#session_id` for custom tools.
|
|
73
|
+
- Per-principal tool authorization: `config.authorize { |principal, tool_name| ... }`.
|
|
74
|
+
- DB plugin statement timeout on MySQL (`max_execution_time`) in addition to
|
|
75
|
+
PostgreSQL. SQLite and MariaDB have no per-statement timeout (the read-only
|
|
76
|
+
role still applies).
|
|
77
|
+
- Flipper plugin: enable/disable across actor, group, and percentage
|
|
78
|
+
(`percentage_of_actors` / `percentage_of_time`) gates, plus a
|
|
79
|
+
`flipper.enabled_flags` tool that lists active flags with their gates and
|
|
80
|
+
last-change timestamps.
|
|
81
|
+
|
|
82
|
+
### Changed
|
|
83
|
+
- **BREAKING — minimum `mcp` SDK raised to `~> 0.25.0`.** 0.23 added `Host`-header
|
|
84
|
+
DNS-rebinding protection (`allowed_hosts:`); 0.24/0.25 add transport fixes
|
|
85
|
+
(SSE write synchronization, header normalization) with no further breaking
|
|
86
|
+
changes to this gem's SDK touch points. Earlier SDK versions reject
|
|
87
|
+
`allowed_hosts:` at boot.
|
|
88
|
+
- `config.allowed_origins` now works: it is forwarded to the mcp SDK transport,
|
|
89
|
+
which owns Origin validation (same-origin allowed, case-insensitive matching,
|
|
90
|
+
no-Origin non-browser clients allowed). Previously the gem's own middleware
|
|
91
|
+
rejected every cross-origin request regardless of the setting — the gem-side
|
|
92
|
+
check is removed in favor of the SDK's.
|
|
93
|
+
- Connections use `with_connection` inside `connected_to` (with explicit
|
|
94
|
+
`prevent_writes:` from the connection spec) so checkouts return to the pool
|
|
95
|
+
when the tool block ends.
|
|
96
|
+
- `401` `WWW-Authenticate` lists only the configured schemes (`Bearer`, `Basic`,
|
|
97
|
+
or both) instead of always advertising Bearer.
|
|
98
|
+
- Install generator: sets `config.stateless = true` in production; stronger
|
|
99
|
+
production comments for `enabled`, `authorize`, and the security checklist.
|
|
100
|
+
- Gem package includes `CHANGELOG.md` and `SECURITY.md`; ships only operator
|
|
101
|
+
docs (`docs/read_only_connections.md`, `docs/plugin_authoring.md`) rather than
|
|
102
|
+
internal plans/brainstorms.
|
|
103
|
+
- README security model updated for opt-in writable DB and the authorize warning.
|
|
104
|
+
- **BREAKING — the `:jobs` plugin is split into `:sidekiq` and `:solid_queue`.**
|
|
105
|
+
Enable the backend you run (or both, e.g. mid-migration):
|
|
106
|
+
`config.plugin :sidekiq, connection: false`. The `adapter:` option is gone.
|
|
107
|
+
Tools are now adapter-namespaced — `sidekiq.queue_sizes` / `solid_queue.queue_sizes`
|
|
108
|
+
(etc.) instead of `jobs.queue_sizes` — so both backends can be exposed at once.
|
|
109
|
+
- **BREAKING — every plugin must declare `connection:`.** Pass a declared
|
|
110
|
+
connection name, or `connection: false` to opt out (`:sidekiq`, `:solid_queue`,
|
|
111
|
+
`:rake`, and connection-less `:custom_tools` use `false`). Enabling any plugin
|
|
112
|
+
without the option fails at boot. `:db` and `:flipper` require a real
|
|
113
|
+
connection (`connection: false` is rejected).
|
|
114
|
+
- **BREAKING — connections are wired into plugins by name.** Plugins no longer
|
|
115
|
+
hardcode a connection name; you declare connections and wire one into each
|
|
116
|
+
plugin that needs a database: `config.plugin :db, connection: :readonly` and
|
|
117
|
+
`config.plugin :flipper, connection: :writer`. Enabling `:db` or `:flipper`
|
|
118
|
+
without `connection:` now fails at boot with an actionable error. The per-tool
|
|
119
|
+
hardcoded connection names were removed. `config.connection`'s `role:` now
|
|
120
|
+
defaults to `:reading`. **Existing initializers must add `connection:` to
|
|
121
|
+
`config.plugin :db`/`:flipper`.** **Plugin authors:** the `requires_connection`
|
|
122
|
+
DSL and the `Plugin.required_connections` alias are removed entirely — the
|
|
123
|
+
framework now enforces `connection:` universally (see above), so plugins no
|
|
124
|
+
longer mark themselves. `Tool::Context#connection_name` is now a no-arg reader
|
|
125
|
+
(it reports the connection the call ran on); pass an explicit override to
|
|
126
|
+
`ctx.connection(:name)`, not to `connection_name`. Connection resolution is
|
|
127
|
+
most-specific-first: an explicit `ctx.connection(:name)` arg, then the tool's
|
|
128
|
+
own static `connection` DSL, then the plugin-wired `connection:` default. A
|
|
129
|
+
tool that declares its own connection is no longer overridden by the
|
|
130
|
+
plugin-wired one (custom tools), and a tool's declared connection is now
|
|
131
|
+
validated at boot, not at first call.
|
|
132
|
+
- **BREAKING — Flipper enforces `role: :writing` at boot.** A Flipper connection
|
|
133
|
+
declared `role: :reading` previously booted and failed only on the first write;
|
|
134
|
+
it now fails closed at boot.
|
|
135
|
+
- **Jobs adapter contract:** the `required_gem` hash key was renamed from
|
|
136
|
+
`name:` to `gem_name:` (e.g. `{ const: "Sidekiq", gem_name: "sidekiq" }`),
|
|
137
|
+
matching the Plugin DSL's `requires_gem` option. **Third-party jobs adapters
|
|
138
|
+
must update their `REQUIRED_GEM`/`required_gem` to use `gem_name:`** — with the
|
|
139
|
+
old `name:` key the boot-time gem check still runs but its error message shows
|
|
140
|
+
a blank gem name.
|
|
141
|
+
- **Jobs adapter response shape:** `enqueued_at` is now an ISO-8601 string
|
|
142
|
+
across all adapters, and every job hash carries the same keys (`jid`, `class`,
|
|
143
|
+
`queue`, `args`, `enqueued_at`, `error_message`), with `error_message` nil for
|
|
144
|
+
jobs that have not failed.
|
|
145
|
+
- **Flipper enable/disable response shape:** now `{ name, enabled, gate_type,
|
|
146
|
+
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.
|
|
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
|
|
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.
|
|
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, **
|
|
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
|
-
`
|
|
124
|
-
`
|
|
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 `
|
|
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.
|
|
284
|
+
compilation; the `mcp` gem is pinned `~> 0.25.0` (see the README upgrade note).
|
data/README.md
CHANGED
|
@@ -12,7 +12,7 @@ It's a thin, Rails-native layer over the official [MCP Ruby SDK](https://github.
|
|
|
12
12
|
|
|
13
13
|
- 🔌 **A Streamable HTTP MCP endpoint in two lines** — `mount TalkToYourApp.rack_app`, and you're live.
|
|
14
14
|
- 🔒 **Fail-closed by design** — API-key or HTTP Basic auth required, optional per-tool authorization, and a read-only DB role the app *refuses to boot without*. Misconfiguration fails at deploy, never on the first request.
|
|
15
|
-
- 🧰 **Batteries-included plugins** — `db` (read-only SQL + schema introspection), `sidekiq` and `solid_queue` (background-job metrics), `flipper` (feature flags), `rake` (allow-listed tasks), and `custom_tools` (your own tools, with a generator).
|
|
15
|
+
- 🧰 **Batteries-included plugins** — `db` (read-only SQL + schema introspection), `sidekiq` and `solid_queue` (background-job metrics), `flipper` (feature flags), `rake` (allow-listed tasks), `cache` (clear the Rails cache), and `custom_tools` (your own tools, with a generator).
|
|
16
16
|
- 📝 **Every call audit-logged** — principal, IP, params, outcome, duration. Subscribe to persist your own trail.
|
|
17
17
|
- ✍️ **A Ruby DSL + generators** for writing first-class tools of your own in a few lines.
|
|
18
18
|
|
|
@@ -107,9 +107,10 @@ Every option lives inside `TalkToYourApp.configure`:
|
|
|
107
107
|
| `config.instructions` | Guidance shown to the LLM on how to use this server's tools (optional). |
|
|
108
108
|
| `config.api_keys` | `{ "principal-name" => "secret" }`. The name is logged as the principal. Multiple keys supported for rotation. |
|
|
109
109
|
| `config.basic_auth { \|user, pass\| ... }` | HTTP Basic callable returning truthy to authenticate. Wire it to your own user model. |
|
|
110
|
-
| `config.authorize { \|principal, tool\| ... }` | Optional per-principal tool authorization. Returns truthy to allow. Without it, any authenticated principal may call any enabled tool. |
|
|
111
|
-
| `config.allowed_origins` |
|
|
110
|
+
| `config.authorize { \|principal, tool, args\| ... }` | Optional per-principal tool authorization. Returns truthy to allow. `args` is the tool's argument hash (symbol keys), so you can authorize per flag/task/argument, not just per tool — two-parameter blocks keep working. Without it, any authenticated principal may call any enabled tool. |
|
|
111
|
+
| `config.allowed_origins` | Cross-origin browser `Origin` values to allow (DNS-rebinding protection, enforced by the MCP SDK transport). Requests without an `Origin` header (non-browser clients) and same-origin requests always pass; matching is case-insensitive. |
|
|
112
112
|
| `config.allowed_hosts` | Extra `Host` header values accepted by the transport (DNS-rebinding protection), beyond the always-allowed loopback defaults (`127.0.0.1`, `::1`, `localhost`). A non-loopback deployment **must** list its host here or every request is rejected with "Forbidden: Invalid Host header". Each entry matches a bare host (any port) or full `host:port`. The generated initializer defaults this to `TalkToYourApp.rails_hosts` (your app's own `config.hosts`, literal strings only — add wildcard/regexp hosts explicitly). |
|
|
113
|
+
| `config.stateless` | Run the Streamable HTTP transport stateless — every request is self-contained, so any worker or replica can serve it. **Set to `true` whenever the app runs more than one Puma/Unicorn worker or replica** (i.e. most production deployments): with the default stateful mode, a follow-up request can land on a process that never saw `initialize` and fail with **"Session not found"**. Stateless mode does not support SSE streaming or server-initiated notifications. Default `false`. |
|
|
113
114
|
| `config.connection :name, database:, role:, replica:, statement_timeout:` | Declares a named connection. `role:` defaults to `:reading`; pass `:writing` for a writer. |
|
|
114
115
|
| `config.plugin :name, connection: :conn, **opts` | Enables a plugin (off by default). **Every plugin must declare `connection:`** — a declared connection name, or `connection: false` to opt out (e.g. `:sidekiq`, `:rake`, `:custom_tools` that don't read a wired SQL connection). |
|
|
115
116
|
| `config.logger` | Audit logger. Defaults to `Rails.logger`. Swappable to any Logger-compatible object. |
|
|
@@ -134,6 +135,10 @@ TalkToYourApp.configure do |config|
|
|
|
134
135
|
|
|
135
136
|
# Optionally scope what each principal may call.
|
|
136
137
|
config.authorize { |principal, tool| principal == "ci-readonly-bot" ? tool.start_with?("db.") : true }
|
|
138
|
+
|
|
139
|
+
# The block also receives the tool's arguments (third parameter), so you can
|
|
140
|
+
# authorize per flag/task/argument, not just per tool:
|
|
141
|
+
# config.authorize { |_p, tool, args| tool != "flipper.enable_flag" || args[:name] != "require_2fa" }
|
|
137
142
|
end
|
|
138
143
|
```
|
|
139
144
|
|
|
@@ -168,6 +173,7 @@ All plugins are **off by default** — enable them explicitly, and an agent can
|
|
|
168
173
|
| [Solid Queue](#jobs-read-only) | Read Solid Queue queue sizes, recent/failed jobs, rates | `config.plugin :solid_queue, connection: false` |
|
|
169
174
|
| [Flipper](#flipper) | Read and toggle feature flags (global, actor, group, %) | `config.plugin :flipper, connection: :writer` |
|
|
170
175
|
| [Rake](#rake-allow-listed-task-runner) | Run allow-listed rake tasks and read their output | `config.plugin :rake, connection: false, allowed: [...]` |
|
|
176
|
+
| [Cache](#cache) | Clear the Rails cache | `config.plugin :cache, connection: false` |
|
|
171
177
|
| [Custom Tools](#custom-tools) | Call tools you write yourself (writes allowed) | `config.plugin :custom_tools, connection: false` |
|
|
172
178
|
|
|
173
179
|
### DB
|
|
@@ -277,6 +283,8 @@ Flipper writes flag state, so it requires a connection declared `role: :writing`
|
|
|
277
283
|
|
|
278
284
|
Declaring `:flipper_writer` is required (the gem refuses to boot without it) and documents that flag writes need a writable connection, kept separate from the DB plugin's read-only role. Flipper itself reads and writes through whatever adapter you configure for it (e.g. `flipper-active_record`); point that adapter at the same writable database.
|
|
279
285
|
|
|
286
|
+
> ⚠️ Enabling the Flipper write tools lets any permitted principal toggle **every** flag. To restrict security-sensitive flags, use the authorizer's `args` parameter — e.g. `config.authorize { |_p, tool, args| !tool.start_with?("flipper.enable", "flipper.disable") || %w[new_dashboard beta_search].include?(args[:name]) }`.
|
|
287
|
+
|
|
280
288
|
### Rake (allow-listed task runner)
|
|
281
289
|
|
|
282
290
|
Runs operator-approved rake tasks and returns their status and output.
|
|
@@ -290,6 +298,16 @@ config.plugin :rake, connection: false, allowed: [...], timeout: 60 # per-task
|
|
|
290
298
|
|
|
291
299
|
> ⚠️ **Security.** Rake tasks can do anything, so this plugin is **fail-closed and allow-list-only**: it refuses to boot without a non-empty `allowed:` list, and refuses any task not on it. The allow-list is the security boundary — keep it tight and prefer read-only/reporting tasks. Combine with `config.authorize` to scope it to specific principals.
|
|
292
300
|
|
|
301
|
+
### Cache
|
|
302
|
+
|
|
303
|
+
One destructive-but-recoverable tool: wipe the Rails cache.
|
|
304
|
+
|
|
305
|
+
```ruby
|
|
306
|
+
config.plugin :cache, connection: false
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
- **`cache.clear`** — calls `Rails.cache.clear` and returns `{ cleared, store }`. Every cached entry is dropped and the app re-warms from cold, so scope it to trusted principals: `config.authorize { |principal, tool, _args| tool != "cache.clear" || principal == "admin" }`.
|
|
310
|
+
|
|
293
311
|
### Custom Tools
|
|
294
312
|
|
|
295
313
|
Write your own tools by subclassing `TalkToYourApp::Tool` — the same base class the bundled tools use, with typed arguments and **writes allowed** (unlike the read-only DB plugin). Drop one per file in `app/talk_to_your_app/custom_tools/` and it's exposed automatically; no explicit registration.
|
|
@@ -442,30 +460,58 @@ For a local end-to-end walkthrough (run the bundled dummy app, connect a client,
|
|
|
442
460
|
|
|
443
461
|
## Security model
|
|
444
462
|
|
|
445
|
-
- **Fail-closed.** Missing required config (auth, a required connection, a missing adapter gem) raises at boot, not at the first request.
|
|
446
|
-
- **Per-
|
|
447
|
-
- **One audit line per invocation** through `Rails.logger`: timestamp, principal, plugin, tool, params, outcome, duration. Mark sensitive arguments `redact: true`.
|
|
448
|
-
- **What the gem does NOT do:** no "execute arbitrary Ruby" tool; no
|
|
463
|
+
- **Fail-closed.** Missing required config (auth, a required connection, a missing adapter gem) raises at boot, not at the first request. Enabling plugins without `config.authorize` boots with a warning — every authenticated principal can then call every enabled tool.
|
|
464
|
+
- **Per-plugin database roles.** Tools run on the connection their plugin wires, switched via Rails' `connected_to`. The DB plugin defaults to a `:reading` connection (Rails `prevent_writes` + your DB grants). Wiring a `:writing` connection is an explicit opt-in that lets `db.query` execute writes — the **database role / replica is the real write boundary**, not SQL parsing in the gem. See [Read-only is enforced by the database](#read-only-is-enforced-by-the-database).
|
|
465
|
+
- **One audit line per invocation** through `Rails.logger`: timestamp, principal, plugin, tool, params, outcome, duration. Mark sensitive arguments `redact: true`. Full SQL is logged as submitted.
|
|
466
|
+
- **What the gem does NOT do:** no "execute arbitrary Ruby" tool; no OAuth/JWT (static API keys and HTTP Basic only); no stdio transport; no web admin UI; no built-in rate limits. Network exposure, TLS, and DB grants are operator-owned — see [SECURITY.md](SECURITY.md).
|
|
467
|
+
|
|
468
|
+
## Troubleshooting
|
|
469
|
+
|
|
470
|
+
**"Forbidden: Invalid Host header" on every request.** The transport's DNS-rebinding protection only accepts loopback hosts (`127.0.0.1`, `::1`, `localhost`) by default. Any deployment served from a real domain must list it:
|
|
471
|
+
|
|
472
|
+
```ruby
|
|
473
|
+
config.allowed_hosts = TalkToYourApp.rails_hosts # your app's own config.hosts…
|
|
474
|
+
config.allowed_hosts += ["app.example.com"] # …or add it explicitly
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
Entries match a bare host (any port) or a full `host:port`. Wildcard and regexp hosts from Rails' `config.hosts` are deliberately not forwarded — add those as literal strings.
|
|
478
|
+
|
|
479
|
+
**"Session not found" after the first request.** The default transport is stateful: the session created by `initialize` lives in one process's memory. With more than one Puma/Unicorn worker (or multiple replicas), the next request can land on a process that never saw it. Turn on stateless mode:
|
|
480
|
+
|
|
481
|
+
```ruby
|
|
482
|
+
config.stateless = true # required for multi-worker/multi-replica deployments
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
**Boot fails with a `ConfigurationError`.** That's the fail-closed contract working: the message names exactly what's missing (auth not configured, a plugin without `connection:`, a connection naming a missing `database.yml` key). Fix the named thing; nothing fails silently at request time.
|
|
486
|
+
|
|
487
|
+
**A write "slipped through" `db.query`.** The Rails `role: :reading` layer is best-effort and bypassable (data-modifying CTEs, stacked statements). The database account is the real boundary — see [docs/read_only_connections.md](docs/read_only_connections.md).
|
|
449
488
|
|
|
450
489
|
## Compatibility
|
|
451
490
|
|
|
452
491
|
| | Supported |
|
|
453
492
|
| --- | --- |
|
|
454
|
-
| Ruby | 3.
|
|
455
|
-
| Rails | 7.
|
|
493
|
+
| Ruby | 3.3+ |
|
|
494
|
+
| Rails | 7.2+ |
|
|
456
495
|
| MCP spec | 2025-11-25 (Streamable HTTP) |
|
|
457
|
-
| MCP SDK (`mcp` gem) | `~> 0.
|
|
496
|
+
| MCP SDK (`mcp` gem) | `~> 0.25.0` |
|
|
458
497
|
|
|
459
|
-
Tested against Rails 7.
|
|
498
|
+
Tested against Rails 7.2, 8.0, and 8.1 on Ruby 3.3 and 4.0 in CI.
|
|
460
499
|
|
|
461
500
|
## Upgrade discipline
|
|
462
501
|
|
|
463
|
-
The `mcp` SDK is pre-1.0; minor releases may break. This gem pins `~> 0.
|
|
502
|
+
The `mcp` SDK is pre-1.0; minor releases may break. This gem pins `~> 0.25.0` and isolates all SDK touch points to the transport mount and tool compilation. Watch the SDK's releases before bumping, and pin it in your own `Gemfile.lock`.
|
|
503
|
+
|
|
504
|
+
This gem is also pre-1.0 — releases may include breaking changes, each documented with migration steps in the **[CHANGELOG](CHANGELOG.md)**.
|
|
464
505
|
|
|
465
506
|
## Development
|
|
466
507
|
|
|
467
508
|
Working on the gem itself? See **[LOCAL_DEVELOPMENT.md](LOCAL_DEVELOPMENT.md)** for setup, how to run the test suite (and what PostgreSQL/Redis it optionally uses), and how to test against the Rails version matrix.
|
|
468
509
|
|
|
510
|
+
## Security
|
|
511
|
+
|
|
512
|
+
See **[SECURITY.md](SECURITY.md)** for how to report vulnerabilities and the
|
|
513
|
+
production misconfiguration checklist.
|
|
514
|
+
|
|
469
515
|
## License
|
|
470
516
|
|
|
471
517
|
MIT. See [LICENSE](LICENSE).
|
data/SECURITY.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
## Supported versions
|
|
4
|
+
|
|
5
|
+
This gem is pre-1.0. Security fixes land on the latest prerelease / release on
|
|
6
|
+
[RubyGems](https://rubygems.org/gems/talk_to_your_app) and on `main`. Older
|
|
7
|
+
prereleases are not patched.
|
|
8
|
+
|
|
9
|
+
## Reporting a vulnerability
|
|
10
|
+
|
|
11
|
+
Please report security issues privately via GitHub Security Advisories on
|
|
12
|
+
[igorkasyanchuk/talk_to_your_app](https://github.com/igorkasyanchuk/talk_to_your_app/security/advisories/new),
|
|
13
|
+
or email **igor@everai.ai**.
|
|
14
|
+
|
|
15
|
+
Do not open a public issue for vulnerabilities that could let an unauthenticated
|
|
16
|
+
or under-authorized caller reach tools, bypass auth, or escalate privileges.
|
|
17
|
+
|
|
18
|
+
Include:
|
|
19
|
+
|
|
20
|
+
- Affected gem version
|
|
21
|
+
- A minimal reproduction (or clear description)
|
|
22
|
+
- Impact (e.g. unauthenticated access, write via `db.query`, secret leakage)
|
|
23
|
+
|
|
24
|
+
You should hear back within a few business days. We will coordinate disclosure
|
|
25
|
+
once a fix is available.
|
|
26
|
+
|
|
27
|
+
## Operator checklist (misconfiguration is the common failure mode)
|
|
28
|
+
|
|
29
|
+
This gem mounts an authenticated MCP control surface on your Rails app. A valid
|
|
30
|
+
API key can use every enabled plugin unless you scope it. Before production:
|
|
31
|
+
|
|
32
|
+
1. **Use HTTPS** — Bearer / Basic credentials must not travel in clear text.
|
|
33
|
+
2. **High-entropy, per-principal keys** from `ENV` or credentials — never a
|
|
34
|
+
shared forever-secret in source.
|
|
35
|
+
3. **Set `config.authorize`** — without it, every authenticated principal may
|
|
36
|
+
call every enabled tool. Boot logs a warning when it is missing.
|
|
37
|
+
4. **DB plugin** — point `:reading` connections at a **SELECT-only** database
|
|
38
|
+
role or a physical replica, never your writable primary credentials. The gem
|
|
39
|
+
does not parse SQL; the database role is the write boundary. See
|
|
40
|
+
[docs/read_only_connections.md](docs/read_only_connections.md).
|
|
41
|
+
5. **Multi-worker / multi-replica** — set `config.stateless = true`.
|
|
42
|
+
6. **`config.allowed_hosts`** — list every non-loopback host that serves the
|
|
43
|
+
endpoint (the install generator defaults this to `TalkToYourApp.rails_hosts`).
|
|
44
|
+
7. **Enable the minimum plugins** — treat `:sidekiq` / `:solid_queue` job args,
|
|
45
|
+
`:flipper` writes, `:rake`, and `:cache` as sensitive; scope with `authorize`.
|
|
46
|
+
8. **Audit sinks** — full SQL (and tool params) may appear in logs; filter or
|
|
47
|
+
retain accordingly.
|
|
48
|
+
|
|
49
|
+
## Threat model (summary)
|
|
50
|
+
|
|
51
|
+
- **In scope for the gem:** fail-closed boot, request authentication, optional
|
|
52
|
+
per-tool authorization, Host/Origin DNS-rebinding controls (via the MCP SDK),
|
|
53
|
+
audit logging, and refusing to expose plugins that are not explicitly enabled.
|
|
54
|
+
- **Out of scope / operator-owned:** network exposure, TLS termination, database
|
|
55
|
+
grants, Redis/job payload sensitivity, LLM prompt injection against data the
|
|
56
|
+
agent is allowed to read, and custom tools you author.
|
data/docs/plugin_authoring.md
CHANGED
|
@@ -110,4 +110,4 @@ class CachePlugin < TalkToYourApp::Plugin
|
|
|
110
110
|
end
|
|
111
111
|
```
|
|
112
112
|
|
|
113
|
-
That's the whole contract. Bundled plugins (`db`, `
|
|
113
|
+
That's the whole contract. Bundled plugins (`db`, `solid_queue`, `flipper`) are good references — they live in `lib/talk_to_your_app/plugins/`.
|
|
@@ -1,20 +1,33 @@
|
|
|
1
1
|
# Setting up a read-only database connection
|
|
2
2
|
|
|
3
3
|
The DB plugin runs operator-submitted SQL, so it must only ever touch a
|
|
4
|
-
**read-only** connection. `talk_to_your_app`
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
4
|
+
**read-only** connection. `talk_to_your_app` has two layers here, and only one
|
|
5
|
+
of them is a security boundary:
|
|
6
|
+
|
|
7
|
+
1. **Database layer (REQUIRED — this is the security boundary).** You point the
|
|
8
|
+
connection at a genuinely read-only target: a physical replica, or a DB
|
|
9
|
+
role/user granted only `SELECT`. The database server then rejects writes no
|
|
10
|
+
matter how the SQL is shaped.
|
|
11
|
+
2. **Rails layer (best-effort convenience, bypassable).** You declare the
|
|
12
|
+
connection with `role: :reading`, and the gem refuses to boot otherwise.
|
|
13
|
+
Rails' `connected_to(role: :reading)` sets `prevent_writes`, which raises
|
|
14
|
+
`ActiveRecord::ReadOnlyError` on statements it detects as writes — but Rails
|
|
15
|
+
detects writes with a **leading-keyword check**, so statements that start
|
|
16
|
+
with a read keyword and still modify data slip through it:
|
|
17
|
+
|
|
18
|
+
```sql
|
|
19
|
+
-- classified as reads by Rails, but they write:
|
|
20
|
+
WITH gone AS (DELETE FROM users RETURNING *) SELECT count(*) FROM gone;
|
|
21
|
+
SELECT 1; UPDATE accounts SET balance = 0; -- stacked statement (PostgreSQL)
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Treat this layer as a guard rail against accidents, never as the thing that
|
|
25
|
+
stops a hostile or confused agent.
|
|
14
26
|
|
|
15
27
|
Never "fake" the read-only connection by pointing it at your primary writable
|
|
16
|
-
user. The Rails layer
|
|
17
|
-
|
|
28
|
+
user. The Rails layer blocks only the writes it can recognize; against the
|
|
29
|
+
statements above your data is protected by nothing at all. A SELECT-only DB
|
|
30
|
+
user or a replica is **required**, not a recommendation.
|
|
18
31
|
|
|
19
32
|
## The two pieces you configure
|
|
20
33
|
|
|
@@ -141,8 +154,15 @@ read-only role still applies), like SQLite.
|
|
|
141
154
|
|
|
142
155
|
## SQLite
|
|
143
156
|
|
|
144
|
-
SQLite has no separate users or replicas
|
|
145
|
-
|
|
157
|
+
SQLite has no separate users or replicas, so it has **no database-level
|
|
158
|
+
read-only account** — the layer that is the real security boundary on
|
|
159
|
+
PostgreSQL/MySQL does not exist here. That makes an OS-level backstop
|
|
160
|
+
**required, not optional**: run the MCP-serving process with the SQLite file
|
|
161
|
+
read-only (a dedicated read-only bind mount, or file permissions that deny
|
|
162
|
+
write to that process's user). Without it, the bypassable Rails-layer check is
|
|
163
|
+
the only thing between an agent and your data.
|
|
164
|
+
|
|
165
|
+
Declare the connection with `role: :reading` as usual:
|
|
146
166
|
|
|
147
167
|
```yaml
|
|
148
168
|
production:
|
|
@@ -158,15 +178,15 @@ production:
|
|
|
158
178
|
config.connection :replica_readonly, database: "primary_replica", role: :reading
|
|
159
179
|
```
|
|
160
180
|
|
|
161
|
-
`role: :reading` makes Rails raise `ActiveRecord::ReadOnlyError` on
|
|
162
|
-
|
|
163
|
-
|
|
181
|
+
`role: :reading` makes Rails raise `ActiveRecord::ReadOnlyError` on writes it
|
|
182
|
+
detects — remember this check is bypassable (see the top of this document), which
|
|
183
|
+
is why the OS-level file protection above is required.
|
|
164
184
|
|
|
165
185
|
> **Avoid the `query_only` pragma with the Rails SQLite adapter.** It looks
|
|
166
186
|
> tempting, but the adapter writes to the database at connect time (WAL/journal
|
|
167
187
|
> setup), so `PRAGMA query_only = ON` makes even `SELECT`s fail with
|
|
168
|
-
> "attempt to write a readonly database". Use `role: :reading`
|
|
169
|
-
>
|
|
188
|
+
> "attempt to write a readonly database". Use `role: :reading` plus the
|
|
189
|
+
> required OS-level file protection instead.
|
|
170
190
|
|
|
171
191
|
> **SQLite has no per-statement timeout.** The `statement_timeout` option is a
|
|
172
192
|
> no-op there; a long-running query is not cancelled by the gem. Prefer
|
|
@@ -8,18 +8,29 @@
|
|
|
8
8
|
#
|
|
9
9
|
# After editing this file, add to config/routes.rb:
|
|
10
10
|
# mount TalkToYourApp.rack_app, at: TalkToYourApp.configuration.mount_at
|
|
11
|
+
#
|
|
12
|
+
# Production checklist: https://github.com/igorkasyanchuk/talk_to_your_app/blob/main/SECURITY.md
|
|
11
13
|
|
|
12
14
|
TalkToYourApp.configure do |config|
|
|
13
15
|
# --- On/off ---------------------------------------------------------------
|
|
14
16
|
# Global switch. When false the endpoint serves 503 and boot validation is
|
|
15
|
-
# skipped
|
|
16
|
-
#
|
|
17
|
-
#
|
|
17
|
+
# skipped. Recommended: keep production off until auth, a SELECT-only DB
|
|
18
|
+
# connection, authorize, and allowed_hosts are wired — then opt in via ENV.
|
|
19
|
+
# Default true when unset.
|
|
20
|
+
# config.enabled = ENV.fetch("TTYA_ENABLED", Rails.env.local?.to_s) == "true"
|
|
18
21
|
|
|
19
22
|
# --- Endpoint -------------------------------------------------------------
|
|
20
23
|
# Path the MCP Streamable HTTP endpoint is served from. Default "/mcp".
|
|
21
24
|
# config.mount_at = "/mcp"
|
|
22
25
|
|
|
26
|
+
# Stateless transport: every request is self-contained, so any worker or
|
|
27
|
+
# replica can serve it. Required for multi-worker / multi-replica deploys
|
|
28
|
+
# (most production apps) — otherwise a follow-up request can land on a
|
|
29
|
+
# process that never saw `initialize` and fail with "Session not found".
|
|
30
|
+
# Stateless mode does not support SSE streaming or server-initiated
|
|
31
|
+
# notifications. Default false when unset.
|
|
32
|
+
config.stateless = true if Rails.env.production?
|
|
33
|
+
|
|
23
34
|
# --- Server identity (shown to MCP clients in the initialize handshake) ---
|
|
24
35
|
# config.server_name = "talk_to_your_app"
|
|
25
36
|
# config.server_title = "Acme — talk to your app"
|
|
@@ -39,19 +50,27 @@ TalkToYourApp.configure do |config|
|
|
|
39
50
|
config.allowed_hosts = TalkToYourApp.rails_hosts
|
|
40
51
|
# config.allowed_hosts += ["app.example.com"]
|
|
41
52
|
|
|
42
|
-
# --- Authentication (at least one is required)
|
|
53
|
+
# --- Authentication (at least one is required once a plugin is enabled) ---
|
|
43
54
|
# Named API keys. The name becomes the logged principal, so use stable,
|
|
44
|
-
# human-meaningful names.
|
|
55
|
+
# human-meaningful names. One high-entropy token per client (from ENV /
|
|
56
|
+
# credentials) — never a shared forever-secret in source.
|
|
45
57
|
# config.api_keys = { "claude-desktop" => ENV.fetch("TTYA_KEY") }
|
|
46
58
|
#
|
|
47
59
|
# HTTP Basic auth. The callable receives (username, password) and returns
|
|
48
60
|
# true/false. Wire it to whatever the host app uses.
|
|
49
61
|
# config.basic_auth { |username, password| User.authenticate(username, password) }
|
|
50
62
|
|
|
51
|
-
#
|
|
52
|
-
#
|
|
53
|
-
#
|
|
54
|
-
#
|
|
63
|
+
# Strongly recommended in production. Without it, every authenticated
|
|
64
|
+
# principal may call every enabled tool (boot logs a warning). Receives
|
|
65
|
+
# (principal, tool_name, args) — args is the tool's argument hash, so you can
|
|
66
|
+
# authorize per flag/task/argument, not just per tool.
|
|
67
|
+
# config.authorize do |principal, tool, _args|
|
|
68
|
+
# case principal
|
|
69
|
+
# when "ci-readonly-bot" then tool.start_with?("db.")
|
|
70
|
+
# when "admin" then true
|
|
71
|
+
# else false
|
|
72
|
+
# end
|
|
73
|
+
# end
|
|
55
74
|
|
|
56
75
|
# --- Connections ----------------------------------------------------------
|
|
57
76
|
# Declare named DB connections, then wire one into each plugin that needs a
|
|
@@ -77,10 +96,11 @@ TalkToYourApp.configure do |config|
|
|
|
77
96
|
# `connection: false` to opt out (no wired SQL connection).
|
|
78
97
|
# config.plugin :db, connection: :read # db.query caps results at 2000 rows
|
|
79
98
|
# config.plugin :db, connection: :read, max_rows: 5000 # ...raise/lower the cap (nil/false/:unlimited removes it)
|
|
80
|
-
# config.plugin :sidekiq, connection: false # Sidekiq job metrics (reads Redis)
|
|
99
|
+
# config.plugin :sidekiq, connection: false # Sidekiq job metrics (reads Redis; job args may contain secrets)
|
|
81
100
|
# config.plugin :solid_queue, connection: false # Solid Queue job metrics (both may run at once)
|
|
82
101
|
# config.plugin :flipper, connection: :write # Flipper writes flag state — requires a :writing connection
|
|
83
102
|
# config.plugin :rake, connection: false, allowed: ["stats", "report:generate"] # optional: timeout: 60 (per-task seconds, default 20)
|
|
103
|
+
# config.plugin :cache, connection: false # cache.clear (Rails.cache.clear) — destructive, scope with config.authorize
|
|
84
104
|
# config.plugin :custom_tools, connection: false # or connection: :read to give tools a default
|
|
85
105
|
#
|
|
86
106
|
# DANGER — running :db against a writable connection. Wiring a :writing
|