Files
kandev/apps/backend/internal/agent/managedruntime/cache.go
T
2026-08-24 21:05:02 +00:00

103 lines
3.6 KiB
Go

package managedruntime
import (
"crypto/sha512"
"encoding/hex"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
)
const npxExecutionKeyLength = 16
// NpxExecutionCacheKey returns npm's deterministic _npx execution-tree key
// for one trusted package spec. The package spec must be the exact value that
// the managed runtime command passes to npx.
func NpxExecutionCacheKey(packageSpec string) string {
digest := sha512.Sum512([]byte(packageSpec))
return hex.EncodeToString(digest[:])[:npxExecutionKeyLength]
}
// ValidateExactPackageSpec accepts only a package name followed by a stable
// version. Cache repair must not accept a path or an unversioned npm selector.
func ValidateExactPackageSpec(packageSpec string) error {
if strings.TrimSpace(packageSpec) != packageSpec || packageSpec == "" {
return errors.New("managed runtime package spec is not exact")
}
if strings.ContainsAny(packageSpec, "\x00\r\n\\") || strings.Contains(packageSpec, "..") {
return errors.New("managed runtime package spec is invalid")
}
separator := strings.LastIndexByte(packageSpec, '@')
if separator <= 0 || separator == len(packageSpec)-1 {
return errors.New("managed runtime package spec must include a version")
}
if err := validateManagedPackageName(packageSpec[:separator]); err != nil {
return err
}
if _, err := ParseStableVersion(packageSpec[separator+1:]); err != nil {
return fmt.Errorf("managed runtime package spec version: %w", err)
}
return nil
}
func validateManagedPackageName(packageName string) error {
if filepath.IsAbs(packageName) || strings.Contains(packageName, ":") || strings.HasSuffix(packageName, "/") {
return errors.New("managed runtime package spec is path-like")
}
if !strings.HasPrefix(packageName, "@") {
if strings.ContainsAny(packageName, "@/") {
return errors.New("managed runtime package spec is path-like")
}
return nil
}
if strings.Contains(packageName[1:], "@") {
return errors.New("managed runtime package spec has an invalid scope")
}
parts := strings.Split(packageName, "/")
if len(parts) != 2 || parts[0] == "@" || parts[1] == "" {
return errors.New("managed runtime package spec has an invalid scope")
}
return nil
}
// RemoveNpxExecutionTree removes only the _npx tree derived from packageSpec.
// It refuses broad roots and symlinked paths so a cache repair cannot escape
// the exact trusted execution key.
func RemoveNpxExecutionTree(cacheRoot, packageSpec string) error {
root, err := validateNpmCacheRoot(cacheRoot)
if err != nil {
return err
}
packageSpec = strings.TrimSpace(packageSpec)
if packageSpec == "" || strings.ContainsRune(packageSpec, '\x00') || strings.Contains(packageSpec, "..") {
return errors.New("managed runtime package spec is invalid")
}
key := NpxExecutionCacheKey(packageSpec)
return removeNpxExecutionTree(root, key)
}
func validateNpmCacheRoot(cacheRoot string) (string, error) {
cacheRoot = strings.TrimSpace(cacheRoot)
if cacheRoot == "" || !filepath.IsAbs(cacheRoot) {
return "", fmt.Errorf("npm cache root is not absolute: %q", cacheRoot)
}
root := filepath.Clean(cacheRoot)
if root == string(filepath.Separator) || filepath.Base(root) == "_npx" {
return "", errors.New("refusing to invalidate npm cache root")
}
if info, err := os.Lstat(root); err == nil {
if info.Mode()&os.ModeSymlink != 0 {
return "", fmt.Errorf("refusing to invalidate execution cache through symlink: %s", root)
}
if !info.IsDir() {
return "", fmt.Errorf("npm cache root is not a directory: %s", root)
}
} else if !errors.Is(err, os.ErrNotExist) {
return "", fmt.Errorf("inspect npm cache root: %w", err)
}
return root, nil
}