@poolzin/pool-bot 2026.2.0 → 2026.2.2
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/CHANGELOG.md +118 -0
- package/README-header.png +0 -0
- package/dist/agents/bash-tools.exec.js +76 -25
- package/dist/agents/cli-runner/helpers.js +9 -11
- package/dist/agents/context.js +1 -1
- package/dist/agents/identity.js +47 -7
- package/dist/agents/memory-search.js +25 -8
- package/dist/agents/model-catalog.js +1 -1
- package/dist/agents/model-selection.js +21 -0
- package/dist/agents/pi-embedded-block-chunker.js +117 -42
- package/dist/agents/pi-embedded-helpers/errors.js +183 -78
- package/dist/agents/pi-embedded-helpers.js +1 -1
- package/dist/agents/pi-embedded-runner/compact.js +8 -10
- package/dist/agents/pi-embedded-runner/model.js +62 -3
- package/dist/agents/pi-embedded-runner/run/attempt.js +21 -11
- package/dist/agents/pi-embedded-runner/run.js +199 -46
- package/dist/agents/pi-embedded-runner/system-prompt.js +10 -2
- package/dist/agents/pi-embedded-subscribe.js +118 -29
- package/dist/agents/pi-tools.js +10 -5
- package/dist/agents/poolbot-tools.js +15 -10
- package/dist/agents/sandbox-paths.js +31 -0
- package/dist/agents/session-tool-result-guard.js +94 -15
- package/dist/agents/shell-utils.js +51 -0
- package/dist/agents/skills/bundled-context.js +23 -0
- package/dist/agents/skills/bundled-dir.js +41 -7
- package/dist/agents/skills-install.js +60 -23
- package/dist/agents/subagent-announce.js +79 -34
- package/dist/agents/tool-policy.conformance.js +14 -0
- package/dist/agents/tool-policy.js +24 -0
- package/dist/agents/tools/cron-tool.js +166 -19
- package/dist/agents/tools/discord-actions-presence.js +78 -0
- package/dist/agents/tools/image-tool.js +1 -1
- package/dist/agents/tools/message-tool.js +56 -2
- package/dist/agents/tools/sessions-history-tool.js +69 -1
- package/dist/agents/tools/web-search.js +211 -42
- package/dist/agents/usage.js +23 -1
- package/dist/agents/workspace-run.js +67 -0
- package/dist/agents/workspace-templates.js +44 -0
- package/dist/auto-reply/command-auth.js +121 -6
- package/dist/auto-reply/envelope.js +74 -82
- package/dist/auto-reply/reply/commands-compact.js +1 -0
- package/dist/auto-reply/reply/commands-context-report.js +1 -0
- package/dist/auto-reply/reply/commands-context.js +1 -0
- package/dist/auto-reply/reply/commands-models.js +107 -60
- package/dist/auto-reply/reply/commands-ptt.js +171 -0
- package/dist/auto-reply/reply/get-reply-run.js +2 -1
- package/dist/auto-reply/reply/inbound-context.js +5 -1
- package/dist/auto-reply/reply/mentions.js +1 -1
- package/dist/auto-reply/reply/model-selection.js +3 -3
- package/dist/auto-reply/thinking.js +88 -43
- package/dist/browser/bridge-server.js +13 -0
- package/dist/browser/cdp.helpers.js +38 -24
- package/dist/browser/client-fetch.js +50 -7
- package/dist/browser/config.js +1 -10
- package/dist/browser/extension-relay.js +101 -40
- package/dist/browser/pw-ai.js +1 -1
- package/dist/browser/pw-session.js +143 -8
- package/dist/browser/pw-tools-core.interactions.js +125 -27
- package/dist/browser/pw-tools-core.responses.js +1 -1
- package/dist/browser/pw-tools-core.state.js +1 -1
- package/dist/browser/routes/agent.act.js +86 -41
- package/dist/browser/routes/dispatcher.js +4 -4
- package/dist/browser/screenshot.js +1 -1
- package/dist/browser/server.js +13 -0
- package/dist/build-info.json +3 -3
- package/dist/canvas-host/a2ui/index.html +28 -28
- package/dist/channels/reply-prefix.js +8 -1
- package/dist/cli/cron-cli/register.cron-add.js +61 -40
- package/dist/cli/cron-cli/register.cron-edit.js +60 -34
- package/dist/cli/cron-cli/shared.js +56 -41
- package/dist/cli/dns-cli.js +26 -14
- package/dist/cli/gateway-cli/register.js +37 -19
- package/dist/cli/memory-cli.js +5 -5
- package/dist/cli/parse-bytes.js +37 -0
- package/dist/cli/update-cli.js +173 -52
- package/dist/commands/agent.js +1 -0
- package/dist/commands/auth-choice.apply.oauth.js +1 -1
- package/dist/commands/doctor-config-flow.js +61 -5
- package/dist/commands/doctor-state-migrations.js +1 -1
- package/dist/commands/health.js +1 -1
- package/dist/commands/model-allowlist.js +29 -0
- package/dist/commands/model-picker.js +2 -1
- package/dist/commands/models/list.registry.js +1 -1
- package/dist/commands/models/list.status-command.js +43 -23
- package/dist/commands/models/shared.js +15 -0
- package/dist/commands/onboard-custom.js +384 -0
- package/dist/commands/onboard-non-interactive/local/auth-choice-inference.js +35 -0
- package/dist/commands/onboard-non-interactive/local/auth-choice.js +6 -3
- package/dist/commands/onboard-skills.js +63 -38
- package/dist/commands/openai-model-default.js +41 -0
- package/dist/compat/legacy-names.js +2 -0
- package/dist/config/defaults.js +3 -2
- package/dist/config/paths.js +136 -35
- package/dist/config/plugin-auto-enable.js +21 -5
- package/dist/config/redact-snapshot.js +153 -0
- package/dist/config/schema.field-metadata.js +590 -0
- package/dist/config/schema.js +2 -2
- package/dist/config/sessions/store.js +291 -23
- package/dist/config/zod-schema.agent-defaults.js +3 -0
- package/dist/config/zod-schema.agent-runtime.js +13 -2
- package/dist/config/zod-schema.providers-core.js +142 -0
- package/dist/config/zod-schema.session.js +3 -0
- package/dist/control-ui/assets/{index-CIRDm-Lu.css → index-CSfXd2LO.css} +1 -1
- package/dist/control-ui/assets/{index-CmNMuoem.js → index-HRr1grwl.js} +446 -413
- package/dist/control-ui/assets/index-HRr1grwl.js.map +1 -0
- package/dist/control-ui/index.html +4 -4
- package/dist/cron/delivery.js +57 -0
- package/dist/cron/isolated-agent/delivery-target.js +18 -3
- package/dist/cron/isolated-agent/helpers.js +22 -5
- package/dist/cron/isolated-agent/run.js +172 -63
- package/dist/cron/isolated-agent/session.js +2 -0
- package/dist/cron/normalize.js +356 -28
- package/dist/cron/parse.js +10 -5
- package/dist/cron/run-log.js +35 -10
- package/dist/cron/schedule.js +41 -6
- package/dist/cron/service/jobs.js +208 -35
- package/dist/cron/service/ops.js +72 -16
- package/dist/cron/service/state.js +2 -0
- package/dist/cron/service/store.js +386 -14
- package/dist/cron/service/timer.js +390 -147
- package/dist/cron/session-reaper.js +86 -0
- package/dist/cron/store.js +23 -8
- package/dist/cron/validate-timestamp.js +43 -0
- package/dist/discord/monitor/agent-components.js +438 -0
- package/dist/discord/monitor/allow-list.js +28 -5
- package/dist/discord/monitor/gateway-registry.js +29 -0
- package/dist/discord/monitor/native-command.js +44 -23
- package/dist/discord/monitor/sender-identity.js +45 -0
- package/dist/discord/pluralkit.js +27 -0
- package/dist/discord/send.outbound.js +92 -5
- package/dist/discord/send.shared.js +60 -23
- package/dist/discord/targets.js +84 -1
- package/dist/entry.js +15 -9
- package/dist/extensionAPI.js +8 -0
- package/dist/gateway/control-ui.js +8 -1
- package/dist/gateway/hooks-mapping.js +3 -0
- package/dist/gateway/hooks.js +65 -0
- package/dist/gateway/net.js +96 -31
- package/dist/gateway/node-command-policy.js +50 -15
- package/dist/gateway/origin-check.js +56 -0
- package/dist/gateway/protocol/client-info.js +9 -0
- package/dist/gateway/protocol/index.js +9 -2
- package/dist/gateway/protocol/schema/agents-models-skills.js +71 -1
- package/dist/gateway/protocol/schema/cron.js +22 -10
- package/dist/gateway/protocol/schema/protocol-schemas.js +16 -2
- package/dist/gateway/protocol/schema/sessions.js +12 -0
- package/dist/gateway/server/hooks.js +1 -1
- package/dist/gateway/server-broadcast.js +26 -9
- package/dist/gateway/server-chat.js +112 -23
- package/dist/gateway/server-discovery-runtime.js +10 -2
- package/dist/gateway/server-http.js +109 -11
- package/dist/gateway/server-methods/agent-timestamp.js +60 -0
- package/dist/gateway/server-methods/agents.js +321 -2
- package/dist/gateway/server-methods/usage.js +559 -16
- package/dist/gateway/server-runtime-state.js +22 -8
- package/dist/gateway/server-startup-memory.js +16 -0
- package/dist/gateway/server.impl.js +5 -1
- package/dist/gateway/session-utils.fs.js +23 -25
- package/dist/gateway/session-utils.js +20 -10
- package/dist/gateway/sessions-patch.js +7 -22
- package/dist/gateway/test-helpers.mocks.js +11 -7
- package/dist/gateway/test-helpers.server.js +35 -2
- package/dist/imessage/constants.js +2 -0
- package/dist/imessage/monitor/deliver.js +4 -1
- package/dist/imessage/monitor/monitor-provider.js +51 -1
- package/dist/infra/bonjour-discovery.js +131 -70
- package/dist/infra/control-ui-assets.js +134 -12
- package/dist/infra/errors.js +12 -0
- package/dist/infra/exec-approvals.js +266 -57
- package/dist/infra/format-time/format-datetime.js +79 -0
- package/dist/infra/format-time/format-duration.js +81 -0
- package/dist/infra/format-time/format-relative.js +80 -0
- package/dist/infra/heartbeat-runner.js +140 -49
- package/dist/infra/home-dir.js +54 -0
- package/dist/infra/net/fetch-guard.js +122 -0
- package/dist/infra/net/ssrf.js +65 -29
- package/dist/infra/outbound/abort.js +14 -0
- package/dist/infra/outbound/message-action-runner.js +77 -13
- package/dist/infra/outbound/outbound-session.js +143 -37
- package/dist/infra/poolbot-root.js +43 -1
- package/dist/infra/session-cost-usage.js +631 -41
- package/dist/infra/state-migrations.js +317 -47
- package/dist/infra/update-global.js +35 -0
- package/dist/infra/update-runner.js +149 -43
- package/dist/infra/warning-filter.js +65 -0
- package/dist/infra/widearea-dns.js +30 -9
- package/dist/logging/redact-identifier.js +12 -0
- package/dist/media/fetch.js +81 -58
- package/dist/media/store.js +2 -0
- package/dist/media-understanding/apply.js +403 -3
- package/dist/media-understanding/attachments.js +38 -27
- package/dist/media-understanding/defaults.js +16 -0
- package/dist/media-understanding/providers/deepgram/audio.js +22 -14
- package/dist/media-understanding/providers/google/audio.js +24 -17
- package/dist/media-understanding/providers/google/video.js +24 -17
- package/dist/media-understanding/providers/image.js +3 -3
- package/dist/media-understanding/providers/index.js +4 -1
- package/dist/media-understanding/providers/openai/audio.js +22 -14
- package/dist/media-understanding/providers/shared.js +16 -11
- package/dist/media-understanding/providers/zai/index.js +6 -0
- package/dist/media-understanding/runner.js +158 -90
- package/dist/memory/batch-voyage.js +277 -0
- package/dist/memory/embeddings-voyage.js +75 -0
- package/dist/memory/embeddings.js +28 -16
- package/dist/memory/internal.js +101 -18
- package/dist/memory/manager.js +154 -48
- package/dist/memory/search-manager.js +173 -0
- package/dist/memory/session-files.js +9 -3
- package/dist/node-host/runner.js +34 -24
- package/dist/node-host/with-timeout.js +27 -0
- package/dist/plugins/commands.js +5 -1
- package/dist/plugins/config-state.js +86 -7
- package/dist/plugins/source-display.js +51 -0
- package/dist/process/exec.js +20 -2
- package/dist/routing/resolve-route.js +12 -0
- package/dist/routing/session-key.js +15 -0
- package/dist/runtime.js +2 -0
- package/dist/security/audit-extra.async.js +601 -0
- package/dist/security/audit-extra.js +2 -830
- package/dist/security/audit-extra.sync.js +505 -0
- package/dist/security/channel-metadata.js +34 -0
- package/dist/security/external-content.js +88 -6
- package/dist/security/skill-scanner.js +330 -0
- package/dist/sessions/session-key-utils.js +7 -0
- package/dist/signal/monitor/event-handler.js +80 -1
- package/dist/slack/monitor/media.js +85 -15
- package/dist/tailscale/detect.js +1 -2
- package/dist/telegram/bot/helpers.js +109 -28
- package/dist/telegram/bot-handlers.js +144 -3
- package/dist/telegram/bot-message-context.js +37 -10
- package/dist/telegram/bot-message-dispatch.js +54 -17
- package/dist/telegram/bot-native-commands.js +86 -29
- package/dist/telegram/bot.js +30 -29
- package/dist/telegram/model-buttons.js +163 -0
- package/dist/telegram/monitor.js +110 -85
- package/dist/telegram/send.js +129 -47
- package/dist/terminal/restore.js +45 -0
- package/dist/test-helpers/state-dir-env.js +16 -0
- package/dist/tts/tts.js +12 -6
- package/dist/tui/tui-session-actions.js +166 -54
- package/dist/utils/fetch-timeout.js +20 -0
- package/dist/utils/normalize-secret-input.js +19 -0
- package/dist/utils/transcript-tools.js +58 -0
- package/dist/utils.js +45 -14
- package/dist/version.js +42 -5
- package/dist/wizard/clack-prompter.js +9 -6
- package/extensions/googlechat/node_modules/.bin/poolbot +21 -0
- package/extensions/googlechat/package.json +2 -2
- package/extensions/line/node_modules/.bin/poolbot +21 -0
- package/extensions/line/package.json +1 -1
- package/extensions/matrix/node_modules/.bin/poolbot +21 -0
- package/extensions/matrix/package.json +1 -1
- package/extensions/memory-core/node_modules/.bin/poolbot +21 -0
- package/extensions/memory-core/package.json +4 -1
- package/extensions/twitch/node_modules/.bin/poolbot +21 -0
- package/extensions/twitch/package.json +1 -1
- package/package.json +183 -24
- package/dist/control-ui/assets/index-CmNMuoem.js.map +0 -1
package/dist/cron/store.js
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
|
+
import JSON5 from "json5";
|
|
1
2
|
import fs from "node:fs";
|
|
2
|
-
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import
|
|
4
|
+
import { expandHomePrefix } from "../infra/home-dir.js";
|
|
5
5
|
import { CONFIG_DIR } from "../utils.js";
|
|
6
6
|
export const DEFAULT_CRON_DIR = path.join(CONFIG_DIR, "cron");
|
|
7
7
|
export const DEFAULT_CRON_STORE_PATH = path.join(DEFAULT_CRON_DIR, "jobs.json");
|
|
8
8
|
export function resolveCronStorePath(storePath) {
|
|
9
9
|
if (storePath?.trim()) {
|
|
10
10
|
const raw = storePath.trim();
|
|
11
|
-
if (raw.startsWith("~"))
|
|
12
|
-
return path.resolve(raw
|
|
11
|
+
if (raw.startsWith("~")) {
|
|
12
|
+
return path.resolve(expandHomePrefix(raw));
|
|
13
|
+
}
|
|
13
14
|
return path.resolve(raw);
|
|
14
15
|
}
|
|
15
16
|
return DEFAULT_CRON_STORE_PATH;
|
|
@@ -17,15 +18,29 @@ export function resolveCronStorePath(storePath) {
|
|
|
17
18
|
export async function loadCronStore(storePath) {
|
|
18
19
|
try {
|
|
19
20
|
const raw = await fs.promises.readFile(storePath, "utf-8");
|
|
20
|
-
|
|
21
|
-
|
|
21
|
+
let parsed;
|
|
22
|
+
try {
|
|
23
|
+
parsed = JSON5.parse(raw);
|
|
24
|
+
}
|
|
25
|
+
catch (err) {
|
|
26
|
+
throw new Error(`Failed to parse cron store at ${storePath}: ${String(err)}`, {
|
|
27
|
+
cause: err,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
const parsedRecord = parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
31
|
+
? parsed
|
|
32
|
+
: {};
|
|
33
|
+
const jobs = Array.isArray(parsedRecord.jobs) ? parsedRecord.jobs : [];
|
|
22
34
|
return {
|
|
23
35
|
version: 1,
|
|
24
36
|
jobs: jobs.filter(Boolean),
|
|
25
37
|
};
|
|
26
38
|
}
|
|
27
|
-
catch {
|
|
28
|
-
|
|
39
|
+
catch (err) {
|
|
40
|
+
if (err?.code === "ENOENT") {
|
|
41
|
+
return { version: 1, jobs: [] };
|
|
42
|
+
}
|
|
43
|
+
throw err;
|
|
29
44
|
}
|
|
30
45
|
}
|
|
31
46
|
export async function saveCronStore(storePath, store) {
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { parseAbsoluteTimeMs } from "./parse.js";
|
|
2
|
+
const ONE_MINUTE_MS = 60 * 1000;
|
|
3
|
+
const TEN_YEARS_MS = 10 * 365.25 * 24 * 60 * 60 * 1000;
|
|
4
|
+
/**
|
|
5
|
+
* Validates at timestamps in cron schedules.
|
|
6
|
+
* Rejects timestamps that are:
|
|
7
|
+
* - More than 1 minute in the past
|
|
8
|
+
* - More than 10 years in the future
|
|
9
|
+
*/
|
|
10
|
+
export function validateScheduleTimestamp(schedule, nowMs = Date.now()) {
|
|
11
|
+
if (schedule.kind !== "at") {
|
|
12
|
+
return { ok: true };
|
|
13
|
+
}
|
|
14
|
+
const atRaw = typeof schedule.at === "string" ? schedule.at.trim() : "";
|
|
15
|
+
const atMs = atRaw ? parseAbsoluteTimeMs(atRaw) : null;
|
|
16
|
+
if (atMs === null || !Number.isFinite(atMs)) {
|
|
17
|
+
return {
|
|
18
|
+
ok: false,
|
|
19
|
+
message: `Invalid schedule.at: expected ISO-8601 timestamp (got ${String(schedule.at)})`,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
const diffMs = atMs - nowMs;
|
|
23
|
+
// Check if timestamp is in the past (allow 1 minute grace period)
|
|
24
|
+
if (diffMs < -ONE_MINUTE_MS) {
|
|
25
|
+
const nowDate = new Date(nowMs).toISOString();
|
|
26
|
+
const atDate = new Date(atMs).toISOString();
|
|
27
|
+
const minutesAgo = Math.floor(-diffMs / ONE_MINUTE_MS);
|
|
28
|
+
return {
|
|
29
|
+
ok: false,
|
|
30
|
+
message: `schedule.at is in the past: ${atDate} (${minutesAgo} minutes ago). Current time: ${nowDate}`,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
// Check if timestamp is too far in the future
|
|
34
|
+
if (diffMs > TEN_YEARS_MS) {
|
|
35
|
+
const atDate = new Date(atMs).toISOString();
|
|
36
|
+
const yearsAhead = Math.floor(diffMs / (365.25 * 24 * 60 * 60 * 1000));
|
|
37
|
+
return {
|
|
38
|
+
ok: false,
|
|
39
|
+
message: `schedule.at is too far in the future: ${atDate} (${yearsAhead} years ahead). Maximum allowed: 10 years`,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
return { ok: true };
|
|
43
|
+
}
|
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
import { Button, StringSelectMenu, } from "@buape/carbon";
|
|
2
|
+
import { ButtonStyle, ChannelType } from "discord-api-types/v10";
|
|
3
|
+
import { logVerbose } from "../../globals.js";
|
|
4
|
+
import { enqueueSystemEvent } from "../../infra/system-events.js";
|
|
5
|
+
import { logDebug, logError } from "../../logger.js";
|
|
6
|
+
import { buildPairingReply } from "../../pairing/pairing-messages.js";
|
|
7
|
+
import { readChannelAllowFromStore, upsertChannelPairingRequest, } from "../../pairing/pairing-store.js";
|
|
8
|
+
import { resolveAgentRoute } from "../../routing/resolve-route.js";
|
|
9
|
+
import { normalizeDiscordAllowList, normalizeDiscordSlug, resolveDiscordAllowListMatch, resolveDiscordChannelConfigWithFallback, resolveDiscordGuildEntry, resolveDiscordUserAllowed, } from "./allow-list.js";
|
|
10
|
+
import { formatDiscordUserTag } from "./format.js";
|
|
11
|
+
const AGENT_BUTTON_KEY = "agent";
|
|
12
|
+
const AGENT_SELECT_KEY = "agentsel";
|
|
13
|
+
/**
|
|
14
|
+
* Build agent button custom ID: agent:componentId=<id>
|
|
15
|
+
* The channelId is NOT embedded in customId - we use interaction.rawData.channel_id instead
|
|
16
|
+
* to prevent channel spoofing attacks.
|
|
17
|
+
*
|
|
18
|
+
* Carbon's customIdParser parses "key:arg1=value1;arg2=value2" into { arg1: value1, arg2: value2 }
|
|
19
|
+
*/
|
|
20
|
+
export function buildAgentButtonCustomId(componentId) {
|
|
21
|
+
return `${AGENT_BUTTON_KEY}:componentId=${encodeURIComponent(componentId)}`;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Build agent select menu custom ID: agentsel:componentId=<id>
|
|
25
|
+
*/
|
|
26
|
+
export function buildAgentSelectCustomId(componentId) {
|
|
27
|
+
return `${AGENT_SELECT_KEY}:componentId=${encodeURIComponent(componentId)}`;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Parse agent component data from Carbon's parsed ComponentData
|
|
31
|
+
* Carbon parses "key:componentId=xxx" into { componentId: "xxx" }
|
|
32
|
+
*/
|
|
33
|
+
function parseAgentComponentData(data) {
|
|
34
|
+
if (!data || typeof data !== "object") {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
const componentId = typeof data.componentId === "string"
|
|
38
|
+
? decodeURIComponent(data.componentId)
|
|
39
|
+
: typeof data.componentId === "number"
|
|
40
|
+
? String(data.componentId)
|
|
41
|
+
: null;
|
|
42
|
+
if (!componentId) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
return { componentId };
|
|
46
|
+
}
|
|
47
|
+
function formatUsername(user) {
|
|
48
|
+
if (user.discriminator && user.discriminator !== "0") {
|
|
49
|
+
return `${user.username}#${user.discriminator}`;
|
|
50
|
+
}
|
|
51
|
+
return user.username;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Check if a channel type is a thread type
|
|
55
|
+
*/
|
|
56
|
+
function isThreadChannelType(channelType) {
|
|
57
|
+
return (channelType === ChannelType.PublicThread ||
|
|
58
|
+
channelType === ChannelType.PrivateThread ||
|
|
59
|
+
channelType === ChannelType.AnnouncementThread);
|
|
60
|
+
}
|
|
61
|
+
async function ensureDmComponentAuthorized(params) {
|
|
62
|
+
const { ctx, interaction, user, componentLabel } = params;
|
|
63
|
+
const dmPolicy = ctx.dmPolicy ?? "pairing";
|
|
64
|
+
if (dmPolicy === "disabled") {
|
|
65
|
+
logVerbose(`agent ${componentLabel}: blocked (DM policy disabled)`);
|
|
66
|
+
try {
|
|
67
|
+
await interaction.reply({
|
|
68
|
+
content: "DM interactions are disabled.",
|
|
69
|
+
ephemeral: true,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
// Interaction may have expired
|
|
74
|
+
}
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
if (dmPolicy === "open") {
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
const storeAllowFrom = await readChannelAllowFromStore("discord").catch(() => []);
|
|
81
|
+
const effectiveAllowFrom = [...(ctx.allowFrom ?? []), ...storeAllowFrom];
|
|
82
|
+
const allowList = normalizeDiscordAllowList(effectiveAllowFrom, ["discord:", "user:", "pk:"]);
|
|
83
|
+
const allowMatch = allowList
|
|
84
|
+
? resolveDiscordAllowListMatch({
|
|
85
|
+
allowList,
|
|
86
|
+
candidate: {
|
|
87
|
+
id: user.id,
|
|
88
|
+
name: user.username,
|
|
89
|
+
tag: formatDiscordUserTag(user),
|
|
90
|
+
},
|
|
91
|
+
})
|
|
92
|
+
: { allowed: false };
|
|
93
|
+
if (allowMatch.allowed) {
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
if (dmPolicy === "pairing") {
|
|
97
|
+
const { code, created } = await upsertChannelPairingRequest({
|
|
98
|
+
channel: "discord",
|
|
99
|
+
id: user.id,
|
|
100
|
+
meta: {
|
|
101
|
+
tag: formatDiscordUserTag(user),
|
|
102
|
+
name: user.username,
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
try {
|
|
106
|
+
await interaction.reply({
|
|
107
|
+
content: created
|
|
108
|
+
? buildPairingReply({
|
|
109
|
+
channel: "discord",
|
|
110
|
+
idLine: `Your Discord user id: ${user.id}`,
|
|
111
|
+
code,
|
|
112
|
+
})
|
|
113
|
+
: "Pairing already requested. Ask the bot owner to approve your code.",
|
|
114
|
+
ephemeral: true,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
// Interaction may have expired
|
|
119
|
+
}
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
logVerbose(`agent ${componentLabel}: blocked DM user ${user.id} (not in allowFrom)`);
|
|
123
|
+
try {
|
|
124
|
+
await interaction.reply({
|
|
125
|
+
content: `You are not authorized to use this ${componentLabel}.`,
|
|
126
|
+
ephemeral: true,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
// Interaction may have expired
|
|
131
|
+
}
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
export class AgentComponentButton extends Button {
|
|
135
|
+
label = AGENT_BUTTON_KEY;
|
|
136
|
+
customId = `${AGENT_BUTTON_KEY}:seed=1`;
|
|
137
|
+
style = ButtonStyle.Primary;
|
|
138
|
+
ctx;
|
|
139
|
+
constructor(ctx) {
|
|
140
|
+
super();
|
|
141
|
+
this.ctx = ctx;
|
|
142
|
+
}
|
|
143
|
+
async run(interaction, data) {
|
|
144
|
+
// Parse componentId from Carbon's parsed ComponentData
|
|
145
|
+
const parsed = parseAgentComponentData(data);
|
|
146
|
+
if (!parsed) {
|
|
147
|
+
logError("agent button: failed to parse component data");
|
|
148
|
+
try {
|
|
149
|
+
await interaction.reply({
|
|
150
|
+
content: "This button is no longer valid.",
|
|
151
|
+
ephemeral: true,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
// Interaction may have expired
|
|
156
|
+
}
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const { componentId } = parsed;
|
|
160
|
+
// P1 FIX: Use interaction's actual channel_id instead of trusting customId
|
|
161
|
+
// This prevents channel ID spoofing attacks where an attacker crafts a button
|
|
162
|
+
// with a different channelId to inject events into other sessions
|
|
163
|
+
const channelId = interaction.rawData.channel_id;
|
|
164
|
+
if (!channelId) {
|
|
165
|
+
logError("agent button: missing channel_id in interaction");
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
const user = interaction.user;
|
|
169
|
+
if (!user) {
|
|
170
|
+
logError("agent button: missing user in interaction");
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const username = formatUsername(user);
|
|
174
|
+
const userId = user.id;
|
|
175
|
+
// P1 FIX: Use rawData.guild_id as source of truth - interaction.guild can be null
|
|
176
|
+
// when guild is not cached even though guild_id is present in rawData
|
|
177
|
+
const rawGuildId = interaction.rawData.guild_id;
|
|
178
|
+
const isDirectMessage = !rawGuildId;
|
|
179
|
+
if (isDirectMessage) {
|
|
180
|
+
const authorized = await ensureDmComponentAuthorized({
|
|
181
|
+
ctx: this.ctx,
|
|
182
|
+
interaction,
|
|
183
|
+
user,
|
|
184
|
+
componentLabel: "button",
|
|
185
|
+
});
|
|
186
|
+
if (!authorized) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
// P2 FIX: Check user allowlist before processing component interaction
|
|
191
|
+
// This prevents unauthorized users from injecting system events
|
|
192
|
+
const guild = interaction.guild;
|
|
193
|
+
const guildInfo = resolveDiscordGuildEntry({
|
|
194
|
+
guild: guild ?? undefined,
|
|
195
|
+
guildEntries: this.ctx.guildEntries,
|
|
196
|
+
});
|
|
197
|
+
// Resolve channel info for thread detection and allowlist inheritance
|
|
198
|
+
const channel = interaction.channel;
|
|
199
|
+
const channelName = channel && "name" in channel ? channel.name : undefined;
|
|
200
|
+
const channelSlug = channelName ? normalizeDiscordSlug(channelName) : "";
|
|
201
|
+
const channelType = channel && "type" in channel ? channel.type : undefined;
|
|
202
|
+
const isThread = isThreadChannelType(channelType);
|
|
203
|
+
// Resolve thread parent for allowlist inheritance
|
|
204
|
+
// Note: We can get parentId from channel but cannot fetch parent name without a client.
|
|
205
|
+
// The parentId alone enables ID-based parent config matching. Name-based matching
|
|
206
|
+
// requires the channel cache to have parent info available.
|
|
207
|
+
let parentId;
|
|
208
|
+
let parentName;
|
|
209
|
+
let parentSlug = "";
|
|
210
|
+
if (isThread && channel && "parentId" in channel) {
|
|
211
|
+
parentId = channel.parentId ?? undefined;
|
|
212
|
+
// Try to get parent name from channel's parent if available
|
|
213
|
+
if ("parent" in channel) {
|
|
214
|
+
const parent = channel.parent;
|
|
215
|
+
if (parent?.name) {
|
|
216
|
+
parentName = parent.name;
|
|
217
|
+
parentSlug = normalizeDiscordSlug(parentName);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
// Only check guild allowlists if this is a guild interaction
|
|
222
|
+
if (rawGuildId) {
|
|
223
|
+
const channelConfig = resolveDiscordChannelConfigWithFallback({
|
|
224
|
+
guildInfo,
|
|
225
|
+
channelId,
|
|
226
|
+
channelName,
|
|
227
|
+
channelSlug,
|
|
228
|
+
parentId,
|
|
229
|
+
parentName,
|
|
230
|
+
parentSlug,
|
|
231
|
+
scope: isThread ? "thread" : "channel",
|
|
232
|
+
});
|
|
233
|
+
const channelUsers = channelConfig?.users ?? guildInfo?.users;
|
|
234
|
+
if (Array.isArray(channelUsers) && channelUsers.length > 0) {
|
|
235
|
+
const userOk = resolveDiscordUserAllowed({
|
|
236
|
+
allowList: channelUsers,
|
|
237
|
+
userId,
|
|
238
|
+
userName: user.username,
|
|
239
|
+
userTag: user.discriminator ? `${user.username}#${user.discriminator}` : undefined,
|
|
240
|
+
});
|
|
241
|
+
if (!userOk) {
|
|
242
|
+
logVerbose(`agent button: blocked user ${userId} (not in allowlist)`);
|
|
243
|
+
try {
|
|
244
|
+
await interaction.reply({
|
|
245
|
+
content: "You are not authorized to use this button.",
|
|
246
|
+
ephemeral: true,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
// Interaction may have expired
|
|
251
|
+
}
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
// Resolve route with full context (guildId, proper peer kind)
|
|
257
|
+
const route = resolveAgentRoute({
|
|
258
|
+
cfg: this.ctx.cfg,
|
|
259
|
+
channel: "discord",
|
|
260
|
+
accountId: this.ctx.accountId,
|
|
261
|
+
guildId: rawGuildId,
|
|
262
|
+
peer: {
|
|
263
|
+
kind: isDirectMessage ? "dm" : "channel",
|
|
264
|
+
id: isDirectMessage ? userId : channelId,
|
|
265
|
+
},
|
|
266
|
+
});
|
|
267
|
+
const eventText = `[Discord component: ${componentId} clicked by ${username} (${userId})]`;
|
|
268
|
+
logDebug(`agent button: enqueuing event for channel ${channelId}: ${eventText}`);
|
|
269
|
+
enqueueSystemEvent(eventText, {
|
|
270
|
+
sessionKey: route.sessionKey,
|
|
271
|
+
contextKey: `discord:agent-button:${channelId}:${componentId}:${userId}`,
|
|
272
|
+
});
|
|
273
|
+
// Acknowledge the interaction
|
|
274
|
+
try {
|
|
275
|
+
await interaction.reply({
|
|
276
|
+
content: "✓",
|
|
277
|
+
ephemeral: true,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
catch (err) {
|
|
281
|
+
logError(`agent button: failed to acknowledge interaction: ${String(err)}`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
export class AgentSelectMenu extends StringSelectMenu {
|
|
286
|
+
customId = `${AGENT_SELECT_KEY}:seed=1`;
|
|
287
|
+
options = [];
|
|
288
|
+
ctx;
|
|
289
|
+
constructor(ctx) {
|
|
290
|
+
super();
|
|
291
|
+
this.ctx = ctx;
|
|
292
|
+
}
|
|
293
|
+
async run(interaction, data) {
|
|
294
|
+
// Parse componentId from Carbon's parsed ComponentData
|
|
295
|
+
const parsed = parseAgentComponentData(data);
|
|
296
|
+
if (!parsed) {
|
|
297
|
+
logError("agent select: failed to parse component data");
|
|
298
|
+
try {
|
|
299
|
+
await interaction.reply({
|
|
300
|
+
content: "This select menu is no longer valid.",
|
|
301
|
+
ephemeral: true,
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
catch {
|
|
305
|
+
// Interaction may have expired
|
|
306
|
+
}
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
const { componentId } = parsed;
|
|
310
|
+
// Use interaction's actual channel_id (trusted source from Discord)
|
|
311
|
+
// This prevents channel spoofing attacks
|
|
312
|
+
const channelId = interaction.rawData.channel_id;
|
|
313
|
+
if (!channelId) {
|
|
314
|
+
logError("agent select: missing channel_id in interaction");
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
const user = interaction.user;
|
|
318
|
+
if (!user) {
|
|
319
|
+
logError("agent select: missing user in interaction");
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
const username = formatUsername(user);
|
|
323
|
+
const userId = user.id;
|
|
324
|
+
// P1 FIX: Use rawData.guild_id as source of truth - interaction.guild can be null
|
|
325
|
+
// when guild is not cached even though guild_id is present in rawData
|
|
326
|
+
const rawGuildId = interaction.rawData.guild_id;
|
|
327
|
+
const isDirectMessage = !rawGuildId;
|
|
328
|
+
if (isDirectMessage) {
|
|
329
|
+
const authorized = await ensureDmComponentAuthorized({
|
|
330
|
+
ctx: this.ctx,
|
|
331
|
+
interaction,
|
|
332
|
+
user,
|
|
333
|
+
componentLabel: "select menu",
|
|
334
|
+
});
|
|
335
|
+
if (!authorized) {
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
// Check user allowlist before processing component interaction
|
|
340
|
+
const guild = interaction.guild;
|
|
341
|
+
const guildInfo = resolveDiscordGuildEntry({
|
|
342
|
+
guild: guild ?? undefined,
|
|
343
|
+
guildEntries: this.ctx.guildEntries,
|
|
344
|
+
});
|
|
345
|
+
// Resolve channel info for thread detection and allowlist inheritance
|
|
346
|
+
const channel = interaction.channel;
|
|
347
|
+
const channelName = channel && "name" in channel ? channel.name : undefined;
|
|
348
|
+
const channelSlug = channelName ? normalizeDiscordSlug(channelName) : "";
|
|
349
|
+
const channelType = channel && "type" in channel ? channel.type : undefined;
|
|
350
|
+
const isThread = isThreadChannelType(channelType);
|
|
351
|
+
// Resolve thread parent for allowlist inheritance
|
|
352
|
+
let parentId;
|
|
353
|
+
let parentName;
|
|
354
|
+
let parentSlug = "";
|
|
355
|
+
if (isThread && channel && "parentId" in channel) {
|
|
356
|
+
parentId = channel.parentId ?? undefined;
|
|
357
|
+
// Try to get parent name from channel's parent if available
|
|
358
|
+
if ("parent" in channel) {
|
|
359
|
+
const parent = channel.parent;
|
|
360
|
+
if (parent?.name) {
|
|
361
|
+
parentName = parent.name;
|
|
362
|
+
parentSlug = normalizeDiscordSlug(parentName);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
// Only check guild allowlists if this is a guild interaction
|
|
367
|
+
if (rawGuildId) {
|
|
368
|
+
const channelConfig = resolveDiscordChannelConfigWithFallback({
|
|
369
|
+
guildInfo,
|
|
370
|
+
channelId,
|
|
371
|
+
channelName,
|
|
372
|
+
channelSlug,
|
|
373
|
+
parentId,
|
|
374
|
+
parentName,
|
|
375
|
+
parentSlug,
|
|
376
|
+
scope: isThread ? "thread" : "channel",
|
|
377
|
+
});
|
|
378
|
+
const channelUsers = channelConfig?.users ?? guildInfo?.users;
|
|
379
|
+
if (Array.isArray(channelUsers) && channelUsers.length > 0) {
|
|
380
|
+
const userOk = resolveDiscordUserAllowed({
|
|
381
|
+
allowList: channelUsers,
|
|
382
|
+
userId,
|
|
383
|
+
userName: user.username,
|
|
384
|
+
userTag: user.discriminator ? `${user.username}#${user.discriminator}` : undefined,
|
|
385
|
+
});
|
|
386
|
+
if (!userOk) {
|
|
387
|
+
logVerbose(`agent select: blocked user ${userId} (not in allowlist)`);
|
|
388
|
+
try {
|
|
389
|
+
await interaction.reply({
|
|
390
|
+
content: "You are not authorized to use this select menu.",
|
|
391
|
+
ephemeral: true,
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
catch {
|
|
395
|
+
// Interaction may have expired
|
|
396
|
+
}
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
// Extract selected values
|
|
402
|
+
const values = interaction.values ?? [];
|
|
403
|
+
const valuesText = values.length > 0 ? ` (selected: ${values.join(", ")})` : "";
|
|
404
|
+
// Resolve route with full context (guildId, proper peer kind)
|
|
405
|
+
const route = resolveAgentRoute({
|
|
406
|
+
cfg: this.ctx.cfg,
|
|
407
|
+
channel: "discord",
|
|
408
|
+
accountId: this.ctx.accountId,
|
|
409
|
+
guildId: rawGuildId,
|
|
410
|
+
peer: {
|
|
411
|
+
kind: isDirectMessage ? "dm" : "channel",
|
|
412
|
+
id: isDirectMessage ? userId : channelId,
|
|
413
|
+
},
|
|
414
|
+
});
|
|
415
|
+
const eventText = `[Discord select menu: ${componentId} interacted by ${username} (${userId})${valuesText}]`;
|
|
416
|
+
logDebug(`agent select: enqueuing event for channel ${channelId}: ${eventText}`);
|
|
417
|
+
enqueueSystemEvent(eventText, {
|
|
418
|
+
sessionKey: route.sessionKey,
|
|
419
|
+
contextKey: `discord:agent-select:${channelId}:${componentId}:${userId}`,
|
|
420
|
+
});
|
|
421
|
+
// Acknowledge the interaction
|
|
422
|
+
try {
|
|
423
|
+
await interaction.reply({
|
|
424
|
+
content: "✓",
|
|
425
|
+
ephemeral: true,
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
catch (err) {
|
|
429
|
+
logError(`agent select: failed to acknowledge interaction: ${String(err)}`);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
export function createAgentComponentButton(ctx) {
|
|
434
|
+
return new AgentComponentButton(ctx);
|
|
435
|
+
}
|
|
436
|
+
export function createAgentSelectMenu(ctx) {
|
|
437
|
+
return new AgentSelectMenu(ctx);
|
|
438
|
+
}
|
|
@@ -68,7 +68,7 @@ export function resolveDiscordAllowListMatch(params) {
|
|
|
68
68
|
return { allowed: false };
|
|
69
69
|
}
|
|
70
70
|
export function resolveDiscordUserAllowed(params) {
|
|
71
|
-
const allowList = normalizeDiscordAllowList(params.allowList, ["discord:", "user:"]);
|
|
71
|
+
const allowList = normalizeDiscordAllowList(params.allowList, ["discord:", "user:", "pk:"]);
|
|
72
72
|
if (!allowList)
|
|
73
73
|
return true;
|
|
74
74
|
return allowListMatches(allowList, {
|
|
@@ -80,7 +80,7 @@ export function resolveDiscordUserAllowed(params) {
|
|
|
80
80
|
export function resolveDiscordCommandAuthorized(params) {
|
|
81
81
|
if (!params.isDirectMessage)
|
|
82
82
|
return true;
|
|
83
|
-
const allowList = normalizeDiscordAllowList(params.allowFrom, ["discord:", "user:"]);
|
|
83
|
+
const allowList = normalizeDiscordAllowList(params.allowFrom, ["discord:", "user:", "pk:"]);
|
|
84
84
|
if (!allowList)
|
|
85
85
|
return true;
|
|
86
86
|
return allowListMatches(allowList, {
|
|
@@ -89,6 +89,28 @@ export function resolveDiscordCommandAuthorized(params) {
|
|
|
89
89
|
tag: formatDiscordUserTag(params.author),
|
|
90
90
|
});
|
|
91
91
|
}
|
|
92
|
+
export function resolveDiscordOwnerAllowFrom(params) {
|
|
93
|
+
const rawAllowList = params.channelConfig?.users ?? params.guildInfo?.users;
|
|
94
|
+
if (!Array.isArray(rawAllowList) || rawAllowList.length === 0) {
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
const allowList = normalizeDiscordAllowList(rawAllowList, ["discord:", "user:", "pk:"]);
|
|
98
|
+
if (!allowList) {
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
const match = resolveDiscordAllowListMatch({
|
|
102
|
+
allowList,
|
|
103
|
+
candidate: {
|
|
104
|
+
id: params.sender.id,
|
|
105
|
+
name: params.sender.name,
|
|
106
|
+
tag: params.sender.tag,
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
if (!match.allowed || !match.matchKey || match.matchKey === "*") {
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
return [match.matchKey];
|
|
113
|
+
}
|
|
92
114
|
export function resolveDiscordGuildEntry(params) {
|
|
93
115
|
const guild = params.guild;
|
|
94
116
|
const entries = params.guildEntries;
|
|
@@ -128,6 +150,7 @@ function resolveDiscordChannelConfigEntry(entry) {
|
|
|
128
150
|
enabled: entry.enabled,
|
|
129
151
|
users: entry.users,
|
|
130
152
|
systemPrompt: entry.systemPrompt,
|
|
153
|
+
includeThreadStarter: entry.includeThreadStarter,
|
|
131
154
|
autoThread: entry.autoThread,
|
|
132
155
|
};
|
|
133
156
|
return resolved;
|
|
@@ -199,13 +222,13 @@ export function resolveGroupDmAllow(params) {
|
|
|
199
222
|
const { channels, channelId, channelName, channelSlug } = params;
|
|
200
223
|
if (!channels || channels.length === 0)
|
|
201
224
|
return true;
|
|
202
|
-
const allowList = channels.map((entry) => normalizeDiscordSlug(String(entry)));
|
|
225
|
+
const allowList = new Set(channels.map((entry) => normalizeDiscordSlug(String(entry))));
|
|
203
226
|
const candidates = [
|
|
204
227
|
normalizeDiscordSlug(channelId),
|
|
205
228
|
channelSlug,
|
|
206
229
|
channelName ? normalizeDiscordSlug(channelName) : "",
|
|
207
230
|
].filter(Boolean);
|
|
208
|
-
return allowList.
|
|
231
|
+
return allowList.has("*") || candidates.some((candidate) => allowList.has(candidate));
|
|
209
232
|
}
|
|
210
233
|
export function shouldEmitDiscordReactionNotification(params) {
|
|
211
234
|
const mode = params.mode ?? "own";
|
|
@@ -217,7 +240,7 @@ export function shouldEmitDiscordReactionNotification(params) {
|
|
|
217
240
|
return Boolean(params.botId && params.messageAuthorId === params.botId);
|
|
218
241
|
}
|
|
219
242
|
if (mode === "allowlist") {
|
|
220
|
-
const list = normalizeDiscordAllowList(params.allowlist, ["discord:", "user:"]);
|
|
243
|
+
const list = normalizeDiscordAllowList(params.allowlist, ["discord:", "user:", "pk:"]);
|
|
221
244
|
if (!list)
|
|
222
245
|
return false;
|
|
223
246
|
return allowListMatches(list, {
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module-level registry of active Discord GatewayPlugin instances.
|
|
3
|
+
* Bridges the gap between agent tool handlers (which only have REST access)
|
|
4
|
+
* and the gateway WebSocket (needed for operations like updatePresence).
|
|
5
|
+
* Follows the same pattern as presence-cache.ts.
|
|
6
|
+
*/
|
|
7
|
+
const gatewayRegistry = new Map();
|
|
8
|
+
// Sentinel key for the default (unnamed) account. Uses a prefix that cannot
|
|
9
|
+
// collide with user-configured account IDs.
|
|
10
|
+
const DEFAULT_ACCOUNT_KEY = "\0__default__";
|
|
11
|
+
function resolveAccountKey(accountId) {
|
|
12
|
+
return accountId ?? DEFAULT_ACCOUNT_KEY;
|
|
13
|
+
}
|
|
14
|
+
/** Register a GatewayPlugin instance for an account. */
|
|
15
|
+
export function registerGateway(accountId, gateway) {
|
|
16
|
+
gatewayRegistry.set(resolveAccountKey(accountId), gateway);
|
|
17
|
+
}
|
|
18
|
+
/** Unregister a GatewayPlugin instance for an account. */
|
|
19
|
+
export function unregisterGateway(accountId) {
|
|
20
|
+
gatewayRegistry.delete(resolveAccountKey(accountId));
|
|
21
|
+
}
|
|
22
|
+
/** Get the GatewayPlugin for an account. Returns undefined if not registered. */
|
|
23
|
+
export function getGateway(accountId) {
|
|
24
|
+
return gatewayRegistry.get(resolveAccountKey(accountId));
|
|
25
|
+
}
|
|
26
|
+
/** Clear all registered gateways (for testing). */
|
|
27
|
+
export function clearGateways() {
|
|
28
|
+
gatewayRegistry.clear();
|
|
29
|
+
}
|