feat(codex): add dashboard control center

- add guided editors for top-level settings, trust, profiles, providers, MCP, and features

- refresh raw snapshots after patch saves to avoid stale mtime conflicts

- block structured saves while raw TOML is dirty and add route plus hook coverage
This commit is contained in:
Tam Nhu Tran committed 2026-03-29 13:14:15 -04:00
1 parent 3c52b1ab6d
commit b47aa0d28d
18 files changed
+2959 -57

No files matched your search

+30
View File
@@ -5,6 +5,7 @@ import {
CodexRawConfigValidationError,
getCodexDashboardDiagnostics,
getCodexRawConfig,
patchCodexConfig,
saveCodexRawConfig,
} from '../services/codex-dashboard-service';
@@ -56,4 +57,33 @@ router.put('/config/raw', async (req: Request, res: Response): Promise<void> =>
}
});
router.patch('/config/patch', async (req: Request, res: Response): Promise<void> => {
try {
const body = req.body ?? {};
if (typeof body.kind !== 'string' || body.kind.trim().length === 0) {
res.status(400).json({ error: 'kind is required.' });
return;
}
if (
body.expectedMtime !== undefined &&
(typeof body.expectedMtime !== 'number' || !Number.isFinite(body.expectedMtime))
) {
res.status(400).json({ error: 'expectedMtime must be a finite number when provided.' });
return;
}
res.json(await patchCodexConfig(body));
} catch (error) {
if (error instanceof CodexRawConfigValidationError) {
res.status(400).json({ error: error.message });
return;
}
if (error instanceof CodexRawConfigConflictError) {
res.status(409).json({ error: error.message, mtime: error.mtime });
return;
}
res.status(500).json({ error: (error as Error).message });
}
});
export default router;
@@ -6,6 +6,8 @@ import {
getCodexBinaryInfo,
} from '../../targets/codex-detector';
import type {
CodexConfigPatchInput,
CodexConfigPatchResult,
CodexDashboardDiagnostics,
CodexFeatureFlagDiagnostics,
CodexMcpServerDiagnostics,
@@ -18,6 +20,7 @@ import {
TomlFileConflictError,
TomlFileValidationError,
probeTomlObjectFile,
stringifyTomlObject,
writeTomlFileAtomic,
} from './compatible-cli-toml-file-service';
import { getCompatibleCliDocsReference } from './compatible-cli-docs-registry';
@@ -44,6 +47,32 @@ export {
TomlFileValidationError as CodexRawConfigValidationError,
};
const KNOWN_CODEX_FEATURES = new Set([
'apps',
'apply_patch_freeform',
'codex_hooks',
'fast_mode',
'js_repl',
'multi_agent',
'personality',
'prevent_idle_sleep',
'runtime_metrics',
'shell_snapshot',
'shell_tool',
'smart_approvals',
'unified_exec',
'undo',
'web_search',
'web_search_cached',
'web_search_request',
]);
const MODEL_REASONING_EFFORT_VALUES = new Set(['minimal', 'low', 'medium', 'high', 'xhigh']);
const APPROVAL_POLICY_VALUES = new Set(['on-request', 'never', 'untrusted']);
const SANDBOX_MODE_VALUES = new Set(['read-only', 'workspace-write', 'danger-full-access']);
const WEB_SEARCH_VALUES = new Set(['cached', 'live', 'disabled']);
const PERSONALITY_VALUES = new Set(['default', 'pragmatic', 'concise', 'direct']);
const PROJECT_TRUST_LEVEL_VALUES = new Set(['trusted', 'ask']);
function isObject(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
@@ -60,10 +89,409 @@ function asNumber(value: unknown): number | null {
return typeof value === 'number' && Number.isFinite(value) ? value : null;
}
function hasOwn(obj: Record<string, unknown>, key: string): boolean {
function hasOwn(obj: object, key: string): boolean {
return Object.prototype.hasOwnProperty.call(obj, key);
}
function isNonEmptyString(value: unknown): value is string {
return typeof value === 'string' && value.trim().length > 0;
}
function ensureObject(target: Record<string, unknown>, key: string): Record<string, unknown> {
const existing = asObject(target[key]);
if (existing) return existing;
const next: Record<string, unknown> = {};
target[key] = next;
return next;
}
function deleteIfEmpty(target: Record<string, unknown>, key: string) {
const value = asObject(target[key]);
if (value && Object.keys(value).length === 0) {
delete target[key];
}
}
function setStringField(target: Record<string, unknown>, key: string, value: unknown) {
if (!isNonEmptyString(value)) {
delete target[key];
return;
}
target[key] = value.trim();
}
function setEnumStringField(
target: Record<string, unknown>,
key: string,
value: unknown,
allowedValues: Set<string>,
label: string
) {
if (!isNonEmptyString(value)) {
delete target[key];
return;
}
const normalized = value.trim();
const currentValue = asString(target[key]);
if (!allowedValues.has(normalized) && normalized !== currentValue) {
throw new TomlFileValidationError(
`${label} must be one of: ${Array.from(allowedValues).join(', ')}.`
);
}
target[key] = normalized;
}
function setBooleanField(target: Record<string, unknown>, key: string, value: unknown) {
if (typeof value !== 'boolean') {
delete target[key];
return;
}
target[key] = value;
}
function setNumberField(
target: Record<string, unknown>,
key: string,
value: unknown,
options: { integer?: boolean; min?: number } = {}
) {
if (typeof value !== 'number' || !Number.isFinite(value)) {
delete target[key];
return;
}
if (options.integer && !Number.isInteger(value)) {
throw new TomlFileValidationError(`${key} must be an integer.`);
}
if (typeof options.min === 'number' && value < options.min) {
throw new TomlFileValidationError(`${key} must be >= ${options.min}.`);
}
target[key] = value;
}
function normalizeStringArray(value: unknown, label: string): string[] | null {
if (value === null || value === undefined) return null;
if (!Array.isArray(value)) {
throw new TomlFileValidationError(`${label} must be an array of strings.`);
}
const normalized = value
.map((entry) => (typeof entry === 'string' ? entry.trim() : ''))
.filter((entry) => entry.length > 0);
return normalized.length > 0 ? normalized : [];
}
function assertPatchableToml(fileProbe: {
diagnostics: { parseError: string | null; readError: string | null };
config: Record<string, unknown> | null;
}): Record<string, unknown> {
if (fileProbe.diagnostics.readError) {
throw new TomlFileValidationError(fileProbe.diagnostics.readError);
}
if (fileProbe.diagnostics.parseError) {
throw new TomlFileValidationError(
'config.toml contains invalid TOML. Fix the raw file before using guided controls.'
);
}
return asObject(fileProbe.config) ?? {};
}
function applyTopLevelSettingsPatch(
target: Record<string, unknown>,
values: Extract<CodexConfigPatchInput, { kind: 'top-level' }>['values']
) {
if (hasOwn(values, 'model')) setStringField(target, 'model', values.model);
if (hasOwn(values, 'modelReasoningEffort')) {
setEnumStringField(
target,
'model_reasoning_effort',
values.modelReasoningEffort,
MODEL_REASONING_EFFORT_VALUES,
'model_reasoning_effort'
);
}
if (hasOwn(values, 'modelProvider')) {
setStringField(target, 'model_provider', values.modelProvider);
}
if (hasOwn(values, 'approvalPolicy')) {
setEnumStringField(
target,
'approval_policy',
values.approvalPolicy,
APPROVAL_POLICY_VALUES,
'approval_policy'
);
}
if (hasOwn(values, 'sandboxMode')) {
setEnumStringField(
target,
'sandbox_mode',
values.sandboxMode,
SANDBOX_MODE_VALUES,
'sandbox_mode'
);
}
if (hasOwn(values, 'webSearch')) {
setEnumStringField(target, 'web_search', values.webSearch, WEB_SEARCH_VALUES, 'web_search');
}
if (hasOwn(values, 'toolOutputTokenLimit')) {
setNumberField(target, 'tool_output_token_limit', values.toolOutputTokenLimit, {
integer: true,
min: 1,
});
}
if (hasOwn(values, 'personality')) {
setEnumStringField(
target,
'personality',
values.personality,
PERSONALITY_VALUES,
'personality'
);
}
}
function applyProjectTrustPatch(
target: Record<string, unknown>,
input: Extract<CodexConfigPatchInput, { kind: 'project-trust' }>
) {
if (!isNonEmptyString(input.path)) {
throw new TomlFileValidationError('Project path is required.');
}
const expandedPath = expandPath(input.path.trim());
if (!path.isAbsolute(expandedPath)) {
throw new TomlFileValidationError('Project path must be absolute or use ~/... expansion.');
}
const canonicalPath = path.resolve(expandedPath);
const projects = ensureObject(target, 'projects');
if (!isNonEmptyString(input.trustLevel)) {
delete projects[canonicalPath];
deleteIfEmpty(target, 'projects');
return;
}
const trustLevel = input.trustLevel.trim();
if (!PROJECT_TRUST_LEVEL_VALUES.has(trustLevel)) {
throw new TomlFileValidationError(
`trust_level must be one of: ${Array.from(PROJECT_TRUST_LEVEL_VALUES).join(', ')}.`
);
}
projects[canonicalPath] = {
...(asObject(projects[canonicalPath]) ?? {}),
trust_level: trustLevel,
};
}
function applyFeaturePatch(
target: Record<string, unknown>,
input: Extract<CodexConfigPatchInput, { kind: 'feature' }>
) {
const feature = input.feature.trim();
const currentFeatures = asObject(target.features);
if (
!feature ||
(!KNOWN_CODEX_FEATURES.has(feature) && !(currentFeatures && hasOwn(currentFeatures, feature)))
) {
throw new TomlFileValidationError(`Unsupported feature key "${input.feature}".`);
}
if (input.enabled !== null && typeof input.enabled !== 'boolean') {
throw new TomlFileValidationError('Feature enabled must be boolean or null.');
}
const features = ensureObject(target, 'features');
if (input.enabled === null) {
delete features[feature];
} else {
features[feature] = input.enabled;
}
deleteIfEmpty(target, 'features');
}
function applyProfilePatch(
target: Record<string, unknown>,
input: Extract<CodexConfigPatchInput, { kind: 'profile' }>
) {
if (!isNonEmptyString(input.name)) {
throw new TomlFileValidationError('Profile name is required.');
}
const profileName = input.name.trim();
if (!['set-active', 'upsert', 'delete'].includes(input.action)) {
throw new TomlFileValidationError('Unsupported profile action.');
}
if (input.action === 'set-active') {
setStringField(target, 'profile', profileName);
return;
}
const profiles = ensureObject(target, 'profiles');
if (input.action === 'delete') {
delete profiles[profileName];
if (asString(target.profile) === profileName) {
delete target.profile;
}
deleteIfEmpty(target, 'profiles');
return;
}
const nextProfile = { ...(asObject(profiles[profileName]) ?? {}) };
applyTopLevelSettingsPatch(nextProfile, input.values ?? {});
if (Object.keys(nextProfile).length === 0) {
throw new TomlFileValidationError('Profile patch must include at least one saved field.');
}
profiles[profileName] = nextProfile;
if (input.setAsActive === true) {
target.profile = profileName;
}
}
function applyModelProviderPatch(
target: Record<string, unknown>,
input: Extract<CodexConfigPatchInput, { kind: 'model-provider' }>
) {
if (!isNonEmptyString(input.name)) {
throw new TomlFileValidationError('Model provider name is required.');
}
const providerName = input.name.trim();
const providers = ensureObject(target, 'model_providers');
if (!['upsert', 'delete'].includes(input.action)) {
throw new TomlFileValidationError('Unsupported model provider action.');
}
if (input.action === 'delete') {
delete providers[providerName];
if (asString(target.model_provider) === providerName) {
delete target.model_provider;
}
deleteIfEmpty(target, 'model_providers');
return;
}
const values = input.values;
if (!values) {
throw new TomlFileValidationError('Model provider values are required.');
}
const nextProvider = { ...(asObject(providers[providerName]) ?? {}) };
if (hasOwn(values, 'displayName')) setStringField(nextProvider, 'name', values.displayName);
if (hasOwn(values, 'baseUrl')) setStringField(nextProvider, 'base_url', values.baseUrl);
if (hasOwn(values, 'envKey')) setStringField(nextProvider, 'env_key', values.envKey);
if (hasOwn(values, 'wireApi')) {
if (values.wireApi !== null && values.wireApi !== undefined && values.wireApi !== 'responses') {
throw new TomlFileValidationError('wire_api must be "responses" for Codex model providers.');
}
setStringField(nextProvider, 'wire_api', values.wireApi);
}
if (hasOwn(values, 'requiresOpenaiAuth')) {
setBooleanField(nextProvider, 'requires_openai_auth', values.requiresOpenaiAuth);
}
if (hasOwn(values, 'supportsWebsockets')) {
setBooleanField(nextProvider, 'supports_websockets', values.supportsWebsockets);
}
if (Object.keys(nextProvider).length === 0) {
throw new TomlFileValidationError(
'Model provider patch must include at least one saved field.'
);
}
providers[providerName] = nextProvider;
}
function applyMcpServerPatch(
target: Record<string, unknown>,
input: Extract<CodexConfigPatchInput, { kind: 'mcp-server' }>
) {
if (!isNonEmptyString(input.name)) {
throw new TomlFileValidationError('MCP server name is required.');
}
const serverName = input.name.trim();
const servers = ensureObject(target, 'mcp_servers');
if (!['upsert', 'delete'].includes(input.action)) {
throw new TomlFileValidationError('Unsupported MCP server action.');
}
if (input.action === 'delete') {
delete servers[serverName];
deleteIfEmpty(target, 'mcp_servers');
return;
}
const values = input.values;
if (!values) {
throw new TomlFileValidationError('MCP server values are required.');
}
if (values.transport !== 'stdio' && values.transport !== 'streamable-http') {
throw new TomlFileValidationError('MCP transport must be "stdio" or "streamable-http".');
}
const nextServer = { ...(asObject(servers[serverName]) ?? {}) };
if (values.transport === 'stdio') {
if (!isNonEmptyString(values.command)) {
throw new TomlFileValidationError('Stdio MCP servers require a command.');
}
nextServer.command = values.command.trim();
const nextArgs = normalizeStringArray(values.args, 'args');
if (nextArgs === null) {
delete nextServer.args;
} else {
nextServer.args = nextArgs;
}
delete nextServer.url;
} else {
if (!isNonEmptyString(values.url)) {
throw new TomlFileValidationError('HTTP MCP servers require a URL.');
}
nextServer.url = values.url.trim();
delete nextServer.command;
delete nextServer.args;
}
if (hasOwn(values, 'enabled')) setBooleanField(nextServer, 'enabled', values.enabled);
if (hasOwn(values, 'required')) setBooleanField(nextServer, 'required', values.required);
if (hasOwn(values, 'startupTimeoutSec')) {
setNumberField(nextServer, 'startup_timeout_sec', values.startupTimeoutSec, {
integer: true,
min: 1,
});
}
if (hasOwn(values, 'toolTimeoutSec')) {
setNumberField(nextServer, 'tool_timeout_sec', values.toolTimeoutSec, {
integer: true,
min: 1,
});
}
if (hasOwn(values, 'enabledTools')) {
const nextEnabledTools = normalizeStringArray(values.enabledTools, 'enabledTools');
if (nextEnabledTools === null) {
delete nextServer.enabled_tools;
} else {
nextServer.enabled_tools = nextEnabledTools;
}
}
if (hasOwn(values, 'disabledTools')) {
const nextDisabledTools = normalizeStringArray(values.disabledTools, 'disabledTools');
if (nextDisabledTools === null) {
delete nextServer.disabled_tools;
} else {
nextServer.disabled_tools = nextDisabledTools;
}
}
servers[serverName] = nextServer;
}
function parseTransport(server: Record<string, unknown>): CodexMcpServerDiagnostics['transport'] {
if (asString(server.command)) return 'stdio';
if (asString(server.url)) return 'streamable-http';
@@ -294,13 +722,17 @@ export async function getCodexDashboardDiagnostics(): Promise<CodexDashboardDiag
supportsConfigOverrides: codexBinarySupportsConfigOverrides(binaryInfo),
},
file: fileProbe.diagnostics,
workspacePath: process.cwd(),
config: {
model: asString(config?.model),
modelReasoningEffort: asString(config?.model_reasoning_effort),
modelProvider: asString(config?.model_provider),
activeProfile,
approvalPolicy: asString(config?.approval_policy),
sandboxMode: asString(config?.sandbox_mode),
webSearch: asString(config?.web_search),
toolOutputTokenLimit: asNumber(config?.tool_output_token_limit),
personality: asString(config?.personality),
topLevelKeys,
profileCount: profileNames.length,
profileNames,
@@ -357,3 +789,57 @@ export async function saveCodexRawConfig(
return { success: true, mtime: saved.mtime };
}
export async function patchCodexConfig(
input: CodexConfigPatchInput
): Promise<CodexConfigPatchResult> {
const paths = resolveCodexConfigPaths();
const fileProbe = await probeTomlObjectFile(
paths.configPath,
'Codex user config',
paths.configDisplayPath
);
const nextConfig = { ...assertPatchableToml(fileProbe) };
switch (input.kind) {
case 'top-level':
applyTopLevelSettingsPatch(nextConfig, input.values);
break;
case 'project-trust':
applyProjectTrustPatch(nextConfig, input);
break;
case 'feature':
applyFeaturePatch(nextConfig, input);
break;
case 'profile':
applyProfilePatch(nextConfig, input);
break;
case 'model-provider':
applyModelProviderPatch(nextConfig, input);
break;
case 'mcp-server':
applyMcpServerPatch(nextConfig, input);
break;
default:
throw new TomlFileValidationError('Unsupported Codex config patch.');
}
const rawText = stringifyTomlObject(nextConfig);
const saved = await writeTomlFileAtomic({
filePath: paths.configPath,
rawText,
expectedMtime: input.expectedMtime,
fileLabel: 'config.toml',
});
return {
success: true,
path: paths.configDisplayPath,
resolvedPath: paths.configPath,
exists: true,
mtime: saved.mtime,
rawText,
config: nextConfig,
parseError: null,
};
}
@@ -1,6 +1,6 @@
import { promises as fs } from 'fs';
import * as path from 'path';
import { parse } from 'smol-toml';
import { parse, stringify } from 'smol-toml';
export interface TomlFileDiagnostics {
label: string;
@@ -92,6 +92,15 @@ export function parseTomlObjectText(
return parsed;
}
export function stringifyTomlObject(config: Record<string, unknown>): string {
if (!isObject(config)) {
throw new TomlFileValidationError('config TOML root must be a table.');
}
const text = stringify(config).trimEnd();
return text ? `${text}\n` : '';
}
export async function probeTomlObjectFile(
filePath: string,
label: string,
@@ -146,11 +146,14 @@ export interface CodexSupportMatrixEntry {
export interface CodexUserConfigDiagnostics {
model: string | null;
modelReasoningEffort: string | null;
modelProvider: string | null;
activeProfile: string | null;
approvalPolicy: string | null;
sandboxMode: string | null;
webSearch: string | null;
toolOutputTokenLimit: number | null;
personality: string | null;
topLevelKeys: string[];
profileCount: number;
profileNames: string[];
@@ -169,6 +172,7 @@ export interface CodexUserConfigDiagnostics {
export interface CodexDashboardDiagnostics {
binary: CodexBinaryDiagnostics;
file: CodexConfigFileDiagnostics;
workspacePath: string;
config: CodexUserConfigDiagnostics;
supportMatrix: CodexSupportMatrixEntry[];
warnings: string[];
@@ -184,3 +188,83 @@ export interface CodexRawConfigResponse {
config: Record<string, unknown> | null;
parseError: string | null;
}
export interface CodexTopLevelSettingsPatch {
model?: string | null;
modelReasoningEffort?: string | null;
modelProvider?: string | null;
approvalPolicy?: string | null;
sandboxMode?: string | null;
webSearch?: string | null;
toolOutputTokenLimit?: number | null;
personality?: string | null;
}
export interface CodexProfilePatchValues extends CodexTopLevelSettingsPatch {}
export interface CodexModelProviderPatchValues {
displayName?: string | null;
baseUrl?: string | null;
envKey?: string | null;
wireApi?: string | null;
requiresOpenaiAuth?: boolean | null;
supportsWebsockets?: boolean | null;
}
export interface CodexMcpServerPatchValues {
transport: 'stdio' | 'streamable-http';
command?: string | null;
args?: string[] | null;
url?: string | null;
enabled?: boolean | null;
required?: boolean | null;
startupTimeoutSec?: number | null;
toolTimeoutSec?: number | null;
enabledTools?: string[] | null;
disabledTools?: string[] | null;
}
export type CodexConfigPatchInput =
| {
kind: 'top-level';
expectedMtime?: number;
values: CodexTopLevelSettingsPatch;
}
| {
kind: 'project-trust';
expectedMtime?: number;
path: string;
trustLevel: string | null;
}
| {
kind: 'feature';
expectedMtime?: number;
feature: string;
enabled: boolean | null;
}
| {
kind: 'profile';
expectedMtime?: number;
action: 'set-active' | 'upsert' | 'delete';
name: string;
values?: CodexProfilePatchValues;
setAsActive?: boolean;
}
| {
kind: 'model-provider';
expectedMtime?: number;
action: 'upsert' | 'delete';
name: string;
values?: CodexModelProviderPatchValues;
}
| {
kind: 'mcp-server';
expectedMtime?: number;
action: 'upsert' | 'delete';
name: string;
values?: CodexMcpServerPatchValues;
};
export interface CodexConfigPatchResult extends CodexRawConfigResponse {
success: true;
}