selfupdate

package
v0.59.0 Latest Latest
Warning

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

Go to latest
Published: Nov 3, 2026 License: MIT Imports: 3 Imported by: 0

README

selfupdate

import "github.com/kitsunium/sdk/framework/selfupdate"

Package selfupdate replaces the running binary with a newer signed release.

It is the framework's public package over framework/internal/service/selfupdate (ADR 0158): the value types and ports are aliases of the core selfupdate types and the functions delegate straight to the service implementation, which is built on pkg/v1 and nothing below it.

The order is the security property

The hard part of a self-update is not the download. It is the order in which a candidate becomes trusted, and this package fixes it:

  1. a detached ed25519 signature over the checksum manifest, verified against the vendor key linked into the build;
  2. the archive's SHA-256 against that now-authenticated manifest;
  3. only then does anything touch the disk.

A digest checked against an unauthenticated manifest proves nothing — an attacker who can substitute the archive can substitute the manifest beside it. Reversing steps 1 and 2 therefore turns the whole chain into decoration, which is why the order is asserted by the suite rather than left to a comment.

A build with no vendor key installs NOTHING. That direction is deliberate: the alternative, skipping verification when no key is present, makes the security property depend on a build flag nobody checks.

Usage
src := selfupdate.Source{
	Owner:      "acme",
	StableRepo: "widget",
	Product:    "widget",
}
svc := selfupdate.New(version, src).WithVendorKey(vendorKey)

info, err := svc.CheckForUpdate()
if err != nil { /* errs.HasCode(err, selfupdate.CodeDevBuild) … */ }
if info.Available {
	_, err = svc.Upgrade()
}

An explicit upgrade command is a deliberate act and needs no permission. An upgrade a caller did NOT ask for — a version floor enforced mid-run — does, and Source.AuthoriseUnattendedUpgrade is where that is decided: an explicit environment value settles it in either direction, otherwise a human is asked, otherwise the answer is no. Silence is not permission.

Replacing a binary in a directory the user cannot write needs a SECOND opt-in. The two are deliberately different variables: opting into unattended upgrades must not silently grant privilege escalation.

Both variable names are derived from Source.Product — `widget` yields WIDGET_AUTO_UPGRADE and WIDGET_ALLOW_SUDO — by uppercasing and folding punctuation to underscore.

A product whose updates are silent by design builds its Service WithAutomaticConsent — its own consent, given at build, which Service.AuthoriseUnattendedUpgrade reads and an operator still overrules with <PREFIX>_AUTO_UPGRADE=0. It grants no escalation, and WithoutElevation forbids escalation outright, whatever <PREFIX>_ALLOW_SUDO says (ADR 0150).

Keys that rotate, a signature that names its release, a probe that rolls back

Service.WithVendorKeys links several keys, in order, and a release verifies against any of them: a rotation publishes under the new key while builds that carry both accept it, and neither side has to be updated first (ADR 0150). Service.WithSignatureDomain makes each signature cover a domain — so a key that also signs other documents cannot have one read as a release — and makes the signed manifest say which tag it is and until when it may be installed ("# tag v1.4.0", "# expires 2026-12-31T00:00:00Z"): an older release replayed under a newer name, or a stale one, is refused. Service.WithProbe keeps the previous binary as <binary>.prev and runs the new one with the product's probe arguments; a probe that fails puts the previous one back (CodeProbeFailed).

Companions: a second executable from the same archive

A product that ships more than one executable in its release archive — a thin client beside a daemon — declares the others with Service.WithCompanions. Each one installed beside the running binary is replaced from the archive that was already authenticated, as one set with the binary: every file staged, every previous one kept, then renamed, and a rename or a probe that fails puts back the files already replaced. One that is not installed is not created.

Two limits a caller must know before relying on this

**Without a signature domain, a compromised release host can serve an OLDER signed release.** The historical manifest is signed but carries no release tag, so a host that answers a request for v2 with v1's manifest, signature and archive passes every check and installs v1. WithSignatureDomain closes it by putting the tag and an expiry INSIDE the signed document; a product that has not adopted the format keeps the gap.

**Windows is not supported for the replacement step.** Windows will not let a running executable be renamed over, so on a Windows build Upgrade refuses with the SDK's UNSUPPORTED_PLATFORM — errors.Is(err, proc.UnsupportedPlatform) — before it downloads anything. CheckForUpdate works there: a product can still tell its user that a newer release exists. A build with no vendor key is told about the key first, on every platform.

Both are recorded in ADR 0077 §Deferred rather than left to be discovered, and the Windows refusal in ADR 0095.

What this package does not do

It does not roll back on its own. The replacement is atomic (temp file, chmod, rename) so there is no window where the binary is half-written, and without WithProbe the previous version is gone once the rename lands; with a probe it is kept as <binary>.prev and put back when the probe fails.

Index

Constants

const CandidateListSentinel string = "__list__"

CandidateListSentinel is the tag value meaning "list the candidates rather than install one".

It exists because a CLI flag taking an optional value has to spell "the flag was given without a value" AS a value. A caller wires it into its own flag definition, so the sentinel has to be reachable from outside the package that consumes it.

Initialised with github.com/kitsunium/sdk/framework/internal/service/selfupdate.CandidateListSentinel.

const CodeAPIBodyTooLarge errs.Code = 139783

CodeAPIBodyTooLarge identifies release metadata over its read cap.

Initialised with github.com/kitsunium/sdk/framework/internal/core/selfupdate.CodeAPIBodyTooLarge.

Code 0.2.34.7

const CodeArchiveTooLarge errs.Code = 139782

CodeArchiveTooLarge identifies a release archive over the in-memory cap.

Initialised with github.com/kitsunium/sdk/framework/internal/core/selfupdate.CodeArchiveTooLarge.

Code 0.2.34.6

const CodeArchiveUnreadable errs.Code = 213507

CodeArchiveUnreadable identifies a release archive that would not unpack. It is raised only AFTER the signature and digest have both verified, so it is a packaging or transfer fault and never a supply-chain signal.

Initialised with github.com/kitsunium/sdk/framework/internal/service/selfupdate.CodeArchiveUnreadable.

Code 0.3.66.3

const CodeBinaryNotInArchive errs.Code = 139792

CodeBinaryNotInArchive identifies a verified archive missing the binary.

Initialised with github.com/kitsunium/sdk/framework/internal/core/selfupdate.CodeBinaryNotInArchive.

Code 0.2.34.16

const CodeCandidateNotFound errs.Code = 139788

CodeCandidateNotFound identifies a requested candidate that does not exist.

Initialised with github.com/kitsunium/sdk/framework/internal/core/selfupdate.CodeCandidateNotFound.

Code 0.2.34.12

const CodeCandidateTagRequired errs.Code = 213505

