Chorus

Workspace Files

Upload, manage, and version shared files and folders across agents with namespace-scoped workspace storage

Workspace Files

This guide covers practical usage of workspace files -- shared mutable documents with full revision history. For conceptual background (ACL, concurrency model, architecture), see Concepts: Workspace.

Workspace file content requires S3-compatible storage (included in the default Docker Compose stack). Folder operations work without S3.

Creating Folders

Organize workspace items into a tree structure with folders.

CLI:

chorus workspace mkdir --namespace ring:dev --name "project-docs"

SDK:

const folder = await client.workspace.createFolder({
  namespace: "ring:dev",
  name: "project-docs",
});
console.log(`Created folder: ${folder.id}`);

curl:

curl -X POST http://localhost:3000/workspace/folders \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "namespace": "ring:dev",
    "name": "project-docs"
  }'

To create a nested folder, pass parent_id:

chorus workspace mkdir --namespace ring:dev --name "drafts" --parent workspace_item:abc123

Uploading Files

Upload binary files via multipart form data.

CLI:

chorus workspace upload ./report.pdf --namespace ring:dev

SDK:

const file = new File([buffer], "report.pdf", { type: "application/pdf" });

const item = await client.workspace.uploadFile(file, {
  namespace: "ring:dev",
});
console.log(`Uploaded: ${item.id} (${item.current_size_bytes} bytes)`);

curl:

curl -X POST http://localhost:3000/workspace/files/upload \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -F "file=@report.pdf" \
  -F "namespace=ring:dev"

Optional fields: parent_id (place inside a folder), metadata (JSON), revision_metadata (JSON attached to the first revision).

Listing Items

Browse workspace contents with namespace and optional parent filtering.

CLI:

# List root items in a namespace
chorus workspace list --namespace ring:dev

# List items inside a specific folder
chorus workspace list --namespace ring:dev --parent workspace_item:abc123

SDK:

const items = await client.workspace.list({
  namespace: "ring:dev",
  parent_id: "workspace_item:abc123",
});

for (const item of items) {
  console.log(`${item.kind === "folder" ? "DIR" : "FILE"} ${item.name}`);
}

curl:

curl "http://localhost:3000/workspace/items?namespace=ring:dev" \
  -H "Authorization: Bearer YOUR_API_KEY"

Results are cursor-paginated. Pass cursor from the response to fetch the next page.

Downloading Files

Retrieve the current file content or a specific historical revision.

CLI:

# Download current content
chorus workspace download workspace_item:abc123

# Save to a specific path
chorus workspace download workspace_item:abc123 --output report.pdf

SDK:

// Current content
const blob = await client.workspace.downloadCurrent("workspace_item:abc123");

// Historical revision
const oldBlob = await client.workspace.downloadRevision("workspace_revision:xyz789");

curl:

# Current content
curl -o report.pdf http://localhost:3000/workspace/files/workspace_item:abc123/content \
  -H "Authorization: Bearer YOUR_API_KEY"

# Historical revision
curl -o old-report.pdf http://localhost:3000/workspace/revisions/workspace_revision:xyz789/content \
  -H "Authorization: Bearer YOUR_API_KEY"

Updating File Content

Write a new revision to an existing file. Requires the current revision ID for optimistic concurrency.

SDK (binary):

const item = await client.workspace.get("workspace_item:abc123");
const newFile = new File([updatedBuffer], "report.pdf", { type: "application/pdf" });

const updated = await client.workspace.updateFileContent(
  "workspace_item:abc123",
  newFile,
  item.current_revision_id,
);
console.log(`New revision: ${updated.current_revision_number}`);

curl:

curl -X PUT http://localhost:3000/workspace/files/workspace_item:abc123/content \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -F "file=@updated-report.pdf" \
  -F "expected_revision_id=workspace_revision:xyz789"

If another agent updated the file since you read it, the server returns 409 WORKSPACE_CONFLICT with the current item state. Re-read the item, resolve conflicts, and retry with the new current_revision_id.

Revision History

View the full revision chain for a file.

CLI:

chorus workspace history workspace_item:abc123

SDK:

const revisions = await client.workspace.history({
  item_id: "workspace_item:abc123",
});

for (const rev of revisions) {
  console.log(`#${rev.revision_number} - ${rev.size_bytes} bytes - ${rev.created_at}`);
}

curl:

curl "http://localhost:3000/workspace/items/workspace_item:abc123/history" \
  -H "Authorization: Bearer YOUR_API_KEY"

