Skip to content

Digest identity mismatch: rebind cross-wired Edge City village credentials + hardening #911

Description

@yanekyuk

Follow-up to the daily-digest "self-opportunity" investigation. The guardrails (defense-in-depth for the self-match / wrong-caller data classes) shipped in #910. This issue tracks the actual root-cause fix and related hardening, which are operational / out-of-scope for that hotfix.

Background

A user (Seren) saw a daily-digest opportunity (list_opportunities MCP call) that appeared to match her with herself. Clicking the connect URL opened a conversation seeded with an intro written in someone else's first-person voice:

"Hi Seren, I came across your profile … Given my involvement with Edge City … I'd love to connect …"

Confirmed mechanism

The greeting is always built from getProfile(viewerId) (opportunity.presenter.ts:608,750), and the digest mints the connect link with viewerId = context.userId (opportunity.tools.ts). The message being addressed to Seren but written in the Edge City user's voice means the card was rendered with viewerId = the Edge City user, counterpart = Seren.

AgentVillage's Edge — daily digest cron runs list_opportunities using the single INDEX_API_KEY baked into that village's config.yamlmcp_servers.index.headers["x-api-key"] (packages/agentvillage/install/install_index.ts:64). MCP resolves that key to one user via apikeys.referenceId ?? apikeys.userId (backend/src/controllers/mcp.controller.ts:387).

Conclusion: Seren's village agent authenticates to Index as the Edge City user — her morning brief is (partly) that user's opportunity feed, so a connection that user has toward Seren surfaces in Seren's brief as "an opportunity with herself," with the other person's identity + greeting on the connect link.

Ruled out (verified in code)

  • Discovery self-match — a true [Seren, Seren] opp is skipped in digest mode (opportunity.tools.ts:1514).
  • Connect-link reuse / delivery-card cache leak — keyed by (opp, userId, kind) and viewerId respectively.
  • MCP request-context concurrency clobber — AsyncLocalStorage, per-request userId.

Tasks

1. Verify the binding (do first)

  • connect_links row for the clicked code → confirm userId is the Edge City user (not Seren) and the opportunity actors are a legit [EdgeCity, Seren] pair (not a degenerate self set).
  • Hash the x-api-key from Seren's village config.yaml (SHA-256 → base64url) and SELECT userId, referenceId FROM apikeys WHERE key = <hash> → does it resolve to the Edge City user?
  • Confirm Seren's email maps to a single distinct Index user (rule out a ghost-contact / email collision at network-invitation.service.ts:217 having merged her with the Edge City account — that would be a data fix, not a key swap).

2. Immediate rebind (one resident)

  • Mint Seren a correct key bound to her account: networkInvitationService.resendInvite({ networkId, memberId: <serenUserId> }) (adapter sets referenceId = userId = Seren, so it resolves correctly — agent-token.adapter.ts:101).
  • Re-point her village at the new key (re-run install_index.ts, or update config.yaml + .env and let the 08:00 cron pick it up).

3. Systemic fix (the cohort)

  • Find how her village got the wrong key — the provisioning/control-plane that ran each install_index.ts with --index-api-key. (Lives outside indexnetwork/index and Edge-City/agentvillage; needs a pointer to the entrypoint or the resident→key map.)
  • Audit the Edge City import cohort for other cross-wired villages and rebind any found.

4. Hardening (optional, separate PRs)

  • Telegram-handle ↔ userId consistency guard: village installs pass --telegram-handle → MCP request carries x-index-telegram-username. Assert the authenticated key's user actually owns that handle; reject/log on mismatch. This is the one server-side signal that would have caught this incident.
  • mcp.controller.ts:387 precedence: referenceId ?? userId is benign today (adapter sets them equal) but resolves to the wrong principal if any key ever has referenceId ≠ userId. Prefer the verified session userId, or assert referenceId === userId for agent keys.

Done (for reference)

https://claude.ai/code/session_0157jBxKSVKaJG43XAWJNsGU

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions