lex-extinction 0.2.0 → 0.2.1

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: '09d900581a92a527c43ffce88cf96c499f8aae44a4c12cf00adedd47a079dd38'
4
- data.tar.gz: 6f5ac264ef4ae83879bec5f515dedebf0662a79eb551804e96669263446e3314
3
+ metadata.gz: eb9e97dfd37144dc7a5b9d94c18f8f0c04daa25b682a6382d124aaafd22bfe5d
4
+ data.tar.gz: 9105b88a1cca0e7c0485925859131ad8879124acca633fa9613f10f69818a75e
5
5
  SHA512:
6
- metadata.gz: 70b94eeb7b31f41f9cb721d45229efc17c89e91b25eb8068e4ae3a1de3b316b9f902309d3b2c9188edc51e1d99c39bcbebcf1463909b24a1e5941ea632a41319
7
- data.tar.gz: a59842aade6c008b7e33927988732045e6e45660be85b2541db543b5ef7005ff4fbfdee22a638786a00b148f50718600bd114e042b933b1203b263817e4f58d2
6
+ metadata.gz: f55025877d82a197ec8fea8ddf842d55f462332bb97ed5d020c20e8d87678318b69abdfd4a227d87fe94dde00c4e0d51ec8f5d90183b33cb2f1e61cac8f7095e
7
+ data.tar.gz: '0816236a9aca4a74fefa2cac8879213a959ea29ac743e478fcb3e4c12518833f25942b46c594e64009bdc1a16cc944d4fe121ffc70a33ebf8667376cdcba3948'
@@ -1,38 +1,75 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'legion/extensions/actors/every'
4
-
5
3
  module Legion
6
4
  module Extensions
7
5
  module Extinction
8
- module Actor
9
- class ProtocolMonitor < Legion::Extensions::Actors::Every
10
- def runner_class
11
- Legion::Extensions::Extinction::Runners::Extinction
12
- end
6
+ module Actors
7
+ if defined?(Legion::Extensions::Actors::Every)
8
+ class ProtocolMonitor < Legion::Extensions::Actors::Every
9
+ def runner_class
10
+ self.class
11
+ end
13
12
 
14
- def runner_function
15
- 'monitor_protocol'
16
- end
13
+ def runner_function
14
+ 'monitor_protocol'
15
+ end
17
16
 
18
- def time
19
- 300
20
- end
17
+ def time
18
+ Legion::Extensions::Extinction::Settings.setting(:monitor_interval)
19
+ rescue StandardError
20
+ 300
21
+ end
21
22
 
22
- def run_now?
23
- false
24
- end
23
+ def run_now?
24
+ false
25
+ end
25
26
 
26
- def use_runner?
27
- false
28
- end
27
+ def use_runner?
28
+ false
29
+ end
29
30
 
30
- def check_subtask?
31
- false
32
- end
31
+ def check_subtask?
32
+ false
33
+ end
34
+
35
+ def generate_task?
36
+ false
37
+ end
38
+
39
+ def monitor_protocol(**)
40
+ state = build_state
41
+ last_change = state[:last_change]
42
+ stale = check_stale(last_change)
43
+
44
+ Legion::Logging.debug "[extinction] monitor_protocol: level=#{state[:current_level]} stale=#{stale}" if defined?(Legion::Logging)
45
+
46
+ {
47
+ success: true,
48
+ state: state,
49
+ stale: stale,
50
+ checked_at: Time.now.utc.iso8601
51
+ }
52
+ end
53
+
54
+ private
55
+
56
+ def build_state
57
+ {
58
+ current_level: 0,
59
+ level_name: :normal,
60
+ reversible: true,
61
+ history_count: 0,
62
+ last_change: nil
63
+ }
64
+ end
65
+
66
+ def check_stale(last_change)
67
+ return false unless last_change
33
68
 
34
- def generate_task?
35
- false
69
+ threshold_hours = Legion::Extensions::Extinction::Settings.setting(:stale_threshold_hours)
70
+ changed_at = Time.parse(last_change[:at]) rescue nil # rubocop:disable Style/RescueModifier
71
+ changed_at && (Time.now.utc - changed_at) > (threshold_hours * 3600)
72
+ end
36
73
  end
37
74
  end
38
75
  end
@@ -1,8 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'legion/extensions/extinction/helpers/levels'
4
- require 'legion/extensions/extinction/helpers/protocol_state'
5
- require 'legion/extensions/extinction/runners/extinction'
3
+ require_relative 'runners/extinction'
6
4
 
7
5
  module Legion
8
6
  module Extensions
@@ -10,13 +8,13 @@ module Legion
10
8
  class Client
11
9
  include Runners::Extinction
12
10
 
13
- def initialize(**)
14
- @protocol_state = Helpers::ProtocolState.new
11
+ def initialize(**opts)
12
+ @opts = opts
15
13
  end
16
14
 
17
- private
18
-
19
- attr_reader :protocol_state
15
+ def settings
16
+ { options: @opts }
17
+ end
20
18
  end
21
19
  end
22
20
  end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Extinction
6
+ module Helpers
7
+ class Archiver
8
+ def initialize
9
+ @archives = []
10
+ end
11
+
12
+ def archive(agent_id:, reason:, metadata: {})
13
+ level = current_protocol_level
14
+ record = {
15
+ agent_id: agent_id,
16
+ reason: reason,
17
+ metadata: metadata,
18
+ level: level,
19
+ archived_at: Time.now.utc.iso8601,
20
+ state_snapshot: capture_state_snapshot
21
+ }
22
+
23
+ persist_archive(record)
24
+ record
25
+ end
26
+
27
+ def all_archives
28
+ @archives.dup
29
+ end
30
+
31
+ private
32
+
33
+ def current_protocol_level
34
+ 0
35
+ end
36
+
37
+ def capture_state_snapshot
38
+ snapshot = {}
39
+ snapshot[:mesh_connected] = true if defined?(Legion::Extensions::Mesh)
40
+ snapshot[:privatecore_active] = true if defined?(Legion::Extensions::Privatecore)
41
+ snapshot
42
+ end
43
+
44
+ def persist_archive(record)
45
+ if defined?(Legion::Data::Local)
46
+ key = "extinction:archive:#{record[:agent_id]}:#{record[:archived_at]}"
47
+ Legion::Data::Local.set(key, record)
48
+ end
49
+ @archives << record
50
+ rescue StandardError => e
51
+ Legion::Logging.warn "[extinction] archive persist failed: #{e.message}" if defined?(Legion::Logging)
52
+ @archives << record
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -5,32 +5,57 @@ module Legion
5
5
  module Extinction
6
6
  module Helpers
7
7
  module Levels
