legionio 1.6.24 → 1.6.25
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 +14 -0
- data/lib/legion/auth/oauth_callback.rb +63 -0
- data/lib/legion/auth/token_manager.rb +74 -0
- data/lib/legion/cli/chat/session.rb +16 -0
- data/lib/legion/cli/connect_command.rb +64 -0
- data/lib/legion/cli.rb +6 -2
- data/lib/legion/extensions/absorbers/base.rb +26 -0
- data/lib/legion/extensions/absorbers/dispatch.rb +102 -0
- data/lib/legion/extensions/absorbers/matchers/file.rb +23 -0
- data/lib/legion/extensions/absorbers/transport.rb +70 -0
- data/lib/legion/version.rb +1 -1
- metadata +7 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8fcd30078f57fd92474d887714ed4ba5a49ed262c50dd9bcde5301e682299d21
|
|
4
|
+
data.tar.gz: 6bee493acba0acc9199fe9c53cb3f844b64498012e7123cf9b8a4cd46bbf0bbe
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2e96c7c7dc687503efb94dda27a1e57aad86b3f05cb6844ab12b603bc20b4c22a155755e2578ed4cca576b9bfd3b3c102b0fc2ce558d5a7ea283dc12f16e02f2
|
|
7
|
+
data.tar.gz: ef81be962e9a9228945babcab90e1222e1efa85b3a94b4ceff38d52f02048538ef4471cb03ca3d3fa2e52bb8b3bc391dce0773e57559fdd7f950940cc6ee157b
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [1.6.25] - 2026-03-28
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- `Legion::Extensions::Absorbers::Dispatch`: module-function dispatch pipeline — `dispatch(input, context:)`, depth limiting, cycle detection via ancestor_chain, `dispatch_children`, `extract_urls`, thread-safe `@dispatched` registry
|
|
9
|
+
- `Legion::Extensions::Absorbers::PatternMatcher`: URL/file pattern registry — `register(absorber_class)`, `resolve(input)`, priority-ordered matching, `reset!`
|
|
10
|
+
- `Legion::Extensions::Absorbers::Transport`: v3.0 AMQP topology — `publish_absorb_request`, `build_message`, `lex_name_from_absorber_class`, `absorber_name_from_class`; exchanges named `lex.{lex_name}`, routing keys `lex.{lex_name}.absorbers.{name}.absorb`
|
|
11
|
+
- `Legion::Extensions::Absorbers::Base`: updated with `TokenRevocationError`, `TokenUnavailableError`, and `with_token(provider:, &block)` helper for OAuth-gated absorbers
|
|
12
|
+
- `Legion::Extensions::Absorbers::Matchers::File`: file-path pattern matcher using `File.fnmatch`
|
|
13
|
+
- `Legion::Auth::OauthCallback`: ephemeral TCP server for OAuth redirect callback — `wait_for_callback`, `parse_callback`; per-port lifecycle
|
|
14
|
+
- `Legion::Auth::TokenManager`: `TokenExpiredError`, `mark_revoked!`, `revoked?` for token lifecycle and revocation detection
|
|
15
|
+
- `Legion::CLI::ConnectCommand`: `legion connect microsoft`, `legion connect github`, `legion connect status`, `legion connect disconnect` — browser OAuth flow entry points registered as `legion connect` subcommand
|
|
16
|
+
- Chat URL detection: `Session#check_for_absorbable_urls` auto-dispatches matched URLs after each user message
|
|
17
|
+
- `spec/integration/absorber_pipeline_spec.rb`: 12-example end-to-end integration spec covering PatternMatcher resolution, Dispatch routing, transport suppression in lite mode, absorber → Apollo.ingest pipeline, depth/cycle guards
|
|
18
|
+
|
|
5
19
|
## [1.6.24] - 2026-03-28
|
|
6
20
|
|
|
7
21
|
### Added
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'socket'
|
|
4
|
+
require 'timeout'
|
|
5
|
+
require 'uri'
|
|
6
|
+
|
|
7
|
+
module Legion
|
|
8
|
+
module Auth
|
|
9
|
+
class OauthCallback
|
|
10
|
+
DEFAULT_TIMEOUT = 120
|
|
11
|
+
LOCALHOST = '127.0.0.1'
|
|
12
|
+
|
|
13
|
+
attr_reader :port, :redirect_uri
|
|
14
|
+
|
|
15
|
+
def initialize(timeout: DEFAULT_TIMEOUT)
|
|
16
|
+
@timeout = timeout
|
|
17
|
+
@server = TCPServer.new(LOCALHOST, 0)
|
|
18
|
+
@port = @server.addr[1]
|
|
19
|
+
@redirect_uri = "http://#{LOCALHOST}:#{@port}/callback"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def wait_for_callback
|
|
23
|
+
Timeout.timeout(@timeout) do
|
|
24
|
+
client = @server.accept
|
|
25
|
+
request_line = client.gets
|
|
26
|
+
parse_callback(request_line, client)
|
|
27
|
+
end
|
|
28
|
+
ensure
|
|
29
|
+
@server.close rescue nil # rubocop:disable Style/RescueModifier
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def close
|
|
33
|
+
@server.close rescue nil # rubocop:disable Style/RescueModifier
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def parse_callback(request_line, client)
|
|
39
|
+
send_response(client)
|
|
40
|
+
return {} unless request_line&.start_with?('GET')
|
|
41
|
+
|
|
42
|
+
path = request_line.split[1] || ''
|
|
43
|
+
query_string = path.split('?', 2)[1] || ''
|
|
44
|
+
params = URI.decode_www_form(query_string).to_h
|
|
45
|
+
params.transform_keys(&:to_sym)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def send_response(client)
|
|
49
|
+
body = '<html><body><h1>Authorization complete.</h1><p>You may close this window.</p></body></html>'
|
|
50
|
+
client.puts 'HTTP/1.1 200 OK'
|
|
51
|
+
client.puts 'Content-Type: text/html'
|
|
52
|
+
client.puts "Content-Length: #{body.bytesize}"
|
|
53
|
+
client.puts 'Connection: close'
|
|
54
|
+
client.puts
|
|
55
|
+
client.puts body
|
|
56
|
+
rescue Errno::ECONNRESET, Errno::EPIPE, IOError
|
|
57
|
+
nil
|
|
58
|
+
ensure
|
|
59
|
+
client.close rescue nil # rubocop:disable Style/RescueModifier
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'time'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Auth
|
|
7
|
+
class TokenManager
|
|
8
|
+
class TokenExpiredError < StandardError
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def initialize(provider:)
|
|
12
|
+
@provider = provider
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def token_valid?
|
|
16
|
+
access_token = secret[:"#{@provider}_access_token"]
|
|
17
|
+
return false unless access_token
|
|
18
|
+
|
|
19
|
+
expires_at_str = secret[:"#{@provider}_token_expires_at"]
|
|
20
|
+
return false unless expires_at_str
|
|
21
|
+
|
|
22
|
+
expires_at = Time.parse(expires_at_str)
|
|
23
|
+
ttl = secret[:"#{@provider}_token_ttl"]
|
|
24
|
+
|
|
25
|
+
expires_at > if ttl
|
|
26
|
+
Time.now + (ttl * 0.25)
|
|
27
|
+
else
|
|
28
|
+
Time.now + 300
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def store_tokens(access_token:, expires_in:, refresh_token: nil, scope: nil)
|
|
33
|
+
secret[:"#{@provider}_access_token"] = access_token
|
|
34
|
+
secret[:"#{@provider}_refresh_token"] = refresh_token if refresh_token
|
|
35
|
+
secret[:"#{@provider}_token_ttl"] = expires_in
|
|
36
|
+
secret[:"#{@provider}_token_scope"] = scope if scope
|
|
37
|
+
secret[:"#{@provider}_token_expires_at"] = (Time.now + expires_in).iso8601
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def ensure_valid_token
|
|
41
|
+
return secret[:"#{@provider}_access_token"] if token_valid?
|
|
42
|
+
|
|
43
|
+
refresh_access_token
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def revoked?
|
|
47
|
+
secret[:"#{@provider}_token_revoked"] == true
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def mark_revoked!
|
|
51
|
+
secret[:"#{@provider}_token_revoked"] = true
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def secret
|
|
57
|
+
@secret ||= begin
|
|
58
|
+
if defined?(Legion::Extensions::Helpers::SecretAccessor)
|
|
59
|
+
Legion::Extensions::Helpers::SecretAccessor.new(lex_name: 'auth')
|
|
60
|
+
else
|
|
61
|
+
{}
|
|
62
|
+
end
|
|
63
|
+
rescue StandardError
|
|
64
|
+
{}
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def refresh_access_token
|
|
69
|
+
# Will be implemented when OAuth2 callback server is wired in Task 2.2
|
|
70
|
+
nil
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -39,6 +39,7 @@ module Legion
|
|
|
39
39
|
|
|
40
40
|
def send_message(message, on_tool_call: nil, on_tool_result: nil, &block)
|
|
41
41
|
check_budget!
|
|
42
|
+
check_for_absorbable_urls(message)
|
|
42
43
|
|
|
43
44
|
@stats[:messages_sent] += 1
|
|
44
45
|
@turn += 1
|
|
@@ -102,6 +103,21 @@ module Legion
|
|
|
102
103
|
format('Budget exceeded: $%<cost>.4f spent of $%<limit>.2f limit',
|
|
103
104
|
cost: cost, limit: @budget_usd)
|
|
104
105
|
end
|
|
106
|
+
|
|
107
|
+
def check_for_absorbable_urls(text)
|
|
108
|
+
return unless defined?(Legion::Extensions::Absorbers::Dispatch)
|
|
109
|
+
return unless defined?(Legion::Extensions::Absorbers::PatternMatcher)
|
|
110
|
+
|
|
111
|
+
urls = Legion::Extensions::Absorbers::Dispatch.extract_urls(text.to_s)
|
|
112
|
+
return if urls.empty?
|
|
113
|
+
|
|
114
|
+
urls.each do |url|
|
|
115
|
+
absorber = Legion::Extensions::Absorbers::PatternMatcher.resolve(url)
|
|
116
|
+
next unless absorber
|
|
117
|
+
|
|
118
|
+
Legion::Extensions::Absorbers::Dispatch.dispatch(url, context: { conversation_id: object_id.to_s })
|
|
119
|
+
end
|
|
120
|
+
end
|
|
105
121
|
end
|
|
106
122
|
end
|
|
107
123
|
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'thor'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module CLI
|
|
7
|
+
class ConnectCommand < Thor
|
|
8
|
+
namespace :connect
|
|
9
|
+
|
|
10
|
+
PROVIDERS = %w[microsoft github google].freeze
|
|
11
|
+
|
|
12
|
+
desc 'microsoft', 'Connect a Microsoft account (OAuth2 delegated auth)'
|
|
13
|
+
method_option :tenant_id, type: :string, desc: 'Azure tenant ID'
|
|
14
|
+
method_option :client_id, type: :string, desc: 'Application client ID'
|
|
15
|
+
method_option :scope, type: :string, default: 'Calendars.Read OnlineMeetings.Read',
|
|
16
|
+
desc: 'OAuth2 scopes (space-separated)'
|
|
17
|
+
method_option :no_browser, type: :boolean, default: false, desc: 'Print URL instead of launching browser'
|
|
18
|
+
def microsoft
|
|
19
|
+
require 'legion/auth/token_manager'
|
|
20
|
+
manager = Legion::Auth::TokenManager.new(provider: :microsoft)
|
|
21
|
+
|
|
22
|
+
if manager.token_valid?
|
|
23
|
+
say 'Already connected to Microsoft. Use --force to reconnect.', :green
|
|
24
|
+
return
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
say 'Connecting to Microsoft...', :blue
|
|
28
|
+
say 'OAuth2 browser flow not yet implemented. Use `legion auth teams` for Teams-specific auth.', :yellow
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
desc 'github', 'Connect a GitHub account (OAuth2 device flow)'
|
|
32
|
+
method_option :client_id, type: :string, desc: 'GitHub OAuth App client ID'
|
|
33
|
+
def github
|
|
34
|
+
say 'GitHub connection not yet implemented.', :yellow
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
desc 'status', 'Show connection status for all providers'
|
|
38
|
+
def status
|
|
39
|
+
require 'legion/auth/token_manager'
|
|
40
|
+
|
|
41
|
+
PROVIDERS.each do |provider|
|
|
42
|
+
manager = Legion::Auth::TokenManager.new(provider: provider.to_sym)
|
|
43
|
+
if manager.token_valid?
|
|
44
|
+
say " #{provider}: connected", :green
|
|
45
|
+
elsif manager.revoked?
|
|
46
|
+
say " #{provider}: revoked", :red
|
|
47
|
+
else
|
|
48
|
+
say " #{provider}: not connected", :yellow
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
desc 'disconnect PROVIDER', 'Disconnect a provider account'
|
|
54
|
+
def disconnect(provider)
|
|
55
|
+
unless PROVIDERS.include?(provider)
|
|
56
|
+
say "Unknown provider: #{provider}. Valid: #{PROVIDERS.join(', ')}", :red
|
|
57
|
+
return
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
say "Disconnected #{provider} account.", :green
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
data/lib/legion/cli.rb
CHANGED
|
@@ -60,8 +60,9 @@ module Legion
|
|
|
60
60
|
autoload :Interactive, 'legion/cli/interactive'
|
|
61
61
|
autoload :Docs, 'legion/cli/docs_command'
|
|
62
62
|
autoload :Failover, 'legion/cli/failover_command'
|
|
63
|
-
autoload :AbsorbCommand,
|
|
64
|
-
autoload :
|
|
63
|
+
autoload :AbsorbCommand, 'legion/cli/absorb_command'
|
|
64
|
+
autoload :ConnectCommand, 'legion/cli/connect_command'
|
|
65
|
+
autoload :Apollo, 'legion/cli/apollo_command'
|
|
65
66
|
autoload :TraceCommand, 'legion/cli/trace_command'
|
|
66
67
|
autoload :Features, 'legion/cli/features_command'
|
|
67
68
|
autoload :Debug, 'legion/cli/debug_command'
|
|
@@ -280,6 +281,9 @@ module Legion
|
|
|
280
281
|
desc 'absorb SUBCOMMAND', 'Absorb content from external sources'
|
|
281
282
|
subcommand 'absorb', AbsorbCommand
|
|
282
283
|
|
|
284
|
+
desc 'connect PROVIDER', 'Connect external accounts via OAuth2'
|
|
285
|
+
subcommand 'connect', ConnectCommand
|
|
286
|
+
|
|
283
287
|
desc 'broker SUBCOMMAND', 'RabbitMQ broker management (stats, cleanup)'
|
|
284
288
|
subcommand 'broker', Legion::CLI::Broker
|
|
285
289
|
|
|
@@ -8,6 +8,12 @@ module Legion
|
|
|
8
8
|
class Base
|
|
9
9
|
extend Legion::Extensions::Definitions
|
|
10
10
|
|
|
11
|
+
class TokenRevocationError < StandardError
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
class TokenUnavailableError < StandardError
|
|
15
|
+
end
|
|
16
|
+
|
|
11
17
|
attr_accessor :job_id, :runners
|
|
12
18
|
|
|
13
19
|
class << self
|
|
@@ -67,8 +73,28 @@ module Legion
|
|
|
67
73
|
Legion::Logging.info("absorb[#{job_id}] #{"#{percent}% " if percent}#{message}")
|
|
68
74
|
end
|
|
69
75
|
|
|
76
|
+
def with_token(provider:)
|
|
77
|
+
raise TokenUnavailableError, "#{provider} token not available" unless token_manager_for(provider).token_valid?
|
|
78
|
+
raise TokenRevocationError, "#{provider} token has been revoked" if token_manager_for(provider).revoked?
|
|
79
|
+
|
|
80
|
+
token = token_manager_for(provider).ensure_valid_token
|
|
81
|
+
raise TokenUnavailableError, "#{provider} token refresh failed" unless token
|
|
82
|
+
|
|
83
|
+
yield token
|
|
84
|
+
rescue Legion::Auth::TokenManager::TokenExpiredError => e
|
|
85
|
+
raise TokenUnavailableError, e.message
|
|
86
|
+
end
|
|
87
|
+
|
|
70
88
|
private
|
|
71
89
|
|
|
90
|
+
def token_manager_for(provider)
|
|
91
|
+
@token_managers ||= {}
|
|
92
|
+
@token_managers[provider] ||= begin
|
|
93
|
+
require 'legion/auth/token_manager'
|
|
94
|
+
Legion::Auth::TokenManager.new(provider: provider)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
72
98
|
def chunker_available?
|
|
73
99
|
defined?(Legion::Extensions::Knowledge::Helpers::Chunker)
|
|
74
100
|
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
require 'uri'
|
|
5
|
+
require_relative 'pattern_matcher'
|
|
6
|
+
|
|
7
|
+
module Legion
|
|
8
|
+
module Extensions
|
|
9
|
+
module Absorbers
|
|
10
|
+
module Dispatch
|
|
11
|
+
@dispatched = []
|
|
12
|
+
@mutex = Mutex.new
|
|
13
|
+
|
|
14
|
+
module_function
|
|
15
|
+
|
|
16
|
+
def dispatch(input, context: {})
|
|
17
|
+
context = default_context.merge(context)
|
|
18
|
+
|
|
19
|
+
return { status: :depth_exceeded, input: input } if context[:depth] >= context[:max_depth]
|
|
20
|
+
|
|
21
|
+
source_key = normalize_source_key(input)
|
|
22
|
+
return { status: :cycle_detected, input: input } if context[:ancestor_chain]&.any? { |a| a.include?(source_key) }
|
|
23
|
+
|
|
24
|
+
absorber_class = PatternMatcher.resolve(input)
|
|
25
|
+
return nil unless absorber_class
|
|
26
|
+
|
|
27
|
+
absorb_id = "absorb:#{SecureRandom.uuid}"
|
|
28
|
+
|
|
29
|
+
record = {
|
|
30
|
+
absorb_id: absorb_id,
|
|
31
|
+
input: input,
|
|
32
|
+
absorber_class: absorber_class.name,
|
|
33
|
+
context: context.merge(
|
|
34
|
+
ancestor_chain: (context[:ancestor_chain] || []) + [absorb_id]
|
|
35
|
+
),
|
|
36
|
+
status: :dispatched,
|
|
37
|
+
dispatched_at: Time.now.utc.iso8601
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
publish_to_transport(absorber_class, input, record) if transport_available?
|
|
41
|
+
|
|
42
|
+
@mutex.synchronize { @dispatched << record }
|
|
43
|
+
record
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def dispatch_children(children, parent_context:)
|
|
47
|
+
children.map do |child|
|
|
48
|
+
child_context = parent_context.merge(
|
|
49
|
+
depth: parent_context[:depth] + 1,
|
|
50
|
+
parent_absorb_id: parent_context[:absorb_id]
|
|
51
|
+
)
|
|
52
|
+
dispatch(child[:url] || child[:file_path], context: child_context)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def dispatched
|
|
57
|
+
@mutex.synchronize { @dispatched.dup }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def reset_dispatched!
|
|
61
|
+
@mutex.synchronize { @dispatched.clear }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def default_context
|
|
65
|
+
{
|
|
66
|
+
depth: 0,
|
|
67
|
+
max_depth: max_depth_setting,
|
|
68
|
+
ancestor_chain: [],
|
|
69
|
+
conversation_id: nil,
|
|
70
|
+
requested_by: nil,
|
|
71
|
+
parent_absorb_id: nil
|
|
72
|
+
}
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def max_depth_setting
|
|
76
|
+
return 5 unless defined?(Legion::Settings)
|
|
77
|
+
|
|
78
|
+
Legion::Settings[:absorbers]&.dig(:max_depth) || 5
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def normalize_source_key(input)
|
|
82
|
+
input.to_s.gsub(%r{^https?://}, '').gsub(/[?#].*/, '')
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def transport_available?
|
|
86
|
+
defined?(Legion::Transport) &&
|
|
87
|
+
Legion::Transport.respond_to?(:connected?) &&
|
|
88
|
+
Legion::Transport.connected?
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def publish_to_transport(absorber_class, _input, record)
|
|
92
|
+
require_relative 'transport'
|
|
93
|
+
Transport.publish_absorb_request(absorber_class: absorber_class, record: record)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def extract_urls(text)
|
|
97
|
+
URI.extract(text, %w[http https]).uniq
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Extensions
|
|
7
|
+
module Absorbers
|
|
8
|
+
module Matchers
|
|
9
|
+
class File < Base
|
|
10
|
+
def self.match?(pattern, input)
|
|
11
|
+
return false unless input.is_a?(::String)
|
|
12
|
+
|
|
13
|
+
::File.fnmatch(pattern, input, ::File::FNM_PATHNAME | ::File::FNM_DOTMATCH)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.type
|
|
17
|
+
:file
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Extensions
|
|
7
|
+
module Absorbers
|
|
8
|
+
module Transport
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
def publish_absorb_request(absorber_class:, record:)
|
|
12
|
+
lex = lex_name_from_absorber_class(absorber_class)
|
|
13
|
+
name = absorber_name_from_class(absorber_class)
|
|
14
|
+
msg = build_message(lex_name: lex, absorber_name: name, record: record)
|
|
15
|
+
return msg unless transport_connected?
|
|
16
|
+
|
|
17
|
+
exchange = Legion::Transport::Exchange.new(msg[:exchange], type: :topic, durable: true)
|
|
18
|
+
exchange.publish(
|
|
19
|
+
Legion::JSON.dump(msg[:payload]),
|
|
20
|
+
routing_key: msg[:routing_key],
|
|
21
|
+
content_type: 'application/json',
|
|
22
|
+
message_id: record[:absorb_id]
|
|
23
|
+
)
|
|
24
|
+
msg
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def build_message(lex_name:, absorber_name:, record:)
|
|
28
|
+
input = record[:input].to_s
|
|
29
|
+
{
|
|
30
|
+
exchange: "lex.#{lex_name}",
|
|
31
|
+
routing_key: "lex.#{lex_name}.absorbers.#{absorber_name}.absorb",
|
|
32
|
+
payload: {
|
|
33
|
+
type: 'absorb.request',
|
|
34
|
+
version: '1.0',
|
|
35
|
+
id: SecureRandom.uuid,
|
|
36
|
+
absorb_id: record[:absorb_id],
|
|
37
|
+
timestamp: Time.now.utc.iso8601,
|
|
38
|
+
url: input.start_with?('http') ? input : nil,
|
|
39
|
+
file_path: input.start_with?('http') ? nil : input,
|
|
40
|
+
context: record[:context],
|
|
41
|
+
metadata: record[:metadata] || {}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def lex_name_from_absorber_class(klass)
|
|
47
|
+
name = klass.name.to_s
|
|
48
|
+
# Legion::Extensions::MicrosoftTeams::Absorbers::Meeting -> microsoft_teams
|
|
49
|
+
# Lex::Example::Absorbers::Content -> example
|
|
50
|
+
m = name.match(/Legion::Extensions::(\w+)::Absorbers::/) ||
|
|
51
|
+
name.match(/Lex::(\w+)::Absorbers::/)
|
|
52
|
+
return 'unknown' unless m
|
|
53
|
+
|
|
54
|
+
m[1].gsub(/([A-Z])/, '_\1').sub(/^_/, '').downcase
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def absorber_name_from_class(klass)
|
|
58
|
+
klass.name.to_s.split('::').last
|
|
59
|
+
.gsub(/([A-Z])/, '_\1').sub(/^_/, '').downcase
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def transport_connected?
|
|
63
|
+
defined?(Legion::Transport) &&
|
|
64
|
+
Legion::Transport.respond_to?(:connected?) &&
|
|
65
|
+
Legion::Transport.connected?
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
data/lib/legion/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: legionio
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.6.
|
|
4
|
+
version: 1.6.25
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Esity
|
|
@@ -507,6 +507,8 @@ files:
|
|
|
507
507
|
- lib/legion/audit/cold_storage.rb
|
|
508
508
|
- lib/legion/audit/hash_chain.rb
|
|
509
509
|
- lib/legion/audit/siem_export.rb
|
|
510
|
+
- lib/legion/auth/oauth_callback.rb
|
|
511
|
+
- lib/legion/auth/token_manager.rb
|
|
510
512
|
- lib/legion/capacity/model.rb
|
|
511
513
|
- lib/legion/catalog.rb
|
|
512
514
|
- lib/legion/chat/notification_bridge.rb
|
|
@@ -595,6 +597,7 @@ files:
|
|
|
595
597
|
- lib/legion/cli/config_command.rb
|
|
596
598
|
- lib/legion/cli/config_import.rb
|
|
597
599
|
- lib/legion/cli/config_scaffold.rb
|
|
600
|
+
- lib/legion/cli/connect_command.rb
|
|
598
601
|
- lib/legion/cli/connection.rb
|
|
599
602
|
- lib/legion/cli/cost/data_client.rb
|
|
600
603
|
- lib/legion/cli/cost_command.rb
|
|
@@ -749,9 +752,12 @@ files:
|
|
|
749
752
|
- lib/legion/extensions.rb
|
|
750
753
|
- lib/legion/extensions/absorbers.rb
|
|
751
754
|
- lib/legion/extensions/absorbers/base.rb
|
|
755
|
+
- lib/legion/extensions/absorbers/dispatch.rb
|
|
752
756
|
- lib/legion/extensions/absorbers/matchers/base.rb
|
|
757
|
+
- lib/legion/extensions/absorbers/matchers/file.rb
|
|
753
758
|
- lib/legion/extensions/absorbers/matchers/url.rb
|
|
754
759
|
- lib/legion/extensions/absorbers/pattern_matcher.rb
|
|
760
|
+
- lib/legion/extensions/absorbers/transport.rb
|
|
755
761
|
- lib/legion/extensions/actors/absorber_dispatch.rb
|
|
756
762
|
- lib/legion/extensions/actors/base.rb
|
|
757
763
|
- lib/legion/extensions/actors/defaults.rb
|