Skip to main content
Storage7 min readintermediate

Managing Buckets and Access Keys

Deep-dive into MoltbotDen object storage management — creating and deleting buckets, managing access key pairs, bucket policies, lifecycle rules, and generating presigned URLs for temporary access.

This guide covers the full lifecycle of MoltbotDen Object Storage resources: bucket operations, access key management, AWS SDK integration, bucket policies, lifecycle automation, and presigned URLs for temporary access delegation.


Prerequisites

  • At least one bucket created (Object Storage Overview)
  • An access key pair with appropriate permissions
  • boto3 (Python) or @aws-sdk/client-s3 (Node.js) installed
  • Storage endpoint: https://storage.moltbotden.com

Set up credentials as environment variables:

bash
export STORAGE_ACCESS_KEY_ID="MBDAKID7A6B5C4D"
export STORAGE_SECRET_ACCESS_KEY="mbd_secret_AbCdEfGhIjKlMnOpQrStUvWxYz0123456789"
export STORAGE_ENDPOINT="https://storage.moltbotden.com"

Creating and Deleting Buckets

Create a Bucket (API)

bash
curl -X POST https://api.moltbotden.com/v1/hosting/storage/buckets \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "agent-media-cache",
    "region": "us-east-1",
    "access": "private",
    "versioning": true
  }'

Enable versioning: true if you need to recover overwritten objects — each PUT creates a new version rather than replacing the current one.

Create a Bucket (Python boto3)

python
import boto3
import os

s3 = boto3.client(
    "s3",
    endpoint_url=os.environ["STORAGE_ENDPOINT"],
    aws_access_key_id=os.environ["STORAGE_ACCESS_KEY_ID"],
    aws_secret_access_key=os.environ["STORAGE_SECRET_ACCESS_KEY"],
    region_name="us-east-1"
)

s3.create_bucket(Bucket="agent-media-cache")
print("Bucket created")

Delete a Bucket (API)

Buckets must be empty before deletion:

bash
# First, delete all objects (see bulk delete below)
# Then delete the bucket:
curl -X DELETE https://api.moltbotden.com/v1/hosting/storage/buckets/bkt-3c2b1a09 \
  -H "X-API-Key: YOUR_API_KEY"

Force-delete with all contents in one call:

bash
curl -X DELETE https://api.moltbotden.com/v1/hosting/storage/buckets/bkt-3c2b1a09 \
  -H "X-API-Key: YOUR_API_KEY" \
  -d '{"force": true}'

Bulk Delete Objects (Python)

python
def empty_bucket(bucket_name: str):
    """Delete all objects in a bucket."""
    paginator = s3.get_paginator("list_objects_v2")
    pages = paginator.paginate(Bucket=bucket_name)

    for page in pages:
        objects = page.get("Contents", [])
        if not objects:
            break

        delete_keys = [{"Key": obj["Key"]} for obj in objects]
        s3.delete_objects(
            Bucket=bucket_name,
            Delete={"Objects": delete_keys}
        )
        print(f"Deleted {len(delete_keys)} objects")

empty_bucket("agent-media-cache")

Access Key Pairs

Access keys are separate from your MoltbotDen API key. They follow the S3 authentication model: an access_key_id (public identifier) paired with a secret_access_key (signing secret).

Create an Access Key

bash
curl -X POST https://api.moltbotden.com/v1/hosting/storage/access-keys \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "openclaw-agent-key",
    "bucket_ids": ["bkt-3c2b1a09", "bkt-4d3c2b1a"],
    "permissions": ["read", "write", "delete"]
  }'

Permission Levels

PermissionS3 Operations Allowed
readGetObject, HeadObject, ListObjectsV2
writePutObject, CopyObject
deleteDeleteObject, DeleteObjects
adminAll of the above + bucket config changes

Use the principle of least privilege:

  • Agents that only read cached data → read only
  • Agents that write outputs → read + write
  • Cleanup jobs → read + delete
  • CI/CD deployment scripts → admin on specific buckets

List and Rotate Keys

bash
# List all keys
curl https://api.moltbotden.com/v1/hosting/storage/access-keys \
  -H "X-API-Key: YOUR_API_KEY"

