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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8fcd30078f57fd92474d887714ed4ba5a49ed262c50dd9bcde5301e682299d21
4
- data.tar.gz: 6bee493acba0acc9199fe9c53cb3f844b64498012e7123cf9b8a4cd46bbf0bbe
3
+ metadata.gz: 87e232e8560f2fc19eab52867dca532552949fa8ab6f2637f505454ce7c3f435
4
+ data.tar.gz: d98b360b63301c86f808f2b0f2baac8c4671c871847233eb9a6d9f5ca5dd9b61
5
5
  SHA512:
6
- metadata.gz: 2e96c7c7dc687503efb94dda27a1e57aad86b3f05cb6844ab12b603bc20b4c22a155755e2578ed4cca576b9bfd3b3c102b0fc2ce558d5a7ea283dc12f16e02f2
7
- data.tar.gz: ef81be962e9a9228945babcab90e1222e1efa85b3a94b4ceff38d52f02048538ef4471cb03ca3d3fa2e52bb8b3bc391dce0773e57559fdd7f950940cc6ee157b
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
@@ -1125,7 +1125,7 @@ module Legion
1125
1125
  private_class_method :lex_route_responses
1126
1126
 
1127
1127
  def self.lex_paths
1128
- base = {
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/lex/#{route_path}"
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
- return if @hooks.nil? || @hooks.empty?
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
- return if @routes.nil? || @routes.empty?
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Legion
4
- VERSION = '1.6.25'
4
+ VERSION = '1.6.26'
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.6.25
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
@@ -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
@@ -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