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
SignedReleasemetadata: 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_pulladapter update is verified against the adapter author’s key from its manifest; adelegatedupdate 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:
| exit | meaning |
|---|---|
0 | every leg succeeded (an already-current core counts as success) |
3 | a leg refused with nothing done — a guard declined and the box is exactly as it was — and no leg failed |
| other nonzero | at 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/updatereturns, 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].messagestill 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.