/** * Per-MCP-session context. * * Cortex's MCP server serves many concurrent clients (each Claude * Code instance, Claude Desktop, the browser extension — all share * the same server process). A single "active workspace" tracked at * process level fails that model: Claude A in workspace alpha and * Claude B in workspace beta need independent views of * memory, taxonomy, and adapter state. * * Solution: AsyncLocalStorage carries the session id across every * MCP request, and a Map holds per-session * preferences. `transport.ts` binds the session id at the HTTP * boundary; every downstream tool handler reads it via * `getCurrentSessionId()`. * * Session id source: the `mcp-session-id` header the streamable HTTP * transport already maintains. When missing (initial handshake, or a * non-streamable client), a fresh UUID is minted and tracked for the * duration of that request — benign because the session state is * empty anyway. */ export interface SessionState { /** * Workspace the session is scoped to. * undefined = session never picked one (fall back to the CLI-side * active pointer — backwards compat for old clients). * null = user explicitly picked "no workspace" mode. * string = bound to that workspace slug. */ workspace: string | null | undefined; /** When the session was first seen. */ firstSeenAt: number; /** Last time we received a tool call on this session. */ lastSeenAt: number; /** * Per-session env bag loaded from the bound workspace's .env at bind * time. Tools that need workspace-scoped secrets should call * `getSessionEnvVar(name)` instead of reading process.env directly — * that way two sessions bound to two workspaces don't see each * other's secrets. Not persisted to disk (it's reloadable from the * workspace's .env). */ envBag?: Map; /** * Resolved tool surface for this session based on the bearer's * scope claims. `undefined` means no scope token was presented — * the legacy opaque-bearer / gateway-secret / cookie path; the * session sees the full ALL_TOOLS surface (backwards compat with * direct API + dashboard usage). A populated set means the bearer * was a cscope JWT and the session is restricted to those names. */ toolAllowList?: Set; /** * Dashboard auth — scopes granted to a browser session that logged * in via `cortex dashboard create-token`. Presence of this field is * the signal that the session id is a `dash_` cookie session; * `requireDashboardAuth` keys gating off it. Absent = not a logged-in * dashboard session (either a plain MCP session, or the cookie was * cleared by logout). */ dashboardScopes?: ReadonlyArray<"read" | "ingest" | "admin">; /** * Normalized label of the token that authenticated this session. * Echoed back from `/api/dashboard/auth/whoami` so the operator can * tell which device they're looking at. */ dashboardTokenLabel?: string; /** * GitHub OAuth identity bound to this dashboard session. Populated by * the GitHub device-flow sign-in path (`/api/dashboard/auth/github/*`). * Absent for sessions minted from raw token paste — `tokenLabel` is the * counterpart there. Both paths share `dashboardScopes`. */ githubLogin?: string; githubUserId?: number; githubAvatarUrl?: string; /** * The raw GitHub access token granted by the device flow. Kept in * SessionState so the Slice B repos API can call GitHub on behalf of * the logged-in user without re-prompting. Persisted to cache_sessions * by the storage layer — operators are expected to keep that SQLite * file out of backups, same as cache_jobs. */ githubAccessToken?: string; } /** * Persistent shadow for dashboard sessions. Set at boot via * `setSessionsStorage` from the MCP server; absent in tests that don't * care about restart persistence. Reads consult the in-memory map first * and fall back to storage; writes go through both. * * The legacy JSON-file persistence (`sessionStatePath`) is independent * — it covers workspace bindings for MCP sessions. SQLite is for the * dashboard auth bindings (cookie → identity) so a server restart * doesn't kick every browser back to the login screen. */ interface PersistentSessionRow { sessionId: string; workspace: string; scopesJson: string; tokenLabel: string | null; githubLogin: string | null; githubUserId: number | null; githubAvatarUrl: string | null; githubAccessToken: string | null; createdAtMs: number; expiresAtMs: number; lastSeenAtMs: number; } export interface SessionsStorage { upsert(row: PersistentSessionRow): void; get(sessionId: string): PersistentSessionRow | null; evict(sessionId: string): boolean; list(): PersistentSessionRow[]; cleanup(nowMs?: number): number; close(): void; } /** * Wire a persistent storage instance. Idempotent — calling with the same * (or a different) storage replaces the global; existing in-memory * sessions are not re-hydrated from the new storage automatically. Call * once at server boot, before the dashboard API starts accepting * traffic. Pass `undefined` to detach (tests do this on teardown). */ export declare function setSessionsStorage(storage: SessionsStorage | undefined): void; /** * Called once at server startup. Rehydrates sessionStates from the * persisted file so clients reconnecting after a restart keep their * workspace binding. Sessions older than `maxAgeMs` are dropped. */ export declare function restoreSessionStates(maxAgeMs?: number): Promise; /** * Extract the MCP session id from an incoming HTTP request. Falls * back to a minted UUID when the header isn't present — still lets * tools run, just without persistent session state across requests. */ export declare function extractSessionId(headers: Record): string; /** * Run `fn` inside an ALS context bound to this session id. Called * from the HTTP upgrade/handleRequest wrapper in transport.ts. */ export declare function runWithSession(sessionId: string, fn: () => T): T; /** * Persist `sessionId` as the ALS context for the remainder of this * async execution and all awaited continuations. For stdio transport * where there's one client for the whole process lifetime — we can't * wrap MCP tool handlers in `runWithSession` because the SDK owns the * read loop, so we bind once at startup. * * Don't call this from the HTTP path: it leaks context across * concurrent requests. */ export declare function enterSession(sessionId: string): void; /** Current session id, or undefined when outside an ALS context. */ export declare function getCurrentSessionId(): string | undefined; /** State for the current session, or undefined when outside a context. */ export declare function getCurrentSessionState(): SessionState | undefined; /** Explicit lookup — used by tools that want the id directly. */ export declare function getSessionState(sessionId: string): SessionState | undefined; /** Set the workspace for a session. `null` = "no workspace" mode. */ export declare function setSessionWorkspace(sessionId: string, workspace: string | null): SessionState; /** * Workspace-scoped env accessor. Reads the current session's envBag * first, falling back to `process.env` so tools unaware of session * scoping still work. Tools that handle workspace-specific secrets * (API keys, etc.) should prefer this over `process.env[name]`. */ export declare function getSessionEnvVar(name: string): string | undefined; /** Read the workspace for the current ALS-bound session. */ export declare function getCurrentWorkspace(): string | null | undefined; /** * Stamp the tool allow-list onto a session. Called by the transport's * authOk after verifying a scope-bearing JWT. `undefined` clears the * restriction (legacy opaque bearer or other unscoped credential). */ export declare function setSessionToolAllowList(sessionId: string, allowList: Set | undefined): void; /** Tool allow-list for the current ALS-bound session. */ export declare function getCurrentToolAllowList(): Set | undefined; /** Count of distinct sessions seen — useful for /api/status. */ export declare function sessionCount(): number; /** * Mint or update a dashboard browser session. Stamped onto sessionStates * keyed by the `dash_` cookie value. Distinct entry point from * MCP-session lifecycle helpers because dashboard sessions never go * through the ALS-bound MCP path — they live entirely in the HTTP API * gate. */ export declare function setDashboardSession(sessionId: string, opts: { workspace: string | null; scopes: ReadonlyArray<"read" | "ingest" | "admin">; tokenLabel: string; }): SessionState; /** * Mint or update a dashboard session that authenticated via the GitHub * Device Flow. Parallel entry point to `setDashboardSession` for the * raw-token-paste path — both bind into the same sessionStates map and * share the dashboardScopes contract, but the github fields here * substitute for `tokenLabel` so `whoami` can render the user's avatar * + login instead of an arbitrary device name. * * Scopes default to `["admin"]` because the device flow is the sign-in * primitive for the dashboard owner — there's no separate "read-only * GitHub user" persona. If a future scoped flow lands, callers will * pass `scopes` explicitly. */ export declare function setGitHubSession(sessionId: string, opts: { workspace: string | null; githubLogin: string; githubUserId: number; githubAvatarUrl: string | null; githubAccessToken: string; scopes?: ReadonlyArray<"read" | "ingest" | "admin">; }): SessionState; /** * Drop a dashboard session — called from `/api/dashboard/auth/logout`. * Removes the session entirely; the cookie on the browser side is * cleared by the same handler. */ export declare function evictDashboardSession(sessionId: string): boolean; /** * Garbage collect sessions last seen more than `olderThanMs` ago. * Called periodically by the server to keep the map bounded when * clients come and go. Sessions are cheap but not free. */ export declare function evictStaleSessions(olderThanMs: number): number; export {}; //# sourceMappingURL=session-context.d.ts.map