Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion docs/BOT_ACCESS_SETUP.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,24 @@ oco pairing list --instance <instance-id> --channel telegram --account support -
oco pairing approve --instance <instance-id> --channel telegram --account support --code <PAIRING_CODE>
```

Optional read-only group mode (enabled accounts can read group traffic but never reply in groups):
Read-only group mode for selected Telegram accounts that still need to ingest group traffic:

```json5
{
channels: {
telegram: {
accounts: {
support: {
groupPolicy: "open",
groups: {
"*": {
requireMention: false,
},
},
},
},
},
},
plugins: {
entries: {
"telegram-group-allowlist-guard": {
Expand All @@ -66,6 +80,16 @@ Optional read-only group mode (enabled accounts can read group traffic but never
}
```

This keeps Telegram open for inbound group visibility, so the bot can still ingest messages and maintain group context.

For a strict no-send guarantee, do not rely on the plugin alone:
- The reply dispatcher must suppress all outbound payloads for the protected Telegram group sessions before they reach the channel adapter.
- The `telegram-group-allowlist-guard` plugin should remain enabled as a second layer so normal group replies and tool paths still fail closed if runtime metadata is incomplete.

Operational hardening outside OCO:
- Disable new group joins for these bots with BotFather `/setjoingroups`.
- Remove the bots from existing Telegram groups or restrict them to read-only if they must stay present.

## 2. Discord Setup

1. Create app + bot in Discord Developer Portal.
Expand Down
6 changes: 5 additions & 1 deletion instances/core-human/config/Dockerfile.custom
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,9 @@ RUN git clone https://github.com/googleworkspace/cli.git /tmp/gws \
FROM ghcr.io/openclaw/openclaw:2026.2.22

COPY --from=gws-builder /usr/local/cargo/bin/gws /usr/local/bin/gws
COPY instances/core-human/config/openclaw-runtime-patches/apply-protected-group-reply-suppression.mjs /tmp/apply-protected-group-reply-suppression.mjs

RUN gws --version
RUN mkdir -p /tmp/corepack /tmp/.cache /tmp/pnpm \
&& node /tmp/apply-protected-group-reply-suppression.mjs \
&& HOME=/tmp COREPACK_HOME=/tmp/corepack XDG_CACHE_HOME=/tmp/.cache PNPM_HOME=/tmp/pnpm pnpm build \
&& gws --version
Original file line number Diff line number Diff line change
Expand Up @@ -581,6 +581,31 @@ function resolveDefaultEnabledAccount(enabledAccounts: Set<string>): string | un
return enabledAccounts.values().next().value as string;
}

function hasExplicitIdentityCandidate(...candidates: unknown[]): boolean {
return candidates.some((candidate) => Boolean(asNonEmptyString(candidate)));
}

function resolveEnabledAccount(
enabledAccounts: Set<string>,
...candidates: unknown[]
): string | undefined {
let sawExplicitCandidate = false;
for (const candidate of candidates) {
const normalized = asNonEmptyString(candidate)?.toLowerCase();
if (!normalized) {
continue;
}
sawExplicitCandidate = true;
if (isAccountEnabled(normalized, enabledAccounts)) {
return normalized;
}
}
if (sawExplicitCandidate) {
return;
}
return resolveDefaultEnabledAccount(enabledAccounts);
}

export default function register(api: OpenClawPluginApi): void {
const pluginConfig = (api.pluginConfig ?? {}) as GuardPluginConfig;
const enabledAccounts = new Set(
Expand Down Expand Up @@ -726,8 +751,14 @@ export default function register(api: OpenClawPluginApi): void {

cleanupExpiredContext(maxSenderAgeMs);
const cached = findLatestInboundForTarget(parsedSession.toTarget, maxSenderAgeMs, parsedSession.accountId);
const accountId =
parsedSession.accountId ?? cached?.accountId ?? resolveDefaultEnabledAccount(enabledAccounts);
const accountId = resolveEnabledAccount(
enabledAccounts,
parsedSession.accountId,
parsedSession.agentId,
ctx.accountId,
ctx.agentId,
cached?.accountId,
);
if (!accountId || !isAccountEnabled(accountId, enabledAccounts)) {
return;
}
Expand Down Expand Up @@ -770,16 +801,16 @@ export default function register(api: OpenClawPluginApi): void {
}

const rawContent = asNonEmptyString(event.content) ?? "";
const accountLabel = asNonEmptyString(ctx.accountId) ?? asNonEmptyString(ctx.agentId) ?? "unknown";
if (isReasoningOnlyMessage(rawContent)) {
const accountLabel = asNonEmptyString(ctx.accountId) ?? "unknown";
api.logger.info?.(`telegram-group-allowlist-guard: cancelled reasoning-only message for ${accountLabel}`);
return { cancel: true };
}

const sanitizedContent = stripReasoningArtifacts(rawContent);
if (hasThinkArtifacts(rawContent) && sanitizedContent.length === 0) {
api.logger.info?.(
`telegram-group-allowlist-guard: cancelled think-artifact-only message for ${accountId}`,
`telegram-group-allowlist-guard: cancelled think-artifact-only message for ${accountLabel}`,
);
return { cancel: true };
}
Expand All @@ -796,16 +827,48 @@ export default function register(api: OpenClawPluginApi): void {

const parsedTo = parseTelegramTarget(toTargetRaw);
const metadata = asRecord(event.metadata);
let accountId =
asNonEmptyString(ctx.accountId)?.toLowerCase() ??
asNonEmptyString(metadata?.accountId)?.toLowerCase();
if (!accountId && parsedTo.kind === "group") {
const sessionKey =
asNonEmptyString(ctx.sessionKey) ??
asNonEmptyString(event.sessionKey) ??
asNonEmptyString(event.session_key) ??
asNonEmptyString(metadata?.sessionKey) ??
asNonEmptyString(metadata?.session_key);
const parsedSession = sessionKey ? parseTelegramSessionKey(sessionKey) : undefined;
const hasExplicitIdentity = hasExplicitIdentityCandidate(
ctx.accountId,
ctx.agentId,
parsedSession?.accountId,
parsedSession?.agentId,
metadata?.accountId,
metadata?.account_id,
metadata?.agentId,
metadata?.agent_id,
);
let accountId = resolveEnabledAccount(
enabledAccounts,
ctx.accountId,
ctx.agentId,
parsedSession?.accountId,
parsedSession?.agentId,
metadata?.accountId,
metadata?.account_id,
metadata?.agentId,
metadata?.agent_id,
);
if (!accountId && parsedTo.kind === "group" && !hasExplicitIdentity) {
cleanupExpiredContext(maxSenderAgeMs);
accountId =
findLatestInboundForTarget(toTarget, maxSenderAgeMs)?.accountId ??
resolveDefaultEnabledAccount(enabledAccounts);
accountId = resolveEnabledAccount(
enabledAccounts,
findLatestInboundForTarget(toTarget, maxSenderAgeMs)?.accountId,
);
}
if (!accountId || !isAccountEnabled(accountId, enabledAccounts)) {
if (parsedTo.kind === "group" && blockAllGroupReplies && enabledAccounts.size > 0) {
api.logger.info?.(
`telegram-group-allowlist-guard: cancelled group send -> ${toTarget} (missing protected account context; failing closed)`,
);
return { cancel: true };
}
Comment on lines 865 to +871
return;
}

Expand Down
Loading
Loading