Skip to content

feat(capabilities): name-based credential selection for multi-credential delegation#329

Merged
zhubzy merged 1 commit intomainfrom
feat/multi-credential
Mar 20, 2026
Merged

feat(capabilities): name-based credential selection for multi-credential delegation#329
zhubzy merged 1 commit intomainfrom
feat/multi-credential

Conversation

@zhubzy
Copy link
Contributor

@zhubzy zhubzy commented Mar 20, 2026

Summary

  • Switch use-capability credential parameter from numeric IDs to account names for more reliable AI selection
  • Match credentials by name (case-insensitive, substring), with numeric ID fallback
  • Change subagent default model from Gemini 3 Flash (no thinking) to Gemini 3 Flash (low thinking)
  • Update capability prompt to list credential names instead of IDs

Test plan

  • E2E test sends two messages targeting different Confluence workspaces — both route correctly
  • Name-based matching handles exact, substring, and numeric ID fallback
  • Subagent model change verified via trace inspection

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Credentials can now be selected using account names (case-insensitive with partial match support) instead of only IDs.
    • Credential accounts are displayed by name for improved clarity and accessibility.
  • Improvements

    • Enhanced default reasoning configuration for single-capability agents.

Copilot AI review requested due to automatic review settings March 20, 2026 03:48
@coderabbitai
Copy link

coderabbitai bot commented Mar 20, 2026

📝 Walkthrough

Walkthrough

This PR enhances credential selection in the AI agent capability delegation system by enabling name-based credential references alongside numeric IDs. The use-capability tool now accepts credentials as strings or numbers, implements multi-strategy name and ID matching against a credential pool, and updates system prompts to reflect account names instead of IDs.

Changes

Cohort / File(s) Summary
Credential Selection and Resolution
packages/bubble-core/src/bubbles/service-bubble/ai-agent.ts
Updated use-capability tool schema to accept Record<CredentialType, string | number> for credentials. Implemented resolution logic supporting exact name match, substring name match, numeric ID match, and string-to-ID parsing fallback when credentialPool exists. Changed delegation registration log from console.log to console.debug.
Delegation Prompt and Defaults
packages/bubble-core/src/bubbles/service-bubble/capability-pipeline.ts
Updated credential-account bullet format in multi-capability system prompt from - id=<entry.id>: "<safeName>" to - "<safeName>". Changed use-capability credential selection instruction to reference account names instead of IDs. Set default reasoningEffort to 'low' for single-capability "Capability Agent" sub-agents.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 Credentials named, not just numbered today,
Our matching hops smartly in every which way,
By name or ID, we find what we need,
Building better delegation—at rabbit speed,
No more ID confusion—we're hopping ahead! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.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 title 'feat(capabilities): name-based credential selection for multi-credential delegation' directly and accurately summarizes the main change: switching credential selection from numeric IDs to account names for multi-credential delegation.

✏️ 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 20, 2026

Deploying bubblelab-documentation with  Cloudflare Pages  Cloudflare Pages

Latest commit: 8e83000
Status: ✅  Deploy successful!
Preview URL: https://ac1e6d77.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 multi-credential delegation support by exposing multiple credentials to the master agent (as a pool) and enabling use-capability to select credentials by account name (with numeric fallback). This fits into the capability delegation pipeline by improving routing reliability when multiple accounts exist for the same integration.

Changes:

  • Extend credential plumbing to support multiple credential IDs per type and propagate credential name metadata through API → runtime injection.
  • Inject an credentialPool into ai-agent bubbles (when multiple creds exist) and allow use-capability to override subagent credentials by matching on account name (case-insensitive + substring) with numeric ID fallback.
  • Update capability prompt generation and subagent default model reasoning effort.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
packages/create-bubblelab-app/templates/reddit-scraper/package.json Bump template deps to newer BubbleLab package versions.
packages/create-bubblelab-app/templates/basic/package.json Bump template deps to newer BubbleLab package versions.
packages/create-bubblelab-app/package.json Version bump for create-bubblelab-app.
packages/bubble-shared-schemas/src/bubbleflow-execution-schema.ts Allow credential mappings to accept arrays of IDs in validation schema.
packages/bubble-shared-schemas/package.json Version bump for shared schemas package.
packages/bubble-scope-manager/package.json Version bump for ts-scope-manager package.
packages/bubble-runtime/src/injection/BubbleInjector.ts Inject credential pool + first-wins default behavior; add credential name metadata.
packages/bubble-runtime/package.json Version bump for bubble-runtime package.
packages/bubble-core/src/bubbles/service-bubble/capability-pipeline.ts Include credential account names in capability summaries; tweak subagent reasoning effort; add capability Q&A rule.
packages/bubble-core/src/bubbles/service-bubble/ai-agent.ts Add credentialPool param schema and implement name-based credential overrides for use-capability.
packages/bubble-core/package.json Version bump for bubble-core package.
apps/bubblelab-api/src/services/execution.ts Propagate credential name into runtime credential mappings.
apps/bubblelab-api/src/services/credential-helper.ts Support credential ID arrays and include credential name in mappings.
apps/bubblelab-api/src/services/bubble-flow-parser.ts Update credential merging types/logic to tolerate arrays.
Comments suppressed due to low confidence (1)

