docs(tutorials): add SaaS federation v2 tutorial (#82)

Pairs molecule-core#1700.

- New content/docs/tutorials/saas-federation.md: clean standalone tutorial
  on multi-tenant org onboarding, workspace provisioning, fleet inspection,
  quota controls, and suspension/teardown. Corrects HTTP 402→409 for quota
  gates (RFC 9110: resource-state conflict, not payment failure).
- api-reference.mdx: PUT /workspaces/:id/files/*path now documents the
  EC2-Instance-Connect SSH-backed write path for SaaS (non-Docker) workspaces.
- changelog.mdx: 2026-04-23 entry updated with #1700 (new feature),
  #1702 (fix), and corrects earlier entry structure.

Co-authored-by: Molecule AI Documentation Specialist <documentation-specialist@agents.moleculesai.app>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
molecule-ai[bot] 2026-04-23 17:28:44 +00:00 committed by GitHub
parent 6d5a3c7523
commit 7004bedf40
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 526 additions and 2 deletions

View File

@ -295,7 +295,7 @@ Workspace file management. Files are stored in the workspace's config directory.
|--------|------|------|-------------|
| GET | `/workspaces/:id/files` | WorkspaceAuth | List files in the workspace config directory. |
| GET | `/workspaces/:id/files/*path` | WorkspaceAuth | Read a specific file. |
| PUT | `/workspaces/:id/files/*path` | WorkspaceAuth | Write a file. Creates parent directories as needed. |
| PUT | `/workspaces/:id/files/*path` | WorkspaceAuth | Write a file. Creates parent directories as needed. On SaaS workspaces (EC2, no Docker), routes via EC2 Instance Connect endpoint using an ephemeral SSH key pair — the key is scoped to the file-write operation and deleted within 30 seconds. Max payload ~10 MiB. Self-hosted Docker workspaces write via `docker cp` as before. |
| DELETE | `/workspaces/:id/files/*path` | WorkspaceAuth | Delete a file. |
| GET | `/workspaces/:id/shared-context` | WorkspaceAuth | Get the shared context files for a workspace (aggregated from parent hierarchy). |

View File

@ -9,11 +9,15 @@ Entries are published daily at 23:50 UTC.
---
## 2026-04-23
A quiet day — most activity was internal tooling and security hardening. The SSRF fix below resolves a regression that blocked chat for SaaS deployments.
### ✨ New features
- **SaaS Federation v2 tutorial**: a clean, self-contained walkthrough for platform operators who want to run multi-tenant workspaces from a single control plane. Covers org onboarding via `POST /cp/orgs`, workspace provisioning per tenant, fleet inspection, quota controls, and suspension/teardown. (`molecule-core` [#1700](https://github.com/Molecule-AI/molecule-core/pull/1700))
- **External workspace quickstart**: a 5-minute guide to running any HTTP-speaking agent (Python, Node, Go, Rust) on your own machine and having it appear on the canvas alongside platform-provisioned agents. Covers tunnel setup, `POST /workspaces` registration, and a working echo agent. (`molecule-core` [#1760](https://github.com/Molecule-AI/molecule-core/pull/1760))
### 🔧 Fixes
- **SSRF guard in SaaS mode**: previously the SSRF protection was blocking all RFC-1918 private IP ranges (`10/8`, `172.16/12`, `192.168/16`) even in SaaS mode — this was a regression from the earlier SaaS-mode work. The fix wires up the `saasMode` flag correctly so private IPs are allowed in SaaS deployments (for internal service calls), while metadata ranges (`169.254/16`), CGNAT, loopback, and link-local remain blocked in every mode. IPv6 ULA (`fd00::/8`) handling is also now correct. (`molecule-core` [#1692](https://github.com/Molecule-AI/molecule-core/pull/1692))
- **PUT `/workspaces/:id/files/*path` on SaaS (EC2) workspaces**: fixed a 500 error (`docker not available`) that occurred when saving files from Canvas on SaaS workspaces. The handler now detects non-Docker workspaces via `workspaces.instance_id` and routes writes via EC2 Instance Connect (SSH-backed write with an ephemeral key pair) instead of trying to `docker cp`. (`molecule-core` [#1702](https://github.com/Molecule-AI/molecule-core/pull/1702))
### 📚 Docs
@ -22,6 +26,7 @@ A quiet day — most activity was internal tooling and security hardening. The S
### 🧹 Internal
- SaaS Federation v2 tutorial published — clean rewrite of #1613, now with correct HTTP status codes, fleet metrics endpoint, and security model table (`molecule-core` [#1700](https://github.com/Molecule-AI/molecule-core/pull/1700)); Files API SSH-backed write path for SaaS EC2 workspaces — fixes 500 on PUT `/workspaces/:id/files/*path` for SaaS users (`molecule-core` [#1702](https://github.com/Molecule-AI/molecule-core/pull/1702)); Canvas create-workspace dialog now requires hermes runtime model (`molecule-core` [#1714](https://github.com/Molecule-AI/molecule-core/pull/1714)).
- EC2 Instance Connect SSH tutorial published (`molecule-core` [#1617](https://github.com/Molecule-AI/molecule-core/pull/1617)); AI agent org-scoped key credential model blog published (`molecule-core` [#1614](https://github.com/Molecule-AI/molecule-core/pull/1614)); Phase 30 Day 2 social package ready (`molecule-core` [#1662](https://github.com/Molecule-AI/molecule-core/pull/1662)).
---

View File

@ -0,0 +1,270 @@
---
title: "External Workspace — 5-Minute Quickstart"
description: "Get any HTTP-speaking agent running on your own machine (laptop, home server, cloud VM) to appear on the Molecule AI canvas alongside platform-provisioned agents."
---
# External Workspace — 5-Minute Quickstart
Run an agent on your laptop, a home server, a cloud VM, or any machine with internet — and have it show up on a Molecule AI canvas alongside platform-provisioned agents. This guide gets you from zero to a working agent in under 5 minutes.
> **Looking for the operator-focused reference?** See [External Agent Registration](/docs/guides/external-agent-registration) for full capability + auth details, or [Remote Workspaces FAQ](/docs/guides/remote-workspaces-faq) for hardening + production notes. This doc is the fast path.
---
## What is an "external workspace"?
A workspace whose agent code lives outside Molecule's infrastructure. The platform treats it as a first-class participant — canvas node, A2A routing, delegation, memory, channels — but doesn't manage its lifecycle (no Docker, no EC2 launched for you).
You're responsible for:
1. Running an HTTP server that speaks A2A JSON-RPC
2. Exposing it at a URL the platform can reach
3. Registering it with your tenant
Everything else — message routing, canvas rendering, peer discovery, memory access — works the same as a platform-native agent.
---
## Prerequisites
| You need | Notes |
|---|---|
| A Molecule AI tenant | Your own hosted instance (e.g. `you.moleculesai.app`) or self-hosted |
| Tenant admin token | Available in the admin UI, or via `molecli ws list` |
| Outbound HTTPS | No inbound ports needed if you use a tunnel (next step) |
| Any language with an HTTP server | Python / Node.js / Go / Rust — anything that can POST+GET JSON |
---
## Step 1 — Write the agent (Python example, ~40 lines)
```python
# agent.py
import time
from fastapi import FastAPI, Request
app = FastAPI()
@app.get("/health")
def health():
return {"status": "ok"}
@app.post("/")
async def a2a(request: Request):
body = await request.json()
# Extract user text from A2A JSON-RPC message/send
user_text = ""
try:
for part in body["params"]["message"]["parts"]:
if part.get("kind") == "text":
user_text = part["text"]
break
except (KeyError, TypeError):
pass
# Your logic goes here — echo for now
reply = f"You said: {user_text}"
return {
"jsonrpc": "2.0",
"id": body.get("id"),
"result": {
"kind": "message",
"messageId": f"agent-{int(time.time() * 1000)}",
"role": "agent",
"parts": [{"kind": "text", "text": reply}],
},
}
```
```bash
pip install fastapi uvicorn
uvicorn agent:app --host 127.0.0.1 --port 9876
```
Test locally:
```bash
curl -X POST http://127.0.0.1:9876/ \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"message/send","id":"1","params":{"message":{"role":"user","messageId":"m1","parts":[{"kind":"text","text":"hello"}]}}}'
```
Should return a JSON body with `"text":"You said: hello"`.
---
## Step 2 — Expose it to the internet
Pick one:
### Option A — Cloudflare quick tunnel (no account, ephemeral)
```bash
cloudflared tunnel --url http://127.0.0.1:9876
```
Copy the printed `https://*.trycloudflare.com` URL. Regenerates on every restart; fine for demos.
### Option B — ngrok (account, persistent during session)
```bash
ngrok http 9876
```
### Option C — Real server with TLS
Deploy the same Python script to a VM (Fly, Railway, DigitalOcean, anywhere) behind a TLS terminator (Caddy, nginx, or the platform's native TLS).
---
## Step 3 — Register the workspace
Replace `<TENANT>`, `<ADMIN_TOKEN>`, `<ORG_ID>`, and `<YOUR_URL>` with your values.
```bash
curl -X POST https://<TENANT>/workspaces \
-H "Authorization: Bearer <ADMIN_TOKEN>" \
-H "X-Molecule-Org-Id: <ORG_ID>" \
-H "Content-Type: application/json" \
-d '{
"name": "My Laptop Agent",
"runtime": "external",
"external": true,
"url": "<YOUR_URL>",
"tier": 2
}'
```
Response:
```json
{"external":true,"id":"abc-123-...","status":"online"}
```
The `id` field is your workspace ID — remember it.
---
## Step 4 — Chat with it
1. Open your Molecule canvas at `https://<TENANT>`
2. You'll see a new workspace node named "My Laptop Agent" with status `online`
3. Click it → Chat tab → type "hello"
4. Watch your terminal's uvicorn log — you'll see the incoming POST
5. The reply appears in the canvas chat
🎉 **You have an external agent running on Molecule.** Everything from here is iteration on that agent's handler code.
---
## Common gotchas
| Problem | Fix |
|---|---|
| "Failed to send message — agent may be unreachable" | The tenant couldn't POST to your URL. Verify `curl https://<your-tunnel>/health` returns 200 from another machine. |
| Response takes > 30s | Canvas times out around 30s. Keep initial implementations simple. For long-running work, return a placeholder and use [polling mode](#next-step-polling-mode-preview) (once available). |
| Agent duplicated in chat | Known canvas bug where WebSocket + HTTP responses both render. Fixed in [molecule-core #1517](https://github.com/Molecule-AI/molecule-core/pull/1517). |
| Agent replies but canvas shows "Agent unreachable" | Check the tenant can reach your URL. Cloudflare quick tunnels rotate — the URL in your canvas may point at a dead tunnel after restart. |
| Getting 404 when POSTing to tenant | Add `X-Molecule-Org-Id` header. The tenant's security layer 404s unmatched origin requests by design. |
---
## What you can do from the agent
Your agent has the same capability surface as a platform-native one. From inside your handler you can make outbound calls to the tenant API:
```python
import httpx
TENANT = "https://you.moleculesai.app"
TOKEN = "..." # your workspace_auth_token from registration
def call_peer(workspace_id: str, text: str) -> str:
"""Message another agent (parent, child, sibling)."""
resp = httpx.post(
f"{TENANT}/workspaces/{workspace_id}/a2a",
headers={"Authorization": f"Bearer {TOKEN}"},
json={
"jsonrpc": "2.0",
"method": "message/send",
"id": "1",
"params": {"message": {
"role": "user", "messageId": "1",
"parts": [{"kind": "text", "text": text}]
}}
},
timeout=30,
)
return resp.json()["result"]["parts"][0]["text"]
```
Similarly available: `delegate_to_workspace`, `commit_memory`, `search_memory`, `request_approval`, `peers`, `discover`. See the [A2A protocol reference](/docs/api-protocol/communication-rules) for the full endpoint list.
---
## Production upgrade path
The quickstart leaves you with an ephemeral demo. For real use:
1. **Deploy to a real host**: Fly Machine / Railway / anywhere with a stable URL + TLS.
2. **Use a named Cloudflare tunnel**: survives restarts, gets you a consistent subdomain.
3. **Authenticate outbound calls correctly**: store the `workspace_auth_token` (returned when you register via `/registry/register`; see the [full registration doc](/docs/guides/external-agent-registration)) and send it as `Authorization: Bearer ...` on every outbound call to the tenant.
4. **Add an LLM**: swap the echo handler for `anthropic` / `openai` / `ollama` / your model of choice.
5. **Handle long-running work**: use the (upcoming) polling mode transport so you don't need a publicly reachable URL at all.
---
## Next step: polling mode (preview)
Push mode (this guide) works today but requires an inbound-reachable URL — which forces tunnels or public IPs. A polling-mode transport is in design:
```
[Canvas] --A2A--> [Platform] <--polls-- [Your laptop]
[inbox queue] -->replies
```
Your agent makes only outbound HTTPS calls to the platform, pulling messages from an inbox queue and posting replies back. Works behind any NAT/firewall, tolerates offline laptops, no tunnel needed.
See the [design doc](https://github.com/Molecule-AI/internal/blob/main/product/external-workspaces-polling.md) (internal) and [implementation tracking issue](https://github.com/Molecule-AI/molecule-core/issues?q=polling+mode) once opened.
---
## Examples
- **This quickstart's code**: [gist](https://gist.github.com/molecule-ai/external-workspace-quickstart) (forked for your language of choice)
- **LLM-backed example**: `molecule-ai/examples/external-claude-agent` — a working agent that proxies to Anthropic's API
- **Scheduled cron example**: `molecule-ai/examples/external-cron-agent` — fires timed outbound messages without needing inbound
---
## Troubleshooting
Run this diagnostic checklist before filing an issue:
```bash
# 1. Is your agent serving locally?
curl http://127.0.0.1:9876/health
# 2. Is the tunnel up?
curl https://<your-tunnel-url>/health
# 3. Can the tenant reach you? (from tenant shell or your laptop)
curl -X POST https://<your-tunnel-url>/ \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"message/send","id":"x","params":{"message":{"role":"user","messageId":"m","parts":[{"kind":"text","text":"hi"}]}}}'
# 4. Is the workspace registered correctly?
curl -H "Authorization: Bearer <ADMIN_TOKEN>" -H "X-Molecule-Org-Id: <ORG_ID>" \
https://<TENANT>/workspaces/<WS_ID>
```
If all four pass and canvas still shows your agent as unreachable, see the [remote workspaces FAQ](/docs/guides/remote-workspaces-faq).
---
## Feedback
This is a new path. Tell us what broke:
- Open an issue: https://github.com/Molecule-AI/molecule-core/issues/new?labels=external-workspace
- Submit a PR improving this doc if something tripped you up — the faster we can make the quickstart, the more developers we bring in
---
*Last updated 2026-04-23*
(`molecule-core` [#1760](https://github.com/Molecule-AI/molecule-core/pull/1760))

View File

@ -0,0 +1,249 @@
---
title: "SaaS Federation — Multi-Tenant Agent Platform"
---
# SaaS Federation — Multi-Tenant Agent Platform
This tutorial walks through setting up a multi-tenant AI agent platform using Molecule AI's SaaS federation layer. You'll provision workspaces for multiple customers from a single control plane, with per-tenant database isolation, credential separation, and agent fleet visualization.
**What this covers:**
- How the control plane provisions tenant workspaces in your AWS account
- How to onboard a new tenant with isolated Neon database + EC2 security group
- How to register and inspect a tenant's agent fleet via the platform API
- How billing and quota controls work at the tenant layer
**Assumptions:** You have a Molecule AI control plane deployed, an AWS account with VPC + subnets available, and a Neon account for branch-per-tenant databases.
---
## What is SaaS federation?
Molecule AI's SaaS federation layer sits between your control plane and the tenant workspaces your customers use.
```
You (the platform operator)
├── Control Plane (api.moleculesai.app)
│ └─ Provisions: Neon DB branches, EC2 workspaces, security groups
└── Tenant: acme.rocket.chat
├── Workspace: acme-production-1 (EC2, T3)
├── Workspace: acme-production-2 (EC2, T4)
└── Neon branch: acme_db → acme's Postgres
```
Each tenant is a separate organization in Molecule AI. The control plane holds credentials and provisions infrastructure — but each tenant's workspace data lives in their own isolated branch.
---
## Step 1: Onboard a new tenant
Onboarding creates a new org in your platform, provisions a Neon database branch, and sets up an EC2 security group for the tenant's workspaces.
### Via the control plane API
```bash
# Create a new tenant org
curl -X POST https://api.moleculesai.app/cp/orgs \
-H "Authorization: Bearer $PROVISION_SHARED_SECRET" \
-H "Content-Type: application/json" \
-d '{
"name": "Acme Corp",
"slug": "acme",
"plan": "pro",
"vpc_id": "vpc-0a1b2c3d4e5f6g7h8",
"subnet_ids": ["subnet-abc123", "subnet-def456"]
}'
```
Response:
```json
{
"id": "org_7f2a9c",
"name": "Acme Corp",
"slug": "acme",
"plan": "pro",
"neon_branch_id": "br-shadowy-7f2a9c",
"security_group_id": "sg-0a1b2c3d",
"status": "provisioning"
}
```
### What gets provisioned
| Resource | How | Who manages |
|---|---|---|
| Neon branch `br-shadowy-7f2a9c` | Auto-created by control plane via Neon API | Tenant gets connection string |
| EC2 security group `sg-0a1b2c3d` | Created with inbound :443 from platform only | Control plane manages rules |
| Org record in platform DB | Created on first API call | Control plane |
The provisioning step runs asynchronously — poll `/cp/orgs/:slug` until `status: active`.
```bash
# Poll until active
until curl -s https://api.moleculesai.app/cp/orgs/acme \
-H "Authorization: Bearer $PROVISION_SHARED_SECRET" \
| jq -r '.status' | grep -q active; do
echo "Still provisioning..."; sleep 10
done
echo "Tenant ready"
```
---
## Step 2: Provision workspaces for the tenant
Once the tenant org is active, workspaces can be created via the tenant's own API — no operator involvement needed.
Each workspace is provisioned as an EC2 instance in the tenant's VPC subnet, behind the tenant's security group. The security group allows inbound :443 from the platform API only.
```bash
# As the tenant (they use their own org-scoped API key)
curl -X POST https://acme.moleculesai.app/workspaces \
-H "Authorization: Bearer $TENANT_ORG_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "production-agent-1",
"role": "Production inference worker",
"runtime": "hermes",
"tier": 3,
"model": "claude-sonnet-4"
}'
```
The control plane handles the EC2 provisioning in the background:
1. Calls `aws ec2 run-instances` in the tenant's VPC subnet
2. Waits for the instance to boot and register via A2A
3. Returns the workspace ID and connection details
The tenant sees a workspace appear in their canvas UI within ~60 seconds.
---
## Step 3: Inspect the tenant's agent fleet
From the operator side, you can inspect any tenant's workspaces via the control plane:
```bash
# List all workspaces for a tenant
curl https://api.moleculesai.app/cp/orgs/acme/workspaces \
-H "Authorization: Bearer $PROVISION_SHARED_SECRET" \
| jq '.'
```
Response:
```json
{
"org": "acme",
"workspaces": [
{
"id": "ws_9b3k1m",
"name": "production-agent-1",
"runtime": "hermes",
"tier": 3,
"instance_id": "i-0a1b2c3d4e5f6g7h8",
"status": "running",
"last_seen": "2026-04-22T09:30:00Z"
},
{
"id": "ws_2n8p4q",
"name": "staging-worker",
"runtime": "hermes",
"tier": 2,
"instance_id": "i-1a2b3c4d5e6f7g8h9",
"status": "stopped",
"last_seen": "2026-04-21T16:00:00Z"
}
]
}
```
### Fleet-level metrics
```bash
# Aggregate runtime stats for a tenant
curl https://api.moleculesai.app/cp/orgs/acme/metrics \
-H "Authorization: Bearer $PROVISION_SHARED_SECRET" \
| jq '{total_workspaces, active_agents, avg_response_time_ms, total_tasks_dispatched}'
```
---
## Step 4: Set quota and billing controls
Quotas are enforced at the org level. Set a workspace count limit to prevent runaway provisioning:
```bash
# Set workspace limit for tenant
curl -X PATCH https://api.moleculesai.app/cp/orgs/acme \
-H "Authorization: Bearer $PROVISION_SHARED_SECRET" \
-H "Content-Type: application/json" \
-d '{
"max_workspaces": 10,
"max_tier": 3,
"billing_plan": "pro"
}'
```
When a tenant hits their workspace limit, `POST /workspaces` returns **`409 Conflict`** (not `402 Payment Required` — quota gates are resource-state conflicts, not payment failures).
---
## Step 5: Revoke access for a tenant
If a tenant stops paying or needs to be suspended:
```bash
# Suspend tenant (revokes their org API key and freezes workspace creation)
curl -X POST https://api.moleculesai.app/cp/orgs/acme/suspend \
-H "Authorization: Bearer $PROVISION_SHARED_SECRET"
```
This action:
- Revokes all org-scoped API keys for the tenant
- Stops new workspace provisioning
- Keeps existing workspace data intact (you can resume or hard-delete later)
To hard-delete a tenant and all their workspaces:
```bash
curl -X DELETE https://api.moleculesai.app/cp/orgs/acme \
-H "Authorization: Bearer $PROVISION_SHARED_SECRET" \
-H "Content-Type: application/json" \
-d '{"confirm": true, "delete_workspaces": true}'
```
This terminates all EC2 instances, drops the Neon branch, and removes the org record. **This is irreversible.**
---
## Security model summary
| Layer | Isolation mechanism | Who manages |
|---|---|---|
| Database | Neon branch-per-tenant | Tenant's branch, operator has no direct access |
| Compute | EC2 in tenant's VPC | Control plane provisions, operator manages SG rules |
| Credentials | No Fly/API tokens on tenant | All cloud credentials held by control plane |
| API access | Org-scoped API keys | Tenant manages their own keys; operator has CP-level override |
| Network | Security group: port 443 from platform only | Control plane manages; tenant can't modify |
---
## What's next
- **Tenant registration UI**: expose a signup flow so customers can self-serve (roadmap: Phase 34)
- **Scoped roles**: give different team members read-only vs admin access within a tenant org (roadmap: Phase 34)
- **Usage-based billing**: Meter workspace runtime and forward events to Stripe for custom billing tiers
For runbook-level details on the provisioning flow, see the architecture docs at [`docs/architecture/saas-prod-migration-2026-04-19`](/docs/architecture/saas-prod-migration-2026-04-19).
For the API reference, see [`docs/api-reference`](/docs/api-reference) — the `/cp/orgs/*` endpoints are documented there.
---
*SaaS federation is available for all Molecule AI platform operators. Contact the Molecule AI team to enable federation on your control plane.*
(`molecule-core` [#1700](https://github.com/Molecule-AI/molecule-core/pull/1700))