key-cape/workplans/KEY-WP-0032-principal-type-provenance-guard.md
tegwick 139994cfac chore(consistency): register KEY-WP-0032 and refresh records [auto]
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NV9oijZukGyGbRQGGKnK4P

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 713576@bnt-lap001
Assistant-Session: 384c511d-9bce-4cb8-a676-2aef6c0c8df6
2026-09-10 23:07:49 +02:00

3.1 KiB

id type title domain repo status owner topic_slug created updated state_hub_workstream_id
KEY-WP-0032 workplan Pin the single route by which a principal becomes human infotech key-cape finished claude principal-type-provenance-guard 2026-09-10 2026-09-10 150b892c-f112-5fb9-b0f0-8072a52b85d5

GH-DEC-2026-016 §5 applies A-16 to what makes a principal human: if human is reachable by two routes — asserted by the identity layer about the person, or supplied by a registration about the client they came through — the record must say which, and a human-in-the-loop control must not be discharged on the registration-supplied one. Refusing a service principal while accepting an unverified assertion of humanity moves the defect rather than closing it.

gate-house said explicitly this was not a request. It is built because the property it protects is true today and is the kind a plausible change erases quietly.

Pin the property, not the behaviour

id: KEY-WP-0032-T01
status: done
priority: medium
state_hub_task_id: "2f42a72e-6292-5c02-9223-835db17b0a63"

principal_type has exactly one route to human: a string literal on the authorization-code path, reached only after an upstream login resolved to a directory user. No configuration field can assert it — verified against source rather than assumed, and it is why the tenant provenance work has no counterpart here and why A-16 does not yet bite.

Asserting the behaviour would not have protected that. A test checking "a human token says human" passes just as happily when the value starts coming from a registration. So the guard parses the package and requires every assignment to principal_type to be a string literal, with the literal set exactly {human, service} — one route each. claims["principal_type"] = client.PrincipalType would look like a feature in review and fails the build instead, with a message saying that if it is deliberate the claim must carry its provenance the way tenant_source does.

Both assignment shapes are covered. The browser path assigns into a map; the service path uses a key-value pair inside a map literal. Checking only assignments found one of two routes and passed — a guard proving less than it claimed, caught because the expected literal set was pinned rather than merely counted.

Verified by mutation: replacing the literal with a function of the client registration fails it at the exact line, and restoring it passes.

What this deliberately does not do

id: KEY-WP-0032-T02
status: done
priority: medium
state_hub_task_id: "13199970-75d1-5c38-aa8c-27d6e38dcf65"

No principal_type_source claim was added. There is one route, so a provenance marker would encode a distinction that does not exist and invite consumers to branch on it — the opposite of the tenant case, where two routes existed and the claim could not say which had been taken.

The guard is the honest response to "not a request today": it makes the absence of a second route a checked fact rather than a remembered one, and hands the decision to whoever creates the second route, at the moment they create it.