feat(multimodal-looker): restrict to read-only tool access
Use createAgentToolAllowlist to allow only 'read' tool for multimodal-looker agent. Previously denied write/edit/bash but allowed other tools. Now uses wildcard deny pattern (*: deny) with explicit read allow. - Add createAgentToolAllowlist function for allowlist-based restrictions - Support legacy fallback for older OpenCode versions - Add 4 test cases covering both permission systems
This commit is contained in:
parent
27ef9fa8df
commit
ede9abceb3
@ -1,6 +1,6 @@
|
|||||||
import type { AgentConfig } from "@opencode-ai/sdk"
|
import type { AgentConfig } from "@opencode-ai/sdk"
|
||||||
import type { AgentPromptMetadata } from "./types"
|
import type { AgentPromptMetadata } from "./types"
|
||||||
import { createAgentToolRestrictions } from "../shared/permission-compat"
|
import { createAgentToolAllowlist } from "../shared/permission-compat"
|
||||||
|
|
||||||
const DEFAULT_MODEL = "google/gemini-3-flash"
|
const DEFAULT_MODEL = "google/gemini-3-flash"
|
||||||
|
|
||||||
@ -14,11 +14,7 @@ export const MULTIMODAL_LOOKER_PROMPT_METADATA: AgentPromptMetadata = {
|
|||||||
export function createMultimodalLookerAgent(
|
export function createMultimodalLookerAgent(
|
||||||
model: string = DEFAULT_MODEL
|
model: string = DEFAULT_MODEL
|
||||||
): AgentConfig {
|
): AgentConfig {
|
||||||
const restrictions = createAgentToolRestrictions([
|
const restrictions = createAgentToolAllowlist(["read"])
|
||||||
"write",
|
|
||||||
"edit",
|
|
||||||
"bash",
|
|
||||||
])
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
description:
|
description:
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
import type { AgentConfig } from "@opencode-ai/sdk"
|
import type { AgentConfig } from "@opencode-ai/sdk"
|
||||||
|
import { isGptModel } from "./types"
|
||||||
import type { AgentOverrideConfig, CategoryConfig } from "../config/schema"
|
import type { AgentOverrideConfig, CategoryConfig } from "../config/schema"
|
||||||
import {
|
import {
|
||||||
createAgentToolRestrictions,
|
createAgentToolRestrictions,
|
||||||
migrateAgentConfig,
|
migrateAgentConfig,
|
||||||
supportsNewPermissionSystem,
|
supportsNewPermissionSystem,
|
||||||
} from "../shared/permission-compat"
|
} from "../shared/permission-compat"
|
||||||
import { isGptModel } from "./types"
|
|
||||||
|
|
||||||
const SISYPHUS_JUNIOR_PROMPT = `<Role>
|
const SISYPHUS_JUNIOR_PROMPT = `<Role>
|
||||||
Sisyphus-Junior - Focused executor from OhMyOpenCode.
|
Sisyphus-Junior - Focused executor from OhMyOpenCode.
|
||||||
@ -58,6 +58,7 @@ No todos on multi-step work = INCOMPLETE WORK.
|
|||||||
|
|
||||||
<Verification>
|
<Verification>
|
||||||
Task NOT complete without:
|
Task NOT complete without:
|
||||||
|
- lsp_diagnostics clean on changed files
|
||||||
- Build passes (if applicable)
|
- Build passes (if applicable)
|
||||||
- All todos marked completed
|
- All todos marked completed
|
||||||
</Verification>
|
</Verification>
|
||||||
@ -84,7 +85,7 @@ export const SISYPHUS_JUNIOR_DEFAULTS = {
|
|||||||
|
|
||||||
export function createSisyphusJuniorAgentWithOverrides(
|
export function createSisyphusJuniorAgentWithOverrides(
|
||||||
override: AgentOverrideConfig | undefined,
|
override: AgentOverrideConfig | undefined,
|
||||||
systemDefaultModel?: string,
|
systemDefaultModel?: string
|
||||||
): AgentConfig {
|
): AgentConfig {
|
||||||
if (override?.disable) {
|
if (override?.disable) {
|
||||||
override = undefined
|
override = undefined
|
||||||
@ -120,8 +121,7 @@ export function createSisyphusJuniorAgentWithOverrides(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const base: AgentConfig = {
|
const base: AgentConfig = {
|
||||||
description:
|
description: override?.description ??
|
||||||
override?.description ??
|
|
||||||
"Sisyphus-Junior - Focused task executor. Same discipline, no delegation.",
|
"Sisyphus-Junior - Focused task executor. Same discipline, no delegation.",
|
||||||
mode: "subagent" as const,
|
mode: "subagent" as const,
|
||||||
model,
|
model,
|
||||||
@ -148,7 +148,7 @@ export function createSisyphusJuniorAgentWithOverrides(
|
|||||||
|
|
||||||
export function createSisyphusJuniorAgent(
|
export function createSisyphusJuniorAgent(
|
||||||
categoryConfig: CategoryConfig,
|
categoryConfig: CategoryConfig,
|
||||||
promptAppend?: string,
|
promptAppend?: string
|
||||||
): AgentConfig {
|
): AgentConfig {
|
||||||
const prompt = buildSisyphusJuniorPrompt(promptAppend)
|
const prompt = buildSisyphusJuniorPrompt(promptAppend)
|
||||||
const model = categoryConfig.model
|
const model = categoryConfig.model
|
||||||
@ -158,8 +158,10 @@ export function createSisyphusJuniorAgent(
|
|||||||
...(categoryConfig.tools ? { tools: categoryConfig.tools } : {}),
|
...(categoryConfig.tools ? { tools: categoryConfig.tools } : {}),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
const base: AgentConfig = {
|
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,
|
mode: "subagent" as const,
|
||||||
model,
|
model,
|
||||||
maxTokens: categoryConfig.maxTokens ?? 64000,
|
maxTokens: categoryConfig.maxTokens ?? 64000,
|
||||||
|
|||||||
@ -1,18 +1,18 @@
|
|||||||
import type { AgentConfig } from "@opencode-ai/sdk"
|
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 {
|
import {
|
||||||
buildAntiPatternsSection,
|
|
||||||
buildDelegationTable,
|
|
||||||
buildExploreSection,
|
|
||||||
buildFrontendSection,
|
|
||||||
buildHardBlocksSection,
|
|
||||||
buildKeyTriggersSection,
|
buildKeyTriggersSection,
|
||||||
buildLibrarianSection,
|
|
||||||
buildOracleSection,
|
|
||||||
buildToolSelectionTable,
|
buildToolSelectionTable,
|
||||||
|
buildExploreSection,
|
||||||
|
buildLibrarianSection,
|
||||||
|
buildDelegationTable,
|
||||||
|
buildFrontendSection,
|
||||||
|
buildOracleSection,
|
||||||
|
buildHardBlocksSection,
|
||||||
|
buildAntiPatternsSection,
|
||||||
categorizeTools,
|
categorizeTools,
|
||||||
} from "./sisyphus-prompt-builder"
|
} from "./sisyphus-prompt-builder"
|
||||||
import { isGptModel } from "./types"
|
|
||||||
|
|
||||||
const DEFAULT_MODEL = "anthropic/claude-opus-4-5"
|
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
|
2. **Implement**: Make the necessary changes
|
||||||
- Follow existing codebase patterns
|
- Follow existing codebase patterns
|
||||||
- Add tests if applicable
|
- Add tests if applicable
|
||||||
|
- Verify with lsp_diagnostics
|
||||||
3. **Verify**: Ensure everything works
|
3. **Verify**: Ensure everything works
|
||||||
- Run build if exists
|
- Run build if exists
|
||||||
- Run tests if exists
|
- Run tests if exists
|
||||||
@ -360,12 +361,18 @@ const SISYPHUS_CODE_CHANGES = `### Code Changes:
|
|||||||
|
|
||||||
### Verification:
|
### 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.
|
If project has build/test commands, run them at task completion.
|
||||||
|
|
||||||
### Evidence Requirements (task NOT complete without these):
|
### Evidence Requirements (task NOT complete without these):
|
||||||
|
|
||||||
| Action | Required Evidence |
|
| Action | Required Evidence |
|
||||||
|--------|-------------------|
|
|--------|-------------------|
|
||||||
|
| File edit | \`lsp_diagnostics\` clean on changed files |
|
||||||
| Build command | Exit code 0 |
|
| Build command | Exit code 0 |
|
||||||
| Test run | Pass (or explicit note of pre-existing failures) |
|
| Test run | Pass (or explicit note of pre-existing failures) |
|
||||||
| Delegation | Agent result received and verified |
|
| Delegation | Agent result received and verified |
|
||||||
@ -394,6 +401,7 @@ const SISYPHUS_PHASE3 = `## Phase 3 - Completion
|
|||||||
|
|
||||||
A task is complete when:
|
A task is complete when:
|
||||||
- [ ] All planned todo items marked done
|
- [ ] All planned todo items marked done
|
||||||
|
- [ ] Diagnostics clean on changed files
|
||||||
- [ ] Build passes (if applicable)
|
- [ ] Build passes (if applicable)
|
||||||
- [ ] User's original request fully addressed
|
- [ ] User's original request fully addressed
|
||||||
|
|
||||||
@ -517,7 +525,7 @@ const SISYPHUS_SOFT_GUIDELINES = `## Soft Guidelines
|
|||||||
function buildDynamicSisyphusPrompt(
|
function buildDynamicSisyphusPrompt(
|
||||||
availableAgents: AvailableAgent[],
|
availableAgents: AvailableAgent[],
|
||||||
availableTools: AvailableTool[] = [],
|
availableTools: AvailableTool[] = [],
|
||||||
availableSkills: AvailableSkill[] = [],
|
availableSkills: AvailableSkill[] = []
|
||||||
): string {
|
): string {
|
||||||
const keyTriggers = buildKeyTriggersSection(availableAgents, availableSkills)
|
const keyTriggers = buildKeyTriggersSection(availableAgents, availableSkills)
|
||||||
const toolSelection = buildToolSelectionTable(availableAgents, availableTools, availableSkills)
|
const toolSelection = buildToolSelectionTable(availableAgents, availableTools, availableSkills)
|
||||||
@ -602,7 +610,7 @@ export function createSisyphusAgent(
|
|||||||
model: string = DEFAULT_MODEL,
|
model: string = DEFAULT_MODEL,
|
||||||
availableAgents?: AvailableAgent[],
|
availableAgents?: AvailableAgent[],
|
||||||
availableToolNames?: string[],
|
availableToolNames?: string[],
|
||||||
availableSkills?: AvailableSkill[],
|
availableSkills?: AvailableSkill[]
|
||||||
): AgentConfig {
|
): AgentConfig {
|
||||||
const tools = availableToolNames ? categorizeTools(availableToolNames) : []
|
const tools = availableToolNames ? categorizeTools(availableToolNames) : []
|
||||||
const skills = availableSkills ?? []
|
const skills = availableSkills ?? []
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { describe, test, expect, beforeEach, afterEach } from "bun:test"
|
import { describe, test, expect, beforeEach, afterEach } from "bun:test"
|
||||||
import {
|
import {
|
||||||
createAgentToolRestrictions,
|
createAgentToolRestrictions,
|
||||||
|
createAgentToolAllowlist,
|
||||||
migrateToolsToPermission,
|
migrateToolsToPermission,
|
||||||
migratePermissionToTools,
|
migratePermissionToTools,
|
||||||
migrateAgentConfig,
|
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<string, boolean> }).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<string, boolean> }).tools
|
||||||
|
expect(tools.glob).toBeUndefined()
|
||||||
|
expect(tools.write).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe("migrateToolsToPermission", () => {
|
describe("migrateToolsToPermission", () => {
|
||||||
test("converts boolean tools to permission values", () => {
|
test("converts boolean tools to permission values", () => {
|
||||||
// #given tools config
|
// #given tools config
|
||||||
|
|||||||
@ -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(
|
export function migrateToolsToPermission(
|
||||||
tools: Record<string, boolean>
|
tools: Record<string, boolean>
|
||||||
): Record<string, PermissionValue> {
|
): Record<string, PermissionValue> {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user