From ca404aeb2477e49294593e8b0fd693f6ac8e2771 Mon Sep 17 00:00:00 2001 From: zhayujie Date: Wed, 8 Jul 2026 01:59:08 +0800 Subject: [PATCH] fix(desktop): stop backend writing into the .app bundle --- .gitignore | 2 ++ plugins/cow_cli/cow_cli.py | 12 ++++++++---- plugins/plugin_manager.py | 21 +++++++++++++++++++++ 3 files changed, 31 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index b22aa395..9cbb011e 100644 --- a/.gitignore +++ b/.gitignore @@ -60,3 +60,5 @@ desktop/build/* # Icon authoring scratch dir: intermediate assets used to produce the final # icons. Only the finished icons under desktop/resources/ should be committed. desktop/resources/.icon-work/ + +.wrangler/ diff --git a/plugins/cow_cli/cow_cli.py b/plugins/cow_cli/cow_cli.py index 74df4799..3642fe09 100644 --- a/plugins/cow_cli/cow_cli.py +++ b/plugins/cow_cli/cow_cli.py @@ -688,8 +688,11 @@ class CowCliPlugin(Plugin): if resolved: updates["bot_type"] = resolved - project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) - config_path = os.path.join(project_root, "config.json") + # Write to the data dir (~/.cow in desktop, project root otherwise), not + # __file__-relative — the latter lives inside the read-only .app bundle + # when frozen and writing there breaks the macOS code-signature seal. + from config import get_data_root + config_path = os.path.join(get_data_root(), "config.json") try: with open(config_path, "r", encoding="utf-8") as f: file_config = _json.load(f) @@ -1630,8 +1633,9 @@ class CowCliPlugin(Plugin): conf()["knowledge"] = enabled - project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) - config_path = os.path.join(project_root, "config.json") + # Data dir (~/.cow in desktop), not __file__-relative — see _config_set. + from config import get_data_root + config_path = os.path.join(get_data_root(), "config.json") try: with open(config_path, "r", encoding="utf-8") as f: file_config = _json.load(f) diff --git a/plugins/plugin_manager.py b/plugins/plugin_manager.py index 26e1e232..0fbd9a70 100644 --- a/plugins/plugin_manager.py +++ b/plugins/plugin_manager.py @@ -202,6 +202,26 @@ class PluginManager: return True return False + # IM-channel-only plugins that make no sense in the single-user desktop app + # and, worse, write config.json into their bundle dir on init — which breaks + # the macOS code-signature seal of the packaged .app. cow_cli stays enabled + # so desktop chat commands (/status, /help, ...) keep working. + DESKTOP_DISABLED_PLUGINS = { + "GODCMD", "KEYWORD", "BANWORDS", "ROLE", "DUNGEON", "HELLO", "FINISH", + } + + def _apply_desktop_plugin_denylist(self): + """In desktop mode, force-disable IM-only plugins regardless of what + plugins.json (bundle default or user copy) says. Runs after scan (which + syncs enabled from plugins.json) and before activate, so denied plugins + are never instantiated and never write into the read-only bundle.""" + if os.environ.get("COW_DESKTOP") != "1": + return + for name in list(self.plugins.keys()): + if name.upper() in self.DESKTOP_DISABLED_PLUGINS and self.plugins[name].enabled: + self.plugins[name].enabled = False + logger.info("[desktop] plugin %s disabled (not needed in desktop client)" % name) + def load_plugins(self): self.load_config() self.scan_plugins() @@ -212,6 +232,7 @@ class PluginManager: for name, plugin in pconf["plugins"].items(): if name.upper() not in self.plugins: logger.error("Plugin %s not found, but found in plugins.json" % name) + self._apply_desktop_plugin_denylist() self.activate_plugins() def emit_event(self, e_context: EventContext, *args, **kwargs):