hybrids3: Almacenamiento de Objetos Compatible con S3 Que No Exige un Doctorado

Pon nginx delante de MinIO en un prefijo de ruta e intenta usar URLs prefirmadas. Venga, adelante, espero.
El problema es AWS Signature V4. La firma cubre la ruta de la petición. nginx en /storage/ quita ese prefijo antes de reenviar, así que boto3 firma /storage/bucket/key, el servidor ve /bucket/key, el HMAC no cuadra, te llevas un 403. Esto no es un error de configuración, es como funciona el proxy por prefijo de ruta de nginx y como funciona la verificación de SigV4. Son estructuralmente incompatibles, a menos que te pongas listo con la config del proxy o te cambies a un servidor que lo maneje de otra forma.
docker-hybrids3 lo maneja de otra forma. Tiene una opción de config path_prefix, la pones en /storage y todas las rutas se mueven nativamente bajo ese prefijo. Sin recorte de ruta en el proxy. boto3 firma /storage/bucket/key, nginx reenvía /storage/bucket/key, el servidor ve /storage/bucket/key. Las firmas cuadran. Por eso también corro todo detrás de una única pasarela nginx (aigate hace esto) sin pelearme con la config del proxy.
La otra cosa que me empujó a construirlo: cuando un agente de IA necesita escribir un fichero y devolverte una URL, no quieres levantar un almacén de objetos distribuido para eso. Quieres algo que arranque en dos minutos y se quite de en medio. HybridS3 lleva un servidor MCP integrado, el agente llama a upload_object, recibe una URL, listo.

Qué Es

SQLite para los metadatos, ficheros planos en disco. boto3 funciona contra él. HTTP pelado con curl funciona. Auth con bearer token, sin la ceremonia de SigV4 para peticiones HTTP simples. Tres interfaces, un container, sin consola web, sin IAM, sin modo distribuido.
Los buckets se definen en la config, no se crean vía API. Siempre sabes exactamente qué existe. ¿Quieres un bucket? Lo añades al YAML y reinicias. ¿Quieres caducidad TTL? Pones ttl: 24h en el bucket y los objetos se borran solos después de su última escritura. Sin políticas de ciclo de vida, sin tareas de cron.

Cómo Lanzarlo

docker run -d --name hybrids3 
    -p 8080:8080 
    -v ./config.yaml:/config/config.yaml:ro 
    -v hybrids3-data:/data 
    psyb0t/hybrids3

Config en /config/config.yaml, datos en /data, puerto 8080. Corre con UID 1000.

Config

master_key: "change-me-to-something-secret"
master_public_key: "master"
cleanup_interval: 1m
# path_prefix: /storage
buckets:
  uploads:
    public: true
    key: "uploads-secret"
    public_key: "uploads-id"
    ttl: 24h
    max_file_size: 50MB
  permanent:
    public: false
    key: "perm-secret"
    public_key: "permanent-id"
    ttl: 0
    max_file_size: 100MB

Cada bucket tiene una clave privada key (nunca se transmite, se usa para verificar los HMAC) y una public_key (el ID de clave de acceso S3, seguro para meter en URLs). Esa separación es lo que hace que funcionen las URLs prefirmadas: la clave pública puede aparecer en la URL, la privada nunca.
public: true significa que GET, HEAD y LIST no requieren auth. PUT y DELETE siguen necesitando una clave. public: false significa que todo requiere auth.

API HTTP

Bearer token pelado en la cabecera Authorization:

# upload
curl -X PUT https://ciprian.51k.eu80/uploads/file.txt 
  -H "Authorization: Bearer uploads-secret" 
  -d "hello"
# read from public bucket — no auth
curl https://ciprian.51k.eu80/uploads/file.txt
# read from private bucket
curl https://ciprian.51k.eu80/permanent/doc.pdf 
  -H "Authorization: Bearer perm-secret"
# list objects with prefix filter
curl "https://ciprian.51k.eu80/uploads?prefix=images/&max-keys=50" 
  -H "Authorization: Bearer uploads-secret"

Las peticiones con una cabecera Authorization de AWS Sig V4 reciben respuestas XML compatibles con S3. Todo lo demás recibe JSON.

boto3

import boto3
from botocore.config import Config
s3 = boto3.client(
    "s3",
    endpoint_url="https://ciprian.51k.eu80",
    aws_access_key_id="uploads-id",         # public_key from config
    aws_secret_access_key="uploads-secret",  # key from config
    region_name="us-east-1",
    config=Config(signature_version="s3v4"),
)
s3.put_object(Bucket="uploads", Key="file.txt", Body=b"hello")
s3.get_object(Bucket="uploads", Key="file.txt")
s3.list_objects_v2(Bucket="uploads", Prefix="images/")

