akua / concepts / lockfile-format

akua.toml + akua.lock

Manifest + lockfile shape, vendor-tree integration, and the verify pipeline.

akua's package manager is modeled on Go's two-file split but uses TOML for both files (the format Cargo, Poetry, and pnpm adopted for richer dep graphs than Go's directive syntax expresses).

Both files are TOML. Both are checked into git. Both are required.


Why two files

Clear separation of concerns:

intentevidence
fileakua.tomlakua.lock
edited byhumanakua add / akua pull / akua publish / akua update
shapesmall, stablemay be large; churns on every resolved-version change
review focus"do we want this dep?""is this the expected digest + signature?"

A PR that modifies akua.lock but not akua.toml is automatically suspicious (someone changed what they got without changing what they asked for). CI can lint for this.

Merged-lockfile alternatives (npm's package-lock.json, Cargo's Cargo.lock with deps embedded in Cargo.toml) bundle both concerns differently; we take Go's split (intent vs evidence in separate files) and Cargo's structured-TOML lockfile (so we can express a richer dep graph than go.sum's line-per-hash format).

Naming


akua.toml

Top-level structure

[package]
name    = "my-app"
version = "0.1.0"
edition = "akua.dev/v1alpha1"                 # akua schema compat marker

# (Optional) workspace members — for monorepos with many akua packages
[workspace]
members = ["./", "./apps/*"]

# Dependencies — every import in KCL / Rego must be declared here
[dependencies]
# key = { source_type = "<ref>", version = "..." }

Dependency forms

formexampleuse when
OCI{ oci = "oci://ghcr.io/.../foo", version = "1.2.3" }published signed artifact (most common)
Git{ git = "https://github.com/foo/bar", tag = "v1.2.3" }non-OCI-distributed sources
Path{ path = "../shared" }workspace-local, dev-only
Helm repo{ repo = "https://go.temporal.io/helm-charts", chart = "temporal", version = "0.62.0" }classic HTTPS Helm repository
Replace{ oci = "...", replace = { path = "../fork" } }local-fork override for debugging

Example

[package]
name    = "payments-api"
version = "3.2.0"
edition = "akua.dev/v1alpha1"

[dependencies]
# KCL sources
k8s       = { oci = "oci://ghcr.io/kcl-lang/k8s",                  version = "1.31.2" }
cnpg      = { oci = "oci://ghcr.io/cloudnative-pg/charts/cluster", version = "0.20.0" }
webapp    = { oci = "oci://ghcr.io/acme/charts/webapp",            version = "2.1.0" }

# Rego policies (compile-resolved as data. imports — see policy-format.md)
tier-prod = { oci = "oci://policies.akua.dev/tier/production",   version = "1.2.0" }
kyv-sec   = { oci = "oci://policies.akua.dev/kyverno/security",  version = "2.0.0" }

# Local fork for debugging
our-glue  = { oci = "oci://pkg.acme.internal/glue", version = "0.3.0",
              replace = { path = "../glue-fork" } }

Version resolution

Fields

fieldrequirednotes
[package].nameyesa valid KCL package identifier
[package].versionyessemver
[package].editionyesakua.dev/v1alpha1 for v0 compatibility
[package].strictSigningnodefault true; set false to permit unsigned deps (discouraged)
[workspace].membersnoglob patterns; enables monorepo
[dependencies]yes (can be empty)see dependency forms above

akua.lock

Cargo.lock-flavored TOML: one [[package]] entry per resolved artifact, alphabetically ordered, with optional dependencies for the transitive graph.

Format

# akua.lock — machine-maintained. Never hand-edit.
# Regenerated by `akua add`, `akua pull`, `akua publish`, `akua update`.

version = 1   # lockfile format version; bumped on incompatible changes

[[package]]
name    = "cnpg"
version = "0.20.0"
source  = "oci://ghcr.io/cloudnative-pg/charts/cluster"
digest  = "sha256:3c5d9e7f1a2b4c6d8e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d"
signature = "cosign:sigstore:cloudnative-pg"

[[package]]
name    = "webapp"
version = "2.1.0"
source  = "oci://ghcr.io/acme/charts/webapp"
digest  = "sha256:a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2"
signature = "cosign:key:acme"
dependencies = [
  "common@2.20.0",
]

[[package]]
name    = "common"
version = "2.20.0"
source  = "oci://ghcr.io/bitnamicharts/common"
digest  = "sha256:f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0"
signature = "cosign:sigstore:bitnamicharts"

Fields per [[package]]

fieldrequirednotes
nameyesmatches the [dependencies] key in akua.toml
versionyesexact resolved semver (not a range)
sourceyesfull source ref: oci://…, git+https://…, path+file://…, or helm+<repo-url>#<chart>
digestyescontent-addressable source hash: sha256: for OCI/path/helm-repo deps; git:<commit-sha> for git deps
vendor_digestnosha256: hash of the vendored on-disk tree when it differs from digest; used by akua vendor check for git deps without losing the commit pin
signatureconditionalcosign signature. Keyless: cosign:sigstore:<issuer>. Keyed: cosign:key:<identity>. Required unless [package].strictSigning = false in akua.toml
dependenciesno["name@version", …] — transitive edges for graph walks
attestationnoSLSA attestation digest; present when the dep's author publishes one alongside
replacedno{ path = "…" } when a local replace is active
yankednotrue for retracted versions

Rules

Helm-repo lock entries

A dep resolved from a classic HTTPS Helm repository locks with:

[[package]]
name    = "temporal"
version = "0.62.0"
source  = "helm+https://go.temporal.io/helm-charts#temporal"
digest  = "sha256:<tgz-sha256>"

source format is helm+<repo-url>#<chart>. digest is the sha256: hash of the downloaded .tgz, content-pinned and verified on every subsequent pull. No cosign signature (signature field absent) — Helm repositories distribute plain tarballs without a signing layer. Private repos use the host-keyed --auth / auth: credential at fetch time; credentials never appear in akua.lock.

What akua.lock does NOT contain


Resolution workflow

akua add <kind> <ref> --version=<v>

  1. Reads current akua.toml
  2. Adds the new entry to [dependencies]
  3. Fetches the artifact; computes digest
  4. Verifies cosign signature
  5. Updates akua.lock — inserts/updates the [[package]] entry; adds transitive deps alphabetically
  6. If the new dep transitively pulls others, repeats for each

Result: both akua.toml and akua.lock updated in one atomic operation.

akua verify (CI gate)

  1. Reads akua.toml and akua.lock
  2. Resolves every dep from akua.toml
  3. Compares expected (manifest) vs locked (lockfile) digest + signature
  4. Exits 0 if everything matches; non-zero otherwise

Run in CI on every PR to catch lockfile tampering.

akua update [dep]

Updates to the highest allowed version per akua.toml constraints; rewrites the relevant [[package]] entries in akua.lock. Leaves other deps untouched unless their constraints also match a new version.

akua vendor (optional)

Materializes a dependency's bytes into .akua/vendor/<name>/ and pins the source digest in akua.lock. The resolver prefers the vendored copy across all dep kinds (path / oci / git / helm), so the canonical source can be deleted post-vendor and akua render still succeeds offline. For git deps, digest stays git:<commit-sha> and vendor_digest stores the vendored tree hash for local drift checks. Required for air-gapped builds, optional otherwise.

Bytes-tied lockfile metadata. Cosign signatures, SLSA attestations, transitive dependency lists, yanked, and Kyverno-converter fields all bind to a specific digest. When a re-vendor or version bump produces a new digest, those fields are dropped on upsert rather than written as (digest=B, sig=sig(A)) entries that no consumer can verify. The source / version / digest triple is always rewritten; everything else is conditional on prior.digest == new.digest.


Workspaces

For monorepos with multiple akua packages:

platform/
├── akua.toml                    # workspace root
├── akua.lock
├── apps/
│   ├── api/
│   │   └── package.k            # member package
│   ├── worker/
│   │   └── package.k
│   └── dashboard/
│       └── package.k
└── policies/
    └── org-baseline/
        └── policy.rego          # member policy module

Workspace root akua.toml:

[workspace]
members = ["./apps/*", "./policies/*"]

[dependencies]
# Shared deps used by all members
k8s = { oci = "oci://ghcr.io/kcl-lang/k8s", version = "1.31.2" }

Members inherit workspace dependencies; they may override in a member-local akua.toml (minimal). Cross-member imports work as path-type deps.


Compatibility with kpm

akua's akua.toml is not the same as KCL's kcl.mod. We don't try to be. akua packages can contain a kcl.mod in their source tree for pure-KCL consumers who want to use upstream kcl run against the package directly; akua's resolver honors either file when it's unambiguous.

Pulling kpm-published packages. OCI deps published by kpm push (e.g. everything under ghcr.io/kcl-lang/) work transparently through the same [dependencies] shape Helm charts use. The fetcher detects the artifact family from the manifest's media type + org.kcllang.package. annotations, unpacks the plain tar, and registers each as a typed ExternalPkg entry inside the wasmtime render sandbox — so a Package.k can write import k8s.api.apps.v1 and have it resolve against the upstream schema bundle. See examples/10-kcl-ecosystem/ for a worked example.

See the broader architecture note for the "akua is the outer package manager; kpm is the inner KCL-layer tool" framing.


Example: a real workspace

./
├── akua.toml
├── akua.lock
├── apps/
│   ├── api/
│   │   └── package.k
│   └── worker/
│       └── package.k
├── policies/
│   └── production.rego
└── environments/
    ├── dev/inputs.yaml
    ├── staging/inputs.yaml
    └── production/inputs.yaml

akua.toml:

[package]
name    = "acme-platform"
version = "0.1.0"
edition = "akua.dev/v1alpha1"

[workspace]
members = ["./apps/*"]

[dependencies]
k8s       = { oci = "oci://ghcr.io/kcl-lang/k8s",                  version = "1.31.2" }
cnpg      = { oci = "oci://ghcr.io/cloudnative-pg/charts/cluster", version = "0.20.0" }
webapp    = { oci = "oci://ghcr.io/acme/charts/webapp",            version = "2.1.0" }
tier-prod = { oci = "oci://policies.akua.dev/tier/production",   version = "1.2.0" }
kyv-sec   = { oci = "oci://policies.akua.dev/kyverno/security",  version = "2.0.0" }

akua.lock (after resolution):

version = 1

[[package]]
name    = "cnpg"
version = "0.20.0"
source  = "oci://ghcr.io/cloudnative-pg/charts/cluster"
digest  = "sha256:d4e5f6…"
signature = "cosign:sigstore:cloudnative-pg"

[[package]]
name    = "k8s"
version = "1.31.2"
source  = "oci://ghcr.io/kcl-lang/k8s"
digest  = "sha256:a1b2c3…"
signature = "cosign:sigstore:kcl-lang"

[[package]]
name    = "kyv-sec"
version = "2.0.0"
source  = "oci://policies.akua.dev/kyverno/security"
digest  = "sha256:j0k1l2…"
signature = "cosign:sigstore:akua-release"

[[package]]
name    = "tier-prod"
version = "1.2.0"
source  = "oci://policies.akua.dev/tier/production"
digest  = "sha256:g7h8i9…"
signature = "cosign:sigstore:akua-release"

[[package]]
name    = "webapp"
version = "2.1.0"
source  = "oci://ghcr.io/acme/charts/webapp"
digest  = "sha256:m3n4o5…"
signature = "cosign:key:acme"

CI runs akua verify on every PR; any digest mismatch or missing signature fails the build.