Files
viettranx 3fa8a02d4b feat(tools): add MessageTargetEnforced session guard helper
Classifies session kinds into FREE (cron, heartbeat, subagent, team) vs
ENFORCED (DM, group, ws, default, unknown, empty). FREE kinds compose
targets per job spec; everything else must match the ctx-bound origin.

postCrossTargetNotice posts the audit breadcrumb back to the origin
chat when an explicit forward succeeds: msgBus preferred, falls back to
direct sender, else slog.Warn so the skip is traceable.
2026-04-18 17:44:57 +07:00

74 lines
2.3 KiB
Go

package tools
import (
"context"
"log/slog"
"github.com/nextlevelbuilder/goclaw/internal/bus"
"github.com/nextlevelbuilder/goclaw/internal/i18n"
"github.com/nextlevelbuilder/goclaw/internal/sessions"
"github.com/nextlevelbuilder/goclaw/internal/store"
)
// MessageTargetEnforced reports whether the message tool must enforce
// (channel, target) == ctx (channel, target) for this session kind.
//
// FREE kinds (cron, heartbeat, subagent, team) compose targets per job spec
// and are allowed to address arbitrary chats. Everything else (DM, group,
// default, unknown, empty) enforces the bound origin — fail-closed.
func MessageTargetEnforced(sessionKey string) bool {
if sessionKey == "" {
return true
}
if sessions.IsCronSession(sessionKey) {
return false
}
if sessions.IsHeartbeatSession(sessionKey) {
return false
}
if sessions.IsSubagentSession(sessionKey) {
return false
}
if sessions.IsTeamSession(sessionKey) {
return false
}
return true
}
// postCrossTargetNotice posts a short notice back to the origin chat after
// an explicit, SUCCESSFUL cross-target forward. Call inline post-send — not
// via defer — so that failed forwards don't announce fake deliveries.
//
// Prefers msgBus (async, group-aware). Falls back to t.sender so deployments
// that wire only a direct sender still get the audit breadcrumb. Last resort:
// slog.Warn so the skip is still traceable.
func (t *MessageTool) postCrossTargetNotice(ctx context.Context, target, reason string) {
origCh := ToolChannelFromCtx(ctx)
origChat := ToolChatIDFromCtx(ctx)
if origCh == "" || origChat == "" {
return
}
locale := store.LocaleFromContext(ctx)
text := i18n.T(locale, i18n.MessageCrossTargetForwarded, target, reason)
if t.msgBus != nil {
msg := bus.OutboundMessage{Channel: origCh, ChatID: origChat, Content: text}
if isGroupContext(ctx) {
msg.Metadata = map[string]string{"group_id": origChat}
}
t.msgBus.PublishOutbound(msg)
return
}
if t.sender != nil {
if err := t.sender(ctx, origCh, origChat, text); err == nil {
return
} else {
slog.Warn("message.cross_target_notice_send_failed",
"channel", origCh, "chat", origChat, "err", err)
return
}
}
slog.Warn("message.cross_target_notice_skipped",
"reason", "no_bus_or_sender", "channel", origCh, "chat", origChat)
}