feat: add openclaw_gateway adapter

New adapter type for invoking OpenClaw agents via the gateway protocol.
Registers in server, CLI, and UI adapter registries. Adds onboarding
wizard support with gateway URL field and e2e smoke test script.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dotta
2026-03-07 08:59:29 -06:00
parent fa8499719a
commit a498c268c5
34 changed files with 4290 additions and 19 deletions

View File

@@ -0,0 +1,221 @@
import { useState } from "react";
import { Eye, EyeOff } from "lucide-react";
import type { AdapterConfigFieldsProps } from "../types";
import {
Field,
DraftInput,
help,
} from "../../components/agent-config-primitives";
const inputClass =
"w-full rounded-md border border-border px-2.5 py-1.5 bg-transparent outline-none text-sm font-mono placeholder:text-muted-foreground/40";
function SecretField({
label,
value,
onCommit,
placeholder,
}: {
label: string;
value: string;
onCommit: (v: string) => void;
placeholder?: string;
}) {
const [visible, setVisible] = useState(false);
return (
<Field label={label}>
<div className="relative">
<button
type="button"
onClick={() => setVisible((v) => !v)}
className="absolute left-2 top-1/2 -translate-y-1/2 text-muted-foreground/50 hover:text-muted-foreground transition-colors"
>
{visible ? <Eye className="h-3.5 w-3.5" /> : <EyeOff className="h-3.5 w-3.5" />}
</button>
<DraftInput
value={value}
onCommit={onCommit}
immediate
type={visible ? "text" : "password"}
className={inputClass + " pl-8"}
placeholder={placeholder}
/>
</div>
</Field>
);
}
function parseScopes(value: unknown): string {
if (Array.isArray(value)) {
return value.filter((entry): entry is string => typeof entry === "string").join(", ");
}
return typeof value === "string" ? value : "";
}
export function OpenClawGatewayConfigFields({
isCreate,
values,
set,
config,
eff,
mark,
}: AdapterConfigFieldsProps) {
const configuredHeaders =
config.headers && typeof config.headers === "object" && !Array.isArray(config.headers)
? (config.headers as Record<string, unknown>)
: {};
const effectiveHeaders =
(eff("adapterConfig", "headers", configuredHeaders) as Record<string, unknown>) ?? {};
const effectiveGatewayToken = typeof effectiveHeaders["x-openclaw-token"] === "string"
? String(effectiveHeaders["x-openclaw-token"])
: typeof effectiveHeaders["x-openclaw-auth"] === "string"
? String(effectiveHeaders["x-openclaw-auth"])
: "";
const commitGatewayToken = (rawValue: string) => {
const nextValue = rawValue.trim();
const nextHeaders: Record<string, unknown> = { ...effectiveHeaders };
if (nextValue) {
nextHeaders["x-openclaw-token"] = nextValue;
delete nextHeaders["x-openclaw-auth"];
} else {
delete nextHeaders["x-openclaw-token"];
delete nextHeaders["x-openclaw-auth"];
}
mark("adapterConfig", "headers", Object.keys(nextHeaders).length > 0 ? nextHeaders : undefined);
};
const sessionStrategy = eff(
"adapterConfig",
"sessionKeyStrategy",
String(config.sessionKeyStrategy ?? "fixed"),
);
return (
<>
<Field label="Gateway URL" hint={help.webhookUrl}>
<DraftInput
value={
isCreate
? values!.url
: eff("adapterConfig", "url", String(config.url ?? ""))
}
onCommit={(v) =>
isCreate
? set!({ url: v })
: mark("adapterConfig", "url", v || undefined)
}
immediate
className={inputClass}
placeholder="ws://127.0.0.1:18789"
/>
</Field>
{!isCreate && (
<>
<Field label="Paperclip API URL override">
<DraftInput
value={
eff(
"adapterConfig",
"paperclipApiUrl",
String(config.paperclipApiUrl ?? ""),
)
}
onCommit={(v) => mark("adapterConfig", "paperclipApiUrl", v || undefined)}
immediate
className={inputClass}
placeholder="https://paperclip.example"
/>
</Field>
<Field label="Session strategy">
<select
value={sessionStrategy}
onChange={(e) => mark("adapterConfig", "sessionKeyStrategy", e.target.value)}
className={inputClass}
>
<option value="fixed">Fixed</option>
<option value="issue">Per issue</option>
<option value="run">Per run</option>
</select>
</Field>
{sessionStrategy === "fixed" && (
<Field label="Session key">
<DraftInput
value={eff("adapterConfig", "sessionKey", String(config.sessionKey ?? "paperclip"))}
onCommit={(v) => mark("adapterConfig", "sessionKey", v || undefined)}
immediate
className={inputClass}
placeholder="paperclip"
/>
</Field>
)}
<SecretField
label="Gateway auth token (x-openclaw-token)"
value={effectiveGatewayToken}
onCommit={commitGatewayToken}
placeholder="OpenClaw gateway token"
/>
<Field label="Role">
<DraftInput
value={eff("adapterConfig", "role", String(config.role ?? "operator"))}
onCommit={(v) => mark("adapterConfig", "role", v || undefined)}
immediate
className={inputClass}
placeholder="operator"
/>
</Field>
<Field label="Scopes (comma-separated)">
<DraftInput
value={eff("adapterConfig", "scopes", parseScopes(config.scopes ?? ["operator.admin"]))}
onCommit={(v) => {
const parsed = v
.split(",")
.map((entry) => entry.trim())
.filter(Boolean);
mark("adapterConfig", "scopes", parsed.length > 0 ? parsed : undefined);
}}
immediate
className={inputClass}
placeholder="operator.admin"
/>
</Field>
<Field label="Wait timeout (ms)">
<DraftInput
value={eff("adapterConfig", "waitTimeoutMs", String(config.waitTimeoutMs ?? "120000"))}
onCommit={(v) => {
const parsed = Number.parseInt(v.trim(), 10);
mark(
"adapterConfig",
"waitTimeoutMs",
Number.isFinite(parsed) && parsed > 0 ? parsed : undefined,
);
}}
immediate
className={inputClass}
placeholder="120000"
/>
</Field>
<Field label="Disable device auth">
<select
value={String(eff("adapterConfig", "disableDeviceAuth", Boolean(config.disableDeviceAuth ?? false)))}
onChange={(e) => mark("adapterConfig", "disableDeviceAuth", e.target.value === "true")}
className={inputClass}
>
<option value="false">No (recommended)</option>
<option value="true">Yes</option>
</select>
</Field>
</>
)}
</>
);
}

View File

@@ -0,0 +1,12 @@
import type { UIAdapterModule } from "../types";
import { parseOpenClawGatewayStdoutLine } from "@paperclipai/adapter-openclaw-gateway/ui";
import { buildOpenClawGatewayConfig } from "@paperclipai/adapter-openclaw-gateway/ui";
import { OpenClawGatewayConfigFields } from "./config-fields";
export const openClawGatewayUIAdapter: UIAdapterModule = {
type: "openclaw_gateway",
label: "OpenClaw Gateway",
parseStdoutLine: parseOpenClawGatewayStdoutLine,
ConfigFields: OpenClawGatewayConfigFields,
buildAdapterConfig: buildOpenClawGatewayConfig,
};

View File

@@ -4,11 +4,21 @@ import { codexLocalUIAdapter } from "./codex-local";
import { cursorLocalUIAdapter } from "./cursor";
import { openCodeLocalUIAdapter } from "./opencode-local";
import { openClawUIAdapter } from "./openclaw";
import { openClawGatewayUIAdapter } from "./openclaw-gateway";
import { processUIAdapter } from "./process";
import { httpUIAdapter } from "./http";
const adaptersByType = new Map<string, UIAdapterModule>(
[claudeLocalUIAdapter, codexLocalUIAdapter, openCodeLocalUIAdapter, cursorLocalUIAdapter, openClawUIAdapter, processUIAdapter, httpUIAdapter].map((a) => [a.type, a]),
[
claudeLocalUIAdapter,
codexLocalUIAdapter,
openCodeLocalUIAdapter,
cursorLocalUIAdapter,
openClawUIAdapter,
openClawGatewayUIAdapter,
processUIAdapter,
httpUIAdapter,
].map((a) => [a.type, a]),
);
export function getUIAdapter(type: string): UIAdapterModule {