maquina-generators 0.8.1 → 0.10.0
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/README.md +191 -9
- data/Rakefile +1 -1
- data/lib/generators/maquina/app/USAGE +6 -0
- data/lib/generators/maquina/app/app_generator.rb +59 -2
- data/lib/generators/maquina/app/templates/README.md.erb +27 -0
- data/lib/generators/maquina/app/templates/app/views/home/index.html.erb +4 -1
- data/lib/generators/maquina/backstage_signin/backstage_signin.rb +245 -0
- data/lib/generators/maquina/backstage_signin/templates/test/integration/backstage_access_test.rb +78 -0
- data/lib/generators/maquina/csp_reports/csp_reports_generator.rb +13 -1
- data/lib/generators/maquina/csp_reports/templates/app/models/mcp/tools/csp_reports.rb +71 -0
- data/lib/generators/maquina/csp_reports/templates/test/models/mcp/tools/csp_reports_test.rb +35 -0
- data/lib/generators/maquina/mcp/USAGE +37 -0
- data/lib/generators/maquina/mcp/mcp_generator.rb +188 -0
- data/lib/generators/maquina/mcp/mcp_sibling_tools.rb +104 -0
- data/lib/generators/maquina/mcp/templates/app/controllers/concerns/mcp_authenticatable.rb +63 -0
- data/lib/generators/maquina/mcp/templates/app/controllers/mcp_controller.rb +205 -0
- data/lib/generators/maquina/mcp/templates/app/models/mcp/tool_definitions.rb +38 -0
- data/lib/generators/maquina/mcp/templates/app/models/mcp/tool_error.rb +17 -0
- data/lib/generators/maquina/mcp/templates/app/models/mcp/tools/app.rb +51 -0
- data/lib/generators/maquina/mcp/templates/app/models/mcp/tools.rb +83 -0
- data/lib/generators/maquina/mcp/templates/app/models/mcp/user_scope.rb +50 -0
- data/lib/generators/maquina/mcp/templates/config/initializers/mcp_cors.rb +38 -0
- data/lib/generators/maquina/mcp/templates/test/integration/mcp_conformance_test.rb +223 -0
- data/lib/generators/maquina/mcp/templates/test/integration/mcp_flow_test.rb.tt +65 -0
- data/lib/generators/maquina/mcp/templates/test/models/mcp/tools_test.rb.tt +145 -0
- data/lib/generators/maquina/mcp/templates/test/test_helpers/mcp_test_helper.rb +89 -0
- data/lib/generators/maquina/mcp_tool/USAGE +26 -0
- data/lib/generators/maquina/mcp_tool/mcp_tool_generator.rb +239 -0
- data/lib/generators/maquina/mcp_tool/templates/test/models/mcp/tools/tool_test.rb.tt +49 -0
- data/lib/generators/maquina/mission_control_jobs/USAGE +3 -0
- data/lib/generators/maquina/mission_control_jobs/mission_control_jobs_generator.rb +19 -5
- data/lib/generators/maquina/mission_control_jobs/templates/app/views/backstage_dashboard/index.html.erb.tt +1 -1
- data/lib/generators/maquina/mission_control_jobs/templates/app/views/layouts/_admin_navigation.html.erb.tt +12 -11
- data/lib/generators/maquina/mission_control_jobs/templates/app/views/mission_control/jobs/jobs/_filters.html.erb +1 -1
- data/lib/generators/maquina/mission_control_jobs/templates/app/views/mission_control/jobs/jobs/_general_information.html.erb +1 -1
- data/lib/generators/maquina/mission_control_jobs/templates/app/views/mission_control/jobs/jobs/_job.html.erb +1 -1
- data/lib/generators/maquina/mission_control_jobs/templates/app/views/mission_control/jobs/jobs/failed/_job.html.erb +1 -1
- data/lib/generators/maquina/mission_control_jobs/templates/app/views/mission_control/jobs/queues/_job.html.erb +1 -1
- data/lib/generators/maquina/mission_control_jobs/templates/app/views/mission_control/jobs/shared/_job.html.erb +1 -1
- data/lib/generators/maquina/mission_control_jobs/templates/app/views/mission_control/jobs/workers/_worker.html.erb +1 -1
- data/lib/generators/maquina/oauth/USAGE +32 -0
- data/lib/generators/maquina/oauth/oauth_generator.rb +352 -0
- data/lib/generators/maquina/oauth/templates/app/controllers/connected_applications_controller.rb +26 -0
- data/lib/generators/maquina/oauth/templates/app/controllers/oauth/authorization_servers_controller.rb +24 -0
- data/lib/generators/maquina/oauth/templates/app/controllers/oauth/authorizations_controller.rb +92 -0
- data/lib/generators/maquina/oauth/templates/app/controllers/oauth/protected_resources_controller.rb +14 -0
- data/lib/generators/maquina/oauth/templates/app/controllers/oauth/revocations_controller.rb +20 -0
- data/lib/generators/maquina/oauth/templates/app/controllers/oauth/tokens_controller.rb +67 -0
- data/lib/generators/maquina/oauth/templates/app/models/access_token.rb +83 -0
- data/lib/generators/maquina/oauth/templates/app/models/connected_application.rb +49 -0
- data/lib/generators/maquina/oauth/templates/app/models/mcp.rb.tt +21 -0
- data/lib/generators/maquina/oauth/templates/app/models/oauth_authorization_code.rb +52 -0
- data/lib/generators/maquina/oauth/templates/app/models/oauth_client/metadata_fetcher.rb +195 -0
- data/lib/generators/maquina/oauth/templates/app/models/oauth_client.rb +50 -0
- data/lib/generators/maquina/oauth/templates/app/views/connected_applications/index.html.erb +43 -0
- data/lib/generators/maquina/oauth/templates/app/views/oauth/authorizations/error.html.erb +8 -0
- data/lib/generators/maquina/oauth/templates/app/views/oauth/authorizations/new.html.erb +36 -0
- data/lib/generators/maquina/oauth/templates/config/locales/oauth.en.yml +34 -0
- data/lib/generators/maquina/oauth/templates/config/locales/oauth.es.yml +34 -0
- data/lib/generators/maquina/oauth/templates/migration_create_access_tokens.rb.tt +20 -0
- data/lib/generators/maquina/oauth/templates/migration_create_oauth_authorization_codes.rb.tt +19 -0
- data/lib/generators/maquina/oauth/templates/migration_create_oauth_clients.rb.tt +14 -0
- data/lib/generators/maquina/oauth/templates/test/fixtures/access_tokens.yml +48 -0
- data/lib/generators/maquina/oauth/templates/test/fixtures/oauth_authorization_codes.yml +11 -0
- data/lib/generators/maquina/oauth/templates/test/fixtures/oauth_clients.yml +7 -0
- data/lib/generators/maquina/oauth/templates/test/integration/connected_applications_test.rb.tt +70 -0
- data/lib/generators/maquina/oauth/templates/test/integration/oauth_authorization_test.rb.tt +150 -0
- data/lib/generators/maquina/oauth/templates/test/integration/oauth_discovery_test.rb +44 -0
- data/lib/generators/maquina/oauth/templates/test/integration/oauth_tokens_test.rb +147 -0
- data/lib/generators/maquina/oauth/templates/test/models/access_token_test.rb +78 -0
- data/lib/generators/maquina/oauth/templates/test/models/connected_application_test.rb +41 -0
- data/lib/generators/maquina/oauth/templates/test/models/oauth_authorization_code_test.rb +58 -0
- data/lib/generators/maquina/oauth/templates/test/test_helpers/oauth_test_helper.rb.tt +82 -0
- data/lib/generators/maquina/rack_attack/rack_attack_generator.rb +4 -1
- data/lib/generators/maquina/rack_attack/templates/config/initializers/rack_attack.rb.tt +317 -4
- data/lib/generators/maquina/security/USAGE +10 -0
- data/lib/generators/maquina/security/security_generator.rb +77 -7
- data/lib/generators/maquina/security/templates/app/models/mcp/tools/security.rb.tt +114 -0
- data/lib/generators/maquina/security/templates/app/views/backstage_dashboard/index.html.erb.tt +1 -1
- data/lib/generators/maquina/security/templates/app/views/layouts/_admin_navigation.html.erb.tt +12 -11
- data/lib/generators/maquina/security/templates/test/models/mcp/tools/security_test.rb +99 -0
- data/lib/generators/maquina/solid_errors/USAGE +15 -9
- data/lib/generators/maquina/solid_errors/solid_errors_generator.rb +40 -31
- data/lib/generators/maquina/solid_errors/templates/app/agents/errors_query.rb +48 -27
- data/lib/generators/maquina/solid_errors/templates/app/models/mcp/tools/solid_errors.rb.tt +114 -0
- data/lib/generators/maquina/solid_errors/templates/app/views/backstage_dashboard/index.html.erb.tt +1 -1
- data/lib/generators/maquina/solid_errors/templates/app/views/layouts/_admin_navigation.html.erb.tt +12 -11
- data/lib/generators/maquina/solid_errors/templates/app/views/solid_errors/errors/show/_actions.html.erb +0 -15
- data/lib/generators/maquina/solid_errors/templates/app/views/solid_errors/errors/show/_error_details.html.erb +5 -5
- data/lib/generators/maquina/solid_errors/templates/app/views/solid_errors/occurrences/_collection.html.erb +1 -1
- data/lib/generators/maquina/solid_errors/templates/test/models/mcp/tools/solid_errors_test.rb +109 -0
- data/lib/generators/maquina/solid_queue/USAGE +11 -0
- data/lib/generators/maquina/solid_queue/solid_queue_generator.rb +25 -1
- data/lib/generators/maquina/solid_queue/templates/app/models/mcp/tools/solid_queue.rb.tt +163 -0
- data/lib/generators/maquina/solid_queue/templates/test/models/mcp/tools/solid_queue_test.rb +99 -0
- data/lib/maquina_generators/version.rb +1 -1
- data/test/generators/maquina/app_generator_test.rb +99 -0
- data/test/generators/maquina/backstage_signin_test.rb +209 -0
- data/test/generators/maquina/csp_reports_generator_test.rb +125 -6
- data/test/generators/maquina/mcp_generator_test.rb +426 -0
- data/test/generators/maquina/mcp_tool_generator_test.rb +235 -0
- data/test/generators/maquina/mission_control_jobs_generator_test.rb +23 -0
- data/test/generators/maquina/oauth_client_metadata_fetcher_test.rb +290 -0
- data/test/generators/maquina/oauth_generator_test.rb +520 -0
- data/test/generators/maquina/rack_attack_generator_test.rb +71 -0
- data/test/generators/maquina/rack_attack_rules_test.rb +304 -0
- data/test/generators/maquina/security_generator_test.rb +188 -13
- data/test/generators/maquina/solid_errors_generator_test.rb +243 -33
- data/test/generators/maquina/solid_queue_generator_test.rb +175 -25
- data/test/test_helper.rb +1 -0
- metadata +69 -29
- data/lib/generators/maquina/solid_errors/templates/app/controllers/failures_mcp_controller.rb.tt +0 -65
- data/test/tmp/Gemfile +0 -3
- data/test/tmp/app/controllers/concerns/authentication.rb +0 -2
- data/test/tmp/app/controllers/concerns/passkey_challenge.rb +0 -42
- data/test/tmp/app/controllers/concerns/passkey_reauthentication.rb +0 -26
- data/test/tmp/app/controllers/passkey_challenges_controller.rb +0 -24
- data/test/tmp/app/controllers/passkeys_controller.rb +0 -68
- data/test/tmp/app/controllers/session/passkey_challenges_controller.rb +0 -18
- data/test/tmp/app/controllers/session/passkeys_controller.rb +0 -55
- data/test/tmp/app/javascript/controllers/passkey_registration_controller.js +0 -109
- data/test/tmp/app/javascript/controllers/passkey_sign_in_controller.js +0 -113
- data/test/tmp/app/models/email_verification.rb +0 -2
- data/test/tmp/app/models/passkey.rb +0 -36
- data/test/tmp/app/models/user.rb +0 -47
- data/test/tmp/app/views/passkeys/index.html.erb +0 -77
- data/test/tmp/app/views/sessions/_passkey.html.erb +0 -23
- data/test/tmp/app/views/sessions/new.html.erb +0 -37
- data/test/tmp/config/initializers/rack_attack.rb +0 -9
- data/test/tmp/config/initializers/webauthn.rb +0 -23
- data/test/tmp/config/locales/passkeys.en.yml +0 -46
- data/test/tmp/config/locales/passkeys.es.yml +0 -46
- data/test/tmp/config/routes.rb +0 -9
- data/test/tmp/db/migrate/20261014213621_add_webauthn_id_to_users.rb +0 -6
- data/test/tmp/db/migrate/20261014213622_create_passkeys.rb +0 -20
- data/test/tmp/test/integration/passkeys_test.rb +0 -99
- data/test/tmp/test/test_helper.rb +0 -4
- data/test/tmp/test/test_helpers/webauthn_test_helper.rb +0 -38
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 490a44073c9b568e54c4b2b83257db4ad4f279ac9e610f022b7556ca81734606
|
|
4
|
+
data.tar.gz: dcae3d4b2aa0f3866775c1f162522824614397dd399403c715e38f3fb67cb8cb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8dffc54baad6e0b5443de42b77802d2fcd35253ebd82acc155c874d4f254181174a40f82e3cf216c4ba0dcbbd29be0bd50569acccaf17e795e65c1c0de7e1da1
|
|
7
|
+
data.tar.gz: 69cf94a55c483bc857d6780d0e89e36f9fb1228959910381ea2e45eb08e07f172f2a6b6676f2b981cdbb61f5c75bf6468e53ed608891eaed459c20fc66d58fb2
|
data/README.md
CHANGED
|
@@ -115,6 +115,141 @@ WEBAUTHN_ORIGINS=https://example.com,https://app.example.com
|
|
|
115
115
|
- **In a browser:** Chrome's virtual authenticator works in DevTools (More tools > WebAuthn) or over CDP (`WebAuthn.addVirtualAuthenticator` with `ctap2`, `internal`, a resident key and user verification). It answers the sign-in page's autofill request on its own, so loading `/session/new` signs you in without a click. To test the button, make `PublicKeyCredential.isConditionalMediationAvailable` return `false`.
|
|
116
116
|
- **Dev server on another port:** set `WEBAUTHN_ORIGINS=http://localhost:<port>`.
|
|
117
117
|
|
|
118
|
+
### OAuth -- OAuth 2.1 Sign-In for AI Clients
|
|
119
|
+
|
|
120
|
+
**OAuth** lets AI clients (the MCP Inspector, Claude, any MCP client) sign people in to your app on top of Clave. It is the authorization server behind `maquina:mcp`. Clients identify themselves with a Client ID Metadata Document (CIMD): the `client_id` is an `https` URL whose JSON names the client and its redirect URIs, so there is no dynamic registration and no client secret. Every flow is PKCE (`S256` only), and people sign in with the email code they already use.
|
|
121
|
+
|
|
122
|
+
No OAuth gem is added: the endpoints, models and fetcher are generated into your app.
|
|
123
|
+
|
|
124
|
+
#### What it generates
|
|
125
|
+
|
|
126
|
+
- **Migrations:** `create_oauth_clients`, `create_oauth_authorization_codes`, `create_access_tokens` (refresh tokens live on the access token row)
|
|
127
|
+
- **Models:** `OauthClient` (fetched metadata, refreshed after 24 hours), `OauthAuthorizationCode` (10-minute, single-use, PKCE-checked), `AccessToken` (1-hour access token, rotating refresh token, `read`/`write` scopes; only SHA-256 digests are stored) and `ConnectedApplication`, which groups a person's grants per client
|
|
128
|
+
- **CIMD fetcher:** `OauthClient::MetadataFetcher`, plain `Net::HTTP`. It refuses non-`https` URLs, literal IPs, hosts that resolve to loopback, private, link-local, unspecified or `100.64.0.0/10` addresses, redirects, bodies over 16 KB, responses slower than 3 seconds and documents whose `client_id` does not match
|
|
129
|
+
- **Discovery:** `GET /.well-known/oauth-authorization-server` and `GET /.well-known/oauth-protected-resource/mcp`
|
|
130
|
+
- **Authorize:** `Oauth::AuthorizationsController` at `/oauth/authorize`, the consent screen. A signed-out visitor signs in with Clave and lands back on the exact authorize URL. An unknown client or a redirect URI the client does not list gets an error page and is never redirected
|
|
131
|
+
- **Token and revoke:** `POST /oauth/token` (authorization code and refresh token grants) and `POST /oauth/revoke` (RFC 7009), JSON with `Cache-Control: no-store`
|
|
132
|
+
- **Connected apps:** `ConnectedApplicationsController` at `<prefix>/connected_apps`, where people see each connected client with its scopes and last use, and revoke it. Revoking stops its tokens at once
|
|
133
|
+
- **Gate:** `app/models/mcp.rb` with `Mcp.enabled_for?(user)`, written only if missing. Your app owns it
|
|
134
|
+
- **Rack::Attack:** `/mcp` and `/oauth/token` leave the per-IP throttle and flood ban (they are limited per token and per client instead), plus an `oauth/token/client` throttle of 30 per minute, when `config/initializers/rack_attack.rb` exists
|
|
135
|
+
- **Locale files:** English and Spanish (`config/locales/oauth.*.yml`)
|
|
136
|
+
- **Tests:** fixtures (`oauth_clients`, `oauth_authorization_codes`, `access_tokens`, and `mcp_*` users and account), `OauthTestHelper`, and tests for PKCE, code reuse and expiry, refresh rotation and reuse, `resource` and `iss` checks, the return-to after sign-in and revoking
|
|
137
|
+
|
|
138
|
+
#### Usage
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
rails g maquina:clave # first
|
|
142
|
+
rails g maquina:oauth
|
|
143
|
+
rails db:migrate
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Anything the generator cannot place safely (a Rack::Attack initializer that no longer has the expected shape) is listed after the install instead of being forced.
|
|
147
|
+
|
|
148
|
+
#### Options
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
rails g maquina:oauth # Full install
|
|
152
|
+
rails g maquina:oauth --skip-views # Endpoints only; bring your own consent and Connected apps views
|
|
153
|
+
rails g maquina:oauth --prefix /account # Connected apps at /account/connected_apps (default: /settings)
|
|
154
|
+
rails g maquina:oauth --flag beta_mcp # Gate consent and Connected apps behind a feature flag
|
|
155
|
+
rails g maquina:oauth --quiet # No post-install message
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
#### Feature flag
|
|
159
|
+
|
|
160
|
+
Without `--flag`, `Mcp.enabled_for?` returns `true` for everyone. With `--flag beta_mcp`, it reads `Rails.application.credentials.features.beta_mcp`, then `ENV["BETA_MCP"]`. When it is off for someone, consent answers `access_denied`, Connected apps is a 404, their MCP tools return nothing and `server/discover` still answers. Replace the method body with Flipper or a column when you need per-user rollout.
|
|
161
|
+
|
|
162
|
+
### MCP -- Model Context Protocol Server
|
|
163
|
+
|
|
164
|
+
**MCP** gives your app a standalone [MCP](https://modelcontextprotocol.io) server at `POST /mcp`, signed in through `maquina:oauth`. It speaks protocol `2026-07-28` over plain HTTP: no SSE, no sessions, no `mcp` gem. It ships a `whoami` tool that proves the wiring works, and `maquina:mcp_tool` adds more.
|
|
165
|
+
|
|
166
|
+
#### What it generates
|
|
167
|
+
|
|
168
|
+
- **Controller:** `McpController` (`ActionController::API`) with `McpAuthenticatable`, which reads the `Authorization: Bearer` token and checks that it was issued for `<base>/mcp`. A missing or bad token gets 401 with `WWW-Authenticate` pointing at the protected-resource metadata. `GET` and `DELETE /mcp` return 405
|
|
169
|
+
- **Protocol:** `server/discover` (no token needed), `tools/list` and `tools/call`. The `Mcp-Method` and `Mcp-Name` headers must match the body, `params._meta` must carry protocol `2026-07-28`, and the JSON-RPC error codes live in one table in `McpController`. A write tool called with a `read` token gets 403 `insufficient_scope`
|
|
170
|
+
- **Rate limit:** 120 requests per minute per token (per IP without one)
|
|
171
|
+
- **CORS:** `config/initializers/mcp_cors.rb`, a small Rack middleware for `/mcp`, `/oauth/token`, `/oauth/revoke` and `/.well-known/oauth-*` (never `/oauth/authorize`); no `rack-cors`
|
|
172
|
+
- **Registry:** `app/models/mcp/` -- `Mcp::Tools` (dispatch), `Mcp::ToolDefinitions` (what a user sees), `Mcp::ToolError` and `Mcp::UserScope` (the token's user and the records it may reach). Every file in `app/models/mcp/tools/` is a module with `DEFINITIONS`, `WRITE_TOOLS`, `ADMIN_ONLY` and one method per tool. Arguments are checked against the tool's JSON Schema (required keys, types, enums) before dispatch
|
|
173
|
+
- **Tool:** `app/models/mcp/tools/app.rb` with `whoami` (email, granted scope, server and protocol versions, and the flag state with `--flag`), written only if missing
|
|
174
|
+
- **Tests:** `McpTestHelper` (`mcp_post`, `oauth_token_for`), `test/integration/mcp_flow_test.rb` (discover, authorize, consent, token, `whoami`, revoke, 401), `test/integration/mcp_conformance_test.rb` and `test/models/mcp/tools_test.rb`, which fails when a tool that writes is missing from its module's `WRITE_TOOLS`
|
|
175
|
+
|
|
176
|
+
#### Usage
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
rails g maquina:oauth # first
|
|
180
|
+
rails g maquina:mcp
|
|
181
|
+
rails db:migrate
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Or all at once: `rails g maquina:app --auth clave --mcp`.
|
|
185
|
+
|
|
186
|
+
#### Options
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
rails g maquina:mcp # Full install
|
|
190
|
+
rails g maquina:mcp --flag beta_mcp # Set the flag in app/models/mcp.rb if it still says FLAG = nil
|
|
191
|
+
rails g maquina:mcp --prefix /account # The Connected apps prefix you gave maquina:oauth (default: /settings)
|
|
192
|
+
rails g maquina:mcp --skip-views # You ran maquina:oauth --skip-views: the flow test skips the consent and Connected apps pages
|
|
193
|
+
rails g maquina:mcp --quiet # No post-install message
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
#### Connecting a client
|
|
197
|
+
|
|
198
|
+
Start the app and run the [MCP Inspector](https://github.com/modelcontextprotocol/inspector):
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
bin/dev
|
|
202
|
+
npx @modelcontextprotocol/inspector
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Choose the Streamable HTTP transport, enter `http://localhost:3000/mcp` and connect. The Inspector discovers the authorization server, sends you to the consent screen (sign in with your email code if needed), and lists `whoami`. The app then shows up at `/settings/connected_apps`, where revoking it makes the next call answer 401.
|
|
206
|
+
|
|
207
|
+
#### Operational tools from other generators
|
|
208
|
+
|
|
209
|
+
When `app/models/mcp/tools.rb` exists, these generators also write an MCP tool module. Every one is `ADMIN_ONLY`: they read data across every account, so only people on the backstage list (`User#backstage_access?`, see [Backstage access](#backstage-access----clave-sign-in-or-basic-auth)) see or call them. An account's `admin` role is not enough, since registration makes everyone who signs up the admin of their own account. Anyone else's token gets "unknown tool". Lists are capped at 50 rows and times are ISO 8601 UTC.
|
|
210
|
+
|
|
211
|
+
The tools are read-only unless the generator runs with `--mcp-write`, which adds write tools that need a token with the `write` scope and log each call with the user's id.
|
|
212
|
+
|
|
213
|
+
| Generator | Tool file | Tools |
|
|
214
|
+
|-----------|-----------|-------|
|
|
215
|
+
| `maquina:solid_errors` | `tools/solid_errors.rb` | `list_failures`, `get_failure`, `top_exceptions` (copies `ErrorsQuery`, `FailureTriage` and `ErrorRedactor` into `app/agents/` if missing); `resolve_failure` with `--mcp-write` |
|
|
216
|
+
| `maquina:solid_queue` | `tools/solid_queue.rb` | `queue_status`, `list_failed_jobs`, `get_failed_job` (arguments redacted); `retry_failed_job` and `discard_failed_job` with `--mcp-write` |
|
|
217
|
+
| `maquina:security` | `tools/security.rb` | `recent_blocks`, `security_summary`; `unban_address` with `--mcp-write` (adds `Rack::Attack.unban!` to an initializer written before it existed) |
|
|
218
|
+
| `maquina:csp_reports` | `tools/csp_reports.rb` | `list_csp_reports`, `csp_report_summary` |
|
|
219
|
+
|
|
220
|
+
The install order does not matter. A sibling run after `maquina:mcp` writes its own tool file. `maquina:mcp` run after the siblings backfills a tool file for each one it finds (`config/initializers/solid_errors.rb`, `config/solid_queue.yml` or `config/queue.yml`, `app/models/security/abuse_event.rb`, `csp_violations` in `db/security_schema.rb`), using the read-only variants. Existing tool files are never overwritten (to add the write tools later, delete the file and run the generator again with `--mcp-write`), and without MCP the siblings write and print nothing about it.
|
|
221
|
+
|
|
222
|
+
### MCP Tool -- Add a Tool to the MCP Server
|
|
223
|
+
|
|
224
|
+
**MCP Tool** adds a tool to `app/models/mcp/tools/app.rb`: its JSON Schema definition, a method stub and a test, without editing the registry by hand. It needs `maquina:mcp`.
|
|
225
|
+
|
|
226
|
+
#### Usage
|
|
227
|
+
|
|
228
|
+
```bash
|
|
229
|
+
rails g maquina:mcp_tool list_posts status:enum[draft,published]? limit:integer?
|
|
230
|
+
rails g maquina:mcp_tool publish_post id:integer --write
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
#### Arguments
|
|
234
|
+
|
|
235
|
+
- `NAME`: lowercase letters, digits and underscores, starting with a letter. A name already in `tools/app.rb` changes nothing; a name in another tool file stops the generator.
|
|
236
|
+
- `name:type`: `string`, `integer`, `number`, `boolean` or `enum[a,b]` (a string limited to those values). A trailing `?` makes the argument optional; every other argument is required.
|
|
237
|
+
|
|
238
|
+
#### Options
|
|
239
|
+
|
|
240
|
+
```bash
|
|
241
|
+
rails g maquina:mcp_tool NAME [arg:type ...] # Read-only tool (readOnlyHint: true)
|
|
242
|
+
rails g maquina:mcp_tool NAME [arg:type ...] --write # Needs the write scope; added to WRITE_TOOLS
|
|
243
|
+
rails g maquina:mcp_tool NAME --quiet # No "Items to finish" message
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
#### What it generates
|
|
247
|
+
|
|
248
|
+
- The definition in `DEFINITIONS`, the name in `WRITE_TOOLS` with `--write`, and `def self.<name>(scope:, ...)` that raises `Mcp::ToolError, "<name> is not implemented yet"`. If a marker comment in `tools/app.rb` is gone, the snippet is printed instead.
|
|
249
|
+
- `test/models/mcp/tools/<name>_test.rb`: listed for a `read` token, -32602 for a missing required argument, 403 `insufficient_scope` for a `read` token with `--write`, and a skipped cross-user test (a record owned by `users(:mcp_other)` must come back as not found) to fill in.
|
|
250
|
+
|
|
251
|
+
Afterwards, write the description for the model (plain English, units stated), the method body (reach records through `scope.records(:posts)` or `scope.find(:posts, id)` so a tool never sees another user's data) and the cross-user test setup.
|
|
252
|
+
|
|
118
253
|
### Registration -- Password-Based Authentication with Accounts
|
|
119
254
|
|
|
120
255
|
**Registration** generates a password-based authentication system with multi-tenant account support. It builds on top of the Rails 8 authentication generator, adding an Account model (tenant), user roles, and a registration flow.
|
|
@@ -162,6 +297,30 @@ All generated code lives in your app -- edit it directly:
|
|
|
162
297
|
|
|
163
298
|
---
|
|
164
299
|
|
|
300
|
+
### Backstage Access -- Clave Sign-In or Basic Auth
|
|
301
|
+
|
|
302
|
+
Solid Errors, Mission Control Jobs and Security share one `BackstageController` and one way in.
|
|
303
|
+
|
|
304
|
+
**With Clave installed** (`app/models/email_verification.rb` is there), each of them puts the backstage behind the app's own sign-in:
|
|
305
|
+
|
|
306
|
+
- `BackstageController` includes `Authentication` and raises `ActiveRecord::RecordNotFound` unless `Current.user.backstage_access?`. Signed out, a page redirects to the sign-in (303 for PATCH/PUT/DELETE, so the engines' Resolve and Discard buttons are not re-sent); signed in but not listed, it is a 404, so the pages never confirm they exist.
|
|
307
|
+
- `User.backstage_emails` and `User#backstage_access?` are added to `app/models/user.rb`. Access is an unblocked user whose email is on the list in credentials:
|
|
308
|
+
|
|
309
|
+
```yaml
|
|
310
|
+
backstage:
|
|
311
|
+
emails:
|
|
312
|
+
- you@example.com
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
An empty list is nobody. The same predicate gates the admin-only MCP tools.
|
|
316
|
+
- The Basic Auth comes out of the overview, the Security page and both engines' initializers (Mission Control gets `http_basic_auth_enabled = false`). Do not set `backstage.username` or `password`: Solid Errors turns its Basic Auth back on when a password is set.
|
|
317
|
+
- `test/integration/backstage_access_test.rb` checks signed out, not listed, blocked, an empty list and a listed user, with `backstage_operator` and `backstage_member` fixtures.
|
|
318
|
+
- A generator run later keeps the backstage on the sign-in. A file you changed by hand keeps its Basic Auth, and the post-install message names it.
|
|
319
|
+
|
|
320
|
+
**Without Clave** (no auth, or `maquina:registration`), the backstage uses HTTP Basic Auth with `backstage.username` / `password` from credentials, or each generator's ENV variables, and answers 503 until they are set.
|
|
321
|
+
|
|
322
|
+
---
|
|
323
|
+
|
|
165
324
|
### Solid Errors -- Error Tracking Dashboard
|
|
166
325
|
|
|
167
326
|
**Solid Errors** installs the [solid_errors](https://github.com/fractaledmind/solid_errors) gem with HTTP authentication and engine mounting.
|
|
@@ -169,11 +328,11 @@ All generated code lives in your app -- edit it directly:
|
|
|
169
328
|
#### What it generates
|
|
170
329
|
|
|
171
330
|
- **BackstageController:** Inherits from `ActionController::Base` (bypasses app's ApplicationController concerns)
|
|
172
|
-
- **Initializer:**
|
|
331
|
+
- **Initializer:** Database connection config, and Basic Auth from credentials or ENV when the app has no Clave (see [Backstage access](#backstage-access----clave-sign-in-or-basic-auth))
|
|
173
332
|
- **Route:** Mounts `SolidErrors::Engine` at `<prefix>/errors`
|
|
174
333
|
- **Mailer templates:** Self-contained `error_occurred` HTML and text templates, so `send_emails` works without the dashboard's helpers
|
|
175
334
|
- **Admin navigation:** Shared navigation bar with Overview, Errors, Jobs and Security tabs
|
|
176
|
-
- **Admin overview:** `BackstageDashboardController` at the prefix root with cards for each installed dashboard and a `@metrics` slot for your own aggregate counts; answers 503 until backstage credentials are set
|
|
335
|
+
- **Admin overview:** `BackstageDashboardController` at the prefix root with cards for each installed dashboard and a `@metrics` slot for your own aggregate counts; behind the backstage sign-in, or with Basic Auth answers 503 until backstage credentials are set
|
|
177
336
|
- **Custom layout:** Tailwind-styled layout with admin navigation and toast flash messages
|
|
178
337
|
- **Stimulus controllers:** `clipboard_controller.js` and `backtrace_filter_controller.js`
|
|
179
338
|
- **Custom views:** Tailwind-styled views to override the gem defaults (included by default, use `--no-copy-views` to skip)
|
|
@@ -192,17 +351,22 @@ The generator automatically runs `bundle install`. After running, execute `bin/r
|
|
|
192
351
|
```bash
|
|
193
352
|
rails g maquina:solid_errors --prefix /admin # Default (with custom views)
|
|
194
353
|
rails g maquina:solid_errors --prefix /admin --no-copy-views # Without custom views
|
|
354
|
+
rails g maquina:solid_errors --prefix /admin --mcp-write # Also generate the resolve_failure MCP tool
|
|
195
355
|
rails g maquina:solid_errors --prefix /backstage \
|
|
196
356
|
--user-env-var ADMIN_USER --password-env-var ADMIN_PASSWORD # Custom env vars
|
|
197
357
|
```
|
|
198
358
|
|
|
199
359
|
#### Authentication
|
|
200
360
|
|
|
201
|
-
|
|
361
|
+
With Clave, the app's sign-in and the backstage list; see [Backstage access](#backstage-access----clave-sign-in-or-basic-auth). Without it, Basic Auth credentials are resolved in order:
|
|
202
362
|
|
|
203
363
|
1. `Rails.application.credentials.backstage.username` / `.password`
|
|
204
364
|
2. `ENV["SOLID_ERRORS_USER"]` / `ENV["SOLID_ERRORS_PASSWORD"]` (configurable)
|
|
205
365
|
|
|
366
|
+
#### MCP
|
|
367
|
+
|
|
368
|
+
With `maquina:mcp` installed, it also writes admin-only MCP tools (`list_failures`, `get_failure`, `top_exceptions`) to `app/models/mcp/tools/solid_errors.rb`. `--mcp-write` adds `resolve_failure`, which needs the `write` scope. See [Operational tools from other generators](#operational-tools-from-other-generators). The old `--mcp-http` endpoint is gone; `--agent` still generates `bin/failures` and the stdio `bin/failures-mcp`.
|
|
369
|
+
|
|
206
370
|
---
|
|
207
371
|
|
|
208
372
|
### Mission Control Jobs -- Job Queue Dashboard
|
|
@@ -216,7 +380,7 @@ Credentials are resolved in order:
|
|
|
216
380
|
- **Initializer:** Sets base controller class, credentials-first auth with ENV variable fallback
|
|
217
381
|
- **Route:** Mounts `MissionControl::Jobs::Engine` at `<prefix>/jobs`
|
|
218
382
|
- **Admin navigation:** Shared navigation bar with Overview, Errors, Jobs and Security tabs
|
|
219
|
-
- **Admin overview:** `BackstageDashboardController` at the prefix root with cards for each installed dashboard and a `@metrics` slot for your own aggregate counts; answers 503 until backstage credentials are set
|
|
383
|
+
- **Admin overview:** `BackstageDashboardController` at the prefix root with cards for each installed dashboard and a `@metrics` slot for your own aggregate counts; behind the backstage sign-in, or with Basic Auth answers 503 until backstage credentials are set
|
|
220
384
|
- **Custom layout:** Tailwind-styled layout with admin navigation, toast flash messages, application/server selection, and tab navigation
|
|
221
385
|
- **Custom views:** Tailwind-styled views for jobs, queues, workers, and recurring tasks (included by default, use `--no-copy-views` to skip)
|
|
222
386
|
|
|
@@ -240,7 +404,7 @@ rails g maquina:mission_control_jobs --prefix /backstage \
|
|
|
240
404
|
|
|
241
405
|
#### Authentication
|
|
242
406
|
|
|
243
|
-
|
|
407
|
+
With Clave, the app's sign-in and the backstage list; see [Backstage access](#backstage-access----clave-sign-in-or-basic-auth). Without it, Basic Auth credentials are resolved in order:
|
|
244
408
|
|
|
245
409
|
1. `Rails.application.credentials.backstage.username` / `.password`
|
|
246
410
|
2. `ENV["MISSION_CONTROL_JOBS_USER"]` / `ENV["MISSION_CONTROL_JOBS_PASSWORD"]` (configurable)
|
|
@@ -271,8 +435,11 @@ The generator automatically runs `bundle install` and installs migrations.
|
|
|
271
435
|
```bash
|
|
272
436
|
rails g maquina:solid_queue # Default (sqlite3)
|
|
273
437
|
rails g maquina:solid_queue --database postgresql # PostgreSQL
|
|
438
|
+
rails g maquina:solid_queue --mcp-write # Also generate the retry_failed_job and discard_failed_job MCP tools
|
|
274
439
|
```
|
|
275
440
|
|
|
441
|
+
With `maquina:mcp` installed, it also writes admin-only MCP tools (`queue_status`, `list_failed_jobs`, `get_failed_job`) to `app/models/mcp/tools/solid_queue.rb`. `--mcp-write` adds `retry_failed_job` and `discard_failed_job`, which need the `write` scope.
|
|
442
|
+
|
|
276
443
|
---
|
|
277
444
|
|
|
278
445
|
### Rack Attack -- Request Protection
|
|
@@ -282,6 +449,7 @@ rails g maquina:solid_queue --database postgresql # PostgreSQL
|
|
|
282
449
|
#### What it generates
|
|
283
450
|
|
|
284
451
|
- **Initializer:** `config/initializers/rack_attack.rb` with the bans, throttles, safelist, 403 responder and an `[ATTACK]` log line per refusal. Its knobs are constants on `Rack::Attack` (`SCANNER_*`, `GENERAL_*`, `FLOOD_*`, `LOGIN_*`) and `Rack::Attack.banned?(ip)` answers whether an address is banned now.
|
|
452
|
+
- **Route guard:** `Rack::Attack.app_route?(path)` answers whether the app itself routes a path.
|
|
285
453
|
|
|
286
454
|
#### Usage
|
|
287
455
|
|
|
@@ -295,6 +463,9 @@ The generator automatically runs `bundle install`.
|
|
|
295
463
|
#### Default Protections
|
|
296
464
|
|
|
297
465
|
- **Scanner ban (Fail2Ban):** three scanner paths in 10 minutes bans the IP for 7 days. Scanner paths are PHP files (`*.php`), WordPress paths (`wp-admin`, `wp-login`, etc.), sensitive files anywhere in the path (`.env`, `.git`, `/etc/passwd`, etc.), backup archives (`.zip`, `.sql`, `.tar.gz`, `.bak`, etc.) and scanner targets (`phpmyadmin`, `cgi-bin`, etc.). `/rails/active_storage` is exempt.
|
|
466
|
+
- The list also names what a modern scanner reaches for first: cloud provider credentials (`service-account.json`, `firebase-adminsdk.json`), container runtime internals (`/var/run/secrets/...`, `/proc/self/environ`), infrastructure as code (`docker-compose.yml`, `terraform.tfstate`), bare secret files (`secrets.json`, `credentials.json`), dev-server artefacts (`__vite_rsc_findSourceMapURL`, `/@fs/...`), source maps, dotfile credentials and encoded path traversal (`..%2F`).
|
|
467
|
+
- The path is unhidden before it is matched, so an encoded traversal is caught like a plain one. A public key is not a private key: `/public_key.pem` is still served.
|
|
468
|
+
- **Route guard:** a scanner rule stands down when the app itself routes the path, so a broad pattern cannot break a real route. A format Rails knows counts as the route, so `/service-worker.js` is `/service-worker(.:format)`, but `/login.php` is not `/login`. A root dynamic segment or catch-all (`/:slug`, `/*path`) does not count, because it would match every path; the app's other routes stay protected.
|
|
298
469
|
- **Flood ban (Allow2Ban):** 600 requests in 5 minutes (twice the general throttle) bans the IP for 1 day.
|
|
299
470
|
- **Throttles:** 300 requests/5min per IP (general; `/assets` and `/rails/active_storage` exempt), 5 `POST /session`/20s per IP
|
|
300
471
|
- **Safelists:** Localhost (`127.0.0.1`, `::1`)
|
|
@@ -347,18 +518,23 @@ The generator automatically runs `bundle install`. The page needs maquina_compon
|
|
|
347
518
|
rails g maquina:security --prefix /admin # Default
|
|
348
519
|
rails g maquina:security --prefix /admin --login-path /sign_in # Different sign-in path
|
|
349
520
|
rails g maquina:security --prefix /admin --no-copy-views # Without views
|
|
521
|
+
rails g maquina:security --prefix /admin --mcp-write # Also generate the unban_address MCP tool
|
|
350
522
|
rails g maquina:security --prefix /backstage \
|
|
351
523
|
--user-env-var ADMIN_USER --password-env-var ADMIN_PASSWORD # Custom env vars
|
|
352
524
|
```
|
|
353
525
|
|
|
354
526
|
#### Authentication
|
|
355
527
|
|
|
356
|
-
HTTP Basic Auth, with credentials resolved in order:
|
|
528
|
+
With Clave, the app's sign-in and the backstage list; see [Backstage access](#backstage-access----clave-sign-in-or-basic-auth). Without it, HTTP Basic Auth, with credentials resolved in order:
|
|
357
529
|
|
|
358
530
|
1. `Rails.application.credentials.backstage.username` / `.password`
|
|
359
531
|
2. `ENV["SECURITY_USER"]` / `ENV["SECURITY_PASSWORD"]` (configurable)
|
|
360
532
|
|
|
361
|
-
|
|
533
|
+
With Basic Auth the page answers 503 until one of them is set, so it is never left open.
|
|
534
|
+
|
|
535
|
+
#### MCP
|
|
536
|
+
|
|
537
|
+
With `maquina:mcp` installed, it also writes admin-only MCP tools (`recent_blocks`, `security_summary`). `--mcp-write` adds `unban_address`, which lifts the scanner and flood bans on one address through `Rack::Attack.unban!` and needs the `write` scope.
|
|
362
538
|
|
|
363
539
|
#### Trying it locally
|
|
364
540
|
|
|
@@ -423,7 +599,7 @@ Watch the Security page while the policy is report-only, then set `content_secur
|
|
|
423
599
|
9. Adds turbo morphing, `yield :head`, and simplifies `<main>` tag in layout
|
|
424
600
|
10. Optionally installs authentication (`maquina:clave` or `maquina:registration`)
|
|
425
601
|
11. Invokes sub-generators: `maquina:mission_control_jobs`, `maquina:solid_errors`
|
|
426
|
-
12. Runs external installers: `solid_queue:install`, `solid_errors:install`, `solid_cache:install`, `solid_cable:install`, `maquina_components:install`, then `maquina:security` (which installs the `maquina:rack_attack` rules)
|
|
602
|
+
12. Runs external installers: `solid_queue:install`, `solid_errors:install`, `solid_cache:install`, `solid_cable:install`, `maquina_components:install`, then `maquina:security` (which installs the `maquina:rack_attack` rules), `maquina:passkeys` with `--passkeys`, and `maquina:oauth` plus `maquina:mcp` with `--mcp`
|
|
427
603
|
13. Restores custom layouts overwritten by gem installers
|
|
428
604
|
14. Configures multi-database `database.yml` (primary, queue, cache, cable, errors, security)
|
|
429
605
|
15. Creates a HomeController with root route
|
|
@@ -436,6 +612,9 @@ Watch the Security page while the policy is report-only, then set `content_secur
|
|
|
436
612
|
rails g maquina:app
|
|
437
613
|
rails g maquina:app --prefix /backstage --port 3100
|
|
438
614
|
rails g maquina:app --auth registration
|
|
615
|
+
rails g maquina:app --auth clave --passkeys
|
|
616
|
+
rails g maquina:app --auth clave --mcp
|
|
617
|
+
rails g maquina:app --auth clave --mcp --mcp-flag beta_mcp
|
|
439
618
|
```
|
|
440
619
|
|
|
441
620
|
#### Options
|
|
@@ -443,11 +622,14 @@ rails g maquina:app --auth registration
|
|
|
443
622
|
- `--prefix` (default: `/admin`) -- Base path prefix for backstage tools (Solid Errors, Mission Control Jobs, Security)
|
|
444
623
|
- `--port` (default: `3000`) -- Default port for the development server
|
|
445
624
|
- `--auth` (default: `none`) -- Authentication type: `none`, `clave`, or `registration`
|
|
625
|
+
- `--passkeys` (default: `false`) -- Adds passkey sign-in with `maquina:passkeys`. It needs `--auth clave` and refuses to run without it. The passkey origin follows `--port`, and the home page links to `/passkeys`.
|
|
626
|
+
- `--mcp` (default: `false`) -- Adds the OAuth-backed MCP server with `maquina:oauth` and `maquina:mcp`, after Security and Passkeys so the Rack::Attack rules and the admin tools from the other generators land. It needs `--auth clave` and refuses to run without it. The README and post-install message point to `/settings/connected_apps` and the MCP Inspector.
|
|
627
|
+
- `--mcp-flag NAME` -- Passed to both as `--flag NAME`, to gate MCP access behind a feature flag.
|
|
446
628
|
|
|
447
629
|
#### After running
|
|
448
630
|
|
|
449
631
|
```bash
|
|
450
|
-
bin/rails credentials:edit #
|
|
632
|
+
bin/rails credentials:edit # --auth clave: backstage.emails; otherwise backstage username/password
|
|
451
633
|
bin/dev
|
|
452
634
|
```
|
|
453
635
|
|
data/Rakefile
CHANGED
|
@@ -11,6 +11,20 @@ module Maquina
|
|
|
11
11
|
desc: "Default port for development server"
|
|
12
12
|
class_option :auth, type: :string, default: "none",
|
|
13
13
|
desc: "Authentication type: none, clave, or registration"
|
|
14
|
+
class_option :passkeys, type: :boolean, default: false,
|
|
15
|
+
desc: "Add passkey sign-in on top of clave (requires --auth clave)"
|
|
16
|
+
class_option :mcp, type: :boolean, default: false,
|
|
17
|
+
desc: "Add an OAuth-backed MCP server on top of clave (requires --auth clave)"
|
|
18
|
+
class_option :mcp_flag, type: :string,
|
|
19
|
+
desc: "Feature flag that gates MCP access (e.g. beta_mcp)"
|
|
20
|
+
|
|
21
|
+
# 0. Check options before touching anything
|
|
22
|
+
def check_options
|
|
23
|
+
return if options[:auth] == "clave"
|
|
24
|
+
|
|
25
|
+
raise Thor::Error, "--passkeys builds on clave. Run with `--auth clave --passkeys`." if options[:passkeys]
|
|
26
|
+
raise Thor::Error, "--mcp builds on clave. Run with `--auth clave --mcp`." if options[:mcp]
|
|
27
|
+
end
|
|
14
28
|
|
|
15
29
|
# 1. Add gems
|
|
16
30
|
def add_gems
|
|
@@ -215,6 +229,34 @@ module Maquina
|
|
|
215
229
|
generate "maquina:security", "--prefix", options[:prefix], "--quiet"
|
|
216
230
|
end
|
|
217
231
|
|
|
232
|
+
# 12b. Install passkeys (after security, so its Rack::Attack throttle lands)
|
|
233
|
+
def install_passkeys
|
|
234
|
+
return unless options[:passkeys] && rails_app?
|
|
235
|
+
|
|
236
|
+
generate "maquina:passkeys", "--quiet"
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# 12c. Point the passkey origin at the development port
|
|
240
|
+
def configure_passkeys_origin
|
|
241
|
+
return unless options[:passkeys] && options[:port] != "3000"
|
|
242
|
+
|
|
243
|
+
initializer = File.join(destination_root, "config/initializers/webauthn.rb")
|
|
244
|
+
return unless File.exist?(initializer)
|
|
245
|
+
|
|
246
|
+
gsub_file "config/initializers/webauthn.rb",
|
|
247
|
+
'"http://localhost:3000"', %("http://localhost:#{options[:port]}")
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# 12d. Install the MCP server (after security and passkeys, so the
|
|
251
|
+
# Rack::Attack rules and every sibling tool marker already exist)
|
|
252
|
+
def install_mcp
|
|
253
|
+
return unless options[:mcp] && rails_app?
|
|
254
|
+
|
|
255
|
+
flag = options[:mcp_flag].to_s.empty? ? [] : ["--flag", options[:mcp_flag]]
|
|
256
|
+
generate "maquina:oauth", "--quiet", *flag
|
|
257
|
+
generate "maquina:mcp", "--quiet", *flag
|
|
258
|
+
end
|
|
259
|
+
|
|
218
260
|
# 13. Restore custom layouts overwritten by gem installers
|
|
219
261
|
def restore_custom_layouts
|
|
220
262
|
return unless rails_app?
|
|
@@ -321,8 +363,13 @@ module Maquina
|
|
|
321
363
|
say "Next steps:", :yellow
|
|
322
364
|
say " 1. Set credentials: bin/rails credentials:edit"
|
|
323
365
|
say " backstage:"
|
|
324
|
-
|
|
325
|
-
|
|
366
|
+
if options[:auth] == "clave"
|
|
367
|
+
say " emails:"
|
|
368
|
+
say " - you@example.com # who may open #{options[:prefix]} (signed in with Clave)"
|
|
369
|
+
else
|
|
370
|
+
say " username: your_user"
|
|
371
|
+
say " password: your_password"
|
|
372
|
+
end
|
|
326
373
|
say " 2. Start the app: bin/dev"
|
|
327
374
|
say ""
|
|
328
375
|
say "Installed components:", :yellow
|
|
@@ -339,6 +386,16 @@ module Maquina
|
|
|
339
386
|
say " - Visit /session/new to sign in"
|
|
340
387
|
say " - Visit /registrations/new to sign up" if options[:auth] == "registration"
|
|
341
388
|
say " - Visit /registration/new to sign up" if options[:auth] == "clave"
|
|
389
|
+
if options[:passkeys]
|
|
390
|
+
say " - Visit /passkeys to add a passkey"
|
|
391
|
+
say " - Production: set WEBAUTHN_RP_ID and WEBAUTHN_ORIGINS before the first passkey"
|
|
392
|
+
end
|
|
393
|
+
if options[:mcp]
|
|
394
|
+
say " - Visit /settings/connected_apps to see and revoke connected AI apps"
|
|
395
|
+
say " - Connect an MCP client: npx @modelcontextprotocol/inspector"
|
|
396
|
+
say " and point it at http://localhost:#{options[:port]}/mcp"
|
|
397
|
+
say " - MCP access is gated by the #{options[:mcp_flag]} flag (app/models/mcp.rb)" unless options[:mcp_flag].to_s.empty?
|
|
398
|
+
end
|
|
342
399
|
say " - Customize: app/controllers/concerns/authentication.rb"
|
|
343
400
|
say " - Locales: config/locales/"
|
|
344
401
|
end
|
|
@@ -26,6 +26,12 @@ Your app will be running at **http://localhost:<%= options[:port] %>**.
|
|
|
26
26
|
<% if options[:auth] != "none" -%>
|
|
27
27
|
| **Authentication** | <%= options[:auth] == "clave" ? "Clave passwordless email-code authentication" : "Registration-based email/password authentication" %> |
|
|
28
28
|
<% end -%>
|
|
29
|
+
<% if options[:passkeys] -%>
|
|
30
|
+
| **Passkeys** | Passkey sign-in with Face ID, a fingerprint or a device PIN; the email code stays as the way in without one |
|
|
31
|
+
<% end -%>
|
|
32
|
+
<% if options[:mcp] -%>
|
|
33
|
+
| **MCP Server** | OAuth 2.1-backed MCP server at `/mcp` for AI clients, with a Connected apps page to revoke them<%= " (gated by the `#{options[:mcp_flag]}` flag)" unless options[:mcp_flag].to_s.empty? %> |
|
|
34
|
+
<% end -%>
|
|
29
35
|
| **Background Jobs** | Solid Queue with Mission Control Jobs dashboard |
|
|
30
36
|
| **Error Tracking** | Solid Errors with custom UI |
|
|
31
37
|
| **Rate Limiting** | Rack::Attack with sensible defaults |
|
|
@@ -52,6 +58,13 @@ Your app will be running at **http://localhost:<%= options[:port] %>**.
|
|
|
52
58
|
<% elsif options[:auth] == "clave" -%>
|
|
53
59
|
| http://localhost:<%= options[:port] %>/registration/new | Sign up |
|
|
54
60
|
<% end -%>
|
|
61
|
+
<% if options[:passkeys] -%>
|
|
62
|
+
| http://localhost:<%= options[:port] %>/passkeys | Passkeys |
|
|
63
|
+
<% end -%>
|
|
64
|
+
<% if options[:mcp] -%>
|
|
65
|
+
| http://localhost:<%= options[:port] %>/settings/connected_apps | Connected apps |
|
|
66
|
+
| http://localhost:<%= options[:port] %>/mcp | MCP endpoint (`npx @modelcontextprotocol/inspector`) |
|
|
67
|
+
<% end -%>
|
|
55
68
|
<% end -%>
|
|
56
69
|
|
|
57
70
|
## Scripts
|
|
@@ -67,6 +80,19 @@ Edit credentials with:
|
|
|
67
80
|
bin/rails credentials:edit
|
|
68
81
|
```
|
|
69
82
|
|
|
83
|
+
<% if options[:auth] == "clave" -%>
|
|
84
|
+
The admin dashboards (overview, errors, jobs, security) are behind the app's sign-in. List who may open them; anyone else gets a 404:
|
|
85
|
+
|
|
86
|
+
```yaml
|
|
87
|
+
backstage:
|
|
88
|
+
emails:
|
|
89
|
+
- you@example.com
|
|
90
|
+
```
|
|
91
|
+
<% if options[:mcp] -%>
|
|
92
|
+
|
|
93
|
+
The same list decides who sees the admin-only MCP tools (errors, jobs, security).
|
|
94
|
+
<% end -%>
|
|
95
|
+
<% else -%>
|
|
70
96
|
Add backstage credentials for the admin dashboards (overview, errors, jobs, security):
|
|
71
97
|
|
|
72
98
|
```yaml
|
|
@@ -74,6 +100,7 @@ backstage:
|
|
|
74
100
|
username: your_user
|
|
75
101
|
password: your_password
|
|
76
102
|
```
|
|
103
|
+
<% end -%>
|
|
77
104
|
|
|
78
105
|
## Maquina Ecosystem
|
|
79
106
|
|
|
@@ -15,6 +15,9 @@
|
|
|
15
15
|
<p class="text-sm text-muted-foreground">
|
|
16
16
|
Signed in as <span class="font-medium text-foreground"><%%= Current.user.email_address %></span>
|
|
17
17
|
</p>
|
|
18
|
+
<% if options[:passkeys] -%>
|
|
19
|
+
<%%= link_to "Passkeys", passkeys_path, data: { component: "button", variant: "outline", size: "sm" } %>
|
|
20
|
+
<% end -%>
|
|
18
21
|
<%%= button_to t(".sign_out", default: "Sign Out"), session_path, method: :delete, data: { component: "button", variant: "outline", size: "sm" } %>
|
|
19
22
|
</div>
|
|
20
23
|
<%% else %>
|
|
@@ -62,7 +65,7 @@
|
|
|
62
65
|
<%%= render "components/card" do %>
|
|
63
66
|
<%%= render "components/card/header" do %>
|
|
64
67
|
<div class="flex items-center gap-2">
|
|
65
|
-
|
|
68
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-5 text-primary" aria-hidden="true"><path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"/><path d="m9 12 2 2 4-4"/></svg>
|
|
66
69
|
<%%= render "components/card/title", text: "Security" %>
|
|
67
70
|
</div>
|
|
68
71
|
<%%= render "components/card/description", text: "See what Rack::Attack throttled, blocked, and banned this week." %>
|