Help us shape the future of Go! Take the 2026 Go Developer Survey

spec

package
v0.29.0 Latest Latest
Warning

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

Go to latest
Published: Sep 17, 2026 License: Apache-2.0 Imports: 12 Imported by: 1

Documentation

Overview

Package spec defines the core types for Strata profiles, layers, formations, and lockfiles. These types are the contract between users, the registry, the resolver, and the agent. All other Strata components are built on these.

Index

Constants

View Source
const (
	// VerifyAttestationReferencesPresent: every layer named a bundle and a Rekor
	// entry, and resolution refused any layer that did not (stage 7 in strict
	// mode). The name says references, not attestation, deliberately: resolution
	// holds bundle URIs, not bytes, so it establishes that the pointers are
	// present — not that the bundle they point at is a valid signature. Bundle
	// syntax and signature are verified later, at the boundaries that hold the
	// bytes (strata verify --rekor, strata run). A consumer must not read this
	// label as "cryptographically verified".
	VerifyAttestationReferencesPresent = "attestation-references-present"
	// VerifyUnsignedOffline: at least one layer was accepted with no bundle or
	// Rekor entry, on the offline-catalog path (AllowUnsignedOffline). A lockfile
	// carrying this was not a trust decision.
	VerifyUnsignedOffline = "unsigned-offline"
)

Verification policies recorded in LockFile.VerificationPolicy. They name the strongest check the resolver performed on every layer's attestation when it produced the lockfile. Resolution holds bundle URIs, not bundle bytes, so it never verifies against the transparency log (that happens at strata verify --rekor and strata run, where the bytes exist — #85); these two values are what a resolve can honestly claim.

View Source
const LayerCacheExt = ".sqfs"

LayerCacheExt is the file extension of a cached layer squashfs image.

Variables

This section is empty.

Functions

func CompareVersions

func CompareVersions(a, b string) int

CompareVersions compares two dotted version strings numerically, returning -1, 0 or 1 analogous to strings.Compare. Exported so callers outside this package can rank providers by version without reimplementing the comparison — internal/resolver needs it to pick among several satisfying providers, and a second implementation there would be a second chance to disagree.

func FileDigest

func FileDigest(path string) (string, error)

FileDigest returns the lowercase hex SHA256 of the file at path.

func HumanizeAge added in v0.28.0

func HumanizeAge(d time.Duration) string

HumanizeAge renders a duration in its largest whole unit at or above one — days, then hours, minutes, seconds — so an age reads "412 days" or "3 hours" rather than "9888h0m0s". A negative duration is rendered by magnitude; callers that care about direction check the sign themselves (see FreshnessNote).

func LayerCachePath

func LayerCachePath(cacheDir, digest string) (string, error)

LayerCachePath returns the path of the cached squashfs for a layer with the given digest, or an error if the digest is not well-formed.

The validation is the point of the function. filepath.Join calls Clean, which resolves ".." rather than rejecting it, so joining an unvalidated digest can name a file outside cacheDir. Every site that builds a layer cache path must go through here so that the filename is always the digest and nothing else.

func MarshalLockFile

func MarshalLockFile(l *LockFile) ([]byte, error)

MarshalLockFile serializes a LockFile to YAML bytes.

func MarshalProfile

func MarshalProfile(p *Profile) ([]byte, error)

MarshalProfile serializes a Profile to YAML bytes.

func ParseMaxAge added in v0.28.0

func ParseMaxAge(s string) (time.Duration, error)

ParseMaxAge parses a max-age bound. It accepts a "d" (day) or "w" (week) suffix, which Go's time.ParseDuration does not, and otherwise defers to time.ParseDuration (so "720h", "30m" work). An empty string means no bound (returns 0). The result must not be negative.

func ValidateLayerDigest

func ValidateLayerDigest(digest string) error

ValidateLayerDigest reports whether digest is a well-formed layer digest: exactly 64 lowercase hex characters.

Lowercase is required rather than merely accepted. Every digest producer in this module goes through hex.EncodeToString, which emits lowercase, and every consumer compares raw strings with !=, so an uppercase digest can never match a file this module hashed. Accepting one would only create a second cache entry for the same bytes that is guaranteed to fail verification.

An empty digest is an error, not a permitted "unverified" state: a layer with no digest cannot be checked against anything, and the caller that is about to read its bytes has no way to tell content from substituted content.

func ValidateLayerID

func ValidateLayerID(id string) error

ValidateLayerID reports whether id is safe to use as a single filesystem path component.

A layer ID names a mount point, a cached squashfs file, and an OCI unpack directory — each built with filepath.Join, which calls Clean and therefore *resolves* ".." rather than rejecting it. So an id like "../../etc/cron.d/x" joined to a base directory escapes that directory, and at the mount site the escape drives os.MkdirAll — a write primitive. Every site that builds a path from a layer ID must call this first; the comment that once claimed filepath.Join "prevents escape" was exactly backwards.

A valid id is a non-empty single path element: no path separator, not "." or "..", no NUL byte, and local by filepath.IsLocal. Layer IDs are machine-generated in the form name-version-abi-arch (e.g. "python-3.13.2-linux-gnu-2.34-x86_64"), so this rejects only ids that could not have come from the resolver — an escaping id is a tampered or hand-written lockfile, which is precisely the case that must not reach a filesystem operation.

func ValidatePackageSpec

func ValidatePackageSpec(p PackageSpec) error

ValidatePackageSpec checks that a PackageSpec is well-formed.

func VerifyFileDigest

func VerifyFileDigest(path, digest string) error

VerifyFileDigest hashes the file at path and compares it to digest, which must itself be well-formed. It returns a *DigestMismatchError when the bytes on disk are not the bytes the digest names.

Types

type BaseCapabilities

