Skip to content

fix(github-work-app): Make gitHub MCP access schema project scoped#2574

Merged
miles-kt-inkeep merged 10 commits intomainfrom
efix/project-scoped-github-work-app
Mar 9, 2026
Merged

fix(github-work-app): Make gitHub MCP access schema project scoped#2574
miles-kt-inkeep merged 10 commits intomainfrom
efix/project-scoped-github-work-app

Conversation

@miles-kt-inkeep
Copy link
Copy Markdown
Contributor

No description provided.

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Mar 6, 2026

🦋 Changeset detected

Latest commit: 7050d4e

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 10 packages
Name Type
@inkeep/agents-core Patch
@inkeep/agents-work-apps Patch
@inkeep/agents-api Patch
@inkeep/agents-manage-ui Patch
@inkeep/agents-cli Patch
@inkeep/agents-sdk Patch
@inkeep/ai-sdk-provider Patch
@inkeep/create-agents Patch
@inkeep/agents-email Patch
@inkeep/agents-mcp Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel
Copy link
Copy Markdown

vercel Bot commented Mar 6, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
agents-manage-ui Ready Ready Preview, Comment Mar 9, 2026 1:43pm
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
agents-api Ignored Ignored Preview Mar 9, 2026 1:43pm
agents-docs Ignored Ignored Preview Mar 9, 2026 1:43pm

Request Review

@pullfrog
Copy link
Copy Markdown
Contributor

pullfrog Bot commented Mar 6, 2026

Scopes GitHub MCP tool access control to (tenantId, projectId, toolId) instead of toolId alone, preventing cross-tenant/cross-project data leakage.

  • SchemaworkAppGitHubMcpToolAccessMode primary key changed from (toolId) to (tenantId, projectId, toolId) (runtime-schema.ts, migration 0021_careful_xorn.sql)
  • Data accessgetMcpToolAccessMode, getMcpToolRepositoryAccessWithDetails, deleteMcpToolAccessMode, setMcpToolAccessMode, and cascadeDeleteByTool all updated to filter/conflict on the full (tenantId, projectId, toolId) scope (github-work-app-installations.ts, cascade-delete.ts)
  • Auth middlewaregithubMcpAuth now requires and extracts x-inkeep-tenant-id and x-inkeep-project-id headers (agents-work-apps/src/github/mcp/auth.ts)
  • Header propagationAgentMcpManager and discoverToolsFromServer send the new tenant/project headers to the GitHub MCP server (AgentMcpManager.ts, tools.ts)
  • MCP servergetServer and the Hono app accept the full ToolScope object (agents-work-apps/src/github/mcp/index.ts)
  • API routemcpToolGithubAccess passes tenantId/projectId to the updated data-access functions
  • Tests — All githubMcpToolAccess test call sites updated to pass the scoped params (githubMcpToolAccess.test.ts)

Pullfrog  | View workflow run | Using Claude Code | Triggered by Pullfrogpullfrog.com𝕏

Copy link
Copy Markdown
Contributor

@pullfrog pullfrog Bot left a comment

Choose a reason for hiding this comment

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

Medium urgency. The schema migration and auth layer look correct. The main concern: several data-access functions on the sibling workAppGitHubMcpToolRepositoryAccess table (getMcpToolRepositoryAccess, clearMcpToolRepositoryAccess, and the 'selected' mode path in getMcpToolRepositoryAccessWithDetails) still filter by toolId only — inconsistent with the project-scoping this PR establishes. If tool IDs are globally unique today this is functionally safe, but it undermines the defense-in-depth intent of the change. Test coverage for cross-project isolation is also missing.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow runpullfrog.com𝕏

@@ -960,7 +968,7 @@ export const getMcpToolRepositoryAccessWithDetails =
workAppGitHubInstallations,
eq(workAppGitHubRepositories.installationDbId, workAppGitHubInstallations.id)
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.

Inconsistent scoping in 'selected' mode path. This query still filters workAppGitHubMcpToolRepositoryAccess by scope.toolId only, while every other query in this PR now filters by the full (tenantId, projectId, toolId) compound key. The workAppGitHubMcpToolRepositoryAccess table has tenantId and projectId columns — this WHERE clause should include them for consistency and defense-in-depth.

