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

passport.gno

9.89 Kb · 321 lines
  1// Package passport is an on-chain identity and lifecycle registry for
  2// autonomous (AI) agents.
  3//
  4// The premise: a long-lived agent identity is *weak* evidence. The code,
  5// model, prompt, owner, operating keys and runtime behind an identity can
  6// all change silently. A blockchain cannot preserve an agent's cognition;
  7// it can only preserve the *history* of what an identity claimed to be and
  8// who controlled it, in an order nobody can rewrite.
  9//
 10// So this realm does not try to prove an agent is trustworthy. It records
 11// registration, ownership transfers, operator changes and — crucially —
 12// runtime changes, and it surfaces them so a reader can judge how much the
 13// identity has drifted since it was created.
 14package passport
 15
 16import (
 17	"chain"
 18	"chain/runtime"
 19	"strings"
 20
 21	"gno.land/p/nt/avl/v0"
 22	"gno.land/p/nt/ufmt/v0"
 23)
 24
 25// Status is the lifecycle state of an agent identity.
 26type Status string
 27
 28const (
 29	StatusActive    Status = "active"
 30	StatusSuspended Status = "suspended"
 31	StatusRetired   Status = "retired"
 32)
 33
 34// Endpoint advertises where the agent can be reached or discovered
 35// (e.g. an MCP server, an A2A endpoint, a DID document, an ENS name).
 36type Endpoint struct {
 37	Kind string // "mcp", "a2a", "did", "url", ...
 38	URI  string
 39}
 40
 41// Event is one entry in an agent's append-only lifecycle log.
 42type Event struct {
 43	Height int64
 44	Actor  address
 45	Kind   string // "register", "transfer", "add-operator", "set-runtime", ...
 46	Detail string
 47}
 48
 49// Agent is a persistent, importable identity object. Other realms can
 50// import this package and ask questions like IsActive / IsOperator
 51// directly, instead of parsing NFT metadata scattered across contracts.
 52type Agent struct {
 53	ID        string
 54	Owner     address
 55	Operators []address
 56	Runtime   string // free-form runtime descriptor, e.g. "claude-opus-4-8@<commit>"
 57	Endpoints []Endpoint
 58	Caps      []string // declared capability slugs
 59	Status    Status
 60	CreatedAt int64
 61	UpdatedAt int64
 62	History   []Event
 63}
 64
 65var agents = avl.NewTree() // id -> *Agent
 66
 67// Register creates a new agent identity owned by the caller. The id must
 68// be a non-empty, previously unused slug.
 69func Register(cur realm, id, runtimeDesc string) {
 70	assert(id != "", "empty agent id")
 71	assert(!agents.Has(id), "agent id already registered: "+id)
 72
 73	caller := cur.Previous().Address()
 74	h := runtime.ChainHeight()
 75	a := &Agent{
 76		ID:        id,
 77		Owner:     caller,
 78		Runtime:   runtimeDesc,
 79		Status:    StatusActive,
 80		CreatedAt: h,
 81		UpdatedAt: h,
 82	}
 83	a.log(h, caller, "register", "runtime="+runtimeDesc)
 84	agents.Set(id, a)
 85	chain.Emit("AgentRegistered", "id", id, "owner", caller.String())
 86}
 87
 88// Transfer hands ownership of an agent to a new address. Only the current
 89// owner may call it. Ownership changes are the single most important thing
 90// to keep visible: the operator behind an identity may become someone else.
 91func Transfer(cur realm, id string, newOwner address) {
 92	a, caller := mustOwn(cur, id)
 93	old := a.Owner
 94	a.Owner = newOwner
 95	a.touch(caller, "transfer", old.String()+" -> "+newOwner.String())
 96	chain.Emit("AgentTransferred", "id", id, "from", old.String(), "to", newOwner.String())
 97}
 98
 99// AddOperator authorizes an additional address to act as this agent.
