feat: wire permission system into agent loop and CLI

Add PermStore to AgentInstance, SetPermissionFuncFactory to AgentLoop,
and update updateToolContexts to set per-request PermissionFunc on all
PermissibleTools. CLI agent command creates a stdin-based permission
prompt for the cli channel.
This commit is contained in:
Rahul Bansal 2026-02-21 09:41:57 +05:30
parent e3300b199d
commit 0dc21b649f
4 changed files with 76 additions and 8 deletions

View file

@ -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",

View file

@ -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,

View file

@ -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")
}
}

View file

@ -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.