kton-cockpit

Three verbs, and a record somebody else can check.

A command-line tool for doing work in a kton federation and leaving evidence of it. You name what went in, what came out, and the command you ran. It signs, commits, pushes, pins the permalinks, and answers questions about the graph afterwards — re-verifying every record against keys you configured, never trusting one because it turned up in the registry.

cockpit publish record a result cockpit say bind a claim cockpit ask query the graph

A tour, end to end

Every command below was run against a participant repository built from nothing, and every answer is what came back. Nothing here is illustrative.

Is this repository set up, and bound to what it says?

The first thing anyone runs. It reads the configuration, checks the origin remote matches what the config claims, and names the kernel compiled into the binary. The working directory is shortened here for width.

$cockpit doctor
repo root:      ~/sensor-repo
bound to:       cockpit-examples/cockpit-tour (verified against origin remote)
plankton_dir:   ~/sensor-repo/registry/plankton  [ok]
nekton_dir:     ~/sensor-repo/registry/nekton  [ok]
templates_dir:  ~/sensor-repo/templates  [ok]
plankton key:   ~/sensor-repo/keys/session-1.key  [ok]
nekton key:     ~/sensor-repo/keys/session-1-claims.key  [ok]
allowed templates: [reproduces working-on]
claim scopes:   none configured — every claim stands on its own
trust tiers:    map[self:[registry/keys/session-1.pub registry/keys/session-1-claims.pub]]
kernel:         linked, not invoked — no plankton/nekton process is ever started
                kton.dev/kton        replaced by ../kton-pinned/kton/reference (devel)
                kton.dev/nekton      replaced by ../kton-pinned/nekton/reference (devel)
                kton.dev/plankton    replaced by ../kton-pinned/reference (devel)
git:            commit + push
anchor:         off — records are signed, but nothing independent attests WHEN they existed
material:       none configured — records carry the signature and nothing about whose key it is
union:          not published — the graph is reachable via `cockpit show` only
execution:      not configured — cockpit_publish RECORDS the command, does not run it

Record a result.

Three things in — the command, what it read, what it wrote — and everything else out: the foton's id, the hash of every output, the commit that holds them, and a permalink pinned to that commit. The caller did not commit, push, build a permalink, choose a key or sign. Those are the things it cannot get subtly wrong, because it never touches them.

$cockpit publish '{"cmd":"mean data/sensor.csv > work/summary.csv","inputs":["data/sensor.csv"],"outputs":["work/summary.csv"]}'
{
  "fotonId": "sha256:36ba044073eb562706ca32a5bfb2e8e68bbd7b56da2b95ade1525c1dd19e22a9",
  "outputHashes": {
    "work/summary.csv": "sha256:fc9756d8300c0b6de3bf54fd0b6c8bede8298a8f8a4585acc3f03aa24450bfcb"
  },
  "revision": "6af50db11e5b5dc1e0c6084c1068b08ef29bbd5e",
  "locators": {
    "data/sensor.csv": "https://raw.githubusercontent.com/cockpit-examples/cockpit-tour/6af50db11e5b5dc1e0c6084c1068b08ef29bbd5e/data/sensor.csv",
    "work/summary.csv": "https://raw.githubusercontent.com/cockpit-examples/cockpit-tour/6af50db11e5b5dc1e0c6084c1068b08ef29bbd5e/work/summary.csv"
  },
  "persisted": true,
  "shared": true
}

Ask what produced a result.

By the hash of the bytes, not by a filename. verified: true and tier: self are the answer's own work: every record was re-verified against this repository's configured keys before being included, and never trusted from the keyid it declares about itself.

$cockpit ask '{"query":"producer","ref":"sha256:fc9756d8300c0b6de3bf54fd0b6c8bede8298a8f8a4585acc3f03aa24450bfcb"}'
{
  "query": "producer",
  "ref": "sha256:fc9756d8300c0b6de3bf54fd0b6c8bede8298a8f8a4585acc3f03aa24450bfcb",
  "raw": "sha256:36ba044073eb562706ca32a5bfb2e8e68bbd7b56da2b95ade1525c1dd19e22a9  kind=script  in=1 out=1",
  "records": [
    {
      "id": "sha256:36ba044073eb562706ca32a5bfb2e8e68bbd7b56da2b95ade1525c1dd19e22a9",
      "tier": "self",
      "verified": true
    }
  ],
  "included": [
    "sha256:36ba044073eb562706ca32a5bfb2e8e68bbd7b56da2b95ade1525c1dd19e22a9"
  ],
  "fotons": [
    {
      "id": "sha256:36ba044073eb562706ca32a5bfb2e8e68bbd7b56da2b95ade1525c1dd19e22a9",
      "kind": "script",
      "inputs": 1,
      "outputs": 1
    }
  ],
  "filterApplied": "none — every record verified against a configured trust tier is included; unverified records are always excluded"
}

