Skip to content

Adopt NyxID channel bots for registrations - #3653

Open
louis4li wants to merge 5 commits into
feature/integratefrom
fix/2026-09-16_channel-registration-nyxid-bots
Open

louis4li wants to merge 5 commits into
feature/integratefrom
fix/2026-09-16_channel-registration-nyxid-bots

Conversation

@louis4li

Copy link
Copy Markdown
Contributor

Issue

Closes #3652

Scope

  • Reworks existing /api/channels/registrations semantics so listing is sourced from NyxID channel bots and overlaid with local registration state.
  • Changes registration creation into adoption/binding of an existing NyxID channel bot, provisioning an Aevatar-owned Agent Key and binding the NyxID conversation route.
  • Allows NyxID-backed non-Lark/Telegram platforms in the local registration mirror and persists updated Agent Key authorization facts during runtime-config updates.
  • Adds focused coverage for NyxID inventory overlay, adoption, duplicate binding, route ambiguity, nontraditional platform persistence, and Agent Key runtime authorization updates.

Verification

  • dotnet test test/Aevatar.GAgents.ChannelRuntime.Tests/Aevatar.GAgents.ChannelRuntime.Tests.csproj --nologo --filter ChannelCallbackEndpointsTests --no-restore /clp:ErrorsOnly passed, 62 tests.
  • dotnet test test/Aevatar.GAgents.ChannelRuntime.Tests/Aevatar.GAgents.ChannelRuntime.Tests.csproj --nologo --filter ChannelBotRegistration --no-restore /clp:ErrorsOnly passed, 51 tests.
  • dotnet test test/Aevatar.GAgents.ChannelRuntime.Tests/Aevatar.GAgents.ChannelRuntime.Tests.csproj --nologo --filter ChannelRegistrationTool --no-restore /clp:ErrorsOnly passed, 35 tests.
  • bash tools/ci/architecture_guards.sh passed.
  • bash tools/ci/test_stability_guards.sh passed.
  • dotnet test aevatar.slnx --nologo --filter ChannelBotRegistration --no-restore /clp:ErrorsOnly blocked by src/Aevatar.Mainnet.Host.Api/Program.cs(2,7): error CS0246: The type or namespace name 'Serilog' could not be found; the focused ChannelRuntime filtered tests passed before the solution build reached this unrelated project error.
  • Local host / NyxID online endpoint verification not run yet in this session.

Notes

  • Existing untracked local directories .claude/worktrees/ and {{pkgetc}}/ were left untouched and not included.
  • ChannelRegistrationTool remains on the existing Lark/Telegram provisioning tool surface; this PR keeps the backend API change scoped to the existing /api/channels/registrations endpoints.

Generated with Claude Code.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 16, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-16T11:43:53.328683Z 398f7c7 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 398f7c734a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +231 to +235
channelAgentKey = await agentKeyProvisioning.ProvisionAsync(
bot.Platform,
accessToken,
relayCallbackUrl,
scopeResolution.ScopeId!,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Move the adoption saga out of the HTTP endpoint

Move owner resolution, API-key provisioning, grant mutation, route selection/rebinding, rollback, and command construction into an application-layer facade. Implementing the saga directly in HandleRegisterAsync makes this API endpoint the owner of core business orchestration and allows other hosts to bypass or duplicate its partial-failure semantics, contrary to the repository's highest-priority layering rule.

AGENTS.md reference: AGENTS.md:L4-L4

Useful? React with 👍 / 👎.

Comment on lines +174 to +177
var existing = (await queryPort.QueryAllSnapshotsAsync(ct))
.Select(static snapshot => snapshot.Registration)
.FirstOrDefault(registration => !registration.Tombstoned &&
string.Equals(registration.NyxChannelBotId, nyxChannelBotId, StringComparison.Ordinal));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Authorize the bot before revealing an existing binding

The global read-model lookup runs before GetChannelBotAsync verifies that the caller's token can access this Nyx bot, and the conflict response exposes the owning registration ID. Consequently, a caller who guesses another tenant's bot ID can distinguish whether it is bound and learn its local registration ID; perform the caller-scoped Nyx lookup first and only return binding details after ownership is established.

Useful? React with 👍 / 👎.

Comment on lines +310 to +314
if (channelAgentKey is not null)
{
using var cleanup = new CancellationTokenSource(TimeSpan.FromSeconds(10));
await agentKeyProvisioning.CleanupAsync(accessToken, channelAgentKey, registrationId, cleanup.Token);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Restore the conversation route when adoption fails

If BindConversationRouteAsync creates a route or replaces an existing route's agent key and any later operation fails, this cleanup deletes only the newly provisioned API key. The Nyx bot is then left with a route pointing at a deleted key, taking an existing bot offline; rollback must delete a newly created route or restore the prior route binding before removing the key.

AGENTS.md reference: AGENTS.md:L78-L78

Useful? React with 👍 / 👎.

nyx_conversation_route_id = routeId,
relay_callback_url = relayCallbackUrl,
webhook_url = bot.WebhookUrl,
workflow_result_delivery_status = "enabled",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Do not claim delivery is enabled before registration commits

The receipt only means the command was accepted for dispatch, yet the response claims workflow delivery is already enabled. For example, a caller-supplied registration_id can collide with an existing active registration—the actor rejects that command asynchronously, while this endpoint still returns 202 and leaves the adopted bot route bound to an untracked key. Return only dispatch-acceptance semantics here, or observe a committed/rejected registration outcome before asserting enablement.

AGENTS.md reference: AGENTS.md:L66-L66

Useful? React with 👍 / 👎.

Comment on lines +750 to +754
updatedAgentKey = await UpdateAgentKeyGrantForSelectionAsync(
nyxClient,
accessToken,
registration.ChannelAgentKey,
effectiveServiceSelection,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Compensate remote grant changes when local dispatch fails

When service_ids is specified, this call mutates the live Nyx API-key grant before dispatching the actor update. If dispatch throws, is rejected, or is accepted but never committed, the remote key immediately has the new permissions while the authoritative registration and read model retain the old authorization, with no rollback or reconciliation path; make this a compensating saga or defer the external mutation until the committed update is observed.

Useful? React with 👍 / 👎.

Comment on lines +607 to +610
var result = bots.Select(bot =>
{
var e = snapshot.Registration;
var capabilityStatus = ChannelWorkflowResultDeliveryCapability.Resolve(e);
var repairFailed = capabilityStatus ==
ChannelWorkflowResultDeliveryCapabilityStatus.RepairFailed;
return new
{
id = e.Id,
platform = e.Platform,
registration_mode = "nyx_relay_webhook",
authorization_mode = MapAuthorizationMode(e),
service_ids = MapRegistrationServiceIds(e),
state_version = snapshot.StateVersion,
nyx_provider_slug = e.NyxProviderSlug,
scope_id = e.ScopeId,
callback_url = string.Empty,
webhook_url = e.WebhookUrl,
nyx_channel_bot_id = e.NyxChannelBotId,
nyx_agent_api_key_id = e.NyxAgentApiKeyId,
nyx_conversation_route_id = e.NyxConversationRouteId,
default_skill_name = e.DefaultSkillName,
default_skill = MapDefaultSkill(e.RuntimeConfig, e.DefaultSkillName),
has_instructions = !string.IsNullOrWhiteSpace(e.RuntimeConfig?.Instructions),
has_tool_set_refs = e.RuntimeConfig?.ToolSetRefs.Count > 0,
has_extra_tool_names = e.RuntimeConfig?.ExtraToolNames.Count > 0,
nyxid_service_selectors = MapNyxIdServiceSelectors(e.RuntimeConfig),
agent_key = MapAgentKeyStatus(e),
workflow_result_delivery_status = MapCapabilityStatus(e, capabilityStatus),
workflow_result_delivery_failure_phase = repairFailed
? MapRepairPhase(e.WorkflowResultDeliveryRepair?.FailurePhase ??
ChannelWorkflowResultDeliveryRepairPhase.Unspecified)
: null,
workflow_result_delivery_failure_reason = repairFailed
? MapRepairFailureReason(e.WorkflowResultDeliveryRepair?.FailureReason ??
ChannelWorkflowResultDeliveryRepairFailureReason.Unspecified)
: null,
// Whether this bot belongs to the caller's account. Cross-account bots
// (admin all-view) cannot have their live status read from NyxID.
owned = string.Equals(e.ScopeId, callerScope, StringComparison.Ordinal),
};
localByBotId.TryGetValue(bot.Id, out var snapshot);
return MapRegistrationListRow(bot, snapshot, callerScope);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Include foreign snapshots in the admin all-scope result

In a real multi-account scope=all request, ListChannelBotsAsync returns only bots visible to the caller's Nyx token, while foreign registrations can only be represented by the local snapshots. Because the response iterates bots rather than the visible snapshots, those foreign registrations are never emitted even though the method promises an all-account admin view; merge unmatched local snapshots into the result with degraded/unavailable live status.

Useful? React with 👍 / 👎.

louis4li and others added 4 commits September 16, 2026 20:06
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant