Skip to content

fix(copilot): strip platform-managed apiKey on hosted-tool blocks in edit_workflow#5217

Merged
TheodoreSpeaks merged 2 commits into
stagingfrom
investigate/hosted-key-mothership
Jun 25, 2026
Merged

fix(copilot): strip platform-managed apiKey on hosted-tool blocks in edit_workflow#5217
TheodoreSpeaks merged 2 commits into
stagingfrom
investigate/hosted-key-mothership

Conversation

@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator

Summary

  • The copilot intermittently authored apiKey: "{{FAL_API_KEY}}" (and similar) onto hosted-tool blocks like the Fal video/image generators. On hosted Sim this both disabled hosted-key injection (a non-empty inline key short-circuits injectHostedKeyIfNeeded) and misled the agent into telling users they must bring their own key.
  • edit_workflow's preValidateCredentialInputs already stripped+warned for hosted LLM models (getHostedModels()), but that path only covers Agent/Router blocks — tool-hosted blocks slipped through.
  • Generalized the strip to key off tool.hosting — the same canonical signal injectHostedKeyIfNeeded uses at execution, so validator and execution agree by construction:
    • resolves the block's active tool via its tools.config.tool selector (so provider: falaivideo_falai),
    • honors the per-provider hosting.enabled gate (e.g. image_generate falai-only),
    • strips the exact hosting.apiKeyParam field (no hardcoded "apiKey" assumption),
    • surfaces the existing non-fatal note back to the agent (success: true preserved).
  • Self-hosted (isHosted=false) and non-hosted providers (Runway/Veo/Luma/MiniMax) are untouched.

Type of Change

  • Bug fix

Testing

  • vitest on validation.test.ts: 31 passed (added hosted-tool cases — provider routing, edit-op provider resolution from workflow state, nested-in-loop, non-apiKey field name, enabled-gate pass/fail, self-hosted preserved)
  • bun run lint:check: clean
  • bun run check:api-validation:strict: passed
  • tsc --noEmit: clean

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

…edit_workflow

preValidateCredentialInputs only stripped apiKey for hosted LLM models (getHostedModels). Agent-authored apiKey on hosted-tool blocks (Fal video/image, etc.) slipped through, disabling hosted-key injection and misleading the agent into telling users to bring a key.

Generalize the strip to key off tool.hosting — the same canonical signal injectHostedKeyIfNeeded uses at execution. Resolve the block's active tool via its tools.config.tool selector, honor the per-provider enabled gate, and strip the exact hosting.apiKeyParam field (no hardcoded 'apiKey' assumption). Surfaces the existing non-fatal note to the agent. Self-hosted and non-hosted providers untouched.
@vercel

vercel Bot commented Jun 25, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
docs Ready Ready Preview, Comment Jun 25, 2026 10:43pm

Request Review

@cursor

cursor Bot commented Jun 25, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Touches copilot workflow validation and API key handling on hosted deployments; behavior is gated on isHosted and covered by new tests, but wrong tool/hosting resolution could strip or preserve keys incorrectly.

Overview
Fixes copilot edit_workflow authoring apiKey (and similar) on Fal-backed video/image blocks on hosted Sim, which blocked platform key injection and misled users.

preValidateCredentialInputs now strips platform-managed key fields when isHosted is true, not only for hosted LLM models (getHostedModels) but also for blocks whose active tool declares tool.hosting—aligned with injectHostedKeyIfNeeded at execution. Resolution uses the block’s tools.config.tool selector, honors hosting.enabled per provider, and removes the field named by hosting.apiKeyParam (not hardcoded apiKey). Edit ops merge existing subblock state (e.g. provider) when inputs omit routing fields; nested loop/parallel children get the same treatment. Stripped inputs return non-fatal validation notes; self-hosted and non-hosted providers are unchanged.

Tests add hosted-tool scenarios (provider routing, edit-only apiKey, nested blocks, custom param names, enabled gate, isHosted false).

Reviewed by Cursor Bugbot for commit 95a449f. Bugbot is set up for automated code reviews on this repo. Configure here.

@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@greptile review

…-key-mothership

# Conflicts:
#	apps/sim/lib/copilot/tools/server/workflow/edit-workflow/validation.test.ts
@greptile-apps

