From 65bc742881b8808ec9a0341f23623975494dc1e8 Mon Sep 17 00:00:00 2001 From: Chocothin Date: Sun, 1 Mar 2026 22:49:47 +0900 Subject: [PATCH] fix(tool-config): respect question permission from OPENCODE_CONFIG_CONTENT applyToolConfig() unconditionally set question permission based only on OPENCODE_CLI_RUN_MODE, ignoring the question:deny already configured via OPENCODE_CONFIG_CONTENT. This caused agents to hang in headless environments (e.g. Maestro Auto Run) where the host sets question:deny but does not know about the plugin-internal OPENCODE_CLI_RUN_MODE variable. Read permission.question from OPENCODE_CONFIG_CONTENT and give it highest priority: config deny > CLI run mode deny > default allow. --- .../tool-config-handler.test.ts | 105 +++++++++++++++++- src/plugin-handlers/tool-config-handler.ts | 17 ++- 2 files changed, 120 insertions(+), 2 deletions(-) diff --git a/src/plugin-handlers/tool-config-handler.test.ts b/src/plugin-handlers/tool-config-handler.test.ts index 4ba70497a..0ef60d56f 100644 --- a/src/plugin-handlers/tool-config-handler.test.ts +++ b/src/plugin-handlers/tool-config-handler.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from "bun:test" +import { describe, it, expect, beforeEach, afterEach } from "bun:test" import { applyToolConfig } from "./tool-config-handler" import type { OhMyOpenCodeConfig } from "../config" @@ -56,6 +56,109 @@ describe("applyToolConfig", () => { }) }) + describe("#given OPENCODE_CONFIG_CONTENT has question set to deny", () => { + let originalConfigContent: string | undefined + let originalCliRunMode: string | undefined + + beforeEach(() => { + originalConfigContent = process.env.OPENCODE_CONFIG_CONTENT + originalCliRunMode = process.env.OPENCODE_CLI_RUN_MODE + }) + + afterEach(() => { + if (originalConfigContent === undefined) { + delete process.env.OPENCODE_CONFIG_CONTENT + } else { + process.env.OPENCODE_CONFIG_CONTENT = originalConfigContent + } + if (originalCliRunMode === undefined) { + delete process.env.OPENCODE_CLI_RUN_MODE + } else { + process.env.OPENCODE_CLI_RUN_MODE = originalCliRunMode + } + }) + + describe("#when config explicitly denies question permission", () => { + it.each(["sisyphus", "hephaestus", "prometheus"])( + "#then should deny question for %s even without CLI_RUN_MODE", + (agentName) => { + process.env.OPENCODE_CONFIG_CONTENT = JSON.stringify({ + permission: { question: "deny" }, + }) + delete process.env.OPENCODE_CLI_RUN_MODE + const params = createParams({ agents: [agentName] }) + + applyToolConfig(params) + + const agent = params.agentResult[agentName] as { + permission: Record + } + expect(agent.permission.question).toBe("deny") + }, + ) + }) + + describe("#when config does not deny question permission", () => { + it.each(["sisyphus", "hephaestus", "prometheus"])( + "#then should allow question for %s in interactive mode", + (agentName) => { + process.env.OPENCODE_CONFIG_CONTENT = JSON.stringify({ + permission: { question: "allow" }, + }) + delete process.env.OPENCODE_CLI_RUN_MODE + const params = createParams({ agents: [agentName] }) + + applyToolConfig(params) + + const agent = params.agentResult[agentName] as { + permission: Record + } + expect(agent.permission.question).toBe("allow") + }, + ) + }) + + describe("#when CLI_RUN_MODE is true and config does not deny", () => { + it.each(["sisyphus", "hephaestus", "prometheus"])( + "#then should deny question for %s via CLI_RUN_MODE", + (agentName) => { + process.env.OPENCODE_CONFIG_CONTENT = JSON.stringify({ + permission: {}, + }) + process.env.OPENCODE_CLI_RUN_MODE = "true" + const params = createParams({ agents: [agentName] }) + + applyToolConfig(params) + + const agent = params.agentResult[agentName] as { + permission: Record + } + expect(agent.permission.question).toBe("deny") + }, + ) + }) + + describe("#when config deny overrides CLI_RUN_MODE allow", () => { + it.each(["sisyphus", "hephaestus", "prometheus"])( + "#then should deny question for %s when config says deny regardless of CLI_RUN_MODE", + (agentName) => { + process.env.OPENCODE_CONFIG_CONTENT = JSON.stringify({ + permission: { question: "deny" }, + }) + process.env.OPENCODE_CLI_RUN_MODE = "false" + const params = createParams({ agents: [agentName] }) + + applyToolConfig(params) + + const agent = params.agentResult[agentName] as { + permission: Record + } + expect(agent.permission.question).toBe("deny") + }, + ) + }) + }) + describe("#given task_system is disabled", () => { describe("#when applying tool config", () => { it.each([ diff --git a/src/plugin-handlers/tool-config-handler.ts b/src/plugin-handlers/tool-config-handler.ts index e488d2da9..1168e272e 100644 --- a/src/plugin-handlers/tool-config-handler.ts +++ b/src/plugin-handlers/tool-config-handler.ts @@ -3,6 +3,17 @@ import { getAgentDisplayName } from "../shared/agent-display-names"; type AgentWithPermission = { permission?: Record }; +function getConfigQuestionPermission(): string | null { + const configContent = process.env.OPENCODE_CONFIG_CONTENT; + if (!configContent) return null; + try { + const parsed = JSON.parse(configContent); + return parsed?.permission?.question ?? null; + } catch { + return null; + } +} + function agentByKey(agentResult: Record, key: string): AgentWithPermission | undefined { return (agentResult[key] ?? agentResult[getAgentDisplayName(key)]) as | AgentWithPermission @@ -32,7 +43,11 @@ export function applyToolConfig(params: { }; const isCliRunMode = process.env.OPENCODE_CLI_RUN_MODE === "true"; - const questionPermission = isCliRunMode ? "deny" : "allow"; + const configQuestionPermission = getConfigQuestionPermission(); + const questionPermission = + configQuestionPermission === "deny" ? "deny" : + isCliRunMode ? "deny" : + "allow"; const librarian = agentByKey(params.agentResult, "librarian"); if (librarian) {