@@ -138,6 +138,75 @@ describe("skill workshop proposals", () => {
138138 expect ( ( await inspectSkillProposal ( proposal . record . id ) ) ?. record . status ) . toBe ( "applied" ) ;
139139 } ) ;
140140
141+ it . runIf ( process . platform !== "win32" ) (
142+ "applies updates through trusted workspace skills symlink targets" ,
143+ async ( ) => {
144+ const workspaceDir = await makeWorkspace ( ) ;
145+ const targetSkillsDir = await tempDirs . make ( "openclaw-skill-workshop-target-skills-" ) ;
146+ await fs . symlink ( targetSkillsDir , path . join ( workspaceDir , "skills" ) , "dir" ) ;
147+ const skillDir = path . join ( targetSkillsDir , "shared-skill" ) ;
148+ await writeSkill ( {
149+ dir : skillDir ,
150+ name : "shared-skill" ,
151+ description : "Shared skill target" ,
152+ body : "# Shared Skill\n\nOld body.\n" ,
153+ } ) ;
154+ const config = { skills : { load : { allowSymlinkTargets : [ targetSkillsDir ] } } } ;
155+ const proposal = await proposeUpdateSkill ( {
156+ workspaceDir,
157+ config,
158+ skillName : "shared-skill" ,
159+ content : "# Shared Skill\n\nNew body.\n" ,
160+ } ) ;
161+
162+ const applied = await applySkillProposal ( {
163+ workspaceDir,
164+ config,
165+ proposalId : proposal . record . id ,
166+ } ) ;
167+
168+ expect ( applied . targetSkillFile ) . toBe (
169+ path . join ( workspaceDir , "skills" , "shared-skill" , "SKILL.md" ) ,
170+ ) ;
171+ await expect ( fs . readFile ( path . join ( skillDir , "SKILL.md" ) , "utf8" ) ) . resolves . toContain (
172+ "New body." ,
173+ ) ;
174+ } ,
175+ ) ;
176+
177+ it . runIf ( process . platform !== "win32" ) (
178+ "blocks untrusted workspace skills symlink targets before support files are written" ,
179+ async ( ) => {
180+ const workspaceDir = await makeWorkspace ( ) ;
181+ const targetSkillsDir = await tempDirs . make ( "openclaw-skill-workshop-untrusted-skills-" ) ;
182+ await fs . symlink ( targetSkillsDir , path . join ( workspaceDir , "skills" ) , "dir" ) ;
183+ const proposal = await proposeCreateSkill ( {
184+ workspaceDir,
185+ name : "Untrusted Symlink Skill" ,
186+ description : "Must not write through an untrusted symlink" ,
187+ content : "# Untrusted\n\nDo not write.\n" ,
188+ supportFiles : [
189+ {
190+ path : "references/details.md" ,
191+ content : "This support file must not be written.\n" ,
192+ } ,
193+ ] ,
194+ } ) ;
195+
196+ await expect (
197+ applySkillProposal ( { workspaceDir, proposalId : proposal . record . id } ) ,
198+ ) . rejects . toThrow ( "untrusted symlink target" ) ;
199+ await expect (
200+ fs . access ( path . join ( targetSkillsDir , "untrusted-symlink-skill" , "SKILL.md" ) ) ,
201+ ) . rejects . toThrow ( ) ;
202+ await expect (
203+ fs . access (
204+ path . join ( targetSkillsDir , "untrusted-symlink-skill" , "references" , "details.md" ) ,
205+ ) ,
206+ ) . rejects . toThrow ( ) ;
207+ } ,
208+ ) ;
209+
141210 it ( "preserves non-proposal frontmatter when proposals become active skills" , async ( ) => {
142211 const workspaceDir = await makeWorkspace ( ) ;
143212 const created = await proposeCreateSkill ( {
0 commit comments