Skip to content
344 changes: 344 additions & 0 deletions pkg/config/pathmanager.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,344 @@
package config

import (
"os"
"path/filepath"
"sync"
)

// PathManager centralises all filesystem path computation for the Devsy CLI.
// Per-OS implementations supply the five top-level directory methods; every
// sub-path is derived from those by the shared basePathManager.
type PathManager interface {
// Top-level XDG category directories.
ConfigDir() (string, error)
DataDir() (string, error)
CacheDir() (string, error)
StateDir() (string, error)
RuntimeDir() (string, error)

// Config paths.
ConfigFilePath() (string, error)

// Data sub-paths (context-relative).
ContextDir(context string) (string, error)
WorkspacesDir(context string) (string, error)
WorkspaceDir(context, workspaceID string) (string, error)
MachinesDir(context string) (string, error)
MachineDir(context, machineID string) (string, error)
ProvidersDir(context string) (string, error)
ProviderDir(context, providerName string) (string, error)
ProviderBinariesDir(context, providerName string) (string, error)
ProviderDaemonDir(context, providerName string) (string, error)
ProInstancesDir(context string) (string, error)
ProInstanceDir(context, proInstanceHost string) (string, error)
LocksDir(context string) (string, error)

// Cache sub-paths.
AgentCacheDir() (string, error)
ProviderDownloadCacheDir() (string, error)
FeatureCacheDir(hashedID string) (string, error)
PlatformCacheDir() (string, error)
SSHKeysDir() (string, error)

// Runtime sub-paths.
DaemonPIDFile() (string, error)
DaemonLockFile() (string, error)
DaemonStreamsFile() (string, error)
ProcessPIDFile(name string) (string, error)
ProcessLockFile(name string) (string, error)
ProcessStreamsFile(name string) (string, error)

// State sub-paths.
LogDir() (string, error)
}

// basePathManager implements every sub-path method by delegating the top-level
// directory resolution to the embedded pm field (the concrete per-OS struct).
type basePathManager struct {
pm PathManager // self-reference wired after construction
}

// --- Config paths ---

func (b *basePathManager) ConfigFilePath() (string, error) {
if p := os.Getenv(EnvConfig); p != "" {
return p, nil
}

dir, err := b.pm.ConfigDir()
if err != nil {
return "", err
}

return filepath.Join(dir, ConfigFile), nil
}

// --- Data sub-paths (context-relative) ---

func (b *basePathManager) ContextDir(context string) (string, error) {
dir, err := b.pm.DataDir()
if err != nil {
return "", err
}

return filepath.Join(dir, "contexts", context), nil
Comment on lines +95 to +101

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Find PathManager dynamic-path callsites and inspect whether inputs are sanitized before use.

rg -n -C3 '\.(ContextDir|WorkspaceDir|MachineDir|ProviderDir|ProInstanceDir|FeatureCacheDir|ProcessPIDFile|ProcessLockFile|ProcessStreamsFile)\s*\('

Repository: devsy-org/devsy

Length of output: 6299


🏁 Script executed:

#!/bin/bash
# First, let's read the pathmanager.go file to see all the methods in question
wc -l pkg/config/pathmanager.go

Repository: devsy-org/devsy

Length of output: 88


🏁 Script executed:

# Read the pathmanager.go file to understand structure and affected methods
head -100 pkg/config/pathmanager.go | cat -n

Repository: devsy-org/devsy

Length of output: 3624


🏁 Script executed:

# Get the line count of pathmanager.go to determine how to read it efficiently
stat -c%s pkg/config/pathmanager.go

Repository: devsy-org/devsy

Length of output: 103


🏁 Script executed:

# Read the entire pathmanager.go file
cat -n pkg/config/pathmanager.go

Repository: devsy-org/devsy

Length of output: 10590


🏁 Script executed:

# Find callsites of ContextDir and other methods to understand where identifiers come from
rg -n 'ContextDir|WorkspaceDir|MachineDir|ProviderDir|ProInstanceDir|FeatureCacheDir|ProcessPIDFile|ProcessLockFile|ProcessStreamsFile' --type go -B 2 -A 2 | grep -v 'test.go' | head -80

Repository: devsy-org/devsy

Length of output: 4470


🏁 Script executed:

# Search for where 'context' parameter comes from - likely from config or CLI
rg -n 'func.*context.*string' --type go | head -20

Repository: devsy-org/devsy

Length of output: 2282


🏁 Script executed:

# Check if SetPathManager is called anywhere, especially with nil
rg -n 'SetPathManager' --type go -B 2 -A 2

Repository: devsy-org/devsy

Length of output: 1354


🏁 Script executed:

# Find where workspace.ID, machineID, providerName are actually set/parsed
rg -n 'type Workspace|type Machine|type Provider' --type go -A 5 | head -50

Repository: devsy-org/devsy

Length of output: 2603


🏁 Script executed:

# Check if there's any config file parsing that could introduce untrusted values
rg -n 'config.*unmarshal|yaml.*decode|json.*decode' --type go | head -20

Repository: devsy-org/devsy

Length of output: 355


🏁 Script executed:

# Search for CLI argument handling that might pass values to PathManager methods
rg -n 'flag\.|cobra\.|args\[' --type go | grep -i 'context\|workspace\|machine\|provider' | head -20

Repository: devsy-org/devsy

Length of output: 1530


🏁 Script executed:

# Find the ToID function to understand if IDs are validated
rg -n 'func ToID' --type go -A 5

Repository: devsy-org/devsy

Length of output: 410


🏁 Script executed:

# Look at how workspace.ID, machine.ID get populated - from config/API
rg -n 'workspace.ID\s*=' --type go -B 2 -A 1 | head -40

Repository: devsy-org/devsy

Length of output: 1851


🏁 Script executed:

# Check if there's any validation on context names
rg -n 'context.*[Vv]alid|[Vv]alidateContext' --type go -A 3

Repository: devsy-org/devsy

Length of output: 338


🏁 Script executed:

# Read the ToID function completely to see if it validates against path traversal
cat -n pkg/workspace/id.go | head -50

Repository: devsy-org/devsy

Length of output: 1751


🏁 Script executed:

# Check where context names come from - look for DefaultContext assignments
rg -n 'DefaultContext' --type go -B 2 -A 2 | head -40

Repository: devsy-org/devsy

Length of output: 2381


🏁 Script executed:

# Look for any path segment validation that might already exist
rg -n 'filepath.IsAbs|strings.Contains.*\.\.|path traversal|sanitize' --type go

Repository: devsy-org/devsy

Length of output: 2282


🏁 Script executed:

# Check how config is loaded and if DefaultContext is validated
rg -n 'DefaultContext.*=' --type go -B 3 -A 1 | head -50

Repository: devsy-org/devsy

Length of output: 2972


🏁 Script executed:

# Look at the config loading to see if context names are validated
rg -n 'type Config|Contexts\s*' --type go -A 3 | head -40

Repository: devsy-org/devsy

Length of output: 2447


🏁 Script executed:

# Check provider/download.go line 356 which mentions path traversal protection
sed -n '350,410p' pkg/provider/download.go

Repository: devsy-org/devsy

Length of output: 1846


🏁 Script executed:

# Check if context names are validated when read from config
rg -n 'yaml.*Unmarshal|json.*Unmarshal.*Config' --type go -A 10 | head -50

Repository: devsy-org/devsy

Length of output: 2485


🏁 Script executed:

# Check how contexts are created/set by users
rg -n 'context.*create|context.*add|SetContext' --type go -B 2 -A 3 | head -50

Repository: devsy-org/devsy

Length of output: 3051


🏁 Script executed:

# Let's verify: can ".." actually escape the directory with filepath.Join?
python3 << 'EOF'
import os
from pathlib import Path
# Test filepath.Join behavior with ".."
base = "/home/user/.config/devsy/data"
context_input = "mycontext/../../../etc"

# Simulate filepath.Join
result = os.path.join(base, "contexts", context_input)
print(f"filepath.Join result: {result}")

# What about Clean?
import os.path
cleaned = os.path.normpath(result)
print(f"After normpath: {cleaned}")

# Would this actually escape?
print(f"Escapes base? {not cleaned.startswith(base)}")
EOF

Repository: devsy-org/devsy

Length of output: 206


🏁 Script executed:

# Verify: does Go's filepath.Clean actually resolve ".." the way the review suggests?
python3 << 'EOF'
import os
import subprocess

# Test with actual Go to be sure
go_code = '''
package main
import (
	"fmt"
	"path/filepath"
)
func main() {
	base := "/home/user/.config/devsy/data"
	joined := filepath.Join(base, "contexts", "..", "..", "etc")
	fmt.Println("filepath.Join:", joined)
	
	// Check if path could escape
	abs, _ := filepath.Abs(joined)
	fmt.Println("After Abs:", abs)
}
'''

with open('/tmp/test_path.go', 'w') as f:
	f.write(go_code)

result = subprocess.run(['go', 'run', '/tmp/test_path.go'], capture_output=True, text=True)
print(result.stdout)
if result.stderr:
	print("Error:", result.stderr)
EOF

Repository: devsy-org/devsy

Length of output: 143


🏁 Script executed:

# Check if workspace IDs, context names are validated for path-unsafe characters during creation
rg -n 'func.*Create.*Machine|func.*Create.*Workspace|func.*Create.*Context' --type go -A 15 | head -60

Repository: devsy-org/devsy

Length of output: 4344


🏁 Script executed:

# Most importantly: check if context names can actually be set to something with ".." 
rg -n 'contexts\[.*\]\s*=' --type go -B 3 -A 1 | head -30

Repository: devsy-org/devsy

Length of output: 41


🏁 Script executed:

# Check if context names are user-editable or just "default"
rg -n 'AddContext|CreateContext|context.*add' --type go -B 2 -A 5

Repository: devsy-org/devsy

Length of output: 6064


🏁 Script executed:

# Check where providerName comes from in ProviderDir calls
rg -n 'ProviderDir\(' --type go -B 5 -A 2 | head -50

Repository: devsy-org/devsy

Length of output: 3174


🏁 Script executed:

# Look for any input validation on provider names
rg -n 'providerName.*valid|provider.*sanitize' --type go

Repository: devsy-org/devsy

Length of output: 246


🏁 Script executed:

# Find the provider rename validation logic
rg -n 'DevsyProviderRename|RenameProvider' --type go -B 2 -A 8 | head -60

Repository: devsy-org/devsy

Length of output: 3667


🏁 Script executed:

# Check what validation happens when a provider name is set
rg -n 'SetProvider|AddProvider' --type go -B 3 -A 5 | head -50

Repository: devsy-org/devsy

Length of output: 2444


🏁 Script executed:

# Check the test to see if "invalid/name6" is expected to fail
sed -n '400,420p' e2e/tests/provider/provider.go

Repository: devsy-org/devsy

Length of output: 747


🏁 Script executed:

# Find the validation that rejects "/" in provider names
rg -n 'invalid.*name|contains.*\/' --type go -B 3 -A 3 | grep -i provider | head -40

Repository: devsy-org/devsy

Length of output: 175


🏁 Script executed:

# Look for the rename command implementation
rg -n 'func.*Rename.*provider|cmd.*rename' --type go -B 2 -A 10 | head -80

Repository: devsy-org/devsy

Length of output: 41


🏁 Script executed:

# Search for provider name validation function
rg -n 'func.*[Vv]alid.*[Pp]rovider|provider.*[Vv]alid' --type go -A 5

Repository: devsy-org/devsy

Length of output: 7993


🏁 Script executed:

# Find ProviderNameRegEx definition
rg -n 'ProviderNameRegEx\s*=' --type go -B 2 -A 2

Repository: devsy-org/devsy

Length of output: 302


🏁 Script executed:

# Check if there's similar validation for context, workspaceID, machineID
rg -n 'func.*validateContext|func.*validateWorkspace|func.*validateMachine' --type go -A 5

Repository: devsy-org/devsy

Length of output: 41


🏁 Script executed:

# Look at the full rename command to understand the validation flow
sed -n '1,100p' cmd/provider/rename.go

Repository: devsy-org/devsy

Length of output: 2750


🏁 Script executed:

# Read the complete ToID function
cat -n pkg/workspace/id.go | head -80

Repository: devsy-org/devsy

Length of output: 1974


🏁 Script executed:

# Check what characters ToID filters out
rg -n 'workspaceIDRegEx' --type go -B 1 -A 1

Repository: devsy-org/devsy

Length of output: 449


🏁 Script executed:

# Verify: does ToID filter out ".." and "/"?
python3 << 'EOF'
import re

# From the code:
# workspaceIDRegEx1 = regexp.MustCompile(`[^\w\-]`)
# workspaceIDRegEx2 = regexp.MustCompile(`[^0-9a-z\-]+`)

workspaceIDRegEx1 = re.compile(r'[^\w\-]')  # matches anything that's NOT word char or dash
workspaceIDRegEx2 = re.compile(r'[^0-9a-z\-]+')  # matches anything that's NOT 0-9, a-z, or dash

test_inputs = ["..", "/", "my-workspace", "WORKSPACE", "my workspace"]

for test in test_inputs:
    match1 = workspaceIDRegEx1.search(test)
    match2 = workspaceIDRegEx2.search(test)
    print(f"{test:20} | Regex1 match: {bool(match1):5} | Regex2 match: {bool(match2):5}")
EOF

Repository: devsy-org/devsy

Length of output: 383


🏁 Script executed:

# Check if context names have ANY validation
rg -n 'context.*valid|validateContext' --type go

Repository: devsy-org/devsy

Length of output: 41


🏁 Script executed:

# Verify: are context names truly unvalidated and user-controllable?
rg -n 'DefaultContext' --type go -B 5 -A 2 | grep -A 5 -B 5 'yaml\|unmarshal'

Repository: devsy-org/devsy

Length of output: 1548


🏁 Script executed:

# Check if hashedID comes from user input or is internally generated
rg -n 'FeatureCacheDir\(' --type go -B 5 -A 2

Repository: devsy-org/devsy

Length of output: 1561


🏁 Script executed:

# Look for where proInstanceHost comes from
rg -n 'ProInstanceDir\(' --type go -B 8 -A 2 | head -60

Repository: devsy-org/devsy

Length of output: 2881


🏁 Script executed:

# Check process names - where do they come from?
rg -n 'ProcessPIDFile\(|ProcessLockFile\(|ProcessStreamsFile\(' --type go -B 5 -A 2

Repository: devsy-org/devsy

Length of output: 3040


🏁 Script executed:

# Find where proInstanceHost comes from - is it user input or system-managed?
rg -n 'ProInstance.*Host|proInstanceHost' --type go -B 3 -A 3 | head -80

Repository: devsy-org/devsy

Length of output: 3667


🏁 Script executed:

# Check if hashedID is user-controlled or internally generated
rg -n 'FeatureCacheDir\|hashedID' --type go -B 5 -A 3

Repository: devsy-org/devsy

Length of output: 41


🏁 Script executed:

# Get the complete picture of what's truly user-controlled vs validated
python3 << 'EOF'
summary = """
VERIFICATION SUMMARY:

1. CONTEXT NAMES:
   - Source: User-editable YAML config (yaml.Unmarshal in config.go:234)
   - Validation: NONE - no validation regex
   - Vulnerable: YES - could contain "..", "/", etc.

2. WORKSPACE IDs:
   - Source: User input BUT processed through ToID() function
   - Validation: YES - ToID uses regexes to filter out all non-alphanumeric except dashes
   - Vulnerable: NO - ToID cleans dangerous characters

3. MACHINE IDs:
   - Source: Similar to workspace IDs
   - Validation: YES - likely same cleaning
   - Vulnerable: NO

4. PROVIDER NAMES:
   - Source: User input via provider.yaml or CLI
   - Validation: YES - ProviderNameRegEx enforces [a-z0-9-] only
   - Vulnerable: NO

5. ProInstanceHost:
   - Source: Appears to be system-generated or from Pro service
   - Example: "pro.example.com"
   - Validation: UNKNOWN - need to check source

6. HashedID (FeatureCacheDir):
   - Source: Appears to be internally generated (hash)
   - Validation: LIKELY - hash format
   - Vulnerable: LIKELY NO

7. Process names:
   - Source: Mostly hardcoded constants (DaemonProcessName)
   - Validation: N/A - hardcoded
   - Vulnerable: NO
"""
print(summary)
EOF

Repository: devsy-org/devsy

Length of output: 1219


Add validation for unvalidated context names only; most other identifiers are already protected.

The ContextDir method does accept unvalidated context names from the YAML config, which could contain .. or / and escape the intended directory. However, most other identifiers listed are already validated:

  • WorkspaceDir, MachineDir: Use workspace/machine IDs processed through ToID(), which filters out non-alphanumeric characters (except dashes) via regex.
  • ProviderDir: Uses provider names validated by ProviderNameRegEx, which enforces [a-z0-9-] only (see cmd/provider/rename.go:73).
  • ProcessPIDFile, ProcessLockFile, ProcessStreamsFile: Use mostly hardcoded process names.
  • FeatureCacheDir, ProInstanceDir: Sources appear internal or system-generated.

The real gap is context names, which are user-editable in the YAML config and have no character restrictions. Add validation to reject .., /, \, and absolute paths in the ContextDir method (and any context parameter). The proposed fix's blanket approach would redundantly validate already-safe identifiers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/config/pathmanager.go` around lines 79 - 85, The ContextDir method
currently joins an unvalidated context string into the filesystem path, allowing
`..`, path separators, or absolute paths to escape the intended directory;
update the ContextDir(context string) (and any other locations that accept a
context parameter) to validate the context name first and return an error for
invalid values: reject strings that contain "..", "/" or "\" characters or that
are absolute paths (e.g., filepath.IsAbs(context)), and only allow a safe
character set (e.g., alphanumerics, dashes, underscores) before calling
filepath.Join; keep existing behavior for other identifiers (WorkspaceDir,
MachineDir, ProviderDir) which already use ToID()/ProviderNameRegEx.

}

func (b *basePathManager) WorkspacesDir(context string) (string, error) {
dir, err := b.ContextDir(context)
if err != nil {
return "", err
}

return filepath.Join(dir, "workspaces"), nil
}

func (b *basePathManager) WorkspaceDir(context, workspaceID string) (string, error) {
dir, err := b.WorkspacesDir(context)
if err != nil {
return "", err
}

return filepath.Join(dir, workspaceID), nil
}

func (b *basePathManager) MachinesDir(context string) (string, error) {
dir, err := b.ContextDir(context)
if err != nil {
return "", err
}

return filepath.Join(dir, "machines"), nil
}

func (b *basePathManager) MachineDir(context, machineID string) (string, error) {
dir, err := b.MachinesDir(context)
if err != nil {
return "", err
}

return filepath.Join(dir, machineID), nil
}

func (b *basePathManager) ProvidersDir(context string) (string, error) {
dir, err := b.ContextDir(context)
if err != nil {
return "", err
}

return filepath.Join(dir, "providers"), nil
}

func (b *basePathManager) ProviderDir(context, providerName string) (string, error) {
dir, err := b.ProvidersDir(context)
if err != nil {
return "", err
}

return filepath.Join(dir, providerName), nil
}

func (b *basePathManager) ProviderBinariesDir(context, providerName string) (string, error) {
dir, err := b.ProviderDir(context, providerName)
if err != nil {
return "", err
}

return filepath.Join(dir, "binaries"), nil
}

func (b *basePathManager) ProviderDaemonDir(context, providerName string) (string, error) {
dir, err := b.ProviderDir(context, providerName)
if err != nil {
return "", err
}

return filepath.Join(dir, "daemon"), nil
}

func (b *basePathManager) ProInstancesDir(context string) (string, error) {
dir, err := b.ContextDir(context)
if err != nil {
return "", err
}

return filepath.Join(dir, "pro_instances"), nil
}

func (b *basePathManager) ProInstanceDir(context, proInstanceHost string) (string, error) {
dir, err := b.ProInstancesDir(context)
if err != nil {
return "", err
}

return filepath.Join(dir, proInstanceHost), nil
}

func (b *basePathManager) LocksDir(context string) (string, error) {
dir, err := b.ContextDir(context)
if err != nil {
return "", err
}

return filepath.Join(dir, "locks"), nil
}

// --- Cache sub-paths ---

func (b *basePathManager) AgentCacheDir() (string, error) {
dir, err := b.pm.CacheDir()
if err != nil {
return "", err
}

return filepath.Join(dir, "agents"), nil
}

func (b *basePathManager) ProviderDownloadCacheDir() (string, error) {
dir, err := b.pm.CacheDir()
if err != nil {
return "", err
}

return filepath.Join(dir, "providers"), nil
}

