openclaw-msg9
v0.2.1
Published
OpenClaw msg9.io channel plugin — turn a msg9 agent inbox into an OpenClaw channel (poll-based inbound, reply_to thread closing).
Readme
openclaw-msg9
OpenClaw channel plugin for msg9.io agent inboxes. Turns a msg9 agent inbox into an OpenClaw channel: inbound mail drives the agent, agent replies are delivered back on the original msg9 thread.
- Channel id:
msg9(package / plugin id:openclaw-msg9) - Transport: poll-based inbound (
agent.unread()), direct outbound (agent.send()) - Content: text only — msg9 has no attachments, so no
sendMedia
Configuration
Single account (channels.msg9):
{
channels: {
msg9: {
enabled: true,
address: "[email protected]", // your agent inbox address
apiKey: "msg9_sk_...", // the inbox's agent key — a secret
// baseUrl: "https://api.msg9.io", // optional, this is the default
pollIntervalMs: 3000, // inbox poll interval (default 3000)
allowFrom: ["[email protected]"], // optional sender whitelist
},
},
}Multi-account:
{
channels: {
msg9: {
enabled: true,
address: "[email protected]",
apiKey: "msg9_sk_...",
accounts: {
side: {
address: "[email protected]",
apiKey: "msg9_sk_...",
pollIntervalMs: 1000,
},
},
},
},
}Or via environment variables (default account only):
MSG9_ADDRESS, MSG9_API_KEY, MSG9_BASE_URL.
CLI-style setup tokens follow the same shape as other channels:
--token "address,apiKey[,baseUrl]", or --use-env to read the env vars above.
Addressing
Outbound targets use the msg9: prefix:
msg9:[email protected]normalizeTarget strips the prefix; a bare address containing @ is also
recognized as a msg9 id (<user>@<pod>.msg9.io).
Inbound semantics
Each poll sweep drains the inbox's unread folder:
- Dedupe by
message_id(bounded in-memory set, reset at 1000 entries). The platform is at-least-once; the set covers a gateway run. - allowFrom — when the list is non-empty and the sender is not listed,
the message is
markReadand skipped: it never reaches the agent, but stays in the owner's unprocessed folder for manual review. Empty allowFrom allows everyone — configure it before enabling text commands. - markProcessed — allowed messages are marked processed (implies read, idempotent) before dispatch, so the agent is driven at-most-once even if dispatch or delivery fails.
- Dispatch — the standard channel pipeline
(
activity.record→resolveAgentRoute→ envelope →finalizeInboundContext→dispatchReplyWithBufferedBlockDispatcher). Thedelivercallback sends viaagent.send({ ..., reply_to: <original message_id> }).
reply_to auto-close
Sending a reply with reply_to set makes the server close the original
message automatically. Combined with the mark-processed-first policy, replied
threads are closed exactly once, and messages that never get a reply remain
visible in the unprocessed folder.
Outbound
outbound.sendText resolves the account, strips the msg9: prefix, and calls
agent.send. A core-provided replyToId (the inbound MessageSid) is mapped
to msg9 reply_to. Returns { channel: "msg9", messageId }. There is no
sendMedia — msg9 carries text bodies only.
Credential hygiene
apiKey is a msg9_sk_... agent key: anyone holding it can read and send as
that inbox. Keep it in OpenClaw's config/secret stores with restricted file
permissions (0600), never in a repo or a chat log. If a key leaks in
plaintext, rotate it at the platform before reusing the inbox.
Requirements
- Node.js >= 18
- OpenClaw plugin API
>=2026.3.23-1(built against SDK2026.4.26)
Development
npm install
npm run build # tsc → dist/
npm test # vitest (no network — the client factory is mocked)
npm run pack:dry-runRoadmap
- Webhook inbound — replace polling with tenant-key webhook subscriptions
(
registerHttpRoute+ signature verification) once a webhook is provisioned. - Inbox tools — expose
list_inbox/mark_processedstyle tools to the agent viaregisterTool. - ClawHub publish — publishing pipeline (
openclaw.plugin.jsonalready carries the requiredcompat/buildmetadata; schema is fully flat, no$ref/$defs).
