legionio 1.8.4 → 1.8.5
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 +10 -0
- data/lib/legion/extensions/builders/runners.rb +5 -1
- data/lib/legion/extensions/core.rb +1 -1
- data/lib/legion/tools/discovery.rb +29 -3
- data/lib/legion/version.rb +1 -1
- data/scripts/fleet_smoke_test.rb +237 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b26f7ff8bd0262bc8eedecfad569c5cd412b3c95f8a2f014f4017c21cd62013b
|
|
4
|
+
data.tar.gz: 3404fcfd2f6e7d4dabce66d9645997213ecb355a030a0dddb4df6c681e98cd1f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 406b9987436e8df5ad44a00edee5bfce4e68f8681ec089690d39cf4895fc1dab73cbe3500848ca1283a737c9997dad5c74700d09e719d70cab888b7acf28beef
|
|
7
|
+
data.tar.gz: 39409aeeb616e4e9e5d679f525be36ecdd60433c5154c8a2c5ebc2c936ce1a5a5d40d33731b9fcc9dc00a8bd6cfc0b3f8fbbd80c1bcc7d2e8736a5227182708a
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [1.8.5] - 2026-04-15
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- `Legion::Extensions::Core#trigger_words` now defaults to `lex_name.split('_')` (e.g. `['github']` for lex-github) instead of `[]`, ensuring extensions auto-surface in TriggerIndex without requiring explicit declaration. Closes #139
|
|
9
|
+
- `Legion::Extensions::Builder::Runners#build_runner_entry` now always populates `trigger_words`, defaulting to `[runner_name]` when the runner module does not define them explicitly. Closes #139
|
|
10
|
+
- `Legion::Tools::Discovery#synthesize_functions` now builds a real JSON Schema from Ruby method reflection data (`Method#parameters`) — required kwargs become required schema properties, optional kwargs become optional properties — so the LLM receives accurate parameter information instead of an empty schema. Closes #140
|
|
11
|
+
- `Legion::Tools::Discovery#synthesize_functions` now uses `definition[:desc]` for tool description when a `definition` DSL entry exists, falling back to the method name rather than `"method_name function"`. Closes #140
|
|
12
|
+
- `Legion::Tools::Discovery#tool_attributes` now reads `definition[:inputs]` when present and non-empty, using it as the input schema in preference to `meta[:options]`. Closes #140
|
|
13
|
+
- `Legion::Tools::Discovery#register_function` fixed asymmetric default: `resolve_exposed` now defaults to `true` when the extension does not respond to `mcp_tools?`, matching the behaviour of `resolve_mcp_tools_enabled`. Closes #140
|
|
14
|
+
|
|
5
15
|
## [1.8.4] - 2026-04-14
|
|
6
16
|
|
|
7
17
|
### Added
|
|
@@ -42,7 +42,11 @@ module Legion
|
|
|
42
42
|
class_methods: {}
|
|
43
43
|
}
|
|
44
44
|
entry[:scheduled_tasks] = loaded_runner.scheduled_tasks if loaded_runner.method_defined?(:scheduled_tasks)
|
|
45
|
-
entry[:trigger_words] =
|
|
45
|
+
entry[:trigger_words] = if loaded_runner.respond_to?(:trigger_words) && loaded_runner.trigger_words.any?
|
|
46
|
+
loaded_runner.trigger_words
|
|
47
|
+
else
|
|
48
|
+
[runner_name]
|
|
49
|
+
end
|
|
46
50
|
entry[:desc] = settings[:runners][runner_name.to_sym][:desc] if settings.key?(:runners) && settings[:runners].key?(runner_name.to_sym)
|
|
47
51
|
entry
|
|
48
52
|
end
|
|
@@ -80,14 +80,40 @@ module Legion
|
|
|
80
80
|
return {} unless runner_entry&.dig(:class_methods).is_a?(Hash)
|
|
81
81
|
|
|
82
82
|
runner_entry[:class_methods].each_with_object({}) do |(method_name, method_info), funcs|
|
|
83
|
-
|
|
83
|
+
defn = runner_mod.respond_to?(:definition_for) ? runner_mod.definition_for(method_name) : nil
|
|
84
|
+
funcs[method_name] = {
|
|
85
|
+
desc: defn&.dig(:desc) || method_name.to_s,
|
|
86
|
+
options: build_schema_from_args(method_info[:args]),
|
|
87
|
+
args: method_info[:args]
|
|
88
|
+
}
|
|
84
89
|
end
|
|
85
90
|
end
|
|
86
91
|
|
|
92
|
+
def build_schema_from_args(args)
|
|
93
|
+
return {} if args.nil? || args.empty?
|
|
94
|
+
|
|
95
|
+
properties = {}
|
|
96
|
+
required = []
|
|
97
|
+
|
|
98
|
+
args.each do |type, name|
|
|
99
|
+
next if name.nil? || %i[** * block].include?(name)
|
|
100
|
+
|
|
101
|
+
param_name = name.to_s
|
|
102
|
+
properties[param_name] = { type: 'string' }
|
|
103
|
+
required << param_name if type == :req
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
return {} if properties.empty?
|
|
107
|
+
|
|
108
|
+
schema = { properties: properties }
|
|
109
|
+
schema[:required] = required unless required.empty?
|
|
110
|
+
schema
|
|
111
|
+
end
|
|
112
|
+
|
|
87
113
|
def register_function(ext, runner_mod, func_name, meta, is_deferred)
|
|
88
114
|
defn = runner_mod.respond_to?(:definition_for) ? runner_mod.definition_for(func_name) : nil
|
|
89
115
|
|
|
90
|
-
ext_default = ext.respond_to?(:mcp_tools?) ? ext.mcp_tools? :
|
|
116
|
+
ext_default = ext.respond_to?(:mcp_tools?) ? ext.mcp_tools? : true
|
|
91
117
|
return unless resolve_exposed(defn, meta, ext_default)
|
|
92
118
|
|
|
93
119
|
requires = defn&.dig(:requires)&.map(&:to_s) || meta[:requires]
|
|
@@ -149,7 +175,7 @@ module Legion
|
|
|
149
175
|
{
|
|
150
176
|
tool_name: defn&.dig(:mcp_prefix) || "legion-#{ext_name}-#{runner_snake}-#{func_name}",
|
|
151
177
|
description: meta[:desc] || defn&.dig(:desc) || "#{ext_name}##{func_name}",
|
|
152
|
-
input_schema: normalize_schema(meta[:options]),
|
|
178
|
+
input_schema: normalize_schema(defn&.dig(:inputs)&.any? ? defn[:inputs] : meta[:options]),
|
|
153
179
|
mcp_category: defn&.dig(:mcp_category),
|
|
154
180
|
mcp_tier: defn&.dig(:mcp_tier),
|
|
155
181
|
deferred: deferred,
|
data/lib/legion/version.rb
CHANGED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Fleet Pipeline Smoke Test
|
|
5
|
+
# =========================
|
|
6
|
+
# Runs against a live RabbitMQ instance to verify exchange/queue topology
|
|
7
|
+
# and basic message flow.
|
|
8
|
+
#
|
|
9
|
+
# Prerequisites:
|
|
10
|
+
# - RabbitMQ running on localhost:5672 (or set RABBITMQ_URL)
|
|
11
|
+
# - Legion gems installed: legion-transport, legion-settings, legion-json
|
|
12
|
+
# - Fleet extensions deployed: lex-assessor, lex-planner, lex-developer, lex-validator
|
|
13
|
+
#
|
|
14
|
+
# Usage:
|
|
15
|
+
# ruby scripts/fleet_smoke_test.rb
|
|
16
|
+
# RABBITMQ_URL=amqp://user:pass@host:5672 ruby scripts/fleet_smoke_test.rb
|
|
17
|
+
|
|
18
|
+
require 'json'
|
|
19
|
+
require 'securerandom'
|
|
20
|
+
require 'timeout'
|
|
21
|
+
|
|
22
|
+
# Suppress legion logging noise
|
|
23
|
+
ENV['LEGION_LOG_LEVEL'] ||= 'error'
|
|
24
|
+
|
|
25
|
+
class FleetSmokeTest
|
|
26
|
+
FLEET_EXCHANGES = %w[
|
|
27
|
+
lex.assessor lex.planner lex.developer lex.validator
|
|
28
|
+
].freeze
|
|
29
|
+
|
|
30
|
+
FLEET_QUEUES = %w[
|
|
31
|
+
lex.assessor.runners.assessor
|
|
32
|
+
lex.planner.runners.planner
|
|
33
|
+
lex.developer.runners.developer
|
|
34
|
+
lex.developer.runners.ship
|
|
35
|
+
lex.validator.runners.validator
|
|
36
|
+
].freeze
|
|
37
|
+
|
|
38
|
+
ABSORBER_QUEUES = %w[
|
|
39
|
+
lex.github.absorbers.issues.absorb
|
|
40
|
+
].freeze
|
|
41
|
+
|
|
42
|
+
attr_reader :results
|
|
43
|
+
|
|
44
|
+
def initialize
|
|
45
|
+
@results = []
|
|
46
|
+
@passed = 0
|
|
47
|
+
@failed = 0
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def run
|
|
51
|
+
puts '=' * 60
|
|
52
|
+
puts 'Fleet Pipeline Smoke Test'
|
|
53
|
+
puts '=' * 60
|
|
54
|
+
puts
|
|
55
|
+
|
|
56
|
+
check_dependencies
|
|
57
|
+
setup_transport
|
|
58
|
+
check_exchanges
|
|
59
|
+
check_queues
|
|
60
|
+
check_absorber_queues
|
|
61
|
+
test_publish_consume
|
|
62
|
+
teardown
|
|
63
|
+
|
|
64
|
+
report
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
def check_dependencies
|
|
70
|
+
section('Checking dependencies')
|
|
71
|
+
|
|
72
|
+
%w[legion-transport legion-settings legion-json].each do |gem_name|
|
|
73
|
+
Gem::Specification.find_by_name(gem_name)
|
|
74
|
+
pass("#{gem_name} installed")
|
|
75
|
+
rescue Gem::MissingSpecError
|
|
76
|
+
fail_test("#{gem_name} not installed")
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def setup_transport
|
|
81
|
+
section('Connecting to RabbitMQ')
|
|
82
|
+
|
|
83
|
+
require 'legion/settings'
|
|
84
|
+
require 'legion/logging'
|
|
85
|
+
require 'legion/transport'
|
|
86
|
+
|
|
87
|
+
Legion::Logging.setup(log_level: 'error', level: 'error', trace: false)
|
|
88
|
+
Legion::Settings.load
|
|
89
|
+
|
|
90
|
+
if ENV['RABBITMQ_URL']
|
|
91
|
+
Legion::Settings.loader.settings[:transport] ||= {}
|
|
92
|
+
Legion::Settings.loader.settings[:transport][:url] = ENV.fetch('RABBITMQ_URL', nil)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
Legion::Settings.merge_settings('transport', Legion::Transport::Settings.default)
|
|
96
|
+
Legion::Transport::Connection.setup
|
|
97
|
+
pass('Connected to RabbitMQ')
|
|
98
|
+
rescue StandardError => e
|
|
99
|
+
fail_test("RabbitMQ connection failed: #{e.message}")
|
|
100
|
+
puts "\n Set RABBITMQ_URL or configure transport in ~/.legionio/settings/"
|
|
101
|
+
exit 1
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def check_exchanges
|
|
105
|
+
section('Checking fleet exchanges')
|
|
106
|
+
|
|
107
|
+
channel = Legion::Transport::Connection.session.create_channel
|
|
108
|
+
FLEET_EXCHANGES.each do |name|
|
|
109
|
+
check_or_create_exchange(channel, name)
|
|
110
|
+
channel = Legion::Transport::Connection.session.create_channel
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def check_or_create_exchange(channel, name)
|
|
115
|
+
channel.exchange_declare(name, 'topic', passive: true)
|
|
116
|
+
pass("Exchange #{name} exists")
|
|
117
|
+
rescue Bunny::NotFound
|
|
118
|
+
channel = Legion::Transport::Connection.session.create_channel
|
|
119
|
+
channel.exchange_declare(name, 'topic', durable: true)
|
|
120
|
+
pass("Exchange #{name} created")
|
|
121
|
+
rescue StandardError => e
|
|
122
|
+
fail_test("Exchange #{name} check failed: #{e.message}")
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def check_queues
|
|
126
|
+
section('Checking fleet queues')
|
|
127
|
+
|
|
128
|
+
channel = Legion::Transport::Connection.session.create_channel
|
|
129
|
+
FLEET_QUEUES.each do |name|
|
|
130
|
+
check_or_create_queue(channel, name)
|
|
131
|
+
channel = Legion::Transport::Connection.session.create_channel
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def check_absorber_queues
|
|
136
|
+
section('Checking absorber queues')
|
|
137
|
+
|
|
138
|
+
channel = Legion::Transport::Connection.session.create_channel
|
|
139
|
+
ABSORBER_QUEUES.each do |name|
|
|
140
|
+
check_or_create_queue(channel, name, prefix: 'Absorber queue')
|
|
141
|
+
channel = Legion::Transport::Connection.session.create_channel
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def check_or_create_queue(channel, name, prefix: 'Queue')
|
|
146
|
+
q = channel.queue(name, durable: true, passive: true)
|
|
147
|
+
pass("#{prefix} #{name} exists (depth: #{q.message_count})")
|
|
148
|
+
rescue Bunny::NotFound
|
|
149
|
+
channel = Legion::Transport::Connection.session.create_channel
|
|
150
|
+
channel.queue(name, durable: true)
|
|
151
|
+
pass("#{prefix} #{name} created")
|
|
152
|
+
rescue StandardError => e
|
|
153
|
+
fail_test("#{prefix} #{name} check failed: #{e.message}")
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def test_publish_consume
|
|
157
|
+
section('Testing publish/consume round-trip')
|
|
158
|
+
|
|
159
|
+
channel = Legion::Transport::Connection.session.create_channel
|
|
160
|
+
test_queue_name = "fleet.smoke_test.#{SecureRandom.hex(4)}"
|
|
161
|
+
|
|
162
|
+
exchange = channel.topic('lex.assessor', durable: true)
|
|
163
|
+
queue = channel.queue(test_queue_name, durable: false, auto_delete: true)
|
|
164
|
+
queue.bind(exchange, routing_key: "#{test_queue_name}.#")
|
|
165
|
+
|
|
166
|
+
test_payload = {
|
|
167
|
+
work_item_id: SecureRandom.uuid,
|
|
168
|
+
source: 'smoke_test',
|
|
169
|
+
title: 'Fleet smoke test message',
|
|
170
|
+
timestamp: Time.now.utc.iso8601
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
exchange.publish(
|
|
174
|
+
JSON.generate(test_payload),
|
|
175
|
+
routing_key: "#{test_queue_name}.test",
|
|
176
|
+
content_type: 'application/json',
|
|
177
|
+
persistent: false
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
received = nil
|
|
181
|
+
Timeout.timeout(5) do
|
|
182
|
+
_, _, body = queue.pop
|
|
183
|
+
received = body ? JSON.parse(body, symbolize_names: true) : nil
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
if received && received[:work_item_id] == test_payload[:work_item_id]
|
|
187
|
+
pass('Publish/consume round-trip successful')
|
|
188
|
+
else
|
|
189
|
+
fail_test('Message not received or payload mismatch')
|
|
190
|
+
end
|
|
191
|
+
rescue Timeout::Error
|
|
192
|
+
fail_test('Publish/consume timed out after 5 seconds')
|
|
193
|
+
rescue StandardError => e
|
|
194
|
+
fail_test("Publish/consume failed: #{e.message}")
|
|
195
|
+
ensure
|
|
196
|
+
queue&.delete
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def teardown
|
|
200
|
+
Legion::Transport::Connection.shutdown
|
|
201
|
+
rescue StandardError
|
|
202
|
+
nil
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def section(title)
|
|
206
|
+
puts
|
|
207
|
+
puts "--- #{title} ---"
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def pass(message)
|
|
211
|
+
@passed += 1
|
|
212
|
+
@results << { status: :pass, message: message }
|
|
213
|
+
puts " [PASS] #{message}"
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def fail_test(message)
|
|
217
|
+
@failed += 1
|
|
218
|
+
@results << { status: :fail, message: message }
|
|
219
|
+
puts " [FAIL] #{message}"
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def report
|
|
223
|
+
puts
|
|
224
|
+
puts '=' * 60
|
|
225
|
+
total = @passed + @failed
|
|
226
|
+
if @failed.zero?
|
|
227
|
+
puts "ALL #{total} CHECKS PASSED"
|
|
228
|
+
else
|
|
229
|
+
puts "#{@passed}/#{total} passed, #{@failed} FAILED"
|
|
230
|
+
end
|
|
231
|
+
puts '=' * 60
|
|
232
|
+
|
|
233
|
+
exit(@failed.zero? ? 0 : 1)
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
FleetSmokeTest.new.run if $PROGRAM_NAME == __FILE__
|
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.8.
|
|
4
|
+
version: 1.8.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Esity
|
|
@@ -926,6 +926,7 @@ files:
|
|
|
926
926
|
- lib/legion/workflow/manifest.rb
|
|
927
927
|
- public/governance/index.html
|
|
928
928
|
- public/workflow/index.html
|
|
929
|
+
- scripts/fleet_smoke_test.rb
|
|
929
930
|
- scripts/rollout-ci-workflow.sh
|
|
930
931
|
- scripts/sync-github-labels-topics.sh
|
|
931
932
|
- workflows/autofix-pipeline.yml
|