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:
parent
ede9abceb3
commit
83cbc56709
@ -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/glm-4.7-free"
|
const DEFAULT_MODEL = "opencode/glm-4.7-free"
|
||||||
|
|
||||||
@ -21,13 +22,21 @@ 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",
|
||||||
|
"task",
|
||||||
|
"sisyphus_task",
|
||||||
|
"call_omo_agent",
|
||||||
|
])
|
||||||
|
|
||||||
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.
|
||||||
|
|||||||
@ -102,6 +102,7 @@ export function createOracleAgent(model: string = DEFAULT_MODEL): AgentConfig {
|
|||||||
"write",
|
"write",
|
||||||
"edit",
|
"edit",
|
||||||
"task",
|
"task",
|
||||||
|
"sisyphus_task",
|
||||||
])
|
])
|
||||||
|
|
||||||
const base = {
|
const base = {
|
||||||
|
|||||||
@ -3,8 +3,7 @@ import { isGptModel } from "./types"
|
|||||||
import type { AgentOverrideConfig, CategoryConfig } from "../config/schema"
|
import type { AgentOverrideConfig, CategoryConfig } from "../config/schema"
|
||||||
import {
|
import {
|
||||||
createAgentToolRestrictions,
|
createAgentToolRestrictions,
|
||||||
migrateAgentConfig,
|
type PermissionValue,
|
||||||
supportsNewPermissionSystem,
|
|
||||||
} from "../shared/permission-compat"
|
} from "../shared/permission-compat"
|
||||||
|
|
||||||
const SISYPHUS_JUNIOR_PROMPT = `<Role>
|
const SISYPHUS_JUNIOR_PROMPT = `<Role>
|
||||||
@ -99,26 +98,14 @@ export function createSisyphusJuniorAgentWithOverrides(
|
|||||||
|
|
||||||
const baseRestrictions = createAgentToolRestrictions(BLOCKED_TOOLS)
|
const baseRestrictions = createAgentToolRestrictions(BLOCKED_TOOLS)
|
||||||
|
|
||||||
let toolsConfig: Record<string, unknown> = {}
|
const userPermission = (override?.permission ?? {}) as Record<string, PermissionValue>
|
||||||
if (supportsNewPermissionSystem()) {
|
const basePermission = baseRestrictions.permission
|
||||||
const userPermission = (override?.permission ?? {}) as Record<string, string>
|
const merged: Record<string, PermissionValue> = { ...userPermission }
|
||||||
const basePermission = (baseRestrictions as { permission: Record<string, string> }).permission
|
for (const tool of BLOCKED_TOOLS) {
|
||||||
const merged: Record<string, string> = { ...userPermission }
|
merged[tool] = "deny"
|
||||||
for (const tool of BLOCKED_TOOLS) {
|
|
||||||
merged[tool] = "deny"
|
|
||||||
}
|
|
||||||
merged.call_omo_agent = "allow"
|
|
||||||
toolsConfig = { permission: { ...merged, ...basePermission } }
|
|
||||||
} else {
|
|
||||||
const userTools = override?.tools ?? {}
|
|
||||||
const baseTools = (baseRestrictions as { tools: Record<string, boolean> }).tools
|
|
||||||
const merged: Record<string, boolean> = { ...userTools }
|
|
||||||
for (const tool of BLOCKED_TOOLS) {
|
|
||||||
merged[tool] = false
|
|
||||||
}
|
|
||||||
merged.call_omo_agent = true
|
|
||||||
toolsConfig = { tools: { ...merged, ...baseTools } }
|
|
||||||
}
|
}
|
||||||
|
merged.call_omo_agent = "allow"
|
||||||
|
const toolsConfig = { permission: { ...merged, ...basePermission } }
|
||||||
|
|
||||||
const base: AgentConfig = {
|
const base: AgentConfig = {
|
||||||
description: override?.description ??
|
description: override?.description ??
|
||||||
@ -153,10 +140,18 @@ export function createSisyphusJuniorAgent(
|
|||||||
const prompt = buildSisyphusJuniorPrompt(promptAppend)
|
const prompt = buildSisyphusJuniorPrompt(promptAppend)
|
||||||
const model = categoryConfig.model
|
const model = categoryConfig.model
|
||||||
const baseRestrictions = createAgentToolRestrictions(BLOCKED_TOOLS)
|
const baseRestrictions = createAgentToolRestrictions(BLOCKED_TOOLS)
|
||||||
const mergedConfig = migrateAgentConfig({
|
const categoryPermission = categoryConfig.tools
|
||||||
...baseRestrictions,
|
? Object.fromEntries(
|
||||||
...(categoryConfig.tools ? { tools: categoryConfig.tools } : {}),
|
Object.entries(categoryConfig.tools).map(([k, v]) => [
|
||||||
})
|
k,
|
||||||
|
v ? ("allow" as const) : ("deny" as const),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
: {}
|
||||||
|
const mergedPermission = {
|
||||||
|
...categoryPermission,
|
||||||
|
...baseRestrictions.permission,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
const base: AgentConfig = {
|
const base: AgentConfig = {
|
||||||
@ -167,7 +162,7 @@ export function createSisyphusJuniorAgent(
|
|||||||
maxTokens: categoryConfig.maxTokens ?? 64000,
|
maxTokens: categoryConfig.maxTokens ?? 64000,
|
||||||
prompt,
|
prompt,
|
||||||
color: "#20B2AA",
|
color: "#20B2AA",
|
||||||
...mergedConfig,
|
permission: mergedPermission,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (categoryConfig.temperature !== undefined) {
|
if (categoryConfig.temperature !== undefined) {
|
||||||
|
|||||||
@ -618,9 +618,7 @@ export function createSisyphusAgent(
|
|||||||
? buildDynamicSisyphusPrompt(availableAgents, tools, skills)
|
? buildDynamicSisyphusPrompt(availableAgents, tools, skills)
|
||||||
: buildDynamicSisyphusPrompt([], tools, skills)
|
: buildDynamicSisyphusPrompt([], tools, skills)
|
||||||
|
|
||||||
// Note: question permission allows agent to ask user questions via OpenCode's QuestionTool
|
const permission = { question: "allow", call_omo_agent: "deny" } as AgentConfig["permission"]
|
||||||
// SDK type doesn't include 'question' yet, but OpenCode runtime supports it
|
|
||||||
const permission = { question: "allow" } as AgentConfig["permission"]
|
|
||||||
const base = {
|
const base = {
|
||||||
description:
|
description:
|
||||||
"Sisyphus - Powerful AI orchestrator from OhMyOpenCode. Plans obsessively with todos, assesses search complexity before exploration, delegates strategically to specialized agents. Uses explore for internal code (parallel-friendly), librarian only for external docs, and always delegates UI work to frontend engineer.",
|
"Sisyphus - Powerful AI orchestrator from OhMyOpenCode. Plans obsessively with todos, assesses search complexity before exploration, delegates strategically to specialized agents. Uses explore for internal code (parallel-friendly), librarian only for external docs, and always delegates UI work to frontend engineer.",
|
||||||
@ -630,7 +628,6 @@ export function createSisyphusAgent(
|
|||||||
prompt,
|
prompt,
|
||||||
color: "#00CED1",
|
color: "#00CED1",
|
||||||
permission,
|
permission,
|
||||||
tools: { call_omo_agent: false },
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isGptModel(model)) {
|
if (isGptModel(model)) {
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import type {
|
|||||||
LaunchInput,
|
LaunchInput,
|
||||||
ResumeInput,
|
ResumeInput,
|
||||||
} from "./types"
|
} from "./types"
|
||||||
import { log } from "../../shared/logger"
|
import { log, getAgentToolRestrictions } from "../../shared"
|
||||||
import { ConcurrencyManager } from "./concurrency"
|
import { ConcurrencyManager } from "./concurrency"
|
||||||
import type { BackgroundTaskConfig } from "../../config/schema"
|
import type { BackgroundTaskConfig } from "../../config/schema"
|
||||||
|
|
||||||
@ -178,6 +178,7 @@ export class BackgroundManager {
|
|||||||
...(input.model ? { model: input.model } : {}),
|
...(input.model ? { model: input.model } : {}),
|
||||||
system: input.skillContent,
|
system: input.skillContent,
|
||||||
tools: {
|
tools: {
|
||||||
|
...getAgentToolRestrictions(input.agent),
|
||||||
task: false,
|
task: false,
|
||||||
sisyphus_task: false,
|
sisyphus_task: false,
|
||||||
call_omo_agent: true,
|
call_omo_agent: true,
|
||||||
@ -406,6 +407,7 @@ export class BackgroundManager {
|
|||||||
body: {
|
body: {
|
||||||
agent: existingTask.agent,
|
agent: existingTask.agent,
|
||||||
tools: {
|
tools: {
|
||||||
|
...getAgentToolRestrictions(existingTask.agent),
|
||||||
task: false,
|
task: false,
|
||||||
sisyphus_task: false,
|
sisyphus_task: false,
|
||||||
call_omo_agent: true,
|
call_omo_agent: true,
|
||||||
|
|||||||
@ -291,37 +291,27 @@ export function createConfigHandler(deps: ConfigHandlerDeps) {
|
|||||||
LspCodeActionResolve: false,
|
LspCodeActionResolve: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type AgentWithPermission = { permission?: Record<string, unknown> };
|
||||||
|
|
||||||
if (agentResult.librarian) {
|
if (agentResult.librarian) {
|
||||||
agentResult.librarian.tools = {
|
const agent = agentResult.librarian as AgentWithPermission;
|
||||||
...agentResult.librarian.tools,
|
agent.permission = { ...agent.permission, "grep_app_*": "allow" };
|
||||||
"grep_app_*": true,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
if (agentResult["multimodal-looker"]) {
|
if (agentResult["multimodal-looker"]) {
|
||||||
agentResult["multimodal-looker"].tools = {
|
const agent = agentResult["multimodal-looker"] as AgentWithPermission;
|
||||||
...agentResult["multimodal-looker"].tools,
|
agent.permission = { ...agent.permission, task: "deny", look_at: "deny" };
|
||||||
task: false,
|
|
||||||
look_at: false,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
if (agentResult["orchestrator-sisyphus"]) {
|
if (agentResult["orchestrator-sisyphus"]) {
|
||||||
agentResult["orchestrator-sisyphus"].tools = {
|
const agent = agentResult["orchestrator-sisyphus"] as AgentWithPermission;
|
||||||
...agentResult["orchestrator-sisyphus"].tools,
|
agent.permission = { ...agent.permission, task: "deny", call_omo_agent: "deny" };
|
||||||
task: false,
|
|
||||||
call_omo_agent: false,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
if (agentResult.Sisyphus) {
|
if (agentResult.Sisyphus) {
|
||||||
agentResult.Sisyphus.tools = {
|
const agent = agentResult.Sisyphus as AgentWithPermission;
|
||||||
...agentResult.Sisyphus.tools,
|
agent.permission = { ...agent.permission, call_omo_agent: "deny" };
|
||||||
call_omo_agent: false,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
if (agentResult["Prometheus (Planner)"]) {
|
if (agentResult["Prometheus (Planner)"]) {
|
||||||
(agentResult["Prometheus (Planner)"] as { tools?: Record<string, unknown> }).tools = {
|
const agent = agentResult["Prometheus (Planner)"] as AgentWithPermission;
|
||||||
...(agentResult["Prometheus (Planner)"] as { tools?: Record<string, unknown> }).tools,
|
agent.permission = { ...agent.permission, call_omo_agent: "deny" };
|
||||||
call_omo_agent: false,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
config.permission = {
|
config.permission = {
|
||||||
|
|||||||
59
src/shared/agent-tool-restrictions.ts
Normal file
59
src/shared/agent-tool-restrictions.ts
Normal file
@ -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 "./session-cursor"
|
||||||
export * from "./shell-env"
|
export * from "./shell-env"
|
||||||
export * from "./system-directive"
|
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 { describe, test, expect, beforeEach, afterEach } from "bun:test"
|
||||||
import * as childProcess from "child_process"
|
|
||||||
import {
|
import {
|
||||||
parseVersion,
|
parseVersion,
|
||||||
compareVersions,
|
compareVersions,
|
||||||
isVersionGte,
|
isVersionGte,
|
||||||
isVersionLt,
|
isVersionLt,
|
||||||
getOpenCodeVersion,
|
getOpenCodeVersion,
|
||||||
supportsNewPermissionSystem,
|
isOpenCodeVersionAtLeast,
|
||||||
usesLegacyToolsSystem,
|
|
||||||
resetVersionCache,
|
resetVersionCache,
|
||||||
setVersionCache,
|
setVersionCache,
|
||||||
PERMISSION_BREAKING_VERSION,
|
MINIMUM_OPENCODE_VERSION,
|
||||||
} from "./opencode-version"
|
} from "./opencode-version"
|
||||||
|
|
||||||
describe("opencode-version", () => {
|
describe("opencode-version", () => {
|
||||||
@ -163,7 +161,7 @@ describe("opencode-version", () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("supportsNewPermissionSystem", () => {
|
describe("isOpenCodeVersionAtLeast", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
resetVersionCache()
|
resetVersionCache()
|
||||||
})
|
})
|
||||||
@ -172,34 +170,34 @@ describe("opencode-version", () => {
|
|||||||
resetVersionCache()
|
resetVersionCache()
|
||||||
})
|
})
|
||||||
|
|
||||||
test("returns true for v1.1.1", () => {
|
test("returns true for exact version", () => {
|
||||||
// #given version is 1.1.1
|
// #given version is 1.1.1
|
||||||
setVersionCache("1.1.1")
|
setVersionCache("1.1.1")
|
||||||
|
|
||||||
// #when checking permission system support
|
// #when checking against 1.1.1
|
||||||
const result = supportsNewPermissionSystem()
|
const result = isOpenCodeVersionAtLeast("1.1.1")
|
||||||
|
|
||||||
// #then returns true
|
// #then returns true
|
||||||
expect(result).toBe(true)
|
expect(result).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("returns true for versions above 1.1.1", () => {
|
test("returns true for versions above target", () => {
|
||||||
// #given version is above 1.1.1
|
// #given version is above target
|
||||||
setVersionCache("1.2.0")
|
setVersionCache("1.2.0")
|
||||||
|
|
||||||
// #when checking
|
// #when checking against 1.1.1
|
||||||
const result = supportsNewPermissionSystem()
|
const result = isOpenCodeVersionAtLeast("1.1.1")
|
||||||
|
|
||||||
// #then returns true
|
// #then returns true
|
||||||
expect(result).toBe(true)
|
expect(result).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("returns false for versions below 1.1.1", () => {
|
test("returns false for versions below target", () => {
|
||||||
// #given version is below 1.1.1
|
// #given version is below target
|
||||||
setVersionCache("1.1.0")
|
setVersionCache("1.1.0")
|
||||||
|
|
||||||
// #when checking
|
// #when checking against 1.1.1
|
||||||
const result = supportsNewPermissionSystem()
|
const result = isOpenCodeVersionAtLeast("1.1.1")
|
||||||
|
|
||||||
// #then returns false
|
// #then returns false
|
||||||
expect(result).toBe(false)
|
expect(result).toBe(false)
|
||||||
@ -210,48 +208,16 @@ describe("opencode-version", () => {
|
|||||||
setVersionCache(null)
|
setVersionCache(null)
|
||||||
|
|
||||||
// #when checking
|
// #when checking
|
||||||
const result = supportsNewPermissionSystem()
|
const result = isOpenCodeVersionAtLeast("1.1.1")
|
||||||
|
|
||||||
// #then returns true (assume newer version)
|
// #then returns true (assume newer version)
|
||||||
expect(result).toBe(true)
|
expect(result).toBe(true)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("usesLegacyToolsSystem", () => {
|
describe("MINIMUM_OPENCODE_VERSION", () => {
|
||||||
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", () => {
|
|
||||||
test("is set to 1.1.1", () => {
|
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"
|
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")
|
const NOT_CACHED = Symbol("NOT_CACHED")
|
||||||
let cachedVersion: string | null | typeof NOT_CACHED = NOT_CACHED
|
let cachedVersion: string | null | typeof NOT_CACHED = NOT_CACHED
|
||||||
@ -53,14 +57,10 @@ export function getOpenCodeVersion(): string | null {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function supportsNewPermissionSystem(): boolean {
|
export function isOpenCodeVersionAtLeast(version: string): boolean {
|
||||||
const version = getOpenCodeVersion()
|
const current = getOpenCodeVersion()
|
||||||
if (!version) return true
|
if (!current) return true
|
||||||
return isVersionGte(version, PERMISSION_BREAKING_VERSION)
|
return isVersionGte(current, version)
|
||||||
}
|
|
||||||
|
|
||||||
export function usesLegacyToolsSystem(): boolean {
|
|
||||||
return !supportsNewPermissionSystem()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resetVersionCache(): void {
|
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 {
|
import {
|
||||||
createAgentToolRestrictions,
|
createAgentToolRestrictions,
|
||||||
createAgentToolAllowlist,
|
createAgentToolAllowlist,
|
||||||
migrateToolsToPermission,
|
migrateToolsToPermission,
|
||||||
migratePermissionToTools,
|
|
||||||
migrateAgentConfig,
|
migrateAgentConfig,
|
||||||
} from "./permission-compat"
|
} from "./permission-compat"
|
||||||
import { setVersionCache, resetVersionCache } from "./opencode-version"
|
|
||||||
|
|
||||||
describe("permission-compat", () => {
|
describe("permission-compat", () => {
|
||||||
beforeEach(() => {
|
|
||||||
resetVersionCache()
|
|
||||||
})
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
resetVersionCache()
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("createAgentToolRestrictions", () => {
|
describe("createAgentToolRestrictions", () => {
|
||||||
test("returns permission format for v1.1.1+", () => {
|
test("returns permission format with deny values", () => {
|
||||||
// #given version is 1.1.1
|
// #given tools to restrict
|
||||||
setVersionCache("1.1.1")
|
|
||||||
|
|
||||||
// #when creating restrictions
|
// #when creating restrictions
|
||||||
const result = createAgentToolRestrictions(["write", "edit"])
|
const result = createAgentToolRestrictions(["write", "edit"])
|
||||||
|
|
||||||
@ -31,38 +19,19 @@ describe("permission-compat", () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
test("returns tools format for versions below 1.1.1", () => {
|
test("returns empty permission for empty array", () => {
|
||||||
// #given version is below 1.1.1
|
// #given empty tools array
|
||||||
setVersionCache("1.0.150")
|
|
||||||
|
|
||||||
// #when creating restrictions
|
// #when creating restrictions
|
||||||
const result = createAgentToolRestrictions(["write", "edit"])
|
const result = createAgentToolRestrictions([])
|
||||||
|
|
||||||
// #then returns tools format
|
// #then returns empty permission
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({ permission: {} })
|
||||||
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("createAgentToolAllowlist", () => {
|
describe("createAgentToolAllowlist", () => {
|
||||||
test("returns wildcard deny with explicit allow for v1.1.1+", () => {
|
test("returns wildcard deny with explicit allow", () => {
|
||||||
// #given version is 1.1.1
|
// #given tools to allow
|
||||||
setVersionCache("1.1.1")
|
|
||||||
|
|
||||||
// #when creating allowlist
|
// #when creating allowlist
|
||||||
const result = createAgentToolAllowlist(["read"])
|
const result = createAgentToolAllowlist(["read"])
|
||||||
|
|
||||||
@ -72,11 +41,9 @@ describe("permission-compat", () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
test("returns wildcard deny with multiple allows for v1.1.1+", () => {
|
test("returns wildcard deny with multiple allows", () => {
|
||||||
// #given version is 1.1.1
|
// #given multiple tools to allow
|
||||||
setVersionCache("1.1.1")
|
// #when creating allowlist
|
||||||
|
|
||||||
// #when creating allowlist with multiple tools
|
|
||||||
const result = createAgentToolAllowlist(["read", "glob"])
|
const result = createAgentToolAllowlist(["read", "glob"])
|
||||||
|
|
||||||
// #then returns wildcard deny with both allows
|
// #then returns wildcard deny with both allows
|
||||||
@ -84,35 +51,6 @@ describe("permission-compat", () => {
|
|||||||
permission: { "*": "deny", read: "allow", glob: "allow" },
|
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", () => {
|
||||||
@ -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", () => {
|
describe("migrateAgentConfig", () => {
|
||||||
test("migrates tools to permission for v1.1.1+", () => {
|
test("migrates tools to permission", () => {
|
||||||
// #given v1.1.1 and config with tools
|
// #given config with tools
|
||||||
setVersionCache("1.1.1")
|
|
||||||
const config = {
|
const config = {
|
||||||
model: "test",
|
model: "test",
|
||||||
tools: { write: false, edit: false },
|
tools: { write: false, edit: false },
|
||||||
@ -178,25 +87,8 @@ describe("permission-compat", () => {
|
|||||||
expect(result.model).toBe("test")
|
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", () => {
|
test("preserves other config fields", () => {
|
||||||
// #given config with other fields
|
// #given config with other fields
|
||||||
setVersionCache("1.1.1")
|
|
||||||
const config = {
|
const config = {
|
||||||
model: "test",
|
model: "test",
|
||||||
temperature: 0.5,
|
temperature: 0.5,
|
||||||
@ -212,5 +104,31 @@ describe("permission-compat", () => {
|
|||||||
expect(result.temperature).toBe(0.5)
|
expect(result.temperature).toBe(0.5)
|
||||||
expect(result.prompt).toBe("hello")
|
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)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,98 +1,48 @@
|
|||||||
import { supportsNewPermissionSystem } from "./opencode-version"
|
/**
|
||||||
|
* Permission system utilities for OpenCode 1.1.1+.
|
||||||
export { supportsNewPermissionSystem }
|
* This module only supports the new permission format.
|
||||||
|
*/
|
||||||
|
|
||||||
export type PermissionValue = "ask" | "allow" | "deny"
|
export type PermissionValue = "ask" | "allow" | "deny"
|
||||||
|
|
||||||
export interface LegacyToolsFormat {
|
export interface PermissionFormat {
|
||||||
tools: Record<string, boolean>
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface NewPermissionFormat {
|
|
||||||
permission: Record<string, PermissionValue>
|
permission: Record<string, PermissionValue>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type VersionAwareRestrictions = LegacyToolsFormat | NewPermissionFormat
|
/**
|
||||||
|
* Creates tool restrictions that deny specified tools.
|
||||||
|
*/
|
||||||
export function createAgentToolRestrictions(
|
export function createAgentToolRestrictions(
|
||||||
denyTools: string[]
|
denyTools: string[]
|
||||||
): VersionAwareRestrictions {
|
): PermissionFormat {
|
||||||
if (supportsNewPermissionSystem()) {
|
|
||||||
return {
|
|
||||||
permission: Object.fromEntries(
|
|
||||||
denyTools.map((tool) => [tool, "deny" as const])
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
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.
|
* Creates tool restrictions that ONLY allow specified tools.
|
||||||
* All other tools are denied by default.
|
* All other tools are denied by default using `*: deny` pattern.
|
||||||
*
|
|
||||||
* Uses `*: deny` pattern for new permission system,
|
|
||||||
* falls back to explicit deny list for legacy systems.
|
|
||||||
*/
|
*/
|
||||||
export function createAgentToolAllowlist(
|
export function createAgentToolAllowlist(
|
||||||
allowTools: string[]
|
allowTools: string[]
|
||||||
): VersionAwareRestrictions {
|
): PermissionFormat {
|
||||||
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 {
|
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(
|
export function migrateToolsToPermission(
|
||||||
tools: Record<string, boolean>
|
tools: Record<string, boolean>
|
||||||
): Record<string, PermissionValue> {
|
): Record<string, PermissionValue> {
|
||||||
@ -104,40 +54,23 @@ export function migrateToolsToPermission(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function migratePermissionToTools(
|
/**
|
||||||
permission: Record<string, PermissionValue>
|
* Migrates agent config from legacy tools format to permission format.
|
||||||
): Record<string, boolean> {
|
* If config has `tools`, converts to `permission`.
|
||||||
return Object.fromEntries(
|
*/
|
||||||
Object.entries(permission)
|
|
||||||
.filter(([, value]) => value !== "ask")
|
|
||||||
.map(([key, value]) => [key, value === "allow"])
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function migrateAgentConfig(
|
export function migrateAgentConfig(
|
||||||
config: Record<string, unknown>
|
config: Record<string, unknown>
|
||||||
): Record<string, unknown> {
|
): Record<string, unknown> {
|
||||||
const result = { ...config }
|
const result = { ...config }
|
||||||
|
|
||||||
if (supportsNewPermissionSystem()) {
|
if (result.tools && typeof result.tools === "object") {
|
||||||
if (result.tools && typeof result.tools === "object") {
|
const existingPermission =
|
||||||
const existingPermission =
|
(result.permission as Record<string, PermissionValue>) || {}
|
||||||
(result.permission as Record<string, PermissionValue>) || {}
|
const migratedPermission = migrateToolsToPermission(
|
||||||
const migratedPermission = migrateToolsToPermission(
|
result.tools as Record<string, boolean>
|
||||||
result.tools as Record<string, boolean>
|
)
|
||||||
)
|
result.permission = { ...migratedPermission, ...existingPermission }
|
||||||
result.permission = { ...migratedPermission, ...existingPermission }
|
delete result.tools
|
||||||
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
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { join } from "node:path"
|
|||||||
import { ALLOWED_AGENTS, CALL_OMO_AGENT_DESCRIPTION } from "./constants"
|
import { ALLOWED_AGENTS, CALL_OMO_AGENT_DESCRIPTION } from "./constants"
|
||||||
import type { CallOmoAgentArgs } from "./types"
|
import type { CallOmoAgentArgs } from "./types"
|
||||||
import type { BackgroundManager } from "../../features/background-agent"
|
import type { BackgroundManager } from "../../features/background-agent"
|
||||||
import { log } from "../../shared/logger"
|
import { log, getAgentToolRestrictions } from "../../shared"
|
||||||
import { consumeNewMessages } from "../../shared/session-cursor"
|
import { consumeNewMessages } from "../../shared/session-cursor"
|
||||||
import { findFirstMessageWithAgent, findNearestMessageWithFields, MESSAGE_STORAGE } from "../../features/hook-message-injector"
|
import { findFirstMessageWithAgent, findNearestMessageWithFields, MESSAGE_STORAGE } from "../../features/hook-message-injector"
|
||||||
import { getSessionAgent } from "../../features/claude-code-session-state"
|
import { getSessionAgent } from "../../features/claude-code-session-state"
|
||||||
@ -188,6 +188,7 @@ async function executeSync(
|
|||||||
body: {
|
body: {
|
||||||
agent: args.subagent_type,
|
agent: args.subagent_type,
|
||||||
tools: {
|
tools: {
|
||||||
|
...getAgentToolRestrictions(args.subagent_type),
|
||||||
task: false,
|
task: false,
|
||||||
sisyphus_task: false,
|
sisyphus_task: false,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,4 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
|
lsp_goto_definition,
|
||||||
|
lsp_find_references,
|
||||||
|
lsp_symbols,
|
||||||
lsp_diagnostics,
|
lsp_diagnostics,
|
||||||
lsp_prepare_rename,
|
lsp_prepare_rename,
|
||||||
lsp_rename,
|
lsp_rename,
|
||||||
@ -52,6 +55,9 @@ export function createBackgroundTools(manager: BackgroundManager, client: Openco
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const builtinTools: Record<string, ToolDefinition> = {
|
export const builtinTools: Record<string, ToolDefinition> = {
|
||||||
|
lsp_goto_definition,
|
||||||
|
lsp_find_references,
|
||||||
|
lsp_symbols,
|
||||||
lsp_diagnostics,
|
lsp_diagnostics,
|
||||||
lsp_prepare_rename,
|
lsp_prepare_rename,
|
||||||
lsp_rename,
|
lsp_rename,
|
||||||
|
|||||||
@ -509,6 +509,37 @@ export class LSPClient {
|
|||||||
await new Promise((r) => setTimeout(r, 1000))
|
await new Promise((r) => setTimeout(r, 1000))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async definition(filePath: string, line: number, character: number): Promise<unknown> {
|
||||||
|
const absPath = resolve(filePath)
|
||||||
|
await this.openFile(absPath)
|
||||||
|
return this.send("textDocument/definition", {
|
||||||
|
textDocument: { uri: pathToFileURL(absPath).href },
|
||||||
|
position: { line: line - 1, character },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async references(filePath: string, line: number, character: number, includeDeclaration = true): Promise<unknown> {
|
||||||
|
const absPath = resolve(filePath)
|
||||||
|
await this.openFile(absPath)
|
||||||
|
return this.send("textDocument/references", {
|
||||||
|
textDocument: { uri: pathToFileURL(absPath).href },
|
||||||
|
position: { line: line - 1, character },
|
||||||
|
context: { includeDeclaration },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async documentSymbols(filePath: string): Promise<unknown> {
|
||||||
|
const absPath = resolve(filePath)
|
||||||
|
await this.openFile(absPath)
|
||||||
|
return this.send("textDocument/documentSymbol", {
|
||||||
|
textDocument: { uri: pathToFileURL(absPath).href },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async workspaceSymbols(query: string): Promise<unknown> {
|
||||||
|
return this.send("workspace/symbol", { query })
|
||||||
|
}
|
||||||
|
|
||||||
async diagnostics(filePath: string): Promise<{ items: Diagnostic[] }> {
|
async diagnostics(filePath: string): Promise<{ items: Diagnostic[] }> {
|
||||||
const absPath = resolve(filePath)
|
const absPath = resolve(filePath)
|
||||||
const uri = pathToFileURL(absPath).href
|
const uri = pathToFileURL(absPath).href
|
||||||
|
|||||||
@ -1,5 +1,34 @@
|
|||||||
import type { LSPServerConfig } from "./types"
|
import type { LSPServerConfig } from "./types"
|
||||||
|
|
||||||
|
export const SYMBOL_KIND_MAP: Record<number, string> = {
|
||||||
|
1: "File",
|
||||||
|
2: "Module",
|
||||||
|
3: "Namespace",
|
||||||
|
4: "Package",
|
||||||
|
5: "Class",
|
||||||
|
6: "Method",
|
||||||
|
7: "Property",
|
||||||
|
8: "Field",
|
||||||
|
9: "Constructor",
|
||||||
|
10: "Enum",
|
||||||
|
11: "Interface",
|
||||||
|
12: "Function",
|
||||||
|
13: "Variable",
|
||||||
|
14: "Constant",
|
||||||
|
15: "String",
|
||||||
|
16: "Number",
|
||||||
|
17: "Boolean",
|
||||||
|
18: "Array",
|
||||||
|
19: "Object",
|
||||||
|
20: "Key",
|
||||||
|
21: "Null",
|
||||||
|
22: "EnumMember",
|
||||||
|
23: "Struct",
|
||||||
|
24: "Event",
|
||||||
|
25: "Operator",
|
||||||
|
26: "TypeParameter",
|
||||||
|
}
|
||||||
|
|
||||||
export const SEVERITY_MAP: Record<number, string> = {
|
export const SEVERITY_MAP: Record<number, string> = {
|
||||||
1: "error",
|
1: "error",
|
||||||
2: "warning",
|
2: "warning",
|
||||||
@ -7,6 +36,8 @@ export const SEVERITY_MAP: Record<number, string> = {
|
|||||||
4: "hint",
|
4: "hint",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const DEFAULT_MAX_REFERENCES = 200
|
||||||
|
export const DEFAULT_MAX_SYMBOLS = 200
|
||||||
export const DEFAULT_MAX_DIAGNOSTICS = 200
|
export const DEFAULT_MAX_DIAGNOSTICS = 200
|
||||||
|
|
||||||
export const LSP_INSTALL_HINTS: Record<string, string> = {
|
export const LSP_INSTALL_HINTS: Record<string, string> = {
|
||||||
|
|||||||
@ -4,4 +4,4 @@ export * from "./config"
|
|||||||
export * from "./client"
|
export * from "./client"
|
||||||
export * from "./utils"
|
export * from "./utils"
|
||||||
// NOTE: lsp_servers removed - duplicates OpenCode's built-in LspServers
|
// NOTE: lsp_servers removed - duplicates OpenCode's built-in LspServers
|
||||||
export { lsp_diagnostics, lsp_prepare_rename, lsp_rename } from "./tools"
|
export { lsp_goto_definition, lsp_find_references, lsp_symbols, lsp_diagnostics, lsp_prepare_rename, lsp_rename } from "./tools"
|
||||||
|
|||||||
@ -1,9 +1,14 @@
|
|||||||
import { tool, type ToolDefinition } from "@opencode-ai/plugin/tool"
|
import { tool, type ToolDefinition } from "@opencode-ai/plugin/tool"
|
||||||
import {
|
import {
|
||||||
|
DEFAULT_MAX_REFERENCES,
|
||||||
|
DEFAULT_MAX_SYMBOLS,
|
||||||
DEFAULT_MAX_DIAGNOSTICS,
|
DEFAULT_MAX_DIAGNOSTICS,
|
||||||
} from "./constants"
|
} from "./constants"
|
||||||
import {
|
import {
|
||||||
withLspClient,
|
withLspClient,
|
||||||
|
formatLocation,
|
||||||
|
formatDocumentSymbol,
|
||||||
|
formatSymbolInfo,
|
||||||
formatDiagnostic,
|
formatDiagnostic,
|
||||||
filterDiagnosticsBySeverity,
|
filterDiagnosticsBySeverity,
|
||||||
formatPrepareRenameResult,
|
formatPrepareRenameResult,
|
||||||
@ -11,14 +16,155 @@ import {
|
|||||||
formatApplyResult,
|
formatApplyResult,
|
||||||
} from "./utils"
|
} from "./utils"
|
||||||
import type {
|
import type {
|
||||||
|
Location,
|
||||||
|
LocationLink,
|
||||||
|
DocumentSymbol,
|
||||||
|
SymbolInfo,
|
||||||
Diagnostic,
|
Diagnostic,
|
||||||
PrepareRenameResult,
|
PrepareRenameResult,
|
||||||
PrepareRenameDefaultBehavior,
|
PrepareRenameDefaultBehavior,
|
||||||
WorkspaceEdit,
|
WorkspaceEdit,
|
||||||
} from "./types"
|
} from "./types"
|
||||||
|
|
||||||
// NOTE: lsp_goto_definition, lsp_find_references, lsp_symbols are removed
|
export const lsp_goto_definition: ToolDefinition = tool({
|
||||||
// as they duplicate OpenCode's built-in LSP tools (LspGotoDefinition, LspFindReferences, LspDocumentSymbols, LspWorkspaceSymbols)
|
description: "Jump to symbol definition. Find WHERE something is defined.",
|
||||||
|
args: {
|
||||||
|
filePath: tool.schema.string(),
|
||||||
|
line: tool.schema.number().min(1).describe("1-based"),
|
||||||
|
character: tool.schema.number().min(0).describe("0-based"),
|
||||||
|
},
|
||||||
|
execute: async (args, context) => {
|
||||||
|
try {
|
||||||
|
const result = await withLspClient(args.filePath, async (client) => {
|
||||||
|
return (await client.definition(args.filePath, args.line, args.character)) as
|
||||||
|
| Location
|
||||||
|
| Location[]
|
||||||
|
| LocationLink[]
|
||||||
|
| null
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
const output = "No definition found"
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
|
||||||
|
const locations = Array.isArray(result) ? result : [result]
|
||||||
|
if (locations.length === 0) {
|
||||||
|
const output = "No definition found"
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
|
||||||
|
const output = locations.map(formatLocation).join("\n")
|
||||||
|
return output
|
||||||
|
} catch (e) {
|
||||||
|
const output = `Error: ${e instanceof Error ? e.message : String(e)}`
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const lsp_find_references: ToolDefinition = tool({
|
||||||
|
description: "Find ALL usages/references of a symbol across the entire workspace.",
|
||||||
|
args: {
|
||||||
|
filePath: tool.schema.string(),
|
||||||
|
line: tool.schema.number().min(1).describe("1-based"),
|
||||||
|
character: tool.schema.number().min(0).describe("0-based"),
|
||||||
|
includeDeclaration: tool.schema.boolean().optional().describe("Include the declaration itself"),
|
||||||
|
},
|
||||||
|
execute: async (args, context) => {
|
||||||
|
try {
|
||||||
|
const result = await withLspClient(args.filePath, async (client) => {
|
||||||
|
return (await client.references(args.filePath, args.line, args.character, args.includeDeclaration ?? true)) as
|
||||||
|
| Location[]
|
||||||
|
| null
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!result || result.length === 0) {
|
||||||
|
const output = "No references found"
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
|
||||||
|
const total = result.length
|
||||||
|
const truncated = total > DEFAULT_MAX_REFERENCES
|
||||||
|
const limited = truncated ? result.slice(0, DEFAULT_MAX_REFERENCES) : result
|
||||||
|
const lines = limited.map(formatLocation)
|
||||||
|
if (truncated) {
|
||||||
|
lines.unshift(`Found ${total} references (showing first ${DEFAULT_MAX_REFERENCES}):`)
|
||||||
|
}
|
||||||
|
const output = lines.join("\n")
|
||||||
|
return output
|
||||||
|
} catch (e) {
|
||||||
|
const output = `Error: ${e instanceof Error ? e.message : String(e)}`
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const lsp_symbols: ToolDefinition = tool({
|
||||||
|
description: "Get symbols from file (document) or search across workspace. Use scope='document' for file outline, scope='workspace' for project-wide symbol search.",
|
||||||
|
args: {
|
||||||
|
filePath: tool.schema.string().describe("File path for LSP context"),
|
||||||
|
scope: tool.schema.enum(["document", "workspace"]).default("document").describe("'document' for file symbols, 'workspace' for project-wide search"),
|
||||||
|
query: tool.schema.string().optional().describe("Symbol name to search (required for workspace scope)"),
|
||||||
|
limit: tool.schema.number().optional().describe("Max results (default 50)"),
|
||||||
|
},
|
||||||
|
execute: async (args, context) => {
|
||||||
|
try {
|
||||||
|
const scope = args.scope ?? "document"
|
||||||
|
|
||||||
|
if (scope === "workspace") {
|
||||||
|
if (!args.query) {
|
||||||
|
return "Error: 'query' is required for workspace scope"
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await withLspClient(args.filePath, async (client) => {
|
||||||
|
return (await client.workspaceSymbols(args.query!)) as SymbolInfo[] | null
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!result || result.length === 0) {
|
||||||
|
return "No symbols found"
|
||||||
|
}
|
||||||
|
|
||||||
|
const total = result.length
|
||||||
|
const limit = Math.min(args.limit ?? DEFAULT_MAX_SYMBOLS, DEFAULT_MAX_SYMBOLS)
|
||||||
|
const truncated = total > limit
|
||||||
|
const limited = result.slice(0, limit)
|
||||||
|
const lines = limited.map(formatSymbolInfo)
|
||||||
|
if (truncated) {
|
||||||
|
lines.unshift(`Found ${total} symbols (showing first ${limit}):`)
|
||||||
|
}
|
||||||
|
return lines.join("\n")
|
||||||
|
} else {
|
||||||
|
const result = await withLspClient(args.filePath, async (client) => {
|
||||||
|
return (await client.documentSymbols(args.filePath)) as DocumentSymbol[] | SymbolInfo[] | null
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!result || result.length === 0) {
|
||||||
|
return "No symbols found"
|
||||||
|
}
|
||||||
|
|
||||||
|
const total = result.length
|
||||||
|
const limit = Math.min(args.limit ?? DEFAULT_MAX_SYMBOLS, DEFAULT_MAX_SYMBOLS)
|
||||||
|
const truncated = total > limit
|
||||||
|
const limited = truncated ? result.slice(0, limit) : result
|
||||||
|
|
||||||
|
const lines: string[] = []
|
||||||
|
if (truncated) {
|
||||||
|
lines.push(`Found ${total} symbols (showing first ${limit}):`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("range" in limited[0]) {
|
||||||
|
lines.push(...(limited as DocumentSymbol[]).map((s) => formatDocumentSymbol(s)))
|
||||||
|
} else {
|
||||||
|
lines.push(...(limited as SymbolInfo[]).map(formatSymbolInfo))
|
||||||
|
}
|
||||||
|
return lines.join("\n")
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return `Error: ${e instanceof Error ? e.message : String(e)}`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
export const lsp_diagnostics: ToolDefinition = tool({
|
export const lsp_diagnostics: ToolDefinition = tool({
|
||||||
description: "Get errors, warnings, hints from language server BEFORE running build.",
|
description: "Get errors, warnings, hints from language server BEFORE running build.",
|
||||||
|
|||||||
@ -17,6 +17,33 @@ export interface Range {
|
|||||||
end: Position
|
end: Position
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Location {
|
||||||
|
uri: string
|
||||||
|
range: Range
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LocationLink {
|
||||||
|
targetUri: string
|
||||||
|
targetRange: Range
|
||||||
|
targetSelectionRange: Range
|
||||||
|
originSelectionRange?: Range
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SymbolInfo {
|
||||||
|
name: string
|
||||||
|
kind: number
|
||||||
|
location: Location
|
||||||
|
containerName?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DocumentSymbol {
|
||||||
|
name: string
|
||||||
|
kind: number
|
||||||
|
range: Range
|
||||||
|
selectionRange: Range
|
||||||
|
children?: DocumentSymbol[]
|
||||||
|
}
|
||||||
|
|
||||||
export interface Diagnostic {
|
export interface Diagnostic {
|
||||||
range: Range
|
range: Range
|
||||||
severity?: number
|
severity?: number
|
||||||
|
|||||||
@ -3,8 +3,12 @@ import { fileURLToPath } from "node:url"
|
|||||||
import { existsSync, readFileSync, writeFileSync } from "fs"
|
import { existsSync, readFileSync, writeFileSync } from "fs"
|
||||||
import { LSPClient, lspManager } from "./client"
|
import { LSPClient, lspManager } from "./client"
|
||||||
import { findServerForExtension } from "./config"
|
import { findServerForExtension } from "./config"
|
||||||
import { SEVERITY_MAP } from "./constants"
|
import { SYMBOL_KIND_MAP, SEVERITY_MAP } from "./constants"
|
||||||
import type {
|
import type {
|
||||||
|
Location,
|
||||||
|
LocationLink,
|
||||||
|
DocumentSymbol,
|
||||||
|
SymbolInfo,
|
||||||
Diagnostic,
|
Diagnostic,
|
||||||
PrepareRenameResult,
|
PrepareRenameResult,
|
||||||
PrepareRenameDefaultBehavior,
|
PrepareRenameDefaultBehavior,
|
||||||
@ -106,11 +110,51 @@ export async function withLspClient<T>(filePath: string, fn: (client: LSPClient)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function formatLocation(loc: Location | LocationLink): string {
|
||||||
|
if ("targetUri" in loc) {
|
||||||
|
const uri = uriToPath(loc.targetUri)
|
||||||
|
const line = loc.targetRange.start.line + 1
|
||||||
|
const char = loc.targetRange.start.character
|
||||||
|
return `${uri}:${line}:${char}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const uri = uriToPath(loc.uri)
|
||||||
|
const line = loc.range.start.line + 1
|
||||||
|
const char = loc.range.start.character
|
||||||
|
return `${uri}:${line}:${char}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatSymbolKind(kind: number): string {
|
||||||
|
return SYMBOL_KIND_MAP[kind] || `Unknown(${kind})`
|
||||||
|
}
|
||||||
|
|
||||||
export function formatSeverity(severity: number | undefined): string {
|
export function formatSeverity(severity: number | undefined): string {
|
||||||
if (!severity) return "unknown"
|
if (!severity) return "unknown"
|
||||||
return SEVERITY_MAP[severity] || `unknown(${severity})`
|
return SEVERITY_MAP[severity] || `unknown(${severity})`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function formatDocumentSymbol(symbol: DocumentSymbol, indent = 0): string {
|
||||||
|
const prefix = " ".repeat(indent)
|
||||||
|
const kind = formatSymbolKind(symbol.kind)
|
||||||
|
const line = symbol.range.start.line + 1
|
||||||
|
let result = `${prefix}${symbol.name} (${kind}) - line ${line}`
|
||||||
|
|
||||||
|
if (symbol.children && symbol.children.length > 0) {
|
||||||
|
for (const child of symbol.children) {
|
||||||
|
result += "\n" + formatDocumentSymbol(child, indent + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatSymbolInfo(symbol: SymbolInfo): string {
|
||||||
|
const kind = formatSymbolKind(symbol.kind)
|
||||||
|
const loc = formatLocation(symbol.location)
|
||||||
|
const container = symbol.containerName ? ` (in ${symbol.containerName})` : ""
|
||||||
|
return `${symbol.name} (${kind})${container} - ${loc}`
|
||||||
|
}
|
||||||
|
|
||||||
export function formatDiagnostic(diag: Diagnostic): string {
|
export function formatDiagnostic(diag: Diagnostic): string {
|
||||||
const severity = formatSeverity(diag.severity)
|
const severity = formatSeverity(diag.severity)
|
||||||
const line = diag.range.start.line + 1
|
const line = diag.range.start.line + 1
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import { discoverSkills } from "../../features/opencode-skill-loader"
|
|||||||
import { getTaskToastManager } from "../../features/task-toast-manager"
|
import { getTaskToastManager } from "../../features/task-toast-manager"
|
||||||
import type { ModelFallbackInfo } from "../../features/task-toast-manager/types"
|
import type { ModelFallbackInfo } from "../../features/task-toast-manager/types"
|
||||||
import { subagentSessions, getSessionAgent } from "../../features/claude-code-session-state"
|
import { subagentSessions, getSessionAgent } from "../../features/claude-code-session-state"
|
||||||
import { log } from "../../shared/logger"
|
import { log, getAgentToolRestrictions } from "../../shared"
|
||||||
|
|
||||||
type OpencodeClient = PluginInput["client"]
|
type OpencodeClient = PluginInput["client"]
|
||||||
|
|
||||||
@ -322,6 +322,7 @@ Use \`background_output\` with task_id="${task.id}" to check progress.`
|
|||||||
...(resumeAgent !== undefined ? { agent: resumeAgent } : {}),
|
...(resumeAgent !== undefined ? { agent: resumeAgent } : {}),
|
||||||
...(resumeModel !== undefined ? { model: resumeModel } : {}),
|
...(resumeModel !== undefined ? { model: resumeModel } : {}),
|
||||||
tools: {
|
tools: {
|
||||||
|
...(resumeAgent ? getAgentToolRestrictions(resumeAgent) : {}),
|
||||||
task: false,
|
task: false,
|
||||||
sisyphus_task: false,
|
sisyphus_task: false,
|
||||||
call_omo_agent: true,
|
call_omo_agent: true,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user