@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.
- package/dist/agents/tools/message-tool.js +10 -0
- package/dist/build-info.json +3 -3
- package/dist/config/schema.js +2 -2
- package/dist/control-ui/assets/{index-QAV6uia0.js → index-Car9AOpb.js} +378 -345
- package/dist/control-ui/assets/index-Car9AOpb.js.map +1 -0
- package/dist/control-ui/index.html +1 -1
- package/dist/gateway/server-methods/channels.js +26 -1
- package/dist/gateway/server-methods/tailscale.js +5 -4
- package/dist/gateway/server-methods/tunnel.js +5 -4
- package/dist/infra/outbound/message-action-runner.js +22 -3
- package/package.json +1 -1
- package/dist/control-ui/assets/index-QAV6uia0.js.map +0 -1
|
@@ -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-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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) {
|