Documentation
¶
Overview ¶
Package lockfile manages the project-level lock file (toolhive.lock.yaml). The lock file pins the exact name, version, source, and digests of every project-scoped skill and plugin install so a team can restore ("thv skill sync" / "thv ai-plugin sync") or refresh ("thv skill upgrade" / "thv ai-plugin upgrade") the pinned state on any machine. RFC THV-0080 owns the version and skills: keys; plugins: is the sibling key ratified by the AI-plugin lock track.
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 RemovePluginEntry(root Root, name string) error
- func Update(root Root, fn func(*Lockfile) error) error
- func UpsertEntry(root Root, entry Entry) error
- func UpsertPluginEntry(root Root, entry Entry) error
- type ContentFile
- type Entry
- type Lockfile
- func (l *Lockfile) Get(name string) (Entry, bool)
- func (l *Lockfile) GetPlugin(name string) (Entry, bool)
- func (l *Lockfile) Remove(name string) bool
- func (l *Lockfile) RemoveParentFromRequiredBy(parent string) []string
- func (l *Lockfile) RemovePlugin(name string) bool
- func (l *Lockfile) Save(root Root) error
- func (l *Lockfile) Upsert(entry Entry)
- func (l *Lockfile) UpsertPlugin(entry Entry)
- 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 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 skills: 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 RemovePluginEntry ¶ added in v0.43.0
RemovePluginEntry loads the lock file, removes the named plugins: 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 a skills: entry, and saves it back, all under a single file lock so concurrent installs cannot race on read-modify-write.
func UpsertPluginEntry ¶ added in v0.43.0
UpsertPluginEntry loads the lock file, upserts a plugins: 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 artifact's unique name within its key (skills: or plugins:).
// The same name may appear once under each key.
Name string `yaml:"name"`
// Version is the artifact's declared version, if any (SKILL.md frontmatter
// or .claude-plugin/plugin.json).
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
// file set (skill directory, or the canonical plugin tree ExtractPlugin
// writes), 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 names in the same key (skills: or plugins:)
// for transitively materialized dependencies. Plugin v1 does not
// populate this; the field is reserved for when plugin requires are
// materialized.
RequiredBy []string `yaml:"requiredBy,omitempty"`
// Explicit is true when the user directly installed this artifact;
// 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 or plugin 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"`
// Plugins is the set of pinned plugin installations, sorted by name
// for stable diffs. Validated as its own name/requiredBy graph, so a
// skill and a plugin may share a name.
Plugins []Entry `yaml:"plugins,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) GetPlugin ¶ added in v0.43.0
GetPlugin returns the plugins: entry for name, if present.
func (*Lockfile) Remove ¶
Remove deletes the skills: entry with the given name, if present. Reports whether an entry was removed.
func (*Lockfile) RemoveParentFromRequiredBy ¶
RemoveParentFromRequiredBy removes parent from every skills: entry's RequiredBy list and returns the names of entries that lost their last parent and are not explicit — the candidates for cascade removal. It does not touch plugins:.
func (*Lockfile) RemovePlugin ¶ added in v0.43.0
RemovePlugin deletes the plugins: entry with the given name, if present. Reports whether an entry was removed.
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.
func (*Lockfile) Upsert ¶
Upsert inserts or replaces the skills: entry with a matching name, keeping the slice sorted by name for stable diffs. It does not touch plugins:.
func (*Lockfile) UpsertPlugin ¶ added in v0.43.0
UpsertPlugin inserts or replaces the plugins: entry with a matching name, keeping the slice sorted by name for stable diffs. It does not touch skills:.
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"`
// RepositoryRef is the git ref the signing workflow ran on, from Fulcio
// certificate extension 1.3.6.1.4.1.57264.1.14, when present. Empty
// means unconstrained: every lock file written before this field existed
// omits it, and such entries must keep verifying against any ref.
RepositoryRef string `yaml:"repositoryRef,omitempty"`
// RunnerEnvironment is the runner class the signing workflow executed in
// (e.g. "github-hosted"), from Fulcio certificate extension
// 1.3.6.1.4.1.57264.1.11, when present. Empty means unconstrained, for
// the same backward-compatibility reason as RepositoryRef.
RunnerEnvironment string `yaml:"runnerEnvironment,omitempty"`
// SigstoreURL is the Sigstore instance the signature chains to.
SigstoreURL string `yaml:"sigstoreUrl,omitempty"`
// Provisional marks provenance whose verification has a documented
// gap — currently git-commit signatures, verified for signature and
// certificate chain but without transparency-log proof of signing
// time. Visible in lock diffs so reduced assurance is never silent;
// removed when the gap closes.
Provisional bool `yaml:"provisional,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.