@dbx-tools/appkit-mastra 0.9.117 → 0.9.119

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,7 +5,7 @@
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 read-only backing directories plus
8
+ * `SKILL.md` tree at app boot and returns read-only filesystem backings plus
9
9
  * their stable virtual workspace mounts.
10
10
  *
11
11
  * Resolution per source, in order:
@@ -26,25 +26,16 @@
26
26
  * source (or the top-level call) sets `failOnError: false`, in which case it is
27
27
  * logged and skipped so one bad source never takes the app down.
28
28
  *
29
- * Every provisioned tree carries a {@link METADATA_FILE} recording when each
30
- * source was last downloaded, and a source is only re-downloaded once that
31
- * record is older than {@link DEFAULT_REFRESH_TTL_MS} (seven days). This runs at app
32
- * BOOT, so without it a container that restarts a dozen times an hour re-pulls
33
- * the whole AI Tools set a dozen times for content that changes rarely. The
34
- * record travels WITH the tree rather than in process memory, so the reuse
35
- * survives a restart - which is the only way it helps a Databricks App at all.
36
- * Pass `refreshTtlMs: 0` to download on every boot.
29
+ * AI Tools uses the shared `@dbx-tools/cache` project cache with a one-day TTL,
30
+ * then hydrates one process-memory filesystem mounted at
31
+ * {@link AITOOLS_SKILLS_MOUNT} for every default workspace. Other sources keep
32
+ * a {@link METADATA_FILE} beside their provisioned tree and default to a
33
+ * seven-day refresh window. Pass `refreshTtlMs: 0` to download on every boot.
37
34
  *
38
- * The default destination is the Databricks workspace Assistant skills tree
39
- * (`/Workspace/.assistant/skills`), so provisioned skills persist across restarts and are
40
- * discovered by the built-in Assistant-skills mount. Pass `userEmail` (or an
41
- * explicit `databricksBasePath`) to target `/Workspace/Users/<email>/.assistant/skills`
42
- * instead. When no Databricks client is resolvable at startup - or the resolved
43
- * identity cannot WRITE the destination, which is the normal case for a
44
- * Databricks App service principal against the admin-owned shared tree (see
45
- * {@link openWritableWorkspace}) - the tree is written under
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.
35
+ * For non-AI-Tools sources, the default destination is the Databricks workspace
36
+ * Assistant skills tree (`/Workspace/.assistant/skills`). Pass `userEmail` or
37
+ * `databricksBasePath` to select another destination. When it is unavailable,
38
+ * the tree is exposed through a stable read-only local mount instead.
48
39
  *
49
40
  * @module
50
41
  */
@@ -55,6 +46,7 @@ import { dirname, join, posix } from "node:path";
55
46
  import type { WorkspaceClient } from "@databricks/appkit";
56
47
  import { appkit } from "@dbx-tools/appkit";
57
48
  import type { WorkspaceClientLike } from "@dbx-tools/appkit";
49
+ import { flushCache, getOrSet, projectCache } from "@dbx-tools/cache/cache";
58
50
  import { exec } from "@dbx-tools/core";
59
51
  import { DatabricksFileSystem, workspaceClient } from "@dbx-tools/databricks";
60
52
  import { localFS, type LocalFileSystem } from "@dbx-tools/fs";
@@ -70,7 +62,7 @@ import {
70
62
  stringUtils,
71
63
  } from "@dbx-tools/shared-core";
72
64
  import type { OneOrMany } from "@dbx-tools/shared-core";
73
- import type { FileSystem } from "@dbx-tools/shared-fs";
65
+ import { MemoryFileSystem, type FileSystem } from "@dbx-tools/shared-fs";
74
66
 
75
67
  import { ORGANIZATION_SKILLS_PATH, personalSkillsPath } from "./skill-paths.ts";
76
68
 
@@ -116,9 +108,17 @@ const METADATA_FILE = ".metadata.json";
116
108
  /** Shape marker for {@link METADATA_FILE}; an unknown version is ignored, not read. */
117
109
  const METADATA_VERSION = 1;
118
110
 
119
- /** How long a provisioned source is reused before being downloaded again. */
111
+ /** How long a general provisioned source is reused before being downloaded again. */
120
112
  const DEFAULT_REFRESH_TTL_MS = 7 * 24 * 60 * 60 * 1000;
121
113
 
114
+ /** How long the shared Databricks AI Tools snapshot is reused. */
115
+ export const AITOOLS_REFRESH_TTL_MS = 24 * 60 * 60 * 1000;
116
+
117
+ const aiToolsCache = projectCache("mastra-aitools", {
118
+ ttl: AITOOLS_REFRESH_TTL_MS,
119
+ maxEntries: 32,
120
+ });
121
+
122
122
  /* ------------------------------ AI Tools ------------------------------ */
123
123
 
124
124
  /**
@@ -203,14 +203,25 @@ export type RemoteSkillsOption =
203
203
  /** A source entry with its `source` flattened to a plain string. */
204
204
  type NormalizedSource = Omit<RemoteSkillSourceOptions, "source"> & { source: string };
205
205
 
206
- /** Read-only local backing storage exposed at a stable virtual workspace path. */
207
- export interface LocalSkillMount {
206
+ /** Read-only local directory exposed at a stable virtual workspace path. */
207
+ export interface LocalSkillDirectoryMount {
208
208
  /** Physical local directory containing the provisioned `SKILL.md` tree. */
209
209
  root: string;
210
210
  /** Virtual path agents see inside the composed Mastra workspace. */
211
211
  mountPath: string;
212
212
  }
213
213
 
214
+ /** Read-only process-local filesystem exposed at a stable virtual workspace path. */
215
+ export interface MemorySkillMount {
216
+ /** Process-shared filesystem containing the provisioned `SKILL.md` tree. */
217
+ filesystem: FileSystem;
218
+ /** Virtual path agents see inside the composed Mastra workspace. */
219
+ mountPath: string;
220
+ }
221
+
222
+ /** Local skill backing accepted by every auto-created workspace. */
223
+ export type LocalSkillMount = LocalSkillDirectoryMount | MemorySkillMount;
224
+
214
225
  /** Top-level remote-skills provisioning options. */
215
226
  export interface ProvisionRemoteSkillsOptions {
216
227
  /** Sources to materialize at startup. */
@@ -233,8 +244,8 @@ export interface ProvisionRemoteSkillsOptions {
233
244
  maxDownloadBytes?: number;
234
245
  /**
235
246
  * How long a provisioned tree is reused before its source is downloaded
236
- * again, in milliseconds. Defaults to seven days; `0` re-downloads on every
237
- * boot. A per-source `refreshTtlMs` wins over this.
247
+ * again, in milliseconds. AI Tools defaults to one day and other sources to
248
+ * seven days; `0` re-downloads on every boot. A per-source `refreshTtlMs` wins.
238
249
  */
239
250
  refreshTtlMs?: number;
240
251
  }
@@ -242,9 +253,10 @@ export interface ProvisionRemoteSkillsOptions {
242
253
  /** What {@link provisionRemoteSkills} resolved. */
243
254
  export interface ProvisionedRemoteSkills {
244
255
  /**
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.
256
+ * Extra read-only local or process-memory skill mounts to hand Mastra. AI
257
+ * Tools always uses process memory; other sources use this fallback when
258
+ * they could not be written to Databricks. Physical paths remain hidden
259
+ * behind stable virtual workspace paths.
248
260
  */
249
261
  localSkillMounts: LocalSkillMount[];
250
262
  /** Absolute Databricks destination each source was written to, if any. */
@@ -257,8 +269,8 @@ export interface ProvisionedRemoteSkills {
257
269
  }
258
270
 
259
271
  /**
260
- * What one source's last download left behind, as recorded in
261
- * {@link METADATA_FILE}.
272
+ * Metadata for one resolved source snapshot. General remote sources store it
273
+ * in {@link METADATA_FILE}; AI Tools stores it with the cache-package value.
262
274
  */
263
275
  export interface RemoteSkillCacheEntry {
264
276
  /** The source string this entry was downloaded from, for readability. */
@@ -281,6 +293,20 @@ export interface RemoteSkillsMetadata {
281
293
  sources: Record<string, RemoteSkillCacheEntry>;
282
294
  }
283
295
 
296
+ interface CachedSkillFile {
297
+ path: string;
298
+ content: string;
299
+ }
300
+
301
+ interface CachedAiToolsTree {
302
+ files: CachedSkillFile[];
303
+ record: RemoteSkillCacheEntry;
304
+ }
305
+
306
+ interface ResolvedAiToolsTree extends CachedAiToolsTree {
307
+ cached: boolean;
308
+ }
309
+
284
310
  /* ------------------------------- helpers ------------------------------- */
285
311
 
286
312
  /** Normalize the `remoteSkills` option into a flat options bag. */
@@ -381,12 +407,16 @@ function remoteSourceIdentifier(source: string): string {
381
407
  return `${url.hostname}-${path}`;
382
408
  }
383
409
 
384
- /** The effective reuse window: per-source, then top-level, then seven days. */
410
+ /** The effective reuse window: per-source, top-level, then the source default. */
385
411
  function resolveRefreshTtl(
386
412
  sourceOptions: NormalizedSource,
387
413
  options: ProvisionRemoteSkillsOptions,
388
414
  ): number {
389
- return sourceOptions.refreshTtlMs ?? options.refreshTtlMs ?? DEFAULT_REFRESH_TTL_MS;
415
+ return (
416
+ sourceOptions.refreshTtlMs ??
417
+ options.refreshTtlMs ??
418
+ (isAiToolsSource(sourceOptions.source) ? AITOOLS_REFRESH_TTL_MS : DEFAULT_REFRESH_TTL_MS)
419
+ );
390
420
  }
391
421
 
392
422
  /**
@@ -465,12 +495,17 @@ export async function provisionRemoteSkills(
465
495
  if (!options) return empty;
466
496
 
467
497
  const failDefault = options.failOnError !== false;
498
+ const sources = (Array.isArray(options.sources) ? options.sources : [options.sources]).map(
499
+ toSourceOptions,
500
+ );
468
501
  const client = options.client ?? appkit.tryGetExecutionContext()?.client;
469
502
  // Probed rather than assumed: an identity that cannot write the workspace
470
503
  // tree falls back to the local one instead of failing startup. The probe also
471
504
  // creates the root, which the first cache READ needs - `copySkillDirs` would
472
505
  // otherwise be the first thing to create it, which is too late.
473
- const requestedBasePath = resolveDatabricksBasePath(options, client);
506
+ const requestedBasePath = sources.some((source) => !isAiToolsSource(source.source))
507
+ ? resolveDatabricksBasePath(options, client)
508
+ : undefined;
474
509
  const destination = requestedBasePath
475
510
  ? await openWritableWorkspace(requestedBasePath, client as WorkspaceClient)
476
511
  : undefined;
@@ -481,15 +516,30 @@ export async function provisionRemoteSkills(
481
516
  let staging: LocalFileSystem | undefined;
482
517
 
483
518
  try {
484
- const sources = Array.isArray(options.sources) ? options.sources : [options.sources];
485
- for (const entry of sources) {
486
- const sourceOptions = toSourceOptions(entry);
519
+ for (const sourceOptions of sources) {
487
520
  const failOnError = sourceOptions.failOnError ?? failDefault;
488
521
  try {
489
522
  // Where this source's bookkeeping lives: the shared Databricks tree, or
490
523
  // the source's own stable local dir.
491
524
  const key = cacheKey(sourceOptions);
492
525
  const mountPath = localSkillMountPath(sourceOptions, key);
526
+ if (isAiToolsSource(sourceOptions.source)) {
527
+ staging ??= await initializedScratch("mastra-remote-skills");
528
+ const tree = await cachedAiToolsTree(sourceOptions, staging.root, options, key);
529
+ localSkillMounts.push({
530
+ filesystem: await memorySkillTree(tree.files, `aitools-${key}`),
531
+ mountPath,
532
+ });
533
+ skillNames.push(...tree.record.skills);
534
+ logger.info("remote skill ready", {
535
+ source: sourceOptions.source,
536
+ destination: mountPath,
537
+ downloadedAt: tree.record.downloadedAt,
538
+ skills: tree.record.skills,
539
+ cached: tree.cached,
540
+ });
541
+ continue;
542
+ }
493
543
  const cacheFS = destination ?? localSkillsFS(key);
494
544
  const metadata = await readMetadata(cacheFS);
495
545
  const cached = metadata?.sources[key];
@@ -563,17 +613,87 @@ export async function provisionRemoteSkills(
563
613
  return { localSkillMounts, databricksBasePath, skillNames };
564
614
  }
565
615
 
616
+ async function cachedAiToolsTree(
617
+ sourceOptions: NormalizedSource,
618
+ stagingRoot: string,
619
+ options: ProvisionRemoteSkillsOptions,
620
+ key: string,
621
+ ): Promise<ResolvedAiToolsTree> {
622
+ const ttl = resolveRefreshTtl(sourceOptions, options);
623
+ let loaded = false;
624
+ const load = async (): Promise<CachedAiToolsTree> => {
625
+ loaded = true;
626
+ const stagedDir = await stageAiTools(
627
+ sourceOptions,
628
+ join(stagingRoot, `aitools-${hash.id()}`),
629
+ options,
630
+ );
631
+ const staged = await collectSkillDirs(stagedDir);
632
+ if (staged.length === 0) {
633
+ throw new Error(`no SKILL.md found for source "${sourceOptions.source}"`);
634
+ }
635
+ const policy = cachePolicy(sourceOptions);
636
+ return {
637
+ files: await serializeSkillDirs(staged),
638
+ record: {
639
+ source: sourceOptions.source,
640
+ downloadedAt: new Date().toISOString(),
641
+ skills: staged.map((dir) => dir.name),
642
+ ...(policy ? { policy } : {}),
643
+ },
644
+ };
645
+ };
646
+ const tree =
647
+ ttl <= 0
648
+ ? await load()
649
+ : await getOrSet(aiToolsCache, key, load, {
650
+ ttl,
651
+ throwErrors: true,
652
+ });
653
+ if (!tree) throw new Error(`AI Tools cache returned no tree for ${key}`);
654
+ await flushCache(aiToolsCache);
655
+ return { ...tree, cached: !loaded };
656
+ }
657
+
658
+ async function serializeSkillDirs(dirs: readonly StagedSkillDir[]): Promise<CachedSkillFile[]> {
659
+ const files: CachedSkillFile[] = [];
660
+ for (const dir of dirs) {
661
+ for (const relative of find.findFiles("**/*", { cwd: dir.absolutePath, nodir: true })) {
662
+ files.push({
663
+ path: posix.join(dir.name, relative.split(/[\\/]/).join("/")),
664
+ content: (await readFile(join(dir.absolutePath, relative))).toString("base64"),
665
+ });
666
+ }
667
+ }
668
+ return files;
669
+ }
670
+
671
+ async function memorySkillTree(
672
+ files: readonly CachedSkillFile[],
673
+ id: string,
674
+ ): Promise<MemoryFileSystem> {
675
+ const filesystem = new MemoryFileSystem({ id, root: "/" });
676
+ await filesystem.init();
677
+ for (const file of files) {
678
+ await filesystem.writeFile(file.path, Buffer.from(file.content, "base64"), {
679
+ overwrite: true,
680
+ });
681
+ }
682
+ return filesystem;
683
+ }
684
+
566
685
  /** Fail before workspace composition when two sources claim one virtual mount. */
567
686
  function assertUniqueLocalSkillMounts(mounts: readonly LocalSkillMount[]): void {
568
687
  const rootsByMount = new Map<string, string>();
569
688
  for (const mount of mounts) {
689
+ const identity = "root" in mount ? mount.root : mount.filesystem.id;
570
690
  const previousRoot = rootsByMount.get(mount.mountPath);
571
- if (previousRoot && previousRoot !== mount.root) {
691
+ if (previousRoot && previousRoot !== identity) {
572
692
  throw new Error(
573
693
  `multiple remote skill sources resolve to virtual mount "${mount.mountPath}"; give each source a unique name`,
574
694
  );
575
695
  }
576
- rootsByMount.set(mount.mountPath, mount.root);
696
+ rootsByMount.set(mount.mountPath, identity);
577
697
  }
578
698
  }
579
699
 
package/src/server.ts CHANGED
@@ -409,6 +409,10 @@ const AGENT_METADATA = /^\/agents(\/[^/]+)?$/;
409
409
  const AGENT_SUSPENDED_RUNS = /^\/agents\/[^/]+\/suspended-runs$/i;
410
410
  /** Official AI SDK UI stream route, including `resumeData` continuations. */
411
411
  const AGENT_CHAT = /^\/chat\/[^/]+$/i;
412
+ /** Durable run observation for one agent-owned chat stream. */
413
+ const AGENT_CHAT_RUN = /^\/chat\/[^/]+\/runs\/[^/]+$/i;
414
+ /** Explicit cancellation for one durable chat run. */
415
+ const AGENT_CHAT_RUN_ABORT = /^\/chat\/[^/]+\/runs\/[^/]+\/abort$/i;
412
416
  /** Native resource-scoped memory reads used by history and the thread list. */
413
417
  const MEMORY_READ = /^\/memory\/threads(?:\/[^/]+(?:\/messages)?)?$/i;
414
418
  /** Native thread rename/delete routes. */
@@ -438,6 +442,8 @@ export function isMastraRequestAllowed(
438
442
  if (opts.mcpEnabled && (p === "/mcp" || p.startsWith("/mcp/"))) return true;
439
443
  const m = method.toUpperCase();
440
444
  if (m === "POST" && AGENT_CHAT.test(p)) return true;
445
+ if (m === "GET" && AGENT_CHAT_RUN.test(p)) return true;
446
+ if (m === "POST" && AGENT_CHAT_RUN_ABORT.test(p)) return true;
441
447
  if (m === "POST" && AGENT_INFERENCE.test(p)) return true;
442
448
  if (m === "GET" && (AGENT_METADATA.test(p) || AGENT_SUSPENDED_RUNS.test(p))) return true;
443
449
  if (m === "GET" && MEMORY_READ.test(p)) return true;
package/src/workspaces.ts CHANGED
@@ -15,7 +15,6 @@
15
15
  import { createHash } from "node:crypto";
16
16
  import { tmpdir } from "node:os";
17
17
  import { join } from "node:path";
18
- import { ConfigurationError, createWorkspaceClient } from "@databricks/appkit";
19
18
  import type { WorkspaceClient } from "@databricks/appkit";
20
19
  import { appkit, pluginRegistry } from "@dbx-tools/appkit";
21
20
  import {
@@ -25,7 +24,7 @@ import {
25
24
  } from "@dbx-tools/appkit/files-cache";
26
25
  import { DatabricksFileSystem, workspaceClient } from "@dbx-tools/databricks";
27
26
  import { LocalFileSystem } from "@dbx-tools/fs";
28
- import { match, type PathMatchInput } from "@dbx-tools/path";
27
+ import { find, match, type PathMatchInput } from "@dbx-tools/path";
29
28
  import { errorUtils, log, object, stringUtils, token } from "@dbx-tools/shared-core";
30
29
  import {
31
30
  fs as sharedFS,
@@ -61,7 +60,6 @@ import {
61
60
  } from "./filesystems.ts";
62
61
  import { MontySandbox } from "./monty-sandbox.ts";
63
62
  import type { LocalSkillMount } from "./remote-skills.ts";
64
- import { DatabricksSandbox, type DatabricksWorkspaceSandboxOptions } from "./sandbox.ts";
65
63
  import { ORGANIZATION_ASSISTANT_PATH, personalWorkspacePath } from "./skill-paths.ts";
66
64
 
67
65
  /* ------------------------------ constants ------------------------------ */
@@ -79,6 +77,7 @@ const CACHEABLE_FILESYSTEM_OPERATIONS = [
79
77
  const logger = log.logger("mastra/workspaces");
80
78
  let workspaceSourceSequence = 0;
81
79
  const databricksWorkspaceOptions = new WeakMap<Workspace, DatabricksWorkspaceOptions>();
80
+ const sharedSkillFilesystems = new Map<string, Promise<FileSystem>>();
82
81
 
83
82
  /* -------------------------------- types -------------------------------- */
84
83
 
@@ -174,16 +173,10 @@ type WorkspaceMountResolver = (
174
173
 
175
174
  /**
176
175
  * Sandbox selection for {@link databricksWorkspace}. Monty is the default;
177
- * command execution can be disabled or replaced with Databricks Sandbox or any
178
- * Mastra provider/resolver.
176
+ * command execution can be disabled or replaced with any Mastra provider/resolver.
179
177
  */
180
178
  export type WorkspaceSandboxSelection =
181
- | false
182
- | "databricks"
183
- | "monty"
184
- | DatabricksWorkspaceSandboxOptions
185
- | WorkspaceSandbox
186
- | WorkspaceSandboxResolver;
179
+ false | true | "monty" | WorkspaceSandbox | WorkspaceSandboxResolver;
187
180
 
188
181
  /** Native Mastra workspace options plus Databricks path shortcuts. */
189
182
  export interface DatabricksWorkspaceOptions extends Omit<
@@ -208,9 +201,8 @@ export interface DatabricksWorkspaceOptions extends Omit<
208
201
  /** AppKit plugin context used to discover optional sibling capabilities. */
209
202
  pluginContext?: pluginRegistry.PluginContextLike;
210
203
  /**
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.
204
+ * Command sandbox. Defaults to Monty. Pass `false` to disable it, or an
205
+ * explicit Mastra sandbox/provider resolver for a remote transport.
214
206
  */
215
207
  sandbox?: WorkspaceSandboxSelection;
216
208
  /** Add user-scoped `/tmp` only when no configured Databricks path mounts writable. */
@@ -418,49 +410,9 @@ function resolveWorkspaceSandbox(
418
410
  ): WorkspaceSandbox | WorkspaceSandboxResolver | undefined {
419
411
  const configured = selection ?? "monty";
420
412
  if (configured === false) return undefined;
421
- if (configured === "monty") return new MontySandbox();
413
+ if (configured === true || configured === "monty") return new MontySandbox();
422
414
  if (typeof configured === "function" || isSandbox(configured)) return configured;
423
-
424
- const options: DatabricksWorkspaceSandboxOptions =
425
- configured === "databricks" ? { provider: "databricks" } : configured;
426
- return ({ requestContext }) => {
427
- const user = requestContext.get(MASTRA_USER_KEY) as User | undefined;
428
- if (!user) {
429
- throw ConfigurationError.resourceNotFound(
430
- "Databricks sandbox user context",
431
- "Invoke command tools from an agent turn served by the Mastra plugin.",
432
- );
433
- }
434
- const configuredId =
435
- typeof options.sandboxId === "function"
436
- ? options.sandboxId({ requestContext })
437
- : options.sandboxId;
438
- const sandboxId = configuredId ?? defaultSandboxId(workspaceId, user.id);
439
- const client =
440
- typeof options.client === "function"
441
- ? options.client({ requestContext })
442
- : (options.client ?? createWorkspaceClient());
443
- const {
444
- provider: _provider,
445
- sandboxId: _sandboxId,
446
- client: _client,
447
- ...sandboxOptions
448
- } = options;
449
- return new DatabricksSandbox({
450
- ...sandboxOptions,
451
- client,
452
- sandboxId,
453
- displayName: options.displayName ?? `Mastra ${workspaceName}`,
454
- });
455
- };
456
- }
457
-
458
- function defaultSandboxId(workspaceId: string, userId: string): string {
459
- const digest = createHash("sha256")
460
- .update(object.toStableKey({ workspaceId, userId }))
461
- .digest("hex")
462
- .slice(0, 32);
463
- return `mastra-${digest}`;
415
+ throw new TypeError(`Unsupported workspace sandbox for ${workspaceId} (${workspaceName})`);
464
416
  }
465
417
 
466
418
  function isSandbox(value: unknown): value is WorkspaceSandbox {
@@ -471,10 +423,9 @@ function isSandbox(value: unknown): value is WorkspaceSandbox {
471
423
 
472
424
  function sandboxName(selection: WorkspaceSandboxSelection | undefined): string {
473
425
  if (selection === undefined || selection === false) return "disabled";
474
- if (selection === "monty") return "monty";
475
- if (selection === "databricks") return "databricks";
426
+ if (selection === true || selection === "monty") return "monty";
476
427
  if (typeof selection === "function") return "resolver";
477
- return isSandbox(selection) ? selection.provider : (selection.provider ?? "databricks");
428
+ return selection.provider;
478
429
  }
479
430
 
480
431
  /**
@@ -844,25 +795,58 @@ function userTempFilesystem(
844
795
  function localSkillMountResolvers(mounts: readonly LocalSkillMount[]): WorkspaceMountResolver[] {
845
796
  const unique = new Map<string, LocalSkillMount>();
846
797
  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 });
798
+ const identity = "root" in entry ? entry.root.trim() : entry.filesystem.id;
799
+ if (!identity) continue;
800
+ unique.set(`${entry.mountPath}\0${identity}`, entry);
850
801
  }
851
- return [...unique.values()].map(({ root, mountPath }) => {
852
- const mount = posixPath.normalizeRoot(mountPath);
853
- const source = new LocalFileSystem({
854
- root,
855
- readOnly: true,
856
- createRoot: false,
857
- });
858
- const filesystem = filesystems(source, { readOnly: true });
859
- return () => ({
860
- mounts: { [mount]: filesystem },
861
- skillPaths: [mount],
862
- });
802
+ return [...unique.values()].map((entry) => {
803
+ const mount = posixPath.normalizeRoot(entry.mountPath);
804
+ const filesystem = inMemorySkillFilesystem(entry);
805
+ return async () => {
806
+ const source = await filesystem;
807
+ return {
808
+ mounts: { [mount]: filesystems(source, { readOnly: true }) },
809
+ skillPaths: [mount],
810
+ };
811
+ };
863
812
  });
864
813
  }
865
814
 
815
+ function inMemorySkillFilesystem(mount: LocalSkillMount): Promise<FileSystem> {
816
+ const key = "root" in mount ? `root:${mount.root}` : `filesystem:${mount.filesystem.id}`;
817
+ const existing = sharedSkillFilesystems.get(key);
818
+ if (existing) return existing;
819
+ const loaded = loadInMemorySkillFilesystem(mount).catch((error) => {
820
+ if (sharedSkillFilesystems.get(key) === loaded) sharedSkillFilesystems.delete(key);
821
+ throw error;
822
+ });
823
+ sharedSkillFilesystems.set(key, loaded);
824
+ return loaded;
825
+ }
826
+
827
+ async function loadInMemorySkillFilesystem(mount: LocalSkillMount): Promise<FileSystem> {
828
+ if ("filesystem" in mount) {
829
+ await mount.filesystem.init();
830
+ return mount.filesystem;
831
+ }
832
+ const source = new LocalFileSystem({
833
+ root: mount.root,
834
+ readOnly: true,
835
+ createRoot: false,
836
+ });
837
+ await source.init();
838
+ const memory = new MemoryFileSystem({
839
+ id: `skills-${createHash("sha256").update(mount.root).digest("hex").slice(0, 16)}`,
840
+ root: "/",
841
+ });
842
+ await memory.init();
843
+ for (const relative of find.findFiles("**/*", { cwd: mount.root, nodir: true })) {
844
+ await memory.writeFile(relative, await source.readFile(relative), { overwrite: true });
845
+ }
846
+ await source.close();
847
+ return memory;
848
+ }
849
+
866
850
  /**
867
851
  * Gate skill-folder mounts on the request's token.
868
852
  *
@@ -1,108 +0,0 @@
1
- /**
2
- * Databricks Sandbox adapter for Mastra workspaces.
3
- *
4
- * Implements Mastra's foreground command surface over the Databricks Sandbox
5
- * REST API. Sandboxes are created lazily, retain their home directory across
6
- * inactivity stops, and execute untrusted shell commands outside the App
7
- * container.
8
- *
9
- * @module
10
- */
11
- import type { WorkspaceClient } from "@databricks/appkit";
12
- import type { RequestContext } from "@mastra/core/request-context";
13
- import type { CommandResult, ExecuteCommandOptions, ProviderStatus, SandboxInfo, WorkspaceSandbox } from "@mastra/core/workspace";
14
- import { type MontySandboxOptions } from "./monty-sandbox.ts";
15
- /** Options for one Databricks-backed Mastra sandbox. */
16
- export interface DatabricksSandboxOptions {
17
- /** AppKit workspace client used for every sandbox API call. */
18
- client: WorkspaceClient;
19
- /** Resource id, with or without the `sandboxes/` prefix. */
20
- sandboxId: string;
21
- /** Human-readable Databricks label. */
22
- displayName?: string;
23
- /** Server-side idle timeout in protobuf duration form. Defaults to `900s`. */
24
- inactivityTimeout?: string;
25
- /** Maximum time to wait for the sandbox to become runnable. */
26
- startupTimeoutMs?: number;
27
- /** Default command timeout. Per-call `options.timeout` wins. */
28
- commandTimeoutMs?: number;
29
- /**
30
- * Provider used only when Databricks Sandbox is definitively unavailable.
31
- * Disabled by default. Pass `"monty"`, Monty options, or another Mastra
32
- * sandbox to enable a fallback explicitly.
33
- */
34
- fallback?: false | "monty" | MontySandboxOptions | WorkspaceSandbox;
35
- }
36
- /** Databricks options accepted by the higher-level workspace factory. */
37
- export interface DatabricksWorkspaceSandboxOptions extends Omit<DatabricksSandboxOptions, "client" | "sandboxId"> {
38
- /** Provider discriminator. Defaults to `databricks`. */
39
- provider?: "databricks";
40
- /**
41
- * Credential source. Defaults to a fresh AppKit client using the normal
42
- * environment/profile chain, which is the app service principal in a
43
- * Databricks App.
44
- */
45
- client?: WorkspaceClient | ((context: {
46
- requestContext: RequestContext;
47
- }) => WorkspaceClient);
48
- /**
49
- * Fixed id or per-request resolver. Omit to derive a stable, opaque id from
50
- * the workspace and attributed user.
51
- */
52
- sandboxId?: string | ((context: {
53
- requestContext: RequestContext;
54
- }) => string);
55
- }
56
- /** Mastra sandbox backed by Databricks serverless Sandbox compute. */
57
- export declare class DatabricksSandbox implements WorkspaceSandbox {
58
- readonly id: string;
59
- readonly name: string;
60
- status: ProviderStatus;
61
- error?: string;
62
- private readonly client;
63
- private readonly displayName;
64
- private readonly inactivityTimeout;
65
- private readonly startupTimeoutMs;
66
- private readonly commandTimeoutMs;
67
- private readonly fallbackConfig;
68
- private readonly createdAt;
69
- private remoteCreatedAt?;
70
- private lastUsedAt?;
71
- private activeFallback?;
72
- private fallbackPromise?;
73
- private startPromise?;
74
- constructor(options: DatabricksSandboxOptions);
75
- get provider(): string;
76
- /** Ensure the remote sandbox exists and has reached its running state. */
77
- start(): Promise<void>;
78
- private startWithSignal;
79
- private performStart;
80
- /** Databricks home persistence is not a cloneable Mastra checkpoint. */
81
- snapshot(): Promise<void>;
82
- get supportsCheckpoints(): boolean;
83
- /** Stop compute while preserving the sandbox home directory. */
84
- stop(): Promise<void>;
85
- /** Permanently delete the sandbox and its persisted home directory. */
86
- destroy(): Promise<void>;
87
- /** Execute one foreground shell command in the remote sandbox. */
88
- executeCommand(command: string, args?: string[], options?: ExecuteCommandOptions): Promise<CommandResult>;
89
- /** Activate the configured fallback once across concurrent command failures. */
90
- private activateFallback;
91
- /** Start and publish the configured fallback after rechecking active state. */
92
- private startFallback;
93
- /** Execute through the active fallback or return an unsupported result. */
94
- private executeFallback;
95
- /** Whether the adapter currently knows the remote sandbox to be running. */
96
- isReady(): Promise<boolean>;
97
- /** Report Mastra-facing sandbox metadata. */
98
- getInfo(): Promise<SandboxInfo>;
99
- /** Explain the Databricks execution boundary to the agent. */
100
- getInstructions(): string;
101
- private get resourcePath();
102
- private getOrCreate;
103
- private get;
104
- private mutate;
105
- private waitForState;
106
- private rememberCreatedAt;
107
- private request;
108
- }