@dbx-tools/appkit-mastra 0.6.15 → 0.6.36
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +87 -11
- package/index.ts +6 -3
- package/lib/index.d.ts +6 -3
- package/lib/index.js +5 -2
- package/lib/src/chart.d.ts +39 -9
- package/lib/src/chart.js +460 -77
- package/lib/src/genie.js +24 -24
- package/lib/src/plugin.js +29 -3
- package/lib/src/remote-skills.js +10 -10
- package/lib/src/skill-paths.d.ts +15 -0
- package/lib/src/skill-paths.js +18 -0
- package/lib/src/workspaces.d.ts +85 -15
- package/lib/src/workspaces.js +128 -64
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +13 -13
- package/src/chart.ts +519 -83
- package/src/genie.ts +24 -23
- package/src/plugin.ts +28 -2
- package/src/remote-skills.ts +10 -11
- package/src/skill-paths.ts +19 -0
- package/src/workspaces.ts +194 -84
package/src/genie.ts
CHANGED
|
@@ -48,14 +48,14 @@ import { plugin } from "@dbx-tools/appkit";
|
|
|
48
48
|
import { chat, space as genieSpace } from "@dbx-tools/genie";
|
|
49
49
|
import { error, log, string } from "@dbx-tools/shared-core";
|
|
50
50
|
import { genieModel, type GenieMessage } from "@dbx-tools/shared-genie";
|
|
51
|
-
import {
|
|
51
|
+
import type { MastraWriter, StartedEvent } from "@dbx-tools/shared-mastra";
|
|
52
52
|
import type { RequestContext } from "@mastra/core/request-context";
|
|
53
53
|
import { MASTRA_THREAD_ID_KEY } from "@mastra/core/request-context";
|
|
54
54
|
import { createTool } from "@mastra/core/tools";
|
|
55
55
|
import { z } from "zod";
|
|
56
56
|
|
|
57
57
|
import type { MastraTools } from "./agents.ts";
|
|
58
|
-
import { chartPlannerRequestSchema, prepareChart } from "./chart.ts";
|
|
58
|
+
import { chartPlannerRequestSchema, chartToolOutputSchema, prepareChart } from "./chart.ts";
|
|
59
59
|
import { MASTRA_USER_KEY, resolveUserKey } from "./config.ts";
|
|
60
60
|
import type { MastraPluginConfig, User } from "./config.ts";
|
|
61
61
|
import { fetchStatementData } from "./statement.ts";
|
|
@@ -687,9 +687,9 @@ function buildGetStatementTool() {
|
|
|
687
687
|
* {@link prepareChart} that resolves the dataset by fetching the
|
|
688
688
|
* Genie statement's rows on demand. The tool mints a `chartId`
|
|
689
689
|
* synchronously, caches an empty placeholder, and kicks off the
|
|
690
|
-
* planner in the background so the agent loop never blocks. The
|
|
691
|
-
* host UI resolves
|
|
692
|
-
*
|
|
690
|
+
* planner in the background so the agent loop never blocks. The result carries
|
|
691
|
+
* the complete marker to copy; the host UI resolves it by reading the cached
|
|
692
|
+
* {@link Chart} entry (1h TTL).
|
|
693
693
|
*
|
|
694
694
|
* Space-agnostic: a Genie `statement_id` is workspace-scoped, so
|
|
695
695
|
* one shared `prepare_chart` tool covers every wired Genie space.
|
|
@@ -709,20 +709,21 @@ function buildPrepareChartTool(opts: { config: MastraPluginConfig }) {
|
|
|
709
709
|
description: string.toDescription([
|
|
710
710
|
`
|
|
711
711
|
Queue a chart for the rows of a Genie statement. Mints a
|
|
712
|
-
|
|
712
|
+
\`chartId\` plus its complete \`marker\` synchronously and
|
|
713
|
+
kicks off a BACKGROUND
|
|
713
714
|
task that fetches the statement's rows, runs the
|
|
714
715
|
chart-planner to pick a chart type and Echarts spec, and
|
|
715
716
|
caches the result under the \`chartId\` for one hour. The
|
|
716
717
|
host UI fetches the cached chart on its own once it lands.
|
|
717
718
|
`,
|
|
718
719
|
`
|
|
719
|
-
To display the chart in your reply,
|
|
720
|
-
\`
|
|
721
|
-
want it to appear
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
720
|
+
To display the chart in your reply, copy the returned
|
|
721
|
+
\`marker\` VERBATIM onto its own line at the position you
|
|
722
|
+
want it to appear. Never construct, alter, or invent a
|
|
723
|
+
marker from \`chartId\` (and never use the \`statement_id\`
|
|
724
|
+
or any variation of it) - only the complete value returned
|
|
725
|
+
by this tool resolves to a real chart. The tool returns
|
|
726
|
+
immediately - do NOT wait or call it again to
|
|
726
727
|
"check progress"; the chart resolves asynchronously on the
|
|
727
728
|
host UI's side.
|
|
728
729
|
`,
|
|
@@ -734,7 +735,7 @@ function buildPrepareChartTool(opts: { config: MastraPluginConfig }) {
|
|
|
734
735
|
`,
|
|
735
736
|
]),
|
|
736
737
|
inputSchema: prepareChartRequestSchema,
|
|
737
|
-
outputSchema:
|
|
738
|
+
outputSchema: chartToolOutputSchema,
|
|
738
739
|
execute: async (request, ctxRaw) => {
|
|
739
740
|
const ctx = ctxRaw as ToolExecuteCtx;
|
|
740
741
|
const { client, requestContext } = requireClient(ctx, toolId);
|
|
@@ -835,20 +836,20 @@ export const GENIE_INSTRUCTIONS = string.toDescription([
|
|
|
835
836
|
`,
|
|
836
837
|
`
|
|
837
838
|
\`[chart:<chartId>]\` - render the rows as a chart. To
|
|
838
|
-
get
|
|
839
|
+
get one, call \`prepare_chart\` with the
|
|
839
840
|
statement's id (and an optional \`title\` / one-line
|
|
840
841
|
\`description\` of the insight to surface). The tool
|
|
841
|
-
returns the \`
|
|
842
|
-
chart spec in the background;
|
|
843
|
-
|
|
844
|
-
|
|
842
|
+
returns the complete \`marker\` synchronously and
|
|
843
|
+
prepares the chart spec in the background; copy that
|
|
844
|
+
marker VERBATIM onto its own line wherever the chart
|
|
845
|
+
should appear. Use a chart when the data has a
|
|
845
846
|
story a visual conveys better than a table (trends,
|
|
846
847
|
rankings, distributions, parts-of-a-whole).
|
|
847
848
|
|
|
848
|
-
NEVER invent or hand-build a
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
849
|
+
NEVER invent or hand-build a marker. A valid marker is
|
|
850
|
+
the complete opaque string a \`prepare_chart\` call
|
|
851
|
+
returned to you in THIS turn - nothing else. Its id is
|
|
852
|
+
NOT a \`statement_id\`, and it is NOT a
|
|
852
853
|
\`statement_id\` prefix with a label appended (e.g.
|
|
853
854
|
\`01f1...-region-fill\`). If you have not called
|
|
854
855
|
\`prepare_chart\` and received an id back, do not write
|
package/src/plugin.ts
CHANGED
|
@@ -103,10 +103,10 @@ import {
|
|
|
103
103
|
isMastraRequestAllowed,
|
|
104
104
|
MastraServer,
|
|
105
105
|
} from "./server.ts";
|
|
106
|
-
import { attachChatTurnTraceIo } from "./trace-io.ts";
|
|
107
106
|
import { resolveServingConfig } from "./serving.ts";
|
|
108
107
|
import { fetchStatementData, STATEMENT_ROW_CAP } from "./statement.ts";
|
|
109
108
|
import { threadsRoute } from "./threads.ts";
|
|
109
|
+
import { attachChatTurnTraceIo } from "./trace-io.ts";
|
|
110
110
|
import { invalidFields } from "./validation.ts";
|
|
111
111
|
|
|
112
112
|
const GENIE_MANIFEST = plugin.data(genie).plugin.manifest;
|
|
@@ -595,10 +595,24 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
595
595
|
const id = string.firstNonEmpty(req.params.id);
|
|
596
596
|
const resolve = embedResolvers[type];
|
|
597
597
|
if (!resolve) {
|
|
598
|
+
this.logger.warn("embed:unsupported", {
|
|
599
|
+
status: 404,
|
|
600
|
+
type,
|
|
601
|
+
id,
|
|
602
|
+
method: req.method,
|
|
603
|
+
path: req.path,
|
|
604
|
+
});
|
|
598
605
|
res.status(404).json({ error: `unsupported embed type: ${type}` });
|
|
599
606
|
return;
|
|
600
607
|
}
|
|
601
608
|
if (!id) {
|
|
609
|
+
this.logger.warn("embed:invalid", {
|
|
610
|
+
status: 400,
|
|
611
|
+
type,
|
|
612
|
+
method: req.method,
|
|
613
|
+
path: req.path,
|
|
614
|
+
error: "id is required",
|
|
615
|
+
});
|
|
602
616
|
res.status(400).json({ error: "id is required" });
|
|
603
617
|
return;
|
|
604
618
|
}
|
|
@@ -616,6 +630,18 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
616
630
|
return;
|
|
617
631
|
}
|
|
618
632
|
if (result.data === undefined) {
|
|
633
|
+
// A cache miss is security-deliberately ambiguous: unknown, expired,
|
|
634
|
+
// or owned by another identity all map to the same 404. Log the
|
|
635
|
+
// opaque id and those possible causes so a fabricated marker is
|
|
636
|
+
// diagnosable from the server without disclosing which cause applied.
|
|
637
|
+
this.logger.warn(`embed:${type}:not-found`, {
|
|
638
|
+
status: 404,
|
|
639
|
+
type,
|
|
640
|
+
id,
|
|
641
|
+
method: req.method,
|
|
642
|
+
path: req.path,
|
|
643
|
+
reason: "unknown, expired, or owned by another identity",
|
|
644
|
+
});
|
|
619
645
|
res.status(404).json({ error: `${type} not found` });
|
|
620
646
|
return;
|
|
621
647
|
}
|
|
@@ -1005,7 +1031,7 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
1005
1031
|
// every default-workspace agent via `extraSkillPaths`.
|
|
1006
1032
|
const provisioned = await provisionRemoteSkills(this.config.remoteSkills);
|
|
1007
1033
|
if (provisioned.skillNames.length > 0) {
|
|
1008
|
-
this.logger.info("remote skills
|
|
1034
|
+
this.logger.info("remote skills configured", {
|
|
1009
1035
|
skills: provisioned.skillNames,
|
|
1010
1036
|
databricksBasePath: provisioned.databricksBasePath,
|
|
1011
1037
|
localSkillPaths: provisioned.localSkillPaths.length,
|
package/src/remote-skills.ts
CHANGED
|
@@ -61,15 +61,9 @@ import { error, hash, json, log, net, object, string } from "@dbx-tools/shared-c
|
|
|
61
61
|
import type { OneOrMany } from "@dbx-tools/shared-core";
|
|
62
62
|
import type { FileSystem } from "@dbx-tools/shared-fs";
|
|
63
63
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
/** Shared Assistant skills tree in the Databricks workspace (default target). */
|
|
67
|
-
const ASSISTANT_SHARED_SKILLS_PATH = "/Workspace/.assistant/skills";
|
|
64
|
+
import { ASSISTANT_SHARED_SKILLS_PATH, userAssistantSkillsPath } from "./skill-paths.ts";
|
|
68
65
|
|
|
69
|
-
|
|
70
|
-
function userAssistantSkillsPath(userEmail: string): string {
|
|
71
|
-
return `/Users/${userEmail.trim()}/.assistant/skills`;
|
|
72
|
-
}
|
|
66
|
+
const logger = log.logger("mastra/remote-skills");
|
|
73
67
|
|
|
74
68
|
/**
|
|
75
69
|
* Agent id the `skills` CLI installs a bare `SKILL.md` tree under.
|
|
@@ -452,14 +446,19 @@ export async function provisionRemoteSkills(
|
|
|
452
446
|
if (cached && isFresh(cached, resolveRefreshTtl(sourceOptions, options))) {
|
|
453
447
|
skillNames.push(...cached.skills);
|
|
454
448
|
if (!destination) localSkillPaths.push(cacheFS.root);
|
|
455
|
-
logger.
|
|
449
|
+
logger.info("remote skill ready", {
|
|
456
450
|
source: sourceOptions.source,
|
|
451
|
+
destination: databricksBasePath ?? cacheFS.root,
|
|
457
452
|
downloadedAt: cached.downloadedAt,
|
|
458
453
|
skills: cached.skills,
|
|
454
|
+
cached: true,
|
|
459
455
|
});
|
|
460
456
|
continue;
|
|
461
457
|
}
|
|
462
458
|
|
|
459
|
+
logger.info("installing remote skill", {
|
|
460
|
+
source: sourceOptions.source,
|
|
461
|
+
});
|
|
463
462
|
staging ??= await initializedScratch("mastra-remote-skills");
|
|
464
463
|
const stagedDir = await stageSource(sourceOptions, staging.root, options);
|
|
465
464
|
const staged = await collectSkillDirs(stagedDir);
|
|
@@ -482,9 +481,9 @@ export async function provisionRemoteSkills(
|
|
|
482
481
|
localSkillPaths.push(await persistLocally(key, staged, record));
|
|
483
482
|
}
|
|
484
483
|
skillNames.push(...record.skills);
|
|
485
|
-
logger.
|
|
484
|
+
logger.info("remote skill installed", {
|
|
486
485
|
source: sourceOptions.source,
|
|
487
|
-
destination: databricksBasePath ??
|
|
486
|
+
destination: databricksBasePath ?? localSkillPaths.at(-1),
|
|
488
487
|
skills: record.skills,
|
|
489
488
|
});
|
|
490
489
|
} catch (err) {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where Databricks Assistant `SKILL.md` trees live.
|
|
3
|
+
*
|
|
4
|
+
* Two modules need these paths and must agree on them: `workspaces.ts` MOUNTS
|
|
5
|
+
* them per request (see `DEFAULT_SKILL_FOLDERS`), and `remote-skills.ts`
|
|
6
|
+
* WRITES provisioned skills into them at startup. They were previously spelled
|
|
7
|
+
* out in both, so a change to one silently provisioned skills into a tree the
|
|
8
|
+
* other never scanned.
|
|
9
|
+
*
|
|
10
|
+
* @module
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** Shared Assistant skills tree, readable by everyone in the workspace. */
|
|
14
|
+
export const ASSISTANT_SHARED_SKILLS_PATH = "/Workspace/.assistant/skills";
|
|
15
|
+
|
|
16
|
+
/** Assistant skills tree owned by one user (the "save this as a skill" target). */
|
|
17
|
+
export function userAssistantSkillsPath(userEmail: string): string {
|
|
18
|
+
return `/Users/${userEmail.trim()}/.assistant/skills`;
|
|
19
|
+
}
|
package/src/workspaces.ts
CHANGED
|
@@ -2,10 +2,14 @@
|
|
|
2
2
|
* Mastra workspace factory for Databricks Apps.
|
|
3
3
|
*
|
|
4
4
|
* Builds a per-request {@link Workspace} whose filesystem is a
|
|
5
|
-
* {@link CompositeFilesystem} over
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* {@link CompositeFilesystem} over the NAMED skill folders resolved for that
|
|
6
|
+
* request. A skill folder maps a name to a location plus its readable /
|
|
7
|
+
* writable policy: a Databricks path mounted through the OBO client on
|
|
8
|
+
* {@link MASTRA_USER_KEY}, or any {@link WorkspaceFilesystem} a consuming
|
|
9
|
+
* library already owns. {@link DEFAULT_SKILL_FOLDERS} supplies the Assistant
|
|
10
|
+
* trees, and `skillFolders` merges over it - same name overrides, `false`
|
|
11
|
+
* disables, a new name adds. Optional mount resolvers contribute further
|
|
12
|
+
* filesystems and skill scan roots on top.
|
|
9
13
|
*
|
|
10
14
|
* Databricks mounts use `@dbx-tools/databricks` {@link DatabricksFileSystem}
|
|
11
15
|
* wrapped by {@link filesystems}; missing roots fall back to
|
|
@@ -16,7 +20,7 @@
|
|
|
16
20
|
|
|
17
21
|
import type { WorkspaceClient } from "@databricks/sdk-experimental";
|
|
18
22
|
import { DatabricksFileSystem } from "@dbx-tools/databricks";
|
|
19
|
-
import { log, string, token } from "@dbx-tools/shared-core";
|
|
23
|
+
import { error, log, string, token } from "@dbx-tools/shared-core";
|
|
20
24
|
import type { RequestContext } from "@mastra/core/request-context";
|
|
21
25
|
import {
|
|
22
26
|
CompositeFilesystem,
|
|
@@ -28,18 +32,10 @@ import {
|
|
|
28
32
|
|
|
29
33
|
import { MASTRA_SCOPES_KEY, MASTRA_USER_EMAIL_KEY, MASTRA_USER_KEY, type User } from "./config.ts";
|
|
30
34
|
import { scratchFilesystem, filesystems } from "./filesystems.ts";
|
|
35
|
+
import { ASSISTANT_SHARED_SKILLS_PATH, userAssistantSkillsPath } from "./skill-paths.ts";
|
|
31
36
|
|
|
32
37
|
/* ------------------------------ constants ------------------------------ */
|
|
33
38
|
|
|
34
|
-
/** Shared Assistant skills tree in the workspace namespace. */
|
|
35
|
-
const ASSISTANT_SHARED_SKILLS_PATH = "/Workspace/.assistant/skills";
|
|
36
|
-
|
|
37
|
-
/** Composite mount for {@link ASSISTANT_SHARED_SKILLS_PATH}. */
|
|
38
|
-
const ASSISTANT_WORKSPACE_SKILLS_MOUNT = "/workspace_skills";
|
|
39
|
-
|
|
40
|
-
/** Composite mount for the caller's `/.assistant/skills` tree. */
|
|
41
|
-
const ASSISTANT_USER_SKILLS_MOUNT = "/workspace_user_skills";
|
|
42
|
-
|
|
43
39
|
/** OAuth scopes that gate Databricks workspace file mounts. */
|
|
44
40
|
const WORKSPACE_FILE_SCOPES = ["workspace", "workspace.workspace", "all-apis"] as const;
|
|
45
41
|
|
|
@@ -47,23 +43,60 @@ const logger = log.logger("mastra/workspaces");
|
|
|
47
43
|
|
|
48
44
|
/* -------------------------------- types -------------------------------- */
|
|
49
45
|
|
|
50
|
-
/** Per-request context for mount resolvers. */
|
|
51
|
-
interface WorkspaceMountContext {
|
|
46
|
+
/** Per-request context for mount and skill-folder resolvers. */
|
|
47
|
+
export interface WorkspaceMountContext {
|
|
52
48
|
requestContext?: RequestContext;
|
|
53
49
|
}
|
|
54
50
|
|
|
51
|
+
/**
|
|
52
|
+
* A skill-folder field given either directly or as a per-request resolver.
|
|
53
|
+
* A resolver returning `undefined` skips the folder for that request.
|
|
54
|
+
*/
|
|
55
|
+
export type SkillFolderValue<T> =
|
|
56
|
+
T | ((context: WorkspaceMountContext) => T | undefined | Promise<T | undefined>);
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* One named skill-folder location and its read / write policy.
|
|
60
|
+
*
|
|
61
|
+
* Give {@link path} for a Databricks workspace tree (mounted through the
|
|
62
|
+
* request's OBO client), or {@link filesystem} for a mount the consumer builds
|
|
63
|
+
* itself. {@link filesystem} wins when both are set.
|
|
64
|
+
*/
|
|
65
|
+
export interface SkillFolderOptions {
|
|
66
|
+
/** Absolute Databricks workspace path for this folder. */
|
|
67
|
+
path?: SkillFolderValue<string>;
|
|
68
|
+
/** Ready-made mount, for locations the OBO client cannot reach. */
|
|
69
|
+
filesystem?: SkillFolderValue<WorkspaceFilesystem>;
|
|
70
|
+
/**
|
|
71
|
+
* Scan this mount for `SKILL.md` files. Defaults to `true`; `false` mounts
|
|
72
|
+
* the location for file tools without adding it to skill discovery.
|
|
73
|
+
*/
|
|
74
|
+
readable?: boolean;
|
|
75
|
+
/**
|
|
76
|
+
* Allow writes to a {@link path} mount (and create the root when missing).
|
|
77
|
+
* Defaults to `false`. A supplied {@link filesystem} carries its own
|
|
78
|
+
* read-only flag instead.
|
|
79
|
+
*/
|
|
80
|
+
writable?: boolean;
|
|
81
|
+
/** Mount point in the composite namespace. Defaults to `/<name>`. */
|
|
82
|
+
mount?: string;
|
|
83
|
+
}
|
|
84
|
+
|
|
55
85
|
/** Mount map plus optional Mastra skill scan roots for one resolver. */
|
|
56
|
-
interface WorkspaceMountContribution {
|
|
86
|
+
export interface WorkspaceMountContribution {
|
|
57
87
|
mounts: Record<string, WorkspaceFilesystem>;
|
|
58
88
|
/** Paths within the composite namespace where `SKILL.md` files are scanned. */
|
|
59
89
|
skillPaths?: string[];
|
|
60
90
|
}
|
|
61
91
|
|
|
62
92
|
/** Contributes filesystem mounts (and optional skill paths) for one request. */
|
|
63
|
-
type WorkspaceMountResolver = (
|
|
93
|
+
export type WorkspaceMountResolver = (
|
|
64
94
|
context: WorkspaceMountContext,
|
|
65
95
|
) => WorkspaceMountContribution | Promise<WorkspaceMountContribution>;
|
|
66
96
|
|
|
97
|
+
/** Names carried by {@link DEFAULT_SKILL_FOLDERS}. */
|
|
98
|
+
export type DefaultSkillFolderName = "workspace-team" | "workspace-team-app";
|
|
99
|
+
|
|
67
100
|
/** Options for {@link createWorkspace}. */
|
|
68
101
|
export interface CreateWorkspaceOptions {
|
|
69
102
|
/** Workspace id; derived from `name` or `"workspace"` when omitted. */
|
|
@@ -71,11 +104,17 @@ export interface CreateWorkspaceOptions {
|
|
|
71
104
|
/** Display name; derived from `id` when omitted. */
|
|
72
105
|
name?: string;
|
|
73
106
|
/**
|
|
74
|
-
*
|
|
75
|
-
*
|
|
107
|
+
* Start from {@link DEFAULT_SKILL_FOLDERS}. Defaults to `true`; `false`
|
|
108
|
+
* starts from an empty map, leaving only the {@link skillFolders} given here.
|
|
76
109
|
*/
|
|
77
110
|
assistantSkills?: boolean;
|
|
78
|
-
/**
|
|
111
|
+
/**
|
|
112
|
+
* Named skill folders merged over {@link DEFAULT_SKILL_FOLDERS}: a matching
|
|
113
|
+
* name overrides that default, `false` disables it, and any other name adds
|
|
114
|
+
* a folder.
|
|
115
|
+
*/
|
|
116
|
+
skillFolders?: Record<string, SkillFolderOptions | false>;
|
|
117
|
+
/** Extra per-request mount resolvers (run after the skill-folder mounts). */
|
|
79
118
|
mounts?: WorkspaceMountResolver[];
|
|
80
119
|
/** Replace the auto-built dynamic skills resolver. */
|
|
81
120
|
skills?: SkillsResolver;
|
|
@@ -93,18 +132,56 @@ export interface CreateWorkspaceOptions {
|
|
|
93
132
|
extraSkillPaths?: string[];
|
|
94
133
|
}
|
|
95
134
|
|
|
135
|
+
/* ------------------------------- defaults ------------------------------- */
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* The skill folders every workspace starts with.
|
|
139
|
+
*
|
|
140
|
+
* - `workspace-team` - the shared workspace Assistant tree, read-only because
|
|
141
|
+
* writing it is a workspace-admin action.
|
|
142
|
+
* - `workspace-team-app` - the requesting user's own Assistant tree, writable
|
|
143
|
+
* so the app can save skills back to it. Skipped when the request carries no
|
|
144
|
+
* user email.
|
|
145
|
+
*/
|
|
146
|
+
export const DEFAULT_SKILL_FOLDERS: Readonly<Record<DefaultSkillFolderName, SkillFolderOptions>> = {
|
|
147
|
+
"workspace-team": {
|
|
148
|
+
path: ASSISTANT_SHARED_SKILLS_PATH,
|
|
149
|
+
readable: true,
|
|
150
|
+
writable: false,
|
|
151
|
+
},
|
|
152
|
+
"workspace-team-app": {
|
|
153
|
+
path: ({ requestContext }) => {
|
|
154
|
+
const email = resolveScopedEmail(requestContext);
|
|
155
|
+
return email ? userAssistantSkillsPath(email) : undefined;
|
|
156
|
+
},
|
|
157
|
+
readable: true,
|
|
158
|
+
writable: true,
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
|
|
96
162
|
/**
|
|
97
163
|
* Create a Mastra {@link Workspace} with per-request Databricks mounts.
|
|
98
164
|
*
|
|
99
|
-
* @example
|
|
165
|
+
* @example Default skill folders only
|
|
100
166
|
* ```ts
|
|
101
167
|
* createWorkspace()
|
|
102
168
|
* ```
|
|
103
169
|
*
|
|
104
|
-
* @example
|
|
170
|
+
* @example Override a default, drop another, and add a location of your own
|
|
171
|
+
* ```ts
|
|
172
|
+
* createWorkspace({
|
|
173
|
+
* skillFolders: {
|
|
174
|
+
* "workspace-team": { path: "/Workspace/Shared/team-skills" },
|
|
175
|
+
* "workspace-team-app": false,
|
|
176
|
+
* runbooks: { path: "/Workspace/Shared/runbooks/skills", writable: true },
|
|
177
|
+
* volume: { filesystem: myVolumeFilesystem },
|
|
178
|
+
* },
|
|
179
|
+
* })
|
|
180
|
+
* ```
|
|
181
|
+
*
|
|
182
|
+
* @example Skill folders plus a custom mount resolver
|
|
105
183
|
* ```ts
|
|
106
184
|
* createWorkspace({
|
|
107
|
-
* assistantSkills: true,
|
|
108
185
|
* mounts: [
|
|
109
186
|
* async ({ requestContext }) => ({
|
|
110
187
|
* mounts: { "/data": myFilesystem },
|
|
@@ -116,20 +193,22 @@ export interface CreateWorkspaceOptions {
|
|
|
116
193
|
*/
|
|
117
194
|
export function createWorkspace(options: CreateWorkspaceOptions = {}): Workspace {
|
|
118
195
|
const { id, name } = resolveWorkspaceIdentity(options);
|
|
119
|
-
const
|
|
196
|
+
const skillFolders = resolveSkillFolders(options);
|
|
197
|
+
const folderNames = Object.keys(skillFolders);
|
|
198
|
+
const resolvers = buildMountResolvers(skillFolders, options.mounts);
|
|
120
199
|
const extraSkillPaths = options.extraSkillPaths ?? [];
|
|
121
200
|
const skills =
|
|
122
201
|
options.skills ??
|
|
123
202
|
(resolvers.length > 0 || extraSkillPaths.length > 0
|
|
124
203
|
? buildWorkspaceSkillsResolver(resolvers, extraSkillPaths)
|
|
125
204
|
: undefined);
|
|
126
|
-
const checkSkillFileMtime = options.checkSkillFileMtime ??
|
|
205
|
+
const checkSkillFileMtime = options.checkSkillFileMtime ?? folderNames.length > 0;
|
|
127
206
|
const bm25 = options.bm25 !== false;
|
|
128
207
|
logger.debug("workspace:create", {
|
|
129
208
|
id,
|
|
130
209
|
name,
|
|
131
210
|
resolverCount: resolvers.length,
|
|
132
|
-
|
|
211
|
+
skillFolders: folderNames,
|
|
133
212
|
customMountResolvers: options.mounts?.length ?? 0,
|
|
134
213
|
customSkillsResolver: Boolean(options.skills),
|
|
135
214
|
checkSkillFileMtime,
|
|
@@ -151,16 +230,29 @@ export function createWorkspace(options: CreateWorkspaceOptions = {}): Workspace
|
|
|
151
230
|
});
|
|
152
231
|
}
|
|
153
232
|
|
|
154
|
-
/* ---------------------------- private helpers ---------------------------- */
|
|
155
|
-
|
|
156
233
|
/**
|
|
157
|
-
*
|
|
158
|
-
*
|
|
234
|
+
* Merge the configured skill folders over {@link DEFAULT_SKILL_FOLDERS}.
|
|
235
|
+
*
|
|
236
|
+
* `assistantSkills: false` drops the defaults, and a `false` value removes one
|
|
237
|
+
* entry by name.
|
|
159
238
|
*/
|
|
160
|
-
function
|
|
161
|
-
|
|
239
|
+
export function resolveSkillFolders(
|
|
240
|
+
options: Pick<CreateWorkspaceOptions, "assistantSkills" | "skillFolders"> = {},
|
|
241
|
+
): Record<string, SkillFolderOptions> {
|
|
242
|
+
const merged: Record<string, SkillFolderOptions> =
|
|
243
|
+
options.assistantSkills === false ? {} : { ...DEFAULT_SKILL_FOLDERS };
|
|
244
|
+
for (const [name, folder] of Object.entries(options.skillFolders ?? {})) {
|
|
245
|
+
if (folder === false) {
|
|
246
|
+
delete merged[name];
|
|
247
|
+
} else {
|
|
248
|
+
merged[name] = folder;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return merged;
|
|
162
252
|
}
|
|
163
253
|
|
|
254
|
+
/* ---------------------------- private helpers ---------------------------- */
|
|
255
|
+
|
|
164
256
|
/**
|
|
165
257
|
* Return whether the request token carries a scope that allows workspace
|
|
166
258
|
* file API access (`workspace` or `all-apis` on {@link MASTRA_SCOPES_KEY}).
|
|
@@ -173,61 +265,81 @@ function hasWorkspaceFileScope(requestContext: RequestContext | undefined): bool
|
|
|
173
265
|
}
|
|
174
266
|
|
|
175
267
|
/**
|
|
176
|
-
*
|
|
268
|
+
* Mount resolver for the named skill folders.
|
|
177
269
|
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
* set. Returns empty mounts when the OBO user or client is missing.
|
|
181
|
-
* Mastra owns filesystem initialization.
|
|
270
|
+
* Gates on workspace file scope (or development mode), then mounts every
|
|
271
|
+
* folder whose location resolves for this request.
|
|
182
272
|
*/
|
|
183
|
-
async function
|
|
273
|
+
async function resolveSkillFolderMounts(
|
|
274
|
+
skillFolders: Record<string, SkillFolderOptions>,
|
|
184
275
|
context: WorkspaceMountContext,
|
|
185
276
|
): Promise<WorkspaceMountContribution> {
|
|
186
277
|
const mounts: Record<string, WorkspaceFilesystem> = {};
|
|
278
|
+
const skillPaths: string[] = [];
|
|
187
279
|
const requestContext = context.requestContext;
|
|
188
280
|
|
|
189
|
-
if (!
|
|
190
|
-
logger.debug("
|
|
281
|
+
if (!requestContext || !shouldMountSkillFolders(requestContext)) {
|
|
282
|
+
logger.debug("skill-folders:skipped", {
|
|
191
283
|
reason: !requestContext ? "no-request-context" : "missing-workspace-scope",
|
|
192
284
|
nodeEnv: process.env.NODE_ENV,
|
|
193
|
-
scopes:
|
|
285
|
+
scopes: requestContext?.get(MASTRA_SCOPES_KEY),
|
|
194
286
|
});
|
|
195
|
-
return { mounts, skillPaths
|
|
287
|
+
return { mounts, skillPaths };
|
|
196
288
|
}
|
|
197
289
|
|
|
198
|
-
const user = requestContext
|
|
290
|
+
const user = requestContext.get(MASTRA_USER_KEY) as User | undefined;
|
|
199
291
|
const client = user?.executionContext.client as WorkspaceClient | undefined;
|
|
200
|
-
if (!client) {
|
|
201
|
-
logger.debug("assistant-skills:skipped", {
|
|
202
|
-
reason: "missing-obo-client",
|
|
203
|
-
userId: user?.id,
|
|
204
|
-
});
|
|
205
|
-
return { mounts, skillPaths: [] };
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
mounts[ASSISTANT_WORKSPACE_SKILLS_MOUNT] = await databricksFilesystem(
|
|
209
|
-
client,
|
|
210
|
-
ASSISTANT_SHARED_SKILLS_PATH,
|
|
211
|
-
);
|
|
212
292
|
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
);
|
|
293
|
+
for (const [name, folder] of Object.entries(skillFolders)) {
|
|
294
|
+
const filesystem = await resolveSkillFolderFilesystem(name, folder, context, client);
|
|
295
|
+
if (!filesystem) continue;
|
|
296
|
+
const mount = folder.mount ?? `/${name}`;
|
|
297
|
+
mounts[mount] = filesystem;
|
|
298
|
+
if (folder.readable !== false) skillPaths.push(mount);
|
|
220
299
|
}
|
|
221
300
|
|
|
222
|
-
logger.debug("
|
|
223
|
-
sharedPath: ASSISTANT_SHARED_SKILLS_PATH,
|
|
224
|
-
sharedMount: ASSISTANT_WORKSPACE_SKILLS_MOUNT,
|
|
225
|
-
userMount: email ? ASSISTANT_USER_SKILLS_MOUNT : undefined,
|
|
226
|
-
userPath: email ? userAssistantSkillsPath(email) : undefined,
|
|
301
|
+
logger.debug("skill-folders:mounted", {
|
|
227
302
|
mountKeys: Object.keys(mounts),
|
|
303
|
+
skillPaths,
|
|
228
304
|
});
|
|
229
305
|
|
|
230
|
-
return { mounts, skillPaths
|
|
306
|
+
return { mounts, skillPaths };
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Resolve one skill folder to a Mastra filesystem, or `undefined` to skip it
|
|
311
|
+
* for this request.
|
|
312
|
+
*/
|
|
313
|
+
async function resolveSkillFolderFilesystem(
|
|
314
|
+
name: string,
|
|
315
|
+
folder: SkillFolderOptions,
|
|
316
|
+
context: WorkspaceMountContext,
|
|
317
|
+
client: WorkspaceClient | undefined,
|
|
318
|
+
): Promise<WorkspaceFilesystem | undefined> {
|
|
319
|
+
if (folder.filesystem !== undefined) {
|
|
320
|
+
return resolveSkillFolderValue(folder.filesystem, context);
|
|
321
|
+
}
|
|
322
|
+
// A path mount needs the request's OBO client to reach the workspace.
|
|
323
|
+
if (folder.path === undefined || !client) {
|
|
324
|
+
logger.debug("skill-folder:skipped", {
|
|
325
|
+
name,
|
|
326
|
+
reason: folder.path === undefined ? "no-location" : "missing-obo-client",
|
|
327
|
+
});
|
|
328
|
+
return undefined;
|
|
329
|
+
}
|
|
330
|
+
const root = string.trimToNull(await resolveSkillFolderValue(folder.path, context));
|
|
331
|
+
if (!root) return undefined;
|
|
332
|
+
return databricksFilesystem(client, root, folder.writable !== true);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** Read a {@link SkillFolderValue}, calling it when it is a per-request resolver. */
|
|
336
|
+
function resolveSkillFolderValue<T>(
|
|
337
|
+
value: SkillFolderValue<T>,
|
|
338
|
+
context: WorkspaceMountContext,
|
|
339
|
+
): T | undefined | Promise<T | undefined> {
|
|
340
|
+
return typeof value === "function"
|
|
341
|
+
? (value as (context: WorkspaceMountContext) => T | undefined | Promise<T | undefined>)(context)
|
|
342
|
+
: value;
|
|
231
343
|
}
|
|
232
344
|
|
|
233
345
|
/**
|
|
@@ -249,19 +361,21 @@ function resolveWorkspaceIdentity(options: CreateWorkspaceOptions): {
|
|
|
249
361
|
return { id, name };
|
|
250
362
|
}
|
|
251
363
|
|
|
252
|
-
/** Collect
|
|
253
|
-
function buildMountResolvers(
|
|
364
|
+
/** Collect the skill-folder resolver and any caller-supplied ones. */
|
|
365
|
+
function buildMountResolvers(
|
|
366
|
+
skillFolders: Record<string, SkillFolderOptions>,
|
|
367
|
+
mounts: WorkspaceMountResolver[] | undefined,
|
|
368
|
+
): WorkspaceMountResolver[] {
|
|
254
369
|
const resolvers: WorkspaceMountResolver[] = [];
|
|
255
|
-
const
|
|
256
|
-
if (
|
|
257
|
-
resolvers.push(
|
|
370
|
+
const folderCount = Object.keys(skillFolders).length;
|
|
371
|
+
if (folderCount > 0) {
|
|
372
|
+
resolvers.push((context) => resolveSkillFolderMounts(skillFolders, context));
|
|
258
373
|
}
|
|
259
374
|
if (mounts?.length) {
|
|
260
375
|
resolvers.push(...mounts);
|
|
261
376
|
}
|
|
262
377
|
logger.debug("mounts:resolvers", {
|
|
263
|
-
|
|
264
|
-
builtInResolver: assistantSkills,
|
|
378
|
+
skillFolderCount: folderCount,
|
|
265
379
|
customResolverCount: mounts?.length ?? 0,
|
|
266
380
|
totalResolverCount: resolvers.length,
|
|
267
381
|
});
|
|
@@ -269,23 +383,19 @@ function buildMountResolvers(options: CreateWorkspaceOptions): WorkspaceMountRes
|
|
|
269
383
|
}
|
|
270
384
|
|
|
271
385
|
/**
|
|
272
|
-
* Gate
|
|
386
|
+
* Gate skill-folder mounts on the request's token.
|
|
273
387
|
*
|
|
274
388
|
* Always allows mounts in development; in other environments requires
|
|
275
389
|
* {@link hasWorkspaceFileScope}.
|
|
276
390
|
*/
|
|
277
|
-
function
|
|
278
|
-
requestContext: RequestContext | undefined,
|
|
279
|
-
): requestContext is RequestContext {
|
|
280
|
-
if (!requestContext) return false;
|
|
391
|
+
function shouldMountSkillFolders(requestContext: RequestContext): boolean {
|
|
281
392
|
if (process.env.NODE_ENV === "development") return true;
|
|
282
393
|
return hasWorkspaceFileScope(requestContext);
|
|
283
394
|
}
|
|
284
395
|
|
|
285
396
|
/** Read the trimmed OBO user email stamped on {@link MASTRA_USER_EMAIL_KEY}. */
|
|
286
397
|
function resolveScopedEmail(requestContext: RequestContext | undefined): string | undefined {
|
|
287
|
-
|
|
288
|
-
return email?.trim() || undefined;
|
|
398
|
+
return string.trimToNull(requestContext?.get(MASTRA_USER_EMAIL_KEY)) ?? undefined;
|
|
289
399
|
}
|
|
290
400
|
|
|
291
401
|
/**
|
|
@@ -313,7 +423,7 @@ async function databricksFilesystem(
|
|
|
313
423
|
logger.debug("databricks-mount:scratch-fallback", {
|
|
314
424
|
root,
|
|
315
425
|
readOnly,
|
|
316
|
-
error:
|
|
426
|
+
error: error.errorMessage(err),
|
|
317
427
|
});
|
|
318
428
|
}
|
|
319
429
|
return scratchFilesystem();
|