Files
openclaw/docs/plugins/sdk-channel-ingress.md
iloveleon19 382d570cbf feat(channels): add unified implicit mention policy (#108829)
* feat(mattermost): add thread.requireExplicitMention to opt out of thread auto-follow

Mattermost treats any reply in a thread the bot has participated in as an
implicit mention, so requireMention only gates the first message and the bot
then answers follow-ups addressed to other people for the participation TTL.
Slack exposes channels.slack.thread.requireExplicitMention for exactly this;
Mattermost had no equivalent and its strict schema rejected the key.

Add channels.mattermost.thread.requireExplicitMention (channel + per-account),
mirroring Slack. When set, thread participation no longer counts as a mention.
Default (unset/false) keeps today's auto-follow behavior unchanged.

Related: #108269

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(channels): add implicit mention policy foundation

* feat(channels): unify implicit mention policy

Co-authored-by: leon <dodoma0919@gmail.com>

* refactor(channels): keep implicit policy inside evaluator

* fix(channels): use exported implicit mention type

* chore(channels): satisfy extension lint

* fix(config): break implicit mention type cycle

* fix(plugin-sdk): account for implicit mention config export

* refactor(config): isolate implicit mention schema

* chore(plugin-sdk): align implicit mention surface budget

* fix(config): remove unused schema re-export

---------

Co-authored-by: leon <dodoma0919@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-07-16 17:16:01 -07:00

140 lines
5.3 KiB
Markdown

---
summary: "Experimental channel ingress API for inbound message authorization"
read_when:
- Building or migrating a messaging channel plugin
- Changing DM or group allowlists, route gates, command auth, event auth, or mention activation
- Reviewing channel ingress redaction or SDK compatibility boundaries
title: "Channel ingress API"
sidebarTitle: "Channel Ingress"
---
Channel ingress is the experimental access-control boundary for inbound
channel events. Plugins own platform facts and side effects; core owns
generic policy: DM/group allowlists, pairing-store DM entries, route gates,
command gates, event auth, mention activation, redacted diagnostics, and
admission.
Use `openclaw/plugin-sdk/channel-ingress-runtime` for receive paths.
## Runtime resolver
```ts
import {
defineStableChannelIngressIdentity,
resolveChannelMessageIngress,
} from "openclaw/plugin-sdk/channel-ingress-runtime";
const identity = defineStableChannelIngressIdentity({
key: "platform-user-id",
normalize: normalizePlatformUserId,
sensitivity: "pii",
});
const result = await resolveChannelMessageIngress({
channelId: "my-channel",
accountId,
identity,
subject: { stableId: platformUserId },
conversation: { kind: isGroup ? "group" : "direct", id: conversationId },
event: { kind: "message", authMode: "inbound", mayPair: !isGroup },
policy: {
dmPolicy: config.dmPolicy,
groupPolicy: config.groupPolicy,
groupAllowFromFallbackToAllowFrom: true,
},
allowFrom: config.allowFrom,
groupAllowFrom: config.groupAllowFrom,
accessGroups: cfg.accessGroups,
route,
readStoreAllowFrom,
command: hasControlCommand ? { allowTextCommands: true, hasControlCommand } : undefined,
});
```
Do not precompute effective allowlists, command owners, or command groups.
The resolver derives them from raw allowlists, store callbacks, route
descriptors, access groups, policy, and conversation kind.
## Result
Bundled plugins should consume modern projections directly:
| Field | Meaning |
| ------------------ | ------------------------------------------------------------------ |
| `ingress` | ordered gate decision and admission |
| `senderAccess` | sender/conversation authorization only |
| `routeAccess` | route and route-sender projection |
| `commandAccess` | command authorization; `requested: false` when no command gate ran |
| `activationAccess` | mention/activation result |
Event authorization stays available on the ordered `ingress.graph` and the
decisive `ingress.reasonCode`; no separate event projection is emitted.
Deprecated third-party SDK helpers may rebuild older shapes internally. New
bundled receive paths should not translate modern results back into local
DTOs.
## Access groups
`accessGroup:<name>` entries stay redacted. Core resolves static
`message.senders` groups itself and calls `resolveAccessGroupMembership` only
for dynamic groups that require a platform lookup. Missing, unsupported, and
failed groups fail closed.
## Event modes
| `authMode` | Meaning |
| ---------------- | ------------------------------------------------ |
| `inbound` | normal inbound sender gates |
| `command` | command gates for callbacks or scoped buttons |
| `origin-subject` | actor must match the original message subject |
| `route-only` | route gates only for route-scoped trusted events |
| `none` | plugin-owned internal events bypass shared auth |
Use `mayPair: false` for reactions, buttons, callbacks, and native commands.
## Routes and activation
Use route descriptors for room, topic, guild, thread, or nested route policy:
```ts
route: {
id: "room",
allowed: roomAllowed,
enabled: roomEnabled,
senderPolicy: "replace",
senderAllowFrom: roomAllowFrom,
blockReason: "room_sender_not_allowlisted",
}
```
Use `channelIngressRoutes(...)` when a plugin has several optional route
descriptors; it filters disabled branches while keeping route facts generic
and ordered by each descriptor's `precedence`.
Mention gating is an activation gate. A mention miss returns
`admission: "skip"` so the turn kernel does not process an observe-only turn.
Most channels should leave activation after sender and command gates. Public
chat surfaces that must quiet non-mentioned traffic before sender allowlist
noise can opt into `activation.order: "before-sender"` when text-command
bypass is disabled. Channels with implicit activation, such as replies in bot
threads, resolve `channels.defaults.implicitMentions` plus channel and account
overrides with `resolveChannelImplicitMentions(...)`, then pass the result as
`activation.implicitMentions`. The projected
`activationAccess.shouldBypassMention` reports when command or implicit
activation bypassed an explicit mention.
## Redaction
Raw sender values and raw allowlist entries are resolver input only. They
must not appear in resolved state, decisions, diagnostics, snapshots, or
compatibility facts. Use opaque subject ids, entry ids, route ids, and
diagnostic ids.
## Verification
```bash
pnpm test src/channels/message-access/message-access.test.ts src/plugin-sdk/channel-ingress-runtime.test.ts
pnpm plugin-sdk:api:check
```