Governance & Provenance (v0.59.0)

Procurement-ready ML compliance from a single CLI. v0.59 ships 4 governance surfaces that previously required a stack of SaaS tools and a security team: CycloneDX/SPDX BOM emitter, in-toto / SLSA-3 attestation, HIPAA / SOC2 audit log, and EU AI Act Annex XI/XII auto-documentation.

soup bom emit — ML Bill of Materials

Generates machine-learning Bills of Material in CycloneDX 1.6 (with ML-BOM extension) or SPDX 2.3 AI-profile formats — or both in a single invocation.

bash
soup bom emit \
  --name llama3-8b-finetuned --version 1.0.0 \
  --base-model meta-llama/Llama-3.1-8B-Instruct \
  --base-sha <64-hex> \
  --config-sha <64-hex> \
  --data-sha <64-hex> \
  --task sft --license apache-2.0 \
  --format both --output ./manifests/llama3-bom

The three SHA values are SHA-256 digests that you supply, and each must be exactly 64 hex characters: a shortened or elided value exits 2 with base_sha must be 64 hex chars. Soup records them in the BOM and does not compute them or check them against anything. sha256sum soup.yaml | cut -d' ' -f1 prints one for a file.

Atomic file write (tempfile.mkstemp + os.replace) with symlink rejection (TOCTOU defense). --format=both produces <prefix>.cdx.json and <prefix>.spdx.json side by side.

Since v0.73.3, --attach-to-registry <id> registers what it wrote as a bom artifact on that entry, so soup card links it in the card's artifact table for free. Those were the two compliance documents soup card existed to publish and could not reach, because the registry had no such artifact kinds. The flag needs --output (omitting it is a usage error, exit 2), and a lookup or attachment failure exits non-zero after leaving the emitted files on disk. With --format both, both files are attached.

soup attest emit — SLSA-3 in-toto attestations

Per-stage attestation aligned with SLSA-3 (Supply-chain Levels for Software Artifacts) and in-toto.

bash
soup attest emit --stage train \
  --subject adapter.safetensors --sha <64-hex> \
  --builder soup-cli \
  --invocation "soup train --config soup.yaml" \
  --sign unsigned --output ./attestations/train.json

--attach-to-registry <id> works here too, as an attestation artifact, and a signed statement attaches its detached .sig sidecar alongside, so the card links every file a verifier needs (v0.73.3).

Stages: extract / train / eval / export / publish. --sha must be the 64-hex SHA-256 of the artifact, and it is recorded as given: Soup does not hash --subject and does not check the value against the file, so compute it yourself (sha256sum adapter.safetensors | cut -d' ' -f1). Backends: unsigned (default) writes the statement with an empty signature, so the statement itself is not tamper-evident; it records the subject digest you supplied. ed25519 is live as of v0.71.2 ([sign] extra; soup attest verify <statement> --signature <sig> does a canonical-JSON cryptographic verify).

soup attest verify without --public-key does not tell you who signed. It checks the signature against the public key stored in the .sig sidecar itself, so a statement re-signed by anyone with their own key and shipped with their own sidecar also passes (exit 0). That shows only that the statement and the sidecar agree with each other: the statement was signed by whoever holds the key recorded in the sidecar. It does not show who that is, and it does not show that the statement is the one the original signer produced, because an edited statement that was re-signed passes the same way. --public-key <pem> takes a key you obtained out of band: the key in the sidecar must then match it, or verification exits 3 with Signed by an untrusted key, and that is the only check that authenticates the signer. Exit 0 is a valid signature. Exit 3 is an invalid signature, a key mismatch, a statement that is not valid JSON, a sidecar that is not ed25519, or a sidecar with no public key when --public-key is not given. Exit 2 is an unreadable input (including an unreadable --public-key file or a sidecar that is not a JSON object) or a statement or sidecar path outside the current directory. soup attest verify has no --strict option (that flag belongs to soup adapters verify). A pass also prints that the subject digest is asserted by the signer and is not re-checked against any file.

--sign sigstore is accepted and is not a signing backend. Sigstore keyless stays infra-blocked (needs an OIDC provider + Fulcio/Rekor), but the flag is not refused: attest emit prints a "Signing infra-blocked" notice, falls back to an unsigned statement, writes no .sig file and exits 0. A pipeline that passes --sign sigstore therefore believes it signs and does not. Only unsigned and ed25519 produce what their names say (soup adapters sign --backend sigstore, by contrast, refuses with exit 2).

soup audit-log — HIPAA/SOC2 audit trail

Every command execution records timestamp, command-line, exit code, operator identity, and host into ~/.soup/audit.jsonl (or $SOUP_AUDIT_LOG_PATH). Token-shaped values and the values of credential-named options are masked before write; nothing else is redacted. The masking covers values that look like hf_..., sk-... or Bearer ..., plus (from v0.75.1) the values of options whose name contains token, key, secret, password or credential, described below. A Slack or Discord webhook URL passed with --slack-url or --discord-url carries its secret in the URL, matches neither rule, and is recorded in full. Every other argument (a prompt passed with --goal, a file path, a model id), the operator's OS user name and the host name are recorded as typed, so do not put personal data or webhook URLs on a command line whose audit trail you ship to a collector.

Until v0.75.1 that masking was by value SHAPE, not by option name, and three credential flags fell through it. A value was masked if it looked like hf_..., sk-... or Bearer ...; anything else was written verbatim. So --tool-auth-token, --auth-token and --api-key, whose values usually look like none of those, were recorded in full, in a log this very page encourages you to collect centrally (0.71.3 to 0.75.0). Masking is now keyed on the option name, covering long and short forms and both --opt value and --opt=value. If you passed any of those three on an affected version and your audit log is collected anywhere, rotate those tokens and purge the log.

bash
# Tail the most recent 100 records (rich table)
soup audit-log tail --limit 100

# Raw JSONL for piping
soup audit-log tail --limit 50 --json

# Rotate at a 500 MB cap
soup audit-log rotate --cap-mb 500

Both subcommands take --path <file> to act on a log other than the default ($SOUP_AUDIT_LOG_PATH, else ~/.soup/audit.jsonl). Unlike SOUP_AUDIT_LOG_PATH, which is honoured only inside your home, current or temp directory, --path is not checked. rotate renames whatever file you name, audit log or not, to <file>.1 when it is larger than --cap-mb (1 to 10000, default 100), replacing an existing <file>.1; otherwise, or when the file does not exist, it prints No rotation needed.

Turning it off

It is on by default for every command, so the opt-out belongs next to the feature rather than three pages away:

bash
soup --no-audit-log train --config soup.yaml   # global flag, before the subcommand
SOUP_NO_AUDIT_LOG=1 soup train --config soup.yaml

Both suppress the one-line record entirely. $SOUP_AUDIT_LOG_PATH is the third control, and it moves the file rather than silencing it, which is what you want when the default location is the problem and the record is not.

EU AI Act Annex XI/XII

soup train ships two new flags that emit the documentation required by the EU AI Act:

bash
soup train --config soup.yaml \
  --annex-xi ./docs/annex-xi.md \
  --repro-receipt ./receipts/repro.json

The reproducibility receipt captures every seed, kernel version, library version, and dataset hash needed to reproduce the run under SR 11-7 model-risk-management standards.

CO₂ energy tracking schema

Energy is tracked by the CLI, not by a config block: soup train --track-energy (with --energy-country) records consumption offline via codecarbon, and soup bom emit --energy folds the measurement into the BOM and the Annex XI doc. There is no co2: key in soup.yaml, and no live grid-intensity lookup.

Numbers

+93 new tests in v0.59.0 (9193 → 9286).

See also

  • Compliance pack — v0.71.35 wraps these commands into a regime-shaped workflow (soup init --template, soup card, soup ci init).
  • Supply-chain security — v0.60 LoRA backdoor scanner, Merkle signing, air-gap bundles.
  • Registry — every BOM and attestation can be attached as an artifact.
  • Pre-flight & tooling (v0.64) — soup license-advisor --target b2c|defense|embedded returns ok/warn/block per (license, deploy-target, MAU) and composes with the v0.59 license-matrix on soup adapters merge.
  • Adapter lifecycle (v0.67) — soup lock SHA256(base || dataset || env) closure makes governance artifacts reproducible across teams.

Soup is free and Apache-2.0. If it saved you a training run, starring the repo costs nothing and helps most. You can also fund the GPU time behind the work a 4 GB laptop cannot reach.