Affected channels: telegram, discord, slack, wechat (any channel registered via MESSAGING_CHANNELS)
Affected versions: Reproduces on main after #3395 landed. #3395 fixed the recovery side (channels start can find the channel afterward) but did not address the build-time disable filter.
Summary
After nemoclaw <sandbox> channels stop <name> followed by a rebuild, the new sandbox image's /opt/nemoclaw/openclaw.json still contains the channel's config, and the bridge starts on next boot. The host-side registry correctly records disabledChannels: [<name>], but the
build-time filter in createSandbox reads from a registry entry that has been wiped earlier in rebuildSandbox, so the filter sees [] and bakes the channel in.
Reproduction
# Onboard a sandbox with at least one messaging channel, e.g. telegram.
nemoclaw onboard --agent openclaw # answer prompts; enable telegram
# Stop the channel.
nemoclaw <sandbox> channels stop telegram
# Answer "y" when asked to rebuild.
# Inspect the new image's openclaw.json.
docker exec <sandbox-container> cat /opt/nemoclaw/openclaw.json | jq .channels
# OR:
docker exec <sandbox-container> grep -i telegram /opt/nemoclaw/openclaw.json
Expected
openclaw.json does not contain the telegram channel; the bridge does not start; gateway logs show no telegram-bridge provider attached.
Actual
openclaw.json still contains the telegram channel config, and the telegram bridge starts on sandbox boot. Host registry has the correct disabledChannels: ["telegram"] and messagingChannels: [..., "telegram"], but those values aren't reflected in the baked image.
Root cause
The wipe-and-rebuild order in rebuildSandbox makes the registry temporarily empty while createSandbox runs:
src/lib/actions/sandbox/rebuild.ts:486 — removeSandboxRegistryEntry(sandboxName) wipes the registry entry (including disabledChannels).
src/lib/onboard.ts:5425 (inside createSandbox) — const disabledChannels = registry.getDisabledChannels(sandboxName) returns [] because the entry no longer exists.
src/lib/onboard.ts:5460 — the messagingTokenDefs filter .filter(({envKey}) => !disabledEnvKeys.has(envKey)) is a no-op (empty disabledEnvKeys).
src/lib/onboard.ts:5902 — activeMessagingChannels still includes the "stopped" channel.
activeMessagingChannels flows into the NEMOCLAW_MESSAGING_CHANNELS_B64 Dockerfile build arg → baked into openclaw.json in the image.
src/lib/actions/sandbox/rebuild.ts:654-663 — preservedRegistryFields later writes disabledChannels back to the registry from the old sb snapshot, so the registry ends up correct. The image does not.
There's no runtime consumer of registry disabledChannels inside the sandbox container — openclaw.json is the source of truth at runtime — so the registry-side preservation alone is insufficient.
Affected channels: telegram, discord, slack, wechat (any channel registered via
MESSAGING_CHANNELS)Affected versions: Reproduces on
mainafter #3395 landed. #3395 fixed the recovery side (channels startcan find the channel afterward) but did not address the build-time disable filter.Summary
After
nemoclaw <sandbox> channels stop <name>followed by a rebuild, the new sandbox image's/opt/nemoclaw/openclaw.jsonstill contains the channel's config, and the bridge starts on next boot. The host-side registry correctly recordsdisabledChannels: [<name>], but thebuild-time filter in
createSandboxreads from a registry entry that has been wiped earlier inrebuildSandbox, so the filter sees[]and bakes the channel in.Reproduction
Expected
openclaw.jsondoes not contain the telegram channel; the bridge does not start; gateway logs show no telegram-bridge provider attached.Actual
openclaw.jsonstill contains the telegram channel config, and the telegram bridge starts on sandbox boot. Host registry has the correctdisabledChannels: ["telegram"]andmessagingChannels: [..., "telegram"], but those values aren't reflected in the baked image.Root cause
The wipe-and-rebuild order in
rebuildSandboxmakes the registry temporarily empty whilecreateSandboxruns:src/lib/actions/sandbox/rebuild.ts:486—removeSandboxRegistryEntry(sandboxName)wipes the registry entry (includingdisabledChannels).src/lib/onboard.ts:5425(insidecreateSandbox) —const disabledChannels = registry.getDisabledChannels(sandboxName)returns[]because the entry no longer exists.src/lib/onboard.ts:5460— the messagingTokenDefs filter.filter(({envKey}) => !disabledEnvKeys.has(envKey))is a no-op (emptydisabledEnvKeys).src/lib/onboard.ts:5902—activeMessagingChannelsstill includes the "stopped" channel.activeMessagingChannelsflows into theNEMOCLAW_MESSAGING_CHANNELS_B64Dockerfile build arg → baked intoopenclaw.jsonin the image.src/lib/actions/sandbox/rebuild.ts:654-663—preservedRegistryFieldslater writesdisabledChannelsback to the registry from the oldsbsnapshot, so the registry ends up correct. The image does not.There's no runtime consumer of registry
disabledChannelsinside the sandbox container —openclaw.jsonis the source of truth at runtime — so the registry-side preservation alone is insufficient.