legionio 1.4.14 → 1.4.43

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 (66) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +2 -0
  3. data/CHANGELOG.md +230 -0
  4. data/CLAUDE.md +20 -5
  5. data/README.md +13 -1
  6. data/lib/legion/alerts.rb +120 -0
  7. data/lib/legion/api/audit.rb +32 -0
  8. data/lib/legion/api/auth.rb +82 -0
  9. data/lib/legion/api/gaia.rb +22 -0
  10. data/lib/legion/api/metrics.rb +22 -0
  11. data/lib/legion/api/middleware/api_version.rb +42 -0
  12. data/lib/legion/api/middleware/auth.rb +1 -1
  13. data/lib/legion/api/middleware/body_limit.rb +31 -0
  14. data/lib/legion/api/middleware/rate_limit.rb +167 -0
  15. data/lib/legion/api/validators.rb +44 -0
  16. data/lib/legion/api/webhooks.rb +29 -0
  17. data/lib/legion/api/workers.rb +22 -1
  18. data/lib/legion/api.rb +10 -0
  19. data/lib/legion/audit/hash_chain.rb +32 -0
  20. data/lib/legion/audit/siem_export.rb +33 -0
  21. data/lib/legion/audit.rb +89 -0
  22. data/lib/legion/catalog.rb +47 -0
  23. data/lib/legion/chat/notification_bridge.rb +82 -0
  24. data/lib/legion/chat/notification_queue.rb +43 -0
  25. data/lib/legion/chat/skills.rb +49 -0
  26. data/lib/legion/cli/audit_command.rb +66 -0
  27. data/lib/legion/cli/chat/progress_bar.rb +55 -0
  28. data/lib/legion/cli/chat/team.rb +43 -0
  29. data/lib/legion/cli/chat_command.rb +43 -1
  30. data/lib/legion/cli/config_scaffold.rb +90 -2
  31. data/lib/legion/cli/cost/data_client.rb +53 -0
  32. data/lib/legion/cli/cost_command.rb +76 -0
  33. data/lib/legion/cli/init/config_generator.rb +55 -0
  34. data/lib/legion/cli/init/environment_detector.rb +65 -0
  35. data/lib/legion/cli/init_command.rb +58 -0
  36. data/lib/legion/cli/lex_templates.rb +64 -0
  37. data/lib/legion/cli/marketplace_command.rb +79 -0
  38. data/lib/legion/cli/mcp_command.rb +12 -1
  39. data/lib/legion/cli/notebook_command.rb +69 -0
  40. data/lib/legion/cli/skill_command.rb +86 -0
  41. data/lib/legion/cli/templates/core.json.erb +14 -0
  42. data/lib/legion/cli/update_command.rb +134 -0
  43. data/lib/legion/cli/worker_command.rb +77 -0
  44. data/lib/legion/cli.rb +28 -0
  45. data/lib/legion/context.rb +53 -0
  46. data/lib/legion/digital_worker/lifecycle.rb +17 -0
  47. data/lib/legion/digital_worker/registry.rb +12 -0
  48. data/lib/legion/digital_worker.rb +6 -0
  49. data/lib/legion/docs/site_generator.rb +48 -0
  50. data/lib/legion/guardrails.rb +50 -0
  51. data/lib/legion/ingress.rb +40 -0
  52. data/lib/legion/isolation.rb +49 -0
  53. data/lib/legion/mcp/auth.rb +50 -0
  54. data/lib/legion/mcp/server.rb +8 -2
  55. data/lib/legion/mcp/tool_governance.rb +77 -0
  56. data/lib/legion/mcp.rb +9 -0
  57. data/lib/legion/metrics.rb +117 -0
  58. data/lib/legion/registry/security_scanner.rb +48 -0
  59. data/lib/legion/registry.rb +72 -0
  60. data/lib/legion/runner.rb +21 -1
  61. data/lib/legion/sandbox.rb +65 -0
  62. data/lib/legion/service.rb +75 -0
  63. data/lib/legion/telemetry.rb +65 -0
  64. data/lib/legion/version.rb +1 -1
  65. data/lib/legion/webhooks.rb +124 -0
  66. metadata +43 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '0927bca374a6df75ab9a1e75067219af7205291a694defff2fa356fe2b330bf3'
4
- data.tar.gz: 1c3171e36c3447a104889d556df53596e611b4965d60abb08d4e91fe5c50a996
3
+ metadata.gz: 51e90ef95b551c27c044e9f9dac0d784a529d4947835ec0d6501276f73281fb9
4
+ data.tar.gz: 533c1b9813327e486e33cb2c1d657907248517deb15935c68616a095be699280
5
5
  SHA512:
