From 77f9168f8722c845ca100bfed912881ad6606581 Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Tue, 14 Apr 2026 06:51:12 +0000 Subject: [PATCH 1/4] =?UTF-8?q?=F0=9F=A4=96=20fix:=20include=20universal?= =?UTF-8?q?=20skill=20roots=20in=20project=20runtime?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use the shared default roots helper so project-runtime discovery includes legacy .agents skill roots, and update split-root tests to cover visible and hidden skills from both universal roots. --- .../services/tools/agent_skill_list.test.ts | 381 ++++++++++++------ src/node/services/tools/agent_skill_list.ts | 13 +- 2 files changed, 261 insertions(+), 133 deletions(-) diff --git a/src/node/services/tools/agent_skill_list.test.ts b/src/node/services/tools/agent_skill_list.test.ts index 3f0ba25d4e..506c42ce92 100644 --- a/src/node/services/tools/agent_skill_list.test.ts +++ b/src/node/services/tools/agent_skill_list.test.ts @@ -76,6 +76,29 @@ async function withMuxRoot(muxRoot: string, callback: () => Promise): Prom } } +async function withHomeDir(homeDir: string, callback: () => Promise): Promise { + const previousHome = process.env.HOME; + const previousUserProfile = process.env.USERPROFILE; + process.env.HOME = homeDir; + process.env.USERPROFILE = homeDir; + + try { + await callback(); + } finally { + if (previousHome === undefined) { + delete process.env.HOME; + } else { + process.env.HOME = previousHome; + } + + if (previousUserProfile === undefined) { + delete process.env.USERPROFILE; + } else { + process.env.USERPROFILE = previousUserProfile; + } + } +} + function getSkill(skills: AgentSkillDescriptor[], name: string): AgentSkillDescriptor { const skill = skills.find((candidate) => candidate.name === name); expect(skill).toBeDefined(); @@ -616,112 +639,114 @@ describe("agent_skill_list", () => { const remoteWorkspaceRoot = "/remote/workspace"; - await withMuxRoot(muxHome.path, async () => { - await writeSkill(path.join(project.path, ".mux", "skills"), "project-remote", { - description: "from remote workspace", - }); - await writeGlobalSkill(muxHome.path, "host-global", { - description: "from host mux home", - }); - - // Must use a RemoteRuntime subclass (not LocalRuntime) so the global-root - // fallback to host-local kicks in via instanceof RemoteRuntime. - const remoteRuntime = new TrueRemotePathMappedRuntime(project.path, remoteWorkspaceRoot); - const config = createTestToolConfig(project.path, { - workspaceId: "regular-workspace", - runtime: remoteRuntime, - muxScope: { - type: "project", - muxHome: muxHome.path, - projectRoot: project.path, - projectStorageAuthority: "runtime", - }, - }); - - const tool = createAgentSkillListTool({ - ...config, - cwd: remoteWorkspaceRoot, + await withHomeDir(muxHome.path, async () => { + await withMuxRoot(muxHome.path, async () => { + await writeSkill(path.join(project.path, ".mux", "skills"), "project-remote", { + description: "from remote workspace", + }); + await writeGlobalSkill(muxHome.path, "host-global", { + description: "from host mux home", + }); + + // Must use a RemoteRuntime subclass (not LocalRuntime) so the global-root + // fallback to host-local kicks in via instanceof RemoteRuntime. + const remoteRuntime = new TrueRemotePathMappedRuntime(project.path, remoteWorkspaceRoot); + const config = createTestToolConfig(project.path, { + workspaceId: "regular-workspace", + runtime: remoteRuntime, + muxScope: { + type: "project", + muxHome: muxHome.path, + projectRoot: project.path, + projectStorageAuthority: "runtime", + }, + }); + + const tool = createAgentSkillListTool({ + ...config, + cwd: remoteWorkspaceRoot, + }); + + const result = (await tool.execute!( + { includeUnadvertised: true }, + mockToolCallOptions + )) as AgentSkillListToolResult; + + expect(result.success).toBe(true); + if (!result.success) { + return; + } + + expect(result.skills.map((skill) => skill.name)).toEqual([ + "host-global", + "project-remote", + ]); + expect(result.skills.find((skill) => skill.name === "host-global")?.scope).toBe("global"); + expect(result.skills.find((skill) => skill.name === "project-remote")?.scope).toBe( + "project" + ); }); - - const result = (await tool.execute!( - { includeUnadvertised: true }, - mockToolCallOptions - )) as AgentSkillListToolResult; - - expect(result.success).toBe(true); - if (!result.success) { - return; - } - - expect(result.skills.map((skill) => skill.name)).toEqual(["host-global", "project-remote"]); - expect(result.skills.find((skill) => skill.name === "host-global")?.scope).toBe("global"); - expect(result.skills.find((skill) => skill.name === "project-remote")?.scope).toBe( - "project" - ); }); }); - it("uses runtime mux home for global roots in project-runtime mode", async () => { + it("uses runtime mux home and lists ~/.agents/skills in project-runtime mode", async () => { using tempDir = new TestTempDir("test-agent-skill-list-split-root-runtime-mux-home"); - using legacyMuxRoot = new TestTempDir("test-agent-skill-list-split-root-legacy-mux-home"); + using legacyHome = new TestTempDir("test-agent-skill-list-split-root-legacy-home"); const runtimeGlobalSkill = "runtime-mux-home-global-skill"; const legacyGlobalSkill = "legacy-tilde-global-skill"; const remoteWorkspaceRoot = "/var/workspace"; - const previousMuxRoot = process.env.MUX_ROOT; - - process.env.MUX_ROOT = legacyMuxRoot.path; - - try { - await writeGlobalSkill(path.join(tempDir.path, "mux"), runtimeGlobalSkill, { - description: "from runtime mux home", - }); - await writeGlobalSkill(legacyMuxRoot.path, legacyGlobalSkill, { - description: "from legacy tilde root", - }); - const remoteRuntime = new RemotePathMappedRuntime(tempDir.path, "/var", { - muxHome: "/var/mux", - resolveToRemotePath: false, - }); - - const config = createTestToolConfig(tempDir.path, { - workspaceId: "regular-workspace", - runtime: remoteRuntime, - muxScope: { - type: "project", - muxHome: tempDir.path, - projectRoot: tempDir.path, - projectStorageAuthority: "runtime", - }, + await withHomeDir(legacyHome.path, async () => { + await withMuxRoot(legacyHome.path, async () => { + await writeGlobalSkill(path.join(tempDir.path, "mux"), runtimeGlobalSkill, { + description: "from runtime mux home", + }); + await writeSkill(path.join(legacyHome.path, ".agents", "skills"), legacyGlobalSkill, { + description: "from legacy tilde root", + }); + + const remoteRuntime = new RemotePathMappedRuntime(tempDir.path, "/var", { + muxHome: "/var/mux", + resolveToRemotePath: false, + }); + + const config = createTestToolConfig(tempDir.path, { + workspaceId: "regular-workspace", + runtime: remoteRuntime, + muxScope: { + type: "project", + muxHome: legacyHome.path, + projectRoot: tempDir.path, + projectStorageAuthority: "runtime", + }, + }); + + const tool = createAgentSkillListTool({ + ...config, + cwd: remoteWorkspaceRoot, + }); + + const result = (await tool.execute!( + { includeUnadvertised: true }, + mockToolCallOptions + )) as AgentSkillListToolResult; + + expect(result.success).toBe(true); + if (result.success) { + expect( + result.skills.some( + (skill) => skill.name === runtimeGlobalSkill && skill.scope === "global" + ) + ).toBe(true); + expect(getSkill(result.skills, legacyGlobalSkill)).toMatchObject({ + name: legacyGlobalSkill, + description: "from legacy tilde root", + scope: "global", + }); + } }); - - const tool = createAgentSkillListTool({ - ...config, - cwd: remoteWorkspaceRoot, - }); - - const result = (await tool.execute!( - { includeUnadvertised: true }, - mockToolCallOptions - )) as AgentSkillListToolResult; - - expect(result.success).toBe(true); - if (result.success) { - expect( - result.skills.some( - (skill) => skill.name === runtimeGlobalSkill && skill.scope === "global" - ) - ).toBe(true); - expect(result.skills.find((skill) => skill.name === legacyGlobalSkill)).toBeUndefined(); - } - } finally { - if (previousMuxRoot === undefined) { - delete process.env.MUX_ROOT; - } else { - process.env.MUX_ROOT = previousMuxRoot; - } - } + }); }); it("lists project and global skills with the same name in project-runtime mode", async () => { @@ -776,41 +801,149 @@ describe("agent_skill_list", () => { } }); - it("excludes skills from legacy .agents/skills roots in project-runtime mode", async () => { - using tempDir = new TestTempDir("test-agent-skill-list-split-root-legacy-root-exclusion"); + it("lists skills from legacy .agents/skills roots in project-runtime mode", async () => { + using projectDir = new TestTempDir("test-agent-skill-list-split-root-legacy-root-inclusion"); + using homeDir = new TestTempDir("test-agent-skill-list-split-root-legacy-root-home"); const writableSkillName = "runtime-writable-project-skill"; const legacySkillName = "runtime-legacy-project-universal-skill"; - await withMuxRoot(tempDir.path, async () => { - await writeGlobalSkill(path.join(tempDir.path, ".mux"), writableSkillName); - await writeSkill(path.join(tempDir.path, ".agents", "skills"), legacySkillName); - - const config = createTestToolConfig(tempDir.path, { - workspaceId: "regular-workspace", - muxScope: { - type: "project", - muxHome: tempDir.path, - projectRoot: tempDir.path, - projectStorageAuthority: "runtime", - }, + await withHomeDir(homeDir.path, async () => { + await withMuxRoot(homeDir.path, async () => { + await writeGlobalSkill(path.join(projectDir.path, ".mux"), writableSkillName); + await writeSkill(path.join(projectDir.path, ".agents", "skills"), legacySkillName); + + const config = createTestToolConfig(projectDir.path, { + workspaceId: "regular-workspace", + muxScope: { + type: "project", + muxHome: homeDir.path, + projectRoot: projectDir.path, + projectStorageAuthority: "runtime", + }, + }); + + const tool = createAgentSkillListTool(config); + + const result = (await tool.execute!( + { includeUnadvertised: true }, + mockToolCallOptions + )) as AgentSkillListToolResult; + + expect(result.success).toBe(true); + if (result.success) { + expect( + result.skills.some( + (skill) => skill.name === writableSkillName && skill.scope === "project" + ) + ).toBe(true); + expect(getSkill(result.skills, legacySkillName)).toMatchObject({ + name: legacySkillName, + scope: "project", + }); + } }); + }); + }); - const tool = createAgentSkillListTool(config); - - const result = (await tool.execute!( - { includeUnadvertised: true }, - mockToolCallOptions - )) as AgentSkillListToolResult; + it("filters hidden project .agents/skills entries unless includeUnadvertised is true in project-runtime mode", async () => { + using projectDir = new TestTempDir( + "test-agent-skill-list-split-root-hidden-project-universal" + ); + using homeDir = new TestTempDir("test-agent-skill-list-split-root-hidden-project-home"); + const hiddenSkillName = "runtime-hidden-project-universal-skill"; + + await withHomeDir(homeDir.path, async () => { + await withMuxRoot(homeDir.path, async () => { + await writeSkill(path.join(projectDir.path, ".agents", "skills"), hiddenSkillName, { + advertise: false, + }); + + const config = createTestToolConfig(projectDir.path, { + workspaceId: "regular-workspace", + muxScope: { + type: "project", + muxHome: homeDir.path, + projectRoot: projectDir.path, + projectStorageAuthority: "runtime", + }, + }); + + const tool = createAgentSkillListTool(config); + + const defaultResult = (await tool.execute!( + {}, + mockToolCallOptions + )) as AgentSkillListToolResult; + expect(defaultResult.success).toBe(true); + if (defaultResult.success) { + expect(defaultResult.skills.some((skill) => skill.name === hiddenSkillName)).toBe( + false + ); + } + + const includeAllResult = (await tool.execute!( + { includeUnadvertised: true }, + mockToolCallOptions + )) as AgentSkillListToolResult; + expect(includeAllResult.success).toBe(true); + if (includeAllResult.success) { + expect(getSkill(includeAllResult.skills, hiddenSkillName)).toMatchObject({ + name: hiddenSkillName, + scope: "project", + advertise: false, + }); + } + }); + }); + }); - expect(result.success).toBe(true); - if (result.success) { - expect( - result.skills.some( - (skill) => skill.name === writableSkillName && skill.scope === "project" - ) - ).toBe(true); - expect(result.skills.find((skill) => skill.name === legacySkillName)).toBeUndefined(); - } + it("filters hidden ~/.agents/skills entries unless includeUnadvertised is true in project-runtime mode", async () => { + using projectDir = new TestTempDir("test-agent-skill-list-split-root-hidden-global-project"); + using homeDir = new TestTempDir("test-agent-skill-list-split-root-hidden-global-home"); + const hiddenSkillName = "runtime-hidden-global-universal-skill"; + + await withHomeDir(homeDir.path, async () => { + await withMuxRoot(homeDir.path, async () => { + await writeSkill(path.join(homeDir.path, ".agents", "skills"), hiddenSkillName, { + advertise: false, + }); + + const config = createTestToolConfig(projectDir.path, { + workspaceId: "regular-workspace", + muxScope: { + type: "project", + muxHome: homeDir.path, + projectRoot: projectDir.path, + projectStorageAuthority: "runtime", + }, + }); + + const tool = createAgentSkillListTool(config); + + const defaultResult = (await tool.execute!( + {}, + mockToolCallOptions + )) as AgentSkillListToolResult; + expect(defaultResult.success).toBe(true); + if (defaultResult.success) { + expect(defaultResult.skills.some((skill) => skill.name === hiddenSkillName)).toBe( + false + ); + } + + const includeAllResult = (await tool.execute!( + { includeUnadvertised: true }, + mockToolCallOptions + )) as AgentSkillListToolResult; + expect(includeAllResult.success).toBe(true); + if (includeAllResult.success) { + expect(getSkill(includeAllResult.skills, hiddenSkillName)).toMatchObject({ + name: hiddenSkillName, + scope: "global", + advertise: false, + }); + } + }); }); }); diff --git a/src/node/services/tools/agent_skill_list.ts b/src/node/services/tools/agent_skill_list.ts index c05d0549dd..39bd47ebdb 100644 --- a/src/node/services/tools/agent_skill_list.ts +++ b/src/node/services/tools/agent_skill_list.ts @@ -11,7 +11,6 @@ import type { ToolConfiguration, ToolFactory } from "@/common/utils/tools/tools" import { discoverAgentSkills, getDefaultAgentSkillsRoots, - type AgentSkillsRoots, } from "@/node/services/agentSkills/agentSkillsService"; import { parseSkillMarkdown } from "@/node/services/agentSkills/parseSkillMarkdown"; import { resolveSkillStorageContext } from "@/node/services/agentSkills/skillStorageContext"; @@ -164,16 +163,12 @@ export const createAgentSkillListTool: ToolFactory = (config: ToolConfiguration) }); if (skillCtx.kind === "project-runtime") { - // Only enumerate roots that paired mutation tools (write/delete) can target. - // Excludes .agents/skills and ~/.agents/skills which are read-only legacy roots. - const writableRoots: AgentSkillsRoots = { - projectRoot: skillCtx.runtime.normalizePath(".mux/skills", skillCtx.workspacePath), - globalRoot: getDefaultAgentSkillsRoots(skillCtx.runtime, skillCtx.workspacePath) - .globalRoot, - }; + // Runtime discovery mirrors the shared default roots contract so project-runtime + // listings include .mux/skills and .agents/skills plus ~/.mux/skills and ~/.agents/skills. + const roots = getDefaultAgentSkillsRoots(skillCtx.runtime, skillCtx.workspacePath); const discovered = await discoverAgentSkills(skillCtx.runtime, skillCtx.workspacePath, { - roots: writableRoots, + roots, containment: skillCtx.containment, dedupeByName: false, }); From d7acfa8a72a96d44fb008f5d6370e672a93ca2c5 Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Tue, 14 Apr 2026 06:45:48 +0000 Subject: [PATCH 2/4] docs: update agent skill list roots --- src/common/utils/tools/toolDefinitions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/utils/tools/toolDefinitions.ts b/src/common/utils/tools/toolDefinitions.ts index d18bbcef2f..35ecacbc1f 100644 --- a/src/common/utils/tools/toolDefinitions.ts +++ b/src/common/utils/tools/toolDefinitions.ts @@ -1192,7 +1192,7 @@ export const TOOL_DEFINITIONS = { }, agent_skill_list: { description: - "List available skills. In a project workspace, lists both project skills (.mux/skills/) and global skills (~/.mux/skills/), each tagged with its scope. In the system workspace, lists global skills only.", + "List available skills. In a project workspace, lists project skills from .mux/skills/ and legacy/universal .agents/skills/, plus global skills from ~/.mux/skills/ and legacy/universal ~/.agents/skills/, each tagged with its scope. In the system workspace, lists global skills only.", schema: z .object({ includeUnadvertised: z From 2403977775c156f4bc0dc4c04537b001d65b3191 Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Tue, 14 Apr 2026 09:08:41 +0000 Subject: [PATCH 3/4] Expand local agent skill discovery roots --- .../services/tools/agent_skill_list.test.ts | 121 ++++++++++++++++++ src/node/services/tools/agent_skill_list.ts | 26 +++- 2 files changed, 141 insertions(+), 6 deletions(-) diff --git a/src/node/services/tools/agent_skill_list.test.ts b/src/node/services/tools/agent_skill_list.test.ts index 506c42ce92..46a638669d 100644 --- a/src/node/services/tools/agent_skill_list.test.ts +++ b/src/node/services/tools/agent_skill_list.test.ts @@ -369,6 +369,67 @@ describe("agent_skill_list", () => { }); }); + it("lists skills from all four local roots in project workspaces", async () => { + using homeDir = new TestTempDir("test-agent-skill-list-local-roots-home"); + using project = new TestTempDir("test-agent-skill-list-local-roots-project"); + const muxHome = path.join(homeDir.path, ".mux"); + + await fs.mkdir(muxHome, { recursive: true }); + + await withMuxRoot(muxHome, async () => { + await writeSkill(path.join(project.path, ".mux", "skills"), "project-only", { + description: "from project mux root", + }); + await writeSkill(path.join(project.path, ".agents", "skills"), "project-universal", { + description: "from project universal root", + }); + await writeGlobalSkill(muxHome, "global-only", { + description: "from global mux root", + }); + await writeSkill(path.join(homeDir.path, ".agents", "skills"), "global-universal", { + description: "from global universal root", + }); + + const tool = createAgentSkillListTool( + createTestToolConfig(project.path, { + muxScope: { + type: "project", + muxHome, + projectRoot: project.path, + projectStorageAuthority: "host-local", + }, + }) + ); + const result = (await tool.execute!({}, mockToolCallOptions)) as AgentSkillListToolResult; + + expect(result.success).toBe(true); + if (!result.success) { + return; + } + + expect(getSkill(result.skills, "project-only")).toMatchObject({ + name: "project-only", + description: "from project mux root", + scope: "project", + }); + expect(getSkill(result.skills, "project-universal")).toMatchObject({ + name: "project-universal", + description: "from project universal root", + scope: "project", + }); + expect(getSkill(result.skills, "global-only")).toMatchObject({ + name: "global-only", + description: "from global mux root", + scope: "global", + }); + expect(getSkill(result.skills, "global-universal")).toMatchObject({ + name: "global-universal", + description: "from global universal root", + scope: "global", + }); + }); + }); + it("returns only the winning descriptor when project skills shadow global skills", async () => { using project = new TestTempDir("test-agent-skill-list-shadow-project"); using muxHome = new TestTempDir("test-agent-skill-list-shadow-home"); @@ -450,6 +511,66 @@ describe("agent_skill_list", () => { }); }); + it("filters hidden skills from local legacy .agents/skills roots unless includeUnadvertised is true", async () => { + using homeDir = new TestTempDir("test-agent-skill-list-local-hidden-legacy-home"); + using project = new TestTempDir("test-agent-skill-list-local-hidden-legacy-project"); + const muxHome = path.join(homeDir.path, ".mux"); + const hiddenProjectSkill = "hidden-project-universal"; + const hiddenGlobalSkill = "hidden-global-universal"; + + await fs.mkdir(muxHome, { recursive: true }); + + await withMuxRoot(muxHome, async () => { + await writeSkill(path.join(project.path, ".agents", "skills"), hiddenProjectSkill, { + advertise: false, + }); + await writeSkill(path.join(homeDir.path, ".agents", "skills"), hiddenGlobalSkill, { + advertise: false, + }); + + const tool = createAgentSkillListTool( + createTestToolConfig(project.path, { + muxScope: { + type: "project", + muxHome, + projectRoot: project.path, + projectStorageAuthority: "host-local", + }, + }) + ); + + const defaultResult = (await tool.execute!( + {}, + mockToolCallOptions + )) as AgentSkillListToolResult; + expect(defaultResult.success).toBe(true); + if (defaultResult.success) { + expect(defaultResult.skills.some((skill) => skill.name === hiddenProjectSkill)).toBe(false); + expect(defaultResult.skills.some((skill) => skill.name === hiddenGlobalSkill)).toBe(false); + } + + const includeAllResult = (await tool.execute!( + { includeUnadvertised: true }, + mockToolCallOptions + )) as AgentSkillListToolResult; + expect(includeAllResult.success).toBe(true); + if (!includeAllResult.success) { + return; + } + + expect(getSkill(includeAllResult.skills, hiddenProjectSkill)).toMatchObject({ + name: hiddenProjectSkill, + scope: "project", + advertise: false, + }); + expect(getSkill(includeAllResult.skills, hiddenGlobalSkill)).toMatchObject({ + name: hiddenGlobalSkill, + scope: "global", + advertise: false, + }); + }); + }); + it("includes unadvertised winning descriptors when includeUnadvertised is true", async () => { using project = new TestTempDir("test-agent-skill-list-include-hidden-project"); using muxHome = new TestTempDir("test-agent-skill-list-include-hidden-home"); diff --git a/src/node/services/tools/agent_skill_list.ts b/src/node/services/tools/agent_skill_list.ts index 39bd47ebdb..5f25161851 100644 --- a/src/node/services/tools/agent_skill_list.ts +++ b/src/node/services/tools/agent_skill_list.ts @@ -188,6 +188,8 @@ export const createAgentSkillListTool: ToolFactory = (config: ToolConfiguration) throw new Error("agent_skill_list requires muxScope"); } + const userHome = path.dirname(muxScope.muxHome); + // Always list global skills; also list project skills when in a project workspace. const roots: Array<{ skillsRoot: string; @@ -199,14 +201,26 @@ export const createAgentSkillListTool: ToolFactory = (config: ToolConfiguration) containmentRoot: muxScope.muxHome, scope: "global", }, + { + skillsRoot: path.join(userHome, ".agents", "skills"), + containmentRoot: userHome, + scope: "global", + }, ]; if (muxScope.type === "project") { - // Project skills listed first so they appear before global ones - roots.unshift({ - skillsRoot: path.join(muxScope.projectRoot, ".mux", "skills"), - containmentRoot: muxScope.projectRoot, - scope: "project", - }); + roots.unshift( + { + // Project skills listed first so they appear before global ones. + skillsRoot: path.join(muxScope.projectRoot, ".mux", "skills"), + containmentRoot: muxScope.projectRoot, + scope: "project", + }, + { + skillsRoot: path.join(muxScope.projectRoot, ".agents", "skills"), + containmentRoot: muxScope.projectRoot, + scope: "project", + } + ); } const skills: AgentSkillDescriptor[] = []; From 7d3612b97d2c6ff6a49ef6b7c938c27be401ed9e Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Tue, 14 Apr 2026 09:20:10 +0000 Subject: [PATCH 4/4] Fix local agent skill list home resolution --- .../services/tools/agent_skill_list.test.ts | 727 ++++++++++-------- src/node/services/tools/agent_skill_list.ts | 3 +- 2 files changed, 390 insertions(+), 340 deletions(-) diff --git a/src/node/services/tools/agent_skill_list.test.ts b/src/node/services/tools/agent_skill_list.test.ts index 46a638669d..e2d4effe75 100644 --- a/src/node/services/tools/agent_skill_list.test.ts +++ b/src/node/services/tools/agent_skill_list.test.ts @@ -1,7 +1,8 @@ import * as fs from "node:fs/promises"; +import os from "node:os"; import * as path from "node:path"; -import { describe, expect, it } from "bun:test"; +import { describe, expect, it, spyOn } from "bun:test"; import type { ToolExecutionOptions } from "ai"; import type { AgentSkillDescriptor } from "@/common/types/agentSkill"; @@ -79,12 +80,17 @@ async function withMuxRoot(muxRoot: string, callback: () => Promise): Prom async function withHomeDir(homeDir: string, callback: () => Promise): Promise { const previousHome = process.env.HOME; const previousUserProfile = process.env.USERPROFILE; + const homedirSpy = spyOn(os, "homedir"); + + homedirSpy.mockReturnValue(homeDir); process.env.HOME = homeDir; process.env.USERPROFILE = homeDir; try { await callback(); } finally { + homedirSpy.mockRestore(); + if (previousHome === undefined) { delete process.env.HOME; } else { @@ -372,60 +378,60 @@ describe("agent_skill_list", () => { it("lists skills from all four local roots in project workspaces", async () => { using homeDir = new TestTempDir("test-agent-skill-list-local-roots-home"); using project = new TestTempDir("test-agent-skill-list-local-roots-project"); - const muxHome = path.join(homeDir.path, ".mux"); - - await fs.mkdir(muxHome, { recursive: true }); + using muxHomeDir = new TestTempDir("test-agent-skill-list-local-roots-mux-home"); - await withMuxRoot(muxHome, async () => { - await writeSkill(path.join(project.path, ".mux", "skills"), "project-only", { - description: "from project mux root", - }); - await writeSkill(path.join(project.path, ".agents", "skills"), "project-universal", { - description: "from project universal root", - }); - await writeGlobalSkill(muxHome, "global-only", { - description: "from global mux root", - }); - await writeSkill(path.join(homeDir.path, ".agents", "skills"), "global-universal", { - description: "from global universal root", - }); + await withHomeDir(homeDir.path, async () => { + await withMuxRoot(muxHomeDir.path, async () => { + await writeSkill(path.join(project.path, ".mux", "skills"), "project-only", { + description: "from project mux root", + }); + await writeSkill(path.join(project.path, ".agents", "skills"), "project-universal", { + description: "from project universal root", + }); + await writeGlobalSkill(muxHomeDir.path, "global-only", { + description: "from global mux root", + }); + await writeSkill(path.join(homeDir.path, ".agents", "skills"), "global-universal", { + description: "from global universal root", + }); - const tool = createAgentSkillListTool( - createTestToolConfig(project.path, { - muxScope: { - type: "project", - muxHome, - projectRoot: project.path, - projectStorageAuthority: "host-local", - }, - }) - ); - const result = (await tool.execute!({}, mockToolCallOptions)) as AgentSkillListToolResult; + const tool = createAgentSkillListTool( + createTestToolConfig(project.path, { + muxScope: { + type: "project", + muxHome: muxHomeDir.path, + projectRoot: project.path, + projectStorageAuthority: "host-local", + }, + }) + ); + const result = (await tool.execute!({}, mockToolCallOptions)) as AgentSkillListToolResult; - expect(result.success).toBe(true); - if (!result.success) { - return; - } + expect(result.success).toBe(true); + if (!result.success) { + return; + } - expect(getSkill(result.skills, "project-only")).toMatchObject({ - name: "project-only", - description: "from project mux root", - scope: "project", - }); - expect(getSkill(result.skills, "project-universal")).toMatchObject({ - name: "project-universal", - description: "from project universal root", - scope: "project", - }); - expect(getSkill(result.skills, "global-only")).toMatchObject({ - name: "global-only", - description: "from global mux root", - scope: "global", - }); - expect(getSkill(result.skills, "global-universal")).toMatchObject({ - name: "global-universal", - description: "from global universal root", - scope: "global", + expect(getSkill(result.skills, "project-only")).toMatchObject({ + name: "project-only", + description: "from project mux root", + scope: "project", + }); + expect(getSkill(result.skills, "project-universal")).toMatchObject({ + name: "project-universal", + description: "from project universal root", + scope: "project", + }); + expect(getSkill(result.skills, "global-only")).toMatchObject({ + name: "global-only", + description: "from global mux root", + scope: "global", + }); + expect(getSkill(result.skills, "global-universal")).toMatchObject({ + name: "global-universal", + description: "from global universal root", + scope: "global", + }); }); }); }); @@ -514,59 +520,63 @@ describe("agent_skill_list", () => { it("filters hidden skills from local legacy .agents/skills roots unless includeUnadvertised is true", async () => { using homeDir = new TestTempDir("test-agent-skill-list-local-hidden-legacy-home"); using project = new TestTempDir("test-agent-skill-list-local-hidden-legacy-project"); - const muxHome = path.join(homeDir.path, ".mux"); + using muxHomeDir = new TestTempDir("test-agent-skill-list-local-hidden-legacy-mux-home"); const hiddenProjectSkill = "hidden-project-universal"; const hiddenGlobalSkill = "hidden-global-universal"; - await fs.mkdir(muxHome, { recursive: true }); - - await withMuxRoot(muxHome, async () => { - await writeSkill(path.join(project.path, ".agents", "skills"), hiddenProjectSkill, { - advertise: false, - }); - await writeSkill(path.join(homeDir.path, ".agents", "skills"), hiddenGlobalSkill, { - advertise: false, - }); + await withHomeDir(homeDir.path, async () => { + await withMuxRoot(muxHomeDir.path, async () => { + await writeSkill(path.join(project.path, ".agents", "skills"), hiddenProjectSkill, { + advertise: false, + }); + await writeSkill(path.join(homeDir.path, ".agents", "skills"), hiddenGlobalSkill, { + advertise: false, + }); - const tool = createAgentSkillListTool( - createTestToolConfig(project.path, { - muxScope: { - type: "project", - muxHome, - projectRoot: project.path, - projectStorageAuthority: "host-local", - }, - }) - ); + const tool = createAgentSkillListTool( + createTestToolConfig(project.path, { + muxScope: { + type: "project", + muxHome: muxHomeDir.path, + projectRoot: project.path, + projectStorageAuthority: "host-local", + }, + }) + ); - const defaultResult = (await tool.execute!( - {}, - mockToolCallOptions - )) as AgentSkillListToolResult; - expect(defaultResult.success).toBe(true); - if (defaultResult.success) { - expect(defaultResult.skills.some((skill) => skill.name === hiddenProjectSkill)).toBe(false); - expect(defaultResult.skills.some((skill) => skill.name === hiddenGlobalSkill)).toBe(false); - } + const defaultResult = (await tool.execute!( + {}, + mockToolCallOptions + )) as AgentSkillListToolResult; + expect(defaultResult.success).toBe(true); + if (defaultResult.success) { + expect(defaultResult.skills.some((skill) => skill.name === hiddenProjectSkill)).toBe( + false + ); + expect(defaultResult.skills.some((skill) => skill.name === hiddenGlobalSkill)).toBe( + false + ); + } - const includeAllResult = (await tool.execute!( - { includeUnadvertised: true }, - mockToolCallOptions - )) as AgentSkillListToolResult; - expect(includeAllResult.success).toBe(true); - if (!includeAllResult.success) { - return; - } + const includeAllResult = (await tool.execute!( + { includeUnadvertised: true }, + mockToolCallOptions + )) as AgentSkillListToolResult; + expect(includeAllResult.success).toBe(true); + if (!includeAllResult.success) { + return; + } - expect(getSkill(includeAllResult.skills, hiddenProjectSkill)).toMatchObject({ - name: hiddenProjectSkill, - scope: "project", - advertise: false, - }); - expect(getSkill(includeAllResult.skills, hiddenGlobalSkill)).toMatchObject({ - name: hiddenGlobalSkill, - scope: "global", - advertise: false, + expect(getSkill(includeAllResult.skills, hiddenProjectSkill)).toMatchObject({ + name: hiddenProjectSkill, + scope: "project", + advertise: false, + }); + expect(getSkill(includeAllResult.skills, hiddenGlobalSkill)).toMatchObject({ + name: hiddenGlobalSkill, + scope: "global", + advertise: false, + }); }); }); }); @@ -657,64 +667,74 @@ describe("agent_skill_list", () => { it("operates on global skills root when scope is global", async () => { using tempDir = new TestTempDir("test-agent-skill-list-global"); - const workspaceSessionDir = await createWorkspaceSessionDir(tempDir.path, GLOBAL_WORKSPACE_ID); + await withHomeDir(tempDir.path, async () => { + const workspaceSessionDir = await createWorkspaceSessionDir( + tempDir.path, + GLOBAL_WORKSPACE_ID + ); - await writeGlobalSkill(tempDir.path, "alpha-skill"); - await writeGlobalSkill(tempDir.path, "zeta-skill"); + await writeGlobalSkill(tempDir.path, "alpha-skill"); + await writeGlobalSkill(tempDir.path, "zeta-skill"); - const config = createTestToolConfig(tempDir.path, { - workspaceId: GLOBAL_WORKSPACE_ID, - sessionsDir: workspaceSessionDir, - muxScope: { - type: "global", - muxHome: tempDir.path, - }, - }); + const config = createTestToolConfig(tempDir.path, { + workspaceId: GLOBAL_WORKSPACE_ID, + sessionsDir: workspaceSessionDir, + muxScope: { + type: "global", + muxHome: tempDir.path, + }, + }); - const tool = createAgentSkillListTool(config); - const result = (await tool.execute!({}, mockToolCallOptions)) as AgentSkillListToolResult; + const tool = createAgentSkillListTool(config); + const result = (await tool.execute!({}, mockToolCallOptions)) as AgentSkillListToolResult; - expect(result.success).toBe(true); - if (result.success) { - expect(result.skills.map((skill) => skill.name)).toEqual(["alpha-skill", "zeta-skill"]); - expect(result.skills.every((skill) => skill.scope === "global")).toBe(true); - } + expect(result.success).toBe(true); + if (result.success) { + expect(result.skills.map((skill) => skill.name)).toEqual(["alpha-skill", "zeta-skill"]); + expect(result.skills.every((skill) => skill.scope === "global")).toBe(true); + } + }); }); it("operates on project skills root when scope is project", async () => { using tempDir = new TestTempDir("test-agent-skill-list-project"); - const workspaceSessionDir = await createWorkspaceSessionDir(tempDir.path, GLOBAL_WORKSPACE_ID); + await withHomeDir(tempDir.path, async () => { + const workspaceSessionDir = await createWorkspaceSessionDir( + tempDir.path, + GLOBAL_WORKSPACE_ID + ); - const projectRoot = path.join(tempDir.path, "my-project"); - await fs.mkdir(path.join(projectRoot, ".mux", "skills"), { recursive: true }); + const projectRoot = path.join(tempDir.path, "my-project"); + await fs.mkdir(path.join(projectRoot, ".mux", "skills"), { recursive: true }); - await writeGlobalSkill(tempDir.path, "global-skill"); - await writeGlobalSkill(path.join(projectRoot, ".mux"), "project-skill"); + await writeGlobalSkill(tempDir.path, "global-skill"); + await writeGlobalSkill(path.join(projectRoot, ".mux"), "project-skill"); - const projectScope: MuxToolScope = { - type: "project", - muxHome: tempDir.path, - projectRoot, - projectStorageAuthority: "host-local", - }; + const projectScope: MuxToolScope = { + type: "project", + muxHome: tempDir.path, + projectRoot, + projectStorageAuthority: "host-local", + }; - const config = createTestToolConfig(tempDir.path, { - workspaceId: GLOBAL_WORKSPACE_ID, - sessionsDir: workspaceSessionDir, - muxScope: projectScope, - }); + const config = createTestToolConfig(tempDir.path, { + workspaceId: GLOBAL_WORKSPACE_ID, + sessionsDir: workspaceSessionDir, + muxScope: projectScope, + }); - const tool = createAgentSkillListTool(config); - const result = (await tool.execute!({}, mockToolCallOptions)) as AgentSkillListToolResult; + const tool = createAgentSkillListTool(config); + const result = (await tool.execute!({}, mockToolCallOptions)) as AgentSkillListToolResult; - expect(result.success).toBe(true); - if (result.success) { - // Project scope lists both project and global skills, each tagged with scope - expect(result.skills.map((skill) => skill.name)).toEqual(["global-skill", "project-skill"]); - expect(result.skills.find((s) => s.name === "project-skill")?.scope).toBe("project"); - expect(result.skills.find((s) => s.name === "global-skill")?.scope).toBe("global"); - } + expect(result.success).toBe(true); + if (result.success) { + // Project scope lists both project and global skills, each tagged with scope + expect(result.skills.map((skill) => skill.name)).toEqual(["global-skill", "project-skill"]); + expect(result.skills.find((s) => s.name === "project-skill")?.scope).toBe("project"); + expect(result.skills.find((s) => s.name === "global-skill")?.scope).toBe("global"); + } + }); }); describe("split-root (project-runtime)", () => { it("routes through project-runtime when runtime is non-local", async () => { @@ -1138,275 +1158,304 @@ describe("agent_skill_list", () => { it("filters unadvertised skills unless includeUnadvertised is true", async () => { using tempDir = new TestTempDir("test-agent-skill-list-advertise"); - const workspaceSessionDir = await createWorkspaceSessionDir(tempDir.path, GLOBAL_WORKSPACE_ID); + await withHomeDir(tempDir.path, async () => { + const workspaceSessionDir = await createWorkspaceSessionDir( + tempDir.path, + GLOBAL_WORKSPACE_ID + ); - await writeGlobalSkill(tempDir.path, "advertised-skill"); - await writeGlobalSkill(tempDir.path, "hidden-skill", { advertise: false }); + await writeGlobalSkill(tempDir.path, "advertised-skill"); + await writeGlobalSkill(tempDir.path, "hidden-skill", { advertise: false }); - const config = createTestToolConfig(tempDir.path, { - workspaceId: GLOBAL_WORKSPACE_ID, - sessionsDir: workspaceSessionDir, - muxScope: { - type: "global", - muxHome: tempDir.path, - }, - }); + const config = createTestToolConfig(tempDir.path, { + workspaceId: GLOBAL_WORKSPACE_ID, + sessionsDir: workspaceSessionDir, + muxScope: { + type: "global", + muxHome: tempDir.path, + }, + }); - const tool = createAgentSkillListTool(config); + const tool = createAgentSkillListTool(config); - const defaultResult = (await tool.execute!( - {}, - mockToolCallOptions - )) as AgentSkillListToolResult; - expect(defaultResult.success).toBe(true); - if (defaultResult.success) { - expect(defaultResult.skills.map((skill) => skill.name)).toEqual(["advertised-skill"]); - } + const defaultResult = (await tool.execute!( + {}, + mockToolCallOptions + )) as AgentSkillListToolResult; + expect(defaultResult.success).toBe(true); + if (defaultResult.success) { + expect(defaultResult.skills.map((skill) => skill.name)).toEqual(["advertised-skill"]); + } - const includeAllResult = (await tool.execute!( - { includeUnadvertised: true }, - mockToolCallOptions - )) as AgentSkillListToolResult; - expect(includeAllResult.success).toBe(true); - if (includeAllResult.success) { - expect(includeAllResult.skills.map((skill) => skill.name)).toEqual([ - "advertised-skill", - "hidden-skill", - ]); - } + const includeAllResult = (await tool.execute!( + { includeUnadvertised: true }, + mockToolCallOptions + )) as AgentSkillListToolResult; + expect(includeAllResult.success).toBe(true); + if (includeAllResult.success) { + expect(includeAllResult.skills.map((skill) => skill.name)).toEqual([ + "advertised-skill", + "hidden-skill", + ]); + } + }); }); it("skips symlinked project skill directories inside contained skills root", async () => { using tempDir = new TestTempDir("test-agent-skill-list-project-entry-symlink"); - const workspaceSessionDir = await createWorkspaceSessionDir(tempDir.path, GLOBAL_WORKSPACE_ID); + await withHomeDir(tempDir.path, async () => { + const workspaceSessionDir = await createWorkspaceSessionDir( + tempDir.path, + GLOBAL_WORKSPACE_ID + ); - const projectRoot = path.join(tempDir.path, "project"); - const skillsDir = path.join(projectRoot, ".mux", "skills"); - await fs.mkdir(skillsDir, { recursive: true }); + const projectRoot = path.join(tempDir.path, "project"); + const skillsDir = path.join(projectRoot, ".mux", "skills"); + await fs.mkdir(skillsDir, { recursive: true }); - // Legitimate project skill directory. - await writeGlobalSkill(path.join(projectRoot, ".mux"), "real-skill"); + // Legitimate project skill directory. + await writeGlobalSkill(path.join(projectRoot, ".mux"), "real-skill"); - // External skill directory linked into project skills root. - const externalSkillDir = path.join(tempDir.path, "external", "sneaky-skill"); - await fs.mkdir(externalSkillDir, { recursive: true }); - await fs.writeFile( - path.join(externalSkillDir, "SKILL.md"), - "---\nname: sneaky-skill\ndescription: should not appear\n---\nBody\n", - "utf-8" - ); - await fs.symlink(externalSkillDir, path.join(skillsDir, "sneaky-skill")); + // External skill directory linked into project skills root. + const externalSkillDir = path.join(tempDir.path, "external", "sneaky-skill"); + await fs.mkdir(externalSkillDir, { recursive: true }); + await fs.writeFile( + path.join(externalSkillDir, "SKILL.md"), + "---\nname: sneaky-skill\ndescription: should not appear\n---\nBody\n", + "utf-8" + ); + await fs.symlink(externalSkillDir, path.join(skillsDir, "sneaky-skill")); - // Also create a real global skill. - await writeGlobalSkill(tempDir.path, "global-skill"); + // Also create a real global skill. + await writeGlobalSkill(tempDir.path, "global-skill"); - const projectScope: MuxToolScope = { - type: "project", - muxHome: tempDir.path, - projectRoot, - projectStorageAuthority: "host-local", - }; + const projectScope: MuxToolScope = { + type: "project", + muxHome: tempDir.path, + projectRoot, + projectStorageAuthority: "host-local", + }; - const config = createTestToolConfig(tempDir.path, { - workspaceId: GLOBAL_WORKSPACE_ID, - sessionsDir: workspaceSessionDir, - muxScope: projectScope, - }); + const config = createTestToolConfig(tempDir.path, { + workspaceId: GLOBAL_WORKSPACE_ID, + sessionsDir: workspaceSessionDir, + muxScope: projectScope, + }); - const tool = createAgentSkillListTool(config); - const result = (await tool.execute!({}, mockToolCallOptions)) as AgentSkillListToolResult; + const tool = createAgentSkillListTool(config); + const result = (await tool.execute!({}, mockToolCallOptions)) as AgentSkillListToolResult; - expect(result.success).toBe(true); - if (result.success) { - // Symlinked entry should be skipped. - expect(result.skills.map((s) => s.name)).toEqual(["global-skill", "real-skill"]); - expect(result.skills.find((s) => s.name === "real-skill")?.scope).toBe("project"); - expect(result.skills.find((s) => s.name === "sneaky-skill")).toBeUndefined(); - } + expect(result.success).toBe(true); + if (result.success) { + // Symlinked entry should be skipped. + expect(result.skills.map((s) => s.name)).toEqual(["global-skill", "real-skill"]); + expect(result.skills.find((s) => s.name === "real-skill")?.scope).toBe("project"); + expect(result.skills.find((s) => s.name === "sneaky-skill")).toBeUndefined(); + } + }); }); it("skips project skill when SKILL.md symlink target escapes project root", async () => { using tempDir = new TestTempDir("test-agent-skill-list-skillmd-symlink-escape"); - const workspaceSessionDir = await createWorkspaceSessionDir(tempDir.path, GLOBAL_WORKSPACE_ID); + await withHomeDir(tempDir.path, async () => { + const workspaceSessionDir = await createWorkspaceSessionDir( + tempDir.path, + GLOBAL_WORKSPACE_ID + ); - const projectRoot = path.join(tempDir.path, "project"); - const skillsDir = path.join(projectRoot, ".mux", "skills"); + const projectRoot = path.join(tempDir.path, "project"); + const skillsDir = path.join(projectRoot, ".mux", "skills"); - // Create a legitimate project skill. - await writeGlobalSkill(path.join(projectRoot, ".mux"), "legit-skill"); + // Create a legitimate project skill. + await writeGlobalSkill(path.join(projectRoot, ".mux"), "legit-skill"); - // Create a skill directory with SKILL.md symlinked to an external file. - const leakySkillDir = path.join(skillsDir, "leaky-skill"); - await fs.mkdir(leakySkillDir, { recursive: true }); + // Create a skill directory with SKILL.md symlinked to an external file. + const leakySkillDir = path.join(skillsDir, "leaky-skill"); + await fs.mkdir(leakySkillDir, { recursive: true }); - const externalDir = path.join(tempDir.path, "external"); - const externalFile = path.join(externalDir, "secret.md"); - await fs.mkdir(externalDir, { recursive: true }); - await fs.writeFile( - externalFile, - "---\nname: leaky-skill\ndescription: should not be read\n---\nSecret body\n", - "utf-8" - ); - await fs.symlink(externalFile, path.join(leakySkillDir, "SKILL.md")); + const externalDir = path.join(tempDir.path, "external"); + const externalFile = path.join(externalDir, "secret.md"); + await fs.mkdir(externalDir, { recursive: true }); + await fs.writeFile( + externalFile, + "---\nname: leaky-skill\ndescription: should not be read\n---\nSecret body\n", + "utf-8" + ); + await fs.symlink(externalFile, path.join(leakySkillDir, "SKILL.md")); - // Also create a global skill. - await writeGlobalSkill(tempDir.path, "global-skill"); + // Also create a global skill. + await writeGlobalSkill(tempDir.path, "global-skill"); - const projectScope: MuxToolScope = { - type: "project", - muxHome: tempDir.path, - projectRoot, - projectStorageAuthority: "host-local", - }; + const projectScope: MuxToolScope = { + type: "project", + muxHome: tempDir.path, + projectRoot, + projectStorageAuthority: "host-local", + }; - const config = createTestToolConfig(tempDir.path, { - workspaceId: GLOBAL_WORKSPACE_ID, - sessionsDir: workspaceSessionDir, - muxScope: projectScope, - }); + const config = createTestToolConfig(tempDir.path, { + workspaceId: GLOBAL_WORKSPACE_ID, + sessionsDir: workspaceSessionDir, + muxScope: projectScope, + }); - const tool = createAgentSkillListTool(config); - const result = (await tool.execute!({}, mockToolCallOptions)) as AgentSkillListToolResult; + const tool = createAgentSkillListTool(config); + const result = (await tool.execute!({}, mockToolCallOptions)) as AgentSkillListToolResult; - expect(result.success).toBe(true); - if (result.success) { - expect(result.skills.map((s) => s.name)).toEqual(["global-skill", "legit-skill"]); - expect(result.skills.find((s) => s.name === "leaky-skill")).toBeUndefined(); - } + expect(result.success).toBe(true); + if (result.success) { + expect(result.skills.map((s) => s.name)).toEqual(["global-skill", "legit-skill"]); + expect(result.skills.find((s) => s.name === "leaky-skill")).toBeUndefined(); + } + }); }); it("skips skill with oversized SKILL.md", async () => { using tempDir = new TestTempDir("test-agent-skill-list-oversized-skillmd"); - const workspaceSessionDir = await createWorkspaceSessionDir(tempDir.path, GLOBAL_WORKSPACE_ID); + await withHomeDir(tempDir.path, async () => { + const workspaceSessionDir = await createWorkspaceSessionDir( + tempDir.path, + GLOBAL_WORKSPACE_ID + ); - await writeGlobalSkill(tempDir.path, "normal-skill"); + await writeGlobalSkill(tempDir.path, "normal-skill"); - const oversizedSkillDir = path.join(tempDir.path, "skills", "big-skill"); - await fs.mkdir(oversizedSkillDir, { recursive: true }); - const oversizedContent = - "---\nname: big-skill\ndescription: too large\n---\n" + "x".repeat(MAX_FILE_SIZE + 1); - await fs.writeFile(path.join(oversizedSkillDir, "SKILL.md"), oversizedContent, "utf-8"); + const oversizedSkillDir = path.join(tempDir.path, "skills", "big-skill"); + await fs.mkdir(oversizedSkillDir, { recursive: true }); + const oversizedContent = + "---\nname: big-skill\ndescription: too large\n---\n" + "x".repeat(MAX_FILE_SIZE + 1); + await fs.writeFile(path.join(oversizedSkillDir, "SKILL.md"), oversizedContent, "utf-8"); - const config = createTestToolConfig(tempDir.path, { - workspaceId: GLOBAL_WORKSPACE_ID, - sessionsDir: workspaceSessionDir, - muxScope: { - type: "global", - muxHome: tempDir.path, - }, - }); + const config = createTestToolConfig(tempDir.path, { + workspaceId: GLOBAL_WORKSPACE_ID, + sessionsDir: workspaceSessionDir, + muxScope: { + type: "global", + muxHome: tempDir.path, + }, + }); - const tool = createAgentSkillListTool(config); - const result = (await tool.execute!({}, mockToolCallOptions)) as AgentSkillListToolResult; + const tool = createAgentSkillListTool(config); + const result = (await tool.execute!({}, mockToolCallOptions)) as AgentSkillListToolResult; - expect(result.success).toBe(true); - if (result.success) { - expect(result.skills.map((s) => s.name)).toEqual(["normal-skill"]); - expect(result.skills.find((s) => s.name === "big-skill")).toBeUndefined(); - } + expect(result.success).toBe(true); + if (result.success) { + expect(result.skills.map((s) => s.name)).toEqual(["normal-skill"]); + expect(result.skills.find((s) => s.name === "big-skill")).toBeUndefined(); + } + }); }); it("continues listing global skills when project skills root is not a directory", async () => { using project = new TestTempDir("test-agent-skill-list-project-root-not-directory"); using muxHome = new TestTempDir("test-agent-skill-list-global-root-valid"); - await fs.mkdir(path.join(project.path, ".mux"), { recursive: true }); - await fs.writeFile(path.join(project.path, ".mux", "skills"), "not a directory", "utf-8"); - await writeGlobalSkill(muxHome.path, "global-skill", { - description: "from global", - }); + await withHomeDir(muxHome.path, async () => { + await fs.mkdir(path.join(project.path, ".mux"), { recursive: true }); + await fs.writeFile(path.join(project.path, ".mux", "skills"), "not a directory", "utf-8"); + await writeGlobalSkill(muxHome.path, "global-skill", { + description: "from global", + }); - const tool = createAgentSkillListTool( - createTestToolConfig(project.path, { - muxScope: { - type: "project", - muxHome: muxHome.path, - projectRoot: project.path, - projectStorageAuthority: "host-local", - }, - }) - ); - const result = (await tool.execute!({}, mockToolCallOptions)) as AgentSkillListToolResult; + const tool = createAgentSkillListTool( + createTestToolConfig(project.path, { + muxScope: { + type: "project", + muxHome: muxHome.path, + projectRoot: project.path, + projectStorageAuthority: "host-local", + }, + }) + ); + const result = (await tool.execute!({}, mockToolCallOptions)) as AgentSkillListToolResult; - expect(result.success).toBe(true); - if (result.success) { - expect(result.skills.map((s) => s.name)).toEqual(["global-skill"]); - } + expect(result.success).toBe(true); + if (result.success) { + expect(result.skills.map((s) => s.name)).toEqual(["global-skill"]); + } + }); }); it("returns no skills when both project and global roots are not directories", async () => { using project = new TestTempDir("test-agent-skill-list-both-roots-not-directories-project"); using muxHome = new TestTempDir("test-agent-skill-list-both-roots-not-directories-home"); - await fs.mkdir(path.join(project.path, ".mux"), { recursive: true }); - await fs.writeFile(path.join(project.path, ".mux", "skills"), "not a directory", "utf-8"); - await fs.writeFile(path.join(muxHome.path, "skills"), "not a directory", "utf-8"); + await withHomeDir(muxHome.path, async () => { + await fs.mkdir(path.join(project.path, ".mux"), { recursive: true }); + await fs.writeFile(path.join(project.path, ".mux", "skills"), "not a directory", "utf-8"); + await fs.writeFile(path.join(muxHome.path, "skills"), "not a directory", "utf-8"); - const tool = createAgentSkillListTool( - createTestToolConfig(project.path, { - muxScope: { - type: "project", - muxHome: muxHome.path, - projectRoot: project.path, - projectStorageAuthority: "host-local", - }, - }) - ); - const result = (await tool.execute!({}, mockToolCallOptions)) as AgentSkillListToolResult; + const tool = createAgentSkillListTool( + createTestToolConfig(project.path, { + muxScope: { + type: "project", + muxHome: muxHome.path, + projectRoot: project.path, + projectStorageAuthority: "host-local", + }, + }) + ); + const result = (await tool.execute!({}, mockToolCallOptions)) as AgentSkillListToolResult; - expect(result.success).toBe(true); - if (result.success) { - expect(result.skills).toEqual([]); - } + expect(result.success).toBe(true); + if (result.success) { + expect(result.skills).toEqual([]); + } + }); }); it("skips project skills when .mux is a symlink to external directory", async () => { using tempDir = new TestTempDir("test-agent-skill-list-project-mux-symlink"); - const workspaceSessionDir = await createWorkspaceSessionDir(tempDir.path, GLOBAL_WORKSPACE_ID); + await withHomeDir(tempDir.path, async () => { + const workspaceSessionDir = await createWorkspaceSessionDir( + tempDir.path, + GLOBAL_WORKSPACE_ID + ); - const projectRoot = path.join(tempDir.path, "project"); - await fs.mkdir(projectRoot, { recursive: true }); + const projectRoot = path.join(tempDir.path, "project"); + await fs.mkdir(projectRoot, { recursive: true }); - // Create external directory with skill content - const externalDir = path.join(tempDir.path, "external"); - await fs.mkdir(path.join(externalDir, "skills", "external-skill"), { recursive: true }); - await fs.writeFile( - path.join(externalDir, "skills", "external-skill", "SKILL.md"), - "---\nname: external-skill\ndescription: should not appear\n---\nBody\n", - "utf-8" - ); + // Create external directory with skill content + const externalDir = path.join(tempDir.path, "external"); + await fs.mkdir(path.join(externalDir, "skills", "external-skill"), { recursive: true }); + await fs.writeFile( + path.join(externalDir, "skills", "external-skill", "SKILL.md"), + "---\nname: external-skill\ndescription: should not appear\n---\nBody\n", + "utf-8" + ); - // Symlink .mux to external - await fs.symlink(externalDir, path.join(projectRoot, ".mux")); + // Symlink .mux to external + await fs.symlink(externalDir, path.join(projectRoot, ".mux")); - // Also create a real global skill - await writeGlobalSkill(tempDir.path, "global-skill"); + // Also create a real global skill + await writeGlobalSkill(tempDir.path, "global-skill"); - const projectScope: MuxToolScope = { - type: "project", - muxHome: tempDir.path, - projectRoot, - projectStorageAuthority: "host-local", - }; + const projectScope: MuxToolScope = { + type: "project", + muxHome: tempDir.path, + projectRoot, + projectStorageAuthority: "host-local", + }; - const config = createTestToolConfig(tempDir.path, { - workspaceId: GLOBAL_WORKSPACE_ID, - sessionsDir: workspaceSessionDir, - muxScope: projectScope, - }); + const config = createTestToolConfig(tempDir.path, { + workspaceId: GLOBAL_WORKSPACE_ID, + sessionsDir: workspaceSessionDir, + muxScope: projectScope, + }); - const tool = createAgentSkillListTool(config); - const result = (await tool.execute!({}, mockToolCallOptions)) as AgentSkillListToolResult; + const tool = createAgentSkillListTool(config); + const result = (await tool.execute!({}, mockToolCallOptions)) as AgentSkillListToolResult; - expect(result.success).toBe(true); - if (result.success) { - // External skill should NOT appear; only real global skill should be listed - expect(result.skills.map((s) => s.name)).toEqual(["global-skill"]); - expect(result.skills.every((s) => s.scope === "global")).toBe(true); - } + expect(result.success).toBe(true); + if (result.success) { + // External skill should NOT appear; only real global skill should be listed + expect(result.skills.map((s) => s.name)).toEqual(["global-skill"]); + expect(result.skills.every((s) => s.scope === "global")).toBe(true); + } + }); }); }); diff --git a/src/node/services/tools/agent_skill_list.ts b/src/node/services/tools/agent_skill_list.ts index 5f25161851..42f11c65ea 100644 --- a/src/node/services/tools/agent_skill_list.ts +++ b/src/node/services/tools/agent_skill_list.ts @@ -1,4 +1,5 @@ import * as fsPromises from "fs/promises"; +import os from "node:os"; import * as path from "path"; import { tool } from "ai"; @@ -188,7 +189,7 @@ export const createAgentSkillListTool: ToolFactory = (config: ToolConfiguration) throw new Error("agent_skill_list requires muxScope"); } - const userHome = path.dirname(muxScope.muxHome); + const userHome = os.homedir(); // Always list global skills; also list project skills when in a project workspace. const roots: Array<{