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
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.