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}