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

6.1 KiB
Raw Blame History

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.