@dbx-tools/appkit-mastra 0.6.48 → 0.6.50
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/README.md +26 -4
- package/index.ts +1 -1
- package/lib/index.d.ts +1 -1
- package/lib/index.js +2 -2
- package/lib/src/config.d.ts +12 -2
- package/lib/src/config.js +6 -3
- package/lib/src/identity.d.ts +31 -25
- package/lib/src/identity.js +32 -40
- package/lib/src/remote-skills.d.ts +6 -3
- package/lib/src/remote-skills.js +54 -17
- package/package.json +13 -13
- package/src/config.ts +16 -4
- package/src/identity.ts +34 -45
- package/src/remote-skills.ts +57 -16
package/package.json
CHANGED
|
@@ -31,18 +31,18 @@
|
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"@databricks/appkit": "^0.43.0",
|
|
33
33
|
"@databricks/sdk-experimental": "^0.17.0",
|
|
34
|
-
"@dbx-tools/appkit": "0.6.
|
|
35
|
-
"@dbx-tools/core": "0.6.
|
|
36
|
-
"@dbx-tools/databricks": "0.6.
|
|
37
|
-
"@dbx-tools/fs": "0.6.
|
|
38
|
-
"@dbx-tools/genie": "0.6.
|
|
39
|
-
"@dbx-tools/model": "0.6.
|
|
40
|
-
"@dbx-tools/path": "0.6.
|
|
41
|
-
"@dbx-tools/shared-core": "0.6.
|
|
42
|
-
"@dbx-tools/shared-fs": "0.6.
|
|
43
|
-
"@dbx-tools/shared-genie": "0.6.
|
|
44
|
-
"@dbx-tools/shared-mastra": "0.6.
|
|
45
|
-
"@dbx-tools/shared-model": "0.6.
|
|
34
|
+
"@dbx-tools/appkit": "0.6.50",
|
|
35
|
+
"@dbx-tools/core": "0.6.50",
|
|
36
|
+
"@dbx-tools/databricks": "0.6.50",
|
|
37
|
+
"@dbx-tools/fs": "0.6.50",
|
|
38
|
+
"@dbx-tools/genie": "0.6.50",
|
|
39
|
+
"@dbx-tools/model": "0.6.50",
|
|
40
|
+
"@dbx-tools/path": "0.6.50",
|
|
41
|
+
"@dbx-tools/shared-core": "0.6.50",
|
|
42
|
+
"@dbx-tools/shared-fs": "0.6.50",
|
|
43
|
+
"@dbx-tools/shared-genie": "0.6.50",
|
|
44
|
+
"@dbx-tools/shared-mastra": "0.6.50",
|
|
45
|
+
"@dbx-tools/shared-model": "0.6.50",
|
|
46
46
|
"@mastra/ai-sdk": "^1.6.0",
|
|
47
47
|
"@mastra/core": "^1.47.0",
|
|
48
48
|
"@mastra/express": "^1.4.2",
|
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
"./package.json": "./package.json"
|
|
72
72
|
}
|
|
73
73
|
},
|
|
74
|
-
"version": "0.6.
|
|
74
|
+
"version": "0.6.50",
|
|
75
75
|
"types": "./lib/index.d.ts",
|
|
76
76
|
"type": "module",
|
|
77
77
|
"exports": {
|
package/src/config.ts
CHANGED
|
@@ -24,7 +24,7 @@ import type { PgVectorConfig, PostgresStoreConfig } from "@mastra/pg";
|
|
|
24
24
|
|
|
25
25
|
import type { MastraAgentDefinition, MastraTools } from "./agents.ts";
|
|
26
26
|
import type { GenieSpacesConfig } from "./genie.ts";
|
|
27
|
-
import type
|
|
27
|
+
import { IDENTITY_MODES, type MastraIdentityMode } from "./identity.ts";
|
|
28
28
|
import type { RemoteSkillsOption } from "./remote-skills.ts";
|
|
29
29
|
|
|
30
30
|
/**
|
|
@@ -526,8 +526,13 @@ export interface MastraPluginConfig extends BasePluginConfig {
|
|
|
526
526
|
* - `"service-principal"`: always the app service principal. Needs no OBO
|
|
527
527
|
* scopes and works for any caller who can open the app, at the cost of
|
|
528
528
|
* per-user attribution in Genie / Unity Catalog.
|
|
529
|
+
* - `"auto"`: OBO when the request carries an OBO token, the service principal
|
|
530
|
+
* when it does not. For an app serving BOTH the platform front door and a
|
|
531
|
+
* door that has no token to forward - a `@dbx-tools/cli-tunnel` gate, a
|
|
532
|
+
* Teams channel - where AppKit's `asUser` would otherwise throw
|
|
533
|
+
* `AuthenticationError` outside `NODE_ENV=development`.
|
|
529
534
|
*
|
|
530
|
-
*
|
|
535
|
+
* The service-principal path changes only the Databricks CREDENTIAL. Memory
|
|
531
536
|
* threads, the per-user cache namespace, and trace metadata still key off the
|
|
532
537
|
* forwarded user, so callers sharing the service principal's data access keep
|
|
533
538
|
* separate conversations and cannot read each other's threads or charts.
|
|
@@ -536,6 +541,11 @@ export interface MastraPluginConfig extends BasePluginConfig {
|
|
|
536
541
|
* ```ts
|
|
537
542
|
* mastra({ genieIdentity: "service-principal", genieSpaces: { default: spaceId } });
|
|
538
543
|
* ```
|
|
544
|
+
*
|
|
545
|
+
* @example One app behind both the front door and a public tunnel
|
|
546
|
+
* ```ts
|
|
547
|
+
* mastra({ genieIdentity: "auto", genieSpaces: { default: spaceId } });
|
|
548
|
+
* ```
|
|
539
549
|
*/
|
|
540
550
|
genieIdentity?: MastraIdentityMode;
|
|
541
551
|
/**
|
|
@@ -691,9 +701,11 @@ export const MASTRA_CONFIG_SCHEMA: ConfigSchema = {
|
|
|
691
701
|
},
|
|
692
702
|
genieIdentity: {
|
|
693
703
|
type: "string",
|
|
694
|
-
|
|
704
|
+
// Derived from the resolver's own list, so a mode can never be accepted at
|
|
705
|
+
// runtime while the published schema (and the UI built from it) omits it.
|
|
706
|
+
enum: [...IDENTITY_MODES],
|
|
695
707
|
description:
|
|
696
|
-
'Which Databricks identity the agents\' workspace calls (serving catalogue, Genie, statement execution) run as. "user" (default) is always OBO, so callers must be workspace members; "service-principal" always uses the app service principal, so any caller who can open the app works, at the cost of per-user attribution. Falls back to MASTRA_GENIE_IDENTITY.',
|
|
708
|
+
'Which Databricks identity the agents\' workspace calls (serving catalogue, Genie, statement execution) run as. "user" (default) is always OBO, so callers must be workspace members; "service-principal" always uses the app service principal, so any caller who can open the app works, at the cost of per-user attribution; "auto" uses OBO when the request carries an OBO token and the service principal when it does not, for an app that also serves a tunnel or webhook door. Falls back to MASTRA_GENIE_IDENTITY.',
|
|
697
709
|
},
|
|
698
710
|
apiAccess: {
|
|
699
711
|
type: "string",
|
package/src/identity.ts
CHANGED
|
@@ -7,7 +7,14 @@
|
|
|
7
7
|
* client on the ambient AppKit execution context. That context is user-scoped
|
|
8
8
|
* (OBO) whenever the plugin enters `asUser(req)` and the app's service principal
|
|
9
9
|
* otherwise, so ONE decision - do we enter that scope for this request - moves
|
|
10
|
-
* every call site together.
|
|
10
|
+
* every call site together.
|
|
11
|
+
*
|
|
12
|
+
* The decision itself is NOT Mastra-specific (the Teams messaging endpoint and
|
|
13
|
+
* the tunnel gate face it too), so it lives in `@dbx-tools/appkit`'s `identity`
|
|
14
|
+
* module - which also documents the measured AppKit behavior that makes `"auto"`
|
|
15
|
+
* necessary. This module is the Mastra-facing binding: the plugin's own config
|
|
16
|
+
* field and env var, re-exporting the shared vocabulary so a consumer importing
|
|
17
|
+
* `@dbx-tools/appkit-mastra` needs no second import.
|
|
11
18
|
*
|
|
12
19
|
* Why the option exists: OBO requires the caller to be a member of the
|
|
13
20
|
* WORKSPACE, not merely of the Databricks account. An app shared with an
|
|
@@ -19,7 +26,7 @@
|
|
|
19
26
|
* / warehouse / serving / Unity Catalog grants the app was deployed with, and
|
|
20
27
|
* works for every caller.
|
|
21
28
|
*
|
|
22
|
-
* The
|
|
29
|
+
* The modes, from `config.genieIdentity` (env: {@link IDENTITY_ENV}):
|
|
23
30
|
*
|
|
24
31
|
* - `"user"` (default): always OBO. Calls are attributed per user and Genie /
|
|
25
32
|
* Unity Catalog row filters apply per user. Correct whenever every caller is
|
|
@@ -28,9 +35,13 @@
|
|
|
28
35
|
* - `"service-principal"`: always the app service principal. Needs no OBO
|
|
29
36
|
* scopes and works for any caller who can open the app, at the cost of
|
|
30
37
|
* per-user attribution in Genie / Unity Catalog.
|
|
38
|
+
* - `"auto"`: OBO when the request carries an OBO token, the service principal
|
|
39
|
+
* when it does not. The mode for an app that serves BOTH the platform front
|
|
40
|
+
* door and a door with no OBO token to forward (a `@dbx-tools/cli-tunnel`
|
|
41
|
+
* gate, a Teams channel), since a single container serves both at once.
|
|
31
42
|
*
|
|
32
|
-
* What
|
|
33
|
-
* memory thread's `resourceId`, the per-user cache namespace, and the user
|
|
43
|
+
* What the service-principal path does NOT change is WHO the turn belongs to.
|
|
44
|
+
* The memory thread's `resourceId`, the per-user cache namespace, and the user
|
|
34
45
|
* metadata on traces still come from the forwarded request headers, so two
|
|
35
46
|
* account users sharing the service principal's data access still get separate
|
|
36
47
|
* conversations and cannot read each other's threads or cached charts. Only the
|
|
@@ -39,72 +50,50 @@
|
|
|
39
50
|
* @module
|
|
40
51
|
*/
|
|
41
52
|
|
|
42
|
-
import {
|
|
43
|
-
import { string } from "@dbx-tools/shared-core";
|
|
44
|
-
import type express from "express";
|
|
53
|
+
import { identity } from "@dbx-tools/appkit";
|
|
45
54
|
|
|
46
55
|
/** Identity a chat turn's Databricks calls run as. See the module docs. */
|
|
47
|
-
export type MastraIdentityMode =
|
|
56
|
+
export type MastraIdentityMode = identity.IdentityMode;
|
|
48
57
|
|
|
49
58
|
/** Environment fallback for `config.genieIdentity`. */
|
|
50
59
|
export const IDENTITY_ENV = "MASTRA_GENIE_IDENTITY";
|
|
51
60
|
|
|
52
61
|
/** Every accepted {@link MastraIdentityMode}. */
|
|
53
|
-
export const IDENTITY_MODES
|
|
62
|
+
export const IDENTITY_MODES = identity.IDENTITY_MODES;
|
|
54
63
|
|
|
55
64
|
/**
|
|
56
65
|
* Default mode. `"user"` keeps OBO the only identity unless an app opts in, so
|
|
57
66
|
* adopting this option can never silently widen an existing app's data access.
|
|
58
67
|
*/
|
|
59
|
-
export const DEFAULT_IDENTITY_MODE
|
|
68
|
+
export const DEFAULT_IDENTITY_MODE = identity.DEFAULT_IDENTITY_MODE;
|
|
69
|
+
|
|
70
|
+
/** Header Databricks Apps forward the signed-in user's id on. */
|
|
71
|
+
export const USER_ID_HEADER = identity.USER_ID_HEADER;
|
|
72
|
+
|
|
73
|
+
/** Header Databricks Apps forward the signed-in user's email on. */
|
|
74
|
+
export const USER_EMAIL_HEADER = identity.USER_EMAIL_HEADER;
|
|
60
75
|
|
|
61
76
|
/**
|
|
62
77
|
* Resolve the configured mode: explicit plugin config, then {@link IDENTITY_ENV},
|
|
63
|
-
* then {@link DEFAULT_IDENTITY_MODE}.
|
|
64
|
-
*
|
|
65
|
-
* An unrecognized value throws rather than falling back, since falling back
|
|
66
|
-
* would silently keep serving OBO - and the 500s it produces - to exactly the
|
|
67
|
-
* callers the option was set to accommodate.
|
|
78
|
+
* then {@link DEFAULT_IDENTITY_MODE}. An unrecognized value throws.
|
|
68
79
|
*/
|
|
69
80
|
export function resolveIdentityMode(configured: string | undefined): MastraIdentityMode {
|
|
70
|
-
|
|
71
|
-
if (raw === null) return DEFAULT_IDENTITY_MODE;
|
|
72
|
-
const mode = raw.toLowerCase() as MastraIdentityMode;
|
|
73
|
-
if (!IDENTITY_MODES.includes(mode)) {
|
|
74
|
-
throw new ConfigurationError(
|
|
75
|
-
`genieIdentity must be one of ${IDENTITY_MODES.join(" | ")} (env: ${IDENTITY_ENV})`,
|
|
76
|
-
{ context: { field: "genieIdentity", envVar: IDENTITY_ENV, received: raw } },
|
|
77
|
-
);
|
|
78
|
-
}
|
|
79
|
-
return mode;
|
|
81
|
+
return identity.resolveIdentityMode(configured, IDENTITY_ENV, "genieIdentity");
|
|
80
82
|
}
|
|
81
83
|
|
|
82
|
-
/** Header Databricks Apps forward the signed-in user's id on. */
|
|
83
|
-
export const USER_ID_HEADER = "x-forwarded-user";
|
|
84
|
-
|
|
85
|
-
/** Header Databricks Apps forward the signed-in user's email on. */
|
|
86
|
-
export const USER_EMAIL_HEADER = "x-forwarded-email";
|
|
87
|
-
|
|
88
84
|
/**
|
|
89
85
|
* The forwarded user id on `req`, or `undefined`. Used to attribute a turn to
|
|
90
|
-
* the real caller
|
|
91
|
-
*
|
|
86
|
+
* the real caller when the Databricks client is the app SP but memory / cache /
|
|
87
|
+
* traces must still key off the user.
|
|
92
88
|
*/
|
|
93
|
-
export
|
|
94
|
-
return string.trimToNull(req.header(USER_ID_HEADER)) ?? undefined;
|
|
95
|
-
}
|
|
89
|
+
export const requestUserId = identity.requestUserId;
|
|
96
90
|
|
|
97
91
|
/** The forwarded user email on `req`, or `undefined`. */
|
|
98
|
-
export
|
|
99
|
-
return string.trimToNull(req.header(USER_EMAIL_HEADER)) ?? undefined;
|
|
100
|
-
}
|
|
92
|
+
export const requestUserEmail = identity.requestUserEmail;
|
|
101
93
|
|
|
102
94
|
/**
|
|
103
95
|
* Whether this request should run its Databricks calls as the app service
|
|
104
|
-
* principal rather than OBO
|
|
105
|
-
*
|
|
106
|
-
* request-scoped policy later; it is unused today.
|
|
96
|
+
* principal rather than OBO - `"service-principal"` always, `"user"` never, and
|
|
97
|
+
* `"auto"` only when the request carries no OBO token for AppKit to use.
|
|
107
98
|
*/
|
|
108
|
-
export
|
|
109
|
-
return mode === "service-principal";
|
|
110
|
-
}
|
|
99
|
+
export const useServicePrincipal = identity.useServicePrincipal;
|
package/src/remote-skills.ts
CHANGED
|
@@ -40,9 +40,12 @@
|
|
|
40
40
|
* action writes to), so provisioned skills persist across restarts and are
|
|
41
41
|
* discovered by the built-in Assistant-skills mount. Pass `userEmail` (or an
|
|
42
42
|
* explicit `databricksBasePath`) to target `/Users/<email>/.assistant/skills`
|
|
43
|
-
* instead. When no Databricks client is resolvable at startup
|
|
44
|
-
*
|
|
45
|
-
*
|
|
43
|
+
* instead. When no Databricks client is resolvable at startup - or the resolved
|
|
44
|
+
* identity cannot WRITE the destination, which is the normal case for a
|
|
45
|
+
* Databricks App service principal against the admin-owned shared tree (see
|
|
46
|
+
* {@link openWritableWorkspace}) - the tree is written under
|
|
47
|
+
* {@link localFS.tmpFS} and returned as an extra local skill path for the
|
|
48
|
+
* current process. Storage location degrades; the skills themselves do not.
|
|
46
49
|
*
|
|
47
50
|
* @module
|
|
48
51
|
*/
|
|
@@ -413,25 +416,20 @@ export async function provisionRemoteSkills(
|
|
|
413
416
|
|
|
414
417
|
const failDefault = options.failOnError !== false;
|
|
415
418
|
const client = options.client ?? appkit.tryGetExecutionContext()?.client;
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
})
|
|
419
|
+
// Probed rather than assumed: an identity that cannot write the workspace
|
|
420
|
+
// tree falls back to the local one instead of failing startup. The probe also
|
|
421
|
+
// creates the root, which the first cache READ needs - `copySkillDirs` would
|
|
422
|
+
// otherwise be the first thing to create it, which is too late.
|
|
423
|
+
const requestedBasePath = resolveDatabricksBasePath(options, client);
|
|
424
|
+
const destination = requestedBasePath
|
|
425
|
+
? await openWritableWorkspace(requestedBasePath, client as WorkspaceClient)
|
|
424
426
|
: undefined;
|
|
427
|
+
const databricksBasePath = destination ? requestedBasePath : undefined;
|
|
425
428
|
|
|
426
429
|
const localSkillPaths: string[] = [];
|
|
427
430
|
const skillNames: string[] = [];
|
|
428
431
|
let staging: LocalFileSystem | undefined;
|
|
429
432
|
|
|
430
|
-
// The destination roots the shared metadata document, so it has to exist
|
|
431
|
-
// before the first cache READ - `copySkillDirs` would otherwise be the first
|
|
432
|
-
// thing to create it, which is too late.
|
|
433
|
-
if (destination) await destination.init();
|
|
434
|
-
|
|
435
433
|
try {
|
|
436
434
|
const sources = Array.isArray(options.sources) ? options.sources : [options.sources];
|
|
437
435
|
for (const entry of sources) {
|
|
@@ -508,6 +506,49 @@ export async function provisionRemoteSkills(
|
|
|
508
506
|
return { localSkillPaths, databricksBasePath, skillNames };
|
|
509
507
|
}
|
|
510
508
|
|
|
509
|
+
/** Probe file {@link openWritableWorkspace} writes and removes. */
|
|
510
|
+
const WRITE_PROBE_FILE = ".dbx-tools-write-probe";
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Open the Databricks skills destination, or `undefined` when this identity
|
|
514
|
+
* cannot write there.
|
|
515
|
+
*
|
|
516
|
+
* The default destination (`/Workspace/.assistant/skills`) is commonly
|
|
517
|
+
* admins-only, and an app service principal is not an admin - so a Databricks
|
|
518
|
+
* App writing there fails with `RESOURCE_DOES_NOT_EXIST`, which is how the
|
|
519
|
+
* workspace API reports a path the caller is not allowed to see. That is a fact
|
|
520
|
+
* about WHERE the tree is stored, not about whether the skills are usable, so it
|
|
521
|
+
* degrades to the local tree (handed to Mastra as an extra scan path) rather
|
|
522
|
+
* than taking app startup down over bookkeeping location.
|
|
523
|
+
*
|
|
524
|
+
* The probe is a real write: `mkdirs` succeeds on an existing directory even for
|
|
525
|
+
* an identity that cannot write into it, so init alone proves nothing. Removing
|
|
526
|
+
* the probe file is best-effort - a failed cleanup must not decide writability.
|
|
527
|
+
*/
|
|
528
|
+
async function openWritableWorkspace(
|
|
529
|
+
basePath: string,
|
|
530
|
+
client: WorkspaceClient,
|
|
531
|
+
): Promise<DatabricksFileSystem | undefined> {
|
|
532
|
+
const fs = new DatabricksFileSystem({
|
|
533
|
+
client,
|
|
534
|
+
root: basePath,
|
|
535
|
+
readOnly: false,
|
|
536
|
+
createRoot: true,
|
|
537
|
+
});
|
|
538
|
+
try {
|
|
539
|
+
await fs.init();
|
|
540
|
+
await fs.writeFile(WRITE_PROBE_FILE, new Date().toISOString(), { overwrite: true });
|
|
541
|
+
} catch (err) {
|
|
542
|
+
logger.warn("destination:unwritable", {
|
|
543
|
+
destination: basePath,
|
|
544
|
+
error: error.errorMessage(err),
|
|
545
|
+
});
|
|
546
|
+
return undefined;
|
|
547
|
+
}
|
|
548
|
+
await fs.deleteFile(WRITE_PROBE_FILE, { force: true }).catch(() => undefined);
|
|
549
|
+
return fs;
|
|
550
|
+
}
|
|
551
|
+
|
|
511
552
|
/** Resolve the Databricks Assistant skills destination, or `undefined` for local temp. */
|
|
512
553
|
function resolveDatabricksBasePath(
|
|
513
554
|
options: ProvisionRemoteSkillsOptions,
|