-
Notifications
You must be signed in to change notification settings - Fork 1.3k
feat(cursor): serve-time blob integrity diagnostic for replay corruption #2656
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
712778e
f1d6097
37f600c
f3bd8b3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -403,6 +403,19 @@ export function storeCursorBlob(data: Uint8Array, requestScope?: CursorBlobReque | |
| return blobId; | ||
| } | ||
|
|
||
| /** | ||
| * Serve-time integrity for content-addressed blobs (devlog 260826_cursor_responses_gap 080): | ||
| * a raw 32-byte blob id IS the SHA-256 of its bytes, so served data whose digest mismatches | ||
| * the id means in-store corruption — the splice signature behind garbled replayed tool | ||
| * results. Ids longer than 32 bytes (digested-key namespace) and server-minted ids are not | ||
| * content-addressed and always pass. | ||
| */ | ||
| export function cursorBlobServeIntegrityOk(blobId: Uint8Array, served: Uint8Array): boolean { | ||
| if (blobId.byteLength !== 32) return true; | ||
| const digest = createHash("sha256").update(served).digest(); | ||
| return digest.equals(Buffer.from(blobId)); | ||
| } | ||
|
|
||
| /** | ||
| * Long-lived pin for blobs referenced by an active Cursor conversation checkpoint. | ||
| * Unlike a request scope, this lease is not sealed and is not released by getBlob hydration. | ||
|
|
@@ -622,6 +635,13 @@ export function handleCursorNativeKv( | |
| if (kvMsg.message.case === "getBlobArgs") { | ||
| const blobKey = key(kvMsg.message.value.blobId); | ||
| const blobData = getBlob(blobKey); | ||
| // Splice-class corruption guard (devlog 260826 080): diagnostic only, never blocks serving. | ||
| if (blobData && !cursorBlobServeIntegrityOk(kvMsg.message.value.blobId, blobData)) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
On every successful Useful? React with 👍 / 👎. |
||
| debugProviderDiagnostic("cursor", "blob-integrity-mismatch", { | ||
| blobKey: blobKey.slice(0, 18), | ||
| servedBytes: blobData.byteLength, | ||
| }); | ||
| } | ||
| if (blobData && requestScope && blobRequestScopes.get(requestScope)?.kind === "request") { | ||
| releaseHydratedBlob(blobKey, requestScope); | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| import { describe, expect, test } from "bun:test"; | ||
| import { createHash } from "node:crypto"; | ||
| import { cursorBlobServeIntegrityOk, storeCursorBlob } from "../src/adapters/cursor/native-exec"; | ||
|
|
||
| describe("cursor blob serve-time integrity (devlog 260826 080)", () => { | ||
| test("content-addressed blob passes when bytes match the id", () => { | ||
| const data = new TextEncoder().encode('{"role":"user","content":"clean"}'); | ||
| const id = storeCursorBlob(data); | ||
| expect(id.byteLength).toBe(32); | ||
| expect(cursorBlobServeIntegrityOk(id, data)).toBe(true); | ||
| }); | ||
|
|
||
| test("mutated bytes are detected (splice fault injection)", () => { | ||
| const data = new TextEncoder().encode('{"role":"assistant","content":"[tool_result] output"}'); | ||
| const id = new Uint8Array(createHash("sha256").update(data).digest()); | ||
| const corrupted = new TextEncoder().encode('{"role":"assistant","content":"[ martool_result] output"}'); | ||
| expect(cursorBlobServeIntegrityOk(id, corrupted)).toBe(false); | ||
| }); | ||
|
|
||
| test("non-content-addressed ids (not 32 bytes) always pass", () => { | ||
| const served = new TextEncoder().encode("anything"); | ||
| expect(cursorBlobServeIntegrityOk(new Uint8Array(8), served)).toBe(true); | ||
| expect(cursorBlobServeIntegrityOk(new Uint8Array(64), served)).toBe(true); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For locally generated replay blobs,
storeCursorBlobderives the ID from the same bytes andsetBlobstores a clone, so corruption introduced during root assembly or serialization is incorporated into both the payload and its hash and this check returns true. The new test bypasses the store by manually pairing an old digest with mutated bytes, so it does not demonstrate that the actualgetBlobArgspath can detect the reported splice. Add the integrity/round-trip check at the assembly boundary, or introduce a real retained-store fault-injection seam and test the emitted diagnostic throughhandleCursorNativeKv.AGENTS.md reference: src/AGENTS.md:L24-L26
Useful? React with 👍 / 👎.