legionio 1.6.9 → 1.6.11
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 +16 -0
- data/lib/legion/cli/bootstrap_command.rb +9 -3
- data/lib/legion/cli/broker_command.rb +152 -0
- data/lib/legion/cli/config_command.rb +6 -2
- data/lib/legion/cli/config_import.rb +32 -7
- data/lib/legion/cli.rb +4 -0
- data/lib/legion/dispatch/local.rb +40 -0
- data/lib/legion/dispatch.rb +26 -0
- data/lib/legion/extensions.rb +31 -1
- data/lib/legion/ingress.rb +16 -1
- data/lib/legion/service.rb +10 -0
- data/lib/legion/version.rb +1 -1
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 33862e1884c9ba2e72a1dc6920fb32626bce8a54a3a866065ae8413f3b351639
|
|
4
|
+
data.tar.gz: 7f78f235733a55b9561d28a454f8d3d5287814b5e4ae627ee83de821ef41a4ac
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 04ae595b050e2790044d84c382b2f42fa2dc34516245695f75728764d0365a203636c1cbd9dffbc7f0c18ec95586f06922310f2933fa6a38e4583a0d2307c5ff
|
|
7
|
+
data.tar.gz: 6ea1841ad50f8fb26c9243ea01194576aee7813721b5a1677c0452e8aa71e99ea5788430d003ac5aa1ea910a25310b565c62b17819c77d1e24cc811260d01eea
|
data/CHANGELOG.md
CHANGED
|
@@ -2,7 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [1.6.11] - 2026-03-26
|
|
6
|
+
|
|
5
7
|
### Added
|
|
8
|
+
- `Legion::Dispatch` module with pluggable strategy interface and `Local` implementation using `Concurrent::FixedThreadPool`
|
|
9
|
+
- Local dispatch wiring in `extensions.rb`: `dispatch_local_actors` registers non-remote extensions in thread pool
|
|
10
|
+
- `Ingress.local_runner?` short-circuit: runners for `remote_invocable? false` extensions skip AMQP round-trip
|
|
11
|
+
- `setup_dispatch` in `Service` boot sequence with graceful shutdown
|
|
12
|
+
- `legion broker stats` and `legion broker cleanup` CLI commands for RabbitMQ management
|
|
6
13
|
- End-to-end integration test for TBI Phase 5 self-generating functions loop (9 examples)
|
|
7
14
|
- Test dependencies: lex-codegen, lex-eval added to Gemfile for integration testing
|
|
8
15
|
- Specs for `legion codegen` CLI subcommand (8 subcommands, 22 examples)
|
|
@@ -15,6 +22,15 @@
|
|
|
15
22
|
- Move `Legion::LLM.chat` stub to `RSpec.configure before(:each)` block so it always intercepts regardless of whether the real `legion-llm` gem is loaded, preventing external LLM calls in integration tests
|
|
16
23
|
- Fix `service_setup_apollo_spec` "starts Apollo::Local" example: stub `Legion::Apollo.start` to prevent internal double-call of `Apollo::Local.start`
|
|
17
24
|
|
|
25
|
+
## [1.6.10] - 2026-03-26
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
- `ConfigImport.write_config` now splits recognized subsystem keys (`microsoft_teams`, `rbac`, `api`, `logging`, `gaia`, `extensions`, `llm`, `data`, `cache_local`, `cache`, `transport`, `crypt`, `role`) into individual `{key}.json` files
|
|
29
|
+
- Remaining unrecognized keys written to `bootstrapped_settings.json` (replaces `imported.json`)
|
|
30
|
+
- Subsystem files are always overwritten on bootstrap; remainder file respects `--force` for merge behavior
|
|
31
|
+
- `write_config` returns an array of written paths instead of a single path
|
|
32
|
+
- `legion bootstrap` and `legion config import` updated to display per-file write confirmations
|
|
33
|
+
|
|
18
34
|
## [1.6.9] - 2026-03-26
|
|
19
35
|
|
|
20
36
|
### Added
|
|
@@ -63,9 +63,15 @@ module Legion
|
|
|
63
63
|
results[:packs_requested] = pack_names
|
|
64
64
|
|
|
65
65
|
# 4. Write config
|
|
66
|
-
|
|
67
|
-
results[:config_written] =
|
|
68
|
-
|
|
66
|
+
paths = ConfigImport.write_config(config, force: options[:force])
|
|
67
|
+
results[:config_written] = paths
|
|
68
|
+
unless options[:json]
|
|
69
|
+
if paths.empty?
|
|
70
|
+
out.warn('No config files were written (config was empty after removing packs).')
|
|
71
|
+
else
|
|
72
|
+
paths.each { |p| out.success("Written: #{p}") }
|
|
73
|
+
end
|
|
74
|
+
end
|
|
69
75
|
|
|
70
76
|
# 5. Scaffold missing subsystem files
|
|
71
77
|
results[:scaffold] = run_scaffold(out)
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'net/http'
|
|
4
|
+
require 'erb'
|
|
5
|
+
require 'json'
|
|
6
|
+
|
|
7
|
+
module Legion
|
|
8
|
+
module CLI
|
|
9
|
+
class Broker < Thor
|
|
10
|
+
namespace 'broker'
|
|
11
|
+
|
|
12
|
+
def self.exit_on_failure?
|
|
13
|
+
true
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
class_option :json, type: :boolean, default: false, desc: 'Output as JSON'
|
|
17
|
+
class_option :no_color, type: :boolean, default: false, desc: 'Disable color output'
|
|
18
|
+
class_option :host, type: :string, default: 'localhost', desc: 'RabbitMQ management host'
|
|
19
|
+
class_option :port, type: :numeric, default: 15_672, desc: 'RabbitMQ management port'
|
|
20
|
+
class_option :user, type: :string, default: 'guest', desc: 'RabbitMQ management username'
|
|
21
|
+
class_option :password, type: :string, default: 'guest', desc: 'RabbitMQ management password'
|
|
22
|
+
class_option :vhost, type: :string, default: '/', desc: 'RabbitMQ vhost'
|
|
23
|
+
|
|
24
|
+
desc 'stats', 'Show RabbitMQ broker statistics (queues, exchanges, consumers, DLX)'
|
|
25
|
+
def stats
|
|
26
|
+
out = formatter
|
|
27
|
+
data = fetch_stats
|
|
28
|
+
|
|
29
|
+
if options[:json]
|
|
30
|
+
out.json(data)
|
|
31
|
+
else
|
|
32
|
+
out.header('RabbitMQ Broker Stats')
|
|
33
|
+
out.spacer
|
|
34
|
+
out.detail({
|
|
35
|
+
queues: data[:queues],
|
|
36
|
+
exchanges: data[:exchanges],
|
|
37
|
+
consumers: data[:consumers],
|
|
38
|
+
dlx: data[:dlx]
|
|
39
|
+
})
|
|
40
|
+
end
|
|
41
|
+
rescue Legion::CLI::Error => e
|
|
42
|
+
formatter.error(e.message)
|
|
43
|
+
exit(1)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
desc 'cleanup', 'Find (and optionally delete) orphaned queues with 0 consumers and 0 messages'
|
|
47
|
+
option :execute, type: :boolean, default: false, desc: 'Actually delete orphaned queues (default: dry-run)'
|
|
48
|
+
def cleanup
|
|
49
|
+
out = formatter
|
|
50
|
+
orphans = find_orphans
|
|
51
|
+
|
|
52
|
+
if orphans.empty?
|
|
53
|
+
out.success('No orphaned queues found')
|
|
54
|
+
return
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
if options[:json]
|
|
58
|
+
out.json({ orphaned_queues: orphans, deleted: options[:execute] })
|
|
59
|
+
delete_orphans(orphans) if options[:execute]
|
|
60
|
+
return
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
out.header("Orphaned Queues (#{orphans.size})")
|
|
64
|
+
orphans.each { |q| out.warn(q) }
|
|
65
|
+
out.spacer
|
|
66
|
+
|
|
67
|
+
if options[:execute]
|
|
68
|
+
delete_orphans(orphans)
|
|
69
|
+
out.success("Deleted #{orphans.size} orphaned queue(s)")
|
|
70
|
+
else
|
|
71
|
+
out.warn('Dry-run mode — pass --execute to delete')
|
|
72
|
+
end
|
|
73
|
+
rescue Legion::CLI::Error => e
|
|
74
|
+
formatter.error(e.message)
|
|
75
|
+
exit(1)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
no_commands do # rubocop:disable Metrics/BlockLength
|
|
79
|
+
def formatter
|
|
80
|
+
@formatter ||= Output::Formatter.new(
|
|
81
|
+
json: options[:json],
|
|
82
|
+
color: !options[:no_color]
|
|
83
|
+
)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
def vhost_encoded
|
|
89
|
+
ERB::Util.url_encode(options[:vhost])
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def management_api(path)
|
|
93
|
+
uri = URI("http://#{options[:host]}:#{options[:port]}/api#{path}")
|
|
94
|
+
req = Net::HTTP::Get.new(uri)
|
|
95
|
+
req.basic_auth(options[:user], options[:password])
|
|
96
|
+
|
|
97
|
+
response = Net::HTTP.start(uri.host, uri.port, open_timeout: 5, read_timeout: 10) do |http|
|
|
98
|
+
http.request(req)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
raise Legion::CLI::Error, "Management API error #{response.code}: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
|
|
102
|
+
|
|
103
|
+
::JSON.parse(response.body, symbolize_names: true)
|
|
104
|
+
rescue Errno::ECONNREFUSED
|
|
105
|
+
raise Legion::CLI::Error, "Cannot connect to RabbitMQ management API at #{options[:host]}:#{options[:port]}"
|
|
106
|
+
rescue Net::OpenTimeout, Net::ReadTimeout
|
|
107
|
+
raise Legion::CLI::Error, "Timed out connecting to RabbitMQ management API at #{options[:host]}:#{options[:port]}"
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def management_delete(path)
|
|
111
|
+
uri = URI("http://#{options[:host]}:#{options[:port]}/api#{path}")
|
|
112
|
+
req = Net::HTTP::Delete.new(uri)
|
|
113
|
+
req.basic_auth(options[:user], options[:password])
|
|
114
|
+
|
|
115
|
+
Net::HTTP.start(uri.host, uri.port, open_timeout: 5, read_timeout: 10) do |http|
|
|
116
|
+
http.request(req)
|
|
117
|
+
end
|
|
118
|
+
rescue Errno::ECONNREFUSED
|
|
119
|
+
raise Legion::CLI::Error, "Cannot connect to RabbitMQ management API at #{options[:host]}:#{options[:port]}"
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def fetch_stats
|
|
123
|
+
queues = management_api("/queues/#{vhost_encoded}")
|
|
124
|
+
exchanges = management_api("/exchanges/#{vhost_encoded}")
|
|
125
|
+
|
|
126
|
+
total_consumers = queues.sum { |q| q[:consumers].to_i }
|
|
127
|
+
dlx_count = queues.count { |q| q.dig(:arguments, :'x-dead-letter-exchange') }
|
|
128
|
+
|
|
129
|
+
{
|
|
130
|
+
queues: queues.size,
|
|
131
|
+
exchanges: exchanges.size,
|
|
132
|
+
consumers: total_consumers,
|
|
133
|
+
dlx: dlx_count
|
|
134
|
+
}
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def find_orphans
|
|
138
|
+
queues = management_api("/queues/#{vhost_encoded}")
|
|
139
|
+
queues
|
|
140
|
+
.select { |q| q[:consumers].to_i.zero? && q[:messages].to_i.zero? }
|
|
141
|
+
.map { |q| q[:name].to_s }
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def delete_orphans(orphans)
|
|
145
|
+
orphans.each do |name|
|
|
146
|
+
management_delete("/queues/#{vhost_encoded}/#{ERB::Util.url_encode(name)}")
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
@@ -192,10 +192,14 @@ module Legion
|
|
|
192
192
|
out.info("Fetching config from #{source}...")
|
|
193
193
|
body = ConfigImport.fetch_source(source)
|
|
194
194
|
config = ConfigImport.parse_payload(body)
|
|
195
|
-
|
|
195
|
+
paths = ConfigImport.write_config(config, force: options[:force])
|
|
196
196
|
summary = ConfigImport.summary(config)
|
|
197
197
|
|
|
198
|
-
|
|
198
|
+
if paths.empty?
|
|
199
|
+
out.warn('No config files were written (empty configuration).')
|
|
200
|
+
else
|
|
201
|
+
paths.each { |p| out.success("Written: #{p}") }
|
|
202
|
+
end
|
|
199
203
|
out.info("Sections: #{summary[:sections].join(', ')}")
|
|
200
204
|
if summary[:vault_clusters].any?
|
|
201
205
|
out.info("Vault clusters: #{summary[:vault_clusters].join(', ')}")
|
|
@@ -10,7 +10,12 @@ module Legion
|
|
|
10
10
|
module CLI
|
|
11
11
|
module ConfigImport
|
|
12
12
|
SETTINGS_DIR = File.expand_path('~/.legionio/settings')
|
|
13
|
-
|
|
13
|
+
BOOTSTRAPPED_FILE = 'bootstrapped_settings.json'
|
|
14
|
+
|
|
15
|
+
SUBSYSTEM_KEYS = %i[
|
|
16
|
+
microsoft_teams rbac api logging gaia extensions
|
|
17
|
+
llm data cache_local cache transport crypt role
|
|
18
|
+
].freeze
|
|
14
19
|
|
|
15
20
|
module_function
|
|
16
21
|
|
|
@@ -56,15 +61,35 @@ module Legion
|
|
|
56
61
|
|
|
57
62
|
def write_config(config, force: false)
|
|
58
63
|
FileUtils.mkdir_p(SETTINGS_DIR)
|
|
59
|
-
|
|
64
|
+
written = []
|
|
65
|
+
remainder = config.dup
|
|
66
|
+
|
|
67
|
+
SUBSYSTEM_KEYS.each do |key|
|
|
68
|
+
next unless remainder.key?(key)
|
|
69
|
+
|
|
70
|
+
subsystem_data = remainder.delete(key)
|
|
71
|
+
path = File.join(SETTINGS_DIR, "#{key}.json")
|
|
72
|
+
to_write = { key => subsystem_data }
|
|
73
|
+
if File.exist?(path) && !force
|
|
74
|
+
existing = ::JSON.parse(File.read(path), symbolize_names: true)
|
|
75
|
+
existing_subsystem = existing[key]
|
|
76
|
+
to_write = { key => deep_merge(existing_subsystem, subsystem_data) } if existing_subsystem.is_a?(Hash) && subsystem_data.is_a?(Hash)
|
|
77
|
+
end
|
|
78
|
+
File.write(path, "#{::JSON.pretty_generate(to_write)}\n")
|
|
79
|
+
written << path
|
|
80
|
+
end
|
|
60
81
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
82
|
+
unless remainder.empty?
|
|
83
|
+
path = File.join(SETTINGS_DIR, BOOTSTRAPPED_FILE)
|
|
84
|
+
if File.exist?(path) && !force
|
|
85
|
+
existing = ::JSON.parse(File.read(path), symbolize_names: true)
|
|
86
|
+
remainder = deep_merge(existing, remainder)
|
|
87
|
+
end
|
|
88
|
+
File.write(path, "#{::JSON.pretty_generate(remainder)}\n")
|
|
89
|
+
written << path
|
|
64
90
|
end
|
|
65
91
|
|
|
66
|
-
|
|
67
|
-
path
|
|
92
|
+
written
|
|
68
93
|
end
|
|
69
94
|
|
|
70
95
|
def deep_merge(base, overlay)
|
data/lib/legion/cli.rb
CHANGED
|
@@ -66,6 +66,7 @@ module Legion
|
|
|
66
66
|
autoload :Debug, 'legion/cli/debug_command'
|
|
67
67
|
autoload :CodegenCommand, 'legion/cli/codegen_command'
|
|
68
68
|
autoload :Bootstrap, 'legion/cli/bootstrap_command'
|
|
69
|
+
autoload :Broker, 'legion/cli/broker_command'
|
|
69
70
|
|
|
70
71
|
module Groups
|
|
71
72
|
autoload :Ai, 'legion/cli/groups/ai_group'
|
|
@@ -275,6 +276,9 @@ module Legion
|
|
|
275
276
|
desc 'dev SUBCOMMAND', 'Generators, docs, marketplace, and shell completion'
|
|
276
277
|
subcommand 'dev', Legion::CLI::Groups::Dev
|
|
277
278
|
|
|
279
|
+
desc 'broker SUBCOMMAND', 'RabbitMQ broker management (stats, cleanup)'
|
|
280
|
+
subcommand 'broker', Legion::CLI::Broker
|
|
281
|
+
|
|
278
282
|
desc 'tree', 'Print a tree of all available commands'
|
|
279
283
|
def tree
|
|
280
284
|
legion_print_command_tree(self.class, ::File.basename($PROGRAM_NAME), '')
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'concurrent-ruby'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Dispatch
|
|
7
|
+
class Local
|
|
8
|
+
def initialize(pool_size: nil)
|
|
9
|
+
max = pool_size || Legion::Settings.dig(:dispatch, :local_pool_size) || 8
|
|
10
|
+
@pool = Concurrent::FixedThreadPool.new(max)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def start; end
|
|
14
|
+
|
|
15
|
+
def submit(&block)
|
|
16
|
+
@pool.post do
|
|
17
|
+
block.call
|
|
18
|
+
rescue StandardError => e
|
|
19
|
+
Legion::Logging.error "[Dispatch::Local] #{e.message}" if defined?(Legion::Logging)
|
|
20
|
+
Legion::Logging.debug e.backtrace&.first(5) if defined?(Legion::Logging)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def stop
|
|
25
|
+
return unless @pool.running?
|
|
26
|
+
|
|
27
|
+
@pool.shutdown
|
|
28
|
+
@pool.wait_for_termination(15)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def capacity
|
|
32
|
+
{
|
|
33
|
+
pool_size: @pool.max_length,
|
|
34
|
+
queue_length: @pool.queue_length,
|
|
35
|
+
running: @pool.running?
|
|
36
|
+
}
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'dispatch/local'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Dispatch
|
|
7
|
+
class << self
|
|
8
|
+
def dispatcher
|
|
9
|
+
@dispatcher ||= Local.new
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def submit(&)
|
|
13
|
+
dispatcher.submit(&)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def shutdown
|
|
17
|
+
@dispatcher&.stop
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def reset!
|
|
21
|
+
@dispatcher&.stop
|
|
22
|
+
@dispatcher = nil
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
data/lib/legion/extensions.rb
CHANGED
|
@@ -89,6 +89,8 @@ module Legion
|
|
|
89
89
|
|
|
90
90
|
@running_instances&.clear
|
|
91
91
|
|
|
92
|
+
Legion::Dispatch.shutdown if defined?(Legion::Dispatch) && Legion::Dispatch.instance_variable_get(:@dispatcher)
|
|
93
|
+
|
|
92
94
|
@loaded_extensions.each do |name|
|
|
93
95
|
Catalog.transition(name, :stopped)
|
|
94
96
|
unregister_capabilities(name)
|
|
@@ -283,6 +285,7 @@ module Legion
|
|
|
283
285
|
end
|
|
284
286
|
|
|
285
287
|
hook_subscription_actors_pooled(sub_actors) unless sub_actors.empty?
|
|
288
|
+
dispatch_local_actors(@local_tasks) unless @local_tasks.empty?
|
|
286
289
|
|
|
287
290
|
@pending_actors.clear
|
|
288
291
|
Legion::Logging.info(
|
|
@@ -290,7 +293,8 @@ module Legion
|
|
|
290
293
|
"every:#{@timer_tasks.count}," \
|
|
291
294
|
"poll:#{@poll_tasks.count}," \
|
|
292
295
|
"once:#{@once_tasks.count}," \
|
|
293
|
-
"loop:#{@loop_tasks.count}"
|
|
296
|
+
"loop:#{@loop_tasks.count}," \
|
|
297
|
+
"local:#{@local_tasks.count}"
|
|
294
298
|
)
|
|
295
299
|
@loaded_extensions&.each { |name| Catalog.transition(name, :running) }
|
|
296
300
|
end
|
|
@@ -466,6 +470,32 @@ module Legion
|
|
|
466
470
|
true
|
|
467
471
|
end
|
|
468
472
|
|
|
473
|
+
def dispatch_local_actors(actors)
|
|
474
|
+
require 'legion/dispatch'
|
|
475
|
+
|
|
476
|
+
actors.each do |actor_hash|
|
|
477
|
+
ext_name = actor_hash[:extension_name]
|
|
478
|
+
|
|
479
|
+
runner_mod = actor_hash[:runner_class]
|
|
480
|
+
unless runner_mod
|
|
481
|
+
actor_str = actor_hash[:actor_class].to_s
|
|
482
|
+
runner_str = actor_str.sub('::Actor::', '::Runners::')
|
|
483
|
+
runner_mod = begin
|
|
484
|
+
Kernel.const_get(runner_str)
|
|
485
|
+
rescue NameError
|
|
486
|
+
Legion::Logging.warn "[LocalDispatch] runner not found for #{ext_name}: #{runner_str}" if defined?(Legion::Logging)
|
|
487
|
+
next
|
|
488
|
+
end
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
actor_hash[:runner_module] = runner_mod
|
|
492
|
+
actor_hash[:running_class] = actor_hash[:actor_class]
|
|
493
|
+
@running_instances&.push(actor_hash[:actor_class])
|
|
494
|
+
|
|
495
|
+
Legion::Logging.info "[LocalDispatch] registered: #{ext_name}/#{actor_hash[:actor_name]}" if defined?(Legion::Logging)
|
|
496
|
+
end
|
|
497
|
+
end
|
|
498
|
+
|
|
469
499
|
public
|
|
470
500
|
|
|
471
501
|
def unregister_capabilities(gem_name)
|
data/lib/legion/ingress.rb
CHANGED
|
@@ -39,7 +39,7 @@ module Legion
|
|
|
39
39
|
|
|
40
40
|
# Normalize and execute via Legion::Runner.run.
|
|
41
41
|
# Returns the runner result hash.
|
|
42
|
-
def run(payload:, runner_class: nil, function: nil, source: 'unknown', principal: nil, **opts) # rubocop:disable Metrics/ParameterLists,Metrics/MethodLength,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
|
|
42
|
+
def run(payload:, runner_class: nil, function: nil, source: 'unknown', principal: nil, **opts) # rubocop:disable Metrics/ParameterLists,Metrics/MethodLength,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity,Metrics/AbcSize
|
|
43
43
|
Legion::Logging.info "[Ingress] run: source=#{source} runner_class=#{runner_class} function=#{function}" if defined?(Legion::Logging)
|
|
44
44
|
check_subtask = opts.fetch(:check_subtask, true)
|
|
45
45
|
generate_task = opts.fetch(:generate_task, true)
|
|
@@ -79,6 +79,12 @@ module Legion
|
|
|
79
79
|
|
|
80
80
|
Legion::Events.emit('ingress.received', runner_class: rc.to_s, function: fn, source: source)
|
|
81
81
|
|
|
82
|
+
if local_runner?(rc)
|
|
83
|
+
Legion::Logging.debug "[Ingress] local short-circuit: #{rc}.#{fn}" if defined?(Legion::Logging)
|
|
84
|
+
klass = rc.is_a?(String) ? Kernel.const_get(rc) : rc
|
|
85
|
+
return klass.send(fn.to_sym, **message)
|
|
86
|
+
end
|
|
87
|
+
|
|
82
88
|
runner_block = lambda {
|
|
83
89
|
Legion::Runner.run(
|
|
84
90
|
runner_class: rc,
|
|
@@ -114,6 +120,15 @@ module Legion
|
|
|
114
120
|
{ success: false, status: 'task.blocked', error: { code: 'insufficient_consent', message: e.message } }
|
|
115
121
|
end
|
|
116
122
|
|
|
123
|
+
def local_runner?(runner_class)
|
|
124
|
+
return false unless defined?(Legion::Extensions) && Legion::Extensions.local_tasks.is_a?(Array)
|
|
125
|
+
|
|
126
|
+
klass = runner_class.is_a?(String) ? Kernel.const_get(runner_class) : runner_class
|
|
127
|
+
Legion::Extensions.local_tasks.any? { |t| t[:runner_module] == klass }
|
|
128
|
+
rescue NameError
|
|
129
|
+
false
|
|
130
|
+
end
|
|
131
|
+
|
|
117
132
|
private
|
|
118
133
|
|
|
119
134
|
def parse_payload(payload)
|
data/lib/legion/service.rb
CHANGED
|
@@ -50,6 +50,8 @@ module Legion
|
|
|
50
50
|
register_logging_hooks
|
|
51
51
|
end
|
|
52
52
|
|
|
53
|
+
setup_dispatch
|
|
54
|
+
|
|
53
55
|
if cache
|
|
54
56
|
begin
|
|
55
57
|
require 'legion/cache'
|
|
@@ -339,6 +341,12 @@ module Legion
|
|
|
339
341
|
Legion::Logging.warn "Legion::Apollo failed to load: #{e.message}"
|
|
340
342
|
end
|
|
341
343
|
|
|
344
|
+
def setup_dispatch
|
|
345
|
+
require 'legion/dispatch'
|
|
346
|
+
Legion::Dispatch.dispatcher.start
|
|
347
|
+
Legion::Logging.info "[Service] Dispatch started (strategy: #{Legion::Dispatch.dispatcher.class.name})"
|
|
348
|
+
end
|
|
349
|
+
|
|
342
350
|
def setup_transport
|
|
343
351
|
Legion::Logging.info 'Setting up Legion::Transport'
|
|
344
352
|
require 'legion/transport'
|
|
@@ -505,6 +513,8 @@ module Legion
|
|
|
505
513
|
@cluster_leader = nil
|
|
506
514
|
end
|
|
507
515
|
|
|
516
|
+
shutdown_component('Dispatch') { Legion::Dispatch.shutdown } if defined?(Legion::Dispatch)
|
|
517
|
+
|
|
508
518
|
ext_timeout = Legion::Settings.dig(:extensions, :shutdown_timeout) || 15
|
|
509
519
|
shutdown_component('Extensions', timeout: ext_timeout) { Legion::Extensions.shutdown }
|
|
510
520
|
Legion::Readiness.mark_not_ready(:extensions)
|
data/lib/legion/version.rb
CHANGED
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.6.
|
|
4
|
+
version: 1.6.11
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Esity
|
|
@@ -514,6 +514,7 @@ files:
|
|
|
514
514
|
- lib/legion/cli/audit_command.rb
|
|
515
515
|
- lib/legion/cli/auth_command.rb
|
|
516
516
|
- lib/legion/cli/bootstrap_command.rb
|
|
517
|
+
- lib/legion/cli/broker_command.rb
|
|
517
518
|
- lib/legion/cli/chain.rb
|
|
518
519
|
- lib/legion/cli/chain_command.rb
|
|
519
520
|
- lib/legion/cli/chat/agent_delegator.rb
|
|
@@ -734,6 +735,8 @@ files:
|
|
|
734
735
|
- lib/legion/digital_worker/registry.rb
|
|
735
736
|
- lib/legion/digital_worker/risk_tier.rb
|
|
736
737
|
- lib/legion/digital_worker/value_metrics.rb
|
|
738
|
+
- lib/legion/dispatch.rb
|
|
739
|
+
- lib/legion/dispatch/local.rb
|
|
737
740
|
- lib/legion/docs/site_generator.rb
|
|
738
741
|
- lib/legion/events.rb
|
|
739
742
|
- lib/legion/extensions.rb
|