legionio 1.4.73 → 1.4.79

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.
Files changed (72) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +13 -0
  3. data/CHANGELOG.md +63 -0
  4. data/CLAUDE.md +39 -45
  5. data/README.md +8 -6
  6. data/docs/plans/2026-03-19-hooks-expansion-design.md +211 -0
  7. data/legionio.gemspec +1 -1
  8. data/lib/legion/api/catalog.rb +82 -0
  9. data/lib/legion/api/hooks.rb +59 -28
  10. data/lib/legion/api/lex.rb +76 -0
  11. data/lib/legion/api/llm.rb +20 -2
  12. data/lib/legion/api/openapi.rb +89 -0
  13. data/lib/legion/api.rb +37 -8
  14. data/lib/legion/data/local_migrations/20260319000001_create_extension_catalog.rb +15 -0
  15. data/lib/legion/data/local_migrations/20260319000002_create_extension_permissions.rb +17 -0
  16. data/lib/legion/extensions/builders/hooks.rb +5 -1
  17. data/lib/legion/extensions/builders/routes.rb +66 -0
  18. data/lib/legion/extensions/builders/runners.rb +1 -0
  19. data/lib/legion/extensions/catalog.rb +102 -0
  20. data/lib/legion/extensions/core.rb +21 -1
  21. data/lib/legion/extensions/hooks/base.rb +5 -1
  22. data/lib/legion/extensions/permissions.rb +141 -0
  23. data/lib/legion/extensions.rb +8 -0
  24. data/lib/legion/version.rb +1 -1
  25. metadata +14 -53
  26. data/lib/legion/api/auth_kerberos.rb +0 -83
  27. data/lib/legion/api/oauth.rb +0 -39
  28. data/lib/legion/mcp/auth.rb +0 -50
  29. data/lib/legion/mcp/context_compiler.rb +0 -173
  30. data/lib/legion/mcp/embedding_index.rb +0 -113
  31. data/lib/legion/mcp/observer.rb +0 -135
  32. data/lib/legion/mcp/resources/extension_info.rb +0 -67
  33. data/lib/legion/mcp/resources/runner_catalog.rb +0 -63
  34. data/lib/legion/mcp/server.rb +0 -165
  35. data/lib/legion/mcp/tool_governance.rb +0 -77
  36. data/lib/legion/mcp/tools/create_chain.rb +0 -50
  37. data/lib/legion/mcp/tools/create_relationship.rb +0 -51
  38. data/lib/legion/mcp/tools/create_schedule.rb +0 -64
  39. data/lib/legion/mcp/tools/delete_chain.rb +0 -52
  40. data/lib/legion/mcp/tools/delete_relationship.rb +0 -52
  41. data/lib/legion/mcp/tools/delete_schedule.rb +0 -52
  42. data/lib/legion/mcp/tools/delete_task.rb +0 -49
  43. data/lib/legion/mcp/tools/describe_runner.rb +0 -92
  44. data/lib/legion/mcp/tools/disable_extension.rb +0 -50
  45. data/lib/legion/mcp/tools/discover_tools.rb +0 -53
  46. data/lib/legion/mcp/tools/do_action.rb +0 -55
  47. data/lib/legion/mcp/tools/enable_extension.rb +0 -50
  48. data/lib/legion/mcp/tools/get_config.rb +0 -63
  49. data/lib/legion/mcp/tools/get_extension.rb +0 -56
  50. data/lib/legion/mcp/tools/get_status.rb +0 -50
  51. data/lib/legion/mcp/tools/get_task.rb +0 -48
  52. data/lib/legion/mcp/tools/get_task_logs.rb +0 -56
  53. data/lib/legion/mcp/tools/list_chains.rb +0 -48
  54. data/lib/legion/mcp/tools/list_extensions.rb +0 -46
  55. data/lib/legion/mcp/tools/list_relationships.rb +0 -45
  56. data/lib/legion/mcp/tools/list_schedules.rb +0 -51
  57. data/lib/legion/mcp/tools/list_tasks.rb +0 -50
  58. data/lib/legion/mcp/tools/list_workers.rb +0 -53
  59. data/lib/legion/mcp/tools/rbac_assignments.rb +0 -45
  60. data/lib/legion/mcp/tools/rbac_check.rb +0 -45
  61. data/lib/legion/mcp/tools/rbac_grants.rb +0 -41
  62. data/lib/legion/mcp/tools/routing_stats.rb +0 -51
  63. data/lib/legion/mcp/tools/run_task.rb +0 -68
  64. data/lib/legion/mcp/tools/show_worker.rb +0 -48
  65. data/lib/legion/mcp/tools/team_summary.rb +0 -53
  66. data/lib/legion/mcp/tools/update_chain.rb +0 -54
  67. data/lib/legion/mcp/tools/update_relationship.rb +0 -55
  68. data/lib/legion/mcp/tools/update_schedule.rb +0 -65
  69. data/lib/legion/mcp/tools/worker_costs.rb +0 -55
  70. data/lib/legion/mcp/tools/worker_lifecycle.rb +0 -54
  71. data/lib/legion/mcp/usage_filter.rb +0 -86
  72. data/lib/legion/mcp.rb +0 -29
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7693068b5190222c508dd07a248424e8f9e1eff77e80059cbd8a4fc81e9e6325
4
- data.tar.gz: 43cc0da2efe7d604101ab87426c21b5f8ce677a8c023aeec38e5b91018b515ae
3
+ metadata.gz: 833830caa5613e077d49c9a67c1dc00b907a473daf7be9033c9389c51546a5e3
4
+ data.tar.gz: 9c93197f8b0a9ecd0f784598c9b81e98d0158ab03b94d4b822a3d6ef55419868
5
5
  SHA512:
6
- metadata.gz: a2d75a86b1646113be917a9f8ed17f8f7aa8ffb48ff371db78ee62fbfdba1929d85e675f2bf7d6a5469b077f10370f3d044eb6f5049db353d471f5be1c3cbab5
7
- data.tar.gz: 2f00a29a6ddf7be18ee71839b97e1a8aeeb74f2cecd820932447bc574fcfeb595a6adce0d757909bf3e3d68909235fcbb32ad95182192b5471dea0a2f3d39a0e
6
+ metadata.gz: 4689fa56cc6ad0cfd06f1b836fd36211edeaedacc65306ed888bdc97d36e1d430f5c7c6f3eb65427ff7c234cf1908f2fb14f26f448b8d6f2a414c726011c699f
7
+ data.tar.gz: 3a127117aba989a57d1e1077e6972a2d32d07c02148fb53eae5c083e8752d509d4a9d84a9db9bb3086292a983e3154d8632e7bca41483a5e9740ccb1769c1ec0
@@ -14,3 +14,16 @@ jobs:
14
14
  uses: LegionIO/.github/.github/workflows/release.yml@main
15
15
  secrets:
16
16
  rubygems-api-key: ${{ secrets.RUBYGEMS_API_KEY }}
17
+
18
+ trigger-homebrew:
19
+ needs: release
20
+ if: needs.release.outputs.changed == 'true'
21
+ runs-on: ubuntu-latest
22
+ steps:
23
+ - name: Trigger daemon build
24
+ env:
25
+ GH_TOKEN: ${{ secrets.HOMEBREW_DISPATCH_TOKEN }}
26
+ run: |
27
+ gh api repos/LegionIO/homebrew-tap/dispatches \
28
+ -f event_type=build-daemon \
29
+ -f "client_payload[legionio_version]=${{ needs.release.outputs.version }}"
data/CHANGELOG.md CHANGED
@@ -1,5 +1,68 @@
1
1
  # Legion Changelog
2
2
 
3
+ ## [1.4.79] - 2026-03-20
4
+
5
+ ### Added
6
+ - Unified LEX routing layer: auto-expose runner functions as POST endpoints at `/api/lex/{ext}/{runner}/{action}`
7
+ - `Builders::Routes` auto-discovers runner public methods during extension autobuild
8
+ - `Routes::Lex` wildcard handler dispatches through Ingress with JWT + RBAC
9
+ - `GET /api/lex` listing endpoint for route discovery
10
+ - Settings-based configuration at `api.lex_routes` (global enable, per-extension enable, runner/function exclusions)
11
+ - `skip_routes` DSL for runner modules to opt out of auto-route exposure
12
+ - Auto-routes included in OpenAPI spec generation
13
+ - `runner_module` reference stored in builders runner hash for introspection
14
+
15
+ ## [1.4.78] - 2026-03-19
16
+
17
+ ### Added
18
+ - Response headers support in `render_custom_response`: runners can return `response[:headers]` hash for custom HTTP headers
19
+
20
+ ### Removed
21
+ - Legacy `POST /api/hooks/:lex_name/:hook_name` route (superseded by `GET|POST /api/hooks/lex/*` splat routes in v1.4.76)
22
+ - Hardcoded `GET /api/auth/negotiate` Kerberos route (migrated to lex-kerberos hook at `/api/hooks/lex/kerberos/negotiate`)
23
+ - `Routes::AuthKerberos` module and `api/auth_kerberos.rb` file
24
+
25
+ ## [1.4.77] - 2026-03-19
26
+
27
+ ### Added
28
+ - Hardcoded deny list in `Extensions::Permissions` blocking access to `~/.ssh`, `~/.gnupg`, `~/.aws/credentials`
29
+ - Deny list overrides all other permission checks including explicit approvals
30
+
31
+ ## [1.4.76] - 2026-03-19
32
+
33
+ ### Added
34
+ - `Hooks::Base.mount(path)` DSL for extension-derived URL suffixes (e.g., `/callback`)
35
+ - `GET /api/hooks/lex/*` splat route for hook discovery via GET requests
36
+ - `POST /api/hooks/lex/*` splat route with `route_path`-based hook dispatch
37
+ - `Legion::API.find_hook_by_path(path)` for direct route-path lookup in hook registry
38
+ - `route_path` field stored in hook registry entries and returned in `GET /api/hooks` listing
39
+ - Runner-controlled responses: `result[:response]` hash with `:status`, `:content_type`, `:body`
40
+ - `build_payload`, `dispatch_hook`, `render_custom_response` extracted helpers in Routes::Hooks
41
+
42
+ ### Changed
43
+ - `register_hook` now accepts `route_path:` keyword; defaults to `lex_name/hook_name` if omitted
44
+ - `builders/hooks.rb` computes `route_path` from `extension_name/hook_name + mount_path`
45
+ - `extensions/core.rb` passes `route_path:` when calling `Legion::API.register_hook`
46
+ - `GET /api/hooks` listing now includes `route_path` and updated `endpoint` field
47
+ - Removed `Routes::OAuth` (moved OAuth callback to lex-microsoft_teams hook with mount path)
48
+ - `handle_hook_request` refactored into smaller helpers to stay within complexity limits
49
+
50
+ ## [1.4.75] - 2026-03-19
51
+
52
+ ### Added
53
+ - `Legion::Extensions::Catalog` singleton state machine tracking extension lifecycle (registered/loaded/starting/running/stopping/stopped)
54
+ - `Legion::Extensions::Permissions` three-layer file permission model (sandbox, declared paths, auto-approve globs)
55
+ - `GET /api/catalog` and `GET /api/catalog/:name` extension capability manifest endpoints
56
+ - Tier 0 routing in `POST /api/llm/chat` via `Legion::MCP::TierRouter` for LLM-free cached responses
57
+ - Data::Local migrations for extension_catalog and extension_permissions tables
58
+ - Catalog lifecycle wired into extension loader (register/loaded/running/stopping/stopped transitions)
59
+
60
+ ## [1.4.74] - 2026-03-19
61
+
62
+ ### Changed
63
+ - Extracted `Legion::MCP` to dedicated `legion-mcp` gem (v0.1.0)
64
+ - Replaced `mcp` gem dependency with `legion-mcp`
65
+
3
66
  ## [1.4.73] - 2026-03-19
4
67
 
5
68
  ### Added
data/CLAUDE.md CHANGED
@@ -9,7 +9,7 @@ The primary gem for the LegionIO framework. An extensible async job engine for s
9
9
 
10
10
  **GitHub**: https://github.com/LegionIO/LegionIO
11
11
  **Gem**: `legionio`
12
- **Version**: 1.4.70
12
+ **Version**: 1.4.79
13
13
  **License**: Apache-2.0
14
14
  **Docker**: `legionio/legion`
15
15
  **Ruby**: >= 3.4
@@ -46,8 +46,8 @@ Legion.start
46
46
  ├── 5. require legion-cache
47
47
  ├── 6. setup_data (legion-data, MySQL/SQLite + migrations, optional)
48
48
  ├── 7. setup_rbac (legion-rbac, optional)
49
- ├── 8. setup_llm (legion-llm, optional)
50
- ├── 9. setup_gaia (legion-gaia, cognitive layer, optional)
49
+ ├── 8. setup_llm (legion-llm, AI provider setup + routing, optional)
50
+ ├── 9. setup_gaia (legion-gaia, cognitive coordination layer, optional)
51
51
  ├── 10. setup_telemetry (OpenTelemetry, optional)
52
52
  ├── 11. setup_supervision (process supervision)
53
53
  ├── 12. load_extensions (two-phase: require+autobuild all, then hook_all_actors)
@@ -95,9 +95,10 @@ Legion (lib/legion.rb)
95
95
  │ │ └── Nothing # No-op actor
96
96
  │ ├── Builders/ # Build actors and runners from LEX definitions
97
97
  │ │ ├── Actors # Build actors from extension definitions
98
- │ │ ├── Runners # Build runners from extension definitions
98
+ │ │ ├── Runners # Build runners from extension definitions (stores runner_module ref)
99
99
  │ │ ├── Helpers # Builder utilities
100
- │ │ └── Hooks # Webhook hook system builder
100
+ │ │ ├── Hooks # Webhook hook system builder
101
+ │ │ └── Routes # Auto-route builder: introspects runners, registers POST /api/lex/* routes
101
102
  │ ├── Helpers/ # Helper mixins for extensions
102
103
  │ │ ├── Base # Base helper mixin
103
104
  │ │ ├── Core # Core helper mixin
@@ -128,6 +129,7 @@ Legion (lib/legion.rb)
128
129
  │ │ ├── Events # SSE stream (sinatra stream) + ring buffer polling fallback
129
130
  │ │ ├── Transport # Connection status, exchanges, queues, publish
130
131
  │ │ ├── Hooks # List + trigger registered extension hooks
132
+ │ │ ├── Lex # Auto-routes: `POST /api/lex/*` wildcard + `GET /api/lex` listing
131
133
  │ │ ├── Workers # Digital worker lifecycle (`/api/workers/*`) + team routes (`/api/teams/*`)
132
134
  │ │ ├── Coldstart # `POST /api/coldstart/ingest` — trigger lex-coldstart ingest from API
133
135
  │ │ ├── Capacity # Aggregate, forecast, per-worker capacity endpoints
@@ -142,26 +144,13 @@ Legion (lib/legion.rb)
142
144
  │ │ ├── ApiVersion # `/api/v1/` rewrite, Deprecation/Sunset headers
143
145
  │ │ ├── BodyLimit # Request body size limit (1MB max, returns 413)
144
146
  │ │ └── RateLimit # Sliding-window rate limiting with per-IP/agent/tenant tiers
145
- └── hook_registry # Class-level registry: register_hook, find_hook, registered_hooks
146
- # Populated by extensions via Legion::API.register_hook(...)
147
+ ├── hook_registry # Class-level registry: register_hook, find_hook, registered_hooks
148
+ # Populated by extensions via Legion::API.register_hook(...)
149
+ │ └── route_registry # Class-level registry: register_route, find_route_by_path, registered_routes
150
+ │ # Populated by Builders::Routes during autobuild
147
151
 
148
- ├── MCP (mcp gem) # MCP server for AI agent integration
149
- ├── MCP.server # Singleton factory: Legion::MCP.server returns MCP::Server instance
150
- │ ├── Server # MCP::Server builder, tool/resource registration
151
- │ ├── Tools/ # 35 MCP::Tool subclasses (legion.* namespace)
152
- │ │ ├── RunTask # Agentic: dot notation task execution
153
- │ │ ├── DescribeRunner # Agentic: runner/function discovery
154
- │ │ ├── List/Get/Delete Task + GetTaskLogs
155
- │ │ ├── List/Create/Update/Delete Chain
156
- │ │ ├── List/Create/Update/Delete Relationship
157
- │ │ ├── List/Get/Enable/Disable Extension
158
- │ │ ├── List/Create/Update/Delete Schedule
159
- │ │ ├── GetStatus, GetConfig
160
- │ │ ├── ListWorkers, ShowWorker, WorkerLifecycle, WorkerCosts, TeamSummary, RoutingStats
161
- │ │ └── RbacAssignments, RbacCheck, RbacGrants
162
- │ └── Resources/
163
- │ ├── RunnerCatalog # legion://runners - all ext.runner.func paths
164
- │ └── ExtensionInfo # legion://extensions/{name} - extension detail template
152
+ ├── MCP (legion-mcp gem) # Extracted to standalone gem see legion-mcp/CLAUDE.md
153
+ └── (35 tools, 2 resources, TierRouter, PatternStore, ContextGuard, Observer, EmbeddingIndex)
165
154
 
166
155
  ├── DigitalWorker # Digital worker platform (AI-as-labor governance)
167
156
  │ ├── Lifecycle # Worker state machine (active/paused/retired/terminated)
@@ -228,6 +217,10 @@ Legion (lib/legion.rb)
228
217
  ├── Pr # `legion pr` - AI-generated PR title and description via LLM
229
218
  ├── Review # `legion review` - AI code review with severity levels
230
219
  ├── Gaia # `legion gaia` - Gaia status
220
+ ├── Llm # `legion llm` - LLM subsystem status and provider health
221
+ ├── Detect # `legion detect scan` - scan environment and recommend extensions
222
+ ├── Observe # `legion observe stats` - MCP tool usage statistics from Observer
223
+ ├── Tty # `legion tty interactive` - launch rich terminal UI (legion-tty)
231
224
  ├── Graph # `legion graph show` - task relationship graph (mermaid/dot)
232
225
  ├── Trace # `legion trace search` - NL trace search via LLM
233
226
  ├── Dashboard # `legion dashboard` - TUI operational dashboard with auto-refresh
@@ -364,7 +357,7 @@ legion
364
357
 
365
358
  plan # read-only exploration mode (no writes/edits/shell)
366
359
  [--model MODEL] [--provider PROVIDER]
367
- # Slash commands: /save (writes plan to docs/plans/), /help, /quit
360
+ # Slash commands: /save (writes plan to docs/work/planning/), /help, /quit
368
361
 
369
362
  swarm # multi-agent workflow orchestration
370
363
  start NAME # run a workflow from .legion/swarms/NAME.json
@@ -483,11 +476,11 @@ legion
483
476
 
484
477
  ### MCP Design
485
478
 
486
- - Uses `mcp` gem (~> 0.8): `MCP::Server`, `MCP::Tool`, `MCP::Resource`
487
- - Transports: `MCP::Server::Transports::StdioTransport`, `MCP::Server::Transports::StreamableHTTPTransport`
488
- - HTTP transport uses rackup + puma
479
+ Extracted to the `legion-mcp` gem (v0.1.0). See `legion-mcp/CLAUDE.md` for full architecture.
480
+
489
481
  - `Legion::MCP.server` is memoized singleton — call `Legion::MCP.reset!` in tests
490
482
  - Tool naming: `legion.snake_case_name` (dot namespace, not slash)
483
+ - Tier 0 routing: PatternStore + TierRouter + ContextGuard for LLM-free cached responses
491
484
 
492
485
  ## Dependencies
493
486
 
@@ -507,7 +500,7 @@ legion
507
500
  | `oj` (>= 3.16) | Fast JSON (C extension) |
508
501
  | `puma` (>= 6.0) | HTTP server for API |
509
502
  | `rackup` (>= 2.0) | Rack server launcher for MCP HTTP transport |
510
- | `mcp` (~> 0.8) | MCP server SDK |
503
+ | `legion-mcp` | MCP server + Tier 0 routing (extracted gem) |
511
504
  | `reline` (>= 0.5) | Interactive line editing for chat REPL |
512
505
  | `rouge` (>= 4.0) | Syntax highlighting for chat markdown rendering |
513
506
  | `tty-spinner` (~> 0.9) | Spinner animation for CLI loading states |
@@ -539,7 +532,7 @@ rack-test, rake, rspec, rubocop, rubocop-rspec, simplecov
539
532
  | `lib/legion/extensions.rb` | LEX discovery, loading, actor hooking, shutdown |
540
533
  | `lib/legion/extensions/core.rb` | Extension mixin (requirement flags, autobuild) |
541
534
  | `lib/legion/extensions/actors/` | Actor types: base, every, loop, once, poll, subscription, nothing, defaults |
542
- | `lib/legion/extensions/builders/` | Build actors, runners, helpers, hooks from definitions |
535
+ | `lib/legion/extensions/builders/` | Build actors, runners, helpers, hooks, routes from definitions |
543
536
  | `lib/legion/extensions/helpers/` | Mixins: base, core, cache, data, logger, transport, task, lex |
544
537
  | `lib/legion/extensions/data/` | Extension-level migrator and model |
545
538
  | `lib/legion/extensions/hooks/base.rb` | Webhook hook base class |
@@ -572,16 +565,19 @@ rack-test, rake, rspec, rubocop, rubocop-rspec, simplecov
572
565
  | `lib/legion/api/settings.rb` | Settings: read/write with redaction + readonly guards |
573
566
  | `lib/legion/api/events.rb` | Events: SSE stream + polling fallback (ring buffer) |
574
567
  | `lib/legion/api/transport.rb` | Transport: status, exchanges, queues, publish |
575
- | `lib/legion/api/hooks.rb` | Hooks: list registered + trigger via Ingress |
568
+ | `lib/legion/api/hooks.rb` | Hooks: list registered + trigger via Ingress; supports custom response headers |
569
+ | `lib/legion/api/lex.rb` | Lex auto-routes: `POST /api/lex/*` wildcard dispatch + `GET /api/lex` listing |
576
570
  | `lib/legion/api/workers.rb` | Workers + Teams: digital worker lifecycle REST endpoints (`/api/workers/*`) and team cost endpoints (`/api/teams/*`) |
577
571
  | `lib/legion/api/coldstart.rb` | Coldstart: `POST /api/coldstart/ingest` — triggers lex-coldstart ingest runner (requires lex-coldstart + lex-memory) |
578
572
  | `lib/legion/api/gaia.rb` | Gaia: system status endpoints |
579
573
  | `lib/legion/api/token.rb` | Token: JWT token issuance endpoint |
580
574
  | `lib/legion/api/openapi.rb` | OpenAPI: `Legion::API::OpenAPI.spec` / `.to_json`; also served at `GET /api/openapi.json` |
581
- | `lib/legion/api/oauth.rb` | OAuth: `GET /api/oauth/microsoft_teams/callback` — receives delegated OAuth redirect and stores tokens |
582
575
  | `lib/legion/api/capacity.rb` | Capacity: aggregate, forecast, and per-worker capacity endpoints |
583
576
  | `lib/legion/api/tenants.rb` | Tenants: listing, provisioning, suspension, quota check |
577
+ | `lib/legion/api/catalog.rb` | Catalog: extension catalog with metadata endpoints |
578
+ | `lib/legion/api/llm.rb` | LLM: provider status and routing configuration endpoints |
584
579
  | `lib/legion/api/audit.rb` | Audit: list, show, count, export audit log entries |
580
+ | `lib/legion/api/auth.rb` | Auth: combined token exchange endpoint (`POST /api/auth/token` — JWKS verify + RBAC claims mapper) |
585
581
  | `lib/legion/api/auth_human.rb` | Auth: human user authentication endpoints |
586
582
  | `lib/legion/api/auth_worker.rb` | Auth: digital worker authentication endpoints |
587
583
  | `lib/legion/api/rbac.rb` | RBAC: role listing, permission grants, access checks |
@@ -604,19 +600,12 @@ rack-test, rake, rspec, rubocop, rubocop-rspec, simplecov
604
600
  | `lib/legion/tenant_context.rb` | Thread-local tenant context propagation (set, clear, with block) |
605
601
  | `lib/legion/tenants.rb` | Tenant CRUD, suspension, quota enforcement |
606
602
  | `lib/legion/capacity/model.rb` | Workforce capacity calculation (throughput, utilization, forecast, per-worker) |
607
- | **MCP** | |
608
- | `lib/legion/mcp.rb` | Entry point: `Legion::MCP.server` singleton factory, `server_for(token:)` |
609
- | `lib/legion/mcp/auth.rb` | MCP authentication: JWT + API key verification |
610
- | `lib/legion/mcp/tool_governance.rb` | Risk-tier tool filtering and invocation audit |
611
- | `lib/legion/mcp/server.rb` | MCP::Server builder, TOOL_CLASSES array, governance-aware build |
603
+ | **MCP** (extracted to `legion-mcp` gem) | |
612
604
  | `lib/legion/digital_worker.rb` | DigitalWorker module entry point |
613
605
  | `lib/legion/digital_worker/lifecycle.rb` | Worker state machine |
614
606
  | `lib/legion/digital_worker/registry.rb` | In-process worker registry |
615
607
  | `lib/legion/digital_worker/risk_tier.rb` | AIRB risk tier + governance constraints |
616
608
  | `lib/legion/digital_worker/value_metrics.rb` | Token/cost/latency tracking |
617
- | `lib/legion/mcp/tools/` | 35 MCP::Tool subclasses (incl. rbac_assignments, rbac_check, rbac_grants) |
618
- | `lib/legion/mcp/resources/runner_catalog.rb` | `legion://runners` resource |
619
- | `lib/legion/mcp/resources/extension_info.rb` | `legion://extensions/{name}` resource template |
620
609
  | **CLI v2** | |
621
610
  | `lib/legion/cli.rb` | `Legion::CLI::Main` Thor app, global flags, version, start/stop/status/check |
622
611
  | `lib/legion/cli/output.rb` | `Output::Formatter`: color, tables, JSON mode, ANSI stripping |
@@ -676,16 +665,23 @@ rack-test, rake, rspec, rubocop, rubocop-rspec, simplecov
676
665
  | `lib/legion/cli/version.rb` | CLI version display helper |
677
666
  | `lib/legion/docs/site_generator.rb` | Static documentation site generator |
678
667
  | `lib/legion/cli/memory_command.rb` | `legion memory` subcommands (list, add, forget, search, clear) |
679
- | `lib/legion/cli/plan_command.rb` | `legion plan` — read-only exploration mode with /save to docs/plans/ |
668
+ | `lib/legion/cli/plan_command.rb` | `legion plan` — read-only exploration mode with /save to docs/work/planning/ |
680
669
  | `lib/legion/cli/swarm_command.rb` | `legion swarm` — multi-agent workflow orchestration from `.legion/swarms/` |
681
670
  | `lib/legion/cli/commit_command.rb` | `legion commit` — AI-generated commit messages via LLM |
682
671
  | `lib/legion/cli/pr_command.rb` | `legion pr` — AI-generated PR title + description via LLM |
683
672
  | `lib/legion/cli/review_command.rb` | `legion review` — AI code review with severity levels (CRITICAL/WARNING/SUGGESTION/NOTE) |
684
673
  | `lib/legion/cli/gaia_command.rb` | `legion gaia` subcommands (status) |
674
+ | `lib/legion/cli/llm_command.rb` | `legion llm` subcommands (status) — LLM subsystem status and provider health |
675
+ | `lib/legion/cli/detect_command.rb` | `legion detect scan` — scan environment and recommend extensions |
676
+ | `lib/legion/cli/observe_command.rb` | `legion observe stats` — MCP tool usage statistics from Observer |
677
+ | `lib/legion/cli/tty_command.rb` | `legion tty interactive` — launch rich terminal UI (legion-tty interactive shell) |
678
+ | `lib/legion/cli/interactive.rb` | `Interactive` Thor class — shared CLI module for `legion` binary entry point |
679
+ | `lib/legion/cli/config_import.rb` | `legion config import` — import config from external sources |
685
680
  | `lib/legion/cli/schedule_command.rb` | `legion schedule` subcommands (list, show, add, remove, logs) |
686
681
  | `lib/legion/cli/completion_command.rb` | `legion completion` subcommands (bash, zsh, install) |
687
682
  | `lib/legion/cli/openapi_command.rb` | `legion openapi` subcommands (generate, routes); also `GET /api/openapi.json` endpoint |
688
- | `lib/legion/cli/doctor_command.rb` | `legion doctor` — 10-check environment diagnosis; `Doctor::Result` value object with status/message/prescription/auto_fixable |
683
+ | `lib/legion/cli/doctor_command.rb` | `legion doctor` — 11-check environment diagnosis; `Doctor::Result` value object with status/message/prescription/auto_fixable |
684
+ | `lib/legion/cli/doctor/` | Individual check modules: ruby_version, bundle, config, rabbitmq, database, cache, vault, extensions, pid, permissions, plus result.rb |
689
685
  | `lib/legion/cli/telemetry_command.rb` | `legion telemetry` subcommands (stats, ingest) — session log analytics |
690
686
  | `lib/legion/cli/auth_command.rb` | `legion auth` subcommands (teams) — delegated OAuth browser flow for external services |
691
687
  | `completions/legion.bash` | Bash tab completion script |
@@ -713,8 +709,6 @@ rack-test, rake, rspec, rubocop, rubocop-rspec, simplecov
713
709
  | `API::Routes::Relationships` | Fully implemented (backed by legion-data migration 013) |
714
710
  | `API::Routes::Chains` | 501 stub - no data model |
715
711
  | `API::Middleware::Auth` | JWT Bearer auth middleware — real token validation and API key (`X-API-Key` header) auth both implemented |
716
- | `MCP::Auth` | JWT + API key authentication for MCP server (HTTP transport) |
717
- | `MCP::ToolGovernance` | Risk-tier tool filtering + audit — disabled by default, opt-in via settings |
718
712
  | `legion-data` chains/relationships models | Not yet implemented |
719
713
 
720
714
  ## Rubocop Notes
@@ -728,7 +722,7 @@ rack-test, rake, rspec, rubocop, rubocop-rspec, simplecov
728
722
 
729
723
  ```bash
730
724
  bundle install
731
- bundle exec rspec # 1433 examples, 0 failures
725
+ bundle exec rspec # 1499 examples, 0 failures
732
726
  bundle exec rubocop # 418 files, 0 offenses
733
727
  ```
734
728
 
data/README.md CHANGED
@@ -14,7 +14,7 @@ Schedule tasks, chain services into dependency graphs, run them concurrently via
14
14
  ╰──────────────────────────────────────╯
15
15
  ```
16
16
 
17
- **Ruby >= 3.4** | **v1.4.67** | **Apache-2.0** | [@Esity](https://github.com/Esity)
17
+ **Ruby >= 3.4** | **v1.4.78** | **Apache-2.0** | [@Esity](https://github.com/Esity)
18
18
 
19
19
  ---
20
20
 
@@ -461,11 +461,13 @@ legion start
461
461
  ├── 4. Transport (legion-transport — RabbitMQ)
462
462
  ├── 5. Cache (legion-cache — Redis/Memcached)
463
463
  ├── 6. Data (legion-data — database + migrations)
464
- ├── 7. LLM (legion-llmAI provider setup + routing)
465
- ├── 8. Supervision (process supervision)
466
- ├── 9. Extensions (discover + load 280+ LEX gems, filtered by role profile)
467
- ├── 10. Cluster Secret (distribute via Vault or memory)
468
- └── 11. API (Sinatra/Puma on port 4567)
464
+ ├── 7. RBAC (legion-rbacoptional role-based access control)
465
+ ├── 8. LLM (legion-llm — AI provider setup + routing)
466
+ ├── 9. GAIA (legion-gaia cognitive coordination layer)
467
+ ├── 10. Supervision (process supervision)
468
+ ├── 11. Extensions (discover + load 280+ LEX gems, filtered by role profile)
469
+ ├── 12. Cluster Secret (distribute via Vault or memory)
470
+ └── 13. API (Sinatra/Puma on port 4567)
469
471
  ```
470
472
 
471
473
  Each phase registers with `Legion::Readiness`. All phases are individually toggleable.
