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:
221
ui/src/adapters/openclaw-gateway/config-fields.tsx
Normal file
221
ui/src/adapters/openclaw-gateway/config-fields.tsx
Normal 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>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
12
ui/src/adapters/openclaw-gateway/index.ts
Normal file
12
ui/src/adapters/openclaw-gateway/index.ts
Normal 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,
|
||||
};
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user