From f78787369812c485d42476523530d533857cea0e Mon Sep 17 00:00:00 2001 From: rabbitblood Date: Mon, 20 Apr 2026 12:53:16 -0700 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20nuke-and-rebuild.sh=20=E2=80=94=20o?= =?UTF-8?q?ne-command=20fleet=20reset?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two scripts: - nuke-and-rebuild.sh: docker down -v, clean orphans, rebuild, setup - post-rebuild-setup.sh: insert global secrets (MiniMax + GH PAT), import org template, wait for platform health Global secrets ensure every provisioned container gets MiniMax API config and GitHub PAT injected as env vars automatically — no manual settings.json deployment needed. Usage: bash scripts/nuke-and-rebuild.sh Co-Authored-By: Claude Opus 4.6 (1M context) --- scripts/nuke-and-rebuild.sh | 18 ++++++++++++++ scripts/post-rebuild-setup.sh | 44 +++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 scripts/nuke-and-rebuild.sh create mode 100644 scripts/post-rebuild-setup.sh diff --git a/scripts/nuke-and-rebuild.sh b/scripts/nuke-and-rebuild.sh new file mode 100644 index 00000000..9faeec46 --- /dev/null +++ b/scripts/nuke-and-rebuild.sh @@ -0,0 +1,18 @@ +#!/bin/bash +# Full nuke + rebuild — one command to reset everything +# Usage: bash scripts/nuke-and-rebuild.sh +set -euo pipefail + +echo "=== NUKE ===" +docker compose down -v 2>/dev/null || true +docker ps -a --format "{{.Names}}" | grep "^ws-" | xargs -r docker rm -f 2>/dev/null || true +docker volume ls --format "{{.Name}}" | grep "^ws-" | xargs -r docker volume rm 2>/dev/null || true +docker network rm molecule-monorepo-net 2>/dev/null || true +echo " cleaned" + +echo "=== REBUILD ===" +docker compose up -d --build +echo " platform + canvas up" + +echo "=== POST-REBUILD SETUP ===" +bash scripts/post-rebuild-setup.sh diff --git a/scripts/post-rebuild-setup.sh b/scripts/post-rebuild-setup.sh new file mode 100644 index 00000000..2bff9e33 --- /dev/null +++ b/scripts/post-rebuild-setup.sh @@ -0,0 +1,44 @@ +#!/bin/bash +# Post-rebuild setup — run after docker compose up -d --build +# Inserts global secrets that the provisioner injects into every workspace container. +# Without these, agents can't call MiniMax or push to GitHub. + +set -euo pipefail + +DB_CONTAINER="${DB_CONTAINER:-molecule-monorepo-postgres-1}" +DB_USER="${DB_USER:-dev}" +DB_NAME="${DB_NAME:-molecule}" +PLATFORM_URL="${PLATFORM_URL:-http://127.0.0.1:8080}" +ADMIN_TOKEN="${ADMIN_TOKEN:-HlgeMb8LjQLXg/B4y8hYzhbCQlg5LNu0oEa4IjShARE=}" + +echo "=== Waiting for platform health ===" +until curl -s --max-time 5 "$PLATFORM_URL/health" >/dev/null 2>&1; do + echo " waiting..." + sleep 3 +done +echo " platform up" + +echo "=== Inserting global secrets ===" +docker exec "$DB_CONTAINER" psql -U "$DB_USER" -d "$DB_NAME" -c " +INSERT INTO global_secrets (key, encrypted_value, encryption_version) VALUES +('ANTHROPIC_BASE_URL', 'https://api.minimax.io/anthropic', 0), +('ANTHROPIC_AUTH_TOKEN', '${MINIMAX_API_KEY:-sk-cp-lHt-QFSyZwZxeo_fMbmLUX3VgHOwbKGMXUZb6PS2U15D3fqjDB2qPh1OVEzvfvWs9CgcrUpyU7C682uVT_8GBy9RFLaFzBcdLkKdVcPX4yj9UaXNTH82KVw}', 0), +('ANTHROPIC_MODEL', 'MiniMax-M2.7', 0), +('ANTHROPIC_SMALL_FAST_MODEL', 'MiniMax-M2.7', 0), +('API_TIMEOUT_MS', '3000000', 0), +('CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC', '1', 0), +('GITHUB_TOKEN', '${GITHUB_PAT:-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}', 0) +ON CONFLICT (key) DO UPDATE SET encrypted_value = EXCLUDED.encrypted_value; +" +echo " 7 global secrets set" + +echo "=== Importing org template ===" +curl -s --max-time 600 -X POST "$PLATFORM_URL/org/import" \ + -H "Authorization: Bearer $ADMIN_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"dir":"molecule-dev"}' | head -1 +echo "" +echo " import complete" + +echo "=== Done ===" +echo "Run: http://127.0.0.1:3000 for canvas" From 1bca58a01b7847b6423cf242d09050cff5fda364 Mon Sep 17 00:00:00 2001 From: Hongming Wang Date: Mon, 20 Apr 2026 12:55:03 -0700 Subject: [PATCH 2/2] fix(canvas): include NEXT_PUBLIC_PLATFORM_URL in CSP connect-src MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tenant page loads were blocked by: Refused to connect to 'https://api.moleculesai.app/cp/auth/me' because it violates the document's Content Security Policy. CSP had `connect-src 'self' wss:` — fine for same-origin + any wss, but browser refuses cross-origin HTTPS fetches that aren't listed. PLATFORM_URL (baked from NEXT_PUBLIC_PLATFORM_URL, which is the CP origin on SaaS tenants) needs to be explicit. Fix: middleware reads NEXT_PUBLIC_PLATFORM_URL at build/runtime and adds both the https and wss siblings to connect-src. Self- hosted deploys that override the build-arg automatically get a matching CSP — no hardcoded hostname. Test added: buildCsp includes NEXT_PUBLIC_PLATFORM_URL origin in connect-src when set. Also loosens the dev `ws:` assertion since dev uses `connect-src *` which subsumes ws (pre-existing behavior, test was stale). Co-Authored-By: Claude Opus 4.7 (1M context) --- canvas/src/__tests__/csp-nonce.test.ts | 23 +++++++++++++++++++++-- canvas/src/middleware.ts | 22 +++++++++++++++++++++- 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/canvas/src/__tests__/csp-nonce.test.ts b/canvas/src/__tests__/csp-nonce.test.ts index 5ba50b6e..a76235aa 100644 --- a/canvas/src/__tests__/csp-nonce.test.ts +++ b/canvas/src/__tests__/csp-nonce.test.ts @@ -58,6 +58,21 @@ describe("buildCsp — production", () => { expect(connectSrc).toContain("wss:"); }); + it("adds NEXT_PUBLIC_PLATFORM_URL origin to connect-src (CP cross-origin fetches)", () => { + // Canvas calls fetch(PLATFORM_URL + "/cp/auth/me") etc. from the + // browser. The browser blocks cross-origin fetches that aren't in + // connect-src, even with CORS headers set server-side. Whitelisting + // the origin is what makes fresh login + session refresh work on + // tenant subdomains after they were broken pre-2026-04-20 by a CSP + // that only allowed 'self' and wss:. + process.env.NEXT_PUBLIC_PLATFORM_URL = "https://api.example.com"; + const built = buildCsp(TEST_NONCE, false); + const connectSrc = built.match(/connect-src[^;]*/)?.[0] ?? ""; + expect(connectSrc).toContain("https://api.example.com"); + expect(connectSrc).toContain("wss://api.example.com"); + delete process.env.NEXT_PUBLIC_PLATFORM_URL; + }); + it("does NOT include bare ws: in connect-src (prod uses wss only)", () => { const connectSrc = csp.match(/connect-src[^;]*/)?.[0] ?? ""; // ws: (without 's') is insecure — should not be in production policy @@ -95,9 +110,13 @@ describe("buildCsp — development", () => { expect(scriptSrc()).toContain("'unsafe-eval'"); }); - it("allows ws: in connect-src (HMR WebSocket uses plain ws://)", () => { + it("permits ws:// in connect-src (HMR WebSocket uses plain ws://)", () => { const connectSrc = csp.match(/connect-src[^;]*/)?.[0] ?? ""; - expect(connectSrc).toContain("ws:"); + // Dev uses `connect-src *` (fully permissive) which subsumes ws:. + // Accept either the literal `ws:` scheme wildcard OR the blanket + // `*` since both let HMR through. Prod tests still enforce `ws:` + // is NOT present. + expect(connectSrc).toMatch(/\bws:|\*/); }); }); diff --git a/canvas/src/middleware.ts b/canvas/src/middleware.ts index 37463768..9903a443 100644 --- a/canvas/src/middleware.ts +++ b/canvas/src/middleware.ts @@ -35,6 +35,26 @@ export function buildCsp(nonce: string, isDev: boolean): string { ].join("; ") + ";"; } + // Canvas makes cross-origin fetches to the control plane for + // /cp/auth/*, /cp/orgs/*, /cp/billing/* — PLATFORM_URL points at + // it (baked in at build time via NEXT_PUBLIC_PLATFORM_URL). CSP + // has to whitelist that origin in connect-src or the browser + // refuses the fetch with "Refused to connect because it violates + // the document's Content Security Policy." + // + // Self-hosted deployments override PLATFORM_URL at build time and + // the CSP adjusts automatically — no hardcoded hostname here. + const platformURL = process.env.NEXT_PUBLIC_PLATFORM_URL ?? ""; + const connectSrcParts = ["'self'", "wss:"]; + if (platformURL) { + connectSrcParts.push(platformURL); + // Also allow the wss:// sibling of PLATFORM_URL explicitly. + // `wss:` scheme-wildcard covers it today but making the exact + // origin explicit survives a future CSP tightening without + // silently breaking auth. + connectSrcParts.push(platformURL.replace(/^http/, "ws")); + } + return [ "default-src 'self'", // Nonce-based: no unsafe-inline, no unsafe-eval. @@ -49,7 +69,7 @@ export function buildCsp(nonce: string, isDev: boolean): string { "base-uri 'self'", "form-action 'self'", "frame-ancestors 'none'", - "connect-src 'self' wss:", + `connect-src ${connectSrcParts.join(" ")}`, "worker-src 'self' blob:", "upgrade-insecure-requests", ].join("; ") + ";";