Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
e534a07
feat(editor): workspaces and skills
DanielSLew Feb 17, 2026
8ee6719
merge main
DanielSLew Feb 17, 2026
f2ae1ff
fix(s3): use nullish coalescing for blob size fallback
DanielSLew Feb 17, 2026
1dff3b4
fix(libsql): throw MastraError instead of plain Error in update()
DanielSLew Feb 17, 2026
216cec6
fix(server): set activeVersionId and validate skillPath in publish route
DanielSLew Feb 17, 2026
21ed076
fix(core): rollback skill record on initial version creation failure
DanielSLew Feb 17, 2026
1258213
fix(editor): handle conditional variant arrays in cache invalidation
DanielSLew Feb 17, 2026
91ff098
fix(mongodb): use caller-provided ID and handle empty slugs
DanielSLew Feb 17, 2026
0fdc025
feat(core): add binary asset support to skill versioning
DanielSLew Feb 17, 2026
3e6bda7
Merge branch 'main' of github.com:mastra-ai/mastra into feat/editor/w…
DanielSLew Feb 17, 2026
dcf4d4c
fix: resolve build errors in cloudflare store and editor workspace types
DanielSLew Feb 17, 2026
ab14fe7
fix imports
DanielSLew Feb 17, 2026
d1c227c
fix(cloudflare): use concrete storage types in RecordTypes
DanielSLew Feb 17, 2026
a9380be
clean up and fix tests
DanielSLew Feb 17, 2026
7c83e82
fix(core): use explicit node:crypto import in inmemory skills
DanielSLew Feb 17, 2026
45803db
fix(editor): add null guard on blobStore and throw on missing version…
DanielSLew Feb 17, 2026
2f45a51
fix(editor): add return type and await to resolveSandbox
DanielSLew Feb 17, 2026
987362b
fix(s3): filter undefined obj.Key and add TOCTOU comment in blob store
DanielSLew Feb 17, 2026
99da345
fix(editor): use deterministic inline workspace ID
DanielSLew Feb 17, 2026
1a77cff
lint
DanielSLew Feb 17, 2026
207b111
merge main
DanielSLew Feb 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .changeset/clever-shirts-hide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
'@mastra/e2b': patch
'@mastra/gcs': patch
---

Added editor provider descriptors for workspace filesystem, sandbox, and blob store packages. Each provider exports an object with `id`, `name`, `description`, `configSchema` (JSON Schema), and a factory method, enabling the editor UI to auto-discover and render configuration forms for workspace providers.

- `@mastra/gcs`: Added `gcsFilesystemProvider` with config schema for bucket, projectId, credentials, prefix, readOnly, and endpoint
- `@mastra/e2b`: Added `e2bSandboxProvider` with config schema for template, timeout, env, metadata, runtimes, domain, and API settings

```ts
import { gcsFilesystemProvider } from '@mastra/gcs';
import { e2bSandboxProvider } from '@mastra/e2b';

const editor = new MastraEditor({
filesystems: {
gcs: gcsFilesystemProvider,
},
sandboxes: {
e2b: e2bSandboxProvider,
},
});

// Enumerate available providers and their config schemas for UI rendering
const fsProviders = editor.getFilesystemProviders();
const sbProviders = editor.getSandboxProviders();
```
101 changes: 101 additions & 0 deletions .changeset/silly-cars-roll.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
---
'@mastra/core': minor
'@mastra/editor': minor
'@mastra/libsql': minor
'@mastra/pg': minor
'@mastra/mongodb': minor
'@mastra/server': minor
'@mastra/e2b': patch
'@mastra/gcs': patch
'@mastra/s3': minor
'@mastra/clickhouse': patch
---

Added workspace and skill storage domains with full CRUD, versioning, and implementations across LibSQL, Postgres, and MongoDB. Added `editor.workspace` and `editor.skill` namespaces for managing workspace configurations and skill definitions through the editor. Agents stored in the editor can now reference workspaces (by ID or inline config) and skills, with full hydration to runtime `Workspace` instances during agent resolution.

**Filesystem-native skill versioning (draft → publish model):**

Skills are versioned as filesystem trees with content-addressable blob storage. The editing surface (live filesystem) is separated from the serving surface (versioned blob store), enabling a `draft → publish` workflow:

- `editor.skill.publish(skillId, source, skillPath)` — Snapshots a skill directory from the filesystem into blob storage, creates a new version with a tree manifest, and sets `activeVersionId`
- Version switching via `editor.skill.update({ id, activeVersionId })` — Points the skill to a previous version without re-publishing
- Publishing a skill automatically invalidates cached agents that reference it, so they re-hydrate with the updated version on next access