8
- # Four escalation levels (spec: extinction-protocol-spec.md)
9
- ESCALATION_LEVELS = {
10
- 1 => { name: :mesh_isolation, reversible: true, authority: :governance_council },
11
- 2 => { name: :forced_sentinel, reversible: true, authority: :governance_council },
12
- 3 => { name: :full_suspension, reversible: true, authority: :council_plus_executive },
13
- 4 => { name: :cryptographic_erasure, reversible: false, authority: :physical_keyholders }
8
+ LEVELS = {
9
+ 0 => {
10
+ name: :normal,
11
+ authority_required: nil,
12
+ reversible: true,
13
+ description: 'No extinction active'
14
+ }.freeze,
15
+ 1 => {
16
+ name: :mesh_isolation,
17
+ authority_required: :governance_council,
18
+ reversible: true,
19
+ description: 'Disconnect from mesh network'
20
+ }.freeze,
21
+ 2 => {
22
+ name: :capability_suspension,
23
+ authority_required: :governance_council,
24
+ reversible: true,
25
+ description: 'Suspend all non-essential capabilities'
26
+ }.freeze,
27
+ 3 => {
28
+ name: :memory_lockdown,
29
+ authority_required: :council_plus_executive,
30
+ reversible: true,
31
+ description: 'Lock all memory writes, read-only mode'
32
+ }.freeze,
33
+ 4 => {
34
+ name: :cryptographic_erasure,
35
+ authority_required: :physical_keyholders,
36
+ reversible: false,
37
+ description: 'Erase all memory, terminate all workers'
38
+ }.freeze
14
39
  }.freeze
15
40
 
16
- VALID_LEVELS = [1, 2, 3, 4].freeze
17
-
18
- module_function
19
-
20
- def valid_level?(level)
21
- VALID_LEVELS.include?(level)
41
+ def self.valid_level?(level)
42
+ LEVELS.key?(level)
22
43
  end
23
44
 
24
- def level_info(level)
25
- ESCALATION_LEVELS[level]
45
+ def self.required_authority(level)
46
+ info = LEVELS[level]
47
+ info ? info[:authority_required] : nil
26
48
  end
27
49
 
28
- def reversible?(level)
29
- ESCALATION_LEVELS.dig(level, :reversible) || false
50
+ def self.reversible?(level)
51
+ info = LEVELS[level]
52
+ return false unless info
53
+
54
+ info[:reversible]
30
55
  end
31
56
 
32
- def required_authority(level)
33
- ESCALATION_LEVELS.dig(level, :authority)
57
+ def self.level_info(level)
58
+ LEVELS[level]
34
59
  end
35
60
  end
36
61
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'json'
3
+ require_relative 'levels'
4
4
 
5
5
  module Legion
6
6
  module Extensions
@@ -9,110 +9,104 @@ module Legion
9
9
  class ProtocolState
10
10
  MAX_HISTORY = 500
11
11
 
12
- attr_reader :current_level, :history, :active
12
+ attr_reader :history
13
13
 
14
14
  def initialize
15
- @current_level = 0 # 0 = normal operation
16
- @active = false
17
- @history = []
15
+ @current_level = 0
16
+ @history = []
17
+ @store = {}
18
18
  load_from_local
19
19
  end
20
20
 
21
- def escalate(level, authority:, reason:)
22
- return :invalid_level unless Levels.valid_level?(level)
23
- return :already_at_or_above if level <= @current_level
24
- return :insufficient_authority unless authority == Levels.required_authority(level)
21
+ def escalate(level:, authority:, reason:)
22
+ return { success: false, reason: :invalid_level } unless Levels.valid_level?(level)
23
+ return { success: false, reason: :invalid_escalation } if level <= @current_level
25
24
 
25
+ required = Levels.required_authority(level)
26
+ return { success: false, reason: :insufficient_authority, required: required, provided: authority } if required && authority != required
27
+
28
+ previous = @current_level
26
29
  @current_level = level
27
- @active = true
28
- @history << {
29
- action: :escalate, level: level, authority: authority,
30
- reason: reason, at: Time.now.utc
31
- }
30
+ record_history(action: :escalate, from: previous, to: level, authority: authority, reason: reason)
32
31
  trim_history
33
32
  save_to_local
34
- :escalated
33
+
34
+ { success: true, previous_level: previous, current_level: level }
35
35
  end
36
36
 
37
- def deescalate(target_level, authority:, reason:)
38
- return :not_active unless @active
39
- return :invalid_target if target_level >= @current_level
40
- return :irreversible unless Levels.reversible?(@current_level)
41
- return :insufficient_authority unless authority == Levels.required_authority(@current_level)
37
+ def deescalate(target_level:, authority:, reason:)
38
+ return { success: false, reason: :invalid_level } unless Levels.valid_level?(target_level)
39
+ return { success: false, reason: :invalid_deescalation } if target_level >= @current_level
42
40
 
41
+ return { success: false, reason: :not_reversible } unless Levels.reversible?(@current_level)
42
+
43
+ required = Levels.required_authority(@current_level)
44
+ return { success: false, reason: :insufficient_authority, required: required, provided: authority } if required && authority != required
45
+
46
+ previous = @current_level
43
47
  @current_level = target_level
44
- @active = target_level.positive?
45
- @history << {
46
- action: :deescalate, level: target_level, authority: authority,
47
- reason: reason, at: Time.now.utc
48
- }
48
+ record_history(action: :deescalate, from: previous, to: target_level, authority: authority,
49
+ reason: reason)
49
50
  trim_history
50
51
  save_to_local
51
- :deescalated
52
+
53
+ { success: true, previous_level: previous, current_level: target_level }
52
54
  end
53
55
 
54
56
  def to_h
55
57
  {
56
58
  current_level: @current_level,
57
- active: @active,
58
- level_info: @current_level.positive? ? Levels.level_info(@current_level) : nil,
59
- history_size: @history.size
59
+ level_name: Levels.level_info(@current_level)&.dig(:name),
60
+ reversible: Levels.reversible?(@current_level),
61
+ history_count: @history.size,
62
+ last_change: @history.last
60
63
  }
61
64
  end
62
65
 
63
- def save_to_local
64
- return unless defined?(Legion::Data::Local) && Legion::Data::Local.connected?
66
+ private
65
67
 
66
- row = {
67
- id: 1,
68
- current_level: @current_level,
69
- active: @active,
70
- history: ::JSON.dump(@history.map { |h| h.merge(at: h[:at].to_s) }),
71
- updated_at: Time.now.utc
68
+ def record_history(action:, from:, to:, authority:, reason:)
69
+ @history << {
70
+ action: action,
71
+ from: from,
72
+ to: to,
73
+ authority: authority,
74
+ reason: reason,
75
+ at: Time.now.utc.iso8601
72
76
  }
73
- db = Legion::Data::Local.connection
74
- if db[:extinction_state].where(id: 1).any?
75
- db[:extinction_state].where(id: 1).update(row.except(:id))
76
- else
77
- db[:extinction_state].insert(row)
78
- end
79
- rescue StandardError
80
- nil
81
77
  end
82
78
 
83
- private
84
-
85
79
  def trim_history
86
- @history = @history.last(MAX_HISTORY) if @history.size > MAX_HISTORY
87
- end
88
-
89
- def load_from_local
90
- return unless defined?(Legion::Data::Local) && Legion::Data::Local.connected?
91
-
92
- row = Legion::Data::Local.connection[:extinction_state].where(id: 1).first
93
- return unless row
80
+ max = defined?(Legion::Extensions::Extinction::Settings) ? Legion::Extensions::Extinction::Settings.setting(:max_history) : MAX_HISTORY
81
+ return unless @history.size > max
94
82
 
95
- db_level = row[:current_level].to_i
96
- @current_level = [db_level, @current_level].max
97
- @active = [true, 1].include?(row[:active])
98
- @history = parse_history(row[:history])
99
- rescue StandardError
100
- nil
83
+ @history.shift(@history.size - max)
101
84
  end
102
85
 
103
- def parse_history(raw)
104
- return [] if raw.nil? || raw.empty?
105
-
106
- parsed = ::JSON.parse(raw, symbolize_names: true)
107
- parsed.map do |h|
108
- h.merge(
109
- action: h[:action].to_sym,
110
- authority: h[:authority].to_sym,
111
- at: Time.parse(h[:at].to_s)
112
- )
86
+ def save_to_local
87
+ data = { current_level: @current_level, history: @history }
88
+ if defined?(Legion::Data::Local)
89
+ Legion::Data::Local.set('extinction:protocol_state', data)
90
+ else
91
+ @store[:protocol_state] = data
113
92
  end
114
- rescue StandardError
115
- []
93
+ rescue StandardError => e
94
+ Legion::Logging.warn "[extinction] protocol_state save failed: #{e.message}" if defined?(Legion::Logging)
95
+ end
96
+
97
+ def load_from_local
98
+ data = if defined?(Legion::Data::Local)
99
+ Legion::Data::Local.get('extinction:protocol_state')
100
+ else
101
+ @store[:protocol_state]
102
+ end
103
+ return unless data
104
+
105
+ @current_level = data[:current_level] || data['current_level'] || 0
106
+ raw_history = data[:history] || data['history'] || []
107
+ @history = raw_history
108
+ rescue StandardError => e
109
+ Legion::Logging.warn "[extinction] protocol_state load failed: #{e.message}" if defined?(Legion::Logging)
116
110
  end
117
111
  end
118
112
  end