Skip to content

Fix/mcp conversation ownership - #600

Merged
ginccc merged 9 commits into
mainfrom
fix/mcp-conversation-ownership
Jul 20, 2026
Merged

Fix/mcp conversation ownership#600
ginccc merged 9 commits into
mainfrom
fix/mcp-conversation-ownership

Conversation

@ginccc

@ginccc ginccc commented Jul 15, 2026

Copy link
Copy Markdown
Member

Summary

This pull request addresses major security gaps in the MCP (Management Control Plane) conversation tools by enforcing ownership and admin checks for all conversation-related operations, aligning them with the stricter REST API policies. The changes ensure that only conversation owners or admins can read or modify conversations and related logs, thus preventing unauthorized access or modification by users with only the general viewer role. Additionally, the implementation centralizes access control logic to prevent future drift and improves test coverage for these security boundaries.

Security and Access Control Tightening:

  • Introduced ConversationAccessGuard to enforce owner-or-admin checks for all MCP conversation tools, ensuring only authorized users can read, list, or modify conversations and their logs. This closes gaps where eddi-viewer users could previously access or inject into any conversation. [1] [2] [3] [4] [5] [6] [7] [8] [9]

  • Updated conversation creation in MCP to stamp the caller as the conversation owner, preventing orphaned conversations that would otherwise be unreadable due to ownership checks. [1] [2] [3]

  • Added uniform, non-leaking error handling for access denials, ensuring error messages do not reveal the existence of other users’ conversations. [1] [2] [3]

Alignment and Refactoring:

  • Refactored RestAgentEngine to delegate ownership validation to ConversationAccessGuard, ensuring consistent access control logic between REST and MCP surfaces. Removed the now-redundant direct descriptor store dependency and validation logic. [1] [2] [3] [4] [5]

Security for Log Access:

  • Hardened read_agent_logs to require eddi-admin for unscoped or agent-scoped log reads, preventing cross-user log exposure. Conversation-scoped log reads remain available to owners or admins, preserving self-service diagnostics without cross-user leakage.

Tests and Documentation:

  • Extended and added tests (McpConversationToolsOwnershipTest, ConversationAccessGuardTest) to cover all new access restrictions and ensure no data is leaked to unauthorized users. Updated documentation and changelog with detailed rationale and design decisions.

These changes close critical authorization gaps, align access policies across all interfaces, and improve maintainability by centralizing access logic.

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✨ New feature (non-breaking change that adds functionality)
  • 💥 Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • 📝 Documentation update
  • ♻️ Refactoring (no functional changes)
  • 🔧 Chore (dependency updates, CI changes, etc.)

Checklist

  • My code follows the project's code style
  • I have added tests that prove my fix/feature works
  • Existing tests pass locally (./mvnw clean verify -DskipITs)
  • I have updated documentation if needed
  • My commit messages follow conventional commits
  • I have not committed any secrets, API keys, or tokens
  • This PR has a clear, focused scope (one concern per PR)

Summary by CodeRabbit

  • Security
    • Introduced centralized conversation “owner-or-admin” authorization across REST and MCP, with consistent access-denied behavior.
    • Enforced ownership for MCP read/write actions, prevented cross-user message sending/continuation and managed-conversation impersonation.
    • Tightened MCP agent log access for unscoped/agent-only reads to require elevated permissions.
  • Improvements
    • Owner-scoped conversation listing now uses a precise scan budget, stops early when exhausted, and emits new metrics for access denials and owner-scan exhaustion.
    • MCP-created conversations are now stamped with the requesting owner.
  • Documentation
    • Updated the changelog with the new security and observability details.
  • Tests
    • Added/expanded authorization and listing coverage, including new metrics assertions.

ginccc added 3 commits July 14, 2026 23:40
Every conversation-scoped tool in McpConversationTools was gated on the coarse
eddi-viewer role and nothing else, while the equivalent REST endpoints all
enforce requireOwnerOrAdmin. With authorization.enabled=true any viewer could,
over MCP, read ANY user's conversation memory and transcript, enumerate all
users' conversations, read another conversation's audit trail (prompts, tool
calls, costs) and server logs, inject turns into someone else's conversation and
read the agent's reply, and take over a managed conversation by naming another
userId. The read half also defeated the group-conversation ownership gate: group
member conversations are ordinary conversations, so list_conversations +
read_conversation_log reached transcripts that read_group_conversation denies.

A naive gate would have broken MCP outright: MCP created conversations with a
null userId, which the engine turns into a generated anonymous-<uuid> — a
non-blank owner matching no principal — so the creator itself could never read
its own conversation back. The fix therefore also stamps the caller as owner at
creation, which is what makes the gate effective instead of merely restrictive.

- New ConversationAccessGuard (engine.security), the non-HITL sibling of
  HitlAccessGuard: requireConversationOwner (owner-or-admin, skip on missing
  descriptor, fail-closed on store error), canAccessConversation /
  seesAllConversations for listings, resolveOwnerUserId for creation. This is
  RestAgentEngine's private ownership check lifted out, so REST and MCP cannot
  drift apart on who may read or drive a conversation.
- RestAgentEngine delegates to the guard (behavior identical; its now-dead
  IConversationDescriptorStore dependency dropped).
- McpConversationTools gates all eight conversation-scoped tools, returning a
  uniform non-leaking "Access denied" that never distinguishes "not yours" from
  "does not exist"; list_conversations owner-filters and over-fetches the store's
  full page so a personal list is not starved by other users' conversations.

Default deployments (authorization.enabled=false) are unaffected — every check
no-ops. With auth on, pre-existing anonymous-* conversations become invisible to
non-admins over MCP: they provably belong to nobody.

Tests: ConversationAccessGuardTest and McpConversationToolsOwnershipTest, which
asserts per tool that a non-owner is denied AND the underlying service is never
reached, while owner and admin pass.
… page

Self-review of the ownership fix: list_conversations filtered a single 100-row
page, which silently starves a personal list. On a shared agent the newest page
is often entirely other users' conversations, so a non-admin caller got count: 0
— indistinguishable from "you have no conversations" — and the requested limit
stopped meaning anything (asking for 20 could return 3 while 50 existed).

- Scan forward page by page until the limit is filled or a 500-descriptor budget
  is spent, instead of filtering only the newest page.
- Dedupe by resource URI: the store's own paging skips deleted rows, so its
  cursor can outrun the rows it hands back and an offset-based scan can re-read
  one, listing a conversation twice.
- Report incomplete: true (with a note) when the scan stops on its budget rather
  than on the store running out — AGENTS.md "no silent caps", rather than passing
  a partial list off as complete.
- chat_managed's denial message said "you cannot chat as another user", but the
  same catch also fires when a stale intent→conversation mapping points at a
  conversation the caller does not own. One accurate message now covers both
  without disclosing which.

Tests: list_conversations gains scan-past-foreign-pages, budget-exhausted
(incomplete), and cross-page dedupe cases.
read_agent_logs enforced ownership only when a conversationId filter was
supplied; without one (unfiltered, or filtered by agentId alone) it still
returned the shared cross-user server-log buffer — workflow logs, LLM
provider errors, internal diagnostics that can quote other users'
conversation data — to any caller holding eddi-viewer.

Require eddi-admin for the unscoped/agent-only path, matching the REST log
endpoint IRestLogAdmin (@RolesAllowed("eddi-admin")) so MCP is not the more
permissive door. The conversation-scoped path is unchanged (owner-or-admin
via ConversationAccessGuard); BoundedLogStore filters by exact
conversationId, so a scoped read returns only that one conversation's lines.
The admin check sits before the try so a role denial surfaces as an honest
role error rather than the ownership "Access denied" message.

Closes the residual read_agent_logs gap filed by the ownership commit.
@ginccc
ginccc requested a review from rolandpickl as a code owner July 15, 2026 07:08
@github-actions

Copy link
Copy Markdown

⚠️ Deprecation Warning: The deny-licenses option is deprecated for possible removal in the next major release. For more information, see issue 997.

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

None

@ginccc
ginccc requested review from Copilot and removed request for rolandpickl July 15, 2026 07:09
@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 066c52ec-92fe-4bbf-ae2e-043a225dcd16

📥 Commits

Reviewing files that changed from the base of the PR and between 3e6ee91 and 08ab16a.

📒 Files selected for processing (1)
  • docs/changelog.md

📝 Walkthrough

Walkthrough

Conversation ownership authorization is centralized in ConversationAccessGuard and applied to REST and MCP conversation operations. MCP creation, reads, writes, listing, logs, audit access, and managed chat now enforce owner/admin rules with expanded tests.

Changes

