v2-refactor-temp/docs/ai/channel-ingress-security.md
How an externally-triggered agent run (inbound IM → auto agent run → tool calls) is secured, and the gaps to close. Answers review item D1. The threat model: an untrusted remote party sends a message on a bound channel (Slack / Discord / Telegram / Feishu / WeChat / QQ); that message drives an agent that can call tools and touch the session workspace, with no human watching the renderer.
adapter (webhook/socket) → ChannelManager registers adapter.on('message', …)
(ChannelManager.ts:301) → ChannelMessageHandler.handleIncoming (per-chat 8 s debounce +
serial queue, :54, :111) → processIncoming resolves the bound session+agent →
startAgentSessionRun({ sessionId, userParts, listeners }) (:552). One run at a time per
${agentId}:${channelId}:${chatId}.
| Layer | Where | What it does |
|---|---|---|
| Output secret-redaction | channels/security/OutputSanitizer.ts (sanitizeChannelOutput, called ChannelMessageHandler.ts:282) | Redacts PEM keys, AWS/GitHub/Anthropic/OpenAI keys, bearer tokens, etc. before any agent output leaves through the channel |
| Workspace isolation | session workspace.path; attachments persisted under ${workspace}/.cherry-studio/channel-* | The agent's fs reach is bounded to the session workspace — but only as strong as the agent's tool policy: a channel-bound agent with broad Bash/Write and no per-channel narrowing (see G3) is not effectively bounded |
| Channel allow-listing | per-adapter allow-list config (allowedChatIds / allowedChannelIds) in channels/adapters/<platform>/<Platform>Adapter.ts | Inbound from a non-allow-listed chat/channel is silently dropped |
| Per-chat serialization | ChannelMessageHandler.ts:111 | One stream per chat; no concurrent interleave |
Trust-boundary summary: inbound text is passed through unchanged; inbound files/images are not content-inspected (persisted to the workspace, agent reads via the Read tool, bounded by workspace); outbound is secret-redacted; sender identity is unvalidated (see gap 1).
Inbound text is deliberately passed to the agent unchanged. It carries no sender prefix, boundary marker, injection warning, normalisation, or detection logging. This is an explicit owner decision: prompt wrappers were removed so the agent receives the literal user message. Treat all channel content as untrusted when configuring the agent's tools and permissions.
Adapters gate on the chat/channel allow-list. So any member of an allow-listed group chat can trigger agent runs. Proposed direction: an optional per-channel sender allow-list (user ids) enforced in the adapter alongside the chat check; default off (chat-level remains the baseline), opt-in for group chats. Deny → silent drop (consistent with the chat gate).
A channel run binds no renderer, so the approval emit is unbound; canUseTool
(runtime/claudeCode/settingsBuilder.ts:418) logs and auto-denies ("Approval emitter not
ready"). Net effect today: an approval-required tool fails the run unless the agent is set to
bypassPermissions — which is the unsafe workaround. This is the key external-run design hole.
Options (pick per product intent, document the choice):
allow/deny from a non-interactive policy (the agent's permission_mode + the
per-channel tool allow/deny list), never "ask". An unlisted approval-required tool denies with a
clear, model-visible reason ("not permitted on this channel"), so the agent can continue or
explain rather than hang.v1 let a channel override the agent's permission_mode; v2 dropped it when config moved onto the
agent (ChannelMessageHandler.ts:202 TODO). Without it, a channel can't be made stricter than
its agent (e.g. read-only tools for an otherwise-broad agent). Proposed direction: a per-channel
permission_mode + tool allow/deny override threaded as a per-dispatch option into
startAgentSessionRun → the Claude Code toolPolicySnapshot, applied on top of the agent's
policy (channel can only narrow, never widen). This is also the lever G2's policy-driven
option reads from.
Channels are an opt-in, high-trust feature. Document the conservative defaults: enable only for
trusted workspaces; require explicit chat allow-listing; give channel-bound agents a read-only
tool set; do not use bypassPermissions for a channel-connected agent. G2 is the first to fix
because today's only "make tools work on a channel" answer (bypassPermissions) is the least safe.
Not implemented in this PR — parity with v1 (which also had no inbound auth). Tracked as a follow-up; this doc is the design the reviewer (D1) asked for.