Skip to content

Commit 32802b1

Browse files
committed
feat(web-ui): gate provider config writes
1 parent d8e553a commit 32802b1

17 files changed

Lines changed: 1623 additions & 886 deletions

cli.js

Lines changed: 167 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,7 @@ const CLAUDE_PROJECTS_DIR = path.join(os.homedir(), '.claude', 'projects');
213213
const CODEBUDDY_DIR = path.join(os.homedir(), '.codebuddy');
214214
const CODEBUDDY_PROJECTS_DIR = path.join(CODEBUDDY_DIR, 'projects');
215215
const CODEXMATE_DIR = path.join(os.homedir(), '.codexmate');
216+
const CODEXMATE_PREFERENCES_FILE = path.join(CODEXMATE_DIR, 'preferences.json');
216217
const CODEXMATE_SESSIONS_DIR = path.join(CODEXMATE_DIR, 'sessions');
217218
const CODEXMATE_DERIVED_SESSIONS_DIR = path.join(CODEXMATE_SESSIONS_DIR, 'derived');
218219
const CODEXMATE_DERIVED_CODEX_DIR = path.join(CODEXMATE_DERIVED_SESSIONS_DIR, 'codex');
@@ -717,6 +718,7 @@ function readConfig() {
717718
}
718719

719720
function writeConfig(content) {
721+
assertToolConfigWriteAllowed('codex');
720722
try {
721723
fs.writeFileSync(CONFIG_FILE, content, 'utf-8');
722724
} catch (e) {
@@ -734,6 +736,7 @@ function readModels() {
734736
}
735737

736738
function writeModels(models) {
739+
assertToolConfigWriteAllowed('codex');
737740
fs.writeFileSync(MODELS_FILE, JSON.stringify(models, null, 2), 'utf-8');
738741
}
739742

@@ -747,10 +750,12 @@ function readCurrentModels() {
747750
}
748751

749752
function writeCurrentModels(data) {
753+
assertToolConfigWriteAllowed('codex');
750754
fs.writeFileSync(CURRENT_MODELS_FILE, JSON.stringify(data, null, 2), 'utf-8');
751755
}
752756

753757
function updateAuthJson(apiKey) {
758+
assertToolConfigWriteAllowed('codex');
754759
let authData = {};
755760
if (fs.existsSync(AUTH_FILE)) {
756761
try {
@@ -766,6 +771,141 @@ function isPlainObject(value) {
766771
return !!value && typeof value === 'object' && !Array.isArray(value);
767772
}
768773

774+
const TOOL_CONFIG_PERMISSION_TARGETS = new Set(['codex', 'claude']);
775+
const TOOL_CONFIG_PERMISSION_DEFAULTS = Object.freeze({ codex: false, claude: false });
776+
let toolConfigWriteGuardDepth = 0;
777+
778+
function enterToolConfigWriteGuard() {
779+
toolConfigWriteGuardDepth += 1;
780+
let active = true;
781+
return () => {
782+
if (!active) return;
783+
active = false;
784+
toolConfigWriteGuardDepth = Math.max(0, toolConfigWriteGuardDepth - 1);
785+
};
786+
}
787+
788+
function isToolConfigWriteGuardActive() {
789+
return toolConfigWriteGuardDepth > 0;
790+
}
791+
792+
function normalizeToolConfigTarget(value) {
793+
const target = typeof value === 'string' ? value.trim().toLowerCase() : '';
794+
return TOOL_CONFIG_PERMISSION_TARGETS.has(target) ? target : '';
795+
}
796+
797+
function normalizeToolConfigPermissions(value) {
798+
const source = isPlainObject(value) ? value : {};
799+
return {
800+
codex: source.codex === true,
801+
claude: source.claude === true
802+
};
803+
}
804+
805+
function readCodexmatePreferences() {
806+
if (!fs.existsSync(CODEXMATE_PREFERENCES_FILE)) return {};
807+
try {
808+
const raw = fs.readFileSync(CODEXMATE_PREFERENCES_FILE, 'utf-8');
809+
const parsed = raw && raw.trim() ? JSON.parse(raw) : {};
810+
return isPlainObject(parsed) ? parsed : {};
811+
} catch (_) {
812+
return {};
813+
}
814+
}
815+
816+
function writeCodexmatePreferences(preferences) {
817+
ensureDir(CODEXMATE_DIR);
818+
writeJsonAtomic(CODEXMATE_PREFERENCES_FILE, isPlainObject(preferences) ? preferences : {});
819+
}
820+
821+
function readToolConfigPermissions() {
822+
const preferences = readCodexmatePreferences();
823+
return normalizeToolConfigPermissions(preferences.toolConfigPermissions || TOOL_CONFIG_PERMISSION_DEFAULTS);
824+
}
825+
826+
function isToolConfigWriteAllowed(target) {
827+
const normalizedTarget = normalizeToolConfigTarget(target);
828+
if (!normalizedTarget) return false;
829+
return readToolConfigPermissions()[normalizedTarget] === true;
830+
}
831+
832+
function buildToolConfigWriteDeniedPayload(target) {
833+
const normalizedTarget = normalizeToolConfigTarget(target) || target || '';
834+
return {
835+
error: '当前为仅浏览,未修改配置。',
836+
errorCode: 'tool-config-write-disabled',
837+
target: normalizedTarget,
838+
permissions: readToolConfigPermissions()
839+
};
840+
}
841+
842+
function assertToolConfigWriteAllowed(target) {
843+
if (!isToolConfigWriteGuardActive()) return;
844+
if (isToolConfigWriteAllowed(target)) return;
845+
const payload = buildToolConfigWriteDeniedPayload(target);
846+
const err = new Error(payload.error);
847+
err.code = payload.errorCode;
848+
err.target = payload.target;
849+
throw err;
850+
}
851+
852+
function getApiToolConfigWriteTarget(action) {
853+
const name = typeof action === 'string' ? action.trim() : '';
854+
if (!name) return '';
855+
const codexWriteActions = new Set([
856+
'apply-config-template',
857+
'add-provider',
858+
'update-provider',
859+
'delete-provider',
860+
'reset-config',
861+
'add-model',
862+
'delete-model',
863+
'restore-codex-dir',
864+
'import-config',
865+
'import-auth-profile',
866+
'switch-auth-profile',
867+
'delete-auth-profile',
868+
'proxy-enable-codex-default',
869+
'proxy-apply-provider',
870+
'local-bridge-toggle',
871+
'local-bridge-set-excluded'
872+
]);
873+
const claudeWriteActions = new Set([
874+
'apply-claude-settings-raw',
875+
'apply-claude-config',
876+
'restore-claude-dir',
877+
'claude-local-bridge-toggle',
878+
'claude-local-bridge-set-excluded',
879+
'claude-local-bridge-sync-providers'
880+
]);
881+
if (codexWriteActions.has(name)) return 'codex';
882+
if (claudeWriteActions.has(name)) return 'claude';
883+
return '';
884+
}
885+
886+
function setToolConfigPermission(params = {}) {
887+
const target = normalizeToolConfigTarget(params && params.target);
888+
if (!target) return { error: '未知配置对象' };
889+
const preferences = readCodexmatePreferences();
890+
const current = normalizeToolConfigPermissions(preferences.toolConfigPermissions || TOOL_CONFIG_PERMISSION_DEFAULTS);
891+
current[target] = params && params.allowWrite === true;
892+
preferences.toolConfigPermissions = current;
893+
writeCodexmatePreferences(preferences);
894+
895+
let bootstrapNotice = '';
896+
if (target === 'codex' && current.codex) {
897+
const bootstrap = ensureManagedConfigBootstrap({ allowWrite: true });
898+
bootstrapNotice = bootstrap && bootstrap.notice ? bootstrap.notice : '';
899+
}
900+
901+
return {
902+
success: true,
903+
target,
904+
permissions: current,
905+
bootstrapNotice
906+
};
907+
}
908+
769909
const PROVIDER_CONFIG_KEYS = new Set([
770910
'name',
771911
'base_url',
@@ -5543,6 +5683,7 @@ function readLocalBridgeSettings() {
55435683
}
55445684

55455685
function writeLocalBridgeSettings(settings) {
5686+
assertToolConfigWriteAllowed('codex');
55465687
fs.writeFileSync(LOCAL_BRIDGE_SETTINGS_FILE, JSON.stringify(settings, null, 2), 'utf-8');
55475688
}
55485689

@@ -5641,6 +5782,7 @@ function readClaudeLocalBridgeSettings() {
56415782
}
56425783

56435784
function writeClaudeLocalBridgeSettings(settings) {
5785+
assertToolConfigWriteAllowed('claude');
56445786
fs.writeFileSync(CLAUDE_LOCAL_BRIDGE_SETTINGS_FILE, JSON.stringify(settings, null, 2), 'utf-8');
56455787
}
56465788

@@ -5655,6 +5797,7 @@ function readClaudeLocalProvidersFile() {
56555797
}
56565798

56575799
function writeClaudeLocalProvidersFile(data) {
5800+
assertToolConfigWriteAllowed('claude');
56585801
ensureDir(CONFIG_DIR);
56595802
fs.writeFileSync(CLAUDE_LOCAL_PROVIDERS_FILE, JSON.stringify(data, null, 2), 'utf-8');
56605803
}
@@ -5669,6 +5812,7 @@ function syncClaudeProvidersToBridgeFile() {
56695812
}
56705813

56715814
function toggleClaudeLocalBridge(params = {}) {
5815+
assertToolConfigWriteAllowed('claude');
56725816
const enable = !!params.enable;
56735817
const settings = readClaudeLocalBridgeSettings();
56745818

@@ -9177,6 +9321,7 @@ function maskKey(key) {
91779321

91789322
// 应用到 Claude Code settings.json(跨平台)
91799323
function applyToClaudeSettings(config = {}) {
9324+
assertToolConfigWriteAllowed('claude');
91809325
try {
91819326
const apiKey = (config.apiKey || '').trim();
91829327
if (!apiKey) {
@@ -9276,6 +9421,7 @@ function readClaudeSettingsRaw() {
92769421
}
92779422

92789423
function applyClaudeSettingsRaw(params = {}) {
9424+
assertToolConfigWriteAllowed('claude');
92799425
const content = typeof params.content === 'string' ? params.content : '';
92809426
if (!content.trim()) {
92819427
return { error: '内容不能为空' };
@@ -10769,14 +10915,28 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
1076910915
});
1077010916
req.on('end', async () => {
1077110917
if (bodyTooLarge) return;
10918+
let leaveToolConfigWriteGuard = null;
1077210919
try {
1077310920
const { action, params } = JSON.parse(body || '{}');
10921+
leaveToolConfigWriteGuard = typeof enterToolConfigWriteGuard === 'function'
10922+
? enterToolConfigWriteGuard()
10923+
: () => {};
1077410924
let result;
1077510925

10776-
switch (action) {
10926+
const guardedToolConfigTarget = getApiToolConfigWriteTarget(action);
10927+
if (guardedToolConfigTarget && !isToolConfigWriteAllowed(guardedToolConfigTarget)) {
10928+
result = buildToolConfigWriteDeniedPayload(guardedToolConfigTarget);
10929+
} else {
10930+
switch (action) {
1077710931
case 'health-check':
1077810932
result = { ok: true };
1077910933
break;
10934+
case 'get-tool-config-permissions':
10935+
result = { permissions: readToolConfigPermissions() };
10936+
break;
10937+
case 'set-tool-config-permission':
10938+
result = setToolConfigPermission(params || {});
10939+
break;
1078010940
case 'status': {
1078110941
const statusConfigResult = readConfigOrVirtualDefault();
1078210942
const config = statusConfigResult.config;
@@ -10815,7 +10975,8 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
1081510975
configReady: !statusConfigResult.isVirtual,
1081610976
configErrorType: statusConfigResult.errorType || '',
1081710977
configNotice: statusConfigResult.reason || '',
10818-
initNotice: consumeInitNotice()
10978+
initNotice: consumeInitNotice(),
10979+
toolConfigPermissions: readToolConfigPermissions()
1081910980
};
1082010981
break;
1082110982
}
@@ -11455,6 +11616,7 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
1145511616
break;
1145611617
default:
1145711618
result = { error: '未知操作' };
11619+
}
1145811620
}
1145911621

1146011622
const responseBody = JSON.stringify(result, null, 2);
@@ -11463,7 +11625,9 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
1146311625
'Content-Length': Buffer.byteLength(responseBody, 'utf-8')
1146411626
});
1146511627
res.end(responseBody, 'utf-8');
11628+
if (leaveToolConfigWriteGuard) leaveToolConfigWriteGuard();
1146611629
} catch (e) {
11630+
if (leaveToolConfigWriteGuard) leaveToolConfigWriteGuard();
1146711631
const errorBody = JSON.stringify({ error: e.message }, null, 2);
1146811632
res.writeHead(500, {
1146911633
'Content-Type': 'application/json; charset=utf-8',
@@ -16062,7 +16226,7 @@ async function main() {
1606216226
const args = process.argv.slice(2);
1606316227
const command = args[0];
1606416228
const isMcpCommand = command === 'mcp';
16065-
const bootstrap = ensureManagedConfigBootstrap();
16229+
const bootstrap = ensureManagedConfigBootstrap({ allowWrite: isToolConfigWriteAllowed('codex') });
1606616230
if (bootstrap && bootstrap.notice) {
1606716231
// MCP stdio transport requires stdout to be protocol-clean.
1606816232
if (!isMcpCommand) {

cli/config-bootstrap.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,12 @@ stream_idle_timeout_ms = 300000
273273
fs.writeFileSync(INIT_MARK_FILE, JSON.stringify(payload, null, 2), 'utf-8');
274274
}
275275

276-
function ensureManagedConfigBootstrap() {
276+
function ensureManagedConfigBootstrap(options = {}) {
277+
const allowWrite = !(options && options.allowWrite === false);
278+
if (!allowWrite) {
279+
initNotice = '';
280+
return { notice: '', readOnly: true };
281+
}
277282
ensureConfigDir();
278283

279284
const initializedAt = new Date().toISOString();

0 commit comments

Comments
 (0)