Implementation notes and design caveats surfaced while building the current MOA workspace. These are not necessarily bugs, but they are places where the current trait surface or helper behavior is awkward enough to review before later steps build on top of them.
Caveats are grouped by root cause / architectural boundary, not by the crate where the symptom first appears. Fixing the root of a group typically unblocks every caveat in it.
All three messaging adapters normalize platform-specific callback payloads into text control messages instead of typed gateway events. The fix is the same everywhere: widen PlatformAdapter to emit structured callback events alongside InboundMessage.
crates/moa-gateway/src/telegram.rsreceives Telegram callback queries for approval buttons.- The core
PlatformAdaptertrait only emitsInboundMessage; it does not have a direct signal callback surface forApprovalDecided. - The adapter converts callback payloads into normalized control text such as
/approval allow <request_id>and forwards that as anInboundMessage. - This works without widening the core gateway trait surface and keeps approval payloads compact and testable.
- A future gateway/orchestrator seam may want a richer event type than
InboundMessage.textfor platform-originated control actions.
crates/moa-gateway/src/slack.rsreceives Block Kit button actions over Socket Mode.- The core
PlatformAdaptertrait still only emitsInboundMessage. - The adapter converts approval button clicks into normalized commands such as
/approval deny <request_id>. - Approval actions from Slack and Telegram share the same downstream parsing model.
- If adapters need richer structured callbacks later,
InboundMessage.textshould stop carrying control commands.
crates/moa-gateway/src/approval.rsis the single source of truth for approval callback encoding and default approval buttons.- Telegram, Slack, and Discord all consume that same callback format and button set, with a fallback to text commands when inline buttons are unavailable.
- Approval rendering is consistent across all current messaging adapters.
- The generic gateway surface still has no first-class modal representation.
PlatformCapabilities.supports_modalsis informative today, but the unified approval flow still chooses inline buttons when available and text fallback otherwise.
All three adapters resolve outbound destinations from reply_to and cannot proactively start conversations. The shared fix is an explicit destination field on OutboundMessage or the adapter trait.
OutboundMessagedoes not carry an explicit destination chat or thread.- The Telegram adapter resolves where to send by using
reply_toas an anchor into either a known inbound Telegram message id or a previously sent synthetic gateway message id. - Reply-chain session mapping works for the intended session model, and the adapter can send, edit, and delete multi-part Telegram replies without changing the existing trait.
- The adapter cannot originate a brand-new top-level Telegram conversation without an existing reply anchor.
OutboundMessagestill has no explicit Slack destination.crates/moa-gateway/src/slack.rsresolves channel/thread targets fromreply_to, using either a known inbound Slack message timestamp or a previously sent synthetic gateway message id.- The intended session model works: one MOA session per Slack thread, with replies and edits anchored correctly.
- The adapter cannot proactively open a brand-new channel/thread without a prior inbound anchor.
crates/moa-gateway/src/discord.rsauto-creates a thread the first time the adapter responds to a guild message that is not already in a thread. Direct messages stay in the DM channel, and existing threads are reused.- The "one MOA session per Discord thread" model works for the normal inbound-driven flow.
- Like Telegram and Slack, the Discord adapter relies on
reply_toas its routing anchor and cannot proactively open a new conversation without an inbound message or prior synthetic gateway message id.
Both Telegram and Slack rendering are intentionally minimal. Upgrading either one requires a proper platform-safe formatting layer with escaping and richer markup.
crates/moa-gateway/src/renderer.rsrenders text, tool cards, approvals, status updates, diffs, and code blocks.- Long messages are split at Telegram's 4096-character limit and approval buttons stay on the final chunk.
- Rendering uses plain text plus fenced blocks instead of full Telegram Markdown/HTML parse-mode formatting. This is robust against escaping bugs and message splitting issues.
- If the bot starts carrying heavier user-facing traffic, the next upgrade should be a proper Telegram-safe formatting layer with escaping and richer inline emphasis.
crates/moa-gateway/src/renderer.rssplits Slack output at the 40K text cap and renders approval buttons as Block Kit actions.- Normal text/code/diff output stays text-first, with Block Kit only added when interactive buttons are needed.
- The adapter uses
chat.updatedirectly and advertises a 1-second edit interval, but does not yet coalesce bursts of intermediate status updates into a smarter buffer. - If Slack becomes a primary surface, the next upgrade should add richer per-event thread rendering and more deliberate edit throttling/coalescing.
These caveats are deliberate security trade-offs where the current implementation is good enough for the current threat model but has a known upgrade path for stricter requirements.
crates/moa-security/src/mcp_proxy.rsissues session-scoped opaque tokens and injects real credentials only when a remote MCP call is dispatched.crates/moa-hands/src/mcp.rssupports stdio and remote JSON-RPC transports, with SSE response parsing for remote endpoints.- Remote MCP servers receive credentials without exposing them to the brain or to serialized tool arguments. Session-scoped auth is enforced at the router/proxy seam.
- Stdio MCP auth is still process/env based; the proxy does not inject credentials into subprocess transports. The credential flow is strongest for HTTP/SSE MCP servers and weaker for stdio servers that need secrets at startup.
crates/moa-security/src/vault.rsstores credentials invault.encas an age-encrypted JSON document. The local passphrase is generated automatically on first use and stored beside the vault asvault.keywith0600permissions on Unix.- This keeps credentials encrypted at rest without requiring user setup before the first run.
- This is stronger than plaintext-at-rest, but weaker than OS keychain or external secret-manager storage because the decryption material still lives on the same machine. The swap point is the
CredentialVaulttrait.
crates/moa-hands/src/local.rsstarts Docker sandboxes with read-only root filesystem, tmpfs scratch mounts,cap-drop=ALL,no-new-privileges:true,pids-limit=256, and Docker seccomp active. The implementation uses--network noneto block the cloud metadata endpoint.- This is stricter than the original spec: local Docker sandboxes are fully offline, not just metadata-blocked.
- If we later need outbound network for local containerized tools, we will need a narrower metadata-blocking mechanism than
--network none.
crates/moa-brain/src/harness.rsinjects a per-turn canary into tool-enabled requests. Tool invocations are blocked if they leak the active canary or anymoa_canary_*marker. Tool outputs are wrapped in<untrusted_tool_output>with an explicit instruction not to follow embedded instructions. Suspicious output producesWarningevents.- The instruction hierarchy is materially stronger and regression tests cover both canary leakage and malicious tool-output containment.
- If a model keeps emitting fresh malicious tool calls after seeing the resulting
ToolError/Warning, the retry behavior is still governed by the turn loop rather than a dedicated security circuit breaker. The next seam to tighten is the orchestrator/harness retry policy.
These caveats relate to the gap between "cloud build succeeds" and "cloud deployment is fully self-service."
Dockerfilebuildsmoa-orchestrator-binand installs it as/usr/local/bin/moa-orchestrator.- The orchestrator process reads
POSTGRES_URLdirectly and optionally readsRESTATE_ADMIN_URL,MOA_SANDBOX_DIR,MOA_MEMORY_DIR,MOA_DOCKER_ENABLED, OTLP settings, and metrics settings from the process environment. - Provider registration is environment-backed. Health readiness validates Postgres and optional Restate registration, not provider reachability; real LLM requests still need a configured key such as
OPENAI_API_KEY,ANTHROPIC_API_KEY, orGOOGLE_API_KEY. fly.tomlstill carries sharedMOA__...settings used by config-driven subsystems, but the Restate handler binary's required database setting isPOSTGRES_URL.- In live validation, a manually stopped machine took roughly 6 seconds to answer the next
/healthrequest, while a suspended machine resumed in about 1.29 seconds — close to, but not consistently below, the sub-second resume target from the step spec.