diff --git a/cmd/picoclaw/cmd_agent.go b/cmd/picoclaw/cmd_agent.go index 6d6ff935f..6151a158b 100644 --- a/cmd/picoclaw/cmd_agent.go +++ b/cmd/picoclaw/cmd_agent.go @@ -18,6 +18,7 @@ import ( "github.com/sipeed/picoclaw/pkg/bus" "github.com/sipeed/picoclaw/pkg/logger" "github.com/sipeed/picoclaw/pkg/providers" + "github.com/sipeed/picoclaw/pkg/tools" ) func agentCmd() { @@ -72,6 +73,15 @@ func agentCmd() { msgBus := bus.NewMessageBus() agentLoop := agent.NewAgentLoop(cfg, msgBus, provider) + // Set up CLI permission prompt for workspace access + cliPermFn := tools.NewCLIPermissionFunc(os.Stdin, os.Stdout) + agentLoop.SetPermissionFuncFactory(func(channel, chatID string) tools.PermissionFunc { + if channel == "cli" { + return cliPermFn + } + return nil // Other channels fall back to LLM-driven flow + }) + // Print agent startup info (only for interactive mode) startupInfo := agentLoop.GetStartupInfo() logger.InfoCF("agent", "Agent initialized", diff --git a/pkg/agent/instance.go b/pkg/agent/instance.go index dfbef9fbc..a7540ceaa 100644 --- a/pkg/agent/instance.go +++ b/pkg/agent/instance.go @@ -28,6 +28,7 @@ type AgentInstance struct { Sessions *session.SessionManager ContextBuilder *ContextBuilder Tools *tools.ToolRegistry + PermStore *tools.PermissionStore Subagents *config.SubagentsConfig SkillsFilter []string Candidates []providers.FallbackCandidate @@ -88,6 +89,16 @@ func NewAgentInstance( temperature = *defaults.Temperature } + // Set up permission store and wire into permissible tools + permStore := tools.NewPermissionStore() + for _, name := range toolsRegistry.List() { + if tool, ok := toolsRegistry.Get(name); ok { + if pt, ok := tool.(tools.PermissibleTool); ok { + pt.SetPermission(permStore, nil) + } + } + } + // Resolve fallback candidates modelCfg := providers.ModelConfig{ Primary: model, @@ -109,6 +120,7 @@ func NewAgentInstance( Sessions: sessionsManager, ContextBuilder: contextBuilder, Tools: toolsRegistry, + PermStore: permStore, Subagents: subagents, SkillsFilter: skillsFilter, Candidates: candidates, diff --git a/pkg/agent/instance_test.go b/pkg/agent/instance_test.go index fcc8e9bea..c9982785c 100644 --- a/pkg/agent/instance_test.go +++ b/pkg/agent/instance_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/sipeed/picoclaw/pkg/config" + "github.com/sipeed/picoclaw/pkg/tools" ) func TestNewAgentInstance_UsesDefaultsTemperatureAndMaxTokens(t *testing.T) { @@ -93,3 +94,29 @@ func TestNewAgentInstance_DefaultsTemperatureWhenUnset(t *testing.T) { t.Fatalf("Temperature = %f, want %f", agent.Temperature, 0.7) } } + +func TestAgentInstance_ToolsImplementPermissibleTool(t *testing.T) { + defaults := &config.AgentDefaults{ + Model: "test-model", + Workspace: t.TempDir(), + RestrictToWorkspace: true, + } + cfg := config.DefaultConfig() + instance := NewAgentInstance(nil, defaults, cfg, nil) + + permissibleTools := []string{"read_file", "write_file", "list_dir", "edit_file", "append_file", "exec"} + for _, name := range permissibleTools { + tool, ok := instance.Tools.Get(name) + if !ok { + t.Errorf("tool %q not registered", name) + continue + } + if _, ok := tool.(tools.PermissibleTool); !ok { + t.Errorf("tool %q does not implement PermissibleTool", name) + } + } + + if instance.PermStore == nil { + t.Error("expected PermStore to be non-nil") + } +} diff --git a/pkg/agent/loop.go b/pkg/agent/loop.go index 9a66c6cd1..7636c7ed5 100644 --- a/pkg/agent/loop.go +++ b/pkg/agent/loop.go @@ -30,14 +30,15 @@ import ( ) type AgentLoop struct { - bus *bus.MessageBus - cfg *config.Config - registry *AgentRegistry - state *state.Manager - running atomic.Bool - summarizing sync.Map - fallback *providers.FallbackChain - channelManager *channels.Manager + bus *bus.MessageBus + cfg *config.Config + registry *AgentRegistry + state *state.Manager + running atomic.Bool + summarizing sync.Map + fallback *providers.FallbackChain + channelManager *channels.Manager + permFuncFactory tools.PermissionFuncFactory } // processOptions configures how a message is processed @@ -212,6 +213,12 @@ func (al *AgentLoop) SetChannelManager(cm *channels.Manager) { al.channelManager = cm } +// SetPermissionFuncFactory sets the factory that creates PermissionFunc instances +// for each channel/chatID pair. This allows channel-specific permission prompts. +func (al *AgentLoop) SetPermissionFuncFactory(factory tools.PermissionFuncFactory) { + al.permFuncFactory = factory +} + // RecordLastChannel records the last active channel for this workspace. // This uses the atomic state save mechanism to prevent data loss on crash. func (al *AgentLoop) RecordLastChannel(channel string) error { @@ -742,6 +749,18 @@ func (al *AgentLoop) updateToolContexts(agent *AgentInstance, channel, chatID st st.SetContext(channel, chatID) } } + + // Set permission function for this request's channel + if al.permFuncFactory != nil { + permFn := al.permFuncFactory(channel, chatID) + for _, name := range agent.Tools.List() { + if tool, ok := agent.Tools.Get(name); ok { + if pt, ok := tool.(tools.PermissibleTool); ok { + pt.SetPermission(agent.PermStore, permFn) + } + } + } + } } // maybeSummarize triggers summarization if the session history exceeds thresholds.