@dbx-tools/appkit 0.9.119 → 0.9.121

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.
@@ -0,0 +1,43 @@
1
+ /** Zod-owned operation contracts converted into native AppKit agent tools. */
2
+ import { defineTool } from "@databricks/appkit/beta";
3
+ /** Preserve operation inference while declaring an exported operation map. */
4
+ export function defineOperations(operations) {
5
+ return operations;
6
+ }
7
+ /** Convert exported Zod operation contracts into a native AppKit tool registry. */
8
+ export function toolRegistryFromOperations(operations) {
9
+ return Object.fromEntries(Object.entries(operations).flatMap(([name, operation]) => {
10
+ const metadata = operation.schema.meta();
11
+ const tool = metadata?.appkitTool ?? {};
12
+ if (tool.agentTool !== true)
13
+ return [];
14
+ const effect = tool.effect ?? "write";
15
+ const approval = tool.approval === true || effect === "destructive";
16
+ const description = operation.schema.description?.trim();
17
+ if (!description) {
18
+ throw new TypeError(`AppKit operation ${name} requires a Zod schema description`);
19
+ }
20
+ return [
21
+ [
22
+ name,
23
+ defineTool({
24
+ description,
25
+ schema: operation.schema,
26
+ annotations: {
27
+ effect,
28
+ ...(approval ? { destructive: true } : {}),
29
+ ...(tool.requiresUserContext === undefined
30
+ ? {}
31
+ : { requiresUserContext: tool.requiresUserContext }),
32
+ ...(tool.idempotent === undefined ? {} : { idempotent: tool.idempotent }),
33
+ },
34
+ ...(tool.autoInheritable === undefined
35
+ ? {}
36
+ : { autoInheritable: tool.autoInheritable }),
37
+ execute: (input, signal) => operation.execute(operation.schema.parse(input), signal),
38
+ }),
39
+ ],
40
+ ];
41
+ }));
42
+ }
43
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidG9vbC1vcGVyYXRpb25zLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL3Rvb2wtb3BlcmF0aW9ucy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSw4RUFBOEU7QUFFOUUsT0FBTyxFQUFFLFVBQVUsRUFBcUIsTUFBTSx5QkFBeUIsQ0FBQztBQThCeEUsOEVBQThFO0FBQzlFLE1BQU0sVUFBVSxnQkFBZ0IsQ0FFOUIsVUFFRDtJQUNDLE9BQU8sVUFBVSxDQUFDO0FBQ3BCLENBQUM7QUFFRCxtRkFBbUY7QUFDbkYsTUFBTSxVQUFVLDBCQUEwQixDQUN4QyxVQUF5RTtJQUV6RSxPQUFPLE1BQU0sQ0FBQyxXQUFXLENBQ3ZCLE1BQU0sQ0FBQyxPQUFPLENBQUMsVUFBVSxDQUFDLENBQUMsT0FBTyxDQUFDLENBQUMsQ0FBQyxJQUFJLEVBQUUsU0FBUyxDQUFDLEVBQUUsRUFBRTtRQUN2RCxNQUFNLFFBQVEsR0FBRyxTQUFTLENBQUMsTUFBTSxDQUFDLElBQUksRUFBMkMsQ0FBQztRQUNsRixNQUFNLElBQUksR0FBRyxRQUFRLEVBQUUsVUFBVSxJQUFJLEVBQUUsQ0FBQztRQUN4QyxJQUFJLElBQUksQ0FBQyxTQUFTLEtBQUssSUFBSTtZQUFFLE9BQU8sRUFBRSxDQUFDO1FBQ3ZDLE1BQU0sTUFBTSxHQUFHLElBQUksQ0FBQyxNQUFNLElBQUksT0FBTyxDQUFDO1FBQ3RDLE1BQU0sUUFBUSxHQUFHLElBQUksQ0FBQyxRQUFRLEtBQUssSUFBSSxJQUFJLE1BQU0sS0FBSyxhQUFhLENBQUM7UUFDcEUsTUFBTSxXQUFXLEdBQUcsU0FBUyxDQUFDLE1BQU0sQ0FBQyxXQUFXLEVBQUUsSUFBSSxFQUFFLENBQUM7UUFDekQsSUFBSSxDQUFDLFdBQVcsRUFBRSxDQUFDO1lBQ2pCLE1BQU0sSUFBSSxTQUFTLENBQUMsb0JBQW9CLElBQUksb0NBQW9DLENBQUMsQ0FBQztRQUNwRixDQUFDO1FBQ0QsT0FBTztZQUNMO2dCQUNFLElBQUk7Z0JBQ0osVUFBVSxDQUFDO29CQUNULFdBQVc7b0JBQ1gsTUFBTSxFQUFFLFNBQVMsQ0FBQyxNQUFNO29CQUN4QixXQUFXLEVBQUU7d0JBQ1gsTUFBTTt3QkFDTixHQUFHLENBQUMsUUFBUSxDQUFDLENBQUMsQ0FBQyxFQUFFLFdBQVcsRUFBRSxJQUFJLEVBQUUsQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDO3dCQUMxQyxHQUFHLENBQUMsSUFBSSxDQUFDLG1CQUFtQixLQUFLLFNBQVM7NEJBQ3hDLENBQUMsQ0FBQyxFQUFFOzRCQUNKLENBQUMsQ0FBQyxFQUFFLG1CQUFtQixFQUFFLElBQUksQ0FBQyxtQkFBbUIsRUFBRSxDQUFDO3dCQUN0RCxHQUFHLENBQUMsSUFBSSxDQUFDLFVBQVUsS0FBSyxTQUFTLENBQUMsQ0FBQyxDQUFDLEVBQUUsQ0FBQyxDQUFDLENBQUMsRUFBRSxVQUFVLEVBQUUsSUFBSSxDQUFDLFVBQVUsRUFBRSxDQUFDO3FCQUMxRTtvQkFDRCxHQUFHLENBQUMsSUFBSSxDQUFDLGVBQWUsS0FBSyxTQUFTO3dCQUNwQyxDQUFDLENBQUMsRUFBRTt3QkFDSixDQUFDLENBQUMsRUFBRSxlQUFlLEVBQUUsSUFBSSxDQUFDLGVBQWUsRUFBRSxDQUFDO29CQUM5QyxPQUFPLEVBQUUsQ0FBQyxLQUFLLEVBQUUsTUFBTSxFQUFFLEVBQUUsQ0FBQyxTQUFTLENBQUMsT0FBTyxDQUFDLFNBQVMsQ0FBQyxNQUFNLENBQUMsS0FBSyxDQUFDLEtBQUssQ0FBQyxFQUFFLE1BQU0sQ0FBQztpQkFDckYsQ0FBQzthQUNIO1NBQ0YsQ0FBQztJQUNKLENBQUMsQ0FBQyxDQUNILENBQUM7QUFDSixDQUFDIiwic291cmNlc0NvbnRlbnQiOlsiLyoqIFpvZC1vd25lZCBvcGVyYXRpb24gY29udHJhY3RzIGNvbnZlcnRlZCBpbnRvIG5hdGl2ZSBBcHBLaXQgYWdlbnQgdG9vbHMuICovXG5cbmltcG9ydCB7IGRlZmluZVRvb2wsIHR5cGUgVG9vbFJlZ2lzdHJ5IH0gZnJvbSBcIkBkYXRhYnJpY2tzL2FwcGtpdC9iZXRhXCI7XG5pbXBvcnQgeyB6IH0gZnJvbSBcInpvZFwiO1xuXG4vKiogTWV0YWRhdGEgc3RvcmVkIHVuZGVyIGBhcHBraXRUb29sYCBieSBhIFpvZCBzY2hlbWEncyBgLm1ldGEoKWAgdmFsdWUuICovXG5leHBvcnQgaW50ZXJmYWNlIEFwcEtpdFRvb2xNZXRhIHtcbiAgLyoqIE9wdCBpbiB0byBleHBvc2luZyB0aGlzIG9wZXJhdGlvbiBhcyBhbiBhZ2VudCB0b29sLiAqL1xuICByZWFkb25seSBhZ2VudFRvb2w/OiBib29sZWFuO1xuICAvKiogVG9vbCBzaWRlLWVmZmVjdCBjbGFzc2lmaWNhdGlvbi4gRGVmYXVsdHMgdG8gYHdyaXRlYC4gKi9cbiAgcmVhZG9ubHkgZWZmZWN0PzogXCJyZWFkXCIgfCBcIndyaXRlXCIgfCBcImRlc3RydWN0aXZlXCI7XG4gIC8qKiBSZXF1aXJlIGh1bWFuIGFwcHJvdmFsIGJlZm9yZSBleGVjdXRpb24uICovXG4gIHJlYWRvbmx5IGFwcHJvdmFsPzogYm9vbGVhbjtcbiAgLyoqIFdoZXRoZXIgZXhlY3V0aW9uIHJlcXVpcmVzIGFuIGF1dGhlbnRpY2F0ZWQgdXNlciBjb250ZXh0LiAqL1xuICByZWFkb25seSByZXF1aXJlc1VzZXJDb250ZXh0PzogYm9vbGVhbjtcbiAgLyoqIFdoZXRoZXIgcmVwZWF0ZWQgY2FsbHMgd2l0aCB0aGUgc2FtZSBpbnB1dCBhcmUgc2FmZS4gKi9cbiAgcmVhZG9ubHkgaWRlbXBvdGVudD86IGJvb2xlYW47XG4gIC8qKiBXaGV0aGVyIEFwcEtpdCBtYXkgaW5oZXJpdCB0aGUgdG9vbCBhdXRvbWF0aWNhbGx5LiAqL1xuICByZWFkb25seSBhdXRvSW5oZXJpdGFibGU/OiBib29sZWFuO1xufVxuXG4vKiogU2hhcGUgcGxhY2VkIGluIGEgWm9kIHNjaGVtYSdzIGAubWV0YSgpYCB2YWx1ZS4gKi9cbmV4cG9ydCBpbnRlcmZhY2UgQXBwS2l0T3BlcmF0aW9uU2NoZW1hTWV0YSB7XG4gIHJlYWRvbmx5IGFwcGtpdFRvb2w/OiBBcHBLaXRUb29sTWV0YTtcbn1cblxuLyoqIE9uZSB0eXBlZCBvcGVyYXRpb24gYmFja2VkIGJ5IGEgc2luZ2xlIFpvZCBpbnB1dCBjb250cmFjdC4gKi9cbmV4cG9ydCBpbnRlcmZhY2UgQXBwS2l0T3BlcmF0aW9uPFRTY2hlbWEgZXh0ZW5kcyB6LlpvZFR5cGUgPSB6LlpvZFR5cGUsIFRSZXN1bHQgPSB1bmtub3duPiB7XG4gIHJlYWRvbmx5IHNjaGVtYTogVFNjaGVtYTtcbiAgcmVhZG9ubHkgZXhlY3V0ZTogKGlucHV0OiB6Lm91dHB1dDxUU2NoZW1hPiwgc2lnbmFsPzogQWJvcnRTaWduYWwpID0+IFRSZXN1bHQgfCBQcm9taXNlPFRSZXN1bHQ+O1xufVxuXG4vKiogUHJlc2VydmUgb3BlcmF0aW9uIGluZmVyZW5jZSB3aGlsZSBkZWNsYXJpbmcgYW4gZXhwb3J0ZWQgb3BlcmF0aW9uIG1hcC4gKi9cbmV4cG9ydCBmdW5jdGlvbiBkZWZpbmVPcGVyYXRpb25zPFxuICBjb25zdCBUU2NoZW1hcyBleHRlbmRzIFJlYWRvbmx5PFJlY29yZDxzdHJpbmcsIHouWm9kVHlwZT4+LFxuPihvcGVyYXRpb25zOiB7XG4gIHJlYWRvbmx5IFtUTmFtZSBpbiBrZXlvZiBUU2NoZW1hc106IEFwcEtpdE9wZXJhdGlvbjxUU2NoZW1hc1tUTmFtZV0sIHVua25vd24+O1xufSk6IHR5cGVvZiBvcGVyYXRpb25zIHtcbiAgcmV0dXJuIG9wZXJhdGlvbnM7XG59XG5cbi8qKiBDb252ZXJ0IGV4cG9ydGVkIFpvZCBvcGVyYXRpb24gY29udHJhY3RzIGludG8gYSBuYXRpdmUgQXBwS2l0IHRvb2wgcmVnaXN0cnkuICovXG5leHBvcnQgZnVuY3Rpb24gdG9vbFJlZ2lzdHJ5RnJvbU9wZXJhdGlvbnMoXG4gIG9wZXJhdGlvbnM6IFJlYWRvbmx5PFJlY29yZDxzdHJpbmcsIEFwcEtpdE9wZXJhdGlvbjx6LlpvZFR5cGUsIHVua25vd24+Pj4sXG4pOiBUb29sUmVnaXN0cnkge1xuICByZXR1cm4gT2JqZWN0LmZyb21FbnRyaWVzKFxuICAgIE9iamVjdC5lbnRyaWVzKG9wZXJhdGlvbnMpLmZsYXRNYXAoKFtuYW1lLCBvcGVyYXRpb25dKSA9PiB7XG4gICAgICBjb25zdCBtZXRhZGF0YSA9IG9wZXJhdGlvbi5zY2hlbWEubWV0YSgpIGFzIEFwcEtpdE9wZXJhdGlvblNjaGVtYU1ldGEgfCB1bmRlZmluZWQ7XG4gICAgICBjb25zdCB0b29sID0gbWV0YWRhdGE/LmFwcGtpdFRvb2wgPz8ge307XG4gICAgICBpZiAodG9vbC5hZ2VudFRvb2wgIT09IHRydWUpIHJldHVybiBbXTtcbiAgICAgIGNvbnN0IGVmZmVjdCA9IHRvb2wuZWZmZWN0ID8/IFwid3JpdGVcIjtcbiAgICAgIGNvbnN0IGFwcHJvdmFsID0gdG9vbC5hcHByb3ZhbCA9PT0gdHJ1ZSB8fCBlZmZlY3QgPT09IFwiZGVzdHJ1Y3RpdmVcIjtcbiAgICAgIGNvbnN0IGRlc2NyaXB0aW9uID0gb3BlcmF0aW9uLnNjaGVtYS5kZXNjcmlwdGlvbj8udHJpbSgpO1xuICAgICAgaWYgKCFkZXNjcmlwdGlvbikge1xuICAgICAgICB0aHJvdyBuZXcgVHlwZUVycm9yKGBBcHBLaXQgb3BlcmF0aW9uICR7bmFtZX0gcmVxdWlyZXMgYSBab2Qgc2NoZW1hIGRlc2NyaXB0aW9uYCk7XG4gICAgICB9XG4gICAgICByZXR1cm4gW1xuICAgICAgICBbXG4gICAgICAgICAgbmFtZSxcbiAgICAgICAgICBkZWZpbmVUb29sKHtcbiAgICAgICAgICAgIGRlc2NyaXB0aW9uLFxuICAgICAgICAgICAgc2NoZW1hOiBvcGVyYXRpb24uc2NoZW1hLFxuICAgICAgICAgICAgYW5ub3RhdGlvbnM6IHtcbiAgICAgICAgICAgICAgZWZmZWN0LFxuICAgICAgICAgICAgICAuLi4oYXBwcm92YWwgPyB7IGRlc3RydWN0aXZlOiB0cnVlIH0gOiB7fSksXG4gICAgICAgICAgICAgIC4uLih0b29sLnJlcXVpcmVzVXNlckNvbnRleHQgPT09IHVuZGVmaW5lZFxuICAgICAgICAgICAgICAgID8ge31cbiAgICAgICAgICAgICAgICA6IHsgcmVxdWlyZXNVc2VyQ29udGV4dDogdG9vbC5yZXF1aXJlc1VzZXJDb250ZXh0IH0pLFxuICAgICAgICAgICAgICAuLi4odG9vbC5pZGVtcG90ZW50ID09PSB1bmRlZmluZWQgPyB7fSA6IHsgaWRlbXBvdGVudDogdG9vbC5pZGVtcG90ZW50IH0pLFxuICAgICAgICAgICAgfSxcbiAgICAgICAgICAgIC4uLih0b29sLmF1dG9Jbmhlcml0YWJsZSA9PT0gdW5kZWZpbmVkXG4gICAgICAgICAgICAgID8ge31cbiAgICAgICAgICAgICAgOiB7IGF1dG9Jbmhlcml0YWJsZTogdG9vbC5hdXRvSW5oZXJpdGFibGUgfSksXG4gICAgICAgICAgICBleGVjdXRlOiAoaW5wdXQsIHNpZ25hbCkgPT4gb3BlcmF0aW9uLmV4ZWN1dGUob3BlcmF0aW9uLnNjaGVtYS5wYXJzZShpbnB1dCksIHNpZ25hbCksXG4gICAgICAgICAgfSksXG4gICAgICAgIF0sXG4gICAgICBdO1xuICAgIH0pLFxuICApO1xufVxuIl19
@@ -1 +1 @@
1
- {"root":["../src/_auto-configure.ts","../src/appkit.ts","../src/brand-context.ts","../src/bundle.ts","../src/cache-storage.ts","../src/child-process.ts","../src/databricks.ts","../src/files-cache.ts","../src/identity.ts","../src/interceptor.ts","../src/lakebase-resolver.ts","../src/migration.ts","../src/pgaddress.ts","../src/plugin-execution.ts","../src/plugin-registry.ts","../src/provision.ts","../src/request-tags.ts","../src/tool-provider.ts","../src/toolkit-entries.ts","../index.ts"],"version":"5.9.3"}
1
+ {"root":["../src/_auto-configure.ts","../src/appkit.ts","../src/brand-context.ts","../src/bundle.ts","../src/cache-storage.ts","../src/child-process.ts","../src/databricks.ts","../src/files-cache.ts","../src/identity.ts","../src/interceptor.ts","../src/lakebase-resolver.ts","../src/migration.ts","../src/pgaddress.ts","../src/plugin-execution.ts","../src/plugin-registry.ts","../src/provision.ts","../src/request-tags.ts","../src/tool-operations.ts","../src/tool-provider.ts","../src/toolkit-entries.ts","../index.ts"],"version":"5.9.3"}
package/package.json CHANGED
@@ -20,7 +20,7 @@
20
20
  },
