/** * `dsh web` — the browser-surface alias over the profile boot: `--profile web` * plus the Web flag family (`--host/--port/--dev/--workspace-root/ * --trusted-host`), each flag becoming a patch over the composed profile * tree. All web runtime glue (dist serving, prompt section, URL line) lives * in the `@deepseek-ai/dsh-web-app` bundle; this launcher only derives * flag patches and the LAN-trust snapshot. * @module @deepseek-ai/dsh/web */ import { networkInterfaces } from 'node:os' import { fileURLToPath } from 'node:url' import type { Context } from 'cordis' import type { PatchOptions } from '@cordisjs/plugin-include' import { addHarnessSourceSection } from '@deepseek-ai/dsh-app-boot' import type { EnvironmentSnapshot } from '@deepseek-ai/dsh-environment' import { runProfile, type ProfileRows } from './profile-boot.ts' const SOURCE_ROOT = fileURLToPath(new URL('../../..', import.meta.url)) /** The webserver schema's all-interfaces bind literal: gates LAN-authority derivation. */ const ALL_INTERFACES_HOST = '0.0.0.0' /** * Non-internal IPv4 interface addresses of this machine — the IP-literal * authorities an all-interfaces bind is reachable by on the LAN. * @returns the addresses in interface order (possibly empty). */ function lanIPv4Addresses(): string[] { return Object.values(networkInterfaces()).flat() .filter((iface): iface is NonNullable => iface !== undefined && iface.family === 'IPv4' && !iface.internal) .map(iface => iface.address) } /** * One LAN-trust resolution for one invocation, sampled exactly once: the * machine's LAN IP literals when the effective bind is all-interfaces, and * the `trustedHosts` value built from them plus the explicit extras. The * single sample is deliberate — display must advertise only addresses the * fence was configured with, so the web-app row receives this same snapshot. * Derived entries are port-less IP literals: DNS rebinding needs an * attacker-controlled name, so an IP-literal Host is safe on any port, and * the bound port may be OS-assigned, unknowable pre-boot. * @param bindHost - the effective webserver bind host (CLI flag, else the composed row value). * @param extra - `--trusted-host` values, in argv order. * @returns the sampled LAN addresses and the connection row's `trustedHosts` value (each possibly empty). */ export function resolveLanTrust( bindHost: string | undefined, extra: readonly string[], ): { lanAddresses: string[]; trustedHosts: string[] } { const lanAddresses = bindHost === ALL_INTERFACES_HOST ? lanIPv4Addresses() : [] return { lanAddresses, trustedHosts: [...lanAddresses, ...extra] } } /** The `dsh web` flag family, already parsed by the argument adapter. */ export interface WebFlags { patches: string[] host?: string port?: number dev: boolean workspaceRoot?: string trustedHosts?: string[] } /** * Derive the web alias's flag patches over an already-composed profile tree. * Patches replace a row's whole config, so each patched row's composed values * are re-read and merged under the overrides. * @param rows - the composed row index from {@link composeProfile}. * @param flags - the parsed flag family. * @returns the flag patch list, in application order. */ function deriveWebFlagPatches( rows: ProfileRows, flags: WebFlags, ): PatchOptions[] { const overrides = new Map>() const put = (entryId: string, key: string, value: unknown): void => { const bag = overrides.get(entryId) ?? {} bag[key] = value overrides.set(entryId, bag) } if (flags.host !== undefined) put('webserver', 'host', flags.host) if (flags.port !== undefined) put('webserver', 'port', flags.port) if (flags.workspaceRoot !== undefined) put('api-gateway', 'workspaceRoot', flags.workspaceRoot) const composedHost = (rows.get('webserver')?.config as { host?: string } | undefined)?.host const { lanAddresses, trustedHosts } = resolveLanTrust(flags.host ?? composedHost, flags.trustedHosts ?? []) if (trustedHosts.length > 0) { // Additive over the composed value: a cordis.patch.yml-configured fence // authority must survive the derived LAN literals and flag extras — a // silent drop of security-relevant fence configuration. const composedTrusted = (rows.get('connection')?.config as { trustedHosts?: string[] } | undefined)?.trustedHosts ?? [] put('connection', 'trustedHosts', [...composedTrusted, ...trustedHosts]) } // mode and lanAddresses are launcher-derived on every boot (--dev also // inserts the client-hmr row), never pass-throughs of composed values. put('web-runtime', 'mode', flags.dev ? 'development' : 'production') put('web-runtime', 'lanAddresses', lanAddresses) // The agent-preset roots are patched by the shared profile boot: they are // an assembly fact of every dsh launcher, and `dsh run` composes agents // from the same roster this alias offers. const patches = [...overrides.entries()].map(([id, bag]): PatchOptions => { const composed = rows.get(id) if (composed === undefined) throw new Error(`dsh: patch target row "${id}" not found in the web profile composition`) return { id, config: { ...(composed.config ?? {}) as Record, ...bag } } }) if (flags.dev) patches.push({ insert: [{ id: 'client-hmr', name: '@deepseek-ai/dsh-client-hmr' }] }) return patches } /** * Whether the composed Web runtime keeps its model- and shell-visible surface * context. The bundle schema defaults the field to true, so only an explicit * false suppresses both the bundle contributions and the launcher-owned * source-checkout section. * @param rows - the composed Web profile rows before launcher flag patches. * @returns true unless the web-runtime row explicitly disables surface context. */ export function webSurfaceContextEnabled(rows: ProfileRows): boolean { return (rows.get('web-runtime')?.config as { surfaceContext?: boolean } | undefined)?.surfaceContext !== false } /** * Serve the browser UI from the web profile. Host/port/workspace-root flags * are passed through only when given (absent, the composed profile values * stand); `web-runtime.mode` and `lanAddresses` are launcher-derived on * every boot. The URL line is printed by the web-app bundle's runtime row * after Loader settlement. * @param flags - the parsed `dsh web` flag family. * @param environment - this run's frozen environment snapshot. */ export async function runWeb(flags: WebFlags, environment: EnvironmentSnapshot): Promise { await runProfile({ environment, profile: 'web', patchFiles: flags.patches, deriveFlagPatches: rows => deriveWebFlagPatches(rows, flags), prepare: (ctx: Context, rows: ProfileRows) => { if (!webSurfaceContextEnabled(rows)) return ctx.inject(['systemPrompt'], (promptCtx) => { addHarnessSourceSection(promptCtx, SOURCE_ROOT) }) }, }) }