legionio 1.6.34 → 1.6.36

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.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +5 -0
  3. data/CHANGELOG.md +21 -0
  4. data/extensions-agentic/lex-consent/db/migrations/001_create_consent_maps.rb +24 -0
  5. data/extensions-agentic/lex-consent/lex-consent.gemspec +26 -0
  6. data/extensions-agentic/lex-consent/lib/legion/extensions/agentic/consent/actors/tier_evaluation.rb +112 -0
  7. data/extensions-agentic/lex-consent/lib/legion/extensions/agentic/consent/models/consent_map.rb +74 -0
  8. data/extensions-agentic/lex-consent/lib/legion/extensions/agentic/consent/runners/consent.rb +210 -0
  9. data/extensions-agentic/lex-consent/lib/legion/extensions/agentic/consent/version.rb +11 -0
  10. data/extensions-agentic/lex-consent/lib/legion/extensions/agentic/consent.rb +15 -0
  11. data/extensions-agentic/lex-reconciliation/Gemfile +10 -0
  12. data/extensions-agentic/lex-reconciliation/README.md +73 -0
  13. data/extensions-agentic/lex-reconciliation/lex-reconciliation.gemspec +26 -0
  14. data/extensions-agentic/lex-reconciliation/lib/legion/extensions/reconciliation/actors/reconciliation_cycle.rb +143 -0
  15. data/extensions-agentic/lex-reconciliation/lib/legion/extensions/reconciliation/drift_log.rb +161 -0
  16. data/extensions-agentic/lex-reconciliation/lib/legion/extensions/reconciliation/runners/drift_checker.rb +137 -0
  17. data/extensions-agentic/lex-reconciliation/lib/legion/extensions/reconciliation/version.rb +9 -0
  18. data/extensions-agentic/lex-reconciliation/lib/legion/extensions/reconciliation.rb +13 -0
  19. data/extensions-agentic/lex-reconciliation/spec/legion/extensions/reconciliation/drift_log_spec.rb +40 -0
  20. data/extensions-agentic/lex-reconciliation/spec/legion/extensions/reconciliation/runners/drift_checker_spec.rb +107 -0
  21. data/extensions-agentic/lex-reconciliation/spec/legion/extensions/reconciliation_spec.rb +19 -0
  22. data/extensions-agentic/lex-reconciliation/spec/spec_helper.rb +15 -0
  23. data/lib/legion/cli/workflow_command.rb +140 -0
  24. data/lib/legion/cli.rb +4 -0
  25. data/lib/legion/extensions/helpers/knowledge.rb +50 -7
  26. data/lib/legion/extensions/helpers/llm.rb +11 -8
  27. data/lib/legion/extensions/helpers/logger.rb +3 -0
  28. data/lib/legion/extensions/helpers/task.rb +3 -0
  29. data/lib/legion/prompts.rb +24 -0
  30. data/lib/legion/version.rb +1 -1
  31. data/lib/legion/workflow/loader.rb +122 -0
  32. data/lib/legion/workflow/manifest.rb +59 -0
  33. data/lib/legion/workflow.rb +8 -0
  34. data/lib/legion.rb +4 -3
  35. data/workflows/autonomous-github-lifecycle.yml +67 -0
  36. metadata +26 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f47bba61ed42d34114fc93779bc2c911b6dd67f03f6d06febaae9424bc0c61cf
4
- data.tar.gz: c4e45617785c4a31b32f9802b0ec77cc57dceb9ad223b6f71a35a9a54b21c119
3
+ metadata.gz: fd73445976635c61163ac93a058fd3253bb164956fff00d8b791817b1dd16ddb
4
+ data.tar.gz: 218c4b3975aa59cb4c0eec3735c2cdbe185cf23f85429b4268d4335a8cb7fdf9
5
5
  SHA512:
6
- metadata.gz: 92a3fed3a076dc34d63fdb69dbde68d3a73ce4f68c17652dfa862fbc355b8df9ac0e1023edd1a93020806d6af838071dd103dc176b6b32a0d03b51e1f03fec6c
7
- data.tar.gz: 13dfb3297160e508288d61f3713d8a97ee930b74cd58eab56311116ab2bbb065c88d20ba279ea6c55b6fb62af5623c8222bcf560a88aa0c93e8f24693ba38aca
6
+ metadata.gz: 247d06dcfb4d405c1a156bac4c9a4a0aaa00eedd0cf2f3bfef74da95f9027acb39f303bc9afda3dbf50d2f14dee2a41eafd10d5522dbf074017548489f69ebd0
7
+ data.tar.gz: 840d3c68d9e48e11e3c92a40cf2a0d849079d5ab68aa39629368f88a75615d1dca151b952987de9a882517c84569bcc7aff9c93becbd9d8277b98152a5fdc942
data/.rubocop.yml CHANGED
@@ -33,6 +33,11 @@ Metrics/BlockLength:
33
33
  Exclude:
34
34
  - 'spec/**/*'
35
35
  - 'integration/**/*'
36
+ - 'extensions-agentic/**/spec/**/*'
37
+ - 'extensions-core/**/spec/**/*'
38
+ - 'extensions/**/spec/**/*'
39
+ - 'extensions-ai/**/spec/**/*'
40
+ - 'extensions-other/**/spec/**/*'
36
41
  - 'legionio.gemspec'
37
42
  - 'lib/legion/cli/chat_command.rb'
38
43
  - 'lib/legion/cli/plan_command.rb'
data/CHANGELOG.md CHANGED
@@ -2,6 +2,27 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [1.6.36] - 2026-03-29
6
+
7
+ ### Added
8
+ - Knowledge helper: `knowledge_connected?`, `knowledge_global_connected?`, `knowledge_local_connected?` status methods
9
+ - Knowledge helper: `knowledge_default_scope` and `knowledge_default_tags` LEX-overridable layered defaults
10
+ - LLM helper: now includes `Legion::LLM::Helper` following cache/transport pattern (with LoadError guard)
11
+ - Wrapper specs for cache and data helpers
12
+
13
+ ### Fixed
14
+ - Logger helper: add missing `include Base` (was relying on transitive inclusion via Lex)
15
+ - Task helper: add missing `include Base`
16
+ - Knowledge helper: add missing `include Base`, `knowledge_default_tags` auto-merged into `ingest_knowledge`
17
+
18
+ ## [1.6.35] - 2026-03-29
19
+
20
+ ### Added
21
+ - `Legion::Workflow::Manifest` — YAML workflow manifest parser with validation
22
+ - `Legion::Workflow::Loader` — installs/uninstalls workflow chains via lex-lex registry
23
+ - `legion workflow` CLI — install, list, uninstall, status subcommands
24
+ - `workflows/autonomous-github-lifecycle.yml` — sample workflow manifest for codegen pipeline
25
+
5
26
  ## [1.6.34] - 2026-03-29
6
27
 
