UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

980 lines (979 loc) • 43.1 kB
import { I as resolveTimestampMsToIsoString, o as asDateTimestampMs } from "./number-coercion-CkhMiRw8.mjs"; import { n as resolveOpenClawPackageRoot } from "./openclaw-root-BGdnWXIW.mjs"; import { o as resolveUserPath } from "./home-dir-Dc6nN6HB.mjs"; import { t as FsSafeError, u as openLocalFileSafely } from "./fs-safe-BBzelG8F.mjs"; import "./utils-CitNzLyF.mjs"; import { t as escapeRegExp } from "./regexp-BZyMFTlj.mjs"; import { l as resolveRuntimeServiceVersion, s as resolveRuntimeServiceBuildId } from "./version-D0DaWqMn.mjs"; import { r as createLazyRuntimeModule } from "./lazy-runtime-CJrzFzqU.mjs"; import { r as isIncognitoSessionKey } from "./session-key-CmZ4o9-l.mjs"; import { c as resolveControlUiHtmlEncoding, d as respondControlUiNotModified, f as respondHeadForControlUiFile, g as isControlUiFocusPath, h as isControlUiPrecompressedAssetExtension, i as isControlUiApprovalDocumentPath, l as resolveControlUiRepresentation, m as serveControlUiAsset, n as readControlUiRootAsset, o as isControlUiFileUnmodified, p as sendControlUiHtmlBody, r as classifyControlUiRequest, s as isControlUiStaticAssetExtension, u as respondControlUiNotAcceptable } from "./server-control-ui-root-D-EgRXOn.mjs"; import "./session-key-DY3MOX4N.mjs"; import { n as startsWithSvgRootElement } from "./svg-image-1iOdCRUI.mjs"; import { i as runCommandWithTimeout } from "./exec-DdJWnONN.mjs"; import { l as createHttpRequestAbortSignal } from "./http-body-BBg_CVrI.mjs"; import { t as resolveGatewayPublicOrigin } from "./gateway-public-origin-BcHLka2A.mjs"; import { t as normalizeControlUiBasePath } from "./control-ui-shared-DqFhbHR8.mjs"; import { d as CONTROL_UI_BUILD_ID_ATTRIBUTE, f as CONTROL_UI_ROOT_PUBLIC_ASSETS, h as isControlUiVersionedPublicAsset, m as isControlUiRootPublicAsset, p as buildControlUiRootAssetPath, u as selectControlUiRoutePreloads } from "./control-ui-assets-B-FudsGG.mjs"; import { t as _usingCtx } from "./usingCtx-BhiZp_rS.mjs"; import { c as resolveAvatarMime } from "./avatar-policy-DiIdI6oM.mjs"; 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.gateway-BpIrk2MA.mjs"; import { t as escapeHtml } from "./html-escape-BMD_QFeA.mjs"; import "./user-profile-constants-DfyZS95p.mjs"; import { t as captureResidentUserProfileAccess } from "./user-profile-list-B5oc-3kB.mjs"; import { n as isCloudWorkerPlacementState } from "./session-placement-state-BsPYZ8PU.mjs"; import { l as kindFromMime, n as detectMime } from "./mime-C3j3dfQb.mjs"; import { n as authorizeOperatorScopesForMethod } from "./method-scopes-CRSVCw9J.mjs"; import { t as safeEqualSecret } from "./secret-equal-DRsL8lKD.mjs"; import { r as resolveRequestedSessionAgentId } from "./session-request-agent-BWnhhVvT.mjs"; import { i as resolveSessionPermissionCoreToolPolicy } from "./session-permission-exec-mode-w9RzCNiZ.mjs"; import { n as resolveEffectiveToolFsWorkspaceOnly } from "./tool-fs-policy-D543Bcua.mjs"; import { i as getDefaultMediaLocalRoots, n as getAgentScopedMediaLocalRoots } from "./local-roots-Bd0Cpppp.mjs"; import { o as extractOriginalFilename } from "./store-7jnF8fA5.mjs"; import { a as resolveMediaReferenceLocalPathInfo } from "./media-reference-CTQTi3FT.mjs"; import { n as LocalMediaAccessError, r as assertLocalMediaAllowed } from "./local-media-access-BBod-__x.mjs"; import { t as resolveSessionWorkerPlacementContext } from "./session-worker-placement-context-CvBn6ZZM.mjs"; import { i as parseControlUiResourcePath, o as resolveAssistantMediaRoutePath } from "./control-ui-resource-routes-B42sqIG-.mjs"; import "./control-ui-contract-IX_0OA0h.mjs"; import { i as DEFAULT_ASSISTANT_IDENTITY, n as prepareGatewayAssistantAvatar, o as resolveAssistantIdentity, r as resolveGatewayAssistantAvatar, t as gatewayAssistantAvatarUrl } from "./assistant-avatar-DvPLNhcv.mjs"; import { i as loadGatewaySessionEntryReadOnly } from "./session-utils-store-qn-zT-Fk.mjs"; import "./session-sharing-BFM0s33P.mjs"; import { p as createProfileSessionEntryFilter } from "./session-sharing-target-read-CNtvN3Ha.mjs"; import { t as resolveSessionWorkspaceRoots } from "./session-workspace-roots-C9j-5mH-.mjs"; import "./session-utils-BLqrCe75.mjs"; import { n as resolvePublicAgentAvatarSource } from "./identity-avatar-s-8FJZYz.mjs"; import { i as respondPlainText, n as isReadHttpMethod, r as respondNotFound } from "./control-ui-http-utils-CpDIFFwm.mjs"; import { i as resolvePlaybackTranscode, n as replacePlaybackFileExtension, r as resolvePlaybackMetadataForSource, t as PlaybackInspectionBusyError } from "./playback-transcode-D0ITaxEJ.mjs"; import { r as resolveAssistantMediaFilename, t as buildAssistantMediaContentDisposition } from "./assistant-media-content-disposition-wN2bcgk6.mjs"; import { i as writeByteHeaders, r as resolveByteResponse, t as createGatewayByteStream } from "./http-byte-range-Dn3lj8O7.mjs"; import { _ as resolveHttpProfile, g as prepareHttpUserProfileCatalog, h as applyHttpOperatorRoleScopeCeiling, t as authorizeControlUiReadRequestOrReply } from "./http-auth-utils-xEG2Jlr0.mjs"; import "./http-utils-CtzRELxw.mjs"; import { r as applyHttpImageContentSecurityPolicy, s as sendHttpImageResponse } from "./http-image-response-BL-Tc_n7.mjs"; import { r as serveControlUiShareDocument, t as isControlUiSharePath } from "./control-ui-share-DjoTgUR8.mjs"; import { t as isTerminalConfigEnabled } from "./enabled-BSjeiWpO.mjs"; 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 { readFileWindowFully, safeFileURLToPath } from "@openclaw/fs-safe/advanced"; import { isWithinDir } from "@openclaw/fs-safe/path"; //#region src/infra/dev-install-branch.ts const GIT_TIMEOUT_MS = 3e3; const HIDDEN_BRANCHES = /* @__PURE__ */ new Set([ "main", "master", "HEAD" ]); async function detectDevInstallGitBranch(packageRoot) { const root = packageRoot ? path.resolve(packageRoot) : null; if (!root) return null; const topRes = await runCommandWithTimeout([ "git", "-C", root, "rev-parse", "--show-toplevel" ], { timeoutMs: GIT_TIMEOUT_MS }); if (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 runCommandWithTimeout([ "git", "-C", root, "rev-parse", "--abbrev-ref", "HEAD" ], { timeoutMs: GIT_TIMEOUT_MS }); if (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(detectDevInstallGitBranch).catch(() => null); return cached; } //#endregion //#region src/gateway/assistant-media-errors.ts function classifyAssistantMediaError(err) { if (err instanceof PlaybackInspectionBusyError) return { available: false, code: "attachment-unavailable", reason: err.message, retryable: true }; 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" }; } else 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" }; } //#endregion //#region src/gateway/assistant-media-policy.ts function resolveAssistantMediaReaderAuth(reader, config) { try { const currentProfile = reader.profileId ? resolveHttpProfile(captureResidentUserProfileAccess(reader.profileId).assertCurrent().id, 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; const remote = Boolean(entry?.execNode || entry?.repositoryWorkspaceId); let session; let sessionRoot; let executionCwd; 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 }; if (!remote) { const workspace = resolveSessionWorkspaceRoots(config, agentId, entry); sessionRoot = entry.sessionRoot ?? workspace.root; executionCwd = workspace.diffCwd; } } const workspaceOnly = !session || (entry?.permissionMode ? resolveSessionPermissionCoreToolPolicy({ mode: entry.permissionMode }).workspaceOnly : resolveEffectiveToolFsWorkspaceOnly({ cfg: config, agentId })); const localRoots = [...remote ? getDefaultMediaLocalRoots() : getAgentScopedMediaLocalRoots(config, agentId, workspaceOnly ? sessionRoot : void 0)]; if (sessionRoot && !localRoots.includes(sessionRoot)) localRoots.push(sessionRoot); const placement = session ? resolveSessionWorkerPlacementContext().workerSessionPlacementService?.getMany([session.sessionId]).get(session.sessionId) : void 0; return { session, executionCwd, remote: remote || isCloudWorkerPlacementState(placement?.state), localRoots, workspaceOnly, reader, canAllow: auth.operatorScopes.includes("operator.admin") }; } const CONTROL_UI_ASSISTANT_MEDIA_TICKET_SCOPE = "assistant-media"; const CONTROL_UI_ASSISTANT_MEDIA_TICKET_TTL_MS = 3e5; const controlUiAssistantMediaTicketSecret = randomBytes(32); 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 decodedPayload = Buffer.from(encodedPayload, "base64url").toString("utf8"); const payload = JSON.parse(decodedPayload); return payload.scope === CONTROL_UI_ASSISTANT_MEDIA_TICKET_SCOPE && typeof payload.source === "string" && (source === void 0 || 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; } } //#endregion //#region src/gateway/control-ui-bootstrap-presentation.ts /** Public presentation settings of the Gateway serving the UI, never model permissions. */ function resolveControlUiBootstrapPresentation(config) { return { embedSandbox: config?.gateway?.controlUi?.embedSandbox ?? "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, newSessionModelDefaults: config?.gateway?.controlUi?.newSessionModelDefaults ?? "last-used", uploadsEnabled: config?.gateway?.uploads?.enabled !== false }; } //#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) { const openTag = match[0].slice(0, match[0].indexOf(">") + 1); if (Array.from(openTag.matchAll(SCRIPT_ATTRIBUTE_NAME_RE)).some((attribute) => attribute[1]?.toLowerCase() === "src")) continue; const content = match[1]; if (!content) continue; const hash = createHash("sha256").update(content, "utf8").digest("base64"); hashes.push(`sha256-${hash}`); } return hashes; } /** 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:", "blob:", "https://api.openai.com", "https://tweakcn.com" ]; if (opts?.portalHost) { const parsed = URL.parse(`http://${opts.portalHost}`); if (parsed?.hostname && !parsed.username && !parsed.password && parsed.pathname === "/" && !parsed.search && !parsed.hash) connectTokens.push(`http://${parsed.hostname}:*`, `https://${parsed.hostname}:*`); } return [ "default-src 'self'", "base-uri 'none'", "object-src 'none'", "frame-ancestors 'none'", "frame-src 'self' blob: http: https:", `script-src ${scriptTokens.join(" ")}`, "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com", "img-src 'self' data: blob: https:", "media-src 'self' data: blob:", "font-src 'self' https://fonts.gstatic.com", "worker-src 'self'", `connect-src ${connectTokens.join(" ")}` ].join("; "); } 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=*"); } //#endregion //#region src/gateway/control-ui-index.ts /** Anchors bundled assets before deep-linked documents begin preloading. */ function rewriteControlUiIndexHtmlAssetHrefs(html, basePath, buildId) { const normalized = normalizeControlUiBasePath(basePath); const replacements = /* @__PURE__ */ new Map([["src=\"./assets/", `src="${normalized}/assets/`], ["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}"`; replacements.set(`href="./${asset}"`, assetHref); replacements.set(`href="/${asset}"`, assetHref); replacements.set(`href="${buildControlUiRootAssetPath(normalized, asset)}"`, assetHref); } const pattern = new RegExp([...replacements.keys()].map(escapeRegExp).join("|"), "g"); return html.replace(pattern, (match) => replacements.get(match) ?? match); } async function serveControlUiIndexHtml(req, res, body, uiPath, basePath, allowWasm, environment, buildId, sessionEntryPath, isSessionEntryCurrent, proxySessionEntry = false) { const normalizedBasePath = normalizeControlUiBasePath(basePath); const preloadRoute = uiPath === "/chat" || uiPath.startsWith("/chat/") ? "chat" : uiPath === "/new" || uiPath === "/new/" ? "new" : null; const withBasePath = rewriteControlUiIndexHtmlAssetHrefs(selectControlUiRoutePreloads(body, preloadRoute), normalizedBasePath, buildId); const basePathAttribute = ` ${CONTROL_UI_BASE_PATH_ATTRIBUTE}="${escapeHtml(normalizedBasePath)}"`; const environmentAttributes = environment ? ` ${CONTROL_UI_ENVIRONMENT_ATTRIBUTE}="${escapeHtml(JSON.stringify(environment))}"` : ""; const buildAttribute = buildId ? ` ${CONTROL_UI_BUILD_ID_ATTRIBUTE}="${escapeHtml(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}${proxySessionEntry ? " data-openclaw-proxy-session-entry=\"true\"" : ""} ${CONTROL_UI_TERMINAL_ENABLED_ATTRIBUTE}="${allowWasm === true}"${environmentAttributes}${buildAttribute}`)); const document = sessionEntryPath ? prepared.replace(/<head\b[^>]*>/i, (tag) => `${tag}<script>history.replaceState(null,"",${JSON.stringify(sessionEntryPath).replaceAll("<", "\\u003c")}+location.hash);<\/script>`) : prepared; const hashes = computeInlineScriptHashes(document); 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", sessionEntryPath ? "no-store" : "no-cache"); await sendControlUiHtmlBody(req, res, document, isSessionEntryCurrent); } //#endregion //#region src/gateway/control-ui.ts const ROOT_PREFIX = "/"; 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 loadAvatarThumbnail = createLazyRuntimeModule(() => import("./assistant-avatar-thumbnail.runtime.js")); const CONTROL_UI_NAMESPACE_PREFIX = "/__openclaw__/"; 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 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, root) { const message = root?.kind === "preparing" ? "Control UI assets are being prepared. Try again shortly." : root?.kind === "failed" ? "Control UI assets could not be prepared. Check the Gateway logs or run `openclaw doctor --fix`." : root?.kind === "invalid" && root.path ? `Control UI assets not found at ${root.path}. Build them with \`pnpm ui:build\` (auto-installs UI deps), or update gateway.controlUi.root.` : CONTROL_UI_ASSETS_MISSING_MESSAGE; if (root?.kind === "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 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, signal, assertCurrent) { try { assertCurrent(); const { opened, mimeType, file } = await openAssistantMedia(source, policy, allowance); await opened[Symbol.asyncDispose](); const mediaKind = kindFromMime(mimeType); const playbackMetadata = mimeType && (mediaKind === "audio" || mediaKind === "video") ? await resolvePlaybackMetadataForSource({ sourcePath: opened.realPath, sourceStat: opened.stat, mimeType, kind: mediaKind, signal, assertCurrent }) : void 0; return { available: true, ...mimeType ? { mimeType } : {}, sizeBytes: opened.stat.size, ...playbackMetadata, ...createAssistantMediaTicket({ source, agentId, session: policy.session, reader: policy.reader, ...file ? { file } : {} }) }; } 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); let 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 relativeSource = !path.isAbsolute(source) && !/^[a-z][a-z0-9+.-]*:/iu.test(source); const ticketCandidate = verifyAssistantMediaTicket(url.searchParams.get("mediaTicket"), relativeSource ? void 0 : source, agentId); const requestAuth = isMetaRequest || !ticketCandidate ? await authorizeControlUiReadRequestOrReply({ ...opts, req, res, cfg: opts?.cfg ?? opts?.config, allowQueryToken: !explicitAllow }) : void 0; if ((isMetaRequest || !ticketCandidate) && !requestAuth) return true; if (requestAuth?.authenticatedUserProfile?.profileId || !isMetaRequest && ticketCandidate?.reader.profileId) { if (!await prepareHttpUserProfileCatalog(res) || requestAuth?.hasCurrentClientAuthority?.() === false || !isMetaRequest && ticketCandidate && !verifyAssistantMediaTicket(url.searchParams.get("mediaTicket"), relativeSource ? void 0 : source, agentId)) { if (!res.destroyed && !res.writableEnded) respondNotFound(res); return true; } } const policyParams = { config: opts?.config ?? {}, sessionKey, agentId }; const policy = resolveAssistantMediaPolicy({ ...policyParams, requestAuth: requestAuth ?? void 0, reader: isMetaRequest ? void 0 : ticketCandidate?.reader }); if (!policy) { respondNotFound(res); return true; } if (relativeSource) { if (policy.remote || !policy.executionCwd || !path.isAbsolute(policy.executionCwd)) { respondNotFound(res); return true; } source = path.resolve(policy.executionCwd, source); } const ticket = ticketCandidate?.source === source ? ticketCandidate : void 0; 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 (requestAuth?.hasCurrentClientAuthority?.() === false || !current || current.session?.sessionKey !== policy.session?.sessionKey || current.session?.agentId !== policy.session?.agentId || current.session?.sessionId !== policy.session?.sessionId || current.remote !== policy.remote || current.executionCwd !== policy.executionCwd || 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) try { var _usingCtx$1 = _usingCtx(); const requestAbort = createHttpRequestAbortSignal(res.req, res); _usingCtx$1.u({ [Symbol.dispose]: requestAbort.cleanup }); const availability = await resolveAssistantMediaAvailability(source, policy, allowance, agentId, requestAbort.signal, assertCurrentPolicy); if (requestAbort.signal.aborted) return true; 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; } catch (_) { _usingCtx$1.e = _; } finally { _usingCtx$1.d(); } let byteStream; try { const media = await openAssistantMedia(source, policy, allowance); let opened = media.opened; byteStream = createGatewayByteStream(res, opened.handle, () => respondNotFound(res)); let contentType = media.mimeType ?? "application/octet-stream"; let filename = media.reference.kind === "inbound" ? resolveAssistantMediaFilename(extractOriginalFilename(media.reference.path), url.searchParams.get("filename")) : path.basename(media.reference.path); 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, signal: byteStream.signal, assertCurrent: assertCurrentPolicy }); 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(); if (!res.destroyed && !res.writableEnded) 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; } const requestAuth = await authorizeControlUiReadRequestOrReply({ ...opts, req, res, cfg: opts.cfg ?? opts.config }); if (!requestAuth) return true; requestAuth.assertCurrent(); try { const identity = await resolveAssistantIdentity({ cfg: opts.config, agentId }); const projection = await prepareGatewayAssistantAvatar({ cfg: opts.config, identity, readBody: url.searchParams.get("meta") !== "1" && (req.method !== "HEAD" || url.searchParams.has("v")) }); requestAuth.assertCurrent(); const resolved = projection.resolution; if (url.searchParams.get("meta") === "1") { sendJson(res, 200, { avatarUrl: gatewayAssistantAvatarUrl(projection, basePath, agentId) ?? (resolved?.kind === "remote" ? resolved.url : null), ...controlUiAvatarResolutionMeta(resolved) }); return true; } if (url.searchParams.has("v") && projection.image) { const image = await (await loadAvatarThumbnail()).readGatewayAvatarThumbnail(projection.image); requestAuth.assertCurrent(); res.setHeader("vary", "Authorization, Cookie"); sendHttpImageResponse({ req, res, image, filename: "avatar", cacheControl: url.searchParams.get("v") === projection.image.revision ? "private, max-age=31536000, immutable" : "private, no-cache" }); return true; } if (resolved?.kind !== "local" || !projection.file) { respondNotFound(res); return true; } res.setHeader("Content-Type", resolveAvatarMime(projection.file.path)); res.setHeader("Cache-Control", "no-cache"); if (req.method === "HEAD") { res.statusCode = 200; res.setHeader("Content-Length", String(projection.file.stat.size)); res.end(); return true; } res.end(projection.file.body); return true; } catch { if (!res.writableEnded && !res.destroyed) { requestAuth.assertCurrent(); respondNotFound(res); } return true; } } function isExpectedSafePathError(error) { const code = typeof error === "object" && error !== null && "code" in error ? String(error.code) : ""; return code === "ENOENT" || code === "ENOTDIR" || code === "ELOOP"; } function isSafeRelativePath(relPath) { if (!relPath) return false; const normalized = path.posix.normalize(relPath); return !(path.posix.isAbsolute(normalized) || path.win32.isAbsolute(normalized) || normalized.startsWith("../") || normalized === ".." || normalized.includes("\0")); } const CONTROL_UI_DEFAULT_NAMESPACE_BOOTSTRAP_CONFIG_PATH = `${CONTROL_UI_NAMESPACE_PREFIX.replace(/\/$/, "")}${CONTROL_UI_BOOTSTRAP_CONFIG_PATH}`; function matchesControlUiBootstrapConfigPath(pathname, basePath) { return pathname === `${basePath}/control-ui-config.json` || 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 (isControlUiSharePath(pathname, basePath) && pathname !== `${basePath}/share/card.png`) { serveControlUiShareDocument(req, res, url, basePath, resolveGatewayPublicOrigin(opts?.config)); return true; } if (matchesControlUiBootstrapConfigPath(pathname, basePath)) { let pluginFrameGrants = []; const requestAuth = await authorizeControlUiReadRequestOrReply({ ...opts, req, res, cfg: opts?.cfg ?? opts?.config, onPluginFrameGrants: (grants) => { pluginFrameGrants = grants; } }); if (!requestAuth) return true; requestAuth.assertCurrent(); 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 ? await resolveAssistantIdentity({ cfg: config, agentId: opts?.agentId }) : void 0; const identity = resolvedIdentity ?? DEFAULT_ASSISTANT_IDENTITY; const assistantAgentId = resolvedIdentity?.agentId; const avatarProjection = config && resolvedIdentity ? await resolveGatewayAssistantAvatar({ cfg: config, identity: resolvedIdentity, httpBasePath: basePath }) : { avatar: identity.avatar, resolution: null }; const avatarMeta = controlUiAvatarResolutionMeta(avatarProjection.resolution); const devGitBranch = await resolveDevInstallGitBranch() ?? void 0; requestAuth.assertCurrent(); 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, ...resolveControlUiBootstrapPresentation(config), terminalEnabled, cliAgentsEnabled: config?.gateway?.cliAgents?.enabled !== false, 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 || rootState.kind !== "bundled" && rootState.kind !== "resolved") { respondControlUiAssetsUnavailable(res, rootState); return true; } const root = rootState.path; if (!(rootState.realPath || await fs.promises.realpath(root).catch((error) => { if (isExpectedSafePathError(error)) return null; throw error; }))) { respondControlUiAssetsUnavailable(res); return true; } const uiPath = basePath && pathname.startsWith(`${basePath}/`) ? pathname.slice(basePath.length) : pathname; const standaloneDocument = isControlUiApprovalDocumentPath({ basePath, pathname }) || isControlUiFocusPath(pathname, basePath); const rel = (() => { if (uiPath === "/share/card.png") return "social-card.png"; 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 fingerprintedAsset = isBundledRoot && fileRel.startsWith("assets/"); const publicAssetBuildId = isBundledRoot ? rootState.publicAssetBuildId : void 0; const immutableAsset = fingerprintedAsset || Boolean(publicAssetBuildId && url.searchParams.get("v") === publicAssetBuildId && isControlUiVersionedPublicAsset(fileRel)); const readBody = req.method !== "HEAD" && req.headers?.["if-none-match"] === void 0 && req.headers?.["if-modified-since"] === void 0; let asset = await readControlUiRootAsset(rootState, fileRel, readBody); if (!asset) { if (isControlUiStaticAssetExtension(path.extname(fileRel).toLowerCase())) { respondNotFound(res); return true; } if (!route.spaFallback) return false; if (filePath !== path.resolve(root, "index.html")) { fileRel = "index.html"; asset = await readControlUiRootAsset(rootState, fileRel, readBody); } } while (asset) { if (path.basename(fileRel) === "index.html" || path.basename(asset.file.path) === "index.html") { if (req.method === "HEAD") { const encoding = resolveControlUiHtmlEncoding(req); if (encoding === "not-acceptable") { respondControlUiNotAcceptable(res); return true; } respondHeadForControlUiFile(res, "index.html", { encoding: encoding === "identity" ? void 0 : encoding }); return true; } if (!asset.file.body) { asset = await readControlUiRootAsset(rootState, fileRel, true); continue; } await serveControlUiIndexHtml(req, res, asset.file.body.toString("utf8"), uiPath, basePath, terminalEnabled, opts?.config?.gateway?.controlUi?.environment, publicAssetBuildId, opts?.sessionEntryPath, opts?.isSessionEntryCurrent, opts?.auth?.mode === "trusted-proxy"); return true; } const originatedAtMs = Date.now(); const lastModifiedMs = Math.floor(Math.min(asset.file.mtimeMs, originatedAtMs) / 1e3) * 1e3; const representation = resolveControlUiRepresentation({ req, asset, contentPath: fileRel, precompressed: fingerprintedAsset }); if (!representation) { respondControlUiNotAcceptable(res); return true; } if (isControlUiFileUnmodified(req, lastModifiedMs, originatedAtMs)) { respondControlUiNotModified(res, { immutable: immutableAsset, lastModifiedMs }); return true; } const headers = { immutable: immutableAsset, encoding: representation.encoding, lastModifiedMs }; if (req.method === "HEAD") respondHeadForControlUiFile(res, fileRel, { ...headers, contentLength: representation.file.size }); else if (representation.file.body) serveControlUiAsset(res, fileRel, representation.file.body, headers); else { asset = await readControlUiRootAsset(rootState, fileRel, true); continue; } return true; } respondNotFound(res); return true; } //#endregion export { handleControlUiAssistantMediaRequest, handleControlUiAvatarRequest, handleControlUiHttpRequest };