@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.
@@ -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 withAbort;
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
+ }
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "2026.3.6",
3
- "commit": "def3163876b28829de26ed4eb0c2166b051434ae",
4
- "builtAt": "2026-03-06T12:13:28.765Z"
2
+ "version": "2026.3.7",
3
+ "commit": "2bb47cd934fe9cb9d58ee7210ee3997714a07f09",
4
+ "builtAt": "2026-03-07T19:13:06.611Z"
5
5
  }
@@ -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";
@@ -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
  }
@@ -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 allowBots = params.discordConfig?.allowBots ?? false;
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 (!allowBots && !sender.isPluralKit) {
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;
@@ -0,0 +1,6 @@
1
+ import { logVerbose } from "../globals.js";
2
+ export function fireAndForgetHook(task, label, logger = logVerbose) {
3
+ void task.catch((err) => {
4
+ logger(`${label}: ${String(err)}`);
5
+ });
6
+ }