Files
kandev/apps/backend/internal/worktree/directory_link.go
T

465 lines
16 KiB
Go

package worktree
import (
"context"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
storageworkspaces "github.com/kandev/kandev/internal/system/storage/workspaces"
)
const errWorkspaceOwnershipMarkerConflict = "workspace ownership marker conflicts with requested task root"
// CreateOwnedDirectoryLink creates a live directory reference below root.
// root is created only through real (non-symlink) ancestors, and name must be
// a single path segment. The caller owns root; this function never alters the
// target or an existing entry.
func CreateOwnedDirectoryLink(root, name, target string) (string, error) {
if !isOwnedDirectoryLinkPath(root, name) {
return "", fmt.Errorf("invalid owned link path")
}
canonicalTarget, err := canonicalDirectoryLinkTarget(target)
if err != nil {
return "", err
}
if err := mkdirOwned(root); err != nil {
return "", err
}
link, err := ownedDirectoryLinkPath(root, name)
if err != nil {
return "", err
}
release, err := acquireWorktreeTargetPath(context.Background(), link)
if err != nil {
return "", fmt.Errorf("acquire owned link lock: %w", err)
}
defer release()
return createOwnedDirectoryLinkLocked(root, name, link, canonicalTarget)
}
func createOwnedDirectoryLinkLocked(root, name, link, canonicalTarget string) (string, error) {
_, err := os.Lstat(link)
if err == nil {
return "", fmt.Errorf("owned link entry already exists: %s", name)
}
if !os.IsNotExist(err) {
return "", fmt.Errorf("inspect owned link entry: %w", err)
}
if err := createPlatformDirectoryLink(canonicalTarget, link); err != nil {
return "", fmt.Errorf("create directory link: %w", err)
}
if err := verifyCreatedOwnedDirectoryLink(root, link); err != nil {
return "", err
}
return link, nil
}
// RestoreOwnedDirectoryLink updates or recreates a live directory reference
// below root so it points at target, preserving the current link if the
// replacement cannot be completed safely.
func RestoreOwnedDirectoryLink(root, name, target string) error {
if !isOwnedDirectoryLinkPath(root, name) {
return fmt.Errorf("invalid owned link path")
}
if err := mkdirOwned(root); err != nil {
return err
}
link, err := ownedDirectoryLinkPath(root, name)
if err != nil {
return err
}
release, err := acquireWorktreeTargetPath(context.Background(), link)
if err != nil {
return fmt.Errorf("acquire owned link lock: %w", err)
}
defer release()
return restoreOwnedDirectoryLinkLocked(root, name, link, target)
}
// RemoveOwnedDirectoryLink removes root/name only while it is still the
// directory link inspected under the per-link lock. A missing entry is already
// rolled back and is therefore treated as success.
func RemoveOwnedDirectoryLink(root, name string) error {
if !isOwnedDirectoryLinkPath(root, name) {
return fmt.Errorf("invalid owned link path")
}
link, err := ownedDirectoryLinkPath(root, name)
if err != nil {
return err
}
release, err := acquireWorktreeTargetPath(context.Background(), link)
if err != nil {
return fmt.Errorf("acquire owned link lock: %w", err)
}
defer release()
info, err := os.Lstat(link)
if os.IsNotExist(err) {
return nil
}
if err != nil {
return fmt.Errorf("inspect owned link entry: %w", err)
}
if !isPlatformDirectoryLink(info, link) {
return fmt.Errorf("owned link entry is not a directory link: %s", name)
}
if err := removeInspectedDirectoryLink(link, info); err != nil {
return fmt.Errorf("remove owned link: %w", err)
}
return nil
}
func isOwnedDirectoryLinkPath(root, name string) bool {
return root != "" && filepath.IsAbs(root) && name != "" && filepath.Base(name) == name && name != "." && name != ".."
}
func canonicalDirectoryLinkTarget(target string) (string, error) {
canonicalTarget, err := filepath.EvalSymlinks(target)
if err != nil {
return "", fmt.Errorf("canonicalize link target: %w", err)
}
info, err := os.Stat(canonicalTarget)
if err != nil || !info.IsDir() {
return "", fmt.Errorf("link target is not a directory: %w", err)
}
return canonicalTarget, nil
}
func ownedDirectoryLinkPath(root, name string) (string, error) {
link := filepath.Join(root, name)
if filepath.Dir(link) != filepath.Clean(root) {
return "", fmt.Errorf("link escapes owned root")
}
return link, nil
}
func verifyCreatedOwnedDirectoryLink(root, link string) error {
if err := requireRealDir(root); err != nil {
_ = os.Remove(link)
return err
}
if err := requirePlatformDirectoryLink(link); err != nil {
_ = os.Remove(link)
return fmt.Errorf("owned link changed during creation")
}
return nil
}
// OwnedDirectoryLinkOwner identifies the task that is allowed to repoint a
// Kandev-owned directory link under a shared task root.
type OwnedDirectoryLinkOwner struct {
TaskID string
TaskDirName string
}
// OwnedDirectoryLinkResult describes the durable effect of EnsureOwnedDirectoryLink.
type OwnedDirectoryLinkResult struct {
Path string
Created bool
PriorTarget string
}
// EnsureOwnedDirectoryLink returns an existing matching live link, repoints an
// owned link whose target drifted, or creates the link. A non-link/collision
// still fails closed and is never removed; only a Kandev-owned directory link
// (a pointer, not content) is replaced when its target no longer matches.
//
// The existing link is matched by filesystem identity, not by resolved path.
// filepath.EvalSymlinks does not traverse a Windows junction — it returns the
// link's own normalized path — so a path comparison rejected every unchanged
// junction as a target mismatch, and each relaunch or resume of a task with
// workspace source links failed. os.Stat follows a junction and a Unix symlink
// alike, and os.SameFile compares volume and file index, which is also immune
// to 8.3 short paths and path case.
//
// Repoint-on-mismatch is deliberately scoped here to the Kandev-owned task root:
// the entry lives below a root built through real ancestors, and the entry is a
// directory link, so removing and recreating it is safe. This is distinct from
// IsSelfReferentialDirectoryLink, which stays report-only because it concerns
// entries inside a user's own repository.
//
// The full inspect, create, and repoint flow is serialized per owned-link path
// with the shared target-path lock used by other Kandev writers. Unix still
// stages a sibling temp link and renames it into place; Windows still removes
// and recreates, but only while that exclusive slot is held.
func EnsureOwnedDirectoryLink(root, name, target string, owner OwnedDirectoryLinkOwner) (OwnedDirectoryLinkResult, error) {
link, err := ownedDirectoryLinkPath(root, name)
if err != nil {
return OwnedDirectoryLinkResult{}, err
}
release, err := acquireWorktreeTargetPath(context.Background(), link)
if err != nil {
return OwnedDirectoryLinkResult{}, fmt.Errorf("acquire owned link lock: %w", err)
}
defer release()
canonicalTarget, err := canonicalDirectoryLinkTarget(target)
if err != nil {
return OwnedDirectoryLinkResult{}, err
}
info, err := os.Lstat(link)
if err == nil {
if !isPlatformDirectoryLink(info, link) {
return OwnedDirectoryLinkResult{}, fmt.Errorf("owned link entry already exists: %s", name)
}
actual, err := os.Stat(link)
if err != nil {
return OwnedDirectoryLinkResult{}, fmt.Errorf("resolve owned link: %w", err)
}
expected, err := os.Stat(canonicalTarget)
if err != nil {
return OwnedDirectoryLinkResult{}, fmt.Errorf("inspect link target: %w", err)
}
if os.SameFile(actual, expected) {
return OwnedDirectoryLinkResult{Path: link}, nil
}
priorTarget, err := platformDirectoryLinkTarget(link)
if err != nil {
return OwnedDirectoryLinkResult{}, fmt.Errorf("read owned link target: %w", err)
}
if err := ensureOwnedLinkRepointAllowed(root, owner); err != nil {
return OwnedDirectoryLinkResult{}, err
}
if err := repointOwnedDirectoryLink(root, name, link, info, priorTarget, canonicalTarget); err != nil {
return OwnedDirectoryLinkResult{}, err
}
return OwnedDirectoryLinkResult{Path: link, Created: true, PriorTarget: priorTarget}, nil
}
if !os.IsNotExist(err) {
return OwnedDirectoryLinkResult{}, fmt.Errorf("inspect owned link entry: %w", err)
}
if err := mkdirOwned(root); err != nil {
return OwnedDirectoryLinkResult{}, err
}
created, err := createOwnedDirectoryLinkLocked(root, name, link, canonicalTarget)
if err != nil {
return OwnedDirectoryLinkResult{}, err
}
return OwnedDirectoryLinkResult{Path: created, Created: true}, nil
}
func repointOwnedDirectoryLink(root, name, link string, inspected os.FileInfo, priorTarget, target string) error {
if err := replacePlatformDirectoryLink(link, inspected, target, priorTarget); err != nil {
return err
}
if err := verifyCreatedOwnedDirectoryLink(root, link); err != nil {
if restoreErr := restoreOwnedDirectoryLinkLocked(root, name, link, priorTarget); restoreErr != nil {
return errors.Join(err, restoreErr)
}
return err
}
return nil
}
func restoreOwnedDirectoryLinkLocked(root, name, link, target string) error {
if target == "" {
return nil
}
canonicalTarget, err := canonicalDirectoryLinkTarget(target)
if err != nil {
return fmt.Errorf("restore prior directory link: %w", err)
}
info, err := os.Lstat(link)
if os.IsNotExist(err) {
if _, err := createOwnedDirectoryLinkLocked(root, name, link, canonicalTarget); err != nil {
return fmt.Errorf("restore prior directory link: %w", err)
}
return nil
}
if err != nil {
return fmt.Errorf("restore prior directory link: inspect owned link entry: %w", err)
}
if !isPlatformDirectoryLink(info, link) {
return fmt.Errorf("restore prior directory link: owned link entry already exists: %s", name)
}
actual, err := os.Stat(link)
if err != nil {
return fmt.Errorf("restore prior directory link: resolve owned link: %w", err)
}
expected, err := os.Stat(canonicalTarget)
if err != nil {
return fmt.Errorf("restore prior directory link: inspect link target: %w", err)
}
if os.SameFile(actual, expected) {
return nil
}
currentTarget, err := platformDirectoryLinkTarget(link)
if err != nil {
return fmt.Errorf("restore prior directory link: read owned link target: %w", err)
}
if err := replacePlatformDirectoryLink(link, info, canonicalTarget, currentTarget); err != nil {
return fmt.Errorf("restore prior directory link: %w", err)
}
if err := verifyCreatedOwnedDirectoryLink(root, link); err != nil {
return fmt.Errorf("restore prior directory link: %w", err)
}
return nil
}
func ensureOwnedLinkRepointAllowed(root string, owner OwnedDirectoryLinkOwner) error {
marker, found, err := storageworkspaces.ReadOwnershipMarker(root)
if err != nil {
return fmt.Errorf("inspect workspace ownership marker: %w", err)
}
if !found {
return nil
}
if marker.TaskID != owner.TaskID || marker.TaskDirName != owner.TaskDirName {
return errors.New(errWorkspaceOwnershipMarkerConflict)
}
return nil
}
func revalidateInspectedDirectoryLink(link string, inspected os.FileInfo) error {
current, err := os.Lstat(link)
if err != nil {
return fmt.Errorf("re-inspect owned link: %w", err)
}
if !isPlatformDirectoryLink(current, link) || !os.SameFile(current, inspected) {
return fmt.Errorf("owned link entry changed during repoint: %s", filepath.Base(link))
}
return nil
}
func renameInspectedDirectoryLink(tempLink, link string, inspected os.FileInfo) error {
if err := revalidateInspectedDirectoryLink(link, inspected); err != nil {
return err
}
if err := os.Rename(tempLink, link); err != nil {
return fmt.Errorf("repoint owned link: %w", err)
}
return nil
}
// IsSelfReferentialDirectoryLink reports whether root/name is a platform
// directory link whose target is root itself — the shape an earlier release
// planted inside a user's own repository for local-executor tasks, and which
// EnsureOwnedDirectoryLink then accepts as valid forever.
//
// It deliberately only reports. Such an entry cannot be shown to be
// Kandev-owned: Kandev writes no ownership marker into user-owned sources, and
// a user (or the repository itself) may keep a link of the same name and target
// on purpose, so removing it could destroy content that is not ours. Nor could
// a stat-then-remove sequence close the window between the check and the
// unlink. Callers surface it and leave removal to the user.
func IsSelfReferentialDirectoryLink(root, name string) (bool, error) {
if !isOwnedDirectoryLinkPath(root, name) {
return false, fmt.Errorf("invalid owned link path")
}
link, err := ownedDirectoryLinkPath(root, name)
if err != nil {
return false, err
}
info, err := os.Lstat(link)
if os.IsNotExist(err) {
return false, nil
}
if err != nil {
return false, fmt.Errorf("inspect owned link entry: %w", err)
}
if !isPlatformDirectoryLink(info, link) {
return false, nil
}
return linkTargetsRoot(root, link)
}
// linkTargetsRoot compares filesystem identity, not resolved path text.
// filepath.EvalSymlinks does not traverse a Windows junction — it returns the
// link's own normalized path — so a path comparison never matches. os.Stat
// follows a junction and a Unix symlink alike, and os.SameFile then compares
// volume and file index, which also absorbs 8.3 short paths and case.
func linkTargetsRoot(root, link string) (bool, error) {
linkInfo, err := os.Stat(link)
if err != nil {
return false, fmt.Errorf("resolve owned link: %w", err)
}
rootInfo, err := os.Stat(root)
if err != nil {
return false, fmt.Errorf("resolve owned root: %w", err)
}
return os.SameFile(linkInfo, rootInfo), nil
}
func mkdirOwned(root string) error {
parent := filepath.Dir(root)
if err := requireNoSymlinkAncestors(parent); err != nil {
return err
}
if err := os.MkdirAll(parent, 0o755); err != nil {
return fmt.Errorf("create owned parent: %w", err)
}
if err := requireRealDir(parent); err != nil {
return err
}
if err := os.Mkdir(root, 0o755); err != nil && !os.IsExist(err) {
return fmt.Errorf("create owned root: %w", err)
}
return requireRealDir(root)
}
// requireNoSymlinkAncestors checks every existing control-path component
// before MkdirAll can traverse it. It deliberately uses Lstat so a junction
// or symlink is rejected rather than resolved.
func requireNoSymlinkAncestors(path string) error {
clean := filepath.Clean(path)
volume := filepath.VolumeName(clean)
rest := strings.TrimPrefix(clean, volume)
rest = strings.TrimPrefix(rest, string(filepath.Separator))
current := volume + string(filepath.Separator)
for _, part := range strings.Split(rest, string(filepath.Separator)) {
if part == "" {
continue
}
current = filepath.Join(current, part)
info, err := os.Lstat(current)
if os.IsNotExist(err) {
return nil
}
if err != nil {
return fmt.Errorf("inspect owned ancestor: %w", err)
}
if isPlatformDirectoryLink(info, current) {
return fmt.Errorf("owned control ancestor is symlink: %s", current)
}
}
return nil
}
func requireRealDir(path string) error {
info, err := os.Lstat(path)
if err != nil {
return fmt.Errorf("inspect owned path: %w", err)
}
if isPlatformDirectoryLink(info, path) || !info.IsDir() {
return fmt.Errorf("owned path is not a real directory: %s", path)
}
return nil
}
// TaskRoot returns the canonical Kandev-owned root for a task directory name.
// It deliberately accepts only the same single-segment names used by worktree
// creation, preventing a persisted environment row from selecting another
// directory beneath (or outside) the configured tasks base.
func (m *Manager) TaskRoot(taskDirName string) (string, error) {
if taskDirName == "" || filepath.Base(taskDirName) != taskDirName || taskDirName == "." || taskDirName == ".." {
return "", fmt.Errorf("invalid task directory name")
}
base, err := m.config.ExpandedTasksBasePath()
if err != nil {
return "", err
}
base, err = filepath.Abs(base)
if err != nil {
return "", err
}
if err := requireRealDir(base); err != nil {
return "", err
}
if err := requireNoSymlinkAncestors(base); err != nil {
return "", err
}
return filepath.Join(base, taskDirName), nil
}