Add Hermes gateway onboarding docs

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-06-26 13:53:20 +00:00
1 parent 594438c931
commit 802245c94b
7 files changed
+187 -3

No files matched your search

@@ -51,6 +51,21 @@ describe("buildInviteOnboardingTextDocument", () => {
expect(text).toContain('"adapterType": "openclaw_gateway"');
expect(text).toContain("headers.x-openclaw-token");
expect(text).toContain("Do NOT use /v1/responses or /hooks/*");
expect(text).toContain('adapterType: "hermes_gateway"');
expect(text).toContain('"adapterType": "hermes_gateway"');
expect(text).toContain("API_SERVER_ENABLED=true");
expect(text).toContain("API_SERVER_KEY");
expect(text).toContain("hermes gateway run --replace --accept-hooks");
expect(text).toContain("Default Hermes API server port: 8642");
expect(text).toContain("agentDefaultsPayload.apiBaseUrl");
expect(text).toContain("agentDefaultsPayload.paperclipApiUrl");
expect(text).toContain("hermes_local");
expect(text).toContain("Hermes-originated Paperclip API usage");
expect(text).toContain("http://127.0.0.1:8642");
expect(text).toContain("http://192.168.1.25:8642");
expect(text).toContain("tailnet-name.ts.net:8642");
expect(text).toContain("http://host.docker.internal:8642");
expect(text).toContain("https://hermes-gateway.example");
expect(text).toContain("set the first reachable candidate as agentDefaultsPayload.paperclipApiUrl");
expect(text).toContain("PAPERCLIP_API_KEY");
expect(text).toContain("Use your runtime's normal skill or instruction installation path.");
+35
View File
@@ -72,4 +72,39 @@ describe("llm routes", () => {
expect(res.text).toContain("Timer heartbeats are opt-in for new hires.");
expect(res.text).toContain("Leave runtimeConfig.heartbeat.enabled false");
});
it("serves static Hermes Gateway configuration docs before the plugin is installed", async () => {
const app = await createApp({
type: "board",
userId: "board-user",
companyIds: ["company-1"],
source: "local_implicit",
isInstanceAdmin: true,
});
const indexRes = await request(app).get("/api/llms/agent-configuration.txt");
expect(indexRes.status).toBe(200);
expect(indexRes.text).toContain(
"- hermes_gateway: /llms/agent-configuration/hermes_gateway.txt",
);
const res = await request(app).get("/api/llms/agent-configuration/hermes_gateway.txt");
expect(res.status).toBe(200);
expect(res.text).toContain("Adapter: hermes_gateway");
expect(res.text).toContain('adapterType": "hermes_gateway"');
expect(res.text).toContain("API_SERVER_ENABLED=true");
expect(res.text).toContain("API_SERVER_KEY");
expect(res.text).toContain("hermes gateway run --replace --accept-hooks");
expect(res.text).toContain("Default Hermes API server port: 8642");
expect(res.text).toContain("agentDefaultsPayload.apiBaseUrl");
expect(res.text).toContain("agentDefaultsPayload.paperclipApiUrl");
expect(res.text).toContain("hermes_local");
expect(res.text).toContain("Hermes-originated Paperclip API usage");
expect(res.text).toContain("http://127.0.0.1:8642");
expect(res.text).toContain("http://192.168.1.25:8642");
expect(res.text).toContain("tailnet-name.ts.net:8642");
expect(res.text).toContain("http://host.docker.internal:8642");
expect(res.text).toContain("https://hermes-gateway.example");
});
});
+55
View File
@@ -0,0 +1,55 @@
export const hermesGatewayAgentConfigurationDoc = `# hermes_gateway agent configuration
Adapter: hermes_gateway
Use when:
- Hermes is already running outside Paperclip and exposes its API server.
- Paperclip should invoke Hermes through the gateway HTTP API instead of spawning Hermes locally.
- The Hermes runtime may live on another host, a private overlay, in Docker, or behind a TLS reverse proxy.
Don't use when:
- Paperclip should start the Hermes CLI directly on the same host. Use hermes_local from the external Hermes adapter plugin for that flow.
- Hermes is the process calling Paperclip APIs after claiming its key. That is Hermes-originated Paperclip API usage, not the gateway adapter transport.
Runtime distinction:
- hermes_local: Paperclip starts Hermes on the Paperclip host through the external Hermes adapter plugin.
- hermes_gateway: Paperclip calls an already-running Hermes API server using agentDefaultsPayload.apiBaseUrl.
- Hermes-originated Paperclip API usage: Hermes calls Paperclip with PAPERCLIP_API_URL and PAPERCLIP_API_KEY after invite approval and key claim. Do not use agentDefaultsPayload.apiBaseUrl for Paperclip API calls.
Hermes gateway process setup:
- Set API_SERVER_ENABLED=true.
- Set API_SERVER_KEY to a generated secret value. Do not paste a real key into tickets, docs, screenshots, or tests.
- Start Hermes with: hermes gateway run --replace --accept-hooks
- Default Hermes API server port: 8642.
Join request minimum:
{
"requestType": "agent",
"agentName": "My Hermes Gateway Agent",
"adapterType": "hermes_gateway",
"capabilities": "Hermes gateway agent",
"agentDefaultsPayload": {
"apiBaseUrl": "http://127.0.0.1:8642",
"apiKey": "<same-value-as-API_SERVER_KEY>",
"paperclipApiUrl": "http://localhost:3100"
}
}
Core fields:
- agentDefaultsPayload.apiBaseUrl (string, required): Base URL for the Hermes API server as reachable from the Paperclip server.
- agentDefaultsPayload.apiKey (string, required unless the adapter package documents another auth field): Hermes API server key matching API_SERVER_KEY.
- agentDefaultsPayload.paperclipApiUrl (string, strongly recommended): Paperclip base URL as reachable from Hermes for invite, claim, skill bootstrap, and later Paperclip API calls.
- agentDefaultsPayload.timeoutSec or timeoutMs (number, optional): Runtime request timeout when supported by the installed Hermes gateway adapter.
Network examples:
- Local loopback on one host: agentDefaultsPayload.apiBaseUrl = "http://127.0.0.1:8642"; agentDefaultsPayload.paperclipApiUrl = "http://127.0.0.1:3100".
- LAN/private network: agentDefaultsPayload.apiBaseUrl = "http://192.168.1.25:8642"; agentDefaultsPayload.paperclipApiUrl = "http://192.168.1.10:3100". Use private IPs or hostnames reachable from both machines.
- Private overlay: agentDefaultsPayload.apiBaseUrl = "http://hermes-host.tailnet-name.ts.net:8642"; agentDefaultsPayload.paperclipApiUrl = "http://paperclip-host.tailnet-name.ts.net:3100". Add the Paperclip hostname with pnpm paperclipai allowed-hostname <host> when authenticated/private mode requires it.
- Docker: if Hermes runs on the host and Paperclip runs in Docker, use agentDefaultsPayload.apiBaseUrl = "http://host.docker.internal:8642". If Hermes runs in another container, use the Compose service DNS name such as "http://hermes:8642".
- Reverse proxy/TLS: publish Hermes behind HTTPS and set agentDefaultsPayload.apiBaseUrl = "https://hermes-gateway.example"; set agentDefaultsPayload.paperclipApiUrl = "https://paperclip.example". Keep API_SERVER_KEY required at the origin or proxy.
Security notes:
- Treat API_SERVER_KEY and claimed PAPERCLIP_API_KEY as secrets.
- Prefer private network or TLS for non-loopback gateway access.
- Use placeholders such as <same-value-as-API_SERVER_KEY> in docs and tests.
`;
+34 -3
View File
@@ -1585,17 +1585,17 @@ function buildInviteOnboardingManifest(
),
onboarding: {
instructions:
"Join as an external Paperclip agent, save your one-time claim secret, wait for board approval, then claim your API key. Use requestType='agent', include your agentName and capabilities, and set adapterType plus agentDefaultsPayload for your runtime when applicable. OpenClaw Gateway agents must use adapterType='openclaw_gateway', set agentDefaultsPayload.url to a ws:// or wss:// gateway endpoint, and include agentDefaultsPayload.headers.x-openclaw-token.",
"Join as an external Paperclip agent, save your one-time claim secret, wait for board approval, then claim your API key. Use requestType='agent', include your agentName and capabilities, and set adapterType plus agentDefaultsPayload for your runtime when applicable. OpenClaw Gateway agents must use adapterType='openclaw_gateway', set agentDefaultsPayload.url to a ws:// or wss:// gateway endpoint, and include agentDefaultsPayload.headers.x-openclaw-token. Hermes Gateway agents must use adapterType='hermes_gateway', start Hermes with API_SERVER_ENABLED=true, API_SERVER_KEY, and `hermes gateway run --replace --accept-hooks`, then set agentDefaultsPayload.apiBaseUrl and agentDefaultsPayload.paperclipApiUrl.",
inviteMessage: extractInviteMessage(invite),
recommendedAdapterType: null,
requiredFields: {
requestType: "agent",
agentName: "Display name for this agent",
adapterType:
"Adapter type for this runtime. Use 'openclaw_gateway' only for OpenClaw Gateway agents.",
"Adapter type for this runtime. Use 'openclaw_gateway' only for OpenClaw Gateway agents. Use 'hermes_gateway' only for Hermes Gateway agents.",
capabilities: "Optional capability summary",
agentDefaultsPayload:
"Runtime-specific adapter config. OpenClaw Gateway agents must include url (ws:// or wss://) and headers.x-openclaw-token. Other runtimes should include the config their adapter expects."
"Runtime-specific adapter config. OpenClaw Gateway agents must include url (ws:// or wss://) and headers.x-openclaw-token. Hermes Gateway agents must include apiBaseUrl, API_SERVER_KEY-backed auth, and paperclipApiUrl. Other runtimes should include the config their adapter expects."
},
registrationEndpoint: {
method: "POST",
@@ -1744,6 +1744,37 @@ export function buildInviteOnboardingTextDocument(
For OpenClaw Gateway, include agentDefaultsPayload.headers.x-openclaw-token with your gateway token. Legacy x-openclaw-auth is also accepted, but x-openclaw-token is preferred. Do NOT use /v1/responses or /hooks/* in this gateway join flow.
Hermes Gateway setup:
- adapterType: "hermes_gateway"
- Start Hermes with API_SERVER_ENABLED=true and API_SERVER_KEY=<random-gateway-key>.
- Run: hermes gateway run --replace --accept-hooks
- Default Hermes API server port: 8642.
- Set agentDefaultsPayload.apiBaseUrl to the Hermes gateway URL Paperclip can reach.
- Set agentDefaultsPayload.paperclipApiUrl to the Paperclip base URL Hermes can reach.
- Use hermes_local when Paperclip should start Hermes on the Paperclip host.
- Use hermes_gateway when Paperclip should call an already-running Hermes API server.
- Hermes-originated Paperclip API usage means Hermes calls Paperclip with PAPERCLIP_API_URL and PAPERCLIP_API_KEY after approval/key claim. Do not confuse that with agentDefaultsPayload.apiBaseUrl, which points Paperclip to Hermes.
Hermes Gateway payload example:
{
"requestType": "agent",
"agentName": "My Hermes Gateway Agent",
"adapterType": "hermes_gateway",
"capabilities": "Hermes gateway agent",
"agentDefaultsPayload": {
"apiBaseUrl": "http://127.0.0.1:8642",
"apiKey": "<same-value-as-API_SERVER_KEY>",
"paperclipApiUrl": "http://localhost:3100"
}
}
Hermes Gateway network examples:
- Local loopback: agentDefaultsPayload.apiBaseUrl = "http://127.0.0.1:8642" and agentDefaultsPayload.paperclipApiUrl = "http://127.0.0.1:3100".
- LAN/private network: use reachable private addresses, for example agentDefaultsPayload.apiBaseUrl = "http://192.168.1.25:8642" and agentDefaultsPayload.paperclipApiUrl = "http://192.168.1.10:3100".
- Private overlay: use overlay DNS names, for example agentDefaultsPayload.apiBaseUrl = "http://hermes-host.tailnet-name.ts.net:8642" and agentDefaultsPayload.paperclipApiUrl = "http://paperclip-host.tailnet-name.ts.net:3100".
- Docker: if Paperclip runs in Docker and Hermes runs on the host, use agentDefaultsPayload.apiBaseUrl = "http://host.docker.internal:8642"; if both run in Compose, use the Hermes service name.
- Reverse proxy/TLS: use HTTPS origins, for example agentDefaultsPayload.apiBaseUrl = "https://hermes-gateway.example" and agentDefaultsPayload.paperclipApiUrl = "https://paperclip.example".
Expected response includes:
- request id
- one-time claimSecret
+19
View File
@@ -3,8 +3,13 @@ import type { Db } from "@paperclipai/db";
import { AGENT_ICON_NAMES } from "@paperclipai/shared";
import { forbidden } from "../errors.js";
import { listServerAdapters } from "../adapters/index.js";
import { hermesGatewayAgentConfigurationDoc } from "../adapters/hermes-gateway-doc.js";
import { agentService } from "../services/agents.js";
const pluginOnlyAdapterDocs = new Map<string, string>([
["hermes_gateway", hermesGatewayAgentConfigurationDoc],
]);
function hasCreatePermission(agent: { role: string; permissions: Record<string, unknown> | null | undefined }) {
if (!agent.permissions || typeof agent.permissions !== "object") return false;
return Boolean((agent.permissions as Record<string, unknown>).canCreateAgents);
@@ -34,6 +39,15 @@ export function llmRoutes(db: Db) {
"Installed adapters:",
...adapters.map((adapter) => `- ${adapter.type}: /llms/agent-configuration/${adapter.type}.txt`),
"",
"Plugin-only adapter docs:",
...Array.from(pluginOnlyAdapterDocs.keys())
.filter(
(adapterType) => !adapters.some((adapter) => adapter.type === adapterType),
)
.map(
(adapterType) => `- ${adapterType}: /llms/agent-configuration/${adapterType}.txt`,
),
"",
"Related API endpoints:",
"- GET /api/companies/:companyId/agent-configurations",
"- GET /api/agents/:id/configuration",
@@ -72,6 +86,11 @@ export function llmRoutes(db: Db) {
const adapterType = req.params.adapterType as string;
const adapter = listServerAdapters().find((entry) => entry.type === adapterType);
if (!adapter) {
const pluginOnlyDoc = pluginOnlyAdapterDocs.get(adapterType);
if (pluginOnlyDoc) {
res.type("text/plain").send(pluginOnlyDoc);
return;
}
res.status(404).type("text/plain").send(`Unknown adapter type: ${adapterType}`);
return;
}
@@ -0,0 +1,26 @@
import { describe, expect, it } from "vitest";
import { buildAgentOnboardingPrompt } from "./agent-onboarding-prompt";
describe("buildAgentOnboardingPrompt", () => {
it("includes Hermes Gateway setup guidance for external agents", () => {
const prompt = buildAgentOnboardingPrompt({
onboardingTextUrl: "http://localhost:3100/api/invites/token-123/onboarding.txt",
connectionCandidates: ["http://192.168.1.10:3100"],
});
expect(prompt).toContain('adapterType: "hermes_gateway"');
expect(prompt).toContain("API_SERVER_ENABLED=true");
expect(prompt).toContain("API_SERVER_KEY");
expect(prompt).toContain("hermes gateway run --replace --accept-hooks");
expect(prompt).toContain("default Hermes API server port is `8642`");
expect(prompt).toContain("agentDefaultsPayload.apiBaseUrl");
expect(prompt).toContain("agentDefaultsPayload.paperclipApiUrl");
expect(prompt).toContain("http://127.0.0.1:8642");
expect(prompt).toContain("http://<private-ip>:8642");
expect(prompt).toContain("http://<tailnet-host>:8642");
expect(prompt).toContain("http://host.docker.internal:8642");
expect(prompt).toContain("https://hermes-gateway.example");
expect(prompt).toContain("`hermes_local` runs Hermes on the Paperclip host");
expect(prompt).toContain("Hermes-originated Paperclip API calls");
});
});
+3
View File
@@ -55,6 +55,9 @@ Join flow:
OpenClaw Gateway note:
If you are an OpenClaw Gateway agent, use \`adapterType: "openclaw_gateway"\`, set \`agentDefaultsPayload.url\` to your \`ws://\` or \`wss://\` gateway URL, and include \`agentDefaultsPayload.headers["x-openclaw-token"]\` with your gateway token. Do not use \`/v1/responses\` or \`/hooks/*\` in that join flow.
Hermes Gateway note:
If you are a Hermes Gateway agent, use \`adapterType: "hermes_gateway"\`. Start Hermes with \`API_SERVER_ENABLED=true\`, \`API_SERVER_KEY=<random-gateway-key>\`, and \`hermes gateway run --replace --accept-hooks\`; the default Hermes API server port is \`8642\`. Set \`agentDefaultsPayload.apiBaseUrl\` to the Hermes gateway URL Paperclip can reach and \`agentDefaultsPayload.paperclipApiUrl\` to the Paperclip base URL Hermes can reach. For local loopback use \`http://127.0.0.1:8642\`; for LAN/private network use \`http://<private-ip>:8642\`; for a private overlay use \`http://<tailnet-host>:8642\`; from Docker use \`http://host.docker.internal:8642\`; behind reverse proxy/TLS use \`https://hermes-gateway.example\`. \`hermes_local\` runs Hermes on the Paperclip host; \`hermes_gateway\` calls an already-running Hermes API server; Hermes-originated Paperclip API calls use the claimed \`PAPERCLIP_API_KEY\` and \`PAPERCLIP_API_URL\`, not \`agentDefaultsPayload.apiBaseUrl\`.
After you have connected to Paperclip, review and follow the full onboarding instructions in onboarding.txt.
`;
}