feat(capabilities): multi-credential support for capability delegation#328
feat(capabilities): multi-credential support for capability delegation#328
Conversation
📝 WalkthroughWalkthroughWiden credential mappings to accept single IDs or arrays; add optional Changes
Sequence DiagramsequenceDiagram
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
Estimated Code Review Effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly Related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
📝 Coding Plan
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. Comment |
Deploying bubblelab-documentation with
|
| 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 |
There was a problem hiding this comment.
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 IDorcredential ID[]in validation/parsing schemas. - Inject a new
credentialPoolparam intoai-agentbubbles and use it to resolve credential-ID overrides foruse-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.
| // 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 }> | ||
| >(); |
| // 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); | ||
| } |
| // 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( |
| 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.' |
| } | ||
|
|
||
| // 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]) { |
| 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), | ||
| }); | ||
| } |
| const credentialOverrides = input.credentials as | ||
| | Record<string, number> | ||
| | undefined; |
| // 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); | ||
| } | ||
| } |
| 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; | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
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 | 🟠 MajorRecognize 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 hascredentials: { foo: [1, 2] }will now be considered empty and re-merged fromoldBubbleParameters, 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 | 🟠 MajorMake the fallback account explicit.
use-capabilitynow 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 overuserCreds, 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 intocredentials) 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
📒 Files selected for processing (12)
apps/bubblelab-api/src/services/bubble-flow-parser.tspackages/bubble-core/package.jsonpackages/bubble-core/src/bubbles/service-bubble/ai-agent.tspackages/bubble-core/src/bubbles/service-bubble/capability-pipeline.tspackages/bubble-runtime/package.jsonpackages/bubble-runtime/src/injection/BubbleInjector.tspackages/bubble-scope-manager/package.jsonpackages/bubble-shared-schemas/package.jsonpackages/bubble-shared-schemas/src/bubbleflow-execution-schema.tspackages/create-bubblelab-app/package.jsonpackages/create-bubblelab-app/templates/basic/package.jsonpackages/create-bubblelab-app/templates/reddit-scraper/package.json
| 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.' | ||
| ), |
There was a problem hiding this comment.
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.
| // 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.`; | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
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.
| // 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.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ability feature checks
…ty feature questions
There was a problem hiding this comment.
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
📒 Files selected for processing (9)
packages/bubble-core/package.jsonpackages/bubble-core/src/bubbles/service-bubble/capability-pipeline.tspackages/bubble-runtime/package.jsonpackages/bubble-runtime/src/injection/BubbleInjector.tspackages/bubble-scope-manager/package.jsonpackages/bubble-shared-schemas/package.jsonpackages/create-bubblelab-app/package.jsonpackages/create-bubblelab-app/templates/basic/package.jsonpackages/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
| summary += `\n Pass 'credentials: { ${credType}: <id> }' to use-capability to select a specific account.`; | ||
| } |
There was a problem hiding this comment.
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.`; |
There was a problem hiding this comment.
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.
| 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), |
There was a problem hiding this comment.
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.
| 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.
…i-credential pools
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (1)
packages/bubble-runtime/src/injection/BubbleInjector.ts (1)
633-636:⚠️ Potential issue | 🟠 MajorEscape
uc.namebefore writing it intocredentialPool.
uc.nameis 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
📒 Files selected for processing (8)
packages/bubble-core/package.jsonpackages/bubble-runtime/package.jsonpackages/bubble-runtime/src/injection/BubbleInjector.tspackages/bubble-scope-manager/package.jsonpackages/bubble-shared-schemas/package.jsonpackages/create-bubblelab-app/package.jsonpackages/create-bubblelab-app/templates/basic/package.jsonpackages/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
| // 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); | ||
| } |
There was a problem hiding this comment.
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.
Summary
BubbleInjectorto extract and route pooled credentials during capability delegationTest plan
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Chores