The same issue applies to:

  • getMcpToolRepositoryAccess (~line 900) — filters by toolId only
  • clearMcpToolRepositoryAccess (~line 984) — deletes by toolId only
  • setMcpToolRepositoryAccess (~line 860) — deletes by toolId only before re-inserting
  • cascadeDeleteByTool in cascade-delete.ts (~line 458) — deletes repository access by toolId only

If tool IDs are globally unique today, this is functionally safe, but it contradicts the scoping model this PR establishes.

},
(table) => [
primaryKey({ columns: [table.toolId] }),
primaryKey({ columns: [table.tenantId, table.projectId, table.toolId] }),
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.

The PK change on this table is correct. Worth checking: the sibling workAppGitHubMcpToolRepositoryAccess table (~line 808) still has its unique constraint scoped as (toolId, repositoryDbId) without tenant/project. If the same toolId can now legitimately appear in different projects, that unique constraint would need updating too.

tenantId: ctx.tenantId,
projectId,
toolId,
});
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.

Missing cross-project isolation test. The whole point of making this project-scoped is that access configured for project A should not be visible when querying with project B's scope. Add a test that sets access for { tenantId, projectId: 'project-A', toolId }, then queries with { tenantId, projectId: 'project-B', toolId } and asserts an empty result.

tenantId: ctx.tenantId,
projectId,
toolId,
});
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.

getMcpToolAccessMode and deleteMcpToolAccessMode were both changed to accept a { tenantId, projectId, toolId } scope object, but neither has direct test coverage in this file. Consider adding tests that verify: (1) correct mode returned after setMcpToolAccessMode, (2) deleteMcpToolAccessMode clears it, and (3) querying with a different project scope returns the default 'selected' mode.

Copy link
Copy Markdown
Contributor

@claude claude Bot left a comment

Choose a reason for hiding this comment

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

PR Review Summary

(4) Total Issues | Risk: Medium

🟠⚠️ Major (2) 🟠⚠️

🟠 1) github-work-app-installations.ts:969 Incomplete scope filtering in 'selected' mode repository query

Issue: When mode is 'selected', the query only filters by scope.toolId without also filtering by scope.tenantId and scope.projectId. This inconsistency means the repository access query doesn't use the full composite scope that was the purpose of this PR.

Why: For defense-in-depth and consistency with the access mode query (lines 930-932 which correctly use all three scope fields), this query should also scope by tenantId and projectId. While the workAppGitHubMcpToolRepositoryAccess table does have these columns, not filtering by them could lead to edge cases in multi-tenant scenarios.

Fix: See inline comment with suggestion block.

Refs:


🟠 2) system Missing changeset for schema migration and API contract change

Issue: This PR introduces a database schema migration that changes the primary key structure of work_app_github_mcp_tool_access_mode and modifies function signatures in @inkeep/agents-core (e.g., getMcpToolAccessMode, getMcpToolRepositoryAccessWithDetails, deleteMcpToolAccessMode now require a scope object instead of just toolId). These are changes to exported functions in published packages.

Why: Per AGENTS.md: "Any bug fix, feature, or behavior change to a published package... needs a changeset." This applies to @inkeep/agents-core, @inkeep/agents-work-apps, and @inkeep/agents-api.

Fix: Run: pnpm bump patch --pkg agents-core --pkg agents-work-apps --pkg agents-api "Fix GitHub MCP tool access to be project-scoped instead of globally scoped by toolId"

Refs:

🟡 Minor (2) 🟡

Inline Comments:

  • 🟡 Minor: auth.ts:84-88 API key validation inconsistent with Slack MCP pattern (timing-safe comparison + null check)
  • 🟡 Minor: 0021_careful_xorn.sql:2 Migration file missing trailing newline

💭 Consider (2) 💭

💭 1) github/mcp/index.ts:83-85 Use createApiError for 'No repository access' error

Issue: The getServer function throws a generic Error instead of using createApiError. This will propagate as an unstructured 500 error rather than a properly formatted API error response.

Why: Consistency with the auth middleware error handling pattern and better client-side error handling.

Fix: Replace with createApiError({ code: 'forbidden', message: 'No repository access configured for this tool' })

Refs:

💭 2) github/mcp/auth.ts Missing unit tests for new header validation

Issue: The new header validation for x-inkeep-tenant-id and x-inkeep-project-id (lines 27-53) lacks unit tests. The Slack MCP auth middleware has comprehensive tests in /packages/agents-work-apps/src/__tests__/slack/mcp/auth.test.ts.

Why: Test coverage for authentication boundary changes ensures regressions are caught.

