legionio 1.6.25 → 1.6.26
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 +24 -0
- data/lib/legion/api/lex_dispatch.rb +65 -2
- data/lib/legion/api/openapi.rb +1 -22
- data/lib/legion/api.rb +3 -67
- data/lib/legion/cli/admin_command.rb +87 -0
- data/lib/legion/cli/broker_command.rb +34 -0
- data/lib/legion/cli.rb +1 -0
- data/lib/legion/extensions/builders/absorbers.rb +16 -0
- data/lib/legion/extensions/builders/routes.rb +1 -1
- data/lib/legion/extensions/core.rb +2 -27
- data/lib/legion/extensions/transport.rb +36 -0
- data/lib/legion/version.rb +1 -1
- metadata +2 -3
- data/lib/legion/api/hooks.rb +0 -109
- data/lib/legion/api/lex.rb +0 -78
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 87e232e8560f2fc19eab52867dca532552949fa8ab6f2637f505454ce7c3f435
|
|
4
|
+
data.tar.gz: d98b360b63301c86f808f2b0f2baac8c4671c871847233eb9a6d9f5ca5dd9b61
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 77bc48601a5384ac7b2b9e9185eebb73a9882b492e15e811b1377b29d672758e648a49a5385e1287e2b49724e4e7e2393ee1452b79cd700aac5171a21cf624e3
|
|
7
|
+
data.tar.gz: 7e31cd03e21c4263bb6b64dbfa0d7e7ef3aa51e0ecfa8801292084cc97ce6850c9795620dbb3fc3f235b9efd84c71a99b1ff41580f34524fcfdd8448b6f48225
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [1.6.26] - 2026-03-28
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- Absorber Router registration: `builders/absorbers.rb` now registers absorbers with `Legion::API.router` for v3.0 API discovery and dispatch (component_type: `absorbers`)
|
|
9
|
+
- Hook-aware LexDispatch: `POST /api/extensions/:lex/hooks/:name/:method` applies verify/route/transform lifecycle for `Hooks::Base` subclasses; auto-generated hooks pass through unchanged
|
|
10
|
+
- Transport message auto-generation: `auto_generate_messages` in `extensions/transport.rb` creates `Legion::Transport::Message` subclasses from runner definitions with inputs at boot time; explicit classes always take precedence
|
|
11
|
+
- `legion broker purge-topology` CLI command: detects old v2.0 AMQP exchanges (`legion.*`) that have v3.0 counterparts (`lex.*`) and optionally deletes them via RabbitMQ management API; defaults to `--dry-run`
|
|
12
|
+
- `spec/api/lex_dispatch_spec.rb`: 10-example spec covering v3.0 LexDispatch routes (replaces old lex_spec.rb)
|
|
13
|
+
- `spec/api/lex_dispatch_hooks_spec.rb`: 5-example spec for hook-aware dispatch (401/422/success/passthrough)
|
|
14
|
+
- `spec/api/old_systems_removed_spec.rb`: 10-example spec verifying old registries are gone
|
|
15
|
+
- `spec/cli/admin_command_spec.rb`: 21-example spec for topology detection logic
|
|
16
|
+
- `spec/extensions/builders/absorbers_spec.rb`: 10-example spec for absorber builder + Router registration
|
|
17
|
+
- `spec/extensions/transport_auto_messages_spec.rb`: 14-example spec for message auto-generation
|
|
18
|
+
- `unless defined?` guards on `Routes::Gaia`, `Routes::Transport`, `Routes::Rbac` registration for library gem self-registration
|
|
19
|
+
|
|
20
|
+
### Removed
|
|
21
|
+
- `Routes::Lex` (`api/lex.rb`): old `/api/lex/*` wildcard dispatcher — use `/api/extensions/:lex/runners/:name/:method`
|
|
22
|
+
- `Routes::Hooks` (`api/hooks.rb`): old `/api/hooks/lex/*` handler — use `/api/extensions/:lex/hooks/:name/:method`
|
|
23
|
+
- `Legion::API.hook_registry`, `.register_hook`, `.find_hook`, `.find_hook_by_path`, `.registered_hooks` — hooks auto-register via builder
|
|
24
|
+
- `Legion::API.route_registry`, `.register_route`, `.find_route_by_path`, `.registered_routes` — routes auto-register via builder
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
- Routes builder log message now uses v3.0 path format (`/api/extensions/...` instead of `/api/lex/...`)
|
|
28
|
+
|
|
5
29
|
## [1.6.25] - 2026-03-28
|
|
6
30
|
|
|
7
31
|
### Added
|
|
@@ -64,7 +64,7 @@ module Legion
|
|
|
64
64
|
end
|
|
65
65
|
end
|
|
66
66
|
|
|
67
|
-
def self.dispatch_request(context, request, params) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
|
|
67
|
+
def self.dispatch_request(context, request, params) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize,Metrics/CyclomaticComplexity
|
|
68
68
|
content_type = 'application/json'
|
|
69
69
|
context.content_type content_type
|
|
70
70
|
|
|
@@ -133,6 +133,10 @@ module Legion
|
|
|
133
133
|
end
|
|
134
134
|
end
|
|
135
135
|
|
|
136
|
+
# Hook-aware dispatch: when component_type is 'hooks' and the runner class
|
|
137
|
+
# is a Hooks::Base subclass, apply verify -> route -> transform -> Ingress.
|
|
138
|
+
return dispatch_hook(context, request, entry, payload, envelope) if entry[:component_type] == 'hooks' && hook_base_subclass?(entry[:runner_class])
|
|
139
|
+
|
|
136
140
|
result = Legion::Ingress.run(
|
|
137
141
|
payload: payload.merge(envelope.slice(:task_id, :conversation_id, :parent_id, :master_id, :chain_id)),
|
|
138
142
|
runner_class: entry[:runner_class],
|
|
@@ -224,9 +228,68 @@ module Legion
|
|
|
224
228
|
raise
|
|
225
229
|
end
|
|
226
230
|
|
|
231
|
+
def self.hook_base_subclass?(runner_class)
|
|
232
|
+
return false unless defined?(Legion::Extensions::Hooks::Base)
|
|
233
|
+
return false if runner_class.nil?
|
|
234
|
+
|
|
235
|
+
klass = runner_class.is_a?(Class) ? runner_class : Kernel.const_get(runner_class.to_s)
|
|
236
|
+
klass < Legion::Extensions::Hooks::Base
|
|
237
|
+
rescue NameError, TypeError
|
|
238
|
+
false
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def self.dispatch_hook(context, request, entry, payload, envelope)
|
|
242
|
+
hook = entry[:runner_class].new
|
|
243
|
+
|
|
244
|
+
# Re-read body for verification (request body was already read for payload parsing)
|
|
245
|
+
request.body.rewind
|
|
246
|
+
body_for_verify = request.body.read
|
|
247
|
+
request.body.rewind
|
|
248
|
+
|
|
249
|
+
unless hook.verify(request.env, body_for_verify)
|
|
250
|
+
context.halt 401, Legion::JSON.dump({
|
|
251
|
+
task_id: nil, conversation_id: nil, status: 'failed',
|
|
252
|
+
error: { code: 401, message: 'hook verification failed' }
|
|
253
|
+
})
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
function = hook.route(request.env, payload)
|
|
257
|
+
unless function
|
|
258
|
+
context.halt 422, Legion::JSON.dump({
|
|
259
|
+
task_id: nil, conversation_id: nil, status: 'failed',
|
|
260
|
+
error: { code: 422, message: 'hook could not route this event' }
|
|
261
|
+
})
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# If the hook defines the routed function as an instance method, call it to transform
|
|
265
|
+
if hook.class.method_defined?(function) && hook.class.instance_method(function).owner != Legion::Extensions::Hooks::Base
|
|
266
|
+
transformed = hook.send(function, payload)
|
|
267
|
+
payload = transformed if transformed
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
runner = hook.runner_class || entry[:runner_class]
|
|
271
|
+
|
|
272
|
+
result = Legion::Ingress.run(
|
|
273
|
+
payload: payload.merge(envelope.slice(:task_id, :conversation_id, :parent_id, :master_id, :chain_id)),
|
|
274
|
+
runner_class: runner,
|
|
275
|
+
function: function,
|
|
276
|
+
source: 'hook',
|
|
277
|
+
check_subtask: true,
|
|
278
|
+
generate_task: true
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
response_body = envelope.merge(
|
|
282
|
+
status: result[:status],
|
|
283
|
+
result: result[:result]
|
|
284
|
+
).compact
|
|
285
|
+
|
|
286
|
+
Legion::JSON.dump(response_body)
|
|
287
|
+
end
|
|
288
|
+
|
|
227
289
|
class << self
|
|
228
290
|
private :register_discovery, :register_dispatch, :dispatch_request, :parse_header_integer,
|
|
229
|
-
:build_envelope, :extension_loaded_locally?, :definition_blocks_remote?, :dispatch_async_amqp
|
|
291
|
+
:build_envelope, :extension_loaded_locally?, :definition_blocks_remote?, :dispatch_async_amqp,
|
|
292
|
+
:hook_base_subclass?, :dispatch_hook
|
|
230
293
|
end
|
|
231
294
|
end
|
|
232
295
|
end
|
data/lib/legion/api/openapi.rb
CHANGED
|
@@ -1125,7 +1125,7 @@ module Legion
|
|
|
1125
1125
|
private_class_method :lex_route_responses
|
|
1126
1126
|
|
|
1127
1127
|
def self.lex_paths
|
|
1128
|
-
|
|
1128
|
+
{
|
|
1129
1129
|
'/api/lex' => {
|
|
1130
1130
|
get: {
|
|
1131
1131
|
tags: ['Lex'],
|
|
@@ -1156,27 +1156,6 @@ module Legion
|
|
|
1156
1156
|
}
|
|
1157
1157
|
}
|
|
1158
1158
|
}
|
|
1159
|
-
|
|
1160
|
-
# Auto-routes (LEX)
|
|
1161
|
-
if defined?(Legion::API) && Legion::API.respond_to?(:registered_routes)
|
|
1162
|
-
Legion::API.registered_routes.each do |route|
|
|
1163
|
-
path_key = "/api/lex/#{route[:route_path]}"
|
|
1164
|
-
base[path_key] = {
|
|
1165
|
-
post: {
|
|
1166
|
-
operationId: "#{route[:lex_name]}.#{route[:runner_name]}.#{route[:function]}",
|
|
1167
|
-
summary: "Invoke #{route[:runner_name]}##{route[:function]} on lex-#{route[:lex_name]}",
|
|
1168
|
-
tags: [route[:lex_name]],
|
|
1169
|
-
requestBody: {
|
|
1170
|
-
required: false,
|
|
1171
|
-
content: { 'application/json' => { schema: { type: 'object' } } }
|
|
1172
|
-
},
|
|
1173
|
-
responses: lex_route_responses
|
|
1174
|
-
}
|
|
1175
|
-
}
|
|
1176
|
-
end
|
|
1177
|
-
end
|
|
1178
|
-
|
|
1179
|
-
base
|
|
1180
1159
|
end
|
|
1181
1160
|
private_class_method :lex_paths
|
|
1182
1161
|
|
data/lib/legion/api.rb
CHANGED
|
@@ -20,8 +20,6 @@ require_relative 'api/chains'
|
|
|
20
20
|
require_relative 'api/settings'
|
|
21
21
|
require_relative 'api/events'
|
|
22
22
|
require_relative 'api/transport'
|
|
23
|
-
require_relative 'api/hooks'
|
|
24
|
-
require_relative 'api/lex'
|
|
25
23
|
require_relative 'api/workers'
|
|
26
24
|
require_relative 'api/coldstart'
|
|
27
25
|
require_relative 'api/gaia'
|
|
@@ -141,13 +139,11 @@ module Legion
|
|
|
141
139
|
register Routes::Chains
|
|
142
140
|
register Routes::Settings
|
|
143
141
|
register Routes::Events
|
|
144
|
-
register Routes::Transport
|
|
145
|
-
register Routes::Hooks
|
|
146
|
-
register Routes::Lex
|
|
142
|
+
register Routes::Transport unless defined?(Legion::Transport::Routes)
|
|
147
143
|
register Routes::Workers
|
|
148
144
|
register Routes::Coldstart
|
|
149
|
-
register Routes::Gaia
|
|
150
|
-
register Routes::Rbac
|
|
145
|
+
register Routes::Gaia unless defined?(Legion::Gaia::Routes)
|
|
146
|
+
register Routes::Rbac unless defined?(Legion::Rbac::Routes)
|
|
151
147
|
register Routes::Auth
|
|
152
148
|
register Routes::AuthWorker
|
|
153
149
|
register Routes::AuthHuman
|
|
@@ -178,65 +174,5 @@ module Legion
|
|
|
178
174
|
@router ||= Legion::API::Router.new
|
|
179
175
|
end
|
|
180
176
|
end
|
|
181
|
-
|
|
182
|
-
# Hook registry (preserved from original implementation)
|
|
183
|
-
class << self
|
|
184
|
-
def hook_registry
|
|
185
|
-
@hook_registry ||= {}
|
|
186
|
-
end
|
|
187
|
-
|
|
188
|
-
def register_hook(lex_name:, hook_name:, hook_class:, default_runner: nil, route_path: nil)
|
|
189
|
-
route = route_path || "#{lex_name}/#{hook_name}"
|
|
190
|
-
key = route
|
|
191
|
-
hook_registry[key] = {
|
|
192
|
-
lex_name: lex_name,
|
|
193
|
-
hook_name: hook_name,
|
|
194
|
-
hook_class: hook_class,
|
|
195
|
-
default_runner: default_runner,
|
|
196
|
-
route_path: route
|
|
197
|
-
}
|
|
198
|
-
Legion::Logging.debug "Registered hook endpoint: /api/hooks/lex/#{route}"
|
|
199
|
-
end
|
|
200
|
-
|
|
201
|
-
def find_hook(lex_name, hook_name = nil)
|
|
202
|
-
if hook_name
|
|
203
|
-
hook_registry["#{lex_name}/#{hook_name}"]
|
|
204
|
-
else
|
|
205
|
-
hook_registry["#{lex_name}/webhook"] ||
|
|
206
|
-
hook_registry.values.find { |h| h[:lex_name] == lex_name }
|
|
207
|
-
end
|
|
208
|
-
end
|
|
209
|
-
|
|
210
|
-
def find_hook_by_path(path)
|
|
211
|
-
hook_registry[path] || hook_registry.values.find { |h| h[:route_path] == path }
|
|
212
|
-
end
|
|
213
|
-
|
|
214
|
-
def registered_hooks
|
|
215
|
-
hook_registry.values
|
|
216
|
-
end
|
|
217
|
-
|
|
218
|
-
def route_registry
|
|
219
|
-
@route_registry ||= {}
|
|
220
|
-
end
|
|
221
|
-
|
|
222
|
-
def register_route(lex_name:, runner_name:, function:, runner_class:, route_path:)
|
|
223
|
-
route_registry[route_path] = {
|
|
224
|
-
lex_name: lex_name,
|
|
225
|
-
runner_name: runner_name,
|
|
226
|
-
function: function,
|
|
227
|
-
runner_class: runner_class,
|
|
228
|
-
route_path: route_path
|
|
229
|
-
}
|
|
230
|
-
Legion::Logging.debug "Registered LEX route: POST /api/lex/#{route_path}"
|
|
231
|
-
end
|
|
232
|
-
|
|
233
|
-
def find_route_by_path(path)
|
|
234
|
-
route_registry[path]
|
|
235
|
-
end
|
|
236
|
-
|
|
237
|
-
def registered_routes
|
|
238
|
-
route_registry.values
|
|
239
|
-
end
|
|
240
|
-
end
|
|
241
177
|
end
|
|
242
178
|
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'net/http'
|
|
4
|
+
require 'uri'
|
|
5
|
+
|
|
6
|
+
module Legion
|
|
7
|
+
module CLI
|
|
8
|
+
class AdminCommand < Thor
|
|
9
|
+
namespace :admin
|
|
10
|
+
|
|
11
|
+
desc 'purge-topology', 'Remove old v2.0 AMQP exchanges (legion.* that have lex.* counterparts)'
|
|
12
|
+
method_option :dry_run, type: :boolean, default: true, desc: 'List without deleting'
|
|
13
|
+
method_option :execute, type: :boolean, default: false, desc: 'Actually delete exchanges'
|
|
14
|
+
method_option :host, type: :string, default: 'localhost', desc: 'RabbitMQ management host'
|
|
15
|
+
method_option :port, type: :numeric, default: 15_672, desc: 'RabbitMQ management port'
|
|
16
|
+
method_option :user, type: :string, default: 'guest', desc: 'RabbitMQ management user'
|
|
17
|
+
method_option :password, type: :string, default: 'guest', desc: 'RabbitMQ management password'
|
|
18
|
+
method_option :vhost, type: :string, default: '/', desc: 'RabbitMQ vhost'
|
|
19
|
+
def purge_topology
|
|
20
|
+
exchanges = fetch_exchanges
|
|
21
|
+
candidates = self.class.detect_old_exchanges(exchanges)
|
|
22
|
+
|
|
23
|
+
if candidates.empty?
|
|
24
|
+
say 'No old v2.0 topology exchanges found.', :green
|
|
25
|
+
return
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
say "Found #{candidates.size} old v2.0 exchange(s):", :yellow
|
|
29
|
+
candidates.each { |e| say " #{e[:name]} (#{e[:type]})" }
|
|
30
|
+
|
|
31
|
+
if options[:execute] && !options[:dry_run]
|
|
32
|
+
candidates.each do |exchange|
|
|
33
|
+
delete_exchange(exchange[:name])
|
|
34
|
+
say " Deleted: #{exchange[:name]}", :red
|
|
35
|
+
end
|
|
36
|
+
say "Purged #{candidates.size} exchange(s).", :green
|
|
37
|
+
else
|
|
38
|
+
say "\nDry run. Use --execute --no-dry-run to delete.", :cyan
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def self.detect_old_exchanges(exchanges)
|
|
43
|
+
lex_names = exchanges.select { |e| e[:name].to_s.start_with?('lex.') }
|
|
44
|
+
.to_set { |e| e[:name].to_s.sub('lex.', '') }
|
|
45
|
+
|
|
46
|
+
exchanges.select do |e|
|
|
47
|
+
next false unless e[:name].to_s.start_with?('legion.')
|
|
48
|
+
|
|
49
|
+
suffix = e[:name].to_s.sub('legion.', '')
|
|
50
|
+
lex_names.include?(suffix)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def management_uri(path)
|
|
57
|
+
vhost = URI.encode_www_form_component(options[:vhost])
|
|
58
|
+
URI("http://#{options[:host]}:#{options[:port]}/api#{path}?vhost=#{vhost}")
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def fetch_exchanges
|
|
62
|
+
uri = management_uri('/exchanges')
|
|
63
|
+
response = management_get(uri)
|
|
64
|
+
Legion::JSON.load(response.body).map { |e| { name: e[:name], type: e[:type] } }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def delete_exchange(name)
|
|
68
|
+
vhost = URI.encode_www_form_component(options[:vhost])
|
|
69
|
+
encoded_name = URI.encode_www_form_component(name)
|
|
70
|
+
uri = URI("http://#{options[:host]}:#{options[:port]}/api/exchanges/#{vhost}/#{encoded_name}")
|
|
71
|
+
management_request(uri, Net::HTTP::Delete)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def management_get(uri)
|
|
75
|
+
management_request(uri, Net::HTTP::Get)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def management_request(uri, method_class)
|
|
79
|
+
Net::HTTP.start(uri.host, uri.port) do |http|
|
|
80
|
+
req = method_class.new(uri)
|
|
81
|
+
req.basic_auth(options[:user], options[:password])
|
|
82
|
+
http.request(req)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -43,6 +43,40 @@ module Legion
|
|
|
43
43
|
exit(1)
|
|
44
44
|
end
|
|
45
45
|
|
|
46
|
+
desc 'purge-topology', 'Remove old v2.0 AMQP exchanges (legion.* that have lex.* counterparts)'
|
|
47
|
+
option :execute, type: :boolean, default: false, desc: 'Actually delete exchanges (default: dry-run)'
|
|
48
|
+
def purge_topology
|
|
49
|
+
require 'legion/cli/admin_command'
|
|
50
|
+
out = formatter
|
|
51
|
+
exchanges = management_api("/exchanges/#{vhost_encoded}").map { |e| { name: e[:name], type: e[:type] } }
|
|
52
|
+
candidates = Legion::CLI::AdminCommand.detect_old_exchanges(exchanges)
|
|
53
|
+
|
|
54
|
+
if candidates.empty?
|
|
55
|
+
out.success('No old v2.0 topology exchanges found.')
|
|
56
|
+
return
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
if options[:json]
|
|
60
|
+
out.json({ candidates: candidates, deleted: options[:execute] })
|
|
61
|
+
candidates.each { |e| management_delete("/exchanges/#{vhost_encoded}/#{ERB::Util.url_encode(e[:name])}") } if options[:execute]
|
|
62
|
+
return
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
out.header("Old v2.0 Exchanges (#{candidates.size})")
|
|
66
|
+
candidates.each { |e| out.warn("#{e[:name]} (#{e[:type]})") }
|
|
67
|
+
out.spacer
|
|
68
|
+
|
|
69
|
+
if options[:execute]
|
|
70
|
+
candidates.each { |e| management_delete("/exchanges/#{vhost_encoded}/#{ERB::Util.url_encode(e[:name])}") }
|
|
71
|
+
out.success("Purged #{candidates.size} exchange(s).")
|
|
72
|
+
else
|
|
73
|
+
out.warn('Dry-run mode — pass --execute to delete')
|
|
74
|
+
end
|
|
75
|
+
rescue Legion::CLI::Error => e
|
|
76
|
+
formatter.error(e.message)
|
|
77
|
+
exit(1)
|
|
78
|
+
end
|
|
79
|
+
|
|
46
80
|
desc 'cleanup', 'Find (and optionally delete) orphaned queues with 0 consumers and 0 messages'
|
|
47
81
|
option :execute, type: :boolean, default: false, desc: 'Actually delete orphaned queues (default: dry-run)'
|
|
48
82
|
def cleanup
|
data/lib/legion/cli.rb
CHANGED
|
@@ -69,6 +69,7 @@ module Legion
|
|
|
69
69
|
autoload :CodegenCommand, 'legion/cli/codegen_command'
|
|
70
70
|
autoload :Bootstrap, 'legion/cli/bootstrap_command'
|
|
71
71
|
autoload :Broker, 'legion/cli/broker_command'
|
|
72
|
+
autoload :AdminCommand, 'legion/cli/admin_command'
|
|
72
73
|
|
|
73
74
|
module Groups
|
|
74
75
|
autoload :Ai, 'legion/cli/groups/ai_group'
|
|
@@ -36,6 +36,22 @@ module Legion
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
Legion::Extensions::Absorbers::PatternMatcher.register(klass)
|
|
39
|
+
|
|
40
|
+
next unless defined?(Legion::API) && Legion::API.respond_to?(:router)
|
|
41
|
+
|
|
42
|
+
absorber_methods = klass.public_instance_methods(false).reject { |m| m.to_s.start_with?('_') }
|
|
43
|
+
absorber_methods = [:absorb] if absorber_methods.empty?
|
|
44
|
+
absorber_methods.each do |method_name|
|
|
45
|
+
Legion::API.router.register_extension_route(
|
|
46
|
+
lex_name: lex_name,
|
|
47
|
+
amqp_prefix: respond_to?(:amqp_prefix) ? amqp_prefix : "lex.#{lex_name}",
|
|
48
|
+
component_type: 'absorbers',
|
|
49
|
+
component_name: snake_name,
|
|
50
|
+
method_name: method_name.to_s,
|
|
51
|
+
runner_class: klass,
|
|
52
|
+
definition: klass.respond_to?(:definition_for) ? klass.definition_for(method_name) : nil
|
|
53
|
+
)
|
|
54
|
+
end
|
|
39
55
|
end
|
|
40
56
|
rescue StandardError => e
|
|
41
57
|
Legion::Logging.error("Failed to build absorbers: #{e.message}") if defined?(Legion::Logging)
|
|
@@ -29,7 +29,7 @@ module Legion
|
|
|
29
29
|
methods.each do |function|
|
|
30
30
|
route_path = "#{extension_name}/#{runner_name}/#{function}"
|
|
31
31
|
defn = runner_module.respond_to?(:definition_for) ? runner_module.definition_for(function) : nil
|
|
32
|
-
log.info "[Routes] auto-route registered: POST /api/
|
|
32
|
+
log.info "[Routes] auto-route registered: POST /api/extensions/#{extension_name}/runners/#{runner_name}/#{function}"
|
|
33
33
|
@routes[route_path] = {
|
|
34
34
|
lex_name: extension_name,
|
|
35
35
|
runner_name: runner_name,
|
|
@@ -217,36 +217,11 @@ module Legion
|
|
|
217
217
|
end
|
|
218
218
|
|
|
219
219
|
def register_hooks
|
|
220
|
-
|
|
221
|
-
return unless defined?(Legion::API)
|
|
222
|
-
|
|
223
|
-
# Find the first runner class as default for hooks that don't specify one
|
|
224
|
-
default_runner = @runners.values.first&.dig(:runner_class)
|
|
225
|
-
|
|
226
|
-
@hooks.each_value do |hook_info|
|
|
227
|
-
Legion::API.register_hook(
|
|
228
|
-
lex_name: extension_name,
|
|
229
|
-
hook_name: hook_info[:hook_name],
|
|
230
|
-
hook_class: hook_info[:hook_class],
|
|
231
|
-
default_runner: hook_info[:hook_class].new.runner_class || default_runner,
|
|
232
|
-
route_path: hook_info[:route_path]
|
|
233
|
-
)
|
|
234
|
-
end
|
|
220
|
+
# Hook registration is handled by Routes::LexDispatch via the Router (v3.0)
|
|
235
221
|
end
|
|
236
222
|
|
|
237
223
|
def register_routes
|
|
238
|
-
|
|
239
|
-
return unless defined?(Legion::API)
|
|
240
|
-
|
|
241
|
-
@routes.each_value do |route_info|
|
|
242
|
-
Legion::API.register_route(
|
|
243
|
-
lex_name: route_info[:lex_name],
|
|
244
|
-
runner_name: route_info[:runner_name],
|
|
245
|
-
function: route_info[:function],
|
|
246
|
-
runner_class: route_info[:runner_class],
|
|
247
|
-
route_path: route_info[:route_path]
|
|
248
|
-
)
|
|
249
|
-
end
|
|
224
|
+
# Route registration is handled by Routes::LexDispatch via the Router (v3.0)
|
|
250
225
|
end
|
|
251
226
|
|
|
252
227
|
def auto_generate_transport
|
|
@@ -22,6 +22,7 @@ module Legion
|
|
|
22
22
|
build_e_to_q(additional_e_to_q)
|
|
23
23
|
auto_create_dlx_exchange
|
|
24
24
|
auto_create_dlx_queue
|
|
25
|
+
auto_generate_messages
|
|
25
26
|
log.info "[Transport] built exchanges=#{@exchanges.count} queues=#{@queues.count} for #{lex_name}"
|
|
26
27
|
rescue StandardError => e
|
|
27
28
|
log.log_exception(e, payload_summary: "[Transport] build failed for #{lex_name}", component_type: :transport)
|
|
@@ -94,6 +95,41 @@ module Legion
|
|
|
94
95
|
dlx_queue.bind("#{special_name}.dlx", { routing_key: '#' })
|
|
95
96
|
end
|
|
96
97
|
|
|
98
|
+
def auto_generate_messages
|
|
99
|
+
return unless defined?(@runners) && @runners.is_a?(Hash)
|
|
100
|
+
|
|
101
|
+
messages_mod = transport_class::Messages
|
|
102
|
+
ext_amqp = amqp_prefix
|
|
103
|
+
@runners.each_value { |info| auto_generate_runner_messages(info, messages_mod, ext_amqp) }
|
|
104
|
+
rescue StandardError => e
|
|
105
|
+
log.error("[Transport] auto-generate messages failed: #{e.message}") if respond_to?(:log)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def auto_generate_runner_messages(runner_info, messages_mod, ext_amqp)
|
|
109
|
+
runner_name = runner_info[:runner_name]
|
|
110
|
+
runner_module = runner_info[:runner_module]
|
|
111
|
+
return if runner_module.nil?
|
|
112
|
+
return unless runner_module.respond_to?(:definition_for)
|
|
113
|
+
|
|
114
|
+
methods = runner_module.respond_to?(:instance_methods) ? runner_module.instance_methods(false) : []
|
|
115
|
+
methods.each { |method_name| auto_generate_message(runner_name, method_name, runner_module, messages_mod, ext_amqp) }
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def auto_generate_message(runner_name, method_name, runner_module, messages_mod, ext_amqp)
|
|
119
|
+
defn = runner_module.definition_for(method_name)
|
|
120
|
+
return if defn.nil? || defn[:inputs].nil? || defn[:inputs].empty?
|
|
121
|
+
|
|
122
|
+
class_name = "#{runner_name.to_s.split('_').collect(&:capitalize).join}#{method_name.to_s.split('_').collect(&:capitalize).join}"
|
|
123
|
+
return if messages_mod.const_defined?(class_name, false)
|
|
124
|
+
|
|
125
|
+
routing_key = "#{ext_amqp}.runners.#{runner_name}.#{method_name}"
|
|
126
|
+
msg_class = Class.new(Legion::Transport::Message) do
|
|
127
|
+
define_method(:exchange_name) { ext_amqp }
|
|
128
|
+
define_method(:routing_key) { routing_key }
|
|
129
|
+
end
|
|
130
|
+
messages_mod.const_set(class_name, msg_class)
|
|
131
|
+
end
|
|
132
|
+
|
|
97
133
|
def build_e_to_q(array)
|
|
98
134
|
array.each do |binding|
|
|
99
135
|
binding[:routing_key] = nil unless binding.key? :routing_key
|
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.26
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Esity
|
|
@@ -468,8 +468,6 @@ files:
|
|
|
468
468
|
- lib/legion/api/graphql/types/task_type.rb
|
|
469
469
|
- lib/legion/api/graphql/types/worker_type.rb
|
|
470
470
|
- lib/legion/api/helpers.rb
|
|
471
|
-
- lib/legion/api/hooks.rb
|
|
472
|
-
- lib/legion/api/lex.rb
|
|
473
471
|
- lib/legion/api/lex_dispatch.rb
|
|
474
472
|
- lib/legion/api/library_routes.rb
|
|
475
473
|
- lib/legion/api/llm.rb
|
|
@@ -518,6 +516,7 @@ files:
|
|
|
518
516
|
- lib/legion/cli/absorb_command.rb
|
|
519
517
|
- lib/legion/cli/acp_command.rb
|
|
520
518
|
- lib/legion/cli/admin/purge_topology.rb
|
|
519
|
+
- lib/legion/cli/admin_command.rb
|
|
521
520
|
- lib/legion/cli/apollo_command.rb
|
|
522
521
|
- lib/legion/cli/audit_command.rb
|
|
523
522
|
- lib/legion/cli/auth_command.rb
|
data/lib/legion/api/hooks.rb
DELETED
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Legion
|
|
4
|
-
class API < Sinatra::Base
|
|
5
|
-
module Routes
|
|
6
|
-
module Hooks
|
|
7
|
-
def self.registered(app)
|
|
8
|
-
register_list(app)
|
|
9
|
-
register_lex_routes(app)
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
def self.register_list(app)
|
|
13
|
-
app.get '/api/hooks' do
|
|
14
|
-
hooks = Legion::API.registered_hooks.map do |h|
|
|
15
|
-
{
|
|
16
|
-
lex_name: h[:lex_name], hook_name: h[:hook_name],
|
|
17
|
-
hook_class: h[:hook_class].to_s, default_runner: h[:default_runner].to_s,
|
|
18
|
-
route_path: h[:route_path],
|
|
19
|
-
endpoint: "/api/hooks/lex/#{h[:route_path]}"
|
|
20
|
-
}
|
|
21
|
-
end
|
|
22
|
-
json_response(hooks)
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
def self.register_lex_routes(app)
|
|
27
|
-
handler = method(:handle_hook_request)
|
|
28
|
-
|
|
29
|
-
app.get '/api/hooks/lex/*' do
|
|
30
|
-
handler.call(self, request)
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
app.post '/api/hooks/lex/*' do
|
|
34
|
-
handler.call(self, request)
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
def self.handle_hook_request(context, request)
|
|
39
|
-
splat_path = request.path_info.sub(%r{^/api/hooks/lex/}, '')
|
|
40
|
-
Legion::Logging.debug "API: #{request.request_method} /api/hooks/lex/#{splat_path}"
|
|
41
|
-
hook_entry = Legion::API.find_hook_by_path(splat_path)
|
|
42
|
-
if hook_entry.nil?
|
|
43
|
-
Legion::Logging.warn "API #{request.request_method} #{request.path_info} returned 404: no hook registered for '#{splat_path}'"
|
|
44
|
-
context.halt 404, context.json_error('not_found', "no hook registered for '#{splat_path}'", status_code: 404)
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
body = request.request_method == 'POST' ? request.body.read : nil
|
|
48
|
-
hook = hook_entry[:hook_class].new
|
|
49
|
-
unless hook.verify(request.env, body || '')
|
|
50
|
-
Legion::Logging.warn "API #{request.request_method} #{request.path_info} returned 401: hook verification failed"
|
|
51
|
-
context.halt 401, context.json_error('unauthorized', 'hook verification failed', status_code: 401)
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
payload = build_payload(request, body)
|
|
55
|
-
function = hook.route(request.env, payload)
|
|
56
|
-
if function.nil?
|
|
57
|
-
Legion::Logging.warn "API #{request.request_method} #{request.path_info} returned 422: hook could not route this event"
|
|
58
|
-
context.halt 422, context.json_error('unhandled_event', 'hook could not route this event', status_code: 422)
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
runner = hook.runner_class || hook_entry[:default_runner]
|
|
62
|
-
if runner.nil?
|
|
63
|
-
Legion::Logging.error "API #{request.request_method} #{request.path_info} returned 500: no runner class configured for hook '#{splat_path}'"
|
|
64
|
-
context.halt 500, context.json_error('no_runner', 'no runner class configured for this hook', status_code: 500)
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
dispatch_hook(context, payload: payload, runner: runner, function: function)
|
|
68
|
-
rescue StandardError => e
|
|
69
|
-
Legion::Logging.log_exception(e, payload_summary: "API #{request.request_method} #{request.path_info}", component_type: :api)
|
|
70
|
-
context.json_error('internal_error', e.message, status_code: 500)
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
def self.build_payload(request, body)
|
|
74
|
-
payload = if body.nil? || body.empty?
|
|
75
|
-
request.params.transform_keys(&:to_sym)
|
|
76
|
-
else
|
|
77
|
-
Legion::JSON.load(body)
|
|
78
|
-
end
|
|
79
|
-
payload[:http_method] = request.request_method
|
|
80
|
-
payload[:headers] = request.env.select { |k, _| k.start_with?('HTTP_') || k == 'CONTENT_TYPE' }
|
|
81
|
-
payload
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
def self.dispatch_hook(context, payload:, runner:, function:)
|
|
85
|
-
result = Legion::Ingress.run(
|
|
86
|
-
payload: payload, runner_class: runner, function: function,
|
|
87
|
-
source: 'hook', check_subtask: true, generate_task: true
|
|
88
|
-
)
|
|
89
|
-
Legion::Logging.info "API: dispatched hook to #{runner}##{function}, task #{result[:task_id]}"
|
|
90
|
-
return render_custom_response(context, result[:response]) if result.is_a?(Hash) && result[:response]
|
|
91
|
-
|
|
92
|
-
context.json_response({ task_id: result[:task_id], status: result[:status] })
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
def self.render_custom_response(context, resp)
|
|
96
|
-
context.status resp[:status] || 200
|
|
97
|
-
context.content_type resp[:content_type] || 'application/json'
|
|
98
|
-
resp[:headers]&.each { |k, v| context.headers[k] = v }
|
|
99
|
-
resp[:body] || ''
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
class << self
|
|
103
|
-
private :register_list, :register_lex_routes,
|
|
104
|
-
:handle_hook_request, :build_payload, :dispatch_hook, :render_custom_response
|
|
105
|
-
end
|
|
106
|
-
end
|
|
107
|
-
end
|
|
108
|
-
end
|
|
109
|
-
end
|
data/lib/legion/api/lex.rb
DELETED
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Legion
|
|
4
|
-
class API < Sinatra::Base
|
|
5
|
-
module Routes
|
|
6
|
-
module Lex
|
|
7
|
-
def self.registered(app)
|
|
8
|
-
register_list(app)
|
|
9
|
-
register_lex_routes(app)
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
def self.register_list(app)
|
|
13
|
-
app.get '/api/lex' do
|
|
14
|
-
routes = Legion::API.registered_routes.map do |r|
|
|
15
|
-
{
|
|
16
|
-
endpoint: "/api/lex/#{r[:route_path]}",
|
|
17
|
-
extension: r[:lex_name],
|
|
18
|
-
runner: r[:runner_name],
|
|
19
|
-
function: r[:function],
|
|
20
|
-
runner_class: r[:runner_class]
|
|
21
|
-
}
|
|
22
|
-
end
|
|
23
|
-
json_response(routes)
|
|
24
|
-
end
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
def self.register_lex_routes(app)
|
|
28
|
-
handler = method(:handle_lex_request)
|
|
29
|
-
app.post '/api/lex/*' do
|
|
30
|
-
handler.call(self, request)
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def self.handle_lex_request(context, request)
|
|
35
|
-
splat_path = request.path_info.sub(%r{^/api/lex/}, '')
|
|
36
|
-
Legion::Logging.debug "API: POST /api/lex/#{splat_path}"
|
|
37
|
-
route_entry = Legion::API.find_route_by_path(splat_path)
|
|
38
|
-
if route_entry.nil?
|
|
39
|
-
Legion::Logging.warn "API POST /api/lex/#{splat_path} returned 404: no route registered"
|
|
40
|
-
context.halt 404, context.json_error('route_not_found',
|
|
41
|
-
"no route registered for '#{splat_path}'", status_code: 404)
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
payload = build_payload(request)
|
|
45
|
-
result = Legion::Ingress.run(
|
|
46
|
-
payload: payload,
|
|
47
|
-
runner_class: route_entry[:runner_class],
|
|
48
|
-
function: route_entry[:function],
|
|
49
|
-
source: 'lex_route',
|
|
50
|
-
generate_task: true
|
|
51
|
-
)
|
|
52
|
-
Legion::Logging.info "API: LEX route #{splat_path} dispatched to #{route_entry[:runner_class]}, task #{result[:task_id]}"
|
|
53
|
-
context.json_response({ task_id: result[:task_id], status: result[:status],
|
|
54
|
-
result: result[:result] }.compact)
|
|
55
|
-
rescue StandardError => e
|
|
56
|
-
Legion::Logging.log_exception(e, payload_summary: "API POST /api/lex/#{request.path_info.sub(%r{^/api/lex/}, '')}", component_type: :api)
|
|
57
|
-
context.json_error('internal_error', e.message, status_code: 500)
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
def self.build_payload(request)
|
|
61
|
-
body = request.body.read
|
|
62
|
-
payload = if body.nil? || body.empty?
|
|
63
|
-
{}
|
|
64
|
-
else
|
|
65
|
-
Legion::JSON.load(body)
|
|
66
|
-
end
|
|
67
|
-
payload[:http_method] = request.request_method
|
|
68
|
-
payload[:headers] = request.env.select { |k, _| k.start_with?('HTTP_') || k == 'CONTENT_TYPE' }
|
|
69
|
-
payload
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
class << self
|
|
73
|
-
private :register_list, :register_lex_routes, :handle_lex_request, :build_payload
|
|
74
|
-
end
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
end
|
|
78
|
-
end
|