6
- metadata.gz: 4bba3556ee1380f6b61aaa14263dbcccb4340428237f0203cbed9d7090d1cf95e811109b75ebf14e1a1bd3767640ad805e2dee4b4d5711352aa7becd528bf259
7
- data.tar.gz: 243d79f20efa83d65b41a6e39f3953aee765369f37aa30caa6b85d16cb0baeab95e46628a443c45b082f233925c3ba413734c5e8aa45146c3827e9f367597ae7
6
+ metadata.gz: 298d051c13e5e658eb572971863a7039d3c08bd168b40b2e6806a3139ccae7261ff29c916bb84ab34fd2fea6c2e6905615f8689e80f5a415feafe75cbf0f56c5
7
+ data.tar.gz: 1f0005c08f21b456e5d64923020e662efbe88a714514ad7bd27b59caa0efc05676551f8b87a150d0226aa1bc9237681d0688bb6b42f1430624ea5ba81e420903
data/.rubocop.yml CHANGED
@@ -35,6 +35,8 @@ Metrics/BlockLength:
35
35
  - 'lib/legion/cli/swarm_command.rb'
36
36
  - 'lib/legion/cli/gaia_command.rb'
37
37
  - 'lib/legion/cli/schedule_command.rb'
38
+ - 'lib/legion/cli/update_command.rb'
39
+ - 'lib/legion/api/auth.rb'
38
40
 
39
41
  Metrics/AbcSize:
40
42
  Max: 60
data/CHANGELOG.md CHANGED
@@ -1,9 +1,239 @@
1
1
  # Legion Changelog
2
2
 
