Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

README.md

4.39 Kb · 111 lines

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

 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

  • 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 — moul's versioned gno.land contracts. See the repository for the full catalog, build/test tooling, and usage.

On mainnet: deployment status transactions unique callers deployed revision

Dependency graph:

gno.land/r/moul/agents/capwallet/v0 dependency graph

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