Add-ons and collections
A Kraft add-on is a directory of chains, library components, skills and agent profiles. A collection is a git repository or a directory that lists add-ons. Kraft ships no collection and has no directory of them, so you add each collection by its address, in one of the forms under Collection sources. config/addons.yaml names the collections an instance knows and the add-ons it installed, and config/addons.lock records what each one resolved to.
Share chains with add-ons walks through publishing and installing one. The kraft admin addon verbs are in Admin verbs.
A collection's layout
acme-kraft/
.kraft/collection.json
addons/
release/
.kraft/addon.json
library.yaml
profiles.yaml
chains/ship.yaml
skills/release-notes/SKILL.md
.kraft/collection.json sits at the root of the collection. Each add-on is a directory inside it with its own .kraft/addon.json.
Both manifests are JSON. Kraft ignores a top-level key it does not read, so a manifest written for a newer Kraft still loads, and kraft admin addon validate lists each such key as a warning. The JSON Schemas are published for editors:
collection.json
{
"$schema": "https://kraft-sdlc.dev/schemas/collection.schema.json",
"name": "acme",
"description": "ACME's Kraft chains and review methods",
"owner": { "name": "ACME Platform", "email": "platform@acme.dev" },
"addons": [
{ "name": "release", "source": "./addons/release", "description": "Release chains" }
]
}
| Field | Type | Required | Meaning |
|---|---|---|---|
name | name | yes | The collection's id: its key in addons.yaml, and the part after @ in an add-on id. |
description | string | no | What the collection holds. |
owner | object | yes | name and email, both required, and an optional url, which is an absolute https:// URL. |
addons | list | yes | At least one entry. No name is listed twice, and no entry's source is inside another's. |
addons[].name | name | yes | The same name the add-on's own addon.json declares. |
addons[].source | string | yes | The add-on's directory: ./ and a relative path, with no . or .. segment, no trailing /, and not inside .kraft/. |
addons[].description | string | no | What the add-on holds. |
An add-on lives in its collection's own repository. A source that is not a relative path is refused.
addon.json
{
"$schema": "https://kraft-sdlc.dev/schemas/addon.schema.json",
"name": "release",
"version": "1.0.0",
"description": "Release chains",
"requires": { "kraft": "2", "harnesses": ["claude"], "profiles": ["strong"] }
}
| Field | Type | Required | Meaning |
|---|---|---|---|
name | name | yes | The add-on's name and its default namespace. Never kraft. |
version | string | yes | MAJOR.MINOR.PATCH, with an optional pre-release suffix such as 2.0.0-rc.1. No build metadata. |
description | string | no | What the add-on holds. |
author | object | no | name and email, both required when author is given, and an optional url. |
homepage | string | no | An absolute https:// URL. |
license | string | no | The license's name. |
requires | object | yes | What the add-on needs from the instance. An unknown key inside it is refused: requires.<key> is not understood by this Kraft; upgrade Kraft. |
requires.kraft | string | yes | MAJOR or MAJOR.MINOR. See Which Kraft an add-on runs on. |
requires.harnesses | list of names | no | Harness profile ids the instance's harnesses.yaml defines. The add-on's tasks name only these. |
requires.profiles | list of names | no | Agent profiles of the instance that the add-on's tasks select by bare name. |
Which Kraft an add-on runs on
requires.kraft names the oldest Kraft the add-on runs on, and that version's major is the only one it runs on.
requires.kraft | Runs on |
|---|---|
"2" | Any 2.x. |
"2.4" | 2.4.0 or later, below 3.0.0. |
- Only the release part of Kraft's version counts, so a release candidate of 2.4.0 satisfies
"2.4". - An add-on whose
requires.kraftdoes not hold is refused at validate, install and update, and left out at load. - A source build of Kraft, such as a checkout, skips the check.
validatesays so.
An add-on's files
Kraft reads these files of an add-on directory and nothing else. Each is optional, and an add-on with none of them is refused.
| Path | Holds |
|---|---|
library.yaml | The tasks, steps, nodes and steering sections, as in the local file. |
chains/<name>.yaml | One chain per file, as in config/chains/. The chain is named by its id, or by <name> when it has none. |
skills/<name>/SKILL.md | One method per directory. Kraft reads the SKILL.md and no file beside it. |
profiles.yaml | A profiles: section, in the shape of agent profiles. A harnesses key is refused. |
Each <name> is a name. A file that looks like part of the layout and is not, such as chains/Ship.yaml or library.yml, is not read, and validate warns about it. Any other file in the directory never reaches Kraft.
Size and content limits
An add-on over one of these limits is refused, and the message names the file.
| Limit | Value |
|---|---|
| Each YAML file | 1 MiB, and 20,000 YAML nodes with each use of an alias counted |
| Each JSON manifest | 256 KiB |
Each SKILL.md | 256 KiB |
| Files in one add-on | 500 |
| One add-on in all | 20 MiB |
Every file is a regular file holding UTF-8 text. These are refused:
- a symlink or a git submodule where a layout file or folder would be;
- a Git LFS pointer in place of the file;
- a file or a manifest string that carries a control character other than newline and tab, or a Unicode format character such as a bidi control or a zero-width character;
- in a directory collection, a layout file with a second hard link.
What an add-on may set
A policy: block in an add-on, at any level, takes only these keys:
| Key | Limits |
|---|---|
time_cap_minutes, total_time_cap_minutes, timeout_minutes | How long. |
budget_usd, token_budget | How much. |
max_attempts | How many times. |
deny_tools | Which tools the permission gate denies. |
policy.yaml defines each key. A limit above the instance's maxima: is refused.
An add-on is refused, with the file and key named, when it holds any of these:
- a task of
kind: subprocess; - any other
policy:key, such asallowed_tools,grants,sandboxorallowed_harnesses; - a
harness:thatrequires.harnessesdoes not list, in a task or in an agent profile'sfallback; - a
harnessessection inprofiles.yaml; - a qualified
extends,steeringorprofilethat names anything but the add-on itself, an agent profile'sfallbackincluded; - a file nested deeper than its parser reads;
- a
skillthat names another installed add-on.
builtin and forge tasks are allowed, mr.merge included.
Names
A name is a lowercase letter, then lowercase letters, digits, _ or -. A collection name, an add-on name and an alias are at most 64 characters.
- An add-on id is
<addon>@<collection>, such asrelease@acme. - A namespace is the add-on's name, or the alias it was installed under with
--as. - A qualified name is
<namespace>:<name>. Everything an add-on declares is addressed that way from outside it.
| What | Qualified reference |
|---|---|
| A chain | --chain release:ship, default_chain: release:ship |
| A task, step or node | extends: release:implementer |
| A steering profile | steering: [release:house-style] |
| A skill | skill: release:release-notes |
| An agent profile | profile: release:careful |
Inside an add-on's own files, a reference resolves like this:
| Key | A bare name means | Qualified |
|---|---|---|
extends, steering, skill | The add-on's own declaration. | The add-on's own name only. A skill may also name kraft: or another tool's plugin. |
profile | The instance's agent profile. | The add-on's own name, for a profile in its profiles.yaml. |
harness | The instance's harness profile. | Never qualified. |
A reference an add-on makes to its own name follows the alias, so an add-on installed under another namespace works unchanged.
These rules keep namespaces apart:
kraftis reserved. No add-on is namedkraftand no alias iskraft.- A namespace is unique on the instance, a disabled add-on's included. A second add-on of the same name needs an alias.
- A namespace never equals the
idof an entry inrepos.yaml. - A local chain id,
library.yamlkey orharnesses.yamlagent profile id never contains:. - A bare name in local configuration never resolves to an add-on's declaration.
A skill: whose qualifier is not a loaded add-on's namespace or kraft is handed to the agent as another tool's plugin skill. A qualifier that is an installed add-on that did not load is an error.
addons.yaml
collections:
acme:
git: https://github.com/acme/kraft-addons.git
ref: main
local:
path: /home/you/src/my-addons
addons:
release@acme: true
review-methods@acme: { auto_update: true }
mobile@acme: { as: acme-mobile }
legacy@acme: false
tools@local: true
Kraft never seeds this file: kraft admin addon collection add creates it. You may edit it by hand. A key Kraft does not read is refused at every level, with the key named.
collections
Each key is the name the collection's collection.json declares.
| Field | Type | Default | Meaning |
|---|---|---|---|
git | string | one of git or path | A URL git clones. See Collection sources. |
path | string | one of git or path | An absolute directory in normal form, read in place. ~ is not expanded. |
ref | string | the remote's default branch | A branch, tag or commit of a git collection. A value YAML reads as a number needs quotes. Not allowed with path. |
auto_update | boolean | false | Every add-on installed from the collection updates itself. true is refused with path. |
addons
Each key is an add-on id whose collection is under collections. The value is true, false or a map. true and false are short for {enabled: true} and {enabled: false}.
| Field | Type | Default | Meaning |
|---|---|---|---|
as | name | the add-on's name | The namespace the add-on is installed under. |
enabled | boolean | true | false keeps the add-on installed and not loaded. |
auto_update | boolean | unset | This add-on updates itself. Refused when its collection sets auto_update: true, and for an add-on of a directory collection. |
Collection sources
kraft admin addon collection add SOURCE writes one of these:
SOURCE | Written as |
|---|---|
| A directory that exists | path:, as its absolute path. |
owner/repo | git: https://github.com/owner/repo.git |
| Anything else | git:, as given. |
git: takes these forms and no other:
https://host/path, with no credentials in it;ssh://host/pathorssh://user@host/path;file:///path;user@host:path.
http://, git:// and a bare owner/repo are refused in the file. A collection already added from another source is refused until it is removed.
addons.lock
Only kraft admin addon writes the lock. Each installed add-on has one entry:
# Written by `kraft admin addon`. Do not edit.
lock_version: 1
addons:
release@acme:
namespace: release
ref: main
git: https://github.com/acme/kraft-addons.git
commit: f4a11422cc8377a6ae61353897431dafa72b8ef5
tree: de1b745c4020ff2d468c14451da2f28a5fb56207
source: ./addons/release
digest: sha256:f915f6266d319e9c0bc2bb2483683d74f307839138258901919e7d071fd37da4
version: 1.0.0
updated_at: '2026-10-10T13:23:58.690123Z'
| Field | Meaning |
|---|---|
namespace | The namespace the add-on was reviewed and installed under. |
ref | The collection's ref when it was locked. null for the default branch and for a directory collection. |
git | The collection's git URL when it was locked. null for a directory collection. |
commit | The collection's commit. null for a directory collection. |
tree | The git tree of the add-on's directory in that commit. null for a directory collection. |
source | The add-on's source in collection.json. |
digest | The digest of the add-on's files. |
version | The add-on's version. |
updated_at | When the entry was written. |
What loads
An add-on loads when it has an enabled entry in addons.yaml and an entry in the lock. Kraft reads it from the store the lock names and never fetches a collection to load the library.
| State | What Kraft does |
|---|---|
| Listed, with no lock entry | Not loaded: is not installed; run `kraft admin addon install` . |
| Disabled | Not loaded. It keeps its namespace. |
| Locked, and not listed | Not loaded. The next accepted install or update, or an uninstall, drops the lock entry. |
The alias, or the collection's ref or git URL, differs from the lock | Loaded under the locked namespace and commit, until kraft admin addon update applies the change after a review. |
An add-on that fails one of these checks is left out by itself, and the rest of the library loads:
- its store is missing and not restored yet, or could not be restored;
- its files do not match the locked digest;
- its
requires.kraftdoes not hold for the running Kraft; harnesses.yamlno longer defines a harness or agent profile it requires;- its namespace is now a repository id;
- one of its own files no longer loads;
- one of its limits is above a
maxima:lowered since it was installed.
GET /api/health names each one under invalid_templates as add-on <id> with the reason, and answers degraded. A chain of yours that extends a left-out add-on stops resolving. The add-on is still checked for updates.
An addons.yaml or addons.lock that does not read loads no add-on at server start. On a reload the server keeps the add-ons it has. Either way invalid_templates names addons.yaml with the reason.
Saving a policy.yaml, harnesses.yaml or repos.yaml that would leave a loaded add-on out is reported before it is applied: would leave add-on release@acme out: ....
Install and update
An install or update fetches the collection, checks the add-on, checks the whole library with the add-on in it, and prints a review. It writes addons.yaml, the lock and the store only after the review is accepted. The fetch itself fills the collection's mirror under run/addons/mirrors/ before the review.
When an add-on has an update
| Collection | An update is waiting when |
|---|---|
| git | The add-on's version differs from the lock's, higher or lower, or the collection's ref or git URL, or the add-on's alias, was changed by hand. A new commit at the same version is not an update. |
| directory | The add-on's digest differs from the lock's, whatever its version, or its alias was changed by hand. |
kraft admin addon install ADDON@COLLECTION --re-installtakes the commit therefnames now, at an unchanged version too.kraft admin addon updatenotes files that changed at an unchanged version:version 1.0.0 unchanged, but its files changed at <commit>; --re-install to take it.- An add-on its collection no longer lists stays on its locked version.
What refuses an update
A candidate is refused, and nothing is written, when:
- it breaks a rule under An add-on's files;
- its namespace is another add-on's, or a repository id;
- the instance lacks a harness or agent profile it requires;
- a chain it ships does not resolve;
- a chain of yours that resolves now would stop resolving;
- a
default_chainorsteeringinrepos.yaml, achaininintake.yamlor apolicy.yamltrigger, or an agent profilefallbackinharnesses.yamlwould stop resolving.
One add-on change runs at a time. A second one waits 5 seconds, then fails with another add-on change is running; try again. An update writes nothing when addons.yaml or addons.lock changed while its review was open: add-ons changed while you reviewed; run update again. Each message and its fix: Add-on problems.
The review
The review compares the candidate with the locked version, or with nothing on an install. It prints two lists, with every control and format character written out as \u{...}.
Reach is what can change how far a run goes:
- a gate removed, or a gate's
auto_reviewadded or changed; - a change to
chain_finalized,skippable,reject_toorartifact_required; - an
mr.mergeormr.mark_readystep, with whether a gate comes before it; - a forge target the chain did not use before, or a forge step removed;
- a task's
harness,model,effort,profileorfallbackchanged, afallback: []removed among them; - an agent profile in
profiles.yamlchanged; - a node or step no longer
read_only, or a task'sscopechanged; - a limit set, raised or removed, a fix loop's
max_attemptsamong them, or adeny_toolsentry dropped; - the collection's
gitURL changed; - the order of a chain's nodes, or of a node's steps, changed;
- the installed version could not be read to compare with;
requireschanged;- a lower version, or a move from a release onto a pre-release;
- each reference in your own files that the add-on's namespace now captures.
Content is the rest: components added, removed or changed, each new or changed prompt and steering text, each agent profile added or removed, each skill as a diff, a lowered limit, and each new reference to another tool's plugin skill.
Both lists also cover each chain of yours that resolves through the add-on.
Auto-update
An add-on auto-updates when its collection sets auto_update: true, or its own entry does. The server runs it in the background after each start, and again once a day while it runs.
An auto-update applies an update only when its review has no Reach line. Anything else is held for a person, and the locked version keeps serving. It never does these:
- install an add-on that has no lock entry;
- take a newer commit at an unchanged version;
- apply an alias, a
refor a collectiongitURL changed by hand.
Kraft does not fetch a collection none of whose installed add-ons auto-update, except to restore a missing store.
Each add-on's last outcome is kept in run/addons/status.json and shown by addon_updates in GET /api/health, as outcome and kind only: message stays in the file, because that endpoint needs no login. The add-on updates row of kraft admin doctor names the held and failed ones:
outcome | Meaning |
|---|---|
applied | The update was taken. |
up to date | Nothing to take. |
held | A person must run kraft admin addon update. message, in the file and in doctor, says why. |
failed | kind is network, auth, refused or busy, and message, in the file and in doctor, says why. |
A held or failed auto-update does not make health degraded. An entry is dropped once you apply the add-on's update yourself or uninstall the add-on.
What a work item pins
A work item's chain is copied at intake, with every prompt and steering text in it. Two things are read later, at each launch: skills and agent profiles. For each add-on the chain resolved through, the item records the add-on's id, commit, tree, source, digest and version, and reads that add-on's skills and agent profiles from that version until the item ends.
- An update or an uninstall does not change a running item.
- A chain revision resolves against the pinned versions.
- The
chain_loadedevent carries the pins.
Restoring a store
A store named by the lock or by a work item's pin can be missing: after config/ is copied to another machine, after run/ is cleared, or after a newer lock is put in config/. Kraft restores it:
- in the background after the server starts, and with each daily auto-update check;
- in the background after a reload;
- before a launch that reads a version its work item pinned.
A restore takes the locked commit from the local mirror, or fetches that commit from the collection's URL. A directory collection is copied again. The files are kept only if their digest is the locked one. A restore is not a review.
Until its store is back, an add-on is left out. Filing an item on one of its chains answers 503 with add-on <id> is being restored; retry. A work item whose pinned store cannot be restored stops.
The digest
The digest is sha256: and the SHA-256 of a manifest with one line per file the add-on holds, sorted by path. Each line is the path, the git mode (100644 or 100755) and the SHA-256 of the file's bytes, separated by NUL bytes and ended by a newline.
The same files give the same digest on every machine. kraft admin addon validate prints it, and the store keeps the manifest beside the files as .kraft-digest.
On disk
Add-on state lives under $KRAFT_HOME/run/addons/:
| Path | Holds |
|---|---|
mirrors/<collection>-<hash>.git | One bare git mirror per git collection. Kraft never checks out a working tree from it and never runs a hook. |
store/<digest>/ | One add-on's files, read-only, shared by every install with the same content. |
staging/ | A store being written. |
status.json | Each add-on's last auto-update. |
The server deletes a store once the lock, the loaded library and every work item that has not ended no longer name it. kraft admin addon never deletes one.
Related
- Share chains with add-ons: publish a collection, install from one, and keep it current.
- Admin verbs: every
kraft admin addonverb and flag. - Security: what an add-on can and cannot change on an instance.
- Doctor failures: the
add-onsandadd-on updatesrows. - Add-on problems: the messages
kraft admin addonprints when it refuses a collection or an add-on, and whatvalidaterejects, with the fix for each.
Environment variables
Every KRAFT_* variable Kraft reads or sets, the other variables it reads, and the variables a worker's environment gets.
Repo libraries
A connected repository's .kraft/ folder as a library: its layout, names, where Kraft reads it from, how a person accepts it, automatic updates, the default chain, and repo-libraries.lock.