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.
1. Run agent, checks and signer apart
Section titled “1. Run agent, checks and signer apart”run.yml: each job on a fresh runner
- The agent worksproducemodel token; read-only contents; no signing identity
- producer receipt (untrusted)Checks verifycheckreruns your checks on the agent's bytes; no write token, no signing identity
- receipt of recordA 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.
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.
2. Pin what you accept
Section titled “2. Pin what you accept”- Trust roots: GitHub Actions OIDC and Sigstoreoutside APMX
- Trusted signer: the attest jobof danielmeppiel/apmx/.github/workflows/run.yml
- Verification summary (SLSA VSA v1): PASSEDnames the factory definition digest and every receipt line by digest
- Receipt statementsprovenance, check results, inventory
- Delivered files, by SHA-256
- 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
Constrains link 2.
require_signed
a signed summary is required
Constrains link 3.
allow
which factory definition digests count
Constrains link 3 and 4.
revoked
skills or instructions that no longer count
Constrains link 4.
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.
Copy apm-policy.yml
to the root of the receiving repository. APM keeps its own keys; APMX reads
only factories.
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:
allowstops a weakened check or contract: any change gives a new definition digest, which is not on the list. Withoutallow, any definition passes.require_signedandsignersstop a hand-written or forged receipt: only a summary from your signer counts. The gate also needssignersto 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.
3. Gate the PR
Section titled “3. Gate the PR”Add the gate workflow from
examples/accept-factory-prs/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_targetruns 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: 0is 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), andapmx auditreceives it asGH_TOKENto look up attestations. - Make the
auditcheck 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.
4. A real pair
Section titled “4. A real pair”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.
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)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)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 intact and well-formed; line digests computed; the VSA binds every other line; evidence not present (optional)[+] 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)[+] Dependencies 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)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] Factory not evaluated (no signed delivery covers this PR)[i] Checks not evaluated (no signed delivery covers this PR)[i] Dependencies 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.
Merge everything else the normal way
Section titled “Merge everything else the normal way”A human pull request fails the gate by design:
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.
One factory for many projects
Section titled “One factory for many projects”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.