Skip to content

fix(relay): scope inbox deduplication per recipient - #3573

Merged
liamcrumm merged 2 commits into
microsoft:mainfrom
1aifanatic:contrib/fix-3526-relay-recipient-dedupe
Sep 8, 2026
Merged

liamcrumm merged 2 commits into
microsoft:mainfrom
1aifanatic:contrib/fix-3526-relay-recipient-dedupe

Conversation

@1aifanatic

@1aifanatic Naveen Chatlapalli (1aifanatic) commented Aug 2, 2026 •

Copy link
Copy Markdown
Contributor

Description

Fixes #3526.

InMemoryInboxStore currently indexes messages only by message_id, so a duplicate ID in a different recipient's mailbox is dropped. Message IDs are unique within a mailbox, not a safe global namespace.

Impact

This is a cross-recipient denial of service on the offline inbox, not just a correctness wart.

message_id is taken straight off the wire at relay/app.py:537 (message_id = frame.get("id")). Unlike from, which is stamped with the DID-PoP-verified sender identity, the ID is entirely attacker-chosen. So any authenticated sender can pre-register an arbitrary ID and permanently suppress offline delivery of that ID to a different recipient. An unscoped acknowledgement then deletes the victim's message from whichever mailbox the global index happened to point at.

Scoping storage to (recipient_did, message_id) closes both halves: the mailboxes no longer share an ID namespace, and an acknowledgement can no longer reach across recipients.

Changes

  • keys stored messages by (recipient_did, message_id);
  • allows the same ID to be delivered to separate recipients;
  • scopes fetch and acknowledgement lookups to the recipient;
  • makes an unscoped acknowledgement refuse ambiguous IDs instead of deleting an arbitrary mailbox;
  • keeps cleanup, message counts, and existing single-mailbox callers working;
  • adds a regression test covering delivery and acknowledgement isolation.

InMemoryInboxStore is the only implementation of this interface, and the unscoped acknowledgement path becomes strictly more restrictive, so no caller loses behaviour it previously relied on.

Validation

  • PYTHONPATH=agent-governance-python/agent-mesh/src python -m pytest agent-governance-python/agent-mesh/tests/test_relay.py -q — 40 tests passed.
  • python -m ruff check --select E,F --ignore E501 on the changed files — passed.
  • git diff --check.

AI Assistance

Codex assisted with implementation and test drafting under user direction. This is a draft PR for human review; the contributor is responsible for reviewing and explaining every change before requesting merge.

Checklist

  • Bug fix (non-breaking change that fixes an issue)
  • Tests added for the regression
  • DCO sign-off included
  • Human review completed

Copilot AI review requested due to automatic review settings August 2, 2026 11:13
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
There may be pipelines that require an authorized user to comment /azp run to run.

@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown

PR Review Summary

Check Status Details
🔍 Code Review ⚠️ Missing No current-run comment
🛡️ Security Scan ⚠️ Missing No current-run comment
🔄 Breaking Changes ⚠️ Missing No current-run comment
📝 Docs Sync ⚠️ Missing No current-run comment
🧪 Test Coverage ⚠️ Missing No current-run comment

Verdict: ⚠️ AI review incomplete; ready for human review

AI review comments are untrusted advisory output. The summary reports workflow-generated completion status only, not model-authored pass/fail claims.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

TL;DR: 0 blockers, 0 warnings. No issues found. Clean change.

# Sev Issue Where
— — No issues found —

This PR fixes a correctness gap in the AgentMesh relay’s in-memory inbox deduplication by scoping message identity to the recipient mailbox, aligning behavior with the wire spec’s mailbox semantics and preventing cross-recipient collisions.

Changes:

  • Re-keys InMemoryInboxStore storage from message_id to (recipient_did, message_id) and updates fetch/ack/cleanup accordingly.
  • Makes unscoped acknowledgements refuse ambiguous IDs shared by multiple recipients.
  • Adds a regression test covering same-ID delivery to multiple recipients plus scoped/unscoped acknowledgement behavior.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
agent-governance-python/agent-mesh/src/agentmesh/relay/store.py Scope inbox storage and lookups to (recipient_did, message_id); refuse ambiguous unscoped acknowledgements.
agent-governance-python/agent-mesh/tests/test_relay.py Add regression test for per-recipient dedupe and acknowledgement isolation.

