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
6 changes: 3 additions & 3 deletions docs/contracts/nyxid-assistant-conformance/v1/sources.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
"schema_version": 1,
"aevatar": {
"repository": "https://github.com/AevatarAI/aevatar.git",
"revision": "0b8ec500087331c3d12819b532e7dfa29e740fb4",
"contract_files_sha256": "0d05d7c2d12437b8281c5ee8a0210b51d9a2757c0ae9be237aab485050524420",
"revision": "2c66f55aa907a96e96b01edd84ebc8e417af0731",
"contract_files_sha256": "d0daddc0594a0f92fe7c2057c4433a7dbb06f2d8097aa0d079e67f5eab361c50",
"files": {
"agents/Aevatar.GAgents.NyxidChat/NyxIdActionPostconditionPort.cs": "23fd2cf48541c4b8da3fa1ef07277a7700c8478f9c638be8221465bf877fbb31",
"agents/Aevatar.GAgents.NyxidChat/NyxIdAssistantActionRegistry.cs": "60e6f67c94ae11b1bf0dac036ad8ac0c35901e31787b1f0c8173964f6a12d263",
Expand All @@ -13,7 +13,7 @@
"agents/Aevatar.GAgents.NyxidChat/protos/nyxid_chat_task.proto": "523b8182bdd0a30224e001a7257ad42450226f42a84542a363e97816e34f23d9",
"docs/adr/0048-nyxid-assistant-operation-class-boundary.md": "884aca09774e773e68154c923fec8078610b2cf8e97f581fedc36e10451ccec3",
"src/Aevatar.AI.Abstractions/ai_messages.proto": "7ca08d69adbd97d82b89fc041d8d0eebd81e3ca125e2c4798bf012f1c29dc26d",
"src/Aevatar.AI.ToolProviders.NyxId/NyxIdApiAccessContracts.cs": "2c679f03818997d1759b5bc60560b37119214bd6d5ee64f41578e02fa44cc9ee",
"src/Aevatar.AI.ToolProviders.NyxId/NyxIdApiAccessContracts.cs": "f209169e3ccb63ec9135b7d1f50d61987890f195efbde5676bbfa38680169748",
"src/Aevatar.AI.ToolProviders.NyxId/NyxIdAssistantToolSource.cs": "1b033df9cb55c741e9b52054cbd4a91067f03c8c3797bd076a7e3d6133eb0fcb",
"src/Aevatar.AI.ToolProviders.NyxId/Tools/NyxIdRequestKeyCreateTool.cs": "2c4f2cda99154f2e667c6cfd291497e697ef11df17f081f96ec70070a8af8b8c",
"src/Aevatar.AI.ToolProviders.NyxId/Tools/NyxIdRequestKeyRotateTool.cs": "18212bb64644cfbca401065bccce439ea5fa00316deff57d730a0d9ac2650e53",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,8 @@ public sealed record NyxIdUserServiceKey(
NyxIdUserServiceCredentialSource CredentialSource,
string? CatalogServiceId,
string? CatalogServiceSlug,
bool Connected);
bool Connected,
bool? AutoConnected = null);

public sealed record NyxIdUserServiceKeys(IReadOnlyList<NyxIdUserServiceKey> Services);

Expand Down Expand Up @@ -425,7 +426,8 @@ private static NyxIdUserServiceKeys ParseUserServiceKeysDocument(JsonElement roo
JsonValueKind.Object)),
ReadOptionalNormalizedString(serviceElement, "catalog_service_id"),
ReadOptionalNormalizedString(serviceElement, "catalog_service_slug"),
RequireBoolean(serviceElement, "connected")));
RequireBoolean(serviceElement, "connected"),
ReadOptionalBoolean(serviceElement, "auto_connected")));
}

return new NyxIdUserServiceKeys(services);
Expand Down
35 changes: 33 additions & 2 deletions src/Aevatar.AI.ToolProviders.NyxId/NyxIdApiClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,12 @@ public Task<string> DeleteServiceAsync(string token, string id, CancellationToke
public Task<string> CreateServiceAsync(string token, string body, CancellationToken ct) =>
PostAsync(token, "/api/v1/keys", body, ct);

internal Task<NyxIdProxyTextResponse> CreateServiceResponseAsync(
string token,
string body,
CancellationToken ct) =>
PostTextResponseAsync(token, "/api/v1/keys", body, ct);

// ─── Session Refresh ───

public async Task<NyxIdSessionRefreshResult> RefreshSessionAsync(string refreshToken, CancellationToken ct)
Expand Down Expand Up @@ -1309,6 +1315,17 @@ public Task<string> UpdateServiceAsync(string token, string id, string body, Can
public Task<string> UpdateServiceRouteAsync(string token, string id, string body, CancellationToken ct) =>
PutAsync(token, $"/api/v1/user-services/{Uri.EscapeDataString(id)}", body, ct);

internal Task<NyxIdProxyTextResponse> UpdateServiceRouteResponseAsync(
string token,
string id,
string body,
CancellationToken ct) =>
PutTextResponseAsync(
token,
$"/api/v1/user-services/{Uri.EscapeDataString(id)}",
body,
ct);

// ─── Proxy (additions) ───

public Task<string> DiscoverProxyServicesAsync(string token, CancellationToken ct) =>
Expand Down Expand Up @@ -1886,12 +1903,19 @@ private async Task<NyxIdProxyTextResponse> GetBoundedAsync(
}

internal async Task<string> PostAsync(string token, string path, string body, CancellationToken ct)
=> (await PostTextResponseAsync(token, path, body, ct)).Content;

private async Task<NyxIdProxyTextResponse> PostTextResponseAsync(
string token,
string path,
string body,
CancellationToken ct)
{
var url = $"{GetPublicApiBaseUrl()}{path}";
using var request = new HttpRequestMessage(HttpMethod.Post, url);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
request.Content = new StringContent(body, Encoding.UTF8, "application/json");
return await SendAsync(request, ct);
return await SendTextResponseAsync(request, ct);
}

internal async Task<string> PostWithoutAuthAsync(string path, string body, CancellationToken ct)
Expand All @@ -1912,12 +1936,19 @@ internal async Task<string> PatchAsync(string token, string path, string body, C
}

internal async Task<string> PutAsync(string token, string path, string body, CancellationToken ct)
=> (await PutTextResponseAsync(token, path, body, ct)).Content;

private async Task<NyxIdProxyTextResponse> PutTextResponseAsync(
string token,
string path,
string body,
CancellationToken ct)
{
var url = $"{GetPublicApiBaseUrl()}{path}";
using var request = new HttpRequestMessage(HttpMethod.Put, url);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
request.Content = new StringContent(body, Encoding.UTF8, "application/json");
return await SendAsync(request, ct);
return await SendTextResponseAsync(request, ct);
}

internal async Task<string> DeleteAsync(string token, string path, CancellationToken ct)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System.Net;
using System.Text.Json;
using System.Text.Json.Serialization;
using Aevatar.AI.Abstractions.CodeExecution;

namespace Aevatar.AI.ToolProviders.NyxId;
Expand All @@ -8,6 +10,7 @@ public enum NyxIdCodeExecutionRouteRepairFailureKind
None = 0,
UpdateException = 1,
PostconditionMismatch = 2,
MutationRejected = 3,
}

public sealed record NyxIdCodeExecutionRouteReconciliation(
Expand Down Expand Up @@ -74,10 +77,14 @@ public async Task<NyxIdCodeExecutionRouteReconciliation> ReconcileAsync(
Verified: postconditionSatisfied,
FailureKind: postconditionSatisfied
? NyxIdCodeExecutionRouteRepairFailureKind.None
: convergence.FailureKind ==
NyxIdUserServiceRouteConvergenceFailureKind.UpdateException
? NyxIdCodeExecutionRouteRepairFailureKind.UpdateException
: NyxIdCodeExecutionRouteRepairFailureKind.PostconditionMismatch);
: convergence.FailureKind switch
{
NyxIdUserServiceRouteConvergenceFailureKind.UpdateException =>
NyxIdCodeExecutionRouteRepairFailureKind.UpdateException,
NyxIdUserServiceRouteConvergenceFailureKind.MutationRejected =>
NyxIdCodeExecutionRouteRepairFailureKind.MutationRejected,
_ => NyxIdCodeExecutionRouteRepairFailureKind.PostconditionMismatch,
});
}

if (!CanCreatePersonalRoute(before, resolution, exactUserServiceId))
Expand Down Expand Up @@ -137,14 +144,13 @@ private static bool CanCreatePersonalRoute(
.Where(service =>
CodeExecutionContract.IsSupportedServiceSlug(service.Slug) &&
snapshot.TryGetExact(service.Id, out var authority) &&
authority is { IsExecutionReady: true } &&
authority is { IsExecutionReady: true, Execution.AutoConnected: true } &&
string.Equals(
authority.Execution.CatalogServiceSlug,
CodeExecutionContract.ServiceSlug,
StringComparison.Ordinal))
.ToArray();
return canonical.Length == 1 &&
canonical[0].AutoConnected &&
string.Equals(
canonical[0].Slug,
CodeExecutionContract.ServiceSlug,
Expand All @@ -156,31 +162,32 @@ private async Task<NyxIdCodeExecutionRouteReconciliation> CreateAndVerifyPersona
NyxIdUserServiceRouteMutationAuthority mutationAuthority,
CancellationToken cancellationToken)
{
var createFailed = false;
var createFailure = NyxIdCodeExecutionRouteRepairFailureKind.None;
try
{
var body = JsonSerializer.Serialize(new
{
service_slug = CodeExecutionContract.ServiceSlug,
slug = CodeExecutionContract.PersonalServiceSlug,
forward_access_token = true,
inject_delegation_token = true,
delegation_token_scope = "proxy:* sandbox:execute",
});
_ = await _clientFactory.CreateClient()
.CreateServiceAsync(
var body = JsonSerializer.Serialize(new CreatePersonalRouteRequest(
CodeExecutionContract.ServiceSlug,
CodeExecutionContract.PersonalServiceSlug,
"Aevatar Code Execution",
true,
true,
"proxy:* sandbox:execute"));
var response = await _clientFactory.CreateClient()
.CreateServiceResponseAsync(
mutationAuthority.BearerToken,
body,
cancellationToken)
.ConfigureAwait(false);
if (!response.Succeeded && response.HttpStatus != (int)HttpStatusCode.Conflict)
createFailure = NyxIdCodeExecutionRouteRepairFailureKind.MutationRejected;
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
throw;
}
catch
{
createFailed = true;
createFailure = NyxIdCodeExecutionRouteRepairFailureKind.UpdateException;
}

var after = await _converger.ReadAsync(mutationAuthority, cancellationToken)
Expand All @@ -196,9 +203,9 @@ private async Task<NyxIdCodeExecutionRouteReconciliation> CreateAndVerifyPersona
Verified: verified,
FailureKind: verified
? NyxIdCodeExecutionRouteRepairFailureKind.None
: createFailed
? NyxIdCodeExecutionRouteRepairFailureKind.UpdateException
: NyxIdCodeExecutionRouteRepairFailureKind.PostconditionMismatch);
: createFailure == NyxIdCodeExecutionRouteRepairFailureKind.None
? NyxIdCodeExecutionRouteRepairFailureKind.PostconditionMismatch
: createFailure);
}

private static NyxIdUserService? SelectVerifiedPersonalRoute(
Expand All @@ -213,10 +220,9 @@ private async Task<NyxIdCodeExecutionRouteReconciliation> CreateAndVerifyPersona
service.Slug,
CodeExecutionContract.PersonalServiceSlug,
StringComparison.Ordinal) &&
!service.AutoConnected &&
service.CredentialSource.Kind == NyxIdUserServiceCredentialSourceKind.Personal &&
snapshot.TryGetExact(service.Id, out var authority) &&
authority is { IsExecutionReady: true } &&
authority is { IsExecutionReady: true, Execution.AutoConnected: false } &&
string.Equals(
authority.Execution.CatalogServiceSlug,
CodeExecutionContract.ServiceSlug,
Expand All @@ -225,4 +231,12 @@ private async Task<NyxIdCodeExecutionRouteReconciliation> CreateAndVerifyPersona
.ToArray();
return candidates.Length == 1 ? candidates[0] : null;
}

private sealed record CreatePersonalRouteRequest(
[property: JsonPropertyName("service_slug")] string ServiceSlug,
[property: JsonPropertyName("slug")] string Slug,
[property: JsonPropertyName("label")] string Label,
[property: JsonPropertyName("forward_access_token")] bool ForwardAccessToken,
[property: JsonPropertyName("inject_delegation_token")] bool InjectDelegationToken,
[property: JsonPropertyName("delegation_token_scope")] string DelegationTokenScope);
}
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,9 @@ public sealed record NyxIdUserServiceAuthority(
: Execution.NodeStatus == NyxIdUserServiceNodeStatus.Online);

public bool CanManageRoute =>
!Route.AutoConnected && IsExecutionReady && Route.CredentialSource.Kind switch
Execution.AutoConnected == false &&
IsExecutionReady &&
Route.CredentialSource.Kind switch
{
NyxIdUserServiceCredentialSourceKind.Personal => true,
NyxIdUserServiceCredentialSourceKind.Organization =>
Expand All @@ -240,7 +242,7 @@ internal bool HasSameIdentity(NyxIdUserServiceAuthority other) =>
Execution.CatalogServiceSlug,
other.Execution.CatalogServiceSlug,
StringComparison.Ordinal) &&
Route.AutoConnected == other.Route.AutoConnected &&
Execution.AutoConnected == other.Execution.AutoConnected &&
SameAuthority(Execution.CredentialSource, other.Execution.CredentialSource);

private static bool SameAuthority(
Expand Down Expand Up @@ -291,9 +293,29 @@ public async Task<NyxIdUserServiceAuthoritySnapshot> ReadAsync(
bearerToken.Trim(),
cancellationToken)
.ConfigureAwait(false);
var routes = NyxIdApiAccessResponseParser.ParseUserServiceRoutes(routeResponse);
var executionInventory = NyxIdApiAccessResponseParser.ParseUserServiceKeys(executionResponse);
return new NyxIdUserServiceAuthoritySnapshot(
NyxIdApiAccessResponseParser.ParseUserServiceRoutes(routeResponse),
NyxIdApiAccessResponseParser.ParseUserServiceKeys(executionResponse));
ApplyExecutionProvenance(routes, executionInventory),
executionInventory);
}

private static NyxIdApiAccessResult<NyxIdUserServices> ApplyExecutionProvenance(
NyxIdApiAccessResult<NyxIdUserServices> routes,
NyxIdApiAccessResult<NyxIdUserServiceKeys> executionInventory)
{
if (!routes.Succeeded || !executionInventory.Succeeded)
return routes;

var executionById = executionInventory.Value!.Services
.ToDictionary(static service => service.Id, StringComparer.Ordinal);
var normalizedRoutes = routes.Value!.Services
.Select(route => executionById.TryGetValue(route.Id, out var execution)
? route with { AutoConnected = execution.AutoConnected == true }
: route)
.ToArray();
return NyxIdApiAccessResult<NyxIdUserServices>.Success(
new NyxIdUserServices(normalizedRoutes));
}
}

Expand All @@ -306,6 +328,7 @@ public enum NyxIdUserServiceRouteConvergenceFailureKind
RouteNotWritable = 4,
UpdateException = 5,
PostconditionMismatch = 6,
MutationRejected = 7,
}

public sealed record NyxIdUserServiceRouteConvergence(
Expand Down Expand Up @@ -380,13 +403,15 @@ internal async Task<NyxIdUserServiceRouteConvergence> ConvergeAsync(
try
{
var body = NyxIdUserServiceRouteUpdateAdapter.Serialize(plan.Patch);
_ = await clientFactory.CreateClient()
.UpdateServiceRouteAsync(
var response = await clientFactory.CreateClient()
.UpdateServiceRouteResponseAsync(
authority.BearerToken,
current.Route.Id,
body,
cancellationToken)
.ConfigureAwait(false);
if (!response.Succeeded)
updateFailure = NyxIdUserServiceRouteConvergenceFailureKind.MutationRejected;
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
Expand Down
Loading
Loading