Skip to main content
This guide is for template publishers and tap operators. It shows how to turn a validated declarative profile package into an immutable signed release that an llmwiki user can discover, verify, install, and update. llmwiki ships the publisher workflow: template publish init | add | build | rotate | revoke | verify creates keys, signs packages, builds a verified static tree, and manages key rotation and revocation. It does not upload: publishing the built tree to HTTPS hosting stays with your existing deploy tooling. Everything below runs offline. Private keys never leave your workspace, and the protocol appendix at the end remains the interoperability contract for anyone implementing it independently.
A signature proves which key signed exact bytes. It does not certify that a profile is useful or that its labels are safe instructions. Remote profiles remain untrusted declarative input and pass the ordinary profile validator on every install or update.

What you publish

A tap is a static HTTPS directory containing one signed index and immutable, content-addressed package envelopes:
If index.json is at /community/index.json, llmwiki derives each package URL as /community/packages/sha256/<hex>.json. The package must stay on the exact scheme, host, and port used by the index. Redirects to another origin are refused. Every release has a coordinate:
Tap, publisher, and template names use lowercase slug-safe identifiers. A released coordinate is immutable: never publish different bytes under the same coordinate.

1. Create a workspace and keys

This creates separate Ed25519 keypairs for the tap root and the publisher, writes them 0600 under my-tap/keys/, and records a workspace at sequence 0. Private keys are never printed and are never overwritten. The command prints each public key’s SHA-256 fingerprint — publish those fingerprints through the channel your users trust.
The workspace holds private keys. Never serve it, and never point --out inside it: build refuses that, because it would publish your signing keys.

2. Sign a package

add validates the package with the same validator a consumer runs, derives its coordinate and payload digest, signs the package claim, and records the coordinate as immutable: the same coordinate may never resolve to different bytes.

3. Build a distribution

build advances the sequence, signs the index, writes packages before the index, and verifies the complete tree as a consumer would before publishing it. Only then does it swap the tree into --out and commit the new sequence. A failed build leaves the workspace at its old sequence, so a retry is a clean re-run. To renew a distribution whose lifetime is expiring without changing its contents, rebuild with --refresh. It republishes the same tree under a fresh --expires-in window and advances the sequence. Without it, a build that finds nothing changed is refused, so an accidental rebuild never burns a sequence. Serve ./dist from ordinary HTTPS hosting.

4. Rotate and revoke

Rotations and revocations are staged, then signed by the next build. This is not an implementation detail: a rotation claim carries the sequence of the index that publishes it, and that sequence is only known at build time.
A tap-root rotation is carried by exactly one index. A client that does not refresh while that index is the published one cannot verify the new root: it must forget and re-add the tap, re-pinning your key through your trusted channel. Publisher-key rotation has no such window — a client can walk the retained chain from any later index. Keep a tap-root rotation published long enough for users to refresh, and announce the new fingerprint out of band.
A publisher-key rotation re-signs every package with the successor key. It must: a package signed by a retired key stops verifying the moment the index announces its successor, because a verifier resolves the publisher key by id from the current index. The payload never changes, so digests and filenames are stable. Revoking the active publisher key requires rotating to a successor in the same build — an index that announces a revoked key is one no client will accept.

5. Verify before you publish

Pass the key from where you distribute it, not from inside the tree being verified: a key carried by the tree it verifies proves only self-consistency. publish verify checks one self-contained snapshot and refuses an index carrying rotations, because a latest-snapshot-only directory holds no pinned keys to walk a chain from. That is a scope limit, not a defect: build already verified the rotation against your workspace’s own key history, and a real client verifies it against the keys it pinned from your previous release.

Protocol appendix

The rest of this guide is the wire contract. You do not need it to publish with the CLI above; it exists so anyone can implement the protocol independently, and so you can audit exactly what the CLI signs.

Create separate signing keys by hand

Use separate Ed25519 keys for the tap and each publisher. The tap key signs the catalog snapshot. A publisher key signs that publisher’s package claims. This Node.js example creates SPKI public bytes and PKCS8 private bytes in the base64 representation used by the protocol:
Store private keys in a secrets manager or offline signing environment. Do not leave the example’s private-key output in shell history or a shared build log. Publish only the SPKI public bytes. Assign stable, globally unambiguous key ids such as acme-publisher-2026-01 and community-tap-2026-01. The tap root key must reach users through a trusted channel independent of the tap server. Users provide it explicitly when adding the tap; llmwiki does not use trust on first use.

2. Build the template payload

The signed payload is an ordinary ProfileTemplatePackage with sourceType: "remote". The template id and profile id must match.
During authoring, test a copy with sourceType: "local" through llmwiki template init --file. Restore sourceType: "remote" before signing the release payload. Remote installation repeats package validation, profile validation, connector-binding checks, and minimum-version checks, so a signature cannot make an invalid package installable.

