@poolzin/pool-bot 2026.3.4 → 2026.3.7

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 (81) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/assets/pool-bot-icon-dark.png +0 -0
  3. package/assets/pool-bot-logo-1.png +0 -0
  4. package/assets/pool-bot-mascot.png +0 -0
  5. package/dist/agents/pi-embedded-runner/tool-result-truncation.js +62 -7
  6. package/dist/agents/pi-tools.js +32 -2
  7. package/dist/agents/poolbot-tools.js +12 -0
  8. package/dist/agents/session-write-lock.js +93 -8
  9. package/dist/agents/tools/pdf-native-providers.js +102 -0
  10. package/dist/agents/tools/pdf-tool.helpers.js +86 -0
  11. package/dist/agents/tools/pdf-tool.js +508 -0
  12. package/dist/auto-reply/reply/get-reply.js +6 -0
  13. package/dist/auto-reply/reply/message-preprocess-hooks.js +17 -0
  14. package/dist/build-info.json +3 -3
  15. package/dist/cli/banner.js +20 -1
  16. package/dist/cli/security-cli.js +211 -2
  17. package/dist/cli/tagline.js +7 -0
  18. package/dist/config/types.cli.js +1 -0
  19. package/dist/config/types.security.js +33 -0
  20. package/dist/config/zod-schema.js +15 -0
  21. package/dist/config/zod-schema.providers-core.js +1 -0
  22. package/dist/config/zod-schema.security.js +113 -0
  23. package/dist/cron/normalize.js +3 -0
  24. package/dist/cron/service/jobs.js +48 -0
  25. package/dist/discord/monitor/message-handler.preflight.js +11 -2
  26. package/dist/gateway/http-common.js +6 -1
  27. package/dist/gateway/protocol/schema/cron.js +3 -0
  28. package/dist/gateway/server-channels.js +99 -14
  29. package/dist/gateway/server-cron.js +89 -0
  30. package/dist/gateway/server-health-probes.js +55 -0
  31. package/dist/gateway/server-http.js +5 -0
  32. package/dist/hooks/bundled/session-memory/handler.js +8 -2
  33. package/dist/hooks/fire-and-forget.js +6 -0
  34. package/dist/hooks/internal-hooks.js +64 -19
  35. package/dist/hooks/message-hook-mappers.js +179 -0
  36. package/dist/infra/abort-signal.js +12 -0
  37. package/dist/infra/boundary-file-read.js +118 -0
  38. package/dist/infra/boundary-path.js +594 -0
  39. package/dist/infra/file-identity.js +12 -0
  40. package/dist/infra/fs-safe.js +377 -12
  41. package/dist/infra/hardlink-guards.js +30 -0
  42. package/dist/infra/json-utf8-bytes.js +8 -0
  43. package/dist/infra/net/fetch-guard.js +63 -13
  44. package/dist/infra/net/proxy-env.js +17 -0
  45. package/dist/infra/net/ssrf.js +74 -272
  46. package/dist/infra/path-alias-guards.js +21 -0
  47. package/dist/infra/path-guards.js +13 -1
  48. package/dist/infra/ports-probe.js +19 -0
  49. package/dist/infra/prototype-keys.js +4 -0
  50. package/dist/infra/restart-stale-pids.js +254 -0
  51. package/dist/infra/safe-open-sync.js +71 -0
  52. package/dist/infra/secure-random.js +7 -0
  53. package/dist/media/ffmpeg-limits.js +4 -0
  54. package/dist/media/input-files.js +6 -2
  55. package/dist/media/temp-files.js +12 -0
  56. package/dist/memory/embedding-chunk-limits.js +5 -2
  57. package/dist/memory/embeddings-ollama.js +91 -138
  58. package/dist/memory/embeddings-remote-fetch.js +11 -10
  59. package/dist/memory/embeddings.js +25 -9
  60. package/dist/memory/manager-embedding-ops.js +1 -1
  61. package/dist/memory/post-json.js +23 -0
  62. package/dist/memory/qmd-manager.js +272 -77
  63. package/dist/memory/remote-http.js +33 -0
  64. package/dist/plugin-sdk/windows-spawn.js +214 -0
  65. package/dist/security/capability-guards.js +89 -0
  66. package/dist/security/capability-manager.js +76 -0
  67. package/dist/security/capability.js +147 -0
  68. package/dist/security/index.js +7 -0
  69. package/dist/security/middleware.js +105 -0
  70. package/dist/shared/net/ip-test-fixtures.js +1 -0
  71. package/dist/shared/net/ip.js +303 -0
  72. package/dist/shared/net/ipv4.js +8 -11
  73. package/dist/shared/pid-alive.js +59 -2
  74. package/dist/slack/monitor/context.js +1 -0
  75. package/dist/slack/monitor/message-handler/dispatch.js +14 -1
  76. package/dist/slack/monitor/provider.js +2 -0
  77. package/dist/test-helpers/ssrf.js +13 -0
  78. package/dist/tui/tui.js +9 -4
  79. package/dist/utils/fetch-timeout.js +12 -1
  80. package/docs/adr/003-feature-gap-analysis.md +112 -0
  81. package/package.json +10 -4
@@ -0,0 +1,214 @@
1
+ import { readFileSync, statSync } from "node:fs";
2
+ import path from "node:path";
3
+ function isFilePath(candidate) {
4
+ try {
5
+ return statSync(candidate).isFile();
6
+ }
7
+ catch {
8
+ return false;
9
+ }
10
+ }
11
+ export function resolveWindowsExecutablePath(command, env) {
12
+ if (command.includes("/") || command.includes("\\") || path.isAbsolute(command)) {
13
+ return command;
14
+ }
15
+ const pathValue = env.PATH ?? env.Path ?? process.env.PATH ?? process.env.Path ?? "";
16
+ const pathEntries = pathValue
17
+ .split(";")
18
+ .map((entry) => entry.trim())
19
+ .filter(Boolean);
20
+ const hasExtension = path.extname(command).length > 0;
21
+ const pathExtRaw = env.PATHEXT ??
22
+ env.Pathext ??
23
+ process.env.PATHEXT ??
24
+ process.env.Pathext ??
25
+ ".EXE;.CMD;.BAT;.COM";
26
+ const pathExt = hasExtension
27
+ ? [""]
28
+ : pathExtRaw
29
+ .split(";")
30
+ .map((ext) => ext.trim())
31
+ .filter(Boolean)
32
+ .map((ext) => (ext.startsWith(".") ? ext : `.${ext}`));
33
+ for (const dir of pathEntries) {
34
+ for (const ext of pathExt) {
35
+ for (const candidateExt of [ext, ext.toLowerCase(), ext.toUpperCase()]) {
36
+ const candidate = path.join(dir, `${command}${candidateExt}`);
37
+ if (isFilePath(candidate)) {
38
+ return candidate;
39
+ }
40
+ }
41
+ }
42
+ }
43
+ return command;
44
+ }
45
+ function resolveEntrypointFromCmdShim(wrapperPath) {
46
+ if (!isFilePath(wrapperPath)) {
47
+ return null;
48
+ }
49
+ try {
50
+ const content = readFileSync(wrapperPath, "utf8");
51
+ const candidates = [];
52
+ for (const match of content.matchAll(/"([^"\r\n]*)"/g)) {
53
+ const token = match[1] ?? "";
54
+ const relMatch = token.match(/%~?dp0%?\s*[\\/]*(.*)$/i);
55
+ const relative = relMatch?.[1]?.trim();
56
+ if (!relative) {
57
+ continue;
58
+ }
59
+ const normalizedRelative = relative.replace(/[\\/]+/g, path.sep).replace(/^[\\/]+/, "");
60
+ const candidate = path.resolve(path.dirname(wrapperPath), normalizedRelative);
61
+ if (isFilePath(candidate)) {
62
+ candidates.push(candidate);
63
+ }
64
+ }
65
+ const nonNode = candidates.find((candidate) => {
66
+ const base = path.basename(candidate).toLowerCase();
67
+ return base !== "node.exe" && base !== "node";
68
+ });
69
+ return nonNode ?? null;
70
+ }
71
+ catch {
72
+ return null;
73
+ }
74
+ }
75
+ function resolveBinEntry(packageName, binField) {
76
+ if (typeof binField === "string") {
77
+ const trimmed = binField.trim();
78
+ return trimmed || null;
79
+ }
80
+ if (!binField || typeof binField !== "object") {
81
+ return null;
82
+ }
83
+ if (packageName) {
84
+ const preferred = binField[packageName];
85
+ if (typeof preferred === "string" && preferred.trim()) {
86
+ return preferred.trim();
87
+ }
88
+ }
89
+ for (const value of Object.values(binField)) {
90
+ if (typeof value === "string" && value.trim()) {
91
+ return value.trim();
92
+ }
93
+ }
94
+ return null;
95
+ }
96
+ function resolveEntrypointFromPackageJson(wrapperPath, packageName) {
97
+ if (!packageName) {
98
+ return null;
99
+ }
100
+ const wrapperDir = path.dirname(wrapperPath);
101
+ const packageDirs = [
102
+ path.resolve(wrapperDir, "..", packageName),
103
+ path.resolve(wrapperDir, "node_modules", packageName),
104
+ ];
105
+ for (const packageDir of packageDirs) {
106
+ const packageJsonPath = path.join(packageDir, "package.json");
107
+ if (!isFilePath(packageJsonPath)) {
108
+ continue;
109
+ }
110
+ try {
111
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
112
+ const entryRel = resolveBinEntry(packageName, packageJson.bin);
113
+ if (!entryRel) {
114
+ continue;
115
+ }
116
+ const entryPath = path.resolve(packageDir, entryRel);
117
+ if (isFilePath(entryPath)) {
118
+ return entryPath;
119
+ }
120
+ }
121
+ catch {
122
+ // Ignore malformed package metadata.
123
+ }
124
+ }
125
+ return null;
126
+ }
127
+ export function resolveWindowsSpawnProgramCandidate(params) {
128
+ const platform = params.platform ?? process.platform;
129
+ const env = params.env ?? process.env;
130
+ const execPath = params.execPath ?? process.execPath;
131
+ if (platform !== "win32") {
132
+ return {
133
+ command: params.command,
134
+ leadingArgv: [],
135
+ resolution: "direct",
136
+ };
137
+ }
138
+ const resolvedCommand = resolveWindowsExecutablePath(params.command, env);
139
+ const ext = path.extname(resolvedCommand).toLowerCase();
140
+ if (ext === ".js" || ext === ".cjs" || ext === ".mjs") {
141
+ return {
142
+ command: execPath,
143
+ leadingArgv: [resolvedCommand],
144
+ resolution: "node-entrypoint",
145
+ windowsHide: true,
146
+ };
147
+ }
148
+ if (ext === ".cmd" || ext === ".bat") {
149
+ const entrypoint = resolveEntrypointFromCmdShim(resolvedCommand) ??
150
+ resolveEntrypointFromPackageJson(resolvedCommand, params.packageName);
151
+ if (entrypoint) {
152
+ const entryExt = path.extname(entrypoint).toLowerCase();
153
+ if (entryExt === ".exe") {
154
+ return {
155
+ command: entrypoint,
156
+ leadingArgv: [],
157
+ resolution: "exe-entrypoint",
158
+ windowsHide: true,
159
+ };
160
+ }
161
+ return {
162
+ command: execPath,
163
+ leadingArgv: [entrypoint],
164
+ resolution: "node-entrypoint",
165
+ windowsHide: true,
166
+ };
167
+ }
168
+ return {
169
+ command: resolvedCommand,
170
+ leadingArgv: [],
171
+ resolution: "unresolved-wrapper",
172
+ };
173
+ }
174
+ return {
175
+ command: resolvedCommand,
176
+ leadingArgv: [],
177
+ resolution: "direct",
178
+ };
179
+ }
180
+ export function applyWindowsSpawnProgramPolicy(params) {
181
+ if (params.candidate.resolution !== "unresolved-wrapper") {
182
+ return {
183
+ command: params.candidate.command,
184
+ leadingArgv: params.candidate.leadingArgv,
185
+ resolution: params.candidate.resolution,
186
+ windowsHide: params.candidate.windowsHide,
187
+ };
188
+ }
189
+ if (params.allowShellFallback !== false) {
190
+ return {
191
+ command: params.candidate.command,
192
+ leadingArgv: [],
193
+ resolution: "shell-fallback",
194
+ shell: true,
195
+ };
196
+ }
197
+ throw new Error(`${path.basename(params.candidate.command)} wrapper resolved, but no executable/Node entrypoint could be resolved without shell execution.`);
198
+ }
199
+ export function resolveWindowsSpawnProgram(params) {
200
+ const candidate = resolveWindowsSpawnProgramCandidate(params);
201
+ return applyWindowsSpawnProgramPolicy({
202
+ candidate,
203
+ allowShellFallback: params.allowShellFallback,
204
+ });
205
+ }
206
+ export function materializeWindowsSpawnProgram(program, argv) {
207
+ return {
208
+ command: program.command,
209
+ argv: [...program.leadingArgv, ...argv],
210
+ resolution: program.resolution,
211
+ shell: program.shell,
212
+ windowsHide: program.windowsHide,
213
+ };
214
+ }
@@ -0,0 +1,89 @@
1
+ import { getCapabilityManager } from "./capability-manager.js";
2
+ /** Error thrown when a capability check fails. */
3
+ export class CapabilityError extends Error {
4
+ agentId;
5
+ required;
6
+ constructor(message, agentId, required) {
7
+ super(message);
8
+ this.agentId = agentId;
9
+ this.required = required;
10
+ this.name = "CapabilityError";
11
+ }
12
+ }
13
+ /** Guard function that throws if capability is denied. */
14
+ export function requireCapability(agentId, required) {
15
+ const manager = getCapabilityManager();
16
+ const check = manager.check(agentId, required);
17
+ if (!check.granted) {
18
+ throw new CapabilityError(check.reason, agentId, required);
19
+ }
20
+ }
21
+ /** Async guard that returns a promise rejection if denied. */
22
+ export async function withCapability(agentId, required, fn) {
23
+ requireCapability(agentId, required);
24
+ return await fn();
25
+ }
26
+ /** Helper to check file read capability. */
27
+ export function checkFileRead(agentId, path) {
28
+ return getCapabilityManager().check(agentId, {
29
+ type: "file:read",
30
+ pattern: path,
31
+ });
32
+ }
33
+ /** Helper to check file write capability. */
34
+ export function checkFileWrite(agentId, path) {
35
+ return getCapabilityManager().check(agentId, {
36
+ type: "file:write",
37
+ pattern: path,
38
+ });
39
+ }
40
+ /** Helper to check tool invocation capability. */
41
+ export function checkToolInvoke(agentId, toolId) {
42
+ return getCapabilityManager().check(agentId, {
43
+ type: "tool:invoke",
44
+ toolId,
45
+ });
46
+ }
47
+ /** Helper to check network connection capability. */
48
+ export function checkNetConnect(agentId, host) {
49
+ return getCapabilityManager().check(agentId, {
50
+ type: "net:connect",
51
+ pattern: host,
52
+ });
53
+ }
54
+ /** Helper to check shell execution capability. */
55
+ export function checkShellExec(agentId, command) {
56
+ return getCapabilityManager().check(agentId, {
57
+ type: "shell:exec",
58
+ pattern: command,
59
+ });
60
+ }
61
+ /** Helper to check LLM query capability. */
62
+ export function checkLlmQuery(agentId, model) {
63
+ return getCapabilityManager().check(agentId, {
64
+ type: "llm:query",
65
+ pattern: model,
66
+ });
67
+ }
68
+ /** Helper to check agent spawn capability. */
69
+ export function checkAgentSpawn(agentId) {
70
+ return getCapabilityManager().check(agentId, { type: "agent:spawn" });
71
+ }
72
+ /** Helper to check memory read capability. */
73
+ export function checkMemoryRead(agentId, scope) {
74
+ return getCapabilityManager().check(agentId, {
75
+ type: "memory:read",
76
+ scope,
77
+ });
78
+ }
79
+ /** Helper to check memory write capability. */
80
+ export function checkMemoryWrite(agentId, scope) {
81
+ return getCapabilityManager().check(agentId, {
82
+ type: "memory:write",
83
+ scope,
84
+ });
85
+ }
86
+ /** Helper to check gateway admin capability. */
87
+ export function checkGatewayAdmin(agentId) {
88
+ return getCapabilityManager().check(agentId, { type: "gateway:admin" });
89
+ }
@@ -0,0 +1,76 @@
1
+ import { capabilityMatches, CapabilityResult } from "./capability.js";
2
+ /**
3
+ * Manages capability grants for all agents.
4
+ * Uses a Map for O(1) lookups. Thread-safe for single-threaded Node.js.
5
+ */
6
+ export class CapabilityManager {
7
+ grants = new Map();
8
+ /** Grant capabilities to an agent. Replaces any existing grants. */
9
+ grant(agentId, capabilities) {
10
+ this.grants.set(agentId, capabilities);
11
+ }
12
+ /** Add capabilities to an agent's existing grants. */
13
+ add(agentId, capabilities) {
14
+ const existing = this.grants.get(agentId) ?? [];
15
+ this.grants.set(agentId, [...existing, ...capabilities]);
16
+ }
17
+ /** Check whether an agent has a specific capability. */
18
+ check(agentId, required) {
19
+ const grants = this.grants.get(agentId);
20
+ if (!grants) {
21
+ return CapabilityResult.denied(`No capabilities registered for agent ${agentId}`);
22
+ }
23
+ for (const granted of grants) {
24
+ if (capabilityMatches(granted, required)) {
25
+ return CapabilityResult.granted();
26
+ }
27
+ }
28
+ return CapabilityResult.denied(`Agent ${agentId} does not have capability: ${JSON.stringify(required)}`);
29
+ }
30
+ /** Check multiple capabilities at once. Returns first denial or grants all. */
31
+ checkAll(agentId, required) {
32
+ for (const req of required) {
33
+ const result = this.check(agentId, req);
34
+ if (!result.granted)
35
+ return result;
36
+ }
37
+ return CapabilityResult.granted();
38
+ }
39
+ /** List all capabilities for an agent. */
40
+ list(agentId) {
41
+ return this.grants.get(agentId) ?? [];
42
+ }
43
+ /** Remove all capabilities for an agent. */
44
+ revokeAll(agentId) {
45
+ this.grants.delete(agentId);
46
+ }
47
+ /** Check if an agent has any capabilities registered. */
48
+ has(agentId) {
49
+ return this.grants.has(agentId);
50
+ }
51
+ /** Get all registered agent IDs. */
52
+ agents() {
53
+ return Array.from(this.grants.keys());
54
+ }
55
+ /** Clear all grants. */
56
+ clear() {
57
+ this.grants.clear();
58
+ }
59
+ }
60
+ /** Global singleton instance. */
61
+ let globalManager;
62
+ /** Get or create the global capability manager. */
63
+ export function getCapabilityManager() {
64
+ if (!globalManager) {
65
+ globalManager = new CapabilityManager();
66
+ }
67
+ return globalManager;
68
+ }
69
+ /** Reset the global manager (useful for testing). */
70
+ export function resetCapabilityManager() {
71
+ globalManager = undefined;
72
+ }
73
+ /** Set a custom global manager (useful for testing). */
74
+ export function setCapabilityManager(manager) {
75
+ globalManager = manager;
76
+ }
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Capability-based security system for Pool Bot.
3
+ *
4
+ * Inspired by OpenFang's capability system, adapted for TypeScript.
5
+ * Agents can only perform actions they've been explicitly granted permission for.
6
+ * Capabilities are immutable after agent creation and enforced at runtime.
7
+ */
8
+ /** All available capability types for CLI and validation. */
9
+ export const CAPABILITY_TYPES = [
10
+ // File system
11
+ "file:read",
12
+ "file:write",
13
+ // Network
14
+ "net:connect",
15
+ "net:listen",
16
+ // Tools
17
+ "tool:invoke",
18
+ "tool:all",
19
+ // LLM
20
+ "llm:query",
21
+ "llm:maxTokens",
22
+ // Agent interaction
23
+ "agent:spawn",
24
+ "agent:message",
25
+ "agent:kill",
26
+ // Memory
27
+ "memory:read",
28
+ "memory:write",
29
+ // Shell
30
+ "shell:exec",
31
+ "env:read",
32
+ // Gateway
33
+ "gateway:admin",
34
+ "gateway:channels:read",
35
+ "gateway:channels:write",
36
+ // Economic
37
+ "econ:spend",
38
+ "econ:earn",
39
+ "econ:transfer",
40
+ ];
41
+ /** Helper to create capability check results. */
42
+ export const CapabilityResult = {
43
+ granted() {
44
+ return { granted: true };
45
+ },
46
+ denied(reason) {
47
+ return { granted: false, reason };
48
+ },
49
+ };
50
+ /** Simple glob pattern matching supporting '*' as wildcard. */
51
+ export function globMatches(pattern, value) {
52
+ if (pattern === "*")
53
+ return true;
54
+ if (pattern === value)
55
+ return true;
56
+ // Prefix wildcard: "*.example.com"
57
+ if (pattern.startsWith("*")) {
58
+ const suffix = pattern.slice(1);
59
+ return value.endsWith(suffix);
60
+ }
61
+ // Suffix wildcard: "api.*"
62
+ if (pattern.endsWith("*")) {
63
+ const prefix = pattern.slice(0, -1);
64
+ return value.startsWith(prefix);
65
+ }
66
+ // Middle wildcard: "api.*.com"
67
+ const starPos = pattern.indexOf("*");
68
+ if (starPos !== -1) {
69
+ const prefix = pattern.slice(0, starPos);
70
+ const suffix = pattern.slice(starPos + 1);
71
+ return (value.startsWith(prefix) &&
72
+ value.endsWith(suffix) &&
73
+ value.length >= prefix.length + suffix.length);
74
+ }
75
+ return false;
76
+ }
77
+ /**
78
+ * Check whether a required capability matches any granted capability.
79
+ */
80
+ export function capabilityMatches(granted, required) {
81
+ // Tool:all grants any specific tool
82
+ if (granted.type === "tool:all" && required.type === "tool:invoke") {
83
+ return true;
84
+ }
85
+ // Same variant type matching
86
+ if (granted.type !== required.type)
87
+ return false;
88
+ switch (granted.type) {
89
+ case "file:read":
90
+ case "file:write":
91
+ return globMatches(granted.pattern, required.pattern);
92
+ case "net:connect":
93
+ return globMatches(granted.pattern, required.pattern);
94
+ case "net:listen":
95
+ return granted.port === required.port;
96
+ case "tool:invoke":
97
+ return granted.toolId === required.toolId || granted.toolId === "*";
98
+ case "llm:query":
99
+ return globMatches(granted.pattern, required.pattern);
100
+ case "llm:maxTokens":
101
+ return granted.limit >= required.limit;
102
+ case "agent:spawn":
103
+ return true;
104
+ case "agent:message":
105
+ case "agent:kill":
106
+ return globMatches(granted.pattern, required.pattern);
107
+ case "memory:read":
108
+ case "memory:write":
109
+ return globMatches(granted.scope, required.scope);
110
+ case "shell:exec":
111
+ return globMatches(granted.pattern, required.pattern);
112
+ case "env:read":
113
+ return globMatches(granted.pattern, required.pattern);
114
+ case "gateway:admin":
115
+ case "gateway:channels:read":
116
+ return true;
117
+ case "gateway:channels:write":
118
+ return globMatches(granted.pattern, required.pattern);
119
+ case "econ:spend":
120
+ return granted.limit >= required.limit;
121
+ case "econ:earn":
122
+ return true;
123
+ case "econ:transfer":
124
+ return globMatches(granted.pattern, required.pattern);
125
+ case "tool:all":
126
+ // tool:all only matches tool:all (already handled above for tool:invoke)
127
+ return false;
128
+ default:
129
+ return false;
130
+ }
131
+ }
132
+ /**
133
+ * Validate that child capabilities are a subset of parent capabilities.
134
+ * Prevents privilege escalation.
135
+ */
136
+ export function validateCapabilityInheritance(parentCaps, childCaps) {
137
+ for (const childCap of childCaps) {
138
+ const isCovered = parentCaps.some((parentCap) => capabilityMatches(parentCap, childCap));
139
+ if (!isCovered) {
140
+ return {
141
+ valid: false,
142
+ reason: `Privilege escalation denied: child requests ${JSON.stringify(childCap)} but parent does not have a matching grant`,
143
+ };
144
+ }
145
+ }
146
+ return { valid: true };
147
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Security module index exports.
3
+ */
4
+ export * from "./capability.js";
5
+ export * from "./capability-manager.js";
6
+ export * from "./capability-guards.js";
7
+ export * from "./middleware.js";
@@ -0,0 +1,105 @@
1
+ import { getCapabilityManager } from "./capability-manager.js";
2
+ import { CapabilityError } from "./capability-guards.js";
3
+ import { logVerbose } from "../globals.js";
4
+ /**
5
+ * Creates a middleware that checks tool invocation capabilities.
6
+ */
7
+ export function createCapabilityMiddleware() {
8
+ return async (ctx, toolId, _args, next) => {
9
+ const manager = getCapabilityManager();
10
+ // Check for tool:all first (grants any tool)
11
+ const allCheck = manager.check(ctx.agentId, { type: "tool:all" });
12
+ if (allCheck.granted) {
13
+ logVerbose(`[capability] ${ctx.agentId} granted tool:all for ${toolId}`);
14
+ return await next();
15
+ }
16
+ // Check for specific tool invocation
17
+ const toolCheck = manager.check(ctx.agentId, {
18
+ type: "tool:invoke",
19
+ toolId,
20
+ });
21
+ if (!toolCheck.granted) {
22
+ logVerbose(`[capability] ${ctx.agentId} denied access to tool ${toolId}: ${toolCheck.reason}`);
23
+ throw new CapabilityError(`Tool '${toolId}' access denied: ${toolCheck.reason}`, ctx.agentId, { type: "tool:invoke", toolId });
24
+ }
25
+ logVerbose(`[capability] ${ctx.agentId} granted access to ${toolId}`);
26
+ return await next();
27
+ };
28
+ }
29
+ /**
30
+ * Creates a middleware that checks file access capabilities.
31
+ */
32
+ export function createFileAccessMiddleware() {
33
+ return async (ctx, toolId, args, next) => {
34
+ // Tools that read files
35
+ if (toolId === "file_read" || toolId === "read_file") {
36
+ const path = args.path || args.file_path || args.filePath;
37
+ if (typeof path === "string") {
38
+ const manager = getCapabilityManager();
39
+ const check = manager.check(ctx.agentId, {
40
+ type: "file:read",
41
+ pattern: path,
42
+ });
43
+ if (!check.granted) {
44
+ throw new CapabilityError(`File read denied for '${path}': ${check.reason}`, ctx.agentId, { type: "file:read", pattern: path });
45
+ }
46
+ }
47
+ }
48
+ // Tools that write files
49
+ if (toolId === "file_write" || toolId === "write_file") {
50
+ const path = args.path || args.file_path || args.filePath;
51
+ if (typeof path === "string") {
52
+ const manager = getCapabilityManager();
53
+ const check = manager.check(ctx.agentId, {
54
+ type: "file:write",
55
+ pattern: path,
56
+ });
57
+ if (!check.granted) {
58
+ throw new CapabilityError(`File write denied for '${path}': ${check.reason}`, ctx.agentId, { type: "file:write", pattern: path });
59
+ }
60
+ }
61
+ }
62
+ return await next();
63
+ };
64
+ }
65
+ /**
66
+ * Creates a middleware that checks shell execution capabilities.
67
+ */
68
+ export function createShellExecutionMiddleware() {
69
+ return async (ctx, toolId, args, next) => {
70
+ if (toolId === "shell" || toolId === "bash" || toolId === "exec") {
71
+ const command = args.command || args.cmd || args.shell;
72
+ if (typeof command === "string") {
73
+ const manager = getCapabilityManager();
74
+ const check = manager.check(ctx.agentId, {
75
+ type: "shell:exec",
76
+ pattern: command,
77
+ });
78
+ if (!check.granted) {
79
+ throw new CapabilityError(`Shell execution denied for '${command}': ${check.reason}`, ctx.agentId, { type: "shell:exec", pattern: command });
80
+ }
81
+ }
82
+ }
83
+ return await next();
84
+ };
85
+ }
86
+ /**
87
+ * Composes multiple middlewares into a single middleware.
88
+ */
89
+ export function composeMiddlewares(...middlewares) {
90
+ return async (ctx, toolId, args, finalNext) => {
91
+ let index = 0;
92
+ const dispatch = async () => {
93
+ if (index >= middlewares.length) {
94
+ return await finalNext();
95
+ }
96
+ const middleware = middlewares[index++];
97
+ return await middleware(ctx, toolId, args, dispatch);
98
+ };
99
+ return await dispatch();
100
+ };
101
+ }
102
+ /** Default security middleware stack. */
103
+ export function createDefaultSecurityMiddleware() {
104
+ return composeMiddlewares(createCapabilityMiddleware(), createFileAccessMiddleware(), createShellExecutionMiddleware());
105
+ }
@@ -0,0 +1 @@
1
+ export const blockedIpv6MulticastLiterals = ["ff02::1", "ff05::1:3", "[ff02::1]"];