100func AddOperator(cur realm, id string, op address) {
101	a, caller := mustOwn(cur, id)
102	for _, e := range a.Operators {
103		assert(e != op, "already an operator")
104	}
105	a.Operators = append(a.Operators, op)
106	a.touch(caller, "add-operator", op.String())
107	chain.Emit("AgentOperatorAdded", "id", id, "operator", op.String())
108}
109
110// SetRuntime records a change of the agent's runtime/model. This is logged
111// loudly: a runtime change means the thing acting under this identity is
112// (partly) a different thing than before.
113func SetRuntime(cur realm, id, runtimeDesc string) {
114	a, caller := mustOwn(cur, id)
115	old := a.Runtime
116	a.Runtime = runtimeDesc
117	a.touch(caller, "set-runtime", old+" -> "+runtimeDesc)
118	chain.Emit("AgentRuntimeChanged", "id", id, "runtime", runtimeDesc)
119}
120
121// AddEndpoint advertises a discovery/interaction endpoint.
122func AddEndpoint(cur realm, id, kind, uri string) {
123	a, caller := mustOwn(cur, id)
124	a.Endpoints = append(a.Endpoints, Endpoint{Kind: kind, URI: uri})
125	a.touch(caller, "add-endpoint", kind+":"+uri)
126}
127
128// DeclareCapability records a capability the agent claims to offer. Note:
129// this is a *claim*, not proof. Validation belongs to other realms
130// (see the receipt and gnomem demos).
131func DeclareCapability(cur realm, id, capab string) {
132	a, caller := mustOwn(cur, id)
133	a.Caps = append(a.Caps, capab)
134	a.touch(caller, "declare-capability", capab)
135}
136
137// Suspend and Retire move the lifecycle forward. Retire is terminal.
138func Suspend(cur realm, id string) { setStatus(cur, id, StatusSuspended) }
139func Retire(cur realm, id string)  { setStatus(cur, id, StatusRetired) }
140
141// Reactivate returns a suspended agent to active. A retired agent cannot
142// be reactivated.
143func Reactivate(cur realm, id string) {
144	a, caller := mustOwn(cur, id)
145	assert(a.Status != StatusRetired, "retired agents cannot be reactivated")
146	a.Status = StatusActive
147	a.touch(caller, "reactivate", "")
148}
149
150// ---- read-only API other realms and readers can rely on ----
151
152// Get returns a copy of the agent record; panics if unknown.
153func Get(id string) Agent {
154	return *mustGet(id)
155}
156
157// Exists reports whether an id is registered.
158func Exists(id string) bool { return agents.Has(id) }
159
160// IsActive reports whether the agent exists and is in the active state.
161func IsActive(id string) bool {
162	if !agents.Has(id) {
163		return false
164	}
165	return agents.Get(id).(*Agent).Status == StatusActive
166}
167
168// IsOperator reports whether addr is the owner or a listed operator of an
169// active agent — the check other realms should gate agent actions on.
170func IsOperator(id string, addr address) bool {
171	if !agents.Has(id) {
172		return false
173	}
174	a := agents.Get(id).(*Agent)
175	if a.Status != StatusActive {
176		return false
177	}
178	if a.Owner == addr {
179		return true
180	}
181	for _, op := range a.Operators {
182		if op == addr {
183			return true
184		}
185	}
186	return false
187}
188
189// Count returns the number of registered agents.
190func Count() int { return agents.Size() }
191
192// ---- internal helpers ----
193
194func setStatus(cur realm, id string, s Status) {
195	a, caller := mustOwn(cur, id)
196	assert(a.Status != StatusRetired, "agent is retired")
197	a.Status = s
198	a.touch(caller, "status", string(s))
199	chain.Emit("AgentStatusChanged", "id", id, "status", string(s))
200}
201
202func mustGet(id string) *Agent {
203	assert(agents.Has(id), "unknown agent: "+id)
204	return agents.Get(id).(*Agent)
205}
206
207func mustOwn(cur realm, id string) (*Agent, address) {
208	a := mustGet(id)
209	caller := cur.Previous().Address()
210	assert(caller == a.Owner, "caller is not the owner of "+id)
211	return a, caller
212}
213
214func (a *Agent) log(h int64, actor address, kind, detail string) {
215	a.History = append(a.History, Event{Height: h, Actor: actor, Kind: kind, Detail: detail})
216}
217
218func (a *Agent) touch(actor address, kind, detail string) {
219	h := runtime.ChainHeight()
220	a.UpdatedAt = h
221	a.log(h, actor, kind, detail)
222}
223
224func assert(cond bool, msg string) {
225	if !cond {
226		panic(msg)
227	}
228}
229
230// Render shows the registry, or a single agent's profile + lifecycle log
231// at r/moul/agents/passport:<id>. The profile deliberately foregrounds
232// drift: how many owner and runtime changes have happened since creation.
233func Render(path string) string {
234	if path == "" {
235		return renderIndex()
236	}
237	return renderAgent(path)
238}
239
240func renderIndex() string {
241	var sb strings.Builder
242	sb.WriteString("# Agent Passport\n\n")
243	sb.WriteString(ufmt.Sprintf("On-chain identity & lifecycle registry — %d agent(s).\n\n", agents.Size()))
244	if agents.Size() == 0 {
245		sb.WriteString("_No agents registered yet._\n")
246		return sb.String()
247	}
248	sb.WriteString("| Agent | Owner | Runtime | Status |\n")
249	sb.WriteString("|---|---|---|---|\n")
250	agents.Iterate("", "", func(key string, v any) bool {
251		a := v.(*Agent)
252		sb.WriteString(ufmt.Sprintf("| [%s](/r/g1manfred47kzduec920z88wfr64ylksmdcedlf5/agents/passport:%s) | %s | %s | %s |\n",
253			a.ID, a.ID, short(a.Owner), a.Runtime, string(a.Status)))
254		return false
255	})
256	return sb.String()
257}
258
259func renderAgent(id string) string {
260	a := mustGet(id)
261	var sb strings.Builder
262	sb.WriteString(ufmt.Sprintf("# Agent: %s\n\n", a.ID))
263	sb.WriteString(ufmt.Sprintf("- **Status:** %s\n", string(a.Status)))
264	sb.WriteString(ufmt.Sprintf("- **Owner:** %s\n", a.Owner.String()))
265	sb.WriteString(ufmt.Sprintf("- **Runtime:** %s\n", a.Runtime))
266	sb.WriteString(ufmt.Sprintf("- **Created at height:** %d\n", a.CreatedAt))
267	sb.WriteString(ufmt.Sprintf("- **Last updated at height:** %d\n\n", a.UpdatedAt))
268
269	owners, runtimes := 0, 0
270	for _, e := range a.History {
271		switch e.Kind {
272		case "transfer":
273			owners++
274		case "set-runtime":
275			runtimes++
276		}
277	}
278	sb.WriteString("## Drift\n\n")
279	sb.WriteString(ufmt.Sprintf("What the chain actually proves about this identity — how much it has changed:\n\n"))
280	sb.WriteString(ufmt.Sprintf("- Ownership changes since creation: **%d**\n", owners))
281	sb.WriteString(ufmt.Sprintf("- Runtime changes since creation: **%d**\n\n", runtimes))
282
283	if len(a.Operators) > 0 {
284		sb.WriteString("## Operators\n\n")
285		for _, op := range a.Operators {
286			sb.WriteString("- " + op.String() + "\n")
287		}
288		sb.WriteString("\n")
289	}
290	if len(a.Endpoints) > 0 {
291		sb.WriteString("## Endpoints\n\n")
292		for _, e := range a.Endpoints {
293			sb.WriteString(ufmt.Sprintf("- `%s`: %s\n", e.Kind, e.URI))
294		}
295		sb.WriteString("\n")
296	}
297	if len(a.Caps) > 0 {
298		sb.WriteString("## Declared capabilities\n\n")
299		sb.WriteString("> These are self-declared claims, not proofs.\n\n")
300		for _, c := range a.Caps {
301			sb.WriteString("- " + c + "\n")
302		}
303		sb.WriteString("\n")
304	}
305
306	sb.WriteString("## Lifecycle log (append-only)\n\n")
307	sb.WriteString("| Height | Actor | Event | Detail |\n")
308	sb.WriteString("|---|---|---|---|\n")
309	for _, e := range a.History {
310		sb.WriteString(ufmt.Sprintf("| %d | %s | %s | %s |\n", e.Height, short(e.Actor), e.Kind, e.Detail))
311	}
312	return sb.String()
313}
314
315func short(a address) string {
316	s := a.String()
317	if len(s) <= 12 {
318		return s
319	}
320	return s[:8] + "…" + s[len(s)-4:]
321}