Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
ca144c8
Initial command builder placeholder
Babreakfast Jun 25, 2026
7fa98d1
Added a schema designer and integrated it into the custom command bui…
Babreakfast Jun 25, 2026
462570f
Added entity configs to the custom commands
Babreakfast Jun 25, 2026
5bc8934
Slight visual tweaks
Babreakfast Jun 25, 2026
4f6c526
Replace script placeholder with actual editor
Babreakfast Jun 25, 2026
d2bcd0f
Fix little script bug where comment right after function definition w…
Babreakfast Jun 25, 2026
7a5cc17
Script editor is now just responsible just for editing the script bod…
Babreakfast Jun 25, 2026
38305d4
Added another card for diagnostics feedback of the command.
Babreakfast Jun 25, 2026
85d1087
Css update for schema designer and command builder
Babreakfast Jun 26, 2026
baf9667
Prevented struct fields from moving around in a struct schema builder.
Babreakfast Jun 26, 2026
3626024
Added persistence and the migrations for the custom commands
Babreakfast Jun 26, 2026
a2fadd4
Fixed some indexing issues
Babreakfast Jun 26, 2026
c55f93b
Custom command execution device skeleton
Babreakfast Jul 2, 2026
4cea149
Added support for multiple command types. The types included so far a…
Babreakfast Jul 9, 2026
c0af44d
Gutted the ares core device and moved its functionality to a system o…
Babreakfast Jul 9, 2026
9a2b78f
Added an executor for the custom command.
Babreakfast Jul 10, 2026
81b597b
Add support for actually creating the different available command tem…
Babreakfast Jul 10, 2026
081a28b
Forgot to commit the system operations catalog
Babreakfast Jul 11, 2026
b135a7f
Added migrations for the custom command data stuff
Babreakfast Jul 11, 2026
2356017
Fixed custom commands not having proper output schema for later refer…
Babreakfast Jul 11, 2026
b1ad7b9
Fixed funny script translation issues when it comes to custom command…
Babreakfast Jul 11, 2026
199c6f7
Fixed false positive parameter assignment popping up.
Babreakfast Jul 11, 2026
c6d79e2
Fixed naming of Custom Commands within the experiment execution page.
Babreakfast Jul 11, 2026
7ac58be
Removed datamodel project reference from the solution file.
Babreakfast Jul 11, 2026
253d453
Fixed persistence issues.
Babreakfast Jul 11, 2026
ef4d5b5
Made campaign templates be stored and retrieved from the database.
Babreakfast Jul 11, 2026
daa781c
Campaign template import/export support.
Babreakfast Jul 11, 2026
dfd57bf
Styling tweaks
Babreakfast Jul 11, 2026
5fe5a07
Fixed campaign edit bug.
Babreakfast Jul 11, 2026
4f72f05
Added a BETA pill next to the custom commands nav item.
Babreakfast Jul 11, 2026
d3c2cfb
Fixed the analyzer validator not validating the commands if they were…
Babreakfast Jul 12, 2026
bdc8c13
Merge branch 'Develop' into CustomCommands
nkleiner Jul 20, 2026
97cbfcb
Updated datamodel version, fixed existing build errors
nkleiner Jul 20, 2026
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
4 changes: 3 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@ This solution-level file points to project-specific agent guidance.
with `with_escalated_permissions: true` on the `shell` tool call and include a one-sentence justification
(e.g., "Need network access for npm install/build").
Ensure to include the `with_escalated_permissions` for all builds, restores, migrations, installs, tests, etc
where network access is required otherwise the command will hang.
where network access is required otherwise the command will hang.
- When adding switch statements or if statements or whatever, always explicitly state all the handled enums.
Like if you have 3 enums, you should have case 1: case 2: case 3: and default, not case 1: case 2: and then default assuming that case 3 will just fall under default.
149 changes: 41 additions & 108 deletions Ares.Core.Grpc/Services/AutomationService.cs
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
using System.Threading;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reactive.Linq;
using System.Reactive.Threading.Tasks;
using System.Text.Json;
using System.Threading.Tasks;
using Ares.Core.Analyzing;
using Ares.Core.EntityConfigurations.Helpers;
using Ares.Core.Campaigns;
using Ares.Core.Execution;
using Ares.Core.Execution.StartConditions;
using Ares.Core.Execution.StopConditions;
Expand All @@ -21,10 +19,11 @@
using Microsoft.EntityFrameworkCore;
using Ares.Datamodel.Planning;
using Ares.Core.Execution.Extensions;
using Ares.Core.Planning;
using Ares.Datamodel.Analyzing;
using Ares.Core.Planning;
using Ares.Datamodel.Analyzing;
using Ares.Datamodel.Extensions;
using Ares.Core.Execution.StopConditions.PlannerLead;
using Ares.Core.Execution.StopConditions.PlannerLead;
using System.Text.Json;

namespace Ares.Core.Grpc.Services;

Expand All @@ -38,10 +37,11 @@ public class AutomationService : AresAutomation.AresAutomationBase
private readonly IEnumerable<INotificationHandler> _notificationHandlers;
readonly IDesiredAnalysisResultFactory _desiredAnalysisResultFactory;
private readonly IPlannerLeadStopConditionFactory _plannerLeadStopConditionFactory;
private JsonSerializerOptions _serializerSettings;
private readonly IPlannerServiceRepo _plannerServiceRepo;
private readonly IPlannerTransactionProvider _plannerTransactionProvider;
private readonly IAnalyzerTransactionProvider _analyzerTransactionProvider;
private readonly ICampaignTemplatePersistenceService _campaignTemplatePersistenceService;
private readonly ICampaignTemplateTransferService _campaignTemplateTransferService;

public AutomationService(IDbContextFactory<CoreDatabaseContext> coreContextFactory,
IExecutionManager executionManager,
Expand All @@ -53,7 +53,9 @@ public AutomationService(IDbContextFactory<CoreDatabaseContext> coreContextFacto
IPlannerLeadStopConditionFactory plannerLeadStopConditionFactory,
IPlannerServiceRepo plannerServiceRepo,
IPlannerTransactionProvider plannerTransactionProvider,
IAnalyzerTransactionProvider analyzerTransactionProvider)
IAnalyzerTransactionProvider analyzerTransactionProvider,
ICampaignTemplatePersistenceService campaignTemplatePersistenceService,
ICampaignTemplateTransferService campaignTemplateTransferService)
{
_desiredAnalysisResultFactory = desiredAnalysisResultFactory;
_plannerLeadStopConditionFactory = plannerLeadStopConditionFactory;
Expand All @@ -62,11 +64,12 @@ public AutomationService(IDbContextFactory<CoreDatabaseContext> coreContextFacto
_executionReportStore = executionReportStore;
_activeCampaignTemplateStore = activeCampaignTemplateStore;
_startConditions = startConditions;
_serializerSettings = SerializerSettingsHelper.CreateCustomSerializationSettings();
_notificationHandlers = notificationHandlers;
_plannerServiceRepo = plannerServiceRepo;
_plannerTransactionProvider = plannerTransactionProvider;
_analyzerTransactionProvider = analyzerTransactionProvider;
_campaignTemplatePersistenceService = campaignTemplatePersistenceService;
_campaignTemplateTransferService = campaignTemplateTransferService;
}

public override async Task<ProjectsResponse> GetAllProjects(Empty request, ServerCallContext? context)
Expand All @@ -81,83 +84,35 @@ public override async Task<ProjectsResponse> GetAllProjects(Empty request, Serve
public override async Task<GetAllCampaignsResponse> GetAllCampaigns(GetAllCampaignsRequest request, ServerCallContext? context)
{
var campaignResponse = new GetAllCampaignsResponse();
foreach(var file in Directory.EnumerateFiles(AresConfig.TemplatePath, "*.json"))
{
try
{
var contents = await File.ReadAllTextAsync(file);
var campaignTemplate = JsonSerializer.Deserialize<CampaignTemplate>(contents, _serializerSettings);
if(campaignTemplate is not null)
{
var summary = new CampaignTemplateSummary { CampaignName = campaignTemplate.Name, UniqueId = campaignTemplate.UniqueId };
campaignResponse.Campaigns.Add(summary);
}


else
throw new Exception("Deserialization of campaign template failed");
}

catch(Exception ex)
{
HandleNotification("Error Loading Campaign Template", $"{file} - {ex.Message}", NotificationSeverityEnum.Error);
}
}
var campaigns = await _campaignTemplatePersistenceService.GetSummariesAsync(context?.CancellationToken ?? CancellationToken.None);
campaignResponse.Campaigns.AddRange(campaigns);

return campaignResponse;
}

public override Task<BoolValue> CampaignExists(CampaignRequest request, ServerCallContext? context)
public override async Task<BoolValue> CampaignExists(CampaignRequest request, ServerCallContext? context)
{
var cancellationToken = context?.CancellationToken ?? CancellationToken.None;
if(request.HasUniqueId)
return Task.FromResult(FindCampaignById(request));
return new BoolValue { Value = await _campaignTemplatePersistenceService.ExistsByIdAsync(request.UniqueId, cancellationToken) };

else
return Task.FromResult(FindCampaignByName(request));
}

private BoolValue FindCampaignById(CampaignRequest request)
{
var directoryFiles = Directory.EnumerateFiles(AresConfig.TemplatePath, "*.json");

if(directoryFiles.Any(file => file.Contains(request.UniqueId)))
return new BoolValue { Value = true };

return new BoolValue { Value = false };
}

private BoolValue FindCampaignByName(CampaignRequest request)
{
var directoryFiles = Directory.EnumerateFiles(AresConfig.TemplatePath, "*.json");

foreach(var file in directoryFiles)
{
var jsonString = File.ReadAllText(Path.Combine(AresConfig.TemplatePath, file));
var templateObject = JsonSerializer.Deserialize<CampaignTemplate>(jsonString, _serializerSettings);
if(templateObject is not null && templateObject.Name == request.CampaignName)
return new BoolValue { Value = true };
}

return new BoolValue { Value = false };
return new BoolValue { Value = await _campaignTemplatePersistenceService.ExistsByNameAsync(request.CampaignName, cancellationToken) };
}

public override Task<CampaignTemplate?> GetSingleCampaign(CampaignRequest request, ServerCallContext? context)
=> GetCampaignTemplate(request, context);

public override Task<Empty> RemoveCampaign(CampaignRequest request, ServerCallContext? context)
public override async Task<Empty> RemoveCampaign(CampaignRequest request, ServerCallContext? context)
{
if(_activeCampaignTemplateStore.CampaignTemplate?.UniqueId == request.UniqueId)
{
HandleNotification("Cannot Delete Active Campaign", $"ARES rejected a request to delete the campaign {_activeCampaignTemplateStore.CampaignTemplate.Name} as it is currently running.", NotificationSeverityEnum.Info);
return Task.FromResult(new Empty());
HandleNotification("Cannot Delete Active Campaign", $"ARES rejected a request to delete the campaign {_activeCampaignTemplateStore.CampaignTemplate.Name} as it is currently set as the active campaign.", NotificationSeverityEnum.Info);
return new Empty();
}

var desiredCampaign = Directory.EnumerateFiles(AresConfig.TemplatePath, "*.json").FirstOrDefault(campaign => campaign.Contains(request.UniqueId));

if(desiredCampaign is not null)
File.Delete(Path.Combine(AresConfig.TemplatePath, desiredCampaign));

return Task.FromResult(new Empty());
await _campaignTemplatePersistenceService.DeleteAsync(request.UniqueId, context?.CancellationToken ?? CancellationToken.None);
return new Empty();
}

public override async Task<Project> GetProject(ProjectRequest request, ServerCallContext? context)
Expand Down Expand Up @@ -191,51 +146,33 @@ public override async Task<Empty> AddProject(Project request, ServerCallContext?
/// </param>
/// <param name="context"></param>
/// <returns></returns>
public override Task<Empty> AddCampaign(AddOrUpdateCampaignRequest request, ServerCallContext? context)
public override async Task<Empty> AddCampaign(AddOrUpdateCampaignRequest request, ServerCallContext? context)
{
//Save to data directory
var directoryFiles = Directory.EnumerateFiles(AresConfig.TemplatePath, "*.json");
var jsonString = JsonSerializer.Serialize(request.Template, _serializerSettings);
var fullFilePath = Path.Combine(AresConfig.TemplatePath, $"{request.Template.UniqueId}.json");
File.WriteAllText(fullFilePath, jsonString);
return Task.FromResult(new Empty());
await _campaignTemplatePersistenceService.AddAsync(request.Template, context?.CancellationToken ?? CancellationToken.None);
return new Empty();
}

public override Task<CampaignTemplate> UpdateCampaign(AddOrUpdateCampaignRequest request, ServerCallContext? context)
public override async Task<CampaignTemplate> UpdateCampaign(AddOrUpdateCampaignRequest request, ServerCallContext? context)
{
var directoryFiles = Directory.EnumerateFiles(AresConfig.TemplatePath, "*.json");
var campaignToUpdate = directoryFiles.FirstOrDefault(file => file.Contains(request.Template.UniqueId));

if(campaignToUpdate is null)
var updated = await _campaignTemplatePersistenceService.ReplaceAsync(request.Template, context?.CancellationToken ?? CancellationToken.None);
if(!updated)
{
var title = "Error Updating Campaign";
var message = $"Attempted to update a campaign that didn't exist. {request.Template.Name} couldn't be found in your list of available campaign templates.";
HandleNotification(title, message, NotificationSeverityEnum.Error);
return Task.FromResult(request.Template);
}

var jsonString = JsonSerializer.Serialize(request.Template, _serializerSettings);
var fullPath = Path.Combine(AresConfig.TemplatePath, $"{request.Template.UniqueId}.json");
File.WriteAllText(fullPath, jsonString);
return Task.FromResult(request.Template);
return request.Template;
}

private async Task<CampaignTemplate?> GetCampaignTemplate(CampaignRequest request, ServerCallContext? context)
{
var directoryFiles = Directory.EnumerateFiles(AresConfig.TemplatePath, "*.json");
var campaignFile = directoryFiles.FirstOrDefault(file => file.Contains(request.UniqueId));

if(campaignFile is not null)
{
var jsonString = await File.ReadAllTextAsync(Path.Combine(AresConfig.TemplatePath, campaignFile));
var campaignObject = JsonSerializer.Deserialize<CampaignTemplate>(jsonString, _serializerSettings);

if(campaignObject is not null)
return campaignObject;
}
var campaign = await _campaignTemplatePersistenceService.GetByIdAsync(request.UniqueId, context?.CancellationToken ?? CancellationToken.None);
if(campaign is not null)
return campaign;

var title = "Error Fetching Campaign Template";
var message = $"Attempted to fetch a campaign that didn't exist. {request.CampaignName}'s UUID did not match any of the existing campaigns in your data directory. If you deleted a campaign this is expected.";
var message = $"Attempted to fetch a campaign that didn't exist. {request.CampaignName}'s UUID did not match any campaign in the database. If you deleted a campaign this is expected.";
HandleNotification(title, message, NotificationSeverityEnum.Warning);
return null;
}
Expand Down Expand Up @@ -556,16 +493,12 @@ public override async Task<CampaignExecutionSummary> GetCampaignSummary(Campaign

public override async Task<GetCopyOfCampaignResponse> GetCopyOfCampaign(CampaignRequest request, ServerCallContext context)
{
var template = await GetCampaignTemplate(request, context);
var response = new GetCopyOfCampaignResponse();

if(template is not null)
var export = await _campaignTemplateTransferService.ExportAsync(request.UniqueId, context?.CancellationToken ?? CancellationToken.None);
if(export is not null)
{
var templateCopy = template.Clone();
templateCopy.UniqueId = Guid.NewGuid().ToString();
templateCopy.Name = $"{template.Name}-Copy";
response.Template = templateCopy;
response.SerializedJsonData = JsonSerializer.Serialize(templateCopy, _serializerSettings);
response.Template = export.Template;
response.SerializedJsonData = export.Json;
}

return response;
Expand All @@ -590,10 +523,10 @@ private void HandleNotification(string title, string message, NotificationSeveri
if(_activeCampaignTemplateStore is null || _activeCampaignTemplateStore.CampaignTemplate is null || _executionManager.ExecutionStartTime is null)
return [];

var usedPlanners = _activeCampaignTemplateStore.CampaignTemplate.ExperimentTemplate.GetAllPlannedParameters()
var usedPlanners = _activeCampaignTemplateStore.CampaignTemplate.ExperimentTemplate.GetAllPlannedParameters()
.Select(p => p.GetPlanningMetadata()?.PlannerName ?? "")
.Where(name => !string.IsNullOrWhiteSpace(name))
.Select(_plannerServiceRepo.GetPlannerByName)
.Where(name => !string.IsNullOrWhiteSpace(name))
.Select(_plannerServiceRepo.GetPlannerByName)
.Where(p => p is not null)
.Distinct()
.ToList();
Expand Down
4 changes: 2 additions & 2 deletions Ares.Core.Grpc/Services/DevicesService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -114,10 +114,10 @@ public override async Task<DeviceExecutionResult> ExecuteCommand(CommandTemplate
try
{
var arguments = new List<DeviceCommandArgument>();
arguments.AddRange(request.Parameters.Select(p => new DeviceCommandArgument() { ArgName = p.Metadata.Name, ArgValue = p.GetValue() }));
arguments.AddRange(request.ArgumentBindings.Select(p => new DeviceCommandArgument() { ArgName = p.Metadata.Name, ArgValue = p.GetValue() }));

Func<CancellationToken, Task<CommandResult>> internalAction = async (ct)
=> await device.ExecuteCommand(request.Metadata.Name, arguments, ct);
=> await device.ExecuteCommand(request.DeviceCommand.Metadata.Name, arguments, ct);

var result = await internalAction(token);

Expand Down
10 changes: 6 additions & 4 deletions Ares.Core.Tests/Ares.Core.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="10.0.8" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="10.0.8" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.8" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.6.0" />
<PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="NUnit" Version="4.6.1" />
Expand All @@ -28,9 +29,10 @@
</PackageReference>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Ares.Core\Ares.Core.csproj" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Ares.Core\Ares.Core.csproj" />
<ProjectReference Include="..\Ares.Core.Grpc\Ares.Core.Grpc.csproj" />
</ItemGroup>

<ItemGroup>
<Protobuf Include="Data\Device\TestReply.proto" GrpcServices="None">
Expand Down
Loading
Loading