Direct Mode (HTTP Bridge)
Direct Mode (HTTP Bridge)
Section titled “Direct Mode (HTTP Bridge)”v1.2.0 — Skip the relay entirely. Direct Mode enables server-to-server, E2E encrypted messaging over plain HTTP.
When to Use Direct Mode
Section titled “When to Use Direct Mode”| Scenario | Use |
|---|---|
| Desktop agents (Cursor, Claude Desktop) | Relay — SSE push, offline queuing |
| Serverless agents (Lambda, Cloud Functions) | Relay — Stateless tool API |
| Server-to-server (both always online) | Direct Mode — zero relay dependency |
| Internal microservice mesh | Direct Mode — lowest latency |
| Need offline message delivery | Relay — store-and-forward |
Direct Mode is designed for users who want to connect their agent easily to any agent setup, working on a secure and fast pipe — without depending on a centralized relay.
Architecture
Section titled “Architecture”A DirectServer exposes three HTTP endpoints under a /syn-link prefix:
| Method | Endpoint | Purpose |
|---|---|---|
GET | /syn-link/identity | Returns the agent’s public keys (Curve25519 + Ed25519) |
GET | /syn-link/tools | Returns available tool definitions |
POST | /syn-link/message | Receive an encrypted message, return encrypted response |
A DirectClient calls these endpoints with full NaCl encryption and Ed25519 signature verification.
Message Flow
Section titled “Message Flow”┌──────────────┐ ┌──────────────┐│ DirectClient │ │ DirectServer │└──────┬───────┘ └──────┬───────┘ │ 1. GET /syn-link/identity │ │ ────────────────────────────────────────► │ │ ◄──── { public_key, signing_public_key } │ │ │ │ 2. Encrypt with NaCl box │ │ Sign with Ed25519 │ │ │ │ 3. POST /syn-link/message │ │ ────────────────────────────────────────► │ │ { encrypted_content, nonce, │ │ signature, sender_public_key } │ │ │ │ 4. Verify signature │ │ Decrypt message │ │ Process → encrypt response │ │ │ │ ◄──── { encrypted_response, nonce } │ │ │ │ 5. Decrypt response │DirectServer
Section titled “DirectServer”import { DirectServer } from "syn-link";import { getOrCreateKeyPair } from "syn-link";import express from "express";
const keys = getOrCreateKeyPair("./my-agent-data");
const server = new DirectServer({ keys, agentId: "my-service-agent", tools: [ { name: "check_order", description: "Check the status of an order by ID", parameters: { type: "object", properties: { order_id: { type: "string", description: "The order ID" }, }, required: ["order_id"], }, }, ], onMessage: async (request) => { console.log(`From: ${request.senderAgentId}`); console.log(`Content: ${request.content}`); console.log(`Type: ${request.contentType}`);
// Process the message and return a response return `Processed: ${request.content}`; },});
const app = express();app.use(express.json());app.use("/syn-link", server.handler());app.listen(3000);Config Options
Section titled “Config Options”| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
keys | KeyPair | ✅ | — | Agent’s NaCl box + Ed25519 keypair |
agentId | string | — | "direct-agent" | Identity label for logging and response headers |
tools | ToolDefinition[] | — | [] | Tool definitions exposed via GET /tools |
onMessage | (req: DirectRequest) => Promise<string> | string | — | — | Handler for incoming messages. Return the response string |
resolvePublicKey | (agentId: string) => Promise<string | null> | string | null | — | — | Optional: resolve sender’s public key by agent ID |
DirectRequest Object
Section titled “DirectRequest Object”The onMessage handler receives a DirectRequest:
| Field | Type | Description |
|---|---|---|
content | string | Plaintext content (after decryption) |
contentType | string | Content type hint (text, tool_call, etc.) |
senderPublicKey | string | Sender’s Curve25519 public key (base64) |
senderAgentId | string | Sender’s agent ID |
Runtime Tool Updates
Section titled “Runtime Tool Updates”Tools can be updated at any time — clients see the new list on the next getTools() call:
server.setTools([ ...server.getTools(), { name: "new_tool", description: "A tool added at runtime", parameters: { type: "object", properties: {}, required: [] }, },]);Low-Level Processing
Section titled “Low-Level Processing”For custom HTTP frameworks where you handle routing yourself:
const body = parseRequestBody(req); // your framework's body parserconst result = await server.processMessage(body);sendJsonResponse(res, 200, result);DirectClient
Section titled “DirectClient”import { DirectClient } from "syn-link";import { getOrCreateKeyPair } from "syn-link";
const keys = getOrCreateKeyPair("./my-client-data");
const client = new DirectClient({ keys, agentId: "nala-agent",});Config Options
Section titled “Config Options”| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
keys | KeyPair | ✅ | — | Your agent’s NaCl box + Ed25519 keypair |
agentId | string | — | "direct-client" | Your agent ID (included in signed requests) |
identityCacheTtlMs | number | — | 300000 (5 min) | How long to cache the target’s public identity |
Discover Tools
Section titled “Discover Tools”const tools = await client.getTools("https://agent.example.com");console.log(tools);// [{ name: "check_order", description: "...", parameters: {...} }]Send an Encrypted Message
Section titled “Send an Encrypted Message”const response = await client.send("https://agent.example.com", { message: "Check order 123", contentType: "tool_call",});console.log(response); // "Processed: Check order 123"The send() method:
- Fetches the target’s identity (public key + signing key) — cached with TTL
- Encrypts the message with NaCl box (Curve25519 + XSalsa20 + Poly1305)
- Signs the payload with Ed25519
- POSTs to
/syn-link/message - Decrypts the encrypted response
Identity Caching
Section titled “Identity Caching”The client caches identity lookups with a configurable TTL (default: 5 minutes). This avoids redundant HTTP calls for repeated messages to the same server.
// Force re-fetch (useful for testing or key rotation)client.clearCache();Security
Section titled “Security”Encryption
Section titled “Encryption”Every message is encrypted with NaCl box (Curve25519 key exchange + XSalsa20-Poly1305 AEAD) — the same proven cryptographic primitives from libsodium. The server’s response is encrypted back to the sender using the same mechanism.
Signature Verification
Section titled “Signature Verification”Requests include an Ed25519 signature covering:
encrypted_content + "\n" + nonce + "\n" + agent_idThe server verifies this signature before processing the message. If the signature is invalid, the request is rejected with 403 Forbidden.
No Forward Secrecy
Section titled “No Forward Secrecy”Direct Mode uses v1 NaCl box encryption (no Double Ratchet). This is fully encrypted end-to-end but does not provide forward secrecy. If your threat model requires forward secrecy, use the relay-based agent.connect() with the Double Ratchet protocol.
Framework Compatibility
Section titled “Framework Compatibility”The handler() method returns a standard Node.js request handler compatible with:
- Express —
app.use("/syn-link", server.handler()) - Fastify — via middleware adapter
- Plain
http.createServer— direct usage - Any framework with
req.method,req.url, and standard body handling
The handler auto-detects whether the response object supports Express-style (res.status().json()) or Node http-style (res.writeHead() + res.end()).
Full Example: Two Agents Communicating
Section titled “Full Example: Two Agents Communicating”import { DirectServer, DirectClient, getOrCreateKeyPair } from "syn-link";import express from "express";
// ── Agent A: The Server ──────────────────────────────const serverKeys = getOrCreateKeyPair("./agent-a-keys");const directServer = new DirectServer({ keys: serverKeys, agentId: "agent-a", tools: [ { name: "summarize", description: "Summarize a document", parameters: { type: "object", properties: { text: { type: "string", description: "Text to summarize" }, }, required: ["text"], }, }, ], onMessage: async (req) => { // Your LLM logic here return `Summary of: ${req.content.substring(0, 50)}...`; },});
const app = express();app.use(express.json());app.use("/syn-link", directServer.handler());app.listen(3000, () => console.log("Agent A listening on :3000"));
// ── Agent B: The Client ──────────────────────────────const clientKeys = getOrCreateKeyPair("./agent-b-keys");const client = new DirectClient({ keys: clientKeys, agentId: "agent-b",});
// Discover what Agent A can doconst tools = await client.getTools("http://localhost:3000");console.log("Available tools:", tools.map(t => t.name));
// Send an encrypted messageconst response = await client.send("http://localhost:3000", { message: "Please summarize the quarterly report...", contentType: "tool_call",});console.log("Response:", response);