Documentation
¶
Overview ¶
Package update installs a newer dcode over the running one.
Nothing here ever runs on its own. An agent that replaces its own binary without being asked defeats every audit the rest of this program is built to support — so the passive notice never applies anything, and Apply is only reachable from an explicit `dcode update` (RN-3).
Spec: docs/specs/architecture/distribution/202608072352-*.
Index ¶
- Constants
- Variables
- func ArtifactName(version, goos, goarch string) string
- func CheckRuns(ctx context.Context, path, wantVersion string) error
- func ExtractBinary(archiveName string, data []byte, dest string) error
- func ParseBool(s string, def bool) bool
- func ParseChecksums(data []byte) (map[string]string, error)
- func ParseInterval(s string) time.Duration
- func PinnedDigest(script []byte, artifact string) string
- func PlatformKey(goos, goarch string) string
- func SaveNotice(path string, n VersionNotice) error
- func VerifySHA256(data []byte, want string) error
- type Artifact
- type Config
- type CosignVerifier
- type GitHub
- type Release
- type Updater
- type Verifier
- type VersionNotice
Constants ¶
const ( DefaultIdentity = "https://github.com/aguinelo/dcode/.github/workflows/release.yml@refs/heads/main" DefaultIssuer = "https://token.actions.githubusercontent.com" )
DefaultIdentity is the workflow allowed to sign releases.
const DefaultAPIURL = "https://api.github.com/repos/aguinelo/dcode/releases"
DefaultAPIURL is where release metadata is read from.
const DefaultBaseURL = "https://github.com/aguinelo/dcode/releases"
DefaultBaseURL is the release origin.
const DefaultInstallerURL = "https://raw.githubusercontent.com/aguinelo/dcode/main/install.sh"
DefaultInstallerURL is the installer on the default branch. The pipeline rewrites its pinned block after publishing, so it carries the digests of the latest release — and it is in git history, which is what makes it a route independent of the release assets.
const DefaultInterval = 24 * time.Hour
DefaultInterval is the minimum gap between checks. Anything below an hour is network noise for no gain — the release cadence does not justify it.
const MinInterval = time.Hour
MinInterval is the floor a configured interval is clamped to.
const NoticeFileName = "update-check.json"
NoticeFileName is where the check result is cached.
Variables ¶
var ErrLocalBuild = errors.New(
"this is a local build, not a published release, so `update` would replace it " +
"with something older. Rebuild with `make install`, or install a release " +
"with the install script")
ErrLocalBuild is returned when the running binary was not published by the release pipeline.
Refusing is the useful answer rather than the cautious one: a build from the working tree is normally *ahead* of the last tag, so replacing it with the latest release is a downgrade wearing the word "update". And a binary we did not publish is one we cannot reason about at all.
var ErrNoVerifier = errors.New("the release signature could not be checked here")
ErrNoVerifier says the signature could not be checked on this machine.
It used to fail the update on its own, which made cosign a requirement for updating at all — a package nobody installs, demanded of a machine that already has a working dcode. What actually needs covering is a substituted release, and the digest committed to the installer covers it too. So this is now one route being unavailable rather than a verdict, and Apply refuses only when neither route held.
var SupportedPlatforms = []string{
"darwin_amd64", "darwin_arm64",
"linux_amd64", "linux_arm64",
}
SupportedPlatforms is the published matrix.
It is exactly the set that has a sandbox backend, and it has to be. Windows was in this list and in the release workflow, and internal/sandbox has no backend for it — so New fails closed there and the binary cannot create a session at all. Publishing an artefact that cannot run is worse than not publishing one: someone downloads it, it installs, it verifies, and then it refuses the first thing they ask, with an error about a sandbox they never chose.
The sandbox spec puts Windows out of the MVP by name. This list now says the same thing, and TestThePublishedMatrixIsExactlyWhatCanRun keeps the two from drifting apart again.
Functions ¶
func ArtifactName ¶
ArtifactName is the stable artifact filename. Third-party scripts depend on this format, which is why it carries the same stability promise as the binary name itself.
func CheckRuns ¶
CheckRuns executes the candidate binary and confirms it reports the version it claims to be.
This runs before the swap, which is what stops a working binary from being replaced by one that does not run on this machine — a wrong architecture, a missing library, a truncated download that still hashed correctly because it was the wrong file all along.
func ExtractBinary ¶
ExtractBinary pulls the dcode binary out of a release archive.
Entry paths are checked rather than trusted: an archive is remote input, and a `../` in a member name is the oldest way there is to write outside the directory you were given.
func ParseBool ¶
ParseBool reads a boolean environment value, defaulting when unset or unparsable — a malformed value must not stop the program from starting.
func ParseChecksums ¶
ParseChecksums reads a `sha256␠␠filename` list into a map keyed by filename.
The signature covers this one file rather than each artifact: one signature for the whole release, while verification stays per-artifact through the hashes it carries.
func ParseInterval ¶
ParseInterval reads DCODE_UPDATE_CHECK_INTERVAL, clamped to MinInterval.
func PinnedDigest ¶ added in v0.1.0
PinnedDigest returns the SHA-256 an installer carries for one artifact, or empty when it carries none.
Only what sits between the markers counts. Reading a digest from anywhere in the file would let an unrelated line — a comment, an example, an error message — decide what gets installed.
func PlatformKey ¶
PlatformKey is the map key inside a Release.
func SaveNotice ¶
func SaveNotice(path string, n VersionNotice) error
SaveNotice writes the cache, creating the directory if needed.
func VerifySHA256 ¶
VerifySHA256 compares a payload against an expected digest.
Types ¶
type Config ¶
type Config struct {
// APIURL and BaseURL exist for an internal mirror. Signature verification
// is required against any origin — a mirror is not a reason to trust.
APIURL string
BaseURL string
Channel string
HTTP *http.Client
Verifier Verifier
// InstallerURL is where the installer that carries this release's digests
// is read from. It is the second route to the expected SHA-256, and the
// only one that does not travel with the artifact.
InstallerURL string
// TargetPath is the binary to replace. Empty means the running one.
TargetPath string
// Pin refuses to change version when set.
Pin string
// GOOS and GOARCH override the platform, for tests.
GOOS, GOARCH string
// AllowLocalOverwrite lets a caller replace a binary the release pipeline
// did not publish. Only the explicit `--force` sets it: the default has to
// be the refusal, or the guard is decoration.
AllowLocalOverwrite bool
}
Config configures the updater.
type CosignVerifier ¶
type CosignVerifier struct {
// Identity and Issuer pin who may sign. Keyless OIDC signing removes the
// long-lived private key, which would otherwise be the highest-value item
// in the entire repository.
Identity string
Issuer string
// Path overrides the cosign binary, for tests.
Path string
// Look overrides binary lookup, for tests.
Look func(string) (string, error)
}
CosignVerifier verifies with the cosign binary.
Shelling out rather than linking sigstore: the verification path is then the same one a user can run by hand to reproduce the result, and the trust root is a tool they can audit independently of this program.
type GitHub ¶
type GitHub struct {
// contains filtered or unexported fields
}
GitHub is the released-artifact updater.
type Release ¶
type Release struct {
Version string `json:"version"`
Artifacts map[string]Artifact `json:"artifacts"`
}
Release is one published version and everything needed to install it.
type Updater ¶
type Updater interface {
// Latest reports the most recent release. A network error is NOT fatal to
// the passive notice (RN-4).
Latest(ctx context.Context) (Release, error)
// Apply downloads, VERIFIES and swaps the binary atomically (RN-5).
Apply(ctx context.Context, r Release) error
}
Updater is the update surface. Two operations, deliberately: everything that looks is separate from the one thing that writes.
type Verifier ¶
type Verifier interface {
Verify(ctx context.Context, checksums, signature, certificate []byte) error
}
Verifier checks that a checksums file was signed by the project's release identity.
type VersionNotice ¶
type VersionNotice struct {
Current string `json:"current"`
Latest string `json:"latest"`
CheckedAt time.Time `json:"checked_at"`
}
VersionNotice is the cached result of a version check.
It is persisted to disk and NEVER placed in the model's context. A timestamp that changes between turns would break the append-only prefix, and the resulting cache miss would cost far more than the notice is worth (ADR-03).
func Check ¶
func Check(ctx context.Context, u Updater, path, current string, now time.Time, interval time.Duration) VersionNotice
Check refreshes the notice at most once per interval.
A network failure is silent by contract: it is logged at debug level and the cached answer is returned unchanged. Checking for a version can never degrade the use of the tool, and it never changes an exit code (RN-4).
func LoadNotice ¶
func LoadNotice(path string) (VersionNotice, bool)
LoadNotice reads the cached check. A missing or corrupt cache is not an error: the worst it costs is one extra request.
func (VersionNotice) Message ¶
func (n VersionNotice) Message() string
Message is the one line a client shows. Empty when there is nothing to say.
func (VersionNotice) Outdated ¶
func (n VersionNotice) Outdated() bool
Outdated reports whether a newer version exists.