@@ -213,6 +213,7 @@ const CLAUDE_PROJECTS_DIR = path.join(os.homedir(), '.claude', 'projects');
213213const CODEBUDDY_DIR = path.join(os.homedir(), '.codebuddy');
214214const CODEBUDDY_PROJECTS_DIR = path.join(CODEBUDDY_DIR, 'projects');
215215const CODEXMATE_DIR = path.join(os.homedir(), '.codexmate');
216+ const CODEXMATE_PREFERENCES_FILE = path.join(CODEXMATE_DIR, 'preferences.json');
216217const CODEXMATE_SESSIONS_DIR = path.join(CODEXMATE_DIR, 'sessions');
217218const CODEXMATE_DERIVED_SESSIONS_DIR = path.join(CODEXMATE_SESSIONS_DIR, 'derived');
218219const CODEXMATE_DERIVED_CODEX_DIR = path.join(CODEXMATE_DERIVED_SESSIONS_DIR, 'codex');
@@ -717,6 +718,7 @@ function readConfig() {
717718}
718719
719720function 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
736738function 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
749752function writeCurrentModels(data) {
753+ assertToolConfigWriteAllowed('codex');
750754 fs.writeFileSync(CURRENT_MODELS_FILE, JSON.stringify(data, null, 2), 'utf-8');
751755}
752756
753757function 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+
769909const PROVIDER_CONFIG_KEYS = new Set([
770910 'name',
771911 'base_url',
@@ -5543,6 +5683,7 @@ function readLocalBridgeSettings() {
55435683}
55445684
55455685function 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
56435784function 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
56575799function 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
56715814function 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(跨平台)
91799323function applyToClaudeSettings(config = {}) {
9324+ assertToolConfigWriteAllowed('claude');
91809325 try {
91819326 const apiKey = (config.apiKey || '').trim();
91829327 if (!apiKey) {
@@ -9276,6 +9421,7 @@ function readClaudeSettingsRaw() {
92769421}
92779422
92789423function 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) {
0 commit comments