first commit

This commit is contained in:
npc0-hue
2026-07-11 14:56:10 +08:00
commit 7e05d0a4e7
660 changed files with 78119 additions and 0 deletions
+35
View File
@@ -0,0 +1,35 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
# shellcheck source=scripts/local-debug-env.sh
source "$ROOT_DIR/scripts/local-debug-env.sh"
export BROWSER_ACCEPTANCE_EVIDENCE_DIR="${BROWSER_ACCEPTANCE_EVIDENCE_DIR:-$LOCAL_DEBUG_ROOT/browser-acceptance}"
export LOCAL_DEBUG_PLATFORM_URL="${LOCAL_DEBUG_PLATFORM_URL:-$(local_debug_platform_url)}"
export LOCAL_DEBUG_WEB_URL="${LOCAL_DEBUG_WEB_URL:-$(local_debug_web_url)}"
SELF_STARTED="${BROWSER_ACCEPTANCE_SELF_START:-true}"
cleanup() {
if [[ "$SELF_STARTED" == "true" ]]; then
LOCAL_DEBUG_ROOT="$LOCAL_DEBUG_ROOT" \
LOCAL_DEBUG_PLATFORM_PORT="$LOCAL_DEBUG_PLATFORM_PORT" \
LOCAL_DEBUG_WEB_PORT="$LOCAL_DEBUG_WEB_PORT" \
"$ROOT_DIR/scripts/local-debug-stop.sh" >/dev/null 2>&1 || true
fi
}
if [[ "$SELF_STARTED" == "true" ]]; then
trap cleanup EXIT
"$ROOT_DIR/scripts/local-debug-reset.sh"
"$ROOT_DIR/scripts/local-debug-start.sh"
else
curl -fsS "$LOCAL_DEBUG_PLATFORM_URL/healthz" >/dev/null
curl -fsS "$LOCAL_DEBUG_WEB_URL" >/dev/null
fi
"$ROOT_DIR/scripts/local-debug-smoke.sh"
mkdir -p "$BROWSER_ACCEPTANCE_EVIDENCE_DIR"
node "$ROOT_DIR/platform_web/acceptance/browser-acceptance.mjs"
+24
View File
@@ -0,0 +1,24 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
ensure_node_deps() {
local dir="$1"
if [[ ! -d "$dir/node_modules" ]]; then
(cd "$dir" && npm ci)
fi
}
"$ROOT_DIR/scripts/check-structure.sh"
(cd "$ROOT_DIR/platform" && go test ./...)
(cd "$ROOT_DIR/run" && go test ./...)
ensure_node_deps "$ROOT_DIR/platform_web"
(cd "$ROOT_DIR/platform_web" && npm run typecheck && npm run test && npm run build)
ensure_node_deps "$ROOT_DIR/plugins"
(cd "$ROOT_DIR/plugins" && npm run typecheck && npm run test && npm run validate:manifest)
printf 'full check passed\n'
+146
View File
@@ -0,0 +1,146 @@
#!/usr/bin/env bash
set -euo pipefail
required_paths=(
"AGENTS.md"
"README.md"
"scripts/check-all.sh"
"run/AGENTS.md"
"run/README.md"
"run/go.mod"
"run/cmd/run/main.go"
"platform/AGENTS.md"
"platform/README.md"
"platform/go.mod"
"platform/cmd/platform/main.go"
"platform_web/AGENTS.md"
"platform_web/README.md"
"platform_web/package.json"
"platform_web/package-lock.json"
"platform_web/tsconfig.json"
"platform_web/vite.config.ts"
"platform_web/index.html"
"plugins/AGENTS.md"
"plugins/README.md"
"plugins/package.json"
"plugins/package-lock.json"
"plugins/tsconfig.json"
"platform/api"
"platform/dto"
"platform/domain"
"platform/model"
"platform/repo"
"platform/service"
"platform/protocol"
"platform/validator"
"platform/config"
"platform/shared"
"platform/api/health_handler.go"
"platform/api/health_handler_test.go"
"platform/config/config.go"
"platform/config/config_test.go"
"platform/dto/health.go"
"platform/domain/resources.md"
"platform/api/routes.md"
"platform/protocol/run-contracts.md"
"platform/protocol/ai-provider-contracts.md"
"platform/protocol/server-lifecycle.md"
"run/api"
"run/protocol"
"run/domain"
"run/runtime"
"run/spool"
"run/artifact"
"run/logingest"
"run/config"
"run/shared"
"run/api/platform_client.go"
"run/api/platform_client_test.go"
"run/config/config.go"
"run/config/config_test.go"
"run/domain/status.go"
"run/runtime/smoke.go"
"run/runtime/smoke_test.go"
"run/protocol/control.md"
"run/protocol/job.md"
"run/protocol/log-ingest.md"
"run/protocol/artifact.md"
"run/protocol/game-client-bridge.md"
"platform_web/api"
"platform_web/routes"
"platform_web/pages"
"platform_web/components"
"platform_web/stores"
"platform_web/contracts"
"platform_web/schemas"
"platform_web/theme"
"platform_web/utils"
"platform_web/app/App.tsx"
"platform_web/app/App.test.tsx"
"platform_web/app/main.tsx"
"platform_web/components/AppShell.tsx"
"platform_web/components/MagicalParticleLayer.tsx"
"platform_web/components/PageFrame.tsx"
"platform_web/contracts/page.ts"
"platform_web/pages/HomePage.tsx"
"platform_web/pages/ServersPage.tsx"
"platform_web/pages/PluginsPage.tsx"
"platform_web/pages/UsersPage.tsx"
"platform_web/pages/AiProvidersPage.tsx"
"platform_web/pages/pageRegistry.tsx"
"platform_web/routes/routes.ts"
"platform_web/theme/base.css"
"platform_web/theme/README.md"
"platform_web/theme/tokens.ts"
"platform_web/routes/routes.md"
"platform_web/api/contracts.md"
"platform_web/api/client.ts"
"platform_web/api/types.ts"
"platform_web/contracts/pages.md"
"platform_web/contracts/plugin-page-bridge.md"
"platform_web/schemas/env.ts"
"platform_web/stores/navigation.ts"
"platform_web/utils/classes.ts"
"plugins/manifests"
"plugins/schemas"
"plugins/sdk"
"plugins/examples"
"plugins/tests"
"plugins/manifests/game-plugin.manifest.schema.json"
"plugins/schemas/create-form.schema.json"
"plugins/sdk/bridge-contract.md"
"plugins/sdk/index.ts"
"plugins/examples/dev-game-plugin/manifest.json"
"plugins/examples/dev-game-plugin/schemas/create-form.schema.json"
"plugins/examples/scum-server-plugin/manifest.json"
"plugins/examples/scum-server-plugin/schemas/create-form.schema.json"
"plugins/examples/scum-server-plugin/actions/install.json"
"plugins/examples/scum-server-plugin/actions/start.json"
"plugins/examples/scum-server-plugin/actions/stop.json"
"plugins/examples/scum-server-plugin/actions/restart.json"
"plugins/examples/scum-server-plugin/actions/status.json"
"plugins/scripts/validate-manifest.ts"
"plugins/tests/manifest-validation.test.ts"
"openspec/changes/bootstrap-game-server-platform-architecture/proposal.md"
"openspec/changes/bootstrap-game-server-platform-architecture/design.md"
"openspec/changes/bootstrap-game-server-platform-architecture/tasks.md"
"openspec/changes/bootstrap-game-server-platform-architecture/specs/project-workspace-governance/spec.md"
"openspec/changes/bootstrap-game-server-platform-architecture/specs/game-server-platform-core/spec.md"
"openspec/changes/bootstrap-game-server-platform-architecture/specs/run-execution-channel/spec.md"
"openspec/changes/bootstrap-game-server-platform-architecture/specs/game-plugin-system/spec.md"
"openspec/changes/bootstrap-game-server-platform-architecture/specs/platform-web-console/spec.md"
)
missing=0
for path in "${required_paths[@]}"; do
if [[ ! -e "$path" ]]; then
printf 'missing required path: %s\n' "$path" >&2
missing=1
fi
done
if [[ "$missing" -ne 0 ]]; then
exit 1
fi
printf 'structure check passed\n'
+51
View File
@@ -0,0 +1,51 @@
#!/usr/bin/env bash
# Shared configuration for the first-party local debug workspace.
set -euo pipefail
LOCAL_DEBUG_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
LOCAL_DEBUG_ROOT_DIR="$(cd "$LOCAL_DEBUG_SCRIPT_DIR/.." && pwd)"
export LOCAL_DEBUG_ROOT="${LOCAL_DEBUG_ROOT:-$LOCAL_DEBUG_ROOT_DIR/.local-debug}"
export LOCAL_DEBUG_LOG_DIR="${LOCAL_DEBUG_LOG_DIR:-$LOCAL_DEBUG_ROOT/logs}"
export LOCAL_DEBUG_PID_DIR="${LOCAL_DEBUG_PID_DIR:-$LOCAL_DEBUG_ROOT/pids}"
export GOCACHE="${GOCACHE:-$LOCAL_DEBUG_ROOT/go-build-cache}"
export LOCAL_DEBUG_PLATFORM_PORT="${LOCAL_DEBUG_PLATFORM_PORT:-18080}"
export LOCAL_DEBUG_WEB_PORT="${LOCAL_DEBUG_WEB_PORT:-5173}"
export PLATFORM_ADDR="${PLATFORM_ADDR:-127.0.0.1:$LOCAL_DEBUG_PLATFORM_PORT}"
export PLATFORM_STORAGE_BACKEND="${PLATFORM_STORAGE_BACKEND:-file}"
export PLATFORM_DATA_DIR="${PLATFORM_DATA_DIR:-$LOCAL_DEBUG_ROOT/platform}"
export PLATFORM_METADATA_PATH="${PLATFORM_METADATA_PATH:-$PLATFORM_DATA_DIR/metadata.json}"
export PLATFORM_LOG_BODY_BACKEND="${PLATFORM_LOG_BODY_BACKEND:-file}"
export PLATFORM_LOG_DIR="${PLATFORM_LOG_DIR:-$PLATFORM_DATA_DIR/logs}"
export RUN_MODE="${RUN_MODE:-worker}"
export RUN_PLATFORM_URL="${RUN_PLATFORM_URL:-http://127.0.0.1:$LOCAL_DEBUG_PLATFORM_PORT}"
export RUN_ENDPOINT_ID="${RUN_ENDPOINT_ID:-run-local-debug}"
export RUN_DISPLAY_NAME="${RUN_DISPLAY_NAME:-Local Debug Run}"
export RUN_VERSION="${RUN_VERSION:-0.1.0-local-debug}"
export RUN_REGISTRATION_TOKEN="${RUN_REGISTRATION_TOKEN:-local-debug-registration}"
export RUN_WORKSPACE_ROOT="${RUN_WORKSPACE_ROOT:-$LOCAL_DEBUG_ROOT/run/workspace}"
export RUN_SPOOL_ROOT="${RUN_SPOOL_ROOT:-$LOCAL_DEBUG_ROOT/run/spool}"
export RUN_MAX_JOBS="${RUN_MAX_JOBS:-1}"
export RUN_HEARTBEAT_INTERVAL_MS="${RUN_HEARTBEAT_INTERVAL_MS:-2000}"
export RUN_POLL_INTERVAL_MS="${RUN_POLL_INTERVAL_MS:-1000}"
export RUN_RETRY_BACKOFF_MS="${RUN_RETRY_BACKOFF_MS:-500}"
export PLATFORM_API_PROXY="${PLATFORM_API_PROXY:-http://127.0.0.1:$LOCAL_DEBUG_PLATFORM_PORT}"
export VITE_PLATFORM_API_BASE_URL="${VITE_PLATFORM_API_BASE_URL:-/api/v1}"
export VITE_ENABLE_LOCAL_AUTH_FALLBACK="${VITE_ENABLE_LOCAL_AUTH_FALLBACK:-false}"
local_debug_platform_url() {
printf 'http://127.0.0.1:%s' "$LOCAL_DEBUG_PLATFORM_PORT"
}
local_debug_web_url() {
printf 'http://127.0.0.1:%s' "$LOCAL_DEBUG_WEB_PORT"
}
local_debug_forbidden_pattern() {
printf '%s' '(/Users/|/private/|unix://|tcp://|Bearer |sk-|password=|apiKeyRef|rawApiKey|sessionToken|run session token|direct run URL|plugin-owned transport)'
}
+22
View File
@@ -0,0 +1,22 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
# shellcheck source=scripts/local-debug-env.sh
source "$ROOT_DIR/scripts/local-debug-env.sh"
case "$LOCAL_DEBUG_ROOT" in
"$ROOT_DIR/.local-debug" | /private/tmp/browser-local-debug-* | /tmp/browser-local-debug-*)
;;
*)
printf 'refusing to reset unexpected LOCAL_DEBUG_ROOT: %s\n' "$LOCAL_DEBUG_ROOT" >&2
printf 'allowed roots: %s/.local-debug, /private/tmp/browser-local-debug-*, /tmp/browser-local-debug-*\n' "$ROOT_DIR" >&2
exit 1
;;
esac
"$ROOT_DIR/scripts/local-debug-stop.sh"
printf 'deleting local debug root only: %s\n' "$LOCAL_DEBUG_ROOT"
rm -rf "$LOCAL_DEBUG_ROOT"
printf 'local debug reset complete\n'
+383
View File
@@ -0,0 +1,383 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
# shellcheck source=scripts/local-debug-env.sh
source "$ROOT_DIR/scripts/local-debug-env.sh"
PLATFORM_URL="$(local_debug_platform_url)"
API_URL="$PLATFORM_URL/api/v1"
WORK_DIR="$LOCAL_DEBUG_ROOT/smoke"
mkdir -p "$WORK_DIR"
SELF_STARTED_PIDS=()
cleanup_self_started() {
local pid
for pid in "${SELF_STARTED_PIDS[@]:-}"; do
if [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null; then
kill "$pid" 2>/dev/null || true
fi
done
}
wait_for_url() {
local name="$1"
local url="$2"
local attempts="${3:-30}"
printf 'waiting for %s at %s\n' "$name" "$url"
for _ in $(seq 1 "$attempts"); do
if curl -fsS "$url" >/dev/null 2>&1; then
printf '%s is ready\n' "$name"
return 0
fi
sleep 1
done
printf '%s did not become ready at %s\n' "$name" "$url" >&2
return 1
}
start_self_hosted_stack() {
mkdir -p "$LOCAL_DEBUG_LOG_DIR" "$LOCAL_DEBUG_PID_DIR" "$PLATFORM_DATA_DIR" "$PLATFORM_LOG_DIR" "$RUN_WORKSPACE_ROOT" "$RUN_SPOOL_ROOT" "$GOCACHE"
trap cleanup_self_started EXIT
printf 'self-starting platform for local debug smoke\n'
(
cd "$ROOT_DIR/platform"
exec env \
GOCACHE="$GOCACHE" \
PLATFORM_ADDR="$PLATFORM_ADDR" \
PLATFORM_STORAGE_BACKEND="$PLATFORM_STORAGE_BACKEND" \
PLATFORM_DATA_DIR="$PLATFORM_DATA_DIR" \
PLATFORM_METADATA_PATH="$PLATFORM_METADATA_PATH" \
PLATFORM_LOG_BODY_BACKEND="$PLATFORM_LOG_BODY_BACKEND" \
PLATFORM_LOG_DIR="$PLATFORM_LOG_DIR" \
go run ./cmd/platform
) >"$LOCAL_DEBUG_LOG_DIR/platform.log" 2>&1 &
SELF_STARTED_PIDS+=("$!")
printf '%s' "$!" >"$LOCAL_DEBUG_PID_DIR/platform.pid"
wait_for_url platform "$PLATFORM_URL/healthz" 45
printf 'self-starting run worker for local debug smoke\n'
(
cd "$ROOT_DIR/run"
exec env \
GOCACHE="$GOCACHE" \
RUN_MODE="$RUN_MODE" \
RUN_PLATFORM_URL="$RUN_PLATFORM_URL" \
RUN_ENDPOINT_ID="$RUN_ENDPOINT_ID" \
RUN_DISPLAY_NAME="$RUN_DISPLAY_NAME" \
RUN_VERSION="$RUN_VERSION" \
RUN_REGISTRATION_TOKEN="$RUN_REGISTRATION_TOKEN" \
RUN_WORKSPACE_ROOT="$RUN_WORKSPACE_ROOT" \
RUN_SPOOL_ROOT="$RUN_SPOOL_ROOT" \
RUN_MAX_JOBS="$RUN_MAX_JOBS" \
RUN_HEARTBEAT_INTERVAL_MS="$RUN_HEARTBEAT_INTERVAL_MS" \
RUN_POLL_INTERVAL_MS="$RUN_POLL_INTERVAL_MS" \
RUN_RETRY_BACKOFF_MS="$RUN_RETRY_BACKOFF_MS" \
go run ./cmd/run
) >"$LOCAL_DEBUG_LOG_DIR/run.log" 2>&1 &
SELF_STARTED_PIDS+=("$!")
printf '%s' "$!" >"$LOCAL_DEBUG_PID_DIR/run.pid"
printf 'self-starting platform_web for local debug smoke\n'
(
cd "$ROOT_DIR"
exec env \
PLATFORM_API_PROXY="$PLATFORM_API_PROXY" \
VITE_PLATFORM_API_BASE_URL="$VITE_PLATFORM_API_BASE_URL" \
VITE_ENABLE_LOCAL_AUTH_FALLBACK="$VITE_ENABLE_LOCAL_AUTH_FALLBACK" \
npm --prefix platform_web run dev -- --port "$LOCAL_DEBUG_WEB_PORT"
) >"$LOCAL_DEBUG_LOG_DIR/platform_web.log" 2>&1 &
SELF_STARTED_PIDS+=("$!")
printf '%s' "$!" >"$LOCAL_DEBUG_PID_DIR/platform_web.pid"
wait_for_url platform_web "$(local_debug_web_url)" 45
}
if [[ "${LOCAL_DEBUG_SELF_START:-false}" == "true" ]]; then
start_self_hosted_stack
fi
json_post() {
local url="$1"
local body_file="$2"
local output_file="$3"
local status
status="$(curl -sS -H 'Content-Type: application/json' --data-binary "@$body_file" "$url" -o "$output_file" -w '%{http_code}')"
case "$status" in
2*)
return 0
;;
*)
printf 'POST %s failed with HTTP %s\n' "$url" "$status" >&2
return 1
;;
esac
}
json_get() {
local url="$1"
local output_file="$2"
shift 2
curl -fsS "$@" "$url" >"$output_file"
}
require_file_contains() {
local file="$1"
local pattern="$2"
if ! grep -Eq "$pattern" "$file"; then
printf 'expected %s to contain pattern: %s\n' "$file" "$pattern" >&2
sed -n '1,120p' "$file" >&2
exit 1
fi
}
reject_forbidden_fragments() {
local file="$1"
local pattern
pattern="$(local_debug_forbidden_pattern)"
if grep -Eq "$pattern" "$file"; then
printf 'forbidden local-debug fragment found in %s\n' "$file" >&2
grep -En "$pattern" "$file" >&2
exit 1
fi
}
session_token_from_login() {
node -e 'const fs=require("fs"); const data=JSON.parse(fs.readFileSync(process.argv[1],"utf8")); if (!data.sessionId) process.exit(2); process.stdout.write(data.sessionId);' "$1"
}
json_id() {
node -e 'const fs=require("fs"); const data=JSON.parse(fs.readFileSync(process.argv[1],"utf8")); const id=data.instance?.id || data.serverInstance?.id || data.id || ((data.items||[])[0]||{}).id; if (!id) process.exit(2); process.stdout.write(id);' "$1"
}
printf 'checking platform health at %s\n' "$PLATFORM_URL"
json_get "$PLATFORM_URL/healthz" "$WORK_DIR/health.json"
require_file_contains "$WORK_DIR/health.json" '"status"[[:space:]]*:[[:space:]]*"ok"'
cat >"$WORK_DIR/login.request.json" <<'JSON'
{"account":"operator.local@example.test","password":"operator-local"}
JSON
json_post "$API_URL/auth/login" "$WORK_DIR/login.request.json" "$WORK_DIR/login.response.json"
SESSION_ID="$(session_token_from_login "$WORK_DIR/login.response.json")"
AUTH_HEADER=(-H "Authorization: Bearer $SESSION_ID")
require_file_contains "$WORK_DIR/login.response.json" '"status"[[:space:]]*:[[:space:]]*"active"'
printf 'validating plugin manifests\n'
(cd "$ROOT_DIR/plugins" && npm run validate:manifest)
node - "$ROOT_DIR/plugins/examples/dev-game-plugin/manifest.json" "$WORK_DIR/register-plugin.request.json" <<'NODE'
const fs = require("fs");
const manifestPath = process.argv[2];
const outputPath = process.argv[3];
const source = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
const manifest = {
id: source.id,
name: source.name,
description: "Development plugin",
version: source.version,
kind: "game-plugin",
tags: ["example", "development"],
server: {
type: source.server.type,
displayName: source.server.displayName,
supportedOs: ["linux", "darwin"],
createFormSchema: source.server.createFormSchema
},
capabilities: ["process.install", "process.start", "process.stop"],
permissions: ["server.read", "server.lifecycle", "server.logs.read", "server.artifacts.read"],
actions: {
install: source.actions.install,
start: source.actions.start,
stop: source.actions.stop,
restart: source.actions.restart
},
pages: [
{
key: "logs",
title: "Logs",
path: "/logs",
permissions: ["server.read", "server.lifecycle", "server.logs.read", "server.artifacts.read"],
bridgeActions: ["server.instances.read", "jobs.dispatch", "logs.query", "artifacts.open"]
}
],
bridge: { actions: ["server.instances.read", "jobs.dispatch", "logs.query", "artifacts.open"] },
ai: { purposes: [] }
};
fs.writeFileSync(outputPath, JSON.stringify({
manifestRef: "artifact://manifests/game.example/0.1.0",
manifest
}, null, 2));
NODE
printf 'registering dev plugin through platform API\n'
if ! json_post "$API_URL/game-plugins/register-manifest" "$WORK_DIR/register-plugin.request.json" "$WORK_DIR/register-plugin.response.json"; then
json_get "$API_URL/game-plugins/game.example" "$WORK_DIR/register-plugin.response.json"
fi
reject_forbidden_fragments "$WORK_DIR/register-plugin.response.json"
node - "$ROOT_DIR/plugins/examples/scum-server-plugin/manifest.json" "$WORK_DIR/register-scum-plugin.request.json" <<'NODE'
const fs = require("fs");
const manifestPath = process.argv[2];
const outputPath = process.argv[3];
const source = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
const manifest = {
id: source.id,
name: source.name,
description: "SCUM local proof plugin",
version: source.version,
kind: "game-plugin",
tags: source.tags,
server: {
type: source.server.type,
displayName: source.server.displayName,
supportedOs: ["linux"],
createFormSchema: source.server.createFormSchema
},
capabilities: ["process.install", "process.start", "process.stop"],
permissions: ["server.create", "server.read", "server.lifecycle", "server.logs.read", "server.artifacts.read"],
actions: {
install: source.actions.install,
start: source.actions.start,
stop: source.actions.stop,
restart: source.actions.restart
},
pages: [
{
key: "overview",
title: "SCUM Overview",
path: "/overview",
permissions: ["server.read", "server.lifecycle", "server.logs.read", "server.artifacts.read"],
bridgeActions: ["server.instances.read", "jobs.dispatch", "logs.query", "artifacts.open"]
},
{
key: "logs",
title: "SCUM Logs",
path: "/logs",
permissions: ["server.read", "server.lifecycle", "server.logs.read", "server.artifacts.read"],
bridgeActions: ["server.instances.read", "jobs.dispatch", "logs.query", "artifacts.open"]
}
],
bridge: { actions: ["server.instances.read", "jobs.dispatch", "logs.query", "artifacts.open"] },
ai: { purposes: [] }
};
fs.writeFileSync(outputPath, JSON.stringify({
manifestRef: "artifact://manifests/game.scum/0.1.0",
manifest
}, null, 2));
NODE
printf 'registering SCUM plugin through platform API\n'
if ! json_post "$API_URL/game-plugins/register-manifest" "$WORK_DIR/register-scum-plugin.request.json" "$WORK_DIR/register-scum-plugin.response.json"; then
json_get "$API_URL/game-plugins/game.scum" "$WORK_DIR/register-scum-plugin.response.json"
fi
reject_forbidden_fragments "$WORK_DIR/register-scum-plugin.response.json"
printf 'waiting for run endpoint heartbeat\n'
for _ in $(seq 1 20); do
if json_get "$API_URL/run/endpoints?status=online" "$WORK_DIR/run-endpoints.response.json" && grep -q "$RUN_ENDPOINT_ID" "$WORK_DIR/run-endpoints.response.json"; then
break
fi
sleep 1
done
require_file_contains "$WORK_DIR/run-endpoints.response.json" "$RUN_ENDPOINT_ID"
reject_forbidden_fragments "$WORK_DIR/run-endpoints.response.json"
cat >"$WORK_DIR/create-server.request.json" <<JSON
{
"id": "server-local-debug",
"pluginId": "game.example",
"runEndpointId": "$RUN_ENDPOINT_ID",
"name": "Local Debug Example Server",
"idempotencyKey": "local-debug-create"
}
JSON
cat >"$WORK_DIR/create-scum-alpha.request.json" <<JSON
{
"id": "scum-alpha",
"pluginId": "game.scum",
"runEndpointId": "$RUN_ENDPOINT_ID",
"name": "SCUM Alpha",
"idempotencyKey": "local-debug-scum-alpha-create"
}
JSON
cat >"$WORK_DIR/create-scum-beta.request.json" <<JSON
{
"id": "scum-beta",
"pluginId": "game.scum",
"runEndpointId": "$RUN_ENDPOINT_ID",
"name": "SCUM Beta",
"idempotencyKey": "local-debug-scum-beta-create"
}
JSON
printf 'creating server lifecycle workflow through platform API\n'
if ! curl -fsS -H 'Content-Type: application/json' "${AUTH_HEADER[@]}" --data-binary "@$WORK_DIR/create-server.request.json" "$API_URL/server-instances/workflows/create" >"$WORK_DIR/create-server.response.json"; then
json_get "$API_URL/server-instances/server-local-debug" "$WORK_DIR/create-server.response.json" "${AUTH_HEADER[@]}"
fi
reject_forbidden_fragments "$WORK_DIR/create-server.response.json"
printf 'creating SCUM server lifecycle workflows through platform API\n'
if ! curl -fsS -H 'Content-Type: application/json' "${AUTH_HEADER[@]}" --data-binary "@$WORK_DIR/create-scum-alpha.request.json" "$API_URL/server-instances/workflows/create" >"$WORK_DIR/create-scum-alpha.response.json"; then
json_get "$API_URL/server-instances/scum-alpha" "$WORK_DIR/create-scum-alpha.response.json" "${AUTH_HEADER[@]}"
fi
if ! curl -fsS -H 'Content-Type: application/json' "${AUTH_HEADER[@]}" --data-binary "@$WORK_DIR/create-scum-beta.request.json" "$API_URL/server-instances/workflows/create" >"$WORK_DIR/create-scum-beta.response.json"; then
json_get "$API_URL/server-instances/scum-beta" "$WORK_DIR/create-scum-beta.response.json" "${AUTH_HEADER[@]}"
fi
reject_forbidden_fragments "$WORK_DIR/create-scum-alpha.response.json"
reject_forbidden_fragments "$WORK_DIR/create-scum-beta.response.json"
require_file_contains "$WORK_DIR/create-scum-alpha.response.json" '"pluginId"[[:space:]]*:[[:space:]]*"game.scum"'
require_file_contains "$WORK_DIR/create-scum-beta.response.json" '"pluginId"[[:space:]]*:[[:space:]]*"game.scum"'
SERVER_ID="$(json_id "$WORK_DIR/create-server.response.json")"
SCUM_ALPHA_ID="$(json_id "$WORK_DIR/create-scum-alpha.response.json")"
SCUM_BETA_ID="$(json_id "$WORK_DIR/create-scum-beta.response.json")"
printf 'checking jobs, logs, artifacts, and marketplace refs\n'
json_get "$API_URL/server-instances" "$WORK_DIR/server-instances.response.json" "${AUTH_HEADER[@]}"
json_get "$API_URL/jobs?serverInstanceId=$SERVER_ID" "$WORK_DIR/jobs.response.json" "${AUTH_HEADER[@]}"
json_get "$API_URL/jobs?serverInstanceId=$SCUM_ALPHA_ID" "$WORK_DIR/scum-alpha-jobs.response.json" "${AUTH_HEADER[@]}"
json_get "$API_URL/jobs?serverInstanceId=$SCUM_BETA_ID" "$WORK_DIR/scum-beta-jobs.response.json" "${AUTH_HEADER[@]}"
json_get "$API_URL/log-streams" "$WORK_DIR/log-streams.response.json" "${AUTH_HEADER[@]}"
json_get "$API_URL/artifacts" "$WORK_DIR/artifacts.response.json" "${AUTH_HEADER[@]}"
json_get "$API_URL/plugin-marketplace/plugins" "$WORK_DIR/marketplace.response.json" "${AUTH_HEADER[@]}"
json_get "$API_URL/plugin-marketplace/plugins?serverType=scum&keyword=scum" "$WORK_DIR/scum-marketplace.response.json" "${AUTH_HEADER[@]}"
require_file_contains "$WORK_DIR/scum-marketplace.response.json" '"id"[[:space:]]*:[[:space:]]*"game.scum"'
require_file_contains "$WORK_DIR/scum-alpha-jobs.response.json" '"serverInstanceId"[[:space:]]*:[[:space:]]*"scum-alpha"'
require_file_contains "$WORK_DIR/scum-beta-jobs.response.json" '"serverInstanceId"[[:space:]]*:[[:space:]]*"scum-beta"'
for file in "$WORK_DIR"/server-instances.response.json "$WORK_DIR"/jobs.response.json "$WORK_DIR"/scum-alpha-jobs.response.json "$WORK_DIR"/scum-beta-jobs.response.json "$WORK_DIR"/log-streams.response.json "$WORK_DIR"/artifacts.response.json "$WORK_DIR"/marketplace.response.json "$WORK_DIR"/scum-marketplace.response.json; do
reject_forbidden_fragments "$file"
done
if [[ "$VITE_PLATFORM_API_BASE_URL" != "/api/v1" ]]; then
printf 'VITE_PLATFORM_API_BASE_URL must be /api/v1, got %s\n' "$VITE_PLATFORM_API_BASE_URL" >&2
exit 1
fi
if [[ "$PLATFORM_API_PROXY" != "$PLATFORM_URL" ]]; then
printf 'PLATFORM_API_PROXY must be %s, got %s\n' "$PLATFORM_URL" "$PLATFORM_API_PROXY" >&2
exit 1
fi
if [[ "$VITE_ENABLE_LOCAL_AUTH_FALLBACK" != "false" ]]; then
printf 'VITE_ENABLE_LOCAL_AUTH_FALLBACK must stay false for local-debug proof, got %s\n' "$VITE_ENABLE_LOCAL_AUTH_FALLBACK" >&2
exit 1
fi
cat >"$WORK_DIR/browser-walkthrough-checklist.md" <<EOF
# Local Debug Browser Walkthrough
- Open $(local_debug_web_url)
- Login with operator.local@example.test / operator-local.
- Confirm the login path is API-backed and no local fallback banner or fallback workspace appears.
- Visit 首页, 服务器管理, 插件市场, 用户管理, AI 提供商管理.
- Open server-local-debug detail and inspect lifecycle history, plugin controls, logs, and artifact references.
- Confirm 插件市场 can find SCUM Server / game.scum, then open scum-alpha and scum-beta from 服务器管理.
- Confirm both SCUM servers are backed by game.scum and have separate lifecycle install jobs and operation history.
- Search visible text for forbidden fragments: /Users/, /private/, unix://, tcp://, Bearer , sk-, password=, apiKeyRef, rawApiKey, run session tokens, direct run URLs, plugin-owned transport details.
- Acceptance requires platform routes, logical IDs, job refs, log refs, artifact refs, and safe metadata only.
EOF
printf 'local debug smoke passed\n'
printf 'evidence directory: %s\n' "$WORK_DIR"
+144
View File
@@ -0,0 +1,144 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
# shellcheck source=scripts/local-debug-env.sh
source "$ROOT_DIR/scripts/local-debug-env.sh"
mkdir -p "$LOCAL_DEBUG_LOG_DIR" "$LOCAL_DEBUG_PID_DIR" "$PLATFORM_DATA_DIR" "$PLATFORM_LOG_DIR" "$RUN_WORKSPACE_ROOT" "$RUN_SPOOL_ROOT"
mkdir -p "$GOCACHE"
managed_pid_running() {
local pid_file="$1"
if [[ ! -f "$pid_file" ]]; then
return 1
fi
local pid
pid="$(cat "$pid_file")"
[[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null
}
port_listener_pid() {
local port="$1"
lsof -tiTCP:"$port" -sTCP:LISTEN 2>/dev/null | head -n 1 || true
}
ensure_port_available() {
local name="$1"
local port="$2"
local pid_file="$LOCAL_DEBUG_PID_DIR/$name.pid"
local listener_pid
listener_pid="$(port_listener_pid "$port")"
if [[ -z "$listener_pid" ]]; then
return 0
fi
if managed_pid_running "$pid_file" && [[ "$listener_pid" == "$(cat "$pid_file")" ]]; then
return 0
fi
printf '%s port %s is already used by pid %s; choose another LOCAL_DEBUG_*_PORT or stop the existing listener\n' "$name" "$port" "$listener_pid" >&2
return 1
}
start_service() {
local name="$1"
local service_dir="$2"
local pid_file="$LOCAL_DEBUG_PID_DIR/$name.pid"
shift 2
if [[ -f "$pid_file" ]]; then
local existing_pid
existing_pid="$(cat "$pid_file")"
if kill -0 "$existing_pid" 2>/dev/null; then
printf '%s already running with pid %s\n' "$name" "$existing_pid"
return 0
fi
rm -f "$pid_file"
fi
printf 'starting %s\n' "$name"
(
cd "$service_dir"
exec nohup "$@" >"$LOCAL_DEBUG_LOG_DIR/$name.log" 2>&1
) >/dev/null 2>&1 &
printf '%s' "$!" >"$pid_file"
printf '%s pid %s log %s\n' "$name" "$(cat "$pid_file")" "$LOCAL_DEBUG_LOG_DIR/$name.log"
}
wait_for_url() {
local name="$1"
local url="$2"
local attempts="${3:-30}"
printf 'waiting for %s at %s\n' "$name" "$url"
for _ in $(seq 1 "$attempts"); do
if curl -fsS "$url" >/dev/null 2>&1; then
printf '%s is ready\n' "$name"
return 0
fi
sleep 1
done
printf '%s did not become ready at %s\n' "$name" "$url" >&2
return 1
}
printf 'local debug root: %s\n' "$LOCAL_DEBUG_ROOT"
printf 'platform: %s\n' "$(local_debug_platform_url)"
printf 'platform_web: %s\n' "$(local_debug_web_url)"
printf 'platform log: %s\n' "$LOCAL_DEBUG_LOG_DIR/platform.log"
printf 'run log: %s\n' "$LOCAL_DEBUG_LOG_DIR/run.log"
printf 'platform_web log: %s\n' "$LOCAL_DEBUG_LOG_DIR/platform_web.log"
ensure_port_available platform "$LOCAL_DEBUG_PLATFORM_PORT"
ensure_port_available platform_web "$LOCAL_DEBUG_WEB_PORT"
start_service platform "$ROOT_DIR/platform" env \
GOCACHE="$GOCACHE" \
PLATFORM_ADDR="$PLATFORM_ADDR" \
PLATFORM_STORAGE_BACKEND="$PLATFORM_STORAGE_BACKEND" \
PLATFORM_DATA_DIR="$PLATFORM_DATA_DIR" \
PLATFORM_METADATA_PATH="$PLATFORM_METADATA_PATH" \
PLATFORM_LOG_BODY_BACKEND="$PLATFORM_LOG_BODY_BACKEND" \
PLATFORM_LOG_DIR="$PLATFORM_LOG_DIR" \
go run ./cmd/platform
wait_for_url platform "$(local_debug_platform_url)/healthz"
start_service run "$ROOT_DIR/run" env \
GOCACHE="$GOCACHE" \
RUN_MODE="$RUN_MODE" \
RUN_PLATFORM_URL="$RUN_PLATFORM_URL" \
RUN_ENDPOINT_ID="$RUN_ENDPOINT_ID" \
RUN_DISPLAY_NAME="$RUN_DISPLAY_NAME" \
RUN_VERSION="$RUN_VERSION" \
RUN_REGISTRATION_TOKEN="$RUN_REGISTRATION_TOKEN" \
RUN_WORKSPACE_ROOT="$RUN_WORKSPACE_ROOT" \
RUN_SPOOL_ROOT="$RUN_SPOOL_ROOT" \
RUN_MAX_JOBS="$RUN_MAX_JOBS" \
RUN_HEARTBEAT_INTERVAL_MS="$RUN_HEARTBEAT_INTERVAL_MS" \
RUN_POLL_INTERVAL_MS="$RUN_POLL_INTERVAL_MS" \
RUN_RETRY_BACKOFF_MS="$RUN_RETRY_BACKOFF_MS" \
go run ./cmd/run
start_service platform_web "$ROOT_DIR" env \
PLATFORM_API_PROXY="$PLATFORM_API_PROXY" \
VITE_PLATFORM_API_BASE_URL="$VITE_PLATFORM_API_BASE_URL" \
VITE_ENABLE_LOCAL_AUTH_FALLBACK="$VITE_ENABLE_LOCAL_AUTH_FALLBACK" \
npm --prefix platform_web run dev -- --port "$LOCAL_DEBUG_WEB_PORT"
wait_for_url platform_web "$(local_debug_web_url)"
cat <<EOF
Local debug stack requested.
Login with:
account: operator.local@example.test
password: operator-local
Run smoke verification:
scripts/local-debug-smoke.sh
Open browser walkthrough:
$(local_debug_web_url)
EOF
+26
View File
@@ -0,0 +1,26 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
# shellcheck source=scripts/local-debug-env.sh
source "$ROOT_DIR/scripts/local-debug-env.sh"
if [[ ! -d "$LOCAL_DEBUG_PID_DIR" ]]; then
printf 'no local debug pid directory at %s\n' "$LOCAL_DEBUG_PID_DIR"
exit 0
fi
for name in platform_web run platform; do
pid_file="$LOCAL_DEBUG_PID_DIR/$name.pid"
if [[ ! -f "$pid_file" ]]; then
continue
fi
pid="$(cat "$pid_file")"
if kill -0 "$pid" 2>/dev/null; then
printf 'stopping %s pid %s\n' "$name" "$pid"
kill "$pid" 2>/dev/null || true
else
printf '%s pid %s is not running\n' "$name" "$pid"
fi
rm -f "$pid_file"
done