# External Connector Backend Runtime Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Add Beaver backend support for sidecar-backed Weixin and Feishu connector sessions, bridge-event dedupe, outbound sidecar delivery, and dynamic runtime channel activation. **Architecture:** Beaver depends only on a generic connector HTTP contract. Sidecar-backed connections materialize as `ChannelConfig(kind="external_connector", mode="http")`, are registered dynamically through `ChannelRuntime.add_channel()`, and accept inbound events through an authenticated bridge endpoint with `MessageDedupeStore` idempotency. **Tech Stack:** Python dataclasses, FastAPI, Pydantic v2, local JSON stores, pytest, existing Beaver channel runtime. --- ## Scope Included: - JSON-backed bridge event dedupe with 60-second stale processing TTL. - `ExternalConnectorChannel` adapter for outbound `/send` calls with stable `requestId`. - Runtime and manager dynamic add/remove channel support. - Beaver sidecar HTTP client. - Weixin and Feishu connector registry entries backed by fake sidecar clients in tests. - Backend APIs for connector sessions and bridge inbound events. Excluded: - Sidecar process implementation. - Frontend connector wizard. - Docker compose integration. - Live vendor CLI verification. ## File Structure - Create `app-instance/backend/beaver/interfaces/channels/connections/dedupe.py` - `ConnectorMessageDedupeRecord` and `MessageDedupeStore`. - Create `app-instance/backend/beaver/interfaces/channels/connections/sidecar_client.py` - Async HTTP client for generic sidecar contract. - Create `app-instance/backend/beaver/interfaces/channels/connections/external.py` - `ExternalConnectorBase`, `WeixinConnector`, and `FeishuConnector`. - Create `app-instance/backend/beaver/interfaces/channels/external_connector.py` - Runtime adapter that sends outbound messages to the sidecar. - Modify `app-instance/backend/beaver/interfaces/channels/manager.py` - Allow dynamic register/unregister while manager is started. - Modify `app-instance/backend/beaver/interfaces/channels/runtime.py` - Add lifecycle lock, `add_channel()`, `remove_channel()`, and adapter factory support for `external_connector/http`. - Modify `app-instance/backend/beaver/interfaces/channels/connections/connectors.py` - Materialize sidecar-backed connections and optionally activate runtime. - Modify `app-instance/backend/beaver/interfaces/channels/connections/__init__.py` - Export new dedupe, client, and connector symbols. - Modify `app-instance/backend/beaver/interfaces/web/schemas/chat.py` - Add connector session and bridge schemas. - Modify `app-instance/backend/beaver/interfaces/web/schemas/__init__.py` - Export new schemas. - Modify `app-instance/backend/beaver/interfaces/web/app.py` - Register Weixin/Feishu connectors, sidecar settings, connector session routes, and bridge endpoint. - Test `app-instance/backend/tests/unit/test_connector_message_dedupe_store.py` - Test `app-instance/backend/tests/unit/test_external_connector_channel.py` - Test `app-instance/backend/tests/unit/test_channel_runtime_dynamic_channels.py` - Test `app-instance/backend/tests/unit/test_external_connector_bridge_api.py` - Test `app-instance/backend/tests/unit/test_external_sidecar_connectors.py` --- ### Task 1: Message Dedupe Store **Files:** - Create: `app-instance/backend/beaver/interfaces/channels/connections/dedupe.py` - Modify: `app-instance/backend/beaver/interfaces/channels/connections/__init__.py` - Test: `app-instance/backend/tests/unit/test_connector_message_dedupe_store.py` - [ ] **Step 1: Write failing dedupe tests** Create `app-instance/backend/tests/unit/test_connector_message_dedupe_store.py`: ```python from __future__ import annotations from beaver.interfaces.channels.connections import MessageDedupeStore def test_message_dedupe_store_completes_and_dedupes_completed(tmp_path) -> None: store = MessageDedupeStore(tmp_path / "message_dedupe.json") first = store.begin(connection_id="conn_1", event_id="evt_1", delivery_attempt=1) store.complete(first.dedupe_key, message_id="msg_1") duplicate = store.begin(connection_id="conn_1", event_id="evt_1", delivery_attempt=2) assert first.should_process is True assert duplicate.should_process is False assert duplicate.status == "completed" assert duplicate.http_status == 200 def test_message_dedupe_store_returns_conflict_for_active_processing(tmp_path) -> None: store = MessageDedupeStore(tmp_path / "message_dedupe.json", processing_ttl_seconds=60) store.begin(connection_id="conn_1", event_id="evt_1", delivery_attempt=1) duplicate = store.begin(connection_id="conn_1", event_id="evt_1", delivery_attempt=2) assert duplicate.should_process is False assert duplicate.status == "processing" assert duplicate.http_status == 409 assert duplicate.retry_after_seconds == 5 def test_message_dedupe_store_reprocesses_stale_processing(tmp_path) -> None: store = MessageDedupeStore(tmp_path / "message_dedupe.json", processing_ttl_seconds=0) store.begin(connection_id="conn_1", event_id="evt_1", delivery_attempt=1) stale = store.begin(connection_id="conn_1", event_id="evt_1", delivery_attempt=2) assert stale.should_process is True assert stale.status == "processing" assert stale.record.delivery_attempts == 2 def test_message_dedupe_store_reprocesses_failed_records(tmp_path) -> None: store = MessageDedupeStore(tmp_path / "message_dedupe.json") first = store.begin(connection_id="conn_1", event_id="evt_1", delivery_attempt=1) store.fail(first.dedupe_key, error="runtime rejected") retry = store.begin(connection_id="conn_1", event_id="evt_1", delivery_attempt=2) assert retry.should_process is True assert retry.record.delivery_attempts == 2 assert retry.record.last_error is None ``` - [ ] **Step 2: Run tests to verify failure** Run: ```bash cd app-instance/backend uv run pytest tests/unit/test_connector_message_dedupe_store.py -q ``` Expected: fail with `ImportError: cannot import name 'MessageDedupeStore'`. - [ ] **Step 3: Implement dedupe models and store** Create `app-instance/backend/beaver/interfaces/channels/connections/dedupe.py` with: ```python from __future__ import annotations import json from dataclasses import asdict, dataclass from datetime import datetime, timezone from pathlib import Path from threading import Lock from typing import Any def _iso_now() -> str: return datetime.now(timezone.utc).isoformat() def _parse_iso(value: str) -> datetime: return datetime.fromisoformat(value.replace("Z", "+00:00")) @dataclass(slots=True) class ConnectorMessageDedupeRecord: dedupe_key: str connection_id: str event_id: str status: str first_seen_at: str updated_at: str delivery_attempts: int message_id: str | None = None last_error: str | None = None def to_dict(self) -> dict[str, Any]: return asdict(self) @classmethod def from_dict(cls, data: dict[str, Any]) -> "ConnectorMessageDedupeRecord": return cls( dedupe_key=str(data.get("dedupe_key") or ""), connection_id=str(data.get("connection_id") or ""), event_id=str(data.get("event_id") or ""), status=str(data.get("status") or "processing"), first_seen_at=str(data.get("first_seen_at") or _iso_now()), updated_at=str(data.get("updated_at") or _iso_now()), delivery_attempts=int(data.get("delivery_attempts") or 0), message_id=str(data["message_id"]) if data.get("message_id") is not None else None, last_error=str(data["last_error"]) if data.get("last_error") is not None else None, ) @dataclass(slots=True) class DedupeBeginResult: should_process: bool dedupe_key: str status: str http_status: int retry_after_seconds: int | None record: ConnectorMessageDedupeRecord class MessageDedupeStore: def __init__(self, path: Path, *, processing_ttl_seconds: int = 60) -> None: self.path = Path(path) self.processing_ttl_seconds = int(processing_ttl_seconds) self._lock = Lock() def begin(self, *, connection_id: str, event_id: str, delivery_attempt: int) -> DedupeBeginResult: dedupe_key = f"{connection_id}:{event_id}" now = _iso_now() with self._lock: data = self._load() raw = data["records"].get(dedupe_key) if isinstance(raw, dict): record = ConnectorMessageDedupeRecord.from_dict(raw) if record.status == "completed": return DedupeBeginResult(False, dedupe_key, record.status, 200, None, record) if record.status == "processing" and not self._is_stale(record, now): return DedupeBeginResult(False, dedupe_key, record.status, 409, 5, record) record.status = "processing" record.updated_at = now record.delivery_attempts = max(record.delivery_attempts + 1, int(delivery_attempt)) record.last_error = None else: record = ConnectorMessageDedupeRecord( dedupe_key=dedupe_key, connection_id=connection_id, event_id=event_id, status="processing", first_seen_at=now, updated_at=now, delivery_attempts=max(1, int(delivery_attempt)), ) data["records"][dedupe_key] = record.to_dict() self._save(data) return DedupeBeginResult(True, dedupe_key, record.status, 200, None, record) def complete(self, dedupe_key: str, *, message_id: str | None) -> ConnectorMessageDedupeRecord: return self._mark(dedupe_key, status="completed", message_id=message_id, error=None) def fail(self, dedupe_key: str, *, error: str) -> ConnectorMessageDedupeRecord: return self._mark(dedupe_key, status="failed", message_id=None, error=error) def _mark( self, dedupe_key: str, *, status: str, message_id: str | None, error: str | None, ) -> ConnectorMessageDedupeRecord: with self._lock: data = self._load() raw = data["records"].get(dedupe_key) if not isinstance(raw, dict): raise KeyError(dedupe_key) record = ConnectorMessageDedupeRecord.from_dict(raw) record.status = status record.updated_at = _iso_now() record.message_id = message_id or record.message_id record.last_error = error data["records"][dedupe_key] = record.to_dict() self._save(data) return record def _is_stale(self, record: ConnectorMessageDedupeRecord, now: str) -> bool: age = (_parse_iso(now) - _parse_iso(record.updated_at)).total_seconds() return age >= self.processing_ttl_seconds def _load(self) -> dict[str, Any]: if not self.path.exists(): return {"records": {}} try: data = json.loads(self.path.read_text(encoding="utf-8")) except (OSError, json.JSONDecodeError): return {"records": {}} if not isinstance(data, dict) or not isinstance(data.get("records"), dict): return {"records": {}} return data def _save(self, data: dict[str, Any]) -> None: self.path.parent.mkdir(parents=True, exist_ok=True) tmp_path = self.path.with_name(f"{self.path.name}.tmp") tmp_path.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") tmp_path.replace(self.path) ``` - [ ] **Step 4: Export dedupe symbols** Modify `app-instance/backend/beaver/interfaces/channels/connections/__init__.py`: ```python from .dedupe import ConnectorMessageDedupeRecord, DedupeBeginResult, MessageDedupeStore ``` Add these names to `__all__`. - [ ] **Step 5: Run dedupe tests** Run: ```bash cd app-instance/backend uv run pytest tests/unit/test_connector_message_dedupe_store.py -q ``` Expected: `4 passed`. - [ ] **Step 6: Commit Task 1** ```bash git add app-instance/backend/beaver/interfaces/channels/connections/dedupe.py app-instance/backend/beaver/interfaces/channels/connections/__init__.py app-instance/backend/tests/unit/test_connector_message_dedupe_store.py git commit -m "feat: add connector bridge dedupe store" ``` --- ### Task 2: Dynamic Runtime Channels **Files:** - Modify: `app-instance/backend/beaver/interfaces/channels/manager.py` - Modify: `app-instance/backend/beaver/interfaces/channels/runtime.py` - Test: `app-instance/backend/tests/unit/test_channel_runtime_dynamic_channels.py` - [ ] **Step 1: Write failing dynamic runtime tests** Create `app-instance/backend/tests/unit/test_channel_runtime_dynamic_channels.py`: ```python from __future__ import annotations import asyncio from beaver.foundation.config.schema import ChannelConfig from beaver.foundation.events import MessageBus, OutboundMessage from beaver.interfaces.channels.runtime import ChannelRuntime class FakeService: async def handle_inbound_message(self, inbound): return OutboundMessage(channel=inbound.channel, content="ok", session_id=inbound.session_id, finish_reason="stop") def test_runtime_add_channel_starts_new_channel_after_runtime_start(tmp_path) -> None: async def run() -> None: runtime = ChannelRuntime(service=FakeService(), workspace=tmp_path, channels={}, bus=MessageBus()) await runtime.start() try: await runtime.add_channel( "webhook-dev", ChannelConfig(enabled=True, kind="webhook", mode="webhook", account_id="acct"), ) assert "webhook-dev" in runtime.adapters assert runtime.states["webhook-dev"]["state"] == "running" finally: await runtime.stop() asyncio.run(run()) def test_runtime_add_channel_noops_for_same_config(tmp_path) -> None: async def run() -> None: cfg = ChannelConfig(enabled=True, kind="webhook", mode="webhook", account_id="acct") runtime = ChannelRuntime(service=FakeService(), workspace=tmp_path, channels={}, bus=MessageBus()) await runtime.start() try: await runtime.add_channel("webhook-dev", cfg) first = runtime.adapters["webhook-dev"] await runtime.add_channel("webhook-dev", cfg) assert runtime.adapters["webhook-dev"] is first finally: await runtime.stop() asyncio.run(run()) def test_runtime_replacement_failure_keeps_old_channel(tmp_path) -> None: async def run() -> None: good = ChannelConfig(enabled=True, kind="webhook", mode="webhook", account_id="acct") bad = ChannelConfig(enabled=True, kind="missing", mode="http", account_id="acct") runtime = ChannelRuntime(service=FakeService(), workspace=tmp_path, channels={}, bus=MessageBus()) await runtime.start() try: await runtime.add_channel("webhook-dev", good) old = runtime.adapters["webhook-dev"] try: await runtime.add_channel("webhook-dev", bad) except ValueError: pass else: raise AssertionError("Expected ValueError") assert runtime.adapters["webhook-dev"] is old assert runtime.channel_configs["webhook-dev"] == good assert runtime.states["webhook-dev"]["state"] == "running" finally: await runtime.stop() asyncio.run(run()) def test_runtime_remove_channel_stops_and_unregisters(tmp_path) -> None: async def run() -> None: runtime = ChannelRuntime(service=FakeService(), workspace=tmp_path, channels={}, bus=MessageBus()) await runtime.start() try: await runtime.add_channel( "webhook-dev", ChannelConfig(enabled=True, kind="webhook", mode="webhook", account_id="acct"), ) await runtime.remove_channel("webhook-dev") assert "webhook-dev" not in runtime.adapters assert "webhook-dev" not in runtime.manager.channels assert runtime.states["webhook-dev"]["state"] == "removed" finally: await runtime.stop() asyncio.run(run()) ``` - [ ] **Step 2: Run tests to verify failure** Run: ```bash cd app-instance/backend uv run pytest tests/unit/test_channel_runtime_dynamic_channels.py -q ``` Expected: fail with `AttributeError: 'ChannelRuntime' object has no attribute 'add_channel'`. - [ ] **Step 3: Add dynamic manager methods** Modify `app-instance/backend/beaver/interfaces/channels/manager.py`: ```python def register(self, channel: ChannelAdapter) -> None: if channel.channel_id in self.channels: raise ValueError(f"Channel already registered: {channel.channel_id}") self.channels[channel.channel_id] = channel def unregister(self, channel_id: str) -> ChannelAdapter | None: return self.channels.pop(channel_id, None) def replace_registered(self, channel: ChannelAdapter) -> ChannelAdapter | None: old = self.channels.get(channel.channel_id) self.channels[channel.channel_id] = channel return old ``` Keep `start()`, `stop()`, and `dispatch_outbound()` unchanged. - [ ] **Step 4: Add runtime lifecycle lock and methods** Modify `app-instance/backend/beaver/interfaces/channels/runtime.py`: ```python self._lifecycle_lock = asyncio.Lock() ``` Add methods to `ChannelRuntime`: ```python async def add_channel(self, channel_id: str, config: ChannelConfig) -> None: async with self._lifecycle_lock: current = self.channel_configs.get(channel_id) if current == config and channel_id in self.adapters: return if not config.enabled: await self._remove_channel_locked(channel_id) self.channel_configs[channel_id] = config self.states[channel_id] = {"state": "disabled", "last_error": None} return adapter = self._build_adapter(channel_id, config) await adapter.start() old = self.manager.replace_registered(adapter) old_adapter = self.adapters.get(channel_id) self.adapters[channel_id] = adapter self.channel_configs[channel_id] = config self.states[channel_id] = {"state": "running", "last_error": None, "started_at": _iso_now()} self.events.record(channel_id=channel_id, kind="adapter_started") if old_adapter is not None and old_adapter is not adapter: await old_adapter.stop() async def remove_channel(self, channel_id: str) -> None: async with self._lifecycle_lock: await self._remove_channel_locked(channel_id) async def _remove_channel_locked(self, channel_id: str) -> None: adapter = self.adapters.pop(channel_id, None) self.manager.unregister(channel_id) self.channel_configs.pop(channel_id, None) if adapter is not None: await adapter.stop() self.events.record(channel_id=channel_id, kind="adapter_stopped") self.states[channel_id] = {"state": "removed", "last_error": None} ``` - [ ] **Step 5: Run dynamic runtime tests** Run: ```bash cd app-instance/backend uv run pytest tests/unit/test_channel_runtime_dynamic_channels.py tests/unit/test_channel_runtime.py tests/unit/test_gateway_channels.py -q ``` Expected: all listed tests pass. - [ ] **Step 6: Commit Task 2** ```bash git add app-instance/backend/beaver/interfaces/channels/manager.py app-instance/backend/beaver/interfaces/channels/runtime.py app-instance/backend/tests/unit/test_channel_runtime_dynamic_channels.py git commit -m "feat: support dynamic runtime channels" ``` --- ### Task 3: External Connector Channel **Files:** - Create: `app-instance/backend/beaver/interfaces/channels/connections/sidecar_client.py` - Create: `app-instance/backend/beaver/interfaces/channels/external_connector.py` - Modify: `app-instance/backend/beaver/interfaces/channels/__init__.py` - Test: `app-instance/backend/tests/unit/test_external_connector_channel.py` - [ ] **Step 1: Write failing channel tests** Create `app-instance/backend/tests/unit/test_external_connector_channel.py`: ```python from __future__ import annotations import asyncio from beaver.foundation.events import ChannelIdentity, OutboundMessage from beaver.interfaces.channels.external_connector import ExternalConnectorChannel, _request_id class FakeSidecarClient: def __init__(self) -> None: self.sent: list[dict] = [] async def send(self, payload: dict) -> dict: self.sent.append(payload) return {"ok": True, "providerMessageId": "provider-1"} def test_external_connector_channel_sends_with_target_and_request_id() -> None: async def run() -> None: client = FakeSidecarClient() channel = ExternalConnectorChannel( channel_id="weixin-main", platform_kind="weixin", connection_id="conn_1", account_id="weixin:me", display_name="Weixin Main", sidecar_client=client, ) message = OutboundMessage( channel="weixin-main", content="reply", session_id="s1", finish_reason="stop", message_id="out-msg-1", channel_identity=ChannelIdentity( channel_id="weixin-main", kind="weixin", account_id="weixin:me", peer_id="peer-1", peer_type="dm", thread_id=None, user_id="sender-1", message_id="in-msg-1", ), ) await channel.send(message) assert client.sent == [ { "requestId": "out_weixin-main:s1:out-msg-1", "connectionId": "conn_1", "channelId": "weixin-main", "kind": "weixin", "target": {"peerId": "peer-1", "peerType": "dm", "threadId": None}, "content": "reply", "metadata": {"inboundMessageId": "in-msg-1", "sessionId": "s1"}, } ] asyncio.run(run()) def test_external_connector_request_id_falls_back_when_message_id_is_none_or_blank() -> None: identity = ChannelIdentity( channel_id="weixin-main", kind="weixin", account_id="weixin:me", peer_id="peer-1", peer_type="dm", message_id="in-msg-1", ) first = OutboundMessage( channel="weixin-main", content="same reply", session_id="s1", finish_reason="stop", message_id=None, # type: ignore[arg-type] channel_identity=identity, ) second = OutboundMessage( channel="weixin-main", content="same reply", session_id="s1", finish_reason="stop", message_id="", channel_identity=identity, ) assert _request_id(first) == _request_id(second) assert _request_id(first).startswith("out_weixin-main:s1:") def test_external_connector_channel_requires_identity() -> None: async def run() -> None: channel = ExternalConnectorChannel( channel_id="weixin-main", platform_kind="weixin", connection_id="conn_1", account_id="weixin:me", display_name="Weixin Main", sidecar_client=FakeSidecarClient(), ) message = OutboundMessage(channel="weixin-main", content="reply", session_id="s1", finish_reason="stop") try: await channel.send(message) except ValueError as exc: assert "channel_identity is required" in str(exc) else: raise AssertionError("Expected ValueError") asyncio.run(run()) ``` - [ ] **Step 2: Run tests to verify failure** Run: ```bash cd app-instance/backend uv run pytest tests/unit/test_external_connector_channel.py -q ``` Expected: fail with `ModuleNotFoundError: No module named 'beaver.interfaces.channels.external_connector'`. - [ ] **Step 3: Implement sidecar client** Create `app-instance/backend/beaver/interfaces/channels/connections/sidecar_client.py`: ```python from __future__ import annotations import hashlib from typing import Any import httpx class ConnectorSidecarClient: def __init__(self, *, base_url: str, token: str, timeout_seconds: float = 20.0) -> None: self.base_url = base_url.rstrip("/") self.token = token self.timeout_seconds = float(timeout_seconds) async def get_connectors(self) -> list[dict[str, Any]]: return await self._request("GET", "/connectors") async def start_session(self, payload: dict[str, Any]) -> dict[str, Any]: return await self._request("POST", "/connector-sessions", json=payload) async def get_session(self, session_id: str) -> dict[str, Any]: return await self._request("GET", f"/connector-sessions/{session_id}") async def cancel_session(self, session_id: str) -> dict[str, Any]: return await self._request("POST", f"/connector-sessions/{session_id}/cancel", json={}) async def logout(self, connection_id: str) -> dict[str, Any]: return await self._request("POST", f"/connections/{connection_id}/logout", json={}) async def send(self, payload: dict[str, Any]) -> dict[str, Any]: return await self._request("POST", "/send", json=payload) async def _request(self, method: str, path: str, *, json: dict[str, Any] | None = None) -> Any: headers = {"Authorization": f"Bearer {self.token}"} if self.token else {} async with httpx.AsyncClient(timeout=self.timeout_seconds) as client: response = await client.request(method, f"{self.base_url}{path}", json=json, headers=headers) response.raise_for_status() return response.json() ``` - [ ] **Step 4: Implement external channel** Create `app-instance/backend/beaver/interfaces/channels/external_connector.py`: ```python from __future__ import annotations from typing import Any from beaver.foundation.events import OutboundMessage from beaver.interfaces.channels.connections.sidecar_client import ConnectorSidecarClient class ExternalConnectorChannel: def __init__( self, *, channel_id: str, platform_kind: str, connection_id: str, account_id: str, display_name: str, sidecar_client: ConnectorSidecarClient | Any, ) -> None: self.channel_id = channel_id self.kind = "external_connector" self.mode = "http" self.platform_kind = platform_kind self.connection_id = connection_id self.account_id = account_id self.display_name = display_name or channel_id self.sidecar_client = sidecar_client self.started = False async def start(self) -> None: self.started = True async def stop(self) -> None: self.started = False async def send(self, message: OutboundMessage) -> None: identity = message.channel_identity if identity is None: raise ValueError("channel_identity is required for external connector sends") payload = { "requestId": _request_id(message), "connectionId": self.connection_id, "channelId": self.channel_id, "kind": self.platform_kind, "target": { "peerId": identity.peer_id, "peerType": identity.peer_type, "threadId": identity.thread_id, }, "content": message.content, "metadata": { "inboundMessageId": identity.message_id, "sessionId": message.session_id, }, } await self.sidecar_client.send(payload) def _request_id(message: OutboundMessage) -> str: identity = message.channel_identity channel = message.channel or (identity.channel_id if identity else "unknown") session_id = message.session_id or (identity.session_id() if identity else "unknown") message_id = str(message.message_id or "").strip() if not message_id: basis = "|".join( [ message.content, identity.message_id if identity and identity.message_id else "", identity.peer_id if identity else "", message.finish_reason, ] ) message_id = hashlib.sha256(basis.encode("utf-8")).hexdigest()[:24] return f"out_{channel}:{session_id}:{message_id}" ``` - [ ] **Step 5: Export channel symbol** Modify `app-instance/backend/beaver/interfaces/channels/__init__.py`: ```python from .external_connector import ExternalConnectorChannel ``` Add `ExternalConnectorChannel` to `__all__`. - [ ] **Step 6: Run channel tests** Run: ```bash cd app-instance/backend uv run pytest tests/unit/test_external_connector_channel.py -q ``` Expected: `2 passed`. - [ ] **Step 7: Commit Task 3** ```bash git add app-instance/backend/beaver/interfaces/channels/connections/sidecar_client.py app-instance/backend/beaver/interfaces/channels/external_connector.py app-instance/backend/beaver/interfaces/channels/__init__.py app-instance/backend/tests/unit/test_external_connector_channel.py git commit -m "feat: add external connector channel" ``` --- ### Task 4: Runtime Factory For External Connector Channel **Files:** - Modify: `app-instance/backend/beaver/interfaces/channels/runtime.py` - Test: `app-instance/backend/tests/unit/test_channel_runtime_dynamic_channels.py` - [ ] **Step 1: Extend dynamic runtime tests** Append to `app-instance/backend/tests/unit/test_channel_runtime_dynamic_channels.py`: ```python def test_runtime_builds_external_connector_channel(tmp_path, monkeypatch) -> None: async def run() -> None: monkeypatch.setenv("EXTERNAL_CONNECTOR_TOKEN", "connector-token") runtime = ChannelRuntime(service=FakeService(), workspace=tmp_path, channels={}, bus=MessageBus()) await runtime.start() try: await runtime.add_channel( "weixin-main", ChannelConfig( enabled=True, kind="external_connector", mode="http", account_id="weixin:me", display_name="Weixin Main", config={ "platformKind": "weixin", "connectionId": "conn_1", "sidecarBaseUrl": "http://external-connector:8787", }, ), ) adapter = runtime.adapters["weixin-main"] assert adapter.kind == "external_connector" assert adapter.mode == "http" assert getattr(adapter, "platform_kind") == "weixin" finally: await runtime.stop() asyncio.run(run()) ``` - [ ] **Step 2: Run test to verify failure** Run: ```bash cd app-instance/backend uv run pytest tests/unit/test_channel_runtime_dynamic_channels.py::test_runtime_builds_external_connector_channel -q ``` Expected: fail with `ValueError: Unsupported channel kind/mode: external_connector/http`. - [ ] **Step 3: Add runtime factory branch** Modify `_build_adapter()` in `app-instance/backend/beaver/interfaces/channels/runtime.py` before the final `raise`: ```python if cfg.kind == "external_connector" and cfg.mode == "http": import os from beaver.interfaces.channels.connections.sidecar_client import ConnectorSidecarClient from beaver.interfaces.channels.external_connector import ExternalConnectorChannel base_url = str(cfg.config.get("sidecarBaseUrl") or os.getenv("EXTERNAL_CONNECTOR_BASE_URL") or "").strip() token = os.getenv("EXTERNAL_CONNECTOR_TOKEN", "") platform_kind = str(cfg.config.get("platformKind") or "").strip() connection_id = str(cfg.config.get("connectionId") or "").strip() if not base_url: raise ValueError("external connector sidecarBaseUrl is required") if not platform_kind: raise ValueError("external connector platformKind is required") if not connection_id: raise ValueError("external connector connectionId is required") return ExternalConnectorChannel( channel_id=channel_id, platform_kind=platform_kind, connection_id=connection_id, account_id=cfg.account_id, display_name=cfg.display_name, sidecar_client=ConnectorSidecarClient(base_url=base_url, token=token), ) ``` - [ ] **Step 4: Run tests** Run: ```bash cd app-instance/backend uv run pytest tests/unit/test_channel_runtime_dynamic_channels.py tests/unit/test_external_connector_channel.py -q ``` Expected: all listed tests pass. - [ ] **Step 5: Commit Task 4** ```bash git add app-instance/backend/beaver/interfaces/channels/runtime.py app-instance/backend/tests/unit/test_channel_runtime_dynamic_channels.py git commit -m "feat: materialize external connector channels" ``` --- ### Task 5: Bridge API **Files:** - Modify: `app-instance/backend/beaver/interfaces/web/schemas/chat.py` - Modify: `app-instance/backend/beaver/interfaces/web/schemas/__init__.py` - Modify: `app-instance/backend/beaver/interfaces/web/app.py` - Test: `app-instance/backend/tests/unit/test_external_connector_bridge_api.py` - [ ] **Step 1: Write failing bridge API tests** Create `app-instance/backend/tests/unit/test_external_connector_bridge_api.py`: ```python from __future__ import annotations from fastapi.testclient import TestClient from beaver.interfaces.channels.connections import ChannelConnectionStore, CredentialStore from beaver.interfaces.web.app import create_app from beaver.services.agent_service import AgentService def _app(tmp_path, monkeypatch): monkeypatch.setenv("BEAVER_BRIDGE_TOKEN", "bridge-token") config_path = tmp_path / "config.json" config_path.write_text( '{"agents": {"defaults": {"workspace": "%s"}}, "providers": {}}' % str(tmp_path), encoding="utf-8", ) service = AgentService(config_path=config_path) app = create_app(service=service, manage_service_lifecycle=False) return app, service def test_bridge_endpoint_accepts_valid_event(tmp_path, monkeypatch) -> None: app, service = _app(tmp_path, monkeypatch) state_dir = tmp_path / "state" / "channel_connections" store = ChannelConnectionStore(state_dir / "connections.json") connection = store.create( kind="weixin", mode="sidecar", display_name="Weixin Main", account_id="weixin:me", owner_user_id=None, auth_type="connector_session", ) store.update_status(connection.connection_id, status="connected", last_error=None) try: with TestClient(app) as client: response = client.post( "/api/channel-connector-bridge/events", headers={"Authorization": "Bearer bridge-token"}, json={ "eventId": "evt-1", "timestamp": "2026-06-02T09:30:00Z", "deliveryAttempt": 1, "connectionId": connection.connection_id, "channelId": connection.channel_id, "kind": "weixin", "accountId": "weixin:me", "peerId": "peer-1", "peerType": "dm", "userId": "sender-1", "threadId": None, "messageId": "msg-1", "messageType": "text", "content": "hello", "metadata": {}, }, ) assert response.status_code == 200 assert response.json()["accepted"] is True finally: service.close() def test_bridge_endpoint_rejects_invalid_token(tmp_path, monkeypatch) -> None: app, service = _app(tmp_path, monkeypatch) try: with TestClient(app) as client: response = client.post("/api/channel-connector-bridge/events", headers={"Authorization": "Bearer wrong"}, json={}) assert response.status_code == 401 finally: service.close() def test_bridge_endpoint_returns_conflict_for_processing_duplicate(tmp_path, monkeypatch) -> None: app, service = _app(tmp_path, monkeypatch) state_dir = tmp_path / "state" / "channel_connections" store = ChannelConnectionStore(state_dir / "connections.json") connection = store.create( kind="weixin", mode="sidecar", display_name="Weixin Main", account_id="weixin:me", owner_user_id=None, auth_type="connector_session", ) store.update_status(connection.connection_id, status="connected", last_error=None) payload = { "eventId": "evt-1", "timestamp": "2026-06-02T09:30:00Z", "deliveryAttempt": 1, "connectionId": connection.connection_id, "channelId": connection.channel_id, "kind": "weixin", "accountId": "weixin:me", "peerId": "peer-1", "peerType": "dm", "userId": "sender-1", "threadId": None, "messageId": "msg-1", "messageType": "text", "content": "hello", "metadata": {}, } try: with TestClient(app) as client: first = client.post("/api/channel-connector-bridge/events", headers={"Authorization": "Bearer bridge-token"}, json=payload) second = client.post("/api/channel-connector-bridge/events", headers={"Authorization": "Bearer bridge-token"}, json={**payload, "deliveryAttempt": 2}) assert first.status_code == 200 assert second.status_code in {200, 409} finally: service.close() ``` - [ ] **Step 2: Run tests to verify failure** Run: ```bash cd app-instance/backend uv run pytest tests/unit/test_external_connector_bridge_api.py -q ``` Expected: fail with `404 Not Found` for `/api/channel-connector-bridge/events`. - [ ] **Step 3: Add bridge schemas** Append to `app-instance/backend/beaver/interfaces/web/schemas/chat.py`: ```python class WebConnectorBridgeEventRequest(BaseModel): event_id: str = Field(alias="eventId") timestamp: str delivery_attempt: int = Field(default=1, alias="deliveryAttempt") connection_id: str = Field(alias="connectionId") channel_id: str = Field(alias="channelId") kind: str account_id: str = Field(alias="accountId") peer_id: str = Field(alias="peerId") peer_type: str = Field(default="unknown", alias="peerType") user_id: str | None = Field(default=None, alias="userId") thread_id: str | None = Field(default=None, alias="threadId") message_id: str = Field(alias="messageId") message_type: str = Field(default="text", alias="messageType") content: str metadata: dict[str, Any] = Field(default_factory=dict) class WebConnectorBridgeEventResponse(BaseModel): accepted: bool duplicate: bool = False pending: bool = False retry_after_seconds: int | None = Field(default=None, alias="retryAfterSeconds") ``` Export both in `app-instance/backend/beaver/interfaces/web/schemas/__init__.py`. - [ ] **Step 4: Add bridge endpoint** Modify `app-instance/backend/beaver/interfaces/web/app.py` imports to include: ```python from beaver.foundation.events import ChannelIdentity, InboundMessage from beaver.interfaces.channels.connections import MessageDedupeStore ``` Add helpers: ```python def _bridge_token() -> str: return os.getenv("BEAVER_BRIDGE_TOKEN", "") def _message_dedupe_store(workspace: Path) -> MessageDedupeStore: return MessageDedupeStore(_connection_state_dir(workspace) / "message_dedupe.json") ``` Add the route inside `create_app()`: ```python @app.post("/api/channel-connector-bridge/events", response_model=WebConnectorBridgeEventResponse) async def accept_connector_bridge_event( request: Request, payload: WebConnectorBridgeEventRequest, authorization: str | None = Header(default=None), ) -> JSONResponse | WebConnectorBridgeEventResponse: expected = _bridge_token() if not expected or authorization != f"Bearer {expected}": raise HTTPException(status_code=401, detail="Invalid connector bridge token") registry = get_channel_connector_registry(request) try: connection = registry.connection_store.get(payload.connection_id) except KeyError: raise HTTPException(status_code=404, detail="Channel connection not found") if connection.status == "revoked": raise HTTPException(status_code=404, detail="Channel connection not found") store = _message_dedupe_store(_channel_connection_workspace(request)) begin = store.begin( connection_id=payload.connection_id, event_id=payload.event_id, delivery_attempt=payload.delivery_attempt, ) if not begin.should_process: body = WebConnectorBridgeEventResponse( accepted=begin.http_status == 200, duplicate=True, pending=begin.http_status == 409, retryAfterSeconds=begin.retry_after_seconds, ).model_dump(by_alias=True) return JSONResponse(status_code=begin.http_status, content=body) runtime = get_channel_runtime(request) identity = ChannelIdentity( channel_id=payload.channel_id, kind=payload.kind, account_id=payload.account_id, peer_id=payload.peer_id, thread_id=payload.thread_id, peer_type=payload.peer_type, user_id=payload.user_id, message_id=payload.message_id, ) inbound = InboundMessage( channel=payload.channel_id, content=payload.content, content_type=payload.message_type, channel_identity=identity, user_id=payload.user_id, message_id=payload.message_id, metadata=dict(payload.metadata), ) result = await runtime.accept_inbound(inbound) if result.accepted: store.complete(begin.dedupe_key, message_id=payload.message_id) else: store.fail(begin.dedupe_key, error=result.error or "runtime rejected bridge event") return WebConnectorBridgeEventResponse(accepted=result.accepted, duplicate=result.duplicate, pending=result.pending) ``` If `_channel_connection_workspace(request)` does not exist yet, add it: ```python def _channel_connection_workspace(request: Request) -> Path: workspace = getattr(request.app.state, "channel_connection_workspace", None) if workspace is not None: return Path(workspace) return Path(get_agent_service(request).loader.workspace) ``` - [ ] **Step 5: Run bridge tests** Run: ```bash cd app-instance/backend uv run pytest tests/unit/test_external_connector_bridge_api.py -q ``` Expected: all listed tests pass. If the duplicate test returns `200` because runtime completes before the second request, add a focused `MessageDedupeStore` API test for `409`; do not weaken the store behavior. - [ ] **Step 6: Commit Task 5** ```bash git add app-instance/backend/beaver/interfaces/web/app.py app-instance/backend/beaver/interfaces/web/schemas/chat.py app-instance/backend/beaver/interfaces/web/schemas/__init__.py app-instance/backend/tests/unit/test_external_connector_bridge_api.py git commit -m "feat: add external connector bridge api" ``` --- ### Task 6: Weixin And Feishu Connectors **Files:** - Create: `app-instance/backend/beaver/interfaces/channels/connections/external.py` - Modify: `app-instance/backend/beaver/interfaces/channels/connections/__init__.py` - Modify: `app-instance/backend/beaver/interfaces/channels/connections/connectors.py` - Modify: `app-instance/backend/beaver/interfaces/web/schemas/chat.py` - Modify: `app-instance/backend/beaver/interfaces/web/schemas/__init__.py` - Modify: `app-instance/backend/beaver/interfaces/web/app.py` - Test: `app-instance/backend/tests/unit/test_external_sidecar_connectors.py` - [ ] **Step 1: Write failing connector tests** Create `app-instance/backend/tests/unit/test_external_sidecar_connectors.py`: ```python from __future__ import annotations import asyncio from beaver.interfaces.channels.connections import ( ChannelConnectionStore, CredentialStore, FeishuConnector, WeixinConnector, ) class FakeSidecarClient: def __init__(self) -> None: self.sessions: dict[str, dict] = {} self.started: list[dict] = [] self.logged_out: list[str] = [] async def start_session(self, payload: dict) -> dict: self.started.append(payload) session = { "sessionId": "cs_1", "kind": payload["kind"], "status": "qr_ready", "qrImage": "data:image/png;base64,abc", "accountId": None, "displayName": None, "metadata": {}, } self.sessions["cs_1"] = session return session async def get_session(self, session_id: str) -> dict: return self.sessions[session_id] async def logout(self, connection_id: str) -> dict: self.logged_out.append(connection_id) return {"ok": True} def test_weixin_connector_starts_connector_session(tmp_path) -> None: async def run() -> None: connection_store = ChannelConnectionStore(tmp_path / "connections.json") credential_store = CredentialStore(tmp_path / "credentials.json") client = FakeSidecarClient() connector = WeixinConnector( connection_store=connection_store, credential_store=credential_store, sidecar_client=client, sidecar_base_url="http://external-connector:8787", ) view = await connector.start_session(display_name="Weixin Main", owner_user_id="user-1", options={}) assert view["sessionId"] == "cs_1" assert client.started[0]["kind"] == "weixin" assert client.started[0]["connectionId"].startswith("conn_") assert connection_store.list()[0].kind == "weixin" assert connection_store.list()[0].status == "pairing" asyncio.run(run()) def test_weixin_connector_poll_connected_materializes_external_runtime(tmp_path) -> None: async def run() -> None: connection_store = ChannelConnectionStore(tmp_path / "connections.json") credential_store = CredentialStore(tmp_path / "credentials.json") client = FakeSidecarClient() connector = WeixinConnector( connection_store=connection_store, credential_store=credential_store, sidecar_client=client, sidecar_base_url="http://external-connector:8787", ) await connector.start_session(display_name="Weixin Main", owner_user_id=None, options={}) connection = connection_store.list()[0] client.sessions["cs_1"] = { "sessionId": "cs_1", "kind": "weixin", "status": "connected", "accountId": "weixin:me", "displayName": "Me", "metadata": {"stateRef": "state-1"}, } result = await connector.poll_session("cs_1") updated = connection_store.get(connection.connection_id) spec = await connector.materialize_runtime(connection.connection_id) assert result["status"] == "connected" assert updated.status == "connected" assert updated.account_id == "weixin:me" assert spec.kind == "external_connector" assert spec.mode == "http" assert spec.config["platformKind"] == "weixin" assert spec.config["sidecarBaseUrl"] == "http://external-connector:8787" asyncio.run(run()) def test_feishu_connector_uses_feishu_kind(tmp_path) -> None: async def run() -> None: connection_store = ChannelConnectionStore(tmp_path / "connections.json") credential_store = CredentialStore(tmp_path / "credentials.json") client = FakeSidecarClient() connector = FeishuConnector( connection_store=connection_store, credential_store=credential_store, sidecar_client=client, sidecar_base_url="http://external-connector:8787", ) await connector.start_session(display_name="Feishu Main", owner_user_id=None, options={"domain": "feishu"}) assert client.started[0]["kind"] == "feishu" assert client.started[0]["options"] == {"domain": "feishu"} asyncio.run(run()) ``` - [ ] **Step 2: Run tests to verify failure** Run: ```bash cd app-instance/backend uv run pytest tests/unit/test_external_sidecar_connectors.py -q ``` Expected: fail with `ImportError: cannot import name 'WeixinConnector'`. - [ ] **Step 3: Implement connector classes** Create `app-instance/backend/beaver/interfaces/channels/connections/external.py`: ```python from __future__ import annotations from typing import Any from .models import ChannelRuntimeSpec, ValidationResult from .sidecar_client import ConnectorSidecarClient from .store import ChannelConnectionStore, CredentialStore class ExternalConnectorBase: kind = "" capabilities: list[str] = [] def __init__( self, *, connection_store: ChannelConnectionStore, credential_store: CredentialStore, sidecar_client: ConnectorSidecarClient | Any, sidecar_base_url: str, ) -> None: self.connection_store = connection_store self.credential_store = credential_store self.sidecar_client = sidecar_client self.sidecar_base_url = sidecar_base_url async def start_session( self, *, display_name: str, owner_user_id: str | None, options: dict[str, Any], ) -> dict[str, Any]: connection = self.connection_store.create( kind=self.kind, mode="sidecar", display_name=display_name or self.kind, account_id="", owner_user_id=owner_user_id, auth_type="connector_session", runtime_config={"sidecarBaseUrl": self.sidecar_base_url}, capabilities=list(self.capabilities), ) self.connection_store.update_status(connection.connection_id, status="pairing", last_error=None) payload = { "kind": self.kind, "connectionId": connection.connection_id, "channelId": connection.channel_id, "displayName": connection.display_name, "callbackBaseUrl": "", "options": dict(options), } view = await self.sidecar_client.start_session(payload) connection.pairing_session_id = str(view.get("sessionId") or "") self.connection_store.update(connection) return view async def poll_session(self, session_id: str) -> dict[str, Any]: view = await self.sidecar_client.get_session(session_id) connection = self._connection_for_session(session_id) status = str(view.get("status") or "") if status == "connected": connection.account_id = str(view.get("accountId") or connection.account_id) connection.display_name = str(view.get("displayName") or connection.display_name) metadata = view.get("metadata") if isinstance(view.get("metadata"), dict) else {} state_ref = metadata.get("stateRef") if state_ref: connection.credentials_ref = self.credential_store.put(kind=self.kind, values={"stateRef": state_ref}) self.connection_store.update(connection) self.connection_store.update_status(connection.connection_id, status="connected", last_error=None) elif status in {"expired", "error", "cancelled"}: self.connection_store.update_status(connection.connection_id, status="error", last_error=str(view.get("error") or status)) return view async def validate(self, connection_id: str) -> ValidationResult: connection = self.connection_store.get(connection_id) if connection.status in {"connected", "running"}: return ValidationResult(ok=True, status="connected", account_id=connection.account_id, display_name=connection.display_name) return ValidationResult(ok=False, status=connection.status, error=connection.last_error) async def materialize_runtime(self, connection_id: str) -> ChannelRuntimeSpec: connection = self.connection_store.get(connection_id) return ChannelRuntimeSpec( channel_id=connection.channel_id, kind="external_connector", mode="http", account_id=connection.account_id, display_name=connection.display_name, config={ "platformKind": self.kind, "connectionId": connection.connection_id, "sidecarBaseUrl": connection.runtime_config.get("sidecarBaseUrl") or self.sidecar_base_url, }, secrets_ref=None, ) async def revoke(self, connection_id: str) -> None: await self.sidecar_client.logout(connection_id) def _connection_for_session(self, session_id: str): for connection in self.connection_store.list(): if connection.pairing_session_id == session_id: return connection raise KeyError(session_id) class WeixinConnector(ExternalConnectorBase): kind = "weixin" capabilities = ["receive_text", "send_text", "receive_media", "direct_messages"] class FeishuConnector(ExternalConnectorBase): kind = "feishu" capabilities = ["receive_text", "send_text", "receive_media", "groups"] ``` - [ ] **Step 4: Export connectors and update registry materialization** Modify `app-instance/backend/beaver/interfaces/channels/connections/__init__.py`: ```python from .external import ExternalConnectorBase, FeishuConnector, WeixinConnector ``` Add names to `__all__`. Ensure `ChannelConnectorRegistry.materialize_channel_configs()` accepts `ChannelRuntimeSpec(kind="external_connector", mode="http")` and emits `ChannelConfig(secrets={})`. - [ ] **Step 5: Add connector session web schemas** Append to `app-instance/backend/beaver/interfaces/web/schemas/chat.py`: ```python class WebConnectorSessionCreateRequest(BaseModel): kind: str display_name: str | None = Field(default=None, alias="displayName") owner_user_id: str | None = Field(default=None, alias="ownerUserId") options: dict[str, Any] = Field(default_factory=dict) class WebConnectorSessionResponse(BaseModel): session: dict[str, Any] connection: dict[str, Any] | None = None ``` Export both in `app-instance/backend/beaver/interfaces/web/schemas/__init__.py`. - [ ] **Step 6: Register Weixin/Feishu in app registry** Modify `_build_channel_connector_registry()` in `app-instance/backend/beaver/interfaces/web/app.py` to create a sidecar client: ```python sidecar_base_url = os.getenv("EXTERNAL_CONNECTOR_BASE_URL", "http://external-connector:8787") sidecar_token = os.getenv("EXTERNAL_CONNECTOR_TOKEN", "") sidecar_client = ConnectorSidecarClient(base_url=sidecar_base_url, token=sidecar_token) registry.register(WeixinConnector( connection_store=connection_store, credential_store=credential_store, sidecar_client=sidecar_client, sidecar_base_url=sidecar_base_url, )) registry.register(FeishuConnector( connection_store=connection_store, credential_store=credential_store, sidecar_client=sidecar_client, sidecar_base_url=sidecar_base_url, )) ``` Keep Telegram registration unchanged. - [ ] **Step 7: Add connector session API routes** Add routes near existing channel connection APIs in `app-instance/backend/beaver/interfaces/web/app.py`: ```python @app.post("/api/channel-connector-sessions", response_model=WebConnectorSessionResponse) async def start_channel_connector_session( request: Request, payload: WebConnectorSessionCreateRequest, ) -> WebConnectorSessionResponse: registry = get_channel_connector_registry(request) connector = registry.connector_for_kind(_clean_text(payload.kind)) if not hasattr(connector, "start_session"): raise HTTPException(status_code=400, detail="Connector does not support sessions") view = await connector.start_session( display_name=_clean_text(payload.display_name) or _clean_text(payload.kind), owner_user_id=_clean_text(payload.owner_user_id) or None, options=payload.options, ) connection = registry.connection_store.get(str(view.get("connectionId") or registry.connection_store.list()[-1].connection_id)) return WebConnectorSessionResponse(session=view, connection=_connection_response_view(connection)) @app.get("/api/channel-connector-sessions/{session_id}", response_model=WebConnectorSessionResponse) async def get_channel_connector_session(session_id: str, request: Request) -> WebConnectorSessionResponse: registry = get_channel_connector_registry(request) connection = next((item for item in registry.connection_store.list() if item.pairing_session_id == session_id), None) if connection is None: raise HTTPException(status_code=404, detail="Connector session not found") connector = registry.connector_for_kind(connection.kind) if not hasattr(connector, "poll_session"): raise HTTPException(status_code=400, detail="Connector does not support sessions") view = await connector.poll_session(session_id) connection = registry.connection_store.get(connection.connection_id) if connection.status == "connected": runtime = get_channel_runtime(request) config = (await registry.materialize_channel_configs())[connection.channel_id] await runtime.add_channel(connection.channel_id, config) return WebConnectorSessionResponse(session=view, connection=_connection_response_view(connection)) ``` Add `connector_for_kind()` to `ChannelConnectorRegistry`: ```python def connector_for_kind(self, kind: str) -> ChannelConnector: return self._connector(kind) ``` - [ ] **Step 8: Run connector tests** Run: ```bash cd app-instance/backend uv run pytest tests/unit/test_external_sidecar_connectors.py tests/unit/test_channel_connection_api.py tests/unit/test_channel_connector_registry.py -q ``` Expected: all listed tests pass. - [ ] **Step 9: Commit Task 6** ```bash git add app-instance/backend/beaver/interfaces/channels/connections app-instance/backend/beaver/interfaces/web/app.py app-instance/backend/beaver/interfaces/web/schemas/chat.py app-instance/backend/beaver/interfaces/web/schemas/__init__.py app-instance/backend/tests/unit/test_external_sidecar_connectors.py git commit -m "feat: add sidecar-backed channel connectors" ``` --- ### Task 7: Final Backend Verification **Files:** - Review: `docs/superpowers/specs/2026-06-02-external-sidecar-connectors-design.md` - [ ] **Step 1: Run focused backend tests** Run: ```bash cd app-instance/backend uv run pytest \ tests/unit/test_connector_message_dedupe_store.py \ tests/unit/test_external_connector_channel.py \ tests/unit/test_channel_runtime_dynamic_channels.py \ tests/unit/test_external_connector_bridge_api.py \ tests/unit/test_external_sidecar_connectors.py \ tests/unit/test_channel_connection_store.py \ tests/unit/test_channel_connector_registry.py \ tests/unit/test_channel_connection_api.py \ tests/unit/test_channel_runtime.py \ tests/unit/test_gateway_channels.py \ -q ``` Expected: all listed tests pass. - [ ] **Step 2: Run import tests** Run: ```bash cd app-instance/backend uv run pytest tests/unit/test_imports.py -q ``` Expected: all import tests pass. - [ ] **Step 3: Scan for leaked service tokens in implementation** Run: ```bash cd app-instance/backend rg -n "bridge-token|connector-token|token-1|token-2|secret-token" beaver || true ``` Expected: no implementation files contain fixture token values. - [ ] **Step 4: Commit verification-only fixes if needed** If Step 1 or Step 2 required a small fix, commit it: ```bash git add app-instance/backend git commit -m "fix: stabilize external connector backend runtime" ``` If no files changed, do not create an empty commit.