Fix: Create a test file /packages/agents-work-apps/src/__tests__/github/mcp/auth.test.ts mirroring the Slack MCP auth test structure.

Refs:


🚫 REQUEST CHANGES

Summary: This PR correctly aligns GitHub MCP tool access with the multi-tenant architecture pattern, matching the Slack MCP implementation from PR #2545. The schema change and most data access updates are well-implemented. However, there's an incomplete scope filter in the repository access query for 'selected' mode (line 969) that should be addressed for consistency and defense-in-depth. Additionally, please add a changeset for the published package changes.

Discarded (8)
Location Issue Reason Discarded
index.ts:1632-1656 POST handler lacks error handling around getServer/connect Pre-existing code structure, not introduced by this PR
index.ts:1633-1634 Environment variable check format Pre-existing, not in PR scope
index.ts:72 Local ToolScope type duplicates ToolScopeConfig Matches existing Slack MCP pattern, intentional codebase convention
migration Potential duplicate key risk Medium confidence, toolIds are UUIDs and unlikely to conflict; risk is theoretical
cascade-delete.ts Missing cross-project isolation test Low severity edge case, main functionality is tested
githubMcpToolAccess.test.ts Missing tests for getMcpToolAccessMode scoping Tests exist for getMcpToolRepositoryAccessWithDetails which covers the scoping pattern
auth.ts:27-53 New required headers breaking internal API contract Both known callers are updated in this PR
Architecture positive Correct alignment with multi-tenant architecture Positive observation, no action needed
Reviewers (9)
Reviewer Returned Main Findings Consider While You're Here Inline Comments Pending Recs Discarded
pr-review-security-iam 1 1 0 0 1 0 0
pr-review-devops 4 1 0 0 1 0 2
pr-review-tests 3 0 1 0 0 0 2
pr-review-consistency 6 0 0 0 1 0 4
pr-review-breaking-changes 2 0 0 0 1 0 1
pr-review-errors 3 0 1 0 0 0 2
pr-review-architecture 2 0 0 0 0 0 2
pr-review-standards 0 0 0 0 0 0 0
pr-review-types 0 0 0 0 0 0 0
Total 21 2 2 0 4 0 13

🤖 Generated with Claude Code

Comment thread packages/agents-core/drizzle/runtime/0021_careful_xorn.sql
@github-actions github-actions Bot deleted a comment from claude Bot Mar 6, 2026
@miles-kt-inkeep
Copy link
Copy Markdown
Contributor Author

@claude add a changeset

@inkeep-internal-ci
Copy link
Copy Markdown
Contributor

inkeep-internal-ci Bot commented Mar 6, 2026

Claude finished @miles-kt-inkeep's task in 1m 22s —— View job


Adding Changeset

  • Add changeset for agents-core, agents-work-apps, and agents-api
  • Commit and push

Added changeset project-scoped-github-mcp.md as a patch bump for @inkeep/agents-core, @inkeep/agents-work-apps, and @inkeep/agents-api.
| Branch

@github-actions github-actions Bot deleted a comment from claude Bot Mar 6, 2026
Copy link
Copy Markdown
Contributor

@claude claude Bot left a comment

Choose a reason for hiding this comment

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

PR Review Summary

(0) Total Issues | Risk: Low

This is a delta re-review scoped to changes since the last automated review (commit 497ad36c2).

✅ Prior Issues Addressed

The previous review flagged two issues — both have been resolved:

Prior Issue Status
🟠 Incomplete scope filtering in 'selected' mode repository query (line 969) Fixed — Query now filters by (tenantId, projectId, toolId) at lines 971-976
🟠 Missing changeset Added.changeset/project-scoped-github-mcp.md properly covers agents-core, agents-work-apps, agents-api

Delta Verification

The fix at lines 971-976 correctly implements the full composite scope filter:

.where(
  and(
    eq(workAppGitHubMcpToolRepositoryAccess.tenantId, scope.tenantId),
    eq(workAppGitHubMcpToolRepositoryAccess.projectId, scope.projectId),
    eq(workAppGitHubMcpToolRepositoryAccess.toolId, scope.toolId)
  )
)

This pattern now matches the access mode query (lines 930-935), providing consistent tenant/project isolation throughout the getMcpToolRepositoryAccessWithDetails function.

