Skip to content

bug(daily): GitHub username binding shared across all Lark users of one bot — last writer wins #436

@eanzhao

Description

@eanzhao

Tracks the same bug as #437 (user-facing report: /daily binding causes cross-user data leakage). This issue captures the engineering analysis and fix direction; #437 is the QA-side reproduction.

Symptom

I shared my Lark bot with a colleague. After they ran /daily and bound their GitHub username (Yuezh0127), my own /daily started reporting on Yuezh0127 instead of my GitHub account. Re-binding mine just flips it again — last writer wins. The GitHub username binding is behaving as a single global value per bot, not per Lark user.

Reproduction:

  1. User A (bot owner) runs /daily in Lark, binds GitHub username alice.
  2. User B (added to the same Lark bot) runs /daily, binds GitHub username bob.
  3. User A runs /daily again → daily report scheduled for bob. A's binding is gone.

(Screenshot in original report: my /daily shows Daily report scheduled for Yuezh0127 — Yuezh0127 is the colleague's GitHub account, not mine.)

Also reproduces in 1:1 private chats with the bot, not only in group chats (see #437).

Root cause

UserConfigGAgent is keyed by bot registration scope, not by Lark user identity. All Lark users sharing one bot share one UserConfigGAgent actor (user-config-{registrationScopeId}), so any user's SaveGithubUsernameAsync overwrites the binding for every other user of that bot.

The relevant chain:

  1. agents/Aevatar.GAgents.ChannelRuntime/ChannelConversationTurnRunner.cs:787 — when an inbound Lark message is converted to ChannelInboundEvent, RegistrationScopeId is set to registration.ScopeId (a per-bot value). The per-Lark-user SenderId is also populated (line 779) but is not propagated downstream as the user-config scope.

  2. agents/Aevatar.GAgents.ChannelRuntime/AgentBuilderCardFlow.cs:79-81 — when prefilling the daily-report form, the saved username is read using evt.RegistrationScopeId:

    preferredGithubUsername = (await userConfigQueryPort.GetAsync(
        NormalizeScopeId(evt.RegistrationScopeId),
        ct)).GithubUsername;
  3. agents/Aevatar.GAgents.ChannelRuntime/AgentBuilderTool.cs:186-187 — when creating the daily-report agent, the same bot-level scope is used:

    var rawScopeId = NormalizeOptional(AgentToolRequestContext.TryGet(\"scope_id\"));
    var configScopeId = NormalizeScopeId(rawScopeId);

    And on save (line 1579):

    await commandService.SaveGithubUsernameAsync(scopeId, githubUsername, ct);
  4. src/Aevatar.Studio.Projection/CommandServices/ActorDispatchUserConfigCommandService.cs:77 — the actor ID is derived purely from the scope:

    var actorId = ActorIdPrefix + NormalizeScopeId(scopeId);
    // -> \"user-config-{bot-registration-scope}\"
  5. src/Aevatar.Studio.Projection/QueryPorts/ProjectionUserConfigQueryPort.cs:41 — read uses the same key, so all Lark users in the bot read the same record.

The proto already carries the per-Lark-user identity (ChannelInboundEvent.sender_id, field 2 in agents/Aevatar.GAgents.ChannelRuntime/channel_runtime_messages.proto); it's just not threaded into the user-config scope.

How this slipped in

Issue #327 (closed) added GithubUsername to UserConfig under the assumption "Day One 的现实是'一个 user 关心一个 github',跨对话共享是 feature 不是 bug." That was correct for a single-owner CLI/Tools UI scenario, where the scope resolver returns a per-NyxID-user scope. But on the Lark adapter the scope is the bot's registration scope (one bot = one NyxID identity = one scope), and the bot is shared across many Lark users. The CLI semantics (one identity per scope) and the Lark semantics (many Lark users per scope) collide on the same actor.

Suggested fix direction (not prescriptive)

The fix has to give each Lark user their own UserConfigGAgent. Two shapes worth considering:

  • Composite scope: thread ChannelInboundEvent.SenderId through AgentToolRequestContext and use a composite key like {registrationScopeId}:lark:{senderId} for the user-config actor on Lark inbound paths. CLI/Tools paths keep their existing scope. This is the smallest blast radius.
  • Channel-user-scoped actor: introduce a per-platform-user identity that the Lark adapter resolves (probably tying back to ChannelUserBindingGAgent, mentioned in Add GithubUsername to UserConfig for /daily template prefill #327's binding decomposition) and use that as the user-config scope. Cleaner long-term, but bigger surface change.

Either way:

  • The sender_id (or a normalized channel-user-id) needs to flow through AgentToolRequestContextAgentBuilderToolIUserConfigCommandService and through AgentBuilderCardFlowIUserConfigQueryPort.
  • Existing data keyed at the bot-registration scope is now ambiguous. Probably acceptable to let each user re-bind on first /daily (the form already prompts when no username is prefilled), and clean up the orphaned shared record out-of-band.

Acceptance criteria

  • Two Lark users in the same bot can each bind a different GitHub username via /daily, and each user's /daily schedules the report for their own GitHub account.
  • Same isolation holds in 1:1 private chats with the bot (per [Bug] /daily binding causes cross-user data leakage #437).
  • Tests cover: (a) two distinct sender_ids in the same RegistrationScopeId round-trip independent GithubUsername values; (b) prefill returns each user's own value, not the most recent writer's.
  • CLI / Tools UI path (UserConfigEditor.tsx) is unaffected — single-user-per-scope semantics there should not regress.
  • Existing SkillRunnerGAgent.State.OutboundConfig.GithubUsername snapshot semantics from Add GithubUsername to UserConfig for /daily template prefill #327 stay intact (running agents are not retroactively repointed when a user re-binds).

Affected files (for triage)

  • `agents/Aevatar.GAgents.ChannelRuntime/ChannelConversationTurnRunner.cs` (line 787)
  • `agents/Aevatar.GAgents.ChannelRuntime/AgentBuilderCardFlow.cs` (lines 67-95)
  • `agents/Aevatar.GAgents.ChannelRuntime/AgentBuilderTool.cs` (lines 186-209, 1564-1590)
  • `src/Aevatar.Studio.Projection/CommandServices/ActorDispatchUserConfigCommandService.cs` (lines 75-89)
  • `src/Aevatar.Studio.Projection/QueryPorts/ProjectionUserConfigQueryPort.cs` (lines 39-63)
  • `agents/Aevatar.GAgents.ChannelRuntime/channel_runtime_messages.proto` (`ChannelInboundEvent.sender_id` already present, currently unused for scope)

Related: #437 (user-facing duplicate of this bug), #327 (introduced the binding), #254 (multi-channel adapter RFC — same multi-tenant concern surfaces here).

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions