feat: ship agent-first mission control v0.2.57
This commit is contained in:
@@ -0,0 +1,228 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.RateLimiting;
|
||||
using Nexus.Api.Models;
|
||||
using Nexus.Api.Services;
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace Nexus.Api.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Owner-only orchestration for the single OpenClaw Attach & Adopt profile.
|
||||
/// The browser never receives Gateway, bootstrap or provider credentials.
|
||||
/// </summary>
|
||||
[Authorize(Roles = "owner")]
|
||||
[ApiController]
|
||||
[Route("api/v1/openclaw/setup")]
|
||||
public sealed class OpenClawSetupController(
|
||||
IOpenClawSetupService setup,
|
||||
IOpenClawWizardService wizard) : ControllerBase
|
||||
{
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<OpenClawSetupStatusDto>> GetStatus(
|
||||
CancellationToken cancellationToken)
|
||||
=> Ok(await setup.GetStatusAsync(cancellationToken));
|
||||
|
||||
[HttpPost("discover")]
|
||||
[EnableRateLimiting("agents")]
|
||||
public async Task<ActionResult<OpenClawDiscoveryDto>> Discover(
|
||||
[FromBody] OpenClawDiscoveryRequest? request,
|
||||
CancellationToken cancellationToken)
|
||||
=> Ok(await setup.DiscoverAsync(
|
||||
request ?? new OpenClawDiscoveryRequest(),
|
||||
cancellationToken));
|
||||
|
||||
[HttpPost("probe")]
|
||||
[EnableRateLimiting("agents")]
|
||||
public async Task<ActionResult<OpenClawSetupOperationDto<OpenClawProbeDto>>> Probe(
|
||||
[FromBody] ProbeOpenClawRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
=> Map(await setup.ProbeAsync(request, cancellationToken));
|
||||
|
||||
[HttpPost("attach")]
|
||||
[EnableRateLimiting("agents")]
|
||||
public async Task<ActionResult<OpenClawSetupOperationDto<OpenClawSetupStatusDto>>> Attach(
|
||||
[FromBody] AttachOpenClawRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
=> Map(await setup.AttachAsync(request, cancellationToken));
|
||||
|
||||
[HttpPost("verify")]
|
||||
[EnableRateLimiting("agents")]
|
||||
public async Task<ActionResult<OpenClawSetupOperationDto<OpenClawSetupStatusDto>>> Verify(
|
||||
[FromBody] VerifyOpenClawRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
=> Map(await setup.VerifyAsync(request, cancellationToken));
|
||||
|
||||
[HttpPost("adopt")]
|
||||
[EnableRateLimiting("agents")]
|
||||
public async Task<ActionResult<OpenClawSetupOperationDto<OpenClawAdoptionInventoryDto>>> Adopt(
|
||||
[FromBody] AdoptOpenClawRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
=> Map(await setup.AdoptAsync(request, cancellationToken));
|
||||
|
||||
[HttpPost("management")]
|
||||
[EnableRateLimiting("agents")]
|
||||
public async Task<ActionResult<OpenClawSetupOperationDto<OpenClawSetupStatusDto>>> SetManagement(
|
||||
[FromBody] SetOpenClawManagementRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
=> Map(await setup.SetManagementAsync(request, cancellationToken));
|
||||
|
||||
[HttpDelete("connection")]
|
||||
[EnableRateLimiting("agents")]
|
||||
public async Task<ActionResult<OpenClawSetupOperationDto<OpenClawSetupStatusDto>>> DeleteConnection(
|
||||
[FromBody] DeleteOpenClawConnectionRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
=> Map(await setup.DeleteAsync(request, cancellationToken));
|
||||
|
||||
[HttpPost("wizard/start")]
|
||||
[EnableRateLimiting("agents")]
|
||||
public async Task<ActionResult<OpenClawWizardResultDto>> StartWizard(
|
||||
[FromBody] StartOpenClawWizardRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (!TryBuildInvocation(out var invocation, out var validationError))
|
||||
return validationError!;
|
||||
return MapWizard(await wizard.StartAsync(
|
||||
request,
|
||||
invocation!,
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPost("wizard/next")]
|
||||
[EnableRateLimiting("agents")]
|
||||
public async Task<ActionResult<OpenClawWizardResultDto>> AdvanceWizard(
|
||||
[FromBody] AdvanceOpenClawWizardRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (!TryBuildInvocation(out var invocation, out var validationError))
|
||||
return validationError!;
|
||||
return MapWizard(await wizard.NextAsync(
|
||||
request,
|
||||
invocation!,
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpGet("wizard/{sessionId}")]
|
||||
public async Task<ActionResult<OpenClawWizardResultDto>> GetWizardStatus(
|
||||
string sessionId,
|
||||
CancellationToken cancellationToken)
|
||||
=> MapWizard(await wizard.GetStatusAsync(sessionId, cancellationToken));
|
||||
|
||||
[HttpPost("wizard/{sessionId}/cancel")]
|
||||
[EnableRateLimiting("agents")]
|
||||
public async Task<ActionResult<OpenClawWizardResultDto>> CancelWizard(
|
||||
string sessionId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (!TryBuildInvocation(out var invocation, out var validationError))
|
||||
return validationError!;
|
||||
return MapWizard(await wizard.CancelAsync(
|
||||
sessionId,
|
||||
invocation!,
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
private ActionResult<OpenClawSetupOperationDto<T>> Map<T>(
|
||||
OpenClawSetupOperationDto<T> result)
|
||||
{
|
||||
if (result.Ok)
|
||||
return Ok(result);
|
||||
|
||||
return StatusCode(StatusFor(result.State), result);
|
||||
}
|
||||
|
||||
private ActionResult<OpenClawWizardResultDto> MapWizard(
|
||||
OpenClawWizardResultDto result)
|
||||
{
|
||||
if (result.Ok)
|
||||
return Ok(result);
|
||||
|
||||
var status = result.State switch
|
||||
{
|
||||
"invalid" or "confirmation_required"
|
||||
=> StatusCodes.Status400BadRequest,
|
||||
"not_found"
|
||||
=> StatusCodes.Status404NotFound,
|
||||
"management_disabled" or "scope_upgrade_required"
|
||||
=> StatusCodes.Status403Forbidden,
|
||||
"conflict" or "server_secret_required" or "unsupported"
|
||||
=> StatusCodes.Status409Conflict,
|
||||
"disconnected" or "gateway_error"
|
||||
=> StatusCodes.Status503ServiceUnavailable,
|
||||
_ => StatusCodes.Status502BadGateway
|
||||
};
|
||||
return StatusCode(status, result);
|
||||
}
|
||||
|
||||
private bool TryBuildInvocation(
|
||||
out OpenClawInvocationContext? invocation,
|
||||
out BadRequestObjectResult? validationError)
|
||||
{
|
||||
invocation = null;
|
||||
validationError = null;
|
||||
var idempotencyKey = Request.Headers["Idempotency-Key"].FirstOrDefault()?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(idempotencyKey) ||
|
||||
idempotencyKey.Length > 128 ||
|
||||
idempotencyKey.Any(char.IsControl))
|
||||
{
|
||||
validationError = BadRequest(new ValidationProblemDetails(
|
||||
new Dictionary<string, string[]>
|
||||
{
|
||||
["Idempotency-Key"] =
|
||||
[
|
||||
"A non-empty Idempotency-Key header with at most 128 non-control characters is required."
|
||||
]
|
||||
}));
|
||||
return false;
|
||||
}
|
||||
|
||||
var actor = User.FindFirst("sub")?.Value
|
||||
?? User.FindFirst(ClaimTypes.NameIdentifier)?.Value
|
||||
?? User.FindFirst(ClaimTypes.Email)?.Value
|
||||
?? User.Identity?.Name
|
||||
?? "authenticated-owner";
|
||||
try
|
||||
{
|
||||
invocation = OpenClawInvocationContext.Create(
|
||||
actor,
|
||||
idempotencyKey,
|
||||
Request.Headers["X-Correlation-ID"].FirstOrDefault(),
|
||||
Request.Headers["traceparent"].FirstOrDefault(),
|
||||
includeIdempotencyParameter: false);
|
||||
}
|
||||
catch (ArgumentException exception)
|
||||
{
|
||||
validationError = BadRequest(new ValidationProblemDetails(
|
||||
new Dictionary<string, string[]>
|
||||
{
|
||||
["request"] = [exception.Message]
|
||||
}));
|
||||
return false;
|
||||
}
|
||||
|
||||
Response.Headers["Idempotency-Key"] = invocation.IdempotencyKey;
|
||||
Response.Headers["X-Correlation-ID"] = invocation.CorrelationId;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static int StatusFor(string state)
|
||||
=> state switch
|
||||
{
|
||||
OpenClawSetupStates.InvalidEndpoint
|
||||
or OpenClawSetupStates.InvalidRequest
|
||||
=> StatusCodes.Status400BadRequest,
|
||||
OpenClawSetupStates.NotFound
|
||||
=> StatusCodes.Status404NotFound,
|
||||
OpenClawSetupStates.ConcurrencyConflict
|
||||
or OpenClawSetupStates.DynamicEndpointUnsupported
|
||||
or OpenClawSetupStates.ExperimentalBlocked
|
||||
or OpenClawSetupStates.ExcessiveScope
|
||||
or OpenClawSetupStates.PairingRequired
|
||||
or OpenClawSetupStates.ScopeUpgradeRequired
|
||||
=> StatusCodes.Status409Conflict,
|
||||
OpenClawSetupStates.Disconnected
|
||||
or OpenClawSetupStates.GatewayUnavailable
|
||||
=> StatusCodes.Status503ServiceUnavailable,
|
||||
_ => StatusCodes.Status502BadGateway
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user