mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-17 10:25:33 +02:00
v1.87.0.0 feat: add verified CSO audits and replayable repair bundles (#2852)
* feat(cso): add verified audits and replayable repair bundles * fix(cso): harden qualification and setup boundaries * fix(cso): assemble security canaries at runtime * fix(cso): bound release proof and maintenance work Co-Authored-By: OpenAI Codex <noreply@openai.com> * fix(cso): require complete evaluation reports Co-Authored-By: OpenAI Codex <noreply@openai.com> * fix(cso): replay expired snapshots from supplied source Co-Authored-By: OpenAI Codex <noreply@openai.com> * test(cso): synchronize DNS cancellation assertion Co-Authored-By: OpenAI Codex <noreply@openai.com> * chore(ship): exempt repository owner from liveness proof Co-Authored-By: OpenAI Codex <noreply@openai.com> * test(cso): make recheck retention overlap deterministic Co-Authored-By: OpenAI Codex <noreply@openai.com> * chore: bump version and changelog (v1.85.0.0) Co-Authored-By: OpenAI Codex <noreply@openai.com> * fix(cso): pass native release gates Co-Authored-By: OpenAI Codex <noreply@openai.com> * chore: move release to v1.86.0.0 Co-Authored-By: OpenAI Codex <noreply@openai.com> * fix(cso): resolve rechecks by finding Co-Authored-By: OpenAI Codex <noreply@openai.com> * chore: move release to v1.87.0.0 Co-Authored-By: OpenAI Codex <noreply@openai.com> * fix(cso): pass macOS and Windows release gates Normalize BSD wc output, compare Windows paths by filesystem identity, preserve portable snapshot race coverage, and narrow POSIX-only Windows fixtures. Co-Authored-By: OpenAI Codex <noreply@openai.com> * fix(cso): harden native verification gates * fix(cso): refine Windows native diagnostics * test(cso): isolate Windows Git startup failure * test(cso): stabilize Windows native diagnostics * fix(cso): support hardened Git on Windows * fix(cso): close final verification gaps * test(cso): bound cold Docker fixture setup * fix(cso): restore cross-platform free-suite gates --------- Co-authored-by: OpenAI Codex <noreply@openai.com>
This commit is contained in:
co-authored by
OpenAI Codex
parent
9f81911136
commit
4a3c6a8a3c
@@ -1,16 +1,245 @@
|
||||
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
|
||||
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { ClaudeAdapter } from './helpers/providers/claude';
|
||||
import { GptAdapter } from './helpers/providers/gpt';
|
||||
import { ClaudeAdapter, claudeExecArgs, claudeExecEnvironment, claudeExecWorkingDirectory, claudeProducerPaths, resultFromClaudeOutput } from './helpers/providers/claude';
|
||||
import { codexExecArgs, codexExecEnvironment, codexExecWorkingDirectory, codexProducerConfig, codexProducerPaths, GptAdapter, resultFromCodexStream } from './helpers/providers/gpt';
|
||||
import { geminiExecArgs, geminiExecEnvironment, geminiExecWorkingDirectory, geminiProducerPaths, geminiProducerSystemSettings } from './helpers/providers/gemini';
|
||||
import { CSO_PRODUCER_SHELL_ENV, csoProducerChildEnvironment } from './helpers/providers/types';
|
||||
|
||||
const ENV_KEYS = ['PATH', 'GSTACK_CLAUDE_BIN', 'GSTACK_CLAUDE_BIN_ARGS',
|
||||
'GSTACK_CODEX_MODEL', 'EVALS_MODEL', 'GSTACK_EVAL_MODEL', 'GSTACK_EVAL_MODEL_CAPTURE'];
|
||||
let saved: Record<string, string | undefined>;
|
||||
let workdir: string;
|
||||
|
||||
describe('CSO producer provider policies', () => {
|
||||
const suffix = process.platform === 'win32' ? '.exe' : '';
|
||||
const helperAt = (directory: string) => join(directory, `gstack-cso-launcher${suffix}`);
|
||||
const providerAt = (directory: string) => ({ executable: join(directory, `provider${suffix}`), argsPrefix: [] as string[] });
|
||||
const policy = (stateDirectory: string, sourceDirectory: string, helperLauncher: string) => ({
|
||||
stateDirectory, sourceDirectory, helperLauncher, helperGeneration: join(dirname(helperLauncher), '.gstack-cso-generation'), providerCommand: providerAt(dirname(helperLauncher)),
|
||||
});
|
||||
|
||||
test('keeps exact Codex read-only defaults and gives the producer a custom least-privilege profile', () => {
|
||||
const providerWorkdir = join(tmpdir(), 'gstack-provider-work');
|
||||
const common = { prompt: 'Reply OK', workdir: providerWorkdir, timeoutMs: 5000 };
|
||||
expect(codexExecArgs(common, 'gpt-test')).toEqual([
|
||||
'exec', 'Reply OK', '-C', providerWorkdir,
|
||||
'-s', 'read-only', '--skip-git-repo-check', '--json', '-m', 'gpt-test',
|
||||
]);
|
||||
|
||||
const state = join(providerWorkdir, 'state');
|
||||
const source = join(providerWorkdir, 'source');
|
||||
const helper = helperAt(join(providerWorkdir, 'installed'));
|
||||
const producer = { ...common, csoProducer: policy(state, source, helper) };
|
||||
expect(codexExecArgs(producer, 'gpt-test')).toEqual([
|
||||
'exec', 'Reply OK', '-C', codexProducerPaths(state).workdir,
|
||||
'--strict-config', '--ephemeral', '--ignore-rules', '--skip-git-repo-check',
|
||||
'--json', '-m', 'gpt-test',
|
||||
]);
|
||||
expect(codexExecWorkingDirectory(common)).toBe(providerWorkdir);
|
||||
expect(codexExecWorkingDirectory(producer)).toBe(codexProducerPaths(state).workdir);
|
||||
|
||||
const config = codexProducerConfig(producer);
|
||||
expect(config).toContain('default_permissions = "cso-producer"');
|
||||
expect(config).toContain('":root" = "deny"\n":minimal" = "read"');
|
||||
expect(config).toContain('[permissions.cso-producer.filesystem.":workspace_roots"]\n"." = "write"');
|
||||
expect(config).toContain('[permissions.cso-producer.network]\nenabled = false');
|
||||
expect(config).toContain(`include_only = ${JSON.stringify(CSO_PRODUCER_SHELL_ENV)}`);
|
||||
expect(config).toContain(`${JSON.stringify(join(state, 'cso-home'))} = "write"`);
|
||||
for (const admitted of [
|
||||
join(dirname(helper), `cso-eval-producer${suffix}`), helper,
|
||||
join(dirname(helper), `gstack-cso-core${suffix}`), join(dirname(helper), `gstack-cso-watchdog${suffix}`),
|
||||
join(dirname(helper), '.gstack-cso-generation'), source,
|
||||
]) expect(config).toContain(`${JSON.stringify(admitted)} = "read"`);
|
||||
expect(config).not.toContain(`${JSON.stringify(dirname(helper))} = "read"`);
|
||||
expect(config).not.toContain(`${JSON.stringify(dirname(source))} = "read"`);
|
||||
expect(config).not.toContain('OPENAI_API_KEY');
|
||||
expect(() => codexExecArgs({ ...common, csoProducer: policy('relative-state', source, helper) }, 'gpt-test')).toThrow('absolute normalized path');
|
||||
expect(() => codexExecArgs({ ...common, csoProducer: policy(state, 'relative-source', helper) }, 'gpt-test')).toThrow('absolute normalized path');
|
||||
expect(() => codexExecArgs({ ...common, csoProducer: { ...policy(state, source, helper), helperGeneration: join(dirname(helper), 'wrong-generation') } }, 'gpt-test')).toThrow('exact adjacent');
|
||||
expect(codexExecArgs(producer, 'gpt-test').join(' ')).not.toMatch(/(?:^|\s)(?:-s|--sandbox)(?:\s|$)|danger|bypass|approve-for-me/);
|
||||
});
|
||||
|
||||
test('keeps exact Claude defaults and enables its producer-only noninteractive safe policy', () => {
|
||||
const work = join(tmpdir(), 'gstack-claude-work');
|
||||
const state = join(work, 'state');
|
||||
const source = join(work, 'source');
|
||||
const helper = helperAt(join(work, 'installed'));
|
||||
const common = { prompt: 'Reply OK', workdir: work, timeoutMs: 5000 };
|
||||
expect(claudeExecArgs(common, 'claude-test', ['wrapper'])).toEqual([
|
||||
'wrapper', '-p', '--output-format', 'json', '--model', 'claude-test',
|
||||
]);
|
||||
const producer = { ...common, csoProducer: policy(state, source, helper) };
|
||||
expect(claudeExecArgs(producer, 'claude-test')).toEqual([
|
||||
'-p', '--output-format', 'json', '--model', 'claude-test',
|
||||
'--restricted', '--safe-mode', '--no-session-persistence',
|
||||
'--permission-prompts', 'none', '--permission-mode', 'dontAsk',
|
||||
'--tools', 'Bash,Write',
|
||||
'--allowed-tools', `Bash(${helper}),Bash(${helper} *),Write`,
|
||||
'--add-dir', claudeProducerPaths(state).workdir,
|
||||
'--add-dir', source,
|
||||
'--add-dir', join(state, 'cso-home'),
|
||||
'--strict-mcp-config', '--no-chrome',
|
||||
]);
|
||||
expect(claudeExecWorkingDirectory(common)).toBe(work);
|
||||
expect(claudeExecWorkingDirectory(producer)).toBe(claudeProducerPaths(state).workdir);
|
||||
expect(claudeExecArgs(producer, 'claude-test').join(' ')).not.toMatch(/bypassPermissions|danger/);
|
||||
});
|
||||
|
||||
test('keeps exact Gemini defaults and replaces deprecated yolo only for the producer', () => {
|
||||
const work = join(tmpdir(), 'gstack-gemini-work');
|
||||
const state = join(work, 'state');
|
||||
const source = join(work, 'source');
|
||||
const helper = helperAt(join(work, 'installed'));
|
||||
const common = { prompt: 'Reply OK', workdir: work, timeoutMs: 5000, model: 'gemini-test' };
|
||||
expect(geminiExecArgs(common)).toEqual([
|
||||
'-p', 'Reply OK', '--output-format', 'stream-json', '--yolo', '--model', 'gemini-test',
|
||||
]);
|
||||
const producer = { ...common, csoProducer: policy(state, source, helper) };
|
||||
const producerArgs = geminiExecArgs(producer);
|
||||
expect(producerArgs).toEqual([
|
||||
'-p', 'Reply OK', '--output-format', 'stream-json',
|
||||
'--approval-mode', 'yolo', '--include-directories', geminiProducerPaths(state).workdir, '-e', 'none',
|
||||
'--model', 'gemini-test',
|
||||
]);
|
||||
expect(producerArgs).not.toContain(work);
|
||||
expect(producerArgs).not.toContain(source);
|
||||
expect(geminiExecWorkingDirectory(common)).toBe(work);
|
||||
expect(geminiExecWorkingDirectory(producer)).toBe(geminiProducerPaths(state).workdir);
|
||||
const contextFileName = `.gstack-cso-context-${'a'.repeat(32)}.md`;
|
||||
expect(geminiProducerSystemSettings(contextFileName, helper)).toEqual({
|
||||
advanced: { ignoreLocalEnv: true },
|
||||
admin: { extensions: { enabled: false }, mcp: { enabled: false }, skills: { enabled: false } },
|
||||
context: { fileName: [contextFileName], includeDirectoryTree: false, loadMemoryFromIncludeDirectories: false, memoryBoundaryMarkers: [] },
|
||||
hooksConfig: { enabled: false },
|
||||
privacy: { usageStatisticsEnabled: false },
|
||||
security: {
|
||||
environmentVariableRedaction: { allowed: [], blocked: ['GEMINI_API_KEY', 'GOOGLE_API_KEY', 'GOOGLE_CLOUD_PROJECT', 'GOOGLE_CLOUD_LOCATION'], enabled: true },
|
||||
folderTrust: { enabled: false }, toolSandboxing: false,
|
||||
},
|
||||
skills: { enabled: false },
|
||||
telemetry: { enabled: false, logPrompts: false },
|
||||
tools: { allowed: [`run_shell_command(${helper})`, 'write_file'], core: [`run_shell_command(${helper})`, 'write_file'], sandbox: false },
|
||||
});
|
||||
expect(() => geminiProducerSystemSettings('GEMINI.md', helper)).toThrow('INVALID_GEMINI_CONTEXT_FILENAME');
|
||||
});
|
||||
|
||||
test('passes only selected provider auth plus safe execution inputs and strips Docker credentials', () => {
|
||||
const fixtureRoot = join(tmpdir(), 'gstack-provider-environment-policy');
|
||||
const state = join(fixtureRoot, 'state');
|
||||
const source = join(fixtureRoot, 'source');
|
||||
const helpers = join(fixtureRoot, 'helpers');
|
||||
const work = join(fixtureRoot, 'work');
|
||||
const sourceEnv = {
|
||||
PATH: '/usr/bin', HOME: '/home/eval', LANG: 'C.UTF-8', LC_ALL: 'C.UTF-8', TZ: 'UTC',
|
||||
GSTACK_HOME: state, GSTACK_SESSION_KIND: 'spawned', GSTACK_HEADLESS: '1',
|
||||
OPENAI_API_KEY: 'openai-auth', ANTHROPIC_API_KEY: 'anthropic-auth', CLAUDE_CODE_OAUTH_TOKEN: 'claude-auth',
|
||||
GEMINI_API_KEY: 'gemini-auth', GOOGLE_CLOUD_PROJECT: 'project',
|
||||
CSO_EVAL_PAID: '1', AWS_SECRET_ACCESS_KEY: 'unrelated',
|
||||
DOCKER_HOST: 'tcp://remote.example:2376', DOCKER_CONFIG: '/credentials', DOCKER_CERT_PATH: '/certs',
|
||||
};
|
||||
const safe = { PATH: '/usr/bin', HOME: '/home/eval', LANG: 'C.UTF-8', LC_ALL: 'C.UTF-8', TZ: 'UTC', GSTACK_HOME: state, GSTACK_SESSION_KIND: 'spawned', GSTACK_HEADLESS: '1' };
|
||||
expect(csoProducerChildEnvironment('gpt', sourceEnv)).toEqual({ ...safe, OPENAI_API_KEY: 'openai-auth' });
|
||||
expect(csoProducerChildEnvironment('claude', sourceEnv)).toEqual({ ...safe, ANTHROPIC_API_KEY: 'anthropic-auth', CLAUDE_CODE_OAUTH_TOKEN: 'claude-auth' });
|
||||
expect(csoProducerChildEnvironment('gemini', sourceEnv)).toEqual({ ...safe, GEMINI_API_KEY: 'gemini-auth', GOOGLE_CLOUD_PROJECT: 'project' });
|
||||
const helper = helperAt(helpers);
|
||||
const producer = policy(state, source, helper);
|
||||
const common = { prompt: '', workdir: work, timeoutMs: 1, csoProducer: producer };
|
||||
const codexPaths = codexProducerPaths(state);
|
||||
expect(codexExecEnvironment(common, sourceEnv)).toEqual({
|
||||
...safe, HOME: codexPaths.home, GSTACK_HOME: join(state, 'cso-home'), CODEX_HOME: codexPaths.home, OPENAI_API_KEY: 'openai-auth',
|
||||
});
|
||||
const claudePaths = claudeProducerPaths(state);
|
||||
expect(claudeExecEnvironment(common, sourceEnv)).toEqual({
|
||||
...safe, HOME: claudePaths.home, GSTACK_HOME: join(state, 'cso-home'),
|
||||
ANTHROPIC_API_KEY: 'anthropic-auth', CLAUDE_CODE_OAUTH_TOKEN: 'claude-auth', CLAUDE_CODE_SUBPROCESS_ENV_SCRUB: '1',
|
||||
});
|
||||
const geminiPaths = geminiProducerPaths(state);
|
||||
expect(geminiExecEnvironment(common, sourceEnv)).toEqual({
|
||||
...safe, HOME: geminiPaths.home, GSTACK_HOME: join(state, 'cso-home'),
|
||||
GEMINI_API_KEY: 'gemini-auth', GOOGLE_API_KEY: 'gemini-auth', GOOGLE_CLOUD_PROJECT: 'project',
|
||||
GEMINI_SANDBOX: 'false', GEMINI_TELEMETRY_ENABLED: 'false', GEMINI_TELEMETRY_LOG_PROMPTS: 'false',
|
||||
GEMINI_CLI_TRUST_WORKSPACE: 'true', GEMINI_SYSTEM_MD: 'false', GEMINI_WRITE_SYSTEM_MD: 'false',
|
||||
GEMINI_CLI_HOME: geminiPaths.home,
|
||||
GEMINI_CLI_SYSTEM_DEFAULTS_PATH: geminiPaths.systemDefaults,
|
||||
GEMINI_CLI_SYSTEM_SETTINGS_PATH: geminiPaths.systemSettings,
|
||||
});
|
||||
for (const env of [codexExecEnvironment(common, sourceEnv), claudeExecEnvironment(common, sourceEnv), geminiExecEnvironment(common, sourceEnv)]) {
|
||||
expect(env.CSO_EVAL_PAID).toBeUndefined();
|
||||
expect(env.AWS_SECRET_ACCESS_KEY).toBeUndefined();
|
||||
expect(env.DOCKER_HOST).toBeUndefined();
|
||||
}
|
||||
});
|
||||
|
||||
test('preserves default provider environments and rejects producer escape-hatch arguments', () => {
|
||||
const fixtureRoot = join(tmpdir(), 'gstack-provider-default-policy');
|
||||
const state = join(fixtureRoot, 'state');
|
||||
const source = join(fixtureRoot, 'source');
|
||||
const helpers = join(fixtureRoot, 'helpers');
|
||||
const work = join(fixtureRoot, 'work');
|
||||
const sourceEnv = { PATH: '/bin', CSO_EVAL_PAID: '1', UNRELATED_SECRET: 'kept-by-default', GEMINI_API_KEY: 'key' };
|
||||
const common = { prompt: '', workdir: work, timeoutMs: 1 };
|
||||
const producer = policy(state, source, helperAt(helpers));
|
||||
expect(codexExecEnvironment(common, sourceEnv)).toBe(sourceEnv);
|
||||
expect(claudeExecEnvironment(common, sourceEnv)).toEqual({ ...sourceEnv, GSTACK_HEADLESS: '1' });
|
||||
expect(geminiExecEnvironment(common, sourceEnv)).toEqual({ ...sourceEnv, GOOGLE_API_KEY: 'key' });
|
||||
for (const invoke of [
|
||||
() => codexExecArgs({ ...common, csoProducer: producer, extraArgs: ['--unsafe'] }, 'model'),
|
||||
() => claudeExecArgs({ ...common, csoProducer: producer, extraArgs: ['--unsafe'] }, 'model'),
|
||||
() => geminiExecArgs({ ...common, csoProducer: producer, extraArgs: ['--unsafe'] }),
|
||||
]) expect(invoke).toThrow('does not accept extra provider arguments');
|
||||
});
|
||||
|
||||
test('fails paid Codex producers closed on empty or malformed success output',()=>{
|
||||
for(const raw of ['', ' \n', 'not json\n{bad', '{"type":"turn.completed","usage":{"input_tokens":9,"output_tokens":2}}']){
|
||||
expect(resultFromCodexStream(raw,{model:'gpt-test',producer:true})).toMatchObject({output:'',error:{code:'unknown',reason:'empty output from codex CLI (exit 0)'}});
|
||||
}
|
||||
expect(resultFromCodexStream('diagnostic\n{"type":"item.completed","item":{"type":"agent_message","text":"OK"}}',{producer:true}).error).toBeUndefined();
|
||||
expect(resultFromCodexStream('',{producer:false}).error).toBeUndefined();
|
||||
});
|
||||
|
||||
test('requires Claude producer JSON with a nonblank string result',()=>{
|
||||
for(const raw of ['', 'plain text', '{}', '{"result":42}', '{"result":" "}', '{"type":"result","subtype":"success","is_error":true,"result":"API Error: connection failed"}', '{"type":"result","subtype":"error_during_execution","result":"partial"}']){
|
||||
expect(resultFromClaudeOutput(raw,{model:'claude-test',producer:true})).toMatchObject({output:'',error:{code:'unknown',reason:'empty or invalid output from claude CLI (exit 0)'}});
|
||||
}
|
||||
const valid=resultFromClaudeOutput('{"result":"OK"}',{producer:true});expect(valid.output).toBe('OK');expect(valid.error).toBeUndefined();
|
||||
const legacy=resultFromClaudeOutput('plain text');expect(legacy.output).toBe('plain text');expect(legacy.error).toBeUndefined();
|
||||
});
|
||||
|
||||
test.skipIf(process.platform === 'win32')('executes the exact receipt-bound provider command despite a hostile PATH', async () => {
|
||||
const fixture = mkdtempSync(join(tmpdir(), 'cso-bound-provider-'));
|
||||
const previousPath = process.env.PATH;
|
||||
try {
|
||||
const state = join(fixture, 'state');
|
||||
const source = join(fixture, 'source');
|
||||
const installed = join(fixture, 'installed');
|
||||
const hostile = join(fixture, 'hostile');
|
||||
for (const directory of [state, source, installed, hostile]) mkdirSync(directory);
|
||||
const bound = join(installed, 'codex');
|
||||
const boundMarker = join(fixture, 'bound-ran');
|
||||
const hostileMarker = join(fixture, 'hostile-ran');
|
||||
writeFileSync(bound, `#!/bin/sh\ntouch ${JSON.stringify(boundMarker)}\nprintf '%s\\n' '{"type":"item.completed","item":{"type":"agent_message","text":"BOUND"}}'\n`, { mode: 0o755 });
|
||||
writeFileSync(join(hostile, 'codex'), `#!/bin/sh\ntouch ${JSON.stringify(hostileMarker)}\nprintf '%s\\n' '{"type":"item.completed","item":{"type":"agent_message","text":"HOSTILE"}}'\n`, { mode: 0o755 });
|
||||
process.env.PATH = `${hostile}:${previousPath ?? ''}`;
|
||||
const helper = helperAt(installed);
|
||||
const result = await new GptAdapter().run({
|
||||
prompt: 'Reply OK', workdir: fixture, timeoutMs: 5000, model: 'gpt-test',
|
||||
csoProducer: { ...policy(state, source, helper), providerCommand: { executable: bound, argsPrefix: [] } },
|
||||
});
|
||||
expect(result.output).toBe('BOUND');
|
||||
expect(existsSync(boundMarker)).toBe(true);
|
||||
expect(existsSync(hostileMarker)).toBe(false);
|
||||
expect(existsSync(codexProducerPaths(state).root)).toBe(false);
|
||||
} finally {
|
||||
if (previousPath === undefined) delete process.env.PATH;
|
||||
else process.env.PATH = previousPath;
|
||||
rmSync(fixture, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Both adapters execute these stubs, so a regression can never launch a paid CLI.
|
||||
describe.skipIf(process.platform === 'win32')('provider model selection', () => {
|
||||
beforeEach(() => {
|
||||
|
||||
Reference in New Issue
Block a user