feat(bridges): per-message synthetic actor + admin UI (LC-78 v1) #243
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/lc-78-bridge-registration-surface"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Server-side surface for LC-78 protocol bridges. A registered out-of-process daemon (Matrix-appservice-bridge or similar) authenticates as a bot user with
bridge:post+bridge:heartbeatscopes and posts foreign-protocol messages into a lets-chat room as per-message synthetic actors. The daemon itself is documented indocs/protocol-bridges.md, NOT shipped here.The single load-bearing extension is the per-MESSAGE synthetic actor: LC-74 webhooks and LC-77 email-ingress bind one identity per channel, but a Matrix bridge needs alice@server and bob@server in the same bridged room as distinct actors. The endpoint snapshots
foreign_name+kindonto the message row at post time, so the render survives bridge-row removal under stop-new lifecycle. The render resolver gates onbridge_foreign_name.is_some(), notbridge_id, soON DELETE SET NULLdoes not flip historical messages back to(unknown).What ships
bridges(sealed daemon config underLETS_CHAT_SECRET_KEY, two-column AES-GCM convention fromimap_inbox_config/vapid_keys), plusmessages.bridge_id(FK ON DELETE SET NULL) + snapshottedbridge_foreign_name+bridge_kind.POST /api/v1/bridges/{id}/messages(scopebridge:post, 400s any non-nullforeign_avatarin v1),POST /api/v1/bridges/{id}/heartbeat(scopebridge:heartbeat, optional{error?}), cursor pagination onGET /api/v1/rooms/{id}/messages(the daemon's initial-sync forcing function: default 50, hard cap 200,next_cursorwalks history).Bridge(BridgeActorMeta { kind, avatar_url })arm rendered with a violet(via matrix)badge in templates + i18n.message.posted/message.edited/message.deleted/reaction.added) now carries anactorblock describing the ORIGINAL author. Daemons MUST self-filter onactor.kind == "bridge" && actor.bridge_id == <self>or they cause infinite cross-network amplification. Operator doc warns prominently.bridge; theApiAuth::require_not_bridgegate denies them every non-bridge endpoint by default, even with mistakenly-grantedmessages:write(defense in depth over scope gating).routes_bridge_role_isolationpins the contract./admin/bridgescreate / list / remove. Status derived fromlast_heartbeat_atage vsBRIDGE_HEARTBEAT_STALE_SECS = 90:pending/healthy/stale/errored. Plain remove (stop-new lifecycle: bridges row DELETED, snapshot preserves history).docs/protocol-bridges.mdcovers API surface, threat model, removal lifecycle, known gaps, and the prominent loop-break warning at the top.What's deferred
LC-78-AVATAR-PROXYtoken in body). Per-render foreign-homeserver fetch leaks viewer IPs. Proxy-cache lands in a follow-up.reaction.addedwithactor.kind=user; daemon ignores in v1.bridges.kindis plain TEXT (no CHECK); schema-free defer. Handler currently gates tokind == "matrix".@alice:matrix.org-> lets-chat userCriterion-owner-deferred decision: stop-new vs delete-history
Criterion 4 (
"removes the synthetic actors") was ambiguous. Owner had no preference. Chose stop-new on the principle that the wrong choice made in code (flipping to delete-history later via an additive admin branch) is recoverable; the wrong choice made in data (every removal hard-deletes history) is permanent. Schema is path-agnostic:ON DELETE SET NULLplus snapshotted columns. If a future requirement materializes for strict delete-history, it is a three-line addition to the admin remove handler.Pre-execution gates resolved
bridgetier, narrowest grant.list_messagessharinglist_messages_paginatedfor the API; web/HTMXlist_messagesuntouched.bridges.idin the payload + prominent operator-doc warning.Test plan
just testclean (115 binaries pass, 0 failed).just test-saasclean (40 binaries pass, 0 failed; standalone-gated tests correctly skipped).routes_api_bridge_messages(7),routes_api_bridge_heartbeat(8),routes_api_pagination(6),routes_bridge_role_isolation(7),outgoing_webhook_actor_payload(4),routes_admin_bridges(8). 40 new tests.routes_api, LC-73routes_bots, LC-74routes_webhooks, LC-75outgoing_webhooks, LC-77lc77_webhook_render_fixture,routes_message_edit_history.send_message_with_attachmentonce the newmessages.bridge_*columns landed in the SELECT.Verifying as the operator
/admin/bridges->Register a bridge. Pick room, name the bot, paste daemon config, submit. Copy the one-time token.POST /api/v1/bridges/{id}/messages+POST /api/v1/bridges/{id}/heartbeat. Use the bridge id shown in the admin URL./admin/outgoing-webhooks) for whichever events the daemon needs. Implement the loop-break filter onactor.kind == "bridge" && actor.bridge_id == <your id>BEFORE testing on a busy room.pending->healthyin/admin/bridges.