Add invite onboarding network host suggestions
This commit is contained in:
@@ -41,6 +41,9 @@ describe("buildInviteOnboardingTextDocument", () => {
|
||||
expect(text).toContain("/api/invites/token-123/accept");
|
||||
expect(text).toContain("/api/join-requests/{requestId}/claim-api-key");
|
||||
expect(text).toContain("/api/invites/token-123/onboarding.txt");
|
||||
expect(text).toContain("Suggested Paperclip base URLs to try");
|
||||
expect(text).toContain("http://localhost:3100");
|
||||
expect(text).toContain("host.docker.internal");
|
||||
});
|
||||
|
||||
it("includes loopback diagnostics for authenticated/private onboarding", () => {
|
||||
@@ -69,6 +72,7 @@ describe("buildInviteOnboardingTextDocument", () => {
|
||||
|
||||
expect(text).toContain("Connectivity diagnostics");
|
||||
expect(text).toContain("loopback hostname");
|
||||
expect(text).toContain("If none are reachable");
|
||||
});
|
||||
|
||||
it("includes inviter message in the onboarding text when provided", () => {
|
||||
|
||||
@@ -377,6 +377,46 @@ function buildOnboardingDiscoveryDiagnostics(input: {
|
||||
return diagnostics;
|
||||
}
|
||||
|
||||
function buildOnboardingConnectionCandidates(input: {
|
||||
apiBaseUrl: string;
|
||||
bindHost: string;
|
||||
allowedHostnames: string[];
|
||||
}): string[] {
|
||||
let base: URL | null = null;
|
||||
try {
|
||||
if (input.apiBaseUrl) {
|
||||
base = new URL(input.apiBaseUrl);
|
||||
}
|
||||
} catch {
|
||||
base = null;
|
||||
}
|
||||
|
||||
const protocol = base?.protocol ?? "http:";
|
||||
const port = base?.port ? `:${base.port}` : "";
|
||||
const candidates = new Set<string>();
|
||||
|
||||
if (base) {
|
||||
candidates.add(base.origin);
|
||||
}
|
||||
|
||||
const bindHost = normalizeHostname(input.bindHost);
|
||||
if (bindHost && !isLoopbackHost(bindHost)) {
|
||||
candidates.add(`${protocol}//${bindHost}${port}`);
|
||||
}
|
||||
|
||||
for (const rawHost of input.allowedHostnames) {
|
||||
const host = normalizeHostname(rawHost);
|
||||
if (!host) continue;
|
||||
candidates.add(`${protocol}//${host}${port}`);
|
||||
}
|
||||
|
||||
if (base && isLoopbackHost(base.hostname)) {
|
||||
candidates.add(`${protocol}//host.docker.internal${port}`);
|
||||
}
|
||||
|
||||
return Array.from(candidates);
|
||||
}
|
||||
|
||||
function buildInviteOnboardingManifest(
|
||||
req: Request,
|
||||
token: string,
|
||||
@@ -402,6 +442,11 @@ function buildInviteOnboardingManifest(
|
||||
bindHost: opts.bindHost,
|
||||
allowedHostnames: opts.allowedHostnames,
|
||||
});
|
||||
const connectionCandidates = buildOnboardingConnectionCandidates({
|
||||
apiBaseUrl: baseUrl,
|
||||
bindHost: opts.bindHost,
|
||||
allowedHostnames: opts.allowedHostnames,
|
||||
});
|
||||
|
||||
return {
|
||||
invite: toInviteSummaryResponse(req, token, invite),
|
||||
@@ -435,6 +480,7 @@ function buildInviteOnboardingManifest(
|
||||
deploymentExposure: opts.deploymentExposure,
|
||||
bindHost: opts.bindHost,
|
||||
allowedHostnames: opts.allowedHostnames,
|
||||
connectionCandidates,
|
||||
diagnostics: discoveryDiagnostics,
|
||||
guidance:
|
||||
opts.deploymentMode === "authenticated" && opts.deploymentExposure === "private"
|
||||
@@ -474,7 +520,7 @@ export function buildInviteOnboardingTextDocument(
|
||||
claimEndpointTemplate: { method: string; path: string };
|
||||
textInstructions: { path: string; url: string };
|
||||
skill: { path: string; url: string; installPath: string };
|
||||
connectivity: { diagnostics?: JoinDiagnostic[]; guidance?: string };
|
||||
connectivity: { diagnostics?: JoinDiagnostic[]; guidance?: string; connectionCandidates?: string[] };
|
||||
};
|
||||
const diagnostics = Array.isArray(onboarding.connectivity?.diagnostics)
|
||||
? onboarding.connectivity.diagnostics
|
||||
@@ -546,6 +592,27 @@ export function buildInviteOnboardingTextDocument(
|
||||
onboarding.connectivity?.guidance ?? "Ensure Paperclip is reachable from your OpenClaw runtime.",
|
||||
);
|
||||
|
||||
const connectionCandidates = Array.isArray(onboarding.connectivity?.connectionCandidates)
|
||||
? onboarding.connectivity.connectionCandidates.filter((entry): entry is string => Boolean(entry))
|
||||
: [];
|
||||
|
||||
if (connectionCandidates.length > 0) {
|
||||
lines.push("", "## Suggested Paperclip base URLs to try");
|
||||
for (const candidate of connectionCandidates) {
|
||||
lines.push(`- ${candidate}`);
|
||||
}
|
||||
lines.push(
|
||||
"",
|
||||
"Test each candidate with:",
|
||||
"- GET <candidate>/api/health",
|
||||
"",
|
||||
"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:",
|
||||
"- pnpm paperclipai allowed-hostname <host>",
|
||||
"- then restart Paperclip and retry onboarding.",
|
||||
);
|
||||
}
|
||||
|
||||
if (diagnostics.length > 0) {
|
||||
lines.push("", "## Connectivity diagnostics");
|
||||
for (const diag of diagnostics) {
|
||||
|
||||
Reference in New Issue
Block a user