@rubytech/taskmaster 1.44.2 → 1.44.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,7 +6,7 @@
6
6
  <title>Taskmaster Control</title>
7
7
  <meta name="color-scheme" content="dark light" />
8
8
  <link rel="icon" type="image/png" href="./favicon.png?v=2" />
9
- <script type="module" crossorigin src="./assets/index-QAV6uia0.js"></script>
9
+ <script type="module" crossorigin src="./assets/index-Car9AOpb.js"></script>
10
10
  <link rel="stylesheet" crossorigin href="./assets/index-CAu2PL0O.css">
11
11
  </head>
12
12
  <body>
@@ -197,7 +197,32 @@ export const channelsHandlers = {
197
197
  const accountsMap = payload.channelAccounts;
198
198
  const defaultAccountIdMap = payload.channelDefaultAccountId;
199
199
  for (const plugin of plugins) {
200
- const { accounts, defaultAccountId, defaultAccount, resolvedAccounts } = await buildChannelAccounts(plugin.id);
200
+ let didTimeout = false;
201
+ const pluginFallback = {
202
+ accounts: [],
203
+ defaultAccountId: DEFAULT_ACCOUNT_ID,
204
+ defaultAccount: undefined,
205
+ resolvedAccounts: {},
206
+ };
207
+ const buildPromise = buildChannelAccounts(plugin.id);
208
+ let timer = null;
209
+ const timeoutPromise = new Promise((resolve) => {
210
+ timer = setTimeout(() => {
211
+ didTimeout = true;
212
+ resolve(pluginFallback);
213
+ }, 10_000);
214
+ });
215
+ const { accounts, defaultAccountId, defaultAccount, resolvedAccounts } = await Promise.race([
216
+ buildPromise.then((result) => {
217
+ if (timer)
218
+ clearTimeout(timer);
219
+ return result;
220
+ }),
221
+ timeoutPromise,
222
+ ]);
223
+ if (didTimeout) {
224
+ context.logGateway.warn(`[${plugin.id}] channels.status timed out after 10s`);
225
+ }
201
226
  const fallbackAccount = resolvedAccounts[defaultAccountId] ?? plugin.config.resolveAccount(cfg, defaultAccountId);
202
227
  const summary = plugin.status?.buildChannelSummary
203
228
  ? await plugin.status.buildChannelSummary({
@@ -6,6 +6,7 @@ import { CONFIG_PATH_TASKMASTER, loadConfig, readConfigFileSnapshot, validateCon
6
6
  import { applyMergePatch } from "../../config/merge-patch.js";
7
7
  import { findTailscaleBinary, getTailnetHostname, readTailscaleStatusJson, } from "../../infra/tailscale.js";
8
8
  import { runExec } from "../../process/exec.js";
9
+ import { withTimeout as withTimeoutThrow } from "../../infra/archive.js";
9
10
  import { scheduleGatewaySigusr1Restart } from "../../infra/restart.js";
10
11
  import { formatDoctorNonInteractiveHint, writeRestartSentinel, } from "../../infra/restart-sentinel.js";
11
12
  import { ErrorCodes, errorShape } from "../protocol/index.js";
@@ -31,21 +32,21 @@ export const tailscaleHandlers = {
31
32
  let running = false;
32
33
  let loggedIn = false;
33
34
  try {
34
- const status = await readTailscaleStatusJson();
35
+ const status = await withTimeoutThrow(readTailscaleStatusJson(), 5_000, "tailscale status");
35
36
  running = true;
36
37
  // BackendState "Running" means logged in and connected to the tailnet
37
38
  loggedIn = status.BackendState === "Running";
38
39
  }
39
40
  catch {
40
- // Daemon not running or not logged in
41
+ // Daemon not running, not logged in, or timed out
41
42
  }
42
43
  let hostname = null;
43
44
  if (loggedIn) {
44
45
  try {
45
- hostname = await getTailnetHostname(runExec, binary);
46
+ hostname = await withTimeoutThrow(getTailnetHostname(runExec, binary), 5_000, "tailscale hostname");
46
47
  }
47
48
  catch {
48
- // Failed to resolve hostname
49
+ // Failed to resolve hostname or timed out
49
50
  }
50
51
  }
51
52
  const cfg = loadConfig();
@@ -6,6 +6,7 @@ import { loadConfig, readConfigFileSnapshot, validateConfigObjectWithPlugins, wr
6
6
  import { applyMergePatch } from "../../config/merge-patch.js";
7
7
  import { ErrorCodes, errorShape } from "../protocol/index.js";
8
8
  import { isCloudflaredInstalled, isCloudflaredAuthenticated, getCloudflaredVersion, getInstallInstructions, installCloudflared, startCloudflaredLogin, createTunnel, routeTunnelDns, listTunnels, getAuthorizedZoneName, } from "../../infra/cloudflared.js";
9
+ import { withTimeout as withTimeoutThrow } from "../../infra/archive.js";
9
10
  import { getTunnelStatus, enableTunnel, disableTunnel } from "../server-tunnel.js";
10
11
  export const tunnelHandlers = {
11
12
  /**
@@ -13,10 +14,10 @@ export const tunnelHandlers = {
13
14
  */
14
15
  "tunnel.status": async ({ respond, context }) => {
15
16
  try {
16
- const installed = await isCloudflaredInstalled();
17
- const version = installed ? await getCloudflaredVersion() : null;
18
- const authenticated = installed ? await isCloudflaredAuthenticated() : false;
19
- const authorizedDomain = authenticated ? await getAuthorizedZoneName() : null;
17
+ const installed = await withTimeoutThrow(isCloudflaredInstalled(), 5_000, "cloudflared install check").catch(() => false);
18
+ const version = installed ? await withTimeoutThrow(getCloudflaredVersion(), 5_000, "cloudflared version").catch(() => null) : null;
19
+ const authenticated = installed ? await withTimeoutThrow(isCloudflaredAuthenticated(), 5_000, "cloudflared auth check").catch(() => false) : false;
20
+ const authorizedDomain = authenticated ? await withTimeoutThrow(getAuthorizedZoneName(), 5_000, "cloudflared zone name").catch(() => null) : null;
20
21
  const processStatus = getTunnelStatus();
21
22
  const cfg = loadConfig();
22
23
  const tunnelCfg = cfg.gateway?.tunnel;
@@ -350,24 +350,43 @@ function resolveGateway(input) {
350
350
  mode: input.gateway.mode,
351
351
  };
352
352
  }
353
+ /**
354
+ * Channels where automated broadcast is prohibited by the provider's Terms of Service.
355
+ * WhatsApp explicitly forbids automated bulk/broadcast messaging — violating this policy
356
+ * risks permanent number bans and account termination.
357
+ */
358
+ const BROADCAST_BLOCKED_CHANNELS = new Set(["whatsapp"]);
353
359
  async function handleBroadcastAction(input, params) {
354
360
  throwIfAborted(input.abortSignal);
355
- const broadcastEnabled = input.cfg.tools?.message?.broadcast?.enabled !== false;
361
+ const broadcastEnabled = input.cfg.tools?.message?.broadcast?.enabled === true;
356
362
  if (!broadcastEnabled) {
357
- throw new Error("Broadcast is disabled. Set tools.message.broadcast.enabled to true.");
363
+ throw new Error("Broadcast is disabled. Set tools.message.broadcast.enabled to true in config. " +
364
+ "Note: WhatsApp is always excluded — automated broadcasts violate WhatsApp's Terms of Service.");
358
365
  }
359
366
  const rawTargets = readStringArrayParam(params, "targets", { required: true }) ?? [];
360
367
  if (rawTargets.length === 0) {
361
368
  throw new Error("Broadcast requires at least one target in --targets.");
362
369
  }
363
370
  const channelHint = readStringParam(params, "channel");
371
+ // Hard block: reject broadcast requests that explicitly target a blocked channel.
372
+ if (channelHint) {
373
+ const normalizedHint = channelHint.trim().toLowerCase();
374
+ if (normalizedHint !== "all" && BROADCAST_BLOCKED_CHANNELS.has(normalizedHint)) {
375
+ throw new Error(`Broadcast to ${channelHint} is blocked. Automated broadcast messaging violates ${channelHint}'s Terms of Service and risks permanent account bans.`);
376
+ }
377
+ }
364
378
  const configured = await listConfiguredMessageChannels(input.cfg);
365
379
  if (configured.length === 0) {
366
380
  throw new Error("Broadcast requires at least one configured channel.");
367
381
  }
368
- const targetChannels = channelHint && channelHint.trim().toLowerCase() !== "all"
382
+ const allTargetChannels = channelHint && channelHint.trim().toLowerCase() !== "all"
369
383
  ? [await resolveChannel(input.cfg, { channel: channelHint })]
370
384
  : configured;
385
+ // Silently exclude blocked channels from broadcast fan-out.
386
+ const targetChannels = allTargetChannels.filter((ch) => !BROADCAST_BLOCKED_CHANNELS.has(ch));
387
+ if (targetChannels.length === 0) {
388
+ throw new Error("No eligible channels for broadcast. WhatsApp is excluded — automated broadcasts violate WhatsApp's Terms of Service.");
389
+ }
371
390
  const results = [];
372
391
  const isAbortError = (err) => err instanceof Error && err.name === "AbortError";
373
392
  for (const targetChannel of targetChannels) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/taskmaster",
3
- "version": "1.44.2",
3
+ "version": "1.44.4",
4
4
  "description": "AI-powered business assistant for small businesses",
5
5
  "publishConfig": {
6
6
  "access": "public"