upgrade

package
v0.13.9 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 29, 2026 License: MIT Imports: 27 Imported by: 0

Documentation

Overview

Package upgrade is the update transaction (lifecycle plan R2): resolve the latest release, decide whether this install may act, download, verify, and STAGE -- never activate. Activation (rename + re-exec + converge) is R3's; everything here is safe to run on a machine doing live work.

Index

Constants

View Source
const DefaultBaseURL = "https://github.com/Nathandela/swarm"

DefaultBaseURL is the release host. A test substitutes its own httptest server; production never overrides it.

Variables

View Source
var ErrNothingStaged = errors.New("upgrade: nothing staged; run `swarm upgrade --stage` first")

ErrNothingStaged is Activate's answer when no verified build awaits.

View Source
var ErrNothingToRollBack = errors.New("upgrade: no rollback slots; nothing was ever activated from here")

ErrNothingToRollBack is Rollback's answer when no slots exist.

View Source
var ErrOffline = errors.New("upgrade: release host unreachable")

ErrOffline wraps every network failure of the resolve step, so callers can fold "no network at 04:00" into a quiet skip.

Functions

func ClearPendingConverge

func ClearPendingConverge(stateDir string) error

ClearPendingConverge marks the install converged. The caller clears ONLY on a converge that exited converged (or found no daemon to converge, which the next client start resolves onto the installed binary).

func HeldVersion

func HeldVersion(stateDir string) string

HeldVersion reads the hold; "" when none.

func LatestVersion

func LatestVersion(ctx context.Context, baseURL string) (string, error)

LatestVersion resolves the latest published tag from the /releases/latest HTTP redirect -- one HEAD, no API, and therefore no per-IP unauthenticated rate limit for a NAT'd fleet to exhaust (committee: Gemini #8, Fable LOW). The Location it follows ends .../releases/tag/<tag>.

func PendingConverge

func PendingConverge(stateDir string) string

PendingConverge reports the version whose install awaits a confirmed converge; "" when none.

func PrevDir

func PrevDir(stateDir string) string

PrevDir holds the rollback slots.

func StageDir

func StageDir(stateDir string) string

StageDir is where a verified build awaits activation.

func StatePath

func StatePath(stateDir string) string

StatePath is where the transaction records each run. cmd/swarm's doctor reads the same function, so reader and writer cannot drift on the location.

func TrustedKey

func TrustedKey(pubHex string) bool

TrustedKey reports whether pubHex is one of the trust slots -- the release pipeline's pre-publish selfcheck, so a valid-but-wrong secret fails the RELEASE instead of shipping assets every fleet machine rejects (R2/R3 audit: codex finding 7, Fable M5).

func VerifyChecksums

func VerifyChecksums(checksums, sig []byte) error

VerifyChecksums reports whether sig (the checksums.txt.sig release asset, base64 of a raw ed25519 signature) signs checksums over any trusted key.

Types

type ActivateOptions

type ActivateOptions struct {
	StateDir string
	BinPath  string // the installed binary to replace (os.Executable, unresolved)
	// Installed is the running binary's version (internal/version.Version): a
	// staged tag equal to it is an interrupted activation's leftover, answered
	// "current" instead of re-running the transaction against the already-
	// installed build -- which would rebuild the rollback slots from it and
	// destroy the true originals (R2/R3 audit, Fable M3b).
	Installed string
	// DaemonAlive reports whether a daemon currently holds the singleton lock.
	// The wire guard needs it: ZERO running sessions does not mean no daemon,
	// and a live old daemon's next launch under a wire-bumped install is the
	// pinned ProcessLost cell (R2/R3 audit, codex finding 2). nil is treated as
	// "assume alive" -- fail closed, never open.
	DaemonAlive func() bool
}

ActivateOptions configures activation.

type CompatManifest

type CompatManifest struct {
	Version  string `json:"version"`  // the release tag
	Shimwire int    `json:"shimwire"` // internal/shimwire.Version
	Protocol int    `json:"protocol"` // internal/protocol.Version
	Schema   int    `json:"schema"`   // internal/persist.SchemaVersion
}

