legionio 1.4.107 → 1.4.109

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: 852b32f5818f330941b385f4298e266c2906ff506f8111c58b708be29f83b707
4
- data.tar.gz: 7a1ba8bfb20275e5e6fec4c89658c501cf81a63db55e873c9a6c08c0cffaba70
3
+ metadata.gz: 52e1d5b8749747a9fe4b89302f1e0122f86a7630e02f668dfa5df8de363399d7
4
+ data.tar.gz: 315b433465e06726391616a01d0cd6d8d8b13c849eff241a2702cdb8ab66cbaa
5
5
  SHA512:
6
- metadata.gz: e8fd883eb244d806bc51a223ff300d12e06d160c8b5096a6e34898dd1f7c0577345c1e2d30f51e168d20da438d28108934cf57645df180c156f91c107d4eb964
7
- data.tar.gz: e133ef05dd03f998185d2aa00fe1fe679a69f9e31db58b11987c8bff75ce2f9ed7baedcc4745ebb3a39362d228e1ca33943897b396dd585ce4981d83d1802709
6
+ metadata.gz: 4b8c67b2e70b786fc16abb5f43ce8a4d8155e87b22fcf4e901afb24c021fc2112b07b884b75e16f9a48e046a959cacc035885d4251f4812e3eb805f2465e66f6
7
+ data.tar.gz: 79b46b3605b2c249090b11dd4d12db71ff523061b9f9ae929cca2d7a1b3646489c3afb01f8dd36d6dcc532d925c86b2b48c6b90ee9cbac39c5bbdee5514618c7
data/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # Legion Changelog
2
2
 
3
+ ## [1.4.109] - 2026-03-21
4
+
5
+ ### Added
6
+ - `Legion::Cluster::Lock` Redis backend: SETNX + TTL acquire, Lua compare-and-delete release, thread-safe token storage via `Concurrent::Map`
7
+ - `Legion::Cluster::Lock.backend` auto-detection: `:redis` (preferred), `:postgres` (advisory locks), or `:none`
8
+ - `Legion::ProcessRole` module: role presets (full, api, worker, router) controlling which Service subsystems start
9
+ - `Legion::Service#initialize` role integration: `role:` parameter resolves via `ProcessRole`, explicit kwargs override role defaults
10
+ - 24 new specs (2404 total, 0 failures)
11
+
12
+ ## [1.4.108] - 2026-03-21
13
+
14
+ ### Added
15
+ - `Legion::Registry::SecurityScanner` static analysis check — detects dangerous Ruby patterns (eval, system, exec, backtick, IO.popen, Open3) in extension source files
16
+ - `Legion::Registry::Persistence` module — syncs in-memory registry with `extensions_registry` DB table (load at boot, persist on register/update)
17
+ - Boot-time auto-population of `Legion::Registry` from discovered extensions with gemspec capability reading
18
+ - `Legion::Sandbox` auto-wiring from gemspec `legion.capabilities` metadata at extension load
19
+ - `legion marketplace install NAME` command — validates lex- naming, installs gem, registers in registry
20
+ - `legion marketplace publish` command — full pipeline: rspec, rubocop, gem build, gem push, security scan, register
21
+ - `Legion::Registry::Governance` module — naming convention enforcement, auto-approve by risk tier, review requirements via `Legion::Settings`
22
+ - 65 new specs (2380 total, 0 failures)
23
+
3
24
  ## [1.4.107] - 2026-03-21
4
25
 
5
26
  ### 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.4.79
12
+ **Version**: 1.4.107
13
13
  **License**: Apache-2.0
14
14
  **Docker**: `legionio/legion`
15
15
  **Ruby**: >= 3.4
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/cli/doctor_command'
@@ -231,6 +231,87 @@ module Legion
231
231
  out.error(e.message)
232
232
  end
233
233
 
234
+ # ──────────────────────────────────────────────────────────
235
+ # install
236
+ # ──────────────────────────────────────────────────────────
237
+
238
+ desc 'install NAME', 'Install a lex extension gem'
239
+ def install(name)
240
+ require 'legion/registry'
241
+ out = formatter
242
+
243
+ unless name.start_with?('lex-')
244
+ out.error("Extension name must start with 'lex-'")
245
+ return
246
+ end
247
+
248
+ if Kernel.system('gem', 'install', name)
249
+ entry = Legion::Registry::Entry.new(name: name, status: :active, airb_status: 'pending')
250
+ Legion::Registry.register(entry)
251
+ out.success("'#{name}' installed successfully")
252
+ else
253
+ out.error("Failed to install '#{name}'")
254
+ end
255
+ end
256
+
257
+ # ──────────────────────────────────────────────────────────
258
+ # publish
259
+ # ──────────────────────────────────────────────────────────
260
+
261
+ desc 'publish', 'Publish current extension to rubygems'
262
+ def publish
263
+ require 'legion/registry'
264
+ require 'legion/registry/security_scanner'
265
+ out = formatter
266
+
267
+ gemspec_files = Dir.glob('*.gemspec')
268
+ if gemspec_files.empty?
269
+ out.error('No gemspec found — publish aborted')
270
+ return
271
+ end
272
+
273
+ gemspec_path = gemspec_files.first
274
+ gem_name = File.basename(gemspec_path, '.gemspec')
275
+
276
+ unless Kernel.system('bundle', 'exec', 'rspec')
277
+ out.error('Specs failed — publish aborted')
278
+ return
279
+ end
280
+
281
+ unless Kernel.system('bundle', 'exec', 'rubocop')
282
+ out.error('Rubocop failed — publish aborted')
283
+ return
284
+ end
285
+
286
+ unless Kernel.system('gem', 'build', gemspec_path)
287
+ out.error("Failed to build gem '#{gem_name}'")
288
+ return
289
+ end
290
+
291
+ gem_files = Dir.glob("#{gem_name}-*.gem")
292
+ if gem_files.empty?
293
+ out.error('No built gem file found after build')
294
+ return
295
+ end
296
+
297
+ gem_file = gem_files.max_by { |f| File.mtime(f) }
298
+
299
+ unless Kernel.system('gem', 'push', gem_file)
300
+ out.error("Failed to push '#{gem_file}'")
301
+ return
302
+ end
303
+
304
+ scanner = Legion::Registry::SecurityScanner.new
305
+ scan_result = scanner.scan(name: gem_file)
306
+
307
+ version = gem_file.sub("#{gem_name}-", '').sub('.gem', '')
308
+ entry = Legion::Registry::Entry.new(name: gem_name, version: version,
309
+ status: :active, airb_status: 'pending')
310
+ Legion::Registry.register(entry)
311
+
312
+ out.success("'#{gem_name}' v#{version} published — security: #{scan_result[:passed] ? 'passed' : 'failed'}")
313
+ end
314
+
234
315
  # ──────────────────────────────────────────────────────────
235
316
  # stats
