@dbx-tools/appkit-mastra 0.9.108 → 0.9.111

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.
@@ -5,8 +5,8 @@
5
5
  * {@link AITOOLS_SOURCE} constant, a GitHub `owner/repo`, any git / GitLab URL,
6
6
  * or a direct download URL - and optional per-source policy.
7
7
  * {@link provisionRemoteSkills} materializes every source into a local
8
- * `SKILL.md` tree at app boot and returns the directories to hand Mastra as
9
- * extra skill scan paths.
8
+ * `SKILL.md` tree at app boot and returns read-only backing directories plus
9
+ * their stable virtual workspace mounts.
10
10
  *
11
11
  * Resolution per source, in order:
12
12
  *
@@ -43,8 +43,8 @@
43
43
  * identity cannot WRITE the destination, which is the normal case for a
44
44
  * Databricks App service principal against the admin-owned shared tree (see
45
45
  * {@link openWritableWorkspace}) - the tree is written under
46
- * {@link localFS.tmpFS} and returned as an extra local skill path for the
47
- * current process. Storage location degrades; the skills themselves do not.
46
+ * {@link localFS.tmpFS} and exposed through a stable read-only virtual mount
47
+ * for the current process. Storage location degrades; the skills themselves do not.
48
48
  *
49
49
  * @module
50
50
  */
@@ -100,6 +100,12 @@ const DEFAULT_DOWNLOAD_TIMEOUT_MS = 60_000;
100
100
  /** Stable temp directory holding one rebuilt skill tree per remote source. */
101
101
  const LOCAL_SKILLS_DIR = "mastra-local-skills";
102
102
 
103
+ /** Virtual workspace path reserved for locally provisioned Databricks AI Tools. */
104
+ export const AITOOLS_SKILLS_MOUNT = "/.databricks/skills";
105
+
106
+ /** Virtual workspace root for every other locally provisioned remote source. */
107
+ export const REMOTE_SKILLS_MOUNT = "/.skills";
108
+
103
109
  /**
104
110
  * Bookkeeping file written at the root of every provisioned skill tree, naming
105
111
  * when each source was last downloaded. A dotfile so the skill scanners, which
@@ -156,6 +162,8 @@ interface AiToolsManifest {
156
162
  export interface RemoteSkillSourceOptions {
157
163
  /** `"aitools"`, a GitHub shorthand, a git / GitLab URL, or a download URL. */
158
164
  source: net.UrlLike | AiToolsSource;
165
+ /** Stable virtual mount name under `/.skills`; generated from the source when omitted. */
166
+ name?: string;
159
167
  /** Install only these skill names from the source. */
160
168
  skills?: string | string[];
161
169
  /** Include experimental skills. `"aitools"` only. */
@@ -195,6 +203,14 @@ export type RemoteSkillsOption =
195
203
  /** A source entry with its `source` flattened to a plain string. */
196
204
  type NormalizedSource = Omit<RemoteSkillSourceOptions, "source"> & { source: string };
197
205
 
206
+ /** Read-only local backing storage exposed at a stable virtual workspace path. */
207
+ export interface LocalSkillMount {
208
+ /** Physical local directory containing the provisioned `SKILL.md` tree. */
209
+ root: string;
210
+ /** Virtual path agents see inside the composed Mastra workspace. */
211
+ mountPath: string;
212
+ }
213
+
198
214
  /** Top-level remote-skills provisioning options. */
199
215
  export interface ProvisionRemoteSkillsOptions {
200
216
  /** Sources to materialize at startup. */
@@ -226,11 +242,11 @@ export interface ProvisionRemoteSkillsOptions {
226
242
  /** What {@link provisionRemoteSkills} resolved. */
227
243
  export interface ProvisionedRemoteSkills {
228
244
  /**
229
- * Extra LOCAL skill scan paths to hand Mastra (a temp dir per source that
230
- * couldn't be written to Databricks). Empty when everything landed in the
231
- * Databricks Assistant tree, which the built-in mount already scans.
245
+ * Extra read-only LOCAL skill mounts to hand Mastra when a source couldn't
246
+ * be written to Databricks. Physical temp/cache paths remain hidden behind
247
+ * stable virtual workspace paths.
232
248
  */
233
- localSkillPaths: string[];
249
+ localSkillMounts: LocalSkillMount[];
234
250
  /** Absolute Databricks destination each source was written to, if any. */
235
251
  databricksBasePath?: string;
236
252
  /**
@@ -342,6 +358,29 @@ function cacheKey(sourceOptions: NormalizedSource): string {
342
358
  );
343
359
  }
344
360
 
361
+ /** Stable virtual workspace path for one locally provisioned source. */
362
+ function localSkillMountPath(sourceOptions: NormalizedSource, key: string): string {
363
+ if (isAiToolsSource(sourceOptions.source)) return AITOOLS_SKILLS_MOUNT;
364
+ const configuredName = stringUtils.trimToNull(sourceOptions.name);
365
+ const identifier = configuredName
366
+ ? stringUtils.toSlug(configuredName)
367
+ : stringUtils.toSlug(remoteSourceIdentifier(sourceOptions.source)) || "source";
368
+ if (!identifier) {
369
+ throw new Error(
370
+ `remote skill source name must contain letters or numbers: "${configuredName}"`,
371
+ );
372
+ }
373
+ return posix.join(REMOTE_SKILLS_MOUNT, configuredName ? identifier : `${identifier}-${key}`);
374
+ }
375
+
376
+ /** Human-readable source portion used by generated virtual mount identifiers. */
377
+ function remoteSourceIdentifier(source: string): string {
378
+ const url = net.urlBuilder(source);
379
+ if (!url) return source;
380
+ const path = url.pathname.replace(/\.(git|md|zip|tar|tgz|gz)$/i, "");
381
+ return `${url.hostname}-${path}`;
382
+ }
383
+
345
384
  /** The effective reuse window: per-source, then top-level, then seven days. */
346
385
  function resolveRefreshTtl(
347
386
  sourceOptions: NormalizedSource,
@@ -416,13 +455,13 @@ function localSkillsFS(key: string): LocalFileSystem {
416
455
  *
417
456
  * Writes each resolved `SKILL.md` tree to the Databricks user Assistant skills
418
457
  * folder when a writable workspace is available, else under
419
- * {@link localFS.tmpFS} returned in {@link ProvisionedRemoteSkills.localSkillPaths}.
458
+ * {@link localFS.tmpFS} returned in {@link ProvisionedRemoteSkills.localSkillMounts}.
420
459
  */
421
460
  export async function provisionRemoteSkills(
422
461
  option: RemoteSkillsOption | undefined,
423
462
  ): Promise<ProvisionedRemoteSkills> {
424
463
  const options = normalizeRemoteSkillsOption(option);
425
- const empty: ProvisionedRemoteSkills = { localSkillPaths: [], skillNames: [] };
464
+ const empty: ProvisionedRemoteSkills = { localSkillMounts: [], skillNames: [] };
426
465
  if (!options) return empty;
427
466
 
428
467
  const failDefault = options.failOnError !== false;
@@ -437,7 +476,7 @@ export async function provisionRemoteSkills(
437
476
  : undefined;
438
477
  const databricksBasePath = destination ? requestedBasePath : undefined;
439
478
 
440
- const localSkillPaths: string[] = [];
479
+ const localSkillMounts: LocalSkillMount[] = [];
441
480
  const skillNames: string[] = [];
442
481
  let staging: LocalFileSystem | undefined;
443
482
 
@@ -450,15 +489,16 @@ export async function provisionRemoteSkills(
450
489
  // Where this source's bookkeeping lives: the shared Databricks tree, or
451
490
  // the source's own stable local dir.
452
491
  const key = cacheKey(sourceOptions);
492
+ const mountPath = localSkillMountPath(sourceOptions, key);
453
493
  const cacheFS = destination ?? localSkillsFS(key);
454
494
  const metadata = await readMetadata(cacheFS);
455
495
  const cached = metadata?.sources[key];
456
496
  if (cached && isFresh(cached, resolveRefreshTtl(sourceOptions, options))) {
457
497
  skillNames.push(...cached.skills);
458
- if (!destination) localSkillPaths.push(cacheFS.root);
498
+ if (!destination) localSkillMounts.push({ root: cacheFS.root, mountPath });
459
499
  logger.info("remote skill ready", {
460
500
  source: sourceOptions.source,
461
- destination: databricksBasePath ?? cacheFS.root,
501
+ destination: databricksBasePath ?? mountPath,
462
502
  downloadedAt: cached.downloadedAt,
463
503
  skills: cached.skills,
464
504
  cached: true,
@@ -489,12 +529,15 @@ export async function provisionRemoteSkills(
489
529
  // mark a failed provision as fresh and suppress the retry for a day.
490
530
  await writeMetadata(destination, key, record);
491
531
  } else {
492
- localSkillPaths.push(await persistLocally(key, staged, record));
532
+ localSkillMounts.push({
533
+ root: await persistLocally(key, staged, record),
534
+ mountPath,
535
+ });
493
536
  }
494
537
  skillNames.push(...record.skills);
495
538
  logger.info("remote skill installed", {
496
539
  source: sourceOptions.source,
497
- destination: databricksBasePath ?? localSkillPaths.at(-1),
540
+ destination: databricksBasePath ?? localSkillMounts.at(-1)?.mountPath,
498
541
  skills: record.skills,
499
542
  });
500
543
  } catch (err) {
@@ -516,7 +559,22 @@ export async function provisionRemoteSkills(
516
559
  }
517
560
  }
518
561
 
519
- return { localSkillPaths, databricksBasePath, skillNames };
562
+ assertUniqueLocalSkillMounts(localSkillMounts);
563
+ return { localSkillMounts, databricksBasePath, skillNames };
564
+ }
565
+
566
+ /** Fail before workspace composition when two sources claim one virtual mount. */
567
+ function assertUniqueLocalSkillMounts(mounts: readonly LocalSkillMount[]): void {
568
+ const rootsByMount = new Map<string, string>();
569
+ for (const mount of mounts) {
570
+ const previousRoot = rootsByMount.get(mount.mountPath);
571
+ if (previousRoot && previousRoot !== mount.root) {
572
+ throw new Error(
573
+ `multiple remote skill sources resolve to virtual mount "${mount.mountPath}"; give each source a unique name`,
574
+ );
575
+ }
576
+ rootsByMount.set(mount.mountPath, mount.root);
577
+ }
520
578
  }
521
579
 
522
580
  /** Remove directories dropped by one source unless another source still owns them. */
package/src/sandbox.ts CHANGED
@@ -68,7 +68,8 @@ export interface DatabricksSandboxOptions {
68
68
  commandTimeoutMs?: number;
69
69
  /**
70
70
  * Provider used only when Databricks Sandbox is definitively unavailable.
71
- * Defaults to the Node Pydantic Monty runtime; `false` fails instead.
71
+ * Disabled by default. Pass `"monty"`, Monty options, or another Mastra
72
+ * sandbox to enable a fallback explicitly.
72
73
  */
73
74
  fallback?: false | "monty" | MontySandboxOptions | WorkspaceSandbox;
74
75
  }
@@ -123,7 +124,7 @@ export class DatabricksSandbox implements WorkspaceSandbox {
123
124
  this.inactivityTimeout = options.inactivityTimeout ?? DEFAULT_INACTIVITY_TIMEOUT;
124
125
  this.startupTimeoutMs = options.startupTimeoutMs ?? DEFAULT_STARTUP_TIMEOUT_MS;
125
126
  this.commandTimeoutMs = options.commandTimeoutMs ?? DEFAULT_COMMAND_TIMEOUT_MS;
126
- this.fallbackConfig = options.fallback ?? "monty";
127
+ this.fallbackConfig = options.fallback ?? false;
127
128
  }
128
129
 
129
130
  get provider(): string {
package/src/workspaces.ts CHANGED
@@ -60,6 +60,7 @@ import {
60
60
  type MastraFileSystemAdapterOptions,
61
61
  } from "./filesystems.ts";
62
62
  import { MontySandbox } from "./monty-sandbox.ts";
63
+ import type { LocalSkillMount } from "./remote-skills.ts";
63
64
  import { DatabricksSandbox, type DatabricksWorkspaceSandboxOptions } from "./sandbox.ts";
64
65
  import { ORGANIZATION_ASSISTANT_PATH, personalWorkspacePath } from "./skill-paths.ts";
65
66
 
@@ -173,8 +174,8 @@ type WorkspaceMountResolver = (
173
174
 
174
175
  /**
175
176
  * Sandbox selection for {@link databricksWorkspace}. Monty is the default;
176
- * `false` disables command execution, and a Mastra provider or resolver is an
177
- * explicit replacement.
177
+ * command execution can be disabled or replaced with Databricks Sandbox or any
178
+ * Mastra provider/resolver.
178
179
  */
179
180
  export type WorkspaceSandboxSelection =
180
181
  | false
@@ -207,19 +208,21 @@ export interface DatabricksWorkspaceOptions extends Omit<
207
208
  /** AppKit plugin context used to discover optional sibling capabilities. */
208
209
  pluginContext?: pluginRegistry.PluginContextLike;
209
210
  /**
210
- * Command sandbox. Defaults to Monty. Pass `"databricks"` or an options
211
- * object for Databricks Sandbox, `false` to disable command execution, or an
212
- * explicit Mastra sandbox/provider resolver.
211
+ * Command sandbox. Defaults to Monty. Pass `false` to disable it,
212
+ * `"databricks"` or Databricks Sandbox options to select the remote provider,
213
+ * or an explicit Mastra sandbox/provider resolver.
213
214
  */
214
215
  sandbox?: WorkspaceSandboxSelection;
216
+ /** Add user-scoped `/tmp` only when no configured Databricks path mounts writable. */
217
+ fallbackToTmp?: boolean;
215
218
  /**
216
- * Extra LOCAL skill paths mounted read-only for every request's skill discovery.
217
- * Used by the plugin to surface remote skills provisioned to a local temp
218
- * dir at startup (see `remote-skills.ts`). Databricks-hosted remote skills
219
- * need no entry here - they land in the Assistant tree the built-in mount
220
- * already scans.
219
+ * Extra LOCAL skill trees mounted read-only for every request's skill
220
+ * discovery. Used by the plugin to expose remote skills from local backing
221
+ * storage at stable virtual paths (see `remote-skills.ts`). Databricks-hosted
222
+ * remote skills need no entry here because the built-in Assistant mount
223
+ * already scans them.
221
224
  */
222
- extraSkillPaths?: string[];
225
+ extraSkillMounts?: LocalSkillMount[];
223
226
  }
224
227
 
225
228
  /* ------------------------------- defaults ------------------------------- */
@@ -273,7 +276,7 @@ export function databricksWorkspaceConfig(
273
276
  const filesystemSourceKey = `${id}:${++workspaceSourceSequence}`;
274
277
  const paths = resolveDatabricksWorkspacePaths(options);
275
278
  const configuredPaths = paths.length;
276
- const extraSkillPaths = options.extraSkillPaths ?? [];
279
+ const extraSkillMounts = options.extraSkillMounts ?? [];
277
280
  const filesCache = pluginRegistry.instance(options.pluginContext, filesCachePlugin)?.exports();
278
281
  const configuredCache = options.cache;
279
282
  const mountFilesCache = configuredCache === false ? undefined : filesCache;
@@ -284,9 +287,10 @@ export function databricksWorkspaceConfig(
284
287
  const resolvers = buildMountResolvers(
285
288
  paths,
286
289
  options.mounts,
287
- localSkillMountResolvers(extraSkillPaths),
290
+ localSkillMountResolvers(extraSkillMounts),
288
291
  mountFilesCache,
289
292
  configuredCache,
293
+ options.fallbackToTmp === true,
290
294
  );
291
295
  const resolveContribution = contributionResolver(resolvers);
292
296
  const resolveFilesystem = (context: DatabricksWorkspaceContext) =>
@@ -296,7 +300,7 @@ export function databricksWorkspaceConfig(
296
300
  retainFilesystemSource ? mountFilesCache : undefined,
297
301
  filesystemSourceKey,
298
302
  );
299
- const hasSkillPaths = paths.some(pathMayProvideSkills) || extraSkillPaths.length > 0;
303
+ const hasSkillPaths = paths.some(pathMayProvideSkills) || extraSkillMounts.length > 0;
300
304
  const skills: SkillsResolver =
301
305
  options.skills ??
302
306
  (async ({ requestContext }) => {
@@ -321,7 +325,7 @@ export function databricksWorkspaceConfig(
321
325
  customSkillsResolver: options.skills !== undefined,
322
326
  checkSkillFileMtime,
323
327
  bm25,
324
- extraSkillPaths: extraSkillPaths.length,
328
+ extraSkillMounts: extraSkillMounts.length,
325
329
  sandbox: sandbox ? sandboxName(options.sandbox) : "disabled",
326
330
  });
327
331
 
@@ -333,7 +337,8 @@ export function databricksWorkspaceConfig(
333
337
  cache: _cache,
334
338
  pluginContext: _pluginContext,
335
339
  sandbox: _sandbox,
336
- extraSkillPaths: _extraSkillPaths,
340
+ fallbackToTmp: _fallbackToTmp,
341
+ extraSkillMounts: _extraSkillMounts,
337
342
  ...workspaceOptions
338
343
  } = options;
339
344
  return {
@@ -465,9 +470,9 @@ function isSandbox(value: unknown): value is WorkspaceSandbox {
465
470
  }
466
471
 
467
472
  function sandboxName(selection: WorkspaceSandboxSelection | undefined): string {
468
- if (selection === undefined || selection === "monty") return "monty";
473
+ if (selection === undefined || selection === false) return "disabled";
474
+ if (selection === "monty") return "monty";
469
475
  if (selection === "databricks") return "databricks";
470
- if (selection === false) return "disabled";
471
476
  if (typeof selection === "function") return "resolver";
472
477
  return isSandbox(selection) ? selection.provider : (selection.provider ?? "databricks");
473
478
  }
@@ -536,9 +541,11 @@ async function resolveWorkspacePathMounts(
536
541
  context: DatabricksWorkspaceContext,
537
542
  filesCache: FilesCacheExports | undefined,
538
543
  cacheConfig: DatabricksWorkspaceCache | undefined,
544
+ fallbackToTmp: boolean,
539
545
  ): Promise<WorkspaceMountContribution> {
540
546
  const mounts: Record<string, WorkspaceFilesystem> = {};
541
547
  const skillPaths: string[] = [];
548
+ let hasWritableMount = false;
542
549
  const requestContext = context.requestContext;
543
550
 
544
551
  const canMountDatabricks = Boolean(requestContext && shouldMountSkillFolders(requestContext));
@@ -553,6 +560,7 @@ async function resolveWorkspacePathMounts(
553
560
  if (posixPath.isWithinRoot(SCRATCH_MOUNT, configured.path)) {
554
561
  const mount = configured.mount ?? configured.path;
555
562
  mounts[mount] = userTempFilesystem(context, configured.path);
563
+ hasWritableMount = true;
556
564
  skillPaths.push(...(configured.skills ?? []).map((path) => mountedSkillPath(mount, path)));
557
565
  continue;
558
566
  }
@@ -574,9 +582,14 @@ async function resolveWorkspacePathMounts(
574
582
  continue;
575
583
  }
576
584
  mounts[mount] = resolved.filesystem;
585
+ hasWritableMount ||= resolved.writable;
577
586
  skillPaths.push(...(configured.skills ?? []).map((path) => mountedSkillPath(mount, path)));
578
587
  }
579
588
 
589
+ if (fallbackToTmp && !hasWritableMount) {
590
+ mounts[SCRATCH_MOUNT] = userTempFilesystem(context, SCRATCH_MOUNT);
591
+ }
592
+
580
593
  logger.debug("workspace-paths:mounted", {
581
594
  mountKeys: Object.keys(mounts),
582
595
  skillPaths,
@@ -632,6 +645,7 @@ async function resolveSkillFolderFilesystem(
632
645
  | {
633
646
  filesystem: WorkspaceFilesystem;
634
647
  root?: string;
648
+ writable: boolean;
635
649
  }
636
650
  | undefined
637
651
  > {
@@ -661,7 +675,7 @@ async function resolveSkillFolderFilesystem(
661
675
  ...(folder.displayName ? { displayName: folder.displayName } : {}),
662
676
  },
663
677
  );
664
- return resolved ? { ...resolved, root } : undefined;
678
+ return resolved ? { ...resolved, root, writable } : undefined;
665
679
  }
666
680
 
667
681
  async function resolveCacheConfig(
@@ -782,12 +796,13 @@ function buildMountResolvers(
782
796
  localMounts: WorkspaceMountResolver[],
783
797
  filesCache: FilesCacheExports | undefined,
784
798
  cacheConfig: DatabricksWorkspaceCache | undefined,
799
+ fallbackToTmp: boolean,
785
800
  ): WorkspaceMountResolver[] {
786
801
  const resolvers: WorkspaceMountResolver[] = [];
787
802
  const pathCount = paths.length;
788
- if (pathCount > 0) {
803
+ if (pathCount > 0 || fallbackToTmp) {
789
804
  resolvers.push((context) =>
790
- resolveWorkspacePathMounts(paths, context, filesCache, cacheConfig),
805
+ resolveWorkspacePathMounts(paths, context, filesCache, cacheConfig, fallbackToTmp),
791
806
  );
792
807
  }
793
808
  if (mounts && Object.keys(mounts).length > 0) resolvers.push(() => ({ mounts }));
@@ -825,10 +840,16 @@ function userTempFilesystem(
825
840
  });
826
841
  }
827
842
 
828
- /** Mount startup-provisioned local skill paths into Mastra's workspace filesystem. */
829
- function localSkillMountResolvers(paths: readonly string[]): WorkspaceMountResolver[] {
830
- return [...new Set(paths.map((path) => path.trim()).filter(Boolean))].map((root) => {
831
- const mount = posixPath.normalizeRoot(root);
843
+ /** Mount startup-provisioned local skill trees into Mastra's workspace filesystem. */
844
+ function localSkillMountResolvers(mounts: readonly LocalSkillMount[]): WorkspaceMountResolver[] {
845
+ const unique = new Map<string, LocalSkillMount>();
846
+ for (const entry of mounts) {
847
+ const root = entry.root.trim();
848
+ if (!root) continue;
849
+ unique.set(`${entry.mountPath}\0${root}`, { root, mountPath: entry.mountPath });
850
+ }
851
+ return [...unique.values()].map(({ root, mountPath }) => {
852
+ const mount = posixPath.normalizeRoot(mountPath);
832
853
  const source = new LocalFileSystem({
833
854
  root,
834
855
  readOnly: true,