3. Canonicalize, digest, and sign the package

All digests and signatures use RFC 8785 canonical JSON bytes. Do not sign pretty-printed JSON or rely on object insertion order. The implementation uses the canonicalize npm package.
The publisher signs the canonical claim { coordinate, payloadDigest }, not the full envelope:
Write the envelope as JSON to:
The filename and signed index both identify the canonical payload digest. The client recomputes it and refuses any mismatch.

4. Build and sign the tap index

The index lists publisher public keys and every release currently available through the tap:
Construct the object without signature, sign that complete unsigned object with the tap private key using the same canonicalBytes and signature helpers, and then add the resulting signature field. For every new snapshot:
  1. Increase sequence; never reuse or decrease it.
  2. Set valid UTC generatedAt and expiresAt values.
  3. Keep every coordinate bound to its original digest forever, even if it is temporarily absent from a later index.
  4. Publish package envelopes before replacing index.json.
  5. Replace index.json atomically at the hosting layer.
The index is limited to 10,000 entries per bounded collection and 4 MiB. A package envelope is limited to 2 MiB.

5. Verify the distribution offline before publishing

Before uploading anything, verify the built tree exactly as a client would verify the bytes it downloads. This runs offline, reads only, and writes nothing.
The directory must be the exact static tree you intend to serve, and nothing else:
Any extra, missing, or symlinked entry is a failure, not a warning. Each package filename must be the hex payload digest of the entry that the signed index names. The command re-runs the same parsers, Ed25519 verification, continuity, revocation, and profile validation that a real client runs, then prints bounded provenance:
Use --json for the same result as a stable object. Failures exit non-zero with a bounded reason and never echo file contents or local paths.
Pass the tap public key from wherever you distribute it, not from inside the distribution you are checking. A key carried by the same tree it verifies proves only that the tree is self-consistent. Verification is meaningful only when the key reaches the verifier through the independent channel your users trust.
Snapshot scope. This command verifies one self-contained snapshot. It refuses any index carrying rotations or a tapKeyRotation, because a rotation chain is only meaningful against a client’s previously pinned key, and a fresh offline check has no pins to walk from. That is why the output says not_applicable_no_rotations. Verify a rotation-bearing index the way a user experiences it: from a clean client that already pinned the old key, as in the next section. See Rotate or revoke deliberately.

6. Verify from a clean client

Distribute the tap public key, its fingerprint, and the index URL through an independent trusted channel. Then test exactly what users will run:
Test installation in an empty project:
The verification client checks the tap signature, sequence, expiry, publisher key continuity, revocations, package digest, publisher signature, coordinate identity, template manifest, and profile contract before installation.

7. Publish an update

Never replace an existing release. Create a new payload with a higher semantic version, sign a new envelope, upload it at its new digest path, and publish a higher-sequence index containing the new coordinate. Users can then preview and apply the update:
The update planner refuses local profile drift, incompatible existing content, pending review candidates, active workflow runs, or changed tap authority. The apply path journals the old profile and provenance so interruption is recoverable.

8. Rotate or revoke deliberately

Publisher key rotation is a signed chain. Each rotation record identifies the publisher, old key id, new public key, and effective sequence. The canonical rotation claim is signed by both the old and new private keys. Retain the rotation history in later indexes so clients that skipped snapshots can walk from their pinned key to the current key. Tap-root rotation uses the same dual-signature principle through tapKeyRotation. Users who cannot verify a cooperative rotation must explicitly forget and re-add the tap, which resets its trust history.
An index carrying rotations or a tapKeyRotation is refused by llmwiki template publish verify, which checks one self-contained snapshot and holds no pinned keys to walk a chain from. This is a scope limit, not a defect in your index. Verify rotation-bearing snapshots from a clean client that already pinned the previous key, exactly as your users will experience the rotation.
To revoke evidence, add a revocations record with kind package and the payload digest, or kind publisher-key and the key id. Include a reason and UTC revokedAt timestamp. Revocations accumulate monotonically in client state; removing one from a later index does not restore trust.

Release checklist

  • The payload is declarative and validates with the target llmwiki version.
  • templateId, profileId, publisher, and coordinate agree.
  • The package claim and index use RFC 8785 canonical bytes.
  • Package files are uploaded before the new index becomes visible.
  • The sequence increased and the expiry window is intentional.
  • The tap root key is distributed outside the tap server.
  • A clean client can refresh, verify, install, and report clean status.
  • Previous coordinates still resolve to their original immutable bytes.
The repository’s offline protocol fixture is the compatibility oracle for envelope and index implementations.