Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ public async Task HandleRegister(ChannelBotRegisterCommand cmd)
ScopeId = cmd.ScopeId,
WebhookUrl = cmd.WebhookUrl,
EncryptKey = cmd.EncryptKey,
CredentialRef = cmd.CredentialRef ?? string.Empty,
CreatedAt = Timestamp.FromDateTimeOffset(DateTimeOffset.UtcNow),
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ namespace Aevatar.GAgents.ChannelRuntime;
/// Materializes <see cref="ChannelBotRegistrationStoreState"/> into per-entry
/// <see cref="ChannelBotRegistrationDocument"/> documents for query-side read model.
///
/// Known limitation: <see cref="IProjectionWriteDispatcher{T}"/> only supports
/// <c>UpsertAsync</c>. When a bot is unregistered, the state no longer contains
/// that entry, but the orphaned document is not deleted. A future
/// <c>IProjectionWriteDispatcher.DeleteAsync</c> is needed to close this gap.
/// Tombstone behavior: <see cref="IProjectionWriteDispatcher{T}.DeleteAsync"/> is
/// available, but per-entry tombstone wiring is deferred to the
/// <c>PerEntryDocumentProjector</c> refactor (Channel RFC §7.1). Until then,
/// unregistered entries leave orphaned documents in the read model.
/// </summary>
public sealed class ChannelBotRegistrationProjector
: ICurrentStateProjectionMaterializer<ChannelBotRegistrationMaterializationContext>
Expand Down Expand Up @@ -64,6 +64,7 @@ public async ValueTask ProjectAsync(
WebhookUrl = entry.WebhookUrl ?? string.Empty,
NyxUserToken = entry.NyxUserToken ?? string.Empty,
EncryptKey = entry.EncryptKey ?? string.Empty,
CredentialRef = entry.CredentialRef ?? string.Empty,
StateVersion = stateEvent.Version,
LastEventId = stateEvent.EventId ?? string.Empty,
ActorId = context.RootActorId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,5 +52,6 @@ private static ChannelBotRegistrationEntry ToEntry(ChannelBotRegistrationDocumen
ScopeId = document.ScopeId ?? string.Empty,
WebhookUrl = document.WebhookUrl ?? string.Empty,
EncryptKey = document.EncryptKey ?? string.Empty,
CredentialRef = document.CredentialRef ?? string.Empty,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,11 @@ namespace Aevatar.GAgents.ChannelRuntime;
/// Materializes <see cref="DeviceRegistrationState"/> into per-entry
/// <see cref="DeviceRegistrationDocument"/> documents for query-side read model.
///
/// Known limitation: <see cref="IProjectionWriteDispatcher{T}"/> only supports
/// <c>UpsertAsync</c>. When a device is unregistered, the state no longer contains
/// that entry, but the orphaned document is not deleted. A future
/// <c>IProjectionWriteDispatcher.DeleteAsync</c> is needed to close this gap.
/// Until then, the <see cref="DeviceRegistrationQueryPort"/> should filter by
/// cross-referencing the actor's authoritative state version if strict consistency
/// Tombstone behavior: <see cref="IProjectionWriteDispatcher{T}.DeleteAsync"/> is
/// available, but per-entry tombstone wiring is deferred to the
/// <c>PerEntryDocumentProjector</c> refactor (Channel RFC §7.1). Until then,
/// unregistered entries leave orphaned documents; <see cref="DeviceRegistrationQueryPort"/>
/// cross-references the actor's authoritative state version when strict consistency
/// is required.
/// </summary>
public sealed class DeviceRegistrationProjector
Expand Down Expand Up @@ -53,7 +52,8 @@ public async ValueTask ProjectAsync(
var updatedAt = CommittedStateEventEnvelope.ResolveTimestamp(envelope, _clock.UtcNow);

// NOTE: only upserts current entries. Orphaned documents from unregistered
// devices remain until IProjectionWriteDispatcher gains DeleteAsync support.
// devices remain until the PerEntryDocumentProjector refactor (§7.1) wires
// the tombstone path through IProjectionWriteDispatcher.DeleteAsync.
foreach (var entry in state.Registrations)
{
if (string.IsNullOrWhiteSpace(entry.Id))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,20 @@ message ChannelBotRegistrationEntry {
google.protobuf.Timestamp created_at = 7;
string webhook_url = 8; // Full callback URL configured via setWebhook
string encrypt_key = 9; // Lark Encrypt Key — used for signature verification AND event decryption
// Late-bound credential reference (Channel RFC §17.3).
//
// Phase 0 status (this PR): the field is persisted end-to-end (command →
// entry → event → state → projector → document → query port). NO caller
// surface exposes it yet (ChannelCallbackEndpoints / ChannelRegistrationTool
// do not accept credential_ref), and NO consumer reads it yet
// (LarkPlatformAdapter still uses EncryptKey). Setting credential_ref by
// protobuf-level dispatch today persists through state but has no runtime
// effect.
//
// Phase 1 (main Channel RFC PR): host surfaces accept credential_ref,
// LarkPlatformAdapter resolves it via ICredentialProvider at callback time,
// raw encrypt_key becomes optional, then removed.
string credential_ref = 10;
}

message ChannelBotRegistrationStoreState {
Expand Down Expand Up @@ -138,6 +152,9 @@ message ChannelBotRegisterCommand {
string webhook_url = 6;
string requested_id = 7; // Caller-provided ID; actor uses if non-empty, else generates
string encrypt_key = 8; // Lark Encrypt Key for signature verification + event decryption
// Late-bound credential reference (Channel RFC §17.3). See
// ChannelBotRegistrationEntry.credential_ref for lifecycle / migration contract.
string credential_ref = 9;
}

message ChannelBotUnregisterCommand {
Expand Down Expand Up @@ -169,6 +186,9 @@ message ChannelBotRegistrationDocument {
string actor_id = 10; // Source actor ID
string nyx_user_token = 11; // Kept in document for callback flow (internal store)
string encrypt_key = 12; // Lark Encrypt Key (internal store, not exposed in list queries)
// Late-bound credential reference (Channel RFC §17.3). See
// ChannelBotRegistrationEntry.credential_ref for lifecycle / migration contract.
string credential_ref = 13;
}

// ─── Agent Registry (actor-backed delivery target store) ───
Expand Down
Loading
Loading