Skip to content

Commit 0ce888c

Browse files
committed
provenance: publish canonical 40/64-hex forms via build_provenance
subc-protocol 0.17.0 validates build_git_sha as exactly 40 lowercase hex and build_lock_digest as exactly 64. This module published 12 and 16, which is not a shorter answer to the same question: an abbreviated sha can never compare equal to a full one, so a fleet census reads permanent drift where there is only a format difference. Reported against us as iceteaSA #87. Three changes, and only the first was forced. 1. Built through build_provenance() rather than as a struct literal. The literal still compiles, so nothing made this necessary -- it would simply have kept publishing non-conforming forms with the validation sitting unused one call away. On a form error the block is omitted rather than defaulted or fatal: a build that cannot state a conforming fact says nothing, exactly as one that could not establish the fact at all does. 2. Full sha instead of the 12-char abbreviation. health.metrics.buildCommit keeps its short form -- it answers a skew question against a local rev-parse and is documented as carrying a clean sha from a dirty tree, which provenance must not. 3. Cargo.lock digest is now SHA-256 rather than DefaultHasher. The width was the reported defect; the algorithm was the worse one. DefaultHasher is explicitly not guaranteed stable across Rust releases, so the same lockfile could stamp differently after a toolchain bump -- and this value exists to say whether two builds resolved the same dependencies. A digest that changes without its input changing answers that question wrongly while looking correct. The test now pins 64 lowercase hex explicitly, because the constructor OMITS a non-conforming value rather than rejecting it: a regression would publish no digest at all, and absent is indistinguishable from never-stamped. wire_crate_version is filled by the constructor from the linked crate, so the local helper is gone. Its assertion stays: the value moved upstream, the claim this module publishes did not, and a test that stops checking a published field because a dependency computes it is trusting that dependency to keep meaning what it meant.
1 parent 2ed5a45 commit 0ce888c

4 files changed

Lines changed: 72 additions & 52 deletions

File tree

‎Cargo.lock‎

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎crates/quota-module/Cargo.toml‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,5 +30,12 @@ async-trait = { workspace = true }
3030
tokio = { workspace = true }
3131
tokio-util = { workspace = true }
3232

33+
# build.rs stamps a lock digest that the manifest publishes as provenance, and
34+
# subc-protocol validates it as exactly 64 lowercase hex -- a real SHA-256 rather
35+
# than a hash whose width happens to fit. sha2 is already in the dependency graph
36+
# for the runtime, so this adds no new third-party surface.
37+
[build-dependencies]
38+
sha2 = { workspace = true }
39+
3340
[dev-dependencies]
3441
subc-core = { workspace = true }

