From ab36e935f875d7441cc802d30ea7640acc04754a Mon Sep 17 00:00:00 2001 From: garrytan Date: Sun, 1 Nov 2026 19:46:02 +0000 Subject: [PATCH] outside-voice-api: write the egress receipt under the caller's env callModel resolved the receipt's state root from process.env even when the caller passed env, so the shadow tests wrote to the real ~/.gstack (the free-suite home-containment guard caught it). The receipt now uses req.env; the shadow tests give the fake model a private HOME, GSTACK_HOME and GSTACK_STATE_ROOT, and the receipt test no longer sets process.env to pass. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --- lib/outside-voice-api.ts | 6 ++++-- test/autoplan-shadow.test.ts | 5 +---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/lib/outside-voice-api.ts b/lib/outside-voice-api.ts index a432cc439..8e2855f0d 100644 --- a/lib/outside-voice-api.ts +++ b/lib/outside-voice-api.ts @@ -26,9 +26,11 @@ export function apiKeyFor(family: ModelFamily, env: Record, body: string, req: ApiCallRequest, provider: string): Promise<{ status: number; json: any }> { - receipted(url, body, provider, req.receipt); + receipted(url, body, provider, req); const controller = new AbortController(); const timer = setTimeout(() => controller.abort(new Error(`timeout after ${req.timeoutMs} ms`)), req.timeoutMs); req.signal?.addEventListener('abort', () => controller.abort(req.signal!.reason), { once: true }); diff --git a/test/autoplan-shadow.test.ts b/test/autoplan-shadow.test.ts index 5ac342799..069eda18d 100644 --- a/test/autoplan-shadow.test.ts +++ b/test/autoplan-shadow.test.ts @@ -36,7 +36,7 @@ async function fake(name: string, script: Array>) { const port = Number(fs.readFileSync(path.join(dir, 'port'), 'utf8')); const state = path.join(dir, 'state'); fs.mkdirSync(state, { recursive: true }); - const env = { GSTACK_STATE_ROOT: state, ANTHROPIC_API_KEY: 'sk-fake-anthropic', OPENAI_API_KEY: 'sk-fake-openai', ANTHROPIC_BASE_URL: `http://127.0.0.1:${port}`, OPENAI_BASE_URL: `http://127.0.0.1:${port}/v1` }; + const env = { HOME: state, GSTACK_HOME: state, GSTACK_STATE_ROOT: state, ANTHROPIC_API_KEY: 'sk-fake-anthropic', OPENAI_API_KEY: 'sk-fake-openai', ANTHROPIC_BASE_URL: `http://127.0.0.1:${port}`, OPENAI_BASE_URL: `http://127.0.0.1:${port}/v1` }; return { dir, env, requests: () => rows(path.join(dir, 'requests.jsonl')), receipts: () => rows(path.join(state, 'security', 'egress.jsonl')) }; } @@ -173,9 +173,6 @@ describe('the 10% audit mark', () => { describe('callModel: the shadow receipt, stop_reason and the OpenAI output bound (existing callers unchanged)', async () => { const f = await fake('transport', [{ mode: 'valid' }, { mode: 'truncated' }, { mode: 'valid' }, { mode: 'truncated' }]); test('a receipt override writes exactly one receipt with its sink and consent; the default stays the outside-voice receipt', async () => { - const saved = process.env.GSTACK_STATE_ROOT; - process.env.GSTACK_STATE_ROOT = f.env.GSTACK_STATE_ROOT; - afterAll(() => { if (saved === undefined) delete process.env.GSTACK_STATE_ROOT; else process.env.GSTACK_STATE_ROOT = saved; }); await callModel({ model: 'claude-opus-5-5', prompt: 'p', timeoutMs: 5000, env: f.env, maxTokens: 10, receipt: { sink: 'autoplan-shadow', consent: 'autoplan_shadow on', payloadClass: 'plan-and-gate' } }); expect(f.receipts().map(r => [r.sink, r.consent, r.payload_class ?? r.payloadClass])).toEqual([['autoplan-shadow', 'autoplan_shadow on', 'plan-and-gate']]); const cut = await callModel({ model: 'claude-opus-5-5', prompt: 'p', timeoutMs: 5000, env: f.env });