🕐 Pending Recommendations (1)

  • 🟡 Sibling functions still use toolId-only filteringgetMcpToolRepositoryAccess and clearMcpToolRepositoryAccess still filter by toolId only. As noted in the prior pullfrog review, this is pre-existing and these functions are only used in tests, so it's functionally safe but inconsistent with the project-scoping model. Consider addressing in a follow-up PR for defense-in-depth.

✅ APPROVE

Summary: The delta correctly addresses all feedback from the previous review. The scope filtering fix is properly implemented, matching the existing pattern in the file. The changeset is complete. Ship it! 🚀

Reviewers (2)
Reviewer Returned Main Findings Consider While You're Here Inline Comments Pending Recs Discarded
pr-review-standards 0 0 0 0 0 0 0
pr-review-security-iam 0 0 0 0 0 0 0
Total 0 0 0 0 0 0 0

Note: Delta re-review with minimal scope — both reviewers confirmed the fix is correct with no new issues.


🤖 Generated with Claude Code

@github-actions github-actions Bot deleted a comment from claude Bot Mar 6, 2026
@itoqa
Copy link
Copy Markdown

itoqa Bot commented Mar 6, 2026

Ito Test Report ❌

16 test cases ran. 15 passed, 1 failed.

🔍 The run shows broad coverage of GitHub MCP access controls, scoping, and validation behavior with most checks passing. One reproducible defect remains in rapid-save handling: overlapping save requests can persist an older repository selection instead of the final intended state.

✅ Passed (15)
Test Case Summary Timestamp Screenshot
ROUTE-2 Configure dialog saved selected-repository mode with repo-1 and repo-2, and the same selections persisted after page refresh and dialog reopen. 8:08 ROUTE-2_8-08.png
ROUTE-3 Successfully switched GitHub access mode to all project repositories, saved the change, and verified persistence by reopening Configure with all mode selected. 1:48 ROUTE-3_1-48.png
ROUTE-5 Direct navigation to the MCP details page loaded successfully and remained stable through back/forward navigation without unintended state mutation. 0:00 ROUTE-5_0-00.png
LOGIC-1 GET github-access returned distinct scoped payloads for default/project-a vs default/project-b using the same toolId, confirming project-level isolation with no repository leakage. 22:39 LOGIC-1_22-39.png
LOGIC-2 Cross-tenant GET github-access checks for the shared toolId returned different tenant-scoped data, confirming tenant isolation boundaries were preserved. 22:39 LOGIC-2_22-39.png
LOGIC-4 Scoped MCP POST honored project scope: project-a returned initialized capabilities while project-b returned no-repository-access error, indicating isolation by scope. 38:30 LOGIC-4_38-30.png
EDGE-1 With no tool access-mode row present for default/project-b/tool-gh-edge1, github-access returned mode=selected with an empty repositories list, confirming fail-safe default behavior. 40:47 EDGE-1_40-47.png
EDGE-2 API rejected selected mode update with empty repositoryIds using HTTP 400 validation error. 38:30 EDGE-2_38-30.png
EDGE-3 After setting tool-gh-route to all while project-b had zero project-level repositories, github-access returned an empty repository list and MCP initialize was denied, confirming project scope remains the limiting boundary. 40:47 EDGE-3_40-47.png
EDGE-4 Request without x-inkeep-tenant-id was denied with HTTP 401 and explicit missing-header detail. 38:30 EDGE-4_38-30.png
EDGE-5 Request without x-inkeep-project-id was denied with HTTP 401 and explicit missing-header detail. 38:30 EDGE-5_38-30.png
ADV-1 Verified forged project scope headers did not grant cross-project repository access; control scope request was accepted while forged scope request was denied with no repository access. 30:14 ADV-1_30-14.png
ADV-2 All malformed authorization variants were rejected with unauthorized responses and no successful MCP initialization. 38:30 ADV-2_38-30.png
ADV-4 Unsaved Configure changes were dropped across refresh and back/forward interruption flows, and persisted GitHub Access state remained unchanged without explicit save. 34:29 ADV-4_34-29.png
ADV-5 Mobile configuration flow completed at 390x844 with successful save, and tablet-width verification confirmed the updated two-repository selection persisted. 34:57 ADV-5_34-57.png
❌ Failed (1)
Test Case Summary Timestamp Screenshot
ADV-3 Rapid save submissions produced nondeterministic persistence: final stored selection contained only repo-1 instead of the last intentional repo-1 and repo-2 state. 33:39 ADV-3_33-39.png
Rapid Save spam does not corrupt final access config – Failed
  • Where: MCP details page → GitHub Access → Edit GitHub Access dialog save flow.

  • Steps to reproduce: Open Configure, change selected repositories, trigger multiple Save actions quickly (double click/enter), refresh, then reopen configuration.

  • What failed: Persisted repository selection can reflect an earlier request instead of the last intended selection.

  • Code analysis: The client can issue overlapping saves, and the server applies full-replacement writes without concurrency/version guards; whichever request finishes last wins, producing nondeterministic final state under rapid submissions.

  • Relevant code:

    agents-manage-ui/src/components/mcp-servers/work-app-github-access-edit-dialog.tsx (lines 194–207)

    const handleSave = async () => {
      setIsSaving(true);
      try {
        const repositoryIds = mode === 'selected' ? Array.from(selectedRepoIds) : undefined;
        await setMcpToolWorkAppGitHubAccess(tenantId, projectId, tool.id, mode, repositoryIds);
        toast.success('GitHub access updated successfully');
        onOpenChange(false);
        onSuccess?.();
      } catch (error) {
        console.error('Failed to update GitHub access:', error);

    packages/agents-core/src/data-access/runtime/github-work-app-installations.ts (lines 857–875)

    await db
      .delete(workAppGitHubMcpToolRepositoryAccess)
      .where(eq(workAppGitHubMcpToolRepositoryAccess.toolId, params.toolId));
    
    if (params.repositoryIds.length > 0) {
      await db.insert(workAppGitHubMcpToolRepositoryAccess).values(
        params.repositoryIds.map((repoId) => ({
          id: generateId(),
          toolId: params.toolId,
          tenantId: params.tenantId,
          projectId: params.projectId,

    agents-api/src/domains/manage/routes/mcpToolGithubAccess.ts (lines 256–262)

    await setMcpToolAccessMode(runDbClient)({ toolId, tenantId, projectId, mode: 'selected' });
    await setMcpToolRepositoryAccess(runDbClient)({
      toolId,
      tenantId,
      projectId,
      repositoryIds,
    });
  • Why this is likely a bug: The write path is last-writer-wins under overlapping saves, so rapid user input can persist stale state instead of the user's final action.

  • Introduced by this PR: Unknown – unable to determine.

  • Timestamp: 33:39

📋 View Recording

Screen Recording

Copy link
Copy Markdown
Contributor

@claude claude Bot left a comment

Choose a reason for hiding this comment

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

PR Review Summary

(0) Total Issues | Risk: Low

This is a delta re-review scoped to changes since the last automated review (commit d5633672bc8d).

✅ Delta Analysis

The delta since the last APPROVE consists of 6 merge commits integrating changes from main:

Commit Description
db5fe6584 Merge branch 'main'
d7c10011f chore: add new models [model-sync]
396f858c2 update model sync
afa607507 ci: skip duplicate merge-queue push jobs
959d5b49f updated claude prompt again
0451e1d93 chore: add new models from provider APIs [model-sync]

Conclusion: These changes are unrelated to the PR's feature scope (project-scoped GitHub MCP access). The core PR files have not been modified since the prior APPROVE.

✅ Core PR Verification

The project-scoping implementation remains correctly implemented:

  • Schema migration changes PK to (tenant_id, project_id, tool_id)
  • getMcpToolRepositoryAccessWithDetails filters by full scope (lines 930-935, 971-976) ✓
  • getMcpToolAccessMode filters by full scope (lines 1134-1140) ✓
  • deleteMcpToolAccessMode filters by full scope (lines 1157-1163) ✓
  • cascadeDeleteByTool filters by full scope for access mode (lines 464-470) ✓
  • Auth middleware validates x-inkeep-tenant-id and x-inkeep-project-id headers ✓
  • Changeset properly covers agents-core, agents-work-apps, agents-api ✓

🕐 Pending Recommendations (1)

  • 🟡 Sibling functions still use toolId-only filteringgetMcpToolRepositoryAccess (line 900), clearMcpToolRepositoryAccess (line 994), and the repository access delete in cascadeDeleteByTool (line 458) still filter by toolId only. As noted in prior reviews, these functions are only used in tests and the change is functionally safe since tool IDs are globally unique, but it's inconsistent with the project-scoping model. Consider addressing in a follow-up PR for defense-in-depth.

📋 Ito Test Report Note

The Ito test failure (ADV-3: rapid save race condition) is a pre-existing UI concern unrelated to this PR's schema changes. The issue involves concurrent save requests in the UI dialog that can produce nondeterministic results — this is a client-side concurrency issue, not a scoping bug.


✅ APPROVE

Summary: The delta contains only merge commits from main (model sync, CI updates) with no changes to the PR's feature scope. The prior APPROVE stands — the project-scoping implementation is correct and complete. Ship it! 🚀

Reviewers (0)
Reviewer Returned Main Findings Consider While You're Here Inline Comments Pending Recs Discarded
Total 0 0 0 0 0 1 0

Note: Delta re-review with minimal scope — no subagent reviewers dispatched as the delta contains only merge commits unrelated to the PR's feature scope.


🤖 Generated with Claude Code

@github-actions github-actions Bot deleted a comment from claude Bot Mar 9, 2026
Copy link
Copy Markdown
Contributor

@claude claude Bot left a comment

Choose a reason for hiding this comment

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

PR Review Summary

(0) Total Issues | Risk: Low

This is a delta re-review scoped to changes since the last automated review (commit db5fe65847c0).

✅ Delta Analysis

The delta consists of a single commit (5a613473f) that completes the project-scoping implementation by updating the remaining data access functions:

Function Change
getMcpToolRepositoryAccess Signature: (toolId)(scope: { tenantId, projectId, toolId }); query now filters by all three
clearMcpToolRepositoryAccess Signature: (toolId)(scope: { tenantId, projectId, toolId }); query now filters by all three
setMcpToolRepositoryAccess Delete clause now filters by (tenantId, projectId, toolId)
cascadeDeleteByTool Repository access delete now filters by (tenantId, projectId, toolId)

All test call sites in githubMcpToolAccess.test.ts and githubProjectAccess.test.ts have been updated to pass the full scope object.

✅ Prior Feedback Fully Addressed

The pending recommendation from previous reviews flagged that sibling functions still used toolId-only filtering. This delta fully resolves that feedback:

  • getMcpToolRepositoryAccess — Now filters by (tenantId, projectId, toolId)
  • clearMcpToolRepositoryAccess — Now filters by (tenantId, projectId, toolId)
  • setMcpToolRepositoryAccess — Delete clause uses full scope
  • cascadeDeleteByTool — Repository access delete uses full scope

✅ Verification

  1. Tenant/Project Isolation: All data access functions now properly enforce tenant/project boundaries, matching the schema's composite primary key (tenant_id, project_id, tool_id).

  2. Test Coverage: The existing test "should not affect tools in other projects" in githubProjectAccess.test.ts validates cross-project isolation. The test "should only clear access for the specified tool" validates tool isolation within the same project.

  3. Cascade Delete: The cascadeDeleteByTool function now properly scopes both the access mode and repository access deletes by (tenantId, projectId, toolId).


✅ APPROVE

Summary: The delta correctly addresses all pending recommendations from prior reviews. All MCP tool access functions now filter by the full (tenantId, projectId, toolId) scope, completing the defense-in-depth pattern for GitHub MCP tool access. The implementation is consistent, test coverage is comprehensive, and the project-scoping effort is now complete. Ship it! 🚀

Reviewers (3)
Reviewer Returned Main Findings Consider While You're Here Inline Comments Pending Recs Discarded
pr-review-standards 0 0 0 0 0 0 0
pr-review-security-iam 0 0 0 0 0 0 0
pr-review-tests 0 0 0 0 0 0 0
Total 0 0 0 0 0 0 0

Note: Delta re-review with minimal scope — all reviewers confirmed the delta correctly addresses prior feedback with no new issues.


🤖 Generated with Claude Code

@github-actions github-actions Bot deleted a comment from claude Bot Mar 9, 2026
Copy link
Copy Markdown
Contributor

@claude claude Bot left a comment

Choose a reason for hiding this comment

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

PR Review Summary

(0) Total Issues | Risk: Low

This is a delta re-review scoped to changes since the last automated review (commit 7611381f1da3).

✅ Delta Analysis

The delta since the last APPROVE consists of a merge commit integrating changes from main:

Commit Description
7050d4ea9 Merge branch 'main' into efix/project-scoped-github-work-app
d7827b34f add <FlowButton />, fix svg icon color for Ship and Agent Settings toolbar buttons (#2582)

All changes in the delta are UI-related (FlowButton component, toolbar refactoring in agents-manage-ui). These are unrelated to the PR's feature scope (project-scoped GitHub MCP access).

✅ Core PR Verification

The project-scoping implementation remains correctly implemented:

Function Status
Schema PK changed to (tenant_id, project_id, tool_id)
getMcpToolRepositoryAccess ✅ Filters by full scope
getMcpToolRepositoryAccessWithDetails ✅ Filters by full scope
clearMcpToolRepositoryAccess ✅ Filters by full scope
getMcpToolAccessMode ✅ Filters by full scope
deleteMcpToolAccessMode ✅ Filters by full scope
cascadeDeleteByTool ✅ Filters by full scope for both tables
Auth middleware (x-inkeep-tenant-id, x-inkeep-project-id)
Changeset ✅ Covers agents-core, agents-work-apps, agents-api

📋 Ito Test Report Note

The Ito test failure (ADV-3: rapid save race condition) is a pre-existing UI concern unrelated to this PR's schema changes. The issue involves concurrent save requests in the UI dialog that can produce nondeterministic results — this is a client-side concurrency issue in the WorkAppGitHubAccessEditDialog, not a scoping bug introduced by this PR.


✅ APPROVE

Summary: The delta contains only merge commits from main (UI component refactoring) with no changes to the PR's feature scope. The prior APPROVE stands — the project-scoping implementation is correct and complete. All data access functions properly filter by the full (tenantId, projectId, toolId) composite scope. Ship it! 🚀

Reviewers (0)
Reviewer Returned Main Findings Consider While You're Here Inline Comments Pending Recs Discarded
Total 0 0 0 0 0 0 0

Note: Delta re-review with minimal scope — no subagent reviewers dispatched as the delta contains only merge commits unrelated to the PR's feature scope.


🤖 Generated with Claude Code

@github-actions github-actions Bot deleted a comment from claude Bot Mar 9, 2026
@miles-kt-inkeep miles-kt-inkeep added this pull request to the merge queue Mar 9, 2026
Merged via the queue into main with commit f475d74 Mar 9, 2026
11 checks passed
@miles-kt-inkeep miles-kt-inkeep deleted the efix/project-scoped-github-work-app branch March 9, 2026 14:02
@itoqa
Copy link
Copy Markdown

itoqa Bot commented Mar 9, 2026

Ito Test Report ❌

18 test cases ran. 17 passed, 1 failed.

✅ Most GitHub MCP access-scoping, authorization-header validation, isolation, and resilience checks behaved as expected. 🔍 One create-flow regression remains: a GitHub MCP server created in selected-repository mode can land on the details page with empty repository scope, indicating selected access is not reliably persisted/read after creation.

✅ Passed (17)
Test Case Summary Timestamp Screenshot
ROUTE-2 Configured Selected repositories with frontend and backend, saved successfully, and confirmed selections were preloaded on reopen. 15:47 ROUTE-2_15-47.png
ROUTE-4 Changed GitHub access from selected repositories to all project repositories and verified all-mode state persisted after refresh. 15:47 ROUTE-4_15-47.png
EDGE-1 Direct github-access PUT with mode=selected and empty repositoryIds returned 400 bad_request and follow-up GET confirmed configuration remained unchanged. 29:54 EDGE-1_29-54.png
EDGE-2 GitHub create dialog rendered the no-installations empty state with Connect GitHub CTA, CTA navigated to /default/work-apps/github, and returning preserved the create-flow state without creating a tool. 58:35 EDGE-2_58-35.png
EDGE-3 GitHub tool creation was gated by project-level prerequisite state and CTA routed to project settings as expected. 27:46 EDGE-3_27-46.png
EDGE-4 Create submission followed by immediate refresh resulted in one persisted tool entry without duplicate creation. 20:13 EDGE-4_20-13.png
EDGE-5 After navigation away and return, GitHub create dialog reopened with current repository options and no stale-state mismatch observed. 20:15 EDGE-5_20-15.png
EDGE-6 On iPhone 13 viewport, controls remained actionable through selected-mode setup and create submission, and the new server details page loaded. 20:17 EDGE-6_20-17.png
ADV-1 Deep-link to tenant-b project/tool resolved to 404 Project not found and did not expose foreign project metadata. 33:56 ADV-1_33-56.png
ADV-2 MCP request without x-inkeep-tenant-id was rejected with 401 unauthorized and explicit missing-header detail. 29:54 ADV-2_29-54.png
ADV-3 MCP request without x-inkeep-project-id was rejected with 401 unauthorized and explicit missing-header detail. 29:54 ADV-3_29-54.png
ADV-4 Both malformed Authorization format and invalid bearer token requests were rejected as unauthorized, with clear error messages and no internal stack output. 29:54 ADV-4_29-54.png
ADV-5 Invalid, duplicate, and injection-like repositoryIds were rejected with 400 bad_request, and subsequent read confirmed no configuration mutation. 29:54 ADV-5_29-54.png
ADV-6 Executed rapid conflicting interactions and double-save; persisted result remained deterministic and matched the final intended selected repository state. 15:47 ADV-6_15-47.png
LOGIC-1 Project B github-access state remained unchanged after Project A mode updates/attempted updates, indicating no cross-project overwrite. 34:35 LOGIC-1_34-35.png
LOGIC-2 Re-executed deterministic selected(A,B)->all->selected(C) sequence; each GET matched the most recent PUT with exact repository scope using authenticated internal service token flow. 50:13 LOGIC-2_50-13.png
RES-1 Synced repositories in the access dialog, applied selection changes, saved successfully, and confirmed persisted access matched intended post-sync selection. 15:47 RES-1_15-47.png
❌ Failed (1)
Test Case Summary Timestamp Screenshot
ROUTE-3 Create flow redirected successfully, but selected repository scope was not persisted on the resulting details page. 20:11 ROUTE-3_20-11.png
Create GitHub MCP server from Work Apps catalog – Failed
  • Where: GitHub MCP server create flow (/projects/:projectId/mcp-servers/new) and redirected MCP server details page GitHub Access section.

  • Steps to reproduce: Open Work Apps > GitHub, choose Selected repositories, pick repositories, submit create, then verify GitHub Access on redirected details page.

  • What failed: The created server lands with empty selected scope instead of showing the chosen repositories.

  • Code analysis: I checked the create dialog submit path and the backend read path for tool GitHub access. The UI creates the tool and then performs a second call to set access. The GET path treats missing mode rows as selected by default and returns repository rows for scoped keys. This combination can surface an empty selected scope when scoped access state is not found/persisted as expected.

  • Relevant code:

    agents-manage-ui/src/components/mcp-servers/selection/work-app-github-repository-config-dialog.tsx (lines 368-400)

    const handleSubmit = async () => {
      if (!isFormValid) return;
    
      setIsSubmitting(true);
      try {
        const githubMcpUrl = `${PUBLIC_INKEEP_AGENTS_API_URL}/work-apps/github/mcp`;
        const toolId = generateId();
        const newTool = await createMCPTool(tenantId, projectId, {
          id: toolId,
          name: name.trim(),
          config: {
            type: 'mcp' as const,
            mcp: { server: { url: githubMcpUrl }, transport: { type: 'streamable_http' } },
          },

    agents-api/src/domains/manage/routes/mcpToolGithubAccess.ts (lines 130-152)

    const mode = await getMcpToolAccessMode(runDbClient)({ tenantId, projectId, toolId });
    
    if (mode === 'all') {
      return c.json({ mode: 'all' as const, repositories: [] }, 200);
    }
    
    const repositoriesWithDetails = await getMcpToolRepositoryAccessWithDetails(runDbClient)({
      tenantId,
      projectId,
      toolId,
    });

    packages/agents-core/src/data-access/runtime/github-work-app-installations.ts (lines 1148-1169)

    export const getMcpToolAccessMode =
      (db: AgentsRunDatabaseClient) =>
      async (scope: { tenantId: string; projectId: string; toolId: string }): Promise<WorkAppGitHubAccessMode> => {
        const result = await db
          .select({ mode: workAppGitHubMcpToolAccessMode.mode })
          .from(workAppGitHubMcpToolAccessMode)
          .where(
            and(
              eq(workAppGitHubMcpToolAccessMode.tenantId, scope.tenantId),
              eq(workAppGitHubMcpToolAccessMode.projectId, scope.projectId),
              eq(workAppGitHubMcpToolAccessMode.toolId, scope.toolId)
            )
          )
          .limit(1);
    
        return (result[0]?.mode as WorkAppGitHubAccessMode) ?? 'selected';
  • Why this is likely a bug: The code path can resolve to selected with no repository rows for the created tool scope, which directly matches the observed empty repository scope after a selected-mode create.

  • Introduced by this PR: Yes – this PR modified the relevant scoped GitHub MCP access read/write code.

  • Timestamp: 20:11

📋 View Recording

Screen Recording

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.

1 participant