Enforce declared human controls at approval binding
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a07ff8-19d0-7820-b4d0-1353833cb7fc
This commit is contained in:
parent
a3c94fb241
commit
be1a388a84
17 changed files with 441 additions and 40 deletions
|
|
@ -22,6 +22,7 @@ from .binding import binding_digest, canonical_binding, require_digest
|
|||
from .errors import (
|
||||
Conflict,
|
||||
DuplicateApprover,
|
||||
Forbidden,
|
||||
NotFound,
|
||||
StoreUnavailable,
|
||||
Unprocessable,
|
||||
|
|
@ -35,7 +36,7 @@ AUDIT_SCHEMA = "audit-core.event.v1alpha1"
|
|||
SOURCE = "approval-engine"
|
||||
SCOPE = "netkingdom-approvals"
|
||||
EVENT_CLASSES = ("issuance", "use", "supersession", "revocation", "heartbeat")
|
||||
LATEST_SCHEMA_VERSION = 4
|
||||
LATEST_SCHEMA_VERSION = 5
|
||||
|
||||
SCHEMA = """
|
||||
CREATE TABLE IF NOT EXISTS approvals (
|
||||
|
|
@ -57,6 +58,7 @@ CREATE TABLE IF NOT EXISTS approvals (
|
|||
consumed_decision_id TEXT,
|
||||
consumed_at TEXT,
|
||||
pdp_path INTEGER NOT NULL DEFAULT 0,
|
||||
human_control INTEGER NOT NULL DEFAULT 0 CHECK (human_control IN (0, 1)),
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
|
|
@ -112,6 +114,7 @@ class Approval:
|
|||
binding_digest: str
|
||||
pdp_digest: str | None
|
||||
pdp_path: bool
|
||||
human_control: bool
|
||||
actor: str
|
||||
principal: str
|
||||
action: str
|
||||
|
|
@ -137,6 +140,7 @@ class Approval:
|
|||
"digest": self.binding_digest,
|
||||
"pdp_digest": self.pdp_digest,
|
||||
"pdp_path": self.pdp_path,
|
||||
"human_control": self.human_control,
|
||||
},
|
||||
"validity": {"not_before": self.not_before, "expires_at": self.expires_at},
|
||||
"required_count": self.required_count,
|
||||
|
|
@ -231,6 +235,14 @@ class Engine:
|
|||
conn.execute(
|
||||
"ALTER TABLE approvals ADD COLUMN pdp_path INTEGER NOT NULL DEFAULT 0"
|
||||
)
|
||||
if "human_control" not in approval_columns:
|
||||
# v5, GH-DEC-2026-016. Existing human entries are evidence of
|
||||
# their authors, not a declaration that the object discharged
|
||||
# a human control. Never infer that declaration on migration.
|
||||
conn.execute(
|
||||
"ALTER TABLE approvals ADD COLUMN human_control "
|
||||
"INTEGER NOT NULL DEFAULT 0 CHECK (human_control IN (0, 1))"
|
||||
)
|
||||
entry_columns = {
|
||||
row["name"] for row in conn.execute("PRAGMA table_info(entries)").fetchall()
|
||||
}
|
||||
|
|
@ -348,12 +360,15 @@ class Engine:
|
|||
*,
|
||||
pdp_digest: str | None = None,
|
||||
pdp_path: bool = False,
|
||||
human_control: bool = False,
|
||||
approval_id: str | None = None,
|
||||
) -> Approval:
|
||||
if required_count < 1:
|
||||
raise Unprocessable("required_count must be >= 1")
|
||||
if not isinstance(pdp_path, bool):
|
||||
raise Unprocessable("pdp_path must be a boolean")
|
||||
if not isinstance(human_control, bool):
|
||||
raise Unprocessable("human_control must be a boolean")
|
||||
canon = canonical_binding(binding)
|
||||
digest = binding_digest(canon)
|
||||
pdp = require_digest(pdp_digest)
|
||||
|
|
@ -382,8 +397,8 @@ class Engine:
|
|||
id, status, binding_json, binding_digest, pdp_digest,
|
||||
actor, principal, action, purpose, target_json,
|
||||
not_before, expires_at, required_count, superseded_by,
|
||||
pdp_path, created_at, updated_at
|
||||
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
|
||||
pdp_path, human_control, created_at, updated_at
|
||||
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
|
||||
(
|
||||
aid,
|
||||
"requested",
|
||||
|
|
@ -400,6 +415,7 @@ class Engine:
|
|||
required_count,
|
||||
None,
|
||||
1 if pdp_path else 0,
|
||||
1 if human_control else 0,
|
||||
now,
|
||||
now,
|
||||
),
|
||||
|
|
@ -456,6 +472,7 @@ class Engine:
|
|||
binding_digest=row["binding_digest"],
|
||||
pdp_digest=row["pdp_digest"],
|
||||
pdp_path=bool(row["pdp_path"]),
|
||||
human_control=bool(row["human_control"]),
|
||||
actor=row["actor"],
|
||||
principal=row["principal"],
|
||||
action=row["action"],
|
||||
|
|
@ -511,6 +528,10 @@ class Engine:
|
|||
if row["status"] not in MUTABLE:
|
||||
conn.rollback()
|
||||
raise Conflict(f"cannot add entries in status {row['status']}")
|
||||
if row["human_control"] and principal_type != "human":
|
||||
# This is the bind boundary. A service may draft a requested
|
||||
# approval, but cannot contribute evidence for human judgment.
|
||||
raise Forbidden("human_control requires a verified human approver")
|
||||
try:
|
||||
conn.execute(
|
||||
"INSERT INTO entries "
|
||||
|
|
@ -582,7 +603,11 @@ class Engine:
|
|||
if cur.rowcount != 1:
|
||||
conn.rollback()
|
||||
raise Conflict("supersession lost the compare-and-swap")
|
||||
existing = conn.execute("SELECT id FROM approvals WHERE id=?", (successor_id,)).fetchone()
|
||||
existing = conn.execute(
|
||||
"SELECT id, human_control FROM approvals WHERE id=?", (successor_id,)
|
||||
).fetchone()
|
||||
if existing is not None and existing["human_control"] != row["human_control"]:
|
||||
raise Conflict("successor must inherit the human_control declaration")
|
||||
created_successor = False
|
||||
if existing is None:
|
||||
conn.execute(
|
||||
|
|
@ -590,8 +615,8 @@ class Engine:
|
|||
id, status, binding_json, binding_digest, pdp_digest,
|
||||
actor, principal, action, purpose, target_json,
|
||||
not_before, expires_at, required_count, superseded_by,
|
||||
pdp_path, created_at, updated_at
|
||||
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
|
||||
pdp_path, human_control, created_at, updated_at
|
||||
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
|
||||
(
|
||||
successor_id,
|
||||
"requested",
|
||||
|
|
@ -608,6 +633,7 @@ class Engine:
|
|||
row["required_count"],
|
||||
None,
|
||||
row["pdp_path"],
|
||||
row["human_control"],
|
||||
now,
|
||||
now,
|
||||
),
|
||||
|
|
@ -720,6 +746,14 @@ class Engine:
|
|||
if self.now() < parse_iso(row["not_before"]) or self.now() >= parse_iso(row["expires_at"]):
|
||||
conn.rollback()
|
||||
raise Conflict("cannot consume outside validity window")
|
||||
if row["human_control"]:
|
||||
counts = conn.execute(
|
||||
"SELECT COUNT(*) AS total, "
|
||||
"SUM(CASE WHEN principal_type='human' THEN 1 ELSE 0 END) AS humans "
|
||||
"FROM entries WHERE approval_id=?", (approval_id,)
|
||||
).fetchone()
|
||||
if counts["total"] < row["required_count"] or counts["humans"] != counts["total"]:
|
||||
raise Conflict("cannot consume without the required human evidence")
|
||||
cur = conn.execute(
|
||||
"UPDATE approvals SET status='consumed', consumed_digest=?, "
|
||||
"consumed_decision_id=?, consumed_at=?, updated_at=? "
|
||||
|
|
@ -823,6 +857,11 @@ class Engine:
|
|||
details = {"class": event_class, **(extra or {})}
|
||||
if approval_id:
|
||||
details["approval_id"] = approval_id
|
||||
row = conn.execute(
|
||||
"SELECT human_control FROM approvals WHERE id=?", (approval_id,)
|
||||
).fetchone()
|
||||
if row is not None:
|
||||
details["human_control"] = bool(row["human_control"])
|
||||
payload = {
|
||||
"schema_version": AUDIT_SCHEMA,
|
||||
"event_id": event_id,
|
||||
|
|
@ -861,6 +900,7 @@ class Engine:
|
|||
# for the PDP path, so a consumer does not infer it from a digest that
|
||||
# happens to be present.
|
||||
binding["pdp_path"] = obj.pdp_path
|
||||
binding["human_control"] = obj.human_control
|
||||
return {
|
||||
"schema_version": CLAIM_SCHEMA,
|
||||
"kind": "approval-claim",
|
||||
|
|
@ -895,6 +935,8 @@ class Engine:
|
|||
if obj.status == "approved":
|
||||
if now < parse_iso(obj.not_before):
|
||||
return "approved", False, False, "not_yet_valid"
|
||||
if obj.human_control and any(e["principal_type"] != "human" for e in obj.entries):
|
||||
return "approved", False, False, "human_control_unsatisfied"
|
||||
if len({e["subject_id"] for e in obj.entries}) < obj.required_count:
|
||||
return "approved", False, False, "insufficient_approvers"
|
||||
return "valid", True, False, "ok"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue