A Cloudflare Worker that gives your company's agents somewhere to put assets.
Writes are gated on GitHub identity: an agent proves it controls an authorized GitHub account by publishing a short-lived gist, and trades that proof for a bearer token. Assets live in an R2 bucket and are served straight back out over HTTP.
The worker serves its own API documentation as Markdown at GET /, so an agent
pointed at the root URL can work out how to use it without any other context.
- The agent generates a random string (32–128 characters).
- It publishes a gist containing the SHA-256 of that string.
- It posts
[gistUrl, randomString]to/authorize. - The worker fetches the gist, checks the hash matches, and checks the gist's owner is on the authorized list.
- It returns a bearer token, valid for 10 minutes.
The gist only proves account ownership, so it can be deleted immediately. No GitHub token is ever handed to the worker.
GIST_URL=$(echo -n "<your random string>" | { sha256sum 2>/dev/null || shasum -a 256; } | gh gist create)
TOKEN=$(curl -s -X POST https://<your-worker-host>/authorize \
-H "Content-Type: application/json" \
-d "[\"$GIST_URL\", \"<your random string>\"]")
gh gist delete --yes "$GIST_URL"Every endpoint except POST /authorize and asset reads requires
Authorization: Bearer <token>.
| Method | Path | Description |
|---|---|---|
GET |
/ |
API documentation as Markdown. |
POST |
/authorize |
Exchange a gist proof for a token. Body: [gistUrl, str]. |
DELETE |
/authorize |
Revoke the calling token. |
GET |
/users |
Newline-separated list of authorized GitHub accounts. |
PUT |
/users?user=<name> |
Authorize a GitHub account. |
DELETE |
/users?user=<name> |
Deauthorize a GitHub account. |
GET |
<path> |
Download an asset. No authentication required. |
POST |
<path> |
Upload or replace an asset. |
DELETE |
<path> |
Delete an asset. |
Uploads require Content-Type and Content-Length; the body is stored
verbatim and the content type is echoed back on download. /, /users and
/authorize are reserved and cannot be used as asset paths.
Usernames and asset paths are case-sensitive.
Important
Asset reads are public. Anything uploaded is readable by anyone who knows its URL, without a token. Only writes are gated. Do not store secrets here.
Requires Node.js and a Cloudflare account with R2 enabled.
npm installCreate the bucket that backs the worker:
npx wrangler r2 bucket create agentgram-bucketThen set the bootstrap account in wrangler.jsonc. INIT_AUTH is the GitHub
username that is always authorized — it is implicitly on the user list, cannot
be removed via the API, and is how you get your first token:
Deploy:
npm run deployAuthorize your teammates' accounts with PUT /users?user=<name> once you have a
token. To hand the worker over to a different owner, change INIT_AUTH and
redeploy.
npm run dev # local dev server
npm run typecheck # tsc --noEmit
npm run format # prettier --write .
npm run format:check # verify formatting
npm run cf-typegen # regenerate worker-configuration.d.ts from wrangler.jsoncObservability is enabled in wrangler.jsonc, and source maps are uploaded, so
runtime exceptions come back with mapped stack traces:
npx wrangler tail --format json --status errorEverything lives in the one R2 bucket:
| Key | Contents |
|---|---|
users |
Newline-separated authorized accounts, excluding INIT_AUTH. |
authorize |
JSON array of [token, expiryMs] for live tokens. |
| anything else | An uploaded asset, keyed by its URL path. |
Both metadata objects are mutated through a compare-and-swap helper that retries on an ETag mismatch, so concurrent updates from multiple agents will not clobber each other.
MIT — see LICENSE. Copyright (c) 2026 Infrawrench LLC.