# Capability Wallet — don't give your agent a wallet, give it a capability The default way to let an agent spend or act on-chain is to hand it a private key and hope its prompt is safe. That is a category error. A key is *all your authority*; a prompt is a suggestion. The moment the agent is confused, jailbroken, or just wrong, it has the full key. Three things get conflated and shouldn't: - **identity** — "this is agent Percy" - **authorization** — "Percy may call `Assign` on `r/moul/issues`" - **approval** — "Percy may do it **at most twice**, for **≤ 5 GNOT each**, **before block H**, and I can **revoke** it instantly" Identity is a passport. Authorization is a role. *Approval* is a capability: a narrow, bounded, expiring, revocable grant. This realm issues the third kind. ## The grant ```go type Capability struct { Granter address Principal address // the ONLY address that may exercise it TargetRealm string // scoped to one realm Function string // scoped to one function MaxCoins int64 // per-exercise ceiling ValidUntil int64 // absolute block height, 0 = never RemainingUses uint32 // a use counter Revoked bool // a kill switch Uses []Use // an audit trail } ``` Issuing one is a single call: ```go // Percy may call r/moul/issues.Assign, ≤5 GNOT, twice, no expiry. id := capwallet.Grant(cross(cur), percyAddr, "gno.land/r/moul/issues", "Assign", 5_000_000, // per-use ceiling (ugnot) 0, // no expiry 2, // two uses "issue IDs 100-200") ``` ## Two consumers, two shapes A **target realm** gates an action with a read-only, side-effect-free check: ```go if !capwallet.Authorized(id, caller, coins) { panic("not authorized") } ``` The **agent** consumes a use when it acts. Every bound is enforced at once — right principal, not revoked, not expired, uses remaining, under the ceiling — and the exercise is logged: ```go capwallet.Exercise(cross(cur), id, 3_000_000) // 1 use left capwallet.Exercise(cross(cur), id, 1_000_000) // exhausted ``` And the granter kills it the instant something looks wrong: ```go capwallet.Revoke(cross(cur), id) // Authorized() now returns false for everyone ``` ## Why this is a real safety primitive The blast radius of a compromised agent is exactly the union of its live capabilities — not its whole wallet. You can reason about "what is the worst this agent can do right now" by reading a table, and shrink it to zero with one transaction. That is not something a system prompt can give you. Browse the wallet at the realm root; each capability's bounds and full exercise log live at `:`. ```sh gno test . ``` ## Limitations - `ArgsPolicy` is a human-readable note, not an enforced predicate. Real argument-level constraints ("only issue IDs 100–200") need the target realm to check them, or a richer on-chain policy language. This demo scopes to *realm + function + coins + uses + expiry*, which is already most of the value. - There's no delegation graph (a capability that can mint narrower capabilities). That's a natural extension, not a starting point. --- Part of **[moul/gno-contracts](https://github.com/moul/gno-contracts)** — moul's versioned gno.land contracts. See the repository for the full catalog, build/test tooling, and usage. **Dependency graph:** ![gno.land/r/moul/agents/capwallet/v0 dependency graph](https://raw.githubusercontent.com/moul/gno-contracts/main/_assets/gno.land/r/moul/agents/capwallet/v0/deps.png) > ⚠️ **Disclaimer:** provided as-is, without warranty; not security-audited. Full disclaimer: [DISCLAIMER](https://github.com/moul/gno-contracts/blob/main/DISCLAIMER.md).