**Agent skill resolution strategies:**

Agents can reference skills with different resolution strategies:
- `strategy: 'latest'` — Resolves the skill's active version (honors `activeVersionId` for rollback)
- `pin: '<versionId>'` — Pins to a specific version, immune to publishes
- `strategy: 'live'` — Reads directly from the live filesystem (no blob store)

**Blob storage infrastructure:**

- `BlobStore` abstract class for content-addressable storage keyed by SHA-256 hash
- `InMemoryBlobStore` for testing
- LibSQL, Postgres, and MongoDB implementations
- `S3BlobStore` for storing blobs in S3 or S3-compatible storage (AWS, R2, MinIO, DO Spaces)
- `BlobStoreProvider` interface and `MastraEditorConfig.blobStores` registry for pluggable blob storage
- `VersionedSkillSource` and `CompositeVersionedSkillSource` for reading skill files from the blob store at runtime

**New storage types:**

- `StorageWorkspaceSnapshotType` and `StorageSkillSnapshotType` with corresponding input/output types
- `StorageWorkspaceRef` for ID-based or inline workspace references on agents
- `StorageSkillConfig` for per-agent skill overrides (`pin`, `strategy`, description, instructions)
- `SkillVersionTree` and `SkillVersionTreeEntry` for tree manifests
- `StorageBlobEntry` for content-addressable blob entries
- `SKILL_BLOBS_SCHEMA` for the `mastra_skill_blobs` table

**New editor namespaces:**

- `editor.workspace` — CRUD for workspace configs, plus `hydrateSnapshotToWorkspace()` for resolving to runtime `Workspace` instances
- `editor.skill` — CRUD for skill definitions, plus `publish()` for filesystem-to-blob snapshots

**Provider registries:**

- `MastraEditorConfig` accepts `filesystems`, `sandboxes`, and `blobStores` provider registries (keyed by provider ID)
- Built-in `local` filesystem and sandbox providers are auto-registered
- `editor.resolveBlobStore()` resolves from provider registry or falls back to the storage backend's blobs domain
- Providers expose `id`, `name`, `description`, `configSchema` (JSON Schema for UI form rendering), and a factory method

**Storage adapter support:**

- LibSQL: Full `workspaces`, `skills`, and `blobs` domain implementations
- Postgres: Full `workspaces`, `skills`, and `blobs` domain implementations
- MongoDB: Full `workspaces`, `skills`, and `blobs` domain implementations
- All three include `workspace`, `skills`, and `skillsFormat` fields on agent versions

**Server endpoints:**

- `GET/POST/PATCH/DELETE /stored/workspaces` — CRUD for stored workspaces
- `GET/POST/PATCH/DELETE /stored/skills` — CRUD for stored skills
- `POST /stored/skills/:id/publish` — Publish a skill from a filesystem source

```ts
import { MastraEditor } from '@mastra/editor';
import { s3FilesystemProvider, s3BlobStoreProvider } from '@mastra/s3';
import { e2bSandboxProvider } from '@mastra/e2b';

const editor = new MastraEditor({
filesystems: { s3: s3FilesystemProvider },
sandboxes: { e2b: e2bSandboxProvider },
blobStores: { s3: s3BlobStoreProvider },
});

// Create a skill and publish it
const skill = await editor.skill.create({
name: 'Code Review',
description: 'Reviews code for best practices',
instructions: 'Analyze the code and provide feedback...',
});
await editor.skill.publish(skill.id, source, 'skills/code-review');

// Agents resolve skills by strategy
await editor.agent.create({
name: 'Dev Assistant',
model: { provider: 'openai', name: 'gpt-4' },
workspace: { type: 'id', workspaceId: workspace.id },
skills: { [skill.id]: { strategy: 'latest' } },
skillsFormat: 'xml',
});
```
5 changes: 5 additions & 0 deletions packages/core/src/editor/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,9 @@ export type {
IEditorMCPNamespace,
IEditorPromptNamespace,
IEditorScorerNamespace,
IEditorWorkspaceNamespace,
IEditorSkillNamespace,
FilesystemProvider,
SandboxProvider,
BlobStoreProvider,
} from './types';
134 changes: 133 additions & 1 deletion packages/core/src/editor/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { MastraScorer } from '../evals';
import type { IMastraLogger } from '../logger';
import type { Mastra } from '../mastra';
import type { RequestContext } from '../request-context';
import type { BlobStore } from '../storage/domains/blobs/base';
import type {
AgentInstructionBlock,
StorageCreateAgentInput,
Expand All @@ -29,13 +30,110 @@ import type {
StorageListMCPClientsOutput,
StorageResolvedMCPClientType,
StorageListMCPClientsResolvedOutput,
StorageCreateWorkspaceInput,
StorageUpdateWorkspaceInput,
StorageListWorkspacesInput,
StorageListWorkspacesOutput,
StorageResolvedWorkspaceType,
StorageListWorkspacesResolvedOutput,
StorageCreateSkillInput,
StorageUpdateSkillInput,
StorageListSkillsInput,
StorageListSkillsOutput,
StorageResolvedSkillType,
StorageListSkillsResolvedOutput,
} from '../storage/types';
import type { ToolProvider } from '../tool-provider';
import type { WorkspaceFilesystem } from '../workspace/filesystem/filesystem';
import type { WorkspaceSandbox } from '../workspace/sandbox/sandbox';

// ============================================================================
// Workspace Provider Interfaces
// ============================================================================

/**
* A registered filesystem provider that the editor can use to hydrate
* stored workspace filesystem configs into runtime instances.
*
* Built-in providers (e.g., local) are auto-registered. External providers
* (e.g., S3, GCS) should be passed to `MastraEditorConfig.filesystems`.
*/
export interface FilesystemProvider<TConfig = Record<string, unknown>> {
/** Unique provider identifier (e.g., 'local', 's3', 'gcs') — matches `StorageFilesystemConfig.provider` */
id: string;
/** Human-readable name for UI display */
name: string;
/** Short description for UI display */
description?: string;
/** JSON Schema describing the provider-specific configuration. Used by UI to render config forms. */
configSchema?: Record<string, unknown>;
/** Create a filesystem instance from the stored config */
createFilesystem(config: TConfig): WorkspaceFilesystem | Promise<WorkspaceFilesystem>;
}

/**
* A registered sandbox provider that the editor can use to hydrate
* stored workspace sandbox configs into runtime instances.
*
* Built-in providers (e.g., local) are auto-registered. External providers
* (e.g., E2B) should be passed to `MastraEditorConfig.sandboxes`.
*/
export interface SandboxProvider<TConfig = Record<string, unknown>> {
/** Unique provider identifier (e.g., 'local', 'e2b') — matches `StorageSandboxConfig.provider` */
id: string;
/** Human-readable name for UI display */
name: string;
/** Short description for UI display */
description?: string;
/** JSON Schema describing the provider-specific configuration. Used by UI to render config forms. */
configSchema?: Record<string, unknown>;
/** Create a sandbox instance from the stored config */
createSandbox(config: TConfig): WorkspaceSandbox | Promise<WorkspaceSandbox>;
}

/**
* A registered blob store provider that the editor can use to store/retrieve
* content-addressable skill blobs.
*
* The built-in 'storage' provider uses the configured storage backend's blobs
* domain. External providers (e.g., S3) can be supplied via `MastraEditorConfig.blobStores`.
*/
export interface BlobStoreProvider<TConfig = Record<string, unknown>> {
/** Unique provider identifier (e.g., 'storage', 's3') */
id: string;
/** Human-readable name for UI display */
name: string;
/** Short description for UI display */
description?: string;
/** JSON Schema describing the provider-specific configuration. Used by UI to render config forms. */
configSchema?: Record<string, unknown>;
/** Create a blob store instance from the stored config */
createBlobStore(config: TConfig): BlobStore | Promise<BlobStore>;
}

export interface MastraEditorConfig {
logger?: IMastraLogger;
/** Tool providers for integration tools (e.g., Composio) */
toolProviders?: Record<string, ToolProvider>;
/**
* Additional filesystem providers beyond the built-in ones.
* Built-in providers (local) are always available.
* @example { [s3FilesystemProvider.id]: s3FilesystemProvider }
*/
filesystems?: Record<string, FilesystemProvider>;
/**
* Additional sandbox providers beyond the built-in ones.
* Built-in providers (local) are always available.
* @example { [e2bSandboxProvider.id]: e2bSandboxProvider }
*/
sandboxes?: Record<string, SandboxProvider>;
/**
* Additional blob store providers beyond the built-in 'storage' provider.
* The built-in 'storage' provider uses the configured storage backend's blobs domain.
* External providers (e.g., S3) allow storing blobs outside the main database.
* @example { [s3BlobStoreProvider.id]: s3BlobStoreProvider }
*/
blobStores?: Record<string, BlobStoreProvider>;
}

export interface GetByIdOptions {
Expand Down Expand Up @@ -113,13 +211,41 @@ export interface IEditorMCPNamespace {
clearCache(id?: string): void;
}

// ============================================================================
// Workspace Namespace Interface
// ============================================================================

export interface IEditorWorkspaceNamespace {
create(input: StorageCreateWorkspaceInput): Promise<StorageResolvedWorkspaceType>;
getById(id: string, options?: GetByIdOptions): Promise<StorageResolvedWorkspaceType | null>;
update(input: StorageUpdateWorkspaceInput): Promise<StorageResolvedWorkspaceType>;
delete(id: string): Promise<void>;
list(args?: StorageListWorkspacesInput): Promise<StorageListWorkspacesOutput>;
listResolved(args?: StorageListWorkspacesInput): Promise<StorageListWorkspacesResolvedOutput>;
clearCache(id?: string): void;
}

// ============================================================================
// Skill Namespace Interface
// ============================================================================

export interface IEditorSkillNamespace {
create(input: StorageCreateSkillInput): Promise<StorageResolvedSkillType>;
getById(id: string, options?: GetByIdOptions): Promise<StorageResolvedSkillType | null>;
update(input: StorageUpdateSkillInput): Promise<StorageResolvedSkillType>;
delete(id: string): Promise<void>;
list(args?: StorageListSkillsInput): Promise<StorageListSkillsOutput>;
listResolved(args?: StorageListSkillsInput): Promise<StorageListSkillsResolvedOutput>;
clearCache(id?: string): void;
}

// ============================================================================
// Main Editor Interface
// ============================================================================

/**
* Interface for the Mastra Editor, which handles agent, prompt, scorer,
* and MCP config management from stored data.
* MCP config, workspace, and skill management from stored data.
*/
export interface IMastraEditor {
/**
Expand All @@ -140,6 +266,12 @@ export interface IMastraEditor {
/** Scorer definition management namespace */
readonly scorer: IEditorScorerNamespace;

/** Workspace management namespace */
readonly workspace: IEditorWorkspaceNamespace;

/** Skill management namespace */
readonly skill: IEditorSkillNamespace;

/** Registered tool providers */
getToolProvider(id: string): ToolProvider | undefined;
/** List all registered tool providers */
Expand Down
20 changes: 20 additions & 0 deletions packages/core/src/storage/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@ import type {
PromptBlocksStorage,
ScorerDefinitionsStorage,
MCPClientsStorage,
WorkspacesStorage,
SkillsStorage,
ScoresStorage,
WorkflowsStorage,
MemoryStorage,
ObservabilityStorage,
BlobStore,
DatasetsStorage,
ExperimentsStorage,
} from './domains';
Expand All @@ -24,6 +27,9 @@ export type StorageDomains = {
promptBlocks?: PromptBlocksStorage;
scorerDefinitions?: ScorerDefinitionsStorage;
mcpClients?: MCPClientsStorage;
workspaces?: WorkspacesStorage;
skills?: SkillsStorage;
blobs?: BlobStore;
};

/**
Expand Down Expand Up @@ -219,6 +225,8 @@ export class MastraCompositeStore extends MastraBase {
promptBlocks: domainOverrides.promptBlocks ?? defaultStores?.promptBlocks,
scorerDefinitions: domainOverrides.scorerDefinitions ?? defaultStores?.scorerDefinitions,
mcpClients: domainOverrides.mcpClients ?? defaultStores?.mcpClients,
workspaces: domainOverrides.workspaces ?? defaultStores?.workspaces,
skills: domainOverrides.skills ?? defaultStores?.skills,
} as StorageDomains;
}
// Otherwise, subclasses set stores themselves
Expand Down Expand Up @@ -295,6 +303,18 @@ export class MastraCompositeStore extends MastraBase {
initTasks.push(this.stores.mcpClients.init());
}

if (this.stores?.workspaces) {
initTasks.push(this.stores.workspaces.init());
}

if (this.stores?.skills) {
initTasks.push(this.stores.skills.init());
}

if (this.stores?.blobs) {
initTasks.push(this.stores.blobs.init());
}

this.hasInitialized = Promise.all(initTasks).then(() => true);

await this.hasInitialized;
Expand Down
Loading
Loading