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.

passport.gno

9.87 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/v0:<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/moul/agents/passport/v0:%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}