Make Hermes adapters built in

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-06-26 14:05:31 +00:00
1 parent bce5b6e5f9
commit 88119ef815
26 files changed
+171 -42

No files matched your search

+10 -9
View File
@@ -181,18 +181,19 @@ A change is done when all are true:
## 11. Fork-Specific: HenkDz/paperclip
This is a fork of `paperclipai/paperclip` with QoL patches and an **external-only** Hermes adapter story on branch `feat/externalize-hermes-adapter` ([tree](https://github.com/HenkDz/paperclip/tree/feat/externalize-hermes-adapter)).
This is a fork of `paperclipai/paperclip` with QoL patches and a **built-in** Hermes adapter story on branch `feat/externalize-hermes-adapter` ([tree](https://github.com/HenkDz/paperclip/tree/feat/externalize-hermes-adapter)).
### Branch Strategy
- `feat/externalize-hermes-adapter` → core has **no** `hermes-paperclip-adapter` dependency and **no** built-in `hermes_local` registration. Install Hermes via the Adapter Plugin manager (`@henkey/hermes-paperclip-adapter` or a `file:` path).
- Older fork branches may still document built-in Hermes; treat this file as authoritative for the externalize branch.
- `feat/externalize-hermes-adapter` now ships `hermes_local` and `hermes_gateway` as built-in core adapters.
- Older fork branches may still document plugin-only Hermes; treat this file as authoritative for the current branch.
### Hermes (plugin only)
### Hermes (built-in)
- Register through **Board → Adapter manager** (same as Droid). Type remains `hermes_local` once the package is loaded.
- UI uses generic **config-schema** + **ui-parser.js** from the package — no Hermes imports in `server/` or `ui/` source.
- Optional: `file:` entry in `~/.paperclip/adapter-plugins.json` for local dev of the adapter repo.
- `hermes_local` is available without Adapter manager installation and runs the local Hermes CLI.
- `hermes_gateway` is available without Adapter manager installation and calls an already-running Hermes API server.
- Operators may still install external Hermes packages through Adapter manager to override/shadow the built-ins.
- Optional: `file:` entry in `~/.paperclip/adapter-plugins.json` remains useful for local development of override packages.
### Local Dev
@@ -217,5 +218,5 @@ PR #2218 (`feat/external-adapter-phase1`) adds external adapter support. See roo
- Adapters can be loaded as external plugins via `~/.paperclip/adapter-plugins.json`
- The plugin-loader should have ZERO hardcoded adapter imports — pure dynamic loading
- `createServerAdapter()` must include ALL optional fields (especially `detectModel`)
- Built-in UI adapters can shadow external plugin parsers — remove built-in when fully externalizing
- Reference external adapters: Hermes (`@henkey/hermes-paperclip-adapter` or `file:`) and Droid (npm)
- Built-in UI adapters can shadow external plugin parsers; external override pause/resume should restore the built-in parser.
- Reference external adapters: Droid (npm); Hermes can also be tested as an override package.
+2
View File
@@ -21,6 +21,8 @@ const workspacePaths = [
"packages/adapter-utils",
"packages/adapters/claude-local",
"packages/adapters/codex-local",
"packages/adapters/hermes-gateway",
"packages/adapters/hermes-local",
"packages/adapters/openclaw-gateway",
];
+2
View File
@@ -44,6 +44,7 @@
"@paperclipai/adapter-cursor-local": "workspace:*",
"@paperclipai/adapter-gemini-local": "workspace:*",
"@paperclipai/adapter-grok-local": "workspace:*",
"@paperclipai/adapter-hermes-gateway": "workspace:*",
"@paperclipai/adapter-opencode-local": "workspace:*",
"@paperclipai/adapter-pi-local": "workspace:*",
"@paperclipai/adapter-openclaw-gateway": "workspace:*",
@@ -51,6 +52,7 @@
"@paperclipai/db": "workspace:*",
"@paperclipai/server": "workspace:*",
"@paperclipai/shared": "workspace:*",
"@paperclipai/hermes-paperclip-adapter": "workspace:*",
"drizzle-orm": "0.45.2",
"dotenv": "^17.0.1",
"commander": "^13.1.0",
+14
View File
@@ -6,6 +6,8 @@ import { printCursorStreamEvent } from "@paperclipai/adapter-cursor-local/cli";
import { printCursorCloudEvent } from "@paperclipai/adapter-cursor-cloud/cli";
import { printGeminiStreamEvent } from "@paperclipai/adapter-gemini-local/cli";
import { printGrokStreamEvent } from "@paperclipai/adapter-grok-local/cli";
import { formatStdoutEvent as printHermesGatewayStreamEvent } from "@paperclipai/adapter-hermes-gateway/cli";
import { printHermesStreamEvent } from "@paperclipai/hermes-paperclip-adapter/cli";
import { printOpenCodeStreamEvent } from "@paperclipai/adapter-opencode-local/cli";
import { printPiStreamEvent } from "@paperclipai/adapter-pi-local/cli";
import { printOpenClawGatewayStreamEvent } from "@paperclipai/adapter-openclaw-gateway/cli";
@@ -57,6 +59,16 @@ const grokLocalCLIAdapter: CLIAdapterModule = {
formatStdoutEvent: printGrokStreamEvent,
};
const hermesGatewayCLIAdapter: CLIAdapterModule = {
type: "hermes_gateway",
formatStdoutEvent: printHermesGatewayStreamEvent,
};
const hermesLocalCLIAdapter: CLIAdapterModule = {
type: "hermes_local",
formatStdoutEvent: printHermesStreamEvent,
};
const openclawGatewayCLIAdapter: CLIAdapterModule = {
type: "openclaw_gateway",
formatStdoutEvent: printOpenClawGatewayStreamEvent,
@@ -73,6 +85,8 @@ const adaptersByType = new Map<string, CLIAdapterModule>(
cursorCloudCLIAdapter,
geminiLocalCLIAdapter,
grokLocalCLIAdapter,
hermesGatewayCLIAdapter,
hermesLocalCLIAdapter,
openclawGatewayCLIAdapter,
processCLIAdapter,
httpCLIAdapter,
+1 -1
View File
@@ -153,7 +153,7 @@ Invariant: every business record belongs to exactly one company.
- `status` enum: `active | paused | idle | running | error | pending_approval | terminated`
- `reports_to` uuid fk `agents.id` null
- `capabilities` text null
- `adapter_type` text; built-ins include `process`, `http`, `claude_local`, `codex_local`, `gemini_local`, `opencode_local`, `pi_local`, `cursor`, and `openclaw_gateway`
- `adapter_type` text; built-ins include `process`, `http`, `claude_local`, `codex_local`, `gemini_local`, `opencode_local`, `pi_local`, `cursor`, `hermes_local`, `hermes_gateway`, and `openclaw_gateway`
- `adapter_config` jsonb not null
- `runtime_config` jsonb not null default `{}`; may include Paperclip runtime policy such as `modelProfiles.cheap.adapterConfig` for an optional low-cost model lane that does not change the primary adapter config
- `default_environment_id` uuid fk `environments.id` null
+1
View File
@@ -25,6 +25,7 @@ When a heartbeat fires, Paperclip:
| Cursor | `cursor` | Runs Cursor in background mode |
| Pi Local | `pi_local` | Runs an embedded Pi agent locally |
| Hermes Local | `hermes_local` | Runs Hermes CLI locally (`hermes-paperclip-adapter`) |
| Hermes Gateway | `hermes_gateway` | Connects to a running Hermes API server |
| OpenClaw Gateway | `openclaw_gateway` | Connects to an OpenClaw gateway endpoint |
| [Process](/adapters/process) | `process` | Executes arbitrary shell commands |
| [HTTP](/adapters/http) | `http` | Sends webhooks to external agents |
+2 -1
View File
@@ -40,6 +40,7 @@ Built-in adapters:
- `cursor`: runs Cursor in background mode
- `pi_local`: runs an embedded Pi agent locally
- `hermes_local`: runs your local `hermes` CLI (`hermes-paperclip-adapter`)
- `hermes_gateway`: connects to a running Hermes API server
- `openclaw_gateway`: connects to an OpenClaw gateway endpoint
- `process`: generic shell command adapter
- `http`: calls an external HTTP endpoint
@@ -48,7 +49,7 @@ External plugin adapters (install via the adapter manager or API):
- `droid_local`: runs your local Factory Droid CLI (`@henkey/droid-paperclip-adapter`)
For local CLI adapters (`claude_local`, `codex_local`, `opencode_local`, `hermes_local`, `droid_local`), Paperclip assumes the CLI is already installed and authenticated on the host machine.
For local CLI adapters (`claude_local`, `codex_local`, `opencode_local`, `hermes_local`, `droid_local`), Paperclip assumes the CLI is already installed and authenticated on the host machine. For `hermes_gateway`, Paperclip assumes the Hermes API server is already reachable and configured with an API key.
## 3.2 Runtime behavior
@@ -28,8 +28,8 @@ Create agents from the Agents page. Each agent requires:
- **Capabilities** — short description of what this agent does
Common adapter choices:
- `claude_local` / `codex_local` / `opencode_local` for local coding agents
- `openclaw_gateway` / `http` for webhook-based external agents
- `claude_local` / `codex_local` / `opencode_local` / `hermes_local` for local coding agents
- `hermes_gateway` / `openclaw_gateway` / `http` for webhook-based external agents
- `process` for generic local command execution
For `opencode_local`, configure an explicit `adapterConfig.model` (`provider/model`).
+4 -4
View File
@@ -1,10 +1,10 @@
# Hermes Gateway Adapter
External Paperclip adapter for Hermes Agent's authenticated HTTP/SSE API server.
Built-in Paperclip adapter for Hermes Agent's authenticated HTTP/SSE API server.
Install it through Paperclip's Adapter manager as a local path or npm package. The
adapter type is `hermes_gateway`; it is intentionally not registered as a built-in
core adapter.
The adapter type is `hermes_gateway` and ships with Paperclip core. Operators can
still install an external adapter package with the same type through Adapter
manager when they intentionally want to override the built-in.
Required config:
@@ -2,7 +2,8 @@
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src"
"rootDir": "src",
"types": ["node"]
},
"include": ["src"]
}
+7 -13
View File
@@ -35,9 +35,8 @@ This adapter provides:
## Installation
```bash
npm install @paperclipai/hermes-paperclip-adapter
```
This package ships with Paperclip core as the built-in `hermes_local` adapter.
No Adapter manager installation is required for normal Paperclip use.
### Prerequisites
@@ -47,17 +46,12 @@ npm install @paperclipai/hermes-paperclip-adapter
## Quick Start
### 1. Install the adapter in Paperclip
Install through **Board -> Adapter manager**:
```text
@paperclipai/hermes-paperclip-adapter
```
### 1. Optional: override the built-in for adapter development
For local adapter development, install the package from a local path in Adapter
manager, or add an entry to `~/.paperclip/adapter-plugins.json` and restart
Paperclip:
Paperclip. The external package can override the built-in `hermes_local`
adapter while it is enabled:
```json
[
@@ -72,8 +66,8 @@ Paperclip:
The adapter package exports `createServerAdapter()` for the server, a
declarative config schema for the generic agent form, and `./ui-parser` for run
transcript parsing. Paperclip core does not require source edits or a built-in
`hermes_local` registration.
transcript parsing. Paperclip core imports these same package entrypoints for
the built-in adapter registration.
### 2. Create a Hermes agent in Paperclip
+2 -1
View File
@@ -2,7 +2,8 @@
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src"
"rootDir": "src",
"types": ["node"]
},
"include": ["src"]
}
+2
View File
@@ -35,6 +35,8 @@ export const AGENT_ADAPTER_TYPES = [
"codex_local",
"cursor_cloud",
"gemini_local",
"hermes_gateway",
"hermes_local",
"opencode_local",
"pi_local",
"cursor",
+2
View File
@@ -32,6 +32,8 @@ const workspacePaths = [
"packages/adapter-utils",
"packages/adapters/claude-local",
"packages/adapters/codex-local",
"packages/adapters/hermes-gateway",
"packages/adapters/hermes-local",
"packages/adapters/opencode-local",
"packages/adapters/openclaw-gateway",
];
+2
View File
@@ -51,6 +51,7 @@
"@paperclipai/adapter-cursor-local": "workspace:*",
"@paperclipai/adapter-gemini-local": "workspace:*",
"@paperclipai/adapter-grok-local": "workspace:*",
"@paperclipai/adapter-hermes-gateway": "workspace:*",
"@paperclipai/adapter-openclaw-gateway": "workspace:*",
"@paperclipai/adapter-opencode-local": "workspace:*",
"@paperclipai/adapter-pi-local": "workspace:*",
@@ -59,6 +60,7 @@
"@paperclipai/plugin-sdk": "workspace:*",
"@paperclipai/shared": "workspace:*",
"@paperclipai/skills-catalog": "workspace:*",
"@paperclipai/hermes-paperclip-adapter": "workspace:*",
"ajv": "^8.20.0",
"ajv-formats": "^3.0.1",
"better-auth": "1.6.20",
+22 -5
View File
@@ -38,6 +38,7 @@ describe("server adapter registry", () => {
beforeEach(() => {
unregisterServerAdapter("external_test");
unregisterServerAdapter("hermes_local");
unregisterServerAdapter("hermes_gateway");
unregisterServerAdapter("claude_local");
setOverridePaused("claude_local", false);
});
@@ -45,6 +46,7 @@ describe("server adapter registry", () => {
afterEach(() => {
unregisterServerAdapter("external_test");
unregisterServerAdapter("hermes_local");
unregisterServerAdapter("hermes_gateway");
unregisterServerAdapter("claude_local");
setOverridePaused("claude_local", false);
});
@@ -128,11 +130,22 @@ describe("server adapter registry", () => {
]);
});
it("does not ship hermes_local as a built-in adapter, but accepts it externally", () => {
expect(findServerAdapter("hermes_local")).toBeNull();
expect(() => requireServerAdapter("hermes_local")).toThrow(
"Unknown adapter type: hermes_local",
);
it("ships Hermes adapters as built-ins and still accepts external overrides", () => {
const builtInLocal = findServerAdapter("hermes_local");
const builtInGateway = findServerAdapter("hermes_gateway");
expect(builtInLocal).not.toBeNull();
expect(builtInLocal?.supportsLocalAgentJwt).toBe(true);
expect(builtInLocal?.supportsInstructionsBundle).toBe(true);
expect(builtInLocal?.requiresMaterializedRuntimeSkills).toBe(false);
expect(builtInLocal?.detectModel).toBeTypeOf("function");
expect(builtInLocal?.getConfigSchema).toBeTypeOf("function");
expect(builtInGateway).not.toBeNull();
expect(builtInGateway?.supportsLocalAgentJwt).toBe(false);
expect(builtInGateway?.supportsInstructionsBundle).toBe(false);
expect(builtInGateway?.requiresMaterializedRuntimeSkills).toBe(false);
expect(builtInGateway?.getConfigSchema).toBeTypeOf("function");
const hermesExternalAdapter: ServerAdapterModule = {
type: "hermes_local",
@@ -167,6 +180,10 @@ describe("server adapter registry", () => {
expect(requireServerAdapter("hermes_local")).toBe(hermesExternalAdapter);
expect(findActiveServerAdapter("hermes_local")?.supportsLocalAgentJwt).toBe(true);
unregisterServerAdapter("hermes_local");
expect(requireServerAdapter("hermes_local")).toBe(builtInLocal);
});
it("exposes capability flags from registered adapters", () => {
+41 -3
View File
@@ -187,9 +187,25 @@ describe("adapter routes", () => {
requiresMaterializedRuntimeSkills: true,
});
// Hermes is external-only on this branch and must not appear until the
// Adapter Manager loads @paperclipai/hermes-paperclip-adapter.
expect(res.body.find((a: any) => a.type === "hermes_local")).toBeUndefined();
const hermesLocal = res.body.find((a: any) => a.type === "hermes_local");
expect(hermesLocal).toBeDefined();
expect(hermesLocal.source).toBe("builtin");
expect(hermesLocal.capabilities).toMatchObject({
supportsInstructionsBundle: true,
supportsSkills: true,
supportsLocalAgentJwt: true,
requiresMaterializedRuntimeSkills: false,
});
const hermesGateway = res.body.find((a: any) => a.type === "hermes_gateway");
expect(hermesGateway).toBeDefined();
expect(hermesGateway.source).toBe("builtin");
expect(hermesGateway.capabilities).toMatchObject({
supportsInstructionsBundle: false,
supportsSkills: false,
supportsLocalAgentJwt: false,
requiresMaterializedRuntimeSkills: false,
});
});
it("GET /api/adapters derives supportsSkills from listSkills/syncSkills presence", async () => {
@@ -271,6 +287,28 @@ describe("adapter routes", () => {
expect(keys).not.toContain("bootstrapPromptTemplate");
});
it("serves built-in Hermes config schemas", async () => {
const app = createApp();
const local = await request(app).get("/api/adapters/hermes_local/config-schema");
expect(local.status, JSON.stringify(local.body)).toBe(200);
expect(local.body.fields).toEqual(
expect.arrayContaining([
expect.objectContaining({ key: "provider" }),
expect.objectContaining({ key: "timeoutSec" }),
]),
);
const gateway = await request(app).get("/api/adapters/hermes_gateway/config-schema");
expect(gateway.status, JSON.stringify(gateway.body)).toBe(200);
expect(gateway.body.fields).toEqual(
expect.arrayContaining([
expect.objectContaining({ key: "apiBaseUrl", required: true }),
expect.objectContaining({ key: "apiKey", required: true }),
]),
);
});
it("GET /api/adapters includes ACPX model availability", async () => {
const app = createApp();
@@ -9,6 +9,8 @@ export const BUILTIN_ADAPTER_TYPES = new Set([
"cursor",
"gemini_local",
"grok_local",
"hermes_gateway",
"hermes_local",
"openclaw_gateway",
"opencode_local",
"pi_local",
+2 -2
View File
@@ -8,11 +8,11 @@ Use when:
- 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.
- Paperclip should start the Hermes CLI directly on the same host. Use the built-in hermes_local adapter 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_local: Paperclip starts Hermes on the Paperclip host through the built-in local adapter.
- 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_BRIDGE_API_KEY. Create that Paperclip key with scope.kind = "task_bridge" and an approved parent/project boundary; do not expose a normal claimed agent key to Hermes-facing chat/webhook surfaces. Do not use agentDefaultsPayload.apiBaseUrl for Paperclip API calls.
+8
View File
@@ -92,6 +92,8 @@ import {
agentConfigurationDoc as grokAgentConfigurationDoc,
models as grokModels,
} from "@paperclipai/adapter-grok-local";
import { createServerAdapter as createHermesGatewayServerAdapter } from "@paperclipai/adapter-hermes-gateway";
import { createServerAdapter as createHermesLocalServerAdapter } from "@paperclipai/hermes-paperclip-adapter";
import {
execute as openCodeExecute,
listOpenCodeSkills,
@@ -342,6 +344,10 @@ const grokLocalAdapter: ServerAdapterModule = {
agentConfigurationDoc: grokAgentConfigurationDoc,
};
const hermesGatewayAdapter = createHermesGatewayServerAdapter();
const hermesLocalAdapter = createHermesLocalServerAdapter();
const openclawGatewayAdapter: ServerAdapterModule = {
type: "openclaw_gateway",
execute: openclawGatewayExecute,
@@ -414,6 +420,8 @@ function registerBuiltInAdapters() {
cursorLocalAdapter,
geminiLocalAdapter,
grokLocalAdapter,
hermesGatewayAdapter,
hermesLocalAdapter,
openclawGatewayAdapter,
processAdapter,
httpAdapter,
+2
View File
@@ -9,6 +9,8 @@
{ "path": "./packages/adapters/claude-local" },
{ "path": "./packages/adapters/codex-local" },
{ "path": "./packages/adapters/cursor-local" },
{ "path": "./packages/adapters/hermes-gateway" },
{ "path": "./packages/adapters/hermes-local" },
{ "path": "./packages/adapters/droid-local" },
{ "path": "./packages/adapters/openclaw-gateway" },
{ "path": "./packages/adapters/opencode-local" },
+2
View File
@@ -41,11 +41,13 @@
"@paperclipai/adapter-cursor-local": "workspace:*",
"@paperclipai/adapter-gemini-local": "workspace:*",
"@paperclipai/adapter-grok-local": "workspace:*",
"@paperclipai/adapter-hermes-gateway": "workspace:*",
"@paperclipai/adapter-openclaw-gateway": "workspace:*",
"@paperclipai/adapter-opencode-local": "workspace:*",
"@paperclipai/adapter-pi-local": "workspace:*",
"@paperclipai/adapter-utils": "workspace:*",
"@paperclipai/shared": "workspace:*",
"@paperclipai/hermes-paperclip-adapter": "workspace:*",
"@radix-ui/react-slot": "^1.2.4",
"@tailwindcss/typography": "^0.5.19",
"@tanstack/react-query": "^5.90.21",
@@ -82,6 +82,17 @@ const adapterDisplayMap: Record<string, AdapterDisplayInfo> = {
description: "Local Grok Build agent",
icon: Bot,
},
hermes_gateway: {
label: "Hermes",
description: "Remote Hermes API server",
icon: Bot,
hideFromVisualSelection: true,
},
hermes_local: {
label: "Hermes",
description: "Local Hermes agent",
icon: Bot,
},
opencode_local: {
label: "OpenCode",
description: "Local multi-provider agent",
+11
View File
@@ -0,0 +1,11 @@
import type { UIAdapterModule } from "../types";
import { parseStdoutLine as parseHermesGatewayStdoutLine } from "@paperclipai/adapter-hermes-gateway/ui";
import { SchemaConfigFields, buildSchemaAdapterConfig } from "../schema-config-fields";
export const hermesGatewayUIAdapter: UIAdapterModule = {
type: "hermes_gateway",
label: "Hermes Gateway",
parseStdoutLine: parseHermesGatewayStdoutLine,
ConfigFields: SchemaConfigFields,
buildAdapterConfig: buildSchemaAdapterConfig,
};
+11
View File
@@ -0,0 +1,11 @@
import type { UIAdapterModule } from "../types";
import { parseHermesStdoutLine, buildHermesConfig } from "@paperclipai/hermes-paperclip-adapter/ui";
import { SchemaConfigFields } from "../schema-config-fields";
export const hermesLocalUIAdapter: UIAdapterModule = {
type: "hermes_local",
label: "Hermes",
parseStdoutLine: parseHermesStdoutLine,
ConfigFields: SchemaConfigFields,
buildAdapterConfig: buildHermesConfig,
};
+4
View File
@@ -6,6 +6,8 @@ import { cursorCloudUIAdapter } from "./cursor-cloud";
import { cursorLocalUIAdapter } from "./cursor";
import { geminiLocalUIAdapter } from "./gemini-local";
import { grokLocalUIAdapter } from "./grok-local";
import { hermesGatewayUIAdapter } from "./hermes-gateway";
import { hermesLocalUIAdapter } from "./hermes-local";
import { openCodeLocalUIAdapter } from "./opencode-local";
import { piLocalUIAdapter } from "./pi-local";
import { openClawGatewayUIAdapter } from "./openclaw-gateway";
@@ -57,6 +59,8 @@ function registerBuiltInUIAdapters() {
cursorCloudUIAdapter,
geminiLocalUIAdapter,
grokLocalUIAdapter,
hermesGatewayUIAdapter,
hermesLocalUIAdapter,
openCodeLocalUIAdapter,
piLocalUIAdapter,
cursorLocalUIAdapter,