Three install/bridge bugs that made every OpenClaw model call to the bridge fail when driven by a non-bundled channel plugin (e.g. Fabric): 1. OpenClaw redacts secret-like keys before exposing pluginConfig to a plugin, so config.bridgeApiKey was the literal __OPENCLAW_REDACTED__ sentinel. The bridge then validated Authorization against the sentinel while the model provider sent the real key -> permanent HTTP 401. Resolve the real shared secret from the raw on-disk config (same pattern resolveAgent already uses); if still missing/redacted, treat as no-auth on the loopback-only bridge instead of 401-locking. 2. install.mjs set the provider apiKey authoritatively but only setIfMissing the plugin bridgeApiKey, so a stale prior value desynced the pair. Make bridgeApiKey authoritative too (they must match). 3. The provider had no timeoutSeconds; a full bridged agent turn far exceeds OpenClaw's default model-fetch timeout, so OpenClaw aborted mid-turn and no reply was ever delivered. Default timeoutSeconds=600 (preserves a user override). Verified live: bridge now returns 200 for the real key and a valid OpenAI SSE completion; the fetch-timeout abort is gone. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
199 lines
7.3 KiB
JavaScript
199 lines
7.3 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Install / uninstall the contractor-agent plugin into OpenClaw.
|
|
*
|
|
* Usage:
|
|
* node scripts/install.mjs --install
|
|
* node scripts/install.mjs --uninstall
|
|
*/
|
|
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import os from "node:os";
|
|
import { execSync } from "node:child_process";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
const PROJECT_ROOT = path.resolve(__dirname, "..");
|
|
const PLUGIN_ID = "contractor-agent";
|
|
const PLUGIN_INSTALL_DIR = path.join(os.homedir(), ".openclaw", "plugins", PLUGIN_ID);
|
|
const OPENCLAW_CONFIG = path.join(os.homedir(), ".openclaw", "openclaw.json");
|
|
const BRIDGE_PORT = 18800;
|
|
const BRIDGE_API_KEY = "contractor-bridge-local";
|
|
|
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
|
|
function readConfig() {
|
|
return JSON.parse(fs.readFileSync(OPENCLAW_CONFIG, "utf8"));
|
|
}
|
|
|
|
function writeConfig(cfg) {
|
|
fs.writeFileSync(OPENCLAW_CONFIG, JSON.stringify(cfg, null, 2) + "\n", "utf8");
|
|
}
|
|
|
|
function setIfMissing(obj, key, value) {
|
|
if (obj[key] === undefined || obj[key] === null) obj[key] = value;
|
|
}
|
|
|
|
// ── Install ───────────────────────────────────────────────────────────────────
|
|
|
|
function install() {
|
|
console.log(`[install] Installing ${PLUGIN_ID}...`);
|
|
|
|
// 1. Copy plugin files to ~/.openclaw/plugins/contractor-agent/
|
|
if (fs.existsSync(PLUGIN_INSTALL_DIR)) {
|
|
fs.rmSync(PLUGIN_INSTALL_DIR, { recursive: true });
|
|
}
|
|
fs.mkdirSync(PLUGIN_INSTALL_DIR, { recursive: true });
|
|
|
|
// Copy plugin/ contents to the install root.
|
|
// plugin/ contains index.ts, openclaw.plugin.json, package.json, and subdirs.
|
|
const pluginDir = path.join(PROJECT_ROOT, "plugin");
|
|
fs.cpSync(pluginDir, PLUGIN_INSTALL_DIR, { recursive: true });
|
|
|
|
// Copy services/ (standalone processes used by the plugin) into the install root.
|
|
const servicesDir = path.join(PROJECT_ROOT, "services");
|
|
if (fs.existsSync(servicesDir)) {
|
|
fs.cpSync(servicesDir, path.join(PLUGIN_INSTALL_DIR, "services"), { recursive: true });
|
|
}
|
|
|
|
// 2. Install npm dependencies inside the plugin dir
|
|
console.log(`[install] Installing npm dependencies...`);
|
|
execSync("npm install --omit=dev --no-audit --no-fund", {
|
|
cwd: PLUGIN_INSTALL_DIR,
|
|
stdio: "inherit",
|
|
});
|
|
|
|
// 3. Update openclaw.json
|
|
const cfg = readConfig();
|
|
|
|
// Add provider — spread existing first so user-added fields
|
|
// (e.g. timeoutSeconds, extraHeaders) survive reinstall. Script-managed
|
|
// fields (baseUrl/apiKey/api/models) are then overridden authoritatively
|
|
// since they're tied to the constants and model catalog above.
|
|
cfg.models = cfg.models ?? {};
|
|
cfg.models.providers = cfg.models.providers ?? {};
|
|
const existingProvider = cfg.models.providers[PLUGIN_ID] ?? {};
|
|
cfg.models.providers[PLUGIN_ID] = {
|
|
...existingProvider,
|
|
baseUrl: `http://127.0.0.1:${BRIDGE_PORT}/v1`,
|
|
apiKey: BRIDGE_API_KEY,
|
|
api: "openai-completions",
|
|
// The bridge wraps a full Claude/Gemini agent turn (tool use, multi-step),
|
|
// which routinely takes far longer than OpenClaw's default model-fetch
|
|
// timeout. Without a generous timeout OpenClaw aborts the request mid-turn
|
|
// and no reply is ever delivered. Preserve a user override if set.
|
|
timeoutSeconds: existingProvider.timeoutSeconds ?? 600,
|
|
models: [
|
|
{
|
|
id: "contractor-claude-bridge",
|
|
name: "Contractor Claude Bridge",
|
|
reasoning: false,
|
|
input: ["text"],
|
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
contextWindow: 200000,
|
|
maxTokens: 16000,
|
|
},
|
|
{
|
|
id: "contractor-gemini-bridge",
|
|
name: "Contractor Gemini Bridge",
|
|
reasoning: false,
|
|
input: ["text"],
|
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
contextWindow: 200000,
|
|
maxTokens: 16000,
|
|
},
|
|
],
|
|
};
|
|
|
|
// Add to plugin allow list
|
|
cfg.plugins = cfg.plugins ?? {};
|
|
cfg.plugins.allow = cfg.plugins.allow ?? [];
|
|
if (!cfg.plugins.allow.includes(PLUGIN_ID)) {
|
|
cfg.plugins.allow.push(PLUGIN_ID);
|
|
}
|
|
|
|
// Add load path
|
|
cfg.plugins.load = cfg.plugins.load ?? {};
|
|
cfg.plugins.load.paths = cfg.plugins.load.paths ?? [];
|
|
if (!cfg.plugins.load.paths.includes(PLUGIN_INSTALL_DIR)) {
|
|
cfg.plugins.load.paths.push(PLUGIN_INSTALL_DIR);
|
|
}
|
|
|
|
// Add plugin entry
|
|
cfg.plugins.entries = cfg.plugins.entries ?? {};
|
|
cfg.plugins.entries[PLUGIN_ID] = cfg.plugins.entries[PLUGIN_ID] ?? {};
|
|
cfg.plugins.entries[PLUGIN_ID].enabled = true;
|
|
|
|
// Set default config — setIfMissing so user values are preserved.
|
|
const pluginCfg = cfg.plugins.entries[PLUGIN_ID].config ?? {};
|
|
setIfMissing(pluginCfg, "bridgePort", BRIDGE_PORT);
|
|
setIfMissing(pluginCfg, "permissionMode", "bypassPermissions");
|
|
// bridgeApiKey is the shared secret between the bridge server (this plugin)
|
|
// and the model provider written above. The provider apiKey is set
|
|
// authoritatively (= BRIDGE_API_KEY); the bridge side MUST stay in lockstep
|
|
// or every request 401s. Set it authoritatively too — never setIfMissing
|
|
// (a stale prior value would desync the pair).
|
|
pluginCfg.bridgeApiKey = BRIDGE_API_KEY;
|
|
cfg.plugins.entries[PLUGIN_ID].config = pluginCfg;
|
|
|
|
writeConfig(cfg);
|
|
|
|
console.log(`[install] Done. Restart the gateway to activate:`);
|
|
console.log(` openclaw gateway restart`);
|
|
console.log(` openclaw contractor-agents add --agent-id <id> --workspace <path> --contractor claude`);
|
|
}
|
|
|
|
// ── Uninstall ─────────────────────────────────────────────────────────────────
|
|
|
|
function uninstall() {
|
|
console.log(`[uninstall] Removing ${PLUGIN_ID}...`);
|
|
|
|
const cfg = readConfig();
|
|
|
|
// Remove provider
|
|
if (cfg.models?.providers?.[PLUGIN_ID]) {
|
|
delete cfg.models.providers[PLUGIN_ID];
|
|
}
|
|
|
|
// Remove from allow list
|
|
if (Array.isArray(cfg.plugins?.allow)) {
|
|
cfg.plugins.allow = cfg.plugins.allow.filter((id) => id !== PLUGIN_ID);
|
|
}
|
|
|
|
// Remove load path
|
|
if (Array.isArray(cfg.plugins?.load?.paths)) {
|
|
cfg.plugins.load.paths = cfg.plugins.load.paths.filter(
|
|
(p) => p !== PLUGIN_INSTALL_DIR,
|
|
);
|
|
}
|
|
|
|
// Remove plugin entry
|
|
if (cfg.plugins?.entries?.[PLUGIN_ID]) {
|
|
delete cfg.plugins.entries[PLUGIN_ID];
|
|
}
|
|
|
|
writeConfig(cfg);
|
|
|
|
// Remove installed files
|
|
if (fs.existsSync(PLUGIN_INSTALL_DIR)) {
|
|
fs.rmSync(PLUGIN_INSTALL_DIR, { recursive: true });
|
|
console.log(`[uninstall] Removed ${PLUGIN_INSTALL_DIR}`);
|
|
}
|
|
|
|
console.log(`[uninstall] Done. Restart the gateway:`);
|
|
console.log(` openclaw gateway restart`);
|
|
}
|
|
|
|
// ── Main ──────────────────────────────────────────────────────────────────────
|
|
|
|
const arg = process.argv[2];
|
|
if (arg === "--install") {
|
|
install();
|
|
} else if (arg === "--uninstall") {
|
|
uninstall();
|
|
} else {
|
|
console.error("Usage: node scripts/install.mjs --install | --uninstall");
|
|
process.exit(1);
|
|
}
|