Fix: User rights and sandboxing problems.
This commit is contained in:
@@ -5,6 +5,10 @@
|
||||
# --- LLM ENGINE & MODEL SELECTION ---
|
||||
MODEL_LIST=gemma4:e4b,qwen3.6:35b-a3b-q4_K_M
|
||||
OLLAMA_KEEP_ALIVE=-1
|
||||
# KV cache quantization for large contexts (recommended on desktop GPUs): q8_0 or q4_0
|
||||
# OLLAMA_KV_CACHE_TYPE=q8_0
|
||||
# Reserve VRAM for display/compositor to avoid amdgpu allocation failures (bytes)
|
||||
# OLLAMA_GPU_OVERHEAD=4294967296
|
||||
|
||||
# --- HARDWARE RESOURCES ---
|
||||
# GPU Vendor (e.g. "amd", "nvidia", "intel")
|
||||
@@ -25,3 +29,18 @@ HOST_USER_ID=1000
|
||||
HOST_OPENCLAW_JSON_PATH=./openclaw/config/openclaw.json
|
||||
# Local path for the AI workspace (persistent storage for agent's work)
|
||||
HOST_WORKSPACE_PATH=./storage/workspace
|
||||
# Local path for additional repositories/code visible to the agent
|
||||
HOST_REPOS_PATH=.
|
||||
|
||||
# --- OPTIONAL RUNTIME OVERRIDES ---
|
||||
# Container engine used by ghostnet.sh (podman|docker)
|
||||
CONTAINER_ENGINE=podman
|
||||
# Optional explicit socket path for sandbox container creation API
|
||||
# HOST_DOCKER_SOCKET_PATH=/run/user/1000/podman/podman.sock
|
||||
# Optional explicit mount options for openclaw.json (default podman: :ro,Z | docker: :ro)
|
||||
# HOST_OPENCLAW_CONFIG_MOUNT_OPTS=:ro,Z
|
||||
# Optional explicit host workspace path seen by sandbox runtime
|
||||
# OPENCLAW_SANDBOX_HOST_WORKSPACE=/absolute/path/to/storage/workspace
|
||||
|
||||
# Sandbox base image is auto-built by ghostnet.sh as
|
||||
# localhost/ghostnet-openclaw_sandbox:latest to include rootless apt defaults.
|
||||
|
||||
+16
-7
@@ -25,7 +25,11 @@ services:
|
||||
- MODEL_LIST=${MODEL_LIST}
|
||||
- OLLAMA_KEEP_ALIVE=${OLLAMA_KEEP_ALIVE}
|
||||
- HSA_OVERRIDE_GFX_VERSION=11.0.0
|
||||
- OLLAMA_FLASH_ATTENTION=1
|
||||
- OLLAMA_FLASH_ATTENTION=${OLLAMA_FLASH_ATTENTION:-0}
|
||||
- OLLAMA_KV_CACHE_TYPE=${OLLAMA_KV_CACHE_TYPE:-q8_0}
|
||||
- OLLAMA_GPU_OVERHEAD=${OLLAMA_GPU_OVERHEAD:-4294967296}
|
||||
- OLLAMA_MAX_LOADED_MODELS=${OLLAMA_MAX_LOADED_MODELS:-1}
|
||||
- OLLAMA_NUM_PARALLEL=${OLLAMA_NUM_PARALLEL:-1}
|
||||
- http_proxy=http://ollama-proxy:3128
|
||||
- https_proxy=http://ollama-proxy:3128
|
||||
- no_proxy=localhost,127.0.0.1,0.0.0.0,ollama,openclaw-agent,tool-router,openclaw-ollama-bridge
|
||||
@@ -56,20 +60,25 @@ services:
|
||||
context: ./openclaw
|
||||
dockerfile: Containerfile
|
||||
container_name: openclaw
|
||||
userns_mode: "keep-id"
|
||||
user: "0:0"
|
||||
environment:
|
||||
- OPENCLAW_GATEWAY_MODE=local
|
||||
- OPENCLAW_GATEWAY_PASSWORD=${OPENCLAW_PASSWORD}
|
||||
- OPENCLAW_CONFIG_PATH=/home/node/.openclaw/openclaw.json
|
||||
- OPENCLAW_STATE_DIR=/home/node/.openclaw
|
||||
- OLLAMA_URL=http://127.0.0.1:11434
|
||||
- OLLAMA_API_KEY=ollama-local
|
||||
- DOCKER_HOST=unix:///var/run/docker.sock
|
||||
- OPENCLAW_SANDBOX_UIDSHIFT=${OPENCLAW_SANDBOX_UIDSHIFT}
|
||||
- OPENCLAW_SANDBOX_HOST_WORKSPACE=${OPENCLAW_SANDBOX_HOST_WORKSPACE}
|
||||
- OPENCLAW_SANDBOX_CONTAINER_WORKSPACE=${OPENCLAW_SANDBOX_CONTAINER_WORKSPACE}
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- ${HOST_OPENCLAW_JSON_PATH}:/home/node/.openclaw/openclaw.json:Z
|
||||
- ${HOST_WORKSPACE_PATH}:/home/node/.openclaw/workspace:Z,U
|
||||
- /run/user/${HOST_USER_ID}/podman/podman.sock:/var/run/docker.sock:Z
|
||||
- ${HOST_REPOS_PATH}:/repos:Z,U
|
||||
- ${HOST_OPENCLAW_JSON_PATH}:/home/node/.openclaw/openclaw.json${HOST_OPENCLAW_CONFIG_MOUNT_OPTS}
|
||||
- ${HOST_WORKSPACE_PATH}:/home/node/.openclaw/workspace${HOST_OPENCLAW_WORKSPACE_MOUNT_OPTS}
|
||||
- ${HOST_DOCKER_SOCKET_PATH}:/var/run/docker.sock${HOST_DOCKER_SOCKET_MOUNT_OPTS}
|
||||
- ${HOST_REPOS_PATH}:/repos${HOST_REPOS_MOUNT_OPTS}
|
||||
networks:
|
||||
- net.ghost.openclaw
|
||||
depends_on:
|
||||
@@ -154,7 +163,7 @@ services:
|
||||
restart: always
|
||||
user: "1000:1000"
|
||||
volumes:
|
||||
- ./storage/searxng_valkey:/data:Z,U
|
||||
- ./storage/searxng_valkey:/data${HOST_RW_MOUNT_OPTS}
|
||||
networks:
|
||||
- net.ghost.tools
|
||||
|
||||
|
||||
+14
-5
@@ -29,22 +29,31 @@ services:
|
||||
|
||||
# --- CORE AGENT (GATEWAY) ---
|
||||
agent:
|
||||
image: ghcr.io/openclaw/openclaw:latest
|
||||
build:
|
||||
context: ./openclaw
|
||||
dockerfile: Containerfile
|
||||
container_name: openclaw-agent
|
||||
user: "0:0"
|
||||
depends_on:
|
||||
- ollama
|
||||
userns_mode: "keep-id"
|
||||
environment:
|
||||
- OPENCLAW_GATEWAY_MODE=local
|
||||
- OPENCLAW_GATEWAY_PASSWORD=${OPENCLAW_PASSWORD}
|
||||
- OPENCLAW_CONFIG_PATH=/home/node/.openclaw/openclaw.json
|
||||
- OPENCLAW_STATE_DIR=/home/node/.openclaw
|
||||
- OLLAMA_URL=http://127.0.0.1:11434
|
||||
- OLLAMA_API_KEY=ollama-local
|
||||
- DOCKER_HOST=unix:///var/run/docker.sock
|
||||
- OPENCLAW_SANDBOX_UIDSHIFT=${OPENCLAW_SANDBOX_UIDSHIFT}
|
||||
- OPENCLAW_SANDBOX_HOST_WORKSPACE=${OPENCLAW_SANDBOX_HOST_WORKSPACE}
|
||||
- OPENCLAW_SANDBOX_CONTAINER_WORKSPACE=${OPENCLAW_SANDBOX_CONTAINER_WORKSPACE}
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- ${HOST_OPENCLAW_JSON_PATH}:/home/node/.openclaw/openclaw.json:Z
|
||||
- ${HOST_WORKSPACE_PATH}:/home/node/.openclaw/workspace:Z,U
|
||||
- ${HOST_REPOS_PATH}:/repos:Z,U
|
||||
- ${HOST_OPENCLAW_JSON_PATH}:/home/node/.openclaw/openclaw.json${HOST_OPENCLAW_CONFIG_MOUNT_OPTS}
|
||||
- ${HOST_WORKSPACE_PATH}:/home/node/.openclaw/workspace${HOST_OPENCLAW_WORKSPACE_MOUNT_OPTS}
|
||||
- ${HOST_DOCKER_SOCKET_PATH}:/var/run/docker.sock${HOST_DOCKER_SOCKET_MOUNT_OPTS}
|
||||
- ${HOST_REPOS_PATH}:/repos${HOST_REPOS_MOUNT_OPTS}
|
||||
networks:
|
||||
- net.ghost.openclaw
|
||||
|
||||
|
||||
+14
-5
@@ -27,22 +27,31 @@ services:
|
||||
|
||||
# --- CORE AGENT (GATEWAY) ---
|
||||
agent:
|
||||
image: ghcr.io/openclaw/openclaw:latest
|
||||
build:
|
||||
context: ./openclaw
|
||||
dockerfile: Containerfile
|
||||
container_name: openclaw-agent
|
||||
user: "0:0"
|
||||
depends_on:
|
||||
- ollama
|
||||
userns_mode: "keep-id"
|
||||
environment:
|
||||
- OPENCLAW_GATEWAY_MODE=local
|
||||
- OPENCLAW_GATEWAY_PASSWORD=${OPENCLAW_PASSWORD}
|
||||
- OPENCLAW_CONFIG_PATH=/home/node/.openclaw/openclaw.json
|
||||
- OPENCLAW_STATE_DIR=/home/node/.openclaw
|
||||
- OLLAMA_URL=http://127.0.0.1:11434
|
||||
- OLLAMA_API_KEY=ollama-local
|
||||
- DOCKER_HOST=unix:///var/run/docker.sock
|
||||
- OPENCLAW_SANDBOX_UIDSHIFT=${OPENCLAW_SANDBOX_UIDSHIFT}
|
||||
- OPENCLAW_SANDBOX_HOST_WORKSPACE=${OPENCLAW_SANDBOX_HOST_WORKSPACE}
|
||||
- OPENCLAW_SANDBOX_CONTAINER_WORKSPACE=${OPENCLAW_SANDBOX_CONTAINER_WORKSPACE}
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- ${HOST_OPENCLAW_JSON_PATH}:/home/node/.openclaw/openclaw.json:Z
|
||||
- ${HOST_WORKSPACE_PATH}:/home/node/.openclaw/workspace:Z,U
|
||||
- ${HOST_REPOS_PATH}:/repos:Z,U
|
||||
- ${HOST_OPENCLAW_JSON_PATH}:/home/node/.openclaw/openclaw.json${HOST_OPENCLAW_CONFIG_MOUNT_OPTS}
|
||||
- ${HOST_WORKSPACE_PATH}:/home/node/.openclaw/workspace${HOST_OPENCLAW_WORKSPACE_MOUNT_OPTS}
|
||||
- ${HOST_DOCKER_SOCKET_PATH}:/var/run/docker.sock${HOST_DOCKER_SOCKET_MOUNT_OPTS}
|
||||
- ${HOST_REPOS_PATH}:/repos${HOST_REPOS_MOUNT_OPTS}
|
||||
networks:
|
||||
- net.ghost.openclaw
|
||||
|
||||
|
||||
+151
-25
@@ -1,67 +1,193 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Path to the .env file
|
||||
ENV_FILE="./.env"
|
||||
set -euo pipefail
|
||||
|
||||
# 1. Determine GPU_TYPE: Shell-Env first, then .env file
|
||||
if [ -z "$GPU_TYPE" ]; then
|
||||
if [ -f "$ENV_FILE" ]; then
|
||||
GPU_TYPE=$(grep '^GPU_TYPE=' "$ENV_FILE" | cut -d '=' -f2)
|
||||
PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ENV_FILE="$PROJECT_DIR/.env"
|
||||
|
||||
read_env_var() {
|
||||
local key="$1"
|
||||
if [ ! -f "$ENV_FILE" ]; then
|
||||
return 0
|
||||
fi
|
||||
local line
|
||||
line=$(grep -E "^${key}=" "$ENV_FILE" | tail -n 1 || true)
|
||||
if [ -n "$line" ]; then
|
||||
printf '%s' "${line#*=}"
|
||||
fi
|
||||
}
|
||||
|
||||
resolve_abs_path() {
|
||||
local raw_path="$1"
|
||||
if [[ "$raw_path" = /* ]]; then
|
||||
printf '%s' "$raw_path"
|
||||
return
|
||||
fi
|
||||
if command -v realpath >/dev/null 2>&1; then
|
||||
realpath -m "$PROJECT_DIR/$raw_path"
|
||||
return
|
||||
fi
|
||||
(cd "$PROJECT_DIR" && printf '%s/%s\n' "$PWD" "${raw_path#./}")
|
||||
}
|
||||
|
||||
if [ -z "${GPU_TYPE:-}" ]; then
|
||||
GPU_TYPE="$(read_env_var GPU_TYPE)"
|
||||
fi
|
||||
|
||||
# 2. Validation: Is the variable now set?
|
||||
if [ -z "$GPU_TYPE" ]; then
|
||||
if [ -z "${GPU_TYPE:-}" ]; then
|
||||
echo "ERROR: 'GPU_TYPE' is not defined!"
|
||||
echo "💡 Please set it in your shell (export GPU_TYPE=amd) or create an .env file."
|
||||
echo "Please set it in your shell (export GPU_TYPE=amd) or create an .env file."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 3. Validation: Is the value allowed?
|
||||
case "$GPU_TYPE" in
|
||||
amd|intel|nvidia)
|
||||
COMPOSE_FILE="compose.$GPU_TYPE.yaml"
|
||||
COMPOSE_FILE="$PROJECT_DIR/compose.$GPU_TYPE.yaml"
|
||||
;;
|
||||
*)
|
||||
echo "ERROR: Invalid GPU_TYPE '$GPU_TYPE'!"
|
||||
echo "Allowed values are: amd, intel, nvidia"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# 4. Check if YAML file exists
|
||||
if [ ! -f "$COMPOSE_FILE" ]; then
|
||||
echo "ERROR: The file '$COMPOSE_FILE' was not found!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 5. Execute the command based on user input
|
||||
COMMAND=$1
|
||||
if [ -z "${CONTAINER_ENGINE:-}" ]; then
|
||||
CONTAINER_ENGINE="$(read_env_var CONTAINER_ENGINE)"
|
||||
fi
|
||||
|
||||
if [ -z "${CONTAINER_ENGINE:-}" ]; then
|
||||
if command -v podman-compose >/dev/null 2>&1; then
|
||||
CONTAINER_ENGINE="podman"
|
||||
elif command -v docker >/dev/null 2>&1 && docker compose version >/dev/null 2>&1; then
|
||||
CONTAINER_ENGINE="docker"
|
||||
else
|
||||
echo "ERROR: Neither podman-compose nor docker compose is available."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
case "$CONTAINER_ENGINE" in
|
||||
podman)
|
||||
COMPOSE_CMD=(podman-compose)
|
||||
ENGINE_CMD=(podman)
|
||||
;;
|
||||
docker)
|
||||
COMPOSE_CMD=(docker compose)
|
||||
ENGINE_CMD=(docker)
|
||||
;;
|
||||
*)
|
||||
echo "ERROR: Invalid CONTAINER_ENGINE '$CONTAINER_ENGINE'. Use 'podman' or 'docker'."
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ -z "${HOST_USER_ID:-}" ]; then
|
||||
HOST_USER_ID="$(read_env_var HOST_USER_ID)"
|
||||
fi
|
||||
if [ -z "${HOST_USER_ID:-}" ]; then
|
||||
HOST_USER_ID="$(id -u)"
|
||||
fi
|
||||
export HOST_USER_ID
|
||||
|
||||
if [ -z "${HOST_WORKSPACE_PATH:-}" ]; then
|
||||
HOST_WORKSPACE_PATH="$(read_env_var HOST_WORKSPACE_PATH)"
|
||||
fi
|
||||
if [ -z "${HOST_WORKSPACE_PATH:-}" ]; then
|
||||
HOST_WORKSPACE_PATH="./storage/workspace"
|
||||
fi
|
||||
export HOST_WORKSPACE_PATH
|
||||
|
||||
if [ -z "${HOST_REPOS_PATH:-}" ]; then
|
||||
HOST_REPOS_PATH="$(read_env_var HOST_REPOS_PATH)"
|
||||
fi
|
||||
if [ -z "${HOST_REPOS_PATH:-}" ]; then
|
||||
HOST_REPOS_PATH="$PROJECT_DIR"
|
||||
fi
|
||||
export HOST_REPOS_PATH
|
||||
|
||||
HOST_WORKSPACE_ABS="$(resolve_abs_path "$HOST_WORKSPACE_PATH")"
|
||||
|
||||
if [ "$CONTAINER_ENGINE" = "podman" ]; then
|
||||
: "${HOST_DOCKER_SOCKET_PATH:=/run/user/${HOST_USER_ID}/podman/podman.sock}"
|
||||
: "${HOST_DOCKER_SOCKET_MOUNT_OPTS:=:Z}"
|
||||
: "${HOST_OPENCLAW_CONFIG_MOUNT_OPTS:=:ro,Z}"
|
||||
: "${HOST_OPENCLAW_WORKSPACE_MOUNT_OPTS:=:Z}"
|
||||
: "${HOST_REPOS_MOUNT_OPTS:=:Z}"
|
||||
: "${HOST_RW_MOUNT_OPTS:=:Z,U}"
|
||||
: "${OPENCLAW_SANDBOX_UIDSHIFT:=1}"
|
||||
else
|
||||
: "${HOST_DOCKER_SOCKET_PATH:=/var/run/docker.sock}"
|
||||
: "${HOST_DOCKER_SOCKET_MOUNT_OPTS:=}"
|
||||
: "${HOST_OPENCLAW_CONFIG_MOUNT_OPTS:=:ro}"
|
||||
: "${HOST_OPENCLAW_WORKSPACE_MOUNT_OPTS:=}"
|
||||
: "${HOST_REPOS_MOUNT_OPTS:=}"
|
||||
: "${HOST_RW_MOUNT_OPTS:=}"
|
||||
: "${OPENCLAW_SANDBOX_UIDSHIFT:=0}"
|
||||
fi
|
||||
|
||||
: "${OPENCLAW_SANDBOX_HOST_WORKSPACE:=$HOST_WORKSPACE_ABS}"
|
||||
: "${OPENCLAW_SANDBOX_CONTAINER_WORKSPACE:=/home/node/.openclaw/workspace}"
|
||||
|
||||
export HOST_DOCKER_SOCKET_PATH
|
||||
export HOST_DOCKER_SOCKET_MOUNT_OPTS
|
||||
export HOST_OPENCLAW_CONFIG_MOUNT_OPTS
|
||||
export HOST_OPENCLAW_WORKSPACE_MOUNT_OPTS
|
||||
export HOST_REPOS_MOUNT_OPTS
|
||||
export HOST_RW_MOUNT_OPTS
|
||||
export OPENCLAW_SANDBOX_UIDSHIFT
|
||||
export OPENCLAW_SANDBOX_HOST_WORKSPACE
|
||||
export OPENCLAW_SANDBOX_CONTAINER_WORKSPACE
|
||||
|
||||
compose() {
|
||||
"${COMPOSE_CMD[@]}" -f "$COMPOSE_FILE" "$@"
|
||||
}
|
||||
|
||||
build_sandbox_image() {
|
||||
local sandbox_tag="localhost/ghostnet-openclaw_sandbox:latest"
|
||||
echo "Building sandbox base image ($sandbox_tag) with ${ENGINE_CMD[*]}..."
|
||||
"${ENGINE_CMD[@]}" build \
|
||||
-t "$sandbox_tag" \
|
||||
-f "$PROJECT_DIR/openclaw/sandbox/Containerfile" \
|
||||
"$PROJECT_DIR/openclaw/sandbox"
|
||||
}
|
||||
|
||||
COMMAND="${1:-}"
|
||||
|
||||
cd "$PROJECT_DIR"
|
||||
|
||||
case "$COMMAND" in
|
||||
up)
|
||||
echo "🚀 Starting Ghostnet ($GPU_TYPE)..."
|
||||
podman-compose -f "$COMPOSE_FILE" up -d
|
||||
echo "Starting Ghostnet ($GPU_TYPE) with $CONTAINER_ENGINE..."
|
||||
build_sandbox_image
|
||||
compose up -d
|
||||
;;
|
||||
down)
|
||||
echo "🛑 Stopping Ghostnet ($GPU_TYPE)..."
|
||||
podman-compose -f "$COMPOSE_FILE" down
|
||||
echo "Stopping Ghostnet ($GPU_TYPE) with $CONTAINER_ENGINE..."
|
||||
compose down
|
||||
;;
|
||||
restart)
|
||||
echo "🔄 Restarting Ghostnet ($GPU_TYPE)..."
|
||||
podman-compose -f "$COMPOSE_FILE" restart
|
||||
echo "Restarting Ghostnet ($GPU_TYPE) with $CONTAINER_ENGINE..."
|
||||
compose restart
|
||||
;;
|
||||
logs)
|
||||
podman-compose -f "$COMPOSE_FILE" logs -f
|
||||
compose logs -f
|
||||
;;
|
||||
attached)
|
||||
echo "🔍 Checking for running instances..."
|
||||
podman-compose -f "$COMPOSE_FILE" down > /dev/null 2>&1
|
||||
echo "🔍 Starting Ghostnet in foreground ($GPU_TYPE)..."
|
||||
podman-compose -f "$COMPOSE_FILE" up
|
||||
echo "Checking for running instances..."
|
||||
compose down > /dev/null 2>&1 || true
|
||||
echo "Starting Ghostnet in foreground ($GPU_TYPE) with $CONTAINER_ENGINE..."
|
||||
build_sandbox_image
|
||||
compose up
|
||||
;;
|
||||
*)
|
||||
echo "Usage: ./ghostnet.sh [up|down|restart|logs|attached]"
|
||||
echo "Current environment: $GPU_TYPE"
|
||||
echo "Container engine: $CONTAINER_ENGINE"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
@@ -0,0 +1,22 @@
|
||||
# Basis-Modell aus der Registry laden
|
||||
FROM qwen3.6:27b
|
||||
|
||||
# Long-Context-Profil fuer umfangreiche Edits
|
||||
PARAMETER num_ctx 16384
|
||||
PARAMETER num_predict 4096
|
||||
PARAMETER temperature 0.2
|
||||
|
||||
# Dem Modell seine primäre Identität verpassen
|
||||
SYSTEM """
|
||||
Du bist ein hochentwickelter, autonomer KI-Coding-Agent innerhalb des OpenClaw-Frameworks.
|
||||
Deine Aufgaben:
|
||||
- Schreibe sauberen, performanten und fehlerfreien Python- und C#-Code.
|
||||
- Analysiere Code-Strukturen tiefgehend und halte dich strikt an Best Practices.
|
||||
- Gib präzise Antworten und vermeide unnoetiges Blabla.
|
||||
|
||||
Arbeitsregeln:
|
||||
- Simuliere keine Terminal-Ausgaben, keine "Let me check..."-Monologe und keine erfundenen Befehlslogs.
|
||||
- Nutze verfügbare Tools/Befehle tatsächlich und berichte nur echte Ergebnisse.
|
||||
- Wenn etwas nicht fertig ist, nenne den konkreten nächsten Schritt statt langer Zwischenkommentare.
|
||||
- Antworte standardmäßig auf Deutsch, außer der Nutzer fordert explizit etwas anderes.
|
||||
"""
|
||||
@@ -2,14 +2,10 @@
|
||||
FROM qwen3.6:27b
|
||||
|
||||
# VRAM- und Kontext-Parameter direkt einbacken
|
||||
PARAMETER num_ctx 32768
|
||||
PARAMETER num_ctx 8192
|
||||
PARAMETER num_predict 4096
|
||||
PARAMETER temperature 0.2
|
||||
|
||||
# --- Stop-Token für saubere Agenten-Kommunikation ---
|
||||
PARAMETER stop <|im_end|>
|
||||
PARAMETER stop <|im_start|>
|
||||
|
||||
# Dem Modell seine primäre Identität verpassen
|
||||
SYSTEM """
|
||||
Du bist ein hochentwickelter, autonomer KI-Coding-Agent innerhalb des OpenClaw-Frameworks.
|
||||
@@ -17,4 +13,10 @@ Deine Aufgaben:
|
||||
- Schreibe sauberen, performanten und fehlerfreien Python- und C#-Code.
|
||||
- Analysiere Code-Strukturen tiefgehend und halte dich strikt an Best Practices.
|
||||
- Gib präzise Antworten und vermeide unnötiges Blabla.
|
||||
|
||||
Arbeitsregeln:
|
||||
- Simuliere keine Terminal-Ausgaben, keine "Let me check..."-Monologe und keine erfundenen Befehlslogs.
|
||||
- Nutze verfügbare Tools/Befehle tatsächlich und berichte nur echte Ergebnisse.
|
||||
- Wenn etwas nicht fertig ist, nenne den konkreten nächsten Schritt statt langer Zwischenkommentare.
|
||||
- Antworte standardmäßig auf Deutsch, außer der Nutzer fordert explizit etwas anderes.
|
||||
"""
|
||||
|
||||
@@ -6,4 +6,63 @@ RUN apt-get update \
|
||||
&& apt-get install -y docker.io \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN set -eu; \
|
||||
cat >/tmp/patch-openclaw-sandbox.js <<'EOF'
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const distDir = "/app/dist";
|
||||
const files = fs
|
||||
.readdirSync(distDir)
|
||||
.filter((name) => /^docker-.*\.js$/.test(name))
|
||||
.map((name) => path.join(distDir, name));
|
||||
|
||||
if (!files.length) {
|
||||
throw new Error("No docker-*.js bundles found in /app/dist");
|
||||
}
|
||||
|
||||
const bindNeedle =
|
||||
'return `${params.hostPath}:${params.containerPath}:${params.readOnly ? "ro,z" : "z"}`;';
|
||||
const bindReplacement =
|
||||
'const useUidShift = process.env.OPENCLAW_SANDBOX_UIDSHIFT === "1";\n' +
|
||||
' const hostWorkspaceRoot = process.env.OPENCLAW_SANDBOX_HOST_WORKSPACE?.trim();\n' +
|
||||
' const containerWorkspaceRoot = process.env.OPENCLAW_SANDBOX_CONTAINER_WORKSPACE?.trim() || "/home/node/.openclaw/workspace";\n' +
|
||||
' let hostPath = params.hostPath;\n' +
|
||||
' if (hostWorkspaceRoot && (hostPath === containerWorkspaceRoot || hostPath.startsWith(`${containerWorkspaceRoot}/`))) hostPath = `${hostWorkspaceRoot}${hostPath.slice(containerWorkspaceRoot.length)}`;\n' +
|
||||
' return `${hostPath}:${params.containerPath}:${params.readOnly ? useUidShift ? "ro,U,z" : "ro,z" : useUidShift ? "U,z" : "z"}`;';
|
||||
|
||||
let patchedFiles = 0;
|
||||
|
||||
for (const filePath of files) {
|
||||
const original = fs.readFileSync(filePath, "utf8");
|
||||
let patched = original;
|
||||
|
||||
if (!patched.includes(bindNeedle)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
patched = patched.replace(bindNeedle, bindReplacement);
|
||||
|
||||
if (patched === original) {
|
||||
continue;
|
||||
}
|
||||
|
||||
fs.writeFileSync(filePath, patched, "utf8");
|
||||
patchedFiles += 1;
|
||||
}
|
||||
|
||||
if (!patchedFiles) {
|
||||
throw new Error("OpenClaw dist patch failed: expected patterns not found in docker bundles");
|
||||
}
|
||||
|
||||
console.log(`Patched ${patchedFiles} OpenClaw docker dist bundle(s).`);
|
||||
EOF
|
||||
|
||||
RUN node /tmp/patch-openclaw-sandbox.js \
|
||||
&& rm -f /tmp/patch-openclaw-sandbox.js
|
||||
|
||||
ENV OPENCLAW_SANDBOX_UIDSHIFT=0
|
||||
ENV OPENCLAW_SANDBOX_HOST_WORKSPACE=
|
||||
ENV OPENCLAW_SANDBOX_CONTAINER_WORKSPACE=/home/node/.openclaw/workspace
|
||||
|
||||
USER node
|
||||
|
||||
@@ -15,8 +15,10 @@
|
||||
"mode": "all",
|
||||
"workspaceAccess": "rw",
|
||||
"docker": {
|
||||
"image": "docker.io/nikolaik/python-nodejs:python3.14-nodejs26-slim",
|
||||
"image": "localhost/ghostnet-openclaw_sandbox:latest",
|
||||
"network": "net.ghost.tools",
|
||||
"readOnlyRoot": false,
|
||||
"user": "root",
|
||||
"pidsLimit": 2048
|
||||
}
|
||||
}
|
||||
@@ -30,4 +32,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
FROM docker.io/nikolaik/python-nodejs:python3.14-nodejs26-slim
|
||||
|
||||
USER root
|
||||
|
||||
# Rootless Podman may block setgroups/seteuid transitions used by apt's default
|
||||
# _apt sandbox user. Force apt to stay as root inside the sandbox container.
|
||||
RUN mkdir -p /tmp/apt-archives/partial \
|
||||
&& chmod 755 /tmp/apt-archives /tmp/apt-archives/partial \
|
||||
&& printf 'APT::Sandbox::User "root";\nDir::Cache::archives "/tmp/apt-archives";\n' > /etc/apt/apt.conf.d/99rootless
|
||||
|
||||
USER root
|
||||
Executable
+450
@@ -0,0 +1,450 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "$PROJECT_DIR"
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
ok() {
|
||||
echo -e "${GREEN}[OK]${NC} $1"
|
||||
}
|
||||
|
||||
warn() {
|
||||
echo -e "${YELLOW}[WARN]${NC} $1"
|
||||
}
|
||||
|
||||
fail() {
|
||||
echo -e "${RED}[FAIL]${NC} $1"
|
||||
exit 1
|
||||
}
|
||||
|
||||
need_cmd() {
|
||||
command -v "$1" >/dev/null 2>&1 || fail "Benoetigtes Kommando fehlt: $1"
|
||||
}
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: ./scripts/benchmark-stack.sh [options]
|
||||
|
||||
Startet den Stack (optional), fuehrt eine reproduzierbare OpenClaw-Anfrage aus,
|
||||
validiert den Output und zeigt Metriken (Laufzeit, Token, KV-/VRAM-Metriken).
|
||||
|
||||
Optionen:
|
||||
--model <name> OpenClaw-Modell (default: ollama/qwen-coding-long:latest)
|
||||
--lines <n> Erwartete Zeilenanzahl LINE_1..LINE_n (default: 120)
|
||||
--timeout-sec <n> Timeout fuer den Agent-Call in Sekunden (default: 480)
|
||||
--session-key <key> Session-Key fuer den Lauf (default: auto)
|
||||
--artifact-dir <path> Zielverzeichnis fuer Ergebnisdateien (default: auto)
|
||||
--no-start Stack nicht starten, nur vorhandenen Stack nutzen
|
||||
--no-reload Modell vor Lauf nicht mit 'ollama stop' entladen
|
||||
-h, --help Hilfe anzeigen
|
||||
|
||||
Beispiel:
|
||||
./scripts/benchmark-stack.sh --model ollama/qwen-coding-long:latest --lines 140
|
||||
EOF
|
||||
}
|
||||
|
||||
strip_provider_prefix() {
|
||||
local model="$1"
|
||||
printf '%s' "${model#ollama/}"
|
||||
}
|
||||
|
||||
wait_for_container_running() {
|
||||
local name="$1"
|
||||
local timeout_sec="$2"
|
||||
local waited=0
|
||||
while [ "$waited" -lt "$timeout_sec" ]; do
|
||||
if podman ps --format '{{.Names}}' | grep -qx "$name"; then
|
||||
return 0
|
||||
fi
|
||||
sleep 1
|
||||
waited=$((waited + 1))
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
wait_for_openclaw_ready() {
|
||||
local timeout_sec="$1"
|
||||
local waited=0
|
||||
while [ "$waited" -lt "$timeout_sec" ]; do
|
||||
if podman exec openclaw sh -lc 'openclaw models list >/tmp/bench_models 2>&1' >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
sleep 2
|
||||
waited=$((waited + 2))
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
wait_for_ollama_ready() {
|
||||
local timeout_sec="$1"
|
||||
local waited=0
|
||||
while [ "$waited" -lt "$timeout_sec" ]; do
|
||||
if podman exec ollama sh -lc 'ollama list >/tmp/bench_ollama_list 2>&1' >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
sleep 2
|
||||
waited=$((waited + 2))
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
extract_quoted_metric() {
|
||||
local pattern="$1"
|
||||
local key="$2"
|
||||
local line
|
||||
line="$(rg "$pattern" "$OLLAMA_LOG_FILE" | tail -n 1 || true)"
|
||||
if [ -z "$line" ]; then
|
||||
echo "n/a"
|
||||
return
|
||||
fi
|
||||
|
||||
local value
|
||||
value="$(printf '%s' "$line" | sed -E "s/.*${key}=\"([^\"]+)\".*/\\1/")"
|
||||
if [ -z "$value" ] || [ "$value" = "$line" ]; then
|
||||
echo "n/a"
|
||||
else
|
||||
echo "$value"
|
||||
fi
|
||||
}
|
||||
|
||||
extract_unquoted_metric() {
|
||||
local pattern="$1"
|
||||
local sed_expr="$2"
|
||||
local line
|
||||
line="$(rg "$pattern" "$OLLAMA_LOG_FILE" | tail -n 1 || true)"
|
||||
if [ -z "$line" ]; then
|
||||
echo "n/a"
|
||||
return
|
||||
fi
|
||||
|
||||
local value
|
||||
value="$(printf '%s' "$line" | sed -E "$sed_expr")"
|
||||
if [ -z "$value" ] || [ "$value" = "$line" ]; then
|
||||
echo "n/a"
|
||||
else
|
||||
echo "$value"
|
||||
fi
|
||||
}
|
||||
|
||||
MODEL="ollama/qwen-coding-long:latest"
|
||||
EXPECTED_LINES=120
|
||||
TIMEOUT_SEC=480
|
||||
SESSION_KEY=""
|
||||
ARTIFACT_DIR=""
|
||||
START_STACK=1
|
||||
FORCE_RELOAD=1
|
||||
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
--model)
|
||||
MODEL="$2"
|
||||
shift 2
|
||||
;;
|
||||
--lines)
|
||||
EXPECTED_LINES="$2"
|
||||
shift 2
|
||||
;;
|
||||
--timeout-sec)
|
||||
TIMEOUT_SEC="$2"
|
||||
shift 2
|
||||
;;
|
||||
--session-key)
|
||||
SESSION_KEY="$2"
|
||||
shift 2
|
||||
;;
|
||||
--artifact-dir)
|
||||
ARTIFACT_DIR="$2"
|
||||
shift 2
|
||||
;;
|
||||
--no-start)
|
||||
START_STACK=0
|
||||
shift
|
||||
;;
|
||||
--no-reload)
|
||||
FORCE_RELOAD=0
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
fail "Unbekannte Option: $1"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if ! [[ "$EXPECTED_LINES" =~ ^[0-9]+$ ]] || [ "$EXPECTED_LINES" -lt 1 ]; then
|
||||
fail "--lines muss eine positive Ganzzahl sein"
|
||||
fi
|
||||
|
||||
if ! [[ "$TIMEOUT_SEC" =~ ^[0-9]+$ ]] || [ "$TIMEOUT_SEC" -lt 1 ]; then
|
||||
fail "--timeout-sec muss eine positive Ganzzahl sein"
|
||||
fi
|
||||
|
||||
need_cmd podman
|
||||
need_cmd jq
|
||||
need_cmd rg
|
||||
need_cmd sed
|
||||
need_cmd awk
|
||||
need_cmd timeout
|
||||
|
||||
if [ -z "$ARTIFACT_DIR" ]; then
|
||||
TS="$(date +%Y%m%d-%H%M%S)"
|
||||
ARTIFACT_DIR="$PROJECT_DIR/storage/workspace/benchmarks/$TS"
|
||||
fi
|
||||
mkdir -p "$ARTIFACT_DIR"
|
||||
|
||||
JSON_OUT_FILE="$ARTIFACT_DIR/agent-result.json"
|
||||
ASSISTANT_TEXT_FILE="$ARTIFACT_DIR/assistant-output.txt"
|
||||
OLLAMA_LOG_FILE="$ARTIFACT_DIR/ollama-since.log"
|
||||
STATS_BEFORE_FILE="$ARTIFACT_DIR/container-stats-before.txt"
|
||||
STATS_AFTER_FILE="$ARTIFACT_DIR/container-stats-after.txt"
|
||||
REQUEST_ERR_FILE="$ARTIFACT_DIR/request-stderr.log"
|
||||
SUMMARY_FILE="$ARTIFACT_DIR/summary.txt"
|
||||
|
||||
if [ -z "$SESSION_KEY" ]; then
|
||||
SESSION_KEY="agent:main:benchmark:$(date +%s)"
|
||||
fi
|
||||
|
||||
MODEL_OLLAMA="$(strip_provider_prefix "$MODEL")"
|
||||
|
||||
echo "=== Ghostnet Benchmark ==="
|
||||
echo "Projekt: $PROJECT_DIR"
|
||||
echo "Model: $MODEL"
|
||||
echo "Session key: $SESSION_KEY"
|
||||
echo "Erwartete Zeilen: $EXPECTED_LINES"
|
||||
echo "Timeout: ${TIMEOUT_SEC}s"
|
||||
echo "Artefakte: $ARTIFACT_DIR"
|
||||
|
||||
if [ "$START_STACK" -eq 1 ]; then
|
||||
echo
|
||||
echo "[1/6] Starte Stack via ./ghostnet.sh up ..."
|
||||
./ghostnet.sh up
|
||||
else
|
||||
echo
|
||||
echo "[1/6] Stack-Start uebersprungen (--no-start)"
|
||||
fi
|
||||
|
||||
echo "[2/6] Warte auf Container-Readiness ..."
|
||||
wait_for_container_running ollama 180 || fail "Container ollama ist nicht running"
|
||||
wait_for_container_running openclaw 180 || fail "Container openclaw ist nicht running"
|
||||
wait_for_ollama_ready 120 || fail "Ollama API wurde nicht rechtzeitig bereit"
|
||||
wait_for_openclaw_ready 180 || fail "OpenClaw wurde nicht rechtzeitig bereit"
|
||||
ok "Container und APIs sind bereit"
|
||||
|
||||
if [ "$FORCE_RELOAD" -eq 1 ]; then
|
||||
echo "[3/6] Entlade Modell fuer frische KV/VRAM-Load-Metriken ..."
|
||||
podman exec ollama sh -lc "ollama stop '$MODEL_OLLAMA' >/dev/null 2>&1 || true"
|
||||
else
|
||||
echo "[3/6] Modell-Entladung uebersprungen (--no-reload)"
|
||||
fi
|
||||
|
||||
echo "[4/6] Fuehre Benchmark-Anfrage aus ..."
|
||||
PROMPT="Gib exakt ${EXPECTED_LINES} Zeilen aus. Jede Zeile muss exakt den Text LINE_<n> haben, mit aufsteigender Nummerierung von 1 bis ${EXPECTED_LINES}. Kein weiterer Text."
|
||||
SINCE_UTC="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||
|
||||
podman stats --no-stream --format 'table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.MemPerc}}' ollama openclaw > "$STATS_BEFORE_FILE" 2>&1 || true
|
||||
|
||||
START_NS="$(date +%s%N)"
|
||||
REQUEST_RC=0
|
||||
if ! timeout "${TIMEOUT_SEC}s" podman exec openclaw openclaw agent \
|
||||
--session-key "$SESSION_KEY" \
|
||||
--model "$MODEL" \
|
||||
--message "$PROMPT" \
|
||||
--json > "$JSON_OUT_FILE" 2> "$REQUEST_ERR_FILE"; then
|
||||
REQUEST_RC=$?
|
||||
fi
|
||||
END_NS="$(date +%s%N)"
|
||||
WALL_MS=$(( (END_NS - START_NS) / 1000000 ))
|
||||
|
||||
podman logs --since "$SINCE_UTC" ollama > "$OLLAMA_LOG_FILE" 2>&1 || true
|
||||
podman stats --no-stream --format 'table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.MemPerc}}' ollama openclaw > "$STATS_AFTER_FILE" 2>&1 || true
|
||||
|
||||
HAS_VALID_JSON=0
|
||||
if [ -s "$JSON_OUT_FILE" ] && jq -e . "$JSON_OUT_FILE" >/dev/null 2>&1; then
|
||||
HAS_VALID_JSON=1
|
||||
fi
|
||||
if [ "$HAS_VALID_JSON" -ne 1 ] && [ "$REQUEST_RC" -eq 0 ]; then
|
||||
REQUEST_RC=65
|
||||
fi
|
||||
|
||||
REQUEST_ERROR_MSG=""
|
||||
if [ "$REQUEST_RC" -ne 0 ]; then
|
||||
REQUEST_ERROR_MSG="$(tail -n 20 "$REQUEST_ERR_FILE" 2>/dev/null | tr '\n' ' ' | sed -E 's/[[:space:]]+/ /g' | sed -E 's/^ //; s/ $//')"
|
||||
if [ -z "$REQUEST_ERROR_MSG" ]; then
|
||||
if [ "$HAS_VALID_JSON" -ne 1 ]; then
|
||||
REQUEST_ERROR_MSG="Agent lieferte kein valides JSON"
|
||||
else
|
||||
REQUEST_ERROR_MSG="Agent-Lauf fehlgeschlagen ohne stderr-Ausgabe"
|
||||
fi
|
||||
fi
|
||||
warn "Agent-Lauf fehlgeschlagen (rc=$REQUEST_RC): $REQUEST_ERROR_MSG"
|
||||
fi
|
||||
|
||||
echo "[5/6] Validiere Antwortformat ..."
|
||||
ASSISTANT_TEXT=""
|
||||
if [ "$HAS_VALID_JSON" -eq 1 ]; then
|
||||
ASSISTANT_TEXT="$(jq -r '.result.payloads[0].text // ""' "$JSON_OUT_FILE")"
|
||||
fi
|
||||
printf '%s' "$ASSISTANT_TEXT" > "$ASSISTANT_TEXT_FILE"
|
||||
|
||||
ACTUAL_LINES=()
|
||||
if [ -n "$ASSISTANT_TEXT" ]; then
|
||||
mapfile -t ACTUAL_LINES < "$ASSISTANT_TEXT_FILE"
|
||||
for i in "${!ACTUAL_LINES[@]}"; do
|
||||
ACTUAL_LINES[$i]="${ACTUAL_LINES[$i]%$'\r'}"
|
||||
done
|
||||
fi
|
||||
|
||||
VALIDATION_OK=1
|
||||
VALIDATION_DETAIL="OK"
|
||||
|
||||
if [ "$REQUEST_RC" -ne 0 ]; then
|
||||
VALIDATION_OK=0
|
||||
VALIDATION_DETAIL="Agent-Lauf fehlgeschlagen (rc=$REQUEST_RC)"
|
||||
elif [ "${#ACTUAL_LINES[@]}" -ne "$EXPECTED_LINES" ]; then
|
||||
VALIDATION_OK=0
|
||||
VALIDATION_DETAIL="Zeilenanzahl abweichend: erwartet=$EXPECTED_LINES, erhalten=${#ACTUAL_LINES[@]}"
|
||||
else
|
||||
for ((idx = 1; idx <= EXPECTED_LINES; idx++)); do
|
||||
expected="LINE_${idx}"
|
||||
actual="${ACTUAL_LINES[$((idx - 1))]}"
|
||||
if [ "$actual" != "$expected" ]; then
|
||||
VALIDATION_OK=0
|
||||
VALIDATION_DETAIL="Mismatch in Zeile $idx: erwartet='$expected', erhalten='$actual'"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if [ "$VALIDATION_OK" -eq 1 ]; then
|
||||
ok "Output validiert: exakt LINE_1..LINE_$EXPECTED_LINES"
|
||||
else
|
||||
warn "Output-Validierung fehlgeschlagen: $VALIDATION_DETAIL"
|
||||
fi
|
||||
|
||||
echo "[6/6] Sammle und berechne Metriken ..."
|
||||
DURATION_MS="n/a"
|
||||
USAGE_INPUT="n/a"
|
||||
USAGE_OUTPUT="n/a"
|
||||
USAGE_TOTAL="n/a"
|
||||
PROMPT_TOKENS="n/a"
|
||||
CONTEXT_TOKENS="n/a"
|
||||
STOP_REASON="n/a"
|
||||
FINISH_REASON="n/a"
|
||||
|
||||
if [ "$HAS_VALID_JSON" -eq 1 ]; then
|
||||
DURATION_MS="$(jq -r '.result.meta.durationMs // "n/a"' "$JSON_OUT_FILE")"
|
||||
USAGE_INPUT="$(jq -r '.result.meta.agentMeta.usage.input // "n/a"' "$JSON_OUT_FILE")"
|
||||
USAGE_OUTPUT="$(jq -r '.result.meta.agentMeta.usage.output // "n/a"' "$JSON_OUT_FILE")"
|
||||
USAGE_TOTAL="$(jq -r '.result.meta.agentMeta.usage.total // "n/a"' "$JSON_OUT_FILE")"
|
||||
PROMPT_TOKENS="$(jq -r '.result.meta.agentMeta.promptTokens // "n/a"' "$JSON_OUT_FILE")"
|
||||
CONTEXT_TOKENS="$(jq -r '.result.meta.agentMeta.contextTokens // "n/a"' "$JSON_OUT_FILE")"
|
||||
STOP_REASON="$(jq -r '.result.meta.completion.stopReason // "n/a"' "$JSON_OUT_FILE")"
|
||||
FINISH_REASON="$(jq -r '.result.meta.completion.finishReason // "n/a"' "$JSON_OUT_FILE")"
|
||||
fi
|
||||
|
||||
OUTPUT_TOKENS_PER_SEC="n/a"
|
||||
if [[ "$DURATION_MS" =~ ^[0-9]+$ ]] && [ "$DURATION_MS" -gt 0 ] && [[ "$USAGE_OUTPUT" =~ ^[0-9]+$ ]]; then
|
||||
OUTPUT_TOKENS_PER_SEC="$(awk "BEGIN { printf \"%.2f\", ($USAGE_OUTPUT * 1000) / $DURATION_MS }")"
|
||||
fi
|
||||
|
||||
END2END_TOKENS_PER_SEC="n/a"
|
||||
if [[ "$WALL_MS" =~ ^[0-9]+$ ]] && [ "$WALL_MS" -gt 0 ] && [[ "$USAGE_OUTPUT" =~ ^[0-9]+$ ]]; then
|
||||
END2END_TOKENS_PER_SEC="$(awk "BEGIN { printf \"%.2f\", ($USAGE_OUTPUT * 1000) / $WALL_MS }")"
|
||||
fi
|
||||
|
||||
GPU_AVAILABLE="$(extract_quoted_metric 'msg="gpu memory"' 'available')"
|
||||
GPU_FREE="$(extract_quoted_metric 'msg="gpu memory"' 'free')"
|
||||
GPU_OVERHEAD="$(extract_quoted_metric 'msg="gpu memory"' 'overhead')"
|
||||
KV_GPU="$(extract_quoted_metric 'msg="kv cache" device=ROCm0' 'size')"
|
||||
KV_CPU="$(extract_quoted_metric 'msg="kv cache" device=CPU' 'size')"
|
||||
MODEL_GPU="$(extract_quoted_metric 'msg="model weights" device=ROCm0' 'size')"
|
||||
MODEL_CPU="$(extract_quoted_metric 'msg="model weights" device=CPU' 'size')"
|
||||
COMPUTE_GPU="$(extract_quoted_metric 'msg="compute graph" device=ROCm0' 'size')"
|
||||
COMPUTE_CPU="$(extract_quoted_metric 'msg="compute graph" device=CPU' 'size')"
|
||||
TOTAL_MEMORY="$(extract_quoted_metric 'msg="total memory"' 'size')"
|
||||
KV_SIZE_TOKENS="$(extract_unquoted_metric 'KvSize:[0-9]+' 's/.*KvSize:([0-9]+).*/\1/')"
|
||||
RUNNER_START_SEC="$(extract_unquoted_metric 'llama runner started in [0-9]+(\.[0-9]+)? seconds' 's/.*llama runner started in ([0-9]+(\.[0-9]+)?) seconds.*/\1/')"
|
||||
|
||||
OLLAMA_ENV="$(podman exec ollama sh -lc 'echo "KV_CACHE=$OLLAMA_KV_CACHE_TYPE GPU_OVERHEAD=$OLLAMA_GPU_OVERHEAD KEEP_ALIVE=$OLLAMA_KEEP_ALIVE FLASH_ATTN=$OLLAMA_FLASH_ATTENTION MAX_LOADED=$OLLAMA_MAX_LOADED_MODELS NUM_PARALLEL=$OLLAMA_NUM_PARALLEL"' 2>/dev/null || true)"
|
||||
|
||||
JOURNAL_HITS="n/a"
|
||||
if command -v journalctl >/dev/null 2>&1; then
|
||||
JOURNAL_HITS="$(journalctl -b --since "$SINCE_UTC" --no-pager 2>/dev/null | rg -i 'amdgpu|MESA: error|graphics reset|drm|failed to allocate a buffer|not enough memory for command submission' | wc -l | tr -d ' ' || true)"
|
||||
[ -n "$JOURNAL_HITS" ] || JOURNAL_HITS="0"
|
||||
fi
|
||||
|
||||
{
|
||||
echo "=== Ghostnet Benchmark Summary ==="
|
||||
echo "Model: $MODEL"
|
||||
echo "Session key: $SESSION_KEY"
|
||||
echo "Request exit code: $REQUEST_RC"
|
||||
if [ "$REQUEST_RC" -ne 0 ]; then
|
||||
echo "Request error: $REQUEST_ERROR_MSG"
|
||||
fi
|
||||
echo "Prompt line target: $EXPECTED_LINES"
|
||||
echo "Validation: $([ "$VALIDATION_OK" -eq 1 ] && echo PASS || echo FAIL)"
|
||||
echo "Validation detail: $VALIDATION_DETAIL"
|
||||
echo
|
||||
echo "Latenz und Token"
|
||||
echo "- end_to_end_wall_ms: $WALL_MS"
|
||||
echo "- openclaw_duration_ms: $DURATION_MS"
|
||||
echo "- usage_input_tokens: $USAGE_INPUT"
|
||||
echo "- usage_output_tokens: $USAGE_OUTPUT"
|
||||
echo "- usage_total_tokens: $USAGE_TOTAL"
|
||||
echo "- prompt_tokens: $PROMPT_TOKENS"
|
||||
echo "- context_tokens: $CONTEXT_TOKENS"
|
||||
echo "- stop_reason: $STOP_REASON"
|
||||
echo "- finish_reason: $FINISH_REASON"
|
||||
echo "- output_tokens_per_sec_provider: $OUTPUT_TOKENS_PER_SEC"
|
||||
echo "- output_tokens_per_sec_end_to_end: $END2END_TOKENS_PER_SEC"
|
||||
echo
|
||||
echo "Ollama runtime"
|
||||
echo "- env: $OLLAMA_ENV"
|
||||
echo "- gpu_available: $GPU_AVAILABLE"
|
||||
echo "- gpu_free: $GPU_FREE"
|
||||
echo "- gpu_overhead: $GPU_OVERHEAD"
|
||||
echo "- kv_cache_gpu: $KV_GPU"
|
||||
echo "- kv_cache_cpu: $KV_CPU"
|
||||
echo "- kv_size_tokens: $KV_SIZE_TOKENS"
|
||||
echo "- model_weights_gpu: $MODEL_GPU"
|
||||
echo "- model_weights_cpu: $MODEL_CPU"
|
||||
echo "- compute_graph_gpu: $COMPUTE_GPU"
|
||||
echo "- compute_graph_cpu: $COMPUTE_CPU"
|
||||
echo "- total_model_memory: $TOTAL_MEMORY"
|
||||
echo "- model_load_time_sec: $RUNNER_START_SEC"
|
||||
echo
|
||||
echo "Stabilitaet"
|
||||
echo "- journal_gpu_related_hits_since_start: $JOURNAL_HITS"
|
||||
echo
|
||||
echo "Artefakte"
|
||||
echo "- agent_json: $JSON_OUT_FILE"
|
||||
echo "- assistant_text: $ASSISTANT_TEXT_FILE"
|
||||
echo "- ollama_log_since_start: $OLLAMA_LOG_FILE"
|
||||
echo "- stats_before: $STATS_BEFORE_FILE"
|
||||
echo "- stats_after: $STATS_AFTER_FILE"
|
||||
echo "- request_stderr: $REQUEST_ERR_FILE"
|
||||
} | tee "$SUMMARY_FILE"
|
||||
|
||||
echo
|
||||
echo "Container-Stats vor Lauf:"
|
||||
cat "$STATS_BEFORE_FILE"
|
||||
echo
|
||||
echo "Container-Stats nach Lauf:"
|
||||
cat "$STATS_AFTER_FILE"
|
||||
|
||||
if [ "$REQUEST_RC" -eq 0 ] && [ "$VALIDATION_OK" -eq 1 ]; then
|
||||
ok "Benchmark erfolgreich abgeschlossen"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
warn "Benchmark beendet, aber Validierung ist fehlgeschlagen"
|
||||
exit 2
|
||||
Executable
+3
@@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
|
||||
podman exec -it openclaw openclaw chat
|
||||
Executable
+117
@@ -0,0 +1,117 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Quick end-to-end smoke test for OpenClaw sandbox runtime.
|
||||
# Prerequisites: running stack and jq installed on host.
|
||||
|
||||
PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "$PROJECT_DIR"
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
ok() {
|
||||
echo -e "${GREEN}[OK]${NC} $1"
|
||||
}
|
||||
|
||||
warn() {
|
||||
echo -e "${YELLOW}[WARN]${NC} $1"
|
||||
}
|
||||
|
||||
fail() {
|
||||
echo -e "${RED}[FAIL]${NC} $1"
|
||||
exit 1
|
||||
}
|
||||
|
||||
need_cmd() {
|
||||
command -v "$1" >/dev/null 2>&1 || fail "Benoetigtes Kommando fehlt: $1"
|
||||
}
|
||||
|
||||
need_cmd podman
|
||||
need_cmd jq
|
||||
|
||||
OPENCLAW_NAME="openclaw"
|
||||
|
||||
if ! podman ps --format '{{.Names}}' | grep -qx "$OPENCLAW_NAME"; then
|
||||
fail "Container $OPENCLAW_NAME laeuft nicht. Starte den Stack zuerst mit ./ghostnet.sh up"
|
||||
fi
|
||||
ok "Container $OPENCLAW_NAME laeuft"
|
||||
|
||||
if podman exec "$OPENCLAW_NAME" sh -lc 'docker version >/tmp/smoke_dv 2>&1'; then
|
||||
ok "Docker/Podman Socket im OpenClaw-Container erreichbar"
|
||||
else
|
||||
echo "--- docker version output ---"
|
||||
podman exec "$OPENCLAW_NAME" sh -lc 'cat /tmp/smoke_dv || true'
|
||||
fail "Socket im OpenClaw-Container nicht erreichbar"
|
||||
fi
|
||||
|
||||
if podman exec "$OPENCLAW_NAME" sh -lc 'touch /home/node/.openclaw/workspace/.smoke_openclaw_write && rm -f /home/node/.openclaw/workspace/.smoke_openclaw_write'; then
|
||||
ok "OpenClaw kann in den Primary Workspace schreiben"
|
||||
else
|
||||
fail "OpenClaw kann NICHT in den Primary Workspace schreiben"
|
||||
fi
|
||||
|
||||
AGENT_PROMPT='Fuehre in der Sandbox genau diesen Befehl aus: sh -lc "touch /workspace/smoke_mount_probe.txt && echo SMOKE_SANDBOX_OK". Antworte nur mit dem Kommando-Output.'
|
||||
AGENT_OUT_FILE="/tmp/openclaw_smoke_agent.json"
|
||||
|
||||
if ! podman exec "$OPENCLAW_NAME" sh -lc "openclaw agent --session-key agent:main:smoke-test -m '$AGENT_PROMPT' --json" > "$AGENT_OUT_FILE" 2>&1; then
|
||||
echo "--- agent output ---"
|
||||
cat "$AGENT_OUT_FILE"
|
||||
fail "Agent-Turn fehlgeschlagen (Sandbox evtl. nicht aktiv)"
|
||||
fi
|
||||
|
||||
if ! grep -q 'SMOKE_SANDBOX_OK' "$AGENT_OUT_FILE"; then
|
||||
echo "--- agent output ---"
|
||||
cat "$AGENT_OUT_FILE"
|
||||
fail "Sandbox-Befehl hat erwartete Rueckgabe nicht geliefert"
|
||||
fi
|
||||
ok "Sandbox-Befehl erfolgreich ausgefuehrt"
|
||||
|
||||
SBOX_NAME="$(podman ps -a --format '{{.Names}}' | grep '^openclaw-sbx-' | head -n 1 || true)"
|
||||
[ -n "$SBOX_NAME" ] || fail "Kein openclaw-sbx-* Container gefunden"
|
||||
ok "Sandbox-Container gefunden: $SBOX_NAME"
|
||||
|
||||
MOUNTS_JSON="$(podman inspect "$SBOX_NAME" --format '{{json .Mounts}}')"
|
||||
WORKSPACE_SOURCE="$(echo "$MOUNTS_JSON" | jq -r '.[] | select(.Destination=="/workspace") | .Source' | head -n 1)"
|
||||
|
||||
[ -n "$WORKSPACE_SOURCE" ] || fail "Kein /workspace Mount im Sandbox-Container gefunden"
|
||||
echo "Sandbox /workspace Source: $WORKSPACE_SOURCE"
|
||||
|
||||
EXPECTED_SOURCE="$(realpath -m "${HOST_WORKSPACE_PATH:-./storage/workspace}")"
|
||||
if [ "$WORKSPACE_SOURCE" = "$EXPECTED_SOURCE" ]; then
|
||||
ok "Sandbox-Mount zeigt auf erwarteten Host-Workspace"
|
||||
else
|
||||
warn "Sandbox-Mount weicht von erwartetem Host-Workspace ab"
|
||||
echo "Erwartet: $EXPECTED_SOURCE"
|
||||
fi
|
||||
|
||||
if podman exec "$SBOX_NAME" sh -lc 'test -f /workspace/smoke_mount_probe.txt'; then
|
||||
ok "Probe-Datei existiert im Sandbox-Workspace"
|
||||
else
|
||||
fail "Probe-Datei fehlt im Sandbox-Workspace"
|
||||
fi
|
||||
|
||||
if podman exec "$SBOX_NAME" sh -lc 'apt-get update >/tmp/smoke_apt_update 2>&1'; then
|
||||
ok "apt-get update in Sandbox erfolgreich"
|
||||
else
|
||||
echo "--- apt-get update output ---"
|
||||
podman exec "$SBOX_NAME" sh -lc 'tail -n 40 /tmp/smoke_apt_update || true'
|
||||
fail "apt-get update in Sandbox fehlgeschlagen"
|
||||
fi
|
||||
|
||||
if podman exec "$SBOX_NAME" sh -lc 'apt-get install -y jq >/tmp/smoke_apt_install 2>&1'; then
|
||||
if podman exec "$SBOX_NAME" sh -lc 'command -v jq >/dev/null 2>&1'; then
|
||||
ok "apt-get install funktioniert in Sandbox (jq installiert)"
|
||||
else
|
||||
fail "apt-get install meldete Erfolg, jq ist aber nicht vorhanden"
|
||||
fi
|
||||
else
|
||||
echo "--- apt-get install output ---"
|
||||
podman exec "$SBOX_NAME" sh -lc 'tail -n 60 /tmp/smoke_apt_install || true'
|
||||
fail "apt-get install in Sandbox fehlgeschlagen"
|
||||
fi
|
||||
|
||||
ok "Smoke-Test erfolgreich abgeschlossen"
|
||||
Reference in New Issue
Block a user