akashyadav758 Claude Opus 4.8 (1M context) commited on
Commit
3666689
Β·
1 Parent(s): dff55ff

Run all 3 backend servers in-container behind a single API-key gateway

Browse files

The HF Space previously ran only Chrome + extensions + monitor; the ChatGPT/
Gemini/Flow API servers existed only in docker-compose (self-host). Because the
extensions dial their servers on hardcoded 127.0.0.1:9225/9226/9227, the servers
must share Chrome's localhost β€” a separate Space can't reach them. So:

- Dockerfile builds the chatgpt (Go) + gemini (Go) binaries and installs the flow
(Python/FastAPI) deps into the chrome image.
- start_hf.sh launches all three on localhost alongside Chrome.
- monitor/main.go doubles as a reverse-proxy gateway on :3001, routing /gpt /gemini
/flow to 127.0.0.1:9225/8000/8101, gated by an API_KEY bearer token (fail-closed).
- docker-compose.yml collapses to the single chrome service (servers are now baked in;
separate containers would collide on the same ports).
- Add cryptography to flow requirements (lazy-imported in cli/api.py).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files changed (7) hide show
  1. Dockerfile +25 -2
  2. README.md +40 -5
  3. SETUP.md +22 -0
  4. docker-compose.yml +14 -39
  5. flow-agent/requirements.txt +1 -0
  6. monitor/main.go +66 -0
  7. start_hf.sh +8 -0
Dockerfile CHANGED
@@ -10,6 +10,18 @@ WORKDIR /src
10
  COPY profilesync/ ./
11
  RUN go mod tidy && CGO_ENABLED=0 go build -ldflags="-s -w" -o /profilesync .
12
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  FROM akashyadav758/chrome:latest
14
 
15
  USER root
@@ -25,6 +37,17 @@ COPY --from=monitor-build /monitor /opt/monitor3/monitor
25
  # Profile-sync binary (Chrome profile <-> Postgres snapshot)
26
  COPY --from=profilesync-build /profilesync /opt/profilesync/profilesync
27
 
 
 
 
 
 
 
 
 
 
 
 
28
  # Install Chrome for Testing (unbranded). Branded google-chrome-stable (>=128)
29
  # silently ignores --load-extension, so unpacked extensions never load. CfT is the
30
  # unbranded build of the same Chrome version where --load-extension still works.
@@ -38,8 +61,8 @@ RUN apt-get update && apt-get install -y --no-install-recommends unzip && \
38
 
39
  # Set correct permissions and ownership
40
  RUN rm -rf /opt/flow-extension/_metadata && \
41
- chown -R 1000:1000 /opt/gpt-extension /opt/gemini-extension /opt/flow-extension /opt/chrome-linux64 /opt/monitor3 /opt/profilesync && \
42
- chmod -R 755 /opt/gpt-extension /opt/gemini-extension /opt/flow-extension /opt/chrome-linux64 /opt/monitor3 /opt/profilesync
43
 
44
  # Replace the start script with the non-root version
45
  COPY --chmod=755 start_hf.sh /start.sh
 
10
  COPY profilesync/ ./
11
  RUN go mod tidy && CGO_ENABLED=0 go build -ldflags="-s -w" -o /profilesync .
12
 
13
+ # Build the ChatGPT API server (talks to gpt-extension on 127.0.0.1:9225).
14
+ FROM golang:1.26-bookworm AS chatgpt-build
15
+ WORKDIR /src
16
+ COPY chatgpt-free-api/ ./
17
+ RUN go mod download && CGO_ENABLED=0 go build -ldflags="-s -w" -o /agent .
18
+
19
+ # Build the Gemini API server (talks to gemini-extension on 127.0.0.1:9226).
20
+ FROM golang:1.26-bookworm AS gemini-build
21
+ WORKDIR /src
22
+ COPY free-gemini-api/ ./
23
+ RUN go mod download && CGO_ENABLED=0 go build -ldflags="-s -w" -o /free-gemini-api .
24
+
25
  FROM akashyadav758/chrome:latest
26
 
27
  USER root
 
37
  # Profile-sync binary (Chrome profile <-> Postgres snapshot)
38
  COPY --from=profilesync-build /profilesync /opt/profilesync/profilesync
39
 
40
+ # Backend API servers (run on localhost; the monitor gateway fronts them).
41
+ COPY --from=chatgpt-build /agent /opt/chatgpt/agent
42
+ COPY chatgpt-free-api/config.json /opt/chatgpt/config.json
43
+ COPY --from=gemini-build /free-gemini-api /opt/gemini-srv/free-gemini-api
44
+ COPY flow-agent/ /opt/flow-srv/
45
+
46
+ # Flow server is Python (FastAPI/uvicorn). Base image has python3; add pip + deps.
47
+ RUN apt-get update && apt-get install -y --no-install-recommends python3-pip && \
48
+ pip3 install --no-cache-dir --break-system-packages -r /opt/flow-srv/requirements.txt && \
49
+ apt-get clean && rm -rf /var/lib/apt/lists/*
50
+
51
  # Install Chrome for Testing (unbranded). Branded google-chrome-stable (>=128)
52
  # silently ignores --load-extension, so unpacked extensions never load. CfT is the
53
  # unbranded build of the same Chrome version where --load-extension still works.
 
61
 
62
  # Set correct permissions and ownership
63
  RUN rm -rf /opt/flow-extension/_metadata && \
64
+ chown -R 1000:1000 /opt/gpt-extension /opt/gemini-extension /opt/flow-extension /opt/chrome-linux64 /opt/monitor3 /opt/profilesync /opt/chatgpt /opt/gemini-srv /opt/flow-srv && \
65
+ chmod -R 755 /opt/gpt-extension /opt/gemini-extension /opt/flow-extension /opt/chrome-linux64 /opt/monitor3 /opt/profilesync /opt/chatgpt /opt/gemini-srv /opt/flow-srv
66
 
67
  # Replace the start script with the non-root version
68
  COPY --chmod=755 start_hf.sh /start.sh
README.md CHANGED
@@ -46,10 +46,11 @@ run yourself β€” see each subproject's README for setup):
46
  | [`free-gemini-api`](free-gemini-api/README.md) | Go reverse-proxy | local server | Gemini text / Imagen 3 images / Gemini video |
47
  | [`flow-agent`](flow-agent/README.md) | Python FastAPI + CLI | `127.0.0.1:8100` | Google Flow T2V / V2V / I2V video + T2I / I2I images |
48
 
49
- > ⚠️ The HF Space builds **only the root `Dockerfile`** β†’ it runs **the browser + the 3
50
- > extensions + the monitor**. It does **not** start the backend servers. To run the full
51
- > stack (browser **and** all three API servers sharing one loopback), use
52
- > `docker-compose.yml` for self-hosting β€” see [Self-hosting](#self-hosting-full-stack).
 
53
 
54
  ---
55
 
@@ -171,13 +172,47 @@ exact UA β€” stays valid across the browser and the backend.
171
 
172
  | URL | Description |
173
  |-----|-------------|
174
- | `https://akash1313-selfapi.hf.space/` | Live Chrome monitor UI |
175
  | `https://akash1313-selfapi.hf.space/chrome.log` | Streaming Chrome log (debugging) |
 
 
 
176
  | `127.0.0.1:9222` (in-container) | Chrome DevTools Protocol |
177
  | `:9223` | CDP proxied for external tools (via socat) |
178
 
179
  ---
180
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
181
  ## Project layout
182
 
183
  ```
 
46
  | [`free-gemini-api`](free-gemini-api/README.md) | Go reverse-proxy | local server | Gemini text / Imagen 3 images / Gemini video |
47
  | [`flow-agent`](flow-agent/README.md) | Python FastAPI + CLI | `127.0.0.1:8100` | Google Flow T2V / V2V / I2V video + T2I / I2I images |
48
 
49
+ > βœ… The HF Space now runs **all three servers in the same container** as Chrome, and the
50
+ > monitor doubles as a **single API gateway** that fronts them on one public port β€” see
51
+ > [API gateway](#api-gateway). (The extensions dial their servers on hardcoded
52
+ > `127.0.0.1:9225/9226/9227`, so the servers *must* share Chrome's localhost β€” a separate
53
+ > Space can't reach them.) `docker-compose.yml` does the same wiring for self-hosting.
54
 
55
  ---
56
 
 
172
 
173
  | URL | Description |
174
  |-----|-------------|
175
+ | `https://akash1313-selfapi.hf.space/` | Live Chrome monitor UI (open, no key) |
176
  | `https://akash1313-selfapi.hf.space/chrome.log` | Streaming Chrome log (debugging) |
177
+ | `https://akash1313-selfapi.hf.space/gpt/…` | ChatGPT API (gateway β†’ `127.0.0.1:9225`) |
178
+ | `https://akash1313-selfapi.hf.space/gemini/…` | Gemini API (gateway β†’ `127.0.0.1:8000`) |
179
+ | `https://akash1313-selfapi.hf.space/flow/…` | Flow API (gateway β†’ `127.0.0.1:8101`) |
180
  | `127.0.0.1:9222` (in-container) | Chrome DevTools Protocol |
181
  | `:9223` | CDP proxied for external tools (via socat) |
182
 
183
  ---
184
 
185
+ ## API gateway
186
+
187
+ The monitor (`monitor/main.go`) is also a reverse-proxy gateway: HF exposes only one public
188
+ port (`3001`), so all three backend APIs are reached under one base URL by path prefix. The
189
+ gateway strips the prefix and forwards to the server on localhost:
190
+
191
+ | Prefix | Backend | Example endpoints |
192
+ |--------|---------|-------------------|
193
+ | `/gpt/` | chatgpt-free-api `:9225` | `/gpt/api/chat`, `/gpt/v1/chat/completions`, `/gpt/health` |
194
+ | `/gemini/` | free-gemini-api `:8000` | `/gemini/chat`, `/gemini/status`, `/gemini/output/*` |
195
+ | `/flow/` | flow-agent `:8101` | `/flow/generate/video`, `/flow/generate/image`, `/flow/health` |
196
+
197
+ ### Auth β€” required
198
+
199
+ The three prefixes are gated by an **`API_KEY`** Space secret. Every request must send it as a
200
+ Bearer token (or `?key=`). **Fail-closed:** if `API_KEY` is unset the gateway returns `503`, so
201
+ the logged-in accounts are never accidentally exposed. The monitor UI (`/`, `/chrome.log`) stays
202
+ open.
203
+
204
+ 1. Set the Space secret **`API_KEY`** (Settings β†’ Variables and secrets) to a strong random string.
205
+ 2. Call it:
206
+ ```bash
207
+ curl -X POST https://akash1313-selfapi.hf.space/gpt/api/chat \
208
+ -H "Authorization: Bearer $API_KEY" \
209
+ -H "Content-Type: application/json" \
210
+ -d '{"message":"hello"}'
211
+ ```
212
+ No/!wrong key β†’ `401`. Wrong path β†’ forwarded to the backend (may `404` there).
213
+
214
+ ---
215
+
216
  ## Project layout
217
 
218
  ```
SETUP.md CHANGED
@@ -52,3 +52,25 @@ HF free Spaces have **no permanent disk**, so there logins need a Postgres datab
52
  4. The Space restarts β†’ **log in once** β†’ it auto-saves every 5 min and survives rebuilds.
53
 
54
  > Deploy code to HF with `git push`. Never commit the connection string.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
  4. The Space restarts β†’ **log in once** β†’ it auto-saves every 5 min and survives rebuilds.
53
 
54
  > Deploy code to HF with `git push`. Never commit the connection string.
55
+
56
+ ---
57
+
58
+ ## Using the API
59
+
60
+ All three servers run inside the container; the monitor fronts them on one URL by path:
61
+
62
+ | Service | Base path | Example |
63
+ |---------|-----------|---------|
64
+ | ChatGPT | `/gpt/` | `POST /gpt/api/chat` |
65
+ | Gemini | `/gemini/` | `POST /gemini/chat` |
66
+ | Flow | `/flow/` | `POST /flow/generate/video` |
67
+
68
+ **Set an `API_KEY` secret** (HF: Settings β†’ secret `API_KEY`; Docker: add to `.env`) β€” every API
69
+ call must send it:
70
+ ```bash
71
+ curl -X POST https://YOUR-SPACE.hf.space/gpt/api/chat \
72
+ -H "Authorization: Bearer YOUR_API_KEY" \
73
+ -H "Content-Type: application/json" \
74
+ -d '{"message":"hello"}'
75
+ ```
76
+ Without the key the API returns `401`/`503`. The monitor UI stays open (no key).
docker-compose.yml CHANGED
@@ -1,13 +1,16 @@
1
- # Runs the headless Chrome + 3-tab monitor together with the three backend servers.
 
2
  #
3
- # The server containers join the chrome container's network namespace
4
- # (network_mode: "service:chrome"), so they all share one 127.0.0.1 β€” exactly what the
5
- # extensions expect. Only the chrome service publishes ports.
 
6
  #
7
- # Port map (shared loopback):
8
- # chrome : 9222 CDP, 3001 monitor UI
9
- # chatgpt : 9225 (ws+http)
10
- # gemini : 8000 http, 9226 cookie-WS
 
11
  # flow : 8101 http API, 8100 ext-callback, 9227 cookie-WS
12
 
13
  services:
@@ -15,41 +18,13 @@ services:
15
  build: .
16
  platform: linux/amd64
17
  ports:
18
- - "3001:3001" # 3-tab monitor UI (also the only published port)
 
 
19
  volumes:
20
  - chrome-profile:/home/chrome/data # persists cookies/logins across restarts
21
  restart: unless-stopped
22
 
23
- chatgpt:
24
- build: ./chatgpt-free-api
25
- platform: linux/amd64
26
- network_mode: "service:chrome"
27
- depends_on:
28
- - chrome
29
- restart: unless-stopped
30
-
31
- gemini:
32
- build: ./free-gemini-api
33
- platform: linux/amd64
34
- environment:
35
- WS_PORT: "9226"
36
- PORT: "8000"
37
- network_mode: "service:chrome"
38
- depends_on:
39
- - chrome
40
- restart: unless-stopped
41
-
42
- flow:
43
- build: ./flow-agent
44
- platform: linux/amd64
45
- environment:
46
- WS_PORT: "9227"
47
- HTTP_PORT: "8100"
48
- network_mode: "service:chrome"
49
- depends_on:
50
- - chrome
51
- restart: unless-stopped
52
-
53
  # Named volume keeps the Chrome profile (logins/cookies) across `docker compose down/up`
54
  # and host reboots β€” the self-host equivalent of HF persistent storage, no Postgres needed.
55
  volumes:
 
1
+ # Runs the whole stack in one container: headless Chrome + 3 extensions + the monitor
2
+ # (which also serves the API gateway) + the three backend servers.
3
  #
4
+ # The chrome image (built from ./Dockerfile) now bakes in all three servers and start_hf.sh
5
+ # launches them on localhost alongside Chrome β€” exactly what the extensions expect (they dial
6
+ # 127.0.0.1:9225/9226/9227). So there are no separate server containers anymore; that would
7
+ # collide on the same ports. Only port 3001 is published.
8
  #
9
+ # Internal port map (all on the container's 127.0.0.1):
10
+ # chrome : 9222 CDP, 9223 CDP-proxy
11
+ # monitor : 3001 UI + API gateway (/gpt /gemini /flow)
12
+ # chatgpt : 9225 (ws + http API)
13
+ # gemini : 8000 http API, 9226 cookie-WS
14
  # flow : 8101 http API, 8100 ext-callback, 9227 cookie-WS
15
 
16
  services:
 
18
  build: .
19
  platform: linux/amd64
20
  ports:
21
+ - "3001:3001" # monitor UI + API gateway (the only published port)
22
+ environment:
23
+ API_KEY: ${API_KEY:-} # gates /gpt /gemini /flow; from .env. Unset = APIs disabled (503)
24
  volumes:
25
  - chrome-profile:/home/chrome/data # persists cookies/logins across restarts
26
  restart: unless-stopped
27
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  # Named volume keeps the Chrome profile (logins/cookies) across `docker compose down/up`
29
  # and host reboots β€” the self-host equivalent of HF persistent storage, no Postgres needed.
30
  volumes:
flow-agent/requirements.txt CHANGED
@@ -2,3 +2,4 @@ websockets>=12.0
2
  fastapi>=0.100.0
3
  uvicorn>=0.22.0
4
  python-multipart>=0.0.6
 
 
2
  fastapi>=0.100.0
3
  uvicorn>=0.22.0
4
  python-multipart>=0.0.6
5
+ cryptography>=42.0 # lazy-imported in cli/api.py for self-signed cert generation
monitor/main.go CHANGED
@@ -18,6 +18,8 @@ import (
18
  "io"
19
  "log"
20
  "net/http"
 
 
21
  "os"
22
  "sort"
23
  "strings"
@@ -496,6 +498,62 @@ func statsHandler(w http.ResponseWriter, r *http.Request) {
496
  })
497
  }
498
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
499
  func main() {
500
  // Pre-open the known AI services in the background so they're ready by first view.
501
  go func() {
@@ -534,6 +592,14 @@ func main() {
534
  http.HandleFunc("/api/stats", statsHandler)
535
  http.HandleFunc("/chrome.log", chromeLogHandler)
536
 
 
 
 
 
 
 
 
 
537
  addr := ":3001"
538
  if p := os.Getenv("MONITOR_PORT"); p != "" {
539
  addr = ":" + p
 
18
  "io"
19
  "log"
20
  "net/http"
21
+ "net/http/httputil"
22
+ "net/url"
23
  "os"
24
  "sort"
25
  "strings"
 
498
  })
499
  }
500
 
501
+ // ---- API gateway ----------------------------------------------------------
502
+ // The monitor doubles as the single public entry point for the three backend
503
+ // servers that run on localhost inside this same container. HF exposes only one
504
+ // port (3001), so /gpt, /gemini and /flow are reverse-proxied to the servers'
505
+ // loopback HTTP ports. All three are gated by an API key (fail-closed).
506
+
507
+ var apiKey = os.Getenv("API_KEY")
508
+
509
+ // authOK enforces the API key on gateway routes. If API_KEY is unset it refuses
510
+ // every call (503) so the logged-in accounts are never accidentally wide open.
511
+ func authOK(w http.ResponseWriter, r *http.Request) bool {
512
+ if apiKey == "" {
513
+ http.Error(w, "gateway disabled: API_KEY not set on the server", http.StatusServiceUnavailable)
514
+ return false
515
+ }
516
+ got := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
517
+ if got == "" {
518
+ got = r.URL.Query().Get("key")
519
+ }
520
+ if got != apiKey {
521
+ http.Error(w, "unauthorized", http.StatusUnauthorized)
522
+ return false
523
+ }
524
+ return true
525
+ }
526
+
527
+ // gatewayHandler builds an API-key-gated reverse proxy that strips prefix and
528
+ // forwards to target (e.g. /gpt/api/chat -> http://127.0.0.1:9225/api/chat).
529
+ func gatewayHandler(prefix, target string) http.HandlerFunc {
530
+ u, err := url.Parse(target)
531
+ if err != nil {
532
+ log.Fatalf("bad gateway target %q: %v", target, err)
533
+ }
534
+ proxy := &httputil.ReverseProxy{
535
+ Director: func(r *http.Request) {
536
+ r.URL.Scheme = u.Scheme
537
+ r.URL.Host = u.Host
538
+ r.Host = u.Host
539
+ p := strings.TrimPrefix(r.URL.Path, prefix)
540
+ if p == "" || p[0] != '/' {
541
+ p = "/" + p
542
+ }
543
+ r.URL.Path = p
544
+ },
545
+ ErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) {
546
+ http.Error(w, "backend unavailable: "+err.Error(), http.StatusBadGateway)
547
+ },
548
+ }
549
+ return func(w http.ResponseWriter, r *http.Request) {
550
+ if !authOK(w, r) {
551
+ return
552
+ }
553
+ proxy.ServeHTTP(w, r)
554
+ }
555
+ }
556
+
557
  func main() {
558
  // Pre-open the known AI services in the background so they're ready by first view.
559
  go func() {
 
592
  http.HandleFunc("/api/stats", statsHandler)
593
  http.HandleFunc("/chrome.log", chromeLogHandler)
594
 
595
+ // API gateway β†’ the three backend servers on localhost (API-key gated).
596
+ http.HandleFunc("/gpt/", gatewayHandler("/gpt", "http://127.0.0.1:9225"))
597
+ http.HandleFunc("/gemini/", gatewayHandler("/gemini", "http://127.0.0.1:8000"))
598
+ http.HandleFunc("/flow/", gatewayHandler("/flow", "http://127.0.0.1:8101"))
599
+ if apiKey == "" {
600
+ log.Println("WARNING: API_KEY not set β€” /gpt /gemini /flow are disabled (503) until it is set")
601
+ }
602
+
603
  addr := ":3001"
604
  if p := os.Getenv("MONITOR_PORT"); p != "" {
605
  addr = ":" + p
start_hf.sh CHANGED
@@ -106,6 +106,14 @@ touch /home/chrome/chrome.log
106
  echo "πŸ”Œ Starting CDP Proxy..."
107
  socat TCP-LISTEN:9223,fork,bind=0.0.0.0 TCP:127.0.0.1:9222 &
108
 
 
 
 
 
 
 
 
 
109
  # 6b. Periodic profile backup (safety net if HF kills the container without a clean
110
  # SIGTERM). Best-effort every 5 minutes; the shutdown trap does the final one.
111
  if [ -n "$DATABASE_URL" ]; then
 
106
  echo "πŸ”Œ Starting CDP Proxy..."
107
  socat TCP-LISTEN:9223,fork,bind=0.0.0.0 TCP:127.0.0.1:9222 &
108
 
109
+ # 6a. Start the three backend API servers on localhost. The monitor doubles as a
110
+ # gateway and fronts them on :3001 (/gpt, /gemini, /flow). Each logs separately
111
+ # and runs in the background so a crash never blocks the boot.
112
+ echo "🧩 Starting backend API servers (gpt / gemini / flow)..."
113
+ ( cd /opt/chatgpt && ./agent >> /home/chrome/chatgpt.log 2>&1 & )
114
+ ( cd /opt/gemini-srv && WS_PORT=9226 PORT=8000 ./free-gemini-api >> /home/chrome/gemini.log 2>&1 & )
115
+ ( cd /opt/flow-srv && WS_PORT=9227 HTTP_PORT=8100 python3 -m uvicorn cli.api:app --host 127.0.0.1 --port 8101 >> /home/chrome/flow.log 2>&1 & )
116
+
117
  # 6b. Periodic profile backup (safety net if HF kills the container without a clean
118
  # SIGTERM). Best-effort every 5 minutes; the shutdown trap does the final one.
119
  if [ -n "$DATABASE_URL" ]; then