export const telegramManifest: ChannelManifest = {
id: "telegram",
displayName: "Telegram",
supportedAgents: ["openclaw", "hermes"],
auth: {
kind: "static-token",
},
inputs: [
{
id: "botToken",
kind: "secret",
required: true,
inputEnv: {
canonical: "NEMOCLAW_TELEGRAM_BOT_TOKEN",
legacy: ["TELEGRAM_BOT_TOKEN"],
},
providerEnvKey: "TELEGRAM_BOT_TOKEN",
prompt: {
label: "Telegram Bot Token",
help: "Create a bot via @BotFather on Telegram, then copy the token.",
},
},
{
id: "allowedIds",
kind: "config",
required: false,
inputEnv: {
canonical: "NEMOCLAW_TELEGRAM_ALLOWED_IDS",
legacy: ["TELEGRAM_ALLOWED_IDS"],
},
prompt: {
label: "Telegram User ID (for DM access)",
help: "Send /start to @userinfobot on Telegram to get your numeric user ID.",
},
statePath: "allowedIds.telegram",
},
{
id: "requireMention",
kind: "config",
required: false,
inputEnv: {
canonical: "NEMOCLAW_TELEGRAM_REQUIRE_MENTION",
legacy: ["TELEGRAM_REQUIRE_MENTION"],
},
validValues: ["0", "1"],
statePath: "telegramConfig.requireMention",
},
],
credentials: [
{
id: "telegramBotToken",
sourceInput: "botToken",
providerName: "{sandboxName}-telegram-bridge",
providerEnvKey: "TELEGRAM_BOT_TOKEN",
placeholder: "openshell:resolve:env:TELEGRAM_BOT_TOKEN",
},
],
policy: {
presets: ["telegram"],
requiredForActivation: true,
},
render: [
{
agent: "openclaw",
target: "openclaw.json",
fragment: {
path: "channels.telegram.accounts.default",
value: {
botToken: "{{credential.telegramBotToken.placeholder}}",
enabled: true,
healthMonitor: { enabled: false },
proxy: "{{proxyUrl}}",
groupPolicy: "{{telegramConfig.groupPolicy}}",
dmPolicy: "{{allowedIds.telegram.dmPolicy}}",
allowFrom: "{{allowedIds.telegram.values}}",
},
},
},
{
agent: "hermes",
target: "~/.hermes/.env",
lines: [
"TELEGRAM_BOT_TOKEN={{credential.telegramBotToken.placeholder}}",
"TELEGRAM_ALLOWED_USERS={{allowedIds.telegram.csv}}",
],
},
],
state: {
persist: {
telegramConfig: ["requireMention"],
allowedIds: ["allowedIds"],
},
rebuildHydration: [
{
statePath: "telegramConfig.requireMention",
env: "TELEGRAM_REQUIRE_MENTION",
},
{
statePath: "allowedIds.telegram",
env: "TELEGRAM_ALLOWED_IDS",
},
],
},
hooks: [],
};
export const wechatManifest: ChannelManifest = {
id: "wechat",
displayName: "WeChat",
supportedAgents: ["openclaw", "hermes"],
auth: {
kind: "static-token",
acquisition: "host-qr",
},
inputs: [
{
id: "botToken",
kind: "secret",
required: true,
inputEnv: {
canonical: "NEMOCLAW_WECHAT_BOT_TOKEN",
legacy: ["WECHAT_BOT_TOKEN"],
},
providerEnvKey: "WECHAT_BOT_TOKEN",
},
{
id: "accountId",
kind: "config",
required: true,
inputEnv: {
canonical: "NEMOCLAW_WECHAT_ACCOUNT_ID",
legacy: ["WECHAT_ACCOUNT_ID"],
},
statePath: "wechatConfig.accountId",
},
{
id: "baseUrl",
kind: "config",
required: false,
inputEnv: {
canonical: "NEMOCLAW_WECHAT_BASE_URL",
legacy: ["WECHAT_BASE_URL"],
},
statePath: "wechatConfig.baseUrl",
},
{
id: "userId",
kind: "config",
required: false,
inputEnv: {
canonical: "NEMOCLAW_WECHAT_USER_ID",
legacy: ["WECHAT_USER_ID"],
},
statePath: "wechatConfig.userId",
},
{
id: "allowedIds",
kind: "config",
required: false,
inputEnv: {
canonical: "NEMOCLAW_WECHAT_ALLOWED_IDS",
legacy: ["WECHAT_ALLOWED_IDS"],
},
statePath: "allowedIds.wechat",
},
],
credentials: [
{
id: "wechatBotToken",
sourceInput: "botToken",
providerName: "{sandboxName}-wechat-bridge",
providerEnvKey: "WECHAT_BOT_TOKEN",
placeholder: "openshell:resolve:env:WECHAT_BOT_TOKEN",
},
],
policy: {
presets: ["wechat"],
requiredForActivation: true,
},
render: [
{
agent: "hermes",
target: "~/.hermes/.env",
lines: [
"WEIXIN_TOKEN={{credential.wechatBotToken.placeholder}}",
"WEIXIN_ACCOUNT_ID={{wechatConfig.accountId}}",
"WEIXIN_BASE_URL={{wechatConfig.baseUrl}}",
"WEIXIN_ALLOWED_USERS={{allowedIds.wechat.csv}}",
],
},
],
state: {
persist: {
wechatConfig: ["accountId", "baseUrl", "userId"],
allowedIds: ["allowedIds"],
},
rebuildHydration: [
{
statePath: "wechatConfig.accountId",
env: "WECHAT_ACCOUNT_ID",
},
{
statePath: "wechatConfig.baseUrl",
env: "WECHAT_BASE_URL",
},
{
statePath: "wechatConfig.userId",
env: "WECHAT_USER_ID",
},
{
statePath: "allowedIds.wechat",
env: "WECHAT_ALLOWED_IDS",
},
],
},
hooks: [
{
id: "wechat-host-qr",
phase: "enroll",
handler: "wechat.ilinkLogin",
outputs: [
{ id: "botToken", kind: "secret", required: true },
{ id: "accountId", kind: "config", required: true },
{ id: "baseUrl", kind: "config" },
{ id: "userId", kind: "config" },
],
onFailure: "skip-channel",
},
{
id: "wechat-seed-openclaw-account",
phase: "post-agent-install",
handler: "wechat.seedOpenClawAccount",
inputs: [
"wechatConfig.accountId",
"wechatConfig.baseUrl",
"wechatConfig.userId",
"credential.wechatBotToken.placeholder",
],
outputs: [
{
id: "openclawWeixinAccountsIndex",
kind: "build-file",
required: true,
},
{
id: "openclawWeixinAccountFile",
kind: "build-file",
required: true,
},
{
id: "openclawChannelPatch",
kind: "build-file",
required: true,
},
],
onFailure: "abort",
},
],
};
The compiler should only see hook declarations and handler IDs. It should not import WeChat code directly.
Summary
Redesign NemoClaw messaging integrations around a manifest-first planning architecture.
Today, channel behavior is spread across onboarding, sandbox actions, policy logic, Dockerfile patching, config rendering, rebuild handling, and channel-specific scripts. This makes messaging channels hard to reason about, difficult to test in isolation, and expensive to extend.
The proposed architecture makes each channel a typed, mostly serializable
ChannelManifest. Shared engines compile selected manifests into a serializableSandboxMessagingPlan, and core CLI workflows apply that plan through a dedicated side-effect boundary.Target flow:
Class Diagram
classDiagram direction LR class ChannelManifest { +id +displayName +supportedAgents +auth +inputs +credentials +policy +render +state +hooks } class ChannelManifestRegistry { +register(manifest) +listAvailable(ctx) +get(channelId) } class MessagingWorkflowPlanner { +buildPlan(ctx) +buildChannelAddPlanFromSandboxEntry(ctx) +buildChannelStopPlanFromSandboxEntry(ctx) +buildChannelStartPlanFromSandboxEntry(ctx) +buildChannelRemovePlanFromSandboxEntry(ctx) +buildRebuildPlanFromSandboxEntry(ctx) } class ManifestCompiler { +compile(manifest, ctx) } class EnrollmentInputResolver { +resolveInputs(manifest, ctx) } class CredentialBindingEngine { +createBindings(credentials, ctx) } class PolicyResolver { +resolve(policySpec, ctx) } class AgentRenderEngine { +render(renderSpec, ctx) } class HookRunner { +runEnrollmentHooks(hooks, ctx) +planHooks(hooks, ctx) } class SandboxMessagingPlan { +channels +disabledChannels +credentialBindings +networkPolicy +agentRender +buildSteps +stateUpdates +healthChecks } class SandboxEntry { +messagingChannels +disabledChannels +messaging.plan } class MessagingSetupApplier { +apply(plan) +writePlanToEnv(plan) } class MessagingHostStateApplier { +applyPlanToRegistry(sandboxName, plan) +readPlanStateFromEnv() } class TelegramManifest class DiscordManifest class SlackManifest class WeChatManifest ChannelManifestRegistry o-- ChannelManifest : contains MessagingWorkflowPlanner --> ChannelManifestRegistry : discovers MessagingWorkflowPlanner --> ManifestCompiler : compiles add/onboard MessagingWorkflowPlanner --> SandboxEntry : loads stored plan MessagingWorkflowPlanner --> SandboxMessagingPlan : produces or mutates ManifestCompiler --> EnrollmentInputResolver : resolves inputs ManifestCompiler --> CredentialBindingEngine : creates bindings ManifestCompiler --> PolicyResolver : resolves policy ManifestCompiler --> AgentRenderEngine : renders config ManifestCompiler --> HookRunner : runs/schedules hooks MessagingSetupApplier --> SandboxMessagingPlan : applies MessagingHostStateApplier --> SandboxEntry : stores plan MessagingHostStateApplier --> SandboxMessagingPlan : persists TelegramManifest ..|> ChannelManifest DiscordManifest ..|> ChannelManifest SlackManifest ..|> ChannelManifest WeChatManifest ..|> ChannelManifestProposed Architecture
ChannelManifestshould become the declarative source of truth for one messaging channel.A manifest defines:
Simple channels such as Telegram, Discord, and Slack should mostly be manifest data.
WeChat should also be manifest-driven, but its exceptional behavior should move behind hooks, such as:
The manifest should remain a serializable declaration. It should not contain imported implementations or inline functions. It may reference hook handlers by stable IDs.
MessagingWorkflowPlannercoordinates workflow-level planning for:The planner should not contain channel-specific behavior. It should discover applicable manifests, compile selected channels, aggregate channel plans into one
SandboxMessagingPlan, and expose that plan for tests, dry-run output, shadow-mode comparison, and application.Generic compile flow:
ManifestCompilershould not contain branches likeif channel === "wechat"orif channel === "slack". Channel variation should live in manifest data and hook specs.SandboxMessagingPlanshould be serializable and inspectable before any mutation happens.MessagingSetupAppliershould be the only layer that performs real side effects:The applier should execute the plan from the relevant workflow phase, including image-build preparation and post-create sandbox updates. It should not reinterpret manifests or duplicate channel logic.
Migration Plan
Build the manifest foundation beside the current implementation.
Add the
messagingmodule with manifest types, channel registry, shared hook registry, and initial channel manifests. Keep this layer isolated until individual responsibilities are ready to replace legacy code.Migrate enrollment first.
Move channel selection, input resolution, token prompts, host-QR enrollment, credential binding intent, and persisted channel config into the manifest-driven path. Concrete channel behavior should come from the channel manifest, with exceptional behavior such as WeChat QR
login handled through registered hooks.
Migrate policy planning and application.
Move channel policy requirements into manifests and have the planner produce policy actions for add/remove/start/stop/rebuild/onboard. Replace channel-name policy branches with
PolicyResolverandMessagingSetupApplierexecution.Migrate credential/provider binding.
Move provider names, env keys, placeholders, attach/detach/delete intent, and credential rotation metadata into manifest-driven credential plans. Preserve current externally visible provider names and placeholder behavior.
Migrate agent rendering and build inputs.
Move OpenClaw JSON fragments, Hermes env lines, Docker build args, rebuild hydration, and WeChat seed-file scheduling into the manifest render/state/hook model. Invoke the setup applier during image-build preparation so build-time config and generated files are available before the sandbox starts.
Adopt full workflow planning.
Once enrollment, policy, credentials, and rendering are manifest-driven, switch workflows to use
MessagingWorkflowPlanner -> SandboxMessagingPlan -> MessagingSetupApplierdirectly for channel lifecycle, rebuild, and onboarding.Remove legacy scattered channel logic.
Delete duplicated messaging behavior from onboarding, policy-channel actions, Dockerfile patching, config generation, Hermes config generation, and WeChat seed scripts after the corresponding responsibility has moved to the new architecture.
Proposed module layout
Each concrete channel should live in its own folder. The shared
hooks/module defines hook interfaces, hook registration, and generic hook execution. Channel-specific hook implementations live beside the channel that owns them.Manifest examples
Telegram
WeChat
WeChat-specific code should be registered separately through the hook registry:
The compiler should only see hook declarations and handler IDs. It should not import WeChat code directly.
Non-goals
Acceptance criteria
This work is ready when:
ChannelManifestSandboxMessagingPlanMessagingWorkflowPlanner -> SandboxMessagingPlan -> MessagingSetupApplier