Files
flynn/src/config/schema.ts
T
William Valentin ee0af0cc06 feat: add tool allow/deny profiles with per-agent and per-provider filtering
Implements configurable tool filtering with four built-in profiles
(minimal, messaging, coding, full), global and per-agent/per-provider
allow/deny lists with glob pattern support, and defense-in-depth
enforcement at both tool listing and execution time.

New: src/tools/policy.ts (ToolPolicy engine), src/tools/policy.test.ts (37 tests)
Modified: config schema, tool registry, tool executor, NativeAgent,
AgentOrchestrator, daemon wiring, gateway tool handler, test mocks
2026-02-06 15:30:34 -08:00

231 lines
8.1 KiB
TypeScript

import { z } from 'zod';
const telegramSchema = z.object({
bot_token: z.string().min(1, 'Bot token is required'),
allowed_chat_ids: z.array(z.number()).min(1, 'At least one chat ID required'),
});
const serverSchema = z.object({
tailscale_only: z.boolean().default(true),
localhost: z.boolean().default(true),
port: z.number().default(18800),
});
const modelConfigSchema = z.object({
provider: z.enum(['anthropic', 'openai', 'gemini', 'ollama', 'llamacpp']),
model: z.string(),
endpoint: z.string().optional(),
api_key: z.string().optional(),
auth_token: z.string().optional(),
for: z.array(z.string()).optional(),
num_gpu: z.number().optional(),
context_window: z.number().optional(),
});
const modelsSchema = z.object({
local: modelConfigSchema.optional(),
fast: modelConfigSchema.optional(),
default: modelConfigSchema,
complex: modelConfigSchema.optional(),
fallback_chain: z.array(z.string()).default(['anthropic']),
local_providers: z.record(z.string(), modelConfigSchema).optional(),
});
const backendsSchema = z.object({
claude_code: z.object({
enabled: z.boolean().default(false),
path: z.string().optional(),
}).default({ enabled: false }),
opencode: z.object({
enabled: z.boolean().default(false),
path: z.string().optional(),
}).default({ enabled: false }),
native: z.object({
enabled: z.boolean().default(true),
}).default({ enabled: true }),
}).default({});
const hooksSchema = z.object({
confirm: z.array(z.string()).default([]),
log: z.array(z.string()).default([]),
silent: z.array(z.string()).default([]),
}).default({});
const skillsSchema = z.object({
/** Directory for user-created workspace skills. */
workspace_dir: z.string().optional(),
/** Directory for managed (installed) skills. Defaults to ~/.flynn/workspace/skills. */
managed_dir: z.string().optional(),
/** Directory for bundled skills shipped with Flynn. */
bundled_dir: z.string().optional(),
}).default({});
const mcpServerSchema = z.object({
name: z.string(),
command: z.string(),
args: z.array(z.string()).default([]),
env: z.record(z.string(), z.string()).optional(),
cwd: z.string().optional(),
});
const mcpSchema = z.object({
servers: z.array(mcpServerSchema).default([]),
}).default({ servers: [] });
const cronJobSchema = z.object({
name: z.string().min(1, 'Cron job name is required'),
schedule: z.string().min(1, 'Cron schedule is required'),
message: z.string().min(1, 'Cron message is required'),
output: z.object({
channel: z.string().min(1),
peer: z.string().min(1),
}),
enabled: z.boolean().default(true),
timezone: z.string().optional(),
});
const automationSchema = z.object({
cron: z.array(cronJobSchema).default([]),
}).default({});
const agentsSchema = z.object({
primary_tier: z.enum(['fast', 'default', 'complex', 'local']).default('default'),
delegation: z.object({
compaction: z.enum(['fast', 'default', 'complex', 'local']).default('fast'),
memory_extraction: z.enum(['fast', 'default', 'complex', 'local']).default('fast'),
classification: z.enum(['fast', 'default', 'complex', 'local']).default('fast'),
tool_summarisation: z.enum(['fast', 'default', 'complex', 'local']).default('fast'),
complex_reasoning: z.enum(['fast', 'default', 'complex', 'local']).default('complex'),
}).default({
compaction: 'fast',
memory_extraction: 'fast',
classification: 'fast',
tool_summarisation: 'fast',
complex_reasoning: 'complex',
}),
auto_escalate: z.boolean().default(false),
max_delegation_depth: z.number().min(1).max(10).default(3),
}).default({});
const memorySchema = z.object({
enabled: z.boolean().default(true),
dir: z.string().optional(), // Default: ~/.local/share/flynn/memory
auto_extract: z.boolean().default(true),
max_context_tokens: z.number().min(100).max(10000).default(2000),
}).default({});
const compactionSchema = z.object({
enabled: z.boolean().default(true),
threshold_pct: z.number().min(10).max(100).default(80),
keep_turns: z.number().min(1).max(50).default(4),
summary_max_tokens: z.number().min(128).max(4096).default(1024),
}).default({});
const discordSchema = z.object({
bot_token: z.string().min(1, 'Bot token is required'),
allowed_guild_ids: z.array(z.string()).default([]),
allowed_channel_ids: z.array(z.string()).default([]),
require_mention: z.boolean().default(true),
}).optional();
const slackSchema = z.object({
bot_token: z.string().min(1, 'Bot token is required'),
app_token: z.string().min(1, 'App token is required'),
signing_secret: z.string().min(1, 'Signing secret is required'),
allowed_channel_ids: z.array(z.string()).default([]),
}).optional();
const whatsappSchema = z.object({
allowed_numbers: z.array(z.string()).default([]),
data_dir: z.string().optional(),
}).optional();
const processSchema = z.object({
max_concurrent: z.number().min(1).max(50).default(10),
max_runtime_minutes: z.number().min(1).max(1440).default(60),
buffer_size: z.number().min(1024).max(1048576).default(65536),
}).default({});
const retrySchema = z.object({
enabled: z.boolean().default(true),
max_retries: z.number().min(0).max(10).default(3),
initial_delay_ms: z.number().min(100).max(60000).default(1000),
backoff_multiplier: z.number().min(1).max(5).default(2),
max_delay_ms: z.number().min(1000).max(120000).default(30000),
}).default({});
const webSearchSchema = z.object({
provider: z.enum(['brave', 'searxng']).default('brave'),
api_key: z.string().optional(),
endpoint: z.string().optional(),
max_results: z.number().min(1).max(20).default(5),
}).default({});
// ── Tool policy schemas ──────────────────────────────────────────────
const toolProfileEnum = z.enum(['minimal', 'messaging', 'coding', 'full']);
const toolOverrideSchema = z.object({
profile: toolProfileEnum.optional(),
allow: z.array(z.string()).default([]),
deny: z.array(z.string()).default([]),
}).default({});
const toolsSchema = z.object({
profile: toolProfileEnum.default('full'),
allow: z.array(z.string()).default([]),
deny: z.array(z.string()).default([]),
agents: z.record(z.string(), toolOverrideSchema).default({}),
providers: z.record(z.string(), toolOverrideSchema).default({}),
}).default({});
const promptSchema = z.object({
/** Additional directories to search for prompt template files. */
search_dirs: z.array(z.string()).default([]),
/** Extra named sections to include in the system prompt. */
extra_sections: z.array(z.object({
name: z.string(),
content: z.string(),
})).default([]),
}).default({});
export const configSchema = z.object({
telegram: telegramSchema,
discord: discordSchema,
slack: slackSchema,
whatsapp: whatsappSchema,
server: serverSchema.default({}),
models: modelsSchema,
backends: backendsSchema.default({}),
hooks: hooksSchema.default({}),
skills: skillsSchema.default({}),
mcp: mcpSchema.default({ servers: [] }),
automation: automationSchema,
agents: agentsSchema,
compaction: compactionSchema,
memory: memorySchema,
process: processSchema,
retry: retrySchema,
web_search: webSearchSchema,
prompt: promptSchema,
tools: toolsSchema,
});
export type Config = z.infer<typeof configSchema>;
export type TelegramConfig = z.infer<typeof telegramSchema>;
export type ModelConfig = z.infer<typeof modelConfigSchema>;
export type CronJobConfig = z.infer<typeof cronJobSchema>;
export type AgentsConfig = z.infer<typeof agentsSchema>;
export type CompactionConfig = z.infer<typeof compactionSchema>;
export type MemoryConfig = z.infer<typeof memorySchema>;
export type WebSearchConfig = z.infer<typeof webSearchSchema>;
export type ProcessConfig = z.infer<typeof processSchema>;
export type DiscordConfig = z.infer<typeof discordSchema>;
export type SlackConfig = z.infer<typeof slackSchema>;
export type WhatsAppConfig = z.infer<typeof whatsappSchema>;
export type RetryPolicyConfig = z.infer<typeof retrySchema>;
export type PromptConfig = z.infer<typeof promptSchema>;
export type ToolProfile = z.infer<typeof toolProfileEnum>;
export type ToolOverrideConfig = z.infer<typeof toolOverrideSchema>;
export type ToolsConfig = z.infer<typeof toolsSchema>;