3
+ ## [1.4.43] - 2026-03-17
4
+
5
+ ### Fixed
6
+ - Auth token exchange route used `Legion::Settings.dig` which doesn't exist — replaced with bracket access
7
+ - Auth spec required `legion/rbac` gem directly — replaced with inline stub for standalone test execution
8
+
9
+ ## [1.4.42] - 2026-03-17
10
+
11
+ ### Added
12
+ - `POST /api/auth/token`: Entra ID token exchange endpoint (validates external JWT via JWKS, maps claims via EntraClaimsMapper, issues Legion token)
13
+
14
+ ## [1.4.41] - 2026-03-17
15
+
16
+ ### Added
17
+ - `Legion::CLI::LexTemplates`: extension template registry (basic, llm-agent, service-integration, scheduled-task, webhook-handler)
18
+ - `Legion::Docs::SiteGenerator`: documentation site generation from existing markdown files
19
+
20
+ ## [1.4.40] - 2026-03-17
21
+
22
+ ### Added
23
+ - `Legion::Guardrails`: embedding similarity and RAG relevancy safety checks
24
+ - `Legion::Context`: session/user tracking with thread-local `SessionContext`
25
+ - `Legion::Catalog`: AI catalog registration for MCP tools and workers
26
+
27
+ ## [1.4.39] - 2026-03-17
28
+
29
+ ### Added
30
+ - `Legion::Webhooks`: outbound webhook dispatcher with HMAC-SHA256 signing
31
+ - Webhook registration, delivery tracking, and dead letter queue
32
+ - API routes: `GET/POST/DELETE /api/webhooks`
33
+
34
+ ## [1.4.38] - 2026-03-17
35
+
36
+ ### Added
37
+ - `Legion::Isolation`: per-agent data and tool access enforcement with thread-local context
38
+ - `Isolation::Context`: tool allowlist, data filter, and risk tier per agent
39
+
40
+ ## [1.4.37] - 2026-03-17
41
+
42
+ ### Added
43
+ - `POST /api/channels/teams/webhook`: Bot Framework activity delivery to GAIA sensory buffer
44
+
45
+ ## [1.4.36] - 2026-03-17
46
+
47
+ ### Added
48
+ - `Audit::HashChain`: SHA-256 hash chain for tamper-evident audit records
49
+ - `Audit::SiemExport`: SIEM-compatible JSON and NDJSON export with integrity metadata
50
+ - `Audit::HashChain.verify_chain` validates hash chain between records
51
+
52
+ ## [1.4.35] - 2026-03-17
53
+
54
+ ### Added
55
+ - `Chat::Team`: multi-user context tracking with thread-local user, env detection
56
+ - `Chat::ProgressBar`: progress indicator for long-running operations with ETA
57
+ - `legion notebook read/export`: Jupyter notebook reading and export (markdown/script)
58
+
59
+ ## [1.4.34] - 2026-03-17
60
+
61
+ ### Added
62
+ - `Legion::Registry`: central extension metadata store with search, risk tier filtering, AIRB status
63
+ - `Legion::Sandbox`: capability-based extension sandboxing with enforcement toggle
64
+ - `Legion::Registry::SecurityScanner`: naming convention, checksum, and gemspec metadata validation
65
+ - `legion marketplace`: CLI for search, info, list, scan operations
66
+
67
+ ## [1.4.33] - 2026-03-17
68
+
69
+ ### Added
70
+ - `legion cost summary`: overall cost summary (today/week/month)
71
+ - `legion cost worker <id>`: per-worker cost breakdown
72
+ - `legion cost top`: top cost consumers ranked by spend
73
+ - `legion cost export`: export cost data as JSON or CSV
74
+ - `Legion::CLI::CostData::Client`: API client for cost data retrieval
75
+
76
+ ### Fixed
77
+ - `Connection.resolve_config_dir` spec now correctly stubs `~/.legionio/settings` path
78
+
79
+ ## [1.4.32] - 2026-03-17
80
+
81
+ ### Fixed
82
+ - `NotificationBridge` missing `require_relative 'notification_queue'` causing `NameError` on `legion chat`
83
+
84
+ ## [1.4.31] - 2026-03-16
85
+
86
+ ### Added
87
+ - Skills system: `.legion/skills/` and `~/.legionio/skills/` YAML frontmatter markdown files
88
+ - `Legion::Chat::Skills`: discovery, parsing, and find for skill files
89
+ - `/skill-name` invocation in chat resolves user-defined skills
90
+ - `legion skill list`, `legion skill show`, `legion skill create`, `legion skill run` CLI subcommands
91
+
92
+ ## [1.4.30] - 2026-03-16
93
+
94
+ ### Added
95
+ - `MCP::Auth`: token-based MCP authentication (JWT + API key)
96
+ - `MCP::ToolGovernance`: risk-tier-aware tool filtering and invocation audit
97
+ - `MCP.server_for(token:)` builds identity-scoped MCP server instances
98
+ - HTTP transport auth: Bearer token validation with 401 response on failure
99
+ - MCP settings: `mcp.auth.enabled`, `mcp.auth.allowed_api_keys`, `mcp.governance.enabled`, `mcp.governance.tool_risk_tiers`
100
+
101
+ ## [1.4.29] - 2026-03-16
102
+
103
+ ### Added
104
+ - `legion init`: one-command workspace setup with environment detection
105
+ - `InitHelpers::EnvironmentDetector`: checks for RabbitMQ, database, Vault, Redis, git, existing config
106
+ - `InitHelpers::ConfigGenerator`: ERB template-based config generation, `.legion/` workspace scaffolding
107
+ - `--local` flag for zero-dependency development mode
108
+ - `--force` flag to overwrite existing config files
109
+
110
+ ## [1.4.28] - 2026-03-16
111
+
112
+ ### Added
113
+ - `Legion::Telemetry` module: opt-in OpenTelemetry tracing with `with_span` wrapper
114
+ - `setup_telemetry` in Service: initializes OTel SDK with OTLP exporter when `telemetry.enabled: true`
115
+ - `sanitize_attributes` helper for safe OTel attribute conversion
116
+ - `record_exception` helper for span error recording
117
+
118
+ ## [1.4.27] - 2026-03-16
119
+
120
+ ### Added
121
+ - `legion update` CLI command: updates all Legion gems (`legionio`, `legion-*`, `lex-*`) using the current Ruby's gem binary
122
+ - `--dry-run` flag to check available updates without installing
123
+ - `--json` flag for machine-readable output
124
+ - Updates install into the running Ruby's GEM_HOME (safe for Homebrew bundled installs)
125
+
126
+ ## [1.4.26] - 2026-03-16
127
+
128
+ ### Added
129
+ - `Legion::Metrics` module: opt-in Prometheus metrics via `prometheus-client` gem
130
+ - `GET /metrics` endpoint returning Prometheus text-format output
131
+ - 9 metrics: uptime, active_workers, tasks_total, tasks_per_second, error_rate, consent_violations, llm_requests, llm_tokens
132
+ - Event-driven counters + pull-based gauge refresh on scrape
133
+ - `/metrics` added to Auth middleware SKIP_PATHS
134
+ - Wired into Service startup and shutdown
135
+
136
+ ## [1.4.25] - 2026-03-16
137
+
138
+ ### Added
139
+ - `Legion::Chat::NotificationQueue`: thread-safe priority queue for background notifications
140
+ - `Legion::Chat::NotificationBridge`: event-driven bridge matching Legion events to chat notifications
141
+ - Chat REPL displays pending notifications before each prompt (critical in red, info in yellow)
142
+ - Configurable notification patterns via `chat.notifications.patterns` setting
143
+
144
+ ## [1.4.24] - 2026-03-16
145
+
146
+ ### Added
147
+ - `Legion::Audit.recent_for` — query audit records by principal and time window
148
+ - `Legion::Audit.count_for` — count audit records by principal and time window
149
+ - `Legion::Audit.failure_count_for` / `success_count_for` — convenience wrappers
150
+ - `Legion::Audit.resources_for` — distinct resources invoked by a principal
151
+ - `Legion::Audit.recent` — most recent N records with optional filters
152
+ - All query methods return safe defaults (`[]` or `0`) when legion-data is unavailable
153
+
154
+ ## [1.4.23] - 2026-03-16
155
+
156
+ ### Added
157
+ - `Middleware::BodyLimit`: request body size limit (1MB max, returns 413)
158
+ - `API::Validators` helper module: `validate_required!`, `validate_string_length!`, `validate_enum!`, `validate_uuid!`, `validate_integer!`
159
+ - Ingress payload validation: 512KB size limit, runner_class/function format checks
160
+
161
+ ### Security
162
+ - Ingress validates runner_class format before `Kernel.const_get` to prevent arbitrary constant resolution
163
+ - Ingress validates function format before `.send` to prevent method injection
164
+
165
+ ## [1.4.22] - 2026-03-16
166
+
167
+ ### Added
168
+ - `Legion::Alerts`: configurable alerting rules engine with event pattern matching
169
+ - `Alerts::Engine`: count-based conditions, cooldown deduplication, multi-channel dispatch
170
+ - 4 default rules: consent_violation, extinction_trigger, error_spike, budget_exceeded
171
+ - Channel dispatch: events (via `Legion::Events`), log (via `Legion::Logging`), webhook
172
+ - Settings: `alerts.enabled`, `alerts.rules`
173
+ - Wired into `Service` startup (opt-in via `alerts.enabled: true`)
174
+
175
+ ## [1.4.21] - 2026-03-16
176
+
177
+ ### Added
178
+ - `Middleware::ApiVersion`: rewrites `/api/v1/` paths to `/api/` for future versioned API support
179
+ - Deprecation headers (`Deprecation`, `Sunset`, `Link`) on unversioned `/api/` paths
180
+ - `X-API-Version` request header set for versioned paths
181
+ - Skip paths: `/api/health`, `/api/ready`, `/api/openapi.json`, `/metrics`
182
+
183
+ ## [1.4.20] - 2026-03-16
184
+
185
+ ### Added
186
+ - `Middleware::RateLimit`: sliding-window rate limiting with per-IP, per-agent, per-tenant tiers
187
+ - In-memory store (default) with lazy reap; distributed store via `Legion::Cache` when available
188
+ - Standard headers: `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`, `Retry-After` (429 only)
189
+ - Skip paths: `/api/health`, `/api/ready`, `/api/metrics`, `/api/openapi.json`
190
+
191
+ ## [1.4.19] - 2026-03-16
192
+
193
+ ### Added
194
+ - Local development mode: `LEGION_LOCAL=true` env var or `local_mode: true` in settings
195
+ - Auto-configures in-memory transport, mock Vault, and dev settings
196
+
197
+ ## [1.4.18] - 2026-03-16
198
+
199
+ ### Added
200
+ - `legion config scaffold` auto-detects environment variables and enables providers
201
+ - Detects: AWS_BEARER_TOKEN_BEDROCK, ANTHROPIC_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY, VAULT_TOKEN, RABBITMQ_USER/PASSWORD
202
+ - Detects running Ollama on localhost:11434
203
+ - First detected LLM provider becomes the default; credentials use `env://` references
204
+ - JSON output includes `detected` array for automation
205
+
206
+ ## [1.4.17] - 2026-03-16
207
+
208
+ ### Added
209
+ - `Legion::Audit` publisher module for immutable audit logging via AMQP
210
+ - Audit hook in `Runner.run` records every runner execution (event_type, duration, status)
211
+ - Audit hook in `DigitalWorker::Lifecycle.transition!` records state transitions
212
+ - `GET /api/audit` endpoint with filters (event_type, principal_id, source, status, since, until)
213
+ - `GET /api/audit/verify` endpoint for hash chain integrity verification
214
+ - `legion audit list` and `legion audit verify` CLI commands
215
+ - Silent degradation: audit never interferes with normal operation (triple guard + rescue)
216
+
217
+ ## [1.4.16] - 2026-03-16
218
+
219
+ ### Added
220
+ - `legion worker create NAME` CLI command: provisions digital worker in bootstrap state with DB record + optional Vault secret storage
221
+
222
+ ## [1.4.15] - 2026-03-16
223
+
224
+ ### Added
225
+ - RAI invariant #2: Ingress.run calls Registry.validate_execution! when worker_id is present
226
+ - Unregistered or inactive workers are blocked with structured error (no exception propagation)
227
+ - Registration check fires before RBAC authorization (registration precedes permission)
228
+
3
229
  ## [1.4.14] - 2026-03-16
4
230
 
5
231
  ### Added
6
232
  - Optional RBAC integration via legion-rbac gem (`if defined?(Legion::Rbac)` guards)
233
+ - `GET /api/workers/:id/health` endpoint returns worker health status with node metrics
234
+ - `health_status` query filter on `GET /api/workers`
235
+ - Thread-safe local worker tracking in `DigitalWorker::Registry` for heartbeat reporting
236
+ - `Legion::DigitalWorker.active_local_ids` delegate method
7
237
  - `setup_rbac` lifecycle hook in Service (after setup_data)
8
238
  - `authorize_execution!` guard in Ingress for task execution
9
239
  - Rack middleware registration in API when legion-rbac loaded
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.13
12
+ **Version**: 1.4.36
13
13
  **License**: Apache-2.0
14
14
  **Docker**: `legionio/legion`
15
15
  **Ruby**: >= 3.4
@@ -274,6 +274,7 @@ legion
274
274
  worker
275
275
  list [-s status] [-t risk_tier]
276
276
  show <id>
277
+ create <name> --entra_app_id ID --owner_msid EMAIL --extension NAME [--team T] [--client_secret S]
277
278
  pause <id>
278
279
  activate <id>
279
280
  retire <id>
@@ -468,10 +469,22 @@ rack-test, rake, rspec, rubocop, rubocop-rspec, simplecov
468
469
  | `lib/legion/api/token.rb` | Token: JWT token issuance endpoint |
