@@ -27,6 +27,7 @@ const (
2727 skillsRepoName = "databricks-agent-skills"
2828 skillsRepoPath = "skills"
2929 defaultSkillsRepoRef = "v0.1.4"
30+ httpTimeout = 30 * time .Second
3031)
3132
3233// managedSkillMarker is written at the root of each materialized (non-symlink)
@@ -94,14 +95,17 @@ func skillFileRawURL(ref, skillName, filePath string) string {
9495}
9596
9697func fetchSkillFile (ctx context.Context , ref , skillName , filePath string ) ([]byte , error ) {
98+ if rootPath , ok := strings .CutPrefix (filePath , sharedFilePrefix ); ok && rootPath == "" {
99+ return nil , fmt .Errorf ("invalid manifest file entry: empty path after %q" , sharedFilePrefix )
100+ }
97101 url := skillFileRawURL (ref , skillName , filePath )
98102
99103 req , err := http .NewRequestWithContext (ctx , http .MethodGet , url , nil )
100104 if err != nil {
101105 return nil , fmt .Errorf ("failed to create request: %w" , err )
102106 }
103107
104- client := & http.Client {Timeout : 30 * time . Second }
108+ client := & http.Client {Timeout : httpTimeout }
105109 resp , err := client .Do (req )
106110 if err != nil {
107111 return nil , fmt .Errorf ("failed to fetch %s: %w" , filePath , err )
@@ -147,6 +151,8 @@ func InstallSkillsForAgents(ctx context.Context, src ManifestSource, targetAgent
147151 if len (targetAgents ) == 0 {
148152 return fmt .Errorf ("no agents support project-scoped skills. The following detected agents are global-only: %s" , strings .Join (incompatible , ", " ))
149153 }
154+ } else if len (targetAgents ) == 0 {
155+ return errors .New ("no target agents" )
150156 }
151157
152158 // Load existing state for idempotency checks.
@@ -419,6 +425,7 @@ func installSkillForAgents(ctx context.Context, skillName string, files []string
419425 // Symlinks point at the canonical tree (full manifest layout for every agent).
420426 symlinkEligible := params .scope == ScopeProject || len (detectedAgents ) > 1
421427
428+ installedForAgent := 0
422429 for _ , agent := range detectedAgents {
423430 agentSkillDir , err := agentSkillsDirForScope (ctx , agent , params .scope , params .cwd )
424431 if err != nil {
@@ -449,15 +456,22 @@ func installSkillForAgents(ctx context.Context, skillName string, files []string
449456 log .Warnf (ctx , "Failed to install for %s: %v" , agent .DisplayName , err )
450457 continue
451458 }
459+ log .Debugf (ctx , "Installed %q for %s (materialized copy)" , skillName , agent .DisplayName )
460+ } else {
461+ log .Debugf (ctx , "Installed %q for %s (symlinked)" , skillName , agent .DisplayName )
452462 }
453- log .Debugf (ctx , "Installed %q for %s (symlinked)" , skillName , agent .DisplayName )
454463 } else {
455464 if err := copySkillMaterialized (canonicalDir , destDir ); err != nil {
456465 log .Warnf (ctx , "Failed to install for %s: %v" , agent .DisplayName , err )
457466 continue
458467 }
459468 log .Debugf (ctx , "Installed %q for %s" , skillName , agent .DisplayName )
460469 }
470+ installedForAgent ++
471+ }
472+
473+ if len (detectedAgents ) > 0 && installedForAgent == 0 {
474+ return fmt .Errorf ("failed to install %q for any agent" , skillName )
461475 }
462476
463477 return nil
@@ -486,19 +500,16 @@ func backupThirdPartySkill(ctx context.Context, destDir, canonicalDir, skillName
486500 if fi .Mode ()& os .ModeSymlink != 0 {
487501 target , err := os .Readlink (destDir )
488502 if err == nil {
489- absTarget := target
490- if ! filepath .IsAbs (target ) {
491- absTarget = filepath .Clean (filepath .Join (filepath .Dir (destDir ), target ))
492- }
493- if absTarget == canonicalDir {
503+ if pathWithinOrEqual (resolveSymlinkTarget (destDir , target ), canonicalDir ) {
494504 return nil
495505 }
496506 }
497507 }
498508
499509 // Prior materialized install from this CLI; copySkillMaterialized will replace it.
500510 if fi .IsDir () {
501- if _ , err := os .Stat (filepath .Join (destDir , managedSkillMarker )); err == nil {
511+ mi , err := os .Lstat (filepath .Join (destDir , managedSkillMarker ))
512+ if err == nil && mi .Mode ().IsRegular () {
502513 return nil
503514 }
504515 }
@@ -527,21 +538,20 @@ func installSkillToDir(ctx context.Context, ref, skillName, destDir string, file
527538 return fmt .Errorf ("failed to create directory: %w" , err )
528539 }
529540
530- for _ , file := range files {
531- content , err := fetchFileFn (ctx , ref , skillName , file )
532- if err != nil {
533- return err
534- }
535-
536- // Strip the @root: prefix so shared assets land at a local path
537- // (e.g. "@root:assets/databricks.svg" → "assets/databricks.svg").
538- if rootPath , ok := strings .CutPrefix (file , sharedFilePrefix ); ok {
541+ for _ , manifestFile := range files {
542+ relForDisk := manifestFile
543+ if rootPath , ok := strings .CutPrefix (manifestFile , sharedFilePrefix ); ok {
539544 if rootPath == "" {
540545 return fmt .Errorf ("invalid manifest file entry: empty path after %q" , sharedFilePrefix )
541546 }
542- file = rootPath
547+ relForDisk = rootPath
548+ }
549+ destPath , err := safeSkillDestPath (destDir , relForDisk )
550+ if err != nil {
551+ return err
543552 }
544- destPath , err := safeSkillDestPath (destDir , file )
553+
554+ content , err := fetchFileFn (ctx , ref , skillName , manifestFile )
545555 if err != nil {
546556 return err
547557 }
@@ -550,9 +560,9 @@ func installSkillToDir(ctx context.Context, ref, skillName, destDir string, file
550560 return fmt .Errorf ("failed to create directory: %w" , err )
551561 }
552562
553- log .Debugf (ctx , "Downloading %s/%s" , skillName , file )
563+ log .Debugf (ctx , "Downloading %s/%s" , skillName , manifestFile )
554564 if err := os .WriteFile (destPath , content , 0o644 ); err != nil {
555- return fmt .Errorf ("failed to write %s: %w" , file , err )
565+ return fmt .Errorf ("failed to write %s: %w" , relForDisk , err )
556566 }
557567 }
558568
@@ -580,6 +590,29 @@ func safeSkillDestPath(destDir, file string) (string, error) {
580590 return destPath , nil
581591}
582592
593+ // resolveSymlinkTarget returns the absolute, cleaned path that a symlink at
594+ // linkPath points to, given the raw target from os.Readlink.
595+ func resolveSymlinkTarget (linkPath , target string ) string {
596+ if ! filepath .IsAbs (target ) {
597+ return filepath .Clean (filepath .Join (filepath .Dir (linkPath ), target ))
598+ }
599+ return filepath .Clean (target )
600+ }
601+
602+ // pathWithinOrEqual reports whether target is exactly base or lexically under base.
603+ func pathWithinOrEqual (target , base string ) bool {
604+ t := filepath .Clean (target )
605+ b := filepath .Clean (base )
606+ if t == b {
607+ return true
608+ }
609+ rel , err := filepath .Rel (b , t )
610+ if err != nil {
611+ return false
612+ }
613+ return filepath .IsLocal (rel )
614+ }
615+
583616// copySkillMaterialized copies the full skill tree from src to dest and writes
584617// managedSkillMarker so uninstall can remove the directory safely.
585618func copySkillMaterialized (src , dest string ) error {
@@ -599,6 +632,10 @@ func copySkillMaterialized(src, dest string) error {
599632
600633 target := filepath .Join (dest , rel )
601634
635+ if d .Type ()& fs .ModeSymlink != 0 {
636+ return fmt .Errorf ("unexpected symlink in skill tree: %s" , rel )
637+ }
638+
602639 if d .IsDir () {
603640 return os .MkdirAll (target , 0o755 )
604641 }
@@ -613,9 +650,14 @@ func copySkillMaterialized(src, dest string) error {
613650 return os .WriteFile (target , data , 0o644 )
614651 })
615652 if err != nil {
653+ _ = os .RemoveAll (dest )
616654 return err
617655 }
618- return os .WriteFile (filepath .Join (dest , managedSkillMarker ), nil , 0o644 )
656+ if werr := os .WriteFile (filepath .Join (dest , managedSkillMarker ), nil , 0o644 ); werr != nil {
657+ _ = os .RemoveAll (dest )
658+ return fmt .Errorf ("failed to write managed skill marker: %w" , werr )
659+ }
660+ return nil
619661}
620662
621663func createSymlink (source , dest string ) error {
0 commit comments