mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-19 00:14:46 +00:00
refactor: centralize channel ingress access
This commit is contained in:
@@ -1,2 +1,2 @@
|
||||
f303fc2fea17115736f9b7565ab2b6cfa07cfb46e5b3c804c55a945922f749db plugin-sdk-api-baseline.json
|
||||
90a4a9538691ee1af4d1fcf10b3b0c1d161e835c03e87b09dc0ff0020d1ded7a plugin-sdk-api-baseline.jsonl
|
||||
d0bbcee28603940444071492f65e293e708d2b437bdc6f0fb492fc41a0920760 plugin-sdk-api-baseline.json
|
||||
f6a73cf469f279671ab00ff5a0e95eb0ff89d8a8b9652cf1ff9cd983aa69093d plugin-sdk-api-baseline.jsonl
|
||||
|
||||
@@ -59,6 +59,22 @@
|
||||
"source": "Channel message API",
|
||||
"target": "频道消息 API"
|
||||
},
|
||||
{
|
||||
"source": "Channel ingress API",
|
||||
"target": "频道入口 API"
|
||||
},
|
||||
{
|
||||
"source": "Channel access cleanup",
|
||||
"target": "频道访问清理"
|
||||
},
|
||||
{
|
||||
"source": "Ingress core shrink plan",
|
||||
"target": "入口核心精简计划"
|
||||
},
|
||||
{
|
||||
"source": "Ingress core shrink",
|
||||
"target": "入口核心精简"
|
||||
},
|
||||
{
|
||||
"source": "Talk mode",
|
||||
"target": "Talk 模式"
|
||||
|
||||
@@ -125,7 +125,26 @@ Access groups are available in shared message-channel authorization paths, inclu
|
||||
- channel-specific per-room sender allowlists that use the same sender matching rules
|
||||
- command authorization paths that reuse message-channel sender allowlists
|
||||
|
||||
Channel support depends on whether that channel is wired through the shared OpenClaw sender-authorization helpers. Current bundled support includes Discord, Google Chat, Nostr, WhatsApp, Zalo, and Zalo Personal. Static `message.senders` groups are designed to be channel-agnostic, so new message channels should support them by using the shared plugin SDK helpers instead of custom allowlist expansion.
|
||||
Channel support depends on whether that channel is wired through the shared OpenClaw sender-authorization helpers. Current bundled support includes BlueBubbles, Discord, Feishu, Google Chat, iMessage, LINE, Mattermost, Microsoft Teams, Nextcloud Talk, Nostr, QQBot, Signal, WhatsApp, Zalo, and Zalo Personal. Static `message.senders` groups are designed to be channel-agnostic, so new message channels should support them by using the shared plugin SDK helpers instead of custom allowlist expansion.
|
||||
|
||||
## Plugin diagnostics
|
||||
|
||||
Plugin authors can inspect structured access-group state without expanding it back into a flat allowlist:
|
||||
|
||||
```typescript
|
||||
import { resolveAccessGroupAllowFromState } from "openclaw/plugin-sdk/security-runtime";
|
||||
|
||||
const state = await resolveAccessGroupAllowFromState({
|
||||
accessGroups: cfg.accessGroups,
|
||||
allowFrom: channelConfig.allowFrom,
|
||||
channel: "my-channel",
|
||||
accountId: "default",
|
||||
senderId,
|
||||
isSenderAllowed,
|
||||
});
|
||||
```
|
||||
|
||||
The result reports referenced, matched, missing, unsupported, and failed groups. Use this when you need diagnostics or conformance tests. Use `expandAllowFromWithAccessGroups(...)` only for compatibility paths that still expect a flat `allowFrom` array.
|
||||
|
||||
## Discord channel audiences
|
||||
|
||||
|
||||
@@ -451,8 +451,8 @@ Example:
|
||||
|
||||
</Tab>
|
||||
|
||||
<Tab title="DM access groups">
|
||||
Discord DMs can use dynamic `accessGroup:<name>` entries in `channels.discord.allowFrom`.
|
||||
<Tab title="Access groups">
|
||||
Discord DMs and text command authorization can use dynamic `accessGroup:<name>` entries in `channels.discord.allowFrom`.
|
||||
|
||||
Access group names are shared across message channels. Use `type: "message.senders"` for a static group whose members are expressed in each channel's normal `allowFrom` syntax, or `type: "discord.channelAudience"` when a Discord channel's current `ViewChannel` audience should define membership dynamically. Shared access-group behavior is documented here: [Access groups](/channels/access-groups).
|
||||
|
||||
|
||||
@@ -217,7 +217,7 @@ If SIP-disabled isn't acceptable for your threat model:
|
||||
|
||||
Allowlist field: `channels.imessage.allowFrom`.
|
||||
|
||||
Allowlist entries can be handles or chat targets (`chat_id:*`, `chat_guid:*`, `chat_identifier:*`).
|
||||
Allowlist entries can be handles, static sender access groups (`accessGroup:<name>`), or chat targets (`chat_id:*`, `chat_guid:*`, `chat_identifier:*`).
|
||||
|
||||
</Tab>
|
||||
|
||||
@@ -230,6 +230,8 @@ If SIP-disabled isn't acceptable for your threat model:
|
||||
|
||||
Group sender allowlist: `channels.imessage.groupAllowFrom`.
|
||||
|
||||
`groupAllowFrom` entries can also reference static sender access groups (`accessGroup:<name>`).
|
||||
|
||||
Runtime fallback: if `groupAllowFrom` is unset, iMessage group sender checks fall back to `allowFrom` when available.
|
||||
Runtime note: if `channels.imessage` is completely missing, runtime falls back to `groupPolicy="allowlist"` and logs a warning (even if `channels.defaults.groupPolicy` is set).
|
||||
|
||||
|
||||
@@ -139,6 +139,7 @@ Allowlists and policies:
|
||||
- `channels.line.groupPolicy`: `allowlist | open | disabled`
|
||||
- `channels.line.groupAllowFrom`: allowlisted LINE user IDs for groups
|
||||
- Per-group overrides: `channels.line.groups.<groupId>.allowFrom`
|
||||
- Static sender access groups can be referenced from `allowFrom`, `groupAllowFrom`, and per-group `allowFrom` with `accessGroup:<name>`.
|
||||
- Runtime note: if `channels.line` is completely missing, runtime falls back to `groupPolicy="allowlist"` for group checks (even if `channels.defaults.groupPolicy` is set).
|
||||
|
||||
LINE IDs are case-sensitive. Valid IDs look like:
|
||||
|
||||
@@ -189,11 +189,13 @@ Notes:
|
||||
- `openclaw pairing list mattermost`
|
||||
- `openclaw pairing approve mattermost <CODE>`
|
||||
- Public DMs: `channels.mattermost.dmPolicy="open"` plus `channels.mattermost.allowFrom=["*"]`.
|
||||
- `channels.mattermost.allowFrom` accepts `accessGroup:<name>` entries. See [Access groups](/channels/access-groups).
|
||||
|
||||
## Channels (groups)
|
||||
|
||||
- Default: `channels.mattermost.groupPolicy = "allowlist"` (mention-gated).
|
||||
- Allowlist senders with `channels.mattermost.groupAllowFrom` (user IDs recommended).
|
||||
- `channels.mattermost.groupAllowFrom` accepts `accessGroup:<name>` entries. See [Access groups](/channels/access-groups).
|
||||
- Per-channel mention overrides live under `channels.mattermost.groups.<channelId>.requireMention` or `channels.mattermost.groups["*"].requireMention` for a default.
|
||||
- `@username` matching is mutable and only enabled when `channels.mattermost.dangerouslyAllowNameMatching: true`.
|
||||
- Open channels: `channels.mattermost.groupPolicy="open"` (mention-gated).
|
||||
|
||||
@@ -146,14 +146,14 @@ Disable with:
|
||||
**DM access**
|
||||
|
||||
- Default: `channels.msteams.dmPolicy = "pairing"`. Unknown senders are ignored until approved.
|
||||
- `channels.msteams.allowFrom` should use stable AAD object IDs.
|
||||
- `channels.msteams.allowFrom` should use stable AAD object IDs or static sender access groups such as `accessGroup:core-team`.
|
||||
- Do not rely on UPN/display-name matching for allowlists - they can change. OpenClaw disables direct name matching by default; opt in explicitly with `channels.msteams.dangerouslyAllowNameMatching: true`.
|
||||
- The wizard can resolve names to IDs via Microsoft Graph when credentials allow.
|
||||
|
||||
**Group access**
|
||||
|
||||
- Default: `channels.msteams.groupPolicy = "allowlist"` (blocked unless you add `groupAllowFrom`). Use `channels.defaults.groupPolicy` to override the default when unset.
|
||||
- `channels.msteams.groupAllowFrom` controls which senders can trigger in group chats/channels (falls back to `channels.msteams.allowFrom`).
|
||||
- `channels.msteams.groupAllowFrom` controls which senders or static sender access groups can trigger in group chats/channels (falls back to `channels.msteams.allowFrom`).
|
||||
- Set `groupPolicy: "open"` to allow any member (still mention-gated by default).
|
||||
- To allow **no channels**, set `channels.msteams.groupPolicy: "disabled"`.
|
||||
|
||||
@@ -164,7 +164,7 @@ Example:
|
||||
channels: {
|
||||
msteams: {
|
||||
groupPolicy: "allowlist",
|
||||
groupAllowFrom: ["user@org.com"],
|
||||
groupAllowFrom: ["00000000-0000-0000-0000-000000000000", "accessGroup:core-team"],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -157,6 +157,7 @@ Provider options:
|
||||
- `channels.nextcloud-talk.groupPolicy`: `allowlist | open | disabled`.
|
||||
- `channels.nextcloud-talk.groupAllowFrom`: group allowlist (user IDs).
|
||||
- `channels.nextcloud-talk.rooms`: per-room settings and allowlist.
|
||||
- Static sender access groups can be referenced from `allowFrom` and `groupAllowFrom` with `accessGroup:<name>`.
|
||||
- `channels.nextcloud-talk.historyLimit`: group history limit (0 disables).
|
||||
- `channels.nextcloud-talk.dmHistoryLimit`: DM history limit (0 disables).
|
||||
- `channels.nextcloud-talk.dms`: per-DM overrides (historyLimit).
|
||||
|
||||
@@ -44,7 +44,14 @@ Account keys:
|
||||
- `botUserId` - Matrix-style bot user id used in target grammar.
|
||||
- `botDisplayName` - display name for outbound messages.
|
||||
- `pollTimeoutMs` - long-poll wait window. Integer between 100 and 30000.
|
||||
- `allowFrom` - sender allowlist (user ids or `"*"`).
|
||||
- `allowFrom` - sender allowlist (user ids or `"*"`). Direct messages and
|
||||
allowlisted group policy both use these synthetic sender ids.
|
||||
- `groupPolicy` - shared-room policy: `"open"` (default), `"allowlist"`, or
|
||||
`"disabled"`.
|
||||
- `groupAllowFrom` - optional shared-room sender allowlist. When omitted under
|
||||
`"allowlist"`, QA Channel falls back to `allowFrom`.
|
||||
- `groups.<room>.requireMention` - require a bot mention before replying in a
|
||||
specific group/channel room. `groups."*"` sets the default.
|
||||
- `defaultTo` - fallback target when none is supplied.
|
||||
- `actions.messages` / `actions.reactions` / `actions.search` / `actions.threads` - per-action tool gating.
|
||||
|
||||
|
||||
@@ -81,7 +81,7 @@ openclaw directory groups list --channel zalouser --query "work"
|
||||
|
||||
`channels.zalouser.dmPolicy` supports: `pairing | allowlist | open | disabled` (default: `pairing`).
|
||||
|
||||
`channels.zalouser.allowFrom` should use stable Zalo user IDs. During interactive setup, entered names can be resolved to IDs using the plugin's in-process contact lookup.
|
||||
`channels.zalouser.allowFrom` should use stable Zalo user IDs. It can also reference static sender access groups (`accessGroup:<name>`). During interactive setup, entered names can be resolved to IDs using the plugin's in-process contact lookup.
|
||||
|
||||
If a raw name remains in config, startup resolves it only when `channels.zalouser.dangerouslyAllowNameMatching: true` is enabled. Without that opt-in, runtime sender checks are ID-only and raw names are ignored for authorization.
|
||||
|
||||
@@ -96,7 +96,7 @@ Approve via:
|
||||
- Restrict to an allowlist with:
|
||||
- `channels.zalouser.groupPolicy = "allowlist"`
|
||||
- `channels.zalouser.groups` (keys should be stable group IDs; names are resolved to IDs on startup only when `channels.zalouser.dangerouslyAllowNameMatching: true` is enabled)
|
||||
- `channels.zalouser.groupAllowFrom` (controls which senders in allowed groups can trigger the bot)
|
||||
- `channels.zalouser.groupAllowFrom` (controls which senders in allowed groups can trigger the bot; static sender access groups can be referenced with `accessGroup:<name>`)
|
||||
- Block all groups: `channels.zalouser.groupPolicy = "disabled"`.
|
||||
- The configure wizard can prompt for group allowlists.
|
||||
- On startup, OpenClaw resolves group/user names in allowlists to IDs and logs the mapping only when `channels.zalouser.dangerouslyAllowNameMatching: true` is enabled.
|
||||
|
||||
137
docs/plugins/sdk-channel-ingress.md
Normal file
137
docs/plugins/sdk-channel-ingress.md
Normal file
@@ -0,0 +1,137 @@
|
||||
---
|
||||
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 API
|
||||
|
||||
Channel ingress is the experimental access-control boundary for inbound channel
|
||||
events. Use `openclaw/plugin-sdk/channel-ingress-runtime` for receive paths.
|
||||
The older `openclaw/plugin-sdk/channel-ingress` subpath stays exported as a
|
||||
deprecated compatibility facade for third-party plugins.
|
||||
|
||||
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.
|
||||
|
||||
## 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:
|
||||
|
||||
- `ingress`: ordered gate decision and admission
|
||||
- `senderAccess`: sender/conversation authorization only
|
||||
- `routeAccess`: route and route-sender projection
|
||||
- `commandAccess`: command authorization; false when no command gate ran
|
||||
- `activationAccess`: mention/activation result
|
||||
|
||||
Event authorization remains 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, can pass `activation.allowedImplicitMentionKinds`; the projected
|
||||
`activationAccess.shouldBypassMention` then 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
|
||||
```
|
||||
@@ -74,6 +74,16 @@ remain available for compatibility dispatchers. Do not use those names for new
|
||||
channel code; new plugins should start with the `message` adapter, receipts, and
|
||||
receive/send lifecycle helpers on `openclaw/plugin-sdk/channel-message`.
|
||||
|
||||
Channels migrating inbound authorization can use the experimental
|
||||
`openclaw/plugin-sdk/channel-ingress-runtime` subpath from runtime receive
|
||||
paths. The subpath keeps platform lookup and side effects in the plugin, while
|
||||
sharing allowlist state resolution, route/sender/command/event/activation
|
||||
decisions, redacted diagnostics, and turn-admission mapping. Keep plugin
|
||||
identity normalization in the descriptor you pass to the resolver; do not
|
||||
serialize raw match values from the resolved state or decision. See
|
||||
[Channel ingress API](/plugins/sdk-channel-ingress) for the API design,
|
||||
ownership boundary, and test expectations.
|
||||
|
||||
If your channel supports typing indicators outside inbound replies, expose
|
||||
`heartbeat.sendTyping(...)` on the channel plugin. Core calls it with the
|
||||
resolved heartbeat delivery target before the heartbeat model run starts and
|
||||
|
||||
@@ -64,6 +64,7 @@ The runtime exposes three preferred entry points so adapters can opt in at the l
|
||||
|
||||
```typescript
|
||||
runtime.channel.turn.run(...) // adapter-driven full pipeline
|
||||
runtime.channel.turn.runAssembled(...) // already-built context + delivery adapter
|
||||
runtime.channel.turn.runPrepared(...) // channel owns dispatch; kernel runs record + finalize
|
||||
runtime.channel.turn.buildContext(...) // pure facts to FinalizedMsgContext mapping
|
||||
```
|
||||
@@ -72,7 +73,7 @@ Two older runtime helpers remain available for Plugin SDK compatibility:
|
||||
|
||||
```typescript
|
||||
runtime.channel.turn.runResolved(...) // deprecated compatibility alias; prefer run
|
||||
runtime.channel.turn.dispatchAssembled(...) // deprecated compatibility alias; prefer run or runPrepared
|
||||
runtime.channel.turn.dispatchAssembled(...) // deprecated compatibility alias; prefer runAssembled
|
||||
```
|
||||
|
||||
### run
|
||||
@@ -114,6 +115,41 @@ await runtime.channel.turn.run({
|
||||
|
||||
`run` is the right shape when the channel has small adapter logic and benefits from owning the lifecycle through hooks.
|
||||
|
||||
### runAssembled
|
||||
|
||||
Use when the channel has already resolved routing, built a `FinalizedMsgContext`,
|
||||
and only needs the shared record, reply-pipeline, dispatch, and finalize
|
||||
ordering. This is the preferred shape for simple bundled inbound paths that
|
||||
would otherwise repeat `createChannelMessageReplyPipeline(...)` and
|
||||
`runPrepared(...)` boilerplate.
|
||||
|
||||
```typescript
|
||||
await runtime.channel.turn.runAssembled({
|
||||
cfg,
|
||||
channel: "irc",
|
||||
accountId,
|
||||
agentId: route.agentId,
|
||||
routeSessionKey: route.sessionKey,
|
||||
storePath,
|
||||
ctxPayload,
|
||||
recordInboundSession: runtime.channel.session.recordInboundSession,
|
||||
dispatchReplyWithBufferedBlockDispatcher:
|
||||
runtime.channel.reply.dispatchReplyWithBufferedBlockDispatcher,
|
||||
delivery: {
|
||||
deliver: async (payload) => {
|
||||
await sendPlatformReply(payload);
|
||||
},
|
||||
onError: (err, info) => {
|
||||
runtime.error?.(`reply ${info.kind} failed: ${String(err)}`);
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Choose `runAssembled` over `runPrepared` when the only channel-owned dispatch
|
||||
behavior is final payload delivery plus optional typing, reply options, durable
|
||||
delivery, or error logging.
|
||||
|
||||
### runPrepared
|
||||
|
||||
Use when the channel has a complex local dispatcher with previews, retries, edits, or thread bootstrap that must stay channel-owned. The kernel still records the inbound session before dispatch and surfaces a uniform `DispatchedChannelTurnResult`.
|
||||
|
||||
@@ -56,6 +56,7 @@ For the plugin authoring guide, see [Plugin SDK overview](/plugins/sdk-overview)
|
||||
| `plugin-sdk/account-id` | `DEFAULT_ACCOUNT_ID`, account-id normalization helpers |
|
||||
| `plugin-sdk/account-resolution` | Account lookup + default-fallback helpers |
|
||||
| `plugin-sdk/account-helpers` | Narrow account-list/account-action helpers |
|
||||
| `plugin-sdk/access-groups` | Access-group allowlist parsing and redacted group diagnostics helpers |
|
||||
| `plugin-sdk/channel-pairing` | `createChannelPairingController` |
|
||||
| `plugin-sdk/channel-reply-pipeline` | Legacy reply pipeline helpers. New channel reply pipeline code should use `createChannelMessageReplyPipeline` and `resolveChannelMessageSourceReplyDeliveryMode` from `plugin-sdk/channel-message`. |
|
||||
| `plugin-sdk/channel-config-helpers` | `createHybridChannelConfigAdapter`, `resolveChannelDmAccess`, `resolveChannelDmAllowFrom`, `resolveChannelDmPolicy`, `normalizeChannelDmPolicy`, `normalizeLegacyDmAliases` |
|
||||
@@ -65,6 +66,8 @@ For the plugin authoring guide, see [Plugin SDK overview](/plugins/sdk-overview)
|
||||
| `plugin-sdk/telegram-command-config` | Telegram custom-command normalization/validation helpers with bundled-contract fallback |
|
||||
| `plugin-sdk/command-gating` | Narrow command authorization gate helpers |
|
||||
| `plugin-sdk/channel-policy` | `resolveChannelGroupRequireMention` |
|
||||
| `plugin-sdk/channel-ingress` | Deprecated low-level channel ingress compatibility facade. New receive paths should use `plugin-sdk/channel-ingress-runtime`. |
|
||||
| `plugin-sdk/channel-ingress-runtime` | Experimental high-level channel ingress runtime resolver and route fact builders for migrated channel receive paths. Prefer this over assembling effective allowlists, command allowlists, and legacy projections in each plugin. See [Channel ingress API](/plugins/sdk-channel-ingress). |
|
||||
| `plugin-sdk/channel-lifecycle` | `createAccountStatusSink`, `createChannelRunQueue`, and legacy draft stream lifecycle helpers. New preview finalization code should use `plugin-sdk/channel-message`. |
|
||||
| `plugin-sdk/channel-message` | Cheap message lifecycle contract helpers such as `defineChannelMessageAdapter`, `createChannelMessageAdapterFromOutbound`, `createChannelMessageReplyPipeline`, `createReplyPrefixContext`, `resolveChannelMessageSourceReplyDeliveryMode`, durable-final capability derivation, capability proof helpers for send/receipt/side-effect capabilities, `MessageReceiveContext`, receive ack policy proofs, `defineFinalizableLivePreviewAdapter`, `deliverWithFinalizableLivePreviewAdapter`, live-preview and live-finalizer capability proofs, durable recovery state, `RenderedMessageBatch`, message receipt types, and receipt id helpers. See [Channel message API](/plugins/sdk-channel-message). Legacy reply-dispatch facades are deprecated compatibility only. |
|
||||
| `plugin-sdk/channel-message-runtime` | Runtime delivery helpers that may load outbound delivery, including `deliverInboundReplyWithMessageSendContext`, `sendDurableMessageBatch`, and `withDurableMessageSendContext`. Deprecated reply-dispatch bridges remain importable for compatibility dispatchers only. Use from monitor/send runtime modules, not hot plugin bootstrap files. |
|
||||
|
||||
9
docs/refactor/access.md
Normal file
9
docs/refactor/access.md
Normal file
@@ -0,0 +1,9 @@
|
||||
---
|
||||
summary: "Redirect to /refactor/ingress-core"
|
||||
read_when:
|
||||
- Changing channel ingress, sender access, command authorization, or access-group handling
|
||||
title: "Channel access cleanup"
|
||||
sidebarTitle: "Channel access cleanup"
|
||||
---
|
||||
|
||||
The channel access cleanup plan now lives in [Ingress core deletion plan](/refactor/ingress-core).
|
||||
341
docs/refactor/ingress-core.md
Normal file
341
docs/refactor/ingress-core.md
Normal file
@@ -0,0 +1,341 @@
|
||||
---
|
||||
summary: "Deletion-first plan for moving repeated channel ingress glue into core."
|
||||
read_when:
|
||||
- Auditing why the channel ingress refactor added too much code
|
||||
- Moving route, command, event, activation, or access-group policy from bundled plugins into core
|
||||
- Reviewing whether a channel ingress helper actually deletes bundled plugin code
|
||||
title: "Ingress core deletion plan"
|
||||
sidebarTitle: "Ingress core deletion"
|
||||
---
|
||||
|
||||
# Ingress core deletion plan
|
||||
|
||||
The ingress refactor is not healthy while it adds thousands of net lines. Core
|
||||
centralization only counts when bundled plugin production code gets smaller and
|
||||
old third-party SDK compatibility is quarantined to SDK/core shims.
|
||||
|
||||
Desired runtime shape:
|
||||
|
||||
```text
|
||||
bundled plugin event
|
||||
-> extract platform facts locally
|
||||
-> resolve shared ingress once when facts are available
|
||||
-> branch on generic ingress projections/outcomes
|
||||
-> perform platform side effects locally
|
||||
|
||||
old third-party helper
|
||||
-> SDK compatibility shim
|
||||
-> shared ingress-compatible projection where possible
|
||||
-> old return shape preserved
|
||||
```
|
||||
|
||||
Bundled plugins should not translate ingress back into local `AccessResult`,
|
||||
`GroupAccessDecision`, `CommandAuthDecision`, `DmCommandAccess`, or
|
||||
`{ allowed, reasonCode }` shapes unless that type is public plugin API.
|
||||
|
||||
## Budget
|
||||
|
||||
Measured against the PR merge-base with `origin/main`, including untracked
|
||||
files.
|
||||
|
||||
```text
|
||||
merge-base 1671e7532adb
|
||||
|
||||
current:
|
||||
core production +3,922 / -546 = +3,376
|
||||
docs +601 / -17 = +584
|
||||
other +145 / -2 = +143
|
||||
plugin production +4,148 / -5,388 = -1,240
|
||||
tests +2,326 / -2,414 = -88
|
||||
total +11,142 / -8,367 = +2,775
|
||||
|
||||
required:
|
||||
plugin production <= -1,500
|
||||
core production <= +1,500, or paid for by larger plugin deletion
|
||||
tests <= +1,000
|
||||
total <= +2,000
|
||||
|
||||
stretch:
|
||||
plugin production <= -2,500
|
||||
core production <= +1,200
|
||||
total <= 0
|
||||
```
|
||||
|
||||
Minimum remaining cleanup:
|
||||
|
||||
```text
|
||||
plugin production needs 260 more net deleted lines
|
||||
total needs 775 more net deleted lines
|
||||
core production still +1,876 over standalone budget, unless paid down by plugin deletion
|
||||
```
|
||||
|
||||
Comment-only deletion does not count as cleanup. The previous budget pass was
|
||||
too generous because it included restored QQBot explanatory comments; this
|
||||
document tracks executable/docs/test code movement only.
|
||||
|
||||
Re-measure after each cleanup wave:
|
||||
|
||||
```sh
|
||||
base=$(git merge-base HEAD origin/main)
|
||||
git diff --shortstat "$base"
|
||||
git diff --numstat "$base" -- src/channels/message-access src/plugin-sdk extensions | sort -nr -k1 | head -n 80
|
||||
pnpm lint:extensions:no-deprecated-channel-access
|
||||
```
|
||||
|
||||
## Diagnosis
|
||||
|
||||
The first pass added the shared ingress kernel, then left too much plugin-local
|
||||
authorization beside it:
|
||||
|
||||
```text
|
||||
platform facts
|
||||
-> shared ingress state and decision
|
||||
-> plugin-local DTO or legacy projection
|
||||
-> plugin-local if/else ladder
|
||||
```
|
||||
|
||||
That duplicates the model. Core production grew by about 3,376 lines, while
|
||||
bundled plugin production is 1,240 lines smaller. That is better than the first
|
||||
pass, but it is not inside the minimum budget. The fix remains deletion-first:
|
||||
|
||||
- delete plugin DTOs that only rename ingress fields
|
||||
- delete tests that only assert wrapper shape
|
||||
- add core helpers only when the same patch deletes bundled plugin code
|
||||
- keep old SDK compatibility in SDK/core shims only
|
||||
- repack core after wrapper deletion exposes the stable shape
|
||||
|
||||
## Hotspots
|
||||
|
||||
Positive bundled production files that still need to shrink:
|
||||
|
||||
```text
|
||||
extensions/telegram/src/ingress.ts +126
|
||||
extensions/discord/src/monitor/dm-command-auth.ts +101
|
||||
extensions/signal/src/monitor/access-policy.ts +92
|
||||
extensions/feishu/src/policy.ts +85
|
||||
extensions/slack/src/monitor/auth.ts +64
|
||||
extensions/googlechat/src/monitor-access.ts +59
|
||||
extensions/nextcloud-talk/src/inbound.ts +51
|
||||
extensions/matrix/src/matrix/monitor/access-state.ts +49
|
||||
extensions/irc/src/inbound.ts +44
|
||||
extensions/imessage/src/monitor/inbound-processing.ts +36
|
||||
extensions/qa-channel/src/inbound.ts +34
|
||||
extensions/qqbot/src/bridge/sdk-adapter.ts +33
|
||||
extensions/tlon/src/monitor/utils.ts +30
|
||||
extensions/twitch/src/access-control.ts +22
|
||||
extensions/qqbot/src/engine/commands/slash-command-handler.ts +20
|
||||
extensions/telegram/src/bot-handlers.runtime.ts +19
|
||||
```
|
||||
|
||||
The branch is not inside the minimum budget yet. The remaining review-relevant
|
||||
work should delete repeated authorization flow, turn scaffolding, or wrapper
|
||||
tests before adding another core abstraction.
|
||||
|
||||
## Current Code Read
|
||||
|
||||
The healthy core seam already exists in `src/channels/message-access/runtime.ts`:
|
||||
it owns identity adapters, effective allowlists, pairing-store reads, route
|
||||
descriptors, command/event presets, access groups, and the final resolved
|
||||
`ResolvedChannelMessageIngress` projection.
|
||||
|
||||
The remaining growth is mostly plugin glue layered on top of that seam:
|
||||
|
||||
- `extensions/telegram/src/ingress.ts` wraps core decisions in Telegram-specific
|
||||
command/event helpers, then call sites still pass precomputed normalized
|
||||
allowlists and owner lists.
|
||||
- `extensions/discord/src/monitor/dm-command-auth.ts`,
|
||||
`extensions/feishu/src/policy.ts`, `extensions/googlechat/src/monitor-access.ts`,
|
||||
and `extensions/matrix/src/matrix/monitor/access-state.ts` still keep
|
||||
local policy DTOs or legacy decision names beside ingress.
|
||||
- `extensions/signal/src/monitor/access-policy.ts` correctly keeps Signal
|
||||
identity normalization and pairing replies local, but still has a wrapper
|
||||
seam that should collapse into direct ingress consumption.
|
||||
- `extensions/nextcloud-talk/src/inbound.ts`, `extensions/irc/src/inbound.ts`,
|
||||
`extensions/qa-channel/src/inbound.ts`, `extensions/zalo/src/monitor.ts`, and
|
||||
`extensions/zalouser/src/monitor.ts` still repeat route/envelope/turn
|
||||
assembly that can move to shared turn helpers outside the ingress kernel.
|
||||
|
||||
Conclusion: moving more code into core is only useful if it deletes these
|
||||
plugin wrapper layers in the same patch. Adding another abstraction while
|
||||
leaving wrapper returns in place repeats the mistake.
|
||||
|
||||
## Boundary
|
||||
|
||||
Core owns generic policy:
|
||||
|
||||
- allowlist normalization and matching
|
||||
- access-group expansion and diagnostics
|
||||
- pairing-store DM allowlist reads
|
||||
- route, sender, command, event, and activation gates
|
||||
- admission mapping: dispatch, drop, skip, observe, pairing
|
||||
- redacted state, decisions, diagnostics, and SDK compatibility projections
|
||||
- reusable generic descriptors for identity, route, command, event, activation,
|
||||
and outcomes
|
||||
|
||||
Plugins own transport facts and side effects:
|
||||
|
||||
- webhook/socket/request authenticity
|
||||
- platform identity extraction and API lookups
|
||||
- channel-specific policy defaults
|
||||
- pairing challenge delivery, replies, acks, reactions, typing, media, history,
|
||||
setup, doctor, status, logs, and user-facing copy
|
||||
|
||||
Core must stay channel-agnostic: no Discord, Slack, Telegram, Matrix, room,
|
||||
guild, space, API client, or plugin-specific default in
|
||||
`src/channels/message-access`.
|
||||
|
||||
## Acceptance Rule
|
||||
|
||||
Every new core helper must delete bundled plugin production code immediately.
|
||||
|
||||
```text
|
||||
one bundled caller reject; keep plugin-local
|
||||
two bundled callers accept only if plugin production LOC drops
|
||||
three or more callers plugin deletion must be at least 2x new core LOC
|
||||
compatibility-only helper SDK/core shim only; never bundled hot paths
|
||||
```
|
||||
|
||||
Stop and redesign if:
|
||||
|
||||
- plugin production LOC increases
|
||||
- tests grow faster than production shrinks
|
||||
- a bundled hot path returns a DTO that only renames `ResolvedChannelMessageIngress`
|
||||
- a core helper needs a channel id, platform object, API client, or
|
||||
channel-specific default
|
||||
|
||||
## Work Packages
|
||||
|
||||
1. Freeze the budget.
|
||||
Put LOC in the PR, keep deprecated-ingress lint green, and include before/after
|
||||
LOC in cleanup commits.
|
||||
|
||||
2. Delete thin DTO seams.
|
||||
Replace plugin-local wrapper returns with `ResolvedChannelMessageIngress`,
|
||||
`senderAccess`, `commandAccess`, `routeAccess`, or `ingress` directly. Start
|
||||
with QQBot, Telegram, Slack, Discord, Signal, Feishu, Matrix, iMessage, and
|
||||
Tlon. Delete wrapper-shape tests; keep behavior tests.
|
||||
|
||||
3. Add outcome classification only with deletions.
|
||||
A generic classifier may expose `dispatch`, `pairing-required`,
|
||||
`skip-activation`, `drop-command`, `drop-route`, `drop-sender`, and
|
||||
`drop-ingress`. It must derive from the decision graph, not reason strings,
|
||||
and migrate at least three plugins in the same patch.
|
||||
|
||||
4. Add route descriptor builders only with deletions.
|
||||
Generic route target and route sender helpers are acceptable only if they
|
||||
immediately shrink route-heavy plugins: Google Chat, IRC, Microsoft Teams,
|
||||
Nextcloud Talk, Mattermost, Slack, Zalo, and Zalo Personal.
|
||||
|
||||
5. Add command/event presets only with deletions.
|
||||
Centralize text-command, native-command, callback, and origin-subject shapes.
|
||||
Command consumers must default to unauthorized when no command gate ran;
|
||||
events must not start pairing.
|
||||
|
||||
6. Add identity presets only where they remove boilerplate.
|
||||
Stable-id, stable-id-plus-aliases, phone/e164, and multi-identifier helpers
|
||||
are allowed when raw values enter only adapter input and redacted state keeps
|
||||
opaque ids/counts.
|
||||
|
||||
7. Share authorized turn assembly.
|
||||
Outside the ingress kernel, remove repeated route/envelope/context/reply
|
||||
scaffolding from QA Channel, IRC, Nextcloud Talk, Zalo, and Zalo Personal.
|
||||
Core may own route/session/envelope/dispatch sequencing; plugins keep
|
||||
delivery and channel-specific context.
|
||||
|
||||
8. Quarantine compatibility.
|
||||
Deprecated SDK helpers stay source-compatible, but bundled hot paths must not
|
||||
import deprecated ingress or command-auth facades. Compatibility tests should
|
||||
use fake third-party plugins, not bundled-plugin internals.
|
||||
|
||||
9. Repack core.
|
||||
After wrapper deletion, collapse one-use modules, remove unused exports, move
|
||||
compatibility projection out of hot paths, and keep focused tests for identity,
|
||||
route, command/event, activation, access groups, and compatibility shims.
|
||||
|
||||
## Deletion Waves
|
||||
|
||||
Run these in order. Each wave must lower bundled production LOC.
|
||||
|
||||
1. Wrapper collapse, expected plugin delta: -400 to -600.
|
||||
Replace plugin-local `resolveXAccess`, `resolveXCommandAccess`, and
|
||||
`accessFromIngress` result types with direct reads from
|
||||
`ResolvedChannelMessageIngress`. First targets: Discord DM command auth,
|
||||
Feishu policy, Matrix access state, Telegram ingress, Signal access policy,
|
||||
QQBot SDK adapter.
|
||||
|
||||
2. Shared outcome helpers, expected plugin delta: -200 to -350.
|
||||
Add one generic classifier only if it deletes repeated
|
||||
`shouldBlockControlCommand`, pairing, activation skip, route block, and sender
|
||||
block ladders across at least three plugins.
|
||||
|
||||
3. Route descriptor builders, expected plugin delta: -200 to -350.
|
||||
Move repeated route target and route sender descriptor assembly into core
|
||||
helpers. First targets: Google Chat, IRC, Microsoft Teams, Nextcloud Talk,
|
||||
Mattermost, Slack, Zalo, Zalo Personal.
|
||||
|
||||
4. Turn assembly sharing, expected plugin delta: -250 to -450.
|
||||
Use common route/session/envelope/dispatch sequencing for simple inbound
|
||||
plugins. First targets: QA Channel, IRC, Nextcloud Talk, Zalo, Zalo Personal.
|
||||
|
||||
5. Core repack, expected core delta: -300 to -700.
|
||||
After plugins consume runtime projections directly, delete one-use modules,
|
||||
merge tiny files back into `runtime.ts` or focused siblings, and keep SDK
|
||||
compatibility files separate from bundled hot paths.
|
||||
|
||||
6. Test pruning, expected test delta: -300 to -600.
|
||||
Delete tests that only assert removed wrapper shapes. Keep behavior tests for
|
||||
command denial, group fallback, origin-subject matching, activation skip,
|
||||
access groups, pairing, and redaction.
|
||||
|
||||
Expected minimum landing shape after these waves:
|
||||
|
||||
```text
|
||||
plugin production <= -1,500
|
||||
core production about +1,800 to +2,200 before final repack
|
||||
tests <= +500
|
||||
total <= +2,000
|
||||
```
|
||||
|
||||
## Do Not Move
|
||||
|
||||
Do not move platform config defaults, setup UX, doctor/fix copy, API lookups,
|
||||
Slack owner-presence checks, Matrix alias/verification handling, Telegram
|
||||
callback parsing, command syntax parsing, native command registration, reaction
|
||||
payload parsing, pairing replies, command replies, acks, typing, media, history,
|
||||
or logs.
|
||||
|
||||
## Verification
|
||||
|
||||
Targeted local loop:
|
||||
|
||||
```sh
|
||||
pnpm lint:extensions:no-deprecated-channel-access
|
||||
pnpm test src/channels/message-access/message-access.test.ts src/plugin-sdk/channel-ingress-runtime.test.ts src/plugin-sdk/access-groups.test.ts
|
||||
pnpm test extensions/<changed-plugin>/src/...
|
||||
pnpm plugin-sdk:api:check
|
||||
pnpm config:docs:check
|
||||
pnpm check:docs
|
||||
git diff --check
|
||||
```
|
||||
|
||||
Use Testbox for broad changed gates/full-suite proof once the LOC trend is
|
||||
inside budget.
|
||||
|
||||
Each work package records:
|
||||
|
||||
- before/after LOC by category
|
||||
- deleted plugin wrappers
|
||||
- new core helper LOC, if any
|
||||
- targeted tests run
|
||||
- remaining hotspot list
|
||||
|
||||
## Exit Criteria
|
||||
|
||||
- bundled production imports no deprecated channel-access or command-auth facades
|
||||
- compatibility code is isolated to SDK/core seams
|
||||
- bundled plugins consume ingress projections or generic outcomes directly
|
||||
- plugin production LOC is at least 1,500 net negative against `origin/main`
|
||||
- core production LOC is <= +1,500, or any excess is paid for while total stays
|
||||
<= +2,000
|
||||
- representative tests cover redaction, route, command/event, activation,
|
||||
access-group, and channel-specific fallback behavior
|
||||
Reference in New Issue
Block a user