rails-ai-bridge 4.0.0 → 4.1.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/.reek.yml +4 -0
- data/AGENTS.md +1 -1
- data/CHANGELOG.md +15 -1
- data/CLAUDE.md +1 -1
- data/GEMINI.md +1 -1
- data/README.md +6 -2
- data/docs/02-port-registry-resolution.md +152 -0
- data/docs/mcp-security.md +53 -0
- data/docs/registry-resolution.md +2 -1
- data/docs/skill-registry-guide.md +69 -1
- data/lib/rails_ai_bridge/config/registry.rb +5 -0
- data/lib/rails_ai_bridge/registry/context_provider_definition.rb +42 -0
- data/lib/rails_ai_bridge/registry/context_tool_spec.rb +53 -0
- data/lib/rails_ai_bridge/registry/pack_resolver.rb +101 -6
- data/lib/rails_ai_bridge/registry/rake_presenter.rb +32 -0
- data/lib/rails_ai_bridge/registry/registry_manifest.rb +137 -2
- data/lib/rails_ai_bridge/registry/skill_source_resolver.rb +69 -7
- data/lib/rails_ai_bridge/registry.rb +2 -0
- data/lib/rails_ai_bridge/server.rb +3 -1
- data/lib/rails_ai_bridge/tasks/rails_ai_bridge.rake +34 -3
- data/lib/rails_ai_bridge/tools/usage_formatter.rb +72 -0
- data/lib/rails_ai_bridge/tools/use_agent.rb +54 -0
- data/lib/rails_ai_bridge/tools/use_skill.rb +57 -0
- data/lib/rails_ai_bridge/version.rb +1 -1
- metadata +7 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2f7810f4efa4400b60d813b2344b82f38e0a731ccc89e68a3be51aabe2526a3d
|
|
4
|
+
data.tar.gz: 7faa7fe7f2f731d572806596a3f60d756d85b589ba46b6dfa7ef2e614bea8749
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1d4cf0bdcf2dcf203a30b112ac940709dcf3222af18efaeb10625d08594197f38d49cfb23f5edc328fce6a4fd6242b02aa545cb6075c5c142ab81b3a5b89d1d5
|
|
7
|
+
data.tar.gz: 2f7dad1ecef89cc739b7a8031f16e753b0b4a55bdabbc52f8692f6b42f05f0cf7a14acc80700d819f401649eb384fc7109d98569372462f651eee2ea608d7767
|
data/.reek.yml
CHANGED
|
@@ -87,6 +87,7 @@ detectors:
|
|
|
87
87
|
- RailsAiBridge::Serializers::RegenerationFooter
|
|
88
88
|
- RailsAiBridge::Introspectors::SemanticIntrospector
|
|
89
89
|
- RailsAiBridge::RubydexAdapter::IncrementalIndexer
|
|
90
|
+
- RailsAiBridge::Registry::RegistryManifest
|
|
90
91
|
DuplicateMethodCall:
|
|
91
92
|
exclude:
|
|
92
93
|
- RailsAiBridge::Config::Introspection#excluded_table?
|
|
@@ -416,6 +417,7 @@ detectors:
|
|
|
416
417
|
- RailsAiBridge::Tools::BaseTool
|
|
417
418
|
- RailsAiBridge::Watcher::BridgeRegenerator
|
|
418
419
|
- RailsAiBridge::Fingerprinter::CachedSnapshot
|
|
420
|
+
- RailsAiBridge::Registry::RegistryManifest
|
|
419
421
|
NestedIterators:
|
|
420
422
|
exclude:
|
|
421
423
|
- RailsAiBridge::Fingerprinter#snapshot
|
|
@@ -495,6 +497,7 @@ detectors:
|
|
|
495
497
|
- RailsAiBridge::Introspectors::TestIntrospector
|
|
496
498
|
- RailsAiBridge::Introspectors::ViewIntrospector
|
|
497
499
|
- RailsAiBridge::RubydexAdapter
|
|
500
|
+
- RailsAiBridge::Registry::RegistryManifest
|
|
498
501
|
- RailsAiBridge::Tools::GetStimulus::ResponseFormatter
|
|
499
502
|
- RailsAiBridge::Tools::SearchSemantic
|
|
500
503
|
TooManyInstanceVariables:
|
|
@@ -502,6 +505,7 @@ detectors:
|
|
|
502
505
|
- RailsAiBridge::Config::Introspection
|
|
503
506
|
- RailsAiBridge::Config::Mcp
|
|
504
507
|
- RailsAiBridge::Config::Output
|
|
508
|
+
- RailsAiBridge::Config::Registry
|
|
505
509
|
- RailsAiBridge::Config::Rubydex
|
|
506
510
|
- RailsAiBridge::Config::Server
|
|
507
511
|
- RailsAiBridge::Configuration
|
data/AGENTS.md
CHANGED
|
@@ -9,7 +9,7 @@ structure to AI assistants via the Model Context Protocol (MCP).
|
|
|
9
9
|
- `lib/rails_ai_bridge/configuration.rb` — User-facing config with presets (:standard, :full)
|
|
10
10
|
- `lib/rails_ai_bridge/introspector.rb` — Orchestrates sub-introspectors
|
|
11
11
|
- `lib/rails_ai_bridge/introspectors/` — Built-in introspector classes; `:standard` preset runs **9**, `:full` runs **26** (see `Configuration::PRESETS`). Registry: `Introspector::BUILTIN_INTROSPECTORS` (includes opt-in symbols such as `database_stats`, `non_ar_models` not listed in those presets).
|
|
12
|
-
- `lib/rails_ai_bridge/tools/` —
|
|
12
|
+
- `lib/rails_ai_bridge/tools/` — 16 built-in MCP tools using the official mcp SDK (hosts can add more via `additional_tools`)
|
|
13
13
|
- `lib/rails_ai_bridge/serializers/` — Output formatters (Codex, claude_rules, cursor_rules, devin, devin_rules, copilot, copilot_instructions, rules, markdown, JSON)
|
|
14
14
|
- `lib/rails_ai_bridge/resources.rb` — MCP resources (static data AI clients read directly)
|
|
15
15
|
- `lib/rails_ai_bridge/server.rb` — MCP server configuration (stdio + HTTP transports)
|
data/CHANGELOG.md
CHANGED
|
@@ -5,7 +5,21 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
-
## [
|
|
8
|
+
## [4.1.0] - 2026-08-11
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **JWT authentication docs for the HTTP MCP transport** (#129) — `docs/mcp-security.md` now documents the `mcp_jwt_decoder` pattern with a worked `JWT.decode` example, the decoder return-value contract, and a token rotation strategy (short expiry, signing-key overlap windows, revocation guidance).
|
|
13
|
+
- **`rails_use_skill` / `rails_use_agent` MCP tools** (#133) — resolve a skill or agent via `Registry::Resolver` and return it framed for immediate in-context application: an intent header, the deprecation notice when the name was redirected, the full content, and a follow-through footer. Use these to *act on* a skill; `rails_resolve_skill` remains the read-only inspection tool (with `pack=` pinning).
|
|
14
|
+
- **JSON output for the registry rake tasks** (#128) — `rails "ai:skills:list[json]"` (or `FORMAT=json rails ai:skills:list`) prints a stable `{"packs": [...], "skills": [...]}` catalog document for CI and custom tooling. `RakePresenter` gains `skills_json`, `packs_json`, and `catalog_json`; the default table output is unchanged.
|
|
15
|
+
- **Transitive `depends_on` loading for skill packs** (#126) — opt in with `config.registry.auto_load_dependencies = true` (default `false`): `PackResolver` expands declared pack dependencies transitively (fixed-point iteration capped at 10 levels) and warns about circular dependency chains while still loading every pack in the cycle. Dependencies missing from the manifest are left to the existing warning, which now points at the new flag.
|
|
16
|
+
- **Structured logging for registry git operations** (#131) — `SkillSourceResolver` accepts an optional `logger:` (defaults to `Rails.logger`, or a stderr logger outside Rails). Clone, pull, and checkout operations log `key=value` lines: DEBUG before the operation, INFO with `duration_ms` on success, ERROR with the failure message before raising.
|
|
17
|
+
- **Context provider definitions in the registry manifest** (#134) — new `Registry::ContextProviderDefinition` and `Registry::ContextToolSpec` value objects (ported from the Rust runtime) parse an optional `context_providers` section of the registry manifest: provider `type`/`endpoint`, optional flag, and tool lists supporting both simple names and `{ name, field, arguments }` mappings. `RegistryManifest#context_providers` defaults to `{}` — parsing is preparatory and nothing consumes these definitions yet.
|
|
18
|
+
- **Manifest schema validation** (#123) — new `RegistryManifest.validate!` raises a descriptive `RegistryManifest::ValidationError` for the first invalid field (missing/empty pack `source`; wrong types for `version`, `default_stack`, `ref`, `tile`, `depends_on`, `always_loaded`, `priority`; non-object `packs` entries). New `rails ai:registry:validate` rake task validates the configured manifest for CI/pre-commit use and exits non-zero on failure.
|
|
19
|
+
|
|
20
|
+
### Changed (breaking)
|
|
21
|
+
|
|
22
|
+
- **Full SHA-256 digests for skill-pack cache keys** (#122) — `SkillSourceResolver.compute_cache_key` now appends the full 64-character SHA-256 hex digest instead of a 16-character truncation, consistent with the gem's fingerprinting. Cache directories created under the old key format are orphaned; clear `~/.rails-ai-bridge/cache` to reclaim disk space.
|
|
9
23
|
|
|
10
24
|
## [4.0.0] - 2026-08-09
|
|
11
25
|
|
data/CLAUDE.md
CHANGED
|
@@ -12,7 +12,7 @@ structure to AI assistants via the Model Context Protocol (MCP).
|
|
|
12
12
|
runs **9**, `:full` runs **26** (see `Configuration::PRESETS`). Registry:
|
|
13
13
|
`Introspector::BUILTIN_INTROSPECTORS` (includes opt-in symbols such as
|
|
14
14
|
`database_stats`, `non_ar_models` not listed in those presets).
|
|
15
|
-
- `lib/rails_ai_bridge/tools/` —
|
|
15
|
+
- `lib/rails_ai_bridge/tools/` — 16 built-in MCP tools using the official mcp SDK
|
|
16
16
|
(hosts can add more via `additional_tools`)
|
|
17
17
|
- `lib/rails_ai_bridge/serializers/` — Output formatters (claude, claude_rules,
|
|
18
18
|
cursor_rules, devin, devin_rules, copilot, copilot_instructions, rules,
|
data/GEMINI.md
CHANGED
|
@@ -9,7 +9,7 @@ structure to AI assistants via the Model Context Protocol (MCP).
|
|
|
9
9
|
- `lib/rails_ai_bridge/configuration.rb` — User-facing config with presets (:standard, :full)
|
|
10
10
|
- `lib/rails_ai_bridge/introspector.rb` — Orchestrates sub-introspectors
|
|
11
11
|
- `lib/rails_ai_bridge/introspectors/` — Built-in introspector classes; `:standard` preset runs **9**, `:full` runs **26** (see `Configuration::PRESETS`). Registry: `Introspector::BUILTIN_INTROSPECTORS` (includes opt-in symbols such as `database_stats`, `non_ar_models` not listed in those presets).
|
|
12
|
-
- `lib/rails_ai_bridge/tools/` —
|
|
12
|
+
- `lib/rails_ai_bridge/tools/` — 16 built-in MCP tools using the official mcp SDK (hosts can add more via `additional_tools`)
|
|
13
13
|
- `lib/rails_ai_bridge/serializers/` — Output formatters (claude, claude_rules, cursor_rules, windsurf, windsurf_rules, copilot, copilot_instructions, rules, markdown, JSON, gemini)
|
|
14
14
|
- `lib/rails_ai_bridge/resources.rb` — MCP resources (static data AI clients read directly)
|
|
15
15
|
- `lib/rails_ai_bridge/server.rb` — MCP server configuration (stdio + HTTP transports)
|
data/README.md
CHANGED
|
@@ -251,7 +251,7 @@ This keeps context focused and avoids unnecessary token usage while still allowi
|
|
|
251
251
|
|
|
252
252
|
## MCP Tools
|
|
253
253
|
|
|
254
|
-
The gem exposes **
|
|
254
|
+
The gem exposes **16 built-in tools** via MCP that AI clients call on-demand (hosts can append more via `config.additional_tools`):
|
|
255
255
|
|
|
256
256
|
| Tool | What it returns |
|
|
257
257
|
|------|----------------|
|
|
@@ -268,6 +268,9 @@ The gem exposes **13 built-in tools** via MCP that AI clients call on-demand (ho
|
|
|
268
268
|
| `rails_search_semantic` | Semantic code search using rubydex — find declarations by name with types, locations, and relationships |
|
|
269
269
|
| `rails_get_stimulus` | Stimulus controllers: targets, values, actions, outlets (requires `:stimulus` introspector) |
|
|
270
270
|
| `rails_list_registry` | Skill pack catalog — list skills, agents, or active packs; requires `config/rails_ai_bridge_registry.json` |
|
|
271
|
+
| `rails_resolve_skill` | Full content of a named skill or agent from the registry (priority ordering + deprecation redirects); optional `pack=` pin and `type=agent` |
|
|
272
|
+
| `rails_use_skill` | Loads a skill framed as an application directive (apply it step by step to the current task) |
|
|
273
|
+
| `rails_use_agent` | Loads an agent/workflow framed as an activation directive (follow it end to end) |
|
|
271
274
|
|
|
272
275
|
All tools are **read-only** — they never modify your application or database.
|
|
273
276
|
|
|
@@ -547,6 +550,7 @@ end
|
|
|
547
550
|
| `registry.resolver_ttl` | `1800` | Seconds to cache the wired resolver in memory; `0` disables caching |
|
|
548
551
|
| `registry.git_pull_ttl` | `86400` | Seconds between `git pull` refreshes per cached pack (24 h default). Set to `0` to pull on every resolver rebuild |
|
|
549
552
|
| `registry.git_timeout` | `30` | Seconds before a git operation (clone, pull, checkout) is forcibly interrupted |
|
|
553
|
+
| `registry.auto_load_dependencies` | `false` | Load declared `depends_on` packs transitively; circular chains warn but still load |
|
|
550
554
|
|
|
551
555
|
Other HTTP MCP knobs live only on the nested object, for example `RailsAiBridge.configuration.mcp.authorize`, `mcp.mode`, `mcp.security_profile`, and `mcp.require_auth_in_production` — see [docs/GUIDE.md](docs/GUIDE.md) and [docs/mcp-security.md](docs/mcp-security.md).
|
|
552
556
|
</details>
|
|
@@ -831,7 +835,7 @@ Bug reports and pull requests: [github.com/igmarin/rails-ai-bridge/issues](https
|
|
|
831
835
|
|
|
832
836
|
## Acknowledgments & Origins
|
|
833
837
|
|
|
834
|
-
This gem ships as **rails-ai-bridge** (Ruby **`RailsAiBridge`**, version **
|
|
838
|
+
This gem ships as **rails-ai-bridge** (Ruby **`RailsAiBridge`**, version **4.1.0**). Earlier iterations of the same codebase were distributed as `rails-ai-context`.
|
|
835
839
|
|
|
836
840
|
RailsMCP evolved from
|
|
837
841
|
[crisnahine/rails-ai-context](https://github.com/crisnahine/rails-ai-context),
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# Plan 2: Port Registry Resolution from Rust Runtime to rails-ai-bridge
|
|
2
|
+
|
|
3
|
+
**Status:** Complete ✅ — implemented via PRs #40–#45
|
|
4
|
+
**Priority:** After Plan 1
|
|
5
|
+
**Estimated effort:** 4 weeks
|
|
6
|
+
**Depends on:** Plan 1 (agent-mcp-runtime archived)
|
|
7
|
+
|
|
8
|
+
> **Completion note (2026-08-10):** All success criteria shipped. See
|
|
9
|
+
> [`port-registry-resolution.md`](port-registry-resolution.md) for as-built decisions and deltas, and
|
|
10
|
+
> [`registry-resolution.md`](registry-resolution.md) for user-facing documentation. Naming deltas from
|
|
11
|
+
> this plan: rake tasks shipped as `ai:skills:list` / `ai:skills:resolve[pack,name]` (planned:
|
|
12
|
+
> `ai:list_skills` / `ai:resolve_skill`); configuration lives under `config.registry.*`
|
|
13
|
+
> (`Config::Registry`); the MCP surface is the unified `rails_list_registry` tool plus
|
|
14
|
+
> `rails_resolve_skill`; git operations use `Open3` (stdlib) — no `git` gem dependency.
|
|
15
|
+
|
|
16
|
+
## Objective
|
|
17
|
+
|
|
18
|
+
Port the registry resolution logic from `agent-mcp-runtime` (Rust) into `rails-ai-bridge` (Ruby). This enables the bridge to resolve skill packs from git repositories, handle priority-based loading, and support deprecation redirects — all necessary for the future skill compiler feature.
|
|
19
|
+
|
|
20
|
+
## Components to Port
|
|
21
|
+
|
|
22
|
+
### 1. New module: `lib/rails_ai_bridge/registry/`
|
|
23
|
+
|
|
24
|
+
#### `lib/rails_ai_bridge/registry/manifest.rb`
|
|
25
|
+
- Port `RegistryManifest` struct from Rust (version, packs, default_stack, context_providers)
|
|
26
|
+
- Port `PackDefinition` struct (source, tile, always_loaded, depends_on)
|
|
27
|
+
- Port `ContextProviderDefinition` and `ContextToolSpec` (for future context provider integration)
|
|
28
|
+
- Use JSON parsing with `json` gem (already in rails-ai-bridge dependencies)
|
|
29
|
+
|
|
30
|
+
#### `lib/rails_ai_bridge/registry/tile_manifest.rb`
|
|
31
|
+
- Port `TileManifest` struct (name, version, summary, depends_on, skills, agents, deprecated_skills)
|
|
32
|
+
- Port `SkillEntry` and `AgentEntry` structs
|
|
33
|
+
- Port `DeprecatedEntry` struct (moved_to, message, removed_in)
|
|
34
|
+
|
|
35
|
+
#### `lib/rails_ai_bridge/registry/git_source_resolver.rb`
|
|
36
|
+
- Port `SkillSourceResolver` from Rust
|
|
37
|
+
- Implement git clone/pull using `git` gem or shell commands
|
|
38
|
+
- Cache directory: `~/.rails-ai-bridge/cache/` (instead of `~/.agent-mcp-runtime/cache/`)
|
|
39
|
+
- Support local path resolution (sibling directories for development)
|
|
40
|
+
- Implement `GitRunner` trait pattern with a default implementation and test mock
|
|
41
|
+
|
|
42
|
+
#### `lib/rails_ai_bridge/registry/pack_detector.rb`
|
|
43
|
+
- Port `PackDetector` from Rust
|
|
44
|
+
- Parse Gemfile to detect Rails vs Hanami
|
|
45
|
+
- Return array of detected frameworks
|
|
46
|
+
|
|
47
|
+
#### `lib/rails_ai_bridge/registry/pack_resolver.rb`
|
|
48
|
+
- Port `PackResolverService` logic
|
|
49
|
+
- Implement priority-based pack loading:
|
|
50
|
+
- Priority 0: Local registries (--registry flag)
|
|
51
|
+
- Priority 10: Framework packs (rails, hanami)
|
|
52
|
+
- Priority 20: Core pack (always_loaded)
|
|
53
|
+
- Priority 30: Default stack (planning)
|
|
54
|
+
- Handle explicit pack overrides
|
|
55
|
+
- Load tile.json manifests from resolved sources
|
|
56
|
+
- Return `RegistryResolver` instance
|
|
57
|
+
|
|
58
|
+
#### `lib/rails_ai_bridge/registry/resolver.rb`
|
|
59
|
+
- Port `RegistryResolver` from Rust
|
|
60
|
+
- Implement `LoadedPack` struct (name, tile, base_path, priority)
|
|
61
|
+
- Implement `ResolvedSkill` struct (name, pack, path, content)
|
|
62
|
+
- Implement `SkillSummary` struct (name, pack, description)
|
|
63
|
+
- Implement `resolve_skill(name)` with deprecation redirect handling
|
|
64
|
+
- Implement `resolve_agent(name)`
|
|
65
|
+
- Implement `list_skills()` with deduplication by priority
|
|
66
|
+
- Implement `validate_dependencies()` to warn on missing dependencies
|
|
67
|
+
- Sort packs by priority (ascending = higher priority)
|
|
68
|
+
|
|
69
|
+
#### `lib/rails_ai_bridge/registry.rb`
|
|
70
|
+
- Main registry module that requires all submodules
|
|
71
|
+
- Public API entry point
|
|
72
|
+
|
|
73
|
+
### 2. Integration with rails-ai-bridge
|
|
74
|
+
|
|
75
|
+
**Add to `lib/rails_ai_bridge.rb`:**
|
|
76
|
+
- Require the new registry module
|
|
77
|
+
- Add convenience method `RailsAiBridge.resolve_skill(pack_name, skill_name)`
|
|
78
|
+
- Add convenience method `RailsAiBridge.list_skills()`
|
|
79
|
+
|
|
80
|
+
**Create new Rake task: `lib/rails_ai_bridge/tasks/registry.rake`**
|
|
81
|
+
|
|
82
|
+
```ruby
|
|
83
|
+
namespace :rails_ai_bridge do
|
|
84
|
+
desc "List all available skills from configured packs"
|
|
85
|
+
task list_skills: :environment do
|
|
86
|
+
# Use registry resolver to list skills
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
desc "Resolve a specific skill"
|
|
90
|
+
task :resolve_skill, [:pack, :skill] => :environment do |t, args|
|
|
91
|
+
# Resolve and print skill content
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### 3. Configuration
|
|
97
|
+
|
|
98
|
+
**Add to `lib/rails_ai_bridge/configuration.rb`:**
|
|
99
|
+
- `registry_manifest_path` — Path to registry.json (default: `config/rails_ai_bridge_registry.json`)
|
|
100
|
+
- `skill_cache_dir` — Path to git cache directory (default: `~/.rails-ai-bridge/cache`)
|
|
101
|
+
- `skill_packs` — Array of explicit pack names to load (optional)
|
|
102
|
+
- `local_registry_paths` — Array of local registry directories (optional)
|
|
103
|
+
|
|
104
|
+
### 4. Tests
|
|
105
|
+
|
|
106
|
+
**Create spec directory: `spec/rails_ai_bridge/registry/`**
|
|
107
|
+
- `manifest_spec.rb`
|
|
108
|
+
- `tile_manifest_spec.rb`
|
|
109
|
+
- `git_source_resolver_spec.rb` (with mock git runner)
|
|
110
|
+
- `pack_detector_spec.rb`
|
|
111
|
+
- `pack_resolver_spec.rb`
|
|
112
|
+
- `resolver_spec.rb`
|
|
113
|
+
|
|
114
|
+
Use the Rust tests in `agent-mcp-runtime/src/registry/pack_resolver.rs` and `resolver.rs` as reference for test cases.
|
|
115
|
+
|
|
116
|
+
### 5. Documentation
|
|
117
|
+
|
|
118
|
+
**Create `docs/registry-resolution.md`** inside rails-ai-bridge:
|
|
119
|
+
- Explain the registry resolution system
|
|
120
|
+
- Document priority rules
|
|
121
|
+
- Provide example registry.json configuration
|
|
122
|
+
- Document configuration options
|
|
123
|
+
|
|
124
|
+
### 6. Dependencies
|
|
125
|
+
|
|
126
|
+
Add to `rails-ai-bridge.gemspec` if not already present:
|
|
127
|
+
- `git` gem (for git operations) — check if already present
|
|
128
|
+
- Ensure `json` gem is available (standard library in Ruby 3+)
|
|
129
|
+
|
|
130
|
+
## Implementation Order
|
|
131
|
+
|
|
132
|
+
1. **Week 1:** Create manifest and tile_manifest structs with tests; create git_source_resolver with mock for tests
|
|
133
|
+
2. **Week 2:** Create pack_detector and pack_resolver with tests
|
|
134
|
+
3. **Week 3:** Create resolver with tests; integrate with rails-ai-bridge main module
|
|
135
|
+
4. **Week 4:** Add Rake tasks, configuration, and documentation
|
|
136
|
+
|
|
137
|
+
## Notes
|
|
138
|
+
|
|
139
|
+
- The existing `ruby-skill-bench` has a `PackResolver` class that does similar work. Review it for patterns but don't directly copy — the bridge version needs git resolution and priority handling that the bench version lacks.
|
|
140
|
+
- The Rust runtime uses async/await. Ruby doesn't have this — use synchronous git operations.
|
|
141
|
+
- Cache directory should be `~/.rails-ai-bridge/cache/` to avoid conflicts with the archived runtime.
|
|
142
|
+
- Framework detection: just check Gemfile for `'rails'` or `'hanami'` gems.
|
|
143
|
+
- Deprecation handling is critical for the skill migration from `rails-agent-skills` to `ruby-core-skills` already in progress.
|
|
144
|
+
|
|
145
|
+
## Success Criteria
|
|
146
|
+
|
|
147
|
+
- [x] All 6 registry modules created with passing specs (323 registry/config/tool examples green, 2026-08-10)
|
|
148
|
+
- [x] `rails ai:skills:list` Rake task works (shipped name; planned as `ai:list_skills`)
|
|
149
|
+
- [x] `rails "ai:skills:resolve[pack,name]"` Rake task works (shipped name; planned as `ai:resolve_skill[pack,name]`)
|
|
150
|
+
- [x] Documentation written (`docs/registry-resolution.md`)
|
|
151
|
+
- [x] Priority-based resolution handles core/rails/hanami/planning correctly
|
|
152
|
+
- [x] Deprecation redirects work (e.g., old skill name → new location)
|
data/docs/mcp-security.md
CHANGED
|
@@ -12,6 +12,59 @@ By default, HTTP MCP allows anonymous access when no auth strategy is configured
|
|
|
12
12
|
|
|
13
13
|
In non-production environments, the standalone HTTP MCP server prints a one-time stderr warning when it starts without authentication to make the default behavior visible.
|
|
14
14
|
|
|
15
|
+
## JWT authentication with short-lived tokens
|
|
16
|
+
|
|
17
|
+
`http_mcp_token` is a static bearer token: rotating it means redistributing a secret to every client. For deployments that need rotation or expiry, configure `config.mcp_jwt_decoder` instead — the highest-priority auth strategy. The gem carries **no JWT dependency**: you supply a lambda that decodes (and verifies) the token.
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
# config/initializers/rails_ai_bridge.rb
|
|
21
|
+
require "jwt" # the host app's own jwt gem
|
|
22
|
+
|
|
23
|
+
RailsAiBridge.configure do |config|
|
|
24
|
+
config.mcp.require_http_auth = true
|
|
25
|
+
|
|
26
|
+
config.mcp_jwt_decoder = ->(token) do
|
|
27
|
+
payload, _header = JWT.decode(
|
|
28
|
+
token,
|
|
29
|
+
Rails.application.credentials.jwt_mcp_secret!,
|
|
30
|
+
true, # verify signature
|
|
31
|
+
algorithm: "HS256",
|
|
32
|
+
verify_expiration: true # reject expired tokens (default when verifying)
|
|
33
|
+
)
|
|
34
|
+
payload # truthy payload => authenticated
|
|
35
|
+
rescue JWT::DecodeError, JWT::ExpiredSignature
|
|
36
|
+
nil # nil => 401 unauthorized
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Decoder contract:
|
|
42
|
+
|
|
43
|
+
- Return a **truthy payload** (Hash recommended) — request is authenticated; the payload is exposed to authorization hooks.
|
|
44
|
+
- Return **`nil`** or **`false`** — request is rejected with `401` (`:unauthorized`).
|
|
45
|
+
- **Raise** — treated as `:decode_error`, also `401`; the exception never propagates.
|
|
46
|
+
|
|
47
|
+
### Token rotation strategy
|
|
48
|
+
|
|
49
|
+
- **Short expiry, frequent re-issue.** Issue MCP tokens with a 5–15 minute `exp` from your existing login/session flow. A leaked token expires on its own; there is nothing to rotate per incident.
|
|
50
|
+
- **Signing-key rotation.** When rotating the HMAC secret (or moving to RS256/JWKS), accept both keys during the overlap window:
|
|
51
|
+
|
|
52
|
+
```ruby
|
|
53
|
+
config.mcp_jwt_decoder = ->(token) do
|
|
54
|
+
keys = [Rails.application.credentials.jwt_mcp_secret!,
|
|
55
|
+
Rails.application.credentials.jwt_mcp_secret_previous!]
|
|
56
|
+
keys.lazy.filter_map do |key|
|
|
57
|
+
JWT.decode(token, key, true, algorithm: "HS256").first
|
|
58
|
+
rescue JWT::DecodeError, JWT::ExpiredSignature
|
|
59
|
+
nil
|
|
60
|
+
end.first
|
|
61
|
+
end
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Retire the previous key once all clients have re-issued tokens.
|
|
65
|
+
- **Immediate revocation.** Stateless JWTs cannot be revoked; if you need it, combine short `exp` with your own denylist check inside the decoder (return `nil` for revoked subjects).
|
|
66
|
+
- **Client capability note.** Some AI hosts cannot refresh tokens and work best with a long-lived static token or `mcp_token_resolver` (which can consult a secret manager per request). Pick the strategy per client: JWT for short-lived machine-to-machine auth, `mcp_token_resolver` when the credential lives in a vault, `http_mcp_token` only for local development.
|
|
67
|
+
|
|
15
68
|
## Rate limiting and proxies
|
|
16
69
|
|
|
17
70
|
Built-in rate limiting keys off the Rack request IP. Behind reverse proxies, configure Rails `trusted_proxies` so `request.ip` reflects the real client; otherwise limits may apply to the wrong address or be bypassed.
|
data/docs/registry-resolution.md
CHANGED
|
@@ -200,9 +200,10 @@ Once a registry manifest is configured:
|
|
|
200
200
|
|
|
201
201
|
| Task | Description |
|
|
202
202
|
|------|-------------|
|
|
203
|
-
| `rails ai:skills:list` | Print skill catalog to stdout |
|
|
203
|
+
| `rails ai:skills:list` | Print skill catalog to stdout; `[json]` argument (or `FORMAT=json`) prints a `{"packs": [...], "skills": [...]}` document |
|
|
204
204
|
| `rails "ai:skills:resolve[pack,skill_name]"` | Resolve and print a skill's full content |
|
|
205
205
|
| `rails ai:skills:clear_cache` | Remove all cached pack git repositories and invalidate the resolver cache |
|
|
206
|
+
| `rails ai:registry:validate` | Validate the registry manifest schema; exits non-zero on the first invalid field |
|
|
206
207
|
|
|
207
208
|
Examples:
|
|
208
209
|
|
|
@@ -47,6 +47,31 @@ Create `config/rails_ai_bridge_registry.json` in your Rails app:
|
|
|
47
47
|
|
|
48
48
|
The `source` field is a GitHub `owner/repo` shorthand. The `tile` field is optional — it defaults to `directory.json` at the root of the pack.
|
|
49
49
|
|
|
50
|
+
#### Context providers (parsed, not yet consumed)
|
|
51
|
+
|
|
52
|
+
An optional `context_providers` section can declare external context services. The bridge parses it into `Registry::ContextProviderDefinition` / `Registry::ContextToolSpec` value objects, but no integration consumes these definitions yet — this is preparatory for future context provider support.
|
|
53
|
+
|
|
54
|
+
```json
|
|
55
|
+
{
|
|
56
|
+
"version": "1.0.0",
|
|
57
|
+
"packs": {},
|
|
58
|
+
"default_stack": [],
|
|
59
|
+
"context_providers": {
|
|
60
|
+
"app_mcp": {
|
|
61
|
+
"type": "mcp",
|
|
62
|
+
"endpoint": "http://localhost:3000/mcp",
|
|
63
|
+
"optional": true,
|
|
64
|
+
"tools": [
|
|
65
|
+
"rails_get_schema",
|
|
66
|
+
{ "name": "rails_get_model_details", "field": "models", "arguments": { "model": "User" } }
|
|
67
|
+
]
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Each tool entry is either a simple tool-name string or a mapped object with `name`, `field`, and optional `arguments`.
|
|
74
|
+
|
|
50
75
|
### Step 2 — Configure the bridge
|
|
51
76
|
|
|
52
77
|
In `config/initializers/rails_ai_bridge.rb`:
|
|
@@ -268,6 +293,15 @@ rails_list_registry type=packs
|
|
|
268
293
|
|
|
269
294
|
Returns all loaded packs with name, version, priority, and summary. Use this to confirm which packs are active and what their priorities are.
|
|
270
295
|
|
|
296
|
+
### Apply a skill or agent in-context
|
|
297
|
+
|
|
298
|
+
```bash
|
|
299
|
+
rails_use_skill name=code-review
|
|
300
|
+
rails_use_agent name=tdd-workflow
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
Resolves the named skill/agent (priority ordering and deprecation redirects included) and returns it framed as a directive the assistant should **execute**, not just read: an intent header, any deprecation notice, the full content, and a follow-through footer. Use `rails_resolve_skill` instead when you only want to inspect content or pin a pack (`pack=`, `type=`).
|
|
304
|
+
|
|
271
305
|
---
|
|
272
306
|
|
|
273
307
|
## Rake task reference
|
|
@@ -280,6 +314,15 @@ rails ai:skills:list
|
|
|
280
314
|
|
|
281
315
|
Prints a skills table to stdout with skill name, pack, and truncated description. Good for quickly checking what is loaded.
|
|
282
316
|
|
|
317
|
+
For machine-readable output (CI, custom tooling), request JSON:
|
|
318
|
+
|
|
319
|
+
```bash
|
|
320
|
+
rails "ai:skills:list[json]"
|
|
321
|
+
FORMAT=json rails ai:skills:list # same via env var
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
The JSON document is `{ "packs": [...], "skills": [...] }` with pack summaries (`name`, `version`, `summary`, `priority`) and skill summaries (`name`, `pack`, `description`).
|
|
325
|
+
|
|
283
326
|
### Resolve a skill
|
|
284
327
|
|
|
285
328
|
```bash
|
|
@@ -305,6 +348,30 @@ Removes all locally cached pack repositories from the skill cache directory and
|
|
|
305
348
|
- You want to free disk space
|
|
306
349
|
- You changed the `skill_cache_dir` configuration
|
|
307
350
|
|
|
351
|
+
### Validate the manifest
|
|
352
|
+
|
|
353
|
+
```bash
|
|
354
|
+
rails ai:registry:validate
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
Checks the registry manifest at `config.registry.registry_manifest_path` against the expected schema: pack `source` must be a non-empty string, `depends_on` an array of strings, `always_loaded` a boolean, and so on. Exits non-zero on the first invalid field, making it suitable for CI and pre-commit hooks.
|
|
358
|
+
|
|
359
|
+
---
|
|
360
|
+
|
|
361
|
+
## Transitive dependency loading (`auto_load_dependencies`)
|
|
362
|
+
|
|
363
|
+
By default, a pack's `depends_on` entries are **not** loaded automatically — the resolver only warns when a dependency is missing from the active set. Enable transitive loading to have declared dependencies pulled in automatically:
|
|
364
|
+
|
|
365
|
+
```ruby
|
|
366
|
+
RailsAiBridge.configure do |config|
|
|
367
|
+
config.registry.auto_load_dependencies = true
|
|
368
|
+
end
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
- Expansion is iterative (dependencies of dependencies), capped at 10 levels.
|
|
372
|
+
- Only dependencies defined in the manifest are loaded; undefined ones still produce the missing-dependency warning.
|
|
373
|
+
- Circular chains are detected and reported as a stderr warning (`Circular dependency detected: a -> b -> a`), but every pack in the cycle is still loaded.
|
|
374
|
+
|
|
308
375
|
---
|
|
309
376
|
|
|
310
377
|
## Git operation settings
|
|
@@ -381,7 +448,8 @@ initializer changes take effect immediately.
|
|
|
381
448
|
| `git checkout <ref>` failed | Invalid ref or detached HEAD issue | Verify the `ref` value matches a branch, tag, or SHA in the remote repo |
|
|
382
449
|
| `Lockfile mismatch for pack '…'` | Resolved pack commit differs from `directory.lock` | Run `rails ai:registry:lockfile` to update the lockfile after reviewing the pack changes |
|
|
383
450
|
| Git operation hangs / server request times out | Slow or unreachable remote | Reduce `git_timeout` to fail faster; check network connectivity to the remote |
|
|
384
|
-
| Warning: "Pack '…' depends on '…' which is not in the active pack set" | A loaded pack has an unsatisfied `depends_on` entry | Add the missing pack name to `always_loaded` or `skill_packs` in your manifest |
|
|
451
|
+
| Warning: "Pack '…' depends on '…' which is not in the active pack set" | A loaded pack has an unsatisfied `depends_on` entry | Add the missing pack name to `always_loaded` or `skill_packs` in your manifest, or enable `config.registry.auto_load_dependencies` |
|
|
452
|
+
| Warning: "Circular dependency detected: …" | Two or more active packs depend on each other | Break the cycle in the manifests; packs still load, but the cycle usually indicates a mistake |
|
|
385
453
|
|
|
386
454
|
---
|
|
387
455
|
|
|
@@ -42,6 +42,10 @@ module RailsAiBridge
|
|
|
42
42
|
# :strict (default) raises, :warn logs but proceeds, :disabled skips verification.
|
|
43
43
|
attr_accessor :lockfile_verification
|
|
44
44
|
|
|
45
|
+
# @return [Boolean] whether declared +depends_on+ packs are loaded transitively
|
|
46
|
+
# (default: false — dependencies must be listed explicitly and missing ones only warn)
|
|
47
|
+
attr_accessor :auto_load_dependencies
|
|
48
|
+
|
|
45
49
|
# Sets the git pull TTL.
|
|
46
50
|
#
|
|
47
51
|
# Coerces the value to a non-negative integer; raises +ArgumentError+ for
|
|
@@ -102,6 +106,7 @@ module RailsAiBridge
|
|
|
102
106
|
@git_timeout = 30
|
|
103
107
|
@lockfile_path = 'config/rails_ai_bridge/directory.lock'
|
|
104
108
|
@lockfile_verification = :strict
|
|
109
|
+
@auto_load_dependencies = false
|
|
105
110
|
end
|
|
106
111
|
end
|
|
107
112
|
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsAiBridge
|
|
4
|
+
module Registry
|
|
5
|
+
# Immutable value object describing a context provider service declared in the
|
|
6
|
+
# registry manifest.
|
|
7
|
+
#
|
|
8
|
+
# Context providers are external services (currently MCP servers) that the
|
|
9
|
+
# bridge can query for project context. This definition is preparatory — the
|
|
10
|
+
# data structures mirror the Rust runtime's manifest format, but no
|
|
11
|
+
# integration consumes them yet.
|
|
12
|
+
#
|
|
13
|
+
# @!attribute [r] type
|
|
14
|
+
# @return [String] provider type, e.g. "mcp"
|
|
15
|
+
# @!attribute [r] endpoint
|
|
16
|
+
# @return [String] provider HTTP endpoint base URL
|
|
17
|
+
# @!attribute [r] optional
|
|
18
|
+
# @return [Boolean] whether the provider may be skipped when unavailable
|
|
19
|
+
# @!attribute [r] tools
|
|
20
|
+
# @return [Array<ContextToolSpec>] tools requested from the provider
|
|
21
|
+
ContextProviderDefinition = Data.define(:type, :endpoint, :optional, :tools) do
|
|
22
|
+
# Builds a {ContextProviderDefinition} from a parsed JSON hash.
|
|
23
|
+
#
|
|
24
|
+
# @param hash [Hash] parsed JSON object
|
|
25
|
+
# @return [ContextProviderDefinition]
|
|
26
|
+
# @raise [ArgumentError] when a required field is missing
|
|
27
|
+
def self.from_json(hash)
|
|
28
|
+
new(
|
|
29
|
+
type: hash.fetch('type'),
|
|
30
|
+
endpoint: hash.fetch('endpoint'),
|
|
31
|
+
optional: hash.fetch('optional', false),
|
|
32
|
+
tools: (hash['tools'] || []).map { |tool| ContextToolSpec.from_json(tool) }
|
|
33
|
+
)
|
|
34
|
+
rescue KeyError => error
|
|
35
|
+
raise ArgumentError, "Context provider definition missing required field: #{error.key}"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# @return [Boolean]
|
|
39
|
+
def optional? = optional
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsAiBridge
|
|
4
|
+
module Registry
|
|
5
|
+
# Immutable value object describing a single tool requested from a context provider.
|
|
6
|
+
#
|
|
7
|
+
# Mirrors the untagged +ContextToolSpec+ enum in the Rust runtime: a spec is
|
|
8
|
+
# either a plain tool name (simple) or a mapping that routes the tool output
|
|
9
|
+
# into a named context field, optionally with arguments (mapped).
|
|
10
|
+
#
|
|
11
|
+
# @!attribute [r] name
|
|
12
|
+
# @return [String] name of the remote tool to execute
|
|
13
|
+
# @!attribute [r] field
|
|
14
|
+
# @return [String, nil] target context field for mapped tools; nil for simple tools
|
|
15
|
+
# @!attribute [r] arguments
|
|
16
|
+
# @return [Hash, nil] optional arguments passed when executing the tool
|
|
17
|
+
ContextToolSpec = Data.define(:name, :field, :arguments) do
|
|
18
|
+
# Builds a {ContextToolSpec} from a parsed JSON value.
|
|
19
|
+
#
|
|
20
|
+
# @param value [String, Hash] plain tool name or mapped tool object
|
|
21
|
+
# @return [ContextToolSpec]
|
|
22
|
+
# @raise [ArgumentError] when the value is neither a String nor a Hash,
|
|
23
|
+
# or a mapped tool is missing a required field
|
|
24
|
+
def self.from_json(value)
|
|
25
|
+
return new(name: value, field: nil, arguments: nil) if value.is_a?(String)
|
|
26
|
+
return from_mapped_json(value) if value.is_a?(Hash)
|
|
27
|
+
|
|
28
|
+
raise ArgumentError, "Context tool spec must be a String or an object, got #{value.class.name}"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# @return [Boolean] true when the spec is a plain tool name
|
|
32
|
+
# :reek:NilCheck -- value-object predicate over an optional attribute
|
|
33
|
+
def simple? = field.nil?
|
|
34
|
+
|
|
35
|
+
# @return [Boolean] true when the spec maps tool output into a context field
|
|
36
|
+
# :reek:NilCheck -- value-object predicate over an optional attribute
|
|
37
|
+
def mapped? = !field.nil?
|
|
38
|
+
|
|
39
|
+
# @api private
|
|
40
|
+
def self.from_mapped_json(hash)
|
|
41
|
+
new(
|
|
42
|
+
name: hash.fetch('name'),
|
|
43
|
+
field: hash.fetch('field'),
|
|
44
|
+
arguments: hash['arguments']
|
|
45
|
+
)
|
|
46
|
+
rescue KeyError => error
|
|
47
|
+
raise ArgumentError, "Context tool spec missing required field: #{error.key}"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private_class_method :from_mapped_json
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|