/** * DshRoleSwitchWebRoute — structural adapter exposing the dsh role * switcher's REST surface as a single `prefix` route on dsh's host web * server (`@deepseek-ai/dsh-host-webserver`). * * dsh's host webserver exposes `ctx.webServer.register(route)` where * * `WebRoute = { kind: 'exact'|'prefix', path: string, handler: * (req: IncomingMessage, res: ServerResponse) => void | Promise }` * * and `register` returns a disposer. This module consumes that surface * structurally (duck typing) — it does NOT import `@deepseek-ai/*` and never * creates an HTTP server of its own. * * Registered route: * * - `{ kind: 'prefix', path: '/rolebox', handler }` * * REST contract under the prefix (migrated 1:1 from the loopback server, * paths renamed `/api/*` → `/rolebox/*`): * * - `GET /rolebox/roles` — JSON array of switchable roles * (`id`/`name`/`description`/`model`/ * `mode`, primary roles only). * - `GET /rolebox/roles/active` — `{ session, role }` — the active * role id for the session, or `null` * for the base agent. * - `POST /rolebox/roles/switch` — body `{ role: string, session?: * string }`; delegates to * {@link DshRoleSwitcher.activate}. * - `DELETE /rolebox/roles/active` — clear the active role for the * session. * - `POST /rolebox/reload` — in-process, NON-DESTRUCTIVE role * reload (re-discovery + re-resolution); * delegates to the optional * {@link DshRoleSwitchRouteOptions.reload} * callback. `200` with * `{ ok, discovered, resolved, skipped }` * on success, `409` when the reloader is * disabled/busy/failed, `404` when no * callback is wired. * * The `session` key is optional everywhere: an explicit session (body or * `?session=` query) wins; otherwise the most recently active session in the * SessionStore is used; with no sessions at all the literal key `"default"` * is used (the same fallback key the standalone loopback server used). * * Error contract — identical to the loopback server: every non-2xx response * is JSON with the stable shape `{ "ok": false, "error": string }`. Status * codes: `400` (malformed JSON, missing `role`, unknown or non-primary * role), `404` (unknown route, or `/reload` with no callback wired), `405` * (known path, wrong method), `409` (reload disabled/busy/failed — the * disabled body additionally carries `"disabled": true`), `413` (request * body over the cap), `500` (unexpected failure). 2xx responses are * resource-shaped: the roles list is a bare array, `active` is * `{ session, role }`, mutations are `{ ok: true, session, role }`, and * `/reload` is `{ ok: true, discovered, resolved, skipped }`. * * Operational notes: * - Request bodies are capped (default 64 KiB) — oversized bodies are * drained and answered `413`, never buffered. * - The handler never rejects: every branch is guarded so a failing * handler yields a stable `500` JSON error instead of a bare socket * teardown. * - The standalone loopback server module was removed; the DTO types are * defined here so the route surface stays self-contained. * * @module */ import type { IncomingMessage, ServerResponse } from "node:http"; import type { DshRoleSwitcher } from "./role-switcher.ts"; import type { DshRoleboxReloadResult } from "./rolebox-reload.ts"; import type { DshSessionStoreLike } from "./session.ts"; /** * Serialized switchable role — the `GET /rolebox/roles` list item shape. * All five keys are always present; `model` / `mode` are `null` when the * definition carries no override (they are optional on * {@link AgentDefinition}). */ export interface RoleSwitchRoleDto { id: string; name: string; description: string; model: string | null; mode: string | null; } /** Stable error shape for every non-2xx JSON response. */ export interface RoleSwitchErrorBody { ok: false; error: string; } /** Route prefix registered on the dsh host web server. */ export declare const ROLE_SWITCH_ROUTE_PREFIX = "/rolebox"; /** * Options for constructing a {@link DshRoleSwitchWebRoute}. */ export interface DshRoleSwitchRouteOptions { /** Maximum request body size in bytes (default 65536). */ maxBodyBytes?: number; /** Optional sub-logger name override (default `"dsh-role-switch-route"`). */ loggerName?: string; /** * Optional `POST /rolebox/reload` callback — the in-process, * non-destructive role reloader (`DshRoleboxReloader.reload` in * `rolebox-reload.ts`, the dsh substitute for the opencode-only * HotReloadService). When omitted, the sub-path is not served and the * route answers `404`, so the route never exposes a reload the plugin did * not explicitly wire. */ reload?: () => Promise; } /** * Structural `WebRoute` from `@deepseek-ai/dsh-host-webserver` — consumed by * duck typing, never imported from the package. */ export interface DshWebRouteLike { kind: "exact" | "prefix"; path: string; handler(req: IncomingMessage, res: ServerResponse): void | Promise; } /** * Structural `ctx.webServer` registrar surface from * `@deepseek-ai/dsh-host-webserver` — `register(route)` returns a disposer. */ export interface DshWebServerRouteRegistrar { register(route: DshWebRouteLike): () => void; } /** * Route adapter exposing the dsh role switcher on the host web server. * * Construct with the switcher and the session store, then * `register(webServer)` to mount the `/rolebox` prefix route; the returned * disposer unmounts it. The handler is also exposed directly as * {@link DshRoleSwitchWebRoute.handle} for tests and non-HTTP callers. */ export declare class DshRoleSwitchWebRoute { private readonly switcher; private readonly store; private readonly maxBodyBytes; private readonly reload; private readonly _log; /** * @param switcher - The dsh role switcher this route delegates to. * @param store - The dsh SessionStore used to resolve the most recent * session when no explicit session is supplied. * @param options - Optional tuning (body cap / logger name). */ constructor(switcher: DshRoleSwitcher, store: DshSessionStoreLike, options?: DshRoleSwitchRouteOptions); /** * Register the `/rolebox` prefix route on the host web server. * * @param webServer - The duck-typed `ctx.webServer` registrar. * @returns The disposer returned by `webServer.register(...)` — call it to * unmount the route. */ register(webServer: DshWebServerRouteRegistrar): () => void; /** * Dispatch a request under the `/rolebox` prefix to its route handler. * * Requests whose path does not start with the prefix are answered `404` * (the host webserver should only forward prefix-matching requests, but * the guard keeps the handler self-contained). Every branch is wrapped in * a try/catch so a failing handler always yields a stable `500` JSON error * instead of a bare socket teardown. */ handle(req: IncomingMessage, res: ServerResponse): Promise; /** `GET /rolebox/roles` — the switchable roles as a bare JSON array. */ private serveRoles; /** `GET /rolebox/roles/active` — `{ session, role }` (role may be null). */ private serveActive; /** * `POST /rolebox/roles/switch` — body `{ role, session? }`. Unknown or * non-primary roles are rejected with `400` (delegated to the switcher). */ private serveSwitch; /** `DELETE /rolebox/roles/active` — clear the active role back to base agent. */ private serveClear; /** * `POST /rolebox/reload` — in-process, NON-DESTRUCTIVE role reload through * the optional {@link DshRoleSwitchRouteOptions.reload} callback. * * Contract: `200 { ok: true, discovered, resolved, skipped }` on success; * `409 { ok: false, disabled: true, error }` when the reloader is * kill-switched; `409 { ok: false, error }` when the reload is busy or * failed; `404` when no callback is wired. A callback that throws is * caught HERE (the `handle` try/catch cannot see a rejection adopted by a * `return ` branch) and answered `500`. */ private serveReload; /** * Resolve the session a request applies to: an explicit session (body / * query) wins; otherwise the most recently active session in the * SessionStore; with no sessions at all, the literal * {@link DEFAULT_SESSION_KEY} (`"default"`) key. */ private resolveSessionId; /** * Read the request body up to {@link maxBodyBytes}. Oversized bodies are * drained and discarded (never buffered) and reported via `tooLarge` so the * caller can answer `413` after the request completes cleanly. */ private readBody; } //# sourceMappingURL=web-role-switch-route.d.ts.map