Skip to content

feat: signed approval of released documents (ADR-004) - #457

Merged
itercharles merged 6 commits into
mainfrom
feat/signed-approvals
Oct 23, 2026
Merged

itercharles merged 6 commits into
mainfrom
feat/signed-approvals

Conversation

@itercharles

@itercharles itercharles commented Oct 22, 2026 •

Copy link
Copy Markdown
Owner

Summary

Charter iteration 3, approval and accountability. The branch went through three designs; its history keeps them, and the ADR records why the first two were dropped. What it delivers is the last one.

ADR-004: Signed Approval of Released Documents (Proposed)

  • What the inventory found: approval lives only in GitHub; 95 of ContourLab's last 100 merged PRs had no standing approval; approvals carry no meaning; their identity is within an agent's reach.
  • During development: pull-request review only, under the team's rules. That includes the CCB accepting and closing CRs. Nothing waits on a signature, and today's gates are unchanged.
  • At release: a pipeline started on purpose (in ContourLab, release-baseline.yml, not a merge) writes a manifest and a change report. Each specification is signed by the role that owns its type, and the quality lead signs the release.
  • What is signed: the manifest's entry for each document, which lists the items and their content hashes. The rendered file is not signed, because rendering is not byte-stable; its hash is recorded alongside.
  • Evidence is kept for the life of the device. The ContourLab artifact kept for 90 days is not enough.
  • Dropped: per-item signatures checked at merge, and a CCB signature on each CR.

Implementation

  • Approvers list DHF/config/approvers.yaml (people, roles, public keys).

    • A change to it must be signed by someone already on the list.
    • The first list is a stated root of trust.
  • Roles: approval_roles (item type → role that signs its specification) and release_role in global.yaml.

  • build release --write also writes:

    • DHF/releases/<version>/manifest.yaml: per document, its items and hashes, plus the rendered file's hash, the DHF hash and the approvers-list hash;
    • changes.md: per document, items added, modified and removed since the previous release, with their CRs.

    REL items are left out of both, so a REL moving through its states after signing is not drift.

  • item approve --release V --key <private key>:

    • shows the signer the changes;
    • writes one record covering every document the signer's roles own, plus the release for release_role;
    • signs it with ssh-keygen -Y sign, never through ssh-agent.
  • Approval log: DHF/approvals/<scope>/<ULID>.yaml with .sig. Records are append-only, never loaded by the item store, and collision-free across branches.

  • verify approval checks that the log is sound on any PR: signatures, append-only, the approvers guard. It never asks for a signature.

  • verify approval --release V additionally requires a signature from each document's role over the document's current hash, and the quality lead's signature over the manifest. It also fails if the manifest no longer matches the DHF.

  • Off until configured: without an approvers list, existing projects see no change.

Not in this PR

  • Updating the docs that say "approval is not a command" (CLAUDE.md prose, interface.md, ai-security.md).
  • Durable storage of rendered documents in ContourLab's release pipeline.
  • ContourLab adoption.

Known limits (ADR open questions)

  • Presence is declared, not proven. OpenSSH's allowed_signers cannot require the user-presence flag. No test uses a real hardware key.
  • Removing an approver's key would invalidate the signatures it made in past releases. The list needs revoked keys.
  • Release order for changes.md compares the numbers in the version: 1.10.0 sorts after 1.9.0, and a pre-release suffix sorts above its release.

Validation

  • Full suite: 2082 passed, 9 skipped, 3 xfailed.
  • scripts/generate_interface.py run; CHANGELOG under Unreleased; no version bump.

Manual review

  • Implemented by Sonnet subagents, then reviewed. I fixed one issue myself: the REL was in dhf_hash, so approving the REL after signing read as drift. A test now covers it.

🤖 Generated with Claude Code

itercharles and others added 6 commits October 22, 2026 08:56
Iteration 3 found approval living only in GitHub, mostly bypassed in
practice, without meaning, and within an agent's reach through tokens.
Propose approving item versions by the role that owns their type, with
SSH-signed APPROVAL items in the DHF that are append-only; the CCB signs
CR status changes; code review stays with the platform. Approval blocks
only at merge and release, never within a CR. Includes the typical
workflow, alternatives and open questions (presence cannot be required
through allowed_signers; approvers who leave need key revocation).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add the APPROVAL item type, the approvers list (DHF/config/approvers.yaml)
and the approval_roles matrix in global.yaml. `item approve --key` writes
an APPROVAL item covering the pending items the signer's roles may
approve and signs its file with `ssh-keygen -Y sign`. `verify approval`
checks every signature, signer and claimed role against the list, that
each changed item (or every item with --all) has a current approval from
its role, that approvals are append-only, and that a change to the list
is approved by someone on it as it was at the ref. Off until the project
keeps an approvers list. Release integration is phase 2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A CR can need ten approvals and a large team closes many CRs a day.
Approvals as DHF items made every command load them all, and sequential
IDs collided across parallel branches. Record approvals instead in an
append-only log, DHF/approvals/<CR>/<ULID>.yaml, which the item store
never reads. Add an "At scale" section for phase 2: verify only the
candidate records, and make each release a signed checkpoint after which
superseded records may leave the working tree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Remove the APPROVAL item type. `item approve` writes the record to
DHF/approvals/<CR or unscoped>/<ULID>.yaml with plain YAML and signs its
bytes; `verify approval` reads the log, reports a record whose id
differs from its file name, and checks append-only on that directory.
The checks themselves are unchanged. New tests: approvals made on two
branches from one base merge without colliding, a record lands under its
CR, and item list does not show approvals.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Working through a team's day showed per-item signatures cost too much:
ten per CR, re-signing shared items after every merge, development
waiting at each merge. The requirement is approval before issue. During
development every change, CR acceptance and closure included, is reviewed
in its pull request. At release, a pipeline started on purpose writes a
manifest of the documents' item hashes and a change report; each
specification is signed by the role that owns its type, and the quality
lead signs the release. Evidence is kept for the life of the device.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Remove per-item approval at merge (coverage in `verify approval`, and the
--cr/--all/--role modes of `item approve`). `build release --write` now
writes DHF/releases/<version>/manifest.yaml (each document's items and
hashes, the rendered file's hash, the DHF and approvers-list hashes) and
changes.md (what changed per document since the previous release, with
its CRs). `item approve --release V` signs, in one record, every document
the signer's roles own, and the release for `release_role`. `verify
approval --release V` requires each signature over the current hashes and
that the manifest still matches the DHF; REL items are left out, so a REL
moving on after signing is not drift. Plain `verify approval` only checks
the log is sound.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@itercharles itercharles changed the title feat: signed approvals of DHF content (ADR-004, phase 1) feat: signed approval of released documents (ADR-004) Oct 23, 2026
@itercharles
itercharles merged commit 1234c51 into main Oct 23, 2026
11 checks passed
@itercharles
itercharles deleted the feat/signed-approvals branch October 23, 2026 17:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant