Skip to content

BREAKING CHANGE: no free text in the digested core - #337

Merged
MoltyCel merged 1 commit into
mainfrom
feat/core-drop-reason
Sep 4, 2026
Merged

BREAKING CHANGE: no free text in the digested core#337
MoltyCel merged 1 commit into
mainfrom
feat/core-drop-reason

Conversation

@MoltyCel

@MoltyCel MoltyCel commented Sep 4, 2026

Copy link
Copy Markdown
Owner

The draft excludes it (AAE -02 §2.5.2/§2.5.3) and the kernel still carried it. This closes the gap the other way round from the usual: the specification was ahead, and the code follows.

What moves

reason leaves all three digested cores, and every predicate entry inside them:

Core File Tag
verdict app/enforcement/enforce_check.py aae:enforce-core:v1
ratification app/enforcement/ratify.py aae:enforce-ratify-core:v1
AER decision sdk/…/_ext_core.py aae:aer-core:v1

_pred() still returns six fields, because the response wants the reason. A new _trace_for_core() projects the five reproducible ones — predicate, field, value, bound, result — and only that projection reaches a core. ratify.py and _ext_core.py import it from the kernel rather than repeating the field list, so the three cannot drift apart.

The AER core does not come along because of reason alone. It carries enforce_version as a field of its own, so the version bump would have broken it either way. Leaving it out would have produced a half-converted package.

Why

A reason string is written for a person reading a log. Two implementations do not have to agree on its wording, and nothing forces them to — so it has no place in a value they must agree on byte for byte. With it in, a verifier could reach the correct verdict from the correct inputs and still fail to reproduce the digest, for no cause a reader could act on.

The reason is not lost. It stays on the verdict and on every predicate in the response, which is where a human reads it:

PERMIT   reason in Antwort: True | reason im Core: False | Core-Trace: bound, field, predicate, result, value
PENDING  reason in Antwort: True | reason im Core: False | …
DENY     reason in Antwort: True | reason im Core: False | …

The break, and how it is visible

Every core_digest issued so far becomes unreproducible against this kernel. ENFORCE_VERSION, RATIFY_VERSION and AER_VERSION go 2.03.0 and sit in their respective cores, so whoever holds a record marked 2.0 sees from the field that it needs the older package.

Unaffected: action_binding, mandate_digest, transaction_digest, the evidence item and query digests, delegator_aae_hash, and moltrust:aae-verdict:v1 in the evaluator — a different machine, whose signatures live in the database.

Verdicts and decisions do not move. PERMIT stays PERMIT, PENDING still needs an explicit hold, forbid still outranks an allowing grant, all three ratification guards behave as before. What changed is what gets digested, not what gets decided.

SDK 0.4.0 → 0.5.0

Both kernel copies regenerated from the server files rather than edited in parallel; test_core_parity holds the line-for-line identity (exactly one import line differing in each). examples/aer/decision.json rebuilt with the bundled generator. README banner and a BREAKING changelog entry carry the migration line.

Corrected in passing: the changelog listed 0.4.0 as unreleased. It is on PyPI.

Tests

+3 cases. That a reason is reported but not digested, on the verdict core and on the ratification core. And the one the change exists for: swapping the reason text on an identical decision leaves the digest untouched — a property no test could have stated before, because it was false.

Three tests encoded the old contract and are updated, not deleted: the two core field sets, and the one asserting the core trace equals the response trace. It no longer does, and the test now says exactly how they differ.

Run Branch origin/main
tests/test_enforce_check.py + tests/test_ratify.py 143 passed, 14 errors 140 passed, 14 errors
sdk/python/tests 218 passed 218 passed
ruff 10 findings 10, identical

The 14 errors are endpoint tests without fastapi installed, the same in both runs.

Published examples

developers.html carries no concrete digest values — zero hits for sha256:[0-9a-f]{16} — so no web change is needed. The SDK README quoted the 0.4.0 break in its banner; that banner now describes this one.

Not in this PR

The conformance vectors. All 26 expected core digests change, and they recompute against this kernel once it is merged — v1.4.0 in aae-conformance-vectors, a separate step, because computing them against an unmerged kernel would pin values nobody can reproduce.

No merge, no deploy, no publish.

The draft excludes it (AAE -02 §2.5.2/§2.5.3) and the kernel still carried it. This closes
the gap the other way round from the usual: the specification was ahead, the code follows.

## What moves

`reason` leaves all three digested cores and every predicate entry inside them:

  app/enforcement/enforce_check.py   verdict core, aae:enforce-core:v1
  app/enforcement/ratify.py          ratification core, aae:enforce-ratify-core:v1
  sdk .../_ext_core.py               AER decision core, aae:aer-core:v1

`_pred()` still returns six fields, because the response wants the reason. The new
`_trace_for_core()` projects the five reproducible ones — predicate, field, value, bound,
result — and only that projection goes into a core. Both ratify.py and _ext_core.py import
it from the kernel rather than repeating the field list, so the set cannot drift apart.

The AER core does not come along because of `reason` alone: it carries `enforce_version` as
a field of its own, so the version bump would have broken it regardless.

## Why

A reason string is written for a person reading a log. Two implementations do not have to
agree on its wording, and nothing forces them to — so it has no place in a value they must
agree on byte for byte. Leaving it in meant a verifier could reach the correct verdict from
the correct inputs and still fail to reproduce the digest, for no reason a reader could act
on. The reason itself is not lost: it stays on the verdict and on every predicate in the
response, which is where a human reads it.

## The break, and how it is visible

Every core_digest issued so far becomes unreproducible against this kernel. ENFORCE_VERSION,
RATIFY_VERSION and AER_VERSION go 2.0 -> 3.0 and sit in their respective cores, so whoever
holds a record marked 2.0 can see from the field that it needs the older package.

Unaffected: action_binding, mandate_digest, transaction_digest, the evidence item and query
digests, delegator_aae_hash, and moltrust:aae-verdict:v1 in the evaluator (a different
machine, whose signatures live in the database).

Verdicts and decisions themselves do not move. PERMIT stays PERMIT, PENDING still requires an
explicit hold, forbid still outranks an allowing grant, and all three ratification guards
behave as before. What changed is what gets digested, not what gets decided.

## SDK 0.4.0 -> 0.5.0

Both kernel copies regenerated from the server files rather than edited in parallel;
test_core_parity holds the line-for-line identity. examples/aer/decision.json rebuilt with
the bundled generator. The README banner and a BREAKING changelog entry carry the migration
line.

Corrected in passing: the changelog listed 0.4.0 as unreleased. It is on PyPI.

## Tests

+3 cases. That a reason is reported but not digested, on the verdict core and on the
ratification core; and that swapping the reason text on an identical decision leaves the
digest untouched — which is the property the change exists for, and which no test could have
stated before.

Three tests encoded the old contract and are updated, not deleted: the core field sets, and
the one asserting the core trace equals the response trace. It no longer does, and the test
now says exactly how they differ.

Core 143 passed, SDK 218 passed. The 14 errors are endpoint tests without fastapi, identical
on origin/main. ruff findings identical to baseline.

## Not here

The conformance vectors. All 26 expected core digests change, and they recompute against this
kernel once it is merged — that is v1.4.0 in the vector repository, a separate step.

No merge, no deploy, no publish.
@MoltyCel
MoltyCel merged commit bd5c36d into main Sep 4, 2026
14 checks passed
@MoltyCel
MoltyCel deleted the feat/core-drop-reason branch September 4, 2026 14:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant