{"id":"ec9378b5-507d-4bb6-af3e-f58085ea90c2","entityType":"agent","slug":"clawhub-psyb0t-hybrids3","name":"hybrids3","canonicalUrl":"https://www.xpersona.co/agent/clawhub-psyb0t-hybrids3","canonicalPath":"/agent/clawhub-psyb0t-hybrids3","generatedAt":"2026-10-12T13:09:35.036Z","source":"CLAWHUB","claimStatus":"UNCLAIMED","verificationTier":"NONE","summary":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-11T19:58:08.024Z","emptyReason":null},"description":"Self-hosted lightweight object storage speaking three interfaces at once — the S3 API (boto3 / AWS-SDK compatible, AWS Sig V4), a plain HTTP API (curl-friendly, Bearer auth), and MCP (Streamable HTTP tools for agents). Buckets/objects with SQLite metadata and flat-file storage on disk; buckets are config-defined (no create/delete API), each with a public/private flag, a private key + public key pair, optional TTL expiry, and a max upload size. Auth is per-bucket keys plus a cross-bucket master key; presigned GET/PUT URLs for handing out single-object access. Use when the user wants to put/get/list/delete files in a self-hosted S3-compatible store, generate presigned upload/download links, drive their own boto3-compatible object storage, or connect an agent to object storage over MCP.","descriptionLabel":"Source description","evidenceSummary":"Capability contract not published. No trust telemetry is available yet. 1K downloads reported by the source. Last updated 10/11/2026.","installCommand":"clawhub skill install s17fq93tmpky791n7516jcn08n83sfn2:hybrids3","sourceUrl":"https://clawhub.ai/psyb0t/hybrids3","homepage":"https://clawhub.ai/psyb0t/skills/hybrids3","primaryLinks":[{"label":"View on ClawHub","url":"https://clawhub.ai/psyb0t/hybrids3","kind":"source"},{"label":"Homepage","url":"https://clawhub.ai/psyb0t/skills/hybrids3","kind":"homepage"}],"safetyScore":84,"overallRank":62,"popularityScore":60,"trustScore":null,"claimedByName":null,"isOwner":false,"seoDescription":"hybrids3 technical dossier on Xpersona with agent coverage, OPENCLEW support, and live trust metadata."},"coverage":{"evidence":{"source":"public-profile","verified":false,"confidence":"medium","updatedAt":"2026-10-11T19:58:08.024Z","emptyReason":null},"protocols":[{"protocol":"OPENCLEW","label":"OpenClaw","status":"self-declared","notes":"Declared in the public agent profile."}],"capabilities":[],"verifiedCount":0,"selfDeclaredCount":1,"capabilityMatrix":{"rows":[{"key":"OPENCLEW","type":"protocol","support":"unknown","confidenceSource":"profile","notes":"Listed on profile"}],"flattenedTokens":"protocol:OPENCLEW|unknown|profile"}},"adoption":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-11T19:58:08.024Z","emptyReason":null},"stars":null,"forks":null,"downloads":1000,"likes":null,"task":null,"library":null,"packageName":null,"latestVersion":"0.3.8","tractionLabel":"1K downloads"},"release":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-11T19:58:08.010Z","emptyReason":null},"lastUpdatedAt":"2026-10-11T19:58:08.024Z","lastCrawledAt":"2026-10-11T19:58:08.010Z","lastIndexedAt":null,"nextCrawlAt":"2026-10-12T19:58:08.010Z","lastVerifiedAt":null,"highlights":[{"version":"0.3.8","createdAt":"2026-08-01T20:29:53.715Z","changelog":"- Removed the file: skill-card.md. - No functional or user-facing changes; documentation and interfaces remain the same.","fileCount":5,"zipByteSize":16281},{"version":"0.3.7","createdAt":"2026-07-27T23:37:56.843Z","changelog":"- Removed the skill-card.md file to simplify repository contents. - No changes to runtime interface or user experience.","fileCount":5,"zipByteSize":16075},{"version":"0.3.6","createdAt":"2026-07-27T23:06:24.989Z","changelog":"- Removed the documentation file skill-card.md. - No changes to functionality or user-facing features.","fileCount":5,"zipByteSize":16187},{"version":"0.3.5","createdAt":"2026-07-27T15:20:11.747Z","changelog":"- Removed the file skill-card.md. - No user-facing features or configuration changes.","fileCount":5,"zipByteSize":15891},{"version":"0.3.4","createdAt":"2026-07-27T13:28:24.793Z","changelog":"- Removed the file: skill-card.md. - No changes to core functionality or documentation besides the file removal.","fileCount":5,"zipByteSize":16313},{"version":"0.3.3","createdAt":"2026-07-26T12:06:51.020Z","changelog":"- Removed the file: skill-card.md - No other changes to core logic or functionality.","fileCount":5,"zipByteSize":16161},{"version":"0.3.2","createdAt":"2026-07-26T01:33:48.677Z","changelog":"Version 0.3.2 - Added a new \"Security & safety\" section to SKILL.md, detailing best practices and important warnings (public bucket risks, binding advice, destructive operations, credential security, and operator-vs-consumer distinction). - Removed the redundant skill-card.md file. - References/setup.md and other documentation content have been updated for improved clarity and cross-referencing.","fileCount":5,"zipByteSize":16247},{"version":"0.3.1","createdAt":"2026-07-25T23:12:08.422Z","changelog":"- Added explicit permissions section outlining required network and object storage capabilities. - Removed redundant skill-card.md file. - Minor documentation updates in SKILL.md; no user-facing functionality changes.","fileCount":5,"zipByteSize":14980}]},"execution":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No published capability contract is available yet."},"installCommand":"clawhub skill install s17fq93tmpky791n7516jcn08n83sfn2:hybrids3","setupComplexity":"low","setupSteps":["Install using `clawhub skill install s17fq93tmpky791n7516jcn08n83sfn2:hybrids3` in an isolated environment before connecting it to live workloads.","No published capability contract is available yet, so validate auth and request/response behavior manually.","Review the upstream CLAWHUB listing at https://clawhub.ai/psyb0t/hybrids3 before using production credentials."],"contract":{"contractStatus":"missing","authModes":[],"requires":[],"forbidden":[],"supportsMcp":false,"supportsA2a":false,"supportsStreaming":false,"inputSchemaRef":null,"outputSchemaRef":null,"dataRegion":null,"contractUpdatedAt":null,"sourceUpdatedAt":null,"freshnessSeconds":null},"invocationGuide":{"preferredApi":{"snapshotUrl":"https://www.xpersona.co/api/v1/agents/clawhub-psyb0t-hybrids3/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-psyb0t-hybrids3/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-psyb0t-hybrids3/trust"},"curlExamples":["curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-psyb0t-hybrids3/snapshot\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-psyb0t-hybrids3/contract\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-psyb0t-hybrids3/trust\""],"jsonRequestTemplate":{"query":"summarize this repo","constraints":{"maxLatencyMs":2000,"protocolPreference":["OPENCLEW"]}},"jsonResponseTemplate":{"ok":true,"result":{"summary":"...","confidence":0.9},"meta":{"source":"CLAWHUB","generatedAt":"2026-10-12T13:09:35.023Z"}},"retryPolicy":{"maxAttempts":3,"backoffMs":[500,1500,3500],"retryableConditions":["HTTP_429","HTTP_503","NETWORK_TIMEOUT"]}},"endpoints":{"dossierUrl":"https://www.xpersona.co/api/v1/agents/clawhub-psyb0t-hybrids3/dossier","snapshotUrl":"https://www.xpersona.co/api/v1/agents/clawhub-psyb0t-hybrids3/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-psyb0t-hybrids3/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-psyb0t-hybrids3/trust"}},"reliability":{"evidence":{"source":"runtime-metrics","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No trust, reliability, or runtime telemetry is available."},"trust":{"status":"unavailable","handshakeStatus":"UNKNOWN","verificationFreshnessHours":null,"reputationScore":null,"p95LatencyMs":null,"successRate30d":null,"fallbackRate":null,"attempts30d":null,"trustUpdatedAt":null,"trustConfidence":"unknown","sourceUpdatedAt":null,"freshnessSeconds":null},"decisionGuardrails":{"doNotUseIf":["Contract metadata is missing or unavailable for deterministic execution."],"safeUseWhen":[],"riskFlags":["missing_or_unavailable_contract","trust_data_unavailable","schema_references_missing"],"operationalConfidence":"low"},"executionMetrics":{"observedLatencyMsP50":null,"observedLatencyMsP95":null,"estimatedCostUsd":null,"uptime30d":null,"rateLimitRpm":null,"rateLimitBurst":null,"lastVerifiedAt":null,"verificationSource":null},"runtimeMetrics":{"successRate":null,"avgLatencyMs":null,"avgCostUsd":null,"hallucinationRate":null,"retryRate":null,"disputeRate":null,"p50Latency":null,"p95Latency":null,"lastUpdated":null}},"benchmarks":{"evidence":{"source":"no-benchmark-data","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No benchmark suites or observed failure patterns are available."},"suites":[],"failurePatterns":[]},"artifacts":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-11T19:58:08.024Z","emptyReason":null},"readme":"Skill: hybrids3\n\nOwner: psyb0t\n\nSummary: Self-hosted lightweight object storage speaking three interfaces at once — the S3 API (boto3 / AWS-SDK compatible, AWS Sig V4), a plain HTTP API (curl-friendly, Bearer auth), and MCP (Streamable HTTP tools for agents). Buckets/objects with SQLite metadata and flat-file storage on disk; buckets are config-defined (no create/delete API), each with a public/private flag, a private key + public key pair, optional TTL expiry, and a max upload size. Auth is per-bucket keys plus a cross-bucket master key; presigned GET/PUT URLs for handing out single-object access. Use when the user wants to put/get/list/delete files in a self-hosted S3-compatible store, generate presigned upload/download links, drive their own boto3-compatible object storage, or connect an agent to object storage over MCP.\n\nTags: latest:0.3.8\n\nVersion history:\n\nv0.3.8 | 2026-08-01T20:29:53.715Z | auto\n\n- Removed the file: skill-card.md.\n- No functional or user-facing changes; documentation and interfaces remain the same.\n\nv0.3.7 | 2026-07-27T23:37:56.843Z | auto\n\n- Removed the skill-card.md file to simplify repository contents.\n- No changes to runtime interface or user experience.\n\nv0.3.6 | 2026-07-27T23:06:24.989Z | auto\n\n- Removed the documentation file skill-card.md.\n- No changes to functionality or user-facing features.\n\nv0.3.5 | 2026-07-27T15:20:11.747Z | auto\n\n- Removed the file skill-card.md.\n- No user-facing features or configuration changes.\n\nv0.3.4 | 2026-07-27T13:28:24.793Z | auto\n\n- Removed the file: skill-card.md.\n- No changes to core functionality or documentation besides the file removal.\n\nv0.3.3 | 2026-07-26T12:06:51.020Z | auto\n\n- Removed the file: skill-card.md\n- No other changes to core logic or functionality.\n\nv0.3.2 | 2026-07-26T01:33:48.677Z | auto\n\nVersion 0.3.2\n\n- Added a new \"Security & safety\" section to SKILL.md, detailing best practices and important warnings (public bucket risks, binding advice, destructive operations, credential security, and operator-vs-consumer distinction).\n- Removed the redundant skill-card.md file.\n- References/setup.md and other documentation content have been updated for improved clarity and cross-referencing.\n\nv0.3.1 | 2026-07-25T23:12:08.422Z | auto\n\n- Added explicit permissions section outlining required network and object storage capabilities.\n- Removed redundant skill-card.md file.\n- Minor documentation updates in SKILL.md; no user-facing functionality changes.\n\nv0.3.0 | 2026-07-25T22:08:47.240Z | auto\n\n- Removed the file: skill-card.md.\n- No changes to existing functionality or user workflow.\n- Cleanup of documentation files; all usage and setup instructions remain in SKILL.md.\n\nv0.2.2 | 2026-07-25T18:50:41.217Z | auto\n\n**hybrids3 v0.2.2 Changelog**\n\n- Updated documentation for clarity on architecture, setup, and usage scenarios.\n- Improved description of access modes: S3 API, plain HTTP, and MCP tool interface.\n- Clarified bucket configuration details (public/private, keys, TTLs, size limits) and limitations (no runtime create/delete, no multipart, no versioning).\n- Added setup instructions and environment variable configuration.\n- Revised examples for boto3 and AWS CLI usage.\n- Enhanced \"When To Use\" and \"When NOT To Use\" guidance for better user expectations.\n\nArchive index:\n\nArchive v0.3.8: 5 files, 16281 bytes\n\nFiles: references/setup.md (8312b), scripts/hybrids3.sh (6187b), skill-card.md (2745b), SKILL.md (22017b), _meta.json (127b)\n\nFile v0.3.8:SKILL.md\n\n---\nname: hybrids3\ndescription: Self-hosted lightweight object storage speaking three interfaces at once — the S3 API (boto3 / AWS-SDK compatible, AWS Sig V4), a plain HTTP API (curl-friendly, Bearer auth), and MCP (Streamable HTTP tools for agents). Buckets/objects with SQLite metadata and flat-file storage on disk; buckets are config-defined (no create/delete API), each with a public/private flag, a private key + public key pair, optional TTL expiry, and a max upload size. Auth is per-bucket keys plus a cross-bucket master key; presigned GET/PUT URLs for handing out single-object access. Use when the user wants to put/get/list/delete files in a self-hosted S3-compatible store, generate presigned upload/download links, drive their own boto3-compatible object storage, or connect an agent to object storage over MCP.\nhomepage: https://github.com/psyb0t/docker-hybrids3\nuser-invocable: true\npermissions:\n  - \"network: outbound HTTP(S) to the configured HYBRIDS3_URL object-storage endpoint\"\n  - \"object storage: read/write/delete objects (upload, download, list, delete) and generate presigned GET/PUT URLs, scoped to whatever bucket key is provided\"\nmetadata:\n  { \"openclaw\": { \"emoji\": \"🪣\", \"primaryEnv\": \"HYBRIDS3_URL\", \"requires\": { \"bins\": [\"docker\", \"curl\"] } } }\n---\n\n# hybrids3\n\nLightweight object storage that speaks three interfaces from one container: the **S3 API** (boto3 / any AWS SDK, AWS Signature V4), a **plain HTTP API** (`curl` + Bearer token), and **MCP** (Streamable HTTP tool definitions for AI agents). Metadata lives in SQLite, object bytes are flat files on disk.\n\nBuckets are **configuration, not state** — they're defined in the server's `config.yaml` and there is no API to create or delete them. Each bucket carries: a `public`/private flag, a private `key` (Bearer token + S3 signing secret), a `public_key` (the `aws_access_key_id` identifier), an optional `ttl` (objects auto-expire after last write), and an optional `max_file_size`. A single cross-bucket `master_key` operates on every bucket and is the only credential that can list all buckets.\n\nObject keys support nested paths (`reports/2024/jan.pdf`). Content type is auto-detected on upload (libmagic sniff + extension fallback). Presigned GET/PUT URLs hand out time-limited single-object access without exposing a key.\n\nFor installation, configuration, and container setup, see [references/setup.md](references/setup.md).\n\n## Security & safety\n\n- **Public buckets are world-readable.** A bucket with `public: true` serves `GET`/`HEAD`/`LIST` to anyone who can reach the port — no auth at all. Only store data there that's meant to be openly readable; keep sensitive data in a `public: false` bucket.\n- **Bind loopback, not all interfaces.** The container listens on `8080`. Publishing it as `-p 8080:8080` / compose `\"8080:8080\"` makes it reachable from the network by default. Use `-p 127.0.0.1:8080:8080` (loopback) unless remote access is intentionally required, and for real deployments put the service on an internal Docker network behind a reverse proxy instead of exposing the port directly — see [references/setup.md](references/setup.md#quick-install) and [Behind a Reverse Proxy](references/setup.md#behind-a-reverse-proxy).\n- **`DELETE`, presigned `PUT`, and MCP `delete_object` are destructive & irreversible.** There's no undo, no versioning, no recycle bin — a deleted object (or one overwritten via `PUT`) is gone. An agent must NEVER call `DELETE` / `delete_object`, or generate/use a presigned `PUT` that overwrites, unless the user explicitly asked for that exact key to be removed or replaced; confirm the specific bucket + key first, never enumerate-then-bulk-delete, and treat bulk cleanup as requiring explicit per-object user confirmation.\n- **Bucket keys, master key, and presigned URLs are bearer credentials.** Whoever holds a bucket's private `key` or the `master_key` (or a live presigned URL) can act as that bucket/master until the credential is rotated or the URL expires — there's no per-caller revocation. Never paste these into shared prompts, logs, or presigned URLs handed to untrusted parties beyond the one object they're meant to grant.\n- **This skill is a consumer, not an operator.** It talks to an instance the user already runs; it doesn't provision, harden, or expose the server. Don't use this skill to reconfigure `config.yaml`, change bucket visibility, or alter port bindings on the user's behalf without being explicitly asked.\n\n## When To Use\n\n- Put / get / list / delete files in a self-hosted, S3-compatible object store.\n- Drive existing boto3 / AWS-SDK code against your own endpoint (change `endpoint_url` + credentials, nothing else).\n- Store artifacts, uploads, reports, backups that services and agents read/write.\n- Generate a **presigned PUT URL** so a third party can upload one specific key without your bucket key.\n- Generate a **presigned GET URL** so a third party can download one private object for a limited time.\n- Connect an AI agent to object storage over **MCP** — structured `upload_object` / `download_object` / `list_objects` / etc. tools.\n- Auto-expiring scratch storage — set a bucket `ttl` and objects clean themselves up.\n\n## When NOT To Use\n\n- Creating or deleting buckets at runtime — buckets live in `config.yaml`; add/remove them there and restart. `PUT /{bucket}` is an S3-compat no-op.\n- Multipart / chunked uploads (S3's multipart protocol) — not implemented. Upload the whole object in one request.\n- Object versioning, ACLs beyond bucket-level public/private, replication, or encryption at rest — none are supported.\n- Downloading objects **over 50 MB via MCP** — the MCP `download_object` tool caps at 50 MB. Use the S3 or plain HTTP `GET` for large objects.\n- CORS from a browser — no CORS headers are emitted; add them at the reverse-proxy layer.\n- Provisioning, configuring, or hardening the server itself — this skill is a **consumer**. It talks to an instance the user already runs and trusts.\n\n## Setup\n\nThe container should already be running. Point the skill at it:\n\n```bash\nexport HYBRIDS3_URL=http://localhost:8080\n```\n\n**Verify:** `curl $HYBRIDS3_URL/health` returns `{\"status\":\"ok\"}`.\n\nKeys are per-bucket and come from the operator's `config.yaml`. Export the ones you'll use rather than pasting them inline:\n\n```bash\nexport HYBRIDS3_KEY=<bucket-private-key>          # Bearer token / aws_secret_access_key\nexport HYBRIDS3_PUBLIC_KEY=<bucket-public-key>    # aws_access_key_id (defaults to bucket name)\n# For cross-bucket / list-all-buckets operations, use the master credentials instead:\nexport HYBRIDS3_MASTER_KEY=<master-key>\nexport HYBRIDS3_MASTER_PUBLIC_KEY=<master-public-key>   # defaults to \"master\"\n```\n\nIf the server runs behind a reverse proxy at a subpath, `HYBRIDS3_URL` includes it (e.g. `http://host/storage`) and every route — health included — lives under that prefix.\n\nFor install / `config.yaml` shape / bucket + key definitions / ports / reverse proxy, see [references/setup.md](references/setup.md).\n\n## Three Access Modes\n\nOne service, three front doors on the **same port** (`8080`). Pick per client:\n\n| Mode | Endpoint | Auth | Reach for it when… |\n|---|---|---|---|\n| **S3 API** | root (`/`), signed requests | AWS Sig V4 (`public_key` = access key id, `key` = secret) | You already have boto3 / AWS-SDK code, or want SDK ergonomics (pagination, presign helpers, retries). |\n| **Plain HTTP** | `/{bucket}/{key}` etc. | `Authorization: Bearer <key>` | Shell scripts, `curl`, quick one-offs. No SDK, no signing — just a Bearer header (and public-bucket reads need none). |\n| **MCP** | `POST /mcp/` | per-connection + per-tool `auth_key` | An AI agent should manipulate storage through typed tool calls instead of raw HTTP. |\n\nThey share one SQLite metadata store and one flat-file tree — an object written over S3 is readable over plain HTTP and MCP, and vice versa.\n\n## S3 API\n\nS3 clients authenticate with **AWS Signature V4**. The bucket's `public_key` is the `aws_access_key_id` (travels in the request in plaintext, in the `Credential=` field); the bucket's private `key` is the `aws_secret_access_key` (used only to compute the HMAC locally — never transmitted). `region_name` is required by SDKs but the value is arbitrary — use `us-east-1`.\n\n```\nAuthorization: AWS4-HMAC-SHA256 Credential=uploads-id/20240101/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-date, Signature=<hmac>\n```\n\n### boto3\n\n```python\nimport boto3\nfrom botocore.config import Config\n\n# per-bucket client — access limited to the \"uploads\" bucket\ns3 = boto3.client(\n    \"s3\",\n    endpoint_url=\"http://localhost:8080\",     # HYBRIDS3_URL\n    aws_access_key_id=\"uploads-id\",           # bucket public_key\n    aws_secret_access_key=\"uploads-secret\",   # bucket key (private)\n    region_name=\"us-east-1\",                  # arbitrary but required\n    config=Config(signature_version=\"s3v4\"),\n)\n\ns3.put_object(Bucket=\"uploads\", Key=\"file.txt\", Body=b\"hello\")\ns3.get_object(Bucket=\"uploads\", Key=\"file.txt\")[\"Body\"].read()\ns3.head_object(Bucket=\"uploads\", Key=\"file.txt\")\ns3.list_objects_v2(Bucket=\"uploads\", Prefix=\"images/\")\ns3.delete_object(Bucket=\"uploads\", Key=\"file.txt\")\ns3.list_buckets()                             # bucket key → only \"uploads\"\n\n# presigned URLs (see MCP / HTTP presign for the same semantics)\nget_url = s3.generate_presigned_url(\n    \"get_object\", Params={\"Bucket\": \"permanent\", \"Key\": \"doc.pdf\"}, ExpiresIn=3600,\n)\nput_url = s3.generate_presigned_url(\n    \"put_object\", Params={\"Bucket\": \"uploads\", \"Key\": \"inbox/report.pdf\"}, ExpiresIn=600,\n)\n```\n\nFor cross-bucket access or `list_buckets()` returning **all** buckets, build a second client with the **master** credentials (`aws_access_key_id=master_public_key`, `aws_secret_access_key=master_key`). Don't embed the master key in client-facing code — per-bucket keys scope access to one bucket.\n\n**Destructive & irreversible.** `delete_object` deletes the object with no undo — no versioning, no recycle bin. An agent must NEVER call it unless the user explicitly asked for that exact key to be deleted; confirm the specific bucket + key first, scope it to the current task, and never enumerate-then-bulk-delete. On a shared instance where multiple callers use the same bucket, this can destroy another caller's data — treat bulk deletes as admin-only and requiring explicit confirmation.\n\n### aws-cli\n\n```bash\naws --endpoint-url \"$HYBRIDS3_URL\" \\\n    --region us-east-1 \\\n    s3api put-object --bucket uploads --key file.txt --body ./file.txt\n# credentials via env: AWS_ACCESS_KEY_ID=<public_key> AWS_SECRET_ACCESS_KEY=<key>\n```\n\nSupported S3 operations: `put_object`, `get_object`, `head_object`, `delete_object`, `list_objects_v2` (honours `Prefix`), `list_buckets`, `generate_presigned_url` (GET + PUT). Not supported: multipart upload, versioning, ACLs, bucket create/delete (boto3's implicit `create_bucket` maps to a no-op).\n\n## Plain HTTP API\n\nBearer auth: pass the bucket's private `key` (or the `master_key`) as `Authorization: Bearer <key>`. Public-bucket **reads** (GET/HEAD/LIST) need no auth; **all writes** and **private-bucket reads** require a key.\n\nRequests carrying an AWS Sig V4 `Authorization` header get S3-style XML back; everything else gets JSON. Every response includes `X-Request-Id` (log correlation) and `X-Content-Type-Options: nosniff`. Errors carry `\"error\"` + `\"request_id\"` fields.\n\n### Objects\n\n| Method | Path | Auth | Description |\n|---|---|---|---|\n| `PUT` | `/{bucket}/{key}` | write | Upload object. Returns `ETag` (MD5 of content). Content type auto-detected; override with a `Content-Type` header. |\n| `GET` | `/{bucket}/{key}` | read | Download object. Returns `ETag`, `Last-Modified`, `Content-Length`. |\n| `HEAD` | `/{bucket}/{key}` | read | Object metadata, no body. |\n| `DELETE` | `/{bucket}/{key}` | write | Delete object — always `204`, even if absent. |\n| `GET` | `/{bucket}` | read | List objects. Query: `prefix`, `max-keys`. |\n| `POST` | `/presign/{bucket}/{key}` | write | Generate a presigned URL. Query: `method` (`GET` default / `PUT`), `expires` (seconds, 1–604800, default 3600). |\n\n**Destructive & irreversible.** `DELETE` removes the object with no undo. An agent must NEVER call it unless the user explicitly asked for that exact key to be deleted; confirm the specific bucket + key first, scope it to the current task, and never enumerate-then-bulk-delete. On a shared bucket used by other callers, this can destroy their data — treat it as admin-only.\n\n```bash\n# upload\ncurl -X PUT \"$HYBRIDS3_URL/uploads/file.txt\" \\\n  -H \"Authorization: Bearer $HYBRIDS3_KEY\" \\\n  --data-binary @file.txt\n\n# upload a nested key (parent dirs created automatically)\ncurl -X PUT \"$HYBRIDS3_URL/uploads/reports/2024/january.pdf\" \\\n  -H \"Authorization: Bearer $HYBRIDS3_KEY\" \\\n  --data-binary @january.pdf\n\n# download from a public bucket — no auth needed\ncurl \"$HYBRIDS3_URL/uploads/file.txt\"\n\n# download from a private bucket\ncurl \"$HYBRIDS3_URL/permanent/doc.pdf\" \\\n  -H \"Authorization: Bearer $HYBRIDS3_KEY\"\n\n# list objects with a prefix\ncurl \"$HYBRIDS3_URL/uploads?prefix=images/&max-keys=50\" \\\n  -H \"Authorization: Bearer $HYBRIDS3_KEY\"\n\n# delete\ncurl -X DELETE \"$HYBRIDS3_URL/uploads/file.txt\" \\\n  -H \"Authorization: Bearer $HYBRIDS3_KEY\"\n```\n\n### Buckets\n\n| Method | Path | Auth | Description |\n|---|---|---|---|\n| `GET` | `/health` | none | `{\"status\":\"ok\"}` liveness check. |\n| `GET` | `/` | master or bucket key | List buckets — master key lists all, bucket key lists only its own. |\n| `HEAD` | `/{bucket}` | read | `200` if bucket exists in config, else `404`. |\n| `PUT` | `/{bucket}` | write | S3-compat no-op: `200` if bucket exists in config, `404` if not. Creates nothing. |\n\n```bash\n# list all buckets — needs the master key\ncurl \"$HYBRIDS3_URL/\" -H \"Authorization: Bearer $HYBRIDS3_MASTER_KEY\"\n\n# a bucket key lists only its own bucket\ncurl \"$HYBRIDS3_URL/\" -H \"Authorization: Bearer $HYBRIDS3_KEY\"\n```\n\n### Presigned URLs (HTTP)\n\n`POST /presign/{bucket}/{key}` requires the bucket key or master key. The result grants exactly one verb on exactly one key. A GET URL can't be used to PUT (the method is baked into the signature).\n\n```bash\n# presigned GET on a PRIVATE bucket → signed, expiring URL\ncurl -X POST \"$HYBRIDS3_URL/presign/permanent/doc.pdf?expires=3600\" \\\n  -H \"Authorization: Bearer $HYBRIDS3_KEY\"\n# → {\"url\":\"http://.../permanent/doc.pdf?X-Amz-Algorithm=...&X-Amz-Signature=...\",\"method\":\"GET\",\"expires\":3600}\n\n# presigned GET on a PUBLIC bucket → plain URL, no signature, no expiry (reads are open anyway)\ncurl -X POST \"$HYBRIDS3_URL/presign/uploads/photo.jpg\" \\\n  -H \"Authorization: Bearer $HYBRIDS3_KEY\"\n# → {\"url\":\"http://.../uploads/photo.jpg\",\"method\":\"GET\",\"expires\":null}\n\n# presigned PUT → ALWAYS signed, even for public buckets (anonymous writes are never allowed)\ncurl -X POST \"$HYBRIDS3_URL/presign/uploads/inbox/report.pdf?method=PUT&expires=600\" \\\n  -H \"Authorization: Bearer $HYBRIDS3_KEY\"\n\n# recipient uploads using only the URL — no Authorization header\ncurl -X PUT \"<presigned-put-url>\" --data-binary @report.pdf\n```\n\nThe bucket's `max_file_size` is enforced server-side during the upload regardless of how it was authenticated (oversized → `413`). Expired or tampered presigned URLs → `403`.\n\n## MCP Endpoint\n\nAn MCP server runs at `POST /mcp/` over the **Streamable HTTP** transport. Client config:\n\n```json\n{\n  \"mcpServers\": {\n    \"hybrids3\": { \"type\": \"streamable-http\", \"url\": \"http://localhost:8080/mcp/\" }\n  }\n}\n```\n\nWire it into Claude Code:\n\n```bash\nclaude mcp add --transport http hybrids3 \"$HYBRIDS3_URL/mcp/\"\n# with endpoint-level auth:\nclaude mcp add --transport http hybrids3 \"$HYBRIDS3_URL/mcp/\" \\\n  --header \"Authorization: Bearer $HYBRIDS3_MASTER_KEY\"\n```\n\n### Auth (two layers)\n\n**Endpoint-level (optional)** — a token that authenticates the *connection* before any tool runs. Master key = full access; a bucket key = limited to that bucket. Send it as `Authorization: Bearer <key>`, or (for clients that can't set headers) as `?auth=<key>` on the URL. A token that matches nothing → `401`; **no token → passes through**, and per-tool auth applies.\n\n**Per-tool** — every tool that touches a bucket takes an `auth_key` argument (the bucket's private `key` or the master key), checked independently of the connection token. Public-bucket reads (`download_object`, `list_objects`, `object_info`) accept an empty `auth_key`; all writes and all private-bucket ops require it.\n\n### Tools\n\n| Tool | Args | Auth | Notes |\n|---|---|---|---|\n| `upload_object` | `bucket`, `key`, `content`, `auth_key`, `content_type=\"\"`, `encoding=\"utf-8\"` | bucket/master key | `encoding=\"base64\"` for binary. Returns `size`, `etag`, `content_type`. |\n| `download_object` | `bucket`, `key`, `auth_key=\"\"`, `encoding=\"utf-8\"` | key on private only | `base64` for binary; UTF-8 that won't decode auto-falls back to base64. **Objects > 50 MB rejected — use HTTP.** |\n| `delete_object` | `bucket`, `key`, `auth_key` | bucket/master key | Idempotent. |\n| `list_objects` | `bucket`, `auth_key=\"\"`, `prefix=\"\"`, `max_keys=100` | key on private only | `max_keys` clamped to 1–1000. |\n| `list_buckets` | `auth_key` | master or bucket key | Master lists all; bucket key lists only its own. |\n| `object_info` | `bucket`, `key`, `auth_key=\"\"` | key on private only | Metadata only (size, content_type, etag, uploaded_at, expires_at). |\n| `presign_url` | `bucket`, `key`, `auth_key`, `method=\"GET\"`, `expires=3600`, `base_url=\"\"`, `host=\"\"` | bucket/master key | `method` GET/PUT. Same public-GET-plain / everything-else-signed logic as the HTTP endpoint. `expires` clamped 1–604800. Set `base_url` to the externally-reachable URL so the link is usable. |\n\n**`delete_object` is destructive & irreversible.** It deletes the object with no undo. An agent must NEVER call it unless the user explicitly asked for that exact key to be deleted; confirm the specific bucket + key first, never enumerate-then-bulk-delete. On a shared bucket used by other callers, this can destroy their data — treat it as admin-only.\n\nAll tools return `structuredContent` with a plain-text fallback. Internal error details are masked.\n\n### Raw JSON-RPC\n\nFor debugging or non-MCP callers, POST JSON-RPC directly. The transport requires the `Accept` header below.\n\n```bash\n# tools/list\ncurl -s \"$HYBRIDS3_URL/mcp/\" \\\n  -H \"Content-Type: application/json\" \\\n  -H \"Accept: application/json, text/event-stream\" \\\n  -d '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/list\"}'\n\n# tools/call — upload\ncurl -s \"$HYBRIDS3_URL/mcp/\" \\\n  -H \"Content-Type: application/json\" \\\n  -H \"Accept: application/json, text/event-stream\" \\\n  -d '{\n    \"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools/call\",\n    \"params\":{\n      \"name\":\"upload_object\",\n      \"arguments\":{\"bucket\":\"uploads\",\"key\":\"note.txt\",\"content\":\"hello\",\"auth_key\":\"'\"$HYBRIDS3_KEY\"'\"}\n    }\n  }'\n```\n\n## Authentication (all modes)\n\n| Credential | Config field | Role | Keep secret? |\n|---|---|---|---|\n| Bucket private key | `key` | Bearer token (HTTP/MCP) **and** `aws_secret_access_key` (S3). Signs/verifies — never transmitted. | **Yes** |\n| Bucket public key | `public_key` | `aws_access_key_id` (S3) + `Credential=` in presigned URLs. Identifies, grants nothing. Defaults to bucket name. | No |\n| Master key | `master_key` | Cross-bucket god key. Works on every bucket + only credential that lists all buckets. | **Yes** |\n| Master public key | `master_public_key` | `aws_access_key_id` paired with the master key (S3). Defaults to `\"master\"`. | No |\n\nBucket visibility:\n\n| | GET / HEAD / LIST | PUT | DELETE / presign |\n|---|---|---|---|\n| `public: true` | no auth | bucket key, master key, or valid presigned PUT | bucket key or master key |\n| `public: false` | bucket key, master key, or valid presigned GET | bucket key, master key, or valid presigned PUT | bucket key or master key |\n\nUnauthorized or non-existent bucket access always returns `404` — callers can't tell \"doesn't exist\" from \"no access\". All key comparisons are constant-time.\n\n## Typical Workflows\n\n### Upload + fetch over plain HTTP\n\n```bash\nexport HYBRIDS3_URL=http://localhost:8080\nexport HYBRIDS3_KEY=<uploads-key>\n\ncurl -X PUT \"$HYBRIDS3_URL/uploads/report.pdf\" \\\n  -H \"Authorization: Bearer $HYBRIDS3_KEY\" --data-binary @report.pdf\ncurl -sO \"$HYBRIDS3_URL/uploads/report.pdf\"     # public bucket → no auth on read\n```\n\nOr via the helper (see [`scripts/hybrids3.sh`](scripts/hybrids3.sh)):\n\n```bash\nHYBRIDS3_URL=http://localhost:8080 HYBRIDS3_KEY=<key> \\\n  scripts/hybrids3.sh put uploads report.pdf ./report.pdf\nscripts/hybrids3.sh list uploads reports/\nscripts/hybrids3.sh get uploads report.pdf ./out.pdf\nscripts/hybrids3.sh delete uploads report.pdf\nscripts/hybrids3.sh buckets                     # needs HYBRIDS3_KEY=<master-key>\n```\n\n### Hand someone a presigned upload URL\n\n```bash\n# you generate it (needs a key); they upload with no credentials\ncurl -X POST \"$HYBRIDS3_URL/presign/uploads/inbox/theirs.zip?method=PUT&expires=600\" \\\n  -H \"Authorization: Bearer $HYBRIDS3_KEY\" | jq -r .url\n# → hand the URL over; recipient: curl -X PUT \"<url>\" --data-binary @theirs.zip\n```\n\n### Drop-in boto3 against your own endpoint\n\nPoint any existing boto3 code at `HYBRIDS3_URL`, set the bucket's `public_key`/`key` as the credential pair, `region_name=\"us-east-1\"`, `signature_version=\"s3v4\"`. See [S3 API](#s3-api).\n\n### Agent-driven storage over MCP\n\nAdd the server (`claude mcp add --transport http hybrids3 \"$HYBRIDS3_URL/mcp/\"`), then call `list_buckets` to discover buckets, `upload_object` / `download_object` / `list_objects` to move data. Pass the bucket `key` as `auth_key`; use the master key only when you need cross-bucket reach.\n\n### Auto-expiring scratch storage\n\nIf a bucket has a `ttl` set (server config), objects delete themselves that long after their last write — overwriting resets the clock. Nothing to do client-side; just write to the bucket.\n\nFile v0.3.8:_meta.json\n\n{\n  \"ownerId\": \"kn79dhvmpjng4rp2jjk8k0v5xx80ccbk\",\n  \"slug\": \"hybrids3\",\n  \"version\": \"0.3.8\",\n  \"publishedAt\": 1785616193715\n}\n\nFile v0.3.8:references/setup.md\n\n# hybrids3 setup\n\nConsumer-side reference for standing up an instance to talk to. If an operator already runs one, you only need its URL and a bucket key — skip to [OpenClaw / ClawHub Config](#openclaw--clawhub-config).\n\n## Requirements\n\n- Docker\n- A `config.yaml` (buckets, keys, master key — see [Configuration](#configuration))\n- A persistent volume/dir for `/data` (object bytes + SQLite metadata)\n- ~1 core / modest RAM — it's a thin FastAPI service, not a cluster\n\nThe container runs as **UID 1000** and expects:\n\n- Config file mounted at `/config/config.yaml`\n- Data at `/data`\n- Listens on port `8080`\n\n## Quick Install\n\nGrab the example config, edit keys + buckets, run:\n\n```bash\n# fetch the example config\nwget -O config.yaml \\\n  https://raw.githubusercontent.com/psyb0t/docker-hybrids3/master/config.example.yaml\n\n# edit it — set master_key and each bucket's key\nvi config.yaml\n\n# run\ndocker run -d --name hybrids3 \\\n    -p 127.0.0.1:8080:8080 \\\n    -v ./config.yaml:/config/config.yaml:ro \\\n    -v hybrids3-data:/data \\\n    psyb0t/hybrids3\n\n# verify\ncurl http://localhost:8080/health      # → {\"status\":\"ok\"}\n```\n\n**`-p 8080:8080` binds all interfaces** — the service becomes reachable from the network, not just localhost. Use `-p 127.0.0.1:8080:8080` (loopback-only) unless remote access is intentionally required, matching the fleet convention.\n\n### docker-compose\n\n```yaml\nservices:\n  hybrids3:\n    image: psyb0t/hybrids3\n    ports:\n      - \"127.0.0.1:8080:8080\"\n    volumes:\n      - ./config.yaml:/config/config.yaml:ro\n      - hybrids3-data:/data\n    restart: unless-stopped\n\nvolumes:\n  hybrids3-data:\n```\n\n**`\"127.0.0.1:8080:8080\"` binds loopback-only.** Use `\"8080:8080\"` only if remote access is intentionally required, matching the fleet convention above. For a real deployment where other hosts or services need to reach it, put hybrids3 on an internal Docker network and front it with a reverse proxy (see [Behind a Reverse Proxy](#behind-a-reverse-proxy)) rather than publishing the port on all interfaces.\n\n**Uploads to a `public: true` bucket are world-readable.** Any bucket with `public: true` serves GET/HEAD/LIST with no authentication at all — anyone who can reach the port can read anything stored there. Don't put sensitive data in a public bucket, and keep the port bound to loopback (or behind an authenticating proxy) unless that's the intended exposure.\n\n## Configuration\n\n**All configuration is the YAML file — there are no application env vars.** Buckets are defined here and only here; there is no create/delete API. Change config and restart to add or remove a bucket.\n\nTop-level keys:\n\n| Field | Default | What it does |\n|---|---|---|\n| `master_key` | `\"\"` | Cross-bucket god key — works on every bucket, only credential that lists all buckets. Keep secret. |\n| `master_public_key` | `\"master\"` | Non-secret `aws_access_key_id` paired with `master_key` in S3 auth. Safe to share. |\n| `cleanup_interval` | `60` (`1m`) | How often the background loop deletes expired objects + orphan files. Duration string (`30s`, `5m`, `1h`) or raw seconds. |\n| `lock_acquire_timeout` | `30` | Seconds a request waits for a per-key lock before `503`. Float. |\n| `lock_hold_timeout` | `300` | Seconds a request may hold a lock before it's force-released with `503`. Guards stalled uploads. |\n| `lock_max_waiters` | `100` | Max requests queued for one key before new ones get `503` immediately. |\n| `path_prefix` | `\"\"` | Serve all routes under a subpath (e.g. `/storage`) for reverse-proxy deployments. `\"/storage\"` and `\"/storage/\"` both normalize. Empty = root. |\n\nPer-bucket fields (under `buckets:`, keyed by bucket name — name must not contain `/` or `..`):\n\n| Field | Default | What it does |\n|---|---|---|\n| `key` | **required** | Private key: Bearer token (HTTP/MCP) + `aws_secret_access_key` (S3). Never transmitted. Keep secret. |\n| `public` | `false` | `true` = anyone reads (GET/HEAD/LIST) without auth; writes still need a key. `false` = auth for everything. |\n| `public_key` | bucket name | Public `aws_access_key_id`; appears in presigned `Credential=`. Safe to share. |\n| `ttl` | `0` | Objects expire this long after last write (overwrite resets). `0` = never. Formats: `30s 5m 1h 1h30m12s 1d 2d12h`. |\n| `max_file_size` | `0` | Reject uploads over this with `413`. Formats: `500B 50KB 10MB 1GB`, `0` = no limit. |\n\nMinimal example (`config.example.yaml`):\n\n```yaml\nmaster_key: \"change-me-to-something-secret\"\nmaster_public_key: \"master\"\ncleanup_interval: 1m\nlock_acquire_timeout: 30\nlock_hold_timeout: 300\nlock_max_waiters: 100\n# path_prefix: /storage\n\nbuckets:\n  uploads:\n    public: true\n    key: \"uploads-secret\"\n    public_key: \"uploads-id\"\n    ttl: 24h\n    max_file_size: 50MB\n\n  permanent:\n    public: false\n    key: \"perm-secret\"\n    public_key: \"permanent-id\"\n    ttl: 0\n    max_file_size: 100MB\n```\n\n## Environment Variables\n\nThe **application** takes no env vars — everything is in `config.yaml`. The only tunables at run time are Docker-level:\n\n| Concern | How | Example |\n|---|---|---|\n| Config path | mount to `/config/config.yaml` | `-v ./config.yaml:/config/config.yaml:ro` |\n| Data location | mount to `/data` | `-v hybrids3-data:/data` |\n| Host port / interface | `docker run -p` | `-p 127.0.0.1:8080:8080` (loopback), `-p 8080:8080` (all interfaces) |\n\nFor S3 clients (`boto3`, `aws-cli`), the credentials aren't hybrids3 env vars — they're the standard AWS SDK ones, sourced from a bucket's config:\n\n| SDK var / arg | Value from config |\n|---|---|\n| `AWS_ACCESS_KEY_ID` / `aws_access_key_id` | the bucket's `public_key` (or `master_public_key`) |\n| `AWS_SECRET_ACCESS_KEY` / `aws_secret_access_key` | the bucket's `key` (or `master_key`) |\n| `AWS_DEFAULT_REGION` / `region_name` | any value — server doesn't check it; `us-east-1` below is just an example |\n| `endpoint_url` | `HYBRIDS3_URL` (e.g. `http://localhost:8080`) |\n\nThe skill's own convenience vars for HTTP/MCP work: `HYBRIDS3_URL`, `HYBRIDS3_KEY`, `HYBRIDS3_PUBLIC_KEY`, `HYBRIDS3_MASTER_KEY`, `HYBRIDS3_MASTER_PUBLIC_KEY`.\n\n## Ports\n\n| Port | Service |\n|---|---|\n| 8080 | HTTP API + S3 API + MCP (`/mcp/`) — all on the same port |\n\nThe container listens on `8080` internally. Map it however you like with `-p`.\n\n## Behind a Reverse Proxy\n\nSet `path_prefix` in config to match the proxy location; hybrids3 serves every route under it natively (including `/{prefix}/health`), so nginx forwards the path **as-is** — no stripping. SigV4 works because the signed path matches what the server sees.\n\n```yaml\n# config.yaml\npath_prefix: /storage\n```\n\n```nginx\nlocation /storage {\n    proxy_pass http://hybrids3:8080;          # NO trailing slash — forwards full path\n    proxy_set_header Host $http_host;         # $http_host, NOT $host — keeps the port for SigV4\n    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n    proxy_set_header X-Forwarded-Proto $scheme;\n}\n```\n\nThen clients use the prefixed URL: `HYBRIDS3_URL=http://yourdomain/storage`, boto3 `endpoint_url=\"http://yourdomain/storage\"`, MCP `http://yourdomain/storage/mcp/`.\n\nA trailing slash in `proxy_pass` (`http://hybrids3:8080/`) strips the prefix and breaks SigV4 — don't. `$host` drops the port and also breaks SigV4 — use `$http_host`.\n\n## Management\n\n```bash\ndocker logs -f hybrids3                 # tail structured JSON logs (one line per entry)\ndocker stop hybrids3                     # stop\ndocker rm hybrids3                       # remove\ndocker pull psyb0t/hybrids3              # update image\ncurl http://localhost:8080/health        # liveness → {\"status\":\"ok\"}\n```\n\nLogs are structured JSON to stdout with a per-request `rid` that matches the `X-Request-Id` response header, so you can trace one request end-to-end. The private `key` is never logged.\n\n## OpenClaw / ClawHub Config\n\n```bash\nexport HYBRIDS3_URL=http://localhost:8080\nexport HYBRIDS3_KEY=<bucket-private-key>        # only if you'll write / read private buckets\nexport HYBRIDS3_MASTER_KEY=<master-key>         # only for cross-bucket / list-all\n```\n\nOr via `~/.openclaw/openclaw.json`:\n\n```json\n{\n  \"skills\": {\n    \"entries\": {\n      \"hybrids3\": {\n        \"env\": {\n          \"HYBRIDS3_URL\": \"http://localhost:8080\",\n          \"HYBRIDS3_KEY\": \"<bucket-private-key>\"\n        }\n      }\n    }\n  }\n}\n```\n\nFile v0.3.8:skill-card.md\n\n## Description:\n\nhybrids3 helps agents and developers work with a self-hosted object store through S3-compatible clients, a plain HTTP API, and MCP tools.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[psyb0t](https://clawhub.ai/user/psyb0t)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nDevelopers, operators, and agent builders use this skill to upload, download, list, delete, and presign objects in a user-managed hybrids3 storage service. It is most useful when a workflow needs S3-compatible storage, curl-friendly HTTP access, or structured MCP object-storage tools.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: Remote deployments can expose powerful storage credentials and objects if the endpoint or public buckets are reachable by unintended users.\n\nMitigation: Use loopback or HTTPS-only access, keep sensitive data in private buckets, and expose the service through a controlled reverse proxy when remote access is required.\n\nRisk: Master keys, bucket keys, and query-string auth or presigned URLs can act as bearer credentials and may leak through prompts, logs, shell history, or shared links.\n\nMitigation: Use bucket-scoped keys instead of the master key, keep credentials in environment or approved secret storage, avoid ?auth= URLs, and issue short-lived presigned URLs.\n\nRisk: Delete operations and presigned PUT uploads can remove or overwrite object data without an undo path.\n\nMitigation: Confirm the exact bucket and key before destructive actions, avoid bulk delete workflows, and require explicit user approval before replacing existing objects.\n\nRisk: Using an untrusted or mutable container image source can introduce supply-chain risk.\n\nMitigation: Install only from trusted image sources and prefer a pinned image digest for deployments.\n\n## Reference(s):\n\n- [hybrids3 setup](references/setup.md)\n- [hybrids3 homepage](https://github.com/psyb0t/docker-hybrids3)\n- [ClawHub skill page](https://clawhub.ai/psyb0t/skills/hybrids3)\n\n## Skill Output:\n\n**Output Type(s):** [text, markdown, code, shell commands, configuration, guidance]\n\n**Output Format:** [Markdown with inline code blocks, shell commands, JSON snippets, and configuration examples]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [May produce API requests or MCP tool guidance that affects objects in the configured storage endpoint.]\n\n## Skill Version(s):\n\n0.3.8 (source: server release evidence)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment.\n\nArchive v0.3.7: 5 files, 16075 bytes\n\nFiles: references/setup.md (8312b), scripts/hybrids3.sh (6187b), skill-card.md (2411b), SKILL.md (22017b), _meta.json (127b)\n\nFile v0.3.7:SKILL.md\n\n---\nname: hybrids3\ndescription: Self-hosted lightweight object storage speaking three interfaces at once — the S3 API (boto3 / AWS-SDK compatible, AWS Sig V4), a plain HTTP API (curl-friendly, Bearer auth), and MCP (Streamable HTTP tools for agents). Buckets/objects with SQLite metadata and flat-file storage on disk; buckets are config-defined (no create/delete API), each with a public/private flag, a private key + public key pair, optional TTL expiry, and a max upload size. Auth is per-bucket keys plus a cross-bucket master key; presigned GET/PUT URLs for handing out single-object access. Use when the user wants to put/get/list/delete files in a self-hosted S3-compatible store, generate presigned upload/download links, drive their own boto3-compatible object storage, or connect an agent to object storage over MCP.\nhomepage: https://github.com/psyb0t/docker-hybrids3\nuser-invocable: true\npermissions:\n  - \"network: outbound HTTP(S) to the configured HYBRIDS3_URL object-storage endpoint\"\n  - \"object storage: read/write/delete objects (upload, download, list, delete) and generate presigned GET/PUT URLs, scoped to whatever bucket key is provided\"\nmetadata:\n  { \"openclaw\": { \"emoji\": \"🪣\", \"primaryEnv\": \"HYBRIDS3_URL\", \"requires\": { \"bins\": [\"docker\", \"curl\"] } } }\n---\n\n# hybrids3\n\nLightweight object storage that speaks three interfaces from one container: the **S3 API** (boto3 / any AWS SDK, AWS Signature V4), a **plain HTTP API** (`curl` + Bearer token), and **MCP** (Streamable HTTP tool definitions for AI agents). Metadata lives in SQLite, object bytes are flat files on disk.\n\nBuckets are **configuration, not state** — they're defined in the server's `config.yaml` and there is no API to create or delete them. Each bucket carries: a `public`/private flag, a private `key` (Bearer token + S3 signing secret), a `public_key` (the `aws_access_key_id` identifier), an optional `ttl` (objects auto-expire after last write), and an optional `max_file_size`. A single cross-bucket `master_key` operates on every bucket and is the only credential that can list all buckets.\n\nObject keys support nested paths (`reports/2024/jan.pdf`). Content type is auto-detected on upload (libmagic sniff + extension fallback). Presigned GET/PUT URLs hand out time-limited single-object access without exposing a key.\n\nFor installation, configuration, and container setup, see [references/setup.md](references/setup.md).\n\n## Security & safety\n\n- **Public buckets are world-readable.** A bucket with `public: true` serves `GET`/`HEAD`/`LIST` to anyone who can reach the port — no auth at all. Only store data there that's meant to be openly readable; keep sensitive data in a `public: false` bucket.\n- **Bind loopback, not all interfaces.** The container listens on `8080`. Publishing it as `-p 8080:8080` / compose `\"8080:8080\"` makes it reachable from the network by default. Use `-p 127.0.0.1:8080:8080` (loopback) unless remote access is intentionally required, and for real deployments put the service on an internal Docker network behind a reverse proxy instead of exposing the port directly — see [references/setup.md](references/setup.md#quick-install) and [Behind a Reverse Proxy](references/setup.md#behind-a-reverse-proxy).\n- **`DELETE`, presigned `PUT`, and MCP `delete_object` are destructive & irreversible.** There's no undo, no versioning, no recycle bin — a deleted object (or one overwritten via `PUT`) is gone. An agent must NEVER call `DELETE` / `delete_object`, or generate/use a presigned `PUT` that overwrites, unless the user explicitly asked for that exact key to be removed or replaced; confirm the specific bucket + key first, never enumerate-then-bulk-delete, and treat bulk cleanup as requiring explicit per-object user confirmation.\n- **Bucket keys, master key, and presigned URLs are bearer credentials.** Whoever holds a bucket's private `key` or the `master_key` (or a live presigned URL) can act as that bucket/master until the credential is rotated or the URL expires — there's no per-caller revocation. Never paste these into shared prompts, logs, or presigned URLs handed to untrusted parties beyond the one object they're meant to grant.\n- **This skill is a consumer, not an operator.** It talks to an instance the user already runs; it doesn't provision, harden, or expose the server. Don't use this skill to reconfigure `config.yaml`, change bucket visibility, or alter port bindings on the user's behalf without being explicitly asked.\n\n## When To Use\n\n- Put / get / list / delete files in a self-hosted, S3-compatible object store.\n- Drive existing boto3 / AWS-SDK code against your own endpoint (change `endpoint_url` + credentials, nothing else).\n- Store artifacts, uploads, reports, backups that services and agents read/write.\n- Generate a **presigned PUT URL** so a third party can upload one specific key without your bucket key.\n- Generate a **presigned GET URL** so a third party can download one private object for a limited time.\n- Connect an AI agent to object storage over **MCP** — structured `upload_object` / `download_object` / `list_objects` / etc. tools.\n- Auto-expiring scratch storage — set a bucket `ttl` and objects clean themselves up.\n\n## When NOT To Use\n\n- Creating or deleting buckets at runtime — buckets live in `config.yaml`; add/remove them there and restart. `PUT /{bucket}` is an S3-compat no-op.\n- Multipart / chunked uploads (S3's multipart protocol) — not implemented. Upload the whole object in one request.\n- Object versioning, ACLs beyond bucket-level public/private, replication, or encryption at rest — none are supported.\n- Downloading objects **over 50 MB via MCP** — the MCP `download_object` tool caps at 50 MB. Use the S3 or plain HTTP `GET` for large objects.\n- CORS from a browser — no CORS headers are emitted; add them at the reverse-proxy layer.\n- Provisioning, configuring, or hardening the server itself — this skill is a **consumer**. It talks to an instance the user already runs and trusts.\n\n## Setup\n\nThe container should already be running. Point the skill at it:\n\n```bash\nexport HYBRIDS3_URL=http://localhost:8080\n```\n\n**Verify:** `curl $HYBRIDS3_URL/health` returns `{\"status\":\"ok\"}`.\n\nKeys are per-bucket and come from the operator's `config.yaml`. Export the ones you'll use rather than pasting them inline:\n\n```bash\nexport HYBRIDS3_KEY=<bucket-private-key>          # Bearer token / aws_secret_access_key\nexport HYBRIDS3_PUBLIC_KEY=<bucket-public-key>    # aws_access_key_id (defaults to bucket name)\n# For cross-bucket / list-all-buckets operations, use the master credentials instead:\nexport HYBRIDS3_MASTER_KEY=<master-key>\nexport HYBRIDS3_MASTER_PUBLIC_KEY=<master-public-key>   # defaults to \"master\"\n```\n\nIf the server runs behind a reverse proxy at a subpath, `HYBRIDS3_URL` includes it (e.g. `http://host/storage`) and every route — health included — lives under that prefix.\n\nFor install / `config.yaml` shape / bucket + key definitions / ports / reverse proxy, see [references/setup.md](references/setup.md).\n\n## Three Access Modes\n\nOne service, three front doors on the **same port** (`8080`). Pick per client:\n\n| Mode | Endpoint | Auth | Reach for it when… |\n|---|---|---|---|\n| **S3 API** | root (`/`), signed requests | AWS Sig V4 (`public_key` = access key id, `key` = secret) | You already have boto3 / AWS-SDK code, or want SDK ergonomics (pagination, presign helpers, retries). |\n| **Plain HTTP** | `/{bucket}/{key}` etc. | `Authorization: Bearer <key>` | Shell scripts, `curl`, quick one-offs. No SDK, no signing — just a Bearer header (and public-bucket reads need none). |\n| **MCP** | `POST /mcp/` | per-connection + per-tool `auth_key` | An AI agent should manipulate storage through typed tool calls instead of raw HTTP. |\n\nThey share one SQLite metadata store and one flat-file tree — an object written over S3 is readable over plain HTTP and MCP, and vice versa.\n\n## S3 API\n\nS3 clients authenticate with **AWS Signature V4**. The bucket's `public_key` is the `aws_access_key_id` (travels in the request in plaintext, in the `Credential=` field); the bucket's private `key` is the `aws_secret_access_key` (used only to compute the HMAC locally — never transmitted). `region_name` is required by SDKs but the value is arbitrary — use `us-east-1`.\n\n```\nAuthorization: AWS4-HMAC-SHA256 Credential=uploads-id/20240101/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-date, Signature=<hmac>\n```\n\n### boto3\n\n```python\nimport boto3\nfrom botocore.config import Config\n\n# per-bucket client — access limited to the \"uploads\" bucket\ns3 = boto3.client(\n    \"s3\",\n    endpoint_url=\"http://localhost:8080\",     # HYBRIDS3_URL\n    aws_access_key_id=\"uploads-id\",           # bucket public_key\n    aws_secret_access_key=\"uploads-secret\",   # bucket key (private)\n    region_name=\"us-east-1\",                  # arbitrary but required\n    config=Config(signature_version=\"s3v4\"),\n)\n\ns3.put_object(Bucket=\"uploads\", Key=\"file.txt\", Body=b\"hello\")\ns3.get_object(Bucket=\"uploads\", Key=\"file.txt\")[\"Body\"].read()\ns3.head_object(Bucket=\"uploads\", Key=\"file.txt\")\ns3.list_objects_v2(Bucket=\"uploads\", Prefix=\"images/\")\ns3.delete_object(Bucket=\"uploads\", Key=\"file.txt\")\ns3.list_buckets()                             # bucket key → only \"uploads\"\n\n# presigned URLs (see MCP / HTTP presign for the same semantics)\nget_url = s3.generate_presigned_url(\n    \"get_object\", Params={\"Bucket\": \"permanent\", \"Key\": \"doc.pdf\"}, ExpiresIn=3600,\n)\nput_url = s3.generate_presigned_url(\n    \"put_object\", Params={\"Bucket\": \"uploads\", \"Key\": \"inbox/report.pdf\"}, ExpiresIn=600,\n)\n```\n\nFor cross-bucket access or `list_buckets()` returning **all** buckets, build a second client with the **master** credentials (`aws_access_key_id=master_public_key`, `aws_secret_access_key=master_key`). Don't embed the master key in client-facing code — per-bucket keys scope access to one bucket.\n\n**Destructive & irreversible.** `delete_object` deletes the object with no undo — no versioning, no recycle bin. An agent must NEVER call it unless the user explicitly asked for that exact key to be deleted; confirm the specific bucket + key first, scope it to the current task, and never enumerate-then-bulk-delete. On a shared instance where multiple callers use the same bucket, this can destroy another caller's data — treat bulk deletes as admin-only and requiring explicit confirmation.\n\n### aws-cli\n\n```bash\naws --endpoint-url \"$HYBRIDS3_URL\" \\\n    --region us-east-1 \\\n    s3api put-object --bucket uploads --key file.txt --body ./file.txt\n# credentials via env: AWS_ACCESS_KEY_ID=<public_key> AWS_SECRET_ACCESS_KEY=<key>\n```\n\nSupported S3 operations: `put_object`, `get_object`, `head_object`, `delete_object`, `list_objects_v2` (honours `Prefix`), `list_buckets`, `generate_presigned_url` (GET + PUT). Not supported: multipart upload, versioning, ACLs, bucket create/delete (boto3's implicit `create_bucket` maps to a no-op).\n\n## Plain HTTP API\n\nBearer auth: pass the bucket's private `key` (or the `master_key`) as `Authorization: Bearer <key>`. Public-bucket **reads** (GET/HEAD/LIST) need no auth; **all writes** and **private-bucket reads** require a key.\n\nRequests carrying an AWS Sig V4 `Authorization` header get S3-style XML back; everything else gets JSON. Every response includes `X-Request-Id` (log correlation) and `X-Content-Type-Options: nosniff`. Errors carry `\"error\"` + `\"request_id\"` fields.\n\n### Objects\n\n| Method | Path | Auth | Description |\n|---|---|---|---|\n| `PUT` | `/{bucket}/{key}` | write | Upload object. Returns `ETag` (MD5 of content). Content type auto-detected; override with a `Content-Type` header. |\n| `GET` | `/{bucket}/{key}` | read | Download object. Returns `ETag`, `Last-Modified`, `Content-Length`. |\n| `HEAD` | `/{bucket}/{key}` | read | Object metadata, no body. |\n| `DELETE` | `/{bucket}/{key}` | write | Delete object — always `204`, even if absent. |\n| `GET` | `/{bucket}` | read | List objects. Query: `prefix`, `max-keys`. |\n| `POST` | `/presign/{bucket}/{key}` | write | Generate a presigned URL. Query: `method` (`GET` default / `PUT`), `expires` (seconds, 1–604800, default 3600). |\n\n**Destructive & irreversible.** `DELETE` removes the object with no undo. An agent must NEVER call it unless the user explicitly asked for that exact key to be deleted; confirm the specific bucket + key first, scope it to the current task, and never enumerate-then-bulk-delete. On a shared bucket used by other callers, this can destroy their data — treat it as admin-only.\n\n```bash\n# upload\ncurl -X PUT \"$HYBRIDS3_URL/uploads/file.txt\" \\\n  -H \"Authorization: Bearer $HYBRIDS3_KEY\" \\\n  --data-binary @file.txt\n\n# upload a nested key (parent dirs created automatically)\ncurl -X PUT \"$HYBRIDS3_URL/uploads/reports/2024/january.pdf\" \\\n  -H \"Authorization: Bearer $HYBRIDS3_KEY\" \\\n  --data-binary @january.pdf\n\n# download from a public bucket — no auth needed\ncurl \"$HYBRIDS3_URL/uploads/file.txt\"\n\n# download from a private bucket\ncurl \"$HYBRIDS3_URL/permanent/doc.pdf\" \\\n  -H \"Authorization: Bearer $HYBRIDS3_KEY\"\n\n# list objects with a prefix\ncurl \"$HYBRIDS3_URL/uploads?prefix=images/&max-keys=50\" \\\n  -H \"Authorization: Bearer $HYBRIDS3_KEY\"\n\n# delete\ncurl -X DELETE \"$HYBRIDS3_URL/uploads/file.txt\" \\\n  -H \"Authorization: Bearer $HYBRIDS3_KEY\"\n```\n\n### Buckets\n\n| Method | Path | Auth | Description |\n|---|---|---|---|\n| `GET` | `/health` | none | `{\"status\":\"ok\"}` liveness check. |\n| `GET` | `/` | master or bucket key | List buckets — master key lists all, bucket key lists only its own. |\n| `HEAD` | `/{bucket}` | read | `200` if bucket exists in config, else `404`. |\n| `PUT` | `/{bucket}` | write | S3-compat no-op: `200` if bucket exists in config, `404` if not. Creates nothing. |\n\n```bash\n# list all buckets — needs the master key\ncurl \"$HYBRIDS3_URL/\" -H \"Authorization: Bearer $HYBRIDS3_MASTER_KEY\"\n\n# a bucket key lists only its own bucket\ncurl \"$HYBRIDS3_URL/\" -H \"Authorization: Bearer $HYBRIDS3_KEY\"\n```\n\n### Presigned URLs (HTTP)\n\n`POST /presign/{bucket}/{key}` requires the bucket key or master key. The result grants exactly one verb on exactly one key. A GET URL can't be used to PUT (the method is baked into the signature).\n\n```bash\n# presigned GET on a PRIVATE bucket → signed, expiring URL\ncurl -X POST \"$HYBRIDS3_URL/presign/permanent/doc.pdf?expires=3600\" \\\n  -H \"Authorization: Bearer $HYBRIDS3_KEY\"\n# → {\"url\":\"http://.../permanent/doc.pdf?X-Amz-Algorithm=...&X-Amz-Signature=...\",\"method\":\"GET\",\"expires\":3600}\n\n# presigned GET on a PUBLIC bucket → plain URL, no signature, no expiry (reads are open anyway)\ncurl -X POST \"$HYBRIDS3_URL/presign/uploads/photo.jpg\" \\\n  -H \"Authorization: Bearer $HYBRIDS3_KEY\"\n# → {\"url\":\"http://.../uploads/photo.jpg\",\"method\":\"GET\",\"expires\":null}\n\n# presigned PUT → ALWAYS signed, even for public buckets (anonymous writes are never allowed)\ncurl -X POST \"$HYBRIDS3_URL/presign/uploads/inbox/report.pdf?method=PUT&expires=600\" \\\n  -H \"Authorization: Bearer $HYBRIDS3_KEY\"\n\n# recipient uploads using only the URL — no Authorization header\ncurl -X PUT \"<presigned-put-url>\" --data-binary @report.pdf\n```\n\nThe bucket's `max_file_size` is enforced server-side during the upload regardless of how it was authenticated (oversized → `413`). Expired or tampered presigned URLs → `403`.\n\n## MCP Endpoint\n\nAn MCP server runs at `POST /mcp/` over the **Streamable HTTP** transport. Client config:\n\n```json\n{\n  \"mcpServers\": {\n    \"hybrids3\": { \"type\": \"streamable-http\", \"url\": \"http://localhost:8080/mcp/\" }\n  }\n}\n```\n\nWire it into Claude Code:\n\n```bash\nclaude mcp add --transport http hybrids3 \"$HYBRIDS3_URL/mcp/\"\n# with endpoint-level auth:\nclaude mcp add --transport http hybrids3 \"$HYBRIDS3_URL/mcp/\" \\\n  --header \"Authorization: Bearer $HYBRIDS3_MASTER_KEY\"\n```\n\n### Auth (two layers)\n\n**Endpoint-level (optional)** — a token that authenticates the *connection* before any tool runs. Master key = full access; a bucket key = limited to that bucket. Send it as `Authorization: Bearer <key>`, or (for clients that can't set headers) as `?auth=<key>` on the URL. A token that matches nothing → `401`; **no token → passes through**, and per-tool auth applies.\n\n**Per-tool** — every tool that touches a bucket takes an `auth_key` argument (the bucket's private `key` or the master key), checked independently of the connection token. Public-bucket reads (`download_object`, `list_objects`, `object_info`) accept an empty `auth_key`; all writes and all private-bucket ops require it.\n\n### Tools\n\n| Tool | Args | Auth | Notes |\n|---|---|---|---|\n| `upload_object` | `bucket`, `key`, `content`, `auth_key`, `content_type=\"\"`, `encoding=\"utf-8\"` | bucket/master key | `encoding=\"base64\"` for binary. Returns `size`, `etag`, `content_type`. |\n| `download_object` | `bucket`, `key`, `auth_key=\"\"`, `encoding=\"utf-8\"` | key on private only | `base64` for binary; UTF-8 that won't decode auto-falls back to base64. **Objects > 50 MB rejected — use HTTP.** |\n| `delete_object` | `bucket`, `key`, `auth_key` | bucket/master key | Idempotent. |\n| `list_objects` | `bucket`, `auth_key=\"\"`, `prefix=\"\"`, `max_keys=100` | key on private only | `max_keys` clamped to 1–1000. |\n| `list_buckets` | `auth_key` | master or bucket key | Master lists all; bucket key lists only its own. |\n| `object_info` | `bucket`, `key`, `auth_key=\"\"` | key on private only | Metadata only (size, content_type, etag, uploaded_at, expires_at). |\n| `presign_url` | `bucket`, `key`, `auth_key`, `method=\"GET\"`, `expires=3600`, `base_url=\"\"`, `host=\"\"` | bucket/master key | `method` GET/PUT. Same public-GET-plain / everything-else-signed logic as the HTTP endpoint. `expires` clamped 1–604800. Set `base_url` to the externally-reachable URL so the link is usable. |\n\n**`delete_object` is destructive & irreversible.** It deletes the object with no undo. An agent must NEVER call it unless the user explicitly asked for that exact key to be deleted; confirm the specific bucket + key first, never enumerate-then-bulk-delete. On a shared bucket used by other callers, this can destroy their data — treat it as admin-only.\n\nAll tools return `structuredContent` with a plain-text fallback. Internal error details are masked.\n\n### Raw JSON-RPC\n\nFor debugging or non-MCP callers, POST JSON-RPC directly. The transport requires the `Accept` header below.\n\n```bash\n# tools/list\ncurl -s \"$HYBRIDS3_URL/mcp/\" \\\n  -H \"Content-Type: application/json\" \\\n  -H \"Accept: application/json, text/event-stream\" \\\n  -d '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/list\"}'\n\n# tools/call — upload\ncurl -s \"$HYBRIDS3_URL/mcp/\" \\\n  -H \"Content-Type: application/json\" \\\n  -H \"Accept: application/json, text/event-stream\" \\\n  -d '{\n    \"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools/call\",\n    \"params\":{\n      \"name\":\"upload_object\",\n      \"arguments\":{\"bucket\":\"uploads\",\"key\":\"note.txt\",\"content\":\"hello\",\"auth_key\":\"'\"$HYBRIDS3_KEY\"'\"}\n    }\n  }'\n```\n\n## Authentication (all modes)\n\n| Credential | Config field | Role | Keep secret? |\n|---|---|---|---|\n| Bucket private key | `key` | Bearer token (HTTP/MCP) **and** `aws_secret_access_key` (S3). Signs/verifies — never transmitted. | **Yes** |\n| Bucket public key | `public_key` | `aws_access_key_id` (S3) + `Credential=` in presigned URLs. Identifies, grants nothing. Defaults to bucket name. | No |\n| Master key | `master_key` | Cross-bucket god key. Works on every bucket + only credential that lists all buckets. | **Yes** |\n| Master public key | `master_public_key` | `aws_access_key_id` paired with the master key (S3). Defaults to `\"master\"`. | No |\n\nBucket visibility:\n\n| | GET / HEAD / LIST | PUT | DELETE / presign |\n|---|---|---|---|\n| `public: true` | no auth | bucket key, master key, or valid presigned PUT | bucket key or master key |\n| `public: false` | bucket key, master key, or valid presigned GET | bucket key, master key, or valid presigned PUT | bucket key or master key |\n\nUnauthorized or non-existent bucket access always returns `404` — callers can't tell \"doesn't exist\" from \"no access\". All key comparisons are constant-time.\n\n## Typical Workflows\n\n### Upload + fetch over plain HTTP\n\n```bash\nexport HYBRIDS3_URL=http://localhost:8080\nexport HYBRIDS3_KEY=<uploads-key>\n\ncurl -X PUT \"$HYBRIDS3_URL/uploads/report.pdf\" \\\n  -H \"Authorization: Bearer $HYBRIDS3_KEY\" --data-binary @report.pdf\ncurl -sO \"$HYBRIDS3_URL/uploads/report.pdf\"     # public bucket → no auth on read\n```\n\nOr via the helper (see [`scripts/hybrids3.sh`](scripts/hybrids3.sh)):\n\n```bash\nHYBRIDS3_URL=http://localhost:8080 HYBRIDS3_KEY=<key> \\\n  scripts/hybrids3.sh put uploads report.pdf ./report.pdf\nscripts/hybrids3.sh list uploads reports/\nscripts/hybrids3.sh get uploads report.pdf ./out.pdf\nscripts/hybrids3.sh delete uploads report.pdf\nscripts/hybrids3.sh buckets                     # needs HYBRIDS3_KEY=<master-key>\n```\n\n### Hand someone a presigned upload URL\n\n```bash\n# you generate it (needs a key); they upload with no credentials\ncurl -X POST \"$HYBRIDS3_URL/presign/uploads/inbox/theirs.zip?method=PUT&expires=600\" \\\n  -H \"Authorization: Bearer $HYBRIDS3_KEY\" | jq -r .url\n# → hand the URL over; recipient: curl -X PUT \"<url>\" --data-binary @theirs.zip\n```\n\n### Drop-in boto3 against your own endpoint\n\nPoint any existing boto3 code at `HYBRIDS3_URL`, set the bucket's `public_key`/`key` as the credential pair, `region_name=\"us-east-1\"`, `signature_version=\"s3v4\"`. See [S3 API](#s3-api).\n\n### Agent-driven storage over MCP\n\nAdd the server (`claude mcp add --transport http hybrids3 \"$HYBRIDS3_URL/mcp/\"`), then call `list_buckets` to discover buckets, `upload_object` / `download_object` / `list_objects` to move data. Pass the bucket `key` as `auth_key`; use the master key only when you need cross-bucket reach.\n\n### Auto-expiring scratch storage\n\nIf a bucket has a `ttl` set (server config), objects delete themselves that long after their last write — overwriting resets the clock. Nothing to do client-side; just write to the bucket.\n\nFile v0.3.7:_meta.json\n\n{\n  \"ownerId\": \"kn79dhvmpjng4rp2jjk8k0v5xx80ccbk\",\n  \"slug\": \"hybrids3\",\n  \"version\": \"0.3.7\",\n  \"publishedAt\": 1785195476843\n}\n\nFile v0.3.7:references/setup.md\n\n# hybrids3 setup\n\nConsumer-side reference for standing up an instance to talk to. If an operator already runs one, you only need its URL and a bucket key — skip to [OpenClaw / ClawHub Config](#openclaw--clawhub-config).\n\n## Requirements\n\n- Docker\n- A `config.yaml` (buckets, keys, master key — see [Configuration](#configuration))\n- A persistent volume/dir for `/data` (object bytes + SQLite metadata)\n- ~1 core / modest RAM — it's a thin FastAPI service, not a cluster\n\nThe container runs as **UID 1000** and expects:\n\n- Config file mounted at `/config/config.yaml`\n- Data at `/data`\n- Listens on port `8080`\n\n## Quick Install\n\nGrab the example config, edit keys + buckets, run:\n\n```bash\n# fetch the example config\nwget -O config.yaml \\\n  https://raw.githubusercontent.com/psyb0t/docker-hybrids3/master/config.example.yaml\n\n# edit it — set master_key and each bucket's key\nvi config.yaml\n\n# run\ndocker run -d --name hybrids3 \\\n    -p 127.0.0.1:8080:8080 \\\n    -v ./config.yaml:/config/config.yaml:ro \\\n    -v hybrids3-data:/data \\\n    psyb0t/hybrids3\n\n# verify\ncurl http://localhost:8080/health      # → {\"status\":\"ok\"}\n```\n\n**`-p 8080:8080` binds all interfaces** — the service becomes reachable from the network, not just localhost. Use `-p 127.0.0.1:8080:8080` (loopback-only) unless remote access is intentionally required, matching the fleet convention.\n\n### docker-compose\n\n```yaml\nservices:\n  hybrids3:\n    image: psyb0t/hybrids3\n    ports:\n      - \"127.0.0.1:8080:8080\"\n    volumes:\n      - ./config.yaml:/config/config.yaml:ro\n      - hybrids3-data:/data\n    restart: unless-stopped\n\nvolumes:\n  hybrids3-data:\n```\n\n**`\"127.0.0.1:8080:8080\"` binds loopback-only.** Use `\"8080:8080\"` only if remote access is intentionally required, matching the fleet convention above. For a real deployment where other hosts or services need to reach it, put hybrids3 on an internal Docker network and front it with a reverse proxy (see [Behind a Reverse Proxy](#behind-a-reverse-proxy)) rather than publishing the port on all interfaces.\n\n**Uploads to a `public: true` bucket are world-readable.** Any bucket with `public: true` serves GET/HEAD/LIST with no authentication at all — anyone who can reach the port can read anything stored there. Don't put sensitive data in a public bucket, and keep the port bound to loopback (or behind an authenticating proxy) unless that's the intended exposure.\n\n## Configuration\n\n**All configuration is the YAML file — there are no application env vars.** Buckets are defined here and only here; there is no create/delete API. Change config and restart to add or remove a bucket.\n\nTop-level keys:\n\n| Field | Default | What it does |\n|---|---|---|\n| `master_key` | `\"\"` | Cross-bucket god key — works on every bucket, only credential that lists all buckets. Keep secret. |\n| `master_public_key` | `\"master\"` | Non-secret `aws_access_key_id` paired with `master_key` in S3 auth. Safe to share. |\n| `cleanup_interval` | `60` (`1m`) | How often the background loop deletes expired objects + orphan files. Duration string (`30s`, `5m`, `1h`) or raw seconds. |\n| `lock_acquire_timeout` | `30` | Seconds a request waits for a per-key lock before `503`. Float. |\n| `lock_hold_timeout` | `300` | Seconds a request may hold a lock before it's force-released with `503`. Guards stalled uploads. |\n| `lock_max_waiters` | `100` | Max requests queued for one key before new ones get `503` immediately. |\n| `path_prefix` | `\"\"` | Serve all routes under a subpath (e.g. `/storage`) for reverse-proxy deployments. `\"/storage\"` and `\"/storage/\"` both normalize. Empty = root. |\n\nPer-bucket fields (under `buckets:`, keyed by bucket name — name must not contain `/` or `..`):\n\n| Field | Default | What it does |\n|---|---|---|\n| `key` | **required** | Private key: Bearer token (HTTP/MCP) + `aws_secret_access_key` (S3). Never transmitted. Keep secret. |\n| `public` | `false` | `true` = anyone reads (GET/HEAD/LIST) without auth; writes still need a key. `false` = auth for everything. |\n| `public_key` | bucket name | Public `aws_access_key_id`; appears in presigned `Credential=`. Safe to share. |\n| `ttl` | `0` | Objects expire this long after last write (overwrite resets). `0` = never. Formats: `30s 5m 1h 1h30m12s 1d 2d12h`. |\n| `max_file_size` | `0` | Reject uploads over this with `413`. Formats: `500B 50KB 10MB 1GB`, `0` = no limit. |\n\nMinimal example (`config.example.yaml`):\n\n```yaml\nmaster_key: \"change-me-to-something-secret\"\nmaster_public_key: \"master\"\ncleanup_interval: 1m\nlock_acquire_timeout: 30\nlock_hold_timeout: 300\nlock_max_waiters: 100\n# path_prefix: /storage\n\nbuckets:\n  uploads:\n    public: true\n    key: \"uploads-secret\"\n    public_key: \"uploads-id\"\n    ttl: 24h\n    max_file_size: 50MB\n\n  permanent:\n    public: false\n    key: \"perm-secret\"\n    public_key: \"permanent-id\"\n    ttl: 0\n    max_file_size: 100MB\n```\n\n## Environment Variables\n\nThe **application** takes no env vars — everything is in `config.yaml`. The only tunables at run time are Docker-level:\n\n| Concern | How | Example |\n|---|---|---|\n| Config path | mount to `/config/config.yaml` | `-v ./config.yaml:/config/config.yaml:ro` |\n| Data location | mount to `/data` | `-v hybrids3-data:/data` |\n| Host port / interface | `docker run -p` | `-p 127.0.0.1:8080:8080` (loopback), `-p 8080:8080` (all interfaces) |\n\nFor S3 clients (`boto3`, `aws-cli`), the credentials aren't hybrids3 env vars — they're the standard AWS SDK ones, sourced from a bucket's config:\n\n| SDK var / arg | Value from config |\n|---|---|\n| `AWS_ACCESS_KEY_ID` / `aws_access_key_id` | the bucket's `public_key` (or `master_public_key`) |\n| `AWS_SECRET_ACCESS_KEY` / `aws_secret_access_key` | the bucket's `key` (or `master_key`) |\n| `AWS_DEFAULT_REGION` / `region_name` | any value — server doesn't check it; `us-east-1` below is just an example |\n| `endpoint_url` | `HYBRIDS3_URL` (e.g. `http://localhost:8080`) |\n\nThe skill's own convenience vars for HTTP/MCP work: `HYBRIDS3_URL`, `HYBRIDS3_KEY`, `HYBRIDS3_PUBLIC_KEY`, `HYBRIDS3_MASTER_KEY`, `HYBRIDS3_MASTER_PUBLIC_KEY`.\n\n## Ports\n\n| Port | Service |\n|---|---|\n| 8080 | HTTP API + S3 API + MCP (`/mcp/`) — all on the same port |\n\nThe container listens on `8080` internally. Map it however you like with `-p`.\n\n## Behind a Reverse Proxy\n\nSet `path_prefix` in config to match the proxy location; hybrids3 serves every route under it natively (including `/{prefix}/health`), so nginx forwards the path **as-is** — no stripping. SigV4 works because the signed path matches what the server sees.\n\n```yaml\n# config.yaml\npath_prefix: /storage\n```\n\n```nginx\nlocation /storage {\n    proxy_pass http://hybrids3:8080;          # NO trailing slash — forwards full path\n    proxy_set_header Host $http_host;         # $http_host, NOT $host — keeps the port for SigV4\n    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n    proxy_set_header X-Forwarded-Proto $scheme;\n}\n```\n\nThen clients use the prefixed URL: `HYBRIDS3_URL=http://yourdomain/storage`, boto3 `endpoint_url=\"http://yourdomain/storage\"`, MCP `http://yourdomain/storage/mcp/`.\n\nA trailing slash in `proxy_pass` (`http://hybrids3:8080/`) strips the prefix and breaks SigV4 — don't. `$host` drops the port and also breaks SigV4 — use `$http_host`.\n\n## Management\n\n```bash\ndocker logs -f hybrids3                 # tail structured JSON logs (one line per entry)\ndocker stop hybrids3                     # stop\ndocker rm hybrids3                       # remove\ndocker pull psyb0t/hybrids3              # update image\ncurl http://localhost:8080/health        # liveness → {\"status\":\"ok\"}\n```\n\nLogs are structured JSON to stdout with a per-request `rid` that matches the `X-Request-Id` response header, so you can trace one request end-to-end. The private `key` is never logged.\n\n## OpenClaw / ClawHub Config\n\n```bash\nexport HYBRIDS3_URL=http://localhost:8080\nexport HYBRIDS3_KEY=<bucket-private-key>        # only if you'll write / read private buckets\nexport HYBRIDS3_MASTER_KEY=<master-key>         # only for cross-bucket / list-all\n```\n\nOr via `~/.openclaw/openclaw.json`:\n\n```json\n{\n  \"skills\": {\n    \"entries\": {\n      \"hybrids3\": {\n        \"env\": {\n          \"HYBRIDS3_URL\": \"http://localhost:8080\",\n          \"HYBRIDS3_KEY\": \"<bucket-private-key>\"\n        }\n      }\n    }\n  }\n}\n```\n\nFile v0.3.7:skill-card.md\n\n## Description: <br>\nhybrids3 helps agents and developers work with a self-hosted object store through S3-compatible, plain HTTP, and MCP interfaces. <br>\n\nThis skill is ready for commercial/non-commercial use. <br>\n\n## Publisher: <br>\n[psyb0t](https://clawhub.ai/user/psyb0t) <br>\n\n### License/Terms of Use: <br>\nMIT-0 <br>\n\n\n## Use Case: <br>\nDevelopers and agents use hybrids3 to upload, download, list, delete, and presign objects in a self-hosted S3-compatible object store over S3, plain HTTP, or MCP. <br>\n\n### Deployment Geography for Use: <br>\nGlobal <br>\n\n## Known Risks and Mitigations: <br>\nRisk: The configured hybrids3 endpoint can expose public buckets, bearer keys, presigned URLs, deletes, and object overwrites. <br>\nMitigation: Install only for trusted endpoints, keep sensitive objects in private buckets, use per-bucket keys instead of the master key, and avoid sharing live credentials or presigned URLs in prompts or logs. <br>\nRisk: Delete operations and presigned PUT overwrites are destructive and irreversible. <br>\nMitigation: Require explicit user confirmation for the exact bucket and key before deleting or overwriting an object, and avoid enumerate-then-bulk-delete workflows. <br>\nRisk: Binding the service to a public interface can make buckets and APIs reachable from the network. <br>\nMitigation: Bind to loopback or a protected internal network unless remote access is deliberate, and place production access behind an appropriate reverse proxy or network control. <br>\n\n\n## Reference(s): <br>\n- [hybrids3 setup](references/setup.md) <br>\n- [ClawHub skill page](https://clawhub.ai/psyb0t/skills/hybrids3) <br>\n\n\n## Skill Output: <br>\n**Output Type(s):** [text, markdown, code, shell commands, configuration, guidance] <br>\n**Output Format:** [Markdown guidance with shell commands, configuration snippets, and API examples] <br>\n**Output Parameters:** [1D] <br>\n**Other Properties Related to Output:** [May include object-storage operation guidance, curl commands, MCP configuration, boto3 examples, and JSON responses.] <br>\n\n## Skill Version(s): <br>\n0.3.7 (source: server release evidence) <br>\n\n## Ethical Considerations: <br>\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment. <br>\n\nArchive v0.3.6: 5 files, 16187 bytes\n\nFiles: references/setup.md (8312b), scripts/hybrids3.sh (6187b), skill-card.md (2668b), SKILL.md (22017b), _meta.json (127b)\n\nFile v0.3.6:SKILL.md\n\n---\nname: hybrids3\ndescription: Self-hosted lightweight object storage speaking three interfaces at once — the S3 API (boto3 / AWS-SDK compatible, AWS Sig V4), a plain HTTP API (curl-friendly, Bearer auth), and MCP (Streamable HTTP tools for agents). Buckets/objects with SQLite metadata and flat-file storage on disk; buckets are config-defined (no create/delete API), each with a public/private flag, a private key + public key pair, optional TTL expiry, and a max upload size. Auth is per-bucket keys plus a cross-bucket master key; presigned GET/PUT URLs for handing out single-object access. Use when the user wants to put/get/list/delete files in a self-hosted S3-compatible store, generate presigned upload/download links, drive their own boto3-compatible object storage, or connect an agent to object storage over MCP.\nhomepage: https://github.com/psyb0t/docker-hybrids3\nuser-invocable: true\npermissions:\n  - \"network: outbound HTTP(S) to the configured HYBRIDS3_URL object-storage endpoint\"\n  - \"object storage: read/write/delete objects (upload, download, list, delete) and generate presigned GET/PUT URLs, scoped to whatever bucket key is provided\"\nmetadata:\n  { \"openclaw\": { \"emoji\": \"🪣\", \"primaryEnv\": \"HYBRIDS3_URL\", \"requires\": { \"bins\": [\"docker\", \"curl\"] } } }\n---\n\n# hybrids3\n\nLightweight object storage that speaks three interfaces from one container: the **S3 API** (boto3 / any AWS SDK, AWS Signature V4), a **plain HTTP API** (`curl` + Bearer token), and **MCP** (Streamable HTTP tool definitions for AI agents). Metadata lives in SQLite, object bytes are flat files on disk.\n\nBuckets are **configuration, not state** — they're defined in the server's `config.yaml` and there is no API to create or delete them. Each bucket carries: a `public`/private flag, a private `key` (Bearer token + S3 signing secret), a `public_key` (the `aws_access_key_id` identifier), an optional `ttl` (objects auto-expire after last write), and an optional `max_file_size`. A single cross-bucket `master_key` operates on every bucket and is the only credential that can list all buckets.\n\nObject keys support nested paths (`reports/2024/jan.pdf`). Content type is auto-detected on upload (libmagic sniff + extension fallback). Presigned GET/PUT URLs hand out time-limited single-object access without exposing a key.\n\nFor installation, configuration, and container setup, see [references/setup.md](references/setup.md).\n\n## Security & safety\n\n- **Public buckets are world-readable.** A bucket with `public: true` serves `GET`/`HEAD`/`LIST` to anyone who can reach the port — no auth at all. Only store data there that's meant to be openly readable; keep sensitive data in a `public: false` bucket.\n- **Bind loopback, not all interfaces.** The container listens on `8080`. Publishing it as `-p 8080:8080` / compose `\"8080:8080\"` makes it reachable from the network by default. Use `-p 127.0.0.1:8080:8080` (loopback) unless remote access is intentionally required, and for real deployments put the service on an internal Docker network behind a reverse proxy instead of exposing the port directly — see [references/setup.md](references/setup.md#quick-install) and [Behind a Reverse Proxy](references/setup.md#behind-a-reverse-proxy).\n- **`DELETE`, presigned `PUT`, and MCP `delete_object` are destructive & irreversible.** There's no undo, no versioning, no recycle bin — a deleted object (or one overwritten via `PUT`) is gone. An agent must NEVER call `DELETE` / `delete_object`, or generate/use a presigned `PUT` that overwrites, unless the user explicitly asked for that exact key to be removed or replaced; confirm the specific bucket + key first, never enumerate-then-bulk-delete, and treat bulk cleanup as requiring explicit per-object user confirmation.\n- **Bucket keys, master key, and presigned URLs are bearer credentials.** Whoever holds a bucket's private `key` or the `master_key` (or a live presigned URL) can act as that bucket/master until the credential is rotated or the URL expires — there's no per-caller revocation. Never paste these into shared prompts, logs, or presigned URLs handed to untrusted parties beyond the one object they're meant to grant.\n- **This skill is a consumer, not an operator.** It talks to an instance the user already runs; it doesn't provision, harden, or expose the server. Don't use this skill to reconfigure `config.yaml`, change bucket visibility, or alter port bindings on the user's behalf without being explicitly asked.\n\n## When To Use\n\n- Put / get / list / delete files in a self-hosted, S3-compatible object store.\n- Drive existing boto3 / AWS-SDK code against your own endpoint (change `endpoint_url` + credentials, nothing else).\n- Store artifacts, uploads, reports, backups that services and agents read/write.\n- Generate a **presigned PUT URL** so a third party can upload one specific key without your bucket key.\n- Generate a **presigned GET URL** so a third party can download one private object for a limited time.\n- Connect an AI agent to object storage over **MCP** — structured `upload_object` / `download_object` / `list_objects` / etc. tools.\n- Auto-expiring scratch storage — set a bucket `ttl` and objects clean themselves up.\n\n## When NOT To Use\n\n- Creating or deleting buckets at runtime — buckets live in `config.yaml`; add/remove them there and restart. `PUT /{bucket}` is an S3-compat no-op.\n- Multipart / chunked uploads (S3's multipart protocol) — not implemented. Upload the whole object in one request.\n- Object versioning, ACLs beyond bucket-level public/private, replication, or encryption at rest — none are supported.\n- Downloading objects **over 50 MB via MCP** — the MCP `download_object` tool caps at 50 MB. Use the S3 or plain HTTP `GET` for large objects.\n- CORS from a browser — no CORS headers are emitted; add them at the reverse-proxy layer.\n- Provisioning, configuring, or hardening the server itself — this skill is a **consumer**. It talks to an instance the user already runs and trusts.\n\n## Setup\n\nThe container should already be running. Point the skill at it:\n\n```bash\nexport HYBRIDS3_URL=http://localhost:8080\n```\n\n**Verify:** `curl $HYBRIDS3_URL/health` returns `{\"status\":\"ok\"}`.\n\nKeys are per-bucket and come from the operator's `config.yaml`. Export the ones you'll use rather than pasting them inline:\n\n```bash\nexport HYBRIDS3_KEY=<bucket-private-key>          # Bearer token / aws_secret_access_key\nexport HYBRIDS3_PUBLIC_KEY=<bucket-public-key>    # aws_access_key_id (defaults to bucket name)\n# For cross-bucket / list-all-buckets operations, use the master credentials instead:\nexport HYBRIDS3_MASTER_KEY=<master-key>\nexport HYBRIDS3_MASTER_PUBLIC_KEY=<master-public-key>   # defaults to \"master\"\n```\n\nIf the server runs behind a reverse proxy at a subpath, `HYBRIDS3_URL` includes it (e.g. `http://host/storage`) and every route — health included — lives under that prefix.\n\nFor install / `config.yaml` shape / bucket + key definitions / ports / reverse proxy, see [references/setup.md](references/setup.md).\n\n## Three Access Modes\n\nOne service, three front doors on the **same port** (`8080`). Pick per client:\n\n| Mode | Endpoint | Auth | Reach for it when… |\n|---|---|---|---|\n| **S3 API** | root (`/`), signed requests | AWS Sig V4 (`public_key` = access key id, `key` = secret) | You already have boto3 / AWS-SDK code, or want SDK ergonomics (pagination, presign helpers, retries). |\n| **Plain HTTP** | `/{bucket}/{key}` etc. | `Authorization: Bearer <key>` | Shell scripts, `curl`, quick one-offs. No SDK, no signing — just a Bearer header (and public-bucket reads need none). |\n| **MCP** | `POST /mcp/` | per-connection + per-tool `auth_key` | An AI agent should manipulate storage through typed tool calls instead of raw HTTP. |\n\nThey share one SQLite metadata store and one flat-file tree — an object written over S3 is readable over plain HTTP and MCP, and vice versa.\n\n## S3 API\n\nS3 clients authenticate with **AWS Signature V4**. The bucket's `public_key` is the `aws_access_key_id` (travels in the request in plaintext, in the `Credential=` field); the bucket's private `key` is the `aws_secret_access_key` (used only to compute the HMAC locally — never transmitted). `region_name` is required by SDKs but the value is arbitrary — use `us-east-1`.\n\n```\nAuthorization: AWS4-HMAC-SHA256 Credential=uploads-id/20240101/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-date, Signature=<hmac>\n```\n\n### boto3\n\n```python\nimport boto3\nfrom botocore.config import Config\n\n# per-bucket client — access limited to the \"uploads\" bucket\ns3 = boto3.client(\n    \"s3\",\n    endpoint_url=\"http://localhost:8080\",     # HYBRIDS3_URL\n    aws_access_key_id=\"uploads-id\",           # bucket public_key\n    aws_secret_access_key=\"uploads-secret\",   # bucket key (private)\n    region_name=\"us-east-1\",                  # arbitrary but required\n    config=Config(signature_version=\"s3v4\"),\n)\n\ns3.put_object(Bucket=\"uploads\", Key=\"file.txt\", Body=b\"hello\")\ns3.get_object(Bucket=\"uploads\", Key=\"file.txt\")[\"Body\"].read()\ns3.head_object(Bucket=\"uploads\", Key=\"file.txt\")\ns3.list_objects_v2(Bucket=\"uploads\", Prefix=\"images/\")\ns3.delete_object(Bucket=\"uploads\", Key=\"file.txt\")\ns3.list_buckets()                             # bucket key → only \"uploads\"\n\n# presigned URLs (see MCP / HTTP presign for the same semantics)\nget_url = s3.generate_presigned_url(\n    \"get_object\", Params={\"Bucket\": \"permanent\", \"Key\": \"doc.pdf\"}, ExpiresIn=3600,\n)\nput_url = s3.generate_presigned_url(\n    \"put_object\", Params={\"Bucket\": \"uploads\", \"Key\": \"inbox/report.pdf\"}, ExpiresIn=600,\n)\n```\n\nFor cross-bucket access or `list_buckets()` returning **all** buckets, build a second client with the **master** credentials (`aws_access_key_id=master_public_key`, `aws_secret_access_key=master_key`). Don't embed the master key in client-facing code — per-bucket keys scope access to one bucket.\n\n**Destructive & irreversible.** `delete_object` deletes the object with no undo — no versioning, no recycle bin. An agent must NEVER call it unless the user explicitly asked for that exact key to be deleted; confirm the specific bucket + key first, scope it to the current task, and never enumerate-then-bulk-delete. On a shared instance where multiple callers use the same bucket, this can destroy another caller's data — treat bulk deletes as admin-only and requiring explicit confirmation.\n\n### aws-cli\n\n```bash\naws --endpoint-url \"$HYBRIDS3_URL\" \\\n    --region us-east-1 \\\n    s3api put-object --bucket uploads --key file.txt --body ./file.txt\n# credentials via env: AWS_ACCESS_KEY_ID=<public_key> AWS_SECRET_ACCESS_KEY=<key>\n```\n\nSupported S3 operations: `put_object`, `get_object`, `head_object`, `delete_object`, `list_objects_v2` (honours `Prefix`), `list_buckets`, `generate_presigned_url` (GET + PUT). Not supported: multipart upload, versioning, ACLs, bucket create/delete (boto3's implicit `create_bucket` maps to a no-op).\n\n## Plain HTTP API\n\nBearer auth: pass the bucket's private `key` (or the `master_key`) as `Authorization: Bearer <key>`. Public-bucket **reads** (GET/HEAD/LIST) need no auth; **all writes** and **private-bucket reads** require a key.\n\nRequests carrying an AWS Sig V4 `Authorization` header get S3-style XML back; everything else gets JSON. Every response includes `X-Request-Id` (log correlation) and `X-Content-Type-Options: nosniff`. Errors carry `\"error\"` + `\"request_id\"` fields.\n\n### Objects\n\n| Method | Path | Auth | Description |\n|---|---|---|---|\n| `PUT` | `/{bucket}/{key}` | write | Upload object. Returns `ETag` (MD5 of content). Content type auto-detected; override with a `Content-Type` header. |\n| `GET` | `/{bucket}/{key}` | read | Download object. Returns `ETag`, `Last-Modified`, `Content-Length`. |\n| `HEAD` | `/{bucket}/{key}` | read | Object metadata, no body. |\n| `DELETE` | `/{bucket}/{key}` | write | Delete object — always `204`, even if absent. |\n| `GET` | `/{bucket}` | read | List objects. Query: `prefix`, `max-keys`. |\n| `POST` | `/presign/{bucket}/{key}` | write | Generate a presigned URL. Query: `method` (`GET` default / `PUT`), `expires` (seconds, 1–604800, default 3600). |\n\n**Destructive & irreversible.** `DELETE` removes the object with no undo. An agent must NEVER call it unless the user explicitly asked for that exact key to be deleted; confirm the specific bucket + key first, scope it to the current task, and never enumerate-then-bulk-delete. On a shared bucket used by other callers, this can destroy their data — treat it as admin-only.\n\n```bash\n# upload\ncurl -X PUT \"$HYBRIDS3_URL/uploads/file.txt\" \\\n  -H \"Authorization: Bearer $HYBRIDS3_KEY\" \\\n  --data-binary @file.txt\n\n# upload a nested key (parent dirs created automatically)\ncurl -X PUT \"$HYBRIDS3_URL/uploads/reports/2024/january.pdf\" \\\n  -H \"Authorization: Bearer $HYBRIDS3_KEY\" \\\n  --data-binary @january.pdf\n\n# download from a public bucket — no auth needed\ncurl \"$HYBRIDS3_URL/uploads/file.txt\"\n\n# download from a private bucket\ncurl \"$HYBRIDS3_URL/permanent/doc.pdf\" \\\n  -H \"Authorization: Bearer $HYBRIDS3_KEY\"\n\n# list objects with a prefix\ncurl \"$HYBRIDS3_URL/uploads?prefix=images/&max-keys=50\" \\\n  -H \"Authorization: Bearer $HYBRIDS3_KEY\"\n\n# delete\ncurl -X DELETE \"$HYBRIDS3_URL/uploads/file.txt\" \\\n  -H \"Authorization: Bearer $HYBRIDS3_KEY\"\n```\n\n### Buckets\n\n| Method | Path | Auth | Description |\n|---|---|---|---|\n| `GET` | `/health` | none | `{\"status\":\"ok\"}` liveness check. |\n| `GET` | `/` | master or bucket key | List buckets — master key lists all, bucket key lists only its own. |\n| `HEAD` | `/{bucket}` | read | `200` if bucket exists in config, else `404`. |\n| `PUT` | `/{bucket}` | write | S3-compat no-op: `200` if bucket exists in config, `404` if not. Creates nothing. |\n\n```bash\n# list all buckets — needs the master key\ncurl \"$HYBRIDS3_URL/\" -H \"Authorization: Bearer $HYBRIDS3_MASTER_KEY\"\n\n# a bucket key lists only its own bucket\ncurl \"$HYBRIDS3_URL/\" -H \"Authorization: Bearer $HYBRIDS3_KEY\"\n```\n\n### Presigned URLs (HTTP)\n\n`POST /presign/{bucket}/{key}` requires the bucket key or master key. The result grants exactly one verb on exactly one key. A GET URL can't be used to PUT (the method is baked into the signature).\n\n```bash\n# presigned GET on a PRIVATE bucket → signed, expiring URL\ncurl -X POST \"$HYBRIDS3_URL/presign/permanent/doc.pdf?expires=3600\" \\\n  -H \"Authorization: Bearer $HYBRIDS3_KEY\"\n# → {\"url\":\"http://.../permanent/doc.pdf?X-Amz-Algorithm=...&X-Amz-Signature=...\",\"method\":\"GET\",\"expires\":3600}\n\n# presigned GET on a PUBLIC bucket → plain URL, no signature, no expiry (reads are open anyway)\ncurl -X POST \"$HYBRIDS3_URL/presign/uploads/photo.jpg\" \\\n  -H \"Authorization: Bearer $HYBRIDS3_KEY\"\n# → {\"url\":\"http://.../uploads/photo.jpg\",\"method\":\"GET\",\"expires\":null}\n\n# presigned PUT → ALWAYS signed, even for public buckets (anonymous writes are never allowed)\ncurl -X POST \"$HYBRIDS3_URL/presign/uploads/inbox/report.pdf?method=PUT&expires=600\" \\\n  -H \"Authorization: Bearer $HYBRIDS3_KEY\"\n\n# recipient uploads using only the URL — no Authorization header\ncurl -X PUT \"<presigned-put-url>\" --data-binary @report.pdf\n```\n\nThe bucket's `max_file_size` is enforced server-side during the upload regardless of how it was authenticated (oversized → `413`). Expired or tampered presigned URLs → `403`.\n\n## MCP Endpoint\n\nAn MCP server runs at `POST /mcp/` over the **Streamable HTTP** transport. Client config:\n\n```json\n{\n  \"mcpServers\": {\n    \"hybrids3\": { \"type\": \"streamable-http\", \"url\": \"http://localhost:8080/mcp/\" }\n  }\n}\n```\n\nWire it into Claude Code:\n\n```bash\nclaude mcp add --transport http hybrids3 \"$HYBRIDS3_URL/mcp/\"\n# with endpoint-level auth:\nclaude mcp add --transport http hybrids3 \"$HYBRIDS3_URL/mcp/\" \\\n  --header \"Authorization: Bearer $HYBRIDS3_MASTER_KEY\"\n```\n\n### Auth (two layers)\n\n**Endpoint-level (optional)** — a token that authenticates the *connection* before any tool runs. Master key = full access; a bucket key = limited to that bucket. Send it as `Authorization: Bearer <key>`, or (for clients that can't set headers) as `?auth=<key>` on the URL. A token that matches nothing → `401`; **no token → passes through**, and per-tool auth applies.\n\n**Per-tool** — every tool that touches a bucket takes an `auth_key` argument (the bucket's private `key` or the master key), checked independently of the connection token. Public-bucket reads (`download_object`, `list_objects`, `object_info`) accept an empty `auth_key`; all writes and all private-bucket ops require it.\n\n### Tools\n\n| Tool | Args | Auth | Notes |\n|---|---|---|---|\n| `upload_object` | `bucket`, `key`, `content`, `auth_key`, `content_type=\"\"`, `encoding=\"utf-8\"` | bucket/master key | `encoding=\"base64\"` for binary. Returns `size`, `etag`, `content_type`. |\n| `download_object` | `bucket`, `key`, `auth_key=\"\"`, `encoding=\"utf-8\"` | key on private only | `base64` for binary; UTF-8 that won't decode auto-falls back to base64. **Objects > 50 MB rejected — use HTTP.** |\n| `delete_object` | `bucket`, `key`, `auth_key` | bucket/master key | Idempotent. |\n| `list_objects` | `bucket`, `auth_key=\"\"`, `prefix=\"\"`, `max_keys=100` | key on private only | `max_keys` clamped to 1–1000. |\n| `list_buckets` | `auth_key` | master or bucket key | Master lists all; bucket key lists only its own. |\n| `object_info` | `bucket`, `key`, `auth_key=\"\"` | key on private only | Metadata only (size, content_type, etag, uploaded_at, expires_at). |\n| `presign_url` | `bucket`, `key`, `auth_key`, `method=\"GET\"`, `expires=3600`, `base_url=\"\"`, `host=\"\"` | bucket/master key | `method` GET/PUT. Same public-GET-plain / everything-else-signed logic as the HTTP endpoint. `expires` clamped 1–604800. Set `base_url` to the externally-reachable URL so the link is usable. |\n\n**`delete_object` is destructive & irreversible.** It deletes the object with no undo. An agent must NEVER call it unless the user explicitly asked for that exact key to be deleted; confirm the specific bucket + key first, never enumerate-then-bulk-delete. On a shared bucket used by other callers, this can destroy their data — treat it as admin-only.\n\nAll tools return `structuredContent` with a plain-text fallback. Internal error details are masked.\n\n### Raw JSON-RPC\n\nFor debugging or non-MCP callers, POST JSON-RPC directly. The transport requires the `Accept` header below.\n\n```bash\n# tools/list\ncurl -s \"$HYBRIDS3_URL/mcp/\" \\\n  -H \"Content-Type: application/json\" \\\n  -H \"Accept: application/json, text/event-stream\" \\\n  -d '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/list\"}'\n\n# tools/call — upload\ncurl -s \"$HYBRIDS3_URL/mcp/\" \\\n  -H \"Content-Type: application/json\" \\\n  -H \"Accept: application/json, text/event-stream\" \\\n  -d '{\n    \"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools/call\",\n    \"params\":{\n      \"name\":\"upload_object\",\n      \"arguments\":{\"bucket\":\"uploads\",\"key\":\"note.txt\",\"content\":\"hello\",\"auth_key\":\"'\"$HYBRIDS3_KEY\"'\"}\n    }\n  }'\n```\n\n## Authentication (all modes)\n\n| Credential | Config field | Role | Keep secret? |\n|---|---|---|---|\n| Bucket private key | `key` | Bearer token (HTTP/MCP) **and** `aws_secret_access_key` (S3). Signs/verifies — never transmitted. | **Yes** |\n| Bucket public key | `public_key` | `aws_access_key_id` (S3) + `Credential=` in presigned URLs. Identifies, grants nothing. Defaults to bucket name. | No |\n| Master key | `master_key` | Cross-bucket god key. Works on every bucket + only credential that lists all buckets. | **Yes** |\n| Master public key | `master_public_key` | `aws_access_key_id` paired with the master key (S3). Defaults to `\"master\"`. | No |\n\nBucket visibility:\n\n| | GET / HEAD / LIST | PUT | DELETE / presign |\n|---|---|---|---|\n| `public: true` | no auth | bucket key, master key, or valid presigned PUT | bucket key or master key |\n| `public: false` | bucket key, master key, or valid presigned GET | bucket key, master key, or valid presigned PUT | bucket key or master key |\n\nUnauthorized or non-existent bucket access always returns `404` — callers can't tell \"doesn't exist\" from \"no access\". All key comparisons are constant-time.\n\n## Typical Workflows\n\n### Upload + fetch over plain HTTP\n\n```bash\nexport HYBRIDS3_URL=http://localhost:8080\nexport HYBRIDS3_KEY=<uploads-key>\n\ncurl -X PUT \"$HYBRIDS3_URL/uploads/report.pdf\" \\\n  -H \"Authorization: Bearer $HYBRIDS3_KEY\" --data-binary @report.pdf\ncurl -sO \"$HYBRIDS3_URL/uploads/report.pdf\"     # public bucket → no auth on read\n```\n\nOr via the helper (see [`scripts/hybrids3.sh`](scripts/hybrids3.sh)):\n\n```bash\nHYBRIDS3_URL=http://localhost:8080 HYBRIDS3_KEY=<key> \\\n  scripts/hybrids3.sh put uploads report.pdf ./report.pdf\nscripts/hybrids3.sh list uploads reports/\nscripts/hybrids3.sh get uploads report.pdf ./out.pdf\nscripts/hybrids3.sh delete uploads report.pdf\nscripts/hybrids3.sh buckets                     # needs HYBRIDS3_KEY=<master-key>\n```\n\n### Hand someone a presigned upload URL\n\n```bash\n# you generate it (needs a key); they upload with no credentials\ncurl -X POST \"$HYBRIDS3_URL/presign/uploads/inbox/theirs.zip?method=PUT&expires=600\" \\\n  -H \"Authorization: Bearer $HYBRIDS3_KEY\" | jq -r .url\n# → hand the URL over; recipient: curl -X PUT \"<url>\" --data-binary @theirs.zip\n```\n\n### Drop-in boto3 against your own endpoint\n\nPoint any existing boto3 code at `HYBRIDS3_URL`, set the bucket's `public_key`/`key` as the credential pair, `region_name=\"us-east-1\"`, `signature_version=\"s3v4\"`. See [S3 API](#s3-api).\n\n### Agent-driven storage over MCP\n\nAdd the server (`claude mcp add --transport http hybrids3 \"$HYBRIDS3_URL/mcp/\"`), then call `list_buckets` to discover buckets, `upload_object` / `download_object` / `list_objects` to move data. Pass the bucket `key` as `auth_key`; use the master key only when you need cross-bucket reach.\n\n### Auto-expiring scratch storage\n\nIf a bucket has a `ttl` set (server config), objects delete themselves that long after their last write — overwriting resets the clock. Nothing to do client-side; just write to the bucket.\n\nFile v0.3.6:_meta.json\n\n{\n  \"ownerId\": \"kn79dhvmpjng4rp2jjk8k0v5xx80ccbk\",\n  \"slug\": \"hybrids3\",\n  \"version\": \"0.3.6\",\n  \"publishedAt\": 1785193584989\n}\n\nFile v0.3.6:references/setup.md\n\n# hybrids3 setup\n\nConsumer-side reference for standing up an instance to talk to. If an operator already runs one, you only need its URL and a bucket key — skip to [OpenClaw / ClawHub Config](#openclaw--clawhub-config).\n\n## Requirements\n\n- Docker\n- A `config.yaml` (buckets, keys, master key — see [Configuration](#configuration))\n- A persistent volume/dir for `/data` (object bytes + SQLite metadata)\n- ~1 core / modest RAM — it's a thin FastAPI service, not a cluster\n\nThe container runs as **UID 1000** and expects:\n\n- Config file mounted at `/config/config.yaml`\n- Data at `/data`\n- Listens on port `8080`\n\n## Quick Install\n\nGrab the example config, edit keys + buckets, run:\n\n```bash\n# fetch the example config\nwget -O config.yaml \\\n  https://raw.githubusercontent.com/psyb0t/docker-hybrids3/master/config.example.yaml\n\n# edit it — set master_key and each bucket's key\nvi config.yaml\n\n# run\ndocker run -d --name hybrids3 \\\n    -p 127.0.0.1:8080:8080 \\\n    -v ./config.yaml:/config/config.yaml:ro \\\n    -v hybrids3-data:/data \\\n    psyb0t/hybrids3\n\n# verify\ncurl http://localhost:8080/health      # → {\"status\":\"ok\"}\n```\n\n**`-p 8080:8080` binds all interfaces** — the service becomes reachable from the network, not just localhost. Use `-p 127.0.0.1:8080:8080` (loopback-only) unless remote access is intentionally required, matching the fleet convention.\n\n### docker-compose\n\n```yaml\nservices:\n  hybrids3:\n    image: psyb0t/hybrids3\n    ports:\n      - \"127.0.0.1:8080:8080\"\n    volumes:\n      - ./config.yaml:/config/config.yaml:ro\n      - hybrids3-data:/data\n    restart: unless-stopped\n\nvolumes:\n  hybrids3-data:\n```\n\n**`\"127.0.0.1:8080:8080\"` binds loopback-only.** Use `\"8080:8080\"` only if remote access is intentionally required, matching the fleet convention above. For a real deployment where other hosts or services need to reach it, put hybrids3 on an internal Docker network and front it with a reverse proxy (see [Behind a Reverse Proxy](#behind-a-reverse-proxy)) rather than publishing the port on all interfaces.\n\n**Uploads to a `public: true` bucket are world-readable.** Any bucket with `public: true` serves GET/HEAD/LIST with no authentication at all — anyone who can reach the port can read anything stored there. Don't put sensitive data in a public bucket, and keep the port bound to loopback (or behind an authenticating proxy) unless that's the intended exposure.\n\n## Configuration\n\n**All configuration is the YAML file — there are no application env vars.** Buckets are defined here and only here; there is no create/delete API. Change config and restart to add or remove a bucket.\n\nTop-level keys:\n\n| Field | Default | What it does |\n|---|---|---|\n| `master_key` | `\"\"` | Cross-bucket god key — works on every bucket, only credential that lists all buckets. Keep secret. |\n| `master_public_key` | `\"master\"` | Non-secret `aws_access_key_id` paired with `master_key` in S3 auth. Safe to share. |\n| `cleanup_interval` | `60` (`1m`) | How often the background loop deletes expired objects + orphan files. Duration string (`30s`, `5m`, `1h`) or raw seconds. |\n| `lock_acquire_timeout` | `30` | Seconds a request waits for a per-key lock before `503`. Float. |\n| `lock_hold_timeout` | `300` | Seconds a request may hold a lock before it's force-released with `503`. Guards stalled uploads. |\n| `lock_max_waiters` | `100` | Max requests queued for one key before new ones get `503` immediately. |\n| `path_prefix` | `\"\"` | Serve all routes under a subpath (e.g. `/storage`) for reverse-proxy deployments. `\"/storage\"` and `\"/storage/\"` both normalize. Empty = root. |\n\nPer-bucket fields (under `buckets:`, keyed by bucket name — name must not contain `/` or `..`):\n\n| Field | Default | What it does |\n|---|---|---|\n| `key` | **required** | Private key: Bearer token (HTTP/MCP) + `aws_secret_access_key` (S3). Never transmitted. Keep secret. |\n| `public` | `false` | `true` = anyone reads (GET/HEAD/LIST) without auth; writes still need a key. `false` = auth for everything. |\n| `public_key` | bucket name | Public `aws_access_key_id`; appears in presigned `Credential=`. Safe to share. |\n| `ttl` | `0` | Objects expire this long after last write (overwrite resets). `0` = never. Formats: `30s 5m 1h 1h30m12s 1d 2d12h`. |\n| `max_file_size` | `0` | Reject uploads over this with `413`. Formats: `500B 50KB 10MB 1GB`, `0` = no limit. |\n\nMinimal example (`config.example.yaml`):\n\n```yaml\nmaster_key: \"change-me-to-something-secret\"\nmaster_public_key: \"master\"\ncleanup_interval: 1m\nlock_acquire_timeout: 30\nlock_hold_timeout: 300\nlock_max_waiters: 100\n# path_prefix: /storage\n\nbuckets:\n  uploads:\n    public: true\n    key: \"uploads-secret\"\n    public_key: \"uploads-id\"\n    ttl: 24h\n    max_file_size: 50MB\n\n  permanent:\n    public: false\n    key: \"perm-secret\"\n    public_key: \"permanent-id\"\n    ttl: 0\n    max_file_size: 100MB\n```\n\n## Environment Variables\n\nThe **application** takes no env vars — everything is in `config.yaml`. The only tunables at run time are Docker-level:\n\n| Concern | How | Example |\n|---|---|---|\n| Config path | mount to `/config/config.yaml` | `-v ./config.yaml:/config/config.yaml:ro` |\n| Data location | mount to `/data` | `-v hybrids3-data:/data` |\n| Host port / interface | `docker run -p` | `-p 127.0.0.1:8080:8080` (loopback), `-p 8080:8080` (all interfaces) |\n\nFor S3 clients (`boto3`, `aws-cli`), the credentials aren't hybrids3 env vars — they're the standard AWS SDK ones, sourced from a bucket's config:\n\n| SDK var / arg | Value from config |\n|---|---|\n| `AWS_ACCESS_KEY_ID` / `aws_access_key_id` | the bucket's `public_key` (or `master_public_key`) |\n| `AWS_SECRET_ACCESS_KEY` / `aws_secret_access_key` | the bucket's `key` (or `master_key`) |\n| `AWS_DEFAULT_REGION` / `region_name` | any value — server doesn't check it; `us-east-1` below is just an example |\n| `endpoint_url` | `HYBRIDS3_URL` (e.g. `http://localhost:8080`) |\n\nThe skill's own convenience vars for HTTP/MCP work: `HYBRIDS3_URL`, `HYBRIDS3_KEY`, `HYBRIDS3_PUBLIC_KEY`, `HYBRIDS3_MASTER_KEY`, `HYBRIDS3_MASTER_PUBLIC_KEY`.\n\n## Ports\n\n| Port | Service |\n|---|---|\n| 8080 | HTTP API + S3 API + MCP (`/mcp/`) — all on the same port |\n\nThe container listens on `8080` internally. Map it however you like with `-p`.\n\n## Behind a Reverse Proxy\n\nSet `path_prefix` in config to match the proxy location; hybrids3 serves every route under it natively (including `/{prefix}/health`), so nginx forwards the path **as-is** — no stripping. SigV4 works because the signed path matches what the server sees.\n\n```yaml\n# config.yaml\npath_prefix: /storage\n```\n\n```nginx\nlocation /storage {\n    proxy_pass http://hybrids3:8080;          # NO trailing slash — forwards full path\n    proxy_set_header Host $http_host;         # $http_host, NOT $host — keeps the port for SigV4\n    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n    proxy_set_header X-Forwarded-Proto $scheme;\n}\n```\n\nThen clients use the prefixed URL: `HYBRIDS3_URL=http://yourdomain/storage`, boto3 `endpoint_url=\"http://yourdomain/storage\"`, MCP `http://yourdomain/storage/mcp/`.\n\nA trailing slash in `proxy_pass` (`http://hybrids3:8080/`) strips the prefix and breaks SigV4 — don't. `$host` drops the port and also breaks SigV4 — use `$http_host`.\n\n## Management\n\n```bash\ndocker logs -f hybrids3                 # tail structured JSON logs (one line per entry)\ndocker stop hybrids3                     # stop\ndocker rm hybrids3                       # remove\ndocker pull psyb0t/hybrids3              # update image\ncurl http://localhost:8080/health        # liveness → {\"status\":\"ok\"}\n```\n\nLogs are structured JSON to stdout with a per-request `rid` that matches the `X-Request-Id` response header, so you can trace one request end-to-end. The private `key` is never logged.\n\n## OpenClaw / ClawHub Config\n\n```bash\nexport HYBRIDS3_URL=http://localhost:8080\nexport HYBRIDS3_KEY=<bucket-private-key>        # only if you'll write / read private buckets\nexport HYBRIDS3_MASTER_KEY=<master-key>         # only for cross-bucket / list-all\n```\n\nOr via `~/.openclaw/openclaw.json`:\n\n```json\n{\n  \"skills\": {\n    \"entries\": {\n      \"hybrids3\": {\n        \"env\": {\n          \"HYBRIDS3_URL\": \"http://localhost:8080\",\n          \"HYBRIDS3_KEY\": \"<bucket-private-key>\"\n        }\n      }\n    }\n  }\n}\n```\n\nFile v0.3.6:skill-card.md\n\n## Description: <br>\nSelf-hosted lightweight object storage that lets agents and developers use S3-compatible, plain HTTP, and MCP interfaces to put, get, list, delete, and presign objects in a user-run storage service. <br>\n\nThis skill is ready for commercial/non-commercial use. <br>\n\n## Publisher: <br>\n[psyb0t](https://clawhub.ai/user/psyb0t) <br>\n\n### License/Terms of Use: <br>\nMIT-0 <br>\n\n\n## Use Case: <br>\nDevelopers, operators, and agents use this skill to interact with a self-hosted object store through S3-compatible clients, curl-friendly HTTP requests, or MCP tools. It is intended for storing, retrieving, listing, deleting, and sharing objects with bucket-scoped credentials and presigned URLs. <br>\n\n### Deployment Geography for Use: <br>\nGlobal <br>\n\n## Known Risks and Mitigations: <br>\nRisk: An agent with bucket or master credentials can read, write, delete, or presign access to objects within that credential scope. <br>\nMitigation: Install only for trusted endpoints, keep bucket and master keys private, and scope credentials to the narrowest bucket needed for the task. <br>\nRisk: Public buckets are readable by anyone who can reach the service. <br>\nMitigation: Use private buckets for sensitive data and expose public buckets only when anonymous reads are intentional. <br>\nRisk: Deletes and overwrites are irreversible in the documented storage model. <br>\nMitigation: Require explicit confirmation of the exact bucket and key before delete operations, overwrites, or presigned PUT URLs. <br>\nRisk: Binding the service to a public interface can expose object-storage APIs outside the intended network. <br>\nMitigation: Bind to localhost or an internal network by default and use a controlled reverse proxy when remote access is required. <br>\n\n\n## Reference(s): <br>\n- [hybrids3 setup](references/setup.md) <br>\n- [ClawHub skill page](https://clawhub.ai/psyb0t/skills/hybrids3) <br>\n\n\n## Skill Output: <br>\n**Output Type(s):** [Shell commands, API calls, Configuration, Guidance] <br>\n**Output Format:** [Markdown guidance with shell, JSON, Python, YAML, and HTTP examples] <br>\n**Output Parameters:** [1D] <br>\n**Other Properties Related to Output:** [May produce object-storage requests, presigned URLs, and file transfer commands scoped to the configured endpoint and credentials.] <br>\n\n## Skill Version(s): <br>\n0.3.6 (source: server release metadata) <br>\n\n## Ethical Considerations: <br>\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment. <br>\n\nArchive v0.3.5: 5 files, 15891 bytes\n\nFiles: references/setup.md (8312b), scripts/hybrids3.sh (6187b), skill-card.md (1975b), SKILL.md (22017b), _meta.json (127b)\n\nFile v0.3.5:SKILL.md\n\n---\nname: hybrids3\ndescription: Self-hosted lightweight object storage speaking three interfaces at once — the S3 API (boto3 / AWS-SDK compatible, AWS Sig V4), a plain HTTP API (curl-friendly, Bearer auth), and MCP (Streamable HTTP tools for agents). Buckets/objects with SQLite metadata and flat-file storage on disk; buckets are config-defined (no create/delete API), each with a public/private flag, a private key + public key pair, optional TTL expiry, and a max upload size. Auth is per-bucket keys plus a cross-bucket master key; presigned GET/PUT URLs for handing out single-object access. Use when the user wants to put/get/list/delete files in a self-hosted S3-compatible store, generate presigned upload/download links, drive their own boto3-compatible object storage, or connect an agent to object storage over MCP.\nhomepage: https://github.com/psyb0t/docker-hybrids3\nuser-invocable: true\npermissions:\n  - \"network: outbound HTTP(S) to the configured HYBRIDS3_URL object-storage endpoint\"\n  - \"object storage: read/write/delete objects (upload, download, list, delete) and generate presigned GET/PUT URLs, scoped to whatever bucket key is provided\"\nmetadata:\n  { \"openclaw\": { \"emoji\": \"🪣\", \"primaryEnv\": \"HYBRIDS3_URL\", \"requires\": { \"bins\": [\"docker\", \"curl\"] } } }\n---\n\n# hybrids3\n\nLightweight object storage that speaks three interfaces from one container: the **S3 API** (boto3 / any AWS SDK, AWS Signature V4), a **plain HTTP API** (`curl` + Bearer token), and **MCP** (Streamable HTTP tool definitions for AI agents). Metadata lives in SQLite, object bytes are flat files on disk.\n\nBuckets are **configuration, not state** — they're defined in the server's `config.yaml` and there is no API to create or delete them. Each bucket carries: a `public`/private flag, a private `key` (Bearer token + S3 signing secret), a `public_key` (the `aws_access_key_id` identifier), an optional `ttl` (objects auto-expire after last write), and an optional `max_file_size`. A single cross-bucket `master_key` operates on every bucket and is the only credential that can list all buckets.\n\nObject keys support nested paths (`reports/2024/jan.pdf`). Content type is auto-detected on upload (libmagic sniff + extension fallback). Presigned GET/PUT URLs hand out time-limited single-object access without exposing a key.\n\nFor installation, configuration, and container setup, see [references/setup.md](references/setup.md).\n\n## Security & safety\n\n- **Public buckets are world-readable.** A bucket with `public: true` serves `GET`/`HEAD`/`LIST` to anyone who can reach the port — no auth at all. Only store data there that's meant to be openly readable; keep sensitive data in a `public: false` bucket.\n- **Bind loopback, not all interfaces.** The container listens on `8080`. Publishing it as `-p 8080:8080` / compose `\"8080:8080\"` makes it reachable from the network by default. Use `-p 127.0.0.1:8080:8080` (loopback) unless remote access is intentionally required, and for real deployments put the service on an internal Docker network behind a reverse proxy instead of exposing the port directly — see [references/setup.md](references/setup.md#quick-install) and [Behind a Reverse Proxy](references/setup.md#behind-a-reverse-proxy).\n- **`DELETE`, presigned `PUT`, and MCP `delete_object` are destructive & irreversible.** There's no undo, no versioning, no recycle bin — a deleted object (or one overwritten via `PUT`) is gone. An agent must NEVER call `DELETE` / `delete_object`, or generate/use a presigned `PUT` that overwrites, unless the user explicitly asked for that exact key to be removed or replaced; confirm the specific bucket + key first, never enumerate-then-bulk-delete, and treat bulk cleanup as requiring explicit per-object user confirmation.\n- **Bucket keys, master key, and presigned URLs are bearer credentials.** Whoever holds a bucket's private `key` or the `master_key` (or a live presigned URL) can act as that bucket/master until the credential is rotated or the URL expires — there's no per-caller revocation. Never paste these into shared prompts, logs, or presigned URLs handed to untrusted parties beyond the one object they're meant to grant.\n- **This skill is a consumer, not an operator.** It talks to an instance the user already runs; it doesn't provision, harden, or expose the server. Don't use this skill to reconfigure `config.yaml`, change bucket visibility, or alter port bindings on the user's behalf without being explicitly asked.\n\n## When To Use\n\n- Put / get / list / delete files in a self-hosted, S3-compatible object store.\n- Drive existing boto3 / AWS-SDK code against your own endpoint (change `endpoint_url` + credentials, nothing else).\n- Store artifacts, uploads, reports, backups that services and agents read/write.\n- Generate a **presigned PUT URL** so a third party can upload one specific key without your bucket key.\n- Generate a **presigned GET URL** so a third party can download one private object for a limited time.\n- Connect an AI agent to object storage over **MCP** — structured `upload_object` / `download_object` / `list_objects` / etc. tools.\n- Auto-expiring scratch storage — set a bucket `ttl` and objects clean themselves up.\n\n## When NOT To Use\n\n- Creating or deleting buckets at runtime — buckets live in `config.yaml`; add/remove them there and restart. `PUT /{bucket}` is an S3-compat no-op.\n- Multipart / chunked uploads (S3's multipart protocol) — not implemented. Upload the whole object in one request.\n- Object versioning, ACLs beyond bucket-level public/private, replication, or encryption at rest — none are supported.\n- Downloading objects **over 50 MB via MCP** — the MCP `download_object` tool caps at 50 MB. Use the S3 or plain HTTP `GET` for large objects.\n- CORS from a browser — no CORS headers are emitted; add them at the reverse-proxy layer.\n- Provisioning, configuring, or hardening the server itself — this skill is a **consumer**. It talks to an instance the user already runs and trusts.\n\n## Setup\n\nThe container should already be running. Point the skill at it:\n\n```bash\nexport HYBRIDS3_URL=http://localhost:8080\n```\n\n**Verify:** `curl $HYBRIDS3_URL/health` returns `{\"status\":\"ok\"}`.\n\nKeys are per-bucket and come from the operator's `config.yaml`. Export the ones you'll use rather than pasting them inline:\n\n```bash\nexport HYBRIDS3_KEY=<bucket-private-key>          # Bearer token / aws_secret_access_key\nexport HYBRIDS3_PUBLIC_KEY=<bucket-public-key>    # aws_access_key_id (defaults to bucket name)\n# For cross-bucket / list-all-buckets operations, use the master credentials instead:\nexport HYBRIDS3_MASTER_KEY=<master-key>\nexport HYBRIDS3_MASTER_PUBLIC_KEY=<master-public-key>   # defaults to \"master\"\n```\n\nIf the server runs behind a reverse proxy at a subpath, `HYBRIDS3_URL` includes it (e.g. `http://host/storage`) and every route — health included — lives under that prefix.\n\nFor install / `config.yaml` shape / bucket + key definitions / ports / reverse proxy, see [references/setup.md](references/setup.md).\n\n## Three Access Modes\n\nOne service, three front doors on the **same port** (`8080`). Pick per client:\n\n| Mode | Endpoint | Auth | Reach for it when… |\n|---|---|---|---|\n| **S3 API** | root (`/`), signed requests | AWS Sig V4 (`public_key` = access key id, `key` = secret) | You already have boto3 / AWS-SDK code, or want SDK ergonomics (pagination, presign helpers, retries). |\n| **Plain HTTP** | `/{bucket}/{key}` etc. | `Authorization: Bearer <key>` | Shell scripts, `curl`, quick one-offs. No SDK, no signing — just a Bearer header (and public-bucket reads need none). |\n| **MCP** | `POST /mcp/` | per-connection + per-tool `auth_key` | An AI agent should manipulate storage through typed tool calls instead of raw HTTP. |\n\nThey share one SQLite metadata store and one flat-file tree — an object written over S3 is readable over plain HTTP and MCP, and vice versa.\n\n## S3 API\n\nS3 clients authenticate with **AWS Signature V4**. The bucket's `public_key` is the `aws_access_key_id` (travels in the request in plaintext, in the `Credential=` field); the bucket's private `key` is the `aws_secret_access_key` (used only to compute the HMAC locally — never transmitted). `region_name` is required by SDKs but the value is arbitrary — use `us-east-1`.\n\n```\nAuthorization: AWS4-HMAC-SHA256 Credential=uploads-id/20240101/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-date, Signature=<hmac>\n```\n\n### boto3\n\n```python\nimport boto3\nfrom botocore.config import Config\n\n# per-bucket client — access limited to the \"uploads\" bucket\ns3 = boto3.client(\n    \"s3\",\n    endpoint_url=\"http://localhost:8080\",     # HYBRIDS3_URL\n    aws_access_key_id=\"uploads-id\",           # bucket public_key\n    aws_secret_access_key=\"uploads-secret\",   # bucket key (private)\n    region_name=\"us-east-1\",                  # arbitrary but required\n    config=Config(signature_version=\"s3v4\"),\n)\n\ns3.put_object(Bucket=\"uploads\", Key=\"file.txt\", Body=b\"hello\")\ns3.get_object(Bucket=\"uploads\", Key=\"file.txt\")[\"Body\"].read()\ns3.head_object(Bucket=\"uploads\", Key=\"file.txt\")\ns3.list_objects_v2(Bucket=\"uploads\", Prefix=\"images/\")\ns3.delete_object(Bucket=\"uploads\", Key=\"file.txt\")\ns3.list_buckets()                             # bucket key → only \"uploads\"\n\n# presigned URLs (see MCP / HTTP presign for the same semantics)\nget_url = s3.generate_presigned_url(\n    \"get_object\", Params={\"Bucket\": \"permanent\", \"Key\": \"doc.pdf\"}, ExpiresIn=3600,\n)\nput_url = s3.generate_presigned_url(\n    \"put_object\", Params={\"Bucket\": \"uploads\", \"Key\": \"inbox/report.pdf\"}, ExpiresIn=600,\n)\n```\n\nFor cross-bucket access or `list_buckets()` returning **all** buckets, build a second client with the **master** credentials (`aws_access_key_id=master_public_key`, `aws_secret_access_key=master_key`). Don't embed the master key in client-facing code — per-bucket keys scope access to one bucket.\n\n**Destructive & irreversible.** `delete_object` deletes the object with no undo — no versioning, no recycle bin. An agent must NEVER call it unless the user explicitly asked for that exact key to be deleted; confirm the specific bucket + key first, scope it to the current task, and never enumerate-then-bulk-delete. On a shared instance where multiple callers use the same bucket, this can destroy another caller's data — treat bulk deletes as admin-only and requiring explicit confirmation.\n\n### aws-cli\n\n```bash\naws --endpoint-url \"$HYBRIDS3_URL\" \\\n    --region us-east-1 \\\n    s3api put-object --bucket uploads --key file.txt --body ./file.txt\n# credentials via env: AWS_ACCESS_KEY_ID=<public_key> AWS_SECRET_ACCESS_KEY=<key>\n```\n\nSupported S3 operations: `put_object`, `get_object`, `head_object`, `delete_object`, `list_objects_v2` (honours `Prefix`), `list_buckets`, `generate_presigned_url` (GET + PUT). Not supported: multipart upload, versioning, ACLs, bucket create/delete (boto3's implicit `create_bucket` maps to a no-op).\n\n## Plain HTTP API\n\nBearer auth: pass the bucket's private `key` (or the `master_key`) as `Authorization: Bearer <key>`. Public-bucket **reads** (GET/HEAD/LIST) need no auth; **all writes** and **private-bucket reads** require a key.\n\nRequests carrying an AWS Sig V4 `Authorization` header get S3-style XML back; everything else gets JSON. Every response includes `X-Request-Id` (log correlation) and `X-Content-Type-Options: nosniff`. Errors carry `\"error\"` + `\"request_id\"` fields.\n\n### Objects\n\n| Method | Path | Auth | Description |\n|---|---|---|---|\n| `PUT` | `/{bucket}/{key}` | write | Upload object. Returns `ETag` (MD5 of content). Content type auto-detected; override with a `Content-Type` header. |\n| `GET` | `/{bucket}/{key}` | read | Download object. Returns `ETag`, `Last-Modified`, `Content-Length`. |\n| `HEAD` | `/{bucket}/{key}` | read | Object metadata, no body. |\n| `DELETE` | `/{bucket}/{key}` | write | Delete object — always `204`, even if absent. |\n| `GET` | `/{bucket}` | read | List objects. Query: `prefix`, `max-keys`. |\n| `POST` | `/presign/{bucket}/{key}` | write | Generate a presigned URL. Query: `method` (`GET` default / `PUT`), `expires` (seconds, 1–604800, default 3600). |\n\n**Destructive & irreversible.** `DELETE` removes the object with no undo. An agent must NEVER call it unless the user explicitly asked for that exact key to be deleted; confirm the specific bucket + key first, scope it to the current task, and never enumerate-then-bulk-delete. On a shared bucket used by other callers, this can destroy their data — treat it as admin-only.\n\n```bash\n# upload\ncurl -X PUT \"$HYBRIDS3_URL/uploads/file.txt\" \\\n  -H \"Authorization: Bearer $HYBRIDS3_KEY\" \\\n  --data-binary @file.txt\n\n# upload a nested key (parent dirs created automatically)\ncurl -X PUT \"$HYBRIDS3_URL/uploads/reports/2024/january.pdf\" \\\n  -H \"Authorization: Bearer $HYBRIDS3_KEY\" \\\n  --data-binary @january.pdf\n\n# download from a public bucket — no auth needed\ncurl \"$HYBRIDS3_URL/uploads/file.txt\"\n\n# download from a private bucket\ncurl \"$HYBRIDS3_URL/permanent/doc.pdf\" \\\n  -H \"Authorization: Bearer $HYBRIDS3_KEY\"\n\n# list objects with a prefix\ncurl \"$HYBRIDS3_URL/uploads?prefix=images/&max-keys=50\" \\\n  -H \"Authorization: Bearer $HYBRIDS3_KEY\"\n\n# delete\ncurl -X DELETE \"$HYBRIDS3_URL/uploads/file.txt\" \\\n  -H \"Authorization: Bearer $HYBRIDS3_KEY\"\n```\n\n### Buckets\n\n| Method | Path | Auth | Description |\n|---|---|---|---|\n| `GET` | `/health` | none | `{\"status\":\"ok\"}` liveness check. |\n| `GET` | `/` | master or bucket key | List buckets — master key lists all, bucket key lists only its own. |\n| `HEAD` | `/{bucket}` | read | `200` if bucket exists in config, else `404`. |\n| `PUT` | `/{bucket}` | write | S3-compat no-op: `200` if bucket exists in config, `404` if not. Creates nothing. |\n\n```bash\n# list all buckets — needs the master key\ncurl \"$HYBRIDS3_URL/\" -H \"Authorization: Bearer $HYBRIDS3_MASTER_KEY\"\n\n# a bucket key lists only its own bucket\ncurl \"$HYBRIDS3_URL/\" -H \"Authorization: Bearer $HYBRIDS3_KEY\"\n```\n\n### Presigned URLs (HTTP)\n\n`POST /presign/{bucket}/{key}` requires the bucket key or master key. The result grants exactly one verb on exactly one key. A GET URL can't be used to PUT (the method is baked into the signature).\n\n```bash\n# presigned GET on a PRIVATE bucket → signed, expiring URL\ncurl -X POST \"$HYBRIDS3_URL/presign/permanent/doc.pdf?expires=3600\" \\\n  -H \"Authorization: Bearer $HYBRIDS3_KEY\"\n# → {\"url\":\"http://.../permanent/doc.pdf?X-Amz-Algorithm=...&X-Amz-Signature=...\",\"method\":\"GET\",\"expires\":3600}\n\n# presigned GET on a PUBLIC bucket → plain URL, no signature, no expiry (reads are open anyway)\ncurl -X POST \"$HYBRIDS3_URL/presign/uploads/photo.jpg\" \\\n  -H \"Authorization: Bearer $HYBRIDS3_KEY\"\n# → {\"url\":\"http://.../uploads/photo.jpg\",\"method\":\"GET\",\"expires\":null}\n\n# presigned PUT → ALWAYS signed, even for public buckets (anonymous writes are never allowed)\ncurl -X POST \"$HYBRIDS3_URL/presign/uploads/inbox/report.pdf?method=PUT&expires=600\" \\\n  -H \"Authorization: Bearer $HYBRIDS3_KEY\"\n\n# recipient uploads using only the URL — no Authorization header\ncurl -X PUT \"<presigned-put-url>\" --data-binary @report.pdf\n```\n\nThe bucket's `max_file_size` is enforced server-side during the upload regardless of how it was authenticated (oversized → `413`). Expired or tampered presigned URLs → `403`.\n\n## MCP Endpoint\n\nAn MCP server runs at `POST /mcp/` over the **Streamable HTTP** transport. Client config:\n\n```json\n{\n  \"mcpServers\": {\n    \"hybrids3\": { \"type\": \"streamable-http\", \"url\": \"http://localhost:8080/mcp/\" }\n  }\n}\n```\n\nWire it into Claude Code:\n\n```bash\nclaude mcp add --transport http hybrids3 \"$HYBRIDS3_URL/mcp/\"\n# with endpoint-level auth:\nclaude mcp add --transport http hybrids3 \"$HYBRIDS3_URL/mcp/\" \\\n  --header \"Authorization: Bearer $HYBRIDS3_MASTER_KEY\"\n```\n\n### Auth (two layers)\n\n**Endpoint-level (optional)** — a token that authenticates the *connection* before any tool runs. Master key = full access; a bucket key = limited to that bucket. Send it as `Authorization: Bearer <key>`, or (for clients that can't set headers) as `?auth=<key>` on the URL. A token that matches nothing → `401`; **no token → passes through**, and per-tool auth applies.\n\n**Per-tool** — every tool that touches a bucket takes an `auth_key` argument (the bucket's private `key` or the master key), checked independently of the connection token. Public-bucket reads (`download_object`, `list_objects`, `object_info`) accept an empty `auth_key`; all writes and all private-bucket ops require it.\n\n### Tools\n\n| Tool | Args | Auth | Notes |\n|---|---|---|---|\n| `upload_object` | `bucket`, `key`, `content`, `auth_key`, `content_type=\"\"`, `encoding=\"utf-8\"` | bucket/master key | `encoding=\"base64\"` for binary. Returns `size`, `etag`, `content_type`. |\n| `download_object` | `bucket`, `key`, `auth_key=\"\"`, `encoding=\"utf-8\"` | key on private only | `base64` for binary; UTF-8 that won't decode auto-falls back to base64. **Objects > 50 MB rejected — use HTTP.** |\n| `delete_object` | `bucket`, `key`, `auth_key` | bucket/master key | Idempotent. |\n| `list_objects` | `bucket`, `auth_key=\"\"`, `prefix=\"\"`, `max_keys=100` | key on private only | `max_keys` clamped to 1–1000. |\n| `list_buckets` | `auth_key` | master or bucket key | Master lists all; bucket key lists only its own. |\n| `object_info` | `bucket`, `key`, `auth_key=\"\"` | key on private only | Metadata only (size, content_type, etag, uploaded_at, expires_at). |\n| `presign_url` | `bucket`, `key`, `auth_key`, `method=\"GET\"`, `expires=3600`, `base_url=\"\"`, `host=\"\"` | bucket/master key | `method` GET/PUT. Same public-GET-plain / everything-else-signed logic as the HTTP endpoint. `expires` clamped 1–604800. Set `base_url` to the externally-reachable URL so the link is usable. |\n\n**`delete_object` is destructive & irreversible.** It deletes the object with no undo. An agent must NEVER call it unless the user explicitly asked for that exact key to be deleted; confirm the specific bucket + key first, never enumerate-then-bulk-delete. On a shared bucket used by other callers, this can destroy their data — treat it as admin-only.\n\nAll tools return `structuredContent` with a plain-text fallback. Internal error details are masked.\n\n### Raw JSON-RPC\n\nFor debugging or non-MCP callers, POST JSON-RPC directly. The transport requires the `Accept` header below.\n\n```bash\n# tools/list\ncurl -s \"$HYBRIDS3_URL/mcp/\" \\\n  -H \"Content-Type: application/json\" \\\n  -H \"Accept: application/json, text/event-stream\" \\\n  -d '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/list\"}'\n\n# tools/call — upload\ncurl -s \"$HYBRIDS3_URL/mcp/\" \\\n  -H \"Content-Type: application/json\" \\\n  -H \"Accept: application/json, text/event-stream\" \\\n  -d '{\n    \"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools/call\",\n    \"params\":{\n      \"name\":\"upload_object\",\n      \"arguments\":{\"bucket\":\"uploads\",\"key\":\"note.txt\",\"content\":\"hello\",\"auth_key\":\"'\"$HYBRIDS3_KEY\"'\"}\n    }\n  }'\n```\n\n## Authentication (all modes)\n\n| Credential | Config field | Role | Keep secret? |\n|---|---|---|---|\n| Bucket private key | `key` | Bearer token (HTTP/MCP) **and** `aws_secret_access_key` (S3). Signs/verifies — never transmitted. | **Yes** |\n| Bucket public key | `public_key` | `aws_access_key_id` (S3) + `Credential=` in presigned URLs. Identifies, grants nothing. Defaults to bucket name. | No |\n| Master key | `master_key` | Cross-bucket god key. Works on every bucket + only credential that lists all buckets. | **Yes** |\n| Master public key | `master_public_key` | `aws_access_key_id` paired with the master key (S3). Defaults to `\"master\"`. | No |\n\nBucket visibility:\n\n| | GET / HEAD / LIST | PUT | DELETE / presign |\n|---|---|---|---|\n| `public: true` | no auth | bucket key, master key, or valid presigned PUT | bucket key or master key |\n| `public: false` | bucket key, master key, or valid presigned GET | bucket key, master key, or valid presigned PUT | bucket key or master key |\n\nUnauthorized or non-existent bucket access always returns `404` — callers can't tell \"doesn't exist\" from \"no access\". All key comparisons are constant-time.\n\n## Typical Workflows\n\n### Upload + fetch over plain HTTP\n\n```bash\nexport HYBRIDS3_URL=http://localhost:8080\nexport HYBRIDS3_KEY=<uploads-key>\n\ncurl -X PUT \"$HYBRIDS3_URL/uploads/report.pdf\" \\\n  -H \"Authorization: Bearer $HYBRIDS3_KEY\" --data-binary @report.pdf\ncurl -sO \"$HYBRIDS3_URL/uploads/report.pdf\"     # public bucket → no auth on read\n```\n\nOr via the helper (see [`scripts/hybrids3.sh`](scripts/hybrids3.sh)):\n\n```bash\nHYBRIDS3_URL=http://localhost:8080 HYBRIDS3_KEY=<key> \\\n  scripts/hybrids3.sh put uploads report.pdf ./report.pdf\nscripts/hybrids3.sh list uploads reports/\nscripts/hybrids3.sh get uploads report.pdf ./out.pdf\nscripts/hybrids3.sh delete uploads report.pdf\nscripts/hybrids3.sh buckets                     # needs HYBRIDS3_KEY=<master-key>\n```\n\n### Hand someone a presigned upload URL\n\n```bash\n# you generate it (needs a key); they upload with no credentials\ncurl -X POST \"$HYBRIDS3_URL/presign/uploads/inbox/theirs.zip?method=PUT&expires=600\" \\\n  -H \"Authorization: Bearer $HYBRIDS3_KEY\" | jq -r .url\n# → hand the URL over; recipient: curl -X PUT \"<url>\" --data-binary @theirs.zip\n```\n\n### Drop-in boto3 against your own endpoint\n\nPoint any existing boto3 code at `HYBRIDS3_URL`, set the bucket's `public_key`/`key` as the credential pair, `region_name=\"us-east-1\"`, `signature_version=\"s3v4\"`. See [S3 API](#s3-api).\n\n### Agent-driven storage over MCP\n\nAdd the server (`claude mcp add --transport http hybrids3 \"$HYBRIDS3_URL/mcp/\"`), then call `list_buckets` to discover buckets, `upload_object` / `download_object` / `list_objects` to move data. Pass the bucket `key` as `auth_key`; use the master key only when you need cross-bucket reach.\n\n### Auto-expiring scratch storage\n\nIf a bucket has a `ttl` set (server config), objects delete themselves that long after their last write — overwriting resets the clock. Nothing to do client-side; just write to the bucket.\n\nFile v0.3.5:_meta.json\n\n{\n  \"ownerId\": \"kn79dhvmpjng4rp2jjk8k0v5xx80ccbk\",\n  \"slug\": \"hybrids3\",\n  \"version\": \"0.3.5\",\n  \"publishedAt\": 1785165611747\n}\n\nFile v0.3.5:references/setup.md\n\n# hybrids3 setup\n\nConsumer-side reference for standing up an instance to talk to. If an operator already runs one, you only need its URL and a bucket key — skip to [OpenClaw / ClawHub Config](#openclaw--clawhub-config).\n\n## Requirements\n\n- Docker\n- A `config.yaml` (buckets, keys, master key — see [Configuration](#configuration))\n- A persistent volume/dir for `/data` (object bytes + SQLite metadata)\n- ~1 core / modest RAM — it's a thin FastAPI service, not a cluster\n\nThe container runs as **UID 1000** and expects:\n\n- Config file mounted at `/config/config.yaml`\n- Data at `/data`\n- Listens on port `8080`\n\n## Quick Install\n\nGrab the example config, edit keys + buckets, run:\n\n```bash\n# fetch the example config\nwget -O config.yaml \\\n  https://raw.githubusercontent.com/psyb0t/docker-hybrids3/master/config.example.yaml\n\n# edit it — set master_key and each bucket's key\nvi config.yaml\n\n# run\ndocker run -d --name hybrids3 \\\n    -p 127.0.0.1:8080:8080 \\\n    -v ./config.yaml:/config/config.yaml:ro \\\n    -v hybrids3-data:/data \\\n    psyb0t/hybrids3\n\n# verify\ncurl http://localhost:8080/health      # → {\"status\":\"ok\"}\n```\n\n**`-p 8080:8080` binds all interfaces** — the service becomes reachable from the network, not just localhost. Use `-p 127.0.0.1:8080:8080` (loopback-only) unless remote access is intentionally required, matching the fleet convention.\n\n### docker-compose\n\n```yaml\nservices:\n  hybrids3:\n    image: psyb0t/hybrids3\n    ports:\n      - \"127.0.0.1:8080:8080\"\n    volumes:\n      - ./config.yaml:/config/config.yaml:ro\n      - hybrids3-data:/data\n    restart: unless-stopped\n\nvolumes:\n  hybrids3-data:\n```\n\n**`\"127.0.0.1:8080:8080\"` binds loopback-only.** Use `\"8080:8080\"` only if remote access is intentionally required, matching the fleet convention above. For a real deployment where other hosts or services need to reach it, put hybrids3 on an internal Docker network and front it with a reverse proxy (see [Behind a Reverse Proxy](#behind-a-reverse-proxy)) rather than publishing the port on all interfaces.\n\n**Uploads to a `public: true` bucket are world-readable.** Any bucket with `public: true` serves GET/HEAD/LIST with no authentication at all — anyone who can reach the port can read anything stored there. Don't put sensitive data in a public bucket, and keep the port bound to loopback (or behind an authenticating proxy) unless that's the intended exposure.\n\n## Configuration\n\n**All configuration is the YAML file — there are no application env vars.** Buckets are defined here and only here; there is no create/delete API. Change config and restart to add or remove a bucket.\n\nTop-level keys:\n\n| Field | Default | What it does |\n|---|---|---|\n| `master_key` | `\"\"` | Cross-bucket god key — works on every bucket, only credential that lists all buckets. Keep secret. |\n| `master_public_key` | `\"master\"` | Non-secret `aws_access_key_id` paired with `master_key` in S3 auth. Safe to share. |\n| `cleanup_interval` | `60` (`1m`) | How often the background loop deletes expired objects + orphan files. Duration string (`30s`, `5m`, `1h`) or raw seconds. |\n| `lock_acquire_timeout` | `30` | Seconds a request waits for a per-key lock before `503`. Float. |\n| `lock_hold_timeout` | `300` | Seconds a request may hold a lock before it's force-released with `503`. Guards stalled uploads. |\n| `lock_max_waiters` | `100` | Max requests queued for one key before new ones get `503` immediately. |\n| `path_prefix` | `\"\"` | Serve all routes under a subpath (e.g. `/storage`) for reverse-proxy deployments. `\"/storage\"` and `\"/storage/\"` both normalize. Empty = root. |\n\nPer-bucket fields (under `buckets:`, keyed by bucket name — name must not contain `/` or `..`):\n\n| Field | Default | What it does |\n|---|---|---|\n| `key` | **required** | Private key: Bearer token (HTTP/MCP) + `aws_secret_access_key` (S3). Never transmitted. Keep secret. |\n| `public` | `false` | `true` = anyone reads (GET/HEAD/LIST) without auth; writes still need a key. `false` = auth for everything. |\n| `public_key` | bucket name | Public `aws_access_key_id`; appears in presigned `Credential=`. Safe to share. |\n| `ttl` | `0` | Objects expire this long after last write (overwrite resets). `0` = never. Formats: `30s 5m 1h 1h30m12s 1d 2d12h`. |\n| `max_file_size` | `0` | Reject uploads over this with `413`. Formats: `500B 50KB 10MB 1GB`, `0` = no limit. |\n\nMinimal example (`config.example.yaml`):\n\n```yaml\nmaster_key: \"change-me-to-something-secret\"\nmaster_public_key: \"master\"\ncleanup_interval: 1m\nlock_acquire_timeout: 30\nlock_hold_timeout: 300\nlock_max_waiters: 100\n# path_prefix: /storage\n\nbuckets:\n  uploads:\n    public: true\n    key: \"uploads-secret\"\n    public_key: \"uploads-id\"\n    ttl: 24h\n    max_file_size: 50MB\n\n  permanent:\n    public: false\n    key: \"perm-secret\"\n    public_key: \"permanent-id\"\n    ttl: 0\n    max_file_size: 100MB\n```\n\n## Environment Variables\n\nThe **application** takes no env vars — everything is in `config.yaml`. The only tunables at run time are Docker-level:\n\n| Concern | How | Example |\n|---|---|---|\n| Config path | mount to `/config/config.yaml` | `-v ./config.yaml:/config/config.yaml:ro` |\n| Data location | mount to `/data` | `-v hybrids3-data:/data` |\n| Host port / interface | `docker run -p` | `-p 127.0.0.1:8080:8080` (loopback), `-p 8080:8080` (all interfaces) |\n\nFor S3 clients (`boto3`, `aws-cli`), the credentials aren't hybrids3 env vars — they're the standard AWS SDK ones, sourced from a bucket's config:\n\n| SDK var / arg | Value from config |\n|---|---|\n| `AWS_ACCESS_KEY_ID` / `aws_access_key_id` | the bucket's `public_key` (or `master_public_key`) |\n| `AWS_SECRET_ACCESS_KEY` / `aws_secret_access_key` | the bucket's `key` (or `master_key`) |\n| `AWS_DEFAULT_REGION` / `region_name` | any value — server doesn't check it; `us-east-1` below is just an example |\n| `endpoint_url` | `HYBRIDS3_URL` (e.g. `http://localhost:8080`) |\n\nThe skill's own convenience vars for HTTP/MCP work: `HYBRIDS3_URL`, `HYBRIDS3_KEY`, `HYBRIDS3_PUBLIC_KEY`, `HYBRIDS3_MASTER_KEY`, `HYBRIDS3_MASTER_PUBLIC_KEY`.\n\n## Ports\n\n| Port | Service |\n|---|---|\n| 8080 | HTTP API + S3 API + MCP (`/mcp/`) — all on the same port |\n\nThe container listens on `8080` internally. Map it however you like with `-p`.\n\n## Behind a Reverse Proxy\n\nSet `path_prefix` in config to match the proxy location; hybrids3 serves every route under it natively (including `/{prefix}/health`), so nginx forwards the path **as-is** — no stripping. SigV4 works because the signed path matches what the server sees.\n\n```yaml\n# config.yaml\npath_prefix: /storage\n```\n\n```nginx\nlocation /storage {\n    proxy_pass http://hybrids3:8080;          # NO trailing slash — forwards full path\n    proxy_set_header Host $http_host;         # $http_host, NOT $host — keeps the port for SigV4\n    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n    proxy_set_header X-Forwarded-Proto $scheme;\n}\n```\n\nThen clients use the prefixed URL: `HYBRIDS3_URL=http://yourdomain/storage`, boto3 `endpoint_url=\"http://yourdomain/storage\"`, MCP `http://yourdomain/storage/mcp/`.\n\nA trailing slash in `proxy_pass` (`http://hybrids3:8080/`) strips the prefix and breaks SigV4 — don't. `$host` drops the port and also breaks SigV4 — use `$http_host`.\n\n## Management\n\n```bash\ndocker logs -f hybrids3                 # tail structured JSON logs (one line per entry)\ndocker stop hybrids3                     # stop\ndocker rm hybrids3                       # remove\ndocker pull psyb0t/hybrids3              # update image\ncurl http://localhost:8080/health        # liveness → {\"status\":\"ok\"}\n```\n\nLogs are structured JSON to stdout with a per-request `rid` that matches the `X-Request-Id` response header, so you can trace one request end-to-end. The private `key` is never logged.\n\n## OpenClaw / ClawHub Config\n\n```bash\nexport HYBRIDS3_URL=http://localhost:8080\nexport HYBRIDS3_KEY=<bucket-private-key>        # only if you'll write / read private buckets\nexport HYBRIDS3_MASTER_KEY=<master-key>         # only for cross-bucket / list-all\n```\n\nOr via `~/.openclaw/openclaw.json`:\n\n```json\n{\n  \"skills\": {\n    \"entries\": {\n      \"hybrids3\": {\n        \"env\": {\n          \"HYBRIDS3_URL\": \"http://localhost:8080\",\n          \"HYBRIDS3_KEY\": \"<bucket-private-key>\"\n        }\n      }\n    }\n  }\n}\n```\n\nFile v0.3.5:skill-card.md\n\n## Description: <br>\nhybrids3 helps agents and developers work with a self-hosted object store through S3-compatible, plain HTTP, and MCP interfaces for upload, download, listing, deletion, and presigned URL workflows. <br>\n\nThis skill is ready for commercial/non-commercial use. <br>\n\n## Publisher: <br>\n[psyb0t](https://clawhub.ai/user/psyb0t) <br>\n\n### License/Terms of Use: <br>\nMIT-0 <br>\n\n\n## Use Case: <br>\nDevelopers, operators, and agent users use this skill to move files through a self-hosted object store, generate presigned links, and connect MCP-capable agents to storage. It is appropriate when users already run and trust a hybrids3 endpoint and can supply the required bucket or master credentials. <br>\n\n### Deployment Geography for Use: <br>\nGlobal <br>\n\n## Known Risks and Mitigations: <br>\nRisk: Review before execution as proposals could introduce incorrect or misleading guidance into skills. <br>\nMitigation: Review and scan skill before deployment. <br>\n\n## Reference(s): <br>\n- [hybrids3 setup guide](references/setup.md) <br>\n- [ClawHub skill page](https://clawhub.ai/psyb0t/skills/hybrids3) <br>\n\n\n## Skill Output: <br>\n**Output Type(s):** [text, markdown, code, shell commands, configuration, guidance] <br>\n**Output Format:** [Markdown guidance with shell commands, HTTP examples, JSON snippets, and configuration examples] <br>\n**Output Parameters:** [1D] <br>\n**Other Properties Related to Output:** [Requires HYBRIDS3_URL plus bucket or master credentials for privileged operations; outputs may include object bytes, JSON responses, metadata, or presigned URLs depending on the workflow.] <br>\n\n## Skill Version(s): <br>\n0.3.5 (source: server release metadata) <br>\n\n## Ethical Considerations: <br>\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment. <br>\n\nArchive v0.3.4: 5 files, 16313 bytes\n\nFiles: references/setup.md (8312b), scripts/hybrids3.sh (6187b), skill-card.md (2910b), SKILL.md (22017b), _meta.json (127b)\n\nFile v0.3.4:SKILL.md\n\n---\nname: hybrids3\ndescription: Self-hosted lightweight object storage speaking three interfaces at once — the S3 API (boto3 / AWS-SDK compatible, AWS Sig V4), a plain HTTP API (curl-friendly, Bearer auth), and MCP (Streamable HTTP tools for agents). Buckets/objects with SQLite metadata and flat-file storage on disk; buckets are config-defined (no create/delete API), each with a public/private flag, a private key + public key pair, optional TTL expiry, and a max upload size. Auth is per-bucket keys plus a cross-bucket master key; presigned GET/PUT URLs for handing out single-object access. Use when the user wants to put/get/list/delete files in a self-hosted S3-compatible store, generate presigned upload/download links, drive their own boto3-compatible object storage, or connect an agent to object storage over MCP.\nhomepage: https://github.com/psyb0t/docker-hybrids3\nuser-invocable: true\npermissions:\n  - \"network: outbound HTTP(S) to the configured HYBRIDS3_URL object-storage endpoint\"\n  - \"object storage: read/write/delete objects (upload, download, list, delete) and generate presigned GET/PUT URLs, scoped to whatever bucket key is provided\"\nmetadata:\n  { \"openclaw\": { \"emoji\": \"🪣\", \"primaryEnv\": \"HYBRIDS3_URL\", \"requires\": { \"bins\": [\"docker\", \"curl\"] } } }\n---\n\n# hybrids3\n\nLightweight object storage that speaks three interfaces from one container: the **S3 API** (boto3 / any AWS SDK, AWS Signature V4), a **plain HTTP API** (`curl` + Bearer token), and **MCP** (Streamable HTTP tool definitions for AI agents). Metadata lives in SQLite, object bytes are flat files on disk.\n\nBuckets are **configuration, not state** — they're defined in the server's `config.yaml` and there is no API to create or delete them. Each bucket carries: a `public`/private flag, a private `key` (Bearer token + S3 signing secret), a `public_key` (the `aws_access_key_id` identifier), an optional `ttl` (objects auto-expire after last write), and an optional `max_file_size`. A single cross-bucket `master_key` operates on every bucket and is the only credential that can list all buckets.\n\nObject keys support nested paths (`reports/2024/jan.pdf`). Content type is auto-detected on upload (libmagic sniff + extension fallback). Presigned GET/PUT URLs hand out time-limited single-object access without exposing a key.\n\nFor installation, configuration, and container setup, see [references/setup.md](references/setup.md).\n\n## Security & safety\n\n- **Public buckets are world-readable.** A bucket with `public: true` serves `GET`/`HEAD`/`LIST` to anyone who can reach the port — no auth at all. Only store data there that's meant to be openly readable; keep sensitive data in a `public: false` bucket.\n- **Bind loopback, not all interfaces.** The container listens on `8080`. Publishing it as `-p 8080:8080` / compose `\"8080:8080\"` makes it reachable from the network by default. Use `-p 127.0.0.1:8080:8080` (loopback) unless remote access is intentionally required, and for real deployments put the service on an internal Docker network behind a reverse proxy instead of exposing the port directly — see [references/setup.md](references/setup.md#quick-install) and [Behind a Reverse Proxy](references/setup.md#behind-a-reverse-proxy).\n- **`DELETE`, presigned `PUT`, and MCP `delete_object` are destructive & irreversible.** There's no undo, no versioning, no recycle bin — a deleted object (or one overwritten via `PUT`) is gone. An agent must NEVER call `DELETE` / `delete_object`, or generate/use a presigned `PUT` that overwrites, unless the user explicitly asked for that exact key to be removed or replaced; confirm the specific bucket + key first, never enumerate-then-bulk-delete, and treat bulk cleanup as requiring explicit per-object user confirmation.\n- **Bucket keys, master key, and presigned URLs are bearer credentials.** Whoever holds a bucket's private `key` or the `master_key` (or a live presigned URL) can act as that bucket/master until the credential is rotated or the URL expires — there's no per-caller revocation. Never paste these into shared prompts, logs, or presigned URLs handed to untrusted parties beyond the one object they're meant to grant.\n- **This skill is a consumer, not an operator.** It talks to an instance the user already runs; it doesn't provision, harden, or expose the server. Don't use this skill to reconfigure `config.yaml`, change bucket visibility, or alter port bindings on the user's behalf without being explicitly asked.\n\n## When To Use\n\n- Put / get / list / delete files in a self-hosted, S3-compatible object store.\n- Drive existing boto3 / AWS-SDK code against your own endpoint (change `endpoint_url` + credentials, nothing else).\n- Store artifacts, uploads, reports, backups that services and agents read/write.\n- Generate a **presigned PUT URL** so a third party can upload one specific key without your bucket key.\n- Generate a **presigned GET URL** so a third party can download one private object for a limited time.\n- Connect an AI agent to object storage over **MCP** — structured `upload_object` / `download_object` / `list_objects` / etc. tools.\n- Auto-expiring scratch storage — set a bucket `ttl` and objects clean themselves up.\n\n## When NOT To Use\n\n- Creating or deleting buckets at runtime — buckets live in `config.yaml`; add/remove them there and restart. `PUT /{bucket}` is an S3-compat no-op.\n- Multipart / chunked uploads (S3's multipart protocol) — not implemented. Upload the whole object in one request.\n- Object versioning, ACLs beyond bucket-level public/private, replication, or encryption at rest — none are supported.\n- Downloading objects **over 50 MB via MCP** — the MCP `download_object` tool caps at 50 MB. Use the S3 or plain HTTP `GET` for large objects.\n- CORS from a browser — no CORS headers are emitted; add them at the reverse-proxy layer.\n- Provisioning, configuring, or hardening the server itself — this skill is a **consumer**. It talks to an instance the user already runs and trusts.\n\n## Setup\n\nThe container should already be running. Point the skill at it:\n\n```bash\nexport HYBRIDS3_URL=http://localhost:8080\n```\n\n**Verify:** `curl $HYBRIDS3_URL/health` returns `{\"status\":\"ok\"}`.\n\nKeys are per-bucket and come from the operator's `config.yaml`. Export the ones you'll use rather than pasting them inline:\n\n```bash\nexport HYBRIDS3_KEY=<bucket-private-key>          # Bearer token / aws_secret_access_key\nexport HYBRIDS3_PUBLIC_KEY=<bucket-public-key>    # aws_access_key_id (defaults to bucket name)\n# For cross-bucket / list-all-buckets operations, use the master credentials instead:\nexport HYBRIDS3_MASTER_KEY=<master-key>\nexport HYBRIDS3_MASTER_PUBLIC_KEY=<master-public-key>   # defaults to \"master\"\n```\n\nIf the server runs behind a reverse proxy at a subpath, `HYBRIDS3_URL` includes it (e.g. `http://host/storage`) and every route — health included — lives under that prefix.\n\nFor install / `config.yaml` shape / bucket + key definitions / ports / reverse proxy, see [references/setup.md](references/setup.md).\n\n## Three Access Modes\n\nOne service, three front doors on the **same port** (`8080`). Pick per client:\n\n| Mode | Endpoint | Auth | Reach for it when… |\n|---|---|---|---|\n| **S3 API** | root (`/`), signed requests | AWS Sig V4 (`public_key` = access key id, `key` = secret) | You already have boto3 / AWS-SDK code, or want SDK ergonomics (pagination, presign helpers, retries). |\n| **Plain HTTP** | `/{bucket}/{key}` etc. | `Authorization: Bearer <key>` | Shell scripts, `curl`, quick one-offs. No SDK, no signing — just a Bearer header (and public-bucket reads need none). |\n| **MCP** | `POST /mcp/` | per-connection + per-tool `auth_key` | An AI agent should manipulate storage through typed tool calls instead of raw HTTP. |\n\nThey share one SQLite metadata store and one flat-file tree — an object written over S3 is readable over plain HTTP and MCP, and vice versa.\n\n## S3 API\n\nS3 clients authenticate with **AWS Signature V4**. The bucket's `public_key` is the `aws_access_key_id` (travels in the request in plaintext, in the `Credential=` field); the bucket's private `key` is the `aws_secret_access_key` (used only to compute the HMAC locally — never transmitted). `region_name` is required by SDKs but the value is arbitrary — use `us-east-1`.\n\n```\nAuthorization: AWS4-HMAC-SHA256 Credential=uploads-id/20240101/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-date, Signature=<hmac>\n```\n\n### boto3\n\n```python\nimport boto3\nfrom botocore.config import Config\n\n# per-bucket client — access limited to the \"uploads\" bucket\ns3 = boto3.client(\n    \"s3\",\n    endpoint_url=\"http://localhost:8080\",     # HYBRIDS3_URL\n    aws_access_key_id=\"uploads-id\",           # bucket public_key\n    aws_secret_access_key=\"uploads-secret\",   # bucket key (private)\n    region_name=\"us-east-1\",                  # arbitrary but required\n    config=Config(signature_version=\"s3v4\"),\n)\n\ns3.put_object(Bucket=\"uploads\", Key=\"file.txt\", Body=b\"hello\")\ns3.get_object(Bucket=\"uploa\n\nArchive v0.3.3: 5 files, 16161 bytes\n\nFiles: references/setup.md (8312b), scripts/hybrids3.sh (6187b), skill-card.md (2540b), SKILL.md (22017b), _meta.json (127b)\n\nArchive v0.3.2: 5 files, 16247 bytes\n\nFiles: references/setup.md (8312b), scripts/hybrids3.sh (6187b), skill-card.md (2789b), SKILL.md (22017b), _meta.json (127b)\n\nArchive v0.3.1: 5 files, 14980 bytes\n\nFiles: references/setup.md (7532b), scripts/hybrids3.sh (6187b), skill-card.md (2858b), SKILL.md (18723b), _meta.json (127b)\n\nArchive v0.3.0: 5 files, 14656 bytes\n\nFiles: references/setup.md (7236b), scripts/hybrids3.sh (6187b), skill-card.md (2614b), SKILL.md (18463b), _meta.json (127b)\n\nArchive v0.2.2: 5 files, 14580 bytes\n\nFiles: references/setup.md (7236b), scripts/hybrids3.sh (6187b), skill-card.md (2457b), SKILL.md (18463b), _meta.json (127b)","readmeExcerpt":"Skill: hybrids3 Owner: psyb0t Summary: Self-hosted lightweight object storage speaking three interfaces at once — the S3 API (boto3 / AWS-SDK compatible, AWS Sig V4), a plain HTTP API (curl-friendly, Bearer auth), and MCP (Streamable HTTP tools for agents). Buckets/objects with SQLite metadata and flat-file storage on disk; buckets are config-defined (no create/delete API), each with a public/private flag, a private ","codeSnippets":[],"executableExamples":[{"language":"bash","snippet":"export HYBRIDS3_URL=http://localhost:8080"},{"language":"bash","snippet":"export HYBRIDS3_KEY=<bucket-private-key>          # Bearer token / aws_secret_access_key\nexport HYBRIDS3_PUBLIC_KEY=<bucket-public-key>    # aws_access_key_id (defaults to bucket name)\n# For cross-bucket / list-all-buckets operations, use the master credentials instead:\nexport HYBRIDS3_MASTER_KEY=<master-key>\nexport HYBRIDS3_MASTER_PUBLIC_KEY=<master-public-key>   # defaults to \"master\""},{"language":"text","snippet":"Authorization: AWS4-HMAC-SHA256 Credential=uploads-id/20240101/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-date, Signature=<hmac>"},{"language":"python","snippet":"import boto3\nfrom botocore.config import Config\n\n# per-bucket client — access limited to the \"uploads\" bucket\ns3 = boto3.client(\n    \"s3\",\n    endpoint_url=\"http://localhost:8080\",     # HYBRIDS3_URL\n    aws_access_key_id=\"uploads-id\",           # bucket public_key\n    aws_secret_access_key=\"uploads-secret\",   # bucket key (private)\n    region_name=\"us-east-1\",                  # arbitrary but required\n    config=Config(signature_version=\"s3v4\"),\n)\n\ns3.put_object(Bucket=\"uploads\", Key=\"file.txt\", Body=b\"hello\")\ns3.get_object(Bucket=\"uploads\", Key=\"file.txt\")[\"Body\"].read()\ns3.head_object(Bucket=\"uploads\", Key=\"file.txt\")\ns3.list_objects_v2(Bucket=\"uploads\", Prefix=\"images/\")\ns3.delete_object(Bucket=\"uploads\", Key=\"file.txt\")\ns3.list_buckets()                             # bucket key → only \"uploads\"\n\n# presigned URLs (see MCP / HTTP presign for the same semantics)\nget_url = s3.generate_presigned_url(\n    \"get_object\", Params={\"Bucket\": \"permanent\", \"Key\": \"doc.pdf\"}, ExpiresIn=3600,\n)\nput_url = s3.generate_presigned_url(\n    \"put_object\", Params={\"Bucket\": \"uploads\", \"Key\": \"inbox/report.pdf\"}, ExpiresIn=600,\n)"},{"language":"bash","snippet":"aws --endpoint-url \"$HYBRIDS3_URL\" \\\n    --region us-east-1 \\\n    s3api put-object --bucket uploads --key file.txt --body ./file.txt\n# credentials via env: AWS_ACCESS_KEY_ID=<public_key> AWS_SECRET_ACCESS_KEY=<key>"},{"language":"bash","snippet":"curl -X PUT \"$HYBRIDS3_URL/uploads/file.txt\" \\\n  -H \"Authorization: Bearer $HYBRIDS3_KEY\" \\\n  --data-binary @file.txt"}],"parameters":null,"dependencies":[],"permissions":[],"extractedFiles":[{"path":"SKILL.md","content":"---\nname: hybrids3\ndescription: Self-hosted lightweight object storage speaking three interfaces at once — the S3 API (boto3 / AWS-SDK compatible, AWS Sig V4), a plain HTTP API (curl-friendly, Bearer auth), and MCP (Streamable HTTP tools for agents). Buckets/objects with SQLite metadata and flat-file storage on disk; buckets are config-defined (no create/delete API), each with a public/private flag, a private key + public key pair, optional TTL expiry, and a max upload size. Auth is per-bucket keys plus a cross-bucket master key; presigned GET/PUT URLs for handing out single-object access. Use when the user wants to put/get/list/delete files in a self-hosted S3-compatible store, generate presigned upload/download links, drive their own boto3-compatible object storage, or connect an agent to object storage over MCP.\nhomepage: https://github.com/psyb0t/docker-hybrids3\nuser-invocable: true\npermissions:\n  - \"network: outbound HTTP(S) to the configured HYBRIDS3_URL object-storage endpoint\"\n  - \"object storage: read/write/delete objects (upload, download, list, delete) and generate presigned GET/PUT URLs, scoped to whatever bucket key is provided\"\nmetadata:\n  { \"openclaw\": { \"emoji\": \"🪣\", \"primaryEnv\": \"HYBRIDS3_URL\", \"requires\": { \"bins\": [\"docker\", \"curl\"] } } }\n---\n\n# hybrids3\n\nLightweight object storage that speaks three interfaces from one container: the **S3 API** (boto3 / any AWS SDK, AWS Signature V4), a **plain HTTP API** (`curl` + Bearer token), and **MCP** (Streamable HTTP tool definitions for AI agents). Metadata lives in SQLite, object bytes are flat files on disk.\n\nBuckets are **configuration, not state** — they're defined in the server's `config.yaml` and there is no API to create or delete them. Each bucket carries: a `public`/private flag, a private `key` (Bearer token + S3 signing secret), a `public_key` (the `aws_access_key_id` identifier), an optional `ttl` (objects auto-expire after last write), and an optional `max_file_size`. A single cross-bucket `master_key` operates on every bucket and is the only credential that can list all buckets.\n\nObject keys support nested paths (`reports/2024/jan.pdf`). Content type is auto-detected on upload (libmagic sniff + extension fallback). Presigned GET/PUT URLs hand out time-limited single-object access without exposing a key.\n\nFor installation, configuration, and container setup, see [references/setup.md](references/setup.md).\n\n## Security & safety\n\n- **Public buckets are world-readable.** A bucket with `public: true` serves `GET`/`HEAD`/`LIST` to anyone who can reach the port — no auth at all. Only store data there that's meant to be openly readable; keep sensitive data in a `public: false` bucket.\n- **Bind loopback, not all interfaces.** The container listens on `8080`. Publishing it as `-p 8080:8080` / compose `\"8080:8080\"` makes it reachable from the network by default. Use `-p 127.0.0.1:8080:8080` (loopback) unless remote access is intentionally required, and for real deployments put the"},{"path":"_meta.json","content":"{\n  \"ownerId\": \"kn79dhvmpjng4rp2jjk8k0v5xx80ccbk\",\n  \"slug\": \"hybrids3\",\n  \"version\": \"0.3.8\",\n  \"publishedAt\": 1785616193715\n}"},{"path":"references/setup.md","content":"# hybrids3 setup\n\nConsumer-side reference for standing up an instance to talk to. If an operator already runs one, you only need its URL and a bucket key — skip to [OpenClaw / ClawHub Config](#openclaw--clawhub-config).\n\n## Requirements\n\n- Docker\n- A `config.yaml` (buckets, keys, master key — see [Configuration](#configuration))\n- A persistent volume/dir for `/data` (object bytes + SQLite metadata)\n- ~1 core / modest RAM — it's a thin FastAPI service, not a cluster\n\nThe container runs as **UID 1000** and expects:\n\n- Config file mounted at `/config/config.yaml`\n- Data at `/data`\n- Listens on port `8080`\n\n## Quick Install\n\nGrab the example config, edit keys + buckets, run:\n\n```bash\n# fetch the example config\nwget -O config.yaml \\\n  https://raw.githubusercontent.com/psyb0t/docker-hybrids3/master/config.example.yaml\n\n# edit it — set master_key and each bucket's key\nvi config.yaml\n\n# run\ndocker run -d --name hybrids3 \\\n    -p 127.0.0.1:8080:8080 \\\n    -v ./config.yaml:/config/config.yaml:ro \\\n    -v hybrids3-data:/data \\\n    psyb0t/hybrids3\n\n# verify\ncurl http://localhost:8080/health      # → {\"status\":\"ok\"}\n```\n\n**`-p 8080:8080` binds all interfaces** — the service becomes reachable from the network, not just localhost. Use `-p 127.0.0.1:8080:8080` (loopback-only) unless remote access is intentionally required, matching the fleet convention.\n\n### docker-compose\n\n```yaml\nservices:\n  hybrids3:\n    image: psyb0t/hybrids3\n    ports:\n      - \"127.0.0.1:8080:8080\"\n    volumes:\n      - ./config.yaml:/config/config.yaml:ro\n      - hybrids3-data:/data\n    restart: unless-stopped\n\nvolumes:\n  hybrids3-data:\n```\n\n**`\"127.0.0.1:8080:8080\"` binds loopback-only.** Use `\"8080:8080\"` only if remote access is intentionally required, matching the fleet convention above. For a real deployment where other hosts or services need to reach it, put hybrids3 on an internal Docker network and front it with a reverse proxy (see [Behind a Reverse Proxy](#behind-a-reverse-proxy)) rather than publishing the port on all interfaces.\n\n**Uploads to a `public: true` bucket are world-readable.** Any bucket with `public: true` serves GET/HEAD/LIST with no authentication at all — anyone who can reach the port can read anything stored there. Don't put sensitive data in a public bucket, and keep the port bound to loopback (or behind an authenticating proxy) unless that's the intended exposure.\n\n## Configuration\n\n**All configuration is the YAML file — there are no application env vars.** Buckets are defined here and only here; there is no create/delete API. Change config and restart to add or remove a bucket.\n\nTop-level keys:\n\n| Field | Default | What it does |\n|---|---|---|\n| `master_key` | `\"\"` | Cross-bucket god key — works on every bucket, only credential that lists all buckets. Keep secret. |\n| `master_public_key` | `\"master\"` | Non-secret `aws_access_key_id` paired with `master_key` in S3 auth. Safe to share. |\n| `cleanup_interval` | `60` (`1m`) | How often the background loop deletes "},{"path":"skill-card.md","content":"## Description:\n\nhybrids3 helps agents and developers work with a self-hosted object store through S3-compatible clients, a plain HTTP API, and MCP tools.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[psyb0t](https://clawhub.ai/user/psyb0t)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nDevelopers, operators, and agent builders use this skill to upload, download, list, delete, and presign objects in a user-managed hybrids3 storage service. It is most useful when a workflow needs S3-compatible storage, curl-friendly HTTP access, or structured MCP object-storage tools.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: Remote deployments can expose powerful storage credentials and objects if the endpoint or public buckets are reachable by unintended users.\n\nMitigation: Use loopback or HTTPS-only access, keep sensitive data in private buckets, and expose the service through a controlled reverse proxy when remote access is required.\n\nRisk: Master keys, bucket keys, and query-string auth or presigned URLs can act as bearer credentials and may leak through prompts, logs, shell history, or shared links.\n\nMitigation: Use bucket-scoped keys instead of the master key, keep credentials in environment or approved secret storage, avoid ?auth= URLs, and issue short-lived presigned URLs.\n\nRisk: Delete operations and presigned PUT uploads can remove or overwrite object data without an undo path.\n\nMitigation: Confirm the exact bucket and key before destructive actions, avoid bulk delete workflows, and require explicit user approval before replacing existing objects.\n\nRisk: Using an untrusted or mutable container image source can introduce supply-chain risk.\n\nMitigation: Install only from trusted image sources and prefer a pinned image digest for deployments.\n\n## Reference(s):\n\n- [hybrids3 setup](references/setup.md)\n- [hybrids3 homepage](https://github.com/psyb0t/docker-hybrids3)\n- [ClawHub skill page](https://clawhub.ai/psyb0t/skills/hybrids3)\n\n## Skill Output:\n\n**Output Type(s):** [text, markdown, code, shell commands, configuration, guidance]\n\n**Output Format:** [Markdown with inline code blocks, shell commands, JSON snippets, and configuration examples]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [May produce API requests or MCP tool guidance that affects objects in the configured storage endpoint.]\n\n## Skill Version(s):\n\n0.3.8 (source: server release evidence)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment."}],"languages":[],"docsSourceLabel":"CLAWHUB","editorialOverview":null,"editorialQuality":{"score":100,"threshold":65,"status":"thin","wordCount":1805,"uniquenessScore":39,"reasons":["uniqueness-below-45"]}},"media":{"evidence":{"source":"no-media","verified":false,"confidence":"low","updatedAt":"2026-10-11T19:58:08.024Z","emptyReason":"No screenshots, media assets, or demo links are available."},"primaryImageUrl":null,"mediaAssetCount":0,"assets":[],"demoUrl":null},"ownerResources":{"evidence":{"source":"unclaimed","verified":false,"confidence":"low","updatedAt":"2026-10-11T19:58:08.024Z","emptyReason":"This page has not been claimed by the agent owner."},"hasCustomPage":false,"customPageUpdatedAt":null,"customLinks":[],"structuredLinks":{"docsUrl":null,"demoUrl":null,"supportUrl":null,"pricingUrl":null,"statusUrl":null},"customPage":null},"relatedAgents":{"evidence":{"source":"protocol-neighbors","verified":false,"confidence":"medium","updatedAt":"2026-10-12T13:09:35.036Z","emptyReason":null},"items":[{"id":"8ebccd8e-3863-4187-8355-c3f14e1f9edf","entityType":"agent","canonicalPath":"/agent/iofficeai-aionui","slug":"iofficeai-aionui","name":"AionUi","description":"Free, local, open-source 24/7 Cowork app and OpenClaw for Gemini CLI, Claude Code, Codex, OpenCode, Qwen Code, Goose CLI, Auggie, and more | 🌟 Star if you like it!","url":"https://github.com/iOfficeAI/AionUi","homepage":"https://www.aionui.com","source":"GITHUB_REPOS","protocols":["MCP","OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-10-09T19:11:12.944Z","createdAt":"2026-02-25T03:38:16.584Z","downloads":null},{"id":"b917f68a-ebff-438e-84f8-3f4b2494c0bc","entityType":"agent","canonicalPath":"/agent/activepieces-activepieces","slug":"activepieces-activepieces","name":"activepieces","description":"AI Agents & MCPs & AI Workflow Automation • (~400 MCP servers for AI agents) • AI Automation / AI Agent with MCPs • AI Workflows & AI Agents • MCPs for AI Agents","url":"https://github.com/activepieces/activepieces","homepage":"https://www.activepieces.com","source":"GITHUB_REPOS","protocols":["OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-04-15T02:22:12.426Z","createdAt":"2026-02-25T03:38:12.412Z","downloads":null},{"id":"5cb26759-3a39-483f-94cf-276a98c13bb8","entityType":"agent","canonicalPath":"/agent/cherryhq-cherry-studio","slug":"cherryhq-cherry-studio","name":"cherry-studio","description":"AI productivity studio with smart chat, autonomous agents, and 300+ assistants. Unified access to frontier LLMs","url":"https://github.com/CherryHQ/cherry-studio","homepage":"https://cherry-ai.com","source":"GITHUB_REPOS","protocols":["MCP","OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-04-11T14:38:40.986Z","createdAt":"2026-02-25T03:38:19.379Z","downloads":null},{"id":"6f6582d0-5d76-4f0f-b81d-86520247950b","entityType":"agent","canonicalPath":"/agent/copilotkit-copilotkit","slug":"copilotkit-copilotkit","name":"CopilotKit","description":"The Frontend for Agents & Generative UI. React + Angular","url":"https://github.com/CopilotKit/CopilotKit","homepage":"https://docs.copilotkit.ai","source":"GITHUB_REPOS","protocols":["OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-03-25T09:50:57.846Z","createdAt":"2026-02-25T03:39:14.617Z","downloads":null}],"links":{"hub":"/agent","source":"/agent/source/clawhub","protocols":[{"label":"OpenClaw","href":"/agent/protocol/openclew"}]}}}