Build a policy rule
A rule is a tiny Move package that lets a wallet owner gate how a delegate spends. It plugs into SupWallet::policy using only its public interface — no core changes, no permission. (To have an agent scaffold one, use the sup-policy-rule-dev skill.)
#Two hard requirements
- A witness type —
public struct MyRule has drop {}. Its full name0x<pkg>::<module>::MyRuleis the rule-type: the marketplace key and theRan owner attaches withadd_auth_rule<R>/add_caveat_rule<R>. Only your module can build it ⇒ unforgeable. - Touch a spend only by stamping — call
policy::add_auth_receipt(MyRule {}, req)(auth, OR) orpolicy::add_caveat_receipt(MyRule {}, req)(caveat, AND) after your condition holds. Never take a vaultCoin, never callconfirm_spend, never transfer funds.
#Auth or caveat?
- Auth answers "is this the right principal?" (address / cap / ZK / contract / agent) — OR-gated.
- Caveat answers "is this spend allowed?" (budget / recipient / time / oracle / KYC) — AND-gated.
#The safety boundary
Your rule gets &mut SpendRequest, but the only mutation exposed is stamping your own witness. You can read policy::spend_amount / spend_recipient / spend_coin / spend_wallet_id / spend_policy_version, stamp, or abort. You cannot change the amount or recipient.
#A caveat rule
module my_rule::rule;
use SupWallet::wallet::{Self, Wallet};
use SupWallet::policy::{Self, SpendRequest};
public struct MyRule has drop {}
public struct Config has key, store { id: UID, wallet_id: ID, owner: address /*, params */ }
public fun create_and_share(wallet: &Wallet, ctx: &mut TxContext) {
// owner-gate via the PUBLIC owner accessor — no core access needed
assert!(tx_context::sender(ctx) == wallet::owner(wallet), 0);
transfer::share_object(Config { id: object::new(ctx), wallet_id: wallet::id(wallet), owner: wallet::owner(wallet) });
}
public fun enforce(cfg: &Config, req: &mut SpendRequest) {
assert!(cfg.wallet_id == policy::spend_wallet_id(req), 1);
assert!(/* your condition over spend_amount / spend_recipient / spend_coin */, 2);
policy::add_caveat_receipt(MyRule {}, req);
}
Owner enables it once with policy::add_caveat_rule<my_rule::rule::MyRule>(&mut wallet, ctx). A spend composes it in one PTB: begin_spend → enforce → confirm_spend.
#An auth rule (bearer cap)
Mint a cap bound to policy::version(wallet); in prove, assert cap.policy_version == policy::spend_policy_version(req) then add_auth_receipt. Binding to the version means the owner's revoke_all invalidates every outstanding cap for free.
#Register it (open marketplace)
policy_rule_registry::registry::register(
registry, package_id, rule_type, kind /* auth | caveat */, manifest_uri, manifest_hash, clock, ctx)
On-chain stores only the anchor; put docs / params schema / audits off-chain at manifest_uri (hash-pinned). Listing grants no power — only an owner attaching the rule does.
#Rule ideas — a menu to build from
A rule is ~30 lines, lists permissionlessly, and any owner can opt in — so the design space is wide open. The built-ins cover only the obvious cases; here's what's missing and worth writing. (Auth = WHO may spend, OR-gated. Caveat = is the spend ALLOWED, AND-gated.)
Auth rules — new ways to be a principal
- zkLogin / Enoki assessor — stamp if the request carries a valid zk proof for an OAuth identity.
- Passkey / WebAuthn cap — a cap unlocked by a device passkey signature.
- M-of-N threshold — release only once N distinct co-signer caps have stamped (on-chain multisig).
- NFT- or attestation-gated — whoever holds a membership NFT or a SuiNS/attestation may spend.
- Time-locked cap — a cap that only becomes valid after a date (vesting cliff, scheduled handoff).
- Oracle-signed geofence / KYC — stamp only with a fresh signed claim from an approved provider.
Caveat rules — new conditions on a spend
- Time window — business hours only, or after a vesting cliff, or before an expiry.
- Velocity / rate limit — at most N spends or X total per rolling day; a cooldown between spends.
- Price / TWAP guard — only allow a swap-spend if the oracle price sits inside a band (anti-MEV).
- Spend-category budget — separate envelopes (groceries vs. entertainment) each with their own cap.
- Recipient denylist / category — the inverse of the allowlist, or "only approved protocol contracts".
- Quorum approval — a human (or second agent) must co-stamp spends over a threshold.
- Dead-man / inactivity gate — only releases after the owner has been silent for N days.
- Destination memo required — enforce an invoice id / reference tag for accounting.
Each is a tiny, separately-deployed package — so one good rule (say, a velocity limiter) becomes infrastructure every Sup owner can attach.
#Reference
The reference caveat rule (public-interface only) is policy_rule_recipient_allowlist. The built-in cap_auth (auth) and sub_delegate (auth + caveat) packages are worked examples. Full design lives in the contract's DELEGATION_POLICY.md and ARCHITECTURE.md.
#Use the agent skill
This whole guide is packaged as an installable skill so your coding agent can scaffold a conforming rule (witness + enforce/prove + tests + manifest) and register it for you.
📥 View / download the skill → /llms/policy-rule-dev.md
· on GitHub: view ·
raw
Save it for your agent: curl -sL $SUP/llms/policy-rule-dev.md -o SKILL.md. To design a policy (which rules to compose) rather than write one, see the Delegation policy guide's design skill.
