Files
wehub-resource-sync f99010fae1
CI / lint (push) Failing after 1s
CI / frontend (push) Failing after 1s
CI / scripts (push) Failing after 1s
CI / Go Test (ubuntu-latest) (push) Failing after 0s
CI / frontend-node-25 (push) Failing after 1s
CI / docs (push) Failing after 0s
CI / coverage (push) Failing after 0s
CI / e2e (push) Failing after 0s
Docker / build-and-push (push) Failing after 1s
CI / integration (push) Failing after 4m43s
CI / Go Test (windows-latest) (push) Has been cancelled
CI / Desktop Unit Tests (Windows) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux (arm64)) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Windows) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (aarch64)) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (x86_64)) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:30:36 +08:00

381 lines
11 KiB
Go

package remotesync
import (
"context"
"os"
"path/filepath"
"slices"
"sort"
"strings"
"go.kenn.io/agentsview/internal/config"
"go.kenn.io/agentsview/internal/parser"
)
func ResolveTargets(cfg config.Config) TargetSet {
dirs := make(map[parser.AgentType][]string)
files := make(map[parser.AgentType][]string)
var extra []string
for _, def := range parser.Registry {
if !resolveAgentHasOnDiskSource(def) {
continue
}
for _, dir := range cfg.ResolveDirs(def.Type) {
if def.Type == parser.AgentAider {
targets := resolveAiderTargets(dir)
if len(targets) > 0 {
dirs[def.Type] = append(dirs[def.Type], targets...)
}
continue
}
if def.Type == parser.AgentWindsurf {
root, targetFiles := resolveWindsurfTarget(dir)
if root != "" && len(targetFiles) > 0 {
dirs[def.Type] = append(dirs[def.Type], root)
files[def.Type] = append(files[def.Type], targetFiles...)
}
continue
}
if info, err := os.Stat(dir); err != nil || !info.IsDir() {
continue
}
dirs[def.Type] = append(dirs[def.Type], dir)
if def.Type == parser.AgentCodex {
index := filepath.Join(filepath.Dir(dir), parser.CodexSessionIndexFilename)
if info, err := os.Stat(index); err == nil && !info.IsDir() {
if !slices.Contains(extra, index) {
extra = append(extra, index)
}
}
}
}
}
return TargetSet{Dirs: dirs, Files: files, ExtraFiles: extra}
}
func resolveAgentHasOnDiskSource(def parser.AgentDef) bool {
if !def.FileBased {
return false
}
switch parser.ProviderMigrationModes()[def.Type] {
case parser.ProviderMigrationProviderAuthoritative:
_, ok := parser.ProviderFactoryByType(def.Type)
return ok
default:
return false
}
}
func resolveAiderTargets(root string) []string {
if isAiderUnsafeRoot(root) {
return nil
}
provider, ok := parser.NewProvider(parser.AgentAider, parser.ProviderConfig{
Roots: []string{root},
})
if !ok {
return nil
}
sources, err := provider.Discover(context.Background())
if err != nil {
return nil
}
out := make([]string, 0, len(sources))
for _, source := range sources {
path := providerDiscoveredPath(source)
if filepath.Base(path) == parser.AiderHistoryFileName() {
out = append(out, path)
}
}
return out
}
func resolveWindsurfTarget(root string) (string, []string) {
targetRoot := filepath.Clean(root)
workspaceRoot := windsurfRemoteWorkspaceRoot(targetRoot)
if info, err := os.Stat(workspaceRoot); err != nil || !info.IsDir() {
return "", nil
}
files := resolveWindsurfFiles(workspaceRoot)
if len(files) == 0 {
return "", nil
}
return targetRoot, files
}
func windsurfRemoteWorkspaceRoot(root string) string {
clean := filepath.Clean(root)
if filepath.Base(clean) == "workspaceStorage" {
return clean
}
return filepath.Join(clean, "workspaceStorage")
}
func resolveWindsurfFiles(workspaceRoot string) []string {
entries, err := os.ReadDir(workspaceRoot)
if err != nil {
return nil
}
var files []string
for _, entry := range entries {
if !entry.IsDir() {
continue
}
workspaceDir := filepath.Join(workspaceRoot, entry.Name())
dbPath := filepath.Join(workspaceDir, parser.WindsurfStateDBName)
if !regularRemoteSyncFile(dbPath) {
continue
}
files = append(files, dbPath)
for _, path := range []string{
dbPath + "-wal",
filepath.Join(workspaceDir, "workspace.json"),
} {
if regularRemoteSyncFile(path) {
files = append(files, path)
}
}
}
sort.Strings(files)
return files
}
func regularRemoteSyncFile(path string) bool {
info, err := os.Lstat(path)
if err != nil {
return false
}
return info.Mode().IsRegular()
}
func providerDiscoveredPath(source parser.SourceRef) string {
for _, path := range []string{
source.DisplayPath,
source.FingerprintKey,
source.Key,
} {
if path != "" {
return path
}
}
return ""
}
func TargetSetAllowed(allowed TargetSet, requested TargetSet) bool {
_, ok := SelectAllowedTargets(allowed, requested)
return ok
}
func SelectAllowedTargets(allowed TargetSet, requested TargetSet) (TargetSet, bool) {
selected := TargetSet{
Dirs: make(map[parser.AgentType][]string),
}
for agent, dirs := range requested.Dirs {
allowedDirs := allowed.Dirs[agent]
if _, fileScoped := allowed.Files[agent]; fileScoped {
requestedFiles, ok := requested.Files[agent]
if !ok || len(requestedFiles) == 0 {
return TargetSet{}, false
}
}
for _, dir := range dirs {
selectedDir, ok := selectAllowedString(allowedDirs, dir)
if !ok {
return TargetSet{}, false
}
selected.Dirs[agent] = append(selected.Dirs[agent], selectedDir)
}
}
for agent, files := range requested.Files {
allowedFiles, ok := allowed.Files[agent]
if !ok {
return TargetSet{}, false
}
for _, file := range files {
selectedFile, ok := selectAllowedString(allowedFiles, file)
if !ok {
return TargetSet{}, false
}
if selected.Files == nil {
selected.Files = make(map[parser.AgentType][]string)
}
selected.Files[agent] = append(selected.Files[agent], selectedFile)
}
}
for _, file := range requested.ExtraFiles {
selectedFile, ok := selectAllowedString(allowed.ExtraFiles, file)
if !ok {
return TargetSet{}, false
}
selected.ExtraFiles = append(selected.ExtraFiles, selectedFile)
}
return selected, true
}
func selectAllowedString(allowed []string, requested string) (string, bool) {
for _, value := range allowed {
if value == requested {
return value, true
}
}
return "", false
}
func isAiderUnsafeRoot(dir string) bool {
if dir == "" {
return true
}
home, err := os.UserHomeDir()
if err != nil || home == "" {
return false
}
return filepath.Clean(dir) == filepath.Clean(home)
}
// SelectAllowedFiles validates a delta-archive file list: every entry
// must be under an allowed dir, exactly an allowed root (some agents,
// like Aider, resolve individual history files into Dirs), or exactly
// an allowed extra file. Only absolute request paths can match an
// allowed root; the absolute check is remote-OS neutral because
// request paths echo the server's own manifest, not local-OS paths.
// Any disallowed entry rejects the whole request (fail closed, like
// SelectAllowedTargets). Path traversal is rejected by
// safeRemotePathArchiveName before any prefix comparison; prefix
// comparisons additionally require matching path dialects and reject
// symlinked ancestors that would escape the allowed root.
func SelectAllowedFiles(allowed TargetSet, files []string) ([]string, bool) {
selected := make([]string, 0, len(files))
for _, file := range files {
canonical, ok := selectAllowedFile(allowed, file)
if !ok {
return nil, false
}
selected = append(selected, canonical)
}
return selected, true
}
func selectAllowedFile(allowed TargetSet, file string) (string, bool) {
if canonical, ok := selectAllowedString(allowed.ExtraFiles, file); ok {
return canonical, true
}
if !isAbsRemotePath(file) {
return "", false
}
if _, err := safeRemotePathArchiveName(file); err != nil {
return "", false
}
for agent, dirs := range allowed.Dirs {
if _, fileScoped := allowed.Files[agent]; fileScoped {
// File-scoped agents (Windsurf) export a curated, sanitized
// file list, not a raw directory walk. Accepting a delta
// request by directory prefix would stream the raw file
// (e.g. an unsanitized state.vscdb or a secret) that the
// full-archive writer never exposes. Such agents fall back
// to the full-archive flow, so a legitimate client never
// requests these as deltas.
continue
}
for _, dir := range dirs {
if remotePathDialect(file) != remotePathDialect(dir) {
// Archive-name remapping flattens dialects into one
// namespace (`C:\x` and `/__drive_C/x` both remap to
// `__drive_C/x`), so a cross-dialect prefix match would
// validate a request the archive writer then reads at a
// literal path outside the allowed root.
continue
}
if _, ok := remoteArchiveRel(dir, file); ok {
if symlinkEscapesRoot(dir, file) {
return "", false
}
// Exact root matches are allowed: file roots (Aider
// history files) must stream, and a directory root
// yields nothing because WriteArchiveFiles skips
// non-regular entries.
return file, true
}
}
}
return "", false
}
type pathDialect int
const (
dialectPOSIX pathDialect = iota
dialectDrive
dialectUNC
)
// remotePathDialect classifies an absolute remote path as POSIX,
// Windows drive-letter, or UNC. Delta validation requires the
// requested file and the allowed root to share a dialect before any
// archive-name prefix comparison.
func remotePathDialect(p string) pathDialect {
if strings.HasPrefix(p, `\\`) || strings.HasPrefix(p, "//") {
return dialectUNC
}
if len(p) >= 2 && p[1] == ':' {
return dialectDrive
}
return dialectPOSIX
}
// symlinkEscapesRoot reports whether the allowed root or any path
// component between it and the requested file's parent is a symlink.
// BuildManifest and the full-archive walk never traverse symlinks, so
// delta validation must not either: a symlinked component would let a
// delta request stream entries no manifest ever lists, and with a
// symlinked root that includes files outside the lexical allowed
// directory. Missing components are not escapes: a vanished file is
// skipped by WriteArchiveFiles, and a missing root has nothing under
// it to stream.
func symlinkEscapesRoot(root, file string) bool {
rootInfo, err := os.Lstat(root)
if err != nil {
return false
}
if rootInfo.Mode()&os.ModeSymlink != 0 {
return true
}
rel, err := filepath.Rel(root, filepath.Dir(file))
if err != nil || rel == "." || rel == ".." ||
strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
// Exact root matches (Aider file roots, where Dir(file) is the
// root's parent) and files directly under the root have no
// intermediate components to check; the root's own ancestors
// are operator-configured territory. A component merely named
// with a ".." prefix (e.g. "..alias") is NOT a parent escape
// and falls through to the symlink walk below.
return false
}
dir := root
for part := range strings.SplitSeq(filepath.ToSlash(rel), "/") {
if part == "" || part == "." {
continue
}
dir = filepath.Join(dir, part)
info, err := os.Lstat(dir)
if err != nil {
return false
}
if info.Mode()&os.ModeSymlink != 0 {
return true
}
}
return false
}
// isAbsRemotePath reports whether a requested path is absolute in any
// remote-OS form: POSIX rooted, UNC, or Windows drive-letter. Host
// filepath.IsAbs semantics would wrongly reject POSIX paths on
// Windows and drive paths on Unix, and requests are validated against
// the server's own resolved targets regardless of the local OS.
func isAbsRemotePath(p string) bool {
if strings.HasPrefix(p, "/") || strings.HasPrefix(p, `\\`) {
return true
}
return len(p) >= 3 && p[1] == ':' && (p[2] == '/' || p[2] == '\\')
}