mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-03 07:18:03 +01:00
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>
This commit is contained in:
1 parent
d5bf54316d
commit
ab36e935f8
2 files changed
+5
-6
No files matched your search
@@ -26,9 +26,11 @@ export function apiKeyFor(family: ModelFamily, env: Record<string, string | unde
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function receipted(url: string, body: string, provider: string, receipt: ApiReceipt | undefined): void {
|
||||
function receipted(url: string, body: string, provider: string, req: ApiCallRequest): void {
|
||||
const receipt = req.receipt;
|
||||
try {
|
||||
writeReceipt({
|
||||
env: req.env,
|
||||
sink: receipt?.sink ?? 'outside-voice-api', host: new URL(url).host, payloadClass: receipt?.payloadClass ?? 'review-prompt', bytes: Buffer.byteLength(body), sha256: sha256Hex(body),
|
||||
consent: receipt?.consent ?? `user ran gstack-outside-voice --runner api (${provider.toUpperCase()}_API_KEY configured)`,
|
||||
});
|
||||
@@ -38,7 +40,7 @@ function receipted(url: string, body: string, provider: string, receipt: ApiRece
|
||||
}
|
||||
|
||||
async function post(url: string, headers: Record<string, string>, 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 });
|
||||
|
||||
@@ -36,7 +36,7 @@ async function fake(name: string, script: Array<Record<string, unknown>>) {
|
||||
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 });
|
||||
|
||||
Reference in new issue
Block a user