greptile-apps Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a bug where the copilot intermittently authored platform-managed API key fields (e.g. apiKey: "{{FAL_API_KEY}}") onto hosted-tool blocks such as Fal video/image generators, which both disabled hosted-key injection at execution time and misled the agent into telling users they must supply their own key.

  • Adds collectHostedToolApiKeyInput in preValidateCredentialInputs, keyed off tool.hosting — the same canonical signal injectHostedKeyIfNeeded uses at execution — to resolve the active tool via the block's tools.config.tool selector, check the per-provider hosting.enabled gate, and strip the exact hosting.apiKeyParam field rather than a hardcoded "apiKey" string.
  • For edit operations on top-level blocks, the new path merges existing subblock values (e.g. the current provider) into toolParams so that multi-provider selectors and enabled gates resolve correctly even when the edit doesn't restate every field; nested blocks in nestedNodes are always fully re-specified so this merging is not needed there.
  • Adds eight targeted test cases covering provider routing, non-apiKey field names, per-provider enabled gates, nested-in-loop stripping, edit-op state merging, and self-hosted preservation.

Confidence Score: 4/5

The fix is safe to merge — it only adds a new stripping pass that runs after the existing hosted-model pass, leaves self-hosted deployments and non-hosted providers completely untouched, and is guarded by isHosted throughout.

The core logic correctly follows tool.hosting as the canonical signal and properly merges existing subblock state for edit ops on top-level blocks. Two minor concerns: a tool selector that returns a non-string is silently treated as a no-op miss rather than an explicit guard, and the existingBlock lookup from workflowState is performed twice for edit ops. Neither affects correctness in the described scenarios, but they leave small silent-miss edge cases unguarded.

Both changed files look good. The test file covers the key scenarios thoroughly. The only area worth a second look is the tool-selector null-return path in collectHostedToolApiKeyInput.

Important Files Changed

Filename Overview
apps/sim/lib/copilot/tools/server/workflow/edit-workflow/validation.ts Adds collectHostedToolApiKeyInput to strip platform-managed API key fields from hosted-tool blocks. Logic correctly follows tool.hosting as the canonical signal. Minor: existingBlock is fetched from workflowState twice on edit ops (once for model lookup, once for tool params).
apps/sim/lib/copilot/tools/server/workflow/edit-workflow/validation.test.ts Comprehensive new test suite covering provider routing, per-provider enabled gates, non-apiKey field names, edit-op state merging, nested-in-loop stripping, and self-hosted preservation. No test for edit ops on nested blocks, but that case is inherently covered since nestedNodes are always fully re-specified.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[preValidateCredentialInputs] --> B{op.params.type?}
    B -- yes --> C[getBlock blockConfig]
    C --> D[collectHostedApiKeyInput\nhosted LLM models]
    C --> E{edit op +\nworkflowState?}
    E -- yes --> F[merge existing subBlock values\ninto toolParams]
    E -- no --> G[toolParams = op.params.inputs]
    F --> H[collectHostedToolApiKeyInput]
    G --> H
    H --> I{blockConfig.tools?}
    I -- no --> Z[skip]
    I -- yes --> J{toolSelector exists?}
    J -- yes --> K[toolSelector toolParams\nresolve active toolId]
    J -- no --> L[all tools.access as candidates]
    K --> M[getTool toolId]
    L --> M
    M --> N{tool.hosting?}
    N -- no --> Z
    N -- yes --> O{hosting.enabled gate?}
    O -- fail --> Z
    O -- pass / no gate --> P[add hosting.apiKeyParam\nto managedFieldIds]
    P --> Q{field present in inputs?}
    Q -- yes --> R[push to hostedApiKeyInputs]
    Q -- no --> Z
    R --> S[structuredClone ops\nstrip field + push error]
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A[preValidateCredentialInputs] --> B{op.params.type?}
    B -- yes --> C[getBlock blockConfig]
    C --> D[collectHostedApiKeyInput\nhosted LLM models]
    C --> E{edit op +\nworkflowState?}
    E -- yes --> F[merge existing subBlock values\ninto toolParams]
    E -- no --> G[toolParams = op.params.inputs]
    F --> H[collectHostedToolApiKeyInput]
    G --> H
    H --> I{blockConfig.tools?}
    I -- no --> Z[skip]
    I -- yes --> J{toolSelector exists?}
    J -- yes --> K[toolSelector toolParams\nresolve active toolId]
    J -- no --> L[all tools.access as candidates]
    K --> M[getTool toolId]
    L --> M
    M --> N{tool.hosting?}
    N -- no --> Z
    N -- yes --> O{hosting.enabled gate?}
    O -- fail --> Z
    O -- pass / no gate --> P[add hosting.apiKeyParam\nto managedFieldIds]
    P --> Q{field present in inputs?}
    Q -- yes --> R[push to hostedApiKeyInputs]
    Q -- no --> Z
    R --> S[structuredClone ops\nstrip field + push error]
