← Back to Realms

Realm

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

Overview

Realm Path
gno.land/r/moul/agents/capwallet/v0
Exported Functions
7
State Entries
3
Source Files
3
Total Package Entries
19

Exported Functions

7 exported functions

State

3 state entries

Source Code

FILES
capwallet.gno
go
1// Package capwallet issues narrow, bounded, revocable capabilities to
2// agents — instead of handing an agent a wallet key and hoping its prompt
3// stays safe.
4//
5// The demo makes three ideas that are usually conflated stand apart:
6//
7//   - identity      — "this is agent Percy"
8//   - authorization — "Percy may call this function on this realm"
9//   - approval      — "Percy may do it at most N times, for ≤ M coins,
10//                      before block H, and I can revoke it at any moment"
11//
12// A capability is the third thing: a least-privilege grant with a ceiling,
13// an expiry, a use counter and a kill switch. Other realms consult
14// Authorized() before honoring an agent action; the granter revokes with
15// one call the instant something looks wrong.
16package capwallet
17
18import (
19	"chain"
20	"chain/runtime"
21	"strings"
22
23	"gno.land/p/nt/ufmt/v0"
24)
25
26// Use records one exercise of a capability.
27type Use struct {
28	By     address
29	Coins  int64
30	Height int64
31}
32
33// Capability is a least-privilege grant from a granter to a principal.
34type Capability struct {
35	ID            uint64
36	Granter       address
37	Principal     address // the only address that may exercise it
38	TargetRealm   string  // realm the capability is scoped to
39	Function      string  // function the capability authorizes
40	MaxCoins      int64   // per-exercise ceiling in ugnot (0 = none allowed)
41	ValidUntil    int64   // block height after which it expires (0 = never)
42	RemainingUses uint32  // exercises left (0 = exhausted)
43	ArgsPolicy    string  // human-readable note on allowed arguments
44	Revoked       bool
45	GrantedAt     int64
46	Uses          []Use
47}
48
49var caps []*Capability // index 0 => ID 1
50
51// Grant issues a capability. The caller becomes its granter and can revoke
52// it later. validUntil is an absolute block height (0 = no expiry).
53func Grant(
54	cur realm,
55	principal address,
56	targetRealm, function string,
57	maxCoins, validUntil int64,
58	uses uint32,
59	argsPolicy string,
60) uint64 {
61	assert(targetRealm != "" && function != "", "target realm and function required")
62	assert(uses > 0, "must grant at least one use")
63
64	id := uint64(len(caps)) + 1
65	c := &Capability{
66		ID:            id,
67		Granter:       cur.Previous().Address(),
68		Principal:     principal,
69		TargetRealm:   targetRealm,
70		Function:      function,
71		MaxCoins:      maxCoins,
72		ValidUntil:    validUntil,
73		RemainingUses: uses,
74		ArgsPolicy:    argsPolicy,
75		GrantedAt:     runtime.ChainHeight(),
76	}
77	caps = append(caps, c)
78	chain.Emit("CapabilityGranted",
79		"id", ufmt.Sprintf("%d", id),
80		"principal", principal.String(),
81		"target", targetRealm+"."+function,
82	)
83	return id
84}
85
86// Exercise consumes one use of a capability. Only the principal may call it,
87// and every bound is checked: not revoked, not expired, uses remaining, and
88// coins within the ceiling. On success the use is recorded and the counter
89// decremented.
90func Exercise(cur realm, id uint64, coins int64) {
91	c := mustGet(id)
92	caller := cur.Previous().Address()
93	assert(caller == c.Principal, "caller is not the capability principal")
94	assertUsable(c)
95	assert(coins <= c.MaxCoins, ufmt.Sprintf("coins %d exceed capability ceiling %d", coins, c.MaxCoins))
96
97	c.RemainingUses--
98	c.Uses = append(c.Uses, Use{By: caller, Coins: coins, Height: runtime.ChainHeight()})
99	chain.Emit("CapabilityExercised",
100		"id", ufmt.Sprintf("%d", id),
101		"coins", ufmt.Sprintf("%d", coins),
102		"remaining", ufmt.Sprintf("%d", c.RemainingUses),
103	)
104}
105
106// Revoke disables a capability immediately. Only the granter may revoke.
107func Revoke(cur realm, id uint64) {
108	c := mustGet(id)
109	assert(cur.Previous().Address() == c.Granter, "only the granter may revoke")
110	c.Revoked = true
111	chain.Emit("CapabilityRevoked", "id", ufmt.Sprintf("%d", id))
112}
113
114// ---- read-only API for gating realms ----
115
116// Authorized reports whether principal may exercise capability id for the
117// given coin amount right now — the check a target realm runs before acting.
118// It never mutates state or consumes a use.
119func Authorized(id uint64, principal address, coins int64) bool {
120	if id < 1 || id > uint64(len(caps)) {
121		return false
122	}
123	c := caps[id-1]
124	if c.Principal != principal || !usable(c) {
125		return false
126	}
127	return coins <= c.MaxCoins
128}
129
130// Get returns a copy of a capability.
131func Get(id uint64) Capability { return *mustGet(id) }
132
133// Count returns the number of capabilities issued.
134func Count() int { return len(caps) }
135
136// ---- internal ----
137
138func usable(c *Capability) bool {
139	if c.Revoked || c.RemainingUses == 0 {
140		return false
141	}
142	if c.ValidUntil != 0 && runtime.ChainHeight() > c.ValidUntil {
143		return false
144	}
145	return true
146}
147
148func assertUsable(c *Capability) {
149	assert(!c.Revoked, "capability revoked")
150	assert(c.RemainingUses > 0, "capability exhausted")
151	assert(c.ValidUntil == 0 || runtime.ChainHeight() <= c.ValidUntil, "capability expired")
152}
153
154func mustGet(id uint64) *Capability {
155	assert(id >= 1 && id <= uint64(len(caps)), "unknown capability")
156	return caps[id-1]
157}
158
159func assert(cond bool, msg string) {
160	if !cond {
161		panic(msg)
162	}
163}
164
165// Render shows all capabilities, or one capability's detail + usage at :<id>.
166func Render(path string) string {
167	if path == "" {
168		return renderIndex()
169	}
170	return renderCap(parseID(path))
171}
172
173func renderIndex() string {
174	var sb strings.Builder
175	sb.WriteString("# Capability Wallet\n\n")
176	sb.WriteString("_Don't give an agent a wallet. Give it a capability._\n\n")
177	if len(caps) == 0 {
178		sb.WriteString("_No capabilities granted yet._\n")
179		return sb.String()
180	}
181	sb.WriteString("| # | Principal | Scope | Ceiling | Uses left | State |\n")
182	sb.WriteString("|---|---|---|---|---|---|\n")
183	for _, c := range caps {
184		sb.WriteString(ufmt.Sprintf("| [%d](/r/moul/agents/capwallet/v0:%d) | %s | `%s.%s` | %d ugnot | %d | %s |\n",
185			c.ID, c.ID, short(c.Principal), c.TargetRealm, c.Function, c.MaxCoins, c.RemainingUses, state(c)))
186	}
187	return sb.String()
188}
189
190func renderCap(id uint64) string {
191	c := mustGet(id)
192	var sb strings.Builder
193	sb.WriteString(ufmt.Sprintf("# Capability #%d\n\n", c.ID))
194	sb.WriteString(ufmt.Sprintf("- **State:** %s\n", state(c)))
195	sb.WriteString(ufmt.Sprintf("- **Granter:** %s\n", c.Granter.String()))
196	sb.WriteString(ufmt.Sprintf("- **Principal:** %s\n", c.Principal.String()))
197	sb.WriteString(ufmt.Sprintf("- **Scope:** `%s.%s`\n", c.TargetRealm, c.Function))
198	sb.WriteString(ufmt.Sprintf("- **Per-use ceiling:** %d ugnot\n", c.MaxCoins))
199	if c.ValidUntil == 0 {
200		sb.WriteString("- **Expires:** never\n")
201	} else {
202		sb.WriteString(ufmt.Sprintf("- **Expires at height:** %d (now %d)\n", c.ValidUntil, runtime.ChainHeight()))
203	}
204	sb.WriteString(ufmt.Sprintf("- **Uses remaining:** %d\n", c.RemainingUses))
205	sb.WriteString(ufmt.Sprintf("- **Args policy:** %s\n\n", c.ArgsPolicy))
206
207	sb.WriteString(ufmt.Sprintf("## Exercise log (%d)\n\n", len(c.Uses)))
208	if len(c.Uses) == 0 {
209		sb.WriteString("_Never exercised._\n")
210		return sb.String()
211	}
212	sb.WriteString("| By | Coins | Height |\n|---|---|---|\n")
213	for _, u := range c.Uses {
214		sb.WriteString(ufmt.Sprintf("| %s | %d | %d |\n", short(u.By), u.Coins, u.Height))
215	}
216	return sb.String()
217}
218
219func state(c *Capability) string {
220	if c.Revoked {
221		return "🚫 revoked"
222	}
223	if c.RemainingUses == 0 {
224		return "· exhausted"
225	}
226	if c.ValidUntil != 0 && runtime.ChainHeight() > c.ValidUntil {
227		return "⏳ expired"
228	}
229	return "✅ live"
230}
231
232func parseID(s string) uint64 {
233	var n uint64
234	for i := 0; i < len(s); i++ {
235		ch := s[i]
236		assert(ch >= '0' && ch <= '9', "invalid capability path: "+s)
237		n = n*10 + uint64(ch-'0')
238	}
239	return n
240}
241
242func short(a address) string {
243	s := a.String()
244	if len(s) <= 12 {
245		return s
246	}
247	return s[:8] + "…" + s[len(s)-4:]
248}
249

Raw Package Data

Raw JSON data