maintainer.gno
9.35 Kb · 294 lines
1// Package maintainer models an AI agent stewarding a versioned on-chain
2// package — without ever letting it deploy on its own authority.
3//
4// The agent can do the tedious, valuable parts: open a version proposal,
5// collect reviews, record CI test attestations, write release notes,
6// deprecate a vulnerable version, keep a compatibility matrix. What it
7// cannot do is ship. Approval is gated by an explicit, machine-checked
8// policy:
9//
10// tests pass AND ≥ MinReviewers independent reviews AND
11// every review scores ≥ MinScore AND the challenge window has elapsed
12//
13// This is the difference between "an agent with commit access" and "an
14// agent that drives a process a human (or a DAO, or a policy) still gates."
15// The realm records *approved* releases; the actual package publish is a
16// separate, deliberately human/authority step.
17package maintainer
18
19import (
20 "chain"
21 "chain/runtime"
22 "strings"
23
24 "gno.land/p/nt/ufmt/v0"
25)
26
27const (
28 MinReviewers = 2 // independent reviews required
29 MinScore = 80 // minimum score every reviewer must give
30)
31
32// Review is one reviewer's score for a proposal.
33type Review struct {
34 By address
35 Score uint8 // 0..100
36 Note string
37 Height int64
38}
39
40// Proposal is a candidate new version working its way toward approval.
41type Proposal struct {
42 ID uint64
43 Version string
44 ArtifactHash string // commitment to the package source
45 Proposer address
46 Reviews []Review
47 TestsPass bool
48 TestsAttestor address
49 OpenedAt int64
50 ChallengeUntil int64 // approval blocked until height passes this
51 Approved bool
52 ApprovedAt int64
53}
54
55// Release is an approved (and possibly later deprecated) version.
56type Release struct {
57 Version string
58 ArtifactHash string
59 ReleasedAt int64
60 Deprecated bool
61 DeprecatedReason string
62}
63
64var (
65 proposals []*Proposal // index 0 => ID 1
66 releases []*Release // in release order
67 byVersion = map[string]*Release{}
68)
69
70// Propose opens a version proposal with a challenge window of the given
71// number of blocks (during which reviews accrue and approval is blocked).
72func Propose(cur realm, version, artifactHash string, challengeBlocks int64) uint64 {
73 assert(version != "", "empty version")
74 assert(artifactHash != "", "empty artifact hash")
75 assert(challengeBlocks >= 0, "negative challenge window")
76 _, exists := byVersion[version]
77 assert(!exists, "version already released: "+version)
78
79 id := uint64(len(proposals)) + 1
80 h := runtime.ChainHeight()
81 proposals = append(proposals, &Proposal{
82 ID: id,
83 Version: version,
84 ArtifactHash: artifactHash,
85 Proposer: cur.Previous().Address(),
86 OpenedAt: h,
87 ChallengeUntil: h + challengeBlocks,
88 })
89 chain.Emit("VersionProposed", "id", ufmt.Sprintf("%d", id), "version", version)
90 return id
91}
92
93// AddReview records a reviewer's score. Reviewers must be independent of the
94// proposer, and each address reviews at most once.
95func AddReview(cur realm, propID uint64, score uint8, note string) {
96 p := mustProposal(propID)
97 assert(!p.Approved, "proposal already approved")
98 assert(score <= 100, "score must be 0..100")
99 reviewer := cur.Previous().Address()
100 assert(reviewer != p.Proposer, "proposer cannot review own proposal")
101 for _, r := range p.Reviews {
102 assert(r.By != reviewer, "reviewer already reviewed")
103 }
104 p.Reviews = append(p.Reviews, Review{By: reviewer, Score: score, Note: note, Height: runtime.ChainHeight()})
105 chain.Emit("VersionReviewed", "id", ufmt.Sprintf("%d", propID), "score", ufmt.Sprintf("%d", score))
106}
107
108// AttestTests records whether CI's test suite passed for the proposal.
109func AttestTests(cur realm, propID uint64, pass bool) {
110 p := mustProposal(propID)
111 assert(!p.Approved, "proposal already approved")
112 p.TestsPass = pass
113 p.TestsAttestor = cur.Previous().Address()
114 chain.Emit("TestsAttested", "id", ufmt.Sprintf("%d", propID), "pass", boolStr(pass))
115}
116
117// Approve promotes a proposal to a release iff every policy condition holds.
118// It panics with the first unmet condition — the policy is the gate, not a
119// suggestion.
120func Approve(cur realm, propID uint64) {
121 p := mustProposal(propID)
122 assert(!p.Approved, "proposal already approved")
123 assert(p.TestsPass, "policy: tests have not passed")
124 assert(len(p.Reviews) >= MinReviewers, ufmt.Sprintf("policy: need %d reviews, have %d", MinReviewers, len(p.Reviews)))
125 for _, r := range p.Reviews {
126 assert(r.Score >= MinScore, ufmt.Sprintf("policy: review from %s scored %d < %d", short(r.By), r.Score, MinScore))
127 }
128 assert(runtime.ChainHeight() > p.ChallengeUntil, "policy: challenge window still open")
129
130 p.Approved = true
131 p.ApprovedAt = runtime.ChainHeight()
132 rel := &Release{Version: p.Version, ArtifactHash: p.ArtifactHash, ReleasedAt: p.ApprovedAt}
133 releases = append(releases, rel)
134 byVersion[p.Version] = rel
135 chain.Emit("VersionApproved", "id", ufmt.Sprintf("%d", propID), "version", p.Version)
136}
137
138// Deprecate flags a released version (e.g. a vulnerability was found).
139func Deprecate(cur realm, version, reason string) {
140 rel, ok := byVersion[version]
141 assert(ok, "unknown version: "+version)
142 rel.Deprecated = true
143 rel.DeprecatedReason = reason
144 chain.Emit("VersionDeprecated", "version", version)
145}
146
147// ---- read-only API ----
148
149// LatestVersion returns the most recently released version, or "".
150func LatestVersion() string {
151 if len(releases) == 0 {
152 return ""
153 }
154 return releases[len(releases)-1].Version
155}
156
157// IsDeprecated reports whether a released version is deprecated.
158func IsDeprecated(version string) bool {
159 rel, ok := byVersion[version]
160 return ok && rel.Deprecated
161}
162
163// GetProposal returns a copy of a proposal.
164func GetProposal(id uint64) Proposal { return *mustProposal(id) }
165
166// ---- internal ----
167
168func mustProposal(id uint64) *Proposal {
169 assert(id >= 1 && id <= uint64(len(proposals)), "unknown proposal")
170 return proposals[id-1]
171}
172
173func boolStr(b bool) string {
174 if b {
175 return "yes"
176 }
177 return "no"
178}
179
180func assert(cond bool, msg string) {
181 if !cond {
182 panic(msg)
183 }
184}
185
186// Render shows the release matrix + open proposals, or one proposal at :<id>.
187func Render(path string) string {
188 if path == "" {
189 return renderIndex()
190 }
191 return renderProposal(parseID(path))
192}
193
194func renderIndex() string {
195 var sb strings.Builder
196 sb.WriteString("# Agent Package Maintainer\n\n")
197 sb.WriteString(ufmt.Sprintf("_The agent drives the process; the policy still gates the ship._ "))
198 sb.WriteString(ufmt.Sprintf("Approval needs tests + ≥%d reviews each ≥%d + an elapsed challenge window.\n\n", MinReviewers, MinScore))
199
200 sb.WriteString("## Releases\n\n")
201 if len(releases) == 0 {
202 sb.WriteString("_No releases yet._\n\n")
203 } else {
204 sb.WriteString("| Version | Released @ | State |\n|---|---|---|\n")
205 for _, r := range releases {
206 st := "✅ current"
207 if r.Deprecated {
208 st = "⚠ deprecated: " + r.DeprecatedReason
209 }
210 sb.WriteString(ufmt.Sprintf("| %s | %d | %s |\n", r.Version, r.ReleasedAt, st))
211 }
212 sb.WriteString("\n")
213 }
214
215 sb.WriteString("## Proposals\n\n")
216 if len(proposals) == 0 {
217 sb.WriteString("_None._\n")
218 return sb.String()
219 }
220 sb.WriteString("| # | Version | Tests | Reviews | Approved |\n|---|---|---|---|---|\n")
221 for _, p := range proposals {
222 sb.WriteString(ufmt.Sprintf("| [%d](/r/g1manfred47kzduec920z88wfr64ylksmdcedlf5/agents/maintainer:%d) | %s | %s | %d | %t |\n",
223 p.ID, p.ID, p.Version, boolStr(p.TestsPass), len(p.Reviews), p.Approved))
224 }
225 return sb.String()
226}
227
228func renderProposal(id uint64) string {
229 p := mustProposal(id)
230 var sb strings.Builder
231 sb.WriteString(ufmt.Sprintf("# Proposal #%d — %s\n\n", p.ID, p.Version))
232 sb.WriteString(ufmt.Sprintf("- **Artifact:** `%s`\n", shortHash(p.ArtifactHash)))
233 sb.WriteString(ufmt.Sprintf("- **Proposer:** %s\n", short(p.Proposer)))
234 sb.WriteString(ufmt.Sprintf("- **Tests pass:** %t\n", p.TestsPass))
235 sb.WriteString(ufmt.Sprintf("- **Challenge window until height:** %d (now %d)\n", p.ChallengeUntil, runtime.ChainHeight()))
236 sb.WriteString(ufmt.Sprintf("- **Approved:** %t\n\n", p.Approved))
237
238 sb.WriteString(ufmt.Sprintf("## Policy checklist\n\n"))
239 sb.WriteString(check(p.TestsPass, "tests pass"))
240 sb.WriteString(check(len(p.Reviews) >= MinReviewers, ufmt.Sprintf("≥%d independent reviews (have %d)", MinReviewers, len(p.Reviews))))
241 minOK := true
242 for _, r := range p.Reviews {
243 if r.Score < MinScore {
244 minOK = false
245 }
246 }
247 sb.WriteString(check(len(p.Reviews) > 0 && minOK, ufmt.Sprintf("every review ≥%d", MinScore)))
248 sb.WriteString(check(runtime.ChainHeight() > p.ChallengeUntil, "challenge window elapsed"))
249 sb.WriteString("\n")
250
251 sb.WriteString(ufmt.Sprintf("## Reviews (%d)\n\n", len(p.Reviews)))
252 if len(p.Reviews) == 0 {
253 sb.WriteString("_None._\n")
254 return sb.String()
255 }
256 sb.WriteString("| Reviewer | Score | Note | Height |\n|---|---|---|---|\n")
257 for _, r := range p.Reviews {
258 sb.WriteString(ufmt.Sprintf("| %s | %d | %s | %d |\n", short(r.By), r.Score, r.Note, r.Height))
259 }
260 return sb.String()
261}
262
263func check(ok bool, label string) string {
264 box := "❌"
265 if ok {
266 box = "✅"
267 }
268 return ufmt.Sprintf("- %s %s\n", box, label)
269}
270
271func parseID(s string) uint64 {
272 var n uint64
273 for i := 0; i < len(s); i++ {
274 ch := s[i]
275 assert(ch >= '0' && ch <= '9', "invalid proposal path: "+s)
276 n = n*10 + uint64(ch-'0')
277 }
278 return n
279}
280
281func short(a address) string {
282 s := a.String()
283 if len(s) <= 12 {
284 return s
285 }
286 return s[:8] + "…" + s[len(s)-4:]
287}
288
289func shortHash(s string) string {
290 if len(s) <= 12 {
291 return s
292 }
293 return s[:6] + "…" + s[len(s)-4:]
294}