rails-hyperdrive 0.3.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 +7 -0
- data/CHANGELOG.md +295 -0
- data/LICENSE.txt +21 -0
- data/README.md +229 -0
- data/Rakefile +6 -0
- data/SECURITY.md +43 -0
- data/config/routes.rb +5 -0
- data/lib/generators/hyperdrive/content_sync_support.rb +54 -0
- data/lib/generators/hyperdrive/discover/discover_generator.rb +103 -0
- data/lib/generators/hyperdrive/gitignore_support.rb +28 -0
- data/lib/generators/hyperdrive/install/USAGE +22 -0
- data/lib/generators/hyperdrive/install/install_generator.rb +168 -0
- data/lib/generators/hyperdrive/install/templates/initializer.rb.tt +3 -0
- data/lib/generators/hyperdrive/install_summary.rb +70 -0
- data/lib/generators/hyperdrive/sync/USAGE +21 -0
- data/lib/generators/hyperdrive/sync/sync_generator.rb +40 -0
- data/lib/generators/hyperdrive/sync_runner.rb +70 -0
- data/lib/rails/hyperdrive/artifact_status.rb +86 -0
- data/lib/rails/hyperdrive/audit_header.rb +83 -0
- data/lib/rails/hyperdrive/auto_install.rb +98 -0
- data/lib/rails/hyperdrive/bundler_artifact_discovery.rb +320 -0
- data/lib/rails/hyperdrive/claude_md_import.rb +69 -0
- data/lib/rails/hyperdrive/companion_discovery.rb +232 -0
- data/lib/rails/hyperdrive/console_executor.rb +64 -0
- data/lib/rails/hyperdrive/data/gem_categories.yml +52 -0
- data/lib/rails/hyperdrive/drift_verdict.rb +42 -0
- data/lib/rails/hyperdrive/eager_footprint.rb +48 -0
- data/lib/rails/hyperdrive/engine.rb +22 -0
- data/lib/rails/hyperdrive/index_document.rb +49 -0
- data/lib/rails/hyperdrive/install_layout.rb +45 -0
- data/lib/rails/hyperdrive/install_pipeline.rb +415 -0
- data/lib/rails/hyperdrive/install_plan.rb +78 -0
- data/lib/rails/hyperdrive/install_shell.rb +43 -0
- data/lib/rails/hyperdrive/lock_file.rb +171 -0
- data/lib/rails/hyperdrive/mcp_server.rb +80 -0
- data/lib/rails/hyperdrive/resources/skill.rb +63 -0
- data/lib/rails/hyperdrive/resources/stack_profile.rb +32 -0
- data/lib/rails/hyperdrive/safety/rack_middleware.rb +54 -0
- data/lib/rails/hyperdrive/skill_template.rb +52 -0
- data/lib/rails/hyperdrive/sql_safety.rb +28 -0
- data/lib/rails/hyperdrive/stack_profile.rb +176 -0
- data/lib/rails/hyperdrive/tools/base.rb +39 -0
- data/lib/rails/hyperdrive/tools/describe_app.rb +21 -0
- data/lib/rails/hyperdrive/tools/list_models.rb +76 -0
- data/lib/rails/hyperdrive/tools/list_routes.rb +33 -0
- data/lib/rails/hyperdrive/tools/locate_source.rb +86 -0
- data/lib/rails/hyperdrive/tools/lookup_doc.rb +60 -0
- data/lib/rails/hyperdrive/tools/run_ruby.rb +31 -0
- data/lib/rails/hyperdrive/tools/run_sql.rb +49 -0
- data/lib/rails/hyperdrive/tools/tail_logs.rb +65 -0
- data/lib/rails/hyperdrive/version.rb +5 -0
- data/lib/rails/hyperdrive.rb +37 -0
- data/lib/rails-hyperdrive.rb +2 -0
- data/lib/tasks/hyperdrive.rake +22 -0
- metadata +161 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 7577ebf92af02fb8bb079a8b32c8751744340390f0f0facc0e682bc31aa6d43d
|
|
4
|
+
data.tar.gz: 990f940533691baf570a17113806fcd274300b56ee171a22195921ea9ddeb6bd
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: af48e37db907f10bf2a74d8303aa57b5cd04bd44d7ab97834bc197d97f39cf77cef87116217ce8d773d95d5ae6d93b6e216cbec275b3886bdb7c9b32cbe356af
|
|
7
|
+
data.tar.gz: 99144761cd64cf3bbed57caf0eaa0ab691511d7246e7add30e8fdf94bcd2f1e857c0b1612b5b95033a9336ca71faf80bd9bdae3db9813c3377dea3efdf6986c7
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.3.0] - 2026-08-04
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `bundler-rails-hyperdrive`, a Bundler plugin gem co-located in this repository
|
|
15
|
+
(`bundler-rails-hyperdrive/`). Once registered, it runs after every
|
|
16
|
+
`bundle install` in development and installs the artifacts that newly
|
|
17
|
+
bundled companion gems ship — additive only (it never overwrites or
|
|
18
|
+
deletes a file) — and reports upgraded or orphaned artifacts with a
|
|
19
|
+
pointer to `bin/rails hyperdrive:sync`. It resolves rails-hyperdrive from
|
|
20
|
+
the application's bundle at runtime (supported range `>= 0.2`), stays
|
|
21
|
+
silent outside development, and never fails a `bundle install`.
|
|
22
|
+
- `hyperdrive:init` now registers the plugin by appending
|
|
23
|
+
`plugin "bundler-rails-hyperdrive"` to the application's Gemfile. Idempotent: an
|
|
24
|
+
existing directive (with any options) is left alone, and an app without a
|
|
25
|
+
Gemfile gets a skip status.
|
|
26
|
+
- **Gem-conditional skill content.** A multi-file skill can now condition parts
|
|
27
|
+
of itself on the app's bundle, through two complementary mechanisms evaluated
|
|
28
|
+
at discovery time:
|
|
29
|
+
- A `conditional:` map in `SKILL.md` frontmatter gates individual supporting
|
|
30
|
+
files per gem. Keys are dir-relative shipped paths; values reuse the
|
|
31
|
+
artifact-level `gem:`/`versions:` forms (single target, comma-separated
|
|
32
|
+
string, YAML list, per-target `versions:` map, `"*"`), with `versions:`
|
|
33
|
+
optional (omitted = unconstrained). A gated file installs only when a
|
|
34
|
+
listed target is bundled at a satisfying version, and disappears on the
|
|
35
|
+
next `hyperdrive:sync` when its gate closes (unedited copies only). A
|
|
36
|
+
malformed condition fails open — the file installs and the problem is
|
|
37
|
+
reported as a discovery warning.
|
|
38
|
+
- Files named `*.md.erb` in a skill directory — including `SKILL.md.erb` in
|
|
39
|
+
place of `SKILL.md` — are rendered at install time with a sealed helper
|
|
40
|
+
binding (`gem?`, `any_gem?`, `gem_version`) over the resolved bundle, and
|
|
41
|
+
install as plain `.md`. A template that fails to render is skipped with a
|
|
42
|
+
warning (the whole skill, for `SKILL.md.erb`); a plain file always beats a
|
|
43
|
+
template rendering to the same path.
|
|
44
|
+
|
|
45
|
+
- **Multi-file skills.** A companion skill directory can now ship more than
|
|
46
|
+
`SKILL.md`: every other file in it (nested however deep — `workflows/`,
|
|
47
|
+
`references/`, `examples/`, …) installs as a **supporting file** under
|
|
48
|
+
`.claude/skills/<name>/`, preserving the relative layout, so `SKILL.md` can
|
|
49
|
+
reference it with directory-relative links (which survive cross-source
|
|
50
|
+
collision postfixing, since that renames the whole directory). Supporting
|
|
51
|
+
files carry no frontmatter contract and no audit header — they land
|
|
52
|
+
byte-identical to what the gem ships, with provenance and a per-file sha
|
|
53
|
+
recorded in `.hyperdrive/lock.yml` under the new `skill_support` kind. The
|
|
54
|
+
full drift state machine applies per file (unedited upgrades rewritten,
|
|
55
|
+
local edits skipped on sync / restored with `--overwrite`, deletions
|
|
56
|
+
reinstalled), a supporting file the gem stops shipping is cleaned up when
|
|
57
|
+
unedited (warned about and left when edited), disabling a skill removes its
|
|
58
|
+
unedited supporting files too, and the install summary shows them as a
|
|
59
|
+
`(+N files)` count on the skill's line.
|
|
60
|
+
|
|
61
|
+
- `hyperdrive:sync` — content-only actualization. Refreshes skills, guidelines,
|
|
62
|
+
`index.md`, and `.hyperdrive/lock.yml` to match the current bundle, and
|
|
63
|
+
touches no bootstrap artifact (`.mcp.json`, the engine mount, the
|
|
64
|
+
optional initializer, the `.gitignore` rule). Locally-edited files are
|
|
65
|
+
preserved by default (skip + warn); pass `--overwrite` to restore the
|
|
66
|
+
gem-shipped content. Works with or without a prior `hyperdrive:init`.
|
|
67
|
+
- An artifact's `gem:` frontmatter field can now declare **several** targets, as
|
|
68
|
+
either a comma-separated string (`gem: "sidekiq, solid_queue"`) or a YAML list.
|
|
69
|
+
The artifact installs when **any** listed target is in the bundle at a
|
|
70
|
+
satisfying version, so one guideline or skill can cover interchangeable
|
|
71
|
+
libraries instead of shipping as near-identical per-target copies. `"*"`
|
|
72
|
+
anywhere in the list makes the artifact universal.
|
|
73
|
+
- `versions:` accepts a **map keyed by gem name** alongside the existing single
|
|
74
|
+
requirement, for target sets that do not share a version cycle:
|
|
75
|
+
|
|
76
|
+
```yaml
|
|
77
|
+
gem: [sidekiq, solid_queue]
|
|
78
|
+
versions:
|
|
79
|
+
sidekiq: ">= 7.0"
|
|
80
|
+
solid_queue: ">= 1.0"
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
A single requirement still applies to every listed target. Targets absent from
|
|
84
|
+
the map are unconstrained.
|
|
85
|
+
- Content installation is now callable without a booted Rails application.
|
|
86
|
+
`Rails::Hyperdrive::InstallPipeline` takes an explicit application root and
|
|
87
|
+
performs the whole install — skills, guidelines, `index.md`, the `CLAUDE.md`
|
|
88
|
+
import line, and `.hyperdrive/lock.yml` — so any process that can
|
|
89
|
+
see the app's bundle can run it. `hyperdrive:init` and `hyperdrive:update`
|
|
90
|
+
call the same pipeline, and their behaviour is unchanged.
|
|
91
|
+
- `Rails::Hyperdrive::ArtifactStatus` compares what the bundle offers against
|
|
92
|
+
what `.hyperdrive/lock.yml` records, classifying every artifact as
|
|
93
|
+
`installed`, `missing`, `outdated`, or `orphaned`.
|
|
94
|
+
- `Rails::Hyperdrive::AutoInstall.run` tops up an already-initialized
|
|
95
|
+
application with artifacts the lockfile does not record yet, and returns
|
|
96
|
+
everything it deliberately left alone. Installation is strictly additive: it
|
|
97
|
+
can create a file that does not exist, and it will never overwrite one, so a
|
|
98
|
+
locally-edited artifact is safe. Upgraded and orphaned artifacts are reported
|
|
99
|
+
for `hyperdrive:update` to handle. It writes only when the environment reads
|
|
100
|
+
as development from `ENV` directly (no Rails to ask), `CI` is unset, and the
|
|
101
|
+
bundle is not frozen, and it reports errors rather than raising them.
|
|
102
|
+
|
|
103
|
+
- `hyperdrive:init` / `hyperdrive:update` now check the *combined* eager
|
|
104
|
+
footprint against a budget, not just per-file size. When the guidelines
|
|
105
|
+
listed in `index.md` exceed ~10,000 tokens, the footprint line is followed
|
|
106
|
+
by a warning naming the two largest contributors, so it is
|
|
107
|
+
clear what to trim or which line to drop from `index.md` to opt a guideline
|
|
108
|
+
out. Individually reasonable guidelines could previously clear every per-file
|
|
109
|
+
check and still add up to real context-window pressure with nothing flagging
|
|
110
|
+
it. The install still proceeds — this is a warning, not a gate.
|
|
111
|
+
|
|
112
|
+
- `hyperdrive:init` / `hyperdrive:update` now list every installed artifact in
|
|
113
|
+
the final summary, grouped under the source gem and version that shipped it,
|
|
114
|
+
instead of reporting bare skill and guideline counts. The listing is built
|
|
115
|
+
from `.hyperdrive/lock.yml`, so it reflects what the app ends up with —
|
|
116
|
+
including files left unchanged, files skipped as locally modified, and
|
|
117
|
+
orphans whose source gem has left the bundle — rather than only what the run
|
|
118
|
+
wrote. `--skip-content` prints no listing, as before.
|
|
119
|
+
|
|
120
|
+
- `hyperdrive:init` / `hyperdrive:update` now warn when git ignores an install
|
|
121
|
+
destination. Installed artifacts are git-tracked on purpose: reviewing the
|
|
122
|
+
diff is how you see what a companion gem added to the agent's context. An app
|
|
123
|
+
that gitignores `.claude/` empties that diff while the artifacts still reach
|
|
124
|
+
the agent, and nothing previously said so. The check asks git whether each
|
|
125
|
+
destination is ignored, so patterns, negations, and per-repository excludes
|
|
126
|
+
are all honored; outside a git repository, or without git installed, it stays
|
|
127
|
+
silent.
|
|
128
|
+
|
|
129
|
+
- Per-artifact opt-out via a `disabled:` list in `.hyperdrive/lock.yml`, keyed by
|
|
130
|
+
artifact type (`skills:` / `guidelines:`). A listed artifact is never installed,
|
|
131
|
+
and one already on disk is removed on the next `hyperdrive:init` /
|
|
132
|
+
`hyperdrive:sync` — but only when the file still matches the content the lock
|
|
133
|
+
recorded, so a locally-modified artifact is reported and left in place instead.
|
|
134
|
+
Disabling a guideline also drops its line from `index.md`. For a name shipped by
|
|
135
|
+
more than one companion gem, the shipped name disables every variant and the
|
|
136
|
+
`--<source-gem>` postfixed name disables one. The list is hand-edited: the
|
|
137
|
+
generator reads it, carries it forward, and writes the empty scaffold on every
|
|
138
|
+
install so the key is discoverable. Previously the only control was
|
|
139
|
+
`--skip-content`, which suppresses all installed content at once.
|
|
140
|
+
|
|
141
|
+
### Changed
|
|
142
|
+
|
|
143
|
+
- **BREAKING:** the eager-content chain is now companion-driven.
|
|
144
|
+
`.claude/hyperdrive/index.md` and the `@.claude/hyperdrive/index.md` line in
|
|
145
|
+
`CLAUDE.md` are created only once a companion gem ships a guideline, and are
|
|
146
|
+
removed again when the last one leaves the bundle. A zero-companion
|
|
147
|
+
`hyperdrive:init` now writes `.mcp.json`, the `.gitignore` rule, the optional
|
|
148
|
+
initializer, the engine mount, and `.hyperdrive/lock.yml` — and nothing into
|
|
149
|
+
the agent's context window. On tear-down, `CLAUDE.md` is deleted only when it
|
|
150
|
+
is byte-identical to the file hyperdrive created; otherwise only hyperdrive's
|
|
151
|
+
own import line is removed and every other byte is left alone. An import line
|
|
152
|
+
you deleted by hand is still never re-added. `hyperdrive:sync` performs the
|
|
153
|
+
tear-down; the `bundler-rails-hyperdrive` plugin (additive) never removes anything.
|
|
154
|
+
Because a `bundle install` must not edit `CLAUDE.md`, the plugin now says so
|
|
155
|
+
when it installs the first guideline into an app that has no import line yet:
|
|
156
|
+
the guideline is on disk but out of context until you run
|
|
157
|
+
`bin/rails hyperdrive:sync`.
|
|
158
|
+
- **BREAKING:** `hyperdrive:init` no longer accepts `--update` /
|
|
159
|
+
`--force-install`; it always preserves locally-edited files (skip + warn).
|
|
160
|
+
The conflict warning and the AutoInstall nudge now point at
|
|
161
|
+
`hyperdrive:sync`.
|
|
162
|
+
- `hyperdrive:init`'s summary now reports the mounted MCP server and its tool
|
|
163
|
+
count.
|
|
164
|
+
- `bundle install` (through the `bundler-rails-hyperdrive` plugin) no longer parses
|
|
165
|
+
`Gemfile.lock`.
|
|
166
|
+
|
|
167
|
+
### Removed
|
|
168
|
+
|
|
169
|
+
- **BREAKING:** `.claude/hyperdrive/stack.md` is no longer generated. Ask the
|
|
170
|
+
running server what the app's stack is instead — the `describe_app` MCP tool
|
|
171
|
+
and the `hyperdrive://stack-profile` resource answer it live, from the
|
|
172
|
+
resolved bundle, and can never go stale the way a written-once file does.
|
|
173
|
+
|
|
174
|
+
**Manual migration:** an app installed before this release keeps
|
|
175
|
+
`.claude/hyperdrive/stack.md` on disk, and `hyperdrive:init` / `hyperdrive:sync`
|
|
176
|
+
will report it as an orphan on every run. Delete the file and its
|
|
177
|
+
`.hyperdrive/lock.yml` entry by hand.
|
|
178
|
+
- **BREAKING:** `hyperdrive:update` is removed (not deprecated) — use
|
|
179
|
+
`bin/rails hyperdrive:sync --overwrite`. Note the default flip: the routine
|
|
180
|
+
refresh (`hyperdrive:sync`) now preserves locally-edited files; overwriting
|
|
181
|
+
them is opt-in via `--overwrite`.
|
|
182
|
+
|
|
183
|
+
### Fixed
|
|
184
|
+
|
|
185
|
+
- The MCP endpoint no longer 403s local requests whose `Origin` port differs
|
|
186
|
+
from the server's (e.g. `Origin: http://localhost` against
|
|
187
|
+
`127.0.0.1:3000`). The `mcp` gem's `StreamableHTTPTransport` gained
|
|
188
|
+
default-on DNS-rebinding protection requiring a same-origin `host:port`
|
|
189
|
+
match; it is now disabled in favor of the engine's own Rack middleware,
|
|
190
|
+
which allows any `localhost` / `127.0.0.1` / `[::1]` origin regardless of
|
|
191
|
+
port. The `mcp` dependency floor moves to `~> 0.25` accordingly.
|
|
192
|
+
|
|
193
|
+
- `.hyperdrive/lock.yml` now round-trips top-level keys it does not recognize.
|
|
194
|
+
Reading it selected only the known keys and writing it rebuilt the document from
|
|
195
|
+
scratch, so anything hand-added to the file was erased on the next run. Entries
|
|
196
|
+
under `files:` remain fully generated.
|
|
197
|
+
|
|
198
|
+
- `BundlerArtifactDiscovery#version_matches?` now correctly parses the documented
|
|
199
|
+
comma-separated single-string form of `versions:` (e.g. `">= 7.0, < 9.0"`).
|
|
200
|
+
Previously, `Gem::Requirement.new` rejected that form with `BadRequirementError`
|
|
201
|
+
(subclass of `ArgumentError`), which was caught and silently treated as a
|
|
202
|
+
version mismatch — installation would skip the artifact with a misleading
|
|
203
|
+
"does not satisfy" warning. The YAML list form (`versions: [">= 7.0", "< 9.0"]`)
|
|
204
|
+
was unaffected and continues to work.
|
|
205
|
+
|
|
206
|
+
- `hyperdrive:init` / `hyperdrive:update` now merge the `rails-hyperdrive` entry
|
|
207
|
+
into an existing `.mcp.json` instead of rendering a template over the whole
|
|
208
|
+
file. Any other MCP servers you have configured, and any sibling top-level
|
|
209
|
+
keys, are preserved; only `mcpServers["rails-hyperdrive"]` is managed. The
|
|
210
|
+
write is also silent — previously a project with a pre-existing `.mcp.json`
|
|
211
|
+
hit Thor's interactive `Overwrite? [Ynaqdhm]` prompt, which stalls CI and
|
|
212
|
+
agent runs and then discarded the other servers anyway. A re-run that changes
|
|
213
|
+
nothing writes nothing, and a `.mcp.json` that cannot be parsed is left
|
|
214
|
+
byte-for-byte intact with a warning rather than overwritten.
|
|
215
|
+
|
|
216
|
+
### Changed
|
|
217
|
+
|
|
218
|
+
- **BREAKING:** the `gem:` value reported for each installed skill by the
|
|
219
|
+
`describe_app` MCP tool and the `hyperdrive://stack-profile` resource is now an
|
|
220
|
+
**array** of every target that matched, rather than a single string. A
|
|
221
|
+
single-target artifact reports a one-element array. Consumers reading that
|
|
222
|
+
field as a string need updating.
|
|
223
|
+
|
|
224
|
+
- Documented the companion gem contract rules the installer enforces but the
|
|
225
|
+
README omitted or misstated: the `hyperdrive_skills_dir` gemspec metadata
|
|
226
|
+
override (an additional skill root, not a replacement; `..` segments ignored),
|
|
227
|
+
the permissive failure model and where its warnings surface, within-gem
|
|
228
|
+
collapsing of same-`name:` artifacts, and both accepted forms of `versions:`.
|
|
229
|
+
Corrected the description of `name:` — it determines the installed path rather
|
|
230
|
+
than merely matching the file or directory stem.
|
|
231
|
+
|
|
232
|
+
- **BREAKING:** renamed the three companion-gem gemspec metadata keys
|
|
233
|
+
`hyperdrive_targets`, `hyperdrive_artifacts`, and `hyperdrive_skills_dir` to
|
|
234
|
+
`rails_hyperdrive_targets`, `rails_hyperdrive_artifacts`, and
|
|
235
|
+
`rails_hyperdrive_skills_dir`. The old spellings are no longer read, with no
|
|
236
|
+
deprecated alias. Gemspec metadata is a single flat namespace shared by every
|
|
237
|
+
published gem, and the unprefixed `hyperdrive_*` prefix belongs to the
|
|
238
|
+
pre-existing, unrelated `hyperdrive` gem on rubygems — the contract is now
|
|
239
|
+
namespaced by the gem that defines it. Companion gems must update their
|
|
240
|
+
gemspecs to the new keys.
|
|
241
|
+
|
|
242
|
+
- **BREAKING:** `hyperdrive:discover` now finds companions by the metadata they
|
|
243
|
+
declare rather than by their name. It queries rubygems for gems declaring
|
|
244
|
+
`rails_hyperdrive_targets` instead of searching for the `rails-hyperdrive-`
|
|
245
|
+
name prefix, so a companion published under its author's own namespace is
|
|
246
|
+
suggested on the same terms as a purpose-built one. The `rails-hyperdrive-`
|
|
247
|
+
prefix remains a recommended naming convention and no longer plays any part in
|
|
248
|
+
discovery. Target matching, version gating, caching, and the never-raise
|
|
249
|
+
offline behaviour are unchanged, and artifact installation was never
|
|
250
|
+
name-filtered, so nothing about `hyperdrive:init` / `hyperdrive:update`
|
|
251
|
+
changes.
|
|
252
|
+
|
|
253
|
+
- **BREAKING:** renamed the `hyperdrive:init` / `hyperdrive:update` flag
|
|
254
|
+
`--skip-skills` to `--skip-content`. The old name is removed outright, with no
|
|
255
|
+
deprecated alias — it never described what the flag does. The flag skips *all*
|
|
256
|
+
installed content, not just skills: skills, guidelines, `index.md`,
|
|
257
|
+
the `CLAUDE.md` import line, and `.hyperdrive/lock.yml`, leaving only
|
|
258
|
+
`.mcp.json`, the discover-cache `.gitignore` rule, the optional initializer,
|
|
259
|
+
and the engine mount. Writing no lockfile is intentional and unchanged: the
|
|
260
|
+
lock is a manifest of installed content, and a later `hyperdrive:init` or
|
|
261
|
+
`hyperdrive:update` reconstructs the full state from scratch. Update any
|
|
262
|
+
scripts or CI invocations passing `--skip-skills`.
|
|
263
|
+
|
|
264
|
+
- Dropped the `< 8.1` upper cap on the `railties` and `activerecord` runtime
|
|
265
|
+
dependencies — both are now floor-only (`>= 7.2`), so the gem installs against
|
|
266
|
+
Rails 8.1 and later without waiting on a new release. The gem uses only stable
|
|
267
|
+
public Rails APIs and degrades gracefully per-tool, so the cap was conservative
|
|
268
|
+
rather than load-bearing. CI now exercises Rails 8.1 in place of 8.0.
|
|
269
|
+
|
|
270
|
+
## [0.2.0] - 2026-05-29
|
|
271
|
+
|
|
272
|
+
### Added
|
|
273
|
+
|
|
274
|
+
- `hyperdrive:discover` — read-only, networked command that suggests uninstalled
|
|
275
|
+
`rails-hyperdrive-*` companion gems for the app's stack. Queries the rubygems
|
|
276
|
+
search API, matches each companion's declared `hyperdrive_targets` against
|
|
277
|
+
`Gemfile.lock`, and prints the `bundle add` lines to run. Results cache to
|
|
278
|
+
`.hyperdrive/discover_cache.json` (24h TTL; `--refresh` busts it); offline or
|
|
279
|
+
rate-limited runs fall back to a stale cache or report "unavailable" without
|
|
280
|
+
failing. Ships dormant — returns nothing until companion gems exist on rubygems.
|
|
281
|
+
- `hyperdrive:init` now adds a `.gitignore` rule for the discover cache.
|
|
282
|
+
|
|
283
|
+
## [0.1.0] - 2026-05-29
|
|
284
|
+
|
|
285
|
+
### Added
|
|
286
|
+
|
|
287
|
+
- Initial release of `rails-hyperdrive`: a dev-only Rails engine that mounts an MCP
|
|
288
|
+
server at `/_hyperdrive/mcp` exposing introspection tools for AI coding agents.
|
|
289
|
+
- `hyperdrive:init` generator that installs architecture skills and auto-discovers
|
|
290
|
+
per-gem skills.
|
|
291
|
+
|
|
292
|
+
[Unreleased]: https://github.com/rails-hyperdrive/rails-hyperdrive/compare/v0.3.0...HEAD
|
|
293
|
+
[0.3.0]: https://github.com/rails-hyperdrive/rails-hyperdrive/releases/tag/v0.3.0
|
|
294
|
+
[0.2.0]: https://github.com/rails-hyperdrive/rails-hyperdrive/releases/tag/v0.2.0
|
|
295
|
+
[0.1.0]: https://github.com/rails-hyperdrive/rails-hyperdrive/releases/tag/v0.1.0
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Bakaface
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
# Rails Hyperdrive
|
|
2
|
+
|
|
3
|
+
> Dev-only Rails engine that bootstraps an MCP server + skills/guidelines for AI coding agents (Claude Code first).
|
|
4
|
+
|
|
5
|
+
Rails Hyperdrive mounts an [MCP (Model Context Protocol)](https://modelcontextprotocol.io) server at `http://localhost:3000/_hyperdrive/mcp` in development, exposing **8 introspection tools** so AI agents stop guessing — they can eval Ruby, query the DB (read-only), tail logs, list models and routes, locate source, fetch docs, and snapshot the stack.
|
|
6
|
+
|
|
7
|
+
It also ships a `hyperdrive:init` generator that discovers and installs **two artifact types** that companion gems ship under a documented contract:
|
|
8
|
+
|
|
9
|
+
- **Skills** — lazy, model-invoked via Claude Code's native description matcher. Procedural ("how to write an idempotent Sidekiq job"). Installed to `.claude/skills/<name>/SKILL.md`.
|
|
10
|
+
- **Guidelines** — eager, always in context via `@`-include from `CLAUDE.md`. Declarative ("this app uses Pundit, not CanCanCan"). Installed to `.claude/hyperdrive/guidelines/<name>.md`.
|
|
11
|
+
|
|
12
|
+
**rails-hyperdrive is the mechanism; companion gems are the content.** rails-hyperdrive itself ships no skills or guidelines — only the contract and the discovery/install engine. Content comes from companion gems, conventionally named `rails-hyperdrive-<library>` (e.g. `rails-hyperdrive-sidekiq`) following the [RuboCop ecosystem](https://github.com/rubocop/rubocop) precedent.
|
|
13
|
+
|
|
14
|
+
Built on the official [`mcp` gem](https://github.com/modelcontextprotocol/ruby-sdk). MIT-licensed.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Golden path
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
# 1. Add the dev gem
|
|
22
|
+
$ bundle add rails-hyperdrive --group=development
|
|
23
|
+
|
|
24
|
+
# 2. (Optional) Add a companion gem for your stack
|
|
25
|
+
$ bundle add rails-hyperdrive-sidekiq --group=development
|
|
26
|
+
|
|
27
|
+
# 3. Run the generator
|
|
28
|
+
$ bin/rails hyperdrive:init
|
|
29
|
+
|
|
30
|
+
create .mcp.json
|
|
31
|
+
insert config/routes.rb
|
|
32
|
+
create .claude/hyperdrive/guidelines/jobs-sidekiq.md
|
|
33
|
+
create .claude/skills/sidekiq-idempotency/SKILL.md
|
|
34
|
+
create .claude/hyperdrive/index.md
|
|
35
|
+
create CLAUDE.md
|
|
36
|
+
create .hyperdrive/lock.yml
|
|
37
|
+
eager 1 guideline(s), ~240 tokens always in context
|
|
38
|
+
|
|
39
|
+
done hyperdrive initialized
|
|
40
|
+
Mount: /_hyperdrive (in config/routes.rb)
|
|
41
|
+
Server: 8 MCP tools at http://localhost:3000/_hyperdrive/mcp
|
|
42
|
+
Installed 1 skill, 1 guideline
|
|
43
|
+
|
|
44
|
+
rails-hyperdrive-sidekiq@1.2.0
|
|
45
|
+
skill sidekiq-idempotency
|
|
46
|
+
guideline jobs-sidekiq
|
|
47
|
+
|
|
48
|
+
# 4. Start the dev server
|
|
49
|
+
$ bin/dev
|
|
50
|
+
|
|
51
|
+
# 5. Open Claude Code in the project directory
|
|
52
|
+
# → Claude Code reads .mcp.json, connects to http://localhost:3000/_hyperdrive/mcp
|
|
53
|
+
# → agent has 8 tools, the eager guidelines (via CLAUDE.md), and the lazy skills
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
`hyperdrive:init` also registers the `bundler-rails-hyperdrive` Bundler plugin in your Gemfile. From then on, `bundle add rails-hyperdrive-<library>` lands the companion's artifacts on that very `bundle install` — no explicit sync. The plugin is additive only (it never touches an existing file); version bumps and orphaned artifacts are only reported, with a pointer to `bin/rails hyperdrive:sync`.
|
|
57
|
+
|
|
58
|
+
Run `bin/rails hyperdrive:sync` any time (e.g. after `bundle update` or adding a companion gem) to refresh installed content to the current bundle. It touches no bootstrap artifact and leaves locally-modified files untouched (skip + warn); pass `--overwrite` to restore them to the gem-shipped content.
|
|
59
|
+
|
|
60
|
+
Run `hyperdrive:discover` to find companion gems published for your stack that you haven't installed yet — it queries rubygems (read-only, results cached for 24h; `--refresh` re-queries) and prints the `bundle add` lines to run, then run `bin/rails hyperdrive:sync`. It never touches your Gemfile or makes network calls unless you invoke it.
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## What ships
|
|
65
|
+
|
|
66
|
+
### MCP tools (8)
|
|
67
|
+
|
|
68
|
+
| # | Tool | Purpose |
|
|
69
|
+
|---|------|---------|
|
|
70
|
+
| 1 | `run_ruby` | Eval Ruby in the booted Rails process, with timeout + output capture |
|
|
71
|
+
| 2 | `run_sql` | Read-only SQL via the AR connection (refuses non-SELECT) |
|
|
72
|
+
| 3 | `tail_logs` | Tail the last N lines of a log under `log/` (defaults to `log/<env>.log`) |
|
|
73
|
+
| 4 | `list_models` | List Active Record model classes with columns/validations/associations |
|
|
74
|
+
| 5 | `locate_source` | Resolve `Const` / `Const#method` / `Const.method` / `dep:<gem>` to a file:line |
|
|
75
|
+
| 6 | `lookup_doc` | Look up RDoc for a symbol (via `ri`) |
|
|
76
|
+
| 7 | `describe_app` | Snapshot: Rails/Ruby/DB versions + full `StackProfile` |
|
|
77
|
+
| 8 | `list_routes` | All routes: HTTP verb, path, controller#action, named route |
|
|
78
|
+
|
|
79
|
+
### Resources
|
|
80
|
+
|
|
81
|
+
- `hyperdrive://stack-profile` — JSON of the resolved `StackProfile`
|
|
82
|
+
- `hyperdrive://skills/{name}` — markdown body of each installed skill
|
|
83
|
+
|
|
84
|
+
### Install layout
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
CLAUDE.md # user-owned; ONE injected line: @.claude/hyperdrive/index.md
|
|
88
|
+
.claude/hyperdrive/
|
|
89
|
+
index.md # managed aggregator: @guidelines/<name>.md
|
|
90
|
+
guidelines/<name>.md # companion-shipped, frontmatter stripped, audit-headered
|
|
91
|
+
.claude/skills/<name>/
|
|
92
|
+
SKILL.md # companion-shipped, frontmatter kept, audit-headered
|
|
93
|
+
<supporting files> # optional companion-shipped extras, installed as shipped (*.md.erb rendered)
|
|
94
|
+
.hyperdrive/lock.yml # git-tracked manifest (source gem, version, content hash)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
`CLAUDE.md` and `index.md` are the **eager chain** — they exist only because a companion gem ships a guideline, and both go when the last one leaves the bundle (the guideline file itself is left on disk and reported as an orphan). With no companion gems installed, `hyperdrive:init` writes `.mcp.json`, the `.gitignore` rule, the optional initializer, the engine mount, and `.hyperdrive/lock.yml` — and nothing into the agent's context window.
|
|
98
|
+
|
|
99
|
+
Everything a companion gem contributes lands here git-tracked, so a diff is where you review what it added — the install summary names each artifact's source gem and version, and every SKILL.md and guideline carries the same provenance in an audit header. A skill's supporting files carry no header — they install byte-identical to the install-ready body (the shipped bytes; for `*.md.erb` templates, the rendered output), and their provenance and content hash live in `.hyperdrive/lock.yml` alone. `hyperdrive:init` warns if your app gitignores these paths, since that empties the diff without changing what reaches the agent. The `hyperdrive:discover` cache is the one file rails-hyperdrive adds to `.gitignore`.
|
|
100
|
+
|
|
101
|
+
### Turning off a single artifact
|
|
102
|
+
|
|
103
|
+
A companion gem you want for one skill but not another doesn't have to be all-or-nothing. Add the artifact's name to the `disabled:` list in `.hyperdrive/lock.yml` — it is written empty on every install, so the shape is already there:
|
|
104
|
+
|
|
105
|
+
```yaml
|
|
106
|
+
disabled:
|
|
107
|
+
skills:
|
|
108
|
+
- sidekiq-idempotency
|
|
109
|
+
guidelines:
|
|
110
|
+
- jobs-sidekiq
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
A disabled artifact is never installed, and one already on disk is removed on the next `hyperdrive:init` or `hyperdrive:sync` — but only if you haven't edited it. A locally-modified file is reported and left alone, for you to delete when you're ready. Disabling a skill removes its shipped supporting files under the same per-file rule; files you created yourself in the skill directory survive and keep the directory alive. Disabling a guideline also drops its line from `index.md`, so it leaves eager context along with the file.
|
|
114
|
+
|
|
115
|
+
The list is yours to edit; the generator only reads it and carries it forward. Delete a name to get the artifact back on the next run. When two companion gems ship the same artifact name, both install under a `<name>--<source-gem>` suffix — the plain name disables both, the suffixed name disables one.
|
|
116
|
+
|
|
117
|
+
To skip installed content wholesale instead, pass `--skip-content`.
|
|
118
|
+
|
|
119
|
+
### Companion gem contract
|
|
120
|
+
|
|
121
|
+
A companion gem ships artifacts under:
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
<gem-source>/lib/<gem_name>/hyperdrive/skills/<name>/SKILL.md # skill (dir-per-skill)
|
|
125
|
+
<gem-source>/lib/<gem_name>/hyperdrive/guidelines/<name>.md # guideline (flat file)
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Skills may ship under an additional root declared in gemspec metadata:
|
|
129
|
+
|
|
130
|
+
```ruby
|
|
131
|
+
spec.metadata["rails_hyperdrive_skills_dir"] = "extra/skills" # optional; relative to the gem root
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
That root is searched **in addition to** the convention path, never instead of it, so an override never hides skills already shipped at the convention path. A value containing a `..` segment is ignored. Guidelines have no override — they are found only at the convention path.
|
|
135
|
+
|
|
136
|
+
A skill is a **directory**, and it may ship more than `SKILL.md`. Everything else in the skill directory — nested however you like (`workflows/`, `references/`, `examples/`, …) — installs alongside it as **supporting files**, preserving the relative layout under `.claude/skills/<name>/`. Reference them from `SKILL.md` with directory-relative links; a cross-source name collision renames the whole installed directory, so those links keep working. Supporting files carry no frontmatter contract and no audit header — they install byte-identical to the install-ready body, which is what the gem ships (markdown, code, or binary alike) except for `*.md.erb` templates, which install as their rendered output. Each is tracked per file in `.hyperdrive/lock.yml`, so local edits are preserved on sync exactly like any other installed file. `SKILL.md` frontmatter remains the skill's sole schema surface. Guidelines stay single-file.
|
|
137
|
+
|
|
138
|
+
Every artifact carries four required YAML frontmatter fields:
|
|
139
|
+
|
|
140
|
+
```yaml
|
|
141
|
+
---
|
|
142
|
+
name: jobs-sidekiq # kebab-case; determines the install path
|
|
143
|
+
description: Background job conventions for Sidekiq.
|
|
144
|
+
gem: sidekiq # TARGET gem(s), resolved + version-matched in the bundle
|
|
145
|
+
versions: ">= 7.0, < 9.0" # Gem::Requirement matched against the target gem
|
|
146
|
+
---
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
`name:` is the artifact's identity, not a label — it is what the installer writes to disk (`.claude/skills/<name>/SKILL.md`, `.claude/hyperdrive/guidelines/<name>.md`). Keep it equal to the file or directory stem: if the two disagree the install still succeeds, but the artifact lands under `name:`. Within one gem, two artifacts of the same type declaring the same `name:` collapse to a single installed file; which one survives is not a guarantee to build on, so give each a distinct `name:`.
|
|
150
|
+
|
|
151
|
+
`versions:` accepts a single comma-separated string (`">= 7.0, < 9.0"`), a YAML list (`[">= 7.0", "< 9.0"]`), or — for multi-target artifacts — a map keyed by gem name.
|
|
152
|
+
|
|
153
|
+
`gem:` names the **targets** (each must be present in the bundle; its resolved version is matched against `versions:`). Use `railties` for "every Rails app" or the quoted `"*"` for "always applicable" (it must be quoted — bare `*` is a YAML alias and the file is skipped). `hyperdrive:init` discovers every such file across the bundle, version-matches it, and installs it with an audit header naming `source`, `sha256`, and `installed_at`. Guidelines are installed with their frontmatter stripped (they are `@`-included eagerly). When two gems ship a same-named artifact, both install, each postfixed by source gem.
|
|
154
|
+
|
|
155
|
+
One artifact can cover several interchangeable libraries — write `gem:` as a comma-separated string or a YAML list, and it installs when **any** listed target is bundled at a satisfying version. `"*"` anywhere in the list makes the artifact universal. Give `versions:` a map keyed by gem name when the targets do not share a version cycle; targets the map omits are unconstrained.
|
|
156
|
+
|
|
157
|
+
```yaml
|
|
158
|
+
---
|
|
159
|
+
name: jobs-conventions
|
|
160
|
+
description: Background job conventions.
|
|
161
|
+
gem: [sidekiq, solid_queue, good_job]
|
|
162
|
+
versions:
|
|
163
|
+
sidekiq: ">= 7.0"
|
|
164
|
+
solid_queue: ">= 1.0"
|
|
165
|
+
---
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Draw the listed targets from the gem's own `hyperdrive_targets` (below): the gem-level declaration decides whether a companion is suggested at all, and an artifact naming a target the gemspec omits is unreachable for apps that have only that target.
|
|
169
|
+
|
|
170
|
+
#### Gem-conditional skill content
|
|
171
|
+
|
|
172
|
+
A multi-file skill can condition parts of itself on the app's bundle, so one skill tree serves apps with different gem sets. Both mechanisms are evaluated at discovery time, against the same resolved bundle that gates whole artifacts.
|
|
173
|
+
|
|
174
|
+
**Per-file gating.** A `conditional:` map in `SKILL.md` frontmatter gates individual supporting files. Keys are dir-relative shipped paths; values take the same `gem:`/`versions:` forms as the artifact-level fields (single target, comma-separated string, YAML list, per-target `versions:` map, `"*"`), and the file installs when **any** listed target is bundled at a satisfying version. Unlike the artifact level, `versions:` is optional here — omitted means unconstrained. Files the map doesn't mention install unconditionally, and the supporting files themselves stay byte-identical to upstream — the condition lives entirely out-of-band.
|
|
175
|
+
|
|
176
|
+
```yaml
|
|
177
|
+
---
|
|
178
|
+
name: layered-rails
|
|
179
|
+
description: Layered architecture conventions.
|
|
180
|
+
gem: railties
|
|
181
|
+
versions: ">= 7.2"
|
|
182
|
+
conditional:
|
|
183
|
+
references/gems/alba.md:
|
|
184
|
+
gem: alba
|
|
185
|
+
references/gems/jobs.md:
|
|
186
|
+
gem: [sidekiq, solid_queue]
|
|
187
|
+
versions:
|
|
188
|
+
sidekiq: ">= 7.0"
|
|
189
|
+
---
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
A malformed condition **fails open**: the file installs unconditionally and the problem is reported with the other discovery warnings — a surplus reference file is harmless, a missing one breaks links from `SKILL.md`. A key naming no shipped file, or naming `SKILL.md` itself (the artifact-level `gem:`/`versions:` gate the whole skill), is warned about and ignored. The `conditional:` key ships through to the installed frontmatter unchanged.
|
|
193
|
+
|
|
194
|
+
**ERB-templated markdown.** A file named `*.md.erb` in a skill directory — including `SKILL.md.erb` in place of `SKILL.md` — is rendered at install time and lands as plain `.md` (the `.erb` suffix is dropped; a `SKILL.md.erb` defines a skill exactly like `SKILL.md`, with frontmatter read from the rendered output). Templates see a sealed binding of exactly three helpers over the resolved bundle, nothing else:
|
|
195
|
+
|
|
196
|
+
- `gem?("name")` / `gem?("name", ">= 2.0")` — is the gem bundled (at a satisfying version)?
|
|
197
|
+
- `any_gem?("a", "b", …)` — is any of these bundled?
|
|
198
|
+
- `gem_version("name")` — the resolved version as a String, or `nil`.
|
|
199
|
+
|
|
200
|
+
Rendering uses ERB's trim mode, so `<%- if gem?("alba") -%>` … `<%- end -%>` control lines leave no blank lines behind. A template that fails to render is skipped with a warning (the whole skill, when it's `SKILL.md.erb`); when a plain file and a template would land at the same path, the plain file wins with a warning. `conditional:` keys refer to templates by their shipped `x.md.erb` name and gate them before rendering. Guidelines get no ERB support.
|
|
201
|
+
|
|
202
|
+
Use ERB sparingly — condition reference manuals via `conditional:` and wrap link-table rows that point at gated files, but keep "consider adopting gem X" recommendations unconditional. An all-wrapped `.md.erb` renders to an empty file; to omit a file entirely, gate it with `conditional:` instead.
|
|
203
|
+
|
|
204
|
+
Gated files appear and disappear as the bundle changes: `hyperdrive:init`/`hyperdrive:sync` install newly gated-in files and remove unedited gated-out ones. The auto top-up after `bundle install` adds newly gated-in files but never removes anything and never rewrites an already-installed file, so removals and re-rendered template output wait for the next `hyperdrive:sync`.
|
|
205
|
+
|
|
206
|
+
Discovery never raises. An artifact with missing or malformed frontmatter, a missing required field, no declared target in the bundle, or every bundled target resolving outside `versions:` is skipped, and the reason is collected. `hyperdrive:init` and `hyperdrive:sync` print the collected reasons at the end of the run, under a yellow `warn` line reading `discovery skipped N artifact(s):`. A companion whose artifacts all fail therefore installs nothing and reports it only there — read that section first when a gem you expected to contribute produces no files.
|
|
207
|
+
|
|
208
|
+
To be discoverable by `hyperdrive:discover` **before** it is installed, a companion also declares gemspec metadata (read remotely from rubygems, so the frontmatter inside the gem isn't visible yet):
|
|
209
|
+
|
|
210
|
+
```ruby
|
|
211
|
+
spec.metadata["rails_hyperdrive_targets"] = "sidekiq" # required; comma-sep, or "*" for always-applicable
|
|
212
|
+
spec.metadata["rails_hyperdrive_artifacts"] = "guideline,skill" # optional; presentational hint
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
`rails_hyperdrive_targets` is what makes a gem discoverable: `hyperdrive:discover` searches rubygems for gems declaring it, so a companion is found by what it declares rather than by what it is named. Naming it `rails-hyperdrive-<library>` is a recommended convention — it makes the gem legible in a Gemfile — but it plays no part in discovery, and a companion published under your own namespace is found on the same terms.
|
|
216
|
+
|
|
217
|
+
`rails_hyperdrive_targets` is a coarse pre-install hint — it is never reconciled against the frontmatter `gem:`; once the gem is bundled, the frontmatter alone governs what installs.
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
## Safety
|
|
222
|
+
|
|
223
|
+
Rails Hyperdrive is **dev-only**. The engine refuses to handle requests outside `Rails.env.development?` and enforces an origin allowlist (`localhost`, `127.0.0.1`, `[::1]`). See [SECURITY.md](SECURITY.md).
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
227
|
+
## License
|
|
228
|
+
|
|
229
|
+
MIT — see [LICENSE.txt](LICENSE.txt).
|
data/Rakefile
ADDED
data/SECURITY.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Security Model
|
|
2
|
+
|
|
3
|
+
`rails-hyperdrive` is a **development-only** tool. Its security model is designed for a single developer running a Rails app on their own machine — nothing more. Read this before exposing your dev server to a network you don't fully control.
|
|
4
|
+
|
|
5
|
+
## Threat model in one paragraph
|
|
6
|
+
|
|
7
|
+
Rails Hyperdrive mounts an MCP server that exposes Ruby eval, raw SQL (read-only guardrail, not a sandbox), log tailing, and source-code introspection. It is intended to be reached from `http://localhost:3000/_hyperdrive/mcp` by a local AI coding agent (Claude Code) running on the same workstation. There is no authentication. If the dev server is reachable by another process, user, or host, that party can read everything in your Rails app and run arbitrary Ruby in your dev environment.
|
|
8
|
+
|
|
9
|
+
## Three layers of defense
|
|
10
|
+
|
|
11
|
+
All three layers key off `Rails::Hyperdrive.dev_mode?` (which is `Rails.env.development?`).
|
|
12
|
+
|
|
13
|
+
1. **Engine load-time warning.** The engine loads in any environment so a production process doesn't crash if `rails-hyperdrive` slips into the wrong Bundler group, but it logs a warning at boot when not in development.
|
|
14
|
+
|
|
15
|
+
2. **Rack middleware** (`Rails::Hyperdrive::Safety::RackMiddleware`) sits in front of the MCP transport on every request:
|
|
16
|
+
- Returns `403` when `Rails.env` is not `development`.
|
|
17
|
+
- Returns `403` when the `Origin` header is set and its host is not in the allowlist (`localhost`, `127.0.0.1`, `[::1]`). Requests with no `Origin` (e.g. `curl`, `Rack::Test`) pass through.
|
|
18
|
+
|
|
19
|
+
3. **Per-tool `with_dev_guard`** in `Rails::Hyperdrive::Tools::Base` catches direct in-process invocations (tests, rake tasks) that would bypass the transport entirely.
|
|
20
|
+
|
|
21
|
+
## What the Origin allowlist does and doesn't do
|
|
22
|
+
|
|
23
|
+
The allowlist blocks DNS-rebinding attacks: an attacker can't trick a victim's browser into POSTing JSON-RPC to `http://localhost:3000/_hyperdrive/mcp` from an attacker-controlled origin, because the browser would attach `Origin: http://evil.example` and we'd 403 it.
|
|
24
|
+
|
|
25
|
+
It does **not** authenticate anything. Any local process that can speak HTTP to your dev server can call every tool. If you run untrusted code on the same workstation, do not run `rails-hyperdrive`.
|
|
26
|
+
|
|
27
|
+
## What `run_sql` actually is
|
|
28
|
+
|
|
29
|
+
A best-effort regex-based gate that rejects mutating SQL at the parser level. It is **not** a sandbox. A determined attacker with `run_ruby` access can trivially bypass it (`run_ruby` runs arbitrary Ruby in the Rails process). The SQL guard exists to keep a confused AI from running `DELETE FROM users` by accident, not to enforce a privilege boundary.
|
|
30
|
+
|
|
31
|
+
## Network exposure is your responsibility
|
|
32
|
+
|
|
33
|
+
Rails Hyperdrive does not bind to a network interface — that is Puma's job. The default `bin/rails server` binding behavior varies by Rails version, operating system, and your `config/puma.rb`. To be safe:
|
|
34
|
+
|
|
35
|
+
- Bind Puma to `127.0.0.1` (the default in recent Rails versions), not `0.0.0.0`.
|
|
36
|
+
- Do not run `bin/rails server -b 0.0.0.0` on a network you share with untrusted parties.
|
|
37
|
+
- Do not forward port 3000 in Docker / `kubectl port-forward` / `ngrok` / Tailscale / VPN unless you understand who can reach it.
|
|
38
|
+
|
|
39
|
+
If you must expose the dev server, put it behind your own authenticating proxy and remove `rails-hyperdrive` from the bundle first.
|
|
40
|
+
|
|
41
|
+
## Reporting a vulnerability
|
|
42
|
+
|
|
43
|
+
Open a GitHub issue describing the impact. Do not include credentials or proprietary code. For sensitive reports, email the gem author listed in `rails-hyperdrive.gemspec`.
|