Skip to content

Commit c5265ba

Browse files
committed
chore: fixup
1 parent ed51a58 commit c5265ba

File tree

5 files changed

+169
-194
lines changed

5 files changed

+169
-194
lines changed

experimental/aitools/lib/agents/agents.go

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,6 @@ type Agent struct {
2323
// ProjectConfigDir is the config directory name relative to a project root
2424
// (e.g., ".claude"). Only used when SupportsProjectScope is true.
2525
ProjectConfigDir string
26-
// PreferSkillCopy when true, installs each skill as a full directory copy
27-
// instead of a symlink. Codex uses this so agents/openai.yaml and assets/
28-
// resolve reliably under its config dir.
29-
PreferSkillCopy bool
30-
// InstallAgentMetadata when true, includes agent-specific UI metadata
31-
// (agents/openai.yaml, assets/) during installation. Agents that don't
32-
// consume these files skip them to keep their skills dirs clean.
33-
InstallAgentMetadata bool
3426
}
3527

3628
// Detected returns true if the agent is installed on the system.
@@ -95,11 +87,9 @@ var Registry = []Agent{
9587
ProjectConfigDir: ".cursor",
9688
},
9789
{
98-
Name: "codex",
99-
DisplayName: "Codex CLI",
100-
ConfigDir: homeSubdir(".codex"),
101-
PreferSkillCopy: true,
102-
InstallAgentMetadata: true,
90+
Name: "codex",
91+
DisplayName: "Codex CLI",
92+
ConfigDir: homeSubdir(".codex"),
10393
},
10494
{
10595
Name: "opencode",

experimental/aitools/lib/installer/installer.go

Lines changed: 60 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"errors"
66
"fmt"
77
"io"
8+
"io/fs"
89
"net/http"
910
"os"
1011
"path/filepath"
@@ -28,6 +29,10 @@ const (
2829
defaultSkillsRepoRef = "v0.1.4"
2930
)
3031

32+
// managedSkillMarker is written at the root of each materialized (non-symlink)
33+
// skill install under an agent directory so uninstall can remove it safely.
34+
const managedSkillMarker = ".databricks-skill-managed"
35+
3136
// fetchFileFn is the function used to download individual skill files.
3237
// It is a package-level var so tests can replace it with a mock.
3338
var fetchFileFn = fetchSkillFile
@@ -78,15 +83,18 @@ func FetchManifest(ctx context.Context) (*Manifest, error) {
7883
// to disk and adjusts the download URL accordingly.
7984
const sharedFilePrefix = "@root:"
8085

81-
func fetchSkillFile(ctx context.Context, ref, skillName, filePath string) ([]byte, error) {
82-
var url string
86+
// skillFileRawURL returns the raw.githubusercontent.com URL for a manifest file entry.
87+
func skillFileRawURL(ref, skillName, filePath string) string {
8388
if rootPath, ok := strings.CutPrefix(filePath, sharedFilePrefix); ok {
84-
url = fmt.Sprintf("https://raw.githubusercontent.com/%s/%s/%s/%s",
89+
return fmt.Sprintf("https://raw.githubusercontent.com/%s/%s/%s/%s",
8590
skillsRepoOwner, skillsRepoName, ref, rootPath)
86-
} else {
87-
url = fmt.Sprintf("https://raw.githubusercontent.com/%s/%s/%s/%s/%s/%s",
88-
skillsRepoOwner, skillsRepoName, ref, skillsRepoPath, skillName, filePath)
8991
}
92+
return fmt.Sprintf("https://raw.githubusercontent.com/%s/%s/%s/%s/%s/%s",
93+
skillsRepoOwner, skillsRepoName, ref, skillsRepoPath, skillName, filePath)
94+
}
95+
96+
func fetchSkillFile(ctx context.Context, ref, skillName, filePath string) ([]byte, error) {
97+
url := skillFileRawURL(ref, skillName, filePath)
9098

9199
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
92100
if err != nil {
@@ -401,32 +409,14 @@ type installParams struct {
401409
ref string
402410
}
403411

404-
// agentMetadataDirs lists subdirectory prefixes that are agent-specific UI
405-
// metadata (marketplace icons, display names). Only agents with
406-
// InstallAgentMetadata=true receive these during a copy install.
407-
var agentMetadataDirs = []string{"agents", "assets"}
408-
409-
// isAgentMetadataPath reports whether rel (forward-slash separated) falls
410-
// under one of the agent metadata directories.
411-
func isAgentMetadataPath(rel string) bool {
412-
normalized := filepath.ToSlash(rel)
413-
for _, dir := range agentMetadataDirs {
414-
if normalized == dir || strings.HasPrefix(normalized, dir+"/") {
415-
return true
416-
}
417-
}
418-
return false
419-
}
420-
421412
func installSkillForAgents(ctx context.Context, skillName string, files []string, detectedAgents []*agents.Agent, params installParams) error {
422413
canonicalDir := filepath.Join(params.baseDir, skillName)
423414
if err := installSkillToDir(ctx, params.ref, skillName, canonicalDir, files); err != nil {
424415
return err
425416
}
426417

427418
// For project scope, always symlink. For global, symlink when multiple agents.
428-
// Agents with PreferSkillCopy always get a full directory copy so
429-
// marketplace metadata under agents/ and assets/ stays under their config dir.
419+
// Symlinks point at the canonical tree (full manifest layout for every agent).
430420
symlinkEligible := params.scope == ScopeProject || len(detectedAgents) > 1
431421

432422
for _, agent := range detectedAgents {
@@ -443,7 +433,7 @@ func installSkillForAgents(ctx context.Context, skillName string, files []string
443433
continue
444434
}
445435

446-
useSymlink := symlinkEligible && !agent.PreferSkillCopy
436+
useSymlink := symlinkEligible
447437
if useSymlink {
448438
symlinkTarget := canonicalDir
449439
// For project scope, use relative symlinks so they work for teammates.
@@ -455,17 +445,14 @@ func installSkillForAgents(ctx context.Context, skillName string, files []string
455445
}
456446
if err := createSymlink(symlinkTarget, destDir); err != nil {
457447
log.Debugf(ctx, "Symlink failed for %s, copying instead: %v", agent.DisplayName, err)
458-
if err := copyDir(canonicalDir, destDir); err != nil {
448+
if err := copySkillMaterialized(canonicalDir, destDir); err != nil {
459449
log.Warnf(ctx, "Failed to install for %s: %v", agent.DisplayName, err)
460450
continue
461451
}
462452
}
463453
log.Debugf(ctx, "Installed %q for %s (symlinked)", skillName, agent.DisplayName)
464454
} else {
465-
// Copy from canonical dir. Skip agent metadata dirs for agents
466-
// that don't consume them.
467-
skipMetadata := !agent.InstallAgentMetadata
468-
if err := copyDirFiltered(canonicalDir, destDir, skipMetadata); err != nil {
455+
if err := copySkillMaterialized(canonicalDir, destDir); err != nil {
469456
log.Warnf(ctx, "Failed to install for %s: %v", agent.DisplayName, err)
470457
continue
471458
}
@@ -509,6 +496,13 @@ func backupThirdPartySkill(ctx context.Context, destDir, canonicalDir, skillName
509496
}
510497
}
511498

499+
// Prior materialized install from this CLI; copySkillMaterialized will replace it.
500+
if fi.IsDir() {
501+
if _, err := os.Stat(filepath.Join(destDir, managedSkillMarker)); err == nil {
502+
return nil
503+
}
504+
}
505+
512506
backupDir, err := os.MkdirTemp("", fmt.Sprintf("databricks-skill-backup-%s-*", skillName))
513507
if err != nil {
514508
return fmt.Errorf("failed to create backup directory: %w", err)
@@ -542,9 +536,15 @@ func installSkillToDir(ctx context.Context, ref, skillName, destDir string, file
542536
// Strip the @root: prefix so shared assets land at a local path
543537
// (e.g. "@root:assets/databricks.svg" → "assets/databricks.svg").
544538
if rootPath, ok := strings.CutPrefix(file, sharedFilePrefix); ok {
539+
if rootPath == "" {
540+
return fmt.Errorf("invalid manifest file entry: empty path after %q", sharedFilePrefix)
541+
}
545542
file = rootPath
546543
}
547-
destPath := filepath.Join(destDir, file)
544+
destPath, err := safeSkillDestPath(destDir, file)
545+
if err != nil {
546+
return err
547+
}
548548

549549
if err := os.MkdirAll(filepath.Dir(destPath), 0o755); err != nil {
550550
return fmt.Errorf("failed to create directory: %w", err)
@@ -559,20 +559,35 @@ func installSkillToDir(ctx context.Context, ref, skillName, destDir string, file
559559
return nil
560560
}
561561

562-
// copyDir copies all files from src to dest, recreating the directory structure.
563-
func copyDir(src, dest string) error {
564-
return copyDirFiltered(src, dest, false)
562+
// safeSkillDestPath joins destDir with a manifest-relative path and rejects escapes.
563+
func safeSkillDestPath(destDir, file string) (string, error) {
564+
if file == "" {
565+
return "", errors.New("invalid manifest file entry: empty path")
566+
}
567+
destPath := filepath.Join(destDir, file)
568+
base := filepath.Clean(destDir)
569+
cleaned := filepath.Clean(destPath)
570+
rel, err := filepath.Rel(base, cleaned)
571+
if err != nil {
572+
return "", fmt.Errorf("invalid skill file path %q: %w", file, err)
573+
}
574+
if rel == "." {
575+
return "", fmt.Errorf("invalid skill file path %q: resolves to skill root", file)
576+
}
577+
if !filepath.IsLocal(rel) {
578+
return "", fmt.Errorf("skill file path %q escapes destination directory", file)
579+
}
580+
return destPath, nil
565581
}
566582

567-
// copyDirFiltered copies files from src to dest. When skipAgentMetadata is
568-
// true, subdirectories matching agentMetadataDirs (agents/, assets/) are
569-
// skipped so non-Codex agents don't receive marketplace UI files.
570-
func copyDirFiltered(src, dest string, skipAgentMetadata bool) error {
583+
// copySkillMaterialized copies the full skill tree from src to dest and writes
584+
// managedSkillMarker so uninstall can remove the directory safely.
585+
func copySkillMaterialized(src, dest string) error {
571586
if err := os.RemoveAll(dest); err != nil {
572587
return fmt.Errorf("failed to remove existing path: %w", err)
573588
}
574589

575-
return filepath.Walk(src, func(path string, info os.FileInfo, err error) error {
590+
err := filepath.WalkDir(src, func(path string, d fs.DirEntry, err error) error {
576591
if err != nil {
577592
return err
578593
}
@@ -582,16 +597,9 @@ func copyDirFiltered(src, dest string, skipAgentMetadata bool) error {
582597
return err
583598
}
584599

585-
if skipAgentMetadata && rel != "." && isAgentMetadataPath(rel) {
586-
if info.IsDir() {
587-
return filepath.SkipDir
588-
}
589-
return nil
590-
}
591-
592600
target := filepath.Join(dest, rel)
593601

594-
if info.IsDir() {
602+
if d.IsDir() {
595603
return os.MkdirAll(target, 0o755)
596604
}
597605

@@ -604,6 +612,10 @@ func copyDirFiltered(src, dest string, skipAgentMetadata bool) error {
604612
}
605613
return os.WriteFile(target, data, 0o644)
606614
})
615+
if err != nil {
616+
return err
617+
}
618+
return os.WriteFile(filepath.Join(dest, managedSkillMarker), nil, 0o644)
607619
}
608620

609621
func createSymlink(source, dest string) error {

0 commit comments

Comments
 (0)