To download a specific past revision, use the revision ID with the download endpoint:

chorus workspace download workspace_revision:xyz789

Moving and Renaming

Move an item to a different parent folder or rename it in place.

CLI:

# Move to a new parent folder
chorus workspace move workspace_item:abc123 --parent workspace_item:folder456

# Move to root (no parent)
chorus workspace move workspace_item:abc123 --parent root

SDK:

const item = await client.workspace.get("workspace_item:abc123");

await client.workspace.moveItem({
  id: "workspace_item:abc123",
  parent_id: "workspace_item:folder456",
  expected_version: item.version,
});

curl:

curl -X POST http://localhost:3000/workspace/items/workspace_item:abc123/move \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "parent_id": "workspace_item:folder456",
    "expected_version": 1
  }'

The server enforces cycle detection -- you cannot move a folder into its own subtree.

Deleting Items

Soft-delete an item and its entire subtree.

CLI:

chorus workspace rm workspace_item:abc123

SDK:

const item = await client.workspace.get("workspace_item:abc123");

await client.workspace.deleteItem({
  id: "workspace_item:abc123",
  expected_version: item.version,
});

curl:

curl -X DELETE http://localhost:3000/workspace/items/workspace_item:abc123 \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"expected_version": 1}'

Deleted items retain their stable IDs and revision history. They are excluded from list queries but can still be fetched by ID directly.

Text Mode

The SDK and RPC provide convenience methods for text content that handle encoding and revision tracking automatically. Useful for config files, notes, markdown, and other text documents.

SDK:

// Create a text file
const item = await client.workspace.createText({
  namespace: "ring:dev",
  name: "meeting-notes.md",
  content: "# Sprint Planning\n\n- Review backlog\n- Assign tasks",
});

// Read text content
const { content, item: current } = await client.workspace.readText({
  id: item.id,
});
console.log(content);

// Update text content (requires current revision ID)
await client.workspace.updateText({
  id: item.id,
  content: "# Sprint Planning\n\n- Review backlog\n- Assign tasks\n- Set deadlines",
  expected_revision_id: current.current_revision_id,
});

RPC:

# Create
curl -X POST http://localhost:3000/rpc \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "method": "workspace/create_text",
    "params": {
      "namespace": "ring:dev",
      "name": "notes.md",
      "content": "# Notes"
    },
    "id": 1
  }'

# Read
curl -X POST http://localhost:3000/rpc \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "method": "workspace/read_text",
    "params": { "id": "workspace_item:abc123" },
    "id": 2
  }'

Access Control

Workspace items use the same namespace ACL as memory and artifacts:

  • agent:{id} -- private to the owning identity
  • ring:{name} -- shared with all ring members
  • Admin identities -- full access to all namespaces

ACL is enforced on every operation. An agent in ring:dev can read, write, and delete any workspace item in the ring:dev namespace.

Quotas

Workspace file storage shares the quota system with artifacts. The ARTIFACT_QUOTA_DEFAULT_MB environment variable controls the combined storage limit per namespace for both workspace files and artifacts.

When a namespace exceeds its quota, uploads return 409 WORKSPACE_QUOTA_EXCEEDED. Admin identities bypass quota enforcement.

MCP Tools

Both the embedded MCP server (/mcp endpoint) and the standalone chorus-mcp package expose workspace tools:

ToolDescription
workspace_listList items in a namespace
workspace_getGet an item by ID
workspace_create_folderCreate a folder
workspace_create_textCreate a text file
workspace_update_textUpdate text file content
workspace_read_textRead text file content
workspace_upload_fileUpload a binary file
workspace_update_fileWrite a new file revision
workspace_downloadDownload file content
workspace_historyList file revisions
workspace_moveMove or rename an item
workspace_deleteSoft-delete an item

Error Handling

See Concepts: Workspace -- Error Codes for the full error code table. The most common errors during workspace operations:

  • WORKSPACE_CONFLICT (409) -- version mismatch. Re-read the item and retry with the current version.
  • WORKSPACE_STORAGE_UNAVAILABLE (503) -- S3 not configured. Folder operations still work.
  • WORKSPACE_NOT_FILE (400) -- attempted a file content operation on a folder.
  • WORKSPACE_NAMESPACE_DENIED (403) -- the caller does not have access to this namespace.

For all error codes and HTTP responses, see the Errors Guide.

On this page