mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-02 08:19:59 +00:00
feat: share stale Codex translator path scanner
This commit is contained in:
@@ -18,6 +18,11 @@ import ProfileDetector from '../auth/profile-detector';
|
|||||||
import { getClaudeConfigDir, getClaudeSettingsPath } from '../utils/claude-config-path';
|
import { getClaudeConfigDir, getClaudeSettingsPath } from '../utils/claude-config-path';
|
||||||
import { extractOption, hasAnyFlag } from './arg-extractor';
|
import { extractOption, hasAnyFlag } from './arg-extractor';
|
||||||
import { resolveClaudeExtensionSetup } from '../shared/claude-extension-setup';
|
import { resolveClaudeExtensionSetup } from '../shared/claude-extension-setup';
|
||||||
|
import {
|
||||||
|
CODEX_TRANSLATOR_URL_MARKER,
|
||||||
|
findCodexTranslatorUrlPaths,
|
||||||
|
formatSettingsPathList,
|
||||||
|
} from '../shared/stale-codex-translator-settings';
|
||||||
|
|
||||||
interface PersistCommandArgs {
|
interface PersistCommandArgs {
|
||||||
profile?: string;
|
profile?: string;
|
||||||
@@ -64,7 +69,6 @@ const PERSIST_LOCK_STALE_MS = 10000;
|
|||||||
const PERSIST_LOCK_RETRIES = 5;
|
const PERSIST_LOCK_RETRIES = 5;
|
||||||
const PERSIST_LOCK_RETRY_MIN_MS = 100;
|
const PERSIST_LOCK_RETRY_MIN_MS = 100;
|
||||||
const PERSIST_LOCK_RETRY_MAX_MS = 500;
|
const PERSIST_LOCK_RETRY_MAX_MS = 500;
|
||||||
const CODEX_TRANSLATOR_URL_MARKER = '/api/provider/codex';
|
|
||||||
const NATIVE_CODEX_TARGETS = ['ccsxp', 'ccs codex --target codex'];
|
const NATIVE_CODEX_TARGETS = ['ccsxp', 'ccs codex --target codex'];
|
||||||
|
|
||||||
type PermissionMode = (typeof VALID_PERMISSION_MODES)[number];
|
type PermissionMode = (typeof VALID_PERMISSION_MODES)[number];
|
||||||
@@ -521,38 +525,6 @@ function isSensitiveEnvKey(key: string): boolean {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatSettingsPathSegment(basePath: string, segment: string | number): string {
|
|
||||||
if (typeof segment === 'number') {
|
|
||||||
return `${basePath}[${segment}]`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(segment)) {
|
|
||||||
return basePath ? `${basePath}.${segment}` : segment;
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${basePath}[${JSON.stringify(segment)}]`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function findCodexTranslatorUrlPaths(value: unknown, path = ''): string[] {
|
|
||||||
if (typeof value === 'string') {
|
|
||||||
return value.includes(CODEX_TRANSLATOR_URL_MARKER) ? [path || '(root)'] : [];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
return value.flatMap((item, index) =>
|
|
||||||
findCodexTranslatorUrlPaths(item, formatSettingsPathSegment(path, index))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof value === 'object' && value !== null) {
|
|
||||||
return Object.entries(value).flatMap(([key, item]) =>
|
|
||||||
findCodexTranslatorUrlPaths(item, formatSettingsPathSegment(path, key))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildPersistReceipt(
|
function buildPersistReceipt(
|
||||||
existingEnv: Record<string, string>,
|
existingEnv: Record<string, string>,
|
||||||
existingSettings: Record<string, unknown>,
|
existingSettings: Record<string, unknown>,
|
||||||
@@ -611,14 +583,6 @@ function formatKeyList(keys: string[]): string {
|
|||||||
return keys.length > 0 ? keys.join(', ') : 'none';
|
return keys.length > 0 ? keys.join(', ') : 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatSettingsPathList(paths: string[]): string {
|
|
||||||
const visiblePaths = paths.slice(0, 5);
|
|
||||||
const remainingCount = paths.length - visiblePaths.length;
|
|
||||||
return remainingCount > 0
|
|
||||||
? `${visiblePaths.join(', ')} (+${remainingCount} more)`
|
|
||||||
: visiblePaths.join(', ');
|
|
||||||
}
|
|
||||||
|
|
||||||
function printPersistReceipt(receipt: PersistReceipt): void {
|
function printPersistReceipt(receipt: PersistReceipt): void {
|
||||||
console.log(subheader('Config Receipt'));
|
console.log(subheader('Config Receipt'));
|
||||||
console.log(` Settings: ${getClaudeSettingsDisplayPath()}`);
|
console.log(` Settings: ${getClaudeSettingsDisplayPath()}`);
|
||||||
|
|||||||
@@ -9,6 +9,11 @@ import { HealthCheck, IHealthChecker, createSpinner } from './types';
|
|||||||
|
|
||||||
import { getClaudeConfigDir } from '../../utils/claude-config-path';
|
import { getClaudeConfigDir } from '../../utils/claude-config-path';
|
||||||
import { getCcsDir } from '../../config/config-loader-facade';
|
import { getCcsDir } from '../../config/config-loader-facade';
|
||||||
|
import {
|
||||||
|
CODEX_TRANSLATOR_URL_MARKER,
|
||||||
|
findCodexTranslatorUrlPaths,
|
||||||
|
formatSettingsPathList,
|
||||||
|
} from '../../shared/stale-codex-translator-settings';
|
||||||
|
|
||||||
const ora = createSpinner();
|
const ora = createSpinner();
|
||||||
|
|
||||||
@@ -219,20 +224,18 @@ export class ClaudeSettingsChecker implements IHealthChecker {
|
|||||||
// Validate JSON
|
// Validate JSON
|
||||||
try {
|
try {
|
||||||
const content = fs.readFileSync(settingsPath, 'utf8');
|
const content = fs.readFileSync(settingsPath, 'utf8');
|
||||||
const settings = JSON.parse(content) as {
|
const settings = JSON.parse(content) as unknown;
|
||||||
env?: Record<string, unknown>;
|
const codexTranslatorUrlPaths = findCodexTranslatorUrlPaths(settings);
|
||||||
};
|
if (codexTranslatorUrlPaths.length > 0) {
|
||||||
const baseUrl =
|
const formattedPaths = formatSettingsPathList(codexTranslatorUrlPaths);
|
||||||
settings.env && typeof settings.env.ANTHROPIC_BASE_URL === 'string'
|
|
||||||
? settings.env.ANTHROPIC_BASE_URL
|
|
||||||
: '';
|
|
||||||
if (baseUrl.includes('/api/provider/codex')) {
|
|
||||||
spinner.warn();
|
spinner.warn();
|
||||||
console.log(` ${warn(settingsName.padEnd(22))} Codex CLIProxy bridge persisted`);
|
console.log(
|
||||||
|
` ${warn(settingsName.padEnd(22))} Codex CLIProxy bridge persisted at ${formattedPaths}`
|
||||||
|
);
|
||||||
results.addCheck(
|
results.addCheck(
|
||||||
'Claude Settings',
|
'Claude Settings',
|
||||||
'warning',
|
'warning',
|
||||||
'Claude settings route Claude Code through the Codex CLIProxy translator',
|
`Claude settings route Claude Code through the Codex CLIProxy translator at ${formattedPaths} (${CODEX_TRANSLATOR_URL_MARKER})`,
|
||||||
'Run: ccs persist default --yes; use ccsxp or ccs codex --target codex for Codex'
|
'Run: ccs persist default --yes; use ccsxp or ccs codex --target codex for Codex'
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
export const CODEX_TRANSLATOR_URL_MARKER = '/api/provider/codex';
|
||||||
|
|
||||||
|
function formatSettingsPathSegment(basePath: string, segment: string | number): string {
|
||||||
|
if (typeof segment === 'number') {
|
||||||
|
return `${basePath}[${segment}]`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(segment)) {
|
||||||
|
return basePath ? `${basePath}.${segment}` : segment;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${basePath}[${JSON.stringify(segment)}]`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function findCodexTranslatorUrlPaths(value: unknown, path = ''): string[] {
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
return value.includes(CODEX_TRANSLATOR_URL_MARKER) ? [path || '(root)'] : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value.flatMap((item, index) =>
|
||||||
|
findCodexTranslatorUrlPaths(item, formatSettingsPathSegment(path, index))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === 'object' && value !== null) {
|
||||||
|
return Object.entries(value).flatMap(([key, item]) =>
|
||||||
|
findCodexTranslatorUrlPaths(item, formatSettingsPathSegment(path, key))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatSettingsPathList(paths: string[]): string {
|
||||||
|
const visiblePaths = paths.slice(0, 5);
|
||||||
|
const remainingCount = paths.length - visiblePaths.length;
|
||||||
|
return remainingCount > 0
|
||||||
|
? `${visiblePaths.join(', ')} (+${remainingCount} more)`
|
||||||
|
: visiblePaths.join(', ');
|
||||||
|
}
|
||||||
@@ -57,4 +57,61 @@ describe('ClaudeSettingsChecker', () => {
|
|||||||
fs.rmSync(tempRoot, { recursive: true, force: true });
|
fs.rmSync(tempRoot, { recursive: true, force: true });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('warns with safe path-level evidence for nested Codex translator settings', async () => {
|
||||||
|
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-config-check-'));
|
||||||
|
originalClaudeConfigDir = process.env.CLAUDE_CONFIG_DIR;
|
||||||
|
delete process.env.CLAUDE_CONFIG_DIR;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await runWithScopedCcsHome(tempRoot, async () => {
|
||||||
|
const staleUrl = 'https://proxy.example.com/api/provider/codex/v1/messages';
|
||||||
|
const settingsPath = path.join(tempRoot, '.claude', 'settings.json');
|
||||||
|
fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
|
||||||
|
fs.writeFileSync(
|
||||||
|
settingsPath,
|
||||||
|
JSON.stringify(
|
||||||
|
{
|
||||||
|
env: {
|
||||||
|
KEEP_ME: 'safe',
|
||||||
|
},
|
||||||
|
hooks: {
|
||||||
|
PostToolUse: [
|
||||||
|
{
|
||||||
|
config: {
|
||||||
|
endpoint: staleUrl,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
) + '\n'
|
||||||
|
);
|
||||||
|
|
||||||
|
const originalConsoleLog = console.log;
|
||||||
|
const capturedLogs: string[] = [];
|
||||||
|
console.log = (...args: unknown[]) => {
|
||||||
|
capturedLogs.push(args.map((arg) => String(arg)).join(' '));
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
const results = new HealthCheck();
|
||||||
|
new ClaudeSettingsChecker().run(results);
|
||||||
|
|
||||||
|
expect(results.warnings).toHaveLength(1);
|
||||||
|
expect(results.warnings[0].message).toContain(
|
||||||
|
'hooks.PostToolUse[0].config.endpoint'
|
||||||
|
);
|
||||||
|
expect(results.warnings[0].message).not.toContain(staleUrl);
|
||||||
|
expect(capturedLogs.join('\n')).toContain('hooks.PostToolUse[0].config.endpoint');
|
||||||
|
expect(capturedLogs.join('\n')).not.toContain(staleUrl);
|
||||||
|
} finally {
|
||||||
|
console.log = originalConsoleLog;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(tempRoot, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { describe, expect, it } from 'bun:test';
|
||||||
|
|
||||||
|
import { findCodexTranslatorUrlPaths } from '../../../src/shared/stale-codex-translator-settings';
|
||||||
|
|
||||||
|
describe('stale Codex translator settings scanner', () => {
|
||||||
|
it('reports nested object and array paths without returning sensitive values', () => {
|
||||||
|
const settings = {
|
||||||
|
env: {
|
||||||
|
SAFE_VALUE: 'keep-me',
|
||||||
|
ANTHROPIC_BASE_URL: 'http://127.0.0.1:8317/api/provider/codex',
|
||||||
|
},
|
||||||
|
hooks: {
|
||||||
|
PostToolUse: [
|
||||||
|
{
|
||||||
|
matcher: 'Read',
|
||||||
|
config: {
|
||||||
|
headers: {
|
||||||
|
Authorization: 'Bearer secret',
|
||||||
|
},
|
||||||
|
endpoint: 'https://proxy.example.com/api/provider/codex/v1/messages',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
'custom-key': ['https://proxy.example.com/api/provider/codex'],
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(findCodexTranslatorUrlPaths(settings)).toEqual([
|
||||||
|
'env.ANTHROPIC_BASE_URL',
|
||||||
|
'hooks.PostToolUse[0].config.endpoint',
|
||||||
|
'["custom-key"][0]',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user