The clean-machine installed journey

This is the whole human release-evidence workflow, walked end to end on a clean, offline machine, through the installed sbomflow command only — from installing the wheel to a four-layer bundle verification, including a real human review with an attributed reviewer, an enforced release gate, and a tamper that is caught and recovered.

Every step below is exercised by an offline harness that builds the wheel, installs it into a fresh virtual environment, unsets PYTHONPATH so only the installed package can be imported, blocks every network transport, and then runs each command and asserts that the guidance on this page actually appears. The harness writes one machine-readable journey record with every username, temporary path, and version normalised out, so two runs at a pinned time are byte-identical. A recipe here cannot rot into something that no longer works.

Three rules hold throughout:

  • Offline by default. Nothing on this page reaches the network. The bundled sample advisory feed is the clearly non-real CVE-SAMPLE-* feed.
  • Observed is not reviewed. The engine observes; a human decides. The review and approval steps record real, attributed decisions — never fabricated, never promoted from an observation. See observed vs reviewed.
  • Exit codes are the contract. 0 healthy, 1 an enforced gate said no, 2 usage/input, 3 a bundle verification failure, 4 integrity/structural, 5 needs attention. See exit codes.
StepWhat it proves
OJ-01Access and install verify
OJ-02Environment doctor
OJ-03Quickstart (init + first offline audit)
OJ-13Quickstart re-run into the same output
OJ-04Offline audit
OJ-05Inspect and explain the gate
OJ-06Human review — attributed
OJ-07Human approval — attributed
OJ-08Enforced release gate
OJ-09Portable evidence bundle
OJ-10Sign / attest (optional)
OJ-14Read-only review after the bundle
OJ-11Four-layer verify — directory
OJ-12Four-layer verify — archive

OJ-01 — Access and install verify#

The wheel arrives through the controlled access channel, never from a package index — there is no public index entry for v0.5.0. Check the bytes you received against the published SHA-256 before installing them:

bash
sha256sum sbomflow-0.5.0-py3-none-any.whl      # Linux
shasum -a 256 sbomflow-0.5.0-py3-none-any.whl  # macOS

The v0.5.0 digest is 89a26452037dcaf523ff94132cc6c4ae92273f0c9aa82d8c15eab52342b54928. A matching digest proves the bytes you received match the bytes the build produced. It is not a signature and does not establish who built them; this build is unsigned and carries no continuous-integration attestation.

Then, on a machine with no package-index egress, install the wheel offline and confirm the binary is on the path:

bash
pip install --no-index --no-deps sbomflow-<version>-py3-none-any.whl
sbomflow --version
sbomflow --help

sbomflow --version prints sbomflow <version>; sbomflow --help lists every command. The harness additionally proves the imported package resolves from the installed wheel, not from any source tree.

OJ-02 — Environment doctor#

Ask the tool what it sees about this machine before doing any work:

bash
sbomflow doctor .

doctor reports environment and config facts — the python: version, which optional extra packages are present, and the discovered config: — and never touches the network. Absent optional extras are stated as absent, never hidden.

OJ-03 — Quickstart#

One command auto-detects the project, runs a first offline audit, scaffolds a starter config, and prints copy-paste next steps:

bash
sbomflow quickstart . --output ./sbomflow-evidence

It prints detected: with the product it found, running offline audit, and a Next steps block, finishing with SBOMFlow quickstart complete. The gate here is informational; nothing is enforced yet.

OJ-13 — Quickstart re-run into the same output#

Run the same quickstart again, into the same output directory:

bash
sbomflow quickstart . --output ./sbomflow-evidence

A re-run is safe: it exits as the first run did and finishes with SBOMFlow quickstart complete. again. With the inputs and --as-of pinned, the evidence pack and bundle are byte-reproducible and sbomflow validate passes on the output; quickstart records timing metrics, so metrics.json and run-completion.json differ between runs. The journey checks the exit, the pack and validate, so a second run that fails or leaves inconsistent evidence is caught here rather than by a user.

OJ-04 — Offline audit#

Produce the canonical, deterministic evidence pack and a portable bundle (run from inside your product directory, shown here as .):

bash
sbomflow audit . --output ./evidence \
  --product-name "Your Product" --product-version 1.0.0 --as-of 2026-06-16T12:00:00+00:00

The summary line starts sbomflow audit, states what it observed, and prints the gate verdict. The run writes evidence-pack.json and the full artifact set; with inputs and --as-of pinned, the artifacts are byte-reproducible.

