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.
Files changed (149) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +31 -0
  3. data/legionio.gemspec +1 -0
  4. data/lib/legion/alerts.rb +7 -2
  5. data/lib/legion/api/acp.rb +8 -4
  6. data/lib/legion/api/audit.rb +11 -2
  7. data/lib/legion/api/auth.rb +14 -2
  8. data/lib/legion/api/auth_human.rb +32 -9
  9. data/lib/legion/api/auth_saml.rb +6 -3
  10. data/lib/legion/api/auth_worker.rb +20 -4
  11. data/lib/legion/api/capacity.rb +11 -1
  12. data/lib/legion/api/catalog.rb +6 -3
  13. data/lib/legion/api/chains.rb +26 -6
  14. data/lib/legion/api/coldstart.rb +16 -4
  15. data/lib/legion/api/events.rb +2 -1
  16. data/lib/legion/api/extensions.rb +6 -2
  17. data/lib/legion/api/gaia.rb +8 -3
  18. data/lib/legion/api/governance.rb +2 -1
  19. data/lib/legion/api/graphql/resolvers/extensions.rb +4 -2
  20. data/lib/legion/api/graphql/resolvers/node.rb +4 -2
  21. data/lib/legion/api/graphql/resolvers/tasks.rb +4 -2
  22. data/lib/legion/api/graphql/resolvers/workers.rb +4 -2
  23. data/lib/legion/api/graphql.rb +3 -1
  24. data/lib/legion/api/helpers.rb +4 -2
  25. data/lib/legion/api/hooks.rb +19 -5
  26. data/lib/legion/api/lex.rb +4 -1
  27. data/lib/legion/api/llm.rb +54 -1
  28. data/lib/legion/api/marketplace.rb +9 -3
  29. data/lib/legion/api/middleware/auth.rb +13 -5
  30. data/lib/legion/api/middleware/body_limit.rb +3 -0
  31. data/lib/legion/api/middleware/rate_limit.rb +3 -1
  32. data/lib/legion/api/middleware/tenant.rb +4 -1
  33. data/lib/legion/api/org_chart.rb +2 -1
  34. data/lib/legion/api/prompts.rb +11 -5
  35. data/lib/legion/api/rbac.rb +17 -1
  36. data/lib/legion/api/relationships.rb +5 -0
  37. data/lib/legion/api/schedules.rb +12 -2
  38. data/lib/legion/api/settings.rb +14 -3
  39. data/lib/legion/api/tasks.rb +13 -3
  40. data/lib/legion/api/transport.rb +17 -7
  41. data/lib/legion/api/webhooks.rb +5 -1
  42. data/lib/legion/api/workers.rb +28 -8
  43. data/lib/legion/api/workflow.rb +2 -1
  44. data/lib/legion/api.rb +2 -1
  45. data/lib/legion/audit.rb +3 -2
  46. data/lib/legion/capacity/model.rb +11 -2
  47. data/lib/legion/catalog.rb +5 -1
  48. data/lib/legion/chat/notification_bridge.rb +2 -1
  49. data/lib/legion/cli/acp_command.rb +3 -1
  50. data/lib/legion/cli/auth_command.rb +7 -1
  51. data/lib/legion/cli/chat/agent_registry.rb +2 -1
  52. data/lib/legion/cli/chat/checkpoint.rb +4 -2
  53. data/lib/legion/cli/chat/context.rb +4 -3
  54. data/lib/legion/cli/chat/extension_tool_loader.rb +4 -2
  55. data/lib/legion/cli/chat/markdown_renderer.rb +2 -1
  56. data/lib/legion/cli/chat/session.rb +2 -1
  57. data/lib/legion/cli/chat/subagent.rb +6 -2
  58. data/lib/legion/cli/chat/tool_registry.rb +4 -3
  59. data/lib/legion/cli/chat/tools/edit_file.rb +1 -0
  60. data/lib/legion/cli/chat/tools/read_file.rb +1 -0
  61. data/lib/legion/cli/chat/tools/run_command.rb +3 -1
  62. data/lib/legion/cli/chat/tools/save_memory.rb +1 -0
  63. data/lib/legion/cli/chat/tools/search_content.rb +7 -3
  64. data/lib/legion/cli/chat/tools/search_files.rb +1 -0
  65. data/lib/legion/cli/chat/tools/search_memory.rb +1 -0
  66. data/lib/legion/cli/chat/tools/spawn_agent.rb +1 -0
  67. data/lib/legion/cli/chat/tools/web_search.rb +2 -0
  68. data/lib/legion/cli/chat/tools/write_file.rb +1 -0
  69. data/lib/legion/cli/chat/web_search.rb +4 -2
  70. data/lib/legion/cli/chat_command.rb +21 -8
  71. data/lib/legion/cli/check/privacy_check.rb +6 -3
  72. data/lib/legion/cli/check_command.rb +4 -3
  73. data/lib/legion/cli/coldstart_command.rb +4 -2
  74. data/lib/legion/cli/config_command.rb +2 -1
  75. data/lib/legion/cli/config_scaffold.rb +2 -1
  76. data/lib/legion/cli/connection.rb +2 -2
  77. data/lib/legion/cli/cost/data_client.rb +2 -1
  78. data/lib/legion/cli/dashboard/data_fetcher.rb +2 -1
  79. data/lib/legion/cli/dashboard_command.rb +1 -0
  80. data/lib/legion/cli/detect_command.rb +2 -1
  81. data/lib/legion/cli/doctor/bundle_check.rb +2 -1
  82. data/lib/legion/cli/doctor/cache_check.rb +2 -1
  83. data/lib/legion/cli/doctor/database_check.rb +2 -1
  84. data/lib/legion/cli/doctor/extensions_check.rb +2 -1
  85. data/lib/legion/cli/doctor/pid_check.rb +2 -1
  86. data/lib/legion/cli/doctor/rabbitmq_check.rb +4 -2
  87. data/lib/legion/cli/doctor/vault_check.rb +2 -1
  88. data/lib/legion/cli/doctor_command.rb +1 -0
  89. data/lib/legion/cli/error_handler.rb +9 -1
  90. data/lib/legion/cli/gaia_command.rb +4 -2
  91. data/lib/legion/cli/init/environment_detector.rb +4 -2
  92. data/lib/legion/cli/lex_cli_manifest.rb +2 -1
  93. data/lib/legion/cli/lex_command.rb +8 -4
  94. data/lib/legion/cli/llm_command.rb +9 -5
  95. data/lib/legion/cli/marketplace_command.rb +2 -1
  96. data/lib/legion/cli/payroll_command.rb +3 -0
  97. data/lib/legion/cli/plan_command.rb +2 -1
  98. data/lib/legion/cli/setup_command.rb +8 -4
  99. data/lib/legion/cli/start.rb +2 -1
  100. data/lib/legion/cli/status.rb +2 -1
  101. data/lib/legion/cli/task_command.rb +6 -3
  102. data/lib/legion/cli/tty_command.rb +1 -0
  103. data/lib/legion/cli/update_command.rb +4 -2
  104. data/lib/legion/cli.rb +10 -4
  105. data/lib/legion/cluster/leader.rb +2 -1
  106. data/lib/legion/cluster/lock.rb +8 -4
  107. data/lib/legion/context.rb +3 -0
  108. data/lib/legion/digital_worker/lifecycle.rb +5 -1
  109. data/lib/legion/digital_worker/registry.rb +7 -0
  110. data/lib/legion/digital_worker/value_metrics.rb +2 -1
  111. data/lib/legion/docs/site_generator.rb +8 -6
  112. data/lib/legion/events.rb +3 -2
  113. data/lib/legion/extensions/actors/every.rb +7 -1
  114. data/lib/legion/extensions/actors/subscription.rb +9 -6
  115. data/lib/legion/extensions/builders/actors.rb +2 -1
  116. data/lib/legion/extensions/builders/routes.rb +1 -0
  117. data/lib/legion/extensions/builders/runners.rb +1 -0
  118. data/lib/legion/extensions/core.rb +11 -3
  119. data/lib/legion/extensions/permissions.rb +6 -3
  120. data/lib/legion/extensions/transport.rb +3 -1
  121. data/lib/legion/extensions.rb +12 -6
  122. data/lib/legion/graph/builder.rb +4 -1
  123. data/lib/legion/graph/exporter.rb +2 -0
  124. data/lib/legion/guardrails.rb +10 -3
  125. data/lib/legion/ingress.rb +14 -2
  126. data/lib/legion/lock.rb +8 -4
  127. data/lib/legion/metrics.rb +2 -1
  128. data/lib/legion/notebook/renderer.rb +2 -1
  129. data/lib/legion/phi/access_log.rb +2 -1
  130. data/lib/legion/phi/erasure.rb +4 -2
  131. data/lib/legion/phi.rb +4 -2
  132. data/lib/legion/process.rb +8 -2
  133. data/lib/legion/process_role.rb +6 -1
  134. data/lib/legion/readiness.rb +10 -4
  135. data/lib/legion/region/failover.rb +2 -1
  136. data/lib/legion/region.rb +14 -7
  137. data/lib/legion/registry/governance.rb +2 -1
  138. data/lib/legion/registry/persistence.rb +2 -1
  139. data/lib/legion/runner/status.rb +8 -8
  140. data/lib/legion/runner.rb +7 -3
  141. data/lib/legion/service.rb +20 -6
  142. data/lib/legion/telemetry/open_inference.rb +18 -9
  143. data/lib/legion/telemetry/safety_metrics.rb +4 -2
  144. data/lib/legion/telemetry.rb +16 -8
  145. data/lib/legion/tenants.rb +2 -1
  146. data/lib/legion/trace_search.rb +3 -0
  147. data/lib/legion/version.rb +1 -1
  148. data/lib/legion/webhooks.rb +17 -4
  149. metadata +15 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ac22c22ec64e117b1568d5569a2983e46fdc9aadb566642c05c35973fb29428a
4
- data.tar.gz: e6b5a99eebc5b0de37a59ad9cebc4fd5e96b8fb125b1974faa64e2454e268907
3
+ metadata.gz: 6d78fc201a2601de3ce04849d24c9234c75bd77f6cdb3b46ca700f7cdd6fcc9f
4
+ data.tar.gz: 5746d467cb709dbf02837ebd2d4288dbe137d234c8f6e1e3b3445dcd899d3515
5
5
  SHA512:
6
- metadata.gz: 789c1485afd78d006d6d9aad725e483c5b6fd467a952476a11e1db29ac8c710f9d80c7e8dce4ac0045627abc45defae5ae45ebe995d167b95ffa866897ae4979
7
- data.tar.gz: 852f58359d4cb4beb1a1708f6d01f9423dc2d18e0d9f028493f2f018eaa5f999b87c564b8015d39004f08ff70e43e41ff92bfcb075fc28b8608281c702704fd8
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 "[alert] #{rule.name}: #{event_name} (#{rule.severity})" if defined?(Legion::Logging)
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
@@ -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
 
@@ -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
- halt 503, json_error('unavailable', 'lex-audit is not loaded', status_code: 503) unless defined?(Legion::Extensions::Audit::Runners::Audit)
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
@@ -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
- halt 400, json_error('missing_subject_token', 'subject_token is required', status_code: 400) unless subject_token
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
- halt 500, json_error('entra_tenant_not_configured', 'rbac.entra.tenant_id not set', status_code: 500) unless tenant_id
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
- halt 500, json_error('entra_not_configured', 'Entra OAuth settings are missing', status_code: 500) unless entra[:tenant_id] && entra[:client_id]
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
- halt 500, json_error('entra_not_configured', 'Entra OAuth settings are missing', status_code: 500) unless entra[:tenant_id] && entra[:client_id]
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
- halt 400, json_error('oauth_error', params[:error_description] || params[:error], status_code: 400) if params[:error]
72
- halt 400, json_error('missing_code', 'authorization code is required', status_code: 400) unless params[:code]
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
- halt 502, json_error('token_exchange_failed', 'Failed to exchange code for tokens', status_code: 502) unless token_response
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
- halt 502, json_error('no_id_token', 'Entra did not return an id_token', status_code: 502) unless id_token
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,
@@ -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
- halt 400, json_error('missing_entra_token', 'entra_token is required', status_code: 400) unless entra_token
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
- halt 401, json_error('invalid_token', 'missing appid claim', status_code: 401) unless app_id
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
 
@@ -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
@@ -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
@@ -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
- halt 501, json_error('not_implemented', 'chain data model is not yet available', status_code: 501) unless Legion::Data::Model.const_defined?(:Chain)
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
- halt 422, json_error('missing_field', 'name is required', status_code: 422) unless body[:name]
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
- halt 501, json_error('not_implemented', 'chain data model is not yet available', status_code: 501) unless Legion::Data::Model.const_defined?(:Chain)
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
- halt 501, json_error('not_implemented', 'chain data model is not yet available', status_code: 501) unless Legion::Data::Model.const_defined?(:Chain)
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
- halt 501, json_error('not_implemented', 'chain data model is not yet available', status_code: 501) unless Legion::Data::Model.const_defined?(:Chain)
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
@@ -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
- halt 422, json_error('missing_field', 'path is required', status_code: 422) if path.nil? || path.empty?
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
- halt 503, json_error('coldstart_unavailable', 'lex-coldstart is not loaded', status_code: 503) unless defined?(Legion::Extensions::Coldstart)
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
- halt 503, json_error('memory_unavailable', 'lex-memory is not loaded', status_code: 503) unless defined?(Legion::Extensions::Memory)
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 ingest error: #{e.message}"
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
@@ -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