lex-governance 0.2.0 → 0.2.1

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: 6ae04ddeb792bd593f11c4b121cf778a1b6fb14b9827de61aca08d88363f3cb8
4
- data.tar.gz: 1c9c72455e758c50f9bdc00995a32d9bb2c7a18d1140f2203020467b08c91b1b
3
+ metadata.gz: c49ec8c4434d925672bf24d03d0d568673dba8007ea72303b5d345fb87a5142c
4
+ data.tar.gz: 85e0ccc337f078cdc9cd7f8b4f2abf0c311f66c1bc9fc15ed0e6483f4c187290
5
5
  SHA512:
6
- metadata.gz: 6cf0a327af81329db2a4f8255c0ac4fdbcdccbfab56ef9803eb70ea14880bedd8bb99f1161381eeed56345ed9b9c6fcd81e3c2e77c92971dabf0e32a8da6bb56
7
- data.tar.gz: 1f2834223f2a83f382e89684afbdffae40f913e36d174f55ef3596feade744f842fa62716ff544da6eb88ad4e9575143a83da85a7deae9865eb0b0aeb9db30cb
6
+ metadata.gz: 16ce4ad3ee7c08f7feeaf9e48efc9ff7ac15a1636be69e7cf59103a61684f3f77f296b2ee791be8c11caa4443e5b5790d55acc31f96899f77c96e7d932847b72
7
+ data.tar.gz: 42ef43f72cfd53b14b3095128b441809568fb03a6885631e3607089b69a2d8c0a981db3e788724d3b5f4164333c5bf3b142735d33e7a6ced5d9739d16740354b
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Governance
6
+ module Helpers
7
+ module Airb
8
+ AIRB_STATUSES = %i[unknown pending approved conditional denied expired].freeze
9
+
10
+ REQUIRE_AIRB_APPROVAL = {
11
+ low: false, medium: false, high: true, critical: true
12
+ }.freeze
13
+
14
+ ACCEPTABLE_STATUSES = {
15
+ low: %i[unknown pending approved conditional],
16
+ medium: %i[unknown pending approved conditional],
17
+ high: %i[approved conditional],
18
+ critical: %i[approved]
19
+ }.freeze
20
+
21
+ AirbRecord = Struct.new(:worker_id, :airb_id, :status, :risk_tier, :expires_at, :conditions)
22
+
23
+ module_function
24
+
25
+ def fetch(worker_id:)
26
+ backend = Legion::Settings.dig(:governance, :airb, :backend)&.to_sym || :settings
27
+ case backend
28
+ when :stub
29
+ AirbRecord.new(worker_id: worker_id, airb_id: 'STUB', status: :approved,
30
+ risk_tier: :low, expires_at: nil, conditions: [])
31
+ else
32
+ fetch_from_settings(worker_id)
33
+ end
34
+ end
35
+
36
+ def fetch_from_settings(worker_id)
37
+ approvals = Legion::Settings[:airb_approvals] || {}
38
+ entry = approvals[worker_id.to_s] || approvals[worker_id.to_sym]
39
+
40
+ unless entry
41
+ return AirbRecord.new(worker_id: worker_id, airb_id: nil, status: :unknown,
42
+ risk_tier: :low, expires_at: nil, conditions: [])
43
+ end
44
+
45
+ AirbRecord.new(
46
+ worker_id: worker_id,
47
+ airb_id: entry[:airb_id],
48
+ status: (entry[:status] || 'unknown').to_sym,
49
+ risk_tier: (entry[:risk_tier] || 'low').to_sym,
50
+ expires_at: entry[:expires_at],
51
+ conditions: entry[:conditions] || []
52
+ )
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -5,76 +5,21 @@ module Legion
5
5
  module Governance
6
6
  module Runners
7
7
  module Governance
8
- include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
9
- Legion::Extensions::Helpers.const_defined?(:Lex)
10
-
11
- def create_proposal(category:, description:, proposer:, council_size: nil, **)
12
- return { error: :invalid_category, valid: Helpers::Layers::PROPOSAL_CATEGORIES } unless Helpers::Layers.valid_category?(category)
13
-
14
- size = council_size || Helpers::Layers::MIN_COUNCIL_SIZE
15
- id = proposal_store.create(category: category, description: description,
16
- proposer: proposer, council_size: size)
17
- Legion::Logging.info "[governance] proposal created: id=#{id[0..7]} category=#{category} proposer=#{proposer} council=#{size}"
18
- { proposal_id: id, category: category, status: :open }
19
- end
20
-
21
- def vote_on_proposal(proposal_id:, voter:, approve:, **)
22
- result = proposal_store.vote(proposal_id, voter: voter, approve: approve)
23
- case result
24
- when nil
25
- Legion::Logging.debug "[governance] vote failed: proposal=#{proposal_id[0..7]} not found or closed"
26
- { error: :not_found_or_closed }
27
- when :already_voted
28
- Legion::Logging.debug "[governance] vote failed: proposal=#{proposal_id[0..7]} voter=#{voter} already voted"
29
- { error: :already_voted }
30
- else
31
- Legion::Logging.info "[governance] vote: proposal=#{proposal_id[0..7]} voter=#{voter} approve=#{approve} resolution=#{result}"
32
- { voted: true, resolution: result }
33
- end
34
- end
35
-
36
- def get_proposal(proposal_id:, **)
37
- prop = proposal_store.get(proposal_id)
38
- Legion::Logging.debug "[governance] get: proposal=#{proposal_id[0..7]} found=#{!prop.nil?}"
39
- prop ? { found: true, proposal: prop } : { found: false }
40
- end
41
-
42
- def open_proposals(**)
43
- props = proposal_store.open_proposals
44
- Legion::Logging.debug "[governance] open proposals: count=#{props.size}"
45
- { proposals: props, count: props.size }
46
- end
47
-
48
- def timeout_proposals(**)
49
- open = proposal_store.open_proposals
50
- timed = open.select { |p| Time.now.utc - p[:created_at] > Helpers::Layers::VOTE_TIMEOUT }
51
- timed.each { |p| proposal_store.resolve_timed_out(p[:proposal_id]) }
52
- timed_ids = timed.map { |p| p[:proposal_id] }
53
- Legion::Logging.debug "[governance] vote timeout sweep: open=#{open.size} timed_out=#{timed.size}"
54
- { checked: open.size, timed_out: timed.size, timed_out_ids: timed_ids }
55
- end
56
-
57
- def validate_action(layer:, action: nil, _context: {}, **) # rubocop:disable Lint/UnusedMethodArgument
58
- return { error: :invalid_layer } unless Helpers::Layers.valid_layer?(layer)
59
-
60
- result = case layer
61
- when :agent_validation
62
- { allowed: true, layer: layer, reason: :self_validated }
63
- when :anomaly_detection
64
- { allowed: true, layer: layer, reason: :no_anomaly }
65
- when :human_deliberation
66
- { allowed: false, layer: layer, reason: :requires_human_approval }
67
- when :transparency
68
- { allowed: true, layer: layer, reason: :logged, audit_required: true }
69
- end
70
- Legion::Logging.debug "[governance] validate: layer=#{layer} allowed=#{result[:allowed]} reason=#{result[:reason]}"
71
- result
72
- end
73
-
74
- private
75
-
76
- def proposal_store
77
- @proposal_store ||= Helpers::Proposal.new
8
+ def check_airb_approval(worker_id:, **)
9
+ require_relative '../helpers/airb'
10
+ record = Helpers::Airb.fetch(worker_id: worker_id)
11
+ required = Helpers::Airb::REQUIRE_AIRB_APPROVAL[record.risk_tier]
12
+ acceptable = Helpers::Airb::ACCEPTABLE_STATUSES[record.risk_tier]
13
+ allowed = !required || acceptable.include?(record.status)
14
+
15
+ {
16
+ allowed: allowed,
17
+ worker_id: worker_id,
18
+ airb_id: record.airb_id,
19
+ status: record.status,
20
+ risk_tier: record.risk_tier,
21
+ reason: allowed ? :airb_cleared : :airb_blocked
22
+ }
78
23
  end
79
24
  end
80
25
  end
@@ -3,7 +3,7 @@
3
3
  module Legion
4
4
  module Extensions
5
5
  module Governance
6
- VERSION = '0.2.0'
6
+ VERSION = '0.2.1'
7
7
  end
8
8
  end
9
9
  end
@@ -1,16 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'legion/extensions/governance/version'
4
- require 'legion/extensions/governance/helpers/layers'
5
- require 'legion/extensions/governance/helpers/proposal'
6
- require 'legion/extensions/governance/runners/governance'
7
- require 'legion/extensions/governance/runners/shadow_ai'
8
- require 'legion/extensions/governance/actors/shadow_ai_scan'
3
+ require_relative 'governance/version'
9
4
 
10
5
  module Legion
11
6
  module Extensions
12
7
  module Governance
13
- extend Legion::Extensions::Core if Legion::Extensions.const_defined? :Core
14
8
  end
15
9
  end
16
10
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lex-governance
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
@@ -9,41 +9,21 @@ bindir: bin
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies: []
12
- description: Four-layer distributed governance protocol for brain-modeled agentic
13
- AI
12
+ description: AIRB compliance gates and governance policy enforcement for LegionIO
14
13
  email:
15
14
  - matthewdiverson@gmail.com
16
15
  executables: []
17
16
  extensions: []
18
17
  extra_rdoc_files: []
19
18
  files:
20
- - Gemfile
21
- - lex-governance.gemspec
22
19
  - lib/legion/extensions/governance.rb
23
- - lib/legion/extensions/governance/actors/shadow_ai_scan.rb
24
- - lib/legion/extensions/governance/actors/vote_timeout.rb
25
- - lib/legion/extensions/governance/client.rb
26
- - lib/legion/extensions/governance/helpers/layers.rb
27
- - lib/legion/extensions/governance/helpers/proposal.rb
20
+ - lib/legion/extensions/governance/helpers/airb.rb
28
21
  - lib/legion/extensions/governance/runners/governance.rb
29
- - lib/legion/extensions/governance/runners/shadow_ai.rb
30
22
  - lib/legion/extensions/governance/version.rb
31
- - spec/legion/extensions/governance/actors/vote_timeout_spec.rb
32
- - spec/legion/extensions/governance/client_spec.rb
33
- - spec/legion/extensions/governance/helpers/layers_spec.rb
34
- - spec/legion/extensions/governance/helpers/proposal_spec.rb
35
- - spec/legion/extensions/governance/runners/governance_spec.rb
36
- - spec/legion/extensions/governance/runners/shadow_ai_spec.rb
37
- - spec/spec_helper.rb
38
- homepage: https://github.com/LegionIO/lex-governance
23
+ homepage: https://github.com/LegionIO
39
24
  licenses:
40
25
  - MIT
41
26
  metadata:
42
- homepage_uri: https://github.com/LegionIO/lex-governance
43
- source_code_uri: https://github.com/LegionIO/lex-governance
44
- documentation_uri: https://github.com/LegionIO/lex-governance
45
- changelog_uri: https://github.com/LegionIO/lex-governance
46
- bug_tracker_uri: https://github.com/LegionIO/lex-governance/issues
47
27
  rubygems_mfa_required: 'true'
48
28
  rdoc_options: []
49
29
  require_paths:
@@ -61,5 +41,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
61
41
  requirements: []
62
42
  rubygems_version: 3.6.9
63
43
  specification_version: 4
64
- summary: LEX Governance
44
+ summary: LEX::Governance
65
45
  test_files: []
data/Gemfile DELETED
@@ -1,14 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- source 'https://rubygems.org'
4
-
5
- gemspec
6
-
7
- gem 'rspec', '~> 3.13'
8
- gem 'rubocop', '~> 1.75', require: false
9
-
10
- if File.directory?(File.expand_path('../../legion-gaia', __dir__))
11
- gem 'legion-gaia', path: '../../legion-gaia'
12
- else
13
- gem 'legion-gaia'
14
- end
@@ -1,28 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'lib/legion/extensions/governance/version'
4
-
5
- Gem::Specification.new do |spec|
6
- spec.name = 'lex-governance'
7
- spec.version = Legion::Extensions::Governance::VERSION
8
- spec.authors = ['Esity']
9
- spec.email = ['matthewdiverson@gmail.com']
10
-
11
- spec.summary = 'LEX Governance'
12
- spec.description = 'Four-layer distributed governance protocol for brain-modeled agentic AI'
13
- spec.homepage = 'https://github.com/LegionIO/lex-governance'
14
- spec.license = 'MIT'
15
- spec.required_ruby_version = '>= 3.4'
16
-
17
- spec.metadata['homepage_uri'] = spec.homepage
18
- spec.metadata['source_code_uri'] = 'https://github.com/LegionIO/lex-governance'
19
- spec.metadata['documentation_uri'] = 'https://github.com/LegionIO/lex-governance'
20
- spec.metadata['changelog_uri'] = 'https://github.com/LegionIO/lex-governance'
21
- spec.metadata['bug_tracker_uri'] = 'https://github.com/LegionIO/lex-governance/issues'
22
- spec.metadata['rubygems_mfa_required'] = 'true'
23
-
24
- spec.files = Dir.chdir(File.expand_path(__dir__)) do
25
- Dir.glob('{lib,spec}/**/*') + %w[lex-governance.gemspec Gemfile]
26
- end
27
- spec.require_paths = ['lib']
28
- end
@@ -1,15 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Legion
4
- module Extensions
5
- module Governance
6
- module Actors
7
- class ShadowAiScan < Legion::Extensions::Actors::Every
8
- def runner_class = Runners::ShadowAi
9
- def runner_function = 'full_scan'
10
- def time = 86_400
11
- end
12
- end
13
- end
14
- end
15
- end
@@ -1,41 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'legion/extensions/actors/every'
4
-
5
- module Legion
6
- module Extensions
7
- module Governance
8
- module Actor
9
- class VoteTimeout < Legion::Extensions::Actors::Every
10
- def runner_class
11
- Legion::Extensions::Governance::Runners::Governance
12
- end
13
-
14
- def runner_function
15
- 'timeout_proposals'
16
- end
17
-
18
- def time
19
- 300
20
- end
21
-
22
- def run_now?
23
- false
24
- end
25
-
26
- def use_runner?
27
- false
28
- end
29
-
30
- def check_subtask?
31
- false
32
- end
33
-
34
- def generate_task?
35
- false
36
- end
37
- end
38
- end
39
- end
40
- end
41
- end
@@ -1,23 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'legion/extensions/governance/helpers/layers'
4
- require 'legion/extensions/governance/helpers/proposal'
5
- require 'legion/extensions/governance/runners/governance'
6
-
7
- module Legion
8
- module Extensions
9
- module Governance
10
- class Client
11
- include Runners::Governance
12
-
13
- def initialize(**)
14
- @proposal_store = Helpers::Proposal.new
15
- end
16
-
17
- private
18
-
19
- attr_reader :proposal_store
20
- end
21
- end
22
- end
23
- end
@@ -1,36 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Legion
4
- module Extensions
5
- module Governance
6
- module Helpers
7
- module Layers
8
- # Four governance layers (spec: governance-protocol-spec.md)
9
- GOVERNANCE_LAYERS = %i[agent_validation anomaly_detection human_deliberation transparency].freeze
10
-
11
- # Council quorum requirements
12
- MIN_COUNCIL_SIZE = 3
13
- QUORUM_FRACTION = 0.66
14
- VOTE_TIMEOUT = 86_400 # 24 hours
15
- PROPOSAL_CATEGORIES = %i[policy_change resource_allocation access_control emergency protocol_update].freeze
16
-
17
- module_function
18
-
19
- def valid_layer?(layer)
20
- GOVERNANCE_LAYERS.include?(layer)
21
- end
22
-
23
- def valid_category?(category)
24
- PROPOSAL_CATEGORIES.include?(category)
25
- end
26
-
27
- def quorum_met?(votes, council_size)
28
- return false if council_size < MIN_COUNCIL_SIZE
29
-
30
- votes >= (council_size * QUORUM_FRACTION).ceil
31
- end
32
- end
33
- end
34
- end
35
- end
36
- end
@@ -1,90 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'securerandom'
4
-
5
- module Legion
6
- module Extensions
7
- module Governance
8
- module Helpers
9
- class Proposal
10
- attr_reader :proposals
11
-
12
- def initialize
13
- @proposals = {}
14
- end
15
-
16
- def create(category:, description:, proposer:, council_size: Layers::MIN_COUNCIL_SIZE)
17
- id = SecureRandom.uuid
18
- @proposals[id] = {
19
- proposal_id: id,
20
- category: category,
21
- description: description,
22
- proposer: proposer,
23
- council_size: council_size,
24
- votes_for: [],
25
- votes_against: [],
26
- status: :open,
27
- created_at: Time.now.utc,
28
- resolved_at: nil
29
- }
30
- id
31
- end
32
-
33
- def vote(proposal_id, voter:, approve:)
34
- prop = @proposals[proposal_id]
35
- return nil unless prop && prop[:status] == :open
36
-
37
- # Prevent double-voting
38
- all_voters = prop[:votes_for] + prop[:votes_against]
39
- return :already_voted if all_voters.include?(voter)
40
-
41
- if approve
42
- prop[:votes_for] << voter
43
- else
44
- prop[:votes_against] << voter
45
- end
46
-
47
- check_resolution(proposal_id)
48
- end
49
-
50
- def get(proposal_id)
51
- @proposals[proposal_id]
52
- end
53
-
54
- def open_proposals
55
- @proposals.values.select { |p| p[:status] == :open }
56
- end
57
-
58
- def resolve_timed_out(proposal_id)
59
- prop = @proposals[proposal_id]
60
- return nil unless prop && prop[:status] == :open
61
-
62
- prop[:status] = :timed_out
63
- prop[:resolved_at] = Time.now.utc
64
- prop
65
- end
66
-
67
- private
68
-
69
- def check_resolution(proposal_id)
70
- prop = @proposals[proposal_id]
71
- total_votes = prop[:votes_for].size + prop[:votes_against].size
72
-
73
- if Layers.quorum_met?(prop[:votes_for].size, prop[:council_size])
74
- prop[:status] = :approved
75
- prop[:resolved_at] = Time.now.utc
76
- :approved
77
- elsif Layers.quorum_met?(prop[:votes_against].size, prop[:council_size]) ||
78
- total_votes >= prop[:council_size]
79
- prop[:status] = :rejected
80
- prop[:resolved_at] = Time.now.utc
81
- :rejected
82
- else
83
- :pending
84
- end
85
- end
86
- end
87
- end
88
- end
89
- end
90
- end
@@ -1,89 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Legion
4
- module Extensions
5
- module Governance
6
- module Runners
7
- module ShadowAi
8
- def scan_unregistered_extensions(**)
9
- installed = Bundler.load.specs.select { |s| s.name.start_with?('lex-') }.map(&:name)
10
- registered = registered_extension_names
11
-
12
- unregistered = installed - registered
13
- { installed: installed.size, registered: registered.size, unregistered: unregistered }
14
- rescue StandardError => e
15
- { installed: 0, registered: 0, unregistered: [], error: e.message }
16
- end
17
-
18
- def check_llm_bypass_indicators(**)
19
- indicators = []
20
- indicators << :direct_openai_key if ENV.key?('OPENAI_API_KEY') && !provider_enabled?(:openai)
21
- indicators << :direct_anthropic_key if ENV.key?('ANTHROPIC_API_KEY') && !provider_enabled?(:anthropic)
22
- { indicators: indicators, bypassed: !indicators.empty? }
23
- end
24
-
25
- def check_airb_compliance(**)
26
- return { checked: 0, source: :unavailable } unless defined?(Legion::Data::Model::DigitalWorker)
27
-
28
- workers = Legion::Data::Model::DigitalWorker.where(lifecycle_state: 'active').all
29
- non_compliant = workers.select do |w|
30
- risk = w.respond_to?(:risk_tier) ? w.risk_tier : nil
31
- %w[high critical].include?(risk) && w.respond_to?(:airb_status) && w.airb_status != 'approved'
32
- end
33
-
34
- { checked: workers.size, compliant: workers.size - non_compliant.size,
35
- non_compliant: non_compliant.map(&:worker_id) }
36
- rescue StandardError => e
37
- { checked: 0, error: e.message }
38
- end
39
-
40
- def full_scan(**)
41
- extensions = scan_unregistered_extensions
42
- bypass = check_llm_bypass_indicators
43
- compliance = check_airb_compliance
44
-
45
- has_issues = extensions[:unregistered]&.any? || bypass[:bypassed] || compliance[:non_compliant]&.any?
46
- emit_shadow_event(extensions, bypass, compliance) if has_issues
47
-
48
- { extensions: extensions, bypass: bypass, compliance: compliance, issues_found: has_issues }
49
- end
50
-
51
- private
52
-
53
- def registered_extension_names
54
- return [] unless defined?(Legion::Data) && Legion::Data.respond_to?(:connection)
55
-
56
- conn = Legion::Data.connection
57
- return [] unless conn&.table_exists?(:extension_registry)
58
-
59
- conn[:extension_registry].select_map(:gem_name)
60
- rescue StandardError
61
- []
62
- end
63
-
64
- def provider_enabled?(provider)
65
- llm = Legion::Settings[:llm]
66
- return false unless llm.is_a?(Hash)
67
-
68
- providers = llm[:providers]
69
- return false unless providers.is_a?(Hash)
70
-
71
- providers.dig(provider, :enabled) == true
72
- rescue StandardError
73
- false
74
- end
75
-
76
- def emit_shadow_event(extensions, bypass, compliance)
77
- return unless defined?(Legion::Events)
78
-
79
- Legion::Events.emit('governance.shadow_ai_detected', {
80
- unregistered: extensions[:unregistered],
81
- bypass: bypass[:indicators],
82
- non_compliant: compliance[:non_compliant]
83
- })
84
- end
85
- end
86
- end
87
- end
88
- end
89
- end
@@ -1,45 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Legion
4
- module Extensions
5
- module Actors
6
- class Every; end # rubocop:disable Lint/EmptyClass
7
- end
8
- end
9
- end
10
-
11
- $LOADED_FEATURES << 'legion/extensions/actors/every'
12
-
13
- require_relative '../../../../../lib/legion/extensions/governance/actors/vote_timeout'
14
-
15
- RSpec.describe Legion::Extensions::Governance::Actor::VoteTimeout do
16
- subject(:actor) { described_class.new }
17
-
18
- describe '#runner_class' do
19
- it { expect(actor.runner_class).to eq Legion::Extensions::Governance::Runners::Governance }
20
- end
21
-
22
- describe '#runner_function' do
23
- it { expect(actor.runner_function).to eq 'timeout_proposals' }
24
- end
25
-
26
- describe '#time' do
27
- it { expect(actor.time).to eq 300 }
28
- end
29
-
30
- describe '#run_now?' do
31
- it { expect(actor.run_now?).to be false }
32
- end
33
-
34
- describe '#use_runner?' do
35
- it { expect(actor.use_runner?).to be false }
36
- end
37
-
38
- describe '#check_subtask?' do
39
- it { expect(actor.check_subtask?).to be false }
40
- end
41
-
42
- describe '#generate_task?' do
43
- it { expect(actor.generate_task?).to be false }
44
- end
45
- end
@@ -1,14 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'legion/extensions/governance/client'
4
-
5
- RSpec.describe Legion::Extensions::Governance::Client do
6
- it 'responds to governance runner methods' do
7
- client = described_class.new
8
- expect(client).to respond_to(:create_proposal)
9
- expect(client).to respond_to(:vote_on_proposal)
10
- expect(client).to respond_to(:get_proposal)
11
- expect(client).to respond_to(:open_proposals)
12
- expect(client).to respond_to(:validate_action)
13
- end
14
- end
@@ -1,190 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'legion/extensions/governance/helpers/layers'
4
-
5
- RSpec.describe Legion::Extensions::Governance::Helpers::Layers do
6
- describe 'GOVERNANCE_LAYERS' do
7
- it 'contains four layers as symbols' do
8
- expect(described_class::GOVERNANCE_LAYERS.size).to eq(4)
9
- end
10
-
11
- it 'includes agent_validation' do
12
- expect(described_class::GOVERNANCE_LAYERS).to include(:agent_validation)
13
- end
14
-
15
- it 'includes anomaly_detection' do
16
- expect(described_class::GOVERNANCE_LAYERS).to include(:anomaly_detection)
17
- end
18
-
19
- it 'includes human_deliberation' do
20
- expect(described_class::GOVERNANCE_LAYERS).to include(:human_deliberation)
21
- end
22
-
23
- it 'includes transparency' do
24
- expect(described_class::GOVERNANCE_LAYERS).to include(:transparency)
25
- end
26
-
27
- it 'is frozen' do
28
- expect(described_class::GOVERNANCE_LAYERS).to be_frozen
29
- end
30
- end
31
-
32
- describe 'PROPOSAL_CATEGORIES' do
33
- it 'contains five categories' do
34
- expect(described_class::PROPOSAL_CATEGORIES.size).to eq(5)
35
- end
36
-
37
- it 'includes policy_change' do
38
- expect(described_class::PROPOSAL_CATEGORIES).to include(:policy_change)
39
- end
40
-
41
- it 'includes resource_allocation' do
42
- expect(described_class::PROPOSAL_CATEGORIES).to include(:resource_allocation)
43
- end
44
-
45
- it 'includes access_control' do
46
- expect(described_class::PROPOSAL_CATEGORIES).to include(:access_control)
47
- end
48
-
49
- it 'includes emergency' do
50
- expect(described_class::PROPOSAL_CATEGORIES).to include(:emergency)
51
- end
52
-
53
- it 'includes protocol_update' do
54
- expect(described_class::PROPOSAL_CATEGORIES).to include(:protocol_update)
55
- end
56
-
57
- it 'is frozen' do
58
- expect(described_class::PROPOSAL_CATEGORIES).to be_frozen
59
- end
60
- end
61
-
62
- describe 'numeric constants' do
63
- it 'sets MIN_COUNCIL_SIZE to 3' do
64
- expect(described_class::MIN_COUNCIL_SIZE).to eq(3)
65
- end
66
-
67
- it 'sets QUORUM_FRACTION to 0.66' do
68
- expect(described_class::QUORUM_FRACTION).to eq(0.66)
69
- end
70
-
71
- it 'sets VOTE_TIMEOUT to 86400' do
72
- expect(described_class::VOTE_TIMEOUT).to eq(86_400)
73
- end
74
- end
75
-
76
- describe '.valid_layer?' do
77
- it 'returns true for agent_validation' do
78
- expect(described_class.valid_layer?(:agent_validation)).to be true
79
- end
80
-
81
- it 'returns true for anomaly_detection' do
82
- expect(described_class.valid_layer?(:anomaly_detection)).to be true
83
- end
84
-
85
- it 'returns true for human_deliberation' do
86
- expect(described_class.valid_layer?(:human_deliberation)).to be true
87
- end
88
-
89
- it 'returns true for transparency' do
90
- expect(described_class.valid_layer?(:transparency)).to be true
91
- end
92
-
93
- it 'returns false for unknown layer symbol' do
94
- expect(described_class.valid_layer?(:unknown)).to be false
95
- end
96
-
97
- it 'returns false for nil' do
98
- expect(described_class.valid_layer?(nil)).to be false
99
- end
100
-
101
- it 'returns false for string version of valid layer' do
102
- expect(described_class.valid_layer?('agent_validation')).to be false
103
- end
104
- end
105
-
106
- describe '.valid_category?' do
107
- it 'returns true for policy_change' do
108
- expect(described_class.valid_category?(:policy_change)).to be true
109
- end
110
-
111
- it 'returns true for resource_allocation' do
112
- expect(described_class.valid_category?(:resource_allocation)).to be true
113
- end
114
-
115
- it 'returns true for access_control' do
116
- expect(described_class.valid_category?(:access_control)).to be true
117
- end
118
-
119
- it 'returns true for emergency' do
120
- expect(described_class.valid_category?(:emergency)).to be true
121
- end
122
-
123
- it 'returns true for protocol_update' do
124
- expect(described_class.valid_category?(:protocol_update)).to be true
125
- end
126
-
127
- it 'returns false for unknown category' do
128
- expect(described_class.valid_category?(:invalid)).to be false
129
- end
130
-
131
- it 'returns false for nil' do
132
- expect(described_class.valid_category?(nil)).to be false
133
- end
134
-
135
- it 'returns false for string version of valid category' do
136
- expect(described_class.valid_category?('emergency')).to be false
137
- end
138
- end
139
-
140
- describe '.quorum_met?' do
141
- context 'when council_size is below minimum' do
142
- it 'returns false for council_size of 2' do
143
- expect(described_class.quorum_met?(2, 2)).to be false
144
- end
145
-
146
- it 'returns false for council_size of 1' do
147
- expect(described_class.quorum_met?(1, 1)).to be false
148
- end
149
-
150
- it 'returns false for council_size of 0' do
151
- expect(described_class.quorum_met?(0, 0)).to be false
152
- end
153
- end
154
-
155
- context 'with minimum council size of 3' do
156
- it 'returns true when 2 of 3 vote (meets ceil(3 * 0.66) = 2)' do
157
- expect(described_class.quorum_met?(2, 3)).to be true
158
- end
159
-
160
- it 'returns true when all 3 vote' do
161
- expect(described_class.quorum_met?(3, 3)).to be true
162
- end
163
-
164
- it 'returns false when only 1 of 3 vote' do
165
- expect(described_class.quorum_met?(1, 3)).to be false
166
- end
167
- end
168
-
169
- context 'with larger council sizes' do
170
- it 'requires ceil(4 * 0.66) = 3 votes for council of 4' do
171
- expect(described_class.quorum_met?(3, 4)).to be true
172
- expect(described_class.quorum_met?(2, 4)).to be false
173
- end
174
-
175
- it 'requires ceil(5 * 0.66) = 4 votes for council of 5' do
176
- expect(described_class.quorum_met?(4, 5)).to be true
177
- expect(described_class.quorum_met?(3, 5)).to be false
178
- end
179
-
180
- it 'requires ceil(9 * 0.66) = 6 votes for council of 9' do
181
- expect(described_class.quorum_met?(6, 9)).to be true
182
- expect(described_class.quorum_met?(5, 9)).to be false
183
- end
184
- end
185
-
186
- it 'returns false for zero votes regardless of council size' do
187
- expect(described_class.quorum_met?(0, 3)).to be false
188
- end
189
- end
190
- end
@@ -1,188 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'legion/extensions/governance/helpers/layers'
4
- require 'legion/extensions/governance/helpers/proposal'
5
-
6
- RSpec.describe Legion::Extensions::Governance::Helpers::Proposal do
7
- subject(:proposal_store) { described_class.new }
8
-
9
- describe '#initialize' do
10
- it 'starts with an empty proposals hash' do
11
- expect(proposal_store.proposals).to eq({})
12
- end
13
- end
14
-
15
- describe '#create' do
16
- let(:created_id) do
17
- proposal_store.create(category: :policy_change, description: 'Enable new policy', proposer: 'agent-1')
18
- end
19
-
20
- it 'returns a UUID string' do
21
- expect(created_id).to match(/\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/)
22
- end
23
-
24
- it 'stores the proposal under the returned ID' do
25
- expect(proposal_store.proposals[created_id]).not_to be_nil
26
- end
27
-
28
- it 'stores the correct category' do
29
- expect(proposal_store.proposals[created_id][:category]).to eq(:policy_change)
30
- end
31
-
32
- it 'stores the correct description' do
33
- expect(proposal_store.proposals[created_id][:description]).to eq('Enable new policy')
34
- end
35
-
36
- it 'stores the correct proposer' do
37
- expect(proposal_store.proposals[created_id][:proposer]).to eq('agent-1')
38
- end
39
-
40
- it 'defaults council_size to MIN_COUNCIL_SIZE' do
41
- expect(proposal_store.proposals[created_id][:council_size])
42
- .to eq(Legion::Extensions::Governance::Helpers::Layers::MIN_COUNCIL_SIZE)
43
- end
44
-
45
- it 'uses the provided council_size when specified' do
46
- id = proposal_store.create(category: :emergency, description: 'urgent', proposer: 'agent-x', council_size: 7)
47
- expect(proposal_store.proposals[id][:council_size]).to eq(7)
48
- end
49
-
50
- it 'initializes with status :open' do
51
- expect(proposal_store.proposals[created_id][:status]).to eq(:open)
52
- end
53
-
54
- it 'initializes votes_for as empty array' do
55
- expect(proposal_store.proposals[created_id][:votes_for]).to eq([])
56
- end
57
-
58
- it 'initializes votes_against as empty array' do
59
- expect(proposal_store.proposals[created_id][:votes_against]).to eq([])
60
- end
61
-
62
- it 'sets created_at to a Time object' do
63
- expect(proposal_store.proposals[created_id][:created_at]).to be_a(Time)
64
- end
65
-
66
- it 'sets resolved_at to nil initially' do
67
- expect(proposal_store.proposals[created_id][:resolved_at]).to be_nil
68
- end
69
-
70
- it 'generates unique IDs for separate calls' do
71
- id1 = proposal_store.create(category: :policy_change, description: 'a', proposer: 'x')
72
- id2 = proposal_store.create(category: :policy_change, description: 'b', proposer: 'y')
73
- expect(id1).not_to eq(id2)
74
- end
75
- end
76
-
77
- describe '#get' do
78
- it 'returns the proposal hash for a valid ID' do
79
- id = proposal_store.create(category: :policy_change, description: 'test', proposer: 'agent-1')
80
- result = proposal_store.get(id)
81
- expect(result[:proposal_id]).to eq(id)
82
- end
83
-
84
- it 'returns nil for an unknown ID' do
85
- expect(proposal_store.get('nonexistent-uuid')).to be_nil
86
- end
87
- end
88
-
89
- describe '#open_proposals' do
90
- it 'returns empty array when no proposals exist' do
91
- expect(proposal_store.open_proposals).to eq([])
92
- end
93
-
94
- it 'returns all open proposals' do
95
- proposal_store.create(category: :policy_change, description: 'first', proposer: 'a1')
96
- proposal_store.create(category: :emergency, description: 'second', proposer: 'a2')
97
- expect(proposal_store.open_proposals.size).to eq(2)
98
- end
99
-
100
- it 'excludes resolved proposals' do
101
- id = proposal_store.create(category: :policy_change, description: 'vote me', proposer: 'a1', council_size: 3)
102
- proposal_store.vote(id, voter: 'v1', approve: true)
103
- proposal_store.vote(id, voter: 'v2', approve: true)
104
- expect(proposal_store.open_proposals).to be_empty
105
- end
106
- end
107
-
108
- describe '#vote' do
109
- let(:proposal_id) do
110
- proposal_store.create(category: :policy_change, description: 'test', proposer: 'agent-1', council_size: 3)
111
- end
112
-
113
- it 'returns nil for an unknown proposal ID' do
114
- expect(proposal_store.vote('no-such-id', voter: 'v1', approve: true)).to be_nil
115
- end
116
-
117
- it 'returns :already_voted when the same voter votes twice' do
118
- proposal_store.vote(proposal_id, voter: 'v1', approve: true)
119
- result = proposal_store.vote(proposal_id, voter: 'v1', approve: false)
120
- expect(result).to eq(:already_voted)
121
- end
122
-
123
- it 'prevents double voting even when switching approve/reject' do
124
- proposal_store.vote(proposal_id, voter: 'v1', approve: false)
125
- result = proposal_store.vote(proposal_id, voter: 'v1', approve: true)
126
- expect(result).to eq(:already_voted)
127
- end
128
-
129
- it 'adds approve vote to votes_for' do
130
- proposal_store.vote(proposal_id, voter: 'v1', approve: true)
131
- expect(proposal_store.proposals[proposal_id][:votes_for]).to include('v1')
132
- end
133
-
134
- it 'adds reject vote to votes_against' do
135
- proposal_store.vote(proposal_id, voter: 'v1', approve: false)
136
- expect(proposal_store.proposals[proposal_id][:votes_against]).to include('v1')
137
- end
138
-
139
- it 'returns :pending when quorum is not yet met' do
140
- result = proposal_store.vote(proposal_id, voter: 'v1', approve: true)
141
- expect(result).to eq(:pending)
142
- end
143
-
144
- context 'when quorum for approval is met' do
145
- it 'returns :approved and sets status' do
146
- proposal_store.vote(proposal_id, voter: 'v1', approve: true)
147
- result = proposal_store.vote(proposal_id, voter: 'v2', approve: true)
148
- expect(result).to eq(:approved)
149
- expect(proposal_store.proposals[proposal_id][:status]).to eq(:approved)
150
- end
151
-
152
- it 'sets resolved_at on approval' do
153
- proposal_store.vote(proposal_id, voter: 'v1', approve: true)
154
- proposal_store.vote(proposal_id, voter: 'v2', approve: true)
155
- expect(proposal_store.proposals[proposal_id][:resolved_at]).to be_a(Time)
156
- end
157
- end
158
-
159
- context 'when quorum for rejection is met' do
160
- it 'returns :rejected when enough votes against' do
161
- proposal_store.vote(proposal_id, voter: 'v1', approve: false)
162
- result = proposal_store.vote(proposal_id, voter: 'v2', approve: false)
163
- expect(result).to eq(:rejected)
164
- expect(proposal_store.proposals[proposal_id][:status]).to eq(:rejected)
165
- end
166
-
167
- it 'sets resolved_at on rejection' do
168
- proposal_store.vote(proposal_id, voter: 'v1', approve: false)
169
- proposal_store.vote(proposal_id, voter: 'v2', approve: false)
170
- expect(proposal_store.proposals[proposal_id][:resolved_at]).to be_a(Time)
171
- end
172
-
173
- it 'returns :rejected when all council members voted and no quorum for approval' do
174
- proposal_store.vote(proposal_id, voter: 'v1', approve: true)
175
- proposal_store.vote(proposal_id, voter: 'v2', approve: false)
176
- result = proposal_store.vote(proposal_id, voter: 'v3', approve: false)
177
- expect(result).to eq(:rejected)
178
- end
179
- end
180
-
181
- it 'returns nil when voting on a resolved (approved) proposal' do
182
- proposal_store.vote(proposal_id, voter: 'v1', approve: true)
183
- proposal_store.vote(proposal_id, voter: 'v2', approve: true)
184
- result = proposal_store.vote(proposal_id, voter: 'v3', approve: true)
185
- expect(result).to be_nil
186
- end
187
- end
188
- end
@@ -1,101 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'legion/extensions/governance/client'
4
-
5
- RSpec.describe Legion::Extensions::Governance::Runners::Governance do
6
- let(:client) { Legion::Extensions::Governance::Client.new }
7
-
8
- describe '#create_proposal' do
9
- it 'creates a proposal' do
10
- result = client.create_proposal(category: :policy_change, description: 'test', proposer: 'agent-1')
11
- expect(result[:proposal_id]).to match(/\A[0-9a-f-]{36}\z/)
12
- expect(result[:status]).to eq(:open)
13
- end
14
-
15
- it 'rejects invalid category' do
16
- result = client.create_proposal(category: :invalid, description: 'test', proposer: 'agent-1')
17
- expect(result[:error]).to eq(:invalid_category)
18
- end
19
- end
20
-
21
- describe '#vote_on_proposal' do
22
- it 'records a vote' do
23
- prop = client.create_proposal(category: :policy_change, description: 'test', proposer: 'agent-1', council_size: 3)
24
- result = client.vote_on_proposal(proposal_id: prop[:proposal_id], voter: 'agent-2', approve: true)
25
- expect(result[:voted]).to be true
26
- end
27
-
28
- it 'prevents double voting' do
29
- prop = client.create_proposal(category: :policy_change, description: 'test', proposer: 'agent-1', council_size: 3)
30
- client.vote_on_proposal(proposal_id: prop[:proposal_id], voter: 'agent-2', approve: true)
31
- result = client.vote_on_proposal(proposal_id: prop[:proposal_id], voter: 'agent-2', approve: true)
32
- expect(result[:error]).to eq(:already_voted)
33
- end
34
-
35
- it 'approves with quorum' do
36
- prop = client.create_proposal(category: :policy_change, description: 'test', proposer: 'agent-1', council_size: 3)
37
- client.vote_on_proposal(proposal_id: prop[:proposal_id], voter: 'v1', approve: true)
38
- result = client.vote_on_proposal(proposal_id: prop[:proposal_id], voter: 'v2', approve: true)
39
- expect(result[:resolution]).to eq(:approved)
40
- end
41
- end
42
-
43
- describe '#validate_action' do
44
- it 'allows agent_validation' do
45
- result = client.validate_action(layer: :agent_validation, action: 'test')
46
- expect(result[:allowed]).to be true
47
- end
48
-
49
- it 'blocks human_deliberation' do
50
- result = client.validate_action(layer: :human_deliberation, action: 'test')
51
- expect(result[:allowed]).to be false
52
- end
53
-
54
- it 'rejects invalid layer' do
55
- result = client.validate_action(layer: :invalid, action: 'test')
56
- expect(result[:error]).to eq(:invalid_layer)
57
- end
58
- end
59
-
60
- describe '#open_proposals' do
61
- it 'lists open proposals' do
62
- client.create_proposal(category: :policy_change, description: 'test', proposer: 'agent-1')
63
- result = client.open_proposals
64
- expect(result[:count]).to eq(1)
65
- end
66
- end
67
-
68
- describe '#timeout_proposals' do
69
- it 'returns zero timed_out when no proposals exist' do
70
- result = client.timeout_proposals
71
- expect(result[:checked]).to eq(0)
72
- expect(result[:timed_out]).to eq(0)
73
- expect(result[:timed_out_ids]).to eq([])
74
- end
75
-
76
- it 'returns zero timed_out for recently created proposals' do
77
- client.create_proposal(category: :policy_change, description: 'fresh', proposer: 'agent-1')
78
- result = client.timeout_proposals
79
- expect(result[:timed_out]).to eq(0)
80
- end
81
-
82
- it 'times out proposals older than VOTE_TIMEOUT' do
83
- prop = client.create_proposal(category: :policy_change, description: 'old', proposer: 'agent-1')
84
- store = client.instance_variable_get(:@proposal_store)
85
- store.proposals[prop[:proposal_id]][:created_at] = Time.now.utc - (Legion::Extensions::Governance::Helpers::Layers::VOTE_TIMEOUT + 1)
86
- result = client.timeout_proposals
87
- expect(result[:timed_out]).to eq(1)
88
- expect(result[:timed_out_ids]).to include(prop[:proposal_id])
89
- end
90
-
91
- it 'does not time out already-resolved proposals' do
92
- prop = client.create_proposal(category: :policy_change, description: 'resolved', proposer: 'agent-1', council_size: 3)
93
- store = client.instance_variable_get(:@proposal_store)
94
- store.proposals[prop[:proposal_id]][:created_at] = Time.now.utc - (Legion::Extensions::Governance::Helpers::Layers::VOTE_TIMEOUT + 1)
95
- client.vote_on_proposal(proposal_id: prop[:proposal_id], voter: 'v1', approve: true)
96
- client.vote_on_proposal(proposal_id: prop[:proposal_id], voter: 'v2', approve: true)
97
- result = client.timeout_proposals
98
- expect(result[:timed_out]).to eq(0)
99
- end
100
- end
101
- end
@@ -1,65 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'spec_helper'
4
-
5
- RSpec.describe Legion::Extensions::Governance::Runners::ShadowAi do
6
- let(:host) { Object.new.extend(described_class) }
7
-
8
- describe '#check_llm_bypass_indicators' do
9
- it 'detects direct API key when provider not enabled' do
10
- allow(ENV).to receive(:key?).and_call_original
11
- allow(ENV).to receive(:key?).with('OPENAI_API_KEY').and_return(true)
12
- allow(ENV).to receive(:key?).with('ANTHROPIC_API_KEY').and_return(false)
13
- allow(Legion::Settings).to receive(:[]).with(:llm).and_return(
14
- { providers: { openai: { enabled: false } } }
15
- )
16
- result = host.check_llm_bypass_indicators
17
- expect(result[:bypassed]).to be true
18
- expect(result[:indicators]).to include(:direct_openai_key)
19
- end
20
-
21
- it 'returns clean when no bypass indicators' do
22
- allow(ENV).to receive(:key?).with('OPENAI_API_KEY').and_return(false)
23
- allow(ENV).to receive(:key?).with('ANTHROPIC_API_KEY').and_return(false)
24
- result = host.check_llm_bypass_indicators
25
- expect(result[:bypassed]).to be false
26
- expect(result[:indicators]).to be_empty
27
- end
28
-
29
- it 'does not flag when provider is enabled' do
30
- allow(ENV).to receive(:key?).and_call_original
31
- allow(ENV).to receive(:key?).with('OPENAI_API_KEY').and_return(true)
32
- allow(ENV).to receive(:key?).with('ANTHROPIC_API_KEY').and_return(false)
33
- allow(Legion::Settings).to receive(:[]).with(:llm).and_return(
34
- { providers: { openai: { enabled: true } } }
35
- )
36
- result = host.check_llm_bypass_indicators
37
- expect(result[:bypassed]).to be false
38
- end
39
- end
40
-
41
- describe '#check_airb_compliance' do
42
- it 'returns unavailable when data model not loaded' do
43
- result = host.check_airb_compliance
44
- expect(result[:source]).to eq(:unavailable)
45
- end
46
- end
47
-
48
- describe '#full_scan' do
49
- it 'returns combined results' do
50
- allow(host).to receive(:scan_unregistered_extensions).and_return(
51
- { installed: 5, registered: 5, unregistered: [] }
52
- )
53
- allow(host).to receive(:check_llm_bypass_indicators).and_return(
54
- { indicators: [], bypassed: false }
55
- )
56
- allow(host).to receive(:check_airb_compliance).and_return(
57
- { checked: 0, source: :unavailable }
58
- )
59
-
60
- result = host.full_scan
61
- expect(result[:issues_found]).to be_falsey
62
- expect(result[:extensions][:installed]).to eq(5)
63
- end
64
- end
65
- end
data/spec/spec_helper.rb DELETED
@@ -1,46 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'bundler/setup'
4
-
5
- module Legion
6
- module Logging
7
- def self.debug(_msg); end
8
- def self.info(_msg); end
9
- def self.warn(_msg); end
10
- def self.error(_msg); end
11
- end
12
-
13
- module Extensions
14
- module Actors
15
- class Every; end # rubocop:disable Lint/EmptyClass
16
- end
17
- end
18
-
19
- module Settings
20
- @store = {}
21
-
22
- class << self
23
- def [](key)
24
- @store[key.to_sym] ||= {}
25
- end
26
-
27
- def reset!
28
- @store = {}
29
- end
30
- end
31
- end
32
-
33
- module Events
34
- def self.emit(_name, _payload); end
35
- end
36
- end
37
-
38
- $LOADED_FEATURES << 'legion/extensions/actors/every'
39
-
40
- require 'legion/extensions/governance'
41
-
42
- RSpec.configure do |config|
43
- config.example_status_persistence_file_path = '.rspec_status'
44
- config.disable_monkey_patching!
45
- config.expect_with(:rspec) { |c| c.syntax = :expect }
46
- end