Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
aa7939c
fix(registryctl): separate evidence from programme decisions
jeremi Jul 23, 2026
0c07fbf
feat(registryctl): define the 1.0 adopter contract
jeremi Jul 23, 2026
23127c0
fix(release): bind upgrade evidence to target source
jeremi Jul 23, 2026
758dc09
test(release): bind conformance evidence candidates
jeremi Jul 23, 2026
2e789cd
fix(release): harden offer file reads
jeremi Jul 23, 2026
3462a3d
fix(release): verify candidate tag lineage
jeremi Jul 23, 2026
428210b
fix(platform): harden deployment waiver metadata
jeremi Jul 23, 2026
1ffaf39
docs(release): add external pilot report template
jeremi Jul 23, 2026
5d9b8ba
docs(registryctl): add OpenSPP owner holdout runbook
jeremi Jul 23, 2026
50dc91a
docs: clarify Solmara reader gate
jeremi Jul 23, 2026
7c6477b
fix(relay): remove inert consent provenance
jeremi Jul 23, 2026
72046fa
test(release): isolate historical schema binding
jeremi Jul 23, 2026
04665ba
test(platform): keep waiver fixtures scanner-safe
jeremi Jul 23, 2026
554538b
fix(relay): refresh consent-free state-plane attestation
jeremi Jul 23, 2026
1b66b6f
fix(release): close candidate evidence gaps
jeremi Jul 23, 2026
c943f8b
fix(platform): scan waiver summaries for auth values
jeremi Jul 23, 2026
36459f2
fix: correct adopter exercise evidence semantics
jeremi Jul 23, 2026
cd293a0
fix(release): enforce discovered candidate promotion
jeremi Jul 23, 2026
a6aa976
test(relay): make connection failure deterministic
jeremi Jul 23, 2026
3e07a62
docs: generalize evidence consumer terminology
jeremi Jul 23, 2026
c79e29e
fix governed integration evidence contracts
jeremi Jul 23, 2026
a71b35f
fix(release): authenticate conformance inputs
jeremi Jul 23, 2026
f22f833
fix(platform): reject pasted waiver credentials
jeremi Jul 23, 2026
ca2c60c
fix(relay): match pinned oasdiff ignore output
jeremi Jul 23, 2026
47b451a
Revert "fix(relay): match pinned oasdiff ignore output"
jeremi Jul 23, 2026
12652ff
fix(release): close conformance trust gaps
jeremi Jul 23, 2026
dc862e5
fix(registryctl): validate live results recursively
jeremi Jul 23, 2026
1a8bc96
fix(platform): close waiver punctuation bypasses
jeremi Jul 23, 2026
c263b83
fix(registryctl): align live evidence guarantees
jeremi Jul 23, 2026
156826b
fix(release): authenticate local suite TLS
jeremi Jul 23, 2026
5e1a9f9
fix(registryctl): reject omitted provenance keys
jeremi Jul 23, 2026
ab9f032
fix(release): satisfy TLS security checks
jeremi Jul 23, 2026
6d8aedf
fix(platform): normalize waiver auth boundaries
jeremi Jul 23, 2026
9be46dc
fix(platform): preserve wrapped waiver prose
jeremi Jul 23, 2026
0422c74
fix(registryctl): require expires-at response field
jeremi Jul 23, 2026
c429e2e
fix(release): bind upgrade image lock evidence
jeremi Jul 23, 2026
e6dedcd
docs: clarify the Solmara reader gate
jeremi Jul 23, 2026
ff46647
fix(registryctl): validate provenance constants
jeremi Jul 23, 2026
99ebd8b
docs(release): add integration pilot report contract
jeremi Jul 23, 2026
121be1b
chore: narrow adopter exercise packet scope
jeremi Jul 23, 2026
b379702
fix(release): prepare versioned upgrade assets
jeremi Jul 23, 2026
0a3e1ad
docs(release): require maintainer evidence comparison
jeremi Jul 23, 2026
bcd5b97
docs: complete the Solmara reader criteria
jeremi Jul 23, 2026
e347da9
fix(platform): align waiver schema metadata
jeremi Jul 23, 2026
662a984
fix(release): snapshot candidate inputs
jeremi Jul 23, 2026
1c363a7
Merge origin/main into agent/registryctl-adopter-contract
jeremi Jul 23, 2026
15363d2
Merge origin/main into agent/relay-consent-provenance
jeremi Jul 23, 2026
3a4ff20
Merge origin/main into agent/adopter-exercise-packets
jeremi Jul 23, 2026
2b777d9
Merge origin/main into agent/release-candidate-conformance
jeremi Jul 23, 2026
2a951b5
fix(platform): reject PGP waiver key markers
jeremi Jul 23, 2026
d038858
Merge origin/main into agent/posture-waiver-contract
jeremi Jul 23, 2026
ac964f9
fix(release): require candidate tag closeout
jeremi Jul 23, 2026
8d4ebd9
test(release): exercise equal closeout bytes
jeremi Jul 23, 2026
50d4197
fix(registryctl): close live evidence validation
jeremi Jul 23, 2026
623e189
fix(registryctl): validate governed result identity
jeremi Jul 23, 2026
0ce574c
fix(registryctl): bind live Notary service
jeremi Jul 23, 2026
ce9e33b
fix(registryctl): harden governed live evidence
jeremi Jul 23, 2026
cc304b5
fix(registryctl): bind configured redaction markers
jeremi Jul 23, 2026
970b168
fix(registryctl): validate field redaction markers
jeremi Jul 23, 2026
9b2ef8e
fix(registryctl): bind live evidence to current run
jeremi Jul 23, 2026
a0e9313
fix(release): complete tutorial Debian 13 migration
jeremi Jul 24, 2026
59f2de9
Merge PR #459: bind release conformance evidence to immutable candidates
jeremi Jul 24, 2026
17757b5
Merge PR #462: harden registryctl governed live evidence validation
jeremi Jul 24, 2026
c5e76c9
Merge PR #463: clarify registryctl 1.0 adopter contract
jeremi Jul 24, 2026
0fe02a7
Merge PR #464: harden deployment waiver metadata
jeremi Jul 24, 2026
90ae87f
Merge PR #465: remove inert Relay consent provenance
jeremi Jul 24, 2026
fcb0910
Merge PR #466: clarify the Solmara reader gate
jeremi Jul 24, 2026
e25e636
Merge PR #467: add integration pilot report contract
jeremi Jul 24, 2026
c4caaaa
Merge PR #468: complete the Debian 13 tutorial builder migration
jeremi Jul 24, 2026
c788d49
fix(release): normalize evidence digest coordinates
jeremi Jul 24, 2026
1ec7430
fix(release): normalize artifact set digests
jeremi Jul 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 39 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,9 @@ jobs:
echo "registryctl_tutorial=${registryctl_tutorial}"
} >> "${GITHUB_OUTPUT}"

- name: Check Debian 13 image contract
run: python3 release/scripts/check-debian13-images.py

secrets:
name: Secret scan
runs-on: ubuntu-24.04
Expand Down Expand Up @@ -558,10 +561,42 @@ jobs:
- name: Test upgrade exercise validator
run: python3 -m unittest release/scripts/test_validate_upgrade_exercise.py

- name: Validate upgrade exercise template
- name: Test upgrade exercise asset preparation
run: python3 -m unittest release/scripts/test_prepare_upgrade_exercise_assets.py

- name: Prepare committed upgrade candidate assets
id: upgrade-assets
env:
GH_TOKEN: ${{ github.token }}
run: >-
python3 release/scripts/validate-upgrade-exercise.py --template
release/exercises/upgrade-exercise-v1.template.json
python3 release/scripts/prepare-upgrade-exercise-assets.py
--discover release/exercises
--asset-root target/upgrade-exercise-assets
--github-output "${GITHUB_OUTPUT}"

- name: Install cosign for committed upgrade evidence
if: steps.upgrade-assets.outputs.has_candidates == 'true'
uses: sigstore/cosign-installer@398d4b0eeef1380460a10c8013a76f728fb906ac # v3

- name: Install SLSA verifier for committed upgrade evidence
if: steps.upgrade-assets.outputs.has_candidates == 'true'
env:
SLSA_VERIFIER_SHA256: 946dbec729094195e88ef78e1734324a27869f03e2c6bd2f61cbc06bd5350339
SLSA_VERIFIER_VERSION: v2.7.1
shell: bash
run: |
set -euo pipefail
tools_dir="${RUNNER_TEMP}/upgrade-evidence-tools"
mkdir -p "${tools_dir}"
curl --proto '=https' --tlsv1.2 --fail --location --retry 3 \
--output "${tools_dir}/slsa-verifier" \
"https://github.com/slsa-framework/slsa-verifier/releases/download/${SLSA_VERIFIER_VERSION}/slsa-verifier-linux-amd64"
echo "${SLSA_VERIFIER_SHA256} ${tools_dir}/slsa-verifier" | sha256sum --check
chmod 0755 "${tools_dir}/slsa-verifier"
echo "${tools_dir}" >> "${GITHUB_PATH}"

- name: Validate committed upgrade exercise records
run: python3 release/scripts/validate-upgrade-exercise.py --discover release/exercises --candidate-asset-root target/upgrade-exercise-assets

- name: Stable surface compatibility
env:
Expand Down Expand Up @@ -644,9 +679,7 @@ jobs:
path: |
target/registryctl-tutorial-cargo-home
target/registryctl-tutorial-linux-amd64
key: registryctl-tutorial-${{ runner.os }}-rust-1.95.0-${{ hashFiles('Cargo.lock') }}
restore-keys: |
registryctl-tutorial-${{ runner.os }}-rust-1.95.0-
key: registryctl-tutorial-${{ runner.os }}-${{ hashFiles('docs/site/scripts/check-registryctl-tutorials.sh', 'Cargo.lock') }}

- name: Execute registryctl tutorials from source
working-directory: docs/site
Expand Down
30 changes: 30 additions & 0 deletions crates/registry-notary-core/src/config/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,36 @@ use super::{SigningKeyProviderConfig, SigningKeyStatus, StandaloneRegistryNotary
pub const CONFIG_SCHEMA_ID: &str =
"https://id.registrystack.org/schemas/registry-notary/registry-notary.config.schema.json";

/// Schema-only deployment-waiver reference contract shared with posture.
pub(crate) struct DeploymentWaiverReferenceSchema;

impl JsonSchema for DeploymentWaiverReferenceSchema {
fn schema_name() -> Cow<'static, str> {
"DeploymentWaiverReference".into()
}

fn json_schema(_: &mut SchemaGenerator) -> Schema {
registry_platform_ops::deployment_waiver_reference_schema_fragment()
.try_into()
.expect("the shared waiver-reference fragment is a valid JSON Schema")
}
}

/// Schema-only structural deployment-waiver summary contract shared with posture.
pub(crate) struct DeploymentWaiverSummarySchema;

impl JsonSchema for DeploymentWaiverSummarySchema {
fn schema_name() -> Cow<'static, str> {
"DeploymentWaiverSummary".into()
}

fn json_schema(_: &mut SchemaGenerator) -> Schema {
registry_platform_ops::deployment_waiver_summary_schema_fragment()
.try_into()
.expect("the shared waiver-summary fragment is a valid JSON Schema")
}
}

/// Schema-only contract for values parsed by `humantime_serde`.
pub(crate) struct HumantimeDurationSchema;

Expand Down
55 changes: 54 additions & 1 deletion crates/registry-notary-core/src/config/tests/root.rs
Original file line number Diff line number Diff line change
Expand Up @@ -389,7 +389,8 @@ pub(super) fn deployment_block_round_trips_through_yaml() {
profile: production
waivers:
- finding: notary.openapi.public
reason: synthetic partner integration waiver
reference: OPS-2026-0042
summary: Synthetic partner integration waiver
expires: 2099-09-30
"#,
)
Expand All @@ -403,6 +404,58 @@ waivers:
.expect("production config with waivable waiver validates");
}

#[test]
pub(super) fn deployment_waiver_rejects_legacy_reason_as_unknown() {
let result: Result<crate::deployment::DeploymentConfig, _> = serde_norway::from_str(
r#"
profile: hosted_lab
waivers:
- finding: notary.openapi.public
reference: OPS-2026-0042
reason: legacy waiver text
expires: 2099-09-30
"#,
);
assert!(
result.is_err(),
"the removed reason field must fail strict deserialization"
);
}

#[test]
pub(super) fn deployment_waiver_rejects_missing_reference() {
let result: Result<crate::deployment::DeploymentConfig, _> = serde_norway::from_str(
r#"
profile: hosted_lab
waivers:
- finding: notary.openapi.public
expires: 2099-09-30
"#,
);
assert!(
result.is_err(),
"a waiver without the required reference must fail deserialization"
);
}

#[test]
pub(super) fn deployment_waiver_rejects_explicit_null_summary() {
let result: Result<crate::deployment::DeploymentConfig, _> = serde_norway::from_str(
r#"
profile: hosted_lab
waivers:
- finding: notary.openapi.public
reference: OPS-2026-0042
summary: null
expires: 2099-09-30
"#,
);
assert!(
result.is_err(),
"summary must be a string when present, not null: {result:?}"
);
}

