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: 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: 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: 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).
|
- 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 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
|
- macOS codesign: skip hardened runtime for ad-hoc signing and avoid empty options args (#70) — thanks @petter-b
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import {
|
|||||||
printWizardHeader,
|
printWizardHeader,
|
||||||
probeGatewayReachable,
|
probeGatewayReachable,
|
||||||
randomToken,
|
randomToken,
|
||||||
|
resolveControlUiLinks,
|
||||||
summarizeExistingConfig,
|
summarizeExistingConfig,
|
||||||
} from "./onboard-helpers.js";
|
} from "./onboard-helpers.js";
|
||||||
import { setupProviders } from "./onboard-providers.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.");
|
outro("Configure complete.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import type { ClawdisConfig } from "../config/config.js";
|
|||||||
import { CONFIG_PATH_CLAWDIS } from "../config/config.js";
|
import { CONFIG_PATH_CLAWDIS } from "../config/config.js";
|
||||||
import { resolveSessionTranscriptsDir } from "../config/sessions.js";
|
import { resolveSessionTranscriptsDir } from "../config/sessions.js";
|
||||||
import { callGateway } from "../gateway/call.js";
|
import { callGateway } from "../gateway/call.js";
|
||||||
|
import { pickPrimaryTailnetIPv4 } from "../infra/tailnet.js";
|
||||||
import { runCommandWithTimeout } from "../process/exec.js";
|
import { runCommandWithTimeout } from "../process/exec.js";
|
||||||
import type { RuntimeEnv } from "../runtime.js";
|
import type { RuntimeEnv } from "../runtime.js";
|
||||||
import { CONFIG_DIR, resolveUserPath } from "../utils.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 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 { GATEWAY_LAUNCH_AGENT_LABEL } from "../daemon/constants.js";
|
||||||
import { resolveGatewayProgramArguments } from "../daemon/program-args.js";
|
import { resolveGatewayProgramArguments } from "../daemon/program-args.js";
|
||||||
import { resolveGatewayService } from "../daemon/service.js";
|
import { resolveGatewayService } from "../daemon/service.js";
|
||||||
import { pickPrimaryTailnetIPv4 } from "../infra/tailnet.js";
|
|
||||||
import type { RuntimeEnv } from "../runtime.js";
|
import type { RuntimeEnv } from "../runtime.js";
|
||||||
import { defaultRuntime } from "../runtime.js";
|
import { defaultRuntime } from "../runtime.js";
|
||||||
import { resolveUserPath, sleep } from "../utils.js";
|
import { resolveUserPath, sleep } from "../utils.js";
|
||||||
@@ -40,6 +39,7 @@ import {
|
|||||||
printWizardHeader,
|
printWizardHeader,
|
||||||
probeGatewayReachable,
|
probeGatewayReachable,
|
||||||
randomToken,
|
randomToken,
|
||||||
|
resolveControlUiLinks,
|
||||||
summarizeExistingConfig,
|
summarizeExistingConfig,
|
||||||
} from "./onboard-helpers.js";
|
} from "./onboard-helpers.js";
|
||||||
import { setupProviders } from "./onboard-providers.js";
|
import { setupProviders } from "./onboard-providers.js";
|
||||||
@@ -481,18 +481,25 @@ export async function runInteractiveOnboarding(
|
|||||||
|
|
||||||
note(
|
note(
|
||||||
(() => {
|
(() => {
|
||||||
const tailnetIPv4 = pickPrimaryTailnetIPv4();
|
const links = resolveControlUiLinks({ bind, port });
|
||||||
const host =
|
return [`Web UI: ${links.httpUrl}`, `Gateway WS: ${links.wsUrl}`].join(
|
||||||
bind === "tailnet" || (bind === "auto" && tailnetIPv4)
|
"\n",
|
||||||
? (tailnetIPv4 ?? "127.0.0.1")
|
);
|
||||||
: "127.0.0.1";
|
|
||||||
return [
|
|
||||||
`Control UI: http://${host}:${port}/`,
|
|
||||||
`Gateway WS: ws://${host}:${port}`,
|
|
||||||
].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.");
|
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(
|
export async function setupProviders(
|
||||||
cfg: ClawdisConfig,
|
cfg: ClawdisConfig,
|
||||||
runtime: RuntimeEnv,
|
runtime: RuntimeEnv,
|
||||||
@@ -198,70 +285,7 @@ export async function setupProviders(
|
|||||||
note("Run `clawdis login` later to link WhatsApp.", "WhatsApp");
|
note("Run `clawdis login` later to link WhatsApp.", "WhatsApp");
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingAllowFrom = cfg.routing?.allowFrom ?? [];
|
next = await promptWhatsAppAllowFrom(next, runtime);
|
||||||
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 },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selection.includes("telegram")) {
|
if (selection.includes("telegram")) {
|
||||||
|
|||||||
Reference in New Issue
Block a user