From 4d30cdbc2123ef554bebc55aab858bfec73877fd Mon Sep 17 00:00:00 2001 From: Michael Heller <21163552+mdheller@users.noreply.github.com> Date: Tue, 4 Aug 2026 03:44:45 -0400 Subject: [PATCH] =?UTF-8?q?spec:=20image-trust=20contracts=20=E2=80=94=20m?= =?UTF-8?q?easured,=20not=20asserted,=20for=20OS=20and=20agent=20images?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Applies the Assay's measured-not-asserted discipline to images. The estate already had Measurement (may-I-rely-on-this-number, gateEligible ceiling) but nothing forced an image's trust to be measured or the promotion gate to consume it — and agents had no image-artifact identity at all. - ImageTrustReport: per-dimension trust checks (reproducibility, attestation_verify, sbom_completeness, signature_verify, measured_boot, provenance_depth), each carrying a Measurement via $ref, plus a render-time projectedTrust ok/sad/bad. Unifies OS and agent images through subjectKind. Reusing Measurement by $ref means its invariants apply for free — a declared/assumed check cannot reach ok, a measured one must name its instrument (both verified biting). - AgentImage: agent runtime-artifact identity + provenance parallel to OSImage, distinct from process-level AgentPassport — so agents are measured like OS images. - ImagePromotionGate: requires trustReportRef when decision=approved (non-breaking if/then). The validator adds the cross-document rule: approved must reference a report projecting ok. Closes the self-assertable-approval gap (Assay F2) for images. validate-image-trust-examples enforces projection soundness + gate soundness. Passes the referential-integrity gate (338 schemas, all 136 $refs resolve). 5 negative tests confirmed biting; examples for an ok OS image + a sad agent image. --- CHANGELOG.md | 1 + Makefile | 8 +- examples/agent_image.json | 19 +++++ examples/image_promotion_gate.json | 1 + examples/image_trust_report.agent.json | 43 ++++++++++ examples/image_trust_report.os.json | 51 ++++++++++++ schemas/AgentImage.json | 52 +++++++++++++ schemas/ImagePromotionGate.json | 15 +++- schemas/ImageTrustReport.json | 60 ++++++++++++++ schemas/image-trust.README.md | 46 +++++++++++ tools/validate_image_trust_examples.py | 104 +++++++++++++++++++++++++ 11 files changed, 396 insertions(+), 4 deletions(-) create mode 100644 examples/agent_image.json create mode 100644 examples/image_trust_report.agent.json create mode 100644 examples/image_trust_report.os.json create mode 100644 schemas/AgentImage.json create mode 100644 schemas/ImageTrustReport.json create mode 100644 schemas/image-trust.README.md create mode 100644 tools/validate_image_trust_examples.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 419e567..8852f7d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). Thi ## [Unreleased] ### Added +- Image-trust contracts — the Assay's measured-not-asserted discipline applied to images, for both OS and agents: `ImageTrustReport` (per-dimension trust checks — reproducibility, attestation_verify, sbom_completeness, signature_verify, measured_boot, provenance_depth — each carrying a `Measurement` via `$ref`, plus a render-time `projectedTrust` ok/sad/bad verdict; unifies OS and agent images through `subjectKind`) and `AgentImage` (agent runtime-artifact identity + provenance parallel to `OSImage`, distinct from process-level `AgentPassport`). `ImagePromotionGate` now requires a `trustReportRef` when `decision: approved` (non-breaking if/then), and the new `validate-image-trust-examples` target enforces projection soundness plus the cross-document rule that an approval must reference a report projecting `ok` — closing, for images, the self-assertable-approval gap the Assay hardening closed for verifiers. Reuses `Measurement`'s gate-eligibility invariants for free via `$ref` (a declared/assumed check cannot reach `ok`). All invariants verified biting. See `schemas/image-trust.README.md`. - Assay fleet-tier contracts (cloud-mesh): `AssayRollup` (fleet/cohort aggregate of `ReasoningAssay` verdicts over a window — ok/sad/bad distribution, calibration-drift view of which `AssayStandard` versions are live across the fleet, unassayed-reason breakdown) and `AssayStandardRollout` (canary→widen/halt promotion of a new `AssayStandard` version across cohorts, gated on an observed `AssayRollup`; rides the release-bundle + lifecycle machinery). New `validate-assay-fleet-examples` target enforces aggregation/rollout soundness (distribution sums, drift-flag agreement, rolloutPct vs node share, decision/phase consistency, and no-promotion-by-hope), all verified biting. This is the boundary between prophet-mesh single-user-local (node self-view, no fleet dashboards, structural) and cloud-mesh (rollup + rollout + dashboards). See `schemas/assay-fleet.README.md`. - Trained DataClass classifiers — adversarial hardening (follow-up to #264): stronger fixture where BOTH monotone features (token_magnitude, decimal_places) genuinely drive the label so the monotone constraint actually BINDS (was vacuous — decimal_places held constant); standardiser fit on the TRAIN split only (no test leakage); ModelManifest named honestly `monotone-logistic` (a realisation of the declared tf-lattice-wide-and-deep contract, not a full lattice). Validator now checks BOTH per-class heads monotone AND asserts the constraint binds (non-vacuous), and compares reproduced accuracy at the stored 4-decimal precision (fixes a latent tolerance bug the trivial 1.0-accuracy fixture had masked). 8 teeth. - Trained DataClass classifiers v0.1 — specified→trained→registered→reproduced (closes the program's last owed thread): `tools/train_dataclass_classifiers.py` trains real per-class LOGISTIC (one-vs-rest) + per-table SOFTMAX heads, MONOTONE-constrained in `monotonicFeatures` (weight >=0 projection = TF-Lattice essence), deterministically; emits trained weights + a conformant `ModelManifest` (id=DataClass modelRef) + train/eval `RunRecord`s (ids=runRef/evalRunRef) so the previously-dangling DataClass classifier references now RESOLVE. `validate-trained-classifiers` (recompute-don't-trust): digest matches weight bytes; eval accuracy REPRODUCED from weights+fixture (inflated claim fails); model verified genuinely monotone (perturb up → score never drops); refs resolve; artifacts schema-conform. Teeth-verified (tampered weights refused). Uses numpy. diff --git a/Makefile b/Makefile index 07f1598..9ffdebd 100644 --- a/Makefile +++ b/Makefile @@ -1,12 +1,16 @@ -.PHONY: validate validate-resource-contract validate-measurement validate-value-type validate-source-locator validate-sourceos-repo-manifest validate-mesh-action-registry validate-control-plane-examples validate-nlboot-examples validate-lattice-data-governai-examples validate-ops-history-examples validate-runtime-observability-examples validate-interpretability-examples validate-lifecycle-boundary-examples validate-svf-contracts validate-sync-cycle-receipts validate-onboarding-examples validate-runtime-causality-examples validate-agentic-os-examples validate-triparty-examples validate-labor-market-examples validate-supply-chain-risk-examples validate-reasoning-examples validate-mpcc-event-examples validate-knowledge-nugget-examples validate-semantic-action-examples validate-epistemic-kernel-examples validate-ab-update-examples validate-device-service-examples validate-duplicate-schema-ids validate-lawful-dispatch-receipt validate-architectural-building-block validate-agent-passport-examples validate-seam-definition-examples validate-agent-system-vocabulary validate-genesis-inception-examples validate-measurement validate-world-model-examples validate-eval-item-examples validate-ingestion-pipeline-examples validate-data-acquisition-examples validate-glossary-alignment-examples validate-data-class-examples validate-table-keys validate-dag-loop validate-vocab-currency-loop vocab-currency-dogfood validate-glossary-promotion validate-agreement validate-stopword-analysis stopword-analysis-live validate-kgram-differential validate-learned-dictionary train-dataclass-classifiers validate-assay-fleet-examples validate-schema-references +.PHONY: validate validate-resource-contract validate-measurement validate-value-type validate-source-locator validate-sourceos-repo-manifest validate-mesh-action-registry validate-control-plane-examples validate-nlboot-examples validate-lattice-data-governai-examples validate-ops-history-examples validate-runtime-observability-examples validate-interpretability-examples validate-lifecycle-boundary-examples validate-svf-contracts validate-sync-cycle-receipts validate-onboarding-examples validate-runtime-causality-examples validate-agentic-os-examples validate-triparty-examples validate-labor-market-examples validate-supply-chain-risk-examples validate-reasoning-examples validate-mpcc-event-examples validate-knowledge-nugget-examples validate-semantic-action-examples validate-epistemic-kernel-examples validate-ab-update-examples validate-device-service-examples validate-duplicate-schema-ids validate-lawful-dispatch-receipt validate-architectural-building-block validate-agent-passport-examples validate-seam-definition-examples validate-agent-system-vocabulary validate-genesis-inception-examples validate-measurement validate-world-model-examples validate-eval-item-examples validate-ingestion-pipeline-examples validate-data-acquisition-examples validate-glossary-alignment-examples validate-data-class-examples validate-table-keys validate-dag-loop validate-vocab-currency-loop vocab-currency-dogfood validate-glossary-promotion validate-agreement validate-stopword-analysis stopword-analysis-live validate-kgram-differential validate-learned-dictionary train-dataclass-classifiers validate-assay-fleet-examples validate-image-trust-examples validate-schema-references -validate: validate-data-class-examples validate-glossary-alignment-examples validate-data-acquisition-examples validate-ingestion-pipeline-examples validate-control-plane-examples validate-nlboot-examples validate-lattice-data-governai-examples validate-ops-history-examples validate-runtime-observability-examples validate-interpretability-examples validate-lifecycle-boundary-examples validate-svf-contracts validate-sync-cycle-receipts validate-onboarding-examples validate-runtime-causality-examples validate-agentic-os-examples validate-triparty-examples validate-labor-market-examples validate-supply-chain-risk-examples validate-reasoning-examples validate-mpcc-event-examples validate-knowledge-nugget-examples validate-semantic-action-examples validate-epistemic-kernel-examples validate-ab-update-examples validate-device-service-examples validate-duplicate-schema-ids validate-value-type validate-source-locator validate-sourceos-repo-manifest validate-mesh-action-registry validate-lawful-dispatch-receipt validate-architectural-building-block validate-agent-passport-examples validate-seam-definition-examples validate-agent-system-vocabulary validate-genesis-inception-examples validate-measurement validate-world-model-examples validate-eval-item-examples validate-resource-contract validate-table-keys validate-dag-loop validate-vocab-currency-loop validate-glossary-promotion validate-agreement validate-stopword-analysis validate-kgram-differential validate-learned-dictionary validate-trained-classifiers validate-multiverseal-twin-examples validate-assay-fleet-examples validate-schema-references +validate: validate-data-class-examples validate-glossary-alignment-examples validate-data-acquisition-examples validate-ingestion-pipeline-examples validate-control-plane-examples validate-nlboot-examples validate-lattice-data-governai-examples validate-ops-history-examples validate-runtime-observability-examples validate-interpretability-examples validate-lifecycle-boundary-examples validate-svf-contracts validate-sync-cycle-receipts validate-onboarding-examples validate-runtime-causality-examples validate-agentic-os-examples validate-triparty-examples validate-labor-market-examples validate-supply-chain-risk-examples validate-reasoning-examples validate-mpcc-event-examples validate-knowledge-nugget-examples validate-semantic-action-examples validate-epistemic-kernel-examples validate-ab-update-examples validate-device-service-examples validate-duplicate-schema-ids validate-value-type validate-source-locator validate-sourceos-repo-manifest validate-mesh-action-registry validate-lawful-dispatch-receipt validate-architectural-building-block validate-agent-passport-examples validate-seam-definition-examples validate-agent-system-vocabulary validate-genesis-inception-examples validate-measurement validate-world-model-examples validate-eval-item-examples validate-resource-contract validate-table-keys validate-dag-loop validate-vocab-currency-loop validate-glossary-promotion validate-agreement validate-stopword-analysis validate-kgram-differential validate-learned-dictionary validate-trained-classifiers validate-multiverseal-twin-examples validate-assay-fleet-examples validate-image-trust-examples validate-schema-references @echo "OK: validate" validate-assay-fleet-examples: python3 -m pip install --user jsonschema >/dev/null python3 tools/validate_assay_fleet_examples.py +validate-image-trust-examples: + python3 -m pip install --user jsonschema referencing >/dev/null + python3 tools/validate_image_trust_examples.py + # General referential-integrity gate: every schema is a valid JSON Schema and every # $ref resolves. The per-example validators only exercise schemas that ship examples; # this catches a dangling/renamed/typo'd $ref in ANY of the 300+ schemas — a diff --git a/examples/agent_image.json b/examples/agent_image.json new file mode 100644 index 0000000..be9a4ab --- /dev/null +++ b/examples/agent_image.json @@ -0,0 +1,19 @@ +{ + "id": "urn:srcos:agent-image:michael-agent", + "type": "AgentImage", + "specVersion": "2.0.0", + "agentClass": "intelligence_automation", + "artifact": "oci", + "architecture": "aarch64", + "digest": "sha256:1f2e3d4c5b6a70819f2e3d4c5b6a70811f2e3d4c5b6a70819f2e3d4c5b6a7081", + "runtime": { + "language": "python", + "entrypoint": "prophet_mesh.cli:main" + }, + "provenance": { + "statementRef": "urn:srcos:attestation:michael-agent-v1", + "slsaPredicateRef": "urn:srcos:slsa:michael-agent-v1", + "sbomRef": "urn:srcos:sbom:michael-agent-v1", + "signatureRef": "urn:srcos:signature:michael-agent-v1" + } +} diff --git a/examples/image_promotion_gate.json b/examples/image_promotion_gate.json index 2cfdc01..2ba0249 100644 --- a/examples/image_promotion_gate.json +++ b/examples/image_promotion_gate.json @@ -14,6 +14,7 @@ "scenario-pass", "promotion-evidence-complete" ], + "trustReportRef": "urn:srcos:image-trust:sourceos-workstation-v0", "decision": "approved", "reasonCodes": [ "all-required-checks-passed" diff --git a/examples/image_trust_report.agent.json b/examples/image_trust_report.agent.json new file mode 100644 index 0000000..24b6f26 --- /dev/null +++ b/examples/image_trust_report.agent.json @@ -0,0 +1,43 @@ +{ + "id": "urn:srcos:image-trust:michael-agent-v1", + "type": "ImageTrustReport", + "specVersion": "2.0.0", + "subjectRef": "urn:srcos:agent-image:michael-agent", + "subjectKind": "agent-image", + "checks": [ + { + "dimension": "reproducibility", + "measurement": { + "schemaVersion": "0.1.0", "kind": "Measurement", + "label": "rebuild digest matches", + "value": true, "source": "measured", "instrument": "srcos-rebuild 2.1 (hermetic)", "gateEligible": true + }, + "passed": true + }, + { + "dimension": "signature_verify", + "measurement": { + "schemaVersion": "0.1.0", "kind": "Measurement", + "label": "artifact signature verified", + "value": true, "source": "measured", "instrument": "cosign verify 2.4", "gateEligible": true + }, + "passed": true + }, + { + "dimension": "sbom_completeness", + "measurement": { + "schemaVersion": "0.1.0", "kind": "Measurement", + "label": "SBOM coverage", + "value": "vendor-attested-complete", + "source": "declared", + "gateEligible": false, + "note": "SBOM completeness was asserted by the build manifest, not measured by an SBOM scanner. A declared measurement is gate-ineligible by shape, so the agent image cannot reach 'ok' on trust until a real scan is run." + }, + "passed": true + } + ], + "projectedTrust": "sad", + "unmetReason": "sbom_completeness:declared", + "measuredAt": "2026-07-05T00:00:00Z", + "evaluator": "srcos-image-trust-harness" +} diff --git a/examples/image_trust_report.os.json b/examples/image_trust_report.os.json new file mode 100644 index 0000000..b838525 --- /dev/null +++ b/examples/image_trust_report.os.json @@ -0,0 +1,51 @@ +{ + "id": "urn:srcos:image-trust:sourceos-workstation-v0", + "type": "ImageTrustReport", + "specVersion": "2.0.0", + "subjectRef": "urn:srcos:osimage:so1-workstation", + "subjectKind": "os-image", + "checks": [ + { + "dimension": "reproducibility", + "measurement": { + "schemaVersion": "0.1.0", "kind": "Measurement", + "label": "hermetic rebuild digest matches the published digest", + "value": true, "source": "measured", "instrument": "srcos-rebuild 2.1 (hermetic, pinned toolchain)", "gateEligible": true + }, + "passed": true, + "evidenceRef": "urn:srcos:slsa:sourceos-workstation-v0" + }, + { + "dimension": "attestation_verify", + "measurement": { + "schemaVersion": "0.1.0", "kind": "Measurement", + "label": "in-toto/SLSA statement verified against the release key", + "value": true, "source": "measured", "instrument": "cosign verify-attestation 2.4", "gateEligible": true + }, + "passed": true, + "evidenceRef": "urn:srcos:attestation:sourceos-workstation-v0" + }, + { + "dimension": "signature_verify", + "measurement": { + "schemaVersion": "0.1.0", "kind": "Measurement", + "label": "image signature verified", + "value": true, "source": "measured", "instrument": "cosign verify 2.4", "gateEligible": true + }, + "passed": true + }, + { + "dimension": "measured_boot", + "measurement": { + "schemaVersion": "0.1.0", "kind": "Measurement", + "label": "PCR7 matches the expected measured-boot value", + "value": true, "source": "measured", "instrument": "tpm2_quote + BootProofRecord cross-check", "gateEligible": true + }, + "passed": true, + "evidenceRef": "urn:srcos:boot-proof:so1-workstation-2026-04-15" + } + ], + "projectedTrust": "ok", + "measuredAt": "2026-07-05T00:00:00Z", + "evaluator": "srcos-image-trust-harness" +} diff --git a/schemas/AgentImage.json b/schemas/AgentImage.json new file mode 100644 index 0000000..111caec --- /dev/null +++ b/schemas/AgentImage.json @@ -0,0 +1,52 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://schemas.srcos.ai/v2/AgentImage.json", + "title": "AgentImage", + "description": "Immutable identity and provenance contract for an agent RUNTIME ARTIFACT — the deployable agent image/bundle — parallel to OSImage for the OS substrate. Distinct from AgentPassport: AgentPassport classifies a running process on a host; AgentImage identifies the built artifact that process was launched from, so an agent image can be measured for trust (via ImageTrustReport) exactly the way an OS image is. Shares OSImage's provenance shape deliberately, so one ImageTrustReport applies to both.", + "type": "object", + "additionalProperties": false, + "required": ["id", "type", "specVersion", "agentClass", "artifact", "architecture", "digest", "provenance"], + "properties": { + "id": { "type": "string", "pattern": "^urn:srcos:agent-image:[a-z0-9][a-z0-9._-]*$", "description": "Stable URN. Pattern: urn:srcos:agent-image:." }, + "type": { "const": "AgentImage" }, + "specVersion": { "type": "string" }, + "agentClass": { + "enum": ["system_core", "intelligence_automation", "app_helper", "legacy_bridge", "third_party"], + "description": "The five-class agent model shared with AgentPassport — the class the built artifact is authorized to run as." + }, + "artifact": { + "enum": ["oci", "wheel", "npm", "binary", "bundle"], + "description": "Rendered artifact form for the agent image." + }, + "architecture": { + "enum": ["x86_64", "aarch64", "any"], + "description": "Target architecture ('any' for a pure-interpreted bundle)." + }, + "digest": { + "type": "string", + "pattern": "^sha256:[0-9a-f]{64}$", + "description": "Content digest of the built agent artifact — the identity a reproducibility check re-derives." + }, + "runtime": { + "type": "object", + "additionalProperties": false, + "description": "Optional declared runtime binding.", + "properties": { + "language": { "type": "string", "description": "e.g. python, node, rust." }, + "entrypoint": { "type": "string", "description": "The invoked entrypoint." } + } + }, + "provenance": { + "type": "object", + "description": "Attestation and provenance references for the agent artifact — same shape as OSImage.provenance so both images are measured by one ImageTrustReport.", + "additionalProperties": false, + "required": ["statementRef", "slsaPredicateRef"], + "properties": { + "statementRef": { "type": "string", "pattern": "^urn:srcos:attestation:[a-z0-9][a-z0-9._:-]*$", "description": "URN reference to the in-toto or equivalent attestation statement." }, + "slsaPredicateRef": { "type": "string", "pattern": "^urn:srcos:slsa:[a-z0-9][a-z0-9._:-]*$", "description": "URN reference to the SLSA provenance predicate artifact." }, + "sbomRef": { "type": "string", "description": "Optional URN or URL reference to the SBOM for this agent image." }, + "signatureRef": { "type": "string", "description": "Optional URN or URL reference to the signature verification material." } + } + } + } +} diff --git a/schemas/ImagePromotionGate.json b/schemas/ImagePromotionGate.json index 235de5d..57ffa5a 100644 --- a/schemas/ImagePromotionGate.json +++ b/schemas/ImagePromotionGate.json @@ -2,7 +2,7 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://schemas.srcos.ai/v2/ImagePromotionGate.json", "title": "ImagePromotionGate", - "description": "Promotion decision envelope for whether a candidate image may update a Git repository and mark a task complete.", + "description": "Promotion decision envelope for whether a candidate image may update a Git repository and mark a task complete. An 'approved' decision may not be self-asserted: it must reference an ImageTrustReport whose checks are measured and gate-eligible (enforced below via if/then for presence, and by the image-trust validator for the cross-document 'projectedTrust: ok' requirement). A 'waived' decision must carry a reasonCode. This closes the same assert-vs-measure gap the Assay's AssayStandard closes for verifiers.", "type": "object", "additionalProperties": false, "required": ["id", "type", "specVersion", "candidateBuildRef", "profileRef", "requiredArtifacts", "requiredChecks", "decision", "gitUpdate", "taskCompletion"], @@ -14,6 +14,7 @@ "profileRef": { "type": "string", "pattern": "^urn:srcos:control-node:" }, "requiredArtifacts": { "type": "array", "items": { "type": "string" } }, "requiredChecks": { "type": "array", "items": { "type": "string" } }, + "trustReportRef": { "type": "string", "pattern": "^urn:srcos:image-trust:", "description": "The ImageTrustReport backing this decision. Required when decision is 'approved' — an approval unbacked by a measured, gate-eligible trust report is exactly the self-asserted 'calibrated' flag the Assay hardening removed." }, "decision": { "enum": ["pending", "approved", "denied", "waived"] }, "reasonCodes": { "type": "array", "items": { "type": "string" } }, "gitUpdate": { @@ -35,5 +36,15 @@ "issueRef": { "type": ["string", "null"] } } } - } + }, + "allOf": [ + { + "$comment": "An approval must reference a trust report. The cross-document requirement that the report projects 'ok' is enforced by the image-trust validator, which JSON Schema cannot express across files.", + "if": { + "required": ["decision"], + "properties": { "decision": { "const": "approved" } } + }, + "then": { "required": ["trustReportRef"] } + } + ] } diff --git a/schemas/ImageTrustReport.json b/schemas/ImageTrustReport.json new file mode 100644 index 0000000..837ae95 --- /dev/null +++ b/schemas/ImageTrustReport.json @@ -0,0 +1,60 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://schemas.srcos.ai/v2/ImageTrustReport.json", + "title": "ImageTrustReport", + "description": "The MEASURED trustworthiness of an image — OS or agent — as a bundle of per-dimension Measurements plus a render-time projected verdict. This is the Assay pattern applied to images: as AssayStandard measures a verifier's reliability so a claim can only project 'ok' behind a measured judge, ImageTrustReport measures an image's checks so ImagePromotionGate can only 'approve' behind measured, gate-eligible evidence. An image cannot declare itself trustworthy — each check is a Measurement whose gateEligible ceiling forbids an asserted number from satisfying a gate, and projectedTrust is recomputed from the checks (a cache, never the source of truth). Unifies OS and agent images through subjectKind, so both are measured identically. Distinct from OSImage.provenance / AgentImage.provenance, which say where an artifact came from; this says whether it may be relied upon.", + "type": "object", + "additionalProperties": false, + "required": ["id", "type", "specVersion", "subjectRef", "subjectKind", "checks", "projectedTrust", "measuredAt"], + "properties": { + "id": { "type": "string", "pattern": "^urn:srcos:image-trust:", "description": "Stable URN. Pattern: urn:srcos:image-trust:." }, + "type": { "const": "ImageTrustReport" }, + "specVersion": { "type": "string" }, + "subjectRef": { + "type": "string", + "description": "The image this report measures — an OSImage (urn:srcos:osimage:) or an AgentImage (urn:srcos:agent-image:). subjectKind disambiguates." + }, + "subjectKind": { + "enum": ["os-image", "agent-image"], + "description": "Which image family the subject belongs to. The unification point: OS and agent images carry the same trust report." + }, + "checks": { + "type": "array", + "minItems": 1, + "description": "One measured trust check per dimension. Each carries a Measurement (so the gateEligible ceiling applies) and the interpreted pass/fail outcome.", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["dimension", "measurement", "passed"], + "properties": { + "dimension": { + "enum": ["reproducibility", "attestation_verify", "sbom_completeness", "signature_verify", "measured_boot", "provenance_depth"], + "description": "reproducibility = a rebuild produced the same digest. attestation_verify = the in-toto/SLSA statement actually verified (not merely referenced). sbom_completeness = measured coverage of the SBOM. signature_verify = signature material verified. measured_boot = PCR/measured-boot match (fed by BootProofRecord/AttestationEvidence). provenance_depth = the provenance chain resolved to a trusted root." + }, + "measurement": { + "$ref": "Measurement.json", + "description": "The measured value for this check. A 'declared' or 'assumed' measurement is not gate-eligible by shape, so it cannot let the image reach 'ok'." + }, + "passed": { + "type": "boolean", + "description": "The interpreted outcome of the check (did it pass?). Distinct from measurement.gateEligible, which says whether the number may be relied upon: a gate-eligible measurement that records a FAILURE (passed:false) projects the whole report to 'bad'." + }, + "evidenceRef": { + "type": "string", + "description": "Optional URN of the artifact this check verified (e.g. the OSImage.provenance.slsaPredicateRef, a BootProofRecord)." + } + } + } + }, + "projectedTrust": { + "enum": ["ok", "sad", "bad"], + "description": "Render-time projection over the checks, AT measuredAt — a cache, re-derivable, never the source of truth. Mirrors ReasoningAssay.projectedState: 'bad' = at least one check whose gate-eligible measurement recorded a failure (passed:false); 'ok' = every check gate-eligible AND passed; 'sad' = otherwise (some check declared/derived/assumed or with unobserved members — not measured-clean — but none decisively failed)." + }, + "unmetReason": { + "type": "string", + "description": "When projectedTrust is 'sad', the specific dimension(s) not yet measured-clean (e.g. 'sbom_completeness:declared'). Frames the amber state as honest pending-work, not soft failure." + }, + "measuredAt": { "type": "string", "format": "date-time" }, + "evaluator": { "type": "string", "description": "Optional identifier of the harness that produced this report." } + } +} diff --git a/schemas/image-trust.README.md b/schemas/image-trust.README.md new file mode 100644 index 0000000..27c03d7 --- /dev/null +++ b/schemas/image-trust.README.md @@ -0,0 +1,46 @@ +# Image Trust — measured, not asserted + +The Assay pattern, applied to images. `AssayStandard` measures a *verifier's* reliability so a +claim can only project `ok` behind a measured judge. **`ImageTrustReport` measures an *image's* +checks so `ImagePromotionGate` can only `approve` behind measured, gate-eligible evidence.** An +image — OS or agent — cannot declare itself trustworthy. + +## Schemas + +| Schema | URN prefix | Purpose | +|---|---|---| +| `ImageTrustReport.json` | `urn:srcos:image-trust:` | Per-dimension measured trust checks + a render-time `projectedTrust` verdict. Unifies OS and agent images via `subjectKind`. | +| `AgentImage.json` | `urn:srcos:agent-image:` | Agent runtime-artifact identity + provenance, parallel to `OSImage` (distinct from `AgentPassport`, which classifies a running process). | +| `ImagePromotionGate.json` | `urn:srcos:image-gate:` | Now requires a `trustReportRef` when `decision: approved` — an approval must be backed. | + +## How it reuses, not reinvents + +- Each check carries a **`Measurement`** (`$ref: Measurement.json`). Measurement's own invariants + therefore apply for free: a `declared`/`assumed` measurement is gate-ineligible by shape, and a + `measured` one must name its `instrument`. A trust check cannot smuggle in an asserted number. +- `measured_boot` is fed by the existing `BootProofRecord` / `AttestationEvidence`; `attestation_verify` + and `signature_verify` record the *outcome* of verifying `OSImage.provenance` / `AgentImage.provenance` + refs, which by themselves are only pointers. + +## The projection (executable, CI-enforced) + +`projectedTrust` is recomputed from the checks by `tools/validate_image_trust_examples.py`, which fails +if the recorded value doesn't follow: + +- **`bad`** — a gate-eligible measurement recorded a failure (`passed: false`). +- **`ok`** — every check is gate-eligible **and** passed. +- **`sad`** — otherwise: a `declared`/`derived`/`assumed` or partly-`unobserved` check, none failed. + Honest pending-work (`unmetReason` names the dimension), not soft failure. + +And the gate rule: an `ImagePromotionGate` with `decision: approved` must reference an `ImageTrustReport` +that projects `ok` — the cross-document half the schema's `if/then` can't express. This closes, for +images, the same self-assertable-approval gap the Assay hardening (F2) closed for verifiers. + +## Examples + +| Example | Projects | +|---|---| +| `examples/image_trust_report.os.json` | `ok` — OS image, all checks measured + gate-eligible | +| `examples/image_trust_report.agent.json` | `sad` — agent image with a *declared* SBOM (not measured) | +| `examples/agent_image.json` | an `AgentImage` | +| `examples/image_promotion_gate.json` | `approved`, backed by the OS report | diff --git a/tools/validate_image_trust_examples.py b/tools/validate_image_trust_examples.py new file mode 100644 index 0000000..ba24239 --- /dev/null +++ b/tools/validate_image_trust_examples.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 +"""Validate the image-trust family: ImageTrustReport, AgentImage, ImagePromotionGate. + +Three checks, the same discipline the Assay uses one level down (claims → images): + 1. schema conformance — every example validates against its schema, with the + ImageTrustReport check.measurement `$ref` resolved to Measurement.json (so a + 'declared'/'assumed' measurement is gate-ineligible by shape); + 2. projection soundness — projectedTrust recomputed from the checks must match the + recorded value (ok/sad/bad is an executable projection, not a stored opinion); + 3. gate soundness — an ImagePromotionGate with decision 'approved' must reference an + ImageTrustReport that projects 'ok'. An approval cannot be self-asserted. +""" +from __future__ import annotations + +import json +from pathlib import Path + +import jsonschema +from referencing import Registry, Resource + +ROOT = Path(__file__).resolve().parents[1] + +# schemas that participate in $ref resolution (ImageTrustReport → Measurement) +REGISTRY_SCHEMAS = ["Measurement.json", "ImageTrustReport.json", "AgentImage.json", "ImagePromotionGate.json"] + +PAIRS = [ + ("ImageTrustReport.json", "image_trust_report.os.json"), + ("ImageTrustReport.json", "image_trust_report.agent.json"), + ("AgentImage.json", "agent_image.json"), + ("ImagePromotionGate.json", "image_promotion_gate.json"), +] + +TRUST_REPORTS = ["image_trust_report.os.json", "image_trust_report.agent.json"] + + +def _load(path: Path) -> dict: + return json.loads(path.read_text(encoding="utf-8")) + + +def build_registry() -> Registry: + resources = [] + for name in REGISTRY_SCHEMAS: + doc = _load(ROOT / "schemas" / name) + resources.append((doc["$id"], Resource.from_contents(doc))) + return Registry().with_resources(resources) + + +def project_trust(checks: list[dict]) -> str: + """Recompute the ok/sad/bad trust verdict from the measured checks. + + bad — a gate-eligible measurement recorded a failure (passed:false). + ok — every check is gate-eligible AND passed. + sad — otherwise (a declared/derived/assumed or partly-unobserved check, none failed). + """ + if any(c["passed"] is False and c["measurement"].get("gateEligible") is True for c in checks): + return "bad" + if all(c["passed"] is True and c["measurement"].get("gateEligible") is True for c in checks): + return "ok" + return "sad" + + +def main() -> int: + registry = build_registry() + checks: dict[str, bool] = {} + + for schema_name, example_name in PAIRS: + schema = _load(ROOT / "schemas" / schema_name) + jsonschema.Draft202012Validator.check_schema(schema) + validator = jsonschema.Draft202012Validator(schema, registry=registry) + validator.validate(_load(ROOT / "examples" / example_name)) + checks[example_name] = True + + # projection soundness + reports_by_id: dict[str, dict] = {} + for name in TRUST_REPORTS: + report = _load(ROOT / "examples" / name) + reports_by_id[report["id"]] = report + recomputed = project_trust(report["checks"]) + if report["projectedTrust"] != recomputed: + raise SystemExit( + f"projection mismatch in {name}: recorded={report['projectedTrust']!r} " + f"but project_trust()={recomputed!r}" + ) + checks[f"projection:{name}"] = True + + # gate soundness — approved requires a backing report that projects ok + gate = _load(ROOT / "examples" / "image_promotion_gate.json") + if gate["decision"] == "approved": + ref = gate.get("trustReportRef") + report = reports_by_id.get(ref) + if report is None: + raise SystemExit(f"gate approved but trustReportRef {ref!r} is not a known ImageTrustReport") + if report["projectedTrust"] != "ok": + raise SystemExit( + f"gate approved but referenced report {ref} projects {report['projectedTrust']!r}, not 'ok'" + ) + checks["gate:image_promotion_gate.json"] = True + + print(json.dumps({"ok": all(checks.values()), "checks": checks}, indent=2, sort_keys=True)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())