@databricks/appkit 0.82.0 → 0.84.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 +21 -1
- package/dist/agents/databricks.d.ts +15 -3
- package/dist/agents/databricks.d.ts.map +1 -1
- package/dist/agents/databricks.js +25 -8
- package/dist/agents/databricks.js.map +1 -1
- package/dist/appkit/package.js +1 -1
- package/dist/beta.d.ts +2 -2
- package/dist/cache/index.d.ts.map +1 -1
- package/dist/cache/index.js +6 -1
- package/dist/cache/index.js.map +1 -1
- package/dist/cli/commands/agent/eval.js +1 -1
- package/dist/cli/commands/generate-types.js +1 -1
- package/dist/cli/commands/plugin/sync/sync.js +28 -15
- package/dist/cli/commands/plugin/sync/sync.js.map +1 -1
- package/dist/cli/commands/registry/add.js +3 -11
- package/dist/cli/commands/registry/add.js.map +1 -1
- package/dist/cli/commands/registry/config-writer.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.d.ts.map +1 -1
- package/dist/connectors/lakebase/routing-pool.js +6 -12
- package/dist/connectors/lakebase/routing-pool.js.map +1 -1
- package/dist/context/caller-context.d.ts +25 -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 +40 -4
- package/dist/context/execution-context.d.ts.map +1 -1
- package/dist/context/execution-context.js +87 -33
- package/dist/context/execution-context.js.map +1 -1
- package/dist/context/index.d.ts +3 -2
- package/dist/context/index.js +3 -1
- package/dist/context/request-scope.d.ts +2 -0
- package/dist/context/request-scope.js +39 -0
- package/dist/context/request-scope.js.map +1 -0
- package/dist/context/resource-capabilities.js +59 -0
- package/dist/context/resource-capabilities.js.map +1 -0
- package/dist/context/scoped-api.js +104 -0
- package/dist/context/scoped-api.js.map +1 -0
- 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 +45 -55
- package/dist/context/service-context.js.map +1 -1
- package/dist/context/user-context.d.ts +16 -22
- package/dist/context/user-context.d.ts.map +1 -1
- package/dist/context/user-context.js +57 -3
- package/dist/context/user-context.js.map +1 -1
- package/dist/core/agent/load-agents.d.ts.map +1 -1
- package/dist/core/agent/load-agents.js +5 -2
- package/dist/core/agent/load-agents.js.map +1 -1
- package/dist/core/agent/run-agent.d.ts +19 -6
- package/dist/core/agent/run-agent.d.ts.map +1 -1
- package/dist/core/agent/run-agent.js +53 -17
- package/dist/core/agent/run-agent.js.map +1 -1
- package/dist/core/agent/types.d.ts +18 -1
- package/dist/core/agent/types.d.ts.map +1 -1
- package/dist/core/agent/types.js.map +1 -1
- package/dist/core/appkit.d.ts +4 -3
- package/dist/core/appkit.d.ts.map +1 -1
- package/dist/core/appkit.js +34 -11
- package/dist/core/appkit.js.map +1 -1
- package/dist/core/plugin-context.d.ts +14 -15
- package/dist/core/plugin-context.d.ts.map +1 -1
- package/dist/core/plugin-context.js +24 -14
- package/dist/core/plugin-context.js.map +1 -1
- package/dist/errors/base.d.ts +2 -2
- package/dist/errors/base.js +2 -2
- package/dist/errors/base.js.map +1 -1
- package/dist/errors/identity-expired.d.ts +14 -0
- package/dist/errors/identity-expired.d.ts.map +1 -0
- package/dist/errors/identity-expired.js +33 -0
- package/dist/errors/identity-expired.js.map +1 -0
- package/dist/errors/index.js +1 -0
- package/dist/index.d.ts +12 -6
- package/dist/index.js +7 -2
- package/dist/logging/logger.js +1 -1
- package/dist/plugin/execution-result.d.ts +4 -1
- package/dist/plugin/execution-result.d.ts.map +1 -1
- package/dist/plugin/interceptors/telemetry.js +6 -4
- package/dist/plugin/interceptors/telemetry.js.map +1 -1
- package/dist/plugin/plugin.d.ts +14 -22
- package/dist/plugin/plugin.d.ts.map +1 -1
- package/dist/plugin/plugin.js +42 -123
- package/dist/plugin/plugin.js.map +1 -1
- package/dist/plugins/agents/agents.d.ts +13 -1
- package/dist/plugins/agents/agents.d.ts.map +1 -1
- package/dist/plugins/agents/agents.js +65 -15
- package/dist/plugins/agents/agents.js.map +1 -1
- package/dist/plugins/agents/auth-mode.js +42 -0
- package/dist/plugins/agents/auth-mode.js.map +1 -0
- package/dist/plugins/agents/index.d.ts +1 -1
- package/dist/plugins/agents/mlflow.js +11 -3
- package/dist/plugins/agents/mlflow.js.map +1 -1
- package/dist/plugins/agents/tool-dispatch.js +9 -1
- package/dist/plugins/agents/tool-dispatch.js.map +1 -1
- package/dist/plugins/ai-search/ai-search.d.ts +1 -1
- package/dist/plugins/ai-search/ai-search.d.ts.map +1 -1
- package/dist/plugins/ai-search/ai-search.js +4 -4
- package/dist/plugins/ai-search/ai-search.js.map +1 -1
- package/dist/plugins/analytics/analytics.d.ts +3 -3
- package/dist/plugins/analytics/analytics.d.ts.map +1 -1
- package/dist/plugins/analytics/analytics.js +9 -7
- package/dist/plugins/analytics/analytics.js.map +1 -1
- package/dist/plugins/database/crud/contract.js +2 -2
- package/dist/plugins/database/crud/contract.js.map +1 -1
- package/dist/plugins/database/database.d.ts +1 -1
- package/dist/plugins/files/plugin.d.ts +21 -15
- package/dist/plugins/files/plugin.d.ts.map +1 -1
- package/dist/plugins/files/plugin.js +50 -29
- package/dist/plugins/files/plugin.js.map +1 -1
- package/dist/plugins/genie/genie.d.ts +10 -1
- package/dist/plugins/genie/genie.d.ts.map +1 -1
- package/dist/plugins/genie/genie.js +12 -3
- package/dist/plugins/genie/genie.js.map +1 -1
- package/dist/plugins/genie/manifest.js +1 -0
- package/dist/plugins/jobs/plugin.d.ts +1 -1
- package/dist/plugins/jobs/plugin.js +2 -2
- package/dist/plugins/jobs/plugin.js.map +1 -1
- package/dist/plugins/lakebase/lakebase.d.ts +11 -18
- package/dist/plugins/lakebase/lakebase.d.ts.map +1 -1
- package/dist/plugins/lakebase/lakebase.js +16 -21
- package/dist/plugins/lakebase/lakebase.js.map +1 -1
- package/dist/plugins/server/client-config-sanitizer.js +1 -4
- package/dist/plugins/server/client-config-sanitizer.js.map +1 -1
- package/dist/plugins/server/dev-obo-middleware.js +60 -0
- package/dist/plugins/server/dev-obo-middleware.js.map +1 -0
- package/dist/plugins/server/index.d.ts +1 -1
- package/dist/plugins/server/index.d.ts.map +1 -1
- package/dist/plugins/server/index.js +3 -0
- package/dist/plugins/server/index.js.map +1 -1
- package/dist/plugins/serving/manifest.js +1 -0
- package/dist/plugins/serving/serving.d.ts +1 -1
- package/dist/plugins/serving/serving.js +6 -6
- package/dist/plugins/serving/serving.js.map +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 +44 -1
- package/dist/schemas/manifest.d.ts.map +1 -1
- package/dist/schemas/manifest.js +122 -3
- package/dist/schemas/manifest.js.map +1 -1
- package/dist/shared/src/dev-obo.js +86 -0
- package/dist/shared/src/dev-obo.js.map +1 -0
- package/dist/shared/src/index.d.ts +1 -1
- package/dist/shared/src/plugin.d.ts +12 -1
- package/dist/shared/src/plugin.d.ts.map +1 -1
- package/dist/shared/src/schemas/manifest.d.ts +17 -0
- package/dist/shared/src/schemas/manifest.d.ts.map +1 -1
- package/dist/shared/src/schemas/manifest.js +53 -3
- package/dist/shared/src/schemas/manifest.js.map +1 -1
- package/dist/stream/stream-manager.d.ts.map +1 -1
- package/dist/stream/stream-manager.js +5 -2
- package/dist/stream/stream-manager.js.map +1 -1
- package/dist/telemetry/execution-span-processor.js +21 -0
- package/dist/telemetry/execution-span-processor.js.map +1 -0
- package/dist/telemetry/telemetry-manager.js +2 -1
- package/dist/telemetry/telemetry-manager.js.map +1 -1
- package/dist/testing/create-test-app.d.ts +2 -2
- 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/testing/test-plugin-context.d.ts +3 -12
- package/dist/testing/test-plugin-context.d.ts.map +1 -1
- package/dist/testing/test-plugin-context.js +24 -16
- package/dist/testing/test-plugin-context.js.map +1 -1
- package/dist/utils/is-plain-object.js +11 -0
- package/dist/utils/is-plain-object.js.map +1 -0
- package/docs/api/appkit/Class.AppKitError.md +2 -1
- package/docs/api/appkit/Class.AuthenticationError.md +1 -1
- package/docs/api/appkit/Class.ConfigurationError.md +1 -1
- package/docs/api/appkit/Class.ConnectionError.md +1 -1
- package/docs/api/appkit/Class.DatabaseValidationError.md +1 -1
- package/docs/api/appkit/Class.ExecutionError.md +1 -1
- package/docs/api/appkit/Class.IdentityExpiredError.md +190 -0
- package/docs/api/appkit/Class.InitializationError.md +1 -1
- package/docs/api/appkit/Class.Plugin.md +9 -13
- package/docs/api/appkit/Class.ServerError.md +1 -1
- package/docs/api/appkit/Class.ServiceContext.md +169 -0
- package/docs/api/appkit/Class.TunnelError.md +1 -1
- package/docs/api/appkit/Class.ValidationError.md +1 -1
- package/docs/api/appkit/Function.createApp.md +12 -12
- package/docs/api/appkit/Function.getCallerContext.md +12 -0
- package/docs/api/appkit/Function.getCurrentActorId.md +12 -0
- package/docs/api/appkit/Function.getCurrentPrincipalKey.md +12 -0
- package/docs/api/appkit/Function.getCurrentUserId.md +14 -0
- package/docs/api/appkit/Function.getExecutionContext.md +5 -3
- package/docs/api/appkit/Function.getUserContext.md +16 -0
- package/docs/api/appkit/Function.getWarehouseId.md +20 -0
- package/docs/api/appkit/Function.isInUserContext.md +12 -0
- package/docs/api/appkit/Function.isUserContext.md +20 -0
- package/docs/api/appkit/Function.runAgent.md +1 -1
- package/docs/api/appkit/Function.runInCallerContext.md +27 -0
- package/docs/api/appkit/Function.runInUserContext.md +29 -0
- package/docs/api/appkit/Interface.AgentDefinition.md +11 -0
- package/docs/api/appkit/Interface.AgentsPluginConfig.md +11 -0
- package/docs/api/appkit/Interface.CallerContext.md +54 -0
- package/docs/api/appkit/Interface.IndexConfig.md +1 -1
- package/docs/api/appkit/Interface.PluginManifest.md +33 -2
- package/docs/api/appkit/Interface.RegisteredAgent.md +11 -0
- package/docs/api/appkit/Interface.RunAgentInput.md +45 -1
- package/docs/api/appkit/TypeAlias.AgentAuth.md +8 -0
- package/docs/api/appkit/TypeAlias.AppKitApi.md +35 -0
- package/docs/api/appkit/TypeAlias.CallerPrincipal.md +13 -0
- package/docs/api/appkit/TypeAlias.ExecutionContext.md +9 -0
- package/docs/api/appkit/TypeAlias.ExecutionResult.md +65 -0
- package/docs/api/appkit/TypeAlias.ScopedPluginMap.md +12 -0
- package/docs/api/appkit/TypeAlias.UserContext.md +109 -0
- package/docs/api/appkit/TypeAlias.UserScopedApp.md +39 -0
- package/docs/api/appkit.md +20 -0
- package/docs/plugins/agents.md +45 -0
- package/docs/plugins/analytics.md +8 -0
- package/docs/plugins/execution-context.md +101 -50
- package/docs/plugins/lakebase.md +6 -82
- package/llms.txt +21 -1
- package/package.json +1 -1
- package/sbom.cdx.json +1 -1
package/dist/plugin/plugin.js
CHANGED
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
import { camelToKebab } from "../shared/src/naming.js";
|
|
2
2
|
import { AppKitError } from "../errors/base.js";
|
|
3
3
|
import { AuthenticationError } from "../errors/authentication.js";
|
|
4
|
+
import { IdentityExpiredError } from "../errors/identity-expired.js";
|
|
4
5
|
import "../errors/index.js";
|
|
5
|
-
import { ServiceContext } from "../context/service-context.js";
|
|
6
6
|
import { createLogger } from "../logging/logger.js";
|
|
7
|
-
import {
|
|
7
|
+
import { warnContextDeprecation } from "../context/deprecation.js";
|
|
8
|
+
import { getCurrentPrincipalId, normalizeIdentityError } from "../context/execution-context.js";
|
|
9
|
+
import { isPlainObject } from "../utils/is-plain-object.js";
|
|
10
|
+
import { scopePlugin } from "../context/scoped-api.js";
|
|
11
|
+
import { assertPluginExecution, getPluginResourceTypes } from "../context/resource-capabilities.js";
|
|
8
12
|
import { normalizeTelemetryOptions } from "../telemetry/config.js";
|
|
9
13
|
import { TelemetryManager } from "../telemetry/telemetry-manager.js";
|
|
10
14
|
import "../telemetry/index.js";
|
|
11
15
|
import { deepMerge } from "../utils/merge.js";
|
|
12
16
|
import { CacheManager } from "../cache/index.js";
|
|
13
17
|
import "../context/index.js";
|
|
18
|
+
import { createRequestScope, isDevOboFallback } from "../context/request-scope.js";
|
|
14
19
|
import { AppManager } from "../app/index.js";
|
|
15
20
|
import { StreamManager } from "../stream/stream-manager.js";
|
|
16
21
|
import "../stream/index.js";
|
|
@@ -20,55 +25,11 @@ import { CacheInterceptor } from "./interceptors/cache.js";
|
|
|
20
25
|
import { RetryInterceptor } from "./interceptors/retry.js";
|
|
21
26
|
import { TelemetryInterceptor } from "./interceptors/telemetry.js";
|
|
22
27
|
import { TimeoutInterceptor } from "./interceptors/timeout.js";
|
|
23
|
-
import { context
|
|
28
|
+
import { context } from "@opentelemetry/api";
|
|
24
29
|
|
|
25
30
|
//#region src/plugin/plugin.ts
|
|
26
31
|
const logger = createLogger("plugin");
|
|
27
32
|
/**
|
|
28
|
-
* OTel context key for marking OBO dev mode fallback.
|
|
29
|
-
* Set when asUser() is called in development mode without a user token.
|
|
30
|
-
*/
|
|
31
|
-
const DEV_OBO_FALLBACK_KEY = createContextKey("appkit.devOboFallback");
|
|
32
|
-
/**
|
|
33
|
-
* Returns true if `value` is a plain object literal (not an array, Date,
|
|
34
|
-
* class instance, etc.). Used to decide whether to recurse into nested
|
|
35
|
-
* export shapes when wrapping functions.
|
|
36
|
-
*
|
|
37
|
-
* @internal exported so the AppKit core can reuse the same predicate for
|
|
38
|
-
* its `bindExportMethods` walk; not part of the public package surface.
|
|
39
|
-
*/
|
|
40
|
-
function isPlainObject(value) {
|
|
41
|
-
if (typeof value !== "object" || value === null) return false;
|
|
42
|
-
const proto = Object.getPrototypeOf(value);
|
|
43
|
-
return proto === Object.prototype || proto === null;
|
|
44
|
-
}
|
|
45
|
-
/**
|
|
46
|
-
* Returns a deep copy of `exports` where every function has been replaced
|
|
47
|
-
* with `wrap(fn)`, walking into nested plain objects.
|
|
48
|
-
*
|
|
49
|
-
* Used by the asUser proxy to make the user context follow function
|
|
50
|
-
* references that escape the proxy via `exports()`. The original input is
|
|
51
|
-
* not mutated, so plugins that memoize `exports()` are safe — each call
|
|
52
|
-
* through the proxy yields an independent, freshly wrapped view.
|
|
53
|
-
*/
|
|
54
|
-
function wrapExportFunctions(exports, wrap) {
|
|
55
|
-
const result = {};
|
|
56
|
-
for (const key of Object.keys(exports)) {
|
|
57
|
-
const val = exports[key];
|
|
58
|
-
if (typeof val === "function") result[key] = wrap(val);
|
|
59
|
-
else if (isPlainObject(val)) result[key] = wrapExportFunctions(val, wrap);
|
|
60
|
-
else result[key] = val;
|
|
61
|
-
}
|
|
62
|
-
return result;
|
|
63
|
-
}
|
|
64
|
-
/**
|
|
65
|
-
* Returns true if the current execution is an OBO dev mode fallback
|
|
66
|
-
* (asUser() was called but fell back to service principal due to missing token).
|
|
67
|
-
*/
|
|
68
|
-
function isDevOboFallback() {
|
|
69
|
-
return context.active().getValue(DEV_OBO_FALLBACK_KEY) === true;
|
|
70
|
-
}
|
|
71
|
-
/**
|
|
72
33
|
* Narrow an unknown thrown value to an Error that carries a numeric
|
|
73
34
|
* `statusCode` property (e.g. `ApiError` from `@databricks/sdk-experimental`).
|
|
74
35
|
*/
|
|
@@ -76,23 +37,6 @@ function hasHttpStatusCode(error) {
|
|
|
76
37
|
return error instanceof Error && "statusCode" in error && typeof error.statusCode === "number";
|
|
77
38
|
}
|
|
78
39
|
/**
|
|
79
|
-
* Methods that should not be proxied by asUser().
|
|
80
|
-
* These are lifecycle/internal methods that don't make sense
|
|
81
|
-
* to execute in a user context.
|
|
82
|
-
*/
|
|
83
|
-
const EXCLUDED_FROM_PROXY = new Set([
|
|
84
|
-
"setup",
|
|
85
|
-
"shutdown",
|
|
86
|
-
"attachContext",
|
|
87
|
-
"injectRoutes",
|
|
88
|
-
"getEndpoints",
|
|
89
|
-
"getSkipBodyParsingPaths",
|
|
90
|
-
"abortActiveOperations",
|
|
91
|
-
"clientConfig",
|
|
92
|
-
"asUser",
|
|
93
|
-
"constructor"
|
|
94
|
-
]);
|
|
95
|
-
/**
|
|
96
40
|
* Base abstract class for creating AppKit plugins.
|
|
97
41
|
*
|
|
98
42
|
* All plugins must declare a static `manifest` property with their metadata
|
|
@@ -321,7 +265,7 @@ var Plugin = class {
|
|
|
321
265
|
*
|
|
322
266
|
* Returns the `x-forwarded-user` header when present. In development mode
|
|
323
267
|
* (`NODE_ENV=development`) falls back to the current context user ID so
|
|
324
|
-
* that callers outside an active `
|
|
268
|
+
* that callers outside an active `runInCallerContext` scope still get a
|
|
325
269
|
* consistent value.
|
|
326
270
|
*
|
|
327
271
|
* @throws AuthenticationError in production when no user header is present.
|
|
@@ -329,72 +273,39 @@ var Plugin = class {
|
|
|
329
273
|
resolveUserId(req) {
|
|
330
274
|
const userId = req.header("x-forwarded-user")?.trim();
|
|
331
275
|
if (userId) return userId;
|
|
332
|
-
if (process.env.NODE_ENV === "development") return
|
|
276
|
+
if (process.env.NODE_ENV === "development") return getCurrentPrincipalId();
|
|
333
277
|
throw AuthenticationError.missingUserId();
|
|
334
278
|
}
|
|
335
|
-
/**
|
|
336
|
-
* Execute operations using the user's identity from the request.
|
|
337
|
-
* Returns a proxy of this plugin where all method calls execute
|
|
338
|
-
* with the user's Databricks credentials instead of the service principal.
|
|
339
|
-
*
|
|
340
|
-
* @param req - The Express request containing the user token in headers
|
|
341
|
-
* @returns A proxied plugin instance that executes as the user
|
|
342
|
-
* @throws AuthenticationError if user token is not available in request headers (production only).
|
|
343
|
-
* In development mode (`NODE_ENV=development`), skips user impersonation instead of throwing.
|
|
344
|
-
*/
|
|
279
|
+
/** @deprecated Use appkit.asUser(req) to scope the whole app. */
|
|
345
280
|
asUser(req) {
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
const isDev = process.env.NODE_ENV === "development";
|
|
350
|
-
if (!token && isDev) {
|
|
351
|
-
logger.warn("asUser() called without user token in development mode. Skipping user impersonation.");
|
|
352
|
-
return this._createAsUserProxy((fn) => (...args) => {
|
|
353
|
-
const ctx = context.active().setValue(DEV_OBO_FALLBACK_KEY, true);
|
|
354
|
-
return context.with(ctx, () => fn(...args));
|
|
355
|
-
});
|
|
356
|
-
}
|
|
357
|
-
if (!token) throw AuthenticationError.missingToken("user token");
|
|
358
|
-
if (!userId && !isDev) throw AuthenticationError.missingUserId();
|
|
359
|
-
const effectiveUserId = userId || "dev-user";
|
|
360
|
-
const userContext = ServiceContext.createUserContext(token, effectiveUserId, void 0, userEmail ?? void 0);
|
|
361
|
-
return this._createAsUserProxy((fn) => (...args) => runInUserContext(userContext, () => fn(...args)));
|
|
281
|
+
warnContextDeprecation("Plugin.asUser", "appkit.asUser(req)");
|
|
282
|
+
assertPluginExecution(this);
|
|
283
|
+
return scopePlugin(this, createRequestScope(req, getPluginResourceTypes(this), { legacy: true }));
|
|
362
284
|
}
|
|
363
285
|
/**
|
|
364
|
-
*
|
|
365
|
-
*
|
|
286
|
+
* User-scoped executor for the plugin's own on-behalf-of routing (for
|
|
287
|
+
* example `.obo.sql` lanes), using the same caller context as the
|
|
288
|
+
* app-level `appkit.asUser(req)`.
|
|
366
289
|
*
|
|
367
|
-
*
|
|
368
|
-
*
|
|
369
|
-
*
|
|
290
|
+
* Unlike the deprecated public `asUser(req)`, this does not emit a
|
|
291
|
+
* deprecation warning and does not opt out of the app-only resource
|
|
292
|
+
* guards, so internal routing matches current on-behalf-of semantics
|
|
293
|
+
* rather than the retained legacy behavior.
|
|
370
294
|
*
|
|
371
|
-
*
|
|
372
|
-
* exports object never re-enter the proxy's `get` trap. Wrapping them
|
|
373
|
-
* here is the only way to make the user context follow function
|
|
374
|
-
* references back out of the plugin.
|
|
295
|
+
* @internal
|
|
375
296
|
*/
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
if (typeof value !== "function") return value;
|
|
380
|
-
if (typeof prop === "string" && EXCLUDED_FROM_PROXY.has(prop)) return value;
|
|
381
|
-
if (prop === "exports") return () => {
|
|
382
|
-
const raw = value.call(target);
|
|
383
|
-
if (raw == null) return {};
|
|
384
|
-
if (typeof raw === "function") return raw;
|
|
385
|
-
if (isPlainObject(raw)) return wrapExportFunctions(raw, (fn) => wrapCall(fn.bind(target)));
|
|
386
|
-
return raw;
|
|
387
|
-
};
|
|
388
|
-
return wrapCall(value.bind(target));
|
|
389
|
-
} });
|
|
297
|
+
_asUserScoped(req) {
|
|
298
|
+
assertPluginExecution(this);
|
|
299
|
+
return scopePlugin(this, createRequestScope(req, getPluginResourceTypes(this)));
|
|
390
300
|
}
|
|
391
301
|
async executeStream(res, fn, options, userKey) {
|
|
302
|
+
assertPluginExecution(this);
|
|
392
303
|
const { stream: streamConfig, default: defaultConfig, user: userConfig } = options;
|
|
393
304
|
const executeConfig = this._buildExecutionConfig({
|
|
394
305
|
default: defaultConfig,
|
|
395
306
|
user: userConfig
|
|
396
307
|
});
|
|
397
|
-
const effectiveUserKey = userKey ??
|
|
308
|
+
const effectiveUserKey = userKey ?? getCurrentPrincipalId();
|
|
398
309
|
const self = this;
|
|
399
310
|
const parentOtelContext = context.active();
|
|
400
311
|
const asyncWrapperFn = async function* (streamSignal) {
|
|
@@ -408,8 +319,12 @@ var Plugin = class {
|
|
|
408
319
|
return await fn(context$1.signal);
|
|
409
320
|
};
|
|
410
321
|
const result = await context.with(parentOtelContext, () => self._executeWithInterceptors(wrappedFn, interceptors, context$1));
|
|
411
|
-
|
|
412
|
-
|
|
322
|
+
try {
|
|
323
|
+
if (self._checkIfGenerator(result)) yield* result;
|
|
324
|
+
else yield result;
|
|
325
|
+
} catch (error) {
|
|
326
|
+
throw normalizeIdentityError(error);
|
|
327
|
+
}
|
|
413
328
|
};
|
|
414
329
|
await this.streamManager.stream(res, asyncWrapperFn, streamConfig, effectiveUserKey);
|
|
415
330
|
}
|
|
@@ -420,12 +335,14 @@ var Plugin = class {
|
|
|
420
335
|
* - `{ ok: true, data: T }` on success
|
|
421
336
|
* - `{ ok: false, status: number, message: string }` on failure
|
|
422
337
|
*
|
|
423
|
-
*
|
|
338
|
+
* Caller credential expiration retains the failure result and additionally
|
|
339
|
+
* exposes a typed error, preserving existing result-based callers.
|
|
424
340
|
*/
|
|
425
341
|
async execute(fn, options, userKey) {
|
|
342
|
+
assertPluginExecution(this);
|
|
426
343
|
const executeConfig = this._buildExecutionConfig(options);
|
|
427
344
|
const interceptors = this._buildInterceptors(executeConfig);
|
|
428
|
-
const effectiveUserKey = userKey ??
|
|
345
|
+
const effectiveUserKey = userKey ?? getCurrentPrincipalId();
|
|
429
346
|
const context = {
|
|
430
347
|
metadata: /* @__PURE__ */ new Map(),
|
|
431
348
|
userKey: effectiveUserKey
|
|
@@ -435,7 +352,8 @@ var Plugin = class {
|
|
|
435
352
|
ok: true,
|
|
436
353
|
data: await this._executeWithInterceptors(fn, interceptors, context)
|
|
437
354
|
};
|
|
438
|
-
} catch (
|
|
355
|
+
} catch (caught) {
|
|
356
|
+
const error = normalizeIdentityError(caught);
|
|
439
357
|
logger.error("Plugin execution failed", {
|
|
440
358
|
error,
|
|
441
359
|
plugin: this.name
|
|
@@ -443,7 +361,8 @@ var Plugin = class {
|
|
|
443
361
|
if (error instanceof AppKitError) return {
|
|
444
362
|
ok: false,
|
|
445
363
|
status: error.statusCode,
|
|
446
|
-
message: error.message
|
|
364
|
+
message: error.message,
|
|
365
|
+
...error instanceof IdentityExpiredError ? { error } : {}
|
|
447
366
|
};
|
|
448
367
|
if (hasHttpStatusCode(error)) {
|
|
449
368
|
const isDev = process.env.NODE_ENV !== "production";
|
|
@@ -498,5 +417,5 @@ var Plugin = class {
|
|
|
498
417
|
};
|
|
499
418
|
|
|
500
419
|
//#endregion
|
|
501
|
-
export { Plugin
|
|
420
|
+
export { Plugin };
|
|
502
421
|
//# sourceMappingURL=plugin.js.map
|
|
@@ -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 { 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 { getCurrentPrincipalId } from \"../context\";\nimport { warnContextDeprecation } from \"../context/deprecation\";\nimport { normalizeIdentityError } from \"../context/execution-context\";\nimport { createRequestScope } from \"../context/request-scope\";\nimport {\n assertPluginExecution,\n getPluginResourceTypes,\n} from \"../context/resource-capabilities\";\nimport { scopePlugin } from \"../context/scoped-api\";\nimport type { PluginContext } from \"../core/plugin-context\";\nimport { AppKitError, AuthenticationError } from \"../errors\";\nimport { IdentityExpiredError } from \"../errors/identity-expired\";\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\nexport { isPlainObject } from \"../utils/is-plain-object\";\nexport { isDevOboFallback } from \"../context/request-scope\";\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 * 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 getCurrentPrincipalId();\n throw AuthenticationError.missingUserId();\n }\n\n /** @deprecated Use appkit.asUser(req) to scope the whole app. */\n asUser(req: express.Request): this {\n warnContextDeprecation(\"Plugin.asUser\", \"appkit.asUser(req)\");\n assertPluginExecution(this);\n return scopePlugin(\n this,\n createRequestScope(req, getPluginResourceTypes(this), { legacy: true }),\n );\n }\n\n /**\n * User-scoped executor for the plugin's own on-behalf-of routing (for\n * example `.obo.sql` lanes), using the same caller context as the\n * app-level `appkit.asUser(req)`.\n *\n * Unlike the deprecated public `asUser(req)`, this does not emit a\n * deprecation warning and does not opt out of the app-only resource\n * guards, so internal routing matches current on-behalf-of semantics\n * rather than the retained legacy behavior.\n *\n * @internal\n */\n protected _asUserScoped(req: express.Request): this {\n assertPluginExecution(this);\n return scopePlugin(\n this,\n createRequestScope(req, getPluginResourceTypes(this)),\n );\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 assertPluginExecution(this);\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 ?? getCurrentPrincipalId();\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 try {\n if (self._checkIfGenerator(result)) {\n yield* result;\n } else {\n yield result;\n }\n } catch (error) {\n throw normalizeIdentityError(error);\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 * Caller credential expiration retains the failure result and additionally\n * exposes a typed error, preserving existing result-based callers.\n */\n protected async execute<T>(\n fn: (signal?: AbortSignal) => Promise<T>,\n options: PluginExecutionSettings,\n userKey?: string,\n ): Promise<ExecutionResult<T>> {\n assertPluginExecution(this);\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 ?? getCurrentPrincipalId();\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 (caught) {\n const error = normalizeIdentityError(caught);\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 ...(error instanceof IdentityExpiredError ? { error } : {}),\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
|
|
@@ -173,6 +173,18 @@ declare class AgentsPlugin extends Plugin implements ToolProvider {
|
|
|
173
173
|
* does not provide. See {@link collectApprovalRequiredToolNames}.
|
|
174
174
|
*/
|
|
175
175
|
private _handleInvoke;
|
|
176
|
+
/**
|
|
177
|
+
* Fail closed for an on-behalf-of-user agent: without a forwarded user token
|
|
178
|
+
* the request is rejected with 401 before any model or tool call, in
|
|
179
|
+
* production and in development. Other agents are unaffected.
|
|
180
|
+
*/
|
|
181
|
+
private allowsCaller;
|
|
182
|
+
/**
|
|
183
|
+
* Run an on-behalf-of-user agent inside the request's user scope, so the
|
|
184
|
+
* model call, hand-rolled tools, and sub-agents all act as the user. Other
|
|
185
|
+
* agents run exactly as before.
|
|
186
|
+
*/
|
|
187
|
+
private runInAgentScope;
|
|
176
188
|
private _streamAgent;
|
|
177
189
|
/**
|
|
178
190
|
* Non-streaming counterpart to {@link _streamAgent} used by `/invocations`
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agents.d.ts","names":[],"sources":["../../../src/plugins/agents/agents.ts"],"mappings":";;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"agents.d.ts","names":[],"sources":["../../../src/plugins/agents/agents.ts"],"mappings":";;;;;;;;;;cAmHa,YAAA,SAAqB,MAAA,YAAkB,YAAA;EAAA,OAC3C,QAAA,EADiB,cAAA;EAAA,OAEjB,KAAA,EAAO,WAAA;EAAA,UAEI,MAAA,EAAQ,kBAAA;EAAA,QAElB,MAAA;EAAA,QACA,gBAAA;;AAPV;;;UAYU,OAAA;EAAA,QACA,SAAA;EAAA,QACA,WAAA;EAAA,QACA,YAAA;EAqFG;EAAA,QAnFH,0BAAA;EAqxBS;EAAA,QAnxBT,6BAAA;cAEI,MAAA,EAAQ,kBAAA;EA+1BJ;;;;;;;;;;EAp3B8C;;;;;EAAA,QAyDtD,YAAA;EAAA,QAEA,oBAAA;EAAA,YAKI,sBAAA,CAAA;EA5DM;EAAA,YAsEN,cAAA,CAAA;EApEJ;EAAA,QA8EA,gBAAA;EAAA,QAIA,WAAA;EAAA,QAQA,aAAA;EAIF,KAAA,CAAA,GAAK,OAAA;EArFH;;;;;;EA0GF,MAAA,CAAA,GAAU,OAAA;EA9DR;;;;;EAAA,QAwFM,kBAAA;EA/CR;;;;;EAAA,QA4JE,mBAAA;EAiBA;;;;EAAA,QAAA,uBAAA;EAuFA;;;;EAAA,QAxEA,0BAAA;EAAA,QAUA,iBAAA;EAAA,QAIA,cAAA;EAAA,QAOM,mBAAA;EAkUN;;;;;;EAAA,QA/QA,mBAAA;EAAA,QAmBM,oBAAA;EA2bN;EAAA,QApZA,gBAAA;EAgaa;;;;;;EAAA,QAtZb,oBAAA;EA+qBA;EAAA,QA1qBA,gBAAA;EAAA,QAIM,kBAAA;EAAA,QAgBA,cAAA;EAslCN;;;;;EAAA,QAtiCM,cAAA;EAqpCA;;;;;;;;;;;EAAA,QAnhCN,eAAA;;;;;;;;;;;;;UA4BA,eAAA;EAAA,QAwBM,gBAAA;EAAA,QA0DA,kBAAA;EAiEd,aAAA,CAAA,GAAiB,mBAAA;EAIX,gBAAA,CAAA,GAAoB,OAAA;EA+8BT;;;;;;EAAA,QAn8BT,iBAAA;EAYR,YAAA,CAAa,MAAA,EAAQ,UAAA;EAkDrB,YAAA,CAAA,GAAgB,MAAA;EAAA,QAeF,WAAA;;;;;;;;;;;;UA8FN,gCAAA;;;;;;;;;;;UAuBM,aAAA;;;;;;UAmGN,YAAA;;;;;;UA0BA,eAAA;EAAA,QASM,YAAA;;;;;;;;;;;;;;;;;;;;;;;;UA4OA,qBAAA;EAAA,QAiLN,iBAAA;;;;;;;UAgBA,iBAAA;;UAQA,gBAAA;EAAA,QAYM,aAAA;EAAA,QA2BA,cAAA;EAAA,QAuCA,kBAAA;EAAA,QASA,gBAAA;EAAA,QAUA,mBAAA;EAAA,QAaN,YAAA;EAAA,QASA,aAAA;EAgBF,QAAA,CAAA,GAAY,OAAA;EAQlB,OAAA,CAAA;6BAE2B,GAAA,EAAO,eAAA,KAAe,OAAA;;2BAG3B,eAAA;;;oCAGS,OAAA,CAAA,MAAA;EAAA;EAAA,QAIjB,iBAAA;AAAA;;;;;;;;;;;;;;;;;;cA2CH,MAAA,EAAM,QAAA,QAAA,YAAA,EAAA,kBAAA"}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
import { AuthenticationError } from "../../errors/authentication.js";
|
|
2
|
+
import { IdentityExpiredError } from "../../errors/identity-expired.js";
|
|
3
|
+
import "../../errors/index.js";
|
|
1
4
|
import { createLogger } from "../../logging/logger.js";
|
|
2
|
-
import { getWorkspaceClient } from "../../context/execution-context.js";
|
|
5
|
+
import { getWorkspaceClient, normalizeIdentityError, runOutsideCallerScope } from "../../context/execution-context.js";
|
|
3
6
|
import "../../context/index.js";
|
|
7
|
+
import { createRequestScope } from "../../context/request-scope.js";
|
|
4
8
|
import { Plugin } from "../../plugin/plugin.js";
|
|
5
9
|
import { toPlugin } from "../../plugin/to-plugin.js";
|
|
6
10
|
import "../../plugin/index.js";
|
|
@@ -22,6 +26,7 @@ import { loadAgentsFromDir } from "../../core/agent/load-agents.js";
|
|
|
22
26
|
import { ActiveStreamTracker } from "./active-stream-tracker.js";
|
|
23
27
|
import { buildAdapterExtensions, supervisorToolDescription, warnOnCapabilityMismatch } from "./adapter-extensions.js";
|
|
24
28
|
import { requiresApproval } from "./approval.js";
|
|
29
|
+
import { isOnBehalfOfUser, modelClientProvider, requireOboCaller, runInOboAgentRun } from "./auth-mode.js";
|
|
25
30
|
import { LOAD_SKILL_TOOL_DEF, READ_SKILL_FILE_TOOL_DEF } from "./builtin-tools.js";
|
|
26
31
|
import { agentStreamDefaults } from "./defaults.js";
|
|
27
32
|
import { EventChannel } from "./event-channel.js";
|
|
@@ -276,6 +281,8 @@ var AgentsPlugin = class extends Plugin {
|
|
|
276
281
|
}
|
|
277
282
|
async buildRegisteredAgent(name, def, src) {
|
|
278
283
|
const adapter = await this.resolveAdapter(def, name);
|
|
284
|
+
const auth = def.auth ?? this.config.auth;
|
|
285
|
+
if (isOnBehalfOfUser(auth) && (await import("../../agents/databricks.js")).hasFixedWorkspaceClient(adapter)) throw new Error(`Agent '${name}' is on-behalf-of-user, but its model adapter has a fixed workspaceClient, so the model would run as the service principal. Pass \`workspaceClient: () => getWorkspaceClient()\` to the adapter, or use a model string.`);
|
|
279
286
|
const skills = await this.resolveAgentSkills(name, def, src);
|
|
280
287
|
const toolIndex = await this.buildToolIndex(name, def, src, skills);
|
|
281
288
|
warnOnCapabilityMismatch(name, adapter, toolIndex);
|
|
@@ -289,7 +296,8 @@ var AgentsPlugin = class extends Plugin {
|
|
|
289
296
|
maxTokens: def.maxTokens,
|
|
290
297
|
generationParams: def.generationParams,
|
|
291
298
|
ephemeral: def.ephemeral,
|
|
292
|
-
skills
|
|
299
|
+
skills,
|
|
300
|
+
...auth && { auth }
|
|
293
301
|
};
|
|
294
302
|
}
|
|
295
303
|
/** Loads the shared global skill pool from `<agentsDir>/skills/`. */
|
|
@@ -329,7 +337,10 @@ var AgentsPlugin = class extends Plugin {
|
|
|
329
337
|
if (!source) throw new Error(`Agent '${name}' has no model configured and no DATABRICKS_SERVING_ENDPOINT_NAME default available`);
|
|
330
338
|
if (typeof source === "string") {
|
|
331
339
|
const { adapterFromModelString } = await import("../../agents/databricks.js");
|
|
332
|
-
return adapterFromModelString(source,
|
|
340
|
+
return adapterFromModelString(source, {
|
|
341
|
+
...adapterOptions,
|
|
342
|
+
workspaceClient: modelClientProvider()
|
|
343
|
+
});
|
|
333
344
|
}
|
|
334
345
|
return await source;
|
|
335
346
|
}
|
|
@@ -629,6 +640,7 @@ var AgentsPlugin = class extends Plugin {
|
|
|
629
640
|
res.status(400).json({ error: agentName ? `Agent "${agentName}" not found` : "No agent registered" });
|
|
630
641
|
return;
|
|
631
642
|
}
|
|
643
|
+
if (!this.allowsCaller(registered, req, res)) return;
|
|
632
644
|
const userId = this.resolveUserId(req);
|
|
633
645
|
const limits = this.resolvedLimits;
|
|
634
646
|
if (this.countUserStreams(userId) >= limits.maxConcurrentStreamsPerUser) {
|
|
@@ -656,7 +668,7 @@ var AgentsPlugin = class extends Plugin {
|
|
|
656
668
|
res.status(500).json({ error: "Thread operation failed" });
|
|
657
669
|
return;
|
|
658
670
|
}
|
|
659
|
-
return this._streamAgent(req, res, registered, thread, userId, mlflowRunId, skill);
|
|
671
|
+
return this.runInAgentScope(registered, req, () => this._streamAgent(req, res, registered, thread, userId, mlflowRunId, skill));
|
|
660
672
|
}
|
|
661
673
|
/**
|
|
662
674
|
* Returns the names of tools in `registered.toolIndex` whose annotations
|
|
@@ -700,6 +712,7 @@ var AgentsPlugin = class extends Plugin {
|
|
|
700
712
|
res.status(400).json({ error: "No agent registered" });
|
|
701
713
|
return;
|
|
702
714
|
}
|
|
715
|
+
if (!this.allowsCaller(registered, req, res)) return;
|
|
703
716
|
const approvalGated = this.collectApprovalRequiredToolNames(registered);
|
|
704
717
|
if (approvalGated.length > 0) {
|
|
705
718
|
res.status(400).json({ error: `Agent '${registered.name}' exposes ${approvalGated.length} approval-gated tool(s) (${approvalGated.join(", ")}); /invocations and /responses are non-streaming and cannot run HITL. Use POST /chat for HITL-capable agents, or disable approval via agents({ approval: { requireForDestructive: false } }).` });
|
|
@@ -737,7 +750,35 @@ var AgentsPlugin = class extends Plugin {
|
|
|
737
750
|
res.status(500).json({ error: "Thread operation failed" });
|
|
738
751
|
return;
|
|
739
752
|
}
|
|
740
|
-
return this._runAgentNonStreaming(req, res, registered, thread, userId, mlflowRunId);
|
|
753
|
+
return this.runInAgentScope(registered, req, () => this._runAgentNonStreaming(req, res, registered, thread, userId, mlflowRunId));
|
|
754
|
+
}
|
|
755
|
+
/**
|
|
756
|
+
* Fail closed for an on-behalf-of-user agent: without a forwarded user token
|
|
757
|
+
* the request is rejected with 401 before any model or tool call, in
|
|
758
|
+
* production and in development. Other agents are unaffected.
|
|
759
|
+
*/
|
|
760
|
+
allowsCaller(registered, req, res) {
|
|
761
|
+
if (!isOnBehalfOfUser(registered.auth)) return true;
|
|
762
|
+
try {
|
|
763
|
+
requireOboCaller(req);
|
|
764
|
+
return true;
|
|
765
|
+
} catch (error) {
|
|
766
|
+
const failure = error instanceof AuthenticationError ? error : AuthenticationError.missingToken("user token");
|
|
767
|
+
res.status(401).json({
|
|
768
|
+
error: failure.clientMessage,
|
|
769
|
+
code: failure.code
|
|
770
|
+
});
|
|
771
|
+
return false;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
/**
|
|
775
|
+
* Run an on-behalf-of-user agent inside the request's user scope, so the
|
|
776
|
+
* model call, hand-rolled tools, and sub-agents all act as the user. Other
|
|
777
|
+
* agents run exactly as before.
|
|
778
|
+
*/
|
|
779
|
+
runInAgentScope(registered, req, fn) {
|
|
780
|
+
if (!isOnBehalfOfUser(registered.auth)) return fn();
|
|
781
|
+
return createRequestScope(req).run(() => runInOboAgentRun(fn));
|
|
741
782
|
}
|
|
742
783
|
async _streamAgent(req, res, registered, thread, userId, mlflowRunId, forcedSkill) {
|
|
743
784
|
const abortController = new AbortController();
|
|
@@ -811,12 +852,12 @@ var AgentsPlugin = class extends Plugin {
|
|
|
811
852
|
role: "assistant",
|
|
812
853
|
content: fullContent
|
|
813
854
|
});
|
|
814
|
-
await this.threadStore.addMessage(thread.id, userId, {
|
|
855
|
+
await runOutsideCallerScope(() => this.threadStore.addMessage(thread.id, userId, {
|
|
815
856
|
id: randomUUID(),
|
|
816
857
|
role: "assistant",
|
|
817
858
|
content: fullContent,
|
|
818
859
|
createdAt: /* @__PURE__ */ new Date()
|
|
819
|
-
});
|
|
860
|
+
}));
|
|
820
861
|
}
|
|
821
862
|
const mlflowTraceId = currentTraceId();
|
|
822
863
|
if (mlflowTraceId) for (const evt of translator.translate({
|
|
@@ -830,14 +871,15 @@ var AgentsPlugin = class extends Plugin {
|
|
|
830
871
|
outboundEvents.close();
|
|
831
872
|
return;
|
|
832
873
|
}
|
|
833
|
-
|
|
834
|
-
|
|
874
|
+
const failure = normalizeIdentityError(error);
|
|
875
|
+
logger.error("Agent chat error: %O", failure);
|
|
876
|
+
outboundEvents.close(failure);
|
|
835
877
|
return;
|
|
836
878
|
} finally {
|
|
837
879
|
this.approvalGate.abortStream(requestId);
|
|
838
880
|
this.untrackStream(requestId);
|
|
839
881
|
if (registered.ephemeral) try {
|
|
840
|
-
await this.threadStore.delete(thread.id, userId);
|
|
882
|
+
await runOutsideCallerScope(() => this.threadStore.delete(thread.id, userId));
|
|
841
883
|
} catch (err) {
|
|
842
884
|
logger.warn("Failed to delete ephemeral thread %s: %O", thread.id, err);
|
|
843
885
|
}
|
|
@@ -939,12 +981,12 @@ var AgentsPlugin = class extends Plugin {
|
|
|
939
981
|
role: "assistant",
|
|
940
982
|
content: fullContent
|
|
941
983
|
});
|
|
942
|
-
await this.threadStore.addMessage(thread.id, userId, {
|
|
984
|
+
await runOutsideCallerScope(() => this.threadStore.addMessage(thread.id, userId, {
|
|
943
985
|
id: randomUUID(),
|
|
944
986
|
role: "assistant",
|
|
945
987
|
content: fullContent,
|
|
946
988
|
createdAt: /* @__PURE__ */ new Date()
|
|
947
|
-
});
|
|
989
|
+
}));
|
|
948
990
|
}
|
|
949
991
|
mlflowTraceId = currentTraceId();
|
|
950
992
|
});
|
|
@@ -953,7 +995,15 @@ var AgentsPlugin = class extends Plugin {
|
|
|
953
995
|
res.status(499).json({ error: "Request aborted" });
|
|
954
996
|
return;
|
|
955
997
|
}
|
|
956
|
-
|
|
998
|
+
const failure = normalizeIdentityError(error);
|
|
999
|
+
if (failure instanceof IdentityExpiredError) {
|
|
1000
|
+
res.status(401).json({
|
|
1001
|
+
error: failure.clientMessage,
|
|
1002
|
+
code: failure.code
|
|
1003
|
+
});
|
|
1004
|
+
return;
|
|
1005
|
+
}
|
|
1006
|
+
logger.error("Agent invoke error: %O", failure);
|
|
957
1007
|
const message = process.env.NODE_ENV === "production" ? "Internal server error" : error instanceof Error ? error.message : String(error);
|
|
958
1008
|
res.status(500).json({ error: message });
|
|
959
1009
|
return;
|
|
@@ -961,7 +1011,7 @@ var AgentsPlugin = class extends Plugin {
|
|
|
961
1011
|
this.approvalGate.abortStream(requestId);
|
|
962
1012
|
this.untrackStream(requestId);
|
|
963
1013
|
if (registered.ephemeral) try {
|
|
964
|
-
await this.threadStore.delete(thread.id, userId);
|
|
1014
|
+
await runOutsideCallerScope(() => this.threadStore.delete(thread.id, userId));
|
|
965
1015
|
} catch (err) {
|
|
966
1016
|
logger.warn("Failed to delete ephemeral thread %s: %O", thread.id, err);
|
|
967
1017
|
}
|
|
@@ -989,7 +1039,7 @@ var AgentsPlugin = class extends Plugin {
|
|
|
989
1039
|
});
|
|
990
1040
|
}
|
|
991
1041
|
dispatchSkillTool(entry, args) {
|
|
992
|
-
return dispatchSkillTool(entry, args, () => this.skillWorkspaceClient());
|
|
1042
|
+
return runOutsideCallerScope(() => dispatchSkillTool(entry, args, () => this.skillWorkspaceClient()));
|
|
993
1043
|
}
|
|
994
1044
|
/**
|
|
995
1045
|
* Renders the prompt addendum for a force-loaded skill (`/skill-name`).
|