Claim ownership of the browser-facing approver UI that approval-engine deliberately does not contain. approval-engine's INTENT names an approvals inbox under Non-Goals, and docs/keycape-service-registrations.md records that the human approver client's client_id and callback URI "must come from its owner once it exists" — leaving key-cape's KEY-WP-0013-T02 blocked on an unassigned component. - INTENT.md: Decision Memo concept, the binding/awareness split and the two hashes, ownership and non-ownership against the named estate repositories, and a provisional PEP-shaped layer placement flagged for a gate-house ruling rather than asserted. - GOAL.md: Stage 1 is the L3 approval approver surface — the narrowest real consumer with a live blocking dependency — plus the written answer to who owns the approver UI. - workplans/INFD-WP-0001: founding documents, the gate-house layer/ownership ruling, the four specs (PRD, UseCaseCatalog, ArchitectureBlueprint, EvidenceModel), schema and canonicalizer promotion out of history/ with the isolation vectors under test, the key-cape client registration, and a walking skeleton that includes return and discuss. history/ is preserved unmodified as provenance. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V3W1dQG7GFFM9d94jFx7iR Assistant: claude-code Assistant-Model: opus Assistant-Process: 1565372@bnt-lap001 Assistant-Session: 16bb2f25-b34c-49ef-8e94-5fec3567a568
6.1 KiB
Canonicalization algorithm v0.2
Two hashes exist on every presentation.
| Hash | Input | Enters the signature? |
|---|---|---|
view_hash |
binding document | yes, for every binding verb at organizational and above |
awareness_hash |
awareness document | no, unless a promote disposition copied named fields into awareness_promoted |
That split is the whole point of pre-sign vs post-sign.
Reference implementation: canonicalize.py.
1. Profile
Restricted JCS (RFC 8785 subset):
- UTF-8.
- No insignificant whitespace.
- Object keys sorted by raw UTF-8 bytes (all keys in this spec are ASCII).
- Arrays keep author order except where this spec sorts (
packetbyitem_id,highlightsbyid, hats/scopes/identities byid, permission lists lexicographically). - Numbers are integers only. Timestamps stay strings (
YYYY-MM-DDTHH:MM:SSZ). - Strings use RFC 8259 escaping; U+0000–U+001F as
\u00xx. - Drop keys whose value is
null. Keep empty arrays. - Unknown keys are stripped. Adding a field to the live memo does not change a hash until it is added to the allow-list below.
2. Binding document (view_hash)
Allow-list, then normalize:
memo_id
memo_version
question
requested_act
binding_level
brief
locale
ui_release
packet[] → {item_id, hash} sorted by item_id
highlights[] → {id, item_id, locator, required_ack, severity} sorted by id
binding → BindingSlice (pre-sign identity + scope)
awareness_promoted → only fields explicitly promoted into the bind
binding allow-list:
principal
available_identities[] sorted by id
target the scope this act enters
available_bind_scopes[] scopes choosable as *this* bind, sorted by id
granted_at_bind roles/permissions sorted
justification
blast_radius
terms
Hats, last-used role, other-tenant orientation, situation notes do not appear here.
hash form is always alg:hex (sha256:…).
Then:
canonical = dumps(binding_document)
view_hash = SHA-256(UTF-8(canonical))
dumps is the JCS-subset serializer in canonicalize.py.
3. Awareness document (awareness_hash)
Allow-list:
memo_id
memo_version
locale
ui_release
proposed_hat
proposed_hat_source system_default | last_used | policy | explicit | inferred
available_hats[] sorted by id
available_scopes[] sorted by id (orientation only)
last_session
situation_note
Same dumps + SHA-256.
4. Dynamic linking
A binding disposition stores both hashes on the presentation and signs only view_hash.
Signed attributes (AES/QES) MUST contain at least:
memo_id
memo_version
disposition.verb
presentation.view_hash
They MUST NOT contain awareness_hash unless promote ran.
5. Promote
promote copies named awareness fields into binding.awareness_promoted on a new memo version. Typical case: an elevating hat (hat.elevates=true) must become part of the bind, or a tenant that looked like a hat is actually a bind-scope.
After promote:
- new
view_hash - previous presentations invalid
- route restarts or current step is re-presented
6. Test vectors
Computed with canonicalize.py.
V1 — login binding (identity + tenant)
Input: vectors/login-binding.json
view_hash.hex =
492d9d311bf44ec9de0d0abef28abac7d31df2781527e8fe276a3186ee1b06b8
What is inside: chosen principal, break-glass identity as an available bind identity, target tenant ACME, sibling tenant Beta as an available bind-scope, gate privileges authenticated + session.create, monitoring terms.
What is not inside: Finance Controller hat, last session, Payroll Admin.
V2 — login awareness (hats + last session)
Input: vectors/login-awareness.json
awareness_hash.hex =
2be7742970a01e7a879ae5040660659fb8c9a5c024e6c7fb338f3944a4fe05d1
Proposed hat hat:finance-controller source last_used. Payroll Admin is listed with elevates=true so the UI can show it and the state machine can refuse configure on it.
V3 — ADR accept
Input: vectors/adr-binding.json
view_hash.hex =
1c89ec07c3cc9d16f85a1ba1be5169456b3c55d21161f64037787779ae91f202
Isolation checks the implementation must keep green
- Shuffling object keys in the input JSON does not change either hash.
- Adding or changing awareness fields on a combined object does not change
view_hash. - Changing
binding.target.idfromtenant:acmetotenant:betadoes changeview_hash(that is a different login). - Changing only
proposed_hatchangesawareness_hashonly. - Selecting
hat:finance-controllerafter login emitssession.hat_selectedand does not rewriteview_hash.
7. UI mapping (login)
┌─────────────────────────────────────────────────────────┐
│ PRE-SIGN (editable, bound) │
│ Identity [ Bernd Worsch ▾ ] │
│ Scope [ ACME Corp / Payroll-Prod ▾ ] │
│ At gate authenticated · session.create │
│ Terms sessions recorded │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ AWARENESS (shown, not signed) │
│ After login you will wear │
│ Finance Controller (last used, Tue 18:12) │
│ other hats: Auditor · Payroll Admin (needs new bind) │
│ Other tenants you support: Beta GmbH (needs new bind) │
└─────────────────────────────────────────────────────────┘
[ Switch identity ] [ Change tenant ] [ Proceed ]
Proceed signs V1. After session start, hat dropdown is configure. Tenant dropdown to Beta opens a child memo.