Documentation
¶
Overview ¶
Package lockfile manages the project-level skills lock file (toolhive.lock.yaml). The lock file pins the exact name, version, source, and digests of every project-scoped skill install so a team can restore ("thv skill sync") or refresh ("thv skill upgrade") the pinned state on any machine. See RFC THV-0080.
All filesystem access goes through Root, a capability type that can only be constructed from a validated project root. Root confines every read and write to that directory using os.Root (OS-enforced containment, not just string validation), so no function in this package can be made to open a path outside the project root regardless of what name is requested.
Index ¶
- Constants
- Variables
- func ContentDigest(files []ContentFile) (string, error)
- func ContentDigestFromDir(skillDir string) (string, error)
- func RemoveEntry(root Root, name string) error
- func Update(root Root, fn func(*Lockfile) error) error
- func UpsertEntry(root Root, entry Entry) error
- type ContentFile
- type Entry
- type Lockfile
- type Provenance
- type Root
Constants ¶
const (
// ContentDigestPrefix is the prefix for contentDigest values in the lock file.
ContentDigestPrefix = "sha256:"
)
const CurrentVersion = 1
CurrentVersion is the schema version written to new lock files. Loading a lock file with a different version is a hard error, never a silent partial parse.
const FileName = "toolhive.lock.yaml"
FileName is the name of the project-level skills lock file, written to the project root alongside .git.
Variables ¶
var ErrUnsupportedVersion = errors.New("unsupported lock file version")
ErrUnsupportedVersion indicates the lock file schema version is not supported by this build.
Functions ¶
func ContentDigest ¶
func ContentDigest(files []ContentFile) (string, error)
ContentDigest computes a deterministic SHA-256 dirhash over files.
Algorithm (content-only; file modes and timestamps are ignored):
- Normalize each path to slash-separated form and sort files by path.
- For each file, feed path + "\x00" + sha256(content) + "\n" into a running SHA-256.
- Return "sha256:" + hex(aggregate).
The algorithm is frozen: lock files in the wild pin its output, so any change would report every installed skill as drifted. Golden vectors in the package tests guard against accidental changes.
Known limitations (accepted, like go.sum's): the digest covers file content only — file modes (e.g. an exec-bit flip) are invisible to it, and ContentDigestFromDir skips symlinks and other non-regular files entirely. Trust in the file set's provenance is the Sigstore verification layer's job, not this integrity pin's.
func ContentDigestFromDir ¶
ContentDigestFromDir walks skillDir and computes the content digest from the on-disk file set. The walk is confined to skillDir via os.Root, so symlinks cannot escape it.
func RemoveEntry ¶
RemoveEntry loads the lock file, removes the named entry if present, and saves it back, all under a single file lock. Removing an entry that does not exist is a no-op, not an error.
func Update ¶
Update loads the lock file, applies fn, and saves the result, all under a single file lock. If fn returns an error the lock file is left unchanged.
func UpsertEntry ¶
UpsertEntry loads the lock file, upserts entry, and saves it back, all under a single file lock so concurrent installs cannot race on read-modify-write.
Types ¶
type ContentFile ¶
type ContentFile struct {
// Path is the relative path within the skill directory, using either
// native or slash separators.
Path string
// Content is the raw file bytes.
Content []byte
}
ContentFile is a single file used for contentDigest computation.
type Entry ¶
type Entry struct {
// Name is the skill's unique name.
Name string `yaml:"name"`
// Version is the skill's declared version, if any (from SKILL.md frontmatter).
Version string `yaml:"version,omitempty"`
// Source is exactly what the user (or the registry resolver) originally
// requested — a plain registry name, an OCI reference, or a git://
// reference. It is never rewritten; upgrade re-resolves this value to
// check for newer content.
Source string `yaml:"source"`
// ResolvedReference is the concrete OCI reference or git:// URL that
// Source resolved to at install time.
ResolvedReference string `yaml:"resolvedReference,omitempty"`
// Digest pins the exact content installed: an OCI "sha256:..." manifest
// digest or a full git commit hash.
Digest string `yaml:"digest"`
// ContentDigest is a deterministic SHA-256 dirhash of the materialized
// skill file set, used for on-disk integrity verification.
ContentDigest string `yaml:"contentDigest,omitempty"`
// Provenance records the verified signer identity of the installed
// artifact — the trust-on-first-use anchor later installs, syncs, and
// upgrades are checked against. Nil for entries recorded before
// verification existed and for unsigned entries (see Unsigned); an
// entry never carries both.
Provenance *Provenance `yaml:"provenance,omitempty"`
// Unsigned is true when the artifact carried no signature and the user
// explicitly accepted that with --allow-unsigned. It is a recorded
// trust decision, not an omission: replacing an unsigned entry with a
// signed one clears it, and a signed entry never becomes unsigned
// without the same explicit flag.
Unsigned bool `yaml:"unsigned,omitempty"`
// RequiredBy lists parent skill names for transitively materialized
// dependencies (skills declared via toolhive.requires).
RequiredBy []string `yaml:"requiredBy,omitempty"`
// Explicit is true when the user directly installed this skill; explicit
// entries are exempt from cascade removal when RequiredBy becomes empty.
Explicit bool `yaml:"explicit,omitempty"`
// Extra round-trips fields this binary does not know about, so a
// Load→modify→Save cycle by an older binary preserves rather than strips
// them. It applies per entry: an entry this binary rewrites (reinstall,
// upgrade) is built fresh, so its unknown fields — which described the
// previous install — are intentionally dropped along with it.
Extra map[string]any `yaml:",inline"`
}
Entry represents a single pinned skill installation in the lock file.
type Lockfile ¶
type Lockfile struct {
// Version is the lock file schema version.
Version int `yaml:"version"`
// Skills is the set of pinned skill installations, sorted by name for
// stable diffs.
Skills []Entry `yaml:"skills,omitempty"`
// Extra round-trips unknown top-level fields, mirroring Entry.Extra.
Extra map[string]any `yaml:",inline"`
}
Lockfile is the parsed contents of a project's toolhive.lock.yaml.
func Load ¶
Load reads and parses the lock file for root. A missing lock file is not an error — it returns an empty Lockfile ready to be populated. A lock file with an unsupported schema version is a hard error.
func (*Lockfile) Remove ¶
Remove deletes the entry with the given name, if present. Reports whether an entry was removed.
func (*Lockfile) RemoveParentFromRequiredBy ¶
RemoveParentFromRequiredBy removes parent from every entry's RequiredBy list and returns the names of entries that lost their last parent and are not explicit — the candidates for cascade removal.
func (*Lockfile) Save ¶
Save writes the lock file into root atomically (temp file + rename), after validating it with the same rules Load enforces on read. This prevents a caller bug from writing a lock file that every subsequent Load/Update call would then hard-fail on, with no recovery path through this package. Callers that need read-modify-write atomicity across processes must use UpsertEntry, RemoveEntry, or Update instead of Load+Save — Save alone is write-atomic but does nothing to serialize concurrent read-modify-write cycles.
type Provenance ¶
type Provenance struct {
// SignerIdentity is the certificate subject identity: for GitHub
// Actions certificates, the workflow path relative to the repository;
// otherwise the certificate SAN verbatim (a URI, email, or SPIFFE ID).
SignerIdentity string `yaml:"signerIdentity"`
// CertIssuer is the OIDC issuer that authenticated the signer.
CertIssuer string `yaml:"certIssuer"`
// RepositoryURI is the source repository from the Fulcio certificate
// extensions, when present.
RepositoryURI string `yaml:"repositoryUri,omitempty"`
// SigstoreURL is the Sigstore instance the signature chains to.
SigstoreURL string `yaml:"sigstoreUrl,omitempty"`
}
Provenance is the Sigstore signer identity recorded for a verified entry.
type Root ¶
type Root struct {
// contains filtered or unexported fields
}
Root is a validated project root directory. It is the only way to address a lock file on disk: constructing one runs full project-root validation (absolute, NUL-free, no traversal segments, symlink-canonical, git-rooted). Every read and write additionally goes through a freshly opened os.Root scoped to the validated directory, so access is confined at the OS level.