lex-mesh 0.1.1 → 0.2.4
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/Gemfile +2 -2
- data/lex-mesh.gemspec +3 -1
- data/lib/legion/extensions/mesh/actors/pending_expiry.rb +37 -0
- data/lib/legion/extensions/mesh/actors/preference_listener.rb +44 -0
- data/lib/legion/extensions/mesh/actors/silence_watchdog.rb +37 -0
- data/lib/legion/extensions/mesh/client.rb +7 -2
- data/lib/legion/extensions/mesh/helpers/delegation.rb +126 -0
- data/lib/legion/extensions/mesh/helpers/peer_verify.rb +79 -0
- data/lib/legion/extensions/mesh/helpers/pending_requests.rb +57 -0
- data/lib/legion/extensions/mesh/helpers/registry.rb +12 -0
- data/lib/legion/extensions/mesh/runners/delegation.rb +87 -0
- data/lib/legion/extensions/mesh/runners/mesh.rb +20 -0
- data/lib/legion/extensions/mesh/runners/preferences.rb +120 -0
- data/lib/legion/extensions/mesh/transport/messages/mesh_departure.rb +34 -0
- data/lib/legion/extensions/mesh/transport/messages/preference_query.rb +34 -0
- data/lib/legion/extensions/mesh/transport/messages/preference_response.rb +34 -0
- data/lib/legion/extensions/mesh/transport/queues/preference.rb +27 -0
- data/lib/legion/extensions/mesh/version.rb +1 -1
- data/lib/legion/extensions/mesh.rb +12 -0
- data/spec/legion/extensions/mesh/actors/pending_expiry_spec.rb +57 -0
- data/spec/legion/extensions/mesh/actors/preference_listener_spec.rb +87 -0
- data/spec/legion/extensions/mesh/actors/silence_watchdog_spec.rb +55 -0
- data/spec/legion/extensions/mesh/client_spec.rb +40 -1
- data/spec/legion/extensions/mesh/helpers/delegation_spec.rb +125 -0
- data/spec/legion/extensions/mesh/helpers/peer_verify_spec.rb +76 -0
- data/spec/legion/extensions/mesh/helpers/pending_requests_spec.rb +80 -0
- data/spec/legion/extensions/mesh/helpers/registry_spec.rb +41 -0
- data/spec/legion/extensions/mesh/runners/delegation_spec.rb +84 -0
- data/spec/legion/extensions/mesh/runners/mesh_spec.rb +25 -0
- data/spec/legion/extensions/mesh/runners/preferences_spec.rb +164 -0
- data/spec/legion/extensions/mesh/transport/messages/mesh_departure_spec.rb +81 -0
- data/spec/legion/extensions/mesh/transport/messages/preference_query_spec.rb +85 -0
- data/spec/legion/extensions/mesh/transport/messages/preference_response_spec.rb +85 -0
- data/spec/legion/extensions/mesh/transport/queues/preference_spec.rb +61 -0
- metadata +41 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 25cafa977ed6aaf671c903e71895312253a412c32c141e8de9674b1a86747bff
|
|
4
|
+
data.tar.gz: c7bbdde6f4c065f0fdaea58a5a5124bd53e49f0c48e0d275de0828bd93ec2439
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 76357a6ca4c31089fd7f5317097f935b07df6349476b344bd2681dccfbd796d79b1061922b22d42ca45b92d045bdbf901ed8d6c502f525cff3a668099dfea56c
|
|
7
|
+
data.tar.gz: 9e17a698c0b941fd2000ab9affffa87cd3bad983b37acacce794ee0fb4d975a8cdc04eee44480f976856e712fb6af8dfa80f976d07c2fd94ea302ba61f31b389
|
data/Gemfile
CHANGED
data/lex-mesh.gemspec
CHANGED
|
@@ -25,5 +25,7 @@ Gem::Specification.new do |spec|
|
|
|
25
25
|
Dir.glob('{lib,spec}/**/*') + %w[lex-mesh.gemspec Gemfile]
|
|
26
26
|
end
|
|
27
27
|
spec.require_paths = ['lib']
|
|
28
|
-
|
|
28
|
+
|
|
29
|
+
spec.add_dependency 'base64'
|
|
30
|
+
spec.add_dependency 'ed25519', '~> 1.3'
|
|
29
31
|
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/actors/every'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Extensions
|
|
7
|
+
module Mesh
|
|
8
|
+
module Actor
|
|
9
|
+
class PendingExpiry < Legion::Extensions::Actors::Every
|
|
10
|
+
def runner_class
|
|
11
|
+
Legion::Extensions::Mesh::Runners::Preferences
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def runner_function
|
|
15
|
+
'expire_pending_requests'
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def time
|
|
19
|
+
30
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def use_runner?
|
|
23
|
+
false
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def check_subtask?
|
|
27
|
+
false
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def generate_task?
|
|
31
|
+
false
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/actors/subscription'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Extensions
|
|
7
|
+
module Mesh
|
|
8
|
+
module Actor
|
|
9
|
+
class PreferenceListener < Legion::Extensions::Actors::Subscription
|
|
10
|
+
def runner_class
|
|
11
|
+
Legion::Extensions::Mesh::Runners::Preferences
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def runner_function
|
|
15
|
+
'dispatch_preference_message'
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def check_subtask?
|
|
19
|
+
false
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def generate_task?
|
|
23
|
+
false
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def use_runner?
|
|
27
|
+
false
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def queue
|
|
31
|
+
Legion::Extensions::Mesh::Transport::Queues::Preference
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def enabled?
|
|
35
|
+
defined?(Legion::Extensions::Mesh::Runners::Preferences) &&
|
|
36
|
+
defined?(Legion::Transport)
|
|
37
|
+
rescue StandardError
|
|
38
|
+
false
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/actors/every'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Extensions
|
|
7
|
+
module Mesh
|
|
8
|
+
module Actor
|
|
9
|
+
class SilenceWatchdog < Legion::Extensions::Actors::Every
|
|
10
|
+
def runner_class
|
|
11
|
+
Legion::Extensions::Mesh::Runners::Mesh
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def runner_function
|
|
15
|
+
'expire_silent_agents'
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def time
|
|
19
|
+
15
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def use_runner?
|
|
23
|
+
false
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def check_subtask?
|
|
27
|
+
false
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def generate_task?
|
|
31
|
+
false
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'legion/extensions/mesh/runners/mesh'
|
|
4
|
+
require 'legion/extensions/mesh/runners/preferences'
|
|
5
|
+
require 'legion/extensions/mesh/helpers/preference_profile'
|
|
6
|
+
require 'legion/extensions/mesh/helpers/pending_requests'
|
|
3
7
|
require 'legion/extensions/mesh/helpers/topology'
|
|
4
8
|
require 'legion/extensions/mesh/helpers/registry'
|
|
5
|
-
require 'legion/extensions/mesh/runners/mesh'
|
|
6
9
|
|
|
7
10
|
module Legion
|
|
8
11
|
module Extensions
|
|
9
12
|
module Mesh
|
|
10
13
|
class Client
|
|
11
14
|
include Runners::Mesh
|
|
15
|
+
include Runners::Preferences
|
|
12
16
|
|
|
13
|
-
def initialize(**)
|
|
17
|
+
def initialize(**opts)
|
|
18
|
+
@opts = opts
|
|
14
19
|
@mesh_registry = Helpers::Registry.new
|
|
15
20
|
end
|
|
16
21
|
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Extensions
|
|
7
|
+
module Mesh
|
|
8
|
+
module Helpers
|
|
9
|
+
class Delegation
|
|
10
|
+
CONSENT_LEVELS = %i[read execute admin].freeze
|
|
11
|
+
|
|
12
|
+
attr_reader :delegations
|
|
13
|
+
|
|
14
|
+
def initialize(max_depth: 3, max_active_per_agent: 10)
|
|
15
|
+
@delegations = {}
|
|
16
|
+
@agent_delegations = Hash.new { |h, k| h[k] = [] }
|
|
17
|
+
@max_depth = max_depth
|
|
18
|
+
@max_active_per_agent = max_active_per_agent
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def create(from:, to:, task_context:, consent_level:, parent_delegation_id: nil)
|
|
22
|
+
depth = compute_depth(parent_delegation_id)
|
|
23
|
+
return { error: :max_depth_exceeded } if depth >= @max_depth
|
|
24
|
+
|
|
25
|
+
if parent_delegation_id
|
|
26
|
+
parent = find(parent_delegation_id)
|
|
27
|
+
return { error: :consent_escalation } if parent && CONSENT_LEVELS.index(consent_level) > CONSENT_LEVELS.index(parent[:consent_level])
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
active_count = (@agent_delegations[from] || []).count do |id|
|
|
31
|
+
@delegations[id]&.fetch(:status, nil) == :active
|
|
32
|
+
end
|
|
33
|
+
return { error: :max_active_exceeded } if active_count >= @max_active_per_agent
|
|
34
|
+
|
|
35
|
+
id = "del-#{SecureRandom.uuid}"
|
|
36
|
+
record = {
|
|
37
|
+
delegation_id: id,
|
|
38
|
+
from_agent_id: from,
|
|
39
|
+
to_agent_id: to,
|
|
40
|
+
task_context: task_context,
|
|
41
|
+
consent_level: consent_level,
|
|
42
|
+
parent_delegation_id: parent_delegation_id,
|
|
43
|
+
depth: depth,
|
|
44
|
+
status: :active,
|
|
45
|
+
created_at: Time.now.utc,
|
|
46
|
+
completed_at: nil
|
|
47
|
+
}
|
|
48
|
+
@delegations[id] = record
|
|
49
|
+
@agent_delegations[from] << id
|
|
50
|
+
@agent_delegations[to] << id
|
|
51
|
+
record
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def complete(delegation_id)
|
|
55
|
+
record = @delegations[delegation_id]
|
|
56
|
+
return nil unless record && record[:status] == :active
|
|
57
|
+
|
|
58
|
+
record[:status] = :completed
|
|
59
|
+
record[:completed_at] = Time.now.utc
|
|
60
|
+
record
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def revoke(delegation_id)
|
|
64
|
+
record = @delegations[delegation_id]
|
|
65
|
+
return nil unless record && record[:status] == :active
|
|
66
|
+
|
|
67
|
+
record[:status] = :revoked
|
|
68
|
+
record[:completed_at] = Time.now.utc
|
|
69
|
+
cascade_revoke(delegation_id)
|
|
70
|
+
record
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def chain(delegation_id)
|
|
74
|
+
result = []
|
|
75
|
+
current = @delegations[delegation_id]
|
|
76
|
+
while current
|
|
77
|
+
result.unshift(current)
|
|
78
|
+
current = current[:parent_delegation_id] ? @delegations[current[:parent_delegation_id]] : nil
|
|
79
|
+
end
|
|
80
|
+
result
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def for_agent(agent_id, status: nil)
|
|
84
|
+
ids = @agent_delegations[agent_id] || []
|
|
85
|
+
results = ids.filter_map { |id| @delegations[id] }
|
|
86
|
+
results = results.select { |d| d[:status] == status } if status
|
|
87
|
+
results
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def find(delegation_id)
|
|
91
|
+
@delegations[delegation_id]
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def stats
|
|
95
|
+
active = @delegations.values.count { |d| d[:status] == :active }
|
|
96
|
+
depths = @delegations.values.map { |d| d[:depth] }
|
|
97
|
+
{
|
|
98
|
+
total: @delegations.size,
|
|
99
|
+
active: active,
|
|
100
|
+
avg_depth: depths.empty? ? 0 : depths.sum.to_f / depths.size,
|
|
101
|
+
max_depth: depths.max || 0
|
|
102
|
+
}
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
private
|
|
106
|
+
|
|
107
|
+
def compute_depth(parent_delegation_id)
|
|
108
|
+
return 0 unless parent_delegation_id
|
|
109
|
+
|
|
110
|
+
parent = find(parent_delegation_id)
|
|
111
|
+
(parent&.fetch(:depth, 0) || 0) + 1
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def cascade_revoke(parent_id)
|
|
115
|
+
@delegations.each_value do |d|
|
|
116
|
+
next unless d[:parent_delegation_id] == parent_id && d[:status] == :active
|
|
117
|
+
|
|
118
|
+
d[:status] = :revoked
|
|
119
|
+
d[:completed_at] = Time.now.utc
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'base64'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Extensions
|
|
7
|
+
module Mesh
|
|
8
|
+
module Helpers
|
|
9
|
+
module PeerVerify
|
|
10
|
+
class << self
|
|
11
|
+
def sign_message(payload, private_key_b64)
|
|
12
|
+
require 'ed25519'
|
|
13
|
+
key = Ed25519::SigningKey.new(Base64.strict_decode64(private_key_b64))
|
|
14
|
+
message_bytes = json_dump(payload)
|
|
15
|
+
signature = key.sign(message_bytes)
|
|
16
|
+
{ payload: payload, signature: Base64.strict_encode64(signature), signed_bytes: message_bytes }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def verify_message(signed_message, org_id:)
|
|
20
|
+
peer = find_peer(org_id)
|
|
21
|
+
return { valid: false, org_id: org_id, reason: :unknown_peer } unless peer
|
|
22
|
+
|
|
23
|
+
require 'ed25519'
|
|
24
|
+
pub_key_b64 = peer[:public_key].sub(/\Aed25519:/, '')
|
|
25
|
+
verify_key = Ed25519::VerifyKey.new(Base64.strict_decode64(pub_key_b64))
|
|
26
|
+
signature = Base64.strict_decode64(signed_message[:signature])
|
|
27
|
+
message_bytes = signed_message[:signed_bytes] || json_dump(signed_message[:payload])
|
|
28
|
+
verify_key.verify(signature, message_bytes)
|
|
29
|
+
{ valid: true, org_id: org_id }
|
|
30
|
+
rescue Ed25519::VerifyError
|
|
31
|
+
{ valid: false, org_id: org_id, reason: :invalid_signature }
|
|
32
|
+
rescue StandardError => e
|
|
33
|
+
{ valid: false, org_id: org_id, reason: :error, message: e.message }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def check_rate_limit(org_id)
|
|
37
|
+
@counters ||= Hash.new { |h, k| h[k] = { count: 0, window_start: Time.now.utc } }
|
|
38
|
+
counter = @counters[org_id]
|
|
39
|
+
peer = find_peer(org_id)
|
|
40
|
+
limit = peer&.dig(:rate_limit) || 100
|
|
41
|
+
|
|
42
|
+
if Time.now.utc - counter[:window_start] > 60
|
|
43
|
+
counter[:count] = 0
|
|
44
|
+
counter[:window_start] = Time.now.utc
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
counter[:count] += 1
|
|
48
|
+
return { allowed: true, remaining: limit - counter[:count] } if counter[:count] <= limit
|
|
49
|
+
|
|
50
|
+
{ allowed: false, error: :rate_limited, org_id: org_id }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def reset_counters!
|
|
54
|
+
@counters = nil
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def find_peer(org_id)
|
|
60
|
+
return nil unless defined?(Legion::Settings)
|
|
61
|
+
|
|
62
|
+
peers = Legion::Settings.dig(:mesh, :trusted_peers) || []
|
|
63
|
+
peers.find { |p| p[:org_id] == org_id }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def json_dump(data)
|
|
67
|
+
if defined?(Legion::JSON)
|
|
68
|
+
Legion::JSON.dump({ data: data })
|
|
69
|
+
else
|
|
70
|
+
require 'json'
|
|
71
|
+
::JSON.dump({ data: data })
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Mesh
|
|
6
|
+
module Helpers
|
|
7
|
+
class PendingRequests
|
|
8
|
+
def initialize(default_ttl: 5)
|
|
9
|
+
@default_ttl = default_ttl
|
|
10
|
+
@requests = {}
|
|
11
|
+
@mutex = Mutex.new
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def register(correlation_id:, callback:, ttl: nil)
|
|
15
|
+
@mutex.synchronize do
|
|
16
|
+
@requests[correlation_id] = {
|
|
17
|
+
callback: callback,
|
|
18
|
+
registered_at: Time.now,
|
|
19
|
+
ttl: ttl || @default_ttl
|
|
20
|
+
}
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def resolve(correlation_id:, result:) # rubocop:disable Naming/PredicateMethod
|
|
25
|
+
entry = @mutex.synchronize { @requests.delete(correlation_id) }
|
|
26
|
+
return false unless entry
|
|
27
|
+
|
|
28
|
+
entry[:callback]&.call(result)
|
|
29
|
+
true
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def pending?(correlation_id)
|
|
33
|
+
@mutex.synchronize { @requests.key?(correlation_id) }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def pending_count
|
|
37
|
+
@mutex.synchronize { @requests.size }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def expire
|
|
41
|
+
now = Time.now
|
|
42
|
+
expired = []
|
|
43
|
+
@mutex.synchronize do
|
|
44
|
+
@requests.each do |id, entry|
|
|
45
|
+
next unless now - entry[:registered_at] >= entry[:ttl]
|
|
46
|
+
|
|
47
|
+
expired << id
|
|
48
|
+
end
|
|
49
|
+
expired.each { |id| @requests.delete(id) }
|
|
50
|
+
end
|
|
51
|
+
expired
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -65,6 +65,18 @@ module Legion
|
|
|
65
65
|
msg
|
|
66
66
|
end
|
|
67
67
|
|
|
68
|
+
def expire_silent_agents(timeout: Topology::MESH_SILENCE_TIMEOUT)
|
|
69
|
+
cutoff = Time.now.utc - timeout
|
|
70
|
+
expired = []
|
|
71
|
+
@agents.each_value do |agent|
|
|
72
|
+
next unless agent[:status] == :online && agent[:last_seen] < cutoff
|
|
73
|
+
|
|
74
|
+
agent[:status] = :offline
|
|
75
|
+
expired << agent[:agent_id]
|
|
76
|
+
end
|
|
77
|
+
expired
|
|
78
|
+
end
|
|
79
|
+
|
|
68
80
|
def online_agents
|
|
69
81
|
@agents.values.select { |a| a[:status] == :online }
|
|
70
82
|
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../helpers/delegation'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Extensions
|
|
7
|
+
module Mesh
|
|
8
|
+
module Runners
|
|
9
|
+
module Delegation
|
|
10
|
+
include Legion::Extensions::Helpers::Lex if defined?(Legion::Extensions::Helpers::Lex)
|
|
11
|
+
|
|
12
|
+
def delegate(from:, to:, task_context:, consent_level: :execute, parent_delegation_id: nil, **) # rubocop:disable Metrics/ParameterLists
|
|
13
|
+
result = delegation_tracker.create(
|
|
14
|
+
from: from,
|
|
15
|
+
to: to,
|
|
16
|
+
task_context: task_context,
|
|
17
|
+
consent_level: consent_level.to_sym,
|
|
18
|
+
parent_delegation_id: parent_delegation_id
|
|
19
|
+
)
|
|
20
|
+
return { success: false, **result } if result[:error]
|
|
21
|
+
|
|
22
|
+
publish_delegation_event('delegation.created', result)
|
|
23
|
+
{ success: true, delegation_id: result[:delegation_id], depth: result[:depth] }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def complete_delegation(delegation_id:, **)
|
|
27
|
+
result = delegation_tracker.complete(delegation_id)
|
|
28
|
+
return { success: false, reason: :not_found_or_inactive } unless result
|
|
29
|
+
|
|
30
|
+
publish_delegation_event('delegation.completed', result)
|
|
31
|
+
{ success: true, delegation_id: delegation_id }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def revoke_delegation(delegation_id:, **)
|
|
35
|
+
result = delegation_tracker.revoke(delegation_id)
|
|
36
|
+
return { success: false, reason: :not_found_or_inactive } unless result
|
|
37
|
+
|
|
38
|
+
publish_delegation_event('delegation.revoked', result)
|
|
39
|
+
{ success: true, delegation_id: delegation_id }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def delegation_chain(delegation_id:, **)
|
|
43
|
+
chain = delegation_tracker.chain(delegation_id)
|
|
44
|
+
{ success: true, chain: chain, depth: [chain.size - 1, 0].max }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def agent_delegations(agent_id:, status: nil, **)
|
|
48
|
+
results = delegation_tracker.for_agent(agent_id, status: status&.to_sym)
|
|
49
|
+
{ success: true, delegations: results, count: results.size }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def delegation_stats(**)
|
|
53
|
+
delegation_tracker.stats.merge(success: true)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def publish_delegation_event(event_type, record)
|
|
59
|
+
return unless defined?(Legion::Extensions::Audit::Transport::Messages::Audit)
|
|
60
|
+
|
|
61
|
+
Legion::Extensions::Audit::Transport::Messages::Audit.new(
|
|
62
|
+
event_type: event_type,
|
|
63
|
+
principal_id: record[:from_agent_id],
|
|
64
|
+
action: event_type.split('.').last,
|
|
65
|
+
resource: "delegation:#{record[:delegation_id]}",
|
|
66
|
+
detail: { to: record[:to_agent_id], depth: record[:depth], consent: record[:consent_level] }
|
|
67
|
+
).publish
|
|
68
|
+
rescue StandardError => e
|
|
69
|
+
Legion::Logging.warn "[mesh] failed to publish #{event_type}: #{e.message}" if defined?(Legion::Logging)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def delegation_tracker
|
|
73
|
+
@delegation_tracker ||= begin
|
|
74
|
+
max_depth = nil
|
|
75
|
+
max_active = nil
|
|
76
|
+
if defined?(Legion::Settings)
|
|
77
|
+
max_depth = Legion::Settings.dig(:mesh, :delegation, :max_depth)
|
|
78
|
+
max_active = Legion::Settings.dig(:mesh, :delegation, :max_active_per_agent)
|
|
79
|
+
end
|
|
80
|
+
Helpers::Delegation.new(max_depth: max_depth || 3, max_active_per_agent: max_active || 10)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -18,6 +18,7 @@ module Legion
|
|
|
18
18
|
result = mesh_registry.unregister_agent(agent_id)
|
|
19
19
|
if result
|
|
20
20
|
Legion::Logging.info "[mesh] unregistered: agent=#{agent_id}"
|
|
21
|
+
publish_mesh_departure(agent_id: agent_id, capabilities: result[:capabilities] || [])
|
|
21
22
|
{ unregistered: true }
|
|
22
23
|
else
|
|
23
24
|
Legion::Logging.debug "[mesh] unregister failed: agent=#{agent_id} not found"
|
|
@@ -55,8 +56,27 @@ module Legion
|
|
|
55
56
|
{ total: total, online: online.size, message_count: msgs }
|
|
56
57
|
end
|
|
57
58
|
|
|
59
|
+
def expire_silent_agents(**)
|
|
60
|
+
expired = mesh_registry.expire_silent_agents
|
|
61
|
+
expired.each do |agent_id|
|
|
62
|
+
Legion::Logging.info "[mesh] expired silent agent: #{agent_id}"
|
|
63
|
+
end
|
|
64
|
+
{ success: true, expired: expired, count: expired.size }
|
|
65
|
+
end
|
|
66
|
+
|
|
58
67
|
private
|
|
59
68
|
|
|
69
|
+
def publish_mesh_departure(agent_id:, capabilities:)
|
|
70
|
+
return unless defined?(Legion::Extensions::Mesh::Transport::Messages::MeshDeparture)
|
|
71
|
+
|
|
72
|
+
Legion::Extensions::Mesh::Transport::Messages::MeshDeparture.new(
|
|
73
|
+
agent_id: agent_id, capabilities: capabilities
|
|
74
|
+
).publish
|
|
75
|
+
Legion::Logging.debug "[mesh] departure signal published: agent=#{agent_id}"
|
|
76
|
+
rescue StandardError => e
|
|
77
|
+
Legion::Logging.warn "[mesh] failed to publish departure signal: #{e.message}"
|
|
78
|
+
end
|
|
79
|
+
|
|
60
80
|
def mesh_registry
|
|
61
81
|
@mesh_registry ||= Helpers::Registry.new
|
|
62
82
|
end
|