Skip to content

porch: gate records store approved_at but not approved_by — an approval cannot be attested after the fact #1457

Description

@amrmelsayed

porch approve records when a gate was approved but never who approved it, so an approval cannot be attested after the fact by anyone who did not witness it.

A gate record today:

gates:
  plan-approval:
    status: approved
    requested_at: '2026-08-14T02:22:53.475Z'
    approved_at: '2026-08-14T06:45:39.351Z'

That is the complete record. The accompanying chore(porch): <id> plan-approval gate-approved commit adds nothing, because every agent in the fleet commits under the same GitHub identity — author is not evidence here.

Why this is not a bookkeeping nicety

The --a-human-explicitly-approved-this flag exists to keep agents from approving their own gates. But the flag leaves no trace of the claim it asserts, so the invariant it protects is unverifiable one minute later. Three separate incidents in a single week all reduce to this gap:

  1. Circular corroboration (PR SD+ Automatic diff press opens the builder's first file diff (dial-ready) (#1414) #1429). A merge authorization relayed through one channel was "corroborated" by a porch record minted downstream of that same relay. It took a conversation to establish that the record proved nothing, because nothing in the record said where it came from.
  2. Builder-run approvals (Porch: gate-approval relay semantics are ambiguous — two builders read the same architect relay oppositely #1393). The architect role doc says the architect relays and the builder runs the command; the porch skill says builders must never run it. Both are followed in practice, and the record cannot distinguish the two — which is most of why the contradiction survived so long.
  3. An unattributable plan gate (pir-1070, 2026-08-14). A gate flipped with no witness in the owning architect's channel. Every artifact was checked — status.yaml, the commit, the builder thread — and none could say who did it. The lane correctly stopped rather than build on an unattestable chain, which is the right behavior and also pure waste when the answer could simply have been recorded.

Proposal

Record the actor and the basis alongside the timestamp:

  plan-approval:
    status: approved
    approved_at: '2026-08-14T06:45:39.351Z'
    approved_by: 'architect:vscode'        # who ran the command
    approval_basis: 'relay'                # 'direct' | 'relay'
    approval_note: "Amr in vscode channel: 'approve 1070'"

Minimum viable version is approved_by alone — the agent identity is already known to the process (afx whoami resolves it), so it can be captured without asking anyone to type anything. approval_basis and approval_note are the part that makes a relay auditable, and both can be optional flags that default to unset rather than blocking the common path.

Worth deciding alongside:

  • whether an unset approved_by on an existing record should be treated as legacy-unknown (yes, for the ~200 projects already on disk);
  • whether porch status should surface the actor, so the question can be answered without reading YAML;
  • whether the same fields belong on the pr gate, where the answer currently gates an admin bypass of branch protection and is therefore the highest-stakes case.

Not urgent in the sense that nothing is broken; urgent in the sense that every recurrence costs a lane its momentum and produces an argument instead of an answer.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area/porchArea: Porch state machine / protocol orchestration

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions