Skip to content

fix(ai): make GitHub adapter suppression authorization-aware#1922

Open
2witstudios wants to merge 3 commits into
masterfrom
pu/github-adapter-suppression-fix
Open

fix(ai): make GitHub adapter suppression authorization-aware#1922
2witstudios wants to merge 3 commits into
masterfrom
pu/github-adapter-suppression-fix

Conversation

@2witstudios

@2witstudios 2witstudios commented Jul 6, 2026

Copy link
Copy Markdown
Owner

Summary

Non-admin users had no working GitHub surface in production at all — a side effect of two independently-correct pieces of code colliding:

  • buildPageSpaceTools (apps/web/src/lib/ai/core/ai-tools.ts) registers the sandbox git/gh CLI toolkit for every user whenever the global CODE_EXECUTION_ENABLED env kill-switch is on, gated only by the env flag, with no per-user check.
  • suppressGithubIntegrationTools (apps/web/src/lib/ai/core/tool-filtering.ts, added in Resolve GitHub integration/sandbox tool overlap #1879 to de-dupe the two GitHub surfaces) hid the always-working GitHub OAuth integration tools whenever the sandbox git/gh tool names were merely present in the resolved tool set — it never checked whether those tools would actually be authorized for this caller.

The real per-user authorization for sandbox code execution lives entirely in canRunCode (packages/lib/src/services/sandbox/can-run-code.ts), which in production requires app-admin (app_admin_required, from #1664's deliberate Fly Sprites rollout guard). That gate is untouched by this PR.

Net effect for a non-admin in production: sandbox tools are "present" (triggering suppression) but denied at execute-time by canRunCode — leaving no GitHub surface at all.

Fix

  • suppressGithubIntegrationTools is now async and calls canRunCode({ userId, driveId, requestOrigin, agentPageId }). It only suppresses the GitHub OAuth tools when the sandbox toolkit is both present and actually authorized for this caller right now. This is a listing/UX decision, not a new security boundary — it never grants sandbox access, only decides which already-permitted-or-not surface to show.
  • resolvePageAgentIntegrationTools / resolveGlobalAssistantIntegrationTools gained optional requestOrigin/agentPageId params, threaded into the now-awaited suppression call.
  • The ask_agent tool's nested consult call (the one agent-to-agent path) now passes requestOrigin: 'agent' and agentPageId derived from chatSource.agentPageId ?? locationContext.currentPage.idnot the consulted target agent's own ID. This matters: resolveSandboxActorContext (the code path that authorizes real sandbox tool execution) always resolves agentPageId from the calling agent's chatSource/parentAgentId, never the target's, and nested ask_agent calls never override chatSource at any depth. An earlier version of this fix passed the target agent's ID here, which would have let the suppression decision diverge from the real execute-time authorization outcome — reintroducing this exact bug via the one path this PR touches. Caught and fixed via self-review before merge; see the second commit for the full trace.
  • The other four call sites (chat route, consult route, global assistant route, workflow executor) are unchanged — 'user' is already the correct default for them (verified page-agents/consult/route.ts is a direct user/MCP-token entry point, structurally equivalent to the chat route, not an agent-to-agent path — it does not need requestOrigin: 'agent').

Test plan

  • bun run --filter web typecheck passes
  • apps/web test suite passes against a real Postgres test DB (790/791 files, 11641/11642 tests) — the one pre-existing failure (grouping.test.ts midnight-boundary test) is untouched by this diff and unrelated (message-grouping display logic)
  • New tests added: tool-filtering.test.ts and integration-tool-resolver.test.ts both cover the "sandbox tools present but not authorized → GitHub NOT suppressed" regression case for both resolvers, plus explicit assertions on the exact canRunCode call args (userId/driveId/requestOrigin/agentPageId)
  • agent-communication-tools.test.ts covers the agentPageId identity fix directly: asserts resolvePageAgentIntegrationTools is called with the calling agent's chatSource.agentPageId (or locationContext.currentPage.id fallback), never the consulted target's ID
  • packages/lib/src/services/sandbox/can-run-code.ts is completely untouched (git diff confirms zero changes)
  • git diff --stat scoped to: tool-filtering.ts, integration-tool-resolver.ts, agent-communication-tools.ts, and their test files — no unrelated refactors

Known trade-offs (accepted, not blocking)

  • Fail-open on transient errors: if canRunCode's DB calls error transiently, it returns {ok: false, reason: 'error'}, and suppression treats that the same as "not authorized" — i.e. it fails open by not suppressing GitHub tools for that one turn. This is the safe direction (matches this PR's whole goal: never leave a user with zero GitHub surface) but means tool-set membership can very rarely vary turn-to-turn under DB flakiness, not just on real config changes.
  • Added DB round trips: any request whose tool set includes the sandbox git/gh toolkit now pays canRunCode's DB queries (permissions/role lookups) during tool-list resolution, in addition to the pre-existing execute-time check. This was a deliberate, explicit instruction for this fix (reuse canRunCode directly, avoid the heavier resolveSandboxActorContext) — an optimization to avoid double-querying would touch can-run-code.ts or call-site plumbing beyond this PR's scope.

https://claude.ai/code/session_01J3NZDRTyLssfCupNLHRgEr

Summary by CodeRabbit

  • Bug Fixes
    • Improved tool availability checks so GitHub-related tools are only hidden when sandbox code access is actually allowed.
    • Kept GitHub integration tools visible when authorization is denied, avoiding unexpected tool loss.
    • Fixed request context handling so agent/page identity is passed correctly during tool resolution, including nested tool calls.
    • Standardized how page and assistant requests resolve tool access across different chat sources and page contexts.

Non-admin users in production had no working GitHub surface at all.
buildPageSpaceTools registers the sandbox git/gh CLI toolkit for every
user whenever the CODE_EXECUTION_ENABLED kill-switch is on, with no
per-user admin check (that check lives entirely in canRunCode). But
suppressGithubIntegrationTools suppressed the always-working GitHub
OAuth integration tools whenever those sandbox tool *names* were merely
present in the resolved tool set, without checking whether the caller
could actually invoke them.

For a non-admin, the sandbox tools were "present" (triggering
suppression) but denied at execute-time by canRunCode's
app_admin_required check — leaving no GitHub surface at all.

suppressGithubIntegrationTools now calls canRunCode and only suppresses
the OAuth tools when the sandbox toolkit is both present and actually
authorized for this caller. Threaded requestOrigin/agentPageId through
the two resolvers and the one agent-to-agent (ask_agent) call site that
needs 'agent' origin; the other four call sites already default
correctly to 'user' origin. The production admin-only gate in
can-run-code.ts is untouched.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

GitHub integration tool suppression in the AI tool-filtering module is changed from synchronous presence-based logic to an async, authorization-gated check using canRunCode. Resolver functions gain optional requestOrigin/agentPageId parameters forwarded to suppression, and ask_agent's caller identity derivation is updated accordingly, with corresponding test coverage added.

Changes

Authorization-gated GitHub tool suppression

Layer / File(s) Summary
Async authorization check in suppressGithubIntegrationTools
apps/web/src/lib/ai/core/tool-filtering.ts, apps/web/src/lib/ai/core/__tests__/tool-filtering.test.ts
suppressGithubIntegrationTools becomes async, importing canRunCode/CanRunCodeInput, and only suppresses GitHub tools when sandbox git/gh tools exist and canRunCode(auth) returns ok: true; tests mock canRunCode and verify suppression, denial, and argument forwarding (userId, driveId, requestOrigin, agentPageId).
Resolver forwarding of requestOrigin/agentPageId
apps/web/src/lib/ai/core/integration-tool-resolver.ts, apps/web/src/lib/ai/core/__tests__/integration-tool-resolver.test.ts
resolvePageAgentIntegrationTools and resolveGlobalAssistantIntegrationTools accept optional requestOrigin/agentPageId and pass userId, driveId (normalized to undefined when null), requestOrigin, agentPageId into suppression; tests cover denial behavior and argument forwarding.
Caller identity wiring in ask_agent
apps/web/src/lib/ai/tools/agent-communication-tools.ts, apps/web/src/lib/ai/tools/__tests__/agent-communication-tools.test.ts
agentPageId is derived from chatSource.agentPageId with fallback to callingPage.id, and nestedContext.parentAgentId now uses callingPage.id instead of locationContext.currentPage.id; tests verify both derivation paths.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ask_agent
  participant resolvePageAgentIntegrationTools
  participant suppressGithubIntegrationTools
  participant canRunCode

  ask_agent->>ask_agent: derive agentPageId (chatSource.agentPageId ?? callingPage.id)
  ask_agent->>resolvePageAgentIntegrationTools: currentTools, requestOrigin, agentPageId
  resolvePageAgentIntegrationTools->>suppressGithubIntegrationTools: integrationTools, currentTools, { userId, driveId, requestOrigin, agentPageId }
  suppressGithubIntegrationTools->>canRunCode: canRunCode(auth)
  canRunCode-->>suppressGithubIntegrationTools: { ok, reason? }
  alt authorized
    suppressGithubIntegrationTools-->>resolvePageAgentIntegrationTools: filtered tools (GitHub suppressed)
  else denied
    suppressGithubIntegrationTools-->>resolvePageAgentIntegrationTools: unfiltered tools (GitHub kept)
  end
  resolvePageAgentIntegrationTools-->>ask_agent: final tool set
Loading

Possibly related PRs

  • 2witstudios/PageSpace#1664: Adds app_admin_required authorization logic to canRunCode, which this PR now depends on for gating GitHub tool suppression.
  • 2witstudios/PageSpace#1879: Prior update to suppressGithubIntegrationTools/integration-tool-resolver for the same GitHub tool suppression flow that this PR extends with authorization and identity forwarding.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: GitHub adapter suppression now depends on authorization checks.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch pu/github-adapter-suppression-fix

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed. For unrecoverable errors, disable the tool in CodeRabbit configuration.


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.

Self-review caught a real bug in the original fix: ask_agent's new
canRunCode-backed suppression check passed `agentPageId: agentId` (the
consulted TARGET agent), but resolveSandboxActorContext — the code path
that actually authorizes real sandbox tool execution — always resolves
agentPageId from `chatSource.agentPageId` (falling back to
`parentAgentId`), both of which are the CALLING agent's identity, never
the target's. Nested ask_agent calls never override chatSource, so this
is true at any nesting depth.

Concretely: if the calling agent lacks drive edit access but the
consulted target agent has it, the new suppression check (using the
target's identity) would authorize and hide GitHub OAuth tools, while
the real invocation-time check (using the caller's identity, per
existing codebase convention — see actor-permissions.ts's
getAgentPageId/resolveActingAgentId, which read the same
chatSource.agentPageId field for the same reason) would deny the
sandbox tool. Net effect: the target agent loses both GitHub surfaces —
exactly the bug this PR exists to fix, reintroduced via the one path
this PR touches.

Fix: pass `executionContext?.chatSource?.agentPageId ??
executionContext?.locationContext?.currentPage?.id`, mirroring
resolveSandboxActorContext's own formula exactly, so the suppression
decision always matches the real authorization outcome.

Also: clarify tool-filtering.ts's module header and JSDoc (the file's
"simple, no DB" self-description no longer fully holds for this one
function — flag it so a future simplification pass doesn't silently
revert the fix), and strengthen resolver tests to assert the exact
canRunCode call args (userId/driveId/requestOrigin/agentPageId), not
just suppress/keep outcomes.
@2witstudios

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Proactive /simplify pass on the previous two commits, informed by a
4-angle parallel review (reuse, simplification, efficiency, altitude):

- tool-filtering.ts: suppressGithubIntegrationTools's `auth` param was a
  hand-copied subset of CanRunCodeInput (minus `deps`), then the body
  destructured and rebuilt it field-by-field just to call canRunCode.
  Now typed as `Omit<CanRunCodeInput, 'deps'>` and forwarded directly
  (`canRunCode(auth)`) — one less place to keep in sync if
  CanRunCodeInput's shape ever changes.

- agent-communication-tools.ts: the previous commit's agentPageId fix
  independently re-derived `executionContext?.locationContext?.currentPage?.id`
  inline, duplicating both the pre-existing `callingPage` local (declared
  earlier in the same function) and nestedContext's own parentAgentId
  assignment a few lines below. Both call sites now reuse `callingPage?.id`,
  removing the second within-function copy of the same expression and
  tightening the invariant the fix's comment describes from "two
  independently-written expressions that happen to agree" to "one shared
  value used twice."

Considered and skipped (would expand scope beyond a proactive cleanup
pass): extracting a new cross-file helper for the agentPageId formula
(the altitude review confirmed this would be misleading — it isn't
really the same formula as resolveSandboxActorContext's, just a
coincidentally-equivalent derivation tied to this file's own
nestedContext construction); making the grants fetch and canRunCode
check run concurrently in the two resolvers (would fire the DB-backed
auth check unconditionally even when grants turn out to be empty,
trading a latency win in the sandbox-active case for wasted queries in
the common no-integrations case — not a clear improvement); and
extracting a shared canRunCode-mock test fixture (two lines duplicated
across two test files, not worth a new fixture file).

No behavior change — typecheck clean, all 93 tests across the four
touched test files still pass, full apps/web suite green (11645/11646,
one pre-existing unrelated failure).

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

Caution

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

⚠️ Outside diff range comments (1)
apps/web/src/lib/ai/core/integration-tool-resolver.ts (1)

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

Avoid the duplicate canRunCode lookup in apps/web/src/lib/ai/core/integration-tool-resolver.ts. Both resolver paths hide GitHub tools with suppressGithubIntegrationTools(...), and the sandbox tool gate runs the same auth again at execution time, so sandbox-tool requests pay an extra DB round-trip. Thread the result through if the later gate uses the same inputs.

🤖 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 `@apps/web/src/lib/ai/core/integration-tool-resolver.ts` around lines 89 - 126,
The resolver in resolvePageAgentIntegrationTools is doing an extra
canRunCode-driven authorization check for sandbox GitHub tools even though
suppressGithubIntegrationTools already applies the same gate, causing a
redundant DB round-trip. Update the integration-tool path so the canRunCode
result is computed once and threaded through to the later sandbox/tool-exposure
check, reusing the existing inputs (userId, driveId, requestOrigin, agentPageId)
instead of re-looking it up.
🤖 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.

Outside diff comments:
In `@apps/web/src/lib/ai/core/integration-tool-resolver.ts`:
- Around line 89-126: The resolver in resolvePageAgentIntegrationTools is doing
an extra canRunCode-driven authorization check for sandbox GitHub tools even
though suppressGithubIntegrationTools already applies the same gate, causing a
redundant DB round-trip. Update the integration-tool path so the canRunCode
result is computed once and threaded through to the later sandbox/tool-exposure
check, reusing the existing inputs (userId, driveId, requestOrigin, agentPageId)
instead of re-looking it up.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8de63400-8093-4ceb-a7bf-db00b98af59c

📥 Commits

Reviewing files that changed from the base of the PR and between 76bb5e9 and ae76628.

📒 Files selected for processing (6)
  • apps/web/src/lib/ai/core/__tests__/integration-tool-resolver.test.ts
  • apps/web/src/lib/ai/core/__tests__/tool-filtering.test.ts
  • apps/web/src/lib/ai/core/integration-tool-resolver.ts
  • apps/web/src/lib/ai/core/tool-filtering.ts
  • apps/web/src/lib/ai/tools/__tests__/agent-communication-tools.test.ts
  • apps/web/src/lib/ai/tools/agent-communication-tools.ts

@2witstudios

Copy link
Copy Markdown
Owner Author

Addressing CodeRabbit's finding on integration-tool-resolver.ts:89-126 (duplicate canRunCode lookup / extra DB round-trip):

Confirmed and correct as an observation — this was flagged and consciously accepted before this review ran (see the "Known trade-offs" section of the PR description, added prior to this comment). Not threading the result through is intentional, for two reasons:

  1. Scope: the task this PR implements explicitly specified reusing canRunCode directly at tool-listing time rather than restructuring the real execute-time gate (resolveSandboxActorContext / gateSandboxToolCall in sandbox-tools-runtime.ts / tool-gate.ts) to accept and reuse a precomputed result. Doing so would mean threading a CanRunCodeResult from the tool-listing call sites, through the merged tool set, through however many turns/tool-calls occur before the model actually invokes a sandbox tool, into the separate execute-time authorization path — a materially larger, riskier change spanning files this PR doesn't otherwise touch, for a bug-fix PR whose diff is intentionally scoped to tool-filtering.ts / integration-tool-resolver.ts / agent-communication-tools.ts.

  2. Correctness risk if we did it anyway: the two canRunCode calls answer different questions at different times. The one this PR adds decides "should the GitHub OAuth tools be suppressed in the tool list built at request start." The existing one at gateSandboxToolCall/execute-time decides "is this specific tool call, happening possibly several turns later in the same conversation, authorized right now." Caching/reusing the listing-time result at execute-time would introduce a TOCTOU gap — e.g. an admin's role is revoked, or a drive membership changes, between when the tool list was built and when the model actually calls git_clone several turns later — and the execute-time gate exists specifically to catch that. Threading the result through would either reintroduce that gap (if the cached result is trusted) or add no real savings (if execute-time still re-verifies independently, making the thread-through pure plumbing with no round-trip actually saved).

No code change made. Flagging as a legitimate, separately-scoped follow-up if the added DB load ever proves material in practice (it's bounded to requests whose tool set includes the sandbox git/gh toolkit, gated behind hasSandboxGitTools before canRunCode is ever called).

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