@@ -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,123 @@ 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+
777+ function normalizeToolConfigTarget(value) {
778+ const target = typeof value === 'string' ? value.trim().toLowerCase() : '';
779+ return TOOL_CONFIG_PERMISSION_TARGETS.has(target) ? target : '';
780+ }
781+
782+ function normalizeToolConfigPermissions(value) {
783+ const source = isPlainObject(value) ? value : {};
784+ return {
785+ codex: source.codex === true,
786+ claude: source.claude === true
787+ };
788+ }
789+
790+ function readCodexmatePreferences() {
791+ if (!fs.existsSync(CODEXMATE_PREFERENCES_FILE)) return {};
792+ try {
793+ const raw = fs.readFileSync(CODEXMATE_PREFERENCES_FILE, 'utf-8');
794+ const parsed = raw && raw.trim() ? JSON.parse(raw) : {};
795+ return isPlainObject(parsed) ? parsed : {};
796+ } catch (_) {
797+ return {};
798+ }
799+ }
800+
801+ function writeCodexmatePreferences(preferences) {
802+ ensureDir(CODEXMATE_DIR);
803+ writeJsonAtomic(CODEXMATE_PREFERENCES_FILE, isPlainObject(preferences) ? preferences : {});
804+ }
805+
806+ function readToolConfigPermissions() {
807+ const preferences = readCodexmatePreferences();
808+ return normalizeToolConfigPermissions(preferences.toolConfigPermissions || TOOL_CONFIG_PERMISSION_DEFAULTS);
809+ }
810+
811+ function isToolConfigWriteAllowed(target) {
812+ const normalizedTarget = normalizeToolConfigTarget(target);
813+ if (!normalizedTarget) return false;
814+ return readToolConfigPermissions()[normalizedTarget] === true;
815+ }
816+
817+ function buildToolConfigWriteDeniedPayload(target) {
818+ const normalizedTarget = normalizeToolConfigTarget(target) || target || '';
819+ return {
820+ error: '当前为仅浏览,未修改配置。',
821+ errorCode: 'tool-config-write-disabled',
822+ target: normalizedTarget,
823+ permissions: readToolConfigPermissions()
824+ };
825+ }
826+
827+ function assertToolConfigWriteAllowed(target) {
828+ if (isToolConfigWriteAllowed(target)) return;
829+ const payload = buildToolConfigWriteDeniedPayload(target);
830+ const err = new Error(payload.error);
831+ err.code = payload.errorCode;
832+ err.target = payload.target;
833+ throw err;
834+ }
835+
836+ function getApiToolConfigWriteTarget(action) {
837+ const name = typeof action === 'string' ? action.trim() : '';
838+ if (!name) return '';
839+ const codexWriteActions = new Set([
840+ 'apply-config-template',
841+ 'add-provider',
842+ 'update-provider',
843+ 'delete-provider',
844+ 'reset-config',
845+ 'add-model',
846+ 'delete-model',
847+ 'restore-codex-dir',
848+ 'import-config',
849+ 'import-auth-profile',
850+ 'switch-auth-profile',
851+ 'delete-auth-profile',
852+ 'proxy-enable-codex-default',
853+ 'proxy-apply-provider',
854+ 'local-bridge-toggle'
855+ ]);
856+ const claudeWriteActions = new Set([
857+ 'apply-claude-settings-raw',
858+ 'apply-claude-config',
859+ 'restore-claude-dir',
860+ 'claude-local-bridge-toggle',
861+ 'claude-local-bridge-sync-providers'
862+ ]);
863+ if (codexWriteActions.has(name)) return 'codex';
864+ if (claudeWriteActions.has(name)) return 'claude';
865+ return '';
866+ }
867+
868+ function setToolConfigPermission(params = {}) {
869+ const target = normalizeToolConfigTarget(params && params.target);
870+ if (!target) return { error: '未知配置对象' };
871+ const preferences = readCodexmatePreferences();
872+ const current = normalizeToolConfigPermissions(preferences.toolConfigPermissions || TOOL_CONFIG_PERMISSION_DEFAULTS);
873+ current[target] = params && params.allowWrite === true;
874+ preferences.toolConfigPermissions = current;
875+ writeCodexmatePreferences(preferences);
876+
877+ let bootstrapNotice = '';
878+ if (target === 'codex' && current.codex) {
879+ const bootstrap = ensureManagedConfigBootstrap({ allowWrite: true });
880+ bootstrapNotice = bootstrap && bootstrap.notice ? bootstrap.notice : '';
881+ }
882+
883+ return {
884+ success: true,
885+ target,
886+ permissions: current,
887+ bootstrapNotice
888+ };
889+ }
890+
769891const PROVIDER_CONFIG_KEYS = new Set([
770892 'name',
771893 'base_url',
@@ -5669,6 +5791,7 @@ function syncClaudeProvidersToBridgeFile() {
56695791}
56705792
56715793function toggleClaudeLocalBridge(params = {}) {
5794+ assertToolConfigWriteAllowed('claude');
56725795 const enable = !!params.enable;
56735796 const settings = readClaudeLocalBridgeSettings();
56745797
@@ -9177,6 +9300,7 @@ function maskKey(key) {
91779300
91789301// 应用到 Claude Code settings.json(跨平台)
91799302function applyToClaudeSettings(config = {}) {
9303+ assertToolConfigWriteAllowed('claude');
91809304 try {
91819305 const apiKey = (config.apiKey || '').trim();
91829306 if (!apiKey) {
@@ -9276,6 +9400,7 @@ function readClaudeSettingsRaw() {
92769400}
92779401
92789402function applyClaudeSettingsRaw(params = {}) {
9403+ assertToolConfigWriteAllowed('claude');
92799404 const content = typeof params.content === 'string' ? params.content : '';
92809405 if (!content.trim()) {
92819406 return { error: '内容不能为空' };
@@ -10773,10 +10898,20 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
1077310898 const { action, params } = JSON.parse(body || '{}');
1077410899 let result;
1077510900
10776- switch (action) {
10901+ const guardedToolConfigTarget = getApiToolConfigWriteTarget(action);
10902+ if (guardedToolConfigTarget && !isToolConfigWriteAllowed(guardedToolConfigTarget)) {
10903+ result = buildToolConfigWriteDeniedPayload(guardedToolConfigTarget);
10904+ } else {
10905+ switch (action) {
1077710906 case 'health-check':
1077810907 result = { ok: true };
1077910908 break;
10909+ case 'get-tool-config-permissions':
10910+ result = { permissions: readToolConfigPermissions() };
10911+ break;
10912+ case 'set-tool-config-permission':
10913+ result = setToolConfigPermission(params || {});
10914+ break;
1078010915 case 'status': {
1078110916 const statusConfigResult = readConfigOrVirtualDefault();
1078210917 const config = statusConfigResult.config;
@@ -10815,7 +10950,8 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
1081510950 configReady: !statusConfigResult.isVirtual,
1081610951 configErrorType: statusConfigResult.errorType || '',
1081710952 configNotice: statusConfigResult.reason || '',
10818- initNotice: consumeInitNotice()
10953+ initNotice: consumeInitNotice(),
10954+ toolConfigPermissions: readToolConfigPermissions()
1081910955 };
1082010956 break;
1082110957 }
@@ -11455,6 +11591,7 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
1145511591 break;
1145611592 default:
1145711593 result = { error: '未知操作' };
11594+ }
1145811595 }
1145911596
1146011597 const responseBody = JSON.stringify(result, null, 2);
@@ -16062,7 +16199,7 @@ async function main() {
1606216199 const args = process.argv.slice(2);
1606316200 const command = args[0];
1606416201 const isMcpCommand = command === 'mcp';
16065- const bootstrap = ensureManagedConfigBootstrap();
16202+ const bootstrap = ensureManagedConfigBootstrap({ allowWrite: isToolConfigWriteAllowed('codex') } );
1606616203 if (bootstrap && bootstrap.notice) {
1606716204 // MCP stdio transport requires stdout to be protocol-clean.
1606816205 if (!isMcpCommand) {
0 commit comments