@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
@@ -3,19 +3,25 @@
3
3
  *
4
4
  * These functions perform I/O (filesystem, config reads) to detect security issues.
5
5
  */
6
- import JSON5 from "json5";
7
6
  import fs from "node:fs/promises";
8
7
  import path from "node:path";
9
- import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js";
8
+ import { resolveDefaultAgentId } from "../agents/agent-scope.js";
9
+ import { isToolAllowedByPolicies } from "../agents/pi-tools.policy.js";
10
+ import { resolveSandboxConfigForAgent, resolveSandboxToolPolicyForAgent, } from "../agents/sandbox.js";
10
11
  import { loadWorkspaceSkillEntries } from "../agents/skills.js";
11
- import { LEGACY_MANIFEST_KEY } from "../compat/legacy-names.js";
12
+ import { resolveToolProfilePolicy } from "../agents/tool-policy.js";
13
+ import { listAgentWorkspaceDirs } from "../agents/workspace-dirs.js";
14
+ import { MANIFEST_KEY } from "../compat/legacy-names.js";
12
15
  import { resolveNativeSkillsEnabled } from "../config/commands.js";
13
16
  import { createConfigIO } from "../config/config.js";
14
- import { INCLUDE_KEY, MAX_INCLUDE_DEPTH } from "../config/includes.js";
17
+ import { collectIncludePathsRecursive } from "../config/includes-scan.js";
15
18
  import { resolveOAuthDir } from "../config/paths.js";
19
+ import { normalizePluginsConfig } from "../plugins/config-state.js";
16
20
  import { normalizeAgentId } from "../routing/session-key.js";
17
21
  import { formatPermissionDetail, formatPermissionRemediation, inspectPathPermissions, safeStat, } from "./audit-fs.js";
18
- import { scanDirectoryWithSummary } from "./skill-scanner.js";
22
+ import { pickSandboxToolPolicy } from "./audit-tool-policy.js";
23
+ import { extensionUsesSkippedScannerPath, isPathInside } from "./scan-paths.js";
24
+ import * as skillScanner from "./skill-scanner.js";
19
25
  // --------------------------------------------------------------------------
20
26
  // Helpers
21
27
  // --------------------------------------------------------------------------
@@ -35,91 +41,6 @@ function expandTilde(p, env) {
35
41
  }
36
42
  return null;
37
43
  }
38
- function resolveIncludePath(baseConfigPath, includePath) {
39
- return path.normalize(path.isAbsolute(includePath)
40
- ? includePath
41
- : path.resolve(path.dirname(baseConfigPath), includePath));
42
- }
43
- function listDirectIncludes(parsed) {
44
- const out = [];
45
- const visit = (value) => {
46
- if (!value) {
47
- return;
48
- }
49
- if (Array.isArray(value)) {
50
- for (const item of value) {
51
- visit(item);
52
- }
53
- return;
54
- }
55
- if (typeof value !== "object") {
56
- return;
57
- }
58
- const rec = value;
59
- const includeVal = rec[INCLUDE_KEY];
60
- if (typeof includeVal === "string") {
61
- out.push(includeVal);
62
- }
63
- else if (Array.isArray(includeVal)) {
64
- for (const item of includeVal) {
65
- if (typeof item === "string") {
66
- out.push(item);
67
- }
68
- }
69
- }
70
- for (const v of Object.values(rec)) {
71
- visit(v);
72
- }
73
- };
74
- visit(parsed);
75
- return out;
76
- }
77
- async function collectIncludePathsRecursive(params) {
78
- const visited = new Set();
79
- const result = [];
80
- const walk = async (basePath, parsed, depth) => {
81
- if (depth > MAX_INCLUDE_DEPTH) {
82
- return;
83
- }
84
- for (const raw of listDirectIncludes(parsed)) {
85
- const resolved = resolveIncludePath(basePath, raw);
86
- if (visited.has(resolved)) {
87
- continue;
88
- }
89
- visited.add(resolved);
90
- result.push(resolved);
91
- const rawText = await fs.readFile(resolved, "utf-8").catch(() => null);
92
- if (!rawText) {
93
- continue;
94
- }
95
- const nestedParsed = (() => {
96
- try {
97
- return JSON5.parse(rawText);
98
- }
99
- catch {
100
- return null;
101
- }
102
- })();
103
- if (nestedParsed) {
104
- // eslint-disable-next-line no-await-in-loop
105
- await walk(resolved, nestedParsed, depth + 1);
106
- }
107
- }
108
- };
109
- await walk(params.configPath, params.parsed, 0);
110
- return result;
111
- }
112
- function isPathInside(basePath, candidatePath) {
113
- const base = path.resolve(basePath);
114
- const candidate = path.resolve(candidatePath);
115
- const rel = path.relative(base, candidate);
116
- return rel === "" || (!rel.startsWith(`..${path.sep}`) && rel !== ".." && !path.isAbsolute(rel));
117
- }
118
- function extensionUsesSkippedScannerPath(entry) {
119
- const segments = entry.split(/[\\/]+/).filter(Boolean);
120
- return segments.some((segment) => segment === "node_modules" ||
121
- (segment.startsWith(".") && segment !== "." && segment !== ".."));
122
- }
123
44
  async function readPluginManifestExtensions(pluginPath) {
124
45
  const manifestPath = path.join(pluginPath, "package.json");
125
46
  const raw = await fs.readFile(manifestPath, "utf-8").catch(() => "");
@@ -127,25 +48,12 @@ async function readPluginManifestExtensions(pluginPath) {
127
48
  return [];
128
49
  }
129
50
  const parsed = JSON.parse(raw);
130
- const extensions = parsed?.[LEGACY_MANIFEST_KEY]?.extensions;
51
+ const extensions = parsed?.[MANIFEST_KEY]?.extensions;
131
52
  if (!Array.isArray(extensions)) {
132
53
  return [];
133
54
  }
134
55
  return extensions.map((entry) => (typeof entry === "string" ? entry.trim() : "")).filter(Boolean);
135
56
  }
