legionio 1.4.13 → 1.4.29

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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +1 -0
  3. data/CHANGELOG.md +143 -0
  4. data/CLAUDE.md +36 -3
  5. data/Gemfile +1 -0
  6. data/README.md +53 -4
  7. data/lib/legion/alerts.rb +120 -0
  8. data/lib/legion/api/audit.rb +32 -0
  9. data/lib/legion/api/metrics.rb +22 -0
  10. data/lib/legion/api/middleware/api_version.rb +42 -0
  11. data/lib/legion/api/middleware/auth.rb +1 -1
  12. data/lib/legion/api/middleware/body_limit.rb +31 -0
  13. data/lib/legion/api/middleware/rate_limit.rb +167 -0
  14. data/lib/legion/api/rbac.rb +186 -0
  15. data/lib/legion/api/validators.rb +44 -0
  16. data/lib/legion/api/workers.rb +22 -1
  17. data/lib/legion/api.rb +12 -0
  18. data/lib/legion/audit.rb +89 -0
  19. data/lib/legion/chat/notification_bridge.rb +80 -0
  20. data/lib/legion/chat/notification_queue.rb +43 -0
  21. data/lib/legion/cli/audit_command.rb +66 -0
  22. data/lib/legion/cli/chat_command.rb +24 -0
  23. data/lib/legion/cli/config_scaffold.rb +91 -3
  24. data/lib/legion/cli/connection.rb +1 -0
  25. data/lib/legion/cli/init/config_generator.rb +55 -0
  26. data/lib/legion/cli/init/environment_detector.rb +65 -0
  27. data/lib/legion/cli/init_command.rb +58 -0
  28. data/lib/legion/cli/rbac_command.rb +209 -0
  29. data/lib/legion/cli/templates/core.json.erb +14 -0
  30. data/lib/legion/cli/update_command.rb +134 -0
  31. data/lib/legion/cli/worker_command.rb +77 -0
  32. data/lib/legion/cli.rb +16 -0
  33. data/lib/legion/digital_worker/lifecycle.rb +17 -0
  34. data/lib/legion/digital_worker/registry.rb +12 -0
  35. data/lib/legion/digital_worker.rb +6 -0
  36. data/lib/legion/ingress.rb +48 -2
  37. data/lib/legion/mcp/server.rb +7 -1
  38. data/lib/legion/mcp/tools/rbac_assignments.rb +45 -0
  39. data/lib/legion/mcp/tools/rbac_check.rb +45 -0
  40. data/lib/legion/mcp/tools/rbac_grants.rb +41 -0
  41. data/lib/legion/metrics.rb +117 -0
  42. data/lib/legion/runner.rb +21 -1
  43. data/lib/legion/service.rb +96 -0
  44. data/lib/legion/telemetry.rb +65 -0
  45. data/lib/legion/version.rb +1 -1
  46. metadata +24 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8b61599f64cd23e52b9cbf85631dce4bdb16afeec87b5022229e7ae63f664917
4
- data.tar.gz: 339df852d48a4ce9eddc92d1a8fd6c3240bda51f7d91f258352a6bc021aea2f0
3
+ metadata.gz: b82509c414c56ab775cbaa5879a5312e9d3fd69226eadae308c52f5cba2bff25
4
+ data.tar.gz: c82590fb0283ee9db793c5b18751dbba8eb51c209351e2674a2ac542e875b718
5
5
  SHA512:
6
- metadata.gz: bbb574075f5512af6a3783acb5bf02d666cf94eb959ac9827639e3bfb29467b1c97ad0d2d562e8318b173e3eaaf73a1ad2db7aaa29957f1682ffd0076dfaa27d
7
- data.tar.gz: 2e4dd37e3f3a7e4520d4b270998a2df0eb1e51accc235061b0974fad27977895e7e29064a6af671b003462815a143081c808fd938e0e4b11eda2823de5416c8d
6
+ metadata.gz: eff59d6453668b41bfa4047f77db11d02dc3fadacd59801d94fc1a7ec4af14598e30d817e0f075631b087a195e1715a68744a0e399fec728c0d426d8b3dc20e6
7
+ data.tar.gz: 10f935f6b10a6db5ef4df40e84a487b5fac01a10bc4789a3eca257aed77294a804309eccca224c70b5a3b082de5e0338408464a2dacb1fc353ef43fa54e128fd
data/.rubocop.yml CHANGED
@@ -35,6 +35,7 @@ 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'
38
39
 
39
40
  Metrics/AbcSize:
40
41
  Max: 60
data/CHANGELOG.md CHANGED
@@ -1,5 +1,148 @@
1
1
  # Legion Changelog
2
2
 
3
+ ## [1.4.29] - 2026-03-16
4
+
5
+ ### Added
6
+ - `legion init`: one-command workspace setup with environment detection
7
+ - `InitHelpers::EnvironmentDetector`: checks for RabbitMQ, database, Vault, Redis, git, existing config
8
+ - `InitHelpers::ConfigGenerator`: ERB template-based config generation, `.legion/` workspace scaffolding
9
+ - `--local` flag for zero-dependency development mode
10
+ - `--force` flag to overwrite existing config files
11
+
12
+ ## [1.4.28] - 2026-03-16
13
+
14
+ ### Added
15
+ - `Legion::Telemetry` module: opt-in OpenTelemetry tracing with `with_span` wrapper
16
+ - `setup_telemetry` in Service: initializes OTel SDK with OTLP exporter when `telemetry.enabled: true`
17
+ - `sanitize_attributes` helper for safe OTel attribute conversion
18
+ - `record_exception` helper for span error recording
19
+
20
+ ## [1.4.27] - 2026-03-16
21
+
22
+ ### Added
23
+ - `legion update` CLI command: updates all Legion gems (`legionio`, `legion-*`, `lex-*`) using the current Ruby's gem binary
24
+ - `--dry-run` flag to check available updates without installing
25
+ - `--json` flag for machine-readable output
26
+ - Updates install into the running Ruby's GEM_HOME (safe for Homebrew bundled installs)
27
+
28
+ ## [1.4.26] - 2026-03-16
29
+
30
+ ### Added
31
+ - `Legion::Metrics` module: opt-in Prometheus metrics via `prometheus-client` gem
32
+ - `GET /metrics` endpoint returning Prometheus text-format output
33
+ - 9 metrics: uptime, active_workers, tasks_total, tasks_per_second, error_rate, consent_violations, llm_requests, llm_tokens
34
+ - Event-driven counters + pull-based gauge refresh on scrape
35
+ - `/metrics` added to Auth middleware SKIP_PATHS
36
+ - Wired into Service startup and shutdown
37
+
38
+ ## [1.4.25] - 2026-03-16
39
+
40
+ ### Added
41
+ - `Legion::Chat::NotificationQueue`: thread-safe priority queue for background notifications
42
+ - `Legion::Chat::NotificationBridge`: event-driven bridge matching Legion events to chat notifications
43
+ - Chat REPL displays pending notifications before each prompt (critical in red, info in yellow)
44
+ - Configurable notification patterns via `chat.notifications.patterns` setting
45
+
46
+ ## [1.4.24] - 2026-03-16
47
+
48
+ ### Added
49
+ - `Legion::Audit.recent_for` — query audit records by principal and time window
50
+ - `Legion::Audit.count_for` — count audit records by principal and time window
51
+ - `Legion::Audit.failure_count_for` / `success_count_for` — convenience wrappers
52
+ - `Legion::Audit.resources_for` — distinct resources invoked by a principal
53
+ - `Legion::Audit.recent` — most recent N records with optional filters
54
+ - All query methods return safe defaults (`[]` or `0`) when legion-data is unavailable
55
+
56
+ ## [1.4.23] - 2026-03-16
57
+
58
+ ### Added
59
+ - `Middleware::BodyLimit`: request body size limit (1MB max, returns 413)
60
+ - `API::Validators` helper module: `validate_required!`, `validate_string_length!`, `validate_enum!`, `validate_uuid!`, `validate_integer!`
61
+ - Ingress payload validation: 512KB size limit, runner_class/function format checks
62
+
63
+ ### Security
64
+ - Ingress validates runner_class format before `Kernel.const_get` to prevent arbitrary constant resolution
65
+ - Ingress validates function format before `.send` to prevent method injection
66
+
67
+ ## [1.4.22] - 2026-03-16
68
+
69
+ ### Added
70
+ - `Legion::Alerts`: configurable alerting rules engine with event pattern matching
71
+ - `Alerts::Engine`: count-based conditions, cooldown deduplication, multi-channel dispatch
72
+ - 4 default rules: consent_violation, extinction_trigger, error_spike, budget_exceeded
73
+ - Channel dispatch: events (via `Legion::Events`), log (via `Legion::Logging`), webhook
74
+ - Settings: `alerts.enabled`, `alerts.rules`
75
+ - Wired into `Service` startup (opt-in via `alerts.enabled: true`)
76
+
77
+ ## [1.4.21] - 2026-03-16
78
+
79
+ ### Added
80
+ - `Middleware::ApiVersion`: rewrites `/api/v1/` paths to `/api/` for future versioned API support
81
+ - Deprecation headers (`Deprecation`, `Sunset`, `Link`) on unversioned `/api/` paths
82
+ - `X-API-Version` request header set for versioned paths
83
+ - Skip paths: `/api/health`, `/api/ready`, `/api/openapi.json`, `/metrics`
84
+
85
+ ## [1.4.20] - 2026-03-16
86
+
87
+ ### Added
88
+ - `Middleware::RateLimit`: sliding-window rate limiting with per-IP, per-agent, per-tenant tiers
89
+ - In-memory store (default) with lazy reap; distributed store via `Legion::Cache` when available
90
+ - Standard headers: `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`, `Retry-After` (429 only)
91
+ - Skip paths: `/api/health`, `/api/ready`, `/api/metrics`, `/api/openapi.json`
92
+
93
+ ## [1.4.19] - 2026-03-16
94
+
95
+ ### Added
96
+ - Local development mode: `LEGION_LOCAL=true` env var or `local_mode: true` in settings
97
+ - Auto-configures in-memory transport, mock Vault, and dev settings
98
+
99
+ ## [1.4.18] - 2026-03-16
100
+
101
+ ### Added
102
+ - `legion config scaffold` auto-detects environment variables and enables providers
103
+ - Detects: AWS_BEARER_TOKEN_BEDROCK, ANTHROPIC_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY, VAULT_TOKEN, RABBITMQ_USER/PASSWORD
104
+ - Detects running Ollama on localhost:11434
105
+ - First detected LLM provider becomes the default; credentials use `env://` references
106
+ - JSON output includes `detected` array for automation
107
+
108
+ ## [1.4.17] - 2026-03-16
109
+
110
+ ### Added
111
+ - `Legion::Audit` publisher module for immutable audit logging via AMQP
112
+ - Audit hook in `Runner.run` records every runner execution (event_type, duration, status)
113
+ - Audit hook in `DigitalWorker::Lifecycle.transition!` records state transitions
114
+ - `GET /api/audit` endpoint with filters (event_type, principal_id, source, status, since, until)
115
+ - `GET /api/audit/verify` endpoint for hash chain integrity verification
116
+ - `legion audit list` and `legion audit verify` CLI commands
117
+ - Silent degradation: audit never interferes with normal operation (triple guard + rescue)
118
+
119
+ ## [1.4.16] - 2026-03-16
120
+
121
+ ### Added
122
+ - `legion worker create NAME` CLI command: provisions digital worker in bootstrap state with DB record + optional Vault secret storage
123
+
124
+ ## [1.4.15] - 2026-03-16
125
+
126
+ ### Added
127
+ - RAI invariant #2: Ingress.run calls Registry.validate_execution! when worker_id is present
128
+ - Unregistered or inactive workers are blocked with structured error (no exception propagation)
129
+ - Registration check fires before RBAC authorization (registration precedes permission)
130
+
131
+ ## [1.4.14] - 2026-03-16
132
+
133
+ ### Added
134
+ - Optional RBAC integration via legion-rbac gem (`if defined?(Legion::Rbac)` guards)
135
+ - `GET /api/workers/:id/health` endpoint returns worker health status with node metrics
136
+ - `health_status` query filter on `GET /api/workers`
137
+ - Thread-safe local worker tracking in `DigitalWorker::Registry` for heartbeat reporting
138
+ - `Legion::DigitalWorker.active_local_ids` delegate method
139
+ - `setup_rbac` lifecycle hook in Service (after setup_data)
140
+ - `authorize_execution!` guard in Ingress for task execution
141
+ - Rack middleware registration in API when legion-rbac loaded
142
+ - REST API routes for RBAC management (roles, assignments, grants, cross-team grants, check)
143
+ - `legion rbac` CLI subcommand (roles, show, assignments, assign, revoke, grants, grant, check)
144
+ - MCP tools: legion.rbac_check, legion.rbac_assignments, legion.rbac_grants
145
+
3
146
  ## [1.4.13] - 2026-03-16
4
147
 
5
148
  ### Changed
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.29
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>
@@ -346,6 +347,22 @@ legion
346
347
  bash # output bash completion script
347
348
  zsh # output zsh completion script
348
349
  install # print installation instructions
350
+
351
+ openapi
352
+ generate [-o FILE] # output OpenAPI 3.1.0 spec JSON
353
+ routes # list all API routes with HTTP method + summary
354
+
355
+ doctor [--fix] [--json] # diagnose environment, suggest/apply fixes
356
+ # checks: Ruby, bundle, config, RabbitMQ, DB, cache, Vault,
357
+ # extensions, PID files, permissions
358
+ # exit 0=all pass, 1=any fail
359
+
360
+ telemetry
361
+ stats [SESSION_ID] # aggregate or per-session telemetry stats
362
+ ingest PATH # manually ingest a session log file
363
+
364
+ auth
365
+ teams [--tenant-id ID] [--client-id ID] # browser OAuth flow for Microsoft Teams
349
366
  ```
350
367
 
351
368
  **CLI design rules:**
@@ -450,7 +467,19 @@ rack-test, rake, rspec, rubocop, rubocop-rspec, simplecov
450
467
  | `lib/legion/api/coldstart.rb` | Coldstart: `POST /api/coldstart/ingest` — triggers lex-coldstart ingest runner (requires lex-coldstart + lex-memory) |
451
468
  | `lib/legion/api/gaia.rb` | Gaia: system status endpoints |
452
469
  | `lib/legion/api/token.rb` | Token: JWT token issuance endpoint |
470
+ | `lib/legion/api/openapi.rb` | OpenAPI: `Legion::API::OpenAPI.spec` / `.to_json`; also served at `GET /api/openapi.json` |
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 |
453
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 |
454
483
  | **MCP** | |
455
484
  | `lib/legion/mcp.rb` | Entry point: `Legion::MCP.server` singleton factory |
456
485
  | `lib/legion/mcp/server.rb` | MCP::Server builder, TOOL_CLASSES array, instructions |
@@ -477,7 +506,7 @@ rack-test, rake, rspec, rubocop, rubocop-rspec, simplecov
477
506
  | `lib/legion/cli/config_scaffold.rb` | `legion config scaffold` — generates starter JSON config files per subsystem |
478
507
  | `lib/legion/cli/generate_command.rb` | `legion generate` subcommands (runner, actor, exchange, queue, message) |
479
508
  | `lib/legion/cli/mcp_command.rb` | `legion mcp` subcommand (stdio + HTTP transports) |
480
- | `lib/legion/cli/worker_command.rb` | `legion worker` subcommands (list, show, pause, retire, terminate, activate, costs) |
509
+ | `lib/legion/cli/worker_command.rb` | `legion worker` subcommands (list, show, create, pause, retire, terminate, activate, costs) |
481
510
  | `lib/legion/cli/coldstart_command.rb` | `legion coldstart` subcommands (ingest, preview, status) |
482
511
  | `lib/legion/cli/chat_command.rb` | `legion chat` — interactive AI REPL + headless prompt mode |
483
512
  | `lib/legion/cli/chat/session.rb` | Chat session: multi-turn conversation, streaming, tool use |
@@ -506,6 +535,10 @@ rack-test, rake, rspec, rubocop, rubocop-rspec, simplecov
506
535
  | `lib/legion/cli/gaia_command.rb` | `legion gaia` subcommands (status) |
507
536
  | `lib/legion/cli/schedule_command.rb` | `legion schedule` subcommands (list, show, add, remove, logs) |
508
537
  | `lib/legion/cli/completion_command.rb` | `legion completion` subcommands (bash, zsh, install) |
538
+ | `lib/legion/cli/openapi_command.rb` | `legion openapi` subcommands (generate, routes); also `GET /api/openapi.json` endpoint |
539
+ | `lib/legion/cli/doctor_command.rb` | `legion doctor` — 10-check environment diagnosis; `Doctor::Result` value object with status/message/prescription/auto_fixable |
540
+ | `lib/legion/cli/telemetry_command.rb` | `legion telemetry` subcommands (stats, ingest) — session log analytics |
541
+ | `lib/legion/cli/auth_command.rb` | `legion auth` subcommands (teams) — delegated OAuth browser flow for external services |
509
542
  | `completions/legion.bash` | Bash tab completion script |
510
543
  | `completions/_legion` | Zsh tab completion script |
511
544
  | `lib/legion/cli/theme.rb` | Purple palette, orbital ASCII banner, branded CLI output |
@@ -544,7 +577,7 @@ rack-test, rake, rspec, rubocop, rubocop-rspec, simplecov
544
577
 
545
578
  ```bash
546
579
  bundle install
547
- bundle exec rspec # 880 examples, 0 failures
580
+ bundle exec rspec # 997 examples, 0 failures
548
581
  bundle exec rubocop # 0 offenses
549
582
  ```
550
583
 
data/Gemfile CHANGED
@@ -14,6 +14,7 @@ unless ENV['CI']
14
14
  gem 'legion-json', path: '../legion-json'
15
15
  gem 'legion-llm', path: '../legion-llm'
16
16
  gem 'legion-logging', path: '../legion-logging'
17
+ gem 'legion-rbac', path: '../legion-rbac'
17
18
  gem 'legion-settings', path: '../legion-settings'
18
19
  gem 'legion-transport', path: '../legion-transport'
19
20
 
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.6** | **Apache-2.0** | [@Esity](https://github.com/Esity)
17
+ **Ruby >= 3.4** | **v1.4.13** | **Apache-2.0** | [@Esity](https://github.com/Esity)
18
18
 
19
19
  ---
20
20
 
@@ -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,11 +206,32 @@ 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
 
216
+ ### Diagnostics
217
+
218
+ ```bash
219
+ legion doctor # diagnose environment, suggest fixes
220
+ legion doctor --fix # auto-remediate fixable issues (stale PIDs, missing gems)
221
+ legion doctor --json # machine-readable output
222
+ ```
223
+
224
+ Checks Ruby version, bundle status, config files, RabbitMQ, database, cache, Vault, extensions, PID files, and permissions. Exits 1 if any check fails.
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
+
213
235
  All commands support `--json` for structured output and `--no-color` to strip ANSI codes.
214
236
 
215
237
  ## REST API
@@ -332,6 +354,25 @@ legion generate actor myactor
332
354
  bundle exec rspec
333
355
  ```
