Skip to content

fix(receipts): tolerate legacy float attention_score so a stale receipt cannot brick boot (#350)#369

Open
gnanirahulnutakki wants to merge 1 commit into
devfrom
fix/350-receipt-legacy-float-2026-07-22
Open

fix(receipts): tolerate legacy float attention_score so a stale receipt cannot brick boot (#350)#369
gnanirahulnutakki wants to merge 1 commit into
devfrom
fix/350-receipt-legacy-float-2026-07-22

Conversation

@gnanirahulnutakki

Copy link
Copy Markdown
Member

Fixes #350.

Root cause (verified against a real receipt)

CostTuple.attention_score drifted from an f64 share (0.0..=1.0) to a milli-attention u64 (crates/core-types/src/cost.rs), but nothing migrated receipts written by older builds. A real receipt on disk (~/.ardur/receipts/chain.jsonl) base64url-decodes to:

"cost":{"tokens_in":0,"tokens_out":0,"cents":0,"wall_ms":0,"attention_score":0.0}

attention_score is a JSON float. The plain derived Deserialize on a u64 field rejects it, so decode_body (crates/fused-runtime/src/receipts.rs:145) fails with payload json: invalid type: floating point 0.0, expected u64. parse_receipt_lines only tolerates a torn trailing line, so a well-formed old-format line aborts the entire chain load during boot reconciliation (builder.rs::buildload_persisted_chainverify_persisted_chain_with_jwks). A single legacy receipt therefore bricks both ardur chat and ardur-server for that data dir, with no migration and no recovery path (ardur doctor still reports ok because it does not reconcile receipts).

Fix

Field-level migration-tolerant deserializer for attention_score (#[serde(deserialize_with = ...)]):

  • an integer passes through unchanged as milli-attention;
  • a legacy float (0.0..=1.0 share) is mapped losslessly onto 0..=1000 (share * MILLI_ATTENTION_PER_UNIT, rounded) — the mapping the module docs already document;
  • negative / non-finite values still error, so tolerance is scoped to the legacy shape, not "accept anything".

The migration is representational only. A receipt's JWS signature and each parent_hash are computed over the original compact-JWS bytes, never over the decoded value, so coercing the float changes no signature or hash-chain outcome — the trust checks are preserved, not weakened. No non-JSON (bincode/postcard/ciborium) serializer exists in-tree, so deserialize_any is safe.

This deliberately does not add the broader "quarantine a mid-chain corrupt line and continue" path the issue also mentions: skipping a mid-chain line breaks parent_hash linkage and needs its own design (noted as follow-up). This change removes the actual brick — the legacy float — without that risk.

Tests

  • crates/core-types: 0.0/0.5/1.00/500/1000, integer pass-through, negative rejected.
  • crates/fused-runtime/tests/legacy_receipt_migration.rs (new): seeds a genuinely ES256-signed legacy float receipt (signed by the runtime's own key via PKCS#8 re-import) and drives the real .build() boot path — asserts boot succeeds, the value migrates to 500, and verify_persisted_chain_with_jwks still authenticates over the original bytes.

Verification

  • Real receipt payload confirmed to carry "attention_score":0.0 (float).
  • Green: ardur-core-types, ardur-receipt, ardur-cost-gate, ardur-fused-runtime, and cargo test -p ardur-e2e-tests.
  • cargo fmt --check clean; cargo clippy (core-types + fused-runtime, all targets) clean.

No Cargo.lock change is required (p256, added as a fused-runtime dev-dependency for the test, is already locked via ardur-receipt).

@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 11f90809-297c-43ef-acd2-20be5a139bd2

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/350-receipt-legacy-float-2026-07-22

Comment @coderabbitai help to get the list of available commands.

…pt cannot brick boot (#350)

`CostTuple.attention_score` drifted from an `f64` share (`0.0..=1.0`) to a
milli-attention `u64`, but nothing migrated receipts written by older builds.
Such a receipt persists the field as a JSON float; the plain derived
`Deserialize` rejects it, and boot-time receipt-chain reconciliation
(`decode_body` -> `serde_json`) aborts the whole load. A single old-format line
therefore locked users out of both `ardur chat` and `ardur-server` for that data
dir, with no migration and no recovery path.

Add a migration-tolerant field deserializer: an integer passes through as
milli-attention; a legacy `0.0..=1.0` float is mapped losslessly onto `0..=1000`
(`share * MILLI_ATTENTION_PER_UNIT`, rounded); negative / non-finite still
errors, so tolerance is scoped to the legacy shape. The migration is
representational only — a receipt's JWS signature and `parent_hash` linkage are
verified over its original on-disk bytes, never over the decoded value, so no
trust check is weakened. No non-JSON (bincode/postcard) serializer exists
in-tree, so `deserialize_any` is safe.

Tests: core-types unit coverage for the float->milli mapping, integer
pass-through, and rejection of malformed values; a new fused-runtime boot test
seeds a genuinely ES256-signed legacy float receipt and asserts the real
`.build()` reconciliation path succeeds, the value migrates to 500, and
`verify_persisted_chain_with_jwks` still authenticates.

Checkpoint: architect/sessions/issue-350-receipt-legacy-float/journal.md
Signed-off-by: GR <gnanirn@gmail.com>
@gnanirahulnutakki
gnanirahulnutakki force-pushed the fix/350-receipt-legacy-float-2026-07-22 branch from 6c8a5cd to 11067cb Compare July 23, 2026 18:29
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