Merge pull request #4450 from code-yeongyu/fix-4447-subagent-task-deny

fix(plugin-handlers): explicit task deny for read-only subagents to prevent UI freeze (#4447)
This commit is contained in:
YeonGyu-Kim
2026-05-25 17:06:51 +09:00
committed by GitHub
2 changed files with 119 additions and 0 deletions
@@ -0,0 +1,100 @@
/// <reference types="bun-types" />
import { describe, expect, it } from "bun:test"
import type { OhMyOpenCodeConfig } from "../config"
import { OhMyOpenCodeConfigSchema } from "../config"
import { applyToolConfig } from "./tool-config-handler"
type TestAgent = {
permission?: Record<string, unknown>
}
const TASK_DENIED_SUBAGENTS = [
"librarian",
"explore",
"oracle",
"multimodal-looker",
"metis",
"momus",
] as const
const TASK_ALLOWED_AGENT_NAMES = [
"sisyphus",
"atlas",
"hephaestus",
"sisyphus-junior",
] as const
function createParams(agentNames: readonly string[]): {
readonly config: Record<string, unknown>
readonly pluginConfig: OhMyOpenCodeConfig
readonly agentResult: Record<string, TestAgent>
} {
const agentResult: Record<string, TestAgent> = {}
for (const agentName of agentNames) {
agentResult[agentName] = { permission: {} }
}
return {
config: { tools: {}, permission: {} },
pluginConfig: OhMyOpenCodeConfigSchema.parse({}),
agentResult,
}
}
function requirePermission(
agentResult: Record<string, TestAgent>,
agentName: string,
): Record<string, unknown> {
const permission = agentResult[agentName]?.permission
if (!permission) {
throw new Error(`Missing permission for ${agentName}`)
}
return permission
}
describe("applyToolConfig task permission hard denials", () => {
describe("#given read-only and specialist subagents", () => {
describe("#when applying tool config", () => {
for (const agentName of TASK_DENIED_SUBAGENTS) {
it(`#then should explicitly deny task for ${agentName}`, () => {
const params = createParams([agentName])
applyToolConfig(params)
const permission = requirePermission(params.agentResult, agentName)
expect(permission.task).toBe("deny")
})
}
})
})
describe("#given librarian search permissions", () => {
describe("#when applying tool config", () => {
it("#then should keep grep_app allowed while task is denied", () => {
const params = createParams(["librarian"])
applyToolConfig(params)
const permission = requirePermission(params.agentResult, "librarian")
expect(permission["grep_app_*"]).toBe("allow")
expect(permission.task).toBe("deny")
})
})
})
describe("#given primary and executor agents", () => {
describe("#when applying tool config", () => {
for (const agentName of TASK_ALLOWED_AGENT_NAMES) {
it(`#then should keep task allowed for ${agentName}`, () => {
const params = createParams([agentName])
applyToolConfig(params)
const permission = requirePermission(params.agentResult, agentName)
expect(permission.task).toBe("allow")
})
}
})
})
})
@@ -4,6 +4,15 @@ import { isTaskSystemEnabled } from "../shared";
type AgentWithPermission = { permission?: Record<string, unknown> };
const TASK_DENIED_SUBAGENT_KEYS = [
"librarian",
"explore",
"oracle",
"multimodal-looker",
"metis",
"momus",
] as const;
function getConfigQuestionPermission(): string | null {
const configContent = process.env.OPENCODE_CONFIG_CONTENT;
if (!configContent) return null;
@@ -21,6 +30,12 @@ function agentByKey(agentResult: Record<string, unknown>, key: string): AgentWit
| undefined;
}
function denyTaskForAgent(agentResult: Record<string, unknown>, key: string): void {
const agent = agentByKey(agentResult, key);
if (!agent) return;
agent.permission = { ...agent.permission, task: "deny" };
}
export function applyToolConfig(params: {
config: Record<string, unknown>;
pluginConfig: OhMyOpenCodeConfig;
@@ -59,6 +74,10 @@ export function applyToolConfig(params: {
isCliRunMode ? "deny" :
"allow";
for (const agentKey of TASK_DENIED_SUBAGENT_KEYS) {
denyTaskForAgent(params.agentResult, agentKey);
}
const librarian = agentByKey(params.agentResult, "librarian");
if (librarian) {
librarian.permission = { ...librarian.permission, "grep_app_*": "allow" };