feat: ship agent-first mission control v0.2.57
This commit is contained in:
@@ -14,7 +14,8 @@ namespace Nexus.Api.Controllers;
|
||||
/// This is the SINGLE entrypoint for agents (Iris + sub-agents) to interact with
|
||||
/// the Nexus task board, activity log, and delegation workflow.
|
||||
///
|
||||
/// AUTHENTICATION: Requires X-Nexus-Api-Key or a known allowed X-Agent-Id.
|
||||
/// AUTHENTICATION: Requires a verified JWT or X-Nexus-Api-Key. X-Agent-Id is
|
||||
/// accepted only as an actor hint for a service or privileged user principal.
|
||||
/// The browser NEVER uses this controller — only backend-to-backend and gateway-to-backend.
|
||||
///
|
||||
/// DESIGN PRINCIPLE: No MCP protocol between Nexus and Gateway — instead, the Gateway
|
||||
@@ -34,6 +35,7 @@ namespace Nexus.Api.Controllers;
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/bridge")]
|
||||
[Authorize]
|
||||
[EnableRateLimiting("agents")]
|
||||
public class GatewayBridgeController(
|
||||
ITaskBridgeService bridge,
|
||||
@@ -42,7 +44,7 @@ public class GatewayBridgeController(
|
||||
ILogger<GatewayBridgeController> logger) : ControllerBase
|
||||
{
|
||||
private const string ApikeyErrorMessage =
|
||||
"Bridge endpoints require X-Nexus-Api-Key or X-Agent-Id header with a recognized agent identity.";
|
||||
"Bridge endpoints require a verified JWT or X-Nexus-Api-Key.";
|
||||
|
||||
[HttpGet("health")]
|
||||
public IResult Health()
|
||||
@@ -236,18 +238,26 @@ public class GatewayBridgeController(
|
||||
var allowedAgentIds = await agentService.GetAllowedAgentIdsAsync(ct);
|
||||
var allowedActorIds = AgentIdentityCatalog.BuildAllowedActorIds(allowedAgentIds);
|
||||
|
||||
var agentHeader = Request.Headers["X-Agent-Id"].FirstOrDefault();
|
||||
if (!string.IsNullOrWhiteSpace(agentHeader))
|
||||
if (!RequestAuthorizationHelper.HasVerifiedAuthentication(HttpContext, configuration))
|
||||
{
|
||||
var normalizedHeader = agentHeader.Trim().ToLowerInvariant();
|
||||
if (allowedActorIds.Contains(normalizedHeader))
|
||||
return (true, normalizedHeader, null);
|
||||
|
||||
logger.LogWarning("Bridge: ignoring unknown X-Agent-Id '{AgentId}' from {Ip} and continuing auth fallback",
|
||||
normalizedHeader,
|
||||
HttpContext.Connection.RemoteIpAddress);
|
||||
var unauthorized = Unauthorized(new { error = ApikeyErrorMessage });
|
||||
logger.LogWarning("Bridge: unauthenticated request rejected from {Ip}", HttpContext.Connection.RemoteIpAddress);
|
||||
return (false, string.Empty, unauthorized);
|
||||
}
|
||||
|
||||
var agentHeader = await RequestAuthorizationHelper.ResolveAgentHeaderAsync(
|
||||
HttpContext,
|
||||
agentService,
|
||||
configuration,
|
||||
ct);
|
||||
if (agentHeader.AgentId is not null)
|
||||
return (true, agentHeader.AgentId, null);
|
||||
|
||||
if (agentHeader.HeaderProvided && !agentHeader.IsRecognized)
|
||||
logger.LogWarning(
|
||||
"Bridge: ignoring unknown X-Agent-Id from authenticated caller at {Ip} and continuing identity fallback",
|
||||
HttpContext.Connection.RemoteIpAddress);
|
||||
|
||||
if (User.Identity?.IsAuthenticated == true)
|
||||
{
|
||||
var normalizedClaim = User.FindFirst(ClaimTypes.NameIdentifier)?.Value?.Trim().ToLowerInvariant();
|
||||
@@ -264,9 +274,9 @@ public class GatewayBridgeController(
|
||||
allowedActorIds.Contains("nexus-system"))
|
||||
return (true, "nexus-system", null);
|
||||
|
||||
var unauthorized = Unauthorized(new { error = ApikeyErrorMessage });
|
||||
logger.LogWarning("Bridge: unauthenticated request rejected from {Ip}", HttpContext.Connection.RemoteIpAddress);
|
||||
return (false, string.Empty, unauthorized);
|
||||
var forbidden = StatusCode(StatusCodes.Status403Forbidden, new { error = "Authenticated caller has no permitted bridge identity." });
|
||||
logger.LogWarning("Bridge: authenticated caller has no permitted identity from {Ip}", HttpContext.Connection.RemoteIpAddress);
|
||||
return (false, string.Empty, forbidden);
|
||||
}
|
||||
|
||||
private static string ResolveSource(string agentId) => agentId switch
|
||||
@@ -275,14 +285,15 @@ public class GatewayBridgeController(
|
||||
_ => agentId
|
||||
};
|
||||
|
||||
private static ActionResult MapResult<T>(TaskBridgeResult<T> result, string command) where T : class
|
||||
private ActionResult MapResult<T>(TaskBridgeResult<T> result, string command) where T : class
|
||||
{
|
||||
if (result.Outcome == TaskBridgeOutcome.Success)
|
||||
return new OkObjectResult(new TaskBridgeCommandResponse<T>
|
||||
{
|
||||
Ok = true,
|
||||
Command = command,
|
||||
Data = result.Data
|
||||
Data = result.Data,
|
||||
Operation = BuildBridgeOperation(command, result.Data)
|
||||
});
|
||||
|
||||
var statusCode = result.Outcome switch
|
||||
@@ -302,7 +313,7 @@ public class GatewayBridgeController(
|
||||
}) { StatusCode = statusCode };
|
||||
}
|
||||
|
||||
private static ActionResult MapActivityResult(TaskBridgeResult<Data.ActivityEvent> result, string command)
|
||||
private ActionResult MapActivityResult(TaskBridgeResult<Data.ActivityEvent> result, string command)
|
||||
{
|
||||
if (result.Outcome == TaskBridgeOutcome.Success)
|
||||
return new OkObjectResult(new TaskBridgeCommandResponse<ActivityEntryDto>
|
||||
@@ -310,7 +321,19 @@ public class GatewayBridgeController(
|
||||
Ok = true,
|
||||
Command = command,
|
||||
Data = result.Data is null ? null : new ActivityEntryDto(
|
||||
result.Data.Id, result.Data.Type, result.Data.Message, result.Data.CreatedAt)
|
||||
result.Data.Id, result.Data.Type, result.Data.Message, result.Data.CreatedAt),
|
||||
Operation = result.Data is null
|
||||
? null
|
||||
: OperationResultFactory.FromHttpContext(
|
||||
HttpContext,
|
||||
"completed",
|
||||
new EntityRefDto(
|
||||
"activity",
|
||||
result.Data.Id.ToString(),
|
||||
result.Data.Type),
|
||||
affectedRefs: result.Data.TaskId is { } taskId
|
||||
? [new EntityRefDto("task", taskId.ToString())]
|
||||
: [])
|
||||
});
|
||||
|
||||
var statusCode = result.Outcome switch
|
||||
@@ -327,6 +350,29 @@ public class GatewayBridgeController(
|
||||
Error = result.Error ?? "Unknown error"
|
||||
}) { StatusCode = statusCode };
|
||||
}
|
||||
|
||||
private OperationResultDto? BuildBridgeOperation<T>(string command, T? data)
|
||||
where T : class
|
||||
{
|
||||
if (command.StartsWith("get_", StringComparison.Ordinal) || data is null)
|
||||
return null;
|
||||
|
||||
if (data is DashboardTaskDto task)
|
||||
{
|
||||
var affected = new List<EntityRefDto>();
|
||||
if (task.ProjectId is { } projectId)
|
||||
affected.Add(new EntityRefDto("project", projectId.ToString()));
|
||||
if (task.ParentTaskId is { } parentTaskId)
|
||||
affected.Add(new EntityRefDto("task", parentTaskId.ToString(), "Parent task"));
|
||||
return OperationResultFactory.FromHttpContext(
|
||||
HttpContext,
|
||||
"completed",
|
||||
new EntityRefDto("task", task.Id.ToString(), task.Title),
|
||||
affectedRefs: affected);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class TaskBridgeCommandResponse<T>
|
||||
@@ -335,6 +381,7 @@ public sealed class TaskBridgeCommandResponse<T>
|
||||
public string Command { get; init; } = string.Empty;
|
||||
public T? Data { get; init; }
|
||||
public string? Error { get; init; }
|
||||
public OperationResultDto? Operation { get; init; }
|
||||
public string Timestamp { get; init; } = DateTimeOffset.UtcNow.ToString("o");
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user