lex-mesh 0.2.3 → 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 -0
- data/lex-mesh.gemspec +3 -0
- 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/runners/delegation.rb +87 -0
- data/lib/legion/extensions/mesh/version.rb +1 -1
- data/lib/legion/extensions/mesh.rb +3 -0
- 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/runners/delegation_spec.rb +84 -0
- metadata +36 -2
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
|
@@ -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,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
|
|
@@ -5,8 +5,11 @@ require 'legion/extensions/mesh/helpers/topology'
|
|
|
5
5
|
require 'legion/extensions/mesh/helpers/registry'
|
|
6
6
|
require 'legion/extensions/mesh/helpers/preference_profile'
|
|
7
7
|
require 'legion/extensions/mesh/helpers/pending_requests'
|
|
8
|
+
require 'legion/extensions/mesh/helpers/delegation'
|
|
9
|
+
require 'legion/extensions/mesh/helpers/peer_verify'
|
|
8
10
|
require 'legion/extensions/mesh/runners/mesh'
|
|
9
11
|
require 'legion/extensions/mesh/runners/preferences'
|
|
12
|
+
require 'legion/extensions/mesh/runners/delegation'
|
|
10
13
|
|
|
11
14
|
if defined?(Legion::Transport)
|
|
12
15
|
require 'legion/extensions/mesh/transport/messages/preference_query'
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe Legion::Extensions::Mesh::Helpers::Delegation do
|
|
6
|
+
subject(:tracker) { described_class.new(max_depth: 3, max_active_per_agent: 5) }
|
|
7
|
+
|
|
8
|
+
describe '#create' do
|
|
9
|
+
it 'creates a delegation record' do
|
|
10
|
+
result = tracker.create(from: 'agent-a', to: 'agent-b', task_context: 'task-1', consent_level: :execute)
|
|
11
|
+
expect(result[:delegation_id]).to start_with('del-')
|
|
12
|
+
expect(result[:from_agent_id]).to eq('agent-a')
|
|
13
|
+
expect(result[:to_agent_id]).to eq('agent-b')
|
|
14
|
+
expect(result[:status]).to eq(:active)
|
|
15
|
+
expect(result[:depth]).to eq(0)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
it 'increments depth for chained delegations' do
|
|
19
|
+
d1 = tracker.create(from: 'a', to: 'b', task_context: 't', consent_level: :execute)
|
|
20
|
+
d2 = tracker.create(from: 'b', to: 'c', task_context: 't', consent_level: :execute,
|
|
21
|
+
parent_delegation_id: d1[:delegation_id])
|
|
22
|
+
expect(d2[:depth]).to eq(1)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it 'refuses delegation beyond max depth' do
|
|
26
|
+
d1 = tracker.create(from: 'a', to: 'b', task_context: 't', consent_level: :execute)
|
|
27
|
+
d2 = tracker.create(from: 'b', to: 'c', task_context: 't', consent_level: :execute,
|
|
28
|
+
parent_delegation_id: d1[:delegation_id])
|
|
29
|
+
d3 = tracker.create(from: 'c', to: 'd', task_context: 't', consent_level: :execute,
|
|
30
|
+
parent_delegation_id: d2[:delegation_id])
|
|
31
|
+
d4 = tracker.create(from: 'd', to: 'e', task_context: 't', consent_level: :execute,
|
|
32
|
+
parent_delegation_id: d3[:delegation_id])
|
|
33
|
+
expect(d4[:error]).to eq(:max_depth_exceeded)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
it 'refuses consent escalation' do
|
|
37
|
+
d1 = tracker.create(from: 'a', to: 'b', task_context: 't', consent_level: :execute)
|
|
38
|
+
d2 = tracker.create(from: 'b', to: 'c', task_context: 't', consent_level: :admin,
|
|
39
|
+
parent_delegation_id: d1[:delegation_id])
|
|
40
|
+
expect(d2[:error]).to eq(:consent_escalation)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it 'allows consent de-escalation' do
|
|
44
|
+
d1 = tracker.create(from: 'a', to: 'b', task_context: 't', consent_level: :admin)
|
|
45
|
+
d2 = tracker.create(from: 'b', to: 'c', task_context: 't', consent_level: :read,
|
|
46
|
+
parent_delegation_id: d1[:delegation_id])
|
|
47
|
+
expect(d2[:delegation_id]).to start_with('del-')
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
it 'refuses when max active per agent exceeded' do
|
|
51
|
+
5.times { |i| tracker.create(from: 'a', to: "b#{i}", task_context: 't', consent_level: :execute) }
|
|
52
|
+
result = tracker.create(from: 'a', to: 'b6', task_context: 't', consent_level: :execute)
|
|
53
|
+
expect(result[:error]).to eq(:max_active_exceeded)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
describe '#complete' do
|
|
58
|
+
it 'marks a delegation as completed' do
|
|
59
|
+
d = tracker.create(from: 'a', to: 'b', task_context: 't', consent_level: :execute)
|
|
60
|
+
result = tracker.complete(d[:delegation_id])
|
|
61
|
+
expect(result[:status]).to eq(:completed)
|
|
62
|
+
expect(result[:completed_at]).to be_a(Time)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
it 'returns nil for non-existent delegation' do
|
|
66
|
+
expect(tracker.complete('del-nonexistent')).to be_nil
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
describe '#revoke' do
|
|
71
|
+
it 'marks delegation and children as revoked' do
|
|
72
|
+
d1 = tracker.create(from: 'a', to: 'b', task_context: 't', consent_level: :execute)
|
|
73
|
+
d2 = tracker.create(from: 'b', to: 'c', task_context: 't', consent_level: :execute,
|
|
74
|
+
parent_delegation_id: d1[:delegation_id])
|
|
75
|
+
tracker.revoke(d1[:delegation_id])
|
|
76
|
+
|
|
77
|
+
expect(tracker.find(d1[:delegation_id])[:status]).to eq(:revoked)
|
|
78
|
+
expect(tracker.find(d2[:delegation_id])[:status]).to eq(:revoked)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
describe '#chain' do
|
|
83
|
+
it 'returns the full delegation chain' do
|
|
84
|
+
d1 = tracker.create(from: 'a', to: 'b', task_context: 't', consent_level: :execute)
|
|
85
|
+
d2 = tracker.create(from: 'b', to: 'c', task_context: 't', consent_level: :execute,
|
|
86
|
+
parent_delegation_id: d1[:delegation_id])
|
|
87
|
+
chain = tracker.chain(d2[:delegation_id])
|
|
88
|
+
expect(chain.size).to eq(2)
|
|
89
|
+
expect(chain.first[:from_agent_id]).to eq('a')
|
|
90
|
+
expect(chain.last[:from_agent_id]).to eq('b')
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
describe '#for_agent' do
|
|
95
|
+
it 'returns delegations involving an agent' do
|
|
96
|
+
tracker.create(from: 'a', to: 'b', task_context: 't1', consent_level: :execute)
|
|
97
|
+
tracker.create(from: 'a', to: 'c', task_context: 't2', consent_level: :execute)
|
|
98
|
+
tracker.create(from: 'x', to: 'y', task_context: 't3', consent_level: :execute)
|
|
99
|
+
|
|
100
|
+
results = tracker.for_agent('a')
|
|
101
|
+
expect(results.size).to eq(2)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
it 'filters by status' do
|
|
105
|
+
d = tracker.create(from: 'a', to: 'b', task_context: 't', consent_level: :execute)
|
|
106
|
+
tracker.create(from: 'a', to: 'c', task_context: 't', consent_level: :execute)
|
|
107
|
+
tracker.complete(d[:delegation_id])
|
|
108
|
+
|
|
109
|
+
active = tracker.for_agent('a', status: :active)
|
|
110
|
+
expect(active.size).to eq(1)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
describe '#stats' do
|
|
115
|
+
it 'returns summary statistics' do
|
|
116
|
+
tracker.create(from: 'a', to: 'b', task_context: 't', consent_level: :execute)
|
|
117
|
+
d = tracker.create(from: 'a', to: 'c', task_context: 't', consent_level: :execute)
|
|
118
|
+
tracker.complete(d[:delegation_id])
|
|
119
|
+
|
|
120
|
+
stats = tracker.stats
|
|
121
|
+
expect(stats[:total]).to eq(2)
|
|
122
|
+
expect(stats[:active]).to eq(1)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'ed25519'
|
|
5
|
+
|
|
6
|
+
RSpec.describe Legion::Extensions::Mesh::Helpers::PeerVerify do
|
|
7
|
+
let(:signing_key) { Ed25519::SigningKey.generate }
|
|
8
|
+
let(:verify_key) { signing_key.verify_key }
|
|
9
|
+
let(:private_key_b64) { Base64.strict_encode64(signing_key.to_bytes) }
|
|
10
|
+
let(:public_key_b64) { Base64.strict_encode64(verify_key.to_bytes) }
|
|
11
|
+
|
|
12
|
+
let(:peer_config) do
|
|
13
|
+
[{ org_id: 'acme', public_key: "ed25519:#{public_key_b64}",
|
|
14
|
+
capabilities: %w[query task], rate_limit: 5 }]
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
before do
|
|
18
|
+
described_class.reset_counters!
|
|
19
|
+
stub_const('Legion::Settings', Module.new) unless defined?(Legion::Settings)
|
|
20
|
+
allow(Legion::Settings).to receive(:dig).with(:mesh, :trusted_peers).and_return(peer_config)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
describe '.sign_message' do
|
|
24
|
+
it 'signs a payload with Ed25519' do
|
|
25
|
+
result = described_class.sign_message({ hello: 'world' }, private_key_b64)
|
|
26
|
+
expect(result[:signature]).to be_a(String)
|
|
27
|
+
expect(result[:payload]).to eq({ hello: 'world' })
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
describe '.verify_message' do
|
|
32
|
+
it 'verifies a validly signed message' do
|
|
33
|
+
signed = described_class.sign_message({ hello: 'world' }, private_key_b64)
|
|
34
|
+
result = described_class.verify_message(signed, org_id: 'acme')
|
|
35
|
+
expect(result[:valid]).to be true
|
|
36
|
+
expect(result[:org_id]).to eq('acme')
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it 'rejects a tampered message' do
|
|
40
|
+
signed = described_class.sign_message({ hello: 'world' }, private_key_b64)
|
|
41
|
+
signed[:signed_bytes] = 'tampered data'
|
|
42
|
+
result = described_class.verify_message(signed, org_id: 'acme')
|
|
43
|
+
expect(result[:valid]).to be false
|
|
44
|
+
expect(result[:reason]).to eq(:invalid_signature)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it 'rejects unknown peers' do
|
|
48
|
+
signed = described_class.sign_message({ hello: 'world' }, private_key_b64)
|
|
49
|
+
result = described_class.verify_message(signed, org_id: 'unknown-org')
|
|
50
|
+
expect(result[:valid]).to be false
|
|
51
|
+
expect(result[:reason]).to eq(:unknown_peer)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
describe '.check_rate_limit' do
|
|
56
|
+
it 'allows messages within limit' do
|
|
57
|
+
result = described_class.check_rate_limit('acme')
|
|
58
|
+
expect(result[:allowed]).to be true
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
it 'blocks messages over limit' do
|
|
62
|
+
5.times { described_class.check_rate_limit('acme') }
|
|
63
|
+
result = described_class.check_rate_limit('acme')
|
|
64
|
+
expect(result[:allowed]).to be false
|
|
65
|
+
expect(result[:error]).to eq(:rate_limited)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
it 'resets after window expires' do
|
|
69
|
+
5.times { described_class.check_rate_limit('acme') }
|
|
70
|
+
counters = described_class.instance_variable_get(:@counters)
|
|
71
|
+
counters['acme'][:window_start] = Time.now.utc - 61
|
|
72
|
+
result = described_class.check_rate_limit('acme')
|
|
73
|
+
expect(result[:allowed]).to be true
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe Legion::Extensions::Mesh::Runners::Delegation do
|
|
6
|
+
subject { Object.new.extend(described_class) }
|
|
7
|
+
|
|
8
|
+
before do
|
|
9
|
+
subject.instance_variable_set(:@delegation_tracker, nil)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
describe '#delegate' do
|
|
13
|
+
it 'creates a delegation' do
|
|
14
|
+
result = subject.delegate(from: 'a', to: 'b', task_context: 'task-1', consent_level: :execute)
|
|
15
|
+
expect(result[:success]).to be true
|
|
16
|
+
expect(result[:delegation_id]).to start_with('del-')
|
|
17
|
+
expect(result[:depth]).to eq(0)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it 'returns failure for depth exceeded' do
|
|
21
|
+
d1 = subject.delegate(from: 'a', to: 'b', task_context: 't', consent_level: :execute)
|
|
22
|
+
d2 = subject.delegate(from: 'b', to: 'c', task_context: 't', consent_level: :execute,
|
|
23
|
+
parent_delegation_id: d1[:delegation_id])
|
|
24
|
+
d3 = subject.delegate(from: 'c', to: 'd', task_context: 't', consent_level: :execute,
|
|
25
|
+
parent_delegation_id: d2[:delegation_id])
|
|
26
|
+
d4 = subject.delegate(from: 'd', to: 'e', task_context: 't', consent_level: :execute,
|
|
27
|
+
parent_delegation_id: d3[:delegation_id])
|
|
28
|
+
expect(d4[:success]).to be false
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
describe '#complete_delegation' do
|
|
33
|
+
it 'completes an active delegation' do
|
|
34
|
+
d = subject.delegate(from: 'a', to: 'b', task_context: 't', consent_level: :execute)
|
|
35
|
+
result = subject.complete_delegation(delegation_id: d[:delegation_id])
|
|
36
|
+
expect(result[:success]).to be true
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it 'fails for non-existent delegation' do
|
|
40
|
+
result = subject.complete_delegation(delegation_id: 'del-fake')
|
|
41
|
+
expect(result[:success]).to be false
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
describe '#revoke_delegation' do
|
|
46
|
+
it 'revokes an active delegation' do
|
|
47
|
+
d = subject.delegate(from: 'a', to: 'b', task_context: 't', consent_level: :execute)
|
|
48
|
+
result = subject.revoke_delegation(delegation_id: d[:delegation_id])
|
|
49
|
+
expect(result[:success]).to be true
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
describe '#delegation_chain' do
|
|
54
|
+
it 'returns the full chain' do
|
|
55
|
+
d1 = subject.delegate(from: 'a', to: 'b', task_context: 't', consent_level: :execute)
|
|
56
|
+
d2 = subject.delegate(from: 'b', to: 'c', task_context: 't', consent_level: :execute,
|
|
57
|
+
parent_delegation_id: d1[:delegation_id])
|
|
58
|
+
result = subject.delegation_chain(delegation_id: d2[:delegation_id])
|
|
59
|
+
expect(result[:success]).to be true
|
|
60
|
+
expect(result[:chain].size).to eq(2)
|
|
61
|
+
expect(result[:depth]).to eq(1)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
describe '#agent_delegations' do
|
|
66
|
+
it 'lists delegations for an agent' do
|
|
67
|
+
subject.delegate(from: 'a', to: 'b', task_context: 't1', consent_level: :execute)
|
|
68
|
+
subject.delegate(from: 'a', to: 'c', task_context: 't2', consent_level: :execute)
|
|
69
|
+
result = subject.agent_delegations(agent_id: 'a')
|
|
70
|
+
expect(result[:success]).to be true
|
|
71
|
+
expect(result[:count]).to eq(2)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
describe '#delegation_stats' do
|
|
76
|
+
it 'returns statistics' do
|
|
77
|
+
subject.delegate(from: 'a', to: 'b', task_context: 't', consent_level: :execute)
|
|
78
|
+
result = subject.delegation_stats
|
|
79
|
+
expect(result[:success]).to be true
|
|
80
|
+
expect(result[:total]).to eq(1)
|
|
81
|
+
expect(result[:active]).to eq(1)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,42 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: lex-mesh
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Esity
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
-
dependencies:
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: base64
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: ed25519
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '1.3'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '1.3'
|
|
12
40
|
description: Agent-to-agent mesh communication protocol for brain-modeled agentic
|
|
13
41
|
AI
|
|
14
42
|
email:
|
|
@@ -25,10 +53,13 @@ files:
|
|
|
25
53
|
- lib/legion/extensions/mesh/actors/preference_listener.rb
|
|
26
54
|
- lib/legion/extensions/mesh/actors/silence_watchdog.rb
|
|
27
55
|
- lib/legion/extensions/mesh/client.rb
|
|
56
|
+
- lib/legion/extensions/mesh/helpers/delegation.rb
|
|
57
|
+
- lib/legion/extensions/mesh/helpers/peer_verify.rb
|
|
28
58
|
- lib/legion/extensions/mesh/helpers/pending_requests.rb
|
|
29
59
|
- lib/legion/extensions/mesh/helpers/preference_profile.rb
|
|
30
60
|
- lib/legion/extensions/mesh/helpers/registry.rb
|
|
31
61
|
- lib/legion/extensions/mesh/helpers/topology.rb
|
|
62
|
+
- lib/legion/extensions/mesh/runners/delegation.rb
|
|
32
63
|
- lib/legion/extensions/mesh/runners/mesh.rb
|
|
33
64
|
- lib/legion/extensions/mesh/runners/preferences.rb
|
|
34
65
|
- lib/legion/extensions/mesh/transport/messages/mesh_departure.rb
|
|
@@ -41,10 +72,13 @@ files:
|
|
|
41
72
|
- spec/legion/extensions/mesh/actors/preference_listener_spec.rb
|
|
42
73
|
- spec/legion/extensions/mesh/actors/silence_watchdog_spec.rb
|
|
43
74
|
- spec/legion/extensions/mesh/client_spec.rb
|
|
75
|
+
- spec/legion/extensions/mesh/helpers/delegation_spec.rb
|
|
76
|
+
- spec/legion/extensions/mesh/helpers/peer_verify_spec.rb
|
|
44
77
|
- spec/legion/extensions/mesh/helpers/pending_requests_spec.rb
|
|
45
78
|
- spec/legion/extensions/mesh/helpers/preference_profile_spec.rb
|
|
46
79
|
- spec/legion/extensions/mesh/helpers/registry_spec.rb
|
|
47
80
|
- spec/legion/extensions/mesh/helpers/topology_spec.rb
|
|
81
|
+
- spec/legion/extensions/mesh/runners/delegation_spec.rb
|
|
48
82
|
- spec/legion/extensions/mesh/runners/mesh_spec.rb
|
|
49
83
|
- spec/legion/extensions/mesh/runners/preferences_spec.rb
|
|
50
84
|
- spec/legion/extensions/mesh/transport/messages/mesh_departure_spec.rb
|