legionio 1.9.33 → 1.9.36
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 +26 -0
- data/Gemfile +32 -29
- data/lib/legion/api/extensions.rb +33 -2
- data/lib/legion/cli/bootstrap_command.rb +17 -5
- data/lib/legion/cli/service_command.rb +164 -0
- data/lib/legion/cli.rb +4 -0
- data/lib/legion/extensions/absorbers/base.rb +3 -1
- data/lib/legion/extensions/core.rb +2 -1
- data/lib/legion/extensions.rb +19 -1
- data/lib/legion/identity/broker.rb +4 -0
- data/lib/legion/identity/resolver.rb +67 -0
- data/lib/legion/service.rb +26 -7
- data/lib/legion/tools/discovery.rb +23 -0
- data/lib/legion/version.rb +1 -1
- 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: 1de97753e1f584403f30a43a47049183871b98e7581bee36c892aac9e758983b
|
|
4
|
+
data.tar.gz: 2042c10f023e59093f60258a65ccec746128c3efef7bb980ee33206dafef446c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e206bc92436c737aa2046580af9b705d147d8601601703980d69944afbcc10189aa479527ee6f0da128eb0a80912d54dbc635f2c32eab4aaba2f7f21e595062e
|
|
7
|
+
data.tar.gz: da6fc109223bf64dced09fd289f86a3f67592d2ad9a096f64376e0ecea074d2611671eda9fecadbfc14e528a377ee1c2a06e15cc2cbde0de7ffda24c524dab3f
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [1.9.36] - 2026-05-22
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- Identity: preload identity provider gems and resolve process identity before LLM setup so `llm.registry` availability events include Legion identity headers.
|
|
9
|
+
- Identity: use the persisted `identity.json` value as a cached resolver fallback ahead of unverified system identity when fresh auth providers are unavailable.
|
|
10
|
+
- Bundler: load sibling Legion and LLM provider path dependencies outside the test group when those local checkouts exist, so local service boots can use the active workspace gems.
|
|
11
|
+
|
|
12
|
+
## [1.9.35] - 2026-05-22
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
- CLI: `legionio service start|stop|restart|status` subcommand for direct launchd control
|
|
16
|
+
- Logging transport forwarding now publishes structured log headers/properties, including identity and Legion version headers supplied by `legion-logging`.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
- CLI: `legionio bootstrap --start` now calls `launchctl kickstart` after brew services start to force immediate spawn on macOS 26+ (Tahoe defers `RunAtLoad` for mid-session bootstraps)
|
|
20
|
+
|
|
21
|
+
## [1.9.34] - 2026-05-18
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
- API: `GET /api/extensions/tools` endpoint with extension, runner, deferred, and triggered filters
|
|
25
|
+
- Tools::Discovery: writes to `Legion::Settings::Extensions.register_tool` (bridges discovery to LLM pipeline)
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
- Extensions: `extension_parts_from_const` no longer converts underscores to dashes (fixes lex-microsoft_teams filtering)
|
|
29
|
+
- Core: `generate_runner_messages` strips `?` and `!` from method names before creating constants
|
|
30
|
+
|
|
5
31
|
## [1.9.33] - 2026-05-15
|
|
6
32
|
|
|
7
33
|
### Added
|
data/Gemfile
CHANGED
|
@@ -8,36 +8,39 @@ gem 'pg'
|
|
|
8
8
|
gem 'kramdown', '>= 2.0'
|
|
9
9
|
gem 'mysql2'
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
end
|
|
32
|
-
if File.exist?(File.expand_path('../extensions-identity/lex-identity-system', __dir__))
|
|
33
|
-
gem 'lex-identity-system', path: '../extensions-identity/lex-identity-system'
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
%w[anthropic azure-foundry bedrock gemini mlx ollama openai vertex vllm].each do |provider|
|
|
37
|
-
provider_path = "../extensions-ai/lex-llm-#{provider}"
|
|
38
|
-
gem "lex-llm-#{provider}", path: provider_path if File.exist?(File.expand_path(provider_path, __dir__))
|
|
39
|
-
end
|
|
11
|
+
gem 'legion-data', path: '../legion-data' if File.exist?(File.expand_path('../legion-data', __dir__))
|
|
12
|
+
gem 'legion-logging', path: '../legion-logging' if File.exist?(File.expand_path('../legion-logging', __dir__))
|
|
13
|
+
gem 'legion-settings', path: '../legion-settings' if File.exist?(File.expand_path('../legion-settings', __dir__))
|
|
14
|
+
|
|
15
|
+
gem 'legion-apollo', path: '../legion-apollo' if File.exist?(File.expand_path('../legion-apollo', __dir__))
|
|
16
|
+
gem 'legion-gaia', path: '../legion-gaia' if File.exist?(File.expand_path('../legion-gaia', __dir__))
|
|
17
|
+
gem 'legion-llm', path: '../legion-llm' if File.exist?(File.expand_path('../legion-llm', __dir__))
|
|
18
|
+
gem 'legion-mcp', path: '../legion-mcp' if File.exist?(File.expand_path('../legion-mcp', __dir__))
|
|
19
|
+
gem 'legion-tty', path: '../legion-tty' if File.exist?(File.expand_path('../legion-tty', __dir__))
|
|
20
|
+
|
|
21
|
+
gem 'lex-apollo', path: '../extensions/lex-apollo' if File.exist?(File.expand_path('../extensions/lex-apollo', __dir__))
|
|
22
|
+
gem 'lex-llm', path: '../extensions-ai/lex-llm' if File.exist?(File.expand_path('../extensions-ai/lex-llm', __dir__))
|
|
23
|
+
# gem 'lex-llm-ledger', path: '../extensions-ai/lex-llm-ledger' if File.exist?(File.expand_path('../extensions-ai/lex-llm-ledger', __dir__))
|
|
24
|
+
|
|
25
|
+
if File.exist?(File.expand_path('../extensions-identity/lex-identity-entra', __dir__))
|
|
26
|
+
gem 'lex-identity-entra', path: '../extensions-identity/lex-identity-entra'
|
|
27
|
+
end
|
|
28
|
+
if File.exist?(File.expand_path('../extensions-identity/lex-identity-kerberos', __dir__))
|
|
29
|
+
gem 'lex-identity-kerberos', path: '../extensions-identity/lex-identity-kerberos'
|
|
30
|
+
end
|
|
40
31
|
|
|
32
|
+
gem 'lex-kerberos', path: '../extensions-identity/lex-kerberos' if File.exist?(File.expand_path('../extensions-identity/lex-kerberos', __dir__))
|
|
33
|
+
|
|
34
|
+
if File.exist?(File.expand_path('../extensions-identity/lex-identity-system', __dir__))
|
|
35
|
+
gem 'lex-identity-system', path: '../extensions-identity/lex-identity-system'
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
%w[anthropic azure-foundry bedrock gemini mlx ollama openai vertex vllm].each do |provider|
|
|
39
|
+
provider_path = "../extensions-ai/lex-llm-#{provider}"
|
|
40
|
+
gem "lex-llm-#{provider}", path: provider_path if File.exist?(File.expand_path(provider_path, __dir__))
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
group :test do
|
|
41
44
|
gem 'faraday'
|
|
42
45
|
gem 'faraday-net_http'
|
|
43
46
|
gem 'graphql'
|
|
@@ -6,6 +6,7 @@ module Legion
|
|
|
6
6
|
module Extensions
|
|
7
7
|
def self.registered(app)
|
|
8
8
|
register_loaded_summary_route(app)
|
|
9
|
+
register_tools_route(app)
|
|
9
10
|
register_available_route(app)
|
|
10
11
|
register_extension_routes(app)
|
|
11
12
|
register_runner_routes(app)
|
|
@@ -29,6 +30,14 @@ module Legion
|
|
|
29
30
|
end
|
|
30
31
|
end
|
|
31
32
|
|
|
33
|
+
def self.register_tools_route(app)
|
|
34
|
+
app.get '/api/extensions/tools' do
|
|
35
|
+
entries = filter_tool_entries(Array(Legion::Settings::Extensions.tools), params)
|
|
36
|
+
tools = entries.map { |e| serialize_tool_entry(e) }
|
|
37
|
+
json_response({ total: tools.size, tools: tools })
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
32
41
|
def self.register_available_route(app)
|
|
33
42
|
app.get '/api/extension_catalog/available' do
|
|
34
43
|
entries = Legion::Extensions::Catalog::Available.all
|
|
@@ -205,8 +214,30 @@ module Legion
|
|
|
205
214
|
started_at: entry[:started_at]&.iso8601 }
|
|
206
215
|
end
|
|
207
216
|
|
|
208
|
-
|
|
209
|
-
|
|
217
|
+
def filter_tool_entries(entries, params)
|
|
218
|
+
entries = entries.select { |e| e[:extension].to_s == params[:extension] } if params[:extension]
|
|
219
|
+
entries = entries.select { |e| e[:runner].to_s == params[:runner] } if params[:runner]
|
|
220
|
+
entries = entries.select { |e| e[:deferred] == (params[:deferred] == 'true') } if params.key?(:deferred)
|
|
221
|
+
entries = entries.select { |e| Array(e[:trigger_words]).any? } if params[:triggered] == 'true'
|
|
222
|
+
entries
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def serialize_tool_entry(entry)
|
|
226
|
+
{
|
|
227
|
+
name: entry[:name],
|
|
228
|
+
description: entry[:description],
|
|
229
|
+
extension: entry[:extension],
|
|
230
|
+
runner: entry[:runner],
|
|
231
|
+
deferred: entry[:deferred],
|
|
232
|
+
trigger_words: entry[:trigger_words],
|
|
233
|
+
source: entry[:source],
|
|
234
|
+
sticky: entry[:sticky]
|
|
235
|
+
}.compact
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
private :register_loaded_summary_route, :register_tools_route, :register_available_route,
|
|
239
|
+
:register_extension_routes, :register_runner_routes, :register_function_routes,
|
|
240
|
+
:register_invoke_route
|
|
210
241
|
end
|
|
211
242
|
end
|
|
212
243
|
end
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require 'English'
|
|
4
4
|
require 'json'
|
|
5
5
|
require 'fileutils'
|
|
6
|
+
require 'open3'
|
|
6
7
|
require 'rbconfig'
|
|
7
8
|
require 'thor'
|
|
8
9
|
require 'legion/cli/output'
|
|
@@ -391,18 +392,29 @@ module Legion
|
|
|
391
392
|
|
|
392
393
|
def run_brew_service(service, out)
|
|
393
394
|
output, success = shell_capture("brew services start #{service}")
|
|
394
|
-
|
|
395
|
-
out.success("#{service} started") unless options[:json]
|
|
396
|
-
true
|
|
397
|
-
else
|
|
395
|
+
unless success
|
|
398
396
|
out.warn("#{service} failed to start: #{output.strip.lines.last&.strip}") unless options[:json]
|
|
399
|
-
false
|
|
397
|
+
return false
|
|
400
398
|
end
|
|
399
|
+
|
|
400
|
+
out.success("#{service} started") unless options[:json]
|
|
401
|
+
kickstart_launchd_service("homebrew.mxcl.#{service}", out)
|
|
401
402
|
rescue StandardError => e
|
|
402
403
|
out.warn("brew services start #{service} raised: #{e.message}") unless options[:json]
|
|
403
404
|
false
|
|
404
405
|
end
|
|
405
406
|
|
|
407
|
+
def kickstart_launchd_service(label, out)
|
|
408
|
+
return true unless RbConfig::CONFIG['host_os'] =~ /darwin/
|
|
409
|
+
|
|
410
|
+
uid = ::Process.uid
|
|
411
|
+
_, status = Open3.capture2e('launchctl', 'kickstart', "gui/#{uid}/#{label}")
|
|
412
|
+
return true if status.success?
|
|
413
|
+
|
|
414
|
+
out.warn("launchctl kickstart #{label} failed (service may already be running)") unless options[:json]
|
|
415
|
+
false
|
|
416
|
+
end
|
|
417
|
+
|
|
406
418
|
def poll_daemon_ready(out, port: 4567, timeout: 30)
|
|
407
419
|
require 'net/http'
|
|
408
420
|
deadline = ::Time.now + timeout
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'open3'
|
|
4
|
+
require 'rbconfig'
|
|
5
|
+
require 'thor'
|
|
6
|
+
require 'legion/cli/output'
|
|
7
|
+
|
|
8
|
+
module Legion
|
|
9
|
+
module CLI
|
|
10
|
+
class ServiceCommand < Thor
|
|
11
|
+
namespace 'service'
|
|
12
|
+
|
|
13
|
+
def self.exit_on_failure?
|
|
14
|
+
true
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
class_option :json, type: :boolean, default: false, desc: 'Output as JSON'
|
|
18
|
+
class_option :no_color, type: :boolean, default: false, desc: 'Disable color output'
|
|
19
|
+
|
|
20
|
+
SERVICE_LABEL = 'homebrew.mxcl.legionio'
|
|
21
|
+
|
|
22
|
+
desc 'start', 'Start the Legion launchd service'
|
|
23
|
+
long_desc <<~DESC
|
|
24
|
+
Starts the Legion background service via launchd. On macOS 26+ (Tahoe),
|
|
25
|
+
uses launchctl kickstart to ensure immediate process spawn after bootstrap.
|
|
26
|
+
DESC
|
|
27
|
+
def start
|
|
28
|
+
out = Output::Formatter.new(json: options[:json], color: !options[:no_color])
|
|
29
|
+
ensure_macos!(out)
|
|
30
|
+
|
|
31
|
+
plist = plist_path
|
|
32
|
+
unless File.exist?(plist)
|
|
33
|
+
out.error("Service plist not found at #{plist}")
|
|
34
|
+
out.info('Run: brew install legionio')
|
|
35
|
+
raise SystemExit, 1
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
uid = ::Process.uid
|
|
39
|
+
target = "gui/#{uid}"
|
|
40
|
+
|
|
41
|
+
if service_loaded?(target)
|
|
42
|
+
out.info('Service already loaded, kicking...')
|
|
43
|
+
else
|
|
44
|
+
_, status = Open3.capture2e('launchctl', 'bootstrap', target, plist)
|
|
45
|
+
out.warn('bootstrap failed (may already be loaded), attempting kickstart anyway') unless status.success?
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
_, status = Open3.capture2e('launchctl', 'kickstart', '-k', "#{target}/#{SERVICE_LABEL}")
|
|
49
|
+
if status.success?
|
|
50
|
+
out.success('Legion service started')
|
|
51
|
+
else
|
|
52
|
+
out.error('Failed to kickstart Legion service')
|
|
53
|
+
raise SystemExit, 1
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
poll_ready(out)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
desc 'stop', 'Stop the Legion launchd service'
|
|
60
|
+
def stop
|
|
61
|
+
out = Output::Formatter.new(json: options[:json], color: !options[:no_color])
|
|
62
|
+
ensure_macos!(out)
|
|
63
|
+
|
|
64
|
+
uid = ::Process.uid
|
|
65
|
+
target = "gui/#{uid}"
|
|
66
|
+
|
|
67
|
+
_, status = Open3.capture2e('launchctl', 'bootout', "#{target}/#{SERVICE_LABEL}")
|
|
68
|
+
if status.success?
|
|
69
|
+
out.success('Legion service stopped')
|
|
70
|
+
else
|
|
71
|
+
out.warn('Service was not loaded (already stopped?)')
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
desc 'restart', 'Restart the Legion launchd service'
|
|
76
|
+
def restart
|
|
77
|
+
out = Output::Formatter.new(json: options[:json], color: !options[:no_color])
|
|
78
|
+
ensure_macos!(out)
|
|
79
|
+
|
|
80
|
+
uid = ::Process.uid
|
|
81
|
+
target = "gui/#{uid}"
|
|
82
|
+
|
|
83
|
+
Open3.capture2e('launchctl', 'bootout', "#{target}/#{SERVICE_LABEL}")
|
|
84
|
+
sleep 1
|
|
85
|
+
|
|
86
|
+
plist = plist_path
|
|
87
|
+
Open3.capture2e('launchctl', 'bootstrap', target, plist) if File.exist?(plist)
|
|
88
|
+
|
|
89
|
+
_, status = Open3.capture2e('launchctl', 'kickstart', '-k', "#{target}/#{SERVICE_LABEL}")
|
|
90
|
+
if status.success?
|
|
91
|
+
out.success('Legion service restarted')
|
|
92
|
+
else
|
|
93
|
+
out.error('Failed to restart Legion service')
|
|
94
|
+
raise SystemExit, 1
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
poll_ready(out)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
desc 'status', 'Show Legion launchd service status'
|
|
101
|
+
def status
|
|
102
|
+
out = Output::Formatter.new(json: options[:json], color: !options[:no_color])
|
|
103
|
+
ensure_macos!(out)
|
|
104
|
+
|
|
105
|
+
uid = ::Process.uid
|
|
106
|
+
target = "gui/#{uid}"
|
|
107
|
+
output, status = Open3.capture2e('launchctl', 'print', "#{target}/#{SERVICE_LABEL}")
|
|
108
|
+
|
|
109
|
+
unless status.success?
|
|
110
|
+
out.info('Service is not loaded')
|
|
111
|
+
return
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
state = output[/state = (.+)/, 1] || 'unknown'
|
|
115
|
+
pid = output[/pid = (\d+)/, 1]
|
|
116
|
+
runs = output[/runs = (\d+)/, 1]
|
|
117
|
+
|
|
118
|
+
if options[:json]
|
|
119
|
+
puts Legion::JSON.dump({ state: state, pid: pid&.to_i, runs: runs&.to_i })
|
|
120
|
+
else
|
|
121
|
+
out.info("State: #{state}")
|
|
122
|
+
out.info("PID: #{pid}") if pid
|
|
123
|
+
out.info("Runs: #{runs}") if runs
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
private
|
|
128
|
+
|
|
129
|
+
def ensure_macos!(out)
|
|
130
|
+
return if RbConfig::CONFIG['host_os'] =~ /darwin/
|
|
131
|
+
|
|
132
|
+
out.error('The service command is only available on macOS (uses launchd)')
|
|
133
|
+
raise SystemExit, 1
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def plist_path
|
|
137
|
+
File.expand_path("~/Library/LaunchAgents/#{SERVICE_LABEL}.plist")
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def service_loaded?(target)
|
|
141
|
+
_, status = Open3.capture2e('launchctl', 'print', "#{target}/#{SERVICE_LABEL}")
|
|
142
|
+
status.success?
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def poll_ready(out, port: 4567, timeout: 15)
|
|
146
|
+
require 'net/http'
|
|
147
|
+
deadline = ::Time.now + timeout
|
|
148
|
+
until ::Time.now > deadline
|
|
149
|
+
begin
|
|
150
|
+
resp = Net::HTTP.get_response(URI("http://localhost:#{port}/api/ready"))
|
|
151
|
+
if resp.is_a?(Net::HTTPSuccess)
|
|
152
|
+
out.success("Daemon ready on port #{port}")
|
|
153
|
+
return
|
|
154
|
+
end
|
|
155
|
+
rescue StandardError
|
|
156
|
+
# not ready yet
|
|
157
|
+
end
|
|
158
|
+
sleep 1
|
|
159
|
+
end
|
|
160
|
+
out.info('Service started but not yet ready (boot in progress)')
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
data/lib/legion/cli.rb
CHANGED
|
@@ -69,6 +69,7 @@ module Legion
|
|
|
69
69
|
autoload :Debug, 'legion/cli/debug_command'
|
|
70
70
|
autoload :CodegenCommand, 'legion/cli/codegen_command'
|
|
71
71
|
autoload :Bootstrap, 'legion/cli/bootstrap_command'
|
|
72
|
+
autoload :ServiceCommand, 'legion/cli/service_command'
|
|
72
73
|
autoload :Broker, 'legion/cli/broker_command'
|
|
73
74
|
autoload :AdminCommand, 'legion/cli/admin_command'
|
|
74
75
|
autoload :Workflow, 'legion/cli/workflow_command'
|
|
@@ -258,6 +259,9 @@ module Legion
|
|
|
258
259
|
desc 'setup SUBCOMMAND', 'Install feature packs and configure IDE integrations'
|
|
259
260
|
subcommand 'setup', Legion::CLI::Setup
|
|
260
261
|
|
|
262
|
+
desc 'service SUBCOMMAND', 'Manage the Legion launchd background service'
|
|
263
|
+
subcommand 'service', Legion::CLI::ServiceCommand
|
|
264
|
+
|
|
261
265
|
desc 'bootstrap SOURCE', 'One-command setup: fetch config, scaffold, and install packs'
|
|
262
266
|
subcommand 'bootstrap', Legion::CLI::Bootstrap
|
|
263
267
|
|
|
@@ -198,7 +198,7 @@ module Legion
|
|
|
198
198
|
end
|
|
199
199
|
|
|
200
200
|
def build_chunk_payload(chunk, tags, opts)
|
|
201
|
-
{
|
|
201
|
+
payload = {
|
|
202
202
|
content: chunk[:content],
|
|
203
203
|
content_type: opts[:content_type] || 'absorbed_chunk',
|
|
204
204
|
content_hash: chunk[:content_hash],
|
|
@@ -211,6 +211,8 @@ module Legion
|
|
|
211
211
|
token_count: chunk[:token_count]
|
|
212
212
|
}.merge(opts.fetch(:metadata, {}))
|
|
213
213
|
}
|
|
214
|
+
payload[:access_scope] = opts[:access_scope] if opts.key?(:access_scope)
|
|
215
|
+
payload
|
|
214
216
|
end
|
|
215
217
|
end
|
|
216
218
|
end
|
|
@@ -174,7 +174,8 @@ module Legion
|
|
|
174
174
|
return unless runner_module.respond_to?(:definitions)
|
|
175
175
|
|
|
176
176
|
runner_module.definitions.each_key do |method_name|
|
|
177
|
-
|
|
177
|
+
sanitized = method_name.to_s.delete('?!')
|
|
178
|
+
const_name = "#{camelize(runner_name)}#{camelize(sanitized)}"
|
|
178
179
|
next if ctx[:messages_mod].const_defined?(const_name, false)
|
|
179
180
|
|
|
180
181
|
rk_value = "#{ctx[:prefix]}.runners.#{runner_name}.#{method_name}"
|
data/lib/legion/extensions.rb
CHANGED
|
@@ -132,6 +132,24 @@ module Legion
|
|
|
132
132
|
Legion::Logging.info "[Extensions] flushed #{count} pending registrations" if defined?(Legion::Logging)
|
|
133
133
|
end
|
|
134
134
|
|
|
135
|
+
def require_identity_extensions
|
|
136
|
+
find_extensions.select { |entry| entry[:category] == :identity }.each do |entry|
|
|
137
|
+
gem_name = entry[:gem_name]
|
|
138
|
+
ext_settings = extension_settings_for_entry(entry)
|
|
139
|
+
|
|
140
|
+
if ext_settings.is_a?(Hash) && ext_settings.key?(:enabled) && !ext_settings[:enabled]
|
|
141
|
+
Legion::Logging.info "Skipping #{gem_name} identity preload because it's disabled"
|
|
142
|
+
next
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
Catalog.register(gem_name)
|
|
146
|
+
register_extension_handle(gem_name, state: :registered,
|
|
147
|
+
latest_installed_version: latest_installed_version(gem_name))
|
|
148
|
+
ensure_namespace(entry[:const_path]) if entry[:segments].length > 1
|
|
149
|
+
gem_load(entry)
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
135
153
|
def pause_actors
|
|
136
154
|
@running_instances&.each do |inst|
|
|
137
155
|
timer = inst.instance_variable_get(:@timer)
|
|
@@ -1061,7 +1079,7 @@ module Legion
|
|
|
1061
1079
|
parts[(idx + 1)..].to_a.each_with_object([]) do |part, extension_parts|
|
|
1062
1080
|
break extension_parts if %w[Actor Actors Runners Helpers Transport Data Hooks Skills].include?(part)
|
|
1063
1081
|
|
|
1064
|
-
extension_parts << camel_to_snake(part)
|
|
1082
|
+
extension_parts << camel_to_snake(part)
|
|
1065
1083
|
end
|
|
1066
1084
|
end
|
|
1067
1085
|
|
|
@@ -21,6 +21,10 @@ module Legion
|
|
|
21
21
|
token
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
+
def credential_for(provider_name, qualifier: nil, for_context: nil, purpose: nil, context: nil)
|
|
25
|
+
token_for(provider_name, qualifier: qualifier, for_context: for_context, purpose: purpose, context: context)
|
|
26
|
+
end
|
|
27
|
+
|
|
24
28
|
def lease_for(provider_name, qualifier: nil)
|
|
25
29
|
name = provider_name.to_sym
|
|
26
30
|
resolved = qualifier || default_qualifier_for(name)
|
|
@@ -33,6 +33,12 @@ module Legion
|
|
|
33
33
|
|
|
34
34
|
winning_provider, winning_result, provider_results = resolve_auth(auth_providers, timeout: timeout)
|
|
35
35
|
|
|
36
|
+
if winning_provider.nil?
|
|
37
|
+
log.debug('resolve!: no auth winner, trying cached identity')
|
|
38
|
+
winning_provider, winning_result, cached_results = resolve_cached_identity
|
|
39
|
+
provider_results.merge!(cached_results) if cached_results
|
|
40
|
+
end
|
|
41
|
+
|
|
36
42
|
if winning_provider.nil?
|
|
37
43
|
log.debug('resolve!: no auth winner, trying fallback providers')
|
|
38
44
|
winning_provider, winning_result, fallback_results = resolve_auth(fallback_providers, timeout: timeout)
|
|
@@ -229,6 +235,67 @@ module Legion
|
|
|
229
235
|
end
|
|
230
236
|
end
|
|
231
237
|
|
|
238
|
+
def resolve_cached_identity
|
|
239
|
+
cached = read_cached_identity
|
|
240
|
+
return [nil, nil, {}] unless cached
|
|
241
|
+
|
|
242
|
+
provider = cached_identity_provider
|
|
243
|
+
result = {
|
|
244
|
+
canonical_name: cached[:canonical_name],
|
|
245
|
+
kind: cached[:kind] || :human,
|
|
246
|
+
source: :identity_json,
|
|
247
|
+
persistent: true
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
[
|
|
251
|
+
provider,
|
|
252
|
+
result,
|
|
253
|
+
{
|
|
254
|
+
provider.provider_name => {
|
|
255
|
+
status: :resolved,
|
|
256
|
+
trust: provider.trust_level,
|
|
257
|
+
resolved_at: Time.now,
|
|
258
|
+
provider: provider,
|
|
259
|
+
result: result
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
]
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def read_cached_identity
|
|
266
|
+
path = File.expand_path('~/.legionio/settings/identity.json')
|
|
267
|
+
return nil unless File.file?(path)
|
|
268
|
+
|
|
269
|
+
data = if defined?(Legion::JSON)
|
|
270
|
+
Legion::JSON.load(File.read(path))
|
|
271
|
+
else
|
|
272
|
+
require 'json'
|
|
273
|
+
::JSON.parse(File.read(path), symbolize_names: true)
|
|
274
|
+
end
|
|
275
|
+
canonical = data[:canonical_name] || data['canonical_name']
|
|
276
|
+
return nil if canonical.to_s.strip.empty?
|
|
277
|
+
|
|
278
|
+
{
|
|
279
|
+
canonical_name: canonical.to_s,
|
|
280
|
+
kind: (data[:kind] || data['kind'] || :human).to_sym
|
|
281
|
+
}
|
|
282
|
+
rescue StandardError => e
|
|
283
|
+
log.warn("identity.json read failed: #{e.message}")
|
|
284
|
+
nil
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def cached_identity_provider
|
|
288
|
+
@cached_identity_provider ||= Module.new do
|
|
289
|
+
module_function
|
|
290
|
+
|
|
291
|
+
def provider_name = :identity_cache
|
|
292
|
+
def provider_type = :auth
|
|
293
|
+
def priority = -100
|
|
294
|
+
def trust_weight = 150
|
|
295
|
+
def trust_level = :cached
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
|
|
232
299
|
def auth_future_status(future, result)
|
|
233
300
|
if future.rejected?
|
|
234
301
|
:failed
|
data/lib/legion/service.rb
CHANGED
|
@@ -112,6 +112,8 @@ module Legion
|
|
|
112
112
|
end
|
|
113
113
|
setup_cluster if data
|
|
114
114
|
|
|
115
|
+
setup_identity_before_llm(extensions: extensions, transport: transport)
|
|
116
|
+
|
|
115
117
|
if llm
|
|
116
118
|
begin
|
|
117
119
|
setup_llm
|
|
@@ -161,15 +163,14 @@ module Legion
|
|
|
161
163
|
setup_safety_metrics
|
|
162
164
|
setup_supervision if supervision
|
|
163
165
|
|
|
164
|
-
require_relative 'identity' if File.exist?(File.expand_path('identity.rb', __dir__))
|
|
165
|
-
|
|
166
166
|
if extensions
|
|
167
167
|
load_extensions
|
|
168
168
|
Legion::Readiness.mark_ready(:extensions)
|
|
169
169
|
setup_generated_functions
|
|
170
170
|
end
|
|
171
171
|
|
|
172
|
-
#
|
|
172
|
+
# Re-run identity after full extension load so any providers with autobuild-time
|
|
173
|
+
# registration can upgrade the pre-LLM identity.
|
|
173
174
|
db_available = defined?(Legion::Data) && Legion::Data.respond_to?(:connected?) && Legion::Data.connected?
|
|
174
175
|
setup_identity if transport || db_available
|
|
175
176
|
register_credential_providers if extensions && (transport || db_available)
|
|
@@ -589,7 +590,7 @@ module Legion
|
|
|
589
590
|
end
|
|
590
591
|
end
|
|
591
592
|
|
|
592
|
-
def setup_logging_transport # rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
|
|
593
|
+
def setup_logging_transport # rubocop:disable Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
|
|
593
594
|
return unless defined?(Legion::Transport::Connection)
|
|
594
595
|
return unless Legion::Transport::Connection.session_open?
|
|
595
596
|
|
|
@@ -612,11 +613,16 @@ module Legion
|
|
|
612
613
|
exchange = log_channel.topic('legion.logging', durable: true)
|
|
613
614
|
|
|
614
615
|
if forward_logs
|
|
615
|
-
Legion::Logging.log_writer = lambda { |event, routing_key
|
|
616
|
+
Legion::Logging.log_writer = lambda { |event, routing_key:, headers: {}, properties: {}|
|
|
616
617
|
begin
|
|
617
618
|
next unless log_channel&.open?
|
|
618
619
|
|
|
619
|
-
exchange.publish(
|
|
620
|
+
exchange.publish(
|
|
621
|
+
Legion::JSON.dump(event),
|
|
622
|
+
routing_key: routing_key,
|
|
623
|
+
headers: headers,
|
|
624
|
+
**properties
|
|
625
|
+
)
|
|
620
626
|
rescue StandardError
|
|
621
627
|
nil
|
|
622
628
|
end
|
|
@@ -918,6 +924,8 @@ module Legion
|
|
|
918
924
|
Legion::Readiness.mark_skipped(:rbac)
|
|
919
925
|
end
|
|
920
926
|
|
|
927
|
+
setup_identity_before_llm(extensions: true, transport: true)
|
|
928
|
+
|
|
921
929
|
if defined?(Legion::LLM)
|
|
922
930
|
setup_llm
|
|
923
931
|
else
|
|
@@ -945,7 +953,6 @@ module Legion
|
|
|
945
953
|
# Phase 5: re-run identity resolution after extensions are loaded so that
|
|
946
954
|
# any identity providers registered by lex-identity-* extensions are
|
|
947
955
|
# available to the resolver (mirrors the boot-time ordering).
|
|
948
|
-
Legion::Identity::Resolver.reset! if defined?(Legion::Identity::Resolver)
|
|
949
956
|
setup_identity
|
|
950
957
|
|
|
951
958
|
db_available = defined?(Legion::Data) && Legion::Data.respond_to?(:connected?) && Legion::Data.connected?
|
|
@@ -976,6 +983,18 @@ module Legion
|
|
|
976
983
|
Legion::Extensions.hook_extensions
|
|
977
984
|
end
|
|
978
985
|
|
|
986
|
+
def setup_identity_before_llm(extensions:, transport:)
|
|
987
|
+
require_relative 'identity' if File.exist?(File.expand_path('identity.rb', __dir__))
|
|
988
|
+
Legion::Extensions.require_identity_extensions if extensions &&
|
|
989
|
+
defined?(Legion::Extensions) &&
|
|
990
|
+
Legion::Extensions.respond_to?(:require_identity_extensions)
|
|
991
|
+
|
|
992
|
+
db_available = defined?(Legion::Data) && Legion::Data.respond_to?(:connected?) && Legion::Data.connected?
|
|
993
|
+
setup_identity if transport || db_available
|
|
994
|
+
rescue StandardError => e
|
|
995
|
+
handle_exception(e, level: :warn, operation: 'service.setup_identity_before_llm')
|
|
996
|
+
end
|
|
997
|
+
|
|
979
998
|
def register_core_tools
|
|
980
999
|
require 'legion/tools'
|
|
981
1000
|
Legion::Tools.register_all
|
|
@@ -125,6 +125,7 @@ module Legion
|
|
|
125
125
|
)
|
|
126
126
|
return unless Legion::Tools::Registry.register(tool_class)
|
|
127
127
|
|
|
128
|
+
register_in_settings_extensions(tool_class, ext, runner_mod, is_deferred)
|
|
128
129
|
record_tool_owner(ext, tool_class)
|
|
129
130
|
end
|
|
130
131
|
|
|
@@ -216,6 +217,28 @@ module Legion
|
|
|
216
217
|
end
|
|
217
218
|
end
|
|
218
219
|
|
|
220
|
+
def register_in_settings_extensions(tool_class, ext, runner_mod, is_deferred)
|
|
221
|
+
return unless defined?(Legion::Settings::Extensions) &&
|
|
222
|
+
Legion::Settings::Extensions.respond_to?(:register_tool)
|
|
223
|
+
|
|
224
|
+
ext_name = derive_extension_name(ext)
|
|
225
|
+
Legion::Settings::Extensions.register_tool(tool_class.tool_name, {
|
|
226
|
+
description: tool_class.respond_to?(:description) ? tool_class.description : nil,
|
|
227
|
+
input_schema: tool_class.respond_to?(:input_schema) ? tool_class.input_schema : {},
|
|
228
|
+
tool_class: tool_class,
|
|
229
|
+
dispatch_type: :class_call,
|
|
230
|
+
extension: "lex-#{ext_name}",
|
|
231
|
+
runner: derive_runner_snake(runner_mod),
|
|
232
|
+
source: :tools_discovery,
|
|
233
|
+
deferred: is_deferred,
|
|
234
|
+
trigger_words: tool_class.respond_to?(:trigger_words) ? tool_class.trigger_words : [],
|
|
235
|
+
sticky: tool_class.respond_to?(:sticky?) ? tool_class.sticky? : true,
|
|
236
|
+
mcp_tier: tool_class.respond_to?(:mcp_tier) ? tool_class.mcp_tier : nil
|
|
237
|
+
})
|
|
238
|
+
rescue StandardError => e
|
|
239
|
+
handle_exception(e, level: :warn, handled: true, operation: :register_in_settings_extensions)
|
|
240
|
+
end
|
|
241
|
+
|
|
219
242
|
def record_tool_owner(ext, tool_class)
|
|
220
243
|
return unless defined?(Legion::Extensions) && Legion::Extensions.respond_to?(:record_extension_resource)
|
|
221
244
|
|
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.9.
|
|
4
|
+
version: 1.9.36
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Esity
|
|
@@ -750,6 +750,7 @@ files:
|
|
|
750
750
|
- lib/legion/cli/relationship.rb
|
|
751
751
|
- lib/legion/cli/review_command.rb
|
|
752
752
|
- lib/legion/cli/schedule_command.rb
|
|
753
|
+
- lib/legion/cli/service_command.rb
|
|
753
754
|
- lib/legion/cli/setup_command.rb
|
|
754
755
|
- lib/legion/cli/skill_command.rb
|
|
755
756
|
- lib/legion/cli/start.rb
|