OJ-05 — Inspect and explain#

Read the gate back in plain English:

bash
sbomflow explain ./evidence --gate

It prints Release gate: with the verdict and a Reviewer boundary: line stating the gate is deterministic policy — not a legal or conformity decision.

OJ-06 — Human review#

Machine observation is not human review. Record a real, attributed evidence acceptance — the decision names its reviewer and appends a hash-chained audit event:

bash
sbomflow review ./evidence --accept vulnerability_disclosure_policy \
  --reviewer you@example.com --reviewed-at 2026-06-16T12:00:00+00:00

The command prints Recorded … with the audit event appended: path. An acceptance without a --reviewer is refused as unattributed — a human decision must name its author.

OJ-07 — Human approval#

Record an attributed release sign-off for a required role:

bash
sbomflow approve ./evidence --role release-manager \
  --reviewer approver@example.com --as-of 2026-06-16T12:00:00+00:00

There is no actorless way to satisfy a quorum; the --role grant is an attributed, audited record.

OJ-08 — Enforced gate#

Now re-run the gate as an enforced policy. The approval just recorded satisfies the required role, so the same policy that would block now passes:

bash
sbomflow audit . --output ./evidence \
  --product-name "Your Product" --product-version 1.0.0 \
  --require-role release-manager --fail-on-missing-approvals \
  --approvals ./evidence/approvals.json --as-of 2026-06-16T12:00:00+00:00

With --require-role and --fail-on-missing-approvals, an unmet role exits 1; here the gate prints PASS and exits 0 because the quorum is complete.

OJ-09 — Evidence bundle#

Assemble the portable reviewer/auditor handoff and its deterministic zip:

bash
sbomflow bundle ./evidence --zip ./evidence/evidence-bundle.zip \
  --as-of 2026-06-16T12:00:00+00:00

This composes existing artifacts into evidence-bundle.json and a reproducible evidence-bundle.zip. The bundle adds no authority of its own; it is a handoff, not a conclusion.

OJ-10 — Sign or attest#

Signing is an optional capability that needs the sbomflow[sign] extra — run sbomflow sign-bundle ./evidence --key reviewer.key with your local PEM key (add --format in-toto for a DSSE attestation). It writes a signature over the bundle manifest (integrity/authenticity only, never conformity). When the cryptography extra is not installed, this step is skipped honestly — the harness never produces a fake signature — and the four-layer verify below still checks content, offline, with the standard library alone. (Shown inline because it needs the optional extra and a key, so the offline docs harness cannot run it; the offline harness exercises it end to end where the extra is present.)

OJ-14 — Read-only review after the bundle#

Looking at the review queue again after the bundle is built must not break it. With no decision flags, sbomflow review is read-only (it rewrites review-queue.json only when its content changed):

bash
sbomflow review ./evidence
sbomflow validate ./evidence

It prints the queue and Record a decision: guidance. The journey runs sbomflow validate before and after the review and requires exit 0 both times, even though a decision was recorded earlier in this journey. The verify steps that follow run against the bundle built before this review.

OJ-11 — Four-layer verify (directory)#

Verify the whole bundle, not just its manifest:

bash
sbomflow verify-bundle ./evidence --layered

It reports four independent dimensions, never collapsed into one verdict: content (every listed member is present and matches its recorded sha256), envelope (the signature verifies over the manifest bytes), semantics (a DSSE attestation has the expected payload type and binding), and authority (the verified signer is on your allow-list). Exit 0 means no dimension failed; exit 3 means one did. For an unsigned bundle, content verifies and the run exits 0, but the overall verdict is honestly NOT TRUSTED — full trust needs a signature and an authorised-signer policy, which is what OJ-10 adds. See read an evidence bundle.

This is the form audit and bundle now advertise on completion: sbomflow verify-bundle <dir> --layered --require-layers content. The bare sbomflow verify-bundle <dir> checks the SIGNATURE, so on a bundle nobody has run sign-bundle on it refuses with E042 — which is every bundle a first run produces. It now names the layered form in its own fix line rather than sending you to look for key material you do not have.

OJ-12 — Four-layer verify (archive)#

The same four-layer check runs against a .zip archive of the bundle — point it at the archive with sbomflow verify-bundle ./evidence/evidence-bundle.zip --layered (shown inline because the offline docs harness does not seed a zip; the OJ-09 zip is verified for real by the offline harness).

