mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-20 00:15:16 +00:00
Add per-agent tool policy support with NO_REPLY placeholder cleanup across channels
Wire AgentToolPolicy from agent spec through loop creation and resolver, passing it to PolicyEngine.FilterTools for per-agent tool restrictions (nil = no restrictions). Set RestrictToWorkspace=true for default agent in managed mode seeding. Clean up thinking/placeholder messages in Discord and Telegram when agent suppresses empty/NO_REPLY responses by publishing empty outbound and deleting placeholders without sending messages.
This commit is contained in:
1 parent
2991283901
commit
d56813d726
8 files changed
+58
-22
No files matched your search
@@ -68,8 +68,10 @@ func createAgentLoop(agentID string, cfg *config.Config, router *agent.Router, p
|
||||
// Per-agent skill allowlist.
|
||||
// AgentSpec.Skills: nil = all skills, [] = none, ["x","y"] = only those.
|
||||
var skillAllowList []string
|
||||
var agentToolPolicy *config.ToolPolicySpec
|
||||
if spec, ok := cfg.Agents.List[agentID]; ok {
|
||||
skillAllowList = spec.Skills
|
||||
agentToolPolicy = spec.Tools
|
||||
}
|
||||
|
||||
loop := agent.NewLoop(agent.LoopConfig{
|
||||
@@ -82,7 +84,8 @@ func createAgentLoop(agentID string, cfg *config.Config, router *agent.Router, p
|
||||
Bus: msgBus,
|
||||
Sessions: sess,
|
||||
Tools: toolsReg,
|
||||
ToolPolicy: toolPE,
|
||||
ToolPolicy: toolPE,
|
||||
AgentToolPolicy: agentToolPolicy,
|
||||
OwnerIDs: cfg.Gateway.OwnerIDs,
|
||||
SkillsLoader: skillsLoader,
|
||||
SkillAllowList: skillAllowList,
|
||||
|
||||
@@ -174,12 +174,19 @@ func consumeInboundMessages(ctx context.Context, msgBus *bus.MessageBus, agents
|
||||
}
|
||||
|
||||
// Suppress empty/NO_REPLY responses (matching TS normalize-reply.ts).
|
||||
// Still publish an empty outbound so channels can clean up placeholder/thinking indicators.
|
||||
if outcome.Result.Content == "" || agent.IsSilentReply(outcome.Result.Content) {
|
||||
slog.Info("inbound: suppressed silent/empty reply",
|
||||
"channel", channel,
|
||||
"chat_id", chatID,
|
||||
"session", session,
|
||||
)
|
||||
msgBus.PublishOutbound(bus.OutboundMessage{
|
||||
Channel: channel,
|
||||
ChatID: chatID,
|
||||
Content: "",
|
||||
Metadata: meta,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
+11
-10
@@ -112,16 +112,17 @@ func seedManagedData(dsn string, cfg *config.Config) error {
|
||||
// 4. Seed default agent (skip if already exists)
|
||||
workspace := config.ExpandHome(cfg.Agents.Defaults.Workspace)
|
||||
agent := &store.AgentData{
|
||||
AgentKey: "default",
|
||||
DisplayName: "Default Agent",
|
||||
OwnerID: "system",
|
||||
AgentType: "open",
|
||||
Provider: defaultProvider,
|
||||
Model: modelID,
|
||||
Workspace: workspace,
|
||||
IsDefault: true,
|
||||
Status: "active",
|
||||
SubagentsConfig: json.RawMessage(`{"maxSpawnDepth":1,"maxConcurrent":20}`),
|
||||
AgentKey: "default",
|
||||
DisplayName: "Default Agent",
|
||||
OwnerID: "system",
|
||||
AgentType: "open",
|
||||
Provider: defaultProvider,
|
||||
Model: modelID,
|
||||
Workspace: workspace,
|
||||
RestrictToWorkspace: true,
|
||||
IsDefault: true,
|
||||
Status: "active",
|
||||
SubagentsConfig: json.RawMessage(`{"maxSpawnDepth":1,"maxConcurrent":20}`),
|
||||
}
|
||||
|
||||
if err := stores.Agents.Create(ctx, agent); err != nil {
|
||||
|
||||
+13
-10
@@ -43,9 +43,10 @@ type Loop struct {
|
||||
|
||||
eventPub bus.EventPublisher // currently unused by Loop; kept for future use
|
||||
sessions store.SessionStore
|
||||
tools *tools.Registry
|
||||
toolPolicy *tools.PolicyEngine // optional: filters tools sent to LLM
|
||||
running atomic.Bool
|
||||
tools *tools.Registry
|
||||
toolPolicy *tools.PolicyEngine // optional: filters tools sent to LLM
|
||||
agentToolPolicy *config.ToolPolicySpec // per-agent tool policy from DB (nil = no restrictions)
|
||||
running atomic.Bool
|
||||
mu sync.Mutex // protects concurrent runs
|
||||
|
||||
// Bootstrap/persona context (loaded at startup, injected into system prompt)
|
||||
@@ -101,9 +102,10 @@ type LoopConfig struct {
|
||||
Workspace string
|
||||
Bus bus.EventPublisher
|
||||
Sessions store.SessionStore
|
||||
Tools *tools.Registry
|
||||
ToolPolicy *tools.PolicyEngine // optional: filters tools sent to LLM
|
||||
OnEvent func(AgentEvent)
|
||||
Tools *tools.Registry
|
||||
ToolPolicy *tools.PolicyEngine // optional: filters tools sent to LLM
|
||||
AgentToolPolicy *config.ToolPolicySpec // per-agent tool policy from DB (nil = no restrictions)
|
||||
OnEvent func(AgentEvent)
|
||||
|
||||
// Bootstrap/persona context
|
||||
OwnerIDs []string
|
||||
@@ -174,9 +176,10 @@ func NewLoop(cfg LoopConfig) *Loop {
|
||||
workspace: cfg.Workspace,
|
||||
eventPub: cfg.Bus,
|
||||
sessions: cfg.Sessions,
|
||||
tools: cfg.Tools,
|
||||
toolPolicy: cfg.ToolPolicy,
|
||||
onEvent: cfg.OnEvent,
|
||||
tools: cfg.Tools,
|
||||
toolPolicy: cfg.ToolPolicy,
|
||||
agentToolPolicy: cfg.AgentToolPolicy,
|
||||
onEvent: cfg.OnEvent,
|
||||
ownerIDs: cfg.OwnerIDs,
|
||||
skillsLoader: cfg.SkillsLoader,
|
||||
skillAllowList: cfg.SkillAllowList,
|
||||
@@ -403,7 +406,7 @@ func (l *Loop) runLoop(ctx context.Context, req RunRequest) (*RunResult, error)
|
||||
// Build provider request with policy-filtered tools
|
||||
var toolDefs []providers.ToolDefinition
|
||||
if l.toolPolicy != nil {
|
||||
toolDefs = l.toolPolicy.FilterTools(l.tools, l.id, l.provider.Name(), nil, nil, false, false)
|
||||
toolDefs = l.toolPolicy.FilterTools(l.tools, l.id, l.provider.Name(), l.agentToolPolicy, nil, false, false)
|
||||
} else {
|
||||
toolDefs = l.tools.ProviderDefs()
|
||||
}
|
||||
|
||||
@@ -136,6 +136,7 @@ func NewManagedResolver(deps ResolverDeps) ResolverFunc {
|
||||
Sessions: deps.Sessions,
|
||||
Tools: toolsReg,
|
||||
ToolPolicy: deps.ToolPolicy,
|
||||
AgentToolPolicy: ag.ParseToolsConfig(),
|
||||
SkillsLoader: deps.Skills,
|
||||
HasMemory: deps.HasMemory,
|
||||
ContextFiles: contextFiles,
|
||||
|
||||
@@ -87,6 +87,17 @@ func (c *Channel) Send(_ context.Context, msg bus.OutboundMessage) error {
|
||||
|
||||
content := msg.Content
|
||||
|
||||
// NO_REPLY cleanup: content is empty when agent suppresses reply.
|
||||
// Delete placeholder and return without sending any message.
|
||||
if content == "" {
|
||||
if pID, ok := c.placeholders.Load(channelID); ok {
|
||||
c.placeholders.Delete(channelID)
|
||||
msgID := pID.(string)
|
||||
_ = c.session.ChannelMessageDelete(channelID, msgID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Try to edit the placeholder "Thinking..." message
|
||||
if pID, ok := c.placeholders.Load(channelID); ok {
|
||||
c.placeholders.Delete(channelID)
|
||||
|
||||
@@ -57,6 +57,16 @@ func (c *Channel) Send(ctx context.Context, msg bus.OutboundMessage) error {
|
||||
c.stopThinking.Delete(localKey)
|
||||
}
|
||||
|
||||
// NO_REPLY cleanup: content is empty when agent suppresses reply (prompt injection, etc.).
|
||||
// Clean up placeholder, then return without sending any message.
|
||||
if msg.Content == "" && len(msg.Media) == 0 {
|
||||
if pID, ok := c.placeholders.Load(localKey); ok {
|
||||
c.placeholders.Delete(localKey)
|
||||
_ = c.deleteMessage(ctx, chatID, pID.(int))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Handle media attachments if present
|
||||
if len(msg.Media) > 0 {
|
||||
// Delete placeholder since we're sending media
|
||||
|
||||
@@ -62,7 +62,7 @@ var safeBins = map[string]bool{
|
||||
"cat": true, "echo": true, "ls": true, "pwd": true, "head": true,
|
||||
"tail": true, "wc": true, "sort": true, "uniq": true, "grep": true,
|
||||
"find": true, "which": true, "whoami": true, "date": true,
|
||||
"uname": true, "id": true, "hostname": true,
|
||||
"uname": true, "hostname": true,
|
||||
"df": true, "du": true, "free": true, "uptime": true, "file": true,
|
||||
"stat": true, "dirname": true, "basename": true, "realpath": true,
|
||||
// Text processing
|
||||
|
||||
Reference in new issue
Block a user