469
470
  | `lib/legion/api/openapi.rb` | OpenAPI: `Legion::API::OpenAPI.spec` / `.to_json`; also served at `GET /api/openapi.json` |
470
471
  | `lib/legion/api/oauth.rb` | OAuth: `GET /api/oauth/microsoft_teams/callback` — receives delegated OAuth redirect and stores tokens |
472
+ | `lib/legion/audit.rb` | Audit logging: AMQP publish + query layer (recent_for, count_for, resources_for, recent) backed by AuditLog model |
473
+ | `lib/legion/alerts.rb` | Configurable alerting rules engine: pattern matching, count conditions, cooldown dedup |
474
+ | `lib/legion/telemetry.rb` | Opt-in OpenTelemetry tracing: `with_span` wrapper, `sanitize_attributes`, `record_exception` |
475
+ | `lib/legion/metrics.rb` | Opt-in Prometheus metrics: event-driven counters, pull-based gauges, `prometheus-client` guarded |
476
+ | `lib/legion/api/metrics.rb` | `GET /metrics` Prometheus text-format endpoint with gauge refresh |
477
+ | `lib/legion/chat/notification_queue.rb` | Thread-safe priority queue for background notifications (critical/info/debug) |
478
+ | `lib/legion/chat/notification_bridge.rb` | Event-driven bridge: matches Legion events to chat notifications via fnmatch patterns |
471
479
  | `lib/legion/api/middleware/auth.rb` | Auth: JWT Bearer auth middleware (real token validation, skip paths for health/ready) |
480
+ | `lib/legion/api/middleware/api_version.rb` | ApiVersion: rewrites `/api/v1/` to `/api/`, adds Deprecation/Sunset headers on unversioned paths |
481
+ | `lib/legion/api/middleware/body_limit.rb` | BodyLimit: request body size limit (1MB max, returns 413) |
482
+ | `lib/legion/api/middleware/rate_limit.rb` | RateLimit: sliding-window rate limiting with per-IP/agent/tenant tiers |
472
483
  | **MCP** | |
473
- | `lib/legion/mcp.rb` | Entry point: `Legion::MCP.server` singleton factory |
474
- | `lib/legion/mcp/server.rb` | MCP::Server builder, TOOL_CLASSES array, instructions |
484
+ | `lib/legion/mcp.rb` | Entry point: `Legion::MCP.server` singleton factory, `server_for(token:)` |
485
+ | `lib/legion/mcp/auth.rb` | MCP authentication: JWT + API key verification |
486
+ | `lib/legion/mcp/tool_governance.rb` | Risk-tier tool filtering and invocation audit |
487
+ | `lib/legion/mcp/server.rb` | MCP::Server builder, TOOL_CLASSES array, governance-aware build |
475
488
  | `lib/legion/digital_worker.rb` | DigitalWorker module entry point |
476
489
  | `lib/legion/digital_worker/lifecycle.rb` | Worker state machine |
477
490
  | `lib/legion/digital_worker/registry.rb` | In-process worker registry |
@@ -495,7 +508,7 @@ rack-test, rake, rspec, rubocop, rubocop-rspec, simplecov
495
508
  | `lib/legion/cli/config_scaffold.rb` | `legion config scaffold` — generates starter JSON config files per subsystem |
496
509
  | `lib/legion/cli/generate_command.rb` | `legion generate` subcommands (runner, actor, exchange, queue, message) |
497
510
  | `lib/legion/cli/mcp_command.rb` | `legion mcp` subcommand (stdio + HTTP transports) |
498
- | `lib/legion/cli/worker_command.rb` | `legion worker` subcommands (list, show, pause, retire, terminate, activate, costs) |
511
+ | `lib/legion/cli/worker_command.rb` | `legion worker` subcommands (list, show, create, pause, retire, terminate, activate, costs) |
499
512
  | `lib/legion/cli/coldstart_command.rb` | `legion coldstart` subcommands (ingest, preview, status) |
