mirror of
https://gitea.toothfairyai.com/ToothFairyAI/tf_code.git
synced 2026-04-04 08:03:14 +00:00
feat: add native skill tool with permission system (#5930)
Co-authored-by: Dax Raad <d@ironbay.co>
This commit is contained in:
committed by
GitHub
parent
b9029afa22
commit
046e351140
@@ -10,6 +10,7 @@ import { TodoWriteTool, TodoReadTool } from "./todo"
|
||||
import { WebFetchTool } from "./webfetch"
|
||||
import { WriteTool } from "./write"
|
||||
import { InvalidTool } from "./invalid"
|
||||
import { SkillTool } from "./skill"
|
||||
import type { Agent } from "../agent/agent"
|
||||
import { Tool } from "./tool"
|
||||
import { Instance } from "../project/instance"
|
||||
@@ -103,6 +104,7 @@ export namespace ToolRegistry {
|
||||
TodoReadTool,
|
||||
WebSearchTool,
|
||||
CodeSearchTool,
|
||||
SkillTool,
|
||||
...(Flag.OPENCODE_EXPERIMENTAL_LSP_TOOL ? [LspTool] : []),
|
||||
...(config.experimental?.batch_tool === true ? [BatchTool] : []),
|
||||
...custom,
|
||||
@@ -150,6 +152,10 @@ export namespace ToolRegistry {
|
||||
result["codesearch"] = false
|
||||
result["websearch"] = false
|
||||
}
|
||||
// Disable skill tool if all skills are denied
|
||||
if (agent.permission.skill["*"] === "deny" && Object.keys(agent.permission.skill).length === 1) {
|
||||
result["skill"] = false
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
84
packages/opencode/src/tool/skill.ts
Normal file
84
packages/opencode/src/tool/skill.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import path from "path"
|
||||
import z from "zod"
|
||||
import { Tool } from "./tool"
|
||||
import { Skill } from "../skill"
|
||||
import { Agent } from "../agent/agent"
|
||||
import { Permission } from "../permission"
|
||||
import { Wildcard } from "../util/wildcard"
|
||||
import { ConfigMarkdown } from "../config/markdown"
|
||||
|
||||
export const SkillTool = Tool.define("skill", async () => {
|
||||
const skills = await Skill.all()
|
||||
return {
|
||||
description: [
|
||||
"Load a skill to get detailed instructions for a specific task.",
|
||||
"Skills provide specialized knowledge and step-by-step guidance.",
|
||||
"Use this when a task matches an available skill's description.",
|
||||
"<available_skills>",
|
||||
...skills.flatMap((skill) => [
|
||||
` <skill>`,
|
||||
` <name>${skill.name}</name>`,
|
||||
` <description>${skill.description}</description>`,
|
||||
` </skill>`,
|
||||
]),
|
||||
"</available_skills>",
|
||||
].join(" "),
|
||||
parameters: z.object({
|
||||
name: z
|
||||
.string()
|
||||
.describe("The skill identifier from available_skills (e.g., 'code-review' or 'category/helper')"),
|
||||
}),
|
||||
async execute(params, ctx) {
|
||||
const agent = await Agent.get(ctx.agent)
|
||||
|
||||
const skill = await Skill.get(params.name)
|
||||
|
||||
if (!skill) {
|
||||
const available = Skill.all().then((x) => Object.keys(x).join(", "))
|
||||
throw new Error(`Skill "${params.name}" not found. Available skills: ${available || "none"}`)
|
||||
}
|
||||
|
||||
// Check permission using Wildcard.all on the skill ID
|
||||
const permissions = agent.permission.skill
|
||||
const action = Wildcard.all(params.name, permissions)
|
||||
|
||||
if (action === "deny") {
|
||||
throw new Permission.RejectedError(
|
||||
ctx.sessionID,
|
||||
"skill",
|
||||
ctx.callID,
|
||||
{ skill: params.name },
|
||||
`Access to skill "${params.name}" is denied for agent "${agent.name}".`,
|
||||
)
|
||||
}
|
||||
|
||||
if (action === "ask") {
|
||||
await Permission.ask({
|
||||
type: "skill",
|
||||
pattern: params.name,
|
||||
sessionID: ctx.sessionID,
|
||||
messageID: ctx.messageID,
|
||||
callID: ctx.callID,
|
||||
title: `Load skill: ${skill.name}`,
|
||||
metadata: { id: params.name, name: skill.name, description: skill.description },
|
||||
})
|
||||
}
|
||||
|
||||
// Load and parse skill content
|
||||
const parsed = await ConfigMarkdown.parse(skill.location)
|
||||
const dir = path.dirname(skill.location)
|
||||
|
||||
// Format output similar to plugin pattern
|
||||
const output = [`## Skill: ${skill.name}`, "", `**Base directory**: ${dir}`, "", parsed.content.trim()].join("\n")
|
||||
|
||||
return {
|
||||
title: `Loaded skill: ${skill.name}`,
|
||||
output,
|
||||
metadata: {
|
||||
name: skill.name,
|
||||
dir,
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user