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.
- checksums.yaml +4 -4
- data/.rubocop.yml +2 -0
- data/CHANGELOG.md +230 -0
- data/CLAUDE.md +20 -5
- data/README.md +13 -1
- data/lib/legion/alerts.rb +120 -0
- data/lib/legion/api/audit.rb +32 -0
- data/lib/legion/api/auth.rb +82 -0
- data/lib/legion/api/gaia.rb +22 -0
- data/lib/legion/api/metrics.rb +22 -0
- data/lib/legion/api/middleware/api_version.rb +42 -0
- data/lib/legion/api/middleware/auth.rb +1 -1
- data/lib/legion/api/middleware/body_limit.rb +31 -0
- data/lib/legion/api/middleware/rate_limit.rb +167 -0
- data/lib/legion/api/validators.rb +44 -0
- data/lib/legion/api/webhooks.rb +29 -0
- data/lib/legion/api/workers.rb +22 -1
- data/lib/legion/api.rb +10 -0
- data/lib/legion/audit/hash_chain.rb +32 -0
- data/lib/legion/audit/siem_export.rb +33 -0
- data/lib/legion/audit.rb +89 -0
- data/lib/legion/catalog.rb +47 -0
- data/lib/legion/chat/notification_bridge.rb +82 -0
- data/lib/legion/chat/notification_queue.rb +43 -0
- data/lib/legion/chat/skills.rb +49 -0
- data/lib/legion/cli/audit_command.rb +66 -0
- data/lib/legion/cli/chat/progress_bar.rb +55 -0
- data/lib/legion/cli/chat/team.rb +43 -0
- data/lib/legion/cli/chat_command.rb +43 -1
- data/lib/legion/cli/config_scaffold.rb +90 -2
- data/lib/legion/cli/cost/data_client.rb +53 -0
- data/lib/legion/cli/cost_command.rb +76 -0
- data/lib/legion/cli/init/config_generator.rb +55 -0
- data/lib/legion/cli/init/environment_detector.rb +65 -0
- data/lib/legion/cli/init_command.rb +58 -0
- data/lib/legion/cli/lex_templates.rb +64 -0
- data/lib/legion/cli/marketplace_command.rb +79 -0
- data/lib/legion/cli/mcp_command.rb +12 -1
- data/lib/legion/cli/notebook_command.rb +69 -0
- data/lib/legion/cli/skill_command.rb +86 -0
- data/lib/legion/cli/templates/core.json.erb +14 -0
- data/lib/legion/cli/update_command.rb +134 -0
- data/lib/legion/cli/worker_command.rb +77 -0
- data/lib/legion/cli.rb +28 -0
- data/lib/legion/context.rb +53 -0
- data/lib/legion/digital_worker/lifecycle.rb +17 -0
- data/lib/legion/digital_worker/registry.rb +12 -0
- data/lib/legion/digital_worker.rb +6 -0
- data/lib/legion/docs/site_generator.rb +48 -0
- data/lib/legion/guardrails.rb +50 -0
- data/lib/legion/ingress.rb +40 -0
- data/lib/legion/isolation.rb +49 -0
- data/lib/legion/mcp/auth.rb +50 -0
- data/lib/legion/mcp/server.rb +8 -2
- data/lib/legion/mcp/tool_governance.rb +77 -0
- data/lib/legion/mcp.rb +9 -0
- data/lib/legion/metrics.rb +117 -0
- data/lib/legion/registry/security_scanner.rb +48 -0
- data/lib/legion/registry.rb +72 -0
- data/lib/legion/runner.rb +21 -1
- data/lib/legion/sandbox.rb +65 -0
- data/lib/legion/service.rb +75 -0
- data/lib/legion/telemetry.rb +65 -0
- data/lib/legion/version.rb +1 -1
- data/lib/legion/webhooks.rb +124 -0
- metadata +43 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 51e90ef95b551c27c044e9f9dac0d784a529d4947835ec0d6501276f73281fb9
|
|
4
|
+
data.tar.gz: 533c1b9813327e486e33cb2c1d657907248517deb15935c68616a095be699280
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 298d051c13e5e658eb572971863a7039d3c08bd168b40b2e6806a3139ccae7261ff29c916bb84ab34fd2fea6c2e6905615f8689e80f5a415feafe75cbf0f56c5
|
|
7
|
+
data.tar.gz: 1f0005c08f21b456e5d64923020e662efbe88a714514ad7bd27b59caa0efc05676551f8b87a150d0226aa1bc9237681d0688bb6b42f1430624ea5ba81e420903
|
data/.rubocop.yml
CHANGED
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.
|
|
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/
|
|
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 #
|
|
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
|
data/lib/legion/api/gaia.rb
CHANGED
|
@@ -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
|