236
317
  # ──────────────────────────────────────────────────────────
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Cluster
5
+ class Leader
6
+ HEARTBEAT_INTERVAL = 10 # seconds
7
+ LOCK_NAME = 'legion_leader'
8
+
9
+ attr_reader :node_id, :is_leader
10
+
11
+ def initialize(node_id: SecureRandom.uuid)
12
+ @node_id = node_id
13
+ @is_leader = false
14
+ @heartbeat_thread = nil
15
+ @running = false
16
+ end
17
+
18
+ def start
19
+ @running = true
20
+ @heartbeat_thread = Thread.new { election_loop }
21
+ end
22
+
23
+ def stop
24
+ @running = false
25
+ @heartbeat_thread&.join(HEARTBEAT_INTERVAL + 2)
26
+ resign if @is_leader
27
+ end
28
+
29
+ def leader?
30
+ @is_leader
31
+ end
32
+
33
+ private
34
+
35
+ def election_loop
36
+ while @running
37
+ attempt_election
38
+ sleep(HEARTBEAT_INTERVAL)
39
+ end
40
+ end
41
+
42
+ def attempt_election
43
+ @is_leader = if Lock.acquire(name: LOCK_NAME)
44
+ true
45
+ else
46
+ false
47
+ end
48
+ rescue StandardError
49
+ @is_leader = false
50
+ end
51
+
52
+ def resign
53
+ Lock.release(name: LOCK_NAME) if @is_leader
54
+ @is_leader = false
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,159 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module Legion
6
+ module Cluster
7
+ module Lock
8
+ module_function
9
+
10
+ @tokens = if defined?(Concurrent::Map)
11
+ Concurrent::Map.new
12
+ else
13
+ {}
14
+ end
15
+ @tokens_mutex = Mutex.new unless defined?(Concurrent::Map)
16
+
17
+ def tokens
18
+ @tokens
19
+ end
20
+
21
+ def backend
22
+ if defined?(Legion::Cache) &&
23
+ Legion::Cache.respond_to?(:const_defined?) &&
24
+ Legion::Cache.const_defined?(:Redis, false) &&
25
+ Legion::Cache::Redis.respond_to?(:client) &&
26
+ !Legion::Cache::Redis.client.nil?
27
+ :redis
28
+ elsif defined?(Legion::Data) &&
29
+ Legion::Data.respond_to?(:connection) &&
30
+ !Legion::Data.connection.nil?
31
+ :postgres
32
+ else
33
+ :none
34
+ end
35
+ end
36
+
37
+ def acquire(name:, ttl: 30, timeout: 5) # rubocop:disable Lint/UnusedMethodArgument
38
+ case backend
39
+ when :redis
40
+ acquire_redis(name: name, ttl: ttl)
41
+ when :postgres
42
+ acquire_postgres(name: name)
43
+ else
44
+ false
45
+ end
46
+ end
47
+
48
+ def release(name:, token: nil)
49
+ case backend
50
+ when :redis
51
+ release_redis(name: name, token: token)
52
+ when :postgres
53
+ release_postgres(name: name)
54
+ else
55
+ false
56
+ end
57
+ end
58
+
59
+ def with_lock(name:, ttl: 30, timeout: 5)
60
+ acquired = acquire(name: name, ttl: ttl, timeout: timeout)
61
+ return unless acquired
62
+
63
+ token = acquired == true ? nil : acquired
64
+
65
+ begin
66
+ yield
67
+ ensure
68
+ release(name: name, token: token)
69
+ end
70
+ end
71
+
72
+ def lock_key(name)
73
+ name.to_s.bytes.reduce(0) { |acc, b| ((acc * 31) + b) & 0x7FFFFFFF }
74
+ end
75
+
76
+ def redis_key(name)
77
+ "legion:lock:#{name}"
78
+ end
79
+
80
+ def acquire_redis(name:, ttl:)
81
+ client = Legion::Cache::Redis.client
82
+ token = SecureRandom.hex(16)
83
+ key = redis_key(name)
84
+ result = client.call('SET', key, token, 'NX', 'PX', ttl * 1000)
85
+ return nil unless result
86
+
87
+ store_token(name, token)
88
+ token
89
+ rescue StandardError
90
+ nil
91
+ end
92
+
93
+ def release_redis(name:, token:)
94
+ client = Legion::Cache::Redis.client
95
+ tok = token || fetch_token(name)
96
+ return false unless tok
97
+
98
+ key = redis_key(name)
99
+ lua = <<~LUA
100
+ if redis.call('GET', KEYS[1]) == ARGV[1] then
101
+ redis.call('DEL', KEYS[1])
102
+ return 1
103
+ else
104
+ return 0
105
+ end
106
+ LUA
107
+ result = client.call('EVAL', lua, 1, key, tok)
108
+ delete_token(name)
109
+ result == 1
110
+ rescue StandardError
111
+ false
112
+ end
113
+
114
+ def acquire_postgres(name:)
115
+ key = lock_key(name)
116
+ db = Legion::Data.connection
117
+ return false unless db
118
+
119
+ db.fetch('SELECT pg_try_advisory_lock(?) AS acquired', key).first[:acquired]
120
+ rescue StandardError
121
+ false
122
+ end
123
+
124
+ def release_postgres(name:)
125
+ key = lock_key(name)
126
+ db = Legion::Data.connection
127
+ return false unless db
128
+
129
+ db.fetch('SELECT pg_advisory_unlock(?) AS released', key).first[:released]
130
+ rescue StandardError
131
+ false
132
+ end
133
+
134
+ def store_token(name, token)
135
+ if defined?(Concurrent::Map)
136
+ @tokens[name.to_s] = token
137
+ else
138
+ @tokens_mutex.synchronize { @tokens[name.to_s] = token }
139
+ end
140
+ end
141
+
142
+ def fetch_token(name)
143
+ if defined?(Concurrent::Map)
144
+ @tokens[name.to_s]
145
+ else
146
+ @tokens_mutex.synchronize { @tokens[name.to_s] }
147
+ end
148
+ end
149
+
150
+ def delete_token(name)
151
+ if defined?(Concurrent::Map)
152
+ @tokens.delete(name.to_s)
153
+ else
154
+ @tokens_mutex.synchronize { @tokens.delete(name.to_s) }
155
+ end
156
+ end
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Cluster
5
+ autoload :Lock, 'legion/cluster/lock'
6
+ autoload :Leader, 'legion/cluster/leader'
7
+ end
8
+ end
@@ -70,6 +70,7 @@ module Legion
70
70
  next
