Governed substrate for autonomous agents: scoped identity (passports), audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
165 lines
6.1 KiB
JavaScript
165 lines
6.1 KiB
JavaScript
/**
|
|
* @swagger
|
|
* tags:
|
|
* - name: AgentVersions
|
|
* description: ADR-0007-S G1-data — agent_versions registry (mint / promote / retire)
|
|
*/
|
|
|
|
/**
|
|
* Agent Versions API Routes — ADR-0007-S G1-data write path.
|
|
*
|
|
* Thin HTTP surface over AgentVersionService.js (the canonical handlers; the
|
|
* orphan kebab `agent-versions/` build was dropped during consolidation). This
|
|
* is the API G2 (`@agent-smith`) calls to self-host versioned `@smith-*`
|
|
* agents: mint a revision, promote a winner, retire, list lineage, resolve the
|
|
* current active revision.
|
|
*
|
|
* The single-active-per-agent invariant is enforced at the service/DB layer
|
|
* (migration 073 partial unique index) — these routes are a transport shell and
|
|
* add no scoring/promotion logic (that stays Genesis-lock-clean: G6 promotion
|
|
* is LLM-rerank via search-rerank, ADR-0007-S §6.1, not a hand-tuned scorer).
|
|
*
|
|
* Mounted at /api/v3 (the router declares two base paths, so it cannot mount
|
|
* under a single /agent-versions prefix). Full paths:
|
|
* GET /api/v3/agents/:agentRowId/versions list lineage (newest first)
|
|
* GET /api/v3/agents/:agentRowId/versions/active current active revision
|
|
* POST /api/v3/agents/:agentRowId/versions mint a revision
|
|
* GET /api/v3/agent-versions/:id fetch one revision
|
|
* POST /api/v3/agent-versions/:id/promote promote → active (atomic)
|
|
* POST /api/v3/agent-versions/:id/retire retire (idempotent)
|
|
*/
|
|
|
|
import { Router } from 'express';
|
|
import { authenticate } from '../../middleware/auth.js';
|
|
import { apiLogger } from '../../utils/logger.js';
|
|
import { success, created, notFound, badRequest, error } from '../../utils/response.js';
|
|
import {
|
|
mintAgentVersion,
|
|
promoteAgentVersion,
|
|
retireAgentVersion,
|
|
getAgentVersion,
|
|
listAgentVersions,
|
|
getActiveVersion,
|
|
} from '../../services/AgentVersionService.js';
|
|
|
|
const router = Router();
|
|
|
|
// Map a service Error.code to an HTTP response. Keeps error semantics in one
|
|
// place so every handler maps the same tagged errors consistently.
|
|
function mapServiceError(res, err, fallbackCode) {
|
|
switch (err.code) {
|
|
case 'VALIDATION':
|
|
return badRequest(res, err.message, 'VALIDATION');
|
|
case 'AGENT_NOT_FOUND':
|
|
case 'VERSION_NOT_FOUND':
|
|
return notFound(res, err.message);
|
|
case 'NOT_AN_AGENT':
|
|
return error(res, 'NOT_AN_AGENT', err.message, 422);
|
|
default:
|
|
apiLogger.error({ err }, `[agent-versions] ${fallbackCode}`);
|
|
return error(res, fallbackCode, err.message, 500);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* GET /agents/:agentRowId/versions — list all revisions of an agent (newest first).
|
|
*/
|
|
router.get('/agents/:agentRowId/versions', authenticate, async (req, res) => {
|
|
try {
|
|
const agentRowId = Number(req.params.agentRowId);
|
|
if (!Number.isInteger(agentRowId)) return badRequest(res, 'agentRowId must be an integer');
|
|
const rows = await listAgentVersions(agentRowId);
|
|
return success(res, rows);
|
|
} catch (err) {
|
|
return mapServiceError(res, err, 'LIST_VERSIONS_ERROR');
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /agents/:agentRowId/versions/active — the current active revision, or 404.
|
|
*/
|
|
router.get('/agents/:agentRowId/versions/active', authenticate, async (req, res) => {
|
|
try {
|
|
const agentRowId = Number(req.params.agentRowId);
|
|
if (!Number.isInteger(agentRowId)) return badRequest(res, 'agentRowId must be an integer');
|
|
const row = await getActiveVersion(agentRowId);
|
|
if (!row) return notFound(res, 'No active version for this agent');
|
|
return success(res, row);
|
|
} catch (err) {
|
|
return mapServiceError(res, err, 'GET_ACTIVE_VERSION_ERROR');
|
|
}
|
|
});
|
|
|
|
/**
|
|
* POST /agents/:agentRowId/versions — mint a new revision.
|
|
* Body: { agentSlug?, prompt?, config?, rubricScore?, trafficPct?, parentVersion?, activate? }
|
|
* Genesis (first revision) is active by default; later mints are draft unless activate:true.
|
|
*/
|
|
router.post('/agents/:agentRowId/versions', authenticate, async (req, res) => {
|
|
try {
|
|
const agentRowId = Number(req.params.agentRowId);
|
|
if (!Number.isInteger(agentRowId)) return badRequest(res, 'agentRowId must be an integer');
|
|
const body = req.body || {};
|
|
const row = await mintAgentVersion({
|
|
agentRowId,
|
|
agentSlug: body.agentSlug,
|
|
prompt: body.prompt,
|
|
config: body.config,
|
|
rubricScore: body.rubricScore,
|
|
trafficPct: body.trafficPct,
|
|
parentVersion: body.parentVersion,
|
|
activate: body.activate,
|
|
createdBy: req.user?.id ?? body.createdBy ?? null,
|
|
});
|
|
return created(res, row);
|
|
} catch (err) {
|
|
return mapServiceError(res, err, 'MINT_VERSION_ERROR');
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /agent-versions/:id — fetch a single revision.
|
|
*/
|
|
router.get('/agent-versions/:id', authenticate, async (req, res) => {
|
|
try {
|
|
const id = Number(req.params.id);
|
|
if (!Number.isInteger(id)) return badRequest(res, 'id must be an integer');
|
|
const row = await getAgentVersion(id);
|
|
if (!row) return notFound(res, 'Agent version not found');
|
|
return success(res, row);
|
|
} catch (err) {
|
|
return mapServiceError(res, err, 'GET_VERSION_ERROR');
|
|
}
|
|
});
|
|
|
|
/**
|
|
* POST /agent-versions/:id/promote — promote this revision to active.
|
|
* Atomically retires the prior active revision (single-active invariant).
|
|
*/
|
|
router.post('/agent-versions/:id/promote', authenticate, async (req, res) => {
|
|
try {
|
|
const id = Number(req.params.id);
|
|
if (!Number.isInteger(id)) return badRequest(res, 'id must be an integer');
|
|
const row = await promoteAgentVersion(id);
|
|
return success(res, row);
|
|
} catch (err) {
|
|
return mapServiceError(res, err, 'PROMOTE_VERSION_ERROR');
|
|
}
|
|
});
|
|
|
|
/**
|
|
* POST /agent-versions/:id/retire — retire this revision (idempotent).
|
|
*/
|
|
router.post('/agent-versions/:id/retire', authenticate, async (req, res) => {
|
|
try {
|
|
const id = Number(req.params.id);
|
|
if (!Number.isInteger(id)) return badRequest(res, 'id must be an integer');
|
|
const row = await retireAgentVersion(id);
|
|
if (!row) return notFound(res, 'Agent version not found');
|
|
return success(res, row);
|
|
} catch (err) {
|
|
return mapServiceError(res, err, 'RETIRE_VERSION_ERROR');
|
|
}
|
|
});
|
|
|
|
export default router;
|