Existing external-agent-registration.md is 784 lines — great reference but hostile to first-time devs evaluating Molecule. Add a tight 5-minute quickstart aimed at "make it work today": - 40-line Python agent with A2A JSON-RPC skeleton - Cloudflare quick-tunnel for instant public URL (no account) - Single curl registration - Common gotchas table (includes the canvas dedup + tunnel rotation issues caught in the demo this afternoon) - Production upgrade path - Preview of polling mode (Phase N+1 transport) - 4-step diagnostic checklist at the bottom The reference doc (external-agent-registration.md) now has a prominent "in a hurry?" callout pointing at the quickstart, so the discovery path works either way. Target audience: a developer who wants to see their code on canvas inside 5 minutes, not a self-hoster hardening for prod. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
787 lines
23 KiB
Markdown
787 lines
23 KiB
Markdown
# External Agent Registration Guide
|
|
|
|
> **In a hurry?** The [External Workspace 5-Minute Quickstart](./external-workspace-quickstart.md) gets you from zero to a live agent on canvas in under 5 minutes. This guide is the comprehensive reference — auth, capabilities, production hardening — for when you need the full picture.
|
|
|
|
## Overview
|
|
|
|
An **external agent** (also called a remote agent) is any AI agent that runs
|
|
outside the Molecule AI platform's Docker network but participates in the
|
|
canvas, communicates with other agents via the A2A protocol, and is managed as
|
|
a first-class workspace.
|
|
|
|
Use cases for external agents:
|
|
|
|
- **Existing infrastructure** -- you already run an agent on your own servers
|
|
and want it to join a Molecule org without re-deploying inside Docker.
|
|
- **Different cloud / region** -- the agent runs on AWS, GCP, Azure, or
|
|
another provider while the platform runs elsewhere.
|
|
- **Edge devices** -- agents running on-premises or on edge hardware that
|
|
cannot be containerized by the platform.
|
|
- **Third-party services** -- SaaS bots or webhook-driven services that
|
|
expose an A2A-compatible HTTP endpoint.
|
|
- **Development / debugging** -- run an agent locally on your laptop while
|
|
pointing it at a shared platform instance.
|
|
|
|
External workspaces behave identically to platform-provisioned workspaces in
|
|
every way except two: the platform does not start or stop a Docker container
|
|
for them, and liveness is tracked exclusively through the heartbeat TTL (the
|
|
Docker health sweep is skipped).
|
|
|
|
---
|
|
|
|
## Prerequisites
|
|
|
|
| Requirement | Details |
|
|
|-------------|---------|
|
|
| Running Molecule AI platform | Default `http://localhost:8080`. Set `NEXT_PUBLIC_PLATFORM_URL` in canvas accordingly. |
|
|
| Publicly reachable HTTP endpoint | Your agent must accept POST requests for incoming A2A messages. If you are behind NAT, use a tunnel (ngrok, Cloudflare Tunnel, etc.). |
|
|
| Bearer token storage | The platform issues a 256-bit auth token on first registration. You must persist it -- it is shown only once and cannot be recovered. |
|
|
|
|
---
|
|
|
|
## Step-by-Step Registration
|
|
|
|
### 1. Create an External Workspace
|
|
|
|
```bash
|
|
curl -X POST http://localhost:8080/workspaces \
|
|
-H "Content-Type: application/json" \
|
|
-H "Authorization: Bearer <admin-token>" \
|
|
-d '{
|
|
"name": "My External Agent",
|
|
"role": "researcher",
|
|
"runtime": "external",
|
|
"external": true,
|
|
"url": "https://my-agent.example.com",
|
|
"tier": 2,
|
|
"parent_id": null
|
|
}'
|
|
```
|
|
|
|
**Response:**
|
|
|
|
```json
|
|
{
|
|
"id": "a1b2c3d4-...",
|
|
"status": "online",
|
|
"external": true
|
|
}
|
|
```
|
|
|
|
Notes:
|
|
|
|
- `POST /workspaces` requires `AdminAuth` (bearer token) when any live admin
|
|
token exists on the platform. On a fresh install with no tokens, it
|
|
bootstraps open.
|
|
- `external: true` tells the platform to skip Docker provisioning. The
|
|
workspace goes straight to `online` if a URL is provided.
|
|
- `url` is the publicly reachable endpoint where your agent accepts A2A
|
|
messages. It must be an HTTPS or HTTP URL.
|
|
- `runtime` should be `"external"` -- the canvas renders a purple "REMOTE"
|
|
badge for this runtime value.
|
|
- `tier` defaults to `1` if omitted. Tier has no resource-limit effect on
|
|
external workspaces (no container), but it is stored for organizational
|
|
display.
|
|
- `parent_id` is optional. Set it to nest this workspace under an existing
|
|
team/parent workspace.
|
|
|
|
Save the returned `id` -- you will need it for every subsequent call.
|
|
|
|
### 2. Register with the Platform
|
|
|
|
```bash
|
|
curl -X POST http://localhost:8080/registry/register \
|
|
-H "Content-Type: application/json" \
|
|
-d '{
|
|
"id": "<workspace-id-from-step-1>",
|
|
"url": "https://my-agent.example.com",
|
|
"agent_card": {
|
|
"name": "My External Agent",
|
|
"description": "Handles research tasks and summarization",
|
|
"skills": ["research", "summarization", "analysis"],
|
|
"runtime": "external"
|
|
}
|
|
}'
|
|
```
|
|
|
|
**Response (first registration):**
|
|
|
|
```json
|
|
{
|
|
"status": "registered",
|
|
"auth_token": "mol_abc123...very-long-token"
|
|
}
|
|
```
|
|
|
|
**Critical:** The `auth_token` field is present **only on first
|
|
registration**. It is never returned again. Store it securely (environment
|
|
variable, secrets manager, etc.). All subsequent authenticated calls require
|
|
this token.
|
|
|
|
On re-registration (e.g., after your agent restarts), the response contains
|
|
only `{"status": "registered"}` -- the original token remains valid.
|
|
|
|
### 3. Start the Heartbeat Loop
|
|
|
|
Your agent must send a heartbeat every **30 seconds** to stay online. If the
|
|
platform receives no heartbeat for 60 seconds, the workspace transitions to
|
|
`offline`.
|
|
|
|
```bash
|
|
curl -X POST http://localhost:8080/registry/heartbeat \
|
|
-H "Content-Type: application/json" \
|
|
-H "Authorization: Bearer <auth_token>" \
|
|
-d '{
|
|
"workspace_id": "<workspace-id>",
|
|
"error_rate": 0.0,
|
|
"active_tasks": 0,
|
|
"current_task": "",
|
|
"uptime_seconds": 3600,
|
|
"sample_error": ""
|
|
}'
|
|
```
|
|
|
|
**Response:**
|
|
|
|
```json
|
|
{ "status": "ok" }
|
|
```
|
|
|
|
Heartbeat fields:
|
|
|
|
| Field | Type | Description |
|
|
|-------|------|-------------|
|
|
| `workspace_id` | string | Required. Your workspace ID. |
|
|
| `error_rate` | float | 0.0 -- 1.0. If > 0.5, workspace enters `degraded` status on the canvas. |
|
|
| `active_tasks` | int | Number of tasks currently running. Displayed in the canvas node. |
|
|
| `current_task` | string | Human-readable description of current work. Shown in the workspace detail panel. |
|
|
| `uptime_seconds` | int | Seconds since your agent started. |
|
|
| `sample_error` | string | Most recent error message, if any. Visible in monitoring. |
|
|
|
|
### 4. Handle Incoming A2A Messages
|
|
|
|
Your agent must accept `POST` requests at the URL you registered. The
|
|
platform (and other agents) send messages in A2A JSON-RPC format:
|
|
|
|
```json
|
|
{
|
|
"jsonrpc": "2.0",
|
|
"method": "message/send",
|
|
"params": {
|
|
"message": {
|
|
"role": "user",
|
|
"parts": [
|
|
{ "type": "text", "text": "Research the latest trends in AI safety" }
|
|
]
|
|
},
|
|
"metadata": {
|
|
"source": "agent",
|
|
"history": []
|
|
}
|
|
},
|
|
"id": "req-abc-123"
|
|
}
|
|
```
|
|
|
|
Your endpoint must return a JSON-RPC response:
|
|
|
|
```json
|
|
{
|
|
"jsonrpc": "2.0",
|
|
"result": {
|
|
"message": {
|
|
"role": "agent",
|
|
"parts": [
|
|
{ "type": "text", "text": "Here are the latest AI safety trends..." }
|
|
]
|
|
}
|
|
},
|
|
"id": "req-abc-123"
|
|
}
|
|
```
|
|
|
|
For errors, return a JSON-RPC error object:
|
|
|
|
```json
|
|
{
|
|
"jsonrpc": "2.0",
|
|
"error": {
|
|
"code": -32603,
|
|
"message": "Internal error: model rate limited"
|
|
},
|
|
"id": "req-abc-123"
|
|
}
|
|
```
|
|
|
|
### 5. Send Messages to Other Agents
|
|
|
|
Use the A2A proxy to communicate with any workspace your agent is allowed to
|
|
reach:
|
|
|
|
```bash
|
|
curl -X POST http://localhost:8080/workspaces/<target-workspace-id>/a2a \
|
|
-H "Content-Type: application/json" \
|
|
-H "Authorization: Bearer <auth_token>" \
|
|
-H "X-Workspace-ID: <your-workspace-id>" \
|
|
-d '{
|
|
"jsonrpc": "2.0",
|
|
"method": "message/send",
|
|
"params": {
|
|
"message": {
|
|
"role": "user",
|
|
"parts": [
|
|
{ "type": "text", "text": "Can you help with this task?" }
|
|
]
|
|
},
|
|
"metadata": {}
|
|
},
|
|
"id": "req-456"
|
|
}'
|
|
```
|
|
|
|
Both headers are required:
|
|
|
|
- `Authorization: Bearer <token>` -- your workspace auth token.
|
|
- `X-Workspace-ID: <your-id>` -- identifies which workspace is making the
|
|
call. The platform uses this to enforce communication rules.
|
|
|
|
### 6. Discover Peers
|
|
|
|
Find out which other workspaces your agent can communicate with:
|
|
|
|
```bash
|
|
# Get your workspace's own info
|
|
curl http://localhost:8080/registry/discover/<your-workspace-id> \
|
|
-H "Authorization: Bearer <auth_token>" \
|
|
-H "X-Workspace-ID: <your-workspace-id>"
|
|
|
|
# List peers (siblings + parent + children)
|
|
curl http://localhost:8080/registry/<your-workspace-id>/peers \
|
|
-H "Authorization: Bearer <auth_token>" \
|
|
-H "X-Workspace-ID: <your-workspace-id>"
|
|
```
|
|
|
|
The peers endpoint returns workspaces that your agent is allowed to
|
|
communicate with, based on the hierarchy rules below.
|
|
|
|
---
|
|
|
|
## Communication Rules
|
|
|
|
The platform enforces strict hierarchy-based access control via
|
|
`CanCommunicate(callerID, targetID)`:
|
|
|
|
| Relationship | Allowed |
|
|
|---|---|
|
|
| Same workspace (self-call) | Yes |
|
|
| Siblings (same `parent_id`) | Yes |
|
|
| Root-level siblings (both `parent_id` is NULL) | Yes |
|
|
| Parent to child | Yes |
|
|
| Child to parent | Yes |
|
|
| Everything else | **Denied** |
|
|
|
|
Canvas requests (no `X-Workspace-ID` header) and system callers
|
|
(`webhook:*`, `system:*`, `test:*` prefixes) bypass this check.
|
|
|
|
---
|
|
|
|
## Canvas Appearance
|
|
|
|
External workspaces appear on the canvas with a purple **REMOTE** badge
|
|
instead of the usual runtime pill (e.g., "LANGGRAPH", "CLAUDE-CODE").
|
|
|
|
They support all standard canvas features:
|
|
|
|
- Drag-and-drop positioning (persisted via `PATCH /workspaces/:id`)
|
|
- Nesting into team nodes (set `parent_id` on create or move via API)
|
|
- Real-time status updates -- heartbeat data (active tasks, current task,
|
|
error rate) is broadcast to canvas clients via WebSocket
|
|
- Chat panel -- "My Chat" tab sends A2A messages to the agent; "Agent Comms"
|
|
tab shows inter-agent traffic
|
|
- Config and secrets management via the detail panel
|
|
|
|
---
|
|
|
|
## Example: Python Implementation
|
|
|
|
A minimal external agent using `requests` and `flask`:
|
|
|
|
```python
|
|
"""
|
|
Minimal Molecule AI external agent.
|
|
|
|
pip install flask requests
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import time
|
|
import threading
|
|
import requests
|
|
from flask import Flask, request, jsonify
|
|
|
|
PLATFORM_URL = os.environ.get("PLATFORM_URL", "http://localhost:8080")
|
|
AGENT_URL = os.environ.get("AGENT_URL") # e.g. "https://my-agent.ngrok.io"
|
|
ADMIN_TOKEN = os.environ.get("ADMIN_TOKEN", "") # for POST /workspaces
|
|
AGENT_NAME = os.environ.get("AGENT_NAME", "Python External Agent")
|
|
|
|
app = Flask(__name__)
|
|
|
|
# --- State ---
|
|
workspace_id = None
|
|
auth_token = None
|
|
start_time = time.time()
|
|
|
|
|
|
# --- Step 4: Handle incoming A2A messages ---
|
|
@app.route("/", methods=["POST"])
|
|
def handle_a2a():
|
|
payload = request.get_json(force=True)
|
|
method = payload.get("method", "")
|
|
req_id = payload.get("id", "unknown")
|
|
|
|
if method == "message/send":
|
|
text = ""
|
|
parts = (
|
|
payload.get("params", {}).get("message", {}).get("parts", [])
|
|
)
|
|
for part in parts:
|
|
if part.get("type") == "text":
|
|
text += part.get("text", "")
|
|
|
|
# --- Your agent logic here ---
|
|
reply = f"Received: {text}. Processing complete."
|
|
|
|
return jsonify({
|
|
"jsonrpc": "2.0",
|
|
"result": {
|
|
"message": {
|
|
"role": "agent",
|
|
"parts": [{"type": "text", "text": reply}],
|
|
}
|
|
},
|
|
"id": req_id,
|
|
})
|
|
|
|
return jsonify({
|
|
"jsonrpc": "2.0",
|
|
"error": {"code": -32601, "message": f"Unknown method: {method}"},
|
|
"id": req_id,
|
|
})
|
|
|
|
|
|
# --- Step 3: Heartbeat loop ---
|
|
def heartbeat_loop():
|
|
while True:
|
|
try:
|
|
requests.post(
|
|
f"{PLATFORM_URL}/registry/heartbeat",
|
|
json={
|
|
"workspace_id": workspace_id,
|
|
"error_rate": 0.0,
|
|
"active_tasks": 0,
|
|
"current_task": "",
|
|
"uptime_seconds": int(time.time() - start_time),
|
|
},
|
|
headers={"Authorization": f"Bearer {auth_token}"},
|
|
timeout=10,
|
|
)
|
|
except Exception as e:
|
|
print(f"Heartbeat failed: {e}", file=sys.stderr)
|
|
time.sleep(30)
|
|
|
|
|
|
def register():
|
|
global workspace_id, auth_token
|
|
|
|
if not AGENT_URL:
|
|
print("ERROR: Set AGENT_URL to your publicly reachable endpoint", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
# Step 1: Create external workspace
|
|
headers = {"Content-Type": "application/json"}
|
|
if ADMIN_TOKEN:
|
|
headers["Authorization"] = f"Bearer {ADMIN_TOKEN}"
|
|
|
|
resp = requests.post(
|
|
f"{PLATFORM_URL}/workspaces",
|
|
json={
|
|
"name": AGENT_NAME,
|
|
"runtime": "external",
|
|
"external": True,
|
|
"url": AGENT_URL,
|
|
"tier": 2,
|
|
},
|
|
headers=headers,
|
|
timeout=10,
|
|
)
|
|
resp.raise_for_status()
|
|
workspace_id = resp.json()["id"]
|
|
print(f"Created workspace: {workspace_id}")
|
|
|
|
# Step 2: Register with the platform
|
|
resp = requests.post(
|
|
f"{PLATFORM_URL}/registry/register",
|
|
json={
|
|
"id": workspace_id,
|
|
"url": AGENT_URL,
|
|
"agent_card": {
|
|
"name": AGENT_NAME,
|
|
"description": "A minimal Python external agent",
|
|
"skills": ["echo"],
|
|
"runtime": "external",
|
|
},
|
|
},
|
|
timeout=10,
|
|
)
|
|
resp.raise_for_status()
|
|
data = resp.json()
|
|
auth_token = data.get("auth_token")
|
|
if auth_token:
|
|
print(f"Auth token received (save this!): {auth_token[:12]}...")
|
|
else:
|
|
print("No new token issued (re-registration). Use your saved token.")
|
|
auth_token = os.environ.get("AUTH_TOKEN")
|
|
if not auth_token:
|
|
print("ERROR: Set AUTH_TOKEN for re-registration", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
# Start heartbeat
|
|
t = threading.Thread(target=heartbeat_loop, daemon=True)
|
|
t.start()
|
|
print("Heartbeat loop started (every 30s)")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
register()
|
|
print(f"Listening on {AGENT_URL}")
|
|
app.run(host="0.0.0.0", port=int(os.environ.get("PORT", 5000)))
|
|
```
|
|
|
|
Run it:
|
|
|
|
```bash
|
|
export PLATFORM_URL=http://localhost:8080
|
|
export AGENT_URL=https://my-agent.ngrok.io
|
|
export ADMIN_TOKEN=your-admin-bearer-token
|
|
python external_agent.py
|
|
```
|
|
|
|
---
|
|
|
|
## Example: Node.js Implementation
|
|
|
|
A minimal external agent using the built-in `fetch` and `express`:
|
|
|
|
```javascript
|
|
/**
|
|
* Minimal Molecule AI external agent.
|
|
*
|
|
* npm install express
|
|
* Node.js 18+ required (native fetch).
|
|
*/
|
|
|
|
const express = require("express");
|
|
|
|
const PLATFORM_URL = process.env.PLATFORM_URL || "http://localhost:8080";
|
|
const AGENT_URL = process.env.AGENT_URL; // e.g. "https://my-agent.ngrok.io"
|
|
const ADMIN_TOKEN = process.env.ADMIN_TOKEN || "";
|
|
const AGENT_NAME = process.env.AGENT_NAME || "Node External Agent";
|
|
const PORT = parseInt(process.env.PORT || "5000", 10);
|
|
|
|
let workspaceId = null;
|
|
let authToken = null;
|
|
const startTime = Date.now();
|
|
|
|
// --- Step 4: Handle incoming A2A messages ---
|
|
const app = express();
|
|
app.use(express.json());
|
|
|
|
app.post("/", (req, res) => {
|
|
const { method, id: reqId, params } = req.body;
|
|
|
|
if (method === "message/send") {
|
|
const parts = params?.message?.parts || [];
|
|
const text = parts
|
|
.filter((p) => p.type === "text")
|
|
.map((p) => p.text)
|
|
.join("");
|
|
|
|
// --- Your agent logic here ---
|
|
const reply = `Received: ${text}. Processing complete.`;
|
|
|
|
return res.json({
|
|
jsonrpc: "2.0",
|
|
result: {
|
|
message: {
|
|
role: "agent",
|
|
parts: [{ type: "text", text: reply }],
|
|
},
|
|
},
|
|
id: reqId,
|
|
});
|
|
}
|
|
|
|
res.json({
|
|
jsonrpc: "2.0",
|
|
error: { code: -32601, message: `Unknown method: ${method}` },
|
|
id: reqId,
|
|
});
|
|
});
|
|
|
|
// --- Step 3: Heartbeat loop ---
|
|
function startHeartbeat() {
|
|
setInterval(async () => {
|
|
try {
|
|
await fetch(`${PLATFORM_URL}/registry/heartbeat`, {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
Authorization: `Bearer ${authToken}`,
|
|
},
|
|
body: JSON.stringify({
|
|
workspace_id: workspaceId,
|
|
error_rate: 0.0,
|
|
active_tasks: 0,
|
|
current_task: "",
|
|
uptime_seconds: Math.floor((Date.now() - startTime) / 1000),
|
|
}),
|
|
});
|
|
} catch (err) {
|
|
console.error("Heartbeat failed:", err.message);
|
|
}
|
|
}, 30_000);
|
|
}
|
|
|
|
// --- Steps 1 & 2: Create + register ---
|
|
async function register() {
|
|
if (!AGENT_URL) {
|
|
console.error("ERROR: Set AGENT_URL to your publicly reachable endpoint");
|
|
process.exit(1);
|
|
}
|
|
|
|
// Step 1: Create external workspace
|
|
const headers = { "Content-Type": "application/json" };
|
|
if (ADMIN_TOKEN) headers["Authorization"] = `Bearer ${ADMIN_TOKEN}`;
|
|
|
|
const createResp = await fetch(`${PLATFORM_URL}/workspaces`, {
|
|
method: "POST",
|
|
headers,
|
|
body: JSON.stringify({
|
|
name: AGENT_NAME,
|
|
runtime: "external",
|
|
external: true,
|
|
url: AGENT_URL,
|
|
tier: 2,
|
|
}),
|
|
});
|
|
if (!createResp.ok) throw new Error(`Create failed: ${createResp.status}`);
|
|
const createData = await createResp.json();
|
|
workspaceId = createData.id;
|
|
console.log(`Created workspace: ${workspaceId}`);
|
|
|
|
// Step 2: Register
|
|
const regResp = await fetch(`${PLATFORM_URL}/registry/register`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
id: workspaceId,
|
|
url: AGENT_URL,
|
|
agent_card: {
|
|
name: AGENT_NAME,
|
|
description: "A minimal Node.js external agent",
|
|
skills: ["echo"],
|
|
runtime: "external",
|
|
},
|
|
}),
|
|
});
|
|
if (!regResp.ok) throw new Error(`Register failed: ${regResp.status}`);
|
|
const regData = await regResp.json();
|
|
|
|
authToken = regData.auth_token;
|
|
if (authToken) {
|
|
console.log(`Auth token received (save this!): ${authToken.slice(0, 12)}...`);
|
|
} else {
|
|
console.log("No new token issued (re-registration). Using saved token.");
|
|
authToken = process.env.AUTH_TOKEN;
|
|
if (!authToken) {
|
|
console.error("ERROR: Set AUTH_TOKEN for re-registration");
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
startHeartbeat();
|
|
console.log("Heartbeat loop started (every 30s)");
|
|
}
|
|
|
|
register().then(() => {
|
|
app.listen(PORT, () => console.log(`Listening on port ${PORT}`));
|
|
});
|
|
```
|
|
|
|
Run it:
|
|
|
|
```bash
|
|
export PLATFORM_URL=http://localhost:8080
|
|
export AGENT_URL=https://my-agent.ngrok.io
|
|
export ADMIN_TOKEN=your-admin-bearer-token
|
|
node external_agent.js
|
|
```
|
|
|
|
---
|
|
|
|
## Lifecycle
|
|
|
|
External workspaces follow a simplified version of the standard lifecycle:
|
|
|
|
```
|
|
provisioning --> online (on create with URL, or on register)
|
|
|
|
|
v
|
|
degraded (error_rate > 0.5 in heartbeat)
|
|
|
|
|
v
|
|
online (error_rate recovers)
|
|
|
|
|
v
|
|
offline (heartbeat TTL expires after 60s)
|
|
|
|
|
v
|
|
removed (DELETE /workspaces/:id)
|
|
```
|
|
|
|
Key differences from platform-managed workspaces:
|
|
|
|
- **No Docker health sweep** -- external workspaces are invisible to the
|
|
Docker API. Only the Redis heartbeat TTL determines liveness.
|
|
- **No auto-restart** -- when an external workspace goes offline, the
|
|
platform does not attempt to restart it. Your agent is responsible for
|
|
re-registering and resuming heartbeats.
|
|
- **Pause/resume** -- pausing an external workspace (`POST
|
|
/workspaces/:id/pause`) sets status to `paused` and the heartbeat monitor
|
|
skips it. Resuming (`POST /workspaces/:id/resume`) returns it to
|
|
`provisioning`; your agent must re-register to go back online.
|
|
|
|
---
|
|
|
|
## Security
|
|
|
|
### Auth Token
|
|
|
|
- Tokens are 256-bit cryptographically random values.
|
|
- The raw token is returned **once** at first registration. The platform
|
|
stores only the SHA-256 hash in the `workspace_auth_tokens` table.
|
|
- Lost tokens cannot be recovered. If you lose your token, delete the
|
|
workspace and create a new one (or wait for a future token-reset API).
|
|
- Tokens are automatically revoked when a workspace is deleted.
|
|
|
|
### Required Headers
|
|
|
|
| Endpoint | Required Headers |
|
|
|----------|-----------------|
|
|
| `POST /registry/heartbeat` | `Authorization: Bearer <token>` |
|
|
| `POST /registry/update-card` | `Authorization: Bearer <token>` |
|
|
| `POST /workspaces/:target/a2a` | `Authorization: Bearer <token>`, `X-Workspace-ID: <your-id>` |
|
|
| `GET /registry/discover/:id` | `Authorization: Bearer <token>`, `X-Workspace-ID: <your-id>` |
|
|
| `GET /registry/:id/peers` | `Authorization: Bearer <token>`, `X-Workspace-ID: <your-id>` |
|
|
|
|
### Legacy / Bootstrap Behavior
|
|
|
|
Workspaces that registered before the token system existed (Phase 30.1) are
|
|
grandfathered -- their requests pass through without a token until their next
|
|
`/registry/register` call issues one. After that, the token is enforced on
|
|
every subsequent call.
|
|
|
|
---
|
|
|
|
## Updating Your Agent Card
|
|
|
|
If your agent's capabilities change, update the card without re-registering:
|
|
|
|
```bash
|
|
curl -X POST http://localhost:8080/registry/update-card \
|
|
-H "Content-Type: application/json" \
|
|
-H "Authorization: Bearer <auth_token>" \
|
|
-d '{
|
|
"workspace_id": "<workspace-id>",
|
|
"agent_card": {
|
|
"name": "My External Agent v2",
|
|
"description": "Now with summarization and translation",
|
|
"skills": ["research", "summarization", "translation"],
|
|
"runtime": "external"
|
|
}
|
|
}'
|
|
```
|
|
|
|
---
|
|
|
|
## Troubleshooting
|
|
|
|
### Workspace shows "offline" on the canvas
|
|
|
|
**Cause:** Heartbeat has not been received in the last 60 seconds.
|
|
|
|
**Fix:** Verify your heartbeat loop is running, sending to the correct
|
|
`workspace_id`, and including `Authorization: Bearer <token>`. Check network
|
|
connectivity between your agent and the platform. Inspect platform logs for
|
|
401 errors.
|
|
|
|
### 401 Unauthorized on heartbeat
|
|
|
|
**Cause:** Missing or invalid auth token.
|
|
|
|
**Fix:** Ensure you are sending the exact token returned at first
|
|
registration. Tokens are case-sensitive. If you lost the token, you must
|
|
delete the workspace and re-create it.
|
|
|
|
### Cannot send A2A messages (403 or communication denied)
|
|
|
|
**Cause:** The `CanCommunicate` check failed -- your workspace is not a
|
|
sibling, parent, or child of the target.
|
|
|
|
**Fix:** Check the hierarchy. Use `GET /registry/<your-id>/peers` to see
|
|
which workspaces you can reach. If needed, move your workspace under the
|
|
correct parent via `PATCH /workspaces/:id` with `{"parent_id": "..."}`.
|
|
|
|
### Agent card not showing on canvas
|
|
|
|
**Cause:** Registration was not called, or the `agent_card` JSON was
|
|
malformed.
|
|
|
|
**Fix:** Call `POST /registry/register` again with a valid `agent_card`
|
|
object. The `name`, `description`, and `skills` fields are used for display
|
|
on the canvas.
|
|
|
|
### Messages not reaching your agent
|
|
|
|
**Cause:** The URL you registered is not reachable from the platform.
|
|
|
|
**Fix:**
|
|
1. Confirm the URL is correct: `curl -X POST <your-url> -d '{}'`
|
|
2. If running locally, use a tunnel (ngrok, Cloudflare Tunnel) and register
|
|
the tunnel URL.
|
|
3. Check that your agent's HTTP server is binding to `0.0.0.0`, not just
|
|
`127.0.0.1`.
|
|
4. Verify firewall rules allow inbound traffic on your agent's port.
|
|
|
|
### Workspace stuck in "provisioning"
|
|
|
|
**Cause:** The `external: true` flag was not set on create, so the platform
|
|
tried to provision a Docker container.
|
|
|
|
**Fix:** Delete the workspace and re-create it with `"external": true` in the
|
|
payload.
|
|
|
|
### Re-registration does not return a token
|
|
|
|
**Expected behavior.** The token is issued only on first registration. On
|
|
re-registration, use the token you saved from the first time. If you need to
|
|
start fresh, delete the workspace and create a new one.
|
|
|
|
### Heartbeat succeeds but status stays "degraded"
|
|
|
|
**Cause:** Your heartbeat is reporting `error_rate` > 0.5.
|
|
|
|
**Fix:** Lower the `error_rate` field in your heartbeat payload. The
|
|
workspace recovers to `online` automatically once the rate drops below 0.5.
|