feat(tools): add kubernetes homelab awareness tools
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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
@@ -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`
|
||||
|
||||
@@ -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
@@ -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