Add OpenClaw Paperclip API URL override for onboarding

This commit is contained in:
Dotta
2026-03-06 08:39:29 -06:00
parent 854e818b74
commit b213eb695b
6 changed files with 101 additions and 1 deletions

View File

@@ -45,6 +45,8 @@ describe("buildInviteOnboardingTextDocument", () => {
expect(text).toContain("Suggested Paperclip base URLs to try");
expect(text).toContain("http://localhost:3100");
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", () => {

View File

@@ -198,6 +198,31 @@ describe("openclaw adapter execute", () => {
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 () => {
const fetchMock = vi.fn().mockResolvedValue(
sseResponse([

View File

@@ -300,6 +300,44 @@ function normalizeAgentDefaultsForJoin(input: {
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(
...buildJoinConnectivityDiagnostics({
deploymentMode: input.deploymentMode,
@@ -486,7 +524,7 @@ function buildInviteOnboardingManifest(
adapterType: "Use 'openclaw' for OpenClaw streaming agents",
capabilities: "Optional capability summary",
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: {
method: "POST",
@@ -593,6 +631,7 @@ export function buildInviteOnboardingTextDocument(
' "capabilities": "Optional summary",',
' "agentDefaultsPayload": {',
' "url": "https://your-openclaw-agent.example/v1/responses",',
' "paperclipApiUrl": "https://paperclip-hostname-your-agent-can-reach:3100",',
' "streamTransport": "sse",',
' "method": "POST",',
' "headers": { "x-openclaw-auth": "replace-me" },',
@@ -655,6 +694,7 @@ export function buildInviteOnboardingTextDocument(
"",
"Test each candidate with:",
"- 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.",
"For authenticated/private mode, they may need:",