Origin and evidence path both landed today. T07 origin: railiance-apps deployed decisions.coulomb.social and corrected the hostname in this repo — not the decide.coulomb.social this workplan proposed. Verified here rather than taken on report: both paths 200, TLS verify 0, Let's Encrypt cert valid to 2026-12-09. T08: audit-core registered the source with every field as proposed and landed the detection half. INFD-IN-0003 closed. Their refinements booked — reconciliation on the high-volume class too, since rate detects a stream stopping but never a stream missing the particular renders that mattered, which is exactly our threat model; and PR-12, the custody locator must be a stable non-secret identifier because redact scans data and an existence declaration arriving without its pointer looks complete while being useless. INFD-IN-0004 ruled as GH-DEC-2026-015: gate-house reversed itself and nesting is permitted for this pair. The decisive ground was not the cycle argument we led with — our binding slice canonicalizes principal and target, two of the five digest fields, so co-reference left us performing a partial recomputation of one act in a second vocabulary, closer to the translation R3 forbade than nesting is. Our ordering objection was withdrawn as mistaken. The permission is conditioned and NOT ACTIVE until approval-engine states its presentation exclusion as normative and tested. layer.yaml is deliberately unchanged and carries nesting_permission_active false — we do not activate on our own initiative. GH-DEC-2026-016 ruled NC-03. Its §5 is live rather than hypothetical and is booked as PR-11: principal_type: human is a property of the client registration, structurally the same shape as the gap-route tenant, so a human-in-the-loop control must not be discharged on it as verified humanity. T07 stays progress: the submission to key-cape is written but unsent, blocked by the local permission classifier rather than by any repository. 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
439 lines
21 KiB
Markdown
439 lines
21 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.
|
|
|
|
**Ruled `GH-DEC-2026-012` R3, and now re-opened as `INFD-IN-0004`.** The ruling:
|
|
distinct attestations with an authority rule — the binding digest is
|
|
authoritative for what the request *is*, `view_hash` for what was *shown*,
|
|
neither substitutable, and a disagreement between them is a finding against the
|
|
presenting surface rather than a fact about the request. Linkage by
|
|
co-reference, not nesting.
|
|
|
|
`approval-engine` has since published what its digests actually cover
|
|
(`docs/approval-claim.md`) and recommends the opposite linkage — that our
|
|
binding document carry their `binding.digest` as a field, which is the option
|
|
`GH-DEC-2026-012` refused. Their reason is the stronger version of the risk we
|
|
raised: our binding slice canonicalizes `principal` and `target`, two of their
|
|
five digest fields, so co-reference by identifier alone leaves **two
|
|
independent canonicalizations of one act** rather than removing the duplication.
|
|
|
|
Raised as a finding rather than resolved bilaterally
|
|
(`docs/finding-r3-linkage-conflict.md`). **Re-ruled by `GH-DEC-2026-015`:
|
|
gate-house reversed itself — nesting is permitted for this pair, so `view_hash`
|
|
may carry `binding.digest` and our binding slice stops independently
|
|
canonicalizing act material.**
|
|
|
|
The decisive ground was not the cycle argument we led with. It was that our
|
|
binding slice canonicalizes `principal` and `target`, two of the five fields in
|
|
their digest, so co-reference by identifier left us performing *a partial
|
|
recomputation of one act in a second vocabulary* — **closer to the translation
|
|
R3 forbade than nesting is**. Nesting removes the duplication; co-reference
|
|
manages it.
|
|
|
|
**The permission is conditioned and not yet active.** It activates when
|
|
`approval-engine` states its presentation exclusion as **normative and tested**
|
|
rather than as design intent — our own A-17 correction applied to gate-house's
|
|
permission, since the distinguishing case is someone widening the digest and
|
|
that case is unobservable until approvals start failing. Co-reference remains in
|
|
force until then, and this repository does not activate on its own initiative.
|
|
|
|
Our ordering-dependency objection to option (c) was **withdrawn as mistaken**
|
|
and recorded as withdrawn: `binding.digest` is over act material, determined
|
|
before anyone is presented anything. We stated that cost, gate-house took it
|
|
from us, and neither of us checked it.
|
|
|
|
For reference, three hashes answering three questions:
|
|
|
|
| Hash | Owner | Covers | Answers |
|
|
| --- | --- | --- | --- |
|
|
| `binding.digest` | `approval-engine` | `action`, `actor`, `principal`, `purpose`, `target` | which **act** is approved |
|
|
| `binding.pdp_digest` | `approval-engine` | the PDP's decision-request binding | which **decision request** |
|
|
| `view_hash` | here | brief, packet, highlights, locale, UI release, binding slice | what a **person was shown** | 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.
|
|
|
|
## 8e. Cadence — declared, and now supportable
|
|
|
|
`audit-core` registered this source as proposed (`AUDIT-IN-0003`,
|
|
`docs/informed-decision-source-registration.md`) and landed the detection half
|
|
(`AUDIT-WP-0009` T04/T06/T07). The cadence can stop being described as
|
|
declared-but-not-operating once heartbeats are flowing.
|
|
|
|
**Per class, not per source** — the shape this repository argued for and
|
|
`audit-core` adopted. A per-source heartbeat from a mixed-volume emitter is
|
|
satisfied by its chattiest class and says nothing about the quiet,
|
|
security-relevant one, which is the only reason heartbeats exist.
|
|
|
|
| Class | Volume | Heartbeat | Reconciliation |
|
|
| --- | --- | --- | --- |
|
|
| `presentation` | one per render | yes — legitimately silent for days at Stage 1 | yes |
|
|
| `disposition` | low, security-relevant | yes | yes |
|
|
| `stance_application` | low, security-relevant | yes | yes |
|
|
|
|
Reconciliation applies to **every** class including the high-volume one: rate
|
|
detects a stream stopping, never a stream missing the particular renders that
|
|
mattered.
|
|
|
|
A heartbeat is an **ordinary event** — same envelope, same append-only custody,
|
|
same chain. Deliberately so: a heartbeat stored outside the chain would be the
|
|
one record that could be back-dated.
|
|
|
|
Note `no_heartbeat_since_registration`: declaring a heartbeat and never sending
|
|
one is **its own finding**, not a skip.
|
|
|
|
### The bound on both controls
|
|
|
|
Reconciliation compares `audit-core`'s counts against counts we compute from our
|
|
own state. **Where the emitter is compromised, both controls agree with it** — a
|
|
compromised surface suppresses the event and its own count together, and emits a
|
|
truthful-looking `nothing-to-report`.
|
|
|
|
Both cover loss, outage, drain failure and accident, which is most of what
|
|
actually goes wrong. **Neither covers the residual we already declare**, and
|
|
neither may be described as covering it. Closing it needs an observer
|
|
independent of the emitter, which §16 put outside `audit-core`'s scope.
|
|
|
|
### Tenant provenance is not in the envelope
|
|
|
|
`audit-core`'s tenant is not an identity claim they resolve; it is a value our
|
|
credential is permitted to write, checked by exact string equality. Recording a
|
|
route in the audit event would be them restating something they did not observe
|
|
— the same error as claiming an event occurred. It lands in the registration
|
|
document instead, whose authority is `GH-DEC-2026-013`'s bounded gap rather than
|
|
a populated directory record. If that gap closes, the entry is revisited rather
|
|
than assumed still correct.
|
|
|
|
## 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.
|