legionio 1.7.17 → 1.7.20
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 +3 -0
- data/CHANGELOG.md +51 -0
- data/CLAUDE.md +34 -9
- data/lib/legion/api/default_settings.rb +1 -1
- data/lib/legion/api/identity_audit.rb +46 -0
- data/lib/legion/api/settings.rb +1 -1
- data/lib/legion/api.rb +2 -0
- data/lib/legion/cli/do_command.rb +1 -1
- data/lib/legion/cli/doctor/api_bind_check.rb +56 -0
- data/lib/legion/cli/doctor/mode_check.rb +40 -0
- data/lib/legion/cli/doctor_command.rb +4 -0
- data/lib/legion/extensions.rb +115 -52
- data/lib/legion/identity/broker.rb +159 -0
- data/lib/legion/identity/lease.rb +63 -0
- data/lib/legion/identity/lease_renewer.rb +82 -0
- data/lib/legion/identity/middleware.rb +79 -0
- data/lib/legion/identity/process.rb +121 -0
- data/lib/legion/identity/request.rb +71 -0
- data/lib/legion/mode.rb +76 -0
- data/lib/legion/process_role.rb +4 -2
- data/lib/legion/readiness.rb +5 -3
- data/lib/legion/service.rb +132 -3
- data/lib/legion/tools/config.rb +1 -0
- data/lib/legion/tools/discovery.rb +33 -6
- data/lib/legion/tools/do.rb +1 -0
- data/lib/legion/tools/registry.rb +4 -1
- data/lib/legion/tools/status.rb +1 -1
- data/lib/legion/tools.rb +0 -3
- data/lib/legion/version.rb +1 -1
- data/lib/legion.rb +7 -0
- metadata +11 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ba8e945b1c999e0c815583388817c35bc019134994745682a774afb6e645db1f
|
|
4
|
+
data.tar.gz: d21bf7aea3328dda51f7139ed36632b31f16e5c57bef938a9eb2f9f000c2c21e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c5ef0de86e4ea0a8e92fc7433af4e36bb8b7088737142abcfa5edcaad3769ede8acf0205e2b8478e80cc4376052e6cb3ef3df384b2cf2c39abdc8205fd965a68
|
|
7
|
+
data.tar.gz: 389cf7d22ac2db9e44489c4118cbef323d0d92d9242b4ec0e8a4c7ed141425497249a6832b2cf1bbecdc9e6666d65251978fac36f299175195d2eefa8fb6e190
|
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,57 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [1.7.20] - 2026-04-06
|
|
6
|
+
### Added
|
|
7
|
+
- `Legion::Mode` module with `LEGACY_MAP`, ENV/Settings fallback chain, `agent?`/`worker?`/`infra?`/`lite?` predicates
|
|
8
|
+
- `Legion.instance_id` — UUID computed at load time, ENV override via `LEGIONIO_INSTANCE_ID`
|
|
9
|
+
- `Legion::Identity::Process` singleton — process identity with `bind!`, `bind_fallback!`, `queue_prefix` per-mode, `AtomicReference` thread safety
|
|
10
|
+
- `Legion::Identity::Request` — per-request immutable identity with `from_env`, `from_auth_context`, `to_caller_hash`, `to_rbac_principal`
|
|
11
|
+
- `Legion::Identity::Lease` — credential lease value object with `expired?`, `stale?` (50% TTL), `ttl_seconds`, `valid?`
|
|
12
|
+
- `Legion::Identity::LeaseRenewer` — background thread per provider, 50% TTL renewal, cooperative shutdown (no `Thread#kill`)
|
|
13
|
+
- `Legion::Identity::Broker` — provider management with groups cache (60s TTL, single-flight CAS), `token_for`, `credentials_for`, `shutdown`
|
|
14
|
+
- `Legion::Identity::Middleware` — Rack middleware bridging `legion.auth` to `legion.principal` (`Identity::Request`)
|
|
15
|
+
- `setup_identity` boot step 9 — parallel provider resolution via `Concurrent::Promises`, fallback to `ENV['USER']`
|
|
16
|
+
- Extension publish suppression — defers `LexRegister.publish` until identity resolves, `flush_pending_registrations!`
|
|
17
|
+
- Identity provider auto-registration during phased extension load (`identity_provider?` duck-type check)
|
|
18
|
+
- `GET /api/identity/audit` route with principal and duration filtering
|
|
19
|
+
- `legion doctor` checks: `ApiBindCheck` (non-loopback without auth), `ModeCheck` (no explicit process.mode)
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
- `Readiness.status` upgraded to `Concurrent::Hash` for thread safety; `:identity` added to `COMPONENTS`
|
|
23
|
+
- `READONLY_SECTIONS` extended with `:identity`, `:rbac`, `:api`
|
|
24
|
+
- Default API bind changed from `0.0.0.0` to `127.0.0.1`
|
|
25
|
+
- `ProcessRole` delegates `.current` to `Mode.current`; added `:agent` and `:infra` role entries
|
|
26
|
+
- `lite_mode?` delegates to `Mode.lite?`
|
|
27
|
+
- Reload path adds `Identity::Process.refresh_credentials` after transport reconnect
|
|
28
|
+
- Shutdown adds cooperative `Identity::Broker.shutdown` and JWKS background refresh stop
|
|
29
|
+
|
|
30
|
+
## [1.7.19] - 2026-04-06
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
- `ALWAYS_LOADED` constant in `Tools::Discovery` — pins apollo/knowledge and eval/evaluation runners to always-loaded regardless of extension DSL
|
|
34
|
+
- `always_loaded_names` method on `Tools::Registry` returning names of all non-deferred registered tools
|
|
35
|
+
|
|
36
|
+
### Changed
|
|
37
|
+
- Tool name format changed from dot-separated to dash-separated (`legion-ext-runner-func`) for LLM provider compatibility
|
|
38
|
+
- Reduced noisy debug logging in `Tools::Discovery` and `Tools::Registry`
|
|
39
|
+
|
|
40
|
+
## [1.7.18] - 2026-04-06
|
|
41
|
+
|
|
42
|
+
### Added
|
|
43
|
+
- Multi-phase extension loading: identity providers (`lex-identity-*`) load in phase 0 before all other extensions in phase 1
|
|
44
|
+
- `identity` category in extension registry with prefix matching for `lex-identity-*` gems at tier 0, phase 0
|
|
45
|
+
- `group_by_phase` method groups discovered extensions by phase from the category registry
|
|
46
|
+
- `load_phase_extensions` replaces `load_extensions` — scopes parallel loading to a subset of entries per phase
|
|
47
|
+
- `hook_phase_actors` replaces `hook_all_actors` — hooks deferred actors after each phase completes
|
|
48
|
+
- Per-phase logging during extension loading shows cumulative actor counts
|
|
49
|
+
|
|
50
|
+
### Changed
|
|
51
|
+
- `hook_extensions` now iterates phases sequentially (phase 0 then phase 1), running full load+hook cycle per phase
|
|
52
|
+
- `default_category_registry` includes `phase:` key on all categories; all non-identity categories default to phase 1
|
|
53
|
+
- Catalog transitions (`transition(:running)` + `flush_persisted_transitions`) happen after all phases complete
|
|
54
|
+
- Reserved prefixes list now includes `identity`
|
|
55
|
+
|
|
5
56
|
### Added
|
|
6
57
|
- `Legion::Tools::Base` - canonical tool base class with DSL
|
|
7
58
|
- `Legion::Tools::Registry` - always/deferred tool classification
|
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.
|
|
12
|
+
**Version**: 1.7.18
|
|
13
13
|
**License**: Apache-2.0
|
|
14
14
|
**Docker**: `legionio/legion`
|
|
15
15
|
**Ruby**: >= 3.4
|
|
@@ -51,14 +51,14 @@ Legion.start
|
|
|
51
51
|
├── 10. setup_gaia (legion-gaia, cognitive coordination layer, optional)
|
|
52
52
|
├── 11. setup_telemetry (OpenTelemetry, optional)
|
|
53
53
|
├── 12. setup_supervision (process supervision)
|
|
54
|
-
├── 13. load_extensions (
|
|
54
|
+
├── 13. load_extensions (multi-phase: phase 0 (identity providers) loads and hooks actors first, then phase 1 (everything else))
|
|
55
55
|
├── 14. Legion::Crypt.cs (distribute cluster secret)
|
|
56
56
|
└── 15. setup_api (start Sinatra/Puma on port 4567)
|
|
57
57
|
```
|
|
58
58
|
|
|
59
59
|
Each phase calls `Legion::Readiness.mark_ready(:component)`. All phases are individually toggleable via `Service.new(transport: false, ...)`.
|
|
60
60
|
|
|
61
|
-
Extension loading is
|
|
61
|
+
Extension loading is multi-phase and parallel: `hook_extensions` calls `group_by_phase` to partition discovered extensions by phase number (from the category registry), then iterates phases sequentially. Phase 0 contains identity providers (`lex-identity-*` gems, category `:identity`, tier 0); phase 1 contains all other extensions. Within each phase, extensions are `require`d and `autobuild` runs concurrently on a `Concurrent::FixedThreadPool(min(count, extensions.parallel_pool_size))`, collecting actors into a thread-safe `Concurrent::Array` of `@pending_actors`. Pool size defaults to 24, configurable via `Legion::Settings[:extensions][:parallel_pool_size]`. After each phase's extensions are loaded, `hook_phase_actors` starts AMQP subscriptions, timers, and other actor types for that phase sequentially — ensuring identity providers are fully running before any other extension boots. Catalog transitions (`transition(:running)` and `flush_persisted_transitions`) happen after all phases complete. Thread safety relies on ThreadLocal AMQP channels, per-extension Settings keys, and sequential post-processing of Catalog transitions and Registry writes.
|
|
62
62
|
|
|
63
63
|
### Reload Sequence
|
|
64
64
|
|
|
@@ -85,7 +85,7 @@ Legion (lib/legion.rb)
|
|
|
85
85
|
│ # Ingress.run(payload:, runner_class:, function:, source:)
|
|
86
86
|
│ # Ingress.normalize returns message hash without executing
|
|
87
87
|
├── Extensions # LEX discovery, loading, and lifecycle management
|
|
88
|
-
│ ├── Core # Mixin: data_required?, cache_required?, crypt_required?, etc.
|
|
88
|
+
│ ├── Core # Mixin: data_required?, cache_required?, crypt_required?, mcp_tools?, mcp_tools_deferred?, etc.
|
|
89
89
|
│ ├── Actors/ # Actor execution modes
|
|
90
90
|
│ │ ├── Base # Base actor class
|
|
91
91
|
│ │ ├── Every # Run at interval (timer)
|
|
@@ -96,7 +96,7 @@ Legion (lib/legion.rb)
|
|
|
96
96
|
│ │ └── Nothing # No-op actor
|
|
97
97
|
│ ├── Builders/ # Build actors and runners from LEX definitions
|
|
98
98
|
│ │ ├── Actors # Build actors from extension definitions
|
|
99
|
-
│ │ ├── Runners # Build runners from extension definitions
|
|
99
|
+
│ │ ├── Runners # Build runners from extension definitions; exposes `runner_modules` accessor for Discovery
|
|
100
100
|
│ │ ├── Helpers # Builder utilities
|
|
101
101
|
│ │ ├── Hooks # Webhook hook system builder
|
|
102
102
|
│ │ └── Routes # Auto-route builder: introspects runners, registers POST /api/extensions/* routes
|
|
@@ -149,7 +149,17 @@ Legion (lib/legion.rb)
|
|
|
149
149
|
│ # Populated by Builders::Routes during autobuild via LexDispatch
|
|
150
150
|
│
|
|
151
151
|
├── MCP (legion-mcp gem) # Extracted to standalone gem — see legion-mcp/CLAUDE.md
|
|
152
|
-
│ └── (
|
|
152
|
+
│ └── (tools, 2 resources, TierRouter, PatternStore, ContextGuard, Observer, EmbeddingIndex)
|
|
153
|
+
│
|
|
154
|
+
├── Tools # Canonical tool layer — replaces Extensions::Capability and Catalog::Registry
|
|
155
|
+
│ ├── Base # Base class for all framework tools (Do, Status, Config are built-in statics)
|
|
156
|
+
│ ├── Registry # always/deferred classification for all tools; replaces Catalog::Registry
|
|
157
|
+
│ │ # Extensions declare tools via `mcp_tools?` / `mcp_tools_deferred?` DSL on Core
|
|
158
|
+
│ ├── Discovery # Auto-discovers tools from extension runner modules at boot
|
|
159
|
+
│ │ # `runner_modules` accessor on Builders::Runners feeds Discovery
|
|
160
|
+
│ │ # `loaded_extension_modules` on Extensions exposes the full set
|
|
161
|
+
│ └── EmbeddingCache # 5-tier persistent embedding cache:
|
|
162
|
+
│ # L0 in-memory hash → L1 Cache::Local → L2 Cache → L3 Data::Local → L4 Data
|
|
153
163
|
│
|
|
154
164
|
├── DigitalWorker # Digital worker platform (AI-as-labor governance)
|
|
155
165
|
│ ├── Lifecycle # Worker state machine (active/paused/retired/terminated)
|
|
@@ -248,6 +258,16 @@ Legion (lib/legion.rb)
|
|
|
248
258
|
|
|
249
259
|
`Legion::Extensions.find_extensions` discovers lex-* gems via `Bundler.load.specs` (when running under Bundler) or falls back to `Gem::Specification.all_names`. It also processes `Legion::Settings[:extensions]` for explicitly configured extensions, attempting `Gem.install` for missing ones if `auto_install` is enabled.
|
|
250
260
|
|
|
261
|
+
**Category registry**: Extensions are classified by `categorize_and_order` using `default_category_registry`. Each category has a `type` (`:list` or `:prefix`), `tier` (load order within a phase), and `phase`:
|
|
262
|
+
|
|
263
|
+
| Category | Type | Tier | Phase | Matches |
|
|
264
|
+
|----------|------|------|-------|---------|
|
|
265
|
+
| `identity` | prefix | 0 | 0 | `lex-identity-*` gems |
|
|
266
|
+
| `core` | list | 1 | 1 | explicitly listed core extensions |
|
|
267
|
+
| `ai` | list | 2 | 1 | explicitly listed AI provider extensions |
|
|
268
|
+
| `gaia` | list | 3 | 1 | explicitly listed GAIA extensions |
|
|
269
|
+
| `agentic` | prefix | 4 | 1 | `lex-agentic-*` gems |
|
|
270
|
+
|
|
251
271
|
**Role-based filtering**: After discovery, `apply_role_filter` prunes extensions based on `Legion::Settings[:role][:profile]`:
|
|
252
272
|
|
|
253
273
|
| Profile | What loads |
|
|
@@ -479,7 +499,7 @@ legion
|
|
|
479
499
|
|
|
480
500
|
### MCP Design
|
|
481
501
|
|
|
482
|
-
Extracted to the `legion-mcp` gem (v0.
|
|
502
|
+
Extracted to the `legion-mcp` gem (v0.7.3). See `legion-mcp/CLAUDE.md` for full architecture.
|
|
483
503
|
|
|
484
504
|
- `Legion::MCP.server` is memoized singleton — call `Legion::MCP.reset!` in tests
|
|
485
505
|
- Tool naming: `legion.snake_case_name` (dot namespace, not slash)
|
|
@@ -571,7 +591,7 @@ rack-test, rake, rspec, rubocop, rubocop-rspec, simplecov
|
|
|
571
591
|
| `lib/legion/readiness.rb` | Component readiness tracking (COMPONENTS constant, `ready?`, `to_h`) |
|
|
572
592
|
| `lib/legion/events.rb` | In-process pub/sub: `on`, `emit`, `once`, `off`, wildcard `*` |
|
|
573
593
|
| `lib/legion/ingress.rb` | Universal runner invocation: `normalize`, `run` |
|
|
574
|
-
| `lib/legion/extensions.rb` | LEX discovery, loading, actor hooking, shutdown |
|
|
594
|
+
| `lib/legion/extensions.rb` | LEX discovery, loading, actor hooking, shutdown; exposes `loaded_extension_modules` for Tools::Discovery |
|
|
575
595
|
| `lib/legion/extensions/core.rb` | Extension mixin (requirement flags, autobuild) |
|
|
576
596
|
| `lib/legion/extensions/actors/` | Actor types: base, every, loop, once, poll, subscription, nothing, defaults |
|
|
577
597
|
| `lib/legion/extensions/builders/` | Build actors, runners, helpers, hooks, routes from definitions |
|
|
@@ -586,7 +606,12 @@ rack-test, rake, rspec, rubocop, rubocop-rspec, simplecov
|
|
|
586
606
|
| `lib/legion/isolation.rb` | Process isolation for untrusted extension execution |
|
|
587
607
|
| `lib/legion/sandbox.rb` | Sandboxed execution environment for extensions |
|
|
588
608
|
| `lib/legion/context.rb` | Thread-local execution context (request tracing, tenant) |
|
|
589
|
-
| `lib/legion/catalog.rb` | Extension catalog: registry of available extensions with metadata |
|
|
609
|
+
| `lib/legion/catalog.rb` | Extension catalog: registry of available extensions with metadata (Catalog::Registry removed — replaced by Tools::Registry) |
|
|
610
|
+
| `lib/legion/tools.rb` | Tools module entry point |
|
|
611
|
+
| `lib/legion/tools/base.rb` | Tools::Base — canonical base class for all tools |
|
|
612
|
+
| `lib/legion/tools/registry.rb` | Tools::Registry — always/deferred classification, replaces Catalog::Registry |
|
|
613
|
+
| `lib/legion/tools/discovery.rb` | Tools::Discovery — auto-discovers tools from extension runner_modules at boot |
|
|
614
|
+
| `lib/legion/tools/embedding_cache.rb` | Tools::EmbeddingCache — 5-tier persistent embedding cache (L0–L4) |
|
|
590
615
|
| `lib/legion/registry.rb` | Extension registry with security scanning |
|
|
591
616
|
| `lib/legion/registry/security_scanner.rb` | Gem security scanner (CVE checks, signature verification) |
|
|
592
617
|
| `lib/legion/webhooks.rb` | Webhook delivery system: HTTP POST with retry, HMAC signing |
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
class API < Sinatra::Base
|
|
5
|
+
module Routes
|
|
6
|
+
module IdentityAudit
|
|
7
|
+
def self.registered(app)
|
|
8
|
+
app.helpers IdentityAuditHelpers
|
|
9
|
+
|
|
10
|
+
app.get '/api/identity/audit' do
|
|
11
|
+
halt 503, json_error('unavailable', 'audit records not available') unless defined?(Legion::Data::Model::AuditRecord)
|
|
12
|
+
|
|
13
|
+
dataset = Legion::Data::Model::AuditRecord.where(entity_type: 'identity')
|
|
14
|
+
|
|
15
|
+
principal = params[:principal]
|
|
16
|
+
dataset = dataset.where(Sequel.lit("metadata->>'principal' = ?", principal)) if principal
|
|
17
|
+
|
|
18
|
+
since = params[:since]
|
|
19
|
+
if since
|
|
20
|
+
duration = parse_since_duration(since)
|
|
21
|
+
dataset = dataset.where { created_at >= Time.now - duration } if duration
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
records = dataset.order(Sequel.desc(:created_at)).limit(100).all
|
|
25
|
+
json_collection(records.map do |r|
|
|
26
|
+
{ id: r.id, action: r.action, entity_type: r.entity_type, metadata: r.parsed_metadata, created_at: r.created_at }
|
|
27
|
+
end)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
module IdentityAuditHelpers
|
|
32
|
+
def parse_since_duration(value)
|
|
33
|
+
return nil unless value.is_a?(String)
|
|
34
|
+
|
|
35
|
+
case value
|
|
36
|
+
when /\A(\d+)h\z/ then Regexp.last_match(1).to_i * 3600
|
|
37
|
+
when /\A(\d+)m\z/ then Regexp.last_match(1).to_i * 60
|
|
38
|
+
when /\A(\d+)s\z/ then Regexp.last_match(1).to_i
|
|
39
|
+
when /\A(\d+)d\z/ then Regexp.last_match(1).to_i * 86_400
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
data/lib/legion/api/settings.rb
CHANGED
|
@@ -5,7 +5,7 @@ module Legion
|
|
|
5
5
|
module Routes
|
|
6
6
|
module Settings
|
|
7
7
|
SENSITIVE_KEYS = %i[password secret token key cert private_key api_key].freeze
|
|
8
|
-
READONLY_SECTIONS = %i[crypt transport].freeze
|
|
8
|
+
READONLY_SECTIONS = %i[crypt transport identity rbac api].freeze
|
|
9
9
|
|
|
10
10
|
def self.registered(app)
|
|
11
11
|
app.get '/api/settings' do
|
data/lib/legion/api.rb
CHANGED
|
@@ -60,6 +60,7 @@ require_relative 'api/tbi_patterns'
|
|
|
60
60
|
require_relative 'api/webhooks'
|
|
61
61
|
require_relative 'api/tenants'
|
|
62
62
|
require_relative 'api/inbound_webhooks'
|
|
63
|
+
require_relative 'api/identity_audit'
|
|
63
64
|
require_relative 'api/graphql' if defined?(GraphQL)
|
|
64
65
|
|
|
65
66
|
module Legion
|
|
@@ -216,6 +217,7 @@ module Legion
|
|
|
216
217
|
register Routes::Webhooks
|
|
217
218
|
register Routes::Tenants
|
|
218
219
|
register Routes::InboundWebhooks
|
|
220
|
+
register Routes::IdentityAudit
|
|
219
221
|
register Routes::GraphQL if defined?(Routes::GraphQL)
|
|
220
222
|
|
|
221
223
|
use Legion::API::Middleware::RequestLogger
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module CLI
|
|
5
|
+
class Doctor
|
|
6
|
+
class ApiBindCheck
|
|
7
|
+
LOOPBACK_BINDS = %w[127.0.0.1 ::1 localhost].freeze
|
|
8
|
+
|
|
9
|
+
def name
|
|
10
|
+
'API bind address'
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def run
|
|
14
|
+
return skip_result unless defined?(Legion::Settings)
|
|
15
|
+
|
|
16
|
+
api_settings = Legion::Settings[:api]
|
|
17
|
+
return skip_result unless api_settings.is_a?(Hash)
|
|
18
|
+
|
|
19
|
+
bind = api_settings[:bind]
|
|
20
|
+
return skip_result if bind.nil?
|
|
21
|
+
|
|
22
|
+
if LOOPBACK_BINDS.include?(bind)
|
|
23
|
+
Result.new(
|
|
24
|
+
name: name,
|
|
25
|
+
status: :pass,
|
|
26
|
+
message: "API bound to loopback (#{bind})"
|
|
27
|
+
)
|
|
28
|
+
elsif api_settings.dig(:auth, :enabled) == true
|
|
29
|
+
Result.new(
|
|
30
|
+
name: name,
|
|
31
|
+
status: :pass,
|
|
32
|
+
message: "API bound to #{bind} with auth enabled"
|
|
33
|
+
)
|
|
34
|
+
else
|
|
35
|
+
Result.new(
|
|
36
|
+
name: name,
|
|
37
|
+
status: :warn,
|
|
38
|
+
message: "API bound to non-loopback address (#{bind}) without explicit auth configuration",
|
|
39
|
+
prescription: "Set api.auth.enabled: true or change api.bind to '127.0.0.1'"
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def skip_result
|
|
47
|
+
Result.new(
|
|
48
|
+
name: name,
|
|
49
|
+
status: :pass,
|
|
50
|
+
message: 'API settings not loaded'
|
|
51
|
+
)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module CLI
|
|
5
|
+
class Doctor
|
|
6
|
+
class ModeCheck
|
|
7
|
+
def name
|
|
8
|
+
'Process mode'
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def run
|
|
12
|
+
unless defined?(Legion::Settings)
|
|
13
|
+
return Result.new(
|
|
14
|
+
name: name,
|
|
15
|
+
status: :pass,
|
|
16
|
+
message: 'Settings not loaded'
|
|
17
|
+
)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
explicit_mode = Legion::Settings.dig(:process, :mode) || Legion::Settings[:mode]
|
|
21
|
+
|
|
22
|
+
if explicit_mode
|
|
23
|
+
Result.new(
|
|
24
|
+
name: name,
|
|
25
|
+
status: :pass,
|
|
26
|
+
message: "Explicit process mode configured: #{explicit_mode}"
|
|
27
|
+
)
|
|
28
|
+
else
|
|
29
|
+
Result.new(
|
|
30
|
+
name: name,
|
|
31
|
+
status: :warn,
|
|
32
|
+
message: 'No explicit process.mode configured (defaulting to agent)',
|
|
33
|
+
prescription: 'Set {"process": {"mode": "agent"}} in settings to prepare for Phase 9 default change to worker'
|
|
34
|
+
)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -17,6 +17,8 @@ module Legion
|
|
|
17
17
|
autoload :PidCheck, 'legion/cli/doctor/pid_check'
|
|
18
18
|
autoload :PermissionsCheck, 'legion/cli/doctor/permissions_check'
|
|
19
19
|
autoload :TlsCheck, 'legion/cli/doctor/tls_check'
|
|
20
|
+
autoload :ApiBindCheck, 'legion/cli/doctor/api_bind_check'
|
|
21
|
+
autoload :ModeCheck, 'legion/cli/doctor/mode_check'
|
|
20
22
|
|
|
21
23
|
def self.exit_on_failure?
|
|
22
24
|
true
|
|
@@ -37,6 +39,8 @@ module Legion
|
|
|
37
39
|
PidCheck
|
|
38
40
|
PermissionsCheck
|
|
39
41
|
TlsCheck
|
|
42
|
+
ApiBindCheck
|
|
43
|
+
ModeCheck
|
|
40
44
|
].freeze
|
|
41
45
|
|
|
42
46
|
# Weights: security > connectivity > convenience
|
data/lib/legion/extensions.rb
CHANGED
|
@@ -21,12 +21,22 @@ module Legion
|
|
|
21
21
|
@local_tasks = []
|
|
22
22
|
@actors = []
|
|
23
23
|
@running_instances = Concurrent::Array.new
|
|
24
|
-
@
|
|
24
|
+
@loaded_extensions = []
|
|
25
|
+
@pending_registrations = Concurrent::Array.new
|
|
25
26
|
|
|
26
27
|
find_extensions
|
|
27
|
-
|
|
28
|
+
|
|
29
|
+
phases = group_by_phase
|
|
30
|
+
phases.each do |phase_num, entries|
|
|
31
|
+
@pending_actors = Concurrent::Array.new
|
|
32
|
+
load_phase_extensions(phase_num, entries)
|
|
33
|
+
hook_phase_actors(phase_num)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
@loaded_extensions&.each { |name| Catalog.transition(name, :running) }
|
|
37
|
+
Catalog.flush_persisted_transitions
|
|
38
|
+
|
|
28
39
|
load_yaml_agents
|
|
29
|
-
hook_all_actors
|
|
30
40
|
end
|
|
31
41
|
|
|
32
42
|
attr_reader :local_tasks
|
|
@@ -97,6 +107,21 @@ module Legion
|
|
|
97
107
|
Legion::Logging.info "Successfully shut down all actors (#{(Time.now - shutdown_start).round(1)}s)"
|
|
98
108
|
end
|
|
99
109
|
|
|
110
|
+
def flush_pending_registrations!
|
|
111
|
+
return if @pending_registrations.nil? || @pending_registrations.empty?
|
|
112
|
+
|
|
113
|
+
registrations = @pending_registrations
|
|
114
|
+
count = registrations.size
|
|
115
|
+
@pending_registrations = nil
|
|
116
|
+
|
|
117
|
+
registrations.each do |registration|
|
|
118
|
+
registration.publish
|
|
119
|
+
rescue StandardError => e
|
|
120
|
+
Legion::Logging.warn "[Extensions] flush registration failed: #{e.message}" if defined?(Legion::Logging)
|
|
121
|
+
end
|
|
122
|
+
Legion::Logging.info "[Extensions] flushed #{count} pending registrations" if defined?(Legion::Logging)
|
|
123
|
+
end
|
|
124
|
+
|
|
100
125
|
def pause_actors
|
|
101
126
|
@running_instances&.each do |inst|
|
|
102
127
|
timer = inst.instance_variable_get(:@timer)
|
|
@@ -107,11 +132,8 @@ module Legion
|
|
|
107
132
|
Legion::Logging.warn 'All actors paused' if defined?(Legion::Logging)
|
|
108
133
|
end
|
|
109
134
|
|
|
110
|
-
def
|
|
111
|
-
|
|
112
|
-
@loaded_extensions ||= []
|
|
113
|
-
|
|
114
|
-
eligible = @extensions.filter_map do |entry|
|
|
135
|
+
def load_phase_extensions(phase_num, entries)
|
|
136
|
+
eligible = entries.filter_map do |entry|
|
|
115
137
|
gem_name = entry[:gem_name]
|
|
116
138
|
ext_name = entry[:require_path].split('/').last
|
|
117
139
|
|
|
@@ -130,15 +152,35 @@ module Legion
|
|
|
130
152
|
load_extensions_parallel(eligible)
|
|
131
153
|
|
|
132
154
|
Legion::Logging.info(
|
|
133
|
-
"#{
|
|
134
|
-
"subscription:#{@subscription_tasks.count}," \
|
|
155
|
+
"Phase #{phase_num}: #{eligible.count} extensions loaded " \
|
|
156
|
+
"(subscription:#{@subscription_tasks.count}," \
|
|
135
157
|
"every:#{@timer_tasks.count}," \
|
|
136
158
|
"poll:#{@poll_tasks.count}," \
|
|
137
159
|
"once:#{@once_tasks.count}," \
|
|
138
|
-
"loop:#{@loop_tasks.count}"
|
|
160
|
+
"loop:#{@loop_tasks.count})"
|
|
139
161
|
)
|
|
140
162
|
end
|
|
141
163
|
|
|
164
|
+
def hook_phase_actors(phase_num)
|
|
165
|
+
return if @pending_actors.nil? || @pending_actors.empty?
|
|
166
|
+
|
|
167
|
+
Legion::Logging.info "Phase #{phase_num}: hooking #{@pending_actors.size} deferred actors"
|
|
168
|
+
|
|
169
|
+
groups = group_pending_actors
|
|
170
|
+
|
|
171
|
+
%i[once poll every loop].each do |type|
|
|
172
|
+
next if groups[type].empty?
|
|
173
|
+
|
|
174
|
+
groups[type].each { |actor| hook_actor(**actor) }
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
hook_subscription_actors_pooled(groups[:subscription]) unless groups[:subscription].empty?
|
|
178
|
+
|
|
179
|
+
dispatch_local_actors(@local_tasks) unless @local_tasks.empty?
|
|
180
|
+
|
|
181
|
+
@pending_actors.clear
|
|
182
|
+
end
|
|
183
|
+
|
|
142
184
|
def load_extensions_parallel(eligible)
|
|
143
185
|
return if eligible.empty?
|
|
144
186
|
|
|
@@ -227,8 +269,15 @@ module Legion
|
|
|
227
269
|
has_logger = extension.respond_to?(:log)
|
|
228
270
|
extension.autobuild
|
|
229
271
|
|
|
272
|
+
register_identity_provider(extension, entry) if identity_provider?(extension)
|
|
273
|
+
|
|
230
274
|
require 'legion/transport/messages/lex_register'
|
|
231
|
-
Legion::Transport::Messages::LexRegister.new(function: 'save', opts: extension.runners)
|
|
275
|
+
registration = Legion::Transport::Messages::LexRegister.new(function: 'save', opts: extension.runners)
|
|
276
|
+
if @pending_registrations
|
|
277
|
+
@pending_registrations << registration
|
|
278
|
+
else
|
|
279
|
+
registration.publish
|
|
280
|
+
end
|
|
232
281
|
|
|
233
282
|
register_capabilities(entry[:gem_name], extension.runners) if extension.respond_to?(:runners)
|
|
234
283
|
write_lex_cli_manifest(entry, extension)
|
|
@@ -273,38 +322,6 @@ module Legion
|
|
|
273
322
|
false
|
|
274
323
|
end
|
|
275
324
|
|
|
276
|
-
def hook_all_actors
|
|
277
|
-
return if @pending_actors.nil? || @pending_actors.empty?
|
|
278
|
-
|
|
279
|
-
Legion::Logging.info "Hooking #{@pending_actors.size} deferred actors"
|
|
280
|
-
|
|
281
|
-
groups = group_pending_actors
|
|
282
|
-
|
|
283
|
-
%i[once poll every loop].each do |type|
|
|
284
|
-
next if groups[type].empty?
|
|
285
|
-
|
|
286
|
-
Legion::Logging.info "Starting #{type} actors (#{groups[type].size})"
|
|
287
|
-
groups[type].each { |actor| hook_actor(**actor) }
|
|
288
|
-
end
|
|
289
|
-
unless groups[:subscription].empty?
|
|
290
|
-
Legion::Logging.info "Starting subscription actors (#{groups[:subscription].size})"
|
|
291
|
-
hook_subscription_actors_pooled(groups[:subscription])
|
|
292
|
-
end
|
|
293
|
-
dispatch_local_actors(@local_tasks) unless @local_tasks.empty?
|
|
294
|
-
|
|
295
|
-
@pending_actors.clear
|
|
296
|
-
Legion::Logging.info(
|
|
297
|
-
"Actors hooked: subscription:#{@subscription_tasks.count}," \
|
|
298
|
-
"every:#{@timer_tasks.count}," \
|
|
299
|
-
"poll:#{@poll_tasks.count}," \
|
|
300
|
-
"once:#{@once_tasks.count}," \
|
|
301
|
-
"loop:#{@loop_tasks.count}," \
|
|
302
|
-
"local:#{@local_tasks.count}"
|
|
303
|
-
)
|
|
304
|
-
@loaded_extensions&.each { |name| Catalog.transition(name, :running) }
|
|
305
|
-
Catalog.flush_persisted_transitions
|
|
306
|
-
end
|
|
307
|
-
|
|
308
325
|
ACTOR_TYPE_MAP = {
|
|
309
326
|
Once: :once,
|
|
310
327
|
Poll: :poll,
|
|
@@ -313,6 +330,17 @@ module Legion
|
|
|
313
330
|
Subscription: :subscription
|
|
314
331
|
}.freeze
|
|
315
332
|
|
|
333
|
+
def group_by_phase
|
|
334
|
+
settings_cats = ::Legion::Settings.dig(:extensions, :categories) || {}
|
|
335
|
+
categories = default_category_registry.merge(settings_cats)
|
|
336
|
+
default_phase = 1
|
|
337
|
+
|
|
338
|
+
@extensions.group_by do |entry|
|
|
339
|
+
cat = entry[:category]
|
|
340
|
+
categories.dig(cat, :phase) || default_phase
|
|
341
|
+
end.sort_by(&:first)
|
|
342
|
+
end
|
|
343
|
+
|
|
316
344
|
def group_pending_actors
|
|
317
345
|
groups = { once: [], poll: [], every: [], loop: [], subscription: [] }
|
|
318
346
|
@pending_actors.each do |actor|
|
|
@@ -400,6 +428,39 @@ module Legion
|
|
|
400
428
|
|
|
401
429
|
private
|
|
402
430
|
|
|
431
|
+
def identity_provider?(extension)
|
|
432
|
+
extension.respond_to?(:provider_name) &&
|
|
433
|
+
extension.respond_to?(:provider_type) &&
|
|
434
|
+
extension.respond_to?(:facing)
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
def register_identity_provider(extension, entry)
|
|
438
|
+
return unless defined?(Legion::Data) && Legion::Data.connected?
|
|
439
|
+
return unless defined?(Legion::Data::Model::IdentityProvider)
|
|
440
|
+
|
|
441
|
+
name = extension.provider_name.to_s
|
|
442
|
+
attrs = {
|
|
443
|
+
provider_type: extension.provider_type.to_s,
|
|
444
|
+
facing: extension.facing.to_s,
|
|
445
|
+
priority: extension.respond_to?(:priority) ? extension.priority : 100,
|
|
446
|
+
trust_weight: extension.respond_to?(:trust_weight) ? extension.trust_weight : 50,
|
|
447
|
+
capabilities: extension.respond_to?(:capabilities) ? Array(extension.capabilities).map(&:to_s) : [],
|
|
448
|
+
source: 'gem',
|
|
449
|
+
enabled: true
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
existing = Legion::Data::Model::IdentityProvider.where(name: name).first
|
|
453
|
+
if existing
|
|
454
|
+
diverged = attrs.any? { |k, v| existing.send(k).to_s != v.to_s }
|
|
455
|
+
Legion::Logging.info "[identity][provider] name=#{name} source=db/gem diverged=#{diverged}" if defined?(Legion::Logging)
|
|
456
|
+
else
|
|
457
|
+
Legion::Data::Model::IdentityProvider.insert_conflict(target: :name, update: attrs).insert(attrs.merge(name: name))
|
|
458
|
+
Legion::Logging.info "[identity][provider] name=#{name} registered" if defined?(Legion::Logging)
|
|
459
|
+
end
|
|
460
|
+
rescue StandardError => e
|
|
461
|
+
Legion::Logging.warn "[identity][provider] registration failed for #{entry[:gem_name]}: #{e.message}" if defined?(Legion::Logging)
|
|
462
|
+
end
|
|
463
|
+
|
|
403
464
|
def write_lex_cli_manifest(entry, extension)
|
|
404
465
|
require 'legion/cli/lex_cli_manifest'
|
|
405
466
|
|
|
@@ -661,9 +722,10 @@ module Legion
|
|
|
661
722
|
ext_settings = ::Legion::Settings[:extensions] || {}
|
|
662
723
|
categories = ext_settings[:categories] || default_category_registry
|
|
663
724
|
lists = {
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
725
|
+
identity: Array(ext_settings[:identity]),
|
|
726
|
+
core: Array(ext_settings[:core]),
|
|
727
|
+
ai: Array(ext_settings[:ai]),
|
|
728
|
+
gaia: Array(ext_settings[:gaia])
|
|
667
729
|
}
|
|
668
730
|
ctx = {
|
|
669
731
|
blocked: Array(ext_settings[:blocked]),
|
|
@@ -696,7 +758,7 @@ module Legion
|
|
|
696
758
|
Legion::Logging.debug "Extensions#check_reserved_words failed to read reserved_prefixes: #{e.message}" if defined?(Legion::Logging)
|
|
697
759
|
[]
|
|
698
760
|
end
|
|
699
|
-
reserved_prefixes = configured_prefixes.empty? ? %w[core ai agentic gaia] : configured_prefixes
|
|
761
|
+
reserved_prefixes = configured_prefixes.empty? ? %w[core ai agentic gaia identity] : configured_prefixes
|
|
700
762
|
|
|
701
763
|
configured_words = begin
|
|
702
764
|
Array(::Legion::Settings.dig(:extensions, :reserved_words))
|
|
@@ -881,10 +943,11 @@ module Legion
|
|
|
881
943
|
|
|
882
944
|
def default_category_registry
|
|
883
945
|
{
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
946
|
+
identity: { type: :prefix, tier: 0, phase: 0 },
|
|
947
|
+
core: { type: :list, tier: 1, phase: 1 },
|
|
948
|
+
ai: { type: :list, tier: 2, phase: 1 },
|
|
949
|
+
gaia: { type: :list, tier: 3, phase: 1 },
|
|
950
|
+
agentic: { type: :prefix, tier: 4, phase: 1 }
|
|
888
951
|
}
|
|
889
952
|
end
|
|
890
953
|
|