feat: modify ExecTool to check PermissionCache before executing outside-workspace commands
This commit is contained in:
parent
d9c55dc939
commit
9ac94f32cf
2 changed files with 99 additions and 0 deletions
|
|
@ -21,6 +21,7 @@ import (
|
||||||
"github.com/sipeed/picoclaw/pkg/config"
|
"github.com/sipeed/picoclaw/pkg/config"
|
||||||
"github.com/sipeed/picoclaw/pkg/constants"
|
"github.com/sipeed/picoclaw/pkg/constants"
|
||||||
"github.com/sipeed/picoclaw/pkg/isolation"
|
"github.com/sipeed/picoclaw/pkg/isolation"
|
||||||
|
"github.com/sipeed/picoclaw/pkg/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|
@ -44,6 +45,8 @@ type ExecTool struct {
|
||||||
restrictToWorkspace bool
|
restrictToWorkspace bool
|
||||||
allowRemote bool
|
allowRemote bool
|
||||||
sessionManager *SessionManager
|
sessionManager *SessionManager
|
||||||
|
permissionCache *PermissionCache
|
||||||
|
askPermission bool
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|
@ -235,12 +238,71 @@ func (t *ExecTool) Parameters() map[string]any {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *ExecTool) checkPermission(command string) string {
|
||||||
|
if !t.askPermission {
|
||||||
|
return "granted"
|
||||||
|
}
|
||||||
|
|
||||||
|
path := t.extractPathFromCommand(command)
|
||||||
|
if path == "" {
|
||||||
|
return "granted"
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.restrictToWorkspace && t.isOutsideWorkspace(path) {
|
||||||
|
if perm := t.permissionCache.Check(path); perm != "" {
|
||||||
|
if perm == "denied" {
|
||||||
|
return "denied"
|
||||||
|
}
|
||||||
|
return "granted"
|
||||||
|
}
|
||||||
|
return "needs_permission"
|
||||||
|
}
|
||||||
|
|
||||||
|
return "granted"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *ExecTool) isOutsideWorkspace(path string) bool {
|
||||||
|
isAbs := filepath.IsAbs(path) || strings.HasPrefix(path, "/")
|
||||||
|
if isAbs {
|
||||||
|
absWorkspace, _ := filepath.Abs(t.workingDir)
|
||||||
|
absPath, _ := filepath.Abs(path)
|
||||||
|
return !strings.HasPrefix(absPath, absWorkspace)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *ExecTool) extractPathFromCommand(command string) string {
|
||||||
|
parts := strings.Fields(command)
|
||||||
|
for _, part := range parts {
|
||||||
|
if filepath.IsAbs(part) || strings.HasPrefix(part, "/") {
|
||||||
|
return part
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
func (t *ExecTool) Execute(ctx context.Context, args map[string]any) *ToolResult {
|
func (t *ExecTool) Execute(ctx context.Context, args map[string]any) *ToolResult {
|
||||||
action, _ := args["action"].(string)
|
action, _ := args["action"].(string)
|
||||||
if action == "" {
|
if action == "" {
|
||||||
return ErrorResult("action is required")
|
return ErrorResult("action is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if action == "run" {
|
||||||
|
command, _ := args["command"].(string)
|
||||||
|
switch t.checkPermission(command) {
|
||||||
|
case "needs_permission":
|
||||||
|
path := t.extractPathFromCommand(command)
|
||||||
|
logger.InfoCF("exec", "Permission needed", map[string]any{"command": command, "path": path})
|
||||||
|
return &ToolResult{
|
||||||
|
ForLLM: fmt.Sprintf("Permission needed for path: %s. Call request_permission tool with path='%s'.", path, path),
|
||||||
|
ForUser: fmt.Sprintf("⚠️ Permission required to access %s", path),
|
||||||
|
}
|
||||||
|
case "denied":
|
||||||
|
path := t.extractPathFromCommand(command)
|
||||||
|
return ErrorResult(fmt.Sprintf("Access to %s was denied", path))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
switch action {
|
switch action {
|
||||||
case "run":
|
case "run":
|
||||||
return t.executeRun(ctx, args)
|
return t.executeRun(ctx, args)
|
||||||
|
|
|
||||||
|
|
@ -1613,3 +1613,40 @@ func TestEncodeKeyTokenWithPtyKeyMode(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestExecTool_CheckPermission(t *testing.T) {
|
||||||
|
pc := NewPermissionCache()
|
||||||
|
|
||||||
|
tool := &ExecTool{
|
||||||
|
permissionCache: pc,
|
||||||
|
askPermission: true,
|
||||||
|
workingDir: "/workspace",
|
||||||
|
restrictToWorkspace: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 1: command with outside-workspace path
|
||||||
|
result := tool.checkPermission("ls /desktop")
|
||||||
|
if result != "needs_permission" {
|
||||||
|
t.Errorf("Expected 'needs_permission', got %s", result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 2: command with workspace path
|
||||||
|
result = tool.checkPermission("ls /workspace/folder")
|
||||||
|
if result != "granted" {
|
||||||
|
t.Errorf("Expected 'granted' for workspace path, got %s", result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 3: permission denied
|
||||||
|
pc.Grant("/desktop", "denied")
|
||||||
|
result = tool.checkPermission("ls /desktop")
|
||||||
|
if result != "denied" {
|
||||||
|
t.Errorf("Expected 'denied', got %s", result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 4: permission granted (session)
|
||||||
|
pc.Grant("/desktop", "session")
|
||||||
|
result = tool.checkPermission("ls /desktop")
|
||||||
|
if result != "granted" {
|
||||||
|
t.Errorf("Expected 'granted' for session permission, got %s", result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue