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;
}
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 {
return Object.prototype.hasOwnProperty.call(obj, key);
}
@@ -567,8 +591,8 @@ export function summarizeCodexModelProviders(value: unknown): CodexModelProvider
return {
name,
baseUrl: asString(provider.base_url),
envKey: asString(provider.env_key),
baseUrl: redactCodexProviderBaseUrl(asString(provider.base_url)),
envKey: redactCodexProviderEnvKey(name, asString(provider.env_key)),
wireApi: asString(provider.wire_api),
requiresOpenaiAuth: provider.requires_openai_auth === true,
supportsWebsockets: provider.supports_websockets === true,
@@ -622,7 +646,10 @@ export function summarizeCodexProjectTrust(value: unknown): CodexProjectTrustDia
const project = asObject(projectValue);
const trustLevel = project ? asString(project.trust_level) : 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)
.sort((left, right) => left.path.localeCompare(right.path));
@@ -20,6 +20,7 @@ describe('api-routes remote write guard', () => {
let tempHome = '';
let originalDashboardAuthEnabled: string | undefined;
let originalCcsHome: string | undefined;
let originalCodexHome: string | undefined;
beforeAll(async () => {
const app = express();
@@ -53,8 +54,10 @@ describe('api-routes remote write guard', () => {
beforeEach(() => {
originalDashboardAuthEnabled = process.env.CCS_DASHBOARD_AUTH_ENABLED;
originalCcsHome = process.env.CCS_HOME;
originalCodexHome = process.env.CODEX_HOME;
tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-api-routes-remote-write-guard-'));
process.env.CCS_HOME = tempHome;
process.env.CODEX_HOME = path.join(tempHome, '.codex');
process.env.CCS_DASHBOARD_AUTH_ENABLED = 'false';
forcedRemoteAddress = '10.10.0.24';
});
@@ -72,6 +75,12 @@ describe('api-routes remote write guard', () => {
delete process.env.CCS_HOME;
}
if (originalCodexHome !== undefined) {
process.env.CODEX_HOME = originalCodexHome;
} else {
delete process.env.CODEX_HOME;
}
if (tempHome && fs.existsSync(tempHome)) {
fs.rmSync(tempHome, { recursive: true, force: true });
tempHome = '';
@@ -99,6 +108,44 @@ describe('api-routes remote write guard', () => {
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 () => {
const response = await fetch(`${baseUrl}/api/profiles`, {
method: 'POST',
@@ -105,11 +105,33 @@ describe('codex-dashboard-service', () => {
expect(summary.length).toBe(2);
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[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', () => {
const features = summarizeCodexFeatureFlags({
multi_agent: true,
@@ -136,11 +158,26 @@ describe('codex-dashboard-service', () => {
expect(features.disabled.map((feature) => feature.name)).toEqual(['shell_snapshot']);
expect(features.all.find((feature) => feature.name === 'custom_mode')?.state).toBe('custom');
expect(projects.length).toBe(2);
expect(projects[0].path).toBe('a');
expect(projects[0].trustLevel).toBe('trusted');
expect(servers[0].transport).toBe('streamable-http');
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 () => {
const raw = await getCodexRawConfig();
@@ -404,7 +441,7 @@ bearer_token = "secret"
expect(diagnostics.config.modelAutoCompactTokenLimit).toBe(700000);
expect(diagnostics.config.toolOutputTokenLimit).toBe(12000);
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_context_window = 800000');
expect(result.rawText).toContain('model_auto_compact_token_limit = 700000');