Deployment of OpenChatCut — an open-source, local-first conversational AI video editor (Node 24, React/Vite 8, Remotion rendering, Agent Skills, MCP) — as a self-hosted web service + MCP on telep-mainframe, behind a dedicated Tailscale sidecar node chatcut per the house per-service sidecar convention. All verified working 2026-08-31.

This host has a CPU hardware fault AND this app does video rendering

telep-mainframe panics under sustained all-core load — 2026-08-31-telep-mainframe-mce-hardware-fault. OpenChatCut runs Remotion video renders = exactly that load profile. Mitigations are in place (CPU cap + render concurrency 1, see Safety) but avoid large batch exports. This is the single most important operational caveat on this page.

No app-level auth — tailnet ACL is the ONLY gate

OpenChatCut has no login (it assumes single-user-on-localhost). Exposed on the tailnet it’s reachable by anyone the ACL lets reach the chatcut (tag:telep) node, and the tailnet is multi-user. If owner-only is desired, tighten the tag:telep grant in the ACL. The MCP endpoint is separately Bearer-token protected; the web UI is not.

For Agents — quick facts

  • Web UI + MCP host: https://chatcut.taild4189d.ts.net (tailnet-only, no Funnel, LetsEncrypt via tailscale serve)
  • MCP endpoint: https://chatcut.taild4189d.ts.net/api/external-mcp/mcp (Streamable HTTP, Bearer auth, protocol 2025-06-18, serverInfo.name="openchatcut")
  • Registered in Claude Code at user scope, status Connected
  • Backend loopback: http://127.0.0.2:5199 (Vite dev server, in Docker container openchatcut)
  • App root on host: /home/levander/openchatcut/ — data persists at /home/levander/openchatcut/data

Access

  • Web UI + MCP: https://chatcut.taild4189d.ts.net — tailnet-only, no Funnel, LetsEncrypt cert issued by tailscale serve.
  • MCP endpoint: https://chatcut.taild4189d.ts.net/api/external-mcp/mcp (Streamable HTTP, Bearer-token auth). Registered in Claude Code at user scope:
    claude mcp add --transport http --scope user chatcut https://chatcut.taild4189d.ts.net/api/external-mcp/mcp
    
    Status: Connected.
  • Token: stored server-side in /home/levander/openchatcut/.env.local (chmod 600) as OPENCHATCUT_MCP_TOKEN; also saved (redacted) in ~/.claude.json. Not reproduced here.

MCP usage nuance

initialize works headless, but timeline/editing tools require binding a target_project and generally a connected browser — open the editorUrl (the web UI) for those to function.

Architecture

Per the house sidecar convention (same shape as the code-server stack): a dedicated userspace tailscaled fronts a loopback-only backend port.

tailnet client (identified by Tailscale, ACL tag:telep)
   │  HTTPS, tailnet-only, LE cert for chatcut.taild4189d.ts.net
   ▼
tailscaled-chatcut.service     --tun=userspace-networking
   socket   /run/tailscale-chatcut/tailscaled.sock
   statedir /var/lib/tailscale-chatcut
   node chatcut = 100.85.231.40, tag:telep
   │  tailscale serve  https://chatcut.taild4189d.ts.net/  →  http://127.0.0.2:5199
   ▼
127.0.0.2:5199                 Docker container `openchatcut` (Vite dev server)

Tailscale sidecar

  • Unit: /etc/systemd/system/tailscaled-chatcut.service — userspace-networking, statedir /var/lib/tailscale-chatcut, socket /run/tailscale-chatcut/tailscaled.sock.
  • Node chatcut = 100.85.231.40, tag:telep. Provisioned via the Aperture Tailnet_provision_node one-time auth-key flow, then admin-approved + tagged tag:telep in the console.
  • Serve mount: https://chatcut.taild4189d.ts.net/http://127.0.0.2:5199. Backend bound to 127.0.0.2 (not 127.0.0.1) per the plaintext-port-hardening rule.

Docker

  • Image openchatcut:local, built from /home/levander/openchatcut/Dockerfile. No upstream Dockerfile existed — hand-written.
  • Container run (host networking, updated 2026-08-31 — see Editor trust-model vs reverse proxy (fixed 2026-08-31) for why -p bridge was dropped):
    docker run -d --name openchatcut --restart unless-stopped \
      --cpus=8 --memory=16g \
      --network host \
      --env-file /home/levander/openchatcut/.env.local \
      -v /home/levander/openchatcut/data:/root/.openchatcut \
      openchatcut:local \
      npm run dev -- --host 127.0.0.2
    
    Vite binds 127.0.0.2 only (loopback) — verify with ss -ltnp that it shows 127.0.0.2:5199 and NOT 0.0.0.0/LAN/tailnet, preserving the plaintext-port-hardening rule. .env.local HOST is set to 127.0.0.2.
  • Data persists: host /home/levander/openchatcut/data → container /root/.openchatcut.

Dockerfile shape

FROM node:24-bookworm → apt git ffmpeg chromium fonts-liberation → ENV to use system chromium for Remotion (PUPPETEER_SKIP_DOWNLOAD=1, REMOTION_CHROME_EXECUTABLE=/usr/bin/chromium, PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1, ONNXRUNTIME_NODE_INSTALL=skip) → git clonenpm installcp .env.example .env.localnpm run predev → the Vite host patch (below) → COPY patch-trust.mjs + RUN node /tmp/patch-trust.mjs (the trust-model patch, see Editor trust-model vs reverse proxy (fixed 2026-08-31)) → CMD npm run dev -- --host 0.0.0.0.

The CMD host is overridden at runtime by the run command’s npm run dev -- --host 127.0.0.2 (loopback-only bind, required for the trust-model fix).

Runs off npm run dev (a Vite dev server) — upstream provides no production/headless server command, so this is the supported path.

Gotchas discovered

1. Vite allowedHosts blocks the tailscale-serve hostname (403)

The Vite-8 equivalent of the FastMCP 421 Host-header trap seen on 2026-08-31-telep-kb-mcp-server / drive-mcp. Behind tailscale serve, requests arrive with Host: chatcut.taild4189d.ts.net, and Vite’s dev server returns 403 “Blocked request. This host is not allowed” for BOTH the UI and the MCP endpoint. Vite 8 has no --allowed-hosts CLI flag (only --host). Fix baked into the Dockerfile — note the server block lives in config/vite.config.ts, not a top-level vite.config:

RUN sed -i "s|port: 5199,|port: 5199,\n      allowedHosts: ['chatcut.taild4189d.ts.net'],|" config/vite.config.ts
  1. Vite binds localhost by default and does NOT read the CRA-style HOST env — must pass --host 0.0.0.0 through to Vite via npm run dev -- --host 0.0.0.0. The repo’s scripts/dev-profile.mjs forwards argv to Vite.

  2. onnxruntime-node postinstall tries to download CUDA/GPU execution-provider libs on linux/x64 (fails + hits an unroutable Akamai edge via MagicDNS). Set ONNXRUNTIME_NODE_INSTALL=skip — keeps the bundled CPU binary.

  3. Docker buildkit couldn’t resolve DNS (didn’t inherit host MagicDNS 100.100.100.100) — build with --network=host.

  4. Runs off npm run dev (a Vite dev server); upstream provides no production/headless server command — that is the supported path.

Safety

  • MCE hardware fault mitigations: Docker --cpus=8 hard cap, plus app env OPENCHATCUT_RENDER_CONCURRENCY=1 and OPENCHATCUT_MAX_ACTIVE_EXPORTS=1 in .env.local. Still avoid large batch exports — see the danger callout at top and 2026-08-31-telep-mainframe-mce-hardware-fault.
  • No app-level auth: access is governed entirely by the tailnet ACL for tag:telep on a multi-user tailnet. Tighten that grant if owner-only is required. MCP endpoint is separately Bearer-protected.

Editor trust-model vs reverse proxy (fixed 2026-08-31)

After the initial deploy the MCP endpoint worked, but the web editor UI showed “Could not load the MCP connection token. Retry from a trusted editor window.” and editor writes failed with 403 {"error":"invalid request origin"}. The MCP Bearer-token path was exempt and fine — this only hit the browser editor.

Root cause — OpenChatCut's hardcoded "local-device trust model"

Implemented in server/project-store-http-auth.ts + server/plugins/request-shape-gate.ts. Every state-changing request must satisfy ALL of:

  1. loopback socketisLoopbackAddress(req.socket.remoteAddress)
  2. loopback Host headerloopbackHost() requires Host ∈ {localhost, 127.0.0.1, ::1} (a DNS-rebinding defense)
  3. same-originOrigin.host === Host
  4. Sec-Fetch-Site — same-origin or none

Only /api/external-mcp/mcp (Bearer route) and /upload?handoff are exempt. This assumes the browser talks to http://localhost:5199 and is fundamentally incompatible with serving via a real hostname behind tailscale serve.

Two distinct failures and their fixes:

(2) loopback-socket check — fixed by dropping the Docker bridge

Failed first because the container ran on a Docker bridge with -p 127.0.0.2:5199:5199; the app saw the request source as the Docker gateway (172.17.0.1), not loopback.

Fix: run the container on --network host, binding Vite to --host 127.0.0.2 (loopback only — verified via ss -ltnp that it shows 127.0.0.2:5199 and NOT 0.0.0.0/LAN/tailnet, preserving the plaintext-port-hardening rule). Then tailscale serve127.0.0.2:5199 and the app sees a real loopback source. See the updated run command in Docker.

(2b) loopback-Host check — fixed by an env-derived app patch

Still failed after (2) because behind serve the Host is chatcut.taild4189d.ts.net. No proxy trick fixes this: Origin is browser-set from where the page loaded, so rewriting Host would break the same-origin check.

Fix: patched loopbackHost() to ALSO accept the host of OPENCHATCUT_EDITOR_URL (env-derived, not hardcoded). Patch script /home/levander/openchatcut/patch-trust.mjs, baked into the Dockerfile (COPY patch-trust.mjs + RUN node /tmp/patch-trust.mjs).

Verified: POST https://chatcut.taild4189d.ts.net/api/external-agent/bootstrap (with Origin + Sec-Fetch-Site: same-origin + x-openchatcut-editor-bootstrap: 1) now returns 200 with the mcpToken.

Security note (deliberate, bounded relaxation)

Patching the DNS-rebinding host check is safe in THIS deployment because:

  • the node is reachable only over the ACL-gated tailnet (loopback-socket ingress = tailscale serve only);
  • the same-origin + Sec-Fetch-Site CSRF defenses remain fully intact.

A random web page cannot make chatcut.taild4189d.ts.net resolve for a victim (MagicDNS-controlled) nor forge the same-origin Origin. Do NOT copy this relaxation to any Funnel-exposed or public service.

Reusable lesson — local-first apps behind tailscale serve fail in THREE Host/origin layers

All three are Host/origin-related and should be expected for any future local-first service:

  • (a) Vite allowedHosts 403 (see Gotchas discovered #1)
  • (b) loopback-socket check needs --network host, not a bridge
  • (c) loopback-Host-header trust check needs an app patch

Same family as the FastMCP 421 Host-header trap (project_telep_kb_mcp / telep-kb, drive-mcp).