Skip to content

feat: wire AI sandbox integrations end-to-end#737

Open
2witstudios wants to merge 6 commits intomasterfrom
ppg/sandbox-wiring
Open

feat: wire AI sandbox integrations end-to-end#737
2witstudios wants to merge 6 commits intomasterfrom
ppg/sandbox-wiring

Conversation

@2witstudios
Copy link
Owner

@2witstudios 2witstudios commented Feb 28, 2026

Summary

  • Add OAUTH_STATE_SECRET, INTEGRATION_GITHUB_CLIENT_ID, and INTEGRATION_GITHUB_CLIENT_SECRET to .env.example files so developers know what to configure
  • Add seedBuiltinProviders() with lazy init in GET /api/integrations/providers — builtin providers auto-seed on first access, removing the admin-only install step
  • Verify chat route integration tool wiring at route.ts:535-552 (already implemented — added tests)
  • Verify settings/integrations UI works end-to-end with auto-seeded providers

Test plan

  • Builtin providers auto-seed on first GET (unit tests pass)
  • Integration tools resolve and merge into chat (resolver tests pass)
  • All 336 integration tests pass
  • TypeScript compiles clean (no new errors)

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Added GitHub OAuth integration configuration support.
    • System now automatically initializes builtin integration providers on first access.
  • Chores

    • Introduced new environment variables for OAuth state management and GitHub integration setup.
    • Added comprehensive tests for integration tool resolution.

2witstudios and others added 3 commits February 27, 2026 22:48
Add OAUTH_STATE_SECRET, INTEGRATION_GITHUB_CLIENT_ID, and
INTEGRATION_GITHUB_CLIENT_SECRET documentation to both root and
apps/web .env.example files so developers know what to configure
for the AI sandbox integration OAuth flows.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add seedBuiltinProviders() to provider-repository that idempotently
inserts any missing builtin providers into the database. Wire it into
GET /api/integrations/providers as lazy init — when zero providers
exist, builtins are seeded automatically. This removes the manual
admin install step that blocked end-to-end flow.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Confirms resolvePageAgentIntegrationTools and
resolveGlobalAssistantIntegrationTools correctly wire resolution,
conversion, and execution dependencies — the critical path for
integration tools appearing in chat.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@chatgpt-codex-connector
Copy link

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
Copy link
Contributor

coderabbitai bot commented Feb 28, 2026

Warning

Rate limit exceeded

@2witstudios has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 12 minutes and 46 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between b241876 and 1c263fd.

📒 Files selected for processing (3)
  • apps/web/src/lib/ai/core/__tests__/integration-tool-resolver.test.ts
  • packages/lib/src/integrations/repositories/provider-repository.test.ts
  • packages/lib/src/integrations/repositories/provider-repository.ts
📝 Walkthrough

Walkthrough

This PR adds OAUTH_STATE_SECRET environment variable configuration, integrates GitHub OAuth App credentials, implements lazy seeding of builtin integration providers in the API route, exports the seeding function, and adds comprehensive test coverage for integration tool resolution logic.

Changes

Cohort / File(s) Summary
Environment Configuration
.env.example, apps/web/.env.example
Added OAUTH_STATE_SECRET for OAuth state signing and GitHub OAuth App integration placeholders (client ID, client secret) with callback URL guidance.
Builtin Provider Seeding
packages/lib/src/integrations/repositories/provider-repository.ts, packages/lib/src/integrations/repositories/provider-repository.test.ts, packages/lib/src/integrations/index.ts
Introduced seedBuiltinProviders function with idempotent logic to seed missing builtin providers, added BuiltinConfig interface, comprehensive test coverage for seeding scenarios, and exported the function from the integrations barrel.
Provider Listing Route
apps/web/src/app/api/integrations/providers/route.ts
Augmented GET handler with lazy initialization: checks if provider list is empty and seeds builtins via seedBuiltinProviders if available, with logging for success and graceful error handling.
Integration Tool Resolution Tests
apps/web/src/lib/ai/core/__tests__/integration-tool-resolver.test.ts
New test suite validating integration tool resolution for page agents and global assistants, covering empty grant scenarios and active grant conversion to AI SDK tool descriptors.

Sequence Diagram

