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.

Update completion messages and consent notifications point to this node’s locally hosted changelog.html. When documentation lands, UPDATE_DOCS_LANDED points to the local index.html. Both links use the running listener’s discovered port, not a guessed default or configured port. If live discovery is unavailable, the changelog retains the release-channel link and UPDATE_DOCS_LANDED retains the installed directory path.

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). Before downloading, fetch performs a bounded read of the selected release repository through the same gh carrier and inherited environment used for the download. GitHub CLI’s effective credential precedence applies (GH_TOKEN before stored credentials on github.com); a stale stored login does not veto a working effective token. A missing CLI refuses with GhCliRequired and the OS install command. A read that times out refuses with GhProbeTimeout, not an authentication verdict. Other failed reads refuse with GhAuthRequired: check credentials, repository access, and connectivity. When gh auth status explicitly reports a failed host/account, the refusal names that account as diagnostic evidence, not as proof that it caused the read failure. Raw authentication output and token bytes are never included. The read and optional diagnostics share one ten-second preflight budget; spt does not switch or delete credentials. Signature verification is unchanged and carrier-independent — downloaded metadata and artifact bytes still pass the existing verification gates. 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.