key-cape/workplans/KEY-WP-0032-principal-type-provenance-guard.md
tegwick f247d3529d Close T04 and pin the single route by which a principal becomes human
KEY-WP-0014-T04 sat `wait` with nothing outstanding, which is a contradiction.
Its title is admit rotation and verify consumer handoff, and both are delivered:
the semantics are published, the executor, authority and transport are named, the
CCR will be prepared on request, and step 4 shipped as keycape verify-client;
ops-warden took option (a) for the reason given, with both lanes owner-confirmed
and no route changed. Executing a rotation was never its deliverable, so treating
the owner's deferral as an open item would have kept the task open against work
it was not scoped to do. The deferral stands separately, revisited on evidence.

GH-DEC-2026-016 §5 applies A-16 to what makes a principal human. Verified against
source that `human` has exactly one route here -- a literal on the
authorization-code path after an upstream login resolved to a directory user,
with no configuration able to assert it -- so A-16 does not yet bite and no
provenance claim is warranted. Adding one would encode a distinction that does
not exist.

Asserting the behaviour would not protect that: a test checking a human token
says human passes just as happily when the value starts coming from a
registration. The guard parses the package and requires every principal_type
assignment to be a string literal, the set being exactly human and service. It
covers both shapes -- the browser path assigns into a map, the service path uses
a key-value pair in a map literal -- and checking only assignments found one of
two routes and passed, which the pinned literal set caught. Verified by mutation.

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:05:16 +02:00

2.9 KiB

id type title domain repo status owner topic_slug created updated
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

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

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

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.