Detrás de Nginx

La parte que de verdad importa. Pon path_prefix: /storage en la config, y luego:

location /storage {
    proxy_pass http://hybrids3:8080;
    proxy_set_header Host $http_host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
}

Dos cosas que hay que acertar: sin barra final en proxy_pass (una barra final le dice a nginx que quite el prefijo de la location, y eso rompe SigV4), y $http_host y no $host ($host quita el puerto, lo que provoca firmas que no cuadran en puertos no estándar).
Con path_prefix puesto, boto3 apunta a http://yourdomain/storage y todo funciona.

MCP

Hay un servidor MCP corriendo en /mcp/. Siete herramientas: upload_object, download_object, delete_object, list_objects, list_buckets, object_info, presign_url. Conecta cualquier cliente compatible con MCP:

{
  "mcpServers": {
    "hybrids3": {
      "type": "streamable-http",
      "url": "https://ciprian.51k.eu80/mcp/"
    }
  }
}

Auth a nivel de endpoint (cabecera Authorization o parámetro de query ?auth=) o por herramienta vía el parámetro auth_key. Usa la clave maestra para acceso completo, una clave de bucket para limitar la conexión a un solo bucket.

URLs Prefirmadas

Para buckets privados, esto genera una URL prefirmada AWS Sig V4 de verdad, firmada con la clave privada del bucket:

curl -X POST "https://ciprian.51k.eu80/presign/permanent/doc.pdf?expires=3600" 
  -H "Authorization: Bearer perm-secret"

Para buckets públicos devuelve la URL pelada, no hace falta firma porque GET no requiere auth de todas formas. El rango de caducidad va de un segundo a siete días, 3600 por defecto. Las URLs caducadas o manipuladas devuelven 403.
PUT prefirmado. Desde la v0.2.0, ?method=PUT le pasa a alguien una URL que le permite subir una clave concreta sin ver nunca tu clave de bucket. Los buckets públicos no son un atajo aquí: las lecturas anónimas están permitidas, las escrituras anónimas nunca, así que un PUT prefirmado siempre va firmado, incluso en un bucket público.

# generate an upload URL
curl -X POST "https://ciprian.51k.eu80/presign/uploads/inbox/report.pdf?method=PUT&expires=600" 
  -H "Authorization: Bearer uploads-secret"
# → {"url": "...X-Amz-Signature=...", "method": "PUT", "expires": 600}
# upload with the URL alone — no Authorization header
curl -X PUT "<url>" --data-binary @report.pdf

Una URL prefirmada está atada a su verbo. Una URL de GET no se puede usar para hacer un PUT ni al revés, el método forma parte de la petición canónica que cubre la firma. Que es justo la gracia: puedes darle a un tercero un buzón de entrega para exactamente una clave, durante exactamente diez minutos, y aun así no puede leer nada más ni subir a ningún otro sitio.
La herramienta MCP presign_url acepta el mismo parámetro method, así que un agente también puede acuñar URLs de subida.

La build está cerrada a cal y canto

La cadena de suministro quedó bloqueada por hash sobre uv con una barrera de edad a fecha fija, así que una dependencia publicada ayer no puede colarse en la build de hoy. Dockerfile de producción multi-stage, imagen base y uv fijadas por digest en vez de seguidas por tag.
El Makefile es el único punto de entrada, y todo lo de desarrollo corre dentro de un container de dev aislado. No se instala nada en tu host. 165 tests, 17 unitarios, 148 de integración.
El repo también trae un skill de agente y un plugin de OpenClaw bajo .agents/, publicados en ClawHub por CI en los pushes de tag.

Limitaciones

Sin subida multiparte, sin versionado de objetos, sin ACL más allá del público/privado a nivel de bucket, sin creación de buckets vía API, sin cabeceras CORS, sin replicación, sin cifrado en reposo. Si necesitas algo de eso, usa otra cosa.
Cógelo: github.com/psyb0t/docker-hybrids3. Bajo licencia WTFPL.

Cómo Instalarlo En Tu Agente

El mismo skill, ahora instalable sin acercarse a OpenClaw. Todo lo que hay bajo .agents/ está catalogado en un solo marketplace, así que son dos comandos:

claude plugin marketplace add psyb0t/agents
claude plugin install hybrids3@psyb0t

Codex usa el mismo marketplace con un verbo distinto, codex plugin add hybrids3@psyb0t, porque no existe codex plugin install. También encuentra el skill por su cuenta en un checkout del repo, ya que escanea .agents/skills/ de forma nativa sin nada instalado en absoluto. Además ahora está listado en el MCP Registry oficial, así que un cliente que resuelva servidores desde ahí puede encontrarlo sin que le den una URL.