APPROVAL-WP-0007-T02 done: actor_consumes; T05 principal_approves split out
Founder decision 2026-11-01, relayed by the net-kingdom coordinator: "Agree to split APPROVAL-WP-0007-T02: build actor_consumes now (opt-in at issue; the bounded executor will set it); principal_approves keeps waiting for the KeyCape subject mapping." - Optional actor_consumes (boolean) at issue, schema v8. Consume refuses with 403 unless the consuming subject equals binding.actor, also on idempotent retries; internal calls without a consumer fail closed. Inherited on supersession (existing-successor mismatch 409), in audit details, served in GET/claim only when declared so undeclared claims keep exact bytes. Legacy rows migrate without the flag. - GH-DEC-2026-026 Ruling 6: under human_control, add_entry refuses (403) the binding actor as approver. human_control already covered a service actor; this covers a human actor. - Workplan: T02 done (actor_consumes); new T05 principal_approves (wait). - Not released or deployed: needs a v8 image and founder-run rollout. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 5502@bnt-lap001 Assistant-Session: 68b3b02f-5d0f-4e71-8421-796f62e50196
This commit is contained in:
parent
e9e9f93317
commit
5c45d3c157
11 changed files with 344 additions and 36 deletions
|
|
@ -134,6 +134,7 @@ class App:
|
|||
pdp_path=bool(data.get("pdp_path", False)),
|
||||
human_control=data.get("human_control", False),
|
||||
required_assurance=data.get("required_assurance"),
|
||||
actor_consumes=data.get("actor_consumes", False),
|
||||
approval_id=data.get("id"),
|
||||
)
|
||||
return 201, obj.as_dict()
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ EVENT_CLASSES = ("issuance", "use", "supersession", "revocation", "heartbeat")
|
|||
# class and says nothing about the quiet one — here that is `revocation`, which
|
||||
# is the class whose silence actually matters.
|
||||
HEARTBEAT_CLASSES = ("issuance", "use", "supersession", "revocation")
|
||||
LATEST_SCHEMA_VERSION = 7
|
||||
LATEST_SCHEMA_VERSION = 8
|
||||
# APPROVAL-WP-0007-T01: authenticator assurance levels, weakest first. A level
|
||||
# outside this list is unknown, and an unknown level never meets a floor.
|
||||
ASSURANCE_LEVELS = ("aal1", "aal2", "aal3")
|
||||
|
|
@ -107,6 +107,7 @@ CREATE TABLE IF NOT EXISTS approvals (
|
|||
pdp_path INTEGER NOT NULL DEFAULT 0,
|
||||
human_control INTEGER NOT NULL DEFAULT 0 CHECK (human_control IN (0, 1)),
|
||||
required_assurance TEXT CHECK (required_assurance IS NULL OR required_assurance IN ('aal1', 'aal2', 'aal3')),
|
||||
actor_consumes INTEGER NOT NULL DEFAULT 0 CHECK (actor_consumes IN (0, 1)),
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
|
|
@ -165,6 +166,7 @@ class Approval:
|
|||
pdp_path: bool
|
||||
human_control: bool
|
||||
required_assurance: str | None
|
||||
actor_consumes: bool
|
||||
actor: str
|
||||
principal: str
|
||||
action: str
|
||||
|
|
@ -203,6 +205,9 @@ class Approval:
|
|||
# APPROVAL-WP-0007-T01. Stated only when declared, so undeclared
|
||||
# objects keep their exact pre-v7 shape for strict readers.
|
||||
result["binding"]["required_assurance"] = {"level": self.required_assurance}
|
||||
if self.actor_consumes:
|
||||
# APPROVAL-WP-0007-T02, same rule: stated only when declared.
|
||||
result["binding"]["actor_consumes"] = True
|
||||
if self.consumed_digest:
|
||||
result["consumption"] = {
|
||||
"request_digest": self.consumed_digest,
|
||||
|
|
@ -305,6 +310,13 @@ class Engine:
|
|||
"ALTER TABLE approvals ADD COLUMN required_assurance TEXT CHECK "
|
||||
"(required_assurance IS NULL OR required_assurance IN ('aal1', 'aal2', 'aal3'))"
|
||||
)
|
||||
if "actor_consumes" not in approval_columns:
|
||||
# v8, APPROVAL-WP-0007-T02. Legacy rows default to 0: nobody
|
||||
# declared at their issue that only the actor may spend them.
|
||||
conn.execute(
|
||||
"ALTER TABLE approvals ADD COLUMN actor_consumes "
|
||||
"INTEGER NOT NULL DEFAULT 0 CHECK (actor_consumes IN (0, 1))"
|
||||
)
|
||||
entry_columns = {
|
||||
row["name"] for row in conn.execute("PRAGMA table_info(entries)").fetchall()
|
||||
}
|
||||
|
|
@ -427,6 +439,7 @@ class Engine:
|
|||
pdp_path: bool = False,
|
||||
human_control: bool = False,
|
||||
required_assurance: dict[str, Any] | None = None,
|
||||
actor_consumes: bool = False,
|
||||
approval_id: str | None = None,
|
||||
) -> Approval:
|
||||
if required_count < 1:
|
||||
|
|
@ -435,6 +448,8 @@ class Engine:
|
|||
raise Unprocessable("pdp_path must be a boolean")
|
||||
if not isinstance(human_control, bool):
|
||||
raise Unprocessable("human_control must be a boolean")
|
||||
if not isinstance(actor_consumes, bool):
|
||||
raise Unprocessable("actor_consumes must be a boolean")
|
||||
floor = require_assurance_floor(required_assurance)
|
||||
canon = canonical_binding(binding)
|
||||
digest = binding_digest(canon)
|
||||
|
|
@ -464,8 +479,9 @@ 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, human_control, required_assurance, created_at, updated_at
|
||||
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
|
||||
pdp_path, human_control, required_assurance, actor_consumes,
|
||||
created_at, updated_at
|
||||
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
|
||||
(
|
||||
aid,
|
||||
"requested",
|
||||
|
|
@ -484,6 +500,7 @@ class Engine:
|
|||
1 if pdp_path else 0,
|
||||
1 if human_control else 0,
|
||||
floor,
|
||||
1 if actor_consumes else 0,
|
||||
now,
|
||||
now,
|
||||
),
|
||||
|
|
@ -543,6 +560,7 @@ class Engine:
|
|||
pdp_path=bool(row["pdp_path"]),
|
||||
human_control=bool(row["human_control"]),
|
||||
required_assurance=row["required_assurance"],
|
||||
actor_consumes=bool(row["actor_consumes"]),
|
||||
actor=row["actor"],
|
||||
principal=row["principal"],
|
||||
action=row["action"],
|
||||
|
|
@ -608,6 +626,12 @@ class Engine:
|
|||
# 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")
|
||||
if row["human_control"] and subject_id == row["actor"]:
|
||||
# GH-DEC-2026-026 Ruling 6: the actor is never an approver of
|
||||
# its own approval. The principal_type check above covers a
|
||||
# service actor; this covers a human actor, who may create a
|
||||
# human_control request but must not also approve it.
|
||||
raise Forbidden("human_control forbids the binding actor approving its own approval")
|
||||
if not entry_meets_floor(assurance, row["required_assurance"]):
|
||||
# The level comes only from the verified token (api.py); a
|
||||
# weaker or missing level cannot count toward this object.
|
||||
|
|
@ -718,13 +742,16 @@ class Engine:
|
|||
conn.rollback()
|
||||
raise Conflict("supersession lost the compare-and-swap")
|
||||
existing = conn.execute(
|
||||
"SELECT id, human_control, required_assurance FROM approvals WHERE id=?",
|
||||
"SELECT id, human_control, required_assurance, actor_consumes "
|
||||
"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")
|
||||
if existing is not None and existing["required_assurance"] != row["required_assurance"]:
|
||||
raise Conflict("successor must inherit the required_assurance declaration")
|
||||
if existing is not None and existing["actor_consumes"] != row["actor_consumes"]:
|
||||
raise Conflict("successor must inherit the actor_consumes declaration")
|
||||
created_successor = False
|
||||
if existing is None:
|
||||
conn.execute(
|
||||
|
|
@ -732,8 +759,9 @@ 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, human_control, required_assurance, created_at, updated_at
|
||||
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
|
||||
pdp_path, human_control, required_assurance, actor_consumes,
|
||||
created_at, updated_at
|
||||
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
|
||||
(
|
||||
successor_id,
|
||||
"requested",
|
||||
|
|
@ -752,6 +780,7 @@ class Engine:
|
|||
row["pdp_path"],
|
||||
row["human_control"],
|
||||
row["required_assurance"],
|
||||
row["actor_consumes"],
|
||||
now,
|
||||
now,
|
||||
),
|
||||
|
|
@ -843,6 +872,12 @@ class Engine:
|
|||
if row is None:
|
||||
conn.rollback()
|
||||
raise NotFound(approval_id)
|
||||
if row["actor_consumes"] and (consumer is None or consumer.subject != row["actor"]):
|
||||
# APPROVAL-WP-0007-T02. Checked before the idempotent branch so
|
||||
# another service cannot obtain a consumption receipt either.
|
||||
# No authenticated consumer (internal call) fails closed.
|
||||
conn.rollback()
|
||||
raise Forbidden("actor_consumes requires the consuming subject to equal binding.actor")
|
||||
if row["status"] == "consumed":
|
||||
if row["consumed_digest"] != digest:
|
||||
conn.rollback()
|
||||
|
|
@ -1004,13 +1039,15 @@ class Engine:
|
|||
if approval_id:
|
||||
details["approval_id"] = approval_id
|
||||
row = conn.execute(
|
||||
"SELECT human_control, required_assurance FROM approvals WHERE id=?",
|
||||
"SELECT human_control, required_assurance, actor_consumes FROM approvals WHERE id=?",
|
||||
(approval_id,),
|
||||
).fetchone()
|
||||
if row is not None:
|
||||
details["human_control"] = bool(row["human_control"])
|
||||
if row["required_assurance"]:
|
||||
details["required_assurance"] = {"level": row["required_assurance"]}
|
||||
if row["actor_consumes"]:
|
||||
details["actor_consumes"] = True
|
||||
payload = {
|
||||
"schema_version": AUDIT_SCHEMA,
|
||||
"event_id": event_id,
|
||||
|
|
@ -1061,6 +1098,10 @@ class Engine:
|
|||
# their exact pre-v7 bytes; absence means no floor was declared.
|
||||
if obj.required_assurance:
|
||||
binding["required_assurance"] = {"level": obj.required_assurance}
|
||||
# APPROVAL-WP-0007-T02: same rule. Absence means any tenant consumer
|
||||
# holding approval:consume may spend the object.
|
||||
if obj.actor_consumes:
|
||||
binding["actor_consumes"] = True
|
||||
return {
|
||||
"schema_version": CLAIM_SCHEMA,
|
||||
"kind": "approval-claim",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue