legionio 1.4.119 → 1.4.123
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 +31 -0
- data/legionio.gemspec +1 -0
- data/lib/legion/alerts.rb +7 -2
- data/lib/legion/api/acp.rb +8 -4
- data/lib/legion/api/audit.rb +11 -2
- data/lib/legion/api/auth.rb +14 -2
- data/lib/legion/api/auth_human.rb +32 -9
- data/lib/legion/api/auth_saml.rb +6 -3
- data/lib/legion/api/auth_worker.rb +20 -4
- data/lib/legion/api/capacity.rb +11 -1
- data/lib/legion/api/catalog.rb +6 -3
- data/lib/legion/api/chains.rb +26 -6
- data/lib/legion/api/coldstart.rb +16 -4
- data/lib/legion/api/events.rb +2 -1
- data/lib/legion/api/extensions.rb +6 -2
- data/lib/legion/api/gaia.rb +8 -3
- data/lib/legion/api/governance.rb +2 -1
- data/lib/legion/api/graphql/resolvers/extensions.rb +4 -2
- data/lib/legion/api/graphql/resolvers/node.rb +4 -2
- data/lib/legion/api/graphql/resolvers/tasks.rb +4 -2
- data/lib/legion/api/graphql/resolvers/workers.rb +4 -2
- data/lib/legion/api/graphql.rb +3 -1
- data/lib/legion/api/helpers.rb +4 -2
- 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/marketplace.rb +9 -3
- data/lib/legion/api/middleware/auth.rb +13 -5
- data/lib/legion/api/middleware/body_limit.rb +3 -0
- data/lib/legion/api/middleware/rate_limit.rb +3 -1
- data/lib/legion/api/middleware/tenant.rb +4 -1
- data/lib/legion/api/org_chart.rb +2 -1
- data/lib/legion/api/prompts.rb +11 -5
- 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 +17 -7
- data/lib/legion/api/webhooks.rb +5 -1
- data/lib/legion/api/workers.rb +28 -8
- data/lib/legion/api/workflow.rb +2 -1
- data/lib/legion/api.rb +2 -1
- data/lib/legion/audit.rb +3 -2
- data/lib/legion/capacity/model.rb +11 -2
- data/lib/legion/catalog.rb +5 -1
- data/lib/legion/chat/notification_bridge.rb +2 -1
- data/lib/legion/cli/acp_command.rb +3 -1
- data/lib/legion/cli/auth_command.rb +7 -1
- data/lib/legion/cli/chat/agent_registry.rb +2 -1
- data/lib/legion/cli/chat/checkpoint.rb +4 -2
- data/lib/legion/cli/chat/context.rb +4 -3
- data/lib/legion/cli/chat/extension_tool_loader.rb +4 -2
- data/lib/legion/cli/chat/markdown_renderer.rb +2 -1
- data/lib/legion/cli/chat/session.rb +2 -1
- data/lib/legion/cli/chat/subagent.rb +6 -2
- data/lib/legion/cli/chat/tool_registry.rb +4 -3
- data/lib/legion/cli/chat/tools/edit_file.rb +1 -0
- data/lib/legion/cli/chat/tools/read_file.rb +1 -0
- data/lib/legion/cli/chat/tools/run_command.rb +3 -1
- data/lib/legion/cli/chat/tools/save_memory.rb +1 -0
- data/lib/legion/cli/chat/tools/search_content.rb +7 -3
- data/lib/legion/cli/chat/tools/search_files.rb +1 -0
- data/lib/legion/cli/chat/tools/search_memory.rb +1 -0
- data/lib/legion/cli/chat/tools/spawn_agent.rb +1 -0
- data/lib/legion/cli/chat/tools/web_search.rb +2 -0
- data/lib/legion/cli/chat/tools/write_file.rb +1 -0
- data/lib/legion/cli/chat/web_search.rb +4 -2
- data/lib/legion/cli/chat_command.rb +21 -8
- data/lib/legion/cli/check/privacy_check.rb +6 -3
- data/lib/legion/cli/check_command.rb +4 -3
- data/lib/legion/cli/coldstart_command.rb +4 -2
- data/lib/legion/cli/config_command.rb +2 -1
- data/lib/legion/cli/config_scaffold.rb +2 -1
- data/lib/legion/cli/connection.rb +2 -2
- data/lib/legion/cli/cost/data_client.rb +2 -1
- data/lib/legion/cli/dashboard/data_fetcher.rb +2 -1
- data/lib/legion/cli/dashboard_command.rb +1 -0
- data/lib/legion/cli/detect_command.rb +2 -1
- data/lib/legion/cli/doctor/bundle_check.rb +2 -1
- data/lib/legion/cli/doctor/cache_check.rb +2 -1
- data/lib/legion/cli/doctor/database_check.rb +2 -1
- data/lib/legion/cli/doctor/extensions_check.rb +2 -1
- data/lib/legion/cli/doctor/pid_check.rb +2 -1
- data/lib/legion/cli/doctor/rabbitmq_check.rb +4 -2
- data/lib/legion/cli/doctor/vault_check.rb +2 -1
- data/lib/legion/cli/doctor_command.rb +1 -0
- data/lib/legion/cli/error_handler.rb +9 -1
- data/lib/legion/cli/gaia_command.rb +4 -2
- data/lib/legion/cli/init/environment_detector.rb +4 -2
- data/lib/legion/cli/lex_cli_manifest.rb +2 -1
- data/lib/legion/cli/lex_command.rb +8 -4
- data/lib/legion/cli/llm_command.rb +9 -5
- data/lib/legion/cli/marketplace_command.rb +2 -1
- data/lib/legion/cli/payroll_command.rb +3 -0
- data/lib/legion/cli/plan_command.rb +2 -1
- data/lib/legion/cli/setup_command.rb +8 -4
- data/lib/legion/cli/start.rb +2 -1
- data/lib/legion/cli/status.rb +2 -1
- data/lib/legion/cli/task_command.rb +6 -3
- data/lib/legion/cli/tty_command.rb +1 -0
- data/lib/legion/cli/update_command.rb +4 -2
- data/lib/legion/cli.rb +10 -4
- data/lib/legion/cluster/leader.rb +2 -1
- data/lib/legion/cluster/lock.rb +8 -4
- 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/digital_worker/value_metrics.rb +2 -1
- data/lib/legion/docs/site_generator.rb +8 -6
- data/lib/legion/events.rb +3 -2
- data/lib/legion/extensions/actors/every.rb +7 -1
- data/lib/legion/extensions/actors/subscription.rb +9 -6
- 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 +11 -3
- data/lib/legion/extensions/permissions.rb +6 -3
- data/lib/legion/extensions/transport.rb +3 -1
- data/lib/legion/extensions.rb +12 -6
- data/lib/legion/graph/builder.rb +4 -1
- data/lib/legion/graph/exporter.rb +2 -0
- data/lib/legion/guardrails.rb +10 -3
- data/lib/legion/ingress.rb +14 -2
- data/lib/legion/lock.rb +8 -4
- data/lib/legion/metrics.rb +2 -1
- data/lib/legion/notebook/renderer.rb +2 -1
- data/lib/legion/phi/access_log.rb +2 -1
- data/lib/legion/phi/erasure.rb +4 -2
- data/lib/legion/phi.rb +4 -2
- data/lib/legion/process.rb +8 -2
- data/lib/legion/process_role.rb +6 -1
- data/lib/legion/readiness.rb +10 -4
- data/lib/legion/region/failover.rb +2 -1
- data/lib/legion/region.rb +14 -7
- data/lib/legion/registry/governance.rb +2 -1
- data/lib/legion/registry/persistence.rb +2 -1
- data/lib/legion/runner/status.rb +8 -8
- data/lib/legion/runner.rb +7 -3
- data/lib/legion/service.rb +20 -6
- data/lib/legion/telemetry/open_inference.rb +18 -9
- data/lib/legion/telemetry/safety_metrics.rb +4 -2
- data/lib/legion/telemetry.rb +16 -8
- data/lib/legion/tenants.rb +2 -1
- data/lib/legion/trace_search.rb +3 -0
- data/lib/legion/version.rb +1 -1
- data/lib/legion/webhooks.rb +17 -4
- 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: 6d78fc201a2601de3ce04849d24c9234c75bd77f6cdb3b46ca700f7cdd6fcc9f
|
|
4
|
+
data.tar.gz: 5746d467cb709dbf02837ebd2d4288dbe137d234c8f6e1e3b3445dcd899d3515
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 106274f7798ddf16c3da87f2c0ba204e98c0f8c8d9dedf312376fc8cbde8dbac447306cfd76c6cb0a5e210a22db9304fc37f5e3305f1cd80d918514aad80b4b4
|
|
7
|
+
data.tar.gz: 54ebdc455f6f5f86a0564809a67d6f3edffc20b7c3f438956382d32bff16d2b3469f51d4a48c1a30c312edfe0ad2d81abacc4962a96e79e7669d652cb4644605
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,36 @@
|
|
|
1
1
|
# Legion Changelog
|
|
2
2
|
|
|
3
|
+
## [1.4.123] - 2026-03-22
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
- Add logging to silent rescue blocks: all rescue blocks now capture the exception variable and emit `Legion::Logging.debug` or `.warn` calls so errors are visible in logs rather than silently swallowed
|
|
7
|
+
|
|
8
|
+
## [1.4.122] - 2026-03-22
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- GraphQL API via `graphql-ruby` gem: `POST /api/graphql` endpoint alongside existing REST API
|
|
12
|
+
- Schema types: QueryType, WorkerType, TaskType, ExtensionType, TeamType with field-level resolvers
|
|
13
|
+
- Resolver modules for workers, tasks, extensions, and teams (safe stubs with `defined?` guards)
|
|
14
|
+
- 45 new specs for GraphQL schema, queries, and error handling
|
|
15
|
+
|
|
16
|
+
## [1.4.121] - 2026-03-22
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
- Route `/api/llm/chat` through full Legion pipeline (Ingress -> RBAC -> Events -> Task -> Gateway metering -> LLM) when `lex-llm-gateway` is loaded
|
|
20
|
+
- `gateway_available?` helper to detect gateway runner presence
|
|
21
|
+
- Proper result extraction from `ingress_result[:result]` with support for RubyLLM response objects, error hashes, and plain strings
|
|
22
|
+
- Error logging in async LLM rescue block (previously silent)
|
|
23
|
+
|
|
24
|
+
## [1.4.120] - 2026-03-22
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
- Comprehensive logging throughout the framework: 55 files, 443 lines of `.info`, `.warn`, `.error`, `.debug` calls
|
|
28
|
+
- API routes: every non-2xx response logs at warn (4xx) or error (5xx), every mutation logs at info, debug for request entry
|
|
29
|
+
- Core framework: ingress, runner, extensions, actors, service lifecycle, readiness, events all log state transitions
|
|
30
|
+
- Extension system: autobuild, actor hooking, transport setup, builder phases all log at debug/info
|
|
31
|
+
- Digital worker lifecycle, capacity model, catalog, guardrails, webhooks, alerts, audit, telemetry all instrumented
|
|
32
|
+
- CLI error handler logs matched patterns (warn) and unhandled errors (error)
|
|
33
|
+
|
|
3
34
|
## [1.4.119] - 2026-03-22
|
|
4
35
|
|
|
5
36
|
### 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
|
|
@@ -113,7 +117,8 @@ module Legion
|
|
|
113
117
|
def load_rules
|
|
114
118
|
custom = begin
|
|
115
119
|
Legion::Settings[:alerts][:rules]
|
|
116
|
-
rescue StandardError
|
|
120
|
+
rescue StandardError => e
|
|
121
|
+
Legion::Logging.debug "Alerts#load_rules failed to read settings: #{e.message}" if defined?(Legion::Logging)
|
|
117
122
|
nil
|
|
118
123
|
end
|
|
119
124
|
custom && !custom.empty? ? custom : DEFAULT_RULES
|
data/lib/legion/api/acp.rb
CHANGED
|
@@ -7,12 +7,14 @@ module Legion
|
|
|
7
7
|
def build_agent_card
|
|
8
8
|
name = begin
|
|
9
9
|
Legion::Settings[:client][:name]
|
|
10
|
-
rescue StandardError
|
|
10
|
+
rescue StandardError => e
|
|
11
|
+
Legion::Logging.debug "Acp#build_agent_card failed to read client name: #{e.message}" if defined?(Legion::Logging)
|
|
11
12
|
'legion'
|
|
12
13
|
end
|
|
13
14
|
port = begin
|
|
14
15
|
settings.port || 4567
|
|
15
|
-
rescue StandardError
|
|
16
|
+
rescue StandardError => e
|
|
17
|
+
Legion::Logging.debug "Acp#build_agent_card failed to read port: #{e.message}" if defined?(Legion::Logging)
|
|
16
18
|
4567
|
|
17
19
|
end
|
|
18
20
|
{
|
|
@@ -34,7 +36,8 @@ module Legion
|
|
|
34
36
|
else
|
|
35
37
|
[]
|
|
36
38
|
end
|
|
37
|
-
rescue StandardError
|
|
39
|
+
rescue StandardError => e
|
|
40
|
+
Legion::Logging.warn "Acp#discover_capabilities failed: #{e.message}" if defined?(Legion::Logging)
|
|
38
41
|
[]
|
|
39
42
|
end
|
|
40
43
|
|
|
@@ -42,7 +45,8 @@ module Legion
|
|
|
42
45
|
return nil unless defined?(Legion::Data)
|
|
43
46
|
|
|
44
47
|
Legion::Data::Model::Task[id.to_i]&.values
|
|
45
|
-
rescue StandardError
|
|
48
|
+
rescue StandardError => e
|
|
49
|
+
Legion::Logging.warn "Acp#find_task failed for id=#{id}: #{e.message}" if defined?(Legion::Logging)
|
|
46
50
|
nil
|
|
47
51
|
end
|
|
48
52
|
|
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',
|
|
@@ -20,7 +20,8 @@ module Legion
|
|
|
20
20
|
return entra if entra.is_a?(Hash)
|
|
21
21
|
|
|
22
22
|
{}
|
|
23
|
-
rescue StandardError
|
|
23
|
+
rescue StandardError => e
|
|
24
|
+
Legion::Logging.debug "AuthHuman#resolve_entra_settings failed: #{e.message}" if defined?(Legion::Logging)
|
|
24
25
|
{}
|
|
25
26
|
end
|
|
26
27
|
|
|
@@ -37,14 +38,18 @@ module Legion
|
|
|
37
38
|
return nil unless response.is_a?(Net::HTTPSuccess)
|
|
38
39
|
|
|
39
40
|
Legion::JSON.load(response.body)
|
|
40
|
-
rescue StandardError
|
|
41
|
+
rescue StandardError => e
|
|
42
|
+
Legion::Logging.warn "AuthHuman#exchange_code failed: #{e.message}" if defined?(Legion::Logging)
|
|
41
43
|
nil
|
|
42
44
|
end
|
|
43
45
|
|
|
44
46
|
def self.register_authorize(app)
|
|
45
47
|
app.get '/api/auth/authorize' do
|
|
46
48
|
entra = Routes::AuthHuman.resolve_entra_settings
|
|
47
|
-
|
|
49
|
+
unless entra[:tenant_id] && entra[:client_id]
|
|
50
|
+
Legion::Logging.error 'API GET /api/auth/authorize returned 500: Entra OAuth settings are missing'
|
|
51
|
+
halt 500, json_error('entra_not_configured', 'Entra OAuth settings are missing', status_code: 500)
|
|
52
|
+
end
|
|
48
53
|
|
|
49
54
|
state = Legion::Crypt::JWT.issue(
|
|
50
55
|
{ nonce: SecureRandom.hex(16), purpose: 'oauth_state' },
|
|
@@ -63,27 +68,43 @@ module Legion
|
|
|
63
68
|
end
|
|
64
69
|
end
|
|
65
70
|
|
|
66
|
-
def self.register_callback(app) # rubocop:disable Metrics/AbcSize
|
|
71
|
+
def self.register_callback(app) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
67
72
|
app.get '/api/auth/callback' do
|
|
68
73
|
entra = Routes::AuthHuman.resolve_entra_settings
|
|
69
|
-
|
|
74
|
+
unless entra[:tenant_id] && entra[:client_id]
|
|
75
|
+
Legion::Logging.error 'API GET /api/auth/callback returned 500: Entra OAuth settings are missing'
|
|
76
|
+
halt 500, json_error('entra_not_configured', 'Entra OAuth settings are missing', status_code: 500)
|
|
77
|
+
end
|
|
70
78
|
|
|
71
|
-
|
|
72
|
-
|
|
79
|
+
if params[:error]
|
|
80
|
+
Legion::Logging.warn "API GET /api/auth/callback returned 400: #{params[:error_description] || params[:error]}"
|
|
81
|
+
halt 400, json_error('oauth_error', params[:error_description] || params[:error], status_code: 400)
|
|
82
|
+
end
|
|
83
|
+
unless params[:code]
|
|
84
|
+
Legion::Logging.warn 'API GET /api/auth/callback returned 400: authorization code is required'
|
|
85
|
+
halt 400, json_error('missing_code', 'authorization code is required', status_code: 400)
|
|
86
|
+
end
|
|
73
87
|
|
|
74
88
|
if params[:state]
|
|
75
89
|
begin
|
|
76
90
|
Legion::Crypt::JWT.verify(params[:state])
|
|
77
91
|
rescue Legion::Crypt::JWT::Error
|
|
92
|
+
Legion::Logging.warn 'API GET /api/auth/callback returned 400: CSRF state token is invalid or expired'
|
|
78
93
|
halt 400, json_error('invalid_state', 'CSRF state token is invalid or expired', status_code: 400)
|
|
79
94
|
end
|
|
80
95
|
end
|
|
81
96
|
|
|
82
97
|
token_response = Routes::AuthHuman.exchange_code(entra, params[:code])
|
|
83
|
-
|
|
98
|
+
unless token_response
|
|
99
|
+
Legion::Logging.error 'API GET /api/auth/callback returned 502: Failed to exchange code for tokens'
|
|
100
|
+
halt 502, json_error('token_exchange_failed', 'Failed to exchange code for tokens', status_code: 502)
|
|
101
|
+
end
|
|
84
102
|
|
|
85
103
|
id_token = token_response[:id_token] || token_response['id_token']
|
|
86
|
-
|
|
104
|
+
unless id_token
|
|
105
|
+
Legion::Logging.error 'API GET /api/auth/callback returned 502: Entra did not return an id_token'
|
|
106
|
+
halt 502, json_error('no_id_token', 'Entra did not return an id_token', status_code: 502)
|
|
107
|
+
end
|
|
87
108
|
|
|
88
109
|
jwks_url = "https://login.microsoftonline.com/#{entra[:tenant_id]}/discovery/v2.0/keys"
|
|
89
110
|
issuer = "https://login.microsoftonline.com/#{entra[:tenant_id]}/v2.0"
|
|
@@ -91,6 +112,7 @@ module Legion
|
|
|
91
112
|
begin
|
|
92
113
|
claims = Legion::Crypt::JWT.verify_with_jwks(id_token, jwks_url: jwks_url, issuers: [issuer])
|
|
93
114
|
rescue Legion::Crypt::JWT::Error => e
|
|
115
|
+
Legion::Logging.warn "API GET /api/auth/callback returned 401: #{e.message}"
|
|
94
116
|
halt 401, json_error('invalid_id_token', e.message, status_code: 401)
|
|
95
117
|
end
|
|
96
118
|
|
|
@@ -110,6 +132,7 @@ module Legion
|
|
|
110
132
|
msid: mapped[:sub], name: mapped[:name], roles: mapped[:roles], ttl: ttl
|
|
111
133
|
)
|
|
112
134
|
|
|
135
|
+
Legion::Logging.info "API: human OAuth callback issued token for sub=#{mapped[:sub]}"
|
|
113
136
|
if request.env['HTTP_ACCEPT']&.include?('application/json')
|
|
114
137
|
json_response({
|
|
115
138
|
access_token: token,
|
data/lib/legion/api/auth_saml.rb
CHANGED
|
@@ -31,7 +31,8 @@ module Legion
|
|
|
31
31
|
return saml if saml.is_a?(Hash)
|
|
32
32
|
|
|
33
33
|
{}
|
|
34
|
-
rescue StandardError
|
|
34
|
+
rescue StandardError => e
|
|
35
|
+
Legion::Logging.debug "AuthSaml#resolve_saml_config failed: #{e.message}" if defined?(Legion::Logging)
|
|
35
36
|
{}
|
|
36
37
|
end
|
|
37
38
|
|
|
@@ -161,7 +162,8 @@ module Legion
|
|
|
161
162
|
names.each do |n|
|
|
162
163
|
v = attrs.multi(n)
|
|
163
164
|
return Array(v) if v
|
|
164
|
-
rescue StandardError
|
|
165
|
+
rescue StandardError => e
|
|
166
|
+
Legion::Logging.debug "AuthSaml#multi_attr failed for attr=#{n}: #{e.message}" if defined?(Legion::Logging)
|
|
165
167
|
nil
|
|
166
168
|
end
|
|
167
169
|
[]
|
|
@@ -169,7 +171,8 @@ module Legion
|
|
|
169
171
|
|
|
170
172
|
def safe_attr(attrs, name)
|
|
171
173
|
attrs[name]
|
|
172
|
-
rescue StandardError
|
|
174
|
+
rescue StandardError => e
|
|
175
|
+
Legion::Logging.debug "AuthSaml#safe_attr failed for name=#{name}: #{e.message}" if defined?(Legion::Logging)
|
|
173
176
|
nil
|
|
174
177
|
end
|
|
175
178
|
|
|
@@ -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',
|
|
@@ -91,7 +106,8 @@ module Legion
|
|
|
91
106
|
return entra if entra.is_a?(Hash)
|
|
92
107
|
|
|
93
108
|
{}
|
|
94
|
-
rescue StandardError
|
|
109
|
+
rescue StandardError => e
|
|
110
|
+
Legion::Logging.debug "AuthWorker#resolve_entra_settings failed: #{e.message}" if defined?(Legion::Logging)
|
|
95
111
|
{}
|
|
96
112
|
end
|
|
97
113
|
|
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
|
|
|
@@ -36,7 +45,8 @@ module Legion
|
|
|
36
45
|
Legion::Data::Model::DigitalWorker.all.map do |w|
|
|
37
46
|
{ worker_id: w.worker_id, status: w.lifecycle_state }
|
|
38
47
|
end
|
|
39
|
-
rescue StandardError
|
|
48
|
+
rescue StandardError => e
|
|
49
|
+
Legion::Logging.warn "Capacity#fetch_worker_list failed: #{e.message}" if defined?(Legion::Logging)
|
|
40
50
|
[]
|
|
41
51
|
end
|
|
42
52
|
end
|
data/lib/legion/api/catalog.rb
CHANGED
|
@@ -45,7 +45,8 @@ module Legion
|
|
|
45
45
|
read_paths: declared[:read_paths],
|
|
46
46
|
write_paths: declared[:write_paths]
|
|
47
47
|
}
|
|
48
|
-
rescue StandardError
|
|
48
|
+
rescue StandardError => e
|
|
49
|
+
Legion::Logging.warn "API#build_catalog_permissions failed for #{name}: #{e.message}" if defined?(Legion::Logging)
|
|
49
50
|
{ sandbox: Legion::Extensions::Permissions.sandbox_path(name), read_paths: [], write_paths: [] }
|
|
50
51
|
end
|
|
51
52
|
|
|
@@ -61,7 +62,8 @@ module Legion
|
|
|
61
62
|
description: runner.values[:description]
|
|
62
63
|
}]
|
|
63
64
|
end
|
|
64
|
-
rescue StandardError
|
|
65
|
+
rescue StandardError => e
|
|
66
|
+
Legion::Logging.warn "API#build_catalog_runners failed for #{name}: #{e.message}" if defined?(Legion::Logging)
|
|
65
67
|
{}
|
|
66
68
|
end
|
|
67
69
|
|
|
@@ -74,7 +76,8 @@ module Legion
|
|
|
74
76
|
matched.map do |_hash, pattern|
|
|
75
77
|
{ intent: pattern[:intent_text], tool_chain: pattern[:tool_chain], confidence: pattern[:confidence] }
|
|
76
78
|
end
|
|
77
|
-
rescue StandardError
|
|
79
|
+
rescue StandardError => e
|
|
80
|
+
Legion::Logging.warn "API#build_catalog_known_intents failed for #{name}: #{e.message}" if defined?(Legion::Logging)
|
|
78
81
|
[]
|
|
79
82
|
end
|
|
80
83
|
end
|
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
|
data/lib/legion/api/events.rb
CHANGED
|
@@ -58,7 +58,8 @@ module Legion
|
|
|
58
58
|
event = queue.pop
|
|
59
59
|
data = Legion::JSON.dump(event.transform_keys(&:to_s))
|
|
60
60
|
out << "event: #{event[:event]}\ndata: #{data}\n\n"
|
|
61
|
-
rescue IOError, Errno::EPIPE
|
|
61
|
+
rescue IOError, Errno::EPIPE => e
|
|
62
|
+
Legion::Logging.debug "Events SSE stream broken for #{event[:event]}: #{e.message}" if defined?(Legion::Logging)
|
|
62
63
|
break
|
|
63
64
|
end
|
|
64
65
|
ensure
|