let workspace to support multiple paths, "workspace": "~/.picoclaw/workspace|path2....|path3.......",
This commit is contained in:
parent
6b429de927
commit
3dfbc00559
6 changed files with 112 additions and 37 deletions
|
|
@ -78,10 +78,11 @@ func gatewayCmd() {
|
|||
|
||||
// Setup cron tool and service
|
||||
execTimeout := time.Duration(cfg.Tools.Cron.ExecTimeoutMinutes) * time.Minute
|
||||
combinedWorkspace := cfg.WorkspacePath()
|
||||
cronService := setupCronTool(
|
||||
agentLoop,
|
||||
msgBus,
|
||||
cfg.WorkspacePath(),
|
||||
combinedWorkspace,
|
||||
cfg.Agents.Defaults.RestrictToWorkspace,
|
||||
execTimeout,
|
||||
cfg,
|
||||
|
|
@ -224,12 +225,13 @@ func gatewayCmd() {
|
|||
func setupCronTool(
|
||||
agentLoop *agent.AgentLoop,
|
||||
msgBus *bus.MessageBus,
|
||||
workspace string,
|
||||
workspace string, // workspace can contain '|' for allowed paths
|
||||
restrict bool,
|
||||
execTimeout time.Duration,
|
||||
cfg *config.Config,
|
||||
) *cron.CronService {
|
||||
cronStorePath := filepath.Join(workspace, "cron", "jobs.json")
|
||||
primaryWorkspace := strings.Split(workspace, "|")[0]
|
||||
cronStorePath := filepath.Join(primaryWorkspace, "cron", "jobs.json")
|
||||
|
||||
// Create cron service
|
||||
cronService := cron.NewCronService(cronStorePath, nil)
|
||||
|
|
|
|||
|
|
@ -30,6 +30,9 @@ func getGlobalConfigDir() string {
|
|||
}
|
||||
|
||||
func NewContextBuilder(workspace string) *ContextBuilder {
|
||||
// workspace can be a multiplexed string separated by '|'
|
||||
primaryWorkspace := strings.Split(workspace, "|")[0]
|
||||
|
||||
// builtin skills: skills directory in current project
|
||||
// Use the skills/ directory under the current working directory
|
||||
wd, _ := os.Getwd()
|
||||
|
|
@ -38,8 +41,8 @@ func NewContextBuilder(workspace string) *ContextBuilder {
|
|||
|
||||
return &ContextBuilder{
|
||||
workspace: workspace,
|
||||
skillsLoader: skills.NewSkillsLoader(workspace, globalSkillsDir, builtinSkillsDir),
|
||||
memory: NewMemoryStore(workspace),
|
||||
skillsLoader: skills.NewSkillsLoader(primaryWorkspace, globalSkillsDir, builtinSkillsDir),
|
||||
memory: NewMemoryStore(primaryWorkspace),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -50,12 +53,25 @@ func (cb *ContextBuilder) SetToolsRegistry(registry *tools.ToolRegistry) {
|
|||
|
||||
func (cb *ContextBuilder) getIdentity() string {
|
||||
now := time.Now().Format("2006-01-02 15:04 (Monday)")
|
||||
workspacePath, _ := filepath.Abs(filepath.Join(cb.workspace))
|
||||
|
||||
allowedPaths := strings.Split(cb.workspace, "|")
|
||||
primaryWorkspace := allowedPaths[0]
|
||||
workspacePath, _ := filepath.Abs(primaryWorkspace)
|
||||
runtime := fmt.Sprintf("%s %s, Go %s", runtime.GOOS, runtime.GOARCH, runtime.Version())
|
||||
|
||||
// Build tools section dynamically
|
||||
toolsSection := cb.buildToolsSection()
|
||||
|
||||
var authorizedDirs strings.Builder
|
||||
for i, p := range allowedPaths {
|
||||
absP, _ := filepath.Abs(p)
|
||||
if i == 0 {
|
||||
authorizedDirs.WriteString(fmt.Sprintf("- %s (Primary Workspace)\n", absP))
|
||||
} else {
|
||||
authorizedDirs.WriteString(fmt.Sprintf("- %s (Authorized Directory)\n", absP))
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Sprintf(`# picoclaw 🦞
|
||||
|
||||
You are picoclaw, a helpful AI assistant.
|
||||
|
|
@ -67,7 +83,10 @@ You are picoclaw, a helpful AI assistant.
|
|||
%s
|
||||
|
||||
## Workspace
|
||||
Your workspace is at: %s
|
||||
Your primary workspace is at: %s
|
||||
You are authorized to access and operate in the following directories:
|
||||
%s
|
||||
|
||||
- Memory: %s/memory/MEMORY.md
|
||||
- Daily Notes: %s/memory/YYYYMM/YYYYMMDD.md
|
||||
- Skills: %s/skills/{skill-name}/SKILL.md
|
||||
|
|
@ -81,7 +100,7 @@ Your workspace is at: %s
|
|||
2. **Be helpful and accurate** - When using tools, briefly explain what you're doing.
|
||||
|
||||
3. **Memory** - When interacting with me if something seems memorable, update %s/memory/MEMORY.md`,
|
||||
now, runtime, workspacePath, workspacePath, workspacePath, workspacePath, toolsSection, workspacePath)
|
||||
now, runtime, workspacePath, authorizedDirs.String(), workspacePath, workspacePath, workspacePath, toolsSection, workspacePath)
|
||||
}
|
||||
|
||||
func (cb *ContextBuilder) buildToolsSection() string {
|
||||
|
|
@ -148,9 +167,11 @@ func (cb *ContextBuilder) LoadBootstrapFiles() string {
|
|||
"IDENTITY.md",
|
||||
}
|
||||
|
||||
primaryWorkspace := strings.Split(cb.workspace, "|")[0]
|
||||
|
||||
var sb strings.Builder
|
||||
for _, filename := range bootstrapFiles {
|
||||
filePath := filepath.Join(cb.workspace, filename)
|
||||
filePath := filepath.Join(primaryWorkspace, filename)
|
||||
if data, err := os.ReadFile(filePath); err == nil {
|
||||
fmt.Fprintf(&sb, "## %s\n\n%s\n\n", filename, data)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,7 +41,10 @@ func NewAgentInstance(
|
|||
provider providers.LLMProvider,
|
||||
) *AgentInstance {
|
||||
workspace := resolveAgentWorkspace(agentCfg, defaults)
|
||||
os.MkdirAll(workspace, 0o755)
|
||||
|
||||
// workspace can contain multiple paths separated by '|'
|
||||
primaryWorkspace := strings.Split(workspace, "|")[0]
|
||||
os.MkdirAll(primaryWorkspace, 0o755)
|
||||
|
||||
model := resolveAgentModel(agentCfg, defaults)
|
||||
fallbacks := resolveAgentFallbacks(agentCfg, defaults)
|
||||
|
|
@ -55,7 +58,7 @@ func NewAgentInstance(
|
|||
toolsRegistry.Register(tools.NewEditFileTool(workspace, restrict))
|
||||
toolsRegistry.Register(tools.NewAppendFileTool(workspace, restrict))
|
||||
|
||||
sessionsDir := filepath.Join(workspace, "sessions")
|
||||
sessionsDir := filepath.Join(primaryWorkspace, "sessions")
|
||||
sessionsManager := session.NewSessionManager(sessionsDir)
|
||||
|
||||
contextBuilder := NewContextBuilder(workspace)
|
||||
|
|
@ -148,6 +151,16 @@ func expandHome(path string) string {
|
|||
if path == "" {
|
||||
return path
|
||||
}
|
||||
|
||||
// Handle multiple paths separated by |
|
||||
if strings.Contains(path, "|") {
|
||||
parts := strings.Split(path, "|")
|
||||
for i, p := range parts {
|
||||
parts[i] = expandHome(strings.TrimSpace(p))
|
||||
}
|
||||
return strings.Join(parts, "|")
|
||||
}
|
||||
|
||||
if path[0] == '~' {
|
||||
home, _ := os.UserHomeDir()
|
||||
if len(path) > 1 && path[1] == '/' {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import (
|
|||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/caarlos0/env/v11"
|
||||
|
|
@ -587,6 +588,16 @@ func expandHome(path string) string {
|
|||
if path == "" {
|
||||
return path
|
||||
}
|
||||
|
||||
// Handle multiple paths separated by |
|
||||
if strings.Contains(path, "|") {
|
||||
parts := strings.Split(path, "|")
|
||||
for i, p := range parts {
|
||||
parts[i] = expandHome(strings.TrimSpace(p))
|
||||
}
|
||||
return strings.Join(parts, "|")
|
||||
}
|
||||
|
||||
if path[0] == '~' {
|
||||
home, _ := os.UserHomeDir()
|
||||
if len(path) > 1 && path[1] == '/' {
|
||||
|
|
|
|||
|
|
@ -8,53 +8,81 @@ import (
|
|||
"strings"
|
||||
)
|
||||
|
||||
// validatePath ensures the given path is within the workspace if restrict is true.
|
||||
func validatePath(path, workspace string, restrict bool) (string, error) {
|
||||
// validatePath ensures the given path is within one of the allowed paths if restrict is true.
|
||||
// workspace can be a single path or multiple paths separated by '|'.
|
||||
func validatePath(path string, workspace string, restrict bool) (string, error) {
|
||||
if workspace == "" {
|
||||
return path, nil
|
||||
}
|
||||
|
||||
absWorkspace, err := filepath.Abs(workspace)
|
||||
allowedPaths := strings.Split(workspace, "|")
|
||||
primaryWorkspace := allowedPaths[0]
|
||||
absPrimary, err := filepath.Abs(primaryWorkspace)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to resolve workspace path: %w", err)
|
||||
return "", fmt.Errorf("failed to resolve primary workspace path: %w", err)
|
||||
}
|
||||
|
||||
var absPath string
|
||||
if filepath.IsAbs(path) {
|
||||
absPath = filepath.Clean(path)
|
||||
} else {
|
||||
absPath, err = filepath.Abs(filepath.Join(absWorkspace, path))
|
||||
absPath, err = filepath.Abs(filepath.Join(absPrimary, path))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to resolve file path: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if restrict {
|
||||
if !isWithinWorkspace(absPath, absWorkspace) {
|
||||
found := false
|
||||
for _, wp := range allowedPaths {
|
||||
absWP, _ := filepath.Abs(wp)
|
||||
if isWithinWorkspace(absPath, absWP) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
return "", fmt.Errorf("access denied: path is outside the workspace")
|
||||
}
|
||||
|
||||
// Symlink identification and deeper validation
|
||||
var resolved string
|
||||
workspaceReal := absWorkspace
|
||||
if resolved, err = filepath.EvalSymlinks(absWorkspace); err == nil {
|
||||
workspaceReal = resolved
|
||||
}
|
||||
|
||||
if resolved, err = filepath.EvalSymlinks(absPath); err == nil {
|
||||
if !isWithinWorkspace(resolved, workspaceReal) {
|
||||
resFound := false
|
||||
for _, wp := range allowedPaths {
|
||||
absWP, _ := filepath.Abs(wp)
|
||||
resolvedWP := absWP
|
||||
if r, err := filepath.EvalSymlinks(absWP); err == nil {
|
||||
resolvedWP = r
|
||||
}
|
||||
if isWithinWorkspace(resolved, resolvedWP) {
|
||||
resFound = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !resFound {
|
||||
return "", fmt.Errorf("access denied: symlink resolves outside workspace")
|
||||
}
|
||||
} else if os.IsNotExist(err) {
|
||||
var parentResolved string
|
||||
if parentResolved, err = resolveExistingAncestor(filepath.Dir(absPath)); err == nil {
|
||||
if !isWithinWorkspace(parentResolved, workspaceReal) {
|
||||
resFound := false
|
||||
for _, wp := range allowedPaths {
|
||||
absWP, _ := filepath.Abs(wp)
|
||||
resolvedWP := absWP
|
||||
if r, err := filepath.EvalSymlinks(absWP); err == nil {
|
||||
resolvedWP = r
|
||||
}
|
||||
if isWithinWorkspace(parentResolved, resolvedWP) {
|
||||
resFound = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !resFound {
|
||||
return "", fmt.Errorf("access denied: symlink resolves outside workspace")
|
||||
}
|
||||
} else if !os.IsNotExist(err) {
|
||||
return "", fmt.Errorf("failed to resolve path: %w", err)
|
||||
}
|
||||
} else {
|
||||
return "", fmt.Errorf("failed to resolve path: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -285,11 +285,6 @@ func (t *ExecTool) guardCommand(command, cwd string) string {
|
|||
return "Command blocked by safety guard (path traversal detected)"
|
||||
}
|
||||
|
||||
cwdPath, err := filepath.Abs(cwd)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
pathPattern := regexp.MustCompile(`[A-Za-z]:\\[^\\\"']+|/[^\s\"']+`)
|
||||
matches := pathPattern.FindAllString(cmd, -1)
|
||||
|
||||
|
|
@ -299,12 +294,17 @@ func (t *ExecTool) guardCommand(command, cwd string) string {
|
|||
continue
|
||||
}
|
||||
|
||||
rel, err := filepath.Rel(cwdPath, p)
|
||||
if err != nil {
|
||||
continue
|
||||
allowedPaths := strings.Split(t.workingDir, "|")
|
||||
found := false
|
||||
for _, wp := range allowedPaths {
|
||||
absWP, _ := filepath.Abs(wp)
|
||||
if isWithinWorkspace(p, absWP) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if strings.HasPrefix(rel, "..") {
|
||||
if !found {
|
||||
return "Command blocked by safety guard (path outside working dir)"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue