feat(tools): add kubernetes homelab awareness tools
This commit is contained in:
+1
-1
@@ -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';
|
||||
|
||||
@@ -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] },
|
||||
|
||||
@@ -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
@@ -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 });
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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
@@ -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';
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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. */
|
||||
|
||||
Reference in New Issue
Block a user