Schemas
Three schemas define the substrate’s wire contracts: the elevation request (in PR bodies), the tier policy config (checked into the repo), and the audit event record (on the orphan audit branch). All are versioned v1; future-incompatible changes bump the version with an explicit migration path, not a silent rewrite.
Elevation PR frontmatter
Section titled “Elevation PR frontmatter”The elevation request is a YAML frontmatter block at the top of the PR body. All fields are required; the validator is strict-mode (unknown fields fail closed).
| Field | Type | Constraint |
|---|---|---|
schema | string | Must equal "v1". |
target_adapter | string enum | Initial values: cloudflare, github-app-pem. The enum records which adapters the substrate knows about; a tier’s allowed_adapters list is the call-site gate. A value in the enum does not imply callability. |
scope | map or list | Adapter-specific opaque payload describing what write capability is requested. The substrate does not interpret; each adapter validates its own scope. Substrate-level constraint: must be a non-empty map or list. |
ttl | ISO-8601 duration string | PT30M, PT1H, PT15M. Validator enforces ttl <= tier.max_ttl for the declared risk_tier. ISO-8601 (rather than integer seconds) is human-readable in PR bodies and unambiguous across timezones. |
risk_tier | string enum | low, medium, or high. Matches the taxonomy used elsewhere in the canon (e.g. scheduled-workflow risk tiers) so the same word means the same thing across the portfolio. |
justification | markdown string | Non-empty after trim. Surfaced to reviewers at PR-review time. A one-line “please approve” body fails the validator. |
rollback_plan | markdown string | Non-empty after trim. Forces the author to articulate how the change is undone. |
Worked example — valid elevation frontmatter
Section titled “Worked example — valid elevation frontmatter”---schema: v1target_adapter: cloudflarescope: account_id: "<redacted>" resource: pages_project project_name: canon-web operation: update_build_config fields: [build_command, root_dir]ttl: PT30Mrisk_tier: lowjustification: | The Pages project's `root_dir` was set to `web` in the original canon-web-infra apply but the repo has since restructured to `apps/web`. Production deploys have been 522 since the rename. This elevation lets the CF-adapter apply run update the build config so deploys resolve.rollback_plan: | Revert this PR. The next reconcile-apply will restore the prior `root_dir` value from state; no manual dashboard action needed.---
Production is currently 522 on `safeagentic.org` — see thecanon-web-infra retrospective's open-items trace. This is theapply that closes it.Validator behaviour
Section titled “Validator behaviour”- Trigger.
on: pull_requestwith typesopened,synchronize,reopened,edited. Theeditedevent re-runs the validator when an author amends the PR body. - Elevation-PR detection. Parse the first YAML frontmatter block at PR-body start. If the parsed object carries both
schema: v1and atarget_adapter:key, apply the full checks. Otherwise, pass through (validator is a no-op). - Fail-closed on unparseable frontmatter. If the
---/---delimiters are present but the YAML does not parse, exit non-zero withelevation-policy: frontmatter present but unparseable. Silent-pass on malformed input would let a malformed-but-merged PR slip past the gate. - Structured errors. Each violation names the specific field or constraint:
missing,unknown(strict mode),bad-enum(target_adapter,risk_tier),bad-duration(ttl),ttl-over-max,adapter-not-allowed,empty(after trim),unparseable. - Status-check context.
elevation-policy— this string is the wire contract referenced by the branch ruleset’srequired_status_checkslist.
Policy config (.github/elevation-policy.yml)
Section titled “Policy config (.github/elevation-policy.yml)”The tier-policy file is declarative and lives in-repo so it rides the same review surface as any other config. A weakening change (raising max_ttl, adding an adapter to allowed_adapters, loosening an environment) is as reviewable as an application change.
schema: v1tiers: low: max_ttl: PT30M allowed_adapters: [cloudflare] environment: null rationale: "Non-destructive config changes — env-var tweaks, build-cache flips, feature-flag updates. Ruleset + PR review + validator is the gate; no environment-reviewer pause." medium: max_ttl: PT1H allowed_adapters: [cloudflare] environment: infrastructure rationale: "Resource modifications — DNS records, Pages build config, new environment secrets. Second pair of eyes on the act of applying." high: max_ttl: PT15M allowed_adapters: [cloudflare] environment: infrastructure rationale: "Destructive or billing-impacting changes — domain removal, account settings, resource destroys. Tight TTL forces scoped, focused action."reviewer_teams: []Field semantics
Section titled “Field semantics”schema: v1— tied to the elevation-metadata schema version. Bumping one without the other requires an explicit migration note.tiers.<name>.max_ttl— ISO-8601 duration, same syntax as the elevation request’sttl. The validator’sttl <= tier.max_ttlcheck reads this.tiers.<name>.allowed_adapters— list oftarget_adapterenum values permitted at this tier. Per-tier trust policy is the primary axis: a future adapter may be appropriate atmedium/highbut notlow, and that distinction lives here rather than on the adapter.tiers.<name>.environment— GitHub Actions environment name, ornull. When non-null, the adapter feature’s apply workflow declaresenvironment: <name>and the environment’srequired_reviewersfire at apply time.nullmeans no apply-time reviewer gate; PR review + validator is the only gate.tiers.<name>.rationale— prose justification for that tier’s boundaries. Mirrored in file header comments of the actual policy file; the rationale is the source-of-truth for the tier-justification acceptance criterion.reviewer_teams— empty list defaults to CODEOWNERS. Duplicating CODEOWNERS into the policy file would create a drift surface. A future tier-specific team (e.g. a security-posture team scoped tohighonly) lands here as an explicit override.
Rationale-comment convention
Section titled “Rationale-comment convention”The .github/elevation-policy.yml file carries the tier rationale twice: once as the rationale: field (machine-readable, surfaced by the validator in error annotations), and once as YAML header comments. The redundancy is deliberate — the header comments are what a human reading the policy file first sees; the rationale: field is what the validator surfaces in structured error output. Keep both in sync on edits.
Audit event record
Section titled “Audit event record”Events land as JSON files on the orphan audit branch at YYYY/MM/DD/<pr-number>-<short-sha>/<event>-<sequence>.json. Each record has this shape:
{ "correlation_id": "canon-elevation-pr-42-a1b2c3d", "pr_number": 42, "pr_sha": "a1b2c3d", "event": "issued", "at": "2026-04-22T14:03:17Z", "actor": "github-app-id:12345", "target_adapter": "cloudflare", "scope": { "account_id": "<redacted>", "resource": "pages_project", "project_name": "canon-web", "operation": "update_build_config", "fields": ["build_command", "root_dir"] }, "ttl_seconds": 1800, "provider_token_id": "<cloudflare-token-uuid>", "apply_run_id": "14823091276", "result": "success"}Field-by-field
Section titled “Field-by-field”correlation_id— canonical formcanon-elevation-pr-<N>-<short_sha>. Embedded in provider credentialnamefields (e.g. Cloudflare’s tokenname) so provider audit logs corroborate without a sidecar metadata table. A reader queryingcf audit-log filter name=canon-elevation-pr-42-a1b2c3dgets the provider-side slice;gh api /repos/.../contents/2026/04/22/42-a1b2c3d/?ref=auditgets the substrate-side slice; the correlation ID joins them.pr_number/pr_sha— primary keys for joining against the GitHub PR object (reviewers, merge event, review decisions viagh api /repos/.../pulls/<N>).event— enum:issued | applied | revoked.at— RFC3339 UTC timestamp.actor— writer identity. For machine-issued events this is the GitHub App ID; for rare human-overridden paths it is the login of the human who performed the action.target_adapter— echo of the PR frontmatter’starget_adapter.scope— echo of the PR frontmatter’sscope, as submitted. The audit record captures what was requested verbatim; adapter-level interpretation is not re-serialised here.ttl_seconds— integer seconds, derived from the frontmatter’s ISO-8601ttl. Integer for downstream arithmetic (“didrevokedfire withinttl_secondsofissued?”).provider_token_id— adapter-specific opaque ID sufficient to look up the credential in the provider’s own audit surface.apply_run_id— GitHub Actions run ID of the workflow that performed the issuance / apply / revocation. Makes the GHA logs reachable from an audit record.result—success | failure. Failure is a first-class event; a failed apply producesapplied-<N>.jsonwithresult: failure, not an absent record.
Event-type enum
Section titled “Event-type enum”issued— adapter has minted the elevated credential.applied— the apply run has executed (success or failure —resultcaptures which).revoked— TTL expiry or explicit revocation has fired and the credential is no longer usable.
File layout on the audit branch
Section titled “File layout on the audit branch”YYYY/MM/DD/<pr-number>-<short-sha>/<event>-<sequence>.jsonExample — PR #42 with short_sha=a1b2c3d, merged 2026-04-22:
2026/04/22/42-a1b2c3d/issued-0.json2026/04/22/42-a1b2c3d/applied-0.json2026/04/22/42-a1b2c3d/applied-1.json # retried apply2026/04/22/42-a1b2c3d/revoked-0.jsonSequence counter
Section titled “Sequence counter”Per-elevation monotonic, starting at 0. Retries of the same event type bump the counter (applied-1.json, applied-2.json for re-runs). The writer’s push loop:
- Fetches
origin/audit. - Scans the elevation’s directory for existing
<event>-<N>.jsonfiles. - Writes
<event>-<max(N)+1>.json. - Commits (GPG-signed with the writer identity’s key) and pushes.
Protected-branch semantics (non_fast_forward) serialise concurrent pushes — racing commits fail the push. The writer re-fetches, recomputes the next sequence counter, and re-pushes. Concurrent writes are rare in practice (elevations are human-gated and operate at human scale), but the serialisation is the correctness story, not the throughput story.
Rejected elevation PRs
Section titled “Rejected elevation PRs”No audit-branch emission. The GitHub PR object itself holds the rejection as a first-class artefact — state=closed, merged=false, review decisions via gh api /repos/.../pulls/<N>/reviews. The audit branch focuses on lifecycle events that produced credentials; a rejection produced none, so there is nothing adapter-side to correlate.
Audit-join result shape
Section titled “Audit-join result shape”The audit-join operation combines per-PR event records with PR metadata fetched via the GitHub API. The return shape is:
interface AuditJoinResult { correlation_id: string; pr_number: number; events: AuditEvent[]; // chronological pr_metadata: { author: string; merged: boolean; reviewers: string[]; // approving reviewers merged_at: string | null; }; missing_phases: { issued: boolean; applied: boolean; revoked: boolean; };}missing_phases
Section titled “missing_phases”Flags lifecycle incompleteness for callers. Each boolean is true when no event of that type appears in events. Examples:
{ issued: false, applied: false, revoked: false }— complete lifecycle, credential issued, applied, and revoked.{ issued: false, applied: false, revoked: true }— apply succeeded but revocation did not fire. Potentially a stuck credential; the caller can cross-check provider-sideexpires_onenforcement.{ issued: true, applied: true, revoked: true }— PR merged but no adapter events landed. Either the apply workflow failed before emittingissued, or the elevation was merged without routing through an adapter (misconfiguration).
missing_phases is advisory; the caller decides how to act (alert, backfill, investigate). The substrate surfaces the signal; interpretation is adapter- or operator-level.