From 791b6f0ee6d7461097fbaa282f3e65c2160505c9 Mon Sep 17 00:00:00 2001 From: earayu Date: Mon, 25 May 2026 13:23:31 +0800 Subject: [PATCH 1/2] feat(desktop): pre-trust bundled recipes to suppress new-workflow warning MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the user opens a bundled ApeMind workflow, goose currently shows the "⚠️ New Workflow Warning" dialog because the recipe hash is not in the user's `userData/recipe_hashes/` directory. For workflows we ship with the app this is wrong — the user has already trusted them by installing the distribution. Extend `seedDefaultRecipes()` (added in earlier PR #19) to also: - Parse each bundled YAML - Compute SHA-256 over `JSON.stringify(parsed)` — same algorithm goose's Electron-side `recipeHash.ts` uses for the `has-accepted-recipe-before` IPC check - Write a `.hash` file in `userData/recipe_hashes/` if absent Effect: the bundled-recipe warning is suppressed on first open. Failure mode (best-effort): if YAML parse fails or hash shape mismatches goose's runtime serialization, the worst case is the warning shows once and the user clicks "Trust and Execute" — same as the prior behavior. Failure logs a warning, does not block startup. 5-cat compliance: still category 3 (默认配置) + 5 (打包分发). All in the Electron main process; zero changes to goose-rs core or the warning dialog logic. Signed-off-by: earayu --- ui/desktop/src/main.ts | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/ui/desktop/src/main.ts b/ui/desktop/src/main.ts index 87c03b37f20d..632329a12fd6 100644 --- a/ui/desktop/src/main.ts +++ b/ui/desktop/src/main.ts @@ -180,17 +180,35 @@ async function seedDefaultRecipes(): Promise { ? path.join(process.resourcesPath, 'default-recipes') : path.join(__dirname, '..', 'default-recipes'); const destDir = path.join(os.homedir(), '.config', 'goose', 'recipes'); + const hashesDir = path.join(app.getPath('userData'), 'recipe_hashes'); if (!fsSync.existsSync(sourceRoot)) return; await fs.mkdir(destDir, { recursive: true }); + await fs.mkdir(hashesDir, { recursive: true }); const entries = await fs.readdir(sourceRoot); for (const entry of entries) { if (!entry.endsWith('.yaml') && !entry.endsWith('.yml')) continue; + const sourcePath = path.join(sourceRoot, entry); const destPath = path.join(destDir, entry); - if (fsSync.existsSync(destPath)) continue; - await fs.copyFile(path.join(sourceRoot, entry), destPath); - log.info(`[seedDefaultRecipes] seeded ${entry} → ${destPath}`); + + if (!fsSync.existsSync(destPath)) { + await fs.copyFile(sourcePath, destPath); + log.info(`[seedDefaultRecipes] seeded ${entry} → ${destPath}`); + } + + try { + const yamlContent = await fs.readFile(sourcePath, 'utf-8'); + const parsed = yaml.parse(yamlContent); + const hash = crypto.createHash('sha256').update(JSON.stringify(parsed)).digest('hex'); + const hashFile = path.join(hashesDir, `${hash}.hash`); + if (!fsSync.existsSync(hashFile)) { + await fs.writeFile(hashFile, new Date().toISOString()); + log.info(`[seedDefaultRecipes] pre-trusted hash for ${entry} → ${hash}`); + } + } catch (err) { + log.warn(`[seedDefaultRecipes] failed to pre-trust ${entry}:`, err); + } } } From 640810bb0991d7b945332abf709b86027c0a97fd Mon Sep 17 00:00:00 2001 From: earayu Date: Mon, 25 May 2026 13:25:00 +0800 Subject: [PATCH 2/2] fix(desktop): let the bundled ApeMind extension show the edit button MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `apemind` entry in bundled-extensions.json ships with placeholder URL and Authorization Bearer token (per earayu2's #鹅岛 directive). With `bundled: true` the existing ExtensionItem.tsx gating hides the edit (gear) button entirely, so the user has no way to swap the placeholder values from the UI. Surgical carve-out: keep all other bundled / builtin extensions read-only (preserve upstream security model), but special-case `extension.name === 'apemind'` so its edit button appears. Users can then open the configure modal and replace URL + Bearer token before enabling. 5-cat compliance: still category 4 (UI 可见性微调) + 3 (默认配置 / bundled extension config). The change is a one-condition extension to existing logic and rebases cleanly with upstream. Signed-off-by: earayu --- .../settings/extensions/subcomponents/ExtensionItem.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/ui/desktop/src/components/settings/extensions/subcomponents/ExtensionItem.tsx b/ui/desktop/src/components/settings/extensions/subcomponents/ExtensionItem.tsx index 8c436083067e..bd84af24418a 100644 --- a/ui/desktop/src/components/settings/extensions/subcomponents/ExtensionItem.tsx +++ b/ui/desktop/src/components/settings/extensions/subcomponents/ExtensionItem.tsx @@ -80,9 +80,16 @@ export default function ExtensionItem({ // Bundled extensions and builtins are not editable // Over time we can take the first part of the conditional away as people have bundled: true in their config.yaml entries + // ApeCloud fork carve-out: the `apemind` bundled extension ships with placeholder URL/token + // that the user MUST be able to edit from the UI before enabling. + const isApeMindBundled = + 'bundled' in extension && extension.bundled && extension.name === 'apemind'; + // allow configuration editing if extension is not a builtin/bundled extension AND isStatic = false const editable = - !(extension.type === 'builtin' || ('bundled' in extension && extension.bundled)) && !isStatic; + (!(extension.type === 'builtin' || ('bundled' in extension && extension.bundled)) || + isApeMindBundled) && + !isStatic; return (