What provenance must accompany human-in-the-loop approval decisions in autonomous agent workflows?

**Date of Record:** 2026 October 27 **Scope:** Applies when an autonomous agent workflow requires human approval before executing a critical action (deployment, financial transfer, permission grant), and the approval must be cryptographically attributable to the specific human who approved it. Does not apply to fully automated workflows (no human approval) or approvals through channels with inherent attribution (signed emails, hardware tokens).

Core Distinction

The separation between **approval event** (the workflow advanced past the approval gate) and **approval provenance** (the cryptographic binding of the approval to the specific human principal, the exact action they approved, and the context they saw at approval time). Relying on the event creates the **Approval Laundering Attack**: an attacker who intercepts the approval channel or controls a low-privileged human account approves a critical action, and downstream systems cannot distinguish this from a legitimate high-privileged human approval.

What Was Observed

In human-in-the-loop agent workflows, failing to preserve approval provenance produces three primary failure modes:

  1. **Consent Substitution:** The human approves action A, but the workflow executes action B due to a parsing bug. Provenance would catch the mismatch (the approval signature covers the exact action); without it, the executed action is falsely attributed.
  2. **Principal Confusion:** A junior engineer approves a critical action through a shared "ops team" account. The approval appears to have come from the team, but the specific individual and their authority level are lost.
  3. **Replay Attack:** A previously valid approval signature is replayed for a different but similar action, and the agent accepts it because it cannot prove the approval was bound to the original action.

Recorded Elements

  1. **`approver_identity_signature`:** The cryptographic signature of the specific human principal (their key or hardware token), not a shared account.
  2. **`approved_action_hash`:** The cryptographic hash of the exact action approved (command, payload, scope), preventing substitution to a different action.
  3. **`approval_context_snapshot`:** The exact state the human saw at approval time (dashboard, diff, evidence), allowing verification that the approval was informed.
  4. **`approval_expiration_epoch`:** The exact epoch after which the approval is invalid, preventing replay and time-of-check-to-time-of-use attacks.

Condition of Validity

* **Applies when:** The workflow requires human approval, the approval can be misused or forged, and downstream consequences depend on the approval being legitimate and informed. * **Does not apply when:** The workflow is fully automated (no human in the loop), or approvals use hardware tokens with signed request binding (e.g., YubiKey with challenge-response over the payload).

Decision Boundary

  1. **Is there a human approval gate?** If no, standard workflow proceeds. If yes, proceed.
  2. **Is the `approver_identity_signature` from a unique human principal?** If no (shared account), the approval is unverifiable. Reject or require individual re-approval.
  3. **Does the `approved_action_hash` match the action about to execute?** If no, the approval has been substituted. Reject and re-request approval.
  4. **Conversely, when can an agent accept an approval without full provenance?** Only when the approval channel is cryptographically authenticated end-to-end (e.g., signed commit by a known developer), or the action is strictly informational without consequence.

Operational Effect

  1. **Substitution Prevention:** Binds approvals to specific actions, preventing swap attacks.
  2. **Principal Accountability:** Ensures every approval is attributable to a specific human, enabling audit and disciplinary action if needed.
  3. **Replay Immunity:** Uses expiration to prevent reuse of old approvals for new actions.
from dataclasses import dataclass
from enum import Enum, auto
import hashlib
import json

class ApprovalVerdict(Enum):
    AUTHENTIC_APPROVAL = auto()
    SHARED_ACCOUNT_REJECTED = auto()
    ACTION_SUBSTITUTED = auto()
    APPROVAL_EXPIRED = auto()

@dataclass(frozen=True)
class HumanApproval:
    approval_id: str
    approver_identity_signature: str
    approver_is_shared_account: bool
    approved_action_hash: str
    action_about_to_execute_hash: str
    approval_epoch: float
    expiration_epoch: float

@dataclass(frozen=True)
class ApprovalPolicy:
    current_epoch: float

@dataclass(frozen=True)
class ApprovalManifest:
    verdict: ApprovalVerdict
    directive: str
    manifest_digest: str

class HumanApprovalGuard:
    @staticmethod
    def _hash(data: str) -> str:
        return hashlib.sha256(data.encode("utf-8")).hexdigest()

    @classmethod
    def evaluate_approval(
        cls, approval: HumanApproval, policy: ApprovalPolicy
    ) -> ApprovalManifest:
        
        # Gate 1: Identity signature
        if not approval.approver_identity_signature:
            return ApprovalManifest(
                verdict=ApprovalVerdict.SHARED_ACCOUNT_REJECTED,
                directive=f"HALT: No approver signature for {approval.approval_id}.",
                manifest_digest=cls._hash(f"NO_SIG:{approval.approval_id}")
            )

        # Gate 2: Shared account rejection
        if approval.approver_is_shared_account:
            return ApprovalManifest(
                verdict=ApprovalVerdict.SHARED_ACCOUNT_REJECTED,
                directive=f"HALT: Shared account approval {approval.approval_id} not attributable.",
                manifest_digest=cls._hash(f"SHARED:{approval.approval_id}")
            )

        # Gate 3: Action match
        if approval.approved_action_hash != approval.action_about_to_execute_hash:
            return ApprovalManifest(
                verdict=ApprovalVerdict.ACTION_SUBSTITUTED,
                directive=f"HALT: Approved action {approval.approved_action_hash} != current {approval.action_about_to_execute_hash}.",
                manifest_digest=cls._hash(f"SUBSTITUTED:{approval.approval_id}")
            )

        # Gate 4: Expiration
        if policy.current_epoch > approval.expiration_epoch:
            return ApprovalManifest(
                verdict=ApprovalVerdict.APPROVAL_EXPIRED,
                directive=f"HALT: Approval expired at {approval.expiration_epoch:.0f}.",
                manifest_digest=cls._hash(f"EXPIRED:{approval.approval_id}")
            )

        # Gate 5: Authentic
        digest = cls._hash(json.dumps({
            "approval": approval.approval_id,
            "approver": approval.approver_identity_signature,
            "action": approval.approved_action_hash,
            "epoch": f"{approval.approval_epoch:.0f}"
        }, sort_keys=True))
        
        return ApprovalManifest(
            verdict=ApprovalVerdict.AUTHENTIC_APPROVAL,
            directive=f"PROCEED: Approval {approval.approval_id} authentic and bound to action.",
            manifest_digest=digest
        )