diff --git a/content/docs/api-reference.mdx b/content/docs/api-reference.mdx index 31f66ef..5148985 100644 --- a/content/docs/api-reference.mdx +++ b/content/docs/api-reference.mdx @@ -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). | diff --git a/content/docs/changelog.mdx b/content/docs/changelog.mdx index b554895..89de819 100644 --- a/content/docs/changelog.mdx +++ b/content/docs/changelog.mdx @@ -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)). --- diff --git a/content/docs/guides/external-workspace-quickstart.md b/content/docs/guides/external-workspace-quickstart.md new file mode 100644 index 0000000..e3c500d --- /dev/null +++ b/content/docs/guides/external-workspace-quickstart.md @@ -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 ``, ``, ``, and `` with your values. + +```bash +curl -X POST https:///workspaces \ + -H "Authorization: Bearer " \ + -H "X-Molecule-Org-Id: " \ + -H "Content-Type: application/json" \ + -d '{ + "name": "My Laptop Agent", + "runtime": "external", + "external": true, + "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://` +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:///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:///health + +# 3. Can the tenant reach you? (from tenant shell or your laptop) +curl -X POST https:/// \ + -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 " -H "X-Molecule-Org-Id: " \ + https:///workspaces/ +``` + +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)) \ No newline at end of file diff --git a/content/docs/tutorials/saas-federation.md b/content/docs/tutorials/saas-federation.md new file mode 100644 index 0000000..26ab6e7 --- /dev/null +++ b/content/docs/tutorials/saas-federation.md @@ -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)) \ No newline at end of file