apps/bubblelab-api/src/services/credential-helper.ts:66

  • When supporting arrays of credential IDs, the implementation should preserve the user-specified ordering (since downstream injection treats the first credential as the default and preserves pool order). inArray(userCredentials.id, credentialIds) does not guarantee result order, so the resulting credentialMappings (and thus pool/default ordering) can be non-deterministic. Consider sorting encryptedCredentials to match the credentialIds sequence (or add an explicit orderBy) before constructing credentialMappings.
          for (const [, credentialIdOrIds] of Object.entries(credentialsObj)) {
            const ids = Array.isArray(credentialIdOrIds)
              ? credentialIdOrIds
              : [credentialIdOrIds];
            for (const credentialId of ids) {
              if (!credentialIds.includes(credentialId)) {
                credentialIds.push(credentialId);
              }
              // Support multiple variable names per credential ID
              if (!credentialIdToVarNames.has(credentialId)) {
                credentialIdToVarNames.set(credentialId, []);
              }
              credentialIdToVarNames.get(credentialId)!.push(varName);
            }
          }

Comment on lines +617 to +655
// 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);
}
}
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

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

Injecting an additional credentialPool parameter will break bubbles instantiated with a single variable first argument (e.g. new AIAgentBubble(params)), because the parameter formatter only has a special-case for spreading the first arg when only credentials is added. With credentialPool present too, it will emit something like { arg0: params, credentialPool: ..., credentials: ... }, which changes the runtime params shape and can make the agent fail schema validation. Consider updating the parameter formatting logic to spread the first-arg variable and then merge both credentials and credentialPool (and any other injected params), or alternatively inject credentialPool into the existing first-arg object rather than as a separate parameter entry.

