03 — dot-apr-approve Minimal Design (design-only)
03 — dot-apr-approve minimal design (DESIGN-ONLY · NOT BUILT · NOT DEPLOYED)
Labels: LOCAL_STAGING_NOT_SSOT · NOT_GOVERNED_RUNTIME · NOT_DEPLOYED · NOT_REGISTRY_PROOF.
Specification only. No script written to runtime; no DOT born/registered; no APR approved.
Purpose
Let one authenticated approver record one approve/reject decision for one APR into apr_approvals,
with identity binding, proposer-exclusion, required rationale, audit, and a quorum readback. It must not
auto-approve, not let the agent/operator impersonate a seat, not mint votes by root/SYNC_SECRET/owner
convenience, not flip APR status, not bypass quorum. It only records a decision by the authenticated
approver. It is not a general authorization framework.
CLI surface
dot-apr-approve --apr APR-0415 \
--decision approve|reject \
--approver <seat_identity> \
--approver-type human|ai_council \
--rationale "<scoped, non-empty text>"
# caller secret supplied out-of-band (e.g. env DOT_APR_SEAT_TOKEN), NEVER on argv, NEVER printed
Write path (critical — avoids the dot-c1-grant-issue defect)
apr_approvals has 0 directus_fields → there is no managed Directus /items/apr_approvals endpoint. The tool
must therefore write via the governed PG path (the same run_pg mechanism dot-apr-execute uses for
execute_patch_ops_code), behind the seat authority gate — never a Directus POST and never a raw manual
psql insert by a human/agent. Preferred shape: standalone governed CLI dot-apr-approve bound to the PG
write path under the caller's authenticated seat credential (not a free-text --approver flag alone).
Requirements → guards mapping (macro §2)
| # | requirement | guard |
|---|---|---|
| 1 | one APR at a time | G8 scope = exactly one --apr; no batch/wildcard |
| 2 | approve or reject only | G9 decision ∈ {approve,reject} else reject |
| 3 | real approver authenticated | G3 caller secret verified against seat substrate (file 04) |
| 4 | claimed == authenticated id | G3 authenticated_seat == --approver else reject (no impersonation) |
| 5 | type ∈ {human, ai_council} | G4 |
| 6 | proposer cannot self-approve | G5 (file 06) — read source_context.proposer + created_by + source + creator; NULL ⇒ fail-closed, not allow |
| 7 | duplicate blocked/idempotent | G6 UNIQUE(apr_id, approver) → ON CONFLICT no-op, never double-count |
| 8 | rationale required | G7 --rationale non-empty |
| 9 | reject blocks quorum | G9 — decision='reject' recorded; quorum_passed/trigger already block on any reject |
| 10 | audit evidence written | G10 one audit-bearing row via governed PG path; provenance captured |
| 11 | readback available | post-write prints quorum_passed(apr) + vote counts; tool does NOT flip status |
| 12 | no auto-approval | G11 — no timer, no implicit vote |
| 13 | no root/operator convenience approval | G11 — refuses if caller secret resolves to operator/root rather than a real seat |
Guards (fail-closed) G1–G11
G1 APR exists (by code) ............................. else reject
G2 APR status = pending ............................. else reject
G3 caller secret authenticates AND seat == --approver else reject (no impersonation; file 04)
G4 approver-type ∈ {human, ai_council} .............. else reject
G5 proposer-exclusion (file 06) .................... approver != proposer across all known proposer signals;
if proposer unknowable ⇒ require explicit override rationale + self-approval-risk flag, never silent pass
G6 duplicate vote UNIQUE(apr_id, approver) ......... ON CONFLICT → no-op/reject, never double-count
G7 --rationale required, non-empty ................. scoped to the one APR
G8 scope = exactly one APR .......................... no batch, no wildcard
G9 decision ∈ {approve, reject} .................... reject recorded; blocks quorum downstream
G10 write ONE audit-bearing row via governed PG path capture channel/actor/auth provenance (file 04 §audit)
G11 NEVER write as root/SYNC_SECRET/owner convenience no auto-approve, no agent-as-approver
post: print quorum_passed(apr) + counts; NEVER flip APR status (that stays a separate governed step)
Status flip stays separate. dot-apr-approve only adds votes; the pending→approved transition (which
re-fires fn_apr_quorum_check) is a distinct, separately-authorized governed step. This DOT never approves the
APR itself, and explicitly never executes it.
Schema note (no DDL needed for the vote itself)
apr_approvals(id, apr_id, approver, approver_type, decision, rationale, created_at) — the vote row needs no new
column. However there is no provenance/auth column today, so G10's "audit how/by whom" must be satisfied by an
adjacent governed audit row (e.g. a dot_apr_approve_audit table written in the same transaction) recording
seat, channel, auth-result, and rationale-hash — never the secret. Adding that audit table is part of the
substrate/lifecycle work (files 04/05), deferred under HOLD.
Explicit non-goals
No generic authorization framework · no arbitrary action_code · no hardcoded approver values · no PASS/seal on
invalid input · no write outside apr_approvals(+its audit) · no status flip · no execute · no W1→W9 · no grant
mint · no handler bind · no dot-c1-grant-issue register.