Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion cmd/root/acp.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (

"github.com/docker/docker-agent/pkg/acp"
"github.com/docker/docker-agent/pkg/config"
pathx "github.com/docker/docker-agent/pkg/path"
"github.com/docker/docker-agent/pkg/paths"
"github.com/docker/docker-agent/pkg/telemetry"
)
Expand Down Expand Up @@ -46,7 +47,7 @@ func (f *acpFlags) runACPCommand(cmd *cobra.Command, args []string) (commandErr
agentFilename := args[0]

// Expand tilde in session database path
sessionDB, err := expandTilde(f.sessionDB)
sessionDB, err := pathx.ExpandHomeDir(f.sessionDB)
if err != nil {
return err
}
Expand Down
21 changes: 2 additions & 19 deletions cmd/root/alias.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package root

import (
"errors"
"fmt"
"maps"
"path/filepath"
Expand All @@ -13,7 +12,7 @@ import (

"github.com/docker/docker-agent/pkg/cli"
"github.com/docker/docker-agent/pkg/config"
"github.com/docker/docker-agent/pkg/paths"
pathx "github.com/docker/docker-agent/pkg/path"
"github.com/docker/docker-agent/pkg/telemetry"
"github.com/docker/docker-agent/pkg/userconfig"
)
Expand Down Expand Up @@ -121,14 +120,12 @@ func runAliasAddCommand(cmd *cobra.Command, args []string, flags *aliasAddFlags)
name := args[0]
agentPath := args[1]

// Load existing config
cfg, err := userconfig.Load()
if err != nil {
return fmt.Errorf("failed to load config: %w", err)
}

// Expand tilde in path if present
absAgentPath, err := expandTilde(agentPath)
absAgentPath, err := pathx.ExpandHomeDir(agentPath)
if err != nil {
return err
}
Expand Down Expand Up @@ -265,17 +262,3 @@ func runAliasRemoveCommand(cmd *cobra.Command, args []string) (commandErr error)
out.Printf("Alias '%s' removed successfully\n", name)
return nil
}

// expandTilde expands the tilde in a path to the user's home directory
func expandTilde(path string) (string, error) {
if !strings.HasPrefix(path, "~/") {
return path, nil
}

homeDir := paths.GetHomeDir()
if homeDir == "" {
return "", errors.New("failed to get user home directory")
}

return filepath.Join(homeDir, strings.TrimPrefix(path, "~/")), nil
}
3 changes: 2 additions & 1 deletion cmd/root/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (

"github.com/docker/docker-agent/pkg/cli"
"github.com/docker/docker-agent/pkg/config"
pathx "github.com/docker/docker-agent/pkg/path"
"github.com/docker/docker-agent/pkg/server"
"github.com/docker/docker-agent/pkg/session"
"github.com/docker/docker-agent/pkg/telemetry"
Expand Down Expand Up @@ -98,7 +99,7 @@ func (f *apiFlags) runAPICommand(cmd *cobra.Command, args []string) (commandErr
slog.DebugContext(ctx, "Starting server", "agents", agentsPath, "addr", ln.Addr().String())

// Expand tilde in session database path
sessionDB, err := expandTilde(f.sessionDB)
sessionDB, err := pathx.ExpandHomeDir(f.sessionDB)
if err != nil {
return err
}
Expand Down
4 changes: 2 additions & 2 deletions cmd/root/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"github.com/docker/docker-agent/pkg/app"
"github.com/docker/docker-agent/pkg/cli"
"github.com/docker/docker-agent/pkg/config"
pathx "github.com/docker/docker-agent/pkg/path"
"github.com/docker/docker-agent/pkg/paths"
"github.com/docker/docker-agent/pkg/permissions"
"github.com/docker/docker-agent/pkg/profiling"
Expand Down Expand Up @@ -344,8 +345,7 @@ func (f *runExecFlags) createLocalRuntimeAndSession(ctx context.Context, loadRes
}
agentName := agt.Name()

// Expand tilde in session database path
sessionDB, err := expandTilde(f.sessionDB)
sessionDB, err := pathx.ExpandHomeDir(f.sessionDB)
if err != nil {
return nil, nil, err
}
Expand Down
37 changes: 10 additions & 27 deletions pkg/environment/env_files.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (
"strings"

"github.com/docker/docker-agent/pkg/path"
"github.com/docker/docker-agent/pkg/paths"
)

type KeyValuePair struct {
Expand All @@ -31,9 +30,16 @@ func AbsolutePaths(parentDir string, relOrAbsPaths []string) ([]string, error) {
}

func AbsolutePath(parentDir, relOrAbsPath string) (string, error) {
p, err := expandTildePath(relOrAbsPath)
if err != nil {
return "", err
p := relOrAbsPath
if strings.HasPrefix(p, "~") {
expanded, err := path.ExpandHomeDir(p)
if err != nil {
return "", err
}
if expanded == p && p != "~" {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[LOW] Fragile tilde-detection via identity comparison in AbsolutePath

The check if expanded == p && p != "~" detects unsupported tilde forms like ~user/foo by relying on ExpandHomeDir returning the input unchanged when it doesn't expand it. This is correct today, but it is brittle: if a future change to ExpandHomeDir normalises or cleans an unrecognised tilde path before returning (e.g. calls filepath.Clean), the check would silently pass and fall through to path resolution with a literal ~ in the path.

Consider adding an explicit check instead:

if strings.HasPrefix(p, "~") && !strings.HasPrefix(p, "~/") && p != "~" {
    return "", fmt.Errorf("unsupported tilde expansion format: %s", p)
}

This makes the intent clear regardless of what ExpandHomeDir does internally.

return "", fmt.Errorf("unsupported tilde expansion format: %s", p)
}
p = expanded
}

// For absolute paths (including tilde-expanded ones), validate against directory traversal
Expand All @@ -52,29 +58,6 @@ func AbsolutePath(parentDir, relOrAbsPath string) (string, error) {
return validatedPath, nil
}

// expandTildePath expands ~ in file paths to the user's home directory
func expandTildePath(p string) (string, error) {
if !strings.HasPrefix(p, "~") {
return p, nil
}

homeDir := paths.GetHomeDir()
if homeDir == "" {
return "", errors.New("failed to get user home directory")
}

if p == "~" {
return homeDir, nil
}

if strings.HasPrefix(p, "~/") {
return filepath.Join(homeDir, p[2:]), nil
}

// Handle ~username/ format - not commonly supported in this context
return "", fmt.Errorf("unsupported tilde expansion format: %s", p)
}

func ReadEnvFiles(absolutePaths []string) ([]KeyValuePair, error) {
if len(absolutePaths) == 0 {
return nil, nil
Expand Down
16 changes: 3 additions & 13 deletions pkg/path/expand.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
package path

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

// ExpandPath expands shell-like patterns in a file path:
// - ~ or ~/ at the start is replaced with the user's home directory
Expand All @@ -17,14 +13,8 @@ func ExpandPath(p string) string {
// Expand environment variables
p = os.ExpandEnv(p)

// Expand tilde to home directory
if p == "~" || strings.HasPrefix(p, "~/") || strings.HasPrefix(p, "~"+string(filepath.Separator)) {
if home, err := os.UserHomeDir(); err == nil {
if p == "~" {
return home
}
return filepath.Join(home, p[2:])
}
if expanded, err := ExpandHomeDir(p); err == nil {
return expanded
}

return p
Expand Down
32 changes: 32 additions & 0 deletions pkg/path/home.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package path

import (
"errors"
"os"
"path/filepath"
"strings"
)

// ExpandHomeDir expands a leading home-directory reference in a path.
// It expands "~", "~/...", and "~\\..." on Windows. Other tilde forms,
// such as "~user/...", are returned unchanged.
func ExpandHomeDir(path string) (string, error) {
if !isHomeDirPath(path) {
return path, nil
}

homeDir, err := os.UserHomeDir()
if err != nil || homeDir == "" {
return "", errors.New("failed to get user home directory")
}

if path == "~" {
return filepath.Clean(homeDir), nil
}

return filepath.Join(homeDir, path[2:]), nil
}

func isHomeDirPath(path string) bool {
return path == "~" || strings.HasPrefix(path, "~/") || (filepath.Separator == '\\' && strings.HasPrefix(path, `~\`))
}
48 changes: 21 additions & 27 deletions cmd/root/tilde_test.go → pkg/path/home_test.go
Original file line number Diff line number Diff line change
@@ -1,89 +1,83 @@
package root
package path

import (
"path/filepath"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/docker/docker-agent/pkg/paths"
)

func TestExpandTilde(t *testing.T) {
t.Parallel()

homeDir := paths.GetHomeDir()
require.NotEmpty(t, homeDir, "Home directory should be available for tests")
func TestExpandHomeDir(t *testing.T) {
homeDir := t.TempDir()
t.Setenv("HOME", homeDir)
t.Setenv("USERPROFILE", homeDir)

tests := []struct {
name string
input string
expected string
wantErr bool
}{
{
name: "expands_tilde_prefix",
name: "tilde only",
input: "~",
expected: homeDir,
},
{
name: "expands tilde prefix",
input: "~/session.db",
expected: filepath.Join(homeDir, "session.db"),
},
{
name: "expands_tilde_with_nested_path",
name: "expands tilde with nested path",
input: "~/.cagent/session.db",
expected: filepath.Join(homeDir, ".cagent", "session.db"),
},
{
name: "expands_tilde_with_deep_path",
name: "expands tilde with deep path",
input: "~/path/to/some/file.db",
expected: filepath.Join(homeDir, "path", "to", "some", "file.db"),
},
{
name: "absolute_path_unchanged",
name: "absolute path unchanged",
input: "/absolute/path/session.db",
expected: "/absolute/path/session.db",
},
{
name: "relative_path_unchanged",
name: "relative path unchanged",
input: "relative/path/session.db",
expected: "relative/path/session.db",
},
{
name: "tilde_in_middle_unchanged",
name: "tilde in middle unchanged",
input: "/some/~/path/session.db",
expected: "/some/~/path/session.db",
},
{
name: "tilde_without_slash_unchanged",
name: "tilde without separator unchanged",
input: "~something",
expected: "~something",
},
{
name: "just_tilde_slash_expands",
name: "tilde slash expands",
input: "~/",
expected: homeDir,
},
{
name: "empty_string_unchanged",
name: "empty string unchanged",
input: "",
expected: "",
},
{
name: "dot_path_unchanged",
name: "dot path unchanged",
input: "./session.db",
expected: "./session.db",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

result, err := expandTilde(tt.input)

if tt.wantErr {
require.Error(t, err)
return
}
result, err := ExpandHomeDir(tt.input)

require.NoError(t, err)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[LOW] Error path of ExpandHomeDir is not tested

The test suite covers all the happy-path expansions, but never exercises the case where os.UserHomeDir() fails (e.g. HOME and USERPROFILE are both unset). This means the error message "failed to get user home directory" and the homeDir == "" guard are untested.

Consider adding a test that unsets both env vars and asserts that ExpandHomeDir("~/foo") returns an error.

assert.Equal(t, tt.expected, result)
Expand Down
25 changes: 4 additions & 21 deletions pkg/tools/builtin/filesystem/filesystem.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (

"github.com/docker/docker-agent/pkg/chat"
"github.com/docker/docker-agent/pkg/fsx"
pathx "github.com/docker/docker-agent/pkg/path"
"github.com/docker/docker-agent/pkg/tools"
)

Expand Down Expand Up @@ -500,34 +501,16 @@ func (t *Tool) executePostEditCommands(ctx context.Context, filePath string) err
// [resolveAndCheckPath] when those checks are required (i.e. for any path
// that originates from a tool argument).
func (t *Tool) resolvePath(path string) string {
path = expandHomeDir(path)
if expandedPath, err := pathx.ExpandHomeDir(path); err == nil {
path = expandedPath
}
if filepath.IsAbs(path) {
return filepath.Clean(path)
}

return filepath.Clean(filepath.Join(t.workingDir, path))
}

// expandHomeDir replaces a leading "~" or "~/" (or "~\\" on Windows) with
// the user's home directory. Returns the path unchanged when it isn't a
// home-relative reference or when the home directory cannot be determined;
// the caller will then resolve it as a literal name and likely surface a
// "not found" error, which is preferable to a hard failure inside path
// resolution.
func expandHomeDir(path string) string {
if path != "~" && !strings.HasPrefix(path, "~/") && !strings.HasPrefix(path, "~"+string(filepath.Separator)) {
return path
}
home, err := os.UserHomeDir()
if err != nil {
return path
}
if path == "~" {
return home
}
return filepath.Join(home, path[2:])
}

// resolveAndCheckPath is the canonical entry point used by every filesystem
// handler that operates on a user-supplied path. It resolves the path against
// the working directory and validates the result against the allow- and
Expand Down
Loading
Loading