Skip to content

docs(lez): Extensions section with admin-authority and extension-library guides - #355

Draft
mmlado wants to merge 13 commits into
logos-co:mainfrom
mmlado:docs/admin-authority-rfp-001
Draft

docs(lez): Extensions section with admin-authority and extension-library guides#355
mmlado wants to merge 13 commits into
logos-co:mainfrom
mmlado:docs/admin-authority-rfp-001

Conversation

@mmlado

@mmlado mmlado commented Jun 24, 2026

Copy link
Copy Markdown

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 in spel-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/ folder

lez/ already groups capabilities by folder (get-started/, transfer-tokens/). Extension libraries are a new capability class. Placing both pages under lez/extensions/ mirrors the existing structure and keeps consumer and author guides discoverable side-by-side.

Companion repositories

Open questions for maintainers

  1. Folder name. Used extensions/ for brevity. Alternatives: extension-libraries/, program-extensions/, access-control/ (too narrow for the generic guide).
  2. Page placement. Both under lez/. The author guide could arguably sit under build-an-app/ instead. Open to splitting if you prefer.
  3. Cross-linking. The pages reference each other directly. No links from the broader 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

mmlado added 5 commits June 24, 2026 17:16
…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.
@kashepavadan

Copy link
Copy Markdown
Contributor

@mmlado is this ready for review? does it need updating for v0.2?

@mmlado

mmlado commented Aug 3, 2026

Copy link
Copy Markdown
Author

@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.

mmlado added 5 commits August 4, 2026 06:50
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.
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.

@danisharora099 danisharora099 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. The dependency snippets are unrunnable: spel-admin-authority with no branch resolves to an empty main (README and LICENSE, no crate), and spel-framework pointing 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.
  2. Missing prerequisites that stop a cargo new user: 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 the cargo fetch step (the scanner's cargo metadata --offline call deterministically fails for fresh git consumers without it).
  3. Smaller: the "library README's dependency table" claim is false for the admin repo; the .write_to helper 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.

mmlado added 2 commits August 10, 2026 12:13
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.
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.
@mmlado

mmlado commented Aug 10, 2026

Copy link
Copy Markdown
Author

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:

  • The dependency snippet lists all five crates the samples need, with a sentence on what nssa_core, borsh and serde are for.
  • The cargo fetch step is documented with its reason, the scanner's offline metadata call.
  • A cargo new consumer learns to delete the default fn main.
  • The README dependency table claim is gone.
  • The hand-written write_to helper is shown next to the embedded snippet.
  • The silent-drop caveat dissolved upstream: an unmatched marker is now a hard compile error regardless of resolution state, and the troubleshooting sections document that error instead.
  • fe8409d. Notable corrections: the embedded metadata example gains the now mandatory state_type key, the bound args trailing rule is stated, Signer transfers walk the co-sign exchange, and a candidate PDA is a claimed account, not a deployed one.

@mmlado
mmlado requested a review from danisharora099 August 10, 2026 11:56

@weboko weboko left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dogfooded both pages end-to-end on a clean Ubuntu 24.04 container (Rust 1.94.1), building a real consumer program from scratch and running every command.

The pages are blocked at step one, and the second failure is silent.

  1. cargo fetch hard-fails immediately: admin-authority is unpinned and its default branch main holds only a README and licences. The crate lives on m3 / tag m3-settle.
  2. After pinning that, the build succeeds — but produces a program with no admin instructions at all, because spel-framework points at logos-co/spel main (v0.6.0), which does not carry the extension scanner these pages are built on. That scanner is still unmerged in logos-co/spel#233.

The second one matters most because both pages promise it cannot happen: "a marker that matches no discoverable extension is a hard compile error." I verified that guarantee holds on the scanner-carrying revision and does not hold on logos-co/spel main, where a bogus marker compiles clean in 3.13s.

With the pins corrected to what admin-authority's own CI fixture uses, everything works and matches the docs:

$ spel generate-idl src/main.rs | jq -r '.instructions[].name'
create_pool
admin_initialize
admin_transfer
admin_renounce

...and the compiled binary embeds all three. So the content is sound — the technical claims I spot-checked in the author guide (read_inject_specs, wrap_instructions keys, inject/embedded/bound_args metadata shapes) all match the shipped code. It's the dependency pointers and one over-broad safety claim that need fixing.

Inline comments have suggested changes for each. Two things for you to decide rather than for me to patch:

  • Shipping order. Both corrected pins point at a personal fork (mmlado/spel) and an unmerged branch. It may be cleaner to hold these pages until logos-co/spel#233 lands and the library has a tagged release under logos-co; otherwise the draft banner should say plainly that the page tracks unreleased code on a fork.
  • spel CLI prerequisites. The pages use the CLI but never say how to install it, and the CLI's own framework revision silently determines whether generate-idl reports the extension instructions. That deserves a short prerequisites section (details on line 122 and 241).

Comment on lines +38 to +39
admin-authority = { git = "https://github.com/mmlado/spel-admin-authority" }
spel-framework = { git = "https://github.com/logos-co/spel" }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocker — this dependency block cannot resolve. cargo fetch fails on the first step.

I followed this page end-to-end on a clean Ubuntu 24.04 box with Rust 1.94.1. Copying this block verbatim and running the cargo fetch from the next paragraph gives:

error: failed to get `admin-authority` as a dependency of package `my_program`
Caused by:
  Unable to update https://github.com/mmlado/spel-admin-authority
Caused by:
  Could not find Cargo.toml in `/root/.cargo/git/checkouts/spel-admin-authority-7dd198e322803bb1/2cd79b3`

Two separate problems:

  1. admin-authority is unpinned, so it resolves to that repo's default branch main — which contains only README.md and two LICENSE files. There is no crate there at all. The library actually lives on m3 (also tagged m3-settle) and feat/derived_embedding.

  2. spel-framework points at logos-co/spel, whose main is v0.6.0 and does not contain the extension scanner this page depends on. There is no spel-framework-core/src/extension.rs on that branch — the scanner is still unmerged in RFP-001 framework enabler: generic [package.metadata.spel] extension scanner spel#233. admin-authority itself pins mmlado/spel.git rev a0dfcd85 for exactly this reason (see admin-authority/Cargo.toml and ci/consumer-fixture/guest/Cargo.toml on m3).

Problem 2 is the dangerous one, because it does not fail loudly. With logos-co/spel as the framework, the program compiles cleanly and you silently get a binary with no admin instructions — see my comments on lines 241 and 252.

With the two pins below, I got a clean build and all three admin instructions in the IDL and in the compiled binary:

Suggested change
admin-authority = { git = "https://github.com/mmlado/spel-admin-authority" }
spel-framework = { git = "https://github.com/logos-co/spel" }
admin-authority = { git = "https://github.com/mmlado/spel-admin-authority", branch = "m3" }
spel-framework = { git = "https://github.com/mmlado/spel.git", rev = "a0dfcd85af7dfdb7ecb8117474d39e85415afc96" }

Since both pins point at a personal fork and an unmerged branch, it's worth deciding whether this page should ship before logos-co/spel#233 lands and the library gets a release tag on logos-co. If it ships now, the draft banner should say explicitly that it tracks unreleased code.


```rust
use spel_framework::prelude::*;
use admin_authority::{admin_authority, require_admin};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Importing the marker produces an unused-import warning. #[admin_authority] is matched by attribute name only — the framework never resolves it as a path, so nothing needs to be in scope for it. Building this snippet as written gives:

warning: unused imports: `admin_authority` and `require_admin`
 --> src/main.rs:2:23

The CI-verified reference program (admin-authority-sample/src/main.rs on m3) imports only the gate:

use admin_authority::require_admin;
use spel_framework::prelude::*;
Suggested change
use admin_authority::{admin_authority, require_admin};
use admin_authority::require_admin;

(require_admin is unused in this particular snippet too, since create_pool isn't gated — it becomes used at the "Gate an instruction" section below. Worth either dropping it here as well or adding #[require_admin] to the example so the import earns its place.)

`admin_initialize` takes no arguments. The signing caller becomes the admin (self-election). There is no candidate argument at initialize because the LEZ duplicate-account rule rejects a transaction listing the same account twice, so a caller could never also pass itself as candidate evidence.

```bash
spel --idl program-idl.json --program <program-id> -- \

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the first use of the spel CLI, but the page never says how to get it, and there is no prerequisites section pointing anywhere that does. Following the page end-to-end stalls here.

For what it's worth, building it from source is not a one-liner in a clean environment — cargo build --manifest-path spel-cli/Cargo.toml pulls logos-blockchain-circuits-pol-sys, whose build script downloads a prebuilt circuits artifact and panics if that download fails. Also note the package is named spel, not spel-cli, so cargo build -p spel-cli fails with package ID specification 'spel-cli' did not match any packages.

A short "Prerequisites" section before "Add the dependency" would fix this — Rust toolchain, how to install/build spel, and (per my comment on line 241) which framework revision the CLI must be built from.

After building your program, check that the admin instructions appear in the IDL:

```bash
spel generate-idl path/to/your/program/src/main.rs | jq '.instructions[].name'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This verification step silently under-reports unless the spel CLI itself was built from a revision carrying the extension scanner. generate-idl resolves extensions in the CLI, not in your program, so the CLI's framework revision decides what you see.

I ran this against one identical consumer program with two CLIs:

# spel built from logos-co/spel main (v0.6.0)
create_pool

# spel built from mmlado/spel rev a0dfcd85 (has the extension scanner)
create_pool
admin_initialize
admin_transfer
admin_renounce

Same source file, same Cargo.toml. The first CLI reports success and just omits the trio — there is no warning and no non-zero exit, so the "Expected output" below reads as a passed check when it actually failed.

Worth stating explicitly here which build of spel this step requires, e.g.:

Suggested change
spel generate-idl path/to/your/program/src/main.rs | jq '.instructions[].name'
spel generate-idl path/to/your/program/src/main.rs | jq '.instructions[].name'

...with a following note: the spel binary must be built from the same framework revision pinned in your Cargo.toml; a CLI built without the extension scanner omits the admin instructions from this output without reporting an error.

"admin_renounce"
```

Plus your own instructions. A marker that matches no discoverable extension is a hard compile error naming the marker, so a broken setup refuses loudly rather than building without the trio. When you hit that error, the most common causes are:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This safety guarantee does not hold for the dependency set this page tells you to use. It's true only when the framework build carries the extension scanner.

I tested both ways with a deliberately bogus marker (#[no_such_extension]) on a #[lez_program] module:

  • Framework = mmlado/spel rev a0dfcd85 (has the scanner) → hard error, exactly as promised:
    error: marker(s) ["no_such_extension"] matched no extension in the program's
    direct dependencies...
    
  • Framework = logos-co/spel main (what line 39 specifies) → Finished dev profile ... in 3.13s. Clean build, no warning, no error.

And with the real #[admin_authority] marker against logos-co/spel main, the program builds and generate-idl shows only create_pool — precisely the "building without the trio" outcome this sentence promises is impossible.

Suggest scoping the claim so a reader on the wrong framework revision isn't told their silent failure can't happen:

Suggested change
Plus your own instructions. A marker that matches no discoverable extension is a hard compile error naming the marker, so a broken setup refuses loudly rather than building without the trio. When you hit that error, the most common causes are:
Plus your own instructions. On a framework build that carries the extension scanner, a marker that matches no discoverable extension is a hard compile error naming the marker, so a broken setup refuses loudly rather than building without the trio. Note that this safety net is itself a property of the pinned framework revision: on a framework without the scanner, the marker is ignored and the program builds silently without the trio. When you hit that error, the most common causes are:

```toml
[dependencies]
my-extension = { git = "https://github.com/you/my-extension" }
spel-framework = { git = "https://github.com/logos-co/spel" }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue as admin-authority.md line 39: logos-co/spel main (v0.6.0) does not contain the extension scanner described throughout this page — there is no spel-framework-core/src/extension.rs on that branch, and extension_attr appears nowhere in the tree. A consumer who copies this line gets a program where the marker is silently ignored.

Everything else I spot-checked on this page matches the shipped code on the scanner-carrying revision: read_inject_specs(crate_dir: &Path) exists at spel_framework_core::extension::read_inject_specs (line 178), and the wrap_instructions keys wrapper / skip / self_exempt_marker / exempt (lines 202-207), the [[package.metadata.spel.inject]] shape, embedded.skip / state_type, and [[package.metadata.spel.bound_args]] all match admin-authority/Cargo.toml and the framework's metadata reader. So it's only the dependency pointer that's wrong.

Suggested change
spel-framework = { git = "https://github.com/logos-co/spel" }
spel-framework = { git = "https://github.com/mmlado/spel.git", rev = "a0dfcd85af7dfdb7ecb8117474d39e85415afc96" }

Swap this for the logos-co URL once logos-co/spel#233 merges.

spel generate-idl path/to/sample/src/main.rs
```

The IDL should contain your extension's instructions alongside the consumer's own. A marker that matches no discoverable extension is a hard compile error naming the marker, regardless of why it did not match, so a broken setup refuses loudly instead of building a program silently missing its extension surface. When you hit that error, the most common causes are:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same qualification as admin-authority.md line 252 — this "refuses loudly" guarantee is a property of the framework revision, not of the mechanism. Verified with a bogus marker: hard compile error on mmlado/spel rev a0dfcd85, clean successful build on logos-co/spel main (which line 247 currently points readers at).

Given this page is the canonical author guide, it's worth being explicit that the fail-closed behaviour requires a framework build carrying the scanner, so an author debugging a missing extension surface checks their framework pin before their metadata.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants