Makoto
Menu
Case 05 · AI dataset gate

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.

The code this page is about. Repeated from The output that catches it below.
$ 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 $?
1

Evidence 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.

  1. 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"
      }
    ]
  2. 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:8cef065b7af83669150b7d32704d3d3e75c3e9aee0d4f6740544c68e81b377aa
  3. Put 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"

    Download makoto-gate.yml.

    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: false is 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.

  4. The output that catches it

    Fifteen checks pass, including signatures, authorization, the full graph, and the artifact bytes. Only artifact-profiles fails, 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 $?
    1

    The machine-readable report

    Output of jq '{decision, primaryError, errors, notPassed: [.checks[] | select(.status != "pass") | {id, status}]}' makoto-report.private-schema-violation.json against 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 FAILED with E_REPORT_DENIED, because the decision is deny. Its evaluation sidecar still records each verified property's exact rule against the report's checks:

    PropertyWhat its rule reads in this reportResult
    MAKOTO_AUTHORIZEDsignatures pass, authorization-thresholds pass, authorization pass; handoff authorization passEstablished
    MAKOTO_GRAPH_COMPLETEindex-payloads pass, graph-dependency-artifacts pass, graph pass, roots-and-heads pass, completeness-anchor passEstablished
    MAKOTO_FRESHNESS_ANCHOREDfreshness-anchors pass; methods: expected-artifacts pass, expected-heads pass, expected-manifest pass, max-age not_checked, nonce not_checkedEstablished
    MAKOTO_SCHEMA_CONFORMANTcore-schemas pass, metadata-profiles pass, artifact-profiles fail; the policy requires 2 digest-pinned profilesNot established: E_PROPERTY_SCHEMA_CONFORMANT
    MAKOTO_REPRODUCEDNeeds a second allow report from an independent Transform L2-or-higher platform; none is suppliedNot established: E_PROPERTY_REPRODUCED

    Computed 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.

    Open the complete private-profile denial.

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

InputA received dataset bundle, receiver trust policy, local schema catalog, expected head or manifest, and expected artifact binding.
GateRun complete bundle verification before enqueueing the training job.
AllowAll required checks pass and the report decision is allow.
DenyA 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.

Fail before the GPU queue

Make the training job consume a verification report, not producer confidence.