Crux
CookbookBasics

Federated workspace sources

Mount retrievers, MCP resources, project indexes, or app documents as workspace files.

This recipe gives an agent one file tree:

  • /workspace for local notes and intermediate files.
  • /outputs for generated deliverables.
  • /sources for 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 custom WorkspaceMountSource for external source files.
  • copy() when the agent needs to snapshot a source-backed text/JSON file into local writable state.
  • Optional write() and delete() 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.

On this page