71
71
  end
72
72
  Catalog.transition(gem_name, :loaded)
73
+ register_in_registry(gem_name: gem_name, version: entry[:version])
73
74
  @loaded_extensions.push(gem_name)
74
75
  end
75
76
  Legion::Logging.info(
@@ -227,8 +228,41 @@ module Legion
227
228
  end
228
229
  end
229
230
 
231
+ def register_in_registry(gem_name:, version: nil, description: nil)
232
+ return unless defined?(Legion::Registry)
233
+ return if Legion::Registry.lookup(gem_name)
234
+
235
+ capabilities = read_gemspec_capabilities(gem_name)
236
+ entry = Legion::Registry::Entry.new(
237
+ name: gem_name,
238
+ version: version,
239
+ description: description,
240
+ capabilities: capabilities,
241
+ airb_status: 'pending',
242
+ risk_tier: 'low'
243
+ )
244
+ Legion::Registry.register(entry)
245
+ register_sandbox_policy(gem_name: gem_name, capabilities: capabilities)
246
+ end
247
+
248
+ def register_sandbox_policy(gem_name:, capabilities: [])
249
+ return unless defined?(Legion::Sandbox)
250
+
251
+ Legion::Sandbox.register_policy(gem_name, capabilities: capabilities)
252
+ end
253
+
230
254
  private
231
255
 
256
+ def read_gemspec_capabilities(gem_name)
257
+ spec = Gem::Specification.find_by_name(gem_name)
258
+ raw = spec.metadata['legion.capabilities']
259
+ return [] unless raw
260
+
261
+ raw.split(',').map(&:strip)
262
+ rescue Gem::MissingSpecError
263
+ []
264
+ end
265
+
232
266
  def hook_subscription_actor(extension_hash, size, opts)
233
267
  ext_name = extension_hash[:extension_name]
234
268
  extension = extension_hash[:extension]
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module ProcessRole
5
+ ROLES = {
6
+ full: { transport: true, cache: true, data: true, extensions: true, api: true, llm: true, gaia: true, crypt: true, supervision: true },
7
+ api: { transport: true, cache: true, data: true, extensions: false, api: true, llm: false, gaia: false, crypt: true, supervision: false },
8
+ worker: { transport: true, cache: true, data: true, extensions: true, api: false, llm: true, gaia: true, crypt: true, supervision: true },
9
+ router: { transport: true, cache: true, data: false, extensions: true, api: false, llm: false, gaia: false, crypt: true, supervision: false }
10
+ }.freeze
11
+
12
+ def self.resolve(role_name)
13
+ key = role_name.to_sym
14
+ unless ROLES.key?(key)
15
+ warn_unrecognized(key)
16
+ key = :full
17
+ end
18
+ ROLES[key]
19
+ end
20
+
21
+ def self.current
22
+ settings = Legion::Settings[:process] rescue nil # rubocop:disable Style/RescueModifier
23
+ return :full unless settings.is_a?(Hash)
24
+
25
+ role = settings[:role]
26
+ return :full if role.nil?
27
+
28
+ role.to_sym
29
+ end
30
+
31
+ def self.role?(name)
32
+ current == name.to_sym
33
+ end
34
+
35
+ def self.warn_unrecognized(key)
36
+ message = "ProcessRole: unrecognized role '#{key}', falling back to :full"
37
+ if defined?(Legion::Logging) && Legion::Logging.respond_to?(:warn)
38
+ Legion::Logging.warn(message)
39
+ else
40
+ warn "[Legion] #{message}"
41
+ end
42
+ end
43
+ private_class_method :warn_unrecognized
44
+ end
45
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Registry
5
+ module Governance
6
+ DEFAULTS = {
7
+ require_airb_approval: false,
8
+ auto_approve_risk_tiers: %w[low],
9
+ review_required_risk_tiers: %w[medium high critical],
10
+ naming_convention: 'lex-[a-z][a-z0-9_]*',
11
+ deprecation_notice_days: 30
12
+ }.freeze
13
+
14
+ class << self
15
+ def config
16
+ @config ||= load_config
17
+ end
18
+
19
+ def check_name(name)
20
+ pattern = Regexp.new("\\A#{config[:naming_convention]}\\z")
21
+ pattern.match?(name.to_s)
22
+ end
23
+
24
+ def auto_approve?(risk_tier)
25
+ config[:auto_approve_risk_tiers].include?(risk_tier.to_s)
26
+ end
27
+
28
+ def review_required?(risk_tier)
29
+ config[:review_required_risk_tiers].include?(risk_tier.to_s)
30
+ end
31
+
32
+ def reset!
33
+ @config = nil
34
+ end
35
+
36
+ private
37
+
38
+ def load_config
39
+ return DEFAULTS unless defined?(Legion::Settings)
40
+
41
+ overrides = Legion::Settings.dig(:registry, :governance)
42
+ return DEFAULTS.merge(overrides) if overrides.is_a?(Hash)
43
+
44
+ DEFAULTS
45
+ rescue StandardError
46
+ DEFAULTS
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Registry
5
+ module Persistence
6
+ class << self
7
+ def data_available?
8
+ return false unless defined?(Legion::Data)
9
+ return false unless Legion::Data.respond_to?(:connection) && Legion::Data.connection
10
+
11
+ Legion::Data.connection.table_exists?(:extensions_registry)
12
+ rescue StandardError
13
+ false
14
+ end
15
+
16
+ def load_from_db
17
+ return 0 unless data_available?
18
+
19
+ count = 0
20
+ registry_dataset.each do |row|
21
+ entry = Entry.new(
22
+ name: row[:name],
23
+ version: row[:version],
24
+ author: row[:author],
25
+ description: row[:description],
26
+ status: row[:status]&.to_sym,
27
+ airb_status: row[:airb_status],
28
+ risk_tier: row[:risk_tier]
29
+ )
30
+ Legion::Registry.register(entry)
31
+ count += 1
32
+ end
33
+ count
34
+ end
35
+
36
+ def persist(entry)
37
+ return false unless data_available?
38
+
39
+ attrs = persistence_attrs(entry)
40
+ existing = registry_dataset.where(name: entry.name).first
41
+
42
+ if existing
43
+ registry_dataset.where(name: entry.name).update(**attrs, updated_at: Time.now)
44
+ else
45
+ registry_dataset.insert(**attrs, created_at: Time.now, updated_at: Time.now)
46
+ end
47
+
48
+ true
49
+ rescue StandardError => e
50
+ Legion::Logging.warn("Registry::Persistence failed to persist #{entry.name}: #{e.message}") if defined?(Legion::Logging)
51
+ false
52
+ end
53
+
54
+ private
55
+
56
+ def registry_dataset
57
+ Legion::Data.connection[:extensions_registry]
58
+ end
59
+
60
+ def persistence_attrs(entry)
61
+ parts = entry.name.to_s.split('-')
62
+ mod_name = parts.map(&:capitalize).join('::')
63
+
64
+ {
65
+ name: entry.name,
66
+ module_name: mod_name,
67
+ status: entry.status.to_s,
68
+ description: entry.description
69
+ }
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -5,10 +5,21 @@ require 'digest'
5
5
  module Legion
6
6
  module Registry
7
7
  class SecurityScanner
8
- CHECKS = %i[checksum naming_convention gemspec_metadata].freeze
8
+ CHECKS = %i[checksum naming_convention gemspec_metadata static_analysis].freeze
9
9
 
10
- def scan(gem_path: nil, name: nil, gemspec: nil)
11
- results = CHECKS.map { |check| send(check, gem_path: gem_path, name: name, gemspec: gemspec) }
10
+ DANGEROUS_PATTERNS = [
11
+ { pattern: /\bKernel\.eval\b|\beval\s*\(/, label: 'eval' },
12
+ { pattern: /\bKernel\.system\b|\bsystem\s*\(/, label: 'system' },
13
+ { pattern: /\bKernel\.exec\b|\bexec\s*\(/, label: 'exec' },
14
+ { pattern: /\bIO\.popen\b/, label: 'IO.popen' },
15
+ { pattern: /\bOpen3\b/, label: 'Open3' },
16
+ { pattern: /`[^`]+`/, label: 'backtick subshell' }
17
+ ].freeze
18
+
19
+ def scan(gem_path: nil, name: nil, gemspec: nil, source_path: nil)
20
+ results = CHECKS.map do |check|
21
+ send(check, gem_path: gem_path, name: name, gemspec: gemspec, source_path: source_path)
22
+ end
12
23
  {
13
24
  passed: results.all? { |r| r[:status] != :fail },
14
25
  checks: results,
@@ -43,6 +54,26 @@ module Legion
43
54
  { check: :gemspec_metadata, status: status,
44
55
  details: has_caps ? 'capabilities declared' : 'no capabilities declared' }
45
56
  end
57
+
58
+ def static_analysis(source_path:, **_)
59
+ return { check: :static_analysis, status: :skip, details: 'no source path' } unless source_path && Dir.exist?(source_path.to_s)
60
+
61
+ findings = []
62
+ Dir.glob(File.join(source_path, '**', '*.rb')).each do |file|
63
+ relative = file.delete_prefix("#{source_path}/")
64
+ File.foreach(file).with_index(1) do |line, lineno|
65
+ DANGEROUS_PATTERNS.each do |entry|
66
+ findings << "#{relative}:#{lineno} #{entry[:label]}" if line.match?(entry[:pattern])
67
+ end
68
+ end
69
+ end
70
+
71
+ if findings.empty?
72
+ { check: :static_analysis, status: :pass, details: 'no dangerous patterns found' }
73
+ else
74
+ { check: :static_analysis, status: :warn, details: findings.join('; ') }
75
+ end
76
+ end
46
77
  end
47
78
  end
48
79
  end
@@ -40,7 +40,15 @@ module Legion
40
40
 
41
41
  class << self
42
42
  def register(entry)
43
+ raise ArgumentError, "Extension name '#{entry.name}' violates naming convention" if defined?(Governance) && !Governance.check_name(entry.name)
44
+
43
45
  store[entry.name] = entry
46
+
47
+ if defined?(Governance) && Governance.auto_approve?(entry.risk_tier)
48
+ update_entry(entry.name, entry, status: :approved, airb_status: 'approved', approved_at: Time.now.utc)
49
+ end
50
+
51
+ Persistence.persist(store[entry.name]) if defined?(Persistence)
44
52
  end
45
53
 
46
54
  def unregister(name)
@@ -147,7 +155,11 @@ module Legion
147
155
  def update_entry(name, entry, **overrides)
148
156
  attrs = entry.to_h.merge(overrides)
149
157
  store[name.to_s] = Entry.new(**attrs)
158
+ Persistence.persist(store[name.to_s]) if defined?(Persistence)
150
159
  end
151
160
  end
152
161
  end
153
162
  end
163
+
164
+ require_relative 'registry/persistence'
165
+ require_relative 'registry/governance'
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'readiness'
4
+ require_relative 'process_role'
4
5
 
5
6
  module Legion
6
7
  class Service
@@ -11,8 +12,20 @@ module Legion
11
12
  base.freeze
12
13
  end
13
14
 
14
- def initialize(transport: true, cache: true, data: true, supervision: true, extensions: true, # rubocop:disable Metrics/CyclomaticComplexity,Metrics/ParameterLists,Metrics/MethodLength,Metrics/PerceivedComplexity
15
- crypt: true, api: true, llm: true, gaia: true, log_level: 'info', http_port: nil)
15
+ def initialize(transport: nil, cache: nil, data: nil, supervision: nil, extensions: nil, # rubocop:disable Metrics/CyclomaticComplexity,Metrics/ParameterLists,Metrics/MethodLength,Metrics/PerceivedComplexity,Metrics/AbcSize
16
+ crypt: nil, api: nil, llm: nil, gaia: nil, log_level: 'info', http_port: nil,
17
+ role: nil)
18
+ role_opts = Legion::ProcessRole.resolve(role || Legion::ProcessRole.current)
19
+ transport = role_opts[:transport] if transport.nil?
20
+ cache = role_opts[:cache] if cache.nil?
21
+ data = role_opts[:data] if data.nil?
22
+ supervision = role_opts[:supervision] if supervision.nil?
23
+ extensions = role_opts[:extensions] if extensions.nil?
24
+ crypt = role_opts[:crypt] if crypt.nil?
25
+ api = role_opts[:api] if api.nil?
26
+ llm = role_opts[:llm] if llm.nil?
27
+ gaia = role_opts[:gaia] if gaia.nil?
28
+
16
29
  setup_logging(log_level: log_level)
17
30
  Legion::Logging.debug('Starting Legion::Service')
18
31
  setup_settings
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Legion
4
- VERSION = '1.4.107'
4
+ VERSION = '1.4.109'
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.4.107
4
+ version: 1.4.109
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
@@ -483,6 +483,7 @@ files:
483
483
  - lib/legion/cli/dataset_command.rb
484
484
  - lib/legion/cli/detect_command.rb
485
485
  - lib/legion/cli/docs_command.rb
486
+ - lib/legion/cli/doctor.rb
486
487
  - lib/legion/cli/doctor/bundle_check.rb
487
488
  - lib/legion/cli/doctor/cache_check.rb
488
489
  - lib/legion/cli/doctor/config_check.rb
@@ -587,6 +588,9 @@ files:
587
588
  - lib/legion/cli/update_command.rb
588
589
  - lib/legion/cli/version.rb
589
590
  - lib/legion/cli/worker_command.rb
591
+ - lib/legion/cluster.rb
592
+ - lib/legion/cluster/leader.rb
593
+ - lib/legion/cluster/lock.rb
590
594
  - lib/legion/context.rb
591
595
  - lib/legion/data/local_migrations/20260319000001_create_extension_catalog.rb
592
596
  - lib/legion/data/local_migrations/20260319000002_create_extension_permissions.rb
@@ -647,8 +651,11 @@ files:
647
651
  - lib/legion/phi/access_log.rb
648
652
  - lib/legion/phi/erasure.rb
649
653
  - lib/legion/process.rb
654
+ - lib/legion/process_role.rb
650
655
  - lib/legion/readiness.rb
651
656
  - lib/legion/registry.rb
657
+ - lib/legion/registry/governance.rb
658
+ - lib/legion/registry/persistence.rb
652
659
  - lib/legion/registry/security_scanner.rb
653
660
  - lib/legion/runner.rb
654
661
  - lib/legion/runner/log.rb