#!/bin/sh # dev-start.sh — one-command local development environment. # # What it does (in order): # 1. Generates ADMIN_TOKEN into .env if missing (closes #684 fail-open) # 2. Runs infra/scripts/setup.sh (postgres + redis + langfuse + clickhouse # + temporal + populates template/plugin registry from manifest.json) # 3. Starts the platform (Go :8080), waits for /health # 4. Starts the canvas (Next.js :3000), waits for HTTP 200 # 5. Prints a readiness banner with API-key add instructions # 6. On Ctrl-C, kills both background processes and tears down infra # # Prerequisites: # - Docker + Docker Compose v2 (for postgres/redis/langfuse/etc) # - Go 1.25+ (for the platform binary) # - Node.js 20+ (for the canvas) # - jq (for setup.sh's manifest clone — optional; # without it, template palette will be # empty until you run clone-manifest.sh # manually) # # Usage: # ./scripts/dev-start.sh # # Open http://localhost:3000, add your model API key in # # Config → Secrets & API Keys, then create your first workspace. # # Idempotent: re-running picks up where the last run left off (existing # .env is preserved, npm install skipped if node_modules present, etc). set -e ROOT="$(cd "$(dirname "$0")/.." && pwd)" ENV_FILE="$ROOT/.env" cleanup() { echo "" echo "==> Shutting down..." kill $PLATFORM_PID $CANVAS_PID 2>/dev/null || true # Use setup.sh's compose file (full infra) since that's what we # brought up. `down` keeps named volumes by default — call with # --volumes here only if you want a clean slate (we don't, since # idempotent re-runs are the usual case). docker compose -f "$ROOT/docker-compose.infra.yml" down 2>/dev/null || true echo " Done." } trap cleanup EXIT INT TERM # ─────────────────────────────────────────────── 1. dev-mode auth posture # # SECURITY (harden/no-fail-open-auth): the workspace-server auth chain is # now fail-CLOSED in EVERY environment, dev included. There is NO dev-mode # fail-open escape hatch anymore — AdminAuth / WorkspaceAuth / discovery all # require a real credential. So local dev must AUTHENTICATE, not run open. # # The clean way to keep the canvas working locally is to provision a # deterministic ADMIN_TOKEN and hand the matching NEXT_PUBLIC_ADMIN_TOKEN to # the canvas bundle. The canvas already attaches `Authorization: Bearer # $NEXT_PUBLIC_ADMIN_TOKEN` on every platform call (canvas/src/lib/api.ts), # and next.config.ts warns if the pair is half-set. We set BOTH here. # # MOLECULE_ENV=development — dev conveniences (loopback bind, relaxed # rate limit). NOT an auth lever. # ADMIN_TOKEN= — server-side bearer AdminAuth/WorkspaceAuth # enforce (Tier-2b). Real credential. # NEXT_PUBLIC_ADMIN_TOKEN — same value, baked into the canvas bundle so # the browser sends the matching bearer. # # For SaaS the platform is provisioned with a random ADMIN_TOKEN + the # canvas image baked with the matching NEXT_PUBLIC_ADMIN_TOKEN, plus # MOLECULE_ENV=production. Same shape, stronger secret. if [ -f "$ENV_FILE" ] && grep -q '^MOLECULE_ENV=' "$ENV_FILE"; then echo "==> Reusing MOLECULE_ENV from existing .env" else echo "==> Setting MOLECULE_ENV=development in .env" { if [ -f "$ENV_FILE" ]; then cat "$ENV_FILE" echo "" fi echo "# Generated by scripts/dev-start.sh on $(date -u +%Y-%m-%dT%H:%M:%SZ)" echo "# Local-dev conveniences (loopback bind, relaxed rate limit)." echo "# Auth is fail-closed even in dev — see ADMIN_TOKEN below." echo "MOLECULE_ENV=development" } > "$ENV_FILE.tmp" mv "$ENV_FILE.tmp" "$ENV_FILE" echo " Saved to $ENV_FILE" fi # Provision a deterministic dev ADMIN_TOKEN (idempotent — preserved across # re-runs). This is the credential the canvas authenticates with locally; it # is NOT a secret (it only guards your own localhost stack), so a fixed, # well-known value is fine and keeps re-runs reproducible. DEV_ADMIN_TOKEN="dev-local-admin-token" if [ -f "$ENV_FILE" ] && grep -q '^ADMIN_TOKEN=' "$ENV_FILE"; then echo "==> Reusing ADMIN_TOKEN from existing .env" else echo "==> Provisioning dev ADMIN_TOKEN in .env (fail-closed auth, authenticated canvas)" { cat "$ENV_FILE" echo "" echo "# Dev ADMIN_TOKEN — the canvas authenticates with this locally." echo "# Auth is fail-closed; without a matching bearer the canvas 401s." echo "# Fixed value is fine: it only guards your localhost stack." echo "ADMIN_TOKEN=$DEV_ADMIN_TOKEN" } > "$ENV_FILE.tmp" mv "$ENV_FILE.tmp" "$ENV_FILE" echo " Saved to $ENV_FILE" fi # Source .env so the platform inherits ADMIN_TOKEN (and anything else # the user has added — e.g. ANTHROPIC_API_KEY for skipping the canvas # Secrets UI). `set -a` exports every assignment in the sourced file # without us having to know the var names. set -a # shellcheck disable=SC1090 . "$ENV_FILE" set +a # The canvas reads NEXT_PUBLIC_ADMIN_TOKEN at build/dev time and attaches it # as the bearer on every platform call. Mirror the server-side ADMIN_TOKEN # into it so the matched-pair guard in canvas/next.config.ts is satisfied and # the browser authenticates. Exported for the `npm run dev` child below. export NEXT_PUBLIC_ADMIN_TOKEN="$ADMIN_TOKEN" # ─────────────────────────────────────────────── 2. infra + templates # Use setup.sh (not raw docker-compose) so the template registry gets # populated from manifest.json. Without that, the canvas template # palette is empty and the user has to manually clone repos — exactly # the friction this script exists to eliminate. echo "==> Running infra/scripts/setup.sh (infra + template registry)" "$ROOT/infra/scripts/setup.sh" # ─────────────────────────────────────────────── 3. platform # # Two paths: # (a) `go` is on PATH → run the platform directly via `go run`. # Fast iteration, attaches to /tmp/molecule-platform.log. # (b) `go` is NOT on PATH → fall back to the published platform # container image. Slower first run (image pull) but the script # still works on a fresh dev box without forcing the dev to # install Go just to read logs. # # The earlier version of this script silently called `go run` and died # with `go: not found` on dev boxes where Go wasn't installed; the # script's own prerequisite list (line 13-21) said "Go 1.25+" but the # user had no signpost between "open the doc" and "command not found # at line 111." This branch makes the failure path either succeed # (fallback) or fail loud with explicit install guidance. if command -v go >/dev/null 2>&1; then echo "==> Starting Platform (Go :8080)" cd "$ROOT/workspace-server" go run ./cmd/server > /tmp/molecule-platform.log 2>&1 & PLATFORM_PID=$! else echo "==> Go not found on PATH — falling back to docker-compose platform service" echo " (Install Go 1.25+ for faster iteration: https://go.dev/dl/)" cd "$ROOT" # Bring up just the platform service from docker-compose.yml. infra/setup.sh # already brought up postgres+redis+etc on docker-compose.infra.yml; this # adds the platform container on top, mapped to :8080 so the rest of this # script's wait-for-/health loop works unchanged. docker compose up -d --build platform > /tmp/molecule-platform.log 2>&1 || { echo " ✗ docker compose up platform failed — see /tmp/molecule-platform.log" echo " Either install Go 1.25+ (https://go.dev/dl/) and rerun, or fix the docker fallback." exit 1 } # PLATFORM_PID is unset on this path; cleanup() handles that with `kill ... 2>/dev/null || true`. PLATFORM_PID= fi echo " Waiting for Platform /health..." PLATFORM_READY=0 for i in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 \ 21 22 23 24 25 26 27 28 29 30; do if curl -sf http://localhost:8080/health >/dev/null 2>&1; then echo " Platform ready (t+${i}s)" PLATFORM_READY=1 break fi sleep 1 done if [ "$PLATFORM_READY" -ne 1 ]; then echo " ✗ Platform did not respond in 30s — check /tmp/molecule-platform.log" exit 1 fi # ─────────────────────────────────────────────── 4. canvas echo "==> Starting Canvas (Next.js :3000)" cd "$ROOT/canvas" if [ ! -d node_modules ]; then echo " First-run: npm install (~30-60s)" npm install fi npm run dev > /tmp/molecule-canvas.log 2>&1 & CANVAS_PID=$! echo " Waiting for Canvas HTTP 200..." CANVAS_READY=0 for i in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 \ 21 22 23 24 25 26 27 28 29 30; do code=$(curl -sf -o /dev/null -w "%{http_code}" http://localhost:3000/ 2>/dev/null || echo "0") if [ "$code" = "200" ]; then echo " Canvas ready (t+${i}s)" CANVAS_READY=1 break fi sleep 1 done if [ "$CANVAS_READY" -ne 1 ]; then echo " ✗ Canvas did not respond in 30s — check /tmp/molecule-canvas.log" exit 1 fi # ─────────────────────────────────────────────── 5. readiness banner cat <