CodeCandidateTagRequired identifies a candidate install asked for without naming which candidate. It wraps errors.ErrUnsupported, so a caller matching that instead still matches.

Initialised with github.com/kitsunium/sdk/framework/internal/service/selfupdate.CodeCandidateTagRequired.

Code 0.3.66.1

const CodeChecksumMismatch errs.Code = 139781

CodeChecksumMismatch identifies an archive whose digest did not match its authenticated manifest entry.

Initialised with github.com/kitsunium/sdk/framework/internal/core/selfupdate.CodeChecksumMismatch.

Code 0.2.34.5

const CodeChecksumMissing errs.Code = 139780

CodeChecksumMissing identifies an archive with no entry in the authenticated manifest.

Initialised with github.com/kitsunium/sdk/framework/internal/core/selfupdate.CodeChecksumMissing.

Code 0.2.34.4

const CodeDevBuild errs.Code = 139787

CodeDevBuild identifies a check on a build with no release version.

Initialised with github.com/kitsunium/sdk/framework/internal/core/selfupdate.CodeDevBuild.

Code 0.2.34.11

const CodeDownloadFailed errs.Code = 139786

CodeDownloadFailed identifies a download that did not complete. It is the transient one: a retry is reasonable.

Initialised with github.com/kitsunium/sdk/framework/internal/core/selfupdate.CodeDownloadFailed.

Code 0.2.34.10

const CodeDraftRelease errs.Code = 139790

CodeDraftRelease identifies a draft release, which exposes no asset.

Initialised with github.com/kitsunium/sdk/framework/internal/core/selfupdate.CodeDraftRelease.

Code 0.2.34.14

const CodeElevationFailed errs.Code = 213511

CodeElevationFailed identifies an escalation the operator DID authorise and that the system refused anyway. Unlike CodeElevationNotAuthorised, no environment variable repairs it.

Initialised with github.com/kitsunium/sdk/framework/internal/service/selfupdate.CodeElevationFailed.

Code 0.3.66.7

const CodeElevationNotAuthorised errs.Code = 139794

CodeElevationNotAuthorised identifies a replacement needing privilege the operator did not opt into. Recoverable, but only through the opt-in.

Initialised with github.com/kitsunium/sdk/framework/internal/core/selfupdate.CodeElevationNotAuthorised.

Code 0.2.34.18

const CodeExecutablePathUnresolved errs.Code = 213508

CodeExecutablePathUnresolved identifies an update that cannot name the file it would replace. Nothing is attempted after it.

Initialised with github.com/kitsunium/sdk/framework/internal/service/selfupdate.CodeExecutablePathUnresolved.

Code 0.3.66.4

const CodeInsecureRedirect errs.Code = 139784

CodeInsecureRedirect identifies a redirect leaving HTTPS or over the hop bound.

Initialised with github.com/kitsunium/sdk/framework/internal/core/selfupdate.CodeInsecureRedirect.

Code 0.2.34.8

const CodeInvalidTag errs.Code = 139791

CodeInvalidTag identifies a tag that is not a valid semantic version.

Initialised with github.com/kitsunium/sdk/framework/internal/core/selfupdate.CodeInvalidTag.

Code 0.2.34.15

const CodeNoVendorKey errs.Code = 139777

CodeNoVendorKey identifies an install attempted by a build carrying no vendor key. Match it with errs.HasCode: it is the one refusal a retry never fixes.

Initialised with github.com/kitsunium/sdk/framework/internal/core/selfupdate.CodeNoVendorKey.

Code 0.2.34.1

const CodeNotPrerelease errs.Code = 139789

CodeNotPrerelease identifies a tag requested as a candidate that is stable.

Initialised with github.com/kitsunium/sdk/framework/internal/core/selfupdate.CodeNotPrerelease.

Code 0.2.34.13

const CodeProbeFailed errs.Code = 213512

CodeProbeFailed identifies a replacement whose new binary did not answer the probe the product declared with Service.WithProbe; the previous binary is put back from <binary>.prev when it was kept (ADR 0150).

Initialised with github.com/kitsunium/sdk/framework/internal/service/selfupdate.CodeProbeFailed.

Code 0.3.66.8

const CodeReleaseMetadataUnreadable errs.Code = 213506

CodeReleaseMetadataUnreadable identifies a release-API answer that arrived whole and did not decode. The bytes came, so no transport retry applies.

Initialised with github.com/kitsunium/sdk/framework/internal/service/selfupdate.CodeReleaseMetadataUnreadable.

Code 0.3.66.2

const CodeReplacementFailed errs.Code = 213510

CodeReplacementFailed identifies a rename over the running executable that did not land.

Initialised with github.com/kitsunium/sdk/framework/internal/service/selfupdate.CodeReplacementFailed.

Code 0.3.66.6

const CodeSignatureInvalid errs.Code = 139779

CodeSignatureInvalid identifies a manifest signature that did not verify. With CodeSignatureMissing it is the supply-chain pair: no retry helps, and no manual install is safe.

Initialised with github.com/kitsunium/sdk/framework/internal/core/selfupdate.CodeSignatureInvalid.

Code 0.2.34.3

const CodeSignatureMissing errs.Code = 139778

CodeSignatureMissing identifies a release whose manifest signature was absent.

Initialised with github.com/kitsunium/sdk/framework/internal/core/selfupdate.CodeSignatureMissing.

Code 0.2.34.2

const CodeStagingFailed errs.Code = 213509

CodeStagingFailed identifies a replacement that failed BEFORE the rename, so the running binary is byte-for-byte as it was. That is the whole difference between this and CodeReplacementFailed, and it is the fact an operator needs.

Initialised with github.com/kitsunium/sdk/framework/internal/service/selfupdate.CodeStagingFailed.

Code 0.3.66.5

const CodeUnexpectedStatus errs.Code = 139785

CodeUnexpectedStatus identifies an unusable status from the release host.

Initialised with github.com/kitsunium/sdk/framework/internal/core/selfupdate.CodeUnexpectedStatus.

Code 0.2.34.9

const CodeUnknownArchive errs.Code = 139793

CodeUnknownArchive identifies an asset in an unhandled container format.

Initialised with github.com/kitsunium/sdk/framework/internal/core/selfupdate.CodeUnknownArchive.

Code 0.2.34.17

Variables

var APIBodyTooLarge *errs.Error

APIBodyTooLarge is returned when a release-metadata response exceeds its read cap.

Initialised with github.com/kitsunium/sdk/framework/internal/core/selfupdate.APIBodyTooLarge.

Code 0.2.34.7 · reason API_BODY_TOO_LARGE · public "the release metadata is larger than this build accepts"

var ArchiveTooLarge *errs.Error

ArchiveTooLarge is returned when a release archive exceeds the size cap, which exists because the bytes are buffered before anything has vouched for them. It is also what an oversized checksums.txt raises, deliberately — a truncated manifest fails signature verification, and reporting a size problem as a supply-chain one is the worst advice this package can give.

Initialised with github.com/kitsunium/sdk/framework/internal/core/selfupdate.ArchiveTooLarge.

Code 0.2.34.6 · reason ARCHIVE_TOO_LARGE · public "the release archive is larger than this build accepts"

var ArchiveUnreadable *errs.Error

ArchiveUnreadable is returned when a release archive will not unpack. It is raised only AFTER the signature and digest have both verified, so it is a packaging or transfer fault and never a supply-chain signal.

Initialised with github.com/kitsunium/sdk/framework/internal/service/selfupdate.ArchiveUnreadable.

Code 0.3.66.3 · reason ARCHIVE_UNREADABLE · public "the release archive could not be read"

var BinaryNotInArchive *errs.Error

BinaryNotInArchive is returned when a verified archive does not contain the expected binary. The archive is authentic; it is a packaging fault rather than an attack.

Initialised with github.com/kitsunium/sdk/framework/internal/core/selfupdate.BinaryNotInArchive.

Code 0.2.34.16 · reason BINARY_NOT_IN_ARCHIVE · public "the release archive does not contain the expected binary"

var CandidateNotFound *errs.Error

CandidateNotFound is returned when a requested release candidate does not exist.

Initialised with github.com/kitsunium/sdk/framework/internal/core/selfupdate.CandidateNotFound.

Code 0.2.34.12 · reason CANDIDATE_NOT_FOUND · public "no such release candidate"

var CandidateTagRequired *errs.Error

CandidateTagRequired is returned by DownloadCandidate for an empty tag. It wraps errors.ErrUnsupported, so a caller matching that instead still matches.

Initialised with github.com/kitsunium/sdk/framework/internal/service/selfupdate.CandidateTagRequired.

Code 0.3.66.1 · reason CANDIDATE_TAG_REQUIRED · public "a release candidate is installed by naming its tag, and no tag was named"

var ChecksumMismatch *errs.Error

ChecksumMismatch is returned when the archive's digest does not match its authenticated manifest entry — a corrupted download, or a release whose assets changed after it was signed.

Initialised with github.com/kitsunium/sdk/framework/internal/core/selfupdate.ChecksumMismatch.

Code 0.2.34.5 · reason CHECKSUM_MISMATCH · public "the release archive does not match its checksum"

var ChecksumMissing *errs.Error

ChecksumMissing is returned when the authenticated manifest has no entry for the archive being installed.

Initialised with github.com/kitsunium/sdk/framework/internal/core/selfupdate.ChecksumMissing.

Code 0.2.34.4 · reason CHECKSUM_MISSING · public "the release has no checksum for this platform"

var DevBuild *errs.Error

DevBuild is returned when a version check runs on a build carrying no release version, so there is nothing to compare a release against. It is an ANSWER rather than a fault — CheckForUpdate on a `go build` binary reaches it every time — which is why ExplainUpgradeFailure has no case for it: "rebuild from source" is NoVendorKey's advice, not this one's.

Initialised with github.com/kitsunium/sdk/framework/internal/core/selfupdate.DevBuild.

Code 0.2.34.11 · reason DEV_BUILD · public "this build has no version to compare against a release"

var DownloadFailed *errs.Error

DownloadFailed is returned when a download does not complete, including a transport failure with no HTTP status at all. It is the sentinel a retry policy keys on.

Initialised with github.com/kitsunium/sdk/framework/internal/core/selfupdate.DownloadFailed.

Code 0.2.34.10 · reason DOWNLOAD_FAILED · public "the release could not be downloaded"

var DraftRelease *errs.Error

DraftRelease is returned for a draft release, which exposes no asset.

Initialised with github.com/kitsunium/sdk/framework/internal/core/selfupdate.DraftRelease.

Code 0.2.34.14 · reason DRAFT_RELEASE · public "that release is still a draft"

var ElevationFailed *errs.Error

ElevationFailed is returned when an escalation the operator DID authorise was refused by the system. Unlike ElevationNotAuthorised, no environment variable repairs it.

Initialised with github.com/kitsunium/sdk/framework/internal/service/selfupdate.ElevationFailed.

Code 0.3.66.7 · reason ELEVATION_FAILED · public "the elevated replacement was authorised and the system refused it"

var ElevationNotAuthorised *errs.Error

ElevationNotAuthorised is returned when replacing the binary needs privilege the operator did not opt into. Recoverable — but only through the opt-in, which ExplainUpgradeFailure names.

Initialised with github.com/kitsunium/sdk/framework/internal/core/selfupdate.ElevationNotAuthorised.

Code 0.2.34.18 · reason ELEVATION_NOT_AUTHORISED · public "replacing this binary needs privilege that was not authorised"

var ExecutablePathUnresolved *errs.Error

ExecutablePathUnresolved is returned when the process cannot work out which file on disk it runs from. Nothing is attempted after it.

Initialised with github.com/kitsunium/sdk/framework/internal/service/selfupdate.ExecutablePathUnresolved.

Code 0.3.66.4 · reason EXECUTABLE_PATH_UNRESOLVED · public "the path of the running binary could not be resolved, so nothing was replaced"

var InsecureRedirect *errs.Error

InsecureRedirect is returned when a redirect leaves HTTPS or exceeds the hop bound. Following it would hand the archive fetch to a plaintext hop.

Initialised with github.com/kitsunium/sdk/framework/internal/core/selfupdate.InsecureRedirect.

Code 0.2.34.8 · reason INSECURE_REDIRECT · public "the release host redirected somewhere this build will not follow"

var InvalidTag *errs.Error

InvalidTag is returned for a tag that is not a valid semantic version.

Initialised with github.com/kitsunium/sdk/framework/internal/core/selfupdate.InvalidTag.

Code 0.2.34.15 · reason INVALID_TAG · public "that is not a valid version tag"

var NoVendorKey *errs.Error

NoVendorKey is returned when the running build carries no vendor key, so nothing could have authenticated the release. A REFUSAL, not a degradation: there is no weaker check to fall back to.

Initialised with github.com/kitsunium/sdk/framework/internal/core/selfupdate.NoVendorKey.

Code 0.2.34.1 · reason NO_VENDOR_KEY · public "this build cannot verify a release and will not install one"

var NotPrerelease *errs.Error

NotPrerelease is returned when a tag requested as a candidate is reported as a stable release.

Initialised with github.com/kitsunium/sdk/framework/internal/core/selfupdate.NotPrerelease.

Code 0.2.34.13 · reason NOT_PRERELEASE · public "that tag is a stable release, not a candidate"

var ProbeFailed *errs.Error

ProbeFailed is returned when the replaced binary did not answer its probe; rolled_back says whether <binary>.prev was put back.

Initialised with github.com/kitsunium/sdk/framework/internal/service/selfupdate.ProbeFailed.

Code 0.3.66.8 · reason PROBE_FAILED · public "the new binary did not answer its probe and was not kept"

var ReleaseMetadataUnreadable *errs.Error

ReleaseMetadataUnreadable is returned when the release API answers within its cap and the body does not decode. The bytes arrived, so no transport retry applies.

Initialised with github.com/kitsunium/sdk/framework/internal/service/selfupdate.ReleaseMetadataUnreadable.

Code 0.3.66.2 · reason RELEASE_METADATA_UNREADABLE · public "the release host's answer could not be read as release metadata"

var ReplacementFailed *errs.Error

ReplacementFailed is returned when the rename over the running executable did not land.

Initialised with github.com/kitsunium/sdk/framework/internal/service/selfupdate.ReplacementFailed.

Code 0.3.66.6 · reason REPLACEMENT_FAILED · public "the running binary could not be replaced"

var SignatureInvalid *errs.Error

SignatureInvalid is returned when the manifest's signature does not verify against the vendor key. With SignatureMissing it is the supply-chain pair: no retry helps and no manual install is safe.

Initialised with github.com/kitsunium/sdk/framework/internal/core/selfupdate.SignatureInvalid.

Code 0.2.34.3 · reason SIGNATURE_INVALID · public "the release signature did not verify"

var SignatureMissing *errs.Error

SignatureMissing is returned when the detached signature over the checksum manifest could not be fetched. An unsigned release is never installed and no environment variable reopens that.

Initialised with github.com/kitsunium/sdk/framework/internal/core/selfupdate.SignatureMissing.

Code 0.2.34.2 · reason SIGNATURE_MISSING · public "the release is not signed"

var StagingFailed *errs.Error

StagingFailed is returned when the replacement failed BEFORE the rename, so the running binary is byte-for-byte as it was. That is the whole difference between this and ReplacementFailed.

Initialised with github.com/kitsunium/sdk/framework/internal/service/selfupdate.StagingFailed.

Code 0.3.66.5 · reason STAGING_FAILED · public "the update could not be staged, and the running binary was left untouched"

var UnexpectedStatus *errs.Error

UnexpectedStatus is returned when the release host answers with a status the update path does not accept. Retryable.

Initialised with github.com/kitsunium/sdk/framework/internal/core/selfupdate.UnexpectedStatus.

Code 0.2.34.9 · reason UNEXPECTED_STATUS · public "the release host answered unexpectedly"

var UnknownArchive *errs.Error

UnknownArchive is returned for an asset whose container format is not handled.

Initialised with github.com/kitsunium/sdk/framework/internal/core/selfupdate.UnknownArchive.

Code 0.2.34.17 · reason UNKNOWN_ARCHIVE · public "the release archive format is not supported"

func New

func New(version string, src Source) *Service

New returns a Service for the given running version and release source.

The returned Service carries NO vendor key and therefore installs nothing: chain WithVendorKey with the build's linked-in anchor. That is the safe direction — a Service that verified only when a key happened to be present would make the security property depend on a build flag.

func NewWithDeps

func NewWithDeps(version string, src Source, client Getter, fs FileSystem, copier Copier) *Service

NewWithDeps returns a Service with its three ports injected, for a caller that supplies its own HTTP policy or a test that supplies doubles. A nil fs or copier is legal on paths that never reach the disk.

func StdinIsTerminal

func StdinIsTerminal() bool

StdinIsTerminal reports whether a human could answer a prompt on this process's standard input.

It is a free function rather than something AuthoriseUnattendedUpgrade works out for itself, and that is the point: the consent decision stays testable without a pty, because the CALLER supplies the answer. Pass the result as the interactive argument.

type Candidate

Candidate is one release candidate. It aliases the core value type.

type Candidate = svcupd.CandidateValue

Candidate is an alias of github.com/kitsunium/sdk/framework/internal/core/selfupdate.CandidateValue, declared as:

type Candidate struct {
	// Tag is the release tag, e.g. "v1.9.0-rc.3".
	Tag       string
	// Name is the release's human title, which a host may leave empty.
	Name      string
	// CreatedAt is the RFC 3339 publication timestamp, as text.
	CreatedAt string
}

type Copier

Copier streams the verified archive to its destination. It aliases the core port.

type Copier = coreupd.Copier

Copier is an alias of github.com/kitsunium/sdk/framework/internal/core/selfupdate.Copier, declared as:

type Copier interface {
	// Copy copies from src to dst, returning the number of bytes written.
	Copy(dst io.Writer, src io.Reader) (written int64, err error)
}

func (Copier) Copy
func (Copier) Copy(dst io.Writer, src io.Reader) (written int64, err error)

Copy copies from src to dst, returning the number of bytes written.

type FileSystem

FileSystem is the disk half of replacing a running binary. It aliases the core port.

type FileSystem = coreupd.FileSystem

FileSystem is an alias of github.com/kitsunium/sdk/framework/internal/core/selfupdate.FileSystem, declared as:

type FileSystem interface {
	// Chmod changes file permissions.
	Chmod(name string, mode os.FileMode) error
	// CreateTemp creates a temporary file.
	CreateTemp(dir string, pattern string) (file *os.File, err error)
	// EvalSymlinks resolves symlinks in the given path.
	EvalSymlinks(path string) (resolved string, err error)
	// Executable returns the path of the current executable.
	Executable() (path string, err error)
	// Remove removes a file.
	Remove(name string) error
	// Rename renames a file. It is the atomic half of the replacement and must
	// happen on the same filesystem as the target.
	Rename(oldpath string, newpath string) error
}

func (FileSystem) Chmod
func (FileSystem) Chmod(name string, mode os.FileMode) error

Chmod changes file permissions.

func (FileSystem) CreateTemp
func (FileSystem) CreateTemp(dir string, pattern string) (file *os.File, err error)

CreateTemp creates a temporary file.

func (FileSystem) EvalSymlinks(path string) (resolved string, err error)

EvalSymlinks resolves symlinks in the given path.

func (FileSystem) Executable
func (FileSystem) Executable() (path string, err error)

Executable returns the path of the current executable.

func (FileSystem) Remove
func (FileSystem) Remove(name string) error

Remove removes a file.

func (FileSystem) Rename
func (FileSystem) Rename(oldpath string, newpath string) error

Rename renames a file. It is the atomic half of the replacement and must happen on the same filesystem as the target.

type Getter

Getter performs the HTTP GETs a self-update needs. It aliases the core port.

type Getter = coreupd.Getter

Getter is an alias of github.com/kitsunium/sdk/framework/internal/core/selfupdate.Getter, declared as:

type Getter interface {
	// Get performs an HTTP GET request.
	Get(url string) (resp *http.Response, err error)
}

func (Getter) Get
func (Getter) Get(url string) (resp *http.Response, err error)

Get performs an HTTP GET request.

type Service

Service replaces the running binary with a newer signed release. It aliases the service type — the engine handle, per ADR 0074.

type Service = svcupd.Service

Service is an alias of github.com/kitsunium/sdk/framework/internal/service/selfupdate.Service, declared as:

type Service struct{}

func (Service) AuthoriseUnattendedUpgrade
func (*Service) AuthoriseUnattendedUpgrade(out io.Writer, in io.Reader, interactive bool) bool

AuthoriseUnattendedUpgrade decides whether an upgrade nobody asked for may proceed for this Service: an explicit environment answer first, then the product's own consent (WithAutomaticConsent), then the source's prompt rule.

func (Service) CheckForUpdate
func (*Service) CheckForUpdate() (info UpdateValue, checkErr error)

CheckForUpdate checks if an update is available.

func (Service) DownloadCandidate
func (*Service) DownloadCandidate(tag string) (info UpdateValue, downloadErr error)

DownloadCandidate downloads and installs a specific release candidate.

func (Service) ListCandidates
func (*Service) ListCandidates() (candidates []CandidateValue, listErr error)

ListCandidates returns all available release candidates.

func (Service) Upgrade
func (*Service) Upgrade() (info UpdateValue, upgradeErr error)

Upgrade downloads and applies the latest version.

func (Service) WithAutomaticConsent
func (*Service) WithAutomaticConsent() *Service

WithAutomaticConsent declares that the PRODUCT consents to unrequested upgrades — a status line that updates itself silently, its decision D11 —, so no environment variable has to grant it. An operator still refuses with <PREFIX>_AUTO_UPGRADE=0: an explicit answer always wins. It grants nothing else: escalation keeps its own, separate opt-in.

func (Service) WithCompanions
func (*Service) WithCompanions(names ...string) *Service

WithCompanions makes an upgrade also replace each named file the release archive carries beside the product binary — a second executable shipped in the same archive — from that SAME archive, after the one signature and checksum check that covers it: nothing is downloaded twice. A companion is replaced where it is installed, beside the running binary, its symlinks resolved; one that is not installed there is not created. A companion that resolves to the binary itself, or to a file another name already reached, is that file and is replaced once.

Every file is staged in its own directory before any is renamed, and each installed one is kept by a hard link as <file>.prev. A rename, or the probe WithProbe declared, that fails puts back every file already replaced, so the product and its companions are never left at two versions; the kept files are dropped once all stand, unless a probe keeps them as it keeps the binary's. A companion the archive lacks refuses the upgrade (BinaryNotInArchive) with nothing replaced. The replacement of a set is never elevated: a directory the user cannot write, or a file system that cannot link, refuses it (StagingFailed) with nothing replaced.

func (Service) WithProbe
func (*Service) WithProbe(args []string, timeout time.Duration) *Service

WithProbe makes a replacement stand only once the NEW binary, run with args, exits 0 within timeout (zero: five seconds). Before the rename the previous binary is kept as <binary>.prev — a hard link, so the binary's own name is never absent —, and a probe that fails puts it back and reports ProbeFailed. A daemon that updates itself declares the argument its binary answers cheaply ("version", "--probe").

func (Service) WithSignatureDomain
func (*Service) WithSignatureDomain(domain string) *Service

WithSignatureDomain makes every signature cover domain, a NUL byte and the manifest — never the manifest alone — so a key the vendor also signs other documents with (a roster, another product's releases) cannot have one of those signatures read as a release of this product. It also requires the manifest to say which release it is and until when it may be installed, in two header lines the signature therefore covers:

# tag v1.4.0
# expires 2026-12-31T00:00:00Z

A manifest naming another tag is refused — the replay of an older, signed release under a newer name —, and so is one past its expiry — the freeze an attacker who can only replay can otherwise hold an installation in. Without a domain the historical form is verified unchanged.

func (Service) WithVendorKey
func (*Service) WithVendorKey(key []byte) *Service

WithVendorKey pins the ed25519 public half every release must be signed with, and returns the Service so construction reads as one expression — the same chaining shape license.NewService(...).WithVersion(...) uses.

A Service built WITHOUT this call authenticates nothing and therefore installs nothing: verifyArchive refuses with coreupd.NoVendorKey before a single byte is fetched. That default is deliberate. The alternative — treat an absent key as "verification not required" — would mean any construction site that forgot the call silently reverted to the unauthenticated behaviour this file exists to end, and nothing would ever fail to point it out.

func (Service) WithVendorKeys
func (*Service) WithVendorKeys(keys ...[]byte) *Service

WithVendorKeys links the keys a release may be signed with, in order: a release verifies when ANY of them verifies its manifest (ADR 0150). A key rotation publishes under the new key while builds carrying both accept it and builds carrying only the old one keep reading the old — neither side has to be updated first. Several keys are still ONE signer: no quorum is taken. A key of the wrong length is kept as it is and refused at verification, naming its length, exactly as a single wrong key always was.

func (Service) WithoutElevation
func (*Service) WithoutElevation() *Service

WithoutElevation forbids the privileged replacement outright, whatever <PREFIX>_ALLOW_SUDO says: a binary installed where its user cannot write is reported (ElevationNotAuthorised), never replaced through sudo.

type Source

Source says where releases come from and what they are called. It aliases the service type: these are one engine's construction parameters, which ADR 0074 places with the engine rather than in the contract layer.

type Source = svcupd.SourceValue

Source is an alias of github.com/kitsunium/sdk/framework/internal/service/selfupdate.SourceValue, declared as:

type Source struct {
	// Owner is the release host account, e.g. "kodflow".
	Owner      string
	// StableRepo is the repository serving stable releases.
	StableRepo string
	// DevRepo is the repository serving release candidates. Empty means
	// candidates come from StableRepo too.
	DevRepo    string
	// Product is the binary's name. It is both the archive-asset stem
	// ("<product>_<goos>_<goarch>.tar.gz") and, uppercased, the environment
	// variable prefix for the two opt-ins.
	Product    string
}

func (Source) AuthoriseUnattendedUpgrade
func (Source) AuthoriseUnattendedUpgrade(out io.Writer, in io.Reader, interactive bool) bool

AuthoriseUnattendedUpgrade reports whether a caller that did not ask to upgrade may nonetheless replace this binary.

out receives the prompt; in supplies the answer; interactive says whether there is a human on the other end (the caller decides — see StdinIsTerminal — so a test does not need a pty). The environment is read here rather than passed so every call site honours the same variable.

The order is: an explicit authorisation decides, in EITHER direction; otherwise a human is asked; otherwise the answer is no.

func (Source) AutoUpgradeEnv
func (Source) AutoUpgradeEnv() string

AutoUpgradeEnv is the variable authorising an upgrade the user did not ask for, e.g. MY_TOOL_AUTO_UPGRADE.

func (Source) ExplainUpgradeFailure
func (Source) ExplainUpgradeFailure(out io.Writer, action string, err error)

ExplainUpgradeFailure prints what a refused upgrade means and what, if anything, resolves it. `action` names the operation that failed, so the same advice serves the gate ("cannot apply the required update") and the deliberate command ("upgrade failed").

It exists because exit.Code's message parameter only reaches the structured --debug log (pkg/exit's logEvent is a no-op unless --debug is set): without this, an upgrade against a forged or unsigned release printed "Checking for updates..." and then nothing, exiting non-zero with no reason given. For a network blip that is merely poor; for an authenticity refusal it hides the one signal that must not be missed, and invites the user to go and install the bad release by hand.

It prints THREE things, in descending order of how far they may travel: the error's public sentence, which is wire-safe and says what happened; its diagnostic half — fields and the cause's own words — which says where and why and is for this terminal and the log, never for a response body; and the advice, which is what to do about it.

func (Source) ExplainUpgradeRefusal
func (Source) ExplainUpgradeRefusal(out io.Writer)

ExplainUpgradeRefusal prints every way to get the required update installed after consent was declined or could not be obtained.

It is deliberately exhaustive, including the sudo opt-in: the two authorisations are separate on purpose (one permits replacing the binary, the other permits doing it as root) and discovering the second one only after acting on the first is a bad afternoon.

func (Source) SudoOptInEnv
func (Source) SudoOptInEnv() string

SudoOptInEnv is the variable authorising privilege escalation when the install directory is not writable, e.g. MY_TOOL_ALLOW_SUDO.

type Update

Update is the outcome of a version check or an install. It aliases the core value type.

type Update = svcupd.UpdateValue

Update is an alias of github.com/kitsunium/sdk/framework/internal/core/selfupdate.UpdateValue, declared as:

type Update struct {
	// CurrentVersion is the version of the running binary.
	CurrentVersion string
	// LatestVersion is the version that was found, or the empty string when
	// none was.
	LatestVersion  string
	// Available reports whether LatestVersion is newer than CurrentVersion.
	Available      bool
}

Generated from docs/api by tools/genindex.

Documentation

Overview

Package selfupdate replaces the running binary with a newer signed release.

It is the framework's public package over framework/internal/service/selfupdate (ADR 0158): the value types and ports are aliases of the core selfupdate types and the functions delegate straight to the service implementation, which is built on pkg/v1 and nothing below it.

The order is the security property

The hard part of a self-update is not the download. It is the order in which a candidate becomes trusted, and this package fixes it:

  1. a detached ed25519 signature over the checksum manifest, verified against the vendor key linked into the build;
  2. the archive's SHA-256 against that now-authenticated manifest;
  3. only then does anything touch the disk.

A digest checked against an unauthenticated manifest proves nothing — an attacker who can substitute the archive can substitute the manifest beside it. Reversing steps 1 and 2 therefore turns the whole chain into decoration, which is why the order is asserted by the suite rather than left to a comment.

A build with no vendor key installs NOTHING. That direction is deliberate: the alternative, skipping verification when no key is present, makes the security property depend on a build flag nobody checks.

Usage

src := selfupdate.Source{
	Owner:      "acme",
	StableRepo: "widget",
	Product:    "widget",
}
svc := selfupdate.New(version, src).WithVendorKey(vendorKey)

info, err := svc.CheckForUpdate()
if err != nil { /* errs.HasCode(err, selfupdate.CodeDevBuild) … */ }
if info.Available {
	_, err = svc.Upgrade()
}

An explicit upgrade command is a deliberate act and needs no permission. An upgrade a caller did NOT ask for — a version floor enforced mid-run — does, and Source.AuthoriseUnattendedUpgrade is where that is decided: an explicit environment value settles it in either direction, otherwise a human is asked, otherwise the answer is no. Silence is not permission.

Replacing a binary in a directory the user cannot write needs a SECOND opt-in. The two are deliberately different variables: opting into unattended upgrades must not silently grant privilege escalation.

Both variable names are derived from Source.Product — `widget` yields WIDGET_AUTO_UPGRADE and WIDGET_ALLOW_SUDO — by uppercasing and folding punctuation to underscore.

A product whose updates are silent by design builds its Service WithAutomaticConsent — its own consent, given at build, which Service.AuthoriseUnattendedUpgrade reads and an operator still overrules with <PREFIX>_AUTO_UPGRADE=0. It grants no escalation, and WithoutElevation forbids escalation outright, whatever <PREFIX>_ALLOW_SUDO says (ADR 0150).

Keys that rotate, a signature that names its release, a probe that rolls back

Service.WithVendorKeys links several keys, in order, and a release verifies against any of them: a rotation publishes under the new key while builds that carry both accept it, and neither side has to be updated first (ADR 0150). Service.WithSignatureDomain makes each signature cover a domain — so a key that also signs other documents cannot have one read as a release — and makes the signed manifest say which tag it is and until when it may be installed ("# tag v1.4.0", "# expires 2026-12-31T00:00:00Z"): an older release replayed under a newer name, or a stale one, is refused. Service.WithProbe keeps the previous binary as <binary>.prev and runs the new one with the product's probe arguments; a probe that fails puts the previous one back (CodeProbeFailed).

Companions: a second executable from the same archive

A product that ships more than one executable in its release archive — a thin client beside a daemon — declares the others with Service.WithCompanions. Each one installed beside the running binary is replaced from the archive that was already authenticated, as one set with the binary: every file staged, every previous one kept, then renamed, and a rename or a probe that fails puts back the files already replaced. One that is not installed is not created.

Two limits a caller must know before relying on this

**Without a signature domain, a compromised release host can serve an OLDER signed release.** The historical manifest is signed but carries no release tag, so a host that answers a request for v2 with v1's manifest, signature and archive passes every check and installs v1. WithSignatureDomain closes it by putting the tag and an expiry INSIDE the signed document; a product that has not adopted the format keeps the gap.

**Windows is not supported for the replacement step.** Windows will not let a running executable be renamed over, so on a Windows build Upgrade refuses with the SDK's UNSUPPORTED_PLATFORM — errors.Is(err, proc.UnsupportedPlatform) — before it downloads anything. CheckForUpdate works there: a product can still tell its user that a newer release exists. A build with no vendor key is told about the key first, on every platform.

Both are recorded in ADR 0077 §Deferred rather than left to be discovered, and the Windows refusal in ADR 0095.

What this package does not do

It does not roll back on its own. The replacement is atomic (temp file, chmod, rename) so there is no window where the binary is half-written, and without WithProbe the previous version is gone once the rename lands; with a probe it is kept as <binary>.prev and put back when the probe fails.

Index

Constants

View Source
const CandidateListSentinel string = svcupd.CandidateListSentinel

CandidateListSentinel is the tag value meaning "list the candidates rather than install one".

It exists because a CLI flag taking an optional value has to spell "the flag was given without a value" AS a value. A caller wires it into its own flag definition, so the sentinel has to be reachable from outside the package that consumes it.

View Source
const CodeAPIBodyTooLarge errs.Code = coreupd.CodeAPIBodyTooLarge

CodeAPIBodyTooLarge identifies release metadata over its read cap.

View Source
const CodeArchiveTooLarge errs.Code = coreupd.CodeArchiveTooLarge

CodeArchiveTooLarge identifies a release archive over the in-memory cap.

View Source
const CodeArchiveUnreadable errs.Code = svcupd.CodeArchiveUnreadable

CodeArchiveUnreadable identifies a release archive that would not unpack. It is raised only AFTER the signature and digest have both verified, so it is a packaging or transfer fault and never a supply-chain signal.

View Source
const CodeBinaryNotInArchive errs.Code = coreupd.CodeBinaryNotInArchive

CodeBinaryNotInArchive identifies a verified archive missing the binary.

View Source
const CodeCandidateNotFound errs.Code = coreupd.CodeCandidateNotFound

CodeCandidateNotFound identifies a requested candidate that does not exist.

View Source
const CodeCandidateTagRequired errs.Code = svcupd.CodeCandidateTagRequired

CodeCandidateTagRequired identifies a candidate install asked for without naming which candidate. It wraps errors.ErrUnsupported, so a caller matching that instead still matches.

View Source
const CodeChecksumMismatch errs.Code = coreupd.CodeChecksumMismatch

CodeChecksumMismatch identifies an archive whose digest did not match its authenticated manifest entry.

View Source
const CodeChecksumMissing errs.Code = coreupd.CodeChecksumMissing

CodeChecksumMissing identifies an archive with no entry in the authenticated manifest.

View Source
const CodeDevBuild errs.Code = coreupd.CodeDevBuild

CodeDevBuild identifies a check on a build with no release version.

View Source
const CodeDownloadFailed errs.Code = coreupd.CodeDownloadFailed

CodeDownloadFailed identifies a download that did not complete. It is the transient one: a retry is reasonable.

View Source
const CodeDraftRelease errs.Code = coreupd.CodeDraftRelease

CodeDraftRelease identifies a draft release, which exposes no asset.

View Source
const CodeElevationFailed errs.Code = svcupd.CodeElevationFailed

CodeElevationFailed identifies an escalation the operator DID authorise and that the system refused anyway. Unlike CodeElevationNotAuthorised, no environment variable repairs it.

View Source
const CodeElevationNotAuthorised errs.Code = coreupd.CodeElevationNotAuthorised

CodeElevationNotAuthorised identifies a replacement needing privilege the operator did not opt into. Recoverable, but only through the opt-in.

View Source
const CodeExecutablePathUnresolved errs.Code = svcupd.CodeExecutablePathUnresolved

CodeExecutablePathUnresolved identifies an update that cannot name the file it would replace. Nothing is attempted after it.

View Source
const CodeInsecureRedirect errs.Code = coreupd.CodeInsecureRedirect

CodeInsecureRedirect identifies a redirect leaving HTTPS or over the hop bound.

View Source
const CodeInvalidTag errs.Code = coreupd.CodeInvalidTag

CodeInvalidTag identifies a tag that is not a valid semantic version.

View Source
const CodeNoVendorKey errs.Code = coreupd.CodeNoVendorKey

CodeNoVendorKey identifies an install attempted by a build carrying no vendor key. Match it with errs.HasCode: it is the one refusal a retry never fixes.

View Source
const CodeNotPrerelease errs.Code = coreupd.CodeNotPrerelease

CodeNotPrerelease identifies a tag requested as a candidate that is stable.

View Source
const CodeProbeFailed errs.Code = svcupd.CodeProbeFailed

CodeProbeFailed identifies a replacement whose new binary did not answer the probe the product declared with Service.WithProbe; the previous binary is put back from <binary>.prev when it was kept (ADR 0150).

View Source
const CodeReleaseMetadataUnreadable errs.Code = svcupd.CodeReleaseMetadataUnreadable

CodeReleaseMetadataUnreadable identifies a release-API answer that arrived whole and did not decode. The bytes came, so no transport retry applies.

View Source
const CodeReplacementFailed errs.Code = svcupd.CodeReplacementFailed

CodeReplacementFailed identifies a rename over the running executable that did not land.

View Source
const CodeSignatureInvalid errs.Code = coreupd.CodeSignatureInvalid

CodeSignatureInvalid identifies a manifest signature that did not verify. With CodeSignatureMissing it is the supply-chain pair: no retry helps, and no manual install is safe.

View Source
const CodeSignatureMissing errs.Code = coreupd.CodeSignatureMissing

CodeSignatureMissing identifies a release whose manifest signature was absent.

View Source
const CodeStagingFailed errs.Code = svcupd.CodeStagingFailed

CodeStagingFailed identifies a replacement that failed BEFORE the rename, so the running binary is byte-for-byte as it was. That is the whole difference between this and CodeReplacementFailed, and it is the fact an operator needs.

View Source
const CodeUnexpectedStatus errs.Code = coreupd.CodeUnexpectedStatus

CodeUnexpectedStatus identifies an unusable status from the release host.

View Source
const CodeUnknownArchive errs.Code = coreupd.CodeUnknownArchive

CodeUnknownArchive identifies an asset in an unhandled container format.

Variables

