Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions .github/actions/inject-agent-receipt/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
name: Inject agent verification receipt
description: >-
Validate an agent verification receipt (ges.agent-verification-receipt.v1) and insert it into the
pull request body between idempotent markers. Fails visibly on an invalid or stale receipt, an
unsupported event, or a token that cannot write the body; it never skips silently.
inputs:
receipt-path:
description: Path to the sealed receipt JSON, relative to the workspace. The receipt must be produced by a trusted step, not read from untrusted PR content.
required: true
pull-request-number:
description: Pull request number. Defaults to the number in the triggering pull_request event.
required: false
default: ''
github-token:
description: Token with pull-requests write permission. The calling job must grant it.
required: false
default: ${{ github.token }}
outputs:
receipt-id:
description: Identity of the receipt now embedded in the pull request body.
value: ${{ steps.inject.outputs.receipt-id }}
runs:
using: composite
steps:
- id: inject
shell: bash
env:
RECEIPT_PATH: ${{ inputs.receipt-path }}
INPUT_PR: ${{ inputs.pull-request-number }}
EVENT_PR: ${{ github.event.pull_request.number }}
EVENT_NAME: ${{ github.event_name }}
REPOSITORY: ${{ github.repository }}
GH_TOKEN: ${{ inputs.github-token }}
ACTION_PATH: ${{ github.action_path }}
WORKSPACE: ${{ github.workspace }}
RUNNER_TEMP_DIR: ${{ runner.temp }}
run: |
set -euo pipefail
fail() { echo "::error title=agent-receipt::$1"; exit 1; }
[ "$EVENT_NAME" != "pull_request_target" ] || fail "pull_request_target is refused: the receipt would come from an untrusted context"
pr="${INPUT_PR:-$EVENT_PR}"
case "$pr" in ''|*[!0-9]*) fail "pull-request-number is missing or not numeric" ;; esac
receipt="$WORKSPACE/$RECEIPT_PATH"
[ -f "$receipt" ] || fail "receipt file not found: $RECEIPT_PATH"
tool="$ACTION_PATH/../../../ges/receipts.py"
python3 "$tool" validate "$receipt" || fail "receipt failed validation"
work="$RUNNER_TEMP_DIR/agent-receipt-$pr"
mkdir -p "$work"
gh api "repos/$REPOSITORY/pulls/$pr" > "$work/pr.json" || fail "cannot read the pull request"
python3 "$tool" inject --receipt "$receipt" --pr-json "$work/pr.json" \
--repository "$REPOSITORY" --pull-request "$pr" --output "$work/body.md" \
|| fail "receipt does not bind to this pull request head or the body cannot be updated"
if ! gh api -X PATCH "repos/$REPOSITORY/pulls/$pr" -F "body=@$work/body.md" > "$work/patched.json"; then
fail "cannot write the pull request body (fork pull requests and read-only tokens cannot; the job needs pull-requests: write)"
fi
gh api "repos/$REPOSITORY/pulls/$pr" > "$work/after.json"
id=$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["id"])' "$receipt")
python3 "$tool" extract --pr-json "$work/after.json" --expect-id "$id" \
--repository "$REPOSITORY" --pull-request "$pr" >/dev/null \
|| fail "after the update the receipt is missing from the body or no longer binds to the current head (concurrent edit or push)"
echo "receipt-id=$id" >> "$GITHUB_OUTPUT"
echo "Embedded $id into $REPOSITORY#$pr"
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,5 @@ See `evidence/recovery-status.json` for measured construction status and
## Unreleased

Phase 1 (secrets & supply chain) warn-only package: reusable `phase1-warn.yml`, `standards/phase1/` checks, pass/fail fixtures, and `docs/phase1-warn-only.md`. No required checks or ruleset changes.

Agent verification receipts: `ges.agent-verification-receipt.v1` schema, stdlib validator (`python -m ges receipt ...`), valid/invalid fixtures, and the `inject-agent-receipt` composite action; see `docs/agent-verification-receipts.md`. No required checks or ruleset changes.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ Unresolved dependencies and conditional/version rendering remain explicit review

## Where to look

`controls/catalog.json` is the canonical authored control definition. `generated/functional-checklist.md`, `source-crosswalk.md` and `bindings.json` are generated from it. `templates/` contains implementation starting points; `profiles/` contains explicitly draft target policies. `ges/` owns acquisition, assessment and compilation behavior. `tests/` exercises false-pass prevention and deterministic outputs. `docs/` records architecture, limitations, decisions and acceptance criteria. `evidence/` records executed work, not aspirations.
`controls/catalog.json` is the canonical authored control definition. `generated/functional-checklist.md`, `source-crosswalk.md` and `bindings.json` are generated from it. `templates/` contains implementation starting points; `profiles/` contains explicitly draft target policies. `ges/` owns acquisition, assessment and compilation behavior. `tests/` exercises false-pass prevention and deterministic outputs. `schemas/receipts/` and [agent verification receipts](docs/agent-verification-receipts.md) define the strict Staged/Merged/Verified receipt, its validator and the PR-body action. `docs/` records architecture, limitations, decisions and acceptance criteria. `evidence/` records executed work, not aspirations.

The large source corpus and page-level review queues are workflow artifacts and a separate downloadable evidence bundle; they are not duplicated into every consumer repository.

Expand Down
157 changes: 157 additions & 0 deletions docs/agent-verification-receipts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
# Agent verification receipts

`ges.agent-verification-receipt.v1` is a strict, closed JSON record of what an agent
observed when it verified a change, plus a validator, fixtures and a reusable GitHub
Action that embeds the receipt in a pull request body. It encodes this repository's
verification standard (AGENTS.md, GOVERNANCE.md) so that "Staged", "Merged" and
"Verified" are three machine-checkable claims instead of prose.

Contract files:

- schema: `schemas/receipts/agent-verification-receipt.v1.json`
- validator, injector and CLI: `ges/receipts.py` (standard library only)
- fixtures: `tests/fixtures/receipts/` (`valid/`, `invalid/`, `expected-errors.json`, `generate.py`)
- action: `.github/actions/inject-agent-receipt/action.yml`

## Formal semantics

Let a receipt `R` carry checks `C`, a claim `claim(R)`, optional merge evidence `M`
and optional human acceptance `A`. Every rule below is enforced by `validate_receipt`;
each has a negative fixture.

**Check status.** A check has exactly one status.

| Status | Meaning | Required | Forbidden |
| --- | --- | --- | --- |
| `PASS` | The command ran and exited 0 | `exit_code = 0`, `output_sha256`, both timestamps | none beyond the table |
| `FAIL` | The command ran and failed | `exit_code != 0`, both timestamps | `exit_code = 0` |
| `NOT_RUN` | No execution occurred | the command that was intended | exit code, output digest, timestamps, provider conclusion |
| `NOT_VERIFIABLE` | A run (or provider verdict) exists but does not establish success | the command or check name | exit code, output digest |

A check that mirrors a GitHub check run records the REST `conclusion` in
`github_conclusion`. The mapping is total and fixed:

| GitHub conclusion | Receipt status |
| --- | --- |
| `success` | `PASS` (exit code may be null; evidence digest still required) |
| `failure`, `timed_out` | `FAIL` |
| `cancelled`, `skipped`, `neutral`, `stale`, `action_required` | `NOT_VERIFIABLE` |
| no conclusion yet | `NOT_RUN` (`github_conclusion` null) |

So a skipped or neutral check can never read as green, matching the repository's
honest-green aggregate (`standards/phase1/honest_green.py`). Missing evidence is
never `PASS`.

**Overall status** is derived, never asserted: `FAIL` if any check is `FAIL`;
otherwise `INCOMPLETE` if any check is `NOT_RUN` or `NOT_VERIFIABLE`; otherwise `PASS`.
A receipt whose `overall_status` differs from the derivation is invalid.

**Claim ladder.** `claim` is the strongest statement the receipt makes.

| Claim | Requires | Forbids |
| --- | --- | --- |
| `STAGED` | nothing beyond valid checks (branch-only evidence) | merge evidence, acceptance |
| `MERGED` | `merge_evidence` (main SHA, merge time, CI run URL in the subject repository, CI conclusion) | acceptance |
| `VERIFIED` | `overall_status = PASS`, a non-empty `required_checks`, `merge_evidence.ci_conclusion = success`, every check started at or after `merged_at` (observed on the merged commit, not the branch), human `acceptance` at or after the merge time | none |

`VERIFIED` therefore requires, in the receipt's own words, the merged-main and
organization-acceptance conditions of the Verification Standard. A receipt cannot
prove who the named human is; `acceptance.reference` is a pointer a reviewer follows.

**Required checks.** `required_checks` names the checks the producer's policy demands
(for this repository, the verification commands in AGENTS.md). Every name must have a
recorded result in any receipt, so an omitted check cannot hide; `VERIFIED` additionally
requires the set to be non-empty. The set is declared by the producer: the validator
proves the receipt is complete against its own declaration, and a reviewer or caller
policy decides whether the declaration is the right one. The schema does not hard-code
any repository's command list.

**Identity and time.** `id` is `agent-verification-receipt:` plus the SHA-256 of the
canonical encoding (sorted keys, compact separators, UTF-8) of every other field.
Timestamps are UTC with a trailing `Z`; a check's finish does not precede its start,
and `generated_at` is not earlier than any check finish or merge/acceptance time.
Check names are unique; commit SHAs are 40 lowercase hex characters.

**What a receipt does not establish.** It does not authenticate the agent, prove the
commands ran, or replace human review. Validation is structural plus internal
consistency. The `limitations` array exists for the producer to state what was not
evaluated.

## Validator

The validator implements the closed JSON Schema vocabulary the schema uses
(`SUPPORTED_KEYWORDS` in `ges/receipts.py`); a test fails if the schema ever uses a
keyword the validator does not enforce, if an object schema is open, or if a property
is optional. It reports every violation, not only the first.

```
python -m ges receipt validate receipt.json # exit 0 valid, 2 invalid
python -m ges receipt seal --input draft.json --output receipt.json
python -m ges receipt render --receipt receipt.json
python -m ges receipt inject --receipt receipt.json --body-file body.md --output new-body.md
python -m ges receipt extract --body-file new-body.md
```

`seal` adds `schema` and `id` and refuses to write an invalid receipt.
Regenerate fixtures with `python tests/fixtures/receipts/generate.py`; a test fails
if committed fixtures drift from the generator.

## PR-body injection

The block sits between `<!-- ges:agent-verification-receipt:begin -->` and
`<!-- ges:agent-verification-receipt:end -->`. Injection is idempotent: a re-run
replaces the block and leaves all other body text untouched. Repeated or unbalanced
markers fail. The visible table shows only the check name (reduced to a safe
character set, so mentions and links cannot render), status and exit code; all other
detail is inside a fenced JSON block whose fence is longer than any backtick run in
the content. Receipt text containing the marker is refused. `extract` re-renders the
embedded receipt and rejects a block that was edited after rendering.

## Action

```yaml
# Caller workflow (pull_request trigger, same-repository branches)
permissions:
contents: read
pull-requests: write
jobs:
receipt:
runs-on: ubuntu-24.04
timeout-minutes: 5
concurrency:
group: agent-receipt-${{ github.event.pull_request.number }}
cancel-in-progress: true
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
with:
persist-credentials: false
# ...a trusted step writes receipt.json from observed results...
- uses: 4444J99/guide-github-standard/.github/actions/inject-agent-receipt@<full-commit-sha>
with:
receipt-path: receipt.json
```

Pin the action to a full commit SHA. The step:

1. refuses `pull_request_target` (the receipt would originate in an untrusted context);
2. validates the receipt and fails on any violation;
3. fetches the PR and refuses a receipt whose repository, PR number or `head_sha` differs
from the PR's current head (stale or foreign evidence is never embedded);
4. patches the body, then re-reads the PR and confirms the same receipt id is present and
still binds to the PR's head at that moment (a push during the update fails the step).

Failure is always visible: an invalid receipt, a read-only token (fork pull requests,
or a job without `pull-requests: write`) and a lost update from a concurrent body edit
each fail the step. Nothing is skipped silently. The action takes no inputs through
shell interpolation; values reach the script as environment variables. It has no
third-party `uses:` dependencies. The receipt path is read from the workspace, so the
producing step must not take its content from untrusted PR text.

Outputs: `receipt-id`.

## Limits

Concurrent edits to the PR description between the read and the write can still be
lost; the post-write check detects the case in which the receipt itself is lost but not
other overwritten text. This tranche adds no required check, ruleset change or consumer
onboarding.
5 changes: 5 additions & 0 deletions ges/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ def main(argv=None):
sub=p.add_subparsers(dest='command',required=True)
from .semantics import configure_parser
configure_parser(sub)
from .receipts import configure_parser as configure_receipts
configure_receipts(sub)
c=sub.add_parser('branch-checks', help='Read-only exact-revision required-check observation; no enforcement certification')
c.add_argument('--snapshot',type=Path,required=True); c.add_argument('--profile',type=Path,required=True)
c.add_argument('--revision',required=True); c.add_argument('--revision-kind',choices=['head','test_merge','merge_group'],required=True)
Expand Down Expand Up @@ -44,6 +46,9 @@ def main(argv=None):
report=assess(load(args.snapshot),load(args.profile),args.revision,args.revision_kind)
dump(args.output,report); print(json.dumps(report['summary'],indent=2))
return 0 if report['observed_checks_pass'] else 1
if args.command == 'receipt':
from .receipts import run as run_receipt
return run_receipt(args)
if args.command == 'semantics':
from .semantics import run
return run(args)
Expand Down
Loading
Loading