500
513
  | `lib/legion/cli/chat_command.rb` | `legion chat` — interactive AI REPL + headless prompt mode |
501
514
  | `lib/legion/cli/chat/session.rb` | Chat session: multi-turn conversation, streaming, tool use |
@@ -553,6 +566,8 @@ rack-test, rake, rspec, rubocop, rubocop-rspec, simplecov
553
566
  | `API::Routes::Relationships` | Fully implemented (backed by legion-data migration 013) |
554
567
  | `API::Routes::Chains` | 501 stub - no data model |
555
568
  | `API::Middleware::Auth` | JWT Bearer auth middleware — real token validation and API key (`X-API-Key` header) auth both implemented |
569
+ | `MCP::Auth` | JWT + API key authentication for MCP server (HTTP transport) |
570
+ | `MCP::ToolGovernance` | Risk-tier tool filtering + audit — disabled by default, opt-in via settings |
556
571
  | `legion-data` chains/relationships models | Not yet implemented |
557
572
 
558
573
  ## Rubocop Notes
@@ -566,7 +581,7 @@ rack-test, rake, rspec, rubocop, rubocop-rspec, simplecov
566
581
 
567
582
  ```bash
568
583
  bundle install
569
- bundle exec rspec # 880 examples, 0 failures
584
+ bundle exec rspec # 1088 examples, 0 failures
570
585
  bundle exec rubocop # 0 offenses
571
586
  ```
572
587
 
data/README.md CHANGED
@@ -176,6 +176,7 @@ AI-as-labor with governance, risk tiers, and cost tracking:
176
176
  ```bash
177
177
  legion worker list # list workers
178
178
  legion worker show <id> # worker detail
179
+ legion worker create <name> # register new worker (bootstrap state)
179
180
  legion worker pause <id> # pause / activate / retire
180
181
  legion worker costs --days 30 # cost report
181
182
  ```
@@ -205,9 +206,11 @@ legion schedule list
205
206
  ```bash
206
207
  legion config show # resolved config (redacted)
207
208
  legion config validate # verify settings + subsystem health
208
- legion config scaffold # generate starter config files
209
+ legion config scaffold # generate starter config files (auto-detects env vars)
209
210
  ```
210
211
 
212
+ `config scaffold` auto-detects environment variables (`ANTHROPIC_API_KEY`, `AWS_BEARER_TOKEN_BEDROCK`, `OPENAI_API_KEY`, `GEMINI_API_KEY`, `VAULT_TOKEN`, `RABBITMQ_USER`/`PASSWORD`) and a running Ollama instance, enabling providers and setting `env://` references automatically.
213
+
211
214
  Settings load from the first directory found: `/etc/legionio/` → `~/legionio/` → `./settings/`
212
215
 
213
216
  ### Diagnostics
@@ -220,6 +223,15 @@ legion doctor --json # machine-readable output
220
223
 
221
224
  Checks Ruby version, bundle status, config files, RabbitMQ, database, cache, Vault, extensions, PID files, and permissions. Exits 1 if any check fails.
222
225
 
226
+ ### Updating
227
+
228
+ ```bash
229
+ legion update # update all legion gems in-place
230
+ legion update --dry-run # check what's available without installing
231
+ ```
232
+
233
+ Uses the same Ruby that `legion` is running from — safe for Homebrew installs (updates go into the bundled gem directory, not your system Ruby).
234
+
223
235
  All commands support `--json` for structured output and `--no-color` to strip ANSI codes.
224
236
 
