fix: resolve userId for user-scoped MCP credentials in playground auth path#2899
Conversation
…h path The playground "Try it" auth flows through tryAppCredentialAuth which sets endUserId in metadata but not initiatedBy. getUserIdFromContext only checked initiatedBy, causing userId to be undefined for all user-scoped credential lookups (e.g. Notion OAuth). This broke tool discovery (401) and MCP connections (invalid_token) for any user-scoped tool used from the playground. Falls back to metadata.endUserId when initiatedBy is not set. Made-with: Cursor
|
|
The latest updates on your projects. Learn more about Vercel for GitHub.
1 Skipped Deployment
|
|
TL;DR — Fixes playground "Try it" returning 401 for agents with user-scoped MCP tools (e.g. Notion OAuth). The Key changes
Summary | 2 files | 1 commit | base:
The identical change is applied to both copies of the function — the canonical one in the
|
There was a problem hiding this comment.
PR Review Summary
(0) Total Issues | Risk: Low
✅ No Blocking Issues Found
The fix correctly addresses the bug where getUserIdFromContext() returns undefined for the playground auth path. The implementation is sound:
- Fallback order is correct:
initiatedBy.id(internal user) takes precedence overendUserId(app credential auth) - Security is preserved: User-scoped credentials can only be created through the manage domain OAuth flow (requires internal user session). External end-users from web_client apps cannot create credentials, so even if
endUserIdis returned, no matching credential will exist for lookup. - Both duplicate files updated: The identical change was correctly applied to both
domains/run/types/executionContext.tsandtypes/runExecutionContext.ts
💭 Consider (2) 💭
💭 1) system Duplicate function definition across two files
Issue: getUserIdFromContext is defined identically in two files: agents-api/src/domains/run/types/executionContext.ts and agents-api/src/types/runExecutionContext.ts.
Why: This creates a maintenance burden where changes must be synchronized manually. The PR correctly updated both, but future changes could drift.
Fix: Consider consolidating into a single canonical location (likely domains/run/types/executionContext.ts since it's re-exported from agents-api/src/index.ts) and re-exporting from the other location. This can be a separate follow-up PR.
Refs:
💭 2) system Track the metadata normalization work
Issue: The PR description notes this is an "interim fix" and a more comprehensive approach would normalize the metadata shape across auth strategies so initiatedBy is always set when a user identity is available.
Why: The current design conflates two semantically different identity types (internal platform users vs external end-users) which could lead to confusion in the future.
Fix: Consider creating a tracking issue for the metadata normalization work to ensure it doesn't get lost.
Inline Comments:
- 🟡 Minor:
executionContext.ts:4-7Docstring doesn't reflect new behavior - 🟡 Minor:
runExecutionContext.ts:4-7Docstring also needs update here
✅ APPROVE
Summary: This is a clean, targeted bug fix that correctly resolves the playground authentication issue for user-scoped MCP tools. The fallback logic is sound, security is preserved (external end-users cannot create credentials), and both duplicate function definitions were updated. The inline comments suggest minor docstring updates for accuracy. Ship it! 🚀
Discarded (3)
| Location | Issue | Reason Discarded |
|---|---|---|
executionContext.ts:9-11 |
Type assertion duplicates canonical type | Verified that BaseExecutionContext.metadata in agents-core already includes both initiatedBy and endUserId. However, the assertion is still needed here because ctx.metadata is typed as the broader Record<string, unknown> from the interface. The assertion narrows it appropriately. |
system |
Cross-identity credential exposure risk | After tracing the code: credentials are only created through manage domain OAuth (requires internal user session). External end-users cannot create credentials. The lookup key (tenantId, projectId, toolId, userId) would never match for an external endUserId since no credential exists. |
system |
Missing unit tests | While tests would be nice, this is a simple fallback logic change. The existing auth middleware tests in runAuth-appCredentialAuth.test.ts already verify endUserId is set correctly in metadata. |
Reviewers (3)
| Reviewer | Returned | Main Findings | Consider | While You're Here | Inline Comments | Pending Recs | Discarded |
|---|---|---|---|---|---|---|---|
pr-review-security-iam |
2 | 0 | 0 | 0 | 0 | 0 | 2 |
pr-review-standards |
2 | 0 | 1 | 0 | 0 | 0 | 1 |
pr-review-consistency |
3 | 0 | 1 | 0 | 2 | 0 | 0 |
| Total | 7 | 0 | 2 | 0 | 2 | 0 | 3 |
There was a problem hiding this comment.
Correct targeted fix. The fallback to metadata.endUserId is safe — getUserScopedCredentialReference queries by (tenantId, projectId, toolId, userId), and no credential references exist for external end-user IDs from customer-facing web_client apps, so the lookup returns undefined gracefully (same as the previous undefined from getUserIdFromContext).
One low-priority observation about the duplicate file.
Claude Opus | 𝕏
| | undefined; | ||
| return metadata?.initiatedBy?.type === 'user' ? metadata.initiatedBy.id : undefined; | ||
| if (metadata?.initiatedBy?.type === 'user') return metadata.initiatedBy.id; | ||
| return metadata?.endUserId; |
There was a problem hiding this comment.
getUserIdFromContext in this file is dead code — it is exported but never imported anywhere. The only consumer of this file is runAuth.ts, which imports createBaseExecutionContext only. Consider deleting this function (or the entire file, migrating the one createBaseExecutionContext import in runAuth.ts to ../domains/run/types/executionContext) in a follow-up to avoid maintaining two copies.
There was a problem hiding this comment.
Looks good. The fix correctly resolves userId for user-scoped MCP credentials in the playground auth path by falling back to metadata.endUserId. The security invariant holds — no credential references exist for external end-user IDs, so non-playground web_client apps are unaffected.
Claude Opus | 𝕏
Ito Test Report ❌5 test cases ran. 2 failed, 3 passed. Overall, 5 test cases were executed in the unified run with 3 passes and 2 failures, while additional exploratory checks reported no confirmed defects or reportable outcomes. The key positives were that Section C regressions passed (non-playground API-key flow remained healthy, missing user identity metadata avoided production crashes, and initiatedBy correctly took precedence over endUserId), but two pre-existing authentication/validation defects were confirmed: a High-severity app isolation gap where mismatched x-inkeep-app-id and JWT could be accepted, and a Medium-severity error-handling bug where tampered scope headers returned 500 instead of a controlled 4xx denial. ❌ Failed (2)
|
…r-user credential After #2899 correctly resolved userId for playground requests, user-scoped tools now enter the user-credential lookup path. If no per-user credential exists, the code previously gave up. This adds an explicit fallback to the project-level credentialReferenceId, restoring the pre-existing behavior where tools configured with a shared project credential continued to work. Made-with: Cursor
…CP credentials The app credential auth paths (both authenticated and anonymous web_client) were setting endUserId but not initiatedBy in the execution context metadata. getUserIdFromContext checks initiatedBy first, so user-scoped MCP tools (like Linear OAuth) couldn't resolve credentials — the userId was undefined, causing the user-scoped credential lookup to be skipped entirely. This is a follow-up to #2899 which fixed the playground auth path but missed the app credential paths. Made-with: Cursor
…CP credentials (#2912) * fix: set initiatedBy for app credential auth to resolve user-scoped MCP credentials The app credential auth paths (both authenticated and anonymous web_client) were setting endUserId but not initiatedBy in the execution context metadata. getUserIdFromContext checks initiatedBy first, so user-scoped MCP tools (like Linear OAuth) couldn't resolve credentials — the userId was undefined, causing the user-scoped credential lookup to be skipped entirely. This is a follow-up to #2899 which fixed the playground auth path but missed the app credential paths. Made-with: Cursor * chore: add changeset Made-with: Cursor
…CP credentials (#2912) * fix: set initiatedBy for app credential auth to resolve user-scoped MCP credentials The app credential auth paths (both authenticated and anonymous web_client) were setting endUserId but not initiatedBy in the execution context metadata. getUserIdFromContext checks initiatedBy first, so user-scoped MCP tools (like Linear OAuth) couldn't resolve credentials — the userId was undefined, causing the user-scoped credential lookup to be skipped entirely. This is a follow-up to #2899 which fixed the playground auth path but missed the app credential paths. Made-with: Cursor * chore: add changeset Made-with: Cursor
Summary
invalid_token/ 401 for any agent using user-scoped MCP tools (e.g. Notion OAuth). Reproducible across all agents.tryAppCredentialAuth) setsendUserIdin execution context metadata, butgetUserIdFromContext()only checkedmetadata.initiatedBy.type === 'user'— which is never set in the app credential auth path. This causeduserIdto beundefined, breaking user-scoped credential reference lookups in both tool discovery andAgentMcpManager.getUserIdFromContextnow falls back tometadata.endUserIdwheninitiatedByis not set. This is safe for customer-facing web_client apps too — no credential references exist for external end-user IDs, so the lookup returnsundefinedgracefully (same behavior as before).Test plan
Made with Cursor