Initial commit: custom OpenClaw skills from docker-test

- workspace: capmetro-monitor, github-notifications, model-selector
- workspace-security: vt-monitor, monitor-unauthorized
- workspace-home: cron-manager, monitor-unauthorized
- extensions: vt-sentinel (VT-Sentinel plugin)

Includes sync.sh for pull/push, README, AGENTS.md, .gitignore.
This commit is contained in:
2026-02-16 15:32:44 +00:00
commit 3b7d6bb67c
37 changed files with 3358 additions and 0 deletions

View File

@@ -0,0 +1,97 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'EOF'
Usage:
clear-session-model-pins.sh --agent <agent-id> [--channel <channel-id>] [--sessions-file <path>]
Examples:
clear-session-model-pins.sh --agent home
clear-session-model-pins.sh --agent home --channel 1470162839284224184
Notes:
- Removes per-session "model" keys so agent defaults apply again.
- By default targets: /home/node/.openclaw/agents/<agent>/sessions/sessions.json
EOF
}
AGENT_ID=""
CHANNEL_ID=""
SESSIONS_FILE=""
while [[ $# -gt 0 ]]; do
case "$1" in
--agent)
AGENT_ID="${2:-}"
shift 2
;;
--channel)
CHANNEL_ID="${2:-}"
shift 2
;;
--sessions-file)
SESSIONS_FILE="${2:-}"
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown argument: $1" >&2
usage >&2
exit 1
;;
esac
done
if [[ -z "$AGENT_ID" ]]; then
echo "--agent is required" >&2
usage >&2
exit 1
fi
if [[ -z "$SESSIONS_FILE" ]]; then
SESSIONS_FILE="/home/node/.openclaw/agents/${AGENT_ID}/sessions/sessions.json"
fi
if [[ ! -f "$SESSIONS_FILE" ]]; then
echo "sessions file not found: $SESSIONS_FILE" >&2
exit 1
fi
python3 - <<PY
import json
from pathlib import Path
path = Path(${SESSIONS_FILE@Q})
channel = ${CHANNEL_ID@Q}
with path.open() as f:
data = json.load(f)
removed = 0
scanned = 0
for key, value in data.items():
if not isinstance(value, dict):
continue
scanned += 1
if channel:
if f"channel:{channel}" not in key:
continue
if "model" in value:
del value["model"]
removed += 1
with path.open("w") as f:
json.dump(data, f, indent=2)
f.write("\n")
print(f"scanned={scanned}")
print(f"removed_model_pins={removed}")
print(f"sessions_file={path}")
PY

View File

@@ -0,0 +1,74 @@
#!/usr/bin/env bash
# list-models.sh — Query the LLM proxy /v1/models endpoint
# Usage:
# list-models.sh # List all model IDs (sorted)
# list-models.sh --providers # List unique provider names
# list-models.sh --json # Raw JSON response
set -euo pipefail
# Resolve proxy URL and API key from environment or defaults
PROXY_URL="${LLM_PROXY_URL:-https://llm-proxy.ext.ben.io/v1}"
PROXY_KEY="${PROXY_API_KEY:-${LLM_PROXY_API_KEY:-}}"
if [[ -z "$PROXY_KEY" ]]; then
# Try to read from openclaw config
for cfg_path in \
"${OPENCLAW_STATE_DIR:-$HOME/.openclaw/state}/openclaw.json" \
"${OPENCLAW_CONFIG_DIR:-$HOME/.openclaw/config}/openclaw.json"; do
if [[ -f "$cfg_path" ]]; then
# Extract apiKey from llm-proxy provider config (handles JSON5 comments)
key=$(grep -A5 '"llm-proxy"' "$cfg_path" | grep '"apiKey"' | head -1 | sed 's/.*"apiKey"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/' || true)
if [[ -n "$key" && "$key" != *'${'* ]]; then
PROXY_KEY="$key"
break
fi
fi
done
fi
if [[ -z "$PROXY_KEY" ]]; then
echo "ERROR: No API key found. Set PROXY_API_KEY or LLM_PROXY_API_KEY environment variable." >&2
exit 1
fi
# Strip trailing /v1 from PROXY_URL if present, then always append /v1/models
# This prevents double /v1/v1/ when LLM_PROXY_URL already includes /v1
PROXY_BASE="${PROXY_URL%/v1}"
PROXY_BASE="${PROXY_BASE%/}"
response=$(curl -s -f -H "Authorization: Bearer $PROXY_KEY" "${PROXY_BASE}/v1/models" 2>&1) || {
echo "ERROR: Failed to query ${PROXY_BASE}/v1/models" >&2
echo "$response" >&2
exit 1
}
case "${1:-}" in
--providers)
echo "$response" | python3 -c "
import sys, json
data = json.load(sys.stdin)
providers = sorted(set(m['id'].split('/')[0] for m in data.get('data', [])))
for p in providers:
print(p)
"
;;
--json)
echo "$response"
;;
--count)
echo "$response" | python3 -c "
import sys, json
data = json.load(sys.stdin)
print(len(data.get('data', [])))
"
;;
*)
echo "$response" | python3 -c "
import sys, json
data = json.load(sys.stdin)
models = sorted(m['id'] for m in data.get('data', []))
for m in models:
print(m)
"
;;
esac

View File

@@ -0,0 +1,97 @@
#!/usr/bin/env bash
# show-current.sh — Display the current model configuration from openclaw state
# Usage: show-current.sh
set -euo pipefail
# Find the state file
STATE_FILE="${OPENCLAW_STATE_DIR:-$HOME/.openclaw/state}/openclaw.json"
if [[ ! -f "$STATE_FILE" ]]; then
# Try alternative locations
for alt in \
"/opt/openclaw/state/openclaw.json" \
"${OPENCLAW_CONFIG_DIR:-$HOME/.openclaw/config}/openclaw.json"; do
if [[ -f "$alt" ]]; then
STATE_FILE="$alt"
break
fi
done
fi
if [[ ! -f "$STATE_FILE" ]]; then
echo "ERROR: Cannot find openclaw.json state file" >&2
echo "Searched:" >&2
echo " ${OPENCLAW_STATE_DIR:-$HOME/.openclaw/state}/openclaw.json" >&2
echo " /opt/openclaw/state/openclaw.json" >&2
echo " ${OPENCLAW_CONFIG_DIR:-$HOME/.openclaw/config}/openclaw.json" >&2
exit 1
fi
echo "📁 Config file: $STATE_FILE"
echo ""
python3 -c "
import json, sys, re
# Read file and strip JSON5 comments for parsing
with open('$STATE_FILE', 'r') as f:
content = f.read()
# Strip single-line comments (// ...) but not inside strings
lines = content.split('\n')
cleaned = []
for line in lines:
stripped = line.rstrip()
s = stripped.lstrip()
if s.startswith('//'):
continue
in_string = False
result = []
i = 0
while i < len(stripped):
c = stripped[i]
if c == '\"' and (i == 0 or stripped[i-1] != '\\\\'):
in_string = not in_string
elif c == '/' and i + 1 < len(stripped) and stripped[i+1] == '/' and not in_string:
break
result.append(c)
i += 1
cleaned.append(''.join(result))
# Remove trailing commas (JSON5)
json_str = '\n'.join(cleaned)
json_str = re.sub(r',\s*([}\]])', r'\1', json_str)
try:
cfg = json.loads(json_str)
except json.JSONDecodeError:
try:
cfg = json.loads(content)
except json.JSONDecodeError as e:
print(f'ERROR: Failed to parse config: {e}', file=sys.stderr)
sys.exit(1)
agents = cfg.get('agents', {})
defaults = agents.get('defaults', {})
model = defaults.get('model', {})
if isinstance(model, str):
print(f'🎯 Primary: {model}')
print(f'⛓️ Fallbacks: (none configured)')
else:
primary = model.get('primary', '(not set)')
fallbacks = model.get('fallbacks', [])
print(f'🎯 Primary: {primary}')
print(f'⛓️ Fallbacks ({len(fallbacks)}):')
for i, fb in enumerate(fallbacks, 1):
print(f' {i}. {fb}')
# Check for per-agent model overrides
agent_list = agents.get('list', [])
overrides = [(a.get('id', '?'), a.get('model', '')) for a in agent_list if 'model' in a]
if overrides:
print()
print('⚠️ Per-agent model overrides:')
for aid, amodel in overrides:
print(f' {aid}: {amodel}')
" 2>&1

View File

@@ -0,0 +1,216 @@
#!/usr/bin/env bash
# update-model.sh — Update model configuration in openclaw state file
# Usage:
# update-model.sh --primary <model-id>
# update-model.sh --fallbacks <model1,model2,model3>
# update-model.sh --primary <model-id> --fallbacks <model1,model2>
#
# All model IDs are validated against /v1/models before writing.
# A backup of the current config is created before any changes.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PRIMARY=""
FALLBACKS=""
while [[ $# -gt 0 ]]; do
case "$1" in
--primary)
PRIMARY="$2"
shift 2
;;
--fallbacks)
FALLBACKS="$2"
shift 2
;;
--help|-h)
echo "Usage: update-model.sh [--primary <model-id>] [--fallbacks <model1,model2,...>]"
echo ""
echo "Options:"
echo " --primary Set the primary model (will be prefixed with llm-proxy/)"
echo " --fallbacks Comma-separated list of fallback models (min 2 required)"
echo ""
echo "All model IDs are validated against /v1/models before writing."
exit 0
;;
*)
echo "ERROR: Unknown argument: $1" >&2
exit 1
;;
esac
done
if [[ -z "$PRIMARY" && -z "$FALLBACKS" ]]; then
echo "ERROR: Must specify --primary and/or --fallbacks" >&2
exit 1
fi
# Find the state file
STATE_FILE="${OPENCLAW_STATE_DIR:-$HOME/.openclaw/state}/openclaw.json"
if [[ ! -f "$STATE_FILE" ]]; then
for alt in \
"/opt/openclaw/state/openclaw.json" \
"${OPENCLAW_CONFIG_DIR:-$HOME/.openclaw/config}/openclaw.json"; do
if [[ -f "$alt" ]]; then
STATE_FILE="$alt"
break
fi
done
fi
if [[ ! -f "$STATE_FILE" ]]; then
echo "ERROR: Cannot find openclaw.json state file" >&2
exit 1
fi
echo "📁 Config file: $STATE_FILE"
# Validate all model IDs first
echo ""
echo "🔍 Validating model IDs against /v1/models..."
VALIDATION_FAILED=0
if [[ -n "$PRIMARY" ]]; then
# Strip llm-proxy/ prefix for validation
PRIMARY_CLEAN="${PRIMARY#llm-proxy/}"
if ! "$SCRIPT_DIR/validate-model.sh" "$PRIMARY_CLEAN" 2>&1; then
VALIDATION_FAILED=1
fi
fi
if [[ -n "$FALLBACKS" ]]; then
IFS=',' read -ra FB_ARRAY <<< "$FALLBACKS"
if [[ ${#FB_ARRAY[@]} -lt 2 ]]; then
echo "❌ ERROR: Minimum 2 fallback models required (got ${#FB_ARRAY[@]})" >&2
VALIDATION_FAILED=1
fi
for fb in "${FB_ARRAY[@]}"; do
fb_clean="${fb#llm-proxy/}"
fb_clean="$(echo "$fb_clean" | xargs)" # trim whitespace
if ! "$SCRIPT_DIR/validate-model.sh" "$fb_clean" 2>&1; then
VALIDATION_FAILED=1
fi
done
fi
if [[ $VALIDATION_FAILED -ne 0 ]]; then
echo ""
echo "❌ Validation failed. No changes made." >&2
exit 1
fi
# Create backup
BACKUP="${STATE_FILE}.backup.$(date +%Y%m%d-%H%M%S)"
cp "$STATE_FILE" "$BACKUP"
echo ""
echo "💾 Backup saved: $BACKUP"
# Apply changes using Python for safe JSON manipulation
python3 -c "
import json, sys, re
state_file = '$STATE_FILE'
primary = '${PRIMARY}'.strip() or None
fallbacks_raw = '${FALLBACKS}'.strip() or None
# Read and parse (handle JSON5 comments)
with open(state_file, 'r') as f:
content = f.read()
# Strip comments for parsing
lines = content.split('\n')
cleaned = []
for line in lines:
s = line.lstrip()
if s.startswith('//'):
continue
in_string = False
result = []
i = 0
while i < len(line):
c = line[i]
if c == '\"' and (i == 0 or line[i-1] != '\\\\'):
in_string = not in_string
elif c == '/' and i + 1 < len(line) and line[i+1] == '/' and not in_string:
break
result.append(c)
i += 1
cleaned.append(''.join(result))
# Remove trailing commas before } or ] (JSON5 feature)
json_str = '\n'.join(cleaned)
json_str = re.sub(r',\s*([}\]])', r'\1', json_str)
try:
cfg = json.loads(json_str)
except json.JSONDecodeError:
try:
cfg = json.loads(content)
except json.JSONDecodeError as e:
print(f'ERROR: Failed to parse config: {e}', file=sys.stderr)
sys.exit(1)
# Ensure path exists
if 'agents' not in cfg:
cfg['agents'] = {}
if 'defaults' not in cfg['agents']:
cfg['agents']['defaults'] = {}
if 'model' not in cfg['agents']['defaults']:
cfg['agents']['defaults']['model'] = {}
model = cfg['agents']['defaults']['model']
if isinstance(model, str):
model = {'primary': model}
cfg['agents']['defaults']['model'] = model
old_primary = model.get('primary', '(none)')
old_fallbacks = model.get('fallbacks', [])
# Apply primary
if primary:
# Ensure llm-proxy/ prefix
if not primary.startswith('llm-proxy/'):
primary = f'llm-proxy/{primary}'
model['primary'] = primary
# Apply fallbacks
if fallbacks_raw:
fbs = [fb.strip() for fb in fallbacks_raw.split(',') if fb.strip()]
fbs = [f'llm-proxy/{fb}' if not fb.startswith('llm-proxy/') else fb for fb in fbs]
model['fallbacks'] = fbs
# Remove per-agent model overrides that match the old primary
# (they were likely set by the same drift that caused the issue)
agent_list = cfg.get('agents', {}).get('list', [])
removed_overrides = []
for agent in agent_list:
if 'model' in agent:
removed_overrides.append((agent.get('id', '?'), agent['model']))
del agent['model']
# Write back
with open(state_file, 'w') as f:
json.dump(cfg, f, indent=2)
f.write('\n')
# Print summary
print()
print('✅ Configuration updated:')
print()
print(f' Primary: {old_primary} → {model.get(\"primary\", \"(none)\")}')
print(f' Fallbacks:')
for i, fb in enumerate(model.get('fallbacks', []), 1):
old_marker = '' if fb in old_fallbacks else ' (new)'
print(f' {i}. {fb}{old_marker}')
if removed_overrides:
print()
print(' 🧹 Cleared per-agent model overrides:')
for aid, amodel in removed_overrides:
print(f' {aid}: {amodel} → (uses default)')
" 2>&1
echo ""
echo "Done. Restart OpenClaw for changes to take effect."

View File

@@ -0,0 +1,39 @@
#!/usr/bin/env bash
# validate-model.sh — Validate that a model ID exists in the LLM proxy
# Usage: validate-model.sh <model-id>
# Exit 0 if valid, 1 if not found
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if [[ $# -lt 1 ]]; then
echo "Usage: validate-model.sh <model-id>" >&2
echo "Example: validate-model.sh nanogpt/deepseek-chat" >&2
exit 1
fi
MODEL_ID="$1"
# Strip llm-proxy/ prefix if present (user might pass the openclaw.json format)
MODEL_ID="${MODEL_ID#llm-proxy/}"
# Get the live model list
available=$("$SCRIPT_DIR/list-models.sh" 2>/dev/null) || {
echo "ERROR: Could not fetch model list from LLM proxy" >&2
exit 1
}
if echo "$available" | grep -qxF "$MODEL_ID"; then
echo "✅ Model '$MODEL_ID' is available"
exit 0
else
echo "❌ Model '$MODEL_ID' NOT found in /v1/models" >&2
# Suggest close matches
partial=$(echo "$available" | grep -i "$(echo "$MODEL_ID" | sed 's|.*/||')" | head -5)
if [[ -n "$partial" ]]; then
echo "" >&2
echo "Did you mean one of these?" >&2
echo "$partial" | sed 's/^/ /' >&2
fi
exit 1
fi