@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/src/workspaces.ts CHANGED
@@ -1,16 +1,10 @@
1
1
  /**
2
2
  * Mastra workspace factory for Databricks Apps.
3
3
  *
4
- * Builds a {@link Workspace} with Databricks Sandbox command execution, an
5
- * isolated `/tmp` scratch mount, and a {@link CompositeFilesystem} over the
6
- * named skill folders
7
- * resolved for that request. A skill folder maps a name to a location plus its
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 { ConfigurationError, createWorkspaceClient, getExecutionContext } from "@databricks/appkit";
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 { fs as sharedFS, posixPath, type FileSystemCache } from "@dbx-tools/shared-fs";
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 WorkspaceMountContext {
85
+ export interface DatabricksWorkspaceContext {
76
86
  requestContext?: RequestContext;
77
87
  }
78
88
 
79
89
  /**
80
- * A skill-folder field given either directly or as a per-request resolver.
81
- * A resolver returning `undefined` skips the folder for that request.
90
+ * One Databricks path mounted into a Mastra workspace.
82
91
  */
83
- export type SkillFolderValue<T> =
84
- T | ((context: WorkspaceMountContext) => T | undefined | Promise<T | undefined>);
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
- /** Mount map plus optional Mastra skill scan roots for one resolver. */
128
- export interface WorkspaceMountContribution {
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
- /** Contributes filesystem mounts (and optional skill paths) for one request. */
135
- export type WorkspaceMountResolver = (
136
- context: WorkspaceMountContext,
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 createWorkspace}. Databricks is the default;
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
- /** Options for {@link createWorkspace}. */
156
- export interface CreateWorkspaceOptions {
157
- /** Workspace id; derived from `name` or `"workspace"` when omitted. */
158
- id?: string;
159
- /** Display name; derived from `id` when omitted. */
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 DEFAULT_SKILL_FOLDERS}. Defaults to `true`; `false`
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
- assistantSkills?: boolean;
199
+ assistantPaths?: boolean;
166
200
  /**
167
- * Named skill folders merged over {@link DEFAULT_SKILL_FOLDERS}: a matching
168
- * name overrides that default, `false` disables it, and any other name adds
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
- skillFolders?: Record<string, SkillFolderOptions | false>;
172
- /** Extra per-request mount resolvers (run after the skill-folder mounts). */
173
- mounts?: WorkspaceMountResolver[];
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
- /** Forwarded to Mastra when skill discovery is enabled. */
177
- checkSkillFileMtime?: boolean;
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
- * The skill folders every workspace starts with.
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 DEFAULT_SKILL_FOLDERS: Readonly<Record<DefaultSkillFolderName, SkillFolderOptions>> = {
211
- "organization-skills": {
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
- "personal-skills": {
221
- description: "Your Databricks workspace home directory for personal files and skills.",
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 skill folders only
254
+ * @example Default Databricks paths
238
255
  * ```ts
239
- * createWorkspace()
256
+ * databricksWorkspace()
240
257
  * ```
241
258
  *
242
- * @example Override a default, drop another, and add a location of your own
259
+ * @example Use only explicit paths
243
260
  * ```ts
244
- * createWorkspace({
245
- * skillFolders: {
246
- * "organization-skills": {
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 Skill folders plus a custom mount resolver
267
+ * @example Build a standard Mastra workspace yourself
258
268
  * ```ts
259
- * createWorkspace({
260
- * mounts: [
261
- * async ({ requestContext }) => ({
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 createWorkspace(options: CreateWorkspaceOptions = {}): Workspace {
275
+ export function databricksWorkspaceConfig(
276
+ options: DatabricksWorkspaceOptions = {},
277
+ ): WorkspaceConfig {
270
278
  const { id, name } = resolveWorkspaceIdentity(options);
271
- const skillFolders = resolveSkillFolders(options);
272
- const folderNames = Object.keys(skillFolders);
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
- filesCache !== undefined &&
277
- (options.mounts?.length ?? 0) === 0 &&
278
- Object.values(skillFolders).every(
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
- skillFolders,
285
- [...(options.mounts ?? []), ...localSkillMountResolvers(extraSkillPaths)],
286
- filesCache,
291
+ paths,
292
+ options.mounts,
293
+ localSkillMountResolvers(extraSkillPaths),
294
+ mountFilesCache,
295
+ configuredCache,
287
296
  );
288
297
  const resolveContribution = contributionResolver(resolvers);
289
- const resolveFilesystem = (context: WorkspaceMountContext) =>
298
+ const resolveFilesystem = (context: DatabricksWorkspaceContext) =>
290
299
  resolveWorkspaceFilesystem(
291
300
  resolveContribution,
292
301
  context,
293
- retainFilesystemSource ? filesCache : undefined,
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 !== false;
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
- skillFolders: folderNames,
314
- customMountResolvers: options.mounts?.length ?? 0,
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 workspace = new Workspace({
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
- * Merge the configured skill folders over {@link DEFAULT_SKILL_FOLDERS}.
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 resolveSkillFolders(
351
- options: Pick<CreateWorkspaceOptions, "assistantSkills" | "skillFolders"> = {},
352
- ): Record<string, SkillFolderOptions> {
353
- const merged: Record<string, SkillFolderOptions> =
354
- options.assistantSkills === false ? {} : { ...DEFAULT_SKILL_FOLDERS };
355
- for (const [name, folder] of Object.entries(options.skillFolders ?? {})) {
356
- if (folder === false) {
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
- ...defaults,
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: WorkspaceMountContext): Promise<
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
- skillFolders: Record<string, SkillFolderOptions>,
536
- context: WorkspaceMountContext,
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
- if (!requestContext || !shouldMountSkillFolders(requestContext)) {
544
- logger.debug("skill-folders:skipped", {
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 [name, folder] of Object.entries(skillFolders)) {
558
- const resolved = await resolveSkillFolderFilesystem(name, folder, context, {
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 = folder.mount ?? resolved.root ?? `/${name}`;
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 (folder.readable !== false) {
566
- skillPaths.push(...(folder.skills ?? ["."]).map((path) => mountedSkillPath(mount, path)));
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
- name: string,
587
- folder: SkillFolderOptions,
588
- context: WorkspaceMountContext,
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 (folder.path === undefined || !cache.client) {
641
+ if (!cache.client) {
610
642
  logger.debug("skill-folder:skipped", {
611
- name,
612
- reason: folder.path === undefined ? "no-location" : "missing-obo-client",
643
+ path: folder.path,
644
+ reason: "missing-obo-client",
613
645
  });
614
646
  return undefined;
615
647
  }
616
- const root = stringUtils.trimToNull(await resolveSkillFolderValue(folder.path, context));
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
- folder.readable === false ? [] : (folder.skills ?? ["."]),
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
- /** Read a {@link SkillFolderValue}, calling it when it is a per-request resolver. */
635
- function resolveSkillFolderValue<T>(
636
- value: SkillFolderValue<T>,
637
- context: WorkspaceMountContext,
638
- ): T | undefined | Promise<T | undefined> {
639
- return typeof value === "function"
640
- ? (value as (context: WorkspaceMountContext) => T | undefined | Promise<T | undefined>)(context)
641
- : value;
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 CreateWorkspaceOptions}.
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: CreateWorkspaceOptions): {
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
- skillFolders: Record<string, SkillFolderOptions>,
666
- mounts: WorkspaceMountResolver[] | undefined,
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[] = [scratchMountResolver()];
670
- const folderCount = Object.keys(skillFolders).length;
671
- if (folderCount > 0) {
672
- resolvers.push((context) => resolveSkillFolderMounts(skillFolders, context, filesCache));
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
- skillFolderCount: folderCount,
679
- customResolverCount: mounts?.length ?? 0,
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
- /** Contribute one isolated local scratch filesystem at `/tmp`. */
686
- function scratchMountResolver(): WorkspaceMountResolver {
687
- return () => ({
688
- mounts: { [SCRATCH_MOUNT]: scratchFilesystem() },
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 the independent `/tmp` mount and
728
- * any other accessible roots remain usable.
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
- skillPaths: readonly string[] = [],
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 source = fileSystemCache
758
- ? sharedFS.cache(fs, fileSystemCache, { readFilePaths: skillPaths })
759
- : fs;
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: WorkspaceMountContext,
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} when any mount resolved; otherwise a
815
- * fresh {@link scratchFilesystem} so Mastra always has a writable local root.
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: WorkspaceMountContext) => Promise<WorkspaceMountContribution>,
819
- context: WorkspaceMountContext,
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:scratch", {
1042
+ logger.debug("filesystem:empty", {
826
1043
  hasRequestContext: Boolean(context.requestContext),
827
1044
  });
828
- return scratchFilesystem();
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 organization roots retain precedence over personal roots.
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: WorkspaceMountContext) => Promise<WorkspaceMountContribution> {
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) => {