Gateway evidence (Pro)
Download a tamper-evident record of your personal Gateway activity and check it without trusting Foo Guard.
Page text and source link for your AI assistant. Nothing is sent automatically.
With Pro, every final Gateway decision and every change to your Gateway connection is added to a tamper-evident log for your account. Each entry is linked to the one before it by its hash and dated by the database. Every hour Foo Guard signs a checkpoint over all logs with its published key and stores it where it can't be changed or deleted.
Entries hold the tool name, rule, decision, outcome and time, or what changed in your connection and the server's host. They never hold tool arguments, responses or credentials. Request details on the Gateway page are kept for 7 days; evidence entries are kept while your account exists. A call whose result never came back is recorded as uncertain after 10 minutes. If Pro ends, no new requests are recorded, but you can still view and download your log. Deleting your account removes it and leaves a deletion record.
Download your evidence
On the Gateway page, choose Download verifiable evidence. The file holds every entry exactly as Foo Guard hashed it, and Foo Guard's latest signed checkpoint with a proof that covers your log. Logs longer than 250,000 entries download in parts (firstSequence, nextFrom); put the parts' entries together, in order, before checking.
{
"type": "fooguard.personal-evidence-file/v1",
"userId": "…",
"exportedAt": "2026-11-20T15:31:00.000Z",
"entries": [
{ "sequence": 1, "entryHash": "4be0…", "entry": "{\"source\":\"connection\",\"changes\":[\"created\"],…}" },
{ "sequence": 2, "entryHash": "9c1e…", "entry": "{\"source\":\"gateway_decision\",\"tool\":\"notes.delete\",…}" }
],
"checkpoint": {
"checkpoint": { "payload": "{\"schemaVersion\":\"fooguard.evidence-checkpoint/v3\",…,\"personalRoot\":\"…\"}", "algorithm": "ECDSA_SHA_256", "keyId": "fooguard-audit-1", "signature": "…" },
"personal": { "userId": "…", "sequence": 1281, "head": "…" },
"salt": "…", "index": 604, "siblings": ["…"]
}
}The proof reveals nothing about anyone else: each hour every log gets a fresh random salt and a random position in a tree of at least 1,024 slots, and empty slots hold random values.
Check it
- Each
entryHashis the SHA-256 of itsentry, each entry'spreviousHashis theentryHashbefore it, and sequence numbers have no gaps. - The checkpoint's signature verifies with the key named by
keyIdfrom `/.well-known/fooguard-audit-keys.json`, itsissuedAtis not before that key'svalidFrom, it is aproductionv3 checkpoint, and the proof leads from your log's entry to the signedpersonalRoot. - The proof's
headequals theentryHashof entrysequencein the file. Everything up to there is signed; later entries are linked to it and are covered by the next checkpoint. - Optionally, the SHA-256 of
checkpoint.payloadappears in the public list at `/api/audit-checkpoints`.
import { createHash, createPublicKey, verify } from 'node:crypto';
const sha = (...parts) => createHash('sha256').update(Buffer.concat(parts)).digest();
export function verifyEvidenceFile(file, publicKeyPem, keyId, keyValidFrom) {
let previous = null;
for (const [i, row] of file.entries.entries()) {
const entry = JSON.parse(row.entry);
if (row.sequence !== i + 1 || entry.sequence !== row.sequence || entry.userId !== file.userId) return false;
if (sha(Buffer.from(row.entry, 'utf8')).toString('hex') !== row.entryHash || entry.previousHash !== previous) return false;
previous = row.entryHash;
}
const { checkpoint, personal, salt, index, siblings } = file.checkpoint ?? {};
if (!checkpoint || checkpoint.keyId !== keyId || personal.userId !== file.userId) return false;
if (!verify('sha256', Buffer.from(checkpoint.payload, 'utf8'), { key: createPublicKey(publicKeyPem), dsaEncoding: 'der' }, Buffer.from(checkpoint.signature, 'base64'))) return false;
const signed = JSON.parse(checkpoint.payload);
if (signed.schemaVersion !== 'fooguard.evidence-checkpoint/v3' || signed.series !== 'production' || signed.keyId !== keyId) return false;
if (keyValidFrom && Date.parse(signed.issuedAt) < Date.parse(keyValidFrom)) return false;
if (siblings.length !== Math.log2(signed.personalTreeSize)) return false;
const leaf = JSON.stringify({ userId: personal.userId, sequence: personal.sequence, head: personal.head });
let hash = sha(Buffer.from([0]), Buffer.from(salt, 'hex'), Buffer.from(leaf, 'utf8'));
let position = index;
for (const sibling of siblings) {
const other = Buffer.from(sibling, 'hex');
hash = position % 2 === 0 ? sha(Buffer.from([1]), hash, other) : sha(Buffer.from([1]), other, hash);
position >>= 1;
}
if (position !== 0 || hash.toString('hex') !== signed.personalRoot) return false;
return file.entries[personal.sequence - 1]?.entryHash === personal.head;
}