@@ -0,0 +1,211 @@
1
+ # Hooks Expansion Design
2
+
3
+ ## Summary
4
+
5
+ Expand the existing hooks system to support GET + POST, extension-derived URL paths, and runner-controlled responses. Removes hardcoded extension routes from LegionIO (starting with `api/oauth.rb`) by letting extensions own their HTTP surface through the existing `hooks/` convention.
6
+
7
+ ## Problem
8
+
9
+ Extensions that need HTTP endpoints (OAuth callbacks, webhooks, status pages) currently require hardcoded routes in LegionIO's `api/` directory. The `api/oauth.rb` file knows about Microsoft Teams specifically. This couples LegionIO to individual extensions and bypasses the Ingress pipeline (no RBAC, no audit, no events).
10
+
11
+ The hooks system already handles inbound webhooks with auto-discovery, verification DSL, and Ingress routing — but it only supports POST and always returns JSON.
12
+
13
+ ## Approach
14
+
15
+ Expand the existing hooks infrastructure. No new module types, no new DSL classes. Three changes:
16
+
17
+ 1. Add GET alongside POST in `api/hooks.rb`
18
+ 2. Add a `mount` class method to `Hooks::Base` for sub-path suffixes
19
+ 3. Add response control so runners can return HTML/redirects instead of JSON
20
+
21
+ ## URL Derivation
22
+
23
+ The full URL is deterministic and non-overridable:
24
+
25
+ ```
26
+ /api/hooks/lex/{extension_name}/{hook_class_name}{mount_suffix}
27
+ fixed from module from class name optional DSL
28
+ ```
29
+
30
+ - `extension_name` — derived from Ruby module hierarchy. `Legion::Extensions::MicrosoftTeams` becomes `microsoft_teams`. Cannot be overridden.
31
+ - `hook_class_name` — derived from the hook class name. `Hooks::Auth` becomes `auth`. Cannot be overridden.
32
+ - `mount_suffix` — optional, declared via `mount '/callback'` in the hook class. Appended after the class name segment.
33
+
34
+ Examples:
35
+
36
+ | Hook class | mount | URL |
37
+ |-----------|-------|-----|
38
+ | `MicrosoftTeams::Hooks::Auth` | `'/callback'` | `/api/hooks/lex/microsoft_teams/auth/callback` |
39
+ | `MicrosoftTeams::Hooks::Webhook` | none | `/api/hooks/lex/microsoft_teams/webhook` |
40
+ | `Github::Hooks::Push` | none | `/api/hooks/lex/github/push` |
41
+ | `Slack::Hooks::Events` | `'/interactive'` | `/api/hooks/lex/slack/events/interactive` |
42
+
43
+ The extension name prefix acts as a namespace fence — extensions can only define routes under their own name. No collisions.
44
+
45
+ ## HTTP Method Support
46
+
47
+ Both GET and POST route to the same handler method. The runner receives a normalized request hash:
48
+
49
+ ```ruby
50
+ {
51
+ http_method: 'GET',
52
+ params: { code: '...', state: '...' },
53
+ headers: { 'HTTP_HOST' => '...' },
54
+ body: nil
55
+ }
56
+ ```
57
+
58
+ For GET requests, `params` comes from query string. For POST, `params` is the parsed body. `body` contains the raw POST body (needed for HMAC verification). `headers` are the Rack-normalized request headers.
59
+
60
+ The API handler:
61
+
62
+ ```ruby
63
+ app.get '/api/hooks/lex/:lex_name/*' do
64
+ handle_hook_request(params, request)
65
+ end
66
+
67
+ app.post '/api/hooks/lex/:lex_name/*' do
68
+ handle_hook_request(params, request)
69
+ end
70
+ ```
71
+
72
+ Both call the same `handle_hook_request` private method that resolves the hook, verifies, and pipes through `Ingress.run`.
73
+
74
+ ## Response Control
75
+
76
+ If the runner result hash contains a `:response` key, the API layer renders it directly. Otherwise, the default JSON task response.
77
+
78
+ ```ruby
79
+ # Runner returning a custom response (OAuth callback):
80
+ def auth_callback(code:, state:, **)
81
+ # ... token exchange logic ...
82
+ {
83
+ result: { authenticated: true },
84
+ response: {
85
+ status: 200,
86
+ content_type: 'text/html',
87
+ body: '<html><body><h2>Authentication complete</h2></body></html>'
88
+ }
89
+ }
90
+ end
91
+ ```
92
+
93
+ API handler logic:
94
+
95
+ ```ruby
96
+ result = Ingress.run(...)
97
+ if result[:response]
98
+ status result[:response][:status] || 200
99
+ content_type result[:response][:content_type] || 'application/json'
100
+ result[:response][:body]
101
+ else
102
+ json_response({ task_id: result[:task_id], status: result[:status] })
103
+ end
104
+ ```
105
+
106
+ The `result` key alongside `response` means the task system still captures the outcome for audit/logging even when the HTTP response is HTML. If `:response` is absent, behavior is identical to today.
107
+
108
+ ## Hooks::Base Changes
109
+
110
+ One new class method:
111
+
112
+ ```ruby
113
+ class Base
114
+ class << self
115
+ def mount(path)
116
+ @mount_path = path
117
+ end
118
+
119
+ attr_reader :mount_path
120
+ end
121
+ end
122
+ ```
123
+
124
+ Existing DSL unchanged: `route_header`, `route_field`, `verify_hmac`, `verify_token` all still work. They operate on the request after URL routing, same as today.
125
+
126
+ For hooks that handle both GET callbacks and POST webhooks on the same path, the existing `route` method can inspect the HTTP method from the payload to decide which runner function to call. Or the runner can handle both in a single method.
127
+
128
+ ## Builder Changes
129
+
130
+ `builders/hooks.rb` `build_hook_list` currently registers hooks keyed by `"lex_name/hook_name"`. Changes:
131
+
132
+ - Read `hook_class.mount_path` (nil if not declared)
133
+ - Build the full route path: `"{extension_name}/{hook_name}{mount_path}"`
134
+ - Store the full route path in the registry entry
135
+
136
+ `find_hook` changes to match against the request splat path instead of discrete lex_name/hook_name params.
137
+
138
+ ## Hook Registry
139
+
140
+ Current registry on `Legion::API`:
141
+
142
+ ```ruby
143
+ register_hook(lex_name:, hook_name:, hook_class:, default_runner:)
144
+ ```
145
+
146
+ Add `route_path:` to the registration:
147
+
148
+ ```ruby
149
+ register_hook(lex_name:, hook_name:, hook_class:, default_runner:, route_path:)
150
+ ```
151
+
152
+ `find_hook` changes from two-param lookup to splat-path matching:
153
+
154
+ ```ruby
155
+ def find_hook_by_path(path)
156
+ hook_registry.values.find { |h| h[:route_path] == path }
157
+ end
158
+ ```
159
+
160
+ ## Backward Compatibility
161
+
162
+ - Hooks without `mount` work exactly as before — filename becomes the hook name, URL is `/api/hooks/lex/{ext}/{hook_name}`
163
+ - Old `POST /api/hooks/:lex_name/:hook_name` route stays as deprecated alias pointing to the new handler
164
+ - All existing `Hooks::Base` DSL works unchanged
165
+ - Extensions that don't define hooks are unaffected
166
+
167
+ ## Migration: api/oauth.rb
168
+
169
+ The hardcoded Microsoft Teams OAuth callback moves to lex-microsoft_teams:
170
+
171
+ **New file:** `lex-microsoft_teams/hooks/auth.rb`
172
+
173
+ ```ruby
174
+ class Auth < Legion::Extensions::Hooks::Base
175
+ mount '/callback'
176
+ end
177
+ ```
178
+
179
+ **Runner method** in lex-microsoft_teams handles the callback: receives `code` and `state` params, emits the event, returns HTML response.
180
+
181
+ **LegionIO:** Remove `require_relative 'api/oauth'` and `register Routes::OAuth` from `api.rb`. Delete or gut `api/oauth.rb`.
182
+
183
+ ## Testing
184
+
185
+ ### LegionIO Specs
186
+
187
+ - Hooks::Base `mount` sets and reads mount_path
188
+ - Builder reads mount_path, builds correct route_path
189
+ - API handler resolves hook from splat path (GET and POST)
190
+ - API handler renders `:response` when present in runner result
191
+ - API handler returns default JSON when `:response` absent
192
+ - Backward compat: old `/api/hooks/:lex_name/:hook_name` still works
193
+ - Verification (HMAC, token) works on both GET and POST
194
+
195
+ ### lex-microsoft_teams Specs
196
+
197
+ - Hook class discovered by builder
198
+ - OAuth callback runner handles code+state, returns HTML response
199
+ - Events emitted on successful callback
200
+
201
+ ## Files Changed
202
+
203
+ | File | Repo | Change |
204
+ |------|------|--------|
205
+ | `extensions/hooks/base.rb` | LegionIO | Add `mount` class method |
206
+ | `extensions/builders/hooks.rb` | LegionIO | Read mount_path, build full route_path |
207
+ | `api/hooks.rb` | LegionIO | Add GET route, splat matching, `handle_hook_request`, response control |
208
+ | `api.rb` | LegionIO | Remove `Routes::OAuth`, add backward compat alias |
209
+ | `api/oauth.rb` | LegionIO | Delete |
210
+ | `hooks/auth.rb` | lex-microsoft_teams | New file |
211
+ | Runner (TBD) | lex-microsoft_teams | OAuth callback handler method |
data/legionio.gemspec CHANGED
@@ -34,7 +34,7 @@ Gem::Specification.new do |spec|
34
34
  spec.bindir = 'exe'