225
237
  ## REST API
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Alerts
5
+ AlertRule = Struct.new(:name, :event_pattern, :condition, :severity, :channels, :cooldown_seconds)
6
+
7
+ DEFAULT_RULES = [
8
+ { name: 'consent_violation', event_pattern: 'governance.consent_violation', severity: 'critical',
9
+ channels: %w[events log], cooldown_seconds: 300 },
10
+ { name: 'extinction_trigger', event_pattern: 'extinction.*', severity: 'critical',
11
+ channels: %w[events log], cooldown_seconds: 0 },
12
+ { name: 'error_spike', event_pattern: 'runner.failure',
13
+ condition: { count_threshold: 10, window_seconds: 60 }, severity: 'warning',
14
+ channels: %w[events log], cooldown_seconds: 300 },
15
+ { name: 'budget_exceeded', event_pattern: 'finops.budget_exceeded', severity: 'warning',
16
+ channels: %w[events log], cooldown_seconds: 3600 }
17
+ ].freeze
18
+
19
+ class Engine
20
+ attr_reader :rules
21
+
22
+ def initialize(rules: [])
23
+ @rules = rules.map { |r| r.is_a?(AlertRule) ? r : AlertRule.new(**r.transform_keys(&:to_sym)) }
24
+ @counters = {}
25
+ @last_fired = {}
26
+ end
27
+
28
+ def evaluate(event_name, payload = {})
29
+ fired = []
30
+ @rules.each do |rule|
31
+ next unless event_matches?(event_name, rule.event_pattern)
32
+ next unless condition_met?(rule, event_name)
33
+ next if in_cooldown?(rule)
34
+
35
+ fire_alert(rule, event_name, payload)
36
+ fired << rule.name
37
+ end
38
+ fired
39
+ end
40
+
41
+ private
42
+
43
+ def event_matches?(name, pattern)
44
+ File.fnmatch?(pattern, name)
45
+ end
46
+
47
+ def condition_met?(rule, event_name)
48
+ cond = rule.condition
49
+ return true unless cond.is_a?(Hash)
50
+
51
+ key = "#{rule.name}:#{event_name}"
52
+ @counters[key] ||= { count: 0, window_start: Time.now }
53
+
54
+ window = cond[:window_seconds] || 60
55
+ @counters[key] = { count: 0, window_start: Time.now } if Time.now - @counters[key][:window_start] > window
56
+
57
+ @counters[key][:count] += 1
58
+ @counters[key][:count] >= (cond[:count_threshold] || 1)
59
+ end
60
+
61
+ def in_cooldown?(rule)
62
+ last = @last_fired[rule.name]
63
+ return false unless last
64
+
65
+ Time.now - last < (rule.cooldown_seconds || 0)
66
+ end
67
+
68
+ def fire_alert(rule, event_name, payload)
69
+ @last_fired[rule.name] = Time.now
70
+ alert = { rule: rule.name, event: event_name, severity: rule.severity,
71
+ payload: payload, fired_at: Time.now.utc }
72
+
73
+ (rule.channels || []).each do |channel|
74
+ case channel.to_sym
75
+ when :events
76
+ Legion::Events.emit('alert.fired', alert) if defined?(Legion::Events)
77
+ when :log
78
+ Legion::Logging.warn "[alert] #{rule.name}: #{event_name} (#{rule.severity})" if defined?(Legion::Logging)
79
+ when :webhook
80
+ Legion::Webhooks.dispatch('alert.fired', alert) if defined?(Legion::Webhooks)
81
+ end
82
+ end
83
+ end
84
+ end
85
+
86
+ class << self
87
+ def setup
88
+ rules = load_rules
89
+ @engine = Engine.new(rules: rules)
90
+ register_listener
91
+ Legion::Logging.debug "Alerts: #{rules.size} rules loaded" if defined?(Legion::Logging)
92
+ end
93
+
94
+ attr_reader :engine
95
+
96
+ def reset!
97
+ @engine = nil
98
+ end
99
+
100
+ private
101
+
102
+ def load_rules
103
+ custom = begin
104
+ Legion::Settings[:alerts][:rules]
105
+ rescue StandardError
106
+ nil
107
+ end
108
+ custom && !custom.empty? ? custom : DEFAULT_RULES
109
+ end
110
+
111
+ def register_listener
112
+ return unless defined?(Legion::Events)
113
+
114
+ Legion::Events.on('*') do |event_name, **payload|
115
+ @engine&.evaluate(event_name, payload)
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ class API < Sinatra::Base
5
+ module Routes
6
+ module Audit
7
+ def self.registered(app)
8
+ app.get '/api/audit' do
9
+ require_data!
10
+ dataset = Legion::Data::Model::AuditLog.order(Sequel.desc(:id))
11
+ dataset = dataset.where(event_type: params[:event_type]) if params[:event_type]
12
+ dataset = dataset.where(principal_id: params[:principal_id]) if params[:principal_id]
13
+ dataset = dataset.where(source: params[:source]) if params[:source]
14
+ dataset = dataset.where(status: params[:status]) if params[:status]
15
+ dataset = dataset.where { created_at >= Time.parse(params[:since]) } if params[:since]
16
+ dataset = dataset.where { created_at <= Time.parse(params[:until]) } if params[:until]
17
+ json_collection(dataset)
18
+ end
19
+
20
+ app.get '/api/audit/verify' do
21
+ require_data!
22
+ halt 503, json_error('unavailable', 'lex-audit is not loaded', status_code: 503) unless defined?(Legion::Extensions::Audit::Runners::Audit)
23
+
24
+ runner = Object.new.extend(Legion::Extensions::Audit::Runners::Audit)
25
+ result = runner.verify
26
+ json_response(result)
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ class API < Sinatra::Base
5
+ module Routes
6
+ module Auth
7
+ def self.registered(app)
8
+ register_token_exchange(app)
9
+ end
10
+
11
+ def self.register_token_exchange(app) # rubocop:disable Metrics/MethodLength
12
+ app.post '/api/auth/token' do
13
+ body = parse_request_body
14
+ grant_type = body[:grant_type]
15
+ subject_token = body[:subject_token]
16
+
17
+ unless grant_type == 'urn:ietf:params:oauth:grant-type:token-exchange'
18
+ halt 400, json_error('unsupported_grant_type', 'expected urn:ietf:params:oauth:grant-type:token-exchange',
19
+ status_code: 400)
20
+ end
21
+
22
+ halt 400, json_error('missing_subject_token', 'subject_token is required', status_code: 400) unless subject_token
23
+
24
+ unless defined?(Legion::Crypt::JWT) && Legion::Crypt::JWT.respond_to?(:verify_with_jwks)
25
+ halt 501, json_error('jwks_validation_not_available', 'legion-crypt JWKS support not loaded',
26
+ status_code: 501)
27
+ end
28
+
29
+ rbac_settings = (Legion::Settings[:rbac].is_a?(Hash) && Legion::Settings[:rbac][:entra]) || {}
30
+ tenant_id = rbac_settings[:tenant_id]
31
+ halt 500, json_error('entra_tenant_not_configured', 'rbac.entra.tenant_id not set', status_code: 500) unless tenant_id
32
+
33
+ jwks_url = "https://login.microsoftonline.com/#{tenant_id}/discovery/v2.0/keys"
34
+ issuer = "https://login.microsoftonline.com/#{tenant_id}/v2.0"
35
+
36
+ begin
37
+ entra_claims = Legion::Crypt::JWT.verify_with_jwks(
38
+ subject_token, jwks_url: jwks_url, issuers: [issuer]
39
+ )
40
+ rescue Legion::Crypt::JWT::ExpiredTokenError
41
+ halt 401, json_error('token_expired', 'Entra token has expired', status_code: 401)
42
+ rescue Legion::Crypt::JWT::InvalidTokenError => e
43
+ halt 401, json_error('invalid_token', e.message, status_code: 401)
44
+ rescue Legion::Crypt::JWT::Error => e
45
+ halt 502, json_error('identity_provider_unavailable', e.message, status_code: 502)
46
+ end
47
+
48
+ unless defined?(Legion::Rbac::EntraClaimsMapper)
49
+ halt 501, json_error('claims_mapper_not_available', 'legion-rbac EntraClaimsMapper not loaded',
50
+ status_code: 501)
51
+ end
52
+
53
+ mapped = Legion::Rbac::EntraClaimsMapper.map_claims(
54
+ entra_claims,
55
+ role_map: rbac_settings[:role_map] || Legion::Rbac::EntraClaimsMapper::DEFAULT_ROLE_MAP,
56
+ group_map: rbac_settings[:group_map] || {},
57
+ default_role: rbac_settings[:default_role] || 'worker'
58
+ )
59
+
60
+ ttl = 28_800
61
+ token = Legion::API::Token.issue_human_token(
62
+ msid: mapped[:sub], name: mapped[:name],
63
+ roles: mapped[:roles], ttl: ttl
64
+ )
65
+
66
+ json_response({
67
+ access_token: token,
68
+ token_type: 'Bearer',
69
+ expires_in: ttl,
70
+ roles: mapped[:roles],
71
+ team: mapped[:team]
72
+ })
73
+ end
74
+ end
75
+
76
+ class << self
77
+ private :register_token_exchange
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -12,6 +12,28 @@ module Legion
12
12
  json_response({ started: false }, status_code: 503)
