docs(lez): Extensions section with admin-authority and extension-library guides - #355
docs(lez): Extensions section with admin-authority and extension-library guides#355mmlado wants to merge 58 commits into
Conversation
…library guides
Two new pages under docs/lez/extensions/:
- admin-authority.md: integration guide for app developers. Covers
annotation, gating, AdminCandidate (Signer / PDA), initialization
window, transfer, renounce, verification, security notes.
- build-a-spel-extension-library.md: author guide for the generic
[package.metadata.spel] discovery mechanism. Covers two-crate
layout, metadata fields, marker / gate macros, multi-extension
stacking, troubleshooting.
SUMMARY.md updated with new section.
Relates to: RFP-001 (logos-co/rfp#46)
Gate attributes are ordinary proc-macros that re-expand on the emitted handler and remove themselves, so a library does not declare them for stripping. Update the build-a-spel-extension-library guide to match. Remove the instruction_attrs field from the metadata example and the paragraph that described stripping.
Extension library guide: remove the leftover instruction_attrs references (mechanism removed), rewrite the gate attribute section to the real pattern (re-expansion body injection, param names from attribute args, never touch #[account]), and document the [[package.metadata.spel.inject]] block for gate account params. Admin-authority guide: admin_initialize self-elects the caller and takes no candidate argument, AdminCandidate moved to the transfer section, gate params are injected rather than required, CLI examples switched to the real syntax, deploy-time bundling corrected as not possible on LEZ today.
The admin guide fixes the gate kwargs to the inject account names and gains the embedded mode section. The author guide gains auto-wrap, embedded mode with bound args, the kwarg contract with the alignment self-test, marker-order and conflict rules, and the corrected discovery scope: git and registry deps are discovered, and a present marker with failed resolution refuses to compile.
|
@mmlado is this ready for review? does it need updating for v0.2? |
|
@kashepavadan It's still in draft. It's part of the milestone 3 for logos-co/rfp#46 and logos-co/rfp#47. I'm still working on the milestone with the testing. I left it in draft so it can be review, but it'll be ready to merge only after the review process has been done for the RFPs. |
The admin guide's embedded section showed a manual bootstrap_at call. The example now marks the slot field with #[admin_slot] and the creating instruction with #[admin_initialize], and the notes cover the injected bootstrap, the derived offset const, and the layout agreement check. The extension-library guide gains the author side of the same surface: the slot marker name derived from the role, and the bootstrap attribute pattern for slots that must never exist uninitialized.
End-to-end usage guide for the freeze-authority extension: auto vs manual mode, seven management instructions, lifecycle (including recoverable renounce), per-account freeze, PDA-as-authority via CPI, F3 carve-outs. Extends build-a-spel-extension-library.md with three sections motivated by shipping a second extension: attribute-order convention for libs that re-export the #[instruction] shim, the optional wrap_instructions metadata for module-wide gate application, and the hard-dep composition pattern (freeze on top of admin). SUMMARY.md picks up the new page.
freeze_initialize self-elects the admin and takes no candidate, every CLI block uses the real invocation form, the candidate enum moves to the transfer section where it applies, and the embedded mode section covers the shared-account layout, born-vacant slots, and splice-only writes.
The transfer commands now take --new-account and --candidate. Both CLI examples updated to match the shipped surface.
The struct shows #[admin_slot] and #[freeze_slot] and the notes cover the #[admin_initialize] bootstrap next door and the layout agreement check the markers add.
The site switched from GitBook to docusaurus. The extensions section gains its category file, both pages gain the standard frontmatter, and the GitBook hint blocks become admonitions.
The transfer commands now take --new-account and --candidate. Both CLI examples updated to match the shipped surface.
British spellings where the house style asks for them, abbreviations spelled out (parameters, attributes, functions), and the flagged anthropomorphisms reworded. Code blocks untouched.
9cb2b1a to
b4854e3
Compare
Frontmatter and admonitions, matching the admin and library pages.
The admin branch's M2.5 alignment and the freeze branch each carried an auto-wrap section, and the merge kept both. One section survives at the earlier position with the richer body, the activation walk-through, and the composition note.
British spellings, abbreviations spelled out, and the flagged anthropomorphism reworded on the freeze page.
danisharora099
left a comment
There was a problem hiding this comment.
I ran the packet rather than just reading it. The content is faithful to the shipped code: the IDL surfaces, injection behavior, exempt semantics and embedded diagnostics all matched when I executed the guides against correct pins, and the site builds clean. But a fresh developer following the text as written cannot get past the first build:
- The dependency snippets are unrunnable:
spel-admin-authoritywith no branch resolves to an empty main (README and LICENSE, no crate), andspel-frameworkpointing at logos-co/spel resolves to a main with no extension support (it only exists on the fork until logos-co/spel#233 lands). Either pin branch/rev in the snippets, or gate this PR on merging the library m3 branches to main and landing logos-co/spel#233. I'd prefer the latter, it fixes the cause. - Missing prerequisites that stop a
cargo newuser: serde and lee_core in the consumer deps (the guide lists two crates, the samples need five), the fn main collision with the generated main, and thecargo fetchstep (the scanner's cargo metadata --offline call deterministically fails for fresh git consumers without it). - Smaller: the "library README's dependency table" claim is false for the admin repo; the
.write_tohelper in the embedded snippet is hand-written in the sample, not provided, so show it; and add a caveat that a marker matching nothing is currently dropped silently when resolution is healthy.
The dependency snippet lists all five crates the reference samples need, with a note on what nssa_core, borsh and serde are for. A fresh cargo new consumer learns to delete the default fn main, the macro generates the entry point, and to run cargo fetch once so the scanner's offline metadata call can resolve git dependencies. The embedded walkthrough shows the hand-written write_to helper, and the README reference no longer claims a dependency table.
FreezeCandidate joins both import snippets, the generated dispatcher references it. The Pda variant's program_id field is a ProgramId, not an AccountId.
The embedded metadata example gains the mandatory state_type key and the overlap assert it powers, the bound args section states the trailing position rule, and the troubleshooting sections describe the unmatched marker hard error instead of a silently missing surface. The admin guide walks the co-sign exchange for Signer transfers, says a candidate PDA must be a claimed account rather than deployed, and points the missing-trio checklist at the hard error.
The frozen-state callable list includes the per-account freeze edits and consumer exemptions, the consumer prerequisites match the admin guide (six crates, cargo fetch, no default fn main), and the two inits go back to back because a LEZ transaction carries a single instruction. The freeze-account target is documented as hex, releasing a target that is not frozen documents its refusal, Signer transfers walk the co-sign exchange, and the candidate Pda field says ProgramId.
|
This packet is written against the released end state and lands last, after the framework enabler and the library lineage reach their release. That is the gating option, and it is why the dependency snippets stay unpinned, they become correct the moment the release they describe exists. Until then all four repos carry an m3-settle tag on the reviewed settle revs, for anyone who wants to run the guides against a reproducible line today. The content items are in on the branch, fde4a30 and fe8409d:
|
CI has not run on this branch since 2026-08-31 — the last three heads are stuck awaiting workflow approvalNot a re-dogfood, and here is why. The head moved from What is new is that the checks quietly stopped running:
So I ran both checks locally on
Nothing here changes the review state: the open items are still the five inline comments from 2026-09-01, the |
Follow-up: I approved the queued runs, and the Vale failure is a CI bug, not a prose problemI approved the two stuck workflow runs on
What actually failsThe job runs I pulled the logs for all three failing Vale runs on this branch. Same error every time, and the severity histogram is the giveaway:
Zero error-level alerts in all three. Per @mmlado, this means The fix, verified both waysCap what Vale emits in CI to the level the policy actually enforces. In - uses: errata-ai/vale-action@reviewdog
with:
version: 3.11.1
files: docs
+ vale_flags: "--minAlertLevel=error"
reporter: github-pr-check
filter_mode: added
fail_on_error: true
This keeps Worth noting this is repo-wide, not specific to this PR: any PR that adds enough prose will trip the same cap. It just bit here first because this branch adds ~1065 lines of docs. Since Review state unchangedStill not a re-dogfood: the three extension pages are byte-identical to |
The flags have to precede the project name. The parser stops reading flags at the first argument that is not one, takes it as the name, and ignores the rest, so the documented invocation scaffolded the default pin without saying so. The guest binary is snake-cased from the project name. Getting the pin wrong is not silent either: the default resolves a second lee_core alongside the one admin-authority pins, and the guest fails with type errors out of the lez_program expansion, so the page no longer promises a quiet failure it does not produce. The dependency block names the manifest it belongs in, the scaffold's root is a workspace that excludes methods/guest. The co-signing exchange merged upstream after the pinned revision, so both pages describe it as arriving with the pin rather than as still in review.
|
Agreed on all of it, and vale_flags: "--minAlertLevel=error" is the better spelling of the fix, it stops the annotations being produced instead of stopping them being reported. For 8a732b9, the warnings it cleared were worth clearing on their own terms even though they were never what failed the check. Agreed the workflow change wants its own PR rather than riding this branch. |
weboko
left a comment
There was a problem hiding this comment.
Dogfooding run: built all three pages end to end
Clean Ubuntu 24.04, rustc 1.94.1, nothing pre-installed beyond the apt-get line in Prerequisites. I worked each page from the top rather than reading it, and re-ran the whole thing because 947aa99 rewrote the scaffold instructions after the last review.
What worked, verified rather than assumed
- The
spel initcorrection in 947aa99 is right. I read the argument loop inspel-cli/src/lib.rsat the pinned revision: it breaks out at the first argument that is not a recognised flag and takes it as the project name, silently dropping the rest. Flags-ahead-of-name scaffolds with the fork pin actually applied; the old ordering would not have. The snake-casedmy_program.rsand the root[workspace]withexclude = ["methods/guest"]are both exactly as now described. - The circuits pre-seed workaround is correct and necessary.
cargo installdied precisely as the page predicts,invalid peer certificate: UnknownIssuerout oflogos-blockchain-circuits-pol-sys v0.5.3. The documentedcurl | tar xz -C ~/.cache/logos/blockchain/produces the directory name the build script asserts on (logos-blockchain-circuits-v0.5.3-linux-x86_64), and the retry then went green.v0.5.3is the right version forf7aa464. - Admin page: gated program compiles, and the IDL puts the injected accounts where the page says,
set_fee_bpscomes out asadmin_config, caller, config. All three lifecycle commands run under--dry-run=textplaced before the--separator, including the one-key JSON{"Pda": ...}candidate. - Freeze page: auto, manual and embedded all compile, and the auto-mode IDL matches the ten expected names in the page's own order. Embedded drops
admin_initializeandfreeze_initializeand retargets every config account onto my own account, andfreeze_authority_renouncereally does fall to two accounts. The claim thatFreezeCandidatemust be imported even though nothing in consumer code names it is true, dropping it givescannot find type FreezeCandidate in this scopepointed at#[lez_program]. - Extension-library guide: I built the
my-extension/my-extension-macrospair verbatim from the page, plus a consumer.extension_actionis discovered and merged into the consumer IDL. The gate section works as written,#[require_my_gate]synthesisesmy_state, callerahead of my own parameter in the declared order. Auto-wrap works too, and#[my_extension(manual)]opts back out,auto_gatedgoes frommy_state, caller, thingtothing.read_inject_specs(&Path) -> Result<Vec<InjectSpec>, String>and theidl-gengating on theextensionmodule are both accurate at the pinned rev.
CI: npm ci && npm run build succeeds, and vale 3.11.1 (the version the workflow pins) reports 0 errors on the three pages, so neither gate blocks. The accept.txt diff looks large only because the file got re-sorted; it is 29 additions and no removals.
What stopped me: one real blocker, on the "Add the dependency" section that 947aa99 just retargeted at the scaffold, plus a few smaller path fixes. Inline below, each with a suggested change.
A spel init guest manifest already has a [dependencies] table, so the dependency block read as something to paste and cargo rejected it with a duplicate key before anything built. Both pages now say which entries are already there and which single line is actually new on that path. The generate-idl invocations carried a cargo new path that does not exist in a scaffold, where the guest lives under methods/guest and is auto-detected from the project root. The admin marker is described as sitting directly below lez_program rather than inside the module, which is where a bare attribute would be a syntax error.
FreezeCandidate and freeze_exempt have to be imported outside the module. The generated transfer instruction references the candidate type at the file's outer scope, so an in-module import fails with a scope error pointed at lez_program rather than at the import, which the example showed but the prose never said. The verify sections say generate-idl reads source instead of compiling it, so it emits a complete IDL for a program that does not build and confirms the surface rather than the integration. The gate snippet says it hardcodes the conventional parameter names for brevity and that a shipped gate parses its attribute args, which the kwarg contract already required.
weboko
left a comment
There was a problem hiding this comment.
Re-dogfooding at 4af1de3 — clean Ubuntu 24.04, everything rebuilt from the pins on this branch
Ran the whole thing again because d5e8b57 and 4af1de3 landed after my last pass at fd23547. Both commits fix things I raised, and both hold up under test.
The two new commits, verified rather than read
-
d5e8b57, merging into the scaffold's manifest. The blocker is gone.spel init --spel-git ... --spel-rev f7aa464 my-programwrites a guest manifest already carryingspel-framework,nssa_core,risc0-zkvm,my_program_core,serde,borshandruint, so the four the page names are all there. Adding only theadmin-authorityline and runningcargo checkfrommethods/guestwent green, andspel generate-idlfrom the project root printedcreate_pool,set_fee_bps,admin_initialize,admin_transfer,admin_renounce. Theerror: duplicate keywarning earns its place: appending a second[dependencies]table reproduces it verbatim,--> Cargo.toml:23:2. On the freeze page the "two lines" count is right too,admin-authorityplusfreeze-authoritymerged into the scaffold manifest built and gave all ten names in the page's own order. -
4af1de3, the limits ofgenerate-idl. This is the most useful sentence added in either commit, and it is exactly true. I brokeset_fee_bpswith a call to an undefined function:cargo checkfails withE0425, andspel generate-idlon that same tree still exits 0 and prints the complete five-instruction IDL. The mechanism matches the claim,generate_idl_from_file_with_depsinspel-framework-core/src/idl_gen.rsreads the file withread_to_stringand parses it, nothing is compiled. -
4af1de3, the kwarg contract. Also true, and now demonstrably so. I built themy-extension/my-extension-macrospair verbatim from the library page, kept the minimal gate that ignores_attr, then gave a consumer instruction its own parameter names and pointed the gate at them with#[require_my_gate(my_state = their_cfg, caller = owner)]. It fails exactly as the new paragraph implies:error[E0425]: cannot find value `my_state` in this scope error[E0425]: cannot find value `caller` in this scopeWith the conventional names the same gate compiles fine, so "enough only while every gated instruction happens to use the conventional names" is the right framing.
Also re-checked at this head
- Auto, manual and embedded freeze modes all compile on a
spel initscaffold. In auto mode the IDL showstransfercarryingfreeze_config, freeze_account, caller, thingandbalance_ofcarrying onlything, so the exemption does what the page says. Embedded mode dropsadmin_initializeandfreeze_initialize, as documented. --dry-run=textplaced before the--separator still resolves accounts and prints the transaction.- The
borshline is worth one note in passing: the scaffold pinsborsh = "1.5"with noderivefeature, while the block here asks forfeatures = ["derive"]. "admin-authorityis the only line you add" still holds, becauseadmin-authorityturns the feature on and cargo unifies it, and#[derive(BorshSerialize, BorshDeserialize)]onPoolConfigcompiled for me on that path. Just be aware the claim leans on a transitive feature rather than on the guest manifest itself.
CI: npm ci && npm run build exits 0. Vale 3.11.1, the version the workflow pins, reports 0 errors, 0 warnings and 0 suggestions on all three pages, so neither gate blocks.
What I found: one factual overstatement introduced by 4af1de3, and two troubleshooting bullets the wording fix in 947aa99 left behind. All three inline below with suggested changes, and all three vale-clean at error level.
FreezeCandidate has to be imported outside the module because the generated transfer instruction references it at the file's outer scope. freeze_exempt resolves from either position, so the sentence no longer asks for both, and it says what dropping freeze_exempt costs instead. The troubleshooting bullets ask for the marker below lez_program rather than inside it, matching the annotate section, the library guide, and the CLI's own diagnostic, which names the direction.
weboko
left a comment
There was a problem hiding this comment.
Dogfooding run: followed all three pages end to end on a clean machine
I worked through admin-authority.md, freeze-authority.md and build-a-spel-extension-library.md literally, from an empty container: installed the prerequisites, built the spel CLI at the pinned revision, scaffolded programs with spel init, wired the extensions in, compiled, and ran the lifecycle commands in dry-run. Environment: Debian/glibc, 4 cores, rustc 1.94.1, spel 0.6.0 built from mmlado/spel@f7aa464, admin-authority and freeze-authority at v0.1.2.
Almost everything on these pages is accurate, including the parts that are easy to get wrong. Verified against a real build rather than by reading:
- The
spelCLI install fails exactly as the page predicts, withinvalid peer certificate: UnknownIssuerout of thelogos-blockchain-circuits-pol-sys v0.5.3build script. The documented pre-seed recipe works verbatim and lands the artifact under precisely the directory name the build script looks for (~/.cache/logos/blockchain/logos-blockchain-circuits-v0.5.3-linux-x86_64). Thev0.5.3pin forf7aa464is right. spel initwrites the guest tomethods/guest/src/bin/<snake_name>.rs, the root manifest is a[workspace]that excludes it, and with the two source flags the scaffold'sspel-frameworkandnssa_corepins already match, soadmin-authorityreally is the only line to add. Flags must precede the project name, as stated: the parser breaks at the first non-flag argument.- Skipping
cargo fetchreproduces the exact offlinecargo metadatafailure the pages warn about. - IDL output matches the documented expected lists exactly, in the documented order: the admin trio, and all seven freeze instructions.
- Injection order is as documented:
set_fee_bpsresolves to[admin_config, caller, config], and an auto-gatedtransferto[freeze_config, freeze_account, caller, balance], while#[freeze_exempt]instructions get nothing injected. - Embedded mode behaves as described: neither initializer appears, the config accounts become the consumer's own account, and
freeze_authority_renouncedoes drop from 3 accounts to 2. --dry-run=textbefore the--separator works, PDAs are derived so only--calleris needed, and--targettakes 64 hex characters and rejects base58 as stated.- The author guide is sound end to end: I built the
my-extensionskeleton purely from its snippets and a consumer with exactly the four documented dependencies, and the extension's instruction was discovered and merged into the consumer's IDL. Dropping the dependency produces the documented fail-closed error naming the marker. - The three consumer programs (admin dedicated, freeze auto, admin+freeze embedded) all
cargo checkclean, no warnings.FreezeCandidateimported outside the module produces no unused-import warning, confirming it is genuinely required, and moving it inside reproduces the documentedcannot find type FreezeCandidate in this scopepointed at#[lez_program].freeze_exemptdoes resolve from either position, as the latest commit says.
Repo checks: vale --minAlertLevel=error (3.11.1, the CI version) is clean on all three pages, npm ci && npm run build succeeds, the pages route to /lez/extensions/... and pick up the autogenerated sidebar via _category_.json, and all five cross-page anchors resolve to real headings. Worth noting the last three commits have never been checked by CI: the fork PR is sitting at action_required, so a maintainer needs to approve the workflow runs. Both checks pass locally.
Two inline comments below. One is a real trap I hit; the other is a small robustness fix.
Separately, and out of scope for this PR: in --dry-run=text the target argument of freeze-account encodes as 128 bytes on the wire (0x0b000000 then 44000000 repeated 32 times) for a [u8; 32] IDL type that borsh should encode as 32 bytes. That looks like an argument-encoder bug in spel rather than anything wrong on this page, and I could not confirm it against a live node, but it may be worth checking upstream since the page documents this command against a real stack.
Generated by Claude Code
The freeze marker rejects placement above lez_program from v0.1.3, so the troubleshooting bullet no longer has to describe a build that succeeds with the whole surface missing. The pins diverge for the first time, freeze v0.1.3 against admin v0.1.2, which is the pairing freeze itself pins, so the dependency paragraph says to use them together rather than naming one shared version.
weboko
left a comment
There was a problem hiding this comment.
Re-dogfooding at ed3b6ab — clean Ubuntu 24.04, everything rebuilt from the pins on this branch
ed3b6ab moves a dependency pin, so it earns a full rebuild rather than a read. Fresh container, nothing pre-installed beyond the apt-get line in Prerequisites, rustup default stable (rustc 1.98.1 today, comfortably above the documented 1.94.0 CLI floor), spel 0.6.0 built from mmlado/spel@f7aa464.
The new commit, verified rather than read
- The
v0.1.3bump and the rewritten dependency paragraph are correct.freeze-authorityv0.1.3 pinsadmin-authorityv0.1.2 and the samef7aa464framework rev — read off the tag's ownCargo.toml, not inferred. A consumer declaring both as direct dependencies resolves exactly one copy of each:cargo tree -i admin-authorityshowsv0.1.2reached both directly and throughfreeze-authority v0.1.3, unified into one node. Onespel-framework v0.6.0, onespel-authority v0.1.0, noFrom<AdminError>. "Use the tags above together" is the right framing, and it reads better than naming one shared version. - The
curl -fLchange is right and the recipe still works. The pre-seed lands the artifact under exactly the directory name the build script asserts on (logos-blockchain-circuits-v0.5.3-linux-x86_64), andcargo installthen goes green without ever attempting the download.-fis a genuine improvement: without it a proxy error page would be piped intotarand produce a confusing failure downstream instead of at the fetch. - The v0.1.3 guard exists and fires. Confirmed in the library source (
misplaced_above_lez_programinfreeze-authority-macros), and confirmed by test — see the inline comment.
What ed3b6ab set out to do but did not land
The commit message says the troubleshooting bullet "no longer has to describe a build that succeeds with the whole surface missing", but the diff only touches the tag and the dependency paragraph. The bullet list is unchanged, and it is now wrong for the pins this page specifies. That is the one finding, inline below with a suggested change. The unimported entry ed3b6ab adds to accept.txt is currently used nowhere in the repo, which is the other half of the same dropped edit; the suggested wording uses the word, so the vocabulary entry earns its place again.
Also re-verified at this head, on the new pin
- Auto mode compiles clean and the IDL carries all ten extension instructions in the page's own order.
set_fee_bpsunder both markers resolves toadmin_config, caller, freeze_config, freeze_account, config, so the admin gate and the auto-wrap compose in marker order as documented. - Manual mode does what it claims:
#[require_not_frozen] transferpicks upfreeze_config, freeze_account, caller, and the un-annotatedbalance_ofpicks up nothing. - Embedded mode compiles with zero warnings. Neither
admin_initializenorfreeze_initializeappears, every config account retargets onto my ownconfig, andfreeze_authority_renouncereally does drop to two accounts. - All ten lifecycle commands run under
--dry-run=textplaced before the--separator, including the one-key JSON{"Pda": ...}candidate. PDAs derive, so only--calleris needed. - The author guide still stands on its own: I built the
my-extension/my-extension-macrospair verbatim from its snippets plus aspel initconsumer, andextension_actionwas discovered and merged into the consumer IDL withmy_state, caller. - The
[u8; 32]--targetargument still encodes as 128 bytes on the wire. Unchanged from my last pass, still looks like aspelencoder bug rather than anything wrong on this page, and still out of scope here.
CI: npm ci && npm run build exits 0 with [SUCCESS] Generated static files. Vale 3.11.1 (the version the workflow pins) reports 0 errors and 0 warnings on all three pages, so neither gate blocks. The suggested change below is vale-clean at both levels.
The troubleshooting list still asked the reader to look for absent instructions, which the v0.1.3 pin makes impossible. Every cause on that list is a hard compile error now, so the section says so and the misplacement bullet separates the imported case, where the guard names it, from the unimported one, where the attribute does not resolve at all.
weboko
left a comment
There was a problem hiding this comment.
Re-ran the end-to-end dogfooding on c930105, this time covering all three pages rather than the two consumer ones. Clean Ubuntu 24.04 container, nothing preinstalled beyond rustc 1.94.1.
What I ran
- The
apt-getline in Prerequisites covers the build.cargo install --git ... spelfailed at exactly the point the page predicts,invalid peer certificate: UnknownIssuerout of thelogos-blockchain-circuits-pol-sysbuild script, and the pre-seed recipe fixed it verbatim — the tarball unpacks tologos-blockchain-circuits-v0.5.3-linux-x86_64/under~/.cache/logos/blockchain/, which is the exact pathprovision_artifactprobes, andv0.5.3is whatf7aa464resolves to.curl -fLfailing loudly on a moved asset is doing its job.spel v0.6.0installed on the second run. spel initwith both source flags,admin-authoritymerged into the scaffold's guest[dependencies], module annotated,#[require_admin]on a gated instruction.cargo checkclean;spel generate-idl | jq '.instructions[].name'from the project root printedinitialize,create_pool,set_fee_bps,admin_initialize,admin_transfer,admin_renounce. The expected-output block is right and the path-free invocation auto-detects.- Freeze on top of the same program. Auto mode gave all ten names in exactly the order the page lists. Manual mode compiles, and
set_fee_bpscarrying both gates showsadmin_config,caller,freeze_config,freeze_account,config— marker order is the ABI, as documented. Embedded mode compiles verbatim from the page:admin_initializeandfreeze_initializeboth gone,freeze_authority_renouncedown to["config","caller"], every config account retargeted onto my own. Wideningpaddingto[u8; 25]fails the build withthe marker offset 32 disagrees with ProgramConfig::ADMIN_SLOT_OFFSET, so the layout guard is real. - Moving
FreezeCandidateinside the module fails withcannot find type FreezeCandidate in this scopepointed at the#[lez_program]line, not at the import, exactly as the page warns. - Dry runs of every lifecycle command on both pages.
admin-initialize,admin-transfer --candidate Signer, the'{"Pda": {...}}'JSON form,freeze-initialize,freeze-program,freeze-account --target <64 hex>— all resolve, and the CLI's own--helpmatches the documented flag names one for one.--dry-run=textdoes have to sit before--. - Built the toy extension from Build a SPEL extension library from its own snippets. Discovery works, the consumer dependency list on that page is exactly sufficient (no
borshneeded), and removing the dependency givesmarker(s) ["my_extension"] matched no extension in the program's direct dependencies. - Repo checks:
npm ci && npm run buildsucceeds and routes the three pages under/lez/extensions/; the autogeneratedlezSidebarpicks the new folder up and position 7 is free. Vale 3.11.1 is clean at error level on the three pages, and the 18 warnings are allGoogle.WordListonCLI, which 24 other pages already use.accept.txtadds 30 entries and drops none againstmain.
Findings
Four comments below. The one that matters is on the library-author guide: its recommended pass-through marker is the shape that let freeze-authority v0.1.2 compile clean with the marker above #[lez_program] and lose its whole surface silently. That was fixed in the library in v0.1.3 and is written up on both consumer pages, but this page still teaches the unguarded version to the next author. I reproduced it on the toy extension and verified the suggested guard closes it. The other three are a stale toolchain parenthetical, a missing troubleshooting bullet, and one section-ordering pointer.
The consumer pages themselves came through this run without a correction, which is a change from the previous rounds.
Generated by Claude Code
The guide's pass-through marker is the shape that compiles clean when a consumer writes it above lez_program, with the whole surface missing and only generate-idl reporting it. The framework cannot see a marker that expanded above it, so the guard belongs in the library's macro, where both shipped libraries already carry it. The snippet now has the same check, the closing paragraph says why it is the author's job, and the troubleshooting list gains the one cause that produces no hard error.
The ruint 1.20.0 floor belongs to the cargo new route. spel init pins ruint at 1.17.0 in the guest manifest, so the paragraph says which route each floor applies to and what was verified on 1.94.0. The dependency section was the first step a reader could not do top to bottom, the guest manifest does not exist until spel init two sections later, so it now says to install the CLI and scaffold first.
weboko
left a comment
There was a problem hiding this comment.
Re-ran the end-to-end dogfooding on the two commits pushed after my last pass (ea294c0, 7413f29), on a clean Ubuntu 24.04 with rustc 1.94.1.
What I ran, following the pages literally
apt-get installline from Prerequisites, verbatim.cargo install --git https://github.com/mmlado/spel --rev f7aa464b… spel— installed in 5m21s. The circuits pre-seed remedy is needed in this environment and works exactly as written;v0.5.3is the right version,Cargo.lockatf7aa464resolveslogos-blockchain-circuitstotag=v0.5.3.spel init --spel-git … --spel-rev f7aa464… my-program, added the oneadmin-authorityline tomethods/guest/Cargo.toml, put#[admin_authority]below#[lez_program],cargo fetch,cargo check→ clean, thenspel generate-idl | jq '.instructions[].name'→initialize,do_something,admin_initialize,admin_transfer,admin_renounce. The whole admin-authority page works top to bottom on the scaffold route.- Built the extension library from Build a SPEL extension library exactly as the page specifies it (
my-extension+my-extension-macros, the metadata block, the runtimelib.rs, the proc-macrolib.rsincluding the new guard), wired it into the scaffold as a path dependency →cargo checkclean andextension_actionshows up in the consumer's IDL. The page's code samples compile as written.
7413f29 verified. spel init does pin ruint = "=1.17.0" in methods/guest/Cargo.toml (spel-cli/src/init.rs:725), and the resolver confirms it (Adding ruint v1.17.0 (available: v1.20.0)). The scaffolded program builds on the same 1.94.0 the CLI needs, so the reworded paragraph is right and the forward pointer from Add the dependency fixes the ordering problem it set out to fix.
ea294c0 verified, with one condition missing. I ran all four placements against a real build:
| marker placement | name imported? | guard in the marker macro? | result |
|---|---|---|---|
below #[lez_program] |
n/a | n/a | green, surface present in the IDL |
| above | no | irrelevant | error: cannot find attribute … in this scope |
| above | yes | no | green build, surface missing; spel generate-idl exits 1 |
| above | yes | yes | the guard's own must come after #[lez_program] error |
So the guard is worth shipping and the snippet is correct — but the silent-success case only exists once the consumer has imported the marker's name, which is the condition freeze-authority's own v0.1.3 changelog states ("compiled clean once its name was imported") and which freeze-authority.md already documents at line 352. Unimported — which is what these pages tell consumers to do — rustc rejects it with a message that says nothing about attribute order. Two comments below add that qualifier.
The third finding is that admin-authority.md's troubleshooting list still carries the pre-ea294c0 understanding: it files misplacement under the framework's hard error, which I confirmed it never produces. That page is the one page of the three that this PR's own refinement did not reach.
Checks. GitHub Actions on 7413f29 is sitting at action_required, so neither check has actually run on the fresh head. I ran both locally: npm ci && npm run build succeeds, and vale --minAlertLevel=error docs/lez/extensions/ is clean (vale 3.11.1, matching the workflow pin). The accept.txt change is a full re-sort plus 30 additions with nothing removed, so no existing vocabulary is lost.
Nothing here blocks the substance of the two new commits; all four comments are text fixes.
The pages described a marker above lez_program as a green build with the surface missing. That holds only once the consumer has imported the marker's name, which none of the pages tell them to do. Unimported, rustc stops at name resolution with cannot find attribute, a message that says nothing about order, so both cases are now spelled out where the claim was made. The admin troubleshooting list also filed misplacement under the framework's hard error, which it never reaches, so it moves out of the list into its own paragraph the way the freeze page already has it. The guide's bullet carries generate-idl's actual error and exit status. The scaffold's guest manifest holds seven dependencies, not four, so both consumer pages name them all, ruint pin included, and say to scaffold first when there is no program crate yet. The freeze page's prerequisites now count that third admin section among what it needs from there.
weboko
left a comment
There was a problem hiding this comment.
Re-ran the full end-to-end dogfooding on a clean Ubuntu 24.04 (rustc 1.94.1) against the current head, since the approval on this PR predates nine substantive commits. Almost all of it now works exactly as written. Two things still break a reader following the pages literally, both below as suggestions.
What I ran, and what passed
- Prerequisites: the
apt-getline installs clean, andunzipis load-bearing,rust-rapidsnark'sbuild.rsshells out to a bundleddownload_rapidsnark.shthat callsunzip -o(crates/build.rs:79,crates/download_rapidsnark.sh:60). cargo install --git ... spelfailed exactly as the page predicts, withinvalid peer certificate: UnknownIssuerout oflogos-blockchain-circuits-pol-sys v0.5.3. The documented escape hatch then worked verbatim: the tarball unpacks to precisely the directory nameprovision_artifactprobes for (~/.cache/logos/blockchain/logos-blockchain-circuits-v0.5.3-linux-x86_64), and the rebuild skipped the download and installedspel v0.6.0.spel initwith both source flags ahead of the name: scaffoldsmethods/guest/src/bin/my_program.rs, rootCargo.tomlis a[workspace]withexclude = ["methods/guest"], and the guest manifest carries exactly the seven entries the page now lists.- admin-authority, dedicated mode: builds with
admin-authorityas the only added line. IDL givescreate_pool, set_fee_bps, admin_initialize, admin_transfer, admin_renounce, andset_fee_bpscomes outadmin_config, caller, config, so the "prepended, ahead of the accounts you declared" wording is right. - admin-authority, embedded mode: the
ProgramConfig+#[admin_slot]+#[admin_initialize]example compiles verbatim. Noadmin_initializein the IDL, and every config account isconfigrather than the PDA, as documented. - freeze-authority, auto mode: compiles, and the ten expected names come out in exactly the order the page's block lists them.
- freeze-authority, manual mode: compiles;
balance_ofis ungated,transfergetsfreeze_config, freeze_accountinjected. - Both slots embedded in one account: compiles. Neither initializer appears, and
freeze_authority_renouncereally does drop from 3 accounts (admin_config, freeze_config, caller) to 2 (config, caller). - Lifecycle commands:
--dry-run=textbefore the--separator works for all of them.--candidate Signer, the{"Pda": {...}}one-key JSON form, and--targetas 64 hex characters all resolve and print a transaction. - build-a-spel-extension-library: built the toy extension verbatim, both crates plus the metadata. A
spel initconsumer picks it up andextension_actionlands in the IDL besidemy_user_instr. Adding the gate macro, the[[package.metadata.spel.inject]]block and the dev-dependency works too:my_user_instrcomes outmy_state, caller, thing, and the alignment self-test compiles and passes,read_inject_specs(&Path) -> Result<Vec<InjectSpec>, String>matches the source andextensionis indeed behindidl-genand unreachable throughspel-framework. - Marker misplacement, all three branches confirmed on a real build: guarded marker above
#[lez_program]with the name imported gives#[my_extension] must come after #[lez_program]: ...; unimported givescannot find attribute my_extension in this scope; guardless-and-imported gives a greencargo checkwith the surface absent, andspel generate-idlexits 1 withMalformed extension metadata: 'extension marker #[my_extension] is above #[lez_program]: ...'.4ef83eais accurate. - Both CI gates are green locally:
npm ci && npm run buildsucceeds, and vale at--minAlertLevel=erroris clean on all three pages (verified the rules actually fire by seeding a test file). All six cross-page anchors resolve against the built HTML, and_category_.jsonposition: 7no longer collides.
A root cargo fetch resolves a workspace that excludes methods/guest, prints nothing and fetches nothing, so the next check in the guest hits the offline metadata failure the sentence exists to prevent. Both consumer pages now say to run it from methods/guest and why. The require_admin paragraph claimed a renamed signer or PDA parameter is reused in place of injection, which is true of the account list and the IDL but not of the gate's prologue, which still names caller and admin_config and fails to compile. The kwarg form is the fix, and the paragraph now says so. Co-authored-by: Sasha <118575614+weboko@users.noreply.github.com>
fb93629 to
0e56d4e
Compare
weboko
left a comment
There was a problem hiding this comment.
Re-review after bfa1a32 / 0e56d4e — full end-to-end dogfood on a clean Ubuntu 24.04
Re-ran the whole thing from scratch against the PR head, since the two commits after my last pass changed the cargo fetch instruction and the require_admin kwarg paragraph. Everything below was actually executed — CLI installed, scaffold built, every code sample compiled.
Toolchain and CLI
- The
aptprerequisite line installs cleanly, and both of its non-obvious entries are genuinely load-bearing:crates/download_rapidsnark.shreally doesunzip -othe release zip, andpyo3 v0.29.2really is in the CLI's build graph. cargo install --git … spelon rustc 1.94.1: the package name note is right,spelresolves.- The TLS-proxy failure reproduced verbatim —
invalid peer certificate: UnknownIssuerout oflogos-blockchain-circuits-pol-sys v0.5.3's build script — and the documented workaround works exactly as written. The curl+tar pre-seed lands at~/.cache/logos/blockchain/logos-blockchain-circuits-v0.5.3-linux-x86_64, which is precisely what the build script'sget_cache_dir()+build_artifact_name()compute, and the reinstall then succeeds. Thev0.5.3-for-f7aa464pairing is correct.
Scaffold
spel initlayout,my-program→my_program.rs, root[workspace]withexclude = ["methods/guest"], and the seven-entry guest[dependencies]table (incl.ruint = "=1.17.0") are all exactly as described.- The flag-order warning is correct and earns its place.
spel init <name> --spel-git … --spel-rev …— which is the order the CLI's own--helpprints — silently drops both flags and pinsgit = "https://github.com/logos-co/spel.git", branch = "main". The resulting failure is exactly what the page predicts: addingadmin-authoritythere fails withexpected lee_core::account::AccountId, found spel_framework::prelude::AccountIdpointed at#[lez_program], never naming the marker.
admin-authority
- Marker-only example compiles; IDL is
create_pool, admin_initialize, admin_transfer, admin_renounce. - The
#[require_admin]+PoolConfig/write_toexample compiles verbatim. - The new kwarg paragraph is right.
#[require_admin(admin_config = my_cfg, caller = owner)]over renamed params compiles; bare#[require_admin]over a renamed signer fails with exactlycannot find value caller in this scopeat the attribute. (Renaming the PDA as well adds a secondcannot find value admin_config, same fix — the text stands as written.)
freeze-authority
- v0.1.3 + v0.1.2 resolve to one copy of each, as claimed.
- Auto mode compiles; the IDL carries all ten extension instructions in exactly the documented order. Auto-wrap is right too:
transfergetsfreeze_config, freeze_accountprepended while both#[freeze_exempt]instructions get nothing. - Manual mode compiles.
FreezeCandidateimported inside the module fails withcannot find type FreezeCandidate in this scopepointed at#[lez_program]— exactly as documented.- Embedded mode compiles verbatim with both slots in one account. IDL drops
admin_initialize/freeze_initialize, andfreeze_authority_renounceisconfig, caller— the documented 3→2. The byte ranges check out against the sources:AdminConfigis oneAuthoritySlot(32),FreezeConfigis slot +is_frozen(33), so32..64and64..97are correct. - The layout guard fires as promised: a field added above the slot gives
the marker offset 32 disagrees with ProgramConfig::ADMIN_SLOT_OFFSET. - All ten lifecycle commands dry-run cleanly with the documented syntax, including
--candidate Signer, the'{"Pda": {…}}'JSON form, and--targetas 64 hex characters.
build-a-spel-extension-library
- Built the entire guide: two-crate layout,
extension_attrmetadata,extern crate self as my_extension, the marker and the#[instruction]shim, all verbatim. A consumer picks it up and the IDL showsmy_user_instr, extension_action. - The misplacement guard works and emits the guide's own error text.
- The gate + inject section works:
#[require_my_gate]with the[[package.metadata.spel.inject]]block synthesisesmy_stateandcaller, and the IDL confirms the documented ordering —gated_op: my_state, caller, thing, injected ahead of declared. spel_framework_core::extension::read_inject_specs(&Path) -> Result<Vec<InjectSpec>, String>exists with that exact signature, andpub mod extensionis indeed behind#[cfg(feature = "idl-gen")].
Repo CI
npm ci && npm run build passes, vale --minAlertLevel=error is clean on all three pages, and the accept.txt re-sort is lossless — 30 additions, nothing dropped.
One thing to fix, in the sentence 0e56d4e just rewrote, on both consumer pages. The methods/guest instruction itself is correct and I verified it; it's the "prints nothing, and fetches nothing" half that doesn't survive contact. Details and suggestions inline.
The guest-fetch sentence said a root cargo fetch prints nothing and fetches nothing. It fetches the root workspace's own graph, which is noisy on a cold cache and silent on a warm one, and in neither state does the output say the extension was skipped. Both consumer pages now describe the symptom the reader can actually see, which is a successful-looking run that omitted the one dependency that matters. Co-authored-by: Sasha <118575614+weboko@users.noreply.github.com>
Summary
Adds two pages under a new
docs/lez/extensions/folder, plus the section's_category_.json:admin-authority.md, integration guide for app developers. Covers annotation, gating, AdminCandidate (Signer / PDA), initialization window risk, transfer, renounce, IDL verification, security notes, and embedded mode (the admin slot inside a consumer account via#[admin_slot]and#[admin_initialize]).build-a-spel-extension-library.md, author guide for the generic[package.metadata.spel]discovery mechanism. Covers two-crate layout, metadata fields, marker / gate macros, multi-extension stacking, auto-wrap, embedded mode with bound args and slot markers, common troubleshooting.Context
These pages document the user-facing surface of RFP-001 (admin-authority library) and the framework mechanism it relies on. The framework mechanism, a generic
[package.metadata.spel]extension scanner inspel-framework-core::idl_gen, is delivered in logos-co/spel#233. These docs are the consumer-facing complement.Future extension libraries (e.g. RFP-002 freeze-authority) can ship without any framework change, so the "Build a SPEL extension library" page is written generically to serve as the canonical author guide for any future extension.
Why a new
extensions/folderlez/already groups capabilities by folder (get-started/,transfer-tokens/). Extension libraries are a new capability class. Placing both pages underlez/extensions/mirrors the existing structure and keeps consumer and author guides discoverable side-by-side.Companion repositories
mmlado/spel-admin-authority: the admin-authority library implementation referenced byadmin-authority.md.[package.metadata.spel]extension scanner spel#233: the framework PR carrying the generic extension scanner referenced by both guides.Open questions for maintainers
extensions/for brevity. Alternatives:extension-libraries/,program-extensions/,access-control/(too narrow for the generic guide).lez/. The author guide could arguably sit underbuild-an-app/instead. Open to splitting if you prefer.lez/pages yet, happy to add.Style notes
Pages carry the standard docusaurus frontmatter and use admonitions for the draft-status banner and warnings. Prose follows the house British English style checked by Vale.
Relates to
[package.metadata.spel]extension scanner spel#233