diff --git a/Dockerfile b/Dockerfile index adef7f1d4..2c5430a6d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -55,6 +55,10 @@ ARG NEMOCLAW_INFERENCE_BASE_URL=https://inference.local/v1 ARG NEMOCLAW_INFERENCE_API=openai-completions ARG NEMOCLAW_INFERENCE_COMPAT_B64=e30= ARG NEMOCLAW_WEB_CONFIG_B64=e30= +# Base64-encoded JSON list of messaging channel names to pre-configure +# (e.g. ["discord","telegram"]). Channels are added with placeholder tokens +# so the L7 proxy can rewrite them at egress. Default: empty list. +ARG NEMOCLAW_MESSAGING_CHANNELS_B64=W10= # Set to "1" to disable device-pairing auth (development/headless only). # Default: "0" (device auth enabled — secure by default). ARG NEMOCLAW_DISABLE_DEVICE_AUTH=0 @@ -73,6 +77,7 @@ ENV NEMOCLAW_MODEL=${NEMOCLAW_MODEL} \ NEMOCLAW_INFERENCE_API=${NEMOCLAW_INFERENCE_API} \ NEMOCLAW_INFERENCE_COMPAT_B64=${NEMOCLAW_INFERENCE_COMPAT_B64} \ NEMOCLAW_WEB_CONFIG_B64=${NEMOCLAW_WEB_CONFIG_B64} \ + NEMOCLAW_MESSAGING_CHANNELS_B64=${NEMOCLAW_MESSAGING_CHANNELS_B64} \ NEMOCLAW_DISABLE_DEVICE_AUTH=${NEMOCLAW_DISABLE_DEVICE_AUTH} WORKDIR /sandbox @@ -94,6 +99,10 @@ inference_base_url = os.environ['NEMOCLAW_INFERENCE_BASE_URL']; \ inference_api = os.environ['NEMOCLAW_INFERENCE_API']; \ inference_compat = json.loads(base64.b64decode(os.environ['NEMOCLAW_INFERENCE_COMPAT_B64']).decode('utf-8')); \ web_config = json.loads(base64.b64decode(os.environ.get('NEMOCLAW_WEB_CONFIG_B64', 'e30=') or 'e30=').decode('utf-8')); \ +msg_channels = json.loads(base64.b64decode(os.environ.get('NEMOCLAW_MESSAGING_CHANNELS_B64', 'W10=') or 'W10=').decode('utf-8')); \ +_token_keys = {'discord': 'token', 'telegram': 'botToken', 'slack': 'botToken'}; \ +_env_keys = {'discord': 'DISCORD_BOT_TOKEN', 'telegram': 'TELEGRAM_BOT_TOKEN', 'slack': 'SLACK_BOT_TOKEN'}; \ +_ch_cfg = {ch: {'accounts': {'main': {_token_keys[ch]: f'openshell:resolve:env:{_env_keys[ch]}', 'enabled': True}}} for ch in msg_channels if ch in _token_keys}; \ parsed = urlparse(chat_ui_url); \ chat_origin = f'{parsed.scheme}://{parsed.netloc}' if parsed.scheme and parsed.netloc else 'http://127.0.0.1:18789'; \ origins = ['http://127.0.0.1:18789']; \ @@ -111,7 +120,7 @@ providers = { \ config = { \ 'agents': {'defaults': {'model': {'primary': primary_model_ref}}}, \ 'models': {'mode': 'merge', 'providers': providers}, \ - 'channels': {'defaults': {'configWrites': False}}, \ + 'channels': dict({'defaults': {'configWrites': False}}, **_ch_cfg), \ 'gateway': { \ 'mode': 'local', \ 'controlUi': { \ diff --git a/bin/lib/onboard.js b/bin/lib/onboard.js index 78a3e7fc1..e9581d652 100644 --- a/bin/lib/onboard.js +++ b/bin/lib/onboard.js @@ -1233,6 +1233,7 @@ function patchStagedDockerfile( provider = null, preferredInferenceApi = null, webSearchConfig = null, + messagingChannels = [], ) { const { providerKey, primaryModelRef, inferenceBaseUrl, inferenceApi, inferenceCompat } = getSandboxInferenceConfig(model, provider, preferredInferenceApi); @@ -1276,6 +1277,12 @@ function patchStagedDockerfile( /^ARG NEMOCLAW_DISABLE_DEVICE_AUTH=.*$/m, `ARG NEMOCLAW_DISABLE_DEVICE_AUTH=1`, ); + if (messagingChannels.length > 0) { + dockerfile = dockerfile.replace( + /^ARG NEMOCLAW_MESSAGING_CHANNELS_B64=.*$/m, + `ARG NEMOCLAW_MESSAGING_CHANNELS_B64=${encodeDockerJsonArg(messagingChannels)}`, + ); + } fs.writeFileSync(dockerfilePath, dockerfile); } @@ -2598,6 +2605,15 @@ async function createSandbox( ); process.exit(1); } + const activeMessagingChannels = messagingTokenDefs + .filter(({ token }) => !!token) + .map(({ envKey }) => { + if (envKey === "DISCORD_BOT_TOKEN") return "discord"; + if (envKey === "SLACK_BOT_TOKEN") return "slack"; + if (envKey === "TELEGRAM_BOT_TOKEN") return "telegram"; + return null; + }) + .filter(Boolean); patchStagedDockerfile( stagedDockerfile, model, @@ -2606,6 +2622,7 @@ async function createSandbox( provider, preferredInferenceApi, webSearchConfig, + activeMessagingChannels, ); // Only pass non-sensitive env vars to the sandbox. Credentials flow through // OpenShell providers — the gateway injects them as placeholders and the L7 diff --git a/scripts/nemoclaw-start.sh b/scripts/nemoclaw-start.sh index cd039e5c6..5d27352be 100755 --- a/scripts/nemoclaw-start.sh +++ b/scripts/nemoclaw-start.sh @@ -145,12 +145,14 @@ configure_messaging_channels() { # Real tokens are never visible inside the sandbox. # # Requires root: openclaw.json is owned by root with chmod 444. - # Non-root mode cannot patch the config — channels are unavailable. + # Non-root mode relies on channels being pre-baked into openclaw.json + # at build time via NEMOCLAW_MESSAGING_CHANNELS_B64. [ -n "${TELEGRAM_BOT_TOKEN:-}" ] || [ -n "${DISCORD_BOT_TOKEN:-}" ] || [ -n "${SLACK_BOT_TOKEN:-}" ] || return 0 if [ "$(id -u)" -ne 0 ]; then - echo "[channels] Messaging tokens detected but running as non-root — skipping openclaw.json patch" >&2 - echo "[channels] Channels still work via L7 proxy token rewriting (no config patch needed)" >&2 + echo "[channels] Messaging tokens detected (non-root mode)" >&2 + echo "[channels] Channel entries should be baked into openclaw.json at build time" >&2 + echo "[channels] (NEMOCLAW_MESSAGING_CHANNELS_B64). L7 proxy rewrites placeholder tokens at egress." >&2 return 0 fi