From 10baea3b70773f69078c9bcd368673f6c7a3a3ec Mon Sep 17 00:00:00 2001 From: "louis.li" Date: Fri, 9 Oct 2026 15:54:13 +0800 Subject: [PATCH 1/3] Decouple workflow admission from credential transport. Co-Authored-By: Claude Opus 4.6 --- ...orkflowCapabilityAdmissionPlanIntegrity.cs | 30 +++- ...kflowExternalCapabilityAdmissionService.cs | 16 +- ...owCapabilityAdmissionPlanIntegrityTests.cs | 21 ++- ...ExternalCapabilityAdmissionServiceTests.cs | 161 ++++++++++++++++++ 4 files changed, 216 insertions(+), 12 deletions(-) diff --git a/src/workflow/Aevatar.Workflow.Abstractions/WorkflowCapabilityAdmissionPlanIntegrity.cs b/src/workflow/Aevatar.Workflow.Abstractions/WorkflowCapabilityAdmissionPlanIntegrity.cs index 1b041c263b..aa3f901fd1 100644 --- a/src/workflow/Aevatar.Workflow.Abstractions/WorkflowCapabilityAdmissionPlanIntegrity.cs +++ b/src/workflow/Aevatar.Workflow.Abstractions/WorkflowCapabilityAdmissionPlanIntegrity.cs @@ -274,8 +274,7 @@ private static CompatibilityEvaluation EvaluateCompatibility( "Workflow capability admission cannot contain legacy external capabilities.")); } - if (executionMode == ExternalCapabilityExecutionMode.Unspecified || - plan.ExecutionMode != executionMode) + if (!IsPlanExecutionModeCompatible(plan.ExecutionMode, executionMode)) { return Failed( WorkflowCapabilityAdmissionCompatibilityFailure.ExecutionModeMismatch, @@ -318,7 +317,9 @@ private static CompatibilityEvaluation EvaluateCompatibility( var actual = plan.InvocationAdmissions.ToArray(); try { - ValidateInvocationAdmissions(actual, executionMode); + ValidateInvocationAdmissions(actual, plan.ExecutionMode); + if (plan.ExecutionMode != executionMode) + ValidateInvocationAdmissions(actual, executionMode); } catch (Exception exception) when (IsStructuralValidationException(exception)) { @@ -405,10 +406,13 @@ private static CompatibilityEvaluation EvaluateCompatibility( .Select(static admission => admission.Capability) .ToArray(); - var requiresDurableAuthorizationCatalog = RequiresDurableAuthorizationCatalog( + var requiresStoredDurableAuthorizationCatalog = RequiresDurableAuthorizationCatalog( + plan.ExecutionMode, + expectedCapabilityArray); + var requiresRequestedDurableAuthorizationCatalog = RequiresDurableAuthorizationCatalog( executionMode, expectedCapabilityArray); - if (requiresDurableAuthorizationCatalog && + if (requiresRequestedDurableAuthorizationCatalog && !HasDurableAuthorizationCatalogSource(plan.SourceStamps)) { return Failed( @@ -416,14 +420,17 @@ private static CompatibilityEvaluation EvaluateCompatibility( new InvalidOperationException( "Workflow capability admission durable authorization catalog source is required.")); } - if (!HasRequiredSourceEvidence(executionMode, expectedCapabilityArray, plan.SourceStamps)) + var requiredSourceEvidenceMode = requiresStoredDurableAuthorizationCatalog + ? plan.ExecutionMode + : executionMode; + if (!HasRequiredSourceEvidence(requiredSourceEvidenceMode, expectedCapabilityArray, plan.SourceStamps)) { return Failed( WorkflowCapabilityAdmissionCompatibilityFailure.RequiredSourceMissing, new InvalidOperationException( "Workflow capability admission required source evidence is invalid.")); } - if (requiresDurableAuthorizationCatalog) + if (requiresStoredDurableAuthorizationCatalog || requiresRequestedDurableAuthorizationCatalog) { if (!IsCanonicalDurableAuthorizationOwner(plan.DurableAuthorizationOwner)) { @@ -454,6 +461,15 @@ private static CompatibilityEvaluation EvaluateCompatibility( null); } + private static bool IsPlanExecutionModeCompatible( + ExternalCapabilityExecutionMode planExecutionMode, + ExternalCapabilityExecutionMode expectedExecutionMode) => + expectedExecutionMode != ExternalCapabilityExecutionMode.Unspecified && + planExecutionMode is ExternalCapabilityExecutionMode.Interactive or ExternalCapabilityExecutionMode.Durable && + (planExecutionMode == expectedExecutionMode || + planExecutionMode == ExternalCapabilityExecutionMode.Durable && + expectedExecutionMode == ExternalCapabilityExecutionMode.Interactive); + public static IReadOnlyList DistinctCapabilities( WorkflowCapabilityAdmissionPlan plan) { diff --git a/src/workflow/Aevatar.Workflow.Application/ExternalCapabilities/WorkflowExternalCapabilityAdmissionService.cs b/src/workflow/Aevatar.Workflow.Application/ExternalCapabilities/WorkflowExternalCapabilityAdmissionService.cs index d7a8e1ba8b..575ccc4e5a 100644 --- a/src/workflow/Aevatar.Workflow.Application/ExternalCapabilities/WorkflowExternalCapabilityAdmissionService.cs +++ b/src/workflow/Aevatar.Workflow.Application/ExternalCapabilities/WorkflowExternalCapabilityAdmissionService.cs @@ -453,29 +453,37 @@ public async Task RefreshPersistedAsync( var confirmations = request.ExplicitRequestConfirmations.Count > 0 ? request.ExplicitRequestConfirmations : RestorePersistedExplicitRequestConfirmations(persisted, request.Access); + var admissionMode = ResolveRefreshAdmissionMode(persisted); var liveRequest = persisted.WorkflowYamls is { Count: > 0 } workflowYamls ? WorkflowExternalCapabilityAdmissionRequest.FromWorkflowYamls( request.Access, workflowYamls, persisted.SourceKind, - persisted.ExpectedExecutionMode, + admissionMode, confirmations, persisted.WorkflowId, persisted.RevisionId, - persisted.ExpectedExecutionMode) + admissionMode) : new WorkflowExternalCapabilityAdmissionRequest( request.Access, persisted.WorkflowYaml, persisted.InlineWorkflowYamls, persisted.SourceKind, - persisted.ExpectedExecutionMode, + admissionMode, confirmations, persisted.WorkflowId, persisted.RevisionId, - persisted.ExpectedExecutionMode); + admissionMode); return await AdmitAsync(liveRequest, cancellationToken); } + private static ExternalCapabilityExecutionMode ResolveRefreshAdmissionMode( + PersistedWorkflowCapabilityAdmissionRequest persisted) => + persisted.Plan.ExecutionMode == ExternalCapabilityExecutionMode.Durable && + persisted.ExpectedExecutionMode == ExternalCapabilityExecutionMode.Interactive + ? ExternalCapabilityExecutionMode.Durable + : persisted.ExpectedExecutionMode; + private async Task ValidatePersistedIntegrityAsync( PersistedWorkflowCapabilityAdmissionRequest request, CancellationToken cancellationToken) diff --git a/test/Aevatar.Workflow.Application.Tests/WorkflowCapabilityAdmissionPlanIntegrityTests.cs b/test/Aevatar.Workflow.Application.Tests/WorkflowCapabilityAdmissionPlanIntegrityTests.cs index 956e9ddf1f..118b66d089 100644 --- a/test/Aevatar.Workflow.Application.Tests/WorkflowCapabilityAdmissionPlanIntegrityTests.cs +++ b/test/Aevatar.Workflow.Application.Tests/WorkflowCapabilityAdmissionPlanIntegrityTests.cs @@ -10,7 +10,7 @@ public sealed class WorkflowCapabilityAdmissionPlanIntegrityTests [Theory] [InlineData("schema", WorkflowCapabilityAdmissionCompatibilityFailure.SchemaMismatch)] - [InlineData("mode", WorkflowCapabilityAdmissionCompatibilityFailure.ExecutionModeMismatch)] + [InlineData("mode", WorkflowCapabilityAdmissionCompatibilityFailure.AdmissionDigestMismatch)] [InlineData("definition", WorkflowCapabilityAdmissionCompatibilityFailure.DefinitionDigestMismatch)] [InlineData("call-site-count", WorkflowCapabilityAdmissionCompatibilityFailure.InvocationMismatch)] [InlineData("call-site-order", WorkflowCapabilityAdmissionCompatibilityFailure.InvocationOrderingInvalid)] @@ -95,6 +95,25 @@ public void CheckCompatibility_WithMutatedPlan_ShouldReturnTypedFailure( result.Failure.Should().Be(expected); } + [Fact] + public void CheckCompatibility_WithInteractivePlanForDurableExpectedMode_ShouldRejectMode() + { + var fixture = ExplicitRequestFixture(ExternalCapabilityExecutionMode.Interactive); + + var result = WorkflowCapabilityAdmissionPlanIntegrity.CheckCompatibility( + fixture.Plan, + fixture.WorkflowYaml, + fixture.InlineWorkflowYamls, + ExternalCapabilityExecutionMode.Durable, + fixture.ExpectedInvocations, + fixture.WorkflowId, + fixture.RevisionId); + + result.Succeeded.Should().BeFalse(); + result.Failure.Should().Be( + WorkflowCapabilityAdmissionCompatibilityFailure.ExecutionModeMismatch); + } + [Theory] [InlineData(WorkflowCapabilityAdmissionPlanIntegrity.LegacySchemaVersion)] [InlineData(WorkflowCapabilityAdmissionPlanIntegrity.OpenApiSchemaVersion)] diff --git a/test/Aevatar.Workflow.Application.Tests/WorkflowExternalCapabilityAdmissionServiceTests.cs b/test/Aevatar.Workflow.Application.Tests/WorkflowExternalCapabilityAdmissionServiceTests.cs index ce502dfbd4..585f3214b5 100644 --- a/test/Aevatar.Workflow.Application.Tests/WorkflowExternalCapabilityAdmissionServiceTests.cs +++ b/test/Aevatar.Workflow.Application.Tests/WorkflowExternalCapabilityAdmissionServiceTests.cs @@ -1711,6 +1711,122 @@ public async Task RevalidatePersistedAsync_ShouldAcceptDurablePlanWithoutTransie readiness.Calls.Should().Be(1); } + [Fact] + public async Task RevalidatePersistedAsync_ShouldAcceptDurableExplicitRequestPlanForInteractiveExecution() + { + const string yaml = "name: explicit-workflow\nsteps: []\n"; + var selector = ExplicitSelector(); + var plan = DurableExplicitPlan(selector); + var service = new WorkflowExternalCapabilityAdmissionService( + new StubParser(WorkflowYamlParseResult.Success("explicit-workflow", ExplicitDependencies(selector))), + new StubReadinessPort(), + new FixedTimeProvider()); + + var verified = await service.RevalidatePersistedAsync( + new PersistedWorkflowCapabilityAdmissionRequest( + plan, + yaml, + new Dictionary(), + "service_revision_prepare", + ExternalCapabilityExecutionMode.Interactive, + ExplicitWorkflowId, + ExplicitRevisionId)); + + verified.Should().BeEquivalentTo(plan); + } + + [Fact] + public async Task RevalidatePersistedAsync_ShouldRejectInteractiveUseOfDurableExplicitPlanWithoutCatalogSource() + { + const string yaml = "name: explicit-workflow\nsteps: []\n"; + var selector = ExplicitSelector(); + var plan = DurableExplicitPlan(selector); + var durableSource = plan.SourceStamps.Single(static source => + source.SourceKind == ExternalCapabilitySourceKind.DurableAuthorizationCatalog); + plan.SourceStamps.Remove(durableSource); + plan.AdmissionDigest = WorkflowCapabilityAdmissionPlanIntegrity.ComputeAdmissionDigest(plan); + var service = new WorkflowExternalCapabilityAdmissionService( + new StubParser(WorkflowYamlParseResult.Success("explicit-workflow", ExplicitDependencies(selector))), + new StubReadinessPort(), + new FixedTimeProvider()); + + Func act = async () => await service.RevalidatePersistedAsync( + new PersistedWorkflowCapabilityAdmissionRequest( + plan, + yaml, + new Dictionary(), + "service_revision_prepare", + ExternalCapabilityExecutionMode.Interactive, + ExplicitWorkflowId, + ExplicitRevisionId)); + + await act.Should().ThrowAsync() + .WithMessage("*required source evidence*"); + } + + [Fact] + public async Task RevalidatePersistedAsync_ShouldRejectInteractiveUseOfDurableExplicitPlanWithoutOwner() + { + const string yaml = "name: explicit-workflow\nsteps: []\n"; + var selector = ExplicitSelector(); + var plan = DurableExplicitPlan(selector); + plan.DurableAuthorizationOwner = null; + plan.AdmissionDigest = WorkflowCapabilityAdmissionPlanIntegrity.ComputeAdmissionDigest(plan); + var service = new WorkflowExternalCapabilityAdmissionService( + new StubParser(WorkflowYamlParseResult.Success("explicit-workflow", ExplicitDependencies(selector))), + new StubReadinessPort(), + new FixedTimeProvider()); + + Func act = async () => await service.RevalidatePersistedAsync( + new PersistedWorkflowCapabilityAdmissionRequest( + plan, + yaml, + new Dictionary(), + "service_revision_prepare", + ExternalCapabilityExecutionMode.Interactive, + ExplicitWorkflowId, + ExplicitRevisionId)); + + await act.Should().ThrowAsync() + .WithMessage("*durable authorization owner is invalid*"); + } + + [Fact] + public async Task RefreshPersistedAsync_ShouldPreserveDurableExplicitAdmissionForInteractiveExpectedMode() + { + const string yaml = "name: explicit-workflow\nsteps: []\n"; + var selector = ExplicitSelector(); + var plan = DurableExplicitPlan(selector); + var capability = ExplicitCapability(selector.NyxIdRequest); + var readiness = DurableExplicitReady(selector, capability); + var service = new WorkflowExternalCapabilityAdmissionService( + new StubParser(WorkflowYamlParseResult.Success("explicit-workflow", ExplicitDependencies(selector))), + new StubReadinessPort(readiness), + new FixedTimeProvider()); + var access = new ExternalWorkflowCapabilityAccessContext( + "scope-alpha", + "caller-alpha", + NyxIdCallerCredentialSelection.SourceReadableUserBearer("runtime-caller-credential")); + + var refreshed = await service.RefreshPersistedAsync( + new RefreshPersistedWorkflowCapabilityAdmissionRequest( + new PersistedWorkflowCapabilityAdmissionRequest( + plan, + yaml, + new Dictionary(), + "service_revision_prepare", + ExternalCapabilityExecutionMode.Interactive, + ExplicitWorkflowId, + ExplicitRevisionId), + access)); + + refreshed.ExecutionMode.Should().Be(ExternalCapabilityExecutionMode.Durable); + refreshed.InvocationAdmissions.Should().ContainSingle().Which + .NyxIdExplicitRequestGrant.AllowedExecutionModes.Should() + .Contain(ExternalCapabilityExecutionMode.Durable); + refreshed.DurableAuthorizationOwner.Should().BeEquivalentTo(DurableOwner()); + } + [Fact] public async Task RevalidatePersistedAsync_ShouldRejectDurableNyxIdPlanWithoutCatalogSource() { @@ -2217,6 +2333,51 @@ private static ExternalToolInvocationSpec ExplicitInvocation( Selector = selector, }; + private static WorkflowAuthorizationDependencies ExplicitDependencies( + ExternalWorkflowCapabilitySelector selector) + { + var dependencies = new WorkflowAuthorizationDependencies + { + ServiceGrantPolicy = WorkflowServiceGrantPolicy.Required, + }; + dependencies.ExternalInvocations.Add(ExplicitInvocation(selector)); + return dependencies; + } + + private static WorkflowCapabilityAdmissionPlan DurableExplicitPlan( + ExternalWorkflowCapabilitySelector selector) + { + var admission = ExplicitAdmission(ExplicitCapability(selector.NyxIdRequest)); + admission.NyxIdExplicitRequestGrant.GrantorOwnerSubject = "caller-alpha"; + admission.Capability.NyxIdUserRequest.ExplicitRequestGrantDigest = + ExplicitGrantDigest(admission.NyxIdExplicitRequestGrant); + return WorkflowCapabilityAdmissionPlanIntegrity.Create( + "name: explicit-workflow\nsteps: []\n", + new Dictionary(), + ExternalCapabilityExecutionMode.Durable, + [admission], + [ExplicitSource(), DurableCatalogSource()], + DurableOwner(), + ExplicitWorkflowId, + ExplicitRevisionId); + } + + private static ExternalCapabilityReadiness DurableExplicitReady( + ExternalWorkflowCapabilitySelector selector, + ExternalWorkflowCapabilityRef capability) => + new() + { + ExecutionMode = ExternalCapabilityExecutionMode.Durable, + Status = ExternalCapabilityReadinessStatus.Ready, + SelectedSelector = selector.Clone(), + SelectedCapability = capability.Clone(), + Sources = + { + ExplicitSource(), + DurableCatalogSource(), + }, + }; + private static ExternalCapabilitySourceStamp ExplicitSource() => new() { From 8891db29e589ceacfb80d36ca4bd6942844477cc Mon Sep 17 00:00:00 2001 From: "louis.li" Date: Fri, 9 Oct 2026 16:10:35 +0800 Subject: [PATCH 2/3] Align workflow artifact mode compatibility. Co-Authored-By: Claude Opus 4.6 --- .../WorkflowServiceDeploymentPlanIntegrity.cs | 8 ++-- .../WorkflowServiceRevisionArtifactBuilder.cs | 6 +-- ...orkflowCapabilityAdmissionPlanIntegrity.cs | 4 +- ...flowServiceRevisionArtifactBuilderTests.cs | 46 +++++++++++++++++-- 4 files changed, 52 insertions(+), 12 deletions(-) diff --git a/src/platform/Aevatar.GAgentService.Abstractions/Services/WorkflowServiceDeploymentPlanIntegrity.cs b/src/platform/Aevatar.GAgentService.Abstractions/Services/WorkflowServiceDeploymentPlanIntegrity.cs index 4b321c6a75..662b867597 100644 --- a/src/platform/Aevatar.GAgentService.Abstractions/Services/WorkflowServiceDeploymentPlanIntegrity.cs +++ b/src/platform/Aevatar.GAgentService.Abstractions/Services/WorkflowServiceDeploymentPlanIntegrity.cs @@ -51,10 +51,10 @@ public static WorkflowServiceBindingIdentity ResolveBindingIdentity( throw new InvalidOperationException( "Workflow service must be reissued under the current tool catalog policy."); } - if (plan.ExecutionMode == ExternalCapabilityExecutionMode.Unspecified || - !Enum.IsDefined(plan.ExecutionMode) || - plan.CapabilityAdmissionPlan == null || - plan.ExecutionMode != plan.CapabilityAdmissionPlan.ExecutionMode) + if (plan.CapabilityAdmissionPlan == null || + !WorkflowCapabilityAdmissionPlanIntegrity.IsExecutionModeCompatible( + plan.CapabilityAdmissionPlan.ExecutionMode, + plan.ExecutionMode)) { throw new InvalidOperationException( "Workflow service deployment execution mode must match the capability admission plan."); diff --git a/src/platform/Aevatar.GAgentService.Abstractions/Services/WorkflowServiceRevisionArtifactBuilder.cs b/src/platform/Aevatar.GAgentService.Abstractions/Services/WorkflowServiceRevisionArtifactBuilder.cs index bac67dea5e..eea5ac22b6 100644 --- a/src/platform/Aevatar.GAgentService.Abstractions/Services/WorkflowServiceRevisionArtifactBuilder.cs +++ b/src/platform/Aevatar.GAgentService.Abstractions/Services/WorkflowServiceRevisionArtifactBuilder.cs @@ -42,9 +42,9 @@ public static PreparedServiceRevisionArtifact Build( throw new InvalidOperationException("workflow capability admission execution mode is required."); } - if (workflowSpec.ExpectedExecutionMode == ExternalCapabilityExecutionMode.Unspecified || - !Enum.IsDefined(workflowSpec.ExpectedExecutionMode) || - workflowSpec.ExpectedExecutionMode != capabilityAdmissionPlan.ExecutionMode) + if (!WorkflowCapabilityAdmissionPlanIntegrity.IsExecutionModeCompatible( + capabilityAdmissionPlan.ExecutionMode, + workflowSpec.ExpectedExecutionMode)) { throw new InvalidOperationException( "workflow expected execution mode must match the capability admission plan."); diff --git a/src/workflow/Aevatar.Workflow.Abstractions/WorkflowCapabilityAdmissionPlanIntegrity.cs b/src/workflow/Aevatar.Workflow.Abstractions/WorkflowCapabilityAdmissionPlanIntegrity.cs index aa3f901fd1..da2654afcc 100644 --- a/src/workflow/Aevatar.Workflow.Abstractions/WorkflowCapabilityAdmissionPlanIntegrity.cs +++ b/src/workflow/Aevatar.Workflow.Abstractions/WorkflowCapabilityAdmissionPlanIntegrity.cs @@ -274,7 +274,7 @@ private static CompatibilityEvaluation EvaluateCompatibility( "Workflow capability admission cannot contain legacy external capabilities.")); } - if (!IsPlanExecutionModeCompatible(plan.ExecutionMode, executionMode)) + if (!IsExecutionModeCompatible(plan.ExecutionMode, executionMode)) { return Failed( WorkflowCapabilityAdmissionCompatibilityFailure.ExecutionModeMismatch, @@ -461,7 +461,7 @@ private static CompatibilityEvaluation EvaluateCompatibility( null); } - private static bool IsPlanExecutionModeCompatible( + public static bool IsExecutionModeCompatible( ExternalCapabilityExecutionMode planExecutionMode, ExternalCapabilityExecutionMode expectedExecutionMode) => expectedExecutionMode != ExternalCapabilityExecutionMode.Unspecified && diff --git a/test/Aevatar.GAgentService.Tests/Application/WorkflowServiceRevisionArtifactBuilderTests.cs b/test/Aevatar.GAgentService.Tests/Application/WorkflowServiceRevisionArtifactBuilderTests.cs index 9acc93670c..997a446b0e 100644 --- a/test/Aevatar.GAgentService.Tests/Application/WorkflowServiceRevisionArtifactBuilderTests.cs +++ b/test/Aevatar.GAgentService.Tests/Application/WorkflowServiceRevisionArtifactBuilderTests.cs @@ -127,19 +127,37 @@ public void Build_WithUnspecifiedExecutionMode_ShouldFailClosed() } [Fact] - public void Build_WhenExpectedModeDiffersFromAdmissionPlan_ShouldFailClosed() + public void Build_WithDurableAdmissionPlanForInteractiveExpectedMode_ShouldSucceed() { var plan = new WorkflowCapabilityAdmissionPlan { ExecutionMode = ExternalCapabilityExecutionMode.Durable, }; - var action = () => BuildArtifactWithIdentity( + var artifact = BuildArtifactWithIdentity( "wf-artifact-alpha", "rev-artifact-alpha", plan, ExternalCapabilityExecutionMode.Interactive); + artifact.DeploymentPlan.WorkflowPlan.CapabilityAdmissionPlan.ExecutionMode.Should() + .Be(ExternalCapabilityExecutionMode.Durable); + } + + [Fact] + public void Build_WithInteractiveAdmissionPlanForDurableExpectedMode_ShouldFailClosed() + { + var plan = new WorkflowCapabilityAdmissionPlan + { + ExecutionMode = ExternalCapabilityExecutionMode.Interactive, + }; + + var action = () => BuildArtifactWithIdentity( + "wf-artifact-alpha", + "rev-artifact-alpha", + plan, + ExternalCapabilityExecutionMode.Durable); + action.Should().Throw() .WithMessage("*execution mode*match*"); } @@ -171,11 +189,33 @@ public void Build_WithoutCurrentToolCatalogPolicy_ShouldFailClosed() } [Fact] - public void ResolveBindingIdentity_WhenDeploymentModeDiffersFromAdmissionPlan_ShouldFailClosed() + public void ResolveBindingIdentity_WithDurableAdmissionPlanForInteractiveDeploymentMode_ShouldSucceed() { var artifact = BuildArtifact(); artifact.DeploymentPlan.WorkflowPlan.ExecutionMode = ExternalCapabilityExecutionMode.Interactive; + var identity = WorkflowServiceDeploymentPlanIntegrity.ResolveBindingIdentity( + artifact, + "rev-artifact-alpha"); + + identity.WorkflowId.Should().Be("wf-artifact-alpha"); + identity.RevisionId.Should().Be("rev-artifact-alpha"); + } + + [Fact] + public void ResolveBindingIdentity_WithInteractiveAdmissionPlanForDurableDeploymentMode_ShouldFailClosed() + { + var plan = new WorkflowCapabilityAdmissionPlan + { + ExecutionMode = ExternalCapabilityExecutionMode.Interactive, + }; + var artifact = BuildArtifactWithIdentity( + "wf-artifact-alpha", + "rev-artifact-alpha", + plan, + ExternalCapabilityExecutionMode.Interactive); + artifact.DeploymentPlan.WorkflowPlan.ExecutionMode = ExternalCapabilityExecutionMode.Durable; + var action = () => WorkflowServiceDeploymentPlanIntegrity.ResolveBindingIdentity( artifact, "rev-artifact-alpha"); From 4d74b1f0fb09073bdb74ad91c5a5726c2cdd152a Mon Sep 17 00:00:00 2001 From: "louis.li" Date: Fri, 9 Oct 2026 16:27:22 +0800 Subject: [PATCH 3/3] Keep workflow draft API layout-free. Co-Authored-By: Claude Opus 4.6 --- .../Endpoints/WorkflowApiEndpoints.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/platform/Aevatar.GAgentService.Hosting/Endpoints/WorkflowApiEndpoints.cs b/src/platform/Aevatar.GAgentService.Hosting/Endpoints/WorkflowApiEndpoints.cs index fa18f8566f..7fe6ccf206 100644 --- a/src/platform/Aevatar.GAgentService.Hosting/Endpoints/WorkflowApiEndpoints.cs +++ b/src/platform/Aevatar.GAgentService.Hosting/Endpoints/WorkflowApiEndpoints.cs @@ -9,7 +9,6 @@ using Aevatar.Studio.Application.Studio.Abstractions; using Aevatar.Studio.Application.Studio.Contracts; using Aevatar.Studio.Application.Studio.Services; -using Aevatar.Studio.Domain.Studio.Models; using Aevatar.Workflow.Application.Abstractions.ExternalCapabilities; using Aevatar.Workflow.Application.Abstractions.Observatory; using Aevatar.Workflow.Application.Abstractions.Runs; @@ -100,7 +99,7 @@ private static async Task WriteDraft(HttpContext http, string? workflow if (!AevatarScopeAccessGuard.TryGetCallerScopeId(http, out var scopeId)) return Results.Unauthorized(); try { - var save = new SaveWorkflowDraftRequest(string.Empty, request.WorkflowName, request.FileName, request.Yaml, request.Layout); + var save = new SaveWorkflowDraftRequest(string.Empty, request.WorkflowName, request.FileName, request.Yaml, null); var receipt = workflowId == null ? await drafts.CreateDraftAsync(scopeId, save, ct) : await drafts.SaveKnownWorkflowDraftAsync(scopeId, workflowId, save, ct); @@ -201,7 +200,7 @@ private static IReadOnlyList SplitCsv(string? value) => string.IsNullOrW ? [] : value.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); private static DateTimeOffset? ParseTimestamp(string? value) => DateTimeOffset.TryParse(value, out var parsed) ? parsed : null; - public sealed record WorkflowDraftWriteRequest(string WorkflowName, string Yaml, string? FileName = null, WorkflowLayoutDocument? Layout = null); + public sealed record WorkflowDraftWriteRequest(string WorkflowName, string Yaml, string? FileName = null); public sealed record WorkflowDraftAcceptedResponse(string WorkflowId, string CommandId); public sealed record WorkflowPublicationAcceptedResponse(string WorkflowId, string RevisionId, DateTimeOffset AcceptedAtUtc, IReadOnlyList CommandHandles);