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

capwallet.gno

7.52 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/g1manfred47kzduec920z88wfr64ylksmdcedlf5/agents/capwallet:%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}