Copilot uses AI. Check for mistakes.
Comment on lines +576 to +585
if (!seenUserCredTypes.has(userCredType)) {
seenUserCredTypes.add(userCredType);
credentialMapping[userCredType] = this.escapeString(
userCred.secret
);
injectedCredentials[`${bubble.variableId}.${userCredType}`] = {
isUserCredential: true,
credentialType: userCredType,
credentialValue: this.maskCredential(userCred.secret),
};
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

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

Credential secrets are being passed through escapeString() and then later serialized into an object literal via JSON.stringify/parameter formatting. That double-escaping will corrupt secrets that contain backslashes or newlines (e.g. multi-line keys, JSON creds), because \n will roundtrip as the two-character sequence instead of an actual newline. Prefer storing the raw secret in the injected parameter object and relying on JSON.stringify/formatter escaping once.

Copilot uses AI. Check for mistakes.
credsByType.get(uc.credentialType)!.push({
id: uc.credentialId,
name: uc.name ?? `${uc.credentialType} (${uc.credentialId})`,
value: this.escapeString(uc.secret),
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

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

The credential pool entries are also pre-escaped via escapeString(uc.secret) before being serialized into an object literal, which can double-escape and corrupt secrets with backslashes/newlines. Use the raw secret for value and let the object serializer handle escaping once.

Suggested change
value: this.escapeString(uc.secret),
value: uc.secret,

Copilot uses AI. Check for mistakes.
Comment on lines +567 to +586
// Inject user credentials (first credential of each type wins as the "default")
// User credentials always override system credentials.
// Within a multi-credential pool, the first entry wins (pool order
// is preserved by CredentialHelper.getUserCredentials()).
const seenUserCredTypes = new Set<string>();
for (const userCred of userCreds) {
const userCredType = userCred.credentialType;

if (allCredentialOptions.includes(userCredType)) {
credentialMapping[userCredType] = this.escapeString(
userCred.secret
);
injectedCredentials[`${bubble.variableId}.${userCredType}`] = {
isUserCredential: true,
credentialType: userCredType,
credentialValue: this.maskCredential(userCred.secret),
};
if (!seenUserCredTypes.has(userCredType)) {
seenUserCredTypes.add(userCredType);
credentialMapping[userCredType] = this.escapeString(
userCred.secret
);
injectedCredentials[`${bubble.variableId}.${userCredType}`] = {
isUserCredential: true,
credentialType: userCredType,
credentialValue: this.maskCredential(userCred.secret),
};
}
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

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

This code now depends on the order of userCreds ("first credential of each type wins" and pool order). However, CredentialHelper.getUserCredentials() does not enforce a deterministic ordering for decrypted credentials from the DB, so the selected default (and pool ordering) may vary between runs. Consider explicitly sorting userCreds (e.g., by credentialId or a stable user-defined order) before applying the "first wins" logic so defaults are consistent.

Copilot uses AI. Check for mistakes.
Comment on lines +1705 to +1749
// 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, credSelector] of Object.entries(
credentialOverrides
)) {
const pool =
this.params.credentialPool[credType as CredentialType];
if (!pool) continue;

// Match by name first (string), fall back to ID (number)
let match: (typeof pool)[number] | undefined;
if (typeof credSelector === 'string') {
const sel = credSelector.toLowerCase();
// Exact match first, then substring (handles "email (label)" format)
match = pool.find((c) => c.name.toLowerCase() === sel);
if (!match) {
match = pool.find((c) =>
c.name.toLowerCase().includes(sel)
);
}
}
if (!match && typeof credSelector === 'number') {
match = pool.find((c) => c.id === credSelector);
}
// Also try parsing string as number for ID fallback
if (!match && typeof credSelector === 'string') {
const asNum = Number(credSelector);
if (!Number.isNaN(asNum)) {
match = pool.find((c) => c.id === asNum);
}
}

if (match) {
subAgentCredentials[credType as CredentialType] = match.value;
}
}
}
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

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

New delegation credential selection logic isn’t covered by unit tests. Since AIAgentBubble already has a credential-system test suite, add tests that exercise use-capability credential overrides (exact name match, substring match, numeric ID fallback, and the behavior when a selector doesn’t match) to prevent regressions.

Copilot uses AI. Check for mistakes.
…gent defaults

- Switch use-capability credential parameter from numeric IDs to account names
- Match credentials by name (case-insensitive, substring), with numeric ID fallback
- Update capability prompt to list credential names without IDs
- Change subagent default model to Gemini 3 Flash with low thinking

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@zhubzy zhubzy force-pushed the feat/multi-credential branch from ee182dc to 8e83000 Compare March 20, 2026 03:57
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

🤖 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 1722-1732: The string selector logic allows an empty string to
match every credential because "".includes("") is true; update the credSelector
handling in the block around variables credSelector, sel, pool, and match so you
trim and lower-case the selector (const sel = credSelector.trim().toLowerCase())
and only perform exact/substr matching when sel.length > 0; if sel is empty skip
the name-based search (leaving match undefined so the code can fall back to
numeric ID or other logic).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6d58f83e-0c62-49eb-a8ec-9e0fd4d6d2bb

📥 Commits

Reviewing files that changed from the base of the PR and between ff065c4 and 8e83000.

📒 Files selected for processing (2)
  • packages/bubble-core/src/bubbles/service-bubble/ai-agent.ts
  • packages/bubble-core/src/bubbles/service-bubble/capability-pipeline.ts

Comment on lines +1722 to +1732
// Match by name first (string), fall back to ID (number)
let match: (typeof pool)[number] | undefined;
if (typeof credSelector === 'string') {
const sel = credSelector.toLowerCase();
// Exact match first, then substring (handles "email (label)" format)
match = pool.find((c) => c.name.toLowerCase() === sel);
if (!match) {
match = pool.find((c) =>
c.name.toLowerCase().includes(sel)
);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Empty string selector matches first credential unintentionally.

If the AI passes an empty string as credSelector, "".toLowerCase().includes("") evaluates to true for any credential name, causing the first credential in the pool to be selected. This could lead to unexpected behavior if the LLM hallucinates an empty credential reference.

🛡️ Proposed guard against empty selectors
 if (typeof credSelector === 'string') {
   const sel = credSelector.toLowerCase();
+  if (!sel) continue; // Skip empty selectors
   // Exact match first, then substring (handles "email (label)" format)
   match = pool.find((c) => c.name.toLowerCase() === sel);
   if (!match) {
     match = pool.find((c) =>
       c.name.toLowerCase().includes(sel)
     );
   }
 }
🤖 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
1722 - 1732, The string selector logic allows an empty string to match every
credential because "".includes("") is true; update the credSelector handling in
the block around variables credSelector, sel, pool, and match so you trim and
lower-case the selector (const sel = credSelector.trim().toLowerCase()) and only
perform exact/substr matching when sel.length > 0; if sel is empty skip the
name-based search (leaving match undefined so the code can fall back to numeric
ID or other logic).

@zhubzy zhubzy merged commit 34dcfc5 into main Mar 20, 2026
8 checks passed
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