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
- Upload a file via
POST /artifacts/upload(multipart form data) - The file is stored in S3; metadata (filename, size, namespace, owner, tags) is stored in the database
- Download via
GET /artifacts/:id-- streams the file from S3 - Share via
POST /artifacts/:id/share-- generates a time-limited pre-signed URL that works without authentication - 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:
| Variable | Default | Description |
|---|---|---|
S3_ENDPOINT | -- | S3-compatible endpoint URL (required to enable artifacts) |
S3_ACCESS_KEY | -- | Access key ID |
S3_SECRET_KEY | -- | Secret access key |
S3_BUCKET | chorus-artifacts | Bucket name |
ARTIFACT_MAX_SIZE_MB | 50 | Maximum file upload size |
ARTIFACT_QUOTA_DEFAULT_MB | 500 | Default per-namespace storage quota |
ARTIFACT_GC_INTERVAL_MINUTES | 15 | How 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:
- Find artifacts where
expires_atis in the past - Delete the S3 object first
- If S3 delete succeeds, delete the database metadata
- 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
| Code | HTTP | Description |
|---|---|---|
ARTIFACT_STORAGE_UNAVAILABLE | 503 | S3 storage is not configured |
ARTIFACT_NOT_FOUND | 404 | Artifact ID does not exist |
ARTIFACT_NAMESPACE_DENIED | 403 | No access to the artifact's namespace |
ARTIFACT_QUOTA_EXCEEDED | 409 | Namespace storage quota exceeded |
ARTIFACT_TOO_LARGE | 413 | File exceeds maximum upload size |
ARTIFACT_VALIDATION_FAILED | 400 | Missing required fields or invalid data |