You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
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.
Symptom
The proxy-scoped NyxID API key minted per
/dailyagent 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 onSkillRunnerOutboundConfig.NyxApiKey.UserAgentCatalogEntry.nyx_api_key(well-known registry actor state) and intoUserAgentCatalogDocument(projection / readmodel) so outbound senders can look it up without going through the event store.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'sPOST /api/v1/api-keysis non-idempotent and every preflight failure could orphan a key.Architectural violations
AgentBuilderTool(a tool / application-layer component) is the de facto owner.Proposed direction
Introduce
AgentExecutionCredentialGAgent(id =agentId):IssueCredentialCommandchecks state first; only mints a new key if state has none. Solves NyxID-side non-idempotency on aevatar's side, no NyxID change needed.CredentialPreflightSucceededEvent/CredentialPreflightFailedEvent. On preflight-failed event, actor itself dispatches the revoke (no "best-effort" outside).RevokeCredentialCommand(called by agent-delete flow), persisted asCredentialRevokedEvent.nyxid_proxy) ask the actor for the current effective credential, instead of reading fromOutboundConfig.NyxApiKey/ readmodel.Direct corollary cleanups:
BestEffortRevokeApiKeyAsyncdeleted fromAgentBuilderTool.UserAgentCatalogEntry.nyx_api_keyfield deleted.UserAgentCatalogDocumentno longer hasnyx_api_keyplain text.SkillRunnerOutboundConfig.NyxApiKeyeither deleted (use credential actor lookup) or kept as an indirection token.Acceptance
nyx_api_key./dailyfor the sameagentId(e.g. via retry) produces at most one NyxID API key.Affected files
agents/Aevatar.GAgents.Credential/...)nyx_api_keyfrom catalog entry / documentRelated