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.

capwallet.gno

7.49 Kb · 248 lines
  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}