# Publishing Extensions


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](#distribute-as-a-nix-package).

For the receiving side — running `kli install` and confirming the trust cards — see [Sharing extensions](/kli/extend/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](#publish-a-single-file). If it is a directory, see [Structure a directory extension](#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](/kli/extend/lisp-extensions/loading-and-managing) 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`](/kli/config/settings#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 **pin** — `git 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`](/kli/config/nix-module). 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`](/kli/extend/lisp-extensions/anatomy) 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.

## Related

- [The programs.kli module](/kli/config/nix-module) — the recipient side of the Nix-package channel: baking your extension into an image.
- [Sharing extensions](/kli/extend/sharing-extensions) — the receiving side: `kli install`, the two trust cards, and what persists.
- [Loading and managing extensions](/kli/extend/lisp-extensions/loading-and-managing) — how a directory unit is discovered and ordered on disk.
- [Write your first Lisp extension](/kli/extend/lisp-extensions/write-your-first) — authoring the extension you are publishing.
- [`trustRoots`](/kli/config/settings#trustroots) — the recipient-side setting that turns signature verification on.
