From 68745133b75a0663e7baf3d282682893ca829df4 Mon Sep 17 00:00:00 2001 From: BBQ <35603386+HoneyBBQ@users.noreply.github.com> Date: Mon, 16 Mar 2026 23:05:23 +0800 Subject: [PATCH] fix(inbound): use bot owner token for agent gateway callbacks (#254) * feat(access): add guest chat ACL and simplify bot access Unify bot chat permissions around owner and guest ACL so public access, whitelist, and blacklist share a single model. Remove unused sharing paths, add searchable platform identity controls, and normalize Feishu identities to stable open_id records. * fix(web): format access control panel Include the post-commit formatting changes applied to the access control UI so the branch stays clean and the PR reflects the final rendered layout. * fix(migrations): drop legacy bot tables before bots Ensure the init down migration removes bot_members and bot_preauth_keys before dropping bots so full rollback succeeds after the ACL refactor. * feat(acl): add source-aware chat trigger rules Support channel-, conversation-, and thread-scoped ACL rules while keeping allow_guest, whitelist, and blacklist compatible. Also expose observed conversation candidates and normalize channel identity rules to their own platform. * fix(lint): resolve golangci-lint errors after rebase - Remove unused receivers and parameters in fakeRows/Service methods - Delete unused makeNoRow helper and toParticipantFields function - Fix gci/gofumpt formatting * fix(lint): fix gci import formatting in acl types and handler * fix(acl): tighten observed group and thread selection (#245) Use inbox plus persisted messages to discover observed group and thread routes, and lock scope fields after selecting a concrete observed target. This keeps Telegram group candidates visible and prevents contradictory private/group scope edits. * chore: regenerate sqlc swagger and sdk after rebase onto main * fix(inbound): use bot owner token for agent gateway callbacks The inbound channel processor issued a JWT for the chatting user's identity. When the agent called back into container/MCP endpoints (e.g. /bots/{id}/tools, /bots/{id}/mcp-stdio), AuthorizeBotAccess rejected non-owner users with HTTP 403 "bot access denied". Resolve the bot owner via PolicyService and issue the downstream token under the owner's identity, consistent with schedule, heartbeat, and email gateways. The chatting user's identity is still tracked via SourceChannelIdentityID and identity headers. --- internal/channel/inbound/channel.go | 32 +++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/internal/channel/inbound/channel.go b/internal/channel/inbound/channel.go index 15c741f3..abb29daf 100644 --- a/internal/channel/inbound/channel.go +++ b/internal/channel/inbound/channel.go @@ -86,6 +86,7 @@ type ChannelInboundProcessor struct { jwtSecret string tokenTTL time.Duration identity *IdentityResolver + policy PolicyService acl chatACL observer channel.StreamObserver ttsService ttsSynthesizer @@ -121,6 +122,7 @@ func NewChannelInboundProcessor( jwtSecret: strings.TrimSpace(jwtSecret), tokenTTL: tokenTTL, identity: identityResolver, + policy: policyService, } } @@ -370,16 +372,30 @@ func (p *ChannelInboundProcessor) HandleInbound(ctx context.Context, cfg channel } } - // Issue user JWT for downstream calls (MCP, schedule, etc.). For guests use chat token as Bearer. + // Issue bot-owner JWT for downstream calls (MCP tools, schedule, etc.). + // The agent uses this token to call back into the server's container/MCP + // endpoints which require bot-owner or admin access. Using the chatting + // user's identity would cause 403 for non-owner users. token := "" - if identity.UserID != "" && p.jwtSecret != "" { - signed, _, err := auth.GenerateToken(identity.UserID, p.jwtSecret, p.tokenTTL) - if err != nil { - if p.logger != nil { - p.logger.Warn("issue channel token failed", slog.Any("error", err)) + if p.jwtSecret != "" { + tokenUserID := strings.TrimSpace(identity.UserID) + if p.policy != nil { + if ownerID, err := p.policy.BotOwnerUserID(ctx, identity.BotID); err == nil && ownerID != "" { + tokenUserID = ownerID + } else if p.logger != nil { + p.logger.Warn("resolve bot owner for token failed, falling back to caller identity", + slog.String("bot_id", identity.BotID), slog.Any("error", err)) + } + } + if tokenUserID != "" { + signed, _, err := auth.GenerateToken(tokenUserID, p.jwtSecret, p.tokenTTL) + if err != nil { + if p.logger != nil { + p.logger.Warn("issue channel token failed", slog.Any("error", err)) + } + } else { + token = "Bearer " + signed } - } else { - token = "Bearer " + signed } } if token == "" && chatToken != "" {