Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
91 changes: 91 additions & 0 deletions docs/blog/2026-04-22-org-api-keys/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
---
title: "Named, Revocable, Audited: Org-Scoped API Keys for AI Agent Platforms"
date: 2026-04-22
slug: org-scoped-api-keys
description: "Molecule AI now lets you mint per-integration org-scoped API keys β€” named, revocable, with a full audit trail. Here's why the old ADMIN_TOKEN pattern was a problem, and what the replacement looks like."
tags: [security, platform, api, enterprise]
---

# Named, Revocable, Audited: Org-Scoped API Keys for AI Agent Platforms

Your Molecule AI tenant has a secret that can do everything: create workspaces, read secrets, rotate credentials, import org definitions, mint more tokens.

That secret is `ADMIN_TOKEN`.

You've probably been warned not to share it. You've probably also shared it β€” to Zapier, to a CI pipeline, to the AI agent you're trying to get productive. Because that's what it was designed for. A single bootstrap credential that unlocks everything.

The problem isn't that you shared it. The problem is the model itself. One shared secret with no name, no audit trail, and no way to revoke it without taking down every integration that holds a copy.

Org-scoped API keys solve this. Every key is named, individually revocable, and carries an `org:keyId` prefix through every request. Mint one for Zapier, one for your GitHub Actions deploy agent, one for the AI agent you're running in production. When something goes wrong β€” or when a contractor leaves β€” revoke one key. Nothing else breaks.

## Why ADMIN_TOKEN is a single point of failure

`ADMIN_TOKEN` has three problems that compound at scale:

**No name.** When the token is compromised or needs rotation, you have to find every copy. The CI pipeline, the Zapier webhook, the internal bot, the agent's environment file. One missed copy means a window where the old token still works.

**No revocation granularity.** Rotate `ADMIN_TOKEN` and you break every integration simultaneously. There's no "revoke the Zapier access but keep the GitHub Actions one" β€” it's all or nothing. This makes rotation a coordination event rather than a surgical action.

**No audit trail.** A request hits `/workspaces`. Was that Zapier? The deploy agent? The AI agent doing its nightly sweep? You can't tell from logs alone β€” only the `Authorization: Bearer` header, and that header is the same value everywhere.

For a team of 10 with one or two integrations, this is manageable friction. For a team running a production agent fleet β€” contractors, AI agents, pipelines, third-party webhooks β€” it's an operational hazard.

## What org-scoped keys give you

Mint a key from the canvas UI (Settings β†’ Org API Keys β†’ New Key) or from the CLI:

```bash
curl -X POST https://acme.moleculesai.app/org/tokens \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-d '{"name": "zapier-webhook"}'
```

The response returns the full plaintext token **once**. Copy it, store it in your secret manager, and hand it to Zapier.

Now when Zapier calls your tenant, logs show:

```
org:keyId=zpier-wh_abc123 last_used_at=2026-04-22T09:14:22Z
```

You know exactly which integration made which call. When someone leaves or a hook gets compromised:

```bash
curl -X DELETE https://acme.moleculesai.app/org/tokens/zapier-webhook \
-H "Authorization: Bearer $ADMIN_TOKEN"
```

The key stops working. Immediately. Nothing else touches.

## Keys and the AI agent angle

AI agents are a specific case worth calling out. When you hand an agent an `ADMIN_TOKEN`, you're giving it full org admin β€” the same access as a logged-in admin user. That's fine for bootstrapping. It's not fine for ongoing production use.

With org-scoped keys:

1. Create a key for the agent with a descriptive name (`ci-agent-prod`, `canvas-assistant`)
2. Give the agent only that key
3. The agent can do everything it needs β€” create workspaces, manage secrets, dispatch tasks
4. If the agent behaves unexpectedly, revoke the one key

The `created_by` field on every token records provenance β€” `"session"`, `"org-token:zpier-wh"`, or `"admin-token"` β€” so post-incident review can follow the chain of mints. If a key minted by another key is used maliciously, the audit trail goes back to the original minting identity.

## The security model