334
356
 
357
+ ## Role Profiles
358
+
359
+ Control which extensions load at startup via `settings/legion.json`:
360
+
361
+ ```json
362
+ {"role": {"profile": "dev"}}
363
+ ```
364
+
365
+ | Profile | What loads |
366
+ |---------|-----------|
367
+ | *(default)* | Everything — no filtering |
368
+ | `core` | 14 core operational extensions only |
369
+ | `cognitive` | core + all agentic extensions |
370
+ | `service` | core + service + other integrations |
371
+ | `dev` | core + AI + essential agentic (~20 extensions) |
372
+ | `custom` | only what's listed in `role.extensions` |
373
+
374
+ Faster boot and lower memory footprint for dedicated worker roles.
375
+
335
376
  ## Scaling
336
377
 
337
378
  Task distribution uses RabbitMQ FIFO queues. Add workers by running more Legion processes — each subscribes to the same queues and picks up work automatically. Tested to 100+ workers.
@@ -363,6 +404,12 @@ CMD ruby --yjit $(which legion) start
363
404
 
364
405
  ## Architecture
365
406
 
407
+ Before any Legion code loads, the executable applies three performance optimizations:
408
+
409
+ - **YJIT** — `RubyVM::YJIT.enable` for 15-30% runtime throughput (Ruby 3.1+ builds)
410
+ - **GC tuning** — pre-allocates 600k heap slots and raises malloc limits (ENV overrides respected)
411
+ - **bootsnap** — caches YARV bytecodes and `$LOAD_PATH` resolution at `~/.legionio/cache/bootsnap/`
412
+
366
413
  ```
367
414
  legion start
368
415
  └── Legion::Service
@@ -374,13 +421,15 @@ legion start
374
421
  ├── 6. Data (legion-data — database + migrations)
375
422
  ├── 7. LLM (legion-llm — AI provider setup + routing)
376
423
  ├── 8. Supervision (process supervision)
377
- ├── 9. Extensions (discover + load 280+ LEX gems)
424
+ ├── 9. Extensions (discover + load 280+ LEX gems, filtered by role profile)
378
425
  ├── 10. Cluster Secret (distribute via Vault or memory)
379
426
  └── 11. API (Sinatra/Puma on port 4567)
380
427
  ```
