Skip to content

feat(capabilities): multi-credential support for capability delegation#328

Open
zhubzy wants to merge 6 commits intomainfrom
feat/multi-credential
Open

feat(capabilities): multi-credential support for capability delegation#328
zhubzy wants to merge 6 commits intomainfrom
feat/multi-credential

Conversation

@zhubzy
Copy link
Contributor

@zhubzy zhubzy commented Mar 19, 2026

Summary

  • Adds credential pool support to capabilities, allowing multiple credentials of the same type to be passed and delegated to sub-agents
  • Adds multi-select validation schema for credential selection in the UI
  • Updates BubbleInjector to extract and route pooled credentials during capability delegation
  • Bumps OSS packages to 0.1.239

Test plan

  • Verify capability with multiple credentials of the same type delegates correctly
  • Verify single-credential capabilities still work unchanged
  • Verify UI multi-select validation schema renders correctly in bubble-studio

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • AI agents can use a credential pool to manage and select multiple credentials per credential type when delegating.
    • Agents show available credential accounts and accept explicit credential choices at runtime; multi-capability prompts include guidance to request capability details.
    • Flow inputs now accept one or more credential IDs per bubble/credential type; user credentials may include optional friendly names.
  • Chores

    • Bumped package versions across the monorepo (0.1.238 → 0.1.242).

Copilot AI review requested due to automatic review settings March 19, 2026 08:44
@coderabbitai
Copy link

coderabbitai bot commented Mar 19, 2026

📝 Walkthrough

Walkthrough

Widen credential mappings to accept single IDs or arrays; add optional credentialPool to AI agent params and capability preprocessing; use-capability accepts credential selection and delegates resolved credentials to sub-agents; BubbleInjector groups/injects credential pools for ai-agent bubbles; several package versions bumped to 0.1.242.

Changes

Cohort / File(s) Summary
API & Schema Types
apps/bubblelab-api/src/services/bubble-flow-parser.ts, packages/bubble-shared-schemas/src/bubbleflow-execution-schema.ts
Widened credentials mapping to accept `number
AI Agent & Capability Pipeline
packages/bubble-core/src/bubbles/service-bubble/ai-agent.ts, packages/bubble-core/src/bubbles/service-bubble/capability-pipeline.ts
Added optional credentialPool to agent params and preprocessing; use-capability tooling accepts credentials input; delegation resolves and injects selected credentials into sub-agents; preprocessing augments prompts with available account listings and a capability-question rule.
Runtime Credential Injection
packages/bubble-runtime/src/injection/BubbleInjector.ts
Added optional name to UserCredentialWithId; injector groups user credentials by type and builds per-type {id,name,value} arrays; injects/upserts credentialPool param into ai-agent bubbles when multiple options exist; prevents overwrite of first-seen credential mapping per type.
Package Versions & Templates
packages/bubble-core/package.json, packages/bubble-runtime/package.json, packages/bubble-scope-manager/package.json, packages/bubble-shared-schemas/package.json, packages/create-bubblelab-app/package.json, packages/create-bubblelab-app/templates/*
Bumped package versions and template dependency ranges to 0.1.242 (version/dependency-range updates only).

Sequence Diagram

sequenceDiagram
    participant Injector as BubbleInjector
    participant CredPool as CredentialPool
    participant AIAgent as AI Agent
    participant CapPipeline as Capability Pipeline
    participant SubAgent as Sub-Agent

    Injector->>CredPool: group user creds by type → build {id,name,value} pools
    Injector->>AIAgent: inject credentialPool param (if multi-option)

    AIAgent->>CapPipeline: applyCapabilityPreprocessing(params, credentialPool)
    CapPipeline-->>AIAgent: enhanced prompts listing available credential ids/names

    AIAgent->>AIAgent: use-capability invoked with input.credentials
    AIAgent->>CredPool: resolve selected credential IDs → values
    AIAgent->>SubAgent: spawn delegated agent with resolved subAgentCredentials
    SubAgent->>SubAgent: execute capability using selected credential
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly Related PRs

  • PR #309: Modifies applyCapabilityPreprocessing signature and capability preprocessing logic — overlaps with credentialPool wiring.
  • PR #285: Changes UserCredentialWithId and BubbleInjector credential handling — touches the same injection surface.

Poem

🐰 I hopped through keys in a tidy little row,
Names and IDs ready, which pools shall we show?
Agents pick, pass down, and softly they hum—
Choices gathered, delegated, and off they run! 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title accurately and specifically describes the main feature: adding multi-credential support for capability delegation, which is the primary objective across all changed files.

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

✨ 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 feat/multi-credential
📝 Coding Plan
  • Generate coding plan for human review comments

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.

@cloudflare-workers-and-pages
Copy link

cloudflare-workers-and-pages bot commented Mar 19, 2026

Deploying bubblelab-documentation with  Cloudflare Pages  Cloudflare Pages

Latest commit: 95b068b
Status: ✅  Deploy successful!
Preview URL: https://4992766e.bubblelab-documentation.pages.dev
Branch Preview URL: https://feat-multi-credential.bubblelab-documentation.pages.dev

View logs

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds support for selecting and delegating multiple credentials of the same type in capability delegation, by introducing a credential “pool” alongside existing single-credential injection and updating schemas to accept multi-select credential IDs.

Changes:

  • Expand BubbleFlow credential mappings to allow credential ID or credential ID[] in validation/parsing schemas.
  • Inject a new credentialPool param into ai-agent bubbles and use it to resolve credential-ID overrides for use-capability.
  • Surface available multi-account choices in the multi-capability system prompt summaries.

Reviewed changes

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

Show a summary per file
File Description
packages/create-bubblelab-app/templates/reddit-scraper/package.json Bumps template deps to 0.1.239.
packages/create-bubblelab-app/templates/basic/package.json Bumps template deps to 0.1.239.
packages/create-bubblelab-app/package.json Bumps package version to 0.1.239.
packages/bubble-shared-schemas/src/bubbleflow-execution-schema.ts Allows credentials mapping values to be number or number[].
packages/bubble-shared-schemas/package.json Bumps package version to 0.1.239.
packages/bubble-scope-manager/package.json Bumps package version to 0.1.239.
packages/bubble-runtime/src/injection/BubbleInjector.ts Adds credential pool extraction/injection into ai-agent bubbles.
packages/bubble-runtime/package.json Bumps package version to 0.1.239.
packages/bubble-core/src/bubbles/service-bubble/capability-pipeline.ts Lists available multi-account credential choices in multi-capability summaries.
packages/bubble-core/src/bubbles/service-bubble/ai-agent.ts Adds credentialPool param schema + use-capability.credentials overrides resolved via pool.
packages/bubble-core/package.json Bumps package version to 0.1.239.
apps/bubblelab-api/src/services/bubble-flow-parser.ts Updates credential merge types to allow `number

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines +610 to +616
// For ai-agent bubbles, build and inject credential pool when
// multiple user credentials exist for the same credential type
if (bubble.bubbleName === 'ai-agent' && userCreds.length > 0) {
const credsByType = new Map<
CredentialType,
Array<{ id: number; name: string; value: string }>
>();
Comment on lines +632 to +647
// Only inject pool if at least one type has multiple credentials
const hasMultiple = Array.from(credsByType.values()).some(
(entries) => entries.length > 1
);
if (hasMultiple) {
const pool: Record<
string,
Array<{ id: number; name: string; value: string }>
> = {};
for (const [credType, entries] of credsByType) {
if (entries.length > 1) {
pool[credType] = entries;
}
}
this.injectCredentialPoolIntoBubble(bubble, pool);
}
Comment on lines +1702 to +1711
// Resolve credential overrides from the credential pool
const subAgentCredentials = this.params.credentials
? { ...this.params.credentials }
: undefined;
if (
credentialOverrides &&
this.params.credentialPool &&
subAgentCredentials
) {
for (const [credType, credId] of Object.entries(
Comment on lines +1711 to +1720
for (const [credType, credId] of Object.entries(
credentialOverrides
)) {
const pool =
this.params.credentialPool[credType as CredentialType];
const match = pool?.find((c) => c.id === credId);
if (match) {
subAgentCredentials[credType as CredentialType] = match.value;
}
}
)
.optional()
.describe(
'All available credentials per type with metadata. Used by master agent for delegation credential selection.'
Comment on lines 855 to 864
}

// Build a map of bubbleName -> credentials from old parameters
const credentialsByBubbleName: Record<string, Record<string, number>> = {};
const credentialsByBubbleName: Record<
string,
Record<string, number | number[]>
> = {};
for (const [oldKey, oldBubble] of Object.entries(oldBubbleParameters)) {
const bubbleName = oldBubble.bubbleName;
if (bubbleName && credentials[oldKey]) {
Comment on lines +613 to +631
const credsByType = new Map<
CredentialType,
Array<{ id: number; name: string; value: string }>
>();
for (const uc of userCreds) {
if (
!allCredentialOptions.includes(uc.credentialType) ||
uc.credentialId == null
)
continue;
if (!credsByType.has(uc.credentialType)) {
credsByType.set(uc.credentialType, []);
}
credsByType.get(uc.credentialType)!.push({
id: uc.credentialId,
name: uc.name ?? `${uc.credentialType} (${uc.credentialId})`,
value: this.escapeString(uc.secret),
});
}
Comment on lines +1694 to +1696
const credentialOverrides = input.credentials as
| Record<string, number>
| undefined;
Comment on lines +610 to +648
// For ai-agent bubbles, build and inject credential pool when
// multiple user credentials exist for the same credential type
if (bubble.bubbleName === 'ai-agent' && userCreds.length > 0) {
const credsByType = new Map<
CredentialType,
Array<{ id: number; name: string; value: string }>
>();
for (const uc of userCreds) {
if (
!allCredentialOptions.includes(uc.credentialType) ||
uc.credentialId == null
)
continue;
if (!credsByType.has(uc.credentialType)) {
credsByType.set(uc.credentialType, []);
}
credsByType.get(uc.credentialType)!.push({
id: uc.credentialId,
name: uc.name ?? `${uc.credentialType} (${uc.credentialId})`,
value: this.escapeString(uc.secret),
});
}
// Only inject pool if at least one type has multiple credentials
const hasMultiple = Array.from(credsByType.values()).some(
(entries) => entries.length > 1
);
if (hasMultiple) {
const pool: Record<
string,
Array<{ id: number; name: string; value: string }>
> = {};
for (const [credType, entries] of credsByType) {
if (entries.length > 1) {
pool[credType] = entries;
}
}
this.injectCredentialPoolIntoBubble(bubble, pool);
}
}
Comment on lines +1684 to +1721
credentials: z
.record(z.nativeEnum(CredentialType), z.number())
.optional()
.describe(
'Optional: map credential type to credential ID to select a specific account. Only needed when multiple credentials of the same type are available. If omitted, the default credential is used.'
),
}),
func: async (input: Record<string, unknown>) => {
const capabilityId = input.capabilityId as string;
const task = input.task as string;
const credentialOverrides = input.credentials as
| Record<string, number>
| undefined;
const capConfig = caps.find((c) => c.id === capabilityId);
const capDef = getCapability(capabilityId);
if (!capConfig || !capDef)
return { error: `Capability "${capabilityId}" not found` };

// Resolve credential overrides from the credential pool
const subAgentCredentials = this.params.credentials
? { ...this.params.credentials }
: undefined;
if (
credentialOverrides &&
this.params.credentialPool &&
subAgentCredentials
) {
for (const [credType, credId] of Object.entries(
credentialOverrides
)) {
const pool =
this.params.credentialPool[credType as CredentialType];
const match = pool?.find((c) => c.id === credId);
if (match) {
subAgentCredentials[credType as CredentialType] = match.value;
}
}
}
Copy link

@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: 2

Caution

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

⚠️ Outside diff range comments (2)
apps/bubblelab-api/src/services/bubble-flow-parser.ts (1)

844-860: ⚠️ Potential issue | 🟠 Major

Recognize array-valued credentials in the populated-value guard.

After widening this merge path to number | number[], the existing skip check still only treats scalar numbers as “real” credentials. A bubble that already has credentials: { foo: [1, 2] } will now be considered empty and re-merged from oldBubbleParameters, which can clobber the new multi-select choice on reparse.

Suggested fix
-      const hasRealCredentials = Object.values(credValue).some(
-        (v) => typeof v === 'number'
-      );
+      const hasRealCredentials = Object.values(credValue).some(
+        (v) =>
+          typeof v === 'number' ||
+          (Array.isArray(v) && v.some((id) => typeof id === 'number'))
+      );

Also applies to: 873-883

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

In `@apps/bubblelab-api/src/services/bubble-flow-parser.ts` around lines 844 -
860, The guard that decides whether a bubble already has populated credentials
treats only scalar numbers as valid and ignores array-valued credentials,
causing arrays like credentials: { foo: [1,2] } to be considered empty and
overwritten; update the merge logic in the function handling
updatedParameters/oldBubbleParameters so that checks on credentials (and the
credentialsByBubbleName lookup) treat both number and number[] as populated
(e.g., test Array.isArray(value) && value.length > 0 or typeof value ===
'number') before deciding to skip merging from oldBubbleParameters, and apply
the same change to the analogous check later in the code that handles lines
around the other merge path (the block that references credentialsByBubbleName).
packages/bubble-runtime/src/injection/BubbleInjector.ts (1)

568-580: ⚠️ Potential issue | 🟠 Major

Make the fallback account explicit.

use-capability now documents “omit credentials to use the default credential”, but this pool only carries { id, name, value }. The actual fallback still comes from the earlier last-write-wins loop over userCreds, so a different caller ordering can silently switch which account a delegated capability uses. Please inject an explicit default id/flag (or derive the pool from the credential already written into credentials) so the no-override path is deterministic.

Also applies to: 610-647

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

In `@packages/bubble-runtime/src/injection/BubbleInjector.ts` around lines 568 -
580, The fallback credential selection is currently implicit (last-write-wins
over userCreds) causing nondeterministic delegation; modify BubbleInjector to
explicitly record the chosen default credential when populating
credentialMapping/injectedCredentials (in the loop handling userCreds and the
similar block at 610-647). Concretely, when you set
credentialMapping[userCredType] and
injectedCredentials[`${bubble.variableId}.${userCredType}`], also set an
explicit default marker or id (e.g.,
injectedCredentials[`${bubble.variableId}.${userCredType}.default`] or a
property like isDefaultCredential/defaultCredentialId) tied to the exact
userCred.id/value so the “omit credentials to use the default credential” path
deterministically derives the pool from that recorded default instead of relying
on caller ordering. Ensure the same explicit-default logic is applied in both
places referenced (the shown loop and the 610-647 block).
🤖 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/bubble-core/src/bubbles/service-bubble/ai-agent.ts`:
- Around line 1684-1689: The code currently validates optional credentials via
the z.record schema but later when applying requested (credType, id) pairs
against credentialPool (in the use-capability / sub-agent credential resolution
logic that reads this.params.credentials and iterates credentialPool) it
silently skips missing ids; change that behavior to explicitly reject/make an
error when a requested credential id is not found. Update the credential
resolution loop (the code that inspects credentialPool and sets
this.params.credentials for the sub-agent) to check each requested pair exists
in credentialPool and throw a descriptive error (or return a rejected Result)
naming the missing credentialType and id instead of falling back, so the caller
sees a clear failure when a stale/hallucinated id is provided.

In `@packages/bubble-core/src/bubbles/service-bubble/capability-pipeline.ts`:
- Around line 96-112: The summary construction in capability-pipeline.ts
currently interpolates user-controlled entry.name from credentialPool into the
system prompt (see variables credentialPool, capCredTypes, and the loop over
entry), which allows quotes/newlines to break the prompt; before appending
entry.name to summary, normalize it to a single line and escape or sanitize
characters that could break prompts (e.g., replace newlines, escape
quotes/backticks, and trim control characters), then append the sanitized value
and keep the guidance text (Pass 'credentials: { ... }' / use-capability)
unchanged so the prompt remains safe.

---

Outside diff comments:
In `@apps/bubblelab-api/src/services/bubble-flow-parser.ts`:
- Around line 844-860: The guard that decides whether a bubble already has
populated credentials treats only scalar numbers as valid and ignores
array-valued credentials, causing arrays like credentials: { foo: [1,2] } to be
considered empty and overwritten; update the merge logic in the function
handling updatedParameters/oldBubbleParameters so that checks on credentials
(and the credentialsByBubbleName lookup) treat both number and number[] as
populated (e.g., test Array.isArray(value) && value.length > 0 or typeof value
=== 'number') before deciding to skip merging from oldBubbleParameters, and
apply the same change to the analogous check later in the code that handles
lines around the other merge path (the block that references
credentialsByBubbleName).

In `@packages/bubble-runtime/src/injection/BubbleInjector.ts`:
- Around line 568-580: The fallback credential selection is currently implicit
(last-write-wins over userCreds) causing nondeterministic delegation; modify
BubbleInjector to explicitly record the chosen default credential when
populating credentialMapping/injectedCredentials (in the loop handling userCreds
and the similar block at 610-647). Concretely, when you set
credentialMapping[userCredType] and
injectedCredentials[`${bubble.variableId}.${userCredType}`], also set an
explicit default marker or id (e.g.,
injectedCredentials[`${bubble.variableId}.${userCredType}.default`] or a
property like isDefaultCredential/defaultCredentialId) tied to the exact
userCred.id/value so the “omit credentials to use the default credential” path
deterministically derives the pool from that recorded default instead of relying
on caller ordering. Ensure the same explicit-default logic is applied in both
places referenced (the shown loop and the 610-647 block).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e409b0a9-52d3-4187-b484-6916cb444c38

📥 Commits

Reviewing files that changed from the base of the PR and between 2d65bfa and 9442961.

📒 Files selected for processing (12)
  • apps/bubblelab-api/src/services/bubble-flow-parser.ts
  • packages/bubble-core/package.json
  • packages/bubble-core/src/bubbles/service-bubble/ai-agent.ts
  • packages/bubble-core/src/bubbles/service-bubble/capability-pipeline.ts
  • packages/bubble-runtime/package.json
  • packages/bubble-runtime/src/injection/BubbleInjector.ts
  • packages/bubble-scope-manager/package.json
  • packages/bubble-shared-schemas/package.json
  • packages/bubble-shared-schemas/src/bubbleflow-execution-schema.ts
  • packages/create-bubblelab-app/package.json
  • packages/create-bubblelab-app/templates/basic/package.json
  • packages/create-bubblelab-app/templates/reddit-scraper/package.json

Comment on lines +1684 to +1689
credentials: z
.record(z.nativeEnum(CredentialType), z.number())
.optional()
.describe(
'Optional: map credential type to credential ID to select a specific account. Only needed when multiple credentials of the same type are available. If omitted, the default credential is used.'
),
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Reject unknown credential selections instead of silently falling back.

If use-capability gets a stale or hallucinated ID, this loop just skips it and the sub-agent runs with whatever credential was already in this.params.credentials. That makes the tool call look successful while operating on the wrong account. Return an error when a requested (credType, id) pair is missing from credentialPool.

Suggested fix
             const credentialOverrides = input.credentials as
               | Record<string, number>
               | undefined;
@@
             const subAgentCredentials = this.params.credentials
               ? { ...this.params.credentials }
               : undefined;
+            const invalidOverrides: string[] = [];
             if (
               credentialOverrides &&
               this.params.credentialPool &&
               subAgentCredentials
             ) {
               for (const [credType, credId] of Object.entries(
                 credentialOverrides
               )) {
                 const pool =
                   this.params.credentialPool[credType as CredentialType];
                 const match = pool?.find((c) => c.id === credId);
-                if (match) {
-                  subAgentCredentials[credType as CredentialType] = match.value;
-                }
+                if (!match) {
+                  invalidOverrides.push(`${credType}:${credId}`);
+                  continue;
+                }
+                subAgentCredentials[credType as CredentialType] = match.value;
               }
             }
+            if (invalidOverrides.length > 0) {
+              return {
+                success: false,
+                error: `Unknown credential selection(s): ${invalidOverrides.join(', ')}`,
+              };
+            }

Also applies to: 1694-1721

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

In `@packages/bubble-core/src/bubbles/service-bubble/ai-agent.ts` around lines
1684 - 1689, The code currently validates optional credentials via the z.record
schema but later when applying requested (credType, id) pairs against
credentialPool (in the use-capability / sub-agent credential resolution logic
that reads this.params.credentials and iterates credentialPool) it silently
skips missing ids; change that behavior to explicitly reject/make an error when
a requested credential id is not found. Update the credential resolution loop
(the code that inspects credentialPool and sets this.params.credentials for the
sub-agent) to check each requested pair exists in credentialPool and throw a
descriptive error (or return a rejected Result) naming the missing
credentialType and id instead of falling back, so the caller sees a clear
failure when a stale/hallucinated id is provided.

Comment on lines +96 to +112
// List available credentials when multiple exist for a type
if (credentialPool) {
const capCredTypes = [
...def.metadata.requiredCredentials,
...(def.metadata.optionalCredentials ?? []),
];
for (const credType of capCredTypes) {
const pool = credentialPool[credType];
if (pool && pool.length > 1) {
summary += `\n Available ${credType} accounts:`;
for (const entry of pool) {
summary += `\n - id=${entry.id}: "${entry.name}"`;
}
summary += `\n Pass 'credentials: { ${credType}: <id> }' to use-capability to select a specific account.`;
}
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Escape credential names before embedding them in the system prompt.

entry.name is user-controlled metadata from the injected credential pool. Concatenating it verbatim lets quotes, newlines, or prompt-like text bleed into the delegator’s system prompt here, which can skew routing or account selection. Normalize it to a single line and escape it before interpolation.

Suggested fix
                 summary += `\n   Available ${credType} accounts:`;
                 for (const entry of pool) {
-                  summary += `\n   - id=${entry.id}: "${entry.name}"`;
+                  const safeName = JSON.stringify(
+                    entry.name.replace(/[\r\n]+/g, ' ').trim()
+                  );
+                  summary += `\n   - id=${entry.id}: ${safeName}`;
                 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// List available credentials when multiple exist for a type
if (credentialPool) {
const capCredTypes = [
...def.metadata.requiredCredentials,
...(def.metadata.optionalCredentials ?? []),
];
for (const credType of capCredTypes) {
const pool = credentialPool[credType];
if (pool && pool.length > 1) {
summary += `\n Available ${credType} accounts:`;
for (const entry of pool) {
summary += `\n - id=${entry.id}: "${entry.name}"`;
}
summary += `\n Pass 'credentials: { ${credType}: <id> }' to use-capability to select a specific account.`;
}
}
}
// List available credentials when multiple exist for a type
if (credentialPool) {
const capCredTypes = [
...def.metadata.requiredCredentials,
...(def.metadata.optionalCredentials ?? []),
];
for (const credType of capCredTypes) {
const pool = credentialPool[credType];
if (pool && pool.length > 1) {
summary += `\n Available ${credType} accounts:`;
for (const entry of pool) {
const safeName = JSON.stringify(
entry.name.replace(/[\r\n]+/g, ' ').trim()
);
summary += `\n - id=${entry.id}: ${safeName}`;
}
summary += `\n Pass 'credentials: { ${credType}: <id> }' to use-capability to select a specific account.`;
}
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/bubble-core/src/bubbles/service-bubble/capability-pipeline.ts`
around lines 96 - 112, The summary construction in capability-pipeline.ts
currently interpolates user-controlled entry.name from credentialPool into the
system prompt (see variables credentialPool, capCredTypes, and the loop over
entry), which allows quotes/newlines to break the prompt; before appending
entry.name to summary, normalize it to a single line and escape or sanitize
characters that could break prompts (e.g., replace newlines, escape
quotes/backticks, and trim control characters), then append the sanitized value
and keep the guidance text (Pass 'credentials: { ... }' / use-capability)
unchanged so the prompt remains safe.

Copy link

@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: 4

🤖 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/bubble-core/src/bubbles/service-bubble/capability-pipeline.ts`:
- Line 114: The system prompt in capability-pipeline.ts unconditionally mandates
calling get_capabilities, but initializeTools() does not register a
get_capabilities tool; update the prompt generation so it does not require a
non-existent call path — either remove the absolute requirement or make it
conditional based on tool registration (e.g., check what initializeTools()
registers and only include the "MUST call get_capabilities" sentence if
get_capabilities is present), and ensure the clause references
params.systemPrompt and the initializeTools()/tool registry so the prompt and
available tools stay consistent.
- Around line 104-105: The use-capability handler currently only accepts and
forwards capabilityId and task, so the new prompt's credentials selection is
dropped; update the use-capability handler signature (the function named
use-capability exposed in ai-agent.ts) to accept a credentials parameter (e.g.,
credentials: Record<string,string> | undefined) and pass that credentials object
through to handleUseCability(...) when invoking it; also update any internal
calls or type definitions related to use-capability and handleUseCability to
include the credentials argument so pooled credentials are preserved and
forwarded to downstream handling.

In `@packages/bubble-runtime/src/injection/BubbleInjector.ts`:
- Around line 621-633: The credential name (uc.name) is not escaped before being
inserted into the generated credentialPool, allowing injected quotes/newlines to
break rewritten source; update the construction inside the userCreds loop (where
credsByType is populated) to escape the name by passing uc.name (and the default
`${uc.credentialType} (${uc.credentialId})` fallback) through
this.escapeString()—e.g., compute an escapedName = this.escapeString(uc.name ??
`${uc.credentialType} (${uc.credentialId})`) and use escapedName for the name
field (and keep using this.escapeString(uc.secret) for value) so all
user-controlled strings are sanitized.
- Around line 567-578: The user credential loop currently skips setting
credentialMapping when a key already exists, which lets earlier-loaded
systemCredentials win; change the guard in the userCreds loop so user
credentials override system fallbacks but still preserve the "first user wins"
semantics: when iterating userCreds (variable userCred, property
credentialType/secret) set credentialMapping[userCredType] =
this.escapeString(userCred.secret) if the key is not present OR if the existing
mapping came from systemCredentials (use the systemCredentials collection to
detect that credentialType), ensuring you don't overwrite a previously-set user
credential; adjust the if condition accordingly around the assignment in
BubbleInjector (keep escapeString usage).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: fb3672be-8769-4370-98f9-b43346de73bb

📥 Commits

Reviewing files that changed from the base of the PR and between 4405f09 and 0cc0c9e.

📒 Files selected for processing (9)
  • packages/bubble-core/package.json
  • packages/bubble-core/src/bubbles/service-bubble/capability-pipeline.ts
  • packages/bubble-runtime/package.json
  • packages/bubble-runtime/src/injection/BubbleInjector.ts
  • packages/bubble-scope-manager/package.json
  • packages/bubble-shared-schemas/package.json
  • packages/create-bubblelab-app/package.json
  • packages/create-bubblelab-app/templates/basic/package.json
  • packages/create-bubblelab-app/templates/reddit-scraper/package.json
✅ Files skipped from review due to trivial changes (7)
  • packages/bubble-scope-manager/package.json
  • packages/create-bubblelab-app/package.json
  • packages/bubble-core/package.json
  • packages/bubble-runtime/package.json
  • packages/bubble-shared-schemas/package.json
  • packages/create-bubblelab-app/templates/reddit-scraper/package.json
  • packages/create-bubblelab-app/templates/basic/package.json

Comment on lines +104 to +105
summary += `\n Pass 'credentials: { ${credType}: <id> }' to use-capability to select a specific account.`;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

use-capability cannot consume this new credentials argument yet.

This prompt now tells the model to pass credentials: { ... }, but the provided packages/bubble-core/src/bubbles/service-bubble/ai-agent.ts:1437-1900 snippet still exposes use-capability with only capabilityId and task, and forwards only those two fields to handleUseCability(...). The selection data will be dropped, so pooled credentials still fall back to the default account.

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

In `@packages/bubble-core/src/bubbles/service-bubble/capability-pipeline.ts`
around lines 104 - 105, The use-capability handler currently only accepts and
forwards capabilityId and task, so the new prompt's credentials selection is
dropped; update the use-capability handler signature (the function named
use-capability exposed in ai-agent.ts) to accept a credentials parameter (e.g.,
credentials: Record<string,string> | undefined) and pass that credentials object
through to handleUseCability(...) when invoking it; also update any internal
calls or type definitions related to use-capability and handleUseCability to
include the credentials argument so pooled credentials are preserved and
forwarded to downstream handling.

).filter((summary): summary is string => Boolean(summary));

params.systemPrompt += `\n\n---\nSYSTEM CAPABILITY EXTENSIONS:\nMultiple specialized capabilities are available. You MUST delegate to them using the 'use-capability' tool.\n\nAvailable Capabilities:\n${summaries.join('\n\n')}\n\nDELEGATION RULES:\n- Use 'use-capability' tool to delegate tasks to the appropriate capability\n- Do NOT attempt to handle capability tasks yourself\n- Include full context when delegating, including all known user details and preferences from context (especially timezone)\n- Can chain multiple capabilities if needed\n- Only respond directly for: greetings, clarifications, or tasks outside all capabilities\n- IMPORTANT: The user CANNOT see tool results from delegate agents. You MUST re-present all information, data, tables, and results returned by delegates in your own response. Never say "as shown above" or assume the user saw the delegate's output.\n- PRESERVE ALL LINKS: When a delegate returns URLs or clickable links (e.g., HubSpot record links, Jira ticket links, document URLs), you MUST include them in your response. Never drop links when reformatting tables — links are critical for the user to navigate to the source. If a table has too many rows to include all links, include links for at least the top/highlighted records.\n- When a delegate returns image or photo URLs, include them directly in your response on their own line as a bare URL (no markdown formatting). The chat client will automatically render the image inline from the URL. NEVER call read_image on URLs returned by delegates.\n- Before asking the user for information you don't have, check if any connected capability could help you find or look up that information first. Prefer proactive discovery over asking.\n---\n\nYour role is to understand the user's request and delegate to the appropriate capability or respond directly when appropriate.`;
params.systemPrompt += `\n\n---\nSYSTEM CAPABILITY EXTENSIONS:\nMultiple specialized capabilities are available. You MUST delegate to them using the 'use-capability' tool.\n\nAvailable Capabilities:\n${summaries.join('\n\n')}\n\nDELEGATION RULES:\n- Use 'use-capability' tool to delegate tasks to the appropriate capability\n- Do NOT attempt to handle capability tasks yourself\n- Include full context when delegating, including all known user details and preferences from context (especially timezone)\n- Can chain multiple capabilities if needed\n- Only respond directly for: greetings, clarifications, or tasks outside all capabilities\n- IMPORTANT: The user CANNOT see tool results from delegate agents. You MUST re-present all information, data, tables, and results returned by delegates in your own response. Never say "as shown above" or assume the user saw the delegate's output.\n- PRESERVE ALL LINKS: When a delegate returns URLs or clickable links (e.g., HubSpot record links, Jira ticket links, document URLs), you MUST include them in your response. Never drop links when reformatting tables — links are critical for the user to navigate to the source. If a table has too many rows to include all links, include links for at least the top/highlighted records.\n- When a delegate returns image or photo URLs, include them directly in your response on their own line as a bare URL (no markdown formatting). The chat client will automatically render the image inline from the URL. NEVER call read_image on URLs returned by delegates.\n- Before asking the user for information you don't have, check if any connected capability could help you find or look up that information first. Prefer proactive discovery over asking.\n- CAPABILITY QUESTIONS: When a user asks whether a capability supports a specific feature, what tools are available, or what parameters a tool accepts, you MUST call get_capabilities with the capability id before answering. Tool names alone do not describe what tools can do — you need the full signatures. NEVER answer capability questions from memory alone. NEVER claim a capability cannot do something without checking first.\n---\n\nYour role is to understand the user's request and delegate to the appropriate capability or respond directly when appropriate.`;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don’t require get_capabilities until the tool exists.

The same initializeTools() snippet does not register any get_capabilities tool, so this new hard rule pushes the model toward a nonexistent call path whenever users ask capability-feature questions.

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

In `@packages/bubble-core/src/bubbles/service-bubble/capability-pipeline.ts` at
line 114, The system prompt in capability-pipeline.ts unconditionally mandates
calling get_capabilities, but initializeTools() does not register a
get_capabilities tool; update the prompt generation so it does not require a
non-existent call path — either remove the absolute requirement or make it
conditional based on tool registration (e.g., check what initializeTools()
registers and only include the "MUST call get_capabilities" sentence if
get_capabilities is present), and ensure the clause references
params.systemPrompt and the initializeTools()/tool registry so the prompt and
available tools stay consistent.

Comment on lines +621 to +633
for (const uc of userCreds) {
if (
!allCredentialOptions.includes(uc.credentialType) ||
uc.credentialId == null
)
continue;
if (!credsByType.has(uc.credentialType)) {
credsByType.set(uc.credentialType, []);
}
credsByType.get(uc.credentialType)!.push({
id: uc.credentialId,
name: uc.name ?? `${uc.credentialType} (${uc.credentialId})`,
value: this.escapeString(uc.secret),
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Escape credential names before rewriting the bubble script.

uc.name is user-controlled metadata, but unlike secret it is inserted into the generated credentialPool object without escapeString(). A quote, backslash, or newline in the name can break the rewritten source.

Suggested fix
             credsByType.get(uc.credentialType)!.push({
               id: uc.credentialId,
-              name: uc.name ?? `${uc.credentialType} (${uc.credentialId})`,
+              name: this.escapeString(
+                uc.name ?? `${uc.credentialType} (${uc.credentialId})`
+              ),
               value: this.escapeString(uc.secret),
             });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for (const uc of userCreds) {
if (
!allCredentialOptions.includes(uc.credentialType) ||
uc.credentialId == null
)
continue;
if (!credsByType.has(uc.credentialType)) {
credsByType.set(uc.credentialType, []);
}
credsByType.get(uc.credentialType)!.push({
id: uc.credentialId,
name: uc.name ?? `${uc.credentialType} (${uc.credentialId})`,
value: this.escapeString(uc.secret),
for (const uc of userCreds) {
if (
!allCredentialOptions.includes(uc.credentialType) ||
uc.credentialId == null
)
continue;
if (!credsByType.has(uc.credentialType)) {
credsByType.set(uc.credentialType, []);
}
credsByType.get(uc.credentialType)!.push({
id: uc.credentialId,
name: this.escapeString(
uc.name ?? `${uc.credentialType} (${uc.credentialId})`
),
value: this.escapeString(uc.secret),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/bubble-runtime/src/injection/BubbleInjector.ts` around lines 621 -
633, The credential name (uc.name) is not escaped before being inserted into the
generated credentialPool, allowing injected quotes/newlines to break rewritten
source; update the construction inside the userCreds loop (where credsByType is
populated) to escape the name by passing uc.name (and the default
`${uc.credentialType} (${uc.credentialId})` fallback) through
this.escapeString()—e.g., compute an escapedName = this.escapeString(uc.name ??
`${uc.credentialType} (${uc.credentialId})`) and use escapedName for the name
field (and keep using this.escapeString(uc.secret) for value) so all
user-controlled strings are sanitized.

Copy link

@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

♻️ Duplicate comments (1)
packages/bubble-runtime/src/injection/BubbleInjector.ts (1)

633-636: ⚠️ Potential issue | 🟠 Major

Escape uc.name before writing it into credentialPool.

uc.name is still user-controlled data going into rewritten source, so quotes or newlines here can break the generated bubble script.

Suggested fix
+            const escapedName = this.escapeString(
+              uc.name ?? `${uc.credentialType} (${uc.credentialId})`
+            );
             credsByType.get(uc.credentialType)!.push({
               id: uc.credentialId,
-              name: uc.name ?? `${uc.credentialType} (${uc.credentialId})`,
+              name: escapedName,
               value: this.escapeString(uc.secret),
             });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/bubble-runtime/src/injection/BubbleInjector.ts` around lines 633 -
636, The code pushes user-controlled uc.name into credentialPool without
escaping; update the credsByType.push call in BubbleInjector (around the block
that constructs credential objects) to run uc.name through the same escaping
used for secrets (e.g., call this.escapeString(uc.name) or a dedicated escape
method) and keep the fallback name construction intact (escape the fallback
string too) so any quotes/newlines are sanitized before writing into the
generated bubble script.
🤖 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/bubble-runtime/src/injection/BubbleInjector.ts`:
- Around line 639-654: The code only injects a credential pool when hasMultiple
is true, but doesn't remove stale pool entries when duplicates disappear; update
BubbleInjector (around the hasMultiple check) to handle the false branch by
explicitly removing/clearing any existing pooled credentials from the bubble
parameters (the same place injectCredentialPoolIntoBubble writes to) so that
when credsByType collapses to a single credential you delete the prior pooled
entries and avoid stale overrides being resolved in injectCredentials/ai-agent;
reference the hasMultiple boolean, credsByType iteration, and
injectCredentialPoolIntoBubble to locate where to add the clearing logic.

---

Duplicate comments:
In `@packages/bubble-runtime/src/injection/BubbleInjector.ts`:
- Around line 633-636: The code pushes user-controlled uc.name into
credentialPool without escaping; update the credsByType.push call in
BubbleInjector (around the block that constructs credential objects) to run
uc.name through the same escaping used for secrets (e.g., call
this.escapeString(uc.name) or a dedicated escape method) and keep the fallback
name construction intact (escape the fallback string too) so any quotes/newlines
are sanitized before writing into the generated bubble script.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 849007f7-425e-4daf-bce0-4553fd7399e1

📥 Commits

Reviewing files that changed from the base of the PR and between 0cc0c9e and 95b068b.

📒 Files selected for processing (8)
  • packages/bubble-core/package.json
  • packages/bubble-runtime/package.json
  • packages/bubble-runtime/src/injection/BubbleInjector.ts
  • packages/bubble-scope-manager/package.json
  • packages/bubble-shared-schemas/package.json
  • packages/create-bubblelab-app/package.json
  • packages/create-bubblelab-app/templates/basic/package.json
  • packages/create-bubblelab-app/templates/reddit-scraper/package.json
✅ Files skipped from review due to trivial changes (7)
  • packages/bubble-scope-manager/package.json
  • packages/bubble-shared-schemas/package.json
  • packages/create-bubblelab-app/templates/reddit-scraper/package.json
  • packages/bubble-runtime/package.json
  • packages/create-bubblelab-app/templates/basic/package.json
  • packages/bubble-core/package.json
  • packages/create-bubblelab-app/package.json

Comment on lines +639 to +654
// Only inject pool if at least one type has multiple credentials
const hasMultiple = Array.from(credsByType.values()).some(
(entries) => entries.length > 1
);
if (hasMultiple) {
const pool: Record<
string,
Array<{ id: number; name: string; value: string }>
> = {};
for (const [credType, entries] of credsByType) {
if (entries.length > 1) {
pool[credType] = entries;
}
}
this.injectCredentialPoolIntoBubble(bubble, pool);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Clear stale credentialPool entries when the pool collapses back to one credential.

This branch only updates the parameter when hasMultiple is true. Because bubble.parameters is mutated in place, a later injectCredentials() call that no longer has duplicates will keep the old pooled secrets around, and packages/bubble-core/src/bubbles/service-bubble/ai-agent.ts will still resolve overrides against those stale entries.

Suggested fix
           if (hasMultiple) {
             const pool: Record<
               string,
               Array<{ id: number; name: string; value: string }>
             > = {};
             for (const [credType, entries] of credsByType) {
               if (entries.length > 1) {
                 pool[credType] = entries;
               }
             }
             this.injectCredentialPoolIntoBubble(bubble, pool);
+          } else {
+            const existingPoolIndex = bubble.parameters.findIndex(
+              (p) => p.name === 'credentialPool'
+            );
+            if (existingPoolIndex !== -1) {
+              bubble.parameters.splice(existingPoolIndex, 1);
+            }
           }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/bubble-runtime/src/injection/BubbleInjector.ts` around lines 639 -
654, The code only injects a credential pool when hasMultiple is true, but
doesn't remove stale pool entries when duplicates disappear; update
BubbleInjector (around the hasMultiple check) to handle the false branch by
explicitly removing/clearing any existing pooled credentials from the bubble
parameters (the same place injectCredentialPoolIntoBubble writes to) so that
when credsByType collapses to a single credential you delete the prior pooled
entries and avoid stale overrides being resolved in injectCredentials/ai-agent;
reference the hasMultiple boolean, credsByType iteration, and
injectCredentialPoolIntoBubble to locate where to add the clearing logic.

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.

2 participants