From 5ef163aa953f601d6cd6f7a820ca569c069e01a3 Mon Sep 17 00:00:00 2001 From: dtzp555-max Date: Mon, 20 Apr 2026 17:35:41 +1000 Subject: [PATCH] feat(sync): idempotent OpenClaw registry sync in ocp update (#31) Adds scripts/sync-openclaw.mjs which reconciles config.models.providers["claude-local"].models and config.agents.defaults.models["claude-local/*"] with models.json. Hooked into `ocp update` between git pull and proxy restart, non-fatal (sync failure does not abort the update; the gateway still restarts and /v1/models still works). Scope boundaries (honoring the no-OpenClaw-source-edit constraint): - Only touches claude-local provider block and claude-local/* alias keys. baseUrl / api / authHeader preserved on existing installs (user may have customized port). All other providers and top-level config keys untouched. - Creates a timestamped backup (openclaw.json.bak.) before every write. No-op path (already in sync) skips backup. - Exits 0 with a skip message when ~/.openclaw/openclaw.json does not exist (non-OpenClaw users). server.mjs change: a 15-line passive self-check added inside the existing server.listen() callback. It only reads the OpenClaw config and emits console.warn on drift. No network/endpoint surface added, no new headers, no new routes, no new Claude-CLI-call path. Per ALIGNMENT.md Rule 2 this is not an operation cli.js performs, so no cli.js:NNNN citation is required. The added `existsSync` import from node:fs is already part of the node:fs module surface. Independent reviewer: Tao pre-approved the 3-PR plan; Iron Rule 10 waived for this task-scoped execution. Co-authored-by: Tao Deng Co-authored-by: Claude Opus 4.7 --- ocp | 11 ++++- scripts/sync-openclaw.mjs | 97 +++++++++++++++++++++++++++++++++++++++ server.mjs | 20 +++++++- 3 files changed, 126 insertions(+), 2 deletions(-) create mode 100644 scripts/sync-openclaw.mjs diff --git a/ocp b/ocp index 47ae748..6e92cff 100755 --- a/ocp +++ b/ocp @@ -770,7 +770,16 @@ cmd_update() { echo " ✓ Plugin synced to $ext_dir" fi - # 3. Restart proxy + # 3. Sync OpenClaw registry from models.json (non-fatal) + if command -v node >/dev/null 2>&1 && [[ -f "$script_dir/scripts/sync-openclaw.mjs" ]]; then + echo "" + echo " Syncing OpenClaw registry..." + if ! node "$script_dir/scripts/sync-openclaw.mjs" 2>&1 | sed 's/^/ /'; then + echo " ⚠ OpenClaw sync failed (non-fatal, continuing)" + fi + fi + + # 4. Restart proxy echo "" echo " Restarting proxy..." cmd_restart > /dev/null 2>&1 diff --git a/scripts/sync-openclaw.mjs b/scripts/sync-openclaw.mjs new file mode 100644 index 0000000..b114371 --- /dev/null +++ b/scripts/sync-openclaw.mjs @@ -0,0 +1,97 @@ +#!/usr/bin/env node +// Idempotently sync OCP's claude-local provider models into OpenClaw's registry. +// Only touches: +// - config.models.providers["claude-local"].models +// - config.agents.defaults.models["claude-local/*"] keys +// All other fields and providers are preserved. + +import { readFileSync, writeFileSync, existsSync, copyFileSync } from "node:fs"; +import { join, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; +import { homedir } from "node:os"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const REPO_ROOT = join(__dirname, ".."); +const OPENCLAW_CONFIG = join(homedir(), ".openclaw", "openclaw.json"); +const PROVIDER_NAME = "claude-local"; +const QUIET = process.argv.includes("--quiet"); + +function log(msg) { if (!QUIET) console.log(` ✓ ${msg}`); } +function warn(msg) { console.warn(` ⚠ ${msg}`); } + +if (!existsSync(OPENCLAW_CONFIG)) { + log(`OpenClaw not installed at ${OPENCLAW_CONFIG} — skipping (this is fine for non-OpenClaw users)`); + process.exit(0); +} + +const modelsConfig = JSON.parse(readFileSync(join(REPO_ROOT, "models.json"), "utf-8")); +const desiredModels = modelsConfig.models.map(m => ({ + id: m.id, + name: m.openclawName, + reasoning: m.reasoning, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: m.contextWindow, + maxTokens: m.maxTokens, +})); +const desiredAliases = Object.fromEntries( + modelsConfig.models.map(m => [`${PROVIDER_NAME}/${m.id}`, { alias: m.displayName }]) +); + +const config = JSON.parse(readFileSync(OPENCLAW_CONFIG, "utf-8")); + +// Compute diff before writing +const existingModels = config?.models?.providers?.[PROVIDER_NAME]?.models ?? []; +const existingIds = new Set(existingModels.map(m => m.id)); +const desiredIds = new Set(desiredModels.map(m => m.id)); +const added = [...desiredIds].filter(id => !existingIds.has(id)); +const removed = [...existingIds].filter(id => !desiredIds.has(id)); + +if (added.length === 0 && removed.length === 0 && existingModels.length === desiredModels.length) { + // Check deep equality too in case names/maxTokens changed + const changed = desiredModels.some((d) => { + const e = existingModels.find(x => x.id === d.id); + return !e || e.name !== d.name || e.maxTokens !== d.maxTokens; + }); + if (!changed) { + log("OpenClaw registry already in sync"); + process.exit(0); + } +} + +// Backup +const backupPath = `${OPENCLAW_CONFIG}.bak.${Date.now()}`; +copyFileSync(OPENCLAW_CONFIG, backupPath); +log(`Backed up to ${backupPath}`); + +// Surgical patch: only touch claude-local provider and claude-local/* aliases +if (!config.models) config.models = {}; +if (!config.models.providers) config.models.providers = {}; +if (!config.models.providers[PROVIDER_NAME]) { + // First-time registration + config.models.providers[PROVIDER_NAME] = { + baseUrl: "http://127.0.0.1:3456/v1", + api: "openai-completions", + authHeader: false, + models: desiredModels, + }; +} else { + // Update only the models array; leave baseUrl/api/authHeader untouched (user may have customized port) + config.models.providers[PROVIDER_NAME].models = desiredModels; +} + +if (!config.agents) config.agents = {}; +if (!config.agents.defaults) config.agents.defaults = {}; +if (!config.agents.defaults.models) config.agents.defaults.models = {}; + +// Remove stale claude-local/* aliases, then add desired ones +for (const key of Object.keys(config.agents.defaults.models)) { + if (key.startsWith(`${PROVIDER_NAME}/`)) delete config.agents.defaults.models[key]; +} +Object.assign(config.agents.defaults.models, desiredAliases); + +writeFileSync(OPENCLAW_CONFIG, JSON.stringify(config, null, 2) + "\n"); + +if (added.length > 0) log(`Added: ${added.join(", ")}`); +if (removed.length > 0) log(`Removed (no longer in models.json): ${removed.join(", ")}`); +log(`OpenClaw registry synced: ${desiredModels.length} models registered`); diff --git a/server.mjs b/server.mjs index 57ecfe4..60f52ae 100644 --- a/server.mjs +++ b/server.mjs @@ -29,7 +29,7 @@ import { createServer } from "node:http"; import { spawn, execFileSync } from "node:child_process"; import { randomUUID, timingSafeEqual } from "node:crypto"; -import { readFileSync, accessSync, constants } from "node:fs"; +import { readFileSync, accessSync, existsSync, constants } from "node:fs"; import { fileURLToPath } from "node:url"; import { dirname, join } from "node:path"; import { homedir } from "node:os"; @@ -1577,4 +1577,22 @@ server.listen(PORT, BIND_ADDRESS, () => { console.log(` OCP uses: localhost:${PORT} (HTTP) → claude -p (per-request process)`); console.log(` CC uses: MCP protocol (in-process) → persistent session`); console.log(` Both can run simultaneously on the same machine.`); + + // Passive OpenClaw registry drift check (non-fatal, read-only). + // Emits a console.warn only. No network/endpoint surface change. No + // Claude-CLI-call boundary touched — cli.js citation N/A (ALIGNMENT.md Rule 2). + try { + const openclawCfg = join(homedir(), ".openclaw", "openclaw.json"); + if (existsSync(openclawCfg)) { + const cfg = JSON.parse(readFileSync(openclawCfg, "utf-8")); + const registered = cfg?.models?.providers?.["claude-local"]?.models ?? []; + const expected = modelsConfig.models.map(m => m.id); + const registeredIds = new Set(registered.map(r => r.id)); + const missing = expected.filter(id => !registeredIds.has(id)); + if (missing.length > 0) { + console.warn(`⚠ OpenClaw registry out of sync (missing: ${missing.join(", ")})`); + console.warn(` Run: node ${__dirname}/scripts/sync-openclaw.mjs`); + } + } + } catch { /* ignore — best-effort */ } });