Do not train on a label and a checksum.
A shared dataset looks plausible and is validly signed by the producer you trust. Its records carry a field the receiver never allows in training data. Before training, the model builder needs exact bytes, signed source and transformation claims, required content profiles, an authorized handoff, and an independent expectation of what should arrive.
$ makoto verify bundle handoffs/private-schema-violation/bundle \
--policy receiver/policy.json \
--schema-catalog receiver/catalog.json \
--expected-manifest sha256:d5db4442079d10670c5a6d3173652eff92762d9ed5e572a5f52e473a5feec2e5 \
--expected-head sha256:44e44cf4563da5eb91856c02de82db2a622ddb580f3156ca7b1a7f91b8c5b029 \
--expected-artifact receiver/expected/private-schema-violation.artifact.json \
--evaluation-time 2026-09-16T16:00:00Z
PASS load-safely
PASS parse-strictly
PASS index-payloads
PASS core-schemas
PASS signatures
PASS authorization-thresholds
PASS metadata-profiles
PASS authorization
PASS graph-dependency-artifacts
PASS graph
PASS roots-and-heads
PASS completeness-anchor
PASS freshness-anchors
PASS artifact-bytes
FAIL artifact-profiles
PASS decision
DENY decision
manifest sha256:d5db4442079d10670c5a6d3173652eff92762d9ed5e572a5f52e473a5feec2e5
E_PROFILE_INVALID: Additional properties are not allowed ('email' was unexpected); Additional properties are not allowed ('email' was unexpected); Additional properties are not allowed ('email' was unexpected)
$ echo $?
1Evidence boundary. Every terminal block on this page was captured by running the command shown against the checked-in reference fixtures with the Makoto core verifier at the pinned commit 276008e, which the gate below runs. Verification-summary tables are computed from the published reports with its property rules. The verification report is byte-for-byte the file published under the end-to-end proof. Nothing below was typed by hand.
The problem
A machine-learning team downloads a training dataset from a shared location. The file name is right, the row count matches, and the columns look correct. Training starts.
What nobody notices is that the privacy step upstream regressed, or was bypassed, and direct identifiers went back into the records. Nothing about the bytes is corrupt, and the producer re-ran its pipeline and signed the new output with its real key. Every integrity check passes, because this is not tampering. It is data the receiver's rules forbid, arriving through a legitimate channel.
The runnable fixture exercises the identifier case: the public-safe step reintroduces an email field and the producer re-signs. A profile that enumerates allowed label values would apply to a re-signed poisoned label the same way; that variant is not among the checked-in fixtures.
The raw example
This is the dataset as it arrived for training: three records, validly signed, each carrying an email address.
$ jq . handoffs/private-schema-violation/bundle/artifacts/final/*.bin [ { "age_band": "25-34", "customer_id": "4956cf12a64fd24a0815", "email": "[email protected]", "marketing_consent": true, "region": "US" }, { "age_band": "0-17", "customer_id": "b8610bcb16fd5eb07f52", "email": "[email protected]", "marketing_consent": true, "region": "CA" }, { "age_band": "65+", "customer_id": "d7ab36aae7b1e4237278", "email": "[email protected]", "marketing_consent": false, "region": "GB" } ]How it failed
Compared with the approved public-safe output, each record gained one field. The file still parses and still has three records, so a loader accepts it. The statement over these bytes is signed by the key the receiver's policy authorizes for the public-safe step.
$ final=artifacts/final/71895faa8bce0d0426b2e447bb12d15e50e9f9d27135ebabbb94bc9d59426d4e.bin $ diff <(jq . handoffs/positive/bundle/$final) <(jq . handoffs/private-schema-violation/bundle/artifacts/final/*.bin) 4a5 > "email": "[email protected]", 10a12 > "email": "[email protected]", 16a19 > "email": "[email protected]", $ jq 'length' handoffs/private-schema-violation/bundle/artifacts/final/*.bin 3 $ envelope() { b=handoffs/$1/bundle; d=$(jq -r '.artifacts[0].statementDigest.sha256' $b/bundle.json); cat $b/attestations/$d.dsse.json; } $ envelope private-schema-violation | jq -r '.signatures[].keyid' sha256:8cef065b7af83669150b7d32704d3d3e75c3e9aee0d4f6740544c68e81b377aa $ jq -r '.rules[] | select(.id == "urn:makoto:demo:rule:public-safe") | .authorizedKeyIds[]' receiver/policy.json sha256:8cef065b7af83669150b7d32704d3d3e75c3e9aee0d4f6740544c68e81b377aaPut Makoto in the path
The training job consumes a verification report, not producer confidence. The gate job must allow the candidate dataset before the train job, which declares
needs: gate, is enqueued.The workflow
# Training admission gate. The training job consumes a # verification report, not producer confidence. # # Copy to .github/workflows/makoto-gate.yml in the repository that # receives handoffs. Layout it expects: # handoffs/<name>/bundle/ what the sender transferred # receiver/policy.json receiver-owned trust policy # receiver/catalog.json, receiver/resources/ pinned private schemas # receiver/expected/<name>.artifact.json expected final artifact name: Makoto gate before training on: workflow_dispatch: push: paths: - "handoffs/**" permissions: contents: read env: # Pinned so these fixtures replay identically. Delete this line to # evaluate against the runner clock. EVALUATION_TIME: "2026-09-16T16:00:00Z" jobs: gate: runs-on: ubuntu-latest strategy: fail-fast: false matrix: include: # The sender publishes manifest and head digests out of band. # The receiver pins them here, never inside the bundle. - handoff: private-schema-violation manifest: sha256:d5db4442079d10670c5a6d3173652eff92762d9ed5e572a5f52e473a5feec2e5 head: sha256:44e44cf4563da5eb91856c02de82db2a622ddb580f3156ca7b1a7f91b8c5b029 steps: - uses: actions/checkout@v4 # Pin the verifier to an exact core commit, like any dependency. # Run it from a checkout: the CLI resolves its core schemas from # the repository tree. This is the core commit the hosted # schemas are pinned to. - uses: actions/checkout@v4 with: repository: makoto-project/makoto ref: 276008eeec0f10d5322c4c627b8c103222fee841 path: .makoto-core persist-credentials: false - uses: astral-sh/setup-uv@v6 - name: Verify the received bundle id: verify shell: bash env: HANDOFF: ${{ matrix.handoff }} EXPECTED_MANIFEST: ${{ matrix.manifest }} EXPECTED_HEAD: ${{ matrix.head }} run: | verify() { uv run --project .makoto-core makoto verify bundle "handoffs/$HANDOFF/bundle" \ --policy receiver/policy.json \ --schema-catalog receiver/catalog.json \ --expected-manifest "$EXPECTED_MANIFEST" \ --expected-head "$EXPECTED_HEAD" \ --expected-artifact "receiver/expected/$HANDOFF.artifact.json" \ --evaluation-time "$EVALUATION_TIME" "$@" } status=0 verify --json > makoto-report.json || status=$? verify || true exit "$status" - name: Keep the verification report if: always() uses: actions/upload-artifact@v4 with: name: makoto-report-${{ matrix.handoff }} path: makoto-report.json train: # Runs only when every handoff in the matrix was allowed. needs: gate runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Enqueue the training job run: echo "replace with your existing step"The gate keeps the JSON report as a build artifact, so a later job can summarize it into a signed verification summary without running verification again.
The receiver's private profile
The receiver pins its own JSON Schema by digest in its catalog and requires it for the final artifact. The producer does not control these bytes.
additionalProperties: falseis the rule that refuses an identifier nobody listed.{ "$schema": "https://usemakoto.dev/schema/v0.2/profile-dialect.schema.json", "$id": "https://schemas.example.internal/makoto/customer-public-v1.schema.json", "type": "array", "items": { "type": "object", "required": [ "age_band", "customer_id", "marketing_consent", "region" ], "properties": { "age_band": { "enum": [ "0-17", "18-24", "25-34", "35-44", "45-54", "55-64", "65+" ] }, "customer_id": { "type": "string", "minLength": 20, "maxLength": 20 }, "marketing_consent": { "type": "boolean" }, "region": { "type": "string", "minLength": 2, "maxLength": 2 } }, "additionalProperties": false } }That is the receiver's pinned schema resource, printed with
jq .. Its SHA-256 digest,68169d04…, is the profile digest. The policy requires that profile for every matching final artifact:[ { "closureDigest": { "sha256": "57eec614be9b2141ad536f486ff4400b514d79b8b1cc9642a839cf1a39b10794" }, "digest": { "sha256": "68169d043c628fda5435cbd7845b02ea3e0d850b7a509c0b35fd304463fffacb" }, "id": "https://schemas.example.internal/makoto/customer-public-v1.schema.json", "mediaType": "application/json", "scope": "eachMatchingFinalArtifact", "subjectName": "customers.public.json", "target": "artifact" } ]Output of
jq '.requiredProfiles' receiver/policy.json. Download the profile reference and catalog.The output that catches it
Fifteen checks pass, including signatures, authorization, the full graph, and the artifact bytes. Only
artifact-profilesfails, and the message names the forbidden field for each of the three records.$ makoto verify bundle handoffs/private-schema-violation/bundle \ --policy receiver/policy.json \ --schema-catalog receiver/catalog.json \ --expected-manifest sha256:d5db4442079d10670c5a6d3173652eff92762d9ed5e572a5f52e473a5feec2e5 \ --expected-head sha256:44e44cf4563da5eb91856c02de82db2a622ddb580f3156ca7b1a7f91b8c5b029 \ --expected-artifact receiver/expected/private-schema-violation.artifact.json \ --evaluation-time 2026-09-16T16:00:00Z PASS load-safely PASS parse-strictly PASS index-payloads PASS core-schemas PASS signatures PASS authorization-thresholds PASS metadata-profiles PASS authorization PASS graph-dependency-artifacts PASS graph PASS roots-and-heads PASS completeness-anchor PASS freshness-anchors PASS artifact-bytes FAIL artifact-profiles PASS decision DENY decision manifest sha256:d5db4442079d10670c5a6d3173652eff92762d9ed5e572a5f52e473a5feec2e5 E_PROFILE_INVALID: Additional properties are not allowed ('email' was unexpected); Additional properties are not allowed ('email' was unexpected); Additional properties are not allowed ('email' was unexpected) $ echo $? 1The machine-readable report
Output of
jq '{decision, primaryError, errors, notPassed: [.checks[] | select(.status != "pass") | {id, status}]}' makoto-report.private-schema-violation.jsonagainst the report the workflow step wrote.{ "decision": "deny", "primaryError": "E_PROFILE_INVALID", "errors": [ { "causedByCheck": "artifact-profiles", "code": "E_PROFILE_INVALID", "context": { "profileDigest": { "sha256": "68169d043c628fda5435cbd7845b02ea3e0d850b7a509c0b35fd304463fffacb" }, "profileId": "https://schemas.example.internal/makoto/customer-public-v1.schema.json", "statementDigest": { "sha256": "44e44cf4563da5eb91856c02de82db2a622ddb580f3156ca7b1a7f91b8c5b029" }, "subjectName": "customers.public.json" }, "message": "Additional properties are not allowed ('email' was unexpected); Additional properties are not allowed ('email' was unexpected); Additional properties are not allowed ('email' was unexpected)", "step": 13 } ], "notPassed": [ { "id": "artifact-profiles", "status": "fail" } ] }What a verification summary records
A verification summary over this report is
FAILEDwithE_REPORT_DENIED, because the decision isdeny. Its evaluation sidecar still records each verified property's exact rule against the report's checks:Property What its rule reads in this report Result MAKOTO_AUTHORIZEDsignaturespass,authorization-thresholdspass,authorizationpass; handoff authorization passEstablished MAKOTO_GRAPH_COMPLETEindex-payloadspass,graph-dependency-artifactspass,graphpass,roots-and-headspass,completeness-anchorpassEstablished MAKOTO_FRESHNESS_ANCHOREDfreshness-anchorspass; methods:expected-artifactspass,expected-headspass,expected-manifestpass,max-agenot_checked,noncenot_checkedEstablished MAKOTO_SCHEMA_CONFORMANTcore-schemaspass,metadata-profilespass,artifact-profilesfail; the policy requires 2 digest-pinned profilesNot established: E_PROPERTY_SCHEMA_CONFORMANTMAKOTO_REPRODUCEDNeeds a second allowreport from an independent Transform L2-or-higher platform; none is suppliedNot established: E_PROPERTY_REPRODUCEDComputed from the published report and the policy whose digest it records, with the property rules in core at the pinned commit. Track levels are not shown: they also need the statements themselves and any platform assessments.
The admission decision
| Check | Training gate |
|---|---|
| Origin and transformations | Every head traces through exact predecessor bindings to an accepted source claim. |
| Signer authorization | Each key is allowed for the source, operation, profile, or handoff role it signed. |
| Required profiles | Labels, schema, allowed values, identifier rules, and other organization-specific constraints pass. |
| Handoff completeness and freshness | The expected manifest or head matches the signed graph presented for this training run. |
| Received bytes | The actual dataset or dataset manifest matches the signed artifact binding. |
A valid gate is not a quality score
The verifier can establish that exact bytes match authorized signed claims and required schemas. It cannot prove the labels are correct, the dataset is unbiased, or the model will be safe. Those require separate evaluation evidence and policy.
Concrete training gate
| Input | A received dataset bundle, receiver trust policy, local schema catalog, expected head or manifest, and expected artifact binding. |
|---|---|
| Gate | Run complete bundle verification before enqueueing the training job. |
| Allow | All required checks pass and the report decision is allow. |
| Deny | A validly re-signed dataset that reintroduces a direct identifier fails the receiver's private artifact profile with E_PROFILE_INVALID. |
What else this handles
- Training-set versioning across model iterations, with the exact digest of each version.
- Documented data provenance for regulated AI systems.
- Datasets assembled from many sources, each verified independently.
- Retraining pipelines that must reject stale or non-conforming data automatically.
Key insight. A model is only as trustworthy as its training data, and trust needs verification against the receiver's own rules, not the producer's assurance that the pipeline ran.