Add OpenClaw Paperclip API URL override for onboarding
This commit is contained in:
@@ -22,6 +22,7 @@ Core fields:
|
|||||||
- headers (object, optional): extra HTTP headers for requests
|
- headers (object, optional): extra HTTP headers for requests
|
||||||
- webhookAuthHeader (string, optional): Authorization header value if your endpoint requires auth
|
- webhookAuthHeader (string, optional): Authorization header value if your endpoint requires auth
|
||||||
- payloadTemplate (object, optional): additional JSON payload fields merged into each wake payload
|
- payloadTemplate (object, optional): additional JSON payload fields merged into each wake payload
|
||||||
|
- paperclipApiUrl (string, optional): absolute http(s) Paperclip base URL to advertise to OpenClaw as \`PAPERCLIP_API_URL\`
|
||||||
|
|
||||||
Session routing fields:
|
Session routing fields:
|
||||||
- sessionKeyStrategy (string, optional): \`fixed\` (default), \`issue\`, or \`run\`
|
- sessionKeyStrategy (string, optional): \`fixed\` (default), \`issue\`, or \`run\`
|
||||||
|
|||||||
@@ -8,6 +8,18 @@ function nonEmpty(value: unknown): string | null {
|
|||||||
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolvePaperclipApiUrlOverride(value: unknown): string | null {
|
||||||
|
const raw = nonEmpty(value);
|
||||||
|
if (!raw) return null;
|
||||||
|
try {
|
||||||
|
const parsed = new URL(raw);
|
||||||
|
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return null;
|
||||||
|
return parsed.toString();
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeSessionKeyStrategy(value: unknown): SessionKeyStrategy {
|
function normalizeSessionKeyStrategy(value: unknown): SessionKeyStrategy {
|
||||||
const normalized = asString(value, "fixed").trim().toLowerCase();
|
const normalized = asString(value, "fixed").trim().toLowerCase();
|
||||||
if (normalized === "issue" || normalized === "run") return normalized;
|
if (normalized === "issue" || normalized === "run") return normalized;
|
||||||
@@ -516,10 +528,14 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||||||
});
|
});
|
||||||
|
|
||||||
const templateText = nonEmpty(payloadTemplate.text);
|
const templateText = nonEmpty(payloadTemplate.text);
|
||||||
|
const paperclipApiUrlOverride = resolvePaperclipApiUrlOverride(config.paperclipApiUrl);
|
||||||
const paperclipEnv: Record<string, string> = {
|
const paperclipEnv: Record<string, string> = {
|
||||||
...buildPaperclipEnv(agent),
|
...buildPaperclipEnv(agent),
|
||||||
PAPERCLIP_RUN_ID: runId,
|
PAPERCLIP_RUN_ID: runId,
|
||||||
};
|
};
|
||||||
|
if (paperclipApiUrlOverride) {
|
||||||
|
paperclipEnv.PAPERCLIP_API_URL = paperclipApiUrlOverride;
|
||||||
|
}
|
||||||
if (wakePayload.taskId) paperclipEnv.PAPERCLIP_TASK_ID = wakePayload.taskId;
|
if (wakePayload.taskId) paperclipEnv.PAPERCLIP_TASK_ID = wakePayload.taskId;
|
||||||
if (wakePayload.wakeReason) paperclipEnv.PAPERCLIP_WAKE_REASON = wakePayload.wakeReason;
|
if (wakePayload.wakeReason) paperclipEnv.PAPERCLIP_WAKE_REASON = wakePayload.wakeReason;
|
||||||
if (wakePayload.wakeCommentId) paperclipEnv.PAPERCLIP_WAKE_COMMENT_ID = wakePayload.wakeCommentId;
|
if (wakePayload.wakeCommentId) paperclipEnv.PAPERCLIP_WAKE_COMMENT_ID = wakePayload.wakeCommentId;
|
||||||
|
|||||||
@@ -45,6 +45,8 @@ describe("buildInviteOnboardingTextDocument", () => {
|
|||||||
expect(text).toContain("Suggested Paperclip base URLs to try");
|
expect(text).toContain("Suggested Paperclip base URLs to try");
|
||||||
expect(text).toContain("http://localhost:3100");
|
expect(text).toContain("http://localhost:3100");
|
||||||
expect(text).toContain("host.docker.internal");
|
expect(text).toContain("host.docker.internal");
|
||||||
|
expect(text).toContain("paperclipApiUrl");
|
||||||
|
expect(text).toContain("set the first reachable candidate as agentDefaultsPayload.paperclipApiUrl");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("includes loopback diagnostics for authenticated/private onboarding", () => {
|
it("includes loopback diagnostics for authenticated/private onboarding", () => {
|
||||||
|
|||||||
@@ -198,6 +198,31 @@ describe("openclaw adapter execute", () => {
|
|||||||
expect(text).toContain("PAPERCLIP_LINKED_ISSUE_IDS=issue-123");
|
expect(text).toContain("PAPERCLIP_LINKED_ISSUE_IDS=issue-123");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("uses paperclipApiUrl override when provided", async () => {
|
||||||
|
const fetchMock = vi.fn().mockResolvedValue(
|
||||||
|
sseResponse([
|
||||||
|
"event: response.completed\n",
|
||||||
|
'data: {"type":"response.completed","status":"completed"}\n\n',
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
vi.stubGlobal("fetch", fetchMock);
|
||||||
|
|
||||||
|
const result = await execute(
|
||||||
|
buildContext({
|
||||||
|
url: "https://agent.example/sse",
|
||||||
|
method: "POST",
|
||||||
|
paperclipApiUrl: "http://dotta-macbook-pro:3100",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.exitCode).toBe(0);
|
||||||
|
const body = JSON.parse(String(fetchMock.mock.calls[0]?.[1]?.body ?? "{}")) as Record<string, unknown>;
|
||||||
|
const paperclip = body.paperclip as Record<string, unknown>;
|
||||||
|
const env = paperclip.env as Record<string, unknown>;
|
||||||
|
expect(env.PAPERCLIP_API_URL).toBe("http://dotta-macbook-pro:3100/");
|
||||||
|
expect(String(body.text ?? "")).toContain("PAPERCLIP_API_URL=http://dotta-macbook-pro:3100/");
|
||||||
|
});
|
||||||
|
|
||||||
it("derives issue session keys when configured", async () => {
|
it("derives issue session keys when configured", async () => {
|
||||||
const fetchMock = vi.fn().mockResolvedValue(
|
const fetchMock = vi.fn().mockResolvedValue(
|
||||||
sseResponse([
|
sseResponse([
|
||||||
|
|||||||
@@ -300,6 +300,44 @@ function normalizeAgentDefaultsForJoin(input: {
|
|||||||
normalized.payloadTemplate = defaults.payloadTemplate;
|
normalized.payloadTemplate = defaults.payloadTemplate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const rawPaperclipApiUrl = typeof defaults.paperclipApiUrl === "string"
|
||||||
|
? defaults.paperclipApiUrl.trim()
|
||||||
|
: "";
|
||||||
|
if (rawPaperclipApiUrl) {
|
||||||
|
try {
|
||||||
|
const parsedPaperclipApiUrl = new URL(rawPaperclipApiUrl);
|
||||||
|
if (parsedPaperclipApiUrl.protocol !== "http:" && parsedPaperclipApiUrl.protocol !== "https:") {
|
||||||
|
diagnostics.push({
|
||||||
|
code: "openclaw_paperclip_api_url_protocol",
|
||||||
|
level: "warn",
|
||||||
|
message: `paperclipApiUrl must use http:// or https:// (got ${parsedPaperclipApiUrl.protocol}).`,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
normalized.paperclipApiUrl = parsedPaperclipApiUrl.toString();
|
||||||
|
diagnostics.push({
|
||||||
|
code: "openclaw_paperclip_api_url_configured",
|
||||||
|
level: "info",
|
||||||
|
message: `paperclipApiUrl set to ${parsedPaperclipApiUrl.toString()}`,
|
||||||
|
});
|
||||||
|
if (isLoopbackHost(parsedPaperclipApiUrl.hostname)) {
|
||||||
|
diagnostics.push({
|
||||||
|
code: "openclaw_paperclip_api_url_loopback",
|
||||||
|
level: "warn",
|
||||||
|
message:
|
||||||
|
"paperclipApiUrl uses loopback hostname. Remote OpenClaw workers cannot reach localhost on the Paperclip host.",
|
||||||
|
hint: "Use a reachable hostname/IP and keep it in allowed hostnames for authenticated/private deployments.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
diagnostics.push({
|
||||||
|
code: "openclaw_paperclip_api_url_invalid",
|
||||||
|
level: "warn",
|
||||||
|
message: `Invalid paperclipApiUrl: ${rawPaperclipApiUrl}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
diagnostics.push(
|
diagnostics.push(
|
||||||
...buildJoinConnectivityDiagnostics({
|
...buildJoinConnectivityDiagnostics({
|
||||||
deploymentMode: input.deploymentMode,
|
deploymentMode: input.deploymentMode,
|
||||||
@@ -486,7 +524,7 @@ function buildInviteOnboardingManifest(
|
|||||||
adapterType: "Use 'openclaw' for OpenClaw streaming agents",
|
adapterType: "Use 'openclaw' for OpenClaw streaming agents",
|
||||||
capabilities: "Optional capability summary",
|
capabilities: "Optional capability summary",
|
||||||
agentDefaultsPayload:
|
agentDefaultsPayload:
|
||||||
"Optional adapter config such as url/method/headers/webhookAuthHeader for OpenClaw SSE endpoint",
|
"Optional adapter config such as url/method/headers/webhookAuthHeader and paperclipApiUrl for OpenClaw SSE endpoint",
|
||||||
},
|
},
|
||||||
registrationEndpoint: {
|
registrationEndpoint: {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -593,6 +631,7 @@ export function buildInviteOnboardingTextDocument(
|
|||||||
' "capabilities": "Optional summary",',
|
' "capabilities": "Optional summary",',
|
||||||
' "agentDefaultsPayload": {',
|
' "agentDefaultsPayload": {',
|
||||||
' "url": "https://your-openclaw-agent.example/v1/responses",',
|
' "url": "https://your-openclaw-agent.example/v1/responses",',
|
||||||
|
' "paperclipApiUrl": "https://paperclip-hostname-your-agent-can-reach:3100",',
|
||||||
' "streamTransport": "sse",',
|
' "streamTransport": "sse",',
|
||||||
' "method": "POST",',
|
' "method": "POST",',
|
||||||
' "headers": { "x-openclaw-auth": "replace-me" },',
|
' "headers": { "x-openclaw-auth": "replace-me" },',
|
||||||
@@ -655,6 +694,7 @@ export function buildInviteOnboardingTextDocument(
|
|||||||
"",
|
"",
|
||||||
"Test each candidate with:",
|
"Test each candidate with:",
|
||||||
"- GET <candidate>/api/health",
|
"- GET <candidate>/api/health",
|
||||||
|
"- set the first reachable candidate as agentDefaultsPayload.paperclipApiUrl when submitting your join request",
|
||||||
"",
|
"",
|
||||||
"If none are reachable: ask your human operator for a reachable hostname/address and help them update network configuration.",
|
"If none are reachable: ask your human operator for a reachable hostname/address and help them update network configuration.",
|
||||||
"For authenticated/private mode, they may need:",
|
"For authenticated/private mode, they may need:",
|
||||||
|
|||||||
@@ -48,6 +48,22 @@ export function OpenClawConfigFields({
|
|||||||
</Field>
|
</Field>
|
||||||
{!isCreate && (
|
{!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="Transport">
|
<Field label="Transport">
|
||||||
<select
|
<select
|
||||||
value={transport}
|
value={transport}
|
||||||
|
|||||||
Reference in New Issue
Block a user