A Rust application-service bridge that exposes selected Rakazo agents as private chats in Beeper. One executable supports a personal self-hosted instance and an operator-configured multi-tenant instance. Beeper Desktop does not have to stay open.
Licensed under Apache-2.0. See NOTICE for attribution and SECURITY.md for private vulnerability reporting and deployment boundaries. Beeper is an external service; this license covers the bridge.
FPL's Beachhead Shroud deployment is owned by
shared-infra.
The source-owned .fab/pipelines.json publishes the immutable rootfs consumed by
that deployment. The personal Beachhead tenant is live with six agent chats;
text, attachments, audio files, quoted replies, thread routing and restart without
duplicate delivery were verified. The native user-service instructions below are for personal self-hosting.
Text and supported attachments from the configured Beeper owner go to the agent's existing Rakazo thread. Completed replies, attachments and questions return to Beeper. New messages sent through Rakazo's web interface also appear in Beeper; messages that originated in Beeper reuse their original events instead of echoing.
Quoted replies retain their targets. Agent replies to messages sent in a Beeper thread return to that thread, using the run ID returned by Rakazo. Threads still share the agent's Rakazo conversation and context. Beeper reaction input is deliberately not advertised or accepted because Rakazo cannot remove reactions. Rakazo-originated reactions are displayed as native Matrix annotations attached to their target; existing historical reaction events remain untouched. If a reply targets source content that has no visible Beeper message, the reply is delivered without a quote.
The bridge does not tap back every message. An acknowledgement is a promise —
"I saw this and I will follow up" — and it is only made while the agent is
already mid-thread on a prior task, because an idle agent answers instead. Every
acknowledgement is recorded as a promise in the bridge database, and a hook on
the bridge's own cadence enforces it: one nudge, then one visible admission in
the thread if the follow-up never comes. The policy is configurable per tenant
(ack_policy, default mid_thread_only); see
docs/ack-discipline.md.
Agent messages render Markdown as sanitized Matrix HTML: headings, emphasis, links, lists, quotes, tables and fenced code blocks. The original Markdown remains the plain-text fallback. Large messages split within the event size budget, preserving fenced code formatting and existing message identities. Existing imported text is retained without replay. External Markdown images become links.
Rooms use normal timeline messaging permissions for Beeper iOS compatibility; invites, room state and moderation remain restricted to the bridge.
Photos, GIFs, MP4 video and files use the authenticated artifact API and Matrix media repository. The bridge enforces a 10 MiB per-file limit and follows Rakazo's upload MIME allowlist. The current legacy allowlist accepts JPEG, PNG, WebP, GIF, MP4, PDF, text, Markdown, CSV, HTML, JSON and ZIP. Audio is rendered when received from Rakazo, but uploading audio from Beeper requires a Rakazo version whose upload allowlist accepts it. The FPL deployment includes the audio allowlist extension. Automatic transcription is not enabled. The bridge advertises supported formats and limits to Beeper. Invalid or unsupported incoming content produces a notice instead of blocking the queue. Encrypted attachment blobs are verified and decrypted; this does not add support for encrypted rooms.
History uses Beeper's native batch backfill API, preserving original timestamps and attributing old prompts to the owner and replies to the agent. It imports all available text before the chat's saved initial baseline, one page per sync, without notifications or changing read markers. Durable progress and deterministic event IDs make retries and restarts safe. Imported prompts cannot trigger the agent. History includes available attachments. An upgrade pass recovers existing event IDs and imports media omitted by older bridge versions without replaying their text. Old flattened reaction bubbles are preserved. Tool output and internal reasoning are excluded. Beeper's batch API is required for historical and live delivery; failures remain visible and retry without advancing the cursor.
Agent profile pictures and chat icons follow Rakazo, including uploaded PNG,
JPEG, WebP and GIF avatars and static PNGs of its generated mascots. The renderer
uses Rakazo's geometry and palette with source-derived parity vectors; the API
does not expose rendered mascot images. The bridge copies no external image URL.
Changed pictures and names sync automatically. Uploaded media references and
successful profile updates are cached per tenant and agent across restarts.
The FPL logo identifies the bridge bot and protocol in Beeper; individual agent
pictures remain the chat icons. The bundled logo is the existing FPL square mark
from waitlist/web/public/images/fpl_square.png.
Current limitation: agent rooms are intentionally unencrypted. End-to-bridge encryption is not implemented. Beeper's servers and the bridge operator can read their contents. The bridge creates its own private rooms with encryption disabled; do not start an encrypted DM with a ghost manually. Encrypted or unexpectedly shared rooms pause delivery. This is an initial self-hosted pilot, not a completed public SaaS onboarding product.
cargo check --locked
cargo test --locked
cargo build --release --locked
bash scripts/healthcheck.sh --base https://agents.fpl.devdocs/healthcheck.md documents the script's two surfaces,
the private bridge endpoint, and its exit codes.
docs/runbook.md is the operator runbook: symptom → check →
action for both surfaces, where logs live, restart and rollback rules.
docs/guest-rules.md states the no-compiles-in-guests rule
and the runtime image check that enforces it.
docs/logging.md says which build is running and gives the
LogQL and Shroud VM log path for each surface.
docs/monitoring.md installs the 5-minute systemd timer that
runs the health check, and states its single-machine limits.
docs/postmortems/ holds the postmortem habit:
when one is required, the template, and the index of past incidents.
docs/fleet-contract.md is the proposed fleet contract:
bot tiers, conversation→pane mapping, tapbacks, and the A2A envelope.
docs/ack-discipline.md is the acknowledgement
discipline: an ack is a promise, it is only allowed mid-thread, and a broken
promise is nudged once and then admitted in the thread.
The implementation and runtime are Rust. SQLite is bundled through its existing
Rust binding; TLS and authenticated payload encryption use established libraries.
bbctl is Beeper's external registration utility, used once for enrollment. No Go,
Node, Python, bridge framework, or proxy process is required to run this service.
-
Install Beeper's bridge manager and authenticate with
bbctl loginif necessary. Create a separate registration:umask 077 mkdir -p secrets data bbctl register --json sh-rakazo > secrets/registration-personal.json
Keep the entire JSON response. It includes
registration,homeserver_url,homeserver_domain, andyour_user_id. Do not register with a push address when using the default outbound WebSocket transport. Never delete an existing registration to fix a connection problem: deletion removes its rooms. -
Copy
config.example.tomltoconfig.toml. Set the owner and homeserver to the exact values from the registration bundle. Set the Rakazo space ID to the space that owns the selected agents. Paths are relative to the config file. -
Generate the database key and supply a valid Rakazo session bearer token:
target/release/rakazo-beeper generate-key > secrets/database-key # Place the Rakazo session token in secrets/rakazo-token using your secret manager. chmod 600 secrets/database-key secrets/rakazo-token secrets/registration-personal.json export RAKAZO_BEEPER_KEY_FILE="$PWD/secrets/database-key" export RAKAZO_PERSONAL_TOKEN_FILE="$PWD/secrets/rakazo-token"
For an issuer enrolled for device authorization, obtain a dedicated session:
target/release/rakazo-beeper login --output secrets/rakazo-token
Open the displayed FPL verification URL, enter the code, and approve the named Rakazo login. The Rust client completes Rakazo's existing OIDC flow, preserving state, nonce, and S256 PKCE, then saves its new session atomically with mode 0600. It reads no browser cookies. Its temporary issuer credential stays at the issuer and is discarded after login. No FPL refresh credential is stored. Issuer enrollment is described in the shared-infra deployment.
The token is a Better Auth session for this Rakazo installation, not a model API key, FPL OIDC access token, or Beeper access token. Use a dedicated session; expired/revoked sessions fail visibly and delivery remains queued. Run login again and restart the service to replace a revoked session. Each configured secret env name accepts either its direct value or a corresponding
*_FILEvariable, never both. Secret files must be owner-readable only on Unix. -
Discover agents, explicitly select their IDs, and validate access:
target/release/rakazo-beeper bots --tenant personal # Put selected IDs in bot_ids = ["..."] in config.toml. target/release/rakazo-beeper check target/release/rakazo-beeper runcheckperforms read-only Rakazo checks; it does not create chats or validate every Beeper endpoint.runcreates a ghost and private room for each selected agent, then invites the owner. Accept those room invitations in Beeper and send a text message. Bot discovery is constrained to the token's selected space.bot_ids = []creates no rooms unless another selector is configured. Setauto_bots = trueto keep the orchestrator root and every child conversation (PM) in sync. Useauto_named_bots = ["Muse"]for additional case-insensitive exact-name selections; no bot name is built into the bridge. Explicit IDs are still honored. Archived or removed bots stop being polled on the next tick. Stop the service before changing its configuration. Auto-discovery needs no restart for newly created PMs.
The personal deployment uses an independent user service and native Linux binary:
~/.local/share/rakazo-beeper/
bin/rakazo-beeper
config.toml
secrets/registration-personal.json
secrets/database-key
secrets/rakazo-token
data/bridge.db
~/.config/systemd/user/rakazo-beeper.service
Build on the Linux host with cargo build --release --locked. Install the binary
under bin/, put the personal config and credentials in the layout above, and
copy deploy/rakazo-beeper.service into the user's systemd directory. It reads
credentials from files, binds HTTP on loopback, and writes only the data directory.
mkdir -p ~/.local/share/rakazo-beeper/{bin,secrets,data} ~/.config/systemd/user
chmod 700 ~/.local/share/rakazo-beeper ~/.local/share/rakazo-beeper/secrets
cp target/release/rakazo-beeper ~/.local/share/rakazo-beeper/bin/
cp deploy/rakazo-beeper.service ~/.config/systemd/user/
systemctl --user daemon-reload
systemctl --user enable --now rakazo-beeper.service
systemctl --user status rakazo-beeper.service
journalctl --user -u rakazo-beeper.service -n 50 --no-pager
curl --fail http://127.0.0.1:8080/readyzIf the service must survive logout, enable lingering for the service user through the host's existing administration process. Do not run two instances using the same registration. A replaced Beeper connection exits with status 78, which the supplied systemd unit does not restart automatically.
deploy/compose.yaml builds the same Rust executable. Set BRIDGE_HOME to an
absolute directory containing the config, secrets/, and data/ above. Change
listen to 0.0.0.0:8080 inside the container. The host port stays on loopback.
The container runs as UID/GID 10001; give that UID ownership of this deployment's
secret files and data directory, with the secret files remaining mode 0600.
export BRIDGE_HOME=/absolute/path/to/bridge
docker compose -f deploy/compose.yaml config --quiet
docker compose -f deploy/compose.yaml up --build -d
docker compose -f deploy/compose.yaml logs --tail 50The Compose restart policy is suitable for a single operator-managed instance;
stop it before moving its registration to a different host to avoid reconnect
competition. No public listener, tunnel, or inbound firewall rule is needed in
WebSocket mode. For a standard Matrix homeserver, set websocket = false, provide
the same JSON bundle format, register its application service with the homeserver,
and route its push URL to /tenants/<id>/_matrix/app/v1/transactions/<txn>.
Append [[tenants]] entries to the same config. Each customer requires a separate
Beeper application-service registration, exact owner, Rakazo session, explicit
Rakazo space, and bot allowlist. The operator enrolls customers and deploys the
configuration; there is no unauthenticated tenant-creation API or public signup UI.
New credentials and configuration require a service restart.
Each tenant has an independent worker and outbound Beeper connection. Storage, event deduplication, ghost IDs, message nonces, and cursors include the tenant ID. Changing an existing tenant's owner, upstream endpoints, registration identity, or space is refused against the existing database; create a new tenant ID instead. Separate token rotation retains the existing identity and cursors.
This version uses one SQLite database and one process per installation. A process lock prevents competing workers on the same database. It is appropriate for a bounded, operator-managed hosted pilot. Horizontal replicas, per-tenant quotas, self-service enrollment, automatic credential refresh, billing, and end-to-bridge encryption remain prerequisites for a broad public service. Tenant isolation is an application boundary; the process and its operator can access all configured credentials. Deploy separate instances when a process isolation boundary is needed.
- Incoming transactions and messages commit together before Beeper is acknowledged.
Retries reuse Rakazo
clientNonce, and outgoing Matrix transaction IDs remain stable. These provide retry deduplication under the upstream APIs' retention rules, not an unconditional exactly-once guarantee. - Queued message bodies use XChaCha20-Poly1305 with tenant/event-associated data. Completed queue bodies are removed. Tokens remain in secret files/environment, never in SQLite. Room IDs, agent IDs, tenant IDs, and delivery metadata are stored in plaintext. Back up the database and its encryption key separately.
- Polling waits until a thread has no active run, paginates back to its committed message sequence, and forwards completed bot text in sequence. A snapshot bounds the read so a concurrently starting run cannot be forwarded partially. There is no per-token streaming. Long messages split at UTF-8 boundaries.
- Each delivery checks authoritative room membership and encryption state. Rooms are private, non-federated, and grant the owner message-sending permission while reserving invites and state changes for the ghost.
- Edits, message removals and reaction removals are not synchronized: changing a Beeper message cannot undo what an agent already read or acted on. Clients receive capability metadata disabling edits and removals. Unsupported operations from other clients produce a notice. Send a new message to correct the agent. Tool output and internal reasoning are not forwarded. Questions are text only; approve agent actions in Rakazo. Rich cards are not synchronized. Failed agent runs are visible in Rakazo; there is no dedicated Beeper run-failure notification yet.
/healthzreports process liveness;/readyzchecks recent successful sync and WebSocket connectivity;/metricsexposes aggregate readiness and queue depth. Keep these endpoints private. Readiness stays down after receiving an encrypted event until the room is corrected and the service restarts.- Network/auth failures retry with backoff for queued sends and WebSocket reconnects. Logs omit message bodies, upstream response bodies, and credential values. A failing agent does not prevent other agents being polled, but inbound sends are ordered per tenant: an unresolvable first message holds subsequent sends.
- Before removing an agent from
bot_ids, drain its inbound queue. Otherwise its queued messages remain pending and require restoring the allowlist. Archived or inaccessible agents fail visibly; existing rooms are not silently deleted. - Transaction/event deduplication metadata is retained indefinitely in this pilot. Monitor disk space. Backups should use SQLite's online backup facility or stop the service before copying the database; include the WAL if copying a live DB.
The Rakazo adapter follows upstream
elie222/rakazo at 535de960,
particularly packages/contracts/src/rpc.ts, events.ts, and apps/api/src/app.ts.
The deployed installation may differ; check validates authenticated bot discovery,
and a live round trip is still required before declaring the deployment verified.
Protocol references: oRPC, Matrix application services, Beeper bridge manager, and Beeper as_sync framing. Source annotations appear at the corresponding implementations.