Carry threshold evidence on issuance and use events
GH-DEC-2026-005 moved the distinct-approver check off the PEP onto this engine's valid_now. secrets-engine has implemented the split and reports it no longer verifies the threshold independently. Gate House accepted that as correct on layering AND as a genuine reduction in defence in depth, and named the compensating control: not a second check at the PEP, which is the duplication the split removes, but reconstructability at the issuer under §9.6. The emitted events could not support that. approval.issuance carried required_count but never who satisfied it, and approval.use carried no threshold evidence at all, so an auditor replaying the stream could not recompute the evaluation without reading live rows -- rows that may since have been superseded, revoked, or expired. Both events now carry a threshold object: required_count, distinct_approver_count, threshold_met, and approvers with approved_at plus assurance and evidence_ref when recorded. Tests prove reconstruction from the use row alone, and that the claim still discloses no approver identities -- they are evidence for audit-core, not consumer-facing, and the claim keeps disclosing the least it can. Writing the tests showed distinctness is already a storage invariant: entries is UNIQUE on (approval_id, subject_id), so a repeat approver is refused at insert and a separate entry_count could never differ from the distinct count. Dropped that field rather than ship a number that cannot vary, and the test now asserts the refusal instead. 88 tests pass (4 new). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TvyJPAaVCGsVheVhcCwNND Assistant: claude-code Assistant-Model: opus Assistant-Process: 411227@bnt-lap001 Assistant-Session: d566f6d3-bcaf-43c3-bc5e-3ddd0f64b535
This commit is contained in:
parent
48ffc34993
commit
87e55e2bca
4 changed files with 163 additions and 2 deletions
|
|
@ -503,7 +503,13 @@ class Engine:
|
|||
"issuance",
|
||||
approval_id,
|
||||
actor=row["actor"],
|
||||
extra={"binding_digest": row["binding_digest"], "required_count": row["required_count"]},
|
||||
extra={
|
||||
"binding_digest": row["binding_digest"],
|
||||
"required_count": row["required_count"],
|
||||
"threshold": self._threshold_evidence(
|
||||
conn, approval_id, row["required_count"]
|
||||
),
|
||||
},
|
||||
)
|
||||
conn.commit()
|
||||
except sqlite3.Error as exc:
|
||||
|
|
@ -693,6 +699,9 @@ class Engine:
|
|||
"binding_digest": row["binding_digest"],
|
||||
"request_digest": digest,
|
||||
**({"decision_id": decision_id} if decision_id else {}),
|
||||
"threshold": self._threshold_evidence(
|
||||
conn, approval_id, row["required_count"]
|
||||
),
|
||||
},
|
||||
)
|
||||
conn.commit()
|
||||
|
|
@ -711,6 +720,50 @@ class Engine:
|
|||
"idempotent": idempotent,
|
||||
}
|
||||
|
||||
def _threshold_evidence(
|
||||
self, conn: sqlite3.Connection, approval_id: str, required_count: int
|
||||
) -> dict[str, Any]:
|
||||
"""Threshold evaluation as evidence, for reconstruction under §9.6.
|
||||
|
||||
`GH-DEC-2026-005` moved the distinct-approver check off the PEP and onto
|
||||
this engine's `valid_now`. The compensating control is detection, not
|
||||
prevention: the emitted evidence must let an auditor recompute the
|
||||
evaluation after the fact without reading live rows, which may since
|
||||
have been superseded or expired.
|
||||
|
||||
Approver identities belong here and not on the claim. The claim is
|
||||
consumer-facing and discloses the least it can; the outbox is the
|
||||
evidence path to audit-core, where the identities are the point.
|
||||
|
||||
Distinctness is a storage invariant, not a recomputation: `entries`
|
||||
has a UNIQUE constraint on (approval_id, subject_id), so a repeat
|
||||
approver is refused at insert. The dedup below is belt-and-braces for
|
||||
a future schema that relaxes it.
|
||||
"""
|
||||
rows = conn.execute(
|
||||
"SELECT subject_id, approved_at, assurance, evidence_ref "
|
||||
"FROM entries WHERE approval_id=? ORDER BY approved_at, subject_id",
|
||||
(approval_id,),
|
||||
).fetchall()
|
||||
seen: dict[str, sqlite3.Row] = {}
|
||||
for r in rows:
|
||||
seen.setdefault(r["subject_id"], r)
|
||||
approvers = [
|
||||
{
|
||||
"subject_id": r["subject_id"],
|
||||
"approved_at": r["approved_at"],
|
||||
**({"assurance": r["assurance"]} if r["assurance"] else {}),
|
||||
**({"evidence_ref": r["evidence_ref"]} if r["evidence_ref"] else {}),
|
||||
}
|
||||
for r in seen.values()
|
||||
]
|
||||
return {
|
||||
"required_count": required_count,
|
||||
"distinct_approver_count": len(approvers),
|
||||
"threshold_met": len(approvers) >= required_count,
|
||||
"approvers": approvers,
|
||||
}
|
||||
|
||||
def _outbox_insert(
|
||||
self,
|
||||
conn: sqlite3.Connection,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue