legion-gaia 0.9.62 → 0.9.63

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: 71bb865569a9d7730467a7ef628b7d4518a138356a7643e193a48ab3007373a1
4
- data.tar.gz: 9dc329a3c2a1965d89e24709eed516d50f4bc17dca0d721ed91152c6b9c7c799
3
+ metadata.gz: d865d2e228e487153ee4681a3d0a939c83090e236f58e99cff670b351d5cc07e
4
+ data.tar.gz: 84252e97bc38980791c4ac74aaedf1fcc7bc00d6de30c8da7d4fce98d37a89a1
5
5
  SHA512:
6
- metadata.gz: d583fa1b87e559a140610e7c8dbef90f6f357833265c21f94ec87ac5bce166f3666250f371d498ce8752453a5e9e0cc33948e5e69f3945beb5e166153c1f7c8e
7
- data.tar.gz: 4200917911b720000f7c029220180817546ba824a12af7ff1d970c0920aa82a56028a6f783ca312ead58ec37ee283d9a362b0f87e164fb97a8351e974bff6e90
6
+ metadata.gz: 4b307e1dfcf3cfba5e3e0378209c4d562578de7085753f216ed6a3d45e969b3d0e92724f6e428d6635dad31d7d19bdbf5f451b7f0e21406492dca09b97a0f1e0
7
+ data.tar.gz: 987ad87a47603e4c65cefaad42b26f8817b65154c070040c84936694e7763e62492664c7bb87305d5d03b6e6e6ce4e7d9ab941290f0f41f251011251968811c1
data/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.9.63] - 2026-07-22
4
+ ### Added
5
+ - H8: `DeathProtocol.terminate_bond(identity:, confirm:)` — single entry point for bond termination; walks cascade across all stores (bond, behavioral_synapses, session, audit, attribution, calibration, preferences, memory_traces, predictions, trust); returns proof-of-destruction receipt with per-store results; emits `gaia.bond.terminated` event
6
+ - H8: `BondRegistry.set_bond_state` / `bond_state` / `terminating?` / `terminated?` — explicit lifecycle state management for partner bonds
7
+ - H8 ingress barrier: `ingest`, `observe_interlocutor`, and `record_response_applied` all reject writes for terminating/terminated identities with a log warning and early return
8
+ - H8 irreversibility: `observe_interlocutor` blocks re-registration of terminated identities — a new bond requires explicit `begin_new_imprint` (out of scope)
9
+ - DELETE `/api/gaia/bond/:identity` route — terminates a bond and returns the receipt
10
+ - Soft-guarded erasure helpers for optional stores (calibration, preferences, memory traces, predictions, trust) — skips gracefully when extension not loaded, raises on actual store errors
11
+
3
12
  ## [0.9.62] - 2026-07-22
4
13
  ### Added
5
14
  - H8 prep: `BondRegistry.erase_partner!(identity:)` — thread-safe deletion of a single bond entry; marks registry dirty; emits `gaia.bond.erased` event if `Legion::Events` is defined; idempotent (no raise on missing identity)
@@ -11,9 +11,10 @@ module Legion
11
11
 
12
12
  TO_APOLLO_TAGS = %w[self-knowledge bond].freeze
13
13
 
14
- @bonds = Concurrent::Hash.new
15
- @dirty = false
16
- @mutex = Mutex.new
14
+ @bonds = Concurrent::Hash.new
15
+ @bond_states = Concurrent::Hash.new
16
+ @dirty = false
17
+ @mutex = Mutex.new
17
18
 
18
19
  module_function
19
20
 
@@ -262,9 +263,26 @@ module Legion
262
263
  { erased: !erased.nil?, identity: identity }
263
264
  end
264
265
 
266
+ def set_bond_state(identity, state)
267
+ @bond_states[identity.to_s] = state.to_sym
268
+ end
269
+
270
+ def bond_state(identity)
271
+ @bond_states[identity.to_s]
272
+ end
273
+
274
+ def terminating?(identity)
275
+ @bond_states[identity.to_s] == :terminating
276
+ end
277
+
278
+ def terminated?(identity)
279
+ @bond_states[identity.to_s] == :terminated
280
+ end
281
+
265
282
  def reset!
266
283
  @mutex.synchronize do
267
284
  @bonds = Concurrent::Hash.new
285
+ @bond_states = Concurrent::Hash.new
268
286
  @dirty = false
269
287
  end
270
288
  log.debug('BondRegistry reset')
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/logging/helper'
4
+
5
+ module Legion
6
+ module Gaia
7
+ module DeathProtocol
8
+ extend Legion::Logging::Helper
9
+
10
+ module_function
11
+
12
+ # The single entry point for bond termination.
13
+ # confirm: must be true (explicit consent gate).
14
+ # Returns: { terminated: true, identity:, receipt: { store => result } }
15
+ def terminate_bond(identity:, confirm:)
16
+ raise ArgumentError, 'confirm: must be true to terminate' unless confirm == true
17
+
18
+ identity_str = identity.to_s
19
+
20
+ # 1. Set bond state to :terminating (blocks new writes)
21
+ Legion::Gaia::BondRegistry.set_bond_state(identity_str, :terminating)
22
+
23
+ # 2. Walk every store's erase_partner!
24
+ receipt = build_erasure_receipt(identity_str)
25
+
26
+ # 3. Set bond state to :terminated
27
+ Legion::Gaia::BondRegistry.set_bond_state(identity_str, :terminated)
28
+
29
+ # 4. Emit event
30
+ if defined?(Legion::Events) && Legion::Events.respond_to?(:emit)
31
+ Legion::Events.emit('gaia.bond.terminated', identity: identity_str, receipt: receipt)
32
+ end
33
+
34
+ # 5. Log receipt
35
+ log.info("[gaia] bond terminated identity=#{identity_str} stores=#{receipt.keys.size}")
36
+
37
+ { terminated: true, identity: identity_str, receipt: receipt }
38
+ end
39
+
40
+ private
41
+
42
+ def build_erasure_receipt(identity_str)
43
+ {
44
+ bond: erase_store(:bond) { Legion::Gaia::BondRegistry.erase_partner!(identity: identity_str) },
45
+ behavioral_synapses: erase_store(:behavioral_synapses) do
46
+ Legion::Gaia::BehavioralSynapse.erase_partner!(identity: identity_str)
47
+ end,
48
+ session: erase_store(:session) do
49
+ Legion::Gaia.session_store&.erase_partner!(identity: identity_str)
50
+ end,
51
+ audit: erase_store(:audit) do
52
+ Legion::Gaia::AuditObserver.instance.erase_partner!(identity: identity_str)
53
+ end,
54
+ attribution: erase_store(:attribution) { Legion::Gaia.erase_attribution!(identity: identity_str) },
55
+ calibration: erase_calibration(identity_str),
56
+ preferences: erase_preferences(identity_str),
57
+ memory_traces: erase_memory_traces(identity_str),
58
+ predictions: erase_predictions(identity_str),
59
+ trust: erase_trust(identity_str)
60
+ }
61
+ end
62
+
63
+ def erase_store(name)
64
+ result = yield
65
+ result.is_a?(Hash) ? result : { erased: true }
66
+ rescue StandardError => e
67
+ log.error("[gaia] death_protocol store=#{name} error=#{e.message}")
68
+ raise
69
+ end
70
+
71
+ def erase_calibration(identity_str)
72
+ unless defined?(Legion::Extensions::Agentic::Social::Calibration::Runners::Calibration)
73
+ return { skipped: true, reason: :calibration_unavailable }
74
+ end
75
+
76
+ runner = Object.new
77
+ runner.extend(Legion::Extensions::Agentic::Social::Calibration::Runners::Calibration)
78
+ return { skipped: true, reason: :no_erase_method } unless runner.respond_to?(:erase_partner!)
79
+
80
+ erase_store(:calibration) { runner.erase_partner!(identity: identity_str) }
81
+ rescue StandardError => e
82
+ log.error("[gaia] death_protocol store=calibration error=#{e.message}")
83
+ raise
84
+ end
85
+
86
+ def erase_preferences(identity_str)
87
+ unless defined?(Legion::Extensions::Mesh::Helpers::PreferenceProfile)
88
+ return { skipped: true, reason: :preferences_unavailable }
89
+ end
90
+
91
+ klass = Legion::Extensions::Mesh::Helpers::PreferenceProfile
92
+ return { skipped: true, reason: :no_erase_method } unless klass.respond_to?(:erase_partner!)
93
+
94
+ erase_store(:preferences) { klass.erase_partner!(identity: identity_str) }
95
+ rescue StandardError => e
96
+ log.error("[gaia] death_protocol store=preferences error=#{e.message}")
97
+ raise
98
+ end
99
+
100
+ def erase_memory_traces(identity_str)
101
+ unless defined?(Legion::Extensions::Agentic::Memory::Trace::Runners::Traces)
102
+ return { skipped: true, reason: :memory_trace_unavailable }
103
+ end
104
+
105
+ runner = Object.new
106
+ runner.extend(Legion::Extensions::Agentic::Memory::Trace::Runners::Traces)
107
+ return { skipped: true, reason: :no_erase_method } unless runner.respond_to?(:erase_partner!)
108
+
109
+ erase_store(:memory_traces) { runner.erase_partner!(identity: identity_str) }
110
+ rescue StandardError => e
111
+ log.error("[gaia] death_protocol store=memory_traces error=#{e.message}")
112
+ raise
113
+ end
114
+
115
+ def erase_predictions(identity_str)
116
+ unless defined?(Legion::Extensions::Agentic::Prediction::Helpers::Store)
117
+ return { skipped: true, reason: :predictions_unavailable }
118
+ end
119
+
120
+ store = Legion::Extensions::Agentic::Prediction::Helpers::Store
121
+ return { skipped: true, reason: :no_erase_method } unless store.respond_to?(:erase_partner!)
122
+
123
+ erase_store(:predictions) { store.erase_partner!(identity: identity_str) }
124
+ rescue StandardError => e
125
+ log.error("[gaia] death_protocol store=predictions error=#{e.message}")
126
+ raise
127
+ end
128
+
129
+ def erase_trust(identity_str)
130
+ unless defined?(Legion::Extensions::Agentic::Social::Trust::Helpers::TrustStore)
131
+ return { skipped: true, reason: :trust_unavailable }
132
+ end
133
+
134
+ store = Legion::Extensions::Agentic::Social::Trust::Helpers::TrustStore
135
+ return { skipped: true, reason: :no_erase_method } unless store.respond_to?(:erase_partner!)
136
+
137
+ erase_store(:trust) { store.erase_partner!(identity: identity_str) }
138
+ rescue StandardError => e
139
+ log.error("[gaia] death_protocol store=trust error=#{e.message}")
140
+ raise
141
+ end
142
+
143
+ module_function :build_erasure_receipt, :erase_store, :erase_calibration, :erase_preferences,
144
+ :erase_memory_traces, :erase_predictions, :erase_trust
145
+ end
146
+ end
147
+ end
@@ -10,7 +10,7 @@
10
10
  module Legion
11
11
  module Gaia
12
12
  module Routes
13
- def self.registered(app)
13
+ def self.registered(app) # rubocop:disable Metrics/AbcSize
14
14
  app.helpers do # rubocop:disable Metrics/BlockLength
15
15
  unless method_defined?(:gaia_available?)
16
16
  define_method(:gaia_available?) do
@@ -62,6 +62,7 @@ module Legion
62
62
  register_sessions_route(app)
63
63
  register_teams_webhook_route(app)
64
64
  register_ingest_route(app)
65
+ register_bond_terminate_route(app)
65
66
  end
66
67
 
67
68
  def self.register_status_route(app)
@@ -193,10 +194,20 @@ module Legion
193
194
  end
194
195
  end
195
196
 
197
+ def self.register_bond_terminate_route(app)
198
+ app.delete '/api/gaia/bond/:identity' do
199
+ identity = params[:identity]
200
+ halt 400, json_error('missing_identity', 'identity is required') if identity.to_s.empty?
201
+
202
+ result = Legion::Gaia::DeathProtocol.terminate_bond(identity: identity, confirm: true)
203
+ json_response(result)
204
+ end
205
+ end
206
+
196
207
  class << self
197
208
  private :register_status_route, :register_ticks_route, :register_channels_route,
198
209
  :register_buffer_route, :register_sessions_route, :register_teams_webhook_route,
199
- :register_ingest_route
210
+ :register_ingest_route, :register_bond_terminate_route
200
211
  end
201
212
  end
202
213
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Legion
4
4
  module Gaia
5
- VERSION = '0.9.62'
5
+ VERSION = '0.9.63'
6
6
  end
7
7
  end
data/lib/legion/gaia.rb CHANGED
@@ -32,6 +32,7 @@ require 'legion/gaia/proactive'
32
32
  require 'legion/gaia/offline_handler'
33
33
  require 'legion/gaia/proactive_dispatcher'
34
34
  require 'legion/gaia/bond_registry'
35
+ require 'legion/gaia/death_protocol'
35
36
  require 'legion/gaia/behavioral_synapse'
36
37
  require 'legion/gaia/partner_model'
37
38
  require 'legion/gaia/tracker_persistence'
@@ -206,10 +207,15 @@ module Legion
206
207
  def ingest(input_frame)
207
208
  return { ingested: false, reason: :not_started } unless started?
208
209
 
210
+ identity = extract_identity(input_frame)
211
+ if identity && (BondRegistry.terminating?(identity.to_s) || BondRegistry.terminated?(identity.to_s))
212
+ log.warn("[gaia] rejected ingest for terminated/terminating identity=#{identity}")
213
+ return { ingested: false, reason: :identity_terminated }
214
+ end
215
+
209
216
  signal = input_frame.to_signal
210
217
  @sensory_buffer.push(signal)
211
218
 
212
- identity = extract_identity(input_frame)
213
219
  session = @session_store&.find_or_create(identity: identity || :anonymous)
214
220
  @session_store&.touch(session.id, channel_id: input_frame.channel_id) if session
215
221
 
@@ -290,6 +296,12 @@ module Legion
290
296
  def record_response_applied(advisory_id:, identity:, applied:)
291
297
  return unless started?
292
298
 
299
+ identity_str = identity.to_s
300
+ if BondRegistry.terminating?(identity_str) || BondRegistry.terminated?(identity_str)
301
+ log.warn("[gaia] rejected write for terminated/terminating identity=#{identity_str}")
302
+ return { recorded: false, reason: :identity_terminated }
303
+ end
304
+
293
305
  synapse_count = Array(applied[:behavioral_synapse_ids]).size
294
306
  log.info("[gaia] attribution advisory_id=#{advisory_id} identity=#{identity} synapses=#{synapse_count}")
295
307
 
@@ -563,8 +575,17 @@ module Legion
563
575
  ctx[:channel_identity] || ctx[:user_id] || ctx[:identity]
564
576
  end
565
577
 
578
+ def identity_lifecycle_blocked?(identity_str)
579
+ return false unless BondRegistry.terminating?(identity_str) || BondRegistry.terminated?(identity_str)
580
+
581
+ log.warn("[gaia] rejected write for terminated/terminating identity=#{identity_str}")
582
+ true
583
+ end
584
+
566
585
  def observe_interlocutor(input_frame, identity)
567
586
  identity_str = identity.to_s
587
+ return if identity_lifecycle_blocked?(identity_str)
588
+
568
589
  auth_ctx = input_frame.auth_context || {}
569
590
 
570
591
  # Ensure the identity is registered so reinforce has a target
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: legion-gaia
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.62
4
+ version: 0.9.63
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
@@ -432,6 +432,7 @@ files:
432
432
  - lib/legion/gaia/channels/teams/webhook_handler.rb
433
433
  - lib/legion/gaia/channels/teams_adapter.rb
434
434
  - lib/legion/gaia/cognitive_bus.rb
435
+ - lib/legion/gaia/death_protocol.rb
435
436
  - lib/legion/gaia/input_frame.rb
436
437
  - lib/legion/gaia/intent_classifier.rb
437
438
  - lib/legion/gaia/logging.rb