mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-09-03 02:20:03 +00:00
Resolves issue #915 (Telegram group write_file permission denied after /addwriter) by reconstructing the real sender on announce re-ingress and migrating ownership/audit call sites off the scope-coupled UserID. Propagation chain (new MetaOriginSenderID key): SubagentTask.OriginSenderID → AnnounceMetadata.OriginSenderID → InboundMessage.Metadata[MetaOriginSenderID] → subagentAnnounceRouting.SenderID → RunRequest.SenderID Same chain for delegate_tool.announceToParent, team_tool_dispatch, sessions_send, team_tasks_blocker escalation. ACTOR migration (owner/audit now uses ActorIDFromContext): - publish_skill: skill owner = individual sender - skill_manage create/patch/delete: owner = sender; ownership check accepts actor or legacy UserID for pre-migration rows - delegate_tool: DelegateRequest.UserID + DomainEvent.UserID = actor - team_tasks_blocker: blocker attribution + escalation = actor - team_tool_cache: team access-policy check = actor (fixes per-user allow/deny lists in group chats) Scope-intentional (unchanged, commented): cron.go, team_tasks_create.go.
147 lines
4.4 KiB
Go
147 lines
4.4 KiB
Go
package tools
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/nextlevelbuilder/goclaw/internal/bus"
|
|
"github.com/nextlevelbuilder/goclaw/internal/store"
|
|
)
|
|
|
|
// ============================================================
|
|
// sessions_send
|
|
// ============================================================
|
|
|
|
type SessionsSendTool struct {
|
|
sessions store.SessionStore
|
|
msgBus *bus.MessageBus
|
|
}
|
|
|
|
func NewSessionsSendTool() *SessionsSendTool { return &SessionsSendTool{} }
|
|
|
|
func (t *SessionsSendTool) SetSessionStore(s store.SessionStore) { t.sessions = s }
|
|
func (t *SessionsSendTool) SetMessageBus(b *bus.MessageBus) { t.msgBus = b }
|
|
|
|
func (t *SessionsSendTool) Name() string { return "sessions_send" }
|
|
func (t *SessionsSendTool) Description() string {
|
|
return "Send a message into another session. Use session_key or label to identify the target."
|
|
}
|
|
|
|
func (t *SessionsSendTool) Parameters() map[string]any {
|
|
return map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{
|
|
"session_key": map[string]any{
|
|
"type": "string",
|
|
"description": "Target session key",
|
|
},
|
|
"label": map[string]any{
|
|
"type": "string",
|
|
"description": "Target session label (alternative to session_key)",
|
|
},
|
|
"message": map[string]any{
|
|
"type": "string",
|
|
"description": "Message to send",
|
|
},
|
|
},
|
|
"required": []string{"message"},
|
|
}
|
|
}
|
|
|
|
func (t *SessionsSendTool) Execute(ctx context.Context, args map[string]any) *Result {
|
|
if t.sessions == nil {
|
|
return ErrorResult("session store not available")
|
|
}
|
|
if t.msgBus == nil {
|
|
return ErrorResult("message bus not available")
|
|
}
|
|
|
|
sessionKey, _ := args["session_key"].(string)
|
|
label, _ := args["label"].(string)
|
|
message, _ := args["message"].(string)
|
|
|
|
if message == "" {
|
|
return ErrorResult("message is required")
|
|
}
|
|
if sessionKey == "" && label == "" {
|
|
return ErrorResult("either session_key or label is required")
|
|
}
|
|
|
|
// Security: fail-closed when agent key missing.
|
|
// Session keys use agent_key (e.g. "agent:victoria:..."), not UUID.
|
|
agentKey := ToolAgentKeyFromCtx(ctx)
|
|
if agentKey == "" {
|
|
return ErrorResult("agent context required")
|
|
}
|
|
|
|
// Resolve by label if needed
|
|
if sessionKey == "" && label != "" {
|
|
sessions := t.sessions.List(ctx, agentKey)
|
|
for _, s := range sessions {
|
|
if s.Label == label {
|
|
sessionKey = s.Key
|
|
break
|
|
}
|
|
}
|
|
if sessionKey == "" {
|
|
return ErrorResult(fmt.Sprintf("no session found with label: %s", label))
|
|
}
|
|
}
|
|
|
|
// Security: validate target session belongs to same agent
|
|
if !strings.HasPrefix(sessionKey, "agent:"+agentKey+":") {
|
|
return ErrorResult("access denied: target session belongs to a different agent")
|
|
}
|
|
|
|
// Scope check: group-scoped users cannot send to other groups' sessions.
|
|
currentSession := ToolSandboxKeyFromCtx(ctx)
|
|
if !isSessionInScope(ctx, sessionKey, currentSession) {
|
|
return ErrorResult("access denied: cannot send to session outside current scope")
|
|
}
|
|
|
|
// Block self-send: agent should not send to its own current session
|
|
// to prevent re-processing loops (same pattern as message tool).
|
|
if currentSession != "" && sessionKey == currentSession {
|
|
return ErrorResult("cannot send to your own current session — your response is already delivered to it")
|
|
}
|
|
|
|
// Publish as an inbound message (same mechanism as channels).
|
|
// Propagate the calling agent's real sender so the target session's turn
|
|
// can still perform user-attributed actions (e.g. write_file permission
|
|
// in a group chat) instead of being blocked by the synthetic sender
|
|
// rule at config_permission_store.go (#915).
|
|
sendMeta := map[string]string{}
|
|
if actorSender := store.SenderIDFromContext(ctx); actorSender != "" {
|
|
sendMeta[MetaOriginSenderID] = actorSender
|
|
}
|
|
t.msgBus.PublishInbound(bus.InboundMessage{
|
|
Channel: "system",
|
|
SenderID: "session_send_tool",
|
|
ChatID: sessionKey,
|
|
Content: message,
|
|
PeerKind: "direct",
|
|
TenantID: store.TenantIDFromContext(ctx),
|
|
Metadata: sendMeta,
|
|
})
|
|
|
|
out, _ := json.Marshal(map[string]any{
|
|
"status": "accepted",
|
|
"session_key": sessionKey,
|
|
})
|
|
return SilentResult(string(out))
|
|
}
|
|
|
|
// ============================================================
|
|
// helpers
|
|
// ============================================================
|
|
|
|
func resolveAgentIDString(ctx context.Context) string {
|
|
id := store.AgentIDFromContext(ctx)
|
|
if id.String() == "00000000-0000-0000-0000-000000000000" {
|
|
return "" // no agent ID in context
|
|
}
|
|
return id.String()
|
|
}
|