update

package
v0.1.190 Latest Latest
Warning

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

Go to latest
Published: Sep 15, 2026 License: Apache-2.0 Imports: 25 Imported by: 0

Documentation

Overview

Package update implements the signed, fail-closed CodeSampleX binary updater. It deliberately uses only the Go standard library so the updater does not add another executable supply-chain dependency of its own.

Index

Constants

View Source
const (
	ManifestSchema     = 1
	DefaultChannel     = "stable"
	DefaultManifestURL = "https://github.com/r2cuerdame/CodeSampleX/releases/latest/download/csx-update-stable.json"
)
View Source
const DefaultReleaseDownloadBase = "https://github.com/r2cuerdame/CodeSampleX/releases/download"

DefaultReleaseDownloadBase is the official release download root. Every rehydrated payload comes from underneath it, and the exact path is rebuilt from the version and target rather than taken from anywhere on this machine.

View Source
const RehydrateCooldown = 5 * time.Minute

RehydrateCooldown bounds how often a failing install repeats a repair it cannot complete. An editor or MCP host restarts a broken csx dozens of times a day; without this, every one of those would pay a full network timeout on a machine that is simply offline. An explicit operator repair ignores it.

Variables

View Source
var ErrPolicyDisabled = errors.New("update: automatic update policy was disabled")
View Source
var ErrRehydrateCooldown = errors.New("update: payload repair was attempted too recently")

ErrRehydrateCooldown reports that a failed repair was attempted too recently.

View Source
var ErrRehydrateNotNeeded = errors.New("update: the current payload is already verified")

ErrRehydrateNotNeeded reports that the pointer's current payload already verifies, so there was nothing to repair.

View Source
var PublicKeyBase64 string

PublicKeyBase64 is stamped into release binaries from the protected release signing secret. Development builds intentionally have no update trust root.

Functions

func AcknowledgeActivation

func AcknowledgeActivation(home, currentVersion string) error

AcknowledgeActivation clears a restart notice only from a newly launched binary that matches the installed target (or the first process after a rollback). It never removes the preserved previous executable.

func AdoptStandalone

func AdoptStandalone(home, executable string) error

func AutoEnabled

func AutoEnabled(mode, policy string) bool

func BootstrapLauncher

func BootstrapLauncher(ctx context.Context, root, staged, legacy, currentVersion string) (launcher.Active, error)

BootstrapLauncher is the one-time Windows installer bridge. It verifies the signed stable manifest with the key embedded in the payload, then lets the shared launcher commit code durably promote that running staged payload.

func CompareVersions

func CompareVersions(a, b string) (int, error)

CompareVersions compares strict numeric release versions, ignoring one leading v. It rejects ranges, aliases, and prerelease text.

func EmbeddedPublicKey

func EmbeddedPublicKey() (ed25519.PublicKey, error)

func InstallPath

func InstallPath(home string) string

func IsCanonicalReleaseVersion

func IsCanonicalReleaseVersion(v string) bool

func LockPidAlive added in v0.1.94

func LockPidAlive(pid int) bool

LockPidAlive reports whether pid names a live process, conservatively: it answers "alive" whenever it cannot prove otherwise, because every caller uses it to decide whether to take somebody else's single-instance lock away, and only a positive proof of death may do that.

Exported so the daemon can stop carrying its own version. That version read os.FindProcess succeeding as proof of life on Windows, which it is not -- FindProcess fails only for an INVALID pid, never for a dead one -- so no stale daemon lock was ever cleared there. Measured 2026-09-01 on a machine whose daemon had been unable to start for two days behind a lock naming a process that had exited.

func OwnsExecutable

func OwnsExecutable(home, executable string) (bool, error)

func ReleaseAssetURL added in v0.1.63

func ReleaseAssetURL(base, version, osName, arch string) (string, error)

ReleaseAssetURL rebuilds the official download URL for one release target. The version and target decide the whole path; nothing on the local machine contributes to it beyond the version string the pointer already recorded.

func Rollback

func Rollback(home, executable string) (string, error)

func SaveState

func SaveState(home string, st State) error

func StableExecutable

func StableExecutable(home, executable string) (string, error)

func StatePath

func StatePath(home string) string

func ValidateLauncherAssetForTest added in v0.1.91

func ValidateLauncherAssetForTest(a Asset, version string) error

ValidateLauncherAssetForTest exposes validateLauncherAsset so the release signer's own test can check that what it writes is what every client will accept. The two live on opposite sides of a signature, and a disagreement between them is an outage that begins at the moment of release.

func VerifyReleaseDirectory added in v0.1.145

