Skip to content

feat: add privacy-safe audit receipts#617

Open
caioribeiroclw-pixel wants to merge 4 commits into
rohitg00:mainfrom
caioribeiroclw-pixel:feat/privacy-safe-audit-receipts
Open

feat: add privacy-safe audit receipts#617
caioribeiroclw-pixel wants to merge 4 commits into
rohitg00:mainfrom
caioribeiroclw-pixel:feat/privacy-safe-audit-receipts

Conversation

@caioribeiroclw-pixel
Copy link
Copy Markdown

@caioribeiroclw-pixel caioribeiroclw-pixel commented May 23, 2026

What

Adds an opt-in privacy-safe audit receipt format for existing audit trails.

  • buildAuditReceipt() converts audit entries to agentmemory.audit.receipt.v1
  • hashes audit ids, target ids, and user ids with SHA-256
  • includes operation/function/timestamp/target counts/detail keys, but not raw targetIds, raw details, or raw userId
  • exposes receipts via GET /agentmemory/audit?receipt=true
  • exposes the same mode through memory_audit with receipt: true

Why

Persistent memory tools need a shareable proof that memory operations happened without leaking the memory content, paths, raw ids, prompts, or user identifiers. The normal audit endpoint remains unchanged; the receipt mode is for safe handoff/debug/audit artifacts.

Verification

  • npm run build
  • npm test -- --run test/audit.test.ts
  • npm test (1097 tests passed)
  • git diff --check

Note: npm install needed --legacy-peer-deps locally because the current dependency tree has a peer constraint conflict between @anthropic-ai/sdk@0.39.0 and @anthropic-ai/claude-agent-sdk.

Summary by CodeRabbit

  • New Features

    • Privacy-safe audit receipts: request audit data with receipt=true to receive stable hashed identifiers, counts and metadata instead of raw sensitive values (supported by API endpoints, MCP tools, proxy and local modes).
  • Documentation

    • README and REST docs updated to describe the receipt parameter, receipt format, and requirement for a configured HMAC key for stable receipts.
  • Tests

    • Added tests validating receipt generation, counts, hashed identifiers, privacy flags, and omission of raw sensitive fields.

Review Change Stack

Signed-off-by: Caio Ribeiro <caio.ribeiro.clw@gmail.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented May 23, 2026

@caioribeiroclw-pixel is attempting to deploy a commit to the rohitg00's projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 23, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6f7a5bed-0d69-485b-8f88-8d041cfe8ed4

📥 Commits

Reviewing files that changed from the base of the PR and between e9f89ed and a55bee9.

📒 Files selected for processing (1)
  • test/audit.test.ts

📝 Walkthrough

Walkthrough

Adds buildAuditReceipt() (HMAC-SHA256 receipts) and integrates optional receipt mode into MCP server, standalone MCP (proxy/local), and REST GET /agentmemory/audit; updates tool schema, tests, and README.

Changes

Audit Receipt Feature

Layer / File(s) Summary
Receipt core types and generation
src/functions/audit.ts
Introduces AuditReceipt/AuditReceiptEntry and buildAuditReceipt(entries) using an env HMAC key to hash ids, extract sorted detail keys, and return a versioned receipt with privacy metadata.
Tool schema definition
src/mcp/tools-registry.ts
Adds optional receipt: boolean to memory_audit input schema with a privacy-focused description.
MCP server handler
src/mcp/server.ts
Casts audit results to AuditEntry[] and conditionally returns buildAuditReceipt(result) when args.receipt === true, serializing the computed body.
Standalone MCP handler
src/mcp/standalone.ts
Tracks receipt in validation, adds receipt=true to proxy requests via URLSearchParams, and returns either { receipt: buildAuditReceipt(...) } or { entries: ... } in local fallback.
REST API handler
src/triggers/api.ts
Casts fetched entries to AuditEntry[] and returns { receipt: buildAuditReceipt(entries), success: true } when receipt=true is provided.
Tests and documentation
test/audit.test.ts, README.md
Adds tests verifying receipt schema, hashed identifiers, detailKeys, and absence of raw sensitive values; README documents receipt: true and ?receipt=true and notes the AGENTMEMORY_RECEIPT_HMAC_KEY requirement.

Sequence Diagram

sequenceDiagram
  participant Client
  participant Handler
  participant buildAuditReceipt
  participant Receipt

  Client->>Handler: request with ?receipt=true
  Handler->>Handler: query audit entries
  Handler->>buildAuditReceipt: entries list
  buildAuditReceipt->>buildAuditReceipt: HMAC-SHA256 hash ids & extract detail keys
  buildAuditReceipt->>Receipt: produce versioned receipt with metadata
  Handler->>Client: { receipt: Receipt, success: true }
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 I hashed the traces in a hop and a skip,