@liamcrumm

Copy link
Copy Markdown
Contributor

message_id comes off the wire at relay/app.py:537, so any sender can pre-register an ID and permanently suppress delivery of that ID to a different recipient, and an unscoped acknowledge then drops it. That makes this a cross-recipient denial of service on the offline inbox rather than a correctness issue, which is worth stating in the description. The fix itself holds up. InMemoryInboxStore is the only implementation, and the unscoped acknowledge path becomes strictly more restrictive.

@1aifanatic

Copy link
Copy Markdown
Contributor Author

Thanks — agreed, and that's a better framing than the one I opened with. I've rewritten the description with an "Impact" section stating it explicitly: because from is stamped with the DID-PoP-verified identity but id is taken verbatim from the frame, the ID is fully attacker-chosen, so any authenticated sender can pre-register an ID and suppress offline delivery of that ID to a different recipient, with an unscoped acknowledgement then dropping the victim's message.

I also folded in your note that InMemoryInboxStore is the only implementation and that the unscoped acknowledgement path is strictly more restrictive, so no existing caller loses behaviour.

No code changes — the diff is unchanged from what you reviewed.

@1aifanatic

Copy link
Copy Markdown
Contributor Author

@microsoft-github-policy-service agree

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
There may be pipelines that require an authorized user to comment /azp run to run.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The impact section is what makes this worth prioritising, and it holds up.

message_id is taken straight off the wire at relay/app.py:537. from is stamped with the DID-PoP-verified sender identity, so it cannot be forged; the ID cannot be trusted at all. Once storage keys on the ID alone, the two mailboxes share a namespace the attacker controls. Any authenticated sender can pre-register an arbitrary ID and permanently suppress offline delivery of that ID to a different recipient, and the unscoped acknowledgement path then deletes the victim's message from whichever mailbox the global index happened to point at. So it is a cross-recipient denial of service plus a cross-recipient delete, from an authenticated but otherwise unprivileged position.

The distinction you drew is the general lesson: an identifier is only a safe key over the scope in which something guarantees its uniqueness. Message IDs are unique within a mailbox because a sender does not collide with itself, and nothing makes that true across mailboxes.

(recipient_did, message_id) is the right key. The detail I would call out is the legacy unscoped acknowledgement: rather than preserving the old behaviour, it now refuses when an ID is ambiguous across recipients. That path can delete someone else's message, so making the compatibility route strictly more restrictive than before is correct, and it is the opposite of what a compatibility shim usually does. Noting that InMemoryInboxStore is the only implementation and no caller loses relied-upon behaviour is the check that makes it safe to do.

Both of your red checks were one platform outage. docker-compose-test failed inside the image build on curl: (22) The requested URL returned error: 503 fetching the OPA binary from GitHub releases, through all its retries, and ci-complete failed only because docker-compose-test did. Nothing to do with your diff. I re-ran it and the run is green.

That 503 hit several PRs today across gitleaks, Validate PR title and this Docker build, so a maintainer triaging the queue right now will see healthy PRs looking broken.

No approve bit on this repo, so a comment, but I would merge it.

@liamcrumm
liamcrumm merged commit 3b27fdc into microsoft:main Sep 8, 2026
211 of 213 checks passed
@1aifanatic
Naveen Chatlapalli (1aifanatic) deleted the contrib/fix-3526-relay-recipient-dedupe branch September 8, 2026 20:55
Karim Mehalebi (karimad) pushed a commit to karimad/agent-governance-toolkit that referenced this pull request Sep 10, 2026
Karim Mehalebi (karimad) pushed a commit to karimad/agent-governance-toolkit that referenced this pull request Sep 14, 2026
Yuvraj Singh (yuvrajsingh2428) pushed a commit to yuvrajsingh2428/agent-governance-toolkit that referenced this pull request Oct 1, 2026
Signed-off-by: Naveen Chatlapalli <[email protected]>
Signed-off-by: yuvrajsingh2428 <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent-mesh agent-mesh package size/M Medium PR (< 200 lines) tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

AgentMesh relay: inbox deduplication is keyed globally by message id, not per recipient

4 participants