lex-agentic-social 0.1.15 → 0.1.17
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 +11 -0
- data/lib/legion/extensions/agentic/social/calibration/runners/calibration.rb +11 -5
- data/lib/legion/extensions/agentic/social/conflict/helpers/llm_enhancer.rb +8 -3
- 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/llm_enhancer.rb +8 -3
- 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 +66 -0
- data/spec/legion/extensions/agentic/social/conflict/helpers/llm_enhancer_spec.rb +88 -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/llm_enhancer_spec.rb +104 -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: 2fc82cf70d0df9c909097bbed4d53fc58e64e2ca8904a49c6d7abf030fc3b2c6
|
|
4
|
+
data.tar.gz: 41e0f3966d50d002ccd5cf3a96d488b7bb1d2208cc4f36b931a5fd311786c80a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e013c357c5281df840b02a4ec06d16fee47d63af742b97ae1a6fe132612b66f6c8464b0316b1d564e6cc3927da03b8b21629500fc52685206af0ef28649865f6
|
|
7
|
+
data.tar.gz: 01a2eb7ebda54716bfb68542f22622aedf96525b98809a84d758f1fd2c8fc44d7eb6e5a02cfbf1354da466cbfebe786b99fed33212d537df1bb9cc1963bb7eb2
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.1.17] - 2026-05-15
|
|
4
|
+
### Fixed
|
|
5
|
+
- Social LLM enhancers now send explicit system and user messages to native `Legion::LLM.chat` dispatch instead of opening legacy nil-input chat sessions.
|
|
6
|
+
- Calibration LLM preference upsert now passes `access_scope: 'private'` and `identity_principal_id: nil` to prevent daemon process identity injection on personal data writes.
|
|
7
|
+
|
|
8
|
+
## [0.1.16] - 2026-05-07
|
|
9
|
+
### Fixed
|
|
10
|
+
- Calibration preference storage now handles symbol-keyed parsed LLM preferences without dropping domain or value.
|
|
11
|
+
- Consent restore preserves pending approval fields after local DB reloads.
|
|
12
|
+
- Shadow AI scans flag scan failures as issues, conflict stale checks parse restored string timestamps, and moral reasoning reinforcement uses dilemma severity.
|
|
13
|
+
|
|
3
14
|
## [0.1.15] - 2026-04-27
|
|
4
15
|
### Fixed
|
|
5
16
|
- Stop social calibration partner-knowledge promotion when Legion is shutting down or Apollo Local becomes unavailable between promotable tag groups
|
|
@@ -194,14 +194,20 @@ 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,
|
|
206
|
+
tags = base_tags + ["preference:#{domain}"]
|
|
207
|
+
Legion::Apollo::Local.upsert(content: content, tags: tags,
|
|
208
|
+
confidence: confidence,
|
|
209
|
+
access_scope: 'private',
|
|
210
|
+
identity_principal_id: nil)
|
|
205
211
|
end
|
|
206
212
|
end
|
|
207
213
|
end
|
|
@@ -57,9 +57,14 @@ module Legion
|
|
|
57
57
|
content = response&.message&.dig(:content)
|
|
58
58
|
::Struct.new(:content).new(content) if content
|
|
59
59
|
else
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
60
|
+
response = Legion::LLM.chat(
|
|
61
|
+
message: [
|
|
62
|
+
{ role: 'system', content: SYSTEM_PROMPT },
|
|
63
|
+
{ role: 'user', content: prompt }
|
|
64
|
+
],
|
|
65
|
+
caller: { extension: 'lex-agentic-social', mode: :conflict }
|
|
66
|
+
)
|
|
67
|
+
response.respond_to?(:content) ? response : response.ask(prompt)
|
|
63
68
|
end
|
|
64
69
|
end
|
|
65
70
|
private_class_method :llm_ask
|
|
@@ -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 }
|
|
@@ -50,9 +50,14 @@ module Legion
|
|
|
50
50
|
content = response&.message&.dig(:content)
|
|
51
51
|
::Struct.new(:content).new(content) if content
|
|
52
52
|
else
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
response = Legion::LLM.chat(
|
|
54
|
+
message: [
|
|
55
|
+
{ role: 'system', content: SYSTEM_PROMPT },
|
|
56
|
+
{ role: 'user', content: prompt }
|
|
57
|
+
],
|
|
58
|
+
caller: { extension: 'lex-agentic-social', mode: :moral_reasoning }
|
|
59
|
+
)
|
|
60
|
+
response.respond_to?(:content) ? response : response.ask(prompt)
|
|
56
61
|
end
|
|
57
62
|
end
|
|
58
63
|
private_class_method :llm_ask
|
|
@@ -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
|
|
@@ -182,4 +200,52 @@ RSpec.describe Legion::Extensions::Agentic::Social::Calibration::Runners::Calibr
|
|
|
182
200
|
expect(result[:results]).to have_key(:promotion)
|
|
183
201
|
end
|
|
184
202
|
end
|
|
203
|
+
|
|
204
|
+
describe '#store_llm_preferences' do
|
|
205
|
+
let(:preferences) do
|
|
206
|
+
[{ 'domain' => 'communication', 'value' => 'direct', 'confidence' => 0.8 }]
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
before do
|
|
210
|
+
mock_local = double('apollo_local')
|
|
211
|
+
stub_const('Legion::Apollo', Module.new)
|
|
212
|
+
stub_const('Legion::Apollo::Local', mock_local)
|
|
213
|
+
allow(mock_local).to receive(:started?).and_return(true)
|
|
214
|
+
allow(mock_local).to receive(:upsert).and_return({ success: true })
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
it 'passes access_scope: private to Apollo::Local.upsert' do
|
|
218
|
+
mock_local = Legion::Apollo::Local
|
|
219
|
+
client.send(:store_llm_preferences, preferences)
|
|
220
|
+
expect(mock_local).to have_received(:upsert).with(
|
|
221
|
+
hash_including(access_scope: 'private')
|
|
222
|
+
)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
it 'passes content and tags to Apollo::Local.upsert' do
|
|
226
|
+
mock_local = Legion::Apollo::Local
|
|
227
|
+
client.send(:store_llm_preferences, preferences)
|
|
228
|
+
expect(mock_local).to have_received(:upsert).with(
|
|
229
|
+
hash_including(
|
|
230
|
+
content: a_string_including('direct'),
|
|
231
|
+
tags: array_including('preference', 'preference:communication')
|
|
232
|
+
)
|
|
233
|
+
)
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
it 'does not inject process identity as the owner' do
|
|
237
|
+
stub_const('Legion::Identity::Process', Module.new do
|
|
238
|
+
extend self
|
|
239
|
+
|
|
240
|
+
define_method(:identity_hash) do
|
|
241
|
+
{ canonical_name: 'daemon', db_principal_id: 999, db_identity_id: 888 }
|
|
242
|
+
end
|
|
243
|
+
end)
|
|
244
|
+
mock_local = Legion::Apollo::Local
|
|
245
|
+
client.send(:store_llm_preferences, preferences)
|
|
246
|
+
expect(mock_local).to have_received(:upsert).with(
|
|
247
|
+
hash_including(identity_principal_id: nil)
|
|
248
|
+
)
|
|
249
|
+
end
|
|
250
|
+
end
|
|
185
251
|
end
|
|
@@ -112,6 +112,53 @@ RSpec.describe Legion::Extensions::Agentic::Social::Conflict::Helpers::LlmEnhanc
|
|
|
112
112
|
expect(result).to be_nil
|
|
113
113
|
end
|
|
114
114
|
end
|
|
115
|
+
|
|
116
|
+
context 'when LLM returns a native content response (direct dispatch path)' do
|
|
117
|
+
let(:native_response) do
|
|
118
|
+
double(content: "OUTCOME: resolved\nNOTES: Native dispatch resolved the conflict directly.")
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
before do
|
|
122
|
+
allow(Legion::LLM).to receive(:chat).and_return(native_response)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
it 'calls Legion::LLM.chat with system and user messages' do
|
|
126
|
+
expect(Legion::LLM).to receive(:chat).with(
|
|
127
|
+
hash_including(
|
|
128
|
+
message: array_including(
|
|
129
|
+
hash_including(role: 'system'),
|
|
130
|
+
hash_including(role: 'user')
|
|
131
|
+
)
|
|
132
|
+
)
|
|
133
|
+
).and_return(native_response)
|
|
134
|
+
described_class.suggest_resolution(
|
|
135
|
+
description: 'Agent and human disagree',
|
|
136
|
+
severity: :medium,
|
|
137
|
+
exchanges: []
|
|
138
|
+
)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
it 'calls Legion::LLM.chat with caller metadata' do
|
|
142
|
+
expect(Legion::LLM).to receive(:chat).with(
|
|
143
|
+
hash_including(caller: { extension: 'lex-agentic-social', mode: :conflict })
|
|
144
|
+
).and_return(native_response)
|
|
145
|
+
described_class.suggest_resolution(
|
|
146
|
+
description: 'Agent and human disagree',
|
|
147
|
+
severity: :medium,
|
|
148
|
+
exchanges: []
|
|
149
|
+
)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
it 'uses the content directly without calling ask' do
|
|
153
|
+
expect(native_response).not_to receive(:ask)
|
|
154
|
+
result = described_class.suggest_resolution(
|
|
155
|
+
description: 'Agent and human disagree',
|
|
156
|
+
severity: :medium,
|
|
157
|
+
exchanges: []
|
|
158
|
+
)
|
|
159
|
+
expect(result[:suggested_outcome]).to eq(:resolved)
|
|
160
|
+
end
|
|
161
|
+
end
|
|
115
162
|
end
|
|
116
163
|
|
|
117
164
|
describe '.analyze_stale_conflict' do
|
|
@@ -185,5 +232,46 @@ RSpec.describe Legion::Extensions::Agentic::Social::Conflict::Helpers::LlmEnhanc
|
|
|
185
232
|
expect(result).to be_nil
|
|
186
233
|
end
|
|
187
234
|
end
|
|
235
|
+
|
|
236
|
+
context 'when LLM returns a native content response (direct dispatch path)' do
|
|
237
|
+
let(:native_response) do
|
|
238
|
+
double(content: "RECOMMENDATION: escalate\nANALYSIS: Native dispatch recommends escalation.")
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
before do
|
|
242
|
+
allow(Legion::LLM).to receive(:chat).and_return(native_response)
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
it 'calls Legion::LLM.chat with system and user messages' do
|
|
246
|
+
expect(Legion::LLM).to receive(:chat).with(
|
|
247
|
+
hash_including(
|
|
248
|
+
message: array_including(
|
|
249
|
+
hash_including(role: 'system'),
|
|
250
|
+
hash_including(role: 'user')
|
|
251
|
+
)
|
|
252
|
+
)
|
|
253
|
+
).and_return(native_response)
|
|
254
|
+
described_class.analyze_stale_conflict(
|
|
255
|
+
description: 'Stale safety concern', severity: :high, age_hours: 40.0, exchange_count: 1
|
|
256
|
+
)
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
it 'calls Legion::LLM.chat with caller metadata' do
|
|
260
|
+
expect(Legion::LLM).to receive(:chat).with(
|
|
261
|
+
hash_including(caller: { extension: 'lex-agentic-social', mode: :conflict })
|
|
262
|
+
).and_return(native_response)
|
|
263
|
+
described_class.analyze_stale_conflict(
|
|
264
|
+
description: 'Stale safety concern', severity: :high, age_hours: 40.0, exchange_count: 1
|
|
265
|
+
)
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
it 'uses the content directly without calling ask' do
|
|
269
|
+
expect(native_response).not_to receive(:ask)
|
|
270
|
+
result = described_class.analyze_stale_conflict(
|
|
271
|
+
description: 'Stale safety concern', severity: :high, age_hours: 40.0, exchange_count: 1
|
|
272
|
+
)
|
|
273
|
+
expect(result[:recommendation]).to eq(:escalate)
|
|
274
|
+
end
|
|
275
|
+
end
|
|
188
276
|
end
|
|
189
277
|
end
|
|
@@ -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
|
|
@@ -150,6 +150,57 @@ RSpec.describe Legion::Extensions::Agentic::Social::MoralReasoning::Helpers::Llm
|
|
|
150
150
|
expect(result).to be_nil
|
|
151
151
|
end
|
|
152
152
|
end
|
|
153
|
+
|
|
154
|
+
context 'when LLM returns a native content response (direct dispatch path)' do
|
|
155
|
+
let(:native_response) do
|
|
156
|
+
content = <<~TEXT
|
|
157
|
+
REASONING: Native dispatch evaluated this action as broadly positive.
|
|
158
|
+
IMPACT: care=0.4 | fairness=0.3 | loyalty=0.1 | authority=0.0 | sanctity=0.2 | liberty=0.1
|
|
159
|
+
TEXT
|
|
160
|
+
double('native_response', content: content)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
before do
|
|
164
|
+
allow(Legion::LLM).to receive(:chat).and_return(native_response)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
it 'calls Legion::LLM.chat with system and user messages' do
|
|
168
|
+
expect(Legion::LLM).to receive(:chat).with(
|
|
169
|
+
hash_including(
|
|
170
|
+
message: array_including(
|
|
171
|
+
hash_including(role: 'system'),
|
|
172
|
+
hash_including(role: 'user')
|
|
173
|
+
)
|
|
174
|
+
)
|
|
175
|
+
).and_return(native_response)
|
|
176
|
+
enhancer.evaluate_action(
|
|
177
|
+
action: :help_stranger,
|
|
178
|
+
description: 'Helping someone in need',
|
|
179
|
+
foundations: foundations
|
|
180
|
+
)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
it 'calls Legion::LLM.chat with caller metadata' do
|
|
184
|
+
expect(Legion::LLM).to receive(:chat).with(
|
|
185
|
+
hash_including(caller: { extension: 'lex-agentic-social', mode: :moral_reasoning })
|
|
186
|
+
).and_return(native_response)
|
|
187
|
+
enhancer.evaluate_action(
|
|
188
|
+
action: :help_stranger,
|
|
189
|
+
description: 'Helping someone in need',
|
|
190
|
+
foundations: foundations
|
|
191
|
+
)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
it 'uses the content directly without calling ask' do
|
|
195
|
+
expect(native_response).not_to receive(:ask)
|
|
196
|
+
result = enhancer.evaluate_action(
|
|
197
|
+
action: :help_stranger,
|
|
198
|
+
description: 'Helping someone in need',
|
|
199
|
+
foundations: foundations
|
|
200
|
+
)
|
|
201
|
+
expect(result[:foundation_impacts]).to include(:care, :fairness)
|
|
202
|
+
end
|
|
203
|
+
end
|
|
153
204
|
end
|
|
154
205
|
|
|
155
206
|
describe '.resolve_dilemma' do
|
|
@@ -228,5 +279,58 @@ RSpec.describe Legion::Extensions::Agentic::Social::MoralReasoning::Helpers::Llm
|
|
|
228
279
|
expect(result).to be_nil
|
|
229
280
|
end
|
|
230
281
|
end
|
|
282
|
+
|
|
283
|
+
context 'when LLM returns a native content response (direct dispatch path)' do
|
|
284
|
+
let(:native_response) do
|
|
285
|
+
content = <<~TEXT
|
|
286
|
+
CHOSEN: opt_b
|
|
287
|
+
CONFIDENCE: 0.75
|
|
288
|
+
REASONING: Native dispatch selected option b based on deontological constraints.
|
|
289
|
+
TEXT
|
|
290
|
+
double('native_response', content: content)
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
before do
|
|
294
|
+
allow(Legion::LLM).to receive(:chat).and_return(native_response)
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
it 'calls Legion::LLM.chat with system and user messages' do
|
|
298
|
+
expect(Legion::LLM).to receive(:chat).with(
|
|
299
|
+
hash_including(
|
|
300
|
+
message: array_including(
|
|
301
|
+
hash_including(role: 'system'),
|
|
302
|
+
hash_including(role: 'user')
|
|
303
|
+
)
|
|
304
|
+
)
|
|
305
|
+
).and_return(native_response)
|
|
306
|
+
enhancer.resolve_dilemma(
|
|
307
|
+
dilemma_description: 'Should the agent reveal sensitive information?',
|
|
308
|
+
options: options,
|
|
309
|
+
framework: :deontological
|
|
310
|
+
)
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
it 'calls Legion::LLM.chat with caller metadata' do
|
|
314
|
+
expect(Legion::LLM).to receive(:chat).with(
|
|
315
|
+
hash_including(caller: { extension: 'lex-agentic-social', mode: :moral_reasoning })
|
|
316
|
+
).and_return(native_response)
|
|
317
|
+
enhancer.resolve_dilemma(
|
|
318
|
+
dilemma_description: 'Should the agent reveal sensitive information?',
|
|
319
|
+
options: options,
|
|
320
|
+
framework: :deontological
|
|
321
|
+
)
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
it 'uses the content directly without calling ask' do
|
|
325
|
+
expect(native_response).not_to receive(:ask)
|
|
326
|
+
result = enhancer.resolve_dilemma(
|
|
327
|
+
dilemma_description: 'Should the agent reveal sensitive information?',
|
|
328
|
+
options: options,
|
|
329
|
+
framework: :deontological
|
|
330
|
+
)
|
|
331
|
+
expect(result[:chosen_option]).to eq('opt_b')
|
|
332
|
+
expect(result[:confidence]).to eq(0.75)
|
|
333
|
+
end
|
|
334
|
+
end
|
|
231
335
|
end
|
|
232
336
|
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',
|