Conversation access authorization

Layer / File(s) Summary
Conversation access guard contract
src/main/java/ai/labs/eddi/engine/security/ConversationAccessGuard.java, src/test/java/ai/labs/eddi/engine/security/ConversationAccessGuardTest.java
Adds ownership checks, admin visibility, listing predicates, owner resolution, fail-closed behavior, and tests.
REST conversation listing enforcement
src/main/java/ai/labs/eddi/engine/memory/rest/RestConversationStore.java, src/test/java/ai/labs/eddi/engine/memory/rest/*
Filters descriptors by owner, handles legacy ownership through snapshots, backfills pages, and bounds owner-filtered scans.
MCP ownership enforcement and listing
src/main/java/ai/labs/eddi/engine/mcp/McpConversationTools.java, src/test/java/ai/labs/eddi/engine/mcp/McpConversationToolsOwnershipTest.java
Applies ownership checks to MCP operations, stamps owners, restricts unscoped logs, standardizes denials, and delegates listing visibility to the store.
REST engine authorization wiring
src/main/java/ai/labs/eddi/engine/internal/RestAgentEngine.java, src/test/java/ai/labs/eddi/engine/internal/*
Replaces direct descriptor-store ownership validation with ConversationAccessGuard while retaining the HITL-specific path.
MCP test and constructor migration
src/test/java/ai/labs/eddi/engine/mcp/McpConversationToolsTest.java, src/test/java/ai/labs/eddi/engine/mcp/McpConversationToolsExtendedTest.java, src/test/java/ai/labs/eddi/engine/mcp/McpConversationToolsHitlTest.java
Updates existing MCP test wiring for the new identity and guard dependencies.
Security changelog updates
docs/changelog.md
Documents denial metrics, owner-scan exhaustion, listing behavior, log authorization, and missing-descriptor semantics.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant MCPClient
  participant MCPConversationTools
  participant ConversationAccessGuard
  participant ConversationService
  MCPClient->>MCPConversationTools: invoke conversation operation
  MCPConversationTools->>ConversationAccessGuard: authorize conversation
  ConversationAccessGuard-->>MCPConversationTools: allow or deny
  MCPConversationTools->>ConversationService: perform authorized operation
  ConversationService-->>MCPConversationTools: return result
  MCPConversationTools-->>MCPClient: result or access-denied response
Loading

Possibly related PRs

  • labsai/EDDI#458: Modifies the same REST conversation listing pagination and overflow paths.
  • labsai/EDDI#515: Modifies overlapping MCP chat_managed handling.
  • labsai/EDDI#530: Modifies overlapping conversation ownership validation.

Suggested reviewers: rolandpickl

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.13% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and directly reflects the main change: fixing MCP conversation ownership authorization.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/mcp-conversation-ownership

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR tightens authorization for MCP conversation tools by enforcing conversation ownership (or admin) checks and aligning MCP behavior with the stricter REST access policies via a shared ConversationAccessGuard.

Changes:

  • Introduces ConversationAccessGuard as a shared owner-or-admin gate used by both REST (RestAgentEngine) and MCP (McpConversationTools).
  • Updates MCP conversation flows to (a) stamp the caller as the owner on new conversations and (b) enforce ownership checks before reading/driving conversations, audit trails, and conversation-scoped logs.
  • Expands unit tests to cover these security boundaries and documents the changes in the changelog.

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/main/java/ai/labs/eddi/engine/security/ConversationAccessGuard.java New shared owner-or-admin guard for conversation access and owner stamping.
src/main/java/ai/labs/eddi/engine/mcp/McpConversationTools.java Applies ownership/admin checks across MCP conversation tools; adds owner-scoped listing behavior and log gating.
src/main/java/ai/labs/eddi/engine/internal/RestAgentEngine.java Refactors REST engine to delegate conversation ownership checks to ConversationAccessGuard.
src/test/java/ai/labs/eddi/engine/security/ConversationAccessGuardTest.java New unit tests for guard behavior (owner/admin/unowned/auth-disabled/store errors).
src/test/java/ai/labs/eddi/engine/mcp/McpConversationToolsOwnershipTest.java New MCP security regression tests ensuring non-owners are denied and services aren’t reached.
src/test/java/ai/labs/eddi/engine/mcp/McpConversationToolsTest.java Updates tool wiring to pass a ConversationAccessGuard.
src/test/java/ai/labs/eddi/engine/mcp/McpConversationToolsHitlTest.java Updates tool wiring to pass a ConversationAccessGuard while keeping HITL behavior covered.
src/test/java/ai/labs/eddi/engine/mcp/McpConversationToolsExtendedTest.java Updates tool wiring to pass a ConversationAccessGuard.
src/test/java/ai/labs/eddi/engine/internal/RestAgentEngineTest.java Updates REST engine construction to include ConversationAccessGuard.
src/test/java/ai/labs/eddi/engine/internal/RestAgentEngineHitlTest.java Updates REST engine construction to include ConversationAccessGuard.
src/test/java/ai/labs/eddi/engine/internal/RestAgentEngineToolPauseDetailsTest.java Updates REST engine construction to include ConversationAccessGuard.
docs/changelog.md Adds rationale, behavior notes, and testing notes for the security changes.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/main/java/ai/labs/eddi/engine/mcp/McpConversationTools.java Outdated
Comment thread src/main/java/ai/labs/eddi/engine/mcp/McpConversationTools.java
Comment thread src/test/java/ai/labs/eddi/engine/mcp/McpConversationToolsOwnershipTest.java Outdated
Comment thread src/test/java/ai/labs/eddi/engine/mcp/McpConversationToolsOwnershipTest.java Outdated

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 6

🧹 Nitpick comments (1)
src/main/java/ai/labs/eddi/engine/internal/RestAgentEngine.java (1)

98-100: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Delegate owner resolution to ConversationAccessGuard.

Since ConversationAccessGuard was introduced to centralize conversation ownership logic and specifically provides resolveOwnerUserId() to stamp new conversations, you can delegate this call directly to the guard instead of invoking OwnershipValidator.

♻️ Proposed refactor
         try {
-            String resolvedUserId = ownershipValidator.validateAndResolveUserId(identity, userId);
+            String resolvedUserId = conversationAccessGuard.resolveOwnerUserId(userId);
             var result = conversationService.startConversation(environment, agentId, resolvedUserId, context);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/ai/labs/eddi/engine/internal/RestAgentEngine.java` around lines
98 - 100, In the conversation-start flow, replace the direct
ownershipValidator.validateAndResolveUserId call with
ConversationAccessGuard.resolveOwnerUserId(), passing the same identity and
userId inputs, and use its result when calling
conversationService.startConversation. Remove the now-unneeded direct
OwnershipValidator dependency for this path.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/changelog.md`:
- Line 42: Update the `docs/changelog.md` entry to remove or correct the claim
that `McpConversationTools` provides uniform non-disclosing access denial; state
the actual behavior until missing and foreign conversation paths are normalized.

In `@src/main/java/ai/labs/eddi/engine/mcp/McpConversationTools.java`:
- Around line 90-97: Inject MeterRegistry into McpConversationTools and register
counters for conversation ownership denials and exhausted listing scans.
Increment the ownership counter wherever ConversationAccessGuard rejects access
and increment the scan counter when listing reaches its exhaustion condition,
using descriptive metric names and the existing MeterRegistry conventions.
- Around line 682-686: Update the managed-conversation tool flow around
conversationAccessGuard.resolveOwnerUserId to stop accepting or trusting userId
as an explicit tool argument. Obtain the conversation owner from
IConversationMemory instead, while preserving authorization for the current
conversation; move any cross-user impersonation behavior into a separate
administrative operation.
- Around line 114-121: Update McpConversationTools call paths that invoke
ConversationAccessGuard.requireConversationOwner() to treat a null conversation
descriptor the same as ForbiddenException by returning accessDenied(...),
preserving the uniform denial response. Update docs/changelog.md at the affected
entry to document that missing conversation descriptors now fail closed with the
same access-denied behavior.
- Around line 408-449: Add the existing `@Blocking` annotation to the
list_conversations MCP tool method, matching the annotation style used by other
synchronous MCP tools. Keep its owner-scoped scanning and store-read behavior
unchanged.
- Around line 423-452: Fix the pagination loop around
readConversationDescriptors so deleted descriptors cannot cause offset re-reads
or premature exhaustion. Use an absolute offset/continuation mechanism supported
by the store, advance it according to the store’s actual paging contract rather
than page.size(), and remove the page-size exhaustion assumption unless the API
explicitly guarantees it; preserve URI deduplication, access filtering, the scan
budget, and incomplete reporting.

---

Nitpick comments:
In `@src/main/java/ai/labs/eddi/engine/internal/RestAgentEngine.java`:
- Around line 98-100: In the conversation-start flow, replace the direct
ownershipValidator.validateAndResolveUserId call with
ConversationAccessGuard.resolveOwnerUserId(), passing the same identity and
userId inputs, and use its result when calling
conversationService.startConversation. Remove the now-unneeded direct
OwnershipValidator dependency for this path.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0542748d-280a-4019-9b5a-055694f0c27e

📥 Commits

Reviewing files that changed from the base of the PR and between 3a74974 and 8f0324b.

📒 Files selected for processing (12)
  • docs/changelog.md
  • src/main/java/ai/labs/eddi/engine/internal/RestAgentEngine.java
  • src/main/java/ai/labs/eddi/engine/mcp/McpConversationTools.java
  • src/main/java/ai/labs/eddi/engine/security/ConversationAccessGuard.java
  • src/test/java/ai/labs/eddi/engine/internal/RestAgentEngineHitlTest.java
  • src/test/java/ai/labs/eddi/engine/internal/RestAgentEngineTest.java
  • src/test/java/ai/labs/eddi/engine/internal/RestAgentEngineToolPauseDetailsTest.java
  • src/test/java/ai/labs/eddi/engine/mcp/McpConversationToolsExtendedTest.java
  • src/test/java/ai/labs/eddi/engine/mcp/McpConversationToolsHitlTest.java
  • src/test/java/ai/labs/eddi/engine/mcp/McpConversationToolsOwnershipTest.java
  • src/test/java/ai/labs/eddi/engine/mcp/McpConversationToolsTest.java
  • src/test/java/ai/labs/eddi/engine/security/ConversationAccessGuardTest.java

Comment thread docs/changelog.md
Comment thread src/main/java/ai/labs/eddi/engine/mcp/McpConversationTools.java
Comment thread src/main/java/ai/labs/eddi/engine/mcp/McpConversationTools.java
Comment thread src/main/java/ai/labs/eddi/engine/mcp/McpConversationTools.java Outdated
Comment thread src/main/java/ai/labs/eddi/engine/mcp/McpConversationTools.java Outdated
Comment thread src/main/java/ai/labs/eddi/engine/mcp/McpConversationTools.java
ginccc added 3 commits July 15, 2026 10:32
RestConversationStore.readConversationDescriptors (GET
/conversationstore/conversations) had no @RolesAllowed and no ownership
filter, so any authenticated caller could enumerate every user's
conversation descriptors (id, agent, state, owner). Inject
ConversationAccessGuard and owner-filter the listing inside the existing
paging do-while: admins and auth-disabled callers short-circuit via
seesAllConversations(), and the loop back-fills across pages so a
personal list is never starved. This is the REST twin of the MCP
list_conversations ownership fix; direct callers (incl. the EDDI-Manager
UI) hit the endpoint with their own identity, so filtering is correct.

Also simplify McpConversationTools.listConversations to a single store
call, removing its owner-scoping over-fetch loop. That loop's only
runtime path (auth-on, non-admin) 401s at the unauthenticated internal
loopback before it runs, and it carried a latent page-index bug
(scanned row-count passed as the store's page index). Under auth-off the
store returns all and the tool relays it, unchanged from before.

Tests: new RestConversationStoreOwnershipTest (own-only, intruder sees
nothing, admin-all, legacy-unowned visible, personal list back-filled
across foreign pages); wire the guard through the existing
RestConversationStore(Filter)Test constructors; reduce the MCP ownership
listing test to a delegation check.
Adversarial review of the previous commit found that owner-filtering
GET /conversationstore/conversations turned the default non-admin
Manager list view into an O(total-conversations) scan under
authorization.enabled=true: the ownership gate ran AFTER
populateDataToDescriptor (a full memory-document load per row) and the
back-fill had no scan budget, so a caller owning few/none of a large
shared store paged the entire descriptor collection, one snapshot load
per row — a repeatable authenticated DoS-amplification primitive. The
access-control result was correct; the cost was not.

- Split the ownership check around the snapshot load: for the common
  case (owner recorded on the descriptor since v5.1.6) decide on
  descriptor.getUserId() BEFORE populateDataToDescriptor, so a foreign
  row is skipped without loading its memory document; only a legacy
  null-owner row falls through to the post-populate re-check that
  resolves the owner from the snapshot.
- Cap the back-fill at MAX_OWNER_SCAN=500 (mirroring the MCP twin's
  budget) so a sparse owner cannot force a full-collection scan. Admins
  and auth-disabled callers are never filtered and never reach the bound.

Tests: RestConversationStoreOwnershipTest gains a foreign-row-skipped-
without-snapshot-load case, a legacy null-owner-resolved-to-foreign-owner
case (guards the post-populate ordering so a reorder above populate now
fails), and a scan-budget-bounded case. Changelog updated with the cost
tradeoff and an owner-scoped-query follow-up.
The two verify(never()).loadConversationMemorySnapshot(anyString())
assertions were vacuous: the test descriptors used non-hex ids, so
extractResourceId(...).getId() returned null and a reordered populate
would call loadConversationMemorySnapshot(null), which anyString() does
not match — so the assertion stayed green even under the DoS reorder.
Give those descriptors valid hex ids so getId() is non-null; a mutation
test (moving populateDataToDescriptor before the ownership check) now
makes both foreignRowSkippedWithoutSnapshotLoad and
scanIsBoundedForSparseOwner fail, so the O(store) DoS reorder can no
longer ship green.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/main/java/ai/labs/eddi/engine/mcp/McpConversationTools.java (1)

389-404: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Forward the caller auth context to the delegated conversation-store request
RestInterfaceFactory.get(IRestConversationStore.class) always creates a new REST client to http://127.0.0.1:<port>, so this call does not automatically carry the MCP caller’s SecurityIdentity. That leaves ConversationAccessGuard running under the wrong principal on the receiving side and can break the owner-filtering guarantee for list_conversations. Forward the caller’s Authorization header here, or inject the store directly instead of going through HTTP.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/ai/labs/eddi/engine/mcp/McpConversationTools.java` around lines
389 - 404, The conversation-store lookup in the list_conversations flow loses
the MCP caller’s authorization context when RestInterfaceFactory.get creates its
HTTP client. Update the request path around
RestInterfaceFactory.get(IRestConversationStore.class) to forward the caller’s
Authorization header to the delegated REST request, or replace the REST lookup
with direct store injection, ensuring ConversationAccessGuard evaluates the
original caller and owner filtering remains enforced.
🧹 Nitpick comments (1)
src/main/java/ai/labs/eddi/engine/memory/rest/RestConversationStore.java (1)

77-104: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add Micrometer metrics for the new ownership-filtering path.

This listing now enforces owner-scoping and a hard scan budget (MAX_OWNER_SCAN), but there's no counter/gauge tracking denied/foreign rows skipped or budget-exhaustion events. Without this, a caller silently hitting the scan cap (getting an incomplete list) is operationally invisible.

As per coding guidelines, "Add Micrometer metrics to new features, using counters, timers, or gauges registered through MeterRegistry."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/ai/labs/eddi/engine/memory/rest/RestConversationStore.java`
around lines 77 - 104, Inject a Micrometer MeterRegistry into
RestConversationStore and add metrics for the ownership-filtering path: count
denied or foreign rows skipped and count events where MAX_OWNER_SCAN is
exhausted. Increment these metrics at the corresponding filtering and scan-cap
branches so incomplete listings are observable, using the project’s established
metric naming and registration conventions.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/changelog.md`:
- Around line 18-19: The changelog's “never starved” claim must be qualified by
the owner-scan limit. Update the first bullet around readDescriptors and the
endpoint’s do-while to state that back-filling prevents starvation only within
the MAX_OWNER_SCAN budget, while preserving the existing explanation that no
resource-URI deduplication is required.

In `@src/main/java/ai/labs/eddi/engine/memory/rest/RestConversationStore.java`:
- Around line 55-61: Update the javadoc for the owner-scan limit in
RestConversationStore to remove the stale reference to an MCP-specific cap and
state that this store provides the sole owner-scan budget used by delegated
filtering. Preserve the existing explanation of the bound and the
admin/auth-disabled exception.

---

Outside diff comments:
In `@src/main/java/ai/labs/eddi/engine/mcp/McpConversationTools.java`:
- Around line 389-404: The conversation-store lookup in the list_conversations
flow loses the MCP caller’s authorization context when RestInterfaceFactory.get
creates its HTTP client. Update the request path around
RestInterfaceFactory.get(IRestConversationStore.class) to forward the caller’s
Authorization header to the delegated REST request, or replace the REST lookup
with direct store injection, ensuring ConversationAccessGuard evaluates the
original caller and owner filtering remains enforced.

---

Nitpick comments:
In `@src/main/java/ai/labs/eddi/engine/memory/rest/RestConversationStore.java`:
- Around line 77-104: Inject a Micrometer MeterRegistry into
RestConversationStore and add metrics for the ownership-filtering path: count
denied or foreign rows skipped and count events where MAX_OWNER_SCAN is
exhausted. Increment these metrics at the corresponding filtering and scan-cap
branches so incomplete listings are observable, using the project’s established
metric naming and registration conventions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: caf35910-9f10-4b37-a123-26a2eee41295

📥 Commits

Reviewing files that changed from the base of the PR and between 8f0324b and 86fe585.

📒 Files selected for processing (7)
  • docs/changelog.md
  • src/main/java/ai/labs/eddi/engine/mcp/McpConversationTools.java
  • src/main/java/ai/labs/eddi/engine/memory/rest/RestConversationStore.java
  • src/test/java/ai/labs/eddi/engine/mcp/McpConversationToolsOwnershipTest.java
  • src/test/java/ai/labs/eddi/engine/memory/rest/RestConversationStoreFilterTest.java
  • src/test/java/ai/labs/eddi/engine/memory/rest/RestConversationStoreOwnershipTest.java
  • src/test/java/ai/labs/eddi/engine/memory/rest/RestConversationStoreTest.java

Comment thread docs/changelog.md Outdated
…doc accuracy

Triaged the Copilot + CodeRabbit review of this branch. Most bot findings
targeted the MCP-side list_conversations over-fetch/scan loop that 009ca0f
already deleted (its page-index bug was the reason for the delete), so they were
moot against current HEAD. The substantive, still-valid items:

- Observability (Micrometer), per the repo "always add metrics to new features"
  convention — the new authorization paths were operationally invisible. Two
  counters via field-injected MeterRegistry (AGENTS.md metrics pattern, with a
  SimpleMeterRegistry default so unit tests that construct the bean directly stay
  non-null):
    * eddi.mcp.conversation.access.denied{tool} on every MCP ownership denial
      (the six gated read/drive tools via accessDenied, plus chat_managed's
      impersonation denial). MCP denials return a 200 error-body, so unlike REST
      403s they are not visible in http.server.requests.
    * eddi.conversations.listing.owner_scan_exhausted when a non-admin listing
      stops on the MAX_OWNER_SCAN budget with fewer than `limit` results — the
      truncation the List return type cannot signal to the caller.
- Fail-open on a missing descriptor: kept deliberately. requireConversationOwner
  returns null (operation proceeds → 404) rather than denying when the descriptor
  is absent; a missing descriptor means the conversation is genuinely not found,
  and flipping the SHARED guard to deny would change REST 404→403 and contradict
  its documented "let the operation handle the 404" contract. Softened
  accessDenied's javadoc, which had over-claimed a denial is indistinguishable
  from "does not exist".
- Doc accuracy: RestConversationStore.MAX_OWNER_SCAN javadoc no longer says it
  "mirrors the MCP owner-scan cap" (MCP has none since 009ca0f — this is now the
  sole budget); the prior changelog entry's "never starved" line is qualified.

Declined: removing userId from chat_managed. The cited rule exempts external
interfaces (MCP/REST) that operate outside a conversation, which is exactly what
chat_managed is (it routes to a per-intent+userId managed conversation rather
than running inside one); resolveOwnerUserId already rejects impersonation.

Tests: metric assertions for both counters (denial tagged by tool; budget
exhaustion). 215 affected tests green.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/main/java/ai/labs/eddi/engine/memory/rest/RestConversationStore.java (1)

145-145: 🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win

Enforce MAX_OWNER_SCAN within the descriptor loop.

The current check runs only after a full page is processed, so a non-admin request can scan up to MAX_OWNER_SCAN + limit - 1 descriptors. That violates the documented upper bound and can make the exhaustion metric report after more work than intended.

Proposed fix
for (var conversationDescriptor : conversationDescriptors) {
+    if (!seesAllConversations && scannedDescriptors >= MAX_OWNER_SCAN) {
+        break;
+    }
     scannedDescriptors++;

Also applies to: 153-154, 226-238

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/ai/labs/eddi/engine/memory/rest/RestConversationStore.java` at
line 145, Move the MAX_OWNER_SCAN enforcement into the descriptor-processing
loop that uses scannedDescriptors, checking the limit before processing each
additional descriptor so non-admin requests never scan beyond the configured
bound. Preserve the existing pagination and exhaustion-metric behavior while
stopping immediately when MAX_OWNER_SCAN is reached.
docs/changelog.md (1)

28-28: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Correct the ownership-check ordering description.

The implementation checks descriptors with a recorded userId before populateDataToDescriptor; only legacy descriptors without an owner are populated and then re-checked. Update this sentence to reflect both paths.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/changelog.md` at line 28, Update the RestConversationStore changelog
description to state that descriptors with a recorded userId are checked before
populateDataToDescriptor, while legacy descriptors without an owner are
populated first and then re-checked. Preserve the existing ownership and
visibility behavior details.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/changelog.md`:
- Line 33: Update the changelog entry’s description of MAX_OWNER_SCAN to remove
the claim that it mirrors an MCP budget. State that it is solely the REST
listing’s owner-scan budget, keeping the surrounding behavior and truncation
details unchanged.

---

Outside diff comments:
In `@docs/changelog.md`:
- Line 28: Update the RestConversationStore changelog description to state that
descriptors with a recorded userId are checked before populateDataToDescriptor,
while legacy descriptors without an owner are populated first and then
re-checked. Preserve the existing ownership and visibility behavior details.

In `@src/main/java/ai/labs/eddi/engine/memory/rest/RestConversationStore.java`:
- Line 145: Move the MAX_OWNER_SCAN enforcement into the descriptor-processing
loop that uses scannedDescriptors, checking the limit before processing each
additional descriptor so non-admin requests never scan beyond the configured
bound. Preserve the existing pagination and exhaustion-metric behavior while
stopping immediately when MAX_OWNER_SCAN is reached.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 07890f18-207f-44e8-b614-5a0672a944fb

📥 Commits

Reviewing files that changed from the base of the PR and between 86fe585 and b9a661c.

📒 Files selected for processing (5)
  • docs/changelog.md
  • src/main/java/ai/labs/eddi/engine/mcp/McpConversationTools.java
  • src/main/java/ai/labs/eddi/engine/memory/rest/RestConversationStore.java
  • src/test/java/ai/labs/eddi/engine/mcp/McpConversationToolsOwnershipTest.java
  • src/test/java/ai/labs/eddi/engine/memory/rest/RestConversationStoreOwnershipTest.java
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/test/java/ai/labs/eddi/engine/mcp/McpConversationToolsOwnershipTest.java
  • src/test/java/ai/labs/eddi/engine/memory/rest/RestConversationStoreOwnershipTest.java
  • src/main/java/ai/labs/eddi/engine/mcp/McpConversationTools.java

Comment thread docs/changelog.md Outdated
…ccuracy

Second CodeRabbit pass (its first pass predated the metrics/doc commit and was
already moot). Two valid, current items:

- Enforce MAX_OWNER_SCAN per-descriptor, not per-page. The budget was checked
  only in the do-while condition (after a full page), so a non-admin scan could
  reach MAX_OWNER_SCAN + limit - 1 before stopping — over the documented bound.
  Added an in-loop break. Impact is small (the overrun rows are within an
  already-fetched page, and foreign rows skip the snapshot load either way), but
  it makes the bound exact and owner_scan_exhausted fire at 500 rather than up to
  a page late. Not separately unit-tested: with all-foreign pages the store
  returns the same empty list and page-read count with or without the break, so
  the tightening is not observable through the store interface; the existing
  bounded-scan tests guard against regression.
- Changelog accuracy: the 009ca0f entry said the ownership check "runs after
  populateDataToDescriptor" — stale since 8bb304b split it (common case decides
  before the snapshot load; only a legacy null-owner row is re-checked after).
  Corrected, and dropped a second stale "mirroring the MCP twin's budget"
  reference (MCP has no scan cap since 009ca0f).
@ginccc
ginccc requested review from aisabella-ai and niedch July 15, 2026 22:25
@ginccc
ginccc merged commit 3e8e58f into main Jul 20, 2026
11 of 13 checks passed
@ginccc
ginccc deleted the fix/mcp-conversation-ownership branch July 20, 2026 16:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants