AgentTrust
AgentTrust
ProgramsPolicyVault

KillSwitch policy

First in the composer — a multisig-controlled emergency pause that blocks any Allow when paused.

KillSwitch is the cheapest policy and runs first. When the agent's KillSwitchState.paused == true, the composer returns Deny(KillSwitchEngaged) immediately — no other policy or foreign-PDA read costs are paid.

Source: programs/policy-vault/src/policies/killswitch.rs.

State

#[account]
pub struct KillSwitchState {
    pub scope_kind: u8,        // off  8 — Global / PerCollection / PerAgent
    pub bump: u8,              // off  9
    pub paused: bool,          // off 10 — the bit
    pub _pad0: u8,             // off 11
    pub _pad1: [u8; 4],        // off 12..16
    pub scope_key: [u8; 32],   // off 16..48 — zeros for Global
    pub paused_at_slot: u64,   // off 48..56
    pub unpaused_at_slot: u64, // off 56..64
    pub paused_by: Pubkey,     // off 64..96
}

PDA seeds: ["killswitch", &[scope_kind], scope_key]. Three scope kinds — global, per-collection, per-agent — all share the same struct.

Mutation — set_killswitch

The pause flag flips through the set_killswitch instruction, which is multisig-gated against the agent's PolicyAuthority. The Anchor handler checks:

let distinct = authority.count_distinct_signing_members(&signer_keys);
require!(
    distinct >= authority.threshold,
    PolicyVaultError::ThresholdNotMet,
);

count_distinct_signing_members is pubkey-based: a single signer signing twice counts as one. Even if PolicyAuthority.members somehow contained duplicates (the init_authority constraint rejects this, but defense in depth), the count cannot exceed min(member_count, signer_keys.len()). That is the load-bearing property of multisig_threshold_enforced (Kani #5, 149 sub-checks, 77.17 s).

PolicyAuthority carries up to 7 members + a threshold (default 2). Source: programs/policy-vault/src/state/policy_authority.rs.

Pure decision function

pub fn evaluate(state: KillSwitchSnapshot) -> Option<DenyReason> {
    if state.paused {
        Some(DenyReason::KillSwitchEngaged)
    } else {
        None
    }
}

Some(KillSwitchEngaged) short-circuits the composer to GateDecision::Deny(KillSwitchEngaged) (DenyReason code 1).

Formal verification

paused_implies_no_allow (Kani #1, 126 sub-checks, 0.25 s) — if the agent's KillSwitch is paused == true AND KIND_KILLSWITCH is set in the policy bitmask, compose_decision cannot return Allow for any values of the other policy fields, ledger, or input parameters. The load-bearing safety invariant of the gate.

If a future change re-orders or skips KillSwitch evaluation, this proof fails loud. Reference: Verification → Kani proofs.

Source

On this page

⌘I