Serrure the lock
Policy & guardrails
Serrure is the program that says no. Every Ombre settlement CPIs into serrure::check before moving funds. If the check fails, the parent transaction fails — no lamports leave the vault, no state is mutated anywhere. This is the guarantee that lets CFOs and auditors put agents on treasury.
What gets enforced
pub struct PolicyParams {
pub max_per_tx_lamports: u64, // hard cap per single transaction
pub max_per_day_lamports: u64, // rolling 24h cap
pub max_total_lamports: u64, // lifetime cap (0 = unlimited)
pub max_per_window: u32, // rate limit: N tx / window
pub window_seconds: i64, // window length
pub merchants_root: [u8; 32], // merkle root of allowed merchants
}Every field above is enforced independently. Failing any one fails the check. The window is a 16-slot sliding array stored on the account; merkle allowlists let you whitelist 10,000 merchants without on-chain rent bloat.
Rejection reasons
Rejections are typed so your UI can show meaningful messages:
#[error_code]
pub enum SerrureError {
NotPrincipal,
WrongAgent,
CircuitBreakerTripped,
ExceedsPerTxLimit,
RateLimited,
DailyCapExceeded,
TotalCapExceeded,
MerchantNotAllowed,
}Circuit breaker
The paused flag is a principal-only kill switch. Set it, and every future check fails with CircuitBreakerTripped until the principal calls unpause. This is the reversible middle step between "everything's fine" and "burn the Masque."
// Anomaly detector raises:
await serrure.methods.pause().accounts({ serrure, principal }).rpc();
// After investigation, if it was a false alarm:
await serrure.methods.unpause().accounts({ serrure, principal }).rpc();
// If it was real, escalate to Masque revocation:
await masque.methods.revoke(2).accounts({ masque, principal }).rpc();Merchant allowlists
You supply a merkle root; the agent supplies a merkle proof at settlement time. On-chain verification is 3-4 hash ops. To update the list, the principal calls update with a new policy (same PDA, new params).
0x00 concatenated with the merchant pubkey. Internal nodes use 0x01. See verify_merkle in the program for the exact hashing.Typical policy shapes
Consumer agent
{
maxPerTxLamports: 5_000_000n, // ~0.005 SOL (~$1)
maxPerDayLamports: 50_000_000n, // ~$10 / day
maxTotalLamports: 0n, // no lifetime cap
maxPerWindow: 5,
windowSeconds: 30,
merchantsRoot: <allowlist root>,
}Enterprise procurement agent
{
maxPerTxLamports: 500_000_000n, // 0.5 SOL (~$100)
maxPerDayLamports: 5_000_000_000n, // ~$1000 / day
maxTotalLamports: 50_000_000_000n, // ~$10K lifetime, then reissue
maxPerWindow: 20,
windowSeconds: 60,
merchantsRoot: <allowlist of approved vendors>,
}Research / unbounded
{
maxPerTxLamports: 100_000_000n,
maxPerDayLamports: 1_000_000_000n,
maxTotalLamports: 0n,
maxPerWindow: 100,
windowSeconds: 10,
merchantsRoot: '0x' + '00'.repeat(32), // allow any merchant
}