#!/usr/bin/env bash # Clean-install contract for the server Pi-state parent bind and its read-only child mounts. set -euo pipefail root="$(cd "$(dirname "$0")/.." && pwd -P)" tmp_parent="${TMPDIR:-/tmp}" tmp_parent="${tmp_parent%/}" fixture="$(mktemp -d "$tmp_parent/thoth-server-pi-state.XXXXXX")" trap 'rm -rf "$fixture"' EXIT HUP INT TERM pi_state="$fixture/empty pi state" auth_config="$fixture/auth" mkdir -p "$pi_state" mkdir -p "$auth_config" chmod 0700 "$auth_config" printf 'mode: local\n' >"$auth_config/auth.yaml" chmod 0600 "$auth_config/auth.yaml" "$root/scripts/prepare-server-pi-state.sh" "$pi_state" "$(id -u)" "$(id -g)" for target in auth.json models.json settings.json; do path="$pi_state/agent/$target" [[ -f "$path" && ! -L "$path" ]] || { echo "server Pi-state initializer did not create regular target: $target" >&2 exit 1 } done printf '%s\n' preserved-placeholder >"$pi_state/agent/models.json" "$root/scripts/prepare-server-pi-state.sh" "$pi_state" "$(id -u)" "$(id -g)" [[ "$(cat "$pi_state/agent/models.json")" == preserved-placeholder ]] || { echo "server Pi-state initializer overwrote an existing target" >&2 exit 1 } printf '{}\n' >"$fixture/pi-auth.json" printf 'THT_MODEL_API_KEY=fixture-model-key\n' >"$fixture/thothii.secrets" printf 'fixture-session-password\n' >"$fixture/session-runtime-password" printf 'fixture-session-migrator-password\n' >"$fixture/session-migrator-password" printf 'fixture-session-ca\n' >"$fixture/session-ca.pem" cp "$root/deploy/workspaces/server-sessions.yaml.example" "$fixture/server-sessions.yaml" chmod 0600 "$fixture"/*.json "$fixture"/*.secrets "$fixture"/*password "$fixture"/*.pem cat >"$fixture/server.env" <"$fixture/rendered.json" node - "$fixture/rendered.json" "$pi_state" "$fixture/pi-auth.json" <<'NODE' const fs = require("fs"); const path = require("path"); const [renderedPath, piState, authSource] = process.argv.slice(2); const config = JSON.parse(fs.readFileSync(renderedPath, "utf8")); const core = config.services?.core; if (!core) throw new Error("server render lacks core"); const mounts = core.volumes || []; const parent = mounts.find((mount) => mount.target === "/home/thoth/.pi"); if (!parent || parent.type !== "bind" || parent.source !== piState || parent.read_only) { throw new Error("server Pi-state parent bind is not the expected writable root"); } const children = new Map(mounts .filter((mount) => mount.target?.startsWith("/home/thoth/.pi/agent/")) .map((mount) => [path.basename(mount.target), mount])); for (const name of ["auth.json", "models.json", "settings.json"]) { const mount = children.get(name); if (!mount || mount.type !== "bind" || !mount.read_only) { throw new Error(`server Pi agent child is not one read-only bind: ${name}`); } const hiddenTarget = path.join(piState, "agent", name); if (!fs.statSync(hiddenTarget).isFile()) { throw new Error(`server Pi-state root lacks nested target: ${name}`); } } if (children.get("auth.json").source !== authSource) { throw new Error("server Pi auth source changed while preparing nested targets"); } if (JSON.stringify(config).includes("fixture-model-key")) { throw new Error("server render leaked a secret value"); } NODE echo "clean empty-root server Pi-state render contract passed."