From e76a0b37d0899378661c9f35f3bda92bb8813818 Mon Sep 17 00:00:00 2001
From: UniDesk Codex
Date: Tue, 30 Jun 2026 21:40:24 +0800
Subject: [PATCH] fix: bootstrap opencode default project
---
deploy/deploy.yaml | 1 +
internal/dev-entrypoint/cloud-web-runtime.mjs | 99 ++++++++++++++++---
.../dev-entrypoint/cloud-web-runtime.test.mjs | 70 +++++++++++++
scripts/gitops-render.test.ts | 4 +
4 files changed, 162 insertions(+), 12 deletions(-)
diff --git a/deploy/deploy.yaml b/deploy/deploy.yaml
index c40042a4..8bf1901a 100644
--- a/deploy/deploy.yaml
+++ b/deploy/deploy.yaml
@@ -586,6 +586,7 @@ lanes:
HWLAB_CLOUD_WEB_OPENCODE_UPSTREAM_URL: http://opencode-server.hwlab-v03.svc.cluster.local:4096
HWLAB_CLOUD_WEB_OPENCODE_USERNAME: secretRef:hwlab-opencode-server-auth/username
HWLAB_CLOUD_WEB_OPENCODE_PASSWORD: secretRef:hwlab-opencode-server-auth/password
+ HWLAB_CLOUD_WEB_OPENCODE_DEFAULT_PROJECT: /workspace
HWLAB_CLOUD_WEB_OPENCODE_EVENT_DIRECTORY_FROM: /workspace
HWLAB_CLOUD_WEB_OPENCODE_EVENT_DIRECTORY_TO: /
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: http://otel-collector.platform-infra.svc.cluster.local:4318/v1/traces
diff --git a/internal/dev-entrypoint/cloud-web-runtime.mjs b/internal/dev-entrypoint/cloud-web-runtime.mjs
index d09749d7..7ea264c8 100644
--- a/internal/dev-entrypoint/cloud-web-runtime.mjs
+++ b/internal/dev-entrypoint/cloud-web-runtime.mjs
@@ -5,7 +5,7 @@ import path from "node:path";
import { gzip } from "node:zlib";
import { promisify } from "node:util";
-import { isCloudWebSseRoute, proxyCloudApiRequest, upstreamRequestHeaders } from "./cloud-web-proxy.mjs";
+import { copyProxyResponseHeaders, isCloudWebSseRoute, proxyCloudApiRequest, upstreamRequestHeaders } from "./cloud-web-proxy.mjs";
import { cloudWebProxyRoutePolicy } from "./cloud-web-routes.mjs";
import { UPSTREAM_UNAVAILABLE_ERROR_CODE, UPSTREAM_UNAVAILABLE_MESSAGE } from "./http.mjs";
const gzipAsync = promisify(gzip);
@@ -64,6 +64,7 @@ export function createCloudWebServer({
}),
opencodeUsername = optionalRuntimeConfigEnv("HWLAB_CLOUD_WEB_OPENCODE_USERNAME") || "",
opencodePassword = optionalRuntimeConfigEnv("HWLAB_CLOUD_WEB_OPENCODE_PASSWORD") || "",
+ opencodeDefaultProject = optionalRuntimeConfigEnv("HWLAB_CLOUD_WEB_OPENCODE_DEFAULT_PROJECT") || "",
opencodeEventDirectoryRewrite = opencodeEventDirectoryRewriteFromEnv()
}) {
return createServer(async (request, response) => {
@@ -79,7 +80,7 @@ export function createCloudWebServer({
return;
}
if (isOpencodeProxyRequest(request, url, opencodeProxyHost)) {
- await proxyOpencodeRequest({ request, response, url, cloudApiBaseUrl, cloudApiProxyTimeoutMs, opencodeUpstreamUrl, opencodeProxyTimeoutMs, opencodeUsername, opencodePassword, opencodeEventDirectoryRewrite, serviceId, sendJson });
+ await proxyOpencodeRequest({ request, response, url, cloudApiBaseUrl, cloudApiProxyTimeoutMs, opencodeUpstreamUrl, opencodeProxyTimeoutMs, opencodeUsername, opencodePassword, opencodeDefaultProject, opencodeEventDirectoryRewrite, serviceId, sendJson });
return;
}
if (await handleCloudWebAuth({ request, response, url, cloudApiBaseUrl, cloudApiProxyTimeoutMs, serviceId, sendJson })) {
@@ -729,7 +730,7 @@ export async function proxyCloudApi({ request, response, url, cloudApiBaseUrl, c
}
}
-async function proxyOpencodeRequest({ request, response, url, cloudApiBaseUrl, cloudApiProxyTimeoutMs, opencodeUpstreamUrl, opencodeProxyTimeoutMs, opencodeUsername, opencodePassword, opencodeEventDirectoryRewrite = null, serviceId, sendJson }) {
+async function proxyOpencodeRequest({ request, response, url, cloudApiBaseUrl, cloudApiProxyTimeoutMs, opencodeUpstreamUrl, opencodeProxyTimeoutMs, opencodeUsername, opencodePassword, opencodeDefaultProject = "", opencodeEventDirectoryRewrite = null, serviceId, sendJson }) {
const traceContext = cloudWebTraceContext(request);
const startedAtMs = Date.now();
if (!cloudApiBaseUrl) {
@@ -821,15 +822,17 @@ async function proxyOpencodeRequest({ request, response, url, cloudApiBaseUrl, c
headers: opencodeUpstreamHeaders({ headers: { ...request.headers, ...cloudWebTraceHeaders(traceContext, serviceId) } }, body, authorization)
};
try {
- const proxyResult = await proxyCloudApiRequest({
- target,
- request: upstreamRequest,
- response,
- body: request.method === "GET" || request.method === "HEAD" ? "" : body,
- timeoutMs: opencodeProxyTimeoutMs,
- extraResponseHeaders,
- streamTransform
- });
+ const proxyResult = shouldInjectOpencodeDefaultProject({ request, target, opencodeDefaultProject })
+ ? await proxyOpencodeHtmlRequest({ target, request: upstreamRequest, response, timeoutMs: opencodeProxyTimeoutMs, extraResponseHeaders, defaultProject: opencodeDefaultProject })
+ : await proxyCloudApiRequest({
+ target,
+ request: upstreamRequest,
+ response,
+ body: request.method === "GET" || request.method === "HEAD" ? "" : body,
+ timeoutMs: opencodeProxyTimeoutMs,
+ extraResponseHeaders,
+ streamTransform
+ });
emitOpencodeProxySpanAsync({
traceContext,
serviceId,
@@ -890,6 +893,78 @@ function opencodeTargetUrl(url, opencodeUpstreamUrl) {
return target;
}
+function shouldInjectOpencodeDefaultProject({ request, target, opencodeDefaultProject }) {
+ if (!opencodeDefaultProject) return false;
+ if (request.method !== "GET") return false;
+ const accept = String(request.headers.accept || "");
+ if (accept && !accept.includes("text/html") && !accept.includes("*/*")) return false;
+ if (target.pathname.startsWith("/assets/")) return false;
+ if (STATIC_ASSET_EXTENSION_PATTERN.test(target.pathname)) return false;
+ return true;
+}
+
+async function proxyOpencodeHtmlRequest({ target, request, response, timeoutMs, extraResponseHeaders, defaultProject }) {
+ const controller = new AbortController();
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
+ try {
+ const upstream = await fetch(target, {
+ method: request.method,
+ headers: request.headers,
+ signal: controller.signal
+ });
+ const upstreamHeaders = Object.fromEntries(upstream.headers.entries());
+ const contentType = String(upstream.headers.get("content-type") || "");
+ const inputBody = Buffer.from(await upstream.arrayBuffer());
+ const outputBody = /^text\/html(?:\s*;|$)/iu.test(contentType)
+ ? Buffer.from(opencodeInjectDefaultProjectHtml(inputBody.toString("utf8"), defaultProject), "utf8")
+ : inputBody;
+ response.writeHead(upstream.status, {
+ ...mergeOpencodeProxyResponseHeaders(copyProxyResponseHeaders(upstreamHeaders), extraResponseHeaders),
+ "content-length": outputBody.length
+ });
+ response.end(outputBody);
+ return {
+ statusCode: upstream.status,
+ streaming: false,
+ bodyBytes: outputBody.length,
+ opencodeDefaultProjectInjected: outputBody.length !== inputBody.length
+ };
+ } catch (error) {
+ if (error?.name === "AbortError") error.timedOut = true;
+ throw error;
+ } finally {
+ clearTimeout(timeout);
+ }
+}
+
+function opencodeInjectDefaultProjectHtml(html, defaultProject) {
+ if (!html || html.includes("data-hwlab-opencode-default-project")) return html;
+ const script = opencodeDefaultProjectBootstrapScript(defaultProject);
+ if (html.includes("")) return html.replace("", `${script}`);
+ if (html.includes("
")) return html.replace("", `${script}`);
+ return `${script}${html}`;
+}
+
+function opencodeDefaultProjectBootstrapScript(defaultProject) {
+ return ``;
+}
+
+function mergeOpencodeProxyResponseHeaders(baseHeaders = {}, extraHeaders = {}) {
+ const result = { ...baseHeaders };
+ for (const [key, value] of Object.entries(extraHeaders)) {
+ if (value === undefined || value === null || value === "") continue;
+ const existingKey = Object.keys(result).find((item) => item.toLowerCase() === key.toLowerCase());
+ if (existingKey && existingKey.toLowerCase() === "set-cookie") {
+ const existing = Array.isArray(result[existingKey]) ? result[existingKey] : [result[existingKey]];
+ const next = Array.isArray(value) ? value : [value];
+ result[existingKey] = [...existing.filter(Boolean), ...next.filter(Boolean)];
+ continue;
+ }
+ result[existingKey || key] = value;
+ }
+ return result;
+}
+
function issueOpencodeTicket(cookieHeader) {
pruneOpencodeTickets();
const ticket = randomBytes(24).toString("base64url");
diff --git a/internal/dev-entrypoint/cloud-web-runtime.test.mjs b/internal/dev-entrypoint/cloud-web-runtime.test.mjs
index efc26e1f..5d8cc549 100644
--- a/internal/dev-entrypoint/cloud-web-runtime.test.mjs
+++ b/internal/dev-entrypoint/cloud-web-runtime.test.mjs
@@ -648,6 +648,76 @@ test("cloud web OpenCode frame-url endpoint mints traced tickets", async () => {
}
});
+test("cloud web OpenCode proxy bootstraps the default project for iframe HTML", async () => {
+ const restoreEnv = withEnv({
+ HWLAB_CLOUD_WEB_OPENCODE_URL: "https://opencode.example.test"
+ });
+ const cloudApi = createServer((request, response) => {
+ request.resume();
+ response.writeHead(200, { "content-type": "application/json" });
+ response.end(JSON.stringify({ authenticated: request.headers.cookie === "hwlab_session=session-a" }));
+ });
+ const opencodeRequests = [];
+ const opencode = createServer((request, response) => {
+ opencodeRequests.push({
+ url: request.url,
+ authorization: request.headers.authorization,
+ cookie: request.headers.cookie
+ });
+ request.resume();
+ response.writeHead(200, { "content-type": "text/html; charset=utf-8" });
+ response.end("