Two rulings landed and both corrected something. GH-DEC-2026-013 accepted our binding-versus-awareness argument, wrote it into the record as its §6, and did not change the outcome — it sharpened the defect. Two different facts share one field named tenant: the act-scope, a property of the act that our binding slice commits, and the principal's membership, a property of the person that approval-engine exact-matches. Gate House's correction stands: a binding slice that must commit the scope being entered should commit that scope, not borrow a membership claim to stand in for it. Our schema already does — binding.target IS the act-scope and is inside view_hash — so no field was added, only a statement (PR-08) and a provenance record (PR-09), since key-cape emits tenant as a bare string. key-cape had already implemented registration-bound tenancy on 2026-09-09, correct under both candidate rulings, so the fail-closed-at-first-use risk that made us withhold the client strings was already retired. IN-0002 closed. The one remaining input to T07 is the deployed origin. GH-DEC-2026-014 granted commitment-only evidence and bounded it. It satisfies non-alteration and NOT reconstructability, and must not be described otherwise anywhere. It also corrected our wording of the gap: we wrote that it leaves us able to erase the content, which understates it. Commitment-only moves integrity out of our control and leaves availability entirely inside it — the party that can withhold the content is the party the evidence is about. Limit 3's condition reduced, not removed. The grant carries a condition we did not propose and would not have thought of: the path must assert that committed content exists and where custody sits, so non-production is a finding attributable to the custodian rather than an unremarkable blank. A commitment with no assertion that something is being committed to is indistinguishable from a commitment to nothing. Booked as PR-53, and marked not-a-reversal-candidate. Recorded the meta-rule Gate House named, now in its third setting here: unknown versus absent in the stance map, directory-asserted versus registration-supplied in the tenant claim, erased versus never held in the evidence path. Wherever a system reaches one appearance by two routes, the record must say which route. 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
343 lines
16 KiB
Markdown
343 lines
16 KiB
Markdown
# Evidence Model
|
|
|
|
**Repo:** informed-decision
|
|
**Workplan task:** `INFD-WP-0001-T06`
|
|
**Status:** draft, pending review
|
|
**Companions:** `ProductRequirementsDocument.md`, `UseCaseCatalog.md`,
|
|
`ArchitectureBlueprint.md` (gated on `INFD-WP-0001-T02`)
|
|
**Normative algorithm:** `history/20260909-initial-exploration/canonicalize.md`
|
|
**Governed implementation:** `informed_decision/canonicalize.py`
|
|
**Tests:** `tests/test_canonicalize.py`
|
|
|
|
---
|
|
|
|
## 1. What this model claims
|
|
|
|
> **This person, in this role, was shown this view, and bound this act.**
|
|
|
|
Not *"the file was signed"*. The difference is the whole product. A signature
|
|
over a document proves a key was used on some bytes; it does not establish what
|
|
the signer was looking at when they decided, which is the question an auditor
|
|
actually asks and the question no e-signature product answers.
|
|
|
|
The mechanism is **dynamic linking**: the human-readable content actually
|
|
presented is canonicalized and its digest is bound into the act.
|
|
|
|
## 2. What this model does not claim
|
|
|
|
Stated first, because an evidence model that oversells is worse than none.
|
|
|
|
**E-01 — It does not prove the person understood.** It proves what was
|
|
presented and what was explicitly acknowledged. Comprehension is not
|
|
observable and this model does not pretend to observe it.
|
|
|
|
**E-02 — It does not prove the person read anything.** Deliberately. Evidence of
|
|
informedness is the hash of the presented view plus explicit acknowledgment —
|
|
never dwell time, scroll depth, or attention telemetry. That choice trades a
|
|
weaker claim for the absence of surveillance, and the trade is intentional
|
|
(`INTENT.md` principle 12, `PR-70`/`PR-71`).
|
|
|
|
**E-03 — It does not survive a compromised surface.** `view_hash` is computed by
|
|
the same component that renders. A compromised surface can present X and attest
|
|
Y. This is structurally the same residual `approval-engine` names for
|
|
adversarial omission at a compromised source, and it is not closed here, not
|
|
closable at this layer, and never to be described as closed.
|
|
|
|
**E-04 — A hash chain proves records were not altered or truncated after
|
|
arrival. It cannot prove a record was never sent.** Carried over from
|
|
`approval-engine`'s reasoning rather than rediscovered: a suppressed event
|
|
leaves the chain perfectly intact and verification reports intact. Completeness
|
|
is a separate obligation met by cadence, not by chaining.
|
|
|
|
**E-05 — Stage 1 binding is organizational only.** No AES, no QES. The object
|
|
carries `binding_level` so higher levels are expressible without a schema
|
|
change; nothing above organizational is implemented or claimed.
|
|
|
|
**E-06 — A Stage 1 escalation is an assertion, not a verified claim.** Without
|
|
the mandate graph, guard `G_ESC` cannot be fully evaluated. The bundle records
|
|
what the approver asserted and marks it unverified.
|
|
|
|
## 3. The two hashes
|
|
|
|
| | `view_hash` | `awareness_hash` |
|
|
| --- | --- | --- |
|
|
| Over | the binding document | the awareness document |
|
|
| Signed | yes, from organizational level up | **no** |
|
|
| Purpose | what the person committed to | what they were oriented with |
|
|
| In AES/QES signed attributes | yes | no, unless promotion ran |
|
|
|
|
**Binding allow-list:** `memo_id`, `memo_version`, `question`, `requested_act`,
|
|
`binding_level`, `brief`, `locale`, `ui_release`, normalized `packet`,
|
|
normalized `highlights`, `binding`, and optionally `awareness_promoted`.
|
|
|
|
**Awareness allow-list:** proposed hat and its source, available hats and
|
|
scopes, last session, situation note.
|
|
|
|
Profile: JCS-inspired restricted JSON — UTF-8, keys sorted by code unit, no
|
|
insignificant whitespace, integers only, unknown keys stripped, nulls dropped.
|
|
|
|
```
|
|
canonical = dumps(document)
|
|
hash = SHA-256(UTF-8(canonical))
|
|
```
|
|
|
|
### Why the split exists
|
|
|
|
The login case forced it and it now applies everywhere. Defaulting a role to
|
|
last-used is **required** for situational awareness — an operator who cannot see
|
|
which hat they are about to wear is less safe, not more — and is **forbidden**
|
|
from silently entering the signed payload. A person must not be bound to
|
|
orientation material they did not commit to.
|
|
|
|
Promotion is the only bridge: moving a field from awareness into binding creates
|
|
a new memo version, invalidates outstanding presentations, and copies the named
|
|
fields into `awareness_promoted` where `view_hash` covers them (guard
|
|
`G_PROMOTE`).
|
|
|
|
## 4. The four isolation properties
|
|
|
|
These are the model. Each is a test in `tests/test_canonicalize.py` and each
|
|
must stay green; a failure means `view_hash` no longer means what §1 claims.
|
|
|
|
| # | Property | Protects | Test |
|
|
| --- | --- | --- | --- |
|
|
| 1 | Shuffling object keys does not change either hash | Any verifier, in any language, agrees with us | `test_key_order_does_not_change_*` |
|
|
| 2 | Editing an awareness field does not change `view_hash` | The person is bound only to what they committed to | `test_editing_awareness_does_not_change_view_hash` |
|
|
| 3 | Changing `binding.target` **does** change `view_hash` | Positive control — without it, 2 could pass vacuously | `test_changing_binding_target_changes_view_hash` |
|
|
| 4 | Selecting a hat after login does not rewrite `view_hash` | Post-bind session state is a different object | `test_post_bind_hat_selection_does_not_change_view_hash` |
|
|
|
|
Property 3 is not decorative. Properties 1, 2 and 4 are all *negative* — they
|
|
assert that things do **not** change the hash. A canonicalizer that returned a
|
|
constant would pass all three. Property 3, plus the per-field variants over
|
|
`question`, `requested_act`, `binding_level` and `packet`, is what makes the
|
|
suite meaningful.
|
|
|
|
**Published vectors** reproduce byte for byte and are asserted in CI:
|
|
|
|
```
|
|
login view_hash 492d9d31…1b06b8
|
|
login awareness_hash 2be77429…4fe05d1
|
|
ADR-0042 view_hash 1c89ec07…e91f202
|
|
```
|
|
|
|
**Provenance is tested.** `history/20260909-initial-exploration/` is never
|
|
edited. `test_governed_vectors_match_the_preserved_history_copy` asserts the
|
|
governed fixtures have not drifted from the founding copies, so a silent edit to
|
|
a vector to make a failing test pass is itself a test failure.
|
|
|
|
## 5. The presentation record
|
|
|
|
Every render creates one. It carries:
|
|
|
|
- `memo_id`, `memo_version`
|
|
- `principal` — who it was shown to
|
|
- `locale`, `ui_release` — both inside `view_hash`
|
|
- `rendered_at`
|
|
- `acked_highlight_ids`
|
|
- `view_hash`, `awareness_hash`
|
|
- `phase` — `pre_bind` | `bind` | `post_bind`
|
|
|
|
A disposition references exactly one presentation, of the same memo version
|
|
(guard `G_PRES`). A presentation taken at version *n* cannot bind after the memo
|
|
advances to *n+1* — no silent upgrade (`NC-07`).
|
|
|
|
Because `locale` is inside `view_hash`, the same memo read in German and in
|
|
English produces two different presentations of one binding. Intended, and
|
|
documented rather than smoothed over (`PR-61`).
|
|
|
|
## 6. The evidence bundle
|
|
|
|
An **export**, not a screenshot (`INTENT.md` principle 7). Contains the memo and
|
|
all its versions, every presentation with both hashes, every disposition, the
|
|
thread, the route as executed, and the append-only hash chain.
|
|
|
|
Requirements:
|
|
|
|
- **Verifies offline.** No network, no access to this service. A verifier
|
|
recomputes `view_hash` from the bundle's own binding document and compares.
|
|
- **Detects mutation.** Altering any covered field breaks verification.
|
|
- **States its own bound.** The residuals in §2 travel with the bundle as
|
|
metadata. An auditor must not have to read this document to learn that E-03
|
|
exists.
|
|
- **Marks unverified assertions as such** — see E-06.
|
|
|
|
## 7. Relationship to `audit-core`
|
|
|
|
`audit-core` holds the archive; this repository holds the operative record and
|
|
produces the export. This surface emits its transitions there and does not
|
|
duplicate the archive's role.
|
|
|
|
Its guarantee is bounded and the bound is inherited, not re-litigated:
|
|
`audit-core` is not WORM or object-locked, its tamper evidence is conditional on
|
|
live preconditions, and stronger custody was **decided against** in statute §16.
|
|
This repository must not plan around a stronger archive, and must not imply the
|
|
bundle is more durable than the archive it reconciles against.
|
|
|
|
## 8. Open — `view_hash` versus the approval binding digest
|
|
|
|
**Unresolved.** Filed as `INFD-IN-0001` R3 with `gate-house`.
|
|
|
|
`approval-engine`'s claim carries *"a digest over the same canonical binding the
|
|
decision point already computes."* `view_hash` is also a digest over "the
|
|
binding". They cover different material: the approval digest exists with no
|
|
human in the loop; `view_hash` additionally covers brief, packet, highlights,
|
|
locale and UI release.
|
|
|
|
Until ruled, this repository computes `view_hash` and **asserts no relationship**
|
|
between the two. Three candidate outcomes and their costs are set out in
|
|
`docs/gate-house-decision-request-layer-placement.md` §2 R3. The outcome to be
|
|
avoided is both digests shipping with no stated authority rule, leaving the
|
|
estate with two canonicalizations of one act.
|
|
|
|
## 8b. Where `view_hash` actually lives (Stage 1)
|
|
|
|
**It is not in the approval entry, by the engine's deliberate design.**
|
|
|
|
`POST /v1/approvals/{id}/entries` reads the request body and **discards it**.
|
|
Approver identity, `assurance` and `evidence_ref` come only from the verified
|
|
token (`evidence_ref` is `jwt-sha256:<digest of the presented token>`). The
|
|
reasoning is sound and we adopt it rather than argue it: a caller-supplied
|
|
approver field is provenance, not evidence, and that entry is currently the one
|
|
record containing no caller-supplied data.
|
|
|
|
Stage 1 arrangement, requiring no change to `approval-engine`:
|
|
|
|
1. this repository holds the presentation record and computes both hashes;
|
|
2. it emits that record to `audit-core` as its own evidence;
|
|
3. correlation to the approval entry is by **`(approval_id, subject,
|
|
approved_at)`** — all three returned by the engine after a successful entry.
|
|
|
|
**Consequence for an auditor, stated plainly:** someone holding only the
|
|
approval object cannot reach the presentation. They need this surface's record
|
|
or `audit-core` as well. `GOAL.md` DoD-3 — "the approval entry is
|
|
reconstructable from a `view_hash`" — is satisfied by the correlation triple,
|
|
not by a hash stored on the entry.
|
|
|
|
Putting the binding *inside* the approval object would be a real change request
|
|
against `approval-engine` (an optional, verified-alongside-token `view_hash` on
|
|
the entry) and it is doctrine before it is a change request, so it goes to
|
|
`gate-house` first. Not raised at Stage 1; recorded here so the option is not
|
|
lost.
|
|
|
|
### `assurance` is the only carrier of authentication facts
|
|
|
|
`approval-engine` persists the token's `assurance` claim verbatim and
|
|
interprets nothing — an empty `{}` is *accepted* and evidences nothing. It is
|
|
therefore the single place where "this was bound under MFA" survives into the
|
|
approval record. The shape must carry authentication method, `acr`/`amr` and
|
|
`auth_time` at minimum, and `informed-decision` and `key-cape` own it between
|
|
them. Nothing downstream reconstructs it.
|
|
|
|
### Principal type is verified evidence; `subject_id` is not
|
|
|
|
Schema v4 records `entries[].principal_type` from the verified token. Design
|
|
principle 10 (humans bind, agents draft) is enforceable from the evidence chain
|
|
by reading that field. Never infer it from the shape of `subject_id` — a naming
|
|
convention is not a verified claim. Entries written before v4 are `null` and
|
|
must not be read as `human`.
|
|
|
|
## 8c. Act-scope is not membership — `GH-DEC-2026-013` §5
|
|
|
|
Gate House made the sharpest observation anyone has made about this object
|
|
model, and it is worth carrying in full because it changes what we record even
|
|
though it changes no field.
|
|
|
|
Two different facts are being asked to travel in one claim named `tenant`:
|
|
|
|
| Fact | Property of | Who needs it |
|
|
| --- | --- | --- |
|
|
| Which scope is this act being entered into | the **act** | this repository's binding slice |
|
|
| Which tenant is this person a member of | the **principal** | `approval-engine`'s exact-match admission |
|
|
|
|
> *"A binding slice that must commit the scope being entered should commit THAT
|
|
> SCOPE, not borrow a membership claim to stand in for it."*
|
|
|
|
**Our schema already does the right thing.** `binding.target` is the act-scope:
|
|
the tenant, environment and system being entered, committed in the binding
|
|
document and therefore inside `view_hash`. It is not derived from, and must
|
|
never be replaced by, the token's `tenant` claim.
|
|
|
|
What was missing is not a field but a **statement and a record**:
|
|
|
|
- `binding.target` is the authoritative act-scope commitment. The `tenant` claim
|
|
is a membership fact about the principal and is **not** the act-scope.
|
|
- The two must never be collapsed. Where they agree, that agreement is itself a
|
|
fact worth recording; where they disagree, issuance is refused upstream
|
|
(`key-cape` `329e48f`) and this surface records the refusal rather than
|
|
choosing a winner.
|
|
- **The `tenant` claim's provenance is recorded.** `key-cape` emits `tenant` as
|
|
a bare string, so a consumer cannot tell a tenant the *directory asserted about
|
|
the person* from one a *registration supplied about the client they came
|
|
through*. `GH-DEC-2026-013` §5 requires the claim to carry its provenance;
|
|
until it does, this surface records which route the value arrived by rather
|
|
than storing an undifferentiated string.
|
|
|
|
This is the same rule as `unknown` versus `absent` in the stance map and
|
|
`erased` versus `never held` in §8d: **wherever a system reaches one appearance
|
|
by two routes, the record must say which route**, or the safer reading becomes
|
|
unavailable to everyone.
|
|
|
|
## 8d. What commitment-only does and does not establish — `GH-DEC-2026-014`
|
|
|
|
Commitment-only emission is **granted for Stage 1**. It is granted on the test
|
|
`GH-DEC-2026-013` set: its distinguishing case fails closed. A reviewer who
|
|
cannot obtain the content gets **no** reconstruction rather than a **wrong** one.
|
|
|
|
**It satisfies non-alteration. It does not satisfy reconstructability, and must
|
|
not be described as doing so** — not here, not in `audit-core`'s documents, and
|
|
not in any conformance claim.
|
|
|
|
A commitment-only record is weaker than an archive in a specific way. An archive
|
|
proves records were not altered or truncated after arrival, never that one was
|
|
never sent (§2, E-04). A commitment additionally **does not carry what it commits
|
|
to**. It establishes that the commitment was made when it says and has not
|
|
changed. About *what was committed to* it establishes nothing — except
|
|
conditionally: **if** a document is later produced, whether it is the one.
|
|
|
|
### The gap, in the right words
|
|
|
|
An earlier draft of ours said commitment-only "leaves us able to erase the
|
|
content". That understates where the problem sits, and Gate House corrected it:
|
|
|
|
> **Commitment-only moves *integrity* out of our control and leaves
|
|
> *availability* entirely inside it. The party that can withhold the content is
|
|
> the party the evidence is about.**
|
|
|
|
That is the condition limit 3 exists to prevent, **reduced and not removed**. The
|
|
reduction is real. It is not sufficient alone — which is why the grant carries
|
|
the condition below.
|
|
|
|
### The condition that makes the grant safe
|
|
|
|
**Non-production must be detectable as a finding, not present as an absence.**
|
|
|
|
If the independent path holds only a commitment, a reviewer who asks for content
|
|
and gets nothing cannot distinguish *erased*, *withheld*, *lost*, and *never
|
|
held*. The absence reads as an unremarkable blank.
|
|
|
|
So the evidence path must carry, alongside the commitment:
|
|
|
|
1. an assertion that committed content **exists**, and
|
|
2. **where custody sits**,
|
|
|
|
such that failure to produce at retrieval is a **conformance failure
|
|
attributable to the custodian**.
|
|
|
|
> *A commitment with no assertion that something is being committed to is
|
|
> indistinguishable from a commitment to nothing.*
|
|
|
|
This is **not a reversal candidate**. If it proves expensive, the answer is a
|
|
cheaper mechanism for the same property, never removal of the property.
|
|
|
|
### Residual, again
|
|
|
|
Not closed, not credited. The existence assertion narrows the erasure gap — a
|
|
detectable non-production tells a reviewer something is missing and who owed it.
|
|
It does not produce the missing thing.
|
|
|
|
## 9. Signed attributes (L4+, horizon)
|
|
|
|
When AES/QES arrives, the signed attributes carry `memo_id`, `memo_version`,
|
|
`disposition.verb` and `presentation.view_hash`. They do **not** carry
|
|
`awareness_hash` unless promotion ran. Recorded here so nothing built at Stage 1
|
|
forecloses it.
|