Discovery — permbook
Mandatory discovery gate (docs/DISCOVERY_AND_REUSE.md) for application-factory benchmark #3:
Build a reusable Gno capability that allows an application to define named permissions, grant those permissions to addresses, revoke them, and efficiently determine whether an address currently possesses a given permission. The capability should be suitable for composition by multiple independent Gno applications.
Run 2026-09-21, before any code was written. This document records what was searched, what was found, how each hit was classified, and what was decided.
The headline finding is stated first because it is the finding that most constrains the decision — and unlike benchmark #2, it points toward reuse, not away from it.
1. Headline finding
Cosmic Bull already owns a live implementation that satisfies the literal objective: r/permission_registry. It defines named permissions, grants them to addresses, revokes them, and answers Has(resource, permission, addr) in O(1). It is deployed, bounded, audited, and holds no value. Any consumer that accepts its trust model can use it today and write no new code at all.
That finding is recorded first, and it is not a formality — it nearly ended this benchmark without an implementation. What kept the question open is a specific, quoted property of that realm:
MaxResources = 1000
MaxResourcesPerAdmin = 20
MaxPermissionsPerResource = 50
MaxHoldersPerPermission = 200
— pearl/r/permission_registry/permission_registry.gno:16-27
These are global, shared, and unraisable. The realm is immutable after addpkg and its 526 lines contain no owner, no governance hook, and no upgrade path (verified by full read; grep for mustBeAdmin|assertOwner across the repo returns this file only, and the only admin concept in it is per resource, not per realm). So:
- resource names are one flat first-come-first-served global namespace, contended across all applications;
- the registry is permanently closed to new applications once 1000 resources exist (
:204 panic("global resource limit reached")); - an application needing a 201st holder of one permission has no recourse (
:274 panic("holder limit reached for permission " + permission)) — it cannot raise the cap, cannot fork the realm, and sharding costs it another of its 20 resource slots.
The gap is therefore not "nobody built this." It is that every implementation found puts permission state either in a shared realm with fixed global capacity, or in a library with no bounds and no authorization at all. Nothing found gives a consuming realm permission state that is simultaneously its own, bounded, and authorization-carrying.
2. Sources searched
| # | Source | How | Result |
|---|---|---|---|
| 1 | Cosmic Bull catalog | catalog/primitives.md, catalog/applications.md | 3 primitives, 14 applications reviewed |
| 2 | Cosmic Bull /p/ and /r/ sources | local tree; full read of permission_registry (526 lines); API grep across all .gno | 1 direct hit (§1, §4) — and only one |
| 3 | On-chain Gno — pearl-1 | vm/qpaths enumeration, all 634 packages (191 /p/, 443 /r/), then full-source reads | 11 candidates read in full |
| 4 | gno.land core corpus | examples/gno.land in the chain-matched source tree (gno@v0.0.0-20260827075919-c4c72fdd288c), 1022 .gno files, plus examples/quarantined/ | 5 non-quarantined + 3 quarantined hits |
| 5 | Public ecosystem / GitHub / docs | delegated web sweep: gnolang/gno tree + PRs/issues, moul/gno-contracts, onbloc, TERITORI, samouraiworld, gnolang/tx-exports, docs.gno.land, awesome-gno | 4 new finds — each re-verified locally (§5) |
On the method
Source 3 was not keyword-only, for the reason recorded in catalog/primitives.md: keyword-only searching is how the feeledger / bazaar/fee/v1 miss happened. All 191 pearl-1 /p/ paths were dumped and swept. That mattered here — p/samcrew/basedao is a live, real role-and-member store and matches none of access|perm|rbac|role|auth|acl|grant.
Source 4 used the chain-matched tree on disk, so the corpus read is the one the target chain was built from. Its access-control surface came back as exactly five non-quarantined packages:
gno.land/p/moul/authz
gno.land/p/nt/groups/v0
gno.land/p/nt/ownable/v0 (+ /exts/authorizable)
gno.land/p/gnoland/boards/exts/permissions
gno.land/r/gov/dao/v3/memberstore
This set matches the pearl-1 enumeration exactly, which is the corroboration that makes both sweeps trustworthy rather than merely two lists.
Source 5 is the weakest link, and benchmark #2 recorded that a delegated web sweep had previously claimed packages that were not actually deployed. Every load-bearing claim it returned this time was therefore checked against my own enumeration before being used. Two claims were confirmed and are load-bearing; the rest are recorded but carry no decision (§5).
3. Candidate matrix
Eleven live pearl-1 candidates plus six off-chain ones, scored against the objective's own clauses. "Grant→many" is the clause that eliminates the most.
| Candidate | Kind | Live on pearl-1 | Named perms | Grant→many | Revoke | Query cost | Bounded | Authorization | Safe cross-realm handle |
|---|---|---|---|---|---|---|---|---|---|
r/…/permission_registry (Cosmic Bull) | /r/ | yes | yes | yes | yes | O(1) | yes, but global+fixed | per-resource admin, 2-step | n/a (no handles) |
p/nt/groups/v0 | /p/ | yes | roles | yes | yes | O(log P + log H) | no | none | yes (Readonly views) |
p/gnoland/boards/exts/permissions | /p/ | yes | yes | yes | yes | O(roles held) | no | via boards | boards-typed only |
p/samcrew/basedao | /p/ | yes | roles only | yes | yes | 2 × O(log) | no | none | no (§4) |
p/gnoswap/rbac + r/gnoswap/{rbac,access} | both | yes | yes | no — 1 addr/role | n/a | O(1) | 13 fixed | gnoswap-internal | n/a |
p/moul/authz v1/v2 | /p/ | yes | no — one authority | yes | yes | O(log) | no | yes | partial |
p/nt/ownable/v0/exts/authorizable | /p/ | yes | no — one list | yes | yes | O(log) | no | owner | no |
r/gov/dao/v3/memberstore | /r/ | yes | fixed tiers T1/T2/T3 | yes | yes | O(log) | no | allowed-DAO gate | gated |
r/g1cz86tl8…/access | /r/ | yes | no — paid tier | n/a | n/a | O(1) | no | single owner | n/a |
p/onbloc/access/manager | /p/ | no | numeric IDs, names not stored | yes | yes | O(log) | — | full OZ model | — |
p/akkadia/v0/rbac | /p/ | no (test13) | yes | yes | yes | O(roles held) | no | none | — |
TERITORI/p/role_manager | /p/ | never deployed | yes | yes | yes | O(roles held) | no | wildcard | pre-interrealm |
p/thox/accesscontrol (quarantined) | /p/ | no | yes | yes | yes | O(R) linear | no | admin — broken (§4) | no |
p/archive/acl (quarantined) | /p/ | no | yes | yes | none | regexp per check | no | — | — |
p/n2p5/mgroup (quarantined) | /p/ | no | no | yes | yes | O(log) | no | owner | — |
4. Classified findings
EXACT DUPLICATE — none
No relevant existing implementation was found in the searched sources that is simultaneously (a) string-named permissions, (b) grant to arbitrary addresses, (c) bounded, (d) consumer-owned, and (e) live on pearl-1.
That sentence is the mandated one, used in its narrow literal sense: a statement about the five sources in §2, at this date. It is not a claim about the Gno ecosystem as a whole, and "not found" is not "does not exist."
COSMIC BULL EXISTING IMPLEMENTATION — satisfies the objective, recommended, not extended
gno.land/r/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/permission_registry — live on pearl-1, [addpkg] height = 604615.
Verified this run: the on-chain source fetched via vm/qfile is byte-identical to pearl/r/permission_registry/permission_registry.gno (18070 bytes, diff clean modulo the transport newline). The [addpkg] height is unchanged from its deployment record, which is proof it has not been redeployed.
It is good work and it does the job. Its Has is genuinely O(1) via three-level map nesting (permissions[resource][permission][addr], :64), its admin handoff is two-step with quota checked at consent time (:343-366, the Y4 remediation), it rejects stray coin sends (:127), and its INTEGRATOR CONTRACT (Y7) at :373 correctly warns consumers that Has authenticates nobody:
// INTEGRATOR CONTRACT (Y7): Has takes the subject address explicitly and
// performs NO caller authentication — it answers "does this address hold
// this permission", not "may my caller do this".
It is not modified, not redeployed, and not extended by this work (CLAUDE.md §6). It is instead recommended: the integrator documentation for the new primitive will name it as the zero-new-code path for consumers who accept a shared namespace. The two are not duplicates — see Decision 2.
Two integrator hazards are recorded here because they belong to the honest picture and not to a marketing comparison:
- The name race is not bound to an identity.
Has("treasury", …)keys on a bare string. A consumer that does not also assertGetAdmin("treasury") == expectedis trusting whoever won the race for that name. The API offersGetAdminbut nothing in it binds a read to an expected admin, and the Y7 contract covers caller derivation only. - The ceilings above are permanent. Not a defect — a deliberate bound on a permissionless shared registry, and the right call for that design. It is a ceiling on that architecture, which is the point of §1.
No other Cosmic Bull code is a candidate. grep -rlE 'func (Has|Grant|Revoke| AddRole|GrantRole|Authorize|IsAuthorized|CanDo)' across pearl/ returns permission_registry, plus r/grants (a funding-escrow market — Grant there is a noun about money, not a verb about authority) and a test file.
REUSABLE EXISTING PRIMITIVE — composed
gno.land/p/nt/groups/v0 — live on pearl-1, and the substrate this work builds on rather than replacing.
A Group holds a base address set plus a bptree registry of named Roles, each with its own addrset.Set of members. Mapped onto the objective, the four verbs are already there:
| Objective clause | groups primitive |
|---|---|
| define a named permission | g.AddRole(name) |
| grant to an address | r.Members().Add(addr) |
| revoke | r.Members().Remove(addr) |
| query | r.Members().Has(addr) |
Its query profile is the best of any named-permission candidate found, and for a structural reason worth stating: cost is O(log P + log H) — two independent B+-tree descents — and is therefore independent of how many permissions the subject already holds. Every other named-permission implementation found (boards/exts/permissions, akkadia/rbac, TERITORI/role_manager) answers the query by iterating the roles the address holds, so a heavily-privileged address is the expensive case. That is the wrong asymptotic for an authorization check on a hot path.
It also ships the cross-realm safety model this objective needs, in its own words (doc.gno:20-38):
- Do not ACCEPT a
*Groupor*Rolefrom an external/untrusted caller …- Do not RETURN a
*Groupor*Rolefrom any method or function callable by untrusted realms. Return*ReadonlyGroupor*ReadonlyRoleinstead.- Do not TRUST a
*ReadonlyGroupor*ReadonlyRolereceived from an untrusted caller — it is a live handle over the sender's data, not a snapshot.
readonly.gno backs that with concrete structs holding unexported fields and read-only methods, and IterateRoles deliberately hands the callback a *ReadonlyRole so that plumbing an untrusted callback into iteration cannot escalate into mutation.
Reused wholesale. No tree, set, or iteration code is written by this work.
Dependency chain verified live on pearl-1 before committing to it: p/nt/groups/v0 → p/moul/addrset + p/nt/bptree/v0, both present in the 191-package /p/ enumeration.
What groups does not supply, and what therefore constitutes the delta: no bound on role count or member-set size; no authorization of any kind (it authenticates nobody, by design); no permission-versus-membership semantics (it has a "base set" concept that is meaningless for permissions); and GetRole returns a mutable *Role, so the discipline in rule 2 is a convention the consumer must keep, not something the type system keeps for it.
RELATED IMPLEMENTATION — examined, not reused
gno.land/p/gnoland/boards/exts/permissions — live on pearl-1, and the single most important structural precedent found: it is exactly the layer this work proposes, already built on groups, by the gno.land core authors. It proves the composition is the intended one.
Not reusable directly because every signature is welded to boards types — boards.Permission, boards.Role, boards.PermissionSet — so a non-boards application cannot import it. It also answers HasPermission(user, perm) by iterating RolesContaining(user) and reading a PermissionSet out of role meta, which is the O(roles-held) profile rejected above. Its ValidatorFunc registry carries an explicit SECURITY note that validators run with full mutation access under the owning realm's authority — the stored-callback hazard, and a feature deliberately not carried forward.
gno.land/p/samcrew/basedao — live on pearl-1 (confirmed independently against my own /p/ enumeration, line 178; the web sweep's claim was correct). Maintains both a forward index (member → roles) and a reverse index (role → members), which gives a clean two-lookup HasRole. Genuinely the closest live /p/ in query shape.
Disqualified on evidence, not on taste:
type MembersStore struct {
Roles *avl.Tree // role name -> *Role
Members *avl.Tree // string -> *avl.Tree [roles -> struct{}]
}
type Role struct {
Name string
Description string
Color string
Members *avl.Tree // string -> struct{}
}
— members.gno:10-31
Both container fields and Role.Members are exported mutable *avl.Tree handles. That is precisely the hazard groups/v0 rule 2 exists to prevent: any realm holding a *MembersStore can call m.Roles.Set(...) directly and, by borrow rule #2, the write commits under the allocating realm's authority. There are no readonly views anywhere in the package. Additionally: keys are string, not address, so nothing validates an address at all; AddRoleToMember (:297) applies no bound of any kind; and RemoveRole (:272) iterates every member of the role in one call, which is unbounded gas. The in-monorepo copy sits under examples/quarantined/, which the examples README defines as not audited and not modernized to the current interrealm spec.
gno.land/p/gnoswap/rbac, r/gnoswap/rbac, r/gnoswap/access — all three live. Disqualified by one quoted line from p/gnoswap/rbac/doc.gno:
"Each role can only have one assigned address. For multi-address authorization, consider creating multiple roles or implementing a wrapper."
Confirmed in the realm source: IsAuthorized(role, caller) reduces to caller == roleAddresses[role] (r/gnoswap/access/access.gno). This is a role→address routing table for a known set of system contracts, not a grant-to-many capability. Correct for gnoswap; not the objective.
gno.land/r/gov/dao/v3/memberstore — live. Three fixed tiers T1/T2/T3 carrying voting power and invitation points, not arbitrary named permissions. Its Get(_ int, rlm realm) is a textbook correct secondary-realm-parameter guard and is cited as a pattern precedent, but access to the store is gated to dao.InAllowedDAOs(currealm), so it is closed infrastructure.
gno.land/p/moul/authz v1/v2 and gno.land/p/nt/ownable/v0/exts/authorizable — both live, both correct, both single unnamed authority sets. authz has one MemberAuthority; authorizable has one auth list plus an ownable superuser. Neither has a concept of which permission. authorizable is cited as the pattern precedent for the (_ int, rlm realm, …) + rlm.IsCurrent() shape a /p/ must use to authorize. p/moul/authz's README carries an explicit "not security-audited" disclaimer.
gno.land/r/g1cz86tl8…/access — live, and a false positive on name alone: it is GnoPulse's paid-subscription gate, with a banker, a withdrawal path, and HasAccess(addr) meaning "has paid." Recorded because "an access realm already exists on pearl-1" is exactly the claim that is true from a package listing and wrong from the source.
DISQUALIFIED ON EVIDENCE — off-chain near-misses
p/thox/accesscontrol (upstream examples/quarantined/) — the most literal match to the objective's wording anywhere: CreateRole, GrantRole, RevokeRole, RenounceRole, SetRoleAdmin, HasAccount. It is also the clearest argument against adopting a near-match without reading it.
Roles []*Roleis a linear slice, soFindRoleis O(R) and is called on every grant and every revoke.- No role removal at all — state grows monotonically.
- No bound on roles or holders.
GrantRoleappends toUserToRolesunconditionally, so repeated identical grants grow state without limit.- Every
Rolefield is exported (Name,Holders *avl.Tree,Ownable) whileCreateRoleandFindRolereturn*Role. Any holder can callr.Holders.Set(...)and self-grant, bypassing the admin check entirely — borrow rule #2, the same class asbasedaoabove but with an actual privilege-escalation path rather than a latent one.
Correction to a claim from the web sweep, recorded because getting it wrong would misrepresent upstream: gnolang/gno PR #2307 which added this package was merged, not rejected; it was quarantined later in a bulk action.
p/archive/acl (quarantined) — HasPerm(addr, verb, resource) runs regexp.MatchString per permission per check, holds unbounded perms slices, and has no revocation whatsoever: // TODO: helpers to remove permissions.
p/n2p5/mgroup (quarantined) — owner, backup owners, one member set. No named permissions.
p/onbloc/access/manager — an OpenZeppelin AccessManager v5.6.1 port, and by some distance the most rigorous access-control code in the ecosystem. Excluded for two independent reasons, the first decisive:
- It is not on pearl-1. Independently verified against my own enumeration: the
@onblocnamespace on pearl-1 contains exactlydiff,int256,json,uint256. Agnolang/tx-exportsarchive carries it under a pearl path, which is a genuine divergence I could not explain and did not rely on. - Roles are
uint64IDs and labels are deliberately not stored in state (event-only), so there is no on-chain name→role mapping. The objective asks for named permissions.
p/akkadia/v0/rbac (test13 archive only, no source repo locatable) and TERITORI/p/role_manager (never deployed on any archived chain, still on the pre-interrealm std.Address API) — both are true named-permission models and both answer the query by iterating the subject's roles. Recorded as prior art; neither is reachable as a dependency.
GENUINELY NEW
The delta, stated as narrowly as it can honestly be stated. Not a permission system — a bounded, authorization-carrying facade over groups:
- An explicit admin bound to the permission set, with
cur-threaded grant/revoke and a two-step admin handoff.groupsauthenticates nobody. The two-step form is not invention: it is the audited Y4 finding from Cosmic Bull's ownpermission_registry(:304-366), which recorded that a one-step transfer to a well-formed-but-unowned address is permanently fatal. - Consumer-chosen bounds on permission count, holders per permission, and name length/charset — the objective's "bounded state and gas", which
groupsdoes not provide and which no live/p/candidate provides. - Permission semantics rather than membership semantics: no base set, an explicit lifecycle (a permission comes into being on first grant and is pruned on last revoke, so an empty permission does not linger as state), and no per-role metadata slot — deliberately, since
groups' own doc devotes a section to the hazard of storing mutable pointers inmeta. - A cross-realm surface that is safe by construction, not by convention: the
*Groupis unexported and never escapes; nothing returns a mutable handle; reads return values orgroups' own readonly views.
5. Decisions
Decision 1 — build a new /p/ primitive, permbook, composing gno.land/p/nt/groups/v0.
Justified by elimination against the objective's own clause "suitable for composition by multiple independent Gno applications":
- Use an existing component unchanged —
permission_registryworks, and is recommended (Decision 2), but its capacity is global, shared and unraisable (§1).groupsalone works, but leaves every consumer to re-derive bounds and authorization. - Use an existing ecosystem implementation — the only one that is both named-permission and structurally sound (
onbloc/access/manager) is not on pearl-1 and does not store names; the only one that is the right layer (boards/exts/permissions) is welded toboardstypes; the rest are disqualified on quoted evidence in §4. - Adapt an existing implementation —
thox/accesscontrolis the closest by name and would require replacing its storage (O(R) slice), adding revocation bounds, and closing a privilege-escalation hole. That is a rewrite, and it is a rewrite of a package that is not deployed. - Composition — this is the answer, and Decision 1 is composition. The new code is a facade; the data structures, iteration, and readonly views all come from a live dependency.
The intervention is deliberately small and the smallness is the point: the benchmark's own metric is dependency-weighted new code, and a thin, audited, bounded layer written once beats the same ~60 lines re-derived by every consuming application. That is the same value boards/exts/permissions delivers to boards — generalized, and typed to address instead of to one application's structs.
Decision 2 — permbook does not replace permission_registry; the two are different architectures and both are documented.
Stated plainly so a reviewer can disagree on evidence. The distinguishing property is who owns the state:
r/permission_registry | p/permbook | |
|---|---|---|
| state lives in | a third-party realm | the consumer's own realm |
| namespace | one global, rivalrous | private per consumer |
| capacity | fixed forever, shared | chosen by the consumer |
| consumer writes | nothing | its own crossing wrappers |
| consumer must trust | the registry realm + the name race | only itself |
That is "shared service" versus "embedded library." Both are legitimate; they are not duplicates. The integrator docs will say so and will name permission_registry as the zero-code path.
Decision 3 — the admin lives inside permbook, authorized via (_ int, rlm realm, …) + rlm.IsCurrent().
Counter to the ecosystem's near-universal posture — the sweep found that every /p/ permission library examined declares that it cannot authenticate callers and punts authorization to the consumer. Taken here because the objective lists "explicit authorization" as a design requirement, and because punting it is what makes each of those libraries a substrate rather than a capability. The mechanism is not invented: it is the live, correct shape in p/nt/ownable/v0 and p/nt/ownable/v0/exts/authorizable.
Decision 4 — ownable/v0 is not composed for the admin, and the reason is recorded rather than assumed.
ownable.Ownable was read in full before rejecting it. Its owner field is unexported (ownable.gno:21-23) and its only handoff is one-step TransferOwnership (:61), which authorizes against the current owner. A two-step model needs the write to happen when the nominee calls, which ownable cannot express and which cannot be layered on from outside without leaving the one-step path reachable. So permbook stores admin and pendingAdmin as its own unexported fields. This costs roughly thirty lines and buys the audited Y4 semantics; composing ownable here would have been reuse for its own sake.
Decision 5 — no stored closures, no callbacks, no validator hooks.
Directly counter to boards/exts/permissions' ValidatorFunc registry and its WithPermission(cb func()) form, and for the reason that package's own SECURITY note gives: a stored callback runs with full mutation access under the owning realm's authority. A general-purpose primitive cannot assume its consumers' code is vetted. Same rule as duebook.
Decision 6 — no economic mechanism, no value custody, no banker.
The objective says so explicitly, and the failure mode is live on pearl-1: r/g1cz86tl8…/access is what happens when a permission gate acquires a price list, a credit balance, and a withdrawal path. permbook imports no banker and holds no coins.
Decision 7 — ship a reference consumer realm.
For the reason established in benchmark #2: the central claims here — "a revoked address is refused in the next transaction", "the admin gate holds against a non-admin caller", "the bounds actually panic" — are claims about persistent state across transactions. A vm/qeval against a pure package is a single ephemeral evaluation and cannot falsify any of them. Only a realm, called repeatedly, can. It will hold no coins.
6. Known limitations, recorded before implementation
permbookcannot authenticate an EOA.rlm.IsCurrent()proves therealmvalue came from a live crossing frame;rlm.Previous().Address()is then the principal. A consumer that threads acurfrom the wrong frame, or that wrapspermbookin a non-crossing exported helper, reintroduces the Class-2 designation-forgery bug. This is the top item in the consumer contract and the package cannot enforce it — the same shape asduebook's caller-supplied clock, and the same honest admission.- The consumer owns the state and can leak it.
permbooknever returns a mutable handle, but it cannot stop a consumer from exposing the*Bookitself.groupsrule 2, inherited. - Bounds are chosen at construction and are then fixed for that book. A consumer that picks them too low has the same problem
permission_registryhas — except that it chose them, it can deploy a second book, and its realm is the only one affected. - Two bptree descents is not one map lookup.
permission_registry's three-level Go map is O(1);permbookis O(log P + log H). The trade is deterministic ordered iteration and per-consumer isolation, and it is a trade, not a free win. - No permission hierarchy, no wildcards, no delegation. One flat set of named permissions per book.
archive/acl's regexp matching androle_manager's"*"wildcard were both seen and both declined — a wildcard makes "does this address hold permission X" stop being a lookup. - Source 5 is a delegated web sweep. Its two load-bearing claims (
basedaolive on pearl-1;onbloc/access/managerabsent from pearl-1) were independently re-verified against my own enumeration and are used. Its other findings are recorded but carry no decision. - Ecosystem-wide uniqueness is not claimed. Five sources, one date.
7. Evidence
Commands and reads that produced the findings above, so they can be re-run.
RPC=https://rpc.pearl.testnets.gno.land:443
# Source 3 — full pearl-1 enumeration (634 packages), not keyword-only
curl -s "$RPC/abci_query?path=%22vm/qpaths%22&data=0x$(printf 'gno.land/p/' | xxd -p)"
curl -s "$RPC/abci_query?path=%22vm/qpaths%22&data=0x$(printf 'gno.land/r/' | xxd -p)"
# -> 191 /p/, 443 /r/
# byte-verification of the deployed permission_registry against the local tree
diff <(curl … vm/qfile … permission_registry/permission_registry.gno) \
pearl/r/permission_registry/permission_registry.gno
# -> identical, 18070 bytes; [addpkg] height = 604615 unchanged
# Source 4 — chain-matched corpus, 1022 .gno files
GNOSRC="$HOME/go/pkg/mod/github.com/gnolang/gno@v0.0.0-20260827075919-c4c72fdd288c"
find "$GNOSRC/examples/gno.land" -type d \
| grep -iE 'acl|rbac|access|permission|role|capab|grant|authz|authoriz|ownable|group|member'
grep -rlE '^func .*(GrantRole|RevokeRole|HasPermission|HasRole|AddPermission)' \
"$GNOSRC/examples/gno.land" --include='*.gno' | grep -v _test
# Source 2 — Cosmic Bull's own tree
grep -rlE 'func (Has|Grant|Revoke|AddRole|GrantRole|Authorize|IsAuthorized|CanDo)' \
--include='*.gno' pearl/
# -> permission_registry, grants (money), one test file
# dependency availability for the composition decision
grep -E 'p/nt/groups/v0|p/nt/bptree/v0|p/moul/addrset' /tmp/pearl-p-packages.txt
Full-source reads (whole files, not outlines — an outline is an author's claim, not evidence):
gno.land/p/nt/groups/v0→doc.gno,group.gno,role.gno,readonly.gnogno.land/p/nt/ownable/v0→ownable.gnogno.land/p/nt/ownable/v0/exts/authorizable→authorizable.gnogno.land/p/gnoland/boards/exts/permissions→permissions.gnogno.land/p/gnoswap/rbac→ all files;gno.land/r/gnoswap/access→access.gnogno.land/p/samcrew/basedao→members.gnogno.land/p/moul/authz/v2→authz.gno,README.mdgno.land/r/gov/dao/v3/memberstore→memberstore.gno,types.gnogno.land/r/g1cz86tl8vnh9956cpuu43ksjy7kmur745wdjxsc/access→access.gno$GNOSRC/examples/quarantined/…→p/archive/acl,p/thox/accesscontrol,p/n2p5/mgroup- local →
pearl/r/permission_registry/permission_registry.gno(all 526 lines)
Re-run this gate before deployment, per docs/DISCOVERY_AND_REUSE.md step 8.