13
13
  end
14
14
  end
15
+
16
+ app.post '/api/channels/teams/webhook' do
17
+ body = request.body.read
18
+ activity = Legion::JSON.load(body)
19
+
20
+ adapter = Routes::Gaia.teams_adapter
21
+ halt 503, json_response({ error: 'teams adapter not available' }, status_code: 503) unless adapter
22
+
23
+ input_frame = adapter.translate_inbound(activity)
24
+ Legion::Gaia.sensory_buffer&.push(input_frame) if defined?(Legion::Gaia)
25
+
26
+ json_response({ status: 'accepted', frame_id: input_frame&.id })
27
+ end
28
+ end
29
+
30
+ def self.teams_adapter
31
+ return nil unless defined?(Legion::Gaia) && Legion::Gaia.respond_to?(:channel_registry)
32
+ return nil unless Legion::Gaia.channel_registry
33
+
34
+ Legion::Gaia.channel_registry.adapter_for(:teams)
35
+ rescue StandardError
36
+ nil
15
37
  end
16
38
  end
17
39
  end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ class API < Sinatra::Base
5
+ module Routes
6
+ module Metrics
7
+ def self.registered(app)
8
+ app.get '/metrics' do
9
+ unless defined?(Legion::Metrics) && Legion::Metrics.available?
10
+ content_type 'text/plain'
11
+ halt 404, 'prometheus-client gem not available'
12
+ end
13
+
14
+ Legion::Metrics.refresh_gauges
15
+ content_type 'text/plain; version=0.0.4; charset=utf-8'
16
+ Legion::Metrics.render
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end