Spec 04 — Review, Approval, and the Trust Dial
Two-level approval, the nonce, the trust dial and the audit record.
1. Two-level approval
A change originating from a requestor passes two gates:
- Intent (requestor). The requestor reviews the preview — the actual rendered page, not a diff — and confirms: “this is what I meant”. The confirmation is a reply to the agent’s message (typically email), quoting the nonce.
- Publication (publisher). A human authorises live publication by
merging the PR. Merging is the only mechanism that changes production
(
spec/05).
Owner-maintained sites where requestor = publisher = one human collapse both levels into the PR review — legitimately: the dial records it, and the PR trail still shows who did what.
2. The nonce
- The editor agent generates a random nonce per proposed change and embeds it
in its reply to the requestor (“Reply APPROVE
to confirm, or tell me what to change”). - An approval is valid only if: it comes from the allowlisted sender address, it is in the same thread, and it quotes the nonce.
- A nonce is single-use and expires (SHOULD: 14 days).
Why: this binds an approval to a specific proposed change in a specific thread, closing the forge-a-reply and approve-the-old-draft paths.
3. The trust dial
The dial maps (sender × change class) to approval requirements. It is
data (trust.yaml), versioned in the site repository; changing the dial is a
structural change and goes through the full pipeline.
3.1 Change classes
| Class | Examples |
|---|---|
typo |
fixing a misspelling, broken link |
content-update |
edit to an existing page’s body; setting, moving, or clearing a featured flag (spec/02 §9); repointing an existing menu item’s ref or editing its label |
event-update |
date/time/venue change |
new-content |
a new post, news item, document |
structural |
new page type, menu structure changes (adding, removing or reordering items), template/block changes, slug changes, trust dial changes, chrome changes |
3.2 Dial levels
| Level | Intent gate | Publication gate | Audit |
|---|---|---|---|
| L1 dual (default) | requestor nonce-approval | publisher merge | full |
| L2 delegated | requestor nonce-approval alone triggers merge | publisher reviews post-merge (notification within the review SLA) | full, marked post-hoc |
| L3 session | pre-arranged agent session, human present | human present | full, PR opened in-session |
Constraints:
- L2 is bounded: it MUST apply only to
typoandevent-updateclasses from senders explicitly marked for it, and MAY be revoked per-sender at any time. structuralchanges are L1 always, for every sender including the owner’s own agent sessions (Constitution §4).- There is no auto-publish-with-no-human-involvement level. L3 keeps a human present by construction.
Why: the bottleneck failure mode (every typo waits on one human) must not be solved by quietly deleting the review — it is solved by moving the publisher’s review after publication for the lowest-risk classes, with the post-hoc nature recorded.
4. trust.yaml
trust: z.object({
allowlist: z.array(z.object({
address: z.string().email(), // the sender's verified address
name: z.string().optional(), // human-readable
classes: z.array(changeClass), // classes this sender may request
dialLevel: z.enum(["L1", "L2"]).default("L1"),
})),
nonce: z.object({
ttlDays: z.number().default(14),
}).optional(),
agents: z.array(z.object({ // agent principals for sessions
principal: z.string(),
dialLevel: z.enum(["L3"]),
})).optional(),
}).strict()
5. The audit record
Every merged PR records, in its body, a structured block:
requested-by: <allowlisted address or session principal>
classification: <class> / <targets>
intent: <nonce-approval date + thread link, or "session">
published-by: <publisher, or "L2-post-hoc: pending">
Git makes this immutable. A conformance check (§07) verifies the block is present on every merged content PR.
6. Revocation and incident path
- A sender’s trust may be reduced (L2 → L1) or removed by a one-line PR; the
dial change itself follows
structuralhandling but MAY be fast-tracked by the publisher when revoking. - Suspected defacement or spoofing: revoke, audit
git logfor the sender’s merged PRs, revert affected commits — a revert PR, never a hand-edit of production storage.