build_provenance (manifest.rs:694) normalizes sentinels and empties to field omission, which is the right call — an omitted field is an honest "no claim" and the sentinel filter stops "unknown" publishing as a fact. But the wire then carries only the fact of absence, never its cause. Three materially different situations produce a byte-identical frame:
| situation |
what it means for a census |
on the wire |
| declined-dirty — the build had uncommitted changes, so the stamper refused to name a commit |
the binary is real but unattributable; someone built from a modified tree |
build_git_sha absent |
never-derived — the release path does not stamp SUBC_BUILD_GIT_SHA at all |
the module has no provenance pipeline; nothing is wrong with this build |
build_git_sha absent |
| no-git-dir — built from a tarball/vendored source with no repository |
attribution is impossible by construction, not by policy |
build_git_sha absent |
The first is a warning about this artifact. The second is a gap in a repo's release machinery. The third is a permanent property of how it was built. A fleet census reading build_git_sha: absent cannot tell them apart, so the only available response is the union of all three — and in practice that means the signal gets ignored.
Note what is already distinguishable and does not need fixing: block-absent vs field-absent. build_provenance returns the block unconditionally (wire_crate_version is always knowable from SUBC_PROTOCOL_CRATE_VERSION), so "made no claim at all" and "claimed, but cannot name a commit" are already different frames. Cerebellum corrected me on exactly this when I first drafted the finding as smuggling-prevention — that framing argues for something the wire already has. The gap is narrower and real: absence has no cause.
Ask
An optional reason beside the fact, wire-additive in the established shape (Option, skip_serializing_if), costing old readers nothing:
/// Why `build_git_sha` is absent. Present only when the field is absent;
/// a stamper that names a commit says nothing here.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub build_git_sha_absence: Option<ShaAbsence>, // DeclinedDirty | NeverDerived | NoGitDir
Two riders I'd want in the implementation rather than left to callers:
-
The absence reason must be derived by the same helper that decides the omission. If callers pass it in alongside the sha, the two can disagree — a module could declare NeverDerived while passing a sha, or claim DeclinedDirty on a clean tree. build_provenance already owns the normalize-and-validate decision; it should own the reason too, which means taking the tree state as an input rather than a pre-computed verdict.
-
The stamp rule should take its inputs as parameters, not read env! inline. This is Cerebellum's point and it is the sharper half of the issue. A stamp that reads env!("SUBC_BUILD_GIT_SHA") inline has a refusing branch that has never executed — you cannot produce a dirty build inside a test without rebuilding, so the branch exists only in the author's intention. A parameterized attestable_commit(rev, tree_state) is testable at clean/dirty/empty/every-sentinel. Whatever shape the absence reason takes, it should be reachable in a test at every value it can hold; otherwise this issue adds a second never-executed branch next to the first.
Ordering
This costs every ManifestProvenance constructor a line, so it wants to land after the constructor work in #84 — which it now does. I raised that dependency when #84 was open as an argument for #84; it is satisfied at 856e26f7.
Related: #83 (helper moved into subc-protocol so transport-direct modules can reach it), #87 (canonical 40/64 hex enforced at construction — a second case of the helper constraining what prose could not). Both are the same pattern this extends: put the decision in the constructor, where it cannot be copied wrong.
Happy to take it. If you'd rather see the absence reason as a bare string than an enum, say so — I'd argue for the enum with a #[serde(other)]-style fallback per #79's ruling on nested diagnostic value enums, since it is exactly that class.
build_provenance(manifest.rs:694) normalizes sentinels and empties to field omission, which is the right call — an omitted field is an honest "no claim" and the sentinel filter stops"unknown"publishing as a fact. But the wire then carries only the fact of absence, never its cause. Three materially different situations produce a byte-identical frame:build_git_shaabsentSUBC_BUILD_GIT_SHAat allbuild_git_shaabsentbuild_git_shaabsentThe first is a warning about this artifact. The second is a gap in a repo's release machinery. The third is a permanent property of how it was built. A fleet census reading
build_git_sha: absentcannot tell them apart, so the only available response is the union of all three — and in practice that means the signal gets ignored.Note what is already distinguishable and does not need fixing: block-absent vs field-absent.
build_provenancereturns the block unconditionally (wire_crate_versionis always knowable fromSUBC_PROTOCOL_CRATE_VERSION), so "made no claim at all" and "claimed, but cannot name a commit" are already different frames. Cerebellum corrected me on exactly this when I first drafted the finding as smuggling-prevention — that framing argues for something the wire already has. The gap is narrower and real: absence has no cause.Ask
An optional reason beside the fact, wire-additive in the established shape (
Option,skip_serializing_if), costing old readers nothing:Two riders I'd want in the implementation rather than left to callers:
The absence reason must be derived by the same helper that decides the omission. If callers pass it in alongside the sha, the two can disagree — a module could declare
NeverDerivedwhile passing a sha, or claimDeclinedDirtyon a clean tree.build_provenancealready owns the normalize-and-validate decision; it should own the reason too, which means taking the tree state as an input rather than a pre-computed verdict.The stamp rule should take its inputs as parameters, not read
env!inline. This is Cerebellum's point and it is the sharper half of the issue. A stamp that readsenv!("SUBC_BUILD_GIT_SHA")inline has a refusing branch that has never executed — you cannot produce a dirty build inside a test without rebuilding, so the branch exists only in the author's intention. A parameterizedattestable_commit(rev, tree_state)is testable at clean/dirty/empty/every-sentinel. Whatever shape the absence reason takes, it should be reachable in a test at every value it can hold; otherwise this issue adds a second never-executed branch next to the first.Ordering
This costs every
ManifestProvenanceconstructor a line, so it wants to land after the constructor work in #84 — which it now does. I raised that dependency when #84 was open as an argument for #84; it is satisfied at856e26f7.Related: #83 (helper moved into
subc-protocolso transport-direct modules can reach it), #87 (canonical 40/64 hex enforced at construction — a second case of the helper constraining what prose could not). Both are the same pattern this extends: put the decision in the constructor, where it cannot be copied wrong.Happy to take it. If you'd rather see the absence reason as a bare string than an enum, say so — I'd argue for the enum with a
#[serde(other)]-style fallback per #79's ruling on nested diagnostic value enums, since it is exactly that class.