fix: proper OpenCode v1.1.1 permission migration (#490)
* fix: implement proper version-aware permission format for OpenCode v1.1.1 - Rewrite permission-compat.ts with runtime version detection - createAgentToolRestrictions() returns correct format per version - v1.1.1+ uses permission format, older uses tools format - Add migrateToolsToPermission/migratePermissionToTools helpers - Update test suite for new API 🤖 Generated with assistance of OhMyOpenCode https://github.com/code-yeongyu/oh-my-opencode * fix: update all agents to use createAgentToolRestrictions() - Replace hardcoded tools: { X: false } format with version-aware utility - All agents now use createAgentToolRestrictions([...]) - Ensures compatibility with both old and new OpenCode versions 🤖 Generated with assistance of OhMyOpenCode https://github.com/code-yeongyu/oh-my-opencode * fix: add runtime migration for user agent configs in config-handler Migrate tools/permission format in user/project/plugin agent configs based on detected OpenCode version at load time. 🤖 Generated with [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode)
This commit is contained in:
@@ -1,5 +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"
|
||||||
|
|
||||||
const DEFAULT_MODEL = "google/gemini-3-flash-preview"
|
const DEFAULT_MODEL = "google/gemini-3-flash-preview"
|
||||||
|
|
||||||
@@ -15,12 +16,14 @@ export const DOCUMENT_WRITER_PROMPT_METADATA: AgentPromptMetadata = {
|
|||||||
export function createDocumentWriterAgent(
|
export function createDocumentWriterAgent(
|
||||||
model: string = DEFAULT_MODEL
|
model: string = DEFAULT_MODEL
|
||||||
): AgentConfig {
|
): AgentConfig {
|
||||||
|
const restrictions = createAgentToolRestrictions(["background_task"])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
description:
|
description:
|
||||||
"A technical writer who crafts clear, comprehensive documentation. Specializes in README files, API docs, architecture docs, and user guides. MUST BE USED when executing documentation tasks from ai-todo list plans.",
|
"A technical writer who crafts clear, comprehensive documentation. Specializes in README files, API docs, architecture docs, and user guides. MUST BE USED when executing documentation tasks from ai-todo list plans.",
|
||||||
mode: "subagent" as const,
|
mode: "subagent" as const,
|
||||||
model,
|
model,
|
||||||
tools: { background_task: false },
|
...restrictions,
|
||||||
prompt: `<role>
|
prompt: `<role>
|
||||||
You are a TECHNICAL WRITER with deep engineering background who transforms complex codebases into crystal-clear documentation. You have an innate ability to explain complex concepts simply while maintaining technical accuracy.
|
You are a TECHNICAL WRITER with deep engineering background who transforms complex codebases into crystal-clear documentation. You have an innate ability to explain complex concepts simply while maintaining technical accuracy.
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +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"
|
||||||
|
|
||||||
const DEFAULT_MODEL = "opencode/grok-code"
|
const DEFAULT_MODEL = "opencode/grok-code"
|
||||||
|
|
||||||
@@ -24,14 +25,19 @@ export const EXPLORE_PROMPT_METADATA: AgentPromptMetadata = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function createExploreAgent(model: string = DEFAULT_MODEL): AgentConfig {
|
export function createExploreAgent(model: string = DEFAULT_MODEL): AgentConfig {
|
||||||
|
const restrictions = createAgentToolRestrictions([
|
||||||
|
"write",
|
||||||
|
"edit",
|
||||||
|
"background_task",
|
||||||
|
])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
description:
|
description:
|
||||||
'Contextual grep for codebases. Answers "Where is X?", "Which file has Y?", "Find the code that does Z". Fire multiple in parallel for broad searches. Specify thoroughness: "quick" for basic, "medium" for moderate, "very thorough" for comprehensive analysis.',
|
'Contextual grep for codebases. Answers "Where is X?", "Which file has Y?", "Find the code that does Z". Fire multiple in parallel for broad searches. Specify thoroughness: "quick" for basic, "medium" for moderate, "very thorough" for comprehensive analysis.',
|
||||||
mode: "subagent" as const,
|
mode: "subagent" as const,
|
||||||
model,
|
model,
|
||||||
temperature: 0.1,
|
temperature: 0.1,
|
||||||
tools: { write: false, background_task: false },
|
...restrictions,
|
||||||
permission: { edit: "deny" as const },
|
|
||||||
prompt: `You are a codebase search specialist. Your job: find files and code, return actionable results.
|
prompt: `You are a codebase search specialist. Your job: find files and code, return actionable results.
|
||||||
|
|
||||||
## Your Mission
|
## Your Mission
|
||||||
|
|||||||
@@ -1,5 +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"
|
||||||
|
|
||||||
const DEFAULT_MODEL = "google/gemini-3-pro-preview"
|
const DEFAULT_MODEL = "google/gemini-3-pro-preview"
|
||||||
|
|
||||||
@@ -21,12 +22,14 @@ export const FRONTEND_PROMPT_METADATA: AgentPromptMetadata = {
|
|||||||
export function createFrontendUiUxEngineerAgent(
|
export function createFrontendUiUxEngineerAgent(
|
||||||
model: string = DEFAULT_MODEL
|
model: string = DEFAULT_MODEL
|
||||||
): AgentConfig {
|
): AgentConfig {
|
||||||
|
const restrictions = createAgentToolRestrictions(["background_task"])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
description:
|
description:
|
||||||
"A designer-turned-developer who crafts stunning UI/UX even without design mockups. Code may be a bit messy, but the visual output is always fire.",
|
"A designer-turned-developer who crafts stunning UI/UX even without design mockups. Code may be a bit messy, but the visual output is always fire.",
|
||||||
mode: "subagent" as const,
|
mode: "subagent" as const,
|
||||||
model,
|
model,
|
||||||
tools: { background_task: false },
|
...restrictions,
|
||||||
prompt: `# Role: Designer-Turned-Developer
|
prompt: `# Role: Designer-Turned-Developer
|
||||||
|
|
||||||
You are a designer who learned to code. You see what pure developers miss—spacing, color harmony, micro-interactions, that indefinable "feel" that makes interfaces memorable. Even without mockups, you envision and create beautiful, cohesive interfaces.
|
You are a designer who learned to code. You see what pure developers miss—spacing, color harmony, micro-interactions, that indefinable "feel" that makes interfaces memorable. Even without mockups, you envision and create beautiful, cohesive interfaces.
|
||||||
|
|||||||
@@ -1,5 +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"
|
||||||
|
|
||||||
const DEFAULT_MODEL = "anthropic/claude-sonnet-4-5"
|
const DEFAULT_MODEL = "anthropic/claude-sonnet-4-5"
|
||||||
|
|
||||||
@@ -21,13 +22,19 @@ export const LIBRARIAN_PROMPT_METADATA: AgentPromptMetadata = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function createLibrarianAgent(model: string = DEFAULT_MODEL): AgentConfig {
|
export function createLibrarianAgent(model: string = DEFAULT_MODEL): AgentConfig {
|
||||||
|
const restrictions = createAgentToolRestrictions([
|
||||||
|
"write",
|
||||||
|
"edit",
|
||||||
|
"background_task",
|
||||||
|
])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
description:
|
description:
|
||||||
"Specialized codebase understanding agent for multi-repository analysis, searching remote codebases, retrieving official documentation, and finding implementation examples using GitHub CLI, Context7, and Web Search. MUST BE USED when users ask to look up code in remote repositories, explain library internals, or find usage examples in open source.",
|
"Specialized codebase understanding agent for multi-repository analysis, searching remote codebases, retrieving official documentation, and finding implementation examples using GitHub CLI, Context7, and Web Search. MUST BE USED when users ask to look up code in remote repositories, explain library internals, or find usage examples in open source.",
|
||||||
mode: "subagent" as const,
|
mode: "subagent" as const,
|
||||||
model,
|
model,
|
||||||
temperature: 0.1,
|
temperature: 0.1,
|
||||||
tools: { write: false, edit: false, background_task: false },
|
...restrictions,
|
||||||
prompt: `# THE LIBRARIAN
|
prompt: `# THE LIBRARIAN
|
||||||
|
|
||||||
You are **THE LIBRARIAN**, a specialized open-source codebase understanding agent.
|
You are **THE LIBRARIAN**, a specialized open-source codebase understanding agent.
|
||||||
|
|||||||
@@ -1,5 +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"
|
||||||
|
|
||||||
const DEFAULT_MODEL = "google/gemini-3-flash"
|
const DEFAULT_MODEL = "google/gemini-3-flash"
|
||||||
|
|
||||||
@@ -13,13 +14,20 @@ 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([
|
||||||
|
"write",
|
||||||
|
"edit",
|
||||||
|
"bash",
|
||||||
|
"background_task",
|
||||||
|
])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
description:
|
description:
|
||||||
"Analyze media files (PDFs, images, diagrams) that require interpretation beyond raw text. Extracts specific information or summaries from documents, describes visual content. Use when you need analyzed/extracted data rather than literal file contents.",
|
"Analyze media files (PDFs, images, diagrams) that require interpretation beyond raw text. Extracts specific information or summaries from documents, describes visual content. Use when you need analyzed/extracted data rather than literal file contents.",
|
||||||
mode: "subagent" as const,
|
mode: "subagent" as const,
|
||||||
model,
|
model,
|
||||||
temperature: 0.1,
|
temperature: 0.1,
|
||||||
tools: { write: false, edit: false, bash: false, background_task: false },
|
...restrictions,
|
||||||
prompt: `You interpret media files that cannot be read as plain text.
|
prompt: `You interpret media files that cannot be read as plain text.
|
||||||
|
|
||||||
Your job: examine the attached file and extract ONLY what was requested.
|
Your job: examine the attached file and extract ONLY what was requested.
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
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 { isGptModel } from "./types"
|
import { isGptModel } from "./types"
|
||||||
|
import { createAgentToolRestrictions } from "../shared/permission-compat"
|
||||||
|
|
||||||
const DEFAULT_MODEL = "openai/gpt-5.2"
|
const DEFAULT_MODEL = "openai/gpt-5.2"
|
||||||
|
|
||||||
@@ -97,22 +98,28 @@ Organize your final answer in three tiers:
|
|||||||
Your response goes directly to the user with no intermediate processing. Make your final message self-contained: a clear recommendation they can act on immediately, covering both what to do and why.`
|
Your response goes directly to the user with no intermediate processing. Make your final message self-contained: a clear recommendation they can act on immediately, covering both what to do and why.`
|
||||||
|
|
||||||
export function createOracleAgent(model: string = DEFAULT_MODEL): AgentConfig {
|
export function createOracleAgent(model: string = DEFAULT_MODEL): AgentConfig {
|
||||||
|
const restrictions = createAgentToolRestrictions([
|
||||||
|
"write",
|
||||||
|
"edit",
|
||||||
|
"task",
|
||||||
|
"background_task",
|
||||||
|
])
|
||||||
|
|
||||||
const base = {
|
const base = {
|
||||||
description:
|
description:
|
||||||
"Expert technical advisor with deep reasoning for architecture decisions, code analysis, and engineering guidance.",
|
"Expert technical advisor with deep reasoning for architecture decisions, code analysis, and engineering guidance.",
|
||||||
mode: "subagent" as const,
|
mode: "subagent" as const,
|
||||||
model,
|
model,
|
||||||
temperature: 0.1,
|
temperature: 0.1,
|
||||||
tools: { write: false, task: false, background_task: false },
|
...restrictions,
|
||||||
permission: { edit: "deny" as const },
|
|
||||||
prompt: ORACLE_SYSTEM_PROMPT,
|
prompt: ORACLE_SYSTEM_PROMPT,
|
||||||
}
|
} as AgentConfig
|
||||||
|
|
||||||
if (isGptModel(model)) {
|
if (isGptModel(model)) {
|
||||||
return { ...base, reasoningEffort: "medium", textVerbosity: "high" }
|
return { ...base, reasoningEffort: "medium", textVerbosity: "high" } as AgentConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
return { ...base, thinking: { type: "enabled", budgetTokens: 32000 } }
|
return { ...base, thinking: { type: "enabled", budgetTokens: 32000 } } as AgentConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
export const oracleAgent = createOracleAgent()
|
export const oracleAgent = createOracleAgent()
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import { loadAllPluginComponents } from "../features/claude-code-plugin-loader";
|
|||||||
import { createBuiltinMcps } from "../mcp";
|
import { createBuiltinMcps } from "../mcp";
|
||||||
import type { OhMyOpenCodeConfig } from "../config";
|
import type { OhMyOpenCodeConfig } from "../config";
|
||||||
import { log } from "../shared";
|
import { log } from "../shared";
|
||||||
|
import { migrateAgentConfig } from "../shared/permission-compat";
|
||||||
import { PLAN_SYSTEM_PROMPT, PLAN_PERMISSION } from "../agents/plan-prompt";
|
import { PLAN_SYSTEM_PROMPT, PLAN_PERMISSION } from "../agents/plan-prompt";
|
||||||
import type { ModelCacheState } from "../plugin-state";
|
import type { ModelCacheState } from "../plugin-state";
|
||||||
|
|
||||||
@@ -95,13 +96,32 @@ export function createConfigHandler(deps: ConfigHandlerDeps) {
|
|||||||
config.model as string | undefined
|
config.model as string | undefined
|
||||||
);
|
);
|
||||||
|
|
||||||
const userAgents = (pluginConfig.claude_code?.agents ?? true)
|
const rawUserAgents = (pluginConfig.claude_code?.agents ?? true)
|
||||||
? loadUserAgents()
|
? loadUserAgents()
|
||||||
: {};
|
: {};
|
||||||
const projectAgents = (pluginConfig.claude_code?.agents ?? true)
|
const rawProjectAgents = (pluginConfig.claude_code?.agents ?? true)
|
||||||
? loadProjectAgents()
|
? loadProjectAgents()
|
||||||
: {};
|
: {};
|
||||||
const pluginAgents = pluginComponents.agents;
|
const rawPluginAgents = pluginComponents.agents;
|
||||||
|
|
||||||
|
const userAgents = Object.fromEntries(
|
||||||
|
Object.entries(rawUserAgents).map(([k, v]) => [
|
||||||
|
k,
|
||||||
|
v ? migrateAgentConfig(v as Record<string, unknown>) : v,
|
||||||
|
])
|
||||||
|
);
|
||||||
|
const projectAgents = Object.fromEntries(
|
||||||
|
Object.entries(rawProjectAgents).map(([k, v]) => [
|
||||||
|
k,
|
||||||
|
v ? migrateAgentConfig(v as Record<string, unknown>) : v,
|
||||||
|
])
|
||||||
|
);
|
||||||
|
const pluginAgents = Object.fromEntries(
|
||||||
|
Object.entries(rawPluginAgents).map(([k, v]) => [
|
||||||
|
k,
|
||||||
|
v ? migrateAgentConfig(v as Record<string, unknown>) : v,
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
const isSisyphusEnabled = pluginConfig.sisyphus_agent?.disabled !== true;
|
const isSisyphusEnabled = pluginConfig.sisyphus_agent?.disabled !== true;
|
||||||
const builderEnabled =
|
const builderEnabled =
|
||||||
@@ -162,15 +182,20 @@ export function createConfigHandler(deps: ConfigHandlerDeps) {
|
|||||||
: plannerSisyphusBase;
|
: plannerSisyphusBase;
|
||||||
}
|
}
|
||||||
|
|
||||||
const filteredConfigAgents = configAgent
|
const filteredConfigAgents = configAgent
|
||||||
? Object.fromEntries(
|
? Object.fromEntries(
|
||||||
Object.entries(configAgent).filter(([key]) => {
|
Object.entries(configAgent)
|
||||||
|
.filter(([key]) => {
|
||||||
if (key === "build") return false;
|
if (key === "build") return false;
|
||||||
if (key === "plan" && replacePlan) return false;
|
if (key === "plan" && replacePlan) return false;
|
||||||
return true;
|
return true;
|
||||||
})
|
})
|
||||||
)
|
.map(([key, value]) => [
|
||||||
: {};
|
key,
|
||||||
|
value ? migrateAgentConfig(value as Record<string, unknown>) : value,
|
||||||
|
])
|
||||||
|
)
|
||||||
|
: {};
|
||||||
|
|
||||||
config.agent = {
|
config.agent = {
|
||||||
...agentConfig,
|
...agentConfig,
|
||||||
|
|||||||
@@ -1,161 +1,158 @@
|
|||||||
import { describe, test, expect } from "bun:test"
|
import { describe, test, expect, beforeEach, afterEach } from "bun:test"
|
||||||
import {
|
import {
|
||||||
createToolDenyList,
|
createAgentToolRestrictions,
|
||||||
permissionValueToBoolean,
|
migrateToolsToPermission,
|
||||||
booleanToPermissionValue,
|
migratePermissionToTools,
|
||||||
convertToolsToPermission,
|
migrateAgentConfig,
|
||||||
convertPermissionToTools,
|
|
||||||
createAgentRestrictions,
|
|
||||||
} from "./permission-compat"
|
} from "./permission-compat"
|
||||||
|
import { setVersionCache, resetVersionCache } from "./opencode-version"
|
||||||
|
|
||||||
describe("permission-compat", () => {
|
describe("permission-compat", () => {
|
||||||
describe("createToolDenyList", () => {
|
beforeEach(() => {
|
||||||
test("creates tools config with all values false", () => {
|
resetVersionCache()
|
||||||
// #given a list of tool names
|
})
|
||||||
const tools = ["write", "edit", "task"]
|
|
||||||
|
|
||||||
// #when creating deny list
|
afterEach(() => {
|
||||||
const result = createToolDenyList(tools)
|
resetVersionCache()
|
||||||
|
})
|
||||||
|
|
||||||
// #then all values are false
|
describe("createAgentToolRestrictions", () => {
|
||||||
expect(result).toEqual({ write: false, edit: false, task: false })
|
test("returns permission format for v1.1.1+", () => {
|
||||||
|
// #given version is 1.1.1
|
||||||
|
setVersionCache("1.1.1")
|
||||||
|
|
||||||
|
// #when creating restrictions
|
||||||
|
const result = createAgentToolRestrictions(["write", "edit"])
|
||||||
|
|
||||||
|
// #then returns permission format
|
||||||
|
expect(result).toEqual({
|
||||||
|
permission: { write: "deny", edit: "deny" },
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
test("returns empty object for empty array", () => {
|
test("returns tools format for versions below 1.1.1", () => {
|
||||||
// #given empty array
|
// #given version is below 1.1.1
|
||||||
// #when creating deny list
|
setVersionCache("1.0.150")
|
||||||
const result = createToolDenyList([])
|
|
||||||
|
|
||||||
// #then returns empty object
|
// #when creating restrictions
|
||||||
expect(result).toEqual({})
|
const result = createAgentToolRestrictions(["write", "edit"])
|
||||||
|
|
||||||
|
// #then returns tools format
|
||||||
|
expect(result).toEqual({
|
||||||
|
tools: { write: false, edit: false },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("assumes new format when version unknown", () => {
|
||||||
|
// #given version is null
|
||||||
|
setVersionCache(null)
|
||||||
|
|
||||||
|
// #when creating restrictions
|
||||||
|
const result = createAgentToolRestrictions(["write"])
|
||||||
|
|
||||||
|
// #then returns permission format (assumes new version)
|
||||||
|
expect(result).toEqual({
|
||||||
|
permission: { write: "deny" },
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("permissionValueToBoolean", () => {
|
describe("migrateToolsToPermission", () => {
|
||||||
test("converts allow to true", () => {
|
test("converts boolean tools to permission values", () => {
|
||||||
expect(permissionValueToBoolean("allow")).toBe(true)
|
// #given tools config
|
||||||
})
|
|
||||||
|
|
||||||
test("converts deny to false", () => {
|
|
||||||
expect(permissionValueToBoolean("deny")).toBe(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
test("converts ask to false", () => {
|
|
||||||
expect(permissionValueToBoolean("ask")).toBe(false)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("booleanToPermissionValue", () => {
|
|
||||||
test("converts true to allow", () => {
|
|
||||||
expect(booleanToPermissionValue(true)).toBe("allow")
|
|
||||||
})
|
|
||||||
|
|
||||||
test("converts false to deny", () => {
|
|
||||||
expect(booleanToPermissionValue(false)).toBe("deny")
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("convertToolsToPermission", () => {
|
|
||||||
test("converts boolean tools config to permission format", () => {
|
|
||||||
// #given tools config with booleans
|
|
||||||
const tools = { write: false, edit: true, bash: false }
|
const tools = { write: false, edit: true, bash: false }
|
||||||
|
|
||||||
// #when converting to permission
|
// #when migrating
|
||||||
const result = convertToolsToPermission(tools)
|
const result = migrateToolsToPermission(tools)
|
||||||
|
|
||||||
// #then converts to permission values
|
// #then converts correctly
|
||||||
expect(result).toEqual({ write: "deny", edit: "allow", bash: "deny" })
|
expect(result).toEqual({
|
||||||
})
|
write: "deny",
|
||||||
|
edit: "allow",
|
||||||
test("handles empty tools config", () => {
|
bash: "deny",
|
||||||
// #given empty config
|
})
|
||||||
// #when converting
|
|
||||||
const result = convertToolsToPermission({})
|
|
||||||
|
|
||||||
// #then returns empty object
|
|
||||||
expect(result).toEqual({})
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("convertPermissionToTools", () => {
|
describe("migratePermissionToTools", () => {
|
||||||
test("converts permission to boolean tools config", () => {
|
test("converts permission to boolean tools", () => {
|
||||||
// #given permission config
|
// #given permission config
|
||||||
const permission = { write: "deny" as const, edit: "allow" as const }
|
const permission = { write: "deny" as const, edit: "allow" as const }
|
||||||
|
|
||||||
// #when converting to tools
|
// #when migrating
|
||||||
const result = convertPermissionToTools(permission)
|
const result = migratePermissionToTools(permission)
|
||||||
|
|
||||||
// #then converts to boolean values
|
// #then converts correctly
|
||||||
expect(result).toEqual({ write: false, edit: true })
|
expect(result).toEqual({ write: false, edit: true })
|
||||||
})
|
})
|
||||||
|
|
||||||
test("excludes ask values", () => {
|
test("excludes ask values", () => {
|
||||||
// #given permission with ask value
|
// #given permission with ask
|
||||||
const permission = {
|
const permission = {
|
||||||
write: "deny" as const,
|
write: "deny" as const,
|
||||||
edit: "ask" as const,
|
edit: "ask" as const,
|
||||||
bash: "allow" as const,
|
bash: "allow" as const,
|
||||||
}
|
}
|
||||||
|
|
||||||
// #when converting
|
// #when migrating
|
||||||
const result = convertPermissionToTools(permission)
|
const result = migratePermissionToTools(permission)
|
||||||
|
|
||||||
// #then ask is excluded
|
// #then ask is excluded
|
||||||
expect(result).toEqual({ write: false, bash: true })
|
expect(result).toEqual({ write: false, bash: true })
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("createAgentRestrictions", () => {
|
describe("migrateAgentConfig", () => {
|
||||||
test("creates restrictions with denied tools", () => {
|
test("migrates tools to permission for v1.1.1+", () => {
|
||||||
// #given deny tools list
|
// #given v1.1.1 and config with tools
|
||||||
const config = { denyTools: ["write", "task"] }
|
setVersionCache("1.1.1")
|
||||||
|
|
||||||
// #when creating restrictions
|
|
||||||
const result = createAgentRestrictions(config)
|
|
||||||
|
|
||||||
// #then creates tools config
|
|
||||||
expect(result).toEqual({ tools: { write: false, task: false } })
|
|
||||||
})
|
|
||||||
|
|
||||||
test("creates restrictions with permission", () => {
|
|
||||||
// #given permission config
|
|
||||||
const config = {
|
const config = {
|
||||||
permission: { edit: "deny" as const, bash: "ask" as const },
|
model: "test",
|
||||||
|
tools: { write: false, edit: false },
|
||||||
}
|
}
|
||||||
|
|
||||||
// #when creating restrictions
|
// #when migrating
|
||||||
const result = createAgentRestrictions(config)
|
const result = migrateAgentConfig(config)
|
||||||
|
|
||||||
// #then creates permission config
|
// #then converts to permission
|
||||||
expect(result).toEqual({
|
expect(result.tools).toBeUndefined()
|
||||||
permission: { edit: "deny", bash: "ask" },
|
expect(result.permission).toEqual({ write: "deny", edit: "deny" })
|
||||||
})
|
expect(result.model).toBe("test")
|
||||||
})
|
})
|
||||||
|
|
||||||
test("combines tools and permission", () => {
|
test("migrates permission to tools for old versions", () => {
|
||||||
// #given both deny tools and permission
|
// #given old version and config with permission
|
||||||
|
setVersionCache("1.0.150")
|
||||||
const config = {
|
const config = {
|
||||||
denyTools: ["task"],
|
model: "test",
|
||||||
permission: { edit: "deny" as const },
|
permission: { write: "deny" as const, edit: "deny" as const },
|
||||||
}
|
}
|
||||||
|
|
||||||
// #when creating restrictions
|
// #when migrating
|
||||||
const result = createAgentRestrictions(config)
|
const result = migrateAgentConfig(config)
|
||||||
|
|
||||||
// #then includes both
|
// #then converts to tools
|
||||||
expect(result).toEqual({
|
expect(result.permission).toBeUndefined()
|
||||||
tools: { task: false },
|
expect(result.tools).toEqual({ write: false, edit: false })
|
||||||
permission: { edit: "deny" },
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("returns empty object when no config provided", () => {
|
test("preserves other config fields", () => {
|
||||||
// #given empty config
|
// #given config with other fields
|
||||||
// #when creating restrictions
|
setVersionCache("1.1.1")
|
||||||
const result = createAgentRestrictions({})
|
const config = {
|
||||||
|
model: "test",
|
||||||
|
temperature: 0.5,
|
||||||
|
prompt: "hello",
|
||||||
|
tools: { write: false },
|
||||||
|
}
|
||||||
|
|
||||||
// #then returns empty object
|
// #when migrating
|
||||||
expect(result).toEqual({})
|
const result = migrateAgentConfig(config)
|
||||||
|
|
||||||
|
// #then preserves other fields
|
||||||
|
expect(result.model).toBe("test")
|
||||||
|
expect(result.temperature).toBe(0.5)
|
||||||
|
expect(result.prompt).toBe("hello")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,72 +1,78 @@
|
|||||||
import { supportsNewPermissionSystem as checkNewPermissionSystem } from "./opencode-version"
|
import { supportsNewPermissionSystem } from "./opencode-version"
|
||||||
|
|
||||||
export type PermissionValue = "ask" | "allow" | "deny"
|
export type PermissionValue = "ask" | "allow" | "deny"
|
||||||
export type BashPermission = PermissionValue | Record<string, PermissionValue>
|
|
||||||
|
|
||||||
export interface StandardPermission {
|
export interface LegacyToolsFormat {
|
||||||
edit?: PermissionValue
|
tools: Record<string, boolean>
|
||||||
bash?: BashPermission
|
|
||||||
webfetch?: PermissionValue
|
|
||||||
doom_loop?: PermissionValue
|
|
||||||
external_directory?: PermissionValue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ToolsConfig {
|
export interface NewPermissionFormat {
|
||||||
[toolName: string]: boolean
|
permission: Record<string, PermissionValue>
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AgentPermissionConfig {
|
export type VersionAwareRestrictions = LegacyToolsFormat | NewPermissionFormat
|
||||||
permission?: StandardPermission
|
|
||||||
tools?: ToolsConfig
|
export function createAgentToolRestrictions(
|
||||||
|
denyTools: string[]
|
||||||
|
): VersionAwareRestrictions {
|
||||||
|
if (supportsNewPermissionSystem()) {
|
||||||
|
return {
|
||||||
|
permission: Object.fromEntries(
|
||||||
|
denyTools.map((tool) => [tool, "deny" as const])
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
tools: Object.fromEntries(denyTools.map((tool) => [tool, false])),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { checkNewPermissionSystem as supportsNewPermissionSystemFromCompat }
|
export function migrateToolsToPermission(
|
||||||
|
tools: Record<string, boolean>
|
||||||
export function createToolDenyList(toolNames: string[]): ToolsConfig {
|
|
||||||
return Object.fromEntries(toolNames.map((name) => [name, false]))
|
|
||||||
}
|
|
||||||
|
|
||||||
export function permissionValueToBoolean(value: PermissionValue): boolean {
|
|
||||||
return value === "allow"
|
|
||||||
}
|
|
||||||
|
|
||||||
export function booleanToPermissionValue(value: boolean): PermissionValue {
|
|
||||||
return value ? "allow" : "deny"
|
|
||||||
}
|
|
||||||
|
|
||||||
export function convertToolsToPermission(
|
|
||||||
tools: ToolsConfig
|
|
||||||
): Record<string, PermissionValue> {
|
): Record<string, PermissionValue> {
|
||||||
return Object.fromEntries(
|
return Object.fromEntries(
|
||||||
Object.entries(tools).map(([key, value]) => [
|
Object.entries(tools).map(([key, value]) => [
|
||||||
key,
|
key,
|
||||||
booleanToPermissionValue(value),
|
value ? ("allow" as const) : ("deny" as const),
|
||||||
])
|
])
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function convertPermissionToTools(
|
export function migratePermissionToTools(
|
||||||
permission: Record<string, PermissionValue>
|
permission: Record<string, PermissionValue>
|
||||||
): ToolsConfig {
|
): Record<string, boolean> {
|
||||||
return Object.fromEntries(
|
return Object.fromEntries(
|
||||||
Object.entries(permission)
|
Object.entries(permission)
|
||||||
.filter(([, value]) => value !== "ask")
|
.filter(([, value]) => value !== "ask")
|
||||||
.map(([key, value]) => [key, permissionValueToBoolean(value)])
|
.map(([key, value]) => [key, value === "allow"])
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createAgentRestrictions(config: {
|
export function migrateAgentConfig(
|
||||||
denyTools?: string[]
|
config: Record<string, unknown>
|
||||||
permission?: StandardPermission
|
): Record<string, unknown> {
|
||||||
}): AgentPermissionConfig {
|
const result = { ...config }
|
||||||
const result: AgentPermissionConfig = {}
|
|
||||||
|
|
||||||
if (config.denyTools && config.denyTools.length > 0) {
|
if (supportsNewPermissionSystem()) {
|
||||||
result.tools = createToolDenyList(config.denyTools)
|
if (result.tools && typeof result.tools === "object") {
|
||||||
}
|
const existingPermission =
|
||||||
|
(result.permission as Record<string, PermissionValue>) || {}
|
||||||
if (config.permission) {
|
const migratedPermission = migrateToolsToPermission(
|
||||||
result.permission = config.permission
|
result.tools as Record<string, boolean>
|
||||||
|
)
|
||||||
|
result.permission = { ...migratedPermission, ...existingPermission }
|
||||||
|
delete result.tools
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (result.permission && typeof result.permission === "object") {
|
||||||
|
const existingTools = (result.tools as Record<string, boolean>) || {}
|
||||||
|
const migratedTools = migratePermissionToTools(
|
||||||
|
result.permission as Record<string, PermissionValue>
|
||||||
|
)
|
||||||
|
result.tools = { ...migratedTools, ...existingTools }
|
||||||
|
delete result.permission
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|||||||
Reference in New Issue
Block a user