func VerifyReleaseDirectory(dist, version string, pub ed25519.PublicKey, now time.Time) error

VerifyReleaseDirectory checks the complete payload and launcher set before publication or deployment makes its stable manifest visible to installers.

func WithLock

func WithLock(home string, fn func() error) error

WithLock serializes a consent/config write with update checks. When it returns, no check that observed the old policy can begin a later request.

Types

type Asset

type Asset struct {
	OS                 string `json:"os"`
	Arch               string `json:"arch"`
	URL                string `json:"url"`
	Size               int64  `json:"size"`
	SHA256             string `json:"sha256"`
	MinLauncherVersion string `json:"minLauncherVersion,omitempty"`

	// LauncherURL, LauncherSize and LauncherSHA256 name the Windows launcher
	// published with this release. Optional, and absent on every manifest
	// signed before they existed.
	//
	// They live in the SIGNED manifest rather than being read from the
	// release's SHA256SUMS.txt, because a launcher is the binary that runs
	// before the payload does. SHA256SUMS.txt is a file served beside the
	// release and nothing vouches for it; the manifest is the one artifact
	// the updater's own key covers. Taking the first thing that executes from
	// an unsigned list would hand it to whoever can write the download
	// surface.
	LauncherURL    string `json:"launcherUrl,omitempty"`
	LauncherSize   int64  `json:"launcherSize,omitempty"`
	LauncherSHA256 string `json:"launcherSha256,omitempty"`
}

func CurrentAsset

func CurrentAsset(m Manifest) (Asset, error)

type Client

type Client struct {
	Home              string
	CurrentVersion    string
	Executable        string
	ManifestURL       string
	Channel           string
	PublicKey         ed25519.PublicKey
	HTTP              *http.Client
	Now               func() time.Time
	SelfTest          func(context.Context, string, string) error
	Replace           func(string, string, string) error
	ValidateTarget    func(string) error
	SyncDir           func(string) error
	LoadState         func(string) (State, error)
	SaveState         func(string, State) error
	CheckApplySupport func() error
	Preflight         func() error
	Automatic         bool
	// contains filtered or unexported fields
}

func (*Client) Check

func (c *Client) Check(ctx context.Context, apply bool) (res Result, retErr error)

func (*Client) Due

func (c *Client) Due() bool

type Envelope

type Envelope struct {
	Payload   string `json:"payload"`
	Signature string `json:"signature"`
}

type Install

type Install struct {
	Schema         int    `json:"schema"`
	Kind           string `json:"kind"`
	ExecutablePath string `json:"executablePath"`
	InstallRoot    string `json:"installRoot,omitempty"`
	LauncherPath   string `json:"launcherPath,omitempty"`
}

func LoadInstall

func LoadInstall(home string) (Install, error)

func ResolveInstall

func ResolveInstall(home, executable string) (Install, error)

ResolveInstall uses the per-profile marker when present, then falls back to the stable launcher's verified environment. The latter is install-scoped, so a second CSX_HOME/profile still resolves the same trusted launcher.

type Manifest

type Manifest struct {
	Schema            int       `json:"schema"`
	Channel           string    `json:"channel"`
	Sequence          uint64    `json:"sequence"`
	Version           string    `json:"version"`
	PublishedAt       time.Time `json:"publishedAt"`
	ExpiresAt         time.Time `json:"expiresAt"`
	MinUpdaterVersion string    `json:"minUpdaterVersion,omitempty"`
	Assets            []Asset   `json:"assets"`
}

func VerifyBootstrapRelease added in v0.1.145

func VerifyBootstrapRelease(stableRaw, bootstrapRaw []byte, pub ed25519.PublicKey, now time.Time, version string) (Manifest, error)

VerifyBootstrapRelease binds the installer snapshot to its signed launcher descriptor. A separate envelope preserves the stable updater schema read by older clients, which reject launcher fields as unknown JSON fields.

func VerifyEnvelope

func VerifyEnvelope(raw []byte, publicKey ed25519.PublicKey, now time.Time, channel string) (Manifest, error)

VerifyEnvelope authenticates the raw payload before parsing it. Signing the exact bytes avoids any dependence on JSON canonicalization rules.

func (Manifest) AssetFor

func (m Manifest) AssetFor(osName, arch string) (Asset, error)

type RehydrateOptions added in v0.1.63

type RehydrateOptions struct {
	// HTTP is a test seam. Production leaves it nil, which is also what makes
	// the official-release URL check mandatory: httptest addresses are
	// deliberately not trusted download hosts.
	HTTP *http.Client
	// BaseURL overrides DefaultReleaseDownloadBase and is honored only
	// alongside an injected HTTP client.
	BaseURL string
	// SelfTest defaults to running `<staged> version`, which is how a payload
	// that downloads and hashes correctly but cannot execute is caught before
	// it is promoted.
	SelfTest func(ctx context.Context, path, version string) error
	// Arch defaults to runtime.GOARCH; OS is always the running one, because a
	// payload for another target could never have produced this pointer.
	Arch string
	// Force skips the cooldown. Explicit operator repair sets it.
	Force bool
	// StartFailed says the caller watched this payload fail to START, not to
	// hash. The launcher declares payload-start-failed repairable and routes
	// it here, but the bytes verify — so without this the repair answered
	// ErrRehydrateNotNeeded and the one failure meaning "these bytes are
	// correct and still will not run" was the one it refused to act on. An
	// install whose payload is blocked from executing stayed dead behind a
	// repair path reporting nothing to do.
	//
	// It replaces the current payload's bytes with the official artifact and
	// self-tests them, which is the only thing this path can do about a file
	// that hashes and does not execute.
	StartFailed bool
	Now         func() time.Time
}

RehydrateOptions configures one repair.

type RehydrateReport added in v0.1.63

type RehydrateReport struct {
	// ExhaustedVersion is the current payload the install could not run.
	ExhaustedVersion string
	// Restored names the versions whose bytes were refetched and verified.
	Restored []string
	// AlreadyVerified names candidates that turned out to be fine on disk.
	AlreadyVerified []string
	// FallbackVersion is the verified previous payload the install has after
	// the repair, empty when the pointer records none.
	FallbackVersion string
}

RehydrateReport says what one repair did. It is returned on success and on failure, because "current came back but the fallback did not" is a different operational state from "nothing came back".

func RehydrateInstall added in v0.1.63

func RehydrateInstall(ctx context.Context, root string, opts RehydrateOptions) (RehydrateReport, error)

RehydrateInstall refetches the payloads this install's pointer already records, from the official release path they were installed from.

It is the answer to the one failure the launcher's last-known-good recovery cannot handle. That recovery works by running a payload the pointer recorded earlier and that is still on disk; Defender quarantining `csx-payload.exe` as a false positive takes files, not pointers, and on 2026-08-29 it took the current payload and the recorded fallback on the same machine. The pointer was still exactly right. Every byte it named was gone, so there was nothing left on the machine to run — including the csx that owns `csx update`.

What makes this safe is that it adopts nothing. The descriptor's SHA-256 was recorded by this install from a signed manifest when the payload was committed, and it is the only thing the refetched bytes are accepted against: a hash mismatch, a truncated transfer, a substituted file and a redirect off the release path all end the same way, with the payload path untouched. A payload directory that merely happens to sit on this disk is still never adopted, exactly as launcher.Resolve refuses to. And the active pointer is not written here at all — this restores bytes the pointer already chose, and leaves every pointer change to the verified-only paths that own it.

It restores the recorded fallback as well as current, so a repaired install gets its minimum recovery set back rather than the single payload it needs to boot: the next quarantine is then handled locally, without the network.

type Result

type Result struct {
	CurrentVersion        string
	LatestVersion         string
	Available             bool
	Applied               bool
	RestartRequired       bool
	ManualInstallRequired bool
	RollbackHeld          bool
	PreviousPath          string
	NextCheck             time.Time

	// LauncherReplaced and LauncherError report the launcher half of the
	// update, which is separate because it is best effort: the payload has
	// already committed by the time it runs, and it must not be able to fail
	// an update that succeeded. An error here is worth SAYING -- a machine
	// silently keeping an old launcher is how a fix reached a release page
	// and stopped there for 26 releases -- but never worth failing over.
	LauncherReplaced bool
	LauncherError    string
}

type State

type State struct {
	Schema              int       `json:"schema"`
	HighestSequence     uint64    `json:"highestSequence,omitempty"`
	HighestVersion      string    `json:"highestVersion,omitempty"`
	LastCheck           time.Time `json:"lastCheck,omitempty"`
	NextCheck           time.Time `json:"nextCheck,omitempty"`
	ConsecutiveFailures int       `json:"consecutiveFailures,omitempty"`
	PendingRestart      string    `json:"pendingRestart,omitempty"`
	PreviousPath        string    `json:"previousPath,omitempty"`
	LastError           string    `json:"lastError,omitempty"`
}

func LoadState

func LoadState(home string) (State, error)

Jump to

Keyboard shortcuts

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