informed-decision/history/20260909-initial-exploration/canonicalize.md
tegwick ee2cca579c Establish INTENT, Stage 1 GOAL, and founding workplan
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
2026-09-09 10:47:36 +02:00

184 lines
6.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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):
1. UTF-8.
2. No insignificant whitespace.
3. Object keys sorted by raw UTF-8 bytes (all keys in this spec are ASCII).
4. Arrays keep author order except where this spec sorts (`packet` by `item_id`, `highlights` by `id`, hats/scopes/identities by `id`, permission lists lexicographically).
5. Numbers are integers only. Timestamps stay strings (`YYYY-MM-DDTHH:MM:SSZ`).
6. Strings use RFC 8259 escaping; U+0000U+001F as `\u00xx`.
7. Drop keys whose value is `null`. Keep empty arrays.
8. 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
1. Shuffling object keys in the input JSON does not change either hash.
2. Adding or changing awareness fields on a combined object does not change `view_hash`.
3. Changing `binding.target.id` from `tenant:acme` to `tenant:beta` **does** change `view_hash` (that is a different login).
4. Changing only `proposed_hat` changes `awareness_hash` only.
5. Selecting `hat:finance-controller` after login emits `session.hat_selected` and does not rewrite `view_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.