legionio 1.7.31 → 1.7.33
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 +27 -0
- data/lib/legion/api/extensions.rb +115 -45
- data/lib/legion/api/helpers.rb +33 -0
- data/lib/legion/api/openapi.rb +97 -59
- data/lib/legion/cli/chat/tools/list_extensions.rb +20 -22
- data/lib/legion/extensions/catalog/available.rb +158 -0
- data/lib/legion/extensions/catalog.rb +1 -0
- data/lib/legion/identity/broker.rb +59 -15
- data/lib/legion/identity/lease_renewer.rb +1 -1
- data/lib/legion/service.rb +18 -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: 839e777ef392bc9f9a005418901f90837a78e10dc16f4425788332f8098a449c
|
|
4
|
+
data.tar.gz: 81c74483a04d1e5ae46789b0648153df04e954b94415168063fb5831a67d83c5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3828b5dc1f52bd1e1a1f6f1c63d5fe2fe148f019be8e24fdf58cc1c0c69c4788d99f64a119398d9d8209042e21ba47f48a24a9b8692227b5851113825dc7d3c1
|
|
7
|
+
data.tar.gz: 3e05508d0d71bbed405d18219f79e7e4745ab28be2e16755747822bc1ec0dae2b441b3810b6dd0c1ec42cef58ccfacea065e2b33588f1bc20d225c5e473f4415
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# Legion Changelog
|
|
2
2
|
|
|
3
|
+
## [1.7.33] - 2026-04-09
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- Phase 8 prerequisites: `Broker.lease_for(name)` returns raw Lease, `Broker.renewer_for(name)` returns LeaseRenewer
|
|
7
|
+
- `LeaseRenewer` now exposes `attr_reader :provider` for structured credential access
|
|
8
|
+
- Non-renewing registration path: static API key providers (expires_at: nil, renewable: false) stored in `Concurrent::AtomicReference` without background LeaseRenewer thread
|
|
9
|
+
- `Broker.refresh_credential(name)` for manual refresh of static credentials
|
|
10
|
+
- `Broker.providers` and `Broker.leases` include both dynamic and static registrations
|
|
11
|
+
- `register_provider_with_broker` in service.rb — winning auth provider auto-registered with Broker after identity resolution
|
|
12
|
+
|
|
13
|
+
### Changed (Copilot review #126)
|
|
14
|
+
- Renamed extension catalog routes from `/api/extensions` to `/api/extension_catalog` to eliminate route conflict with LexDispatch's `GET /api/extensions/:lex_name/:component_type/:component_name/:method_name` wildcard
|
|
15
|
+
- Updated `GET /api/extension_catalog/available` (was `/api/extensions/available`)
|
|
16
|
+
- Updated OpenAPI spec paths and `list_extensions` chat tool to match new route prefix
|
|
17
|
+
- Froze individual entry hashes in `Catalog::Available::EXTENSIONS` via `.each(&:freeze).freeze`; `all`, `by_category`, and `find` now return dup copies to prevent caller mutation
|
|
18
|
+
- Added explicit `require 'legion/api/helpers'` and `require 'legion/api/extensions'` to `spec/legion/api/extensions_spec.rb` for deterministic spec loading
|
|
19
|
+
- Added `loader.settings[:data]`, `[:transport]`, and `[:extensions]` initialization to extensions spec `before(:all)` for isolation
|
|
20
|
+
|
|
21
|
+
## [1.7.32] - 2026-04-09
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
- Rewrote `/api/extensions` routes to use in-memory state from `Catalog` instead of database queries — no `require_data!` dependency
|
|
25
|
+
- All extension routes now use `:name` (string identifier like `lex-node`) instead of numeric `:id` params
|
|
26
|
+
- Added `GET /api/extensions/available` route backed by `Catalog::Available.all` (static ecosystem list, filterable by `?category=`)
|
|
27
|
+
- Added `Legion::Extensions::Catalog::Available` module with 120+ known LEX gems organized by category
|
|
28
|
+
- Extension helper methods (`find_extension_module`, `find_runner_info`, `runner_summaries`, `halt_not_found`) moved into `Legion::API::Helpers` for reuse across all API tests
|
|
29
|
+
|
|
3
30
|
## [1.7.31] - 2026-04-08
|
|
4
31
|
|
|
5
32
|
### Added
|
|
@@ -5,87 +5,157 @@ module Legion
|
|
|
5
5
|
module Routes
|
|
6
6
|
module Extensions
|
|
7
7
|
def self.registered(app)
|
|
8
|
+
register_available_route(app)
|
|
8
9
|
register_extension_routes(app)
|
|
9
10
|
register_runner_routes(app)
|
|
10
11
|
register_function_routes(app)
|
|
12
|
+
register_invoke_route(app)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.register_available_route(app)
|
|
16
|
+
app.get '/api/extension_catalog/available' do
|
|
17
|
+
entries = Legion::Extensions::Catalog::Available.all
|
|
18
|
+
entries = entries.select { |e| e[:category] == params[:category] } if params[:category]
|
|
19
|
+
json_response(entries)
|
|
20
|
+
end
|
|
11
21
|
end
|
|
12
22
|
|
|
13
23
|
def self.register_extension_routes(app)
|
|
14
|
-
app.get '/api/
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
24
|
+
app.get '/api/extension_catalog' do
|
|
25
|
+
entries = Legion::Extensions::Catalog.all.map do |name, entry|
|
|
26
|
+
{ name: name, state: entry[:state].to_s,
|
|
27
|
+
registered_at: entry[:registered_at]&.iso8601,
|
|
28
|
+
started_at: entry[:started_at]&.iso8601 }
|
|
29
|
+
end
|
|
30
|
+
entries = entries.select { |e| e[:state] == params[:state] } if params[:state]
|
|
31
|
+
json_response(entries)
|
|
19
32
|
end
|
|
20
33
|
|
|
21
|
-
app.get '/api/
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
34
|
+
app.get '/api/extension_catalog/:name' do
|
|
35
|
+
name = params[:name]
|
|
36
|
+
entry = Legion::Extensions::Catalog.entry(name)
|
|
37
|
+
halt_not_found("extension '#{name}' not found") unless entry
|
|
38
|
+
|
|
39
|
+
ext_mod = find_extension_module(name)
|
|
40
|
+
version = ext_mod&.const_defined?(:VERSION) ? ext_mod::VERSION : nil
|
|
41
|
+
|
|
42
|
+
runners = ext_mod ? runner_summaries(ext_mod) : []
|
|
43
|
+
|
|
44
|
+
json_response({
|
|
45
|
+
name: name,
|
|
46
|
+
state: entry[:state].to_s,
|
|
47
|
+
version: version,
|
|
48
|
+
registered_at: entry[:registered_at]&.iso8601,
|
|
49
|
+
started_at: entry[:started_at]&.iso8601,
|
|
50
|
+
runners: runners
|
|
51
|
+
}.compact)
|
|
25
52
|
end
|
|
26
53
|
end
|
|
27
54
|
|
|
28
55
|
def self.register_runner_routes(app)
|
|
29
|
-
app.get '/api/
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
56
|
+
app.get '/api/extension_catalog/:name/runners' do
|
|
57
|
+
name = params[:name]
|
|
58
|
+
halt_not_found("extension '#{name}' not found") unless Legion::Extensions::Catalog.entry(name)
|
|
59
|
+
|
|
60
|
+
ext_mod = find_extension_module(name)
|
|
61
|
+
halt_not_found("extension '#{name}' not loaded") unless ext_mod
|
|
62
|
+
|
|
63
|
+
json_response(runner_summaries(ext_mod))
|
|
34
64
|
end
|
|
35
65
|
|
|
36
|
-
app.get '/api/
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
66
|
+
app.get '/api/extension_catalog/:name/runners/:runner_name' do
|
|
67
|
+
name = params[:name]
|
|
68
|
+
halt_not_found("extension '#{name}' not found") unless Legion::Extensions::Catalog.entry(name)
|
|
69
|
+
|
|
70
|
+
ext_mod = find_extension_module(name)
|
|
71
|
+
halt_not_found("extension '#{name}' not loaded") unless ext_mod
|
|
72
|
+
|
|
73
|
+
info = find_runner_info(ext_mod, params[:runner_name])
|
|
74
|
+
halt_not_found("runner '#{params[:runner_name]}' not found") unless info
|
|
75
|
+
|
|
76
|
+
runner_mod = info[:runner_module]
|
|
77
|
+
functions = runner_mod.instance_methods(false).map(&:to_s)
|
|
78
|
+
|
|
79
|
+
json_response({
|
|
80
|
+
name: info[:runner_name],
|
|
81
|
+
runner_class: info[:runner_class],
|
|
82
|
+
functions: functions
|
|
83
|
+
})
|
|
41
84
|
end
|
|
42
85
|
end
|
|
43
86
|
|
|
44
|
-
def self.register_function_routes(app)
|
|
45
|
-
app.get '/api/
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
87
|
+
def self.register_function_routes(app)
|
|
88
|
+
app.get '/api/extension_catalog/:name/runners/:runner_name/functions' do
|
|
89
|
+
name = params[:name]
|
|
90
|
+
halt_not_found("extension '#{name}' not found") unless Legion::Extensions::Catalog.entry(name)
|
|
91
|
+
|
|
92
|
+
ext_mod = find_extension_module(name)
|
|
93
|
+
halt_not_found("extension '#{name}' not loaded") unless ext_mod
|
|
94
|
+
|
|
95
|
+
info = find_runner_info(ext_mod, params[:runner_name])
|
|
96
|
+
halt_not_found("runner '#{params[:runner_name]}' not found") unless info
|
|
97
|
+
|
|
98
|
+
functions = info[:runner_module].instance_methods(false).map do |m|
|
|
99
|
+
args = info.dig(:class_methods, m, :args)
|
|
100
|
+
{ name: m.to_s, args: args }
|
|
101
|
+
end
|
|
102
|
+
json_response(functions)
|
|
51
103
|
end
|
|
52
104
|
|
|
53
|
-
app.get '/api/
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
105
|
+
app.get '/api/extension_catalog/:name/runners/:runner_name/functions/:function_name' do
|
|
106
|
+
name = params[:name]
|
|
107
|
+
halt_not_found("extension '#{name}' not found") unless Legion::Extensions::Catalog.entry(name)
|
|
108
|
+
|
|
109
|
+
ext_mod = find_extension_module(name)
|
|
110
|
+
halt_not_found("extension '#{name}' not loaded") unless ext_mod
|
|
111
|
+
|
|
112
|
+
info = find_runner_info(ext_mod, params[:runner_name])
|
|
113
|
+
halt_not_found("runner '#{params[:runner_name]}' not found") unless info
|
|
114
|
+
|
|
115
|
+
func_sym = params[:function_name].to_sym
|
|
116
|
+
halt_not_found("function '#{params[:function_name]}' not found") unless info[:runner_module].method_defined?(func_sym, false)
|
|
117
|
+
|
|
118
|
+
args = info.dig(:class_methods, func_sym, :args)
|
|
119
|
+
json_response({ name: params[:function_name], runner: params[:runner_name], args: args })
|
|
59
120
|
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def self.register_invoke_route(app)
|
|
124
|
+
app.post '/api/extension_catalog/:name/runners/:runner_name/functions/:function_name/invoke' do
|
|
125
|
+
name = params[:name]
|
|
126
|
+
halt_not_found("extension '#{name}' not found") unless Legion::Extensions::Catalog.entry(name)
|
|
127
|
+
|
|
128
|
+
ext_mod = find_extension_module(name)
|
|
129
|
+
halt_not_found("extension '#{name}' not loaded") unless ext_mod
|
|
130
|
+
|
|
131
|
+
info = find_runner_info(ext_mod, params[:runner_name])
|
|
132
|
+
halt_not_found("runner '#{params[:runner_name]}' not found") unless info
|
|
133
|
+
|
|
134
|
+
func_sym = params[:function_name].to_sym
|
|
135
|
+
halt_not_found("function '#{params[:function_name]}' not found") unless info[:runner_module].method_defined?(func_sym, false)
|
|
60
136
|
|
|
61
|
-
app.post '/api/extensions/:id/runners/:runner_id/functions/:function_id/invoke' do
|
|
62
|
-
require_data!
|
|
63
|
-
path = "/api/extensions/#{params[:id]}/runners/#{params[:runner_id]}/functions/#{params[:function_id]}/invoke"
|
|
64
|
-
Legion::Logging.debug "API: POST #{path} params=#{params.keys}"
|
|
65
|
-
find_or_halt(Legion::Data::Model::Extension, params[:id])
|
|
66
|
-
runner = find_or_halt(Legion::Data::Model::Runner, params[:runner_id])
|
|
67
|
-
func = find_or_halt(Legion::Data::Model::Function, params[:function_id])
|
|
68
137
|
body = parse_request_body
|
|
69
138
|
|
|
70
139
|
result = Legion::Ingress.run(
|
|
71
|
-
payload:
|
|
72
|
-
|
|
140
|
+
payload: body,
|
|
141
|
+
runner_class: info[:runner_class],
|
|
142
|
+
function: func_sym,
|
|
143
|
+
source: 'api',
|
|
73
144
|
check_subtask: body.fetch(:check_subtask, true),
|
|
74
145
|
generate_task: body.fetch(:generate_task, true)
|
|
75
146
|
)
|
|
76
|
-
Legion::Logging.info "API: invoked function #{func.values[:name]} via runner #{runner.values[:namespace]}, task #{result[:task_id]}"
|
|
77
147
|
json_response(result, status_code: 201)
|
|
78
148
|
rescue NameError => e
|
|
79
|
-
Legion::Logging.warn "API POST /api/extensions invoke returned 422: #{e.message}"
|
|
80
149
|
json_error('invalid_runner', e.message, status_code: 422)
|
|
81
150
|
rescue StandardError => e
|
|
82
|
-
Legion::Logging.error "API POST /api/
|
|
151
|
+
Legion::Logging.error "API POST /api/extension_catalog invoke: #{e.class} - #{e.message}" if defined?(Legion::Logging)
|
|
83
152
|
json_error('execution_error', e.message, status_code: 500)
|
|
84
153
|
end
|
|
85
154
|
end
|
|
86
155
|
|
|
87
156
|
class << self
|
|
88
|
-
private :
|
|
157
|
+
private :register_available_route, :register_extension_routes,
|
|
158
|
+
:register_runner_routes, :register_function_routes, :register_invoke_route
|
|
89
159
|
end
|
|
90
160
|
end
|
|
91
161
|
end
|
data/lib/legion/api/helpers.rb
CHANGED
|
@@ -101,6 +101,39 @@ module Legion
|
|
|
101
101
|
halt 400, json_error('invalid_json', 'request body is not valid JSON', status_code: 400)
|
|
102
102
|
end
|
|
103
103
|
|
|
104
|
+
def find_extension_module(lex_name)
|
|
105
|
+
short = lex_name.delete_prefix('lex-')
|
|
106
|
+
short_no_sep = short.tr('-', '_').delete('_')
|
|
107
|
+
Legion::Extensions.loaded_extension_modules.find do |mod|
|
|
108
|
+
parts = mod.name&.split('::')
|
|
109
|
+
mod_short = parts&.last&.downcase
|
|
110
|
+
mod_short == short.tr('-', '_') ||
|
|
111
|
+
mod_short == short.delete('-') ||
|
|
112
|
+
mod_short == short_no_sep
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def find_runner_info(ext_mod, runner_name)
|
|
117
|
+
return nil unless ext_mod.respond_to?(:runners)
|
|
118
|
+
|
|
119
|
+
ext_mod.runners.values.find do |r|
|
|
120
|
+
r[:runner_name].to_s.downcase == runner_name.downcase
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def runner_summaries(ext_mod)
|
|
125
|
+
return [] unless ext_mod.respond_to?(:runners)
|
|
126
|
+
|
|
127
|
+
ext_mod.runners.values.map do |r|
|
|
128
|
+
functions = r[:runner_module]&.instance_methods(false)&.map(&:to_s) || []
|
|
129
|
+
{ name: r[:runner_name], runner_class: r[:runner_class], functions: functions }
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def halt_not_found(message)
|
|
134
|
+
halt 404, json_error('not_found', message, status_code: 404)
|
|
135
|
+
end
|
|
136
|
+
|
|
104
137
|
def find_or_halt(model_class, id)
|
|
105
138
|
record = model_class[id.to_i]
|
|
106
139
|
halt 404, json_error('not_found', "#{model_class.name.split('::').last} #{id} not found", status_code: 404) if record.nil?
|
data/lib/legion/api/openapi.rb
CHANGED
|
@@ -177,22 +177,23 @@ module Legion
|
|
|
177
177
|
}
|
|
178
178
|
},
|
|
179
179
|
schemas: {
|
|
180
|
-
Meta:
|
|
181
|
-
MetaCollection:
|
|
182
|
-
ErrorResponse:
|
|
183
|
-
DeletedResponse:
|
|
184
|
-
TaskObject:
|
|
185
|
-
TaskInput:
|
|
186
|
-
ExtensionObject:
|
|
187
|
-
RunnerObject:
|
|
188
|
-
FunctionObject:
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
180
|
+
Meta: META_SCHEMA,
|
|
181
|
+
MetaCollection: META_COLLECTION_SCHEMA,
|
|
182
|
+
ErrorResponse: ERROR_SCHEMA,
|
|
183
|
+
DeletedResponse: deleted_response_schema,
|
|
184
|
+
TaskObject: task_object_schema,
|
|
185
|
+
TaskInput: task_input_schema,
|
|
186
|
+
ExtensionObject: extension_object_schema,
|
|
187
|
+
RunnerObject: runner_object_schema,
|
|
188
|
+
FunctionObject: function_object_schema,
|
|
189
|
+
AvailableExtensionObject: available_extension_object_schema,
|
|
190
|
+
NodeObject: node_object_schema,
|
|
191
|
+
ScheduleObject: schedule_object_schema,
|
|
192
|
+
ScheduleInput: schedule_input_schema,
|
|
193
|
+
RelationshipObject: stub_object_schema('Relationship'),
|
|
194
|
+
ChainObject: stub_object_schema('Chain'),
|
|
195
|
+
WorkerObject: worker_object_schema,
|
|
196
|
+
WorkerInput: worker_input_schema
|
|
196
197
|
}
|
|
197
198
|
}
|
|
198
199
|
end
|
|
@@ -240,11 +241,12 @@ module Legion
|
|
|
240
241
|
{
|
|
241
242
|
type: 'object',
|
|
242
243
|
properties: {
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
244
|
+
name: { type: 'string' },
|
|
245
|
+
state: { type: 'string' },
|
|
246
|
+
version: { type: 'string', nullable: true },
|
|
247
|
+
registered_at: { type: 'string', format: 'date-time', nullable: true },
|
|
248
|
+
started_at: { type: 'string', format: 'date-time', nullable: true },
|
|
249
|
+
runners: { type: 'array', items: { '$ref' => '#/components/schemas/RunnerObject' } }
|
|
248
250
|
}
|
|
249
251
|
}
|
|
250
252
|
end
|
|
@@ -254,10 +256,9 @@ module Legion
|
|
|
254
256
|
{
|
|
255
257
|
type: 'object',
|
|
256
258
|
properties: {
|
|
257
|
-
id: { type: 'integer' },
|
|
258
|
-
extension_id: { type: 'integer' },
|
|
259
259
|
name: { type: 'string' },
|
|
260
|
-
|
|
260
|
+
runner_class: { type: 'string' },
|
|
261
|
+
functions: { type: 'array', items: { type: 'string' } }
|
|
261
262
|
}
|
|
262
263
|
}
|
|
263
264
|
end
|
|
@@ -267,14 +268,26 @@ module Legion
|
|
|
267
268
|
{
|
|
268
269
|
type: 'object',
|
|
269
270
|
properties: {
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
271
|
+
name: { type: 'string' },
|
|
272
|
+
runner: { type: 'string' },
|
|
273
|
+
args: { type: 'object', nullable: true }
|
|
273
274
|
}
|
|
274
275
|
}
|
|
275
276
|
end
|
|
276
277
|
private_class_method :function_object_schema
|
|
277
278
|
|
|
279
|
+
def self.available_extension_object_schema
|
|
280
|
+
{
|
|
281
|
+
type: 'object',
|
|
282
|
+
properties: {
|
|
283
|
+
name: { type: 'string' },
|
|
284
|
+
category: { type: 'string' },
|
|
285
|
+
description: { type: 'string' }
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
end
|
|
289
|
+
private_class_method :available_extension_object_schema
|
|
290
|
+
|
|
278
291
|
def self.node_object_schema
|
|
279
292
|
{
|
|
280
293
|
type: 'object',
|
|
@@ -370,6 +383,17 @@ module Legion
|
|
|
370
383
|
|
|
371
384
|
# --- route path builders ---
|
|
372
385
|
|
|
386
|
+
def self.wrap_array(schema_ref)
|
|
387
|
+
{
|
|
388
|
+
type: 'object',
|
|
389
|
+
properties: {
|
|
390
|
+
data: { type: 'array', items: { '$ref' => "#/components/schemas/#{schema_ref}" } },
|
|
391
|
+
meta: { '$ref' => '#/components/schemas/Meta' }
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
end
|
|
395
|
+
private_class_method :wrap_array
|
|
396
|
+
|
|
373
397
|
def self.wrap_data(schema_ref)
|
|
374
398
|
{
|
|
375
399
|
type: 'object',
|
|
@@ -520,28 +544,42 @@ module Legion
|
|
|
520
544
|
|
|
521
545
|
def self.extension_paths
|
|
522
546
|
{
|
|
523
|
-
'/api/
|
|
547
|
+
'/api/extension_catalog' => {
|
|
524
548
|
get: {
|
|
525
549
|
tags: ['Extensions'],
|
|
526
|
-
summary: 'List extensions',
|
|
550
|
+
summary: 'List loaded extensions',
|
|
527
551
|
operationId: 'listExtensions',
|
|
528
|
-
parameters:
|
|
529
|
-
{ name: '
|
|
530
|
-
schema: { type: '
|
|
552
|
+
parameters: [
|
|
553
|
+
{ name: 'state', in: 'query', description: 'Filter by extension state (e.g. running)', required: false,
|
|
554
|
+
schema: { type: 'string' } }
|
|
531
555
|
],
|
|
532
556
|
responses: {
|
|
533
|
-
'200' => ok_response('Extension list',
|
|
534
|
-
'401' => UNAUTH_RESPONSE
|
|
535
|
-
'503' => { description: 'legion-data not connected' }
|
|
557
|
+
'200' => ok_response('Extension list', wrap_array('ExtensionObject')),
|
|
558
|
+
'401' => UNAUTH_RESPONSE
|
|
536
559
|
}
|
|
537
560
|
}
|
|
538
561
|
},
|
|
539
|
-
'/api/
|
|
562
|
+
'/api/extension_catalog/available' => {
|
|
540
563
|
get: {
|
|
541
564
|
tags: ['Extensions'],
|
|
542
|
-
summary: '
|
|
565
|
+
summary: 'List all available extensions in the ecosystem registry',
|
|
566
|
+
operationId: 'listAvailableExtensions',
|
|
567
|
+
parameters: [
|
|
568
|
+
{ name: 'category', in: 'query', description: 'Filter by category (core, ai, agentic, identity, service, other)',
|
|
569
|
+
required: false, schema: { type: 'string' } }
|
|
570
|
+
],
|
|
571
|
+
responses: {
|
|
572
|
+
'200' => ok_response('Available extension list', wrap_array('AvailableExtensionObject')),
|
|
573
|
+
'401' => UNAUTH_RESPONSE
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
},
|
|
577
|
+
'/api/extension_catalog/{name}' => {
|
|
578
|
+
get: {
|
|
579
|
+
tags: ['Extensions'],
|
|
580
|
+
summary: 'Get extension by name',
|
|
543
581
|
operationId: 'getExtension',
|
|
544
|
-
parameters: [{ name: '
|
|
582
|
+
parameters: [{ name: 'name', in: 'path', required: true, schema: { type: 'string' } }],
|
|
545
583
|
responses: {
|
|
546
584
|
'200' => ok_response('Extension detail', wrap_data('ExtensionObject')),
|
|
547
585
|
'401' => UNAUTH_RESPONSE,
|
|
@@ -549,27 +587,27 @@ module Legion
|
|
|
549
587
|
}
|
|
550
588
|
}
|
|
551
589
|
},
|
|
552
|
-
'/api/
|
|
590
|
+
'/api/extension_catalog/{name}/runners' => {
|
|
553
591
|
get: {
|
|
554
592
|
tags: ['Extensions'],
|
|
555
593
|
summary: 'List runners for extension',
|
|
556
594
|
operationId: 'listExtensionRunners',
|
|
557
|
-
parameters: [{ name: '
|
|
595
|
+
parameters: [{ name: 'name', in: 'path', required: true, schema: { type: 'string' } }],
|
|
558
596
|
responses: {
|
|
559
|
-
'200' => ok_response('Runner list',
|
|
597
|
+
'200' => ok_response('Runner list', wrap_array('RunnerObject')),
|
|
560
598
|
'401' => UNAUTH_RESPONSE,
|
|
561
599
|
'404' => NOT_FOUND_RESPONSE
|
|
562
600
|
}
|
|
563
601
|
}
|
|
564
602
|
},
|
|
565
|
-
'/api/
|
|
603
|
+
'/api/extension_catalog/{name}/runners/{runner_name}' => {
|
|
566
604
|
get: {
|
|
567
605
|
tags: ['Extensions'],
|
|
568
|
-
summary: 'Get runner by
|
|
606
|
+
summary: 'Get runner by name',
|
|
569
607
|
operationId: 'getExtensionRunner',
|
|
570
608
|
parameters: [
|
|
571
|
-
{ name: '
|
|
572
|
-
{ name: '
|
|
609
|
+
{ name: 'name', in: 'path', required: true, schema: { type: 'string' } },
|
|
610
|
+
{ name: 'runner_name', in: 'path', required: true, schema: { type: 'string' } }
|
|
573
611
|
],
|
|
574
612
|
responses: {
|
|
575
613
|
'200' => ok_response('Runner detail', wrap_data('RunnerObject')),
|
|
@@ -578,31 +616,31 @@ module Legion
|
|
|
578
616
|
}
|
|
579
617
|
}
|
|
580
618
|
},
|
|
581
|
-
'/api/
|
|
619
|
+
'/api/extension_catalog/{name}/runners/{runner_name}/functions' => {
|
|
582
620
|
get: {
|
|
583
621
|
tags: ['Extensions'],
|
|
584
622
|
summary: 'List functions for runner',
|
|
585
623
|
operationId: 'listRunnerFunctions',
|
|
586
624
|
parameters: [
|
|
587
|
-
{ name: '
|
|
588
|
-
{ name: '
|
|
589
|
-
]
|
|
625
|
+
{ name: 'name', in: 'path', required: true, schema: { type: 'string' } },
|
|
626
|
+
{ name: 'runner_name', in: 'path', required: true, schema: { type: 'string' } }
|
|
627
|
+
],
|
|
590
628
|
responses: {
|
|
591
|
-
'200' => ok_response('Function list',
|
|
629
|
+
'200' => ok_response('Function list', wrap_array('FunctionObject')),
|
|
592
630
|
'401' => UNAUTH_RESPONSE,
|
|
593
631
|
'404' => NOT_FOUND_RESPONSE
|
|
594
632
|
}
|
|
595
633
|
}
|
|
596
634
|
},
|
|
597
|
-
'/api/
|
|
635
|
+
'/api/extension_catalog/{name}/runners/{runner_name}/functions/{function_name}' => {
|
|
598
636
|
get: {
|
|
599
637
|
tags: ['Extensions'],
|
|
600
|
-
summary: 'Get function by
|
|
638
|
+
summary: 'Get function by name',
|
|
601
639
|
operationId: 'getRunnerFunction',
|
|
602
640
|
parameters: [
|
|
603
|
-
{ name: '
|
|
604
|
-
{ name: '
|
|
605
|
-
{ name: '
|
|
641
|
+
{ name: 'name', in: 'path', required: true, schema: { type: 'string' } },
|
|
642
|
+
{ name: 'runner_name', in: 'path', required: true, schema: { type: 'string' } },
|
|
643
|
+
{ name: 'function_name', in: 'path', required: true, schema: { type: 'string' } }
|
|
606
644
|
],
|
|
607
645
|
responses: {
|
|
608
646
|
'200' => ok_response('Function detail', wrap_data('FunctionObject')),
|
|
@@ -611,15 +649,15 @@ module Legion
|
|
|
611
649
|
}
|
|
612
650
|
}
|
|
613
651
|
},
|
|
614
|
-
'/api/
|
|
652
|
+
'/api/extension_catalog/{name}/runners/{runner_name}/functions/{function_name}/invoke' => {
|
|
615
653
|
post: {
|
|
616
654
|
tags: ['Extensions'],
|
|
617
655
|
summary: 'Invoke a function directly',
|
|
618
656
|
operationId: 'invokeFunction',
|
|
619
657
|
parameters: [
|
|
620
|
-
{ name: '
|
|
621
|
-
{ name: '
|
|
622
|
-
{ name: '
|
|
658
|
+
{ name: 'name', in: 'path', required: true, schema: { type: 'string' } },
|
|
659
|
+
{ name: 'runner_name', in: 'path', required: true, schema: { type: 'string' } },
|
|
660
|
+
{ name: 'function_name', in: 'path', required: true, schema: { type: 'string' } }
|
|
623
661
|
],
|
|
624
662
|
requestBody: {
|
|
625
663
|
required: false,
|
|
@@ -18,19 +18,19 @@ module Legion
|
|
|
18
18
|
description 'List loaded Legion extensions and their runners/functions. ' \
|
|
19
19
|
'Use this to discover what capabilities are available, what extensions are active, ' \
|
|
20
20
|
'and what tasks can be triggered through the framework.'
|
|
21
|
-
param :
|
|
22
|
-
|
|
23
|
-
param :
|
|
24
|
-
|
|
21
|
+
param :extension_name, type: 'string',
|
|
22
|
+
desc: 'Show runners for a specific extension by name (e.g. lex-node)', required: false
|
|
23
|
+
param :state, type: 'string',
|
|
24
|
+
desc: 'Filter by state (e.g. "running"). Default: all', required: false
|
|
25
25
|
|
|
26
26
|
DEFAULT_PORT = 4567
|
|
27
27
|
DEFAULT_HOST = '127.0.0.1'
|
|
28
28
|
|
|
29
|
-
def execute(
|
|
30
|
-
if
|
|
31
|
-
fetch_extension_detail(
|
|
29
|
+
def execute(extension_name: nil, state: nil)
|
|
30
|
+
if extension_name
|
|
31
|
+
fetch_extension_detail(extension_name)
|
|
32
32
|
else
|
|
33
|
-
fetch_extension_list(
|
|
33
|
+
fetch_extension_list(state)
|
|
34
34
|
end
|
|
35
35
|
rescue Errno::ECONNREFUSED
|
|
36
36
|
'Legion daemon not running (cannot query extensions API).'
|
|
@@ -41,9 +41,9 @@ module Legion
|
|
|
41
41
|
|
|
42
42
|
private
|
|
43
43
|
|
|
44
|
-
def fetch_extension_list(
|
|
45
|
-
path = '/api/
|
|
46
|
-
path +=
|
|
44
|
+
def fetch_extension_list(state)
|
|
45
|
+
path = '/api/extension_catalog'
|
|
46
|
+
path += "?state=#{state}" if state
|
|
47
47
|
data = api_get(path)
|
|
48
48
|
return "API error: #{data[:error]}" if data[:error]
|
|
49
49
|
|
|
@@ -54,37 +54,35 @@ module Legion
|
|
|
54
54
|
format_list(extensions)
|
|
55
55
|
end
|
|
56
56
|
|
|
57
|
-
def fetch_extension_detail(
|
|
58
|
-
ext_data = api_get("/api/
|
|
59
|
-
runners_data = api_get("/api/extensions/#{ext_id}/runners")
|
|
60
|
-
|
|
57
|
+
def fetch_extension_detail(name)
|
|
58
|
+
ext_data = api_get("/api/extension_catalog/#{name}")
|
|
61
59
|
return "API error: #{ext_data[:error]}" if ext_data[:error]
|
|
62
60
|
|
|
61
|
+
runners_data = api_get("/api/extension_catalog/#{name}/runners")
|
|
63
62
|
runners = runners_data[:data] || runners_data[:items] || runners_data
|
|
64
63
|
runners = [runners] if runners.is_a?(Hash)
|
|
65
64
|
runners = [] unless runners.is_a?(Array)
|
|
66
65
|
|
|
67
|
-
format_detail(ext_data, runners)
|
|
66
|
+
format_detail(ext_data[:data] || ext_data, runners)
|
|
68
67
|
end
|
|
69
68
|
|
|
70
69
|
def format_list(extensions)
|
|
71
70
|
lines = ["Loaded Extensions (#{extensions.size}):\n"]
|
|
72
71
|
extensions.each do |ext|
|
|
73
|
-
|
|
74
|
-
lines << " #{ext[:id]}. #{ext[:name]} (#{status})"
|
|
72
|
+
lines << " #{ext[:name]} (#{ext[:state]})"
|
|
75
73
|
end
|
|
76
74
|
lines.join("\n")
|
|
77
75
|
end
|
|
78
76
|
|
|
79
77
|
def format_detail(ext, runners)
|
|
80
|
-
lines = ["Extension: #{ext[:name]}
|
|
81
|
-
lines << "
|
|
82
|
-
lines << "
|
|
78
|
+
lines = ["Extension: #{ext[:name]}\n"]
|
|
79
|
+
lines << " State: #{ext[:state]}"
|
|
80
|
+
lines << " Version: #{ext[:version]}" if ext[:version]
|
|
83
81
|
|
|
84
82
|
if runners.any?
|
|
85
83
|
lines << "\n Runners (#{runners.size}):"
|
|
86
84
|
runners.each do |r|
|
|
87
|
-
lines << " #{r[:
|
|
85
|
+
lines << " #{r[:name]} (#{r[:runner_class]})"
|
|
88
86
|
end
|
|
89
87
|
else
|
|
90
88
|
lines << "\n No runners registered."
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Catalog
|
|
6
|
+
module Available
|
|
7
|
+
EXTENSIONS = [
|
|
8
|
+
# core
|
|
9
|
+
{ name: 'lex-acp', category: 'core', description: 'Agent communication protocol' },
|
|
10
|
+
{ name: 'lex-audit', category: 'core', description: 'Audit logging and trail' },
|
|
11
|
+
{ name: 'lex-codegen', category: 'core', description: 'Code generation pipeline' },
|
|
12
|
+
{ name: 'lex-conditioner', category: 'core', description: 'Task chain conditioning' },
|
|
13
|
+
{ name: 'lex-detect', category: 'core', description: 'Environment detection and recommendations' },
|
|
14
|
+
{ name: 'lex-exec', category: 'core', description: 'Shell command execution' },
|
|
15
|
+
{ name: 'lex-health', category: 'core', description: 'Health monitoring and metrics' },
|
|
16
|
+
{ name: 'lex-lex', category: 'core', description: 'Extension management' },
|
|
17
|
+
{ name: 'lex-llm-gateway', category: 'core', description: 'LLM gateway and routing' },
|
|
18
|
+
{ name: 'lex-llm-ledger', category: 'core', description: 'LLM cost and usage ledger' },
|
|
19
|
+
{ name: 'lex-log', category: 'core', description: 'Log shipping and aggregation' },
|
|
20
|
+
{ name: 'lex-metering', category: 'core', description: 'Resource metering and accounting' },
|
|
21
|
+
{ name: 'lex-node', category: 'core', description: 'Node identity and registration' },
|
|
22
|
+
{ name: 'lex-ping', category: 'core', description: 'Connectivity checks' },
|
|
23
|
+
{ name: 'lex-react', category: 'core', description: 'Event-driven reaction engine' },
|
|
24
|
+
{ name: 'lex-scheduler', category: 'core', description: 'Cron and interval scheduling' },
|
|
25
|
+
{ name: 'lex-synapse', category: 'core', description: 'Agent-to-agent relationships' },
|
|
26
|
+
{ name: 'lex-tasker', category: 'core', description: 'Task management and lifecycle' },
|
|
27
|
+
{ name: 'lex-telemetry', category: 'core', description: 'OpenTelemetry tracing integration' },
|
|
28
|
+
{ name: 'lex-transformer', category: 'core', description: 'Task chain transformation' },
|
|
29
|
+
{ name: 'lex-webhook', category: 'core', description: 'Inbound webhook receiver' },
|
|
30
|
+
# ai
|
|
31
|
+
{ name: 'lex-azure-ai', category: 'ai', description: 'Azure OpenAI provider integration' },
|
|
32
|
+
{ name: 'lex-bedrock', category: 'ai', description: 'AWS Bedrock LLM provider integration' },
|
|
33
|
+
{ name: 'lex-claude', category: 'ai', description: 'Anthropic Claude provider integration' },
|
|
34
|
+
{ name: 'lex-foundry', category: 'ai', description: 'Azure AI Foundry provider integration' },
|
|
35
|
+
{ name: 'lex-gemini', category: 'ai', description: 'Google Gemini provider integration' },
|
|
36
|
+
{ name: 'lex-ollama', category: 'ai', description: 'Ollama local LLM provider integration' },
|
|
37
|
+
{ name: 'lex-openai', category: 'ai', description: 'OpenAI provider integration' },
|
|
38
|
+
{ name: 'lex-xai', category: 'ai', description: 'xAI Grok provider integration' },
|
|
39
|
+
# agentic
|
|
40
|
+
{ name: 'lex-agentic-affect', category: 'agentic', description: 'Affective state modeling' },
|
|
41
|
+
{ name: 'lex-agentic-attention', category: 'agentic', description: 'Attentional focus and salience' },
|
|
42
|
+
{ name: 'lex-agentic-defense', category: 'agentic', description: 'Defensive behavior and threat response' },
|
|
43
|
+
{ name: 'lex-agentic-executive', category: 'agentic', description: 'Executive function and planning' },
|
|
44
|
+
{ name: 'lex-agentic-homeostasis', category: 'agentic', description: 'Internal state regulation' },
|
|
45
|
+
{ name: 'lex-agentic-imagination', category: 'agentic', description: 'Generative imagination and hypothesis' },
|
|
46
|
+
{ name: 'lex-agentic-inference', category: 'agentic', description: 'Probabilistic inference engine' },
|
|
47
|
+
{ name: 'lex-agentic-integration', category: 'agentic', description: 'Cross-domain knowledge integration' },
|
|
48
|
+
{ name: 'lex-agentic-language', category: 'agentic', description: 'Natural language understanding' },
|
|
49
|
+
{ name: 'lex-agentic-learning', category: 'agentic', description: 'Online learning and adaptation' },
|
|
50
|
+
{ name: 'lex-agentic-memory', category: 'agentic', description: 'Long-term memory and recall' },
|
|
51
|
+
{ name: 'lex-agentic-self', category: 'agentic', description: 'Self-model and identity' },
|
|
52
|
+
{ name: 'lex-agentic-social', category: 'agentic', description: 'Social cognition and theory of mind' },
|
|
53
|
+
{ name: 'lex-adapter', category: 'agentic', description: 'Protocol and format adaptation' },
|
|
54
|
+
{ name: 'lex-apollo', category: 'agentic', description: 'Shared knowledge store client' },
|
|
55
|
+
{ name: 'lex-autofix', category: 'agentic', description: 'Autonomous code fix pipeline' },
|
|
56
|
+
{ name: 'lex-coldstart', category: 'agentic', description: 'Bootstrap knowledge ingestion' },
|
|
57
|
+
{ name: 'lex-cost-scanner', category: 'agentic', description: 'Cloud cost scanning and analysis' },
|
|
58
|
+
{ name: 'lex-dataset', category: 'agentic', description: 'Dataset management and versioning' },
|
|
59
|
+
{ name: 'lex-eval', category: 'agentic', description: 'LLM evaluation framework' },
|
|
60
|
+
{ name: 'lex-extinction', category: 'agentic', description: 'Worker lifecycle termination' },
|
|
61
|
+
{ name: 'lex-factory', category: 'agentic', description: 'Spec-to-code generation pipeline' },
|
|
62
|
+
{ name: 'lex-finops', category: 'agentic', description: 'FinOps cost optimization' },
|
|
63
|
+
{ name: 'lex-governance', category: 'agentic', description: 'Policy and compliance governance' },
|
|
64
|
+
{ name: 'lex-knowledge', category: 'agentic', description: 'Corpus ingestion and knowledge query' },
|
|
65
|
+
{ name: 'lex-mesh', category: 'agentic', description: 'Agent mesh and preference exchange' },
|
|
66
|
+
{ name: 'lex-mind-growth', category: 'agentic', description: 'Autonomous cognitive expansion' },
|
|
67
|
+
{ name: 'lex-onboard', category: 'agentic', description: 'New agent onboarding workflow' },
|
|
68
|
+
{ name: 'lex-pilot-infra-monitor', category: 'agentic', description: 'Infrastructure monitoring pilot' },
|
|
69
|
+
{ name: 'lex-pilot-knowledge-assist', category: 'agentic', description: 'Knowledge assist pilot worker' },
|
|
70
|
+
{ name: 'lex-privatecore', category: 'agentic', description: 'Private execution enclave' },
|
|
71
|
+
{ name: 'lex-prompt', category: 'agentic', description: 'Prompt management and versioning' },
|
|
72
|
+
{ name: 'lex-swarm', category: 'agentic', description: 'Multi-agent swarm orchestration' },
|
|
73
|
+
{ name: 'lex-swarm-github', category: 'agentic', description: 'GitHub code review swarm' },
|
|
74
|
+
{ name: 'lex-tick', category: 'agentic', description: 'Gaia tick cycle driver' },
|
|
75
|
+
# identity
|
|
76
|
+
{ name: 'lex-identity-approle', category: 'identity', description: 'Vault AppRole identity provider' },
|
|
77
|
+
{ name: 'lex-identity-aws', category: 'identity', description: 'AWS IAM identity provider' },
|
|
78
|
+
{ name: 'lex-identity-entra', category: 'identity', description: 'Microsoft Entra identity provider' },
|
|
79
|
+
{ name: 'lex-identity-github', category: 'identity', description: 'GitHub App identity provider' },
|
|
80
|
+
{ name: 'lex-identity-kerberos', category: 'identity', description: 'Kerberos identity provider' },
|
|
81
|
+
{ name: 'lex-identity-kubernetes', category: 'identity', description: 'Kubernetes service account identity provider' },
|
|
82
|
+
{ name: 'lex-identity-ldap', category: 'identity', description: 'LDAP identity provider' },
|
|
83
|
+
{ name: 'lex-identity-system', category: 'identity', description: 'System identity provider' },
|
|
84
|
+
# service integrations
|
|
85
|
+
{ name: 'lex-consul', category: 'service', description: 'HashiCorp Consul service mesh integration' },
|
|
86
|
+
{ name: 'lex-github', category: 'service', description: 'GitHub API integration' },
|
|
87
|
+
{ name: 'lex-http', category: 'service', description: 'Generic HTTP client runner' },
|
|
88
|
+
{ name: 'lex-kerberos', category: 'service', description: 'Kerberos authentication integration' },
|
|
89
|
+
{ name: 'lex-microsoft_teams', category: 'service', description: 'Microsoft Teams messaging integration' },
|
|
90
|
+
{ name: 'lex-nomad', category: 'service', description: 'HashiCorp Nomad job integration' },
|
|
91
|
+
{ name: 'lex-redis', category: 'service', description: 'Redis integration' },
|
|
92
|
+
{ name: 'lex-s3', category: 'service', description: 'AWS S3 object storage integration' },
|
|
93
|
+
{ name: 'lex-tfe', category: 'service', description: 'Terraform Enterprise integration' },
|
|
94
|
+
{ name: 'lex-uais', category: 'service', description: 'UHG AI Services integration' },
|
|
95
|
+
{ name: 'lex-vault', category: 'service', description: 'HashiCorp Vault secrets integration' },
|
|
96
|
+
# other integrations
|
|
97
|
+
{ name: 'lex-aha', category: 'other', description: 'Aha! roadmap integration' },
|
|
98
|
+
{ name: 'lex-chef', category: 'other', description: 'Chef infrastructure automation' },
|
|
99
|
+
{ name: 'lex-cloudflare', category: 'other', description: 'Cloudflare DNS and CDN integration' },
|
|
100
|
+
{ name: 'lex-discord', category: 'other', description: 'Discord messaging integration' },
|
|
101
|
+
{ name: 'lex-dns', category: 'other', description: 'DNS query and management' },
|
|
102
|
+
{ name: 'lex-docker', category: 'other', description: 'Docker container integration' },
|
|
103
|
+
{ name: 'lex-dynatrace', category: 'other', description: 'Dynatrace APM integration' },
|
|
104
|
+
{ name: 'lex-elastic_app_search', category: 'other', description: 'Elastic App Search integration' },
|
|
105
|
+
{ name: 'lex-elasticsearch', category: 'other', description: 'Elasticsearch integration' },
|
|
106
|
+
{ name: 'lex-gitlab', category: 'other', description: 'GitLab integration' },
|
|
107
|
+
{ name: 'lex-google-calendar', category: 'other', description: 'Google Calendar integration' },
|
|
108
|
+
{ name: 'lex-grafana', category: 'other', description: 'Grafana dashboard integration' },
|
|
109
|
+
{ name: 'lex-home-assistant', category: 'other', description: 'Home Assistant smart home integration' },
|
|
110
|
+
{ name: 'lex-influxdb', category: 'other', description: 'InfluxDB time series integration' },
|
|
111
|
+
{ name: 'lex-infoblox', category: 'other', description: 'Infoblox IPAM/DNS integration' },
|
|
112
|
+
{ name: 'lex-jenkins', category: 'other', description: 'Jenkins CI/CD integration' },
|
|
113
|
+
{ name: 'lex-jfrog', category: 'other', description: 'JFrog Artifactory integration' },
|
|
114
|
+
{ name: 'lex-jira', category: 'other', description: 'Jira issue tracking integration' },
|
|
115
|
+
{ name: 'lex-kafka', category: 'other', description: 'Apache Kafka messaging integration' },
|
|
116
|
+
{ name: 'lex-kubernetes', category: 'other', description: 'Kubernetes cluster integration' },
|
|
117
|
+
{ name: 'lex-lambda', category: 'other', description: 'AWS Lambda function integration' },
|
|
118
|
+
{ name: 'lex-memcached', category: 'other', description: 'Memcached cache integration' },
|
|
119
|
+
{ name: 'lex-mongodb', category: 'other', description: 'MongoDB integration' },
|
|
120
|
+
{ name: 'lex-mqtt', category: 'other', description: 'MQTT IoT messaging integration' },
|
|
121
|
+
{ name: 'lex-openweathermap', category: 'other', description: 'OpenWeatherMap weather integration' },
|
|
122
|
+
{ name: 'lex-pagerduty', category: 'other', description: 'PagerDuty alerting integration' },
|
|
123
|
+
{ name: 'lex-pihole', category: 'other', description: 'Pi-hole DNS filtering integration' },
|
|
124
|
+
{ name: 'lex-postgres', category: 'other', description: 'PostgreSQL database integration' },
|
|
125
|
+
{ name: 'lex-prometheus', category: 'other', description: 'Prometheus metrics integration' },
|
|
126
|
+
{ name: 'lex-pushbullet', category: 'other', description: 'Pushbullet notification integration' },
|
|
127
|
+
{ name: 'lex-pushover', category: 'other', description: 'Pushover notification integration' },
|
|
128
|
+
{ name: 'lex-sftp', category: 'other', description: 'SFTP file transfer integration' },
|
|
129
|
+
{ name: 'lex-slack', category: 'other', description: 'Slack messaging integration' },
|
|
130
|
+
{ name: 'lex-sleepiq', category: 'other', description: 'SleepIQ bed sensor integration' },
|
|
131
|
+
{ name: 'lex-smtp', category: 'other', description: 'SMTP email integration' },
|
|
132
|
+
{ name: 'lex-sonos', category: 'other', description: 'Sonos audio integration' },
|
|
133
|
+
{ name: 'lex-sqs', category: 'other', description: 'AWS SQS queue integration' },
|
|
134
|
+
{ name: 'lex-ssh', category: 'other', description: 'SSH remote execution integration' },
|
|
135
|
+
{ name: 'lex-telegram', category: 'other', description: 'Telegram messaging integration' },
|
|
136
|
+
{ name: 'lex-todoist', category: 'other', description: 'Todoist task management integration' },
|
|
137
|
+
{ name: 'lex-twilio', category: 'other', description: 'Twilio SMS/voice integration' },
|
|
138
|
+
{ name: 'lex-wled', category: 'other', description: 'WLED LED controller integration' }
|
|
139
|
+
].each(&:freeze).freeze
|
|
140
|
+
|
|
141
|
+
class << self
|
|
142
|
+
def all
|
|
143
|
+
EXTENSIONS.map(&:dup)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def by_category(category)
|
|
147
|
+
EXTENSIONS.select { |e| e[:category] == category }.map(&:dup)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def find(name)
|
|
151
|
+
entry = EXTENSIONS.find { |e| e[:name] == name }
|
|
152
|
+
entry&.dup
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
@@ -9,18 +9,25 @@ module Legion
|
|
|
9
9
|
|
|
10
10
|
class << self
|
|
11
11
|
def token_for(provider_name)
|
|
12
|
-
|
|
13
|
-
return nil unless renewer
|
|
14
|
-
|
|
15
|
-
lease = renewer.current_lease
|
|
12
|
+
lease = lease_for(provider_name)
|
|
16
13
|
lease&.valid? ? lease.token : nil
|
|
17
14
|
end
|
|
18
15
|
|
|
19
|
-
def
|
|
20
|
-
|
|
21
|
-
|
|
16
|
+
def lease_for(provider_name)
|
|
17
|
+
name = provider_name.to_sym
|
|
18
|
+
renewer = renewers[name]
|
|
19
|
+
return renewer.current_lease if renewer
|
|
22
20
|
|
|
23
|
-
|
|
21
|
+
static_ref = static_leases[name]
|
|
22
|
+
static_ref&.get
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def renewer_for(provider_name)
|
|
26
|
+
renewers[provider_name.to_sym]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def credentials_for(provider_name, service: nil)
|
|
30
|
+
lease = lease_for(provider_name)
|
|
24
31
|
return nil unless lease&.valid?
|
|
25
32
|
|
|
26
33
|
{ token: lease.token, provider: provider_name.to_sym, service: service, lease: lease }
|
|
@@ -28,12 +35,37 @@ module Legion
|
|
|
28
35
|
|
|
29
36
|
def register_provider(provider_name, provider:, lease:)
|
|
30
37
|
name = provider_name.to_sym
|
|
38
|
+
|
|
31
39
|
renewers[name]&.stop!
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
lease
|
|
36
|
-
|
|
40
|
+
if lease&.expires_at.nil? && !lease&.renewable
|
|
41
|
+
# Static credential — store without a background renewal thread
|
|
42
|
+
renewers.delete(name)
|
|
43
|
+
static_leases[name] = Concurrent::AtomicReference.new(lease)
|
|
44
|
+
providers_map[name] = provider
|
|
45
|
+
else
|
|
46
|
+
# Dynamic credential — create LeaseRenewer
|
|
47
|
+
static_leases.delete(name)
|
|
48
|
+
renewers[name] = LeaseRenewer.new(
|
|
49
|
+
provider_name: name,
|
|
50
|
+
provider: provider,
|
|
51
|
+
lease: lease
|
|
52
|
+
)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def refresh_credential(provider_name)
|
|
57
|
+
name = provider_name.to_sym
|
|
58
|
+
ref = static_leases[name]
|
|
59
|
+
return false unless ref
|
|
60
|
+
|
|
61
|
+
provider = providers_map[name]
|
|
62
|
+
return false unless provider.respond_to?(:provide_token)
|
|
63
|
+
|
|
64
|
+
new_lease = provider.provide_token
|
|
65
|
+
return false unless new_lease&.valid?
|
|
66
|
+
|
|
67
|
+
ref.set(new_lease)
|
|
68
|
+
true
|
|
37
69
|
end
|
|
38
70
|
|
|
39
71
|
def authenticated?
|
|
@@ -77,11 +109,13 @@ module Legion
|
|
|
77
109
|
end
|
|
78
110
|
|
|
79
111
|
def providers
|
|
80
|
-
renewers.keys
|
|
112
|
+
(renewers.keys + static_leases.keys).uniq
|
|
81
113
|
end
|
|
82
114
|
|
|
83
115
|
def leases
|
|
84
|
-
renewers.transform_values { |r| r.current_lease&.to_h }
|
|
116
|
+
dynamic = renewers.transform_values { |r| r.current_lease&.to_h }
|
|
117
|
+
static = static_leases.transform_values { |ref| ref.get&.to_h }
|
|
118
|
+
dynamic.merge(static)
|
|
85
119
|
end
|
|
86
120
|
|
|
87
121
|
def shutdown
|
|
@@ -91,6 +125,8 @@ module Legion
|
|
|
91
125
|
nil
|
|
92
126
|
end
|
|
93
127
|
renewers.clear
|
|
128
|
+
static_leases.clear
|
|
129
|
+
providers_map.clear
|
|
94
130
|
end
|
|
95
131
|
|
|
96
132
|
def reset!
|
|
@@ -105,6 +141,14 @@ module Legion
|
|
|
105
141
|
@renewers ||= Concurrent::Hash.new
|
|
106
142
|
end
|
|
107
143
|
|
|
144
|
+
def static_leases
|
|
145
|
+
@static_leases ||= Concurrent::Hash.new
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def providers_map
|
|
149
|
+
@providers_map ||= Concurrent::Hash.new
|
|
150
|
+
end
|
|
151
|
+
|
|
108
152
|
def fetch_groups
|
|
109
153
|
process_groups = Identity::Process.identity_hash[:groups]
|
|
110
154
|
return process_groups if process_groups && !process_groups.empty?
|
data/lib/legion/service.rb
CHANGED
|
@@ -1107,6 +1107,11 @@ module Legion
|
|
|
1107
1107
|
identity = future.value
|
|
1108
1108
|
Legion::Identity::Process.bind!(provider, identity)
|
|
1109
1109
|
log.info "[Identity] resolved via #{provider.class.name}: #{identity[:canonical_name]}"
|
|
1110
|
+
|
|
1111
|
+
# Phase 8: Register winning auth provider with Broker so extensions can
|
|
1112
|
+
# call Broker.token_for(:provider_name) without managing tokens themselves.
|
|
1113
|
+
register_provider_with_broker(provider)
|
|
1114
|
+
|
|
1110
1115
|
true
|
|
1111
1116
|
else
|
|
1112
1117
|
false
|
|
@@ -1119,6 +1124,19 @@ module Legion
|
|
|
1119
1124
|
pool&.kill unless pool&.wait_for_termination(2)
|
|
1120
1125
|
end
|
|
1121
1126
|
|
|
1127
|
+
def register_provider_with_broker(provider)
|
|
1128
|
+
return unless provider.respond_to?(:provide_token) && defined?(Legion::Identity::Broker)
|
|
1129
|
+
|
|
1130
|
+
lease = provider.provide_token
|
|
1131
|
+
return unless lease
|
|
1132
|
+
|
|
1133
|
+
provider_name = provider.respond_to?(:provider_name) ? provider.provider_name : provider.class.name.to_sym
|
|
1134
|
+
Legion::Identity::Broker.register_provider(provider_name, provider: provider, lease: lease)
|
|
1135
|
+
log.info "[Identity] registered provider #{provider_name} with Broker"
|
|
1136
|
+
rescue StandardError => e
|
|
1137
|
+
handle_exception(e, level: :warn, operation: 'service.register_provider_with_broker')
|
|
1138
|
+
end
|
|
1139
|
+
|
|
1122
1140
|
def find_identity_providers
|
|
1123
1141
|
return [] unless defined?(Legion::Extensions)
|
|
1124
1142
|
|
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.7.
|
|
4
|
+
version: 1.7.33
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Esity
|
|
@@ -818,6 +818,7 @@ files:
|
|
|
818
818
|
- lib/legion/extensions/builders/runners.rb
|
|
819
819
|
- lib/legion/extensions/capability.rb
|
|
820
820
|
- lib/legion/extensions/catalog.rb
|
|
821
|
+
- lib/legion/extensions/catalog/available.rb
|
|
821
822
|
- lib/legion/extensions/catalog/registry.rb
|
|
822
823
|
- lib/legion/extensions/core.rb
|
|
823
824
|
- lib/legion/extensions/data.rb
|