@databricks/appkit 0.82.0 → 0.83.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +6 -0
- package/dist/appkit/package.js +1 -1
- package/dist/connectors/lakebase/index.js +1 -1
- package/dist/connectors/lakebase/routing-pool.d.ts +1 -1
- package/dist/connectors/lakebase/routing-pool.js +3 -3
- package/dist/connectors/lakebase/routing-pool.js.map +1 -1
- package/dist/context/caller-context.d.ts +22 -0
- package/dist/context/caller-context.d.ts.map +1 -0
- package/dist/context/caller-context.js +21 -0
- package/dist/context/caller-context.js.map +1 -0
- package/dist/context/deprecation.js +14 -0
- package/dist/context/deprecation.js.map +1 -0
- package/dist/context/execution-context.d.ts +12 -4
- package/dist/context/execution-context.d.ts.map +1 -1
- package/dist/context/execution-context.js +36 -34
- package/dist/context/execution-context.js.map +1 -1
- package/dist/context/index.d.ts +3 -2
- package/dist/context/index.js +2 -1
- package/dist/context/service-context.d.ts +18 -12
- package/dist/context/service-context.d.ts.map +1 -1
- package/dist/context/service-context.js +44 -54
- package/dist/context/service-context.js.map +1 -1
- package/dist/context/user-context.d.ts +5 -8
- package/dist/context/user-context.d.ts.map +1 -1
- package/dist/context/user-context.js +33 -6
- package/dist/context/user-context.js.map +1 -1
- package/dist/core/appkit.d.ts.map +1 -1
- package/dist/core/appkit.js +4 -2
- package/dist/core/appkit.js.map +1 -1
- package/dist/index.d.ts +7 -4
- package/dist/index.js +4 -2
- package/dist/plugin/plugin.d.ts +2 -2
- package/dist/plugin/plugin.d.ts.map +1 -1
- package/dist/plugin/plugin.js +6 -6
- package/dist/plugin/plugin.js.map +1 -1
- package/dist/plugins/agents/agents.d.ts +1 -1
- package/dist/plugins/ai-search/ai-search.d.ts +1 -1
- package/dist/plugins/analytics/analytics.d.ts +1 -1
- package/dist/plugins/analytics/analytics.d.ts.map +1 -1
- package/dist/plugins/analytics/analytics.js +3 -1
- package/dist/plugins/analytics/analytics.js.map +1 -1
- package/dist/plugins/database/database.d.ts +1 -1
- package/dist/plugins/files/plugin.d.ts +11 -11
- package/dist/plugins/files/plugin.js +17 -17
- package/dist/plugins/files/plugin.js.map +1 -1
- package/dist/plugins/genie/genie.d.ts +1 -1
- package/dist/plugins/jobs/plugin.d.ts +1 -1
- package/dist/plugins/lakebase/lakebase.d.ts +1 -1
- package/dist/plugins/lakebase/lakebase.d.ts.map +1 -1
- package/dist/plugins/lakebase/lakebase.js +4 -4
- package/dist/plugins/lakebase/lakebase.js.map +1 -1
- package/dist/plugins/server/index.d.ts +1 -1
- package/dist/plugins/server/index.js +2 -2
- package/dist/plugins/server/index.js.map +1 -1
- package/dist/plugins/server/remote-tunnel/remote-tunnel-manager.js +3 -3
- package/dist/plugins/server/remote-tunnel/remote-tunnel-manager.js.map +1 -1
- package/dist/plugins/server/static-server.js +3 -3
- package/dist/plugins/server/static-server.js.map +1 -1
- package/dist/plugins/server/utils.js +3 -3
- package/dist/plugins/server/utils.js.map +1 -1
- package/dist/plugins/server/vite-dev-server.js +4 -4
- package/dist/plugins/server/vite-dev-server.js.map +1 -1
- package/dist/plugins/serving/serving.d.ts +1 -1
- package/dist/resources/index.d.ts +1 -0
- package/dist/resources/index.js +3 -0
- package/dist/resources/warehouse.d.ts +16 -0
- package/dist/resources/warehouse.d.ts.map +1 -0
- package/dist/resources/warehouse.js +93 -0
- package/dist/resources/warehouse.js.map +1 -0
- package/dist/schemas/manifest.d.ts +9 -0
- package/dist/schemas/manifest.d.ts.map +1 -1
- package/dist/schemas/manifest.js +11 -0
- package/dist/schemas/manifest.js.map +1 -1
- package/dist/shared/src/schemas/manifest.d.ts +42 -33
- package/dist/shared/src/schemas/manifest.d.ts.map +1 -1
- package/dist/shared/src/schemas/manifest.js +11 -0
- package/dist/shared/src/schemas/manifest.js.map +1 -1
- package/dist/testing/create-test-app.js +2 -2
- package/dist/testing/create-test-app.js.map +1 -1
- package/dist/testing/fixtures.d.ts +3 -2
- package/dist/testing/fixtures.d.ts.map +1 -1
- package/dist/testing/fixtures.js +21 -14
- package/dist/testing/fixtures.js.map +1 -1
- package/dist/testing/reset-singletons.js +1 -1
- package/dist/type-generator/database/generate.js +3 -3
- package/dist/type-generator/database/generate.js.map +1 -1
- package/dist/type-generator/migration.js +2 -2
- package/dist/type-generator/migration.js.map +1 -1
- package/dist/type-generator/serving/server-file-extractor.js +3 -3
- package/dist/type-generator/serving/server-file-extractor.js.map +1 -1
- package/docs/api/appkit/Class.Plugin.md +1 -1
- package/docs/api/appkit/Function.getCurrentActorId.md +12 -0
- package/docs/api/appkit/Function.getCurrentPrincipalKey.md +12 -0
- package/docs/api/appkit/Function.getExecutionContext.md +5 -3
- package/docs/api/appkit/Function.getWarehouseId.md +20 -0
- package/docs/api/appkit/Interface.CallerContext.md +41 -0
- package/docs/api/appkit/Interface.PluginManifest.md +25 -2
- package/docs/api/appkit/TypeAlias.CallerPrincipal.md +13 -0
- package/docs/api/appkit/TypeAlias.ExecutionContext.md +6 -0
- package/docs/api/appkit.md +6 -0
- package/docs/plugins/analytics.md +8 -0
- package/llms.txt +6 -0
- package/package.json +1 -1
- package/sbom.cdx.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.js","names":["otelContext","context"],"sources":["../../src/plugin/plugin.ts"],"sourcesContent":["import { createContextKey, context as otelContext } from \"@opentelemetry/api\";\nimport type express from \"express\";\nimport type {\n BasePlugin,\n BasePluginConfig,\n IAppResponse,\n PluginEndpointMap,\n PluginExecuteConfig,\n PluginExecutionSettings,\n PluginPhase,\n RouteConfig,\n StreamExecuteHandler,\n StreamExecutionSettings,\n} from \"shared\";\nimport { camelToKebab } from \"shared\";\n\nimport { AppManager } from \"../app\";\nimport { CacheManager } from \"../cache\";\nimport { getCurrentUserId, runInUserContext, ServiceContext } from \"../context\";\nimport type { PluginContext } from \"../core/plugin-context\";\nimport { AppKitError, AuthenticationError } from \"../errors\";\nimport { createLogger } from \"../logging/logger\";\nimport { StreamManager } from \"../stream\";\nimport {\n type ITelemetry,\n normalizeTelemetryOptions,\n TelemetryManager,\n} from \"../telemetry\";\nimport { deepMerge } from \"../utils\";\nimport { forwardAsyncErrors } from \"../utils/safe-handler\";\nimport { DevFileReader } from \"./dev-reader\";\nimport type { ExecutionResult } from \"./execution-result\";\nimport { CacheInterceptor } from \"./interceptors/cache\";\nimport { RetryInterceptor } from \"./interceptors/retry\";\nimport { TelemetryInterceptor } from \"./interceptors/telemetry\";\nimport { TimeoutInterceptor } from \"./interceptors/timeout\";\nimport type {\n ExecutionInterceptor,\n InterceptorContext,\n} from \"./interceptors/types\";\n\nconst logger = createLogger(\"plugin\");\n\n/**\n * OTel context key for marking OBO dev mode fallback.\n * Set when asUser() is called in development mode without a user token.\n */\nconst DEV_OBO_FALLBACK_KEY = createContextKey(\"appkit.devOboFallback\");\n\n/**\n * Returns true if `value` is a plain object literal (not an array, Date,\n * class instance, etc.). Used to decide whether to recurse into nested\n * export shapes when wrapping functions.\n *\n * @internal exported so the AppKit core can reuse the same predicate for\n * its `bindExportMethods` walk; not part of the public package surface.\n */\nexport function isPlainObject(\n value: unknown,\n): value is Record<string, unknown> {\n if (typeof value !== \"object\" || value === null) return false;\n const proto = Object.getPrototypeOf(value);\n return proto === Object.prototype || proto === null;\n}\n\n/**\n * Returns a deep copy of `exports` where every function has been replaced\n * with `wrap(fn)`, walking into nested plain objects.\n *\n * Used by the asUser proxy to make the user context follow function\n * references that escape the proxy via `exports()`. The original input is\n * not mutated, so plugins that memoize `exports()` are safe — each call\n * through the proxy yields an independent, freshly wrapped view.\n */\nfunction wrapExportFunctions(\n exports: Record<string, unknown>,\n wrap: (fn: (...a: unknown[]) => unknown) => (...a: unknown[]) => unknown,\n): Record<string, unknown> {\n const result: Record<string, unknown> = {};\n for (const key of Object.keys(exports)) {\n const val = exports[key];\n if (typeof val === \"function\") {\n result[key] = wrap(val as (...a: unknown[]) => unknown);\n } else if (isPlainObject(val)) {\n result[key] = wrapExportFunctions(val, wrap);\n } else {\n result[key] = val;\n }\n }\n return result;\n}\n\n/**\n * Returns true if the current execution is an OBO dev mode fallback\n * (asUser() was called but fell back to service principal due to missing token).\n */\nexport function isDevOboFallback(): boolean {\n return otelContext.active().getValue(DEV_OBO_FALLBACK_KEY) === true;\n}\n\n/**\n * Narrow an unknown thrown value to an Error that carries a numeric\n * `statusCode` property (e.g. `ApiError` from `@databricks/sdk-experimental`).\n */\nfunction hasHttpStatusCode(\n error: unknown,\n): error is Error & { statusCode: number } {\n return (\n error instanceof Error &&\n \"statusCode\" in error &&\n typeof (error as Record<string, unknown>).statusCode === \"number\"\n );\n}\n\n/**\n * Methods that should not be proxied by asUser().\n * These are lifecycle/internal methods that don't make sense\n * to execute in a user context.\n */\nconst EXCLUDED_FROM_PROXY = new Set([\n // Lifecycle methods\n \"setup\",\n \"shutdown\",\n \"attachContext\",\n \"injectRoutes\",\n \"getEndpoints\",\n \"getSkipBodyParsingPaths\",\n \"abortActiveOperations\",\n \"clientConfig\",\n // asUser itself - prevent chaining like .asUser().asUser()\n \"asUser\",\n // Internal methods\n \"constructor\",\n]);\n\n/**\n * Base abstract class for creating AppKit plugins.\n *\n * All plugins must declare a static `manifest` property with their metadata\n * and resource requirements. The manifest defines:\n * - `required` resources: Always needed for the plugin to function\n * - `optional` resources: May be needed depending on plugin configuration\n *\n * ## Static vs Runtime Resource Requirements\n *\n * The manifest is static and doesn't know the plugin's runtime configuration.\n * For resources that become required based on config options, plugins can\n * implement a static `getResourceRequirements(config)` method.\n *\n * At runtime, this method is called with the actual config to determine\n * which \"optional\" resources should be treated as \"required\".\n *\n * @example Basic plugin with static requirements\n * ```typescript\n * import { Plugin, toPlugin, PluginManifest, ResourceType } from '@databricks/appkit';\n *\n * const myManifest: PluginManifest = {\n * name: 'myPlugin',\n * displayName: 'My Plugin',\n * description: 'Does something awesome',\n * resources: {\n * required: [\n * { type: ResourceType.SQL_WAREHOUSE, alias: 'warehouse', ... }\n * ],\n * optional: []\n * }\n * };\n *\n * class MyPlugin extends Plugin<MyConfig> {\n * static manifest = myManifest;\n * }\n * ```\n *\n * @example Plugin with config-dependent resources\n * ```typescript\n * interface MyConfig extends BasePluginConfig {\n * enableCaching?: boolean;\n * }\n *\n * const myManifest: PluginManifest = {\n * name: 'myPlugin',\n * resources: {\n * required: [\n * { type: ResourceType.SQL_WAREHOUSE, alias: 'warehouse', ... }\n * ],\n * optional: [\n * // Database is optional in the static manifest\n * { type: ResourceType.DATABASE, alias: 'cache', description: 'Required if caching enabled', ... }\n * ]\n * }\n * };\n *\n * class MyPlugin extends Plugin<MyConfig> {\n * static manifest = myManifest<\"myPlugin\">;\n *\n * // Runtime method: converts optional resources to required based on config\n * static getResourceRequirements(config: MyConfig) {\n * const resources = [];\n * if (config.enableCaching) {\n * // When caching is enabled, Database becomes required\n * resources.push({\n * type: ResourceType.DATABASE,\n * alias: 'cache',\n * resourceKey: 'database',\n * description: 'Cache storage for query results',\n * permission: 'CAN_CONNECT_AND_CREATE',\n * fields: {\n * instance_name: { env: 'DATABRICKS_CACHE_INSTANCE' },\n * database_name: { env: 'DATABRICKS_CACHE_DB' },\n * },\n * required: true // Mark as required at runtime\n * });\n * }\n * return resources;\n * }\n * }\n * ```\n */\nexport abstract class Plugin<\n TConfig extends BasePluginConfig = BasePluginConfig,\n> implements BasePlugin {\n protected isReady = false;\n protected cache!: CacheManager;\n protected app: AppManager;\n protected devFileReader: DevFileReader;\n protected streamManager: StreamManager;\n protected telemetry!: ITelemetry;\n protected context?: PluginContext;\n\n /** Registered endpoints for this plugin */\n private registeredEndpoints: PluginEndpointMap = {};\n\n /** Paths that opt out of JSON body parsing (e.g. file upload routes) */\n private skipBodyParsingPaths: Set<string> = new Set();\n\n /**\n * Plugin initialization phase.\n * - 'core': Initialized first (e.g., config plugins)\n * - 'normal': Initialized second (most plugins)\n * - 'deferred': Initialized last (e.g., server plugin)\n */\n static phase: PluginPhase = \"normal\";\n\n /**\n * Plugin name identifier.\n */\n name: string;\n\n constructor(protected config: TConfig) {\n this.name =\n config.name ??\n (this.constructor as { manifest?: { name: string } }).manifest?.name ??\n \"plugin\";\n this.streamManager = new StreamManager(config.streamConfig);\n this.app = new AppManager();\n this.devFileReader = DevFileReader.getInstance();\n this.context = (config as Record<string, unknown>).context as\n | PluginContext\n | undefined;\n\n // Eagerly bind telemetry + cache if the core services have already been\n // initialized (normal createApp path, or tests that mock CacheManager).\n // If they haven't, we leave these undefined and rely on `attachContext`\n // being called later — this lets factories eagerly construct plugin\n // instances at module top-level before `createApp` has run.\n this.tryAttachContext();\n }\n\n private tryAttachContext(): void {\n try {\n this.cache = CacheManager.getInstanceSync();\n } catch {\n return;\n }\n this.telemetry = TelemetryManager.getProvider(\n this.name,\n this.config.telemetry,\n );\n this.isReady = true;\n }\n\n /**\n * Binds runtime dependencies (telemetry provider, cache, plugin context) to\n * this plugin. Called by `AppKit._createApp` after construction and before\n * `setup()`. Idempotent: safe to call if the constructor already bound them\n * eagerly. Kept separate so factories can eagerly construct plugin instances\n * without running this before `TelemetryManager.initialize()` /\n * `CacheManager.getInstance()` have run.\n */\n attachContext(\n deps: {\n context?: unknown;\n telemetryConfig?: BasePluginConfig[\"telemetry\"];\n } = {},\n ): void {\n if (!this.cache) {\n this.cache = CacheManager.getInstanceSync();\n }\n this.telemetry = TelemetryManager.getProvider(\n this.name,\n deps.telemetryConfig ?? this.config.telemetry,\n );\n if (deps.context !== undefined) {\n this.context = deps.context as PluginContext;\n }\n this.isReady = true;\n }\n\n injectRoutes(_: express.Router) {\n return;\n }\n\n async setup() {}\n\n getEndpoints(): PluginEndpointMap {\n return this.registeredEndpoints;\n }\n\n getSkipBodyParsingPaths(): ReadonlySet<string> {\n return this.skipBodyParsingPaths;\n }\n\n abortActiveOperations(): void {\n this.streamManager.abortAll();\n }\n\n /**\n * Returns the public exports for this plugin.\n * Override this to define a custom public API.\n * By default, returns an empty object.\n *\n * The returned object becomes the plugin's public API on the AppKit instance\n * (e.g. `appkit.myPlugin.method()`). AppKit automatically binds method context\n * and adds `asUser(req)` for user-scoped execution.\n *\n * @example\n * ```ts\n * class MyPlugin extends Plugin {\n * private getData() { return []; }\n *\n * exports() {\n * return { getData: this.getData };\n * }\n * }\n *\n * // After registration:\n * const appkit = await createApp({ plugins: [myPlugin()] });\n * appkit.myPlugin.getData();\n * ```\n */\n exports(): unknown {\n return {};\n }\n\n /**\n * Returns startup config to expose to the client.\n * Override this to surface server-side values that are safe to publish to the\n * frontend, such as feature flags, resource IDs, or other app boot settings.\n *\n * This runs once when the server starts, so it should not depend on\n * request-scoped or user-specific state.\n *\n * String values that match non-public environment variables are redacted\n * unless you intentionally expose them via a matching `PUBLIC_APPKIT_` env var.\n *\n * Values must be JSON-serializable plain data (no functions, Dates, classes,\n * Maps, Sets, BigInts, or circular references).\n * By default returns an empty object (plugin contributes nothing to client config).\n *\n * On the client, read the config with the `usePluginClientConfig` hook\n * (React) or the `getPluginClientConfig` function (vanilla JS), both\n * from `@databricks/appkit-ui`.\n *\n * @example\n * ```ts\n * // Server — plugin definition\n * class MyPlugin extends Plugin<MyConfig> {\n * clientConfig() {\n * return {\n * warehouseId: this.config.warehouseId,\n * features: { darkMode: true },\n * };\n * }\n * }\n *\n * // Client — React component\n * import { usePluginClientConfig } from \"@databricks/appkit-ui/react\";\n *\n * interface MyPluginConfig { warehouseId: string; features: { darkMode: boolean } }\n *\n * const config = usePluginClientConfig<MyPluginConfig>(\"myPlugin\");\n * config.warehouseId; // \"abc-123\"\n *\n * // Client — vanilla JS\n * import { getPluginClientConfig } from \"@databricks/appkit-ui/js\";\n *\n * const config = getPluginClientConfig<MyPluginConfig>(\"myPlugin\");\n * ```\n */\n clientConfig(): Record<string, unknown> {\n return {};\n }\n\n /**\n * Resolve the effective user ID from a request.\n *\n * Returns the `x-forwarded-user` header when present. In development mode\n * (`NODE_ENV=development`) falls back to the current context user ID so\n * that callers outside an active `runInUserContext` scope still get a\n * consistent value.\n *\n * @throws AuthenticationError in production when no user header is present.\n */\n protected resolveUserId(req: express.Request): string {\n const userId = req.header(\"x-forwarded-user\")?.trim();\n if (userId) return userId;\n if (process.env.NODE_ENV === \"development\") return getCurrentUserId();\n throw AuthenticationError.missingUserId();\n }\n\n /**\n * Execute operations using the user's identity from the request.\n * Returns a proxy of this plugin where all method calls execute\n * with the user's Databricks credentials instead of the service principal.\n *\n * @param req - The Express request containing the user token in headers\n * @returns A proxied plugin instance that executes as the user\n * @throws AuthenticationError if user token is not available in request headers (production only).\n * In development mode (`NODE_ENV=development`), skips user impersonation instead of throwing.\n */\n asUser(req: express.Request): this {\n const token = req.header(\"x-forwarded-access-token\")?.trim();\n const userId = req.header(\"x-forwarded-user\")?.trim();\n const userEmail = req.header(\"x-forwarded-email\");\n const isDev = process.env.NODE_ENV === \"development\";\n\n // In local development, skip user impersonation since there's no user\n // token available. Mark execution as OBO dev fallback via OTel context\n // so telemetry can distinguish intended OBO calls from regular SP calls.\n if (!token && isDev) {\n logger.warn(\n \"asUser() called without user token in development mode. Skipping user impersonation.\",\n );\n\n return this._createAsUserProxy((fn) => (...args) => {\n const ctx = otelContext.active().setValue(DEV_OBO_FALLBACK_KEY, true);\n return otelContext.with(ctx, () => fn(...args));\n });\n }\n\n if (!token) {\n throw AuthenticationError.missingToken(\"user token\");\n }\n\n if (!userId && !isDev) {\n throw AuthenticationError.missingUserId();\n }\n\n const effectiveUserId = userId || \"dev-user\";\n\n const userContext = ServiceContext.createUserContext(\n token,\n effectiveUserId,\n undefined,\n userEmail ?? undefined,\n );\n\n return this._createAsUserProxy(\n (fn) =>\n (...args) =>\n runInUserContext(userContext, () => fn(...args)),\n );\n }\n\n /**\n * Creates a proxy of `this` where every method call — and every function\n * in the result of `exports()` — runs inside `wrapCall`.\n *\n * `wrapCall` decides the per-call scope. Two strategies are used today:\n * - real OBO: fn => (...args) => runInUserContext(userContext, () => fn(...args))\n * - dev fallback: fn => (...args) => otelContext.with(DEV_OBO_FALLBACK_KEY=true, () => fn(...args))\n *\n * `exports` is intercepted because methods captured in the returned\n * exports object never re-enter the proxy's `get` trap. Wrapping them\n * here is the only way to make the user context follow function\n * references back out of the plugin.\n */\n private _createAsUserProxy(\n wrapCall: (\n fn: (...a: unknown[]) => unknown,\n ) => (...a: unknown[]) => unknown,\n ): this {\n return new Proxy(this, {\n get: (target, prop, receiver) => {\n const value = Reflect.get(target, prop, receiver);\n\n if (typeof value !== \"function\") return value;\n if (typeof prop === \"string\" && EXCLUDED_FROM_PROXY.has(prop))\n return value;\n\n if (prop === \"exports\") {\n return () => {\n const raw = (value as () => unknown).call(target);\n if (raw == null) return {};\n // Callable exports (e.g. files, jobs) manage per-call asUser\n // themselves; leave them untouched.\n if (typeof raw === \"function\") return raw;\n if (isPlainObject(raw)) {\n return wrapExportFunctions(raw, (fn) =>\n wrapCall(fn.bind(target)),\n );\n }\n return raw;\n };\n }\n\n const fn = (value as (...a: unknown[]) => unknown).bind(target);\n return wrapCall(fn);\n },\n }) as this;\n }\n\n // streaming execution with interceptors\n protected async executeStream<T>(\n res: IAppResponse,\n fn: StreamExecuteHandler<T>,\n options: StreamExecutionSettings,\n userKey?: string,\n ) {\n // destructure options\n const {\n stream: streamConfig,\n default: defaultConfig,\n user: userConfig,\n } = options;\n\n // build execution options\n const executeConfig = this._buildExecutionConfig({\n default: defaultConfig,\n user: userConfig,\n });\n\n // get user key from context if not provided\n const effectiveUserKey = userKey ?? getCurrentUserId();\n\n const self = this;\n // capture the active OTel context (HTTP span) before entering the async generator,\n // where it would otherwise be lost across the async boundary\n const parentOtelContext = otelContext.active();\n\n // wrapper function to ensure it returns a generator\n const asyncWrapperFn = async function* (streamSignal?: AbortSignal) {\n // build execution context\n const context: InterceptorContext = {\n signal: streamSignal,\n metadata: new Map(),\n userKey: effectiveUserKey,\n };\n\n // build interceptors\n const interceptors = self._buildInterceptors(executeConfig);\n\n // wrap the function to ensure it returns a promise\n const wrappedFn = async () => {\n const result = await fn(context.signal);\n return result;\n };\n\n // execute the function with interceptors, restoring the parent OTel context\n // so telemetry spans are linked as children of the HTTP request span\n const result = await otelContext.with(parentOtelContext, () =>\n self._executeWithInterceptors(\n wrappedFn as (signal?: AbortSignal) => Promise<T>,\n interceptors,\n context,\n ),\n );\n\n // check if result is a generator\n if (self._checkIfGenerator(result)) {\n yield* result;\n } else {\n yield result;\n }\n };\n\n // stream the result to the client. The effective user key is forwarded\n // to the stream manager so that reconnections to existing streamIds are\n // bound to the original creator (prevents cross-user stream takeover via\n // guessed/leaked IDs).\n await this.streamManager.stream(\n res,\n asyncWrapperFn,\n streamConfig,\n effectiveUserKey,\n );\n }\n\n /**\n * Execute a function with the plugin's interceptor chain.\n *\n * Returns an {@link ExecutionResult} discriminated union:\n * - `{ ok: true, data: T }` on success\n * - `{ ok: false, status: number, message: string }` on failure\n *\n * Errors are never thrown — the method is production-safe.\n */\n protected async execute<T>(\n fn: (signal?: AbortSignal) => Promise<T>,\n options: PluginExecutionSettings,\n userKey?: string,\n ): Promise<ExecutionResult<T>> {\n const executeConfig = this._buildExecutionConfig(options);\n\n const interceptors = this._buildInterceptors(executeConfig);\n\n // get user key from context if not provided\n const effectiveUserKey = userKey ?? getCurrentUserId();\n\n const context: InterceptorContext = {\n metadata: new Map(),\n userKey: effectiveUserKey,\n };\n\n try {\n const data = await this._executeWithInterceptors(\n fn,\n interceptors,\n context,\n );\n return { ok: true, data };\n } catch (error) {\n logger.error(\"Plugin execution failed\", { error, plugin: this.name });\n\n if (error instanceof AppKitError) {\n return {\n ok: false,\n status: error.statusCode,\n message: error.message,\n };\n }\n\n if (hasHttpStatusCode(error)) {\n const isDev = process.env.NODE_ENV !== \"production\";\n const isClientError = error.statusCode >= 400 && error.statusCode < 500;\n return {\n ok: false,\n status: error.statusCode,\n message: isDev || isClientError ? error.message : \"Server error\",\n };\n }\n\n const isDev = process.env.NODE_ENV !== \"production\";\n return {\n ok: false,\n status: 500,\n message:\n isDev && error instanceof Error ? error.message : \"Server error\",\n };\n }\n }\n\n protected registerEndpoint(name: string, path: string): void {\n this.registeredEndpoints[name] = path;\n }\n\n protected route<_TResponse>(\n router: express.Router,\n config: RouteConfig,\n ): void {\n const { name, method, path, handler } = config;\n\n router[method](path, forwardAsyncErrors(handler));\n\n const fullPath = `/api/${camelToKebab(this.name)}${path}`;\n this.registerEndpoint(name, fullPath);\n\n if (config.skipBodyParsing) {\n this.skipBodyParsingPaths.add(fullPath);\n }\n }\n\n // build execution options by merging defaults, plugin config, and user overrides\n private _buildExecutionConfig(\n options: PluginExecutionSettings,\n ): PluginExecuteConfig {\n const { default: methodDefaults, user: userOverride } = options;\n\n // Merge: method defaults <- plugin config <- user override (highest priority)\n return deepMerge(\n deepMerge(methodDefaults, this.config),\n userOverride ?? {},\n ) as PluginExecuteConfig;\n }\n\n // build interceptors based on execute options\n private _buildInterceptors(\n options: PluginExecuteConfig,\n ): ExecutionInterceptor[] {\n const interceptors: ExecutionInterceptor[] = [];\n\n // order matters: telemetry → timeout → retry → cache (innermost to outermost)\n\n const telemetryConfig = normalizeTelemetryOptions(this.config.telemetry);\n if (\n telemetryConfig.traces &&\n (options.telemetryInterceptor?.enabled ?? true)\n ) {\n interceptors.push(\n new TelemetryInterceptor(this.telemetry, options.telemetryInterceptor),\n );\n }\n\n if (options.timeout && options.timeout > 0) {\n interceptors.push(new TimeoutInterceptor(options.timeout));\n }\n\n if (\n options.retry?.enabled &&\n options.retry.attempts &&\n options.retry.attempts > 1\n ) {\n interceptors.push(new RetryInterceptor(options.retry));\n }\n\n if (options.cache?.enabled && options.cache.cacheKey?.length) {\n interceptors.push(new CacheInterceptor(this.cache, options.cache));\n }\n\n return interceptors;\n }\n\n // execute method wrapped with interceptors\n private async _executeWithInterceptors<T>(\n fn: (signal?: AbortSignal) => Promise<T>,\n interceptors: ExecutionInterceptor[],\n context: InterceptorContext,\n ): Promise<T> {\n // no interceptors, execute directly\n if (interceptors.length === 0) {\n return fn(context.signal);\n }\n // build nested execution chain from interceptors\n let wrappedFn = () => fn(context.signal);\n\n // wrap each interceptor around the previous function\n for (const interceptor of interceptors) {\n const previousFn = wrappedFn;\n wrappedFn = () => interceptor.intercept(previousFn, context);\n }\n\n return wrappedFn();\n }\n\n private _checkIfGenerator(\n result: any,\n ): result is AsyncGenerator<any, void, unknown> {\n return (\n result && typeof result === \"object\" && Symbol.asyncIterator in result\n );\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"plugin.js","names":["otelContext","context"],"sources":["../../src/plugin/plugin.ts"],"sourcesContent":["import { createContextKey, context as otelContext } from \"@opentelemetry/api\";\nimport type express from \"express\";\nimport type {\n BasePlugin,\n BasePluginConfig,\n IAppResponse,\n PluginEndpointMap,\n PluginExecuteConfig,\n PluginExecutionSettings,\n PluginPhase,\n RouteConfig,\n StreamExecuteHandler,\n StreamExecutionSettings,\n} from \"shared\";\nimport { camelToKebab } from \"shared\";\n\nimport { AppManager } from \"../app\";\nimport { CacheManager } from \"../cache\";\nimport {\n getCurrentUserId,\n runInCallerContext,\n ServiceContext,\n} from \"../context\";\nimport type { PluginContext } from \"../core/plugin-context\";\nimport { AppKitError, AuthenticationError } from \"../errors\";\nimport { createLogger } from \"../logging/logger\";\nimport { StreamManager } from \"../stream\";\nimport {\n type ITelemetry,\n normalizeTelemetryOptions,\n TelemetryManager,\n} from \"../telemetry\";\nimport { deepMerge } from \"../utils\";\nimport { forwardAsyncErrors } from \"../utils/safe-handler\";\nimport { DevFileReader } from \"./dev-reader\";\nimport type { ExecutionResult } from \"./execution-result\";\nimport { CacheInterceptor } from \"./interceptors/cache\";\nimport { RetryInterceptor } from \"./interceptors/retry\";\nimport { TelemetryInterceptor } from \"./interceptors/telemetry\";\nimport { TimeoutInterceptor } from \"./interceptors/timeout\";\nimport type {\n ExecutionInterceptor,\n InterceptorContext,\n} from \"./interceptors/types\";\n\nconst logger = createLogger(\"plugin\");\n\n/**\n * OTel context key for marking OBO dev mode fallback.\n * Set when asUser() is called in development mode without a user token.\n */\nconst DEV_OBO_FALLBACK_KEY = createContextKey(\"appkit.devOboFallback\");\n\n/**\n * Returns true if `value` is a plain object literal (not an array, Date,\n * class instance, etc.). Used to decide whether to recurse into nested\n * export shapes when wrapping functions.\n *\n * @internal exported so the AppKit core can reuse the same predicate for\n * its `bindExportMethods` walk; not part of the public package surface.\n */\nexport function isPlainObject(\n value: unknown,\n): value is Record<string, unknown> {\n if (typeof value !== \"object\" || value === null) return false;\n const proto = Object.getPrototypeOf(value);\n return proto === Object.prototype || proto === null;\n}\n\n/**\n * Returns a deep copy of `exports` where every function has been replaced\n * with `wrap(fn)`, walking into nested plain objects.\n *\n * Used by the asUser proxy to make the user context follow function\n * references that escape the proxy via `exports()`. The original input is\n * not mutated, so plugins that memoize `exports()` are safe — each call\n * through the proxy yields an independent, freshly wrapped view.\n */\nfunction wrapExportFunctions(\n exports: Record<string, unknown>,\n wrap: (fn: (...a: unknown[]) => unknown) => (...a: unknown[]) => unknown,\n): Record<string, unknown> {\n const result: Record<string, unknown> = {};\n for (const key of Object.keys(exports)) {\n const val = exports[key];\n if (typeof val === \"function\") {\n result[key] = wrap(val as (...a: unknown[]) => unknown);\n } else if (isPlainObject(val)) {\n result[key] = wrapExportFunctions(val, wrap);\n } else {\n result[key] = val;\n }\n }\n return result;\n}\n\n/**\n * Returns true if the current execution is an OBO dev mode fallback\n * (asUser() was called but fell back to service principal due to missing token).\n */\nexport function isDevOboFallback(): boolean {\n return otelContext.active().getValue(DEV_OBO_FALLBACK_KEY) === true;\n}\n\n/**\n * Narrow an unknown thrown value to an Error that carries a numeric\n * `statusCode` property (e.g. `ApiError` from `@databricks/sdk-experimental`).\n */\nfunction hasHttpStatusCode(\n error: unknown,\n): error is Error & { statusCode: number } {\n return (\n error instanceof Error &&\n \"statusCode\" in error &&\n typeof (error as Record<string, unknown>).statusCode === \"number\"\n );\n}\n\n/**\n * Methods that should not be proxied by asUser().\n * These are lifecycle/internal methods that don't make sense\n * to execute in a user context.\n */\nconst EXCLUDED_FROM_PROXY = new Set([\n // Lifecycle methods\n \"setup\",\n \"shutdown\",\n \"attachContext\",\n \"injectRoutes\",\n \"getEndpoints\",\n \"getSkipBodyParsingPaths\",\n \"abortActiveOperations\",\n \"clientConfig\",\n // asUser itself - prevent chaining like .asUser().asUser()\n \"asUser\",\n // Internal methods\n \"constructor\",\n]);\n\n/**\n * Base abstract class for creating AppKit plugins.\n *\n * All plugins must declare a static `manifest` property with their metadata\n * and resource requirements. The manifest defines:\n * - `required` resources: Always needed for the plugin to function\n * - `optional` resources: May be needed depending on plugin configuration\n *\n * ## Static vs Runtime Resource Requirements\n *\n * The manifest is static and doesn't know the plugin's runtime configuration.\n * For resources that become required based on config options, plugins can\n * implement a static `getResourceRequirements(config)` method.\n *\n * At runtime, this method is called with the actual config to determine\n * which \"optional\" resources should be treated as \"required\".\n *\n * @example Basic plugin with static requirements\n * ```typescript\n * import { Plugin, toPlugin, PluginManifest, ResourceType } from '@databricks/appkit';\n *\n * const myManifest: PluginManifest = {\n * name: 'myPlugin',\n * displayName: 'My Plugin',\n * description: 'Does something awesome',\n * resources: {\n * required: [\n * { type: ResourceType.SQL_WAREHOUSE, alias: 'warehouse', ... }\n * ],\n * optional: []\n * }\n * };\n *\n * class MyPlugin extends Plugin<MyConfig> {\n * static manifest = myManifest;\n * }\n * ```\n *\n * @example Plugin with config-dependent resources\n * ```typescript\n * interface MyConfig extends BasePluginConfig {\n * enableCaching?: boolean;\n * }\n *\n * const myManifest: PluginManifest = {\n * name: 'myPlugin',\n * resources: {\n * required: [\n * { type: ResourceType.SQL_WAREHOUSE, alias: 'warehouse', ... }\n * ],\n * optional: [\n * // Database is optional in the static manifest\n * { type: ResourceType.DATABASE, alias: 'cache', description: 'Required if caching enabled', ... }\n * ]\n * }\n * };\n *\n * class MyPlugin extends Plugin<MyConfig> {\n * static manifest = myManifest<\"myPlugin\">;\n *\n * // Runtime method: converts optional resources to required based on config\n * static getResourceRequirements(config: MyConfig) {\n * const resources = [];\n * if (config.enableCaching) {\n * // When caching is enabled, Database becomes required\n * resources.push({\n * type: ResourceType.DATABASE,\n * alias: 'cache',\n * resourceKey: 'database',\n * description: 'Cache storage for query results',\n * permission: 'CAN_CONNECT_AND_CREATE',\n * fields: {\n * instance_name: { env: 'DATABRICKS_CACHE_INSTANCE' },\n * database_name: { env: 'DATABRICKS_CACHE_DB' },\n * },\n * required: true // Mark as required at runtime\n * });\n * }\n * return resources;\n * }\n * }\n * ```\n */\nexport abstract class Plugin<\n TConfig extends BasePluginConfig = BasePluginConfig,\n> implements BasePlugin {\n protected isReady = false;\n protected cache!: CacheManager;\n protected app: AppManager;\n protected devFileReader: DevFileReader;\n protected streamManager: StreamManager;\n protected telemetry!: ITelemetry;\n protected context?: PluginContext;\n\n /** Registered endpoints for this plugin */\n private registeredEndpoints: PluginEndpointMap = {};\n\n /** Paths that opt out of JSON body parsing (e.g. file upload routes) */\n private skipBodyParsingPaths: Set<string> = new Set();\n\n /**\n * Plugin initialization phase.\n * - 'core': Initialized first (e.g., config plugins)\n * - 'normal': Initialized second (most plugins)\n * - 'deferred': Initialized last (e.g., server plugin)\n */\n static phase: PluginPhase = \"normal\";\n\n /**\n * Plugin name identifier.\n */\n name: string;\n\n constructor(protected config: TConfig) {\n this.name =\n config.name ??\n (this.constructor as { manifest?: { name: string } }).manifest?.name ??\n \"plugin\";\n this.streamManager = new StreamManager(config.streamConfig);\n this.app = new AppManager();\n this.devFileReader = DevFileReader.getInstance();\n this.context = (config as Record<string, unknown>).context as\n | PluginContext\n | undefined;\n\n // Eagerly bind telemetry + cache if the core services have already been\n // initialized (normal createApp path, or tests that mock CacheManager).\n // If they haven't, we leave these undefined and rely on `attachContext`\n // being called later — this lets factories eagerly construct plugin\n // instances at module top-level before `createApp` has run.\n this.tryAttachContext();\n }\n\n private tryAttachContext(): void {\n try {\n this.cache = CacheManager.getInstanceSync();\n } catch {\n return;\n }\n this.telemetry = TelemetryManager.getProvider(\n this.name,\n this.config.telemetry,\n );\n this.isReady = true;\n }\n\n /**\n * Binds runtime dependencies (telemetry provider, cache, plugin context) to\n * this plugin. Called by `AppKit._createApp` after construction and before\n * `setup()`. Idempotent: safe to call if the constructor already bound them\n * eagerly. Kept separate so factories can eagerly construct plugin instances\n * without running this before `TelemetryManager.initialize()` /\n * `CacheManager.getInstance()` have run.\n */\n attachContext(\n deps: {\n context?: unknown;\n telemetryConfig?: BasePluginConfig[\"telemetry\"];\n } = {},\n ): void {\n if (!this.cache) {\n this.cache = CacheManager.getInstanceSync();\n }\n this.telemetry = TelemetryManager.getProvider(\n this.name,\n deps.telemetryConfig ?? this.config.telemetry,\n );\n if (deps.context !== undefined) {\n this.context = deps.context as PluginContext;\n }\n this.isReady = true;\n }\n\n injectRoutes(_: express.Router) {\n return;\n }\n\n async setup() {}\n\n getEndpoints(): PluginEndpointMap {\n return this.registeredEndpoints;\n }\n\n getSkipBodyParsingPaths(): ReadonlySet<string> {\n return this.skipBodyParsingPaths;\n }\n\n abortActiveOperations(): void {\n this.streamManager.abortAll();\n }\n\n /**\n * Returns the public exports for this plugin.\n * Override this to define a custom public API.\n * By default, returns an empty object.\n *\n * The returned object becomes the plugin's public API on the AppKit instance\n * (e.g. `appkit.myPlugin.method()`). AppKit automatically binds method context\n * and adds `asUser(req)` for user-scoped execution.\n *\n * @example\n * ```ts\n * class MyPlugin extends Plugin {\n * private getData() { return []; }\n *\n * exports() {\n * return { getData: this.getData };\n * }\n * }\n *\n * // After registration:\n * const appkit = await createApp({ plugins: [myPlugin()] });\n * appkit.myPlugin.getData();\n * ```\n */\n exports(): unknown {\n return {};\n }\n\n /**\n * Returns startup config to expose to the client.\n * Override this to surface server-side values that are safe to publish to the\n * frontend, such as feature flags, resource IDs, or other app boot settings.\n *\n * This runs once when the server starts, so it should not depend on\n * request-scoped or user-specific state.\n *\n * String values that match non-public environment variables are redacted\n * unless you intentionally expose them via a matching `PUBLIC_APPKIT_` env var.\n *\n * Values must be JSON-serializable plain data (no functions, Dates, classes,\n * Maps, Sets, BigInts, or circular references).\n * By default returns an empty object (plugin contributes nothing to client config).\n *\n * On the client, read the config with the `usePluginClientConfig` hook\n * (React) or the `getPluginClientConfig` function (vanilla JS), both\n * from `@databricks/appkit-ui`.\n *\n * @example\n * ```ts\n * // Server — plugin definition\n * class MyPlugin extends Plugin<MyConfig> {\n * clientConfig() {\n * return {\n * warehouseId: this.config.warehouseId,\n * features: { darkMode: true },\n * };\n * }\n * }\n *\n * // Client — React component\n * import { usePluginClientConfig } from \"@databricks/appkit-ui/react\";\n *\n * interface MyPluginConfig { warehouseId: string; features: { darkMode: boolean } }\n *\n * const config = usePluginClientConfig<MyPluginConfig>(\"myPlugin\");\n * config.warehouseId; // \"abc-123\"\n *\n * // Client — vanilla JS\n * import { getPluginClientConfig } from \"@databricks/appkit-ui/js\";\n *\n * const config = getPluginClientConfig<MyPluginConfig>(\"myPlugin\");\n * ```\n */\n clientConfig(): Record<string, unknown> {\n return {};\n }\n\n /**\n * Resolve the effective user ID from a request.\n *\n * Returns the `x-forwarded-user` header when present. In development mode\n * (`NODE_ENV=development`) falls back to the current context user ID so\n * that callers outside an active `runInCallerContext` scope still get a\n * consistent value.\n *\n * @throws AuthenticationError in production when no user header is present.\n */\n protected resolveUserId(req: express.Request): string {\n const userId = req.header(\"x-forwarded-user\")?.trim();\n if (userId) return userId;\n if (process.env.NODE_ENV === \"development\") return getCurrentUserId();\n throw AuthenticationError.missingUserId();\n }\n\n /**\n * Execute operations using the user's identity from the request.\n * Returns a proxy of this plugin where all method calls execute\n * with the user's Databricks credentials instead of the service principal.\n *\n * @param req - The Express request containing the user token in headers\n * @returns A proxied plugin instance that executes as the user\n * @throws AuthenticationError if user token is not available in request headers (production only).\n * In development mode (`NODE_ENV=development`), skips user impersonation instead of throwing.\n */\n asUser(req: express.Request): this {\n const token = req.header(\"x-forwarded-access-token\")?.trim();\n const userId = req.header(\"x-forwarded-user\")?.trim();\n const userEmail = req.header(\"x-forwarded-email\");\n const isDev = process.env.NODE_ENV === \"development\";\n\n // In local development, skip user impersonation since there's no user\n // token available. Mark execution as OBO dev fallback via OTel context\n // so telemetry can distinguish intended OBO calls from regular SP calls.\n if (!token && isDev) {\n logger.warn(\n \"asUser() called without user token in development mode. Skipping user impersonation.\",\n );\n\n return this._createAsUserProxy((fn) => (...args) => {\n const ctx = otelContext.active().setValue(DEV_OBO_FALLBACK_KEY, true);\n return otelContext.with(ctx, () => fn(...args));\n });\n }\n\n if (!token) {\n throw AuthenticationError.missingToken(\"user token\");\n }\n\n if (!userId && !isDev) {\n throw AuthenticationError.missingUserId();\n }\n\n const effectiveUserId = userId || \"dev-user\";\n\n const userContext = ServiceContext.createCallerContext(\n token,\n effectiveUserId,\n undefined,\n userEmail ?? undefined,\n );\n\n return this._createAsUserProxy(\n (fn) =>\n (...args) =>\n runInCallerContext(userContext, () => fn(...args)),\n );\n }\n\n /**\n * Creates a proxy of `this` where every method call — and every function\n * in the result of `exports()` — runs inside `wrapCall`.\n *\n * `wrapCall` decides the per-call scope. Two strategies are used today:\n * - real OBO: fn => (...args) => runInCallerContext(userContext, () => fn(...args))\n * - dev fallback: fn => (...args) => otelContext.with(DEV_OBO_FALLBACK_KEY=true, () => fn(...args))\n *\n * `exports` is intercepted because methods captured in the returned\n * exports object never re-enter the proxy's `get` trap. Wrapping them\n * here is the only way to make the user context follow function\n * references back out of the plugin.\n */\n private _createAsUserProxy(\n wrapCall: (\n fn: (...a: unknown[]) => unknown,\n ) => (...a: unknown[]) => unknown,\n ): this {\n return new Proxy(this, {\n get: (target, prop, receiver) => {\n const value = Reflect.get(target, prop, receiver);\n\n if (typeof value !== \"function\") return value;\n if (typeof prop === \"string\" && EXCLUDED_FROM_PROXY.has(prop))\n return value;\n\n if (prop === \"exports\") {\n return () => {\n const raw = (value as () => unknown).call(target);\n if (raw == null) return {};\n // Callable exports (e.g. files, jobs) manage per-call asUser\n // themselves; leave them untouched.\n if (typeof raw === \"function\") return raw;\n if (isPlainObject(raw)) {\n return wrapExportFunctions(raw, (fn) =>\n wrapCall(fn.bind(target)),\n );\n }\n return raw;\n };\n }\n\n const fn = (value as (...a: unknown[]) => unknown).bind(target);\n return wrapCall(fn);\n },\n }) as this;\n }\n\n // streaming execution with interceptors\n protected async executeStream<T>(\n res: IAppResponse,\n fn: StreamExecuteHandler<T>,\n options: StreamExecutionSettings,\n userKey?: string,\n ) {\n // destructure options\n const {\n stream: streamConfig,\n default: defaultConfig,\n user: userConfig,\n } = options;\n\n // build execution options\n const executeConfig = this._buildExecutionConfig({\n default: defaultConfig,\n user: userConfig,\n });\n\n // get user key from context if not provided\n const effectiveUserKey = userKey ?? getCurrentUserId();\n\n const self = this;\n // capture the active OTel context (HTTP span) before entering the async generator,\n // where it would otherwise be lost across the async boundary\n const parentOtelContext = otelContext.active();\n\n // wrapper function to ensure it returns a generator\n const asyncWrapperFn = async function* (streamSignal?: AbortSignal) {\n // build execution context\n const context: InterceptorContext = {\n signal: streamSignal,\n metadata: new Map(),\n userKey: effectiveUserKey,\n };\n\n // build interceptors\n const interceptors = self._buildInterceptors(executeConfig);\n\n // wrap the function to ensure it returns a promise\n const wrappedFn = async () => {\n const result = await fn(context.signal);\n return result;\n };\n\n // execute the function with interceptors, restoring the parent OTel context\n // so telemetry spans are linked as children of the HTTP request span\n const result = await otelContext.with(parentOtelContext, () =>\n self._executeWithInterceptors(\n wrappedFn as (signal?: AbortSignal) => Promise<T>,\n interceptors,\n context,\n ),\n );\n\n // check if result is a generator\n if (self._checkIfGenerator(result)) {\n yield* result;\n } else {\n yield result;\n }\n };\n\n // stream the result to the client. The effective user key is forwarded\n // to the stream manager so that reconnections to existing streamIds are\n // bound to the original creator (prevents cross-user stream takeover via\n // guessed/leaked IDs).\n await this.streamManager.stream(\n res,\n asyncWrapperFn,\n streamConfig,\n effectiveUserKey,\n );\n }\n\n /**\n * Execute a function with the plugin's interceptor chain.\n *\n * Returns an {@link ExecutionResult} discriminated union:\n * - `{ ok: true, data: T }` on success\n * - `{ ok: false, status: number, message: string }` on failure\n *\n * Errors are never thrown — the method is production-safe.\n */\n protected async execute<T>(\n fn: (signal?: AbortSignal) => Promise<T>,\n options: PluginExecutionSettings,\n userKey?: string,\n ): Promise<ExecutionResult<T>> {\n const executeConfig = this._buildExecutionConfig(options);\n\n const interceptors = this._buildInterceptors(executeConfig);\n\n // get user key from context if not provided\n const effectiveUserKey = userKey ?? getCurrentUserId();\n\n const context: InterceptorContext = {\n metadata: new Map(),\n userKey: effectiveUserKey,\n };\n\n try {\n const data = await this._executeWithInterceptors(\n fn,\n interceptors,\n context,\n );\n return { ok: true, data };\n } catch (error) {\n logger.error(\"Plugin execution failed\", { error, plugin: this.name });\n\n if (error instanceof AppKitError) {\n return {\n ok: false,\n status: error.statusCode,\n message: error.message,\n };\n }\n\n if (hasHttpStatusCode(error)) {\n const isDev = process.env.NODE_ENV !== \"production\";\n const isClientError = error.statusCode >= 400 && error.statusCode < 500;\n return {\n ok: false,\n status: error.statusCode,\n message: isDev || isClientError ? error.message : \"Server error\",\n };\n }\n\n const isDev = process.env.NODE_ENV !== \"production\";\n return {\n ok: false,\n status: 500,\n message:\n isDev && error instanceof Error ? error.message : \"Server error\",\n };\n }\n }\n\n protected registerEndpoint(name: string, path: string): void {\n this.registeredEndpoints[name] = path;\n }\n\n protected route<_TResponse>(\n router: express.Router,\n config: RouteConfig,\n ): void {\n const { name, method, path, handler } = config;\n\n router[method](path, forwardAsyncErrors(handler));\n\n const fullPath = `/api/${camelToKebab(this.name)}${path}`;\n this.registerEndpoint(name, fullPath);\n\n if (config.skipBodyParsing) {\n this.skipBodyParsingPaths.add(fullPath);\n }\n }\n\n // build execution options by merging defaults, plugin config, and user overrides\n private _buildExecutionConfig(\n options: PluginExecutionSettings,\n ): PluginExecuteConfig {\n const { default: methodDefaults, user: userOverride } = options;\n\n // Merge: method defaults <- plugin config <- user override (highest priority)\n return deepMerge(\n deepMerge(methodDefaults, this.config),\n userOverride ?? {},\n ) as PluginExecuteConfig;\n }\n\n // build interceptors based on execute options\n private _buildInterceptors(\n options: PluginExecuteConfig,\n ): ExecutionInterceptor[] {\n const interceptors: ExecutionInterceptor[] = [];\n\n // order matters: telemetry → timeout → retry → cache (innermost to outermost)\n\n const telemetryConfig = normalizeTelemetryOptions(this.config.telemetry);\n if (\n telemetryConfig.traces &&\n (options.telemetryInterceptor?.enabled ?? true)\n ) {\n interceptors.push(\n new TelemetryInterceptor(this.telemetry, options.telemetryInterceptor),\n );\n }\n\n if (options.timeout && options.timeout > 0) {\n interceptors.push(new TimeoutInterceptor(options.timeout));\n }\n\n if (\n options.retry?.enabled &&\n options.retry.attempts &&\n options.retry.attempts > 1\n ) {\n interceptors.push(new RetryInterceptor(options.retry));\n }\n\n if (options.cache?.enabled && options.cache.cacheKey?.length) {\n interceptors.push(new CacheInterceptor(this.cache, options.cache));\n }\n\n return interceptors;\n }\n\n // execute method wrapped with interceptors\n private async _executeWithInterceptors<T>(\n fn: (signal?: AbortSignal) => Promise<T>,\n interceptors: ExecutionInterceptor[],\n context: InterceptorContext,\n ): Promise<T> {\n // no interceptors, execute directly\n if (interceptors.length === 0) {\n return fn(context.signal);\n }\n // build nested execution chain from interceptors\n let wrappedFn = () => fn(context.signal);\n\n // wrap each interceptor around the previous function\n for (const interceptor of interceptors) {\n const previousFn = wrappedFn;\n wrappedFn = () => interceptor.intercept(previousFn, context);\n }\n\n return wrappedFn();\n }\n\n private _checkIfGenerator(\n result: any,\n ): result is AsyncGenerator<any, void, unknown> {\n return (\n result && typeof result === \"object\" && Symbol.asyncIterator in result\n );\n }\n}\n"],"mappings":"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"}
|
|
@@ -2,9 +2,9 @@ import { AgentToolDefinition, Thread, ToolProvider } from "../../shared/src/agen
|
|
|
2
2
|
import { IAppRouter, PluginPhase, ToPlugin } from "../../shared/src/plugin.js";
|
|
3
3
|
import "../../shared/src/index.js";
|
|
4
4
|
import { AgentDefinition, AgentsPluginConfig, RegisteredAgent } from "../../core/agent/types.js";
|
|
5
|
+
import { PluginManifest } from "../../registry/types.js";
|
|
5
6
|
import { Plugin } from "../../plugin/plugin.js";
|
|
6
7
|
import "../../plugin/index.js";
|
|
7
|
-
import { PluginManifest } from "../../registry/types.js";
|
|
8
8
|
import "../../index.js";
|
|
9
9
|
|
|
10
10
|
//#region src/plugins/agents/agents.d.ts
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { IAppRouter, ToPlugin } from "../../shared/src/plugin.js";
|
|
2
2
|
import "../../shared/src/index.js";
|
|
3
|
+
import { PluginManifest } from "../../registry/types.js";
|
|
3
4
|
import { Plugin } from "../../plugin/plugin.js";
|
|
4
5
|
import "../../plugin/index.js";
|
|
5
|
-
import { PluginManifest } from "../../registry/types.js";
|
|
6
6
|
import "../../index.js";
|
|
7
7
|
import { IAiSearchConfig, IndexSummary, SearchRequest, SearchResponse } from "./types.js";
|
|
8
8
|
|
|
@@ -3,10 +3,10 @@ import { IAppRouter, ToPlugin } from "../../shared/src/plugin.js";
|
|
|
3
3
|
import { SQLTypeMarker } from "../../shared/src/sql/types.js";
|
|
4
4
|
import "../../shared/src/index.js";
|
|
5
5
|
import { ToolkitEntry, ToolkitOptions } from "../../core/agent/types.js";
|
|
6
|
+
import { PluginManifest } from "../../registry/types.js";
|
|
6
7
|
import { Plugin } from "../../plugin/plugin.js";
|
|
7
8
|
import "../../plugin/index.js";
|
|
8
9
|
import { IAnalyticsConfig } from "./types.js";
|
|
9
|
-
import { PluginManifest } from "../../registry/types.js";
|
|
10
10
|
import "../../index.js";
|
|
11
11
|
import "../agents/index.js";
|
|
12
12
|
import express from "express";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"analytics.d.ts","names":[],"sources":["../../../src/plugins/analytics/analytics.ts"],"mappings":";;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"analytics.d.ts","names":[],"sources":["../../../src/plugins/analytics/analytics.ts"],"mappings":";;;;;;;;;;;;;;cA+Ga,eAAA,SAAwB,MAAA,YAAkB,YAAA;;SAE9C,QAAA,EAFoB,cAAA;EAAA,iBAIV,WAAA;EAAA,UACC,MAAA,EAAQ,gBAAA;EAAA,QAGlB,SAAA;EAAA,QACA,cAAA;;;;AATV;;;;;UAmBU,gBAAA;cAEI,MAAA,EAAQ,gBAAA;EAWpB,YAAA,CAAa,MAAA,EAAQ,UAAA;EA0Cd;;;;;;EAFD,mBAAA,CACJ,GAAA,EAAK,OAAA,CAAQ,OAAA,EACb,GAAA,EAAK,OAAA,CAAQ,QAAA,GACZ,OAAA;EA2RI;;;;;;;EAAA,QAtQO,mBAAA;EAs1BX;;;;;EA1zBG,eAAA,CAAgB,WAAA,WAAsB,OAAA;EA63BkB;;;;EAr3BxD,iBAAA,CACJ,GAAA,EAAK,OAAA,CAAQ,OAAA,EACb,GAAA,EAAK,OAAA,CAAQ,QAAA,GACZ,OAAA;EA8yBQ;;;;;;;;;;;;;;;EAjlBL,kBAAA,CACJ,GAAA,EAAK,OAAA,CAAQ,OAAA,EACb,GAAA,EAAK,OAAA,CAAQ,QAAA,GACZ,OAAA;;;;;;;UA8RW,qBAAA;EA5jBP;;;;;;;;;EAAA,QAylBC,sBAAA;EAtiBoC;;;;;;;;;;;EAAA,QAskB9B,uBAAA;EA7VZ;;;;;;;EAkfI,0BAAA,CAA2B,MAAA,EAAQ,WAAA,GAAc,OAAA;EAAjD;;;;;;;;;;;;;;;;;;EAAA,QAiCE,qBAAA;EA8GS;;;;;;;;;;;;;;;EArDX,KAAA,CACJ,KAAA,UACA,UAAA,GAAa,MAAA,SAAe,aAAA,sBAC5B,gBAAA,GAAmB,MAAA,eACnB,MAAA,GAAS,WAAA,GACR,OAAA;EAqBG,QAAA,CAAA,GAAY,OAAA;EAAA,QAIV,KAAA;EAuBR,aAAA,CAAA,GAAiB,mBAAA;EAIX,gBAAA,CACJ,IAAA,UACA,IAAA,WACA,MAAA,GAAS,WAAA,GACR,OAAA;EAzDQ;;;;;AA0Jb;;EAtFE,OAAA,CAAQ,IAAA,GAXE,cAAA,GAWoD,MAAA,SAAA,YAAA;EAsF1C;;;;EA9EpB,OAAA,CAAA;IA8EoB;;;2BA7JL,UAAA,GACA,MAAA,SAAe,aAAA,sBAAiC,gBAAA,GAC1C,MAAA,eAAmB,MAAA,GAC7B,WAAA,KACR,OAAA;EAAA;AAAA;;;;cAyJQ,SAAA,EAAS,QAAA,QAAA,eAAA,EAAA,gBAAA"}
|
|
@@ -2,14 +2,16 @@ import { makeResultMessage } from "../../shared/src/sse/analytics.js";
|
|
|
2
2
|
import { AppKitError } from "../../errors/base.js";
|
|
3
3
|
import { ExecutionError } from "../../errors/execution.js";
|
|
4
4
|
import "../../errors/index.js";
|
|
5
|
+
import { getWarehouseId } from "../../resources/warehouse.js";
|
|
5
6
|
import { createLogger } from "../../logging/logger.js";
|
|
6
|
-
import {
|
|
7
|
+
import { getWorkspaceClient } from "../../context/execution-context.js";
|
|
7
8
|
import "../../context/index.js";
|
|
8
9
|
import { Plugin } from "../../plugin/plugin.js";
|
|
9
10
|
import { toPlugin } from "../../plugin/to-plugin.js";
|
|
10
11
|
import "../../plugin/index.js";
|
|
11
12
|
import { defineManifest } from "../../registry/manifest-loader.js";
|
|
12
13
|
import "../../registry/index.js";
|
|
14
|
+
import "../../resources/index.js";
|
|
13
15
|
import { DEFAULT_WAREHOUSE_STARTUP_TIMEOUT_MS, SQLWarehouseConnector } from "../../connectors/sql-warehouse/client.js";
|
|
14
16
|
import "../../connectors/index.js";
|
|
15
17
|
import { buildToolkitEntries } from "../../core/agent/build-toolkit.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"analytics.js","names":["manifest"],"sources":["../../../src/plugins/analytics/analytics.ts"],"sourcesContent":["import type express from \"express\";\nimport {\n type AgentToolDefinition,\n type AnalyticsSseMessage,\n type IAppRouter,\n makeResultMessage,\n type PluginExecuteConfig,\n type SQLTypeMarker,\n type StreamExecutionSettings,\n type ToolProvider,\n} from \"shared\";\nimport { z } from \"zod\";\n\nimport { SQLWarehouseConnector } from \"../../connectors\";\nimport {\n DEFAULT_WAREHOUSE_STARTUP_TIMEOUT_MS,\n type WarehouseStatusUpdate,\n} from \"../../connectors/sql-warehouse/client\";\nimport { getWarehouseId, getWorkspaceClient } from \"../../context\";\nimport { buildToolkitEntries } from \"../../core/agent/build-toolkit\";\nimport {\n defineTool,\n executeFromRegistry,\n toolsFromRegistry,\n} from \"../../core/agent/tools/define-tool\";\nimport { assertReadOnlySql } from \"../../core/agent/tools/sql-policy\";\nimport { AppKitError, ExecutionError } from \"../../errors\";\nimport { createLogger } from \"../../logging/logger\";\nimport { Plugin, toPlugin } from \"../../plugin\";\nimport { defineManifest } from \"../../registry\";\nimport type { WorkspaceClient } from \"../../workspace-client\";\nimport { queryDefaults } from \"./defaults\";\nimport manifest from \"./manifest.json\";\nimport {\n buildMetricSql,\n composeMetricCacheKey,\n deriveMetricExecutorKey,\n loadMetricMetadata,\n loadMetricRegistry,\n METRIC_METADATA_FILE,\n selectMetricMetadata,\n validateMetricRequest,\n} from \"./metric\";\nimport { QueryProcessor } from \"./query\";\nimport {\n type ArrowCapability,\n deliverArrowBytes,\n deliverJsonResult,\n type QueryExecutor,\n} from \"./result-delivery\";\nimport {\n type AnalyticsQueryResponse,\n type AnalyticsStreamMessage,\n type IAnalyticsConfig,\n type IAnalyticsQueryRequest,\n type MetricRegistration,\n normalizeAnalyticsFormat,\n type WarehouseStatus,\n} from \"./types\";\n\nconst logger = createLogger(\"analytics\");\n\n/**\n * Bridges a callback-emitting async function into an async iterable.\n *\n * `start(emit)` runs concurrently; every value passed to `emit` is yielded\n * in order. The iterable completes when `start`'s promise resolves and\n * re-throws (after draining) if it rejects. Lets a callback-based progress\n * API (e.g. SQL warehouse readiness) be consumed with `for await`.\n */\nasync function* streamCallbacks<T>(\n start: (emit: (value: T) => void) => Promise<void>,\n): AsyncGenerator<T, void, unknown> {\n const queue: T[] = [];\n let wake: (() => void) | null = null;\n let settled = false;\n let error: unknown = null;\n\n const notify = (): void => {\n wake?.();\n wake = null;\n };\n\n // The .then(_, err => ...) chain converts a rejection into a resolved\n // promise; the consumer surfaces `error` after draining the queue.\n void start((value) => {\n queue.push(value);\n notify();\n }).then(\n () => {\n settled = true;\n notify();\n },\n (err) => {\n error = err;\n settled = true;\n notify();\n },\n );\n\n while (!settled || queue.length > 0) {\n while (queue.length > 0) yield queue.shift() as T;\n if (settled) break;\n await new Promise<void>((resolve) => {\n wake = resolve;\n });\n }\n if (error) throw error;\n}\n\nexport class AnalyticsPlugin extends Plugin implements ToolProvider {\n /** Plugin manifest declaring metadata and resource requirements */\n static manifest = defineManifest<\"analytics\">(manifest);\n\n protected static description = \"Analytics plugin for data analysis\";\n declare protected config: IAnalyticsConfig;\n\n // analytics services\n private SQLClient: SQLWarehouseConnector;\n private queryProcessor: QueryProcessor;\n\n /**\n * In-process memo of which arrow delivery mode each warehouse supports\n * (keyed by warehouse id). A standard warehouse rejects `INLINE+ARROW_STREAM`\n * on every query, so once learned we skip that doomed probe; Reyden stays\n * `\"inline\"`. Capability is a property of the warehouse, not the user, so it\n * is not user-scoped. Bounded by the number of distinct warehouses a process\n * talks to (effectively one).\n */\n private _arrowCapability = new Map<string, ArrowCapability>();\n\n constructor(config: IAnalyticsConfig) {\n super(config);\n this.config = config;\n this.queryProcessor = new QueryProcessor();\n\n this.SQLClient = new SQLWarehouseConnector({\n timeout: config.timeout,\n telemetry: config.telemetry,\n });\n }\n\n injectRoutes(router: IAppRouter) {\n this.route<AnalyticsQueryResponse>(router, {\n name: \"query\",\n method: \"post\",\n path: \"/query/:query_key\",\n handler: async (req: express.Request, res: express.Response) => {\n await this._handleQueryRoute(req, res);\n },\n });\n\n // Metric-view route. Registered parallel to `/query`\n // measures a registered UC Metric View over the same SSE envelope.\n this.route(router, {\n name: \"metric\",\n method: \"post\",\n path: \"/metric/:key\",\n handler: async (req: express.Request, res: express.Response) => {\n await this._handleMetricRoute(req, res);\n },\n });\n\n // Column-names fallback for very wide Arrow schemas whose names don't fit\n // in the `X-Appkit-Arrow-Columns` response header.\n // The client hits this with the statement id from `X-Appkit-Arrow-Columns-Ref`.\n this.route(router, {\n name: \"arrow-columns\",\n method: \"get\",\n path: \"/columns/:statementId\",\n handler: async (req: express.Request, res: express.Response) => {\n await this._handleColumnsRoute(req, res);\n },\n });\n }\n\n /**\n * Column-names fallback endpoint. Re-derives the real column names from the\n * statement's result manifest (stateless — no server cache), for the client\n * to relabel a positional Arrow schema when the names were too large for the\n * response header.\n */\n async _handleColumnsRoute(\n req: express.Request,\n res: express.Response,\n ): Promise<void> {\n const { statementId } = req.params;\n const columns = await this._resolveColumnNames(req, statementId);\n if (columns && columns.length > 0) {\n res.setHeader(\"Cache-Control\", \"no-store\");\n res.json({ columns });\n return;\n }\n res.status(404).json({\n error: \"Column names unavailable\",\n plugin: this.name,\n });\n }\n\n /**\n * Resolve a statement's real column names, trying the user's identity first\n * (required for `.obo.sql` statements, which the service principal cannot\n * `getStatement`) then falling back to the service principal (for\n * SP-executed statements). Returns undefined if neither identity can read it,\n * so the client falls back to the raw positional Arrow schema names.\n */\n private async _resolveColumnNames(\n req: express.Request,\n statementId: string,\n ): Promise<string[] | undefined> {\n const attempts: Array<() => Promise<string[] | undefined>> = [\n () => this.asUser(req)._getColumnNames(statementId),\n () => this._getColumnNames(statementId),\n ];\n for (const attempt of attempts) {\n try {\n const columns = await attempt();\n if (columns && columns.length > 0) return columns;\n } catch (error) {\n logger.debug(\n \"Arrow column-names lookup attempt failed for %s: %O\",\n statementId,\n error,\n );\n }\n }\n return undefined;\n }\n\n /**\n * Fetch column names in the current execution context. Proxied by `asUser`,\n * so `getWorkspaceClient()` resolves to the user's client when invoked via\n * `this.asUser(req)` and the service principal's otherwise.\n */\n async _getColumnNames(statementId: string): Promise<string[] | undefined> {\n return this.SQLClient.getColumnNames(getWorkspaceClient(), statementId);\n }\n\n /**\n * Handle SQL query execution requests.\n * When called via asUser(req), uses the user's Databricks credentials.\n */\n async _handleQueryRoute(\n req: express.Request,\n res: express.Response,\n ): Promise<void> {\n const { query_key } = req.params;\n const { parameters, format: rawFormat = \"JSON_ARRAY\" } =\n req.body as IAnalyticsQueryRequest;\n\n if (\n rawFormat !== \"JSON_ARRAY\" &&\n rawFormat !== \"ARROW_STREAM\" &&\n rawFormat !== \"JSON\" &&\n rawFormat !== \"ARROW\"\n ) {\n res.status(400).json({\n error: `Invalid format: ${String(rawFormat)}. Expected \"JSON_ARRAY\" or \"ARROW_STREAM\".`,\n });\n return;\n }\n\n const format = normalizeAnalyticsFormat(rawFormat);\n\n // Request-scoped logging with WideEvent tracking\n logger.debug(req, \"Executing query: %s (format=%s)\", query_key, format);\n\n const event = logger.event(req);\n event?.setComponent(\"analytics\", \"executeQuery\").setContext(\"analytics\", {\n query_key,\n format,\n parameter_count: parameters ? Object.keys(parameters).length : 0,\n plugin: this.name,\n });\n\n if (!query_key) {\n res.status(400).json({ error: \"query_key is required\" });\n return;\n }\n\n const queryResult = await this.app.getAppQuery(\n query_key,\n req,\n this.devFileReader,\n );\n\n if (!queryResult) {\n res.status(404).json({ error: \"Query not found\" });\n return;\n }\n\n const { query, isAsUser } = queryResult;\n\n // ARROW_STREAM streams the raw Arrow IPC bytes back as the HTTP response\n // body — no SSE, no server-side stash, no second /arrow-result request.\n // INLINE attachments are piped straight through (the bytes are already in\n // hand from executeStatement); a warehouse that refuses INLINE falls back\n // to EXTERNAL_LINKS and streams those chunks. JSON keeps the SSE path\n // below (it carries warehouse-readiness progress + cached rows).\n if (format === \"ARROW_STREAM\") {\n await this._handleArrowStreamQuery(\n req,\n res,\n query_key,\n query,\n isAsUser,\n parameters,\n );\n return;\n }\n\n // get execution context - user-scoped if .obo.sql, otherwise service principal\n const executor = isAsUser ? this.asUser(req) : this;\n const executorKey = isAsUser ? this.resolveUserId(req) : \"global\";\n\n const hashedQuery = this.queryProcessor.hashQuery(query);\n\n const cacheConfig = {\n ...queryDefaults.cache,\n cacheKey: [\n \"analytics:query\",\n query_key,\n JSON.stringify(parameters),\n format,\n hashedQuery,\n executorKey,\n ],\n };\n\n // Cache/retry/timeout are scoped to the SQL execution itself (inner\n // `execute`) so the warehouse-readiness phase isn't subject to retries\n // and the generator value never leaks into the cache.\n const sqlConfig: PluginExecuteConfig = {\n ...queryDefaults,\n cache: cacheConfig,\n };\n\n // Outer stream: no cache/retry — `executeStream` would otherwise wrap the\n // generator factory and cache the generator object itself. Telemetry +\n // user-scoped trace context still apply.\n const streamExecutionSettings: StreamExecutionSettings = {\n default: {\n cache: { enabled: false },\n retry: { enabled: false },\n },\n };\n\n const startupTimeoutMs =\n this.config.warehouseStartupTimeoutMs ??\n DEFAULT_WAREHOUSE_STARTUP_TIMEOUT_MS;\n const autoStartWarehouse = this.config.autoStartWarehouse ?? true;\n\n const self = this;\n\n await executor.executeStream(\n res,\n async function* (\n signal,\n ): AsyncGenerator<AnalyticsStreamMessage, void, unknown> {\n const workspaceClient = getWorkspaceClient();\n const warehouseId = await getWarehouseId();\n\n // Stream warehouse-readiness updates as SSE events, then run SQL.\n const readinessUpdates = streamCallbacks<WarehouseStatusUpdate>(\n (emit) =>\n self.SQLClient.ensureWarehouseRunning(\n workspaceClient,\n warehouseId,\n {\n signal,\n timeoutMs: startupTimeoutMs,\n autoStart: autoStartWarehouse,\n onStatus: emit,\n },\n ),\n );\n for await (const update of readinessUpdates) {\n yield {\n type: \"warehouse_status\",\n status: {\n state: update.state as WarehouseStatus[\"state\"],\n elapsedMs: update.elapsedMs,\n },\n };\n }\n\n // `execute()` reduces a thrown error to `{ status, message }`,\n // dropping the rich fields (`errorCode`, `clientMessage`) the\n // fallback's `ExecutionError`s carry. Capture the original here so\n // we can re-throw it intact — the SSE error path\n // (`StreamManager`) reads `errorCode`/`clientMessage` off it.\n let originalError: unknown;\n const sqlResult = await executor.execute(\n async (sig) => {\n try {\n const processedParams =\n await self.queryProcessor.processQueryParams(query, parameters);\n // JSON_ARRAY path: tries INLINE + JSON_ARRAY and, if the\n // warehouse only accepts ARROW_STREAM for INLINE, retries as\n // ARROW_STREAM and decodes server-side — returning the SSE\n // `result` message with plain rows. (ARROW_STREAM requests are\n // handled earlier via `_handleArrowStreamQuery`.)\n return await self._executeJsonArrayPath(\n executor,\n query,\n processedParams,\n sig,\n );\n } catch (err) {\n originalError = err;\n throw err;\n }\n },\n { default: sqlConfig },\n executorKey,\n );\n\n if (!sqlResult.ok) {\n const msg = sqlResult.message;\n const lower = msg.toLowerCase();\n if (\n lower.includes(\"operation was aborted\") ||\n lower.includes(\"the request was aborted\") ||\n lower.includes(\"statement was canceled\")\n ) {\n const err = new DOMException(\n lower.includes(\"canceled\") ? msg : \"The operation was aborted.\",\n \"AbortError\",\n );\n throw err;\n }\n // Re-throw the original error so its structured `errorCode` (e.g.\n // RESULT_TOO_LARGE_FOR_JSON_FALLBACK) and sanitized `clientMessage`\n // survive to the SSE error payload. Fall back to a generic\n // statement failure only if the original wasn't an AppKitError.\n if (originalError instanceof AppKitError) {\n throw originalError;\n }\n const inner = msg.startsWith(\"Statement failed: \")\n ? msg.slice(\"Statement failed: \".length)\n : msg;\n throw ExecutionError.statementFailed(inner);\n }\n\n yield sqlResult.data as AnalyticsStreamMessage;\n },\n streamExecutionSettings,\n executorKey,\n );\n }\n\n /**\n * Handle metric-view execution requests (`POST /api/analytics/metric/:key`).\n *\n * Mirrors {@link _handleQueryRoute}'s JSON SSE path: the outer\n * `executeStream` disables cache/retry and streams warehouse-readiness\n * (`warehouse_status`) events, then the inner `execute` builds the metric SQL\n * and delivers rows through {@link deliverJsonResult} as a `result` message.\n * The `originalError` re-throw discipline preserves each error's structured\n * `errorCode`/`clientMessage` for the SSE error payload.\n *\n * Lane dispatch is driven by the registration: an SP-lane metric runs as the\n * app service principal (shared cache); an OBO-lane metric runs\n * on-behalf-of the requesting user via `asUser(req)` (per-user cache keyed by\n * a hash of the user identity).\n */\n async _handleMetricRoute(\n req: express.Request,\n res: express.Response,\n ): Promise<void> {\n const { key } = req.params;\n\n logger.debug(req, \"Executing metric: %s\", key);\n\n const event = logger.event(req);\n event?.setComponent(\"analytics\", \"executeMetric\").setContext(\"analytics\", {\n metric_key: key,\n plugin: this.name,\n });\n\n if (!key) {\n res.status(400).json({ error: \"metric key is required\" });\n return;\n }\n\n // Resolve the registry from disk: read + parse `definitions.json` once per\n // request (no memoization). Reads through the plugin's shared `this.app`\n // (the base `Plugin`'s `AppManager`) from `config/metric-views/` under the\n // process cwd, so this is dev-tunnel aware and inherits the traversal\n // guard — the same mechanism as the sibling `.sql` query path.\n let registry: Record<string, MetricRegistration>;\n try {\n registry = await loadMetricRegistry(this.app, req, this.devFileReader);\n } catch (err) {\n const reason = err instanceof Error ? err.message : String(err);\n logger.warn(req, \"Failed to load metric registry: %s\", reason);\n event?.setContext(\"analytics\", {\n metric_registry_load_error: reason,\n });\n res.status(503).json({\n error: \"Metric registry not available\",\n code: \"METRIC_REGISTRY_LOAD_FAILED\",\n });\n return;\n }\n\n // Own-property lookup: never resolve `key` to an inherited `Object.prototype` member.\n const registration = Object.hasOwn(registry, key)\n ? registry[key]\n : undefined;\n if (!registration) {\n // Don't echo the user-supplied `key` back in the public response.\n event?.setContext(\"analytics\", { unknown_metric_key: key });\n res.status(404).json({ error: \"Metric not found\" });\n return;\n }\n\n // Validate the body on the canonical error path.\n // `validateMetricRequest` throws a `ValidationError` (400) whose message names only field paths, never raw values.\n let request: ReturnType<typeof validateMetricRequest>;\n try {\n request = validateMetricRequest(req.body ?? {});\n } catch (err) {\n if (err instanceof AppKitError) {\n res.status(err.statusCode).json({ error: err.message, code: err.code });\n return;\n }\n event?.setContext(\"analytics\", {\n unexpected_error: err instanceof Error ? err.message : String(err),\n metric_key: key,\n });\n logger.warn(\n req,\n \"Unexpected throw during metric request validation for %s: %s\",\n key,\n err instanceof Error ? err.message : String(err),\n );\n res.status(400).json({ error: \"Invalid request body\" });\n return;\n }\n\n // Lane dispatch. The lane comes from the registration (the entry's\n // `executor` in definitions.json), NOT a URL segment or `.obo.sql`\n // filename: an OBO-lane metric runs on-behalf-of the requesting user\n // (per-user cache via `asUser(req)`), an SP-lane metric as the app service\n // principal (shared cache).\n let executor: AnalyticsPlugin;\n let executorKey: string;\n try {\n const isObo = registration.lane === \"obo\";\n executor = isObo ? this.asUser(req) : this;\n executorKey = deriveMetricExecutorKey({\n lane: registration.lane,\n userIdentity: isObo ? this.resolveUserId(req) : undefined,\n });\n } catch (err) {\n if (err instanceof AppKitError) {\n res.status(err.statusCode).json({ error: err.message, code: err.code });\n return;\n }\n throw err;\n }\n\n const injectedMetadata = this.config.metricViewsMetadata;\n const allMetadata =\n injectedMetadata ??\n (await loadMetricMetadata(this.app, req, this.devFileReader));\n\n if (allMetadata !== undefined && !Object.hasOwn(allMetadata, key)) {\n if (injectedMetadata !== undefined) {\n logger.warn(\n req,\n \"No display metadata for metric key %s in the injected metricViewsMetadata\",\n key,\n );\n } else {\n logger.warn(\n req,\n \"No display metadata for metric key %s — regenerate types to refresh %s\",\n key,\n METRIC_METADATA_FILE,\n );\n }\n }\n\n // Computed here, outside the cached execute below, so a cache hit still\n // serves the current metadata. Absent config → `undefined` → the `result`\n // message omits the field (envelope-identical to `/query`).\n const metadata = selectMetricMetadata(\n allMetadata,\n key,\n request.measures,\n request.dimensions,\n );\n\n // Cache key. Composed over the canonicalized args (sorted measures/\n // dimensions, stable-sorted predicates, grain, timeDimension, limit, orderBy) plus\n // the `executorKey` — `\"sp\"` shares the cache across all users, a per-user\n // identity hash isolates OBO callers.\n const cacheConfig = {\n ...queryDefaults.cache,\n cacheKey: composeMetricCacheKey({\n metricKey: key,\n source: registration.source,\n measures: request.measures,\n dimensions: request.dimensions,\n timeGrain: request.timeGrain,\n timeDimension: request.timeDimension,\n filter: request.filter,\n format: \"JSON_ARRAY\",\n executorKey,\n limit: request.limit,\n orderBy: request.orderBy,\n }),\n };\n\n // Cache/retry/timeout scoped to the SQL execution itself (inner `execute`)\n // so the warehouse-readiness phase isn't retried and the generator value\n // never leaks into the cache.\n const sqlConfig: PluginExecuteConfig = {\n ...queryDefaults,\n cache: cacheConfig,\n };\n\n // Outer stream: no cache/retry — `executeStream` would otherwise wrap the\n // generator factory and cache the generator object itself. Telemetry +\n // trace context still apply.\n const streamExecutionSettings: StreamExecutionSettings = {\n default: {\n cache: { enabled: false },\n retry: { enabled: false },\n },\n };\n\n const startupTimeoutMs =\n this.config.warehouseStartupTimeoutMs ??\n DEFAULT_WAREHOUSE_STARTUP_TIMEOUT_MS;\n const autoStartWarehouse = this.config.autoStartWarehouse ?? true;\n\n const self = this;\n\n await executor.executeStream(\n res,\n async function* (\n signal,\n ): AsyncGenerator<AnalyticsStreamMessage, void, unknown> {\n const workspaceClient = getWorkspaceClient();\n const warehouseId = await getWarehouseId();\n\n // Stream warehouse-readiness updates as SSE events, then run SQL.\n const readinessUpdates = streamCallbacks<WarehouseStatusUpdate>(\n (emit) =>\n self.SQLClient.ensureWarehouseRunning(\n workspaceClient,\n warehouseId,\n {\n signal,\n timeoutMs: startupTimeoutMs,\n autoStart: autoStartWarehouse,\n onStatus: emit,\n },\n ),\n );\n for await (const update of readinessUpdates) {\n yield {\n type: \"warehouse_status\",\n status: {\n state: update.state as WarehouseStatus[\"state\"],\n elapsedMs: update.elapsedMs,\n },\n };\n }\n\n // `execute()` reduces a thrown error to `{ status, message }`,\n // dropping the rich fields (`errorCode`, `clientMessage`). Capture the\n // original here so we can re-throw it intact — the SSE error path\n // (`StreamManager`) reads `errorCode`/`clientMessage` off it.\n let originalError: unknown;\n const sqlResult = await executor.execute(\n async (sig) => {\n try {\n const { statement, parameters } = buildMetricSql(\n registration,\n request,\n );\n const processedParams =\n await self.queryProcessor.processQueryParams(\n statement,\n Object.keys(parameters).length > 0 ? parameters : undefined,\n );\n // Reuse the query route's JSON delivery: INLINE JSON_ARRAY with\n // an ARROW_STREAM-inline fallback, returning plain rows in a\n // `result` message — byte-identical envelope to `/query`.\n return await self._executeJsonArrayPath(\n executor,\n statement,\n processedParams,\n sig,\n );\n } catch (err) {\n originalError = err;\n throw err;\n }\n },\n { default: sqlConfig },\n executorKey,\n );\n\n if (!sqlResult.ok) {\n const msg = sqlResult.message;\n const lower = msg.toLowerCase();\n if (\n lower.includes(\"operation was aborted\") ||\n lower.includes(\"the request was aborted\") ||\n lower.includes(\"statement was canceled\")\n ) {\n const err = new DOMException(\n lower.includes(\"canceled\") ? msg : \"The operation was aborted.\",\n \"AbortError\",\n );\n throw err;\n }\n // Re-throw the original error so its structured `errorCode` and\n // sanitized `clientMessage` survive to the SSE error payload. Fall\n // back to a generic statement failure only if it wasn't an\n // AppKitError.\n if (originalError instanceof AppKitError) {\n throw originalError;\n }\n const inner = msg.startsWith(\"Statement failed: \")\n ? msg.slice(\"Statement failed: \".length)\n : msg;\n throw ExecutionError.statementFailed(inner);\n }\n\n // Stamp the metadata onto the (possibly cached) result message; the\n // cached message never carries it.\n const resultMessage = sqlResult.data as AnalyticsSseMessage;\n yield (\n metadata !== undefined\n ? { ...resultMessage, metadata }\n : resultMessage\n ) as AnalyticsStreamMessage;\n },\n streamExecutionSettings,\n executorKey,\n );\n }\n\n /**\n * JSON_ARRAY SSE path. Delegates the disposition/format fallback to\n * {@link deliverJsonResult} (INLINE JSON_ARRAY → on `needs-arrow-inline`,\n * INLINE ARROW_STREAM decoded to rows) and wraps the rows in a `result`\n * message. External links are never used for the JSON fallback.\n */\n private async _executeJsonArrayPath(\n executor: AnalyticsPlugin,\n query: string,\n processedParams:\n | Record<string, SQLTypeMarker | null | undefined>\n | undefined,\n signal?: AbortSignal,\n ): Promise<AnalyticsSseMessage> {\n const result = await deliverJsonResult(\n executor,\n query,\n processedParams,\n signal,\n );\n return makeResultMessage(result.data, {\n status: result.status,\n statement_id: result.statement_id,\n });\n }\n\n /**\n * Attach the real column names so the client can relabel the positional\n * Arrow schema (Databricks encodes ARROW_STREAM columns as col_0, …).\n *\n * Small schemas ride `X-Appkit-Arrow-Columns` directly. A very wide schema\n * whose URL-encoded names would blow the HTTP header size limit instead\n * advertises the statement id in `X-Appkit-Arrow-Columns-Ref`, and the\n * client fetches the names from `GET /columns/:statementId`.\n */\n private _setArrowColumnsHeader(\n res: express.Response,\n columnsRef: { columnNames?: string[]; statementId?: string },\n ): void {\n const names = columnsRef.columnNames;\n if (!names || names.length === 0) return;\n\n const encoded = encodeURIComponent(JSON.stringify(names));\n if (encoded.length <= MAX_ARROW_COLUMNS_HEADER_BYTES) {\n res.setHeader(\"X-Appkit-Arrow-Columns\", encoded);\n return;\n }\n if (columnsRef.statementId) {\n res.setHeader(\"X-Appkit-Arrow-Columns-Ref\", columnsRef.statementId);\n } else {\n logger.warn(\n \"Arrow column names exceed the header limit and no statement id is available for the fallback endpoint; client will fall back to the raw schema names\",\n );\n }\n }\n\n /**\n * ARROW_STREAM query handler: stream the raw Arrow IPC bytes back as the\n * HTTP response body — no SSE, no server-side stash, no second\n * `/arrow-result` request.\n *\n * The first chunk is pulled before headers are sent so a failure still\n * yields a clean JSON error; once bytes are in flight a mid-stream failure\n * can only abort the socket. Warehouse readiness is awaited (no SSE\n * progress on this path) — a no-op for a warm warehouse, a blocking wait\n * on a cold start. Runs under the user's context for `.obo.sql` queries.\n */\n private async _handleArrowStreamQuery(\n req: express.Request,\n res: express.Response,\n query_key: string,\n query: string,\n isAsUser: boolean,\n parameters: IAnalyticsQueryRequest[\"parameters\"],\n ): Promise<void> {\n const executor = isAsUser ? this.asUser(req) : this;\n const executorKey = isAsUser ? this.resolveUserId(req) : \"global\";\n const abortController = new AbortController();\n const onClose = () => abortController.abort();\n res.on(\"close\", onClose);\n const signal = abortController.signal;\n\n // Fail-fast: bound the wait for the first byte (warehouse readiness +\n // execute + first chunk) so a stuck/overloaded warehouse returns a clear\n // 503 instead of hanging until the client gives up. Cleared once the\n // first chunk arrives — a legitimately long stream is never interrupted.\n const firstByteTimeoutMs =\n this.config.arrowFirstByteTimeoutMs ??\n DEFAULT_ARROW_FIRST_BYTE_TIMEOUT_MS;\n let timedOut = false;\n const failFast = setTimeout(() => {\n timedOut = true;\n abortController.abort();\n }, firstByteTimeoutMs);\n\n try {\n // Run warehouse readiness in the SAME identity context as the query:\n // for `.obo.sql`, `executor` is the `asUser(req)` proxy, so\n // `getWorkspaceClient()` inside resolves to the user's client (matching\n // the SSE path). Calling it bare here would auto-start the warehouse as\n // the service principal even for OBO requests.\n await executor._ensureArrowWarehouseReady(signal);\n\n const processedParams = await this.queryProcessor.processQueryParams(\n query,\n parameters,\n );\n\n const warehouseId = await getWarehouseId();\n\n // Populated by `deliverArrowBytes` from the result manifest before the\n // first chunk is yielded, so the header below carries the real names.\n const columnsRef: { columnNames?: string[]; statementId?: string } = {};\n const bytes = deliverArrowBytes(\n // Wrap the executor so the INLINE attempt runs through the interceptor\n // chain (cache + retry), matching the JSON path. Only inline attachments\n // are cached; EXTERNAL_LINKS carry expiring pre-signed URLs and are\n // never cached (see `_arrowCachingExecutor`).\n this._arrowCachingExecutor(\n executor,\n query_key,\n query,\n parameters,\n executorKey,\n ),\n this.SQLClient,\n query,\n processedParams,\n columnsRef,\n signal,\n {\n // Skip the doomed INLINE probe on a warehouse already known to need\n // EXTERNAL_LINKS; remember the resolved mode for next time.\n capabilityHint: this._arrowCapability.get(warehouseId),\n onCapabilityResolved: (capability) =>\n this._arrowCapability.set(warehouseId, capability),\n },\n );\n const first = await bytes.next();\n // First byte in hand — stop the fail-fast clock.\n clearTimeout(failFast);\n\n res.setHeader(\"Content-Type\", \"application/vnd.apache.arrow.stream\");\n res.setHeader(\"Cache-Control\", \"no-store\");\n this._setArrowColumnsHeader(res, columnsRef);\n\n if (!first.done) {\n await writeChunk(res, first.value);\n for await (const buf of bytes) {\n await writeChunk(res, buf);\n }\n }\n res.end();\n } catch (error) {\n clearTimeout(failFast);\n // Fail-fast timeout: the warehouse never produced a first byte. This also\n // aborts the signal, so it must be handled before the generic\n // `signal.aborted` branch below. Headers aren't sent yet (we time out\n // before the first chunk), so a clean 503 is still possible.\n if (timedOut) {\n logger.warn(\n \"Arrow query timed out before first byte after %dms\",\n firstByteTimeoutMs,\n );\n res.status(503).json({\n error:\n \"The SQL warehouse is starting or overloaded and did not respond in time. Please retry.\",\n errorCode: \"WAREHOUSE_UNAVAILABLE\",\n plugin: this.name,\n });\n return;\n }\n // Client disconnect / unmount aborts the signal (see `onClose`). That's\n // routine UI behavior, not a server error — tear down quietly whether it\n // fires before or after headers. Checked before the headersSent branch so\n // a mid-stream disconnect doesn't spam ERROR logs / alerting.\n if (signal.aborted) {\n if (res.headersSent) res.destroy();\n else res.end();\n return;\n }\n if (res.headersSent) {\n logger.error(\"Arrow query stream failed mid-flight: %O\", error);\n res.destroy(error instanceof Error ? error : new Error(String(error)));\n return;\n }\n logger.error(\"Arrow query error: %O\", error);\n // Do not echo upstream / SDK error text — it can include statement\n // fragments and correlation ids. Keep the structured code so the\n // client can branch (e.g. RESULT_TOO_LARGE_FOR_JSON_FALLBACK,\n // ARROW_DELIVERY_UNSUPPORTED).\n const errorCode =\n error instanceof ExecutionError ? error.errorCode : undefined;\n res.status(500).json({\n // `clientMessage` is the sanitized, actionable text (e.g. \"Re-run with\n // JSON_ARRAY\" for ARROW_DELIVERY_UNSUPPORTED); it never carries raw\n // warehouse/SDK strings. Fall back to a generic message otherwise.\n error:\n error instanceof AppKitError\n ? error.clientMessage\n : \"Unable to execute query\",\n errorCode,\n plugin: this.name,\n });\n } finally {\n res.off(\"close\", onClose);\n }\n }\n\n /**\n * Await SQL warehouse readiness for the direct-binary Arrow path. There is no\n * SSE progress channel here — readiness is simply awaited, bounded by the\n * caller's abort signal / fail-fast timeout. Invoked via the request executor\n * (`asUser(req)` for `.obo.sql`) so `getWorkspaceClient()` resolves in the\n * correct identity context rather than defaulting to the service principal.\n */\n async _ensureArrowWarehouseReady(signal: AbortSignal): Promise<void> {\n const workspaceClient = getWorkspaceClient();\n const warehouseId = await getWarehouseId();\n const startupTimeoutMs =\n this.config.warehouseStartupTimeoutMs ??\n DEFAULT_WAREHOUSE_STARTUP_TIMEOUT_MS;\n const autoStart = this.config.autoStartWarehouse ?? true;\n await this.SQLClient.ensureWarehouseRunning(workspaceClient, warehouseId, {\n signal,\n timeoutMs: startupTimeoutMs,\n autoStart,\n onStatus: () => {},\n });\n }\n\n /**\n * Wrap an executor so the `INLINE + ARROW_STREAM` attempt is served from\n * (and populates) the same per-user TTL cache the JSON path uses — otherwise\n * every arrow chart render is a fresh warehouse execution, unlike its JSON\n * twin. Caching is deliberately scoped to inline attachments:\n *\n * - INLINE results carry a bounded (<=25 MiB) base64 `attachment` with the\n * same lifecycle as cached JSON rows — safe to cache.\n * - EXTERNAL_LINKS results carry short-lived pre-signed URLs that expire in\n * minutes; caching them would serve dead links, so those pass through\n * uncached (only the tiny link metadata would be cached anyway).\n *\n * Uses `this.cache.getOrExecute` directly rather than `this.execute()`\n * because `execute()` reduces a thrown error to `{ ok:false, message }`,\n * dropping the `errorCode` the capability fallback classifies on. The cache\n * re-throws `AppKitError`s intact and never caches a rejection, so the\n * INLINE→EXTERNAL_LINKS fallback still sees the structured rejection.\n */\n private _arrowCachingExecutor(\n executor: AnalyticsPlugin,\n query_key: string,\n query: string,\n parameters: IAnalyticsQueryRequest[\"parameters\"],\n executorKey: string,\n ): QueryExecutor {\n const hashedQuery = this.queryProcessor.hashQuery(query);\n const cache = this.cache;\n const ttl = queryDefaults.cache?.ttl;\n return {\n query: (q, params, formatParameters, signal) => {\n // Only the inline-arrow attempt is cacheable — EXTERNAL_LINKS carry\n // short-lived pre-signed URLs, so those pass straight through.\n if (\n formatParameters.disposition !== \"INLINE\" ||\n formatParameters.format !== \"ARROW_STREAM\"\n ) {\n return executor.query(q, params, formatParameters, signal);\n }\n // On a standard warehouse this throws a capability rejection — the\n // cache never stores a rejection, so the fallback still sees the\n // structured error. On Reyden it returns a bounded (<=25 MiB)\n // attachment that caches like the JSON path's rows. The shared signal\n // dedupes concurrent renders (e.g. React StrictMode double-mount).\n return cache.getOrExecute(\n [\n \"analytics:query:arrow\",\n query_key,\n JSON.stringify(parameters),\n hashedQuery,\n executorKey,\n ],\n (sharedSignal) =>\n executor.query(q, params, formatParameters, sharedSignal ?? signal),\n executorKey,\n { ttl, callerSignal: signal },\n );\n },\n };\n }\n\n /**\n * Execute a SQL query using the current execution context.\n *\n * When called directly: uses service principal credentials.\n * When called via asUser(req).query(...): uses user's credentials.\n *\n * @example\n * ```typescript\n * // Service principal execution\n * const result = await analytics.query(\"SELECT * FROM table\")\n *\n * // User context execution (in route handler)\n * const result = await this.asUser(req).query(\"SELECT * FROM table\")\n * ```\n */\n async query(\n query: string,\n parameters?: Record<string, SQLTypeMarker | null | undefined>,\n formatParameters?: Record<string, any>,\n signal?: AbortSignal,\n ): Promise<any> {\n const workspaceClient = getWorkspaceClient();\n const warehouseId = await getWarehouseId();\n\n const { statement, parameters: sqlParameters } =\n this.queryProcessor.convertToSQLParameters(query, parameters);\n\n const response = await this.SQLClient.executeStatement(\n workspaceClient,\n {\n statement,\n warehouse_id: warehouseId,\n parameters: sqlParameters,\n ...formatParameters,\n },\n signal,\n );\n\n return response.result;\n }\n\n async shutdown(): Promise<void> {\n this.streamManager.abortAll();\n }\n\n private tools = {\n query: defineTool({\n description:\n \"Execute a read-only SQL query against the Databricks SQL warehouse. Only SELECT, WITH, SHOW, EXPLAIN, and DESCRIBE statements are accepted; writes are rejected. Returns the query results as JSON.\",\n schema: z.object({\n query: z\n .string()\n .describe(\n \"The SQL query to execute. Must be a SELECT, WITH, SHOW, EXPLAIN, or DESCRIBE statement.\",\n ),\n }),\n annotations: {\n effect: \"read\",\n requiresUserContext: true,\n },\n autoInheritable: true,\n execute: (args, signal) => {\n assertReadOnlySql(args.query);\n return this.query(args.query, undefined, undefined, signal);\n },\n }),\n };\n\n getAgentTools(): AgentToolDefinition[] {\n return toolsFromRegistry(this.tools);\n }\n\n async executeAgentTool(\n name: string,\n args: unknown,\n signal?: AbortSignal,\n ): Promise<unknown> {\n return executeFromRegistry(this.tools, name, args, signal);\n }\n\n /**\n * Returns the plugin's tools as a keyed record of `ToolkitEntry` markers.\n * Called by the agents plugin (via `resolveToolkitFromProvider`) to spread\n * a filtered, renamed view of the plugin's tools into an agent's tool\n * index. Inside the function form of `AgentDefinition.tools`, callers\n * reach this method via `plugins.analytics.toolkit(opts)`.\n */\n toolkit(opts?: import(\"../../core/agent/types\").ToolkitOptions) {\n return buildToolkitEntries(this.name, this.tools, opts);\n }\n\n /**\n * Returns the public exports for the analytics plugin.\n * Note: `asUser()` is automatically added by AppKit.\n */\n exports() {\n return {\n /**\n * Execute a SQL query using service principal credentials.\n */\n query: this.query,\n };\n }\n}\n\n/**\n * Write one chunk to the response honoring backpressure: if the socket\n * buffer is full (`res.write` returns false), wait for `drain` before\n * resolving so a slow client can't balloon Node's internal write queue and\n * defeat the constant-memory goal of streaming.\n *\n * @internal exported for unit testing the backpressure/disconnect behavior.\n */\nexport function writeChunk(\n res: express.Response,\n bytes: Uint8Array,\n): Promise<void> {\n const buf = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);\n // If the socket is already gone, `res.write` won't return true and the\n // `drain`/`close`/`error` events have already fired — so the promise below\n // would never settle, wedging the for-await loop and the upstream reader.\n // Reject up front instead.\n if (res.destroyed || res.writableEnded) {\n return Promise.reject(\n new DOMException(\"The response stream closed\", \"AbortError\"),\n );\n }\n if (res.write(buf)) return Promise.resolve();\n // Backpressured: resolve on `drain`, but also settle on `close`/`error`. A\n // client that disconnects mid-backpressure never emits `drain` on the\n // destroyed socket, so waiting on `drain` alone would wedge this promise —\n // and with it the awaiting for-await loop and the upstream Arrow reader —\n // forever. Rejecting instead unwinds the stream so its `finally` can cancel\n // the reader.\n return new Promise<void>((resolve, reject) => {\n const cleanup = () => {\n res.off(\"drain\", onDrain);\n res.off(\"close\", onClose);\n res.off(\"error\", onClose);\n };\n const onDrain = () => {\n cleanup();\n resolve();\n };\n const onClose = () => {\n cleanup();\n reject(new DOMException(\"The response stream closed\", \"AbortError\"));\n };\n res.once(\"drain\", onDrain);\n res.once(\"close\", onClose);\n res.once(\"error\", onClose);\n });\n}\n\n/**\n * Fail-fast ceiling on the wait for the first Arrow byte (warehouse\n * readiness + execute + first chunk). Past this a stuck/overloaded warehouse\n * yields a clear 503 instead of hanging. Override per plugin via\n * `arrowFirstByteTimeoutMs`.\n */\nconst DEFAULT_ARROW_FIRST_BYTE_TIMEOUT_MS = 120_000;\n\n/**\n * Byte ceiling for the `X-Appkit-Arrow-Columns` header value. Beyond this (a\n * very wide schema) the names are served via the `/columns/:statementId`\n * fallback endpoint instead of the header. Kept well under the common ~8 KiB\n * per-header limit.\n */\nconst MAX_ARROW_COLUMNS_HEADER_BYTES = 6000;\n\n/**\n * @internal\n */\nexport const analytics = toPlugin(AnalyticsPlugin);\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"analytics.js","names":["manifest"],"sources":["../../../src/plugins/analytics/analytics.ts"],"sourcesContent":["import type express from \"express\";\nimport {\n type AgentToolDefinition,\n type AnalyticsSseMessage,\n type IAppRouter,\n makeResultMessage,\n type PluginExecuteConfig,\n type SQLTypeMarker,\n type StreamExecutionSettings,\n type ToolProvider,\n} from \"shared\";\nimport { z } from \"zod\";\n\nimport { SQLWarehouseConnector } from \"../../connectors\";\nimport {\n DEFAULT_WAREHOUSE_STARTUP_TIMEOUT_MS,\n type WarehouseStatusUpdate,\n} from \"../../connectors/sql-warehouse/client\";\nimport { getWorkspaceClient } from \"../../context\";\nimport { buildToolkitEntries } from \"../../core/agent/build-toolkit\";\nimport {\n defineTool,\n executeFromRegistry,\n toolsFromRegistry,\n} from \"../../core/agent/tools/define-tool\";\nimport { assertReadOnlySql } from \"../../core/agent/tools/sql-policy\";\nimport { AppKitError, ExecutionError } from \"../../errors\";\nimport { createLogger } from \"../../logging/logger\";\nimport { Plugin, toPlugin } from \"../../plugin\";\nimport { defineManifest } from \"../../registry\";\nimport { getWarehouseId } from \"../../resources\";\nimport type { WorkspaceClient } from \"../../workspace-client\";\nimport { queryDefaults } from \"./defaults\";\nimport manifest from \"./manifest.json\";\nimport {\n buildMetricSql,\n composeMetricCacheKey,\n deriveMetricExecutorKey,\n loadMetricMetadata,\n loadMetricRegistry,\n METRIC_METADATA_FILE,\n selectMetricMetadata,\n validateMetricRequest,\n} from \"./metric\";\nimport { QueryProcessor } from \"./query\";\nimport {\n type ArrowCapability,\n deliverArrowBytes,\n deliverJsonResult,\n type QueryExecutor,\n} from \"./result-delivery\";\nimport {\n type AnalyticsQueryResponse,\n type AnalyticsStreamMessage,\n type IAnalyticsConfig,\n type IAnalyticsQueryRequest,\n type MetricRegistration,\n normalizeAnalyticsFormat,\n type WarehouseStatus,\n} from \"./types\";\n\nconst logger = createLogger(\"analytics\");\n\n/**\n * Bridges a callback-emitting async function into an async iterable.\n *\n * `start(emit)` runs concurrently; every value passed to `emit` is yielded\n * in order. The iterable completes when `start`'s promise resolves and\n * re-throws (after draining) if it rejects. Lets a callback-based progress\n * API (e.g. SQL warehouse readiness) be consumed with `for await`.\n */\nasync function* streamCallbacks<T>(\n start: (emit: (value: T) => void) => Promise<void>,\n): AsyncGenerator<T, void, unknown> {\n const queue: T[] = [];\n let wake: (() => void) | null = null;\n let settled = false;\n let error: unknown = null;\n\n const notify = (): void => {\n wake?.();\n wake = null;\n };\n\n // The .then(_, err => ...) chain converts a rejection into a resolved\n // promise; the consumer surfaces `error` after draining the queue.\n void start((value) => {\n queue.push(value);\n notify();\n }).then(\n () => {\n settled = true;\n notify();\n },\n (err) => {\n error = err;\n settled = true;\n notify();\n },\n );\n\n while (!settled || queue.length > 0) {\n while (queue.length > 0) yield queue.shift() as T;\n if (settled) break;\n await new Promise<void>((resolve) => {\n wake = resolve;\n });\n }\n if (error) throw error;\n}\n\nexport class AnalyticsPlugin extends Plugin implements ToolProvider {\n /** Plugin manifest declaring metadata and resource requirements */\n static manifest = defineManifest<\"analytics\">(manifest);\n\n protected static description = \"Analytics plugin for data analysis\";\n declare protected config: IAnalyticsConfig;\n\n // analytics services\n private SQLClient: SQLWarehouseConnector;\n private queryProcessor: QueryProcessor;\n\n /**\n * In-process memo of which arrow delivery mode each warehouse supports\n * (keyed by warehouse id). A standard warehouse rejects `INLINE+ARROW_STREAM`\n * on every query, so once learned we skip that doomed probe; Reyden stays\n * `\"inline\"`. Capability is a property of the warehouse, not the user, so it\n * is not user-scoped. Bounded by the number of distinct warehouses a process\n * talks to (effectively one).\n */\n private _arrowCapability = new Map<string, ArrowCapability>();\n\n constructor(config: IAnalyticsConfig) {\n super(config);\n this.config = config;\n this.queryProcessor = new QueryProcessor();\n\n this.SQLClient = new SQLWarehouseConnector({\n timeout: config.timeout,\n telemetry: config.telemetry,\n });\n }\n\n injectRoutes(router: IAppRouter) {\n this.route<AnalyticsQueryResponse>(router, {\n name: \"query\",\n method: \"post\",\n path: \"/query/:query_key\",\n handler: async (req: express.Request, res: express.Response) => {\n await this._handleQueryRoute(req, res);\n },\n });\n\n // Metric-view route. Registered parallel to `/query`\n // measures a registered UC Metric View over the same SSE envelope.\n this.route(router, {\n name: \"metric\",\n method: \"post\",\n path: \"/metric/:key\",\n handler: async (req: express.Request, res: express.Response) => {\n await this._handleMetricRoute(req, res);\n },\n });\n\n // Column-names fallback for very wide Arrow schemas whose names don't fit\n // in the `X-Appkit-Arrow-Columns` response header.\n // The client hits this with the statement id from `X-Appkit-Arrow-Columns-Ref`.\n this.route(router, {\n name: \"arrow-columns\",\n method: \"get\",\n path: \"/columns/:statementId\",\n handler: async (req: express.Request, res: express.Response) => {\n await this._handleColumnsRoute(req, res);\n },\n });\n }\n\n /**\n * Column-names fallback endpoint. Re-derives the real column names from the\n * statement's result manifest (stateless — no server cache), for the client\n * to relabel a positional Arrow schema when the names were too large for the\n * response header.\n */\n async _handleColumnsRoute(\n req: express.Request,\n res: express.Response,\n ): Promise<void> {\n const { statementId } = req.params;\n const columns = await this._resolveColumnNames(req, statementId);\n if (columns && columns.length > 0) {\n res.setHeader(\"Cache-Control\", \"no-store\");\n res.json({ columns });\n return;\n }\n res.status(404).json({\n error: \"Column names unavailable\",\n plugin: this.name,\n });\n }\n\n /**\n * Resolve a statement's real column names, trying the user's identity first\n * (required for `.obo.sql` statements, which the service principal cannot\n * `getStatement`) then falling back to the service principal (for\n * SP-executed statements). Returns undefined if neither identity can read it,\n * so the client falls back to the raw positional Arrow schema names.\n */\n private async _resolveColumnNames(\n req: express.Request,\n statementId: string,\n ): Promise<string[] | undefined> {\n const attempts: Array<() => Promise<string[] | undefined>> = [\n () => this.asUser(req)._getColumnNames(statementId),\n () => this._getColumnNames(statementId),\n ];\n for (const attempt of attempts) {\n try {\n const columns = await attempt();\n if (columns && columns.length > 0) return columns;\n } catch (error) {\n logger.debug(\n \"Arrow column-names lookup attempt failed for %s: %O\",\n statementId,\n error,\n );\n }\n }\n return undefined;\n }\n\n /**\n * Fetch column names in the current execution context. Proxied by `asUser`,\n * so `getWorkspaceClient()` resolves to the user's client when invoked via\n * `this.asUser(req)` and the service principal's otherwise.\n */\n async _getColumnNames(statementId: string): Promise<string[] | undefined> {\n return this.SQLClient.getColumnNames(getWorkspaceClient(), statementId);\n }\n\n /**\n * Handle SQL query execution requests.\n * When called via asUser(req), uses the user's Databricks credentials.\n */\n async _handleQueryRoute(\n req: express.Request,\n res: express.Response,\n ): Promise<void> {\n const { query_key } = req.params;\n const { parameters, format: rawFormat = \"JSON_ARRAY\" } =\n req.body as IAnalyticsQueryRequest;\n\n if (\n rawFormat !== \"JSON_ARRAY\" &&\n rawFormat !== \"ARROW_STREAM\" &&\n rawFormat !== \"JSON\" &&\n rawFormat !== \"ARROW\"\n ) {\n res.status(400).json({\n error: `Invalid format: ${String(rawFormat)}. Expected \"JSON_ARRAY\" or \"ARROW_STREAM\".`,\n });\n return;\n }\n\n const format = normalizeAnalyticsFormat(rawFormat);\n\n // Request-scoped logging with WideEvent tracking\n logger.debug(req, \"Executing query: %s (format=%s)\", query_key, format);\n\n const event = logger.event(req);\n event?.setComponent(\"analytics\", \"executeQuery\").setContext(\"analytics\", {\n query_key,\n format,\n parameter_count: parameters ? Object.keys(parameters).length : 0,\n plugin: this.name,\n });\n\n if (!query_key) {\n res.status(400).json({ error: \"query_key is required\" });\n return;\n }\n\n const queryResult = await this.app.getAppQuery(\n query_key,\n req,\n this.devFileReader,\n );\n\n if (!queryResult) {\n res.status(404).json({ error: \"Query not found\" });\n return;\n }\n\n const { query, isAsUser } = queryResult;\n\n // ARROW_STREAM streams the raw Arrow IPC bytes back as the HTTP response\n // body — no SSE, no server-side stash, no second /arrow-result request.\n // INLINE attachments are piped straight through (the bytes are already in\n // hand from executeStatement); a warehouse that refuses INLINE falls back\n // to EXTERNAL_LINKS and streams those chunks. JSON keeps the SSE path\n // below (it carries warehouse-readiness progress + cached rows).\n if (format === \"ARROW_STREAM\") {\n await this._handleArrowStreamQuery(\n req,\n res,\n query_key,\n query,\n isAsUser,\n parameters,\n );\n return;\n }\n\n // get execution context - user-scoped if .obo.sql, otherwise service principal\n const executor = isAsUser ? this.asUser(req) : this;\n const executorKey = isAsUser ? this.resolveUserId(req) : \"global\";\n\n const hashedQuery = this.queryProcessor.hashQuery(query);\n\n const cacheConfig = {\n ...queryDefaults.cache,\n cacheKey: [\n \"analytics:query\",\n query_key,\n JSON.stringify(parameters),\n format,\n hashedQuery,\n executorKey,\n ],\n };\n\n // Cache/retry/timeout are scoped to the SQL execution itself (inner\n // `execute`) so the warehouse-readiness phase isn't subject to retries\n // and the generator value never leaks into the cache.\n const sqlConfig: PluginExecuteConfig = {\n ...queryDefaults,\n cache: cacheConfig,\n };\n\n // Outer stream: no cache/retry — `executeStream` would otherwise wrap the\n // generator factory and cache the generator object itself. Telemetry +\n // user-scoped trace context still apply.\n const streamExecutionSettings: StreamExecutionSettings = {\n default: {\n cache: { enabled: false },\n retry: { enabled: false },\n },\n };\n\n const startupTimeoutMs =\n this.config.warehouseStartupTimeoutMs ??\n DEFAULT_WAREHOUSE_STARTUP_TIMEOUT_MS;\n const autoStartWarehouse = this.config.autoStartWarehouse ?? true;\n\n const self = this;\n\n await executor.executeStream(\n res,\n async function* (\n signal,\n ): AsyncGenerator<AnalyticsStreamMessage, void, unknown> {\n const workspaceClient = getWorkspaceClient();\n const warehouseId = await getWarehouseId();\n\n // Stream warehouse-readiness updates as SSE events, then run SQL.\n const readinessUpdates = streamCallbacks<WarehouseStatusUpdate>(\n (emit) =>\n self.SQLClient.ensureWarehouseRunning(\n workspaceClient,\n warehouseId,\n {\n signal,\n timeoutMs: startupTimeoutMs,\n autoStart: autoStartWarehouse,\n onStatus: emit,\n },\n ),\n );\n for await (const update of readinessUpdates) {\n yield {\n type: \"warehouse_status\",\n status: {\n state: update.state as WarehouseStatus[\"state\"],\n elapsedMs: update.elapsedMs,\n },\n };\n }\n\n // `execute()` reduces a thrown error to `{ status, message }`,\n // dropping the rich fields (`errorCode`, `clientMessage`) the\n // fallback's `ExecutionError`s carry. Capture the original here so\n // we can re-throw it intact — the SSE error path\n // (`StreamManager`) reads `errorCode`/`clientMessage` off it.\n let originalError: unknown;\n const sqlResult = await executor.execute(\n async (sig) => {\n try {\n const processedParams =\n await self.queryProcessor.processQueryParams(query, parameters);\n // JSON_ARRAY path: tries INLINE + JSON_ARRAY and, if the\n // warehouse only accepts ARROW_STREAM for INLINE, retries as\n // ARROW_STREAM and decodes server-side — returning the SSE\n // `result` message with plain rows. (ARROW_STREAM requests are\n // handled earlier via `_handleArrowStreamQuery`.)\n return await self._executeJsonArrayPath(\n executor,\n query,\n processedParams,\n sig,\n );\n } catch (err) {\n originalError = err;\n throw err;\n }\n },\n { default: sqlConfig },\n executorKey,\n );\n\n if (!sqlResult.ok) {\n const msg = sqlResult.message;\n const lower = msg.toLowerCase();\n if (\n lower.includes(\"operation was aborted\") ||\n lower.includes(\"the request was aborted\") ||\n lower.includes(\"statement was canceled\")\n ) {\n const err = new DOMException(\n lower.includes(\"canceled\") ? msg : \"The operation was aborted.\",\n \"AbortError\",\n );\n throw err;\n }\n // Re-throw the original error so its structured `errorCode` (e.g.\n // RESULT_TOO_LARGE_FOR_JSON_FALLBACK) and sanitized `clientMessage`\n // survive to the SSE error payload. Fall back to a generic\n // statement failure only if the original wasn't an AppKitError.\n if (originalError instanceof AppKitError) {\n throw originalError;\n }\n const inner = msg.startsWith(\"Statement failed: \")\n ? msg.slice(\"Statement failed: \".length)\n : msg;\n throw ExecutionError.statementFailed(inner);\n }\n\n yield sqlResult.data as AnalyticsStreamMessage;\n },\n streamExecutionSettings,\n executorKey,\n );\n }\n\n /**\n * Handle metric-view execution requests (`POST /api/analytics/metric/:key`).\n *\n * Mirrors {@link _handleQueryRoute}'s JSON SSE path: the outer\n * `executeStream` disables cache/retry and streams warehouse-readiness\n * (`warehouse_status`) events, then the inner `execute` builds the metric SQL\n * and delivers rows through {@link deliverJsonResult} as a `result` message.\n * The `originalError` re-throw discipline preserves each error's structured\n * `errorCode`/`clientMessage` for the SSE error payload.\n *\n * Lane dispatch is driven by the registration: an SP-lane metric runs as the\n * app service principal (shared cache); an OBO-lane metric runs\n * on-behalf-of the requesting user via `asUser(req)` (per-user cache keyed by\n * a hash of the user identity).\n */\n async _handleMetricRoute(\n req: express.Request,\n res: express.Response,\n ): Promise<void> {\n const { key } = req.params;\n\n logger.debug(req, \"Executing metric: %s\", key);\n\n const event = logger.event(req);\n event?.setComponent(\"analytics\", \"executeMetric\").setContext(\"analytics\", {\n metric_key: key,\n plugin: this.name,\n });\n\n if (!key) {\n res.status(400).json({ error: \"metric key is required\" });\n return;\n }\n\n // Resolve the registry from disk: read + parse `definitions.json` once per\n // request (no memoization). Reads through the plugin's shared `this.app`\n // (the base `Plugin`'s `AppManager`) from `config/metric-views/` under the\n // process cwd, so this is dev-tunnel aware and inherits the traversal\n // guard — the same mechanism as the sibling `.sql` query path.\n let registry: Record<string, MetricRegistration>;\n try {\n registry = await loadMetricRegistry(this.app, req, this.devFileReader);\n } catch (err) {\n const reason = err instanceof Error ? err.message : String(err);\n logger.warn(req, \"Failed to load metric registry: %s\", reason);\n event?.setContext(\"analytics\", {\n metric_registry_load_error: reason,\n });\n res.status(503).json({\n error: \"Metric registry not available\",\n code: \"METRIC_REGISTRY_LOAD_FAILED\",\n });\n return;\n }\n\n // Own-property lookup: never resolve `key` to an inherited `Object.prototype` member.\n const registration = Object.hasOwn(registry, key)\n ? registry[key]\n : undefined;\n if (!registration) {\n // Don't echo the user-supplied `key` back in the public response.\n event?.setContext(\"analytics\", { unknown_metric_key: key });\n res.status(404).json({ error: \"Metric not found\" });\n return;\n }\n\n // Validate the body on the canonical error path.\n // `validateMetricRequest` throws a `ValidationError` (400) whose message names only field paths, never raw values.\n let request: ReturnType<typeof validateMetricRequest>;\n try {\n request = validateMetricRequest(req.body ?? {});\n } catch (err) {\n if (err instanceof AppKitError) {\n res.status(err.statusCode).json({ error: err.message, code: err.code });\n return;\n }\n event?.setContext(\"analytics\", {\n unexpected_error: err instanceof Error ? err.message : String(err),\n metric_key: key,\n });\n logger.warn(\n req,\n \"Unexpected throw during metric request validation for %s: %s\",\n key,\n err instanceof Error ? err.message : String(err),\n );\n res.status(400).json({ error: \"Invalid request body\" });\n return;\n }\n\n // Lane dispatch. The lane comes from the registration (the entry's\n // `executor` in definitions.json), NOT a URL segment or `.obo.sql`\n // filename: an OBO-lane metric runs on-behalf-of the requesting user\n // (per-user cache via `asUser(req)`), an SP-lane metric as the app service\n // principal (shared cache).\n let executor: AnalyticsPlugin;\n let executorKey: string;\n try {\n const isObo = registration.lane === \"obo\";\n executor = isObo ? this.asUser(req) : this;\n executorKey = deriveMetricExecutorKey({\n lane: registration.lane,\n userIdentity: isObo ? this.resolveUserId(req) : undefined,\n });\n } catch (err) {\n if (err instanceof AppKitError) {\n res.status(err.statusCode).json({ error: err.message, code: err.code });\n return;\n }\n throw err;\n }\n\n const injectedMetadata = this.config.metricViewsMetadata;\n const allMetadata =\n injectedMetadata ??\n (await loadMetricMetadata(this.app, req, this.devFileReader));\n\n if (allMetadata !== undefined && !Object.hasOwn(allMetadata, key)) {\n if (injectedMetadata !== undefined) {\n logger.warn(\n req,\n \"No display metadata for metric key %s in the injected metricViewsMetadata\",\n key,\n );\n } else {\n logger.warn(\n req,\n \"No display metadata for metric key %s — regenerate types to refresh %s\",\n key,\n METRIC_METADATA_FILE,\n );\n }\n }\n\n // Computed here, outside the cached execute below, so a cache hit still\n // serves the current metadata. Absent config → `undefined` → the `result`\n // message omits the field (envelope-identical to `/query`).\n const metadata = selectMetricMetadata(\n allMetadata,\n key,\n request.measures,\n request.dimensions,\n );\n\n // Cache key. Composed over the canonicalized args (sorted measures/\n // dimensions, stable-sorted predicates, grain, timeDimension, limit, orderBy) plus\n // the `executorKey` — `\"sp\"` shares the cache across all users, a per-user\n // identity hash isolates OBO callers.\n const cacheConfig = {\n ...queryDefaults.cache,\n cacheKey: composeMetricCacheKey({\n metricKey: key,\n source: registration.source,\n measures: request.measures,\n dimensions: request.dimensions,\n timeGrain: request.timeGrain,\n timeDimension: request.timeDimension,\n filter: request.filter,\n format: \"JSON_ARRAY\",\n executorKey,\n limit: request.limit,\n orderBy: request.orderBy,\n }),\n };\n\n // Cache/retry/timeout scoped to the SQL execution itself (inner `execute`)\n // so the warehouse-readiness phase isn't retried and the generator value\n // never leaks into the cache.\n const sqlConfig: PluginExecuteConfig = {\n ...queryDefaults,\n cache: cacheConfig,\n };\n\n // Outer stream: no cache/retry — `executeStream` would otherwise wrap the\n // generator factory and cache the generator object itself. Telemetry +\n // trace context still apply.\n const streamExecutionSettings: StreamExecutionSettings = {\n default: {\n cache: { enabled: false },\n retry: { enabled: false },\n },\n };\n\n const startupTimeoutMs =\n this.config.warehouseStartupTimeoutMs ??\n DEFAULT_WAREHOUSE_STARTUP_TIMEOUT_MS;\n const autoStartWarehouse = this.config.autoStartWarehouse ?? true;\n\n const self = this;\n\n await executor.executeStream(\n res,\n async function* (\n signal,\n ): AsyncGenerator<AnalyticsStreamMessage, void, unknown> {\n const workspaceClient = getWorkspaceClient();\n const warehouseId = await getWarehouseId();\n\n // Stream warehouse-readiness updates as SSE events, then run SQL.\n const readinessUpdates = streamCallbacks<WarehouseStatusUpdate>(\n (emit) =>\n self.SQLClient.ensureWarehouseRunning(\n workspaceClient,\n warehouseId,\n {\n signal,\n timeoutMs: startupTimeoutMs,\n autoStart: autoStartWarehouse,\n onStatus: emit,\n },\n ),\n );\n for await (const update of readinessUpdates) {\n yield {\n type: \"warehouse_status\",\n status: {\n state: update.state as WarehouseStatus[\"state\"],\n elapsedMs: update.elapsedMs,\n },\n };\n }\n\n // `execute()` reduces a thrown error to `{ status, message }`,\n // dropping the rich fields (`errorCode`, `clientMessage`). Capture the\n // original here so we can re-throw it intact — the SSE error path\n // (`StreamManager`) reads `errorCode`/`clientMessage` off it.\n let originalError: unknown;\n const sqlResult = await executor.execute(\n async (sig) => {\n try {\n const { statement, parameters } = buildMetricSql(\n registration,\n request,\n );\n const processedParams =\n await self.queryProcessor.processQueryParams(\n statement,\n Object.keys(parameters).length > 0 ? parameters : undefined,\n );\n // Reuse the query route's JSON delivery: INLINE JSON_ARRAY with\n // an ARROW_STREAM-inline fallback, returning plain rows in a\n // `result` message — byte-identical envelope to `/query`.\n return await self._executeJsonArrayPath(\n executor,\n statement,\n processedParams,\n sig,\n );\n } catch (err) {\n originalError = err;\n throw err;\n }\n },\n { default: sqlConfig },\n executorKey,\n );\n\n if (!sqlResult.ok) {\n const msg = sqlResult.message;\n const lower = msg.toLowerCase();\n if (\n lower.includes(\"operation was aborted\") ||\n lower.includes(\"the request was aborted\") ||\n lower.includes(\"statement was canceled\")\n ) {\n const err = new DOMException(\n lower.includes(\"canceled\") ? msg : \"The operation was aborted.\",\n \"AbortError\",\n );\n throw err;\n }\n // Re-throw the original error so its structured `errorCode` and\n // sanitized `clientMessage` survive to the SSE error payload. Fall\n // back to a generic statement failure only if it wasn't an\n // AppKitError.\n if (originalError instanceof AppKitError) {\n throw originalError;\n }\n const inner = msg.startsWith(\"Statement failed: \")\n ? msg.slice(\"Statement failed: \".length)\n : msg;\n throw ExecutionError.statementFailed(inner);\n }\n\n // Stamp the metadata onto the (possibly cached) result message; the\n // cached message never carries it.\n const resultMessage = sqlResult.data as AnalyticsSseMessage;\n yield (\n metadata !== undefined\n ? { ...resultMessage, metadata }\n : resultMessage\n ) as AnalyticsStreamMessage;\n },\n streamExecutionSettings,\n executorKey,\n );\n }\n\n /**\n * JSON_ARRAY SSE path. Delegates the disposition/format fallback to\n * {@link deliverJsonResult} (INLINE JSON_ARRAY → on `needs-arrow-inline`,\n * INLINE ARROW_STREAM decoded to rows) and wraps the rows in a `result`\n * message. External links are never used for the JSON fallback.\n */\n private async _executeJsonArrayPath(\n executor: AnalyticsPlugin,\n query: string,\n processedParams:\n | Record<string, SQLTypeMarker | null | undefined>\n | undefined,\n signal?: AbortSignal,\n ): Promise<AnalyticsSseMessage> {\n const result = await deliverJsonResult(\n executor,\n query,\n processedParams,\n signal,\n );\n return makeResultMessage(result.data, {\n status: result.status,\n statement_id: result.statement_id,\n });\n }\n\n /**\n * Attach the real column names so the client can relabel the positional\n * Arrow schema (Databricks encodes ARROW_STREAM columns as col_0, …).\n *\n * Small schemas ride `X-Appkit-Arrow-Columns` directly. A very wide schema\n * whose URL-encoded names would blow the HTTP header size limit instead\n * advertises the statement id in `X-Appkit-Arrow-Columns-Ref`, and the\n * client fetches the names from `GET /columns/:statementId`.\n */\n private _setArrowColumnsHeader(\n res: express.Response,\n columnsRef: { columnNames?: string[]; statementId?: string },\n ): void {\n const names = columnsRef.columnNames;\n if (!names || names.length === 0) return;\n\n const encoded = encodeURIComponent(JSON.stringify(names));\n if (encoded.length <= MAX_ARROW_COLUMNS_HEADER_BYTES) {\n res.setHeader(\"X-Appkit-Arrow-Columns\", encoded);\n return;\n }\n if (columnsRef.statementId) {\n res.setHeader(\"X-Appkit-Arrow-Columns-Ref\", columnsRef.statementId);\n } else {\n logger.warn(\n \"Arrow column names exceed the header limit and no statement id is available for the fallback endpoint; client will fall back to the raw schema names\",\n );\n }\n }\n\n /**\n * ARROW_STREAM query handler: stream the raw Arrow IPC bytes back as the\n * HTTP response body — no SSE, no server-side stash, no second\n * `/arrow-result` request.\n *\n * The first chunk is pulled before headers are sent so a failure still\n * yields a clean JSON error; once bytes are in flight a mid-stream failure\n * can only abort the socket. Warehouse readiness is awaited (no SSE\n * progress on this path) — a no-op for a warm warehouse, a blocking wait\n * on a cold start. Runs under the user's context for `.obo.sql` queries.\n */\n private async _handleArrowStreamQuery(\n req: express.Request,\n res: express.Response,\n query_key: string,\n query: string,\n isAsUser: boolean,\n parameters: IAnalyticsQueryRequest[\"parameters\"],\n ): Promise<void> {\n const executor = isAsUser ? this.asUser(req) : this;\n const executorKey = isAsUser ? this.resolveUserId(req) : \"global\";\n const abortController = new AbortController();\n const onClose = () => abortController.abort();\n res.on(\"close\", onClose);\n const signal = abortController.signal;\n\n // Fail-fast: bound the wait for the first byte (warehouse readiness +\n // execute + first chunk) so a stuck/overloaded warehouse returns a clear\n // 503 instead of hanging until the client gives up. Cleared once the\n // first chunk arrives — a legitimately long stream is never interrupted.\n const firstByteTimeoutMs =\n this.config.arrowFirstByteTimeoutMs ??\n DEFAULT_ARROW_FIRST_BYTE_TIMEOUT_MS;\n let timedOut = false;\n const failFast = setTimeout(() => {\n timedOut = true;\n abortController.abort();\n }, firstByteTimeoutMs);\n\n try {\n // Run warehouse readiness in the SAME identity context as the query:\n // for `.obo.sql`, `executor` is the `asUser(req)` proxy, so\n // `getWorkspaceClient()` inside resolves to the user's client (matching\n // the SSE path). Calling it bare here would auto-start the warehouse as\n // the service principal even for OBO requests.\n await executor._ensureArrowWarehouseReady(signal);\n\n const processedParams = await this.queryProcessor.processQueryParams(\n query,\n parameters,\n );\n\n const warehouseId = await getWarehouseId();\n\n // Populated by `deliverArrowBytes` from the result manifest before the\n // first chunk is yielded, so the header below carries the real names.\n const columnsRef: { columnNames?: string[]; statementId?: string } = {};\n const bytes = deliverArrowBytes(\n // Wrap the executor so the INLINE attempt runs through the interceptor\n // chain (cache + retry), matching the JSON path. Only inline attachments\n // are cached; EXTERNAL_LINKS carry expiring pre-signed URLs and are\n // never cached (see `_arrowCachingExecutor`).\n this._arrowCachingExecutor(\n executor,\n query_key,\n query,\n parameters,\n executorKey,\n ),\n this.SQLClient,\n query,\n processedParams,\n columnsRef,\n signal,\n {\n // Skip the doomed INLINE probe on a warehouse already known to need\n // EXTERNAL_LINKS; remember the resolved mode for next time.\n capabilityHint: this._arrowCapability.get(warehouseId),\n onCapabilityResolved: (capability) =>\n this._arrowCapability.set(warehouseId, capability),\n },\n );\n const first = await bytes.next();\n // First byte in hand — stop the fail-fast clock.\n clearTimeout(failFast);\n\n res.setHeader(\"Content-Type\", \"application/vnd.apache.arrow.stream\");\n res.setHeader(\"Cache-Control\", \"no-store\");\n this._setArrowColumnsHeader(res, columnsRef);\n\n if (!first.done) {\n await writeChunk(res, first.value);\n for await (const buf of bytes) {\n await writeChunk(res, buf);\n }\n }\n res.end();\n } catch (error) {\n clearTimeout(failFast);\n // Fail-fast timeout: the warehouse never produced a first byte. This also\n // aborts the signal, so it must be handled before the generic\n // `signal.aborted` branch below. Headers aren't sent yet (we time out\n // before the first chunk), so a clean 503 is still possible.\n if (timedOut) {\n logger.warn(\n \"Arrow query timed out before first byte after %dms\",\n firstByteTimeoutMs,\n );\n res.status(503).json({\n error:\n \"The SQL warehouse is starting or overloaded and did not respond in time. Please retry.\",\n errorCode: \"WAREHOUSE_UNAVAILABLE\",\n plugin: this.name,\n });\n return;\n }\n // Client disconnect / unmount aborts the signal (see `onClose`). That's\n // routine UI behavior, not a server error — tear down quietly whether it\n // fires before or after headers. Checked before the headersSent branch so\n // a mid-stream disconnect doesn't spam ERROR logs / alerting.\n if (signal.aborted) {\n if (res.headersSent) res.destroy();\n else res.end();\n return;\n }\n if (res.headersSent) {\n logger.error(\"Arrow query stream failed mid-flight: %O\", error);\n res.destroy(error instanceof Error ? error : new Error(String(error)));\n return;\n }\n logger.error(\"Arrow query error: %O\", error);\n // Do not echo upstream / SDK error text — it can include statement\n // fragments and correlation ids. Keep the structured code so the\n // client can branch (e.g. RESULT_TOO_LARGE_FOR_JSON_FALLBACK,\n // ARROW_DELIVERY_UNSUPPORTED).\n const errorCode =\n error instanceof ExecutionError ? error.errorCode : undefined;\n res.status(500).json({\n // `clientMessage` is the sanitized, actionable text (e.g. \"Re-run with\n // JSON_ARRAY\" for ARROW_DELIVERY_UNSUPPORTED); it never carries raw\n // warehouse/SDK strings. Fall back to a generic message otherwise.\n error:\n error instanceof AppKitError\n ? error.clientMessage\n : \"Unable to execute query\",\n errorCode,\n plugin: this.name,\n });\n } finally {\n res.off(\"close\", onClose);\n }\n }\n\n /**\n * Await SQL warehouse readiness for the direct-binary Arrow path. There is no\n * SSE progress channel here — readiness is simply awaited, bounded by the\n * caller's abort signal / fail-fast timeout. Invoked via the request executor\n * (`asUser(req)` for `.obo.sql`) so `getWorkspaceClient()` resolves in the\n * correct identity context rather than defaulting to the service principal.\n */\n async _ensureArrowWarehouseReady(signal: AbortSignal): Promise<void> {\n const workspaceClient = getWorkspaceClient();\n const warehouseId = await getWarehouseId();\n const startupTimeoutMs =\n this.config.warehouseStartupTimeoutMs ??\n DEFAULT_WAREHOUSE_STARTUP_TIMEOUT_MS;\n const autoStart = this.config.autoStartWarehouse ?? true;\n await this.SQLClient.ensureWarehouseRunning(workspaceClient, warehouseId, {\n signal,\n timeoutMs: startupTimeoutMs,\n autoStart,\n onStatus: () => {},\n });\n }\n\n /**\n * Wrap an executor so the `INLINE + ARROW_STREAM` attempt is served from\n * (and populates) the same per-user TTL cache the JSON path uses — otherwise\n * every arrow chart render is a fresh warehouse execution, unlike its JSON\n * twin. Caching is deliberately scoped to inline attachments:\n *\n * - INLINE results carry a bounded (<=25 MiB) base64 `attachment` with the\n * same lifecycle as cached JSON rows — safe to cache.\n * - EXTERNAL_LINKS results carry short-lived pre-signed URLs that expire in\n * minutes; caching them would serve dead links, so those pass through\n * uncached (only the tiny link metadata would be cached anyway).\n *\n * Uses `this.cache.getOrExecute` directly rather than `this.execute()`\n * because `execute()` reduces a thrown error to `{ ok:false, message }`,\n * dropping the `errorCode` the capability fallback classifies on. The cache\n * re-throws `AppKitError`s intact and never caches a rejection, so the\n * INLINE→EXTERNAL_LINKS fallback still sees the structured rejection.\n */\n private _arrowCachingExecutor(\n executor: AnalyticsPlugin,\n query_key: string,\n query: string,\n parameters: IAnalyticsQueryRequest[\"parameters\"],\n executorKey: string,\n ): QueryExecutor {\n const hashedQuery = this.queryProcessor.hashQuery(query);\n const cache = this.cache;\n const ttl = queryDefaults.cache?.ttl;\n return {\n query: (q, params, formatParameters, signal) => {\n // Only the inline-arrow attempt is cacheable — EXTERNAL_LINKS carry\n // short-lived pre-signed URLs, so those pass straight through.\n if (\n formatParameters.disposition !== \"INLINE\" ||\n formatParameters.format !== \"ARROW_STREAM\"\n ) {\n return executor.query(q, params, formatParameters, signal);\n }\n // On a standard warehouse this throws a capability rejection — the\n // cache never stores a rejection, so the fallback still sees the\n // structured error. On Reyden it returns a bounded (<=25 MiB)\n // attachment that caches like the JSON path's rows. The shared signal\n // dedupes concurrent renders (e.g. React StrictMode double-mount).\n return cache.getOrExecute(\n [\n \"analytics:query:arrow\",\n query_key,\n JSON.stringify(parameters),\n hashedQuery,\n executorKey,\n ],\n (sharedSignal) =>\n executor.query(q, params, formatParameters, sharedSignal ?? signal),\n executorKey,\n { ttl, callerSignal: signal },\n );\n },\n };\n }\n\n /**\n * Execute a SQL query using the current execution context.\n *\n * When called directly: uses service principal credentials.\n * When called via asUser(req).query(...): uses user's credentials.\n *\n * @example\n * ```typescript\n * // Service principal execution\n * const result = await analytics.query(\"SELECT * FROM table\")\n *\n * // User context execution (in route handler)\n * const result = await this.asUser(req).query(\"SELECT * FROM table\")\n * ```\n */\n async query(\n query: string,\n parameters?: Record<string, SQLTypeMarker | null | undefined>,\n formatParameters?: Record<string, any>,\n signal?: AbortSignal,\n ): Promise<any> {\n const workspaceClient = getWorkspaceClient();\n const warehouseId = await getWarehouseId();\n\n const { statement, parameters: sqlParameters } =\n this.queryProcessor.convertToSQLParameters(query, parameters);\n\n const response = await this.SQLClient.executeStatement(\n workspaceClient,\n {\n statement,\n warehouse_id: warehouseId,\n parameters: sqlParameters,\n ...formatParameters,\n },\n signal,\n );\n\n return response.result;\n }\n\n async shutdown(): Promise<void> {\n this.streamManager.abortAll();\n }\n\n private tools = {\n query: defineTool({\n description:\n \"Execute a read-only SQL query against the Databricks SQL warehouse. Only SELECT, WITH, SHOW, EXPLAIN, and DESCRIBE statements are accepted; writes are rejected. Returns the query results as JSON.\",\n schema: z.object({\n query: z\n .string()\n .describe(\n \"The SQL query to execute. Must be a SELECT, WITH, SHOW, EXPLAIN, or DESCRIBE statement.\",\n ),\n }),\n annotations: {\n effect: \"read\",\n requiresUserContext: true,\n },\n autoInheritable: true,\n execute: (args, signal) => {\n assertReadOnlySql(args.query);\n return this.query(args.query, undefined, undefined, signal);\n },\n }),\n };\n\n getAgentTools(): AgentToolDefinition[] {\n return toolsFromRegistry(this.tools);\n }\n\n async executeAgentTool(\n name: string,\n args: unknown,\n signal?: AbortSignal,\n ): Promise<unknown> {\n return executeFromRegistry(this.tools, name, args, signal);\n }\n\n /**\n * Returns the plugin's tools as a keyed record of `ToolkitEntry` markers.\n * Called by the agents plugin (via `resolveToolkitFromProvider`) to spread\n * a filtered, renamed view of the plugin's tools into an agent's tool\n * index. Inside the function form of `AgentDefinition.tools`, callers\n * reach this method via `plugins.analytics.toolkit(opts)`.\n */\n toolkit(opts?: import(\"../../core/agent/types\").ToolkitOptions) {\n return buildToolkitEntries(this.name, this.tools, opts);\n }\n\n /**\n * Returns the public exports for the analytics plugin.\n * Note: `asUser()` is automatically added by AppKit.\n */\n exports() {\n return {\n /**\n * Execute a SQL query using service principal credentials.\n */\n query: this.query,\n };\n }\n}\n\n/**\n * Write one chunk to the response honoring backpressure: if the socket\n * buffer is full (`res.write` returns false), wait for `drain` before\n * resolving so a slow client can't balloon Node's internal write queue and\n * defeat the constant-memory goal of streaming.\n *\n * @internal exported for unit testing the backpressure/disconnect behavior.\n */\nexport function writeChunk(\n res: express.Response,\n bytes: Uint8Array,\n): Promise<void> {\n const buf = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);\n // If the socket is already gone, `res.write` won't return true and the\n // `drain`/`close`/`error` events have already fired — so the promise below\n // would never settle, wedging the for-await loop and the upstream reader.\n // Reject up front instead.\n if (res.destroyed || res.writableEnded) {\n return Promise.reject(\n new DOMException(\"The response stream closed\", \"AbortError\"),\n );\n }\n if (res.write(buf)) return Promise.resolve();\n // Backpressured: resolve on `drain`, but also settle on `close`/`error`. A\n // client that disconnects mid-backpressure never emits `drain` on the\n // destroyed socket, so waiting on `drain` alone would wedge this promise —\n // and with it the awaiting for-await loop and the upstream Arrow reader —\n // forever. Rejecting instead unwinds the stream so its `finally` can cancel\n // the reader.\n return new Promise<void>((resolve, reject) => {\n const cleanup = () => {\n res.off(\"drain\", onDrain);\n res.off(\"close\", onClose);\n res.off(\"error\", onClose);\n };\n const onDrain = () => {\n cleanup();\n resolve();\n };\n const onClose = () => {\n cleanup();\n reject(new DOMException(\"The response stream closed\", \"AbortError\"));\n };\n res.once(\"drain\", onDrain);\n res.once(\"close\", onClose);\n res.once(\"error\", onClose);\n });\n}\n\n/**\n * Fail-fast ceiling on the wait for the first Arrow byte (warehouse\n * readiness + execute + first chunk). Past this a stuck/overloaded warehouse\n * yields a clear 503 instead of hanging. Override per plugin via\n * `arrowFirstByteTimeoutMs`.\n */\nconst DEFAULT_ARROW_FIRST_BYTE_TIMEOUT_MS = 120_000;\n\n/**\n * Byte ceiling for the `X-Appkit-Arrow-Columns` header value. Beyond this (a\n * very wide schema) the names are served via the `/columns/:statementId`\n * fallback endpoint instead of the header. Kept well under the common ~8 KiB\n * per-header limit.\n */\nconst MAX_ARROW_COLUMNS_HEADER_BYTES = 6000;\n\n/**\n * @internal\n */\nexport const analytics = toPlugin(AnalyticsPlugin);\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6DA,MAAM,SAAS,aAAa,YAAY;;;;;;;;;AAUxC,gBAAgB,gBACd,OACkC;CAClC,MAAM,QAAa,EAAE;CACrB,IAAI,OAA4B;CAChC,IAAI,UAAU;CACd,IAAI,QAAiB;CAErB,MAAM,eAAqB;AACzB,UAAQ;AACR,SAAO;;AAKT,CAAK,OAAO,UAAU;AACpB,QAAM,KAAK,MAAM;AACjB,UAAQ;GACR,CAAC,WACK;AACJ,YAAU;AACV,UAAQ;KAET,QAAQ;AACP,UAAQ;AACR,YAAU;AACV,UAAQ;GAEX;AAED,QAAO,CAAC,WAAW,MAAM,SAAS,GAAG;AACnC,SAAO,MAAM,SAAS,EAAG,OAAM,MAAM,OAAO;AAC5C,MAAI,QAAS;AACb,QAAM,IAAI,SAAe,YAAY;AACnC,UAAO;IACP;;AAEJ,KAAI,MAAO,OAAM;;AAGnB,IAAa,kBAAb,cAAqC,OAA+B;;CAElE,OAAO,WAAW,eAA4BA,iBAAS;CAEvD,OAAiB,cAAc;CAI/B,AAAQ;CACR,AAAQ;;;;;;;;;CAUR,AAAQ,mCAAmB,IAAI,KAA8B;CAE7D,YAAY,QAA0B;AACpC,QAAM,OAAO;AACb,OAAK,SAAS;AACd,OAAK,iBAAiB,IAAI,gBAAgB;AAE1C,OAAK,YAAY,IAAI,sBAAsB;GACzC,SAAS,OAAO;GAChB,WAAW,OAAO;GACnB,CAAC;;CAGJ,aAAa,QAAoB;AAC/B,OAAK,MAA8B,QAAQ;GACzC,MAAM;GACN,QAAQ;GACR,MAAM;GACN,SAAS,OAAO,KAAsB,QAA0B;AAC9D,UAAM,KAAK,kBAAkB,KAAK,IAAI;;GAEzC,CAAC;AAIF,OAAK,MAAM,QAAQ;GACjB,MAAM;GACN,QAAQ;GACR,MAAM;GACN,SAAS,OAAO,KAAsB,QAA0B;AAC9D,UAAM,KAAK,mBAAmB,KAAK,IAAI;;GAE1C,CAAC;AAKF,OAAK,MAAM,QAAQ;GACjB,MAAM;GACN,QAAQ;GACR,MAAM;GACN,SAAS,OAAO,KAAsB,QAA0B;AAC9D,UAAM,KAAK,oBAAoB,KAAK,IAAI;;GAE3C,CAAC;;;;;;;;CASJ,MAAM,oBACJ,KACA,KACe;EACf,MAAM,EAAE,gBAAgB,IAAI;EAC5B,MAAM,UAAU,MAAM,KAAK,oBAAoB,KAAK,YAAY;AAChE,MAAI,WAAW,QAAQ,SAAS,GAAG;AACjC,OAAI,UAAU,iBAAiB,WAAW;AAC1C,OAAI,KAAK,EAAE,SAAS,CAAC;AACrB;;AAEF,MAAI,OAAO,IAAI,CAAC,KAAK;GACnB,OAAO;GACP,QAAQ,KAAK;GACd,CAAC;;;;;;;;;CAUJ,MAAc,oBACZ,KACA,aAC+B;EAC/B,MAAM,WAAuD,OACrD,KAAK,OAAO,IAAI,CAAC,gBAAgB,YAAY,QAC7C,KAAK,gBAAgB,YAAY,CACxC;AACD,OAAK,MAAM,WAAW,SACpB,KAAI;GACF,MAAM,UAAU,MAAM,SAAS;AAC/B,OAAI,WAAW,QAAQ,SAAS,EAAG,QAAO;WACnC,OAAO;AACd,UAAO,MACL,uDACA,aACA,MACD;;;;;;;;CAWP,MAAM,gBAAgB,aAAoD;AACxE,SAAO,KAAK,UAAU,eAAe,oBAAoB,EAAE,YAAY;;;;;;CAOzE,MAAM,kBACJ,KACA,KACe;EACf,MAAM,EAAE,cAAc,IAAI;EAC1B,MAAM,EAAE,YAAY,QAAQ,YAAY,iBACtC,IAAI;AAEN,MACE,cAAc,gBACd,cAAc,kBACd,cAAc,UACd,cAAc,SACd;AACA,OAAI,OAAO,IAAI,CAAC,KAAK,EACnB,OAAO,mBAAmB,OAAO,UAAU,CAAC,6CAC7C,CAAC;AACF;;EAGF,MAAM,SAAS,yBAAyB,UAAU;AAGlD,SAAO,MAAM,KAAK,mCAAmC,WAAW,OAAO;AAGvE,EADc,OAAO,MAAM,IAAI,EACxB,aAAa,aAAa,eAAe,CAAC,WAAW,aAAa;GACvE;GACA;GACA,iBAAiB,aAAa,OAAO,KAAK,WAAW,CAAC,SAAS;GAC/D,QAAQ,KAAK;GACd,CAAC;AAEF,MAAI,CAAC,WAAW;AACd,OAAI,OAAO,IAAI,CAAC,KAAK,EAAE,OAAO,yBAAyB,CAAC;AACxD;;EAGF,MAAM,cAAc,MAAM,KAAK,IAAI,YACjC,WACA,KACA,KAAK,cACN;AAED,MAAI,CAAC,aAAa;AAChB,OAAI,OAAO,IAAI,CAAC,KAAK,EAAE,OAAO,mBAAmB,CAAC;AAClD;;EAGF,MAAM,EAAE,OAAO,aAAa;AAQ5B,MAAI,WAAW,gBAAgB;AAC7B,SAAM,KAAK,wBACT,KACA,KACA,WACA,OACA,UACA,WACD;AACD;;EAIF,MAAM,WAAW,WAAW,KAAK,OAAO,IAAI,GAAG;EAC/C,MAAM,cAAc,WAAW,KAAK,cAAc,IAAI,GAAG;EAEzD,MAAM,cAAc,KAAK,eAAe,UAAU,MAAM;EAExD,MAAM,cAAc;GAClB,GAAG,cAAc;GACjB,UAAU;IACR;IACA;IACA,KAAK,UAAU,WAAW;IAC1B;IACA;IACA;IACD;GACF;EAKD,MAAM,YAAiC;GACrC,GAAG;GACH,OAAO;GACR;EAKD,MAAM,0BAAmD,EACvD,SAAS;GACP,OAAO,EAAE,SAAS,OAAO;GACzB,OAAO,EAAE,SAAS,OAAO;GAC1B,EACF;EAED,MAAM,mBACJ,KAAK,OAAO,6BACZ;EACF,MAAM,qBAAqB,KAAK,OAAO,sBAAsB;EAE7D,MAAM,OAAO;AAEb,QAAM,SAAS,cACb,KACA,iBACE,QACuD;GACvD,MAAM,kBAAkB,oBAAoB;GAC5C,MAAM,cAAc,MAAM,gBAAgB;GAG1C,MAAM,mBAAmB,iBACtB,SACC,KAAK,UAAU,uBACb,iBACA,aACA;IACE;IACA,WAAW;IACX,WAAW;IACX,UAAU;IACX,CACF,CACJ;AACD,cAAW,MAAM,UAAU,iBACzB,OAAM;IACJ,MAAM;IACN,QAAQ;KACN,OAAO,OAAO;KACd,WAAW,OAAO;KACnB;IACF;GAQH,IAAI;GACJ,MAAM,YAAY,MAAM,SAAS,QAC/B,OAAO,QAAQ;AACb,QAAI;KACF,MAAM,kBACJ,MAAM,KAAK,eAAe,mBAAmB,OAAO,WAAW;AAMjE,YAAO,MAAM,KAAK,sBAChB,UACA,OACA,iBACA,IACD;aACM,KAAK;AACZ,qBAAgB;AAChB,WAAM;;MAGV,EAAE,SAAS,WAAW,EACtB,YACD;AAED,OAAI,CAAC,UAAU,IAAI;IACjB,MAAM,MAAM,UAAU;IACtB,MAAM,QAAQ,IAAI,aAAa;AAC/B,QACE,MAAM,SAAS,wBAAwB,IACvC,MAAM,SAAS,0BAA0B,IACzC,MAAM,SAAS,yBAAyB,CAMxC,OAJY,IAAI,aACd,MAAM,SAAS,WAAW,GAAG,MAAM,8BACnC,aACD;AAOH,QAAI,yBAAyB,YAC3B,OAAM;IAER,MAAM,QAAQ,IAAI,WAAW,qBAAqB,GAC9C,IAAI,MAAM,GAA4B,GACtC;AACJ,UAAM,eAAe,gBAAgB,MAAM;;AAG7C,SAAM,UAAU;KAElB,yBACA,YACD;;;;;;;;;;;;;;;;;CAkBH,MAAM,mBACJ,KACA,KACe;EACf,MAAM,EAAE,QAAQ,IAAI;AAEpB,SAAO,MAAM,KAAK,wBAAwB,IAAI;EAE9C,MAAM,QAAQ,OAAO,MAAM,IAAI;AAC/B,SAAO,aAAa,aAAa,gBAAgB,CAAC,WAAW,aAAa;GACxE,YAAY;GACZ,QAAQ,KAAK;GACd,CAAC;AAEF,MAAI,CAAC,KAAK;AACR,OAAI,OAAO,IAAI,CAAC,KAAK,EAAE,OAAO,0BAA0B,CAAC;AACzD;;EAQF,IAAI;AACJ,MAAI;AACF,cAAW,MAAM,mBAAmB,KAAK,KAAK,KAAK,KAAK,cAAc;WAC/D,KAAK;GACZ,MAAM,SAAS,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI;AAC/D,UAAO,KAAK,KAAK,sCAAsC,OAAO;AAC9D,UAAO,WAAW,aAAa,EAC7B,4BAA4B,QAC7B,CAAC;AACF,OAAI,OAAO,IAAI,CAAC,KAAK;IACnB,OAAO;IACP,MAAM;IACP,CAAC;AACF;;EAIF,MAAM,eAAe,OAAO,OAAO,UAAU,IAAI,GAC7C,SAAS,OACT;AACJ,MAAI,CAAC,cAAc;AAEjB,UAAO,WAAW,aAAa,EAAE,oBAAoB,KAAK,CAAC;AAC3D,OAAI,OAAO,IAAI,CAAC,KAAK,EAAE,OAAO,oBAAoB,CAAC;AACnD;;EAKF,IAAI;AACJ,MAAI;AACF,aAAU,sBAAsB,IAAI,QAAQ,EAAE,CAAC;WACxC,KAAK;AACZ,OAAI,eAAe,aAAa;AAC9B,QAAI,OAAO,IAAI,WAAW,CAAC,KAAK;KAAE,OAAO,IAAI;KAAS,MAAM,IAAI;KAAM,CAAC;AACvE;;AAEF,UAAO,WAAW,aAAa;IAC7B,kBAAkB,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI;IAClE,YAAY;IACb,CAAC;AACF,UAAO,KACL,KACA,gEACA,KACA,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,CACjD;AACD,OAAI,OAAO,IAAI,CAAC,KAAK,EAAE,OAAO,wBAAwB,CAAC;AACvD;;EAQF,IAAI;EACJ,IAAI;AACJ,MAAI;GACF,MAAM,QAAQ,aAAa,SAAS;AACpC,cAAW,QAAQ,KAAK,OAAO,IAAI,GAAG;AACtC,iBAAc,wBAAwB;IACpC,MAAM,aAAa;IACnB,cAAc,QAAQ,KAAK,cAAc,IAAI,GAAG;IACjD,CAAC;WACK,KAAK;AACZ,OAAI,eAAe,aAAa;AAC9B,QAAI,OAAO,IAAI,WAAW,CAAC,KAAK;KAAE,OAAO,IAAI;KAAS,MAAM,IAAI;KAAM,CAAC;AACvE;;AAEF,SAAM;;EAGR,MAAM,mBAAmB,KAAK,OAAO;EACrC,MAAM,cACJ,oBACC,MAAM,mBAAmB,KAAK,KAAK,KAAK,KAAK,cAAc;AAE9D,MAAI,gBAAgB,UAAa,CAAC,OAAO,OAAO,aAAa,IAAI,CAC/D,KAAI,qBAAqB,OACvB,QAAO,KACL,KACA,6EACA,IACD;MAED,QAAO,KACL,KACA,0EACA,KACA,qBACD;EAOL,MAAM,WAAW,qBACf,aACA,KACA,QAAQ,UACR,QAAQ,WACT;EAMD,MAAM,cAAc;GAClB,GAAG,cAAc;GACjB,UAAU,sBAAsB;IAC9B,WAAW;IACX,QAAQ,aAAa;IACrB,UAAU,QAAQ;IAClB,YAAY,QAAQ;IACpB,WAAW,QAAQ;IACnB,eAAe,QAAQ;IACvB,QAAQ,QAAQ;IAChB,QAAQ;IACR;IACA,OAAO,QAAQ;IACf,SAAS,QAAQ;IAClB,CAAC;GACH;EAKD,MAAM,YAAiC;GACrC,GAAG;GACH,OAAO;GACR;EAKD,MAAM,0BAAmD,EACvD,SAAS;GACP,OAAO,EAAE,SAAS,OAAO;GACzB,OAAO,EAAE,SAAS,OAAO;GAC1B,EACF;EAED,MAAM,mBACJ,KAAK,OAAO,6BACZ;EACF,MAAM,qBAAqB,KAAK,OAAO,sBAAsB;EAE7D,MAAM,OAAO;AAEb,QAAM,SAAS,cACb,KACA,iBACE,QACuD;GACvD,MAAM,kBAAkB,oBAAoB;GAC5C,MAAM,cAAc,MAAM,gBAAgB;GAG1C,MAAM,mBAAmB,iBACtB,SACC,KAAK,UAAU,uBACb,iBACA,aACA;IACE;IACA,WAAW;IACX,WAAW;IACX,UAAU;IACX,CACF,CACJ;AACD,cAAW,MAAM,UAAU,iBACzB,OAAM;IACJ,MAAM;IACN,QAAQ;KACN,OAAO,OAAO;KACd,WAAW,OAAO;KACnB;IACF;GAOH,IAAI;GACJ,MAAM,YAAY,MAAM,SAAS,QAC/B,OAAO,QAAQ;AACb,QAAI;KACF,MAAM,EAAE,WAAW,eAAe,eAChC,cACA,QACD;KACD,MAAM,kBACJ,MAAM,KAAK,eAAe,mBACxB,WACA,OAAO,KAAK,WAAW,CAAC,SAAS,IAAI,aAAa,OACnD;AAIH,YAAO,MAAM,KAAK,sBAChB,UACA,WACA,iBACA,IACD;aACM,KAAK;AACZ,qBAAgB;AAChB,WAAM;;MAGV,EAAE,SAAS,WAAW,EACtB,YACD;AAED,OAAI,CAAC,UAAU,IAAI;IACjB,MAAM,MAAM,UAAU;IACtB,MAAM,QAAQ,IAAI,aAAa;AAC/B,QACE,MAAM,SAAS,wBAAwB,IACvC,MAAM,SAAS,0BAA0B,IACzC,MAAM,SAAS,yBAAyB,CAMxC,OAJY,IAAI,aACd,MAAM,SAAS,WAAW,GAAG,MAAM,8BACnC,aACD;AAOH,QAAI,yBAAyB,YAC3B,OAAM;IAER,MAAM,QAAQ,IAAI,WAAW,qBAAqB,GAC9C,IAAI,MAAM,GAA4B,GACtC;AACJ,UAAM,eAAe,gBAAgB,MAAM;;GAK7C,MAAM,gBAAgB,UAAU;AAChC,SACE,aAAa,SACT;IAAE,GAAG;IAAe;IAAU,GAC9B;KAGR,yBACA,YACD;;;;;;;;CASH,MAAc,sBACZ,UACA,OACA,iBAGA,QAC8B;EAC9B,MAAM,SAAS,MAAM,kBACnB,UACA,OACA,iBACA,OACD;AACD,SAAO,kBAAkB,OAAO,MAAM;GACpC,QAAQ,OAAO;GACf,cAAc,OAAO;GACtB,CAAC;;;;;;;;;;;CAYJ,AAAQ,uBACN,KACA,YACM;EACN,MAAM,QAAQ,WAAW;AACzB,MAAI,CAAC,SAAS,MAAM,WAAW,EAAG;EAElC,MAAM,UAAU,mBAAmB,KAAK,UAAU,MAAM,CAAC;AACzD,MAAI,QAAQ,UAAU,gCAAgC;AACpD,OAAI,UAAU,0BAA0B,QAAQ;AAChD;;AAEF,MAAI,WAAW,YACb,KAAI,UAAU,8BAA8B,WAAW,YAAY;MAEnE,QAAO,KACL,uJACD;;;;;;;;;;;;;CAeL,MAAc,wBACZ,KACA,KACA,WACA,OACA,UACA,YACe;EACf,MAAM,WAAW,WAAW,KAAK,OAAO,IAAI,GAAG;EAC/C,MAAM,cAAc,WAAW,KAAK,cAAc,IAAI,GAAG;EACzD,MAAM,kBAAkB,IAAI,iBAAiB;EAC7C,MAAM,gBAAgB,gBAAgB,OAAO;AAC7C,MAAI,GAAG,SAAS,QAAQ;EACxB,MAAM,SAAS,gBAAgB;EAM/B,MAAM,qBACJ,KAAK,OAAO,2BACZ;EACF,IAAI,WAAW;EACf,MAAM,WAAW,iBAAiB;AAChC,cAAW;AACX,mBAAgB,OAAO;KACtB,mBAAmB;AAEtB,MAAI;AAMF,SAAM,SAAS,2BAA2B,OAAO;GAEjD,MAAM,kBAAkB,MAAM,KAAK,eAAe,mBAChD,OACA,WACD;GAED,MAAM,cAAc,MAAM,gBAAgB;GAI1C,MAAM,aAA+D,EAAE;GACvE,MAAM,QAAQ,kBAKZ,KAAK,sBACH,UACA,WACA,OACA,YACA,YACD,EACD,KAAK,WACL,OACA,iBACA,YACA,QACA;IAGE,gBAAgB,KAAK,iBAAiB,IAAI,YAAY;IACtD,uBAAuB,eACrB,KAAK,iBAAiB,IAAI,aAAa,WAAW;IACrD,CACF;GACD,MAAM,QAAQ,MAAM,MAAM,MAAM;AAEhC,gBAAa,SAAS;AAEtB,OAAI,UAAU,gBAAgB,sCAAsC;AACpE,OAAI,UAAU,iBAAiB,WAAW;AAC1C,QAAK,uBAAuB,KAAK,WAAW;AAE5C,OAAI,CAAC,MAAM,MAAM;AACf,UAAM,WAAW,KAAK,MAAM,MAAM;AAClC,eAAW,MAAM,OAAO,MACtB,OAAM,WAAW,KAAK,IAAI;;AAG9B,OAAI,KAAK;WACF,OAAO;AACd,gBAAa,SAAS;AAKtB,OAAI,UAAU;AACZ,WAAO,KACL,sDACA,mBACD;AACD,QAAI,OAAO,IAAI,CAAC,KAAK;KACnB,OACE;KACF,WAAW;KACX,QAAQ,KAAK;KACd,CAAC;AACF;;AAMF,OAAI,OAAO,SAAS;AAClB,QAAI,IAAI,YAAa,KAAI,SAAS;QAC7B,KAAI,KAAK;AACd;;AAEF,OAAI,IAAI,aAAa;AACnB,WAAO,MAAM,4CAA4C,MAAM;AAC/D,QAAI,QAAQ,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO,MAAM,CAAC,CAAC;AACtE;;AAEF,UAAO,MAAM,yBAAyB,MAAM;GAK5C,MAAM,YACJ,iBAAiB,iBAAiB,MAAM,YAAY;AACtD,OAAI,OAAO,IAAI,CAAC,KAAK;IAInB,OACE,iBAAiB,cACb,MAAM,gBACN;IACN;IACA,QAAQ,KAAK;IACd,CAAC;YACM;AACR,OAAI,IAAI,SAAS,QAAQ;;;;;;;;;;CAW7B,MAAM,2BAA2B,QAAoC;EACnE,MAAM,kBAAkB,oBAAoB;EAC5C,MAAM,cAAc,MAAM,gBAAgB;EAC1C,MAAM,mBACJ,KAAK,OAAO,6BACZ;EACF,MAAM,YAAY,KAAK,OAAO,sBAAsB;AACpD,QAAM,KAAK,UAAU,uBAAuB,iBAAiB,aAAa;GACxE;GACA,WAAW;GACX;GACA,gBAAgB;GACjB,CAAC;;;;;;;;;;;;;;;;;;;;CAqBJ,AAAQ,sBACN,UACA,WACA,OACA,YACA,aACe;EACf,MAAM,cAAc,KAAK,eAAe,UAAU,MAAM;EACxD,MAAM,QAAQ,KAAK;EACnB,MAAM,MAAM,cAAc,OAAO;AACjC,SAAO,EACL,QAAQ,GAAG,QAAQ,kBAAkB,WAAW;AAG9C,OACE,iBAAiB,gBAAgB,YACjC,iBAAiB,WAAW,eAE5B,QAAO,SAAS,MAAM,GAAG,QAAQ,kBAAkB,OAAO;AAO5D,UAAO,MAAM,aACX;IACE;IACA;IACA,KAAK,UAAU,WAAW;IAC1B;IACA;IACD,GACA,iBACC,SAAS,MAAM,GAAG,QAAQ,kBAAkB,gBAAgB,OAAO,EACrE,aACA;IAAE;IAAK,cAAc;IAAQ,CAC9B;KAEJ;;;;;;;;;;;;;;;;;CAkBH,MAAM,MACJ,OACA,YACA,kBACA,QACc;EACd,MAAM,kBAAkB,oBAAoB;EAC5C,MAAM,cAAc,MAAM,gBAAgB;EAE1C,MAAM,EAAE,WAAW,YAAY,kBAC7B,KAAK,eAAe,uBAAuB,OAAO,WAAW;AAa/D,UAXiB,MAAM,KAAK,UAAU,iBACpC,iBACA;GACE;GACA,cAAc;GACd,YAAY;GACZ,GAAG;GACJ,EACD,OACD,EAEe;;CAGlB,MAAM,WAA0B;AAC9B,OAAK,cAAc,UAAU;;CAG/B,AAAQ,QAAQ,EACd,OAAO,WAAW;EAChB,aACE;EACF,QAAQ,EAAE,OAAO,EACf,OAAO,EACJ,QAAQ,CACR,SACC,0FACD,EACJ,CAAC;EACF,aAAa;GACX,QAAQ;GACR,qBAAqB;GACtB;EACD,iBAAiB;EACjB,UAAU,MAAM,WAAW;AACzB,qBAAkB,KAAK,MAAM;AAC7B,UAAO,KAAK,MAAM,KAAK,OAAO,QAAW,QAAW,OAAO;;EAE9D,CAAC,EACH;CAED,gBAAuC;AACrC,SAAO,kBAAkB,KAAK,MAAM;;CAGtC,MAAM,iBACJ,MACA,MACA,QACkB;AAClB,SAAO,oBAAoB,KAAK,OAAO,MAAM,MAAM,OAAO;;;;;;;;;CAU5D,QAAQ,MAAwD;AAC9D,SAAO,oBAAoB,KAAK,MAAM,KAAK,OAAO,KAAK;;;;;;CAOzD,UAAU;AACR,SAAO,EAIL,OAAO,KAAK,OACb;;;;;;;;;;;AAYL,SAAgB,WACd,KACA,OACe;CACf,MAAM,MAAM,OAAO,KAAK,MAAM,QAAQ,MAAM,YAAY,MAAM,WAAW;AAKzE,KAAI,IAAI,aAAa,IAAI,cACvB,QAAO,QAAQ,OACb,IAAI,aAAa,8BAA8B,aAAa,CAC7D;AAEH,KAAI,IAAI,MAAM,IAAI,CAAE,QAAO,QAAQ,SAAS;AAO5C,QAAO,IAAI,SAAe,SAAS,WAAW;EAC5C,MAAM,gBAAgB;AACpB,OAAI,IAAI,SAAS,QAAQ;AACzB,OAAI,IAAI,SAAS,QAAQ;AACzB,OAAI,IAAI,SAAS,QAAQ;;EAE3B,MAAM,gBAAgB;AACpB,YAAS;AACT,YAAS;;EAEX,MAAM,gBAAgB;AACpB,YAAS;AACT,UAAO,IAAI,aAAa,8BAA8B,aAAa,CAAC;;AAEtE,MAAI,KAAK,SAAS,QAAQ;AAC1B,MAAI,KAAK,SAAS,QAAQ;AAC1B,MAAI,KAAK,SAAS,QAAQ;GAC1B;;;;;;;;AASJ,MAAM,sCAAsC;;;;;;;AAQ5C,MAAM,iCAAiC;;;;AAKvC,MAAa,YAAY,SAAS,gBAAgB"}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { BasePluginConfig, PluginConstructor } from "../../shared/src/plugin.js";
|
|
2
2
|
import "../../shared/src/index.js";
|
|
3
3
|
import { Schema } from "../../database/schema-builder/types.js";
|
|
4
|
+
import { PluginManifest } from "../../registry/types.js";
|
|
4
5
|
import { Plugin } from "../../plugin/plugin.js";
|
|
5
6
|
import "../../plugin/index.js";
|
|
6
|
-
import { PluginManifest } from "../../registry/types.js";
|
|
7
7
|
import "../../registry/index.js";
|
|
8
8
|
import { DatabaseExports } from "./entity-types.js";
|
|
9
9
|
import { DefaultDatabaseSchema, IDatabaseConfig } from "./types.js";
|
|
@@ -2,10 +2,10 @@ import { AgentToolDefinition, ToolProvider } from "../../shared/src/agent.js";
|
|
|
2
2
|
import { IAppRouter, ToPlugin } from "../../shared/src/plugin.js";
|
|
3
3
|
import "../../shared/src/index.js";
|
|
4
4
|
import { ToolkitEntry, ToolkitOptions } from "../../core/agent/types.js";
|
|
5
|
+
import { PluginManifest, ResourceRequirement } from "../../registry/types.js";
|
|
5
6
|
import { Plugin } from "../../plugin/plugin.js";
|
|
6
7
|
import "../../plugin/index.js";
|
|
7
8
|
import { FilePolicy, FilePolicyUser } from "./policy.js";
|
|
8
|
-
import { PluginManifest, ResourceRequirement } from "../../registry/types.js";
|
|
9
9
|
import "../../registry/index.js";
|
|
10
10
|
import { FilesExport, IFilesConfig, VolumeAPI, VolumeConfig } from "./types.js";
|
|
11
11
|
import "../../index.js";
|
|
@@ -69,7 +69,7 @@ declare class FilesPlugin extends Plugin implements ToolProvider {
|
|
|
69
69
|
* return a policy user explicitly marked `isServicePrincipal: true`, so
|
|
70
70
|
* even in dev a `usersOnly`-style policy that gates on
|
|
71
71
|
* `!user.isServicePrincipal` cannot be tricked. The matching SDK execution
|
|
72
|
-
* path also falls through to the SP client (no `
|
|
72
|
+
* path also falls through to the SP client (no `runInCallerContext` wrap),
|
|
73
73
|
* so the policy user and the SDK identity stay aligned.
|
|
74
74
|
*/
|
|
75
75
|
private _extractUser;
|
|
@@ -104,7 +104,7 @@ declare class FilesPlugin extends Plugin implements ToolProvider {
|
|
|
104
104
|
* NOTE: This method only selects which identity the *policy* sees. The
|
|
105
105
|
* matching SDK execution identity is selected separately by
|
|
106
106
|
* `_resolveAuthForRequest` and applied via `_runWithAuth` /
|
|
107
|
-
* `
|
|
107
|
+
* `runInCallerContext` in each handler. The two selections are designed to
|
|
108
108
|
* converge on the same identity per the policy-user matrix in the docs —
|
|
109
109
|
* see `docs/docs/plugins/files.md#policy-user-matrix`.
|
|
110
110
|
*/
|
|
@@ -197,7 +197,7 @@ declare class FilesPlugin extends Plugin implements ToolProvider {
|
|
|
197
197
|
private _handleDelete;
|
|
198
198
|
private _resolveAuth;
|
|
199
199
|
/**
|
|
200
|
-
* Build a `
|
|
200
|
+
* Build a `CallerContext` from request headers when both
|
|
201
201
|
* `x-forwarded-access-token` and `x-forwarded-user` are present, otherwise
|
|
202
202
|
* return `null`. Used by OBO route handlers to wrap SDK calls in the
|
|
203
203
|
* end-user's identity. A `null` result means "fall back to the service
|
|
@@ -209,7 +209,7 @@ declare class FilesPlugin extends Plugin implements ToolProvider {
|
|
|
209
209
|
/**
|
|
210
210
|
* Build the telemetry attribute hash for the `files.auth_mode` span
|
|
211
211
|
* attribute. The value reflects what operationally happened — i.e.
|
|
212
|
-
* whether `
|
|
212
|
+
* whether `runInCallerContext` actually wrapped the SDK call:
|
|
213
213
|
* - HTTP route on OBO volume + valid token → `"on-behalf-of-user"`.
|
|
214
214
|
* - HTTP route on OBO volume + dev-fallback (no token) →
|
|
215
215
|
* `"service-principal"` (the route falls through to the SP client).
|
|
@@ -221,12 +221,12 @@ declare class FilesPlugin extends Plugin implements ToolProvider {
|
|
|
221
221
|
private _authModeAttributes;
|
|
222
222
|
/**
|
|
223
223
|
* One-shot resolver for HTTP route handlers. Builds the request's
|
|
224
|
-
* `
|
|
224
|
+
* `CallerContext` AT MOST ONCE (when the volume is OBO and the headers are
|
|
225
225
|
* present) and returns both the operationally-effective auth mode and the
|
|
226
|
-
* pre-built `
|
|
226
|
+
* pre-built `CallerContext`.
|
|
227
227
|
*
|
|
228
228
|
* Handlers thread the `userCtx` into `_runWithAuth(userCtx, fn)` to avoid
|
|
229
|
-
* a second `ServiceContext.
|
|
229
|
+
* a second `ServiceContext.createCallerContext()` allocation. That call
|
|
230
230
|
* builds a fresh `WorkspaceClient` per invocation, so doing it twice per
|
|
231
231
|
* request was pure throwaway overhead.
|
|
232
232
|
*/
|
|
@@ -237,7 +237,7 @@ declare class FilesPlugin extends Plugin implements ToolProvider {
|
|
|
237
237
|
* `WorkspaceClient` and `getCurrentUserId()` are used — identical
|
|
238
238
|
* behavior to pre-OBO releases. This covers both SP volumes and the
|
|
239
239
|
* OBO dev-fallback path (where headers were missing).
|
|
240
|
-
* - `userCtx` is a `
|
|
240
|
+
* - `userCtx` is a `CallerContext`: wraps `fn` in `runInCallerContext(userCtx)`,
|
|
241
241
|
* so SDK calls execute as the end user and `getCurrentUserId()` (and
|
|
242
242
|
* therefore cache keys) resolve to the user's ID.
|
|
243
243
|
*
|
|
@@ -275,7 +275,7 @@ declare class FilesPlugin extends Plugin implements ToolProvider {
|
|
|
275
275
|
private _wrapVolumeAPIWithSPSpan;
|
|
276
276
|
/**
|
|
277
277
|
* Wrap each `VolumeAPI` method so its execution runs inside
|
|
278
|
-
* `
|
|
278
|
+
* `runInCallerContext(userCtx, ...)`. Used by `VolumeHandle.asUser(req)` to
|
|
279
279
|
* force the SDK identity to the end user regardless of the volume's
|
|
280
280
|
* `auth` setting. The policy check baked into each method (via
|
|
281
281
|
* `createVolumeAPI`) runs inside the same scope, so `getCurrentUserId()`
|
|
@@ -318,7 +318,7 @@ declare class FilesPlugin extends Plugin implements ToolProvider {
|
|
|
318
318
|
* through the HTTP routes run as the end user; for programmatic calls
|
|
319
319
|
* outside a route, use `asUser(req)` to opt into per-user execution.
|
|
320
320
|
* `asUser(req)` is a hard override at the SDK level: it forces every
|
|
321
|
-
* subsequent call to execute as the end user inside `
|
|
321
|
+
* subsequent call to execute as the end user inside `runInCallerContext`,
|
|
322
322
|
* regardless of the volume's `auth` setting. Policies control per-user
|
|
323
323
|
* access in either mode.
|
|
324
324
|
*
|