mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 10:16:49 +00:00
fix: redact Codex diagnostics metadata
This commit is contained in:
@@ -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');
|
||||||
|
|||||||
Reference in New Issue
Block a user