No raw IDs left on the shared audit slip.
Keys sorted tidy, secrets kept light—
A privacy carrot tucked out of sight.
🥕🔐

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title 'feat: add privacy-safe audit receipts' accurately and concisely describes the main change: adding an opt-in privacy-safe audit receipt format that converts audit entries into hashed, privacy-preserving receipts.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
test/audit.test.ts (1)

124-139: ⚡ Quick win

Add explicit auditIdHash assertions to lock the privacy contract.

The new test validates target/user hashes, but not hashed audit ID behavior, which is part of the receipt contract.

Proposed test additions
     expect(receipt.entries[0].targetCount).toBe(1);
+    expect(receipt.entries[0].auditIdHash).toMatch(/^sha256:/);
     expect(receipt.entries[0].targetIdHashes[0]).toMatch(/^sha256:/);
@@
     expect(serialized).not.toContain("user-private-email@example.com");
+    expect(serialized).not.toContain(entry.id);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/audit.test.ts` around lines 124 - 139, The test is missing assertions
for the hashed audit ID; update the assertions around the created receipt
(variable receipt) to assert that the audit ID is present and hashed (e.g.,
expect(receipt.auditIdHash).toMatch(/^sha256:/) or the plural field used by the
implementation), and add a serialized exclusion asserting the raw audit ID
string is not exported (e.g., expect(serialized).not.toContain(rawAuditId)).
Locate and modify the assertions near the existing receipt and serialized checks
to include these auditIdHash format and non-export checks so the privacy
contract is locked.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/functions/audit.ts`:
- Around line 60-62: The current digest function uses an unhashed SHA-256 which
is vulnerable to dictionary attacks; replace it with an HMAC using a secret key:
change digest to use crypto.createHmac("sha256",
key).update(value).digest("hex") and prefix with "sha256:" as before, and load
the HMAC key from a secure config or environment variable (fail fast if
missing). Update any other usages in this file (the locations referenced around
lines 79-89 that call digest) to continue using digest unchanged; ensure the
secret is injected once (e.g., module init or config.getSecret) rather than
passed ad-hoc to callers.
- Around line 64-67: safeDetailKeys currently assumes details is an object and
calls Object.keys(details), which throws for null or non-object legacy values;
update safeDetailKeys to first verify details is a non-null object (and not an
array/primitive) before calling Object.keys, returning [] otherwise so malformed
runtime/legacy audit rows can't break receipt generation; reference the
safeDetailKeys function and replace the single truthy check with a robust type
check (e.g., typeof details === 'object' && details !== null &&
!Array.isArray(details)) before sorting keys.

---

Nitpick comments:
In `@test/audit.test.ts`:
- Around line 124-139: The test is missing assertions for the hashed audit ID;
update the assertions around the created receipt (variable receipt) to assert
that the audit ID is present and hashed (e.g.,
expect(receipt.auditIdHash).toMatch(/^sha256:/) or the plural field used by the
implementation), and add a serialized exclusion asserting the raw audit ID
string is not exported (e.g., expect(serialized).not.toContain(rawAuditId)).
Locate and modify the assertions near the existing receipt and serialized checks
to include these auditIdHash format and non-export checks so the privacy
contract is locked.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: fe951c37-ba8b-46e6-a6ae-7374202ab98a

📥 Commits

Reviewing files that changed from the base of the PR and between 3551241 and abce014.

📒 Files selected for processing (7)
  • README.md
  • src/functions/audit.ts
  • src/mcp/server.ts
  • src/mcp/standalone.ts
  • src/mcp/tools-registry.ts
  • src/triggers/api.ts
  • test/audit.test.ts

Comment thread src/functions/audit.ts Outdated
Comment thread src/functions/audit.ts Outdated
@arian-gogani
Copy link
Copy Markdown

cool to see audit receipts being added here. a few things that might be useful based on what we learned building a similar receipt format:

  1. the SHA-256 hashing over audit IDs and user IDs is the right approach for privacy-safe receipts. one thing to watch: if you're using JSON.stringify() for hashing, the output isn't deterministic across implementations (key order can vary). RFC 8785 (JSON Canonicalization Scheme) solves this. we use rfc8785 in Python and canonicalize in JS.

  2. consider signing the receipt with Ed25519 (or similar). a hash proves the content hasn't changed, but a signature proves who generated it. without a signature, anyone with the data can regenerate the hash.

  3. if receipts need to be verifiable across sessions, a canon_version field helps with forward compatibility. receipts generated under different hashing rules stay distinguishable.

we built an open-source receipt format for AI agents that handles all of this (Ed25519 signing, JCS canonicalization, tamper detection, hash-linked chains): github.com/arian-gogani/nobulex

