namespace marlin.web.Controllers.Api; using marlin.core.models; using marlin.core.util; using marlin.web.Data; using marlin.web.Models; using marlin.web.Util; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; [Route("api/dispatch")] [ApiController] [Authorize(Policy = "Agent")] public class DispatchController(ApplicationDbContext dbContext) : ControllerBase { [HttpPost("handshake")] public async Task> Handshake(AgentStatus status, CancellationToken ct) { var agentId = this.User.RequireAgentId(); var agent = await dbContext.Agents .FirstAsync(x => x.Id == agentId, ct); foreach (var capability in status.CapabilitySet.Items) agent.Capabilities.Items[capability.Key] = capability.Value; await dbContext.SaveChangesAsync(ct); return new AgentConfiguration( Name: agent.Name, MaxConcurrentJobs: agent.MaxConcurrentJobs ); } [HttpGet("queue")] public async Task> Queue(CancellationToken ct) { var agentId = this.User.RequireAgentId(); var nextJob = await dbContext .Jobs .Include(x => x.Source) .Include(x => x.Branch) .Include(x => x.Commit) .Where(x => x.Status == JobStatus.Pending && x.AgentId == null) .OrderBy(x => x.CreatedAt) .FirstOrDefaultAsync(ct); if (nextJob == null) return (DispatchJob?)null; string claimToken = SecureTokenGenerator.CreateClaimToken(); nextJob.ClaimToken = claimToken; nextJob.AgentId = agentId; await dbContext.SaveChangesAsync(ct); return new DispatchJob( Id: nextJob.Id, Token: claimToken, Vcs: nextJob.Source.UpstreamUrl, Ref: nextJob.Branch.Name, Commit: nextJob.Commit.Hash, Recipe: nextJob.Recipe ); } }