Skip to content

Auto-merge factory PRs on trusted evidence

Review the factory once, then merge its pull requests on trusted evidence instead of re-reading every generated diff. Automated merging is the outcome APMX’s four promises unlock:

  • Traceability. Every delivered file is cryptographically bound to the factory that made it and the checks it passed.
  • Portability. Same factory, skills and checks on your choice of harness and model.
  • Security. Agent, checks and signer run apart. The agent cannot sign or change its checks.
  • Trust. Only factories you approved ship: signed provenance, intact files, your checks passed.

Merge on trusted evidence. The receiving side is one policy section and one required check. Each promise holds under conditions; the trust model states them.

run.yml: each job on a fresh runner

  1. The agent worksproducemodel token; read-only contents; no signing identity
  2. producer receipt (untrusted)
    Checks verifycheckreruns your checks on the agent's bytes; no write token, no signing identity
  3. receipt of record
    A trusted signer signsattestno agent, no candidate code; signs only after an offline audit is VALID
Three isolated jobs: the agent works, checks verify, a trusted signer signs. CI only: a local run has none of this separation.

Three isolated jobs: the agent works, checks verify, a trusted signer signs. The factory repository calls APMX’s reusable run.yml (danielmeppiel/apmx/.github/workflows/run.yml@v0.7.1); see run a factory in CI.

In v0.7.1 the trusted signer is the attest job of APMX’s run.yml, signing through Sigstore with the workflow’s GitHub OIDC identity. Your policy pins it by workflow and commit (step 2).

The pull request carries code only. Never commit a receipt: the gate finds the signed evidence by the digest of each changed file.

  1. Trust roots: GitHub Actions OIDC and Sigstoreoutside APMX
  2. Trusted signer: the attest jobof danielmeppiel/apmx/.github/workflows/run.yml
  3. Verification summary (SLSA VSA v1): PASSEDnames the factory definition digest and every receipt line by digest
  4. Receipt statementsprovenance, check results, inventory
  5. Delivered files, by SHA-256
  6. Pull requestevery changed file must be a delivered file, with the same bytes, on a current base

apm-policy.yml, read from the PR base

  • signers with workflow_sha

    which workflow may sign, pinned to a commit

  • require_signed

    a signed summary is required

  • allow

    which factory definition digests count

  • revoked

    skills or instructions that no longer count

apmx audit checks each link it can. A checked link that is wrong is [x]; a link that could not be checked makes the verdict [!] INCOMPLETE.

Trust flows from GitHub's identity for the signer, through a signed summary, to the exact files in the pull request. Your policy decides which signer and which factories count.

Copy apm-policy.yml to the root of the receiving repository. APM keeps its own keys; APMX reads only factories.

apm-policy.yml
factories:
allow:
# Replace with your factory's definition digest: the Factory row of
# `apmx audit`, or facts.definitionSha256 in its JSON output.
# This all-zero placeholder allows nothing.
- "acme/checkout-factory@sha256:0000000000000000000000000000000000000000000000000000000000000000"
require_signed: true
signers:
# apmx's reusable run.yml, pinned to the commit your factory calls
# (`git rev-parse v0.7.1^{commit}`). A tag can move; this SHA cannot.
# Re-pin when you upgrade the run.yml your factory calls.
- workflow: danielmeppiel/apmx/.github/workflows/run.yml
workflow_sha: a70c1939dddc6f5d7888b76fd5bb8bab3e69e886 # git rev-parse v0.7.1^{commit}
repo: acme/factories # where the factory ran: its attestations are stored there
revoked: []

Until you replace the placeholder digest, every delivery fails the Factory row. The name before @sha256: is only a label.

The protections need all three keys:

  • allow stops a weakened check or contract: any change gives a new definition digest, which is not on the list. Without allow, any definition passes.
  • require_signed and signers stop a hand-written or forged receipt: only a summary from your signer counts. The gate also needs signers to find a delivery at all.

How to find the digest, roll out in warn mode and re-pin the signer: approve, upgrade, revoke. Every key: policy reference.

Add the gate workflow from examples/accept-factory-prs/audit.yml:

.github/workflows/audit.yml
# .github/workflows/audit.yml in the receiving repository.
# pull_request_target runs THIS file from the base branch, never the PR's copy,
# so a PR cannot rewrite its own gate. Nothing from the PR is executed. The head
# is fetched with the job's read-only token, which is not persisted, and is
# checked out only as bytes for `apmx audit` to hash.
on:
pull_request_target:
types: [opened, synchronize, reopened]
permissions: { contents: read, attestations: read }
jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 0
persist-credentials: false
- uses: danielmeppiel/apmx/.github/actions/setup@v0.7.1 # installs apmx 0.7.1
- run: apmx audit
env: { GH_TOKEN: "${{ github.token }}" }
  • pull_request_target runs the base branch’s copy, so a PR cannot edit or delete its own gate. The policy is read from the PR base commit too.
  • fetch-depth: 0 is required: the gate compares the PR’s base and head.
  • The only credential is the job’s read-only GITHUB_TOKEN: checkout uses it without persisting it (persist-credentials: false), and apmx audit receives it as GH_TOKEN to look up attestations.
  • Make the audit check required, with GitHub Actions as its source.

Green means accepted on trusted evidence, and nothing else. It is the configured condition for auto-merge; it is not a guarantee that the code is correct.

Both ran against the public apmx-demo-shop. PR #11 is a factory delivery. PR #12 is the same delivery plus a hand edit to src/checkout.py. Output is unabridged.

PR #11: apmx audit (v0.7.1)
Receipt attestations for PR #11 (looked up by file digest) 1 contract signed
[+] Policy apm-policy.yml (repository, PR base 4fed7ee21e1d) factories enforcement block
[+] Integrity 4 statements; line digests computed; the VSA binds every other line; evidence not present (optional)
[+] Standards in-toto Statement v1, SLSA Provenance v1, in-toto Test Result, CycloneDX 1.5, SLSA VSA v1: schema-valid
[+] Factory definition sha256:c011c35b5790.. on factories.allow (danielmeppiel/apmx/tests/fixtures/run-workflow-package)
definition sha256:c011c35b5790.. danielmeppiel/apmx/tests/fixtures/run-workflow-package#v0.7.1 1 contract, 1 check
[+] Checks 1/1 passed (in-toto test-result)
[+] Ingredients 1 APM dependency apm audit --ci: clean (content, lock, drift)
installed bytes match CycloneDX inventory and recorded hashes
[+] Revocation 0 of 0 capabilities revoked; factories.revoked is empty
[+] Outputs PR #11: 1 changed file = delivered files at head d85c418bdf11; before-images match base 4fed7ee21e1d
built on 2ca997db3993, in the PR base's history
1 file bound: handoff.json
[+] Identity signed by danielmeppiel/apmx/.github/workflows/run.yml@refs/tags/v0.7.1 at a70c1939dddc (tag object 917ffb4e5406)
run https://github.com/danielmeppiel/apmx-demo-shop/actions/runs/39740103826/attempts/1
[+] VALID (authenticated: danielmeppiel/apmx/.github/workflows/run.yml)
PR #12: apmx audit (v0.7.1)
Receipt attestations for PR #12 (looked up by file digest) unsigned
[+] Policy apm-policy.yml (repository, PR base 4fed7ee21e1d) factories enforcement block
[i] Integrity not evaluated (no signed delivery covers this PR)
[i] Standards not evaluated (no signed delivery covers this PR)
[i] Factory not evaluated (no signed delivery covers this PR)
[i] Checks not evaluated (no signed delivery covers this PR)
[i] Ingredients not evaluated (no signed delivery covers this PR)
[i] Revocation not evaluated (no signed delivery covers this PR)
[i] Outputs not evaluated (no signed delivery covers this PR)
[x] Identity no PASSED verification summary from factories.signers covers every changed file (src/checkout.py is not a delivered file)
danielmeppiel/apmx/.github/workflows/run.yml: attestation ignored (summary does not cover src/checkout.py)
danielmeppiel/apmx/.github/workflows/run.yml: attestation ignored (summary does not cover src/checkout.py)
danielmeppiel/apmx/.github/workflows/run.yml: attestation ignored (summary does not cover src/checkout.py)
[x] INVALID Identity: no PASSED verification summary from factories.signers covers every changed file (src/checkout.py is not a delivered file)

PR #12’s summary covers only the delivered file, so the extra file src/checkout.py has no signed delivery behind it: INVALID.

A human pull request fails the gate by design:

PR #9, a human PR: apmx audit (main, unreleased)
Receipt attestations for PR #9 (looked up by file digest) unsigned
[+] Policy apm-policy.yml (repository, PR base e738f0fd7734) factories enforcement block
[i] Integrity not evaluated (not a factory delivery)
[i] Factory not evaluated (not a factory delivery)
[i] Checks not evaluated (not a factory delivery)
[i] Dependencies not evaluated (not a factory delivery)
[i] Revocation not evaluated (not a factory delivery)
[i] Outputs not evaluated (not a factory delivery)
[x] Identity not a factory delivery: none of 1 changed file digest(s) carries a factory attestation in danielmeppiel/apmx-demo-shop
[x] Not a factory delivery: no changed file carries an attestation from factories.signers. This check only passes factory deliveries; merge other PRs through your normal review process (e.g. a ruleset bypass for maintainers).

Merge it through a ruleset bypass after your usual review, or let factories deliver to repositories or branches only they write to. APMX is for your own factories; it does not judge AI pull requests from outside contributors, and it posts no comments: it is one required check.

A private platform repository can run approved factories against many repositories, with a GitHub App token per job scoped to one target that the agent’s job never receives. An organization ruleset then requires the gate everywhere, and the organization’s apm-policy.yml pins the approved factories and the signer. See one factory, many repositories.

Next: what the gate stops, and under which policy, in the trust model; day-two policy tasks in approve, upgrade, revoke.