136
- function listWorkspaceDirs(cfg) {
137
- const dirs = new Set();
138
- const list = cfg.agents?.list;
139
- if (Array.isArray(list)) {
140
- for (const entry of list) {
141
- if (entry && typeof entry === "object" && typeof entry.id === "string") {
142
- dirs.add(resolveAgentWorkspaceDir(cfg, entry.id));
143
- }
144
- }
145
- }
146
- dirs.add(resolveAgentWorkspaceDir(cfg, resolveDefaultAgentId(cfg)));
147
- return [...dirs];
148
- }
149
57
  function formatCodeSafetyDetails(findings, rootDir) {
150
58
  return findings
151
59
  .map((finding) => {
@@ -158,74 +66,340 @@ function formatCodeSafetyDetails(findings, rootDir) {
158
66
  })
159
67
  .join("\n");
160
68
  }
161
- // --------------------------------------------------------------------------
162
- // Exported collectors
163
- // --------------------------------------------------------------------------
164
- export async function collectPluginsTrustFindings(params) {
165
- const findings = [];
69
+ async function listInstalledPluginDirs(params) {
166
70
  const extensionsDir = path.join(params.stateDir, "extensions");
167
71
  const st = await safeStat(extensionsDir);
168
72
  if (!st.ok || !st.isDir) {
169
- return findings;
73
+ return { extensionsDir, pluginDirs: [] };
170
74
  }
171
- const entries = await fs.readdir(extensionsDir, { withFileTypes: true }).catch(() => []);
75
+ const entries = await fs.readdir(extensionsDir, { withFileTypes: true }).catch((err) => {
76
+ params.onReadError?.(err);
77
+ return [];
78
+ });
172
79
  const pluginDirs = entries
173
- .filter((e) => e.isDirectory())
174
- .map((e) => e.name)
80
+ .filter((entry) => entry.isDirectory())
81
+ .map((entry) => entry.name)
175
82
  .filter(Boolean);
176
- if (pluginDirs.length === 0) {
177
- return findings;
83
+ return { extensionsDir, pluginDirs };
84
+ }
85
+ function resolveToolPolicies(params) {
86
+ const profile = params.agentTools?.profile ?? params.cfg.tools?.profile;
87
+ const profilePolicy = resolveToolProfilePolicy(profile);
88
+ const policies = [
89
+ profilePolicy,
90
+ pickSandboxToolPolicy(params.cfg.tools ?? undefined),
91
+ pickSandboxToolPolicy(params.agentTools),
92
+ ];
93
+ if (params.sandboxMode === "all") {
94
+ policies.push(resolveSandboxToolPolicyForAgent(params.cfg, params.agentId ?? undefined));
95
+ }
96
+ return policies;
97
+ }
98
+ function normalizePluginIdSet(entries) {
99
+ return new Set(entries.map((entry) => entry.trim().toLowerCase()).filter(Boolean));
100
+ }
101
+ function resolveEnabledExtensionPluginIds(params) {
102
+ const normalized = normalizePluginsConfig(params.cfg.plugins);
103
+ if (!normalized.enabled) {
104
+ return [];
105
+ }
106
+ const allowSet = normalizePluginIdSet(normalized.allow);
107
+ const denySet = normalizePluginIdSet(normalized.deny);
108
+ const entryById = new Map();
109
+ for (const [id, entry] of Object.entries(normalized.entries)) {
110
+ entryById.set(id.trim().toLowerCase(), entry);
111
+ }
112
+ const enabled = [];
113
+ for (const id of params.pluginDirs) {
114
+ const normalizedId = id.trim().toLowerCase();
115
+ if (!normalizedId) {
116
+ continue;
117
+ }
118
+ if (denySet.has(normalizedId)) {
119
+ continue;
120
+ }
121
+ if (allowSet.size > 0 && !allowSet.has(normalizedId)) {
122
+ continue;
123
+ }
124
+ if (entryById.get(normalizedId)?.enabled === false) {
125
+ continue;
126
+ }
127
+ enabled.push(normalizedId);
178
128
  }
179
- const allow = params.cfg.plugins?.allow;
180
- const allowConfigured = Array.isArray(allow) && allow.length > 0;
181
- if (!allowConfigured) {
182
- const hasString = (value) => typeof value === "string" && value.trim().length > 0;
183
- const hasAccountStringKey = (account, key) => Boolean(account &&
184
- typeof account === "object" &&
185
- hasString(account[key]));
186
- const discordConfigured = hasString(params.cfg.channels?.discord?.token) ||
187
- Boolean(params.cfg.channels?.discord?.accounts &&
188
- Object.values(params.cfg.channels.discord.accounts).some((a) => hasAccountStringKey(a, "token"))) ||
189
- hasString(process.env.DISCORD_BOT_TOKEN);
190
- const telegramConfigured = hasString(params.cfg.channels?.telegram?.botToken) ||
191
- hasString(params.cfg.channels?.telegram?.tokenFile) ||
192
- Boolean(params.cfg.channels?.telegram?.accounts &&
193
- Object.values(params.cfg.channels.telegram.accounts).some((a) => hasAccountStringKey(a, "botToken") || hasAccountStringKey(a, "tokenFile"))) ||
194
- hasString(process.env.TELEGRAM_BOT_TOKEN);
195
- const slackConfigured = hasString(params.cfg.channels?.slack?.botToken) ||
196
- hasString(params.cfg.channels?.slack?.appToken) ||
197
- Boolean(params.cfg.channels?.slack?.accounts &&
198
- Object.values(params.cfg.channels.slack.accounts).some((a) => hasAccountStringKey(a, "botToken") || hasAccountStringKey(a, "appToken"))) ||
199
- hasString(process.env.SLACK_BOT_TOKEN) ||
200
- hasString(process.env.SLACK_APP_TOKEN);
201
- const skillCommandsLikelyExposed = (discordConfigured &&
202
- resolveNativeSkillsEnabled({
203
- providerId: "discord",
204
- providerSetting: params.cfg.channels?.discord?.commands?.nativeSkills,
205
- globalSetting: params.cfg.commands?.nativeSkills,
206
- })) ||
207
- (telegramConfigured &&
129
+ return enabled;
130
+ }
131
+ function collectAllowEntries(config) {
132
+ const out = [];
133
+ if (Array.isArray(config?.allow)) {
134
+ out.push(...config.allow);
135
+ }
136
+ if (Array.isArray(config?.alsoAllow)) {
137
+ out.push(...config.alsoAllow);
138
+ }
139
+ return out.map((entry) => entry.trim().toLowerCase()).filter(Boolean);
140
+ }
141
+ function hasExplicitPluginAllow(params) {
142
+ return params.allowEntries.some((entry) => entry === "group:plugins" || params.enabledPluginIds.has(entry));
143
+ }
144
+ function hasProviderPluginAllow(params) {
145
+ if (!params.byProvider) {
146
+ return false;
147
+ }
148
+ for (const policy of Object.values(params.byProvider)) {
149
+ if (hasExplicitPluginAllow({
150
+ allowEntries: collectAllowEntries(policy),
151
+ enabledPluginIds: params.enabledPluginIds,
152
+ })) {
153
+ return true;
154
+ }
155
+ }
156
+ return false;
157
+ }
158
+ function isPinnedRegistrySpec(spec) {
159
+ const value = spec.trim();
160
+ if (!value) {
161
+ return false;
162
+ }
163
+ const at = value.lastIndexOf("@");
164
+ if (at <= 0 || at >= value.length - 1) {
165
+ return false;
166
+ }
167
+ const version = value.slice(at + 1).trim();
168
+ return /^v?\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(version);
169
+ }
170
+ async function readInstalledPackageVersion(dir) {
171
+ try {
172
+ const raw = await fs.readFile(path.join(dir, "package.json"), "utf-8");
173
+ const parsed = JSON.parse(raw);
174
+ return typeof parsed.version === "string" ? parsed.version : undefined;
175
+ }
176
+ catch {
177
+ return undefined;
178
+ }
179
+ }
180
+ // --------------------------------------------------------------------------
181
+ // Exported collectors
182
+ // --------------------------------------------------------------------------
183
+ export async function collectPluginsTrustFindings(params) {
184
+ const findings = [];
185
+ const { extensionsDir, pluginDirs } = await listInstalledPluginDirs({
186
+ stateDir: params.stateDir,
187
+ });
188
+ if (pluginDirs.length > 0) {
189
+ const allow = params.cfg.plugins?.allow;
190
+ const allowConfigured = Array.isArray(allow) && allow.length > 0;
191
+ if (!allowConfigured) {
192
+ const hasString = (value) => typeof value === "string" && value.trim().length > 0;
193
+ const hasAccountStringKey = (account, key) => Boolean(account &&
194
+ typeof account === "object" &&
195
+ hasString(account[key]));
196
+ const discordConfigured = hasString(params.cfg.channels?.discord?.token) ||
197
+ Boolean(params.cfg.channels?.discord?.accounts &&
198
+ Object.values(params.cfg.channels.discord.accounts).some((a) => hasAccountStringKey(a, "token"))) ||
199
+ hasString(process.env.DISCORD_BOT_TOKEN);
200
+ const telegramConfigured = hasString(params.cfg.channels?.telegram?.botToken) ||
201
+ hasString(params.cfg.channels?.telegram?.tokenFile) ||
202
+ Boolean(params.cfg.channels?.telegram?.accounts &&
203
+ Object.values(params.cfg.channels.telegram.accounts).some((a) => hasAccountStringKey(a, "botToken") || hasAccountStringKey(a, "tokenFile"))) ||
204
+ hasString(process.env.TELEGRAM_BOT_TOKEN);
205
+ const slackConfigured = hasString(params.cfg.channels?.slack?.botToken) ||
206
+ hasString(params.cfg.channels?.slack?.appToken) ||
207
+ Boolean(params.cfg.channels?.slack?.accounts &&
208
+ Object.values(params.cfg.channels.slack.accounts).some((a) => hasAccountStringKey(a, "botToken") || hasAccountStringKey(a, "appToken"))) ||
209
+ hasString(process.env.SLACK_BOT_TOKEN) ||
210
+ hasString(process.env.SLACK_APP_TOKEN);
211
+ const skillCommandsLikelyExposed = (discordConfigured &&
208
212
  resolveNativeSkillsEnabled({
209
- providerId: "telegram",
210
- providerSetting: params.cfg.channels?.telegram?.commands?.nativeSkills,
213
+ providerId: "discord",
214
+ providerSetting: params.cfg.channels?.discord?.commands?.nativeSkills,
211
215
  globalSetting: params.cfg.commands?.nativeSkills,
212
216
  })) ||
213
- (slackConfigured &&
214
- resolveNativeSkillsEnabled({
215
- providerId: "slack",
216
- providerSetting: params.cfg.channels?.slack?.commands?.nativeSkills,
217
- globalSetting: params.cfg.commands?.nativeSkills,
218
- }));
219
- findings.push({
220
- checkId: "plugins.extensions_no_allowlist",
221
- severity: skillCommandsLikelyExposed ? "critical" : "warn",
222
- title: "Extensions exist but plugins.allow is not set",
223
- detail: `Found ${pluginDirs.length} extension(s) under ${extensionsDir}. Without plugins.allow, any discovered plugin id may load (depending on config and plugin behavior).` +
224
- (skillCommandsLikelyExposed
225
- ? "\nNative skill commands are enabled on at least one configured chat surface; treat unpinned/unallowlisted extensions as high risk."
226
- : ""),
227
- remediation: "Set plugins.allow to an explicit list of plugin ids you trust.",
217
+ (telegramConfigured &&
218
+ resolveNativeSkillsEnabled({
219
+ providerId: "telegram",
220
+ providerSetting: params.cfg.channels?.telegram?.commands?.nativeSkills,
221
+ globalSetting: params.cfg.commands?.nativeSkills,
222
+ })) ||
223
+ (slackConfigured &&
224
+ resolveNativeSkillsEnabled({
225
+ providerId: "slack",
226
+ providerSetting: params.cfg.channels?.slack?.commands?.nativeSkills,
227
+ globalSetting: params.cfg.commands?.nativeSkills,
228
+ }));
229
+ findings.push({
230
+ checkId: "plugins.extensions_no_allowlist",
231
+ severity: skillCommandsLikelyExposed ? "critical" : "warn",
232
+ title: "Extensions exist but plugins.allow is not set",
233
+ detail: `Found ${pluginDirs.length} extension(s) under ${extensionsDir}. Without plugins.allow, any discovered plugin id may load (depending on config and plugin behavior).` +
234
+ (skillCommandsLikelyExposed
235
+ ? "\nNative skill commands are enabled on at least one configured chat surface; treat unpinned/unallowlisted extensions as high risk."
236
+ : ""),
237
+ remediation: "Set plugins.allow to an explicit list of plugin ids you trust.",
238
+ });
239
+ }
240
+ const enabledExtensionPluginIds = resolveEnabledExtensionPluginIds({
241
+ cfg: params.cfg,
242
+ pluginDirs,
228
243
  });
244
+ if (enabledExtensionPluginIds.length > 0) {
245
+ const enabledPluginSet = new Set(enabledExtensionPluginIds);
246
+ const contexts = [{ label: "default" }];
247
+ for (const entry of params.cfg.agents?.list ?? []) {
248
+ if (!entry || typeof entry !== "object" || typeof entry.id !== "string") {
249
+ continue;
250
+ }
251
+ contexts.push({
252
+ label: `agents.list.${entry.id}`,
253
+ agentId: entry.id,
254
+ tools: entry.tools,
255
+ });
256
+ }
257
+ const permissiveContexts = [];
258
+ for (const context of contexts) {
259
+ const profile = context.tools?.profile ?? params.cfg.tools?.profile;
260
+ const restrictiveProfile = Boolean(resolveToolProfilePolicy(profile));
261
+ const sandboxMode = resolveSandboxConfigForAgent(params.cfg, context.agentId).mode;
262
+ const policies = resolveToolPolicies({
263
+ cfg: params.cfg,
264
+ agentTools: context.tools,
265
+ sandboxMode,
266
+ agentId: context.agentId,
267
+ });
268
+ const broadPolicy = isToolAllowedByPolicies("__openclaw_plugin_probe__", policies);
269
+ const explicitPluginAllow = !restrictiveProfile &&
270
+ (hasExplicitPluginAllow({
271
+ allowEntries: collectAllowEntries(params.cfg.tools),
272
+ enabledPluginIds: enabledPluginSet,
273
+ }) ||
274
+ hasProviderPluginAllow({
275
+ byProvider: params.cfg.tools?.byProvider,
276
+ enabledPluginIds: enabledPluginSet,
277
+ }) ||
278
+ hasExplicitPluginAllow({
279
+ allowEntries: collectAllowEntries(context.tools),
280
+ enabledPluginIds: enabledPluginSet,
281
+ }) ||
282
+ hasProviderPluginAllow({
283
+ byProvider: context.tools?.byProvider,
284
+ enabledPluginIds: enabledPluginSet,
285
+ }));
286
+ if (broadPolicy || explicitPluginAllow) {
287
+ permissiveContexts.push(context.label);
288
+ }
289
+ }
290
+ if (permissiveContexts.length > 0) {
291
+ findings.push({
292
+ checkId: "plugins.tools_reachable_permissive_policy",
293
+ severity: "warn",
294
+ title: "Extension plugin tools may be reachable under permissive tool policy",
295
+ detail: `Enabled extension plugins: ${enabledExtensionPluginIds.join(", ")}.\n` +
296
+ `Permissive tool policy contexts:\n${permissiveContexts.map((entry) => `- ${entry}`).join("\n")}`,
297
+ remediation: "Use restrictive profiles (`minimal`/`coding`) or explicit tool allowlists that exclude plugin tools for agents handling untrusted input.",
298
+ });
299
+ }
300
+ }
301
+ }
302
+ const pluginInstalls = params.cfg.plugins?.installs ?? {};
303
+ const npmPluginInstalls = Object.entries(pluginInstalls).filter(([, record]) => record?.source === "npm");
304
+ if (npmPluginInstalls.length > 0) {
305
+ const unpinned = npmPluginInstalls
306
+ .filter(([, record]) => typeof record.spec === "string" && !isPinnedRegistrySpec(record.spec))
307
+ .map(([pluginId, record]) => `${pluginId} (${record.spec})`);
308
+ if (unpinned.length > 0) {
309
+ findings.push({
310
+ checkId: "plugins.installs_unpinned_npm_specs",
311
+ severity: "warn",
312
+ title: "Plugin installs include unpinned npm specs",
313
+ detail: `Unpinned plugin install records:\n${unpinned.map((entry) => `- ${entry}`).join("\n")}`,
314
+ remediation: "Pin install specs to exact versions (for example, `@scope/pkg@1.2.3`) for higher supply-chain stability.",
315
+ });
316
+ }
317
+ const missingIntegrity = npmPluginInstalls
318
+ .filter(([, record]) => typeof record.integrity !== "string" || record.integrity.trim() === "")
319
+ .map(([pluginId]) => pluginId);
320
+ if (missingIntegrity.length > 0) {
321
+ findings.push({
322
+ checkId: "plugins.installs_missing_integrity",
323
+ severity: "warn",
324
+ title: "Plugin installs are missing integrity metadata",
325
+ detail: `Plugin install records missing integrity:\n${missingIntegrity.map((entry) => `- ${entry}`).join("\n")}`,
326
+ remediation: "Reinstall or update plugins to refresh install metadata with resolved integrity hashes.",
327
+ });
328
+ }
329
+ const pluginVersionDrift = [];
330
+ for (const [pluginId, record] of npmPluginInstalls) {
331
+ const recordedVersion = record.resolvedVersion ?? record.version;
332
+ if (!recordedVersion) {
333
+ continue;
334
+ }
335
+ const installPath = record.installPath ?? path.join(params.stateDir, "extensions", pluginId);
336
+ // eslint-disable-next-line no-await-in-loop
337
+ const installedVersion = await readInstalledPackageVersion(installPath);
338
+ if (!installedVersion || installedVersion === recordedVersion) {
339
+ continue;
340
+ }
341
+ pluginVersionDrift.push(`${pluginId} (recorded ${recordedVersion}, installed ${installedVersion})`);
342
+ }
343
+ if (pluginVersionDrift.length > 0) {
344
+ findings.push({
345
+ checkId: "plugins.installs_version_drift",
346
+ severity: "warn",
347
+ title: "Plugin install records drift from installed package versions",
348
+ detail: `Detected plugin install metadata drift:\n${pluginVersionDrift.map((entry) => `- ${entry}`).join("\n")}`,
349
+ remediation: "Run `openclaw plugins update --all` (or reinstall affected plugins) to refresh install metadata.",
350
+ });
351
+ }
352
+ }
353
+ const hookInstalls = params.cfg.hooks?.internal?.installs ?? {};
354
+ const npmHookInstalls = Object.entries(hookInstalls).filter(([, record]) => record?.source === "npm");
355
+ if (npmHookInstalls.length > 0) {
356
+ const unpinned = npmHookInstalls
357
+ .filter(([, record]) => typeof record.spec === "string" && !isPinnedRegistrySpec(record.spec))
358
+ .map(([hookId, record]) => `${hookId} (${record.spec})`);
359
+ if (unpinned.length > 0) {
360
+ findings.push({
361
+ checkId: "hooks.installs_unpinned_npm_specs",
362
+ severity: "warn",
363
+ title: "Hook installs include unpinned npm specs",
364
+ detail: `Unpinned hook install records:\n${unpinned.map((entry) => `- ${entry}`).join("\n")}`,
365
+ remediation: "Pin hook install specs to exact versions (for example, `@scope/pkg@1.2.3`) for higher supply-chain stability.",
366
+ });
367
+ }
368
+ const missingIntegrity = npmHookInstalls
369
+ .filter(([, record]) => typeof record.integrity !== "string" || record.integrity.trim() === "")
370
+ .map(([hookId]) => hookId);
371
+ if (missingIntegrity.length > 0) {
372
+ findings.push({
373
+ checkId: "hooks.installs_missing_integrity",
374
+ severity: "warn",
375
+ title: "Hook installs are missing integrity metadata",
376
+ detail: `Hook install records missing integrity:\n${missingIntegrity.map((entry) => `- ${entry}`).join("\n")}`,
377
+ remediation: "Reinstall or update hooks to refresh install metadata with resolved integrity hashes.",
378
+ });
379
+ }
380
+ const hookVersionDrift = [];
381
+ for (const [hookId, record] of npmHookInstalls) {
382
+ const recordedVersion = record.resolvedVersion ?? record.version;
383
+ if (!recordedVersion) {
384
+ continue;
385
+ }
386
+ const installPath = record.installPath ?? path.join(params.stateDir, "hooks", hookId);
387
+ // eslint-disable-next-line no-await-in-loop
388
+ const installedVersion = await readInstalledPackageVersion(installPath);
389
+ if (!installedVersion || installedVersion === recordedVersion) {
390
+ continue;
391
+ }
392
+ hookVersionDrift.push(`${hookId} (recorded ${recordedVersion}, installed ${installedVersion})`);
393
+ }
394
+ if (hookVersionDrift.length > 0) {
395
+ findings.push({
396
+ checkId: "hooks.installs_version_drift",
397
+ severity: "warn",
398
+ title: "Hook install records drift from installed package versions",
399
+ detail: `Detected hook install metadata drift:\n${hookVersionDrift.map((entry) => `- ${entry}`).join("\n")}`,
400
+ remediation: "Run `openclaw hooks update --all` (or reinstall affected hooks) to refresh install metadata.",
401
+ });
402
+ }
229
403
  }
230
404
  return findings;
231
405
  }
@@ -452,22 +626,18 @@ export async function readConfigSnapshotForAudit(params) {
452
626
  }
453
627
  export async function collectPluginsCodeSafetyFindings(params) {
454
628
  const findings = [];
455
- const extensionsDir = path.join(params.stateDir, "extensions");
456
- const st = await safeStat(extensionsDir);
457
- if (!st.ok || !st.isDir) {
458
- return findings;
459
- }
460
- const entries = await fs.readdir(extensionsDir, { withFileTypes: true }).catch((err) => {
461
- findings.push({
462
- checkId: "plugins.code_safety.scan_failed",
463
- severity: "warn",
464
- title: "Plugin extensions directory scan failed",
465
- detail: `Static code scan could not list extensions directory: ${String(err)}`,
466
- remediation: "Check file permissions and plugin layout, then rerun `poolbot security audit --deep`.",
467
- });
468
- return [];
629
+ const { extensionsDir, pluginDirs } = await listInstalledPluginDirs({
630
+ stateDir: params.stateDir,
631
+ onReadError: (err) => {
632
+ findings.push({
633
+ checkId: "plugins.code_safety.scan_failed",
634
+ severity: "warn",
635
+ title: "Plugin extensions directory scan failed",
636
+ detail: `Static code scan could not list extensions directory: ${String(err)}`,
637
+ remediation: "Check file permissions and plugin layout, then rerun `poolbot security audit --deep`.",
638
+ });
639
+ },
469
640
  });
470
- const pluginDirs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
471
641
  for (const pluginName of pluginDirs) {
472
642
  const pluginPath = path.join(extensionsDir, pluginName);
473
643
  const extensionEntries = await readPluginManifestExtensions(pluginPath).catch(() => []);
@@ -496,12 +666,14 @@ export async function collectPluginsCodeSafetyFindings(params) {
496
666
  severity: "critical",
497
667
  title: `Plugin "${pluginName}" has extension entry path traversal`,
498
668
  detail: `Found extension entries that escape the plugin directory:\n${escapedEntries.map((entry) => ` - ${entry}`).join("\n")}`,
499
- remediation: "Update the plugin manifest so all poolbot.extensions entries stay inside the plugin directory.",
669
+ remediation: "Update the plugin manifest so all openclaw.extensions entries stay inside the plugin directory.",
500
670
  });
501
671
  }
502
- const summary = await scanDirectoryWithSummary(pluginPath, {
672
+ const summary = await skillScanner
673
+ .scanDirectoryWithSummary(pluginPath, {
503
674
  includeFiles: forcedScanEntries,
504
- }).catch((err) => {
675
+ })
676
+ .catch((err) => {
505
677
  findings.push({
506
678
  checkId: "plugins.code_safety.scan_failed",
507
679
  severity: "warn",
@@ -522,7 +694,7 @@ export async function collectPluginsCodeSafetyFindings(params) {
522
694
  severity: "critical",
523
695
  title: `Plugin "${pluginName}" contains dangerous code patterns`,
524
696
  detail: `Found ${summary.critical} critical issue(s) in ${summary.scannedFiles} scanned file(s):\n${details}`,
525
- remediation: "Review the plugin source code carefully before use. If untrusted, remove the plugin from your Pool Bot extensions state directory.",
697
+ remediation: "Review the plugin source code carefully before use. If untrusted, remove the plugin from your OpenClaw extensions state directory.",
526
698
  });
527
699
  }
528
700
  else if (summary.warn > 0) {
@@ -543,7 +715,7 @@ export async function collectInstalledSkillsCodeSafetyFindings(params) {
543
715
  const findings = [];
544
716
  const pluginExtensionsDir = path.join(params.stateDir, "extensions");
545
717
  const scannedSkillDirs = new Set();
546
- const workspaceDirs = listWorkspaceDirs(params.cfg);
718
+ const workspaceDirs = listAgentWorkspaceDirs(params.cfg);
547
719
  for (const workspaceDir of workspaceDirs) {
548
720
  const entries = loadWorkspaceSkillEntries(workspaceDir, { config: params.cfg });
549
721
  for (const entry of entries) {
@@ -560,7 +732,7 @@ export async function collectInstalledSkillsCodeSafetyFindings(params) {
560
732
  }
561
733
  scannedSkillDirs.add(skillDir);
562
734
  const skillName = entry.skill.name;
563
- const summary = await scanDirectoryWithSummary(skillDir).catch((err) => {
735
+ const summary = await skillScanner.scanDirectoryWithSummary(skillDir).catch((err) => {
564
736
  findings.push({
565
737
  checkId: "skills.code_safety.scan_failed",
566
738
  severity: "warn",
@@ -1,2 +1,12 @@
1
- export { collectAttackSurfaceSummaryFindings, collectExposureMatrixFindings, collectHooksHardeningFindings, collectModelHygieneFindings, collectSecretsInConfigFindings, collectSmallModelRiskFindings, collectSyncedFolderFindings, } from "./audit-extra.sync.js";
1
+ /**
2
+ * Re-export barrel for security audit collector functions.
3
+ *
4
+ * Maintains backward compatibility with existing imports from audit-extra.
5
+ * Implementation split into:
6
+ * - audit-extra.sync.ts: Config-based checks (no I/O)
7
+ * - audit-extra.async.ts: Filesystem/plugin checks (async I/O)
8
+ */
9
+ // Sync collectors
10
+ export { collectAttackSurfaceSummaryFindings, collectExposureMatrixFindings, collectGatewayHttpNoAuthFindings, collectGatewayHttpSessionKeyOverrideFindings, collectHooksHardeningFindings, collectMinimalProfileOverrideFindings, collectModelHygieneFindings, collectNodeDenyCommandPatternFindings, collectSandboxDangerousConfigFindings, collectSandboxDockerNoopFindings, collectSecretsInConfigFindings, collectSmallModelRiskFindings, collectSyncedFolderFindings, } from "./audit-extra.sync.js";
11
+ // Async collectors
2
12
  export { collectIncludeFilePermFindings, collectInstalledSkillsCodeSafetyFindings, collectPluginsCodeSafetyFindings, collectPluginsTrustFindings, collectStateDeepFilesystemFindings, readConfigSnapshotForAudit, } from "./audit-extra.async.js";