receipt.gno
7.94 Kb · 250 lines
1// Package receipt is an append-only log of agent execution receipts.
2//
3// The thesis: provenance is not correctness. A chain can prove *who*
4// committed to an output, *when*, that the bytes have not changed since,
5// and *what* was staked or attested — but it cannot, by itself, prove the
6// output was any good. Correctness comes from an external validation
7// mechanism.
8//
9// So a receipt stores commitments (hashes) to the task, inputs, outputs,
10// tool calls and runtime, never the raw data — that lives off-chain. The
11// realm then lets independent validators attach attestations: "I re-ran
12// this and it reproduced", "the tests pass", "the output matches the
13// commitment", or "rejected". The receipt's trust level is exactly the sum
14// of the attestations it has collected, and nothing more.
15package receipt
16
17import (
18 "chain"
19 "chain/runtime"
20 "strings"
21
22 "gno.land/p/nt/ufmt/v0"
23)
24
25// Verdicts a validator can record against a receipt.
26const (
27 VerdictReproduced = "reproduced" // re-ran and got the same output commitment
28 VerdictTestsPass = "tests-pass" // the referenced test suite passed
29 VerdictOutputMatch = "output-match" // output matches its commitment
30 VerdictPolicyOK = "policy-ok" // no forbidden tool / policy respected
31 VerdictRejected = "rejected" // the work is wrong or invalid
32)
33
34// Attestation is an independent validator's verdict on a receipt.
35type Attestation struct {
36 Validator address
37 Verdict string
38 Note string
39 Height int64
40}
41
42// Receipt commits to one agent action. Every field except Attestations is
43// immutable once recorded.
44type Receipt struct {
45 Seq uint64
46 Agent string // agent id (e.g. a gno.land/r/g1manfred47kzduec920z88wfr64ylksmdcedlf5/agents/passport id)
47 TaskHash string // commitment to the task spec
48 InputCommitment string
49 OutputCommitment string
50 RuntimeID string // which runtime/model produced it
51 PolicyVersion string // policy in force at execution time
52 ToolCallsRoot string // root hash over the tool-call trace
53 HumanIntervened bool
54 Author address
55 Height int64
56 Attestations []Attestation
57}
58
59var receipts []*Receipt // index 0 => Seq 1
60
61// Record appends a new execution receipt and returns its sequence number.
62// The caller commits to opaque hashes; the realm never sees raw payloads.
63func Record(
64 cur realm,
65 agent, taskHash, inputCommitment, outputCommitment,
66 runtimeID, policyVersion, toolCallsRoot string,
67 humanIntervened bool,
68) uint64 {
69 assert(agent != "", "empty agent id")
70 assert(taskHash != "", "empty task hash")
71 assert(outputCommitment != "", "empty output commitment")
72
73 seq := uint64(len(receipts)) + 1
74 r := &Receipt{
75 Seq: seq,
76 Agent: agent,
77 TaskHash: taskHash,
78 InputCommitment: inputCommitment,
79 OutputCommitment: outputCommitment,
80 RuntimeID: runtimeID,
81 PolicyVersion: policyVersion,
82 ToolCallsRoot: toolCallsRoot,
83 HumanIntervened: humanIntervened,
84 Author: cur.Previous().Address(),
85 Height: runtime.ChainHeight(),
86 }
87 receipts = append(receipts, r)
88 chain.Emit("ReceiptRecorded",
89 "seq", ufmt.Sprintf("%d", seq),
90 "agent", agent,
91 "output", outputCommitment,
92 )
93 return seq
94}
95
96// Attest attaches an independent validator's verdict to a receipt. The same
97// validator cannot attest twice; the author of a receipt cannot attest to
98// their own work (self-attestation proves nothing).
99func Attest(cur realm, seq uint64, verdict, note string) {
100 r := mustGet(seq)
101 validator := cur.Previous().Address()
102 assert(validator != r.Author, "author cannot attest to their own receipt")
103 assertVerdict(verdict)
104 for _, a := range r.Attestations {
105 assert(a.Validator != validator, "validator already attested")
106 }
107 r.Attestations = append(r.Attestations, Attestation{
108 Validator: validator,
109 Verdict: verdict,
110 Note: note,
111 Height: runtime.ChainHeight(),
112 })
113 chain.Emit("ReceiptAttested",
114 "seq", ufmt.Sprintf("%d", seq),
115 "validator", validator.String(),
116 "verdict", verdict,
117 )
118}
119
120// ---- read-only API ----
121
122// Get returns a copy of a receipt by sequence number.
123func Get(seq uint64) Receipt { return *mustGet(seq) }
124
125// Count returns the number of receipts recorded.
126func Count() int { return len(receipts) }
127
128// Confirmations returns (positive, rejections) attestation counts. It is a
129// count of independent verdicts, deliberately NOT a truth value: a caller
130// decides what threshold it trusts.
131func Confirmations(seq uint64) (positive, rejections int) {
132 for _, a := range mustGet(seq).Attestations {
133 if a.Verdict == VerdictRejected {
134 rejections++
135 } else {
136 positive++
137 }
138 }
139 return positive, rejections
140}
141
142// ---- internal ----
143
144func mustGet(seq uint64) *Receipt {
145 assert(seq >= 1 && seq <= uint64(len(receipts)), "unknown receipt")
146 return receipts[seq-1]
147}
148
149func assertVerdict(v string) {
150 switch v {
151 case VerdictReproduced, VerdictTestsPass, VerdictOutputMatch, VerdictPolicyOK, VerdictRejected:
152 return
153 }
154 panic("unknown verdict: " + v)
155}
156
157func assert(cond bool, msg string) {
158 if !cond {
159 panic(msg)
160 }
161}
162
163// Render shows the receipt log, or a single receipt at :<seq>.
164func Render(path string) string {
165 if path == "" {
166 return renderIndex()
167 }
168 seq := parseSeq(path)
169 return renderReceipt(seq)
170}
171
172func renderIndex() string {
173 var sb strings.Builder
174 sb.WriteString("# Agent Execution Receipts\n\n")
175 sb.WriteString("_Provenance is not correctness._ Each row commits to an agent action; ")
176 sb.WriteString("trust comes only from independent attestations.\n\n")
177 if len(receipts) == 0 {
178 sb.WriteString("_No receipts recorded yet._\n")
179 return sb.String()
180 }
181 sb.WriteString("| # | Agent | Output commitment | Human? | ✅ | ❌ |\n")
182 sb.WriteString("|---|---|---|---|---|---|\n")
183 for _, r := range receipts {
184 pos, rej := 0, 0
185 for _, a := range r.Attestations {
186 if a.Verdict == VerdictRejected {
187 rej++
188 } else {
189 pos++
190 }
191 }
192 human := "no"
193 if r.HumanIntervened {
194 human = "yes"
195 }
196 sb.WriteString(ufmt.Sprintf("| [%d](/r/g1manfred47kzduec920z88wfr64ylksmdcedlf5/agents/receipt:%d) | %s | `%s` | %s | %d | %d |\n",
197 r.Seq, r.Seq, r.Agent, shortHash(r.OutputCommitment), human, pos, rej))
198 }
199 return sb.String()
200}
201
202func renderReceipt(seq uint64) string {
203 r := mustGet(seq)
204 var sb strings.Builder
205 sb.WriteString(ufmt.Sprintf("# Receipt #%d\n\n", r.Seq))
206 sb.WriteString(ufmt.Sprintf("- **Agent:** %s\n", r.Agent))
207 sb.WriteString(ufmt.Sprintf("- **Author:** %s\n", r.Author.String()))
208 sb.WriteString(ufmt.Sprintf("- **Recorded at height:** %d\n", r.Height))
209 sb.WriteString(ufmt.Sprintf("- **Runtime:** %s\n", r.RuntimeID))
210 sb.WriteString(ufmt.Sprintf("- **Policy version:** %s\n", r.PolicyVersion))
211 sb.WriteString(ufmt.Sprintf("- **Human intervened:** %t\n\n", r.HumanIntervened))
212
213 sb.WriteString("## Commitments\n\n")
214 sb.WriteString("The realm stores only these hashes; the payloads live off-chain.\n\n")
215 sb.WriteString(ufmt.Sprintf("- **Task:** `%s`\n", r.TaskHash))
216 sb.WriteString(ufmt.Sprintf("- **Input:** `%s`\n", r.InputCommitment))
217 sb.WriteString(ufmt.Sprintf("- **Output:** `%s`\n", r.OutputCommitment))
218 sb.WriteString(ufmt.Sprintf("- **Tool-calls root:** `%s`\n\n", r.ToolCallsRoot))
219
220 pos, rej := Confirmations(seq)
221 sb.WriteString(ufmt.Sprintf("## Attestations (%d ✅ / %d ❌)\n\n", pos, rej))
222 if len(r.Attestations) == 0 {
223 sb.WriteString("> Unverified. This receipt proves the commitment above was made, nothing more.\n")
224 return sb.String()
225 }
226 sb.WriteString("| Validator | Verdict | Note | Height |\n")
227 sb.WriteString("|---|---|---|---|\n")
228 for _, a := range r.Attestations {
229 sb.WriteString(ufmt.Sprintf("| %s | %s | %s | %d |\n",
230 shortHash(a.Validator.String()), a.Verdict, a.Note, a.Height))
231 }
232 return sb.String()
233}
234
235func parseSeq(s string) uint64 {
236 var n uint64
237 for i := 0; i < len(s); i++ {
238 c := s[i]
239 assert(c >= '0' && c <= '9', "invalid receipt path: "+s)
240 n = n*10 + uint64(c-'0')
241 }
242 return n
243}
244
245func shortHash(s string) string {
246 if len(s) <= 12 {
247 return s
248 }
249 return s[:6] + "…" + s[len(s)-4:]
250}