legionio 1.4.119 → 1.4.122
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/CHANGELOG.md +26 -0
- data/legionio.gemspec +1 -0
- data/lib/legion/alerts.rb +5 -1
- data/lib/legion/api/audit.rb +11 -2
- data/lib/legion/api/auth.rb +14 -2
- data/lib/legion/api/auth_human.rb +28 -7
- data/lib/legion/api/auth_worker.rb +18 -3
- data/lib/legion/api/capacity.rb +9 -0
- data/lib/legion/api/chains.rb +26 -6
- data/lib/legion/api/coldstart.rb +16 -4
- data/lib/legion/api/extensions.rb +6 -2
- data/lib/legion/api/gaia.rb +6 -2
- data/lib/legion/api/graphql.rb +3 -1
- data/lib/legion/api/hooks.rb +19 -5
- data/lib/legion/api/lex.rb +4 -1
- data/lib/legion/api/llm.rb +54 -1
- data/lib/legion/api/middleware/auth.rb +3 -0
- data/lib/legion/api/middleware/body_limit.rb +3 -0
- data/lib/legion/api/middleware/rate_limit.rb +1 -0
- data/lib/legion/api/middleware/tenant.rb +4 -1
- data/lib/legion/api/prompts.rb +9 -4
- data/lib/legion/api/rbac.rb +17 -1
- data/lib/legion/api/relationships.rb +5 -0
- data/lib/legion/api/schedules.rb +12 -2
- data/lib/legion/api/settings.rb +14 -3
- data/lib/legion/api/tasks.rb +13 -3
- data/lib/legion/api/transport.rb +11 -4
- data/lib/legion/api/webhooks.rb +5 -1
- data/lib/legion/api/workers.rb +28 -8
- data/lib/legion/api.rb +2 -1
- data/lib/legion/audit.rb +1 -1
- data/lib/legion/capacity/model.rb +11 -2
- data/lib/legion/catalog.rb +2 -0
- data/lib/legion/cli/error_handler.rb +9 -1
- data/lib/legion/context.rb +3 -0
- data/lib/legion/digital_worker/lifecycle.rb +5 -1
- data/lib/legion/digital_worker/registry.rb +7 -0
- data/lib/legion/events.rb +3 -2
- data/lib/legion/extensions/actors/every.rb +7 -1
- data/lib/legion/extensions/actors/subscription.rb +7 -5
- data/lib/legion/extensions/builders/actors.rb +2 -1
- data/lib/legion/extensions/builders/routes.rb +1 -0
- data/lib/legion/extensions/builders/runners.rb +1 -0
- data/lib/legion/extensions/core.rb +9 -1
- data/lib/legion/extensions/transport.rb +3 -1
- data/lib/legion/graph/builder.rb +2 -0
- data/lib/legion/graph/exporter.rb +2 -0
- data/lib/legion/guardrails.rb +8 -2
- data/lib/legion/ingress.rb +14 -2
- data/lib/legion/process.rb +4 -0
- data/lib/legion/readiness.rb +10 -4
- data/lib/legion/runner/status.rb +8 -8
- data/lib/legion/runner.rb +4 -1
- data/lib/legion/service.rb +10 -0
- data/lib/legion/telemetry.rb +2 -1
- data/lib/legion/trace_search.rb +3 -0
- data/lib/legion/version.rb +1 -1
- data/lib/legion/webhooks.rb +9 -0
- metadata +15 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 102a045825ff17747b2724547e3704b958c55b51e77ce846d4dd8146460e8dbb
|
|
4
|
+
data.tar.gz: eccf79a4549c7f5bc949c2dc7141dd9b5ff794c85483d6cc414c790b7c867c06
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fb2d2613bfd4e539755cf225e5917ff8aec939840f1435c1308c71ecc862617e31e516876ca6dd513dfe5496106296af1421ae9e3b9431871a99594cdbab5d19
|
|
7
|
+
data.tar.gz: 383b632d9dbb961d01895b273b19ead0a1742d56dae85a6090a371aec4a995b065b226c5db1768454cbab95db947fd5eea3443fe89979a529fcdb06bf4d6af56
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# Legion Changelog
|
|
2
2
|
|
|
3
|
+
## [1.4.122] - 2026-03-22
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- GraphQL API via `graphql-ruby` gem: `POST /api/graphql` endpoint alongside existing REST API
|
|
7
|
+
- Schema types: QueryType, WorkerType, TaskType, ExtensionType, TeamType with field-level resolvers
|
|
8
|
+
- Resolver modules for workers, tasks, extensions, and teams (safe stubs with `defined?` guards)
|
|
9
|
+
- 45 new specs for GraphQL schema, queries, and error handling
|
|
10
|
+
|
|
11
|
+
## [1.4.121] - 2026-03-22
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
- Route `/api/llm/chat` through full Legion pipeline (Ingress -> RBAC -> Events -> Task -> Gateway metering -> LLM) when `lex-llm-gateway` is loaded
|
|
15
|
+
- `gateway_available?` helper to detect gateway runner presence
|
|
16
|
+
- Proper result extraction from `ingress_result[:result]` with support for RubyLLM response objects, error hashes, and plain strings
|
|
17
|
+
- Error logging in async LLM rescue block (previously silent)
|
|
18
|
+
|
|
19
|
+
## [1.4.120] - 2026-03-22
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
- Comprehensive logging throughout the framework: 55 files, 443 lines of `.info`, `.warn`, `.error`, `.debug` calls
|
|
23
|
+
- API routes: every non-2xx response logs at warn (4xx) or error (5xx), every mutation logs at info, debug for request entry
|
|
24
|
+
- Core framework: ingress, runner, extensions, actors, service lifecycle, readiness, events all log state transitions
|
|
25
|
+
- Extension system: autobuild, actor hooking, transport setup, builder phases all log at debug/info
|
|
26
|
+
- Digital worker lifecycle, capacity model, catalog, guardrails, webhooks, alerts, audit, telemetry all instrumented
|
|
27
|
+
- CLI error handler logs matched patterns (warn) and unhandled errors (error)
|
|
28
|
+
|
|
3
29
|
## [1.4.119] - 2026-03-22
|
|
4
30
|
|
|
5
31
|
### Added
|
data/legionio.gemspec
CHANGED
|
@@ -42,6 +42,7 @@ Gem::Specification.new do |spec|
|
|
|
42
42
|
spec.add_dependency 'concurrent-ruby', '>= 1.2'
|
|
43
43
|
spec.add_dependency 'concurrent-ruby-ext', '>= 1.2'
|
|
44
44
|
spec.add_dependency 'daemons', '>= 1.4'
|
|
45
|
+
spec.add_dependency 'graphql', '>= 2.0'
|
|
45
46
|
spec.add_dependency 'oj', '>= 3.16'
|
|
46
47
|
spec.add_dependency 'puma', '>= 6.0'
|
|
47
48
|
spec.add_dependency 'rackup', '>= 2.0'
|
data/lib/legion/alerts.rb
CHANGED
|
@@ -40,6 +40,8 @@ module Legion
|
|
|
40
40
|
fired = []
|
|
41
41
|
@rules.each do |rule|
|
|
42
42
|
next unless event_matches?(event_name, rule.event_pattern)
|
|
43
|
+
|
|
44
|
+
Legion::Logging.debug "[Alerts] evaluating rule=#{rule.name} for event=#{event_name}" if defined?(Legion::Logging)
|
|
43
45
|
next unless condition_met?(rule, event_name)
|
|
44
46
|
next if in_cooldown?(rule)
|
|
45
47
|
|
|
@@ -81,12 +83,14 @@ module Legion
|
|
|
81
83
|
alert = { rule: rule.name, event: event_name, severity: rule.severity,
|
|
82
84
|
payload: payload, fired_at: Time.now.utc }
|
|
83
85
|
|
|
86
|
+
Legion::Logging.info "[Alerts] alert fired: rule=#{rule.name} event=#{event_name} severity=#{rule.severity}" if defined?(Legion::Logging)
|
|
87
|
+
|
|
84
88
|
(rule.channels || []).each do |channel|
|
|
85
89
|
case channel.to_sym
|
|
86
90
|
when :events
|
|
87
91
|
Legion::Events.emit('alert.fired', alert) if defined?(Legion::Events)
|
|
88
92
|
when :log
|
|
89
|
-
Legion::Logging.warn "[
|
|
93
|
+
Legion::Logging.warn "[Alerts] #{rule.name}: #{event_name} (#{rule.severity})" if defined?(Legion::Logging)
|
|
90
94
|
when :webhook
|
|
91
95
|
Legion::Webhooks.dispatch('alert.fired', alert) if defined?(Legion::Webhooks)
|
|
92
96
|
end
|
data/lib/legion/api/audit.rb
CHANGED
|
@@ -4,7 +4,7 @@ module Legion
|
|
|
4
4
|
class API < Sinatra::Base
|
|
5
5
|
module Routes
|
|
6
6
|
module Audit
|
|
7
|
-
def self.registered(app)
|
|
7
|
+
def self.registered(app) # rubocop:disable Metrics/AbcSize
|
|
8
8
|
app.get '/api/audit' do
|
|
9
9
|
require_data!
|
|
10
10
|
dataset = Legion::Data::Model::AuditLog.order(Sequel.desc(:id))
|
|
@@ -15,15 +15,24 @@ module Legion
|
|
|
15
15
|
dataset = dataset.where { created_at >= Time.parse(params[:since]) } if params[:since]
|
|
16
16
|
dataset = dataset.where { created_at <= Time.parse(params[:until]) } if params[:until]
|
|
17
17
|
json_collection(dataset)
|
|
18
|
+
rescue StandardError => e
|
|
19
|
+
Legion::Logging.error "API GET /api/audit: #{e.class} — #{e.message}"
|
|
20
|
+
json_error('audit_error', e.message, status_code: 500)
|
|
18
21
|
end
|
|
19
22
|
|
|
20
23
|
app.get '/api/audit/verify' do
|
|
21
24
|
require_data!
|
|
22
|
-
|
|
25
|
+
unless defined?(Legion::Extensions::Audit::Runners::Audit)
|
|
26
|
+
Legion::Logging.warn 'API GET /api/audit/verify returned 503: lex-audit is not loaded'
|
|
27
|
+
halt 503, json_error('unavailable', 'lex-audit is not loaded', status_code: 503)
|
|
28
|
+
end
|
|
23
29
|
|
|
24
30
|
runner = Object.new.extend(Legion::Extensions::Audit::Runners::Audit)
|
|
25
31
|
result = runner.verify
|
|
26
32
|
json_response(result)
|
|
33
|
+
rescue StandardError => e
|
|
34
|
+
Legion::Logging.error "API GET /api/audit/verify: #{e.class} — #{e.message}"
|
|
35
|
+
json_error('audit_error', e.message, status_code: 500)
|
|
27
36
|
end
|
|
28
37
|
end
|
|
29
38
|
end
|
data/lib/legion/api/auth.rb
CHANGED
|
@@ -10,16 +10,21 @@ module Legion
|
|
|
10
10
|
|
|
11
11
|
def self.register_token_exchange(app) # rubocop:disable Metrics/MethodLength
|
|
12
12
|
app.post '/api/auth/token' do
|
|
13
|
+
Legion::Logging.debug "API: POST /api/auth/token params=#{params.keys}"
|
|
13
14
|
body = parse_request_body
|
|
14
15
|
grant_type = body[:grant_type]
|
|
15
16
|
subject_token = body[:subject_token]
|
|
16
17
|
|
|
17
18
|
unless grant_type == 'urn:ietf:params:oauth:grant-type:token-exchange'
|
|
19
|
+
Legion::Logging.warn "API POST /api/auth/token returned 400: unsupported grant_type=#{grant_type}"
|
|
18
20
|
halt 400, json_error('unsupported_grant_type', 'expected urn:ietf:params:oauth:grant-type:token-exchange',
|
|
19
21
|
status_code: 400)
|
|
20
22
|
end
|
|
21
23
|
|
|
22
|
-
|
|
24
|
+
unless subject_token
|
|
25
|
+
Legion::Logging.warn 'API POST /api/auth/token returned 400: subject_token is required'
|
|
26
|
+
halt 400, json_error('missing_subject_token', 'subject_token is required', status_code: 400)
|
|
27
|
+
end
|
|
23
28
|
|
|
24
29
|
unless defined?(Legion::Crypt::JWT) && Legion::Crypt::JWT.respond_to?(:verify_with_jwks)
|
|
25
30
|
halt 501, json_error('jwks_validation_not_available', 'legion-crypt JWKS support not loaded',
|
|
@@ -28,7 +33,10 @@ module Legion
|
|
|
28
33
|
|
|
29
34
|
rbac_settings = (Legion::Settings[:rbac].is_a?(Hash) && Legion::Settings[:rbac][:entra]) || {}
|
|
30
35
|
tenant_id = rbac_settings[:tenant_id]
|
|
31
|
-
|
|
36
|
+
unless tenant_id
|
|
37
|
+
Legion::Logging.error 'API POST /api/auth/token returned 500: rbac.entra.tenant_id not set'
|
|
38
|
+
halt 500, json_error('entra_tenant_not_configured', 'rbac.entra.tenant_id not set', status_code: 500)
|
|
39
|
+
end
|
|
32
40
|
|
|
33
41
|
jwks_url = "https://login.microsoftonline.com/#{tenant_id}/discovery/v2.0/keys"
|
|
34
42
|
issuer = "https://login.microsoftonline.com/#{tenant_id}/v2.0"
|
|
@@ -38,10 +46,13 @@ module Legion
|
|
|
38
46
|
subject_token, jwks_url: jwks_url, issuers: [issuer]
|
|
39
47
|
)
|
|
40
48
|
rescue Legion::Crypt::JWT::ExpiredTokenError
|
|
49
|
+
Legion::Logging.warn 'API POST /api/auth/token returned 401: Entra token has expired'
|
|
41
50
|
halt 401, json_error('token_expired', 'Entra token has expired', status_code: 401)
|
|
42
51
|
rescue Legion::Crypt::JWT::InvalidTokenError => e
|
|
52
|
+
Legion::Logging.warn "API POST /api/auth/token returned 401: #{e.message}"
|
|
43
53
|
halt 401, json_error('invalid_token', e.message, status_code: 401)
|
|
44
54
|
rescue Legion::Crypt::JWT::Error => e
|
|
55
|
+
Legion::Logging.error "API POST /api/auth/token returned 502: #{e.message}"
|
|
45
56
|
halt 502, json_error('identity_provider_unavailable', e.message, status_code: 502)
|
|
46
57
|
end
|
|
47
58
|
|
|
@@ -63,6 +74,7 @@ module Legion
|
|
|
63
74
|
roles: mapped[:roles], ttl: ttl
|
|
64
75
|
)
|
|
65
76
|
|
|
77
|
+
Legion::Logging.info "API: issued human token for sub=#{mapped[:sub]} roles=#{mapped[:roles]&.join(',')}"
|
|
66
78
|
json_response({
|
|
67
79
|
access_token: token,
|
|
68
80
|
token_type: 'Bearer',
|
|
@@ -44,7 +44,10 @@ module Legion
|
|
|
44
44
|
def self.register_authorize(app)
|
|
45
45
|
app.get '/api/auth/authorize' do
|
|
46
46
|
entra = Routes::AuthHuman.resolve_entra_settings
|
|
47
|
-
|
|
47
|
+
unless entra[:tenant_id] && entra[:client_id]
|
|
48
|
+
Legion::Logging.error 'API GET /api/auth/authorize returned 500: Entra OAuth settings are missing'
|
|
49
|
+
halt 500, json_error('entra_not_configured', 'Entra OAuth settings are missing', status_code: 500)
|
|
50
|
+
end
|
|
48
51
|
|
|
49
52
|
state = Legion::Crypt::JWT.issue(
|
|
50
53
|
{ nonce: SecureRandom.hex(16), purpose: 'oauth_state' },
|
|
@@ -63,27 +66,43 @@ module Legion
|
|
|
63
66
|
end
|
|
64
67
|
end
|
|
65
68
|
|
|
66
|
-
def self.register_callback(app) # rubocop:disable Metrics/AbcSize
|
|
69
|
+
def self.register_callback(app) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
67
70
|
app.get '/api/auth/callback' do
|
|
68
71
|
entra = Routes::AuthHuman.resolve_entra_settings
|
|
69
|
-
|
|
72
|
+
unless entra[:tenant_id] && entra[:client_id]
|
|
73
|
+
Legion::Logging.error 'API GET /api/auth/callback returned 500: Entra OAuth settings are missing'
|
|
74
|
+
halt 500, json_error('entra_not_configured', 'Entra OAuth settings are missing', status_code: 500)
|
|
75
|
+
end
|
|
70
76
|
|
|
71
|
-
|
|
72
|
-
|
|
77
|
+
if params[:error]
|
|
78
|
+
Legion::Logging.warn "API GET /api/auth/callback returned 400: #{params[:error_description] || params[:error]}"
|
|
79
|
+
halt 400, json_error('oauth_error', params[:error_description] || params[:error], status_code: 400)
|
|
80
|
+
end
|
|
81
|
+
unless params[:code]
|
|
82
|
+
Legion::Logging.warn 'API GET /api/auth/callback returned 400: authorization code is required'
|
|
83
|
+
halt 400, json_error('missing_code', 'authorization code is required', status_code: 400)
|
|
84
|
+
end
|
|
73
85
|
|
|
74
86
|
if params[:state]
|
|
75
87
|
begin
|
|
76
88
|
Legion::Crypt::JWT.verify(params[:state])
|
|
77
89
|
rescue Legion::Crypt::JWT::Error
|
|
90
|
+
Legion::Logging.warn 'API GET /api/auth/callback returned 400: CSRF state token is invalid or expired'
|
|
78
91
|
halt 400, json_error('invalid_state', 'CSRF state token is invalid or expired', status_code: 400)
|
|
79
92
|
end
|
|
80
93
|
end
|
|
81
94
|
|
|
82
95
|
token_response = Routes::AuthHuman.exchange_code(entra, params[:code])
|
|
83
|
-
|
|
96
|
+
unless token_response
|
|
97
|
+
Legion::Logging.error 'API GET /api/auth/callback returned 502: Failed to exchange code for tokens'
|
|
98
|
+
halt 502, json_error('token_exchange_failed', 'Failed to exchange code for tokens', status_code: 502)
|
|
99
|
+
end
|
|
84
100
|
|
|
85
101
|
id_token = token_response[:id_token] || token_response['id_token']
|
|
86
|
-
|
|
102
|
+
unless id_token
|
|
103
|
+
Legion::Logging.error 'API GET /api/auth/callback returned 502: Entra did not return an id_token'
|
|
104
|
+
halt 502, json_error('no_id_token', 'Entra did not return an id_token', status_code: 502)
|
|
105
|
+
end
|
|
87
106
|
|
|
88
107
|
jwks_url = "https://login.microsoftonline.com/#{entra[:tenant_id]}/discovery/v2.0/keys"
|
|
89
108
|
issuer = "https://login.microsoftonline.com/#{entra[:tenant_id]}/v2.0"
|
|
@@ -91,6 +110,7 @@ module Legion
|
|
|
91
110
|
begin
|
|
92
111
|
claims = Legion::Crypt::JWT.verify_with_jwks(id_token, jwks_url: jwks_url, issuers: [issuer])
|
|
93
112
|
rescue Legion::Crypt::JWT::Error => e
|
|
113
|
+
Legion::Logging.warn "API GET /api/auth/callback returned 401: #{e.message}"
|
|
94
114
|
halt 401, json_error('invalid_id_token', e.message, status_code: 401)
|
|
95
115
|
end
|
|
96
116
|
|
|
@@ -110,6 +130,7 @@ module Legion
|
|
|
110
130
|
msid: mapped[:sub], name: mapped[:name], roles: mapped[:roles], ttl: ttl
|
|
111
131
|
)
|
|
112
132
|
|
|
133
|
+
Legion::Logging.info "API: human OAuth callback issued token for sub=#{mapped[:sub]}"
|
|
113
134
|
if request.env['HTTP_ACCEPT']&.include?('application/json')
|
|
114
135
|
json_response({
|
|
115
136
|
access_token: token,
|
|
@@ -8,18 +8,23 @@ module Legion
|
|
|
8
8
|
register_worker_token_exchange(app)
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
-
def self.register_worker_token_exchange(app) # rubocop:disable Metrics/MethodLength
|
|
11
|
+
def self.register_worker_token_exchange(app) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
12
12
|
app.post '/api/auth/worker-token' do
|
|
13
|
+
Legion::Logging.debug "API: POST /api/auth/worker-token params=#{params.keys}"
|
|
13
14
|
body = parse_request_body
|
|
14
15
|
grant_type = body[:grant_type]
|
|
15
16
|
entra_token = body[:entra_token]
|
|
16
17
|
|
|
17
18
|
unless grant_type == 'client_credentials'
|
|
19
|
+
Legion::Logging.warn "API POST /api/auth/worker-token returned 400: unsupported grant_type=#{grant_type}"
|
|
18
20
|
halt 400, json_error('unsupported_grant_type', 'grant_type must be client_credentials',
|
|
19
21
|
status_code: 400)
|
|
20
22
|
end
|
|
21
23
|
|
|
22
|
-
|
|
24
|
+
unless entra_token
|
|
25
|
+
Legion::Logging.warn 'API POST /api/auth/worker-token returned 400: entra_token is required'
|
|
26
|
+
halt 400, json_error('missing_entra_token', 'entra_token is required', status_code: 400)
|
|
27
|
+
end
|
|
23
28
|
|
|
24
29
|
unless defined?(Legion::Crypt::JWT) && Legion::Crypt::JWT.respond_to?(:verify_with_jwks)
|
|
25
30
|
halt 501, json_error('jwks_validation_not_available',
|
|
@@ -29,6 +34,7 @@ module Legion
|
|
|
29
34
|
entra_settings = Routes::AuthWorker.resolve_entra_settings
|
|
30
35
|
tenant_id = entra_settings[:tenant_id]
|
|
31
36
|
unless tenant_id
|
|
37
|
+
Legion::Logging.error 'API POST /api/auth/worker-token returned 500: Entra tenant_id is not configured'
|
|
32
38
|
halt 500, json_error('entra_tenant_not_configured',
|
|
33
39
|
'Entra tenant_id is not configured', status_code: 500)
|
|
34
40
|
end
|
|
@@ -41,25 +47,33 @@ module Legion
|
|
|
41
47
|
entra_token, jwks_url: jwks_url, issuers: [issuer]
|
|
42
48
|
)
|
|
43
49
|
rescue Legion::Crypt::JWT::ExpiredTokenError
|
|
50
|
+
Legion::Logging.warn 'API POST /api/auth/worker-token returned 401: Entra token has expired'
|
|
44
51
|
halt 401, json_error('token_expired', 'Entra token has expired', status_code: 401)
|
|
45
52
|
rescue Legion::Crypt::JWT::InvalidTokenError => e
|
|
53
|
+
Legion::Logging.warn "API POST /api/auth/worker-token returned 401: #{e.message}"
|
|
46
54
|
halt 401, json_error('invalid_token', e.message, status_code: 401)
|
|
47
55
|
rescue Legion::Crypt::JWT::Error => e
|
|
56
|
+
Legion::Logging.error "API POST /api/auth/worker-token returned 502: #{e.message}"
|
|
48
57
|
halt 502, json_error('identity_provider_unavailable', e.message, status_code: 502)
|
|
49
58
|
end
|
|
50
59
|
|
|
51
60
|
app_id = claims[:appid] || claims[:azp] || claims['appid'] || claims['azp']
|
|
52
|
-
|
|
61
|
+
unless app_id
|
|
62
|
+
Legion::Logging.warn 'API POST /api/auth/worker-token returned 401: missing appid claim'
|
|
63
|
+
halt 401, json_error('invalid_token', 'missing appid claim', status_code: 401)
|
|
64
|
+
end
|
|
53
65
|
|
|
54
66
|
halt 503, json_error('data_unavailable', 'legion-data not connected', status_code: 503) unless defined?(Legion::Data::Model::DigitalWorker)
|
|
55
67
|
|
|
56
68
|
worker = Legion::Data::Model::DigitalWorker.first(entra_app_id: app_id)
|
|
57
69
|
unless worker
|
|
70
|
+
Legion::Logging.warn "API POST /api/auth/worker-token returned 404: no worker for entra_app_id=#{app_id}"
|
|
58
71
|
halt 404, json_error('worker_not_found',
|
|
59
72
|
"no worker registered for entra_app_id #{app_id}", status_code: 404)
|
|
60
73
|
end
|
|
61
74
|
|
|
62
75
|
unless worker.lifecycle_state == 'active'
|
|
76
|
+
Legion::Logging.warn "API POST /api/auth/worker-token returned 403: worker #{worker.worker_id} is in #{worker.lifecycle_state} state"
|
|
63
77
|
halt 403, json_error('worker_not_active',
|
|
64
78
|
"worker is in #{worker.lifecycle_state} state", status_code: 403)
|
|
65
79
|
end
|
|
@@ -69,6 +83,7 @@ module Legion
|
|
|
69
83
|
worker_id: worker.worker_id, owner_msid: worker.owner_msid, ttl: ttl
|
|
70
84
|
)
|
|
71
85
|
|
|
86
|
+
Legion::Logging.info "API: issued worker token for worker_id=#{worker.worker_id}"
|
|
72
87
|
json_response({
|
|
73
88
|
access_token: token,
|
|
74
89
|
token_type: 'Bearer',
|
data/lib/legion/api/capacity.rb
CHANGED
|
@@ -11,6 +11,9 @@ module Legion
|
|
|
11
11
|
workers = Routes::Capacity.fetch_worker_list
|
|
12
12
|
model = Legion::Capacity::Model.new(workers: workers)
|
|
13
13
|
json_response(model.aggregate)
|
|
14
|
+
rescue StandardError => e
|
|
15
|
+
Legion::Logging.error "API GET /api/capacity: #{e.class} — #{e.message}"
|
|
16
|
+
json_error('capacity_error', e.message, status_code: 500)
|
|
14
17
|
end
|
|
15
18
|
|
|
16
19
|
app.get '/api/capacity/forecast' do
|
|
@@ -21,12 +24,18 @@ module Legion
|
|
|
21
24
|
growth_rate: (params[:growth_rate] || 0).to_f
|
|
22
25
|
)
|
|
23
26
|
json_response(forecast)
|
|
27
|
+
rescue StandardError => e
|
|
28
|
+
Legion::Logging.error "API GET /api/capacity/forecast: #{e.class} — #{e.message}"
|
|
29
|
+
json_error('capacity_error', e.message, status_code: 500)
|
|
24
30
|
end
|
|
25
31
|
|
|
26
32
|
app.get '/api/capacity/workers' do
|
|
27
33
|
workers = Routes::Capacity.fetch_worker_list
|
|
28
34
|
model = Legion::Capacity::Model.new(workers: workers)
|
|
29
35
|
json_response(model.per_worker_stats)
|
|
36
|
+
rescue StandardError => e
|
|
37
|
+
Legion::Logging.error "API GET /api/capacity/workers: #{e.class} — #{e.message}"
|
|
38
|
+
json_error('capacity_error', e.message, status_code: 500)
|
|
30
39
|
end
|
|
31
40
|
end
|
|
32
41
|
|
data/lib/legion/api/chains.rb
CHANGED
|
@@ -4,7 +4,7 @@ module Legion
|
|
|
4
4
|
class API < Sinatra::Base
|
|
5
5
|
module Routes
|
|
6
6
|
module Chains
|
|
7
|
-
def self.registered(app)
|
|
7
|
+
def self.registered(app) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
8
8
|
app.get '/api/chains' do
|
|
9
9
|
require_data!
|
|
10
10
|
halt 501, json_error('not_implemented', 'chain data model is not yet available', status_code: 501) unless Legion::Data::Model.const_defined?(:Chain)
|
|
@@ -13,42 +13,62 @@ module Legion
|
|
|
13
13
|
end
|
|
14
14
|
|
|
15
15
|
app.post '/api/chains' do
|
|
16
|
+
Legion::Logging.debug "API: POST /api/chains params=#{params.keys}"
|
|
16
17
|
require_data!
|
|
17
|
-
|
|
18
|
+
unless Legion::Data::Model.const_defined?(:Chain)
|
|
19
|
+
Legion::Logging.warn 'API POST /api/chains returned 501: chain data model is not yet available'
|
|
20
|
+
halt 501, json_error('not_implemented', 'chain data model is not yet available', status_code: 501)
|
|
21
|
+
end
|
|
18
22
|
|
|
19
23
|
body = parse_request_body
|
|
20
|
-
|
|
24
|
+
unless body[:name]
|
|
25
|
+
Legion::Logging.warn 'API POST /api/chains returned 422: name is required'
|
|
26
|
+
halt 422, json_error('missing_field', 'name is required', status_code: 422)
|
|
27
|
+
end
|
|
21
28
|
|
|
22
29
|
id = Legion::Data::Model::Chain.insert(body)
|
|
23
30
|
record = Legion::Data::Model::Chain[id]
|
|
31
|
+
Legion::Logging.info "API: created chain #{id} (#{body[:name]})"
|
|
24
32
|
json_response(record.values, status_code: 201)
|
|
25
33
|
end
|
|
26
34
|
|
|
27
35
|
app.get '/api/chains/:id' do
|
|
28
36
|
require_data!
|
|
29
|
-
|
|
37
|
+
unless Legion::Data::Model.const_defined?(:Chain)
|
|
38
|
+
Legion::Logging.warn "API GET /api/chains/#{params[:id]} returned 501: chain data model is not yet available"
|
|
39
|
+
halt 501, json_error('not_implemented', 'chain data model is not yet available', status_code: 501)
|
|
40
|
+
end
|
|
30
41
|
|
|
31
42
|
record = find_or_halt(Legion::Data::Model::Chain, params[:id])
|
|
32
43
|
json_response(record.values)
|
|
33
44
|
end
|
|
34
45
|
|
|
35
46
|
app.put '/api/chains/:id' do
|
|
47
|
+
Legion::Logging.debug "API: PUT /api/chains/#{params[:id]} params=#{params.keys}"
|
|
36
48
|
require_data!
|
|
37
|
-
|
|
49
|
+
unless Legion::Data::Model.const_defined?(:Chain)
|
|
50
|
+
Legion::Logging.warn "API PUT /api/chains/#{params[:id]} returned 501: chain data model is not yet available"
|
|
51
|
+
halt 501, json_error('not_implemented', 'chain data model is not yet available', status_code: 501)
|
|
52
|
+
end
|
|
38
53
|
|
|
39
54
|
record = find_or_halt(Legion::Data::Model::Chain, params[:id])
|
|
40
55
|
body = parse_request_body
|
|
41
56
|
record.update(body)
|
|
42
57
|
record.refresh
|
|
58
|
+
Legion::Logging.info "API: updated chain #{params[:id]}"
|
|
43
59
|
json_response(record.values)
|
|
44
60
|
end
|
|
45
61
|
|
|
46
62
|
app.delete '/api/chains/:id' do
|
|
47
63
|
require_data!
|
|
48
|
-
|
|
64
|
+
unless Legion::Data::Model.const_defined?(:Chain)
|
|
65
|
+
Legion::Logging.warn "API DELETE /api/chains/#{params[:id]} returned 501: chain data model is not yet available"
|
|
66
|
+
halt 501, json_error('not_implemented', 'chain data model is not yet available', status_code: 501)
|
|
67
|
+
end
|
|
49
68
|
|
|
50
69
|
record = find_or_halt(Legion::Data::Model::Chain, params[:id])
|
|
51
70
|
record.delete
|
|
71
|
+
Legion::Logging.info "API: deleted chain #{params[:id]}"
|
|
52
72
|
json_response({ deleted: true })
|
|
53
73
|
end
|
|
54
74
|
end
|
data/lib/legion/api/coldstart.rb
CHANGED
|
@@ -6,13 +6,23 @@ module Legion
|
|
|
6
6
|
module Coldstart
|
|
7
7
|
def self.registered(app)
|
|
8
8
|
app.post '/api/coldstart/ingest' do
|
|
9
|
+
Legion::Logging.debug "API: POST /api/coldstart/ingest params=#{params.keys}"
|
|
9
10
|
body = parse_request_body
|
|
10
11
|
path = body[:path]
|
|
11
|
-
|
|
12
|
+
if path.nil? || path.empty?
|
|
13
|
+
Legion::Logging.warn 'API POST /api/coldstart/ingest returned 422: path is required'
|
|
14
|
+
halt 422, json_error('missing_field', 'path is required', status_code: 422)
|
|
15
|
+
end
|
|
12
16
|
|
|
13
|
-
|
|
17
|
+
unless defined?(Legion::Extensions::Coldstart)
|
|
18
|
+
Legion::Logging.warn 'API POST /api/coldstart/ingest returned 503: lex-coldstart is not loaded'
|
|
19
|
+
halt 503, json_error('coldstart_unavailable', 'lex-coldstart is not loaded', status_code: 503)
|
|
20
|
+
end
|
|
14
21
|
|
|
15
|
-
|
|
22
|
+
unless defined?(Legion::Extensions::Memory)
|
|
23
|
+
Legion::Logging.warn 'API POST /api/coldstart/ingest returned 503: lex-memory is not loaded'
|
|
24
|
+
halt 503, json_error('memory_unavailable', 'lex-memory is not loaded', status_code: 503)
|
|
25
|
+
end
|
|
16
26
|
|
|
17
27
|
runner = Object.new.extend(Legion::Extensions::Coldstart::Runners::Ingest)
|
|
18
28
|
|
|
@@ -24,12 +34,14 @@ module Legion
|
|
|
24
34
|
pattern: body[:pattern] || '**/{CLAUDE,MEMORY}.md'
|
|
25
35
|
)
|
|
26
36
|
else
|
|
37
|
+
Legion::Logging.warn "API POST /api/coldstart/ingest returned 404: path not found: #{path}"
|
|
27
38
|
halt 404, json_error('path_not_found', "path not found: #{path}", status_code: 404)
|
|
28
39
|
end
|
|
29
40
|
|
|
41
|
+
Legion::Logging.info "API: coldstart ingest completed for path=#{path}"
|
|
30
42
|
json_response(result, status_code: 201)
|
|
31
43
|
rescue StandardError => e
|
|
32
|
-
Legion::Logging.error "API coldstart
|
|
44
|
+
Legion::Logging.error "API POST /api/coldstart/ingest: #{e.class} — #{e.message}"
|
|
33
45
|
json_error('execution_error', e.message, status_code: 500)
|
|
34
46
|
end
|
|
35
47
|
end
|
|
@@ -41,7 +41,7 @@ module Legion
|
|
|
41
41
|
end
|
|
42
42
|
end
|
|
43
43
|
|
|
44
|
-
def self.register_function_routes(app)
|
|
44
|
+
def self.register_function_routes(app) # rubocop:disable Metrics/AbcSize
|
|
45
45
|
app.get '/api/extensions/:id/runners/:runner_id/functions' do
|
|
46
46
|
require_data!
|
|
47
47
|
find_or_halt(Legion::Data::Model::Extension, params[:id])
|
|
@@ -60,6 +60,8 @@ module Legion
|
|
|
60
60
|
|
|
61
61
|
app.post '/api/extensions/:id/runners/:runner_id/functions/:function_id/invoke' do
|
|
62
62
|
require_data!
|
|
63
|
+
path = "/api/extensions/#{params[:id]}/runners/#{params[:runner_id]}/functions/#{params[:function_id]}/invoke"
|
|
64
|
+
Legion::Logging.debug "API: POST #{path} params=#{params.keys}"
|
|
63
65
|
find_or_halt(Legion::Data::Model::Extension, params[:id])
|
|
64
66
|
runner = find_or_halt(Legion::Data::Model::Runner, params[:runner_id])
|
|
65
67
|
func = find_or_halt(Legion::Data::Model::Function, params[:function_id])
|
|
@@ -71,11 +73,13 @@ module Legion
|
|
|
71
73
|
check_subtask: body.fetch(:check_subtask, true),
|
|
72
74
|
generate_task: body.fetch(:generate_task, true)
|
|
73
75
|
)
|
|
76
|
+
Legion::Logging.info "API: invoked function #{func.values[:name]} via runner #{runner.values[:namespace]}, task #{result[:task_id]}"
|
|
74
77
|
json_response(result, status_code: 201)
|
|
75
78
|
rescue NameError => e
|
|
79
|
+
Legion::Logging.warn "API POST /api/extensions invoke returned 422: #{e.message}"
|
|
76
80
|
json_error('invalid_runner', e.message, status_code: 422)
|
|
77
81
|
rescue StandardError => e
|
|
78
|
-
Legion::Logging.error "API invoke
|
|
82
|
+
Legion::Logging.error "API POST /api/extensions invoke: #{e.class} — #{e.message}"
|
|
79
83
|
json_error('execution_error', e.message, status_code: 500)
|
|
80
84
|
end
|
|
81
85
|
end
|
data/lib/legion/api/gaia.rb
CHANGED
|
@@ -14,15 +14,19 @@ module Legion
|
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
app.post '/api/channels/teams/webhook' do
|
|
17
|
+
Legion::Logging.debug "API: POST /api/channels/teams/webhook params=#{params.keys}"
|
|
17
18
|
body = request.body.read
|
|
18
19
|
activity = Legion::JSON.load(body)
|
|
19
20
|
|
|
20
21
|
adapter = Routes::Gaia.teams_adapter
|
|
21
|
-
|
|
22
|
+
unless adapter
|
|
23
|
+
Legion::Logging.warn 'API POST /api/channels/teams/webhook returned 503: teams adapter not available'
|
|
24
|
+
halt 503, json_response({ error: 'teams adapter not available' }, status_code: 503)
|
|
25
|
+
end
|
|
22
26
|
|
|
23
27
|
input_frame = adapter.translate_inbound(activity)
|
|
24
28
|
Legion::Gaia.sensory_buffer&.push(input_frame) if defined?(Legion::Gaia)
|
|
25
|
-
|
|
29
|
+
Legion::Logging.info "API: accepted Teams webhook frame_id=#{input_frame&.id}"
|
|
26
30
|
json_response({ status: 'accepted', frame_id: input_frame&.id })
|
|
27
31
|
end
|
|
28
32
|
end
|
data/lib/legion/api/graphql.rb
CHANGED
|
@@ -11,6 +11,7 @@ module Legion
|
|
|
11
11
|
def self.registered(app)
|
|
12
12
|
app.post '/api/graphql' do
|
|
13
13
|
content_type :json
|
|
14
|
+
Legion::Logging.debug "API: POST /api/graphql params=#{params.keys}" if defined?(Legion::Logging)
|
|
14
15
|
|
|
15
16
|
body_str = request.body.read
|
|
16
17
|
payload = body_str.empty? ? {} : Legion::JSON.load(body_str)
|
|
@@ -21,6 +22,7 @@ module Legion
|
|
|
21
22
|
operation_name = payload[:operationName]
|
|
22
23
|
|
|
23
24
|
if query.nil? || query.strip.empty?
|
|
25
|
+
Legion::Logging.warn 'API POST /api/graphql returned 400: query is required' if defined?(Legion::Logging)
|
|
24
26
|
status 400
|
|
25
27
|
next Legion::JSON.dump({
|
|
26
28
|
errors: [{ message: 'query is required' }]
|
|
@@ -37,7 +39,7 @@ module Legion
|
|
|
37
39
|
status 200
|
|
38
40
|
Legion::JSON.dump(result.to_h)
|
|
39
41
|
rescue StandardError => e
|
|
40
|
-
Legion::Logging.error "
|
|
42
|
+
Legion::Logging.error "API POST /api/graphql: #{e.class} — #{e.message}" if defined?(Legion::Logging)
|
|
41
43
|
status 500
|
|
42
44
|
Legion::JSON.dump({ errors: [{ message: e.message }] })
|
|
43
45
|
end
|
data/lib/legion/api/hooks.rb
CHANGED
|
@@ -37,23 +37,36 @@ module Legion
|
|
|
37
37
|
|
|
38
38
|
def self.handle_hook_request(context, request)
|
|
39
39
|
splat_path = request.path_info.sub(%r{^/api/hooks/lex/}, '')
|
|
40
|
+
Legion::Logging.debug "API: #{request.request_method} /api/hooks/lex/#{splat_path}"
|
|
40
41
|
hook_entry = Legion::API.find_hook_by_path(splat_path)
|
|
41
|
-
|
|
42
|
+
if hook_entry.nil?
|
|
43
|
+
Legion::Logging.warn "API #{request.request_method} #{request.path_info} returned 404: no hook registered for '#{splat_path}'"
|
|
44
|
+
context.halt 404, context.json_error('not_found', "no hook registered for '#{splat_path}'", status_code: 404)
|
|
45
|
+
end
|
|
42
46
|
|
|
43
47
|
body = request.request_method == 'POST' ? request.body.read : nil
|
|
44
48
|
hook = hook_entry[:hook_class].new
|
|
45
|
-
|
|
49
|
+
unless hook.verify(request.env, body || '')
|
|
50
|
+
Legion::Logging.warn "API #{request.request_method} #{request.path_info} returned 401: hook verification failed"
|
|
51
|
+
context.halt 401, context.json_error('unauthorized', 'hook verification failed', status_code: 401)
|
|
52
|
+
end
|
|
46
53
|
|
|
47
54
|
payload = build_payload(request, body)
|
|
48
55
|
function = hook.route(request.env, payload)
|
|
49
|
-
|
|
56
|
+
if function.nil?
|
|
57
|
+
Legion::Logging.warn "API #{request.request_method} #{request.path_info} returned 422: hook could not route this event"
|
|
58
|
+
context.halt 422, context.json_error('unhandled_event', 'hook could not route this event', status_code: 422)
|
|
59
|
+
end
|
|
50
60
|
|
|
51
61
|
runner = hook.runner_class || hook_entry[:default_runner]
|
|
52
|
-
|
|
62
|
+
if runner.nil?
|
|
63
|
+
Legion::Logging.error "API #{request.request_method} #{request.path_info} returned 500: no runner class configured for hook '#{splat_path}'"
|
|
64
|
+
context.halt 500, context.json_error('no_runner', 'no runner class configured for this hook', status_code: 500)
|
|
65
|
+
end
|
|
53
66
|
|
|
54
67
|
dispatch_hook(context, payload: payload, runner: runner, function: function)
|
|
55
68
|
rescue StandardError => e
|
|
56
|
-
Legion::Logging.error "
|
|
69
|
+
Legion::Logging.error "API #{request.request_method} #{request.path_info}: #{e.class} — #{e.message}"
|
|
57
70
|
Legion::Logging.error e.backtrace&.first(5)
|
|
58
71
|
context.json_error('internal_error', e.message, status_code: 500)
|
|
59
72
|
end
|
|
@@ -74,6 +87,7 @@ module Legion
|
|
|
74
87
|
payload: payload, runner_class: runner, function: function,
|
|
75
88
|
source: 'hook', check_subtask: true, generate_task: true
|
|
76
89
|
)
|
|
90
|
+
Legion::Logging.info "API: dispatched hook to #{runner}##{function}, task #{result[:task_id]}"
|
|
77
91
|
return render_custom_response(context, result[:response]) if result.is_a?(Hash) && result[:response]
|
|
78
92
|
|
|
79
93
|
context.json_response({ task_id: result[:task_id], status: result[:status] })
|
data/lib/legion/api/lex.rb
CHANGED
|
@@ -33,8 +33,10 @@ module Legion
|
|
|
33
33
|
|
|
34
34
|
def self.handle_lex_request(context, request)
|
|
35
35
|
splat_path = request.path_info.sub(%r{^/api/lex/}, '')
|
|
36
|
+
Legion::Logging.debug "API: POST /api/lex/#{splat_path}"
|
|
36
37
|
route_entry = Legion::API.find_route_by_path(splat_path)
|
|
37
38
|
if route_entry.nil?
|
|
39
|
+
Legion::Logging.warn "API POST /api/lex/#{splat_path} returned 404: no route registered"
|
|
38
40
|
context.halt 404, context.json_error('route_not_found',
|
|
39
41
|
"no route registered for '#{splat_path}'", status_code: 404)
|
|
40
42
|
end
|
|
@@ -47,10 +49,11 @@ module Legion
|
|
|
47
49
|
source: 'lex_route',
|
|
48
50
|
generate_task: true
|
|
49
51
|
)
|
|
52
|
+
Legion::Logging.info "API: LEX route #{splat_path} dispatched to #{route_entry[:runner_class]}, task #{result[:task_id]}"
|
|
50
53
|
context.json_response({ task_id: result[:task_id], status: result[:status],
|
|
51
54
|
result: result[:result] }.compact)
|
|
52
55
|
rescue StandardError => e
|
|
53
|
-
Legion::Logging.error "
|
|
56
|
+
Legion::Logging.error "API POST /api/lex/#{request.path_info.sub(%r{^/api/lex/}, '')}: #{e.class} — #{e.message}"
|
|
54
57
|
Legion::Logging.error e.backtrace&.first(5)
|
|
55
58
|
context.json_error('internal_error', e.message, status_code: 500)
|
|
56
59
|
end
|