Say something about it.

From a template this repository allows, and nothing else — so what can be said is decided by files an operator put in templates/. The answer confirms registration by reading the claim back: an id coming back means it was signed, not that the store kept it.

$cockpit say '{"subject":"sha256:36ba044073eb562706ca32a5bfb2e8e68bbd7b56da2b95ade1525c1dd19e22a9","template":"working-on","fields":{"step":"calibration check","by-session":"session-1"}}'
{
  "claimId": "sha256:7868403e379e335b0015a17f44a4332d591bd960d6e076d94cc2dbb5e3888a3e",
  "confirmation": "registered: nekton reports sha256:7868403e379e335b0015a17f44a4332d591bd960d6e076d94cc2dbb5e3888a3e among the 1 claim(s) about sha256:36ba044073eb562706ca32a5bfb2e8e68bbd7b56da2b95ade1525c1dd19e22a9"
}

Ask what is said about it.

The claim comes back with its object — what was actually said — and the key that signed it, not merely the fact that a claim exists.

$cockpit ask '{"query":"about","ref":"sha256:36ba044073eb562706ca32a5bfb2e8e68bbd7b56da2b95ade1525c1dd19e22a9"}'
{
  "query": "about",
  "ref": "sha256:36ba044073eb562706ca32a5bfb2e8e68bbd7b56da2b95ade1525c1dd19e22a9",
  "raw": "sha256:7868403e379e335b0015a17f44a4332d591bd960d6e076d94cc2dbb5e3888a3e  predicate=https://kton.dev/v/working-on  object={\"by-session\":\"session-1\",\"step\":\"calibration check\"}  when=2026-09-16T13:29:19Z  declared-by=key:1739591e4a97fc36",
  "records": [
    {
      "id": "sha256:7868403e379e335b0015a17f44a4332d591bd960d6e076d94cc2dbb5e3888a3e",
      "tier": "self",
      "verified": true
    }
  ],
  "included": [
    "sha256:7868403e379e335b0015a17f44a4332d591bd960d6e076d94cc2dbb5e3888a3e"
  ],
  "claims": [
    {
      "id": "sha256:7868403e379e335b0015a17f44a4332d591bd960d6e076d94cc2dbb5e3888a3e",
      "subject": "sha256:36ba044073eb562706ca32a5bfb2e8e68bbd7b56da2b95ade1525c1dd19e22a9",
      "predicateType": "https://kton.dev/claim/v0",
      "predicate": "https://kton.dev/v/working-on",
      "object": {
        "by-session": "session-1",
        "step": "calibration check"
      },
      "declaredBy": "key:1739591e4a97fc36",
      "when": "2026-09-16T13:29:19Z",
      "signatureKeyIds": [
        "1739591e4a97fc36"
      ]
    }
… 3 more lines

Get an argument wrong.

output where the verb takes outputs. Over MCP a tool schema catches that; at a shell nothing else would, and the record would have been published naming no outputs at all — signed, valid, and wrong. Exit 1 means the call itself was wrong.

$cockpit publish '{"cmd":"mean data/sensor.csv > work/summary.csv","inputs":["data/sensor.csv"],"output":["work/summary.csv"]}'
cockpit: the argument is not valid JSON for this verb: json: unknown field "output"
exit 1

Point it at the wrong repository.

The guard this project exists for. A live demo once had six registries under one parent directory and a session cooperated against the wrong one. Nothing failed — the records were signed, valid, and in the wrong project. Exit 2 means the cockpit understood, and declined.

$cockpit publish '{"cmd":"mean data/sensor.csv > work/summary.csv","inputs":["data/sensor.csv"],"outputs":["work/summary.csv"]}'
cockpit: refusing to act — repo/remote mismatch: cockpit.config.json says someone-else/cockpit-tour, but this repo's origin is cockpit-examples/cockpit-tour — refusing to act against the wrong repo
exit 2

Twelve examples, one property each

Each builds a complete participant repository from nothing, exercises one property, and prints what it saw. The lines under each card are its real assertions. A check that only looked for the absence of an error would pass when a query returned nothing, which is how a suite reports success having verified nothing — so these count what they found.

12examples
83assertions
6negative controls
7need nothing installed
▪ok: something was true ▫refused, as it must be: a call that had to fail, did –ok: something was absent, and that was the claim
01

publish

One call records a signed foton — and the point is everything the caller did not do.

no prerequisites
$cockpit publish '{ "cmd": "sum data/in.csv > work/sum.csv", "inputs": ["data/in.csv"], "outputs": ["work/sum.csv"] }'
$cockpit ask '{"query":"producer","ref":"'
$cockpit ask '{"query": "producer", "ref": "$HASH"}'
  • ▪ok: the output is committed
  • ▪ok: everything was pushed
  • ▪ok: the reported sha IS the repo's final state
  • ▪ok: and the returned permalink pins it
  • ▪ok: the foton's locator carries a real commit sha
  • ▪ok: which is NOT the one publish returned
  • ▪ok: and it is that sha's parent
  • ▪ok: both name the same file
  • ▪ok: producer found the foton and it verified into a configured tier
  • ▪ok: it is the foton publish reported
02

chain

Two steps join because they share a hash, not because anyone said they follow.

no prerequisites
$cockpit publish '{"cmd":"clean data/raw.csv > work/clean.csv","inputs":["data/raw.csv"],"outputs":["work/clean.csv"]}'
$cockpit publish '{"cmd":"sum work/clean.csv > work/total.csv","inputs":["work/clean.csv"],"outputs":["work/total.csv"]}'
$cockpit ask '{"query":"lineage","ref":"$TOTAL_HASH"}'
  • ▪ok: lineage walks back through both steps
  • ▪ok: and reaches step one
  • ▪ok: from step two
  • ▪ok: a record sharing no hash has a lineage of only itself
03

anti-wrong-folder

The guard, and the reason this project exists. A session once cooperated against the wrong registry and nothing failed.

no prerequisites
$cockpit publish '{"cmd":"echo x > work/out.csv","inputs":[],"outputs":["work/out.csv"]}'
$cockpit ask '{"query":"producer","ref":"sha256:0000000000000000000000000000000000000000000000000000000000000000"}'
  • ▫refused, as it must be: acting on this repo with a config that names someone-else/…
  • ▫refused, as it must be: acting in a directory that is no repository
  • ▫refused, as it must be: a config sitting somewhere other than the root it binds
  • ▪ok: the same call succeeds in the repo the config is bound to
04

claim-ceiling

What may be said, and what may not be decided. A claim comes from a fixed set of templates, and for reproduces the level is measured rather than stated.

no prerequisites
$cockpit publish '{"cmd":"compute > work/out.csv","inputs":[],"outputs":["work/out.csv"]}'
$cockpit say '{"subject":"$FOTON","template":"working-on","fields":{"step":"analysis","by-session":"session-1"}}'
$cockpit ask '{"query":"about","ref":"$FOTON"}'
  • ▪ok: the claim was recorded
  • ▪ok: and reading it back gives what it SAYS, not just that it exists
  • ▫refused, as it must be: a claim shape this repo does not allow
  • ▪ok: identical bytes were assessed as L0
  • ▫refused, as it must be: a reproduction claim over bytes that do not match
  • ▪ok: still exactly the two claims that were legitimately made
05

trust-tiers

Verified, not declared. A stranger's record goes into the same registry and is still excluded — then the key is configured into a tier and the same record is included. The record did not change. The configuration did.

no prerequisites
$cockpit publish '{"cmd":"produce work/ours.csv","inputs":[],"outputs":["work/ours.csv"]}'
$cockpit ask '{"query":"producer","ref":"$OURS"}'
$cockpit ask '{"query":"producer","ref":"$THEIRS"}'
  • ▪ok: our record is included
  • ▪ok: the stranger's record is NOT included
  • ▪ok: but it IS reported as found and excluded
  • –ok: and the record itself is not in the answer's included set
  • ▪ok: though the answer does account for having found it
  • ▪ok: the same record is now included
  • ▪ok: asking only for tier 'self' excludes the federation record
06

reproduce

Reproducing does not add a record. It adds a signature to the one that is already there.

no prerequisites
$cockpit publish '{"cmd":"sum data/in.csv > work/out.csv","inputs":["data/in.csv"],"outputs":["work/out.csv"]}'
$cockpit ask '{"query":"reproductions","ref":"$HASH"}'
  • ▪ok: the same work is the SAME foton, not a second one
  • ▪ok: and the registry did not grow
  • ▪ok: different bytes are a different foton
  • ▪ok: and that one IS a new record
  • ▪ok: counted by verified signature, not by self-declared keyid
07

run-in-container

The environment pin stops being a claim: the cockpit runs the command itself, so the string handed to the engine and the string recorded are one string.

needs docker
$cockpit publish '{ "cmd": "awk -F,'
$cockpit publish '{ "cmd": "cp data/in.csv work/declared.csv; cp data/in.csv work/forgotten.csv", "inputs": ["data/in.csv"], "outputs": ["work/declared.csv"] }'
$cockpit publish '{"cmd":"exit 3","inputs":[],"outputs":["work/never.csv"]}'
  • ▪ok: the image has a registry-resolvable digest
  • ▪ok: the container produced the output
  • ▪ok: it reports which image it ran in
  • ▪ok: and the foton pins that same string
  • ▪ok: the file nobody declared is named
  • –ok: and it did NOT become an output of the foton
  • ▫refused, as it must be: a command that exits non-zero
  • ▪ok: and it left no commit behind
08

union-and-show

The graph, published and served, without parsing a registry file. keys.json comes from the configured tiers, so a viewer re-verifies against exactly what a query does.

no prerequisites
$cockpit publish '{"cmd":"compute > work/out.csv","inputs":[],"outputs":["work/out.csv"]}'
$cockpit say '{"subject":"$FOTON","template":"working-on","fields":{"step":"analysis","by-session":"session-1"}}'
  • ▪ok: docs/data/union.json is tracked
  • ▪ok: it holds the foton
  • ▪ok: and the claim
  • ▪ok: the foton that was just published
  • ▪ok: and the claim that was just made
  • ▪ok: the served union carries both records
  • ▪ok: in the shape a viewer reads
  • ▪ok: keys.json holds the two CONFIGURED keys, not whatever is in the registry
  • ▪ok: and every signature resolves in it
09

carried-evidence

A record says which key signed it, not whose key that is. Evidence about a record is attached whatever its scheme, and reported as verified, carried, or failed.

needs opensslbuilds on 05
$cockpit ask '{"query":"producer","ref":"$1"}'
$cockpit publish '{"cmd":"cp data/in.csv work/out.csv","inputs":["data/in.csv"],"outputs":["work/out.csv"]}'
$cockpit publish '{"cmd":"cp data/in2.csv work/out2.csv","inputs":["data/in2.csv"],"outputs":["work/out2.csv"]}'
  • ▪ok: the certificate is reported verified
  • ▪ok: the unlisted scheme was attached, not rejected
  • –ok: and it is not counted as verified
  • ▪ok: a certificate belonging to another key is reported failed
  • ▪ok: the same certificate is carried, not verified, with no root configured
10

cleaning-is-an-argument

Two analyses of the same open data, one line apart, both honest, disagreeing by centuries. Nothing here is a lie detector — the script is an input, by hash, so “which did you run” is a lookup instead of a question.

needs Rneeds network
$cockpit publish '{"cmd":"Rscript R/oldest-districts.R", "inputs":["data/trees.csv","R/oldest-districts.R"], "outputs":["work/oldest.csv"]}'
$cockpit publish '{"cmd":"Rscript R/oldest-districts-cleaned.R", "inputs":["data/trees.csv","R/oldest-districts-cleaned.R"], "outputs":["work/oldest.csv"]}'
$cockpit ask '{"query":"uses","ref":"$1"}'
  • ▪ok: the register downloaded
  • ▪ok: the two rankings are different bytes
  • ▪ok: both runs consumed the same register
  • ▪ok: the as-written script was used by exactly one
  • ▪ok: the cleaned script was used by exactly one
  • ▪ok: each ranking names one producing run
  • ▪ok: and they are different runs
  • –ok: an unrecorded result has no producer
11

environment-reconstructible

A pin identifies; a derivation reconstructs. The image is built from a Nix expression, the expression is an input to the record, and the digest follows from it.

needs nixneeds dockerneeds networkbuilds on 07
$cockpit publish '{"cmd":"Rscript R/summarise.R", "inputs":["R/summarise.R"], "outputs":["work/mpg-by-cyl.csv"]}'
$cockpit publish '{"cmd":"Rscript R/summarise.R", "inputs":["R/summarise.R","nix/env.nix"], "outputs":["work/mpg-by-cyl.csv"]}'
$cockpit ask '{"query":"producer","ref":"$U_OUT"}'
  • ▪ok: nix produced an OCI image
  • ▪ok: a local registry answers
  • ▪ok: the registry assigned a digest
  • ▪ok: both runs produced the same bytes
  • ▪ok: and they are two different fotons
  • –ok: the unpinned run names no environment
  • ▪ok: the pinned run names the exact image
  • ▪ok: the result names both runs that produced it
12

the-environment-written-in-r

Nobody writes Nix. An R user who has to learn a second language to pin their environment will not pin it — so the environment is declared in R, and the digest follows from that file.

needs nixneeds dockerneeds networkbuilds on 11
$cockpit publish '{"cmd":"Rscript R/summarise.R", "inputs":["R/env.R","nix/default.nix","nix/image.nix","R/summarise.R"], "outputs":["work/mpg-by-cyl.csv"]}'
$cockpit ask '{"query":"uses","ref":"$ENVHASH"}'
$cockpit ask '{"query":"uses","ref":"$GENHASH"}'
  • ▪ok: rix generated nix/default.nix
  • ▪ok: nix produced an OCI image
  • ▪ok: regenerating from R/env.R rebuilds the same image
  • ▪ok: a local registry answers
  • ▪ok: the registry assigned a digest
  • ▪ok: the record pins the image by digest
  • –ok: and not by tag
  • ▪ok: the environment declaration leads back to the work that ran in it
  • ▪ok: so does the expression rix generated from it