mirror of
https://github.com/garrytan/gstack.git
synced 2026-06-18 15:50:11 +02:00
40e5dcf57d
D9 cross-model finding from codex outside voice: salience-sourced digests can include emotionally-weighted personal pages (family, therapy, reflection). Pulling those into a coding-review prompt leaks sensitive context into work-flow reasoning. fetchSalience now strips entries whose slugs don't match an allowlist prefix BEFORE writing to the cache file. Default allowlist is SALIENCE_DEFAULT_ALLOWLIST = ['projects/', 'concepts/', 'gstack/']. User can extend via: gstack-config set salience_allowlist 'projects/,gstack/,concepts/,custom/' or override with GSTACK_SALIENCE_ALLOWLIST env var. Digest still records the strip count for transparency. Empty result emits 'all N entries stripped' note rather than silent absence. test/salience-allowlist.test.ts: 9 tests covering default permits, default blocks, empty allowlist, env override, whitespace trimming, and the invariant that defaults contain nothing sensitive (personal, family, therapy, reflection, private, medical, health). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
96 lines
4.1 KiB
TypeScript
96 lines
4.1 KiB
TypeScript
/**
|
|
* D9 salience privacy gate (T17).
|
|
*
|
|
* Verifies that fetchSalience strips entries whose slugs don't match the
|
|
* allowlist prefixes BEFORE writing the digest to disk. Sensitive content
|
|
* (family, therapy, reflection) is never persisted into the cache.
|
|
*
|
|
* Gate-tier, free.
|
|
*/
|
|
|
|
import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
|
|
import { SALIENCE_DEFAULT_ALLOWLIST } from '../scripts/brain-cache-spec';
|
|
|
|
const ORIGINAL_ENV = process.env.GSTACK_SALIENCE_ALLOWLIST;
|
|
|
|
beforeEach(() => {
|
|
delete require.cache[require.resolve('../bin/gstack-brain-cache')];
|
|
});
|
|
|
|
afterEach(() => {
|
|
if (ORIGINAL_ENV) process.env.GSTACK_SALIENCE_ALLOWLIST = ORIGINAL_ENV;
|
|
else delete process.env.GSTACK_SALIENCE_ALLOWLIST;
|
|
});
|
|
|
|
async function importCache(): Promise<typeof import('../bin/gstack-brain-cache')> {
|
|
return (await import('../bin/gstack-brain-cache')) as typeof import('../bin/gstack-brain-cache');
|
|
}
|
|
|
|
describe('salience allowlist gate', () => {
|
|
test('default allowlist permits projects/ + gstack/ + concepts/', async () => {
|
|
const mod = await importCache();
|
|
expect(mod.isSalienceSlugAllowed('projects/myrepo', SALIENCE_DEFAULT_ALLOWLIST)).toBe(true);
|
|
expect(mod.isSalienceSlugAllowed('gstack/product/helsinki', SALIENCE_DEFAULT_ALLOWLIST)).toBe(true);
|
|
expect(mod.isSalienceSlugAllowed('concepts/some-idea', SALIENCE_DEFAULT_ALLOWLIST)).toBe(true);
|
|
});
|
|
|
|
test('default allowlist BLOCKS personal/ + family/ + therapy/ + reflections', async () => {
|
|
const mod = await importCache();
|
|
expect(mod.isSalienceSlugAllowed('personal/reflection-2026-05', SALIENCE_DEFAULT_ALLOWLIST)).toBe(false);
|
|
expect(mod.isSalienceSlugAllowed('family/in-laws/ngo-kim-shing', SALIENCE_DEFAULT_ALLOWLIST)).toBe(false);
|
|
expect(mod.isSalienceSlugAllowed('therapy-session/2026-05-15', SALIENCE_DEFAULT_ALLOWLIST)).toBe(false);
|
|
expect(mod.isSalienceSlugAllowed('reflection/notes', SALIENCE_DEFAULT_ALLOWLIST)).toBe(false);
|
|
});
|
|
|
|
test('isSalienceSlugAllowed handles empty allowlist (blocks everything)', async () => {
|
|
const mod = await importCache();
|
|
expect(mod.isSalienceSlugAllowed('anything/at-all', [])).toBe(false);
|
|
});
|
|
|
|
test('isSalienceSlugAllowed handles arbitrary prefixes', async () => {
|
|
const mod = await importCache();
|
|
expect(mod.isSalienceSlugAllowed('custom/scope', ['custom/'])).toBe(true);
|
|
expect(mod.isSalienceSlugAllowed('other/scope', ['custom/'])).toBe(false);
|
|
});
|
|
|
|
test('getSalienceAllowlist returns default when env unset and config silent', async () => {
|
|
delete process.env.GSTACK_SALIENCE_ALLOWLIST;
|
|
const mod = await importCache();
|
|
const list = mod.getSalienceAllowlist();
|
|
expect(Array.isArray(list)).toBe(true);
|
|
expect(list.length).toBeGreaterThan(0);
|
|
// Should at minimum contain the curated defaults
|
|
expect(list).toContain('projects/');
|
|
expect(list).toContain('gstack/');
|
|
});
|
|
|
|
test('GSTACK_SALIENCE_ALLOWLIST env override is honored', async () => {
|
|
process.env.GSTACK_SALIENCE_ALLOWLIST = 'custom-a/,custom-b/,custom-c/';
|
|
const mod = await importCache();
|
|
const list = mod.getSalienceAllowlist();
|
|
expect(list).toEqual(['custom-a/', 'custom-b/', 'custom-c/']);
|
|
});
|
|
|
|
test('GSTACK_SALIENCE_ALLOWLIST with whitespace is trimmed', async () => {
|
|
process.env.GSTACK_SALIENCE_ALLOWLIST = ' projects/ , gstack/ , concepts/ ';
|
|
const mod = await importCache();
|
|
const list = mod.getSalienceAllowlist();
|
|
expect(list).toEqual(['projects/', 'gstack/', 'concepts/']);
|
|
});
|
|
|
|
test('empty env value falls through to default (not empty list)', async () => {
|
|
process.env.GSTACK_SALIENCE_ALLOWLIST = '';
|
|
const mod = await importCache();
|
|
const list = mod.getSalienceAllowlist();
|
|
expect(list.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
test('default allowlist contains nothing sensitive', async () => {
|
|
const sensitivePrefixes = ['personal', 'family', 'therapy', 'reflection', 'private', 'medical', 'health'];
|
|
for (const prefix of sensitivePrefixes) {
|
|
const matched = SALIENCE_DEFAULT_ALLOWLIST.some((p) => p.startsWith(prefix));
|
|
expect(matched).toBe(false);
|
|
}
|
|
});
|
|
});
|