Record the verified principal type on approver entries (schema v4)

An entry stored subject_id, assurance and evidence_ref but nothing about
what kind of principal bound the approval, and subject_id is a naming
convention rather than a verified claim. /entries is not restricted by
principal type — only /consume is — and the approval-engine-operator
client holds approval:approve, so a service can supply approver evidence
today. Whether it may is gate-house doctrine; that it is legible is ours.

Add entries.principal_type, populate it from the verified token, surface
it on the object and the audit evidence path (not the claim, which stays
least-disclosure), and migrate v3 stores leaving legacy rows null rather
than back-filling a claim nobody made.

Also corrects two statements in the requirements issued to
informed-decision: agent tokens are not barred from approval:approve, and
an empty assurance object is accepted rather than refused.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HyybaE7DUXrWYrhbnESCTe

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 1275879@bnt-lap001
Assistant-Session: eb464208-f821-41b2-bc5a-a6c33d92a8ad
This commit is contained in:
tegwick 2026-09-09 14:09:54 +02:00
parent 8b8ada6c4a
commit 31da1af5e4
6 changed files with 160 additions and 20 deletions

View file

@ -145,6 +145,7 @@ class App:
identity.subject,
assurance=json.dumps(identity.assurance, sort_keys=True),
evidence_ref=identity.evidence_ref,
principal_type=identity.principal_type,
)
return 200, obj.as_dict()
if rest == ["revoke"] and method == "POST":

View file

@ -35,7 +35,7 @@ AUDIT_SCHEMA = "audit-core.event.v1alpha1"
SOURCE = "approval-engine"
SCOPE = "netkingdom-approvals"
EVENT_CLASSES = ("issuance", "use", "supersession", "revocation", "heartbeat")
LATEST_SCHEMA_VERSION = 3
LATEST_SCHEMA_VERSION = 4
SCHEMA = """
CREATE TABLE IF NOT EXISTS approvals (
@ -66,6 +66,7 @@ CREATE TABLE IF NOT EXISTS entries (
approved_at TEXT NOT NULL,
assurance TEXT,
evidence_ref TEXT,
principal_type TEXT,
PRIMARY KEY (approval_id, subject_id)
);
CREATE TABLE IF NOT EXISTS outbox (
@ -230,6 +231,15 @@ class Engine:
conn.execute(
"ALTER TABLE approvals ADD COLUMN pdp_path INTEGER NOT NULL DEFAULT 0"
)
entry_columns = {
row["name"] for row in conn.execute("PRAGMA table_info(entries)").fetchall()
}
if "principal_type" not in entry_columns:
# v4. Legacy entries stay NULL. An entry recorded before this
# column existed carries no verified statement about what kind
# of principal bound it, and defaulting it to 'human' would
# manufacture approver evidence that was never presented.
conn.execute("ALTER TABLE entries ADD COLUMN principal_type TEXT")
outbox_columns = {
row["name"] for row in conn.execute("PRAGMA table_info(outbox)").fetchall()
}
@ -423,10 +433,11 @@ class Engine:
"approved_at": e["approved_at"],
"assurance": e["assurance"],
"evidence_ref": e["evidence_ref"],
"principal_type": e["principal_type"],
}
for e in self._conn()
.execute(
"SELECT subject_id, approved_at, assurance, evidence_ref "
"SELECT subject_id, approved_at, assurance, evidence_ref, principal_type "
"FROM entries WHERE approval_id=? ORDER BY approved_at",
(row["id"],),
)
@ -485,6 +496,7 @@ class Engine:
*,
assurance: str | None = None,
evidence_ref: str | None = None,
principal_type: str | None = None,
) -> Approval:
if not subject_id:
raise Unprocessable("subject_id is required")
@ -501,9 +513,10 @@ class Engine:
raise Conflict(f"cannot add entries in status {row['status']}")
try:
conn.execute(
"INSERT INTO entries (approval_id, subject_id, approved_at, assurance, evidence_ref) "
"VALUES (?,?,?,?,?)",
(approval_id, subject_id, now, assurance, evidence_ref),
"INSERT INTO entries "
"(approval_id, subject_id, approved_at, assurance, evidence_ref, principal_type) "
"VALUES (?,?,?,?,?,?)",
(approval_id, subject_id, now, assurance, evidence_ref, principal_type),
)
except sqlite3.IntegrityError as exc:
conn.rollback()
@ -767,7 +780,7 @@ class Engine:
a future schema that relaxes it.
"""
rows = conn.execute(
"SELECT subject_id, approved_at, assurance, evidence_ref "
"SELECT subject_id, approved_at, assurance, evidence_ref, principal_type "
"FROM entries WHERE approval_id=? ORDER BY approved_at, subject_id",
(approval_id,),
).fetchall()
@ -780,6 +793,7 @@ class Engine:
"approved_at": r["approved_at"],
**({"assurance": r["assurance"]} if r["assurance"] else {}),
**({"evidence_ref": r["evidence_ref"]} if r["evidence_ref"] else {}),
**({"principal_type": r["principal_type"]} if r["principal_type"] else {}),
}
for r in seen.values()
]