Hostile archives (path traversal, symlinks, duplicate members, decompression bombs) are refused before any byte is trusted, and nothing is extracted outside a temporary working directory.


When something goes wrong — failure injections#

Each scenario below creates a real failure and recovers it strictly by its published playbook — no hand-editing files, no deleting journals. The harness runs all six and records the diagnose → recover path for each.

The count is part of the verdict. A run that records fewer than six rows, no rows at all, or six rows that were every one of them skipped is refused and names why — it is never reported as a pass. "Nothing was injected" and "everything recovered" must never produce the same answer.

FI-01 — Wrong directory#

Point the tool at the wrong directory — a path that does not exist, or a single file — and the failure explains itself: [E001] (target not found) or [E002] (not a directory), each with a one-line fix and a docs link, exit 2. Recover by pointing at a real product directory. See your first audit.

FI-02 — Evidence gap or unmet approval role#

Enforce a required sign-off role with no approval on file and the gate blocks: exit 1, reason code GATE_MISSING_APPROVALS, naming the unmet role. explain --gate restates it. Recover with an attributed sbomflow approve --role … and re-run — the gate passes once the role is covered. Follows PB-08. The sibling case, an evidence gap, is closed by supplying real evidence and a human acceptance, never inference.

FI-03 — Invalid bundle content#

A bundle whose listed member is absent is not partially trusted: the layered content dimension reports member_missing and the run exits 3. Recover by re-exporting from the canonical output directory (re-run the audit), then verify again. See read an evidence bundle.

FI-04 — Unauthorized signer#

A cryptographically valid signature from a signer you did not authorise is reported valid_but_unauthorized and is not trusted (exit 3). Recover by confirming you hold the right key and adding its fingerprint to the --authorized-key-sha256 allow-list. This dimension needs a real signature, so it runs only when the sbomflow[sign] extra is present — otherwise it is skipped honestly. See read an evidence bundle.

FI-05 — Tamper (alter one non-identity artifact)#

Alter a single non-identity artifact inside the bundle — a derived coverage report, not the product identity — and the content dimension catches it: member_hash_mismatch, verdict NOT TRUSTED, exit 3, even though the manifest signature would still be intact. The documented recovery is a re-export from the canonical output directory (re-run the audit); because artifacts are deterministic, the true bytes are restored and the next verify exits 0. Never hand-edit a member to a new shape.

FI-06 — Interrupted decision transaction#

If a decision command is killed between writing the decision file and appending its audit event, a recoverable journal is left behind — never a silently half-recorded decision. Diagnose with sbomflow decisions verify-transactions (exit 5, a recoverable row) and heal it with sbomflow decisions recover --operation-id <id>; a clean re-verify follows. Follows PB-01.


The same journey for an AI-enabled build#

If your release contains models, agent-tool configurations or an AI-BOM, the workflow above is unchanged — the AI evidence simply joins it. make ai-onboard-check walks that arc end to end on the same clean, offline machine, through the installed command only, over a generated synthetic AI-enabled product. It is the same wheel, the same fresh virtual environment and the same blocked network as the journey above; only the product and the evidence differ.

StepWhat it proves
AIJ-01Offline audit of an AI-enabled build
AIJ-02The AI work a human has to look at
AIJ-03Human review — attributed
AIJ-04Enforced AI control — the gate blocks
AIJ-05Resolve in the product, then re-run
AIJ-06Portable evidence bundle
AIJ-07Four-layer verify — directory
AIJ-08Four-layer verify — archive
AIT-01Tamper with the AI record

AIJ-01 — Offline audit of an AI-enabled build#

bash
sbomflow audit . --output ./evidence --emit-aibom \
  --as-of 2026-06-16T12:00:00+00:00

Alongside the ordinary evidence pack this writes ai-evidence.json — the AI inventory, the evidence graph and what could not be compared — and, with --emit-aibom, an SPDX 3.0.1 AI-BOM you can hand to someone else. Both are written on every run of an AI-enabled build, including the run that found nothing: an absent record and an empty one mean different things, and only the artifact can tell you which you are holding.

AIJ-02 — The AI work a human has to look at#

bash
sbomflow explain ./evidence --gate

The gate explanation carries an AI evidence: section listing what a person has to look at: conflicts, entities nothing was declared about, and the outcome of any AI control. In the synthetic product two release files describe the same MCP service differently, so the section names the disagreement and marks it human review required. None of those rows is a defect or a decision — they are questions with no recorded answer yet.

Nothing on this surface resolves a disagreement for you. Both values are kept, attributed to the file each came from; silently preferring one would destroy the only evidence that they ever differed.

AIJ-03 — Human review — attributed#

bash
sbomflow review ./evidence --accept vulnerability_disclosure_policy \
  --reviewer you@example.com --reviewed-at 2026-06-16T12:00:00+00:00

Exactly as in the conventional journey: a real, attributed decision about something the engine actually observed. Observed status is never promoted to reviewed status, and no AI observation records a decision for you. See observed vs reviewed.

AIJ-04 — Enforced AI control#

Every AI control is off until you write it down, so the AI surface stays informational until you decide otherwise. Turn one on in your own policy file:

json
{"policy": {"name": "ai-conflict-blocking", "version": "1",
            "fail_on_ai_evidence_conflict": true}}
bash
sbomflow audit . --output ./evidence --config ./ai-policy.json \
  --as-of 2026-06-16T12:00:00+00:00

The gate now reports BLOCKED (enforced) and names fail_on_ai_evidence_conflict as the reason, exit 1. A gate that refuses without saying what refused is not something you can act on, so the control that blocked is always named. See exit codes.

AIJ-05 — Resolve and re-run#

The disposition of a conflict is to resolve the disagreement in the product — here, making the two agent-tool configurations agree — and then re-run the audit. The gate passes because the release stopped contradicting itself, not because an artifact was edited or an observation suppressed.

If the two values are both correct for different environments, that is worth recording in your own release notes; what you must not do is edit a produced artifact, because the next run will simply observe the tree again.

AIJ-06 — Evidence bundle#

bash
sbomflow bundle ./evidence --zip ./evidence/evidence-bundle.zip \
  --as-of 2026-06-16T12:00:00+00:00

ai-evidence.json is a listed bundle member with its own digest, so the AI record travels with the rest of the evidence and is covered by the same integrity check.

AIJ-07 — Four-layer verify (directory)#

bash
sbomflow verify-bundle ./evidence --layered

The same four dimensions — content, envelope, semantics, authority — reported separately. Without the sbomflow[sign] extra the verdict is honestly NOT TRUSTED: content verifies, and no signer authority was established.

AIJ-08 — Four-layer verify (archive)#

Point the same command at the archive written by AIJ-06 — sbomflow verify-bundle ./evidence/evidence-bundle.zip --layered.

The portable archive (.zip) verified as a content-integrity snapshot, exactly as in the conventional journey.

AIT-01 — Tamper with the AI record#

Alter ai-evidence.json inside the bundle by a single byte and the content dimension catches it by name: member_hash_mismatch: ai-evidence.json, verdict NOT TRUSTED. The recovery is a re-export from the canonical output directory — never a hand-edit. Because the AI record is deterministic, restoring it restores the original bytes and the next verify exits 0.

What this journey does not walk#

It exercises observed AI evidence. The declared side is reachable with --ai-manifest, and this journey does not pass one, so the declared-versus-observed arc is not walked here; with no manifest supplied, the reconciliation in ai-evidence.json says plainly that nothing was compared, which is not the same as agreement.

The synthetic product does carry a supplier's declared AI-BOM, and the journey asserts that the installed binary reads it, preserves its original by digest and folds nothing from it — the state before anybody has reviewed it. The other half, an operator recording an acceptance and the supplier's entities entering the graph, is not walked here: it needs a reviews file this journey does not supply, and ai-evidence.json says which of the two states it is holding rather than leaving a reader to assume. See capabilities for what is available today.


The journey record#

The offline harness writes one normalised JSON record: per step the command, the expected and observed exit code, and the deterministic artifact digest; per injection the diagnose and recover commands with their exit codes; and whether the sign/attest step ran or was skipped. Usernames, temporary paths, and the version are normalised to stable placeholders, so a rerun at the same pinned time is byte-identical. Artifact digests are preserved as deterministic facts.

The AI-journey harness writes the same shape for the AI arc: per step the command and its exit code, the conflicts that were observed, how they were disposed of, whether the tamper was detected, and whether ai-evidence.json was byte-identical across two runs at the pinned time. A run that reported zero steps, or zero conflicts, is refused rather than printed as a pass — a journey that asserted nothing is not a journey that succeeded.

See also#