lex-agentic-social 0.1.15 → 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 +6 -0
- data/lib/legion/extensions/agentic/social/calibration/runners/calibration.rb +8 -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 +18 -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,11 @@
|
|
|
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
|
+
|
|
3
9
|
## [0.1.15] - 2026-04-27
|
|
4
10
|
### Fixed
|
|
5
11
|
- Stop social calibration partner-knowledge promotion when Legion is shutting down or Apollo Local becomes unavailable between promotable tag groups
|
|
@@ -194,14 +194,17 @@ module Legion
|
|
|
194
194
|
|
|
195
195
|
base_tags = %w[partner preference llm_inference]
|
|
196
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
|
|
197
200
|
content = Legion::JSON.dump({
|
|
198
|
-
'domain' =>
|
|
199
|
-
'value' =>
|
|
201
|
+
'domain' => domain,
|
|
202
|
+
'value' => value,
|
|
200
203
|
'source' => 'llm_inference',
|
|
201
|
-
'confidence' =>
|
|
204
|
+
'confidence' => confidence
|
|
202
205
|
})
|
|
203
|
-
tags = base_tags + ["preference:#{
|
|
204
|
-
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)
|
|
205
208
|
end
|
|
206
209
|
end
|
|
207
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
|
|
@@ -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',
|