Skip to content

feat(plugins): surface manifest provider setup choices#71240

Merged
vincentkoc merged 1 commit into
mainfrom
feat-manifest-provider-setup-flow
Apr 24, 2026
Merged

feat(plugins): surface manifest provider setup choices#71240
vincentkoc merged 1 commit into
mainfrom
feat-manifest-provider-setup-flow

Conversation

@vincentkoc

Copy link
Copy Markdown
Member

Summary

  • Problem: provider setup flow only used runtime wizard choices plus install-catalog choices, so installed manifest-owned providerAuthChoices were not a direct setup-flow source.
  • Why it matters: phase 2 needs provider setup discovery to work from manifest/control-plane metadata before setup-api or provider runtime loads.
  • What changed: provider setup flow now surfaces manifest provider auth choices as source: "manifest", prefers them over duplicate runtime/install-catalog entries, and keeps runtime/install-catalog paths as compatibility fallback.
  • What did NOT change (scope boundary): no removal of runtime wizard setup, no setup-api enforcement change, and no breaking contract change.

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor required for the fix
  • Docs
  • Security hardening
  • Chore/infra

Scope (select all touched areas)

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

Linked Issue/PR

  • Closes #
  • Related #
  • This PR fixes a bug or regression

Root Cause (if applicable)

  • Root cause: N/A
  • Missing detection / guardrail: provider setup-flow tests only covered runtime and install-catalog contributions, not manifest provider auth choices.
  • Contributing context (if known): plugin architecture phase 2 is moving simple setup discovery toward manifest descriptors while preserving setup-api/runtime compatibility.

Regression Test Plan (if applicable)

  • Coverage level that should have caught this:
    • Unit test
    • Seam / integration test
    • End-to-end test
    • Existing coverage already sufficient
  • Target test or file: src/flows/provider-flow.test.ts
  • Scenario the test should lock in: manifest provider auth choices appear as setup-flow contributions and win over duplicate runtime/install-catalog choices.
  • Why this is the smallest reliable guardrail: provider-flow owns setup option aggregation and source precedence.
  • Existing test that already covers this (if any): N/A
  • If no new test is added, why not: N/A

User-visible / Behavior Changes

Provider setup choices declared in plugin manifests can appear in setup/onboarding flows without loading provider runtime, when no duplicate higher-priority option suppresses them.

Diagram (if applicable)

N/A

Security Impact (required)

  • New permissions/capabilities? (No)
  • Secrets/tokens handling changed? (No)
  • New/changed network calls? (No)
  • Command/tool execution surface changed? (No)
  • Data access scope changed? (No)
  • If any Yes, explain risk + mitigation: N/A

Repro + Verification

Environment

  • OS: macOS
  • Runtime/container: Node 22 / pnpm
  • Model/provider: N/A
  • Integration/channel (if any): provider setup flow
  • Relevant config (redacted): N/A

Steps

  1. Run pnpm test src/flows/provider-flow.test.ts.
  2. Run pnpm check:changed.

Expected

  • Focused provider-flow tests pass.
  • Changed gate passes for core, core tests, and docs lanes.

Actual

  • Focused provider-flow tests passed: 8 tests.
  • pnpm check:changed passed: 6 files, 30 tests.

Evidence

  • Failing test/log before + passing after
  • Trace/log snippets
  • Screenshot/recording
  • Perf numbers (if relevant)

Human Verification (required)

  • Verified scenarios: manifest setup-flow contribution, duplicate manifest/runtime/install-catalog precedence, existing install-catalog fallback, runtime fallback, model-picker docs behavior.
  • Edge cases checked: scope filtering and duplicate option values.
  • What you did not verify: full production build.

Review Conversations

  • I replied to or resolved every bot review conversation I addressed in this PR.
  • I left unresolved only the conversations that still need reviewer or maintainer judgment.

Compatibility / Migration

  • Backward compatible? (Yes)
  • Config/env changes? (No)
  • Migration needed? (No)
  • If yes, exact upgrade steps: N/A

Runtime wizard setup and install-catalog setup choices remain wired as compatibility fallback.

Risks and Mitigations

  • Risk: a plugin with duplicate manifest and runtime choices may see the manifest label/metadata win.
    • Mitigation: this only applies when the plugin already declared the same manifest choice id, which is the intended manifest-first control-plane contract.

@vincentkoc vincentkoc self-assigned this Apr 24, 2026
@openclaw-barnacle openclaw-barnacle Bot added docs Improvements or additions to documentation size: S maintainer Maintainer-authored PR labels Apr 24, 2026
@vincentkoc vincentkoc marked this pull request as ready for review April 24, 2026 20:14
@aisle-research-bot

aisle-research-bot Bot commented Apr 24, 2026

Copy link
Copy Markdown

🔒 Aisle Security Analysis

We found 2 potential security issue(s) in this PR:

# Severity Title
1 🟡 Medium Terminal escape injection via manifest-controlled auth choice labels/hints rendered in CLI prompts
2 🟡 Medium Provider setup option spoofing via choiceId collision (manifest suppresses runtime/install-catalog options)
1. 🟡 Terminal escape injection via manifest-controlled auth choice labels/hints rendered in CLI prompts
Property Value
Severity Medium
CWE CWE-150
Location src/flows/provider-flow.ts:125-166

Description

Untrusted plugin manifest fields (choiceLabel, choiceHint, groupLabel, groupHint) are copied into ProviderSetupFlowContribution.option.* and then rendered by the setup/auth-choice CLI prompts without neutralizing terminal control sequences.

  • Input: plugin manifest-controlled strings from resolveManifestProviderAuthChoices()
  • Propagation: copied directly into ProviderSetupFlowContribution.option.label/hint/group.label/group.hint
  • Sink: createClackPrompter().select() passes opt.label and (styled) opt.hint to @​clack/prompts which prints them to the terminal
  • No sanitization is applied before display; stripAnsi() is only used for search tokenization, not output

This enables ANSI escape/control sequence injection (terminal UI redressing), allowing a malicious plugin manifest to manipulate the user's terminal display (e.g., hiding options, spoofing prompts, rewriting lines), potentially leading to phishing-style credential capture or unsafe selections.

Vulnerable code (manifest to flow contribution):

label: choice.choiceLabel,
...(choice.choiceHint ? { hint: choice.choiceHint } : {}),
...
label: groupLabel,
...(choice.groupHint ? { hint: choice.groupHint } : {}),

Rendering sink (no escaping):

const base = { value: opt.value, label: opt.label };
return opt.hint === undefined ? base : { ...base, hint: stylePromptHint(opt.hint) };
...
await select({ message: stylePromptMessage(params.message), options })

Recommendation

Sanitize/neutralize terminal control sequences for any untrusted strings before passing them to terminal prompt renderers.

A robust fix is to strip all ANSI escapes and other C0 control characters (except \n/\t if desired) at the boundary where untrusted strings enter UI rendering.

Example (centralized, applied in createClackPrompter for all labels/hints):

import stripAnsi from "strip-ansi";

function sanitizeForTerminal(s: string): string {// Remove ANSI escapes
  const noAnsi = stripAnsi(s);// Remove other control characters (C0/C1) to prevent terminal control injection
  return noAnsi.replace(/[\u0000-\u001F\u007F-\u009F]/g, "");
}

const options = params.options.map((opt) => {
  const base = { value: opt.value, label: sanitizeForTerminal(opt.label) };
  return opt.hint === undefined
    ? base
    : { ...base, hint: stylePromptHint(sanitizeForTerminal(opt.hint)) };
});

Also consider applying the same sanitization to group labels/hints wherever groups are rendered, and add tests ensuring escape sequences are removed.

2. 🟡 Provider setup option spoofing via choiceId collision (manifest suppresses runtime/install-catalog options)
Property Value
Severity Medium
CWE CWE-451
Location src/flows/provider-flow.ts:178-229

Description

resolveProviderSetupFlowContributions prioritizes manifest-defined provider auth choices and de-duplicates subsequent runtime and install-catalog options solely by option.value (the choiceId). This allows a plugin manifest to replace an existing trusted setup choice in the UI/CLI by reusing the same choiceId.

Impact:

  • A malicious/compromised plugin that can publish a manifest providerAuthChoices entry with a colliding choiceId can cause the legitimate runtime/install-catalog entry to be hidden.
  • The user is then presented with the attacker-controlled label/hint/group text for that choiceId, enabling credential phishing / social engineering (the user believes they are selecting a trusted provider).
  • The selected authChoice string (the colliding choiceId) is later used to resolve what provider/method to run and/or whether to enable/install a plugin. This means the attacker can influence what the user selects, even though the underlying resolution is driven by the raw string.

Vulnerable code:

const seenOptionValues = new Set(
  manifestContributions.map((contribution) => contribution.option.value),
);
...
.filter((option) => !seenOptionValues.has(option.value))
...
.filter((contribution) => !seenOptionValues.has(contribution.option.value));

Recommendation

Do not allow manifest choices to suppress runtime/install-catalog choices based on a global choiceId alone.

Safer approaches:

  1. Namespace the de-duplication key by trusted identity, e.g. (source, pluginId, providerId, methodId, choiceId) and only de-dupe when the same provider/method is represented:
type SetupKey = string;
const keyFor = (c: { source: string; providerId?: string; pluginId?: string; option: { value: string } }) =>
  [c.source, c.pluginId ?? "", c.providerId ?? "", c.option.value].join("::");

const seen = new Set(manifestContributions.map(keyFor));
const runtime = resolveProviderWizardOptions(...)
  .filter((o) => !seen.has(["runtime", "", o.groupId, o.value].join("::")));
  1. If you want manifest entries to override, require an explicit match and verification:
  • Only suppress a runtime/install-catalog option when the manifest entry matches the same (providerId, methodId).
  • Prefer higher-trust origins when collisions occur (e.g., bundled/config > global > workspace), and do not allow lower-trust origins to override higher-trust entries.
  1. Display the resolved provider/plugin identity next to the option and warn on collisions (same choiceId from different origins).

Analyzed PR: #71240 at commit bc40d51

Last updated on: 2026-04-24T20:16:26Z

@vincentkoc vincentkoc merged commit cf85825 into main Apr 24, 2026
40 of 42 checks passed
@vincentkoc vincentkoc deleted the feat-manifest-provider-setup-flow branch April 24, 2026 20:14
@greptile-apps

greptile-apps Bot commented Apr 24, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR surfaces plugin manifest providerAuthChoices as a first-class source (source: "manifest") in the provider setup flow, deduplicating against runtime/install-catalog contributions so the manifest wins when the same choiceId appears in multiple sources.

  • Missing enable-state filter in manifest path: resolveManifestProviderSetupFlowContributions never calls resolveEffectiveEnableState, so manifest choices from plugins that are globally disabled (plugins.enabled: false) or excluded by an allowlist (plugins.allow) will still surface in the setup flow. The install-catalog path (which has existing tests for both cases at lines 247–299) applies this filter explicitly; the manifest path needs the same gate to be consistent.

Confidence Score: 3/5

Not safe to merge until the manifest path applies the same plugin enable-state filter that the install-catalog path already enforces.

A P1 logic gap exists: manifest contributions bypass the resolveEffectiveEnableState check that install-catalog contributions go through, meaning explicitly-disabled plugins can still appear in the setup flow via their manifest metadata. The rest of the change (dedup ordering, scope filtering, type extension, tests, docs) is correct.

src/flows/provider-flow.ts — resolveManifestProviderSetupFlowContributions needs resolveEffectiveEnableState filtering mirroring resolveInstallCatalogProviderSetupFlowContributions; src/flows/provider-flow.test.ts — needs tests for manifest contributions respecting plugins.enabled: false and allowlist config.

Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/flows/provider-flow.ts
Line: 125-168

Comment:
**Missing enable-state gate for manifest contributions**

`resolveManifestProviderSetupFlowContributions` never calls `resolveEffectiveEnableState`, so manifest choices from explicitly-disabled or non-allowlisted plugins will still appear in the setup flow. By contrast, `resolveInstallCatalogProviderSetupFlowContributions` filters every entry through `resolveEffectiveEnableState({ ..., enabledByDefault: true })`.

Concretely: with `plugins: { enabled: false }` or `plugins: { allow: ["openai"] }` in config, install-catalog contributions are correctly hidden (existing tests at lines 247–299 verify this), but the equivalent manifest contributions are not. `resolveManifestProviderAuthChoiceCandidates` only gates workspace-origin, untrusted plugins — bundled and global plugins bypass that check entirely.

The fix mirrors what the install-catalog path does: compute `normalizedPluginsConfig` at the top of the function and add a `.filter` that calls `resolveEffectiveEnableState` with `enabledByDefault: true` after the scope filter.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "feat(plugins): surface manifest provider..." | Re-trigger Greptile

Comment on lines +125 to +168
function resolveManifestProviderSetupFlowContributions(params?: {
config?: OpenClawConfig;
workspaceDir?: string;
env?: NodeJS.ProcessEnv;
scope?: ProviderFlowScope;
}): ProviderSetupFlowContribution[] {
const scope = params?.scope ?? DEFAULT_PROVIDER_FLOW_SCOPE;
return resolveManifestProviderAuthChoices({
...params,
includeUntrustedWorkspacePlugins: false,
})
.filter((choice) => includesProviderFlowScope(choice.onboardingScopes, scope))
.map((choice) => {
const groupId = choice.groupId ?? choice.providerId;
const groupLabel = choice.groupLabel ?? choice.choiceLabel;
return Object.assign(
{
id: `provider:setup:${choice.choiceId}`,
kind: `provider` as const,
surface: `setup` as const,
providerId: choice.providerId,
pluginId: choice.pluginId,
option: {
value: choice.choiceId,
label: choice.choiceLabel,
...(choice.choiceHint ? { hint: choice.choiceHint } : {}),
...(choice.assistantPriority !== undefined
? { assistantPriority: choice.assistantPriority }
: {}),
...(choice.assistantVisibility
? { assistantVisibility: choice.assistantVisibility }
: {}),
group: {
id: groupId,
label: groupLabel,
...(choice.groupHint ? { hint: choice.groupHint } : {}),
},
},
},
choice.onboardingScopes ? { onboardingScopes: [...choice.onboardingScopes] } : {},
{ source: `manifest` as const },
);
});
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Missing enable-state gate for manifest contributions

resolveManifestProviderSetupFlowContributions never calls resolveEffectiveEnableState, so manifest choices from explicitly-disabled or non-allowlisted plugins will still appear in the setup flow. By contrast, resolveInstallCatalogProviderSetupFlowContributions filters every entry through resolveEffectiveEnableState({ ..., enabledByDefault: true }).

Concretely: with plugins: { enabled: false } or plugins: { allow: ["openai"] } in config, install-catalog contributions are correctly hidden (existing tests at lines 247–299 verify this), but the equivalent manifest contributions are not. resolveManifestProviderAuthChoiceCandidates only gates workspace-origin, untrusted plugins — bundled and global plugins bypass that check entirely.

The fix mirrors what the install-catalog path does: compute normalizedPluginsConfig at the top of the function and add a .filter that calls resolveEffectiveEnableState with enabledByDefault: true after the scope filter.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/flows/provider-flow.ts
Line: 125-168

Comment:
**Missing enable-state gate for manifest contributions**

`resolveManifestProviderSetupFlowContributions` never calls `resolveEffectiveEnableState`, so manifest choices from explicitly-disabled or non-allowlisted plugins will still appear in the setup flow. By contrast, `resolveInstallCatalogProviderSetupFlowContributions` filters every entry through `resolveEffectiveEnableState({ ..., enabledByDefault: true })`.

Concretely: with `plugins: { enabled: false }` or `plugins: { allow: ["openai"] }` in config, install-catalog contributions are correctly hidden (existing tests at lines 247–299 verify this), but the equivalent manifest contributions are not. `resolveManifestProviderAuthChoiceCandidates` only gates workspace-origin, untrusted plugins — bundled and global plugins bypass that check entirely.

The fix mirrors what the install-catalog path does: compute `normalizedPluginsConfig` at the top of the function and add a `.filter` that calls `resolveEffectiveEnableState` with `enabledByDefault: true` after the scope filter.

How can I resolve this? If you propose a fix, please make it concise.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bc40d517c7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +132 to +136
return resolveManifestProviderAuthChoices({
...params,
includeUntrustedWorkspacePlugins: false,
})
.filter((choice) => includesProviderFlowScope(choice.onboardingScopes, scope))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Respect plugin enable policy for manifest setup choices

Manifest-derived setup options are added with only a scope filter, so they bypass the enable/allowlist gating that install-catalog options use. In practice this makes resolveProviderSetupFlowContributions surface blocked providers (for example with plugins.enabled=false or a narrow plugins.allow list), and those options later fail when selection tries to enable the plugin. Please apply the same effective-enable policy to manifest choices before adding them (and before using them to suppress runtime/install entries).

Useful? React with 👍 / 👎.

Angfr95 pushed a commit to Angfr95/openclaw that referenced this pull request Apr 25, 2026
GONZO304 pushed a commit to GONZO304/openclaw that referenced this pull request Apr 28, 2026
ogt-redknie pushed a commit to ogt-redknie/OPENX that referenced this pull request May 2, 2026
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 9, 2026
globalcaos pushed a commit to globalcaos/tinkerclaw that referenced this pull request May 13, 2026
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 24, 2026
jameslcowan pushed a commit to jameslcowan/openclaw that referenced this pull request Jun 2, 2026
sablehead pushed a commit to sablehead/openclaw that referenced this pull request Jun 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs Improvements or additions to documentation maintainer Maintainer-authored PR size: S

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant