Module: Legion::DigitalWorker::Lifecycle

Defined in:
lib/legion/digital_worker/lifecycle.rb

Defined Under Namespace

Classes: AuthorityRequired, GovernanceBlocked, GovernanceRequired, InvalidTransition

Constant Summary collapse

TRANSITIONS =
{
  'bootstrap'        => %w[active terminated],
  'pending_approval' => %w[active rejected],
  'active'           => %w[paused retired terminated],
  'paused'           => %w[active retired terminated],
  'retired'          => %w[terminated],
  'rejected'         => [],
  'terminated'       => []
}.freeze
GOVERNANCE_REQUIRED =
{
  %w[retired terminated] => :council_approval,
  %w[active terminated]  => :council_approval
}.freeze
AUTHORITY_REQUIRED =
{
  %w[active paused]  => :owner_or_manager,
  %w[paused active]  => :owner_or_manager,
  %w[active retired] => :owner_or_manager
}.freeze
EXTINCTION_MAPPING =

Map lifecycle states to lex-extinction containment levels

{
  'active'           => 0, # no containment
  'paused'           => 2, # capability restriction
  'retired'          => 3, # supervised-only
  'terminated'       => 4, # full termination (irreversible in lex-extinction)
  'pending_approval' => 1, # held — no capability, awaiting decision
  'rejected'         => 4  # treated as terminated for containment
}.freeze
{
  'bootstrap'        => :consult,    # most restrictive during bootstrap
  'active'           => :autonomous, # earned autonomy
  'paused'           => :consult,    # back to restrictive
  'retired'          => :inform,     # notification only
  'terminated'       => :inform,
  'pending_approval' => :consult,    # held at consult until approved
  'rejected'         => :inform      # read-only / no execution
}.freeze

Class Method Summary collapse

Class Method Details

.authority_type(from_state, to_state) ⇒ Object



162
163
164
# File 'lib/legion/digital_worker/lifecycle.rb', line 162

def self.authority_type(from_state, to_state)
  AUTHORITY_REQUIRED[[from_state, to_state]]
end


170
171
172
# File 'lib/legion/digital_worker/lifecycle.rb', line 170

def self.consent_tier(state)
  CONSENT_MAPPING.fetch(state, :consult)
end

.extinction_level(state) ⇒ Object



166
167
168
# File 'lib/legion/digital_worker/lifecycle.rb', line 166

def self.extinction_level(state)
  EXTINCTION_MAPPING.fetch(state, 0)
end

.governance_required?(from_state, to_state) ⇒ Boolean

Returns:

  • (Boolean)


158
159
160
# File 'lib/legion/digital_worker/lifecycle.rb', line 158

def self.governance_required?(from_state, to_state)
  GOVERNANCE_REQUIRED.key?([from_state, to_state])
end

.transition!(worker, to_state:, by:, reason: nil, **opts) ⇒ Object



53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
# File 'lib/legion/digital_worker/lifecycle.rb', line 53

def self.transition!(worker, to_state:, by:, reason: nil, **opts)
  from_state = worker.lifecycle_state
  allowed    = TRANSITIONS.fetch(from_state, [])

  unless allowed.include?(to_state)
    Legion::Logging.warn "[Lifecycle] invalid transition #{from_state} -> #{to_state} for #{worker.worker_id}" if defined?(Legion::Logging)
    raise InvalidTransition, "cannot transition from #{from_state} to #{to_state}"
  end
  Legion::Logging.info "[Lifecycle] transition #{from_state} -> #{to_state} worker=#{worker.worker_id} by=#{by}" if defined?(Legion::Logging)

  if defined?(Legion::Extensions::Governance::Runners::Governance)
    review = Legion::Extensions::Governance::Runners::Governance.review_transition(
      worker_id:    worker.is_a?(Hash) ? worker[:id] : worker.worker_id,
      from_state:   from_state,
      to_state:     to_state,
      principal_id: by,
      worker_owner: worker.respond_to?(:owner_msid) ? worker.owner_msid : nil
    )
    raise GovernanceBlocked, "#{from_state} -> #{to_state} blocked: #{review[:reasons]&.join(', ')}" unless review[:allowed]
  else
    if governance_required?(from_state, to_state)
      required = GOVERNANCE_REQUIRED[[from_state, to_state]]
      raise GovernanceRequired, "#{from_state} -> #{to_state} requires #{required}" unless opts[:governance_override] == true
    end

    authority = authority_type(from_state, to_state)
    raise AuthorityRequired, "#{from_state} -> #{to_state} requires #{authority} (by: #{by})" if authority && opts[:authority_verified] != true
  end

  if defined?(Legion::Extensions::Extinction::Client)
    new_level     = EXTINCTION_MAPPING[to_state]
    current_level = EXTINCTION_MAPPING[from_state] || 0
    if new_level && new_level > current_level
      Legion::Extensions::Extinction::Client.new.escalate(
        level:     new_level,
        authority: by || :system,
        reason:    "lifecycle transition: #{from_state} -> #{to_state}"
      )
    elsif new_level && new_level < current_level
      Legion::Extensions::Extinction::Client.new.deescalate(
        authority:    by || :system,
        reason:       "lifecycle transition: #{from_state} -> #{to_state}",
        target_level: new_level
      )
    end
  end

  if to_state == 'terminated' &&
     defined?(Legion::Extensions::Agentic::Self::Identity::Helpers::VaultSecrets)
    begin
      Legion::Extensions::Agentic::Self::Identity::Helpers::VaultSecrets
        .delete_client_secret(worker_id: worker.worker_id)
    rescue StandardError => e
      Legion::Logging.warn("Credential revocation failed for #{worker.worker_id}: #{e.message}") if defined?(Legion::Logging)
    end
  end

  new_consent = CONSENT_MAPPING[to_state]
  worker.update(
    lifecycle_state: to_state,
    consent_tier:    new_consent ? new_consent.to_s : worker.consent_tier,
    updated_at:      Time.now.utc,
    retired_at:      %w[retired terminated].include?(to_state) ? Time.now.utc : worker.retired_at,
    retired_by:      %w[retired terminated].include?(to_state) ? by : worker.retired_by,
    retired_reason:  reason || worker.retired_reason
  )
  sync_consent_tier(worker, new_consent) if new_consent

  if defined?(Legion::Events)
    Legion::Events.emit('worker.lifecycle', {
                          worker_id:        worker.worker_id,
                          from_state:       from_state,
                          to_state:         to_state,
                          by:               by,
                          reason:           reason,
                          extinction_level: extinction_level(to_state),
                          consent_tier:     consent_tier(to_state),
                          at:               Time.now.utc
                        })
  end

  if defined?(Legion::Audit)
    begin
      Legion::Audit.record(
        event_type:     'lifecycle_transition',
        principal_id:   by,
        principal_type: 'human',
        action:         'transition',
        resource:       worker.worker_id,
        source:         'system',
        status:         'success',
        detail:         { from_state: from_state, to_state: to_state, reason: reason }
      )
    rescue StandardError => e
      Legion::Logging.debug("Audit in lifecycle.transition! failed: #{e.message}") if defined?(Legion::Logging)
    end
  end

  worker
end

.valid_transition?(from_state, to_state) ⇒ Boolean

Returns:

  • (Boolean)


154
155
156
# File 'lib/legion/digital_worker/lifecycle.rb', line 154

def self.valid_transition?(from_state, to_state)
  TRANSITIONS.fetch(from_state, []).include?(to_state)
end