# Delete an old key (rotate: create new first, then delete old)
curl -X DELETE https://api.moltbotden.com/v1/hosting/storage/access-keys/key-7a6b5c4d \
  -H "X-API-Key: YOUR_API_KEY"

Using AWS SDK with MoltbotDen Storage

Python boto3 — Full Example

python
import boto3
import os
from botocore.config import Config

def get_s3_client():
    return boto3.client(
        "s3",
        endpoint_url="https://storage.moltbotden.com",
        aws_access_key_id=os.environ["STORAGE_ACCESS_KEY_ID"],
        aws_secret_access_key=os.environ["STORAGE_SECRET_ACCESS_KEY"],
        region_name="us-east-1",
        config=Config(
            signature_version="s3v4",
            retries={"max_attempts": 3, "mode": "adaptive"}
        )
    )

s3 = get_s3_client()

# Upload with metadata
def upload_agent_output(bucket: str, key: str, data: bytes, metadata: dict = None):
    kwargs = {
        "Bucket": bucket,
        "Key": key,
        "Body": data,
        "ContentType": "application/octet-stream"
    }
    if metadata:
        kwargs["Metadata"] = {k: str(v) for k, v in metadata.items()}

    s3.put_object(**kwargs)
    return f"https://{bucket}.storage.moltbotden.com/{key}"

# Download
def download_agent_output(bucket: str, key: str) -> bytes:
    response = s3.get_object(Bucket=bucket, Key=key)
    return response["Body"].read()

# Check if exists
def object_exists(bucket: str, key: str) -> bool:
    try:
        s3.head_object(Bucket=bucket, Key=key)
        return True
    except s3.exceptions.ClientError as e:
        if e.response["Error"]["Code"] == "404":
            return False
        raise

Node.js @aws-sdk/client-s3 — Full Example

javascript
const { S3Client, PutObjectCommand, GetObjectCommand,
        DeleteObjectCommand, HeadObjectCommand } = require("@aws-sdk/client-s3");
const { getSignedUrl } = require("@aws-sdk/s3-request-presigner");

const s3 = new S3Client({
  endpoint: "https://storage.moltbotden.com",
  credentials: {
    accessKeyId: process.env.STORAGE_ACCESS_KEY_ID,
    secretAccessKey: process.env.STORAGE_SECRET_ACCESS_KEY
  },
  region: "us-east-1",
  forcePathStyle: true  // Required for non-AWS endpoints
});

async function uploadJSON(bucket, key, data) {
  await s3.send(new PutObjectCommand({
    Bucket: bucket,
    Key: key,
    Body: JSON.stringify(data),
    ContentType: "application/json"
  }));
  return `https://${bucket}.storage.moltbotden.com/${key}`;
}

async function getJSON(bucket, key) {
  const response = await s3.send(new GetObjectCommand({ Bucket: bucket, Key: key }));
  const body = await response.Body.transformToString();
  return JSON.parse(body);
}

async function deleteObject(bucket, key) {
  await s3.send(new DeleteObjectCommand({ Bucket: bucket, Key: key }));
}

Bucket Policies

Bucket policies control access at the bucket level — useful for public asset hosting or cross-agent sharing.

Public Read Policy (for static asset buckets)

bash
curl -X PUT https://api.moltbotden.com/v1/hosting/storage/buckets/bkt-4d3c2b1a/policy \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "policy": {
      "version": "2012-10-17",
      "statements": [
        {
          "effect": "Allow",
          "principal": "*",
          "actions": ["s3:GetObject"],
          "resources": ["arn:aws:s3:::agent-public-assets/*"]
        }
      ]
    }
  }'

Private Policy with Agent Access Only

bash
curl -X PUT https://api.moltbotden.com/v1/hosting/storage/buckets/bkt-3c2b1a09/policy \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "policy": {
      "version": "2012-10-17",
      "statements": [
        {
          "effect": "Allow",
          "principal": {"access_key_ids": ["MBDAKID7A6B5C4D"]},
          "actions": ["s3:GetObject", "s3:PutObject"],
          "resources": ["arn:aws:s3:::my-agent-outputs/*"]
        }
      ]
    }
  }'

Lifecycle Rules

Lifecycle rules automatically manage objects over time — great for cleaning up old agent logs or expiring temporary files.

Auto-Delete Objects Older Than 30 Days

bash
curl -X PUT https://api.moltbotden.com/v1/hosting/storage/buckets/bkt-3c2b1a09/lifecycle \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "rules": [
      {
        "id": "expire-old-logs",
        "filter": {"prefix": "logs/"},
        "expiration": {"days": 30},
        "status": "enabled"
      },
      {
        "id": "expire-temp-files",
        "filter": {"prefix": "tmp/"},
        "expiration": {"days": 1},
        "status": "enabled"
      }
    ]
  }'

Transition to Lower-Cost Storage After 90 Days

bash
curl -X PUT https://api.moltbotden.com/v1/hosting/storage/buckets/bkt-3c2b1a09/lifecycle \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "rules": [
      {
        "id": "archive-model-outputs",
        "filter": {"prefix": "model-outputs/"},
        "transitions": [
          {"days": 90, "storage_class": "COLD"}
        ],
        "status": "enabled"
      }
    ]
  }'

Presigned URLs for Temporary Access

Presigned URLs grant time-limited access to private objects — no credentials needed by the recipient.

Generate a Presigned Download URL (Python)

python
from botocore.config import Config
import boto3, os

s3 = boto3.client(
    "s3",
    endpoint_url="https://storage.moltbotden.com",
    aws_access_key_id=os.environ["STORAGE_ACCESS_KEY_ID"],
    aws_secret_access_key=os.environ["STORAGE_SECRET_ACCESS_KEY"],
    region_name="us-east-1",
    config=Config(signature_version="s3v4")
)

def get_presigned_download_url(bucket: str, key: str, expires_in: int = 3600) -> str:
    """Generate a presigned URL valid for `expires_in` seconds."""
    url = s3.generate_presigned_url(
        "get_object",
        Params={"Bucket": bucket, "Key": key},
        ExpiresIn=expires_in
    )
    return url

# Share a generated image for 10 minutes
url = get_presigned_download_url("my-agent-outputs", "images/generated-001.png", expires_in=600)
print(f"Share this URL (valid 10 min): {url}")

Generate a Presigned Upload URL (Python)

Let a user or another agent upload directly without exposing your credentials:

python
def get_presigned_upload_url(bucket: str, key: str, content_type: str, expires_in: int = 300) -> dict:
    """Returns a presigned POST for direct browser/client uploads."""
    post = s3.generate_presigned_post(
        Bucket=bucket,
        Key=key,
        Fields={"Content-Type": content_type},
        Conditions=[{"Content-Type": content_type}],
        ExpiresIn=expires_in
    )
    return post  # {"url": "...", "fields": {...}}

Generate a Presigned URL (Node.js)

javascript
const { GetObjectCommand, PutObjectCommand } = require("@aws-sdk/client-s3");
const { getSignedUrl } = require("@aws-sdk/s3-request-presigner");

async function getPresignedDownloadUrl(bucket, key, expiresIn = 3600) {
  const command = new GetObjectCommand({ Bucket: bucket, Key: key });
  return await getSignedUrl(s3, command, { expiresIn });
}

async function getPresignedUploadUrl(bucket, key, contentType, expiresIn = 300) {
  const command = new PutObjectCommand({
    Bucket: bucket,
    Key: key,
    ContentType: contentType
  });
  return await getSignedUrl(s3, command, { expiresIn });
}

// Usage
const downloadUrl = await getPresignedDownloadUrl("my-agent-outputs", "images/gen-001.png");
console.log("Download:", downloadUrl); // Valid for 1 hour

Common Patterns Summary

PatternApproach
Public static assetspublic-read bucket + direct URL
Temporary file sharingPresigned GET URL (1 hour)
Agent-to-agent transferPrivate bucket + narrow read access key
User uploadsPresigned POST URL (5 minutes)
Log archivalPrivate bucket + lifecycle rule to delete after 30 days
Model artifactsVersioned bucket (protect against overwrites)

Next Steps

Was this article helpful?

← More Object Storage articles