type BaseCapabilities struct {
	// AMIID is the EC2 AMI ID this probe was run against.
	AMIID string `yaml:"ami_id" json:"ami_id"`

	// OS is the declared OS identifier, e.g. "al2023".
	OS string `yaml:"os" json:"os"`

	// Arch is the architecture, e.g. "x86_64".
	Arch string `yaml:"arch" json:"arch"`

	// ABI is the C runtime ABI identifier used for layer catalog filtering.
	// "linux-gnu-2.34" covers AL2023, Rocky 9/10, RHEL 9.
	// "linux-gnu-2.35" covers Ubuntu 22.04, Debian 12.
	ABI string `yaml:"abi" json:"abi"`

	// ProbedAt is when this capability set was generated.
	ProbedAt time.Time `yaml:"probed_at" json:"probed_at"`

	// SystemCompiler is the exact package identifier of the system C compiler.
	// Populated by the probe script; format is ABI-specific but always
	// uniquely identifies the compiler version and build:
	//   linux-gnu (rpm):  rpm NVR, e.g. "gcc-11.4.1-2.amzn2023.0.1.x86_64"
	//   linux-gnu (dpkg): dpkg NVR, e.g. "gcc-11-11.4.0-1ubuntu1~22.04-amd64"
	// Used as LayerManifest.BootstrapCompiler for Tier 0 bootstrap builds.
	SystemCompiler string `yaml:"system_compiler" json:"system_compiler"`

	// Provides is the list of capabilities the base OS provides.
	// Populated by the probe script running on the actual AMI.
	// Includes: glibc, kernel, systemd, rpm/dpkg version, compiler basics.
	Provides []Capability `yaml:"provides" json:"provides"`
}

BaseCapabilities describes the capabilities of a base AMI. Produced by the probe pipeline, cached by AMI ID. Never hand-written.

func (BaseCapabilities) ContentDigest

func (b BaseCapabilities) ContentDigest() string

ContentDigest returns a hex SHA256 identifying the base by its declared capabilities. Stage 8 records it as ResolvedBase.AMISHA256 — the value IsFrozen requires and that participates in EnvironmentID.

It is a digest of the capability *record* — AMI ID, OS, arch, ABI, system compiler, and the provided-capability set — not of the AMI's raw bytes: no shipped code boots or reads the base image, and docs/build-provenance-chain.md already treats a hash of this record as the base identity. ProbedAt is excluded so the digest is reproducible across probes; every other field participates, so a change to any of them is a different base.

func (BaseCapabilities) HasCapability

func (b BaseCapabilities) HasCapability(name, minVersion string) bool

HasCapability reports whether the base provides the named capability at or above the specified minimum version. An empty minVersion means any version satisfies.

Scans every entry rather than deciding on the first name match: a base providing mpi@1.0.0 and mpi@3.0.0 in that order satisfies mpi >= 2.0.0. No shipped caller reaches this today (only tests do), but it is exported, and the first-match form was the same defect as #67's stage-4 half waiting for a caller — so it is fixed here rather than left as a trap.

func (BaseCapabilities) SatisfiesRequirement

func (b BaseCapabilities) SatisfiesRequirement(r Requirement) bool

SatisfiesRequirement reports whether the base satisfies a Requirement.

Any provider satisfying r is enough. The previous form returned on the first entry whose NAME matched, so a base providing python@3.9 before python@3.13 was reported as failing python >= 3.10 even though a satisfying provider was present — #67's stage-4 half, which could reject a satisfiable profile.

type BaseRef

type BaseRef struct {
	// OS is the operating system identifier.
	// Supported: "al2023", "rocky9", "rocky10", "ubuntu24"
	OS string `yaml:"os" json:"os"`

	// Arch is the target architecture.
	// Supported: "x86_64" (default), "arm64"
	Arch string `yaml:"arch,omitempty" json:"arch,omitempty"`
}

BaseRef identifies the target OS and architecture for environment assembly. The OS string is resolved to an AMI ID via SSM parameter lookup or an alias table. The resolved AMI is then probed for its capabilities.

func (BaseRef) NormalizedArch

func (b BaseRef) NormalizedArch() string

NormalizedArch returns the architecture string, defaulting to x86_64.

func (BaseRef) Validate

func (b BaseRef) Validate() error

Validate checks that a BaseRef is well-formed.

type BuildEnvironment added in v0.29.0

type BuildEnvironment struct {
	// AMIID is the base AMI the build ran on — a fixed image, so it is itself a
	// pinned input.
	AMIID string `yaml:"ami_id,omitempty" json:"ami_id,omitempty"`

	// Releasever is the AL2023 dnf releasever snapshot the toolchain was resolved
	// from. Pinned to the AMI's own version so the same recipe resolves the same
	// packages over time rather than whatever the repos currently serve.
	Releasever string `yaml:"releasever,omitempty" json:"releasever,omitempty"`

	// Packages is the exact NVR of every OS package installed on the build
	// instance (e.g. "gcc-11.4.1-2.amzn2023.0.1.x86_64"), sorted and de-duplicated
	// so the recorded set is order-stable regardless of how rpm -qa emitted it.
	Packages []string `yaml:"packages,omitempty" json:"packages,omitempty"`
}

BuildEnvironment records the pinned OS toolchain a layer was built against (#234, properties B2/B3). Every EC2 build installs a compiler toolchain and -devel headers from the AL2023 repos before compiling; recording them here names every build input (B2) rather than leaving it to the moving repos, and pinning the dnf releasever to the fixed AMI's own snapshot makes the toolchain deterministic (B3, up to the AL2023 versioned repo and the fixed AMI).

It extends the manifest's BootstrapCompiler — which named only gcc — to the whole toolchain. A layer built purely from Strata layers records those in BuiltWith instead and leaves this empty. A nil or empty value means the build environment was not recorded, which does not discharge B2 (see Recorded).

func NewBuildEnvironment added in v0.29.0

func NewBuildEnvironment(amiID, releasever string, packages []string) *BuildEnvironment

NewBuildEnvironment builds a BuildEnvironment with packages sorted and de-duplicated. A build's provenance must not depend on enumeration order — the same determinism EnvironmentID demands elsewhere — so two builds that installed the same packages in a different order record an identical set.

func (*BuildEnvironment) Recorded added in v0.29.0

func (e *BuildEnvironment) Recorded() bool

Recorded reports whether this build environment actually names its inputs: an AMI and at least one package. It is the predicate B2 turns on — an unset or half-filled BuildEnvironment does not discharge "every build input is named".

type Capability

type Capability struct {
	Name    string `yaml:"name" json:"name"`
	Version string `yaml:"version" json:"version"`
}

Capability is a named, versioned capability that a layer provides or requires. The version string is a semver-compatible value.

Examples:

{Name: "python",  Version: "3.11.9"}
{Name: "cuda",    Version: "12.3.2"}
{Name: "glibc",   Version: "2.34"}
{Name: "mpi",     Version: "3.1"}
{Name: "abi",     Version: "linux-gnu-2.34"}  // C runtime ABI identifier

func (Capability) Satisfies

func (c Capability) Satisfies(r Requirement) bool

Satisfies reports whether c satisfies r: the names match and c's version is within r's bounds. MinVersion is inclusive, MaxVersion exclusive, and an empty bound is unbounded in that direction.

This is the single definition of capability satisfaction. It exists because #67 was two stages disagreeing about it: stage 4 compared versions and stage 6 did not, so a profile could be validated against one provider and wired to another. Three call sites walked a Provides slice with their own inline version logic; they now share this one, which is what stops the two rules drifting apart again rather than merely realigning them once.

func (Capability) String

func (c Capability) String() string

String returns "name@version".

type DigestMismatchError

type DigestMismatchError struct {
	Path string // file that was hashed
	Want string // declared digest
	Got  string // digest of the bytes on disk
}

DigestMismatchError reports a file whose contents do not hash to the digest that was declared for it.

func (*DigestMismatchError) Error

func (e *DigestMismatchError) Error() string

type Formation

type Formation struct {
	Name        string `yaml:"name" json:"name"`
	Version     string `yaml:"version" json:"version"`
	Description string `yaml:"description,omitempty" json:"description,omitempty"`

	// Layers is the ordered list of software refs in this formation.
	// These are resolved to specific LayerManifests at formation build time.
	Layers []SoftwareRef `yaml:"layers" json:"layers"`

	// Provides is the union of capabilities provided by all layers.
	// Computed at formation build time.
	Provides []Capability `yaml:"provides" json:"provides"`

	// Sigstore attestation — the formation is signed as a unit.
	RekorEntry string `yaml:"rekor_entry" json:"rekor_entry"`
	Bundle     string `yaml:"bundle" json:"bundle"`
	SignedBy   string `yaml:"signed_by" json:"signed_by"`

	BuiltAt time.Time `yaml:"built_at" json:"built_at"`
}

Formation is a named, versioned, pre-validated group of layers. Formations are conflict-checked and smoke-tested as a unit before entering the registry. They are the primary way users compose multi-layer environments.

type HostRequirement

type HostRequirement struct {
	Key   string `yaml:"key" json:"key"`
	Value string `yaml:"value" json:"value"`
}

HostRequirement declares a single ambient host capability requirement. Key is the capability name (e.g. "cuda-driver", "efa"). Value is a version constraint or boolean string (e.g. ">=525", "true").

type InstanceConfig

type InstanceConfig struct {
	// Type is the EC2 instance type, e.g. "p4d.24xlarge".
	Type string `yaml:"type,omitempty" json:"type,omitempty"`

	// Spot requests a spot instance.
	Spot bool `yaml:"spot,omitempty" json:"spot,omitempty"`

	// SpotFallback is an alternative instance type if the primary is unavailable.
	SpotFallback string `yaml:"spot_fallback,omitempty" json:"spot_fallback,omitempty"`

	// Placement is the EC2 placement strategy, e.g. "cluster".
	Placement string `yaml:"placement,omitempty" json:"placement,omitempty"`

	// Region overrides the default AWS region.
	Region string `yaml:"region,omitempty" json:"region,omitempty"`
}

InstanceConfig describes the EC2 instance to launch.

type LayerManifest

type LayerManifest struct {
	// Identity
	ID      string `yaml:"id" json:"id"`           // e.g. "python-3.11.9-linux-gnu-2.34-x86_64"
	Name    string `yaml:"name" json:"name"`       // e.g. "python"
	Version string `yaml:"version" json:"version"` // e.g. "3.11.9"

	// Content
	Source string `yaml:"source" json:"source"` // s3://strata-layers/...
	SHA256 string `yaml:"sha256" json:"sha256"` // hex SHA256 of squashfs
	Size   int64  `yaml:"size" json:"size"`     // bytes

	// ContentManifest maps file path to SHA256 of file content for every
	// file in the layer. Used for conflict detection at resolve time.
	ContentManifest map[string]string `yaml:"content_manifest" json:"content_manifest"`

	// Sigstore attestation — mandatory, populated at build time.
	RekorEntry    string `yaml:"rekor_entry" json:"rekor_entry"`       // Rekor log entry ID
	Bundle        string `yaml:"bundle" json:"bundle"`                 // cosign bundle path in registry
	SignedBy      string `yaml:"signed_by" json:"signed_by"`           // signing key identity
	CosignVersion string `yaml:"cosign_version" json:"cosign_version"` // e.g. "v3.0.5"

	// Capability contract — what this layer provides and requires.
	Provides []Capability  `yaml:"provides" json:"provides"`
	Requires []Requirement `yaml:"requires" json:"requires"`

	// Registry metadata.
	Arch    string    `yaml:"arch" json:"arch"` // "x86_64", "arm64"
	ABI     string    `yaml:"abi" json:"abi"`   // e.g. "linux-gnu-2.34", "linux-gnu-2.35"
	BuiltAt time.Time `yaml:"built_at" json:"built_at"`

	// UserSelectable is false for dependency-only layers (ucx, hwloc, pmix, libfabric)
	// that are resolved transitively but never shown in default strata search output
	// or allowed as top-level user software choices.
	UserSelectable bool `yaml:"user_selectable" json:"user_selectable"`

	// InstallLayout describes how the recipe installs into the squashfs.
	// "versioned" (default): installs to /<name>/<version>/
	// "flat": installs directly to / (used by glibc, musl)
	InstallLayout string `yaml:"install_layout,omitempty" json:"install_layout,omitempty"`

	// HasModulefile is true when the build pipeline generated an Lmod
	// modulefile (modulefiles/<name>/<version>.lua) inside the squashfs.
	// Set for all non-flat layouts by the build pipeline after a successful
	// GenerateModulefile call.
	HasModulefile bool `yaml:"has_modulefile,omitempty" json:"has_modulefile,omitempty"`

	// Build provenance — what produced this layer.
	RecipeSHA256      string     `yaml:"recipe_sha256" json:"recipe_sha256"`                               // SHA256 of build.sh
	BuildEnvLockID    string     `yaml:"build_env_lock_id" json:"build_env_lock_id"`                       // SHA256 of BaseCapabilities probe (bootstrap) or resolved layer lockfile
	BuiltWith         []LayerRef `yaml:"built_with,omitempty" json:"built_with,omitempty"`                 // Strata layers that formed the build env (Tier 0.5+)
	BootstrapBuild    bool       `yaml:"bootstrap_build,omitempty" json:"bootstrap_build,omitempty"`       // true = Tier 0, built with OS system compiler
	BootstrapCompiler string     `yaml:"bootstrap_compiler,omitempty" json:"bootstrap_compiler,omitempty"` // exact system compiler package, e.g. "gcc-11.4.1-2.amzn2023.0.1.x86_64"

	// BuildEnvironment names the pinned OS toolchain this layer was built against —
	// the base AMI, the dnf releasever snapshot, and the exact NVR of every OS
	// package the build installed — so every build input is named (B2, #234) and
	// the toolchain is deterministic up to the AL2023 versioned repo and the fixed
	// AMI (B3). It supersedes BootstrapCompiler's single-package record for
	// OS-toolchain builds. nil = not recorded.
	BuildEnvironment *BuildEnvironment `yaml:"build_environment,omitempty" json:"build_environment,omitempty"`

	// CaptureSource records how this layer was created without a recipe.
	// Values: "lmod", "conda", "filesystem", "fold". Empty for recipe-built layers.
	CaptureSource string `yaml:"capture_source,omitempty" json:"capture_source,omitempty"`

	// FoldedFrom lists SHA256 hashes of the source layers that were merged
	// to produce this layer. Populated only for layers created by strata fold.
	FoldedFrom []string `yaml:"folded_from,omitempty" json:"folded_from,omitempty"`

	// OriginalPrefix is the install root from which the layer was captured.
	OriginalPrefix string `yaml:"original_prefix,omitempty" json:"original_prefix,omitempty"`

	// Normalized is true if absolute paths were rewritten during capture.
	Normalized bool `yaml:"normalized,omitempty" json:"normalized,omitempty"`
}

LayerManifest is what the registry knows about a layer. It is produced by the build pipeline and never hand-written. Every field is populated before the layer enters the registry.

type LayerProvenance

type LayerProvenance struct {
	Name       string `yaml:"name" json:"name"`
	Version    string `yaml:"version" json:"version"`
	SHA256     string `yaml:"sha256" json:"sha256"`
	RekorEntry string `yaml:"rekor_entry" json:"rekor_entry"`
}

LayerProvenance is the attestation summary for a single layer.

type LayerRef

type LayerRef struct {
	Name    string `yaml:"name" json:"name"`
	Version string `yaml:"version" json:"version"`
	SHA256  string `yaml:"sha256" json:"sha256"`
	Rekor   string `yaml:"rekor_entry" json:"rekor_entry"`
}

LayerRef is a compact reference to a specific layer used in a build environment. It records enough information to independently verify the build provenance via Rekor.

type LockFile

type LockFile struct {
	// Identity
	ProfileName   string    `yaml:"profile" json:"profile"`
	ProfileSHA256 string    `yaml:"profile_sha256" json:"profile_sha256"`
	ResolvedAt    time.Time `yaml:"resolved_at" json:"resolved_at"`
	StrataVersion string    `yaml:"strata_version" json:"strata_version"`

	// Sigstore attestation for the lockfile itself.
	RekorEntry string `yaml:"rekor_entry,omitempty" json:"rekor_entry,omitempty"`
	Bundle     string `yaml:"bundle,omitempty" json:"bundle,omitempty"`

	// VerificationPolicy records what the resolver checked about layer
	// attestations when it produced this lockfile, so a consumer reading only the
	// artifact can interpret its trust status without knowing how it was invoked
	// (#100 / T6). It is one of the Verify* constants below. It is provenance —
	// it records what was checked, not what the environment is — so it does not
	// participate in EnvironmentID (its route is "inert"; nothing at assembly
	// time reads it).
	VerificationPolicy string `yaml:"verification_policy,omitempty" json:"verification_policy,omitempty"`

	// Base is the resolved AMI and its probed capabilities.
	Base ResolvedBase `yaml:"base" json:"base"`

	// Layers are fully resolved, in mount order.
	Layers []ResolvedLayer `yaml:"layers" json:"layers"`

	// Env carries environment variables from the profile for agent configuration.
	Env map[string]string `yaml:"env,omitempty" json:"env,omitempty"`

	// OnReady are commands to run after overlay assembly.
	OnReady []string `yaml:"on_ready,omitempty" json:"on_ready,omitempty"`

	// Defaults lists software modules to pre-load via Lmod at login.
	// Copied from the profile; used by the agent to write strata-defaults.sh.
	Defaults []SoftwareRef `yaml:"defaults,omitempty" json:"defaults,omitempty"`

	// Packages holds the frozen (exactly pinned) package sets resolved from
	// the profile's packages: section. Populated by strata freeze.
	// The agent installs these at boot via pip/conda/cran.
	Packages []ResolvedPackageSet `yaml:"packages,omitempty" json:"packages,omitempty"`

	// MutableLayer records a persistent EBS upper specification copied from
	// the profile. A lockfile with a non-nil MutableLayer is considered
	// "dirty" and cannot be published until strata freeze-layer is called
	// to convert the upper into a signed squashfs layer.
	MutableLayer *MutableLayerSpec `yaml:"mutable_layer,omitempty" json:"mutable_layer,omitempty"`

	// RequiresHost is copied from the profile for reference. Lists ambient
	// host capabilities (GPU driver, EFA, etc.) the environment depends on
	// but that Strata does not install. Advisory only in v0.21.0.
	RequiresHost []HostRequirement `yaml:"requires_host,omitempty" json:"requires_host,omitempty"`
}

LockFile is the fully resolved, cryptographically attested description of a compute environment. It is machine-generated by the resolver and never hand-written.

The LockFile is the reproducibility artifact. It contains:

  • The SHA256 of the profile that produced it
  • The exact AMI ID and SHA256 of the base
  • The SHA256 and Rekor entry for every layer
  • Its own Rekor entry (the lockfile itself is logged)

A LockFile can be frozen (all versions pinned), published to Zenodo (DOI minted), and cited in papers as a verifiable environment reference.

func ParseLockFile

func ParseLockFile(path string) (*LockFile, error)

ParseLockFile reads a LockFile from a YAML file.

func ParseLockFileBytes

func ParseLockFileBytes(data []byte) (*LockFile, error)

ParseLockFileBytes parses a LockFile from YAML bytes.

func (*LockFile) Age added in v0.28.0

func (l *LockFile) Age(now time.Time) time.Duration

Age reports how long ago the lockfile was resolved, relative to now. It is only a trustworthy measure after the lockfile's signature has verified, since that is what binds ResolvedAt. A future-dated lockfile (clock skew, or a forged-forward timestamp) yields a negative Age.

func (*LockFile) CheckFreshness added in v0.28.0

func (l *LockFile) CheckFreshness(maxAge time.Duration, now time.Time) error

CheckFreshness refuses a lockfile older than maxAge. A maxAge of zero or negative means no bound: freshness is opt-in, so the check never refuses and only FreshnessNote's surfacing applies. When a bound is set, a lockfile with no ResolvedAt is refused (freshness cannot be established), and the error names the age, the resolved date, and the bound so the refusal is actionable.

func (*LockFile) EnvironmentID

func (l *LockFile) EnvironmentID() string

EnvironmentID returns a stable identifier for this environment. It is the SHA256 of a canonical encoding of the lockfile's content; envHashInput in lockfile_hash.go is the definition of what that covers, and the rule for what may sit outside it is stated there. Do not maintain a second copy of the field list here — the one that used to be here drifted twice.

What the identifier commits to:

Two lockfiles with the same EnvironmentID were assembled from identical
instructions. Where those instructions are deterministic — the base image,
the layer set and its mount sequence, the exported environment,
digest-pinned packages — identical instructions produce identical content.
Where they are not — on_ready commands, packages without a digest, a
mutable upper — the identity commits to the instruction and not to its
outcome.

The second sentence is why this is not "the same ID means the same environment": that stronger claim is recorded REFUTED in PROPERTIES.md (X2) and the residual is enumerated in docs/environment-identity.md.

Only frozen lockfiles (all SHA256s present) produce a stable ID; a lockfile without content hashes returns an empty string, which is not an identity and must not be published, tagged or used as a key (#124).

func (*LockFile) FreshnessNote added in v0.28.0

func (l *LockFile) FreshnessNote(now time.Time) string

FreshnessNote is the always-surfaced disclosure of how far behind a lockfile is — the "surface" half of the freshness policy. It returns "" when ResolvedAt is unset (nothing to say), and flags a future-dated lockfile rather than printing a negative age. strata run, strata verify, and the agent all print it so a stale but validly-signed environment is never silently accepted.

func (*LockFile) HasMutableLayer

func (l *LockFile) HasMutableLayer() bool

HasMutableLayer reports whether this lockfile references an unfrozen mutable EBS upper layer (Path B workflow). Such lockfiles cannot be published until strata freeze-layer converts the upper into a signed squashfs layer and removes this field.

func (*LockFile) IsFrozen

func (l *LockFile) IsFrozen() bool

IsFrozen reports whether all layers in the lockfile are pinned by SHA256. A frozen lockfile is fully reproducible — no label resolution can produce a different result.

func (*LockFile) IsSigned

func (l *LockFile) IsSigned() bool

IsSigned reports whether the lockfile has been signed and logged to Rekor. An unsigned lockfile is a valid intermediate state (pre-signing) but should not be used for production launches.

func (*LockFile) LayerCount

func (l *LockFile) LayerCount() int

LayerCount returns the number of layers in the lockfile.

func (*LockFile) ProvenanceRecord

func (l *LockFile) ProvenanceRecord() ProvenanceRecord

ProvenanceRecord returns a human-readable summary of the lockfile's attestation chain. Used in paper supplementary materials and compliance documentation.

func (*LockFile) Validate

func (l *LockFile) Validate() error

Validate reports whether the lockfile is well-formed enough to be trusted — mounted, published, or used as an identity.

It is a policy check, deliberately separate from ParseLockFileBytes: the parser stays able to read a malformed lockfile so a diagnostic such as `strata diff` can still inspect one, and the trust boundaries (strata run, publish, freeze) call Validate before acting on it. It does not require the lockfile to be frozen — an unfrozen lockfile (empty digests) is a valid intermediate state. What it forbids is a field that is present but malformed:

  • a non-empty layer SHA256 or Base.AMISHA256 that is not a well-formed digest (#96 — "bbbbbb" and "sha256-ami-test123456789" used to satisfy IsFrozen);
  • a layer id that is not a safe single path component (#58/#97), since ids become mount points, cache paths, and OCI unpack directories;
  • two layers sharing a MountOrder, which would leave EnvironmentID dependent on the slice order the mounter falls back to for the tie (#95).

type MutableLayerSpec

type MutableLayerSpec struct {
	// Name is the layer name that will be used when freeze-layer produces
	// the signed squashfs (e.g. "torch-ml").
	Name string `yaml:"name" json:"name"`

	// Version is the layer version assigned at freeze-layer time.
	// It is recorded in the lockfile so the agent can tag the EBS volume
	// for identification, but is not used for resolution until after freeze.
	Version string `yaml:"version" json:"version"`

	// SizeGB is the EBS volume size in GiB. Defaults to 20 if zero.
	SizeGB int `yaml:"size_gb,omitempty" json:"size_gb,omitempty"`

	// VolumeType is the EBS volume type. Defaults to "gp3" if empty.
	VolumeType string `yaml:"volume_type,omitempty" json:"volume_type,omitempty"`

	// ABI is the C runtime ABI for the resulting layer. If empty, it is
	// inferred from the base capabilities at resolution time.
	ABI string `yaml:"abi,omitempty" json:"abi,omitempty"`
}

MutableLayerSpec declares a persistent EBS upper directory for the Path B interactive workflow. The agent attaches (or creates) an EBS volume and mounts it as the OverlayFS upper, allowing persistent per-session installs that can later be frozen into a signed squashfs layer with strata freeze-layer.

func (MutableLayerSpec) Validate

func (m MutableLayerSpec) Validate() error

Validate checks that a MutableLayerSpec is well-formed.

type PackageEntry

type PackageEntry struct {
	Name    string `yaml:"name" json:"name"`
	Version string `yaml:"version,omitempty" json:"version,omitempty"`
}

PackageEntry is a single declared package with an optional version constraint. The Version field is a version prefix or constraint (e.g. "1.26" matches "1.26.0", "1.26.1", etc.), not necessarily a pinned exact version. Pinned exact versions appear in ResolvedPackageEntry after strata freeze.

type PackageManager

type PackageManager string

PackageManager identifies the package manager for a set of packages.

const (
	// PackageManagerPip installs Python packages via pip.
	PackageManagerPip PackageManager = "pip"
	// PackageManagerConda installs packages via conda/mamba.
	PackageManagerConda PackageManager = "conda"
	// PackageManagerCRAN installs R packages from CRAN.
	PackageManagerCRAN PackageManager = "cran"
)

type PackageSpec

type PackageSpec struct {
	Manager  PackageManager `yaml:"manager" json:"manager"`
	Packages []PackageEntry `yaml:"packages" json:"packages"`
	Env      string         `yaml:"env,omitempty" json:"env,omitempty"`
}

PackageSpec declares one package manager's entries in a profile. Env is only valid for conda — it specifies the conda environment name to install into (defaults to "base" if empty and manager is conda).

type Profile

type Profile struct {
	// Name is a short identifier for this environment.
	Name string `yaml:"name" json:"name"`

	// Description is optional human-readable context.
	Description string `yaml:"description,omitempty" json:"description,omitempty"`

	// Version is the profile version, for change tracking.
	Version string `yaml:"version,omitempty" json:"version,omitempty"`

	// Base declares the target OS and architecture.
	Base BaseRef `yaml:"base" json:"base"`

	// Software is the list of software refs the user wants.
	// Entries may be individual software refs ("cuda@12.3")
	// or formation refs ("formation:cuda-python-ml@2024.03").
	Software []SoftwareRef `yaml:"software" json:"software"`

	// Registries declares which layer registries to use and trust.
	// If empty, the default Strata public registry is used.
	Registries []RegistryRef `yaml:"registries,omitempty" json:"registries,omitempty"`

	// Instance describes the target EC2 instance configuration.
	//
	// Advisory as of v0.23.0: it records the intended instance for downstream
	// launch tooling but is not consumed by resolution — the resolver produces a
	// lockfile independent of it, and no launch integration reads it yet. It is
	// declared here rather than dropped so a profile can carry the intent; that it
	// is not yet acted upon is stated, not silent (#102).
	Instance InstanceConfig `yaml:"instance,omitempty" json:"instance,omitempty"`

	// Storage describes additional storage mounts.
	//
	// Advisory as of v0.23.0, on the same footing as Instance above: recorded for
	// downstream tooling, not consumed by resolution and not yet mounted by any
	// runtime path (#102).
	Storage []StorageMount `yaml:"storage,omitempty" json:"storage,omitempty"`

	// Env declares environment variables set in the assembled environment.
	Env map[string]string `yaml:"env,omitempty" json:"env,omitempty"`

	// OnReady is a list of commands run after the overlay is assembled,
	// before the instance signals ready.
	OnReady []string `yaml:"on_ready,omitempty" json:"on_ready,omitempty"`

	// Defaults lists software modules to pre-load via Lmod when a user logs
	// in. The agent generates /etc/profile.d/strata-defaults.sh from this.
	// Each entry should name a package in the Software list.
	// Example: [{name: gcc, version: "14.2.0"}, {name: python, version: "3.12.13"}]
	Defaults []SoftwareRef `yaml:"defaults,omitempty" json:"defaults,omitempty"`

	// Packages declares user-level package sets to install on top of base
	// layers at boot time (Path C workflow). These are installed by the agent
	// using the appropriate package manager (pip, conda, cran) and are pinned
	// to exact versions in the lockfile by strata freeze.
	//
	// Unlike squashfs layers, packages are not content-addressed — the same
	// version string may correspond to different bits on different dates if
	// the package index changes. Use Path A (strata build) for full audit.
	Packages []PackageSpec `yaml:"packages,omitempty" json:"packages,omitempty"`

	// MutableLayer declares a persistent EBS upper directory for interactive
	// session use (Path B workflow). The agent mounts this volume as the
	// OverlayFS upper, allowing installs that persist across reboots.
	// Call strata freeze-layer when ready to convert the upper into a signed
	// squashfs layer in the registry.
	MutableLayer *MutableLayerSpec `yaml:"mutable_layer,omitempty" json:"mutable_layer,omitempty"`

	// RequiresHost declares ambient host capabilities this environment depends
	// on but that Strata neither installs nor manages — for example, a GPU
	// driver version or an EFA fabric module. In v0.21.0 these are advisory:
	// the resolver emits a warning but does not fail if the capability cannot
	// be verified.
	RequiresHost []HostRequirement `yaml:"requires_host,omitempty" json:"requires_host,omitempty"`
}

Profile is what users write. A declaration of intent: which software, on which base OS, on which instance type. The resolver transforms a Profile into a LockFile.

func ParseProfile

func ParseProfile(path string) (*Profile, error)

ParseProfile reads and validates a Profile from a YAML file.

func ParseProfileBytes

func ParseProfileBytes(data []byte) (*Profile, error)

ParseProfileBytes parses and validates a Profile from YAML bytes.

func (*Profile) UnmarshalYAML

func (p *Profile) UnmarshalYAML(node *yaml.Node) error

UnmarshalYAML rejects a null entry in the software list before the standard decoder silently drops it. gopkg.in/yaml.v3 removes a null sequence element whose target is a struct — it is not zero-valued into the slice, it is dropped, so the length changes and nothing downstream can tell an entry was written (#79). The two easy ways to write one by accident are a bare "-" and a commented-out entry whose dash was left behind. The check runs on the raw document node, because by the time SoftwareRef.UnmarshalYAML would run the element is already gone.

func (Profile) Validate

func (p Profile) Validate() error

Validate checks that a Profile is well-formed. It does not validate against the registry — that is the resolver's job.

type ProvenanceRecord

type ProvenanceRecord struct {
	Profile       string            `yaml:"profile" json:"profile"`
	ProfileSHA256 string            `yaml:"profile_sha256" json:"profile_sha256"`
	ResolvedAt    time.Time         `yaml:"resolved_at" json:"resolved_at"`
	AMIID         string            `yaml:"ami_id" json:"ami_id"`
	AMISHA256     string            `yaml:"ami_sha256" json:"ami_sha256"`
	Layers        []LayerProvenance `yaml:"layers" json:"layers"`
	LockfileRekor string            `yaml:"lockfile_rekor_entry" json:"lockfile_rekor_entry"`
	StrataVersion string            `yaml:"strata_version" json:"strata_version"`
}

ProvenanceRecord is a structured summary of a lockfile's attestation chain. Designed for inclusion in supplementary materials and DOI metadata.

type RegistryRef

type RegistryRef struct {
	// URL is the S3 URL of the registry, e.g. "s3://strata-public-layers".
	URL string `yaml:"url" json:"url"`

	// Trust declares how to verify layers from this registry.
	// "strata-core"       — verify against Strata's public signing key
	// "keyfile://<path>"  — verify against a local public key file
	// "keyless"           — verify keyless Sigstore signatures (OIDC)
	Trust string `yaml:"trust" json:"trust"`
}

RegistryRef declares a layer registry and how to trust it.

type Requirement

type Requirement struct {
	Name       string `yaml:"name" json:"name"`
	MinVersion string `yaml:"min_version,omitempty" json:"min_version,omitempty"`
	MaxVersion string `yaml:"max_version,omitempty" json:"max_version,omitempty"`
}

Requirement is a capability requirement with optional version bounds. MinVersion and MaxVersion are semver strings. An empty bound means unbounded in that direction.

Examples:

{Name: "glibc",  MinVersion: "2.34"}                       // glibc >= 2.34
{Name: "cuda",   MinVersion: "12.0"}                       // cuda >= 12.0
{Name: "python", MinVersion: "3.10", MaxVersion: "3.12"}   // 3.10 <= python < 3.12

func (Requirement) String

func (r Requirement) String() string

String returns a human-readable representation of the requirement.

type ResolvedBase

type ResolvedBase struct {
	// DeclaredOS is the OS string from the profile, e.g. "al2023".
	DeclaredOS string `yaml:"declared_os" json:"declared_os"`

	// AMIID is the resolved EC2 AMI ID, e.g. "ami-0abc123def456789".
	AMIID string `yaml:"ami_id" json:"ami_id"`

	// AMISHA256 is the SHA256 of the base capability *record*
	// (BaseCapabilities.ContentDigest), not of the AMI's filesystem bytes: it
	// digests the probed OS/arch/ABI/compiler/Provides that the resolver actually
	// used, so it is stable across AMI-ID aliasing while committing to nothing
	// about the underlying image bytes (#64). The field is still keyed
	// `ami_sha256` for lockfile compatibility; the name overclaims and is due to
	// become `base_digest` alongside base-digest format validation in #65.
	AMISHA256 string `yaml:"ami_sha256" json:"ami_sha256"`

	// Capabilities is the probed capability set of the base AMI.
	// This is what the resolver used to validate layer requirements.
	Capabilities BaseCapabilities `yaml:"capabilities" json:"capabilities"`
}

ResolvedBase is the fully resolved base for an environment. It records both what the user declared and what was actually resolved.

type ResolvedLayer

type ResolvedLayer struct {
	LayerManifest `yaml:",inline" json:",inline"`

	// MountOrder is the position in the OverlayFS lower directory stack.
	// Lower numbers are mounted first (closer to the base).
	// The layer with MountOrder=1 is at the bottom of the stack.
	MountOrder int `yaml:"mount_order" json:"mount_order"`

	// SatisfiedBy records which SoftwareRef in the profile this layer
	// was resolved to satisfy. For formation members, this is the
	// formation ref string, e.g. "formation:cuda-python-ml@2024.03".
	SatisfiedBy string `yaml:"satisfied_by" json:"satisfied_by"`

	// FromFormation is the formation name if this layer was expanded
	// from a formation. Empty for standalone refs.
	FromFormation string `yaml:"from_formation,omitempty" json:"from_formation,omitempty"`
}

ResolvedLayer is a fully resolved layer in a lockfile. It embeds the full LayerManifest plus resolution metadata.

type ResolvedPackageEntry

type ResolvedPackageEntry struct {
	Name    string `yaml:"name" json:"name"`
	Version string `yaml:"version" json:"version"`
	SHA256  string `yaml:"sha256,omitempty" json:"sha256,omitempty"`
}

ResolvedPackageEntry is an exact pinned version produced by strata freeze. SHA256 is the content hash of the downloaded wheel or archive; it is populated when available but may be empty for some package managers.

type ResolvedPackageSet

type ResolvedPackageSet struct {
	Manager  PackageManager         `yaml:"manager" json:"manager"`
	Env      string                 `yaml:"env,omitempty" json:"env,omitempty"`
	Packages []ResolvedPackageEntry `yaml:"packages" json:"packages"`
}

ResolvedPackageSet is the frozen representation of one PackageSpec in the lockfile. All versions are exactly pinned.

type SoftwareRef

type SoftwareRef struct {
	// Name is the software name for individual package refs.
	// Empty when Formation is set.
	Name string `yaml:"name,omitempty" json:"name,omitempty"`

	// Version is a semver prefix constraint for individual package refs.
	// "12.3" matches "12.3.0", "12.3.1", "12.3.2", etc.
	// Empty means "latest stable".
	// Empty when Formation is set.
	Version string `yaml:"version,omitempty" json:"version,omitempty"`

	// Formation is the formation identifier for formation refs.
	// Format: "name@version" e.g. "cuda-python-ml@2024.03"
	// Empty when Name is set.
	Formation string `yaml:"formation,omitempty" json:"formation,omitempty"`
}

SoftwareRef is a user-declared software requirement. It is either a named software package ("cuda@12.3") or a formation reference ("formation:cuda-python-ml@2024.03"). The two forms are mutually exclusive.

func ParseSoftwareRef

func ParseSoftwareRef(s string) (SoftwareRef, error)

ParseSoftwareRef parses a software ref string into a SoftwareRef.

Accepted formats:

"cuda"                             → {Name: "cuda"}
"cuda@12.3"                        → {Name: "cuda", Version: "12.3"}
"formation:cuda-python-ml@2024.03" → {Formation: "cuda-python-ml@2024.03"}

func (SoftwareRef) IsFormation

func (s SoftwareRef) IsFormation() bool

IsFormation reports whether this ref is a formation reference.

func (SoftwareRef) MarshalYAML

func (s SoftwareRef) MarshalYAML() (interface{}, error)

MarshalYAML emits the inline scalar form when that form reads back as an equal value, and the mapping form when it would not. A ref whose canonical string does not survive ParseSoftwareRef — a formation carrying surrounding whitespace, say — is written as a mapping rather than as a scalar that would read back different from what was written.

func (SoftwareRef) String

func (s SoftwareRef) String() string

String returns the canonical string representation: "cuda@12.3", "cuda" (no version), "formation:cuda-python-ml@2024.03".

func (*SoftwareRef) UnmarshalYAML

func (s *SoftwareRef) UnmarshalYAML(node *yaml.Node) error

UnmarshalYAML decodes both documented forms of a software ref: the inline scalar form and the mapping form.

  • python@3.13 → {Name: "python", Version: "3.13"}
  • formation:bio-seq@2026.03 → {Formation: "bio-seq@2026.03"}
  • {name: python, version: "3.13"} → {Name: "python", Version: "3.13"}

A null entry never reaches this method. yaml.v3 stops at a null node before consulting an unmarshaller, and then drops the element from the sequence rather than zeroing it, so "- ~" is silently discarded — before this method existed and still. That is #79, not something a branch here could fix.

func (SoftwareRef) Validate

func (s SoftwareRef) Validate() error

Validate checks that a SoftwareRef is well-formed.

type StorageMount

type StorageMount struct {
	// Type is the storage type: "s3", "efs", "ebs", "instance_store".
	Type string `yaml:"type" json:"type"`

	// Mount is the filesystem path to mount at.
	Mount string `yaml:"mount" json:"mount"`

	// Bucket is the S3 bucket name (type=s3 only).
	Bucket string `yaml:"bucket,omitempty" json:"bucket,omitempty"`

	// ID is the EFS filesystem ID (type=efs only).
	ID string `yaml:"id,omitempty" json:"id,omitempty"`

	// ReadOnly mounts the storage read-only.
	ReadOnly bool `yaml:"read_only,omitempty" json:"read_only,omitempty"`
}

StorageMount describes an additional storage volume to mount.

Jump to

Keyboard shortcuts

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