feat(tools): add kubernetes homelab awareness tools

This commit is contained in:
William Valentin
2026-02-16 14:31:33 -08:00
parent 21c986b671
commit 63df791b26
14 changed files with 501 additions and 4 deletions
+17
View File
@@ -28,6 +28,7 @@ Self-hosted personal AI assistant with Telegram and Terminal interfaces.
- **Scheduled Backups**: Interval- or cron-based snapshot backups with optional startup run
- **MinIO File Sharing Tool**: Upload a local file and return a temporary MinIO share link via `minio.share`
- **MinIO Knowledge Ingestion Tool**: Pull text-like objects from MinIO into memory namespaces via `minio.ingest`
- **Kubernetes Homelab Tools**: Inspect pods/deployments and fetch logs via `k8s.pods`, `k8s.deployments`, `k8s.logs`
- **Gmail Pub/Sub Watcher**: Monitor Gmail inbox via Google Cloud Pub/Sub push notifications with polling fallback
- **Vector Memory Search**: Hybrid keyword + semantic search with embeddings (OpenAI, Gemini, Ollama, llama.cpp, Voyage AI)
- **Docker Deployment**: Multi-stage Dockerfile and docker-compose.yml for production containers
@@ -629,6 +630,22 @@ When `backup.minio.enabled` is configured, Flynn also exposes MinIO tools:
- `minio.ingest`: read a text-like object from MinIO and append/replace a memory namespace (useful for syncing notes/runbooks into long-term memory)
- `minio.sync`: recursively ingest a MinIO prefix into nested memory namespaces with object and size limits
## Kubernetes Tools
Optional Kubernetes tools are available when `k8s.enabled: true`:
- `k8s.pods`: list pod status (phase, ready count, restarts)
- `k8s.deployments`: list deployment replica readiness
- `k8s.logs`: fetch recent pod logs
```yaml
k8s:
enabled: true
kubectl_path: kubectl
default_namespace: observability
allowed_namespaces: [observability, platform]
```
## Inbound Webhooks
HTTP endpoints that trigger agent processing. Each webhook accepts POST requests, optionally verifies an HMAC signature, renders a message template, and routes the agent's response to an output channel.
+7
View File
@@ -186,6 +186,13 @@ models:
# Then reference them in fallback_chain:
# fallback_chain: [ollama, llamacpp, local]
# Optional: Kubernetes / homelab awareness tools (k8s.pods, k8s.deployments, k8s.logs)
# k8s:
# enabled: false
# kubectl_path: kubectl
# default_namespace: default
# allowed_namespaces: [] # Empty = allow any namespace; set to restrict access.
hooks:
confirm:
- shell.*
+49 -1
View File
@@ -28,6 +28,7 @@ Tools are executable capabilities that the AI agent can call to perform actions
- **Browser**: `browser.navigate`, `browser.screenshot`
- **Memory**: `memory.read`, `memory.write`, `memory.search`
- **MinIO**: `minio.share`, `minio.ingest`, `minio.sync`
- **Kubernetes**: `k8s.pods`, `k8s.deployments`, `k8s.logs`
- **Media**: `media.send`, `image.analyze`, `audio.transcribe`
- **System**: `system.info`
- **Session**: `sessions.list`, `sessions.delete`
@@ -467,7 +468,7 @@ Tools are organized into groups:
- `group:web`: Web and browser tools
- `group:memory`: Memory and search tools
There are additional groups for specific integrations (gmail/gcal/gdocs/gdrive/gtasks/cron/minio). See `TOOL_GROUPS` in `src/tools/policy.ts`.
There are additional groups for specific integrations (gmail/gcal/gdocs/gdrive/gtasks/cron/minio/k8s). See `TOOL_GROUPS` in `src/tools/policy.ts`.
### Policy Resolution
@@ -1053,6 +1054,53 @@ Sync text-like objects from a MinIO prefix into nested memory namespaces.
}
```
### Kubernetes Tools
#### `k8s.pods`
List Kubernetes pods with status summary.
#### `k8s.deployments`
List Kubernetes deployments with replica readiness summary.
#### `k8s.logs`
Fetch recent logs for a pod.
```json
{
"name": "k8s.logs",
"description": "Fetch recent logs for a Kubernetes pod.",
"inputSchema": {
"type": "object",
"properties": {
"pod": {
"type": "string",
"description": "Pod name"
},
"namespace": {
"type": "string",
"description": "Namespace"
},
"container": {
"type": "string",
"description": "Container name (optional)"
},
"lines": {
"type": "number",
"description": "Tail line count"
},
"since": {
"type": "string",
"description": "Lookback duration (e.g. 10m, 1h)"
}
},
"required": ["pod"]
}
}
```
### Media Tools
#### `media.send`
+23
View File
@@ -118,6 +118,29 @@
],
"test_status": "pnpm test:run src/automation/minioSync.test.ts src/config/schema.test.ts + pnpm typecheck passing"
},
"k8s-homelab-awareness-tools": {
"status": "completed",
"date": "2026-02-16",
"updated": "2026-02-16",
"summary": "Added optional Kubernetes homelab-awareness tools (`k8s.pods`, `k8s.deployments`, `k8s.logs`) with `k8s.enabled` config gating, default/allowed namespace constraints, daemon registration, tool-policy group support (`group:k8s`), docs, and tests.",
"files_modified": [
"src/tools/builtin/k8s.ts",
"src/tools/builtin/k8s.test.ts",
"src/tools/builtin/index.ts",
"src/tools/index.ts",
"src/daemon/index.ts",
"src/config/schema.ts",
"src/config/schema.test.ts",
"src/config/index.ts",
"src/tools/policy.ts",
"src/tools/policy.test.ts",
"config/default.yaml",
"README.md",
"docs/api/TOOLS.md",
"docs/plans/state.json"
],
"test_status": "pnpm test:run src/tools/builtin/k8s.test.ts src/tools/policy.test.ts src/config/schema.test.ts + pnpm typecheck passing"
},
"backup-session-summary-audit-trail": {
"status": "completed",
"date": "2026-02-16",
+1 -1
View File
@@ -1,3 +1,3 @@
export { loadConfig, deepMerge } from './loader.js';
export { persistConfig } from './persistence.js';
export { configSchema, MODEL_PROVIDERS, type ModelProvider, type Config, type TelegramConfig, type ModelConfig, type CronJobConfig, type AgentsConfig, type CompactionConfig, type ToolProfile, type ToolOverrideConfig, type ToolsConfig, type SandboxConfig, type AgentConfigEntry, type RoutingConfig, type ServerConfig, type BackupConfig } from './schema.js';
export { configSchema, MODEL_PROVIDERS, type ModelProvider, type Config, type TelegramConfig, type ModelConfig, type CronJobConfig, type AgentsConfig, type CompactionConfig, type ToolProfile, type ToolOverrideConfig, type ToolsConfig, type SandboxConfig, type AgentConfigEntry, type RoutingConfig, type ServerConfig, type BackupConfig, type K8sConfig } from './schema.js';
+29
View File
@@ -849,6 +849,35 @@ describe('configSchema — skills watcher', () => {
});
});
describe('configSchema — k8s', () => {
const baseConfig = {
telegram: { bot_token: 'test-token', allowed_chat_ids: [123] },
models: { default: { provider: 'anthropic', model: 'claude-sonnet' } },
};
it('accepts config without k8s section', () => {
const result = configSchema.parse(baseConfig);
expect(result.k8s).toBeUndefined();
});
it('accepts k8s config with namespace restrictions', () => {
const result = configSchema.parse({
...baseConfig,
k8s: {
enabled: true,
kubectl_path: '/usr/local/bin/kubectl',
default_namespace: 'observability',
allowed_namespaces: ['observability', 'platform'],
},
});
expect(result.k8s?.enabled).toBe(true);
expect(result.k8s?.kubectl_path).toBe('/usr/local/bin/kubectl');
expect(result.k8s?.default_namespace).toBe('observability');
expect(result.k8s?.allowed_namespaces).toEqual(['observability', 'platform']);
});
});
describe('configSchema automation', () => {
const baseConfig = {
telegram: { bot_token: 'test-token', allowed_chat_ids: [123] },
+9
View File
@@ -605,6 +605,13 @@ const processSchema = z.object({
buffer_size: z.number().min(1024).max(1048576).default(65536),
}).default({});
const k8sSchema = z.object({
enabled: z.boolean().default(false),
kubectl_path: z.string().default('kubectl'),
default_namespace: z.string().optional(),
allowed_namespaces: z.array(z.string()).default([]),
}).optional();
const retrySchema = z.object({
enabled: z.boolean().default(true),
max_retries: z.number().min(0).max(10).default(3),
@@ -817,6 +824,7 @@ export const configSchema = z.object({
memory: memorySchema,
process: processSchema,
browser: browserSchema,
k8s: k8sSchema,
retry: retrySchema,
web_search: webSearchSchema,
audio: audioSchema,
@@ -846,6 +854,7 @@ export type WebSearchConfig = z.infer<typeof webSearchSchema>;
export type AudioConfig = z.infer<typeof audioSchema>;
export type ProcessConfig = z.infer<typeof processSchema>;
export type BrowserConfig = z.infer<typeof browserSchema>;
export type K8sConfig = z.infer<typeof k8sSchema>;
export type DiscordConfig = z.infer<typeof discordSchema>;
export type SlackConfig = z.infer<typeof slackSchema>;
export type WhatsAppConfig = z.infer<typeof whatsappSchema>;
+6 -1
View File
@@ -27,7 +27,7 @@ import { RoutingPolicy } from '../routing/index.js';
import type { ModelRouter } from '../models/index.js';
import { SessionStore, SessionManager, parseDuration } from '../session/index.js';
import { HookEngine } from '../hooks/index.js';
import { createSessionTools, createAgentsListTool, createMessageSendTool, createCronTools, createGmailTools, createGcalTools, createGdocsTools, createGdriveTools, createGtasksTools, createMinioShareTool, createMinioIngestTool, createMinioSyncTool } from '../tools/index.js';
import { createSessionTools, createAgentsListTool, createMessageSendTool, createCronTools, createGmailTools, createGcalTools, createGdocsTools, createGdriveTools, createGtasksTools, createMinioShareTool, createMinioIngestTool, createMinioSyncTool, createK8sTools } from '../tools/index.js';
import { ChannelRegistry } from '../channels/index.js';
import type { McpManager } from '../mcp/index.js';
import type { SkillRegistry, SkillInstaller } from '../skills/index.js';
@@ -198,6 +198,11 @@ export async function startDaemon(config: Config, options?: StartDaemonOptions):
toolRegistry.register(createMinioSyncTool(config.backup, memoryStore));
}
}
if (config.k8s?.enabled) {
for (const tool of createK8sTools(config.k8s)) {
toolRegistry.register(tool);
}
}
// ── Lifecycle ──
await startServices({ config, lifecycle, channelRegistry, gateway, modelRouter, memoryStore, memoryDir, dataDir });
+1
View File
@@ -30,6 +30,7 @@ export { createGtasksTools } from './gtasks.js';
export { createMinioShareTool } from './minio-share.js';
export { createMinioIngestTool } from './minio-ingest.js';
export { createMinioSyncTool } from './minio-sync.js';
export { createK8sTools } from './k8s.js';
export { screenCaptureTool, cameraCaptureTool } from './capture.js';
import type { Tool } from '../types.js';
+91
View File
@@ -0,0 +1,91 @@
import { describe, expect, it, vi } from 'vitest';
import { createK8sTools } from './k8s.js';
import type { K8sConfig } from '../../config/index.js';
function makeConfig(overrides?: Partial<NonNullable<K8sConfig>>): NonNullable<K8sConfig> {
return {
enabled: true,
kubectl_path: 'kubectl',
default_namespace: 'default',
allowed_namespaces: [],
...overrides,
};
}
describe('createK8sTools', () => {
it('lists pods', async () => {
const runner = vi.fn(async () => ({
stdout: JSON.stringify({
items: [{
metadata: { namespace: 'default', name: 'api-123' },
status: {
phase: 'Running',
containerStatuses: [{ ready: true, restartCount: 1 }],
},
}],
}),
stderr: '',
}));
const tools = createK8sTools(makeConfig(), { runner });
const podsTool = tools.find((t) => t.name === 'k8s.pods');
if (!podsTool) {throw new Error('k8s.pods tool missing');}
const result = await podsTool.execute({});
expect(result.success).toBe(true);
expect(result.output).toContain('default/api-123');
expect(result.output).toContain('phase=Running');
expect(runner).toHaveBeenCalledWith('kubectl', ['get', 'pods', '-o', 'json', '-n', 'default'], expect.any(Object));
});
it('lists deployments across all namespaces', async () => {
const runner = vi.fn(async () => ({
stdout: JSON.stringify({
items: [{
metadata: { namespace: 'ops', name: 'worker' },
spec: { replicas: 3 },
status: { readyReplicas: 2, availableReplicas: 2 },
}],
}),
stderr: '',
}));
const tools = createK8sTools(makeConfig(), { runner });
const tool = tools.find((t) => t.name === 'k8s.deployments');
if (!tool) {throw new Error('k8s.deployments tool missing');}
const result = await tool.execute({ all_namespaces: true });
expect(result.success).toBe(true);
expect(result.output).toContain('ops/worker');
expect(result.output).toContain('replicas=2/3');
});
it('fetches logs with tail + since', async () => {
const runner = vi.fn(async () => ({
stdout: 'line1\nline2\n',
stderr: '',
}));
const tools = createK8sTools(makeConfig({ default_namespace: 'ops' }), { runner });
const tool = tools.find((t) => t.name === 'k8s.logs');
if (!tool) {throw new Error('k8s.logs tool missing');}
const result = await tool.execute({ pod: 'api-123', lines: 50, since: '10m' });
expect(result.success).toBe(true);
expect(result.output).toContain('line1');
expect(runner).toHaveBeenCalledWith(
'kubectl',
['logs', 'api-123', '--tail', '50', '-n', 'ops', '--since', '10m'],
expect.any(Object),
);
});
it('blocks unauthorized namespaces', async () => {
const runner = vi.fn();
const tools = createK8sTools(makeConfig({ allowed_namespaces: ['observability'] }), { runner });
const tool = tools.find((t) => t.name === 'k8s.logs');
if (!tool) {throw new Error('k8s.logs tool missing');}
const result = await tool.execute({ pod: 'api-123', namespace: 'default' });
expect(result.success).toBe(false);
expect(result.error).toContain('not allowed');
expect(runner).not.toHaveBeenCalled();
});
});
+244
View File
@@ -0,0 +1,244 @@
import { promisify } from 'node:util';
import { execFile } from 'node:child_process';
import type { Tool, ToolResult } from '../types.js';
import type { K8sConfig } from '../../config/index.js';
const execFileAsync = promisify(execFile);
type Runner = (
file: string,
args: string[],
options?: { env?: NodeJS.ProcessEnv; maxBuffer?: number },
) => Promise<{ stdout: string; stderr: string }>;
interface PodItem {
metadata?: { name?: string; namespace?: string };
status?: { phase?: string; containerStatuses?: Array<{ ready?: boolean; restartCount?: number }> };
}
interface DeploymentItem {
metadata?: { name?: string; namespace?: string };
spec?: { replicas?: number };
status?: { readyReplicas?: number; availableReplicas?: number };
}
function resolveNamespace(argsNamespace: string | undefined, cfg: NonNullable<K8sConfig>): string | undefined {
return argsNamespace ?? cfg.default_namespace;
}
function assertAllowedNamespace(namespace: string | undefined, cfg: NonNullable<K8sConfig>): string | null {
if (!namespace) {return null;}
if (!cfg.allowed_namespaces || cfg.allowed_namespaces.length === 0) {
return null;
}
return cfg.allowed_namespaces.includes(namespace)
? null
: `Namespace "${namespace}" is not allowed by k8s.allowed_namespaces`;
}
function parseJson<T>(value: string): T {
return JSON.parse(value) as T;
}
function formatPodReady(statuses?: Array<{ ready?: boolean }>): string {
const total = statuses?.length ?? 0;
const ready = statuses?.filter((s) => s.ready).length ?? 0;
return `${ready}/${total}`;
}
function formatPodRestarts(statuses?: Array<{ restartCount?: number }>): number {
return (statuses ?? []).reduce((sum, s) => sum + (s.restartCount ?? 0), 0);
}
export interface K8sToolDeps {
runner?: Runner;
}
export function createK8sTools(config: NonNullable<K8sConfig>, deps?: K8sToolDeps): Tool[] {
const runner = deps?.runner ?? (async (file: string, args: string[], options?: { env?: NodeJS.ProcessEnv; maxBuffer?: number }) => {
return execFileAsync(file, args, options);
});
const kubectl = config.kubectl_path;
const podsTool: Tool = {
name: 'k8s.pods',
description: 'List Kubernetes pods in a namespace or across all namespaces.',
inputSchema: {
type: 'object',
properties: {
namespace: { type: 'string', description: 'Namespace to query (defaults to k8s.default_namespace)' },
all_namespaces: { type: 'boolean', description: 'Query all namespaces' },
selector: { type: 'string', description: 'Label selector (e.g. "app=api")' },
limit: { type: 'number', description: 'Maximum pods to display (default 25)' },
},
required: [],
},
execute: async (rawArgs: unknown): Promise<ToolResult> => {
const args = rawArgs as { namespace?: string; all_namespaces?: boolean; selector?: string; limit?: number };
try {
if (args.all_namespaces && config.allowed_namespaces.length > 0) {
return {
success: false,
output: '',
error: 'all_namespaces=true is not allowed when k8s.allowed_namespaces is configured',
};
}
const namespace = resolveNamespace(args.namespace, config);
const nsError = assertAllowedNamespace(namespace, config);
if (nsError) {
return { success: false, output: '', error: nsError };
}
const cmdArgs = ['get', 'pods', '-o', 'json'];
if (args.all_namespaces) {
cmdArgs.push('-A');
} else if (namespace) {
cmdArgs.push('-n', namespace);
}
if (args.selector) {
cmdArgs.push('-l', args.selector);
}
const { stdout } = await runner(kubectl, cmdArgs, { maxBuffer: 10 * 1024 * 1024 });
const parsed = parseJson<{ items: PodItem[] }>(typeof stdout === 'string' ? stdout : stdout.toString('utf-8'));
const limit = Math.max(1, Math.floor(args.limit ?? 25));
const items = (parsed.items ?? []).slice(0, limit);
if (items.length === 0) {
return { success: true, output: 'No pods found.' };
}
const lines = items.map((item) => {
const ns = item.metadata?.namespace ?? '-';
const name = item.metadata?.name ?? '-';
const phase = item.status?.phase ?? 'Unknown';
const ready = formatPodReady(item.status?.containerStatuses);
const restarts = formatPodRestarts(item.status?.containerStatuses);
return `- ${ns}/${name} phase=${phase} ready=${ready} restarts=${restarts}`;
});
return {
success: true,
output: `Pods (${items.length} shown):\n${lines.join('\n')}`,
};
} catch (error) {
return { success: false, output: '', error: error instanceof Error ? error.message : String(error) };
}
},
};
const deploymentsTool: Tool = {
name: 'k8s.deployments',
description: 'List Kubernetes deployments in a namespace or across all namespaces.',
inputSchema: {
type: 'object',
properties: {
namespace: { type: 'string', description: 'Namespace to query (defaults to k8s.default_namespace)' },
all_namespaces: { type: 'boolean', description: 'Query all namespaces' },
selector: { type: 'string', description: 'Label selector (e.g. "app=api")' },
limit: { type: 'number', description: 'Maximum deployments to display (default 25)' },
},
required: [],
},
execute: async (rawArgs: unknown): Promise<ToolResult> => {
const args = rawArgs as { namespace?: string; all_namespaces?: boolean; selector?: string; limit?: number };
try {
if (args.all_namespaces && config.allowed_namespaces.length > 0) {
return {
success: false,
output: '',
error: 'all_namespaces=true is not allowed when k8s.allowed_namespaces is configured',
};
}
const namespace = resolveNamespace(args.namespace, config);
const nsError = assertAllowedNamespace(namespace, config);
if (nsError) {
return { success: false, output: '', error: nsError };
}
const cmdArgs = ['get', 'deployments', '-o', 'json'];
if (args.all_namespaces) {
cmdArgs.push('-A');
} else if (namespace) {
cmdArgs.push('-n', namespace);
}
if (args.selector) {
cmdArgs.push('-l', args.selector);
}
const { stdout } = await runner(kubectl, cmdArgs, { maxBuffer: 10 * 1024 * 1024 });
const parsed = parseJson<{ items: DeploymentItem[] }>(typeof stdout === 'string' ? stdout : stdout.toString('utf-8'));
const limit = Math.max(1, Math.floor(args.limit ?? 25));
const items = (parsed.items ?? []).slice(0, limit);
if (items.length === 0) {
return { success: true, output: 'No deployments found.' };
}
const lines = items.map((item) => {
const ns = item.metadata?.namespace ?? '-';
const name = item.metadata?.name ?? '-';
const desired = item.spec?.replicas ?? 0;
const ready = item.status?.readyReplicas ?? 0;
const available = item.status?.availableReplicas ?? 0;
return `- ${ns}/${name} replicas=${ready}/${desired} available=${available}`;
});
return {
success: true,
output: `Deployments (${items.length} shown):\n${lines.join('\n')}`,
};
} catch (error) {
return { success: false, output: '', error: error instanceof Error ? error.message : String(error) };
}
},
};
const logsTool: Tool = {
name: 'k8s.logs',
description: 'Fetch recent logs for a Kubernetes pod.',
inputSchema: {
type: 'object',
properties: {
pod: { type: 'string', description: 'Pod name' },
namespace: { type: 'string', description: 'Namespace (defaults to k8s.default_namespace)' },
container: { type: 'string', description: 'Container name (if pod has multiple containers)' },
lines: { type: 'number', description: 'Tail lines (default 200, max 2000)' },
since: { type: 'string', description: 'Only return logs newer than duration (e.g. "10m", "1h")' },
},
required: ['pod'],
},
execute: async (rawArgs: unknown): Promise<ToolResult> => {
const args = rawArgs as { pod: string; namespace?: string; container?: string; lines?: number; since?: string };
try {
const namespace = resolveNamespace(args.namespace, config);
const nsError = assertAllowedNamespace(namespace, config);
if (nsError) {
return { success: false, output: '', error: nsError };
}
const tail = Math.max(1, Math.min(2000, Math.floor(args.lines ?? 200)));
const cmdArgs = ['logs', args.pod, '--tail', String(tail)];
if (namespace) {
cmdArgs.push('-n', namespace);
}
if (args.container) {
cmdArgs.push('-c', args.container);
}
if (args.since) {
cmdArgs.push('--since', args.since);
}
const { stdout } = await runner(kubectl, cmdArgs, { maxBuffer: 10 * 1024 * 1024 });
const text = typeof stdout === 'string' ? stdout : stdout.toString('utf-8');
const body = text.trim();
return {
success: true,
output: body.length > 0 ? body : '(no log output)',
};
} catch (error) {
return { success: false, output: '', error: error instanceof Error ? error.message : String(error) };
}
},
};
return [podsTool, deploymentsTool, logsTool];
}
+1 -1
View File
@@ -5,7 +5,7 @@ export { ToolExecutor } from './executor.js';
export type { ToolExecutorConfig } from './executor.js';
export { ToolPolicy } from './policy.js';
export type { ToolPolicyContext } from './policy.js';
export { allBuiltinTools, createWebSearchTools, createProcessTools, ProcessManager, BrowserManager, createBrowserTools, createMediaSendTool, createAudioTranscribeTool, createSessionTools, createAgentsListTool, createMessageSendTool, createCronTools, createGmailTools, createGcalTools, createGdocsTools, createGdriveTools, createGtasksTools, createMinioShareTool, createMinioIngestTool, createMinioSyncTool } from './builtin/index.js';
export { allBuiltinTools, createWebSearchTools, createProcessTools, ProcessManager, BrowserManager, createBrowserTools, createMediaSendTool, createAudioTranscribeTool, createSessionTools, createAgentsListTool, createMessageSendTool, createCronTools, createGmailTools, createGcalTools, createGdocsTools, createGdriveTools, createGtasksTools, createMinioShareTool, createMinioIngestTool, createMinioSyncTool, createK8sTools } from './builtin/index.js';
export type { WebSearchConfig } from './builtin/web-search.js';
export type { ProcessManagerConfig } from './builtin/process/index.js';
export type { BrowserManagerConfig } from './builtin/browser/index.js';
+16
View File
@@ -20,6 +20,9 @@ const ALL_TOOL_NAMES = [
'minio.share',
'minio.ingest',
'minio.sync',
'k8s.pods',
'k8s.deployments',
'k8s.logs',
'process.start',
'process.status',
'process.output',
@@ -497,6 +500,19 @@ describe('ToolPolicy', () => {
expect(names).not.toContain('shell.exec');
});
it('expands group:k8s', () => {
const policy = new ToolPolicy(defaultConfig({
profile: 'minimal',
allow: ['group:k8s'],
}));
const result = policy.filterTools(ALL_TOOLS);
const names = result.map(t => t.name);
expect(names).toContain('k8s.pods');
expect(names).toContain('k8s.deployments');
expect(names).toContain('k8s.logs');
expect(names).not.toContain('shell.exec');
});
it('unknown group name passes through as literal', () => {
const policy = new ToolPolicy(defaultConfig({
profile: 'minimal',
+7
View File
@@ -42,6 +42,9 @@ const PROFILE_TOOLS: Record<ToolProfile, Set<string>> = {
'minio.share',
'minio.ingest',
'minio.sync',
'k8s.pods',
'k8s.deployments',
'k8s.logs',
]),
coding: new Set([
'file.read',
@@ -73,6 +76,9 @@ const PROFILE_TOOLS: Record<ToolProfile, Set<string>> = {
'minio.share',
'minio.ingest',
'minio.sync',
'k8s.pods',
'k8s.deployments',
'k8s.logs',
'file.write',
'file.edit',
'file.patch',
@@ -109,6 +115,7 @@ export const TOOL_GROUPS: Record<string, string[]> = {
'group:gtasks': ['tasks.lists', 'tasks.list'],
'group:cron': ['cron.list', 'cron.trigger', 'cron.create', 'cron.delete'],
'group:minio': ['minio.share', 'minio.ingest', 'minio.sync'],
'group:k8s': ['k8s.pods', 'k8s.deployments', 'k8s.logs'],
};
/** Expand group references in a list of tool names/patterns. */