Files
openclaw-workspace/skills/ollama-memory-embeddings/enforce.sh
2026-04-11 09:45:12 -05:00

373 lines
11 KiB
Bash

#!/usr/bin/env bash
# Enforce OpenClaw memorySearch to use Ollama embeddings settings.
# Idempotent: safe to run repeatedly.
set -euo pipefail
MODEL=""
BASE_URL="http://127.0.0.1:11434/v1/"
CONFIG_PATH="${OPENCLAW_CONFIG_PATH:-${HOME}/.openclaw/openclaw.json}"
CHECK_ONLY=0
QUIET=0
RESTART_ON_CHANGE=0
API_KEY_VALUE="ollama"
usage() {
cat <<'EOF'
Usage:
enforce.sh [options]
Options:
--model <id> embedding model id (required unless already in config)
--base-url <url> Ollama OpenAI-compatible base URL (default: http://127.0.0.1:11434/v1/)
--openclaw-config <path> OpenClaw config path (default: ~/.openclaw/openclaw.json)
--api-key-value <value> apiKey to set if missing (default: ollama)
--check-only exit non-zero if drift is detected, do not modify config
--restart-on-change restart gateway if config was changed
--quiet suppress non-error output
--help show help
Exit codes:
0 success (no drift or drift healed)
10 drift detected in --check-only mode
1 error
EOF
}
while [ $# -gt 0 ]; do
case "$1" in
--model) MODEL="$2"; shift 2 ;;
--base-url) BASE_URL="$2"; shift 2 ;;
--openclaw-config) CONFIG_PATH="$2"; shift 2 ;;
--api-key-value) API_KEY_VALUE="$2"; shift 2 ;;
--check-only) CHECK_ONLY=1; shift ;;
--restart-on-change) RESTART_ON_CHANGE=1; shift ;;
--quiet) QUIET=1; shift ;;
--help|-h) usage; exit 0 ;;
*) echo "Unknown option: $1"; usage; exit 1 ;;
esac
done
log() {
[ "$QUIET" -eq 1 ] || echo "$@"
}
normalize_model() {
local m="$1"
if [[ "$m" != *:* ]]; then
echo "${m}:latest"
else
echo "$m"
fi
}
normalize_base_url() {
local u="${1%/}"
if [[ "$u" != */v1 ]]; then
u="${u}/v1"
fi
echo "${u}/"
}
restart_gateway() {
if ! command -v openclaw >/dev/null 2>&1; then
log "NOTE: openclaw CLI not found; skip restart."
return 0
fi
if openclaw gateway restart 2>/dev/null; then
log "Gateway restarted."
return 0
fi
log "WARNING: openclaw gateway restart failed; restart manually."
return 1
}
require_cmd() {
command -v "$1" >/dev/null 2>&1 || {
echo "ERROR: '$1' not found in PATH."
exit 1
}
}
require_cmd node
mkdir -p "$(dirname "$CONFIG_PATH")"
[ -f "$CONFIG_PATH" ] || echo "{}" > "$CONFIG_PATH"
BASE_URL_NORM="$(normalize_base_url "$BASE_URL")"
# If model omitted, try current config model; otherwise enforce requires explicit model.
if [ -z "$MODEL" ]; then
MODEL="$(node -e '
const fs=require("fs");
const p=process.argv[1];
const CANDIDATES = [
{ label: "agents.defaults.memorySearch", path: ["agents", "defaults", "memorySearch"] },
{ label: "memorySearch", path: ["memorySearch"] },
{ label: "agents.memorySearch", path: ["agents", "memorySearch"] },
{ label: "agents.defaults.memory.search", path: ["agents", "defaults", "memory", "search"] },
{ label: "memory.search", path: ["memory", "search"] },
];
function getAt(obj, path) {
let cur = obj;
for (const k of path) {
if (!cur || typeof cur !== "object" || !(k in cur)) return undefined;
cur = cur[k];
}
return cur;
}
function resolveActive(cfg) {
const canonical = CANDIDATES[0];
const canonicalVal = getAt(cfg, canonical.path);
if (canonicalVal && typeof canonicalVal === "object" && !Array.isArray(canonicalVal)) return canonical;
for (const c of CANDIDATES.slice(1)) {
const v = getAt(cfg, c.path);
if (v && typeof v === "object" && !Array.isArray(v)) return c;
}
return canonical;
}
try {
const cfg=JSON.parse(fs.readFileSync(p,"utf8"));
const active = resolveActive(cfg);
const ms = getAt(cfg, active.path) || {};
const m=ms.model||"";
process.stdout.write(m);
} catch (_) {}
' "$CONFIG_PATH")"
fi
if [ -z "$MODEL" ]; then
echo "ERROR: no model provided and no existing memorySearch.model in config."
exit 1
fi
MODEL_NORM="$(normalize_model "$MODEL")"
if [ -z "$API_KEY_VALUE" ]; then
echo "ERROR: --api-key-value must be non-empty."
exit 1
fi
export CONFIG_PATH MODEL_NORM BASE_URL_NORM API_KEY_VALUE
if [ "$CHECK_ONLY" -eq 1 ]; then
set +e
node <<'EOF'
const fs = require("fs");
const p = process.env.CONFIG_PATH;
const model = process.env.MODEL_NORM;
const base = process.env.BASE_URL_NORM;
const CANDIDATES = [
{ label: "agents.defaults.memorySearch", path: ["agents", "defaults", "memorySearch"] },
{ label: "memorySearch", path: ["memorySearch"] },
{ label: "agents.memorySearch", path: ["agents", "memorySearch"] },
{ label: "agents.defaults.memory.search", path: ["agents", "defaults", "memory", "search"] },
{ label: "memory.search", path: ["memory", "search"] },
];
function getAt(obj, path) {
let cur = obj;
for (const k of path) {
if (!cur || typeof cur !== "object" || !(k in cur)) return undefined;
cur = cur[k];
}
return cur;
}
function resolveActive(cfg) {
const canonical = CANDIDATES[0];
const canonicalVal = getAt(cfg, canonical.path);
if (canonicalVal && typeof canonicalVal === "object" && !Array.isArray(canonicalVal)) return canonical;
for (const c of CANDIDATES.slice(1)) {
const v = getAt(cfg, c.path);
if (v && typeof v === "object" && !Array.isArray(v)) return c;
}
return canonical;
}
let cfg = {};
try { cfg = JSON.parse(fs.readFileSync(p, "utf8")); } catch { process.exit(10); }
const active = resolveActive(cfg);
const ms = getAt(cfg, active.path) || {};
const apiKey = ms?.remote?.apiKey || "";
const drift =
ms.provider !== "openai" ||
(ms.model || "") !== model ||
(ms?.remote?.baseUrl || "") !== base ||
apiKey === "";
process.exit(drift ? 10 : 0);
EOF
status=$?
set -e
if [ "$status" -eq 0 ]; then
log "No drift detected."
exit 0
elif [ "$status" -eq 10 ]; then
log "Drift detected."
exit 10
else
echo "ERROR: drift check failed."
exit 1
fi
fi
set +e
PLAN_OUT="$(node <<'EOF'
const fs = require("fs");
const path = process.env.CONFIG_PATH;
const model = process.env.MODEL_NORM;
const base = process.env.BASE_URL_NORM;
const desiredApiKey = process.env.API_KEY_VALUE;
const CANDIDATES = [
{ label: "agents.defaults.memorySearch", path: ["agents", "defaults", "memorySearch"] },
{ label: "memorySearch", path: ["memorySearch"] },
{ label: "agents.memorySearch", path: ["agents", "memorySearch"] },
{ label: "agents.defaults.memory.search", path: ["agents", "defaults", "memory", "search"] },
{ label: "memory.search", path: ["memory", "search"] },
];
function getAt(obj, path) {
let cur = obj;
for (const k of path) {
if (!cur || typeof cur !== "object" || !(k in cur)) return undefined;
cur = cur[k];
}
return cur;
}
function ensureAt(obj, path) {
let cur = obj;
for (const k of path) {
if (!cur[k] || typeof cur[k] !== "object" || Array.isArray(cur[k])) cur[k] = {};
cur = cur[k];
}
return cur;
}
function resolveActive(cfg) {
const canonical = CANDIDATES[0];
const canonicalVal = getAt(cfg, canonical.path);
if (canonicalVal && typeof canonicalVal === "object" && !Array.isArray(canonicalVal)) return canonical;
for (const c of CANDIDATES.slice(1)) {
const v = getAt(cfg, c.path);
if (v && typeof v === "object" && !Array.isArray(v)) return c;
}
return canonical;
}
let cfg = {};
try { cfg = JSON.parse(fs.readFileSync(path, "utf8")); } catch (_) { cfg = {}; }
const before = JSON.stringify(cfg);
const canonical = CANDIDATES[0];
const active = resolveActive(cfg);
const targets = [canonical];
if (active.label !== canonical.label) targets.push(active);
for (const t of targets) {
const ms = ensureAt(cfg, t.path);
ms.provider = "openai";
ms.model = model;
ms.remote = ms.remote || {};
ms.remote.baseUrl = base;
// Preserve existing non-empty apiKey to avoid false drift with custom conventions.
if (!ms.remote.apiKey) ms.remote.apiKey = desiredApiKey;
}
const afterObj = getAt(cfg, canonical.path) || {};
const changed = before !== JSON.stringify(cfg);
console.log(changed ? "changed" : "unchanged");
console.log(afterObj.provider || "");
console.log(afterObj.model || "");
console.log((afterObj.remote && afterObj.remote.baseUrl) || "");
console.log((afterObj.remote && afterObj.remote.apiKey) ? "(set)" : "(missing)");
console.log(active.label);
console.log(active.label !== canonical.label ? "yes" : "no");
EOF
)"
status=$?
set -e
if [ "$status" -ne 0 ]; then
echo "ERROR: failed to plan memorySearch enforcement."
exit 1
fi
CHANGED="$(printf "%s\n" "$PLAN_OUT" | sed -n '1p')"
PROVIDER_NOW="$(printf "%s\n" "$PLAN_OUT" | sed -n '2p')"
MODEL_NOW="$(printf "%s\n" "$PLAN_OUT" | sed -n '3p')"
BASE_NOW="$(printf "%s\n" "$PLAN_OUT" | sed -n '4p')"
APIKEY_NOW="$(printf "%s\n" "$PLAN_OUT" | sed -n '5p')"
ACTIVE_PATH="$(printf "%s\n" "$PLAN_OUT" | sed -n '6p')"
MIRRORING_LEGACY="$(printf "%s\n" "$PLAN_OUT" | sed -n '7p')"
if [ "$CHANGED" = "changed" ]; then
BACKUP_PATH="${CONFIG_PATH}.bak.$(date -u +%Y-%m-%dT%H-%M-%SZ)"
cp "$CONFIG_PATH" "$BACKUP_PATH"
node <<'EOF'
const fs = require("fs");
const path = process.env.CONFIG_PATH;
const model = process.env.MODEL_NORM;
const base = process.env.BASE_URL_NORM;
const desiredApiKey = process.env.API_KEY_VALUE;
const CANDIDATES = [
{ label: "agents.defaults.memorySearch", path: ["agents", "defaults", "memorySearch"] },
{ label: "memorySearch", path: ["memorySearch"] },
{ label: "agents.memorySearch", path: ["agents", "memorySearch"] },
{ label: "agents.defaults.memory.search", path: ["agents", "defaults", "memory", "search"] },
{ label: "memory.search", path: ["memory", "search"] },
];
function getAt(obj, path) {
let cur = obj;
for (const k of path) {
if (!cur || typeof cur !== "object" || !(k in cur)) return undefined;
cur = cur[k];
}
return cur;
}
function ensureAt(obj, path) {
let cur = obj;
for (const k of path) {
if (!cur[k] || typeof cur[k] !== "object" || Array.isArray(cur[k])) cur[k] = {};
cur = cur[k];
}
return cur;
}
function resolveActive(cfg) {
const canonical = CANDIDATES[0];
const canonicalVal = getAt(cfg, canonical.path);
if (canonicalVal && typeof canonicalVal === "object" && !Array.isArray(canonicalVal)) return canonical;
for (const c of CANDIDATES.slice(1)) {
const v = getAt(cfg, c.path);
if (v && typeof v === "object" && !Array.isArray(v)) return c;
}
return canonical;
}
let cfg = {};
try { cfg = JSON.parse(fs.readFileSync(path, "utf8")); } catch (_) { cfg = {}; }
const canonical = CANDIDATES[0];
const active = resolveActive(cfg);
const targets = [canonical];
if (active.label !== canonical.label) targets.push(active);
for (const t of targets) {
const ms = ensureAt(cfg, t.path);
ms.provider = "openai";
ms.model = model;
ms.remote = ms.remote || {};
ms.remote.baseUrl = base;
if (!ms.remote.apiKey) ms.remote.apiKey = desiredApiKey;
}
fs.writeFileSync(path, JSON.stringify(cfg, null, 2));
EOF
fi
log "Config: ${CONFIG_PATH}"
if [ "$CHANGED" = "changed" ]; then
log "Backup: ${BACKUP_PATH}"
fi
log "provider=${PROVIDER_NOW}"
log "model=${MODEL_NOW}"
log "baseUrl=${BASE_NOW}"
log "apiKey=${APIKEY_NOW}"
if [ -n "$ACTIVE_PATH" ] && [ "$ACTIVE_PATH" != "agents.defaults.memorySearch" ]; then
log "legacyPathDetected=${ACTIVE_PATH}"
fi
if [ "$MIRRORING_LEGACY" = "yes" ]; then
log "legacyMirrored=yes"
fi
if [ "$CHANGED" = "changed" ]; then
log "Drift healed: memorySearch settings updated."
if [ "$RESTART_ON_CHANGE" -eq 1 ]; then
restart_gateway || true
fi
else
log "No changes required."
fi