From 5878ed8f496cfa7e524a3ebe89d01f6e20ea7729 Mon Sep 17 00:00:00 2001 From: roboomp Date: Tue, 14 Jul 2026 18:06:10 +0000 Subject: [PATCH] fix(tools): gate generate_image behind setting and tool whitelist generate_image was registered as a custom tool and force-activated via the alwaysInclude list in createAgentSession, so it survived --no-tools (empty toolNames) and any explicit whitelist that omitted it. There was also no generate_image.enabled setting, so /settings had no toggle. Add a generate_image.enabled setting and only register the tool when enabled and either no whitelist is given or it names generate_image. Fixes #5305 --- packages/coding-agent/CHANGELOG.md | 8 ++ .../src/config/settings-schema.ts | 10 +++ packages/coding-agent/src/sdk.ts | 15 +++- .../sdk-generate-image-tool-gating.test.ts | 76 +++++++++++++++++++ 4 files changed, 105 insertions(+), 4 deletions(-) create mode 100644 packages/coding-agent/test/sdk-generate-image-tool-gating.test.ts diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 10f7883ed..b75c189c8 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,14 @@ ## [Unreleased] +### Added + +- Added a `generate_image.enabled` setting (Settings › Tools › Generate Image) so the image generation tool can be toggled like every other tool ([#5305](https://github.com/can1357/oh-my-pi/issues/5305)) + +### Fixed + +- Fixed `generate_image` staying active under `--no-tools` and any explicit tool whitelist that omitted it; the tool now honors the whitelist and the new `generate_image.enabled` setting instead of being force-activated as a custom tool ([#5305](https://github.com/can1357/oh-my-pi/issues/5305)) + ## [16.4.8] - 2026-07-12 ### Fixed diff --git a/packages/coding-agent/src/config/settings-schema.ts b/packages/coding-agent/src/config/settings-schema.ts index e07b14045..068e1ae12 100644 --- a/packages/coding-agent/src/config/settings-schema.ts +++ b/packages/coding-agent/src/config/settings-schema.ts @@ -3528,6 +3528,16 @@ export const SETTINGS_SCHEMA = { description: "Enable the tts tool for on-device (Kokoro) or xAI Grok Voice speech-file synthesis", }, }, + "generate_image.enabled": { + type: "boolean", + default: true, + ui: { + tab: "tools", + group: "Available Tools", + label: "Generate Image", + description: "Enable the generate_image tool for text-to-image generation and editing", + }, + }, "inspect_image.enabled": { type: "boolean", diff --git a/packages/coding-agent/src/sdk.ts b/packages/coding-agent/src/sdk.ts index d03b414a4..935459c56 100644 --- a/packages/coding-agent/src/sdk.ts +++ b/packages/coding-agent/src/sdk.ts @@ -1812,10 +1812,17 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {} // to mirror the AsyncJobManager ownership rule. if (mcpManager && !options.parentTaskPrefix) MCPManager.setInstance(mcpManager); - // Add image tools when the active model or configured image providers can generate images. - const imageGenTools = await logger.time("getImageGenTools", () => getImageGenTools(modelRegistry, model)); - if (imageGenTools.length > 0) { - customTools.push(...(imageGenTools as unknown as CustomTool[])); + // Add image tools when generation is enabled and either no explicit tool + // whitelist was given or it names `generate_image`. Unlike built-in tools + // (filtered in `createTools`), custom tools are force-activated via + // `alwaysInclude` below, so an explicit `--no-tools`/whitelist must be + // honored here or image-gen would leak past every filter (issue #5305). + const imageGenRequested = !options.toolNames || options.toolNames.includes("generate_image"); + if (settings.get("generate_image.enabled") && imageGenRequested) { + const imageGenTools = await logger.time("getImageGenTools", () => getImageGenTools(modelRegistry, model)); + if (imageGenTools.length > 0) { + customTools.push(...(imageGenTools as unknown as CustomTool[])); + } } if (settings.get("speechgen.enabled")) { diff --git a/packages/coding-agent/test/sdk-generate-image-tool-gating.test.ts b/packages/coding-agent/test/sdk-generate-image-tool-gating.test.ts new file mode 100644 index 000000000..977adbd9a --- /dev/null +++ b/packages/coding-agent/test/sdk-generate-image-tool-gating.test.ts @@ -0,0 +1,76 @@ +import { afterAll, beforeAll, describe, expect, it } from "bun:test"; +import * as fs from "node:fs"; +import * as os from "node:os"; +import * as path from "node:path"; +import { AuthStorage } from "@oh-my-pi/pi-ai"; +import { getBundledModel } from "@oh-my-pi/pi-catalog/models"; +import { ModelRegistry } from "@oh-my-pi/pi-coding-agent/config/model-registry"; +import { Settings } from "@oh-my-pi/pi-coding-agent/config/settings"; +import { createAgentSession } from "@oh-my-pi/pi-coding-agent/sdk"; +import type { AgentSession } from "@oh-my-pi/pi-coding-agent/session/agent-session"; +import { SessionManager } from "@oh-my-pi/pi-coding-agent/session/session-manager"; +import { removeSyncWithRetries, Snowflake } from "@oh-my-pi/pi-utils"; + +// Regression for issue #5305: image-gen is registered as a custom tool, and +// custom tools are force-activated regardless of the `toolNames` filter. Before +// the fix, `generate_image` survived `--no-tools` (an empty `toolNames`), any +// explicit whitelist that omitted it, and had no `generate_image.enabled` +// settings toggle. The SDK must honor the whitelist and the new setting. +describe("generate_image tool gating", () => { + let registryDir: string; + let authStorage: AuthStorage; + let modelRegistry: ModelRegistry; + const sessions: AgentSession[] = []; + + beforeAll(async () => { + registryDir = path.join(os.tmpdir(), `pi-generate-image-gating-${Snowflake.next()}`); + fs.mkdirSync(registryDir, { recursive: true }); + authStorage = await AuthStorage.create(path.join(registryDir, "auth.db")); + modelRegistry = new ModelRegistry(authStorage); + }); + + afterAll(async () => { + for (const session of sessions) await session.dispose().catch(() => {}); + authStorage.close(); + if (fs.existsSync(registryDir)) removeSyncWithRetries(registryDir); + }); + + async function activeToolNames(settings: Settings, toolNames?: string[]): Promise { + const { session } = await createAgentSession({ + cwd: registryDir, + agentDir: registryDir, + modelRegistry, + sessionManager: SessionManager.inMemory(), + settings, + model: getBundledModel("openai", "gpt-4o-mini"), + disableExtensionDiscovery: true, + toolNames, + }); + sessions.push(session); + return session.getActiveToolNames(); + } + + it("excludes generate_image from a restricted tool whitelist", async () => { + const names = await activeToolNames(Settings.isolated({}), ["read"]); + expect(names).toContain("read"); + expect(names).not.toContain("generate_image"); + }); + + it("excludes generate_image under --no-tools (empty whitelist)", async () => { + const names = await activeToolNames(Settings.isolated({}), []); + expect(names).not.toContain("generate_image"); + }); + + it("respects generate_image.enabled=false even when requested", async () => { + const names = await activeToolNames(Settings.isolated({ "generate_image.enabled": false }), [ + "read", + "generate_image", + ]); + expect(names).not.toContain("generate_image"); + }); + + it("includes generate_image when explicitly requested and enabled", async () => { + const names = await activeToolNames(Settings.isolated({}), ["read", "generate_image"]); + expect(names).toContain("generate_image"); + }); +});