Skip to main content
Enforcement is in the Move contract, not in a server. Every gated agent action routes through policy::check_and_consume, which asserts the policy is live and the spend is in scope before any coin leaves the vault. A failed assertion aborts the whole transaction. No coin moves, and on a dry-run no gas is spent.

The gate

public(package) fun check_and_consume<T>(
    policy: &mut Policy,
    cap: &AgentCap,
    amount: u64,
    target_package: address,
    clock: &Clock,
) {
    assert!(agent::policy_id(cap) == object::id(policy), EWrongPolicy);
    assert!(!policy.revoked, EPolicyRevoked);
    assert!(!policy.paused, EPolicyPaused);
    assert!(now < policy.expires_at_ms, EPolicyExpired);
    assert!(policy.allowed_packages.contains(&target_package), EPackageNotAllowed);
    // for a capped (budget) asset: enforce per_tx + per_day, then record the spend
    // ...
    audit::emit_allowed(policy.agent_id, policy.version, amount, target_package, now);
}
Order matters: liveness first (right policy, not revoked, not paused, not expired), then scope, then caps. The first failing assertion is the abort.

Abort codes

Each abort code maps to a stable reason. decodePolicyAbort reads the abort out of a thrown error or a dry-run failure status and returns the reason.
Move errorCodeReason codeMeaning
EPolicyRevoked1policy_revokedThe owner revoked the agent. Terminal.
EPolicyExpired2policy_expiredexpires_at_ms has passed.
EPolicyPaused3agent_pausedThe owner paused the agent. Reversible.
ECapExceededPerTx4over_per_tx_capAmount exceeds per_tx_cap.
ECapExceededPerDay5over_per_day_capWould push the rolling 24h total over per_day_cap.
EPackageNotAllowed6package_not_allowedTarget package or pool is not in allowed_packages.
EWrongPolicy7wrong_policyThe cap’s policy_id does not match this policy.
ENotAllowedAction8action_not_allowedAction is not in allowed_actions.
EActionConfigMissing9action_config_missingA required per-action config (e.g. swap floor) is absent.
EAssetNotAllowed10asset_not_allowedTransfer-out of an asset with no cap entry.

Denied without spending gas

A denied action aborts on-chain and emits no event, so a chain indexer never sees it. The agent records the denial itself: dry-run the transaction with client.devInspectTransactionBlock, decode the policy abort, log it, and skip submitting. guardedSubmit does exactly this — dry-run, and on a policy abort record the denial and return without submitting; otherwise submit for real. A non-policy dry-run failure (DeepBook liquidity, gas) is not a policy decision. decodePolicyAbort returns null for it and guardedSubmit surfaces it rather than recording a denial.

Pause and revoke

Both are owner operations signed with the OwnerCap. Both take effect on the next agent action.
  • Pause sets paused. The next action aborts agent_paused. Unpause clears it and the agent resumes.
  • Revoke sets revoked and emits PolicyRevoked. The next action aborts policy_revoked. There is no path back. killAndDrain revokes and sweeps every vault asset to the owner in one transaction.

Next: audit trail

The on-chain events every decision emits, and how denials reach the Chronicle.