diff --git a/src/agents/builtin-agents.ts b/src/agents/builtin-agents.ts
index dde78131b..b3c56f7c0 100644
--- a/src/agents/builtin-agents.ts
+++ b/src/agents/builtin-agents.ts
@@ -100,8 +100,6 @@ export async function createBuiltinAgents(
description: categories?.[name]?.description ?? CATEGORY_DESCRIPTIONS[name] ?? "General tasks",
}))
- const availableSkills = buildAvailableSkills(discoveredSkills, browserProvider, disabledSkills, teamModeEnabled)
-
// Collect general agents first (for availableAgents), but don't add to result yet
const { pendingAgentConfigs, availableAgents } = collectPendingBuiltinAgents({
agentSources,
@@ -129,7 +127,7 @@ export async function createBuiltinAgents(
systemDefaultModel,
isFirstRunNoCache,
availableAgents,
- availableSkills,
+ availableSkills: buildAvailableSkills(discoveredSkills, browserProvider, disabledSkills, teamModeEnabled, "sisyphus"),
availableCategories,
mergedCategories,
directory,
@@ -148,7 +146,7 @@ export async function createBuiltinAgents(
systemDefaultModel,
isFirstRunNoCache,
availableAgents,
- availableSkills,
+ availableSkills: buildAvailableSkills(discoveredSkills, browserProvider, disabledSkills, teamModeEnabled, "hephaestus"),
availableCategories,
mergedCategories,
directory,
@@ -171,7 +169,7 @@ export async function createBuiltinAgents(
availableModels,
systemDefaultModel,
availableAgents,
- availableSkills,
+ availableSkills: buildAvailableSkills(discoveredSkills, browserProvider, disabledSkills, teamModeEnabled, "atlas"),
mergedCategories,
directory,
userCategories: categories,
diff --git a/src/agents/builtin-agents/available-skills.test.ts b/src/agents/builtin-agents/available-skills.test.ts
index fe5ea8441..581b63249 100644
--- a/src/agents/builtin-agents/available-skills.test.ts
+++ b/src/agents/builtin-agents/available-skills.test.ts
@@ -1,9 +1,24 @@
-import { describe, expect, test } from "bun:test"
-
+///
+import { describe, expect, it, test } from "bun:test"
+import type { LoadedSkill } from "../../features/opencode-skill-loader/types"
import { buildAvailableSkills } from "./available-skills"
type DiscoveredSkills = Parameters[0]
+function makeSkill(name: string, agent?: string): LoadedSkill {
+ return {
+ name,
+ resolvedPath: `/test/skills/${name}`,
+ definition: {
+ name,
+ description: `Skill ${name}`,
+ template: "",
+ agent,
+ },
+ scope: "user",
+ }
+}
+
describe("buildAvailableSkills", () => {
test("includes team-mode when team mode is enabled", () => {
// given
@@ -27,3 +42,67 @@ describe("buildAvailableSkills", () => {
expect(availableSkills.some((skill) => skill.name === "team-mode")).toBe(false)
})
})
+
+describe("buildAvailableSkills - agentName filtering", () => {
+ it("includes agent-restricted skill when agentName is not provided (backward compat)", () => {
+ // given
+ const skills = [makeSkill("oracle-only", "oracle")]
+
+ // when
+ const result = buildAvailableSkills(skills, undefined, undefined, undefined, undefined)
+
+ // then: no agentName → no filtering, skill is included
+ expect(result.map((s) => s.name)).toContain("oracle-only")
+ })
+
+ it("includes skill when agentName matches the skill's agent field", () => {
+ // given
+ const skills = [makeSkill("sisyphus-only", "sisyphus")]
+
+ // when
+ const result = buildAvailableSkills(skills, undefined, undefined, undefined, "sisyphus")
+
+ // then: matching agent → included
+ expect(result.map((s) => s.name)).toContain("sisyphus-only")
+ })
+
+ it("excludes skill when agentName does not match the skill's agent field", () => {
+ // given
+ const skills = [makeSkill("sisyphus-only", "sisyphus")]
+
+ // when
+ const result = buildAvailableSkills(skills, undefined, undefined, undefined, "oracle")
+
+ // then: wrong agent → excluded
+ expect(result.map((s) => s.name)).not.toContain("sisyphus-only")
+ })
+
+ it("includes skill with no agent field regardless of agentName", () => {
+ // given
+ const skills = [makeSkill("public-skill")]
+
+ // when
+ const result = buildAvailableSkills(skills, undefined, undefined, undefined, "sisyphus")
+
+ // then: no agent restriction → always included
+ expect(result.map((s) => s.name)).toContain("public-skill")
+ })
+
+ it("filters per-agent while keeping public skills", () => {
+ // given
+ const skills = [
+ makeSkill("public-skill"),
+ makeSkill("sisyphus-only", "sisyphus"),
+ makeSkill("oracle-only", "oracle"),
+ ]
+
+ // when
+ const result = buildAvailableSkills(skills, undefined, undefined, undefined, "sisyphus")
+
+ // then
+ const names = result.map((s) => s.name)
+ expect(names).toContain("public-skill")
+ expect(names).toContain("sisyphus-only")
+ expect(names).not.toContain("oracle-only")
+ })
+})
diff --git a/src/agents/builtin-agents/available-skills.ts b/src/agents/builtin-agents/available-skills.ts
index d6aafa8cd..136679168 100644
--- a/src/agents/builtin-agents/available-skills.ts
+++ b/src/agents/builtin-agents/available-skills.ts
@@ -14,6 +14,7 @@ export function buildAvailableSkills(
browserProvider?: BrowserAutomationProvider,
disabledSkills?: Set,
teamModeEnabled?: boolean,
+ agentName?: string,
): AvailableSkill[] {
const builtinSkills = createBuiltinSkills({ browserProvider, disabledSkills, teamModeEnabled })
const builtinSkillNames = new Set(builtinSkills.map(s => s.name))
@@ -25,7 +26,13 @@ export function buildAvailableSkills(
}))
const discoveredAvailable: AvailableSkill[] = discoveredSkills
- .filter(s => !builtinSkillNames.has(s.name) && !disabledSkills?.has(s.name))
+ .filter(s => {
+ if (builtinSkillNames.has(s.name) || disabledSkills?.has(s.name)) return false
+ // If the skill declares an agent restriction and we know the current agent,
+ // exclude skills that don't belong to this agent.
+ if (agentName && s.definition.agent && s.definition.agent !== agentName) return false
+ return true
+ })
.map((skill) => ({
name: skill.name,
description: skill.definition.description ?? "",
diff --git a/src/tools/skill/tools.ts b/src/tools/skill/tools.ts
index 0d4067505..2345b4298 100644
--- a/src/tools/skill/tools.ts
+++ b/src/tools/skill/tools.ts
@@ -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
diff --git a/src/tools/skill/zauc-mocks-skill-tools/tools.test.ts b/src/tools/skill/zauc-mocks-skill-tools/tools.test.ts
index ccdfba648..ec28c3e66 100644
--- a/src/tools/skill/zauc-mocks-skill-tools/tools.test.ts
+++ b/src/tools/skill/zauc-mocks-skill-tools/tools.test.ts
@@ -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 ", () => {
+ // 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