CoolFace
Datasetpublic

basant307/AI_Governance_Project

sourceHugging Faceapache-2.0updated 2mo agoView on Hugging Face
0likes45downloads
server.js777 linesDownload Raw Back to serve
1/**2 * @license3 * Copyright 2025 Qwen Team4 * SPDX-License-Identifier: Apache-2.05 */6import express from 'express';7import { hashDaemonWorkspace } from '@qwen-code/qwen-code-core';8import { allowOriginCors, bearerAuth, createMutationGate, denyBrowserOriginCors, hostAllowlist, parseAllowOriginPatterns, } from './auth.js';9import { createBridgeFileSystemAdapter } from './bridge-file-system-adapter.js';10import { createDaemonStatusProvider } from './daemon-status-provider.js';11import { createWorkspaceProvidersStatusProvider } from './workspace-providers-status.js';12import { createWorkspaceSkillsStatusProvider } from './workspace-skills-status.js';13import { mountAcpHttp } from './acp-http/index.js';14import { createVoiceWsConnectionHandler } from './voice/voice-ws.js';15import { ClientMcpSenderRegistry, createClientMcpServerProvider, } from './acp-http/client-mcp-sender-registry.js';16import { CdpTunnelRegistry } from './cdp-tunnel/cdp-tunnel-registry.js';17import { canonicalizeWorkspace, createAcpSessionBridge, } from './acp-session-bridge.js';18import {} from './types.js';19import { mountWebShellAssets, mountWebShellSpaFallback, } from './web-shell-static.js';20import { mountWorkspaceMemoryRoutes } from './workspace-memory.js';21import { mountWorkspaceMemoryRememberRoutes, WorkspaceRememberTaskLane, } from './workspace-remember.js';22import { mountWorkspaceAgentsRoutes } from './workspace-agents.js';23import { registerDaemonStatusRoutes } from './routes/daemon-status.js';24import { createHealthDemoRoutes } from './routes/health-demo.js';25import { registerWorkspaceAuthRoutes } from './routes/workspace-auth.js';26import { registerWorkspaceExtensionRoutes } from './routes/workspace-extensions.js';27import { registerWorkspaceFileReadRoutes } from './routes/workspace-file-read.js';28import { registerWorkspaceFileWriteRoutes } from './routes/workspace-file-write.js';29import { registerWorkspaceSetupGithubRoutes } from './routes/workspace-setup-github.js';30import { registerWorkspaceTrustRoutes } from './routes/workspace-trust.js';31import { registerPermissionRoutes } from './routes/permission.js';32import { registerSessionRoutes } from './routes/session.js';33import { registerScheduledTasksRoutes } from './routes/scheduled-tasks.js';34import { registerUsageStatsRoutes } from './routes/usage-stats.js';35import { startScheduledTaskKeepalive, rehydrateScheduledTaskSessions, } from './scheduled-task-keepalive.js';36import { registerWorkspaceDiagnosticStatusRoutes, registerWorkspaceStatusRoutes, } from './routes/workspace-status.js';37import { createDaemonWorkspaceService, } from './workspace-service/index.js';38import { registerCapabilitiesRoutes } from './routes/capabilities.js';39import { registerWorkspacePermissionsRoutes } from './routes/workspace-permissions.js';40import { registerWorkspaceSettingsRoutes } from './routes/workspace-settings.js';41import { getActiveSseCount, registerSseEventsRoutes, } from './routes/sse-events.js';42import { registerWorkspaceVoiceRoutes, } from './routes/workspace-voice.js';43import { registerA2uiActionRoutes } from './routes/a2ui-action.js';44import { setRateLimiter } from './rate-limit.js';45import { createTotalSessionAdmissionController, } from './total-session-admission.js';46import { sendBridgeError as sendBridgeErrorResponse, sendPermissionVoteError as sendPermissionVoteErrorResponse, } from './server/error-response.js';47import { resolveBridgeFsFactory } from './server/fs-factory.js';48import { createBuildWorkspaceCtx, parseAndValidateWorkspaceClientId, parseClientIdHeader, safeBody, } from './server/request-helpers.js';49import { daemonTelemetryMiddleware } from './server/telemetry.js';50import { installAccessLogMiddleware } from './server/access-log.js';51import { setupDeviceFlowRegistry } from './server/device-flow-registry.js';52import { installFinalErrorHandler, installJsonBodyParser, } from './server/error-handlers.js';53import { installRateLimiter } from './server/rate-limiter-setup.js';54import { createServeFeatures } from './server/serve-features.js';55import { SessionArchiveCoordinator } from './server/session-archive.js';56import { installSelfOriginStripMiddleware } from './server/self-origin.js';57import { createSingleWorkspaceRegistry, } from './workspace-registry.js';58import { registerWorkspaceLifecycleRoutes } from './routes/workspace-lifecycle.js';59import { registerWorkspaceMcpControlRoutes } from './routes/workspace-mcp-control.js';60import { registerWorkspaceToolsRoutes } from './routes/workspace-tools.js';61export { createDefaultFsAuditEmit, resolveBoundWorkspacesFromIdeEnv, resolveBridgeFsFactory, } from './server/fs-factory.js';62export { PromptDeadlineExceededError, resolvePromptDeadlineMs, } from './server/prompt-deadline.js';63export { detectFromLoopback } from './server/request-helpers.js';64export { InvalidCursorError, listWorkspaceSessionsForResponse, } from './server/session-list.js';65export { getActiveSseCount } from './routes/sse-events.js';66/**67 * Module-scoped once-per-process guard for the `createServeApp`68 * default-trust stderr warning. Without this, tests calling69 * `createServeApp` repeatedly would flood stderr with identical lines.70 */71let warnedDefaultTrust = false;72function describeRegistryPrimaryForConflict(registry) {73    return (`registry primary cwd=${JSON.stringify(registry.primary.workspaceCwd)}, ` +74        `workspaceId=${JSON.stringify(registry.primary.workspaceId)}`);75}76function getRuntimeEffectiveEnv(metadata) {77    return metadata?.effectiveEnv;78}79/**80 * Build the Express app for `qwen serve`. Pure function — no side effects on81 * the network or process; `runQwenServe` does the listen/signal handling.82 *83 * `getPort` is invoked lazily by the host-allowlist middleware so callers84 * binding to port 0 (ephemeral) can supply the actual port after `listen()`85 * resolves. Defaults to `opts.port` for callers (e.g. tests) that pin a port86 * up front.87 *88 * Route modules are registered below in middleware order. Keep this file as89 * the assembly point so auth/rate-limit/body-parser/REST/ACP/Web Shell order90 * stays reviewable in one place.91 *92 * **Workspace validation contract.** `createServeApp` itself does NOT93 * verify that `opts.workspace` exists or is a directory — it94 * canonicalizes via `canonicalizeWorkspace`, which falls back to95 * `path.resolve` on ENOENT so the app boots even against a missing96 * path. `runQwenServe` is the production entry point and DOES97 * perform the `fs.statSync` + `isDirectory()` boot-loud check before98 * calling this function. Tests inject synthetic paths (`/work/bound`99 * etc.) on purpose: they want to exercise the route layer's100 * canonicalization and `workspace_mismatch` translation without101 * needing a real directory on disk. If a future entry point binds102 * `createServeApp` directly to user input, it MUST replicate the103 * `runQwenServe` validation (or call into a shared helper if one is104 * extracted) — otherwise a non-existent `--workspace` would boot105 * a "healthy"-looking daemon whose every spawn fails with cryptic106 * child-process ENOENT.107 */108// Mirrors the bridge's session-idle reaper default (30 min). Used only to109// size the scheduled-task keepalive interval when no explicit timeout is set.110const DEFAULT_SESSION_IDLE_TIMEOUT_MS = 30 * 60_000;111// Bounds for the keepalive interval: ≥30s (avoid busy-looping on a tiny custom112// timeout) and ≤10min (stay well inside the 30-min default reaper window).113const KEEPALIVE_MIN_INTERVAL_MS = 30_000;114const KEEPALIVE_MAX_INTERVAL_MS = 10 * 60_000;115/**116 * Sizes the keepalive heartbeat interval so a resident task session is beaten117 * BEFORE the idle reaper closes it. Targets a third of the reaper window, but118 * never exceeds HALF of it — so at least one heartbeat lands in time even for a119 * small custom timeout, where the 30s floor would otherwise overshoot the whole120 * window and let the session be reaped before the first beat. When the reaper is121 * disabled (idle timeout ≤ 0) sessions are never reaped, so heartbeats aren't122 * needed — the loop still runs (to revive re-enabled bound sessions) but at the123 * relaxed max cadence.124 */125export function computeKeepaliveIntervalMs(idleTimeoutMs) {126    if (idleTimeoutMs <= 0)127        return KEEPALIVE_MAX_INTERVAL_MS;128    const target = Math.min(Math.max(KEEPALIVE_MIN_INTERVAL_MS, Math.floor(idleTimeoutMs / 3)), KEEPALIVE_MAX_INTERVAL_MS);129    return Math.max(1, Math.min(target, Math.floor(idleTimeoutMs / 2)));130}131export function createServeApp(opts, getPort = () => opts.port, deps = {}) {132    const app = express();133    // Forward `maxSessions` into the default-constructed bridge so134    // direct callers of `createServeApp` (tests, embeds) get the same135    // cap they configured via `ServeOptions`. Previously the default136    // bridge silently fell back to `DEFAULT_MAX_SESSIONS` (20) and137    // only the `runQwenServe` path piped the option through.138    //139    // The daemon is bound to exactly one workspace. The value advertised140    // on `/capabilities`, used for the `POST /session` cwd fallback,141    // AND passed into the bridge must be the SAME canonical form.142    // `deps.boundWorkspace` is the pre-canonicalized fast-path from143    // `runQwenServe`; when omitted we canonicalize ourselves.144    const injectedWorkspaceRegistry = deps.workspaceRegistry;145    const boundWorkspace = injectedWorkspaceRegistry?.primary.workspaceCwd ??146        deps.boundWorkspace ??147        canonicalizeWorkspace(opts.workspace ?? process.cwd());148    if (injectedWorkspaceRegistry) {149        const primary = injectedWorkspaceRegistry.primary;150        const registryConflictCandidates = [151            {152                depName: 'deps.boundWorkspace',153                depValue: deps.boundWorkspace,154                registryValue: primary.workspaceCwd,155                detail: `deps.boundWorkspace=${JSON.stringify(deps.boundWorkspace)}`,156            },157            {158                depName: 'deps.bridge',159                depValue: deps.bridge,160                registryValue: primary.bridge,161                detail: 'deps.bridge is a different object',162            },163            {164                depName: 'deps.workspace',165                depValue: deps.workspace,166                registryValue: primary.workspaceService,167                detail: 'deps.workspace is a different object',168            },169            {170                depName: 'deps.fsFactory',171                depValue: deps.fsFactory,172                registryValue: primary.routeFileSystemFactory,173                detail: 'deps.fsFactory is a different object',174            },175            {176                depName: 'deps.clientMcpSenderRegistry',177                depValue: deps.clientMcpSenderRegistry,178                registryValue: primary.clientMcpSenderRegistry,179                detail: 'deps.clientMcpSenderRegistry is a different object',180            },181        ];182        for (const candidate of registryConflictCandidates) {183            if (candidate.depValue === undefined ||184                candidate.depValue === candidate.registryValue) {185                continue;186            }187            throw new Error('createServeApp: workspaceRegistry conflicts with ' +188                `${candidate.depName}: ${describeRegistryPrimaryForConflict(injectedWorkspaceRegistry)}; ${candidate.detail}.`);189        }190    }191    // Construct `fsFactory` BEFORE the bridge so the bridge can wire it192    // through `BridgeFileSystem` for ACP-side writeTextFile/readTextFile.193    // Default trust is `false` (test-safe). Embeds without `deps.fsFactory`194    // or `deps.bridge` will see agent writes rejected with195    // `untrusted_workspace` — warn once so the asymmetry is visible.196    if (!injectedWorkspaceRegistry &&197        !deps.fsFactory &&198        !deps.bridge &&199        !warnedDefaultTrust) {200        warnedDefaultTrust = true;201        process.stderr.write('qwen serve: createServeApp default fsFactory uses trusted=false ' +202            '— agent ACP writeTextFile calls will reject with untrusted_workspace. ' +203            'Inject deps.fsFactory (with explicit trust) or deps.bridge to override.\n');204    }205    const fsFactory = injectedWorkspaceRegistry?.primary.routeFileSystemFactory ??206        resolveBridgeFsFactory({207            boundWorkspaces: [boundWorkspace],208            injected: deps.fsFactory,209            trusted: false,210        });211    const tokenConfigured = typeof opts.token === 'string' && opts.token.length > 0;212    const sessionShellCommandEnabled = opts.enableSessionShell === true && tokenConfigured;213    // Reverse tool channel (issue #5626, Phase 2). Process-scoped registry that214    // bridges the daemon WS (per-connection `ClientMcpRegistrar`) and the ACP215    // child's `client_mcp/message` ext-method. Prefer the registry `runQwenServe`216    // already wired into its injected bridge (`deps.clientMcpSenderRegistry`) so217    // the bridge that answers the child and the WS provider share ONE map.218    // Standalone `createServeApp` (no injected bridge) builds its own and wires219    // it into the bridge it creates below. Inert until a WS client sends220    // `mcp_register` (gated by `clientMcpOverWs`).221    // Guard the split-brain case: an injected `deps.bridge` was already wired to222    // its own sender, so building a fresh registry here would leave the bridge223    // and this registry pointing at different maps. A caller injecting the bridge224    // must inject the matching registry too. Only enforced when `clientMcpOverWs`225    // is active — that's the only path that processes `mcp_*` frames, so without226    // it the registry is inert and a mismatch can't manifest (and the vast227    // majority of tests inject a fake bridge without ever touching client-MCP).228    if (opts.clientMcpOverWs === true &&229        deps.bridge &&230        !injectedWorkspaceRegistry &&231        !deps.clientMcpSenderRegistry) {232        throw new Error('createServeApp: deps.bridge requires deps.clientMcpSenderRegistry ' +233            'when clientMcpOverWs is enabled (the bridge is already wired to its ' +234            'own sender; a fresh registry here would be an orphan).');235    }236    const clientMcpSenderRegistry = injectedWorkspaceRegistry?.primary.clientMcpSenderRegistry ??237        deps.clientMcpSenderRegistry ??238        new ClientMcpSenderRegistry();239    const primaryRuntimeEnvMetadata = injectedWorkspaceRegistry?.primary.env ?? deps.primaryRuntimeEnv;240    const primaryEffectiveEnv = getRuntimeEffectiveEnv(primaryRuntimeEnvMetadata);241    const { languageCodes, currentServeFeatures, invalidateServeFeaturesCache } = createServeFeatures({242        opts,243        boundWorkspace,244        persistSettingAvailable: deps.persistSetting !== undefined,245        // Registry injection supplies the primary workspace service through the246        // runtime, so it has the same reload surface as legacy deps.workspace.247        reloadAvailable: deps.workspace !== undefined || injectedWorkspaceRegistry !== undefined,248        sessionShellCommandEnabled,249    });250    const statusProvider = deps.statusProvider ??251        createDaemonStatusProvider(primaryEffectiveEnv ? { env: primaryEffectiveEnv } : {});252    let defaultBridgeForAdmission;253    const totalSessionAdmission = !deps.bridge && !injectedWorkspaceRegistry254        ? createTotalSessionAdmissionController({255            maxTotalSessions: opts.maxTotalSessions,256            getBridges: () => defaultBridgeForAdmission ? [defaultBridgeForAdmission] : [],257        })258        : undefined;259    const bridge = injectedWorkspaceRegistry?.primary.bridge ??260        deps.bridge ??261        createAcpSessionBridge({262            maxSessions: opts.maxSessions,263            ...(totalSessionAdmission264                ? { freshSessionAdmission: totalSessionAdmission.admit }265                : {}),266            maxPendingPromptsPerSession: opts.maxPendingPromptsPerSession,267            eventRingSize: opts.eventRingSize,268            permissionResponseTimeoutMs: opts.permissionResponseTimeoutMs,269            boundWorkspace,270            sessionShellCommandEnabled,271            // Wire the production status provider so direct embeds / tests272            // that don't inject `deps.bridge` get daemon env + preflight cells.273            statusProvider,274            // Wire the WorkspaceFileSystem adapter so ACP writeTextFile /275            // readTextFile pick up trust / TOCTOU / audit.276            fileSystem: createBridgeFileSystemAdapter(fsFactory),277            // Reverse tool channel: answer the child's `client_mcp/message`278            // ext-method by reaching the WS connection that hosts the named server.279            clientMcpSender: clientMcpSenderRegistry.lookup,280        });281    if (!injectedWorkspaceRegistry && !deps.bridge) {282        defaultBridgeForAdmission = bridge;283    }284    const archiveCoordinator = new SessionArchiveCoordinator();285    installSelfOriginStripMiddleware(app, getPort);286    // Park the factory on `app.locals` so route handlers can pick it up287    // via `req.app.locals.fsFactory` without re-threading the value288    // through every handler signature.289    app.locals.fsFactory =290        fsFactory;291    // Surface the bound workspace on `app.locals` so file routes can292    // compute workspace-relative response paths without re-resolving.293    app.locals.boundWorkspace = boundWorkspace;294    const { deviceFlowRegistry, getSupportedDeviceFlowProviders } = setupDeviceFlowRegistry({295        app,296        bridge,297        registry: deps.deviceFlowRegistry,298        providers: deps.deviceFlowProviders,299    });300    const { daemonLog } = deps;301    const sendBridgeError = (res, err, ctx) => sendBridgeErrorResponse(res, err, ctx, daemonLog);302    const sendPermissionVoteError = (res, err, ctx) => sendPermissionVoteErrorResponse(res, err, ctx, daemonLog);303    const workspace = injectedWorkspaceRegistry?.primary.workspaceService ??304        deps.workspace ??305        createDaemonWorkspaceService({306            boundWorkspace,307            contextFilename: deps.contextFilename ?? 'QWEN.md',308            statusProvider,309            workspaceProvidersStatusProvider: createWorkspaceProvidersStatusProvider(primaryEffectiveEnv ? { env: primaryEffectiveEnv } : {}),310            workspaceSkillsStatusProvider: createWorkspaceSkillsStatusProvider(),311            isChannelLive: () => bridge.isChannelLive(),312            persistDisabledTools: deps.persistDisabledTools ??313                (async () => {314                    throw new Error('setWorkspaceToolEnabled requires persistDisabledTools in ServeAppDeps');315                }),316            queryWorkspaceStatus: (method, idle) => bridge.queryWorkspaceStatus(method, idle),317            invokeWorkspaceCommand: (method, params, invokeOpts) => bridge.invokeWorkspaceCommand(method, params, invokeOpts),318            refreshExtensionsForAllSessions: () => bridge.refreshExtensionsForAllSessions(),319            ...(deps.persistSetting ? { persistSetting: deps.persistSetting } : {}),320            ...(deps.persistSettings321                ? { persistSettings: deps.persistSettings }322                : {}),323            publishWorkspaceEvent: (event) => {324                if (event.type === 'settings_changed' ||325                    event.type === 'settings_reloaded') {326                    invalidateServeFeaturesCache();327                }328                bridge.publishWorkspaceEvent(event);329            },330        });331    const workspaceRegistry = injectedWorkspaceRegistry ??332        createSingleWorkspaceRegistry({333            workspaceId: hashDaemonWorkspace(boundWorkspace),334            workspaceCwd: boundWorkspace,335            primary: true,336            trusted: deps.primaryWorkspaceTrusted ?? false,337            env: primaryRuntimeEnvMetadata ?? {338                mode: 'parent-process',339                overlayKeys: [],340            },341            bridge,342            workspaceService: workspace,343            routeFileSystemFactory: fsFactory,344            clientMcpSenderRegistry,345        });346    app.locals.workspaceRegistry =347        workspaceRegistry;348    const primaryRuntime = workspaceRegistry.primary;349    const primaryBoundWorkspace = primaryRuntime.workspaceCwd;350    const primaryBridge = primaryRuntime.bridge;351    const primaryWorkspace = primaryRuntime.workspaceService;352    const primaryRouteFileSystemFactory = primaryRuntime.routeFileSystemFactory;353    // Order matters: rejection guards (CORS / Host allowlist / bearer auth)354    // run BEFORE the JSON body parser. Otherwise an unauthenticated POST355    // gets a full 10MB `JSON.parse` before the 401 fires — a trivially356    // amplified CPU/memory cost from any wrong-token client.357    //358    // When `--allow-origin` is configured, install the359    // allowlist middleware instead of the deny-wall. The allowlist owns360    // both halves of the policy (matched → CORS headers + pass-through or361    // 204 preflight; unmatched → 403 with the same error envelope as the362    // wall). When `--allow-origin` is empty/undefined, the deny-wall stays363    // installed. Pattern parsing happens in `run-qwen-serve.ts` for validation;364    // here we still keep the wildcard/no-token invariant for embedded365    // callers that construct the app directly.366    if (opts.allowOrigins && opts.allowOrigins.length > 0) {367        const parsedAllowOrigins = parseAllowOriginPatterns(opts.allowOrigins);368        if (parsedAllowOrigins.allowAny && !opts.token) {369            throw new Error(`Refusing to start with --allow-origin '*' but no bearer token ` +370                `configured. '*' admits any cross-origin browser to the API; ` +371                `without a token, any local page can drive the daemon. Set a ` +372                `token or list specific origins instead of '*'.`);373        }374        app.use(allowOriginCors(parsedAllowOrigins));375    }376    else {377        app.use(denyBrowserOriginCors);378    }379    app.use(hostAllowlist(opts.hostname, getPort));380    const healthDemoRoutes = createHealthDemoRoutes({381        opts,382        getPort,383        bridge: primaryBridge,384        getActiveSseCount,385        getRateLimiter: () => rateLimiter,386    });387    if (healthDemoRoutes.exposeHealthPreAuth) {388        healthDemoRoutes.register(app);389    }390    installAccessLogMiddleware(app, daemonLog);391    // Serve the Web Shell static assets (/ and /assets) BEFORE bearerAuth. The392    // static shell carries no secrets and a browser cannot attach an393    // Authorization header to a `<script src>` subresource or an address-bar394    // navigation, so gating it would just break the UI — the front-end's own395    // API calls still carry the bearer (getDaemonAuthHeaders) and every API396    // route below stays token-gated. The SPA deep-link fallback is registered397    // LATER (after all API routes, see mountWebShellSpaFallback) so authed398    // routes win over the shell. The assets dir is resolved by the caller399    // (runQwenServe) and injected via deps.webShellDir; `--no-web` sets400    // opts.serveWebShell=false to opt out.401    const webShellDir = opts.serveWebShell !== false ? deps.webShellDir : undefined;402    // Extension origins (chrome-extension://…) explicitly allowed via403    // --allow-origin may frame the Web Shell so the extension can host the UI in404    // a Chrome side panel (issue #5626). All other origins still get405    // frame-ancestors 'none' + X-Frame-Options: DENY.406    const webShellFrameAncestors = opts.allowOrigins && opts.allowOrigins.length > 0407        ? [...parseAllowOriginPatterns(opts.allowOrigins).origins].filter((o) => o.startsWith('chrome-extension://') ||408            o.startsWith('moz-extension://'))409        : [];410    if (webShellDir) {411        mountWebShellAssets(app, webShellDir, webShellFrameAncestors);412    }413    app.use(bearerAuth(opts.token));414    // Rate limiter: after auth (only count authenticated requests),415    // before body parser (reject early without burning JSON.parse CPU).416    const rateLimiter = installRateLimiter(app, opts, daemonLog);417    installJsonBodyParser(app);418    if (!healthDemoRoutes.exposeHealthPreAuth) {419        // Non-loopback OR loopback with `--require-auth`: register420        // `/health` and `/demo` AFTER `bearerAuth` so probes must carry421        // the token. Otherwise unauthenticated callers can ping any422        // reachable address:port to confirm a daemon exists (and `/demo`423        // leaks the full API surface).424        healthDemoRoutes.register(app);425    }426    // Mutation-route gate factory. Non-strict mode is passthrough;427    // `{ strict: true }` requires a token even on loopback defaults.428    const mutate = createMutationGate({429        tokenConfigured,430        requireAuth: opts.requireAuth === true,431    });432    app.use(daemonTelemetryMiddleware(() => primaryBoundWorkspace, deps.recordDaemonRequest));433    const buildWorkspaceCtx = createBuildWorkspaceCtx(primaryBoundWorkspace);434    const acpHandleRef = {};435    const workspaceRememberLane = new WorkspaceRememberTaskLane(primaryBridge);436    // Plan C CDP tunnel (issue #5626): process-scoped registry pairing the437    // extension `/acp` connection with the `/cdp` puppeteer endpoint. Inert until438    // both ends connect (gated by `cdpTunnelOverWs`).439    const cdpTunnelRegistry = opts.cdpTunnelOverWs === true ? new CdpTunnelRegistry() : undefined;440    registerDaemonStatusRoutes(app, {441        opts,442        boundWorkspace: primaryBoundWorkspace,443        bridge: primaryBridge,444        workspace: primaryWorkspace,445        daemonLog,446        startup: deps.startup,447        qwenCodeVersion: deps.qwenCodeVersion,448        getAcpHandle: () => acpHandleRef.current,449        getRateLimiter: () => rateLimiter,450        getRestSseActive: getActiveSseCount,451        currentServeFeatures,452        getSupportedDeviceFlowProviders,453        deviceFlowRegistry,454        sessionShellCommandEnabled,455        getChannelWorkerSnapshot: deps.getChannelWorkerSnapshot,456        getPerfSnapshot: deps.getPerfSnapshot,457        getMetricsSeries: deps.getMetricsSeries,458        getTotalSessionAdmissionSnapshot: deps.getTotalSessionAdmissionSnapshot ?? totalSessionAdmission?.snapshot,459    });460    registerCapabilitiesRoutes(app, {461        qwenCodeVersion: deps.qwenCodeVersion,462        mode: opts.mode,463        currentServeFeatures,464        boundWorkspace: primaryBoundWorkspace,465        permissionPolicy: primaryBridge.permissionPolicy,466        maxPendingPromptsPerSession: opts.maxPendingPromptsPerSession,467        languageCodes,468    });469    registerWorkspaceStatusRoutes(app, {470        boundWorkspace: primaryBoundWorkspace,471        bridge: primaryBridge,472        workspace: primaryWorkspace,473        sendBridgeError,474    });475    // Workspace memory + agents CRUD routes.476    mountWorkspaceMemoryRoutes(app, {477        bridge: primaryBridge,478        boundWorkspace: primaryBoundWorkspace,479        mutate,480        parseClientId: parseClientIdHeader,481        safeBody,482    });483    mountWorkspaceMemoryRememberRoutes(app, {484        bridge: primaryBridge,485        lane: workspaceRememberLane,486        mutate,487        parseClientId: parseClientIdHeader,488        safeBody,489    });490    mountWorkspaceAgentsRoutes(app, {491        bridge: primaryBridge,492        boundWorkspace: primaryBoundWorkspace,493        mutate,494        parseClientId: parseClientIdHeader,495        safeBody,496    });497    registerWorkspaceDiagnosticStatusRoutes(app, {498        boundWorkspace: primaryBoundWorkspace,499        bridge: primaryBridge,500        workspace: primaryWorkspace,501        sendBridgeError,502    });503    registerWorkspaceExtensionRoutes(app, {504        boundWorkspace: primaryBoundWorkspace,505        bridge: primaryBridge,506        workspace: primaryWorkspace,507        mutate,508        safeBody,509        sendBridgeError,510        ...(deps.maxExtensionOperationHistory === undefined511            ? {}512            : { maxExtensionOperationHistory: deps.maxExtensionOperationHistory }),513    });514    // Workspace file routes (read-only + mutation).515    registerWorkspaceFileReadRoutes(app, {516        parseClientId: parseClientIdHeader,517    });518    registerWorkspaceFileWriteRoutes(app, {519        bridge: primaryBridge,520        mutate,521        parseClientId: parseClientIdHeader,522        safeBody,523    });524    registerWorkspaceSetupGithubRoutes(app, {525        boundWorkspace: primaryBoundWorkspace,526        bridge: primaryBridge,527        mutate,528        parseClientId: parseClientIdHeader,529        safeBody,530    });531    registerWorkspaceTrustRoutes(app, {532        boundWorkspace: primaryBoundWorkspace,533        workspace: primaryWorkspace,534        mutate,535        safeBody,536        parseAndValidateClientId: (req, res) => parseAndValidateWorkspaceClientId(req, res, primaryBridge),537    });538    const broadcastSettingsChanged = (key, value, scope, clientId) => {539        invalidateServeFeaturesCache();540        primaryBridge.publishWorkspaceEvent({541            type: 'settings_changed',542            data: { key, value, scope },543            ...(clientId ? { originatorClientId: clientId } : {}),544        });545    };546    if (deps.persistSetting) {547        const persistSetting = deps.persistSetting;548        registerWorkspaceSettingsRoutes(app, {549            boundWorkspace: primaryBoundWorkspace,550            mutate,551            safeBody,552            persistSetting: async (...args) => {553                await persistSetting(...args);554            },555            broadcastSettingsChanged,556            parseAndValidateClientId: (req, res) => parseAndValidateWorkspaceClientId(req, res, primaryBridge),557        });558    }559    registerWorkspacePermissionsRoutes(app, {560        boundWorkspace: primaryBoundWorkspace,561        mutate,562        safeBody,563        workspace: primaryWorkspace,564        parseAndValidateClientId: (req, res) => parseAndValidateWorkspaceClientId(req, res, primaryBridge),565    });566    registerWorkspaceVoiceRoutes(app, {567        boundWorkspace: primaryBoundWorkspace,568        mutate,569        safeBody,570        persistSetting: deps.persistSetting,571        persistSettings: deps.persistSettings,572        transcribe: deps.voiceTranscriber,573        broadcastSettingsChanged,574        parseAndValidateClientId: (req, res) => parseAndValidateWorkspaceClientId(req, res, primaryBridge),575    });576    // A2UI action inbound (the upstream half of A2UI-over-MCP): user577    // interactions from web clients are proxied to the UI MCP server's578    // standard `action` tool.579    registerA2uiActionRoutes(app, {580        boundWorkspace: primaryBoundWorkspace,581        mutate,582        safeBody,583        env: getRuntimeEffectiveEnv(primaryRuntime.env),584        // UI-server discovery uses the daemon's workspace MCP status, which585        // includes servers registered at runtime.586        getMcpServers: async () => {587            const ctx = buildWorkspaceCtx('POST /session/:id/a2ui-action');588            const status = await primaryWorkspace.getWorkspaceMcpStatus(ctx);589            return (status.servers ?? []);590        },591    });592    registerWorkspaceAuthRoutes(app, {593        mutate,594        deviceFlowRegistry,595        getSupportedDeviceFlowProviders,596        sendBridgeError,597        boundWorkspace: primaryBoundWorkspace,598        allowPrivateAuthBaseUrl: opts.allowPrivateAuthBaseUrl === true,599        installAuthProvider: deps.installAuthProvider,600    });601    registerSessionRoutes(app, {602        boundWorkspace: primaryBoundWorkspace,603        bridge: primaryBridge,604        archiveCoordinator,605        mutate,606        sendBridgeError,607        daemonLog,608        promptDeadlineMs: opts.promptDeadlineMs,609        sessionShellCommandEnabled,610        languageCodes,611    });612    registerWorkspaceMcpControlRoutes(app, {613        boundWorkspace: primaryBoundWorkspace,614        bridge: primaryBridge,615        workspace: primaryWorkspace,616        mutate,617        safeBody,618        sendBridgeError,619        parseAndValidateClientId: (req, res) => parseAndValidateWorkspaceClientId(req, res, primaryBridge),620    });621    registerWorkspaceLifecycleRoutes(app, {622        boundWorkspace: primaryBoundWorkspace,623        workspace: primaryWorkspace,624        mutate,625        safeBody,626        sendBridgeError,627        invalidateServeFeaturesCache,628        parseAndValidateClientId: (req, res) => parseAndValidateWorkspaceClientId(req, res, primaryBridge),629    });630    registerWorkspaceToolsRoutes(app, {631        boundWorkspace: primaryBoundWorkspace,632        workspace: primaryWorkspace,633        mutate,634        safeBody,635        sendBridgeError,636        parseAndValidateClientId: (req, res) => parseAndValidateWorkspaceClientId(req, res, primaryBridge),637    });638    // Durable scheduled-tasks CRUD (the Web Shell "Scheduled tasks" page).639    // Reads/writes the per-project cron file only; firing stays with the640    // session-side scheduler. Non-strict mutate: creating a scheduled prompt641    // is the same capability class as POST /session/:id/prompt.642    //643    // The bridge is passed ONLY when resident task-session management is enabled.644    // Binding a task to a dedicated session is only safe when something keeps that645    // session resident and reloads it after a restart (the keepalive + rehydration646    // below); without it, a bound task would fire only inside a session nothing647    // revives and silently go dormant. So embedders that leave the manager off648    // get UNBOUND tasks (shared-owner firing) instead.649    registerScheduledTasksRoutes(app, {650        boundWorkspace: primaryBoundWorkspace,651        mutate,652        safeBody,653        bridge: deps.manageScheduledTaskSessions ? bridge : undefined,654    });655    // Read-only token-usage dashboard (Daemon Status "统计" tab). Aggregate local656    // usage only; open GET like /daemon/status, with its own short TTL cache.657    registerUsageStatsRoutes(app);658    // Resident management of scheduled-task-owned sessions — opt-in, so tests and659    // embeds that call createServeApp neither spawn sessions on boot nor hold a660    // heartbeat timer (both would read the bound workspace's real tasks file).661    if (deps.manageScheduledTaskSessions) {662        // Keepalive: keep task sessions resident so their in-child schedulers keep663        // ticking rather than being idle-reaped, AND revive a re-enabled bound664        // session the reaper already let go. The revive loop is needed even when the665        // reaper is disabled (idle timeout ≤ 0), because archiving a task closes its666        // session — so this always runs when task sessions are managed, not only667        // when a reaper is active.668        const idleTimeoutMs = opts.sessionIdleTimeoutMs ?? DEFAULT_SESSION_IDLE_TIMEOUT_MS;669        const keepalive = startScheduledTaskKeepalive({670            bridge,671            boundWorkspace,672            intervalMs: computeKeepaliveIntervalMs(idleTimeoutMs),673        });674        // Park the stop fn on `app.locals` (same pattern as `fsFactory` /675        // `boundWorkspace` / `acpHandle` above) so the shutdown sequence in676        // run-qwen-serve.ts can invoke it without threading it back through the677        // createServeApp return type.678        app.locals.stopScheduledTaskKeepalive = keepalive.stop;679        // Rehydrate task-owned sessions on boot so their schedulers re-arm after a680        // restart (a bound task fires only in its own session, which nothing else681        // reloads). Fire-and-forget so it never delays the server coming up; a682        // no-op when there are no bound tasks. Deliberately not awaited.683        void rehydrateScheduledTaskSessions({684            bridge,685            boundWorkspace,686            onError: (sessionId, err) => {687                process.stderr.write(`qwen serve: failed to rehydrate scheduled-task session ${sessionId}: ${err instanceof Error ? err.message : String(err)}\n`);688            },689            // Outer catch is defense-in-depth: rehydrateScheduledTaskSessions already690            // catches readCronTasks failures and per-session load errors internally691            // (returning { loaded, failed }), so this only guards an unexpected throw692            // from the function entry itself. Log rather than swallow it — a silent693            // failure here leaves every bound task dormant with no diagnostic.694        }).catch((err) => {695            process.stderr.write(`qwen serve: unexpected scheduled-task rehydration failure: ${err instanceof Error ? err.message : String(err)}\n`);696        });697    }698    registerPermissionRoutes(app, {699        bridge: primaryBridge,700        mutate,701        sendPermissionVoteError,702    });703    registerSseEventsRoutes(app, {704        bridge: primaryBridge,705        daemonLog,706        writerIdleTimeoutMs: opts.writerIdleTimeoutMs,707        sendBridgeError,708    });709    // Official ACP Streamable HTTP transport (RFD #721) mounted at `/acp`710    // alongside the REST surface, sharing this same `bridge` instance.711    // Additive + toggleable (`QWEN_SERVE_ACP_HTTP=0` opts out).712    // See `docs/design/daemon-acp-http/README.md` for the dual-transport713    // decision. Mounted AFTER the REST routes (distinct path, no overlap)714    // and BEFORE the final error handler so malformed `/acp` bodies still715    // route through the JSON error contract below.716    acpHandleRef.current = mountAcpHttp(app, primaryBridge, {717        boundWorkspace: primaryBoundWorkspace,718        archiveCoordinator,719        workspace: primaryWorkspace,720        fsFactory: primaryRouteFileSystemFactory,721        deviceFlowRegistry,722        token: opts.token,723        // Mirror the REST CORS allowlist onto the WS CSRF wall so an724        // explicitly permitted origin (e.g. the extension's725        // `chrome-extension://<id>`) can open the reverse tool channel.726        allowedOrigins: opts.allowOrigins && opts.allowOrigins.length > 0727            ? parseAllowOriginPatterns(opts.allowOrigins)728            : undefined,729        hostname: opts.hostname,730        sessionShellCommandEnabled,731        workspaceRememberLane,732        checkRate: rateLimiter?.checkRate,733        clientMcpOverWs: opts.clientMcpOverWs === true,734        // Reverse tool channel (issue #5626, Phase 2). Per-connection provider:735        // on `mcp_register` it records the WS registrar's sender in the shared736        // registry and adds an SDK-type runtime MCP server in the ACP child737        // (originator = the connection id). Only meaningful when738        // `clientMcpOverWs` is on; the WS layer never builds a provider otherwise.739        ...(opts.clientMcpOverWs === true740            ? {741                clientMcpProviderFactory: (connectionId) => createClientMcpServerProvider(primaryRuntime.clientMcpSenderRegistry, primaryBridge, connectionId),742            }743            : {}),744        // Plan C CDP tunnel (issue #5626): the `/cdp` branch + `cdp_*` routing745        // activate only when the flag is on and a registry is supplied.746        cdpTunnelOverWs: opts.cdpTunnelOverWs === true,747        ...(cdpTunnelRegistry ? { cdpTunnelRegistry } : {}),748        // Browser captures audio and streams raw PCM here; the daemon transcribes749        // server-side via the reused CLI voice pipeline. Shares the ACP upgrade750        // listener's loopback/CSRF/bearer checks.751        extraWsRoutes: [752            {753                path: '/voice/stream',754                onConnection: createVoiceWsConnectionHandler(primaryBoundWorkspace, {755                    env: getRuntimeEffectiveEnv(primaryRuntime.env),756                }),757            },758        ],759    });760    if (acpHandleRef.current) {761        app.locals['acpHandle'] = acpHandleRef.current;762    }763    // Web Shell SPA deep-link fallback — registered AFTER every API route (and764    // just before the error handler) so real routes, including their bearerAuth765    // 401s, always win; only genuine 404 misses fall through to the shell. This766    // is what keeps an attacker-controlled `Accept: text/html` from coaxing the767    // 200 shell out of an authed route.768    if (webShellDir) {769        mountWebShellSpaFallback(app, webShellDir, webShellFrameAncestors);770    }771    installFinalErrorHandler(app);772    if (rateLimiter) {773        setRateLimiter(app, rateLimiter);774    }775    return app;776}777//# sourceMappingURL=server.js.map
basant307/AI_Governance_Project · CoolFace