import { Router } from 'express'; import { z } from 'zod'; import * as repo from '../../db/repos/agents.js'; import { validate } from '../validate.js'; import { requireOwner } from '../cap.js'; import { NotFoundError, ValidationError, asyncWrap } from '../errors.js'; const KINDS = ['claude', 'ollama', 'mastra', 'mcp-client', 'external']; const createSchema = z.object({ slug: z.string().min(1).max(64).regex(/^[a-z0-9-]+$/), name: z.string().min(1).max(200), kind: z.enum(KINDS), model: z.string().nullable().optional(), persona_path: z.string().nullable().optional(), capabilities: z.record(z.string(), z.any()).optional(), scopes: z.record(z.string(), z.any()).optional() }); const capsSchema = z.object({ capabilities: z.record(z.string(), z.any()), scopes: z.record(z.string(), z.any()).optional() }); const tokenSchema = z.object({ label: z.string().min(1).max(200).optional() }); const idParams = z.object({ id: z.string().uuid() }); const tokenParams = z.object({ token_id: z.string().uuid() }); export const router = Router(); export const tokensRouter = Router(); router.use(requireOwner); tokensRouter.use(requireOwner); router.get('/', asyncWrap(async (_req, res) => { res.json(await repo.list()); })); // Token metadata across all agents (label/usage/revocation; never the hash) for Settings. tokensRouter.get('/', asyncWrap(async (_req, res) => { res.json(await repo.listTokenMeta()); })); router.post('/', validate({ body: createSchema }), asyncWrap(async (req, res) => { try { const row = await repo.create(req.body, req.actor); res.status(201).json(row); } catch (e) { if (e.code === '23505') throw new ValidationError('agent slug already used', { slug: req.body.slug }); throw e; } }) ); router.get('/:id', validate({ params: idParams }), asyncWrap(async (req, res) => { const row = await repo.getById(req.params.id); if (!row) throw new NotFoundError('agent not found'); res.json(row); }) ); router.patch('/:id/capabilities', validate({ params: idParams, body: capsSchema }), asyncWrap(async (req, res) => { const existing = await repo.getById(req.params.id); if (!existing) throw new NotFoundError('agent not found'); res.json(await repo.setCapabilities(req.params.id, req.body.capabilities, req.body.scopes)); }) ); // Mint a token. Plaintext is returned EXACTLY ONCE; future reads only // see the bcrypt hash. router.post('/:id/tokens', validate({ params: idParams, body: tokenSchema }), asyncWrap(async (req, res) => { const existing = await repo.getById(req.params.id); if (!existing) throw new NotFoundError('agent not found'); const { token, id } = await repo.createToken(req.params.id, req.body.label); res.status(201).json({ id, token }); }) ); tokensRouter.delete('/:token_id', validate({ params: tokenParams }), asyncWrap(async (req, res) => { await repo.revokeToken(req.params.token_id); res.status(204).end(); }) );