diff --git a/docs/README.md b/docs/README.md
index f700d5a2c..8edb05ecb 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -6,6 +6,7 @@
Authoritative architecture and developer guides. Each covers one topic.
+- [[RFC] Aevatar Chat — Multi-Channel Adapter Architecture](canon/aevatar-channel-architecture.md)
- [Aevatar Foundation](canon/architecture.md)
- [Workflow Chat API 能力说明(框架层)](canon/chat-api.md)
- [Connector 配置与执行逻辑](canon/connector.md)
@@ -30,6 +31,9 @@ Immutable records of architectural choices and their rationale.
- [Orleans Kafka Provider Backend Architecture](decisions/0003-kafka-transport.md)
- [Workflow 调度 Actor 化 & 多智能体协作演进方案](decisions/0006-multi-agent-evolution.md)
- [Aevatar Stream Forward 架构说明(2026-02-22)](decisions/0007-stream-forward.md)
+- [Channel Runtime Multi-Token Credential Routing](decisions/0008-channel-runtime-multi-token-routing.md)
+- [Channel Bot Callback Architecture — Lessons from Lark Integration](decisions/0009-channel-bot-callback-architecture.md)
+- [Channel Phase 0 Persistent Provider Validation Result](decisions/0010-channel-phase0-provider-validation.md)
## History
@@ -44,11 +48,14 @@ Point-in-time design snapshots. Not authoritative — for context only.
### 2026-04
+- [2026-04-09-scripting-authority-write-path-cqrs-closure](history/2026-04/2026-04-09-scripting-authority-write-path-cqrs-closure.md)
+- [2026-04-17-nyxid-chat-registry-lifecycle](history/2026-04/2026-04-17-nyxid-chat-registry-lifecycle.md)
- [claude-code-architecture-learnings](history/2026-04/claude-code-architecture-learnings.md)
- [nyxid-chat-console-design](history/2026-04/nyxid-chat-console-design.md)
## Audit Scorecard
+- [2026-04-08-architecture-audit-detailed.md](audit-scorecard/2026-04-08-architecture-audit-detailed.md)
- [FEATURE_APP_SERVICES_AUDIT.md](audit-scorecard/FEATURE_APP_SERVICES_AUDIT.md)
## Templates
diff --git a/docs/decisions/0010-channel-phase0-provider-validation.md b/docs/decisions/0010-channel-phase0-provider-validation.md
new file mode 100644
index 000000000..1957ebd50
--- /dev/null
+++ b/docs/decisions/0010-channel-phase0-provider-validation.md
@@ -0,0 +1,41 @@
+---
+title: "Channel Phase 0 Persistent Provider Validation Result"
+status: accepted
+owner: eanzhao
+---
+
+# ADR-0010: Channel Phase 0 Persistent Provider Validation Result
+
+## Context
+
+Issue `#255` defined Phase 0 as a prerequisite for the Channel RFC. One acceptance item required a documented result for the persistent-provider validation harness before Channel runtime work could rely on durable inbox semantics.
+
+The repository now contains a provider redelivery harness in [PersistentStreamProviderRedeliveryValidationTests](../../test/Aevatar.Foundation.Runtime.Hosting.Tests/PersistentStreamProviderRedeliveryValidationTests.cs), but the in-repo Orleans stream backends remain:
+
+- `InMemory` for local development and deterministic tests
+- `KafkaProvider` for the durable backend that exists in this repository today
+
+There is still no EventHubs transport/provider implementation in the repo to validate directly.
+
+## Decision
+
+Phase 0 records the following validation result:
+
+- `KafkaProvider` is the only durable backend validated in-repo today.
+- EventHubs is not treated as implicitly equivalent to Kafka.
+- Until an EventHubs backend exists in this repository and passes the same throw-vs-return redelivery harness, Channel runtime work must keep Kafka as the durable-provider fallback.
+
+This is an explicit fallback outcome, not an EventHubs pass.
+
+## Evidence
+
+- PR `#267` added the redelivery harness and then narrowed its scope honestly to Kafka-only.
+- The harness verifies both required semantics for the currently supported durable backend:
+ - `OnNextAsync` returns normally -> no redelivery
+ - `OnNextAsync` throws with propagated failure -> message is redelivered
+
+## Consequences
+
+- Issue `#255` can treat the provider-validation acceptance item as documented with a fallback stance.
+- Future EventHubs work must extend the harness for EventHubs explicitly and capture a fresh result before claiming EventHubs as a durable inbox provider.
+- Channel Phase 1 implementations must not assume EventHubs checkpoint semantics from vendor docs or from Kafka behavior.
diff --git a/src/Aevatar.Foundation.Abstractions/Credentials/AuthContext.cs b/src/Aevatar.Foundation.Abstractions/Credentials/AuthContext.cs
new file mode 100644
index 000000000..c3bf17030
--- /dev/null
+++ b/src/Aevatar.Foundation.Abstractions/Credentials/AuthContext.cs
@@ -0,0 +1,96 @@
+namespace Aevatar.Foundation.Abstractions.Credentials;
+
+///
+/// Stable auth intent that can cross module boundaries without carrying a raw secret.
+/// The credential reference is late-bound and resolved only at the provider edge.
+///
+public sealed record AuthContext
+{
+ public AuthContext(
+ AuthPrincipal principal,
+ string? principalId = null,
+ string? credentialRef = null,
+ string? onBehalfOfUserId = null)
+ {
+ Principal = principal;
+ PrincipalId = Normalize(principalId);
+ CredentialRef = Normalize(credentialRef);
+ OnBehalfOfUserId = Normalize(onBehalfOfUserId);
+
+ Validate(Principal, PrincipalId, OnBehalfOfUserId);
+ }
+
+ public AuthPrincipal Principal { get; }
+
+ ///
+ /// Stable user identity when the principal is user-scoped.
+ ///
+ public string? PrincipalId { get; }
+
+ ///
+ /// Opaque late-bound reference. This is never the raw secret itself.
+ ///
+ public string? CredentialRef { get; }
+
+ ///
+ /// Audit target for delegated sends.
+ ///
+ public string? OnBehalfOfUserId { get; }
+
+ public bool UsesBotIdentity => Principal == AuthPrincipal.Bot;
+
+ public static AuthContext Bot(string? credentialRef = null) =>
+ new(AuthPrincipal.Bot, credentialRef: credentialRef);
+
+ public static AuthContext User(string principalId, string? credentialRef = null) =>
+ new(AuthPrincipal.User, principalId: principalId, credentialRef: credentialRef);
+
+ public static AuthContext OnBehalfOfUser(
+ string principalId,
+ string onBehalfOfUserId,
+ string? credentialRef = null) =>
+ new(
+ AuthPrincipal.OnBehalfOfUser,
+ principalId: principalId,
+ credentialRef: credentialRef,
+ onBehalfOfUserId: onBehalfOfUserId);
+
+ private static string? Normalize(string? value) =>
+ string.IsNullOrWhiteSpace(value) ? null : value;
+
+ private static void Validate(
+ AuthPrincipal principal,
+ string? principalId,
+ string? onBehalfOfUserId)
+ {
+ switch (principal)
+ {
+ case AuthPrincipal.Bot:
+ if (principalId is not null)
+ throw new ArgumentException("Bot auth context cannot carry a principal id.", nameof(principalId));
+
+ if (onBehalfOfUserId is not null)
+ throw new ArgumentException("Bot auth context cannot carry an on-behalf-of user id.", nameof(onBehalfOfUserId));
+
+ break;
+ case AuthPrincipal.User:
+ if (principalId is null)
+ throw new ArgumentException("User auth context requires a principal id.", nameof(principalId));
+
+ if (onBehalfOfUserId is not null)
+ throw new ArgumentException("User auth context cannot carry an on-behalf-of user id.", nameof(onBehalfOfUserId));
+
+ break;
+ case AuthPrincipal.OnBehalfOfUser:
+ if (principalId is null)
+ throw new ArgumentException("Delegated auth context requires a principal id.", nameof(principalId));
+
+ if (onBehalfOfUserId is null)
+ throw new ArgumentException("Delegated auth context requires an on-behalf-of user id.", nameof(onBehalfOfUserId));
+
+ break;
+ default:
+ throw new ArgumentOutOfRangeException(nameof(principal), principal, "Unsupported auth principal.");
+ }
+ }
+}
diff --git a/src/Aevatar.Foundation.Abstractions/Credentials/AuthPrincipal.cs b/src/Aevatar.Foundation.Abstractions/Credentials/AuthPrincipal.cs
new file mode 100644
index 000000000..e6414b6df
--- /dev/null
+++ b/src/Aevatar.Foundation.Abstractions/Credentials/AuthPrincipal.cs
@@ -0,0 +1,12 @@
+namespace Aevatar.Foundation.Abstractions.Credentials;
+
+///
+/// Identifies which principal should be used when resolving an auth context.
+/// The raw secret is still obtained later through .
+///
+public enum AuthPrincipal
+{
+ Bot = 0,
+ User = 1,
+ OnBehalfOfUser = 2,
+}
diff --git a/src/Aevatar.Foundation.Abstractions/README.md b/src/Aevatar.Foundation.Abstractions/README.md
index 769177896..aa8cdb79f 100644
--- a/src/Aevatar.Foundation.Abstractions/README.md
+++ b/src/Aevatar.Foundation.Abstractions/README.md
@@ -7,6 +7,7 @@
- 定义 Agent/Actor/Runtime 的核心接口
- 定义事件发布、流、模块与持久化接口
- 定义框架级 connector 契约(`IConnector` / `IConnectorRegistry`)
+- 定义框架级凭证解析契约(`AuthContext` / `ICredentialProvider`)
- 提供跨项目共享的 Proto 消息
- 提供少量基础工具类型(如 `AgentId`、时间工具、属性标记)
@@ -26,6 +27,7 @@ Aevatar.Foundation.Abstractions/
├── Attributes/
├── EventModules/
├── Connectors/
+├── Credentials/
├── Context/
├── Propagation/
├── Persistence/
@@ -45,6 +47,7 @@ Aevatar.Foundation.Abstractions/
- `IEventContext`:模块上下文的共性根接口
- `IEventModule`:可插拔事件处理模块(含优先级)
- `IConnector` / `IConnectorRegistry`:命名 connector 调用契约与注册表
+- `AuthContext` / `ICredentialProvider`:principal-aware 凭证引用与延迟解析契约
- `IStateStore` / `IEventStore`:状态与事件持久化契约
## Proto 说明
diff --git a/test/Aevatar.Foundation.Abstractions.Tests/AuthContextTests.cs b/test/Aevatar.Foundation.Abstractions.Tests/AuthContextTests.cs
new file mode 100644
index 000000000..12f36c297
--- /dev/null
+++ b/test/Aevatar.Foundation.Abstractions.Tests/AuthContextTests.cs
@@ -0,0 +1,78 @@
+using Aevatar.Foundation.Abstractions.Credentials;
+using Shouldly;
+
+namespace Aevatar.Foundation.Abstractions.Tests;
+
+public sealed class AuthContextTests
+{
+ [Fact]
+ public void Bot_Factory_CreatesBotContext()
+ {
+ var context = AuthContext.Bot("secrets://bot/default");
+
+ context.Principal.ShouldBe(AuthPrincipal.Bot);
+ context.PrincipalId.ShouldBeNull();
+ context.CredentialRef.ShouldBe("secrets://bot/default");
+ context.OnBehalfOfUserId.ShouldBeNull();
+ context.UsesBotIdentity.ShouldBeTrue();
+ }
+
+ [Fact]
+ public void User_Factory_CreatesUserContext()
+ {
+ var context = AuthContext.User("user-123", "secrets://user/123");
+
+ context.Principal.ShouldBe(AuthPrincipal.User);
+ context.PrincipalId.ShouldBe("user-123");
+ context.CredentialRef.ShouldBe("secrets://user/123");
+ context.OnBehalfOfUserId.ShouldBeNull();
+ context.UsesBotIdentity.ShouldBeFalse();
+ }
+
+ [Fact]
+ public void OnBehalfOfUser_Factory_CreatesDelegatedContext()
+ {
+ var context = AuthContext.OnBehalfOfUser(
+ principalId: "workflow-runner",
+ onBehalfOfUserId: "owner-42",
+ credentialRef: "secrets://workflow/owner-42");
+
+ context.Principal.ShouldBe(AuthPrincipal.OnBehalfOfUser);
+ context.PrincipalId.ShouldBe("workflow-runner");
+ context.OnBehalfOfUserId.ShouldBe("owner-42");
+ context.CredentialRef.ShouldBe("secrets://workflow/owner-42");
+ }
+
+ [Fact]
+ public void Constructor_NormalizesWhitespaceOnlyOptionals()
+ {
+ var context = new AuthContext(AuthPrincipal.Bot, credentialRef: " ");
+
+ context.CredentialRef.ShouldBeNull();
+ }
+
+ [Fact]
+ public void Constructor_RejectsBotPrincipalId()
+ {
+ Should.Throw(() => new AuthContext(
+ AuthPrincipal.Bot,
+ principalId: "bot-id"))
+ .ParamName.ShouldBe("principalId");
+ }
+
+ [Fact]
+ public void Constructor_RejectsMissingUserPrincipalId()
+ {
+ Should.Throw(() => new AuthContext(AuthPrincipal.User))
+ .ParamName.ShouldBe("principalId");
+ }
+
+ [Fact]
+ public void Constructor_RejectsMissingDelegatedAuditTarget()
+ {
+ Should.Throw(() => new AuthContext(
+ AuthPrincipal.OnBehalfOfUser,
+ principalId: "workflow-runner"))
+ .ParamName.ShouldBe("onBehalfOfUserId");
+ }
+}