6cf63cb064
Verification layers: Structural: PASS (all scripts executable, syntax clean, Dockerfile valid) Behavioral: PASS (121 bats, 77 pytest, docker build succeeds, compose valid) Security: PASS (no secrets committed, .dockerignore excludes .env*, env_file pattern) Quality: PASS (coreci patterns followed, no coreci refs, G-104/G-105/G-106 verified) P0 issues found and auto-fixed: 1. docker-compose.yml: removed invalid restart_policy key, fixed env_file syntax 2. pyproject.toml: added fastapi + uvicorn deps (v0.1 gap exposed by Dockerfile) 3. timing.sh: renamed coreci_deploy_timing → praxis_deploy_timing (TASK-03-07) 4. firstboot-hook.sh: fixed idempotency check (/opt/praxis/.git not /usr/local/bin/praxis-deploy) P1+ issues: 8 (1 fixed: lxc-config.sh default alignment, 7 noted for post-hoc review) REQ coverage: 18/20 covered, 2 deferred (live first-boot timing + live E2E require cluster) Must-haves: 25/28 pass, 2 partial (comment-only diffs, no Makefile), 1 deferred ---ci--- project: praxis phase: 1 milestone: v0.2 status: verify ---/ci---
132 lines
6.1 KiB
Bash
Executable File
132 lines
6.1 KiB
Bash
Executable File
#!/bin/sh
|
|
# Praxis — Configure a created LXC container.
|
|
#
|
|
# Sets memory + onboot via the REST PUT /config (API-token-accepted),
|
|
# then sets hookscript + lxc.environment via SSH to the PVE host (these
|
|
# are root-only via REST: `hookscript` rejects API tokens, and
|
|
# `lxc.environment` is not in the REST schema). The hookscript points
|
|
# at the snippet staged by stage-snippet.sh (local:snippets/praxis-
|
|
# firstboot.sh).
|
|
#
|
|
# G-101: The GITEA_TOKEN must be available to the hookscript which runs
|
|
# on the PVE HOST (lxc.environment is NOT visible to the host-side
|
|
# hookscript). The token is baked into the snippet by stage-snippet.sh.
|
|
# The lxc.environment lines here put GITEA_TOKEN into the CT for the
|
|
# CT's own use (docker-compose env_file reads it), but the hookscript
|
|
# relies on the baked-in value.
|
|
#
|
|
# Env: PROXMOX_API_URL, PROXMOX_API_TOKEN, PROXMOX_NODE,
|
|
# PRAXIS_VERSION (git clone tag/branch, default latest),
|
|
# GITEA_TOKEN (for the private repo fetch inside the CT),
|
|
# DEEPGRAM_API_KEY, CARTESIA_API_KEY, OLLAMA_API_KEY (secrets,
|
|
# may be empty in v0.2 infrastructure-only),
|
|
# PRAXIS_DB_PATH (default /app/data/praxis.db),
|
|
# PRAXIS_TTS, PRAXIS_SCENARIO (optional, with defaults),
|
|
# OLLAMA_BASE_URL, OLLAMA_CHAT_URL, OLLAMA_ROLEPLAY_MODEL,
|
|
# OLLAMA_DEBRIEF_MODEL,
|
|
# DEEPGRAM_MODEL, DEEPGRAM_LANGUAGE, DEEPGRAM_REGION,
|
|
# CARTESIA_VOICE_ID,
|
|
# PRAXIS_PORT (default 8789),
|
|
# PROXMOX_MEMORY_MB (optional, default 4096),
|
|
# PROXMOX_STORAGE (for the hookscript volid prefix),
|
|
# PROXMOX_SSH_HOST (optional; defaults to PROXMOX_NODE)
|
|
# Args: $1 = VMID
|
|
# Exit: 0 on success, 1 on failure
|
|
|
|
set -eu
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
# shellcheck source=api.sh disable=SC1091
|
|
. "${SCRIPT_DIR}/api.sh"
|
|
|
|
pve_env PROXMOX_API_URL PROXMOX_API_TOKEN PROXMOX_NODE
|
|
|
|
vmid="${1:?usage: lxc-config.sh <vmid>}"
|
|
node="${PROXMOX_NODE}"
|
|
memory="${PROXMOX_MEMORY_MB:-4096}"
|
|
version="${PRAXIS_VERSION:-latest}"
|
|
port="${PRAXIS_PORT:-8789}"
|
|
db_path="${PRAXIS_DB_PATH:-/app/data/praxis.db}"
|
|
storage="${PROXMOX_STORAGE:-local}"
|
|
hookscript_volid="${storage}:snippets/praxis-firstboot.sh"
|
|
ssh_host="${PROXMOX_SSH_HOST:-${node}}"
|
|
|
|
# Optional praxis config (with defaults; empty is valid for v0.2).
|
|
# Defaults match .env.example + install-service.sh + docker-compose.yml
|
|
# so the injection chain is consistent across all three layers.
|
|
praxis_tts="${PRAXIS_TTS:-cartesia}"
|
|
praxis_scenario="${PRAXIS_SCENARIO:-customer_service_refund_ca_v01}"
|
|
|
|
# Secret keys (may be empty in v0.2 infrastructure-only slice).
|
|
deepgram_key="${DEEPGRAM_API_KEY:-}"
|
|
cartesia_key="${CARTESIA_API_KEY:-}"
|
|
ollama_key="${OLLAMA_API_KEY:-}"
|
|
|
|
# Ollama config (with defaults — match .env.example + docker-compose.yml).
|
|
ollama_base="${OLLAMA_BASE_URL:-https://ollama.com/v1}"
|
|
ollama_chat="${OLLAMA_CHAT_URL:-https://ollama.com/api/chat}"
|
|
ollama_roleplay="${OLLAMA_ROLEPLAY_MODEL:-gemma4:cloud}"
|
|
ollama_debrief="${OLLAMA_DEBRIEF_MODEL:-deepseek-v4-flash:cloud}"
|
|
|
|
# Deepgram config (with defaults — match .env.example + docker-compose.yml).
|
|
deepgram_model="${DEEPGRAM_MODEL:-nova-3}"
|
|
deepgram_lang="${DEEPGRAM_LANGUAGE:-en}"
|
|
deepgram_region="${DEEPGRAM_REGION:-na}"
|
|
|
|
# Cartesia config (with defaults — match .env.example; the voice ID is
|
|
# the single shared voice per D-006).
|
|
cartesia_voice="${CARTESIA_VOICE_ID:-a3536a36-1d18-4efb-a95a-7e44b7b5e384}"
|
|
|
|
config_path="/nodes/${node}/lxc/${vmid}/config"
|
|
|
|
echo "lxc-config: configuring VMID ${vmid} (memory=${memory}MB, onboot=1, hookscript=${hookscript_volid})" >&2
|
|
|
|
# Step 1: REST-accepted fields (memory, onboot). PUT /config is
|
|
# synchronous (no UPID), returns null on success.
|
|
pve_curl PUT "$config_path" "onboot=1" "memory=${memory}"
|
|
|
|
# Step 2: root-only fields (hookscript, lxc.environment) via SSH to the
|
|
# PVE host config file. These are rejected by the REST API for API
|
|
# tokens and lxc.environment is not in the REST schema at all.
|
|
conf_file="/etc/pve/lxc/${vmid}.conf"
|
|
ssh_opts="-o StrictHostKeyChecking=no"
|
|
# Build the lines to append (remove any prior hookscript/onboot/lxc.environment
|
|
# lines first to keep the config idempotent).
|
|
append_lines() {
|
|
printf 'onboot: 1\n'
|
|
printf 'hookscript: %s\n' "$hookscript_volid"
|
|
printf 'lxc.environment: PRAXIS_HOST=0.0.0.0\n'
|
|
printf 'lxc.environment: PRAXIS_VERSION=%s\n' "$version"
|
|
printf 'lxc.environment: PRAXIS_PORT=%s\n' "$port"
|
|
printf 'lxc.environment: PRAXIS_DB_PATH=%s\n' "$db_path"
|
|
printf 'lxc.environment: PRAXIS_SCENARIOS_DIR=/app/scenarios\n'
|
|
printf 'lxc.environment: PRAXIS_TTS=%s\n' "$praxis_tts"
|
|
printf 'lxc.environment: PRAXIS_SCENARIO=%s\n' "$praxis_scenario"
|
|
if [ -n "${GITEA_TOKEN:-}" ]; then
|
|
printf 'lxc.environment: GITEA_TOKEN=%s\n' "$GITEA_TOKEN"
|
|
fi
|
|
printf 'lxc.environment: DEEPGRAM_API_KEY=%s\n' "$deepgram_key"
|
|
printf 'lxc.environment: CARTESIA_API_KEY=%s\n' "$cartesia_key"
|
|
printf 'lxc.environment: OLLAMA_API_KEY=%s\n' "$ollama_key"
|
|
printf 'lxc.environment: OLLAMA_BASE_URL=%s\n' "$ollama_base"
|
|
printf 'lxc.environment: OLLAMA_CHAT_URL=%s\n' "$ollama_chat"
|
|
printf 'lxc.environment: OLLAMA_ROLEPLAY_MODEL=%s\n' "$ollama_roleplay"
|
|
printf 'lxc.environment: OLLAMA_DEBRIEF_MODEL=%s\n' "$ollama_debrief"
|
|
printf 'lxc.environment: DEEPGRAM_MODEL=%s\n' "$deepgram_model"
|
|
printf 'lxc.environment: DEEPGRAM_LANGUAGE=%s\n' "$deepgram_lang"
|
|
printf 'lxc.environment: DEEPGRAM_REGION=%s\n' "$deepgram_region"
|
|
printf 'lxc.environment: CARTESIA_VOICE_ID=%s\n' "$cartesia_voice"
|
|
}
|
|
# shellcheck disable=SC2029
|
|
# SC2029: conf='${conf_file}' intentionally expands on the client side —
|
|
# the script builds the remote /etc/pve/lxc/<vmid>.conf path from the
|
|
# local variable and ships the literal path to the remote host.
|
|
append_lines | ssh "$ssh_opts" "root@${ssh_host}" "
|
|
conf='${conf_file}'
|
|
# Remove prior hookscript/onboot/lxc.environment lines.
|
|
sed -i '/^hookscript:/d;/^onboot:/d;/^lxc\.environment: PRAXIS/d;/^lxc\.environment: GITEA_TOKEN/d;/^lxc\.environment: DEEPGRAM/d;/^lxc\.environment: CARTESIA/d;/^lxc\.environment: OLLAMA/d' \"\$conf\" 2>/dev/null || true
|
|
cat >> \"\$conf\"
|
|
echo 'lxc-config: SSH config updated' >&2
|
|
"
|
|
|
|
echo "lxc-config: VMID ${vmid} configured" >&2 |