Class: OpenLoam::PendingAction

Inherits:
TenantRecord
  • Object
show all
Includes:
Auditable, Encryptable, Workflow
Defined in:
app/models/open_loam/pending_action.rb

Overview

A staged mutation awaiting human approval. When a caller runs under confirm-mode (an MCP tool acting for an AI agent — see OpenLoam.mutation_mode), a write is staged HERE as a preview instead of committing; a manager approves or rejects, and only approval executes it. This is OpenLoam's thesis made concrete: agent-triggered writes are gate-able.

The approval gate IS a workflow (OpenLoam::Workflow): pending → (manager) approve / reject → executed / failed. "Who may approve" lives in one declarative, role-gated place. The proposed changes are encrypted at rest (OpenLoam::Encryptable), because a staged change to an encrypted target field would otherwise sit here — and in this row's own audit — as plaintext, reopening the leak L-901 closed.

Constant Summary

Constants included from Auditable

Auditable::IGNORED_ATTRIBUTES

Instance Method Summary collapse

Methods included from Workflow

#open_loam_workflow_state, #workflow_transitions_available

Methods included from Encryptable

#open_loam_reencrypt!

Instance Method Details

#approve!(by:) ⇒ Object

Approve and execute, atomically enough: the transition is role-gated to a manager; execution runs in a transaction so a failure leaves NO partial write; the record ends "executed" or "failed". Executes as by, so the TARGET's audit names the approving human — the person owns the change.



98
99
100
101
102
103
104
105
106
107
# File 'app/models/open_loam/pending_action.rb', line 98

def approve!(by:)
  OpenLoam.as_tenant(tenant, actor: by) do
    reject_self_approval!(by)
    self.reviewed_by_id = by.id
    self.reviewed_at = Time.current
    to_approved!   # NotAuthorizedError if not a manager; InvalidTransitionError if not pending
    execute_and_record!
  end
  self
end

#changeset ⇒ Object

changeset is a Hash in Ruby but an encrypted JSON string at rest. The class methods win over Encryptable's included module and reach it via super.



67
68
69
70
# File 'app/models/open_loam/pending_action.rb', line 67

def changeset
  raw = super
  raw.present? ? JSON.parse(raw) : {}
end

#changeset=(value) ⇒ Object



72
73
74
# File 'app/models/open_loam/pending_action.rb', line 72

def changeset=(value)
  super(value.nil? ? nil : value.to_json)
end

#preview ⇒ Object

A structured before/after diff. An encrypted target field shows "[encrypted]" on BOTH sides — a reviewer sees THAT a secret changes, never its value.



79
80
81
82
83
84
85
86
87
88
89
90
91
92
# File 'app/models/open_loam/pending_action.rb', line 79

def preview
  target = load_target
  encrypted = encrypted_target_fields

  changeset.each_with_object({}) do |(field, proposed), diff|
    field = field.to_s
    diff[field] =
      if encrypted.include?(field)
        { "from" => (target ? "[encrypted]" : nil), "to" => "[encrypted]" }
      else
        { "from" => target&.read_attribute(field), "to" => proposed }
      end
  end
end

#reject!(by:, reason: nil) ⇒ Object



109
110
111
112
113
114
115
116
117
# File 'app/models/open_loam/pending_action.rb', line 109

def reject!(by:, reason: nil)
  OpenLoam.as_tenant(tenant, actor: by) do
    self.reviewed_by_id = by.id
    self.reviewed_at = Time.current
    self.result = [ "rejected", reason.presence ].compact.join(": ")
    to_rejected!
  end
  self
end