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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a62e4a3003e8feb85f8ace7ec52c9ae89a2d0cd7c004673ca669cbacaf4fe665
4
- data.tar.gz: cf85e1034658180a1530dde70fa4b2c724b9ad289451f216b6723f40e94f4bb7
3
+ metadata.gz: 2f7810f4efa4400b60d813b2344b82f38e0a731ccc89e68a3be51aabe2526a3d
4
+ data.tar.gz: 7faa7fe7f2f731d572806596a3f60d756d85b589ba46b6dfa7ef2e614bea8749
5
5
  SHA512:
6
- metadata.gz: 95a1948e2f1ae02299f8b1505c83674f3172ea7da8fd384e17a9414c43ccee911a69e2f6713f320e8d520337c3cf8c343c082ad716ebb84d6128c64f4b2ceee9
7
- data.tar.gz: f4c81faedfe64c9268c635409d049c9a2b128306bcc666a55d0ef2f6624775bb098f10378d16d4819c6163d48cd8da42ebbf973474cb24183e09280a27e3548d
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/` — 14 built-in MCP tools using the official mcp SDK (hosts can add more via `additional_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
- ## [Unreleased]
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/` — 14 built-in MCP tools using the official mcp SDK
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/` — 11 built-in MCP tools using the official mcp SDK (hosts can add more via `additional_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 **13 built-in tools** via MCP that AI clients call on-demand (hosts can append more via `config.additional_tools`):
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 **3.6.2**). Earlier iterations of the same codebase were distributed as `rails-ai-context`.
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.
@@ -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