Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Self-update

spt-core keeps itself current without ever interrupting your agents, and without trusting anything unsigned.

The invariant

No endpoint process terminates or suspends during a self-update. The daemon’s broker (holding PTYs, child processes, sockets) stays up; the brain (all logic) swaps under it. A hosted session’s process id and byte stream are identical before and after.

The trust chain

  • Every release ships SignedRelease metadata: an Ed25519 signature over the release’s artifact digests.
  • Every binary embeds the two-key trusted set — an active primary and a never-used offline recovery key. Verification requires a valid signature from a trusted key and a matching artifact digest; an unverified binary never reaches the apply step.
  • Losing the primary key is a non-event: the next release is signed with the recovery key (already trusted by every deployed binary) and rotates in a fresh primary.
  • Adapters sign their own content. A file_pull adapter update is verified against the adapter author’s key from its manifest; a delegated update is trusted only when the manifest attests the delegated updater verifies its own content (self_verifies). spt-core’s release keys never vouch for adapter bytes.

One command: spt update

Bare spt update is the primary form (since v0.32.0): it fetches and installs the latest signed core release, then updates every release-shipped adapter — the whole node current in one command. When the core is already current, only the adapters update. The invoking session survives a bare spt update by construction: installing cycles only the daemon’s coordinator process, never the hosted terminals — so it is safe to run from inside an spt-hosted session. --core-only (-c) skips the adapters leg.

spt update --restart is the full-cycle form: fetch, update adapters, then finish by restarting the daemon onto the new version as the final step — after it, the whole node (coordinator and every live agent) runs the new version. The finish restart bounces hosted sessions (they come back automatically) — that consequence is why it is opt-in rather than the default, and why it runs last: everything else has already completed, from any invoking context, before the restart lands.

Missing web routes after an in-place update. If the installed release is 0.68.0 or newer but the resident network layer reports a version below 0.68.0, spt node status and a successful in-place update both explain that node-prefixed docs URLs and serve controls are unavailable. Loading those routes requires a full daemon restart, which stops hosted sessions. spt node refresh only refreshes the coordinator; it does not replace the network layer. Ordinary compatible version skew does not need this notice, and an unknown resident version is not grounds for prescribing a restart. The update-completion check uses the signed release just applied, not the old updater’s own compiled version; a failed or unanswered diagnostic query does not turn a successful update into a restart instruction.

What the composite’s exit code means. spt update runs several legs, and its exit is the worst leg outcome by precedence — never simply the last one that failed:

exitmeaning
0every leg succeeded (an already-current core counts as success)
3a leg refused with nothing done — a guard declined and the box is exactly as it was — and no leg failed
other nonzeroat least one leg failed; the code is the first failure’s

A failure always outranks a refusal, whichever order they happen in. That matters under --restart, the one form that runs a leg after the failure-isolated adapters leg: if adapters fail and the finish then refuses, the command exits with the failure, not the refusal. Scripts branching on these codes can therefore treat 3 as “nothing changed, act on the guard’s message” without a failure ever hiding behind it.

spt update adapters [<a>[,<b>…]] runs the adapters leg alone (an alias of spt adapter update, which also stays; both accept a comma-separated list). Names are validated before anything updates — a typo never leaves a half-updated set — one adapter’s failure never stops the rest, each adapter gets a summary line, and the exit is nonzero if any failed. A registered adapter without a release channel (a local dev registration) is skipped loudly, not failed.

How updates move

Peer-propagated: one node fetches a release; paired nodes offer/fetch staged releases from each other, each verifying independently before staging. Updating is consent-gated by default — a notification surfaces at your most-recently-active endpoint, and spt update apply is the explicit ack (it re-verifies the staged release before touching the live daemon). Full-auto is an explicit opt-in.

The lower-level verbs remain for surgical control: spt update fetch pulls the latest signed release from the origin and stages it, then spt update apply installs it. spt update fetch --apply does both in one step (and still installs when the latest was already staged). (--apply since v0.18.0)

Prerequisite: the GitHub CLI. The release channel is a private GitHub repository, and spt update fetch downloads releases through an authenticated gh (since v0.32.0). A node without gh installed is refused loud with the OS’s install command; gh installed but not logged in is refused with a pointer at gh auth login. Signature verification is unchanged and carrier-independent — bytes are verified after download exactly as before. See Installing for the gh setup steps.

After self-updating, spt-core ripple-updates registered adapters through each manifest’s declared [update] avenue — the same engine behind spt update adapters and the bare composite’s second leg.

Composite adapter updates — a delegated post-step (since v0.16.0)

An adapter can run a second, adapter-owned step after its primary update avenue resolves — under spt adapter update and under spt adapter add (install is the first update, so a fresh install runs it too; since v0.19.0). Declaring an optional [update.post] sub-table (command required; an attestation-only self_verifies flag) lets one lever both pull the adapter’s .spt (e.g. from gh_release) and run an in-harness sync (e.g. a plugin updater). The post-step:

  • runs unconditionally — even when the primary avenue was a no-op (its own idempotent check decides what changes);
  • runs foreground and bounded — a child of the CLI, 120 s timeout, never backgrounded or detached: when spt adapter add/update returns, the step has finished or failed;
  • receives a published JSON line on stdin describing the just-resolved update (adapter_applied, version, previous_version, adapter_dir, …; additive keys only — ignore unknown);
  • decides the post-update notice via stdout — custom text supersedes the static [update].message, the reserved sentinel !!update-message!! fires the static message, empty prints nothing;
  • is failure-isolated and loud — a nonzero exit / spawn failure / timeout prints ADAPTER_UPDATE_POST_FAIL:<adapter> (with the step’s stderr detail) on stderr and makes the CLI exit nonzero; the committed pull is never rolled back, and the static [update].message still fires when the adapter applied — so check the exit code, and don’t let a static message promise what the post-step may have failed to do.

The exact stdin keys, sentinel, notice precedence, timing, and the verify-then-notify pattern are in the manifest [update.post] reference.

What changed in a release

The Changelog is a page of these docs, so the answer to what does this update give me is on the machine you are updating — no repository checkout, no network. It is generated from the project’s CHANGELOG.md and drift-gated in CI, so a release note that exists anywhere exists here, verbatim.

Commands

spt update · the consent notification flow (spt notif) — CLI reference.