mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
ACP: add persistent Discord channel and Telegram topic bindings (#34873)
* docs: add ACP persistent binding experiment plan * docs: align ACP persistent binding spec to channel-local config * docs: scope Telegram ACP bindings to forum topics only * docs: lock bound /new and /reset behavior to in-place ACP reset * ACP: add persistent discord/telegram conversation bindings * ACP: fix persistent binding reuse and discord thread parent context * docs: document channel-specific persistent ACP bindings * ACP: split persistent bindings and share conversation id helpers * ACP: defer configured binding init until preflight passes * ACP: fix discord thread parent fallback and explicit disable inheritance * ACP: keep bound /new and /reset in-place * ACP: honor configured bindings in native command flows * ACP: avoid configured fallback after runtime bind failure * docs: refine ACP bindings experiment config examples * acp: cut over to typed top-level persistent bindings * ACP bindings: harden reset recovery and native command auth * Docs: add ACP bound command auth proposal * Tests: normalize i18n registry zh-CN assertion encoding * ACP bindings: address review findings for reset and fallback routing * ACP reset: gate hooks on success and preserve /new arguments * ACP bindings: fix auth and binding-priority review findings * Telegram ACP: gate ensure on auth and accepted messages * ACP bindings: fix session-key precedence and unavailable handling * ACP reset/native commands: honor fallback targets and abort on bootstrap failure * Config schema: validate ACP binding channel and Telegram topic IDs * Discord ACP: apply configured DM bindings to native commands * ACP reset tails: dispatch through ACP after command handling * ACP tails/native reset auth: fix target dispatch and restore full auth * ACP reset detection: fallback to active ACP keys for DM contexts * Tests: type runTurn mock input in ACP dispatch test * ACP: dedup binding route bootstrap and reset target resolution * reply: align ACP reset hooks with bound session key * docs: replace personal discord ids with placeholders * fix: add changelog entry for ACP persistent bindings (#34873) (thanks @dutifulbob) --------- Co-authored-by: Onur <2453968+osolmaz@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,375 @@
|
||||
# ACP Persistent Bindings for Discord Channels and Telegram Topics
|
||||
|
||||
Status: Draft
|
||||
|
||||
## Summary
|
||||
|
||||
Introduce persistent ACP bindings that map:
|
||||
|
||||
- Discord channels (and existing threads, where needed), and
|
||||
- Telegram forum topics in groups/supergroups (`chatId:topic:topicId`)
|
||||
|
||||
to long-lived ACP sessions, with binding state stored in top-level `bindings[]` entries using explicit binding types.
|
||||
|
||||
This makes ACP usage in high-traffic messaging channels predictable and durable, so users can create dedicated channels/topics such as `codex`, `claude-1`, or `claude-myrepo`.
|
||||
|
||||
## Why
|
||||
|
||||
Current thread-bound ACP behavior is optimized for ephemeral Discord thread workflows. Telegram does not have the same thread model; it has forum topics in groups/supergroups. Users want stable, always-on ACP “workspaces” in chat surfaces, not only temporary thread sessions.
|
||||
|
||||
## Goals
|
||||
|
||||
- Support durable ACP binding for:
|
||||
- Discord channels/threads
|
||||
- Telegram forum topics (groups/supergroups)
|
||||
- Make binding source-of-truth config-driven.
|
||||
- Keep `/acp`, `/new`, `/reset`, `/focus`, and delivery behavior consistent across Discord and Telegram.
|
||||
- Preserve existing temporary binding flows for ad-hoc usage.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- Full redesign of ACP runtime/session internals.
|
||||
- Removing existing ephemeral binding flows.
|
||||
- Expanding to every channel in the first iteration.
|
||||
- Implementing Telegram channel direct-messages topics (`direct_messages_topic_id`) in this phase.
|
||||
- Implementing Telegram private-chat topic variants in this phase.
|
||||
|
||||
## UX Direction
|
||||
|
||||
### 1) Two binding types
|
||||
|
||||
- **Persistent binding**: saved in config, reconciled on startup, intended for “named workspace” channels/topics.
|
||||
- **Temporary binding**: runtime-only, expires by idle/max-age policy.
|
||||
|
||||
### 2) Command behavior
|
||||
|
||||
- `/acp spawn ... --thread here|auto|off` remains available.
|
||||
- Add explicit bind lifecycle controls:
|
||||
- `/acp bind [session|agent] [--persist]`
|
||||
- `/acp unbind [--persist]`
|
||||
- `/acp status` includes whether binding is `persistent` or `temporary`.
|
||||
- In bound conversations, `/new` and `/reset` reset the bound ACP session in place and keep the binding attached.
|
||||
|
||||
### 3) Conversation identity
|
||||
|
||||
- Use canonical conversation IDs:
|
||||
- Discord: channel/thread ID.
|
||||
- Telegram topic: `chatId:topic:topicId`.
|
||||
- Never key Telegram bindings by bare topic ID alone.
|
||||
|
||||
## Config Model (Proposed)
|
||||
|
||||
Unify routing and persistent ACP binding configuration in top-level `bindings[]` with explicit `type` discriminator:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"agents": {
|
||||
"list": [
|
||||
{
|
||||
"id": "main",
|
||||
"default": true,
|
||||
"workspace": "~/.openclaw/workspace-main",
|
||||
"runtime": { "type": "embedded" },
|
||||
},
|
||||
{
|
||||
"id": "codex",
|
||||
"workspace": "~/.openclaw/workspace-codex",
|
||||
"runtime": {
|
||||
"type": "acp",
|
||||
"acp": {
|
||||
"agent": "codex",
|
||||
"backend": "acpx",
|
||||
"mode": "persistent",
|
||||
"cwd": "/workspace/repo-a",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"id": "claude",
|
||||
"workspace": "~/.openclaw/workspace-claude",
|
||||
"runtime": {
|
||||
"type": "acp",
|
||||
"acp": {
|
||||
"agent": "claude",
|
||||
"backend": "acpx",
|
||||
"mode": "persistent",
|
||||
"cwd": "/workspace/repo-b",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
"acp": {
|
||||
"enabled": true,
|
||||
"backend": "acpx",
|
||||
"allowedAgents": ["codex", "claude"],
|
||||
},
|
||||
"bindings": [
|
||||
// Route bindings (existing behavior)
|
||||
{
|
||||
"type": "route",
|
||||
"agentId": "main",
|
||||
"match": { "channel": "discord", "accountId": "default" },
|
||||
},
|
||||
{
|
||||
"type": "route",
|
||||
"agentId": "main",
|
||||
"match": { "channel": "telegram", "accountId": "default" },
|
||||
},
|
||||
// Persistent ACP conversation bindings
|
||||
{
|
||||
"type": "acp",
|
||||
"agentId": "codex",
|
||||
"match": {
|
||||
"channel": "discord",
|
||||
"accountId": "default",
|
||||
"peer": { "kind": "channel", "id": "222222222222222222" },
|
||||
},
|
||||
"acp": {
|
||||
"label": "codex-main",
|
||||
"mode": "persistent",
|
||||
"cwd": "/workspace/repo-a",
|
||||
"backend": "acpx",
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "acp",
|
||||
"agentId": "claude",
|
||||
"match": {
|
||||
"channel": "discord",
|
||||
"accountId": "default",
|
||||
"peer": { "kind": "channel", "id": "333333333333333333" },
|
||||
},
|
||||
"acp": {
|
||||
"label": "claude-repo-b",
|
||||
"mode": "persistent",
|
||||
"cwd": "/workspace/repo-b",
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "acp",
|
||||
"agentId": "codex",
|
||||
"match": {
|
||||
"channel": "telegram",
|
||||
"accountId": "default",
|
||||
"peer": { "kind": "group", "id": "-1001234567890:topic:42" },
|
||||
},
|
||||
"acp": {
|
||||
"label": "tg-codex-42",
|
||||
"mode": "persistent",
|
||||
},
|
||||
},
|
||||
],
|
||||
"channels": {
|
||||
"discord": {
|
||||
"guilds": {
|
||||
"111111111111111111": {
|
||||
"channels": {
|
||||
"222222222222222222": {
|
||||
"enabled": true,
|
||||
"requireMention": false,
|
||||
},
|
||||
"333333333333333333": {
|
||||
"enabled": true,
|
||||
"requireMention": false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"telegram": {
|
||||
"groups": {
|
||||
"-1001234567890": {
|
||||
"topics": {
|
||||
"42": {
|
||||
"requireMention": false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### Minimal Example (No Per-Binding ACP Overrides)
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"agents": {
|
||||
"list": [
|
||||
{ "id": "main", "default": true, "runtime": { "type": "embedded" } },
|
||||
{
|
||||
"id": "codex",
|
||||
"runtime": {
|
||||
"type": "acp",
|
||||
"acp": { "agent": "codex", "backend": "acpx", "mode": "persistent" },
|
||||
},
|
||||
},
|
||||
{
|
||||
"id": "claude",
|
||||
"runtime": {
|
||||
"type": "acp",
|
||||
"acp": { "agent": "claude", "backend": "acpx", "mode": "persistent" },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
"acp": { "enabled": true, "backend": "acpx" },
|
||||
"bindings": [
|
||||
{
|
||||
"type": "route",
|
||||
"agentId": "main",
|
||||
"match": { "channel": "discord", "accountId": "default" },
|
||||
},
|
||||
{
|
||||
"type": "route",
|
||||
"agentId": "main",
|
||||
"match": { "channel": "telegram", "accountId": "default" },
|
||||
},
|
||||
|
||||
{
|
||||
"type": "acp",
|
||||
"agentId": "codex",
|
||||
"match": {
|
||||
"channel": "discord",
|
||||
"accountId": "default",
|
||||
"peer": { "kind": "channel", "id": "222222222222222222" },
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "acp",
|
||||
"agentId": "claude",
|
||||
"match": {
|
||||
"channel": "discord",
|
||||
"accountId": "default",
|
||||
"peer": { "kind": "channel", "id": "333333333333333333" },
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "acp",
|
||||
"agentId": "codex",
|
||||
"match": {
|
||||
"channel": "telegram",
|
||||
"accountId": "default",
|
||||
"peer": { "kind": "group", "id": "-1009876543210:topic:5" },
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- `bindings[].type` is explicit:
|
||||
- `route`: normal agent routing.
|
||||
- `acp`: persistent ACP harness binding for a matched conversation.
|
||||
- For `type: "acp"`, `match.peer.id` is the canonical conversation key:
|
||||
- Discord channel/thread: raw channel/thread ID.
|
||||
- Telegram topic: `chatId:topic:topicId`.
|
||||
- `bindings[].acp.backend` is optional. Backend fallback order:
|
||||
1. `bindings[].acp.backend`
|
||||
2. `agents.list[].runtime.acp.backend`
|
||||
3. global `acp.backend`
|
||||
- `mode`, `cwd`, and `label` follow the same override pattern (`binding override -> agent runtime default -> global/default behavior`).
|
||||
- Keep existing `session.threadBindings.*` and `channels.discord.threadBindings.*` for temporary binding policies.
|
||||
- Persistent entries declare desired state; runtime reconciles to actual ACP sessions/bindings.
|
||||
- One active ACP binding per conversation node is the intended model.
|
||||
- Backward compatibility: missing `type` is interpreted as `route` for legacy entries.
|
||||
|
||||
### Backend Selection
|
||||
|
||||
- ACP session initialization already uses configured backend selection during spawn (`acp.backend` today).
|
||||
- This proposal extends spawn/reconcile logic to prefer typed ACP binding overrides:
|
||||
- `bindings[].acp.backend` for conversation-local override.
|
||||
- `agents.list[].runtime.acp.backend` for per-agent defaults.
|
||||
- If no override exists, keep current behavior (`acp.backend` default).
|
||||
|
||||
## Architecture Fit in Current System
|
||||
|
||||
### Reuse existing components
|
||||
|
||||
- `SessionBindingService` already supports channel-agnostic conversation references.
|
||||
- ACP spawn/bind flows already support binding through service APIs.
|
||||
- Telegram already carries topic/thread context via `MessageThreadId` and `chatId`.
|
||||
|
||||
### New/extended components
|
||||
|
||||
- **Telegram binding adapter** (parallel to Discord adapter):
|
||||
- register adapter per Telegram account,
|
||||
- resolve/list/bind/unbind/touch by canonical conversation ID.
|
||||
- **Typed binding resolver/index**:
|
||||
- split `bindings[]` into `route` and `acp` views,
|
||||
- keep `resolveAgentRoute` on `route` bindings only,
|
||||
- resolve persistent ACP intent from `acp` bindings only.
|
||||
- **Inbound binding resolution for Telegram**:
|
||||
- resolve bound session before route finalization (Discord already does this).
|
||||
- **Persistent binding reconciler**:
|
||||
- on startup: load configured top-level `type: "acp"` bindings, ensure ACP sessions exist, ensure bindings exist.
|
||||
- on config change: apply deltas safely.
|
||||
- **Cutover model**:
|
||||
- no channel-local ACP binding fallback is read,
|
||||
- persistent ACP bindings are sourced only from top-level `bindings[].type="acp"` entries.
|
||||
|
||||
## Phased Delivery
|
||||
|
||||
### Phase 1: Typed binding schema foundation
|
||||
|
||||
- Extend config schema to support `bindings[].type` discriminator:
|
||||
- `route`,
|
||||
- `acp` with optional `acp` override object (`mode`, `backend`, `cwd`, `label`).
|
||||
- Extend agent schema with runtime descriptor to mark ACP-native agents (`agents.list[].runtime.type`).
|
||||
- Add parser/indexer split for route vs ACP bindings.
|
||||
|
||||
### Phase 2: Runtime resolution + Discord/Telegram parity
|
||||
|
||||
- Resolve persistent ACP bindings from top-level `type: "acp"` entries for:
|
||||
- Discord channels/threads,
|
||||
- Telegram forum topics (`chatId:topic:topicId` canonical IDs).
|
||||
- Implement Telegram binding adapter and inbound bound-session override parity with Discord.
|
||||
- Do not include Telegram direct/private topic variants in this phase.
|
||||
|
||||
### Phase 3: Command parity and resets
|
||||
|
||||
- Align `/acp`, `/new`, `/reset`, and `/focus` behavior in bound Telegram/Discord conversations.
|
||||
- Ensure binding survives reset flows as configured.
|
||||
|
||||
### Phase 4: Hardening
|
||||
|
||||
- Better diagnostics (`/acp status`, startup reconciliation logs).
|
||||
- Conflict handling and health checks.
|
||||
|
||||
## Guardrails and Policy
|
||||
|
||||
- Respect ACP enablement and sandbox restrictions exactly as today.
|
||||
- Keep explicit account scoping (`accountId`) to avoid cross-account bleed.
|
||||
- Fail closed on ambiguous routing.
|
||||
- Keep mention/access policy behavior explicit per channel config.
|
||||
|
||||
## Testing Plan
|
||||
|
||||
- Unit:
|
||||
- conversation ID normalization (especially Telegram topic IDs),
|
||||
- reconciler create/update/delete paths,
|
||||
- `/acp bind --persist` and unbind flows.
|
||||
- Integration:
|
||||
- inbound Telegram topic -> bound ACP session resolution,
|
||||
- inbound Discord channel/thread -> persistent binding precedence.
|
||||
- Regression:
|
||||
- temporary bindings continue to work,
|
||||
- unbound channels/topics keep current routing behavior.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- Should `/acp spawn --thread auto` in Telegram topic default to `here`?
|
||||
- Should persistent bindings always bypass mention-gating in bound conversations, or require explicit `requireMention=false`?
|
||||
- Should `/focus` gain `--persist` as an alias for `/acp bind --persist`?
|
||||
|
||||
## Rollout
|
||||
|
||||
- Ship as opt-in per conversation (`bindings[].type="acp"` entry present).
|
||||
- Start with Discord + Telegram only.
|
||||
- Add docs with examples for:
|
||||
- “one channel/topic per agent”
|
||||
- “multiple channels/topics per same agent with different `cwd`”
|
||||
- “team naming patterns (`codex-1`, `claude-repo-x`)".
|
||||
89
docs/experiments/proposals/acp-bound-command-auth.md
Normal file
89
docs/experiments/proposals/acp-bound-command-auth.md
Normal file
@@ -0,0 +1,89 @@
|
||||
---
|
||||
summary: "Proposal: long-term command authorization model for ACP-bound conversations"
|
||||
read_when:
|
||||
- Designing native command auth behavior in Telegram/Discord ACP-bound channels/topics
|
||||
title: "ACP Bound Command Authorization (Proposal)"
|
||||
---
|
||||
|
||||
# ACP Bound Command Authorization (Proposal)
|
||||
|
||||
Status: Proposed, **not implemented yet**.
|
||||
|
||||
This document describes a long-term authorization model for native commands in
|
||||
ACP-bound conversations. It is an experiments proposal and does not replace
|
||||
current production behavior.
|
||||
|
||||
For implemented behavior, read source and tests in:
|
||||
|
||||
- `src/telegram/bot-native-commands.ts`
|
||||
- `src/discord/monitor/native-command.ts`
|
||||
- `src/auto-reply/reply/commands-core.ts`
|
||||
|
||||
## Problem
|
||||
|
||||
Today we have command-specific checks (for example `/new` and `/reset`) that
|
||||
need to work inside ACP-bound channels/topics even when allowlists are empty.
|
||||
This solves immediate UX pain, but command-name-based exceptions do not scale.
|
||||
|
||||
## Long-term shape
|
||||
|
||||
Move command authorization from ad-hoc handler logic to command metadata plus a
|
||||
shared policy evaluator.
|
||||
|
||||
### 1) Add auth policy metadata to command definitions
|
||||
|
||||
Each command definition should declare an auth policy. Example shape:
|
||||
|
||||
```ts
|
||||
type CommandAuthPolicy =
|
||||
| { mode: "owner_or_allowlist" } // default, current strict behavior
|
||||
| { mode: "bound_acp_or_owner_or_allowlist" } // allow in explicitly bound ACP conversations
|
||||
| { mode: "owner_only" };
|
||||
```
|
||||
|
||||
`/new` and `/reset` would use `bound_acp_or_owner_or_allowlist`.
|
||||
Most other commands would remain `owner_or_allowlist`.
|
||||
|
||||
### 2) Share one evaluator across channels
|
||||
|
||||
Introduce one helper that evaluates command auth using:
|
||||
|
||||
- command policy metadata
|
||||
- sender authorization state
|
||||
- resolved conversation binding state
|
||||
|
||||
Both Telegram and Discord native handlers should call the same helper to avoid
|
||||
behavior drift.
|
||||
|
||||
### 3) Use binding-match as the bypass boundary
|
||||
|
||||
When policy allows bound ACP bypass, authorize only if a configured binding
|
||||
match was resolved for the current conversation (not just because current
|
||||
session key looks ACP-like).
|
||||
|
||||
This keeps the boundary explicit and minimizes accidental widening.
|
||||
|
||||
## Why this is better
|
||||
|
||||
- Scales to future commands without adding more command-name conditionals.
|
||||
- Keeps behavior consistent across channels.
|
||||
- Preserves current security model by requiring explicit binding match.
|
||||
- Keeps allowlists optional hardening instead of a universal requirement.
|
||||
|
||||
## Rollout plan (future)
|
||||
|
||||
1. Add command auth policy field to command registry types and command data.
|
||||
2. Implement shared evaluator and migrate Telegram + Discord native handlers.
|
||||
3. Move `/new` and `/reset` to metadata-driven policy.
|
||||
4. Add tests per policy mode and channel surface.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- This proposal does not change ACP session lifecycle behavior.
|
||||
- This proposal does not require allowlists for all ACP-bound commands.
|
||||
- This proposal does not change existing route binding semantics.
|
||||
|
||||
## Note
|
||||
|
||||
This proposal is intentionally additive and does not delete or replace existing
|
||||
experiments documents.
|
||||
Reference in New Issue
Block a user