35
35
  spec.executables = %w[legion legionio]
36
36
 
37
- spec.add_dependency 'mcp', '~> 0.8'
37
+ spec.add_dependency 'legion-mcp'
38
38
 
39
39
  spec.add_dependency 'bootsnap', '>= 1.18'
40
40
  spec.add_dependency 'concurrent-ruby', '>= 1.2'
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ class API < Sinatra::Base
5
+ module Routes
6
+ module ExtensionCatalog
7
+ def self.registered(app)
8
+ app.get '/api/catalog' do
9
+ entries = Legion::Extensions::Catalog.all.map do |name, entry|
10
+ build_catalog_manifest(name, entry)
11
+ end
12
+ json_response(entries)
13
+ end
14
+
15
+ app.get '/api/catalog/:name' do
16
+ name = params[:name]
17
+ entry = Legion::Extensions::Catalog.entry(name)
18
+ unless entry
19
+ halt 404, { 'Content-Type' => 'application/json' },
20
+ Legion::JSON.dump({ error: { code: 'not_found', message: "Extension #{name} not found" } })
21
+ end
22
+
23
+ json_response(build_catalog_manifest(name, entry))
24
+ end
25
+ end
26
+ end
27
+ end
28
+
29
+ helpers do # rubocop:disable Metrics/BlockLength
30
+ def build_catalog_manifest(name, entry)
31
+ {
32
+ name: name,
33
+ state: entry[:state].to_s,
34
+ started_at: entry[:started_at]&.iso8601,
35
+ permissions: build_catalog_permissions(name),
36
+ runners: build_catalog_runners(name),
37
+ known_intents: build_catalog_known_intents(name)
38
+ }
39
+ end
40
+
41
+ def build_catalog_permissions(name)
42
+ declared = Legion::Extensions::Permissions.declared_paths(name)
43
+ {
44
+ sandbox: Legion::Extensions::Permissions.sandbox_path(name),
45
+ read_paths: declared[:read_paths],
46
+ write_paths: declared[:write_paths]
47
+ }
48
+ rescue StandardError
49
+ { sandbox: Legion::Extensions::Permissions.sandbox_path(name), read_paths: [], write_paths: [] }
50
+ end
51
+
52
+ def build_catalog_runners(name)
53
+ return {} unless defined?(Legion::Data) && Legion::Data.respond_to?(:connected?) && Legion::Data.connected?
54
+
55
+ ext = Legion::Data::Model::Extension.where(gem_name: name).first
56
+ return {} unless ext
57
+
58
+ ext.runners.to_h do |runner|
59
+ [runner.values[:name], {
60
+ methods: runner.functions.map { |f| f.values[:name] },
61
+ description: runner.values[:description]
62
+ }]
63
+ end
64
+ rescue StandardError
65
+ {}
66
+ end
67
+
68
+ def build_catalog_known_intents(name)
69
+ return [] unless defined?(Legion::MCP::PatternStore)
70
+
71
+ matched = Legion::MCP::PatternStore.patterns.select do |_hash, pattern|
72
+ pattern[:tool_chain]&.any? { |t| t.start_with?(name) }
73
+ end
74
+ matched.map do |_hash, pattern|
75
+ { intent: pattern[:intent_text], tool_chain: pattern[:tool_chain], confidence: pattern[:confidence] }
76
+ end
77
+ rescue StandardError
78
+ []
79
+ end
80
+ end
81
+ end
82
+ end