feat: Add tool allow list configuration and enforcement for Telegram channels, allowing per-group/topic tool restrictions.

This commit is contained in:
viettranx committed 2026-03-02 21:17:19 +07:00
1 parent ae89239dd5
commit 278a98ba74
7 files changed
+48 -3

No files matched your search

+1
View File
@@ -199,6 +199,7 @@ func consumeInboundMessages(ctx context.Context, msgBus *bus.MessageBus, agents
RunID: runID,
Stream: enableStream,
HistoryLimit: msg.HistoryLimit,
ToolAllow: msg.ToolAllow,
ExtraSystemPrompt: extraPrompt,
SkillFilter: skillFilter,
}, scheduler.ScheduleOpts{
+21 -3
View File
@@ -254,6 +254,7 @@ type RunRequest struct {
ExtraSystemPrompt string // optional: injected into system prompt (skills, subagent context, etc.)
SkillFilter []string // per-request skill override: nil=use agent default, []=no skills, ["x","y"]=whitelist
HistoryLimit int // max user turns to keep in context (0=unlimited, from channel config)
ToolAllow []string // per-group tool allow list (nil = no restriction, supports "group:xxx")
LocalKey string // composite key with topic/thread suffix for routing (e.g. "-100123:topic:42")
ParentTraceID uuid.UUID // if set, reuse parent trace instead of creating new (announce runs)
ParentRootSpanID uuid.UUID // if set, nest announce agent span under this parent span
@@ -573,8 +574,13 @@ func (l *Loop) runLoop(ctx context.Context, req RunRequest) (*RunResult, error)
// Build provider request with policy-filtered tools
var toolDefs []providers.ToolDefinition
var allowedTools map[string]bool
if l.toolPolicy != nil {
toolDefs = l.toolPolicy.FilterTools(l.tools, l.id, l.provider.Name(), l.agentToolPolicy, nil, false, false)
toolDefs = l.toolPolicy.FilterTools(l.tools, l.id, l.provider.Name(), l.agentToolPolicy, req.ToolAllow, false, false)
allowedTools = make(map[string]bool, len(toolDefs))
for _, td := range toolDefs {
allowedTools[td.Function.Name] = true
}
} else {
toolDefs = l.tools.ProviderDefs()
}
@@ -703,7 +709,13 @@ func (l *Loop) runLoop(ctx context.Context, req RunRequest) (*RunResult, error)
argsHash := loopDetector.record(tc.Name, tc.Arguments)
toolSpanStart := time.Now().UTC()
result := l.tools.ExecuteWithContext(ctx, tc.Name, tc.Arguments, req.Channel, req.ChatID, req.PeerKind, req.SessionKey, nil)
var result *tools.Result
if allowedTools != nil && !allowedTools[tc.Name] {
slog.Warn("security.tool_policy_blocked", "agent", l.id, "tool", tc.Name)
result = tools.ErrorResult("tool not allowed by policy: " + tc.Name)
} else {
result = l.tools.ExecuteWithContext(ctx, tc.Name, tc.Arguments, req.Channel, req.ChatID, req.PeerKind, req.SessionKey, nil)
}
l.emitToolSpan(ctx, toolSpanStart, tc.Name, tc.ID, string(argsJSON), result)
@@ -796,7 +808,13 @@ func (l *Loop) runLoop(ctx context.Context, req RunRequest) (*RunResult, error)
argsJSON, _ := json.Marshal(tc.Arguments)
slog.Info("tool call", "agent", l.id, "tool", tc.Name, "args_len", len(argsJSON), "parallel", true)
spanStart := time.Now().UTC()
result := l.tools.ExecuteWithContext(ctx, tc.Name, tc.Arguments, req.Channel, req.ChatID, req.PeerKind, req.SessionKey, nil)
var result *tools.Result
if allowedTools != nil && !allowedTools[tc.Name] {
slog.Warn("security.tool_policy_blocked", "agent", l.id, "tool", tc.Name)
result = tools.ErrorResult("tool not allowed by policy: " + tc.Name)
} else {
result = l.tools.ExecuteWithContext(ctx, tc.Name, tc.Arguments, req.Channel, req.ChatID, req.PeerKind, req.SessionKey, nil)
}
resultCh <- indexedResult{idx: idx, tc: tc, result: result, argsJSON: string(argsJSON), spanStart: spanStart}
}(i, tc)
}
+1
View File
@@ -14,6 +14,7 @@ type InboundMessage struct {
AgentID string `json:"agent_id,omitempty"` // target agent (for multi-agent routing)
UserID string `json:"user_id,omitempty"` // external user ID for per-user scoping (memory, bootstrap)
HistoryLimit int `json:"history_limit,omitempty"` // max turns to keep in context (0=unlimited, from channel config)
ToolAllow []string `json:"tool_allow,omitempty"` // per-group tool allow list (nil = no restriction)
Metadata map[string]string `json:"metadata,omitempty"`
}
+1
View File
@@ -441,6 +441,7 @@ func (c *Channel) handleMessage(ctx context.Context, update telego.Update) {
UserID: userID,
AgentID: targetAgentID,
HistoryLimit: c.historyLimit,
ToolAllow: topicCfg.tools,
Metadata: metadata,
})
@@ -16,6 +16,7 @@ type resolvedTopicConfig struct {
allowFrom []string
enabled *bool
skills []string // nil = inherit, non-nil = override (empty = no skills)
tools []string // nil = inherit (all tools), non-nil = override (supports "group:xxx")
systemPrompt string // concatenated group + topic prompts
}
@@ -73,6 +74,9 @@ func mergeGroupInto(dst *resolvedTopicConfig, src *config.TelegramGroupConfig) {
if src.Skills != nil {
dst.skills = src.Skills
}
if src.Tools != nil {
dst.tools = src.Tools
}
if src.SystemPrompt != "" {
dst.systemPrompt = src.SystemPrompt
}
@@ -97,6 +101,10 @@ func mergeTopicInto(dst *resolvedTopicConfig, src *config.TelegramTopicConfig, g
if src.Skills != nil {
dst.skills = src.Skills
}
// Tools: topic overrides group (same firstDefined pattern as skills).
if src.Tools != nil {
dst.tools = src.Tools
}
// SystemPrompt: concatenate group + topic (both may exist, matching TS).
if src.SystemPrompt != "" {
parts := []string{}
+2
View File
@@ -48,6 +48,7 @@ type TelegramGroupConfig struct {
AllowFrom FlexibleStringSlice `json:"allow_from,omitempty"` // override allow_from for this group
Enabled *bool `json:"enabled,omitempty"` // disable bot for this group (default: true)
Skills []string `json:"skills,omitempty"` // skill whitelist (nil = all, [] = none)
Tools []string `json:"tools,omitempty"` // tool allow list (nil = all, supports "group:xxx")
SystemPrompt string `json:"system_prompt,omitempty"` // extra system prompt for this group
Topics map[string]*TelegramTopicConfig `json:"topics,omitempty"` // per-topic overrides (key: thread ID string)
}
@@ -58,6 +59,7 @@ type TelegramTopicConfig struct {
RequireMention *bool `json:"require_mention,omitempty"`
GroupPolicy string `json:"group_policy,omitempty"`
Skills []string `json:"skills,omitempty"`
Tools []string `json:"tools,omitempty"` // tool allow list (nil = inherit, supports "group:xxx")
Enabled *bool `json:"enabled,omitempty"`
AllowFrom FlexibleStringSlice `json:"allow_from,omitempty"`
SystemPrompt string `json:"system_prompt,omitempty"`
@@ -8,6 +8,7 @@ import {
SelectValue,
} from "@/components/ui/select";
import { groupPolicyOptions } from "./channel-schemas";
import { ToolNameSelect } from "@/components/shared/tool-name-select";
export interface TelegramGroupConfigValues {
group_policy?: string;
@@ -15,6 +16,7 @@ export interface TelegramGroupConfigValues {
enabled?: boolean;
allow_from?: string[];
skills?: string[];
tools?: string[];
system_prompt?: string;
}
@@ -141,6 +143,18 @@ export function TelegramGroupFields({ config, onChange, idPrefix }: Props) {
/>
</div>
<div className="grid gap-1.5">
<Label>Tool Allow</Label>
<ToolNameSelect
value={config.tools ?? []}
onChange={(v) => update({ tools: v.length > 0 ? v : undefined })}
placeholder="Select allowed tools (empty = inherit all)..."
/>
<p className="text-xs text-muted-foreground">
Restrict which tools are available in this group. Supports group names like &quot;group:web&quot;, &quot;group:fs&quot;. Empty = inherit all tools.
</p>
</div>
<div className="grid gap-1.5">
<Label htmlFor={`${idPrefix}-prompt`}>System Prompt</Label>
<Textarea