Skip to content

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.

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

FieldTypeConstraint
schemastringMust equal "v1".
target_adapterstring enumInitial 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.
scopemap or listAdapter-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.
ttlISO-8601 duration stringPT30M, 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_tierstring enumlow, 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.
justificationmarkdown stringNon-empty after trim. Surfaced to reviewers at PR-review time. A one-line “please approve” body fails the validator.
rollback_planmarkdown stringNon-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: v1
target_adapter: cloudflare
scope:
account_id: "<redacted>"
resource: pages_project
project_name: canon-web
operation: update_build_config
fields: [build_command, root_dir]
ttl: PT30M
risk_tier: low
justification: |
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 the
canon-web-infra retrospective's open-items trace. This is the
apply that closes it.
  • Trigger. on: pull_request with types opened, synchronize, reopened, edited. The edited event 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: v1 and a target_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 with elevation-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’s required_status_checks list.

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: v1
tiers:
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: []
  • 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’s ttl. The validator’s ttl <= tier.max_ttl check reads this.
  • tiers.<name>.allowed_adapters — list of target_adapter enum values permitted at this tier. Per-tier trust policy is the primary axis: a future adapter may be appropriate at medium/high but not low, and that distinction lives here rather than on the adapter.
  • tiers.<name>.environment — GitHub Actions environment name, or null. When non-null, the adapter feature’s apply workflow declares environment: <name> and the environment’s required_reviewers fire at apply time. null means 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 to high only) lands here as an explicit override.

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.

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"
}
  • correlation_id — canonical form canon-elevation-pr-<N>-<short_sha>. Embedded in provider credential name fields (e.g. Cloudflare’s token name) so provider audit logs corroborate without a sidecar metadata table. A reader querying cf audit-log filter name=canon-elevation-pr-42-a1b2c3d gets the provider-side slice; gh api /repos/.../contents/2026/04/22/42-a1b2c3d/?ref=audit gets 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 via gh 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’s target_adapter.
  • scope — echo of the PR frontmatter’s scope, 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-8601 ttl. Integer for downstream arithmetic (“did revoked fire within ttl_seconds of issued?”).
  • 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.
  • resultsuccess | failure. Failure is a first-class event; a failed apply produces applied-<N>.json with result: failure, not an absent record.
  • issued — adapter has minted the elevated credential.
  • applied — the apply run has executed (success or failure — result captures which).
  • revoked — TTL expiry or explicit revocation has fired and the credential is no longer usable.
YYYY/MM/DD/<pr-number>-<short-sha>/<event>-<sequence>.json

Example — PR #42 with short_sha=a1b2c3d, merged 2026-04-22:

2026/04/22/42-a1b2c3d/issued-0.json
2026/04/22/42-a1b2c3d/applied-0.json
2026/04/22/42-a1b2c3d/applied-1.json # retried apply
2026/04/22/42-a1b2c3d/revoked-0.json

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:

  1. Fetches origin/audit.
  2. Scans the elevation’s directory for existing <event>-<N>.json files.
  3. Writes <event>-<max(N)+1>.json.
  4. 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.

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.

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;
};
}

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-side expires_on enforcement.
  • { issued: true, applied: true, revoked: true } — PR merged but no adapter events landed. Either the apply workflow failed before emitting issued, 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.