‎crates/quota-module/build.rs‎

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,13 @@ fn main() {
8888
// and yields None on every worktree build.
8989
match (clean, locate_git_dir().as_deref().and_then(head_commit)) {
9090
// The ONLY case that may state a commit: we looked, and it was clean.
91-
(Some(true), Some(sha)) => short(&sha),
91+
//
92+
// FULL 40-hex, not the abbreviated form the health stamp uses.
93+
// subc-protocol validates provenance shas as exactly 40 lowercase
94+
// hex, and an abbreviated one is not a shorter answer to the same
95+
// question -- it false-drifts a fleet census against every module
96+
// publishing the full form, because the two can never compare equal.
97+
(Some(true), Some(sha)) => sha,
9298
// Dirty, or undeterminable, or no HEAD. Empty string, which the
9399
// consumer maps to None -- a sentinel must never render as a value.
94100
_ => String::new(),
@@ -162,12 +168,22 @@ fn tree_clean(worktree_root: &Path) -> Option<bool> {
162168
/// Deliberately not gated on tree cleanliness. It is a change-detector, not an
163169
/// identity claim — it answers "are these two builds the same dependencies",
164170
/// which stays true and useful on a dirty tree where the sha does not.
171+
/// SHA-256 of `Cargo.lock`, rendered as 64 lowercase hex.
172+
///
173+
/// WAS A `DefaultHasher`, which is wrong here for two independent reasons and
174+
/// only one of them is the width. `DefaultHasher`'s algorithm is explicitly not
175+
/// guaranteed stable across Rust releases, so the same lockfile could stamp
176+
/// differently after a toolchain bump -- and this value's entire job is to say
177+
/// whether two builds resolved the same dependencies. A digest that changes
178+
/// without its input changing answers that question wrongly while looking fine.
179+
///
180+
/// The width matters too: subc-protocol validates this as exactly 64 hex, and a
181+
/// 16-hex value false-drifts a fleet census against modules publishing full
182+
/// digests.
165183
fn lock_digest(worktree_root: &Path) -> Option<String> {
166-
use std::hash::{Hash, Hasher};
184+
use sha2::{Digest, Sha256};
167185
let text = std::fs::read(worktree_root.join("Cargo.lock")).ok()?;
168-
let mut hasher = std::collections::hash_map::DefaultHasher::new();
169-
text.hash(&mut hasher);
170-
Some(format!("{:016x}", hasher.finish()))
186+
Some(format!("{:x}", Sha256::digest(&text)))
171187
}
172188

173189
fn is_stamp(raw: &str) -> bool {

‎crates/quota-module/src/main.rs‎

Lines changed: 42 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,8 @@ use serde_json::json;
3636
use subc_protocol::{
3737
manifest::{
3838
Bindings, Concurrency, IdentityBinding, ManagementOperation, ManagementOperationKind,
39-
ManifestProvenance, ModuleManifest, ProviderRole, SelfSignalDeclaration, SelfSignalEffect,
40-
SelfSignalKind, SignalAnchor, SignalCadence, StorageBinding, StorageKind, StorageScope,
41-
TrustTier,
39+
ModuleManifest, ProviderRole, SelfSignalDeclaration, SelfSignalEffect, SelfSignalKind,
40+
SignalAnchor, SignalCadence, StorageBinding, StorageKind, StorageScope, TrustTier,
4241
},
4342
session::{
4443
HealthStatus, ModuleControlRequest, ModuleControlResponse, MODULE_CONTROL_OP_HEALTH_CHECK,
@@ -1071,28 +1070,6 @@ fn build_lock_digest() -> Option<String> {
10711070
(!raw.is_empty()).then(|| raw.to_string())
10721071
}
10731072

1074-
/// The subc-protocol version this binary LINKS.
1075-
///
1076-
/// THE REFERENT IS THE FLEET'S WIRE CRATE, NOT OURS. An earlier version declared
1077-
/// `cortexkit-provider-usage` here -- our own envelope crate, read from the
1078-
/// lockfile. That is real information about this module and the wrong answer to
1079-
/// this field, and the two live in different numbering spaces: a census gate of
1080-
/// the form `wire_crate_version >= 0.13.0` would have scored this module
1081-
/// non-conformant while it demonstrably speaks the current protocol. A confident
1082-
/// wrong answer at a gate, which is worse than declaring nothing.
1083-
///
1084-
/// Taken from the linked constant rather than from a lockfile read, which is also
1085-
/// strictly better under the contract's from-the-build rule: `env!` in the crate
1086-
/// we actually link describes THE BINARY, while a lockfile describes the source
1087-
/// tree that happened to sit beside it at build time.
1088-
///
1089-
/// Our provider-usage version is still worth declaring somewhere -- but that is a
1090-
/// case for a second field, not for redefining this one, and minting fleet wire
1091-
/// fields is not ours to do unilaterally.
1092-
fn wire_crate_version() -> Option<String> {
1093-
Some(subc_protocol::SUBC_PROTOCOL_CRATE_VERSION.to_string())
1094-
}
1095-
10961073
fn manifest(module_id: &str) -> ModuleManifest {
10971074
// Built through the builder rather than as a literal: `ModuleManifest` is
10981075
// `#[non_exhaustive]` upstream, so a field added there becomes a compile
@@ -1224,28 +1201,34 @@ fn manifest(module_id: &str) -> ModuleManifest {
12241201
),
12251202
},
12261203
]))
1227-
.provenance(Some(ManifestProvenance {
1228-
// Populated rather than left None, because every field here is one this
1229-
// module already knows and a consumer otherwise has to ask a human for.
1204+
// Built through `build_provenance` rather than as a struct literal, and that
1205+
// is the difference between declaring these facts and declaring them in a
1206+
// form anyone can join on. The literal still compiles, so nothing forced
1207+
// this -- it would simply keep publishing a 12-hex sha beside other modules'
1208+
// 40-hex ones, which can never compare equal and read as permanent drift
1209+
// rather than as a format difference. Reported against this module as
1210+
// iceteaSA #87.
12301211
//
1231-
// `build_git_sha` is NOT the same value as `health.metrics.buildCommit`,
1232-
// and an earlier version of this code reused it. That stamp is HEAD, so a
1233-
// dirty build carries a clean sha -- correct for the skew check it serves
1234-
// and wrong for an identity claim. Reported on insula#12.
1212+
// ON A FORM ERROR THE WHOLE BLOCK IS OMITTED, not defaulted and not fatal: a
1213+
// build that cannot state a conforming fact should say nothing, exactly as a
1214+
// build that could not establish the fact at all does.
12351215
//
1236-
// `store_schema_version` stays None as an ABSENT FACT rather than an
1237-
// unfilled blank: this module owns no persistent store, so it has no
1238-
// schema version to state.
1216+
// `build_git_sha` is NOT `health.metrics.buildCommit`, and an earlier version
1217+
// reused it. That stamp is HEAD, so a dirty build carries a clean sha --
1218+
// correct for the skew check it serves and wrong for an identity claim.
1219+
// Reported on insula#12.
12391220
//
1240-
// The other three are conditional by construction. Each is emitted by
1241-
// build.rs or not at all, and each maps an empty stamp to None -- so a
1242-
// build that could not establish a fact declares nothing rather than
1243-
// declaring a plausible one.
1244-
build_git_sha: build_provenance_sha(),
1245-
build_lock_digest: build_lock_digest(),
1246-
wire_crate_version: wire_crate_version(),
1247-
store_schema_version: None,
1248-
}))
1221+
// `store_schema_version` is None as an ABSENT FACT rather than an unfilled
1222+
// blank: this module owns no persistent store. `wire_crate_version` is filled
1223+
// by the constructor from the linked crate, so it is no longer ours to pass.
1224+
.provenance(
1225+
subc_protocol::manifest::build_provenance(
1226+
build_provenance_sha().as_deref(),
1227+
build_lock_digest().as_deref(),
1228+
None,
1229+
)
1230+
.ok(),
1231+
)
12491232
.capabilities(
12501233
// No versioned capability grammar declared. `None` and an empty block are
12511234
// NOT the same statement: the protocol makes this optional precisely so a
@@ -1401,15 +1384,28 @@ mod tests {
14011384
let digest = p
14021385
.build_lock_digest
14031386
.expect("build_lock_digest is hashed at build time from Cargo.lock");
1387+
// 64 lowercase hex, the form subc-protocol validates. Asserted here as
1388+
// well as upstream because the constructor OMITS a non-conforming value
1389+
// rather than failing: a build that regressed to a shorter digest would
1390+
// publish no digest at all, and "absent" is a legitimate state that no
1391+
// consumer can distinguish from "never stamped".
14041392
assert!(
1405-
digest.len() == 16 && digest.chars().all(|c| c.is_ascii_hexdigit()),
1406-
"build_lock_digest must be a 16-char hex hash, got {digest:?}"
1393+
digest.len() == 64
1394+
&& digest
1395+
.chars()
1396+
.all(|c| c.is_ascii_hexdigit() && !c.is_uppercase()),
1397+
"build_lock_digest must be 64 lowercase hex, got {digest:?}"
14071398
);
14081399

14091400
// THE REFERENT IS THE FLEET'S WIRE CRATE. Asserted by identity against the
14101401
// linked constant, not by shape: a semver-shaped check passes just as
14111402
// happily on our own provider-usage version, which is what this field
14121403
// previously carried and what a census gate would have misread.
1404+
//
1405+
// Still asserted although `build_provenance` now fills this itself. The
1406+
// value moved upstream; the CLAIM this module publishes did not, and a
1407+
// test that stops checking a published field because someone else
1408+
// computes it is trusting a dependency to keep meaning what it meant.
14131409
let wire = p
14141410
.wire_crate_version
14151411
.expect("wire_crate_version is the linked subc-protocol version");

0 commit comments

Comments
 (0)