21
21
  "devDependencies": {
22
22
  "@databricks/appkit": "0.81.0",
23
- "@dbx-tools/projen": "^0.9.119",
23
+ "@dbx-tools/projen": "^0.9.121",
24
24
  "@types/node": "^24.6.0",
25
25
  "typescript": "^5.9.3",
26
26
  "vitest": "3.2.4"
@@ -30,13 +30,13 @@
30
30
  },
31
31
  "dependencies": {
32
32
  "@databricks/sdk-experimental": "^0.17.0",
33
- "@dbx-tools/auth": "^0.9.119",
34
- "@dbx-tools/cache": "^0.9.119",
35
- "@dbx-tools/core": "^0.9.119",
36
- "@dbx-tools/lakebase": "^0.9.119",
37
- "@dbx-tools/postgres": "^0.9.119",
38
- "@dbx-tools/shared-core": "^0.9.119",
39
- "@dbx-tools/shared-fs": "^0.9.119",
33
+ "@dbx-tools/auth": "^0.9.121",
34
+ "@dbx-tools/cache": "^0.9.121",
35
+ "@dbx-tools/core": "^0.9.121",
36
+ "@dbx-tools/lakebase": "^0.9.121",
37
+ "@dbx-tools/postgres": "^0.9.121",
38
+ "@dbx-tools/shared-core": "^0.9.121",
39
+ "@dbx-tools/shared-fs": "^0.9.121",
40
40
  "zod": "4.3.6"
41
41
  },
42
42
  "main": "./lib/index.js",
@@ -114,6 +114,10 @@
114
114
  "types": "./lib/src/request-tags.d.ts",
115
115
  "default": "./lib/src/request-tags.js"
116
116
  },
117
+ "./tool-operations": {
118
+ "types": "./lib/src/tool-operations.d.ts",
119
+ "default": "./lib/src/tool-operations.js"
120
+ },
117
121
  "./tool-provider": {
118
122
  "types": "./lib/src/tool-provider.d.ts",
119
123
  "default": "./lib/src/tool-provider.js"
@@ -129,7 +133,7 @@
129
133
  "./package.json": "./package.json"
130
134
  }
131
135
  },
132
- "version": "0.9.119",
136
+ "version": "0.9.121",
133
137
  "types": "./lib/index.d.ts",
134
138
  "dbxToolsConfig": {
135
139
  "tags": [
@@ -206,6 +210,10 @@
206
210
  "types": "./lib/src/request-tags.d.ts",
207
211
  "default": "./lib/src/request-tags.js"
208
212
  },
213
+ "./tool-operations": {
214
+ "types": "./lib/src/tool-operations.d.ts",
215
+ "default": "./lib/src/tool-operations.js"
216
+ },
209
217
  "./tool-provider": {
210
218
  "types": "./lib/src/tool-provider.d.ts",
211
219
  "default": "./lib/src/tool-provider.js"
package/src/appkit.ts CHANGED
@@ -48,6 +48,7 @@ import {
48
48
  lifecycleBridge,
49
49
  type ResolvedAppEnv,
50
50
  } from "./interceptor.ts";
51
+ import { installAsUserDevelopmentPatch } from "./identity.ts";
51
52
  import { applyLakebaseEnv, type LakebaseConnection } from "./lakebase-resolver.ts";
52
53
  import { provisionCacheSchema } from "./provision.ts";
53
54
 
@@ -298,6 +299,7 @@ function interceptorList(interceptor: CreateAppConfig["interceptor"]): Intercept
298
299
  export async function createApp<T extends AppKitPlugins>(
299
300
  config?: CreateAppConfig<T>,
300
301
  ): Promise<PluginMap<T>> {
302
+ installAsUserDevelopmentPatch();
301
303
  const plugins = pluginNames(config);
302
304
  const interceptors = interceptorList(config?.interceptor);
303
305
  logger.debug("createApp: start", {
@@ -217,7 +217,7 @@ export function loadPersistentStorage(): PersistentStorageConstructor | undefine
217
217
  return loaded;
218
218
  } catch (err) {
219
219
  logger.debug("soft persistent cache skipped (PersistentStorage unavailable)", {
220
- error: errorUtils.errorMessage(err),
220
+ error: errorUtils.toError(err),
221
221
  });
222
222
  persistentStorageCtor = undefined;
223
223
  return undefined;
@@ -330,7 +330,7 @@ async function createPersistentStorage(
330
330
  return storage;
331
331
  } catch (err) {
332
332
  logger.debug("soft persistent cache unavailable", {
333
- error: errorUtils.errorMessage(err),
333
+ error: errorUtils.toError(err),
334
334
  });
335
335
  if (pool) {
336
336
  await pool.end().catch(() => {});
@@ -247,7 +247,7 @@ export class FilesCacheManager {
247
247
  state.group.timer = undefined;
248
248
  void this.flushInvalidations(state.group).catch((error) => {
249
249
  logger.warn("filesystem cache invalidation batch failed", {
250
- error: errorUtils.errorMessage(error),
250
+ error: errorUtils.toError(error),
251
251
  });
252
252
  });
253
253
  }, this.config.invalidationBatchMs);
package/src/identity.ts CHANGED
@@ -52,9 +52,9 @@
52
52
  * @module
53
53
  */
54
54
 
55
- import { ConfigurationError } from "@databricks/appkit";
55
+ import { ConfigurationError, Plugin } from "@databricks/appkit";
56
56
  import { configUtils } from "@dbx-tools/core";
57
- import { stringUtils, token } from "@dbx-tools/shared-core";
57
+ import { log, stringUtils, token } from "@dbx-tools/shared-core";
58
58
 
59
59
  /**
60
60
  * Header the Databricks Apps front door forwards the caller's OBO token on.
@@ -158,3 +158,68 @@ export function useServicePrincipal(mode: IdentityMode, req?: HeaderBearing): bo
158
158
  if (mode === "user") return false;
159
159
  return requestAccessToken(req) === undefined;
160
160
  }
161
+
162
+ const logger = log.logger("appkit");
163
+ const skippedAsUser = new Set<string>();
164
+ const AS_USER_SKIP_WARN = "Skipping user impersonation";
165
+ const AS_USER_PATCHED = Symbol.for("dbx-tools.appkit.asUserDevLog");
166
+
167
+ /**
168
+ * In development, keep AppKit `asUser` behavior. The first missing-token skip
169
+ * for a forwarded email or user id uses AppKit's own warn; later skips for
170
+ * that user, and every skip with no forwarded identity, are silent. Request
171
+ * objects are not a stable key: each plugin call typically has its own `req`.
172
+ *
173
+ * No-op outside development, when `Plugin.asUser` is missing or not a
174
+ * function, or if the prototype cannot be rewritten. Failures never throw:
175
+ * AppKit keeps its original `asUser`. Extra arguments are forwarded so a
176
+ * later signature change still reaches the original method.
177
+ */
178
+ export function installAsUserDevelopmentPatch(): void {
179
+ if (process.env.NODE_ENV !== "development") return;
180
+ try {
181
+ const proto = Plugin.prototype as unknown as Record<PropertyKey, unknown>;
182
+ if (proto[AS_USER_PATCHED] === true) return;
183
+ const original = proto.asUser;
184
+ if (typeof original !== "function") return;
185
+ proto[AS_USER_PATCHED] = true;
186
+ proto.asUser = function patchedAsUser(this: unknown, req: unknown, ...rest: unknown[]) {
187
+ const headers = asHeaderBearing(req);
188
+ const args = [req, ...rest];
189
+ if (!headers || requestAccessToken(headers) !== undefined) {
190
+ return Function.prototype.apply.call(original, this, args);
191
+ }
192
+ return withAsUserSkipWarnSuppressed(headers, () =>
193
+ Function.prototype.apply.call(original, this, args),
194
+ );
195
+ };
196
+ } catch (error) {
197
+ logger.debug("asUser development patch skipped", { error: String(error) });
198
+ }
199
+ }
200
+
201
+ function asHeaderBearing(req: unknown): HeaderBearing | undefined {
202
+ if (req === null || typeof req !== "object") return undefined;
203
+ if (typeof (req as HeaderBearing).header !== "function") return undefined;
204
+ return req as HeaderBearing;
205
+ }
206
+
207
+ function withAsUserSkipWarnSuppressed<T>(req: HeaderBearing, fn: () => T): T {
208
+ const user = requestUserEmail(req) ?? requestUserId(req);
209
+ if (user && !skippedAsUser.has(user)) {
210
+ skippedAsUser.add(user);
211
+ return fn();
212
+ }
213
+ const warn = console.warn;
214
+ console.warn = (...args: unknown[]) => {
215
+ if (!log.isLevelEnabled("debug") && args.some((value) => typeof value === "string" && value.includes(AS_USER_SKIP_WARN))) {
216
+ return;
217
+ }
218
+ warn.apply(console, args as Parameters<typeof console.warn>);
219
+ };
220
+ try {
221
+ return fn();
222
+ } finally {
223
+ console.warn = warn;
224
+ }
225
+ }
@@ -20,7 +20,7 @@
20
20
  */
21
21
 
22
22
  import { Plugin, toPlugin, type BasePluginConfig, type PluginManifest } from "@databricks/appkit";
23
- import { log } from "@dbx-tools/shared-core";
23
+ import { errorUtils, log } from "@dbx-tools/shared-core";
24
24
  import type { LakebaseConnection } from "./lakebase-resolver.ts";
25
25
 
26
26
  const logger = log.logger("interceptor");
@@ -120,7 +120,10 @@ export function createInterceptorContext(env: ResolvedAppEnv): InterceptorRuntim
120
120
  } catch (error) {
121
121
  // Match AppKit: a failing lifecycle handler is logged, never fatal, and
122
122
  // never blocks its siblings.
123
- logger.warn("lifecycle handler failed", { event, error });
123
+ logger.warn("lifecycle handler failed", {
124
+ event,
125
+ error: errorUtils.toError(error),
126
+ });
124
127
  }
125
128
  }
126
129
  };
@@ -40,11 +40,13 @@ import {
40
40
  ExecutionError,
41
41
  getUsernameWithApiLookup,
42
42
  getWorkspaceClient,
43
+ type LakebasePool,
43
44
  ValidationError,
44
45
  } from "@databricks/appkit";
45
46
  import { configUtils, projectUtils } from "@dbx-tools/core";
46
47
  import {
47
48
  asyncUtils,
49
+ errorUtils,
48
50
  log,
49
51
  object,
50
52
  options as sharedOptions,
@@ -113,6 +115,23 @@ export interface LakebaseConnection extends LakebaseConnectionInputs {
113
115
  sslMode: SslMode;
114
116
  }
115
117
 
118
+ /**
119
+ * Capture AppKit's existing service-principal Lakebase pool.
120
+ *
121
+ * Call this during `setup:complete`, outside any `asUser()` scope. AppKit's
122
+ * exported pool routes dynamically by request context, but its runtime
123
+ * `activePool()` method returns the already-created underlying pool selected in
124
+ * the current context. Capturing it once at startup pins application-owned
125
+ * storage to the service principal without creating another pool or connection.
126
+ *
127
+ * A future AppKit version may export a fixed pool directly; in that case the
128
+ * pool has no `activePool()` method and is returned unchanged.
129
+ */
130
+ export function captureServicePrincipalPool(pool: LakebasePool): LakebasePool {
131
+ const routingPool = pool as LakebasePool & { activePool?: () => LakebasePool };
132
+ return routingPool.activePool?.call(pool) ?? pool;
133
+ }
134
+
116
135
  /**
117
136
  * Every field of the Lakebase REST payloads is modelled as optional and
118
137
  * validated at the boundary rather than asserted with a cast: the pickers below
@@ -778,7 +797,10 @@ function isAlreadyExistsError(error: unknown): boolean {
778
797
  * echo request context, so it goes to the log instead.
779
798
  */
780
799
  function operationFailed(opName: string, opError: unknown): ExecutionError {
781
- logger.error("autopg: operation failed", { operation: opName, error: opError });
800
+ logger.error("autopg: operation failed", {
801
+ operation: opName,
802
+ error: errorUtils.toError(opError),
803
+ });
782
804
  const code = object.isRecord(opError) ? (opError.code ?? opError.reason) : undefined;
783
805
  return new ExecutionError(`Lakebase operation failed: ${opName}`, {
784
806
  context: {
package/src/migration.ts CHANGED
@@ -31,10 +31,8 @@ export function handleOwnershipMigrationError(
31
31
  const key = `${options.scope}\0${message}`;
32
32
  if (loggedOwnershipErrors.has(key)) return true;
33
33
  loggedOwnershipErrors.add(key);
34
- if (log.isLevelEnabled("debug")) {
35
- options.logger.error(options.event, cause);
36
- } else {
37
- options.logger.warn(options.event, { error: message, ...options.context });
38
- }
34
+ const error = errorUtils.toError(cause);
35
+ if (log.isLevelEnabled("debug")) options.logger.error(options.event, { error });
36
+ else options.logger.warn(options.event, { error, ...options.context });
39
37
  return true;
40
38
  }
@@ -1,7 +1,7 @@
1
1
  /** AppKit-specific adapters over the shared intercepted-execution contract. */
2
2
 
3
3
  import { ExecutionError } from "@databricks/appkit";
4
- import { execution, type Logger } from "@dbx-tools/shared-core";
4
+ import { errorUtils, execution, type Logger } from "@dbx-tools/shared-core";
5
5
 
6
6
  /** Inputs for one plugin operation routed through AppKit interceptors. */
7
7
  export interface RunPluginExecutionOptions<T, Settings> {
@@ -32,7 +32,7 @@ export function runPluginExecution<T, Settings>(
32
32
  options.logger.warn("execution-failed", {
33
33
  operation: failure.operation,
34
34
  status: failure.status,
35
- error: failure.message,
35
+ error: errorUtils.toError(failure.message),
36
36
  });
37
37
  return new ExecutionError(`${options.plugin}: ${failure.operation} failed`, {
38
38
  context: { operation: failure.operation, status: failure.status },
package/src/provision.ts CHANGED
@@ -128,7 +128,7 @@ export async function provisionCacheSchema(
128
128
  } catch (err) {
129
129
  logger.warn("autopg: cache provisioning failed (continuing)", {
130
130
  schema: CACHE_SCHEMA,
131
- error: errorUtils.errorMessage(err),
131
+ error: errorUtils.toError(err),
132
132
  });
133
133
  } finally {
134
134
  await pool.end().catch(() => {});
@@ -0,0 +1,80 @@
1
+ /** Zod-owned operation contracts converted into native AppKit agent tools. */
2
+
3
+ import { defineTool, type ToolRegistry } from "@databricks/appkit/beta";
4
+ import { z } from "zod";
5
+
6
+ /** Metadata stored under `appkitTool` by a Zod schema's `.meta()` value. */
7
+ export interface AppKitToolMeta {
8
+ /** Opt in to exposing this operation as an agent tool. */
9
+ readonly agentTool?: boolean;
10
+ /** Tool side-effect classification. Defaults to `write`. */
11
+ readonly effect?: "read" | "write" | "destructive";
12
+ /** Require human approval before execution. */
13
+ readonly approval?: boolean;
14
+ /** Whether execution requires an authenticated user context. */
15
+ readonly requiresUserContext?: boolean;
16
+ /** Whether repeated calls with the same input are safe. */
17
+ readonly idempotent?: boolean;
18
+ /** Whether AppKit may inherit the tool automatically. */
19
+ readonly autoInheritable?: boolean;
20
+ }
21
+
22
+ /** Shape placed in a Zod schema's `.meta()` value. */
23
+ export interface AppKitOperationSchemaMeta {
24
+ readonly appkitTool?: AppKitToolMeta;
25
+ }
26
+
27
+ /** One typed operation backed by a single Zod input contract. */
28
+ export interface AppKitOperation<TSchema extends z.ZodType = z.ZodType, TResult = unknown> {
29
+ readonly schema: TSchema;
30
+ readonly execute: (input: z.output<TSchema>, signal?: AbortSignal) => TResult | Promise<TResult>;
31
+ }
32
+
33
+ /** Preserve operation inference while declaring an exported operation map. */
34
+ export function defineOperations<
35
+ const TSchemas extends Readonly<Record<string, z.ZodType>>,
36
+ >(operations: {
37
+ readonly [TName in keyof TSchemas]: AppKitOperation<TSchemas[TName], unknown>;
38
+ }): typeof operations {
39
+ return operations;
40
+ }
41
+
42
+ /** Convert exported Zod operation contracts into a native AppKit tool registry. */
43
+ export function toolRegistryFromOperations(
44
+ operations: Readonly<Record<string, AppKitOperation<z.ZodType, unknown>>>,
45
+ ): ToolRegistry {
46
+ return Object.fromEntries(
47
+ Object.entries(operations).flatMap(([name, operation]) => {
48
+ const metadata = operation.schema.meta() as AppKitOperationSchemaMeta | undefined;
49
+ const tool = metadata?.appkitTool ?? {};
50
+ if (tool.agentTool !== true) return [];
51
+ const effect = tool.effect ?? "write";
52
+ const approval = tool.approval === true || effect === "destructive";
53
+ const description = operation.schema.description?.trim();
54
+ if (!description) {
55
+ throw new TypeError(`AppKit operation ${name} requires a Zod schema description`);
56
+ }
57
+ return [
58
+ [
59
+ name,
60
+ defineTool({
61
+ description,
62
+ schema: operation.schema,
63
+ annotations: {
64
+ effect,
65
+ ...(approval ? { destructive: true } : {}),
66
+ ...(tool.requiresUserContext === undefined
67
+ ? {}
68
+ : { requiresUserContext: tool.requiresUserContext }),
69
+ ...(tool.idempotent === undefined ? {} : { idempotent: tool.idempotent }),
70
+ },
71
+ ...(tool.autoInheritable === undefined
72
+ ? {}
73
+ : { autoInheritable: tool.autoInheritable }),
74
+ execute: (input, signal) => operation.execute(operation.schema.parse(input), signal),
75
+ }),
76
+ ],
77
+ ];
78
+ }),
79
+ );
80
+ }