#[test]
pub(super) fn deployment_evidence_block_round_trips_through_yaml() {
let mut config = minimal_config();
Expand Down
151 changes: 132 additions & 19 deletions crates/registry-notary-core/src/deployment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -162,23 +162,54 @@ impl DeploymentEvidenceConfig {

/// One operator-configured waiver.
///
/// A waiver names exactly one finding id, a free-text reason, and a mandatory
/// expiry date (`YYYY-MM-DD`). Reasons must not contain secrets.
/// A waiver names exactly one finding id, a required operator reference, an
/// optional summary, and a mandatory expiry date (`YYYY-MM-DD`). The shared
/// operations contract validates metadata before it can reach posture or logs.
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, JsonSchema)]
#[serde(deny_unknown_fields)]
#[serde(deny_unknown_fields, from = "DeploymentWaiverConfigDocument")]
#[schemars(!from)]
pub struct DeploymentWaiverConfig {
pub finding: String,
pub reason: String,
#[schemars(with = "crate::config::schema::DeploymentWaiverReferenceSchema")]
pub reference: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[schemars(with = "crate::config::schema::DeploymentWaiverSummarySchema")]
pub summary: Option<String>,
pub expires: String,
}

#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct DeploymentWaiverConfigDocument {
finding: String,
reference: String,
#[serde(default)]
summary: registry_platform_ops::OptionalDeploymentWaiverSummary,
expires: String,
}

impl From<DeploymentWaiverConfigDocument> for DeploymentWaiverConfig {
fn from(value: DeploymentWaiverConfigDocument) -> Self {
Self {
finding: value.finding,
reference: value.reference,
summary: value.summary.into(),
expires: value.expires,
}
}
}

/// Errors raised while validating the deployment block at config load.
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum DeploymentConfigError {
#[error("deployment.waivers[{index}].finding must not be empty")]
EmptyWaiverFinding { index: usize },
#[error("deployment.waivers[{index}].reason must not be empty")]
EmptyWaiverReason { index: usize },
#[error("deployment.waivers[{index}].{field} is invalid: {error}")]
InvalidWaiverMetadata {
index: usize,
field: &'static str,
error: registry_platform_ops::DeploymentWaiverMetadataError,
},
#[error("deployment.waivers[{index}].expires must be a YYYY-MM-DD date")]
InvalidWaiverExpiry { index: usize },
#[error(
Expand All @@ -194,19 +225,25 @@ pub enum DeploymentConfigError {
impl DeploymentConfig {
/// Validate the deployment block at config load.
///
/// This checks waiver shape (non-empty fields, parseable expiry) and the
/// hard rule that `startup_fail` and `readiness_fail` gates can never be
/// waived under the declared profile. An undeclared profile still validates
/// waiver shape here so typos are caught early; startup refusal is handled by
/// gate evaluation.
/// This checks waiver shape (shared metadata contract, parseable expiry)
/// and the hard rule that `startup_fail` and `readiness_fail` gates can
/// never be waived under the declared profile. An undeclared profile still
/// validates waiver shape here so typos are caught early; startup refusal
/// is handled by gate evaluation.
pub fn validate(&self) -> Result<(), DeploymentConfigError> {
for (index, waiver) in self.waivers.iter().enumerate() {
if waiver.finding.trim().is_empty() {
return Err(DeploymentConfigError::EmptyWaiverFinding { index });
}
if waiver.reason.trim().is_empty() {
return Err(DeploymentConfigError::EmptyWaiverReason { index });
}
registry_platform_ops::validate_deployment_waiver_metadata(
&waiver.reference,
waiver.summary.as_deref(),
)
.map_err(|error| DeploymentConfigError::InvalidWaiverMetadata {
index,
field: error.field(),
error,
})?;
if parse_iso_date(&waiver.expires).is_none() {
return Err(DeploymentConfigError::InvalidWaiverExpiry { index });
}
Expand Down Expand Up @@ -461,7 +498,8 @@ pub struct EvaluatedFinding {
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EvaluatedWaiver {
pub finding: String,
pub reason: String,
pub reference: String,
pub summary: Option<String>,
pub expires: String,
}

Expand Down Expand Up @@ -518,7 +556,8 @@ pub fn evaluate_gates(
status: DeploymentFindingStatus::Active,
waiver: Some(EvaluatedWaiver {
finding: waiver.finding.clone(),
reason: waiver.reason.clone(),
reference: waiver.reference.clone(),
summary: waiver.summary.clone(),
expires: waiver.expires.clone(),
}),
});
Expand All @@ -536,7 +575,8 @@ pub fn evaluate_gates(
waived_findings.push(waiver);
evaluation.active_waivers.push(EvaluatedWaiver {
finding: waiver.finding.clone(),
reason: waiver.reason.clone(),
reference: waiver.reference.clone(),
summary: waiver.summary.clone(),
expires: waiver.expires.clone(),
});
}
Expand Down Expand Up @@ -568,7 +608,8 @@ pub fn evaluate_gates(
status: DeploymentFindingStatus::Waived,
waiver: Some(EvaluatedWaiver {
finding: waiver.finding.clone(),
reason: waiver.reason.clone(),
reference: waiver.reference.clone(),
summary: waiver.summary.clone(),
expires: waiver.expires.clone(),
}),
});
Expand Down Expand Up @@ -627,7 +668,8 @@ mod tests {
fn waiver(finding: &str, expires: &str) -> DeploymentWaiverConfig {
DeploymentWaiverConfig {
finding: finding.to_string(),
reason: "synthetic test waiver reason".to_string(),
reference: "OPS-TEST-DEPLOYMENT".to_string(),
summary: Some("Synthetic test waiver summary".to_string()),
expires: expires.to_string(),
}
}
Expand Down Expand Up @@ -834,6 +876,77 @@ mod tests {
));
}

#[test]
fn validate_accepts_absent_summary_and_ordinary_metadata() {
let mut waiver_config = waiver(FINDING_OPENAPI_PUBLIC, "2099-01-01");
waiver_config.reference = "OPS-2026:INC_42".to_string();
waiver_config.summary = None;
let config = DeploymentConfig {
profile: Some(DeploymentProfile::HostedLab),
multi_instance: false,
waivers: vec![waiver_config],
evidence: DeploymentEvidenceConfig::default(),
};
config
.validate()
.expect("ordinary reference with absent summary is valid");

let mut waiver_config = waiver(FINDING_OPENAPI_PUBLIC, "2099-01-01");
waiver_config.summary =
Some("Public API catalog approved in the operations ticket".to_string());
let config = DeploymentConfig {
profile: Some(DeploymentProfile::HostedLab),
multi_instance: false,
waivers: vec![waiver_config],
evidence: DeploymentEvidenceConfig::default(),
};
config.validate().expect("ordinary short summary is valid");
}

#[test]
fn validate_rejects_invalid_waiver_metadata_with_field_and_limit() {
let mut cases = Vec::new();
for reference in ["", " OPS-42", "OPS/42"] {
let mut waiver_config = waiver(FINDING_OPENAPI_PUBLIC, "2099-01-01");
waiver_config.reference = reference.to_string();
cases.push((waiver_config, "reference", "128"));
}
let mut waiver_config = waiver(FINDING_OPENAPI_PUBLIC, "2099-01-01");
waiver_config.reference = "x".repeat(129);
cases.push((waiver_config, "reference", "128"));

for summary in [
"",
" summary",
"summary\ncontinued",
"Bearer credential-value",
"Basic credential-value",
"rotated leaked Bearer abcdef",
"-----BEGIN OPENSSH PRIVATE KEY-----",
concat!("-----BEGIN PGP PRIVATE KEY ", "BLOCK-----"),
] {
let mut waiver_config = waiver(FINDING_OPENAPI_PUBLIC, "2099-01-01");
waiver_config.summary = Some(summary.to_string());
cases.push((waiver_config, "summary", "256"));
}
let mut waiver_config = waiver(FINDING_OPENAPI_PUBLIC, "2099-01-01");
waiver_config.summary = Some("x".repeat(257));
cases.push((waiver_config, "summary", "256"));

for (waiver, field, limit) in cases {
let config = DeploymentConfig {
profile: Some(DeploymentProfile::HostedLab),
multi_instance: false,
waivers: vec![waiver],
evidence: DeploymentEvidenceConfig::default(),
};
let error = config.validate().expect_err("invalid metadata rejected");
let rendered = error.to_string();
assert!(rendered.contains(&format!("deployment.waivers[0].{field}")));
assert!(rendered.contains(limit), "limit missing from: {rendered}");
}
}

#[test]
fn validate_rejects_missing_or_malformed_expiry() {
let config = DeploymentConfig {
Expand Down
Loading
Loading