--- id: KEY-WP-0032 type: workplan title: "Pin the single route by which a principal becomes human" domain: infotech repo: key-cape status: finished owner: claude topic_slug: principal-type-provenance-guard created: "2026-09-10" updated: "2026-09-10" state_hub_workstream_id: "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 ```task 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 ```task 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.