Federated workspace sources
Mount retrievers, MCP resources, project indexes, or app documents as workspace files.
This recipe gives an agent one file tree:
/workspacefor local notes and intermediate files./outputsfor generated deliverables./sourcesfor provider-backed reference files.
Use this when reference material already lives somewhere else and copying it all into the workspace would duplicate bytes or lose provider authorization rules.
Primitives Used
workspace()for the agent's unified file view.retriever()or a customWorkspaceMountSourcefor external source files.copy()when the agent needs to snapshot a source-backed text/JSON file into local writable state.- Optional
write()anddelete()hooks when a provider should be editable through the workspace or receive copied files.
Retriever-backed sources
Retriever mounts project retrieval hits as virtual text files.
import { prompt, retriever, workspace } from "@use-crux/core";
import { storage } from "@use-crux/core/storage";
const docs = retriever({
id: "docs",
namespace,
retrieve: async (query, options) => {
return searchDocuments(query, options);
},
});
const files = workspace({
id: "research-files",
namespace,
storage: storage({ records, assets }),
mounts: [
{ path: "/workspace", access: "readwrite" },
{ path: "/outputs", access: "readwrite" },
{
path: "/sources",
access: "read",
description: "Relevant source documents from the retrieval corpus.",
source: {
kind: "retriever",
retriever: docs,
query: "current task source documents",
pathForHit: (hit) =>
hit.source.path ?? `${hit.source.id}/${hit.chunkId}.md`,
contentForHit: (hit) => hit.parent?.content ?? hit.content,
},
},
],
});
export const researcher = prompt({
id: "researcher",
use: [files],
system: `
Use /sources for read-only reference material.
Use /workspace for notes.
Write final deliverables to /outputs.
`,
});The agent can call:
await files.list("/sources");
await files.read("/sources/brief.md");
await files.grep("launch", { path: "/sources/**/*.md" });
await files.copy("/sources/brief.md", "/workspace/brief.md");copy() is explicit. Retriever-backed mounts and custom sources without write
hooks stay read-only, and the copied file starts normal local workspace history
at the destination path.
Custom provider source
Use kind: "custom" for providers that are not bundled as first-class source
kinds.
import type { WorkspaceCustomMountSource } from "@use-crux/core/workspace";
export function appDocumentSource(): WorkspaceCustomMountSource {
return {
kind: "custom",
list: async (path, options) => ({
entries: await appDocuments.list({
workspacePath: path,
limit: options?.limit,
}),
}),
read: async (path) => {
const document = await appDocuments.readText(path);
if (!document) return null;
return {
kind: "text",
path,
mimeType: document.mimeType,
content: document.text,
size: document.size,
metadata: {
title: document.title,
providerId: document.id,
},
};
},
exists: async (path) => appDocuments.exists(path),
};
}Then mount it:
const files = workspace({
id: "research-files",
namespace,
storage,
mounts: [
{ path: "/workspace", access: "readwrite" },
{ path: "/outputs", access: "readwrite" },
{
path: "/sources",
access: "read",
source: appDocumentSource(),
},
],
});Crux normalizes public workspace paths before calling the source. It also checks
that list/stat/grep/read results stay under the mount root, so a provider cannot
accidentally expose /outside/... through /sources.
Custom sources can also opt into provider writes. See Editable workspace sources when workspace edits should update the provider instead of creating a local copy.
MCP resources pattern
MCP resource APIs vary by client. Wrap the client as a custom source and keep authorization inside the wrapper.
export function mcpResourceSource(
client: McpResourceClient,
): WorkspaceCustomMountSource {
return {
kind: "custom",
list: async (_path, options) => ({
entries: (await client.listResources({ limit: options?.limit })).map(
(resource) => ({
kind: "file" as const,
path: `/sources/mcp/${resource.name}.md`,
mount: "/sources",
mimeType: "text/markdown",
size: resource.size ?? 0,
storage: "virtual" as const,
metadata: { uri: resource.uri },
createdAt: 0,
updatedAt: 0,
}),
),
}),
read: async (path) => {
const uri = pathToMcpUri(path);
const resource = await client.readResource(uri);
return resource
? {
kind: "text" as const,
path,
mimeType: "text/markdown",
content: resource.text,
size: new TextEncoder().encode(resource.text).byteLength,
metadata: { uri },
}
: null;
},
};
}Keep provider URIs in metadata, not in workspace paths. Workspace paths should be stable, readable, and scoped under the mount root.
Project Index pattern
Expose summaries or selected files from a project index as generated markdown. Do not expose raw compiler objects or backend-specific AST handles.
export function projectIndexSource(
index: ProjectIndexClient,
): WorkspaceCustomMountSource {
return {
kind: "custom",
list: async () => ({
entries: (await index.definitions()).map((definition) => ({
kind: "file" as const,
path: `/sources/project/${definition.id}.md`,
mount: "/sources",
mimeType: "text/markdown",
size: 0,
storage: "virtual" as const,
metadata: { definitionId: definition.id },
createdAt: 0,
updatedAt: 0,
})),
}),
read: async (path) => {
const definition = await index.definition(pathToDefinitionId(path));
return definition
? {
kind: "text" as const,
path,
mimeType: "text/markdown",
content: renderDefinitionMarkdown(definition),
size: new TextEncoder().encode(renderDefinitionMarkdown(definition))
.byteLength,
}
: null;
},
};
}Connected drive or CMS pattern
For Google Drive, WordPress media, or another connected account, the important part is the boundary:
- Authenticate and authorize inside the source wrapper.
- Only list the folder/library the app granted to the agent.
- Map opaque provider ids to stable workspace paths.
- Keep native provider ids in metadata for app-side follow-up actions.
- Redact fields before returning
metadata.
export function connectedDriveSource(
drive: DriveClient,
): WorkspaceCustomMountSource {
return {
kind: "custom",
list: async (_path, options) => ({
entries: (await drive.listAuthorizedFiles({ limit: options?.limit })).map(
(file) => ({
kind: "file" as const,
path: `/sources/drive/${file.slug}.md`,
mount: "/sources",
mimeType: "text/markdown",
size: file.size,
storage: "virtual" as const,
metadata: { providerId: file.id, title: file.title },
createdAt: 0,
updatedAt: 0,
}),
),
}),
read: async (path) => {
const file = await drive.readMarkdown(pathToDriveId(path));
return file
? {
kind: "text" as const,
path,
mimeType: "text/markdown",
content: file.markdown,
size: file.size,
metadata: { providerId: file.id, title: file.title },
}
: null;
},
};
}Operational notes
Source-backed mounts are deliberately not sync engines. They provide a live view
into another system, read-only by default and writeable only when the provider
opts into mutation hooks. Use copy() when the agent needs a durable local
snapshot, version history, or artifact lifecycle on a source file.
Caching and invalidation belong in the provider wrapper today. For example, an MCP source can cache a listing for one request, while a drive source can use the provider's ETag or revision id. Crux still enforces workspace path boundaries, mount access, and provider write opt-in at the mount boundary.