CompatManifest is the release's compatibility card: the wire and schema constants the archived build was compiled with, emitted at build time from the SAME source constants (scripts/releasemanifest) and read at activation time from the STAGED archive. It exists so activation is a pure disk comparison -- the committee killed both alternatives: executing a staged binary to ask it (R4's probe, withdrawn), and installing first so converge could ask afterwards, which put a new binary under an old daemon and broke every launch on a wire bump (the compat matrix's own pinned ProcessLost cell). The manifest rides INSIDE the tarball as compat.json, so the signed checksum that covers the archive covers it too -- signed compatibility metadata, not a hidden verb (committee: codex finding 1).

func CurrentManifest

func CurrentManifest(tag string) CompatManifest

CurrentManifest is THIS build's card -- what the emitter writes at release, and what activation compares a staged card against for the axes that gate.

type Decision

type Decision struct {
	Action string // "stage" | "current" | "refuse"
	Reason string // human sentence; for refuse, names the owning delegate
	Latest string // the resolved tag, when resolution succeeded
	Owner  Owner
	// RefuseOutcome is the machine-readable refusal class ("refused-dev",
	// "refused-owner", "refused-downgrade"), set exactly where the refusal is
	// decided -- outcomes must never be re-derived from prose (Fable L4).
	RefuseOutcome string
}

Decision is Check's answer: what a run of Stage would do, and why.

func Check

func Check(ctx context.Context, opts Options) (Decision, error)

Check resolves the latest release and decides, WITHOUT downloading anything. The order is deliberate: the cheap local refusals (dev build, foreign owner) come before the network, so an air-gapped dev machine never even dials.

type Options

type Options struct {
	StateDir  string // the swarm state dir; staging, lock and upgrade.json live under it
	BinPath   string // the binary being upgraded (os.Executable, unresolved)
	Installed string // internal/version.Version of the running binary
	BaseURL   string // "" means DefaultBaseURL; tests point it at a fixture server
	// AllowDowngrade permits latest < installed, for the yanked-release day.
	// Never set by any unattended path (committee M-4: a re-pointed `latest`
	// must not silently downgrade a fleet).
	AllowDowngrade bool
}

Options configures one transaction run.

type Owner

type Owner string

Owner classifies who manages the binary at a path. The update transaction self-replaces ONLY OwnerSelf: every package manager keeps its own books, and a binary rewritten behind those books is the recorded 2026-08-27 brew incident and the dpkg/rpm corruption the committee named (C2/C5) -- the exact argument that produced brew deference produces deference for every owner.

const (
	OwnerSelf Owner = "self"    // tarball / install-script: ours to replace
	OwnerBrew Owner = "brew"    // Homebrew cask: `brew upgrade --cask swarm` owns it
	OwnerDpkg Owner = "dpkg"    // apt/dpkg package: apt owns it
	OwnerRpm  Owner = "rpm"     // rpm/dnf package: dnf owns it
	OwnerGo   Owner = "go"      // `go install` under GOPATH/bin: the user's toolchain owns it
	OwnerNone Owner = "unknown" // classification failed; treated as NOT self (refuse)
)

func ClassifyOwner

func ClassifyOwner(binPath string) Owner

ClassifyOwner answers for the RESOLVED path of the binary actually being upgraded -- never for what a package database merely remembers. The recorded brew incident is precisely both-present-and-diverged: a hand-copied /usr/local/bin/swarm beside a brew record, where `brew list --cask` succeeds and says nothing about the binary the daemon runs (committee H4, Sonnet #2).

type State

type State struct {
	CheckedAt time.Time `json:"checked_at"`
	Installed string    `json:"installed"`
	Latest    string    `json:"latest,omitempty"`
	Owner     string    `json:"owner,omitempty"`
	// Outcome: "current", "staged", "offline", "busy", or "refused-dev",
	// "refused-owner", "refused-downgrade", "failed-<step>". The fetch half's
	// outcome ONLY -- activation (R3) records its own, separately, because
	// merging them is how a checksum failure once hid behind a green converge.
	Outcome string `json:"outcome"`
	Detail  string `json:"detail,omitempty"`
	// StagedVersion is set while a verified build sits in the staging dir
	// awaiting activation; "" otherwise.
	StagedVersion string `json:"staged_version,omitempty"`
}

State is one run's durable outcome, written on EVERY run -- including the refusals and the offline skips -- because the unit exit code is deliberately lossy (a deferral is green) and this file is what `swarm doctor` and the TUI's quiet line read instead (committee C3: a month of failed downloads must not be invisible).

func Activate

func Activate(opts ActivateOptions) (State, error)

Activate installs the staged build and execs its converge. ON SUCCESS IT DOES NOT RETURN -- the process becomes the new binary's `daemon restart --unattended`. Every other outcome returns with the state recorded: "deferred-wirebump" (staged kept, nothing installed), "failed-*" or ErrNothingStaged.

func ReadState

func ReadState(stateDir string) (State, error)

ReadState loads the last run's record; os.ErrNotExist when none ever ran.

func Rollback

func Rollback(opts ActivateOptions) (State, error)

Rollback restores the slot binaries over BinPath and execs the RESTORED binary's converge -- the same handoff rule as activation, for the same reason. On success it does not return. Hardened per the R2/R3 audit: the slots' card is REQUIRED (a rollback whose compatibility cannot be read is refused, never risked -- codex finding 5), the wire guard applies in the rollback direction too (finding 3: wire skew strands sessions either way), ownership is re-checked (finding 6), and a successful rollback CONSUMES its slots -- a second invocation finds nothing to restore instead of overwriting the hold with the restored version and re-arming the bad release (finding 5's treadmill).

func Stage

func Stage(ctx context.Context, opts Options) (State, error)

Stage runs Check and, on "stage", downloads, verifies (sha256 against checksums.txt, checksums.txt against its ed25519 signature) and unpacks the build into StageDir. It NEVER touches BinPath -- activation is R3 -- so a machine mid-work loses nothing when this runs at 04:00. Every outcome, including each refusal, lands in upgrade.json. The whole run holds an exclusive flock; a second concurrent run reports "busy" and touches nothing (committee M-3).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL