依 autoflow-agent workspace v2 設計把 PRD / 設計 / 架構 / 交付類 共享文件從個人層 .autoflow/(ignored)搬到 docs/autoflow/(進 git), 讓團隊可共享產品與架構文件,個人層只留 progress / review / testing 等 per-branch 筆記。 - 02-prd/ 21 個檔(PRD、features、market-analysis 等) - 03-design/ 18 個檔(design-spec、wireframes、flows 等) - 04-architecture/ 31 個檔(TDD、design-doc、ADR×14、API 規格等) - 07-delivery/ 3 個檔(project-summary、phase-0.6-handover、stage-deployment-setup) 合計 73 檔。原檔已從 .autoflow/ 移除(migration 工具執行 git mv, 但因 .autoflow/ 在 .gitignore 中、git 將此操作視為新增、無 rename history)。
8.5 KiB
Internal API — api-server ↔ remote-proxy
雛形(Phase 0)就要實作(2026-04-22 Q1 裁決)。 雛形採雙 binary 部署:
api-server與remote-proxy各自為獨立 process,透過本文件定義的 HTTP 介面溝通。api-server無狀態;remote-proxy持有所有 tunnel session(in-memory,無 Redis,見 ADR-006)。
背景
雛形(單一 remote-proxy instance):
[瀏覽器] ─HTTPS─► [api-server] [remote-proxy] ←WS+yamux─ [local agent]
│ ▲
│ GET /internal/session/:token │
│ POST /internal/forward/http │
│ GET /internal/forward/ws │
└────────────────────────────────────┘
(internal HTTP,走內網 / localhost)
api-server不持有*yamux.Session;要走 tunnel 的請求一律轉發給remote-proxyremote-proxy持有map[token]*yamux.Session,本機記憶體
Phase 1 當 remote-proxy 水平擴展到多節點時,api-server 還需要有「找到對的 proxy」的能力(見 tunnel.md §5.4),但此 HTTP 介面的形狀不變。
端點
雙軌設計(B3 Review Major 1 修復 / 2026-04-22):
api-server ↔ remote-proxy 的 HTTP forward 提供兩條路徑:
Endpoint 封裝 支援 streaming 支援 WS upgrade 適合場景 POST /internal/forward/httpJSON + base64 body ❌ ❌ 一次性 JSON request/response(GET /healthz、POST /api/devices 等) POST /internal/forward/rawHijack 成 raw TCP ✅(MJPEG / SSE / chunked) ✅(只要 caller 按 HTTP 協定送 upgrade) session.ProxyClient.OpenStream(ctx) net.Conn的底層;B4 ProxyClient 預設走這條兩者並存是刻意為之 — JSON 版對簡單 API 好寫好測,raw 版是 OpenStream 語意的真實底層(B4 的
session.RemoteHandle必須走這條)。
POST /internal/forward/http(JSON 版)
用途:api-server 把一個一次性的 JSON request 轉發給 proxy 節點,由該節點透過 tunnel 打給 local agent。不支援 streaming;對 MJPEG / SSE / chunked body 應改走 /internal/forward/raw。
Request:
POST /internal/forward/http?<optional-token-in-query>
Content-Type: application/json
{
"session_token": "vAc_...",
"method": "GET",
"path": "/api/devices",
"headers": {"X-From-Api-Server": "value"},
"body": "<base64-encoded request body,可省略>"
}
Response(200 OK 或 502 Bad Gateway):
{
"status": 200,
"headers": { "Content-Type": ["application/json"] },
"body": "<base64-encoded response body>",
"error": null
}
失敗時:
{
"error": { "code": "TUNNEL_DISCONNECTED", "message": "session not connected" }
}
實作要點(proxy 端):
store.Lookup(token)找 handlehandle.OpenStream(ctx)開 yamux stream- 組
http.Request→req.Write(stream) http.ReadResponse(bufio.NewReader(stream), req)→io.ReadAll(body)→ base64 encode 回 JSON
POST /internal/forward/raw(raw bytes + Hijack 版,B3 Review Major 1 修復)
用途:把 api-server 的 HTTP 連線接管成 raw TCP,與 tunnel 裡的 yamux stream 雙向 pipe。支援任意 HTTP 流量(streaming body、長連線、WS upgrade),是 session.ProxyClient.OpenStream(ctx) net.Conn 語意的真實底層。
Request(api-server 送):
POST /internal/forward/raw?token=<session-token> HTTP/1.1
Host: <proxy-internal-host>
Content-Length: 0
Response 握手(proxy 在 session 找到後寫回):
HTTP/1.1 200 Connected
<空白行>
此時 HTTP 協議結束,proxy 呼叫 http.Hijacker.Hijack() 把底層連線交出,與 yamux stream 做雙向 io.Copy。api-server 此後把這條連線當成 net.Conn 直接用:
// api-server 端(B4 實作,示意)
conn := dial(/internal/forward/raw?token=xxx)
readHandshake(conn) // 讀 "HTTP/1.1 200 Connected" + 空白行
// 從這裡開始 conn 就是一條通到 local agent 的 raw TCP stream(透過 yamux)
req.Write(conn) // 送完整 HTTP request
resp, _ := http.ReadResponse(bufio.NewReader(conn), req)
io.Copy(browserResponseWriter, resp.Body) // streaming friendly
失敗處理:
- Session 不存在:在 hijack 之前回
502 JSON { error: TUNNEL_DISCONNECTED }(一般 HTTP client 可讀) - Hijacking 不支援:回
500 JSON { error: HIJACK_UNSUPPORTED } - Hijack 後
OpenStream失敗:在 hijacked 連線上寫回HTTP/1.1 502 Bad Gateway+ JSON body 再關閉(caller 拿到的仍是合法 HTTP response)
為何用 CONNECT-style 握手而非直接雙向 pipe?
- 需要一個明確的「session ready」訊號,避免 caller 不知道 session 是否存在就開始 write
- 與既有 HTTP 基礎設施相容(middleware / LB 能正確路由與紀錄一次 request)
- api-server 端 B4 實作時可以用標準
http.Client發第一個 request,拿到 underlying conn 後再切換成 raw 模式
Flusher 支援:不需要 — raw 模式下 io.Copy 天然支援任意 chunk / streaming 語意,由 yamux 負責在 TCP 上傳輸。
GET /internal/forward/ws(WebSocket upgrade)
用途:與 /http 同,但用於 WebSocket。api-server 收到瀏覽器 WS → 升級 → 在內部開一個到 proxy 的 HTTP Connection Upgrade。
Request:
GET /internal/forward/ws?token=<pairing-token>
Upgrade: websocket
... (原瀏覽器 WS upgrade request headers)
Response:
101 Switching Protocols
... (yamux stream 裡 local agent 回來的 101 response)
proxy 拿到 upgrade request 後:
session.Open()拿 stream- 把 upgrade request 寫進 stream(給 local agent)
- 讀 stream 回來的 101(從 local agent)
- Hijack caller 連線,與 stream 雙向 pipe
邏輯同 POC proxyWebSocket,只是「caller」換成 api-server(internal)。
GET /internal/session/:token
用途:proxy 自我查詢某個 token 的 session 狀態,debug / health 用。
Response:
{
"token": "pk_xxx",
"connected": true,
"connected_at": "...",
"last_seen_at": "...",
"stream_open_count": 3
}
POST /internal/session/:token/close
用途:後台運維強制斷某條 tunnel(使用者 revoke token 後)。
安全
- 內部 API 只監聽 internal network interface(VPC 私網、K8s ClusterIP)
- 防禦:
- 網路層面:security group / NetworkPolicy 只允許 api-server pod 連
- 應用層面(Phase 1+):mTLS 或 shared secret header
X-Internal-Auth: <bearer>
- 絕不對外暴露(公網 LB 白名單)
觀測
Metrics:
proxy_internal_forward_total{endpoint, result}proxy_internal_forward_duration_secondsproxy_stream_open_totalproxy_stream_error_total{reason}
Log:
event=forward path=/api/devices token_prefix=pk_abcd duration_ms=45 status=200
雛形行為(Phase 0 必做,Q1 裁決)
雛形就是雙 binary,本文件的所有 endpoints 都是 Phase 0 必須實作:
remote-proxy提供這些 internal HTTP endpointsapi-server透過ProxyClientStore呼叫這些 endpoints
雛形 API handler 端的呼叫方式:
// api-server 端
handle, err := sessionStore.Lookup(ctx, token) // 實際走 GET /internal/session/:token
if err != nil { return ErrTunnelDisconnected }
stream, err := handle.OpenStream(ctx) // RemoteHandle 實作:POST /internal/forward/raw(B3 Major 1 修復後)
// 寫 request、讀 response — 語義跟直接拿 yamux stream 一致
B4 實作提醒:
RemoteHandle.OpenStream必須走POST /internal/forward/raw(hijack raw TCP),不能走 JSON 版的/internal/forward/http- 對於簡單 JSON API 的同步呼叫(如 internal 健康檢查),仍可使用 JSON 版以簡化程式碼
Phase 1 多節點時:
- 介面完全不變
ProxyClientStore底層增加「找到正確 proxy 節點」的邏輯(見tunnel.md§5.4)
總結:
- ✅ 雛形:
api-server↔remote-proxy透過本文件定義的 HTTP 介面溝通(必做) - ✅ Phase 1:介面不變,底層 routing 策略升級
- ❌ 雛形 不走「api-server 與 remote-proxy 同進程共用 map」的捷徑(那條路會讓 api-server 變成有狀態,走不到 Phase 1)