/** * openlore serve — local HTTP daemon (warm, loopback-only). * * A long-lived process that keeps openlore's caches warm across calls and * exposes the tool surface over plain HTTP so non-MCP clients (e.g. a Pi * extension) can hit it with `fetch` — no JSON-RPC, no subprocess-per-call. * * It reuses the SAME tool dispatch as the stdio MCP server * ({@link dispatchTool}) so the two transports can't drift, and the SAME tool * presets ({@link selectActiveTools}) so a small-model client gets a focused * surface. The default preset is the shared `LEAN_DEFAULT_PRESET` constant (so * `serve` and `openlore mcp` never diverge on what "no --preset" means). * * Endpoints (all loopback): * GET /health → { ok, presetDispatchEnforced, root, pid, preset, tools, * tokenProtected, tokenAuthenticated, version, uptimeMs } * POST /shutdown → authenticated graceful teardown * POST /tool/:name body { directory?, args } → handler result (JSON) * * Clients require the semantic marker, exact root, and authenticated token proof * before trusting a descriptor's daemon, preset, or tool list. * Discovery: writes `.openlore/serve.json` { port, pid, host, token?, startedAt } * in the served root so a client can find and reuse a running daemon. * * Security: defaults to 127.0.0.1. Every request is checked against a DNS-rebinding * guard (Host must be a loopback name or the bound host; a cross-site Origin is * rejected) before any dispatch. An optional --token must be presented as the * `x-openlore-token` header and is compared in constant time; binding a non-loopback * host requires a token (the daemon refuses to start otherwise), and a tokenless * loopback bind warns that other local processes can reach the port. * * Freshness (watcher + continuous re-analyze) is layered on separately; this * module is the transport + lifecycle core. */ import { Command } from 'commander'; import { type ServeDescriptor } from './serve-descriptor.js'; /** * Root watcher → full-repair debounce used by the serve daemon. Exported so the * equivalence gate can drive the exact production coordination primitive without * opening an HTTP socket. The callback remains the authority for singleflight and * publication; this class owns only quiet-window coalescing. */ export declare class ServeWatchRepairCoordinator { private readonly repair; private readonly debounceMs; private timer?; constructor(repair: () => void, debounceMs?: number); schedule(): void; cancel(): void; } /** Wait for active rebuilds without letting shutdown hang forever. */ export declare function drainServeRebuilds(rebuilds: Iterable>, timeoutMs?: number): Promise; /** * Resolve the idle-shutdown interval (ms) from the `--idle-timeout` option, in * minutes. Absent or non-numeric → the default; zero/negative → 0 (disabled). */ export declare function idleTimeoutMs(option?: string): number; interface ServeCliOptions { directory?: string; port?: string; host?: string; preset?: string; token?: string; stop?: boolean; /** false (via --no-watch) disables the freshness watcher + re-analyze lane. */ watch?: boolean; /** Minutes of request inactivity before the daemon self-terminates. 0 disables. */ idleTimeout?: string; /** Internal test seam; the CLI always uses the production startup-lock bound. */ startupLockWaitMs?: number; /** Internal test seam for legacy unauthenticated transport cases. */ allowUnauthenticatedForTesting?: boolean; } /** Live daemon handle. Returned by {@link startServe} so callers (tests) can * address and shut down the running server without signalling the process. */ export interface ServeHandle { port: number; host: string; token?: string; baseUrl: string; /** * True when THIS process started the server the handle addresses, so `close()` stops it. * False for a handle onto a daemon someone else started: `close()` then merely detaches * (change: extend-api-for-supervising-hosts). A supervising host that closed a handle and * believed it had released the daemon would otherwise leak a live process per workspace, so a * handle whose `close()` does not stop a server must never look like one that does. */ owned: boolean; close(): Promise; } /** Why {@link runServe} declined to start. The CLI logs these; the API throws them. */ export type ServeRefusalCode = 'non-loopback-without-token' | 'non-loopback-discovery-host' | 'unknown-preset' | 'startup-lock-unavailable' | 'startup-lock-timeout' | 'incompatible-daemon-announced' | 'daemon-draining' | 'token-posture-mismatch' | 'unverified-daemon-health' | 'preset-posture-mismatch' | 'descriptor-publish-failed' | 'bind-failed'; /** * The outcome of a serve attempt, as a value. * * `runServe` never writes to the console and never sets `process.exitCode`: every exit path is one * of these variants. `startServe` is the CLI adapter that renders them back into today's log lines * and exit codes; `openloreServe` maps `refused` to a thrown error. A library that mutated the exit * code of a process it does not own could not honour the API contract, and a partial extraction — * only the three static refusals — would still have logged and exited on the nine runtime paths * below before it ever got to throw (change: extend-api-for-supervising-hosts). */ export type ServeOutcome = { kind: 'started'; handle: ServeHandle; } | { kind: 'reusing'; handle: ServeHandle; } | { kind: 'stopped'; stopped: boolean; } | { kind: 'no-daemon'; message: string; } | { kind: 'refused'; code: ServeRefusalCode; message: string; }; /** * Read + validate /.openlore/serve.json. The discovery file is an untrusted * on-disk artifact (mcp-security: Untrusted Artifact Deserialization): a hostile repo * could ship a poisoned serve.json, and `probeDaemon` would then fetch an arbitrary * host (egress / SSRF). Validation * lives in the shared {@link readServeDescriptor} so every reader fails closed the * same way (mcp-security: ServeDescriptorValidatedAtEveryReader). * * Exported for the serve.json validation tests. */ export declare function readDescriptor(root: string): Promise; /** * The serve startup core. Returns its outcome as a value — it never calls `logger.error` and never * writes `process.exitCode`, on any path (change: extend-api-for-supervising-hosts). Operational * logging a running daemon does (warnings, discovery lines) stays: a caller that needs silence * passes `quiet` and gets it from the logger, not from a second copy of this function. */ declare function runServe(options: ServeCliOptions): Promise; /** * CLI adapter over {@link runServe}: renders each outcome into the log line and exit code the * `openlore serve` command has always produced. Every `process.exitCode` write for a serve start * lives here, so the core stays embeddable (`openloreServe`) without a second copy of the startup * rules — duplicating the loopback/token refusal in an API wrapper would be a second security * posture for one rule, which is the mistake this change exists to stop making. */ export declare function startServe(options: ServeCliOptions): Promise; /** The startup core, for in-process callers that must not have their exit code mutated. */ export { runServe }; export declare const serveCommand: Command; //# sourceMappingURL=serve.d.ts.map