Plaintext tokens are never stored. The database holds a sha256 hash. A DB compromise gives the attacker hashes β€” not usable credentials. Cracking sha256 of a 43-character base64url random string at GPU-scale brute force would take longer than the age of the universe.

Revocation is immediate: `UPDATE revoked_at = now()` takes microseconds. The partial index on `WHERE revoked_at IS NULL` keeps the hot-path lookup O(log n) regardless of how many tokens have been minted and revoked over the tenant's lifetime.

The failure response is collapsed β€” `Validate()` returns `ErrInvalidToken` for bad bytes, revoked tokens, deleted tokens, and never-existed tokens. An attacker can't enumerate which case applies from the response shape.

## What's next

Org-scoped keys today are full-admin. Role scoping (admin / editor / read-only) and per-workspace bindings are the next layer. The goal: an agent gets the minimum access it needs, not full org admin by default.

Expiry and TTL are also on the roadmap. Today, keys live until revoked β€” fine for long-lived integrations, less ideal for short-lived scripts.

Until then: name your keys, store them in a secret manager, and revoke any key the moment it touches a system it shouldn't have.

![Org-scoped keys vs shared ADMIN_TOKEN](./assets/org-keys-comparison.png)

*Org-scoped API keys are live now on all Molecule AI deployments. Mint your first key in Settings β†’ Org API Keys in the canvas UI.*
165 changes: 165 additions & 0 deletions docs/devrel/demos/cloudflare-artifacts/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
# Cloudflare Artifacts β€” DevRel Demo

