@dbx-tools/appkit-mastra 0.9.95 → 0.9.105
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 +238 -285
- package/index.ts +4 -5
- package/lib/index.d.ts +4 -5
- package/lib/index.js +4 -5
- package/lib/src/agents.d.ts +5 -3
- package/lib/src/agents.js +70 -19
- package/lib/src/chat.d.ts +3 -1
- package/lib/src/chat.js +98 -12
- package/lib/src/config.d.ts +7 -1
- package/lib/src/config.js +6 -2
- package/lib/src/model.d.ts +3 -6
- package/lib/src/model.js +11 -94
- package/lib/src/monty-sandbox.d.ts +3 -7
- package/lib/src/monty-sandbox.js +17 -28
- package/lib/src/openapi-tool.js +3 -12
- package/lib/src/plugin.js +3 -1
- package/lib/src/server.js +1 -1
- package/lib/src/skill-paths.d.ts +1 -1
- package/lib/src/skill-paths.js +2 -2
- package/lib/src/workspaces.d.ts +53 -92
- package/lib/src/workspaces.js +319 -192
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +16 -22
- package/src/agents.ts +69 -18
- package/src/chat.ts +103 -11
- package/src/config.ts +13 -2
- package/src/model.ts +10 -108
- package/src/monty-sandbox.ts +23 -33
- package/src/openapi-tool.ts +2 -11
- package/src/plugin.ts +2 -0
- package/src/server.ts +22 -20
- package/src/skill-paths.ts +1 -1
- package/src/workspaces.ts +485 -266
- package/lib/src/serving-sanitize.d.ts +0 -6
- package/lib/src/serving-sanitize.js +0 -7
- package/src/serving-sanitize.ts +0 -19
package/src/workspaces.ts
CHANGED
|
@@ -1,16 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Mastra workspace factory for Databricks Apps.
|
|
3
3
|
*
|
|
4
|
-
* Builds
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* readable / writable policy: a Databricks path mounted through the OBO client
|
|
9
|
-
* on {@link MASTRA_USER_KEY}, or any {@link WorkspaceFilesystem} a consuming
|
|
10
|
-
* library already owns. {@link DEFAULT_SKILL_FOLDERS} supplies the Assistant
|
|
11
|
-
* trees, and `skillFolders` merges over it - same name overrides, `false`
|
|
12
|
-
* disables, a new name adds. Optional mount resolvers contribute further
|
|
13
|
-
* filesystems and skill scan roots on top.
|
|
4
|
+
* Builds native Mastra workspace configuration from Databricks paths. Paths
|
|
5
|
+
* mount at the same path by default, `~` resolves to the current user's
|
|
6
|
+
* Databricks workspace home, and `/tmp[/subpath]` resolves to stable
|
|
7
|
+
* user-scoped storage under the host's ephemeral temp directory.
|
|
14
8
|
*
|
|
15
9
|
* Databricks mounts use `@dbx-tools/databricks` {@link DatabricksFileSystem}
|
|
16
10
|
* wrapped by {@link filesystems}; inaccessible roots are skipped independently.
|
|
@@ -19,9 +13,11 @@
|
|
|
19
13
|
*/
|
|
20
14
|
|
|
21
15
|
import { createHash } from "node:crypto";
|
|
22
|
-
import {
|
|
16
|
+
import { tmpdir } from "node:os";
|
|
17
|
+
import { join } from "node:path";
|
|
18
|
+
import { ConfigurationError, createWorkspaceClient } from "@databricks/appkit";
|
|
23
19
|
import type { WorkspaceClient } from "@databricks/appkit";
|
|
24
|
-
import { pluginRegistry } from "@dbx-tools/appkit";
|
|
20
|
+
import { appkit, pluginRegistry } from "@dbx-tools/appkit";
|
|
25
21
|
import {
|
|
26
22
|
filesCache as filesCachePlugin,
|
|
27
23
|
type FilesCacheExports,
|
|
@@ -29,18 +25,25 @@ import {
|
|
|
29
25
|
} from "@dbx-tools/appkit/files-cache";
|
|
30
26
|
import { DatabricksFileSystem, workspaceClient } from "@dbx-tools/databricks";
|
|
31
27
|
import { LocalFileSystem } from "@dbx-tools/fs";
|
|
28
|
+
import { match, type PathMatchInput } from "@dbx-tools/path";
|
|
32
29
|
import { errorUtils, log, object, stringUtils, token } from "@dbx-tools/shared-core";
|
|
33
|
-
import {
|
|
30
|
+
import {
|
|
31
|
+
fs as sharedFS,
|
|
32
|
+
posixPath,
|
|
33
|
+
type CacheableFileSystemOperation,
|
|
34
|
+
type FileSystem,
|
|
35
|
+
type FileSystemCache,
|
|
36
|
+
type FileSystemCacheOptions,
|
|
37
|
+
MemoryFileSystem,
|
|
38
|
+
} from "@dbx-tools/shared-fs";
|
|
34
39
|
import type { RequestContext } from "@mastra/core/request-context";
|
|
35
40
|
import {
|
|
36
|
-
WORKSPACE_TOOLS,
|
|
37
41
|
Workspace,
|
|
38
42
|
type SkillsResolver,
|
|
43
|
+
type WorkspaceConfig,
|
|
39
44
|
type WorkspaceFilesystem,
|
|
40
45
|
type WorkspaceSandbox,
|
|
41
46
|
type WorkspaceSandboxResolver,
|
|
42
|
-
type ToolConfigWithArgsContext,
|
|
43
|
-
type WorkspaceToolConfig,
|
|
44
47
|
type WorkspaceToolsConfig,
|
|
45
48
|
} from "@mastra/core/workspace";
|
|
46
49
|
|
|
@@ -54,7 +57,6 @@ import {
|
|
|
54
57
|
import {
|
|
55
58
|
filesystems,
|
|
56
59
|
MountedCompositeFilesystem,
|
|
57
|
-
scratchFilesystem,
|
|
58
60
|
type MastraFileSystemAdapterOptions,
|
|
59
61
|
} from "./filesystems.ts";
|
|
60
62
|
import { MontySandbox } from "./monty-sandbox.ts";
|
|
@@ -66,35 +68,30 @@ import { ORGANIZATION_ASSISTANT_PATH, personalWorkspacePath } from "./skill-path
|
|
|
66
68
|
/** OAuth scopes that gate Databricks workspace file mounts. */
|
|
67
69
|
const WORKSPACE_FILE_SCOPES = ["workspace", "workspace.workspace", "all-apis"] as const;
|
|
68
70
|
const SCRATCH_MOUNT = "/tmp";
|
|
71
|
+
const CACHEABLE_FILESYSTEM_OPERATIONS = [
|
|
72
|
+
"exists",
|
|
73
|
+
"readFile",
|
|
74
|
+
"readdir",
|
|
75
|
+
"stat",
|
|
76
|
+
] as const satisfies readonly CacheableFileSystemOperation[];
|
|
69
77
|
|
|
70
78
|
const logger = log.logger("mastra/workspaces");
|
|
79
|
+
let workspaceSourceSequence = 0;
|
|
80
|
+
const databricksWorkspaceOptions = new WeakMap<Workspace, DatabricksWorkspaceOptions>();
|
|
71
81
|
|
|
72
82
|
/* -------------------------------- types -------------------------------- */
|
|
73
83
|
|
|
74
84
|
/** Per-request context for mount and skill-folder resolvers. */
|
|
75
|
-
export interface
|
|
85
|
+
export interface DatabricksWorkspaceContext {
|
|
76
86
|
requestContext?: RequestContext;
|
|
77
87
|
}
|
|
78
88
|
|
|
79
89
|
/**
|
|
80
|
-
*
|
|
81
|
-
* A resolver returning `undefined` skips the folder for that request.
|
|
90
|
+
* One Databricks path mounted into a Mastra workspace.
|
|
82
91
|
*/
|
|
83
|
-
export
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* One named skill-folder location and its read / write policy.
|
|
88
|
-
*
|
|
89
|
-
* Give {@link path} for a Databricks workspace tree (mounted through the
|
|
90
|
-
* request's OBO client), or {@link filesystem} for a mount the consumer builds
|
|
91
|
-
* itself. {@link filesystem} wins when both are set.
|
|
92
|
-
*/
|
|
93
|
-
export interface SkillFolderOptions {
|
|
94
|
-
/** Absolute Databricks workspace root mounted at its actual path. */
|
|
95
|
-
path?: SkillFolderValue<string>;
|
|
96
|
-
/** Ready-made mount, for locations the OBO client cannot reach. */
|
|
97
|
-
filesystem?: SkillFolderValue<WorkspaceFilesystem>;
|
|
92
|
+
export interface DatabricksWorkspacePathOptions {
|
|
93
|
+
/** Absolute Databricks path or `~` shortcut for the current user's workspace home. */
|
|
94
|
+
path: string;
|
|
98
95
|
/** Human-friendly mount name shown in filesystem listings. */
|
|
99
96
|
displayName?: string;
|
|
100
97
|
/** Mount description shown in filesystem listings and workspace instructions. */
|
|
@@ -124,23 +121,62 @@ export interface SkillFolderOptions {
|
|
|
124
121
|
mount?: string;
|
|
125
122
|
}
|
|
126
123
|
|
|
127
|
-
/**
|
|
128
|
-
export
|
|
124
|
+
/** String shorthand or full options for one Databricks workspace path. */
|
|
125
|
+
export type DatabricksWorkspacePath = string | DatabricksWorkspacePathOptions;
|
|
126
|
+
|
|
127
|
+
/** Fixed or request-dependent Databricks path entry. */
|
|
128
|
+
export type DatabricksWorkspacePathValue =
|
|
129
|
+
| DatabricksWorkspacePath
|
|
130
|
+
| false
|
|
131
|
+
| ((
|
|
132
|
+
context: DatabricksWorkspaceContext,
|
|
133
|
+
) =>
|
|
134
|
+
| DatabricksWorkspacePath
|
|
135
|
+
| false
|
|
136
|
+
| undefined
|
|
137
|
+
| Promise<DatabricksWorkspacePath | false | undefined>);
|
|
138
|
+
|
|
139
|
+
/** Operations and path globs cached by the Databricks filesystem shortcut. */
|
|
140
|
+
export interface DatabricksWorkspaceCacheFilter {
|
|
141
|
+
/** Cached filesystem operations. Omit for `exists`, `readFile`, `readdir`, and `stat`. */
|
|
142
|
+
operations?: CacheableFileSystemOperation | readonly CacheableFileSystemOperation[];
|
|
143
|
+
/** Absolute or root-relative globs and predicates. `~` expands per request. */
|
|
144
|
+
paths?: PathMatchInput | readonly PathMatchInput[];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Fixed or request-dependent filesystem cache selection. */
|
|
148
|
+
export type DatabricksWorkspaceCache =
|
|
149
|
+
| boolean
|
|
150
|
+
| DatabricksWorkspaceCacheFilter
|
|
151
|
+
| readonly DatabricksWorkspaceCacheFilter[]
|
|
152
|
+
| ((
|
|
153
|
+
context: DatabricksWorkspaceContext,
|
|
154
|
+
) =>
|
|
155
|
+
| boolean
|
|
156
|
+
| DatabricksWorkspaceCacheFilter
|
|
157
|
+
| readonly DatabricksWorkspaceCacheFilter[]
|
|
158
|
+
| undefined
|
|
159
|
+
| Promise<
|
|
160
|
+
| boolean
|
|
161
|
+
| DatabricksWorkspaceCacheFilter
|
|
162
|
+
| readonly DatabricksWorkspaceCacheFilter[]
|
|
163
|
+
| undefined
|
|
164
|
+
>);
|
|
165
|
+
|
|
166
|
+
/** Mount map plus optional Mastra skill scan roots resolved for one request. */
|
|
167
|
+
interface WorkspaceMountContribution {
|
|
129
168
|
mounts: Record<string, WorkspaceFilesystem>;
|
|
130
169
|
/** Paths within the composite namespace where `SKILL.md` files are scanned. */
|
|
131
170
|
skillPaths?: string[];
|
|
132
171
|
}
|
|
133
172
|
|
|
134
|
-
/**
|
|
135
|
-
|
|
136
|
-
context:
|
|
173
|
+
/** Internal filesystem contribution resolver. */
|
|
174
|
+
type WorkspaceMountResolver = (
|
|
175
|
+
context: DatabricksWorkspaceContext,
|
|
137
176
|
) => WorkspaceMountContribution | Promise<WorkspaceMountContribution>;
|
|
138
177
|
|
|
139
|
-
/** Names carried by {@link DEFAULT_SKILL_FOLDERS}. */
|
|
140
|
-
export type DefaultSkillFolderName = "organization-skills" | "personal-skills";
|
|
141
|
-
|
|
142
178
|
/**
|
|
143
|
-
* Sandbox selection for {@link
|
|
179
|
+
* Sandbox selection for {@link databricksWorkspace}. Monty is the default;
|
|
144
180
|
* `false` disables command execution, and a Mastra provider or resolver is an
|
|
145
181
|
* explicit replacement.
|
|
146
182
|
*/
|
|
@@ -152,35 +188,28 @@ export type WorkspaceSandboxSelection =
|
|
|
152
188
|
| WorkspaceSandbox
|
|
153
189
|
| WorkspaceSandboxResolver;
|
|
154
190
|
|
|
155
|
-
/**
|
|
156
|
-
export interface
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
name?: string;
|
|
191
|
+
/** Native Mastra workspace options plus Databricks path shortcuts. */
|
|
192
|
+
export interface DatabricksWorkspaceOptions extends Omit<
|
|
193
|
+
WorkspaceConfig,
|
|
194
|
+
"filesystem" | "mounts" | "sandbox" | "skills"
|
|
195
|
+
> {
|
|
161
196
|
/**
|
|
162
|
-
* Start from {@link
|
|
163
|
-
* starts from an empty map, leaving only the {@link skillFolders} given here.
|
|
197
|
+
* Start from {@link DEFAULT_DATABRICKS_WORKSPACE_PATHS}. Defaults to `true`.
|
|
164
198
|
*/
|
|
165
|
-
|
|
199
|
+
assistantPaths?: boolean;
|
|
166
200
|
/**
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
* a folder.
|
|
201
|
+
* Databricks paths appended after the defaults. A string is shorthand for
|
|
202
|
+
* `{ path }`; a resolver may return either form per request.
|
|
170
203
|
*/
|
|
171
|
-
|
|
172
|
-
/**
|
|
173
|
-
mounts?:
|
|
204
|
+
paths?: readonly DatabricksWorkspacePathValue[];
|
|
205
|
+
/** Native Mastra filesystems composed beside the Databricks paths. */
|
|
206
|
+
mounts?: Record<string, WorkspaceFilesystem>;
|
|
174
207
|
/** Replace the auto-built dynamic skills resolver. */
|
|
175
208
|
skills?: SkillsResolver;
|
|
176
|
-
/**
|
|
177
|
-
|
|
178
|
-
/** Additional native Mastra workspace tool configuration and overrides. */
|
|
179
|
-
tools?: WorkspaceToolsConfig;
|
|
209
|
+
/** Filesystem cache filters. Ignored when the AppKit files-cache plugin is absent. */
|
|
210
|
+
cache?: DatabricksWorkspaceCache;
|
|
180
211
|
/** AppKit plugin context used to discover optional sibling capabilities. */
|
|
181
212
|
pluginContext?: pluginRegistry.PluginContextLike;
|
|
182
|
-
/** Enable BM25 keyword search over indexed workspace content. */
|
|
183
|
-
bm25?: boolean;
|
|
184
213
|
/**
|
|
185
214
|
* Command sandbox. Defaults to Monty. Pass `"databricks"` or an options
|
|
186
215
|
* object for Databricks Sandbox, `false` to disable command execution, or an
|
|
@@ -200,97 +229,78 @@ export interface CreateWorkspaceOptions {
|
|
|
200
229
|
/* ------------------------------- defaults ------------------------------- */
|
|
201
230
|
|
|
202
231
|
/**
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
* - `organization-skills` - `/Workspace/.assistant` with `skills`; write
|
|
206
|
-
* attempts require approval and remain subject to Databricks permissions.
|
|
207
|
-
* - `personal-skills` - the requesting user's `/Workspace/Users/<email>` root
|
|
208
|
-
* with `.assistant/skills`, writable and skipped without an email.
|
|
232
|
+
* Default Databricks paths for shared and current-user Agent Skills.
|
|
209
233
|
*/
|
|
210
|
-
export const
|
|
211
|
-
|
|
212
|
-
description: "Organization skills shared with everyone in this Databricks workspace.",
|
|
213
|
-
displayName: "Organization Skills",
|
|
234
|
+
export const DEFAULT_DATABRICKS_WORKSPACE_PATHS: readonly DatabricksWorkspacePathOptions[] = [
|
|
235
|
+
{
|
|
214
236
|
path: ORGANIZATION_ASSISTANT_PATH,
|
|
215
237
|
readable: true,
|
|
216
238
|
skills: ["skills"],
|
|
217
239
|
writable: true,
|
|
218
240
|
createRoot: false,
|
|
219
241
|
},
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
displayName: "Home",
|
|
223
|
-
path: ({ requestContext }) => {
|
|
224
|
-
const email = resolveScopedEmail(requestContext);
|
|
225
|
-
return email ? personalWorkspacePath(email) : undefined;
|
|
226
|
-
},
|
|
242
|
+
{
|
|
243
|
+
path: "~",
|
|
227
244
|
readable: true,
|
|
228
245
|
skills: [".assistant/skills"],
|
|
229
246
|
writable: true,
|
|
230
247
|
createRoot: false,
|
|
231
248
|
},
|
|
232
|
-
|
|
249
|
+
];
|
|
233
250
|
|
|
234
251
|
/**
|
|
235
252
|
* Create a Mastra {@link Workspace} with per-request Databricks mounts.
|
|
236
253
|
*
|
|
237
|
-
* @example Default
|
|
254
|
+
* @example Default Databricks paths
|
|
238
255
|
* ```ts
|
|
239
|
-
*
|
|
256
|
+
* databricksWorkspace()
|
|
240
257
|
* ```
|
|
241
258
|
*
|
|
242
|
-
* @example
|
|
259
|
+
* @example Use only explicit paths
|
|
243
260
|
* ```ts
|
|
244
|
-
*
|
|
245
|
-
*
|
|
246
|
-
*
|
|
247
|
-
* path: "/Workspace/Shared/.assistant",
|
|
248
|
-
* skills: ["skills"],
|
|
249
|
-
* },
|
|
250
|
-
* "personal-skills": false,
|
|
251
|
-
* runbooks: { path: "/Workspace/Shared/runbooks/skills", writable: true },
|
|
252
|
-
* volume: { filesystem: myVolumeFilesystem },
|
|
253
|
-
* },
|
|
261
|
+
* databricksWorkspace({
|
|
262
|
+
* assistantPaths: false,
|
|
263
|
+
* paths: ["~/project", "/tmp/project", "/Volumes/main/default/data"],
|
|
254
264
|
* })
|
|
255
265
|
* ```
|
|
256
266
|
*
|
|
257
|
-
* @example
|
|
267
|
+
* @example Build a standard Mastra workspace yourself
|
|
258
268
|
* ```ts
|
|
259
|
-
*
|
|
260
|
-
*
|
|
261
|
-
*
|
|
262
|
-
* mounts: { "/data": myFilesystem },
|
|
263
|
-
* skillPaths: [],
|
|
264
|
-
* }),
|
|
265
|
-
* ],
|
|
269
|
+
* new Workspace({
|
|
270
|
+
* ...databricksWorkspaceConfig({ paths: ["~/project"] }),
|
|
271
|
+
* sandbox: mySandbox,
|
|
266
272
|
* })
|
|
267
273
|
* ```
|
|
268
274
|
*/
|
|
269
|
-
export function
|
|
275
|
+
export function databricksWorkspaceConfig(
|
|
276
|
+
options: DatabricksWorkspaceOptions = {},
|
|
277
|
+
): WorkspaceConfig {
|
|
270
278
|
const { id, name } = resolveWorkspaceIdentity(options);
|
|
271
|
-
const
|
|
272
|
-
const
|
|
279
|
+
const filesystemSourceKey = `${id}:${++workspaceSourceSequence}`;
|
|
280
|
+
const paths = resolveDatabricksWorkspacePaths(options);
|
|
281
|
+
const configuredPaths = paths.length;
|
|
273
282
|
const extraSkillPaths = options.extraSkillPaths ?? [];
|
|
274
283
|
const filesCache = pluginRegistry.instance(options.pluginContext, filesCachePlugin)?.exports();
|
|
284
|
+
const configuredCache = options.cache;
|
|
285
|
+
const mountFilesCache = configuredCache === false ? undefined : filesCache;
|
|
275
286
|
const retainFilesystemSource =
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
(folder) =>
|
|
280
|
-
typeof folder.filesystem !== "function" &&
|
|
281
|
-
!(typeof folder.path === "function" && folder.mount !== undefined),
|
|
282
|
-
);
|
|
287
|
+
mountFilesCache !== undefined &&
|
|
288
|
+
typeof configuredCache !== "function" &&
|
|
289
|
+
paths.every((path) => typeof path !== "function");
|
|
283
290
|
const resolvers = buildMountResolvers(
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
291
|
+
paths,
|
|
292
|
+
options.mounts,
|
|
293
|
+
localSkillMountResolvers(extraSkillPaths),
|
|
294
|
+
mountFilesCache,
|
|
295
|
+
configuredCache,
|
|
287
296
|
);
|
|
288
297
|
const resolveContribution = contributionResolver(resolvers);
|
|
289
|
-
const resolveFilesystem = (context:
|
|
298
|
+
const resolveFilesystem = (context: DatabricksWorkspaceContext) =>
|
|
290
299
|
resolveWorkspaceFilesystem(
|
|
291
300
|
resolveContribution,
|
|
292
301
|
context,
|
|
293
|
-
retainFilesystemSource ?
|
|
302
|
+
retainFilesystemSource ? mountFilesCache : undefined,
|
|
303
|
+
filesystemSourceKey,
|
|
294
304
|
);
|
|
295
305
|
const skills: SkillsResolver =
|
|
296
306
|
options.skills ??
|
|
@@ -303,15 +313,15 @@ export function createWorkspace(options: CreateWorkspaceOptions = {}): Workspace
|
|
|
303
313
|
);
|
|
304
314
|
});
|
|
305
315
|
const checkSkillFileMtime = options.checkSkillFileMtime ?? false;
|
|
306
|
-
const bm25 = options.bm25
|
|
316
|
+
const bm25 = options.bm25 ?? true;
|
|
307
317
|
const sandbox = resolveWorkspaceSandbox(options.sandbox, id, name);
|
|
308
318
|
const tools = workspaceTools(options.tools);
|
|
309
319
|
logger.debug("workspace:create", {
|
|
310
320
|
id,
|
|
311
321
|
name,
|
|
312
322
|
resolverCount: resolvers.length,
|
|
313
|
-
|
|
314
|
-
|
|
323
|
+
configuredPaths,
|
|
324
|
+
customMounts: Object.keys(options.mounts ?? {}).length,
|
|
315
325
|
customSkillsResolver: Boolean(options.skills),
|
|
316
326
|
checkSkillFileMtime,
|
|
317
327
|
bm25,
|
|
@@ -319,7 +329,19 @@ export function createWorkspace(options: CreateWorkspaceOptions = {}): Workspace
|
|
|
319
329
|
sandbox: sandbox ? sandboxName(options.sandbox) : "disabled",
|
|
320
330
|
});
|
|
321
331
|
|
|
322
|
-
const
|
|
332
|
+
const {
|
|
333
|
+
assistantPaths: _assistantPaths,
|
|
334
|
+
paths: _paths,
|
|
335
|
+
mounts: _mounts,
|
|
336
|
+
skills: _skills,
|
|
337
|
+
cache: _cache,
|
|
338
|
+
pluginContext: _pluginContext,
|
|
339
|
+
sandbox: _sandbox,
|
|
340
|
+
extraSkillPaths: _extraSkillPaths,
|
|
341
|
+
...workspaceOptions
|
|
342
|
+
} = options;
|
|
343
|
+
return {
|
|
344
|
+
...workspaceOptions,
|
|
323
345
|
id,
|
|
324
346
|
name,
|
|
325
347
|
filesystem: resolveFilesystem,
|
|
@@ -337,75 +359,50 @@ export function createWorkspace(options: CreateWorkspaceOptions = {}): Workspace
|
|
|
337
359
|
: {}),
|
|
338
360
|
bm25,
|
|
339
361
|
tools,
|
|
340
|
-
}
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/** Create a native Mastra {@link Workspace} from {@link databricksWorkspaceConfig}. */
|
|
366
|
+
export function databricksWorkspace(options: DatabricksWorkspaceOptions = {}): Workspace {
|
|
367
|
+
const workspace = new Workspace(databricksWorkspaceConfig(options));
|
|
368
|
+
databricksWorkspaceOptions.set(workspace, options);
|
|
341
369
|
return workspace;
|
|
342
370
|
}
|
|
343
371
|
|
|
372
|
+
/** @internal Bind a caller-created Databricks workspace to its AppKit plugin context. */
|
|
373
|
+
export function bindDatabricksWorkspaceContext(
|
|
374
|
+
workspace: Workspace,
|
|
375
|
+
pluginContext: pluginRegistry.PluginContextLike | undefined,
|
|
376
|
+
): Workspace {
|
|
377
|
+
const options = databricksWorkspaceOptions.get(workspace);
|
|
378
|
+
if (!options || options.pluginContext !== undefined || pluginContext === undefined) {
|
|
379
|
+
return workspace;
|
|
380
|
+
}
|
|
381
|
+
return databricksWorkspace({ ...options, pluginContext });
|
|
382
|
+
}
|
|
383
|
+
|
|
344
384
|
/**
|
|
345
|
-
*
|
|
346
|
-
*
|
|
347
|
-
* `assistantSkills: false` drops the defaults, and a `false` value removes one
|
|
348
|
-
* entry by name.
|
|
385
|
+
* Resolve default and caller-provided Databricks paths in priority order.
|
|
349
386
|
*/
|
|
350
|
-
export function
|
|
351
|
-
options: Pick<
|
|
352
|
-
):
|
|
353
|
-
|
|
354
|
-
options.
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
delete merged[name];
|
|
358
|
-
} else {
|
|
359
|
-
merged[name] = folder;
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
return merged;
|
|
387
|
+
export function resolveDatabricksWorkspacePaths(
|
|
388
|
+
options: Pick<DatabricksWorkspaceOptions, "assistantPaths" | "paths"> = {},
|
|
389
|
+
): DatabricksWorkspacePathValue[] {
|
|
390
|
+
return [
|
|
391
|
+
...(options.assistantPaths === false ? [] : DEFAULT_DATABRICKS_WORKSPACE_PATHS),
|
|
392
|
+
...(options.paths ?? []),
|
|
393
|
+
];
|
|
363
394
|
}
|
|
364
395
|
|
|
365
396
|
/* ---------------------------- private helpers ---------------------------- */
|
|
366
397
|
|
|
367
|
-
/** Require approval unless a filesystem mutation stays inside a user-owned writable root. */
|
|
368
|
-
function requireApprovalOutsideWritableRoots({
|
|
369
|
-
args,
|
|
370
|
-
requestContext,
|
|
371
|
-
}: ToolConfigWithArgsContext): boolean {
|
|
372
|
-
const inputPath = args.path;
|
|
373
|
-
if (typeof inputPath !== "string" || !posixPath.isAbsolute(inputPath)) return true;
|
|
374
|
-
const normalized = posixPath.normalize(inputPath);
|
|
375
|
-
if (!normalized.ok) return true;
|
|
376
|
-
if (posixPath.isWithinRoot(SCRATCH_MOUNT, normalized.path)) return false;
|
|
377
|
-
const emailValue = requestContext[MASTRA_USER_EMAIL_KEY];
|
|
378
|
-
const email = typeof emailValue === "string" ? stringUtils.trimToNull(emailValue) : undefined;
|
|
379
|
-
if (!email) return true;
|
|
380
|
-
return !posixPath.isWithinRoot(personalWorkspacePath(email), normalized.path);
|
|
381
|
-
}
|
|
382
|
-
|
|
383
398
|
function workspaceTools(configured: WorkspaceToolsConfig | undefined): WorkspaceToolsConfig {
|
|
384
|
-
const requireApproval: WorkspaceToolConfig =
|
|
385
|
-
configured?.requireApproval === undefined
|
|
386
|
-
? { requireApproval: requireApprovalOutsideWritableRoots }
|
|
387
|
-
: {};
|
|
388
|
-
const defaults = {
|
|
389
|
-
[WORKSPACE_TOOLS.FILESYSTEM.WRITE_FILE]: requireApproval,
|
|
390
|
-
[WORKSPACE_TOOLS.FILESYSTEM.EDIT_FILE]: requireApproval,
|
|
391
|
-
[WORKSPACE_TOOLS.FILESYSTEM.AST_EDIT]: requireApproval,
|
|
392
|
-
[WORKSPACE_TOOLS.FILESYSTEM.DELETE]: requireApproval,
|
|
393
|
-
[WORKSPACE_TOOLS.FILESYSTEM.MKDIR]: requireApproval,
|
|
394
|
-
} satisfies WorkspaceToolsConfig;
|
|
395
|
-
const configuredTools = configured as
|
|
396
|
-
Readonly<Record<string, WorkspaceToolConfig | undefined>> | undefined;
|
|
397
|
-
const mergedTools = Object.fromEntries(
|
|
398
|
-
Object.entries(defaults).map(([name, defaultConfig]) => [
|
|
399
|
-
name,
|
|
400
|
-
{ ...defaultConfig, ...configuredTools?.[name] },
|
|
401
|
-
]),
|
|
402
|
-
);
|
|
403
399
|
return {
|
|
404
|
-
|
|
400
|
+
enabled: true,
|
|
401
|
+
requireApproval: false,
|
|
405
402
|
...configured,
|
|
406
|
-
...mergedTools,
|
|
407
403
|
};
|
|
408
404
|
}
|
|
405
|
+
|
|
409
406
|
function resolveWorkspaceSandbox(
|
|
410
407
|
selection: WorkspaceSandboxSelection | undefined,
|
|
411
408
|
workspaceId: string,
|
|
@@ -484,7 +481,7 @@ function hasWorkspaceFileScope(requestContext: RequestContext | undefined): bool
|
|
|
484
481
|
}
|
|
485
482
|
|
|
486
483
|
/** Resolve the request client and authorization-safe files-cache scope. */
|
|
487
|
-
async function resolveFilesCacheScope(context:
|
|
484
|
+
async function resolveFilesCacheScope(context: DatabricksWorkspaceContext): Promise<
|
|
488
485
|
| {
|
|
489
486
|
client: WorkspaceClient;
|
|
490
487
|
scope: FilesCacheScope;
|
|
@@ -532,38 +529,52 @@ function isWorkspaceRoot(root: string): boolean {
|
|
|
532
529
|
* folder whose location resolves for this request.
|
|
533
530
|
*/
|
|
534
531
|
async function resolveSkillFolderMounts(
|
|
535
|
-
|
|
536
|
-
context:
|
|
532
|
+
paths: readonly DatabricksWorkspacePathValue[],
|
|
533
|
+
context: DatabricksWorkspaceContext,
|
|
537
534
|
filesCache: FilesCacheExports | undefined,
|
|
535
|
+
cacheConfig: DatabricksWorkspaceCache | undefined,
|
|
538
536
|
): Promise<WorkspaceMountContribution> {
|
|
539
537
|
const mounts: Record<string, WorkspaceFilesystem> = {};
|
|
540
538
|
const skillPaths: string[] = [];
|
|
541
539
|
const requestContext = context.requestContext;
|
|
542
540
|
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
reason: !requestContext ? "no-request-context" : "missing-workspace-scope",
|
|
546
|
-
nodeEnv: process.env.NODE_ENV,
|
|
547
|
-
scopes: requestContext?.get(MASTRA_SCOPES_KEY),
|
|
548
|
-
});
|
|
549
|
-
return { mounts, skillPaths };
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
const scoped = await resolveFilesCacheScope(context);
|
|
541
|
+
const canMountDatabricks = Boolean(requestContext && shouldMountSkillFolders(requestContext));
|
|
542
|
+
const scoped = canMountDatabricks ? await resolveFilesCacheScope(context) : undefined;
|
|
553
543
|
const client = scoped?.client;
|
|
554
544
|
const fileSystemCache =
|
|
555
545
|
filesCache && scoped ? await filesCache.forScope(scoped.scope) : undefined;
|
|
556
546
|
|
|
557
|
-
for (const [
|
|
558
|
-
const
|
|
547
|
+
for (const [index, path] of paths.entries()) {
|
|
548
|
+
const configured = await resolveDatabricksWorkspacePath(path, context);
|
|
549
|
+
if (!configured) continue;
|
|
550
|
+
if (posixPath.isWithinRoot(SCRATCH_MOUNT, configured.path)) {
|
|
551
|
+
const mount = configured.mount ?? configured.path;
|
|
552
|
+
mounts[mount] = userTempFilesystem(context, configured.path);
|
|
553
|
+
if (configured.readable !== false) {
|
|
554
|
+
skillPaths.push(...(configured.skills ?? []).map((path) => mountedSkillPath(mount, path)));
|
|
555
|
+
}
|
|
556
|
+
continue;
|
|
557
|
+
}
|
|
558
|
+
if (!canMountDatabricks) {
|
|
559
|
+
logger.debug("workspace-path:skipped", {
|
|
560
|
+
path: configured.path,
|
|
561
|
+
reason: !requestContext ? "no-request-context" : "missing-workspace-scope",
|
|
562
|
+
});
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
const resolved = await resolveSkillFolderFilesystem(configured, context, cacheConfig, {
|
|
559
566
|
client,
|
|
560
567
|
fileSystemCache,
|
|
561
568
|
});
|
|
562
569
|
if (!resolved) continue;
|
|
563
|
-
const mount =
|
|
570
|
+
const mount = configured.mount ?? resolved.root;
|
|
571
|
+
if (!mount) {
|
|
572
|
+
logger.debug("workspace-path:skipped", { index, reason: "missing-mount" });
|
|
573
|
+
continue;
|
|
574
|
+
}
|
|
564
575
|
mounts[mount] = resolved.filesystem;
|
|
565
|
-
if (
|
|
566
|
-
skillPaths.push(...(
|
|
576
|
+
if (configured.readable !== false) {
|
|
577
|
+
skillPaths.push(...(configured.skills ?? ["."]).map((path) => mountedSkillPath(mount, path)));
|
|
567
578
|
}
|
|
568
579
|
}
|
|
569
580
|
|
|
@@ -578,14 +589,43 @@ async function resolveSkillFolderMounts(
|
|
|
578
589
|
};
|
|
579
590
|
}
|
|
580
591
|
|
|
592
|
+
interface ResolvedDatabricksWorkspacePath {
|
|
593
|
+
path: string;
|
|
594
|
+
displayName?: string;
|
|
595
|
+
description?: string;
|
|
596
|
+
readable?: boolean;
|
|
597
|
+
skills?: readonly string[];
|
|
598
|
+
writable?: boolean;
|
|
599
|
+
createRoot?: boolean;
|
|
600
|
+
mount?: string;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
async function resolveDatabricksWorkspacePath(
|
|
604
|
+
value: DatabricksWorkspacePathValue,
|
|
605
|
+
context: DatabricksWorkspaceContext,
|
|
606
|
+
): Promise<ResolvedDatabricksWorkspacePath | undefined> {
|
|
607
|
+
const selected = typeof value === "function" ? await value(context) : value;
|
|
608
|
+
if (selected === false || selected === undefined) return undefined;
|
|
609
|
+
const configured = typeof selected === "string" ? { path: selected } : selected;
|
|
610
|
+
const requestContext = requestContextValues(context.requestContext);
|
|
611
|
+
const resolvedPath = resolveHomePath(configured.path, requestContext);
|
|
612
|
+
if (!resolvedPath) return undefined;
|
|
613
|
+
const resolvedMount = resolveHomePath(configured.mount, requestContext);
|
|
614
|
+
return {
|
|
615
|
+
...configured,
|
|
616
|
+
path: resolvedPath,
|
|
617
|
+
...(resolvedMount ? { mount: resolvedMount } : {}),
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
|
|
581
621
|
/**
|
|
582
622
|
* Resolve one skill folder to a Mastra filesystem, or `undefined` to skip it
|
|
583
623
|
* for this request.
|
|
584
624
|
*/
|
|
585
625
|
async function resolveSkillFolderFilesystem(
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
626
|
+
folder: ResolvedDatabricksWorkspacePath,
|
|
627
|
+
context: DatabricksWorkspaceContext,
|
|
628
|
+
cacheConfig: DatabricksWorkspaceCache | undefined,
|
|
589
629
|
cache: {
|
|
590
630
|
client: WorkspaceClient | undefined;
|
|
591
631
|
fileSystemCache: FileSystemCache | undefined;
|
|
@@ -597,32 +637,32 @@ async function resolveSkillFolderFilesystem(
|
|
|
597
637
|
}
|
|
598
638
|
| undefined
|
|
599
639
|
> {
|
|
600
|
-
if (folder.filesystem !== undefined) {
|
|
601
|
-
const filesystem = await resolveSkillFolderValue(folder.filesystem, context);
|
|
602
|
-
return filesystem
|
|
603
|
-
? {
|
|
604
|
-
filesystem,
|
|
605
|
-
}
|
|
606
|
-
: undefined;
|
|
607
|
-
}
|
|
608
640
|
// A path mount needs the request's OBO client to reach the workspace.
|
|
609
|
-
if (
|
|
641
|
+
if (!cache.client) {
|
|
610
642
|
logger.debug("skill-folder:skipped", {
|
|
611
|
-
|
|
612
|
-
reason:
|
|
643
|
+
path: folder.path,
|
|
644
|
+
reason: "missing-obo-client",
|
|
613
645
|
});
|
|
614
646
|
return undefined;
|
|
615
647
|
}
|
|
616
|
-
const root = stringUtils.trimToNull(
|
|
648
|
+
const root = stringUtils.trimToNull(folder.path);
|
|
617
649
|
if (!root) return undefined;
|
|
618
650
|
const writable = folder.writable ?? isWorkspaceRoot(root);
|
|
651
|
+
const cacheOptions = cache.fileSystemCache
|
|
652
|
+
? await resolveFileCacheOptions(
|
|
653
|
+
cacheConfig,
|
|
654
|
+
context,
|
|
655
|
+
root,
|
|
656
|
+
folder.readable === false ? [] : (folder.skills ?? ["."]),
|
|
657
|
+
)
|
|
658
|
+
: undefined;
|
|
619
659
|
const resolved = await databricksFilesystem(
|
|
620
660
|
cache.client,
|
|
621
661
|
root,
|
|
622
662
|
!writable,
|
|
623
663
|
folder.createRoot ?? folder.writable === true,
|
|
624
664
|
cache.fileSystemCache,
|
|
625
|
-
|
|
665
|
+
cacheOptions,
|
|
626
666
|
{
|
|
627
667
|
...(folder.description ? { description: folder.description } : {}),
|
|
628
668
|
...(folder.displayName ? { displayName: folder.displayName } : {}),
|
|
@@ -631,21 +671,103 @@ async function resolveSkillFolderFilesystem(
|
|
|
631
671
|
return resolved ? { ...resolved, root } : undefined;
|
|
632
672
|
}
|
|
633
673
|
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
674
|
+
async function resolveCacheConfig(
|
|
675
|
+
value: DatabricksWorkspaceCache | undefined,
|
|
676
|
+
context: DatabricksWorkspaceContext,
|
|
677
|
+
): Promise<
|
|
678
|
+
boolean | DatabricksWorkspaceCacheFilter | readonly DatabricksWorkspaceCacheFilter[] | undefined
|
|
679
|
+
> {
|
|
680
|
+
return typeof value === "function" ? value(context) : value;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function requestContextValues(requestContext: RequestContext | undefined): Record<string, unknown> {
|
|
684
|
+
return requestContext?.toJSON() ?? {};
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function resolveHomePath(
|
|
688
|
+
input: string | undefined,
|
|
689
|
+
requestContext: Record<string, unknown>,
|
|
690
|
+
): string | undefined {
|
|
691
|
+
const value = stringUtils.trimToNull(input);
|
|
692
|
+
if (!value || (value !== "~" && !value.startsWith("~/"))) return value ?? undefined;
|
|
693
|
+
const emailValue = requestContext[MASTRA_USER_EMAIL_KEY];
|
|
694
|
+
const email = typeof emailValue === "string" ? stringUtils.trimToNull(emailValue) : undefined;
|
|
695
|
+
if (!email) return undefined;
|
|
696
|
+
const home = personalWorkspacePath(email);
|
|
697
|
+
return value === "~" ? home : `${home}/${value.slice(2)}`;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
function toPolicyArray<T>(value: T | readonly T[] | undefined): readonly T[] {
|
|
701
|
+
if (value === undefined) return [];
|
|
702
|
+
return Array.isArray(value) ? (value as readonly T[]) : [value as T];
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
function cacheFilterMatches(
|
|
706
|
+
policy: DatabricksWorkspaceCacheFilter,
|
|
707
|
+
operation: CacheableFileSystemOperation,
|
|
708
|
+
path: string,
|
|
709
|
+
requestContext: Record<string, unknown>,
|
|
710
|
+
root?: string,
|
|
711
|
+
): boolean {
|
|
712
|
+
const operations = toPolicyArray(policy.operations ?? CACHEABLE_FILESYSTEM_OPERATIONS);
|
|
713
|
+
if (!operations.includes(operation)) return false;
|
|
714
|
+
if (policy.paths === undefined) return true;
|
|
715
|
+
const inputs: PathMatchInput[] = [];
|
|
716
|
+
for (const input of toPolicyArray<PathMatchInput>(policy.paths)) {
|
|
717
|
+
if (typeof input !== "string") {
|
|
718
|
+
inputs.push(input);
|
|
719
|
+
continue;
|
|
720
|
+
}
|
|
721
|
+
const expanded = resolveHomePath(input, requestContext);
|
|
722
|
+
if (!expanded) continue;
|
|
723
|
+
inputs.push(
|
|
724
|
+
!root || posixPath.isAbsolute(expanded) || expanded.startsWith("**")
|
|
725
|
+
? expanded
|
|
726
|
+
: `${posixPath.normalizeRoot(root)}/${expanded.replace(/^\.\//, "")}`,
|
|
727
|
+
);
|
|
728
|
+
}
|
|
729
|
+
return inputs.length > 0 && match.toPathMatcher(...inputs)(path);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
async function resolveFileCacheOptions(
|
|
733
|
+
configured: DatabricksWorkspaceCache | undefined,
|
|
734
|
+
context: DatabricksWorkspaceContext,
|
|
735
|
+
root: string,
|
|
736
|
+
skillPaths: readonly string[],
|
|
737
|
+
): Promise<FileSystemCacheOptions | undefined> {
|
|
738
|
+
const requestContext = requestContextValues(context.requestContext);
|
|
739
|
+
const resolved = await resolveCacheConfig(configured, context);
|
|
740
|
+
if (resolved === false) return undefined;
|
|
741
|
+
if (resolved === undefined || resolved === true) {
|
|
742
|
+
const roots = skillPaths.map((path) => mountedSkillPath(root, path));
|
|
743
|
+
return {
|
|
744
|
+
operations: CACHEABLE_FILESYSTEM_OPERATIONS,
|
|
745
|
+
filter: (operation, path) =>
|
|
746
|
+
operation !== "readFile" ||
|
|
747
|
+
roots.some((skillRoot) => posixPath.isWithinRoot(skillRoot, path)),
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
const policies = toPolicyArray(resolved);
|
|
751
|
+
if (policies.length === 0) return undefined;
|
|
752
|
+
const operations = [
|
|
753
|
+
...new Set(
|
|
754
|
+
policies.flatMap((policy) =>
|
|
755
|
+
toPolicyArray(policy.operations ?? CACHEABLE_FILESYSTEM_OPERATIONS),
|
|
756
|
+
),
|
|
757
|
+
),
|
|
758
|
+
];
|
|
759
|
+
return {
|
|
760
|
+
operations,
|
|
761
|
+
filter: (operation, path) =>
|
|
762
|
+
policies.some((policy) => cacheFilterMatches(policy, operation, path, requestContext, root)),
|
|
763
|
+
};
|
|
642
764
|
}
|
|
643
765
|
|
|
644
766
|
/**
|
|
645
|
-
* Fill in `id` and `name` when either is omitted on {@link
|
|
767
|
+
* Fill in `id` and `name` when either is omitted on {@link DatabricksWorkspaceOptions}.
|
|
646
768
|
* Slugifies `name` into `id`; tokenizes `id` into a display `name`.
|
|
647
769
|
*/
|
|
648
|
-
function resolveWorkspaceIdentity(options:
|
|
770
|
+
function resolveWorkspaceIdentity(options: DatabricksWorkspaceOptions): {
|
|
649
771
|
id: string;
|
|
650
772
|
name: string;
|
|
651
773
|
} {
|
|
@@ -662,30 +784,49 @@ function resolveWorkspaceIdentity(options: CreateWorkspaceOptions): {
|
|
|
662
784
|
|
|
663
785
|
/** Collect the skill-folder resolver and any caller-supplied ones. */
|
|
664
786
|
function buildMountResolvers(
|
|
665
|
-
|
|
666
|
-
mounts:
|
|
787
|
+
paths: readonly DatabricksWorkspacePathValue[],
|
|
788
|
+
mounts: Record<string, WorkspaceFilesystem> | undefined,
|
|
789
|
+
localMounts: WorkspaceMountResolver[],
|
|
667
790
|
filesCache: FilesCacheExports | undefined,
|
|
791
|
+
cacheConfig: DatabricksWorkspaceCache | undefined,
|
|
668
792
|
): WorkspaceMountResolver[] {
|
|
669
|
-
const resolvers: WorkspaceMountResolver[] = [
|
|
670
|
-
const
|
|
671
|
-
if (
|
|
672
|
-
resolvers.push((context) => resolveSkillFolderMounts(
|
|
673
|
-
}
|
|
674
|
-
if (mounts?.length) {
|
|
675
|
-
resolvers.push(...mounts);
|
|
793
|
+
const resolvers: WorkspaceMountResolver[] = [];
|
|
794
|
+
const pathCount = paths.length;
|
|
795
|
+
if (pathCount > 0) {
|
|
796
|
+
resolvers.push((context) => resolveSkillFolderMounts(paths, context, filesCache, cacheConfig));
|
|
676
797
|
}
|
|
798
|
+
if (mounts && Object.keys(mounts).length > 0) resolvers.push(() => ({ mounts }));
|
|
799
|
+
resolvers.push(...localMounts);
|
|
677
800
|
logger.debug("mounts:resolvers", {
|
|
678
|
-
|
|
679
|
-
|
|
801
|
+
pathCount,
|
|
802
|
+
customMountCount: Object.keys(mounts ?? {}).length,
|
|
803
|
+
localMountCount: localMounts.length,
|
|
680
804
|
totalResolverCount: resolvers.length,
|
|
681
805
|
});
|
|
682
806
|
return resolvers;
|
|
683
807
|
}
|
|
684
808
|
|
|
685
|
-
/**
|
|
686
|
-
function
|
|
687
|
-
|
|
688
|
-
|
|
809
|
+
/** Stable user-scoped local storage under the host operating system's temp directory. */
|
|
810
|
+
function userTempFilesystem(
|
|
811
|
+
context: DatabricksWorkspaceContext,
|
|
812
|
+
workspacePath: string,
|
|
813
|
+
): WorkspaceFilesystem {
|
|
814
|
+
const userKey = resolveUserKey(context.requestContext);
|
|
815
|
+
const digest = createHash("sha256")
|
|
816
|
+
.update(object.toStableKey({ package: "@dbx-tools/appkit-mastra", userKey }))
|
|
817
|
+
.digest("hex")
|
|
818
|
+
.slice(0, 16);
|
|
819
|
+
const normalized = posixPath.normalizeRoot(workspacePath);
|
|
820
|
+
const relative = normalized === SCRATCH_MOUNT ? "" : normalized.slice(`${SCRATCH_MOUNT}/`.length);
|
|
821
|
+
const source = new LocalFileSystem({
|
|
822
|
+
root: join(tmpdir(), "dbx-tools", "mastra", digest, relative),
|
|
823
|
+
readOnly: false,
|
|
824
|
+
createRoot: true,
|
|
825
|
+
});
|
|
826
|
+
return filesystems(source, {
|
|
827
|
+
displayName: "Temporary Files",
|
|
828
|
+
description:
|
|
829
|
+
"User-scoped temporary files retained while the host keeps its ephemeral temp storage.",
|
|
689
830
|
});
|
|
690
831
|
}
|
|
691
832
|
|
|
@@ -717,15 +858,10 @@ function shouldMountSkillFolders(requestContext: RequestContext): boolean {
|
|
|
717
858
|
return hasWorkspaceFileScope(requestContext);
|
|
718
859
|
}
|
|
719
860
|
|
|
720
|
-
/** Read the trimmed OBO user email stamped on {@link MASTRA_USER_EMAIL_KEY}. */
|
|
721
|
-
function resolveScopedEmail(requestContext: RequestContext | undefined): string | undefined {
|
|
722
|
-
return stringUtils.trimToNull(requestContext?.get(MASTRA_USER_EMAIL_KEY)) ?? undefined;
|
|
723
|
-
}
|
|
724
|
-
|
|
725
861
|
/**
|
|
726
862
|
* Wrap a {@link DatabricksFileSystem} as a Mastra filesystem. Missing or
|
|
727
|
-
* inaccessible roots return `undefined` so
|
|
728
|
-
*
|
|
863
|
+
* inaccessible roots return `undefined` so other accessible roots remain
|
|
864
|
+
* usable.
|
|
729
865
|
*/
|
|
730
866
|
async function databricksFilesystem(
|
|
731
867
|
client: WorkspaceClient,
|
|
@@ -733,7 +869,7 @@ async function databricksFilesystem(
|
|
|
733
869
|
readOnly: boolean = true,
|
|
734
870
|
createRoot: boolean = !readOnly,
|
|
735
871
|
fileSystemCache?: FileSystemCache,
|
|
736
|
-
|
|
872
|
+
cacheOptions?: FileSystemCacheOptions,
|
|
737
873
|
adapterOptions: Pick<MastraFileSystemAdapterOptions, "description" | "displayName"> = {},
|
|
738
874
|
): Promise<
|
|
739
875
|
| {
|
|
@@ -741,22 +877,15 @@ async function databricksFilesystem(
|
|
|
741
877
|
}
|
|
742
878
|
| undefined
|
|
743
879
|
> {
|
|
744
|
-
const initialClient = workspaceClient.toLegacyWorkspaceClient(client);
|
|
745
880
|
const fs = new DatabricksFileSystem({
|
|
746
|
-
client: () =>
|
|
747
|
-
try {
|
|
748
|
-
return workspaceClient.toLegacyWorkspaceClient(getExecutionContext().client);
|
|
749
|
-
} catch {
|
|
750
|
-
return initialClient;
|
|
751
|
-
}
|
|
752
|
-
},
|
|
881
|
+
client: () => workspaceClient.toLegacyWorkspaceClient(requestWorkspaceClient(client)),
|
|
753
882
|
root,
|
|
754
883
|
readOnly,
|
|
755
884
|
createRoot,
|
|
756
885
|
});
|
|
757
|
-
const
|
|
758
|
-
? sharedFS.cache(fs, fileSystemCache,
|
|
759
|
-
|
|
886
|
+
const cached =
|
|
887
|
+
fileSystemCache && cacheOptions ? sharedFS.cache(fs, fileSystemCache, cacheOptions) : fs;
|
|
888
|
+
const source = verifyFirstFileSystemAccess(cached, () => fs.stat("."), root);
|
|
760
889
|
const filesystem = filesystems(source, { ...adapterOptions, readOnly });
|
|
761
890
|
try {
|
|
762
891
|
await filesystem.init();
|
|
@@ -773,13 +902,100 @@ async function databricksFilesystem(
|
|
|
773
902
|
return undefined;
|
|
774
903
|
}
|
|
775
904
|
|
|
905
|
+
/** @internal Keep service context from replacing the request-scoped workspace client. */
|
|
906
|
+
export function requestWorkspaceClient(
|
|
907
|
+
initialClient: WorkspaceClient,
|
|
908
|
+
executionContext: ReturnType<
|
|
909
|
+
typeof appkit.tryGetExecutionContext
|
|
910
|
+
> = appkit.tryGetExecutionContext(),
|
|
911
|
+
): WorkspaceClient {
|
|
912
|
+
if (
|
|
913
|
+
executionContext &&
|
|
914
|
+
"isUserContext" in executionContext &&
|
|
915
|
+
executionContext.isUserContext === true
|
|
916
|
+
) {
|
|
917
|
+
return executionContext.client;
|
|
918
|
+
}
|
|
919
|
+
return initialClient;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
function verifyFirstFileSystemAccess<TFileSystem extends FileSystem>(
|
|
923
|
+
filesystem: TFileSystem,
|
|
924
|
+
readProbe: () => Promise<unknown>,
|
|
925
|
+
root: string,
|
|
926
|
+
): TFileSystem {
|
|
927
|
+
let readCheck: Promise<void> | undefined;
|
|
928
|
+
let writeVerified = false;
|
|
929
|
+
const boundMethods = new Map<PropertyKey, unknown>();
|
|
930
|
+
const readOperations = new Set<PropertyKey>(["exists", "readFile", "readdir", "stat"]);
|
|
931
|
+
const writeOperations = new Set<PropertyKey>([
|
|
932
|
+
"appendFile",
|
|
933
|
+
"copyFile",
|
|
934
|
+
"deleteFile",
|
|
935
|
+
"mkdir",
|
|
936
|
+
"moveFile",
|
|
937
|
+
"rmdir",
|
|
938
|
+
"writeFile",
|
|
939
|
+
]);
|
|
940
|
+
const verifyRead = async (): Promise<void> => {
|
|
941
|
+
readCheck ??= Promise.resolve(readProbe()).then(() => undefined);
|
|
942
|
+
try {
|
|
943
|
+
await readCheck;
|
|
944
|
+
} catch (error) {
|
|
945
|
+
readCheck = undefined;
|
|
946
|
+
logger.debug("databricks-mount:read-check-failed", {
|
|
947
|
+
root,
|
|
948
|
+
error: errorUtils.errorMessage(error),
|
|
949
|
+
});
|
|
950
|
+
throw error;
|
|
951
|
+
}
|
|
952
|
+
};
|
|
953
|
+
|
|
954
|
+
return new Proxy(filesystem, {
|
|
955
|
+
get(target, property) {
|
|
956
|
+
const value = Reflect.get(target, property, target);
|
|
957
|
+
if (typeof value !== "function") return value;
|
|
958
|
+
if (boundMethods.has(property)) return boundMethods.get(property);
|
|
959
|
+
const delegated = value.bind(target) as (...args: unknown[]) => unknown;
|
|
960
|
+
let method = delegated;
|
|
961
|
+
if (readOperations.has(property)) {
|
|
962
|
+
method = async (...args: unknown[]) => {
|
|
963
|
+
await verifyRead();
|
|
964
|
+
return delegated(...args);
|
|
965
|
+
};
|
|
966
|
+
} else if (writeOperations.has(property)) {
|
|
967
|
+
method = async (...args: unknown[]) => {
|
|
968
|
+
try {
|
|
969
|
+
const result = await delegated(...args);
|
|
970
|
+
if (!writeVerified) {
|
|
971
|
+
writeVerified = true;
|
|
972
|
+
logger.debug("databricks-mount:write-check-passed", { root });
|
|
973
|
+
}
|
|
974
|
+
return result;
|
|
975
|
+
} catch (error) {
|
|
976
|
+
if (!writeVerified) {
|
|
977
|
+
logger.debug("databricks-mount:write-check-failed", {
|
|
978
|
+
root,
|
|
979
|
+
error: errorUtils.errorMessage(error),
|
|
980
|
+
});
|
|
981
|
+
}
|
|
982
|
+
throw error;
|
|
983
|
+
}
|
|
984
|
+
};
|
|
985
|
+
}
|
|
986
|
+
boundMethods.set(property, method);
|
|
987
|
+
return method;
|
|
988
|
+
},
|
|
989
|
+
});
|
|
990
|
+
}
|
|
991
|
+
|
|
776
992
|
/**
|
|
777
993
|
* Run every mount resolver for one request and merge mounts plus skill paths.
|
|
778
994
|
* Later resolvers overwrite mount keys from earlier ones.
|
|
779
995
|
*/
|
|
780
996
|
async function resolveWorkspaceContribution(
|
|
781
997
|
resolvers: WorkspaceMountResolver[],
|
|
782
|
-
context:
|
|
998
|
+
context: DatabricksWorkspaceContext,
|
|
783
999
|
): Promise<WorkspaceMountContribution> {
|
|
784
1000
|
const mounts: Record<string, WorkspaceFilesystem> = {};
|
|
785
1001
|
const skillPaths: string[] = [];
|
|
@@ -811,34 +1027,37 @@ async function resolveWorkspaceContribution(
|
|
|
811
1027
|
/**
|
|
812
1028
|
* Dynamic filesystem resolver passed to Mastra {@link Workspace}.
|
|
813
1029
|
*
|
|
814
|
-
* Returns a {@link CompositeFilesystem}
|
|
815
|
-
*
|
|
1030
|
+
* Returns a {@link CompositeFilesystem} over every path that resolved for the
|
|
1031
|
+
* request. An empty composite exposes no implicit local storage.
|
|
816
1032
|
*/
|
|
817
1033
|
async function resolveWorkspaceFilesystem(
|
|
818
|
-
resolveContribution: (context:
|
|
819
|
-
context:
|
|
1034
|
+
resolveContribution: (context: DatabricksWorkspaceContext) => Promise<WorkspaceMountContribution>,
|
|
1035
|
+
context: DatabricksWorkspaceContext,
|
|
820
1036
|
filesCache?: FilesCacheExports,
|
|
1037
|
+
sourceKey?: string,
|
|
821
1038
|
): Promise<WorkspaceFilesystem> {
|
|
822
1039
|
const { mounts } = await resolveContribution(context);
|
|
823
1040
|
const mountKeys = Object.keys(mounts);
|
|
824
1041
|
if (mountKeys.length === 0) {
|
|
825
|
-
logger.debug("filesystem:
|
|
1042
|
+
logger.debug("filesystem:empty", {
|
|
826
1043
|
hasRequestContext: Boolean(context.requestContext),
|
|
827
1044
|
});
|
|
828
|
-
return
|
|
1045
|
+
return filesystems(new MemoryFileSystem({ root: "/", readOnly: true }), {
|
|
1046
|
+
readOnly: true,
|
|
1047
|
+
});
|
|
829
1048
|
}
|
|
830
1049
|
logger.debug("filesystem:composite", { mountKeys });
|
|
831
1050
|
const load = () => new MountedCompositeFilesystem({ mounts });
|
|
832
1051
|
if (!filesCache) return load();
|
|
833
1052
|
const scoped = await resolveFilesCacheScope(context);
|
|
834
1053
|
if (!scoped) return load();
|
|
835
|
-
return filesCache.forFileSystem(scoped.scope, { paths: mountKeys }, load);
|
|
1054
|
+
return filesCache.forFileSystem(scoped.scope, { paths: mountKeys, key: sourceKey }, load);
|
|
836
1055
|
}
|
|
837
1056
|
|
|
838
1057
|
/**
|
|
839
1058
|
* Expand skill roots into concrete skill directories and keep the first
|
|
840
1059
|
* same-named directory. Mastra still owns parsing, indexing, and refreshes,
|
|
841
|
-
* while
|
|
1060
|
+
* while earlier configured roots retain precedence over later roots.
|
|
842
1061
|
*/
|
|
843
1062
|
async function resolveDistinctSkillPaths(
|
|
844
1063
|
filesystem: WorkspaceFilesystem,
|
|
@@ -870,7 +1089,7 @@ async function resolveDistinctSkillPaths(
|
|
|
870
1089
|
/** Memoize one complete mount contribution for each request context. */
|
|
871
1090
|
function contributionResolver(
|
|
872
1091
|
resolvers: WorkspaceMountResolver[],
|
|
873
|
-
): (context:
|
|
1092
|
+
): (context: DatabricksWorkspaceContext) => Promise<WorkspaceMountContribution> {
|
|
874
1093
|
const scoped = new WeakMap<RequestContext, Promise<WorkspaceMountContribution>>();
|
|
875
1094
|
let unscoped: Promise<WorkspaceMountContribution> | undefined;
|
|
876
1095
|
return (context) => {
|