Loading

Comments Outside Diff (2)

  1. apps/sim/lib/copilot/tools/server/workflow/edit-workflow/validation.ts, line 1149-1163 (link)

    P2 Duplicated workflowState lookup for edit ops — existingBlock is resolved twice in the same branch (once for the model-value lookup above, and again here for toolParams). Merging into a single lookup makes it easier to see the two consumers at a glance and avoids the double cast.

    Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

  2. apps/sim/lib/copilot/tools/server/workflow/edit-workflow/validation.ts, line 1063-1070 (link)

    P2 Tool selector may return a non-string silently

    toolSelector(toolParams) is typed as returning the type inferred from the block config, but at runtime it could return undefined or null (e.g. if the provider param is missing on an add-op). The result lands directly in candidateToolIds = [toolSelector(toolParams)] and is then passed to getTool. getTool calls resolveToolId(toolId) which indexes into tools[toolId] — with a null/undefined toolId that's a no-op miss, so no crash, but the field is silently not stripped for that op. Adding a truthiness guard before pushing into candidateToolIds would make the intent explicit and prevent subtle misses.

Reviews (1): Last reviewed commit: "Merge remote-tracking branch 'origin/sta..." | Re-trigger Greptile

@greptile-apps

greptile-apps Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR generalizes the existing hosted-LLM apiKey stripping in preValidateCredentialInputs to also cover hosted-tool blocks (e.g. Fal video/image generators). Previously, only Agent/Router blocks had their copilot-authored platform keys stripped; tool-hosted blocks slipped through, leading to disabled key injection and misleading user-facing errors. The fix resolves the active tool via the block's tools.config.tool selector, checks tool.hosting, and strips hosting.apiKeyParam — mirroring the exact signal used by injectHostedKeyIfNeeded at execution.

  • Adds collectHostedToolApiKeyInput helper that resolves the tool selector, checks the enabled gate, and generically strips hosting.apiKeyParam (not hardcoded to \"apiKey\").
  • For edit operations on main blocks, merges existing subblock values from workflowState so the tool selector resolves correctly even when provider isn't restated.
  • 7 new test cases cover provider routing, enabled gate pass/fail, non-apiKey field names, nested-in-loop, and self-hosted preservation.

Confidence Score: 4/5

Safe to merge — the change fixes a real bug on the hosted platform and is well-covered by tests for the primary paths. The two gaps (unguarded enabled() predicate, no state enrichment for nested-edit ops) are edge cases that don't affect the common add-operation scenario this fix targets.

The core fix correctly mirrors injectHostedKeyIfNeeded's logic and is exercised by 7 new tests covering the key variants. The enabled() predicate in collectHostedToolApiKeyInput is not wrapped in try/catch unlike the toolSelector call, so a misbehaving predicate could surface as an unhandled error on any edit_workflow call touching a tool-hosted block. Nested edit operations inside loop/parallel blocks also don't benefit from the workflowState enrichment added for main blocks, leaving a blind spot when a child's provider isn't restated in the edit op.

validation.ts — specifically the collectHostedToolApiKeyInput helper around the enabled() gate and the nested-node call site.

Important Files Changed

Filename Overview
apps/sim/lib/copilot/tools/server/workflow/edit-workflow/validation.ts Core logic change — adds collectHostedToolApiKeyInput and wires it into both main-block and nested-block paths. The tool selector is guarded with try/catch, but the hosting.enabled() predicate is not, so a throwing enabled gate would surface as an unhandled error from preValidateCredentialInputs. Nested edit ops inside loops also don't enrich toolParams from workflowState, leaving a gap for partial edit operations on child blocks.
apps/sim/lib/copilot/tools/server/workflow/edit-workflow/validation.test.ts Adds 7 targeted test cases that cover the new hosting path well — provider routing, enabled gate, non-apiKey field name, nested-in-loop, and self-hosted bypass. Dynamic isHosted mock via getter is a clean pattern for this test environment.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[preValidateCredentialInputs] --> B{Main block inputs present?}
    B -- yes --> C[getBlock type]
    C --> D[collectHostedApiKeyInput - LLM model path]
    C --> E{edit op and workflowState?}
    E -- yes --> F[Merge existing subBlock values into toolParams]
    E -- no --> G[toolParams = op.params.inputs]
    F --> H[collectHostedToolApiKeyInput]
    G --> H
    B -- also --> Y[nestedNodes present?]
    Y -- yes --> Z[for each child block]
    Z --> AA[collectHostedToolApiKeyInput - childInputs only, no state merge]
    H --> I{blockConfig.tools exists?}
    I -- no --> J[return - skip]
    I -- yes --> K{tools.config.tool selector?}
    K -- yes --> L[try selector - catch return]
    K -- no --> M[candidateToolIds = tools.access]
    L --> N[candidateToolIds = single resolved tool]
    N --> O[for each candidateToolId - getTool]
    M --> O
    O --> P{tool.hosting?}
    P -- no --> Q[skip]
    P -- yes --> R{hosting.enabled gate passes?}
    R -- no --> Q
    R -- yes --> S[managedFieldIds add apiKeyParam]
    S --> T{field is non-empty string?}
    T -- no --> Q
    T -- yes --> U[push to hostedApiKeyInputs]
    U --> V[structuredClone operations]
    AA --> V
    V --> W[Strip managed fields from filteredOperations]
    W --> X[Push validation errors with stripMessage]
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A[preValidateCredentialInputs] --> B{Main block inputs present?}
    B -- yes --> C[getBlock type]
    C --> D[collectHostedApiKeyInput - LLM model path]
    C --> E{edit op and workflowState?}
    E -- yes --> F[Merge existing subBlock values into toolParams]
    E -- no --> G[toolParams = op.params.inputs]
    F --> H[collectHostedToolApiKeyInput]
    G --> H
    B -- also --> Y[nestedNodes present?]
    Y -- yes --> Z[for each child block]
    Z --> AA[collectHostedToolApiKeyInput - childInputs only, no state merge]
    H --> I{blockConfig.tools exists?}
    I -- no --> J[return - skip]
    I -- yes --> K{tools.config.tool selector?}
    K -- yes --> L[try selector - catch return]
    K -- no --> M[candidateToolIds = tools.access]
    L --> N[candidateToolIds = single resolved tool]
    N --> O[for each candidateToolId - getTool]
    M --> O
    O --> P{tool.hosting?}
    P -- no --> Q[skip]
    P -- yes --> R{hosting.enabled gate passes?}
    R -- no --> Q
    R -- yes --> S[managedFieldIds add apiKeyParam]
    S --> T{field is non-empty string?}
    T -- no --> Q
    T -- yes --> U[push to hostedApiKeyInputs]
    U --> V[structuredClone operations]
    AA --> V
    V --> W[Strip managed fields from filteredOperations]
    W --> X[Push validation errors with stripMessage]
Loading

Comments Outside Diff (2)

  1. apps/sim/lib/copilot/tools/server/workflow/edit-workflow/validation.ts, line 1079 (link)

    P2 The toolSelector call is wrapped in try/catch to guard against selector errors, but the tool.hosting.enabled(toolParams) call immediately below is not. If any tool's enabled predicate throws (e.g. due to an unexpected toolParams shape), the error propagates unhandled out of preValidateCredentialInputs, which would break the copilot's edit_workflow for that request. The same defensive pattern used for the selector should apply here.

  2. apps/sim/lib/copilot/tools/server/workflow/edit-workflow/validation.ts, line 1202-1210 (link)

    P2 Nested edit ops miss workflowState enrichment

    For main blocks, toolParams is enriched from workflowState on edit ops so the tool selector resolves correctly when provider is not re-stated. The nested-node path always passes childInputs as toolParams, so an edit op on a loop block that only specifies apiKey for a child — without restating provider — will feed { apiKey: '…' } to the selector. For a multi-provider block like video_generator_v3, the selector returns the wrong tool (e.g. video_runway), and the key is not stripped even though it should be. The test suite only covers the add case for nested nodes, so this gap is unverified.

Reviews (2): Last reviewed commit: "Merge remote-tracking branch 'origin/sta..." | Re-trigger Greptile

@TheodoreSpeaks TheodoreSpeaks merged commit 5db5963 into staging Jun 25, 2026
16 checks passed
@TheodoreSpeaks TheodoreSpeaks deleted the investigate/hosted-key-mothership branch June 25, 2026 23:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant