diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 19be1a753..662cc31c5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -24,6 +24,18 @@ dotnet restore Nexo.LocalDevCore.slnf dotnet build Nexo.LocalDevCore.slnf -v minimal ``` +## Solution filters, Makefile targets, and CI + +| Artifact | Typical use | +| -------- | ----------- | +| **`Nexo.sln`** | Full repository build — run locally after **`Nexo.Hosting`**, **`Nexo.Infrastructure`** Sdk surface, or registrar phase edits. | +| **`Nexo.LocalDevCore.slnf`** | Faster slice (CLI + core tests); **`make restore-core`** / **`make build-core`**. | +| **`Nexo.PrimeTime.slnf`** | Nine **`Nexo.Tests.*`** assemblies — **`make test-prime-time`** runs **`Category=ProdStyle`** across this filter; **`make test-prime-time-full`** runs the full test matrix after that gate. | + +**Cross-platform workflow:** `.github/workflows/cross-platform-tests.yml` triggers on changes under **`src/Nexo.Infrastructure/**`** (among other paths) and runs **`dotnet restore`** / **`dotnet build`** on **`Nexo.sln`** (implicit via repo root). **Prod-shaped Compose:** `.github/workflows/prod-dry-run-pr.yml` runs **`scripts/prod-dry-run.sh`** on PRs to **`master`**, **`main`**, and **`cursor/**`** branches. + +For Infrastructure Sdk / Hosting registration changes, prefer **`dotnet build Nexo.sln`** then **`make test-framework-prod-first`** or **`make test-prime-time`** when validating framework-wide behaviour (see also **`Makefile`** targets **`test-prod-style`**, **`ci-verify`**). + ## Testing: xUnit vs. `UnitTestBase` - **xUnit** suites (for example `Nexo.Tests.Infrastructure`) run with normal `dotnet test` filters. diff --git a/Nexo.sln b/Nexo.sln index 4f3f23a4f..959d530b8 100644 --- a/Nexo.sln +++ b/Nexo.sln @@ -65,6 +65,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nexo.BackgroundAgents.HostR EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nexo.Contracts", "src\Nexo.Contracts\Nexo.Contracts.csproj", "{F2A54F4C-34A8-49AC-A620-1B75EBA4424A}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nexo.Framework.Sdk", "src\Nexo.Framework.Sdk\Nexo.Framework.Sdk.csproj", "{5202644E-F8CC-47EA-92BA-22D18AC55C50}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nexo.Runtime.Bundle", "src\Nexo.Runtime.Bundle\Nexo.Runtime.Bundle.csproj", "{DAF7CF6D-2BCB-411F-860D-2CF3EF396E70}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nexo.Ingress.AwsSns", "src\Nexo.Ingress.AwsSns\Nexo.Ingress.AwsSns.csproj", "{7C104A42-14C9-4A59-9494-014D238DF354}" @@ -445,6 +447,18 @@ Global {F2A54F4C-34A8-49AC-A620-1B75EBA4424A}.Release|x64.Build.0 = Release|Any CPU {F2A54F4C-34A8-49AC-A620-1B75EBA4424A}.Release|x86.ActiveCfg = Release|Any CPU {F2A54F4C-34A8-49AC-A620-1B75EBA4424A}.Release|x86.Build.0 = Release|Any CPU + {5202644E-F8CC-47EA-92BA-22D18AC55C50}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5202644E-F8CC-47EA-92BA-22D18AC55C50}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5202644E-F8CC-47EA-92BA-22D18AC55C50}.Debug|x64.ActiveCfg = Debug|Any CPU + {5202644E-F8CC-47EA-92BA-22D18AC55C50}.Debug|x64.Build.0 = Debug|Any CPU + {5202644E-F8CC-47EA-92BA-22D18AC55C50}.Debug|x86.ActiveCfg = Debug|Any CPU + {5202644E-F8CC-47EA-92BA-22D18AC55C50}.Debug|x86.Build.0 = Debug|Any CPU + {5202644E-F8CC-47EA-92BA-22D18AC55C50}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5202644E-F8CC-47EA-92BA-22D18AC55C50}.Release|Any CPU.Build.0 = Release|Any CPU + {5202644E-F8CC-47EA-92BA-22D18AC55C50}.Release|x64.ActiveCfg = Release|Any CPU + {5202644E-F8CC-47EA-92BA-22D18AC55C50}.Release|x64.Build.0 = Release|Any CPU + {5202644E-F8CC-47EA-92BA-22D18AC55C50}.Release|x86.ActiveCfg = Release|Any CPU + {5202644E-F8CC-47EA-92BA-22D18AC55C50}.Release|x86.Build.0 = Release|Any CPU {DAF7CF6D-2BCB-411F-860D-2CF3EF396E70}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {DAF7CF6D-2BCB-411F-860D-2CF3EF396E70}.Debug|Any CPU.Build.0 = Debug|Any CPU {DAF7CF6D-2BCB-411F-860D-2CF3EF396E70}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -539,6 +553,7 @@ Global {9A6E02CA-56C8-4F7C-A1AE-6AB21D8F8AFB} = {9D4F8B1A-0B6E-4A3E-8A6A-0DE12C7C6E2F} {C4A004BB-AFCC-4A69-8D36-61C2E9EEFD01} = {9D4F8B1A-0B6E-4A3E-8A6A-0DE12C7C6E2F} {F2A54F4C-34A8-49AC-A620-1B75EBA4424A} = {9D4F8B1A-0B6E-4A3E-8A6A-0DE12C7C6E2F} + {5202644E-F8CC-47EA-92BA-22D18AC55C50} = {9D4F8B1A-0B6E-4A3E-8A6A-0DE12C7C6E2F} {DAF7CF6D-2BCB-411F-860D-2CF3EF396E70} = {9D4F8B1A-0B6E-4A3E-8A6A-0DE12C7C6E2F} {7C104A42-14C9-4A59-9494-014D238DF354} = {9D4F8B1A-0B6E-4A3E-8A6A-0DE12C7C6E2F} {6FE29659-FF7F-4373-9B4F-637CE3D7386B} = {9D4F8B1A-0B6E-4A3E-8A6A-0DE12C7C6E2F} diff --git a/application/src/Nexo.CLI/Nexo.CLI.csproj b/application/src/Nexo.CLI/Nexo.CLI.csproj index e4f27efdd..593b5fde6 100644 --- a/application/src/Nexo.CLI/Nexo.CLI.csproj +++ b/application/src/Nexo.CLI/Nexo.CLI.csproj @@ -60,6 +60,10 @@ + + + + diff --git a/application/src/Nexo.Tests.CLI/Tests/Commands/ForgeRelatedCommandTests.cs b/application/src/Nexo.Tests.CLI/Tests/Commands/ForgeRelatedCommandTests.cs index 776814cfc..797daefa5 100644 --- a/application/src/Nexo.Tests.CLI/Tests/Commands/ForgeRelatedCommandTests.cs +++ b/application/src/Nexo.Tests.CLI/Tests/Commands/ForgeRelatedCommandTests.cs @@ -13,24 +13,27 @@ namespace Nexo.Tests.CLI.Tests.Commands; public sealed class ForgeRelatedCommandTests { [Fact(Timeout = 15000)] - public void RuntimeStudioCommand_HasApplyTuneSubcommand() + public async Task RuntimeStudioCommand_HasApplyTuneSubcommand() { + await Task.CompletedTask; var cmd = new RuntimeStudioCommand(); var subcommands = cmd.Subcommands.Select(s => s.Name).ToList(); subcommands.Should().Contain("apply-tune"); } [Fact(Timeout = 15000)] - public void RuntimeStudioCommand_HasStatusSubcommand() + public async Task RuntimeStudioCommand_HasStatusSubcommand() { + await Task.CompletedTask; var cmd = new RuntimeStudioCommand(); var subcommands = cmd.Subcommands.Select(s => s.Name).ToList(); subcommands.Should().Contain("status"); } [Fact(Timeout = 15000)] - public void RuntimeStudioCommand_ApplyTune_HasExpectedOptions() + public async Task RuntimeStudioCommand_ApplyTune_HasExpectedOptions() { + await Task.CompletedTask; var cmd = new RuntimeStudioCommand(); var applyTune = cmd.Subcommands.Single(s => s.Name == "apply-tune"); var optionNames = applyTune.Options.Select(o => o.Name).ToList(); @@ -42,8 +45,9 @@ public void RuntimeStudioCommand_ApplyTune_HasExpectedOptions() } [Fact(Timeout = 15000)] - public void RuntimeStudioCommand_Status_HasExpectedOptions() + public async Task RuntimeStudioCommand_Status_HasExpectedOptions() { + await Task.CompletedTask; var cmd = new RuntimeStudioCommand(); var status = cmd.Subcommands.Single(s => s.Name == "status"); var optionNames = status.Options.Select(o => o.Name).ToList(); @@ -53,8 +57,9 @@ public void RuntimeStudioCommand_Status_HasExpectedOptions() } [Fact(Timeout = 15000)] - public void RuntimeStudioCommand_ParsesApplyTuneWithDryRun() + public async Task RuntimeStudioCommand_ParsesApplyTuneWithDryRun() { + await Task.CompletedTask; var cmd = new RuntimeStudioCommand(); var parseResult = cmd.Parse("apply-tune --dry-run --format-json"); @@ -62,8 +67,9 @@ public void RuntimeStudioCommand_ParsesApplyTuneWithDryRun() } [Fact(Timeout = 15000)] - public void RuntimeStudioCommand_ParsesStatusWithJson() + public async Task RuntimeStudioCommand_ParsesStatusWithJson() { + await Task.CompletedTask; var cmd = new RuntimeStudioCommand(); var parseResult = cmd.Parse("status --format-json"); @@ -71,8 +77,9 @@ public void RuntimeStudioCommand_ParsesStatusWithJson() } [Fact(Timeout = 15000)] - public void TuneApplier_Apply_RejectsFailedOptimizePayload() + public async Task TuneApplier_Apply_RejectsFailedOptimizePayload() { + await Task.CompletedTask; var tempDir = CreateTempDirectory(); try { @@ -96,8 +103,9 @@ public void TuneApplier_Apply_RejectsFailedOptimizePayload() } [Fact(Timeout = 15000)] - public void TuneApplier_Apply_RejectsMissingModelProfileId() + public async Task TuneApplier_Apply_RejectsMissingModelProfileId() { + await Task.CompletedTask; var tempDir = CreateTempDirectory(); try { @@ -123,8 +131,9 @@ public void TuneApplier_Apply_RejectsMissingModelProfileId() } [Fact(Timeout = 15000)] - public void TuneApplier_ResolveOllamaModelForLabAgent_ReturnsMixedProfileModel() + public async Task TuneApplier_ResolveOllamaModelForLabAgent_ReturnsMixedProfileModel() { + await Task.CompletedTask; var spec = WorkflowLabRuntimeSpec.Default(); var composition = spec.Compositions.First(c => c.Id == "hierarchy-squad"); var profile = spec.ModelProfiles.First(p => p.Id == "ollama-mixed"); @@ -139,8 +148,9 @@ public void TuneApplier_ResolveOllamaModelForLabAgent_ReturnsMixedProfileModel() } [Fact(Timeout = 15000)] - public void TuneApplier_ResolveOllamaModelForLabAgent_FallsBackToDefault() + public async Task TuneApplier_ResolveOllamaModelForLabAgent_FallsBackToDefault() { + await Task.CompletedTask; var spec = WorkflowLabRuntimeSpec.Default(); var composition = spec.Compositions.First(c => c.Id == "hierarchy-squad"); var profile = spec.ModelProfiles.First(p => p.Id == "ollama-balanced"); @@ -151,15 +161,17 @@ public void TuneApplier_ResolveOllamaModelForLabAgent_FallsBackToDefault() } [Fact(Timeout = 15000)] - public void AgentSetReader_ReturnsEmpty_ForMissingFile() + public async Task AgentSetReader_ReturnsEmpty_ForMissingFile() { + await Task.CompletedTask; var rows = RuntimeStudioAgentSetReader.TryListOllamaAgents("/nonexistent/path/agents.json"); rows.Should().BeEmpty(); } [Fact(Timeout = 15000)] - public void AgentSetReader_ParsesOllamaAgents_SortedById() + public async Task AgentSetReader_ParsesOllamaAgents_SortedById() { + await Task.CompletedTask; var tempDir = CreateTempDirectory(); try { @@ -191,8 +203,9 @@ public void AgentSetReader_ParsesOllamaAgents_SortedById() } [Fact(Timeout = 90000)] - public void TuneApplier_Apply_DryRun_ReportsPlannedChanges() + public async Task TuneApplier_Apply_DryRun_ReportsPlannedChanges() { + await Task.CompletedTask; var tempDir = CreateTempDirectory(); try { @@ -243,8 +256,9 @@ public void TuneApplier_Apply_DryRun_ReportsPlannedChanges() } [Fact(Timeout = 15000)] - public void WorkflowLabRuntimeSpec_Default_HasExpectedCompositions() + public async Task WorkflowLabRuntimeSpec_Default_HasExpectedCompositions() { + await Task.CompletedTask; var spec = WorkflowLabRuntimeSpec.Default(); spec.Compositions.Should().HaveCount(2); diff --git a/application/src/Nexo.Tests.CLI/Tests/Commands/UnityDevCommandTests.cs b/application/src/Nexo.Tests.CLI/Tests/Commands/UnityDevCommandTests.cs index 2f304c100..304bb0c05 100644 --- a/application/src/Nexo.Tests.CLI/Tests/Commands/UnityDevCommandTests.cs +++ b/application/src/Nexo.Tests.CLI/Tests/Commands/UnityDevCommandTests.cs @@ -10,15 +10,17 @@ namespace Nexo.Tests.CLI.Tests.Commands; public sealed class UnityDevCommandTests { [Fact(Timeout = 15000)] - public void ParseFiles_EmptyInput_ReturnsEmpty() + public async Task ParseFiles_EmptyInput_ReturnsEmpty() { + await Task.CompletedTask; UnityDevCommand.ParseFiles("").Should().BeEmpty(); UnityDevCommand.ParseFiles(" ").Should().BeEmpty(); } [Fact(Timeout = 15000)] - public void ParseFiles_SingleFile_ParsesCorrectly() + public async Task ParseFiles_SingleFile_ParsesCorrectly() { + await Task.CompletedTask; var input = @"// FILE: Assets/Scripts/Player.cs using UnityEngine; @@ -35,8 +37,9 @@ public class Player : MonoBehaviour } [Fact(Timeout = 15000)] - public void ParseFiles_MultipleFiles_SplitsCorrectly() + public async Task ParseFiles_MultipleFiles_SplitsCorrectly() { + await Task.CompletedTask; var input = @"// FILE: Assets/Scripts/Weapons/IWeapon.cs public interface IWeapon { @@ -67,8 +70,9 @@ public void Fire_DoesNotThrow() { } } [Fact(Timeout = 15000)] - public void ParseFiles_IgnoresContentBeforeFirstMarker() + public async Task ParseFiles_IgnoresContentBeforeFirstMarker() { + await Task.CompletedTask; var input = @"Here is some preamble text from the LLM. This should be ignored. @@ -83,8 +87,9 @@ public class Foo { }"; } [Fact(Timeout = 15000)] - public void ParseFiles_HandlesBackslashPaths() + public async Task ParseFiles_HandlesBackslashPaths() { + await Task.CompletedTask; var input = @"// FILE: Assets\Scripts\Bar.cs public class Bar { }"; var result = UnityDevCommand.ParseFiles(input); @@ -94,8 +99,9 @@ public class Bar { }"; } [Fact(Timeout = 15000)] - public void ValidateProjectRoot_MissingDirectory_ReturnsFalse() + public async Task ValidateProjectRoot_MissingDirectory_ReturnsFalse() { + await Task.CompletedTask; var originalErr = Console.Error; using var errWriter = new StringWriter(); Console.SetError(errWriter); @@ -112,8 +118,9 @@ public void ValidateProjectRoot_MissingDirectory_ReturnsFalse() } [Fact(Timeout = 15000)] - public void ValidateProjectRoot_MissingAssetsFolder_ReturnsFalse() + public async Task ValidateProjectRoot_MissingAssetsFolder_ReturnsFalse() { + await Task.CompletedTask; var tempDir = Path.Combine(Path.GetTempPath(), $"nexo-unity-test-{Guid.NewGuid():N}"); Directory.CreateDirectory(tempDir); try @@ -139,8 +146,9 @@ public void ValidateProjectRoot_MissingAssetsFolder_ReturnsFalse() } [Fact(Timeout = 15000)] - public void ValidateProjectRoot_ValidProject_ReturnsTrue() + public async Task ValidateProjectRoot_ValidProject_ReturnsTrue() { + await Task.CompletedTask; var tempDir = Path.Combine(Path.GetTempPath(), $"nexo-unity-test-{Guid.NewGuid():N}"); Directory.CreateDirectory(Path.Combine(tempDir, "Assets")); try @@ -154,8 +162,9 @@ public void ValidateProjectRoot_ValidProject_ReturnsTrue() } [Fact(Timeout = 15000)] - public void ValidateProjectRoot_Json_MissingDirectory_EmitsJsonError() + public async Task ValidateProjectRoot_Json_MissingDirectory_EmitsJsonError() { + await Task.CompletedTask; var originalOut = Console.Out; using var outWriter = new StringWriter(); Console.SetOut(outWriter); @@ -176,8 +185,9 @@ public void ValidateProjectRoot_Json_MissingDirectory_EmitsJsonError() } [Fact(Timeout = 15000)] - public void WriteManifest_CreatesValidJsonManifest() + public async Task WriteManifest_CreatesValidJsonManifest() { + await Task.CompletedTask; var tempDir = Path.Combine(Path.GetTempPath(), $"nexo-unity-manifest-{Guid.NewGuid():N}"); Directory.CreateDirectory(tempDir); try @@ -208,8 +218,9 @@ public void WriteManifest_CreatesValidJsonManifest() } [Fact(Timeout = 15000)] - public void ExecuteList_EmptyProject_ReturnsZeroSystems() + public async Task ExecuteList_EmptyProject_ReturnsZeroSystems() { + await Task.CompletedTask; var tempDir = Path.Combine(Path.GetTempPath(), $"nexo-unity-list-{Guid.NewGuid():N}"); Directory.CreateDirectory(Path.Combine(tempDir, "Assets")); try @@ -239,8 +250,9 @@ public void ExecuteList_EmptyProject_ReturnsZeroSystems() } [Fact(Timeout = 15000)] - public void ExecuteList_WithGeneratedSystems_ListsThem() + public async Task ExecuteList_WithGeneratedSystems_ListsThem() { + await Task.CompletedTask; var tempDir = Path.Combine(Path.GetTempPath(), $"nexo-unity-list-{Guid.NewGuid():N}"); var genDir = Path.Combine(tempDir, "Assets", "Scripts", "Generated", "Weapons"); Directory.CreateDirectory(genDir); @@ -275,8 +287,9 @@ public void ExecuteList_WithGeneratedSystems_ListsThem() } [Fact(Timeout = 15000)] - public void UnityDevCommand_HasExpectedSubcommands() + public async Task UnityDevCommand_HasExpectedSubcommands() { + await Task.CompletedTask; var cmd = new UnityDevCommand(() => null!); var subNames = cmd.Subcommands.Select(s => s.Name).ToList(); subNames.Should().Contain("generate"); @@ -288,8 +301,9 @@ public void UnityDevCommand_HasExpectedSubcommands() } [Fact(Timeout = 15000)] - public void Generate_Subcommand_HasExpectedOptions() + public async Task Generate_Subcommand_HasExpectedOptions() { + await Task.CompletedTask; var cmd = new UnityDevCommand(() => null!); var gen = cmd.Subcommands.Single(s => s.Name == "generate"); var optNames = gen.Options.Select(o => o.Name).ToList(); @@ -301,8 +315,9 @@ public void Generate_Subcommand_HasExpectedOptions() } [Fact(Timeout = 15000)] - public void Iterate_Subcommand_HasExpectedOptions() + public async Task Iterate_Subcommand_HasExpectedOptions() { + await Task.CompletedTask; var cmd = new UnityDevCommand(() => null!); var iter = cmd.Subcommands.Single(s => s.Name == "iterate"); var optNames = iter.Options.Select(o => o.Name).ToList(); @@ -312,15 +327,17 @@ public void Iterate_Subcommand_HasExpectedOptions() } [Fact(Timeout = 15000)] - public void Init_Subcommand_Exists() + public async Task Init_Subcommand_Exists() { + await Task.CompletedTask; var cmd = new UnityDevCommand(() => null!); cmd.Subcommands.Should().Contain(s => s.Name == "init"); } [Fact(Timeout = 15000)] - public void ExecuteInit_CreatesProjectStructure() + public async Task ExecuteInit_CreatesProjectStructure() { + await Task.CompletedTask; var tempDir = Path.Combine(Path.GetTempPath(), "nexo-unity-init-" + Guid.NewGuid().ToString("N")[..8]); try { @@ -343,8 +360,9 @@ public void ExecuteInit_CreatesProjectStructure() } [Fact(Timeout = 15000)] - public void ExecuteInit_CreatesManifestWithDependencies() + public async Task ExecuteInit_CreatesManifestWithDependencies() { + await Task.CompletedTask; var tempDir = Path.Combine(Path.GetTempPath(), "nexo-unity-init-" + Guid.NewGuid().ToString("N")[..8]); try { @@ -364,8 +382,9 @@ public void ExecuteInit_CreatesManifestWithDependencies() } [Fact(Timeout = 15000)] - public void ExecuteInit_CreatesAssemblyDefinitions() + public async Task ExecuteInit_CreatesAssemblyDefinitions() { + await Task.CompletedTask; var tempDir = Path.Combine(Path.GetTempPath(), "nexo-unity-init-" + Guid.NewGuid().ToString("N")[..8]); try { @@ -389,8 +408,9 @@ public void ExecuteInit_CreatesAssemblyDefinitions() } [Fact(Timeout = 15000)] - public void ExecuteInit_CreatesGitignore() + public async Task ExecuteInit_CreatesGitignore() { + await Task.CompletedTask; var tempDir = Path.Combine(Path.GetTempPath(), "nexo-unity-init-" + Guid.NewGuid().ToString("N")[..8]); try { @@ -409,8 +429,9 @@ public void ExecuteInit_CreatesGitignore() } [Fact(Timeout = 15000)] - public void ExecuteInit_CreatesNexoConfig() + public async Task ExecuteInit_CreatesNexoConfig() { + await Task.CompletedTask; var tempDir = Path.Combine(Path.GetTempPath(), "nexo-unity-init-" + Guid.NewGuid().ToString("N")[..8]); try { @@ -428,8 +449,9 @@ public void ExecuteInit_CreatesNexoConfig() } [Fact(Timeout = 15000)] - public void ExecuteInit_IdempotentOnExistingProject() + public async Task ExecuteInit_IdempotentOnExistingProject() { + await Task.CompletedTask; var tempDir = Path.Combine(Path.GetTempPath(), "nexo-unity-init-" + Guid.NewGuid().ToString("N")[..8]); try { @@ -448,8 +470,9 @@ public void ExecuteInit_IdempotentOnExistingProject() } [Fact(Timeout = 15000)] - public void ExecuteInit_JsonOutput_ReturnsStructuredResult() + public async Task ExecuteInit_JsonOutput_ReturnsStructuredResult() { + await Task.CompletedTask; var tempDir = Path.Combine(Path.GetTempPath(), "nexo-unity-init-" + Guid.NewGuid().ToString("N")[..8]); try { @@ -469,8 +492,9 @@ public void ExecuteInit_JsonOutput_ReturnsStructuredResult() } [Fact(Timeout = 15000)] - public void Assets_Subcommand_HasExpectedOptions() + public async Task Assets_Subcommand_HasExpectedOptions() { + await Task.CompletedTask; var cmd = new UnityDevCommand(() => null!); var assets = cmd.Subcommands.Single(s => s.Name == "assets"); var optNames = assets.Options.Select(o => o.Name).ToList(); @@ -481,8 +505,9 @@ public void Assets_Subcommand_HasExpectedOptions() } [Fact(Timeout = 15000)] - public void Qa_Subcommand_HasExpectedOptions() + public async Task Qa_Subcommand_HasExpectedOptions() { + await Task.CompletedTask; var cmd = new UnityDevCommand(() => null!); var qa = cmd.Subcommands.Single(s => s.Name == "qa"); var optNames = qa.Options.Select(o => o.Name).ToList(); @@ -493,8 +518,9 @@ public void Qa_Subcommand_HasExpectedOptions() } [Fact(Timeout = 15000)] - public void Fullstack_Subcommand_Exists() + public async Task Fullstack_Subcommand_Exists() { + await Task.CompletedTask; var cmd = new UnityDevCommand(() => null!); var fullstack = cmd.Subcommands.SingleOrDefault(s => s.Name == "fullstack"); fullstack.Should().NotBeNull(); @@ -506,8 +532,9 @@ public void Fullstack_Subcommand_Exists() } [Fact(Timeout = 15000)] - public void FindUnityEditor_ReturnsNullWhenNotInstalled() + public async Task FindUnityEditor_ReturnsNullWhenNotInstalled() { + await Task.CompletedTask; var result = UnityDevCommand.FindUnityEditor(); // On CI/test machines without Unity, this should return null (or a valid path if installed) // We just verify it doesn't throw @@ -515,8 +542,9 @@ public void FindUnityEditor_ReturnsNullWhenNotInstalled() } [Fact(Timeout = 15000)] - public void BuildAssetPrompt_ContainsAssetTypeAndDescription() + public async Task BuildAssetPrompt_ContainsAssetTypeAndDescription() { + await Task.CompletedTask; var prompt = UnityDevCommand.BuildAssetPrompt("material", "shiny gold material"); prompt.Should().Contain("material"); prompt.Should().Contain("shiny gold material"); @@ -525,8 +553,9 @@ public void BuildAssetPrompt_ContainsAssetTypeAndDescription() } [Fact(Timeout = 15000)] - public void BuildAssetPrompt_AllTypesProduceSchemaHints() + public async Task BuildAssetPrompt_AllTypesProduceSchemaHints() { + await Task.CompletedTask; var types = new[] { "material", "prefab", "scene", "audio", "animation", "soundbank", "animationset", "ui" }; foreach (var type in types) { @@ -536,14 +565,16 @@ public void BuildAssetPrompt_AllTypesProduceSchemaHints() } [Fact(Timeout = 15000)] - public void Truncate_ShortText_ReturnsUnchanged() + public async Task Truncate_ShortText_ReturnsUnchanged() { + await Task.CompletedTask; UnityDevCommand.Truncate("hello", 100).Should().Be("hello"); } [Fact(Timeout = 15000)] - public void Truncate_LongText_IsTruncated() + public async Task Truncate_LongText_IsTruncated() { + await Task.CompletedTask; var long_text = new string('x', 200); var result = UnityDevCommand.Truncate(long_text, 50); result.Should().HaveLength(50 + "\n... (truncated)".Length); @@ -551,15 +582,17 @@ public void Truncate_LongText_IsTruncated() } [Fact(Timeout = 15000)] - public void Truncate_NullOrEmpty_ReturnsInput() + public async Task Truncate_NullOrEmpty_ReturnsInput() { + await Task.CompletedTask; UnityDevCommand.Truncate("", 10).Should().Be(""); UnityDevCommand.Truncate(null!, 10).Should().BeNull(); } [Fact(Timeout = 15000)] - public void BuildAssetPrompt_Animation_ContainsSchemaHints() + public async Task BuildAssetPrompt_Animation_ContainsSchemaHints() { + await Task.CompletedTask; var prompt = UnityDevCommand.BuildAssetPrompt("animation", "FPS character animator"); prompt.Should().Contain("AnimationDescriptor"); prompt.Should().Contain("state machine"); @@ -570,8 +603,9 @@ public void BuildAssetPrompt_Animation_ContainsSchemaHints() } [Fact(Timeout = 15000)] - public void BuildAssetPrompt_SoundBank_ContainsSchemaHints() + public async Task BuildAssetPrompt_SoundBank_ContainsSchemaHints() { + await Task.CompletedTask; var prompt = UnityDevCommand.BuildAssetPrompt("soundbank", "weapon sounds"); prompt.Should().Contain("SoundBankDescriptor"); prompt.Should().Contain("SelectionMode"); @@ -581,8 +615,9 @@ public void BuildAssetPrompt_SoundBank_ContainsSchemaHints() } [Fact(Timeout = 15000)] - public void BuildAssetPrompt_AnimationSet_ContainsSchemaHints() + public async Task BuildAssetPrompt_AnimationSet_ContainsSchemaHints() { + await Task.CompletedTask; var prompt = UnityDevCommand.BuildAssetPrompt("animationset", "FPS character locomotion"); prompt.Should().Contain("AnimationSetDescriptor"); prompt.Should().Contain("BlendTree"); @@ -592,8 +627,9 @@ public void BuildAssetPrompt_AnimationSet_ContainsSchemaHints() } [Fact(Timeout = 15000)] - public void Animation_SoundBank_AnimationSet_AreValidAssetTypes() + public async Task Animation_SoundBank_AnimationSet_AreValidAssetTypes() { + await Task.CompletedTask; var prompt1 = UnityDevCommand.BuildAssetPrompt("animation", "test"); var prompt2 = UnityDevCommand.BuildAssetPrompt("soundbank", "test"); var prompt3 = UnityDevCommand.BuildAssetPrompt("animationset", "test"); @@ -604,8 +640,9 @@ public void Animation_SoundBank_AnimationSet_AreValidAssetTypes() } [Fact(Timeout = 15000)] - public void AssetDescriptor_JsonRoundTrip_Material() + public async Task AssetDescriptor_JsonRoundTrip_Material() { + await Task.CompletedTask; var original = new Nexo.GameDomain.Assets.MaterialDescriptor { Id = "mat-1", @@ -627,8 +664,9 @@ public void AssetDescriptor_JsonRoundTrip_Material() } [Fact(Timeout = 15000)] - public void AssetDescriptor_JsonRoundTrip_Audio() + public async Task AssetDescriptor_JsonRoundTrip_Audio() { + await Task.CompletedTask; var original = new Nexo.GameDomain.Assets.AudioDescriptor { Id = "explosion", @@ -663,8 +701,9 @@ public void AssetDescriptor_JsonRoundTrip_Audio() } [Fact(Timeout = 15000)] - public void Pin_Subcommand_Exists() + public async Task Pin_Subcommand_Exists() { + await Task.CompletedTask; var cmd = new UnityDevCommand(() => null!); var pin = cmd.Subcommands.SingleOrDefault(s => s.Name == "pin"); pin.Should().NotBeNull(); @@ -678,8 +717,9 @@ public void Pin_Subcommand_Exists() } [Fact(Timeout = 15000)] - public void Compose_Subcommand_Exists() + public async Task Compose_Subcommand_Exists() { + await Task.CompletedTask; var cmd = new UnityDevCommand(() => null!); var compose = cmd.Subcommands.SingleOrDefault(s => s.Name == "compose"); compose.Should().NotBeNull(); @@ -691,8 +731,9 @@ public void Compose_Subcommand_Exists() } [Fact(Timeout = 15000)] - public void Generate_Subcommand_HasTemplateOption() + public async Task Generate_Subcommand_HasTemplateOption() { + await Task.CompletedTask; var cmd = new UnityDevCommand(() => null!); var gen = cmd.Subcommands.Single(s => s.Name == "generate"); var optNames = gen.Options.Select(o => o.Name).ToList(); @@ -700,8 +741,9 @@ public void Generate_Subcommand_HasTemplateOption() } [Fact(Timeout = 15000)] - public void ExecutePin_PinsField() + public async Task ExecutePin_PinsField() { + await Task.CompletedTask; var tempDir = Path.Combine(Path.GetTempPath(), $"nexo-pin-test-{Guid.NewGuid():N}"); Directory.CreateDirectory(tempDir); var descriptorFile = Path.Combine("weapons", "rifle.json"); @@ -727,8 +769,9 @@ public void ExecutePin_PinsField() } [Fact(Timeout = 15000)] - public void ExecutePin_UnpinsField() + public async Task ExecutePin_UnpinsField() { + await Task.CompletedTask; var tempDir = Path.Combine(Path.GetTempPath(), $"nexo-pin-test-{Guid.NewGuid():N}"); Directory.CreateDirectory(tempDir); var descriptorFile = Path.Combine("weapons", "rifle.json"); @@ -756,15 +799,17 @@ public void ExecutePin_UnpinsField() } [Fact(Timeout = 15000)] - public void ExecutePin_NoValue_ReturnsError() + public async Task ExecutePin_NoValue_ReturnsError() { + await Task.CompletedTask; var code = UnityDevCommand.ExecutePin("/tmp", "file.json", "field", null, false, false); code.Should().Be(1); } [Fact(Timeout = 15000)] - public void BuildGeneratePrompt_InjectsConstraintFragment() + public async Task BuildGeneratePrompt_InjectsConstraintFragment() { + await Task.CompletedTask; var prompt = UnityDevCommand.BuildGeneratePrompt( "weapon system", "Assets/Scripts/Generated", "Assets/Tests", constraintFragment: "\nProject constraints:\n- Max 200 lines per file"); @@ -774,8 +819,9 @@ public void BuildGeneratePrompt_InjectsConstraintFragment() } [Fact(Timeout = 15000)] - public void BuildGeneratePrompt_InjectsTemplateFragment() + public async Task BuildGeneratePrompt_InjectsTemplateFragment() { + await Task.CompletedTask; var prompt = UnityDevCommand.BuildGeneratePrompt( "weapon system", "Assets/Scripts/Generated", "Assets/Tests", templateFragment: "\nFixed values from template:\n name: Shotgun"); @@ -785,8 +831,9 @@ public void BuildGeneratePrompt_InjectsTemplateFragment() } [Fact(Timeout = 15000)] - public void BuildGeneratePrompt_InjectsCompositionContext() + public async Task BuildGeneratePrompt_InjectsCompositionContext() { + await Task.CompletedTask; var prompt = UnityDevCommand.BuildGeneratePrompt( "combat system", "Assets/Scripts/Generated", "Assets/Tests", compositionContext: "Health system provides IDamageable interface"); diff --git a/docs/DocsIndex.md b/docs/DocsIndex.md index 36da89388..11fa9b9bd 100644 --- a/docs/DocsIndex.md +++ b/docs/DocsIndex.md @@ -64,7 +64,7 @@ Documentation index for the Nexo platform. Start here to find what you need. - `docs/FriendMeshPrefab.md` — prefab Docker Compose + env template for a small shared **Nexo.API** hub (friends / tailnet). - `docs/MeshPhase8OperatorHardening.md` — **Mesh Phase 8:** discovery admission, trust alias, `nexo mesh hub` / `mesh director`, TLS example. -- `docs/MeshVirtualLab.md` — **Virtual mesh lab:** two Nexo.API nodes in Docker + verify script (no extra hardware). +- `docs/MeshVirtualLab.md` — **Virtual mesh lab:** two Nexo.API nodes in Docker + verify script (no extra hardware); **`scripts/bootstrap-cloud-mesh-lab.sh`** for Ubuntu/Debian cloud VMs. - `docs/MeshAgentSetupCapabilityBreakdown.md` — mesh agent setup **tear sheet**: capability tiers, ports, and ops checklist mapped to mesh DI surfaces. - `docs/TrustAndInformationArchitecture.md` — sanitization, audit, access boundaries. - `docs/TailscaleAndNexo.md` — Tailscale + Nexo exposure profile, ACL guidance, advisory endpoint. diff --git a/docs/IntegratorGuide.md b/docs/IntegratorGuide.md index 842e03a73..a8c4ea0bf 100644 --- a/docs/IntegratorGuide.md +++ b/docs/IntegratorGuide.md @@ -4,7 +4,9 @@ This guide is for teams embedding Nexo, extending bricks, or hosting custom back ## Getting started with the Nexo SDK -The managed SDK entry point is the `Nexo.Sdk` project (`src/Nexo.Sdk/Nexo.Sdk.csproj`). Add a project reference from your integrator assembly: +The slim **HTTP client** package is the `Nexo.Sdk` project (`src/Nexo.Sdk/Nexo.Sdk.csproj`). Register it with **`AddNexoClientSdk(baseUrl, ...)`** (`Nexo.Sdk.Client`). The obsolete **`AddNexoSdk(string baseUrl, ...)`** name remains as a compat shim. For **host-side** brick/agent registration, use **`Nexo.Hosting.Sdk.AddNexoSdk`** (before `AddNexo`). See [`docs/architecture/SdkStructure.md`](architecture/SdkStructure.md). + +Add a project reference from your integrator assembly: ```xml diff --git a/docs/MeshVirtualLab.md b/docs/MeshVirtualLab.md index e6bae4006..3ca2501c7 100644 --- a/docs/MeshVirtualLab.md +++ b/docs/MeshVirtualLab.md @@ -31,6 +31,31 @@ docker compose -f docker-compose.mesh-lab.yml --env-file .env.mesh-lab up -d --b Host URLs: **`http://127.0.0.1:18081`** (peer-a), **`http://127.0.0.1:18082`** (peer-b). +## Cloud VM (one-shot bootstrap) + +This Cursor/workspace cannot provision cloud VMs or run Docker for you. On **any fresh Ubuntu/Debian VM** (AWS/GCP/Azure/Linode): + +1. **SSH** into the VM and install Git if needed. +2. **Clone** this repository and `cd` to the repo root. +3. Run: + + ```bash + chmod +x scripts/bootstrap-cloud-mesh-lab.sh + ./scripts/bootstrap-cloud-mesh-lab.sh --install-docker + ``` + + `--install-docker` uses **`apt-get`** to install **`docker.io`** and **`docker-compose-v2`**, then creates **`.env.mesh-lab`** from **`docs/config/mesh-lab.env.example`** (random lab secrets), **`docker compose up --build`**, and **`scripts/mesh-lab-verify.sh`**. + +4. From your laptop, **tunnel** the peer ports if the VM has no public listener: + + ```bash + ssh -L 18081:127.0.0.1:18081 -L 18082:127.0.0.1:18082 user@your-vm + ``` + + Then open **`http://127.0.0.1:18081`** / **`18082`** locally. + +If Docker is already installed, omit **`--install-docker`**. For non-apt Linux, install Docker + Compose v2 manually ([Docker Engine install](https://docs.docker.com/engine/install/)), then run **`./scripts/bootstrap-cloud-mesh-lab.sh`** without **`--install-docker`**. + ## Workers + stress ramp ```bash diff --git a/docs/architecture/README.md b/docs/architecture/README.md index bbe6053cf..120670965 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -11,4 +11,7 @@ High-level maps of how Nexo is structured. For day-to-day commands, see the repo | [Forge map adaptation](forge-map-adaptation.md) | `MapAdaptationPlanner`, dry-run pipeline, engine manifest JSON, and Forge persistence options. | | [Forge map host integration](forge-map-host-integration.md) | Milestones M1–M6; terrain payload summaries; optional material **`IModel`** augmentation; tile cache; Unity/Godot package layouts. | | [Aesthetic and engine adaptation](aesthetic-engine-adaptation.md) | Cross-engine `AestheticPack` fields, validation, Forge `apply-pack`, and shared Mapbox tile helpers. | +| [SDK-style layout](SdkStructure.md) | Ports vs options vs builders; `Nexo.Hosting.Sdk` vs `Nexo.Sdk.Client`; folder conventions. | +| [SDK migration plan (remaining gaps)](SdkMigrationPlan.md) | **Execution status** at top; **[Plan: close remaining gaps](#plan-close-remaining-gaps-post-migration)** (D1–D6: docs, sweep, consumers, optional `Sdk/Options`, hosting polish, CI clarity). | +| **`Nexo.Framework.Sdk`** | Optional megaproject in `src/Nexo.Framework.Sdk/` — `AddNexoFramework` combines HTTP client + `AddNexo`. | | [GitHub Actions trigger policy](../../.github/workflows/README.md) | Manual-first workflow policy (`workflow_dispatch`); tag-driven release automation unchanged. | diff --git a/docs/architecture/SdkMigrationPlan.md b/docs/architecture/SdkMigrationPlan.md new file mode 100644 index 000000000..a74394d8d --- /dev/null +++ b/docs/architecture/SdkMigrationPlan.md @@ -0,0 +1,204 @@ +# Plan: close remaining SDK-style layout gaps + +## Execution status (branch) + +| Item | Status | +| ---- | ------ | +| **Track A — Hosting partials** | **`AddNexo`** → **`NexoKernelRegistrar.Register`**; **`NexoKernelRegistrationContext`** + **20 phase methods** in **`NexoKernelRegistrar.Phases.cs`** + **`NexoKernelRegistrar.Ephemeral.cs`** (`EphemeralModelsEnabled`). **`ModuleSelection`** in **`NexoKernelRegistrationModels.cs`**. **`NexoServiceCollectionExtensions.Deployment.cs`** — deployment profile helpers; **`NexoServiceCollectionExtensions.NodeCapabilityRuntime.cs`** — **`RegisterNodeCapabilityRuntime`**. | +| **Track B — Infrastructure `Sdk/`** | **Done:** `*ServiceCollectionExtensions` under **`Feature/Sdk/Extensions/`** with **`Nexo.Infrastructure.Sdk.`** namespaces. Collision-safe: **`Nexo.Infrastructure.NodeCapabilityRuntime.Sdk`**, **`Nexo.Infrastructure.Execution.Sdk`**, **`Nexo.Infrastructure.Execution.Routing.Sdk`**, **`Nexo.Infrastructure.Mesh.Sdk`**. | +| **Consumer projects** | **`GlobalUsings.Infrastructure.Sdk.cs`** in **`Nexo.Hosting`**; **`Nexo.CLI`** and **`Nexo.Tests.Infrastructure`** link it for Sdk extension resolution. | +| **Non-goal — ports** | **`INexoSdkBuilder`** in **`Nexo.Infrastructure.Sdk.Ports`**. | +| **Non-goal — megapackage** | **`Nexo.Framework.Sdk`** + **`AddNexoFramework`**. | + +--- + +This document turns the “remaining gaps” from [`SdkStructure.md`](SdkStructure.md) into **ordered, low-risk work** with clear completion criteria. No calendar estimates — only **what must change**, **dependencies**, and **risk**. + +--- + +## Goal + +1. **`Nexo.Hosting` — kernel registration** + Keep **`AddNexo`** as the public entry point while making wiring **navigable**: deployment/profile resolution stays in **`NexoServiceCollectionExtensions`** partials (**`Deployment`**, **`NodeCapabilityRuntime`**); all subsystem registration runs through **`NexoKernelRegistrar`** (**`Register`** → **`NexoKernelRegistrationContext`** → **`RegisterPhase01`–`RegisterPhase20`** in **`NexoKernelRegistrar.Phases.cs`**). Behavior and order remain unchanged. + +2. **`Nexo.Infrastructure`** + DI registration surface lives under **`Feature/Sdk/Extensions/`** with **`Nexo.Infrastructure.Sdk.*`** extension namespaces (collision-safe **`Nexo.Infrastructure..Sdk`** where needed). Implementation types stay under **`Nexo.Infrastructure.`**. Optional physical **`Sdk/Options/`** groups option types without renaming namespaces (see **`Pipelines/Sdk/Options/`** pilot). + +--- + +## Principle (non-negotiable) + +- **Sdk extension namespaces** — `*ServiceCollectionExtensions` for DI use **`Nexo.Infrastructure.Sdk.*`** (see [`SdkStructure.md`](SdkStructure.md)). Application/runtime types keep existing **`Nexo.Infrastructure.`** namespaces. Consumer apps may use **`GlobalUsings.Infrastructure.Sdk.cs`** (or explicit `using` lines) to bring extension methods into scope. +- **One mechanical theme per PR** — easier review, bisection, and rollback. +- **`dotnet build Nexo.sln` + relevant `dotnet test` filters** after each merge. + +--- + +## Track A — Hosting composition (supersedes old “slice AddNexo into many partials” plan) + +### A.1 Files (current) + +| File | Role | +| ---- | ---- | +| `NexoServiceCollectionExtensions.cs` | **`AddNexo`**, **`AddNexoProfile`** → builds **`NexoKernelRegistrationContext`** and calls **`NexoKernelRegistrar.Register`**. | +| `NexoServiceCollectionExtensions.Deployment.cs` | **`ResolveDeploymentProfile`**, **`GetModuleSelection`**, **`ResolveStrictMode`**, **`ParseBooleanEnvironmentVariable`** (internal). | +| `NexoServiceCollectionExtensions.NodeCapabilityRuntime.cs` | **`RegisterNodeCapabilityRuntime`** (internal) — NCR + model artifact catalog wiring when NCR is enabled. | +| `NexoKernelRegistrationModels.cs` | **`ModuleSelection`**, **`NexoKernelRegistrationContext`**. | +| `NexoKernelRegistrar.cs` | **`Register`** dispatches phases **01–20**. | +| `NexoKernelRegistrar.Phases.cs` | One private method per **`// ──`** section (kernel subsystems). | +| `NexoKernelRegistrar.Ephemeral.cs` | **`EphemeralModelsEnabled()`** shared by ephemeral lifecycle + trust phases. | + +### A.2 Completion criteria + +- **Zero** behavior change vs monolithic registration: same order, env vars, and deployment-profile gates. +- Reviewers can open **`NexoKernelRegistrar.Phases.cs`** and jump by section comment. + +### A.3 Risks + +- **Merge conflicts** if many branches touch **`NexoKernelRegistrar.Phases.cs`** or **`NexoServiceCollectionExtensions`** entry points — coordinate kernel wiring changes in focused PRs. + +--- + +## Track B — Infrastructure SDK folders (incremental) + +### B.1 Convention (repeat per area) + +Under each **top-level feature folder** (e.g. `Observation/`, `NodeCapabilityRuntime/`, `Pipelines/`): + +``` +Feature/ + Sdk/ + Options/ # optional — registration-related *Options.cs (namespaces often unchanged) + Extensions/ # *ServiceCollectionExtensions.cs — DI registration surface + ... existing impl files ... +``` + +- **DI extension classes** (`*ServiceCollectionExtensions`) use **`Nexo.Infrastructure.Sdk.`** (or **`Nexo.Infrastructure..Sdk`** when the simple name collides with runtime types). +- **Implementation types** (stores, adapters, domain-ish infrastructure services) keep **`Nexo.Infrastructure.`** (or deeper sub-namespaces). Physical folder may differ from namespace by design. +- **Do not** force every type into `Sdk/` — only **registration entry points** and, optionally, **options bags** under **`Sdk/Options/`**. + +### B.2 Suggested order (dependency / churn) + +| Phase | Area | Rationale | +| ----- | ---- | ----------- | +| **B.2.1** | **NodeCapabilityRuntime** | Already has `NodeCapabilityRuntimeOptions.cs` + `*ServiceCollectionExtensions.cs`; small, validates pattern. | +| **B.2.2** | **Observation** | `Observation`-related extensions + options grouped; touches Forge-adjacent tests. | +| **B.2.3** | **Pipelines** | Many `Pipeline*` options + `PipelineServiceCollectionExtensions`; high readability win. | +| **B.2.4** | **Persistence / Database** | `PersistenceServiceCollectionExtensions`, `DatabaseServiceCollectionExtensions`, Ephemeral options. | +| **B.2.5** | **Adaptation** | `AdaptationServiceCollectionExtensions`, `AdaptationBrickOptions`. | +| **B.2.6** | **Trust** | `TrustServiceCollectionExtensions`, boundary/gate options. | +| **B.2.7** | **Execution** (routing, mesh bricks) | Larger; split only extension entry files first, leave adapters in place. | +| **B.2.8** | **Remaining** `*ServiceCollectionExtensions.cs` at Infrastructure root | Sweep or fold into nearest feature `Sdk/Extensions`. | + +Lower phases can proceed in parallel **only** if they touch disjoint paths (separate PRs). + +### B.3 Completion criteria (per phase) + +- Types compile with **stable public namespaces** (extensions moved to **`Nexo.Infrastructure.Sdk.*`** as agreed; options moved physically may keep **`Nexo.Infrastructure.`** namespace). +- No new public API unless explicitly intended (prefer moves only). +- **`dotnet build Nexo.sln`** + targeted tests when touching DI. + +### B.4 Risks + +- **Glob imports / IDE** — developers rely on path; communicate in [`SdkStructure.md`](SdkStructure.md) when a phase completes. +- **Copy-assemblies / test harness** — `Nexo.Tests.Infrastructure` copies assemblies; confirm **no hard-coded paths** to old locations (usually unaffected). + +--- + +## Track C — Documentation sync + +After each Track A/B milestone: + +- One-line note under **Folder conventions** in [`SdkStructure.md`](SdkStructure.md) listing completed areas (optional table). +- No duplicate prose — link to this plan for “what’s left.” + +--- + +## Definition of “done” for the overall initiative + +This initiative is **functionally complete** for kernel DI and Infrastructure Sdk extensions (see **Execution status** above). Remaining work is **documentation alignment**, **optional layout polish**, **consumer ergonomics**, and **CI clarity** — tracked in **[Plan: close remaining gaps](#plan-close-remaining-gaps-post-migration)** below. + +Historical bullets (superseded where noted): + +- **Track A — achieved differently:** `AddNexo` delegates to **`NexoKernelRegistrar`** with phase partials (`NexoKernelRegistrar.Phases.cs`), not multiple `NexoServiceCollectionExtensions.*` partial files. Navigation goal is met via registrar phases + `Deployment` partial. +- **Track B — DI extensions:** `*ServiceCollectionExtensions` live under **`Feature/Sdk/Extensions/`** with **`Nexo.Infrastructure.Sdk.*`** (and collision-safe `*.Sdk` namespaces). Optional **`Sdk/Options`** physical grouping remains incremental. +- CI green on **`Nexo.sln`**; **`Nexo.LocalDevCore.slnf`** / **`Nexo.PrimeTime.slnf`** as documented in repo CI / contributor docs (see closing plan). + +--- + +## Plan: close remaining gaps (post-migration) + +Ordered for **low risk** first; each phase can be its own PR. + +### Phase D1 — Documentation alignment (required) + +| Step | Action | Done when | +| ---- | ------ | --------- | +| D1.1 | Rewrite **Goal**, **Track A §A.1–A.2**, and **Definition of done** in this file so they describe **`NexoKernelRegistrar`** + **`NexoKernelRegistrationContext`** + **`NexoKernelRegistrar.Phases.cs`**, not hypothetical `NexoServiceCollectionExtensions.AddNexo.*` partials. | Text matches repo; no contradictory inventory tables. | +| D1.2 | Fix **Track B §B.1**: state that **DI extension types** use **`Nexo.Infrastructure.Sdk.*`** (and **`Nexo.Infrastructure..Sdk`** where collision-safe), while **implementation types** remain **`Nexo.Infrastructure.`**. Remove “keep namespace on moved files” if it implies zero namespace change for extensions. | Single coherent rule for extensions vs runtime types. | +| D1.3 | Add a short **“Completed areas”** table to [`SdkStructure.md`](SdkStructure.md) (folders + extension namespace pattern), or a bullet list linking to feature folders under **`Sdk/Extensions/`**. | Readers see what’s migrated without reading git history. | + +### Phase D2 — Mechanical repo sweep (required) + +| Step | Action | Done when | +| ---- | ------ | --------- | +| D2.1 | Search for **`*ServiceCollectionExtensions.cs`** outside **`**/Sdk/Extensions/`** under `src/Nexo.Infrastructure`. Either move stragglers into **`Sdk/Extensions/`** or document why they stay (e.g. generated, exceptional). | No unexplained duplicates at old paths. | +| D2.2 | Confirm **Observation** and other pilots still compile and tests touching DI registration pass (narrow filters acceptable). | `dotnet build Nexo.sln` green. | + +### Phase D3 — Consumer ergonomics (recommended) + +| Step | Action | Done when | +| ---- | ------ | --------- | +| D3.1 | Audit **`*.csproj`** files that **reference `Nexo.Infrastructure`** and call Sdk extension methods **without** going through **`AddNexo`**. For each: add **``** (same pattern as CLI / Tests.Infrastructure) **or** explicit **`using Nexo.Infrastructure.Sdk.*`** in a single `Usings.cs`. | No CS1061 surprises when adding new Sdk namespaces to Hosting’s global-usings file. | +| D3.2 | Document the **recommended pattern** in [`SdkStructure.md`](SdkStructure.md) (“link Hosting `GlobalUsings.Infrastructure.Sdk.cs` vs explicit usings”). | Contributors have a default choice. | + +### Phase D4 — Optional `Sdk/Options` layout (incremental, descoping allowed) + +| Step | Action | Done when | +| ---- | ------ | --------- | +| D4.1 | Pick **one** feature (e.g. **Pipelines** or **NodeCapabilityRuntime**) and move **registration-related option types** into **`Feature/Sdk/Options/`** without changing **public type names** or namespaces unless deliberate. | Pattern validated; tests/build green. | +| D4.2 | Repeat per feature **only** where readability wins; otherwise list **explicitly descoped** areas in this plan. | No forced churn for marginal benefit. | + +**D4.1 pilot (done):** `PipelineExecutionOptions`, `PipelinePersistenceOptions`, and `PipelineExecutionAdapterOptions` live under **`src/Nexo.Infrastructure/Pipelines/Sdk/Options/`**; namespaces remain **`Nexo.Infrastructure.Pipelines`**. + +**D4.2 descoped (for now):** bulk moves for NodeCapabilityRuntime options, Trust, Adaptation, Persistence DB options — defer until a feature owner requests clearer separation. + +### Phase D5 — Hosting polish (optional) + +| Step | Action | Done when | +| ---- | ------ | --------- | +| D5.1 | Extract **`RegisterNodeCapabilityRuntime`** into a dedicated **`NexoServiceCollectionExtensions.NodeCapabilityRuntime.cs`** partial **or** leave as-is with a one-line comment pointing to **`NexoKernelRegistrar`** phase 01. | Clear ownership of NCR registration story. | +| D5.2 | Optionally deduplicate **`ephemeralModels`** computation between **`RegisterPhase14_EphemeralLifecycle`** and **`RegisterPhase15_TrustProviderFactory3wayBranching`** via a private static helper or a small value on **`NexoKernelRegistrationContext`** (only if behavior stays identical). | One env-read path or documented equivalence. | + +### Phase D6 — CI / “definition of done” clarity (recommended) + +| Step | Action | Done when | +| ---- | ------ | --------- | +| D6.1 | Align **contributor / CI docs** (e.g. `.github` workflows, `CONTRIBUTING.md` if present) with which solution filters run on PRs: **`Nexo.sln`**, **`Nexo.LocalDevCore.slnf`**, **`Nexo.PrimeTime.slnf`**. | Expectations match automation. | +| D6.2 | If **`PrimeTime`** is PR-gated, note **minimum test command** for SDK-touching PRs in one place. | Authors know what to run locally. | + +### Risks and mitigations + +- **D3 wide linking** — Linking global usings into many projects can hide missing imports; mitigation: keep Hosting file as **single source of truth** and review link list when adding Sdk namespaces. +- **D4 options moves** — Namespace or folder churn can break analyzers; mitigation: **one feature per PR**, namespace-stable moves only. + +--- + +### Phase D — execution checklist + +| Phase | Summary | +| ----- | ------- | +| **D1** | Done — Goal / Track A / Track B §B.1 aligned with **`NexoKernelRegistrar`** and Sdk extension namespaces; **`SdkStructure.md`** lists completed areas and consumer guidance. | +| **D2** | Done — All **`src/Nexo.Infrastructure/**/*ServiceCollectionExtensions*.cs`** files live under **`Sdk/Extensions/`**; **`dotnet build Nexo.sln`** verified. | +| **D3** | Done — Audit documented in **`SdkStructure.md`**: **`GlobalUsings.Infrastructure.Sdk.cs`** linked from **CLI** and **Tests.Infrastructure**; **`AddNexo`** hosts (**e.g. Nexo.API**) need no separate Sdk usings; projects that only reference Infrastructure **types** skip the link. | +| **D4** | Done — **`Pipelines/Sdk/Options/`** pilot; further option-folder moves descoped in **D4.2** above. | +| **D5** | Done — **`NexoServiceCollectionExtensions.NodeCapabilityRuntime.cs`**; shared **`EphemeralModelsEnabled()`** in **`NexoKernelRegistrar.Ephemeral.cs`**. | +| **D6** | Done — **`CONTRIBUTING.md`** — solution filters, **PrimeTime** / **LocalDevCore**, **Makefile** targets, workflow pointers. | + +--- + +## Explicit non-goals (unless product asks) + +- **Mass-renaming** of **implementation** types from **`Nexo.Infrastructure.`** to **`Nexo.Infrastructure.Sdk.*`** (breaking; extension **classes** already use Sdk-style namespaces by design). +- Introducing a **single mega-package** that re-exports all extensions (maintenance burden). +- Moving **port interfaces** out of `Nexo.Core.Application` into Infrastructure. diff --git a/docs/architecture/SdkStructure.md b/docs/architecture/SdkStructure.md new file mode 100644 index 000000000..c6b93023b --- /dev/null +++ b/docs/architecture/SdkStructure.md @@ -0,0 +1,81 @@ +# SDK-style layout in the Nexo codebase + +This repository uses a consistent **SDK composition** model so features stay **extensible** (interfaces + options + default implementations) and **discoverable** (grouped folders and entry types). + +## Layers + +| Layer | Responsibility | Typical contents | +| ----- | -------------- | ---------------- | +| **Ports** (`Nexo.Core.Application.*.Ports`, plus **`Nexo.Infrastructure.Sdk.Ports`** for host SDK surface) | Contracts (`interface`), commands/queries, DTOs | Stable integration surface | +| **Options** | Immutable-style configuration (`record` / `class` with init-only props) | Bound from DI / env / config sections | +| **Infrastructure** | Default adapters implementing ports | Swappable in tests or overrides | +| **Hosting SDK** (`Nexo.Hosting.Sdk`) | Kernel composition for bricks, agents, cards | `AddNexoSdk`, `NexoSdkOptions`, `HostNexoSdkBuilder` | +| **HTTP client SDK** (`Nexo.Sdk` NuGet, namespace `Nexo.Sdk.Client`) | Slim remote API client (`AddNexoClientSdk`, `NexoClientSdkBuilder`) | Unity / embedded callers | + +## Folder conventions (physical) + +These conventions usually **preserve** existing namespaces for bulk moves; **new** SDK folders may introduce **`Nexo.Infrastructure.Sdk.*`** namespaces where called out below. + +- **`Sdk/Options/`** — option bags and enums tied to registration (`NexoHostingOptions`, deployment profile, host SDK options). +- **`Sdk/Builders/`** — fluent builders implementing port interfaces (`HostNexoSdkBuilder` implements `INexoSdkBuilder`). +- **`Sdk/Extensions/`** — `*ServiceCollectionExtensions`, OpenTelemetry hooks, etc. +- **`Observation/Sdk/Extensions/`** — DI extensions use namespace **`Nexo.Infrastructure.Sdk.Observation`** (`AddObservationCore`, `AddObservationInfrastructure`). +- **Other feature areas** — same physical layout; namespaces follow **`Nexo.Infrastructure.Sdk.`** unless a **name collision** with runtime types forces **`Nexo.Infrastructure..Sdk`** (see **`NodeCapabilityRuntime`**, **`Execution`**, **`Execution.Routing`**, **`Mesh`**). + +### Mechanical sweep (`*ServiceCollectionExtensions`) + +Every **`Nexo.Infrastructure`** DI extension file is under **`Feature/Sdk/Extensions/`** (filename pattern **`*ServiceCollectionExtensions*.cs`**). There are no parallel copies under legacy paths. + +### Completed areas (DI extensions) + +Extension entry points and namespaces (collision-safe variants where noted): + +| Feature folder | Extension namespace | +| -------------- | ------------------- | +| **Adaptation** (`Adaptation/Sdk/Extensions/`) | `Nexo.Infrastructure.Sdk.Adaptation` | +| **Analysis** (`Analysis/BrickAnalyzer/Sdk/Extensions/`) | `Nexo.Infrastructure.Sdk.Analysis` | +| **Composition** (`Composition/Sdk/Extensions/`) | `Nexo.Infrastructure.Sdk.Composition` | +| **Execution** (`Execution/Sdk/Extensions/`) | `Nexo.Infrastructure.Execution.Sdk` | +| **Execution/Routing** (`Execution/Routing/Sdk/Extensions/`) | `Nexo.Infrastructure.Execution.Routing.Sdk` | +| **Maintenance** (`Maintenance/Sdk/Extensions/`) | `Nexo.Infrastructure.Sdk.Maintenance` | +| **Mesh** (`Mesh/Sdk/Extensions/`) | `Nexo.Infrastructure.Mesh.Sdk` | +| **ModelArtifacts** (`ModelArtifacts/Sdk/Extensions/`) | `Nexo.Infrastructure.Sdk.ModelArtifacts` | +| **NodeCapabilityRuntime** (`NodeCapabilityRuntime/Sdk/Extensions/`) | `Nexo.Infrastructure.NodeCapabilityRuntime.Sdk` | +| **Observation** (`Observation/Sdk/Extensions/`) | `Nexo.Infrastructure.Sdk.Observation` | +| **ParallelTesting** (`ParallelTesting/Sdk/Extensions/`) | `Nexo.Infrastructure.Sdk.ParallelTesting` | +| **Persistence** (`Persistence/Sdk/Extensions/`) | `Nexo.Infrastructure.Sdk.Persistence` | +| **Pipelines** (`Pipelines/Sdk/Extensions/`) | `Nexo.Infrastructure.Sdk.Pipelines` | +| **Rollback** (`Rollback/Sdk/Extensions/`) | `Nexo.Infrastructure.Sdk.Rollback` | +| **SelfContext** (`SelfContext/Sdk/Extensions/`) | `Nexo.Infrastructure.Sdk.SelfContext` | +| **SelfImprovement** (`SelfImprovement/Sdk/Extensions/`) | `Nexo.Infrastructure.Sdk.SelfImprovement` | +| **Trust** (`Trust/Sdk/Extensions/`) | `Nexo.Infrastructure.Sdk.Trust` | + +### Optional `Sdk/Options` pilot + +**Pipelines:** **`PipelineExecutionOptions`**, **`PipelinePersistenceOptions`**, **`PipelineExecutionAdapterOptions`** live under **`Pipelines/Sdk/Options/`** with namespaces unchanged (**`Nexo.Infrastructure.Pipelines`**). + +### Bringing Sdk extensions into scope + +Extension methods require their namespace in scope. + +| Approach | When to use | +| -------- | ----------- | +| **Link `src/Nexo.Hosting/GlobalUsings.Infrastructure.Sdk.cs`** from your `.csproj` (``) | Projects that **wire `IServiceCollection` manually** and call Infrastructure Sdk extensions (**same pattern as `Nexo.CLI`** and **`Nexo.Tests.Infrastructure`**). Keeps one source of truth when Hosting adds Sdk namespaces. | +| **Explicit `using Nexo.Infrastructure.Sdk.*`** (or feature-specific Sdk namespaces) | Libraries that **cannot** reference Hosting paths; small surface area. | +| **Neither** | Apps that only call **`services.AddNexo(...)`** (**e.g. `Nexo.API`**) — kernel registration pulls in dependencies; no Infrastructure Sdk `using` needed for typical **`Program.cs`**. | +| **Types only** | Projects like **`Nexo.Bricks.Owasp`** that reference Infrastructure for **adapters / types** but not registration extensions — **no** global Sdk usings file. | + +**`src/Nexo.Hosting/GlobalUsings.Infrastructure.Sdk.cs`** lists **`global using`** lines for Sdk namespaces used by the host. + +## Naming + +- **`HostNexoSdkBuilder`** — in-process Nexo kernel registration (before `AddNexo`). +- **`NexoClientSdkBuilder`** — NuGet client package configuration (`Nexo.Sdk`). +- Legacy aliases (`[Obsolete]`) remain until the next major bump. + +## Composition order + +1. Optional: `services.AddNexoSdk(...)` (host SDK — bricks/agents). +2. `services.AddNexo(...)` (kernel). + +Remote-only apps use **`AddNexoClientSdk`** instead of `AddNexo`. diff --git a/docs/bricks/unknown.md b/docs/bricks/unknown.md index 57a5a9129..9a0e96d4c 100644 --- a/docs/bricks/unknown.md +++ b/docs/bricks/unknown.md @@ -2,18 +2,18 @@ ## Behavior -Adapted from promotion 00ba68f2bf7d453baf4e01540f18688d. +Adapted from promotion f13a77ebd79a40fa81b5db46c16548b0. -**Last updated:** 2026-04-16 +**Last updated:** 2026-05-09 ## Changelog ```markdown # Changelog -## 2026-04-15 – 2026-04-16 +## 2026-05-08 – 2026-05-09 -- **unknown**: Fixed EmptyCatch in EmptyCatch.cs (2026-04-16 01:42) +- **unknown**: Fixed EmptyCatch in EmptyCatch.cs (2026-05-09 20:57) ``` diff --git a/docs/sdk.md b/docs/sdk.md index 582812e47..bea572b04 100644 --- a/docs/sdk.md +++ b/docs/sdk.md @@ -11,9 +11,10 @@ Use the host surface when embedding Nexo into your own service. Use the client s ### Stable -- `Nexo.Hosting.Sdk` (`AddNexoSdk(Action)` on `IServiceCollection`) -- `Nexo.Core.Application.Sdk.Ports.INexoSdkBuilder` -- `Nexo.Sdk` + `Nexo.Client` (`INexoClient`, `AddNexoSdk(baseUrl, ...)`) +- `Nexo.Hosting.Sdk` (`AddNexoSdk(Action)` on `IServiceCollection`; builder implementation `HostNexoSdkBuilder`) +- `Nexo.Infrastructure.Sdk.Ports.INexoSdkBuilder` +- `Nexo.Sdk.Client` (`AddNexoClientSdk(baseUrl, ...)`, `NexoClientSdkBuilder`) + `Nexo.Client` (`INexoClient`) +- Obsolete compat: `AddNexoSdk(baseUrl, ...)` / `NexoSdkBuilder` on the client package (same assembly as `Nexo.Sdk`) ### Deprecated diff --git a/scripts/bootstrap-cloud-mesh-lab.sh b/scripts/bootstrap-cloud-mesh-lab.sh new file mode 100755 index 000000000..d5addaed6 --- /dev/null +++ b/scripts/bootstrap-cloud-mesh-lab.sh @@ -0,0 +1,127 @@ +#!/usr/bin/env bash +# Bootstrap a Linux host (e.g. cloud VM) for the virtual mesh lab: Docker + Compose, +# env file, build/up, verify. Run from the Nexo repository root after clone. +# +# Usage: +# chmod +x scripts/bootstrap-cloud-mesh-lab.sh +# ./scripts/bootstrap-cloud-mesh-lab.sh # create .env.mesh-lab if missing, up + verify +# ./scripts/bootstrap-cloud-mesh-lab.sh --install-docker # apt-only: install docker.io + compose v2 +# +# See docs/MeshVirtualLab.md + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "${REPO_ROOT}" + +INSTALL_DOCKER=false +SKIP_VERIFY=false + +usage() { + echo "Usage: $0 [--install-docker] [--skip-verify]" + echo " --install-docker On Debian/Ubuntu: apt-get install docker.io docker-compose-v2 (requires sudo)." + echo " --skip-verify Only docker compose up (no mesh-lab-verify.sh)." +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --install-docker) INSTALL_DOCKER=true ;; + --skip-verify) SKIP_VERIFY=true ;; + -h|--help) usage; exit 0 ;; + *) echo "Unknown option: $1" >&2; usage; exit 1 ;; + esac + shift +done + +run_sudo() { + if [[ "${EUID}" -eq 0 ]]; then + "$@" + elif command -v sudo >/dev/null 2>&1; then + sudo "$@" + else + echo "Root or sudo required for: $*" >&2 + exit 1 + fi +} + +install_docker_apt() { + if command -v docker >/dev/null 2>&1 && docker compose version >/dev/null 2>&1; then + echo "Docker and docker compose already available." + return 0 + fi + if ! command -v apt-get >/dev/null 2>&1; then + echo "This script only auto-installs Docker on apt-based systems. Install Docker Engine + Compose v2 manually:" >&2 + echo " https://docs.docker.com/engine/install/" >&2 + exit 1 + fi + echo "Installing docker.io and docker-compose-v2 (Ubuntu/Debian)..." + run_sudo apt-get update + run_sudo apt-get install -y docker.io docker-compose-v2 + if command -v systemctl >/dev/null 2>&1; then + run_sudo systemctl enable --now docker || true + fi +} + +ensure_compose() { + if command -v docker >/dev/null 2>&1 && docker compose version >/dev/null 2>&1; then + return 0 + fi + echo "Docker Compose v2 is required ('docker compose version')." >&2 + echo " Debian/Ubuntu: sudo apt-get install -y docker.io docker-compose-v2" >&2 + echo " Or run this script with: --install-docker" >&2 + exit 1 +} + +ensure_env_file() { + local example="${REPO_ROOT}/docs/config/mesh-lab.env.example" + local target="${REPO_ROOT}/.env.mesh-lab" + if [[ -f "${target}" ]]; then + echo "Using existing ${target}" + return 0 + fi + if [[ ! -f "${example}" ]]; then + echo "Missing ${example}" >&2 + exit 1 + fi + cp "${example}" "${target}" + # Non-cryptographic placeholders for a throwaway lab (override for real secrets). + local k b w + k="$(openssl rand -hex 16 2>/dev/null || printf '%s' "lab-key-change-me")" + b="$(openssl rand -hex 16 2>/dev/null || printf '%s' "lab-bearer-change-me")" + w="$(openssl rand -hex 12 2>/dev/null || printf '%s' "lab-basic-change-me")" + sed -i.bak \ + -e "s/^Nexo__Security__ApiKey=.*/Nexo__Security__ApiKey=${k}/" \ + -e "s/^Nexo__Security__PeerB__BearerToken=.*/Nexo__Security__PeerB__BearerToken=${b}/" \ + -e "s/^Nexo__Security__Worker__BasicAuthPassword=.*/Nexo__Security__Worker__BasicAuthPassword=${w}/" \ + "${target}" && rm -f "${target}.bak" + echo "Created ${target} with random lab secrets (review before production use)." +} + +main() { + if [[ "${INSTALL_DOCKER}" == "true" ]]; then + install_docker_apt + fi + ensure_compose + + ensure_env_file + + echo "Building and starting mesh lab (docker-compose.mesh-lab.yml)..." + docker compose -f docker-compose.mesh-lab.yml --env-file .env.mesh-lab up -d --build + + if [[ "${SKIP_VERIFY}" == "true" ]]; then + echo "Skip verify (--skip-verify). Peers: http://127.0.0.1:18081 and :18082 on this host." + exit 0 + fi + + echo "Running mesh-lab-verify.sh ..." + bash "${REPO_ROOT}/scripts/mesh-lab-verify.sh" "${REPO_ROOT}/.env.mesh-lab" + + echo "" + echo "Mesh lab is up. On this machine:" + echo " peer-a: http://127.0.0.1:18081" + echo " peer-b: http://127.0.0.1:18082" + echo "SSH tunnel from laptop: ssh -L 18081:127.0.0.1:18081 -L 18082:127.0.0.1:18082 user@vm" + echo "Stop: docker compose -f docker-compose.mesh-lab.yml --env-file .env.mesh-lab down -v" +} + +main diff --git a/scripts/fix-xunit-sync-timeout-tests.py b/scripts/fix-xunit-sync-timeout-tests.py new file mode 100644 index 000000000..2011ddedd --- /dev/null +++ b/scripts/fix-xunit-sync-timeout-tests.py @@ -0,0 +1,181 @@ +#!/usr/bin/env python3 +"""Convert sync [Fact|Theory(Timeout)] tests to async Task + await Task.CompletedTask (xUnit 2.9+).""" + +from __future__ import annotations + +import re +import sys +from pathlib import Path + +# After [Fact(Timeout)] / [Theory(Timeout)], the next method may be separated by +# other attributes (e.g. [InlineData]). +_METHOD_START = re.compile( + r"^(\s*)public\s+(async\s+)?(void|Task)\s+(\w+)\s*\(" +) +_ATTR_WITH_TIMEOUT = re.compile(r"^\s*\[(Fact|Theory)\(Timeout") + + +def _next_nonempty_line_index(lines: list[str], start: int) -> int | None: + j = start + while j < len(lines): + if lines[j].strip(): + return j + j += 1 + return None + + +def _find_open_brace_line(lines: list[str], sig_start: int) -> tuple[int, bool]: + """Return (line_index_of_brace, brace_only_line). Walks multi-line signatures.""" + idx = sig_start + depth = lines[idx].count("(") - lines[idx].count(")") + while idx < len(lines) and depth > 0: + idx += 1 + if idx >= len(lines): + break + depth += lines[idx].count("(") - lines[idx].count(")") + scan_from = idx + for k in range(scan_from, min(scan_from + 10, len(lines))): + ln = lines[k] + if "{" not in ln: + continue + stripped = ln.strip() + if stripped == "{": + return k, True + if "{" in ln: + return k, stripped.endswith("{") and ln.rstrip().endswith("{") + return sig_start, False + + +def _insert_after_open_brace(lines: list[str], brace_idx: int, brace_only: bool) -> None: + ln = lines[brace_idx] + indent_match = re.match(r"^(\s*)", ln) + base_indent = indent_match.group(1) if indent_match else "" + + if brace_only and ln.strip() == "{": + inner = base_indent + " " + insert_at = brace_idx + 1 + ni = _next_nonempty_line_index(lines, insert_at) + if ni is not None: + nxs = lines[ni].lstrip() + if nxs.startswith(("await ", "return ", "throw ")): + return + lines.insert(insert_at, f"{inner}await Task.CompletedTask;") + return + + # Brace on same line as signature or closing paren + pos = ln.find("{") + if pos < 0: + return + after = ln[pos + 1 :] + inner = base_indent + " " + if after.strip(): + rest_stripped = after.lstrip() + if rest_stripped.startswith(("await ", "return ", "throw ")): + return + lines[brace_idx] = ln[: pos + 1] + lines.insert(brace_idx + 1, f"{inner}await Task.CompletedTask;") + if after.strip(): + lines.insert(brace_idx + 2, after) + return + + inner_indent = base_indent + " " + insert_at = brace_idx + 1 + ni = _next_nonempty_line_index(lines, insert_at) + if ni is not None: + nxs = lines[ni].lstrip() + if nxs.startswith(("await ", "return ", "throw ")): + return + lines.insert(insert_at, f"{inner_indent}await Task.CompletedTask;") + + +def _find_method_after_timeout(lines: list[str], timeout_attr_idx: int) -> int | None: + """First line that starts a public instance method (void or Task). Skips attributes.""" + j = timeout_attr_idx + 1 + while j < len(lines): + raw = lines[j] + stripped = raw.strip() + if stripped.startswith("public ") and "(" in raw: + return j + # Skip attributes and trivia (comments only — careful: block comments rare) + if stripped.startswith("//") or stripped.startswith("#"): + j += 1 + continue + if stripped.startswith("["): + j += 1 + continue + if not stripped: + j += 1 + continue + # Hit something else (e.g. closing brace of previous method) — stop + break + return None + + +def process_file(text: str) -> tuple[str, int]: + lines = text.split("\n") + converted = 0 + i = 0 + while i < len(lines): + line = lines[i] + if not _ATTR_WITH_TIMEOUT.match(line): + i += 1 + continue + + attr_idx = i + meth_idx = _find_method_after_timeout(lines, attr_idx) + if meth_idx is None: + i += 1 + continue + + sig_line = lines[meth_idx] + m = _METHOD_START.match(sig_line) + if not m: + i += 1 + continue + + if m.group(2): # async + i += 1 + continue + + ret = m.group(3) + if ret != "void": + i += 1 + continue + + lines[meth_idx] = sig_line.replace("public void ", "public async Task ", 1) + converted += 1 + + brace_idx, brace_only = _find_open_brace_line(lines, meth_idx) + _insert_after_open_brace(lines, brace_idx, brace_only) + + i = attr_idx + 1 + + return "\n".join(lines), converted + + +def main() -> int: + root = Path(__file__).resolve().parents[1] / "src" + if not root.is_dir(): + print(f"Missing {root}", file=sys.stderr) + return 1 + total_files = 0 + total_methods = 0 + for path in sorted(root.rglob("*.cs")): + raw = path.read_text(encoding="utf-8") + if "public void" not in raw: + continue + if "[Fact(Timeout" not in raw and "[Theory(Timeout" not in raw: + continue + fixed, n = process_file(raw) + if n == 0: + continue + path.write_text(fixed, encoding="utf-8") + total_files += 1 + total_methods += n + print(f"{path.relative_to(root.parents[1])}: {n} method(s)") + print(f"Files updated: {total_files}; methods converted: {total_methods}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/Nexo.BackgroundAgents/ServiceCollectionExtensions.cs b/src/Nexo.BackgroundAgents/ServiceCollectionExtensions.cs index e7748df55..0dc7fb54d 100644 --- a/src/Nexo.BackgroundAgents/ServiceCollectionExtensions.cs +++ b/src/Nexo.BackgroundAgents/ServiceCollectionExtensions.cs @@ -23,6 +23,7 @@ using Nexo.Core.Application.Trust.Ports; using Nexo.Infrastructure.Observation; using Nexo.Infrastructure.Trust; +using Nexo.Infrastructure.Sdk.Trust; using Nexo.Orchestration.Agents; namespace Nexo.BackgroundAgents; diff --git a/src/Nexo.Framework.Sdk/Nexo.Framework.Sdk.csproj b/src/Nexo.Framework.Sdk/Nexo.Framework.Sdk.csproj new file mode 100644 index 000000000..c44b65114 --- /dev/null +++ b/src/Nexo.Framework.Sdk/Nexo.Framework.Sdk.csproj @@ -0,0 +1,18 @@ + + + + net8.0 + 12.0 + enable + enable + Nexo.Framework.Sdk + Optional megapackage-style composition: HTTP client + Nexo kernel registration entry points. + false + + + + + + + + diff --git a/src/Nexo.Framework.Sdk/NexoFrameworkServiceCollectionExtensions.cs b/src/Nexo.Framework.Sdk/NexoFrameworkServiceCollectionExtensions.cs new file mode 100644 index 000000000..3da92aac4 --- /dev/null +++ b/src/Nexo.Framework.Sdk/NexoFrameworkServiceCollectionExtensions.cs @@ -0,0 +1,47 @@ +using Microsoft.Extensions.DependencyInjection; +using Nexo.Hosting; + +namespace Nexo.Framework.Sdk; + +/// +/// Single visible option bag for apps that want client + host wiring without importing multiple extension namespaces. +/// +public sealed class NexoFrameworkOptions +{ + /// + /// When set, registers AddNexoClientSdk from Nexo.Sdk.Client. + /// Remote-only apps can skip . + /// + public string? RemoteApiBaseUrl { get; set; } + + /// Forwarded to Hosting AddNexo. + public Action? ConfigureHost { get; set; } + + /// When true (default), registers the full Nexo kernel via AddNexo. + public bool RegisterKernel { get; set; } = true; +} + +/// +/// Aggregates stable Nexo integration extension methods for hybrid or quick bootstrap scenarios. +/// +public static class NexoFrameworkServiceCollectionExtensions +{ + /// + /// Optionally registers the HTTP Nexo client and/or the in-process kernel. + /// + public static IServiceCollection AddNexoFramework( + this IServiceCollection services, + Action? configure = null) + { + var opt = new NexoFrameworkOptions(); + configure?.Invoke(opt); + + if (!string.IsNullOrWhiteSpace(opt.RemoteApiBaseUrl)) + Nexo.Sdk.Client.NexoClientSdkServiceCollectionExtensions.AddNexoClientSdk(services, opt.RemoteApiBaseUrl); + + if (opt.RegisterKernel) + Nexo.Hosting.NexoServiceCollectionExtensions.AddNexo(services, opt.ConfigureHost); + + return services; + } +} diff --git a/src/Nexo.Hosting/GlobalUsings.Infrastructure.Sdk.cs b/src/Nexo.Hosting/GlobalUsings.Infrastructure.Sdk.cs new file mode 100644 index 000000000..3c5777955 --- /dev/null +++ b/src/Nexo.Hosting/GlobalUsings.Infrastructure.Sdk.cs @@ -0,0 +1,17 @@ +// Infrastructure DI extension surface area (Sdk namespaces). +global using Nexo.Infrastructure.Execution.Routing.Sdk; +global using Nexo.Infrastructure.Execution.Sdk; +global using Nexo.Infrastructure.Mesh.Sdk; +global using Nexo.Infrastructure.NodeCapabilityRuntime.Sdk; +global using Nexo.Infrastructure.Sdk.Adaptation; +global using Nexo.Infrastructure.Sdk.Analysis; +global using Nexo.Infrastructure.Sdk.Composition; +global using Nexo.Infrastructure.Sdk.Maintenance; +global using Nexo.Infrastructure.Sdk.ModelArtifacts; +global using Nexo.Infrastructure.Sdk.Observation; +global using Nexo.Infrastructure.Sdk.ParallelTesting; +global using Nexo.Infrastructure.Sdk.Persistence; +global using Nexo.Infrastructure.Sdk.Pipelines; +global using Nexo.Infrastructure.Sdk.SelfContext; +global using Nexo.Infrastructure.Sdk.SelfImprovement; +global using Nexo.Infrastructure.Sdk.Trust; diff --git a/src/Nexo.Hosting/NexoKernelRegistrar.Ephemeral.cs b/src/Nexo.Hosting/NexoKernelRegistrar.Ephemeral.cs new file mode 100644 index 000000000..1d3e3f65e --- /dev/null +++ b/src/Nexo.Hosting/NexoKernelRegistrar.Ephemeral.cs @@ -0,0 +1,13 @@ +namespace Nexo.Hosting; + +internal static partial class NexoKernelRegistrar +{ + /// + /// Mirrors NEXO_EPHEMERAL / NEXO_EPHEMERAL_MODELS handling used by ephemeral lifecycle and trust wiring. + /// + private static bool EphemeralModelsEnabled() + { + var ephemeralAll = string.Equals(Environment.GetEnvironmentVariable("NEXO_EPHEMERAL"), "1", StringComparison.OrdinalIgnoreCase); + return ephemeralAll || string.Equals(Environment.GetEnvironmentVariable("NEXO_EPHEMERAL_MODELS"), "1", StringComparison.OrdinalIgnoreCase); + } +} diff --git a/src/Nexo.Hosting/NexoKernelRegistrar.Phases.cs b/src/Nexo.Hosting/NexoKernelRegistrar.Phases.cs new file mode 100644 index 000000000..cabb085ef --- /dev/null +++ b/src/Nexo.Hosting/NexoKernelRegistrar.Phases.cs @@ -0,0 +1,673 @@ +using FluentValidation; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Http; +using Nexo.Abstractions.Routing; +using Nexo.Abstractions.Transport; +using Nexo.BackgroundAgents; +using Nexo.BackgroundAgents.Trust; +using Nexo.Core.Application.Adaptation.Ports; +using Nexo.Core.Application.Analysis.UseCases.AnalyzeCode; +using Nexo.Core.Application.Common.Ports; +using Nexo.Core.Application.Common.Services; +using Nexo.Core.Application.Copilot.Ports; +using Nexo.Core.Application.Ephemeral.Ports; +using Nexo.Core.Application.Knowledge.Ports; +using Nexo.Core.Application.Observation.Ports; +using Nexo.Core.Application.Paths; +using Nexo.Core.Application.Testing.UseCases.RunTests; +using Nexo.Core.Application.Trust.Ports; +using Nexo.Core.Application.Validation.UseCases.RunValidation; +using Nexo.Contracts; +using Nexo.Infrastructure; +using Nexo.Infrastructure.Environments; +using Nexo.Infrastructure.Fleet; +using Nexo.Infrastructure.Copilot; +using Nexo.Infrastructure.Execution; +using Nexo.Infrastructure.Execution.Ephemeral; +using Nexo.Infrastructure.Execution.LoadPolicy; +using Nexo.Infrastructure.Execution.Routing; +using Nexo.Infrastructure.Knowledge; +using Nexo.Infrastructure.Maintenance; +using Nexo.Infrastructure.ModelArtifacts; +using Nexo.Infrastructure.NodeCapabilityRuntime; +using Nexo.Infrastructure.Persistence; +using Nexo.Infrastructure.Persistence.Ephemeral; +using Nexo.Infrastructure.Pipelines; +using Nexo.Orchestration; +using Nexo.Orchestration.Models; +using Nexo.Orchestration.Transport; +using Nexo.Runtime; +using Nexo.Runtime.Routing; +using Nexo.Transport.Grpc; + +namespace Nexo.Hosting; + +internal static partial class NexoKernelRegistrar +{ + private static void RegisterPhase01_ConfigurationNodeCapabilityRuntime(NexoKernelRegistrationContext ctx) + { + var services = ctx.Services; + var options = ctx.Options; + var modules = ctx.Modules; + var configuration = ctx.Configuration; + + // ── Configuration & Node Capability Runtime ──────────────────── + // Environment variables are the primary config source; appsettings + // is intentionally NOT loaded here so that containerised deployments + // stay 12-factor compliant. RemoteCapabilitiesOptions binds from + // the "Nexo:RemoteCapabilities" section for RunPod/cloud routing. + services.AddOptions() + .Bind(configuration.GetSection("Nexo:RemoteCapabilities")); + if (modules.IncludeNodeCapabilityRuntime) + { + services.AddRunPodCapabilityRouting(configuration); + NexoServiceCollectionExtensions.RegisterNodeCapabilityRuntime(services, configuration); + } + + } + + private static void RegisterPhase02_CQRSMediatRFluentValidation(NexoKernelRegistrationContext ctx) + { + var services = ctx.Services; + var options = ctx.Options; + var modules = ctx.Modules; + var configuration = ctx.Configuration; + + // ── CQRS (MediatR) & FluentValidation ───────────────────────── + // MediatR handlers from both the Analysis and Testing assemblies + // are registered in one pass. The ValidationBehavior pipeline + // behavior runs FluentValidation before each handler, so + // validators must also be registered here. + services.AddMediatR(cfg => + { + cfg.RegisterServicesFromAssembly(typeof(AnalyzeCodeCommand).Assembly); + cfg.RegisterServicesFromAssembly(typeof(RunTestsCommand).Assembly); + }); + + services.TryAddSingleton(); + + services.AddValidatorsFromAssembly(typeof(AnalyzeCodeValidator).Assembly); + services.AddTransient(typeof(MediatR.IPipelineBehavior<,>), typeof(Nexo.Core.Application.Behaviors.IngressLoggingPipelineBehavior<,>)); + services.AddTransient(typeof(MediatR.IPipelineBehavior<,>), typeof(Nexo.Core.Application.Behaviors.ValidationBehavior<,>)); + services.TryAddSingleton(); + + } + + private static void RegisterPhase03_ConfigurationServiceAdapter(NexoKernelRegistrationContext ctx) + { + var services = ctx.Services; + var options = ctx.Options; + var modules = ctx.Modules; + var configuration = ctx.Configuration; + + // ── Configuration service adapter ────────────────────────────── + // Bridges the domain-level IConfigurationService port to the + // infrastructure adapter. Strict mode controls whether config + // warnings escalate to hard failures (useful in CI pipelines). + services.AddSingleton(sp => + { + var logger = sp.GetRequiredService>(); + var strictMode = sp.GetService(); + return new Nexo.Infrastructure.Configuration.ConfigurationServiceAdapter(logger, strictMode?.ShouldFailOnConfigurationWarnings ?? false); + }); + + } + + private static void RegisterPhase04_LoopKernelDecoratorChain(NexoKernelRegistrationContext ctx) + { + var services = ctx.Services; + var options = ctx.Options; + var modules = ctx.Modules; + var configuration = ctx.Configuration; + + // ── Loop kernel (decorator chain) ────────────────────────────── + // The loop kernel runs brick-level iterations. It is composed via + // the decorator pattern: + // SequentialLoopKernel (always present — baseline) + // → ParallelLoopKernel (if NEXO_LOOP_PARALLEL=1) + // → InstrumentedLoopKernel (if NEXO_LOOP_INSTRUMENT=1) + // + // NEXO_LOOP_PARALLEL ("1"): wraps in a parallelising decorator for + // concurrent brick evaluation; useful on multi-core servers. + // NEXO_LOOP_INSTRUMENT ("1"): adds timing/counter telemetry around + // each loop iteration; adds overhead, meant for dev profiling. + services.AddSingleton(sp => + { + ILoopKernel k = new SequentialLoopKernel(); + var enableParallel = string.Equals(Environment.GetEnvironmentVariable("NEXO_LOOP_PARALLEL"), "1", StringComparison.OrdinalIgnoreCase); + if (enableParallel) + { + k = new ParallelLoopKernel(k); + } + + var instrument = string.Equals(Environment.GetEnvironmentVariable("NEXO_LOOP_INSTRUMENT"), "1", StringComparison.OrdinalIgnoreCase); + if (instrument) + { + k = new InstrumentedLoopKernel(k, sp.GetRequiredService>()); + } + + return k; + }); + + } + + private static void RegisterPhase05_OrchestrationTransport(NexoKernelRegistrationContext ctx) + { + var services = ctx.Services; + var options = ctx.Options; + var modules = ctx.Modules; + var configuration = ctx.Configuration; + + // ── Orchestration & transport ────────────────────────────────── + // Orchestration is always registered (it owns the runtime spec + // accessor used by the model decorator chain). Transport is + // optional: when present it registers gRPC channels plus the + // dual in-process / gRPC agent transport pair used for peer + // communication. See Nexo.Transport.Grpc for channel config. + services.AddNexoOrchestration(); + if (modules.IncludeRuntimeTransport) + { + services.AddOptions(); + services.AddOptions(); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.AddNexoRuntimeTransport(); + } + + } + + private static void RegisterPhase06_Persistence(NexoKernelRegistrationContext ctx) + { + var services = ctx.Services; + var options = ctx.Options; + var modules = ctx.Modules; + var configuration = ctx.Configuration; + + // ── Persistence ──────────────────────────────────────────────── + if (modules.IncludePersistence) + { + services.AddNexoPersistence(); + services.AddPostgresIsolatedDatabaseProvisioner(); + } + + } + + private static void RegisterPhase07_Adaptation(NexoKernelRegistrationContext ctx) + { + var services = ctx.Services; + var options = ctx.Options; + var modules = ctx.Modules; + var configuration = ctx.Configuration; + + // ── Adaptation ───────────────────────────────────────────────── + // Pattern store path is forwarded so the adaptation layer knows + // where to persist learned patterns on disk. + if (modules.IncludeAdaptation) + { + services.AddAdaptationInfrastructure(options.PatternStorePath); + } + + if (modules.IncludeAdaptation) + { + services.AddNexoFederatedBrickMesh(configuration); + } + + } + + private static void RegisterPhase08_CopilotTaskStore(NexoKernelRegistrationContext ctx) + { + var services = ctx.Services; + var options = ctx.Options; + var modules = ctx.Modules; + var configuration = ctx.Configuration; + + // ── Copilot task store ────────────────────────────────────────── + // LiteDB file is co-located with the pattern store directory + // (or the repo root as fallback) to keep all Nexo-generated + // state in one discoverable location. + var copilotTasksBasePath = !string.IsNullOrEmpty(options.PatternStorePath) + ? Path.GetDirectoryName(options.PatternStorePath) ?? "." + : RepoPathResolver.FindRepoRoot(); + var copilotTasksDbPath = Path.Combine(copilotTasksBasePath, "nexo-copilot-tasks.db"); + services.TryAddSingleton(_ => new LiteDbCopilotTaskStore(copilotTasksDbPath)); + + } + + private static void RegisterPhase09_KnowledgeQueryService(NexoKernelRegistrationContext ctx) + { + var services = ctx.Services; + var options = ctx.Options; + var modules = ctx.Modules; + var configuration = ctx.Configuration; + + // ── Knowledge query service ──────────────────────────────────── + // Aggregates adaptation logs, pattern store, and (optionally) + // user-knowledge logs into a single query façade. Falls back to + // an in-memory knowledge log when the trust module is absent. + services.TryAddSingleton(sp => + { + var adaptationLog = sp.GetRequiredService(); + var patternStore = sp.GetRequiredService(); + var userKnowledgeStore = sp.GetService() + ?? new Nexo.Infrastructure.Trust.InMemoryUserKnowledgeLogStore(); + return new KnowledgeQueryService(adaptationLog, patternStore, userKnowledgeStore); + }); + + } + + private static void RegisterPhase10_PipelineComposition(NexoKernelRegistrationContext ctx) + { + var services = ctx.Services; + var options = ctx.Options; + var modules = ctx.Modules; + var configuration = ctx.Configuration; + + // ── Pipeline composition ─────────────────────────────────────── + if (modules.IncludePipelineComposition) + { + services.AddPipelineCompositionLayer(); + } + + } + + private static void RegisterPhase11_BackgroundAgentsRAG(NexoKernelRegistrationContext ctx) + { + var services = ctx.Services; + var options = ctx.Options; + var modules = ctx.Modules; + var configuration = ctx.Configuration; + + // ── Background agents & RAG ──────────────────────────────────── + if (modules.IncludeBackgroundAgents) + { + services.AddBackgroundAgents(registerHostedService: options.RegisterBackgroundAgentHostedService); + } + + if (modules.IncludeBackgroundAgentRag) + { + services.AddBackgroundAgentsRAG(); + } + + } + + private static void RegisterPhase12_ObservationPipeline(NexoKernelRegistrationContext ctx) + { + var services = ctx.Services; + var options = ctx.Options; + var modules = ctx.Modules; + var configuration = ctx.Configuration; + + // ── Observation pipeline ─────────────────────────────────────── + // Captures runtime telemetry and persists it alongside patterns. + // NEXO_OBSERVATION_FAIL_OPEN ("1" / "true"): when set, store I/O + // errors are swallowed instead of failing the pipeline — safe + // for edge nodes with unreliable storage. + if (modules.IncludeObservationPipeline && !options.DisableObservationPipeline) + { + var repoRoot = RepoPathResolver.FindRepoRoot(); + var observationFailOpen = options.ObservationFailOpen ?? NexoServiceCollectionExtensions.ParseBooleanEnvironmentVariable("NEXO_OBSERVATION_FAIL_OPEN"); + services.AddObservationPipeline(opts => + { + opts.RepoRoot = repoRoot; + opts.StorePath = options.PatternStorePath ?? "nexo-patterns.db"; + opts.FailOpenOnStoreErrors = observationFailOpen; + }, registerHostedService: options.RegisterBackgroundAgentHostedService); + } + + // Mock web-search provider is registered as a fallback so + // background agents can be instantiated even when no real + // provider is configured. + if (modules.IncludeBackgroundAgents) + { + services.TryAddSingleton(); + } + + } + + private static void RegisterPhase13_ModelDecoratorChain(NexoKernelRegistrationContext ctx) + { + var services = ctx.Services; + var options = ctx.Options; + var modules = ctx.Modules; + var configuration = ctx.Configuration; + + // ── Model decorator chain ────────────────────────────────────── + // The IModel abstraction is built as a three-layer decorator: + // + // 1. ProviderBackedModel – delegates to IProviderFactory + // 2. HotSwappableModel – allows runtime model switching + // without restarting the host + // 3. OrchestrationRuntimeModelDecorator + // – injects orchestration-level + // spec overrides (temperature, + // token limits, etc.) per-call + // + // HotSwappableModel is registered as a concrete singleton so that + // administrative endpoints can resolve it directly for hot-swap + // operations, while IModel always returns the fully decorated chain. + services.AddSingleton(sp => + { + var providerFactory = sp.GetRequiredService(); + var providerBacked = new Nexo.Infrastructure.Execution.Models.ProviderBackedModel( + providerFactory, + sp.GetRequiredService>()); + return new Nexo.Infrastructure.Execution.Models.HotSwappableModel( + providerBacked, + sp.GetRequiredService>()); + }); + + services.AddSingleton(sp => + { + var accessor = sp.GetRequiredService(); + var inner = sp.GetRequiredService(); + return new Nexo.Orchestration.Models.OrchestrationRuntimeModelDecorator( + inner, + accessor, + sp.GetRequiredService>()); + }); + + } + + private static void RegisterPhase14_EphemeralLifecycle(NexoKernelRegistrationContext ctx) + { + var services = ctx.Services; + var options = ctx.Options; + var modules = ctx.Modules; + var configuration = ctx.Configuration; + + // ── Ephemeral lifecycle ──────────────────────────────────────── + // "Ephemeral" means Nexo can spin up and tear down backing + // resources on demand (Ollama models, Postgres databases). + // + // NEXO_EPHEMERAL ("1"): master switch — enables ALL ephemeral + // subsystems. + // NEXO_EPHEMERAL_MODELS ("1"): enables only ephemeral model + // lifecycle (Ollama pull/remove) without affecting databases. + // NEXO_EPHEMERAL_DB ("postgres"): enables ephemeral Postgres + // database creation; only takes effect when persistence is on. + if (EphemeralModelsEnabled()) + { + services.AddSingleton(); + } + + var ephemeralDb = Environment.GetEnvironmentVariable("NEXO_EPHEMERAL_DB")?.Trim(); + if (modules.IncludePersistence && string.Equals(ephemeralDb, "postgres", StringComparison.OrdinalIgnoreCase)) + { + services.AddSingleton(); + } + + } + + private static void RegisterPhase15_TrustProviderFactory3wayBranching(NexoKernelRegistrationContext ctx) + { + var services = ctx.Services; + var options = ctx.Options; + var modules = ctx.Modules; + var configuration = ctx.Configuration; + + // ── Trust & provider factory (3-way branching) ───────────────── + // The provider factory is the gateway through which every LLM + // call flows. Three mutually-exclusive wiring paths exist: + // + // Path A — Adaptive load-balancing (NEXO_LOAD_PREFERENCE set): + // ProviderFactory → (optional SanitizingProviderFactory if + // trust is on) → AdaptiveProviderFactory. + // Load policy is driven by NEXO_LOAD_PREFERENCE value. + // + // Path B — Trust without adaptive (NEXO_TRUST_ENABLED=1, + // no load pref): + // Trust module registers its own SanitizingProviderFactory + // via AddTrustServices (skipProviderRegistration: false). + // + // Path C — Plain (neither trust nor adaptive): + // Bare ProviderFactory is registered directly. + // + // NEXO_TRUST_ENABLED ("1"): activates the sanitization proxy + // that scrubs PII before LLM calls leave the trust boundary. + // NEXO_LOAD_PREFERENCE (string, e.g. "latency" / "cost"): + // activates adaptive load balancing and selects the policy. + var ephemeralModels = EphemeralModelsEnabled(); + var trustEnabledByConfig = options.TrustEnabled ?? string.Equals(Environment.GetEnvironmentVariable("NEXO_TRUST_ENABLED"), "1", StringComparison.OrdinalIgnoreCase); + var trustEnabled = modules.IncludeTrustServices && trustEnabledByConfig; + var loadPref = Environment.GetEnvironmentVariable("NEXO_LOAD_PREFERENCE")?.Trim(); + var useAdaptive = options.UseAdaptiveLoadBalancing ?? !string.IsNullOrEmpty(loadPref); + + if (modules.IncludeTrustServices) + { + services.AddTrustServices(useSanitizingProviderFactory: trustEnabled, ephemeralLifecycle: ephemeralModels, skipProviderRegistration: useAdaptive); + } + + // Path A: adaptive load-balancing wraps everything + if (useAdaptive) + { + services.AddSingleton(sp => + { + var logger = sp.GetRequiredService>(); + var lifecycle = sp.GetService(); + return new ProviderFactory(logger, lifecycle); + }); + services.TryAddSingleton(); + services.AddSingleton(sp => + { + var pf = sp.GetRequiredService(); + Nexo.Infrastructure.Execution.IProviderFactory inner = trustEnabled + ? new SanitizingProviderFactory(pf, sp.GetRequiredService(), + sp.GetRequiredService>()) + : pf; + var policy = sp.GetRequiredService(); + var logger = sp.GetService>(); + return new AdaptiveProviderFactory(inner, policy, logger); + }); + } + // Path C: plain provider (Path B is handled inside AddTrustServices) + else if (!trustEnabled) + { + services.AddSingleton(sp => + { + var logger = sp.GetRequiredService>(); + var lifecycle = sp.GetService(); + return new ProviderFactory(logger, lifecycle); + }); + } + + } + + private static void RegisterPhase16_ExecutionCoreWorkflow(NexoKernelRegistrationContext ctx) + { + var services = ctx.Services; + var options = ctx.Options; + var modules = ctx.Modules; + var configuration = ctx.Configuration; + + // ── Execution core & workflow ────────────────────────────────── + services.AddSingleton(); + services.AddMapDataProviderRouting(); + + // Workflow integrations (PDF export, webhooks, DB read/write, + // cluster store) are only available in Full/Server profiles. + // WorkflowExecutor resolves them as optional dependencies so it + // can still execute pure in-memory workflows without them. + if (modules.IncludeWorkflowIntegrations) + { + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + } + + // Semantic cache, behavior registry, step mode, and behavior + // executor form the brick execution pipeline. TryAddSingleton + // is used so that test hosts or SDK consumers can substitute + // any of these before calling AddNexo. + services.TryAddSingleton(sp => + new Nexo.Infrastructure.Execution.SemanticCache(sp.GetRequiredService>())); + services.TryAddSingleton(_ => + new Nexo.Infrastructure.Execution.BehaviorRegistry(Array.Empty())); + services.TryAddSingleton(sp => + new Nexo.Infrastructure.Execution.StepExecutionModeStore( + null, + sp.GetService>())); + services.TryAddSingleton(sp => + new Nexo.Infrastructure.Execution.BehaviorExecutor( + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService>(), + sp.GetService(), + sp.GetService(), + sp.GetService())); + // Agent registry is populated from SDK-provided AgentCards; if + // none are supplied the registry starts empty and agents can be + // registered later at runtime. + services.TryAddSingleton(sp => + { + var sdkOptions = sp.GetService(); + var cards = sdkOptions?.AgentCards?.ToList() ?? new List(); + return new Nexo.Infrastructure.Execution.AgentRegistry(cards); + }); + + } + + private static void RegisterPhase17_WorkflowExecutor(NexoKernelRegistrationContext ctx) + { + var services = ctx.Services; + var options = ctx.Options; + var modules = ctx.Modules; + var configuration = ctx.Configuration; + + // ── Workflow executor ────────────────────────────────────────── + // Scoped because a single workflow execution may accumulate + // state (e.g. cluster affinity) that should not leak across + // independent request scopes. + services.AddScoped(sp => + new Nexo.Core.Application.Workflows.WorkflowExecutor( + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService>(), + pdfExporter: sp.GetService(), + webhookClient: sp.GetService(), + databaseReader: sp.GetService(), + databaseWriter: sp.GetService(), + clusterStore: sp.GetService())); + + services.AddScoped(); + services.AddScoped(); + + } + + private static void RegisterPhase18_AnalysisValidation(NexoKernelRegistrationContext ctx) + { + var services = ctx.Services; + var options = ctx.Options; + var modules = ctx.Modules; + var configuration = ctx.Configuration; + + // ── Analysis & validation ────────────────────────────────────── + // Both services use a caching decorator (CachedAnalysis/ + // CachedValidation) to avoid re-running expensive analysis + // or parsing when the same input appears within a scope. + services.AddScoped(sp => + { + var inner = new Nexo.Infrastructure.Analysis.Adapters.AnalysisServiceAdapter( + sp.GetRequiredService>(), + sp.GetRequiredService()); + var cache = sp.GetRequiredService(); + var logger = sp.GetRequiredService>(); + return new Nexo.Infrastructure.Analysis.Adapters.CachedAnalysisServiceAdapter(inner, cache, logger); + }); + + services.AddScoped(sp => + { + var inner = new Nexo.Infrastructure.Validation.Adapters.ValidationServiceAdapter( + sp.GetRequiredService>(), + sp.GetRequiredService()); + var cache = sp.GetRequiredService(); + var logger = sp.GetRequiredService>(); + return new Nexo.Infrastructure.Validation.Adapters.CachedValidationServiceAdapter(inner, cache, logger); + }); + + services.AddSingleton(); + services.AddSingleton(); + } + + private static void RegisterPhase19_TestingAdapters(NexoKernelRegistrationContext ctx) + { + var services = ctx.Services; + var options = ctx.Options; + var modules = ctx.Modules; + var configuration = ctx.Configuration; + + // ── Testing adapters ─────────────────────────────────────────── + services.AddScoped(); + + // NEXO_EXECUTION_REMOTE_URL (URL string): when set, test + // execution is delegated to a remote execution service via + // HTTP instead of the local Docker-based platform. Useful in + // CI environments where Docker-in-Docker is unavailable. + if (modules.IncludeTestingAdapters) + { + var executionRemoteUrl = options.ExecutionRemoteUrl ?? Environment.GetEnvironmentVariable("NEXO_EXECUTION_REMOTE_URL")?.Trim(); + if (!string.IsNullOrEmpty(executionRemoteUrl)) + { + var baseUrl = executionRemoteUrl.TrimEnd('/') + "/"; + services.AddHttpClient("NexoExecution", c => c.BaseAddress = new Uri(baseUrl)); + services.AddSingleton(sp => + { + var factory = sp.GetRequiredService(); + var client = factory.CreateClient("NexoExecution"); + var logger = sp.GetService>(); + return new Nexo.Infrastructure.Testing.ExecutionPlatform.RemoteExecutionPlatform(client, logger); + }); + } + else + { + services.AddSingleton(sp => + new Nexo.Infrastructure.Testing.ExecutionPlatform.DockerExecutionPlatform(sp.GetRequiredService>())); + } + + services.AddSingleton(sp => + new Nexo.Infrastructure.Testing.Docker.DockerService(sp.GetRequiredService>())); + services.AddSingleton(sp => + new Nexo.Infrastructure.Testing.CodeAnalysis.RoslynCodeAnalysisService(sp.GetRequiredService>())); + services.AddArtifactCleanup(); + } + + } + + private static void RegisterPhase20_AnalysisRuleEngine(NexoKernelRegistrationContext ctx) + { + var services = ctx.Services; + var options = ctx.Options; + var modules = ctx.Modules; + var configuration = ctx.Configuration; + + // ── Analysis rule engine ─────────────────────────────────────── + // Rules are collected via DI multi-registration and fed into + // the engine. Add new IAnalysisRule implementations to extend + // the static analysis suite without touching this file. + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(sp => + { + var rules = sp.GetServices(); + var logger = sp.GetRequiredService>(); + return new Nexo.Infrastructure.Analysis.Rules.AnalysisRuleEngine(rules, logger); + }); + + services.AddNexoFleetDirector(); + services.AddNexoMeshElasticScheduling(configuration); + services.AddNexoMeshCheckpointScheduling(configuration); + + } + +} diff --git a/src/Nexo.Hosting/NexoKernelRegistrar.cs b/src/Nexo.Hosting/NexoKernelRegistrar.cs new file mode 100644 index 000000000..de0f52b6c --- /dev/null +++ b/src/Nexo.Hosting/NexoKernelRegistrar.cs @@ -0,0 +1,75 @@ +using FluentValidation; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Http; +using Nexo.Abstractions.Routing; +using Nexo.Abstractions.Transport; +using Nexo.BackgroundAgents; +using Nexo.BackgroundAgents.Trust; +using Nexo.Core.Application.Adaptation.Ports; +using Nexo.Core.Application.Analysis.UseCases.AnalyzeCode; +using Nexo.Core.Application.Common.Ports; +using Nexo.Core.Application.Common.Services; +using Nexo.Core.Application.Copilot.Ports; +using Nexo.Core.Application.Ephemeral.Ports; +using Nexo.Core.Application.Knowledge.Ports; +using Nexo.Core.Application.Observation.Ports; +using Nexo.Core.Application.Paths; +using Nexo.Core.Application.Testing.UseCases.RunTests; +using Nexo.Core.Application.Trust.Ports; +using Nexo.Core.Application.Validation.UseCases.RunValidation; +using Nexo.Infrastructure; +using Nexo.Infrastructure.Copilot; +using Nexo.Infrastructure.Execution; +using Nexo.Infrastructure.Execution.Ephemeral; +using Nexo.Infrastructure.Execution.LoadPolicy; +using Nexo.Infrastructure.Execution.Routing; +using Nexo.Infrastructure.Knowledge; +using Nexo.Infrastructure.Maintenance; +using Nexo.Infrastructure.ModelArtifacts; +using Nexo.Infrastructure.NodeCapabilityRuntime; +using Nexo.Infrastructure.Persistence; +using Nexo.Infrastructure.Persistence.Ephemeral; +using Nexo.Infrastructure.Pipelines; +using Nexo.Orchestration; +using Nexo.Orchestration.Models; +using Nexo.Orchestration.Transport; +using Nexo.Runtime; +using Nexo.Runtime.Routing; +using Nexo.Transport.Grpc; + +namespace Nexo.Hosting; + +/// Extracted kernel DI phases from . Registration order is preserved. +internal static partial class NexoKernelRegistrar +{ + public static void Register( + IServiceCollection services, + NexoHostingOptions options, + ModuleSelection modules, + IConfiguration configuration) + { + var ctx = new NexoKernelRegistrationContext(services, options, modules, configuration); + RegisterPhase01_ConfigurationNodeCapabilityRuntime(ctx); + RegisterPhase02_CQRSMediatRFluentValidation(ctx); + RegisterPhase03_ConfigurationServiceAdapter(ctx); + RegisterPhase04_LoopKernelDecoratorChain(ctx); + RegisterPhase05_OrchestrationTransport(ctx); + RegisterPhase06_Persistence(ctx); + RegisterPhase07_Adaptation(ctx); + RegisterPhase08_CopilotTaskStore(ctx); + RegisterPhase09_KnowledgeQueryService(ctx); + RegisterPhase10_PipelineComposition(ctx); + RegisterPhase11_BackgroundAgentsRAG(ctx); + RegisterPhase12_ObservationPipeline(ctx); + RegisterPhase13_ModelDecoratorChain(ctx); + RegisterPhase14_EphemeralLifecycle(ctx); + RegisterPhase15_TrustProviderFactory3wayBranching(ctx); + RegisterPhase16_ExecutionCoreWorkflow(ctx); + RegisterPhase17_WorkflowExecutor(ctx); + RegisterPhase18_AnalysisValidation(ctx); + RegisterPhase19_TestingAdapters(ctx); + RegisterPhase20_AnalysisRuleEngine(ctx); + } +} diff --git a/src/Nexo.Hosting/NexoKernelRegistrationModels.cs b/src/Nexo.Hosting/NexoKernelRegistrationModels.cs new file mode 100644 index 000000000..a93f3cc68 --- /dev/null +++ b/src/Nexo.Hosting/NexoKernelRegistrationModels.cs @@ -0,0 +1,28 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace Nexo.Hosting; + +/// +/// Flags produced by that decide which subsystem +/// modules are registered. +/// +internal sealed record ModuleSelection( + bool IncludeNodeCapabilityRuntime, + bool IncludeRuntimeTransport, + bool IncludePersistence, + bool IncludeAdaptation, + bool IncludePipelineComposition, + bool IncludeBackgroundAgents, + bool IncludeBackgroundAgentRag, + bool IncludeObservationPipeline, + bool IncludeTrustServices, + bool IncludeWorkflowIntegrations, + bool IncludeTestingAdapters); + +/// Tuple-style context passed sequentially through phase methods. +internal readonly record struct NexoKernelRegistrationContext( + IServiceCollection Services, + NexoHostingOptions Options, + ModuleSelection Modules, + IConfiguration Configuration); diff --git a/src/Nexo.Hosting/NexoServiceCollectionExtensions.Deployment.cs b/src/Nexo.Hosting/NexoServiceCollectionExtensions.Deployment.cs new file mode 100644 index 000000000..fe1763d9c --- /dev/null +++ b/src/Nexo.Hosting/NexoServiceCollectionExtensions.Deployment.cs @@ -0,0 +1,164 @@ +namespace Nexo.Hosting; + +public static partial class NexoServiceCollectionExtensions +{ + /// + /// Resolves the deployment profile from (in priority order): + /// 1. Explicit set by the caller. + /// 2. NEXO_DEPLOYMENT_PROFILE environment variable (case-insensitive; + /// accepts "full", "server", "edge", "airgapped"/"air-gapped", "system"/"core"). + /// 3. Falls back to . + /// + private static NexoDeploymentProfile ResolveDeploymentProfile(NexoHostingOptions options) + { + if (options.DeploymentProfile.HasValue) + { + return options.DeploymentProfile.Value; + } + + var raw = Environment.GetEnvironmentVariable("NEXO_DEPLOYMENT_PROFILE"); + if (string.IsNullOrWhiteSpace(raw)) + { + return NexoDeploymentProfile.Full; + } + + if (TryParseDeploymentProfile(raw, out var parsed)) + { + return parsed; + } + + throw new InvalidOperationException( + $"NEXO_DEPLOYMENT_PROFILE='{raw}' is not recognized. " + + "Valid values: full, server, edge, air-gapped, system."); + } + + private static bool TryParseDeploymentProfile(string? raw, out NexoDeploymentProfile profile) + { + profile = NexoDeploymentProfile.Full; + if (string.IsNullOrWhiteSpace(raw)) + { + return false; + } + + var normalized = raw.Trim().ToLowerInvariant(); + profile = normalized switch + { + "full" => NexoDeploymentProfile.Full, + "server" => NexoDeploymentProfile.Server, + "edge" => NexoDeploymentProfile.Edge, + "airgapped" => NexoDeploymentProfile.AirGapped, + "air-gapped" => NexoDeploymentProfile.AirGapped, + "system" => NexoDeploymentProfile.System, + "core" => NexoDeploymentProfile.System, + _ => profile + }; + + return normalized is "full" or "server" or "edge" or "airgapped" or "air-gapped" or "system" or "core"; + } + + /// + /// Maps a deployment profile to the set of subsystem modules that should + /// be registered. The peeling order (Full → Server → Edge → AirGapped + /// → System) progressively strips capabilities: + /// + /// Full — everything; used in development & CI. + /// Server — same as Full (reserved for future server-specific gating). + /// Edge — persistence + pipelines only; no NCR, no agents. + /// AirGapped— NCR + adaptation + persistence; no network transport. + /// System — bare minimum for CLI tooling; nothing optional. + /// + /// + private static ModuleSelection GetModuleSelection(NexoDeploymentProfile profile) + { + return profile switch + { + NexoDeploymentProfile.Full => new ModuleSelection( + IncludeNodeCapabilityRuntime: true, + IncludeRuntimeTransport: true, + IncludePersistence: true, + IncludeAdaptation: true, + IncludePipelineComposition: true, + IncludeBackgroundAgents: true, + IncludeBackgroundAgentRag: true, + IncludeObservationPipeline: true, + IncludeTrustServices: true, + IncludeWorkflowIntegrations: true, + IncludeTestingAdapters: true), + NexoDeploymentProfile.Server => new ModuleSelection( + IncludeNodeCapabilityRuntime: true, + IncludeRuntimeTransport: true, + IncludePersistence: true, + IncludeAdaptation: true, + IncludePipelineComposition: true, + IncludeBackgroundAgents: true, + IncludeBackgroundAgentRag: true, + IncludeObservationPipeline: true, + IncludeTrustServices: true, + IncludeWorkflowIntegrations: true, + IncludeTestingAdapters: true), + NexoDeploymentProfile.Edge => new ModuleSelection( + IncludeNodeCapabilityRuntime: false, + IncludeRuntimeTransport: false, + IncludePersistence: true, + IncludeAdaptation: false, + IncludePipelineComposition: true, + IncludeBackgroundAgents: false, + IncludeBackgroundAgentRag: false, + IncludeObservationPipeline: false, + IncludeTrustServices: false, + IncludeWorkflowIntegrations: false, + IncludeTestingAdapters: false), + NexoDeploymentProfile.AirGapped => new ModuleSelection( + IncludeNodeCapabilityRuntime: true, + IncludeRuntimeTransport: false, + IncludePersistence: true, + IncludeAdaptation: true, + IncludePipelineComposition: true, + IncludeBackgroundAgents: false, + IncludeBackgroundAgentRag: false, + IncludeObservationPipeline: false, + IncludeTrustServices: false, + IncludeWorkflowIntegrations: false, + IncludeTestingAdapters: false), + NexoDeploymentProfile.System => new ModuleSelection( + IncludeNodeCapabilityRuntime: false, + IncludeRuntimeTransport: false, + IncludePersistence: false, + IncludeAdaptation: false, + IncludePipelineComposition: false, + IncludeBackgroundAgents: false, + IncludeBackgroundAgentRag: false, + IncludeObservationPipeline: false, + IncludeTrustServices: false, + IncludeWorkflowIntegrations: false, + IncludeTestingAdapters: false), + _ => throw new ArgumentOutOfRangeException(nameof(profile), profile, "Unknown Nexo deployment profile.") + }; + } + + /// + /// Applies the NEXO_STRICT_MODE ("1" / "true") environment variable + /// when the caller has not already enabled strict mode programmatically. + /// Strict mode turns configuration warnings into hard failures — intended + /// for CI gates where misconfiguration should break the build. + /// + private static void ResolveStrictMode(NexoHostingOptions options) + { + if (!options.StrictMode.Enabled) + { + options.StrictMode.Enabled = ParseBooleanEnvironmentVariable("NEXO_STRICT_MODE"); + } + } + + internal static bool ParseBooleanEnvironmentVariable(string key) + { + var value = Environment.GetEnvironmentVariable(key); + if (string.IsNullOrWhiteSpace(value)) + { + return false; + } + + return string.Equals(value, "1", StringComparison.OrdinalIgnoreCase) || + string.Equals(value, "true", StringComparison.OrdinalIgnoreCase); + } +} diff --git a/src/Nexo.Hosting/NexoServiceCollectionExtensions.NodeCapabilityRuntime.cs b/src/Nexo.Hosting/NexoServiceCollectionExtensions.NodeCapabilityRuntime.cs new file mode 100644 index 000000000..d294db9b9 --- /dev/null +++ b/src/Nexo.Hosting/NexoServiceCollectionExtensions.NodeCapabilityRuntime.cs @@ -0,0 +1,53 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Nexo.Infrastructure.ModelArtifacts; +using Nexo.Infrastructure.NodeCapabilityRuntime; + +namespace Nexo.Hosting; + +public static partial class NexoServiceCollectionExtensions +{ + /// + /// Registers the platform-specific NCR (Node Capability Runtime) module. + /// NCR probes local hardware (GPU, RAM, accelerators) and exposes + /// capabilities used by ICapabilityRouter to decide + /// whether a job can run locally or must be routed to a peer/cloud. + /// Falls back to Linux when the OS is not recognised. + /// Also wires model artifact catalog sources used during NCR/model discovery. + /// Invoked from phase 01 when the deployment profile includes NCR. + /// + internal static void RegisterNodeCapabilityRuntime(IServiceCollection services, IConfiguration configuration) + { + services.AddNodeCapabilityRuntimeCore(configuration); + if (OperatingSystem.IsWindows()) + { + services.AddNodeCapabilityRuntimeWindows(configuration); + } + else if (OperatingSystem.IsMacOS()) + { + services.AddNodeCapabilityRuntimeMacOS(configuration); + } + else if (OperatingSystem.IsLinux()) + { + services.AddNodeCapabilityRuntimeLinux(configuration); + } + else if (OperatingSystem.IsIOS()) + { + services.AddNodeCapabilityRuntimeiOS(configuration); + } + else if (OperatingSystem.IsAndroid()) + { + services.AddNodeCapabilityRuntimeAndroid(configuration); + } + else + { + services.AddNodeCapabilityRuntimeLinux(configuration); + } + + services.AddModelArtifactCatalog(configuration); + if (OperatingSystem.IsWindows() || OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) + { + services.AddDockerOllamaModelArtifactCatalogSource(); + } + } +} diff --git a/src/Nexo.Hosting/NexoServiceCollectionExtensions.cs b/src/Nexo.Hosting/NexoServiceCollectionExtensions.cs index 05167edd9..523ec75d4 100644 --- a/src/Nexo.Hosting/NexoServiceCollectionExtensions.cs +++ b/src/Nexo.Hosting/NexoServiceCollectionExtensions.cs @@ -1,754 +1,124 @@ -using FluentValidation; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Http; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; -using Nexo.BackgroundAgents; -using Nexo.BackgroundAgents.Trust; -using Nexo.Core.Application.Adaptation.Ports; -using Nexo.Contracts; -using Nexo.Core.Application.Analysis.UseCases.AnalyzeCode; -using Nexo.Core.Application.Ephemeral.Ports; -using Nexo.Core.Application.Knowledge.Ports; -using Nexo.Core.Application.Observation.Ports; -using Nexo.Core.Application.Validation.UseCases.RunValidation; -using Nexo.Core.Application.Testing.UseCases.RunTests; -using Nexo.Core.Application.Common.Ports; -using Nexo.Core.Application.Common.Services; -using Nexo.Core.Application.Copilot.Ports; -using Nexo.Core.Application.Paths; -using Nexo.Core.Application.Trust.Ports; -using Nexo.Infrastructure; -using Nexo.Infrastructure.Environments; -using Nexo.Infrastructure.Execution; -using Nexo.Infrastructure.Execution.Routing; -using Nexo.Infrastructure.Execution.Ephemeral; -using Nexo.Infrastructure.Execution.LoadPolicy; -using Nexo.Infrastructure.Knowledge; -using Nexo.Infrastructure.Maintenance; -using Nexo.Infrastructure.NodeCapabilityRuntime; -using Nexo.Infrastructure.ModelArtifacts; -using Nexo.Infrastructure.Pipelines; -using Nexo.Infrastructure.Persistence.Ephemeral; -using Nexo.Infrastructure.Persistence; -using Nexo.Infrastructure.Copilot; -using Nexo.Infrastructure.Fleet; -using Nexo.Orchestration; -using Nexo.Orchestration.Models; -using Nexo.Abstractions.Routing; -using Nexo.Abstractions.Transport; -using Nexo.Orchestration.Transport; -using Nexo.Runtime; -using Nexo.Runtime.Routing; -using Nexo.Transport.Grpc; - -namespace Nexo.Hosting; - -/// -/// DI composition root for the Nexo kernel. This is the single place that wires every -/// subsystem together — orchestration, adaptation, persistence, trust, execution, etc. -/// -/// Architecture: The method follows a strict registration -/// order because later registrations depend on services registered earlier (e.g. the -/// model decorator chain wraps ProviderBackedModel → HotSwappableModel → -/// OrchestrationRuntimeModelDecorator, so the provider factory must already exist). -/// -/// -/// Deployment profiles: A (resolved from -/// NEXO_DEPLOYMENT_PROFILE or ) -/// controls which subsystem modules are included via . -/// Profiles range from Full (all modules) down to System (bare minimum -/// for CLI/headless tooling). -/// -/// -/// Related files: -/// — caller-facing option bag; -/// — deployment tier enum; -/// Nexo.Core.Domain.NexoDefaults — all tuneable default constants. -/// -/// -public static class NexoServiceCollectionExtensions -{ - /// - /// Flags produced by that decide which subsystem - /// modules are registered. Each flag maps 1-to-1 to a conditional block inside - /// . The mapping is intentionally explicit (no reflection) - /// so that trimming and ahead-of-time compilation remain safe. - /// - private sealed record ModuleSelection( - bool IncludeNodeCapabilityRuntime, - bool IncludeRuntimeTransport, - bool IncludePersistence, - bool IncludeAdaptation, - bool IncludePipelineComposition, - bool IncludeBackgroundAgents, - bool IncludeBackgroundAgentRag, - bool IncludeObservationPipeline, - bool IncludeTrustServices, - bool IncludeWorkflowIntegrations, - bool IncludeTestingAdapters); - - /// - /// Adds Nexo with an explicit deployment profile. - /// - /// The service collection. - /// Dependency profile to apply. - /// Optional additional options overrides. - /// The service collection for chaining. - public static IServiceCollection AddNexoProfile( - this IServiceCollection services, - NexoDeploymentProfile profile, - Action? configure = null) - { - return services.AddNexo(options => - { - options.DeploymentProfile = profile; - configure?.Invoke(options); - }); - } - - /// - /// Registers every Nexo subsystem into the DI container. The registration order - /// matters: downstream registrations (model decorator chain, workflow executor) - /// resolve services registered in earlier blocks. - /// - /// Environment variables read here (see inline comments for each): - /// NEXO_STRICT_MODE, NEXO_DEPLOYMENT_PROFILE, - /// NEXO_LOOP_PARALLEL, NEXO_LOOP_INSTRUMENT, - /// NEXO_OBSERVATION_FAIL_OPEN, NEXO_EPHEMERAL, - /// NEXO_EPHEMERAL_MODELS, NEXO_EPHEMERAL_DB, - /// NEXO_TRUST_ENABLED, NEXO_LOAD_PREFERENCE, - /// NEXO_EXECUTION_REMOTE_URL. - /// - /// - public static IServiceCollection AddNexo( - this IServiceCollection services, - Action? configure = null) - { - // ── Strict mode & deployment profile ─────────────────────────── - // Strict mode is resolved first because the configuration service - // adapter (registered below) reads it to decide whether config - // warnings should throw. Deployment profile gates every - // conditional module block that follows. - var options = new NexoHostingOptions(); - configure?.Invoke(options); - ResolveStrictMode(options); - var deploymentProfile = ResolveDeploymentProfile(options); - var modules = GetModuleSelection(deploymentProfile); - - services.AddSingleton(options.StrictMode); - - // ── Configuration & Node Capability Runtime ──────────────────── - // Environment variables are the primary config source; appsettings - // is intentionally NOT loaded here so that containerised deployments - // stay 12-factor compliant. RemoteCapabilitiesOptions binds from - // the "Nexo:RemoteCapabilities" section for RunPod/cloud routing. - services.AddHttpClient(); - var configuration = new ConfigurationBuilder() - .AddEnvironmentVariables() - .Build(); - services.AddOptions() - .Bind(configuration.GetSection("Nexo:RemoteCapabilities")); - if (modules.IncludeNodeCapabilityRuntime) - { - services.AddRunPodCapabilityRouting(configuration); - RegisterNodeCapabilityRuntime(services, configuration); - } - - // ── CQRS (MediatR) & FluentValidation ───────────────────────── - // MediatR handlers from both the Analysis and Testing assemblies - // are registered in one pass. The ValidationBehavior pipeline - // behavior runs FluentValidation before each handler, so - // validators must also be registered here. - services.AddMediatR(cfg => - { - cfg.RegisterServicesFromAssembly(typeof(AnalyzeCodeCommand).Assembly); - cfg.RegisterServicesFromAssembly(typeof(RunTestsCommand).Assembly); - }); - - services.TryAddSingleton(); - - services.AddValidatorsFromAssembly(typeof(AnalyzeCodeValidator).Assembly); - services.AddTransient(typeof(MediatR.IPipelineBehavior<,>), typeof(Nexo.Core.Application.Behaviors.IngressLoggingPipelineBehavior<,>)); - services.AddTransient(typeof(MediatR.IPipelineBehavior<,>), typeof(Nexo.Core.Application.Behaviors.ValidationBehavior<,>)); - services.TryAddSingleton(); - - // ── Configuration service adapter ────────────────────────────── - // Bridges the domain-level IConfigurationService port to the - // infrastructure adapter. Strict mode controls whether config - // warnings escalate to hard failures (useful in CI pipelines). - services.AddSingleton(sp => - { - var logger = sp.GetRequiredService>(); - var strictMode = sp.GetService(); - return new Nexo.Infrastructure.Configuration.ConfigurationServiceAdapter(logger, strictMode?.ShouldFailOnConfigurationWarnings ?? false); - }); - - // ── Loop kernel (decorator chain) ────────────────────────────── - // The loop kernel runs brick-level iterations. It is composed via - // the decorator pattern: - // SequentialLoopKernel (always present — baseline) - // → ParallelLoopKernel (if NEXO_LOOP_PARALLEL=1) - // → InstrumentedLoopKernel (if NEXO_LOOP_INSTRUMENT=1) - // - // NEXO_LOOP_PARALLEL ("1"): wraps in a parallelising decorator for - // concurrent brick evaluation; useful on multi-core servers. - // NEXO_LOOP_INSTRUMENT ("1"): adds timing/counter telemetry around - // each loop iteration; adds overhead, meant for dev profiling. - services.AddSingleton(sp => - { - ILoopKernel k = new SequentialLoopKernel(); - var enableParallel = string.Equals(Environment.GetEnvironmentVariable("NEXO_LOOP_PARALLEL"), "1", StringComparison.OrdinalIgnoreCase); - if (enableParallel) - k = new ParallelLoopKernel(k); - var instrument = string.Equals(Environment.GetEnvironmentVariable("NEXO_LOOP_INSTRUMENT"), "1", StringComparison.OrdinalIgnoreCase); - if (instrument) - k = new InstrumentedLoopKernel(k, sp.GetRequiredService>()); - return k; - }); - - // ── Orchestration & transport ────────────────────────────────── - // Orchestration is always registered (it owns the runtime spec - // accessor used by the model decorator chain). Transport is - // optional: when present it registers gRPC channels plus the - // dual in-process / gRPC agent transport pair used for peer - // communication. See Nexo.Transport.Grpc for channel config. - services.AddNexoOrchestration(); - if (modules.IncludeRuntimeTransport) - { - services.AddOptions(); - services.AddOptions(); - services.TryAddSingleton(); - services.TryAddSingleton(); - services.AddNexoRuntimeTransport(); - } - - // ── Persistence ──────────────────────────────────────────────── - if (modules.IncludePersistence) - { - services.AddNexoPersistence(); - services.AddPostgresIsolatedDatabaseProvisioner(); - } - - // ── Adaptation ───────────────────────────────────────────────── - // Pattern store path is forwarded so the adaptation layer knows - // where to persist learned patterns on disk. - if (modules.IncludeAdaptation) - { - services.AddAdaptationInfrastructure(options.PatternStorePath); - services.AddNexoMeshKnowledgeReplication(configuration); - } - - if (modules.IncludeAdaptation) - services.AddNexoFederatedBrickMesh(configuration); - - // ── Copilot task store ────────────────────────────────────────── - // LiteDB file is co-located with the pattern store directory - // (or the repo root as fallback) to keep all Nexo-generated - // state in one discoverable location. - var copilotTasksBasePath = !string.IsNullOrEmpty(options.PatternStorePath) - ? Path.GetDirectoryName(options.PatternStorePath) ?? "." - : RepoPathResolver.FindRepoRoot(); - var copilotTasksDbPath = Path.Combine(copilotTasksBasePath, "nexo-copilot-tasks.db"); - services.TryAddSingleton(_ => new LiteDbCopilotTaskStore(copilotTasksDbPath)); - - // ── Knowledge query service ──────────────────────────────────── - // Aggregates adaptation logs, pattern store, and (optionally) - // user-knowledge logs into a single query façade. Falls back to - // an in-memory knowledge log when the trust module is absent. - services.TryAddSingleton(sp => - { - var adaptationLog = sp.GetRequiredService(); - var patternStore = sp.GetRequiredService(); - var userKnowledgeStore = sp.GetService() - ?? new Nexo.Infrastructure.Trust.InMemoryUserKnowledgeLogStore(); - return new KnowledgeQueryService(adaptationLog, patternStore, userKnowledgeStore); - }); - - // ── Pipeline composition ─────────────────────────────────────── - if (modules.IncludePipelineComposition) - services.AddPipelineCompositionLayer(); - - // ── Background agents & RAG ──────────────────────────────────── - if (modules.IncludeBackgroundAgents) - services.AddBackgroundAgents(registerHostedService: options.RegisterBackgroundAgentHostedService); - - if (modules.IncludeBackgroundAgentRag) - services.AddBackgroundAgentsRAG(); - - // ── Observation pipeline ─────────────────────────────────────── - // Captures runtime telemetry and persists it alongside patterns. - // NEXO_OBSERVATION_FAIL_OPEN ("1" / "true"): when set, store I/O - // errors are swallowed instead of failing the pipeline — safe - // for edge nodes with unreliable storage. - if (modules.IncludeObservationPipeline && !options.DisableObservationPipeline) - { - var repoRoot = RepoPathResolver.FindRepoRoot(); - var observationFailOpen = options.ObservationFailOpen ?? ParseBooleanEnvironmentVariable("NEXO_OBSERVATION_FAIL_OPEN"); - services.AddObservationPipeline(opts => - { - opts.RepoRoot = repoRoot; - opts.StorePath = options.PatternStorePath ?? "nexo-patterns.db"; - opts.FailOpenOnStoreErrors = observationFailOpen; - }, registerHostedService: options.RegisterBackgroundAgentHostedService); - } - - // Mock web-search provider is registered as a fallback so - // background agents can be instantiated even when no real - // provider is configured. - if (modules.IncludeBackgroundAgents) - services.TryAddSingleton(); - - // ── Model decorator chain ────────────────────────────────────── - // The IModel abstraction is built as a three-layer decorator: - // - // 1. ProviderBackedModel – delegates to IProviderFactory - // 2. HotSwappableModel – allows runtime model switching - // without restarting the host - // 3. OrchestrationRuntimeModelDecorator - // – injects orchestration-level - // spec overrides (temperature, - // token limits, etc.) per-call - // - // HotSwappableModel is registered as a concrete singleton so that - // administrative endpoints can resolve it directly for hot-swap - // operations, while IModel always returns the fully decorated chain. - services.AddSingleton(sp => - { - var providerFactory = sp.GetRequiredService(); - var providerBacked = new Nexo.Infrastructure.Execution.Models.ProviderBackedModel( - providerFactory, - sp.GetRequiredService>()); - return new Nexo.Infrastructure.Execution.Models.HotSwappableModel( - providerBacked, - sp.GetRequiredService>()); - }); - - services.AddSingleton(sp => - { - var accessor = sp.GetRequiredService(); - var inner = sp.GetRequiredService(); - return new Nexo.Orchestration.Models.OrchestrationRuntimeModelDecorator( - inner, - accessor, - sp.GetRequiredService>()); - }); - - // ── Ephemeral lifecycle ──────────────────────────────────────── - // "Ephemeral" means Nexo can spin up and tear down backing - // resources on demand (Ollama models, Postgres databases). - // - // NEXO_EPHEMERAL ("1"): master switch — enables ALL ephemeral - // subsystems. - // NEXO_EPHEMERAL_MODELS ("1"): enables only ephemeral model - // lifecycle (Ollama pull/remove) without affecting databases. - // NEXO_EPHEMERAL_DB ("postgres"): enables ephemeral Postgres - // database creation; only takes effect when persistence is on. - var ephemeralAll = string.Equals(Environment.GetEnvironmentVariable("NEXO_EPHEMERAL"), "1", StringComparison.OrdinalIgnoreCase); - var ephemeralModels = ephemeralAll || string.Equals(Environment.GetEnvironmentVariable("NEXO_EPHEMERAL_MODELS"), "1", StringComparison.OrdinalIgnoreCase); - if (ephemeralModels) - services.AddSingleton(); - - var ephemeralDb = Environment.GetEnvironmentVariable("NEXO_EPHEMERAL_DB")?.Trim(); - if (modules.IncludePersistence && string.Equals(ephemeralDb, "postgres", StringComparison.OrdinalIgnoreCase)) - services.AddSingleton(); - - // ── Trust & provider factory (3-way branching) ───────────────── - // The provider factory is the gateway through which every LLM - // call flows. Three mutually-exclusive wiring paths exist: - // - // Path A — Adaptive load-balancing (NEXO_LOAD_PREFERENCE set): - // ProviderFactory → (optional SanitizingProviderFactory if - // trust is on) → AdaptiveProviderFactory. - // Load policy is driven by NEXO_LOAD_PREFERENCE value. - // - // Path B — Trust without adaptive (NEXO_TRUST_ENABLED=1, - // no load pref): - // Trust module registers its own SanitizingProviderFactory - // via AddTrustServices (skipProviderRegistration: false). - // - // Path C — Plain (neither trust nor adaptive): - // Bare ProviderFactory is registered directly. - // - // NEXO_TRUST_ENABLED ("1"): activates the sanitization proxy - // that scrubs PII before LLM calls leave the trust boundary. - // NEXO_LOAD_PREFERENCE (string, e.g. "latency" / "cost"): - // activates adaptive load balancing and selects the policy. - var trustEnabledByConfig = options.TrustEnabled ?? string.Equals(Environment.GetEnvironmentVariable("NEXO_TRUST_ENABLED"), "1", StringComparison.OrdinalIgnoreCase); - var trustEnabled = modules.IncludeTrustServices && trustEnabledByConfig; - var loadPref = Environment.GetEnvironmentVariable("NEXO_LOAD_PREFERENCE")?.Trim(); - var useAdaptive = options.UseAdaptiveLoadBalancing ?? !string.IsNullOrEmpty(loadPref); - - if (modules.IncludeTrustServices) - { - services.AddTrustServices(useSanitizingProviderFactory: trustEnabled, ephemeralLifecycle: ephemeralModels, skipProviderRegistration: useAdaptive); - } - - // Path A: adaptive load-balancing wraps everything - if (useAdaptive) - { - services.AddSingleton(sp => - { - var logger = sp.GetRequiredService>(); - var lifecycle = sp.GetService(); - return new ProviderFactory(logger, lifecycle); - }); - services.TryAddSingleton(); - services.AddSingleton(sp => - { - var pf = sp.GetRequiredService(); - Nexo.Infrastructure.Execution.IProviderFactory inner = trustEnabled - ? new SanitizingProviderFactory(pf, sp.GetRequiredService(), - sp.GetRequiredService>()) - : pf; - var policy = sp.GetRequiredService(); - var logger = sp.GetService>(); - return new AdaptiveProviderFactory(inner, policy, logger); - }); - } - // Path C: plain provider (Path B is handled inside AddTrustServices) - else if (!trustEnabled) - { - services.AddSingleton(sp => - { - var logger = sp.GetRequiredService>(); - var lifecycle = sp.GetService(); - return new ProviderFactory(logger, lifecycle); - }); - } - - // ── Execution core & workflow ────────────────────────────────── - services.AddSingleton(); - services.AddMapDataProviderRouting(); - - // Workflow integrations (PDF export, webhooks, DB read/write, - // cluster store) are only available in Full/Server profiles. - // WorkflowExecutor resolves them as optional dependencies so it - // can still execute pure in-memory workflows without them. - if (modules.IncludeWorkflowIntegrations) - { - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - } - - // Semantic cache, behavior registry, step mode, and behavior - // executor form the brick execution pipeline. TryAddSingleton - // is used so that test hosts or SDK consumers can substitute - // any of these before calling AddNexo. - services.TryAddSingleton(sp => - new Nexo.Infrastructure.Execution.SemanticCache(sp.GetRequiredService>())); - services.TryAddSingleton(_ => - new Nexo.Infrastructure.Execution.BehaviorRegistry(Array.Empty())); - services.TryAddSingleton(sp => - new Nexo.Infrastructure.Execution.StepExecutionModeStore( - null, - sp.GetService>())); - services.TryAddSingleton(sp => - new Nexo.Infrastructure.Execution.BehaviorExecutor( - sp.GetRequiredService(), - sp.GetRequiredService(), - sp.GetRequiredService(), - sp.GetRequiredService(), - sp.GetRequiredService>(), - sp.GetService(), - sp.GetService(), - sp.GetService())); - // Agent registry is populated from SDK-provided AgentCards; if - // none are supplied the registry starts empty and agents can be - // registered later at runtime. - services.TryAddSingleton(sp => - { - var sdkOptions = sp.GetService(); - var cards = sdkOptions?.AgentCards?.ToList() ?? new List(); - return new Nexo.Infrastructure.Execution.AgentRegistry(cards); - }); - - // ── Workflow executor ────────────────────────────────────────── - // Scoped because a single workflow execution may accumulate - // state (e.g. cluster affinity) that should not leak across - // independent request scopes. - services.AddScoped(sp => - new Nexo.Core.Application.Workflows.WorkflowExecutor( - sp.GetRequiredService(), - sp.GetRequiredService(), - sp.GetRequiredService(), - sp.GetRequiredService(), - sp.GetRequiredService(), - sp.GetRequiredService(), - sp.GetRequiredService>(), - pdfExporter: sp.GetService(), - webhookClient: sp.GetService(), - databaseReader: sp.GetService(), - databaseWriter: sp.GetService(), - clusterStore: sp.GetService())); - - services.AddScoped(); - services.AddScoped(); - - // ── Analysis & validation ────────────────────────────────────── - // Both services use a caching decorator (CachedAnalysis/ - // CachedValidation) to avoid re-running expensive analysis - // or parsing when the same input appears within a scope. - services.AddScoped(sp => - { - var inner = new Nexo.Infrastructure.Analysis.Adapters.AnalysisServiceAdapter( - sp.GetRequiredService>(), - sp.GetRequiredService()); - var cache = sp.GetRequiredService(); - var logger = sp.GetRequiredService>(); - return new Nexo.Infrastructure.Analysis.Adapters.CachedAnalysisServiceAdapter(inner, cache, logger); - }); - - services.AddScoped(sp => - { - var inner = new Nexo.Infrastructure.Validation.Adapters.ValidationServiceAdapter( - sp.GetRequiredService>(), - sp.GetRequiredService()); - var cache = sp.GetRequiredService(); - var logger = sp.GetRequiredService>(); - return new Nexo.Infrastructure.Validation.Adapters.CachedValidationServiceAdapter(inner, cache, logger); - }); - - services.AddSingleton(); - services.AddSingleton(); - // ── Testing adapters ─────────────────────────────────────────── - services.AddScoped(); - - // NEXO_EXECUTION_REMOTE_URL (URL string): when set, test - // execution is delegated to a remote execution service via - // HTTP instead of the local Docker-based platform. Useful in - // CI environments where Docker-in-Docker is unavailable. - if (modules.IncludeTestingAdapters) - { - var executionRemoteUrl = options.ExecutionRemoteUrl ?? Environment.GetEnvironmentVariable("NEXO_EXECUTION_REMOTE_URL")?.Trim(); - if (!string.IsNullOrEmpty(executionRemoteUrl)) - { - var baseUrl = executionRemoteUrl.TrimEnd('/') + "/"; - services.AddHttpClient("NexoExecution", c => c.BaseAddress = new Uri(baseUrl)); - services.AddSingleton(sp => - { - var factory = sp.GetRequiredService(); - var client = factory.CreateClient("NexoExecution"); - var logger = sp.GetService>(); - return new Nexo.Infrastructure.Testing.ExecutionPlatform.RemoteExecutionPlatform(client, logger); - }); - } - else - { - services.AddSingleton(sp => - new Nexo.Infrastructure.Testing.ExecutionPlatform.DockerExecutionPlatform(sp.GetRequiredService>())); - } - - services.AddSingleton(sp => - new Nexo.Infrastructure.Testing.Docker.DockerService(sp.GetRequiredService>())); - services.AddSingleton(sp => - new Nexo.Infrastructure.Testing.CodeAnalysis.RoslynCodeAnalysisService(sp.GetRequiredService>())); - services.AddArtifactCleanup(); - } - - // ── Analysis rule engine ─────────────────────────────────────── - // Rules are collected via DI multi-registration and fed into - // the engine. Add new IAnalysisRule implementations to extend - // the static analysis suite without touching this file. - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(sp => - { - var rules = sp.GetServices(); - var logger = sp.GetRequiredService>(); - return new Nexo.Infrastructure.Analysis.Rules.AnalysisRuleEngine(rules, logger); - }); - - // Phase 1 mesh director (in-memory fleet + task placement). See docs/MeshPhase0NorthStar.md. - services.AddNexoFleetDirector(); - services.AddNexoMeshElasticScheduling(configuration); - services.AddNexoMeshCheckpointScheduling(configuration); - - return services; - } - - /// - /// Registers the platform-specific NCR (Node Capability Runtime) module. - /// NCR probes local hardware (GPU, RAM, accelerators) and exposes - /// capabilities used by ICapabilityRouter to decide - /// whether a job can run locally or must be routed to a peer/cloud. - /// Falls back to Linux when the OS is not recognised. - /// - private static void RegisterNodeCapabilityRuntime(IServiceCollection services, IConfiguration configuration) - { - services.AddNodeCapabilityRuntimeCore(configuration); - if (OperatingSystem.IsWindows()) - services.AddNodeCapabilityRuntimeWindows(configuration); - else if (OperatingSystem.IsMacOS()) - services.AddNodeCapabilityRuntimeMacOS(configuration); - else if (OperatingSystem.IsLinux()) - services.AddNodeCapabilityRuntimeLinux(configuration); - else if (OperatingSystem.IsIOS()) - services.AddNodeCapabilityRuntimeiOS(configuration); - else if (OperatingSystem.IsAndroid()) - services.AddNodeCapabilityRuntimeAndroid(configuration); - else - services.AddNodeCapabilityRuntimeLinux(configuration); - - services.AddModelArtifactCatalog(configuration); - if (OperatingSystem.IsWindows() || OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) - { - services.AddDockerOllamaModelArtifactCatalogSource(); - } - } - - /// - /// Resolves the deployment profile from (in priority order): - /// 1. Explicit set by the caller. - /// 2. NEXO_DEPLOYMENT_PROFILE environment variable (case-insensitive; - /// accepts "full", "server", "edge", "airgapped"/"air-gapped", "system"/"core"). - /// 3. Falls back to . - /// - private static NexoDeploymentProfile ResolveDeploymentProfile(NexoHostingOptions options) - { - if (options.DeploymentProfile.HasValue) - return options.DeploymentProfile.Value; - - var raw = Environment.GetEnvironmentVariable("NEXO_DEPLOYMENT_PROFILE"); - if (string.IsNullOrWhiteSpace(raw)) - return NexoDeploymentProfile.Full; - - if (TryParseDeploymentProfile(raw, out var parsed)) - return parsed; - - throw new InvalidOperationException( - $"NEXO_DEPLOYMENT_PROFILE='{raw}' is not recognized. " + - "Valid values: full, server, edge, air-gapped, system."); - } - - private static bool TryParseDeploymentProfile(string? raw, out NexoDeploymentProfile profile) - { - profile = NexoDeploymentProfile.Full; - if (string.IsNullOrWhiteSpace(raw)) - return false; - - var normalized = raw.Trim().ToLowerInvariant(); - profile = normalized switch - { - "full" => NexoDeploymentProfile.Full, - "server" => NexoDeploymentProfile.Server, - "edge" => NexoDeploymentProfile.Edge, - "airgapped" => NexoDeploymentProfile.AirGapped, - "air-gapped" => NexoDeploymentProfile.AirGapped, - "system" => NexoDeploymentProfile.System, - "core" => NexoDeploymentProfile.System, - _ => profile - }; - - return normalized is "full" or "server" or "edge" or "airgapped" or "air-gapped" or "system" or "core"; - } - - /// - /// Maps a deployment profile to the set of subsystem modules that should - /// be registered. The peeling order (Full → Server → Edge → AirGapped - /// → System) progressively strips capabilities: - /// - /// Full — everything; used in development & CI. - /// Server — same as Full (reserved for future server-specific gating). - /// Edge — persistence + pipelines only; no NCR, no agents. - /// AirGapped— NCR + adaptation + persistence; no network transport. - /// System — bare minimum for CLI tooling; nothing optional. - /// - /// - private static ModuleSelection GetModuleSelection(NexoDeploymentProfile profile) - { - return profile switch - { - NexoDeploymentProfile.Full => new ModuleSelection( - IncludeNodeCapabilityRuntime: true, - IncludeRuntimeTransport: true, - IncludePersistence: true, - IncludeAdaptation: true, - IncludePipelineComposition: true, - IncludeBackgroundAgents: true, - IncludeBackgroundAgentRag: true, - IncludeObservationPipeline: true, - IncludeTrustServices: true, - IncludeWorkflowIntegrations: true, - IncludeTestingAdapters: true), - NexoDeploymentProfile.Server => new ModuleSelection( - IncludeNodeCapabilityRuntime: true, - IncludeRuntimeTransport: true, - IncludePersistence: true, - IncludeAdaptation: true, - IncludePipelineComposition: true, - IncludeBackgroundAgents: true, - IncludeBackgroundAgentRag: true, - IncludeObservationPipeline: true, - IncludeTrustServices: true, - IncludeWorkflowIntegrations: true, - IncludeTestingAdapters: true), - NexoDeploymentProfile.Edge => new ModuleSelection( - IncludeNodeCapabilityRuntime: false, - IncludeRuntimeTransport: false, - IncludePersistence: true, - IncludeAdaptation: false, - IncludePipelineComposition: true, - IncludeBackgroundAgents: false, - IncludeBackgroundAgentRag: false, - IncludeObservationPipeline: false, - IncludeTrustServices: false, - IncludeWorkflowIntegrations: false, - IncludeTestingAdapters: false), - NexoDeploymentProfile.AirGapped => new ModuleSelection( - IncludeNodeCapabilityRuntime: true, - IncludeRuntimeTransport: false, - IncludePersistence: true, - IncludeAdaptation: true, - IncludePipelineComposition: true, - IncludeBackgroundAgents: false, - IncludeBackgroundAgentRag: false, - IncludeObservationPipeline: false, - IncludeTrustServices: false, - IncludeWorkflowIntegrations: false, - IncludeTestingAdapters: false), - NexoDeploymentProfile.System => new ModuleSelection( - IncludeNodeCapabilityRuntime: false, - IncludeRuntimeTransport: false, - IncludePersistence: false, - IncludeAdaptation: false, - IncludePipelineComposition: false, - IncludeBackgroundAgents: false, - IncludeBackgroundAgentRag: false, - IncludeObservationPipeline: false, - IncludeTrustServices: false, - IncludeWorkflowIntegrations: false, - IncludeTestingAdapters: false), - _ => throw new ArgumentOutOfRangeException(nameof(profile), profile, "Unknown Nexo deployment profile.") - }; - } - - /// - /// Applies the NEXO_STRICT_MODE ("1" / "true") environment variable - /// when the caller has not already enabled strict mode programmatically. - /// Strict mode turns configuration warnings into hard failures — intended - /// for CI gates where misconfiguration should break the build. - /// - private static void ResolveStrictMode(NexoHostingOptions options) - { - if (!options.StrictMode.Enabled) - options.StrictMode.Enabled = ParseBooleanEnvironmentVariable("NEXO_STRICT_MODE"); - } - - private static bool ParseBooleanEnvironmentVariable(string key) - { - var value = Environment.GetEnvironmentVariable(key); - if (string.IsNullOrWhiteSpace(value)) - { - return false; - } - - return string.Equals(value, "1", StringComparison.OrdinalIgnoreCase) || - string.Equals(value, "true", StringComparison.OrdinalIgnoreCase); - } -} +using FluentValidation; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Http; +using Nexo.Abstractions.Routing; +using Nexo.Abstractions.Transport; +using Nexo.BackgroundAgents; +using Nexo.BackgroundAgents.Trust; +using Nexo.Core.Application.Adaptation.Ports; +using Nexo.Core.Application.Analysis.UseCases.AnalyzeCode; +using Nexo.Core.Application.Common.Ports; +using Nexo.Core.Application.Common.Services; +using Nexo.Core.Application.Copilot.Ports; +using Nexo.Core.Application.Ephemeral.Ports; +using Nexo.Core.Application.Knowledge.Ports; +using Nexo.Core.Application.Observation.Ports; +using Nexo.Core.Application.Paths; +using Nexo.Core.Application.Testing.UseCases.RunTests; +using Nexo.Core.Application.Trust.Ports; +using Nexo.Core.Application.Validation.UseCases.RunValidation; +using Nexo.Infrastructure; +using Nexo.Infrastructure.Copilot; +using Nexo.Infrastructure.Execution; +using Nexo.Infrastructure.Execution.Ephemeral; +using Nexo.Infrastructure.Execution.LoadPolicy; +using Nexo.Infrastructure.Execution.Routing; +using Nexo.Infrastructure.Knowledge; +using Nexo.Infrastructure.Maintenance; +using Nexo.Infrastructure.ModelArtifacts; +using Nexo.Infrastructure.NodeCapabilityRuntime; +using Nexo.Infrastructure.Persistence; +using Nexo.Infrastructure.Persistence.Ephemeral; +using Nexo.Infrastructure.Pipelines; +using Nexo.Orchestration; +using Nexo.Orchestration.Models; +using Nexo.Orchestration.Transport; +using Nexo.Runtime; +using Nexo.Runtime.Routing; +using Nexo.Transport.Grpc; + +namespace Nexo.Hosting; + +/// +/// DI composition root for the Nexo kernel. This is the single place that wires every +/// subsystem together — orchestration, adaptation, persistence, trust, execution, etc. +/// +/// Architecture: The method follows a strict registration +/// order because later registrations depend on services registered earlier (e.g. the +/// model decorator chain wraps ProviderBackedModel → HotSwappableModel → +/// OrchestrationRuntimeModelDecorator, so the provider factory must already exist). +/// +/// +/// Deployment profiles: A (resolved from +/// NEXO_DEPLOYMENT_PROFILE or ) +/// controls which subsystem modules are included via . +/// Profiles range from Full (all modules) down to System (bare minimum +/// for CLI/headless tooling). +/// +/// +/// Related files: +/// — caller-facing option bag; +/// — deployment tier enum; +/// Nexo.Core.Domain.NexoDefaults — all tuneable default constants. +/// +/// +public static partial class NexoServiceCollectionExtensions +{ + /// + /// Adds Nexo with an explicit deployment profile. + /// + /// The service collection. + /// Dependency profile to apply. + /// Optional additional options overrides. + /// The service collection for chaining. + public static IServiceCollection AddNexoProfile( + this IServiceCollection services, + NexoDeploymentProfile profile, + Action? configure = null) + { + return services.AddNexo(options => + { + options.DeploymentProfile = profile; + configure?.Invoke(options); + }); + } + + /// + /// Registers every Nexo subsystem into the DI container. The registration order + /// matters: downstream registrations (model decorator chain, workflow executor) + /// resolve services registered in earlier blocks. + /// + /// Environment variables read here (see inline comments for each): + /// NEXO_STRICT_MODE, NEXO_DEPLOYMENT_PROFILE, + /// NEXO_LOOP_PARALLEL, NEXO_LOOP_INSTRUMENT, + /// NEXO_OBSERVATION_FAIL_OPEN, NEXO_EPHEMERAL, + /// NEXO_EPHEMERAL_MODELS, NEXO_EPHEMERAL_DB, + /// NEXO_TRUST_ENABLED, NEXO_LOAD_PREFERENCE, + /// NEXO_EXECUTION_REMOTE_URL. + /// + /// + public static IServiceCollection AddNexo( + this IServiceCollection services, + Action? configure = null) + { + var options = new NexoHostingOptions(); + configure?.Invoke(options); + ResolveStrictMode(options); + var deploymentProfile = ResolveDeploymentProfile(options); + var modules = GetModuleSelection(deploymentProfile); + + services.AddSingleton(options.StrictMode); + + services.AddHttpClient(); + var configuration = new ConfigurationBuilder() + .AddEnvironmentVariables() + .Build(); + + NexoKernelRegistrar.Register(services, options, modules, configuration); + + return services; + } +} + diff --git a/src/Nexo.Hosting/Sdk/NexoSdkBuilder.cs b/src/Nexo.Hosting/Sdk/Builders/HostNexoSdkBuilder.cs similarity index 61% rename from src/Nexo.Hosting/Sdk/NexoSdkBuilder.cs rename to src/Nexo.Hosting/Sdk/Builders/HostNexoSdkBuilder.cs index 616170bb5..46fb71937 100644 --- a/src/Nexo.Hosting/Sdk/NexoSdkBuilder.cs +++ b/src/Nexo.Hosting/Sdk/Builders/HostNexoSdkBuilder.cs @@ -1,49 +1,69 @@ -using Microsoft.Extensions.DependencyInjection; -using Nexo.Abstractions; -using Nexo.Core.Application.Sdk.Ports; -using Nexo.Core.Domain.Agents; -using Nexo.Core.Domain.Bricks; - -namespace Nexo.Hosting.Sdk; - -/// -/// Implementation of INexoSdkBuilder. Configures NexoSdkOptions for runtime registration. -/// -public sealed class NexoSdkBuilder : INexoSdkBuilder -{ - private readonly NexoSdkOptions _options; - - /// - /// Creates a new SDK builder with the given options. - /// - /// Options to populate. - public NexoSdkBuilder(NexoSdkOptions options) - { - _options = options ?? throw new ArgumentNullException(nameof(options)); - } - - /// - public INexoSdkBuilder RegisterBrick() where T : Brick - { - _options.BrickTypes.Add(typeof(T)); - return this; - } - - /// - public INexoSdkBuilder RegisterAgent() where T : class - { - if (!typeof(IAgent).IsAssignableFrom(typeof(T))) - throw new ArgumentException($"Type {typeof(T).Name} must implement {nameof(IAgent)}", nameof(T)); - _options.AgentTypes.Add(typeof(T)); - return this; - } - - /// - public INexoSdkBuilder RegisterAgentCard(AgentCard card) - { - if (card == null) - throw new ArgumentNullException(nameof(card)); - _options.AgentCards.Add(card); - return this; - } -} +using Nexo.Abstractions; +using Nexo.Core.Domain.Agents; +using Nexo.Core.Domain.Bricks; +using Nexo.Infrastructure.Sdk.Ports; + +namespace Nexo.Hosting.Sdk; + +#pragma warning disable CS0618 // NexoSdkBuilder is an obsolete type forwarder in this file + +/// +/// Default implementation of . Configures for kernel registration. +/// +public class HostNexoSdkBuilder : INexoSdkBuilder +{ + private readonly NexoSdkOptions _options; + + /// + /// Creates a new SDK builder with the given options. + /// + /// Options to populate. + public HostNexoSdkBuilder(NexoSdkOptions options) + { + _options = options ?? throw new ArgumentNullException(nameof(options)); + } + + /// + public INexoSdkBuilder RegisterBrick() where T : Brick + { + _options.BrickTypes.Add(typeof(T)); + return this; + } + + /// + public INexoSdkBuilder RegisterAgent() where T : class + { + if (!typeof(IAgent).IsAssignableFrom(typeof(T))) + { + throw new ArgumentException($"Type {typeof(T).Name} must implement {nameof(IAgent)}", nameof(T)); + } + + _options.AgentTypes.Add(typeof(T)); + return this; + } + + /// + public INexoSdkBuilder RegisterAgentCard(AgentCard card) + { + if (card == null) + { + throw new ArgumentNullException(nameof(card)); + } + + _options.AgentCards.Add(card); + return this; + } +} + +/// +/// Back-compat type name for . +/// +[Obsolete("Renamed to HostNexoSdkBuilder.", error: false)] +public sealed class NexoSdkBuilder : HostNexoSdkBuilder +{ + /// + public NexoSdkBuilder(NexoSdkOptions options) + : base(options) + { + } +} diff --git a/src/Nexo.Hosting/Sdk/NexoSdkServiceCollectionExtensions.cs b/src/Nexo.Hosting/Sdk/Extensions/NexoSdkServiceCollectionExtensions.cs similarity index 89% rename from src/Nexo.Hosting/Sdk/NexoSdkServiceCollectionExtensions.cs rename to src/Nexo.Hosting/Sdk/Extensions/NexoSdkServiceCollectionExtensions.cs index cb76da6aa..d99263bb4 100644 --- a/src/Nexo.Hosting/Sdk/NexoSdkServiceCollectionExtensions.cs +++ b/src/Nexo.Hosting/Sdk/Extensions/NexoSdkServiceCollectionExtensions.cs @@ -1,45 +1,49 @@ -using Microsoft.Extensions.DependencyInjection; -using Nexo.Abstractions; -using Nexo.Core.Application.Sdk.Ports; -using Nexo.Infrastructure.Adaptation; - -namespace Nexo.Hosting.Sdk; - -/// -/// Extension methods for SDK-based component registration. -/// Call AddNexoSdk before AddNexo to register external bricks and agents at runtime. -/// -public static class NexoSdkServiceCollectionExtensions -{ - /// - /// Configures the Nexo SDK builder for runtime registration of bricks and agents. - /// Call before AddNexo(). Example: - /// - /// services.AddNexoSdk(sdk => sdk - /// .RegisterBrick<MyBrick>() - /// .RegisterAgent<MyAgent>() - /// .RegisterAgentCard(myCard)); - /// services.AddNexo(); - /// - /// - /// The service collection. - /// Action to configure the SDK builder. - /// The service collection for chaining. - public static IServiceCollection AddNexoSdk( - this IServiceCollection services, - Action configure) - { - var options = new NexoSdkOptions(); - configure(new NexoSdkBuilder(options)); - - services.AddSingleton(options); - - if (options.BrickTypes.Count > 0) - services.Configure(o => o.AdditionalBrickTypes.AddRange(options.BrickTypes)); - - foreach (var agentType in options.AgentTypes) - services.AddSingleton(typeof(IAgent), agentType); - - return services; - } -} +using Microsoft.Extensions.DependencyInjection; +using Nexo.Abstractions; +using Nexo.Infrastructure.Adaptation; +using Nexo.Infrastructure.Sdk.Ports; + +namespace Nexo.Hosting.Sdk; + +/// +/// Extension methods for SDK-based component registration. +/// Call AddNexoSdk before AddNexo to register external bricks and agents at runtime. +/// +public static class NexoSdkServiceCollectionExtensions +{ + /// + /// Configures the Nexo SDK builder for runtime registration of bricks and agents. + /// Call before AddNexo(). Example: + /// + /// services.AddNexoSdk(sdk => sdk + /// .RegisterBrick<MyBrick>() + /// .RegisterAgent<MyAgent>() + /// .RegisterAgentCard(myCard)); + /// services.AddNexo(); + /// + /// + /// The service collection. + /// Action to configure the SDK builder. + /// The service collection for chaining. + public static IServiceCollection AddNexoSdk( + this IServiceCollection services, + Action configure) + { + var options = new NexoSdkOptions(); + configure(new HostNexoSdkBuilder(options)); + + services.AddSingleton(options); + + if (options.BrickTypes.Count > 0) + { + services.Configure(o => o.AdditionalBrickTypes.AddRange(options.BrickTypes)); + } + + foreach (var agentType in options.AgentTypes) + { + services.AddSingleton(typeof(IAgent), agentType); + } + + return services; + } +} diff --git a/src/Nexo.Hosting/OpenTelemetryServiceCollectionExtensions.cs b/src/Nexo.Hosting/Sdk/Extensions/OpenTelemetryServiceCollectionExtensions.cs similarity index 97% rename from src/Nexo.Hosting/OpenTelemetryServiceCollectionExtensions.cs rename to src/Nexo.Hosting/Sdk/Extensions/OpenTelemetryServiceCollectionExtensions.cs index b5f6d1633..08145e60c 100644 --- a/src/Nexo.Hosting/OpenTelemetryServiceCollectionExtensions.cs +++ b/src/Nexo.Hosting/Sdk/Extensions/OpenTelemetryServiceCollectionExtensions.cs @@ -1,39 +1,39 @@ -using Microsoft.Extensions.DependencyInjection; -using Nexo.Core.Application.Common.Ports; -using Nexo.Infrastructure.Metrics; -using OpenTelemetry.Metrics; - -namespace Nexo.Hosting; - -/// -/// OpenTelemetry integration for Nexo. Call AddNexoOpenTelemetry() after AddNexo() to enable -/// metrics export via OpenTelemetry (OTLP, Console, etc.). -/// -public static class OpenTelemetryServiceCollectionExtensions -{ - /// - /// Adds OpenTelemetry metrics for Nexo and replaces IMetricsCollector with OpenTelemetryMetricsCollector. - /// Call after AddNexo(). Optionally configure the MeterProviderBuilder (e.g. AddConsoleExporter, AddOtlpExporter). - /// - /// The service collection (after AddNexo). - /// Optional action to configure the MeterProviderBuilder. - /// The service collection for chaining. - public static IServiceCollection AddNexoOpenTelemetry( - this IServiceCollection services, - Action? configure = null) - { - services.AddOpenTelemetry() - .WithMetrics(m => - { - m.AddMeter(OpenTelemetryMetricsCollector.MeterName); - configure?.Invoke(m); - }); - - // Replace default MemoryMetricsCollector with OpenTelemetryMetricsCollector (last registration wins) - services.AddSingleton(sp => - new OpenTelemetryMetricsCollector( - sp.GetService>())); - - return services; - } -} +using Microsoft.Extensions.DependencyInjection; +using Nexo.Core.Application.Common.Ports; +using Nexo.Infrastructure.Metrics; +using OpenTelemetry.Metrics; + +namespace Nexo.Hosting; + +/// +/// OpenTelemetry integration for Nexo. Call AddNexoOpenTelemetry() after AddNexo() to enable +/// metrics export via OpenTelemetry (OTLP, Console, etc.). +/// +public static class OpenTelemetryServiceCollectionExtensions +{ + /// + /// Adds OpenTelemetry metrics for Nexo and replaces IMetricsCollector with OpenTelemetryMetricsCollector. + /// Call after AddNexo(). Optionally configure the MeterProviderBuilder (e.g. AddConsoleExporter, AddOtlpExporter). + /// + /// The service collection (after AddNexo). + /// Optional action to configure the MeterProviderBuilder. + /// The service collection for chaining. + public static IServiceCollection AddNexoOpenTelemetry( + this IServiceCollection services, + Action? configure = null) + { + services.AddOpenTelemetry() + .WithMetrics(m => + { + m.AddMeter(OpenTelemetryMetricsCollector.MeterName); + configure?.Invoke(m); + }); + + // Replace default MemoryMetricsCollector with OpenTelemetryMetricsCollector (last registration wins) + services.AddSingleton(sp => + new OpenTelemetryMetricsCollector( + sp.GetService>())); + + return services; + } +} diff --git a/src/Nexo.Hosting/NexoDeploymentProfile.cs b/src/Nexo.Hosting/Sdk/Options/NexoDeploymentProfile.cs similarity index 97% rename from src/Nexo.Hosting/NexoDeploymentProfile.cs rename to src/Nexo.Hosting/Sdk/Options/NexoDeploymentProfile.cs index 46d486c90..ce592b2a7 100644 --- a/src/Nexo.Hosting/NexoDeploymentProfile.cs +++ b/src/Nexo.Hosting/Sdk/Options/NexoDeploymentProfile.cs @@ -1,54 +1,54 @@ -namespace Nexo.Hosting; - -/// -/// Deployment profiles that control which optional Nexo modules are registered -/// into the DI container. Each profile maps to a ModuleSelection record -/// inside that toggles 11 boolean -/// module flags. Profiles are resolved via -/// NexoHostingOptions.DeploymentProfile or the NEXO_DEPLOYMENT_PROFILE -/// environment variable, defaulting to . -/// -public enum NexoDeploymentProfile -{ - /// - /// Registers every module: node capability runtime, gRPC transport, - /// persistence, adaptation, pipeline composition, background agents (+ RAG), - /// observation pipeline, trust services, workflow integrations, and testing - /// adapters. Use for development or a fully-featured server deployment. - /// - Full = 0, - - /// - /// Currently identical to — all modules are included. - /// Exists as a semantic alias for long-running server hosts, allowing the - /// Full profile to diverge later (e.g. adding dev-only tooling) - /// without breaking server deployments. - /// - Server = 1, - - /// - /// Lightweight profile for resource-constrained or edge devices. - /// Includes: persistence, pipeline composition. - /// Excludes: node capability runtime, runtime transport, adaptation, - /// background agents, background-agent RAG, observation pipeline, trust - /// services, workflow integrations, testing adapters. - /// - Edge = 2, - - /// - /// Profile for environments with no external network access. - /// Includes: node capability runtime, persistence, adaptation, - /// pipeline composition. - /// Excludes: runtime transport (no gRPC egress), background agents, - /// background-agent RAG, observation pipeline, trust services (no remote - /// attestation), workflow integrations, testing adapters. - /// - AirGapped = 3, - - /// - /// Bare-minimum profile: only core system/kernel services, no optional - /// modules at all. Every module flag is false. Useful for CLI - /// tooling, unit tests, or hosts that register integrations manually. - /// - System = 4 -} +namespace Nexo.Hosting; + +/// +/// Deployment profiles that control which optional Nexo modules are registered +/// into the DI container. Each profile maps to a ModuleSelection record +/// inside that toggles 11 boolean +/// module flags. Profiles are resolved via +/// NexoHostingOptions.DeploymentProfile or the NEXO_DEPLOYMENT_PROFILE +/// environment variable, defaulting to . +/// +public enum NexoDeploymentProfile +{ + /// + /// Registers every module: node capability runtime, gRPC transport, + /// persistence, adaptation, pipeline composition, background agents (+ RAG), + /// observation pipeline, trust services, workflow integrations, and testing + /// adapters. Use for development or a fully-featured server deployment. + /// + Full = 0, + + /// + /// Currently identical to — all modules are included. + /// Exists as a semantic alias for long-running server hosts, allowing the + /// Full profile to diverge later (e.g. adding dev-only tooling) + /// without breaking server deployments. + /// + Server = 1, + + /// + /// Lightweight profile for resource-constrained or edge devices. + /// Includes: persistence, pipeline composition. + /// Excludes: node capability runtime, runtime transport, adaptation, + /// background agents, background-agent RAG, observation pipeline, trust + /// services, workflow integrations, testing adapters. + /// + Edge = 2, + + /// + /// Profile for environments with no external network access. + /// Includes: node capability runtime, persistence, adaptation, + /// pipeline composition. + /// Excludes: runtime transport (no gRPC egress), background agents, + /// background-agent RAG, observation pipeline, trust services (no remote + /// attestation), workflow integrations, testing adapters. + /// + AirGapped = 3, + + /// + /// Bare-minimum profile: only core system/kernel services, no optional + /// modules at all. Every module flag is false. Useful for CLI + /// tooling, unit tests, or hosts that register integrations manually. + /// + System = 4 +} diff --git a/src/Nexo.Hosting/NexoHostingOptions.cs b/src/Nexo.Hosting/Sdk/Options/NexoHostingOptions.cs similarity index 97% rename from src/Nexo.Hosting/NexoHostingOptions.cs rename to src/Nexo.Hosting/Sdk/Options/NexoHostingOptions.cs index b9db728d5..a60ccadc8 100644 --- a/src/Nexo.Hosting/NexoHostingOptions.cs +++ b/src/Nexo.Hosting/Sdk/Options/NexoHostingOptions.cs @@ -1,79 +1,79 @@ -namespace Nexo.Hosting; - -/// -/// Options for configuring the Nexo kernel when using AddNexo(). -/// -public sealed class NexoHostingOptions -{ - /// - /// Optional dependency profile that controls which non-core modules are registered. - /// Defaults to unless overridden by - /// NEXO_DEPLOYMENT_PROFILE. - /// - public NexoDeploymentProfile? DeploymentProfile { get; set; } - - /// - /// Path to the configuration file (default: ~/.nexo/config.json). - /// - public string? ConfigPath { get; set; } - - /// - /// Path to the pattern store for observation/adaptation (optional). - /// When set, enables observation context and pattern-based adaptation. - /// - public string? PatternStorePath { get; set; } - - /// - /// When true, enables Trust & Information Architecture (sanitization, audit). - /// Default: from NEXO_TRUST_ENABLED env var, or false. - /// - public bool? TrustEnabled { get; set; } - - /// - /// When true, registers background agents as IHostedService (for long-running hosts). - /// Default: false (CLI mode; agents run on-demand). - /// - public bool RegisterBackgroundAgentHostedService { get; set; } - - /// - /// When true, disables the observation pipeline (pattern store, event sources, ObservationPipelineService). - /// Default: false — observation pipeline is registered by default. Set true for lightweight/CLI-only hosts. - /// - public bool DisableObservationPipeline { get; set; } - - /// - /// When true, observation pipeline store errors are fail-open (logged and skipped) instead of stopping the host. - /// Default: null (resolved from NEXO_OBSERVATION_FAIL_OPEN env var, then false). - /// - public bool? ObservationFailOpen { get; set; } - - /// - /// Base URL for the Nexo API when used as a remote client (e.g. for mobile thin client). - /// - public string? ApiBaseUrl { get; set; } - - /// - /// Optional API key for authentication when connecting to Nexo API. - /// - public string? ApiKey { get; set; } - - /// - /// When true, enables adaptive load balancing (edge/server routing via NEXO_LOAD_PREFERENCE). - /// Default: true when NEXO_LOAD_PREFERENCE is set (edge|server|auto). - /// - public bool? UseAdaptiveLoadBalancing { get; set; } - - /// - /// When set, IExecutionPlatform uses RemoteExecutionPlatform delegating to this Nexo API URL. - /// Default: from NEXO_EXECUTION_REMOTE_URL. Use when Docker is not available (e.g. mobile, CI). - /// - public string? ExecutionRemoteUrl { get; set; } - - /// - /// Strict mode configuration. When enabled, the system fails fast with verbose - /// diagnostics during development. Set NEXO_STRICT_MODE=1 or configure - /// individual sub-flags. Flip to permissive (disabled) for production. - /// Default: resolved from NEXO_STRICT_MODE env var, or disabled. - /// - public StrictModeOptions StrictMode { get; set; } = new(); -} +namespace Nexo.Hosting; + +/// +/// Options for configuring the Nexo kernel when using AddNexo(). +/// +public sealed class NexoHostingOptions +{ + /// + /// Optional dependency profile that controls which non-core modules are registered. + /// Defaults to unless overridden by + /// NEXO_DEPLOYMENT_PROFILE. + /// + public NexoDeploymentProfile? DeploymentProfile { get; set; } + + /// + /// Path to the configuration file (default: ~/.nexo/config.json). + /// + public string? ConfigPath { get; set; } + + /// + /// Path to the pattern store for observation/adaptation (optional). + /// When set, enables observation context and pattern-based adaptation. + /// + public string? PatternStorePath { get; set; } + + /// + /// When true, enables Trust & Information Architecture (sanitization, audit). + /// Default: from NEXO_TRUST_ENABLED env var, or false. + /// + public bool? TrustEnabled { get; set; } + + /// + /// When true, registers background agents as IHostedService (for long-running hosts). + /// Default: false (CLI mode; agents run on-demand). + /// + public bool RegisterBackgroundAgentHostedService { get; set; } + + /// + /// When true, disables the observation pipeline (pattern store, event sources, ObservationPipelineService). + /// Default: false — observation pipeline is registered by default. Set true for lightweight/CLI-only hosts. + /// + public bool DisableObservationPipeline { get; set; } + + /// + /// When true, observation pipeline store errors are fail-open (logged and skipped) instead of stopping the host. + /// Default: null (resolved from NEXO_OBSERVATION_FAIL_OPEN env var, then false). + /// + public bool? ObservationFailOpen { get; set; } + + /// + /// Base URL for the Nexo API when used as a remote client (e.g. for mobile thin client). + /// + public string? ApiBaseUrl { get; set; } + + /// + /// Optional API key for authentication when connecting to Nexo API. + /// + public string? ApiKey { get; set; } + + /// + /// When true, enables adaptive load balancing (edge/server routing via NEXO_LOAD_PREFERENCE). + /// Default: true when NEXO_LOAD_PREFERENCE is set (edge|server|auto). + /// + public bool? UseAdaptiveLoadBalancing { get; set; } + + /// + /// When set, IExecutionPlatform uses RemoteExecutionPlatform delegating to this Nexo API URL. + /// Default: from NEXO_EXECUTION_REMOTE_URL. Use when Docker is not available (e.g. mobile, CI). + /// + public string? ExecutionRemoteUrl { get; set; } + + /// + /// Strict mode configuration. When enabled, the system fails fast with verbose + /// diagnostics during development. Set NEXO_STRICT_MODE=1 or configure + /// individual sub-flags. Flip to permissive (disabled) for production. + /// Default: resolved from NEXO_STRICT_MODE env var, or disabled. + /// + public StrictModeOptions StrictMode { get; set; } = new(); +} diff --git a/src/Nexo.Hosting/Sdk/NexoSdkOptions.cs b/src/Nexo.Hosting/Sdk/Options/NexoSdkOptions.cs similarity index 96% rename from src/Nexo.Hosting/Sdk/NexoSdkOptions.cs rename to src/Nexo.Hosting/Sdk/Options/NexoSdkOptions.cs index 7de0784f0..df612c07a 100644 --- a/src/Nexo.Hosting/Sdk/NexoSdkOptions.cs +++ b/src/Nexo.Hosting/Sdk/Options/NexoSdkOptions.cs @@ -1,24 +1,24 @@ -using Nexo.Core.Domain.Agents; - -namespace Nexo.Hosting.Sdk; - -/// -/// Options for SDK-registered components. Populated by INexoSdkBuilder. -/// -public sealed class NexoSdkOptions -{ - /// - /// Brick types to register in the adaptation pipeline. - /// - public List BrickTypes { get; } = new(); - - /// - /// Agent types (IAgent implementations) to register in DI. - /// - public List AgentTypes { get; } = new(); - - /// - /// Agent cards to register for workflow execution. - /// - public List AgentCards { get; } = new(); -} +using Nexo.Core.Domain.Agents; + +namespace Nexo.Hosting.Sdk; + +/// +/// Options for SDK-registered components. Populated by INexoSdkBuilder. +/// +public sealed class NexoSdkOptions +{ + /// + /// Brick types to register in the adaptation pipeline. + /// + public List BrickTypes { get; } = new(); + + /// + /// Agent types (IAgent implementations) to register in DI. + /// + public List AgentTypes { get; } = new(); + + /// + /// Agent cards to register for workflow execution. + /// + public List AgentCards { get; } = new(); +} diff --git a/src/Nexo.Hosting/StrictModeOptions.cs b/src/Nexo.Hosting/Sdk/Options/StrictModeOptions.cs similarity index 97% rename from src/Nexo.Hosting/StrictModeOptions.cs rename to src/Nexo.Hosting/Sdk/Options/StrictModeOptions.cs index 01b2f656d..838ef14ae 100644 --- a/src/Nexo.Hosting/StrictModeOptions.cs +++ b/src/Nexo.Hosting/Sdk/Options/StrictModeOptions.cs @@ -1,69 +1,69 @@ -namespace Nexo.Hosting; - -/// -/// Controls Nexo strict mode behavior. When enabled, the system fails fast with -/// verbose diagnostics — ideal for development and CI. Flip to permissive once -/// confident in the agentic layer for production deployments. -/// -/// Environment variable: NEXO_STRICT_MODE (1 / true to enable). -/// -public sealed class StrictModeOptions -{ - /// - /// Configuration section name for binding from appsettings.json. - /// - public const string SectionName = "Nexo:StrictMode"; - - /// - /// Master switch. When true, all strict-mode sub-flags default to their - /// fail-fast values unless explicitly overridden. - /// - public bool Enabled { get; set; } - - /// - /// Throw on the first validation failure instead of collecting all failures. - /// Default: matches . - /// - public bool? FailFastOnValidationErrors { get; set; } - - /// - /// Throw when a provider is misconfigured rather than falling back silently. - /// Default: matches . - /// - public bool? FailFastOnProviderErrors { get; set; } - - /// - /// Throw when pipeline stages encounter transient errors instead of retrying. - /// Default: matches . - /// - public bool? FailFastOnPipelineErrors { get; set; } - - /// - /// Emit verbose diagnostics (debug-level logging, detailed error messages). - /// Default: matches . - /// - public bool? VerboseDiagnostics { get; set; } - - /// - /// Treat configuration warnings (e.g. missing optional config, fallback to defaults) - /// as hard errors. Default: matches . - /// - public bool? FailOnConfigurationWarnings { get; set; } - - // ── Resolved properties ────────────────────────────────────────── - - /// Resolved value: fail fast on validation errors. - public bool ShouldFailFastOnValidation => FailFastOnValidationErrors ?? Enabled; - - /// Resolved value: fail fast on provider errors. - public bool ShouldFailFastOnProvider => FailFastOnProviderErrors ?? Enabled; - - /// Resolved value: fail fast on pipeline errors. - public bool ShouldFailFastOnPipeline => FailFastOnPipelineErrors ?? Enabled; - - /// Resolved value: emit verbose diagnostics. - public bool ShouldEmitVerboseDiagnostics => VerboseDiagnostics ?? Enabled; - - /// Resolved value: treat config warnings as errors. - public bool ShouldFailOnConfigurationWarnings => FailOnConfigurationWarnings ?? Enabled; -} +namespace Nexo.Hosting; + +/// +/// Controls Nexo strict mode behavior. When enabled, the system fails fast with +/// verbose diagnostics — ideal for development and CI. Flip to permissive once +/// confident in the agentic layer for production deployments. +/// +/// Environment variable: NEXO_STRICT_MODE (1 / true to enable). +/// +public sealed class StrictModeOptions +{ + /// + /// Configuration section name for binding from appsettings.json. + /// + public const string SectionName = "Nexo:StrictMode"; + + /// + /// Master switch. When true, all strict-mode sub-flags default to their + /// fail-fast values unless explicitly overridden. + /// + public bool Enabled { get; set; } + + /// + /// Throw on the first validation failure instead of collecting all failures. + /// Default: matches . + /// + public bool? FailFastOnValidationErrors { get; set; } + + /// + /// Throw when a provider is misconfigured rather than falling back silently. + /// Default: matches . + /// + public bool? FailFastOnProviderErrors { get; set; } + + /// + /// Throw when pipeline stages encounter transient errors instead of retrying. + /// Default: matches . + /// + public bool? FailFastOnPipelineErrors { get; set; } + + /// + /// Emit verbose diagnostics (debug-level logging, detailed error messages). + /// Default: matches . + /// + public bool? VerboseDiagnostics { get; set; } + + /// + /// Treat configuration warnings (e.g. missing optional config, fallback to defaults) + /// as hard errors. Default: matches . + /// + public bool? FailOnConfigurationWarnings { get; set; } + + // ── Resolved properties ────────────────────────────────────────── + + /// Resolved value: fail fast on validation errors. + public bool ShouldFailFastOnValidation => FailFastOnValidationErrors ?? Enabled; + + /// Resolved value: fail fast on provider errors. + public bool ShouldFailFastOnProvider => FailFastOnProviderErrors ?? Enabled; + + /// Resolved value: fail fast on pipeline errors. + public bool ShouldFailFastOnPipeline => FailFastOnPipelineErrors ?? Enabled; + + /// Resolved value: emit verbose diagnostics. + public bool ShouldEmitVerboseDiagnostics => VerboseDiagnostics ?? Enabled; + + /// Resolved value: treat config warnings as errors. + public bool ShouldFailOnConfigurationWarnings => FailOnConfigurationWarnings ?? Enabled; +} diff --git a/src/Nexo.Infrastructure/Adaptation/AdaptationServiceCollectionExtensions.cs b/src/Nexo.Infrastructure/Adaptation/Sdk/Extensions/AdaptationServiceCollectionExtensions.cs similarity index 97% rename from src/Nexo.Infrastructure/Adaptation/AdaptationServiceCollectionExtensions.cs rename to src/Nexo.Infrastructure/Adaptation/Sdk/Extensions/AdaptationServiceCollectionExtensions.cs index 2e32af7cc..c418365e5 100644 --- a/src/Nexo.Infrastructure/Adaptation/AdaptationServiceCollectionExtensions.cs +++ b/src/Nexo.Infrastructure/Adaptation/Sdk/Extensions/AdaptationServiceCollectionExtensions.cs @@ -9,9 +9,11 @@ using Nexo.Infrastructure.Adaptation; using Nexo.Infrastructure.Execution; using Nexo.Infrastructure.Observation; +using Nexo.Infrastructure.Sdk.Observation; +using Nexo.Infrastructure.Sdk.Rollback; using Nexo.Infrastructure.Rollback; -namespace Nexo.Infrastructure; +namespace Nexo.Infrastructure.Sdk.Adaptation; /// /// DI extension methods for the adaptation engine (Block 3 + Block 4). diff --git a/src/Nexo.Infrastructure/Adaptation/SharedAdaptationServiceCollectionExtensions.cs b/src/Nexo.Infrastructure/Adaptation/Sdk/Extensions/SharedAdaptationServiceCollectionExtensions.cs similarity index 97% rename from src/Nexo.Infrastructure/Adaptation/SharedAdaptationServiceCollectionExtensions.cs rename to src/Nexo.Infrastructure/Adaptation/Sdk/Extensions/SharedAdaptationServiceCollectionExtensions.cs index 401ea7c3b..2d528e332 100644 --- a/src/Nexo.Infrastructure/Adaptation/SharedAdaptationServiceCollectionExtensions.cs +++ b/src/Nexo.Infrastructure/Adaptation/Sdk/Extensions/SharedAdaptationServiceCollectionExtensions.cs @@ -3,8 +3,9 @@ using Nexo.Core.Application.Adaptation.Ports; using Nexo.Core.Application.Analysis.Ports; using Nexo.Infrastructure.Analysis; +using Nexo.Infrastructure.Adaptation; -namespace Nexo.Infrastructure.Adaptation; +namespace Nexo.Infrastructure.Sdk.Adaptation; /// /// DI extensions for P2.3 shared adaptation cache. diff --git a/src/Nexo.Infrastructure/Analysis/BrickAnalyzer/CodeAnalyzerServiceCollectionExtensions.cs b/src/Nexo.Infrastructure/Analysis/BrickAnalyzer/Sdk/Extensions/CodeAnalyzerServiceCollectionExtensions.cs similarity index 95% rename from src/Nexo.Infrastructure/Analysis/BrickAnalyzer/CodeAnalyzerServiceCollectionExtensions.cs rename to src/Nexo.Infrastructure/Analysis/BrickAnalyzer/Sdk/Extensions/CodeAnalyzerServiceCollectionExtensions.cs index c273dbc9f..9aca8c802 100644 --- a/src/Nexo.Infrastructure/Analysis/BrickAnalyzer/CodeAnalyzerServiceCollectionExtensions.cs +++ b/src/Nexo.Infrastructure/Analysis/BrickAnalyzer/Sdk/Extensions/CodeAnalyzerServiceCollectionExtensions.cs @@ -2,7 +2,7 @@ using Nexo.Core.Application.Analysis.Ports; using Nexo.Infrastructure.Analysis.BrickAnalyzer; -namespace Nexo.Infrastructure.Analysis; +namespace Nexo.Infrastructure.Sdk.Analysis; /// /// DI registration for Block 2 code analyzers. diff --git a/src/Nexo.Infrastructure/Composition/CompositionServiceCollectionExtensions.cs b/src/Nexo.Infrastructure/Composition/Sdk/Extensions/CompositionServiceCollectionExtensions.cs similarity index 96% rename from src/Nexo.Infrastructure/Composition/CompositionServiceCollectionExtensions.cs rename to src/Nexo.Infrastructure/Composition/Sdk/Extensions/CompositionServiceCollectionExtensions.cs index 5991ed96a..02b059889 100644 --- a/src/Nexo.Infrastructure/Composition/CompositionServiceCollectionExtensions.cs +++ b/src/Nexo.Infrastructure/Composition/Sdk/Extensions/CompositionServiceCollectionExtensions.cs @@ -3,7 +3,7 @@ using Nexo.Core.Application.ParallelTesting.Ports; using Nexo.Infrastructure.Composition; -namespace Nexo.Infrastructure; +namespace Nexo.Infrastructure.Sdk.Composition; /// /// DI extensions for Block 7 composition. diff --git a/src/Nexo.Infrastructure/Execution/Routing/RunPodCapabilityRoutingServiceCollectionExtensions.cs b/src/Nexo.Infrastructure/Execution/Routing/Sdk/Extensions/RunPodCapabilityRoutingServiceCollectionExtensions.cs similarity index 98% rename from src/Nexo.Infrastructure/Execution/Routing/RunPodCapabilityRoutingServiceCollectionExtensions.cs rename to src/Nexo.Infrastructure/Execution/Routing/Sdk/Extensions/RunPodCapabilityRoutingServiceCollectionExtensions.cs index a8996a0cd..0c6ddf08d 100644 --- a/src/Nexo.Infrastructure/Execution/Routing/RunPodCapabilityRoutingServiceCollectionExtensions.cs +++ b/src/Nexo.Infrastructure/Execution/Routing/Sdk/Extensions/RunPodCapabilityRoutingServiceCollectionExtensions.cs @@ -11,7 +11,7 @@ using Nexo.Infrastructure.Adaptation; using Nexo.Infrastructure.Mesh; -namespace Nexo.Infrastructure.Execution.Routing; +namespace Nexo.Infrastructure.Execution.Routing.Sdk; /// /// Registers RunPod + NCR capability-routing execution components. diff --git a/src/Nexo.Infrastructure/Execution/BrickHostServiceCollectionExtensions.cs b/src/Nexo.Infrastructure/Execution/Sdk/Extensions/BrickHostServiceCollectionExtensions.cs similarity index 91% rename from src/Nexo.Infrastructure/Execution/BrickHostServiceCollectionExtensions.cs rename to src/Nexo.Infrastructure/Execution/Sdk/Extensions/BrickHostServiceCollectionExtensions.cs index b82b085a9..866e07793 100644 --- a/src/Nexo.Infrastructure/Execution/BrickHostServiceCollectionExtensions.cs +++ b/src/Nexo.Infrastructure/Execution/Sdk/Extensions/BrickHostServiceCollectionExtensions.cs @@ -1,7 +1,8 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; +using Nexo.Infrastructure.Execution; -namespace Nexo.Infrastructure.Execution; +namespace Nexo.Infrastructure.Execution.Sdk; /// /// DI extensions for brick host (options and optional remote catalogs). diff --git a/src/Nexo.Infrastructure/Execution/NexoFederatedBrickMeshServiceCollectionExtensions.cs b/src/Nexo.Infrastructure/Execution/Sdk/Extensions/NexoFederatedBrickMeshServiceCollectionExtensions.cs similarity index 98% rename from src/Nexo.Infrastructure/Execution/NexoFederatedBrickMeshServiceCollectionExtensions.cs rename to src/Nexo.Infrastructure/Execution/Sdk/Extensions/NexoFederatedBrickMeshServiceCollectionExtensions.cs index 17fac4ca2..4fb0779da 100644 --- a/src/Nexo.Infrastructure/Execution/NexoFederatedBrickMeshServiceCollectionExtensions.cs +++ b/src/Nexo.Infrastructure/Execution/Sdk/Extensions/NexoFederatedBrickMeshServiceCollectionExtensions.cs @@ -4,7 +4,7 @@ using Microsoft.Extensions.Options; using Nexo.Core.Domain.Execution; -namespace Nexo.Infrastructure.Execution; +namespace Nexo.Infrastructure.Execution.Sdk; /// /// Optional wiring so each Nexo host resolves bricks from peers over HTTP diff --git a/src/Nexo.Infrastructure/Maintenance/ServiceCollectionExtensions.cs b/src/Nexo.Infrastructure/Maintenance/Sdk/Extensions/ServiceCollectionExtensions.cs similarity index 96% rename from src/Nexo.Infrastructure/Maintenance/ServiceCollectionExtensions.cs rename to src/Nexo.Infrastructure/Maintenance/Sdk/Extensions/ServiceCollectionExtensions.cs index b50d4bcbd..3987fe430 100644 --- a/src/Nexo.Infrastructure/Maintenance/ServiceCollectionExtensions.cs +++ b/src/Nexo.Infrastructure/Maintenance/Sdk/Extensions/ServiceCollectionExtensions.cs @@ -5,8 +5,9 @@ using Nexo.Infrastructure.Maintenance.Adapters; using Nexo.Infrastructure.Maintenance.Ports; using Nexo.Infrastructure.Maintenance.Strategies; +using Nexo.Infrastructure.Maintenance; -namespace Nexo.Infrastructure.Maintenance; +namespace Nexo.Infrastructure.Sdk.Maintenance; /// /// DI extensions for artifact cleanup services. diff --git a/src/Nexo.Infrastructure/Mesh/MeshServiceCollectionExtensions.cs b/src/Nexo.Infrastructure/Mesh/Sdk/Extensions/MeshServiceCollectionExtensions.cs similarity index 98% rename from src/Nexo.Infrastructure/Mesh/MeshServiceCollectionExtensions.cs rename to src/Nexo.Infrastructure/Mesh/Sdk/Extensions/MeshServiceCollectionExtensions.cs index eeec1537d..919a6166a 100644 --- a/src/Nexo.Infrastructure/Mesh/MeshServiceCollectionExtensions.cs +++ b/src/Nexo.Infrastructure/Mesh/Sdk/Extensions/MeshServiceCollectionExtensions.cs @@ -5,7 +5,7 @@ using Nexo.Core.Application.Mesh.Ports; using Nexo.Infrastructure.Mesh; -namespace Nexo.Infrastructure; +namespace Nexo.Infrastructure.Mesh.Sdk; /// /// DI extensions for Block 9 mesh. diff --git a/src/Nexo.Infrastructure/ModelArtifacts/ModelArtifactCatalogServiceCollectionExtensions.cs b/src/Nexo.Infrastructure/ModelArtifacts/Sdk/Extensions/ModelArtifactCatalogServiceCollectionExtensions.cs similarity index 96% rename from src/Nexo.Infrastructure/ModelArtifacts/ModelArtifactCatalogServiceCollectionExtensions.cs rename to src/Nexo.Infrastructure/ModelArtifacts/Sdk/Extensions/ModelArtifactCatalogServiceCollectionExtensions.cs index 31fa4e212..19e6504ac 100644 --- a/src/Nexo.Infrastructure/ModelArtifacts/ModelArtifactCatalogServiceCollectionExtensions.cs +++ b/src/Nexo.Infrastructure/ModelArtifacts/Sdk/Extensions/ModelArtifactCatalogServiceCollectionExtensions.cs @@ -3,9 +3,10 @@ using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; using Nexo.Core.Application.ModelArtifacts.Ports; +using Nexo.Infrastructure.ModelArtifacts; using Nexo.Infrastructure.NodeCapabilityRuntime.Backends; -namespace Nexo.Infrastructure.ModelArtifacts; +namespace Nexo.Infrastructure.Sdk.ModelArtifacts; public static class ModelArtifactCatalogServiceCollectionExtensions { @@ -43,7 +44,7 @@ public static IServiceCollection AddModelArtifactCatalog( { var opts = sp.GetRequiredService>().CurrentValue; var baseUrl = string.IsNullOrWhiteSpace(opts.BaseUrl) ? "https://ollama.com" : opts.BaseUrl.Trim(); - client.BaseAddress = new Uri(baseUrl.TrimEnd('/') + "/", UriKind.Absolute); + client.BaseAddress = new Uri(baseUrl.TrimEnd('/') + "/", global::System.UriKind.Absolute); client.Timeout = opts.RequestTimeout <= TimeSpan.Zero ? TimeSpan.FromSeconds(60) : opts.RequestTimeout; }); diff --git a/src/Nexo.Infrastructure/NodeCapabilityRuntime/NodeCapabilityRuntimeServiceCollectionExtensions.cs b/src/Nexo.Infrastructure/NodeCapabilityRuntime/Sdk/Extensions/NodeCapabilityRuntimeServiceCollectionExtensions.cs similarity index 98% rename from src/Nexo.Infrastructure/NodeCapabilityRuntime/NodeCapabilityRuntimeServiceCollectionExtensions.cs rename to src/Nexo.Infrastructure/NodeCapabilityRuntime/Sdk/Extensions/NodeCapabilityRuntimeServiceCollectionExtensions.cs index 93b8fbc76..5695fd431 100644 --- a/src/Nexo.Infrastructure/NodeCapabilityRuntime/NodeCapabilityRuntimeServiceCollectionExtensions.cs +++ b/src/Nexo.Infrastructure/NodeCapabilityRuntime/Sdk/Extensions/NodeCapabilityRuntimeServiceCollectionExtensions.cs @@ -11,7 +11,7 @@ using Nexo.Infrastructure.NodeCapabilityRuntime.Profiles; using Nexo.Infrastructure.NodeCapabilityRuntime.Scoring; -namespace Nexo.Infrastructure.NodeCapabilityRuntime; +namespace Nexo.Infrastructure.NodeCapabilityRuntime.Sdk; /// /// DI registration helpers for NCR core and per-platform policy bindings. diff --git a/src/Nexo.Infrastructure/Observation/ObservationServiceCollectionExtensions.cs b/src/Nexo.Infrastructure/Observation/Sdk/Extensions/ObservationServiceCollectionExtensions.cs similarity index 93% rename from src/Nexo.Infrastructure/Observation/ObservationServiceCollectionExtensions.cs rename to src/Nexo.Infrastructure/Observation/Sdk/Extensions/ObservationServiceCollectionExtensions.cs index c0d599610..1c3453a5f 100644 --- a/src/Nexo.Infrastructure/Observation/ObservationServiceCollectionExtensions.cs +++ b/src/Nexo.Infrastructure/Observation/Sdk/Extensions/ObservationServiceCollectionExtensions.cs @@ -1,7 +1,8 @@ using Microsoft.Extensions.DependencyInjection; using Nexo.Core.Application.Observation.Ports; +using Nexo.Infrastructure.Observation; -namespace Nexo.Infrastructure.Observation; +namespace Nexo.Infrastructure.Sdk.Observation; /// /// DI extension methods for the observation pipeline (Infrastructure layer). diff --git a/src/Nexo.Infrastructure/ParallelTesting/ParallelTestingServiceCollectionExtensions.cs b/src/Nexo.Infrastructure/ParallelTesting/Sdk/Extensions/ParallelTestingServiceCollectionExtensions.cs similarity index 94% rename from src/Nexo.Infrastructure/ParallelTesting/ParallelTestingServiceCollectionExtensions.cs rename to src/Nexo.Infrastructure/ParallelTesting/Sdk/Extensions/ParallelTestingServiceCollectionExtensions.cs index 33651c4d0..7f42cd783 100644 --- a/src/Nexo.Infrastructure/ParallelTesting/ParallelTestingServiceCollectionExtensions.cs +++ b/src/Nexo.Infrastructure/ParallelTesting/Sdk/Extensions/ParallelTestingServiceCollectionExtensions.cs @@ -2,7 +2,7 @@ using Nexo.Core.Application.ParallelTesting.Ports; using Nexo.Infrastructure.ParallelTesting; -namespace Nexo.Infrastructure; +namespace Nexo.Infrastructure.Sdk.ParallelTesting; /// /// DI extensions for Block 8 parallel testing. diff --git a/src/Nexo.Infrastructure/Persistence/DatabaseServiceCollectionExtensions.cs b/src/Nexo.Infrastructure/Persistence/Sdk/Extensions/DatabaseServiceCollectionExtensions.cs similarity index 88% rename from src/Nexo.Infrastructure/Persistence/DatabaseServiceCollectionExtensions.cs rename to src/Nexo.Infrastructure/Persistence/Sdk/Extensions/DatabaseServiceCollectionExtensions.cs index d2603783c..a894d29fa 100644 --- a/src/Nexo.Infrastructure/Persistence/DatabaseServiceCollectionExtensions.cs +++ b/src/Nexo.Infrastructure/Persistence/Sdk/Extensions/DatabaseServiceCollectionExtensions.cs @@ -1,8 +1,9 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Nexo.Abstractions.Database; +using Nexo.Infrastructure.Persistence; -namespace Nexo.Infrastructure.Persistence; +namespace Nexo.Infrastructure.Sdk.Persistence; /// /// DI registration for isolated database provisioning. diff --git a/src/Nexo.Infrastructure/Persistence/PersistenceServiceCollectionExtensions.cs b/src/Nexo.Infrastructure/Persistence/Sdk/Extensions/PersistenceServiceCollectionExtensions.cs similarity index 91% rename from src/Nexo.Infrastructure/Persistence/PersistenceServiceCollectionExtensions.cs rename to src/Nexo.Infrastructure/Persistence/Sdk/Extensions/PersistenceServiceCollectionExtensions.cs index 426b5502b..d5c407298 100644 --- a/src/Nexo.Infrastructure/Persistence/PersistenceServiceCollectionExtensions.cs +++ b/src/Nexo.Infrastructure/Persistence/Sdk/Extensions/PersistenceServiceCollectionExtensions.cs @@ -1,7 +1,8 @@ using Microsoft.Extensions.DependencyInjection; using Nexo.Core.Application.Persistence.Ports; +using Nexo.Infrastructure.Persistence; -namespace Nexo.Infrastructure.Persistence; +namespace Nexo.Infrastructure.Sdk.Persistence; /// /// DI registration for Nexo persistence. Use in-memory by default; replace with diff --git a/src/Nexo.Infrastructure/Pipelines/PipelineServiceCollectionExtensions.cs b/src/Nexo.Infrastructure/Pipelines/Sdk/Extensions/PipelineServiceCollectionExtensions.cs similarity index 98% rename from src/Nexo.Infrastructure/Pipelines/PipelineServiceCollectionExtensions.cs rename to src/Nexo.Infrastructure/Pipelines/Sdk/Extensions/PipelineServiceCollectionExtensions.cs index fea15c2d4..04d8221e7 100644 --- a/src/Nexo.Infrastructure/Pipelines/PipelineServiceCollectionExtensions.cs +++ b/src/Nexo.Infrastructure/Pipelines/Sdk/Extensions/PipelineServiceCollectionExtensions.cs @@ -3,8 +3,9 @@ using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; using Nexo.Core.Application.Pipelines.Ports; +using Nexo.Infrastructure.Pipelines; -namespace Nexo.Infrastructure.Pipelines; +namespace Nexo.Infrastructure.Sdk.Pipelines; /// /// Registers pipeline composition layer services. diff --git a/src/Nexo.Infrastructure/Pipelines/PipelineExecutionAdapterOptions.cs b/src/Nexo.Infrastructure/Pipelines/Sdk/Options/PipelineExecutionAdapterOptions.cs similarity index 100% rename from src/Nexo.Infrastructure/Pipelines/PipelineExecutionAdapterOptions.cs rename to src/Nexo.Infrastructure/Pipelines/Sdk/Options/PipelineExecutionAdapterOptions.cs diff --git a/src/Nexo.Infrastructure/Pipelines/PipelineExecutionOptions.cs b/src/Nexo.Infrastructure/Pipelines/Sdk/Options/PipelineExecutionOptions.cs similarity index 100% rename from src/Nexo.Infrastructure/Pipelines/PipelineExecutionOptions.cs rename to src/Nexo.Infrastructure/Pipelines/Sdk/Options/PipelineExecutionOptions.cs diff --git a/src/Nexo.Infrastructure/Pipelines/PipelinePersistenceOptions.cs b/src/Nexo.Infrastructure/Pipelines/Sdk/Options/PipelinePersistenceOptions.cs similarity index 100% rename from src/Nexo.Infrastructure/Pipelines/PipelinePersistenceOptions.cs rename to src/Nexo.Infrastructure/Pipelines/Sdk/Options/PipelinePersistenceOptions.cs diff --git a/src/Nexo.Infrastructure/Rollback/RollbackServiceCollectionExtensions.cs b/src/Nexo.Infrastructure/Rollback/Sdk/Extensions/RollbackServiceCollectionExtensions.cs similarity index 96% rename from src/Nexo.Infrastructure/Rollback/RollbackServiceCollectionExtensions.cs rename to src/Nexo.Infrastructure/Rollback/Sdk/Extensions/RollbackServiceCollectionExtensions.cs index 083b39bbd..c633e5be4 100644 --- a/src/Nexo.Infrastructure/Rollback/RollbackServiceCollectionExtensions.cs +++ b/src/Nexo.Infrastructure/Rollback/Sdk/Extensions/RollbackServiceCollectionExtensions.cs @@ -5,7 +5,7 @@ using Nexo.Infrastructure.Adaptation; using Nexo.Infrastructure.Rollback; -namespace Nexo.Infrastructure; +namespace Nexo.Infrastructure.Sdk.Rollback; /// /// DI extensions for rollback infrastructure. diff --git a/src/Nexo.Core.Application/Sdk/Ports/INexoSdkBuilder.cs b/src/Nexo.Infrastructure/Sdk/Ports/INexoSdkBuilder.cs similarity index 85% rename from src/Nexo.Core.Application/Sdk/Ports/INexoSdkBuilder.cs rename to src/Nexo.Infrastructure/Sdk/Ports/INexoSdkBuilder.cs index acd0b4454..59ca5d426 100644 --- a/src/Nexo.Core.Application/Sdk/Ports/INexoSdkBuilder.cs +++ b/src/Nexo.Infrastructure/Sdk/Ports/INexoSdkBuilder.cs @@ -1,11 +1,12 @@ using Nexo.Core.Domain.Agents; using Nexo.Core.Domain.Bricks; -namespace Nexo.Core.Application.Sdk.Ports; +namespace Nexo.Infrastructure.Sdk.Ports; /// /// Fluent builder for registering external components (bricks, agents) with Nexo at runtime. -/// Call before AddNexo(). Enables runtime registration without recompiling Nexo. +/// Call before AddNexo(). Enables runtime registration without recompiling Nexo. +/// Default implementation in the Nexo.Hosting.Sdk assembly (HostNexoSdkBuilder). /// public interface INexoSdkBuilder { diff --git a/src/Nexo.Infrastructure/SelfContext/SelfContextServiceCollectionExtensions.cs b/src/Nexo.Infrastructure/SelfContext/Sdk/Extensions/SelfContextServiceCollectionExtensions.cs similarity index 97% rename from src/Nexo.Infrastructure/SelfContext/SelfContextServiceCollectionExtensions.cs rename to src/Nexo.Infrastructure/SelfContext/Sdk/Extensions/SelfContextServiceCollectionExtensions.cs index 46aa393aa..71a63e88e 100644 --- a/src/Nexo.Infrastructure/SelfContext/SelfContextServiceCollectionExtensions.cs +++ b/src/Nexo.Infrastructure/SelfContext/Sdk/Extensions/SelfContextServiceCollectionExtensions.cs @@ -7,9 +7,10 @@ using Nexo.Core.Application.Trust.Ports; using Nexo.Infrastructure.Knowledge; using Nexo.Infrastructure.Observation; +using Nexo.Infrastructure.Sdk.Observation; using Nexo.Infrastructure.SelfContext; -namespace Nexo.Infrastructure; +namespace Nexo.Infrastructure.Sdk.SelfContext; /// /// DI extensions for Block 6 self-context. diff --git a/src/Nexo.Infrastructure/SelfImprovement/SelfImprovementServiceCollectionExtensions.cs b/src/Nexo.Infrastructure/SelfImprovement/Sdk/Extensions/SelfImprovementServiceCollectionExtensions.cs similarity index 97% rename from src/Nexo.Infrastructure/SelfImprovement/SelfImprovementServiceCollectionExtensions.cs rename to src/Nexo.Infrastructure/SelfImprovement/Sdk/Extensions/SelfImprovementServiceCollectionExtensions.cs index dd1e4bc90..e9a3681ad 100644 --- a/src/Nexo.Infrastructure/SelfImprovement/SelfImprovementServiceCollectionExtensions.cs +++ b/src/Nexo.Infrastructure/SelfImprovement/Sdk/Extensions/SelfImprovementServiceCollectionExtensions.cs @@ -9,8 +9,9 @@ using Nexo.Infrastructure.Adaptation; using Nexo.Infrastructure.SelfImprovement; using Nexo.Infrastructure.Trust; +using Nexo.Infrastructure.Sdk.Trust; -namespace Nexo.Infrastructure; +namespace Nexo.Infrastructure.Sdk.SelfImprovement; /// /// DI extensions for self-improvement loop. diff --git a/src/Nexo.Infrastructure/Trust/TrustServiceCollectionExtensions.cs b/src/Nexo.Infrastructure/Trust/Sdk/Extensions/TrustServiceCollectionExtensions.cs similarity index 98% rename from src/Nexo.Infrastructure/Trust/TrustServiceCollectionExtensions.cs rename to src/Nexo.Infrastructure/Trust/Sdk/Extensions/TrustServiceCollectionExtensions.cs index d4d7e21ac..c7e42190f 100644 --- a/src/Nexo.Infrastructure/Trust/TrustServiceCollectionExtensions.cs +++ b/src/Nexo.Infrastructure/Trust/Sdk/Extensions/TrustServiceCollectionExtensions.cs @@ -1,8 +1,9 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Nexo.Core.Application.Trust.Ports; +using Nexo.Infrastructure.Trust; -namespace Nexo.Infrastructure.Trust; +namespace Nexo.Infrastructure.Sdk.Trust; /// /// DI extensions for Trust & Information Architecture. diff --git a/src/Nexo.Sdk/Client/NexoClientSdkBuilder.cs b/src/Nexo.Sdk/Client/NexoClientSdkBuilder.cs new file mode 100644 index 000000000..f07b39050 --- /dev/null +++ b/src/Nexo.Sdk/Client/NexoClientSdkBuilder.cs @@ -0,0 +1,16 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace Nexo.Sdk.Client; + +/// +/// Fluent builder for HTTP client–oriented Nexo integration (Unity, Unreal, embedded hosts talking to a Nexo API). +/// For registering bricks/agents inside the Nexo kernel process, use Nexo.Hosting.Sdk.AddNexoSdk before AddNexo. +/// +public class NexoClientSdkBuilder +{ + /// Creates a builder bound to the caller's . + protected internal NexoClientSdkBuilder(IServiceCollection services) => Services = services; + + /// Services being configured. + protected internal IServiceCollection Services { get; } +} diff --git a/src/Nexo.Sdk/Client/NexoClientSdkServiceCollectionExtensions.cs b/src/Nexo.Sdk/Client/NexoClientSdkServiceCollectionExtensions.cs new file mode 100644 index 000000000..65b2d03c3 --- /dev/null +++ b/src/Nexo.Sdk/Client/NexoClientSdkServiceCollectionExtensions.cs @@ -0,0 +1,26 @@ +using Microsoft.Extensions.DependencyInjection; +using Nexo.Client; + +namespace Nexo.Sdk.Client; + +/// +/// DI extensions for the slim NuGet Nexo.Sdk package (remote API client). +/// +public static class NexoClientSdkServiceCollectionExtensions +{ + /// + /// Registers and optional client-side SDK hooks against the given Nexo API base URL. + /// + /// The service collection. + /// Base URL of the Nexo API (e.g. https://your-server:5000). + /// Optional builder configuration. + public static IServiceCollection AddNexoClientSdk( + this IServiceCollection services, + string baseUrl, + Action? configure = null) + { + services.AddNexoClient(c => c.BaseUrl = baseUrl); + configure?.Invoke(new NexoClientSdkBuilder(services)); + return services; + } +} diff --git a/src/Nexo.Sdk/Legacy/NexoSdkLegacyApiAliases.cs b/src/Nexo.Sdk/Legacy/NexoSdkLegacyApiAliases.cs new file mode 100644 index 000000000..38bef708f --- /dev/null +++ b/src/Nexo.Sdk/Legacy/NexoSdkLegacyApiAliases.cs @@ -0,0 +1,36 @@ +#pragma warning disable CS0618 // intentional obsolete forwarding surface for NuGet compatibility + +using Microsoft.Extensions.DependencyInjection; +using Nexo.Sdk.Client; + +namespace Nexo.Sdk; + +/// +/// Back-compat names for the HTTP client SDK. Prefer and +/// . +/// +[Obsolete("Renamed to NexoClientSdkBuilder (namespace Nexo.Sdk.Client).", error: false)] +public sealed class NexoSdkBuilder : NexoClientSdkBuilder +{ + internal NexoSdkBuilder(IServiceCollection services) + : base(services) + { + } +} + +/// Obsolete entry points preserved for binary compatibility. +public static class NexoSdkLegacyExtensions +{ + /// + [Obsolete("Use Nexo.Sdk.Client.NexoClientSdkServiceCollectionExtensions.AddNexoClientSdk.", error: false)] + public static IServiceCollection AddNexoSdk( + this IServiceCollection services, + string baseUrl, + Action? configure = null) + { + return NexoClientSdkServiceCollectionExtensions.AddNexoClientSdk( + services, + baseUrl, + configure == null ? null : b => configure(new NexoSdkBuilder(b.Services))); + } +} diff --git a/src/Nexo.Sdk/NexoSdkBuilder.cs b/src/Nexo.Sdk/NexoSdkBuilder.cs deleted file mode 100644 index 54c10f11e..000000000 --- a/src/Nexo.Sdk/NexoSdkBuilder.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Nexo.Client; - -namespace Nexo.Sdk; - -/// -/// Builder for configuring Nexo SDK options. -/// Use for Unity, Unreal, or embedded scenarios where local model or server can be used. -/// -public sealed class NexoSdkBuilder -{ - private readonly IServiceCollection _services; - - internal NexoSdkBuilder(IServiceCollection services) - { - _services = services; - } -} - -/// -/// Extension methods for adding Nexo SDK to dependency injection. -/// -public static class NexoSdkServiceCollectionExtensions -{ - /// - /// Adds Nexo SDK (Nexo.Client) with optional adaptive configuration. - /// - /// The service collection. - /// Base URL of the Nexo API (e.g. https://your-server:5000). - /// Optional builder configuration. - public static IServiceCollection AddNexoSdk( - this IServiceCollection services, - string baseUrl, - Action? configure = null) - { - services.AddNexoClient(c => c.BaseUrl = baseUrl); - configure?.Invoke(new NexoSdkBuilder(services)); - return services; - } -} diff --git a/src/Nexo.Tests.BackgroundAgents/Tests/InMemorySanitizationAuditLogTests.cs b/src/Nexo.Tests.BackgroundAgents/Tests/InMemorySanitizationAuditLogTests.cs index dcaf255a0..c1baec61f 100644 --- a/src/Nexo.Tests.BackgroundAgents/Tests/InMemorySanitizationAuditLogTests.cs +++ b/src/Nexo.Tests.BackgroundAgents/Tests/InMemorySanitizationAuditLogTests.cs @@ -8,8 +8,9 @@ namespace Nexo.Tests.BackgroundAgents.Tests; public sealed class InMemorySanitizationAuditLogTests { [Fact(Timeout = 15000)] - public void LogRedaction_StoresEntry() + public async Task LogRedaction_StoresEntry() { + await Task.CompletedTask; var log = new InMemorySanitizationAuditLog(); var now = DateTimeOffset.UtcNow; @@ -25,8 +26,9 @@ public void LogRedaction_StoresEntry() } [Fact(Timeout = 15000)] - public void GetRecent_FiltersBySince() + public async Task GetRecent_FiltersBySince() { + await Task.CompletedTask; var log = new InMemorySanitizationAuditLog(); var old = DateTimeOffset.UtcNow.AddHours(-2); var recent = DateTimeOffset.UtcNow; @@ -42,8 +44,9 @@ public void GetRecent_FiltersBySince() } [Fact(Timeout = 15000)] - public void MaxEntries_EnforcesLimit() + public async Task MaxEntries_EnforcesLimit() { + await Task.CompletedTask; var log = new InMemorySanitizationAuditLog(); var max = NexoDefaults.SanitizationAuditMaxEntries; @@ -57,8 +60,9 @@ public void MaxEntries_EnforcesLimit() } [Fact(Timeout = 15000)] - public void GetRecent_OrdersByTimestampDescending() + public async Task GetRecent_OrdersByTimestampDescending() { + await Task.CompletedTask; var log = new InMemorySanitizationAuditLog(); var t1 = DateTimeOffset.UtcNow.AddMinutes(-3); var t2 = DateTimeOffset.UtcNow.AddMinutes(-1); diff --git a/src/Nexo.Tests.Infrastructure/Helpers/TestTimeouts.cs b/src/Nexo.Tests.Infrastructure/Helpers/TestTimeouts.cs index 4b2a3bf50..b9630fbb3 100644 --- a/src/Nexo.Tests.Infrastructure/Helpers/TestTimeouts.cs +++ b/src/Nexo.Tests.Infrastructure/Helpers/TestTimeouts.cs @@ -9,6 +9,11 @@ public static class TestTimeouts /// 60 seconds for Integration tests. public const int Integration = 60_000; + /// + /// FileSystemWatcher integration: parallel net8+net9 test hosts must serialize; the waiter polls until the lock is free. + /// + public const int FileSystemPipelineIntegration = 240_000; + /// 90 seconds for E2E tests. public const int E2E = 90_000; diff --git a/src/Nexo.Tests.Infrastructure/Locking/CrossProcessLockDefaults.cs b/src/Nexo.Tests.Infrastructure/Locking/CrossProcessLockDefaults.cs new file mode 100644 index 000000000..3310795e2 --- /dev/null +++ b/src/Nexo.Tests.Infrastructure/Locking/CrossProcessLockDefaults.cs @@ -0,0 +1,23 @@ +namespace Nexo.Tests.Infrastructure.Locking; + +/// +/// Entry points and shared defaults for cross-process locking in test hosts. +/// +public static class CrossProcessLockDefaults +{ + /// Default options used by when none are supplied. + public static CrossProcessLockOptions DefaultOptions { get; } = new() + { + MaxWait = TimeSpan.FromMinutes(3), + PollInterval = TimeSpan.FromMilliseconds(150), + FileNamePrefix = "nexo-cross-process", + FileNameSuffix = ".lock", + }; + + /// + /// Shared provider instance suitable for most tests (parallel TFMs, CLI integration, etc.). + /// For isolation or custom defaults, construct explicitly. + /// + public static ICrossProcessLockProvider SharedProvider { get; } = + new ExclusiveFileCrossProcessLockProvider(); +} diff --git a/src/Nexo.Tests.Infrastructure/Locking/CrossProcessLockOptions.cs b/src/Nexo.Tests.Infrastructure/Locking/CrossProcessLockOptions.cs new file mode 100644 index 000000000..ff8136dae --- /dev/null +++ b/src/Nexo.Tests.Infrastructure/Locking/CrossProcessLockOptions.cs @@ -0,0 +1,39 @@ +namespace Nexo.Tests.Infrastructure.Locking; + +/// +/// Configuration for / exclusive-file locks. +/// Immutable overrides merge with provider defaults (last write wins per property). +/// +public sealed record CrossProcessLockOptions +{ + /// Maximum wall-clock time to wait when another process holds the lock. + public TimeSpan? MaxWait { get; init; } + + /// Delay between attempts when the lock file is busy (Linux often throws instead of blocking). + public TimeSpan? PollInterval { get; init; } + + /// Directory for lock files; default is . + public string? DirectoryPath { get; init; } + + /// Prefix for lock filenames. + public string? FileNamePrefix { get; init; } + + /// Suffix including extension (e.g. .integration.lock). + public string? FileNameSuffix { get; init; } + + /// Merges onto this instance (override wins when set). + public CrossProcessLockOptions Merge(CrossProcessLockOptions? overrides) + { + if (overrides is null) + return this; + + return this with + { + MaxWait = overrides.MaxWait ?? MaxWait, + PollInterval = overrides.PollInterval ?? PollInterval, + DirectoryPath = overrides.DirectoryPath ?? DirectoryPath, + FileNamePrefix = overrides.FileNamePrefix ?? FileNamePrefix, + FileNameSuffix = overrides.FileNameSuffix ?? FileNameSuffix, + }; + } +} diff --git a/src/Nexo.Tests.Infrastructure/Locking/ExclusiveFileCrossProcessLock.cs b/src/Nexo.Tests.Infrastructure/Locking/ExclusiveFileCrossProcessLock.cs new file mode 100644 index 000000000..422fa2d2a --- /dev/null +++ b/src/Nexo.Tests.Infrastructure/Locking/ExclusiveFileCrossProcessLock.cs @@ -0,0 +1,16 @@ +namespace Nexo.Tests.Infrastructure.Locking; + +internal sealed class ExclusiveFileCrossProcessLock : ICrossProcessLock +{ + private readonly FileStream _stream; + + public ExclusiveFileCrossProcessLock(FileStream stream, string lockPath) + { + _stream = stream ?? throw new ArgumentNullException(nameof(stream)); + LockPath = lockPath ?? throw new ArgumentNullException(nameof(lockPath)); + } + + public string LockPath { get; } + + public ValueTask DisposeAsync() => _stream.DisposeAsync(); +} diff --git a/src/Nexo.Tests.Infrastructure/Locking/ExclusiveFileCrossProcessLockProvider.cs b/src/Nexo.Tests.Infrastructure/Locking/ExclusiveFileCrossProcessLockProvider.cs new file mode 100644 index 000000000..7384a1be8 --- /dev/null +++ b/src/Nexo.Tests.Infrastructure/Locking/ExclusiveFileCrossProcessLockProvider.cs @@ -0,0 +1,93 @@ +using System.Security.Cryptography; +using System.Text; + +namespace Nexo.Tests.Infrastructure.Locking; + +/// +/// Cross-process lock via an exclusively opened file. +/// On Linux, another process holding typically causes rather than blocking, +/// so acquisition polls until success or timeout. +/// +/// +/// Subclass and override or to customize naming without replacing the whole provider. +/// +public class ExclusiveFileCrossProcessLockProvider : ICrossProcessLockProvider +{ + /// Creates a provider with optional defaults applied to every call. + public ExclusiveFileCrossProcessLockProvider(CrossProcessLockOptions? defaultOptions = null) + { + DefaultOptions = CrossProcessLockDefaults.DefaultOptions.Merge(defaultOptions ?? new CrossProcessLockOptions()); + } + + /// Baseline options merged with per-call overrides. + protected CrossProcessLockOptions DefaultOptions { get; } + + /// + public async Task AcquireAsync( + string name, + CrossProcessLockOptions? options = null, + CancellationToken cancellationToken = default) + { + ArgumentException.ThrowIfNullOrWhiteSpace(name); + + var merged = DefaultOptions.Merge(options ?? new CrossProcessLockOptions()); + var maxWait = merged.MaxWait ?? TimeSpan.FromMinutes(3); + var poll = merged.PollInterval ?? TimeSpan.FromMilliseconds(150); + var deadline = DateTimeOffset.UtcNow + maxWait; + var path = ResolveLockPath(name, merged); + + while (DateTimeOffset.UtcNow < deadline) + { + cancellationToken.ThrowIfCancellationRequested(); + try + { + var stream = new FileStream( + path, + FileMode.OpenOrCreate, + FileAccess.ReadWrite, + FileShare.None, + bufferSize: 1, + FileOptions.Asynchronous | FileOptions.DeleteOnClose); + + return new ExclusiveFileCrossProcessLock(stream, path); + } + catch (IOException) + { + await Task.Delay(poll, cancellationToken).ConfigureAwait(false); + } + } + + throw new TimeoutException($"Could not acquire exclusive lock for '{path}' within {maxWait}."); + } + + /// + /// Builds the lock file path. Override to change layout (per-repo subdirectory, hashing only, etc.). + /// + protected virtual string ResolveLockPath(string lockName, CrossProcessLockOptions merged) + { + var dir = merged.DirectoryPath ?? Path.GetTempPath(); + var prefix = merged.FileNamePrefix ?? "nexo-cross-process"; + var suffix = merged.FileNameSuffix ?? ".lock"; + var safe = SanitizeLockName(lockName); + return Path.Combine(dir, $"{prefix}.{safe}{suffix}"); + } + + /// + /// Produces a filesystem-safe segment from . Override for custom rules. + /// + protected virtual string SanitizeLockName(string lockName) + { + var trimmed = lockName.Trim(); + var invalid = Path.GetInvalidFileNameChars(); + var sb = new StringBuilder(trimmed.Length); + foreach (var ch in trimmed) + sb.Append(Array.IndexOf(invalid, ch) >= 0 ? '_' : ch); + + var s = sb.ToString(); + if (s.Length <= 120 && s.Length > 0) + return s; + + var hash = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(lockName)))[..24]; + return hash; + } +} diff --git a/src/Nexo.Tests.Infrastructure/Locking/ICrossProcessLock.cs b/src/Nexo.Tests.Infrastructure/Locking/ICrossProcessLock.cs new file mode 100644 index 000000000..4f27e740c --- /dev/null +++ b/src/Nexo.Tests.Infrastructure/Locking/ICrossProcessLock.cs @@ -0,0 +1,10 @@ +namespace Nexo.Tests.Infrastructure.Locking; + +/// +/// An acquired cross-process lock. Dispose when the critical section ends (typically await using). +/// +public interface ICrossProcessLock : IAsyncDisposable +{ + /// Resolved filesystem path backing this lock (useful for diagnostics). + string LockPath { get; } +} diff --git a/src/Nexo.Tests.Infrastructure/Locking/ICrossProcessLockProvider.cs b/src/Nexo.Tests.Infrastructure/Locking/ICrossProcessLockProvider.cs new file mode 100644 index 000000000..5620f9176 --- /dev/null +++ b/src/Nexo.Tests.Infrastructure/Locking/ICrossProcessLockProvider.cs @@ -0,0 +1,19 @@ +namespace Nexo.Tests.Infrastructure.Locking; + +/// +/// Acquires locks that work across processes (e.g. parallel dotnet test TFMs). +/// Implementations may use OS-specific primitives; callers should treat behavior as best-effort mutual exclusion. +/// +public interface ICrossProcessLockProvider +{ + /// + /// Blocks until the lock is acquired or / options limits are reached. + /// + /// Logical lock name (sanitized to a file segment). + /// Optional overrides; merged with provider defaults. + /// Cancellation token. + Task AcquireAsync( + string name, + CrossProcessLockOptions? options = null, + CancellationToken cancellationToken = default); +} diff --git a/src/Nexo.Tests.Infrastructure/Nexo.Tests.Infrastructure.csproj b/src/Nexo.Tests.Infrastructure/Nexo.Tests.Infrastructure.csproj index d13f91cf2..c8b6b5d3f 100644 --- a/src/Nexo.Tests.Infrastructure/Nexo.Tests.Infrastructure.csproj +++ b/src/Nexo.Tests.Infrastructure/Nexo.Tests.Infrastructure.csproj @@ -70,5 +70,9 @@ + + + + diff --git a/src/Nexo.Tests.Infrastructure/Tests/BackgroundAgents/BackgroundAgentLifecycleE2ETests.cs b/src/Nexo.Tests.Infrastructure/Tests/BackgroundAgents/BackgroundAgentLifecycleE2ETests.cs index 19a0d37b0..9dba1fc56 100644 --- a/src/Nexo.Tests.Infrastructure/Tests/BackgroundAgents/BackgroundAgentLifecycleE2ETests.cs +++ b/src/Nexo.Tests.Infrastructure/Tests/BackgroundAgents/BackgroundAgentLifecycleE2ETests.cs @@ -177,8 +177,9 @@ public async Task LogStore_BoundsPerAgent() } [Fact(Timeout = TestTimeouts.E2E)] - public void InMemorySanitizationAuditLog_Bounds() + public async Task InMemorySanitizationAuditLog_Bounds() { + await Task.CompletedTask; var auditLog = new InMemorySanitizationAuditLog(); for (var i = 0; i < NexoDefaults.SanitizationAuditMaxEntries + 500; i++) auditLog.LogRedaction(DateTimeOffset.UtcNow, "v1", "field", "redact", null); @@ -188,8 +189,9 @@ public void InMemorySanitizationAuditLog_Bounds() } [Fact(Timeout = TestTimeouts.E2E)] - public void DataDecisionAuditLog_Bounds() + public async Task DataDecisionAuditLog_Bounds() { + await Task.CompletedTask; var auditLog = new DataDecisionAuditLog(); for (var i = 0; i < NexoDefaults.DataDecisionAuditMaxEntries + 500; i++) auditLog.LogClassification("type", "level", "reason"); diff --git a/src/Nexo.Tests.Infrastructure/Tests/Configuration/NexoDefaultsTests.cs b/src/Nexo.Tests.Infrastructure/Tests/Configuration/NexoDefaultsTests.cs index 9a7a7d031..7da1b1948 100644 --- a/src/Nexo.Tests.Infrastructure/Tests/Configuration/NexoDefaultsTests.cs +++ b/src/Nexo.Tests.Infrastructure/Tests/Configuration/NexoDefaultsTests.cs @@ -18,8 +18,9 @@ namespace Nexo.Tests.Infrastructure.Tests.Configuration; public sealed class NexoDefaultsTests { [Fact(Timeout = TestTimeouts.Quick)] - public void LlmDefaults_AreStable() + public async Task LlmDefaults_AreStable() { + await Task.CompletedTask; NexoDefaults.LlmRetryCount.Should().Be(3); NexoDefaults.LlmTemperature.Should().Be(0.2); NexoDefaults.LlmMaxTokens.Should().Be(4096); @@ -27,22 +28,25 @@ public void LlmDefaults_AreStable() } [Fact(Timeout = TestTimeouts.Quick)] - public void OpenAiDefaults_AreStable() + public async Task OpenAiDefaults_AreStable() { + await Task.CompletedTask; NexoDefaults.OpenAiDefaultModel.Should().Be("gpt-4o-mini"); NexoDefaults.OpenAiDefaultBaseUrl.Should().Be("https://api.openai.com/v1/chat/completions"); NexoDefaults.OpenAiDefaultVisionModel.Should().Be("gpt-4o-mini"); } [Fact(Timeout = TestTimeouts.Quick)] - public void AzureDefaults_AreStable() + public async Task AzureDefaults_AreStable() { + await Task.CompletedTask; NexoDefaults.AzureOpenAiDefaultApiVersion.Should().Be("2024-06-01"); } [Fact(Timeout = TestTimeouts.Quick)] - public void OllamaDefaults_AreStable() + public async Task OllamaDefaults_AreStable() { + await Task.CompletedTask; NexoDefaults.OllamaDefaultBaseUrl.Should().Be("http://localhost:11434"); NexoDefaults.OllamaDefaultModel.Should().Be("llama3.1:latest"); NexoDefaults.OllamaDefaultVisionModel.Should().Be("richardyoung/smolvlm2-2.2b-instruct"); @@ -50,15 +54,17 @@ public void OllamaDefaults_AreStable() } [Fact(Timeout = TestTimeouts.Quick)] - public void PipelineDefaults_AreStable() + public async Task PipelineDefaults_AreStable() { + await Task.CompletedTask; NexoDefaults.PipelineMaxRetryAttempts.Should().Be(3); NexoDefaults.PipelineRetryDelayMs.Should().Be(100); } [Fact(Timeout = TestTimeouts.Quick)] - public void ConfigDefaults_AreStable() + public async Task ConfigDefaults_AreStable() { + await Task.CompletedTask; NexoDefaults.AnalysisMaxComplexityThreshold.Should().Be(20); NexoDefaults.ValidationTimeoutSeconds.Should().Be(300); NexoDefaults.ConfigFileName.Should().Be("config.json"); @@ -66,16 +72,18 @@ public void ConfigDefaults_AreStable() } [Fact(Timeout = TestTimeouts.Quick)] - public void AuditDefaults_AreStable() + public async Task AuditDefaults_AreStable() { + await Task.CompletedTask; NexoDefaults.SanitizationAuditMaxEntries.Should().Be(10_000); NexoDefaults.DataDecisionAuditMaxEntries.Should().Be(50_000); NexoDefaults.AgentLogMaxEntriesPerAgent.Should().Be(1_000); } [Fact(Timeout = TestTimeouts.Quick)] - public void RunPodDefaults_AreStable() + public async Task RunPodDefaults_AreStable() { + await Task.CompletedTask; NexoDefaults.RunPodDefaultBaseUrl.Should().Be("https://api.runpod.io"); NexoDefaults.RunPodDefaultGpuTier.Should().Be("NVIDIA_A4000"); NexoDefaults.RunPodDefaultTimeoutMinutes.Should().Be(10); @@ -83,24 +91,27 @@ public void RunPodDefaults_AreStable() } [Fact(Timeout = TestTimeouts.Quick)] - public void NetworkingDefaults_AreStable() + public async Task NetworkingDefaults_AreStable() { + await Task.CompletedTask; NexoDefaults.NetworkBusHeartbeatIntervalSeconds.Should().Be(30); NexoDefaults.NetworkBusMaxEventHistory.Should().Be(10_000); NexoDefaults.NetworkBusDefaultMaxHops.Should().Be(3); } [Fact(Timeout = TestTimeouts.Quick)] - public void PipelineExecutionOptions_UsesNexoDefaults() + public async Task PipelineExecutionOptions_UsesNexoDefaults() { + await Task.CompletedTask; var opts = new PipelineExecutionOptions(); opts.MaxRetryAttempts.Should().Be(NexoDefaults.PipelineMaxRetryAttempts); opts.RetryDelayMs.Should().Be(NexoDefaults.PipelineRetryDelayMs); } [Fact(Timeout = TestTimeouts.Quick)] - public void RunPodBrickConfig_UsesNexoDefaults() + public async Task RunPodBrickConfig_UsesNexoDefaults() { + await Task.CompletedTask; var cfg = new RunPodBrickConfig(); cfg.BaseUrl.Should().Be(NexoDefaults.RunPodDefaultBaseUrl); cfg.PreferredGpuTier.Should().Be(NexoDefaults.RunPodDefaultGpuTier); @@ -110,8 +121,9 @@ public void RunPodBrickConfig_UsesNexoDefaults() } [Fact(Timeout = TestTimeouts.Quick)] - public void NetworkBusOptions_UsesNexoDefaults() + public async Task NetworkBusOptions_UsesNexoDefaults() { + await Task.CompletedTask; var opts = new NetworkBusOptions(); opts.HeartbeatIntervalSeconds.Should().Be(NexoDefaults.NetworkBusHeartbeatIntervalSeconds); opts.MaxEventHistory.Should().Be(NexoDefaults.NetworkBusMaxEventHistory); @@ -119,22 +131,25 @@ public void NetworkBusOptions_UsesNexoDefaults() } [Fact(Timeout = TestTimeouts.Quick)] - public void BrickUsageTrackerOptions_UsesNexoDefaults() + public async Task BrickUsageTrackerOptions_UsesNexoDefaults() { + await Task.CompletedTask; var opts = new BrickUsageTrackerOptions(); opts.MaxEntries.Should().Be(NexoDefaults.BrickUsageTrackerMaxEntries); opts.RollingHourWindowSeconds.Should().Be(NexoDefaults.BrickUsageTrackerRollingHourWindowSeconds); } [Fact(Timeout = TestTimeouts.Quick)] - public void VideoDefaults_AreStable() + public async Task VideoDefaults_AreStable() { + await Task.CompletedTask; NexoDefaults.VideoDefaultFps.Should().Be(5); } [Fact(Timeout = TestTimeouts.Quick)] - public void EmbeddingDefaults_AreStable() + public async Task EmbeddingDefaults_AreStable() { + await Task.CompletedTask; NexoDefaults.EmbeddingDefaultDimension.Should().Be(64); } } diff --git a/src/Nexo.Tests.Infrastructure/Tests/Execution/HotSwapTests.cs b/src/Nexo.Tests.Infrastructure/Tests/Execution/HotSwapTests.cs index 3f1225feb..5310c3d07 100644 --- a/src/Nexo.Tests.Infrastructure/Tests/Execution/HotSwapTests.cs +++ b/src/Nexo.Tests.Infrastructure/Tests/Execution/HotSwapTests.cs @@ -22,16 +22,18 @@ public sealed class HotSwapTests : TempDirTestBase public HotSwapTests() : base("nexo-hotswap") { } [Fact(Timeout = 10000)] - public void HotSwap_Orchestrator_ChecksMode_BeforeEachStep() + public async Task HotSwap_Orchestrator_ChecksMode_BeforeEachStep() { + await Task.CompletedTask; var store = new StepExecutionModeStore(Path.Combine(TempDir, $"mode-{Guid.NewGuid():N}.json")); var mode = store.GetMode("step-1"); mode.Should().Be(ExecutionMode.Deterministic, "default mode is Deterministic"); } [Fact(Timeout = 10000)] - public void HotSwap_DeterministicStep_ExecutesBrick() + public async Task HotSwap_DeterministicStep_ExecutesBrick() { + await Task.CompletedTask; var configPath = Path.Combine(TempDir, $"det-{Guid.NewGuid():N}.json"); var store = new StepExecutionModeStore(configPath); var mode = store.GetMode("step-1"); diff --git a/src/Nexo.Tests.Infrastructure/Tests/Execution/ProviderFactoryEdgeCaseTests.cs b/src/Nexo.Tests.Infrastructure/Tests/Execution/ProviderFactoryEdgeCaseTests.cs index 71eddac99..300c986fd 100644 --- a/src/Nexo.Tests.Infrastructure/Tests/Execution/ProviderFactoryEdgeCaseTests.cs +++ b/src/Nexo.Tests.Infrastructure/Tests/Execution/ProviderFactoryEdgeCaseTests.cs @@ -22,8 +22,9 @@ private ProviderFactory CreateFactory() } [Fact(Timeout = TestTimeouts.Quick)] - public void MockProvider_WhenNotAllowed_IsUnavailable() + public async Task MockProvider_WhenNotAllowed_IsUnavailable() { + await Task.CompletedTask; var prev = Environment.GetEnvironmentVariable("NEXO_ALLOW_MOCK"); try { @@ -42,8 +43,9 @@ public void MockProvider_WhenNotAllowed_IsUnavailable() } [Fact(Timeout = TestTimeouts.Quick)] - public void MockProvider_WhenAllowed_IsAvailable() + public async Task MockProvider_WhenAllowed_IsAvailable() { + await Task.CompletedTask; var prev = Environment.GetEnvironmentVariable("NEXO_ALLOW_MOCK"); try { @@ -62,8 +64,9 @@ public void MockProvider_WhenAllowed_IsAvailable() } [Fact(Timeout = TestTimeouts.Quick)] - public void UnknownProvider_IsNotAvailable() + public async Task UnknownProvider_IsNotAvailable() { + await Task.CompletedTask; var factory = CreateFactory(); factory.IsProviderAvailable("unknown").Should().BeFalse(); @@ -72,8 +75,9 @@ public void UnknownProvider_IsNotAvailable() } [Fact(Timeout = TestTimeouts.Quick)] - public void ProviderAvailability_IsCaseInsensitive() + public async Task ProviderAvailability_IsCaseInsensitive() { + await Task.CompletedTask; var prev = Environment.GetEnvironmentVariable("NEXO_ALLOW_MOCK"); try { diff --git a/src/Nexo.Tests.Infrastructure/Tests/Hosting/HostingDeploymentProfileTests.cs b/src/Nexo.Tests.Infrastructure/Tests/Hosting/HostingDeploymentProfileTests.cs index 1fda7c735..5aefe61f7 100644 --- a/src/Nexo.Tests.Infrastructure/Tests/Hosting/HostingDeploymentProfileTests.cs +++ b/src/Nexo.Tests.Infrastructure/Tests/Hosting/HostingDeploymentProfileTests.cs @@ -23,8 +23,9 @@ public sealed class HostingDeploymentProfileTests [InlineData(NexoDeploymentProfile.Edge)] [InlineData(NexoDeploymentProfile.AirGapped)] [InlineData(NexoDeploymentProfile.System)] - public void AllProfiles_BuildWithoutException(NexoDeploymentProfile profile) + public async Task AllProfiles_BuildWithoutException(NexoDeploymentProfile profile) { + await Task.CompletedTask; var services = new ServiceCollection(); services.AddLogging(); services.AddNexoProfile(profile); @@ -34,8 +35,9 @@ public void AllProfiles_BuildWithoutException(NexoDeploymentProfile profile) } [Fact(Timeout = TestTimeouts.E2E)] - public void FullProfile_ResolvesConfigurationService() + public async Task FullProfile_ResolvesConfigurationService() { + await Task.CompletedTask; var services = new ServiceCollection(); services.AddLogging(); services.AddNexo(); @@ -45,8 +47,9 @@ public void FullProfile_ResolvesConfigurationService() } [Fact(Timeout = TestTimeouts.E2E)] - public void SystemProfile_MinimalRegistration_StillResolvesCore() + public async Task SystemProfile_MinimalRegistration_StillResolvesCore() { + await Task.CompletedTask; var services = new ServiceCollection(); services.AddLogging(); services.AddNexoProfile(NexoDeploymentProfile.System); @@ -57,8 +60,9 @@ public void SystemProfile_MinimalRegistration_StillResolvesCore() } [Fact(Timeout = TestTimeouts.E2E)] - public void EdgeProfile_OmitsBackgroundAgents() + public async Task EdgeProfile_OmitsBackgroundAgents() { + await Task.CompletedTask; var services = new ServiceCollection(); services.AddLogging(); services.AddNexoProfile(NexoDeploymentProfile.Edge); @@ -69,8 +73,9 @@ public void EdgeProfile_OmitsBackgroundAgents() } [Fact(Timeout = TestTimeouts.E2E)] - public void AirGappedProfile_OmitsTrustServices() + public async Task AirGappedProfile_OmitsTrustServices() { + await Task.CompletedTask; var services = new ServiceCollection(); services.AddLogging(); services.AddNexoProfile(NexoDeploymentProfile.AirGapped); @@ -81,8 +86,9 @@ public void AirGappedProfile_OmitsTrustServices() } [Fact(Timeout = TestTimeouts.E2E)] - public void DeploymentProfile_FromEnvironmentVariable() + public async Task DeploymentProfile_FromEnvironmentVariable() { + await Task.CompletedTask; var prev = Environment.GetEnvironmentVariable("NEXO_DEPLOYMENT_PROFILE"); try { @@ -102,8 +108,9 @@ public void DeploymentProfile_FromEnvironmentVariable() } [Fact(Timeout = TestTimeouts.E2E)] - public void DeploymentProfile_ExplicitOverridesEnvVar() + public async Task DeploymentProfile_ExplicitOverridesEnvVar() { + await Task.CompletedTask; var prev = Environment.GetEnvironmentVariable("NEXO_DEPLOYMENT_PROFILE"); try { @@ -122,8 +129,9 @@ public void DeploymentProfile_ExplicitOverridesEnvVar() } [Fact(Timeout = TestTimeouts.E2E)] - public void StrictMode_FromEnvironmentVariable() + public async Task StrictMode_FromEnvironmentVariable() { + await Task.CompletedTask; var prev = Environment.GetEnvironmentVariable("NEXO_STRICT_MODE"); try { @@ -142,8 +150,9 @@ public void StrictMode_FromEnvironmentVariable() } [Fact(Timeout = TestTimeouts.E2E)] - public void StrictMode_ExplicitOverridesEnvVar() + public async Task StrictMode_ExplicitOverridesEnvVar() { + await Task.CompletedTask; var prev = Environment.GetEnvironmentVariable("NEXO_STRICT_MODE"); try { @@ -163,8 +172,9 @@ public void StrictMode_ExplicitOverridesEnvVar() } [Fact(Timeout = TestTimeouts.E2E)] - public void AddNexo_CalledTwice_DoesNotThrow() + public async Task AddNexo_CalledTwice_DoesNotThrow() { + await Task.CompletedTask; var services = new ServiceCollection(); services.AddLogging(); services.AddNexo(); @@ -175,8 +185,9 @@ public void AddNexo_CalledTwice_DoesNotThrow() } [Fact(Timeout = TestTimeouts.E2E)] - public void DeploymentProfile_InvalidValue_Throws() + public async Task DeploymentProfile_InvalidValue_Throws() { + await Task.CompletedTask; var prev = Environment.GetEnvironmentVariable("NEXO_DEPLOYMENT_PROFILE"); try { diff --git a/src/Nexo.Tests.Infrastructure/Tests/Hosting/HostingE2ESmokeTests.cs b/src/Nexo.Tests.Infrastructure/Tests/Hosting/HostingE2ESmokeTests.cs index 8f5b21003..caea161ee 100644 --- a/src/Nexo.Tests.Infrastructure/Tests/Hosting/HostingE2ESmokeTests.cs +++ b/src/Nexo.Tests.Infrastructure/Tests/Hosting/HostingE2ESmokeTests.cs @@ -22,8 +22,9 @@ namespace Nexo.Tests.Infrastructure.Tests.Hosting; public sealed class HostingE2ESmokeTests { [Fact(Timeout = TestTimeouts.E2E)] - public void AddNexo_ShouldBuildServiceProvider() + public async Task AddNexo_ShouldBuildServiceProvider() { + await Task.CompletedTask; var services = new ServiceCollection(); services.AddLogging(); services.AddNexo(); @@ -54,8 +55,9 @@ public async Task AddNexo_ShouldResolveAndRunValidation() } [Fact(Timeout = TestTimeouts.E2E)] - public void AddNexo_ShouldResolveAnalysisService() + public async Task AddNexo_ShouldResolveAnalysisService() { + await Task.CompletedTask; var services = new ServiceCollection(); services.AddLogging(); services.AddNexo(); @@ -66,8 +68,9 @@ public void AddNexo_ShouldResolveAnalysisService() } [Fact(Timeout = TestTimeouts.E2E)] - public void AddNexo_RegistersObservationPipeline_ByDefault() + public async Task AddNexo_RegistersObservationPipeline_ByDefault() { + await Task.CompletedTask; var services = new ServiceCollection(); services.AddLogging(); services.AddNexo(); @@ -78,8 +81,9 @@ public void AddNexo_RegistersObservationPipeline_ByDefault() } [Fact(Timeout = TestTimeouts.E2E)] - public void AddNexo_WithDisableObservationPipeline_DoesNotRegister() + public async Task AddNexo_WithDisableObservationPipeline_DoesNotRegister() { + await Task.CompletedTask; var services = new ServiceCollection(); services.AddLogging(); services.AddNexo(o => o.DisableObservationPipeline = true); @@ -90,8 +94,9 @@ public void AddNexo_WithDisableObservationPipeline_DoesNotRegister() } [Fact(Timeout = TestTimeouts.E2E)] - public void AddNexoProfile_Edge_PeelsOffOptionalServices() + public async Task AddNexoProfile_Edge_PeelsOffOptionalServices() { + await Task.CompletedTask; var services = new ServiceCollection(); services.AddLogging(); services.AddNexoProfile(NexoDeploymentProfile.Edge); @@ -104,8 +109,9 @@ public void AddNexoProfile_Edge_PeelsOffOptionalServices() } [Fact(Timeout = TestTimeouts.E2E)] - public void AddNexo_UsesEnvironmentDeploymentProfile_WhenOptionsDoNotOverride() + public async Task AddNexo_UsesEnvironmentDeploymentProfile_WhenOptionsDoNotOverride() { + await Task.CompletedTask; const string profileKey = "NEXO_DEPLOYMENT_PROFILE"; var previous = Environment.GetEnvironmentVariable(profileKey); Environment.SetEnvironmentVariable(profileKey, "edge"); diff --git a/src/Nexo.Tests.Infrastructure/Tests/Hosting/OpenTelemetryTests.cs b/src/Nexo.Tests.Infrastructure/Tests/Hosting/OpenTelemetryTests.cs index ad1248e25..9ada64f3a 100644 --- a/src/Nexo.Tests.Infrastructure/Tests/Hosting/OpenTelemetryTests.cs +++ b/src/Nexo.Tests.Infrastructure/Tests/Hosting/OpenTelemetryTests.cs @@ -12,8 +12,9 @@ namespace Nexo.Tests.Infrastructure.Tests.Hosting; public sealed class OpenTelemetryTests { [Fact(Timeout = TestTimeouts.E2E)] - public void AddNexoOpenTelemetry_BuildsProvider_WithoutError() + public async Task AddNexoOpenTelemetry_BuildsProvider_WithoutError() { + await Task.CompletedTask; var services = new ServiceCollection(); services.AddLogging(); services.AddNexo(); diff --git a/src/Nexo.Tests.Infrastructure/Tests/Hosting/StrictModeE2ETests.cs b/src/Nexo.Tests.Infrastructure/Tests/Hosting/StrictModeE2ETests.cs index b63d29380..481a62bfb 100644 --- a/src/Nexo.Tests.Infrastructure/Tests/Hosting/StrictModeE2ETests.cs +++ b/src/Nexo.Tests.Infrastructure/Tests/Hosting/StrictModeE2ETests.cs @@ -21,8 +21,9 @@ namespace Nexo.Tests.Infrastructure.Tests.Hosting; public sealed class StrictModeE2ETests { [Fact(Timeout = TestTimeouts.E2E)] - public void StrictMode_Disabled_ByDefault() + public async Task StrictMode_Disabled_ByDefault() { + await Task.CompletedTask; var services = new ServiceCollection(); services.AddLogging(); services.AddNexo(); @@ -38,8 +39,9 @@ public void StrictMode_Disabled_ByDefault() } [Fact(Timeout = TestTimeouts.E2E)] - public void StrictMode_Enabled_AllSubFlagsFollowMaster() + public async Task StrictMode_Enabled_AllSubFlagsFollowMaster() { + await Task.CompletedTask; var services = new ServiceCollection(); services.AddLogging(); services.AddNexo(opts => opts.StrictMode.Enabled = true); @@ -55,8 +57,9 @@ public void StrictMode_Enabled_AllSubFlagsFollowMaster() } [Fact(Timeout = TestTimeouts.E2E)] - public void StrictMode_IndividualOverrides_TakePrecedence() + public async Task StrictMode_IndividualOverrides_TakePrecedence() { + await Task.CompletedTask; var services = new ServiceCollection(); services.AddLogging(); services.AddNexo(opts => @@ -76,8 +79,9 @@ public void StrictMode_IndividualOverrides_TakePrecedence() } [Fact(Timeout = TestTimeouts.E2E)] - public void StrictMode_SelectiveEnable_WhenMasterDisabled() + public async Task StrictMode_SelectiveEnable_WhenMasterDisabled() { + await Task.CompletedTask; var services = new ServiceCollection(); services.AddLogging(); services.AddNexo(opts => @@ -134,8 +138,9 @@ public async Task StrictMode_Permissive_FallsBackToDefaultsOnMissingConfig() } [Fact(Timeout = TestTimeouts.E2E)] - public void StrictMode_RegisteredAsSingleton_SameInstanceEverywhere() + public async Task StrictMode_RegisteredAsSingleton_SameInstanceEverywhere() { + await Task.CompletedTask; var services = new ServiceCollection(); services.AddLogging(); services.AddNexo(opts => opts.StrictMode.Enabled = true); diff --git a/src/Nexo.Tests.Infrastructure/Tests/Locking/ExclusiveFileCrossProcessLockProviderTests.cs b/src/Nexo.Tests.Infrastructure/Tests/Locking/ExclusiveFileCrossProcessLockProviderTests.cs new file mode 100644 index 000000000..01e0f5a14 --- /dev/null +++ b/src/Nexo.Tests.Infrastructure/Tests/Locking/ExclusiveFileCrossProcessLockProviderTests.cs @@ -0,0 +1,26 @@ +using Nexo.Tests.Infrastructure.Locking; +using Xunit; + +namespace Nexo.Tests.Infrastructure.Tests.Locking; + +public sealed class ExclusiveFileCrossProcessLockProviderTests +{ + [Fact] + public async Task AcquireAsync_LockPath_UnderTempDirectory() + { + var name = "sdk-test-" + Guid.NewGuid(); + var provider = new ExclusiveFileCrossProcessLockProvider( + new CrossProcessLockOptions + { + FileNamePrefix = "nexo-test", + FileNameSuffix = ".lock", + MaxWait = TimeSpan.FromSeconds(5), + }); + + await using (var l = await provider.AcquireAsync(name)) + { + Assert.StartsWith(Path.GetTempPath(), l.LockPath, StringComparison.Ordinal); + Assert.Contains("nexo-test", l.LockPath, StringComparison.Ordinal); + } + } +} diff --git a/src/Nexo.Tests.Infrastructure/Tests/Networking/NetworkBusOptionsTests.cs b/src/Nexo.Tests.Infrastructure/Tests/Networking/NetworkBusOptionsTests.cs index 027ed7dd4..2c108810f 100644 --- a/src/Nexo.Tests.Infrastructure/Tests/Networking/NetworkBusOptionsTests.cs +++ b/src/Nexo.Tests.Infrastructure/Tests/Networking/NetworkBusOptionsTests.cs @@ -9,8 +9,9 @@ namespace Nexo.Tests.Infrastructure.Tests.Networking; public sealed class NetworkBusOptionsTests { [Fact(Timeout = TestTimeouts.Quick)] - public void DefaultValues_MatchNexoDefaults() + public async Task DefaultValues_MatchNexoDefaults() { + await Task.CompletedTask; var opts = new NetworkBusOptions(); opts.HeartbeatIntervalSeconds.Should().Be(NexoDefaults.NetworkBusHeartbeatIntervalSeconds); @@ -19,24 +20,27 @@ public void DefaultValues_MatchNexoDefaults() } [Fact(Timeout = TestTimeouts.Quick)] - public void DefaultNodeId_IsMachineName() + public async Task DefaultNodeId_IsMachineName() { + await Task.CompletedTask; var opts = new NetworkBusOptions(); opts.NodeId.Should().Be(Environment.MachineName); } [Fact(Timeout = TestTimeouts.Quick)] - public void DefaultPeerUrls_IsEmpty() + public async Task DefaultPeerUrls_IsEmpty() { + await Task.CompletedTask; var opts = new NetworkBusOptions(); opts.PeerUrls.Should().BeEmpty(); } [Fact(Timeout = TestTimeouts.Quick)] - public void CustomValues_OverrideDefaults() + public async Task CustomValues_OverrideDefaults() { + await Task.CompletedTask; var opts = new NetworkBusOptions { NodeId = "custom-node", @@ -54,8 +58,9 @@ public void CustomValues_OverrideDefaults() } [Fact(Timeout = TestTimeouts.Quick)] - public void SectionName_IsNetworkBus() + public async Task SectionName_IsNetworkBus() { + await Task.CompletedTask; NetworkBusOptions.SectionName.Should().Be("NetworkBus"); } } diff --git a/src/Nexo.Tests.Infrastructure/Tests/Observation/ObservationPipelineIntegrationTests.cs b/src/Nexo.Tests.Infrastructure/Tests/Observation/ObservationPipelineIntegrationTests.cs index edd5b591b..7a927dc44 100644 --- a/src/Nexo.Tests.Infrastructure/Tests/Observation/ObservationPipelineIntegrationTests.cs +++ b/src/Nexo.Tests.Infrastructure/Tests/Observation/ObservationPipelineIntegrationTests.cs @@ -6,6 +6,7 @@ using Nexo.Infrastructure.Trust; using Nexo.Tests.Application.Helpers; using Nexo.Tests.Infrastructure.Helpers; +using Nexo.Tests.Infrastructure.Locking; using Xunit; namespace Nexo.Tests.Infrastructure.Tests.Observation; @@ -33,9 +34,17 @@ public void Dispose() _tempDirCleanup.Dispose(); } - [Fact(Timeout = TestTimeouts.Integration)] + [Fact(Timeout = TestTimeouts.FileSystemPipelineIntegration)] public async Task FullPipeline_FileSystemToPatternStore_StoresPatterns() { + // Multi-target `dotnet test` runs net8 + net9 hosts in parallel; Linux inotify/FileSystemWatcher can drop events when both run this test at once. + await using var crossHostGate = await CrossProcessLockDefaults.SharedProvider.AcquireAsync( + "observation-fs-pipeline.integration", + new CrossProcessLockOptions + { + FileNamePrefix = "nexo-observation", + FileNameSuffix = ".lock", + }); var watchPath = Path.Combine(_tempDir, "src"); Directory.CreateDirectory(watchPath); var testFile = Path.Combine(watchPath, "foo.cs"); @@ -54,7 +63,7 @@ public async Task FullPipeline_FileSystemToPatternStore_StoresPatterns() new[] { "*.cs" }, loggerFactory.CreateLogger()); - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30)); + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(45)); var eventCount = 0; var processTask = Task.Run(async () => { @@ -62,23 +71,24 @@ public async Task FullPipeline_FileSystemToPatternStore_StoresPatterns() { eventCount++; await patternDetector.ProcessAsync(evt, cts.Token); - if (eventCount >= 3) + if (eventCount >= 12) break; } }, cts.Token); + await Task.Delay(500, CancellationToken.None); await File.WriteAllTextAsync(testFile, "// v1", cts.Token); - await Task.Delay(400, cts.Token); + await Task.Delay(500, cts.Token); await File.WriteAllTextAsync(testFile, "// v2", cts.Token); - await Task.Delay(400, cts.Token); + await Task.Delay(500, cts.Token); await File.WriteAllTextAsync(testFile, "// v3", cts.Token); await processTask; - await Task.Delay(600, CancellationToken.None); + await Task.Delay(1000, CancellationToken.None); - using var queryCts = new CancellationTokenSource(TimeSpan.FromSeconds(15)); + using var queryCts = new CancellationTokenSource(TimeSpan.FromSeconds(20)); IReadOnlyList patterns = []; - for (var attempt = 0; attempt < 15; attempt++) + for (var attempt = 0; attempt < 30; attempt++) { patterns = await store.QueryAsync(new PatternStoreQueryParams { MaxCount = 10 }, queryCts.Token); if (patterns.Any(p => p.EventType == "repeated-edits")) diff --git a/src/Nexo.Tests.Infrastructure/Tests/Onboarding/OnboardingE2ETests.cs b/src/Nexo.Tests.Infrastructure/Tests/Onboarding/OnboardingE2ETests.cs index fa9495061..36c1dd92e 100644 --- a/src/Nexo.Tests.Infrastructure/Tests/Onboarding/OnboardingE2ETests.cs +++ b/src/Nexo.Tests.Infrastructure/Tests/Onboarding/OnboardingE2ETests.cs @@ -40,8 +40,9 @@ public void Dispose() // ── Provider Availability ──────────────────────────────────────── [Fact(Timeout = TestTimeouts.E2E)] - public void ProviderDetection_MockDisabledByDefault() + public async Task ProviderDetection_MockDisabledByDefault() { + await Task.CompletedTask; var prev = Environment.GetEnvironmentVariable("NEXO_ALLOW_MOCK"); try { @@ -59,8 +60,9 @@ public void ProviderDetection_MockDisabledByDefault() } [Fact(Timeout = TestTimeouts.E2E)] - public void ProviderDetection_MockEnabledByEnvVar() + public async Task ProviderDetection_MockEnabledByEnvVar() { + await Task.CompletedTask; var prev = Environment.GetEnvironmentVariable("NEXO_ALLOW_MOCK"); try { @@ -78,8 +80,9 @@ public void ProviderDetection_MockEnabledByEnvVar() } [Fact(Timeout = TestTimeouts.E2E)] - public void ProviderDetection_OpenAiRequiresApiKey() + public async Task ProviderDetection_OpenAiRequiresApiKey() { + await Task.CompletedTask; var prevKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY"); try { @@ -98,8 +101,9 @@ public void ProviderDetection_OpenAiRequiresApiKey() } [Fact(Timeout = TestTimeouts.E2E)] - public void ProviderDetection_AzureRequiresAllThreeVars() + public async Task ProviderDetection_AzureRequiresAllThreeVars() { + await Task.CompletedTask; var prevE = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT"); var prevK = Environment.GetEnvironmentVariable("AZURE_OPENAI_API_KEY"); var prevD = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT"); @@ -124,8 +128,9 @@ public void ProviderDetection_AzureRequiresAllThreeVars() } [Fact(Timeout = TestTimeouts.E2E)] - public void ProviderDetection_LocalRequiresModelPath() + public async Task ProviderDetection_LocalRequiresModelPath() { + await Task.CompletedTask; var prevPath = Environment.GetEnvironmentVariable("NEXO_LOCAL_MODEL_PATH"); try { @@ -140,8 +145,9 @@ public void ProviderDetection_LocalRequiresModelPath() } [Fact(Timeout = TestTimeouts.E2E)] - public void ProviderDetection_AllProviderNames_DoNotThrow() + public async Task ProviderDetection_AllProviderNames_DoNotThrow() { + await Task.CompletedTask; var factory = CreateProviderFactory(); var names = new[] { "openai", "azure", "ollama", "local", "video", "mock", "offline", "mock-json", "echo", "", " ", "nonexistent" }; foreach (var name in names) @@ -306,8 +312,9 @@ public async Task ConfigResolution_PartialJson_LoadsWithNulls() // ── DI Host Onboarding ─────────────────────────────────────────── [Fact(Timeout = TestTimeouts.E2E)] - public void HostBootstrap_FullProfile_ResolvesAllOnboardingServices() + public async Task HostBootstrap_FullProfile_ResolvesAllOnboardingServices() { + await Task.CompletedTask; var services = new ServiceCollection(); services.AddLogging(); services.AddNexo(); @@ -319,8 +326,9 @@ public void HostBootstrap_FullProfile_ResolvesAllOnboardingServices() } [Fact(Timeout = TestTimeouts.E2E)] - public void HostBootstrap_StrictModeEnabled_PropagatesConfigFailure() + public async Task HostBootstrap_StrictModeEnabled_PropagatesConfigFailure() { + await Task.CompletedTask; var prev = Environment.GetEnvironmentVariable("NEXO_CONFIG_PATH"); try { @@ -332,7 +340,7 @@ public void HostBootstrap_StrictModeEnabled_PropagatesConfigFailure() var configService = sp.GetRequiredService(); var act = async () => await configService.LoadAsync(); - act.Should().ThrowAsync(); + await act.Should().ThrowAsync(); } finally { @@ -370,8 +378,9 @@ public async Task HostBootstrap_MockProvider_CanExecuteFirstTask() [InlineData(NexoDeploymentProfile.Edge)] [InlineData(NexoDeploymentProfile.AirGapped)] [InlineData(NexoDeploymentProfile.System)] - public void HostBootstrap_AllProfiles_ResolveConfigService(NexoDeploymentProfile profile) + public async Task HostBootstrap_AllProfiles_ResolveConfigService(NexoDeploymentProfile profile) { + await Task.CompletedTask; var services = new ServiceCollection(); services.AddLogging(); services.AddNexoProfile(profile); @@ -385,8 +394,9 @@ public void HostBootstrap_AllProfiles_ResolveConfigService(NexoDeploymentProfile // ── Defaults Alignment ─────────────────────────────────────────── [Fact(Timeout = TestTimeouts.E2E)] - public void Defaults_ConfigService_MatchesNexoDefaults() + public async Task Defaults_ConfigService_MatchesNexoDefaults() { + await Task.CompletedTask; var logger = new LoggerFactory().CreateLogger(); var adapter = new ConfigurationServiceAdapter(logger); var defaults = adapter.GetDefault(); @@ -398,8 +408,9 @@ public void Defaults_ConfigService_MatchesNexoDefaults() } [Fact(Timeout = TestTimeouts.E2E)] - public void Defaults_StrictMode_DisabledByDefault() + public async Task Defaults_StrictMode_DisabledByDefault() { + await Task.CompletedTask; var services = new ServiceCollection(); services.AddLogging(); services.AddNexo(); @@ -413,8 +424,9 @@ public void Defaults_StrictMode_DisabledByDefault() } [Fact(Timeout = TestTimeouts.E2E)] - public void Defaults_StrictMode_FromEnvVar() + public async Task Defaults_StrictMode_FromEnvVar() { + await Task.CompletedTask; var prev = Environment.GetEnvironmentVariable("NEXO_STRICT_MODE"); try { diff --git a/src/Nexo.Tests.Infrastructure/Tests/Pipelines/PipelineLifecycleE2ETests.cs b/src/Nexo.Tests.Infrastructure/Tests/Pipelines/PipelineLifecycleE2ETests.cs index 1be7b3f96..509d6c628 100644 --- a/src/Nexo.Tests.Infrastructure/Tests/Pipelines/PipelineLifecycleE2ETests.cs +++ b/src/Nexo.Tests.Infrastructure/Tests/Pipelines/PipelineLifecycleE2ETests.cs @@ -67,8 +67,9 @@ private static PipelineTemplate SingleStageTemplate(string id) => }; [Fact(Timeout = TestTimeouts.E2E)] - public void DefaultOptions_UseNexoDefaults() + public async Task DefaultOptions_UseNexoDefaults() { + await Task.CompletedTask; var opts = new PipelineExecutionOptions(); opts.MaxRetryAttempts.Should().Be(NexoDefaults.PipelineMaxRetryAttempts); opts.RetryDelayMs.Should().Be(NexoDefaults.PipelineRetryDelayMs); @@ -228,8 +229,9 @@ await orchestrator.RunAsync(new PipelineExecutionRequest } [Fact(Timeout = TestTimeouts.E2E)] - public void CompletionPolicy_EnumValues() + public async Task CompletionPolicy_EnumValues() { + await Task.CompletedTask; Enum.GetValues().Should().HaveCount(2); } diff --git a/src/Nexo.Tests.Infrastructure/Tests/Runtime/InMemoryAgentMemoryTests.cs b/src/Nexo.Tests.Infrastructure/Tests/Runtime/InMemoryAgentMemoryTests.cs index 1b272eaac..5a5e9f015 100644 --- a/src/Nexo.Tests.Infrastructure/Tests/Runtime/InMemoryAgentMemoryTests.cs +++ b/src/Nexo.Tests.Infrastructure/Tests/Runtime/InMemoryAgentMemoryTests.cs @@ -8,8 +8,9 @@ namespace Nexo.Tests.Infrastructure.Tests.Runtime; public sealed class InMemoryAgentMemoryTests { [Fact(Timeout = 15000)] - public void Write_ThenQuery_ReturnsMatchingEvents() + public async Task Write_ThenQuery_ReturnsMatchingEvents() { + await Task.CompletedTask; var memory = new InMemoryAgentMemory(); memory.Write(new EventRecord(DateTimeOffset.UtcNow, "agent-1", "info", "Hello world")); @@ -22,8 +23,9 @@ public void Write_ThenQuery_ReturnsMatchingEvents() } [Fact(Timeout = 15000)] - public void Query_IsCaseInsensitive() + public async Task Query_IsCaseInsensitive() { + await Task.CompletedTask; var memory = new InMemoryAgentMemory(); memory.Write(new EventRecord(DateTimeOffset.UtcNow, "a", "info", "Build Succeeded")); @@ -33,8 +35,9 @@ public void Query_IsCaseInsensitive() } [Fact(Timeout = 15000)] - public void Query_RespectsKLimit() + public async Task Query_RespectsKLimit() { + await Task.CompletedTask; var memory = new InMemoryAgentMemory(); for (var i = 0; i < 10; i++) @@ -49,8 +52,9 @@ public void Query_RespectsKLimit() } [Fact(Timeout = 15000)] - public void Query_OrdersByTimestampDescending() + public async Task Query_OrdersByTimestampDescending() { + await Task.CompletedTask; var memory = new InMemoryAgentMemory(); var t1 = DateTimeOffset.UtcNow.AddMinutes(-5); var t2 = DateTimeOffset.UtcNow.AddMinutes(-1); @@ -69,8 +73,9 @@ public void Query_OrdersByTimestampDescending() } [Fact(Timeout = 15000)] - public void Query_NoMatches_ReturnsEmpty() + public async Task Query_NoMatches_ReturnsEmpty() { + await Task.CompletedTask; var memory = new InMemoryAgentMemory(); memory.Write(new EventRecord(DateTimeOffset.UtcNow, "a", "info", "Alpha")); diff --git a/src/Nexo.Tests.Orchestration/Tests/NegotiationHelpersTests.cs b/src/Nexo.Tests.Orchestration/Tests/NegotiationHelpersTests.cs index cbe12ce4d..d47ef627f 100644 --- a/src/Nexo.Tests.Orchestration/Tests/NegotiationHelpersTests.cs +++ b/src/Nexo.Tests.Orchestration/Tests/NegotiationHelpersTests.cs @@ -25,8 +25,9 @@ public NegotiationHelpersTests() // ── ParetoOptimizer ──────────────────────────────────────────── [Fact(Timeout = 15000)] - public void ParetoOptimizer_WithinBudget_ReturnsNoConflict() + public async Task ParetoOptimizer_WithinBudget_ReturnsNoConflict() { + await Task.CompletedTask; var optimizer = CreateParetoOptimizer(); var agents = CreateAgentsWithResources( ("a1", compute: 100, memory: 200), @@ -44,8 +45,9 @@ public void ParetoOptimizer_WithinBudget_ReturnsNoConflict() } [Fact(Timeout = 15000)] - public void ParetoOptimizer_ExceedsBudget_ReturnsParetoFrontier() + public async Task ParetoOptimizer_ExceedsBudget_ReturnsParetoFrontier() { + await Task.CompletedTask; var optimizer = CreateParetoOptimizer(); var agents = CreateAgentsWithResources( ("a1", compute: 600, memory: 400), @@ -66,8 +68,9 @@ public void ParetoOptimizer_ExceedsBudget_ReturnsParetoFrontier() } [Fact(Timeout = 15000)] - public void ParetoOptimizer_RecommendedAllocation_RespectsBudget() + public async Task ParetoOptimizer_RecommendedAllocation_RespectsBudget() { + await Task.CompletedTask; var optimizer = CreateParetoOptimizer(); var agents = CreateAgentsWithResources( ("a1", compute: 800, memory: 600), @@ -93,8 +96,9 @@ public void ParetoOptimizer_RecommendedAllocation_RespectsBudget() // ── ConstraintRelaxer ────────────────────────────────────────── [Fact(Timeout = 15000)] - public void ConstraintRelaxer_HardConflict_ReturnsUnresolvable() + public async Task ConstraintRelaxer_HardConflict_ReturnsUnresolvable() { + await Task.CompletedTask; var relaxer = CreateConstraintRelaxer(); var positions = new List @@ -129,8 +133,9 @@ public void ConstraintRelaxer_HardConflict_ReturnsUnresolvable() } [Fact(Timeout = 15000)] - public void ConstraintRelaxer_FlexibleSoftConstraints_RelaxesSuccessfully() + public async Task ConstraintRelaxer_FlexibleSoftConstraints_RelaxesSuccessfully() { + await Task.CompletedTask; var relaxer = CreateConstraintRelaxer(); var positions = new List @@ -174,8 +179,9 @@ public void ConstraintRelaxer_FlexibleSoftConstraints_RelaxesSuccessfully() } [Fact(Timeout = 15000)] - public void ConstraintRelaxer_LowFlexibility_SkipsConstraint() + public async Task ConstraintRelaxer_LowFlexibility_SkipsConstraint() { + await Task.CompletedTask; var relaxer = CreateConstraintRelaxer(); var positions = new List diff --git a/src/Nexo.Tests.Orchestration/Tests/OrchestrationMetricsTests.cs b/src/Nexo.Tests.Orchestration/Tests/OrchestrationMetricsTests.cs index 2fa043d67..c21909998 100644 --- a/src/Nexo.Tests.Orchestration/Tests/OrchestrationMetricsTests.cs +++ b/src/Nexo.Tests.Orchestration/Tests/OrchestrationMetricsTests.cs @@ -18,8 +18,9 @@ public OrchestrationMetricsTests() } [Fact(Timeout = 15000)] - public void RecordOperation_StoresMetrics() + public async Task RecordOperation_StoresMetrics() { + await Task.CompletedTask; _sut.RecordOperation("test-op", TimeSpan.FromMilliseconds(100)); var report = _sut.GetPerformanceReport(); @@ -30,8 +31,9 @@ public void RecordOperation_StoresMetrics() } [Fact(Timeout = 15000)] - public void RecordOperation_MultipleCalls_AggregatesCorrectly() + public async Task RecordOperation_MultipleCalls_AggregatesCorrectly() { + await Task.CompletedTask; _sut.RecordOperation("op", TimeSpan.FromMilliseconds(100)); _sut.RecordOperation("op", TimeSpan.FromMilliseconds(200)); _sut.RecordOperation("op", TimeSpan.FromMilliseconds(50)); @@ -46,8 +48,9 @@ public void RecordOperation_MultipleCalls_AggregatesCorrectly() } [Fact(Timeout = 15000)] - public void RecordAgentExecution_StoresAndRetrieves() + public async Task RecordAgentExecution_StoresAndRetrieves() { + await Task.CompletedTask; _sut.RecordAgentExecution( "agent-1", "code-gen", TimeSpan.FromSeconds(5), @@ -66,14 +69,16 @@ public void RecordAgentExecution_StoresAndRetrieves() } [Fact(Timeout = 15000)] - public void GetAgentMetrics_NonExistent_ReturnsNull() + public async Task GetAgentMetrics_NonExistent_ReturnsNull() { + await Task.CompletedTask; _sut.GetAgentMetrics("does-not-exist").Should().BeNull(); } [Fact(Timeout = 15000)] - public void StartSpan_EndSpan_RecordsTrace() + public async Task StartSpan_EndSpan_RecordsTrace() { + await Task.CompletedTask; var spanId = _sut.StartSpan("my-op", tags: new Dictionary { ["correlationId"] = "corr-1" @@ -88,8 +93,9 @@ public void StartSpan_EndSpan_RecordsTrace() } [Fact(Timeout = 15000)] - public void GetTraces_FiltersByOperationName() + public async Task GetTraces_FiltersByOperationName() { + await Task.CompletedTask; var s1 = _sut.StartSpan("alpha"); var s2 = _sut.StartSpan("beta"); _sut.EndSpan(s1); @@ -101,8 +107,9 @@ public void GetTraces_FiltersByOperationName() } [Fact(Timeout = 15000)] - public void Clear_RemovesAllMetrics() + public async Task Clear_RemovesAllMetrics() { + await Task.CompletedTask; _sut.RecordOperation("op", TimeSpan.FromMilliseconds(10)); _sut.RecordAgentExecution("a1", "d", TimeSpan.FromSeconds(1), AgentState.Completed); _sut.StartSpan("span"); @@ -116,8 +123,9 @@ public void Clear_RemovesAllMetrics() } [Fact(Timeout = 15000)] - public void PerformanceReport_AgentsOrderedByExecutionCount() + public async Task PerformanceReport_AgentsOrderedByExecutionCount() { + await Task.CompletedTask; _sut.RecordAgentExecution("few", "d", TimeSpan.FromSeconds(1), AgentState.Completed); _sut.RecordAgentExecution("many", "d", TimeSpan.FromSeconds(1), AgentState.Completed); _sut.RecordAgentExecution("many", "d", TimeSpan.FromSeconds(1), AgentState.Completed);