Skip to content

refactor(daily-credential): introduce AgentExecutionCredentialGAgent — proxy API key as actor-owned resource #445

@eanzhao

Description

@eanzhao

Architectural follow-up surfaced in docs/audit-scorecard/2026-04-27-daily-pipeline-architecture-review.md §A1 + §C1.

Symptom

The proxy-scoped NyxID API key minted per /daily agent is a real resource with a lifecycle (create / preflight / revoke), but it has no actor owner today:

  • AgentBuilderTool.CreateDailyReportAgentAsync (AgentBuilderTool.cs:236-262) creates the key directly via NyxID HTTP and stores the value as a string field on SkillRunnerOutboundConfig.NyxApiKey.
  • The key value is mirrored into UserAgentCatalogEntry.nyx_api_key (well-known registry actor state) and into UserAgentCatalogDocument (projection / readmodel) so outbound senders can look it up without going through the event store.
  • Failure recovery is BestEffortRevokeApiKeyAsync ("PR fix(agent-builder): use UserService.id for api-key allowed_service_ids (#417) #418 review r3141846175" pattern) — a band-aid for the fact that NyxID's POST /api/v1/api-keys is non-idempotent and every preflight failure could orphan a key.
  • Catalog readmodel is now LLM-adjacent secret storage. QA documentation has to warn against screenshotting it.

Architectural violations

  • CLAUDE.md "默认路径须定义资源语义:任何「缺失即创建」策略须同时定义归属、复用规则和清理责任。" The api_key is the textbook case of such a resource without ownership.
  • CLAUDE.md "事实源唯一" — the same key value lives in actor state, well-known catalog state, and readmodel document.
  • CLAUDE.md "中间层维护事实状态" — AgentBuilderTool (a tool / application-layer component) is the de facto owner.

Proposed direction

Introduce AgentExecutionCredentialGAgent (id = agentId):

  • Owns the proxy api_key as actor state.
  • Idempotent issuance: IssueCredentialCommand checks state first; only mints a new key if state has none. Solves NyxID-side non-idempotency on aevatar's side, no NyxID change needed.
  • Preflight as a state transition: CredentialPreflightSucceededEvent / CredentialPreflightFailedEvent. On preflight-failed event, actor itself dispatches the revoke (no "best-effort" outside).
  • Revocation as a domain command: RevokeCredentialCommand (called by agent-delete flow), persisted as CredentialRevokedEvent.
  • Outbound sender (Lark reply) and tool middleware (nyxid_proxy) ask the actor for the current effective credential, instead of reading from OutboundConfig.NyxApiKey / readmodel.

Direct corollary cleanups:

  • BestEffortRevokeApiKeyAsync deleted from AgentBuilderTool.
  • UserAgentCatalogEntry.nyx_api_key field deleted.
  • UserAgentCatalogDocument no longer has nyx_api_key plain text.
  • SkillRunnerOutboundConfig.NyxApiKey either deleted (use credential actor lookup) or kept as an indirection token.

Acceptance

  • No code path outside the credential actor mints or revokes a NyxID API key.
  • Catalog readmodel does not contain raw nyx_api_key.
  • Repeating /daily for the same agentId (e.g. via retry) produces at most one NyxID API key.
  • Agent delete revokes the key as a domain event, not a best-effort side call.
  • Test: on preflight failure, exactly zero orphan keys exist in NyxID after the operation completes.

Affected files

  • new: `agents/Aevatar.GAgents.ChannelRuntime/AgentExecutionCredentialGAgent.cs` (or agents/Aevatar.GAgents.Credential/...)
  • new: `channel_runtime_messages.proto` additions (commands + events)
  • `agents/Aevatar.GAgents.ChannelRuntime/AgentBuilderTool.cs` — remove direct NyxID key calls + best-effort revoke
  • `agents/Aevatar.GAgents.ChannelRuntime/SkillRunnerGAgent.cs` — outbound config no longer holds key; ask credential actor
  • `agents/Aevatar.GAgents.ChannelRuntime/UserAgentCatalogProjector.cs` — drop key mirroring
  • `agents/Aevatar.GAgents.ChannelRuntime/channel_runtime_messages.proto` — drop nyx_api_key from catalog entry / document

Related

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions