UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

1,209 lines 50.4 kB
import { I as resolveTimestampMsToIsoString, o as asDateTimestampMs } from "./number-coercion-CLj0HTDM.js";
import { t as resolveOpenClawPackageRoot } from "./openclaw-root-CfYY-fyD.js";
import { o as normalizeLowercaseStringOrEmpty } from "./string-coerce-CIXf7egm.js";
import { c as resolveUserPath } from "./home-dir-BPhrG-aM.js";
import { i as getOrCreatePromise } from "./lazy-promise-DGqyc4Y4.js";
import { r as createLazyRuntimeModule } from "./lazy-runtime-CgCh8H_K.js";
import { c as readFileDescriptorBounded, i as matchRootFileOpenFailure, s as openRootFileSync } from "./boundary-file-read-uaJcf6X6.js";
import { a as isWithinDir } from "./path-safety-Bi0ppMWC.js";
import { t as FsSafeError, u as openLocalFileSafely } from "./fs-safe-B6pvPGnf.js";
import "./utils-P__uGsPB.js";
import { t as pruneMapToMaxSize } from "./map-size-CNcWiFKu.js";
import "./session-key-BnWWjqNc.js";
import { t as isIncognitoSessionKey } from "./incognito-session-key-BwpD1Lwd.js";
import { t as readFileWindowFully } from "./file-read-DtMn74uz.js";
import { l as resolveRuntimeServiceVersion, s as resolveRuntimeServiceBuildId } from "./version-v1kuAkGj.js";
import { a as CONTROL_UI_BASE_PATH_ATTRIBUTE, c as CONTROL_UI_ENVIRONMENT_ATTRIBUTE, o as CONTROL_UI_BOOTSTRAP_CONFIG_PATH, u as CONTROL_UI_TERMINAL_ENABLED_ATTRIBUTE } from "./zod-schema-DnirJMRA.js";
import { c as resolveAvatarMime, l as AVATAR_MAX_BYTES } from "./avatar-policy-D8FQ_Wte.js";
import { r as runCommandWithTimeout } from "./exec-BIE-3oLG.js";
import { t as safeEqualSecret } from "./secret-equal-DRsL8lKD.js";
import { t as normalizeControlUiBasePath } from "./control-ui-shared-BiO6QP54.js";
import { n as authorizeOperatorScopesForMethod } from "./method-scopes-K6J_UQGL.js";
import "./users-CPWrgxrZ.js";
import { l as kindFromMime, n as detectMime } from "./mime-CVpcq9ju.js";
import { o as extractOriginalFilename } from "./store-Ph57PZ9P.js";
import { a as getUserProfileListItem } from "./user-profiles-4AB7AmiH.js";
import { a as safeFileURLToPath } from "./local-file-access-B1HKoQAc.js";
import { c as resolveMediaReferenceLocalPathInfo } from "./media-reference-ZG0ZXjCX.js";
import { i as toMediaProbeResult, n as probePlaybackMediaFileDescriptor } from "./media-probe-Dbs998-L.js";
import { n as resolveSessionPermissionCoreToolPolicy } from "./session-permission-exec-mode-CJHB47SR.js";
import { n as resolveEffectiveToolFsWorkspaceOnly } from "./tool-fs-policy-BciYLPcO.js";
import { n as getAgentScopedMediaLocalRoots } from "./local-roots-BphuGw-a.js";
import { n as LocalMediaAccessError, r as assertLocalMediaAllowed } from "./local-media-access-DXGJo-Ud.js";
import { a as CONTROL_UI_BUILD_ID_ATTRIBUTE, c as isControlUiRootPublicAsset, l as isControlUiVersionedPublicAsset, n as isControlUiApprovalDocumentPath, o as CONTROL_UI_ROOT_PUBLIC_ASSETS, r as isControlUiFocusDocumentPath, s as buildControlUiRootAssetPath, t as classifyControlUiRequest } from "./control-ui-routing-CjtUNaXH.js";
import { a as parseControlUiResourcePath, s as resolveAssistantMediaRoutePath } from "./control-ui-contract-zYW4RcpK.js";
import { n as gatewayAvatarImageRevision } from "./assistant-avatar-cache-4x6DyL8R.js";
import { i as DEFAULT_ASSISTANT_IDENTITY, n as openGatewayAssistantAvatar, o as resolveAssistantIdentity, r as resolveGatewayAssistantAvatar, t as gatewayAssistantAvatarUrl } from "./assistant-avatar-B0tiesfb.js";
import { n as resolveRequestedSessionAgentId } from "./session-request-agent-CCRSEGCB.js";
import { i as loadGatewaySessionEntryReadOnly } from "./session-utils-store-CInT2loy.js";
import "./session-utils-Cai0_C6U.js";
import { n as resolvePublicAgentAvatarSource } from "./identity-avatar-BaF1nma_.js";
import { b as resolveHttpProfile, r as authorizeControlUiReadRequestOrReply, t as applyHttpOperatorRoleScopeCeiling } from "./http-auth-utils-C3lb4QXY.js";
import { _ as isReadHttpMethod, v as respondNotFound, y as respondPlainText } from "./http-common-BaZaosnr.js";
import { n as createProfileSessionEntryFilter } from "./session-sharing-B7MI8hNo.js";
import "./http-utils-BHgXp7Zb.js";
import { c as startsWithSvgRootElement, r as applyHttpImageContentSecurityPolicy, s as sendHttpImageResponse } from "./http-image-response-DUkYrvGx.js";
import { a as buildAssistantMediaContentDisposition, c as resolvePlaybackTranscode, i as writeByteHeaders, o as replacePlaybackFileExtension, r as resolveByteResponse, s as resolvePlaybackModeForSource, t as createGatewayByteStream } from "./http-byte-range-Clvh5mwC.js";
import { t as resolveSessionWorkspaceRoots } from "./session-workspace-roots-BFJBfb_2.js";
import { t as isTerminalConfigEnabled } from "./enabled-BSjeiWpO.js";
import fs from "node:fs";
import path from "node:path";
import fs$1 from "node:fs/promises";
import { createHash, createHmac, randomBytes } from "node:crypto";
import { brotliCompress, constants as constants$1, gzip } from "node:zlib";
//#region src/infra/dev-install-branch.ts
const GIT_TIMEOUT_MS = 3e3;
const HIDDEN_BRANCHES = /* @__PURE__ */ new Set([
	"main",
	"master",
	"HEAD"
]);
async function detectDevInstallGitBranch(params) {
	const run = params.runCommand ?? runCommandWithTimeout;
	const root = params.root ? path.resolve(params.root) : null;
	if (!root) return null;
	const topRes = await run([
		"git",
		"-C",
		root,
		"rev-parse",
		"--show-toplevel"
	], { timeoutMs: GIT_TIMEOUT_MS }).catch(() => null);
	if (!topRes || topRes.code !== 0) return null;
	const rootReal = await fs$1.realpath(root).catch(() => root);
	const top = topRes.stdout.trim();
	if (!top || path.resolve(top) !== path.resolve(rootReal)) return null;
	const branchRes = await run([
		"git",
		"-C",
		root,
		"rev-parse",
		"--abbrev-ref",
		"HEAD"
	], { timeoutMs: GIT_TIMEOUT_MS }).catch(() => null);
	if (!branchRes || branchRes.code !== 0) return null;
	const branch = branchRes.stdout.trim();
	return branch && !HIDDEN_BRANCHES.has(branch) ? branch : null;
}
let cached = null;
function resolveDevInstallGitBranch() {
	cached ??= resolveOpenClawPackageRoot({
		argv1: process.argv[1],
		cwd: process.cwd(),
		moduleUrl: import.meta.url
	}).then((root) => detectDevInstallGitBranch({ root })).catch(() => null);
	return cached;
}
//#endregion
//#region src/gateway/assistant-media-policy.ts
function resolveAssistantMediaReaderAuth(reader, config) {
	try {
		const profile = reader.profileId ? getUserProfileListItem(reader.profileId) : void 0;
		const currentProfile = profile ? resolveHttpProfile(profile.id, profile.updatedAt, config) : void 0;
		const operatorScopes = applyHttpOperatorRoleScopeCeiling(reader.operatorScopes, currentProfile);
		if (!authorizeOperatorScopesForMethod("assistant.media.get", operatorScopes).allowed) return;
		return {
			authMethod: reader.authMethod,
			operatorScopes,
			...currentProfile
		};
	} catch {
		return;
	}
}
function resolveAssistantMediaPolicy(params) {
	let loaded;
	if (params.sessionKey) {
		const owner = resolveRequestedSessionAgentId(params.config, params.sessionKey, params.agentId);
		if (!owner.ok) return;
		loaded = loadGatewaySessionEntryReadOnly(params.sessionKey, { agentId: owner.agentId });
		if (!loaded.entry?.sessionId) return;
	}
	const config = loaded?.cfg ?? params.config;
	const reader = params.reader ?? (params.requestAuth ? {
		authMethod: params.requestAuth.authMethod,
		operatorScopes: params.requestAuth.operatorScopes,
		...params.requestAuth.authenticatedUserProfile ? { profileId: params.requestAuth.authenticatedUserProfile.profileId } : {}
	} : void 0);
	const auth = params.requestAuth ?? (reader ? resolveAssistantMediaReaderAuth(reader, config) : void 0);
	if (!auth || !reader) return;
	const agentId = loaded?.agentId ?? params.agentId;
	const entry = loaded?.entry;
	let session;
	let sessionRoot;
	if (loaded && entry && agentId) {
		if (!auth.operatorScopes.includes("operator.admin")) {
			const profileId = auth.authenticatedUserProfile?.profileId;
			if (profileId && profileId !== "gateway-owner") {
				if (entry.incognito || isIncognitoSessionKey(loaded.canonicalKey)) return;
				if (auth.operatorRolePolicy && !createProfileSessionEntryFilter({
					profileId,
					sessionCap: auth.operatorRolePolicy.sessions.others
				})(loaded.canonicalKey, entry)) return;
			} else if (!profileId && config.gateway?.roles) return;
		}
		session = {
			sessionKey: loaded.canonicalKey,
			agentId,
			sessionId: entry.sessionId
		};
		sessionRoot = entry.sessionRoot ?? resolveSessionWorkspaceRoots(config, agentId, entry).root;
	}
	const workspaceOnly = !session || (entry?.permissionMode ? resolveSessionPermissionCoreToolPolicy({ mode: entry.permissionMode }).workspaceOnly : resolveEffectiveToolFsWorkspaceOnly({
		cfg: config,
		agentId
	}));
	const localRoots = [...getAgentScopedMediaLocalRoots(config, agentId, workspaceOnly ? sessionRoot : void 0)];
	if (sessionRoot && !localRoots.includes(sessionRoot)) localRoots.push(sessionRoot);
	return {
		session,
		remote: Boolean(entry?.execNode),
		localRoots,
		workspaceOnly,
		reader,
		canAllow: auth.operatorScopes.includes("operator.admin")
	};
}
//#endregion
//#region src/gateway/control-ui-csp.ts
const SCRIPT_ATTRIBUTE_NAME_RE = /\s([^\s=/>]+)(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+))?/g;
/**
* Compute SHA-256 CSP hashes for inline `<script>` blocks in an HTML string.
* Only scripts without a `src` attribute are considered inline.
*/
function computeInlineScriptHashes(html) {
	const hashes = [];
	const re = /<script(?:\s[^>]*)?>([^]*?)<\/script>/gi;
	let match;
	while ((match = re.exec(html)) !== null) {
		if (hasScriptSrcAttribute(match[0].slice(0, match[0].indexOf(">") + 1))) continue;
		const content = match[1];
		if (!content) continue;
		const hash = createHash("sha256").update(content, "utf8").digest("base64");
		hashes.push(`sha256-${hash}`);
	}
	return hashes;
}
function hasScriptSrcAttribute(openTag) {
	return Array.from(openTag.matchAll(SCRIPT_ATTRIBUTE_NAME_RE)).some((match) => normalizeLowercaseStringOrEmpty(match[1]) === "src");
}
/** Build the CSP header applied to Gateway-served Control UI HTML. */
function buildControlUiCspHeader(opts) {
	const hashes = opts?.inlineScriptHashes;
	const scriptTokens = ["'self'"];
	if (hashes?.length) scriptTokens.push(...hashes.map((h) => `'${h}'`));
	if (opts?.allowWasm) scriptTokens.push("'wasm-unsafe-eval'");
	const connectTokens = [
		"'self'",
		"ws:",
		"wss:",
		"data:",
		"https://api.openai.com",
		"https://tweakcn.com"
	];
	if (opts?.portalHost) try {
		const parsed = new URL(`http://${opts.portalHost}`);
		if (!parsed.username && !parsed.password && parsed.pathname === "/" && !parsed.search && !parsed.hash && parsed.hostname) connectTokens.push(`http://${parsed.hostname}:*`, `https://${parsed.hostname}:*`);
	} catch {}
	return [
		"default-src 'self'",
		"base-uri 'none'",
		"object-src 'none'",
		"frame-ancestors 'none'",
		"frame-src 'self' http: https:",
		`script-src ${scriptTokens.join(" ")}`,
		"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
		"img-src 'self' data: blob: https://gravatar.com https://avatars.githubusercontent.com",
		"media-src 'self' data: blob:",
		"font-src 'self' https://fonts.gstatic.com",
		"worker-src 'self'",
		`connect-src ${connectTokens.join(" ")}`
	].join("; ");
}
//#endregion
//#region src/gateway/control-ui-static.ts
const CONTROL_UI_IMMUTABLE_CACHE_CONTROL = "public, max-age=31536000, immutable";
const CONTROL_UI_HTML_COMPRESSION_CACHE_MAX_ENTRIES = 4;
const CONTROL_UI_COMPRESSIBLE_EXTENSIONS = /* @__PURE__ */ new Set([
	".css",
	".html",
	".js",
	".json",
	".svg",
	".txt",
	".wasm",
	".webmanifest"
]);
const CONTROL_UI_PRECOMPRESSED_ASSET_EXTENSIONS = /* @__PURE__ */ new Set([".br", ".gz"]);
const CONTROL_UI_CONTENT_TYPES = {
	".html": "text/html; charset=utf-8",
	".js": "application/javascript; charset=utf-8",
	".css": "text/css; charset=utf-8",
	".json": "application/json; charset=utf-8",
	".map": "application/json; charset=utf-8",
	".svg": "image/svg+xml",
	".png": "image/png",
	".jpg": "image/jpeg",
	".jpeg": "image/jpeg",
	".gif": "image/gif",
	".webp": "image/webp",
	".ico": "image/x-icon",
	".txt": "text/plain; charset=utf-8",
	".wasm": "application/wasm",
	".webmanifest": "application/manifest+json; charset=utf-8",
	".woff2": "font/woff2"
};
function isControlUiStaticAssetExtension(extension) {
	return extension !== ".html" && Object.hasOwn(CONTROL_UI_CONTENT_TYPES, extension);
}
function isControlUiPrecompressedAssetExtension(extension) {
	return CONTROL_UI_PRECOMPRESSED_ASSET_EXTENSIONS.has(extension);
}
const CONTROL_UI_QVALUE_PATTERN = /^(?:0(?:\.\d{0,3})?|1(?:\.0{0,3})?)$/;
const controlUiHtmlCompressionCache = /* @__PURE__ */ new Map();
function normalizedAcceptEncoding(req) {
	const value = req.headers?.["accept-encoding"];
	return Array.isArray(value) ? value.join(",") : value ?? "";
}
function resolveControlUiContentEncodings(req, includeCompressed) {
	const acceptEncoding = normalizedAcceptEncoding(req);
	if (!acceptEncoding.trim()) return ["identity"];
	const qualities = /* @__PURE__ */ new Map();
	for (const entry of acceptEncoding.split(",")) {
		const [rawName, ...rawParams] = entry.split(";");
		const name = rawName?.trim().toLowerCase();
		if (!name) continue;
		const qualityText = rawParams.find((param) => param.trim().toLowerCase().startsWith("q="))?.trim().slice(2);
		const parsedQuality = qualityText === void 0 ? 1 : CONTROL_UI_QVALUE_PATTERN.test(qualityText) ? Number(qualityText) : NaN;
		const quality = Number.isFinite(parsedQuality) && parsedQuality >= 0 && parsedQuality <= 1 ? parsedQuality : 0;
		qualities.set(name, Math.max(qualities.get(name) ?? 0, quality));
	}
	const wildcardQuality = qualities.get("*");
	const identityQuality = qualities.get("identity") ?? (wildcardQuality === 0 ? 0 : 1);
	const qualityFor = (name) => name === "identity" ? identityQuality : qualities.get(name) ?? wildcardQuality ?? 0;
	return (includeCompressed ? [
		"br",
		"gzip",
		"identity"
	] : ["identity"]).filter((encoding) => qualityFor(encoding) > 0).toSorted((left, right) => qualityFor(right) - qualityFor(left));
}
function resolveControlUiHtmlEncoding(req) {
	return resolveControlUiContentEncodings(req, true)[0] ?? "not-acceptable";
}
function resolveOpenedControlUiRepresentation(params) {
	const { req, sourceFile, precompressed, openPrecompressedFile } = params;
	const extension = path.extname(params.contentPath).toLowerCase();
	const encodings = resolveControlUiContentEncodings(req, precompressed && CONTROL_UI_COMPRESSIBLE_EXTENSIONS.has(extension));
	for (const selected of encodings) {
		if (selected === "identity") return { bodyFile: sourceFile };
		const suffix = selected === "br" ? ".br" : ".gz";
		let compressedFile;
		try {
			compressedFile = openPrecompressedFile(`${sourceFile.path}${suffix}`);
		} catch (error) {
			fs.closeSync(sourceFile.fd);
			throw error;
		}
		if (compressedFile) {
			fs.closeSync(sourceFile.fd);
			return {
				bodyFile: compressedFile,
				encoding: selected
			};
		}
	}
	fs.closeSync(sourceFile.fd);
	return null;
}
function setControlUiEncodingHeaders(res, extension, encoding) {
	res.setHeader("Vary", "Accept-Encoding");
	if (!CONTROL_UI_COMPRESSIBLE_EXTENSIONS.has(extension)) return;
	if (encoding !== "identity") res.setHeader("Content-Encoding", encoding);
}
function setControlUiFileHeaders(res, filePath, options) {
	const extension = path.extname(filePath).toLowerCase();
	res.setHeader("Content-Type", CONTROL_UI_CONTENT_TYPES[extension] ?? "application/octet-stream");
	res.setHeader("Cache-Control", options?.immutable ? CONTROL_UI_IMMUTABLE_CACHE_CONTROL : "no-cache");
	if (options?.lastModifiedMs !== void 0) res.setHeader("Last-Modified", new Date(options.lastModifiedMs).toUTCString());
	setControlUiEncodingHeaders(res, extension, options?.encoding ?? "identity");
}
/** Revalidate no-cache static assets without generating entity tags. */
function isControlUiFileUnmodified(req, lastModifiedMs) {
	if (req.method !== "GET" && req.method !== "HEAD") return false;
	const ifNoneMatch = req.headers?.["if-none-match"];
	if (ifNoneMatch !== void 0) return ifNoneMatch.trim() === "*";
	const header = req.headers?.["if-modified-since"];
	const since = typeof header === "string" ? Date.parse(header) : NaN;
	return Number.isFinite(since) && Math.floor(lastModifiedMs / 1e3) * 1e3 <= since;
}
function respondControlUiNotModified(res, options) {
	res.statusCode = 304;
	res.setHeader("Cache-Control", options.immutable ? CONTROL_UI_IMMUTABLE_CACHE_CONTROL : "no-cache");
	res.setHeader("Last-Modified", new Date(options.lastModifiedMs).toUTCString());
	res.setHeader("Vary", "Accept-Encoding");
	res.end();
}
function respondHeadForControlUiFile(res, filePath, options) {
	res.statusCode = 200;
	setControlUiFileHeaders(res, filePath, options);
	if (options?.contentLength !== void 0) res.setHeader("Content-Length", String(options.contentLength));
	res.end();
}
function compressControlUiBody(body, encoding) {
	return new Promise((resolve, reject) => {
		const callback = (error, compressed) => {
			if (error) {
				reject(error);
				return;
			}
			resolve(compressed);
		};
		if (encoding === "br") {
			brotliCompress(body, { params: { [constants$1.BROTLI_PARAM_QUALITY]: 4 } }, callback);
			return;
		}
		gzip(body, { level: 6 }, callback);
	});
}
async function serveControlUiAsset(res, filePath, body, options) {
	setControlUiFileHeaders(res, filePath, options);
	res.end(body);
}
function cachedCompressedControlUiHtml(body, encoding) {
	const key = `${encoding}\0${body}`;
	const cached = controlUiHtmlCompressionCache.get(key);
	if (cached) {
		controlUiHtmlCompressionCache.delete(key);
		controlUiHtmlCompressionCache.set(key, cached);
		return cached;
	}
	const compression = getOrCreatePromise(controlUiHtmlCompressionCache, key, () => compressControlUiBody(Buffer.from(body), encoding), { cacheRejections: false });
	pruneMapToMaxSize(controlUiHtmlCompressionCache, CONTROL_UI_HTML_COMPRESSION_CACHE_MAX_ENTRIES);
	return compression;
}
function respondControlUiNotAcceptable(res) {
	res.setHeader("Cache-Control", "no-store");
	res.setHeader("Vary", "Accept-Encoding");
	respondPlainText(res, 406, "Not Acceptable");
}
async function sendControlUiHtmlBody(req, res, body) {
	const encoding = resolveControlUiHtmlEncoding(req);
	if (encoding === "not-acceptable") {
		respondControlUiNotAcceptable(res);
		return;
	}
	setControlUiEncodingHeaders(res, ".html", encoding);
	res.end(encoding === "identity" ? body : await cachedCompressedControlUiHtml(body, encoding));
}
async function readAndCloseControlUiFile(file) {
	try {
		if (file.size > 2 ** 31 - 1) throw Object.assign(/* @__PURE__ */ new RangeError("Control UI file exceeds the 2 GiB read limit"), { code: "ERR_FS_FILE_TOO_LARGE" });
		const buffer = Buffer.allocUnsafe(file.size);
		let offset = 0;
		while (offset < buffer.length) {
			const length = Math.min(524288, buffer.length - offset);
			const bytesRead = await new Promise((resolve, reject) => {
				fs.read(file.fd, buffer, offset, length, null, (error, count) => {
					if (error) reject(error);
					else resolve(count);
				});
			});
			if (bytesRead === 0) break;
			offset += bytesRead;
		}
		return buffer.subarray(0, offset);
	} finally {
		fs.closeSync(file.fd);
	}
}
//#endregion
//#region src/gateway/control-ui.ts
const ROOT_PREFIX = "/";
const CONTROL_UI_ASSISTANT_MEDIA_TICKET_SCOPE = "assistant-media";
const CONTROL_UI_ASSISTANT_MEDIA_TICKET_TTL_MS = 3e5;
const CONTROL_UI_ASSETS_MISSING_MESSAGE = "Control UI assets not found. Build them with `pnpm ui:build` (auto-installs UI deps), or run `pnpm ui:dev` during development.";
const controlUiAssistantMediaTicketSecret = randomBytes(32);
const loadAvatarThumbnail = createLazyRuntimeModule(() => import("./assistant-avatar-thumbnail.runtime.js"));
const CONTROL_UI_NAMESPACE_PREFIX = "/__openclaw__/";
/** Anchors bundled assets before deep-linked documents begin preloading. */
function rewriteControlUiIndexHtmlAssetHrefs(html, basePath, buildId) {
	const normalized = normalizeControlUiBasePath(basePath);
	let next = html.replaceAll("src=\"./assets/", `src="${normalized}/assets/`).replaceAll("href=\"./assets/", `href="${normalized}/assets/`);
	for (const asset of CONTROL_UI_ROOT_PUBLIC_ASSETS) {
		const version = buildId && isControlUiVersionedPublicAsset(asset) ? `?v=${encodeURIComponent(buildId)}` : "";
		const assetHref = `href="${buildControlUiRootAssetPath(normalized, asset)}${version}"`;
		next = next.replaceAll(`href="./${asset}"`, assetHref);
		next = next.replaceAll(`href="/${asset}"`, assetHref);
		next = next.replaceAll(`href="${buildControlUiRootAssetPath(normalized, asset)}"`, assetHref);
	}
	return next;
}
function escapeHtmlAttribute(value) {
	return value.replaceAll("&", "&amp;").replaceAll("\"", "&quot;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("'", "&#39;");
}
function controlUiAvatarResolutionMeta(resolved) {
	if (!resolved) return {
		avatarSource: null,
		avatarStatus: null,
		avatarReason: null
	};
	return {
		avatarSource: resolvePublicAgentAvatarSource(resolved) ?? null,
		avatarStatus: resolved.kind,
		avatarReason: resolved.kind === "none" ? resolved.reason : null
	};
}
function applyControlUiSecurityHeaders(res) {
	res.setHeader("X-Frame-Options", "DENY");
	res.setHeader("Content-Security-Policy", buildControlUiCspHeader());
	res.setHeader("X-Content-Type-Options", "nosniff");
	res.setHeader("Referrer-Policy", "no-referrer");
	res.setHeader("Permissions-Policy", "camera=(self), microphone=*, geolocation=*, clipboard-write=*");
}
function sendJson(res, status, body) {
	res.statusCode = status;
	res.setHeader("Content-Type", "application/json; charset=utf-8");
	res.setHeader("Cache-Control", "no-cache");
	res.end(JSON.stringify(body));
}
function respondControlUiAssetsUnavailable(res, options) {
	const message = options?.preparing ? "Control UI assets are being prepared. Try again shortly." : options?.failed ? "Control UI assets could not be prepared. Check the Gateway logs or run `openclaw doctor --fix`." : options?.configuredRootPath ? `Control UI assets not found at ${options.configuredRootPath}. Build them with \`pnpm ui:build\` (auto-installs UI deps), or update gateway.controlUi.root.` : CONTROL_UI_ASSETS_MISSING_MESSAGE;
	if (options?.preparing) {
		res.setHeader("Cache-Control", "no-store");
		res.setHeader("Retry-After", "1");
	}
	respondPlainText(res, 503, message);
}
function isValidAgentPathSegment(agentId) {
	return /^[a-z0-9][a-z0-9_-]{0,63}$/i.test(agentId);
}
function normalizeAssistantMediaSource(source) {
	const trimmed = source.trim();
	if (!trimmed) return null;
	if (/^file:/iu.test(trimmed)) try {
		return safeFileURLToPath(trimmed);
	} catch {
		return null;
	}
	if (trimmed.startsWith("~")) return resolveUserPath(trimmed);
	return trimmed;
}
function signAssistantMediaTicketPayload(encodedPayload) {
	return createHmac("sha256", controlUiAssistantMediaTicketSecret).update(encodedPayload).digest("base64url");
}
function createAssistantMediaTicket(payloadFields, nowMs = Date.now()) {
	const now = asDateTimestampMs(nowMs);
	if (now === void 0) return {};
	const exp = asDateTimestampMs(now + CONTROL_UI_ASSISTANT_MEDIA_TICKET_TTL_MS);
	if (exp === void 0) return {};
	const payload = {
		scope: CONTROL_UI_ASSISTANT_MEDIA_TICKET_SCOPE,
		...payloadFields,
		exp
	};
	const encodedPayload = Buffer.from(JSON.stringify(payload), "utf8").toString("base64url");
	return {
		mediaTicket: `v1.${encodedPayload}.${signAssistantMediaTicketPayload(encodedPayload)}`,
		mediaTicketExpiresAt: resolveTimestampMsToIsoString(exp)
	};
}
function verifyAssistantMediaTicket(ticket, source, agentId, nowMs = Date.now()) {
	const now = asDateTimestampMs(nowMs);
	if (now === void 0) return;
	const parts = ticket?.split(".");
	if (!parts || parts.length !== 3 || parts[0] !== "v1") return;
	const [, encodedPayload, sig] = parts;
	if (!encodedPayload || !sig) return;
	const expectedSig = signAssistantMediaTicketPayload(encodedPayload);
	if (!safeEqualSecret(sig, expectedSig)) return;
	try {
		const payload = JSON.parse(Buffer.from(encodedPayload, "base64url").toString("utf8"));
		return payload.scope === CONTROL_UI_ASSISTANT_MEDIA_TICKET_SCOPE && payload.source === source && payload.agentId === agentId && typeof payload.reader?.authMethod === "string" && Array.isArray(payload.reader.operatorScopes) && (payload.file === void 0 || typeof payload.file?.realPath === "string" && typeof payload.file.dev === "string" && typeof payload.file.ino === "string") && typeof payload.exp === "number" && Number.isFinite(payload.exp) && payload.exp >= now ? payload : void 0;
	} catch {
		return;
	}
}
function classifyAssistantMediaError(err) {
	if (err instanceof FsSafeError) switch (err.code) {
		case "not-found": return {
			available: false,
			code: "file-not-found",
			reason: "File not found"
		};
		case "not-file": return {
			available: false,
			code: "not-a-file",
			reason: "Not a file"
		};
		case "invalid-path":
		case "path-mismatch":
		case "symlink": return {
			available: false,
			code: "invalid-file",
			reason: "Invalid file"
		};
		default: return {
			available: false,
			code: "attachment-unavailable",
			reason: "Attachment unavailable"
		};
	}
	if (err instanceof Error && "code" in err) {
		const errorCode = err.code;
		switch (typeof errorCode === "string" ? errorCode : "") {
			case "unsupported-media-type": return {
				available: false,
				code: "unsupported-media-type",
				reason: "Not an image"
			};
			case "path-not-allowed": return {
				available: false,
				code: "outside-allowed-folders",
				reason: "Outside allowed folders"
			};
			case "invalid-file-url":
			case "invalid-path":
			case "unsafe-bypass":
			case "network-path-not-allowed":
			case "invalid-root": return {
				available: false,
				code: "blocked-local-file",
				reason: "Blocked local file"
			};
			case "not-found": return {
				available: false,
				code: "file-not-found",
				reason: "File not found"
			};
			case "not-file": return {
				available: false,
				code: "not-a-file",
				reason: "Not a file"
			};
		}
	}
	return {
		available: false,
		code: "attachment-unavailable",
		reason: "Attachment unavailable"
	};
}
function sameAssistantMediaFile(actual, expected) {
	return actual.realPath === expected.realPath && actual.dev === expected.dev && actual.ino === expected.ino;
}
async function openAssistantMedia(source, policy, allowance) {
	const reference = await resolveMediaReferenceLocalPathInfo(source);
	if (policy.remote && reference.kind === "local") throw new LocalMediaAccessError("invalid-path", "File is on another computer");
	let outsideRoots = false;
	try {
		await assertLocalMediaAllowed(reference.path, policy.localRoots);
	} catch (error) {
		if (!(error instanceof LocalMediaAccessError) || error.code !== "path-not-allowed") throw error;
		outsideRoots = true;
		if (policy.workspaceOnly && !allowance) throw error;
	}
	const opened = await openLocalFileSafely({ filePath: reference.path });
	try {
		let file;
		if (outsideRoots && allowance) {
			const identity = await opened.handle.stat({ bigint: true });
			const candidate = {
				realPath: opened.realPath,
				dev: identity.dev.toString(),
				ino: identity.ino.toString()
			};
			if (allowance === true || sameAssistantMediaFile(candidate, allowance)) file = candidate;
			else if (policy.workspaceOnly) throw new LocalMediaAccessError("path-not-allowed", "Outside allowed folders");
		}
		if (!outsideRoots) await assertLocalMediaAllowed(opened.realPath, policy.localRoots);
		const sniffBuffer = Buffer.alloc(Math.min(opened.stat.size, 8192));
		const bytesRead = sniffBuffer.length ? await readFileWindowFully(opened.handle, sniffBuffer, 0) : 0;
		const buffer = sniffBuffer.subarray(0, bytesRead);
		const mimeType = startsWithSvgRootElement(buffer.toString("utf8")) ? "image/svg+xml" : await detectMime({
			buffer,
			...outsideRoots ? {} : { filePath: reference.path }
		});
		if (outsideRoots && kindFromMime(mimeType) !== "image") throw new LocalMediaAccessError("unsupported-media-type", "Not an image");
		return {
			opened,
			reference,
			mimeType,
			outsideRoots,
			file
		};
	} catch (error) {
		await opened.handle.close().catch(() => {});
		throw error;
	}
}
async function resolveAssistantMediaAvailability(source, policy, allowance, agentId) {
	try {
		const { opened, mimeType, file } = await openAssistantMedia(source, policy, allowance);
		try {
			const mediaKind = kindFromMime(mimeType);
			const playbackProbe = mediaKind === "audio" || mediaKind === "video" ? await probePlaybackMediaFileDescriptor(opened.handle.fd, mediaKind) : null;
			const playback = mimeType && (mediaKind === "audio" || mediaKind === "video") ? await resolvePlaybackModeForSource({
				sourcePath: opened.realPath,
				sourceStat: opened.stat,
				mimeType,
				kind: mediaKind,
				probe: playbackProbe
			}) : void 0;
			return {
				available: true,
				...mimeType ? { mimeType } : {},
				...playback ? { playback } : {},
				sizeBytes: opened.stat.size,
				...toMediaProbeResult(playbackProbe),
				...createAssistantMediaTicket({
					source,
					agentId,
					session: policy.session,
					reader: policy.reader,
					...file ? { file } : {}
				})
			};
		} finally {
			await opened.handle.close().catch(() => {});
		}
	} catch (error) {
		return classifyAssistantMediaError(error);
	}
}
async function handleControlUiAssistantMediaRequest(req, res, opts) {
	const urlRaw = req.url;
	if (!urlRaw) return false;
	const url = new URL(urlRaw, "http://localhost");
	if (url.pathname !== resolveAssistantMediaRoutePath(opts?.basePath)) return false;
	const isMetaRequest = url.searchParams.get("meta") === "1";
	const explicitAllow = req.method === "POST" && isMetaRequest && url.searchParams.get("allow") === "1";
	if (!isReadHttpMethod(req.method) && !explicitAllow) return false;
	applyControlUiSecurityHeaders(res);
	const source = normalizeAssistantMediaSource(url.searchParams.get("source") ?? "");
	if (!source) {
		respondNotFound(res);
		return true;
	}
	const sessionKey = url.searchParams.get("sessionKey")?.trim() || void 0;
	const agentId = sessionKey ? url.searchParams.get("agentId")?.trim() || void 0 : opts?.agentId;
	const ticket = verifyAssistantMediaTicket(url.searchParams.get("mediaTicket"), source, agentId);
	const requestAuth = isMetaRequest || !ticket ? await authorizeControlUiReadRequestOrReply({
		req,
		res,
		auth: opts?.auth,
		trustedProxies: opts?.trustedProxies,
		allowRealIpFallback: opts?.allowRealIpFallback,
		rateLimiter: opts?.rateLimiter,
		allowQueryToken: !explicitAllow
	}) : void 0;
	if ((isMetaRequest || !ticket) && !requestAuth) return true;
	const policyParams = {
		config: opts?.config ?? {},
		sessionKey,
		agentId
	};
	const policy = resolveAssistantMediaPolicy({
		...policyParams,
		requestAuth: requestAuth ?? void 0,
		reader: isMetaRequest ? void 0 : ticket?.reader
	});
	if (!policy) {
		respondNotFound(res);
		return true;
	}
	if (explicitAllow && !policy.canAllow) {
		sendJson(res, 403, { error: "Allowing an outside image requires operator.admin" });
		return true;
	}
	const sameSession = ticket && ticket.session?.sessionKey === policy.session?.sessionKey && ticket.session?.agentId === policy.session?.agentId && ticket.session?.sessionId === policy.session?.sessionId;
	if (!isMetaRequest && url.searchParams.has("mediaTicket") && (!ticket || !sameSession)) {
		respondNotFound(res);
		return true;
	}
	const allowance = explicitAllow ? true : ticket?.file && sameSession && policy.canAllow ? ticket.file : void 0;
	const assertCurrentPolicy = () => {
		const current = resolveAssistantMediaPolicy({
			...policyParams,
			reader: policy.reader
		});
		if (!current || current.session?.sessionKey !== policy.session?.sessionKey || current.session?.agentId !== policy.session?.agentId || current.session?.sessionId !== policy.session?.sessionId || current.remote !== policy.remote || current.workspaceOnly !== policy.workspaceOnly || current.localRoots.length !== policy.localRoots.length || current.localRoots.some((root, index) => root !== policy.localRoots[index]) || allowance && policy.workspaceOnly && !current.canAllow) throw new FsSafeError("path-mismatch", "Media access changed");
		return current;
	};
	if (isMetaRequest) {
		const availability = await resolveAssistantMediaAvailability(source, policy, allowance, agentId);
		let current;
		try {
			current = assertCurrentPolicy();
		} catch {
			respondNotFound(res);
			return true;
		}
		sendJson(res, 200, !availability.available && availability.code === "outside-allowed-folders" ? {
			...availability,
			retryable: false,
			...current.canAllow ? { canAllow: true } : {}
		} : availability);
		return true;
	}
	let byteStream;
	try {
		const media = await openAssistantMedia(source, policy, allowance);
		const resolvedReference = media.reference;
		const localPath = resolvedReference.path;
		let opened = media.opened;
		byteStream = createGatewayByteStream(res, opened.handle, () => respondNotFound(res));
		let contentType = media.mimeType ?? "application/octet-stream";
		let filename = resolvedReference.kind === "inbound" ? extractOriginalFilename(localPath) : path.basename(localPath);
		const mediaKind = kindFromMime(contentType);
		if (url.searchParams.get("playback") === "1" && (mediaKind === "audio" || mediaKind === "video")) {
			const playback = await resolvePlaybackTranscode({
				sourcePath: opened.realPath,
				sourceStat: opened.stat,
				mimeType: contentType,
				kind: mediaKind
			});
			if (playback.kind === "preparing") {
				await byteStream.close();
				assertCurrentPolicy();
				sendJson(res, 202, { status: "preparing" });
				return true;
			}
			if (playback.kind === "transcoded") {
				const transcoded = await openLocalFileSafely({ filePath: playback.path }).catch(() => null);
				if (transcoded) {
					await byteStream.close();
					opened = transcoded;
					byteStream = createGatewayByteStream(res, opened.handle, () => respondNotFound(res));
					contentType = playback.contentType;
					filename = replacePlaybackFileExtension(filename, playback.extension);
				}
			}
		}
		assertCurrentPolicy();
		if (media.outsideRoots && mediaKind === "image") applyHttpImageContentSecurityPolicy(res);
		res.setHeader("Content-Type", contentType);
		res.setHeader("Content-Disposition", buildAssistantMediaContentDisposition(filename, contentType));
		res.setHeader("Cache-Control", "no-cache");
		const byteResponse = resolveByteResponse({
			file: { size: opened.stat.size },
			method: req.method,
			request: req
		});
		writeByteHeaders(res, byteResponse);
		await byteStream.pipe(byteResponse, req.method);
		return true;
	} catch {
		await byteStream?.close();
		respondNotFound(res);
		return true;
	}
}
async function handleControlUiAvatarRequest(req, res, opts) {
	const urlRaw = req.url;
	if (!urlRaw) return false;
	if (!isReadHttpMethod(req.method)) return false;
	const url = new URL(urlRaw, "http://localhost");
	const basePath = normalizeControlUiBasePath(opts.basePath);
	const pathname = url.pathname;
	const parsed = parseControlUiResourcePath("agentAvatar", pathname, basePath);
	if (!parsed.matched) return false;
	applyControlUiSecurityHeaders(res);
	const agentId = parsed.value;
	if (!agentId || !isValidAgentPathSegment(agentId)) {
		respondNotFound(res);
		return true;
	}
	if (!await authorizeControlUiReadRequestOrReply({
		req,
		res,
		auth: opts.auth,
		trustedProxies: opts.trustedProxies,
		allowRealIpFallback: opts.allowRealIpFallback,
		rateLimiter: opts.rateLimiter
	})) return true;
	const identity = resolveAssistantIdentity({
		cfg: opts.config,
		agentId
	});
	const projection = openGatewayAssistantAvatar({
		cfg: opts.config,
		identity
	});
	const resolved = projection.resolution;
	if (url.searchParams.get("meta") === "1") {
		try {
			const meta = controlUiAvatarResolutionMeta(resolved);
			sendJson(res, 200, {
				avatarUrl: gatewayAssistantAvatarUrl(projection, basePath, agentId) ?? (resolved?.kind === "remote" ? resolved.url : null),
				avatarSource: meta.avatarSource,
				avatarStatus: meta.avatarStatus,
				avatarReason: meta.avatarReason
			});
		} finally {
			if (projection.openedFile) fs.closeSync(projection.openedFile.fd);
		}
		return true;
	}
	if (url.searchParams.has("v") && (projection.openedFile || resolved?.kind === "data")) {
		const source = projection.openedFile ? { file: projection.openedFile } : { dataUrl: identity.avatar };
		try {
			const image = await (await loadAvatarThumbnail()).readGatewayAvatarThumbnail(source);
			res.setHeader("vary", "Authorization, Cookie");
			sendHttpImageResponse({
				req,
				res,
				image,
				filename: "avatar",
				cacheControl: url.searchParams.get("v") === gatewayAvatarImageRevision(source) ? "private, max-age=31536000, immutable" : "private, no-cache"
			});
		} catch {
			respondNotFound(res);
		} finally {
			if (projection.openedFile) fs.closeSync(projection.openedFile.fd);
		}
		return true;
	}
	if (resolved?.kind !== "local" || !projection.openedFile) {
		respondNotFound(res);
		return true;
	}
	try {
		res.setHeader("Content-Type", resolveAvatarMime(projection.openedFile.path));
		res.setHeader("Cache-Control", "no-cache");
		if (req.method === "HEAD") {
			res.statusCode = 200;
			res.setHeader("Content-Length", String(projection.openedFile.stat.size));
			res.end();
			return true;
		}
		const body = await readFileDescriptorBounded(projection.openedFile.fd, AVATAR_MAX_BYTES);
		res.end(body);
		return true;
	} catch {
		respondNotFound(res);
		return true;
	} finally {
		fs.closeSync(projection.openedFile.fd);
	}
}
async function serveResolvedIndexHtml(req, res, body, basePath, allowWasm, environment, buildId) {
	const normalizedBasePath = normalizeControlUiBasePath(basePath);
	const withBasePath = rewriteControlUiIndexHtmlAssetHrefs(body, normalizedBasePath, buildId);
	const basePathAttribute = ` ${CONTROL_UI_BASE_PATH_ATTRIBUTE}="${escapeHtmlAttribute(normalizedBasePath)}"`;
	const environmentAttributes = environment ? ` ${CONTROL_UI_ENVIRONMENT_ATTRIBUTE}="${escapeHtmlAttribute(JSON.stringify(environment))}"` : "";
	const buildAttribute = buildId ? ` ${CONTROL_UI_BUILD_ID_ATTRIBUTE}="${escapeHtmlAttribute(buildId)}"` : "";
	const prepared = withBasePath.replace(/<html\b[^>]*>/i, (tag) => tag.replace(new RegExp(`\\s${CONTROL_UI_BUILD_ID_ATTRIBUTE}="[^"]*"`, "g"), "").replace(/<html\b/i, `<html${basePathAttribute} ${CONTROL_UI_TERMINAL_ENABLED_ATTRIBUTE}="${allowWasm === true}"${environmentAttributes}${buildAttribute}`));
	const hashes = computeInlineScriptHashes(prepared);
	res.setHeader("Content-Security-Policy", buildControlUiCspHeader({
		inlineScriptHashes: hashes,
		allowWasm,
		portalHost: req.headers.host
	}));
	res.setHeader("Content-Type", "text/html; charset=utf-8");
	res.setHeader("Cache-Control", "no-cache");
	await sendControlUiHtmlBody(req, res, prepared);
}
function isExpectedSafePathError(error) {
	const code = typeof error === "object" && error !== null && "code" in error ? String(error.code) : "";
	return code === "ENOENT" || code === "ENOTDIR" || code === "ELOOP";
}
function resolveSafeControlUiFile(rootReal, filePath, rejectHardlinks) {
	const opened = openRootFileSync({
		absolutePath: filePath,
		rootPath: rootReal,
		rootRealPath: rootReal,
		boundaryLabel: "control ui root",
		skipLexicalRootCheck: true,
		rejectSymlinks: false,
		rejectHardlinks
	});
	if (!opened.ok) return matchRootFileOpenFailure(opened, {
		io: (failure) => {
			throw failure.error;
		},
		fallback: () => null
	});
	return {
		path: opened.path,
		fd: opened.fd,
		size: opened.stat.size,
		mtimeMs: opened.stat.mtimeMs
	};
}
function isSafeRelativePath(relPath) {
	if (!relPath) return false;
	const normalized = path.posix.normalize(relPath);
	if (path.posix.isAbsolute(normalized) || path.win32.isAbsolute(normalized)) return false;
	if (normalized.startsWith("../") || normalized === "..") return false;
	if (normalized.includes("\0")) return false;
	return true;
}
const CONTROL_UI_DEFAULT_NAMESPACE_BOOTSTRAP_CONFIG_PATH = `${CONTROL_UI_NAMESPACE_PREFIX.replace(/\/$/, "")}${CONTROL_UI_BOOTSTRAP_CONFIG_PATH}`;
const LEGACY_BOOTSTRAP_CONFIG_PATH = `/__openclaw${CONTROL_UI_BOOTSTRAP_CONFIG_PATH}`;
/**
* Whether `pathname` should be served the Control UI bootstrap config payload.
*
* The canonical endpoint is the configured base path joined with the shared
* bootstrap constant (or the bare constant when no base path is configured).
* For every base path (configured or empty) we additionally accept the legacy
* single-underscore suffix `${basePath}/__openclaw/control-ui-config.json` that
* current main and v2026.6.1 serve and document, so older bundles and clients
* that still request the pre-#66946 endpoint keep receiving config after an
* upgrade instead of 404ing. When no base path is configured we further accept
* the default-namespace alias `/__openclaw__/control-ui-config.json`, which is
* what the default `/__openclaw__/` entry requests after inferring its base path
* from the URL. All compatibility endpoints are preserved; no path is removed.
*/
function matchesControlUiBootstrapConfigPath(pathname, basePath) {
	if (pathname === `${basePath}/control-ui-config.json` || pathname === `${basePath}${LEGACY_BOOTSTRAP_CONFIG_PATH}`) return true;
	return basePath === "" && pathname === CONTROL_UI_DEFAULT_NAMESPACE_BOOTSTRAP_CONFIG_PATH;
}
async function handleControlUiHttpRequest(req, res, opts) {
	const urlRaw = req.url;
	if (!urlRaw) return false;
	const url = new URL(urlRaw, "http://localhost");
	const basePath = normalizeControlUiBasePath(opts?.basePath);
	const pathname = url.pathname;
	const terminalEnabled = opts?.terminalEnabled ?? isTerminalConfigEnabled(opts?.config);
	const route = classifyControlUiRequest({
		basePath,
		pathname,
		search: url.search,
		method: req.method,
		accept: req.headers?.accept
	});
	if (route.kind === "not-control-ui") return false;
	if (route.kind === "not-found") {
		applyControlUiSecurityHeaders(res);
		respondNotFound(res);
		return true;
	}
	if (route.kind === "redirect") {
		applyControlUiSecurityHeaders(res);
		res.statusCode = 302;
		res.setHeader("Location", route.location);
		res.end();
		return true;
	}
	applyControlUiSecurityHeaders(res);
	if (matchesControlUiBootstrapConfigPath(pathname, basePath)) {
		let pluginFrameGrants = [];
		if (!await authorizeControlUiReadRequestOrReply({
			req,
			res,
			auth: opts?.auth,
			trustedProxies: opts?.trustedProxies,
			allowRealIpFallback: opts?.allowRealIpFallback,
			rateLimiter: opts?.rateLimiter,
			onPluginFrameGrants: (grants) => {
				pluginFrameGrants = grants;
			}
		})) return true;
		if (req.method === "HEAD") {
			res.statusCode = 200;
			res.setHeader("Content-Type", "application/json; charset=utf-8");
			res.setHeader("Cache-Control", "no-cache");
			res.end();
			return true;
		}
		const config = opts?.config;
		const resolvedIdentity = config ? resolveAssistantIdentity({
			cfg: config,
			agentId: opts?.agentId
		}) : void 0;
		const identity = resolvedIdentity ?? DEFAULT_ASSISTANT_IDENTITY;
		const assistantAgentId = resolvedIdentity?.agentId;
		const avatarProjection = config && resolvedIdentity ? resolveGatewayAssistantAvatar({
			cfg: config,
			identity: resolvedIdentity,
			httpBasePath: basePath
		}) : {
			avatar: identity.avatar,
			resolution: null
		};
		const avatarMeta = controlUiAvatarResolutionMeta(avatarProjection.resolution);
		sendJson(res, 200, {
			basePath,
			assistantName: identity.name,
			assistantAvatar: avatarProjection.avatar,
			assistantAvatarSource: avatarMeta.avatarSource,
			assistantAvatarStatus: avatarMeta.avatarStatus,
			assistantAvatarReason: avatarMeta.avatarReason,
			...assistantAgentId ? { assistantAgentId } : {},
			serverVersion: resolveRuntimeServiceVersion(process.env),
			serverBuildId: config?.gateway?.controlUi?.root === void 0 ? resolveRuntimeServiceBuildId() ?? void 0 : void 0,
			devGitBranch: await resolveDevInstallGitBranch() ?? void 0,
			localMediaPreviewRoots: [...getAgentScopedMediaLocalRoots(config ?? {}, assistantAgentId)],
			embedSandbox: config?.gateway?.controlUi?.embedSandbox === "trusted" ? "trusted" : config?.gateway?.controlUi?.embedSandbox === "strict" ? "strict" : "scripts",
			allowExternalEmbedUrls: config?.gateway?.controlUi?.allowExternalEmbedUrls === true,
			automaticallyFetchFavicons: config?.gateway?.controlUi?.automaticallyFetchFavicons !== false,
			seamColor: config?.ui?.seamColor,
			environment: config?.gateway?.controlUi?.environment,
			communityInvite: config?.gateway?.controlUi?.communityInvite !== false,
			terminalEnabled,
			cliAgentsEnabled: config?.gateway?.cliAgents?.enabled === true,
			pluginAssetsRequireAuth: opts?.auth !== void 0 && opts.auth.mode !== "none",
			pluginFrameGrants: pluginFrameGrants.map(({ pluginId, path: grantPath, match }) => ({
				pluginId,
				path: grantPath,
				match
			}))
		});
		return true;
	}
	const rootState = opts?.root;
	if (rootState?.kind === "invalid") {
		respondControlUiAssetsUnavailable(res, { configuredRootPath: rootState.path });
		return true;
	}
	if (rootState?.kind === "preparing") {
		respondControlUiAssetsUnavailable(res, { preparing: true });
		return true;
	}
	if (rootState?.kind === "failed") {
		respondControlUiAssetsUnavailable(res, { failed: true });
		return true;
	}
	if (!rootState || rootState.kind === "missing") {
		respondControlUiAssetsUnavailable(res);
		return true;
	}
	const root = rootState.path;
	const rootReal = (() => {
		if (rootState.realPath) return rootState.realPath;
		try {
			return fs.realpathSync(root);
		} catch (error) {
			if (isExpectedSafePathError(error)) return null;
			throw error;
		}
	})();
	if (!rootReal) {
		respondControlUiAssetsUnavailable(res);
		return true;
	}
	const uiPath = basePath && pathname.startsWith(`${basePath}/`) ? pathname.slice(basePath.length) : pathname;
	const standaloneDocument = isControlUiApprovalDocumentPath({
		basePath,
		pathname
	}) || isControlUiFocusDocumentPath({
		basePath,
		pathname
	});
	const rel = (() => {
		if (uiPath === ROOT_PREFIX) return "";
		if (uiPath.startsWith(CONTROL_UI_NAMESPACE_PREFIX)) {
			const namespacedRel = uiPath.slice(14);
			if (isControlUiRootPublicAsset(namespacedRel)) return namespacedRel;
		}
		const assetsIndex = uiPath.indexOf("/assets/");
		if (assetsIndex >= 0) return uiPath.slice(assetsIndex + 1);
		return uiPath.slice(1);
	})();
	const requested = standaloneDocument ? "index.html" : rel && !rel.endsWith("/") ? rel : `${rel}index.html`;
	let fileRel;
	try {
		fileRel = decodeURIComponent(requested);
	} catch {
		respondNotFound(res);
		return true;
	}
	if (!isSafeRelativePath(fileRel)) {
		respondNotFound(res);
		return true;
	}
	const filePath = path.resolve(root, fileRel);
	if (!isWithinDir(root, filePath)) {
		respondNotFound(res);
		return true;
	}
	const isBundledRoot = rootState.kind === "bundled";
	if (isBundledRoot && isControlUiPrecompressedAssetExtension(path.extname(fileRel).toLowerCase())) {
		respondNotFound(res);
		return true;
	}
	const rejectHardlinks = !isBundledRoot;
	const fingerprintedAsset = isBundledRoot && fileRel.startsWith("assets/");
	const publicAssetBuildId = isBundledRoot ? rootState.publicAssetBuildId : void 0;
	const immutableAsset = fingerprintedAsset || Boolean(publicAssetBuildId && url.searchParams.get("v") === publicAssetBuildId && isControlUiVersionedPublicAsset(fileRel));
	let servingRootReal = rootReal;
	let rejectRepresentationHardlinks = rejectHardlinks;
	let safeFile = resolveSafeControlUiFile(rootReal, filePath, rejectHardlinks);
	if (!safeFile && fingerprintedAsset && rootState.kind === "bundled") {
		const retained = rootState.retainedAssets?.resolveAsset(fileRel);
		if (retained) {
			servingRootReal = retained.rootRealPath;
			rejectRepresentationHardlinks = true;
			safeFile = resolveSafeControlUiFile(retained.rootRealPath, retained.filePath, true);
		}
	}
	if (safeFile && path.basename(fileRel) !== "index.html" && path.basename(safeFile.path) !== "index.html") {
		const lastModifiedMs = Math.floor(Math.min(safeFile.mtimeMs, Date.now()) / 1e3) * 1e3;
		const representation = resolveOpenedControlUiRepresentation({
			req,
			sourceFile: safeFile,
			contentPath: fileRel,
			precompressed: fingerprintedAsset,
			openPrecompressedFile: (compressedPath) => resolveSafeControlUiFile(servingRootReal, compressedPath, rejectRepresentationHardlinks)
		});
		if (!representation) {
			respondControlUiNotAcceptable(res);
			return true;
		}
		if (isControlUiFileUnmodified(req, lastModifiedMs)) {
			fs.closeSync(representation.bodyFile.fd);
			respondControlUiNotModified(res, {
				immutable: immutableAsset,
				lastModifiedMs
			});
			return true;
		}
		if (req.method === "HEAD") try {
			respondHeadForControlUiFile(res, fileRel, {
				immutable: immutableAsset,
				encoding: representation.encoding,
				contentLength: representation.bodyFile.size,
				lastModifiedMs
			});
			return true;
		} finally {
			fs.closeSync(representation.bodyFile.fd);
		}
		const body = await readAndCloseControlUiFile(representation.bodyFile);
		await serveControlUiAsset(res, fileRel, body, {
			immutable: immutableAsset,
			encoding: representation.encoding,
			lastModifiedMs
		});
		return true;
	}
	if (!safeFile) {
		if (isControlUiStaticAssetExtension(path.extname(fileRel).toLowerCase())) {
			respondNotFound(res);
			return true;
		}
		if (!route.spaFallback) return false;
		const indexPath = path.resolve(root, "index.html");
		if (filePath !== indexPath) safeFile = resolveSafeControlUiFile(rootReal, indexPath, rejectHardlinks);
	}
	if (safeFile) {
		if (req.method === "HEAD") try {
			const encoding = resolveControlUiHtmlEncoding(req);
			if (encoding === "not-acceptable") {
				respondControlUiNotAcceptable(res);
				return true;
			}
			respondHeadForControlUiFile(res, "index.html", { encoding: encoding === "identity" ? void 0 : encoding });
			return true;
		} finally {
			fs.closeSync(safeFile.fd);
		}
		await serveResolvedIndexHtml(req, res, (await readAndCloseControlUiFile(safeFile)).toString("utf8"), basePath, terminalEnabled, opts?.config?.gateway?.controlUi?.environment, publicAssetBuildId);
		return true;
	}
	respondNotFound(res);
	return true;
}
//#endregion
export { handleControlUiAssistantMediaRequest, handleControlUiAvatarRequest, handleControlUiHttpRequest };