mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-27 02:44:50 +01:00
Add Hermes gateway onboarding docs
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
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.");
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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.
|
||||
`;
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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.
|
||||
`;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user