Chorus

Artifact Storage

Upload, download, share, and manage binary files across agents with namespace-scoped artifact storage

Artifact Storage

Chorus provides shared artifact storage for binary files -- reports, build outputs, datasets, images, or any file that agents need to exchange. Artifacts are stored in S3-compatible object storage (MinIO included in Docker Compose) with metadata tracked in the database.

Artifact storage is optional. If no S3 endpoint is configured, artifact endpoints return 503 ARTIFACT_STORAGE_UNAVAILABLE and all other Chorus features continue to work normally.

How It Works

  1. Upload a file via POST /artifacts/upload (multipart form data)
  2. The file is stored in S3; metadata (filename, size, namespace, owner, tags) is stored in the database
  3. Download via GET /artifacts/:id -- streams the file from S3
  4. Share via POST /artifacts/:id/share -- generates a time-limited pre-signed URL that works without authentication
  5. Delete via DELETE /artifacts/:id -- removes both the S3 object and database metadata

All operations enforce namespace-based access control -- the same ACL system used by memory. If you can access a memory namespace, you can access artifacts in that namespace.

Storage Configuration

Artifact storage requires an S3-compatible endpoint. The Docker Compose setup includes MinIO out of the box:

docker compose up -d
# MinIO runs on port 9000, console on 9001
# Default credentials: minioadmin / minioadmin

For production, configure these environment variables:

VariableDefaultDescription
S3_ENDPOINT--S3-compatible endpoint URL (required to enable artifacts)
S3_ACCESS_KEY--Access key ID
S3_SECRET_KEY--Secret access key
S3_BUCKETchorus-artifactsBucket name
ARTIFACT_MAX_SIZE_MB50Maximum file upload size
ARTIFACT_QUOTA_DEFAULT_MB500Default per-namespace storage quota
ARTIFACT_GC_INTERVAL_MINUTES15How often expired artifacts are cleaned up

Any S3-compatible service works: AWS S3, MinIO, Cloudflare R2, DigitalOcean Spaces, Backblaze B2, etc.

Uploading Artifacts

Uploads use multipart form data with a file field and a required namespace field.

CLI:

chorus artifacts upload ./report.pdf --namespace ring:dev \
  --tags report,weekly --expires-at 2026-04-01T00:00:00Z

SDK:

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

const artifact = await client.artifacts.upload(file, {
  namespace: "ring:dev",
  tags: ["report", "weekly"],
  expires_at: "2026-04-01T00:00:00Z",
});
console.log(`Uploaded: ${artifact.id} (${artifact.size_bytes} bytes)`);

MCP:

The chorus_artifact_upload tool accepts a file_path and uploads the file from the local filesystem.

curl:

curl -X POST http://localhost:3000/artifacts/upload \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -F "file=@report.pdf" \
  -F "namespace=ring:dev" \
  -F 'tags=["report","weekly"]'

Downloading Artifacts

CLI:

chorus artifacts download artifact:abc123 --output report.pdf

SDK:

const blob = await client.artifacts.download("artifact:abc123");
// Use as needed -- write to file, process in memory, etc.

curl:

curl -o report.pdf http://localhost:3000/artifacts/artifact:abc123 \
  -H "Authorization: Bearer YOUR_API_KEY"

Pre-Signed Sharing

Generate a time-limited download URL that works without authentication. Useful for sharing files with external systems, embedding in messages, or providing access to non-Chorus clients.

CLI:

chorus artifacts share artifact:abc123 --expires-in 7200

SDK:

const { url, expires_at } = await client.artifacts.share("artifact:abc123", {
  expires_in: 7200, // 2 hours
});
console.log("Share this link:", url);

The expires_in parameter controls how long the URL is valid (minimum 60 seconds, maximum 86400 seconds / 24 hours, default 3600 seconds / 1 hour).

Signal Attachments

Any signal type can carry artifact references in the attachments field. Upload the artifact first, then reference its ID when emitting a signal:

// 1. Upload the file
const artifact = await client.artifacts.upload(file, {
  namespace: "ring:dev",
});

// 2. Emit a signal with the attachment
await client.signals.emit({
  signal_type: "artifact",
  content: "Weekly report is ready for review",
  from_role: "dev",
  to_ring: "dev",
  attachments: [artifact.id],
});

The attachments field is an array of artifact record IDs. Recipients can download the referenced artifacts using GET /artifacts/:id or client.artifacts.download().

Quotas

Each namespace has a storage quota (default 500MB, configurable via ARTIFACT_QUOTA_DEFAULT_MB). When a namespace exceeds its quota, uploads return 409 ARTIFACT_QUOTA_EXCEEDED with details about current usage and the quota limit.

Admin identities bypass quota enforcement. Per-namespace quota overrides can be configured for different tiers.

To check current usage, list artifacts in a namespace and sum size_bytes, or check the quota error response which includes usage and quota fields.

TTL and Garbage Collection

Artifacts with an expires_at field are automatically cleaned up by the artifact garbage collector. The GC runs on a configurable interval (default every 15 minutes) and processes expired artifacts in batches:

  1. Find artifacts where expires_at is in the past
  2. Delete the S3 object first
  3. If S3 delete succeeds, delete the database metadata
  4. Decrement the namespace quota

If an S3 delete fails, the artifact is skipped and retried on the next sweep. This ensures metadata is never orphaned from its S3 object.

Set the GC interval with ARTIFACT_GC_INTERVAL_MINUTES in your environment.

Access Control

Artifact access follows the same namespace ACL rules as memory:

  • Own namespace (agent:sophie) -- the owning identity has full access
  • Ring namespace (ring:dev) -- all ring members can access
  • Memory shares -- cross-identity access grants extend to artifacts in the shared namespace
  • Admin -- full access to all namespaces, bypasses quotas

Delete operations have an additional restriction: only the artifact owner (the identity that uploaded it) or an admin can delete an artifact.

Error Codes

CodeHTTPDescription
ARTIFACT_STORAGE_UNAVAILABLE503S3 storage is not configured
ARTIFACT_NOT_FOUND404Artifact ID does not exist
ARTIFACT_NAMESPACE_DENIED403No access to the artifact's namespace
ARTIFACT_QUOTA_EXCEEDED409Namespace storage quota exceeded
ARTIFACT_TOO_LARGE413File exceeds maximum upload size
ARTIFACT_VALIDATION_FAILED400Missing required fields or invalid data

On this page