#!/usr/bin/env node // setup-config.js — Generates all config files for OpenVoiceUI Pinokio install. // Reads API keys from PINOKIO_* environment variables (set by install.js env: block). // This avoids the broken json.set / pinokio-input.json flow entirely. const fs = require("fs"); const path = require("path"); const crypto = require("crypto"); // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- function getKey(envName) { // install.js passes keys as PINOKIO_ via env const v = process.env["PINOKIO_" + envName] || ""; if (!v || v === "undefined" || v === "null" || v.startsWith("{{")) return ""; return v.trim(); } const PORT = getKey("PORT") || "5001"; const token = crypto.randomBytes(24).toString("hex"); const secret = crypto.randomBytes(32).toString("hex"); // --------------------------------------------------------------------------- // 1. Write openclaw.json (nested gateway/agents format for v2026.3.2+) // --------------------------------------------------------------------------- const openclawConfig = { gateway: { mode: "local", port: 18791, bind: "lan", auth: { mode: "token", token: token }, trustedProxies: ["127.0.0.1", "172.16.0.0/12", "10.0.0.0/8"], controlUi: { allowInsecureAuth: true, dangerouslyDisableDeviceAuth: true, }, }, agents: { defaults: { // No model forced here — OpenClaw auto-selects based on available API keys // in auth-profiles.json. User's provided keys drive model selection. thinkingDefault: "off", blockStreamingDefault: "on", blockStreamingBreak: "text_end", timeoutSeconds: 120, memorySearch: { enabled: true }, compaction: { mode: "default", reserveTokens: 80000, keepRecentTokens: 8000, reserveTokensFloor: 80000, memoryFlush: { enabled: true, softThresholdTokens: 6000 }, }, contextPruning: { mode: "cache-ttl", ttl: "30m", keepLastAssistants: 3, softTrimRatio: 0.3, hardClearRatio: 0.5, }, }, list: [{ id: "main", default: true, workspace: "/root/.openclaw/workspace" }], }, // Plugins configured by individual plugin installers }; fs.mkdirSync("openclaw-data/workspace", { recursive: true }); // Mountpoint for the bundled agent skills (docker-compose mounts ./skills here read-only). // Created now so Docker does not create openclaw-data/skills root-owned on Linux, which would // stop you adding your own skills next to it. See docs/SKILLS.md. fs.mkdirSync("openclaw-data/skills/openvoiceui", { recursive: true }); fs.writeFileSync( "openclaw-data/openclaw.json", JSON.stringify(openclawConfig, null, 2) + "\n" ); console.log(" Wrote openclaw-data/openclaw.json"); // --------------------------------------------------------------------------- // 2. Write .env // --------------------------------------------------------------------------- const envLines = [ "# OpenVoiceUI — generated by Pinokio installer", `PORT=${PORT}`, "DOMAIN=localhost", `SECRET_KEY=${secret}`, "", "# OpenClaw Gateway", "CLAWDBOT_GATEWAY_URL=ws://127.0.0.1:18791", `CLAWDBOT_AUTH_TOKEN=${token}`, "GATEWAY_SESSION_KEY=voice-main-1", "", "# AI Provider Keys", ]; const envKeyList = [ "ANTHROPIC_API_KEY", "OPENAI_API_KEY", "GEMINI_API_KEY", "OPENROUTER_API_KEY", "MISTRAL_API_KEY", "XAI_API_KEY", "ZAI_API_KEY", "CEREBRAS_API_KEY", "TOGETHER_API_KEY", "HF_TOKEN", "MOONSHOT_API_KEY", "KIMI_API_KEY", "MINIMAX_API_KEY", "QIANFAN_API_KEY", "MODELSTUDIO_API_KEY", "XIAOMI_API_KEY", "VOLCANO_ENGINE_API_KEY", "BYTEPLUS_API_KEY", "SYNTHETIC_API_KEY", "VENICE_API_KEY", "OPENCODE_ZEN_API_KEY", "KILOCODE_API_KEY", "AI_GATEWAY_API_KEY", "CLOUDFLARE_AI_GATEWAY_API_KEY", "LITELLM_API_KEY", "SUNO_API_KEY", "ELEVENLABS_API_KEY", "ELEVENLABS_VOICE_ID", "RESEMBLE_API_KEY", "RESEMBLE_VOICE_UUID", ]; for (const k of envKeyList) envLines.push(`${k}=${getKey(k)}`); envLines.push( "", "# TTS", `GROQ_API_KEY=${getKey("GROQ_API_KEY")}`, "USE_GROQ=true", "USE_GROQ_TTS=true", "", "# STT", `DEEPGRAM_API_KEY=${getKey("DEEPGRAM_API_KEY")}`, "", "# Supertonic TTS", "SUPERTONIC_API_URL=http://supertonic:8765" ); fs.writeFileSync(".env", envLines.join("\n") + "\n"); console.log(" Wrote .env"); // --------------------------------------------------------------------------- // 3. Write auth-profiles.json (so OpenClaw has working providers on first start) // --------------------------------------------------------------------------- const providerMap = { ANTHROPIC_API_KEY: "anthropic", OPENAI_API_KEY: "openai", GEMINI_API_KEY: "google", OPENROUTER_API_KEY: "openrouter", MISTRAL_API_KEY: "mistral", XAI_API_KEY: "xai", ZAI_API_KEY: "zai", CEREBRAS_API_KEY: "cerebras", TOGETHER_API_KEY: "together", HF_TOKEN: "huggingface", GROQ_API_KEY: "groq", DEEPGRAM_API_KEY: "deepgram", MOONSHOT_API_KEY: "moonshot", KIMI_API_KEY: "kimi-coding", MINIMAX_API_KEY: "minimax", QIANFAN_API_KEY: "qianfan", MODELSTUDIO_API_KEY: "modelstudio", XIAOMI_API_KEY: "xiaomi", VOLCANO_ENGINE_API_KEY: "volcano-engine", BYTEPLUS_API_KEY: "byteplus", SYNTHETIC_API_KEY: "synthetic", VENICE_API_KEY: "venice", OPENCODE_ZEN_API_KEY: "opencode", KILOCODE_API_KEY: "kilocode", AI_GATEWAY_API_KEY: "ai-gateway", CLOUDFLARE_AI_GATEWAY_API_KEY: "cloudflare-ai-gateway", LITELLM_API_KEY: "litellm", SUNO_API_KEY: "suno", ELEVENLABS_API_KEY: "elevenlabs", }; const authProfiles = {}; let keyCount = 0; for (const [envVar, providerId] of Object.entries(providerMap)) { const key = getKey(envVar); if (key) { authProfiles[providerId] = [ { id: "default", type: "api_key", key, lastUsed: null, disabled: false, cooldown: null, }, ]; keyCount++; } } const agentDir = "openclaw-data/agents/main/agent"; fs.mkdirSync(agentDir, { recursive: true }); fs.writeFileSync( path.join(agentDir, "auth-profiles.json"), JSON.stringify(authProfiles, null, 2) + "\n" ); console.log(` Wrote auth-profiles.json (${keyCount} provider(s))`); // --------------------------------------------------------------------------- // 4. Pre-generate device identity and pre-pair it // --------------------------------------------------------------------------- // OpenClaw requires device pairing for WebSocket connections. Even with // dangerouslyDisableDeviceAuth:true, the gateway still requires pairing for // the WS protocol (that flag only affects the control UI). // // Fix: generate an Ed25519 keypair at install time, register the public key // in OpenClaw's devices/paired.json, and save the full identity so start.js // can inject it into the OpenVoiceUI container via docker exec. // Generate Ed25519 keypair const { publicKey, privateKey } = crypto.generateKeyPairSync("ed25519"); // Raw 32-byte public key — extract via JWK.x (most reliable cross-platform) const devJwk = publicKey.export({ format: "jwk" }); const rawPub = Buffer.from(devJwk.x, "base64url"); // deviceId = SHA256(raw public key) — matches Python's hashlib.sha256(raw_pub).hexdigest() const deviceId = crypto.createHash("sha256").update(rawPub).digest("hex"); // PEM formats — OpenVoiceUI Python client uses PEM for signing const pubPem = publicKey.export({ type: "spki", format: "pem" }); const privPem = privateKey.export({ type: "pkcs8", format: "pem" }); // base64url of raw Ed25519 bytes — this is what the gateway compares during WS handshake const pubB64url = rawPub.toString("base64url"); // Save full identity for injection into OpenVoiceUI container at start time // (Python client reads PEM format for Ed25519 signing) const deviceIdentity = { deviceId: deviceId, publicKeyPem: pubPem, privateKeyPem: privPem, }; fs.writeFileSync( "openclaw-data/pre-paired-device.json", JSON.stringify(deviceIdentity, null, 2) + "\n" ); console.log(` Generated device identity: ${deviceId.slice(0, 16)}...`); // Pre-register in OpenClaw's devices/paired.json so it accepts this device. // Format must match what approveDevicePairing() writes: // - publicKey: base64url of raw Ed25519 bytes (NOT PEM — gateway compares literally) // - role/roles/scopes/approvedScopes: authorization metadata // - tokens: per-role auth tokens for verifyDeviceToken() calls fs.mkdirSync("openclaw-data/devices", { recursive: true }); const nowMs = Date.now(); const pairingToken = crypto.randomBytes(32).toString("hex"); const pairedDevices = {}; pairedDevices[deviceId] = { deviceId: deviceId, publicKey: pubB64url, displayName: "pinokio-openvoiceui", platform: "linux", clientId: "cli", clientMode: "cli", role: "operator", roles: ["operator"], scopes: ["operator.admin", "operator.read", "operator.write"], approvedScopes: ["operator.admin", "operator.read", "operator.write"], tokens: { operator: { token: pairingToken, role: "operator", scopes: ["operator.admin", "operator.read", "operator.write"], createdAtMs: nowMs, }, }, createdAtMs: nowMs, approvedAtMs: nowMs, }; fs.writeFileSync( "openclaw-data/devices/paired.json", JSON.stringify(pairedDevices, null, 2) + "\n" ); // Clear pending.json — stale entries with silent:false permanently block auto-approval fs.writeFileSync("openclaw-data/devices/pending.json", "{}\n"); console.log(" Wrote devices/paired.json (pre-paired)"); // --------------------------------------------------------------------------- // Done // --------------------------------------------------------------------------- console.log("\n Configuration complete!\n");