feat: filter agent-restricted skills from prompts and tool description
Skills with an `agent` frontmatter field are intended for a specific agent. Previously they still appeared in: - every agent's system prompt (via `buildAvailableSkills`) - the `skill` tool's `<available_items>` description visible to all agents This wasted tokens and could mislead agents into attempting calls that would be rejected at execution time. Changes: - `buildAvailableSkills`: new optional `agentName` parameter; when provided, skills whose `definition.agent` does not match are excluded - `builtin-agents.ts`: pass per-agent name to `buildAvailableSkills` for sisyphus, hephaestus, and atlas, so each agent's prompt only lists the skills it is allowed to use - `createSkillTool` (`tools.ts`): exclude agent-restricted skills from both the eager and lazy description builds, keeping the shared tool description free of skills the current agent cannot access Execution-time enforcement (throwing on mismatch) is unchanged; this change adds the earlier, description-level visibility gate. Tests: new `available-skills.test.ts` (5 cases) + 3 new cases in `tools.test.ts` covering the description-filter and execute paths. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -64,13 +64,18 @@ export function createSkillTool(options: SkillLoadOptions = {}): ToolDefinition
|
||||
if (!force && cachedDescription) return cachedDescription
|
||||
const skills = await getSkills()
|
||||
const commands = getCommands()
|
||||
const skillInfos = skills.map(loadedSkillToInfo)
|
||||
// Exclude agent-restricted skills from the description: they must not be
|
||||
// visible to agents that are not their designated owner. The execute-time
|
||||
// check already enforces the restriction at call time.
|
||||
const publicSkills = skills.filter((s) => !s.definition.agent)
|
||||
const skillInfos = publicSkills.map(loadedSkillToInfo)
|
||||
cachedDescription = formatCombinedDescription(skillInfos, commands)
|
||||
return cachedDescription
|
||||
}
|
||||
|
||||
if (options.skills !== undefined) {
|
||||
const skillInfos = options.skills.map(loadedSkillToInfo)
|
||||
const publicSkills = options.skills.filter((s) => !s.definition.agent)
|
||||
const skillInfos = publicSkills.map(loadedSkillToInfo)
|
||||
const commandsForDescription = options.commands ?? []
|
||||
let needsAsyncRefresh = false
|
||||
|
||||
|
||||
@@ -636,6 +636,50 @@ describe("skill tool - dynamic discovery", () => {
|
||||
expect(result).not.toContain("SHOULD_BE_OVERRIDDEN")
|
||||
})
|
||||
})
|
||||
describe("skill tool - agent-restricted skill visibility in description", () => {
|
||||
it("excludes agent-restricted skill from description <available_items>", () => {
|
||||
// given: a skill restricted to oracle, and a public skill
|
||||
const loadedSkills = [
|
||||
createMockSkill("public-skill"),
|
||||
createMockSkill("oracle-only-skill", { agent: "oracle" }),
|
||||
]
|
||||
|
||||
// when: tool is created with these skills (as tool-registry would inject them)
|
||||
const tool = createSkillTool({ skills: loadedSkills })
|
||||
|
||||
// then: oracle-only skill must NOT appear in the description
|
||||
expect(tool.description).toContain("public-skill")
|
||||
expect(tool.description).not.toContain("oracle-only-skill")
|
||||
})
|
||||
|
||||
it("includes public skill (no agent field) in description regardless of context", () => {
|
||||
// given
|
||||
const loadedSkills = [createMockSkill("public-skill")]
|
||||
|
||||
// when
|
||||
const tool = createSkillTool({ skills: loadedSkills })
|
||||
|
||||
// then
|
||||
expect(tool.description).toContain("public-skill")
|
||||
})
|
||||
|
||||
it("execute still works for agent-restricted skill when called with correct agent context", async () => {
|
||||
// given: tool created WITHOUT the restricted skill in description list,
|
||||
// but the full skill list is available for execute via getSkills()
|
||||
// (simulating what tool-registry does: description uses filtered list,
|
||||
// but execute discovers from disk / full list)
|
||||
const restrictedSkill = createMockSkill("oracle-only-skill", { agent: "oracle" })
|
||||
const tool = createSkillTool({ skills: [restrictedSkill] })
|
||||
const oracleContext = { ...mockContext, agent: "oracle" }
|
||||
|
||||
// when: oracle agent explicitly calls the skill
|
||||
const result = await tool.execute({ name: "oracle-only-skill" }, oracleContext)
|
||||
|
||||
// then: execution succeeds
|
||||
expect(result).toContain("oracle-only-skill")
|
||||
})
|
||||
})
|
||||
|
||||
describe("skill tool - dynamic description cache invalidation", () => {
|
||||
it("keeps description available after execute misses a skill", async () => {
|
||||
// given
|
||||
|
||||
Reference in New Issue
Block a user