381
428
 
382
429
  Each phase registers with `Legion::Readiness`. All phases are individually toggleable.
383
430
 
431
+ `SIGHUP` triggers a live reload (`Legion.reload`) — subsystems shut down in reverse order and restart fresh without killing the process. Useful for rolling restarts and config changes.
432
+
384
433
  ## Similar Projects
385
434
 
386
435
  | Project | Language | HA | AI | Cognitive |
@@ -397,7 +446,7 @@ Each phase registers with `Legion::Readiness`. All phases are individually toggl
397
446
  git clone https://github.com/LegionIO/LegionIO.git
398
447
  cd LegionIO
399
448
  bundle install
400
- bundle exec rspec # 694 examples, 0 failures
449
+ bundle exec rspec # 880 examples, 0 failures
401
450
  bundle exec rubocop # 0 offenses
402
451
  ```
403
452
 
@@ -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,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
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sinatra/base'
4
+
5
+ module Legion
6
+ class API < Sinatra::Base
7
+ module Middleware
8
+ class ApiVersion
9
+ SKIP_PATHS = %w[/api/health /api/ready /api/openapi.json /metrics].freeze
10
+
11
+ def initialize(app)
12
+ @app = app
13
+ end
14
+
15
+ def call(env)
16
+ path = env['PATH_INFO']
17
+
18
+ if path.start_with?('/api/v1/')
19
+ env['PATH_INFO'] = path.sub('/api/v1/', '/api/')
20
+ env['HTTP_X_API_VERSION'] = '1'
21
+ @app.call(env)
22
+ elsif path.start_with?('/api/') && !skip_path?(path)
23
+ status, headers, body = @app.call(env)
24
+ headers['Deprecation'] = 'true'
25
+ headers['Sunset'] = (Time.now + (180 * 86_400)).httpdate
26
+ successor = path.sub('/api/', '/api/v1/')
27
+ headers['Link'] = "<#{successor}>; rel=\"successor-version\""
28
+ [status, headers, body]
29
+ else
30
+ @app.call(env)
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def skip_path?(path)
37
+ SKIP_PATHS.any? { |skip| path.start_with?(skip) }
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -4,7 +4,7 @@ module Legion
4
4
  class API < Sinatra::Base
5
5
  module Middleware
6
6
  class Auth
7
- SKIP_PATHS = %w[/api/health /api/ready /api/openapi.json].freeze
7
+ SKIP_PATHS = %w[/api/health /api/ready /api/openapi.json /metrics].freeze
8
8
  AUTH_HEADER = 'HTTP_AUTHORIZATION'
9
9
  BEARER_PATTERN = /\ABearer\s+(.+)\z/i
10
10
  API_KEY_HEADER = 'HTTP_X_API_KEY'
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sinatra/base'
4
+
5
+ module Legion
6
+ class API < Sinatra::Base
7
+ module Middleware
8
+ class BodyLimit
9
+ MAX_BODY_SIZE = 1_048_576 # 1MB
10
+
11
+ def initialize(app, max_size: MAX_BODY_SIZE)
12
+ @app = app
13
+ @max_size = max_size
14
+ end
15
+
16
+ def call(env)
17
+ content_length = env['CONTENT_LENGTH'].to_i
18
+ if content_length > @max_size
19
+ body = Legion::JSON.dump({
20
+ error: { code: 'payload_too_large',
21
+ message: "request body exceeds #{@max_size} bytes" },
22
+ meta: { timestamp: Time.now.utc.iso8601 }
23
+ })
24
+ return [413, { 'content-type' => 'application/json' }, [body]]
25
+ end
26
+ @app.call(env)
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end