import type { FastifyInstance } from 'fastify'; import { z } from 'zod'; import { getHermesTelemetrySnapshot, getHermesTokenUsageSnapshot } from './repository.js'; import { HermesInstanceIdSchema, HermesTelemetrySnapshotSchema, HermesTokenUsageSnapshotSchema } from './types.js'; import { requireAdmin } from '../../lib/auth.js'; const ParamsSchema = z.object({ instance: HermesInstanceIdSchema }); export async function hermesTelemetryRoutes(fastify: FastifyInstance) { // GET /api/hermes/telemetry/:instance // Admin-only: this endpoint shells out to `hermes` CLI in the instance // owner's environment (`runuser -u uma --` for Bheem) and reads the // watchdog log + backup repo. Treat it as privileged the same way the // VM/system endpoints are. See `dashboard/DEPLOYMENT.md` Privilege Surface. fastify.get('/hermes/telemetry/:instance', { preHandler: async (req) => requireAdmin(req), }, async (req, reply) => { let params: z.infer; try { params = ParamsSchema.parse(req.params); } catch (err) { return reply.code(400).send({ error: 'Invalid instance', detail: (err as Error).message }); } try { const snapshot = await getHermesTelemetrySnapshot(params.instance); // Validate our own response so a shape regression surfaces here as a // 500 rather than a corrupt UI state — same approach as hermes-ops. const validated = HermesTelemetrySnapshotSchema.parse(snapshot); return reply.send(validated); } catch (err) { fastify.log.error(err, 'failed to build hermes telemetry snapshot'); return reply.code(500).send({ error: 'Failed to build hermes telemetry snapshot' }); } }); // GET /api/hermes/telemetry/:instance/token-usage // Admin-only: aggregate/session-level token analytics from Hermes state.db. // The repository deliberately avoids selecting raw message content. fastify.get('/hermes/telemetry/:instance/token-usage', { preHandler: async (req) => requireAdmin(req), }, async (req, reply) => { let params: z.infer; try { params = ParamsSchema.parse(req.params); } catch (err) { return reply.code(400).send({ error: 'Invalid instance', detail: (err as Error).message }); } try { const snapshot = await getHermesTokenUsageSnapshot(params.instance); const validated = HermesTokenUsageSnapshotSchema.parse(snapshot); return reply.send(validated); } catch (err) { fastify.log.error(err, 'failed to build hermes token usage snapshot'); return reply.code(500).send({ error: 'Failed to build hermes token usage snapshot' }); } }); }