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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -947,6 +947,12 @@ WorkflowDeliveryAcceptanceMode.AutomaticPreview or
string.IsNullOrWhiteSpace(package.AcceptancePolicy.Limitation))
throw new InvalidOperationException("manual workflow delivery acceptance policy requires a limitation.");
WorkflowDeliveryConventions.ValidateAcceptanceInput(package.AcceptancePolicy.Input);
foreach (var slot in package.ConnectionSlots)
{
WorkflowDeliveryConventions.NormalizeRequired(slot.Key, "connection_slot.key");
WorkflowDeliveryConventions.NormalizeRequired(slot.ServiceSlug, "connection_slot.service_slug");
WorkflowDeliveryConventions.NormalizeRequired(slot.YamlPointer, "connection_slot.yaml_pointer");
}
var expectedPackageHash = WorkflowDeliveryConventions.ComputePackageHash(package);
if (!string.Equals(package.PackageHash, expectedPackageHash, StringComparison.Ordinal))
throw new InvalidOperationException("workflow delivery package hash does not match its immutable content.");
Expand All @@ -960,6 +966,11 @@ WorkflowDeliveryAcceptanceMode.AutomaticPreview or
throw new InvalidOperationException("workflow delivery variable keys must be unique.");
if (package.ConnectionSlots.Select(x => x.Key).Distinct(StringComparer.Ordinal).Count() != package.ConnectionSlots.Count)
throw new InvalidOperationException("workflow delivery connection slot keys must be unique.");
if (package.ConnectionSlots.Select(x => x.YamlPointer).Distinct(StringComparer.Ordinal).Count() != package.ConnectionSlots.Count)
throw new InvalidOperationException("workflow delivery connection slot yaml pointers must be unique.");
var variableYamlPointers = package.VariableSchema.Select(x => x.YamlPointer).ToHashSet(StringComparer.Ordinal);
if (package.ConnectionSlots.Any(x => variableYamlPointers.Contains(x.YamlPointer)))
throw new InvalidOperationException("workflow delivery connection slot yaml pointers must not overlap variable yaml pointers.");
}

private void ValidateInstallation(StartWorkflowInstallationCommand command)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ message WorkflowDeliveryConnectionSlotDefinition {
string label = 2;
string service_slug = 3;
bool required = 4;
string yaml_pointer = 5;
}

enum WorkflowDeliveryAcceptanceDateProjection {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,8 @@ public sealed record WorkflowDeliveryConnectionSlotDefinition(
string Key,
string Label,
string ServiceSlug,
bool Required);
bool Required,
string YamlPointer);

public enum WorkflowDeliveryAcceptanceDateProjection
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,17 +109,13 @@ public WorkflowDeliveryRenderResult Render(
}

var resolvedConnections = ResolveConnections(package, connectionReferences);
if (resolvedConnections.Count != 0)
foreach (var slot in package.ConnectionSlots)
{
var replacements = ReplaceUserServiceIds(
stream.Documents[0].RootNode,
resolvedConnections.Values.Single());
if (replacements == 0)
{
throw new WorkflowDeliveryConfigurationException(
"CONNECTION_BINDING_NOT_FOUND",
"The workflow package has no structured user_service_id fields for its connection slot.");
}
var yamlNode = ResolveYamlPointer(stream.Documents[0].RootNode, slot.YamlPointer);
if (yamlNode is not YamlScalarNode scalar)
throw InvalidPointer(slot.Key);
scalar.Value = resolvedConnections.GetValueOrDefault(slot.Key) ?? string.Empty;
scalar.Style = ScalarStyle.Plain;
}

string resolvedYaml;
Expand Down Expand Up @@ -156,8 +152,6 @@ private static IReadOnlyDictionary<string, string> ResolveConnections(
}
resolved.Add(slot.Key, raw.Trim());
}
if (resolved.Count > 1)
throw new WorkflowDeliveryConfigurationException("MULTIPLE_CONNECTIONS_UNSUPPORTED", "This MVP supports one connection slot per workflow package.");
return resolved;
}

Expand Down Expand Up @@ -260,36 +254,6 @@ JsonArray array when int.TryParse(segments[i], NumberStyles.None, CultureInfo.In
}
}

private static int ReplaceUserServiceIds(YamlNode node, string userServiceId)
{
var count = 0;
switch (node)
{
case YamlMappingNode mapping:
foreach (var pair in mapping.Children)
{
if (pair.Key is YamlScalarNode key &&
string.Equals(key.Value, "user_service_id", StringComparison.Ordinal) &&
pair.Value is YamlScalarNode value)
{
value.Value = userServiceId;
value.Style = ScalarStyle.Plain;
count++;
}
else
{
count += ReplaceUserServiceIds(pair.Value, userServiceId);
}
}
break;
case YamlSequenceNode sequence:
foreach (var child in sequence.Children)
count += ReplaceUserServiceIds(child, userServiceId);
break;
}
return count;
}

private static string DecodePointerSegment(string value) =>
value.Replace("~1", "/", StringComparison.Ordinal).Replace("~0", "~", StringComparison.Ordinal);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ public sealed class WorkflowDeliveryConnectionSlotOptions

public string ServiceSlug { get; set; } = string.Empty;

public string YamlPointer { get; set; } = string.Empty;

public bool Required { get; set; }
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ private static WorkflowDeliveryConnectionSlotDefinition ToProto(ConnectionSlotDe
Label = value.Label,
ServiceSlug = value.ServiceSlug,
Required = value.Required,
YamlPointer = value.YamlPointer,
};

private static PackageDefinition ToDefinition(WorkflowDeliveryPackageOptions options)
Expand All @@ -195,9 +196,15 @@ private static PackageDefinition ToDefinition(WorkflowDeliveryPackageOptions opt
NormalizeRequired(value.Key, "delivery connection slot key"),
NormalizeRequired(value.Label, "delivery connection slot label"),
NormalizeRequired(value.ServiceSlug, "delivery connection service slug"),
value.Required)).ToArray();
value.Required,
NormalizeRequired(value.YamlPointer, "delivery connection yaml pointer"))).ToArray();
if (connectionSlots.Select(static value => value.Key).Distinct(StringComparer.Ordinal).Count() != connectionSlots.Length)
throw new InvalidOperationException("Delivery package connection slot keys must be unique.");
if (connectionSlots.Select(static value => value.YamlPointer).Distinct(StringComparer.Ordinal).Count() != connectionSlots.Length)
throw new InvalidOperationException("Delivery package connection slot yaml pointers must be unique.");
var variableYamlPointers = variables.Select(static value => value.YamlPointer).ToHashSet(StringComparer.Ordinal);
if (connectionSlots.Any(value => variableYamlPointers.Contains(value.YamlPointer)))
throw new InvalidOperationException("Delivery package connection slot yaml pointers must not overlap variable yaml pointers.");

var acceptance = options.Acceptance ?? throw new InvalidOperationException(
"Delivery package acceptance policy is required.");
Expand Down Expand Up @@ -413,7 +420,8 @@ private sealed record ConnectionSlotDefinition(
string Key,
string Label,
string ServiceSlug,
bool Required);
bool Required,
string YamlPointer);
}

public sealed class WorkflowDeliveryPackageNotAllowedException(string workflowName)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1099,7 +1099,8 @@ private static PackageSnapshot ToContract(WorkflowPackageVersionSnapshot package
item.Key,
item.Label,
item.ServiceSlug,
item.Required)).ToArray(),
item.Required,
item.YamlPointer)).ToArray(),
package.Capabilities.ToArray(),
package.RiskSummary,
package.ParserDiagnostics.ToArray(),
Expand Down Expand Up @@ -1142,6 +1143,7 @@ private static WorkflowPackageVersionSnapshot ToActorPackage(PackageSnapshot pac
Label = item.Label,
ServiceSlug = item.ServiceSlug,
Required = item.Required,
YamlPointer = item.YamlPointer,
}));
result.Capabilities.Add(package.Capabilities);
result.ParserDiagnostics.Add(package.ParserDiagnostics);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -520,6 +520,7 @@ private static WorkflowDeliveryConnectionSlotDefinition MapConnectionSlot(
Label = value.Label,
ServiceSlug = value.ServiceSlug,
Required = value.Required,
YamlPointer = value.YamlPointer,
};

private static WorkflowDeliveryTriggerIntent MapTrigger(DeliveryApplication.WorkflowDeliveryTriggerIntent value)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,7 @@ private static ApplicationVariableDefinition MapVariable(

private static ApplicationConnectionSlotDefinition MapConnectionSlot(
Aevatar.GAgents.WorkflowDelivery.WorkflowDeliveryConnectionSlotDefinition value) =>
new(value.Key, value.Label, value.ServiceSlug, value.Required);
new(value.Key, value.Label, value.ServiceSlug, value.Required, value.YamlPointer);

private static WorkflowDeliveryConnectionSnapshot MapConnection(
WorkflowDeliveryConnectionState value) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,12 @@ private static WorkflowDeliveryPackageSnapshot Package(bool inputDeclared = true
null,
"10"),
],
[new ApplicationConnectionSlotDefinition("mail", "Mail", "lark", true)],
[new ApplicationConnectionSlotDefinition(
"mail",
"Mail",
"lark",
true,
"/steps/0/capability/nyxid_request/user_service_id")],
["network.write"],
"Writes a notification",
[],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ public async Task GetForScopeAsync_ShouldFilterBeforeMappingAndReturnTypedImmuta
-2));
snapshot.Package.AcceptancePolicy.Input.Bindings[1].Source.Should().BeOfType<
DeliveryApplication.WorkflowDeliveryAuthenticatedOwnerExternalUserIdInput>();
snapshot.Package.ConnectionSlots.Should().ContainSingle().Which.YamlPointer
.Should().Be("/steps/0/capability/nyxid_request/user_service_id");
snapshot.LifecycleStatus.Should().Be(DeliveryApplication.WorkflowDeliveryLifecycleStatus.Active);
snapshot.Installation!.Status.Should().Be(DeliveryApplication.WorkflowInstallationStatus.Ready);
snapshot.Installation.AcceptanceInput.Should().NotBeNull();
Expand Down Expand Up @@ -300,6 +302,17 @@ private static WorkflowDeliveryCurrentStateDocument ValidDocument() =>
},
CreatedBy = "admin-alpha",
CreatedAtUtc = At(0),
ConnectionSlots =
{
new WorkflowDeliveryConnectionSlotDefinition
{
Key = "calendar",
Label = "Calendar",
ServiceSlug = "service-calendar",
Required = true,
YamlPointer = "/steps/0/capability/nyxid_request/user_service_id",
},
},
},
TargetScopeId = "scope-alpha",
ExpiresAtUtc = At(8),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,97 @@ public void Render_WhenOptionalConfigurationFieldIsMissing_ShouldKeepPackageDefa
result.ResolvedYaml.Should().Contain("\"keep\":\"yes\"");
}

[Fact]
public void Render_WhenOptionalConnectionSlotIsMissing_ShouldClearPackageDefault()
{
var package = Package();
package.ConnectionSlots[0].Required = false;
var renderer = new WorkflowDeliveryConfigurationRenderer();

var result = renderer.Render(
package,
new Dictionary<string, JsonElement>
{
["threshold"] = Json("25"),
},
null);

result.ConnectionReferences.Should().BeEmpty();
var root = ParseRoot(result.ResolvedYaml);
var steps = (YamlSequenceNode)Child(root, "steps");
var call = (YamlMappingNode)steps.Children[1];
var capability = (YamlMappingNode)Child(call, "capability");
var request = (YamlMappingNode)Child(capability, "nyxid_request");
Scalar(request, "user_service_id").Value.Should().BeEmpty();
}

[Fact]
public void Render_ShouldResolveMultipleConnectionSlotsByDeclaredYamlPointers()
{
const string sourceYaml = """
name: merchant-intake-workflow
description: 'user_service_id: decoy'
steps:
- id: calendar
type: tool_call
capability:
nyxid_request:
user_service_id: calendar-placeholder
- id: document
type: tool_call
capability:
nyxid_request:
user_service_id: document-placeholder
""";
var package = new WorkflowPackageVersionSnapshot
{
PackageId = "merchant-intake-workflow",
PackageVersionId = "merchant-intake-workflow@source-alpha",
WorkflowName = "merchant-intake-workflow",
Version = "1",
DisplayName = "Merchant Intake Workflow",
SourceYaml = sourceYaml,
SourceHash = Hash(sourceYaml),
CreatedBy = "admin-alpha",
};
package.ConnectionSlots.Add(new WorkflowDeliveryConnectionSlotDefinition
{
Key = "calendar",
Label = "Calendar",
ServiceSlug = "service-calendar",
Required = true,
YamlPointer = "/steps/0/capability/nyxid_request/user_service_id",
});
package.ConnectionSlots.Add(new WorkflowDeliveryConnectionSlotDefinition
{
Key = "document",
Label = "Document",
ServiceSlug = "service-document",
Required = true,
YamlPointer = "/steps/1/capability/nyxid_request/user_service_id",
});
var renderer = new WorkflowDeliveryConfigurationRenderer();

var result = renderer.Render(
package,
null,
new Dictionary<string, string>
{
["calendar"] = "user-service-calendar",
["document"] = "user-service-document",
});

result.ConnectionReferences.Should().Contain("calendar", "user-service-calendar");
result.ConnectionReferences.Should().Contain("document", "user-service-document");
var root = ParseRoot(result.ResolvedYaml);
Scalar(root, "description").Value.Should().Be("user_service_id: decoy");
var steps = (YamlSequenceNode)Child(root, "steps");
var calendar = (YamlMappingNode)Child((YamlMappingNode)Child((YamlMappingNode)steps.Children[0], "capability"), "nyxid_request");
var document = (YamlMappingNode)Child((YamlMappingNode)Child((YamlMappingNode)steps.Children[1], "capability"), "nyxid_request");
Scalar(calendar, "user_service_id").Value.Should().Be("user-service-calendar");
Scalar(document, "user_service_id").Value.Should().Be("user-service-document");
}

private static WorkflowPackageVersionSnapshot Package()
{
var package = new WorkflowPackageVersionSnapshot
Expand Down Expand Up @@ -160,6 +251,7 @@ private static WorkflowPackageVersionSnapshot Package()
Label = "Mail",
ServiceSlug = "api-lark-bot",
Required = true,
YamlPointer = "/steps/1/capability/nyxid_request/user_service_id",
});
return package;
}
Expand Down
47 changes: 47 additions & 0 deletions test/Aevatar.Studio.Tests/WorkflowDeliveryGAgentTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,52 @@ await action.Should().ThrowAsync<InvalidOperationException>()
agent.EventSourcing!.CurrentVersion.Should().Be(0);
}

[Fact]
public async Task Create_WhenConnectionSlotYamlPointersAreDuplicated_ShouldRejectBeforeCommit()
{
var agent = await CreateAgentAsync("delivery-alpha");
var command = CreateCommandWithConnectionSlot();
command.Package.ConnectionSlots.Add(new WorkflowDeliveryConnectionSlotDefinition
{
Key = "calendar-secondary",
Label = "Calendar Secondary",
ServiceSlug = "api-calendar-secondary",
Required = true,
YamlPointer = command.Package.ConnectionSlots[0].YamlPointer,
});
ResealPackage(command.Package);

var action = () => agent.HandleCreateAsync(command);

await action.Should().ThrowAsync<InvalidOperationException>()
.WithMessage("*connection slot yaml pointers must be unique*");
agent.EventSourcing!.CurrentVersion.Should().Be(0);
}

[Fact]
public async Task Create_WhenConnectionSlotYamlPointerOverlapsVariable_ShouldRejectBeforeCommit()
{
var agent = await CreateAgentAsync("delivery-alpha");
var command = CreateCommandWithConnectionSlot();
command.Package.VariableSchema.Add(new WorkflowDeliveryVariableDefinition
{
Key = "threshold",
Label = "Threshold",
Description = "Approval threshold",
Kind = WorkflowDeliveryVariableKind.Integer,
Required = true,
YamlPointer = "/steps/0/parameters/value",
});
command.Package.ConnectionSlots[0].YamlPointer = command.Package.VariableSchema[0].YamlPointer;
ResealPackage(command.Package);

var action = () => agent.HandleCreateAsync(command);

await action.Should().ThrowAsync<InvalidOperationException>()
.WithMessage("*connection slot yaml pointers must not overlap variable yaml pointers*");
agent.EventSourcing!.CurrentVersion.Should().Be(0);
}

[Fact]
public async Task DuplicateCreate_WithDefaultExpiryClockDrift_ShouldKeepFirstExpiryWhileExplicitDriftConflicts()
{
Expand Down Expand Up @@ -1392,6 +1438,7 @@ private static CreateWorkflowDeliveryCommand CreateCommandWithConnectionSlot()
Label = "Lark",
ServiceSlug = "api-lark",
Required = true,
YamlPointer = "/steps/0/capability/nyxid_request/user_service_id",
});
command.Package.PackageHash = WorkflowDeliveryConventions.ComputePackageHash(command.Package);
command.Package.Version = command.Package.PackageHash[..16];
Expand Down
Loading
Loading