7
28
  ### Fixed
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ Sequel.migration do
4
+ change do
5
+ create_table(:consent_maps) do
6
+ primary_key :id
7
+ String :worker_id, null: false
8
+ String :from_tier, null: false
9
+ String :to_tier, null: false
10
+ String :requested_by, null: false
11
+ String :state, null: false, default: 'pending_approval'
12
+ String :resolved_by
13
+ Time :resolved_at
14
+ String :notes, text: true
15
+ String :context, text: true
16
+ Time :created_at
17
+ Time :updated_at
18
+
19
+ index :worker_id
20
+ index :state
21
+ index %i[worker_id state]
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/legion/extensions/agentic/consent/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'lex-consent'
7
+ spec.version = Legion::Extensions::Agentic::Consent::VERSION
8
+ spec.authors = ['Esity']
9
+ spec.email = ['matthewdiverson@gmail.com']
10
+ spec.summary = 'LegionIO HITL consent gate for autonomous tier promotion'
11
+ spec.description = 'A LegionIO Extension (LEX) that gates agent autonomous tier promotion by human approval'
12
+ spec.homepage = 'https://github.com/LegionIO/lex-consent'
13
+ spec.license = 'MIT'
14
+ spec.required_ruby_version = '>= 3.4'
15
+
16
+ spec.metadata = {
17
+ 'homepage_uri' => spec.homepage,
18
+ 'source_code_uri' => spec.homepage,
19
+ 'rubygems_mfa_required' => 'true'
20
+ }
21
+
22
+ spec.files = Dir['lib/**/*', 'LICENSE', 'README.md']
23
+ spec.require_paths = ['lib']
24
+
25
+ spec.add_dependency 'legionio', '>= 1.2'
26
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ return unless defined?(Legion::Extensions::Actors::Every)
4
+
5
+ module Legion
6
+ module Extensions
7
+ module Agentic
8
+ module Consent
9
+ module Actor
10
+ class TierEvaluation < Legion::Extensions::Actors::Every
11
+ # Run tier evaluation and pending approval expiry every hour
12
+ INTERVAL = 3600
13
+
14
+ def perform
15
+ expire_stale_approvals
16
+ evaluate_pending_workers
17
+ rescue StandardError => e
18
+ Legion::Logging.error "[TierEvaluation] perform failed: #{e.message}" if defined?(Legion::Logging)
19
+ end
20
+
21
+ private
22
+
23
+ def expire_stale_approvals
24
+ return unless runner_available?
25
+
26
+ runner = runner_instance
27
+ ttl_hours = Legion::Settings.dig(:consent, :pending_ttl_hours) || 72
28
+ result = runner.expire_pending_approvals(ttl_hours: ttl_hours)
29
+ return unless result[:expired].to_i.positive? && defined?(Legion::Logging)
30
+
31
+ Legion::Logging.info "[TierEvaluation] expired #{result[:expired]} stale consent requests"
32
+ rescue StandardError => e
33
+ Legion::Logging.warn "[TierEvaluation] expire_stale_approvals failed: #{e.message}" if defined?(Legion::Logging)
34
+ end
35
+
36
+ def evaluate_pending_workers
37
+ return unless defined?(Legion::Data::Model::DigitalWorker)
38
+ return unless defined?(Legion::Extensions::Agentic::Consent::Models::ConsentMap)
39
+
40
+ # Find active workers that may be eligible for autonomous tier promotion
41
+ # but do not yet have a pending approval request.
42
+ active_workers = Legion::Data::Model::DigitalWorker
43
+ .where(lifecycle_state: 'active')
44
+ .exclude(consent_tier: 'autonomous')
45
+ .all
46
+
47
+ active_workers.each do |worker|
48
+ evaluate_worker_for_promotion(worker)
49
+ rescue StandardError => e
50
+ Legion::Logging.warn "[TierEvaluation] evaluate failed for worker=#{worker.worker_id}: #{e.message}" if defined?(Legion::Logging)
51
+ end
52
+ rescue StandardError => e
53
+ Legion::Logging.warn "[TierEvaluation] evaluate_pending_workers failed: #{e.message}" if defined?(Legion::Logging)
54
+ end
55
+
56
+ def evaluate_worker_for_promotion(worker)
57
+ return unless promotion_eligible?(worker)
58
+ return if pending_request_exists?(worker.worker_id)
59
+
60
+ from_tier = worker.consent_tier
61
+ to_tier = next_tier(from_tier)
62
+ return unless to_tier
63
+
64
+ runner = runner_instance
65
+ runner.request_promotion(
66
+ worker_id: worker.worker_id,
67
+ from_tier: from_tier,
68
+ to_tier: to_tier,
69
+ requested_by: 'system:tier_evaluation'
70
+ )
71
+ end
72
+
73
+ def promotion_eligible?(worker)
74
+ return false unless worker.trust_score.to_f >= trust_threshold
75
+ return false unless (worker.risk_tier || 'low') == 'low'
76
+
77
+ true
78
+ end
79
+
80
+ def trust_threshold
81
+ Legion::Settings.dig(:consent, :promotion_trust_threshold) || 0.85
82
+ rescue StandardError
83
+ 0.85
84
+ end
85
+
86
+ def next_tier(current_tier)
87
+ hierarchy = %w[supervised inform consult autonomous]
88
+ idx = hierarchy.index(current_tier)
89
+ return nil unless idx
90
+ return nil if idx >= hierarchy.length - 1
91
+
92
+ hierarchy[idx + 1]
93
+ end
94
+
95
+ def pending_request_exists?(worker_id)
96
+ Legion::Extensions::Agentic::Consent::Models::ConsentMap
97
+ .pending_for_worker(worker_id).any?
98
+ end
99
+
100
+ def runner_available?
101
+ defined?(Legion::Extensions::Agentic::Consent::Runners::Consent)
102
+ end
103
+
104
+ def runner_instance
105
+ Object.new.extend(Legion::Extensions::Agentic::Consent::Runners::Consent)
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ return unless defined?(Legion::Data)
4
+
5
+ module Legion
6
+ module Extensions
7
+ module Agentic
8
+ module Consent
9
+ module Models
10
+ class ConsentMap < Legion::Data::Model::Base
11
+ set_dataset :consent_maps
12
+
13
+ STATES = %w[pending_approval approved rejected expired].freeze
14
+
15
+ def self.pending
16
+ where(state: 'pending_approval')
17
+ end
18
+
19
+ def self.for_worker(worker_id)
20
+ where(worker_id: worker_id)
21
+ end
22
+
23
+ def self.pending_for_worker(worker_id)
24
+ where(worker_id: worker_id, state: 'pending_approval')
25
+ end
26
+
27
+ def approve!(approver:, notes: nil)
28
+ update(
29
+ state: 'approved',
30
+ resolved_by: approver,
31
+ resolved_at: Time.now.utc,
32
+ notes: notes,
33
+ updated_at: Time.now.utc
34
+ )
35
+ end
36
+
37
+ def reject!(approver:, reason: nil)
38
+ update(
39
+ state: 'rejected',
40
+ resolved_by: approver,
41
+ resolved_at: Time.now.utc,
42
+ notes: reason,
43
+ updated_at: Time.now.utc
44
+ )
45
+ end
46
+
47
+ def expire!
48
+ update(
49
+ state: 'expired',
50
+ updated_at: Time.now.utc
51
+ )
52
+ end
53
+
54
+ def pending?
55
+ state == 'pending_approval'
56
+ end
57
+
58
+ def approved?
59
+ state == 'approved'
60
+ end
61
+
62
+ def rejected?
63
+ state == 'rejected'
64
+ end
65
+
66
+ def expired?
67
+ state == 'expired'
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,210 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Agentic
6
+ module Consent
7
+ module Runners
8
+ module Consent
9
+ # Request human approval for a worker's autonomous tier promotion.
10
+ # Creates a ConsentMap record in pending_approval state.
11
+ #
12
+ # @param worker_id [String] the worker requesting promotion
13
+ # @param from_tier [String] current consent tier
14
+ # @param to_tier [String] requested consent tier
15
+ # @param requested_by [String] identity requesting the promotion
16
+ # @param context [Hash] optional metadata about why promotion is requested
17
+ # @return [Hash]
18
+ def request_promotion(worker_id:, from_tier:, to_tier:, **opts)
19
+ requested_by = opts.fetch(:requested_by, 'system')
20
+ context = opts.fetch(:context, {})
21
+
22
+ return { success: false, reason: :model_unavailable } unless defined?(Legion::Extensions::Agentic::Consent::Models::ConsentMap)
23
+
24
+ existing = Legion::Extensions::Agentic::Consent::Models::ConsentMap
25
+ .pending_for_worker(worker_id).first
26
+
27
+ if existing
28
+ Legion::Logging.info "[lex-consent] promotion already pending for worker=#{worker_id}" if defined?(Legion::Logging)
29
+ return { success: false, reason: :already_pending, consent_map_id: existing.id }
30
+ end
31
+
32
+ record = Legion::Extensions::Agentic::Consent::Models::ConsentMap.create(
33
+ worker_id: worker_id,
34
+ from_tier: from_tier,
35
+ to_tier: to_tier,
36
+ requested_by: requested_by,
37
+ state: 'pending_approval',
38
+ context: defined?(Legion::JSON) ? Legion::JSON.dump(context) : context.to_json,
39
+ created_at: Time.now.utc,
40
+ updated_at: Time.now.utc
41
+ )
42
+
43
+ if defined?(Legion::Events)
44
+ Legion::Events.emit('consent.promotion_requested', {
45
+ worker_id: worker_id,
46
+ from_tier: from_tier,
47
+ to_tier: to_tier,
48
+ requested_by: requested_by,
49
+ consent_map_id: record.id,
50
+ at: Time.now.utc
51
+ })
52
+ end
53
+
54
+ Legion::Logging.info "[lex-consent] promotion requested worker=#{worker_id} #{from_tier}->#{to_tier} id=#{record.id}" if defined?(Legion::Logging)
55
+
56
+ { success: true, consent_map_id: record.id, state: 'pending_approval' }
57
+ rescue StandardError => e
58
+ Legion::Logging.error "[lex-consent] request_promotion failed: #{e.message}" if defined?(Legion::Logging)
59
+ { success: false, reason: e.message }
60
+ end
61
+
62
+ # Approve a pending tier promotion request.
63
+ #
64
+ # @param consent_map_id [Integer] the ConsentMap record to approve
65
+ # @param approver [String] identity of the approver
66
+ # @param notes [String] optional approval notes
67
+ # @return [Hash]
68
+ def approve_promotion(consent_map_id:, approver:, notes: nil, **)
69
+ return { success: false, reason: :model_unavailable } unless defined?(Legion::Extensions::Agentic::Consent::Models::ConsentMap)
70
+
71
+ record = Legion::Extensions::Agentic::Consent::Models::ConsentMap[consent_map_id.to_i]
72
+ return { success: false, reason: :not_found } unless record
73
+ return { success: false, reason: :not_pending, state: record.state } unless record.pending?
74
+
75
+ record.approve!(approver: approver, notes: notes)
76
+
77
+ apply_promotion(record)
78
+
79
+ if defined?(Legion::Events)
80
+ Legion::Events.emit('consent.promotion_approved', {
81
+ consent_map_id: record.id,
82
+ worker_id: record.worker_id,
83
+ from_tier: record.from_tier,
84
+ to_tier: record.to_tier,
85
+ approver: approver,
86
+ at: Time.now.utc
87
+ })
88
+ end
89
+
90
+ Legion::Logging.info "[lex-consent] approved consent_map_id=#{record.id} worker=#{record.worker_id} by=#{approver}" if defined?(Legion::Logging)
91
+
92
+ { success: true, consent_map_id: record.id, worker_id: record.worker_id, state: 'approved', to_tier: record.to_tier }
93
+ rescue StandardError => e
94
+ Legion::Logging.error "[lex-consent] approve_promotion failed: #{e.message}" if defined?(Legion::Logging)
95
+ { success: false, reason: e.message }
96
+ end
97
+
98
+ # Reject a pending tier promotion request.
99
+ #
100
+ # @param consent_map_id [Integer] the ConsentMap record to reject
101
+ # @param approver [String] identity of the approver
102
+ # @param reason [String] rejection reason (required)
103
+ # @return [Hash]
104
+ def reject_promotion(consent_map_id:, approver:, reason:, **)
105
+ return { success: false, reason: :model_unavailable } unless defined?(Legion::Extensions::Agentic::Consent::Models::ConsentMap)
106
+
107
+ return { success: false, reason: :missing_reason } if reason.nil? || reason.to_s.strip.empty?
108
+
109
+ record = Legion::Extensions::Agentic::Consent::Models::ConsentMap[consent_map_id.to_i]
110
+ return { success: false, reason: :not_found } unless record
111
+ return { success: false, reason: :not_pending, state: record.state } unless record.pending?
112
+
113
+ record.reject!(approver: approver, reason: reason)
114
+
115
+ if defined?(Legion::Events)
116
+ Legion::Events.emit('consent.promotion_rejected', {
117
+ consent_map_id: record.id,
118
+ worker_id: record.worker_id,
119
+ from_tier: record.from_tier,
120
+ to_tier: record.to_tier,
121
+ approver: approver,
122
+ reason: reason,
123
+ at: Time.now.utc
124
+ })
125
+ end
126
+
127
+ Legion::Logging.info "[lex-consent] rejected consent_map_id=#{record.id} worker=#{record.worker_id} by=#{approver}" if defined?(Legion::Logging)
128
+
129
+ { success: true, consent_map_id: record.id, worker_id: record.worker_id, state: 'rejected' }
130
+ rescue StandardError => e
131
+ Legion::Logging.error "[lex-consent] reject_promotion failed: #{e.message}" if defined?(Legion::Logging)
132
+ { success: false, reason: e.message }
133
+ end
134
+
135
+ # Expire all pending promotion requests older than ttl_hours.
136
+ # Intended to be run on a schedule (e.g. every hour).
137
+ #
138
+ # @param ttl_hours [Integer] how many hours before a pending request expires (default 72)
139
+ # @return [Hash]
140
+ def expire_pending_approvals(ttl_hours: 72, **)
141
+ return { success: false, reason: :model_unavailable } unless defined?(Legion::Extensions::Agentic::Consent::Models::ConsentMap)
142
+
143
+ cutoff = Time.now.utc - (ttl_hours * 3600)
144
+ expired_count = 0
145
+
146
+ Legion::Extensions::Agentic::Consent::Models::ConsentMap
147
+ .pending
148
+ .where { created_at < cutoff }
149
+ .each do |record|
150
+ record.expire!
151
+ expired_count += 1
152
+
153
+ if defined?(Legion::Events)
154
+ Legion::Events.emit('consent.promotion_expired', {
155
+ consent_map_id: record.id,
156
+ worker_id: record.worker_id,
157
+ from_tier: record.from_tier,
158
+ to_tier: record.to_tier,
159
+ at: Time.now.utc
160
+ })
161
+ end
162
+ rescue StandardError => e
163
+ Legion::Logging.warn "[lex-consent] expire failed for id=#{record.id}: #{e.message}" if defined?(Legion::Logging)
164
+ end
165
+
166
+ Legion::Logging.info "[lex-consent] expired #{expired_count} pending approvals (ttl=#{ttl_hours}h)" if defined?(Legion::Logging)
167
+
168
+ { success: true, expired: expired_count, ttl_hours: ttl_hours }
169
+ rescue StandardError => e
170
+ Legion::Logging.error "[lex-consent] expire_pending_approvals failed: #{e.message}" if defined?(Legion::Logging)
171
+ { success: false, reason: e.message }
172
+ end
173
+
174
+ # List pending promotion requests.
175
+ #
176
+ # @param worker_id [String] optional filter by worker
177
+ # @return [Hash]
178
+ def list_pending(worker_id: nil, **)
179
+ return { success: false, reason: :model_unavailable } unless defined?(Legion::Extensions::Agentic::Consent::Models::ConsentMap)
180
+
181
+ ds = Legion::Extensions::Agentic::Consent::Models::ConsentMap.pending
182
+ ds = ds.where(worker_id: worker_id) if worker_id
183
+ records = ds.all
184
+
185
+ { success: true, count: records.size, pending: records.map(&:values) }
186
+ rescue StandardError => e
187
+ Legion::Logging.error "[lex-consent] list_pending failed: #{e.message}" if defined?(Legion::Logging)
188
+ { success: false, reason: e.message }
189
+ end
190
+
191
+ private
192
+
193
+ def apply_promotion(record)
194
+ return unless defined?(Legion::Data::Model::DigitalWorker)
195
+
196
+ worker = Legion::Data::Model::DigitalWorker.first(worker_id: record.worker_id)
197
+ return unless worker
198
+
199
+ worker.update(consent_tier: record.to_tier, updated_at: Time.now.utc)
200
+
201
+ Legion::Logging.info "[lex-consent] applied tier promotion worker=#{record.worker_id} tier=#{record.to_tier}" if defined?(Legion::Logging)
202
+ rescue StandardError => e
203
+ Legion::Logging.warn "[lex-consent] apply_promotion failed for worker=#{record.worker_id}: #{e.message}" if defined?(Legion::Logging)
204
+ end
205
+ end
206
+ end
207
+ end
208
+ end
209
+ end
210
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Agentic
6
+ module Consent
7
+ VERSION = '0.1.0'
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'consent/version'
4
+ require_relative 'consent/models/consent_map'
5
+ require_relative 'consent/runners/consent'
6
+ require_relative 'consent/actors/tier_evaluation'
7
+
8
+ module Legion
9
+ module Extensions
10
+ module Agentic
11
+ module Consent
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+ gemspec
5
+
6
+ group :development, :test do
7
+ gem 'rspec', '~> 3.12'
8
+ gem 'rubocop', '~> 1.50'
9
+ gem 'rubocop-rspec', '~> 2.20'
10
+ end
@@ -0,0 +1,73 @@
1
+ # lex-reconciliation
2
+
3
+ A [LegionIO](https://github.com/LegionIO) extension for drift detection and reconciliation.
4
+
5
+ Detects drift between expected (desired) state and actual (observed) state for managed resources,
6
+ persists drift events to a log, and runs periodic reconciliation cycles that emit events for
7
+ downstream runners to act on.
8
+
9
+ ## Components
10
+
11
+ ### `Runners::DriftChecker`
12
+
13
+ Detects drift between expected and actual state for one or more resources.
14
+
15
+ ruby
16
+ result = drift_checker.check(
17
+ resource: 'my-service',
18
+ expected: { status: 'running', replicas: 3 },
19
+ actual: { status: 'stopped', replicas: 1 },
20
+ severity: 'high'
21
+ )
22
+ # => { drifted: true, drift_id: '...', differences: [...], summary: { total: 2 } }
23
+
24
+
25
+ ### `DriftLog`
26
+
27
+ Persistent drift event log backed by `legion-data`.
28
+
29
+ ruby
30
+ Legion::Extensions::Reconciliation::DriftLog.record(
31
+ resource: 'my-service',
32
+ expected: { status: 'running' },
33
+ actual: { status: 'stopped' },
34
+ severity: 'high'
35
+ )
36
+
37
+ Legion::Extensions::Reconciliation::DriftLog.open_entries(severity: 'high')
38
+ Legion::Extensions::Reconciliation::DriftLog.summary
39
+
40
+
41
+ ### `Actors::ReconciliationCycle`
42
+
43
+ Interval actor (default: every 5 minutes) that checks all configured targets and emits
44
+ `reconciliation.drift_detected` and `reconciliation.reconcile_requested` events.
45
+
46
+ Configure targets in settings:
47
+
48
+
49
+ {
50
+ "extensions": {
51
+ "reconciliation": {
52
+ "interval": 300,
53
+ "targets": [
54
+ {
55
+ "resource": "my-service",
56
+ "expected": { "status": "running", "replicas": 3 },
57
+ "severity": "high"
58
+ }
59
+ ]
60
+ }
61
+ }
62
+ }
63
+
64
+
65
+ ## Installation
66
+
67
+ ruby
68
+ gem 'lex-reconciliation'
69
+
70
+
71
+ ## License
72
+
73
+ MIT
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/legion/extensions/reconciliation/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'lex-reconciliation'
7
+ spec.version = Legion::Extensions::Reconciliation::VERSION
8
+ spec.authors = ['Esity']
9
+ spec.email = ['matthewdiverson@gmail.com']
10
+ spec.summary = 'A LegionIO Extension for drift detection and reconciliation'
11
+ spec.description = 'A LegionIO Extension (LEX) for detecting drift between expected and actual state and reconciling differences'
12
+ spec.homepage = 'https://github.com/LegionIO/lex-reconciliation'
13
+ spec.license = 'MIT'
14
+ spec.required_ruby_version = '>= 3.4'
15
+
16
+ spec.metadata = {
17
+ 'homepage_uri' => spec.homepage,
18
+ 'source_code_uri' => spec.homepage,
19
+ 'rubygems_mfa_required' => 'true'
20
+ }
21
+
22
+ spec.files = Dir['lib/**/*', 'LICENSE', 'README.md']
23
+ spec.require_paths = ['lib']
24
+
25
+ spec.add_dependency 'legionio', '>= 1.2'
26
+ end