lex-agentic-social 0.1.14 → 0.1.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +10 -0
- data/lib/legion/extensions/agentic/social/calibration/runners/calibration.rb +20 -5
- data/lib/legion/extensions/agentic/social/conflict/runners/conflict.rb +10 -2
- data/lib/legion/extensions/agentic/social/consent/helpers/consent_map.rb +2 -2
- data/lib/legion/extensions/agentic/social/governance/runners/shadow_ai.rb +4 -2
- data/lib/legion/extensions/agentic/social/moral_reasoning/helpers/moral_engine.rb +3 -3
- data/lib/legion/extensions/agentic/social/version.rb +1 -1
- data/spec/legion/extensions/agentic/social/calibration/runners/calibration_spec.rb +44 -0
- data/spec/legion/extensions/agentic/social/conflict/runners/conflict_spec.rb +11 -0
- data/spec/legion/extensions/agentic/social/consent/local_persistence_spec.rb +19 -0
- data/spec/legion/extensions/agentic/social/governance/runners/shadow_ai_spec.rb +15 -0
- data/spec/legion/extensions/agentic/social/moral_reasoning/helpers/moral_engine_spec.rb +19 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e415a1e8df317c84447b21607aa89a50091a52ee603c103532434e613b5c79f8
|
|
4
|
+
data.tar.gz: '0485cc912a12c05e87ed0d6a89f34a20e678f92bdb1fd33f40bee1f03fd5f6ce'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 73b527381fc34d087abce74fd3fa0f704bd8c2717c5100b2b822a83c8a392c9a79aa5a44e78d9667d6429b733390d31056c1573661e03696ea317fdb25fecb30
|
|
7
|
+
data.tar.gz: 37e46998cc523737472ca6884db0a24320c18e252190cef94fefed44e70b70f5c60c57fa30f20832d7aed9cc847fb6d90615d1a7589b754e12099cadde1edbaf
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.1.16] - 2026-05-07
|
|
4
|
+
### Fixed
|
|
5
|
+
- Calibration preference storage now handles symbol-keyed parsed LLM preferences without dropping domain or value.
|
|
6
|
+
- Consent restore preserves pending approval fields after local DB reloads.
|
|
7
|
+
- Shadow AI scans flag scan failures as issues, conflict stale checks parse restored string timestamps, and moral reasoning reinforcement uses dilemma severity.
|
|
8
|
+
|
|
9
|
+
## [0.1.15] - 2026-04-27
|
|
10
|
+
### Fixed
|
|
11
|
+
- Stop social calibration partner-knowledge promotion when Legion is shutting down or Apollo Local becomes unavailable between promotable tag groups
|
|
12
|
+
|
|
3
13
|
## [0.1.14] - 2026-04-22
|
|
4
14
|
### Added
|
|
5
15
|
- `Governance#review_transition` method — API expected by lex-extinction for containment governance gate
|
|
@@ -79,10 +79,14 @@ module Legion
|
|
|
79
79
|
end
|
|
80
80
|
|
|
81
81
|
def promote_partner_knowledge(**)
|
|
82
|
+
return { success: true, skipped: :shutting_down } if shutting_down?
|
|
82
83
|
return { success: true, skipped: :local_unavailable } unless apollo_local_available?
|
|
83
84
|
|
|
84
85
|
total = 0
|
|
85
86
|
Helpers::Constants::PROMOTABLE_TAGS.each do |tags|
|
|
87
|
+
return { success: true, promoted: total, stopped: :shutting_down } if shutting_down?
|
|
88
|
+
return { success: true, promoted: total, stopped: :local_unavailable } unless apollo_local_available?
|
|
89
|
+
|
|
86
90
|
result = Legion::Apollo::Local.promote_to_global(tags: tags, min_confidence: Helpers::Constants::PROMOTION_MIN_CONFIDENCE)
|
|
87
91
|
total += result[:promoted] if result[:success]
|
|
88
92
|
end
|
|
@@ -112,6 +116,14 @@ module Legion
|
|
|
112
116
|
defined?(Legion::Apollo::Local) && Legion::Apollo::Local.started?
|
|
113
117
|
end
|
|
114
118
|
|
|
119
|
+
def shutting_down?
|
|
120
|
+
return Legion.shutting_down? if defined?(Legion) && Legion.respond_to?(:shutting_down?)
|
|
121
|
+
|
|
122
|
+
defined?(Legion::Settings) && Legion::Settings.dig(:client, :shutting_down) == true
|
|
123
|
+
rescue StandardError => _e
|
|
124
|
+
false
|
|
125
|
+
end
|
|
126
|
+
|
|
115
127
|
def retrieve_interaction_traces
|
|
116
128
|
traces = retrieve_from_memory
|
|
117
129
|
return traces if traces.any?
|
|
@@ -182,14 +194,17 @@ module Legion
|
|
|
182
194
|
|
|
183
195
|
base_tags = %w[partner preference llm_inference]
|
|
184
196
|
preferences.each do |pref|
|
|
197
|
+
domain = pref[:domain] || pref['domain']
|
|
198
|
+
value = pref[:value] || pref['value']
|
|
199
|
+
confidence = pref[:confidence] || pref['confidence'] || 0.65
|
|
185
200
|
content = Legion::JSON.dump({
|
|
186
|
-
'domain' =>
|
|
187
|
-
'value' =>
|
|
201
|
+
'domain' => domain,
|
|
202
|
+
'value' => value,
|
|
188
203
|
'source' => 'llm_inference',
|
|
189
|
-
'confidence' =>
|
|
204
|
+
'confidence' => confidence
|
|
190
205
|
})
|
|
191
|
-
tags = base_tags + ["preference:#{
|
|
192
|
-
Legion::Apollo::Local.upsert(content: content, tags: tags, confidence:
|
|
206
|
+
tags = base_tags + ["preference:#{domain}"]
|
|
207
|
+
Legion::Apollo::Local.upsert(content: content, tags: tags, confidence: confidence)
|
|
193
208
|
end
|
|
194
209
|
end
|
|
195
210
|
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'time'
|
|
4
|
+
|
|
3
5
|
module Legion
|
|
4
6
|
module Extensions
|
|
5
7
|
module Agentic
|
|
@@ -70,12 +72,12 @@ module Legion
|
|
|
70
72
|
|
|
71
73
|
def check_stale_conflicts(**)
|
|
72
74
|
active = conflict_log.active_conflicts
|
|
73
|
-
stale = active.select { |c|
|
|
75
|
+
stale = active.select { |c| conflict_age_seconds(c) > Helpers::Severity::STALE_CONFLICT_TIMEOUT }
|
|
74
76
|
stale.each do |c|
|
|
75
77
|
message = 'conflict marked stale — no resolution after 24h'
|
|
76
78
|
|
|
77
79
|
if Helpers::LlmEnhancer.available?
|
|
78
|
-
age_hours = (
|
|
80
|
+
age_hours = conflict_age_seconds(c) / 3600.0
|
|
79
81
|
analysis = Helpers::LlmEnhancer.analyze_stale_conflict(
|
|
80
82
|
description: c[:description],
|
|
81
83
|
severity: c[:severity],
|
|
@@ -103,6 +105,12 @@ module Legion
|
|
|
103
105
|
def conflict_log
|
|
104
106
|
@conflict_log ||= Helpers::ConflictLog.new
|
|
105
107
|
end
|
|
108
|
+
|
|
109
|
+
def conflict_age_seconds(conflict)
|
|
110
|
+
created_at = conflict[:created_at]
|
|
111
|
+
created_at = Time.parse(created_at) if created_at.is_a?(String)
|
|
112
|
+
Time.now.utc - created_at
|
|
113
|
+
end
|
|
106
114
|
end
|
|
107
115
|
end
|
|
108
116
|
end
|
|
@@ -170,14 +170,14 @@ module Legion
|
|
|
170
170
|
[]
|
|
171
171
|
end
|
|
172
172
|
|
|
173
|
-
@domains[key]
|
|
173
|
+
@domains[key].merge!(
|
|
174
174
|
tier: row[:tier].to_sym,
|
|
175
175
|
success_count: row[:success_count].to_i,
|
|
176
176
|
failure_count: row[:failure_count].to_i,
|
|
177
177
|
total_actions: row[:total_actions].to_i,
|
|
178
178
|
last_changed_at: row[:last_changed_at],
|
|
179
179
|
history: history
|
|
180
|
-
|
|
180
|
+
)
|
|
181
181
|
end
|
|
182
182
|
rescue StandardError => e
|
|
183
183
|
Legion::Logging.warn "[consent] load_from_local failed: #{e.message}"
|
|
@@ -14,7 +14,8 @@ module Legion
|
|
|
14
14
|
unregistered = installed - registered
|
|
15
15
|
{ installed: installed.size, registered: registered.size, unregistered: unregistered }
|
|
16
16
|
rescue StandardError => e
|
|
17
|
-
|
|
17
|
+
Legion::Logging.warn("[governance:shadow_ai] extension scan failed: #{e.message}")
|
|
18
|
+
{ installed: 0, registered: 0, unregistered: [], scan_failed: true, error: e.message }
|
|
18
19
|
end
|
|
19
20
|
|
|
20
21
|
def check_llm_bypass_indicators(**)
|
|
@@ -47,7 +48,8 @@ module Legion
|
|
|
47
48
|
bypass = check_llm_bypass_indicators
|
|
48
49
|
compliance = check_airb_compliance
|
|
49
50
|
|
|
50
|
-
has_issues = extensions[:
|
|
51
|
+
has_issues = extensions[:scan_failed] || extensions[:unregistered]&.any? ||
|
|
52
|
+
bypass[:bypassed] || compliance[:non_compliant]&.any?
|
|
51
53
|
emit_shadow_event(extensions, bypass, compliance) if has_issues
|
|
52
54
|
|
|
53
55
|
{ extensions: extensions, bypass: bypass, compliance: compliance, issues_found: has_issues }
|
|
@@ -117,7 +117,7 @@ module Legion
|
|
|
117
117
|
return { success: false, reason: :invalid_option } unless chosen
|
|
118
118
|
|
|
119
119
|
dilemma.resolve(option_id: option_id, reasoning: reasoning, framework: framework)
|
|
120
|
-
reinforce_chosen_foundations(chosen)
|
|
120
|
+
reinforce_chosen_foundations(chosen, severity: dilemma.severity)
|
|
121
121
|
weaken_unchosen_foundations(dilemma.options, option_id)
|
|
122
122
|
add_history(type: :resolution, dilemma_id: dilemma_id, option_id: option_id,
|
|
123
123
|
framework: framework, severity: dilemma.severity)
|
|
@@ -210,9 +210,9 @@ module Legion
|
|
|
210
210
|
end
|
|
211
211
|
end
|
|
212
212
|
|
|
213
|
-
def reinforce_chosen_foundations(chosen_option)
|
|
213
|
+
def reinforce_chosen_foundations(chosen_option, severity:)
|
|
214
214
|
chosen_option.fetch(:foundations, []).each do |fid|
|
|
215
|
-
@foundations[fid]&.reinforce(amount:
|
|
215
|
+
@foundations[fid]&.reinforce(amount: severity)
|
|
216
216
|
end
|
|
217
217
|
end
|
|
218
218
|
|
|
@@ -70,6 +70,24 @@ RSpec.describe Legion::Extensions::Agentic::Social::Calibration::Runners::Calibr
|
|
|
70
70
|
result = client.extract_preferences_via_llm
|
|
71
71
|
expect(result[:skipped]).to eq(:llm_unavailable)
|
|
72
72
|
end
|
|
73
|
+
|
|
74
|
+
it 'stores symbol-keyed parsed preferences with domain and value intact' do
|
|
75
|
+
mock_local = double('apollo_local')
|
|
76
|
+
stub_const('Legion::Apollo', Module.new)
|
|
77
|
+
stub_const('Legion::Apollo::Local', mock_local)
|
|
78
|
+
allow(mock_local).to receive(:started?).and_return(true)
|
|
79
|
+
allow(mock_local).to receive(:upsert)
|
|
80
|
+
|
|
81
|
+
client.send(:store_llm_preferences, [{ domain: 'tone', value: 'concise', confidence: 0.8 }])
|
|
82
|
+
|
|
83
|
+
expect(mock_local).to have_received(:upsert) do |args|
|
|
84
|
+
content = Legion::JSON.load(args[:content])
|
|
85
|
+
expect(content[:domain]).to eq('tone')
|
|
86
|
+
expect(content[:value]).to eq('concise')
|
|
87
|
+
expect(args[:tags]).to include('preference:tone')
|
|
88
|
+
expect(args[:confidence]).to eq(0.8)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
73
91
|
end
|
|
74
92
|
|
|
75
93
|
describe '#retrieve_from_memory' do
|
|
@@ -146,6 +164,32 @@ RSpec.describe Legion::Extensions::Agentic::Social::Calibration::Runners::Calibr
|
|
|
146
164
|
result = client.promote_partner_knowledge
|
|
147
165
|
expect(result[:skipped]).to eq(:local_unavailable)
|
|
148
166
|
end
|
|
167
|
+
|
|
168
|
+
it 'skips when Legion is shutting down' do
|
|
169
|
+
allow(Legion::Settings).to receive(:dig).with(:client, :shutting_down).and_return(true)
|
|
170
|
+
|
|
171
|
+
result = client.promote_partner_knowledge
|
|
172
|
+
expect(result[:skipped]).to eq(:shutting_down)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
it 'stops when Apollo Local becomes unavailable between tag groups' do
|
|
176
|
+
mock_local = double('apollo_local')
|
|
177
|
+
stub_const('Legion::Apollo', Module.new)
|
|
178
|
+
stub_const('Legion::Apollo::Local', mock_local)
|
|
179
|
+
allow(Legion::Settings).to receive(:dig).with(:client, :shutting_down).and_return(false)
|
|
180
|
+
allow(mock_local).to receive(:started?).and_return(true, true, false)
|
|
181
|
+
expect(mock_local).to receive(:promote_to_global)
|
|
182
|
+
.with(
|
|
183
|
+
tags: Legion::Extensions::Agentic::Social::Calibration::Helpers::Constants::PROMOTABLE_TAGS.first,
|
|
184
|
+
min_confidence: Legion::Extensions::Agentic::Social::Calibration::Helpers::Constants::PROMOTION_MIN_CONFIDENCE
|
|
185
|
+
)
|
|
186
|
+
.once
|
|
187
|
+
.and_return({ success: true, promoted: 2 })
|
|
188
|
+
|
|
189
|
+
result = client.promote_partner_knowledge
|
|
190
|
+
expect(result[:promoted]).to eq(2)
|
|
191
|
+
expect(result[:stopped]).to eq(:local_unavailable)
|
|
192
|
+
end
|
|
149
193
|
end
|
|
150
194
|
|
|
151
195
|
describe '#sync_partner_knowledge' do
|
|
@@ -78,6 +78,17 @@ RSpec.describe Legion::Extensions::Agentic::Social::Conflict::Runners::Conflict
|
|
|
78
78
|
expect(result[:stale_ids]).to include(c[:conflict_id])
|
|
79
79
|
end
|
|
80
80
|
|
|
81
|
+
it 'detects stale conflicts restored with string created_at values' do
|
|
82
|
+
c = client.register_conflict(parties: %w[a b], severity: :medium, description: 'old')
|
|
83
|
+
conflict = client.instance_variable_get(:@conflict_log).conflicts[c[:conflict_id]]
|
|
84
|
+
conflict[:created_at] = (Time.now.utc - (Legion::Extensions::Agentic::Social::Conflict::Helpers::Severity::STALE_CONFLICT_TIMEOUT + 1)).iso8601
|
|
85
|
+
|
|
86
|
+
result = client.check_stale_conflicts
|
|
87
|
+
|
|
88
|
+
expect(result[:stale_count]).to eq(1)
|
|
89
|
+
expect(result[:stale_ids]).to include(c[:conflict_id])
|
|
90
|
+
end
|
|
91
|
+
|
|
81
92
|
it 'does not include resolved conflicts in stale check' do
|
|
82
93
|
c = client.register_conflict(parties: %w[a b], severity: :low, description: 'resolved')
|
|
83
94
|
conflict = client.instance_variable_get(:@conflict_log).conflicts[c[:conflict_id]]
|
|
@@ -122,6 +122,25 @@ RSpec.describe 'lex-consent local SQLite persistence' do
|
|
|
122
122
|
expect(map.domains['email'][:total_actions]).to eq(6)
|
|
123
123
|
end
|
|
124
124
|
|
|
125
|
+
it 'restores pending fields with default nil values' do
|
|
126
|
+
db[:consent_domains].insert(
|
|
127
|
+
domain_key: 'email',
|
|
128
|
+
tier: 'consult',
|
|
129
|
+
success_count: 1,
|
|
130
|
+
failure_count: 0,
|
|
131
|
+
total_actions: 1,
|
|
132
|
+
history: '[]'
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
map = Legion::Extensions::Agentic::Social::Consent::Helpers::ConsentMap.new
|
|
136
|
+
expect(map.pending?('email')).to be false
|
|
137
|
+
expect(map.domains['email']).to include(
|
|
138
|
+
pending_tier: nil,
|
|
139
|
+
pending_since: nil,
|
|
140
|
+
pending_requested_by: nil
|
|
141
|
+
)
|
|
142
|
+
end
|
|
143
|
+
|
|
125
144
|
it 'restores history as an array of hashes with symbol keys' do
|
|
126
145
|
history_json = JSON.generate([{ 'from' => 'consult', 'to' => 'act_notify', 'at' => Time.now.utc.to_s }])
|
|
127
146
|
db[:consent_domains].insert(
|
|
@@ -75,5 +75,20 @@ RSpec.describe Legion::Extensions::Agentic::Social::Governance::Runners::ShadowA
|
|
|
75
75
|
expect(result[:issues_found]).to be_falsey
|
|
76
76
|
expect(result[:extensions][:installed]).to eq(5)
|
|
77
77
|
end
|
|
78
|
+
|
|
79
|
+
it 'treats extension scan failure as an issue' do
|
|
80
|
+
allow(host).to receive(:scan_unregistered_extensions).and_return(
|
|
81
|
+
{ installed: 0, registered: 0, unregistered: [], scan_failed: true, error: 'Gemfile not found' }
|
|
82
|
+
)
|
|
83
|
+
allow(host).to receive(:check_llm_bypass_indicators).and_return(
|
|
84
|
+
{ indicators: [], bypassed: false }
|
|
85
|
+
)
|
|
86
|
+
allow(host).to receive(:check_airb_compliance).and_return(
|
|
87
|
+
{ checked: 0, source: :unavailable }
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
result = host.full_scan
|
|
91
|
+
expect(result[:issues_found]).to be true
|
|
92
|
+
end
|
|
78
93
|
end
|
|
79
94
|
end
|
|
@@ -89,6 +89,25 @@ RSpec.describe Legion::Extensions::Agentic::Social::MoralReasoning::Helpers::Mor
|
|
|
89
89
|
expect(result[:dilemma][:resolved]).to be true
|
|
90
90
|
end
|
|
91
91
|
|
|
92
|
+
it 'uses the dilemma severity when reinforcing chosen foundations' do
|
|
93
|
+
dilemma_id = engine.pose_dilemma(
|
|
94
|
+
description: 'Low severity',
|
|
95
|
+
options: options,
|
|
96
|
+
severity: 0.2
|
|
97
|
+
)[:dilemma][:id]
|
|
98
|
+
before_weight = engine.foundation_profile[:care][:weight]
|
|
99
|
+
|
|
100
|
+
engine.resolve_dilemma(
|
|
101
|
+
dilemma_id: dilemma_id,
|
|
102
|
+
option_id: 'opt_a',
|
|
103
|
+
reasoning: 'Least harm',
|
|
104
|
+
framework: :care_ethics
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
after_weight = engine.foundation_profile[:care][:weight]
|
|
108
|
+
expect(after_weight - before_weight).to be_within(0.001).of(0.02)
|
|
109
|
+
end
|
|
110
|
+
|
|
92
111
|
it 'returns failure for unknown dilemma_id' do
|
|
93
112
|
result = engine.resolve_dilemma(
|
|
94
113
|
dilemma_id: 'nonexistent', option_id: 'opt_a',
|