@poolzin/pool-bot 2026.2.23 → 2026.2.25

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 (235) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/acp/client.js +207 -18
  3. package/dist/acp/secret-file.js +22 -0
  4. package/dist/agents/agent-scope.js +10 -0
  5. package/dist/agents/bash-process-registry.test-helpers.js +29 -0
  6. package/dist/agents/bash-tools.exec-approval-request.js +20 -0
  7. package/dist/agents/bash-tools.exec-host-gateway.js +230 -0
  8. package/dist/agents/bash-tools.exec-host-node.js +235 -0
  9. package/dist/agents/bash-tools.exec-types.js +1 -0
  10. package/dist/agents/bash-tools.process.js +224 -218
  11. package/dist/agents/content-blocks.js +16 -0
  12. package/dist/agents/model-fallback.js +96 -101
  13. package/dist/agents/models-config.providers.js +299 -182
  14. package/dist/agents/pi-embedded-payloads.js +1 -0
  15. package/dist/agents/pi-embedded-runner/run.overflow-compaction.fixture.js +34 -0
  16. package/dist/agents/skills.test-helpers.js +13 -0
  17. package/dist/agents/stable-stringify.js +12 -0
  18. package/dist/agents/subagent-registry.mocks.shared.js +12 -0
  19. package/dist/agents/test-helpers/assistant-message-fixtures.js +29 -0
  20. package/dist/agents/test-helpers/pi-tools-sandbox-context.js +27 -0
  21. package/dist/agents/tool-policy-shared.js +108 -0
  22. package/dist/agents/tools/browser-tool.js +160 -54
  23. package/dist/agents/tools/cron-tool.test-helpers.js +12 -0
  24. package/dist/agents/tools/discord-actions-moderation-shared.js +27 -0
  25. package/dist/agents/tools/image-tool.js +214 -99
  26. package/dist/agents/tools/sessions-history-tool.js +140 -108
  27. package/dist/agents/workspace.js +222 -46
  28. package/dist/auto-reply/commands-registry.js +15 -18
  29. package/dist/auto-reply/fallback-state.js +114 -0
  30. package/dist/auto-reply/model-runtime.js +68 -0
  31. package/dist/auto-reply/reply/agent-runner-execution.js +36 -4
  32. package/dist/auto-reply/reply/agent-runner.js +165 -39
  33. package/dist/auto-reply/reply/commands-setunset-standard.js +13 -0
  34. package/dist/browser/config.js +26 -0
  35. package/dist/browser/navigation-guard.js +31 -0
  36. package/dist/browser/routes/agent.act.js +431 -424
  37. package/dist/browser/routes/agent.shared.js +47 -3
  38. package/dist/browser/routes/agent.snapshot.js +122 -116
  39. package/dist/browser/routes/agent.storage.js +303 -297
  40. package/dist/browser/routes/tabs.js +154 -100
  41. package/dist/browser/server-lifecycle.js +37 -0
  42. package/dist/build-info.json +3 -3
  43. package/dist/channels/allow-from.js +25 -0
  44. package/dist/channels/plugins/account-action-gate.js +13 -0
  45. package/dist/channels/plugins/message-actions.js +10 -0
  46. package/dist/channels/telegram/api.js +18 -0
  47. package/dist/cli/argv.js +84 -21
  48. package/dist/cli/banner.js +2 -1
  49. package/dist/cli/exec-approvals-cli.js +92 -124
  50. package/dist/cli/memory-cli.js +158 -61
  51. package/dist/cli/nodes-cli/register.push.js +63 -0
  52. package/dist/cli/nodes-media-utils.js +21 -0
  53. package/dist/cli/plugins-cli.js +245 -61
  54. package/dist/cli/program/build-program.js +3 -1
  55. package/dist/cli/program/command-registry.js +223 -136
  56. package/dist/cli/program/help.js +43 -12
  57. package/dist/cli/route.js +1 -1
  58. package/dist/cli/test-runtime-capture.js +24 -0
  59. package/dist/commands/agent.js +163 -87
  60. package/dist/commands/channels.mock-harness.js +23 -0
  61. package/dist/commands/daemon-install-runtime-warning.js +11 -0
  62. package/dist/commands/onboard-helpers.js +4 -4
  63. package/dist/commands/sessions.test-helpers.js +61 -0
  64. package/dist/compat/legacy-names.js +2 -2
  65. package/dist/config/commands.js +3 -0
  66. package/dist/config/config.js +1 -1
  67. package/dist/config/env-substitution.js +62 -34
  68. package/dist/config/env-vars.js +9 -0
  69. package/dist/config/io.js +571 -171
  70. package/dist/config/merge-patch.js +50 -4
  71. package/dist/config/redact-snapshot.js +404 -76
  72. package/dist/config/schema.js +58 -570
  73. package/dist/config/validation.js +140 -85
  74. package/dist/config/zod-schema.hooks.js +40 -11
  75. package/dist/config/zod-schema.installs.js +20 -0
  76. package/dist/config/zod-schema.js +8 -7
  77. package/dist/control-ui/assets/{index-HRr1grwl.js → index-Dvkl4Xlx.js} +2 -1
  78. package/dist/control-ui/assets/{index-HRr1grwl.js.map → index-Dvkl4Xlx.js.map} +1 -1
  79. package/dist/control-ui/index.html +1 -1
  80. package/dist/daemon/cmd-argv.js +21 -0
  81. package/dist/daemon/cmd-set.js +58 -0
  82. package/dist/daemon/service-types.js +1 -0
  83. package/dist/discord/monitor/exec-approvals.js +357 -162
  84. package/dist/gateway/auth.js +38 -3
  85. package/dist/gateway/call.js +149 -68
  86. package/dist/gateway/canvas-capability.js +75 -0
  87. package/dist/gateway/control-plane-audit.js +28 -0
  88. package/dist/gateway/control-plane-rate-limit.js +53 -0
  89. package/dist/gateway/events.js +1 -0
  90. package/dist/gateway/hooks.js +109 -54
  91. package/dist/gateway/http-common.js +22 -0
  92. package/dist/gateway/method-scopes.js +169 -0
  93. package/dist/gateway/net.js +23 -0
  94. package/dist/gateway/openresponses-http.js +120 -110
  95. package/dist/gateway/probe-auth.js +2 -0
  96. package/dist/gateway/protocol/index.js +3 -2
  97. package/dist/gateway/protocol/schema/protocol-schemas.js +2 -0
  98. package/dist/gateway/protocol/schema/push.js +18 -0
  99. package/dist/gateway/protocol/schema.js +1 -0
  100. package/dist/gateway/server-http.js +236 -52
  101. package/dist/gateway/server-methods/agent.js +162 -24
  102. package/dist/gateway/server-methods/chat.js +461 -130
  103. package/dist/gateway/server-methods/config.js +193 -150
  104. package/dist/gateway/server-methods/nodes.helpers.js +12 -0
  105. package/dist/gateway/server-methods/nodes.js +251 -69
  106. package/dist/gateway/server-methods/push.js +53 -0
  107. package/dist/gateway/server-reload-handlers.js +2 -3
  108. package/dist/gateway/server-runtime-config.js +5 -0
  109. package/dist/gateway/server-runtime-state.js +2 -0
  110. package/dist/gateway/server-ws-runtime.js +1 -0
  111. package/dist/gateway/server.impl.js +296 -139
  112. package/dist/gateway/session-preview.test-helpers.js +11 -0
  113. package/dist/gateway/startup-auth.js +126 -0
  114. package/dist/gateway/test-helpers.agent-results.js +15 -0
  115. package/dist/gateway/test-helpers.mocks.js +37 -14
  116. package/dist/gateway/test-helpers.server.js +161 -77
  117. package/dist/hooks/bundled/session-memory/handler.js +165 -34
  118. package/dist/hooks/gmail-watcher-lifecycle.js +23 -0
  119. package/dist/infra/archive-path.js +49 -0
  120. package/dist/infra/device-pairing.js +148 -167
  121. package/dist/infra/exec-approvals-allowlist.js +19 -70
  122. package/dist/infra/exec-approvals-analysis.js +44 -17
  123. package/dist/infra/exec-safe-bin-policy.js +269 -0
  124. package/dist/infra/fixed-window-rate-limit.js +33 -0
  125. package/dist/infra/git-root.js +61 -0
  126. package/dist/infra/heartbeat-active-hours.js +2 -2
  127. package/dist/infra/heartbeat-reason.js +40 -0
  128. package/dist/infra/heartbeat-runner.js +72 -32
  129. package/dist/infra/install-source-utils.js +91 -7
  130. package/dist/infra/node-pairing.js +50 -105
  131. package/dist/infra/npm-integrity.js +45 -0
  132. package/dist/infra/npm-pack-install.js +40 -0
  133. package/dist/infra/outbound/channel-adapters.js +20 -7
  134. package/dist/infra/outbound/message-action-runner.js +107 -327
  135. package/dist/infra/outbound/message.js +59 -36
  136. package/dist/infra/outbound/outbound-policy.js +52 -25
  137. package/dist/infra/outbound/outbound-send-service.js +58 -71
  138. package/dist/infra/pairing-files.js +10 -0
  139. package/dist/infra/plain-object.js +9 -0
  140. package/dist/infra/push-apns.js +365 -0
  141. package/dist/infra/restart-sentinel.js +16 -1
  142. package/dist/infra/restart.js +229 -26
  143. package/dist/infra/scp-host.js +54 -0
  144. package/dist/infra/update-startup.js +86 -9
  145. package/dist/media/inbound-path-policy.js +114 -0
  146. package/dist/media/input-files.js +16 -0
  147. package/dist/memory/test-manager.js +8 -0
  148. package/dist/plugin-sdk/temp-path.js +47 -0
  149. package/dist/plugins/discovery.js +217 -23
  150. package/dist/plugins/hook-runner-global.js +16 -0
  151. package/dist/plugins/loader.js +192 -26
  152. package/dist/plugins/logger.js +8 -0
  153. package/dist/plugins/manifest-registry.js +3 -0
  154. package/dist/plugins/path-safety.js +34 -0
  155. package/dist/plugins/registry.js +5 -2
  156. package/dist/plugins/runtime/index.js +271 -206
  157. package/dist/providers/github-copilot-models.js +4 -1
  158. package/dist/security/audit-channel.js +8 -19
  159. package/dist/security/audit-extra.async.js +354 -182
  160. package/dist/security/audit-extra.js +11 -1
  161. package/dist/security/audit-extra.sync.js +340 -33
  162. package/dist/security/audit-fs.js +31 -13
  163. package/dist/security/audit.js +145 -371
  164. package/dist/security/dm-policy-shared.js +24 -0
  165. package/dist/security/external-content.js +20 -8
  166. package/dist/security/fix.js +49 -85
  167. package/dist/security/scan-paths.js +20 -0
  168. package/dist/security/secret-equal.js +3 -7
  169. package/dist/security/windows-acl.js +30 -15
  170. package/dist/shared/node-list-parse.js +13 -0
  171. package/dist/shared/operator-scope-compat.js +37 -0
  172. package/dist/shared/text-chunking.js +29 -0
  173. package/dist/slack/blocks.test-helpers.js +31 -0
  174. package/dist/slack/monitor/mrkdwn.js +8 -0
  175. package/dist/telegram/bot-message-dispatch.js +366 -164
  176. package/dist/telegram/draft-stream.js +30 -7
  177. package/dist/telegram/reasoning-lane-coordinator.js +128 -0
  178. package/dist/terminal/prompt-select-styled.js +9 -0
  179. package/dist/test-utils/command-runner.js +6 -0
  180. package/dist/test-utils/internal-hook-event-payload.js +10 -0
  181. package/dist/test-utils/model-auth-mock.js +12 -0
  182. package/dist/test-utils/provider-usage-fetch.js +14 -0
  183. package/dist/test-utils/temp-home.js +33 -0
  184. package/dist/tui/components/chat-log.js +9 -0
  185. package/dist/tui/tui-command-handlers.js +36 -27
  186. package/dist/tui/tui-event-handlers.js +122 -32
  187. package/dist/tui/tui.js +181 -45
  188. package/dist/utils/mask-api-key.js +10 -0
  189. package/dist/utils/run-with-concurrency.js +39 -0
  190. package/dist/web/media.js +4 -0
  191. package/docs/tools/slash-commands.md +5 -1
  192. package/extensions/bluebubbles/package.json +1 -1
  193. package/extensions/copilot-proxy/package.json +1 -1
  194. package/extensions/diagnostics-otel/package.json +1 -1
  195. package/extensions/discord/package.json +1 -1
  196. package/extensions/feishu/package.json +1 -1
  197. package/extensions/feishu/src/external-keys.ts +19 -0
  198. package/extensions/google-antigravity-auth/package.json +1 -1
  199. package/extensions/google-gemini-cli-auth/package.json +1 -1
  200. package/extensions/googlechat/package.json +1 -1
  201. package/extensions/imessage/package.json +1 -1
  202. package/extensions/irc/package.json +1 -1
  203. package/extensions/line/package.json +1 -1
  204. package/extensions/llm-task/package.json +1 -1
  205. package/extensions/lobster/package.json +1 -1
  206. package/extensions/lobster/src/windows-spawn.ts +193 -0
  207. package/extensions/matrix/CHANGELOG.md +5 -0
  208. package/extensions/matrix/package.json +1 -1
  209. package/extensions/matrix/src/matrix/actions/limits.ts +6 -0
  210. package/extensions/mattermost/package.json +1 -1
  211. package/extensions/mattermost/src/mattermost/reactions.test-helpers.ts +83 -0
  212. package/extensions/memory-core/package.json +1 -1
  213. package/extensions/memory-lancedb/package.json +1 -1
  214. package/extensions/minimax-portal-auth/package.json +1 -1
  215. package/extensions/msteams/CHANGELOG.md +5 -0
  216. package/extensions/msteams/package.json +1 -1
  217. package/extensions/nextcloud-talk/package.json +1 -1
  218. package/extensions/nostr/CHANGELOG.md +5 -0
  219. package/extensions/nostr/package.json +1 -1
  220. package/extensions/open-prose/package.json +1 -1
  221. package/extensions/openai-codex-auth/package.json +1 -1
  222. package/extensions/signal/package.json +1 -1
  223. package/extensions/slack/package.json +1 -1
  224. package/extensions/telegram/package.json +1 -1
  225. package/extensions/tlon/package.json +1 -1
  226. package/extensions/twitch/CHANGELOG.md +5 -0
  227. package/extensions/twitch/package.json +1 -1
  228. package/extensions/voice-call/CHANGELOG.md +5 -0
  229. package/extensions/voice-call/package.json +1 -1
  230. package/extensions/whatsapp/package.json +1 -1
  231. package/extensions/zalo/CHANGELOG.md +5 -0
  232. package/extensions/zalo/package.json +1 -1
  233. package/extensions/zalouser/CHANGELOG.md +5 -0
  234. package/extensions/zalouser/package.json +1 -1
  235. package/package.json +1 -1
@@ -0,0 +1,47 @@
1
+ import crypto from "node:crypto";
2
+ import { mkdtemp, rm } from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ function sanitizePrefix(prefix) {
6
+ const normalized = prefix.replace(/[^a-zA-Z0-9_-]+/g, "-").replace(/^-+|-+$/g, "");
7
+ return normalized || "tmp";
8
+ }
9
+ function sanitizeExtension(extension) {
10
+ if (!extension) {
11
+ return "";
12
+ }
13
+ const normalized = extension.startsWith(".") ? extension : `.${extension}`;
14
+ const suffix = normalized.match(/[a-zA-Z0-9._-]+$/)?.[0] ?? "";
15
+ const token = suffix.replace(/^[._-]+/, "");
16
+ if (!token) {
17
+ return "";
18
+ }
19
+ return `.${token}`;
20
+ }
21
+ function sanitizeFileName(fileName) {
22
+ const base = path.basename(fileName).replace(/[^a-zA-Z0-9._-]+/g, "-");
23
+ const normalized = base.replace(/^-+|-+$/g, "");
24
+ return normalized || "download.bin";
25
+ }
26
+ export function buildRandomTempFilePath(params) {
27
+ const prefix = sanitizePrefix(params.prefix);
28
+ const extension = sanitizeExtension(params.extension);
29
+ const nowCandidate = params.now;
30
+ const now = typeof nowCandidate === "number" && Number.isFinite(nowCandidate)
31
+ ? Math.trunc(nowCandidate)
32
+ : Date.now();
33
+ const uuid = params.uuid?.trim() || crypto.randomUUID();
34
+ return path.join(params.tmpDir ?? os.tmpdir(), `${prefix}-${now}-${uuid}${extension}`);
35
+ }
36
+ export async function withTempDownloadPath(params, fn) {
37
+ const tempRoot = params.tmpDir ?? os.tmpdir();
38
+ const prefix = `${sanitizePrefix(params.prefix)}-`;
39
+ const dir = await mkdtemp(path.join(tempRoot, prefix));
40
+ const tmpPath = path.join(dir, sanitizeFileName(params.fileName ?? "download.bin"));
41
+ try {
42
+ return await fn(tmpPath);
43
+ }
44
+ finally {
45
+ await rm(dir, { recursive: true, force: true }).catch(() => { });
46
+ }
47
+ }
@@ -1,19 +1,145 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
+ import { isPathInsideWithRealpath } from "../security/scan-paths.js";
3
4
  import { resolveConfigDir, resolveUserPath } from "../utils.js";
4
5
  import { resolveBundledPluginsDir } from "./bundled-dir.js";
5
6
  import { getPackageManifestMetadata, } from "./manifest.js";
7
+ import { formatPosixMode, isPathInside, safeRealpathSync, safeStatSync } from "./path-safety.js";
6
8
  const EXTENSION_EXTS = new Set([".ts", ".js", ".mts", ".cts", ".mjs", ".cjs"]);
9
+ function currentUid(overrideUid) {
10
+ if (overrideUid !== undefined) {
11
+ return overrideUid;
12
+ }
13
+ if (process.platform === "win32") {
14
+ return null;
15
+ }
16
+ if (typeof process.getuid !== "function") {
17
+ return null;
18
+ }
19
+ return process.getuid();
20
+ }
21
+ function checkSourceEscapesRoot(params) {
22
+ const sourceRealPath = safeRealpathSync(params.source);
23
+ const rootRealPath = safeRealpathSync(params.rootDir);
24
+ if (!sourceRealPath || !rootRealPath) {
25
+ return null;
26
+ }
27
+ if (isPathInside(rootRealPath, sourceRealPath)) {
28
+ return null;
29
+ }
30
+ return {
31
+ reason: "source_escapes_root",
32
+ sourcePath: params.source,
33
+ rootPath: params.rootDir,
34
+ targetPath: params.source,
35
+ sourceRealPath,
36
+ rootRealPath,
37
+ };
38
+ }
39
+ function checkPathStatAndPermissions(params) {
40
+ if (process.platform === "win32") {
41
+ return null;
42
+ }
43
+ const pathsToCheck = [params.rootDir, params.source];
44
+ const seen = new Set();
45
+ for (const targetPath of pathsToCheck) {
46
+ const normalized = path.resolve(targetPath);
47
+ if (seen.has(normalized)) {
48
+ continue;
49
+ }
50
+ seen.add(normalized);
51
+ const stat = safeStatSync(targetPath);
52
+ if (!stat) {
53
+ return {
54
+ reason: "path_stat_failed",
55
+ sourcePath: params.source,
56
+ rootPath: params.rootDir,
57
+ targetPath,
58
+ };
59
+ }
60
+ const modeBits = stat.mode & 0o777;
61
+ if ((modeBits & 0o002) !== 0) {
62
+ return {
63
+ reason: "path_world_writable",
64
+ sourcePath: params.source,
65
+ rootPath: params.rootDir,
66
+ targetPath,
67
+ modeBits,
68
+ };
69
+ }
70
+ if (params.origin !== "bundled" &&
71
+ params.uid !== null &&
72
+ typeof stat.uid === "number" &&
73
+ stat.uid !== params.uid &&
74
+ stat.uid !== 0) {
75
+ return {
76
+ reason: "path_suspicious_ownership",
77
+ sourcePath: params.source,
78
+ rootPath: params.rootDir,
79
+ targetPath,
80
+ foundUid: stat.uid,
81
+ expectedUid: params.uid,
82
+ };
83
+ }
84
+ }
85
+ return null;
86
+ }
87
+ function findCandidateBlockIssue(params) {
88
+ const escaped = checkSourceEscapesRoot({
89
+ source: params.source,
90
+ rootDir: params.rootDir,
91
+ });
92
+ if (escaped) {
93
+ return escaped;
94
+ }
95
+ return checkPathStatAndPermissions({
96
+ source: params.source,
97
+ rootDir: params.rootDir,
98
+ origin: params.origin,
99
+ uid: currentUid(params.ownershipUid),
100
+ });
101
+ }
102
+ function formatCandidateBlockMessage(issue) {
103
+ if (issue.reason === "source_escapes_root") {
104
+ return `blocked plugin candidate: source escapes plugin root (${issue.sourcePath} -> ${issue.sourceRealPath}; root=${issue.rootRealPath})`;
105
+ }
106
+ if (issue.reason === "path_stat_failed") {
107
+ return `blocked plugin candidate: cannot stat path (${issue.targetPath})`;
108
+ }
109
+ if (issue.reason === "path_world_writable") {
110
+ return `blocked plugin candidate: world-writable path (${issue.targetPath}, mode=${formatPosixMode(issue.modeBits ?? 0)})`;
111
+ }
112
+ return `blocked plugin candidate: suspicious ownership (${issue.targetPath}, uid=${issue.foundUid}, expected uid=${issue.expectedUid} or root)`;
113
+ }
114
+ function isUnsafePluginCandidate(params) {
115
+ const issue = findCandidateBlockIssue({
116
+ source: params.source,
117
+ rootDir: params.rootDir,
118
+ origin: params.origin,
119
+ ownershipUid: params.ownershipUid,
120
+ });
121
+ if (!issue) {
122
+ return false;
123
+ }
124
+ params.diagnostics.push({
125
+ level: "warn",
126
+ source: issue.targetPath,
127
+ message: formatCandidateBlockMessage(issue),
128
+ });
129
+ return true;
130
+ }
7
131
  function isExtensionFile(filePath) {
8
132
  const ext = path.extname(filePath);
9
- if (!EXTENSION_EXTS.has(ext))
133
+ if (!EXTENSION_EXTS.has(ext)) {
10
134
  return false;
135
+ }
11
136
  return !filePath.endsWith(".d.ts");
12
137
  }
13
138
  function readPackageManifest(dir) {
14
139
  const manifestPath = path.join(dir, "package.json");
15
- if (!fs.existsSync(manifestPath))
140
+ if (!fs.existsSync(manifestPath)) {
16
141
  return null;
142
+ }
17
143
  try {
18
144
  const raw = fs.readFileSync(manifestPath, "utf-8");
19
145
  return JSON.parse(raw);
@@ -24,34 +150,48 @@ function readPackageManifest(dir) {
24
150
  }
25
151
  function resolvePackageExtensions(manifest) {
26
152
  const raw = getPackageManifestMetadata(manifest)?.extensions;
27
- if (!Array.isArray(raw))
153
+ if (!Array.isArray(raw)) {
28
154
  return [];
155
+ }
29
156
  return raw.map((entry) => (typeof entry === "string" ? entry.trim() : "")).filter(Boolean);
30
157
  }
31
158
  function deriveIdHint(params) {
32
159
  const base = path.basename(params.filePath, path.extname(params.filePath));
33
160
  const rawPackageName = params.packageName?.trim();
34
- if (!rawPackageName)
161
+ if (!rawPackageName) {
35
162
  return base;
163
+ }
36
164
  // Prefer the unscoped name so config keys stay stable even when the npm
37
165
  // package is scoped (example: @poolbot/voice-call -> voice-call).
38
166
  const unscoped = rawPackageName.includes("/")
39
167
  ? (rawPackageName.split("/").pop() ?? rawPackageName)
40
168
  : rawPackageName;
41
- if (!params.hasMultipleExtensions)
169
+ if (!params.hasMultipleExtensions) {
42
170
  return unscoped;
171
+ }
43
172
  return `${unscoped}/${base}`;
44
173
  }
45
174
  function addCandidate(params) {
46
175
  const resolved = path.resolve(params.source);
47
- if (params.seen.has(resolved))
176
+ if (params.seen.has(resolved)) {
48
177
  return;
178
+ }
179
+ const resolvedRoot = path.resolve(params.rootDir);
180
+ if (isUnsafePluginCandidate({
181
+ source: resolved,
182
+ rootDir: resolvedRoot,
183
+ origin: params.origin,
184
+ diagnostics: params.diagnostics,
185
+ ownershipUid: params.ownershipUid,
186
+ })) {
187
+ return;
188
+ }
49
189
  params.seen.add(resolved);
50
190
  const manifest = params.manifest ?? null;
51
191
  params.candidates.push({
52
192
  idHint: params.idHint,
53
193
  source: resolved,
54
- rootDir: path.resolve(params.rootDir),
194
+ rootDir: resolvedRoot,
55
195
  origin: params.origin,
56
196
  workspaceDir: params.workspaceDir,
57
197
  packageName: manifest?.name?.trim() || undefined,
@@ -61,9 +201,24 @@ function addCandidate(params) {
61
201
  packagePoolbot: getPackageManifestMetadata(manifest ?? undefined),
62
202
  });
63
203
  }
204
+ function resolvePackageEntrySource(params) {
205
+ const source = path.resolve(params.packageDir, params.entryPath);
206
+ if (!isPathInsideWithRealpath(params.packageDir, source, {
207
+ requireRealpath: true,
208
+ })) {
209
+ params.diagnostics.push({
210
+ level: "error",
211
+ message: `extension entry escapes package directory: ${params.entryPath}`,
212
+ source: params.sourceLabel,
213
+ });
214
+ return null;
215
+ }
216
+ return source;
217
+ }
64
218
  function discoverInDirectory(params) {
65
- if (!fs.existsSync(params.dir))
219
+ if (!fs.existsSync(params.dir)) {
66
220
  return;
221
+ }
67
222
  let entries = [];
68
223
  try {
69
224
  entries = fs.readdirSync(params.dir, { withFileTypes: true });
@@ -79,27 +234,40 @@ function discoverInDirectory(params) {
79
234
  for (const entry of entries) {
80
235
  const fullPath = path.join(params.dir, entry.name);
81
236
  if (entry.isFile()) {
82
- if (!isExtensionFile(fullPath))
237
+ if (!isExtensionFile(fullPath)) {
83
238
  continue;
239
+ }
84
240
  addCandidate({
85
241
  candidates: params.candidates,
242
+ diagnostics: params.diagnostics,
86
243
  seen: params.seen,
87
244
  idHint: path.basename(entry.name, path.extname(entry.name)),
88
245
  source: fullPath,
89
246
  rootDir: path.dirname(fullPath),
90
247
  origin: params.origin,
248
+ ownershipUid: params.ownershipUid,
91
249
  workspaceDir: params.workspaceDir,
92
250
  });
93
251
  }
94
- if (!entry.isDirectory())
252
+ if (!entry.isDirectory()) {
95
253
  continue;
254
+ }
96
255
  const manifest = readPackageManifest(fullPath);
97
256
  const extensions = manifest ? resolvePackageExtensions(manifest) : [];
98
257
  if (extensions.length > 0) {
99
258
  for (const extPath of extensions) {
100
- const resolved = path.resolve(fullPath, extPath);
259
+ const resolved = resolvePackageEntrySource({
260
+ packageDir: fullPath,
261
+ entryPath: extPath,
262
+ sourceLabel: fullPath,
263
+ diagnostics: params.diagnostics,
264
+ });
265
+ if (!resolved) {
266
+ continue;
267
+ }
101
268
  addCandidate({
102
269
  candidates: params.candidates,
270
+ diagnostics: params.diagnostics,
103
271
  seen: params.seen,
104
272
  idHint: deriveIdHint({
105
273
  filePath: resolved,
@@ -109,6 +277,7 @@ function discoverInDirectory(params) {
109
277
  source: resolved,
110
278
  rootDir: fullPath,
111
279
  origin: params.origin,
280
+ ownershipUid: params.ownershipUid,
112
281
  workspaceDir: params.workspaceDir,
113
282
  manifest,
114
283
  packageDir: fullPath,
@@ -123,11 +292,13 @@ function discoverInDirectory(params) {
123
292
  if (indexFile && isExtensionFile(indexFile)) {
124
293
  addCandidate({
125
294
  candidates: params.candidates,
295
+ diagnostics: params.diagnostics,
126
296
  seen: params.seen,
127
297
  idHint: entry.name,
128
298
  source: indexFile,
129
299
  rootDir: fullPath,
130
300
  origin: params.origin,
301
+ ownershipUid: params.ownershipUid,
131
302
  workspaceDir: params.workspaceDir,
132
303
  manifest,
133
304
  packageDir: fullPath,
@@ -157,11 +328,13 @@ function discoverFromPath(params) {
157
328
  }
158
329
  addCandidate({
159
330
  candidates: params.candidates,
331
+ diagnostics: params.diagnostics,
160
332
  seen: params.seen,
161
333
  idHint: path.basename(resolved, path.extname(resolved)),
162
334
  source: resolved,
163
335
  rootDir: path.dirname(resolved),
164
336
  origin: params.origin,
337
+ ownershipUid: params.ownershipUid,
165
338
  workspaceDir: params.workspaceDir,
166
339
  });
167
340
  return;
@@ -171,9 +344,18 @@ function discoverFromPath(params) {
171
344
  const extensions = manifest ? resolvePackageExtensions(manifest) : [];
172
345
  if (extensions.length > 0) {
173
346
  for (const extPath of extensions) {
174
- const source = path.resolve(resolved, extPath);
347
+ const source = resolvePackageEntrySource({
348
+ packageDir: resolved,
349
+ entryPath: extPath,
350
+ sourceLabel: resolved,
351
+ diagnostics: params.diagnostics,
352
+ });
353
+ if (!source) {
354
+ continue;
355
+ }
175
356
  addCandidate({
176
357
  candidates: params.candidates,
358
+ diagnostics: params.diagnostics,
177
359
  seen: params.seen,
178
360
  idHint: deriveIdHint({
179
361
  filePath: source,
@@ -183,6 +365,7 @@ function discoverFromPath(params) {
183
365
  source,
184
366
  rootDir: resolved,
185
367
  origin: params.origin,
368
+ ownershipUid: params.ownershipUid,
186
369
  workspaceDir: params.workspaceDir,
187
370
  manifest,
188
371
  packageDir: resolved,
@@ -197,11 +380,13 @@ function discoverFromPath(params) {
197
380
  if (indexFile && isExtensionFile(indexFile)) {
198
381
  addCandidate({
199
382
  candidates: params.candidates,
383
+ diagnostics: params.diagnostics,
200
384
  seen: params.seen,
201
385
  idHint: path.basename(resolved),
202
386
  source: indexFile,
203
387
  rootDir: resolved,
204
388
  origin: params.origin,
389
+ ownershipUid: params.ownershipUid,
205
390
  workspaceDir: params.workspaceDir,
206
391
  manifest,
207
392
  packageDir: resolved,
@@ -211,6 +396,7 @@ function discoverFromPath(params) {
211
396
  discoverInDirectory({
212
397
  dir: resolved,
213
398
  origin: params.origin,
399
+ ownershipUid: params.ownershipUid,
214
400
  workspaceDir: params.workspaceDir,
215
401
  candidates: params.candidates,
216
402
  diagnostics: params.diagnostics,
@@ -226,14 +412,17 @@ export function discoverPoolBotPlugins(params) {
226
412
  const workspaceDir = params.workspaceDir?.trim();
227
413
  const extra = params.extraPaths ?? [];
228
414
  for (const extraPath of extra) {
229
- if (typeof extraPath !== "string")
415
+ if (typeof extraPath !== "string") {
230
416
  continue;
417
+ }
231
418
  const trimmed = extraPath.trim();
232
- if (!trimmed)
419
+ if (!trimmed) {
233
420
  continue;
421
+ }
234
422
  discoverFromPath({
235
423
  rawPath: trimmed,
236
424
  origin: "config",
425
+ ownershipUid: params.ownershipUid,
237
426
  workspaceDir: workspaceDir?.trim() || undefined,
238
427
  candidates,
239
428
  diagnostics,
@@ -242,20 +431,24 @@ export function discoverPoolBotPlugins(params) {
242
431
  }
243
432
  if (workspaceDir) {
244
433
  const workspaceRoot = resolveUserPath(workspaceDir);
245
- const workspaceExt = path.join(workspaceRoot, ".poolbot", "extensions");
246
- discoverInDirectory({
247
- dir: workspaceExt,
248
- origin: "workspace",
249
- workspaceDir: workspaceRoot,
250
- candidates,
251
- diagnostics,
252
- seen,
253
- });
434
+ const workspaceExtDirs = [path.join(workspaceRoot, ".poolbot", "extensions")];
435
+ for (const dir of workspaceExtDirs) {
436
+ discoverInDirectory({
437
+ dir,
438
+ origin: "workspace",
439
+ ownershipUid: params.ownershipUid,
440
+ workspaceDir: workspaceRoot,
441
+ candidates,
442
+ diagnostics,
443
+ seen,
444
+ });
445
+ }
254
446
  }
255
447
  const globalDir = path.join(resolveConfigDir(), "extensions");
256
448
  discoverInDirectory({
257
449
  dir: globalDir,
258
450
  origin: "global",
451
+ ownershipUid: params.ownershipUid,
259
452
  candidates,
260
453
  diagnostics,
261
454
  seen,
@@ -265,6 +458,7 @@ export function discoverPoolBotPlugins(params) {
265
458
  discoverInDirectory({
266
459
  dir: bundledDir,
267
460
  origin: "bundled",
461
+ ownershipUid: params.ownershipUid,
268
462
  candidates,
269
463
  diagnostics,
270
464
  seen,
@@ -48,6 +48,22 @@ export function getGlobalPluginRegistry() {
48
48
  export function hasGlobalHooks(hookName) {
49
49
  return globalHookRunner?.hasHooks(hookName) ?? false;
50
50
  }
51
+ export async function runGlobalGatewayStopSafely(params) {
52
+ const hookRunner = getGlobalHookRunner();
53
+ if (!hookRunner?.hasHooks("gateway_stop")) {
54
+ return;
55
+ }
56
+ try {
57
+ await hookRunner.runGatewayStop(params.event, params.ctx);
58
+ }
59
+ catch (err) {
60
+ if (params.onError) {
61
+ params.onError(err);
62
+ return;
63
+ }
64
+ log.warn(`gateway_stop hook failed: ${String(err)}`);
65
+ }
66
+ }
51
67
  /**
52
68
  * Reset the global hook runner (for testing).
53
69
  */