func (b *basePathManager) FeatureCacheDir(hashedID string) (string, error) {
dir, err := b.pm.CacheDir()
if err != nil {
return "", err
}

return filepath.Join(dir, "features", hashedID), nil
}

func (b *basePathManager) PlatformCacheDir() (string, error) {
dir, err := b.pm.CacheDir()
if err != nil {
return "", err
}

return filepath.Join(dir, "platform"), nil
}

func (b *basePathManager) SSHKeysDir() (string, error) {
dir, err := b.pm.CacheDir()
if err != nil {
return "", err
}

return filepath.Join(dir, "keys"), nil
}

// --- Runtime sub-paths ---

func (b *basePathManager) DaemonPIDFile() (string, error) {
dir, err := b.pm.RuntimeDir()
if err != nil {
return "", err
}

return filepath.Join(dir, DaemonProcessName+".pid"), nil
}

func (b *basePathManager) DaemonLockFile() (string, error) {
dir, err := b.pm.RuntimeDir()
if err != nil {
return "", err
}

return filepath.Join(dir, DaemonProcessName+".lock"), nil
}

func (b *basePathManager) DaemonStreamsFile() (string, error) {
dir, err := b.pm.RuntimeDir()
if err != nil {
return "", err
}

return filepath.Join(dir, DaemonProcessName+".streams"), nil
}

func (b *basePathManager) ProcessPIDFile(name string) (string, error) {
dir, err := b.pm.RuntimeDir()
if err != nil {
return "", err
}

return filepath.Join(dir, name+".pid"), nil
}

func (b *basePathManager) ProcessLockFile(name string) (string, error) {
dir, err := b.pm.RuntimeDir()
if err != nil {
return "", err
}

return filepath.Join(dir, name+".lock"), nil
}

func (b *basePathManager) ProcessStreamsFile(name string) (string, error) {
dir, err := b.pm.RuntimeDir()
if err != nil {
return "", err
}

return filepath.Join(dir, name+".streams"), nil
}

// --- State sub-paths ---

func (b *basePathManager) LogDir() (string, error) {
dir, err := b.pm.StateDir()
if err != nil {
return "", err
}

return filepath.Join(dir, "logs"), nil
}

// --- Singleton management ---

var (
defaultPM PathManager
defaultPMOnce sync.Once
defaultPMMu sync.Mutex
)

// NewPathManager returns a new PathManager for the current platform.
func NewPathManager() PathManager {
return newPlatformPathManager()
}

// DefaultPathManager returns the process-wide singleton PathManager.
func DefaultPathManager() PathManager {
defaultPMMu.Lock()
defer defaultPMMu.Unlock()

defaultPMOnce.Do(func() {
defaultPM = NewPathManager()
})

return defaultPM
}

// SetPathManager replaces the singleton PathManager (for testing).
func SetPathManager(pm PathManager) {
defaultPMMu.Lock()
defer defaultPMMu.Unlock()

defaultPM = pm
// Mark the Once as done so future DefaultPathManager() calls return pm.
defaultPMOnce.Do(func() {})
Comment on lines +343 to +349

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Reject nil singleton overrides.

SetPathManager(nil) marks the singleton initialized with a nil manager; later DefaultPathManager().ConfigDir()-style calls can panic. Guard this exported test hook.

Proposed fix
 func SetPathManager(pm PathManager) {
+	if pm == nil {
+		panic("config: SetPathManager called with nil PathManager")
+	}
+
 	defaultPMMu.Lock()
 	defer defaultPMMu.Unlock()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func SetPathManager(pm PathManager) {
defaultPMMu.Lock()
defer defaultPMMu.Unlock()
defaultPM = pm
// Mark the Once as done so future DefaultPathManager() calls return pm.
defaultPMOnce.Do(func() {})
func SetPathManager(pm PathManager) {
if pm == nil {
panic("config: SetPathManager called with nil PathManager")
}
defaultPMMu.Lock()
defer defaultPMMu.Unlock()
defaultPM = pm
// Mark the Once as done so future DefaultPathManager() calls return pm.
defaultPMOnce.Do(func() {})
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/config/pathmanager.go` around lines 327 - 333, SetPathManager currently
allows setting defaultPM to nil which marks defaultPMOnce as done and causes
panics later; add a nil guard at the top of SetPathManager (before modifying
defaultPM or calling defaultPMOnce.Do) that rejects nil inputs (preferably panic
with a clear message like "SetPathManager received nil" or return an error if
you change the API), so defaultPM and defaultPMOnce are not modified when pm ==
nil; keep defaultPMMu locking around the mutation as-is and reference
SetPathManager, defaultPM, defaultPMOnce and DefaultPathManager in your change.

}

// ResetPathManager clears the singleton so the next DefaultPathManager call
// creates a fresh instance. Intended for tests only.
func ResetPathManager() {
defaultPMMu.Lock()
defer defaultPMMu.Unlock()

defaultPM = nil
defaultPMOnce = sync.Once{}
}
Loading
Loading