/** * Per-request context using AsyncLocalStorage. * * Replaces the unsafe pattern of mutating `process.env.AGENT_USER_EMAIL` / * `process.env.AGENT_ORG_ID` on every request. On Node.js (Netlify, self-hosted) * concurrent requests would overwrite each other's env vars. AsyncLocalStorage * gives each async call-chain its own isolated context. * * Supported on all deployment targets: * - Node.js (native) * - Cloudflare Workers (via nodejs_compat flag) * - Deno Deploy (via node:async_hooks compat) * * For CLI scripts that run outside a request context, the getters fall back to * process.env so existing `AGENT_USER_EMAIL=x pnpm action foo` invocations * continue to work. */ /** * Per-request agent-run state. Lives on `RequestContext.run` so the * agent-chat plugin can populate fields as the run progresses (owner, * resolved API key, system prompt, engine, model, threadId) without * mutating module-scope `let` bindings — those leak across concurrent * requests on a single Node.js process. * * Mutated in-place by `prepareRun`, `onEngineResolved`, `onRunStart` so * tool factory closures (automation, fetch, team, builder-browser) read * the live per-request value via `getRequestRunContext()`. */ export interface RequestRunContext { /** Request-scoped serverless continuation hook, when the runtime provides one. */ waitUntil?: (promise: Promise) => void; /** Origin of the current request (used by the builder-browser tool). */ requestOrigin?: string; /** Stable browser tab id for tab-scoped app-state reads/writes. */ browserTabId?: string; /** Resource scope for the current chat thread, e.g. the active deck. */ chatScope?: { type: string; id: string; label?: string; } | null; /** Resolved owner email (set by prepareRun). */ owner?: string; /** Owner's active Anthropic API key (set by prepareRun). */ userApiKey?: string; /** Thread ID for the current run (set by onRunStart). */ threadId?: string; /** System prompt actually sent to the model for this run. */ systemPrompt?: string; /** Engine instance for this run (set by onEngineResolved). */ engine?: import("../agent/engine/types.js").AgentEngine; /** Model name for this run (set by onEngineResolved). */ model?: string; /** * True when this run is executing inside the durable background-function * worker (the `_process-run` self-dispatch), not the synchronous foreground * request. Template `extraContext` / system-prompt builders can read this to * skip heavy, hang-prone enrichment (large data-dictionary DB reads, etc.) * in the worker so it reliably claims its run within the setup budget. */ isBackgroundWorker?: boolean; /** Tool calls made so far in the current agent loop. */ toolCalls?: Array<{ name: string; input: unknown; }>; /** Tool results returned so far in the current agent loop. */ toolResults?: Array<{ name: string; content: string; isError: boolean; }>; /** Per-run fingerprints for large extension bodies already sent to the LLM. */ extensionContentReads?: Record; /** Per-run fingerprints for repeated tool-search calls already sent to the LLM. */ toolSearchReads?: Record; } export interface RequestContext { userEmail?: string; userName?: string; orgId?: string; timezone?: string; /** * Set when code reads authenticated request context. Public SSR shell/data * should not depend on this value; user/org-specific reads belong behind * client-side actions/API after hydration. */ authContextAccessed?: boolean; /** * Origin of the inbound request (e.g. `http://127.0.0.1:8100`). Set by the * MCP mount from the request headers so actions that build externally * fetchable URLs (e.g. design `export-coding-handoff`'s signed raw-code URL) * resolve the real local-workspace origin instead of a prod/localhost * fallback. Optional — absent on paths that don't populate it. */ requestOrigin?: string; /** * True when this request is being processed by an integration-platform * webhook (Slack, Telegram, etc.) where the function timeout is the * binding constraint. Code that calls slow remote APIs can use this to apply * tighter budgets on this path while leaving normal agent-chat callers * (5+ min budget) unaffected. */ isIntegrationCaller?: boolean; /** * Metadata for the currently-processing integration task. This lets tools * that start long-running remote work persist a continuation that can update * the originating platform thread after the current function budget ends. */ integration?: { taskId: string; attempts?: number; incoming: import("../integrations/types.js").IncomingMessage; placeholderRef?: string; /** Opaque provider-native progress surface for a durable continuation. */ progressRef?: import("../integrations/types.js").PlatformRunProgressRef; installationId?: string; scopeId?: string; principalType?: "user" | "service"; lineage?: { runId?: string; parentTaskId?: string; source?: { kind: string; platform?: string; id: string; url?: string; }; network?: { protocol: "a2a" | "mcp" | "provider-api"; id: string; peer?: string; }; }; }; /** * Mutable per-request agent-run state. Populated by the agent-chat plugin * during a run; tool closures dereference it on each invocation. */ run?: RequestRunContext; } type RequestContextObserver = (ctx: RequestContext) => void; /** * Register a callback fired every time `runWithRequestContext` enters a new * scope. The hook runs INSIDE the AsyncLocalStorage scope, so observability * helpers that read the current isolation scope (e.g. Sentry) attach to the * right per-request context. * * Returned function unregisters the observer. Observers must never throw — * any error is swallowed so a misbehaving observer can't break the request * path. */ export declare function addRequestContextObserver(observer: RequestContextObserver): () => void; /** * Run a callback within a per-request context. The context is available to all * async operations spawned from `fn` via `getRequestUserEmail()` / `getRequestOrgId()`. * * Any registered `addRequestContextObserver` callbacks fire inside the new * scope before `fn` runs, so observability code can pin user/org info onto * isolation-scoped backends (Sentry, OpenTelemetry, etc.). */ export declare function runWithRequestContext(ctx: RequestContext, fn: () => T | Promise): T | Promise; /** * Return the active request context, if this call chain is running under one. * * This is intentionally distinct from `getRequestUserEmail()`: callers that * have an active context with no authenticated user must not fall through to * process-wide CLI fallbacks such as `AGENT_USER_EMAIL` or "latest session". */ export declare function getRequestContext(): RequestContext | undefined; /** * True when AsyncLocalStorage has an active context for this call chain. * Useful for helpers that support both HTTP requests and standalone CLI runs. */ export declare function hasRequestContext(): boolean; /** * Record that the framework's request-boundary middleware is installed in this * process, so every inbound HTTP request runs inside a `RequestContext`. * * Once that is true, a request-scoped identity read that finds no store can no * longer be an HTTP caller — which is what makes the ambient-identity warning * in `getRequestUserEmail()` specific enough to be worth emitting. */ export declare function markRequestBoundaryInstalled(): void; export declare function hasRequestBoundary(): boolean; /** * The ambient, process-wide identity configured for this deployment * (`AGENT_USER_EMAIL`). Legitimate callers are the ones with no request behind * them at all: CLI invocations, cron/scheduled jobs, seed and QA scripts. * * TRAP: this is not the caller's identity, and a request handler that reads it * authorizes whoever the deploy env names rather than whoever signed in — it * fails open toward more privilege. Request handlers read * `getRequestUserEmail()` and fail closed when it returns undefined. */ export declare function getAmbientUserEmail(): string | undefined; /** Ambient process-wide org (`AGENT_ORG_ID`). Same trap as `getAmbientUserEmail()`. */ export declare function getAmbientOrgId(): string | undefined; /** * Get the current request's user email. * * - If a request context exists (HTTP/A2A path), returns its `userEmail` — * even when that value is `undefined`. The env fallback MUST NOT fire here: * a stale process-wide `AGENT_USER_EMAIL` from a CLI run or previous bug * would leak into an unauthenticated A2A/API call (e.g. unsigned or API-key * modes where `runWithRequestContext({ userEmail: undefined })` is used). * - Only when there is NO request context (CLI scripts) do we fall back to * `process.env.AGENT_USER_EMAIL`. In a process that serves HTTP requests the * framework installs a request boundary so that case cannot be a request; * if it happens anyway we warn loudly rather than answer silently. */ export declare function getRequestUserEmail(): string | undefined; /** * Get the current request's display name, when the auth provider supplied one. * * The same request-context fallback rules as `getRequestUserEmail()` apply: * HTTP/A2A calls only read AsyncLocalStorage, while CLI scripts may opt in via * `AGENT_USER_NAME`. */ export declare function getRequestUserName(): string | undefined; /** * Get the current request's org ID. * * Same store-aware semantics as `getRequestUserEmail()` — env fallback is * CLI-only, so a request that explicitly has no org doesn't inherit a stale * `process.env.AGENT_ORG_ID` from a prior request on the same Lambda instance. */ export declare function getRequestOrgId(): string | undefined; export declare function hasAuthContextAccess(ctx: RequestContext | undefined): boolean; /** * Get the current request's IANA timezone (e.g. "America/Los_Angeles"). * The UI sends this via the `x-user-timezone` header on every action call, and * the agent chat plugin propagates it into the request context so that * agent-initiated tool calls also see the user's timezone. Falls back to * `process.env.AGENT_USER_TIMEZONE` only for CLI scripts (no request context). */ export declare function getRequestTimezone(): string | undefined; /** * Returns true when this request is on an integration-platform path (Slack, * Telegram, etc.) — i.e. we're inside the integration plugin's processor * function and the platform's deliver-by deadline plus the host's function * timeout are the binding budget. Non-integration callers (CLI, normal * agent chat) should treat this as `false`. */ export declare function isIntegrationCallerRequest(): boolean; export declare function getIntegrationRequestContext(): NonNullable | undefined; /** * Convenience: returns `{ userEmail, orgId }` from the active request context, * suitable for passing to `resolveCredential(key, ctx)`. Returns `null` when * no user is associated with the call (e.g. an unauthenticated public route). * * For framework actions auto-mounted at `/_agent-native/actions/...` this is * always populated because action-routes wraps every invocation in * `runWithRequestContext`. For hand-written `/api/*` routes the calling code * is responsible for setting up the context (see `runWithRequestContext`). */ export declare function getCredentialContext(): { userEmail: string; orgId: string | null; } | null; /** * Get the active request's mutable agent-run state. Returns `undefined` when * called outside an agent run (e.g. before `prepareRun` or in a non-agent * code path). Callers must tolerate the field absence; use the helper * `requireRequestRunContext()` if missing context is a programming error. */ export declare function getRequestRunContext(): RequestRunContext | undefined; /** * Ensure a `RequestRunContext` exists on the active request store and * return it. Used by the agent-chat handler to attach run state once it * starts processing a chat request. Returns `undefined` if there is no * active request store (caller should not be invoking this outside ALS). */ export declare function ensureRequestRunContext(): RequestRunContext | undefined; export {}; //# sourceMappingURL=request-context.d.ts.map