sequenceDiagram
    participant Client
    participant APIRoute as API Route<br/>(providers)
    participant ProviderRepo as Provider<br/>Repository
    participant Database

    Client->>APIRoute: GET /api/integrations/providers
    APIRoute->>ProviderRepo: listEnabledProviders()
    ProviderRepo->>Database: Query enabled providers
    Database-->>ProviderRepo: Empty or partial list
    ProviderRepo-->>APIRoute: providers = []

    alt Providers Empty && Builtins Available
        APIRoute->>ProviderRepo: seedBuiltinProviders(builtins)
        ProviderRepo->>Database: Filter existing providers
        ProviderRepo->>Database: Insert missing builtins
        Database-->>ProviderRepo: Seeded providers
        ProviderRepo-->>APIRoute: Success + seeded count
        APIRoute->>ProviderRepo: listEnabledProviders()
        ProviderRepo->>Database: Query updated list
        Database-->>ProviderRepo: Full provider list
        ProviderRepo-->>APIRoute: Complete providers
    else Seeding Fails
        APIRoute->>APIRoute: Log warning, continue
    end

    APIRoute-->>Client: Return providers (stripped)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰 Whiskers twitching with delight,
Planting OAuth secrets, shiny and tight,
GitHub providers sprouting with care,
Lazy seeds growing everywhere!
Integration tools bloom— 🌱hooray!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: wire AI sandbox integrations end-to-end' accurately describes the main objective of the PR, which wires AI sandbox integrations across environment configuration, provider seeding, and chat tool resolution.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch ppg/sandbox-wiring

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 and usage tips.

The logger's warn() method expects LogInput (an object), not Error.
Convert seedError to a message string in an object to fix the CI
build type error.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (4)
packages/lib/src/integrations/repositories/provider-repository.ts (2)

156-173: Consider wrapping sequential inserts in a transaction.

The loop inserts providers sequentially without a transaction. If an insert fails mid-loop, some providers will be seeded while others won't, leaving the database in a partially seeded state. A transaction would ensure all-or-nothing semantics.

♻️ Suggested transaction wrapper
+  return database.transaction(async (tx) => {
     const seeded: IntegrationProvider[] = [];
     for (const builtin of toSeed) {
-      const [provider] = await database
+      const [provider] = await tx
         .insert(integrationProviders)
         .values({
           slug: builtin.id,
           name: builtin.name,
           description: builtin.description ?? null,
           iconUrl: builtin.iconUrl ?? null,
           documentationUrl: builtin.documentationUrl ?? null,
           providerType: 'builtin',
           config: builtin as unknown as Record<string, unknown>,
           isSystem: true,
           enabled: true,
         })
         .returning();
       seeded.push(provider);
     }
-
     return seeded;
+  });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/lib/src/integrations/repositories/provider-repository.ts` around
lines 156 - 173, The seeding loop currently inserts each provider sequentially
(using database.insert(integrationProviders).values(...).returning()) and pushes
results into the seeded array, which can leave a partial state if one insert
fails; wrap the entire loop in a single database transaction so all inserts are
committed together or rolled back on error. Locate the block that iterates over
toSeed and performs database.insert(...) against integrationProviders, start a
transaction via your database client (e.g., database.transaction or equivalent),
perform all inserts inside that transactional callback, and on success
return/assign the seeded results; ensure any thrown error causes the transaction
to rollback and that you propagate or log the error accordingly.

167-167: Type cast loses type safety.

The cast builtin as unknown as Record<string, unknown> bypasses TypeScript's type checking. Since IntegrationProviderConfig should already be compatible with the JSON column type, consider using a more specific cast or ensuring the schema types align properly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/lib/src/integrations/repositories/provider-repository.ts` at line
167, The current cast "builtin as unknown as Record<string, unknown>" on the
config field loses type safety; instead ensure the value assigned to config is
typed to the expected schema type (e.g., IntegrationProviderConfig) or narrow it
with a specific cast/transform rather than double-casting to unknown. Update the
assignment in provider-repository.ts so that the "builtin" value is either
declared/validated as IntegrationProviderConfig (or the precise JSON column type
used by your ORM) before assignment, or explicitly convert it to a safe
Record<string, unknown> via a typed mapping/serialization step; reference the
"config" property assignment and the "builtin" symbol when making the change.
apps/web/src/lib/ai/core/__tests__/integration-tool-resolver.test.ts (1)

109-125: Consider adding test coverage for resolveGlobalAssistantIntegrationTools with active grants.

The test suite for resolvePageAgentIntegrationTools includes both "no grants" and "active grants" scenarios, but resolveGlobalAssistantIntegrationTools only tests the "no grants" case. For symmetry and completeness, consider adding a test that verifies behavior when grants are present.

💡 Example test case to add
it('given active grants, should convert to AI SDK tools', async () => {
  const mockGrants = [{
    id: 'grant-1',
    agentId: null,
    connectionId: 'conn-1',
    allowedTools: null,
    deniedTools: null,
    readOnly: false,
    rateLimitOverride: null,
    connection: {
      id: 'conn-1',
      name: 'GitHub',
      status: 'active',
      providerId: 'prov-1',
      provider: {
        id: 'prov-1',
        slug: 'github',
        name: 'GitHub',
        config: { id: 'github', name: 'GitHub', tools: [], baseUrl: 'https://api.github.com', authMethod: { type: 'oauth2', config: {} } },
      },
    },
  }];

  mockResolveGlobalIntegrations.mockResolvedValue(mockGrants);
  const mockExecutor = vi.fn();
  mockCreateExecutor.mockReturnValue(mockExecutor);
  mockConvert.mockReturnValue({
    'int__github__conn1234__list_repos': {
      description: '[GitHub] List repos',
      parameters: {} as never,
      execute: vi.fn(),
    },
  });

  const result = await resolveGlobalAssistantIntegrationTools({
    userId: 'user-1',
    driveId: 'drive-1',
    userDriveRole: 'owner',
  });

  expect(Object.keys(result)).toHaveLength(1);
  expect(mockConvert).toHaveBeenCalledWith(
    mockGrants,
    { userId: 'user-1', agentId: null, driveId: 'drive-1' },
    mockExecutor
  );
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/lib/ai/core/__tests__/integration-tool-resolver.test.ts` around
lines 109 - 125, Add a positive test for resolveGlobalAssistantIntegrationTools
that mirrors the existing active-grants test for
resolvePageAgentIntegrationTools: mock mockResolveGlobalIntegrations to return a
sample grant array, mock mockCreateExecutor to return a mock executor, and
mockConvert to return a mapped tool object; then call
resolveGlobalAssistantIntegrationTools with a non-null driveId and userDriveRole
and assert that the returned tool keys length and that mockConvert was invoked
with (mockGrants, { userId: 'user-1', agentId: null, driveId: 'drive-1' },
mockExecutor) to verify conversion is performed.
apps/web/src/app/api/integrations/providers/route.ts (1)

31-48: Consider race condition during concurrent first-access requests.

If multiple requests hit this endpoint simultaneously when no providers exist, they could all pass the providers.length === 0 check before any completes seeding. While seedBuiltinProviders is idempotent (skips existing slugs), this could result in:

  1. Multiple concurrent insert attempts for the same providers
  2. Duplicate log entries for "Auto-seeded builtin integration providers"

This is likely low-risk given the idempotent design, but worth noting for high-traffic scenarios.

One potential mitigation is to use a database-level constraint (unique on slug) and handle the constraint violation gracefully, or implement an application-level mutex/lock for the seeding operation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/app/api/integrations/providers/route.ts` around lines 31 - 48,
Concurrent requests can race through the providers.length === 0 branch and cause
duplicate insert attempts and duplicate "Auto-seeded" logs; fix by making
seeding resilient: ensure a DB-unique constraint on provider.slug and update
seedBuiltinProviders to catch unique-constraint/duplicate-key errors and treat
them as non-fatal (ignore duplicate errors), then have the caller (the block
using listEnabledProviders and seedBuiltinProviders) re-query providers and only
log when the final re-query returns newly inserted slugs (i.e., compute
actualInserted = seeded.filter(s => !awaitExistingSlugs.includes(s.slug)) or
simply rely on the post-seed listEnabledProviders() result to decide whether to
log), so duplicate insert attempts are safely ignored and the "Auto-seeded
builtin integration providers" log only occurs for real inserts.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/lib/src/integrations/repositories/provider-repository.ts`:
- Around line 147-152: The code currently only queries enabled providers via
database.query.integrationProviders.findMany({ where:
eq(integrationProviders.enabled, true) }) so disabled-but-existing slugs are
treated as missing; update the query to fetch all existing providers (or at
minimum select all slugs) from integrationProviders, build installedSlugs from
that full result, and then compute toSeed = builtins.filter(b =>
!installedSlugs.has(b.id)) to avoid inserting duplicate slugs for previously
disabled providers.

---

Nitpick comments:
In `@apps/web/src/app/api/integrations/providers/route.ts`:
- Around line 31-48: Concurrent requests can race through the providers.length
=== 0 branch and cause duplicate insert attempts and duplicate "Auto-seeded"
logs; fix by making seeding resilient: ensure a DB-unique constraint on
provider.slug and update seedBuiltinProviders to catch
unique-constraint/duplicate-key errors and treat them as non-fatal (ignore
duplicate errors), then have the caller (the block using listEnabledProviders
and seedBuiltinProviders) re-query providers and only log when the final
re-query returns newly inserted slugs (i.e., compute actualInserted =
seeded.filter(s => !awaitExistingSlugs.includes(s.slug)) or simply rely on the
post-seed listEnabledProviders() result to decide whether to log), so duplicate
insert attempts are safely ignored and the "Auto-seeded builtin integration
providers" log only occurs for real inserts.

In `@apps/web/src/lib/ai/core/__tests__/integration-tool-resolver.test.ts`:
- Around line 109-125: Add a positive test for
resolveGlobalAssistantIntegrationTools that mirrors the existing active-grants
test for resolvePageAgentIntegrationTools: mock mockResolveGlobalIntegrations to
return a sample grant array, mock mockCreateExecutor to return a mock executor,
and mockConvert to return a mapped tool object; then call
resolveGlobalAssistantIntegrationTools with a non-null driveId and userDriveRole
and assert that the returned tool keys length and that mockConvert was invoked
with (mockGrants, { userId: 'user-1', agentId: null, driveId: 'drive-1' },
mockExecutor) to verify conversion is performed.

In `@packages/lib/src/integrations/repositories/provider-repository.ts`:
- Around line 156-173: The seeding loop currently inserts each provider
sequentially (using
database.insert(integrationProviders).values(...).returning()) and pushes
results into the seeded array, which can leave a partial state if one insert
fails; wrap the entire loop in a single database transaction so all inserts are
committed together or rolled back on error. Locate the block that iterates over
toSeed and performs database.insert(...) against integrationProviders, start a
transaction via your database client (e.g., database.transaction or equivalent),
perform all inserts inside that transactional callback, and on success
return/assign the seeded results; ensure any thrown error causes the transaction
to rollback and that you propagate or log the error accordingly.
- Line 167: The current cast "builtin as unknown as Record<string, unknown>" on
the config field loses type safety; instead ensure the value assigned to config
is typed to the expected schema type (e.g., IntegrationProviderConfig) or narrow
it with a specific cast/transform rather than double-casting to unknown. Update
the assignment in provider-repository.ts so that the "builtin" value is either
declared/validated as IntegrationProviderConfig (or the precise JSON column type
used by your ORM) before assignment, or explicitly convert it to a safe
Record<string, unknown> via a typed mapping/serialization step; reference the
"config" property assignment and the "builtin" symbol when making the change.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between afb36e6 and b241876.

📒 Files selected for processing (7)
  • .env.example
  • apps/web/.env.example
  • apps/web/src/app/api/integrations/providers/route.ts
  • apps/web/src/lib/ai/core/__tests__/integration-tool-resolver.test.ts
  • packages/lib/src/integrations/index.ts
  • packages/lib/src/integrations/repositories/provider-repository.test.ts
  • packages/lib/src/integrations/repositories/provider-repository.ts

2witstudios and others added 2 commits February 28, 2026 08:24
The GrantWithConnectionAndProvider type has deeply nested
IntegrationProviderConfig that's impractical to fully satisfy
in test mocks. Use 'as unknown as' assertion for the mock data.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…isions

Previously queried only enabled providers, which could miss disabled ones
and attempt to re-seed their slugs causing unique constraint violations.
Now fetches all provider slugs regardless of enabled status.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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