Skip to main content

Supply Chain Security

datamitsu treats supply chain integrity as a non-negotiable property of every install. This guide explains the defenses applied to pnpm (node apps), UV (Python apps), and Go dependencies, and how to configure overrides when a real workload requires them.

Hash Verification (All Downloads)

Every artifact downloaded from the internet must have a SHA-256 hash. This applies to binary apps, managed runtimes, JVM JAR files, and remote config files. If a hash is missing, datamitsu refuses to download — there is no permissive fallback mode.

The hash is verified before the artifact is unpacked or executed. Lock files are mandatory for all UV and node apps; the lock file content itself is hash-verified as part of the app config.

See the Binary Management guide for the verification pipeline applied to binary apps.

Registry-Sourced Parser Modules

A WASM output parser declares its module either as an https URL or as an OCI artifact (oci: { ref, digest }) — exactly one of the two. Declaring both, or neither, is a config error at load, and there is no fallback between them: a config that pins a registry never reaches back to a URL.

hash stays mandatory for both sources. For an OCI source it does double duty: it is also the expected digest of the artifact's single layer. datamitsu requires that layer to have digest "sha256:" + hash, so the registry digest chain and the config hash are the same number rather than two independent claims. A registry serving a correctly-digested manifest that points at different content is rejected before one payload byte is requested.

The module is therefore hash-checked three times on its way into the store:

  1. Manifest — the manifest body is re-hashed against the pinned digest (the registry's Docker-Content-Digest header is never trusted), then its layer digest is compared against the config hash. Nothing is requested from the blob endpoint until both hold.
  2. Stream — the blob bytes are hashed as they are written; a size or digest mismatch is a permanent failure, not a retry.
  3. File — the finished file is verified against the config hash before it is published into the store.

After that the store is self-healing: a parser module already on disk is re-hashed against the declared hash on every use and discarded when it does not match — whatever put it there (a download, an OCI bundle seed, a restored CI cache, a copy by hand). A module is stored at {store}/.parsers/{name}/{key}, where the key is derived from the module's SHA-256 alone — so one entry resolves to one directory no matter which channel delivered the bytes.

:::note --no-oci does not disable a declared parser source --no-oci (or DATAMITSU_NO_OCI) turns off OCI bundle store seeding, an accelerator that degrades gracefully when it is unavailable. A parser that declares an oci source keeps using it, because that is the only route to its bytes. DATAMITSU_OFFLINE is the single hard network gate. :::

A Registry ref Is a Trusted-Host Setting

The ref on a parser or a bundle decides which host datamitsu talks to, and datamitsu can be holding a GITHUB_TOKEN while it does. Changing a ref is a credential decision, not only a mirroring one.

That token is sent as Basic auth to a registry's token endpoint only when the configured registry host is ghcr.io and the token realm advertised in that registry's own WWW-Authenticate challenge is ghcr.io. Both conditions are required: the realm arrives in a header the registry controls, so checking the realm alone would let any host name ghcr.io as its realm and be handed a real GitHub token. A ref pointed at any other host pulls anonymously. The hash chain protects the bytes; it does not protect a credential handed to the wrong host.

List every registry reference a config pins, without touching the network:

datamitsu store refs --oci-only
# ghcr.io/datamitsu/datamitsu-parsers@sha256:...

Signatures Are Not Verified

This build verifies no signatures at all — there is no sigstore dependency in the binary. A signer set on a bundle's oci or on a parser's oci is rejected at config load rather than accepted and ignored, so a config can never assert a guarantee the binary does not deliver.

Signatures still exist upstream: CI signs checksums.txt and the published parser artifact manifest with keyless cosign, and both can be verified out of band (cosign verify-blob, cosign verify). What the binary enforces is the hash chain above.

Minimum Release Age (Version Selection)

When you pin a tool or runtime version with the devtools pull-* commands, datamitsu refuses to select a release that is too fresh. A brand-new release is the most likely to be a typosquat, a compromised publish, or an accidental break that gets yanked within hours. A short soak time lets the ecosystem catch and pull bad releases before you adopt them.

The global default is 10080 minutes (7 days). It applies to every command that selects a concrete version from a registry:

CommandAge-filtered registries
pull-githubGitHub releases
pull-nodenpm
pull-uvPyPI
pull-runtimesnpm (pnpm), GitHub (uv and JVM binary releases)

Major-version-line lookups are not age-filtered, because they select a release line rather than a specific build: the Node.js LTS line and Python stable line (endoflife.date), the Temurin major version (Adoptium API), and the Go release listing (go.dev). Only the concrete binary release or package version chosen within those lines passes through the age filter.

The --min-age flag

Every pull-* command accepts --min-age <minutes>:

  • --min-age -1 (the default) — use the global effective minimum release age
  • --min-age 0 — disable age filtering and take the latest release
  • --min-age 43200 — a custom cutoff (here, 30 days)
# Pin only releases at least 7 days old (the default)
datamitsu devtools pull-github apps/githubApps.json --update

# Require 30 days of soak time
datamitsu devtools pull-node apps/nodeApps.json --update --min-age 43200

# Bypass the filter and take the newest release
datamitsu devtools pull-github apps/githubApps.json --update --min-age 0

Each command prints the effective cutoff in its status banner (Minimum release age: 10080 minutes, or disabled when set to 0).

When no release is old enough

If every available release is younger than the cutoff, datamitsu's behavior depends on whether a safe fallback exists:

  • pull-github — an existing app keeps its current tag with a warning; a new app (no prior binary) is a hard error, since there is nothing safe to pin.
  • pull-node / pull-uv — the package is skipped with a warning and keeps its current version.
  • pull-runtimes — a hard error, since runtimes must resolve to a concrete version.

The error message always points at the --min-age 0 escape hatch.

Overriding the global default

Set DATAMITSU_MIN_RELEASE_AGE (in minutes) to change the effective default for all commands without passing --min-age each time. 0 disables filtering globally.

# Require 14 days globally
DATAMITSU_MIN_RELEASE_AGE=20160 datamitsu devtools pull-uv apps/uvApps.json --update

Verify the effective value mechanically with datamitsu config runtime:

datamitsu config runtime | jq .minimumReleaseAgeMinutes # -> 10080
DATAMITSU_MIN_RELEASE_AGE=20160 datamitsu config runtime | jq .minimumReleaseAgeMinutes # -> 20160

:::note Distinct from the pnpm minimumReleaseAge setting This version-selection filter — applied when you pin versions with pull-* — is separate from the minimumReleaseAge key in pnpm-workspace.yaml, which pnpm applies when it resolves a node app's transitive dependencies at install time. Both default to 7 days; see pnpm (Node Apps) for the install-time setting. :::

pnpm (Node Apps)

The Node.js runtime itself is acquired as a direct, SHA-256-pinned archive download (like the JVM runtime), and pnpm is pinned by SHA-256 as well — so the toolchain executing your installs is integrity-verified before any package is fetched.

pnpm 11 introduces strict supply chain defaults that block lifecycle scripts for unapproved packages. datamitsu integrates with these defaults rather than disabling them: when a node app is installed, datamitsu writes a pnpm-workspace.yaml containing a secure baseline. Any per-app overrides supplied via App.files["pnpm-workspace.yaml"] are shallow-merged on top.

The baseline pnpm-workspace.yaml that datamitsu writes for every node app:

strictDepBuilds: true
blockExoticSubdeps: true
enablePrePostScripts: false
dangerouslyAllowAllBuilds: false
minimumReleaseAge: 10080 # 7 days in minutes
trustPolicy: no-downgrade
lockfile: true
preferFrozenLockfile: true

What each setting does:

SettingPurpose
strictDepBuildsBlocks lifecycle scripts for unapproved packages (the pnpm 11 default)
blockExoticSubdepsRejects transitive deps from non-registry sources (git URLs, local paths) added by deps
enablePrePostScriptsDisables pre*/post* script execution across all packages
dangerouslyAllowAllBuildsMust stay false; setting true neutralizes strictDepBuilds
minimumReleaseAgeRefuses to install packages published less than 7 days ago — defeats most typosquats
trustPolicy: no-downgradeBlocks unsigned/older provenance from replacing a previously-trusted version
lockfileRequires a lockfile in every install
preferFrozenLockfileUses --frozen-lockfile semantics when a lockfile is present

Per-App Overrides via App.files

When a package legitimately needs build scripts (puppeteer, sharp, esbuild, etc.), allowlist it via App.files["pnpm-workspace.yaml"]. Your overrides are shallow-merged on top of the defaults — keys you do not set keep the secure baseline.

const mapOfApps: BinManager.MapOfApps = {
mmdc: {
files: {
"pnpm-workspace.yaml": YAML.stringify({
allowBuilds: { puppeteer: true },
}),
},
node: {
packageName: "@mermaid-js/mermaid-cli",
binPath: "node_modules/.bin/mmdc",
version: "11.15.0",
lockFile: "br:...",
},
},
};

The merged file written to the app environment includes both the secure defaults (e.g., strictDepBuilds: true) and the user's allowBuilds entry. The original pnpm-workspace.yaml from App.files is consumed by the merge and never written as a raw file.

Overriding a Security Setting

User intent wins. If you explicitly set a key, it overrides the default — including security keys. For example, setting strictDepBuilds: false disables strict build approval. Use this sparingly and only when you understand the trade-off.

files: {
"pnpm-workspace.yaml": YAML.stringify({
strictDepBuilds: false, // overrides the default
}),
}

Reusing Defaults in Project Repos via sharedStorage

For users who want to write a secure pnpm-workspace.yaml into a project repository (not into a datamitsu-managed node app environment), the default config.js publishes the recommended defaults via sharedStorage["pnpm-workspace-defaults"]. Your config can read, extend, and write them.

function getConfig(config: BinManager.Config): BinManager.Config {
const pnpmDefaults = YAML.parse(config.sharedStorage?.["pnpm-workspace-defaults"] ?? "{}");

const repoPnpmConfig = {
...pnpmDefaults,
packages: ["packages/*"],
allowBuilds: { esbuild: true, sharp: true },
};

return {
...config,
bundles: {
"project-pnpm-config": {
files: {
"pnpm-workspace.yaml": YAML.stringify(repoPnpmConfig),
},
links: { "pnpm-workspace": "pnpm-workspace.yaml" },
},
},
};
}

This pattern is for repo-level configuration — a project's own pnpm-workspace.yaml. It is separate from the per-app node merge above, which is fully automatic.

Lockfile Enforcement

Node apps require a lockFile field. datamitsu runs pnpm install --frozen-lockfile, so any drift between the lock file and package.json fails the install. Regenerate lock files after version bumps via datamitsu config lockfile <appName>.

UV (Python Apps)

UV apps use uv to install Python packages into isolated environments. Two install-time defenses are layered:

--locked Enforces the Lock File

When a lockFile is present, uv sync runs with --locked. UV refuses to modify uv.lock and exits non-zero if it would need to resolve versions differently.

--no-build Blocks Source Distributions

When a lockFile is present, datamitsu also passes --no-build. UV will install only pre-built wheels — never source distributions (sdists), which would otherwise execute arbitrary build code during install.

The pair --locked --no-build is intentional. With a lock file you know exactly which versions will be installed, so you can be confident wheels exist for your target platforms. Without a lock file, --no-build is omitted because wheel availability cannot be predicted.

Trade-off: if a locked version has no wheel for your platform (e.g., a niche arch, or a pure-Python package without a wheel), the install fails. This is intentional — the failure surfaces a security-relevant gap rather than silently executing setup.py.

const mapOfApps: BinManager.MapOfApps = {
yamllint: {
uv: {
packageName: "yamllint",
version: "1.38.0",
lockFile: "br:...", // mandatory; triggers --locked + --no-build
},
},
};

Hash Verification

UV's lock file format embeds hashes for every resolved artifact. Combined with --locked, this gives end-to-end integrity for the dependency tree.

Go (Go Apps)

Go apps build a command-line tool from source with the managed Go SDK. Three layers of defense apply before the resulting binary runs.

Hash-Pinned Go SDK

The Go toolchain itself is downloaded as a SHA-256-pinned archive (like the Node.js and JVM runtimes), so the compiler building your tool is integrity-verified before any source is fetched. GOTOOLCHAIN=local is forced, so the pinned SDK can never silently swap itself for a different toolchain version.

Mandatory Lock File (go.mod + go.sum)

Go apps require a lockFile field. datamitsu stores it as a JSON wrapper carrying both go.mod and go.sum, writes both files into an isolated build directory, and builds with -mod=readonly. Any drift between the resolved modules and the pinned go.sum fails the build instead of silently rewriting it. The checksum database (sum.golang.org) is consulted via GOSUMDB, and GOPRIVATE/GONOPROXY/GONOSUMDB/GOINSECURE are explicitly cleared so no module can opt out of verification.

const mapOfApps: BinManager.MapOfApps = {
govulncheck: {
go: {
packageName: "golang.org/x/vuln/cmd/govulncheck",
version: "v1.3.0",
lockFile: "br:...", // mandatory; carries go.mod + go.sum
},
},
};

Reproducible Builds

The build runs go build -trimpath -mod=readonly. -trimpath strips local filesystem paths so the output is reproducible, and -mod=readonly forbids any go.mod/go.sum mutation. Regenerate the lock file after a version bump via datamitsu config lockfile <appName>.

Securing Your Own Repository's Go Code

datamitsu manages Go apps (tools it builds for you); your repository's own Go modules are still managed by go itself. Enforce the standard defenses in CI:

# Verify go.sum checksums against the local module cache
go mod verify

# Ensure go.mod and go.sum are current and committed
go mod tidy && git diff --exit-code

# Build without silently mutating go.mod or downloading new modules
GOFLAGS=-mod=readonly go build ./...

# Scan for known vulnerabilities in dependencies and stdlib
govulncheck ./...

Common Patterns

Adding a Package that Requires Build Scripts

  1. Identify the package — the install error will name it: ERR_PNPM_IGNORED_BUILDS
  2. Add it to App.files["pnpm-workspace.yaml"] under allowBuilds
  3. Regenerate the lock file: pnpm exec datamitsu config lockfile <appName>
  4. Commit both the config change and the new lockFile value

Tightening minimumReleaseAge

The 7-day default catches typosquats and rushed compromised releases. For higher-security environments, override per app:

files: {
"pnpm-workspace.yaml": YAML.stringify({
minimumReleaseAge: 43200, // 30 days
}),
}

CI Hardening

A reasonable CI pipeline for repositories that consume datamitsu:

datamitsu init --no-cache # fresh install, all hashes re-verified
datamitsu check # run the full check pipeline
go mod verify # if your repo has Go code
govulncheck ./... # if your repo has Go code

For wrapper packages, also run datamitsu devtools verify-all to confirm every platform's binaries and runtimes still hash-match.