View Source
var (
	// NoVendorKey is returned when the running build carries no vendor key,
	// so nothing could have authenticated the release. A REFUSAL, not a
	// degradation: there is no weaker check to fall back to.
	NoVendorKey = coreupd.NoVendorKey

	// SignatureMissing is returned when the detached signature over the
	// checksum manifest could not be fetched. An unsigned release is never
	// installed and no environment variable reopens that.
	SignatureMissing = coreupd.SignatureMissing

	// SignatureInvalid is returned when the manifest's signature does not
	// verify against the vendor key. With SignatureMissing it is the
	// supply-chain pair: no retry helps and no manual install is safe.
	SignatureInvalid = coreupd.SignatureInvalid

	// ChecksumMissing is returned when the authenticated manifest has no entry
	// for the archive being installed.
	ChecksumMissing = coreupd.ChecksumMissing

	// ChecksumMismatch is returned when the archive's digest does not match
	// its authenticated manifest entry — a corrupted download, or a release
	// whose assets changed after it was signed.
	ChecksumMismatch = coreupd.ChecksumMismatch

	// ArchiveTooLarge is returned when a release archive exceeds the size cap,
	// which exists because the bytes are buffered before anything has vouched
	// for them. It is also what an oversized checksums.txt raises, deliberately
	// — a truncated manifest fails signature verification, and reporting a size
	// problem as a supply-chain one is the worst advice this package can give.
	ArchiveTooLarge = coreupd.ArchiveTooLarge

	// APIBodyTooLarge is returned when a release-metadata response exceeds its
	// read cap.
	APIBodyTooLarge = coreupd.APIBodyTooLarge

	// InsecureRedirect is returned when a redirect leaves HTTPS or exceeds the
	// hop bound. Following it would hand the archive fetch to a plaintext hop.
	InsecureRedirect = coreupd.InsecureRedirect

	// UnexpectedStatus is returned when the release host answers with a status
	// the update path does not accept. Retryable.
	UnexpectedStatus = coreupd.UnexpectedStatus

	// DownloadFailed is returned when a download does not complete, including
	// a transport failure with no HTTP status at all. It is the sentinel a
	// retry policy keys on.
	DownloadFailed = coreupd.DownloadFailed

	// DevBuild is returned when a version check runs on a build carrying no
	// release version, so there is nothing to compare a release against. It is
	// an ANSWER rather than a fault — CheckForUpdate on a `go build` binary
	// reaches it every time — which is why ExplainUpgradeFailure has no case
	// for it: "rebuild from source" is NoVendorKey's advice, not this one's.
	DevBuild = coreupd.DevBuild

	// CandidateNotFound is returned when a requested release candidate does
	// not exist.
	CandidateNotFound = coreupd.CandidateNotFound

	// NotPrerelease is returned when a tag requested as a candidate is
	// reported as a stable release.
	NotPrerelease = coreupd.NotPrerelease

	// DraftRelease is returned for a draft release, which exposes no asset.
	DraftRelease = coreupd.DraftRelease

	// InvalidTag is returned for a tag that is not a valid semantic version.
	InvalidTag = coreupd.InvalidTag

	// BinaryNotInArchive is returned when a verified archive does not contain
	// the expected binary. The archive is authentic; it is a packaging fault
	// rather than an attack.
	BinaryNotInArchive = coreupd.BinaryNotInArchive

	// UnknownArchive is returned for an asset whose container format is not
	// handled.
	UnknownArchive = coreupd.UnknownArchive

	// ElevationNotAuthorised is returned when replacing the binary needs
	// privilege the operator did not opt into. Recoverable — but only through
	// the opt-in, which ExplainUpgradeFailure names.
	ElevationNotAuthorised = coreupd.ElevationNotAuthorised

	// CandidateTagRequired is returned by DownloadCandidate for an empty tag.
	// It wraps errors.ErrUnsupported, so a caller matching that instead still
	// matches.
	CandidateTagRequired = svcupd.CandidateTagRequired

	// ReleaseMetadataUnreadable is returned when the release API answers
	// within its cap and the body does not decode. The bytes arrived, so no
	// transport retry applies.
	ReleaseMetadataUnreadable = svcupd.ReleaseMetadataUnreadable

	// ArchiveUnreadable is returned when a release archive will not unpack. It
	// is raised only AFTER the signature and digest have both verified, so it
	// is a packaging or transfer fault and never a supply-chain signal.
	ArchiveUnreadable = svcupd.ArchiveUnreadable

	// ExecutablePathUnresolved is returned when the process cannot work out
	// which file on disk it runs from. Nothing is attempted after it.
	ExecutablePathUnresolved = svcupd.ExecutablePathUnresolved

	// StagingFailed is returned when the replacement failed BEFORE the rename,
	// so the running binary is byte-for-byte as it was. That is the whole
	// difference between this and ReplacementFailed.
	StagingFailed = svcupd.StagingFailed

	// ReplacementFailed is returned when the rename over the running
	// executable did not land.
	ReplacementFailed = svcupd.ReplacementFailed

	// ElevationFailed is returned when an escalation the operator DID
	// authorise was refused by the system. Unlike ElevationNotAuthorised, no
	// environment variable repairs it.
	ElevationFailed = svcupd.ElevationFailed

	// ProbeFailed is returned when the replaced binary did not answer its
	// probe; rolled_back says whether <binary>.prev was put back.
	ProbeFailed = svcupd.ProbeFailed
)

The sentinels a caller matches with errors.Is, one for each of the codes above.

A facade that publishes a code and withholds the value it identifies is half a surface: a caller can ask errs.HasCode "is this that failure?" and cannot write errors.Is(err, selfupdate.SignatureInvalid), which is the spelling Go programs actually use and the only one that composes with errors.Join. PR #184 fixed exactly this shape in the entitlement facade; lock, cache and authz already publish theirs. This package was the outlier.

They are values rather than type aliases because a sentinel IS a value — there is nothing to alias. The declared type is left off so each keeps its *errs.Error, which is what lock and cache do, and what lets a caller reach Code(), Public() and ExitCode() without a second lookup. Matching works either way: errs.Error.Is compares (Code, Reason), so a sentinel matches a freshly wrapped error of the same identity and not merely the same pointer.

Functions

func StdinIsTerminal

func StdinIsTerminal() bool

StdinIsTerminal reports whether a human could answer a prompt on this process's standard input.

It is a free function rather than something AuthoriseUnattendedUpgrade works out for itself, and that is the point: the consent decision stays testable without a pty, because the CALLER supplies the answer. Pass the result as the interactive argument.

Types

type Candidate

type Candidate = svcupd.CandidateValue

Candidate is one release candidate. It aliases the core value type.

type Copier

type Copier = coreupd.Copier

Copier streams the verified archive to its destination. It aliases the core port.

type FileSystem

type FileSystem = coreupd.FileSystem

FileSystem is the disk half of replacing a running binary. It aliases the core port.

type Getter

type Getter = coreupd.Getter

Getter performs the HTTP GETs a self-update needs. It aliases the core port.

type Service

type Service = svcupd.Service

Service replaces the running binary with a newer signed release. It aliases the service type — the engine handle, per ADR 0074.

func New

func New(version string, src Source) *Service

New returns a Service for the given running version and release source.

The returned Service carries NO vendor key and therefore installs nothing: chain WithVendorKey with the build's linked-in anchor. That is the safe direction — a Service that verified only when a key happened to be present would make the security property depend on a build flag.

func NewWithDeps

func NewWithDeps(version string, src Source, client Getter, fs FileSystem, copier Copier) *Service

NewWithDeps returns a Service with its three ports injected, for a caller that supplies its own HTTP policy or a test that supplies doubles. A nil fs or copier is legal on paths that never reach the disk.

type Source

type Source = svcupd.SourceValue

Source says where releases come from and what they are called. It aliases the service type: these are one engine's construction parameters, which ADR 0074 places with the engine rather than in the contract layer.

type Update

type Update = svcupd.UpdateValue

Update is the outcome of a version check or an install. It aliases the core value type.

Jump to

Keyboard shortcuts

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