legionio 1.7.20 → 1.7.24

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ba8e945b1c999e0c815583388817c35bc019134994745682a774afb6e645db1f
4
- data.tar.gz: d21bf7aea3328dda51f7139ed36632b31f16e5c57bef938a9eb2f9f000c2c21e
3
+ metadata.gz: 8e469923d32f8ead43a892e473c93737139885622f40683a63aa0a757a0c282d
4
+ data.tar.gz: 2e88c8468007f617c075178410da534337931e37a3c50fb11a67fa3801807689
5
5
  SHA512:
6
- metadata.gz: c5ef0de86e4ea0a8e92fc7433af4e36bb8b7088737142abcfa5edcaad3769ede8acf0205e2b8478e80cc4376052e6cb3ef3df384b2cf2c39abdc8205fd965a68
7
- data.tar.gz: 389cf7d22ac2db9e44489c4118cbef323d0d92d9242b4ec0e8a4c7ed141425497249a6832b2cf1bbecdc9e6666d65251978fac36f299175195d2eefa8fb6e190
6
+ metadata.gz: 4d7cd5a0513e26ca92bf5b909ac924a7041bbe2b74602d6a552357d155f9101c0d81b75c6d4db0c06b9748b08fd174363ad7d26ffa021f82001923d428ecd5ec
7
+ data.tar.gz: 252dc15217cf3d320fa9f96703967d9c9320f2d3e26d18d91c992a8819dbf34ced50eca926ddcba0a3c2be1a74619d3f6106d20ca9f09919ba6c32901b8d5197
data/CHANGELOG.md CHANGED
@@ -1,6 +1,28 @@
1
1
  # Legion Changelog
2
2
 
3
- ## [Unreleased]
3
+ ## [1.7.24] - 2026-04-06
4
+
5
+ ### Fixed
6
+ - `Routes::Events` SSE stream: qualify `stream_queue` call with `Routes::Events.` to fix NoMethodError on Legion::API instance
7
+
8
+ ### Added
9
+ - `Identity::Process.source` accessor — exposes provider source in identity hash (Wire Format Phase 3)
10
+ - `source:` key in `Identity::Process.identity_hash`, `bind!`, `bind_fallback!`, and `EMPTY_STATE`
11
+
12
+ ## [1.7.22] - 2026-04-06
13
+ ### Added
14
+ - Elastic APM integration for Sinatra API via `elastic-apm` gem
15
+ - Full APM config under `api.elastic_apm` settings: server_url, api_key, secret_token, api_buffer_size, api_request_size, api_request_time, capture_body, capture_headers, capture_env, disable_send, enabled, environment, hostname, ignore_url_patterns, pool_size, service_name, service_node_name, service_version, sample_rate
16
+ - `setup_apm` / `shutdown_apm` lifecycle in Service (boot, shutdown, reload)
17
+ - `ElasticAPM::Middleware` wired into API when available
18
+ - Health/ready endpoints excluded from APM tracing by default
19
+
20
+ ## [1.7.21] - 2026-04-06
21
+ ### Fixed
22
+ - Optional components (rbac, llm, apollo, gaia) no longer block readiness when not installed
23
+ - Split `Readiness::COMPONENTS` into `REQUIRED_COMPONENTS` and `OPTIONAL_COMPONENTS`
24
+ - Added `Readiness.mark_skipped` for components that are absent or disabled
25
+ - Reload path now correctly marks optional components as skipped when not loaded
4
26
 
5
27
  ## [1.7.20] - 2026-04-06
6
28
  ### Added
data/CLAUDE.md CHANGED
@@ -9,7 +9,7 @@ The primary gem for the LegionIO framework. An extensible async job engine for s
9
9
 
10
10
  **GitHub**: https://github.com/LegionIO/LegionIO
11
11
  **Gem**: `legionio`
12
- **Version**: 1.7.18
12
+ **Version**: 1.7.21
13
13
  **License**: Apache-2.0
14
14
  **Docker**: `legionio/legion`
15
15
  **Ruby**: >= 3.4
data/README.md CHANGED
@@ -8,13 +8,13 @@ Schedule tasks, chain services into dependency graphs, run them concurrently via
8
8
  ╭──────────────────────────────────────╮
