mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-06-10 10:10:49 +00:00
2504095dfe
ShellDenyGroups was defined in SystemPromptConfig but lacked full propagation through parser, Loop fields, context injection, and system prompt population. Per-agent overrides from other_config JSONB had zero runtime effect. Changes: - agent_store.go: Add ParseShellDenyGroups() to extract overrides from JSONB - loop_types.go: Add shellDenyGroups field to Loop and LoopConfig, wire in NewLoop - resolver.go: Wire agent-parsed shell deny groups into LoopConfig - loop.go: Inject shellDenyGroups into context via store.WithShellDenyGroups - loop_history.go: Populate ShellDenyGroups in system prompt config - message_test.go: Fix macOS symlink path normalization in test expectations Fixes test failures on macOS where /var/folders symlinks to /private/var/folders.
133 lines
4.0 KiB
Go
133 lines
4.0 KiB
Go
package tools
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
func TestResolveMediaPath(t *testing.T) {
|
|
tmpDir := os.TempDir()
|
|
|
|
// Create a temp workspace with a test file for workspace-relative tests.
|
|
workspace := t.TempDir()
|
|
docsDir := filepath.Join(workspace, "docs")
|
|
if err := os.MkdirAll(docsDir, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
testFile := filepath.Join(docsDir, "report.pdf")
|
|
if err := os.WriteFile(testFile, []byte("test"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Normalize paths to canonical form (resolves macOS /var/folders → /private/var/folders symlink).
|
|
// The resolvePath function uses filepath.EvalSymlinks, so test expectations must too.
|
|
testFileCanonical, _ := filepath.EvalSymlinks(testFile)
|
|
workspaceCanonical, _ := filepath.EvalSymlinks(workspace)
|
|
|
|
t.Run("restricted", func(t *testing.T) {
|
|
tool := NewMessageTool(workspaceCanonical, true)
|
|
ctx := context.Background()
|
|
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
want string
|
|
wantOK bool
|
|
}{
|
|
// /tmp/ always allowed
|
|
{"valid temp file", "MEDIA:" + filepath.Join(tmpDir, "test.png"), filepath.Join(tmpDir, "test.png"), true},
|
|
{"valid nested temp", "MEDIA:" + filepath.Join(tmpDir, "sub", "file.txt"), filepath.Join(tmpDir, "sub", "file.txt"), true},
|
|
|
|
// Workspace files allowed
|
|
{"workspace absolute", "MEDIA:" + testFileCanonical, testFileCanonical, true},
|
|
{"workspace relative", "MEDIA:docs/report.pdf", testFileCanonical, true},
|
|
|
|
// Not a MEDIA: message
|
|
{"no prefix", filepath.Join(tmpDir, "test.png"), "", false},
|
|
{"empty after prefix", "MEDIA:", "", false},
|
|
{"dot path", "MEDIA:.", "", false},
|
|
{"empty string", "", "", false},
|
|
{"just MEDIA", "MEDIA", "", false},
|
|
|
|
// Outside workspace + outside /tmp/ → blocked
|
|
{"outside workspace", "MEDIA:/etc/passwd", "", false},
|
|
{"traversal attack", "MEDIA:" + filepath.Join(workspaceCanonical, "..", "etc", "passwd"), "", false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got, ok := tool.resolveMediaPath(ctx, tt.input)
|
|
if ok != tt.wantOK {
|
|
t.Errorf("resolveMediaPath(%q) ok = %v, want %v", tt.input, ok, tt.wantOK)
|
|
}
|
|
if ok && got != tt.want {
|
|
t.Errorf("resolveMediaPath(%q) = %q, want %q", tt.input, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("unrestricted", func(t *testing.T) {
|
|
tool := NewMessageTool(workspace, false)
|
|
ctx := context.Background()
|
|
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
wantOK bool
|
|
}{
|
|
{"any absolute path", "MEDIA:/etc/hostname", true},
|
|
{"workspace relative", "MEDIA:docs/report.pdf", true},
|
|
{"temp file", "MEDIA:" + filepath.Join(tmpDir, "test.png"), true},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
_, ok := tool.resolveMediaPath(ctx, tt.input)
|
|
if ok != tt.wantOK {
|
|
t.Errorf("resolveMediaPath(%q) ok = %v, want %v", tt.input, ok, tt.wantOK)
|
|
}
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("context workspace override", func(t *testing.T) {
|
|
// Tool has no workspace, but context provides one.
|
|
tool := NewMessageTool("", true)
|
|
ctx := WithToolWorkspace(context.Background(), workspaceCanonical)
|
|
|
|
got, ok := tool.resolveMediaPath(ctx, "MEDIA:docs/report.pdf")
|
|
if !ok {
|
|
t.Fatal("expected ok=true for workspace-relative path with context workspace")
|
|
}
|
|
if got != testFileCanonical {
|
|
t.Errorf("got %q, want %q", got, testFileCanonical)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestIsInTempDir(t *testing.T) {
|
|
tmpDir := os.TempDir()
|
|
tests := []struct {
|
|
name string
|
|
path string
|
|
want bool
|
|
}{
|
|
{"in tmp", filepath.Join(tmpDir, "test.png"), true},
|
|
{"nested in tmp", filepath.Join(tmpDir, "sub", "file.txt"), true},
|
|
{"tmp itself", tmpDir, false}, // only files inside, not the dir itself
|
|
{"outside tmp", "/etc/passwd", false},
|
|
{"relative path", "relative/path.txt", false},
|
|
{"traversal", filepath.Join(tmpDir, "..", "etc", "passwd"), false},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if got := isInTempDir(tt.path); got != tt.want {
|
|
t.Errorf("isInTempDir(%q) = %v, want %v", tt.path, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|