Navigation
Copy page

Publishing Extensions

On this page

This page is the publishing side. To let someone install your extension with kli install, you host the code at a URL and hand out two things: the URL and a pin — the git object id of the exact bytes you published. kli refuses to load anything whose content does not hash to that pin, so the pin is what lets a stranger install your code and still know exactly which bytes ran. Signing is an optional layer on top that also proves who published them. If your recipients are Nix users, you can instead distribute the extension as a flake package they bake into their image — see Distribute as a Nix package.

For the receiving side — running kli install and confirming the trust cards — see Sharing extensions.

Decide the shape

An extension is published in one of two shapes, and the pin is a different git object id for each.

  • A single Lisp file is the common case: one .lisp file, one blob, pinned by its git blob id. Publish the file as-is.
  • A directory extension is a multi-file unit — several source files, a load order, usually an .asd. It is published as one bundle blob whose pin is the git tree id over its unpacked tree. cairn ships this way.

If your extension is a single file, skip to Publish a single file. If it is a directory, see Structure a directory extension first.

Publish a single file

Host the .lisp file at a stable URL that returns its raw bytes. Compute the pin with git:

git hash-object extension.lisp

That prints the git blob object id — git's sha-1 over the blob header and the file content, the same id git stores the file under. Hand out the URL and this pin. A recipient installs it with:

kli install https://example.com/extension.lisp <pin>

kli fetches the bytes, hashes them the same way, and refuses the install if the result does not equal the pin. Serve the file over HTTPS from a location whose bytes do not change under the same URL; if you edit the file, its pin changes and you publish a new pin.

Structure a directory extension

A directory becomes a single loadable unit when it carries either an extension.lisp marker file or exactly one .asd system definition. kli then treats the whole top-level directory as one extension and does not descend into it.

Load order is decided as follows:

  • With an .asd, the :components order in the system definition governs. kli loads the unit by running asdf:load-asd then asdf:load-system, so ASDF's declared order is honored exactly.
  • Without an .asd, the convention governs: package.lisp loads first, extension.lisp loads last, and the remaining files load in alphabetical order.

If your files have dependencies that alphabetical order would break — a store file that must load before the model that uses it, say — author an .asd and declare the order. A minimal one:

lisp
(defsystem "my-extension"
  :serial t
  :components ((:file "src/package")
               (:file "src/store")
               (:file "src/model")
               (:file "src/extension")))

:serial t loads the components in the listed order. The unit is rooted at the directory holding the .asd; the marker and package conventions from Loading and managing extensions still describe how a local directory is discovered.

Bundle a directory extension

A directory is published as one bundle blob: a JSON envelope that carries every file, so it fetches, verifies, and signs exactly like a single file does. The envelope is:

json
{
  "format": "kli-dir-bundle-v1",
  "files": {
    "my-extension.asd": "<base64 of the file bytes>",
    "src/package.lisp": "<base64 of the file bytes>",
    "src/store.lisp":   "<base64 of the file bytes>"
  }
}

Each key is a path relative to the extension root; each value is the base64 of that file's raw bytes. Sort the paths and emit compact JSON so the bytes are a deterministic function of the source — the same input always produces the same bundle, and therefore the same pin.

The pin for a bundle is the git tree id over the unpacked files, which is what git write-tree produces for the same file set:

git init -q tree && cd tree
# copy the extension's files into place, preserving relative paths
git add -A && git write-tree

That id verifies the whole tree at once. Host the bundle blob at a URL and hand out the URL and this pin; kli detects the envelope, unpacks it, verifies the tree, and places the directory unit under the recipient's extensions directory.

Sign a release

The pin proves integrity — that the bytes are the ones the pin names. A signature proves authenticity — that you published them. Signing is opt-in: a recipient who has configured no trust roots verifies the pin only, and one who trusts your key requires a valid signature from it or refuses the install. See trustRoots for the recipient side.

To sign, mint an ed25519 keypair, keep the private seed secret, and publish the public key as hex. Sign the raw bytes of the published artifact — the exact file or bundle blob you serve — producing a detached ed25519 signature over those bytes (ed25519 hashes the message internally, so the bytes are signed unhashed). Host the signature next to the artifact at <url>.sig: for extension.lisp that is extension.lisp.sig. kli fetches the signature by this convention only when the recipient has configured trust roots.

Because the signature is over the served bytes, re-sign whenever you re-serialize the artifact. Rotating your key means publishing the new public key; recipients update their trust roots to match.

What you hand out

A published extension is fully described by:

  • the URL the artifact is served from,
  • the pingit hash-object for a single file, git write-tree for a directory bundle,
  • your public key hex, if you signed it, for recipients to add to their trust roots.

A checksums file listing the artifact's sha-256 is a courtesy for out-of-band verification, but the pin, not the checksum, is what kli enforces. With those in hand, a recipient runs kli install <url> <pin> and, for a directory extension like cairn, serves it afterward with kli mcp-serve <id>.

Distribute as a Nix package

The URL-and-pin channel above is per-user and runtime: a recipient runs kli install and the extension lands in their config directory. The other way to hand out an extension is as a Nix package a recipient bakes into their kli image with programs.kli. Nothing is fetched or pinned at install time — the extension is compiled into the image, and trust rides on the recipient's flake inputs and Nix hashing. This is the channel Nix users prefer, and it is how cairn ships. You can offer both from one source: a URL-and-pin build for runtime installs, and a flake package for image builds.

To distribute this way, expose your extension as a flake package that is a buildLisp library carrying two passthru fields:

  • passthru.name — the extension id.
  • passthru.manifestSymbol — the package-qualified symbol naming your manifest, the *…-extension-manifest* variable defextension binds. kli fails the image build if the compiled result does not export it, so a mislabelled package cannot ship silently.

kli composes that package into the image as a build dependency, so it must be built with the same buildLisp kli uses. The plug-and-play way to get buildLisp and a batteries-included set of Common Lisp libraries is the public cl-deps flake — the exact dependency set kli itself is built from:

nix
{
  inputs.cl-deps.url = "github:kleisli-io/cl-deps";
}

cl-deps.lib.<system>.buildLisp builds your library, and cl-deps.lib.<system>.lisp.* supplies common dependencies. A minimal extension package:

nix
cl-deps.lib.${system}.buildLisp.library {
  name = "greet";
  srcs = [ ./src/package.lisp ./src/greet.lisp ];
  deps = [ cl-deps.lib.${system}.lisp.alexandria ];
  passthru = {
    name = "greet";
    manifestSymbol = "greet:*greet-extension-manifest*";
  };
}

Expose that as packages.<system>.default, and a recipient adds inputs.greet.packages.${system}.default to programs.kli.extensions.

If cl-deps does not already package a Lisp library you depend on, fork it and add the definition — its README shows the one place to declare a new library, and native C dependencies use the native attribute. cl-deps is one of several ways to build Common Lisp under Nix; you are free to use another for your own code, but the package you hand to programs.kli.extensions must be a buildLisp library so it can compile into the image.