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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 75ab8e1dbc78d725a92257640520bfda3f3acb36ae1dc1f1315cc7e8453ca467
4
- data.tar.gz: 269027a72a18770e8bf0338fa7c2941bc354546ffc6157bd2533fc258b054baf
3
+ metadata.gz: 25cafa977ed6aaf671c903e71895312253a412c32c141e8de9674b1a86747bff
4
+ data.tar.gz: c7bbdde6f4c065f0fdaea58a5a5124bd53e49f0c48e0d275de0828bd93ec2439
5
5
  SHA512:
6
- metadata.gz: 78afa9c435f105a2c18b107d14de0c6d4b9c8850c5181ce394aafe46613c70dedccb24b5b19dbf5ee71f2fa71657905e656d2d4eb916f3429446f0c61398a46d
7
- data.tar.gz: 766eb42fd99abaa287af0cd1f48b8daba505f9e4d20dccb927e748cb9734460839915ba2e8513aa8df5bf28817f032f230e08e5bf775f3330d9e4f34d48f47db
6
+ metadata.gz: 76357a6ca4c31089fd7f5317097f935b07df6349476b344bd2681dccfbd796d79b1061922b22d42ca45b92d045bdbf901ed8d6c502f525cff3a668099dfea56c
7
+ data.tar.gz: 9e17a698c0b941fd2000ab9affffa87cd3bad983b37acacce794ee0fb4d975a8cdc04eee44480f976856e712fb6af8dfa80f976d07c2fd94ea302ba61f31b389
data/Gemfile CHANGED
@@ -4,5 +4,7 @@ source 'https://rubygems.org'
4
4
 
5
5
  gemspec
6
6
 
7
+ gem 'base64'
8
+ gem 'ed25519', '~> 1.3'
7
9
  gem 'rspec', '~> 3.13'
8
10
  gem 'rubocop', '~> 1.75', require: false
data/lex-mesh.gemspec CHANGED
@@ -25,4 +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
+
29
+ spec.add_dependency 'base64'
30
+ spec.add_dependency 'ed25519', '~> 1.3'
28
31
  end
@@ -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
@@ -3,7 +3,7 @@
3
3
  module Legion
4
4
  module Extensions
5
5
  module Mesh
6
- VERSION = '0.2.3'
6
+ VERSION = '0.2.4'
7
7
  end
8
8
  end
9
9
  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.3
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