import * as aa from '../db/repos/agent_actions.js'; import { loadActions } from './registry.js'; import { powerGuest } from './channels/proxmox.js'; import { restartService } from './channels/ssh.js'; // Single choke point. Dispatches one whitelisted action to its channel. The // registry + channels are injectable for tests; production wiring uses defaults. export function makeActionService({ registry = loadActions(), channels = { powerGuest, restartService } } = {}) { async function execute(a) { if (a.kind === 'guest_power') return channels.powerGuest({ node: a.node, vmid: a.vmid, op: a.op, kindPath: a.kindPath || 'lxc' }); if (a.kind === 'service_restart') return channels.restartService({ ip: registry.hostIp(a.host), actionId: a.id }); throw new Error(`unknown kind: ${a.kind}`); } async function run(actionId, actor, agent_id = null) { const a = registry.get(actionId); if (!a) throw new Error(`unknown action: ${actionId}`); if (a.tier === 'risky') { const row = await aa.create({ action_id: a.id, tier: a.tier, params: {}, agent_id, requested_by: actor }); return { queued: true, action_row_id: row.id }; } const result = await execute(a); const row = await aa.create({ action_id: a.id, tier: a.tier, agent_id, requested_by: actor }); await aa.resolve(row.id, 'executed', result, actor); return { executed: true, result }; } async function approve(rowId, owner) { const row = await aa.getById(rowId); if (!row || row.status !== 'pending') throw new Error('not a pending action'); const a = registry.get(row.action_id); if (!a) throw new Error(`unknown action: ${row.action_id}`); try { const result = await execute(a); return aa.resolve(rowId, 'executed', result, owner); } catch (e) { return aa.resolve(rowId, 'failed', { error: String(e?.message || e) }, owner); } } const reject = (rowId, owner) => aa.resolve(rowId, 'rejected', null, owner); return { run, approve, reject, list: () => registry.list() }; }