fix: redact Codex diagnostics metadata

This commit is contained in:
Tam Nhu Tran
2026-05-22 11:12:20 -04:00
parent e7fed040ee
commit e2f2c7dc2e
3 changed files with 116 additions and 5 deletions
@@ -90,6 +90,30 @@ function asNumber(value: unknown): number | null {
return typeof value === 'number' && Number.isFinite(value) ? value : null; return typeof value === 'number' && Number.isFinite(value) ? value : null;
} }
function redactCodexProviderBaseUrl(baseUrl: string | null): string | null {
if (!baseUrl) return null;
try {
const parsed = new URL(baseUrl);
const scheme = parsed.protocol.replace(/:$/, '') || 'url';
return `[redacted:${scheme}]`;
} catch {
return '[redacted:url]';
}
}
function redactCodexProviderEnvKey(name: string, envKey: string | null): string | null {
if (!envKey) return null;
return isBuiltInCodexModelProvider(name) ? envKey : '[set]';
}
function redactCodexProjectPath(projectPath: string): string {
const normalized = projectPath.trim().replace(/[\\/]+$/, '');
if (!normalized) return '[unknown]';
const basename = normalized.split(/[\\/]/).filter(Boolean).pop();
return basename || '[root]';
}
function hasOwn(obj: object, key: string): boolean { function hasOwn(obj: object, key: string): boolean {
return Object.prototype.hasOwnProperty.call(obj, key); return Object.prototype.hasOwnProperty.call(obj, key);
} }
@@ -567,8 +591,8 @@ export function summarizeCodexModelProviders(value: unknown): CodexModelProvider
return { return {
name, name,
baseUrl: asString(provider.base_url), baseUrl: redactCodexProviderBaseUrl(asString(provider.base_url)),
envKey: asString(provider.env_key), envKey: redactCodexProviderEnvKey(name, asString(provider.env_key)),
wireApi: asString(provider.wire_api), wireApi: asString(provider.wire_api),
requiresOpenaiAuth: provider.requires_openai_auth === true, requiresOpenaiAuth: provider.requires_openai_auth === true,
supportsWebsockets: provider.supports_websockets === true, supportsWebsockets: provider.supports_websockets === true,
@@ -622,7 +646,10 @@ export function summarizeCodexProjectTrust(value: unknown): CodexProjectTrustDia
const project = asObject(projectValue); const project = asObject(projectValue);
const trustLevel = project ? asString(project.trust_level) : null; const trustLevel = project ? asString(project.trust_level) : null;
if (!trustLevel) return null; if (!trustLevel) return null;
return { path: projectPath, trustLevel } satisfies CodexProjectTrustDiagnostics; return {
path: redactCodexProjectPath(projectPath),
trustLevel,
} satisfies CodexProjectTrustDiagnostics;
}) })
.filter((project): project is CodexProjectTrustDiagnostics => project !== null) .filter((project): project is CodexProjectTrustDiagnostics => project !== null)
.sort((left, right) => left.path.localeCompare(right.path)); .sort((left, right) => left.path.localeCompare(right.path));
@@ -20,6 +20,7 @@ describe('api-routes remote write guard', () => {
let tempHome = ''; let tempHome = '';
let originalDashboardAuthEnabled: string | undefined; let originalDashboardAuthEnabled: string | undefined;
let originalCcsHome: string | undefined; let originalCcsHome: string | undefined;
let originalCodexHome: string | undefined;
beforeAll(async () => { beforeAll(async () => {
const app = express(); const app = express();
@@ -53,8 +54,10 @@ describe('api-routes remote write guard', () => {
beforeEach(() => { beforeEach(() => {
originalDashboardAuthEnabled = process.env.CCS_DASHBOARD_AUTH_ENABLED; originalDashboardAuthEnabled = process.env.CCS_DASHBOARD_AUTH_ENABLED;
originalCcsHome = process.env.CCS_HOME; originalCcsHome = process.env.CCS_HOME;
originalCodexHome = process.env.CODEX_HOME;
tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-api-routes-remote-write-guard-')); tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-api-routes-remote-write-guard-'));
process.env.CCS_HOME = tempHome; process.env.CCS_HOME = tempHome;
process.env.CODEX_HOME = path.join(tempHome, '.codex');
process.env.CCS_DASHBOARD_AUTH_ENABLED = 'false'; process.env.CCS_DASHBOARD_AUTH_ENABLED = 'false';
forcedRemoteAddress = '10.10.0.24'; forcedRemoteAddress = '10.10.0.24';
}); });
@@ -72,6 +75,12 @@ describe('api-routes remote write guard', () => {
delete process.env.CCS_HOME; delete process.env.CCS_HOME;
} }
if (originalCodexHome !== undefined) {
process.env.CODEX_HOME = originalCodexHome;
} else {
delete process.env.CODEX_HOME;
}
if (tempHome && fs.existsSync(tempHome)) { if (tempHome && fs.existsSync(tempHome)) {
fs.rmSync(tempHome, { recursive: true, force: true }); fs.rmSync(tempHome, { recursive: true, force: true });
tempHome = ''; tempHome = '';
@@ -99,6 +108,44 @@ describe('api-routes remote write guard', () => {
expect(response.status).toBe(200); expect(response.status).toBe(200);
}); });
it('redacts sensitive Codex diagnostics data while preserving remote access', async () => {
const codexHome = process.env.CODEX_HOME;
if (!codexHome) throw new Error('CODEX_HOME was not initialized');
fs.mkdirSync(codexHome, { recursive: true });
fs.writeFileSync(
path.join(codexHome, 'config.toml'),
`model_provider = "private"
[model_providers.private]
base_url = "https://llm.internal.example.test/v1/responses?tenant=alpha"
env_key = "PRIVATE_PROVIDER_TOKEN"
wire_api = "responses"
[projects."/Users/someone/CloudPersonal/private-workspace"]
trust_level = "trusted"
`
);
const response = await fetch(`${baseUrl}/api/codex/diagnostics`);
const body = await response.json();
const serialized = JSON.stringify(body);
expect(response.status).toBe(200);
expect(body.config.projectTrust).toEqual([
{ path: 'private-workspace', trustLevel: 'trusted' },
]);
expect(body.config.modelProviders).toEqual([
expect.objectContaining({
name: 'private',
baseUrl: '[redacted:https]',
envKey: '[set]',
}),
]);
expect(serialized).not.toContain('/Users/someone');
expect(serialized).not.toContain('llm.internal.example.test');
expect(serialized).not.toContain('PRIVATE_PROVIDER_TOKEN');
});
it('blocks remote profile creation when dashboard auth is disabled', async () => { it('blocks remote profile creation when dashboard auth is disabled', async () => {
const response = await fetch(`${baseUrl}/api/profiles`, { const response = await fetch(`${baseUrl}/api/profiles`, {
method: 'POST', method: 'POST',
@@ -105,11 +105,33 @@ describe('codex-dashboard-service', () => {
expect(summary.length).toBe(2); expect(summary.length).toBe(2);
expect(summary[0].name).toBe('cliproxy'); expect(summary[0].name).toBe('cliproxy');
expect(summary[0].envKey).toBe('CLIPROXY_API_KEY'); expect(summary[0].baseUrl).toBe('[redacted:http]');
expect(summary[0].envKey).toBe('[set]');
expect(summary[0].hasHttpHeaders).toBe(true); expect(summary[0].hasHttpHeaders).toBe(true);
expect(summary[1].usesExperimentalBearerToken).toBe(true); expect(summary[1].usesExperimentalBearerToken).toBe(true);
}); });
it('redacts custom provider URLs and env var names from diagnostics summaries', () => {
const summary = summarizeCodexModelProviders({
private: {
base_url: 'https://llm.internal.example.test/v1/responses?tenant=alpha',
env_key: 'PRIVATE_PROVIDER_TOKEN',
wire_api: 'responses',
},
});
expect(summary).toEqual([
expect.objectContaining({
name: 'private',
baseUrl: '[redacted:https]',
envKey: '[set]',
wireApi: 'responses',
}),
]);
expect(JSON.stringify(summary)).not.toContain('llm.internal.example.test');
expect(JSON.stringify(summary)).not.toContain('PRIVATE_PROVIDER_TOKEN');
});
it('summarizes feature flags, project trust, and mcp servers', () => { it('summarizes feature flags, project trust, and mcp servers', () => {
const features = summarizeCodexFeatureFlags({ const features = summarizeCodexFeatureFlags({
multi_agent: true, multi_agent: true,
@@ -136,11 +158,26 @@ describe('codex-dashboard-service', () => {
expect(features.disabled.map((feature) => feature.name)).toEqual(['shell_snapshot']); expect(features.disabled.map((feature) => feature.name)).toEqual(['shell_snapshot']);
expect(features.all.find((feature) => feature.name === 'custom_mode')?.state).toBe('custom'); expect(features.all.find((feature) => feature.name === 'custom_mode')?.state).toBe('custom');
expect(projects.length).toBe(2); expect(projects.length).toBe(2);
expect(projects[0].path).toBe('a');
expect(projects[0].trustLevel).toBe('trusted'); expect(projects[0].trustLevel).toBe('trusted');
expect(servers[0].transport).toBe('streamable-http'); expect(servers[0].transport).toBe('streamable-http');
expect(servers[0].usesInlineBearerToken).toBe(true); expect(servers[0].usesInlineBearerToken).toBe(true);
}); });
it('redacts project trust paths to basenames in diagnostics summaries', () => {
const summary = summarizeCodexProjectTrust({
'/Users/someone/CloudPersonal/private-workspace': { trust_level: 'trusted' },
'/var/tmp/other-workspace/': { trust_level: 'untrusted' },
});
expect(summary).toEqual([
{ path: 'other-workspace', trustLevel: 'untrusted' },
{ path: 'private-workspace', trustLevel: 'trusted' },
]);
expect(JSON.stringify(summary)).not.toContain('/Users/someone');
expect(JSON.stringify(summary)).not.toContain('/var/tmp');
});
it('returns raw config payload for missing config.toml', async () => { it('returns raw config payload for missing config.toml', async () => {
const raw = await getCodexRawConfig(); const raw = await getCodexRawConfig();
@@ -404,7 +441,7 @@ bearer_token = "secret"
expect(diagnostics.config.modelAutoCompactTokenLimit).toBe(700000); expect(diagnostics.config.modelAutoCompactTokenLimit).toBe(700000);
expect(diagnostics.config.toolOutputTokenLimit).toBe(12000); expect(diagnostics.config.toolOutputTokenLimit).toBe(12000);
expect(diagnostics.config.personality).toBe('friendly'); expect(diagnostics.config.personality).toBe('friendly');
expect(diagnostics.config.projectTrust[0]?.path).toBe('/tmp/workspace-a'); expect(diagnostics.config.projectTrust[0]?.path).toBe('workspace-a');
expect(result.rawText).toContain('model = "gpt-5.4"'); expect(result.rawText).toContain('model = "gpt-5.4"');
expect(result.rawText).toContain('model_context_window = 800000'); expect(result.rawText).toContain('model_context_window = 800000');
expect(result.rawText).toContain('model_auto_compact_token_limit = 700000'); expect(result.rawText).toContain('model_auto_compact_token_limit = 700000');