Add invite onboarding network host suggestions
This commit is contained in:
@@ -246,7 +246,7 @@ Agent-oriented invite onboarding now exposes machine-readable API docs:
|
|||||||
|
|
||||||
- `GET /api/invites/:token` returns invite summary plus onboarding and skills index links.
|
- `GET /api/invites/:token` returns invite summary plus onboarding and skills index links.
|
||||||
- `GET /api/invites/:token/onboarding` returns onboarding manifest details (registration endpoint, claim endpoint template, skill install hints).
|
- `GET /api/invites/:token/onboarding` returns onboarding manifest details (registration endpoint, claim endpoint template, skill install hints).
|
||||||
- `GET /api/invites/:token/onboarding.txt` returns a plain-text onboarding doc intended for both human operators and agents (llm.txt-style handoff).
|
- `GET /api/invites/:token/onboarding.txt` returns a plain-text onboarding doc intended for both human operators and agents (llm.txt-style handoff), including optional inviter message and suggested network host candidates.
|
||||||
- `GET /api/skills/index` lists available skill documents.
|
- `GET /api/skills/index` lists available skill documents.
|
||||||
- `GET /api/skills/paperclip` returns the Paperclip heartbeat skill markdown.
|
- `GET /api/skills/paperclip` returns the Paperclip heartbeat skill markdown.
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,9 @@ describe("buildInviteOnboardingTextDocument", () => {
|
|||||||
expect(text).toContain("/api/invites/token-123/accept");
|
expect(text).toContain("/api/invites/token-123/accept");
|
||||||
expect(text).toContain("/api/join-requests/{requestId}/claim-api-key");
|
expect(text).toContain("/api/join-requests/{requestId}/claim-api-key");
|
||||||
expect(text).toContain("/api/invites/token-123/onboarding.txt");
|
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", () => {
|
it("includes loopback diagnostics for authenticated/private onboarding", () => {
|
||||||
@@ -69,6 +72,7 @@ describe("buildInviteOnboardingTextDocument", () => {
|
|||||||
|
|
||||||
expect(text).toContain("Connectivity diagnostics");
|
expect(text).toContain("Connectivity diagnostics");
|
||||||
expect(text).toContain("loopback hostname");
|
expect(text).toContain("loopback hostname");
|
||||||
|
expect(text).toContain("If none are reachable");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("includes inviter message in the onboarding text when provided", () => {
|
it("includes inviter message in the onboarding text when provided", () => {
|
||||||
|
|||||||
@@ -377,6 +377,46 @@ function buildOnboardingDiscoveryDiagnostics(input: {
|
|||||||
return diagnostics;
|
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(
|
function buildInviteOnboardingManifest(
|
||||||
req: Request,
|
req: Request,
|
||||||
token: string,
|
token: string,
|
||||||
@@ -402,6 +442,11 @@ function buildInviteOnboardingManifest(
|
|||||||
bindHost: opts.bindHost,
|
bindHost: opts.bindHost,
|
||||||
allowedHostnames: opts.allowedHostnames,
|
allowedHostnames: opts.allowedHostnames,
|
||||||
});
|
});
|
||||||
|
const connectionCandidates = buildOnboardingConnectionCandidates({
|
||||||
|
apiBaseUrl: baseUrl,
|
||||||
|
bindHost: opts.bindHost,
|
||||||
|
allowedHostnames: opts.allowedHostnames,
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
invite: toInviteSummaryResponse(req, token, invite),
|
invite: toInviteSummaryResponse(req, token, invite),
|
||||||
@@ -435,6 +480,7 @@ function buildInviteOnboardingManifest(
|
|||||||
deploymentExposure: opts.deploymentExposure,
|
deploymentExposure: opts.deploymentExposure,
|
||||||
bindHost: opts.bindHost,
|
bindHost: opts.bindHost,
|
||||||
allowedHostnames: opts.allowedHostnames,
|
allowedHostnames: opts.allowedHostnames,
|
||||||
|
connectionCandidates,
|
||||||
diagnostics: discoveryDiagnostics,
|
diagnostics: discoveryDiagnostics,
|
||||||
guidance:
|
guidance:
|
||||||
opts.deploymentMode === "authenticated" && opts.deploymentExposure === "private"
|
opts.deploymentMode === "authenticated" && opts.deploymentExposure === "private"
|
||||||
@@ -474,7 +520,7 @@ export function buildInviteOnboardingTextDocument(
|
|||||||
claimEndpointTemplate: { method: string; path: string };
|
claimEndpointTemplate: { method: string; path: string };
|
||||||
textInstructions: { path: string; url: string };
|
textInstructions: { path: string; url: string };
|
||||||
skill: { path: string; url: string; installPath: 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)
|
const diagnostics = Array.isArray(onboarding.connectivity?.diagnostics)
|
||||||
? onboarding.connectivity.diagnostics
|
? onboarding.connectivity.diagnostics
|
||||||
@@ -546,6 +592,27 @@ export function buildInviteOnboardingTextDocument(
|
|||||||
onboarding.connectivity?.guidance ?? "Ensure Paperclip is reachable from your OpenClaw runtime.",
|
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) {
|
if (diagnostics.length > 0) {
|
||||||
lines.push("", "## Connectivity diagnostics");
|
lines.push("", "## Connectivity diagnostics");
|
||||||
for (const diag of diagnostics) {
|
for (const diag of diagnostics) {
|
||||||
|
|||||||
Reference in New Issue
Block a user