Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 39 additions & 91 deletions src/Orbit.Api/Controllers/AiController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
[Authorize]
[ApiController]
[Route("api/ai")]
public class AiController(

Check warning on line 20 in src/Orbit.Api/Controllers/AiController.cs

View workflow job for this annotation

GitHub Actions / SonarCloud Analysis

Constructor has 8 parameters, which is greater than the 7 authorized.
IAgentCatalogService catalogService,
IAgentPolicyEvaluator policyEvaluator,
IPendingAgentOperationStore pendingOperationStore,
Expand All @@ -28,127 +28,75 @@
IValidator<ResolveClarificationRequest> resolveClarificationValidator) : ControllerBase
{
[HttpGet("capabilities")]
public async Task<IActionResult> GetCapabilitiesMetadata(CancellationToken cancellationToken)
{
var userId = HttpContext.GetUserId();
var decision = policyEvaluator.Evaluate(new AgentPolicyEvaluationContext(
AgentCapabilityIds.CatalogCapabilitiesRead,
userId,
AgentExecutionSurface.Metadata,
HttpContext.User.GetAgentAuthMethod(),
HttpContext.User.GetGrantedAgentScopes(),
nameof(GetCapabilitiesMetadata),
"Read AI capability catalog"));

if (decision.Status != AgentPolicyDecisionStatus.Allowed)
return Forbid();

await auditService.RecordAsync(new AgentAuditEntry(
userId,
public Task<IActionResult> GetCapabilitiesMetadata(CancellationToken cancellationToken) =>
ReadCatalogAsync(
AgentCapabilityIds.CatalogCapabilitiesRead,
nameof(GetCapabilitiesMetadata),
AgentExecutionSurface.Metadata,
HttpContext.User.GetAgentAuthMethod(),
AgentRiskClass.Low,
AgentPolicyDecisionStatus.Allowed,
AgentOperationStatus.Succeeded,
HttpContext.TraceIdentifier,
"Read AI capability catalog"), cancellationToken);

return Ok(catalogService.GetCapabilities());
}
"Read AI capability catalog",
catalogService.GetCapabilities,
cancellationToken);

[HttpGet("operations")]
public async Task<IActionResult> GetOperationsMetadata(CancellationToken cancellationToken)
{
var userId = HttpContext.GetUserId();
var decision = policyEvaluator.Evaluate(new AgentPolicyEvaluationContext(
public Task<IActionResult> GetOperationsMetadata(CancellationToken cancellationToken) =>
ReadCatalogAsync(
AgentCapabilityIds.CatalogCapabilitiesRead,
userId,
AgentExecutionSurface.Metadata,
HttpContext.User.GetAgentAuthMethod(),
HttpContext.User.GetGrantedAgentScopes(),
nameof(GetOperationsMetadata),
"Read AI operation catalog"));

if (decision.Status != AgentPolicyDecisionStatus.Allowed)
return Forbid();

await auditService.RecordAsync(new AgentAuditEntry(
userId,
AgentCapabilityIds.CatalogCapabilitiesRead,
nameof(GetOperationsMetadata),
AgentExecutionSurface.Metadata,
HttpContext.User.GetAgentAuthMethod(),
AgentRiskClass.Low,
AgentPolicyDecisionStatus.Allowed,
AgentOperationStatus.Succeeded,
HttpContext.TraceIdentifier,
"Read AI operation catalog"), cancellationToken);

return Ok(catalogService.GetOperations());
}
"Read AI operation catalog",
catalogService.GetOperations,
cancellationToken);

[HttpGet("data-catalog")]
public async Task<IActionResult> GetUserDataCatalog(CancellationToken cancellationToken)
{
var userId = HttpContext.GetUserId();
var decision = policyEvaluator.Evaluate(new AgentPolicyEvaluationContext(
AgentCapabilityIds.CatalogDataRead,
userId,
AgentExecutionSurface.Metadata,
HttpContext.User.GetAgentAuthMethod(),
HttpContext.User.GetGrantedAgentScopes(),
nameof(GetUserDataCatalog),
"Read AI user-data catalog"));

if (decision.Status != AgentPolicyDecisionStatus.Allowed)
return Forbid();

await auditService.RecordAsync(new AgentAuditEntry(
userId,
public Task<IActionResult> GetUserDataCatalog(CancellationToken cancellationToken) =>
ReadCatalogAsync(
AgentCapabilityIds.CatalogDataRead,
nameof(GetUserDataCatalog),
AgentExecutionSurface.Metadata,
HttpContext.User.GetAgentAuthMethod(),
AgentRiskClass.Low,
AgentPolicyDecisionStatus.Allowed,
AgentOperationStatus.Succeeded,
HttpContext.TraceIdentifier,
"Read AI user-data catalog"), cancellationToken);

return Ok(catalogService.GetUserDataCatalog());
}
"Read AI user-data catalog",
catalogService.GetUserDataCatalog,
cancellationToken);

[HttpGet("surfaces")]
public async Task<IActionResult> GetAppSurfaces(CancellationToken cancellationToken)
public Task<IActionResult> GetAppSurfaces(CancellationToken cancellationToken) =>
ReadCatalogAsync(
AgentCapabilityIds.CatalogSurfacesRead,
nameof(GetAppSurfaces),
"Read AI app surface catalog",
catalogService.GetSurfaces,
cancellationToken);

private async Task<IActionResult> ReadCatalogAsync<TResult>(
string capabilityId,
string operationName,
string description,
Func<TResult> readCatalog,
CancellationToken cancellationToken)
{
var userId = HttpContext.GetUserId();
var authMethod = HttpContext.User.GetAgentAuthMethod();
var decision = policyEvaluator.Evaluate(new AgentPolicyEvaluationContext(
AgentCapabilityIds.CatalogSurfacesRead,
capabilityId,
userId,
AgentExecutionSurface.Metadata,
HttpContext.User.GetAgentAuthMethod(),
authMethod,
HttpContext.User.GetGrantedAgentScopes(),
nameof(GetAppSurfaces),
"Read AI app surface catalog"));
operationName,
description));

if (decision.Status != AgentPolicyDecisionStatus.Allowed)
return Forbid();

await auditService.RecordAsync(new AgentAuditEntry(
userId,
AgentCapabilityIds.CatalogSurfacesRead,
nameof(GetAppSurfaces),
capabilityId,
operationName,
AgentExecutionSurface.Metadata,
HttpContext.User.GetAgentAuthMethod(),
authMethod,
AgentRiskClass.Low,
AgentPolicyDecisionStatus.Allowed,
AgentOperationStatus.Succeeded,
HttpContext.TraceIdentifier,
"Read AI app surface catalog"), cancellationToken);
description), cancellationToken);

return Ok(catalogService.GetSurfaces());
return Ok(readCatalog());
}

public record ConfirmPendingOperationResponse(Guid PendingOperationId, string ConfirmationToken, DateTime ExpiresAtUtc);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,11 +86,7 @@ public static HashSet<DateOnly> GetUnionScheduledDates(
var union = new HashSet<DateOnly>();
foreach (var habit in habits)
{
if (habit.IsDeleted) continue;
if (habit.IsBadHabit) continue;
if (habit.IsGeneral) continue;
if (habit.IsFlexible) continue;
if (habit.FrequencyUnit is null && habit.IsCompleted) continue;
if (!IsStreakContributingHabit(habit)) continue;

foreach (var date in GetScheduledDates(habit, from, to))
union.Add(date);
Expand Down
13 changes: 13 additions & 0 deletions src/Orbit.Domain/Entities/ProcessedExternalEvent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using Orbit.Domain.Common;

namespace Orbit.Domain.Entities;

/// <summary>
/// Shared base for idempotency-ledger entities that record an external provider event
/// (Stripe webhook, Google Play RTDN) once it has been handled. Holds the processing
/// timestamp; each derived type owns its provider-specific unique key.
/// </summary>
public abstract class ProcessedExternalEvent : Entity
{
public DateTime ProcessedAtUtc { get; protected set; }
}
5 changes: 1 addition & 4 deletions src/Orbit.Domain/Entities/ProcessedPlayNotification.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
using Orbit.Domain.Common;

namespace Orbit.Domain.Entities;

public class ProcessedPlayNotification : Entity
public class ProcessedPlayNotification : ProcessedExternalEvent
{
public string MessageId { get; private set; } = "";
public DateTime ProcessedAtUtc { get; private set; }

private ProcessedPlayNotification() { }

Expand Down
5 changes: 1 addition & 4 deletions src/Orbit.Domain/Entities/ProcessedStripeEvent.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
using Orbit.Domain.Common;

namespace Orbit.Domain.Entities;

public class ProcessedStripeEvent : Entity
public class ProcessedStripeEvent : ProcessedExternalEvent
{
public string EventId { get; private set; } = "";
public DateTime ProcessedAtUtc { get; private set; }

private ProcessedStripeEvent() { }

Expand Down
Loading
Loading