refactor: remove legacy tools format, use permission only
BREAKING: Requires OpenCode 1.1.1+ - Remove supportsNewPermissionSystem/usesLegacyToolsSystem checks - Simplify permission-compat.ts to permission format only - Unify explore/librarian deny lists: write, edit, task, sisyphus_task, call_omo_agent - Add sisyphus_task to oracle deny list - Update agent-tool-restrictions.ts with correct per-agent restrictions - Clean config-handler.ts conditional version checks - Update tests for simplified API
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
import type { PermissionValue } from "./permission-compat"
|
||||
|
||||
/**
|
||||
* Agent tool restrictions for session.prompt calls.
|
||||
* OpenCode SDK's session.prompt `tools` parameter OVERRIDES agent-level permissions.
|
||||
* This provides complete restriction sets so session.prompt calls include all necessary restrictions.
|
||||
*/
|
||||
|
||||
const EXPLORATION_AGENT_DENYLIST: Record<string, PermissionValue> = {
|
||||
write: "deny",
|
||||
edit: "deny",
|
||||
task: "deny",
|
||||
sisyphus_task: "deny",
|
||||
call_omo_agent: "deny",
|
||||
}
|
||||
|
||||
const AGENT_RESTRICTIONS: Record<string, Record<string, PermissionValue>> = {
|
||||
explore: EXPLORATION_AGENT_DENYLIST,
|
||||
|
||||
librarian: EXPLORATION_AGENT_DENYLIST,
|
||||
|
||||
oracle: {
|
||||
write: "deny",
|
||||
edit: "deny",
|
||||
task: "deny",
|
||||
sisyphus_task: "deny",
|
||||
},
|
||||
|
||||
"multimodal-looker": {
|
||||
"*": "deny",
|
||||
read: "allow",
|
||||
},
|
||||
|
||||
"document-writer": {
|
||||
task: "deny",
|
||||
sisyphus_task: "deny",
|
||||
call_omo_agent: "deny",
|
||||
},
|
||||
|
||||
"frontend-ui-ux-engineer": {
|
||||
task: "deny",
|
||||
sisyphus_task: "deny",
|
||||
call_omo_agent: "deny",
|
||||
},
|
||||
|
||||
"Sisyphus-Junior": {
|
||||
task: "deny",
|
||||
sisyphus_task: "deny",
|
||||
},
|
||||
}
|
||||
|
||||
export function getAgentToolRestrictions(agentName: string): Record<string, PermissionValue> {
|
||||
return AGENT_RESTRICTIONS[agentName] ?? {}
|
||||
}
|
||||
|
||||
export function hasAgentToolRestrictions(agentName: string): boolean {
|
||||
const restrictions = AGENT_RESTRICTIONS[agentName]
|
||||
return restrictions !== undefined && Object.keys(restrictions).length > 0
|
||||
}
|
||||
@@ -25,3 +25,4 @@ export * from "./agent-variant"
|
||||
export * from "./session-cursor"
|
||||
export * from "./shell-env"
|
||||
export * from "./system-directive"
|
||||
export * from "./agent-tool-restrictions"
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
import { describe, test, expect, beforeEach, afterEach, spyOn, mock } from "bun:test"
|
||||
import * as childProcess from "child_process"
|
||||
import { describe, test, expect, beforeEach, afterEach } from "bun:test"
|
||||
import {
|
||||
parseVersion,
|
||||
compareVersions,
|
||||
isVersionGte,
|
||||
isVersionLt,
|
||||
getOpenCodeVersion,
|
||||
supportsNewPermissionSystem,
|
||||
usesLegacyToolsSystem,
|
||||
isOpenCodeVersionAtLeast,
|
||||
resetVersionCache,
|
||||
setVersionCache,
|
||||
PERMISSION_BREAKING_VERSION,
|
||||
MINIMUM_OPENCODE_VERSION,
|
||||
} from "./opencode-version"
|
||||
|
||||
describe("opencode-version", () => {
|
||||
@@ -163,7 +161,7 @@ describe("opencode-version", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("supportsNewPermissionSystem", () => {
|
||||
describe("isOpenCodeVersionAtLeast", () => {
|
||||
beforeEach(() => {
|
||||
resetVersionCache()
|
||||
})
|
||||
@@ -172,34 +170,34 @@ describe("opencode-version", () => {
|
||||
resetVersionCache()
|
||||
})
|
||||
|
||||
test("returns true for v1.1.1", () => {
|
||||
test("returns true for exact version", () => {
|
||||
// #given version is 1.1.1
|
||||
setVersionCache("1.1.1")
|
||||
|
||||
// #when checking permission system support
|
||||
const result = supportsNewPermissionSystem()
|
||||
// #when checking against 1.1.1
|
||||
const result = isOpenCodeVersionAtLeast("1.1.1")
|
||||
|
||||
// #then returns true
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
|
||||
test("returns true for versions above 1.1.1", () => {
|
||||
// #given version is above 1.1.1
|
||||
test("returns true for versions above target", () => {
|
||||
// #given version is above target
|
||||
setVersionCache("1.2.0")
|
||||
|
||||
// #when checking
|
||||
const result = supportsNewPermissionSystem()
|
||||
// #when checking against 1.1.1
|
||||
const result = isOpenCodeVersionAtLeast("1.1.1")
|
||||
|
||||
// #then returns true
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
|
||||
test("returns false for versions below 1.1.1", () => {
|
||||
// #given version is below 1.1.1
|
||||
test("returns false for versions below target", () => {
|
||||
// #given version is below target
|
||||
setVersionCache("1.1.0")
|
||||
|
||||
// #when checking
|
||||
const result = supportsNewPermissionSystem()
|
||||
// #when checking against 1.1.1
|
||||
const result = isOpenCodeVersionAtLeast("1.1.1")
|
||||
|
||||
// #then returns false
|
||||
expect(result).toBe(false)
|
||||
@@ -210,48 +208,16 @@ describe("opencode-version", () => {
|
||||
setVersionCache(null)
|
||||
|
||||
// #when checking
|
||||
const result = supportsNewPermissionSystem()
|
||||
const result = isOpenCodeVersionAtLeast("1.1.1")
|
||||
|
||||
// #then returns true (assume newer version)
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe("usesLegacyToolsSystem", () => {
|
||||
beforeEach(() => {
|
||||
resetVersionCache()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
resetVersionCache()
|
||||
})
|
||||
|
||||
test("returns true for versions below 1.1.1", () => {
|
||||
// #given version is below 1.1.1
|
||||
setVersionCache("1.0.150")
|
||||
|
||||
// #when checking
|
||||
const result = usesLegacyToolsSystem()
|
||||
|
||||
// #then returns true
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
|
||||
test("returns false for v1.1.1 and above", () => {
|
||||
// #given version is 1.1.1
|
||||
setVersionCache("1.1.1")
|
||||
|
||||
// #when checking
|
||||
const result = usesLegacyToolsSystem()
|
||||
|
||||
// #then returns false
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("PERMISSION_BREAKING_VERSION", () => {
|
||||
describe("MINIMUM_OPENCODE_VERSION", () => {
|
||||
test("is set to 1.1.1", () => {
|
||||
expect(PERMISSION_BREAKING_VERSION).toBe("1.1.1")
|
||||
expect(MINIMUM_OPENCODE_VERSION).toBe("1.1.1")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { execSync } from "child_process"
|
||||
|
||||
export const PERMISSION_BREAKING_VERSION = "1.1.1"
|
||||
/**
|
||||
* Minimum OpenCode version required for this plugin.
|
||||
* This plugin only supports OpenCode 1.1.1+ which uses the permission system.
|
||||
*/
|
||||
export const MINIMUM_OPENCODE_VERSION = "1.1.1"
|
||||
|
||||
const NOT_CACHED = Symbol("NOT_CACHED")
|
||||
let cachedVersion: string | null | typeof NOT_CACHED = NOT_CACHED
|
||||
@@ -53,14 +57,10 @@ export function getOpenCodeVersion(): string | null {
|
||||
}
|
||||
}
|
||||
|
||||
export function supportsNewPermissionSystem(): boolean {
|
||||
const version = getOpenCodeVersion()
|
||||
if (!version) return true
|
||||
return isVersionGte(version, PERMISSION_BREAKING_VERSION)
|
||||
}
|
||||
|
||||
export function usesLegacyToolsSystem(): boolean {
|
||||
return !supportsNewPermissionSystem()
|
||||
export function isOpenCodeVersionAtLeast(version: string): boolean {
|
||||
const current = getOpenCodeVersion()
|
||||
if (!current) return true
|
||||
return isVersionGte(current, version)
|
||||
}
|
||||
|
||||
export function resetVersionCache(): void {
|
||||
|
||||
@@ -1,27 +1,15 @@
|
||||
import { describe, test, expect, beforeEach, afterEach } from "bun:test"
|
||||
import { describe, test, expect } from "bun:test"
|
||||
import {
|
||||
createAgentToolRestrictions,
|
||||
createAgentToolAllowlist,
|
||||
migrateToolsToPermission,
|
||||
migratePermissionToTools,
|
||||
migrateAgentConfig,
|
||||
} from "./permission-compat"
|
||||
import { setVersionCache, resetVersionCache } from "./opencode-version"
|
||||
|
||||
describe("permission-compat", () => {
|
||||
beforeEach(() => {
|
||||
resetVersionCache()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
resetVersionCache()
|
||||
})
|
||||
|
||||
describe("createAgentToolRestrictions", () => {
|
||||
test("returns permission format for v1.1.1+", () => {
|
||||
// #given version is 1.1.1
|
||||
setVersionCache("1.1.1")
|
||||
|
||||
test("returns permission format with deny values", () => {
|
||||
// #given tools to restrict
|
||||
// #when creating restrictions
|
||||
const result = createAgentToolRestrictions(["write", "edit"])
|
||||
|
||||
@@ -31,38 +19,19 @@ describe("permission-compat", () => {
|
||||
})
|
||||
})
|
||||
|
||||
test("returns tools format for versions below 1.1.1", () => {
|
||||
// #given version is below 1.1.1
|
||||
setVersionCache("1.0.150")
|
||||
|
||||
test("returns empty permission for empty array", () => {
|
||||
// #given empty tools array
|
||||
// #when creating restrictions
|
||||
const result = createAgentToolRestrictions(["write", "edit"])
|
||||
const result = createAgentToolRestrictions([])
|
||||
|
||||
// #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" },
|
||||
})
|
||||
// #then returns empty permission
|
||||
expect(result).toEqual({ permission: {} })
|
||||
})
|
||||
})
|
||||
|
||||
describe("createAgentToolAllowlist", () => {
|
||||
test("returns wildcard deny with explicit allow for v1.1.1+", () => {
|
||||
// #given version is 1.1.1
|
||||
setVersionCache("1.1.1")
|
||||
|
||||
test("returns wildcard deny with explicit allow", () => {
|
||||
// #given tools to allow
|
||||
// #when creating allowlist
|
||||
const result = createAgentToolAllowlist(["read"])
|
||||
|
||||
@@ -72,11 +41,9 @@ describe("permission-compat", () => {
|
||||
})
|
||||
})
|
||||
|
||||
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
|
||||
test("returns wildcard deny with multiple allows", () => {
|
||||
// #given multiple tools to allow
|
||||
// #when creating allowlist
|
||||
const result = createAgentToolAllowlist(["read", "glob"])
|
||||
|
||||
// #then returns wildcard deny with both allows
|
||||
@@ -84,35 +51,6 @@ describe("permission-compat", () => {
|
||||
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", () => {
|
||||
@@ -132,38 +70,9 @@ describe("permission-compat", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("migratePermissionToTools", () => {
|
||||
test("converts permission to boolean tools", () => {
|
||||
// #given permission config
|
||||
const permission = { write: "deny" as const, edit: "allow" as const }
|
||||
|
||||
// #when migrating
|
||||
const result = migratePermissionToTools(permission)
|
||||
|
||||
// #then converts correctly
|
||||
expect(result).toEqual({ write: false, edit: true })
|
||||
})
|
||||
|
||||
test("excludes ask values", () => {
|
||||
// #given permission with ask
|
||||
const permission = {
|
||||
write: "deny" as const,
|
||||
edit: "ask" as const,
|
||||
bash: "allow" as const,
|
||||
}
|
||||
|
||||
// #when migrating
|
||||
const result = migratePermissionToTools(permission)
|
||||
|
||||
// #then ask is excluded
|
||||
expect(result).toEqual({ write: false, bash: true })
|
||||
})
|
||||
})
|
||||
|
||||
describe("migrateAgentConfig", () => {
|
||||
test("migrates tools to permission for v1.1.1+", () => {
|
||||
// #given v1.1.1 and config with tools
|
||||
setVersionCache("1.1.1")
|
||||
test("migrates tools to permission", () => {
|
||||
// #given config with tools
|
||||
const config = {
|
||||
model: "test",
|
||||
tools: { write: false, edit: false },
|
||||
@@ -178,25 +87,8 @@ describe("permission-compat", () => {
|
||||
expect(result.model).toBe("test")
|
||||
})
|
||||
|
||||
test("migrates permission to tools for old versions", () => {
|
||||
// #given old version and config with permission
|
||||
setVersionCache("1.0.150")
|
||||
const config = {
|
||||
model: "test",
|
||||
permission: { write: "deny" as const, edit: "deny" as const },
|
||||
}
|
||||
|
||||
// #when migrating
|
||||
const result = migrateAgentConfig(config)
|
||||
|
||||
// #then converts to tools
|
||||
expect(result.permission).toBeUndefined()
|
||||
expect(result.tools).toEqual({ write: false, edit: false })
|
||||
})
|
||||
|
||||
test("preserves other config fields", () => {
|
||||
// #given config with other fields
|
||||
setVersionCache("1.1.1")
|
||||
const config = {
|
||||
model: "test",
|
||||
temperature: 0.5,
|
||||
@@ -212,5 +104,31 @@ describe("permission-compat", () => {
|
||||
expect(result.temperature).toBe(0.5)
|
||||
expect(result.prompt).toBe("hello")
|
||||
})
|
||||
|
||||
test("merges existing permission with migrated tools", () => {
|
||||
// #given config with both tools and permission
|
||||
const config = {
|
||||
tools: { write: false },
|
||||
permission: { bash: "deny" as const },
|
||||
}
|
||||
|
||||
// #when migrating
|
||||
const result = migrateAgentConfig(config)
|
||||
|
||||
// #then merges permission (existing takes precedence)
|
||||
expect(result.tools).toBeUndefined()
|
||||
expect(result.permission).toEqual({ write: "deny", bash: "deny" })
|
||||
})
|
||||
|
||||
test("returns unchanged config if no tools", () => {
|
||||
// #given config without tools
|
||||
const config = { model: "test", permission: { edit: "deny" as const } }
|
||||
|
||||
// #when migrating
|
||||
const result = migrateAgentConfig(config)
|
||||
|
||||
// #then returns unchanged
|
||||
expect(result).toEqual(config)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
+36
-103
@@ -1,98 +1,48 @@
|
||||
import { supportsNewPermissionSystem } from "./opencode-version"
|
||||
|
||||
export { supportsNewPermissionSystem }
|
||||
/**
|
||||
* Permission system utilities for OpenCode 1.1.1+.
|
||||
* This module only supports the new permission format.
|
||||
*/
|
||||
|
||||
export type PermissionValue = "ask" | "allow" | "deny"
|
||||
|
||||
export interface LegacyToolsFormat {
|
||||
tools: Record<string, boolean>
|
||||
}
|
||||
|
||||
export interface NewPermissionFormat {
|
||||
export interface PermissionFormat {
|
||||
permission: Record<string, PermissionValue>
|
||||
}
|
||||
|
||||
export type VersionAwareRestrictions = LegacyToolsFormat | NewPermissionFormat
|
||||
|
||||
/**
|
||||
* Creates tool restrictions that deny specified tools.
|
||||
*/
|
||||
export function createAgentToolRestrictions(
|
||||
denyTools: string[]
|
||||
): VersionAwareRestrictions {
|
||||
if (supportsNewPermissionSystem()) {
|
||||
return {
|
||||
permission: Object.fromEntries(
|
||||
denyTools.map((tool) => [tool, "deny" as const])
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
): PermissionFormat {
|
||||
return {
|
||||
tools: Object.fromEntries(denyTools.map((tool) => [tool, false])),
|
||||
permission: Object.fromEntries(
|
||||
denyTools.map((tool) => [tool, "deny" as const])
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* All other tools are denied by default using `*: deny` pattern.
|
||||
*/
|
||||
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))
|
||||
|
||||
): PermissionFormat {
|
||||
return {
|
||||
tools: Object.fromEntries(denyTools.map((tool) => [tool, false])),
|
||||
permission: {
|
||||
"*": "deny" as const,
|
||||
...Object.fromEntries(
|
||||
allowTools.map((tool) => [tool, "allow" as const])
|
||||
),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts legacy tools format to permission format.
|
||||
* For migrating user configs from older versions.
|
||||
*/
|
||||
export function migrateToolsToPermission(
|
||||
tools: Record<string, boolean>
|
||||
): Record<string, PermissionValue> {
|
||||
@@ -104,40 +54,23 @@ export function migrateToolsToPermission(
|
||||
)
|
||||
}
|
||||
|
||||
export function migratePermissionToTools(
|
||||
permission: Record<string, PermissionValue>
|
||||
): Record<string, boolean> {
|
||||
return Object.fromEntries(
|
||||
Object.entries(permission)
|
||||
.filter(([, value]) => value !== "ask")
|
||||
.map(([key, value]) => [key, value === "allow"])
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrates agent config from legacy tools format to permission format.
|
||||
* If config has `tools`, converts to `permission`.
|
||||
*/
|
||||
export function migrateAgentConfig(
|
||||
config: Record<string, unknown>
|
||||
): Record<string, unknown> {
|
||||
const result = { ...config }
|
||||
|
||||
if (supportsNewPermissionSystem()) {
|
||||
if (result.tools && typeof result.tools === "object") {
|
||||
const existingPermission =
|
||||
(result.permission as Record<string, PermissionValue>) || {}
|
||||
const migratedPermission = migrateToolsToPermission(
|
||||
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
|
||||
}
|
||||
if (result.tools && typeof result.tools === "object") {
|
||||
const existingPermission =
|
||||
(result.permission as Record<string, PermissionValue>) || {}
|
||||
const migratedPermission = migrateToolsToPermission(
|
||||
result.tools as Record<string, boolean>
|
||||
)
|
||||
result.permission = { ...migratedPermission, ...existingPermission }
|
||||
delete result.tools
|
||||
}
|
||||
|
||||
return result
|
||||
|
||||
Reference in New Issue
Block a user