@poolzin/pool-bot 2026.3.6 → 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.
- package/dist/agents/pi-tools.js +32 -2
- package/dist/auto-reply/reply/get-reply.js +6 -0
- package/dist/auto-reply/reply/message-preprocess-hooks.js +17 -0
- package/dist/build-info.json +3 -3
- package/dist/cli/banner.js +20 -1
- package/dist/cli/security-cli.js +211 -2
- package/dist/cli/tagline.js +7 -0
- package/dist/config/types.cli.js +1 -0
- package/dist/config/types.security.js +33 -0
- package/dist/config/zod-schema.js +15 -0
- package/dist/config/zod-schema.providers-core.js +1 -0
- package/dist/config/zod-schema.security.js +113 -0
- package/dist/discord/monitor/message-handler.preflight.js +11 -2
- package/dist/gateway/http-common.js +6 -1
- package/dist/hooks/fire-and-forget.js +6 -0
- package/dist/hooks/internal-hooks.js +64 -19
- package/dist/hooks/message-hook-mappers.js +179 -0
- package/dist/security/capability-guards.js +89 -0
- package/dist/security/capability-manager.js +76 -0
- package/dist/security/capability.js +147 -0
- package/dist/security/index.js +7 -0
- package/dist/security/middleware.js +105 -0
- package/dist/slack/monitor/context.js +1 -0
- package/dist/slack/monitor/message-handler/dispatch.js +14 -1
- package/dist/slack/monitor/provider.js +2 -0
- package/package.json +1 -1
package/dist/agents/pi-tools.js
CHANGED
|
@@ -16,8 +16,10 @@ import { assertRequiredParams, CLAUDE_PARAM_GROUPS, createPoolbotReadTool, creat
|
|
|
16
16
|
import { cleanToolSchemaForGemini, normalizeToolParameters } from "./pi-tools.schema.js";
|
|
17
17
|
import { getSubagentDepthFromSessionStore } from "./subagent-depth.js";
|
|
18
18
|
import { applyToolPolicyPipeline, buildDefaultToolPolicyPipelineSteps, } from "./tool-policy-pipeline.js";
|
|
19
|
-
import { applyOwnerOnlyToolPolicy, collectExplicitAllowlist, mergeAlsoAllowPolicy, resolveToolProfilePolicy, } from "./tool-policy.js";
|
|
19
|
+
import { applyOwnerOnlyToolPolicy, collectExplicitAllowlist, mergeAlsoAllowPolicy, normalizeToolName, resolveToolProfilePolicy, } from "./tool-policy.js";
|
|
20
20
|
import { resolveWorkspaceRoot } from "./workspace-dir.js";
|
|
21
|
+
import { CapabilityError } from "../security/capability-guards.js";
|
|
22
|
+
import { createDefaultSecurityMiddleware } from "../security/middleware.js";
|
|
21
23
|
function isOpenAIProvider(provider) {
|
|
22
24
|
const normalized = provider?.trim().toLowerCase();
|
|
23
25
|
return normalized === "openai" || normalized === "openai-codex";
|
|
@@ -339,8 +341,36 @@ export function createPoolbotCodingTools(options) {
|
|
|
339
341
|
const withAbort = options?.abortSignal
|
|
340
342
|
? withHooks.map((tool) => wrapToolWithAbortSignal(tool, options.abortSignal))
|
|
341
343
|
: withHooks;
|
|
344
|
+
// Apply capability-based security middleware if enabled
|
|
345
|
+
const withCapabilities = options?.config?.security?.enabled && agentId
|
|
346
|
+
? withAbort.map((tool) => wrapToolWithCapabilityCheck(tool, agentId))
|
|
347
|
+
: withAbort;
|
|
342
348
|
// NOTE: Keep canonical (lowercase) tool names here.
|
|
343
349
|
// pi-ai's Anthropic OAuth transport remaps tool names to Claude Code-style names
|
|
344
350
|
// on the wire and maps them back for tool dispatch.
|
|
345
|
-
return
|
|
351
|
+
return withCapabilities;
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Wraps a tool with capability-based security checks.
|
|
355
|
+
* This enforces fine-grained permissions for tool invocation.
|
|
356
|
+
*/
|
|
357
|
+
function wrapToolWithCapabilityCheck(tool, agentId) {
|
|
358
|
+
const middleware = createDefaultSecurityMiddleware();
|
|
359
|
+
return {
|
|
360
|
+
...tool,
|
|
361
|
+
execute: async (toolCallId, args, signal, onUpdate) => {
|
|
362
|
+
const ctx = { agentId };
|
|
363
|
+
const toolId = normalizeToolName(tool.name);
|
|
364
|
+
const result = await middleware(ctx, toolId, (args ?? {}), async () => {
|
|
365
|
+
if (!tool.execute) {
|
|
366
|
+
throw new CapabilityError(`Tool ${tool.name} has no execute function`, agentId, {
|
|
367
|
+
type: "tool:invoke",
|
|
368
|
+
toolId,
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
return await tool.execute(toolCallId, args, signal, onUpdate);
|
|
372
|
+
});
|
|
373
|
+
return result;
|
|
374
|
+
},
|
|
375
|
+
};
|
|
346
376
|
}
|
|
@@ -14,6 +14,7 @@ import { resolveReplyDirectives } from "./get-reply-directives.js";
|
|
|
14
14
|
import { handleInlineActions } from "./get-reply-inline-actions.js";
|
|
15
15
|
import { runPreparedReply } from "./get-reply-run.js";
|
|
16
16
|
import { finalizeInboundContext } from "./inbound-context.js";
|
|
17
|
+
import { emitPreAgentMessageHooks } from "./message-preprocess-hooks.js";
|
|
17
18
|
import { applyResetModelOverride } from "./session-reset-model.js";
|
|
18
19
|
import { initSessionState } from "./session.js";
|
|
19
20
|
import { stageSandboxMedia } from "./stage-sandbox-media.js";
|
|
@@ -110,6 +111,11 @@ export async function getReplyFromConfig(ctx, opts, configOverride) {
|
|
|
110
111
|
cfg,
|
|
111
112
|
});
|
|
112
113
|
}
|
|
114
|
+
emitPreAgentMessageHooks({
|
|
115
|
+
ctx: finalized,
|
|
116
|
+
cfg,
|
|
117
|
+
isFastTestEnv,
|
|
118
|
+
});
|
|
113
119
|
const commandAuthorized = finalized.CommandAuthorized;
|
|
114
120
|
resolveCommandAuthorization({
|
|
115
121
|
ctx: finalized,
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { fireAndForgetHook } from "../../hooks/fire-and-forget.js";
|
|
2
|
+
import { createInternalHookEvent, triggerInternalHook } from "../../hooks/internal-hooks.js";
|
|
3
|
+
import { deriveInboundMessageHookContext, toInternalMessagePreprocessedContext, toInternalMessageTranscribedContext, } from "../../hooks/message-hook-mappers.js";
|
|
4
|
+
export function emitPreAgentMessageHooks(params) {
|
|
5
|
+
if (params.isFastTestEnv) {
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
const sessionKey = params.ctx.SessionKey?.trim();
|
|
9
|
+
if (!sessionKey) {
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
const canonical = deriveInboundMessageHookContext(params.ctx);
|
|
13
|
+
if (canonical.transcript) {
|
|
14
|
+
fireAndForgetHook(triggerInternalHook(createInternalHookEvent("message", "transcribed", sessionKey, toInternalMessageTranscribedContext(canonical, params.cfg))), "get-reply: message:transcribed internal hook failed");
|
|
15
|
+
}
|
|
16
|
+
fireAndForgetHook(triggerInternalHook(createInternalHookEvent("message", "preprocessed", sessionKey, toInternalMessagePreprocessedContext(canonical, params.cfg))), "get-reply: message:preprocessed internal hook failed");
|
|
17
|
+
}
|
package/dist/build-info.json
CHANGED
package/dist/cli/banner.js
CHANGED
|
@@ -1,9 +1,28 @@
|
|
|
1
|
+
import { loadConfig } from "../config/config.js";
|
|
1
2
|
import { resolveCommitHash } from "../infra/git-commit.js";
|
|
2
3
|
import { visibleWidth } from "../terminal/ansi.js";
|
|
3
4
|
import { isRich, theme } from "../terminal/theme.js";
|
|
4
5
|
import { hasRootVersionAlias } from "./argv.js";
|
|
5
6
|
import { pickTagline } from "./tagline.js";
|
|
6
7
|
import { resolveCliName } from "./cli-name.js";
|
|
8
|
+
function parseTaglineMode(value) {
|
|
9
|
+
if (value === "random" || value === "default" || value === "off") {
|
|
10
|
+
return value;
|
|
11
|
+
}
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
function resolveTaglineMode(options) {
|
|
15
|
+
const explicit = parseTaglineMode(options.mode);
|
|
16
|
+
if (explicit) {
|
|
17
|
+
return explicit;
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
return parseTaglineMode(loadConfig().cli?.banner?.taglineMode);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
7
26
|
let bannerEmitted = false;
|
|
8
27
|
const graphemeSegmenter = typeof Intl !== "undefined" && "Segmenter" in Intl
|
|
9
28
|
? new Intl.Segmenter(undefined, { granularity: "grapheme" })
|
|
@@ -23,7 +42,7 @@ const hasVersionFlag = (argv) => argv.some((arg) => arg === "--version" || arg =
|
|
|
23
42
|
export function formatCliBannerLine(version, options = {}) {
|
|
24
43
|
const commit = options.commit ?? resolveCommitHash({ env: options.env });
|
|
25
44
|
const commitLabel = commit ?? "unknown";
|
|
26
|
-
const tagline = pickTagline(options);
|
|
45
|
+
const tagline = pickTagline({ ...options, mode: resolveTaglineMode(options) });
|
|
27
46
|
const rich = options.richTty ?? isRich();
|
|
28
47
|
const cliName = resolveCliName(options.argv ?? process.argv);
|
|
29
48
|
const title = cliName === "poolbot" ? "🎱 Pool Bot" : "🎱 Pool Bot";
|
package/dist/cli/security-cli.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { loadConfig } from "../config/config.js";
|
|
1
|
+
import { loadConfig, writeConfigFile } from "../config/config.js";
|
|
2
2
|
import { defaultRuntime } from "../runtime.js";
|
|
3
3
|
import { runSecurityAudit } from "../security/audit.js";
|
|
4
4
|
import { fixSecurityFootguns } from "../security/fix.js";
|
|
5
|
+
import { CAPABILITY_TYPES } from "../security/capability.js";
|
|
5
6
|
import { formatDocsLink } from "../terminal/links.js";
|
|
6
7
|
import { isRich, theme } from "../terminal/theme.js";
|
|
7
8
|
import { shortenHomeInString, shortenHomePath } from "../utils.js";
|
|
@@ -17,10 +18,24 @@ function formatSummary(summary) {
|
|
|
17
18
|
parts.push(rich ? theme.muted(`${i} info`) : `${i} info`);
|
|
18
19
|
return parts.join(" · ");
|
|
19
20
|
}
|
|
21
|
+
function formatCapability(cap) {
|
|
22
|
+
let result = cap.type;
|
|
23
|
+
if ("pattern" in cap && cap.pattern)
|
|
24
|
+
result += ` pattern="${cap.pattern}"`;
|
|
25
|
+
if ("toolId" in cap && cap.toolId)
|
|
26
|
+
result += ` tool="${cap.toolId}"`;
|
|
27
|
+
if ("limit" in cap && cap.limit)
|
|
28
|
+
result += ` limit=${cap.limit}`;
|
|
29
|
+
if ("scope" in cap && cap.scope)
|
|
30
|
+
result += ` scope="${cap.scope}"`;
|
|
31
|
+
if ("port" in cap && cap.port)
|
|
32
|
+
result += ` port=${cap.port}`;
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
20
35
|
export function registerSecurityCli(program) {
|
|
21
36
|
const security = program
|
|
22
37
|
.command("security")
|
|
23
|
-
.description("Security tools (audit)")
|
|
38
|
+
.description("Security tools (audit, capabilities)")
|
|
24
39
|
.addHelpText("after", () => `\n${theme.muted("Docs:")} ${formatDocsLink("/cli/security", "docs.molt.bot/cli/security")}\n`);
|
|
25
40
|
security
|
|
26
41
|
.command("audit")
|
|
@@ -120,4 +135,198 @@ export function registerSecurityCli(program) {
|
|
|
120
135
|
render("info");
|
|
121
136
|
defaultRuntime.log(lines.join("\n"));
|
|
122
137
|
});
|
|
138
|
+
// Capability management commands
|
|
139
|
+
const capabilities = security
|
|
140
|
+
.command("capabilities")
|
|
141
|
+
.description("Manage agent capabilities (permission system)");
|
|
142
|
+
capabilities
|
|
143
|
+
.command("grant")
|
|
144
|
+
.description("Grant a capability to an agent or pattern")
|
|
145
|
+
.argument("<type>", `Capability type (${CAPABILITY_TYPES.join(", ")})`)
|
|
146
|
+
.option("-a, --agent <pattern>", "Agent ID pattern (e.g., 'agent-*', 'coder')", "*")
|
|
147
|
+
.option("-p, --pattern <glob>", "File path pattern for file:* capabilities")
|
|
148
|
+
.option("-t, --tool <name>", "Tool name for tool:invoke capability")
|
|
149
|
+
.option("-h, --host <hostname>", "Host pattern for net:connect capability")
|
|
150
|
+
.option("-m, --max-tokens <n>", "Max tokens for llm:maxTokens capability")
|
|
151
|
+
.option("--json", "Print JSON output")
|
|
152
|
+
.action(async (type, opts) => {
|
|
153
|
+
const rich = isRich();
|
|
154
|
+
const cfg = loadConfig();
|
|
155
|
+
// Validate capability type
|
|
156
|
+
if (!CAPABILITY_TYPES.includes(type)) {
|
|
157
|
+
defaultRuntime.log(rich
|
|
158
|
+
? theme.error(`Invalid capability type: ${type}`)
|
|
159
|
+
: `Invalid capability type: ${type}`);
|
|
160
|
+
defaultRuntime.log(`Valid types: ${CAPABILITY_TYPES.join(", ")}`);
|
|
161
|
+
process.exit(1);
|
|
162
|
+
}
|
|
163
|
+
// Build capability object
|
|
164
|
+
const capability = { type: type };
|
|
165
|
+
if (opts.pattern && (type.startsWith("file:") || type === "shell:exec")) {
|
|
166
|
+
capability.pattern = opts.pattern;
|
|
167
|
+
}
|
|
168
|
+
if (opts.tool && type === "tool:invoke") {
|
|
169
|
+
capability.toolId = opts.tool;
|
|
170
|
+
}
|
|
171
|
+
if (opts.host && type === "net:connect") {
|
|
172
|
+
capability.pattern = opts.host;
|
|
173
|
+
}
|
|
174
|
+
if (opts.maxTokens && type === "llm:maxTokens") {
|
|
175
|
+
capability.limit = parseInt(opts.maxTokens, 10);
|
|
176
|
+
}
|
|
177
|
+
// Initialize security config if needed
|
|
178
|
+
if (!cfg.security) {
|
|
179
|
+
cfg.security = { enabled: true };
|
|
180
|
+
}
|
|
181
|
+
if (!cfg.security.agents) {
|
|
182
|
+
cfg.security.agents = [];
|
|
183
|
+
}
|
|
184
|
+
// Find or create agent entry
|
|
185
|
+
const agentId = opts.agent || "*";
|
|
186
|
+
let agentEntry = cfg.security.agents.find((a) => a.agentId === agentId);
|
|
187
|
+
if (!agentEntry) {
|
|
188
|
+
agentEntry = { agentId, capabilities: [] };
|
|
189
|
+
cfg.security.agents.push(agentEntry);
|
|
190
|
+
}
|
|
191
|
+
// Add capability
|
|
192
|
+
agentEntry.capabilities.push(capability);
|
|
193
|
+
// Write config
|
|
194
|
+
await writeConfigFile(cfg);
|
|
195
|
+
if (opts.json) {
|
|
196
|
+
defaultRuntime.log(JSON.stringify({ success: true, capability, agentId }, null, 2));
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
defaultRuntime.log(rich
|
|
200
|
+
? theme.success(`Granted ${type} to agent "${agentId}"`)
|
|
201
|
+
: `Granted ${type} to agent "${agentId}"`);
|
|
202
|
+
defaultRuntime.log(formatCapability(capability));
|
|
203
|
+
});
|
|
204
|
+
capabilities
|
|
205
|
+
.command("revoke")
|
|
206
|
+
.description("Revoke capabilities from an agent")
|
|
207
|
+
.argument("[type]", `Capability type to revoke (omit to revoke all)`)
|
|
208
|
+
.option("-a, --agent <pattern>", "Agent ID pattern", "*")
|
|
209
|
+
.option("--json", "Print JSON output")
|
|
210
|
+
.action(async (type, opts) => {
|
|
211
|
+
const rich = isRich();
|
|
212
|
+
const cfg = loadConfig();
|
|
213
|
+
if (!cfg.security?.agents || cfg.security.agents.length === 0) {
|
|
214
|
+
if (opts.json) {
|
|
215
|
+
defaultRuntime.log(JSON.stringify({ success: true, removed: 0 }));
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
defaultRuntime.log(rich ? theme.muted("No capabilities configured") : "No capabilities configured");
|
|
219
|
+
}
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
const agentId = opts.agent || "*";
|
|
223
|
+
const agentEntry = cfg.security.agents.find((a) => a.agentId === agentId);
|
|
224
|
+
if (!agentEntry) {
|
|
225
|
+
if (opts.json) {
|
|
226
|
+
defaultRuntime.log(JSON.stringify({ success: true, removed: 0 }));
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
defaultRuntime.log(rich
|
|
230
|
+
? theme.muted(`No capabilities found for agent "${agentId}"`)
|
|
231
|
+
: `No capabilities found for agent "${agentId}"`);
|
|
232
|
+
}
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
const beforeCount = agentEntry.capabilities.length;
|
|
236
|
+
if (type) {
|
|
237
|
+
agentEntry.capabilities = agentEntry.capabilities.filter((c) => c.type !== type);
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
agentEntry.capabilities = [];
|
|
241
|
+
}
|
|
242
|
+
const removed = beforeCount - agentEntry.capabilities.length;
|
|
243
|
+
// Remove empty agent entries
|
|
244
|
+
cfg.security.agents = cfg.security.agents.filter((a) => a.capabilities.length > 0);
|
|
245
|
+
await writeConfigFile(cfg);
|
|
246
|
+
if (opts.json) {
|
|
247
|
+
defaultRuntime.log(JSON.stringify({ success: true, removed }));
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
if (removed === 0) {
|
|
251
|
+
defaultRuntime.log(rich ? theme.muted("No matching capabilities found") : "No matching capabilities found");
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
defaultRuntime.log(rich
|
|
255
|
+
? theme.success(`Revoked ${removed} capability(s) from agent "${agentId}"`)
|
|
256
|
+
: `Revoked ${removed} capability(s) from agent "${agentId}"`);
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
capabilities
|
|
260
|
+
.command("list")
|
|
261
|
+
.description("List agent capabilities")
|
|
262
|
+
.option("-a, --agent <pattern>", "Filter by agent ID pattern")
|
|
263
|
+
.option("--json", "Print JSON output")
|
|
264
|
+
.action(async (opts) => {
|
|
265
|
+
const rich = isRich();
|
|
266
|
+
const cfg = loadConfig();
|
|
267
|
+
const agents = cfg.security?.agents || [];
|
|
268
|
+
const filtered = opts.agent
|
|
269
|
+
? agents.filter((a) => a.agentId === opts.agent || (opts.agent && a.agentId.includes(opts.agent)))
|
|
270
|
+
: agents;
|
|
271
|
+
if (opts.json) {
|
|
272
|
+
defaultRuntime.log(JSON.stringify({ enabled: cfg.security?.enabled ?? false, agents: filtered }, null, 2));
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
if (filtered.length === 0) {
|
|
276
|
+
defaultRuntime.log(rich ? theme.muted("No capabilities configured") : "No capabilities configured");
|
|
277
|
+
defaultRuntime.log(`\nEnable capability system: ${formatCliCommand("poolbot config set security.enabled true")}`);
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
const lines = [];
|
|
281
|
+
lines.push(rich ? theme.heading("Agent Capabilities") : "Agent Capabilities");
|
|
282
|
+
lines.push(rich
|
|
283
|
+
? theme.muted(`Security enabled: ${cfg.security?.enabled ?? false}`)
|
|
284
|
+
: `Security enabled: ${cfg.security?.enabled ?? false}`);
|
|
285
|
+
lines.push("");
|
|
286
|
+
for (const agent of filtered) {
|
|
287
|
+
lines.push(rich ? theme.heading(agent.agentId) : agent.agentId);
|
|
288
|
+
for (const cap of agent.capabilities) {
|
|
289
|
+
lines.push(` ${formatCapability(cap)}`);
|
|
290
|
+
}
|
|
291
|
+
lines.push("");
|
|
292
|
+
}
|
|
293
|
+
defaultRuntime.log(lines.join("\n"));
|
|
294
|
+
});
|
|
295
|
+
capabilities
|
|
296
|
+
.command("enable")
|
|
297
|
+
.description("Enable the capability security system")
|
|
298
|
+
.option("--json", "Print JSON output")
|
|
299
|
+
.action(async (opts) => {
|
|
300
|
+
const cfg = loadConfig();
|
|
301
|
+
cfg.security = cfg.security || {};
|
|
302
|
+
cfg.security.enabled = true;
|
|
303
|
+
await writeConfigFile(cfg);
|
|
304
|
+
if (opts.json) {
|
|
305
|
+
defaultRuntime.log(JSON.stringify({ success: true, enabled: true }));
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
defaultRuntime.log(isRich()
|
|
309
|
+
? theme.success("Capability security system enabled")
|
|
310
|
+
: "Capability security system enabled");
|
|
311
|
+
defaultRuntime.log(`Run ${formatCliCommand("poolbot security capabilities list")} to view current grants`);
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
capabilities
|
|
315
|
+
.command("disable")
|
|
316
|
+
.description("Disable the capability security system (permissive mode)")
|
|
317
|
+
.option("--json", "Print JSON output")
|
|
318
|
+
.action(async (opts) => {
|
|
319
|
+
const cfg = loadConfig();
|
|
320
|
+
cfg.security = cfg.security || {};
|
|
321
|
+
cfg.security.enabled = false;
|
|
322
|
+
await writeConfigFile(cfg);
|
|
323
|
+
if (opts.json) {
|
|
324
|
+
defaultRuntime.log(JSON.stringify({ success: true, enabled: false }));
|
|
325
|
+
}
|
|
326
|
+
else {
|
|
327
|
+
defaultRuntime.log(isRich()
|
|
328
|
+
? theme.warn("Capability security system disabled (permissive mode)")
|
|
329
|
+
: "Capability security system disabled (permissive mode)");
|
|
330
|
+
}
|
|
331
|
+
});
|
|
123
332
|
}
|
package/dist/cli/tagline.js
CHANGED
|
@@ -191,6 +191,13 @@ export function activeTaglines(options = {}) {
|
|
|
191
191
|
return filtered.length > 0 ? filtered : TAGLINES;
|
|
192
192
|
}
|
|
193
193
|
export function pickTagline(options = {}) {
|
|
194
|
+
const mode = options.mode;
|
|
195
|
+
if (mode === "off") {
|
|
196
|
+
return "";
|
|
197
|
+
}
|
|
198
|
+
if (mode === "default") {
|
|
199
|
+
return DEFAULT_TAGLINE;
|
|
200
|
+
}
|
|
194
201
|
const env = options.env ?? process.env;
|
|
195
202
|
const override = env?.POOLBOT_TAGLINE_INDEX ?? env?.CLAWDBOT_TAGLINE_INDEX;
|
|
196
203
|
if (override !== undefined) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** Default permissive capabilities (when security is disabled). */
|
|
2
|
+
export const PERMISSIVE_CAPABILITIES = [
|
|
3
|
+
{ type: "file:read", pattern: "*" },
|
|
4
|
+
{ type: "file:write", pattern: "*" },
|
|
5
|
+
{ type: "net:connect", pattern: "*" },
|
|
6
|
+
{ type: "tool:all" },
|
|
7
|
+
{ type: "llm:query", pattern: "*" },
|
|
8
|
+
{ type: "llm:maxTokens", limit: Number.MAX_SAFE_INTEGER },
|
|
9
|
+
{ type: "agent:spawn" },
|
|
10
|
+
{ type: "agent:message", pattern: "*" },
|
|
11
|
+
{ type: "agent:kill", pattern: "*" },
|
|
12
|
+
{ type: "memory:read", scope: "*" },
|
|
13
|
+
{ type: "memory:write", scope: "*" },
|
|
14
|
+
{ type: "shell:exec", pattern: "*" },
|
|
15
|
+
{ type: "env:read", pattern: "*" },
|
|
16
|
+
{ type: "gateway:admin" },
|
|
17
|
+
{ type: "gateway:channels:read" },
|
|
18
|
+
{ type: "gateway:channels:write", pattern: "*" },
|
|
19
|
+
{ type: "econ:spend", limit: Number.MAX_SAFE_INTEGER },
|
|
20
|
+
{ type: "econ:earn" },
|
|
21
|
+
{ type: "econ:transfer", pattern: "*" },
|
|
22
|
+
];
|
|
23
|
+
/** Restricted capabilities for untrusted agents. */
|
|
24
|
+
export const RESTRICTED_CAPABILITIES = [
|
|
25
|
+
{ type: "file:read", pattern: "/data/*" },
|
|
26
|
+
{ type: "net:connect", pattern: "*.openai.com:443" },
|
|
27
|
+
{ type: "tool:invoke", toolId: "web_search" },
|
|
28
|
+
{ type: "tool:invoke", toolId: "file_read" },
|
|
29
|
+
{ type: "llm:query", pattern: "gpt-4*" },
|
|
30
|
+
{ type: "llm:maxTokens", limit: 10000 },
|
|
31
|
+
{ type: "memory:read", scope: "session/*" },
|
|
32
|
+
{ type: "memory:write", scope: "session/*" },
|
|
33
|
+
];
|
|
@@ -6,6 +6,7 @@ import { HexColorSchema, ModelsConfigSchema } from "./zod-schema.core.js";
|
|
|
6
6
|
import { HookMappingSchema, HooksGmailSchema, InternalHooksSchema } from "./zod-schema.hooks.js";
|
|
7
7
|
import { InstallRecordShape } from "./zod-schema.installs.js";
|
|
8
8
|
import { ChannelsSchema } from "./zod-schema.providers.js";
|
|
9
|
+
import { SecuritySchema } from "./zod-schema.security.js";
|
|
9
10
|
import { sensitive } from "./zod-schema.sensitive.js";
|
|
10
11
|
import { CommandsSchema, MessagesSchema, SessionSchema, SessionSendPolicySchema, } from "./zod-schema.session.js";
|
|
11
12
|
const BrowserSnapshotDefaultsSchema = z
|
|
@@ -641,6 +642,20 @@ export const PoolBotSchema = z
|
|
|
641
642
|
})
|
|
642
643
|
.strict()
|
|
643
644
|
.optional(),
|
|
645
|
+
cli: z
|
|
646
|
+
.object({
|
|
647
|
+
banner: z
|
|
648
|
+
.object({
|
|
649
|
+
taglineMode: z
|
|
650
|
+
.union([z.literal("random"), z.literal("default"), z.literal("off")])
|
|
651
|
+
.optional(),
|
|
652
|
+
})
|
|
653
|
+
.strict()
|
|
654
|
+
.optional(),
|
|
655
|
+
})
|
|
656
|
+
.strict()
|
|
657
|
+
.optional(),
|
|
658
|
+
security: SecuritySchema,
|
|
644
659
|
})
|
|
645
660
|
.strict()
|
|
646
661
|
.superRefine((cfg, ctx) => {
|
|
@@ -583,6 +583,7 @@ export const SlackAccountSchema = z
|
|
|
583
583
|
heartbeat: ChannelHeartbeatVisibilitySchema,
|
|
584
584
|
responsePrefix: z.string().optional(),
|
|
585
585
|
ackReaction: z.string().optional(),
|
|
586
|
+
typingReaction: z.string().optional(),
|
|
586
587
|
})
|
|
587
588
|
.strict()
|
|
588
589
|
.superRefine((value, ctx) => {
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zod schema for security configuration.
|
|
3
|
+
*/
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
/** Capability type schema - matches the TypeScript Capability type. */
|
|
6
|
+
const CapabilitySchema = z.union([
|
|
7
|
+
// File capabilities
|
|
8
|
+
z.object({
|
|
9
|
+
type: z.literal("file:read"),
|
|
10
|
+
pattern: z.string(),
|
|
11
|
+
}),
|
|
12
|
+
z.object({
|
|
13
|
+
type: z.literal("file:write"),
|
|
14
|
+
pattern: z.string(),
|
|
15
|
+
}),
|
|
16
|
+
// Network capabilities
|
|
17
|
+
z.object({
|
|
18
|
+
type: z.literal("net:connect"),
|
|
19
|
+
pattern: z.string(),
|
|
20
|
+
}),
|
|
21
|
+
// Tool capabilities
|
|
22
|
+
z.object({
|
|
23
|
+
type: z.literal("tool:invoke"),
|
|
24
|
+
toolId: z.string(),
|
|
25
|
+
}),
|
|
26
|
+
z.object({
|
|
27
|
+
type: z.literal("tool:all"),
|
|
28
|
+
}),
|
|
29
|
+
// LLM capabilities
|
|
30
|
+
z.object({
|
|
31
|
+
type: z.literal("llm:query"),
|
|
32
|
+
pattern: z.string(),
|
|
33
|
+
}),
|
|
34
|
+
z.object({
|
|
35
|
+
type: z.literal("llm:maxTokens"),
|
|
36
|
+
limit: z.number().int().positive(),
|
|
37
|
+
}),
|
|
38
|
+
// Agent capabilities
|
|
39
|
+
z.object({
|
|
40
|
+
type: z.literal("agent:spawn"),
|
|
41
|
+
}),
|
|
42
|
+
z.object({
|
|
43
|
+
type: z.literal("agent:message"),
|
|
44
|
+
pattern: z.string(),
|
|
45
|
+
}),
|
|
46
|
+
z.object({
|
|
47
|
+
type: z.literal("agent:kill"),
|
|
48
|
+
pattern: z.string(),
|
|
49
|
+
}),
|
|
50
|
+
// Memory capabilities
|
|
51
|
+
z.object({
|
|
52
|
+
type: z.literal("memory:read"),
|
|
53
|
+
scope: z.string(),
|
|
54
|
+
}),
|
|
55
|
+
z.object({
|
|
56
|
+
type: z.literal("memory:write"),
|
|
57
|
+
scope: z.string(),
|
|
58
|
+
}),
|
|
59
|
+
// Shell capabilities
|
|
60
|
+
z.object({
|
|
61
|
+
type: z.literal("shell:exec"),
|
|
62
|
+
pattern: z.string(),
|
|
63
|
+
}),
|
|
64
|
+
// Environment capabilities
|
|
65
|
+
z.object({
|
|
66
|
+
type: z.literal("env:read"),
|
|
67
|
+
pattern: z.string(),
|
|
68
|
+
}),
|
|
69
|
+
// Gateway capabilities
|
|
70
|
+
z.object({
|
|
71
|
+
type: z.literal("gateway:admin"),
|
|
72
|
+
}),
|
|
73
|
+
z.object({
|
|
74
|
+
type: z.literal("gateway:channels:read"),
|
|
75
|
+
}),
|
|
76
|
+
z.object({
|
|
77
|
+
type: z.literal("gateway:channels:write"),
|
|
78
|
+
pattern: z.string(),
|
|
79
|
+
}),
|
|
80
|
+
// Economic capabilities
|
|
81
|
+
z.object({
|
|
82
|
+
type: z.literal("econ:spend"),
|
|
83
|
+
limit: z.number().int().nonnegative(),
|
|
84
|
+
}),
|
|
85
|
+
z.object({
|
|
86
|
+
type: z.literal("econ:earn"),
|
|
87
|
+
}),
|
|
88
|
+
z.object({
|
|
89
|
+
type: z.literal("econ:transfer"),
|
|
90
|
+
pattern: z.string(),
|
|
91
|
+
}),
|
|
92
|
+
]);
|
|
93
|
+
/** Agent capability configuration schema. */
|
|
94
|
+
const AgentCapabilityConfigSchema = z
|
|
95
|
+
.object({
|
|
96
|
+
agentId: z.string(),
|
|
97
|
+
capabilities: z.array(CapabilitySchema),
|
|
98
|
+
})
|
|
99
|
+
.strict();
|
|
100
|
+
/** Security configuration schema. */
|
|
101
|
+
export const SecuritySchema = z
|
|
102
|
+
.object({
|
|
103
|
+
/** Enable capability enforcement. Default: false (permissive mode). */
|
|
104
|
+
enabled: z.boolean().optional(),
|
|
105
|
+
/** Default capabilities for agents without explicit config. */
|
|
106
|
+
defaultCapabilities: z.array(CapabilitySchema).optional(),
|
|
107
|
+
/** Per-agent capability configurations. */
|
|
108
|
+
agents: z.array(AgentCapabilityConfigSchema).optional(),
|
|
109
|
+
/** Capabilities for the system/root agent. */
|
|
110
|
+
systemCapabilities: z.array(CapabilitySchema).optional(),
|
|
111
|
+
})
|
|
112
|
+
.strict()
|
|
113
|
+
.optional();
|
|
@@ -66,7 +66,8 @@ export async function preflightDiscordMessage(params) {
|
|
|
66
66
|
logVerbose(`discord: drop message ${message.id} (missing channel id)`);
|
|
67
67
|
return null;
|
|
68
68
|
}
|
|
69
|
-
const
|
|
69
|
+
const allowBotsSetting = params.discordConfig?.allowBots;
|
|
70
|
+
const allowBotsMode = allowBotsSetting === "mentions" ? "mentions" : allowBotsSetting === true ? "all" : "off";
|
|
70
71
|
if (params.botUserId && author.id === params.botUserId) {
|
|
71
72
|
// Always ignore own messages to prevent self-reply loops
|
|
72
73
|
return null;
|
|
@@ -92,7 +93,7 @@ export async function preflightDiscordMessage(params) {
|
|
|
92
93
|
pluralkitInfo,
|
|
93
94
|
});
|
|
94
95
|
if (author.bot) {
|
|
95
|
-
if (
|
|
96
|
+
if (allowBotsMode === "off" && !sender.isPluralKit) {
|
|
96
97
|
logVerbose("discord: drop bot message (allowBots=false)");
|
|
97
98
|
return null;
|
|
98
99
|
}
|
|
@@ -523,6 +524,14 @@ export async function preflightDiscordMessage(params) {
|
|
|
523
524
|
});
|
|
524
525
|
return null;
|
|
525
526
|
}
|
|
527
|
+
if (author.bot && !sender.isPluralKit && allowBotsMode === "mentions") {
|
|
528
|
+
const botMentioned = isDirectMessage || wasMentioned || implicitMention;
|
|
529
|
+
if (!botMentioned) {
|
|
530
|
+
logDebug(`[discord-preflight] drop: bot message missing mention (allowBots=mentions)`);
|
|
531
|
+
logVerbose("discord: drop bot message (allowBots=mentions, missing mention)");
|
|
532
|
+
return null;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
526
535
|
if (!messageText) {
|
|
527
536
|
logDebug(`[discord-preflight] drop: empty content`);
|
|
528
537
|
logVerbose(`discord: drop message ${message.id} (empty content)`);
|
|
@@ -5,9 +5,14 @@ import { readJsonBody } from "./hooks.js";
|
|
|
5
5
|
* Content-Security-Policy are intentionally omitted here because some handlers
|
|
6
6
|
* (canvas host, A2UI) serve content that may be loaded inside frames.
|
|
7
7
|
*/
|
|
8
|
-
export function setDefaultSecurityHeaders(res) {
|
|
8
|
+
export function setDefaultSecurityHeaders(res, opts) {
|
|
9
9
|
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
10
10
|
res.setHeader("Referrer-Policy", "no-referrer");
|
|
11
|
+
res.setHeader("Permissions-Policy", "camera=(), microphone=(), geolocation=()");
|
|
12
|
+
const strictTransportSecurity = opts?.strictTransportSecurity;
|
|
13
|
+
if (typeof strictTransportSecurity === "string" && strictTransportSecurity.length > 0) {
|
|
14
|
+
res.setHeader("Strict-Transport-Security", strictTransportSecurity);
|
|
15
|
+
}
|
|
11
16
|
}
|
|
12
17
|
export function sendJson(res, status, body) {
|
|
13
18
|
res.statusCode = status;
|