fix: improve onboarding allowlist + Control UI link
This commit is contained in:
@@ -54,6 +54,7 @@
|
||||
- CLI onboarding: explain Tailscale exposure options (Off/Serve/Funnel) and colorize provider status (linked/configured/needs setup).
|
||||
- CLI onboarding: add provider primers (WhatsApp/Telegram/Discord/Signal) incl. Discord bot token setup steps.
|
||||
- CLI onboarding: allow skipping the “install missing skill dependencies” selection without canceling the wizard.
|
||||
- CLI onboarding: always prompt for WhatsApp `routing.allowFrom` and print (optionally open) the Control UI URL when done.
|
||||
- CLI onboarding: detect gateway reachability and annotate Local/Remote choices (helps pick the right mode).
|
||||
- macOS settings: colorize provider status subtitles to distinguish healthy vs degraded states.
|
||||
- macOS codesign: skip hardened runtime for ad-hoc signing and avoid empty options args (#70) — thanks @petter-b
|
||||
|
||||
@@ -39,6 +39,7 @@ import {
|
||||
printWizardHeader,
|
||||
probeGatewayReachable,
|
||||
randomToken,
|
||||
resolveControlUiLinks,
|
||||
summarizeExistingConfig,
|
||||
} from "./onboard-helpers.js";
|
||||
import { setupProviders } from "./onboard-providers.js";
|
||||
@@ -550,6 +551,30 @@ export async function runConfigureWizard(
|
||||
}
|
||||
}
|
||||
|
||||
note(
|
||||
(() => {
|
||||
const bind = nextConfig.gateway?.bind ?? "loopback";
|
||||
const links = resolveControlUiLinks({ bind, port: gatewayPort });
|
||||
return [`Web UI: ${links.httpUrl}`, `Gateway WS: ${links.wsUrl}`].join(
|
||||
"\n",
|
||||
);
|
||||
})(),
|
||||
"Control UI",
|
||||
);
|
||||
|
||||
const wantsOpen = guardCancel(
|
||||
await confirm({
|
||||
message: "Open Control UI now?",
|
||||
initialValue: false,
|
||||
}),
|
||||
runtime,
|
||||
);
|
||||
if (wantsOpen) {
|
||||
const bind = nextConfig.gateway?.bind ?? "loopback";
|
||||
const links = resolveControlUiLinks({ bind, port: gatewayPort });
|
||||
await openUrl(links.httpUrl);
|
||||
}
|
||||
|
||||
outro("Configure complete.");
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import type { ClawdisConfig } from "../config/config.js";
|
||||
import { CONFIG_PATH_CLAWDIS } from "../config/config.js";
|
||||
import { resolveSessionTranscriptsDir } from "../config/sessions.js";
|
||||
import { callGateway } from "../gateway/call.js";
|
||||
import { pickPrimaryTailnetIPv4 } from "../infra/tailnet.js";
|
||||
import { runCommandWithTimeout } from "../process/exec.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import { CONFIG_DIR, resolveUserPath } from "../utils.js";
|
||||
@@ -205,3 +206,20 @@ function summarizeError(err: unknown): string {
|
||||
}
|
||||
|
||||
export const DEFAULT_WORKSPACE = DEFAULT_AGENT_WORKSPACE_DIR;
|
||||
|
||||
export function resolveControlUiLinks(params: {
|
||||
port: number;
|
||||
bind?: "auto" | "lan" | "tailnet" | "loopback";
|
||||
}): { httpUrl: string; wsUrl: string } {
|
||||
const port = params.port;
|
||||
const bind = params.bind ?? "loopback";
|
||||
const tailnetIPv4 = pickPrimaryTailnetIPv4();
|
||||
const host =
|
||||
bind === "tailnet" || (bind === "auto" && tailnetIPv4)
|
||||
? (tailnetIPv4 ?? "127.0.0.1")
|
||||
: "127.0.0.1";
|
||||
return {
|
||||
httpUrl: `http://${host}:${port}/`,
|
||||
wsUrl: `ws://${host}:${port}`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@ import {
|
||||
import { GATEWAY_LAUNCH_AGENT_LABEL } from "../daemon/constants.js";
|
||||
import { resolveGatewayProgramArguments } from "../daemon/program-args.js";
|
||||
import { resolveGatewayService } from "../daemon/service.js";
|
||||
import { pickPrimaryTailnetIPv4 } from "../infra/tailnet.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import { defaultRuntime } from "../runtime.js";
|
||||
import { resolveUserPath, sleep } from "../utils.js";
|
||||
@@ -40,6 +39,7 @@ import {
|
||||
printWizardHeader,
|
||||
probeGatewayReachable,
|
||||
randomToken,
|
||||
resolveControlUiLinks,
|
||||
summarizeExistingConfig,
|
||||
} from "./onboard-helpers.js";
|
||||
import { setupProviders } from "./onboard-providers.js";
|
||||
@@ -481,18 +481,25 @@ export async function runInteractiveOnboarding(
|
||||
|
||||
note(
|
||||
(() => {
|
||||
const tailnetIPv4 = pickPrimaryTailnetIPv4();
|
||||
const host =
|
||||
bind === "tailnet" || (bind === "auto" && tailnetIPv4)
|
||||
? (tailnetIPv4 ?? "127.0.0.1")
|
||||
: "127.0.0.1";
|
||||
return [
|
||||
`Control UI: http://${host}:${port}/`,
|
||||
`Gateway WS: ws://${host}:${port}`,
|
||||
].join("\n");
|
||||
const links = resolveControlUiLinks({ bind, port });
|
||||
return [`Web UI: ${links.httpUrl}`, `Gateway WS: ${links.wsUrl}`].join(
|
||||
"\n",
|
||||
);
|
||||
})(),
|
||||
"Open the Control UI",
|
||||
"Control UI",
|
||||
);
|
||||
|
||||
const wantsOpen = guardCancel(
|
||||
await confirm({
|
||||
message: "Open Control UI now?",
|
||||
initialValue: true,
|
||||
}),
|
||||
runtime,
|
||||
);
|
||||
if (wantsOpen) {
|
||||
const links = resolveControlUiLinks({ bind, port });
|
||||
await openUrl(links.httpUrl);
|
||||
}
|
||||
|
||||
outro("Onboarding complete.");
|
||||
}
|
||||
|
||||
@@ -64,6 +64,93 @@ function noteDiscordTokenHelp(): void {
|
||||
);
|
||||
}
|
||||
|
||||
function setRoutingAllowFrom(cfg: ClawdisConfig, allowFrom?: string[]) {
|
||||
return {
|
||||
...cfg,
|
||||
routing: {
|
||||
...(cfg.routing ?? {}),
|
||||
allowFrom,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function promptWhatsAppAllowFrom(
|
||||
cfg: ClawdisConfig,
|
||||
runtime: RuntimeEnv,
|
||||
): Promise<ClawdisConfig> {
|
||||
const existingAllowFrom = cfg.routing?.allowFrom ?? [];
|
||||
const existingLabel =
|
||||
existingAllowFrom.length > 0 ? existingAllowFrom.join(", ") : "unset";
|
||||
|
||||
note(
|
||||
[
|
||||
"WhatsApp direct chats are gated by `routing.allowFrom`.",
|
||||
'Default (unset) = self-chat only; use "*" to allow anyone.',
|
||||
`Current: ${existingLabel}`,
|
||||
].join("\n"),
|
||||
"WhatsApp allowlist",
|
||||
);
|
||||
|
||||
const options =
|
||||
existingAllowFrom.length > 0
|
||||
? ([
|
||||
{ value: "keep", label: "Keep current" },
|
||||
{ value: "self", label: "Self-chat only (unset)" },
|
||||
{ value: "list", label: "Specific numbers (recommended)" },
|
||||
{ value: "any", label: "Anyone (*)" },
|
||||
] as const)
|
||||
: ([
|
||||
{ value: "self", label: "Self-chat only (default)" },
|
||||
{ value: "list", label: "Specific numbers (recommended)" },
|
||||
{ value: "any", label: "Anyone (*)" },
|
||||
] as const);
|
||||
|
||||
const mode = guardCancel(
|
||||
await select({
|
||||
message: "Who can trigger the bot via WhatsApp?",
|
||||
options: options.map((opt) => ({ value: opt.value, label: opt.label })),
|
||||
}),
|
||||
runtime,
|
||||
) as (typeof options)[number]["value"];
|
||||
|
||||
if (mode === "keep") return cfg;
|
||||
if (mode === "self") return setRoutingAllowFrom(cfg, undefined);
|
||||
if (mode === "any") return setRoutingAllowFrom(cfg, ["*"]);
|
||||
|
||||
const allowRaw = guardCancel(
|
||||
await text({
|
||||
message: "Allowed sender numbers (comma-separated, E.164)",
|
||||
placeholder: "+15555550123, +447700900123",
|
||||
validate: (value) => {
|
||||
const raw = String(value ?? "").trim();
|
||||
if (!raw) return "Required";
|
||||
const parts = raw
|
||||
.split(/[\n,;]+/g)
|
||||
.map((p) => p.trim())
|
||||
.filter(Boolean);
|
||||
if (parts.length === 0) return "Required";
|
||||
for (const part of parts) {
|
||||
if (part === "*") continue;
|
||||
const normalized = normalizeE164(part);
|
||||
if (!normalized) return `Invalid number: ${part}`;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
}),
|
||||
runtime,
|
||||
);
|
||||
|
||||
const parts = String(allowRaw)
|
||||
.split(/[\n,;]+/g)
|
||||
.map((p) => p.trim())
|
||||
.filter(Boolean);
|
||||
const normalized = parts.map((part) =>
|
||||
part === "*" ? "*" : normalizeE164(part),
|
||||
);
|
||||
const unique = [...new Set(normalized.filter(Boolean))];
|
||||
return setRoutingAllowFrom(cfg, unique);
|
||||
}
|
||||
|
||||
export async function setupProviders(
|
||||
cfg: ClawdisConfig,
|
||||
runtime: RuntimeEnv,
|
||||
@@ -198,70 +285,7 @@ export async function setupProviders(
|
||||
note("Run `clawdis login` later to link WhatsApp.", "WhatsApp");
|
||||
}
|
||||
|
||||
const existingAllowFrom = cfg.routing?.allowFrom ?? [];
|
||||
if (existingAllowFrom.length === 0) {
|
||||
note(
|
||||
[
|
||||
"WhatsApp direct chats are gated by `routing.allowFrom`.",
|
||||
'Default (unset) = self-chat only; use "*" to allow anyone.',
|
||||
].join("\n"),
|
||||
"Allowlist (recommended)",
|
||||
);
|
||||
const mode = guardCancel(
|
||||
await select({
|
||||
message: "Who can trigger the bot via WhatsApp?",
|
||||
options: [
|
||||
{ value: "self", label: "Self-chat only (default)" },
|
||||
{ value: "list", label: "Specific numbers (recommended)" },
|
||||
{ value: "any", label: "Anyone (*)" },
|
||||
],
|
||||
}),
|
||||
runtime,
|
||||
) as "self" | "list" | "any";
|
||||
|
||||
if (mode === "any") {
|
||||
next = {
|
||||
...next,
|
||||
routing: { ...next.routing, allowFrom: ["*"] },
|
||||
};
|
||||
} else if (mode === "list") {
|
||||
const allowRaw = guardCancel(
|
||||
await text({
|
||||
message: "Allowed sender numbers (comma-separated, E.164)",
|
||||
placeholder: "+15555550123, +447700900123",
|
||||
validate: (value) => {
|
||||
const raw = String(value ?? "").trim();
|
||||
if (!raw) return "Required";
|
||||
const parts = raw
|
||||
.split(/[\n,;]+/g)
|
||||
.map((p) => p.trim())
|
||||
.filter(Boolean);
|
||||
if (parts.length === 0) return "Required";
|
||||
for (const part of parts) {
|
||||
if (part === "*") continue;
|
||||
const normalized = normalizeE164(part);
|
||||
if (!normalized) return `Invalid number: ${part}`;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
}),
|
||||
runtime,
|
||||
);
|
||||
|
||||
const parts = String(allowRaw)
|
||||
.split(/[\n,;]+/g)
|
||||
.map((p) => p.trim())
|
||||
.filter(Boolean);
|
||||
const normalized = parts.map((part) =>
|
||||
part === "*" ? "*" : normalizeE164(part),
|
||||
);
|
||||
const unique = [...new Set(normalized.filter(Boolean))];
|
||||
next = {
|
||||
...next,
|
||||
routing: { ...next.routing, allowFrom: unique },
|
||||
};
|
||||
}
|
||||
}
|
||||
next = await promptWhatsAppAllowFrom(next, runtime);
|
||||
}
|
||||
|
||||
if (selection.includes("telegram")) {
|
||||
|
||||
Reference in New Issue
Block a user