fix(setup): decouple OpenClaw config patch from being mandatory (#73)

OCP markets itself as a standalone OpenAI-compatible proxy with six
supported IDE clients (Cline / Cursor / Continue.dev / OpenCode / Aider /
OpenClaw). The README §Server Setup says "node setup.mjs" is the
installer entrypoint. But on a truly fresh box without OpenClaw
installed, setup.mjs hard-fails at line 111:

    if (!existsSync(CONFIG_PATH)) fail(`OpenClaw config not found...`);

This contradicts the standalone-proxy stance and was caught during
PR #71 dogfood testing on Pi231.

Root cause: the OpenClaw config patch (lines 110-148) was baked in as
required because OCP started life as an OpenClaw-internal helper. The
project has since rebranded to standalone OCP without decoupling the
installer.

Fix: gate the OpenClaw config patch (Step 2) and auth-profiles patch
(Step 3) on existsSync(CONFIG_PATH). When OpenClaw is present, behavior
is byte-for-byte identical to current main (verified via dry-run hash
diff against ~/.openclaw/openclaw.json — UNCHANGED). When OpenClaw is
absent, the installer logs a graceful warning, skips both patch
sections, and continues to start.sh / launchd-plist / systemd-unit
creation as before. The summary banner is also conditional: when
OpenClaw is absent, it points the user at README § "Client Setup"
instead of giving openclaw.json edit instructions.

Also moves `import { readdirSync }` from mid-file (line 178, post-use)
to the top-level imports block; this was a latent ESM-hoisting quirk
that worked but is now syntactically required at the top because the
readdirSync call moved inside an `if` block.

Out of scope (Iron Rule 11, single-layer PR): server.mjs, models.json,
scripts/sync-openclaw.mjs, README.md, ALIGNMENT.md, package.json
version, the duplicate-spawn / health-verify logic at line 268+ (that's
a separate fix/setup-spawn-conflict PR).

Mac mini production unaffected: ~/.openclaw/openclaw.json already
exists there, so the OpenClaw-present path is preserved. `ocp update`
doesn't invoke setup.mjs, so running services are not touched.

Smoke-tested locally:
- Path 1 (OpenClaw present): dry-run output functionally identical to
  current main; config sha256 unchanged after run.
- Path 2 (OpenClaw absent, OPENCLAW_STATE_DIR=/tmp/no-such-dir-*):
  graceful warn, both patch sections skipped, banner shows
  standalone-mode message, dry-run completes successfully.
- npm test: 43/43 pass.

Co-authored-by: dtzp555 <dtzp555@gmail.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
dtzp555-max
2026-05-08 15:03:27 +10:00
committed by GitHub
co-authored by taodeng Claude Opus 4.7
parent 4b01d4e768
commit 49baffe2da
+138 -117
View File
@@ -12,7 +12,7 @@
* 4. Creates start.sh for easy launch * 4. Creates start.sh for easy launch
* 5. Optionally starts the proxy * 5. Optionally starts the proxy
*/ */
import { readFileSync, writeFileSync, existsSync, mkdirSync, unlinkSync } from "node:fs"; import { readFileSync, writeFileSync, existsSync, mkdirSync, unlinkSync, readdirSync } from "node:fs";
import { execSync } from "node:child_process"; import { execSync } from "node:child_process";
import { join, dirname } from "node:path"; import { join, dirname } from "node:path";
import { homedir } from "node:os"; import { homedir } from "node:os";
@@ -107,106 +107,112 @@ try {
warn("Make sure you're logged in: claude login"); warn("Make sure you're logged in: claude login");
} }
// Check openclaw config // Check openclaw config (optional — OCP runs standalone without OpenClaw)
if (!existsSync(CONFIG_PATH)) fail(`OpenClaw config not found at ${CONFIG_PATH}`); const OPENCLAW_PRESENT = existsSync(CONFIG_PATH);
log(`OpenClaw config: ${CONFIG_PATH}`); if (OPENCLAW_PRESENT) {
log(`OpenClaw config: ${CONFIG_PATH}`);
} else {
warn(`OpenClaw not detected at ${CONFIG_PATH} — skipping OpenClaw integration.`);
warn(`To register OCP with OpenClaw later, install OpenClaw and re-run \`node setup.mjs\`,`);
warn(`or run \`ocp update\` if OpenClaw is installed afterward.`);
}
// ── Step 2: Patch openclaw.json ───────────────────────────────────────── // ── Step 2: Patch openclaw.json ─────────────────────────────────────────
console.log("\n📝 Configuring OpenClaw...\n"); if (OPENCLAW_PRESENT) {
console.log("\n📝 Configuring OpenClaw...\n");
const config = readJSON(CONFIG_PATH); const config = readJSON(CONFIG_PATH);
// Ensure models.providers exists // Ensure models.providers exists
if (!config.models) config.models = {}; if (!config.models) config.models = {};
if (!config.models.providers) config.models.providers = {}; if (!config.models.providers) config.models.providers = {};
// Add/update claude-local provider // Add/update claude-local provider
config.models.providers[PROVIDER_NAME] = { config.models.providers[PROVIDER_NAME] = {
baseUrl: `http://127.0.0.1:${PORT}/v1`, baseUrl: `http://127.0.0.1:${PORT}/v1`,
api: "openai-completions", api: "openai-completions",
authHeader: false, authHeader: false,
models: MODELS, models: MODELS,
}; };
log(`Provider "${PROVIDER_NAME}" → http://127.0.0.1:${PORT}/v1`); log(`Provider "${PROVIDER_NAME}" → http://127.0.0.1:${PORT}/v1`);
// Ensure auth profile in config // Ensure auth profile in config
if (!config.auth) config.auth = {}; if (!config.auth) config.auth = {};
if (!config.auth.profiles) config.auth.profiles = {}; if (!config.auth.profiles) config.auth.profiles = {};
config.auth.profiles[`${PROVIDER_NAME}:default`] = { config.auth.profiles[`${PROVIDER_NAME}:default`] = {
provider: PROVIDER_NAME, provider: PROVIDER_NAME,
mode: "api_key", mode: "api_key",
}; };
log(`Auth profile "${PROVIDER_NAME}:default" registered`); log(`Auth profile "${PROVIDER_NAME}:default" registered`);
// Add models to agents.defaults.models // Add models to agents.defaults.models
if (!config.agents) config.agents = {}; if (!config.agents) config.agents = {};
if (!config.agents.defaults) config.agents.defaults = {}; if (!config.agents.defaults) config.agents.defaults = {};
if (!config.agents.defaults.models) config.agents.defaults.models = {}; if (!config.agents.defaults.models) config.agents.defaults.models = {};
for (const [key, val] of Object.entries(MODEL_ALIASES)) { for (const [key, val] of Object.entries(MODEL_ALIASES)) {
config.agents.defaults.models[key] = val; config.agents.defaults.models[key] = val;
}
log(`Model aliases added to agents.defaults.models`);
// Set idleTimeoutSeconds to 0 — critical for Claude tool-use.
// When Claude calls tools (Bash, Read, etc.), the token stream pauses for 30-120s.
// OpenClaw's default idleTimeoutSeconds (60s) kills the connection mid-tool-call,
// causing exit 143 (SIGTERM) and stuck sessions. Setting to 0 disables the idle timer.
if (!config.agents.defaults.llm) config.agents.defaults.llm = {};
if (config.agents.defaults.llm.idleTimeoutSeconds === undefined ||
config.agents.defaults.llm.idleTimeoutSeconds > 0) {
config.agents.defaults.llm.idleTimeoutSeconds = 0;
log(`Set agents.defaults.llm.idleTimeoutSeconds = 0 (prevents tool-call timeouts)`);
} else {
log(`idleTimeoutSeconds already configured: ${config.agents.defaults.llm.idleTimeoutSeconds}`);
}
writeJSON(CONFIG_PATH, config);
log(`Config saved`);
// ── Step 3: Patch auth-profiles.json ────────────────────────────────────
console.log("\n🔑 Configuring auth profiles...\n");
// Find all agent auth-profiles.json files
const agentsDir = join(OPENCLAW_DIR, "agents");
const agentDirs = existsSync(agentsDir)
? readdirSync(agentsDir).filter((d) => {
const ap = join(agentsDir, d, "agent", "auth-profiles.json");
return existsSync(ap);
})
: [];
import { readdirSync } from "node:fs";
for (const agentId of agentDirs) {
const apPath = join(agentsDir, agentId, "agent", "auth-profiles.json");
try {
const ap = readJSON(apPath);
if (!ap.profiles) ap.profiles = {};
// Add claude-local profile if missing
if (!ap.profiles[`${PROVIDER_NAME}:default`]) {
ap.profiles[`${PROVIDER_NAME}:default`] = {
type: "api_key",
provider: PROVIDER_NAME,
key: "local-proxy-no-auth",
};
}
// Add to lastGood if missing
if (!ap.lastGood) ap.lastGood = {};
if (!ap.lastGood[PROVIDER_NAME]) {
ap.lastGood[PROVIDER_NAME] = `${PROVIDER_NAME}:default`;
}
writeJSON(apPath, ap);
log(`Agent "${agentId}" auth profile updated`);
} catch (e) {
warn(`Skipped agent "${agentId}": ${e.message}`);
} }
} log(`Model aliases added to agents.defaults.models`);
if (agentDirs.length === 0) { // Set idleTimeoutSeconds to 0 — critical for Claude tool-use.
warn("No agent auth-profiles.json found — you may need to restart the gateway first"); // When Claude calls tools (Bash, Read, etc.), the token stream pauses for 30-120s.
// OpenClaw's default idleTimeoutSeconds (60s) kills the connection mid-tool-call,
// causing exit 143 (SIGTERM) and stuck sessions. Setting to 0 disables the idle timer.
if (!config.agents.defaults.llm) config.agents.defaults.llm = {};
if (config.agents.defaults.llm.idleTimeoutSeconds === undefined ||
config.agents.defaults.llm.idleTimeoutSeconds > 0) {
config.agents.defaults.llm.idleTimeoutSeconds = 0;
log(`Set agents.defaults.llm.idleTimeoutSeconds = 0 (prevents tool-call timeouts)`);
} else {
log(`idleTimeoutSeconds already configured: ${config.agents.defaults.llm.idleTimeoutSeconds}`);
}
writeJSON(CONFIG_PATH, config);
log(`Config saved`);
// ── Step 3: Patch auth-profiles.json ────────────────────────────────────
console.log("\n🔑 Configuring auth profiles...\n");
// Find all agent auth-profiles.json files
const agentsDir = join(OPENCLAW_DIR, "agents");
const agentDirs = existsSync(agentsDir)
? readdirSync(agentsDir).filter((d) => {
const ap = join(agentsDir, d, "agent", "auth-profiles.json");
return existsSync(ap);
})
: [];
for (const agentId of agentDirs) {
const apPath = join(agentsDir, agentId, "agent", "auth-profiles.json");
try {
const ap = readJSON(apPath);
if (!ap.profiles) ap.profiles = {};
// Add claude-local profile if missing
if (!ap.profiles[`${PROVIDER_NAME}:default`]) {
ap.profiles[`${PROVIDER_NAME}:default`] = {
type: "api_key",
provider: PROVIDER_NAME,
key: "local-proxy-no-auth",
};
}
// Add to lastGood if missing
if (!ap.lastGood) ap.lastGood = {};
if (!ap.lastGood[PROVIDER_NAME]) {
ap.lastGood[PROVIDER_NAME] = `${PROVIDER_NAME}:default`;
}
writeJSON(apPath, ap);
log(`Agent "${agentId}" auth profile updated`);
} catch (e) {
warn(`Skipped agent "${agentId}": ${e.message}`);
}
}
if (agentDirs.length === 0) {
warn("No agent auth-profiles.json found — you may need to restart the gateway first");
}
} }
// ── Step 4: Create start.sh ───────────────────────────────────────────── // ── Step 4: Create start.sh ─────────────────────────────────────────────
@@ -238,31 +244,46 @@ if (!DRY_RUN) {
log(`Launcher: ${startPath}`); log(`Launcher: ${startPath}`);
// ── Step 5: Summary ───────────────────────────────────────────────────── // ── Step 5: Summary ─────────────────────────────────────────────────────
console.log(` const banner = [
╔══════════════════════════════════════════════════════════════╗ `╔══════════════════════════════════════════════════════════════╗`,
║ Setup complete! ║ `║ Setup complete! ║`,
╠══════════════════════════════════════════════════════════════╣ `╠══════════════════════════════════════════════════════════════╣`,
║ ║ `║ ║`,
║ Provider: ${PROVIDER_NAME.padEnd(44)} `║ Provider: ${PROVIDER_NAME.padEnd(44)}`,
║ Port: ${String(PORT).padEnd(44)} `║ Port: ${String(PORT).padEnd(44)}`,
║ Models: ${`see models.json (${MODELS.length} available)`.padEnd(44)} `║ Models: ${`see models.json (${MODELS.length} available)`.padEnd(44)}`,
║ Default: ${DEFAULT_MODEL_ID.padEnd(44)} `║ Default: ${DEFAULT_MODEL_ID.padEnd(44)}`,
║ ║ `║ ║`,
║ Start proxy: ║ `║ Start proxy: ║`,
║ bash ${startPath.replace(HOME, "~").padEnd(50)} `║ bash ${startPath.replace(HOME, "~").padEnd(50)}`,
║ ║ `║ ║`,
║ Or directly: ║ `║ Or directly: ║`,
║ node ${serverPath.replace(HOME, "~").padEnd(49)} `║ node ${serverPath.replace(HOME, "~").padEnd(49)}`,
║ ║ `║ ║`,
║ Set as default model in openclaw.json: ║ ];
║ agents.defaults.model.primary = ║ if (OPENCLAW_PRESENT) {
║ "${PROVIDER_NAME}/${DEFAULT_MODEL_ID}"${" ".repeat(Math.max(0, 30 - PROVIDER_NAME.length - DEFAULT_MODEL_ID.length))} banner.push(
`║ Set as default model in openclaw.json:`,
║ Then restart gateway: `║ agents.defaults.model.primary =`,
║ openclaw gateway restart ║ `║ "${PROVIDER_NAME}/${DEFAULT_MODEL_ID}"${" ".repeat(Math.max(0, 30 - PROVIDER_NAME.length - DEFAULT_MODEL_ID.length))}`,
║ ║ `║ ║`,
╚══════════════════════════════════════════════════════════════╝ `║ Then restart gateway: ║`,
`); `║ openclaw gateway restart ║`,
`║ ║`,
);
} else {
banner.push(
`║ OpenClaw not detected — running in standalone mode. ║`,
`║ Point your IDE (Cline / Cursor / Continue / OpenCode / ║`,
`║ Aider / OpenClaw) at: ║`,
`║ http://${BIND_ADDRESS}:${String(PORT)}/v1${" ".repeat(Math.max(0, 47 - BIND_ADDRESS.length - String(PORT).length))}`,
`║ ║`,
`║ See README § "Client Setup" for per-IDE instructions. ║`,
`║ ║`,
);
}
banner.push(`╚══════════════════════════════════════════════════════════════╝`);
console.log("\n" + banner.join("\n") + "\n");
// ── Step 7: Install auto-start on boot ────────────────────────────────── // ── Step 7: Install auto-start on boot ──────────────────────────────────
if (!DRY_RUN) { if (!DRY_RUN) {