@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.
Files changed (258) hide show
  1. package/CHANGELOG.md +118 -0
  2. package/README-header.png +0 -0
  3. package/dist/agents/bash-tools.exec.js +76 -25
  4. package/dist/agents/cli-runner/helpers.js +9 -11
  5. package/dist/agents/context.js +1 -1
  6. package/dist/agents/identity.js +47 -7
  7. package/dist/agents/memory-search.js +25 -8
  8. package/dist/agents/model-catalog.js +1 -1
  9. package/dist/agents/model-selection.js +21 -0
  10. package/dist/agents/pi-embedded-block-chunker.js +117 -42
  11. package/dist/agents/pi-embedded-helpers/errors.js +183 -78
  12. package/dist/agents/pi-embedded-helpers.js +1 -1
  13. package/dist/agents/pi-embedded-runner/compact.js +8 -10
  14. package/dist/agents/pi-embedded-runner/model.js +62 -3
  15. package/dist/agents/pi-embedded-runner/run/attempt.js +21 -11
  16. package/dist/agents/pi-embedded-runner/run.js +199 -46
  17. package/dist/agents/pi-embedded-runner/system-prompt.js +10 -2
  18. package/dist/agents/pi-embedded-subscribe.js +118 -29
  19. package/dist/agents/pi-tools.js +10 -5
  20. package/dist/agents/poolbot-tools.js +15 -10
  21. package/dist/agents/sandbox-paths.js +31 -0
  22. package/dist/agents/session-tool-result-guard.js +94 -15
  23. package/dist/agents/shell-utils.js +51 -0
  24. package/dist/agents/skills/bundled-context.js +23 -0
  25. package/dist/agents/skills/bundled-dir.js +41 -7
  26. package/dist/agents/skills-install.js +60 -23
  27. package/dist/agents/subagent-announce.js +79 -34
  28. package/dist/agents/tool-policy.conformance.js +14 -0
  29. package/dist/agents/tool-policy.js +24 -0
  30. package/dist/agents/tools/cron-tool.js +166 -19
  31. package/dist/agents/tools/discord-actions-presence.js +78 -0
  32. package/dist/agents/tools/image-tool.js +1 -1
  33. package/dist/agents/tools/message-tool.js +56 -2
  34. package/dist/agents/tools/sessions-history-tool.js +69 -1
  35. package/dist/agents/tools/web-search.js +211 -42
  36. package/dist/agents/usage.js +23 -1
  37. package/dist/agents/workspace-run.js +67 -0
  38. package/dist/agents/workspace-templates.js +44 -0
  39. package/dist/auto-reply/command-auth.js +121 -6
  40. package/dist/auto-reply/envelope.js +74 -82
  41. package/dist/auto-reply/reply/commands-compact.js +1 -0
  42. package/dist/auto-reply/reply/commands-context-report.js +1 -0
  43. package/dist/auto-reply/reply/commands-context.js +1 -0
  44. package/dist/auto-reply/reply/commands-models.js +107 -60
  45. package/dist/auto-reply/reply/commands-ptt.js +171 -0
  46. package/dist/auto-reply/reply/get-reply-run.js +2 -1
  47. package/dist/auto-reply/reply/inbound-context.js +5 -1
  48. package/dist/auto-reply/reply/mentions.js +1 -1
  49. package/dist/auto-reply/reply/model-selection.js +3 -3
  50. package/dist/auto-reply/thinking.js +88 -43
  51. package/dist/browser/bridge-server.js +13 -0
  52. package/dist/browser/cdp.helpers.js +38 -24
  53. package/dist/browser/client-fetch.js +50 -7
  54. package/dist/browser/config.js +1 -10
  55. package/dist/browser/extension-relay.js +101 -40
  56. package/dist/browser/pw-ai.js +1 -1
  57. package/dist/browser/pw-session.js +143 -8
  58. package/dist/browser/pw-tools-core.interactions.js +125 -27
  59. package/dist/browser/pw-tools-core.responses.js +1 -1
  60. package/dist/browser/pw-tools-core.state.js +1 -1
  61. package/dist/browser/routes/agent.act.js +86 -41
  62. package/dist/browser/routes/dispatcher.js +4 -4
  63. package/dist/browser/screenshot.js +1 -1
  64. package/dist/browser/server.js +13 -0
  65. package/dist/build-info.json +3 -3
  66. package/dist/canvas-host/a2ui/index.html +28 -28
  67. package/dist/channels/reply-prefix.js +8 -1
  68. package/dist/cli/cron-cli/register.cron-add.js +61 -40
  69. package/dist/cli/cron-cli/register.cron-edit.js +60 -34
  70. package/dist/cli/cron-cli/shared.js +56 -41
  71. package/dist/cli/dns-cli.js +26 -14
  72. package/dist/cli/gateway-cli/register.js +37 -19
  73. package/dist/cli/memory-cli.js +5 -5
  74. package/dist/cli/parse-bytes.js +37 -0
  75. package/dist/cli/update-cli.js +173 -52
  76. package/dist/commands/agent.js +1 -0
  77. package/dist/commands/auth-choice.apply.oauth.js +1 -1
  78. package/dist/commands/doctor-config-flow.js +61 -5
  79. package/dist/commands/doctor-state-migrations.js +1 -1
  80. package/dist/commands/health.js +1 -1
  81. package/dist/commands/model-allowlist.js +29 -0
  82. package/dist/commands/model-picker.js +2 -1
  83. package/dist/commands/models/list.registry.js +1 -1
  84. package/dist/commands/models/list.status-command.js +43 -23
  85. package/dist/commands/models/shared.js +15 -0
  86. package/dist/commands/onboard-custom.js +384 -0
  87. package/dist/commands/onboard-non-interactive/local/auth-choice-inference.js +35 -0
  88. package/dist/commands/onboard-non-interactive/local/auth-choice.js +6 -3
  89. package/dist/commands/onboard-skills.js +63 -38
  90. package/dist/commands/openai-model-default.js +41 -0
  91. package/dist/compat/legacy-names.js +2 -0
  92. package/dist/config/defaults.js +3 -2
  93. package/dist/config/paths.js +136 -35
  94. package/dist/config/plugin-auto-enable.js +21 -5
  95. package/dist/config/redact-snapshot.js +153 -0
  96. package/dist/config/schema.field-metadata.js +590 -0
  97. package/dist/config/schema.js +2 -2
  98. package/dist/config/sessions/store.js +291 -23
  99. package/dist/config/zod-schema.agent-defaults.js +3 -0
  100. package/dist/config/zod-schema.agent-runtime.js +13 -2
  101. package/dist/config/zod-schema.providers-core.js +142 -0
  102. package/dist/config/zod-schema.session.js +3 -0
  103. package/dist/control-ui/assets/{index-CIRDm-Lu.css → index-CSfXd2LO.css} +1 -1
  104. package/dist/control-ui/assets/{index-CmNMuoem.js → index-HRr1grwl.js} +446 -413
  105. package/dist/control-ui/assets/index-HRr1grwl.js.map +1 -0
  106. package/dist/control-ui/index.html +4 -4
  107. package/dist/cron/delivery.js +57 -0
  108. package/dist/cron/isolated-agent/delivery-target.js +18 -3
  109. package/dist/cron/isolated-agent/helpers.js +22 -5
  110. package/dist/cron/isolated-agent/run.js +172 -63
  111. package/dist/cron/isolated-agent/session.js +2 -0
  112. package/dist/cron/normalize.js +356 -28
  113. package/dist/cron/parse.js +10 -5
  114. package/dist/cron/run-log.js +35 -10
  115. package/dist/cron/schedule.js +41 -6
  116. package/dist/cron/service/jobs.js +208 -35
  117. package/dist/cron/service/ops.js +72 -16
  118. package/dist/cron/service/state.js +2 -0
  119. package/dist/cron/service/store.js +386 -14
  120. package/dist/cron/service/timer.js +390 -147
  121. package/dist/cron/session-reaper.js +86 -0
  122. package/dist/cron/store.js +23 -8
  123. package/dist/cron/validate-timestamp.js +43 -0
  124. package/dist/discord/monitor/agent-components.js +438 -0
  125. package/dist/discord/monitor/allow-list.js +28 -5
  126. package/dist/discord/monitor/gateway-registry.js +29 -0
  127. package/dist/discord/monitor/native-command.js +44 -23
  128. package/dist/discord/monitor/sender-identity.js +45 -0
  129. package/dist/discord/pluralkit.js +27 -0
  130. package/dist/discord/send.outbound.js +92 -5
  131. package/dist/discord/send.shared.js +60 -23
  132. package/dist/discord/targets.js +84 -1
  133. package/dist/entry.js +15 -9
  134. package/dist/extensionAPI.js +8 -0
  135. package/dist/gateway/control-ui.js +8 -1
  136. package/dist/gateway/hooks-mapping.js +3 -0
  137. package/dist/gateway/hooks.js +65 -0
  138. package/dist/gateway/net.js +96 -31
  139. package/dist/gateway/node-command-policy.js +50 -15
  140. package/dist/gateway/origin-check.js +56 -0
  141. package/dist/gateway/protocol/client-info.js +9 -0
  142. package/dist/gateway/protocol/index.js +9 -2
  143. package/dist/gateway/protocol/schema/agents-models-skills.js +71 -1
  144. package/dist/gateway/protocol/schema/cron.js +22 -10
  145. package/dist/gateway/protocol/schema/protocol-schemas.js +16 -2
  146. package/dist/gateway/protocol/schema/sessions.js +12 -0
  147. package/dist/gateway/server/hooks.js +1 -1
  148. package/dist/gateway/server-broadcast.js +26 -9
  149. package/dist/gateway/server-chat.js +112 -23
  150. package/dist/gateway/server-discovery-runtime.js +10 -2
  151. package/dist/gateway/server-http.js +109 -11
  152. package/dist/gateway/server-methods/agent-timestamp.js +60 -0
  153. package/dist/gateway/server-methods/agents.js +321 -2
  154. package/dist/gateway/server-methods/usage.js +559 -16
  155. package/dist/gateway/server-runtime-state.js +22 -8
  156. package/dist/gateway/server-startup-memory.js +16 -0
  157. package/dist/gateway/server.impl.js +5 -1
  158. package/dist/gateway/session-utils.fs.js +23 -25
  159. package/dist/gateway/session-utils.js +20 -10
  160. package/dist/gateway/sessions-patch.js +7 -22
  161. package/dist/gateway/test-helpers.mocks.js +11 -7
  162. package/dist/gateway/test-helpers.server.js +35 -2
  163. package/dist/imessage/constants.js +2 -0
  164. package/dist/imessage/monitor/deliver.js +4 -1
  165. package/dist/imessage/monitor/monitor-provider.js +51 -1
  166. package/dist/infra/bonjour-discovery.js +131 -70
  167. package/dist/infra/control-ui-assets.js +134 -12
  168. package/dist/infra/errors.js +12 -0
  169. package/dist/infra/exec-approvals.js +266 -57
  170. package/dist/infra/format-time/format-datetime.js +79 -0
  171. package/dist/infra/format-time/format-duration.js +81 -0
  172. package/dist/infra/format-time/format-relative.js +80 -0
  173. package/dist/infra/heartbeat-runner.js +140 -49
  174. package/dist/infra/home-dir.js +54 -0
  175. package/dist/infra/net/fetch-guard.js +122 -0
  176. package/dist/infra/net/ssrf.js +65 -29
  177. package/dist/infra/outbound/abort.js +14 -0
  178. package/dist/infra/outbound/message-action-runner.js +77 -13
  179. package/dist/infra/outbound/outbound-session.js +143 -37
  180. package/dist/infra/poolbot-root.js +43 -1
  181. package/dist/infra/session-cost-usage.js +631 -41
  182. package/dist/infra/state-migrations.js +317 -47
  183. package/dist/infra/update-global.js +35 -0
  184. package/dist/infra/update-runner.js +149 -43
  185. package/dist/infra/warning-filter.js +65 -0
  186. package/dist/infra/widearea-dns.js +30 -9
  187. package/dist/logging/redact-identifier.js +12 -0
  188. package/dist/media/fetch.js +81 -58
  189. package/dist/media/store.js +2 -0
  190. package/dist/media-understanding/apply.js +403 -3
  191. package/dist/media-understanding/attachments.js +38 -27
  192. package/dist/media-understanding/defaults.js +16 -0
  193. package/dist/media-understanding/providers/deepgram/audio.js +22 -14
  194. package/dist/media-understanding/providers/google/audio.js +24 -17
  195. package/dist/media-understanding/providers/google/video.js +24 -17
  196. package/dist/media-understanding/providers/image.js +3 -3
  197. package/dist/media-understanding/providers/index.js +4 -1
  198. package/dist/media-understanding/providers/openai/audio.js +22 -14
  199. package/dist/media-understanding/providers/shared.js +16 -11
  200. package/dist/media-understanding/providers/zai/index.js +6 -0
  201. package/dist/media-understanding/runner.js +158 -90
  202. package/dist/memory/batch-voyage.js +277 -0
  203. package/dist/memory/embeddings-voyage.js +75 -0
  204. package/dist/memory/embeddings.js +28 -16
  205. package/dist/memory/internal.js +101 -18
  206. package/dist/memory/manager.js +154 -48
  207. package/dist/memory/search-manager.js +173 -0
  208. package/dist/memory/session-files.js +9 -3
  209. package/dist/node-host/runner.js +34 -24
  210. package/dist/node-host/with-timeout.js +27 -0
  211. package/dist/plugins/commands.js +5 -1
  212. package/dist/plugins/config-state.js +86 -7
  213. package/dist/plugins/source-display.js +51 -0
  214. package/dist/process/exec.js +20 -2
  215. package/dist/routing/resolve-route.js +12 -0
  216. package/dist/routing/session-key.js +15 -0
  217. package/dist/runtime.js +2 -0
  218. package/dist/security/audit-extra.async.js +601 -0
  219. package/dist/security/audit-extra.js +2 -830
  220. package/dist/security/audit-extra.sync.js +505 -0
  221. package/dist/security/channel-metadata.js +34 -0
  222. package/dist/security/external-content.js +88 -6
  223. package/dist/security/skill-scanner.js +330 -0
  224. package/dist/sessions/session-key-utils.js +7 -0
  225. package/dist/signal/monitor/event-handler.js +80 -1
  226. package/dist/slack/monitor/media.js +85 -15
  227. package/dist/tailscale/detect.js +1 -2
  228. package/dist/telegram/bot/helpers.js +109 -28
  229. package/dist/telegram/bot-handlers.js +144 -3
  230. package/dist/telegram/bot-message-context.js +37 -10
  231. package/dist/telegram/bot-message-dispatch.js +54 -17
  232. package/dist/telegram/bot-native-commands.js +86 -29
  233. package/dist/telegram/bot.js +30 -29
  234. package/dist/telegram/model-buttons.js +163 -0
  235. package/dist/telegram/monitor.js +110 -85
  236. package/dist/telegram/send.js +129 -47
  237. package/dist/terminal/restore.js +45 -0
  238. package/dist/test-helpers/state-dir-env.js +16 -0
  239. package/dist/tts/tts.js +12 -6
  240. package/dist/tui/tui-session-actions.js +166 -54
  241. package/dist/utils/fetch-timeout.js +20 -0
  242. package/dist/utils/normalize-secret-input.js +19 -0
  243. package/dist/utils/transcript-tools.js +58 -0
  244. package/dist/utils.js +45 -14
  245. package/dist/version.js +42 -5
  246. package/dist/wizard/clack-prompter.js +9 -6
  247. package/extensions/googlechat/node_modules/.bin/poolbot +21 -0
  248. package/extensions/googlechat/package.json +2 -2
  249. package/extensions/line/node_modules/.bin/poolbot +21 -0
  250. package/extensions/line/package.json +1 -1
  251. package/extensions/matrix/node_modules/.bin/poolbot +21 -0
  252. package/extensions/matrix/package.json +1 -1
  253. package/extensions/memory-core/node_modules/.bin/poolbot +21 -0
  254. package/extensions/memory-core/package.json +4 -1
  255. package/extensions/twitch/node_modules/.bin/poolbot +21 -0
  256. package/extensions/twitch/package.json +1 -1
  257. package/package.json +183 -24
  258. package/dist/control-ui/assets/index-CmNMuoem.js.map +0 -1
@@ -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 JSON5 from "json5";
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.replace("~", os.homedir()));
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
- const parsed = JSON5.parse(raw);
21
- const jobs = Array.isArray(parsed?.jobs) ? parsed?.jobs : [];
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
- return { version: 1, jobs: [] };
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.includes("*") || candidates.some((candidate) => allowList.includes(candidate));
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
+ }