README.md
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
Assignonr/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
1type Capability struct {
2 Granter address
3 Principal address // the ONLY address that may exercise it
4 TargetRealm string // scoped to one realm
5 Function string // scoped to one function
6 MaxCoins int64 // per-exercise ceiling
7 ValidUntil int64 // absolute block height, 0 = never
8 RemainingUses uint32 // a use counter
9 Revoked bool // a kill switch
10 Uses []Use // an audit trail
11}
Issuing one is a single call:
1// Percy may call r/moul/issues.Assign, ≤5 GNOT, twice, no expiry.
2id := capwallet.Grant(cross(cur), percyAddr,
3 "gno.land/r/moul/issues", "Assign",
4 5_000_000, // per-use ceiling (ugnot)
5 0, // no expiry
6 2, // two uses
7 "issue IDs 100-200")
Two consumers, two shapes
A target realm gates an action with a read-only, side-effect-free check:
1if !capwallet.Authorized(id, caller, coins) {
2 panic("not authorized")
3}
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:
1capwallet.Exercise(cross(cur), id, 3_000_000) // 1 use left
2capwallet.Exercise(cross(cur), id, 1_000_000) // exhausted
And the granter kills it the instant something looks wrong:
1capwallet.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 :<id>.
1gno test .
Limitations
ArgsPolicyis 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 — moul's versioned gno.land contracts. See the repository for the full catalog, build/test tooling, and usage.
Dependency graph:

⚠️ Disclaimer: provided as-is, without warranty; not security-audited. Full disclaimer: DISCLAIMER.