You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Verify content hashes on every get_raw() read #110
Fix audit finding #3 (high): both object stores verify content-addressed hashes only in get(), but restore and manifest paths use get_raw(), which returns bytes without verification — a corrupted local cache/disk or a GCS writer with bucket access can alter a segment/manifest/blob while retaining its object key. This falsifies the "atomic rollback is honest" core claim.
Scope:
Hash-check every get_raw() result before returning or parsing, in both stores, including cache hits and decompressed GCS data; never cache a failed verification (evidence: crates/agentic-core/src/store.rs:151, crates/agentic-core/src/gcs_store.rs:358, crates/agentic-memory/src/restore.rs:70).
Regression tests: tampered blob / segment / manifest rejected on read, on both store impls.
Re-check snapshot-model §9 perf targets after the change (rollback < 5s, commit < 2s, write overhead < 5ms p99) — hashing on the restore path adds CPU to exactly the path the demo promises is fast.
Blocked by the Postgres CI job ticket so its restore-path tests run in CI.
Resolution records: PR link, perf numbers before/after, test names.
Shipped as PR #120 (pending Toni's review/merge per the map's PR gate).
What landed: a single check_integrity chokepoint in agentic-core::store — get() verifies object.hash(), get_raw() verifies Hash::of(bytes) (the two distinct addressing schemes, which is exactly why naive verification in get_raw would have broken get()). FS store gets the raw check directly. GCS store's get()/get_raw() share a fetch_checked(verify) helper enforcing the audit's two clauses: poisoned cache hit → evict and self-heal from GCS; corrupt download → rejected before cache_write (never cache a failed verification). No wire change — Error::IntegrityError already classifies non-retryable.
Demo-safety check (advisor-flagged): confirmed every get_raw caller reads segments/manifests (raw-hash addressed); the demo's prompt/tools/model write-back uses get() via load_blob/load_tree, so the new raw invariant can't break the broken-prompt demo.
Tests: FS tampered-on-disk rejection; GCS poisoned-cache eviction (deterministic, no server); GCS corrupt-download rejected-and-not-cached (fake-gcs integration — ran the full suite against a live fake-gcs, all 5 pass). Workspace tests, clippy -D warnings, fmt green.
Perf (before/after deliverable): added cost is one BLAKE3 pass per get_raw — 4 KB → +3.4 µs, 64 KB → +40 µs, 1 MB → +488 µs (~0.5 ms/MiB @ ~2 GB/s). Sub-ms at demo scale vs a 5 s rollback budget dominated by Postgres INSERT replay; §9 targets unaffected. Recorded in docs/architecture/benchmarks.md.
Activity
tonibergholm commentedon Jul 9, 2026
Resolution
Shipped as PR #120 (pending Toni's review/merge per the map's PR gate).
What landed: a single
check_integritychokepoint inagentic-core::store—get()verifiesobject.hash(),get_raw()verifiesHash::of(bytes)(the two distinct addressing schemes, which is exactly why naive verification inget_rawwould have brokenget()). FS store gets the raw check directly. GCS store'sget()/get_raw()share afetch_checked(verify)helper enforcing the audit's two clauses: poisoned cache hit → evict and self-heal from GCS; corrupt download → rejected beforecache_write(never cache a failed verification). No wire change —Error::IntegrityErroralready classifies non-retryable.Demo-safety check (advisor-flagged): confirmed every
get_rawcaller reads segments/manifests (raw-hash addressed); the demo's prompt/tools/model write-back usesget()viaload_blob/load_tree, so the new raw invariant can't break the broken-prompt demo.Tests: FS tampered-on-disk rejection; GCS poisoned-cache eviction (deterministic, no server); GCS corrupt-download rejected-and-not-cached (fake-gcs integration — ran the full suite against a live fake-gcs, all 5 pass). Workspace tests, clippy
-D warnings, fmt green.Perf (before/after deliverable): added cost is one BLAKE3 pass per get_raw — 4 KB → +3.4 µs, 64 KB → +40 µs, 1 MB → +488 µs (~0.5 ms/MiB @ ~2 GB/s). Sub-ms at demo scale vs a 5 s rollback budget dominated by Postgres INSERT replay; §9 targets unaffected. Recorded in
docs/architecture/benchmarks.md.Verify content hashes on every get_raw() read (#120)