kb: batch from 2026-07-30 parallel run (#181 #183 #189 #192 #164 #128 #219)

Work produced by the /kb driver on 2026-07-30. Each change is recorded on its
Kanboard task; all remain Done-unverified or parked pending alvis's decisions.

#183 agap-mcp/src/gitea.js
  askpassScript() and giteaWikiWrite()'s wiki checkout both used
  /tmp/agap-mcp-wiki, so writing the askpass helper made the dir non-empty and
  git clone always failed. gitea_wiki_write had likely never succeeded in
  production. Askpass moved to its own dir.

#181 agap-mcp/src/server.js
  Initialise registeredToolCount at module load so /health reports the real
  count immediately instead of 0 until the first MCP request.

#189 kanboard/backup.sh, seafile/backup.sh, vaultwarden/backup.sh,
     users-backup.sh, openai/backup-{hindsight-adolf,llm-dbs}.sh
  Remove the dead *.ts Zabbix trapper pushes (never landed). users-backup.sh
  also pointed at localhost:81 instead of 192.168.1.4:81 and pushed a date
  string into a numeric item. Freshness monitoring now rides the .age items.

#192 RESTORE-RUNBOOK.md, {kanboard,seafile,vaultwarden}/restore.sh
  Restore path for the three services, verified in throwaway containers.
  Note: this work found Seafile backups have carried an empty ccnet_db.sql
  since 2026-07-07 -- filed as kb#222, not fixed here.

#164 openai/litellm-config.yaml
  Metered `judge` (anthropic/claude-haiku-4-5) entry removed per alvis's
  2026-07-30 decision. ANTHROPIC_API_KEY was never wired, so it could not spend.

#128 openai/agent_registry.py
  litellm_key_spec() now also grants the routing-mode aliases, gated by the
  same _reachable_tiers() check as raw grants, so a small-tier agent cannot
  acquire automatic routing that resolves to tier-large.

#219 openai/migrate-adolf-state.sh
  Migration script only; inert until run. Copies (never moves) the
  openai_adolf-state volume to /mnt/ssd/dbs/adolf, verifying a full sha256
  manifest before declaring success. Tested against a throwaway volume.

Deliberately NOT included, both awaiting alvis:
  agap-mcp/docker-compose.yml -- kb#174's contested BW_EMAIL revert (parked).
  openai/docker-compose.yml   -- kb#219's bind-mount switch; the target dirs
                                 under /mnt/ssd/dbs/adolf do not exist yet, so
                                 committing it would let a later `compose up`
                                 recreate Adolf against empty paths.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Y5QPagv4iun1ghpwM96Ff
This commit is contained in:
2026-07-30 15:06:09 +00:00
parent 4a9ae75912
commit a27bae828a
15 changed files with 666 additions and 110 deletions

View File

@@ -210,6 +210,33 @@ def litellm_key_spec(registry, agent_id, model_registry=None):
if name not in models:
models.append(name)
# kb#128 gap (flagged 2026-07-26, closed 2026-07-30): the raw litellm_
# model_names above (e.g. "ollama/gemma3:4b") are the BACKING deployments
# for openai/litellm-config.yaml's alias model_names -- tier-small/
# tier-large (alvis's "tier" routing mode) and auto_router/
# complexity_router (alvis's "automatic" routing mode). Without granting
# the aliases too, a provisioned key could reach a model directly but not
# by tier or through the router, so "all three routing modes exercisable"
# (kb#128 acceptance) wasn't actually true per-agent. Gate exactly like
# the raw grants above -- reachable tiers, not a separate allow-list --
# so an agent's routing-mode access never exceeds its direct-model access:
# - "small" reachable -> tier-small (mirrors the always-granted small
# pool; every agent with a backbone gets at least this).
# - "large" reachable -> tier-large, PLUS auto_router/complexity_router.
# Both routers' pools include tier-large in their upper bands (COMPLEX/
# REASONING, or the semantic "complex reasoning" route), so granting
# them to a small-only (sandboxed) agent would let automatic routing
# escalate it past its trust class -- exactly the asymmetry
# _reachable_tiers()/kb#147 exists to prevent. A small-only agent gets
# neither router: it can still call tier-small directly.
reachable = _reachable_tiers(a.get("preferred_tier"))
if "small" in reachable and "tier-small" not in models:
models.append("tier-small")
if "large" in reachable:
for alias in ("tier-large", "auto_router", "complexity_router"):
if alias not in models:
models.append(alias)
classes = registry.get("trust_classes", {})
cls = classes.get(a["trust_class"], {})
return {

View File

@@ -2,8 +2,8 @@
# Backup script for hindsight (Adolf's long-term memory bank) and the
# openai_adolf-state Docker volume (Matrix E2EE identity/sessions + config).
# Mirrors the seafile/vaultwarden/openai-llm-dbs backup.sh pattern (same repo):
# dump/tar via `docker exec`, gzip, retention of last 5, Zabbix freshness
# trapper item per target.
# dump/tar via `docker exec`, gzip, retention of last 5. Backup-freshness
# monitored via .age items.
#
# hindsight is an embedded Postgres (pg0) instance living at
# /mnt/ssd/dbs/hindsight on the host, bind-mounted into the `hindsight`
@@ -36,30 +36,14 @@
set -euo pipefail
BACKUP_DIR="/mnt/backups/hindsight-adolf"
ZABBIX_TOKEN_FILE="/root/.zabbix_token"
ZABBIX_URL="http://192.168.1.4:81/api_jsonrpc.php"
DATE=$(date '+%Y%m%d-%H%M')
DEST="$BACKUP_DIR/$DATE"
mkdir -p "$DEST"
notify_zabbix() {
local itemid="$1" label="$2"
if [[ -f "$ZABBIX_TOKEN_FILE" ]]; then
local token now_epoch
token=$(cat "$ZABBIX_TOKEN_FILE")
now_epoch=$(date '+%s')
env -u HTTPS_PROXY -u HTTP_PROXY -u ALL_PROXY -u https_proxy -u http_proxy -u all_proxy \
curl -s -X POST "$ZABBIX_URL" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $token" \
-d "{\"jsonrpc\":\"2.0\",\"method\":\"history.push\",\"id\":1,\"params\":{\"itemid\":\"$itemid\",\"value\":$now_epoch}}" > /dev/null \
&& echo "Zabbix notified ($label=$now_epoch)."
else
echo "WARNING: $ZABBIX_TOKEN_FILE not found -- skipped Zabbix freshness push for $label." >&2
fi
}
# Backup-freshness monitoring is now done via .age items (calculated fields showing
# age of the backup). The .ts (timestamp) trappers were unreliable (history.push not
# landing); removed in kb#189 in favor of .age overdue triggers.
# --- hindsight (Postgres logical dump, live/read-only) ---
echo "Dumping hindsight..."
@@ -67,13 +51,11 @@ docker exec -e PGPASSWORD=hindsight hindsight \
/home/hindsight/.pg0/installation/18.1.0/bin/pg_dump -U hindsight -h 127.0.0.1 -p 5432 hindsight \
| gzip > "$DEST/hindsight.sql.gz"
echo "Dumped: hindsight -> $DEST/hindsight.sql.gz"
notify_zabbix "70639" "hindsight.backup.ts"
# --- adolf-state (tar the volume from inside the adolf container) ---
echo "Archiving adolf-state..."
docker exec adolf tar czf - -C /home/node/.openclaw . > "$DEST/adolf-state.tar.gz"
echo "Archived: adolf-state -> $DEST/adolf-state.tar.gz"
notify_zabbix "70640" "adolf-state.backup.ts"
echo "$(date): Backup complete: $DEST"
ls -la "$DEST/"

View File

@@ -2,9 +2,9 @@
# Backup script for litellm-db and langfuse-db (openai stack postgres containers).
# litellm-db holds provisioned virtual keys + spend; langfuse-db holds all traces.
# Mirrors the seafile/vaultwarden backup.sh pattern (same repo): dump via
# `docker exec <container> pg_dump`, gzip, retention of last 5, Zabbix freshness
# trapper item per DB. Uses pg_dump (safe against a live/running DB, no downtime
# needed — unlike gitea's stop-the-world dump).
# `docker exec <container> pg_dump`, gzip, retention of last 5. Uses pg_dump (safe
# against a live/running DB, no downtime needed — unlike gitea's stop-the-world dump).
# Backup-freshness monitored via .age items.
#
# Run every 3 days via root crontab (same schedule as vaultwarden/seafile), e.g.:
# 0 3 */3 * * /home/alvis/agap_git/openai/backup-llm-dbs.sh >> /mnt/backups/openai-llm-dbs/backup.log 2>&1
@@ -21,42 +21,24 @@
set -euo pipefail
BACKUP_DIR="/mnt/backups/openai-llm-dbs"
ZABBIX_TOKEN_FILE="/root/.zabbix_token"
ZABBIX_URL="http://192.168.1.4:81/api_jsonrpc.php"
DATE=$(date '+%Y%m%d-%H%M')
DEST="$BACKUP_DIR/$DATE"
mkdir -p "$DEST"
notify_zabbix() {
local itemid="$1" label="$2"
if [[ -f "$ZABBIX_TOKEN_FILE" ]]; then
local token now_epoch
token=$(cat "$ZABBIX_TOKEN_FILE")
now_epoch=$(date '+%s')
env -u HTTPS_PROXY -u HTTP_PROXY -u ALL_PROXY -u https_proxy -u http_proxy -u all_proxy \
curl -s -X POST "$ZABBIX_URL" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $token" \
-d "{\"jsonrpc\":\"2.0\",\"method\":\"history.push\",\"id\":1,\"params\":{\"itemid\":\"$itemid\",\"value\":$now_epoch}}" > /dev/null \
&& echo "Zabbix notified ($label=$now_epoch)."
else
echo "WARNING: $ZABBIX_TOKEN_FILE not found -- skipped Zabbix freshness push for $label." >&2
fi
}
# Backup-freshness monitoring is now done via .age items (calculated fields showing
# age of the backup). The .ts (timestamp) trappers were unreliable (history.push not
# landing); removed in kb#189 in favor of .age overdue triggers.
# --- litellm-db ---
echo "Dumping litellm-db..."
docker exec litellm-db pg_dump -U litellm litellm | gzip > "$DEST/litellm-db.sql.gz"
echo "Dumped: litellm-db -> $DEST/litellm-db.sql.gz"
notify_zabbix "70637" "litellm.db.backup.ts"
# --- langfuse-db ---
echo "Dumping langfuse-db..."
docker exec langfuse-db pg_dump -U langfuse langfuse | gzip > "$DEST/langfuse-db.sql.gz"
echo "Dumped: langfuse-db -> $DEST/langfuse-db.sql.gz"
notify_zabbix "70638" "langfuse.db.backup.ts"
echo "$(date): Backup complete: $DEST"
ls -la "$DEST/"

View File

@@ -21,10 +21,11 @@ model_list:
model: ollama/bge-m3
api_base: http://host.docker.internal:11436
- model_name: judge
litellm_params:
model: anthropic/claude-haiku-4-5-20251001
api_key: os.environ/ANTHROPIC_API_KEY
# kb#164: the `judge` alias (anthropic/claude-haiku-4-5, metered) was removed
# 2026-07-30 by alvis's decision. No ANTHROPIC_API_KEY was ever set in this
# container or .env, so it could not spend; it was kept only as a latent
# paid-fallback footgun. Per design §3a (no metered API by default), do not
# re-add a metered deployment without an explicit opt-in decision.
# Kimi Code CLI agent (own container, own Moonshot/Kimi subscription via `kimi login`)
- model_name: kimi-agent

151
openai/migrate-adolf-state.sh Executable file
View File

@@ -0,0 +1,151 @@
#!/bin/bash
# Migration script for kb#219 — move Adolf's runtime state off the named
# Docker volume (openai_adolf-state) onto a host bind mount at
# /mnt/ssd/dbs/adolf/state, matching the convention every other Agap
# service already follows (hindsight, litellm, qdrant, langfuse, ...).
#
# SAFETY MODEL:
# - COPY ONLY. Never touches or deletes the source volume. The volume
# stays intact and usable as a rollback source until a human explicitly
# removes it (see rollback section in the compose-diff writeup /
# kb#219 report), long after this script has run and the container has
# been soak-tested on the new mount.
# - Dry-run by default. Pass --apply to actually copy.
# - Idempotent. Safe to re-run; re-copying onto an already-populated
# destination just refreshes it (cp -a overwrite-in-place). It will
# NOT delete files at the destination that were removed from the
# source between runs -- if that matters, wipe the dest dir yourself
# before re-running.
# - Verifies file counts + a sha256 manifest diff between source and
# destination before declaring success. Non-zero exit if they disagree.
# - Uses only `docker run` (alvis is in the `docker` group -- no `sudo`
# needed for container operations) to read the volume; never reads
# /var/lib/docker/volumes directly (root-only, 0700).
# - Does NOT create /mnt/ssd/dbs/adolf itself. That directory tree is
# root-owned (/mnt/ssd/dbs is 0755 root:root, same as every other
# service dir under it) and must be created + chowned by a human with
# sudo first -- see the paste-ready root block in the kb#219 report.
# This script aborts early with a clear message if the destination
# parent doesn't exist or isn't writable.
#
# USAGE:
# ./migrate-adolf-state.sh # dry run (default), prints plan
# ./migrate-adolf-state.sh --apply # actually copies + verifies
# ./migrate-adolf-state.sh --apply --dest /path/to/scratch --volume some-test-volume
# # point at a throwaway volume/dest for a trial run
#
# This script is NOT executed against live state as part of kb#219 prep.
# It has been dry-run tested and trial-run tested against a throwaway
# volume with a handful of files (see kb#219 report for the transcript).
set -euo pipefail
SRC_VOLUME="openai_adolf-state"
DEST_DIR="/mnt/ssd/dbs/adolf/state"
APPLY=0
while [ $# -gt 0 ]; do
case "$1" in
--apply) APPLY=1; shift ;;
--dest) DEST_DIR="$2"; shift 2 ;;
--volume) SRC_VOLUME="$2"; shift 2 ;;
-h|--help)
grep '^#' "$0" | sed 's/^#//'
exit 0
;;
*)
echo "Unknown argument: $1" >&2
exit 2
;;
esac
done
echo "== kb#219 adolf-state migration =="
echo "Source volume : $SRC_VOLUME"
echo "Dest dir : $DEST_DIR"
echo "Mode : $([ "$APPLY" -eq 1 ] && echo APPLY || echo DRY-RUN)"
echo
# --- 0. sanity: source volume exists ---
if ! docker volume inspect "$SRC_VOLUME" >/dev/null 2>&1; then
echo "ERROR: source volume '$SRC_VOLUME' does not exist." >&2
exit 1
fi
# --- 1. sanity: destination parent exists and is writable ---
DEST_PARENT="$(dirname "$DEST_DIR")"
if [ ! -d "$DEST_PARENT" ]; then
cat >&2 <<EOF
ERROR: $DEST_PARENT does not exist.
/mnt/ssd/dbs is root-owned; this directory must be created by a human
with sudo before this script can run. See the paste-ready root block in
the kb#219 report (creates /mnt/ssd/dbs/adolf/{state,config,personas},
chowned 1000:1000 to match the adolf container's node user).
EOF
exit 1
fi
if [ ! -w "$DEST_PARENT" ]; then
echo "ERROR: $DEST_PARENT exists but is not writable by $(whoami). Check ownership (should be chowned to your uid, or 1000:1000)." >&2
exit 1
fi
mkdir -p "$DEST_DIR"
# --- 2. source manifest (counts + sha256, computed inside a container) ---
echo "-- Computing source manifest (read-only mount of $SRC_VOLUME) --"
SRC_COUNT=$(docker run --rm -v "$SRC_VOLUME":/from:ro alpine sh -c "find /from -type f | wc -l")
echo "Source file count: $SRC_COUNT"
if [ "$APPLY" -eq 0 ]; then
echo
echo "[DRY RUN] Would copy $SRC_COUNT files from volume '$SRC_VOLUME' into $DEST_DIR,"
echo "[DRY RUN] then verify file count + sha256 manifest match."
echo "[DRY RUN] Re-run with --apply to actually copy."
exit 0
fi
# --- 3. copy (tar stream preserves ownership/perms across the boundary) ---
echo "-- Copying (tar stream, preserves perms/ownership) --"
docker run --rm \
-v "$SRC_VOLUME":/from:ro \
-v "$DEST_DIR":/to \
alpine sh -c "cd /from && tar cf - . | (cd /to && tar xf -)"
# --- 4. verify: file count ---
DEST_COUNT=$(docker run --rm -v "$DEST_DIR":/to:ro alpine sh -c "find /to -type f | wc -l")
echo "Dest file count: $DEST_COUNT"
if [ "$SRC_COUNT" != "$DEST_COUNT" ]; then
echo "ERROR: file count mismatch (source=$SRC_COUNT dest=$DEST_COUNT). NOT declaring success." >&2
exit 1
fi
# --- 5. verify: sha256 manifest diff ---
echo "-- Verifying sha256 manifests match --"
SRC_MANIFEST=$(mktemp)
DEST_MANIFEST=$(mktemp)
trap 'rm -f "$SRC_MANIFEST" "$DEST_MANIFEST"' EXIT
docker run --rm -v "$SRC_VOLUME":/from:ro alpine sh -c \
"cd /from && find . -type f -exec sha256sum {} \; | sort -k2" > "$SRC_MANIFEST"
docker run --rm -v "$DEST_DIR":/to:ro alpine sh -c \
"cd /to && find . -type f -exec sha256sum {} \; | sort -k2" > "$DEST_MANIFEST"
if diff -u "$SRC_MANIFEST" "$DEST_MANIFEST" > /tmp/adolf-state-migration.diff; then
echo "OK: manifests match byte-for-byte ($SRC_COUNT files)."
else
echo "ERROR: manifest mismatch, see /tmp/adolf-state-migration.diff" >&2
cat /tmp/adolf-state-migration.diff >&2
exit 1
fi
echo
echo "== Migration copy verified OK =="
echo "Source volume '$SRC_VOLUME' left untouched (not deleted, not modified)."
echo "Next steps (NOT done by this script -- human-supervised, see kb#219 report):"
echo " 1. Apply the docker-compose.yml bind-mount diff for the 'adolf' service."
echo " 2. docker compose -f openai/docker-compose.yml config -q # validate"
echo " 3. docker compose -f openai/docker-compose.yml up -d adolf # recreates container on new mount"
echo " 4. Verify: docker inspect adolf shows /mnt/ssd/dbs/adolf/state, not the volume;"
echo " Matrix session survives (no re-login), memory/config/persona intact."
echo " 5. Only after a soak period: docker volume rm $SRC_VOLUME"