Closes the gap between the merged Memory v2 code (PR #2757 wired the client into main.go) and operator activation. Without this PR an operator wanting to flip MEMORY_V2_CUTOVER=true had to provision a separate memory-plugin service and point MEMORY_PLUGIN_URL at it — extra ops surface for what the design intends to be a built-in. What ships: * Both Dockerfile + Dockerfile.tenant build the cmd/memory-plugin-postgres binary into /memory-plugin. * Entrypoints spawn the plugin in the background on :9100 BEFORE starting the main server; wait up to 30s for /v1/health to return 200; abort boot loud if it doesn't (better to crash-loop than to silently route cutover traffic against a dead plugin). * Default env: MEMORY_PLUGIN_DATABASE_URL=$DATABASE_URL (share the existing tenant Postgres — plugin's `memory_namespaces` / `memory_records` tables coexist with platform schema, no conflicts), MEMORY_PLUGIN_LISTEN_ADDR=:9100. * MEMORY_PLUGIN_DISABLE=1 escape hatch for operators running the plugin externally on a separate host. * Platform image: plugin runs as the `platform` user (not root) via su-exec — matches the privilege boundary the main server already drops to. Tenant image already starts as `canvas` so the plugin inherits non-root automatically. What stays operator-controlled: * MEMORY_V2_CUTOVER is NOT auto-set. Behavior change for existing deployments: zero. The wiring at workspace-server/internal/memory/ wiring/wiring.go skips building the plugin client until the operator opts in, so the running sidecar is a no-op for traffic until then. * MEMORY_PLUGIN_URL is NOT auto-set either, for the same reason — setting it implies cutover-active intent. Operators set both on staging first, verify a live commit/recall round-trip (closes pending task #292), then promote to production. Operator activation steps after this PR ships: 1. Verify pgvector extension is available on the target Postgres (the plugin's first migration runs CREATE EXTENSION IF NOT EXISTS vector). Railway's managed Postgres ships pgvector available; some self-hosted operators may need to enable it. 2. Redeploy the workspace-server with this image. 3. Set MEMORY_PLUGIN_URL=http://localhost:9100 + MEMORY_V2_CUTOVER=true in the environment (staging first). 4. Watch boot logs for "memory-plugin: ✅ sidecar healthy" and the wiring.go cutover messages; do a live commit_memory + recall_memory round-trip via the canvas Memory tab to verify. 5. Promote to production once staging holds for a sweep window. Refs RFC #2728. Closes the dormant-plugin gap noted in task #294.
85 lines
3.0 KiB
Bash
85 lines
3.0 KiB
Bash
#!/bin/sh
|
|
# Tenant entrypoint — starts both Go platform (API) and Canvas (UI).
|
|
#
|
|
# Container runs as non-root 'canvas' user (USER directive in Dockerfile.tenant).
|
|
# Both processes start as non-root. SIGTERM propagates to child processes via the
|
|
# shell's trap + wait -n pattern below.
|
|
#
|
|
# Go platform listens on :8080 (Fly health checks hit this port).
|
|
# Canvas Node.js listens on :3000 (internal only).
|
|
# The Go platform's fallback handler proxies non-API routes to :3000
|
|
# so the browser only ever talks to :8080.
|
|
#
|
|
# If either process dies, we kill the other and exit non-zero so Fly
|
|
# restarts the machine.
|
|
|
|
set -e
|
|
|
|
# Start Canvas in background
|
|
cd /canvas
|
|
PORT=3000 HOSTNAME=0.0.0.0 node server.js &
|
|
CANVAS_PID=$!
|
|
|
|
# Memory v2 sidecar (built-in postgres plugin). See Dockerfile entrypoint
|
|
# comment for rationale. Stays inert at the protocol layer until the
|
|
# operator sets MEMORY_V2_CUTOVER=true; running it is cheap.
|
|
#
|
|
# Defaults the plugin's DATABASE_URL to the tenant's DATABASE_URL so
|
|
# operators don't need to configure two of them. Plugin tables coexist
|
|
# with the platform schema.
|
|
MEMORY_PLUGIN_PID=""
|
|
if [ -z "$MEMORY_PLUGIN_DISABLE" ] && [ -n "$DATABASE_URL" ]; then
|
|
: "${MEMORY_PLUGIN_DATABASE_URL:=$DATABASE_URL}"
|
|
: "${MEMORY_PLUGIN_LISTEN_ADDR:=:9100}"
|
|
export MEMORY_PLUGIN_DATABASE_URL MEMORY_PLUGIN_LISTEN_ADDR
|
|
echo "memory-plugin: starting sidecar on $MEMORY_PLUGIN_LISTEN_ADDR" >&2
|
|
/memory-plugin &
|
|
MEMORY_PLUGIN_PID=$!
|
|
# Wait up to 30s for /v1/health. Boot failure is fatal so a misconfigured
|
|
# tenant crash-loops instead of silently serving cutover traffic against
|
|
# a dead plugin.
|
|
health_port=${MEMORY_PLUGIN_LISTEN_ADDR#:}
|
|
ready=0
|
|
for _ in $(seq 1 30); do
|
|
if wget -qO- --timeout=2 "http://localhost:${health_port}/v1/health" >/dev/null 2>&1; then
|
|
ready=1
|
|
break
|
|
fi
|
|
sleep 1
|
|
done
|
|
if [ "$ready" != "1" ]; then
|
|
echo "memory-plugin: ❌ /v1/health never returned 200 after 30s — aborting boot. Check DATABASE_URL reachability + pgvector extension + migrations." >&2
|
|
kill "$MEMORY_PLUGIN_PID" 2>/dev/null || true
|
|
kill "$CANVAS_PID" 2>/dev/null || true
|
|
exit 1
|
|
fi
|
|
echo "memory-plugin: ✅ sidecar healthy on :$health_port" >&2
|
|
fi
|
|
|
|
# Start Go platform in foreground-ish (we trap signals)
|
|
# CANVAS_PROXY_URL tells the platform to proxy unmatched routes to Canvas.
|
|
# CONTAINER_BACKEND: empty = Docker (default for self-hosted/local).
|
|
# Set to "flyio" via Fly machine env to use Fly Machines API instead.
|
|
export CANVAS_PROXY_URL="${CANVAS_PROXY_URL:-http://localhost:3000}"
|
|
cd /
|
|
/platform &
|
|
PLATFORM_PID=$!
|
|
|
|
# If any process exits, kill the others
|
|
cleanup() {
|
|
kill $CANVAS_PID 2>/dev/null || true
|
|
kill $PLATFORM_PID 2>/dev/null || true
|
|
[ -n "$MEMORY_PLUGIN_PID" ] && kill $MEMORY_PLUGIN_PID 2>/dev/null || true
|
|
}
|
|
trap cleanup EXIT SIGTERM SIGINT
|
|
|
|
# Wait for any to exit — whichever exits first triggers cleanup
|
|
if [ -n "$MEMORY_PLUGIN_PID" ]; then
|
|
wait -n $CANVAS_PID $PLATFORM_PID $MEMORY_PLUGIN_PID
|
|
else
|
|
wait -n $CANVAS_PID $PLATFORM_PID
|
|
fi
|
|
EXIT_CODE=$?
|
|
cleanup
|
|
exit $EXIT_CODE
|