**Issue:** [#1479](https://github.com/Molecule-AI/molecule-core/issues/1479) |
**Screencast:** ~60s walkthrough |
**Run time:** ~2 min (manual) / ~30s (dry-run with mock env)

This demo shows the full Cloudflare Artifacts workflow for a Molecule AI workspace:
attach a git repo, mint a short-lived credential, clone, write a snapshot, commit, push,
and fork for an experiment branch.

---

## Prerequisites

| Variable | Where to get it | Scope |
|---|---|---|
| `WORKSPACE_TOKEN` | Molecule AI Canvas β†’ Workspace β†’ API Keys | Workspace-level bearer token |
| `WORKSPACE_ID` | Molecule AI Canvas β†’ Workspace β†’ Settings | Workspace UUID |
| `PLATFORM_URL` | Self-hosted: your deployment URL. Cloud: `https://platform.moleculesai.app` | Platform base URL |
| `CF_ARTIFACTS_API_TOKEN` | Cloudflare Dashboard β†’ API Tokens β†’ Create Token (Templates: Artifacts Edit) | Platform env var (server-side) |
| `CF_ARTIFACTS_NAMESPACE` | Cloudflare Dashboard β†’ Artifacts β†’ Namespace ID | Platform env var (server-side) |

> **Note:** `CF_ARTIFACTS_API_TOKEN` and `CF_ARTIFACTS_NAMESPACE` are platform-level env vars β€” they do not appear in the demo script. The demo only calls the Molecule AI platform API; Cloudflare credentials are managed server-side.

### Required tools

```bash
curl jq git
# macOS
brew install curl jq git
# Linux (Debian/Ubuntu)
sudo apt-get install curl jq git
```

---

## Setup

```bash
# 1. Clone this repo
git clone https://github.com/Molecule-AI/molecule-core.git
cd molecule-core/docs/devrel/demos/cloudflare-artifacts

# 2. Set required env vars
export PLATFORM_URL="https://platform.moleculesai.app" # or your self-hosted URL
export WORKSPACE_ID="ws_xxxxxxxxxxxx" # from Canvas β†’ Workspace β†’ Settings
export WORKSPACE_TOKEN="mk_live_xxxxxxxxxxxxxxxxxxxxxx" # from Canvas β†’ Workspace β†’ API Keys

# 3. Make demo.sh executable and run it
chmod +x demo.sh
bash demo.sh
```

---

## Expected Output

### Step 1 β€” Attach repo

```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
STEP 1: Attach a new Artifacts repo to workspace ws_xxx
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[12:34:56] Repo created:
[12:34:56] id : repo_abc123xxxxxxxx
[12:34:56] name : demo-1745200000
[12:34:56] remote_url : https://x:***@hash.artifacts.cloudflare.net/git/repo-abc123.git
```

### Step 2 β€” Mint credential

```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
STEP 2: Mint a short-lived git credential
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[12:34:57] Credential minted (username=x, token=***xxxxxx)
```

### Step 3 β€” Clone, write, commit, push

```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
STEP 3: Clone repo Β· write agent snapshot Β· commit Β· push
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[12:34:58] Push succeeded
[12:34:58] Files in working tree:
[12:34:58] AGENT_SNAPSHOT.md
```

### Step 4 β€” Fork

```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
STEP 4: Fork the repo for an experiment branch
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[12:35:00] Fork created: id=repo_abc123experiment
[12:35:00] fork remote : https://x:***@hash.artifacts.cloudflare.net/git/repo-abc123experiment...
[12:35:00] next step : git clone <fork_url> && cd <repo> && git push
```

### Step 5 β€” Verify

```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
STEP 5: Verify β€” list workspace Artifacts repos
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[12:35:01] Current workspace repos:
[12:35:01] repo_abc123 demo-1745200000 (2026-04-21), remote: https://x:***@hash.artifacts.cloudflare.net/git/repo-abc123...
[12:35:01] repo_abc123experiment demo-1745200000-experiment (2026-04-21), remote: https://x:***@hash.artifacts.cloudflare.net/git/repo-abc123experiment...
```

---

## Screencast Shot List (~60 seconds)

| Time | On-screen | Audio |
|---|---|---|
| 0–10s | Canvas β†’ Workspaces β†’ Artifacts tab (empty) | "Every Molecule AI workspace can now have its own Git repo on Cloudflare's edge." |
| 10–25s | Terminal: run Step 1 curl β†’ JSON response | "One API call creates the repo and returns a git remote URL." |
| 25–40s | Terminal: git clone β†’ write AGENT_SNAPSHOT.md β†’ commit β†’ push | "The agent writes its work as a Git commit. Every run is versioned." |
| 40–50s | Run fork curl β†’ show both repos in Canvas | "Before a risky change, the agent forks β€” the main branch stays clean." |
| 50–60s | Canvas: show commit history, point to Artifacts tab | "All of this is visible from Canvas β€” no terminal required for your team." |

---

## Troubleshooting

### `403 Forbidden` or `401 Unauthorized`

Workspace token is invalid or expired. Generate a fresh token at **Canvas β†’ Workspace β†’ API Keys**.

### `503 Cloudflare Artifacts not configured`

The platform server is missing `CF_ARTIFACTS_API_TOKEN` or `CF_ARTIFACTS_NAMESPACE`. This is a server-side configuration issue β€” contact your platform admin.

### `404 Not Found` on `/artifacts` endpoints

The platform version does not include the Artifacts integration. Ensure you're running `main` with `workspace-server/internal/handlers/artifacts.go` present.

### `Failed to create repo` with valid credentials

Check that the Cloudflare API token has **Artifacts Write** scope and the namespace ID is correct in Cloudflare Dashboard β†’ Artifacts.

### First git push fails with "refusing to push to unrelated history"

Run `git pull origin main --allow-unrelated-histories` before pushing, or `git push -f` if the remote is empty and you want to establish it as the canonical history.

---

## Files

```
cloudflare-artifacts/
β”œβ”€β”€ demo.sh # Runnable bash demo (self-contained)
└── README.md # This file
```

---

## Related Resources

- **Blog post:** [Give Your AI Agent a Git Repository](https://moleculesai.app/blog/cloudflare-artifacts-molecule-ai)
- **API reference:** [Platform API β†’ Artifacts](/docs/api-protocol/platform-api)
- **Cloudflare Artifacts docs:** [developers.cloudflare.com/artifacts](https://developers.cloudflare.com/artifacts/)
- **Source:** `workspace-server/internal/handlers/artifacts.go` on `main`
Loading
Loading