| 80 | * Tool that creates/updates files in the contextual skills directory. |
| 81 | */ |
| 82 | export const createAgentSkillWriteTool: ToolFactory = (config: ToolConfiguration) => { |
| 83 | return tool({ |
| 84 | description: TOOL_DEFINITIONS.agent_skill_write.description, |
| 85 | inputSchema: TOOL_DEFINITIONS.agent_skill_write.schema, |
| 86 | execute: async ({ |
| 87 | name, |
| 88 | filePath, |
| 89 | content, |
| 90 | }: AgentSkillWriteToolArgs): Promise<AgentSkillWriteToolResult> => { |
| 91 | const parsedName = SkillNameSchema.safeParse(name); |
| 92 | if (!parsedName.success) { |
| 93 | return { |
| 94 | success: false, |
| 95 | error: parsedName.error.message, |
| 96 | }; |
| 97 | } |
| 98 | |
| 99 | try { |
| 100 | const relativeFilePath = filePath ?? SKILL_FILENAME; |
| 101 | const skillCtx = resolveSkillStorageContext({ |
| 102 | runtime: config.runtime, |
| 103 | workspacePath: config.cwd, |
| 104 | muxScope: config.muxScope ?? null, |
| 105 | }); |
| 106 | |
| 107 | if (skillCtx.kind === "project-runtime") { |
| 108 | const skillsRoot = config.runtime.normalizePath(".mux/skills", skillCtx.workspacePath); |
| 109 | const skillDir = config.runtime.normalizePath(parsedName.data, skillsRoot); |
| 110 | |
| 111 | let resolvedTarget: ReturnType<typeof resolveSkillFilePathForRuntime>; |
| 112 | try { |
| 113 | resolvedTarget = resolveSkillFilePathForRuntime( |
| 114 | config.runtime, |
| 115 | skillDir, |
| 116 | relativeFilePath |
| 117 | ); |
| 118 | } catch (error) { |
| 119 | return { |
| 120 | success: false, |
| 121 | error: getErrorMessage(error), |
| 122 | }; |
| 123 | } |
| 124 | |
| 125 | // Canonicalize any casing variant of SKILL.md to the canonical path. |
| 126 | // Validate the exact path we will write so casing aliases cannot bypass leaf-symlink checks. |
| 127 | if (isSkillMarkdownRootFile(resolvedTarget.normalizedRelativePath)) { |
| 128 | resolvedTarget = { |
| 129 | ...resolvedTarget, |
| 130 | resolvedPath: config.runtime.normalizePath(SKILL_FILENAME, skillDir), |
| 131 | normalizedRelativePath: SKILL_FILENAME, |
| 132 | }; |
| 133 | } |
| 134 | |
| 135 | const targetContainment = await inspectContainmentOnRuntime( |
| 136 | config.runtime, |
| 137 | skillDir, |
| 138 | resolvedTarget.resolvedPath |
| 139 | ); |