Skip to content

feat(telegram/acp): Topic Binding, Pin Binding Message, Fix Spawn Param Parsing#36683

Merged
osolmaz merged 11 commits intoopenclaw:mainfrom
huntharo:acp-telegram-value-reapply-main
Mar 6, 2026
Merged

feat(telegram/acp): Topic Binding, Pin Binding Message, Fix Spawn Param Parsing#36683
osolmaz merged 11 commits intoopenclaw:mainfrom
huntharo:acp-telegram-value-reapply-main

Conversation

@huntharo
Copy link
Member

@huntharo huntharo commented Mar 5, 2026

Summary

Describe the problem and fix in 2–5 bullets:

  • Problem: Telegram topic-bound ACP flows were unreliable: -- args from Telegram Mac could fail parsing, --thread here could report unavailable on Telegram, approvals could fail due callback/approval-id mismatches, and bound topics did not consistently route follow-ups to ACP sessions.
  • Why it matters: This blocked practical ACP use in Telegram topics, forcing manual /acp steer workarounds and breaking multi-topic workspace isolation.
  • What changed: Added Telegram thread-binding support for ACP/session flows, Unicode-dash tolerant /acp spawn parsing, Telegram exec-approval buttons + approval-id prefix resolution, ACPX permission-denied guidance, and topic bind confirmation pinning for ACP spawns.
  • What did NOT change (scope boundary): No new ACP backend/plugin type was added; this remains ACPX-backed behavior and does not change non-ACP runtime architecture.

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor
  • Docs
  • Security hardening
  • Chore/infra

Scope (select all touched areas)

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

Linked Issue/PR

User-visible / Behavior Changes

List user-visible changes (including defaults/config).
If none, write None.

  • /acp spawn now accepts Unicode em/en-dash option prefixes used by Telegram Mac input (auto-converts -- when typed).
  • Telegram topics can be thread-bound ACP targets via /acp spawn ... --thread here|auto when Telegram threadBindings ACP spawn is enabled.
  • In bound Telegram topics, normal follow-up messages route to the bound ACP session without requiring /acp steer.
  • Telegram exec approval prompts include actionable inline approval buttons and callback cleanup.
  • Approval resolution tolerates prefixed approval IDs.
  • ACPX permission-denied failures include explicit guidance for permissionMode profiles.
  • Successful Telegram topic ACP bind messages are pinned in-topic.
  • Docs now explicitly call out Telegram topic support for thread-bound ACP sessions.

Security Impact (required)

  • New permissions/capabilities? (No)
  • Secrets/tokens handling changed? (No)
  • New/changed network calls? (No)
  • Command/tool execution surface changed? (No)
  • Data access scope changed? (No)
  • If any Yes, explain risk + mitigation:

Repro + Verification

Environment

  • OS: macOS (Telegram Desktop on Mac)
  • Runtime/container: local OpenClaw gateway + ACPX backend
  • Model/provider: ACP Codex harness via ACPX
  • Integration/channel (if any): Telegram groups with forum topics (and topic-bound sessions)
  • Relevant config (redacted): Telegram enabled; topic usage enabled; threadBindings ACP spawn enabled; ACPX backend active

Steps

  1. In a Telegram forum topic, run /acp spawn codex —mode persistent —thread here —cwd <repoA> —label <labelA> using Telegram Mac typed args (Unicode dash input).
  2. Send a normal follow-up message in the same topic without /acp steer, such as Who are you?.
  3. Observe typing indicator and response identity in that topic.
  4. In a second Telegram topic, run /acp spawn codex —mode persistent —thread here —cwd <repoB> —label <labelB>.
  5. In each topic, ask for repo-specific summaries and run /acp cwd <path> in-topic, then send another follow-up.

Expected

  • Spawn succeeds in-topic, binds the topic to ACP, posts a bind confirmation, and pins the bind confirmation message with session id.
  • Follow-ups in bound topics route directly to the bound ACP session.
  • Typing indicator appears in the active topic.
  • Different topics stay isolated and respect their bound CWD/session context.

Actual

  • Observed expected behavior in both topics.
  • Spawn confirmation was pinned in-topic.
  • Follow-ups routed to Codex without /acp steer.
  • Second topic remained isolated and responded from its own CWD/session context.

Evidence

Attach at least one:

  • Failing test/log before + passing after
  • Trace/log snippets
  • Screenshot/recording
  • Perf numbers (if relevant)

Baseline - Almost Nothing Works / Binding Not Supported on Telegram

image

PR - Typing Indicator Works in Topics / Pins Binding Message to Topic / Routes to Codex when Bound

image

PR - Topic: ACP Bound - CWD is jeerreview - Describes package.json from JeerReview

image

PR - Topic: ACP - Bound 2 - CWD is openclaw - Describes package.json from OpenClaw

image

Human Verification (required)

What you personally verified (not just CI), and how:

  • Verified scenarios:
    • Baseline failure state (before): Telegram topic ACP had argument parsing/thread-binding/focus/routing failures.
    • Post-fix: /acp spawn ... --thread here succeeded in Telegram topics.
    • Post-fix: topic bind confirmation was pinned.
    • Post-fix: follow-up requests routed to Codex in-topic without /acp steer.
    • Post-fix: typing indicator appeared in Telegram topics.
    • Post-fix: two different topics with different CWDs remained correctly isolated and routed.
  • Edge cases checked:
    • Unicode dash options from Telegram Mac.
    • Multi-topic routing isolation.
    • In-topic runtime option update (/acp cwd) and subsequent routing correctness.
  • What you did not verify:
    • Non-Telegram channel regressions in live environments (I don't have Discord setup - sorry!).
    • DM-topic ACP flows under every dmPolicy combination.

Compatibility / Migration

  • Backward compatible? (Yes/No) Yes
  • Config/env changes? (Yes/No) No (existing flags used; no new required fields)
  • Migration needed? (Yes/No) No
  • If yes, exact upgrade steps:

Failure Recovery (if this breaks)

  • How to disable/revert this change quickly:
    • Set channels.telegram.threadBindings.spawnAcpSessions=false to disable thread-bound ACP spawn in Telegram.
    • Use /acp spawn ... --thread off as immediate operational fallback.
    • Reset to backup branch: backup-acp-telegram-topic-checkpoint-2026-03-05.
  • Files/config to restore:
    • Telegram thread-binding behavior and ACP command handling files under src/telegram/* and src/auto-reply/reply/commands-acp/*.
  • Known bad symptoms reviewers should watch for:
    • /acp spawn ... --thread here returning unavailable for Telegram topics.
    • Follow-up topic messages routing to main/non-ACP sessions.
    • Approval prompts appearing without actionable buttons in Telegram.

Risks and Mitigations

List only real risks for this PR. Add/remove entries as needed. If none, write None.

  • Risk: Telegram thread-binding behavior may vary across forum topics vs DM topics in edge chat states.
    • Mitigation: Adapter-level thread spec handling + tests for topic conversation IDs and fallback behavior.
  • Risk: Approval callback routing regressions could deny valid approvals.
    • Mitigation: Prefix-based approval-id resolution + Telegram approval button tests.
  • Risk: Could have broken Discord since I don't have it setup.
    • Mitigation: If someone could test Discord, that would be fantastic!

@openclaw-barnacle openclaw-barnacle bot added docs Improvements or additions to documentation channel: telegram Channel integration: telegram extensions: acpx size: XL trusted-contributor labels Mar 5, 2026
@huntharo huntharo force-pushed the acp-telegram-value-reapply-main branch from 36c06b4 to b2727d2 Compare March 5, 2026 20:50
@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 5, 2026

Greptile Summary

This PR adds Telegram topic-binding support for ACP/subagent flows, Unicode em/en-dash normalization for /acp spawn parsing, in-topic pin of ACP bind confirmations, ACPX permission-denied guidance, and extends /focus, /unfocus, /session idle|max-age, and /agents to work on Telegram as well as Discord. The changes are well-scoped and include comprehensive tests for the new behaviour.

Key highlights:

  • src/telegram/thread-bindings.ts introduces a full Telegram binding manager (conversation-keyed, persisted to disk, with idle/max-age sweeper) that mirrors the Discord implementation and plugs into the shared session-binding-service adapter interface.
  • src/auto-reply/reply/channel-context.ts (renamed from discord-context.ts) extracts channel-agnostic helpers; backward compat is preserved via re-exports.
  • Unicode dash normalization in shared.ts elegantly handles Telegram Mac auto-substitution.
  • One logic bug found: setTelegramThreadBindingMaxAgeBySessionKey resets boundAt to now on every call, which defeats the purpose of the hard max-age cap — the sweeper uses boundAt + maxAgeMs to decide expiry, so the timer restarts every time /session max-age is invoked.

Confidence Score: 3/5

  • Safe to merge after fixing the boundAt reset in setTelegramThreadBindingMaxAgeBySessionKey; the bug allows repeated /session max-age calls to extend bindings indefinitely past their intended hard cap.
  • One clear logic bug was found (boundAt incorrectly reset in the max-age setter, making the hard max-age cap ineffective). The rest of the changes are well-tested with a broad new test suite and the core functionality (routing, spawn, pin, unicode normalization) is correctly implemented. The bug is isolated to the /session max-age Telegram path and doesn't affect spawning or routing correctness.
  • src/telegram/thread-bindings.ts — specifically setTelegramThreadBindingMaxAgeBySessionKey at line 714.

Last reviewed commit: 7fde11b

Comment on lines 345 to 346
if (currentConversationId && boundConversationId === currentConversationId) {
parts.push(`Bound this thread to ${sessionKey}.`);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The bound-topic confirmation message hardcodes "thread" for all channels. For Telegram contexts, "conversation" or "topic" would match the Telegram terminology used elsewhere in this PR.

Suggested change
if (currentConversationId && boundConversationId === currentConversationId) {
parts.push(`Bound this thread to ${sessionKey}.`);
parts.push(
binding.conversation.channel === "telegram"
? `Bound this conversation to ${sessionKey}.`
: `Bound this thread to ${sessionKey}.`,
);
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/auto-reply/reply/commands-acp/lifecycle.ts
Line: 345-346

Comment:
The bound-topic confirmation message hardcodes "thread" for all channels. For Telegram contexts, "conversation" or "topic" would match the Telegram terminology used elsewhere in this PR.

```suggestion
      parts.push(
        binding.conversation.channel === "telegram"
          ? `Bound this conversation to ${sessionKey}.`
          : `Bound this thread to ${sessionKey}.`,
      );
```

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in c8ba3c2. Telegram bind confirmations now use conversation wording on Telegram surfaces and keep thread wording for Discord.

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 36c06b4006

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +279 to +283
route = {
...route,
sessionKey: boundSessionKey,
agentId: resolveAgentIdFromSessionKey(boundSessionKey),
};

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Mark bound Telegram routes as explicit before account guard

When a Telegram conversation binding is found, buildTelegramMessageContext only updates route.sessionKey/agentId and leaves route.matchedBy unchanged. On named Telegram accounts, the later fail-closed guard (candidate.accountId !== DEFAULT_ACCOUNT_ID && candidate.matchedBy === "default") will still drop the message even though an explicit conversation binding exists, so bound-topic/session routing silently fails for those accounts. Set matchedBy to a binding value (as done in Discord preflight) when applying a Telegram binding override.

Useful? React with 👍 / 👎.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in c8ba3c2. Bound Telegram conversation overrides now mark matchedBy=binding.channel before the non-default account guard, and there is regression coverage in src/telegram/bot-message-context.thread-binding.test.ts.

@huntharo huntharo force-pushed the acp-telegram-value-reapply-main branch from 300cc7d to 11f9c3d Compare March 5, 2026 21:13
@huntharo huntharo force-pushed the acp-telegram-value-reapply-main branch from 11f9c3d to 7fde11b Compare March 5, 2026 21:18
@huntharo
Copy link
Member Author

huntharo commented Mar 5, 2026

@greptile-apps can you recheck the code.

Comment on lines +714 to +715
boundAt: now,
lastActivityAt: now,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

boundAt reset incorrectly extends the max-age window on every /session max-age call

setTelegramThreadBindingMaxAgeBySessionKey resets boundAt: now when updating the max-age setting. The entire point of maxAgeMs is to cap the total lifetime of a binding from when it was first created. The sweeper (shouldExpireByMaxAge) and resolveEffectiveBindingExpiresAt both compute expiry as record.boundAt + maxAgeMs. By writing boundAt: now, calling /session max-age 6h restarts the clock from the time of the command, so a binding can be kept alive indefinitely with repeated calls — defeating the purpose of the hard cap.

Contrast this with setTelegramThreadBindingIdleTimeoutBySessionKey (line 680), which correctly leaves boundAt untouched and only updates lastActivityAt: now.

Suggested change
boundAt: now,
lastActivityAt: now,
const next: TelegramThreadBindingRecord = {
...entry,
maxAgeMs,
lastActivityAt: now,
};
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/telegram/thread-bindings.ts
Line: 714-715

Comment:
**`boundAt` reset incorrectly extends the max-age window on every `/session max-age` call**

`setTelegramThreadBindingMaxAgeBySessionKey` resets `boundAt: now` when updating the max-age setting. The entire point of `maxAgeMs` is to cap the total lifetime of a binding from when it was *first created*. The sweeper (`shouldExpireByMaxAge`) and `resolveEffectiveBindingExpiresAt` both compute expiry as `record.boundAt + maxAgeMs`. By writing `boundAt: now`, calling `/session max-age 6h` restarts the clock from the time of the command, so a binding can be kept alive indefinitely with repeated calls — defeating the purpose of the hard cap.

Contrast this with `setTelegramThreadBindingIdleTimeoutBySessionKey` (line 680), which correctly leaves `boundAt` untouched and only updates `lastActivityAt: now`.

```suggestion
      const next: TelegramThreadBindingRecord = {
        ...entry,
        maxAgeMs,
        lastActivityAt: now,
      };
```

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in c8ba3c2. setTelegramThreadBindingMaxAgeBySessionKey now preserves the original boundAt and only updates maxAgeMs plus lastActivityAt. The regression test in src/telegram/thread-bindings.test.ts asserts that boundAt stays unchanged.

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7fde11b02b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: af8eb60dda

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +686 to +689
void persistBindingsToDisk({
accountId: manager.accountId,
persist: true,
});

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Respect disabled persistence for Telegram lifecycle updates

setTelegramThreadBindingIdleTimeoutBySessionKey always persists with persist: true, so /session idle will write binding state even when the manager was intentionally created with persist: false. In in-memory/test deployments this leaks ephemeral bindings to disk and can reload stale conversation focus after restart, changing routing behavior unexpectedly; this update path should use the manager’s configured persistence mode (same pattern as Discord lifecycle updates).

Useful? React with 👍 / 👎.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 2808e9b.

setTelegramThreadBindingIdleTimeoutBySessionKey() now follows the manager's configured persistence mode instead of forcing persist: true, and src/telegram/thread-bindings.test.ts now covers the persist: false case to ensure lifecycle updates do not write state to disk.

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c8ba3c24e1

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

defaultMaxAgeMs: threadBindings.getMaxAgeMs(),
});
const now = Date.now();
const nextExpiry = durationMs > 0 ? now + durationMs : undefined;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Compute max-age expiry from binding age

The confirmation message for /session max-age always uses Date.now() + duration, but max-age expiry is actually anchored to each binding’s original boundAt timestamp. For bindings that were created earlier, this reports a deadline that is later than the real auto-unfocus time, so operators can be told a session is safe for hours even though it may expire almost immediately.

Useful? React with 👍 / 👎.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 2808e9b.

/session max-age confirmation now derives the displayed expiry from the updated binding state, so Telegram max-age uses boundAt + maxAgeMs instead of Date.now() + duration. Added a regression test in src/auto-reply/reply/commands-session-lifecycle.test.ts for an older binding.

if (updated.length > 0) {
void persistBindingsToDisk({
accountId: manager.accountId,
persist: true,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Respect non-persistent mode when updating max age

setTelegramThreadBindingMaxAgeBySessionKey unconditionally writes with persist: true, so /session max-age will still flush bindings to disk even when the manager was intentionally created with persist: false (for ephemeral/test deployments). That leaks temporary focus state into persisted files and can reapply stale bindings after restart.

Useful? React with 👍 / 👎.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 2808e9b.

setTelegramThreadBindingMaxAgeBySessionKey() now respects the manager's persistence mode instead of always persisting, and the new persist: false regression test exercises both idle and max-age lifecycle updates.

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 09d5cfe881

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

const currentThreadId = bindingContext.threadId ?? "";

if (threadMode === "here" && !currentThreadId) {
const currentConversationId = bindingContext.conversationId?.trim() || "";

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Resolve Telegram DM conversation IDs before spawn binding

Telegram DM /acp spawn ... --thread here can never bind because this flow relies on bindingContext.conversationId, but that value is empty in normal DMs (resolveAcpCommandConversationId falls back to resolveConversationIdFromTargets, which does not parse telegram:<chatId> targets). As a result, users in active Telegram DMs hit the --thread here requires... / Could not resolve... errors even though DM conversation bindings are otherwise supported, so ACP spawn auto-binding is broken for DMs.

Useful? React with 👍 / 👎.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 5badacd.

ACP command context now reuses the shared Telegram conversation resolver, so Telegram DMs produce the canonical DM conversation id instead of falling through to the generic target parser. Added regressions in src/auto-reply/reply/commands-acp/context.test.ts and src/auto-reply/reply/commands-acp.test.ts covering DM context resolution and /acp spawn ... --thread here in a Telegram DM.

@dutifulbob
Copy link
Contributor

Addressed the latest Telegram ACP review in 5badacd63.

What changed:

  • ACP command context now reuses the shared Telegram conversation resolver, so Telegram DMs resolve to the canonical DM conversation id instead of falling through to the generic target parser.
  • /acp spawn ... --thread here now binds correctly in Telegram DMs.

Coverage added:

  • src/auto-reply/reply/commands-acp/context.test.ts
  • src/auto-reply/reply/commands-acp.test.ts

Validated with:

  • pnpm vitest src/auto-reply/reply/commands-acp/context.test.ts src/auto-reply/reply/commands-acp.test.ts
  • pnpm build

@osolmaz osolmaz self-assigned this Mar 6, 2026
Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5badacd639

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

};
configuredBinding = null;
configuredBindingSessionKey = "";
getSessionBindingService().touch(threadBinding.bindingId);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Move binding touch after Telegram access checks

buildTelegramMessageContext updates the Telegram binding activity timestamp before any allowlist/DM policy checks run, so messages that are later rejected (for example, unauthorized users in a restricted group/topic) still call touch and extend idle timeout. In a busy restricted chat this can keep a binding alive indefinitely even though no authorized traffic is occurring, which defeats /session idle auto-unfocus behavior for that conversation.

Useful? React with 👍 / 👎.

@osolmaz osolmaz force-pushed the acp-telegram-value-reapply-main branch from 5badacd to 78e428d Compare March 6, 2026 01:17
@osolmaz osolmaz merged commit d58dafa into openclaw:main Mar 6, 2026
@osolmaz
Copy link
Contributor

osolmaz commented Mar 6, 2026

Landed via temp rebase onto main.

  • Gate: pnpm lint && pnpm build && pnpm test
  • Land commit: 78e428d
  • Merge commit: d58dafa

Thanks @huntharo!

mrosmarin added a commit to mrosmarin/openclaw that referenced this pull request Mar 6, 2026
* main:
  fix(tui): prevent stale model indicator after /model
  fix(gateway): preserve streamed prefixes across tool boundaries
  feat(telegram/acp): Topic Binding, Pin Binding Message, Fix Spawn Param Parsing (openclaw#36683)
  fix(auth): harden openai-codex oauth login path
  fix(llm-task): load runEmbeddedPiAgent from dist/extensionAPI in installs
  fix(gateway): honor insecure ws override for remote hostnames
  fix(routing): avoid full binding rescans in resolveAgentRoute (openclaw#36915)
  Diffs: restore system prompt guidance (openclaw#36904)
  fix(browser): close tracked tabs on session cleanup (openclaw#36666)
  Harden Telegram poll gating and schema consistency (openclaw#36547)
  fix(memory): avoid destructive qmd collection rebinds
  plugins: enforce prompt hook policy with runtime validation (openclaw#36567)
  fix: decouple Discord inbound worker timeout from listener timeout (openclaw#36602) (thanks @dutifulbob) (openclaw#36602)
  README: add algal to contributors list (openclaw#2046)
  fix(slack): preserve dedupe while recovering dropped app_mention (openclaw#34937)
  fix(slack): propagate mediaLocalRoots through Slack send path
  fix(config): prevent RangeError in merged schema cache key generation
  fix(auth): grant senderIsOwner for internal channels with operator.admin scope (openclaw#35704)
  feat: append UTC time alongside local time in shared Current time lines (openclaw#32423)
@huntharo huntharo deleted the acp-telegram-value-reapply-main branch March 6, 2026 01:58
@aisle-research-bot
Copy link

aisle-research-bot bot commented Mar 6, 2026

🔒 Aisle Security Analysis

We found 1 potential security issue(s) in this PR:

# Severity Title
1 🟠 High Telegram topic conversation binding routes any participant’s messages to bound ACP/session without verifying binder

1. 🟠 Telegram topic conversation binding routes any participant’s messages to bound ACP/session without verifying binder

Property Value
Severity High
CWE CWE-862
Location src/telegram/bot-message-context.ts:265-292

Description

buildTelegramMessageContext() now resolves a Telegram conversation binding (topic/DM) and unconditionally overrides route.sessionKey to the bound target session.

  • Input: any Telegram message in the bound forum topic (or DM) provides chatId/message_thread_id and senderId.
  • Authorization gap: routing to the bound session does not verify the sender is authorized for that binding (e.g., senderId === binding.metadata.boundBy) and does not require an explicit control command.
  • Impact: in a group/supergroup forum topic, any member who can post in the topic can inject instructions into the bound session (including ACP sessions spawned with /acp spawn --thread here), potentially steering tool-enabled behavior or exfiltrating session context.

Vulnerable routing logic:

const threadBinding = getSessionBindingService().resolveByConversation({ ... });
...
route = {
  ...route,
  sessionKey: boundSessionKey,
  agentId: resolveAgentIdFromSessionKey(boundSessionKey),
  matchedBy: "binding.channel",
};

While boundBy is recorded and used to restrict rebinding/unfocusing in command handlers, it is not enforced for message routing, enabling cross-user session steering in shared topics.

Recommendation

Enforce an authorization check before routing via a conversation binding.

At minimum, restrict routing to the binding creator (or an explicitly configured allowlist/admin set), especially for group topics:

const boundBy = typeof threadBinding.metadata?.boundBy === "string"
  ? threadBinding.metadata.boundBy.trim()
  : "";

const isGroupTopic = isGroup && replyThreadId != null;

if (isGroupTopic && boundBy && boundBy !== "system" && senderId && senderId !== boundBy) {// Do not route to bound session; fall back to normal routing or ignore.
} else {// existing route override
}

More flexible options:

  • Add a config flag such as channels.telegram.threadBindings.enforceBoundByForRouting=true (default on for Telegram topics).
  • Store binding-specific allowedSenders and check it on each inbound message.
  • For ACP targets, require an explicit authorized control command (e.g., /acp steer) or require mention + command authorization before allowing bound routing.

Analyzed PR: #36683 at commit 78e428d

Last updated on: 2026-03-06T02:55:39Z

thinstripe pushed a commit to thinstripe/openclaw that referenced this pull request Mar 6, 2026
…am Parsing (openclaw#36683)

* fix(acp): normalize unicode flags and Telegram topic binding

* feat(telegram/acp): restore topic-bound ACP and session bindings

* fix(acpx): clarify permission-denied guidance

* feat(telegram/acp): pin spawn bind notice in topics

* docs(telegram): document ACP topic thread binding behavior

* refactor(reply): share Telegram conversation-id resolver

* fix(telegram/acp): preserve bound session routing semantics

* fix(telegram): respect binding persistence and expiry reporting

* refactor(telegram): simplify binding lifecycle persistence

* fix(telegram): bind acp spawns in direct messages

* fix: document telegram ACP topic binding changelog (openclaw#36683) (thanks @huntharo)

---------

Co-authored-by: Onur <[email protected]>
handsdiff pushed a commit to handsdiff/activeclaw that referenced this pull request Mar 6, 2026
…am Parsing (openclaw#36683)

* fix(acp): normalize unicode flags and Telegram topic binding

* feat(telegram/acp): restore topic-bound ACP and session bindings

* fix(acpx): clarify permission-denied guidance

* feat(telegram/acp): pin spawn bind notice in topics

* docs(telegram): document ACP topic thread binding behavior

* refactor(reply): share Telegram conversation-id resolver

* fix(telegram/acp): preserve bound session routing semantics

* fix(telegram): respect binding persistence and expiry reporting

* refactor(telegram): simplify binding lifecycle persistence

* fix(telegram): bind acp spawns in direct messages

* fix: document telegram ACP topic binding changelog (openclaw#36683) (thanks @huntharo)

---------

Co-authored-by: Onur <[email protected]>
jenawant pushed a commit to jenawant/openclaw that referenced this pull request Mar 10, 2026
…am Parsing (openclaw#36683)

* fix(acp): normalize unicode flags and Telegram topic binding

* feat(telegram/acp): restore topic-bound ACP and session bindings

* fix(acpx): clarify permission-denied guidance

* feat(telegram/acp): pin spawn bind notice in topics

* docs(telegram): document ACP topic thread binding behavior

* refactor(reply): share Telegram conversation-id resolver

* fix(telegram/acp): preserve bound session routing semantics

* fix(telegram): respect binding persistence and expiry reporting

* refactor(telegram): simplify binding lifecycle persistence

* fix(telegram): bind acp spawns in direct messages

* fix: document telegram ACP topic binding changelog (openclaw#36683) (thanks @huntharo)

---------

Co-authored-by: Onur <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

channel: telegram Channel integration: telegram docs Improvements or additions to documentation extensions: acpx size: XL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants