fix(facts): fold visibility tier into hot-memory meta cache key

trustedFactReads is re-read from config on every stdio tool call, so the
reader's visibility tier can now flip within one server process. The
meta-hook cache key (source, session, allow-list hash) didn't capture it,
so a mid-session opt-out of facts.trust_local_reads could keep serving a
private-inclusive cached payload for the 30s TTL. Compute the visibility
filter before the cache lookup and append the tier to the key, restoring
the header's cross-tier no-bleed invariant.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-07-22 11:10:57 -07:00
co-authored by Claude Fable 5
parent d6b0253c59
commit 76589d3ddc
+7 -5
View File
@@ -51,7 +51,13 @@ export async function getBrainHotMemoryMeta(
const sessionId = (ctx as { source_session?: string }).source_session
?? null;
const allowListHash = hashAllowList(ctx.takesHoldersAllowList);
const cacheKey = `${sourceId}::${sessionId ?? '_'}::${allowListHash}`;
// Visibility tier: untrusted remote → world-only; trusted local +
// owner-trusted reads → all rows. Folded into the cache key (the header's
// "cache entries don't bleed across tiers" invariant): trustedFactReads is
// re-read from config per call, so a mid-session opt-out must not keep
// serving a private-inclusive cached payload for the TTL window.
const visibility = readableFactVisibilities(ctx);
const cacheKey = `${sourceId}::${sessionId ?? '_'}::${allowListHash}::${visibility ? 'world' : 'all'}`;
const ttl = Math.max(1000, opts.ttlMs ?? DEFAULT_TTL_MS);
const topK = Math.max(1, Math.min(opts.topK ?? DEFAULT_TOP_K, 25));
@@ -62,10 +68,6 @@ export async function getBrainHotMemoryMeta(
return cached.payload;
}
// Build a fresh payload. Visibility tier: untrusted remote → world-only;
// trusted local + owner-trusted reads → all rows.
const visibility = readableFactVisibilities(ctx);
let rows: FactRow[] = [];
if (sessionId) {
rows = await ctx.engine.listFactsBySession(sourceId, sessionId, {