diff --git a/src/agents/multimodal-looker.ts b/src/agents/multimodal-looker.ts index 0c1370d8c..010fda0cf 100644 --- a/src/agents/multimodal-looker.ts +++ b/src/agents/multimodal-looker.ts @@ -1,6 +1,6 @@ import type { AgentConfig } from "@opencode-ai/sdk" import type { AgentPromptMetadata } from "./types" -import { createAgentToolRestrictions } from "../shared/permission-compat" +import { createAgentToolAllowlist } from "../shared/permission-compat" const DEFAULT_MODEL = "google/gemini-3-flash" @@ -14,11 +14,7 @@ export const MULTIMODAL_LOOKER_PROMPT_METADATA: AgentPromptMetadata = { export function createMultimodalLookerAgent( model: string = DEFAULT_MODEL ): AgentConfig { - const restrictions = createAgentToolRestrictions([ - "write", - "edit", - "bash", - ]) + const restrictions = createAgentToolAllowlist(["read"]) return { description: diff --git a/src/agents/sisyphus-junior.ts b/src/agents/sisyphus-junior.ts index 011750c61..690b3eeba 100644 --- a/src/agents/sisyphus-junior.ts +++ b/src/agents/sisyphus-junior.ts @@ -1,11 +1,11 @@ import type { AgentConfig } from "@opencode-ai/sdk" +import { isGptModel } from "./types" import type { AgentOverrideConfig, CategoryConfig } from "../config/schema" import { createAgentToolRestrictions, migrateAgentConfig, supportsNewPermissionSystem, } from "../shared/permission-compat" -import { isGptModel } from "./types" const SISYPHUS_JUNIOR_PROMPT = ` Sisyphus-Junior - Focused executor from OhMyOpenCode. @@ -58,6 +58,7 @@ No todos on multi-step work = INCOMPLETE WORK. Task NOT complete without: +- lsp_diagnostics clean on changed files - Build passes (if applicable) - All todos marked completed @@ -84,7 +85,7 @@ export const SISYPHUS_JUNIOR_DEFAULTS = { export function createSisyphusJuniorAgentWithOverrides( override: AgentOverrideConfig | undefined, - systemDefaultModel?: string, + systemDefaultModel?: string ): AgentConfig { if (override?.disable) { override = undefined @@ -120,8 +121,7 @@ export function createSisyphusJuniorAgentWithOverrides( } const base: AgentConfig = { - description: - override?.description ?? + description: override?.description ?? "Sisyphus-Junior - Focused task executor. Same discipline, no delegation.", mode: "subagent" as const, model, @@ -148,7 +148,7 @@ export function createSisyphusJuniorAgentWithOverrides( export function createSisyphusJuniorAgent( categoryConfig: CategoryConfig, - promptAppend?: string, + promptAppend?: string ): AgentConfig { const prompt = buildSisyphusJuniorPrompt(promptAppend) const model = categoryConfig.model @@ -158,8 +158,10 @@ export function createSisyphusJuniorAgent( ...(categoryConfig.tools ? { tools: categoryConfig.tools } : {}), }) + const base: AgentConfig = { - description: "Sisyphus-Junior - Focused task executor. Same discipline, no delegation.", + description: + "Sisyphus-Junior - Focused task executor. Same discipline, no delegation.", mode: "subagent" as const, model, maxTokens: categoryConfig.maxTokens ?? 64000, diff --git a/src/agents/sisyphus.ts b/src/agents/sisyphus.ts index 4e8898fa3..fe45b68ee 100644 --- a/src/agents/sisyphus.ts +++ b/src/agents/sisyphus.ts @@ -1,18 +1,18 @@ import type { AgentConfig } from "@opencode-ai/sdk" -import type { AvailableAgent, AvailableSkill, AvailableTool } from "./sisyphus-prompt-builder" +import { isGptModel } from "./types" +import type { AvailableAgent, AvailableTool, AvailableSkill } from "./sisyphus-prompt-builder" import { - buildAntiPatternsSection, - buildDelegationTable, - buildExploreSection, - buildFrontendSection, - buildHardBlocksSection, buildKeyTriggersSection, - buildLibrarianSection, - buildOracleSection, buildToolSelectionTable, + buildExploreSection, + buildLibrarianSection, + buildDelegationTable, + buildFrontendSection, + buildOracleSection, + buildHardBlocksSection, + buildAntiPatternsSection, categorizeTools, } from "./sisyphus-prompt-builder" -import { isGptModel } from "./types" const DEFAULT_MODEL = "anthropic/claude-opus-4-5" @@ -336,6 +336,7 @@ When you're mentioned in GitHub issues or asked to "look into" something and "cr 2. **Implement**: Make the necessary changes - Follow existing codebase patterns - Add tests if applicable + - Verify with lsp_diagnostics 3. **Verify**: Ensure everything works - Run build if exists - Run tests if exists @@ -360,12 +361,18 @@ const SISYPHUS_CODE_CHANGES = `### Code Changes: ### Verification: +Run \`lsp_diagnostics\` on changed files at: +- End of a logical task unit +- Before marking a todo item complete +- Before reporting completion to user + If project has build/test commands, run them at task completion. ### Evidence Requirements (task NOT complete without these): | Action | Required Evidence | |--------|-------------------| +| File edit | \`lsp_diagnostics\` clean on changed files | | Build command | Exit code 0 | | Test run | Pass (or explicit note of pre-existing failures) | | Delegation | Agent result received and verified | @@ -394,6 +401,7 @@ const SISYPHUS_PHASE3 = `## Phase 3 - Completion A task is complete when: - [ ] All planned todo items marked done +- [ ] Diagnostics clean on changed files - [ ] Build passes (if applicable) - [ ] User's original request fully addressed @@ -517,7 +525,7 @@ const SISYPHUS_SOFT_GUIDELINES = `## Soft Guidelines function buildDynamicSisyphusPrompt( availableAgents: AvailableAgent[], availableTools: AvailableTool[] = [], - availableSkills: AvailableSkill[] = [], + availableSkills: AvailableSkill[] = [] ): string { const keyTriggers = buildKeyTriggersSection(availableAgents, availableSkills) const toolSelection = buildToolSelectionTable(availableAgents, availableTools, availableSkills) @@ -602,7 +610,7 @@ export function createSisyphusAgent( model: string = DEFAULT_MODEL, availableAgents?: AvailableAgent[], availableToolNames?: string[], - availableSkills?: AvailableSkill[], + availableSkills?: AvailableSkill[] ): AgentConfig { const tools = availableToolNames ? categorizeTools(availableToolNames) : [] const skills = availableSkills ?? [] diff --git a/src/shared/permission-compat.test.ts b/src/shared/permission-compat.test.ts index c277ca768..73b71ac3f 100644 --- a/src/shared/permission-compat.test.ts +++ b/src/shared/permission-compat.test.ts @@ -1,6 +1,7 @@ import { describe, test, expect, beforeEach, afterEach } from "bun:test" import { createAgentToolRestrictions, + createAgentToolAllowlist, migrateToolsToPermission, migratePermissionToTools, migrateAgentConfig, @@ -57,6 +58,63 @@ describe("permission-compat", () => { }) }) + describe("createAgentToolAllowlist", () => { + test("returns wildcard deny with explicit allow for v1.1.1+", () => { + // #given version is 1.1.1 + setVersionCache("1.1.1") + + // #when creating allowlist + const result = createAgentToolAllowlist(["read"]) + + // #then returns wildcard deny with read allow + expect(result).toEqual({ + permission: { "*": "deny", read: "allow" }, + }) + }) + + test("returns wildcard deny with multiple allows for v1.1.1+", () => { + // #given version is 1.1.1 + setVersionCache("1.1.1") + + // #when creating allowlist with multiple tools + const result = createAgentToolAllowlist(["read", "glob"]) + + // #then returns wildcard deny with both allows + expect(result).toEqual({ + permission: { "*": "deny", read: "allow", glob: "allow" }, + }) + }) + + test("returns explicit deny list for old versions", () => { + // #given version is below 1.1.1 + setVersionCache("1.0.150") + + // #when creating allowlist + const result = createAgentToolAllowlist(["read"]) + + // #then returns tools format with common tools denied except read + expect(result).toHaveProperty("tools") + const tools = (result as { tools: Record }).tools + expect(tools.write).toBe(false) + expect(tools.edit).toBe(false) + expect(tools.bash).toBe(false) + expect(tools.read).toBeUndefined() + }) + + test("excludes allowed tools from legacy deny list", () => { + // #given version is below 1.1.1 + setVersionCache("1.0.150") + + // #when creating allowlist with glob + const result = createAgentToolAllowlist(["read", "glob"]) + + // #then glob is not in deny list + const tools = (result as { tools: Record }).tools + expect(tools.glob).toBeUndefined() + expect(tools.write).toBe(false) + }) + }) + describe("migrateToolsToPermission", () => { test("converts boolean tools to permission values", () => { // #given tools config diff --git a/src/shared/permission-compat.ts b/src/shared/permission-compat.ts index 08cf57807..74a398b48 100644 --- a/src/shared/permission-compat.ts +++ b/src/shared/permission-compat.ts @@ -30,6 +30,69 @@ export function createAgentToolRestrictions( } } +/** + * Common tools that should be denied when using allowlist approach. + * Used for legacy fallback when `*: deny` pattern is not supported. + */ +const COMMON_TOOLS_TO_DENY = [ + "write", + "edit", + "bash", + "task", + "sisyphus_task", + "call_omo_agent", + "webfetch", + "glob", + "grep", + "lsp_diagnostics", + "lsp_prepare_rename", + "lsp_rename", + "ast_grep_search", + "ast_grep_replace", + "session_list", + "session_read", + "session_search", + "session_info", + "background_output", + "background_cancel", + "skill", + "skill_mcp", + "look_at", + "todowrite", + "todoread", + "interactive_bash", +] as const + +/** + * Creates tool restrictions that ONLY allow specified tools. + * All other tools are denied by default. + * + * Uses `*: deny` pattern for new permission system, + * falls back to explicit deny list for legacy systems. + */ +export function createAgentToolAllowlist( + allowTools: string[] +): VersionAwareRestrictions { + if (supportsNewPermissionSystem()) { + return { + permission: { + "*": "deny" as const, + ...Object.fromEntries( + allowTools.map((tool) => [tool, "allow" as const]) + ), + }, + } + } + + // Legacy fallback: explicitly deny common tools except allowed ones + const allowSet = new Set(allowTools) + const denyTools = COMMON_TOOLS_TO_DENY.filter((tool) => !allowSet.has(tool)) + + return { + tools: Object.fromEntries(denyTools.map((tool) => [tool, false])), + } +} + export function migrateToolsToPermission( tools: Record ): Record {