import { createCurrentNotificationChannelRepository, createCurrentAutomationRepository, createCurrentCommandHistoryRepository, createCurrentFleetRepository, createCurrentHomepageItemRepository, createCurrentHostRepository, } from "../../../../../src/backend/database/repositories/factory.js"; import { num, objectSchema, type AiTool } from "./types.js"; import { listSavedWorkspaces, getNetworkTopology, listSnippets, } from "../services.js"; /** * Read tools project explicit fields rather than spreading rows. Redaction runs * afterwards as a second line of defense, but the projection here is the * primary control: a field that is never selected cannot leak. */ interface HostSummary { id: number; name: string | null; ip: string | null; port: number | null; username: string | null; folder: string | null; tags: unknown; protocol: string | null; enableTerminal: boolean | null; enableFileManager: boolean | null; enableTunnel: boolean | null; enableDocker: boolean | null; } function toHostSummary(host: Record): HostSummary { return { id: host.id, name: host.name ?? null, ip: host.ip ?? null, port: host.port ?? null, username: host.username ?? null, folder: host.folder ?? null, tags: host.tags ?? null, protocol: host.protocol ?? null, enableTerminal: host.enableTerminal ?? null, enableFileManager: host.enableFileManager ?? null, enableTunnel: host.enableTunnel ?? null, enableDocker: host.enableDocker ?? null, }; } export const readTools: AiTool[] = [ { name: "list_hosts", description: "List the user's SSH hosts with their names, addresses, folders and tags. Never returns passwords or keys. Call this before proposing anything that references a host.", category: "read", parameters: objectSchema({}), handler: async (_args, context) => { const hosts = await createCurrentHostRepository().listByUserId( context.userId, ); return { hosts: hosts.map((host) => toHostSummary(host as any)) }; }, }, { name: "get_host", description: "Get one host's non-secret configuration by id. Use it to inspect settings before proposing an update.", category: "read", parameters: objectSchema( { hostId: num("The host id, as returned by list_hosts") }, ["hostId"], ), handler: async (args, context) => { const hostId = Number(args.hostId); const host = await createCurrentHostRepository().findByIdForUser( context.userId, hostId, ); if (!host) return { error: "Host not found" }; return { host: toHostSummary(host as any) }; }, }, { name: "list_fleets", description: "List the user's fleets. Fleets group hosts for bulk operations and inventory.", category: "read", parameters: objectSchema({}), handler: async (_args, context) => { const fleets = await createCurrentFleetRepository().listByUser( context.userId, ); return { fleets: fleets.map((fleet: any) => ({ id: fleet.id, name: fleet.name, description: fleet.description ?? null, })), }; }, }, { name: "list_snippets", description: "List the user's saved command snippets, including their folder and the command text. Unavailable if the snippets plugin is disabled.", category: "read", parameters: objectSchema({}), handler: async (_args, context) => { const snippets = await listSnippets(context.userId); if (!snippets) return { unavailable: true, snippets: [] }; return { snippets: snippets.map((snippet) => ({ id: snippet.id, name: snippet.name, content: snippet.content, folder: snippet.folder ?? null, description: snippet.description ?? null, })), }; }, }, { name: "list_automations", description: "List the user's automations with their trigger kind and enabled state. Read this before proposing a change to an existing automation.", category: "read", parameters: objectSchema({}), handler: async (_args, context) => { const automations = await createCurrentAutomationRepository().list( context.userId, ); return { automations: automations.map((automation: any) => ({ id: automation.id, name: automation.name, description: automation.description ?? null, enabled: automation.enabled, lastRunAt: automation.lastRunAt ?? null, lastRunStatus: automation.lastRunStatus ?? null, })), }; }, }, { name: "get_automation", description: "Get one automation's full definition (trigger and steps) by id.", category: "read", parameters: objectSchema( { automationId: num("The automation id, as returned by list_automations"), }, ["automationId"], ), handler: async (args, context) => { const automation = await createCurrentAutomationRepository().findForUser( Number(args.automationId), context.userId, ); if (!automation) return { error: "Automation not found" }; return { automation: { id: (automation as any).id, name: (automation as any).name, enabled: (automation as any).enabled, definition: (automation as any).definition, }, }; }, }, { name: "list_workspaces", description: "List the user's saved workspace layouts (named sets of open tabs and splits).", category: "read", parameters: objectSchema({}), handler: async (_args, context) => { const workspaces = await listSavedWorkspaces(context.userId); if (workspaces === null) return { workspaces: [], unavailable: true }; return { workspaces }; }, }, { name: "list_notification_channels", description: "List the user's notification channels by id, name and type. Channel configuration is never returned because it holds tokens.", category: "read", parameters: objectSchema({}), handler: async (_args, context) => { const channels = await createCurrentNotificationChannelRepository().listNotificationChannels( context.userId, ); return { channels: channels.map((channel) => ({ id: channel.id, name: channel.name, type: channel.type, enabled: channel.enabled === 1, })), }; }, }, { name: "list_homepage_items", description: "List the user's homepage service-link tiles.", category: "read", parameters: objectSchema({}), handler: async (_args, context) => { const items = await createCurrentHomepageItemRepository().listByUserId( context.userId, ); return { items: (items as any[]).map((item) => ({ id: item.id, typeId: item.typeId, title: item.title ?? null, })), }; }, }, { name: "get_command_history", description: "Recent commands the user has run on one host, newest first. Useful for understanding what they have been working on.", category: "read", parameters: objectSchema( { hostId: num("The host id, as returned by list_hosts"), limit: num("How many entries to return (default 25, max 100)"), }, ["hostId"], ), handler: async (args, context) => { const limit = Math.min(Math.max(Number(args.limit) || 25, 1), 100); const hostId = Number(args.hostId); // Ownership is enforced here rather than trusted from the model. const host = await createCurrentHostRepository().findByIdForUser( context.userId, hostId, ); if (!host) return { error: "Host not found" }; const commands = await createCurrentCommandHistoryRepository().listCommandsForHost( context.userId, hostId, limit, ); return { commands }; }, }, { name: "get_network_topology", description: "The user's saved network topology graph, if they have one.", category: "read", parameters: objectSchema({}), handler: async (_args, context) => { const topology = await getNetworkTopology(context.userId); if (topology === null) return { topology: null, unavailable: true }; return { topology }; }, }, ];