mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 04:18:05 +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;
|
||||
}
|
||||
|
||||
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');
|
||||
|
||||
Reference in New Issue
Block a user