From c46b712997a96a7aee2484cc38a0216b6b7dd1b6 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Mon, 27 Apr 2026 12:49:20 +0900 Subject: [PATCH 01/10] fix(agents): hide grep glob for frontier agents Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/agents/builtin-agents/hephaestus-agent.ts | 6 ++ .../builtin-agents/sisyphus-agent.test.ts | 76 ++++++++++++++++++- src/agents/builtin-agents/sisyphus-agent.ts | 6 ++ src/agents/frontier-tool-schema-guard.ts | 12 +++ src/agents/hephaestus/agent.test.ts | 76 ++++++++++++++++++- src/agents/hephaestus/agent.ts | 2 + src/agents/sisyphus.ts | 4 + src/agents/tool-restrictions.test.ts | 46 +++++++++++ 8 files changed, 222 insertions(+), 6 deletions(-) create mode 100644 src/agents/frontier-tool-schema-guard.ts diff --git a/src/agents/builtin-agents/hephaestus-agent.ts b/src/agents/builtin-agents/hephaestus-agent.ts index a32064c63..fc7b3d571 100644 --- a/src/agents/builtin-agents/hephaestus-agent.ts +++ b/src/agents/builtin-agents/hephaestus-agent.ts @@ -8,6 +8,7 @@ import { applyEnvironmentContext } from "./environment-context" import { applyCategoryOverride, mergeAgentConfig } from "./agent-overrides" import { applyModelResolution, getFirstFallbackModel } from "./model-resolution" import { getGptApplyPatchPermission } from "../gpt-apply-patch-guard" +import { getFrontierToolSchemaPermission } from "../frontier-tool-schema-guard" export function maybeCreateHephaestusConfig(input: { disabledAgents: string[] @@ -89,6 +90,11 @@ export function maybeCreateHephaestusConfig(input: { } const resolvedModel = hephaestusConfig.model ?? "" + const frontierDeny = getFrontierToolSchemaPermission(resolvedModel) + if (Object.keys(frontierDeny).length > 0 && hephaestusConfig.permission) { + Object.assign(hephaestusConfig.permission, frontierDeny) + } + const gptDeny = getGptApplyPatchPermission(resolvedModel) if (Object.keys(gptDeny).length > 0 && hephaestusConfig.permission) { Object.assign(hephaestusConfig.permission, gptDeny) diff --git a/src/agents/builtin-agents/sisyphus-agent.test.ts b/src/agents/builtin-agents/sisyphus-agent.test.ts index e7289f6c0..b5d14895a 100644 --- a/src/agents/builtin-agents/sisyphus-agent.test.ts +++ b/src/agents/builtin-agents/sisyphus-agent.test.ts @@ -1,3 +1,5 @@ +/// + import { describe, expect, test } from "bun:test"; import { maybeCreateSisyphusConfig } from "./sisyphus-agent"; import type { AgentOverrides } from "../types"; @@ -12,7 +14,7 @@ describe("maybeCreateSisyphusConfig", () => { model: "openai/gpt-5.4", permission: { apply_patch: "allow", - }, + } as Record, }, }; const mergedCategories: Record = {}; @@ -46,7 +48,7 @@ describe("maybeCreateSisyphusConfig", () => { model: "anthropic/claude-opus-4-7", permission: { apply_patch: "allow", - }, + } as Record, }, }; const mergedCategories: Record = {}; @@ -73,6 +75,74 @@ describe("maybeCreateSisyphusConfig", () => { }); }); + describe("#given Opus 4.7 model with user override allowing grep and glob", () => { + test("#when config is created #then grep and glob are still denied", () => { + // given + const agentOverrides: AgentOverrides = { + sisyphus: { + model: "anthropic/claude-opus-4-7", + permission: { + grep: "allow", + glob: "allow", + } as Record, + }, + }; + const mergedCategories: Record = {}; + + // when + const config = maybeCreateSisyphusConfig({ + disabledAgents: [], + agentOverrides, + availableModels: new Set(["anthropic/claude-opus-4-7"]), + systemDefaultModel: "anthropic/claude-opus-4-7", + isFirstRunNoCache: false, + availableAgents: [], + availableSkills: [], + availableCategories: [], + mergedCategories, + useTaskSystem: false, + }); + + // then + expect(config?.permission).toHaveProperty("grep", "deny"); + expect(config?.permission).toHaveProperty("glob", "deny"); + }); + }); + + describe("#given GPT 5.5 model with user override allowing grep and glob", () => { + test("#when config is created #then grep and glob are still denied", () => { + // given + const agentOverrides: AgentOverrides = { + sisyphus: { + model: "openai/gpt-5.5", + permission: { + grep: "allow", + glob: "allow", + } as Record, + }, + }; + const mergedCategories: Record = {}; + + // when + const config = maybeCreateSisyphusConfig({ + disabledAgents: [], + agentOverrides, + availableModels: new Set(["openai/gpt-5.5"]), + systemDefaultModel: "openai/gpt-5.5", + isFirstRunNoCache: false, + availableAgents: [], + availableSkills: [], + availableCategories: [], + mergedCategories, + useTaskSystem: false, + }); + + // then + expect(config?.permission).toHaveProperty("grep", "deny"); + expect(config?.permission).toHaveProperty("glob", "deny"); + }); + }); + describe("#given generic GPT model with user override allowing apply_patch", () => { test("#when config is created #then apply_patch is still denied", () => { // given @@ -81,7 +151,7 @@ describe("maybeCreateSisyphusConfig", () => { model: "openai/gpt-4o", permission: { apply_patch: "allow", - }, + } as Record, }, }; const mergedCategories: Record = {}; diff --git a/src/agents/builtin-agents/sisyphus-agent.ts b/src/agents/builtin-agents/sisyphus-agent.ts index 97aef5f61..1b111d6cd 100644 --- a/src/agents/builtin-agents/sisyphus-agent.ts +++ b/src/agents/builtin-agents/sisyphus-agent.ts @@ -8,6 +8,7 @@ import { applyOverrides } from "./agent-overrides" import { applyModelResolution, getFirstFallbackModel } from "./model-resolution" import { createSisyphusAgent } from "../sisyphus" import { getGptApplyPatchPermission } from "../gpt-apply-patch-guard" +import { getFrontierToolSchemaPermission } from "../frontier-tool-schema-guard" export function maybeCreateSisyphusConfig(input: { disabledAgents: string[] @@ -83,6 +84,11 @@ export function maybeCreateSisyphusConfig(input: { sisyphusConfig = applyOverrides(sisyphusConfig, sisyphusOverride, mergedCategories, directory) const resolvedModel = sisyphusConfig.model ?? "" + const frontierDeny = getFrontierToolSchemaPermission(resolvedModel) + if (Object.keys(frontierDeny).length > 0 && sisyphusConfig.permission) { + Object.assign(sisyphusConfig.permission, frontierDeny) + } + const gptDeny = getGptApplyPatchPermission(resolvedModel) if (Object.keys(gptDeny).length > 0 && sisyphusConfig.permission) { Object.assign(sisyphusConfig.permission, gptDeny) diff --git a/src/agents/frontier-tool-schema-guard.ts b/src/agents/frontier-tool-schema-guard.ts new file mode 100644 index 000000000..028943dd8 --- /dev/null +++ b/src/agents/frontier-tool-schema-guard.ts @@ -0,0 +1,12 @@ +import { isGpt5_5Model } from "./types" + +function isOpus47Model(model: string): boolean { + const modelName = model.includes("/") ? (model.split("/").pop() ?? model) : model + return modelName.toLowerCase().includes("claude-opus-4-7") +} + +export function getFrontierToolSchemaPermission(model: string): Record { + return isOpus47Model(model) || isGpt5_5Model(model) + ? { grep: "deny" as const, glob: "deny" as const } + : {} +} diff --git a/src/agents/hephaestus/agent.test.ts b/src/agents/hephaestus/agent.test.ts index f7d1087f6..22f1d8c81 100644 --- a/src/agents/hephaestus/agent.test.ts +++ b/src/agents/hephaestus/agent.test.ts @@ -1,3 +1,5 @@ +/// + import { describe, expect, test } from "bun:test"; import { getHephaestusPromptSource, @@ -321,7 +323,7 @@ describe("maybeCreateHephaestusConfig GPT apply_patch guard", () => { model: "openai/gpt-5.4", permission: { apply_patch: "allow", - }, + } as Record, }, }; const mergedCategories: Record = {}; @@ -355,7 +357,7 @@ describe("maybeCreateHephaestusConfig GPT apply_patch guard", () => { model: "anthropic/claude-opus-4-7", permission: { apply_patch: "allow", - }, + } as Record, }, }; const mergedCategories: Record = {}; @@ -389,7 +391,7 @@ describe("maybeCreateHephaestusConfig GPT apply_patch guard", () => { model: "openai/gpt-4o", permission: { apply_patch: "allow", - }, + } as Record, }, }; const mergedCategories: Record = {}; @@ -414,4 +416,72 @@ describe("maybeCreateHephaestusConfig GPT apply_patch guard", () => { expect(config?.permission).toHaveProperty("apply_patch", "deny"); }); }); + + describe("#given Opus 4.7 model with user override allowing grep and glob", () => { + test("#when config is created #then grep and glob are still denied", () => { + // given + const agentOverrides: AgentOverrides = { + hephaestus: { + model: "anthropic/claude-opus-4-7", + permission: { + grep: "allow", + glob: "allow", + } as Record, + }, + }; + const mergedCategories: Record = {}; + + // when + const config = maybeCreateHephaestusConfig({ + disabledAgents: [], + agentOverrides, + availableModels: new Set(["anthropic/claude-opus-4-7"]), + systemDefaultModel: "anthropic/claude-opus-4-7", + isFirstRunNoCache: false, + availableAgents: [], + availableSkills: [], + availableCategories: [], + mergedCategories, + useTaskSystem: false, + }); + + // then + expect(config?.permission).toHaveProperty("grep", "deny"); + expect(config?.permission).toHaveProperty("glob", "deny"); + }); + }); + + describe("#given GPT 5.5 model with user override allowing grep and glob", () => { + test("#when config is created #then grep and glob are still denied", () => { + // given + const agentOverrides: AgentOverrides = { + hephaestus: { + model: "openai/gpt-5.5", + permission: { + grep: "allow", + glob: "allow", + } as Record, + }, + }; + const mergedCategories: Record = {}; + + // when + const config = maybeCreateHephaestusConfig({ + disabledAgents: [], + agentOverrides, + availableModels: new Set(["openai/gpt-5.5"]), + systemDefaultModel: "openai/gpt-5.5", + isFirstRunNoCache: false, + availableAgents: [], + availableSkills: [], + availableCategories: [], + mergedCategories, + useTaskSystem: false, + }); + + // then + expect(config?.permission).toHaveProperty("grep", "deny"); + expect(config?.permission).toHaveProperty("glob", "deny"); + }); + }); }); diff --git a/src/agents/hephaestus/agent.ts b/src/agents/hephaestus/agent.ts index b348f30b7..3aa773bac 100644 --- a/src/agents/hephaestus/agent.ts +++ b/src/agents/hephaestus/agent.ts @@ -9,6 +9,7 @@ import type { } from "../dynamic-agent-prompt-builder"; import { categorizeTools, buildAgentIdentitySection } from "../dynamic-agent-prompt-builder"; import { getGptApplyPatchPermission } from "../gpt-apply-patch-guard"; +import { getFrontierToolSchemaPermission } from "../frontier-tool-schema-guard"; import { buildHephaestusPrompt as buildGptPrompt } from "./gpt"; import { buildHephaestusPrompt as buildGpt53CodexPrompt } from "./gpt-5-3-codex"; @@ -139,6 +140,7 @@ export function createHephaestusAgent( permission: { question: "allow", call_omo_agent: "deny", + ...getFrontierToolSchemaPermission(model), ...getGptApplyPatchPermission(model), } as AgentConfig["permission"], reasoningEffort: "medium", diff --git a/src/agents/sisyphus.ts b/src/agents/sisyphus.ts index 21a36b8a0..d0bfb0979 100644 --- a/src/agents/sisyphus.ts +++ b/src/agents/sisyphus.ts @@ -13,6 +13,7 @@ import { buildGpt54SisyphusPrompt } from "./sisyphus/gpt-5-4"; import { buildGpt55SisyphusPrompt } from "./sisyphus/gpt-5-5"; import { buildTaskManagementSection } from "./sisyphus/default"; import { getGptApplyPatchPermission } from "./gpt-apply-patch-guard"; +import { getFrontierToolSchemaPermission } from "./frontier-tool-schema-guard"; const MODE: AgentMode = "primary"; export const SISYPHUS_PROMPT_METADATA: AgentPromptMetadata = { @@ -501,6 +502,7 @@ export function createSisyphusAgent( permission: { question: "allow", call_omo_agent: "deny", + ...getFrontierToolSchemaPermission(model), ...getGptApplyPatchPermission(model), } as AgentConfig["permission"], reasoningEffort: "medium", @@ -527,6 +529,7 @@ export function createSisyphusAgent( permission: { question: "allow", call_omo_agent: "deny", + ...getFrontierToolSchemaPermission(model), ...getGptApplyPatchPermission(model), } as AgentConfig["permission"], reasoningEffort: "medium", @@ -567,6 +570,7 @@ export function createSisyphusAgent( const permission = { question: "allow", call_omo_agent: "deny", + ...getFrontierToolSchemaPermission(model), ...getGptApplyPatchPermission(model), } as AgentConfig["permission"]; const base = { diff --git a/src/agents/tool-restrictions.test.ts b/src/agents/tool-restrictions.test.ts index 3ae7bfcfe..1d0fed4fd 100644 --- a/src/agents/tool-restrictions.test.ts +++ b/src/agents/tool-restrictions.test.ts @@ -1,3 +1,5 @@ +/// + import { describe, test, expect } from "bun:test" import { createOracleAgent } from "./oracle" import { createLibrarianAgent } from "./librarian" @@ -6,6 +8,7 @@ import { createMomusAgent } from "./momus" import { createMetisAgent } from "./metis" import { createAtlasAgent } from "./atlas" import { createSisyphusAgent } from "./sisyphus" +import { createHephaestusAgent } from "./hephaestus" const TEST_MODEL = "anthropic/claude-sonnet-4-5" @@ -131,4 +134,47 @@ describe("read-only agent tool restrictions", () => { expect(claudePermission["apply_patch"]).toBeUndefined() }) }) + + describe("Sisyphus and Hephaestus frontier tool schema restrictions", () => { + test("deny grep and glob for Opus 4.7 and GPT 5.5 models", () => { + // given + const frontierAgents = [ + createSisyphusAgent("anthropic/claude-opus-4-7"), + createSisyphusAgent("openai/gpt-5.5"), + createHephaestusAgent("anthropic/claude-opus-4-7"), + createHephaestusAgent("openai/gpt-5.5"), + ] + + // when + const permissions = frontierAgents.map( + (agent) => (agent.permission ?? {}) as Record, + ) + + // then + for (const permission of permissions) { + expect(permission.grep).toBe("deny") + expect(permission.glob).toBe("deny") + } + }) + + test("keeps grep and glob available for other models", () => { + // given + const otherAgents = [ + createSisyphusAgent("anthropic/claude-sonnet-4-5"), + createSisyphusAgent("openai/gpt-5.4"), + createHephaestusAgent("openai/gpt-5.4"), + ] + + // when + const permissions = otherAgents.map( + (agent) => (agent.permission ?? {}) as Record, + ) + + // then + for (const permission of permissions) { + expect(permission.grep).toBeUndefined() + expect(permission.glob).toBeUndefined() + } + }) + }) }) From f1353cd9a8337b09b464754176bdd7a758bb15cf Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Mon, 27 Apr 2026 13:24:50 +0900 Subject: [PATCH 02/10] fix(tmux): use stable fallback server URL Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/features/tmux-subagent/manager.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/features/tmux-subagent/manager.ts b/src/features/tmux-subagent/manager.ts index 3340fb55d..a8d93ca16 100644 --- a/src/features/tmux-subagent/manager.ts +++ b/src/features/tmux-subagent/manager.ts @@ -72,8 +72,7 @@ export class TmuxSessionManager { this.client = ctx.client this.tmuxConfig = tmuxConfig this.deps = deps - const defaultPort = process.env.OPENCODE_PORT ?? "4096" - const fallbackUrl = `http://localhost:${defaultPort}` + const fallbackUrl = "http://localhost:4096" const rawServerUrl = ctx.serverUrl?.toString() try { if (rawServerUrl) { From ecce65bb673664eea40872e1642b20fc3a29fea7 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Mon, 27 Apr 2026 13:24:50 +0900 Subject: [PATCH 03/10] fix(skill): isolate injected skill discovery Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/tools/skill/tools.factory.test.ts | 6 ++---- src/tools/skill/tools.ts | 9 +-------- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/src/tools/skill/tools.factory.test.ts b/src/tools/skill/tools.factory.test.ts index 5b9a5ba30..b307d9589 100644 --- a/src/tools/skill/tools.factory.test.ts +++ b/src/tools/skill/tools.factory.test.ts @@ -4,13 +4,10 @@ import { afterEach, beforeEach, describe, expect, it, mock, spyOn } from "bun:te import type { ToolContext } from "@opencode-ai/plugin/tool" import type { LoadedSkill } from "../../features/opencode-skill-loader/types" import * as skillContent from "../../features/opencode-skill-loader/skill-content" +import * as commandDiscovery from "../slashcommand/command-discovery" const discoverCommandsSync = mock(() => []) -mock.module("../slashcommand/command-discovery", () => ({ - discoverCommandsSync, -})) - function createMockSkill(name: string): LoadedSkill { return { name, @@ -50,6 +47,7 @@ function createMockContext(sessionID: string): ToolContext { } beforeEach(() => { + spyOn(commandDiscovery, "discoverCommandsSync").mockImplementation(discoverCommandsSync) spyOn(skillContent, "getAllSkills").mockImplementation(getAllSkills) spyOn(skillContent, "clearSkillCache").mockImplementation(clearSkillCache) }) diff --git a/src/tools/skill/tools.ts b/src/tools/skill/tools.ts index d49936f95..1ffbe364b 100644 --- a/src/tools/skill/tools.ts +++ b/src/tools/skill/tools.ts @@ -37,14 +37,7 @@ export function createSkillTool(options: SkillLoadOptions = {}): ToolDefinition disabledSkills: options?.disabledSkills, browserProvider: options?.browserProvider, })) ?? [] - const allSkills = !options.skills - ? discovered - : [ - ...discovered, - ...options.skills.filter( - (skill) => !new Set(discovered.map((discoveredSkill) => discoveredSkill.name)).has(skill.name) - ), - ] + const allSkills = options.skills ? [...options.skills] : discovered if (options.nativeSkills) { try { From a2c3804b5f7adcfbaa8cd67b0c1184dd1033af6c Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Mon, 27 Apr 2026 13:24:50 +0900 Subject: [PATCH 04/10] test(slashcommand): isolate command discovery mocks Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- .../auto-slash-command/executor-resolution.test.ts | 10 ++++++++-- src/hooks/auto-slash-command/executor.ts | 10 ++++------ src/tools/slashcommand/command-discovery-deps.ts | 6 ++++++ src/tools/slashcommand/command-discovery.ts | 7 ++++--- 4 files changed, 22 insertions(+), 11 deletions(-) create mode 100644 src/tools/slashcommand/command-discovery-deps.ts diff --git a/src/hooks/auto-slash-command/executor-resolution.test.ts b/src/hooks/auto-slash-command/executor-resolution.test.ts index 45c905467..82d924902 100644 --- a/src/hooks/auto-slash-command/executor-resolution.test.ts +++ b/src/hooks/auto-slash-command/executor-resolution.test.ts @@ -1,8 +1,9 @@ +/// + import { afterEach, describe, expect, it, spyOn } from "bun:test" import type { LoadedSkill } from "../../features/opencode-skill-loader" import * as shared from "../../shared" -import * as slashcommand from "../../tools/slashcommand" -import { executeSlashCommand } from "./executor" +import * as slashcommand from "../../tools/slashcommand/command-discovery" let resolveCommandsInTextSpy: { mockRestore: () => void } | undefined let resolveFileReferencesInTextSpy: { mockRestore: () => void } | undefined @@ -38,6 +39,11 @@ function restoreExecutorSpies(): void { discoverCommandsSyncSpy = undefined } +async function executeSlashCommand(...args: Parameters): ReturnType { + const module = await import(`./executor?test=${Date.now()}-${Math.random()}`) + return module.executeSlashCommand(...args) +} + afterEach(restoreExecutorSpies) function createRestrictedSkill(): LoadedSkill { diff --git a/src/hooks/auto-slash-command/executor.ts b/src/hooks/auto-slash-command/executor.ts index eedd8881f..0b5c7ceb9 100644 --- a/src/hooks/auto-slash-command/executor.ts +++ b/src/hooks/auto-slash-command/executor.ts @@ -1,10 +1,8 @@ import { dirname } from "path" -import { - resolveCommandsInText, - resolveFileReferencesInText, -} from "../../shared" +import { resolveCommandsInText } from "../../shared/command-executor/resolve-commands-in-text" +import { resolveFileReferencesInText } from "../../shared/file-reference-resolver" import { discoverAllSkills, type LoadedSkill, type LazyContentLoader } from "../../features/opencode-skill-loader" -import { discoverCommandsSync } from "../../tools/slashcommand" +import * as commandDiscovery from "../../tools/slashcommand/command-discovery" import type { CommandInfo as DiscoveredCommandInfo, CommandMetadata } from "../../tools/slashcommand/types" import type { ParsedSlashCommand } from "./types" @@ -47,7 +45,7 @@ export interface ExecutorOptions { async function discoverAllCommands(options?: ExecutorOptions): Promise { - const discoveredCommands = discoverCommandsSync(options?.directory ?? process.cwd(), { + const discoveredCommands = commandDiscovery.discoverCommandsSync(options?.directory ?? process.cwd(), { pluginsEnabled: options?.pluginsEnabled, enabledPluginsOverride: options?.enabledPluginsOverride, }) diff --git a/src/tools/slashcommand/command-discovery-deps.ts b/src/tools/slashcommand/command-discovery-deps.ts new file mode 100644 index 000000000..5465e0dfc --- /dev/null +++ b/src/tools/slashcommand/command-discovery-deps.ts @@ -0,0 +1,6 @@ +export { EXCLUDED_DIRS } from "../../shared/excluded-dirs" +export { parseFrontmatter } from "../../shared/frontmatter" +export { sanitizeModelField } from "../../shared/model-sanitizer" +export { getOpenCodeCommandDirs } from "../../shared/opencode-command-dirs" +export { discoverPluginCommandDefinitions } from "../../shared/plugin-command-discovery" +export { findProjectOpencodeCommandDirs } from "../../shared/project-discovery-dirs" diff --git a/src/tools/slashcommand/command-discovery.ts b/src/tools/slashcommand/command-discovery.ts index 855f6dc28..0900dec42 100644 --- a/src/tools/slashcommand/command-discovery.ts +++ b/src/tools/slashcommand/command-discovery.ts @@ -7,11 +7,12 @@ import { getOpenCodeCommandDirs, discoverPluginCommandDefinitions, EXCLUDED_DIRS, -} from "../../shared" +} from "./command-discovery-deps" import type { CommandFrontmatter } from "../../features/claude-code-command-loader/types" import { isMarkdownFile } from "../../shared/file-utils" -import { getClaudeConfigDir, log } from "../../shared" -import { loadBuiltinCommands } from "../../features/builtin-commands" +import { getClaudeConfigDir } from "../../shared/claude-config-dir" +import { log } from "../../shared/logger" +import { loadBuiltinCommands } from "../../features/builtin-commands/commands" import type { CommandInfo, CommandMetadata, CommandScope } from "./types" export interface CommandDiscoveryOptions { From 168057870ac9ff22d45caf4325c18577cff3c648 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Mon, 27 Apr 2026 13:31:49 +0900 Subject: [PATCH 05/10] test(skill): stabilize command discovery spy Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/tools/skill/tools.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tools/skill/tools.ts b/src/tools/skill/tools.ts index 1ffbe364b..81bb14485 100644 --- a/src/tools/skill/tools.ts +++ b/src/tools/skill/tools.ts @@ -7,7 +7,7 @@ import type { SkillArgs, SkillLoadOptions } from "./types" import type { LoadedSkill } from "../../features/opencode-skill-loader" import { clearSkillCache, getAllSkills } from "../../features/opencode-skill-loader/skill-content" import { injectGitMasterConfig } from "../../features/opencode-skill-loader/skill-content" -import { discoverCommandsSync } from "../slashcommand/command-discovery" +import * as commandDiscovery from "../slashcommand/command-discovery" import type { CommandInfo } from "../slashcommand/types" import { formatLoadedCommand } from "../slashcommand/command-output-formatter" import { formatCombinedDescription } from "./description-formatter" @@ -51,7 +51,7 @@ export function createSkillTool(options: SkillLoadOptions = {}): ToolDefinition } const getCommands = (): CommandInfo[] => { - return discoverCommandsSync(undefined, { + return commandDiscovery.discoverCommandsSync(undefined, { pluginsEnabled: options.pluginsEnabled, enabledPluginsOverride: options.enabledPluginsOverride, }) ?? [] From 8a49a03100d6cef976c73a21a15849c07fe08650 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Mon, 27 Apr 2026 13:37:51 +0900 Subject: [PATCH 06/10] test(skill): fresh import skill tool factory Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/tools/skill/tools.factory.test.ts | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/tools/skill/tools.factory.test.ts b/src/tools/skill/tools.factory.test.ts index b307d9589..b1e992607 100644 --- a/src/tools/skill/tools.factory.test.ts +++ b/src/tools/skill/tools.factory.test.ts @@ -46,6 +46,11 @@ function createMockContext(sessionID: string): ToolContext { } } +async function createSkillTool(...args: Parameters): ReturnType { + const module = await import(`./tools?test=${Date.now()}-${Math.random()}`) + return module.createSkillTool(...args) +} + beforeEach(() => { spyOn(commandDiscovery, "discoverCommandsSync").mockImplementation(discoverCommandsSync) spyOn(skillContent, "getAllSkills").mockImplementation(getAllSkills) @@ -63,8 +68,7 @@ describe("createSkillTool", () => { const baselineDiscoverCommandsSyncCalls = discoverCommandsSync.mock.calls.length // when - const { createSkillTool } = await import("./tools") - const skillTool = createSkillTool({}) + const skillTool = await createSkillTool({}) // then expect(discoverCommandsSync.mock.calls.length).toBe(baselineDiscoverCommandsSyncCalls) @@ -80,8 +84,7 @@ describe("createSkillTool", () => { const baselineGetAllSkillsCalls = getAllSkills.mock.calls.length // when - const { createSkillTool } = await import("./tools") - const skillTool = createSkillTool({}) + const skillTool = await createSkillTool({}) // then expect(getAllSkills.mock.calls.length).toBe(baselineGetAllSkillsCalls) @@ -97,8 +100,7 @@ describe("createSkillTool", () => { const sessionContext = createMockContext("session-clear-once") // when - const { createSkillTool } = await import("./tools") - const skillTool = createSkillTool({}) + const skillTool = await createSkillTool({}) void skillTool.description await flushMicrotasks() await skillTool.execute({ name: "lazy-skill" }, sessionContext) @@ -114,8 +116,7 @@ describe("createSkillTool", () => { const baselineGetAllSkillsCalls = getAllSkills.mock.calls.length const sessionAContext = createMockContext("session-a") const sessionBContext = createMockContext("session-b") - const { createSkillTool } = await import("./tools") - const skillTool = createSkillTool({}) + const skillTool = await createSkillTool({}) // when await skillTool.execute({ name: "lazy-skill" }, sessionAContext) From 467248535e03f3b724ac22fde667ddeea6abb011 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Mon, 27 Apr 2026 13:51:16 +0900 Subject: [PATCH 07/10] fix(agents): clear stale frontier tool denies Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/agents/builtin-agents/hephaestus-agent.ts | 11 +-- .../builtin-agents/sisyphus-agent.test.ts | 69 +++++++++++++++++++ src/agents/builtin-agents/sisyphus-agent.ts | 11 +-- src/agents/frontier-tool-schema-guard.ts | 27 ++++++++ src/agents/hephaestus/agent.test.ts | 69 +++++++++++++++++++ 5 files changed, 177 insertions(+), 10 deletions(-) diff --git a/src/agents/builtin-agents/hephaestus-agent.ts b/src/agents/builtin-agents/hephaestus-agent.ts index fc7b3d571..38fc5a63e 100644 --- a/src/agents/builtin-agents/hephaestus-agent.ts +++ b/src/agents/builtin-agents/hephaestus-agent.ts @@ -8,7 +8,7 @@ import { applyEnvironmentContext } from "./environment-context" import { applyCategoryOverride, mergeAgentConfig } from "./agent-overrides" import { applyModelResolution, getFirstFallbackModel } from "./model-resolution" import { getGptApplyPatchPermission } from "../gpt-apply-patch-guard" -import { getFrontierToolSchemaPermission } from "../frontier-tool-schema-guard" +import { applyFrontierToolSchemaPermission } from "../frontier-tool-schema-guard" export function maybeCreateHephaestusConfig(input: { disabledAgents: string[] @@ -90,10 +90,11 @@ export function maybeCreateHephaestusConfig(input: { } const resolvedModel = hephaestusConfig.model ?? "" - const frontierDeny = getFrontierToolSchemaPermission(resolvedModel) - if (Object.keys(frontierDeny).length > 0 && hephaestusConfig.permission) { - Object.assign(hephaestusConfig.permission, frontierDeny) - } + hephaestusConfig.permission = applyFrontierToolSchemaPermission( + hephaestusConfig.permission, + resolvedModel, + hephaestusOverride?.permission + ) const gptDeny = getGptApplyPatchPermission(resolvedModel) if (Object.keys(gptDeny).length > 0 && hephaestusConfig.permission) { diff --git a/src/agents/builtin-agents/sisyphus-agent.test.ts b/src/agents/builtin-agents/sisyphus-agent.test.ts index b5d14895a..1a799f303 100644 --- a/src/agents/builtin-agents/sisyphus-agent.test.ts +++ b/src/agents/builtin-agents/sisyphus-agent.test.ts @@ -143,6 +143,75 @@ describe("maybeCreateSisyphusConfig", () => { }); }); + describe("#given frontier default model with category override to non-frontier model", () => { + test("#when config is created #then stale grep and glob denies are cleared", () => { + // given + const agentOverrides: AgentOverrides = { + sisyphus: { + category: "non-frontier", + }, + }; + const mergedCategories: Record = { + "non-frontier": { + model: "openai/gpt-5.4", + }, + }; + + // when + const config = maybeCreateSisyphusConfig({ + disabledAgents: [], + agentOverrides, + availableModels: new Set(["anthropic/claude-opus-4-7", "openai/gpt-5.4"]), + systemDefaultModel: "anthropic/claude-opus-4-7", + isFirstRunNoCache: false, + availableAgents: [], + availableSkills: [], + availableCategories: [], + mergedCategories, + useTaskSystem: false, + }); + + // then + expect(config?.model).toBe("openai/gpt-5.4"); + expect(config?.permission).not.toHaveProperty("grep"); + expect(config?.permission).not.toHaveProperty("glob"); + }); + }); + + describe("#given non-frontier model with user override denying grep and glob", () => { + test("#when config is created #then explicit user denies are preserved", () => { + // given + const agentOverrides: AgentOverrides = { + sisyphus: { + model: "openai/gpt-5.4", + permission: { + grep: "deny", + glob: "deny", + } as Record, + }, + }; + const mergedCategories: Record = {}; + + // when + const config = maybeCreateSisyphusConfig({ + disabledAgents: [], + agentOverrides, + availableModels: new Set(["openai/gpt-5.4"]), + systemDefaultModel: "openai/gpt-5.4", + isFirstRunNoCache: false, + availableAgents: [], + availableSkills: [], + availableCategories: [], + mergedCategories, + useTaskSystem: false, + }); + + // then + expect(config?.permission).toHaveProperty("grep", "deny"); + expect(config?.permission).toHaveProperty("glob", "deny"); + }); + }); + describe("#given generic GPT model with user override allowing apply_patch", () => { test("#when config is created #then apply_patch is still denied", () => { // given diff --git a/src/agents/builtin-agents/sisyphus-agent.ts b/src/agents/builtin-agents/sisyphus-agent.ts index 1b111d6cd..ea1b82bf1 100644 --- a/src/agents/builtin-agents/sisyphus-agent.ts +++ b/src/agents/builtin-agents/sisyphus-agent.ts @@ -8,7 +8,7 @@ import { applyOverrides } from "./agent-overrides" import { applyModelResolution, getFirstFallbackModel } from "./model-resolution" import { createSisyphusAgent } from "../sisyphus" import { getGptApplyPatchPermission } from "../gpt-apply-patch-guard" -import { getFrontierToolSchemaPermission } from "../frontier-tool-schema-guard" +import { applyFrontierToolSchemaPermission } from "../frontier-tool-schema-guard" export function maybeCreateSisyphusConfig(input: { disabledAgents: string[] @@ -84,10 +84,11 @@ export function maybeCreateSisyphusConfig(input: { sisyphusConfig = applyOverrides(sisyphusConfig, sisyphusOverride, mergedCategories, directory) const resolvedModel = sisyphusConfig.model ?? "" - const frontierDeny = getFrontierToolSchemaPermission(resolvedModel) - if (Object.keys(frontierDeny).length > 0 && sisyphusConfig.permission) { - Object.assign(sisyphusConfig.permission, frontierDeny) - } + sisyphusConfig.permission = applyFrontierToolSchemaPermission( + sisyphusConfig.permission, + resolvedModel, + sisyphusOverride?.permission + ) const gptDeny = getGptApplyPatchPermission(resolvedModel) if (Object.keys(gptDeny).length > 0 && sisyphusConfig.permission) { diff --git a/src/agents/frontier-tool-schema-guard.ts b/src/agents/frontier-tool-schema-guard.ts index 028943dd8..4c6a0bd55 100644 --- a/src/agents/frontier-tool-schema-guard.ts +++ b/src/agents/frontier-tool-schema-guard.ts @@ -1,4 +1,9 @@ +import type { AgentConfig } from "@opencode-ai/sdk" import { isGpt5_5Model } from "./types" +import type { PermissionValue } from "../shared/permission-compat" + +const FRONTIER_TOOL_SCHEMA_NAMES = ["grep", "glob"] as const +type MutablePermission = Record> function isOpus47Model(model: string): boolean { const modelName = model.includes("/") ? (model.split("/").pop() ?? model) : model @@ -10,3 +15,25 @@ export function getFrontierToolSchemaPermission(model: string): Record 0) { + Object.assign(nextPermission, frontierDeny) + return nextPermission as AgentConfig["permission"] + } + + for (const toolName of FRONTIER_TOOL_SCHEMA_NAMES) { + if (explicitPermissionMap?.[toolName] === "deny") continue + delete nextPermission[toolName] + } + return nextPermission as AgentConfig["permission"] +} diff --git a/src/agents/hephaestus/agent.test.ts b/src/agents/hephaestus/agent.test.ts index 22f1d8c81..165f1e095 100644 --- a/src/agents/hephaestus/agent.test.ts +++ b/src/agents/hephaestus/agent.test.ts @@ -484,4 +484,73 @@ describe("maybeCreateHephaestusConfig GPT apply_patch guard", () => { expect(config?.permission).toHaveProperty("glob", "deny"); }); }); + + describe("#given frontier default model with category override to non-frontier model", () => { + test("#when config is created #then stale grep and glob denies are cleared", () => { + // given + const agentOverrides: AgentOverrides = { + hephaestus: { + category: "non-frontier", + }, + }; + const mergedCategories: Record = { + "non-frontier": { + model: "openai/gpt-5.4", + }, + }; + + // when + const config = maybeCreateHephaestusConfig({ + disabledAgents: [], + agentOverrides, + availableModels: new Set(["openai/gpt-5.5", "openai/gpt-5.4"]), + systemDefaultModel: "openai/gpt-5.5", + isFirstRunNoCache: false, + availableAgents: [], + availableSkills: [], + availableCategories: [], + mergedCategories, + useTaskSystem: false, + }); + + // then + expect(config?.model).toBe("openai/gpt-5.4"); + expect(config?.permission).not.toHaveProperty("grep"); + expect(config?.permission).not.toHaveProperty("glob"); + }); + }); + + describe("#given non-frontier model with user override denying grep and glob", () => { + test("#when config is created #then explicit user denies are preserved", () => { + // given + const agentOverrides: AgentOverrides = { + hephaestus: { + model: "openai/gpt-5.4", + permission: { + grep: "deny", + glob: "deny", + } as Record, + }, + }; + const mergedCategories: Record = {}; + + // when + const config = maybeCreateHephaestusConfig({ + disabledAgents: [], + agentOverrides, + availableModels: new Set(["openai/gpt-5.4"]), + systemDefaultModel: "openai/gpt-5.4", + isFirstRunNoCache: false, + availableAgents: [], + availableSkills: [], + availableCategories: [], + mergedCategories, + useTaskSystem: false, + }); + + // then + expect(config?.permission).toHaveProperty("grep", "deny"); + expect(config?.permission).toHaveProperty("glob", "deny"); + }); + }); }); From c65f90ee09fd9d9d3c89a2eaf4892a0a3ef62476 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Mon, 27 Apr 2026 13:51:16 +0900 Subject: [PATCH 08/10] fix(tmux): preserve configured fallback port Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/features/tmux-subagent/manager.test.ts | 73 +++++++++++++++++----- src/features/tmux-subagent/manager.ts | 3 +- 2 files changed, 60 insertions(+), 16 deletions(-) diff --git a/src/features/tmux-subagent/manager.test.ts b/src/features/tmux-subagent/manager.test.ts index e63f4bf4d..11724d1a6 100644 --- a/src/features/tmux-subagent/manager.test.ts +++ b/src/features/tmux-subagent/manager.test.ts @@ -3,7 +3,7 @@ import { describe, test, expect, mock, beforeEach, spyOn, afterAll } from 'bun:t import type { TmuxConfig } from '../../config/schema' import type { WindowState, PaneAction } from './types' import type { ActionResult, ExecuteContext } from './action-executor' -import type { TmuxUtilDeps } from './manager' +import type { TmuxSessionManager as TmuxSessionManagerType, TmuxUtilDeps } from './manager' import * as sharedModule from '../../shared' type ExecuteActionsResult = { @@ -287,24 +287,67 @@ describe('TmuxSessionManager', () => { test('falls back to default port when serverUrl has port 0', async () => { // given - mockIsInsideTmux.mockReturnValue(true) - const { TmuxSessionManager } = await import('./manager') - const ctx = { - ...createMockContext(), - serverUrl: new URL('http://127.0.0.1:0/'), - } - const config = createTmuxConfig({ enabled: true, - layout: 'main-vertical', - main_pane_size: 60, - main_pane_min_width: 80, - agent_pane_min_width: 40, }) + const previousOpenCodePort = process.env.OPENCODE_PORT + delete process.env.OPENCODE_PORT + let manager: TmuxSessionManagerType | undefined + try { + mockIsInsideTmux.mockReturnValue(true) + const { TmuxSessionManager } = await import('./manager') + const ctx = { + ...createMockContext(), + serverUrl: new URL('http://127.0.0.1:0/'), + } + const config = createTmuxConfig({ enabled: true, + layout: 'main-vertical', + main_pane_size: 60, + main_pane_min_width: 80, + agent_pane_min_width: 40, }) - // when - const manager = new TmuxSessionManager(ctx, config, mockTmuxDeps) + // when + manager = new TmuxSessionManager(ctx, config, mockTmuxDeps) + } finally { + if (previousOpenCodePort === undefined) { + delete process.env.OPENCODE_PORT + } else { + process.env.OPENCODE_PORT = previousOpenCodePort + } + } // then expect((manager as any).serverUrl).toBe('http://localhost:4096') }) + + test('falls back to configured OPENCODE_PORT when serverUrl has port 0', async () => { + // given + const previousOpenCodePort = process.env.OPENCODE_PORT + process.env.OPENCODE_PORT = '5678' + let manager: TmuxSessionManagerType | undefined + try { + mockIsInsideTmux.mockReturnValue(true) + const { TmuxSessionManager } = await import('./manager') + const ctx = { + ...createMockContext(), + serverUrl: new URL('http://127.0.0.1:0/'), + } + const config = createTmuxConfig({ enabled: true, + layout: 'main-vertical', + main_pane_size: 60, + main_pane_min_width: 80, + agent_pane_min_width: 40, }) + + // when + manager = new TmuxSessionManager(ctx, config, mockTmuxDeps) + } finally { + if (previousOpenCodePort === undefined) { + delete process.env.OPENCODE_PORT + } else { + process.env.OPENCODE_PORT = previousOpenCodePort + } + } + + // then + expect((manager as any).serverUrl).toBe('http://localhost:5678') + }) }) describe('onSessionCreated', () => { @@ -1989,7 +2032,7 @@ describe('TmuxSessionManager', () => { const cleanupPromise = manager.cleanup() // then - await expect(cleanupPromise).resolves.toBeUndefined() + expect(await cleanupPromise).toBeUndefined() expect(mockKillTmuxSessionIfExists).toHaveBeenCalledTimes(1) }) }) diff --git a/src/features/tmux-subagent/manager.ts b/src/features/tmux-subagent/manager.ts index a8d93ca16..3340fb55d 100644 --- a/src/features/tmux-subagent/manager.ts +++ b/src/features/tmux-subagent/manager.ts @@ -72,7 +72,8 @@ export class TmuxSessionManager { this.client = ctx.client this.tmuxConfig = tmuxConfig this.deps = deps - const fallbackUrl = "http://localhost:4096" + const defaultPort = process.env.OPENCODE_PORT ?? "4096" + const fallbackUrl = `http://localhost:${defaultPort}` const rawServerUrl = ctx.serverUrl?.toString() try { if (rawServerUrl) { From ac8987a73227c0acc1582f2662b7cd468a9eeb83 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Mon, 27 Apr 2026 14:24:42 +0900 Subject: [PATCH 09/10] fix(agents): preserve explicit legacy tool denies Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/agents/builtin-agents/hephaestus-agent.ts | 3 +- .../builtin-agents/sisyphus-agent.test.ts | 35 +++++++++++++++++++ src/agents/builtin-agents/sisyphus-agent.ts | 3 +- src/agents/frontier-tool-schema-guard.ts | 4 ++- src/agents/hephaestus/agent.test.ts | 35 +++++++++++++++++++ 5 files changed, 77 insertions(+), 3 deletions(-) diff --git a/src/agents/builtin-agents/hephaestus-agent.ts b/src/agents/builtin-agents/hephaestus-agent.ts index 38fc5a63e..c05b1fa71 100644 --- a/src/agents/builtin-agents/hephaestus-agent.ts +++ b/src/agents/builtin-agents/hephaestus-agent.ts @@ -93,7 +93,8 @@ export function maybeCreateHephaestusConfig(input: { hephaestusConfig.permission = applyFrontierToolSchemaPermission( hephaestusConfig.permission, resolvedModel, - hephaestusOverride?.permission + hephaestusOverride?.permission, + (hephaestusOverride as { tools?: Record } | undefined)?.tools ) const gptDeny = getGptApplyPatchPermission(resolvedModel) diff --git a/src/agents/builtin-agents/sisyphus-agent.test.ts b/src/agents/builtin-agents/sisyphus-agent.test.ts index 1a799f303..b32ede5af 100644 --- a/src/agents/builtin-agents/sisyphus-agent.test.ts +++ b/src/agents/builtin-agents/sisyphus-agent.test.ts @@ -212,6 +212,41 @@ describe("maybeCreateSisyphusConfig", () => { }); }); + describe("#given non-frontier model with legacy user tools denying grep and glob", () => { + test("#when config is created #then explicit legacy denies are preserved", () => { + // given + const legacyOverride = { + model: "openai/gpt-5.4", + tools: { + grep: false, + glob: false, + }, + }; + const agentOverrides: AgentOverrides = { + sisyphus: legacyOverride, + }; + const mergedCategories: Record = {}; + + // when + const config = maybeCreateSisyphusConfig({ + disabledAgents: [], + agentOverrides, + availableModels: new Set(["openai/gpt-5.4"]), + systemDefaultModel: "openai/gpt-5.4", + isFirstRunNoCache: false, + availableAgents: [], + availableSkills: [], + availableCategories: [], + mergedCategories, + useTaskSystem: false, + }); + + // then + expect(config?.permission).toHaveProperty("grep", "deny"); + expect(config?.permission).toHaveProperty("glob", "deny"); + }); + }); + describe("#given generic GPT model with user override allowing apply_patch", () => { test("#when config is created #then apply_patch is still denied", () => { // given diff --git a/src/agents/builtin-agents/sisyphus-agent.ts b/src/agents/builtin-agents/sisyphus-agent.ts index ea1b82bf1..6cb91370f 100644 --- a/src/agents/builtin-agents/sisyphus-agent.ts +++ b/src/agents/builtin-agents/sisyphus-agent.ts @@ -87,7 +87,8 @@ export function maybeCreateSisyphusConfig(input: { sisyphusConfig.permission = applyFrontierToolSchemaPermission( sisyphusConfig.permission, resolvedModel, - sisyphusOverride?.permission + sisyphusOverride?.permission, + (sisyphusOverride as { tools?: Record } | undefined)?.tools ) const gptDeny = getGptApplyPatchPermission(resolvedModel) diff --git a/src/agents/frontier-tool-schema-guard.ts b/src/agents/frontier-tool-schema-guard.ts index 4c6a0bd55..68158149a 100644 --- a/src/agents/frontier-tool-schema-guard.ts +++ b/src/agents/frontier-tool-schema-guard.ts @@ -19,7 +19,8 @@ export function getFrontierToolSchemaPermission(model: string): Record ): AgentConfig["permission"] | undefined { if (!permission) return permission @@ -33,6 +34,7 @@ export function applyFrontierToolSchemaPermission( for (const toolName of FRONTIER_TOOL_SCHEMA_NAMES) { if (explicitPermissionMap?.[toolName] === "deny") continue + if (explicitTools?.[toolName] === false) continue delete nextPermission[toolName] } return nextPermission as AgentConfig["permission"] diff --git a/src/agents/hephaestus/agent.test.ts b/src/agents/hephaestus/agent.test.ts index 165f1e095..ca1d65c01 100644 --- a/src/agents/hephaestus/agent.test.ts +++ b/src/agents/hephaestus/agent.test.ts @@ -553,4 +553,39 @@ describe("maybeCreateHephaestusConfig GPT apply_patch guard", () => { expect(config?.permission).toHaveProperty("glob", "deny"); }); }); + + describe("#given non-frontier model with legacy user tools denying grep and glob", () => { + test("#when config is created #then explicit legacy denies are preserved", () => { + // given + const legacyOverride = { + model: "openai/gpt-5.4", + tools: { + grep: false, + glob: false, + }, + }; + const agentOverrides: AgentOverrides = { + hephaestus: legacyOverride, + }; + const mergedCategories: Record = {}; + + // when + const config = maybeCreateHephaestusConfig({ + disabledAgents: [], + agentOverrides, + availableModels: new Set(["openai/gpt-5.4"]), + systemDefaultModel: "openai/gpt-5.4", + isFirstRunNoCache: false, + availableAgents: [], + availableSkills: [], + availableCategories: [], + mergedCategories, + useTaskSystem: false, + }); + + // then + expect(config?.permission).toHaveProperty("grep", "deny"); + expect(config?.permission).toHaveProperty("glob", "deny"); + }); + }); }); From acf293de96d1d0e83fbfc7d16cb4f8da5d9f727f Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Mon, 27 Apr 2026 14:24:42 +0900 Subject: [PATCH 10/10] fix(tmux): validate fallback port Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/features/tmux-subagent/manager.test.ts | 32 ++++++++++++++++++++++ src/features/tmux-subagent/manager.ts | 6 +++- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/src/features/tmux-subagent/manager.test.ts b/src/features/tmux-subagent/manager.test.ts index 11724d1a6..943f246fb 100644 --- a/src/features/tmux-subagent/manager.test.ts +++ b/src/features/tmux-subagent/manager.test.ts @@ -348,6 +348,38 @@ describe('TmuxSessionManager', () => { // then expect((manager as any).serverUrl).toBe('http://localhost:5678') }) + + test('ignores invalid OPENCODE_PORT when serverUrl has port 0', async () => { + // given + const previousOpenCodePort = process.env.OPENCODE_PORT + process.env.OPENCODE_PORT = 'not-a-port' + let manager: TmuxSessionManagerType | undefined + try { + mockIsInsideTmux.mockReturnValue(true) + const { TmuxSessionManager } = await import('./manager') + const ctx = { + ...createMockContext(), + serverUrl: new URL('http://127.0.0.1:0/'), + } + const config = createTmuxConfig({ enabled: true, + layout: 'main-vertical', + main_pane_size: 60, + main_pane_min_width: 80, + agent_pane_min_width: 40, }) + + // when + manager = new TmuxSessionManager(ctx, config, mockTmuxDeps) + } finally { + if (previousOpenCodePort === undefined) { + delete process.env.OPENCODE_PORT + } else { + process.env.OPENCODE_PORT = previousOpenCodePort + } + } + + // then + expect((manager as any).serverUrl).toBe('http://localhost:4096') + }) }) describe('onSessionCreated', () => { diff --git a/src/features/tmux-subagent/manager.ts b/src/features/tmux-subagent/manager.ts index 3340fb55d..353bdffec 100644 --- a/src/features/tmux-subagent/manager.ts +++ b/src/features/tmux-subagent/manager.ts @@ -72,7 +72,11 @@ export class TmuxSessionManager { this.client = ctx.client this.tmuxConfig = tmuxConfig this.deps = deps - const defaultPort = process.env.OPENCODE_PORT ?? "4096" + const configuredPort = process.env.OPENCODE_PORT + const parsedPort = configuredPort ? Number(configuredPort) : 4096 + const defaultPort = Number.isInteger(parsedPort) && parsedPort > 0 && parsedPort <= 65535 + ? String(parsedPort) + : "4096" const fallbackUrl = `http://localhost:${defaultPort}` const rawServerUrl = ctx.serverUrl?.toString() try {