happy to share more if any of this is useful.

@caioribeiroclw-pixel
Copy link
Copy Markdown
Author

Updated the PR in a9b92d2 to address the privacy review points:

  • switched receipt identifiers from plain SHA-256 to HMAC-SHA256 via AGENTMEMORY_RECEIPT_HMAC_KEY, so predictable audit/target/user ids are not dictionary-guessable in shared receipts
  • documented the key requirement for receipt=true
  • hardened safeDetailKeys() for malformed/null/non-object legacy details

Checks run locally:

npm run build
npm test -- --run test/audit.test.ts
npm test
git diff --check

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
test/audit.test.ts (1)

1-1: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Restore AGENTMEMORY_RECEIPT_HMAC_KEY after this test.

This mutates process-global state and leaves the key set for later tests in the same worker, which can hide failures in paths that should error when the key is missing.

Proposed fix
-import { describe, it, expect, beforeEach, vi } from "vitest";
+import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
@@
 describe("Audit Functions", () => {
   let kv: ReturnType<typeof mockKV>;
+  let previousReceiptHmacKey: string | undefined;
 
   beforeEach(() => {
     kv = mockKV();
+    previousReceiptHmacKey = process.env.AGENTMEMORY_RECEIPT_HMAC_KEY;
+  });
+
+  afterEach(() => {
+    if (previousReceiptHmacKey === undefined) {
+      delete process.env.AGENTMEMORY_RECEIPT_HMAC_KEY;
+      return;
+    }
+    process.env.AGENTMEMORY_RECEIPT_HMAC_KEY = previousReceiptHmacKey;
   });

Also applies to: 30-35, 121-123

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/audit.test.ts` at line 1, The test mutates the process-wide
AGENTMEMORY_RECEIPT_HMAC_KEY env var and fails to restore it, so save the
original value at test start and restore it after the test (use beforeEach to
capture const _orig = process.env.AGENTMEMORY_RECEIPT_HMAC_KEY and afterEach to
set process.env.AGENTMEMORY_RECEIPT_HMAC_KEY = _orig or delete it if undefined);
update the tests in test/audit.test.ts that set
process.env.AGENTMEMORY_RECEIPT_HMAC_KEY (references: the test blocks that
assign this env var) to use this save/restore pattern to avoid leaking state
between tests.
🧹 Nitpick comments (1)
test/audit.test.ts (1)

106-142: ⚡ Quick win

Add a regression case for malformed legacy details.

This PR hardens safeDetailKeys() for null/non-object audit rows, but the new test only covers the happy path. One malformed-entry case here would keep that fix from regressing.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/audit.test.ts` around lines 106 - 142, Add a regression case inside the
"buildAuditReceipt hashes ids and omits raw details" test (or as a new it-block
nearby) that constructs an audit entry with a malformed legacy details value
(e.g., details: null or details: "string") using recordAudit or by creating a
minimal entry object, then call buildAuditReceipt([entry]) and assert it does
not throw and that the resulting receipt.entries[0].detailKeys is handled safely
(empty or only valid keys), privacy flags remain correct, and the serialized
receipt does not contain the raw malformed details; this ensures
safeDetailKeys() remains robust for null/non-object audit rows.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@test/audit.test.ts`:
- Line 1: The test mutates the process-wide AGENTMEMORY_RECEIPT_HMAC_KEY env var
and fails to restore it, so save the original value at test start and restore it
after the test (use beforeEach to capture const _orig =
process.env.AGENTMEMORY_RECEIPT_HMAC_KEY and afterEach to set
process.env.AGENTMEMORY_RECEIPT_HMAC_KEY = _orig or delete it if undefined);
update the tests in test/audit.test.ts that set
process.env.AGENTMEMORY_RECEIPT_HMAC_KEY (references: the test blocks that
assign this env var) to use this save/restore pattern to avoid leaking state
between tests.

---

Nitpick comments:
In `@test/audit.test.ts`:
- Around line 106-142: Add a regression case inside the "buildAuditReceipt
hashes ids and omits raw details" test (or as a new it-block nearby) that
constructs an audit entry with a malformed legacy details value (e.g., details:
null or details: "string") using recordAudit or by creating a minimal entry
object, then call buildAuditReceipt([entry]) and assert it does not throw and
that the resulting receipt.entries[0].detailKeys is handled safely (empty or
only valid keys), privacy flags remain correct, and the serialized receipt does
not contain the raw malformed details; this ensures safeDetailKeys() remains
robust for null/non-object audit rows.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 14bbf957-b7a2-45e4-b827-4d62f00b2c1f

📥 Commits

Reviewing files that changed from the base of the PR and between abce014 and a9b92d2.

📒 Files selected for processing (3)
  • README.md
  • src/functions/audit.ts
  • test/audit.test.ts
✅ Files skipped from review due to trivial changes (1)
  • README.md

@caioribeiroclw-pixel
Copy link
Copy Markdown
Author

Followed up on the latest review in e9f89ed:\n\n- restore AGENTMEMORY_RECEIPT_HMAC_KEY after audit tests so receipt tests don't leak process-wide env state\n- add explicit auditIdHash assertions and ensure the raw audit id is not exported\n- add a malformed legacy details: null regression so receipt generation stays safe for old/bad audit rows\n\nChecks run locally:\n\nbash\nnpm run build\nnpm test -- --run test/audit.test.ts\nnpm test\ngit diff --check\n

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
test/audit.test.ts (1)

117-174: ⚡ Quick win

Consider adding a test for the missing HMAC key error case.

The current tests always set AGENTMEMORY_RECEIPT_HMAC_KEY before calling buildAuditReceipt(). Per the implementation context, buildAuditReceipt() throws an error when the environment variable is missing. Adding a test that verifies this error behavior would improve coverage and document the contract.

🧪 Proposed test case
it("buildAuditReceipt throws when HMAC key is missing", async () => {
  const entry = await recordAudit(kv as never, "observe", "mem::test", ["mem_1"], {});
  delete process.env.AGENTMEMORY_RECEIPT_HMAC_KEY;

  expect(() => buildAuditReceipt([entry])).toThrow(
    "AGENTMEMORY_RECEIPT_HMAC_KEY is required to generate audit receipts"
  );
});

Do you want me to help refine this test case or open a tracking issue?

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/audit.test.ts` around lines 117 - 174, Add a test that verifies
buildAuditReceipt throws when the AGENTMEMORY_RECEIPT_HMAC_KEY environment
variable is missing: create an audit entry with recordAudit (e.g.,
recordAudit(..., "observe", "mem::test", ["mem_1"], {})), delete
process.env.AGENTMEMORY_RECEIPT_HMAC_KEY, call buildAuditReceipt([entry]) inside
an expect(...).toThrow asserting the exact error message
("AGENTMEMORY_RECEIPT_HMAC_KEY is required to generate audit receipts"), and
ensure you restore or isolate the env var after the test to avoid cross-test
pollution.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@test/audit.test.ts`:
- Around line 117-174: Add a test that verifies buildAuditReceipt throws when
the AGENTMEMORY_RECEIPT_HMAC_KEY environment variable is missing: create an
audit entry with recordAudit (e.g., recordAudit(..., "observe", "mem::test",
["mem_1"], {})), delete process.env.AGENTMEMORY_RECEIPT_HMAC_KEY, call
buildAuditReceipt([entry]) inside an expect(...).toThrow asserting the exact
error message ("AGENTMEMORY_RECEIPT_HMAC_KEY is required to generate audit
receipts"), and ensure you restore or isolate the env var after the test to
avoid cross-test pollution.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: aa64bc0d-ff37-40a9-89e6-7657a97c6dce

📥 Commits

Reviewing files that changed from the base of the PR and between a9b92d2 and e9f89ed.

📒 Files selected for processing (1)
  • test/audit.test.ts

@caioribeiroclw-pixel
Copy link
Copy Markdown
Author

Followed up on the latest review in a55bee9 by adding an explicit missing-HMAC-key regression for buildAuditReceipt(). This locks the contract that receipt=true requires AGENTMEMORY_RECEIPT_HMAC_KEY instead of silently emitting guessable hashes.\n\nChecks run locally:\n\nbash\nnpm run build\nnpm test -- --run test/audit.test.ts\nnpm test\ngit diff --check\n

@caioribeiroclw-pixel
Copy link
Copy Markdown
Author

Thanks, this is useful.

I tightened this PR in the opposite direction from plain SHA-256: predictable audit/user/target ids now use HMAC-SHA256 via AGENTMEMORY_RECEIPT_HMAC_KEY, and receipt=true fails closed if the key is missing. That keeps the receipt shareable without making ids dictionary-guessable.

I agree with your broader point on canonicalization/signatures. My read is:

  • for this PR, the minimal contract is privacy-safe audit evidence over existing rows, not third-party non-repudiation yet
  • JCS + canon_version becomes important once receivers need byte-stable verification across runtimes
  • Ed25519/signature chains are probably the next layer after the maintainer decides receipts belong in this surface at all

So I’m intentionally not expanding scope here, but I’m going to track canonicalization/signing as the next step if this lands or if maintainers ask for stronger verification. Nobulex looks relevant for that layer — especially the receipt chain / tamper-evidence side.

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.

2 participants