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:
Z
2026-03-25 22:08:41 +08:00
parent 2926e366bd
commit 088693697a
5 changed files with 143 additions and 10 deletions
+7 -2
View File
@@ -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