9
9
  │ L E G I O N I O │
10
10
  │ │
11
- │ 280+ extensions · 58 MCP tools │
11
+ │ 280+ extensions · 60 MCP tools │
12
12
  │ AI chat CLI · REST API · HA │
13
13
  │ cognitive architecture · Vault │
14
14
  ╰──────────────────────────────────────╯
15
15
  ```
16
16
 
17
- **Ruby >= 3.4** | **v1.6.20** | **Apache-2.0** | [@Esity](https://github.com/Esity)
17
+ **Ruby >= 3.4** | **v1.7.21** | **Apache-2.0** | [@Esity](https://github.com/Esity)
18
18
 
19
19
  ---
20
20
 
@@ -33,7 +33,7 @@ When A completes, B runs. B triggers C, D, and E in parallel. Conditions gate ex
33
33
  But that's just the foundation. LegionIO is also:
34
34
 
35
35
  - **An AI coding assistant** — interactive chat with tools, code review, commit messages, PR generation, and multi-agent workflows
36
- - **An MCP server** — 58 tools that let any AI agent run tasks, manage extensions, and query your infrastructure
36
+ - **An MCP server** — 60 tools that let any AI agent run tasks, manage extensions, and query your infrastructure
37
37
  - **A cognitive computing platform** — 242 brain-modeled extensions across 18 cognitive domains
38
38
  - **A digital worker platform** — AI-as-labor with governance, risk tiers, and cost tracking
39
39
 
@@ -359,7 +359,7 @@ legion mcp http # streamable HTTP on localhost:9393
359
359
  legion mcp http --port 8080 --host 0.0.0.0
360
360
  ```
361
361
 
362
- **58 tools** in the `legion.*` namespace:
362
+ **60 tools** in the `legion.*` namespace:
363
363
 
364
364
  | Category | Tools |
365
365
  |----------|-------|
@@ -90,7 +90,7 @@ module Legion
90
90
  end
91
91
 
92
92
  stream do |out|
93
- stream_queue(out: out, queue: queue, listener: listener)
93
+ Routes::Events.stream_queue(out: out, queue: queue, listener: listener)
94
94
  end
95
95
  end
96
96
 
@@ -10,19 +10,45 @@ module Legion
10
10
 
11
11
  def call(env)
12
12
  method_path = "#{env['REQUEST_METHOD']} #{env['PATH_INFO']}"
13
- Legion::Logging.info "[api][request-start] #{method_path}"
13
+ client_info = build_client_info(env)
14
+ Legion::Logging.info "[api][request-start] #{method_path} #{client_info}"
14
15
  start = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
15
16
  status, headers, body = @app.call(env)
16
17
  duration = ((::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - start) * 1000).round(2)
17
18
 
18
19
  level = duration > 5000 ? :warn : :info
19
- Legion::Logging.send(level, "[api] #{method_path} #{status} #{duration}ms")
20
+ Legion::Logging.send(level, "[api] #{method_path} #{status} #{duration}ms #{client_info}")
20
21
  [status, headers, body]
21
22
  rescue StandardError => e
22
23
  duration = ((::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - start) * 1000).round(2)
23
- Legion::Logging.error "[api] #{method_path} 500 #{duration}ms - #{e.message}"
24
+ Legion::Logging.error "[api] #{method_path} 500 #{duration}ms #{client_info} - #{e.message}"
24
25
  raise
25
26
  end
27
+
28
+ private
29
+
30
+ def build_client_info(env)
31
+ ip = env['HTTP_X_FORWARDED_FOR'] || env['REMOTE_ADDR'] || '-'
32
+ ua = env['HTTP_USER_AGENT'] || '-'
33
+ origin = env['HTTP_ORIGIN'] || '-'
34
+ referer = env['HTTP_REFERER'] || '-'
35
+ auth = env['HTTP_AUTHORIZATION'] ? 'Bearer(present)' : 'none'
36
+ content_type = env['CONTENT_TYPE'] || '-'
37
+ content_length = env['CONTENT_LENGTH'] || '-'
38
+ query = env['QUERY_STRING'] && env['QUERY_STRING'].empty? ? nil : env['QUERY_STRING']
39
+
40
+ parts = [
41
+ "ip=#{ip}",
42
+ "ua=#{ua}",
43
+ "origin=#{origin}",
44
+ "referer=#{referer}",
45
+ "auth=#{auth}",
46
+ "content_type=#{content_type}",
47
+ "content_length=#{content_length}"
48
+ ]
49
+ parts << "query=#{query}" if query
50
+ parts.join(' ')
51
+ end
26
52
  end
27
53
  end
28
54
  end
@@ -11,6 +11,7 @@ module Legion
11
11
  id: nil,
12
12
  canonical_name: nil,
13
13
  kind: nil,
14
+ source: nil,
14
15
  persistent: false,
15
16
  groups: [].freeze,
16
17
  metadata: {}.freeze
@@ -53,11 +54,16 @@ module Legion
53
54
  @state.get[:persistent] == true
54
55
  end
55
56
 
57
+ def source
58
+ @state.get[:source]
59
+ end
60
+
56
61
  def identity_hash
57
62
  {
58
63
  id: id,
59
64
  canonical_name: canonical_name,
60
65
  kind: kind,
66
+ source: source,
61
67
  mode: mode,
62
68
  queue_prefix: queue_prefix,
63
69
  resolved: resolved?,
@@ -69,10 +75,12 @@ module Legion
69
75
 
70
76
  def bind!(provider, identity_hash)
71
77
  @provider = provider
78
+ provider_source = provider.respond_to?(:provider_name) ? provider.provider_name : nil
72
79
  @state.set({
73
80
  id: identity_hash[:id],
74
81
  canonical_name: identity_hash[:canonical_name],
75
82
  kind: identity_hash[:kind],
83
+ source: identity_hash.key?(:source) ? identity_hash[:source] : provider_source,
76
84
  persistent: identity_hash.fetch(:persistent, true),
77
85
  groups: Array(identity_hash[:groups]).compact.freeze,
78
86
  metadata: identity_hash[:metadata].is_a?(Hash) ? identity_hash[:metadata].dup.freeze : {}.freeze
@@ -86,6 +94,7 @@ module Legion
86
94
  id: nil,
87
95
  canonical_name: user,
88
96
  kind: :human,
97
+ source: :system,
89
98
  persistent: false,
90
99
  groups: [].freeze,
91
100
  metadata: {}.freeze
@@ -4,7 +4,9 @@ require 'concurrent'
4
4
 
5
5
  module Legion
6
6
  module Readiness
7
- COMPONENTS = %i[settings crypt transport cache data rbac llm apollo gaia identity extensions api].freeze
7
+ REQUIRED_COMPONENTS = %i[settings crypt transport cache data extensions api].freeze
8
+ OPTIONAL_COMPONENTS = %i[rbac llm apollo gaia identity].freeze
9
+ COMPONENTS = (REQUIRED_COMPONENTS + OPTIONAL_COMPONENTS).freeze
8
10
  DRAIN_TIMEOUT = 5
9
11
 
10
12
  class << self
@@ -22,14 +24,19 @@ module Legion
22
24
  Legion::Logging.debug "[Readiness] #{component} is not ready" if defined?(Legion::Logging)
23
25
  end
24
26
 
27
+ def mark_skipped(component)
28
+ status[component.to_sym] = :skipped
29
+ Legion::Logging.debug "[Readiness] #{component} skipped (optional)" if defined?(Legion::Logging)
30
+ end
31
+
25
32
  def ready?(component = nil)
26
33
  if component
27
- result = status[component.to_sym] == true
34
+ result = [true, :skipped].include?(status[component.to_sym])
28
35
  Legion::Logging.warn "[Readiness] #{component} is not ready" if !result && defined?(Legion::Logging)
29
36
  return result
30
37
  end
31
38
 
32
- not_ready = COMPONENTS.reject { |c| status[c] == true }
39
+ not_ready = COMPONENTS.reject { |c| [true, :skipped].include?(status[c]) }
33
40
  not_ready.each { |c| Legion::Logging.warn "[Readiness] #{c} is not ready" } if !not_ready.empty? && defined?(Legion::Logging)
34
41
  not_ready.empty?
35
42
  end
@@ -50,7 +57,8 @@ module Legion
50
57
 
51
58
  def to_h
52
59
  COMPONENTS.to_h do |c|
53
- [c, status[c] == true]
60
+ val = status[c]
61
+ [c, [true, :skipped].include?(val)]
54
62
  end
55
63
  end
56
64
  end
@@ -102,7 +102,11 @@ module Legion
102
102
  end
103
103
  end
104
104
 
105
- setup_rbac if data
105
+ if data
106
+ setup_rbac
107
+ else
108
+ Legion::Readiness.mark_skipped(:rbac)
109
+ end
106
110
  setup_cluster if data
107
111
 
108
112
  if llm
@@ -112,9 +116,13 @@ module Legion
112
116
  rescue LoadError => e
113
117
  handle_exception(e, level: :debug, operation: 'service.initialize.llm', availability: 'missing')
114
118
  log.info 'Legion::LLM gem is not installed'
119
+ Legion::Readiness.mark_skipped(:llm)
115
120
  rescue StandardError => e
116
121
  handle_exception(e, level: :warn, operation: 'service.initialize.llm')
122
+ Legion::Readiness.mark_skipped(:llm)
117
123
  end
124
+ else
125
+ Legion::Readiness.mark_skipped(:llm)
118
126
  end
119
127
 
120
128
  begin
@@ -123,8 +131,10 @@ module Legion
123
131
  rescue LoadError => e
124
132
  handle_exception(e, level: :debug, operation: 'service.initialize.apollo', availability: 'missing')
125
133
  log.info 'Legion::Apollo gem is not installed, starting without Apollo'
134
+ Legion::Readiness.mark_skipped(:apollo)
126
135
  rescue StandardError => e
127
136
  handle_exception(e, level: :warn, operation: 'service.initialize.apollo')
137
+ Legion::Readiness.mark_skipped(:apollo)
128
138
  end
129
139
 
130
140
  if gaia
@@ -134,9 +144,13 @@ module Legion
134
144
  rescue LoadError => e
135
145
  handle_exception(e, level: :debug, operation: 'service.initialize.gaia', availability: 'missing')
136
146
  log.info 'Legion::Gaia gem is not installed'
147
+ Legion::Readiness.mark_skipped(:gaia)
137
148
  rescue StandardError => e
138
149
  handle_exception(e, level: :warn, operation: 'service.initialize.gaia')
150
+ Legion::Readiness.mark_skipped(:gaia)
139
151
  end
152
+ else
153
+ Legion::Readiness.mark_skipped(:gaia)
140
154
  end
141
155
 
142
156
  setup_telemetry
@@ -178,6 +192,7 @@ module Legion
178
192
  require 'legion/api/default_settings'
179
193
  api_settings = Legion::Settings[:api]
180
194
  @api_enabled = api && api_settings[:enabled]
195
+ setup_apm if @api_enabled
181
196
  setup_api if @api_enabled
182
197
  setup_network_watchdog
183
198
  Legion::Settings[:client][:ready] = true
@@ -232,8 +247,10 @@ module Legion
232
247
  rescue LoadError => e
233
248
  handle_exception(e, level: :debug, operation: 'service.setup_rbac', availability: 'missing')
234
249
  log.debug 'Legion::Rbac gem is not installed, starting without RBAC'
250
+ Legion::Readiness.mark_skipped(:rbac)
235
251
  rescue StandardError => e
236
252
  handle_exception(e, level: :warn, operation: 'service.setup_rbac')
253
+ Legion::Readiness.mark_skipped(:rbac)
237
254
  end
238
255
 
239
256
  def setup_cluster
@@ -309,6 +326,42 @@ module Legion
309
326
  )
310
327
  end
311
328
 
329
+ def setup_apm
330
+ apm_settings = Legion::Settings[:apm] || {}
331
+ return unless apm_settings[:enabled]
332
+
333
+ require 'elastic-apm'
334
+
335
+ config = {
336
+ service_name: apm_settings[:service_name] || "legion-#{Legion::Settings[:client][:name]}",
337
+ server_url: apm_settings[:server_url] || 'http://localhost:8200',
338
+ environment: apm_settings[:environment] || Legion::Settings[:environment] || 'development',
339
+ secret_token: apm_settings[:secret_token],
340
+ api_key: apm_settings[:api_key],
341
+ log_level: apm_settings[:log_level]&.to_sym || Logger::WARN,
342
+ transaction_sample_rate: apm_settings[:sample_rate] || 1.0
343
+ }.compact
344
+
345
+ ElasticAPM.start(**config)
346
+ @apm_running = true
347
+ log.info "Elastic APM started: server=#{config[:server_url]} service=#{config[:service_name]}"
348
+ rescue LoadError => e
349
+ handle_exception(e, level: :debug, operation: 'service.setup_apm', availability: 'missing')
350
+ log.info 'elastic-apm gem is not installed, starting without APM'
351
+ rescue StandardError => e
352
+ handle_exception(e, level: :warn, operation: 'service.setup_apm')
353
+ end
354
+
355
+ def shutdown_apm
356
+ return unless @apm_running
357
+
358
+ ElasticAPM.stop if defined?(ElasticAPM) && ElasticAPM.running?
359
+ @apm_running = false
360
+ log.info 'Elastic APM stopped'
361
+ rescue StandardError => e
362
+ handle_exception(e, level: :warn, operation: 'service.shutdown_apm')
363
+ end
364
+
312
365
  def setup_api # rubocop:disable Metrics/MethodLength
313
366
  if @api_thread&.alive?
314
367
  log.warn 'API already running, skipping duplicate setup_api call'
@@ -667,6 +720,7 @@ module Legion
667
720
  shutdown_network_watchdog
668
721
  shutdown_audit_archiver
669
722
  shutdown_api
723
+ shutdown_apm
670
724
 
671
725
  Legion::Metrics.reset! if defined?(Legion::Metrics)
672
726
 
@@ -738,6 +792,7 @@ module Legion
738
792
 
739
793
  shutdown_network_watchdog
740
794
  shutdown_api
795
+ shutdown_apm
741
796
 
742
797
  if defined?(Legion::Gaia) && Legion::Gaia.respond_to?(:started?) && Legion::Gaia.started?
743
798
  shutdown_component('Gaia') { Legion::Gaia.shutdown }
@@ -786,11 +841,33 @@ module Legion
786
841
  setup_data
787
842
  Legion::Readiness.mark_ready(:data)
788
843
 
789
- setup_rbac if defined?(Legion::Rbac)
790
- setup_llm if defined?(Legion::LLM)
844
+ if defined?(Legion::Rbac)
845
+ setup_rbac
846
+ else
847
+ Legion::Readiness.mark_skipped(:rbac)
848
+ end
849
+
850
+ if defined?(Legion::LLM)
851
+ setup_llm
852
+ else
853
+ Legion::Readiness.mark_skipped(:llm)
854
+ end
791
855
 
792
- setup_gaia if defined?(Legion::Gaia)
793
- Legion::Readiness.mark_ready(:gaia)
856
+ if defined?(Legion::Apollo)
857
+ setup_apollo
858
+ Legion::Readiness.mark_ready(:apollo)
859
+ else
860
+ Legion::Readiness.mark_skipped(:apollo)
861
+ end
862
+
863
+ if defined?(Legion::Gaia)
864
+ setup_gaia
865
+ Legion::Readiness.mark_ready(:gaia)
866
+ else
867
+ Legion::Readiness.mark_skipped(:gaia)
868
+ end
869
+
870
+ Legion::Readiness.mark_ready(:identity)
794
871
 
795
872
  setup_supervision
796
873
  load_extensions
@@ -801,6 +878,7 @@ module Legion
801
878
  register_core_tools
802
879
 
803
880
  Legion::Crypt.cs
881
+ setup_apm if @api_enabled
804
882
  setup_api if @api_enabled
805
883
 
806
884
  if defined?(Legion::MCP)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Legion
4
- VERSION = '1.7.20'
4
+ VERSION = '1.7.24'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: legionio
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.7.20
4
+ version: 1.7.24
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity