Skip to content

Plugins: keep untrusted workspace channel shadows out of setup-time loads#64154

Closed
hxy91819 wants to merge 8 commits into
openclaw:mainfrom
hxy91819:fix/channels-add-shadow-bypass
Closed

Plugins: keep untrusted workspace channel shadows out of setup-time loads#64154
hxy91819 wants to merge 8 commits into
openclaw:mainfrom
hxy91819:fix/channels-add-shadow-bypass

Conversation

@hxy91819

@hxy91819 hxy91819 commented Apr 10, 2026

Copy link
Copy Markdown
Member

Summary

  • route channels add through the same trust-aware catalog discovery used by setup flows
  • block disabled workspace channel plugins from entering setup-only loader and registry paths, even when explicitly scoped
  • fix loader setup-only gate to allow trusted workspace plugins (was over-blocking all workspace origins)
  • mark the unfiltered listChannelPluginCatalogEntries as @deprecated and migrate internal callers to listChannelPluginCatalogEntriesUnfiltered (@internal)
  • add regression coverage for both trusted-catalog resolution and untrusted workspace setup-only loads

Why

The original fix aligned channels add with the setup/catalog trust rules so the CLI no longer selected an untrusted workspace shadow when resolving a built-in channel id.

That still left a lower-level bypass: setup-only scoped loads could construct a temporary plugin registry and execute plugin code without going through the catalog trust filter first. In practice that meant an untrusted workspace channel plugin could still be imported and registered during setup-time flows if it reached the loader directly.

The hardening here keeps the catalog fix, but moves the security boundary down to the shared setup-time loader/registry entry so downstream callers do not need to each remember to re-apply the same trust rule.

graph TD
    SOURCES["Plugin source directories"] --> MANIFEST["Manifest Registry<br/>(pluginId + origin + channels)"]

    MANIFEST -->|"setup/add"| TRUSTED["listTrustedChannelPluginCatalogEntries<br/>✅ strict: untrusted workspace-only dropped"]
    MANIFEST -->|"setup/add"| DISCOVERY["listSetupDiscoveryChannelPluginCatalogEntries<br/>⚠️ lenient: untrusted workspace-only kept"]
    MANIFEST -->|"gateway"| GW["Gateway startup selection<br/>no catalog trust filter"]

    TRUSTED --> ONBOARD["onboard / setup"]
    TRUSTED --> ADD["channels add<br/>✅ now uses strict list"]
    DISCOVERY --> SETUP_UI["setup wizard installable list<br/>(UI discovery only)"]
    ONBOARD --> LOAD
    ADD --> LOAD
    GW --> LOAD

    LOAD["loadOpenClawPlugins()<br/>imports plugin module<br/>executes register(...)<br/><br/>trust enforcement now lives here<br/>✅ gate based on enableState<br/>(trusted workspace → allowed)"]
    LOAD --> SNAP["Snapshot Registry<br/>(temporary setup/add registry)"]
    LOAD --> ACTIVE["Active Runtime Registry<br/>(gateway global registry)"]
    SNAP --> SNAPUSE["validateInput()<br/>applyAccountConfig()<br/>write config<br/>setup/lifecycle callbacks"]

    style TRUSTED fill:#d4edda,stroke:#28a745
    style ADD fill:#d4edda,stroke:#28a745
    style DISCOVERY fill:#fff3cd,stroke:#ffc107,stroke-width:2px
    style SETUP_UI fill:#eeeeee,stroke:#999999
    style LOAD fill:#d4edda,stroke:#28a745,stroke-width:3px
    style SNAP fill:#d9edf7,stroke:#31708f
    style SNAPUSE fill:#eeeeee,stroke:#999999
    style ACTIVE fill:#d4edda,stroke:#28a745
Loading

The key point is that both the gateway path and the setup/add snapshot path pass through loadOpenClawPlugins() before they can construct runtime-visible registry content. Blocking untrusted workspace channel shadows there keeps them out of both:

  • the active runtime registry used by gateway/runtime lookups
  • the temporary snapshot registry used by setup/add flows

onboard, setup, and now channels add all use the strict trusted catalog list (which drops untrusted workspace-only entries). The lenient discovery list is only used for the setup wizard's "installable" display bucket. The loader/registry enforcement is the lower-level backstop that prevents setup-time code execution even if a caller somehow bypasses the catalog layer.

Catalog API hardening

listChannelPluginCatalogEntries (the unfiltered raw listing) is not part of the plugin SDK — it is not exported through any plugin-sdk entrypoint and third-party plugins cannot import it. To prevent internal misuse:

  • listChannelPluginCatalogEntries is now @deprecated — IDE shows strikethrough, guiding developers to the safe alternatives
  • listChannelPluginCatalogEntriesUnfiltered (@internal) — extracted for use by trusted-catalog.ts internals without triggering deprecation warnings
  • listTrustedChannelPluginCatalogEntries — the recommended entry point for all execution-facing paths
  • listSetupDiscoveryChannelPluginCatalogEntries — documented as UI-only, with explicit warning against execution use

Tests

  • pnpm test src/plugins/loader.test.ts -t "does not setup-load untrusted workspace channel plugins even when explicitly scoped"
  • pnpm test src/commands/channel-setup/channel-plugin-resolution.test.ts
  • pnpm test src/commands/channel-setup/trusted-catalog.test.ts

Notes

  • pnpm tsgo in this worktree still hits unrelated existing failures outside the touched surface.

@openclaw-barnacle openclaw-barnacle Bot added commands Command implementations size: M maintainer Maintainer-authored PR labels Apr 10, 2026
@greptile-apps

greptile-apps Bot commented Apr 10, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR completes a trust-aware catalog resolution fix for channels add, aligning its non-wizard path with setup/onboarding flows that already used trust-aware discovery. It adds listSetupDiscoveryChannelPluginCatalogEntries as the resolution source in resolveCatalogChannelEntry, falling back to the bundled entry when an untrusted workspace plugin shadows a built-in channel ID. Two additional hardening layers are included: a loader-level block preventing workspace plugins from entering setup-only registration mode even when explicitly scoped via onlyPluginIds, and a registry-level guard rejecting channel registration from disabled workspace plugins as defense-in-depth.

Confidence Score: 5/5

Safe to merge — the changes correctly fix a trust-bypass in channels add, are well-tested, and include two complementary defense-in-depth layers in loader and registry.

All findings are informational. The security fix is logically sound: trusted-and-enabled workspace plugins continue to work (they route through the enableState.enabled=true branch in loader), while untrusted workspace shadows of built-in channels are correctly replaced with bundled entries at both the catalog-resolution and module-load layers. Test coverage is thorough across unit, integration, and filesystem-marker levels.

No files require special attention.

Reviews (2): Last reviewed commit: "Plugins: block untrusted workspace setup..." | Re-trigger Greptile

@hxy91819 hxy91819 force-pushed the fix/channels-add-shadow-bypass branch from b3cd093 to 6e1d53a Compare April 11, 2026 15:15
@hxy91819

Copy link
Copy Markdown
Member Author

@codex review
@greptile-apps review

@hxy91819 hxy91819 changed the title CLI: align channels add catalog resolution with setup trust rules Plugins: keep untrusted workspace channel shadows out of setup-time loads Apr 11, 2026
@hxy91819 hxy91819 requested a review from vincentkoc April 11, 2026 15:23
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Already looking forward to the next diff.

ℹ️ 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".

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

ℹ️ 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 thread src/plugins/loader.ts
Comment on lines +1392 to +1394
const allowSetupOnlyChannelRegistration =
candidate.origin !== "workspace" && !validateOnly && onlyPluginIdSet
? manifestRecord.channels.length > 0

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep scoped workspace setup plugins loadable for add flows

This new origin gate blocks setup-only registration for all disabled workspace plugins, not just channel shadows, so loadChannelSetupPluginRegistrySnapshotForChannel() can no longer return a setup plugin when channels add scopes to an installed-but-not-allowlisted workspace channel plugin. In that case channels add still resolves the catalog entry, skips reinstall when the manifest is present, then fails with Channel <id> does not support add because the scoped snapshot is empty; previously the setup-only path allowed onboarding to proceed. This regresses setup for preinstalled workspace channel plugins unless users manually edit trust config first.

Useful? React with 👍 / 👎.

@hxy91819 hxy91819 force-pushed the fix/channels-add-shadow-bypass branch from bb1630f to 1a09bd7 Compare April 12, 2026 03:44

@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: 1a09bd711b

ℹ️ 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 +39 to +43
const catalogEntries = cfg
? listSetupDiscoveryChannelPluginCatalogEntries({
cfg,
workspaceDir,
env: process.env,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Don’t treat untrusted workspace catalog entries as add targets

Routing channels add through listSetupDiscoveryChannelPluginCatalogEntries lets workspace-only (not allowlisted) channel entries remain selectable here. When that plugin is already present in manifests, the command can skip install/enable and then fail at load time because this same commit blocks setup-only loading for disabled workspace plugins, producing Channel <id> does not support add for installed-but-untrusted workspace channels. Fresh evidence in this patch is that the new discovery helper is now used directly in channels add, so this failure path is reachable without any caller bypassing trust checks.

Useful? React with 👍 / 👎.

@hxy91819 hxy91819 requested review from Copilot and removed request for vincentkoc April 12, 2026 04:03

Copilot AI left a comment

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.

Pull request overview

This PR hardens setup-time channel plugin loading so untrusted workspace “shadow” plugins cannot be imported/executed during setup/add flows, while preserving trusted workspace overrides and bundled fallback behavior.

Changes:

  • Enforce “disabled workspace plugins can’t register channels” at the plugin registry boundary during setup-time loads.
  • Restrict setup-only scoped loads to avoid importing disabled workspace channel plugins, even when explicitly scoped.
  • Route channels add catalog resolution through trust-aware setup discovery and add regression tests covering the trust/fallback rules.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
src/plugins/registry.ts Reject channel registrations coming from disabled workspace plugins (setup-time backstop).
src/plugins/loader.ts Prevent setup-only registration mode for disabled workspace channel plugins when explicitly scoped.
src/plugins/loader.test.ts Add coverage ensuring untrusted workspace shadows aren’t imported during setup-only or gateway-style loads, while trusted workspace channels still load.
src/commands/channels/add.ts Use trust-aware setup discovery catalog when resolving non-normalized channel inputs.
src/commands/channels.add.test.ts Add tests asserting bundled fallback for untrusted shadows and acceptance of trusted workspace overrides in channels add.
src/commands/channel-setup/trusted-catalog.test.ts Add focused unit tests for trusted-catalog fallback/trust behavior (including auto-enable trust).

Comment thread src/plugins/loader.ts Outdated
Comment on lines +1384 to +1386
candidate.origin !== "workspace" && !validateOnly && onlyPluginIdSet
? manifestRecord.channels.length > 0
: false;

Copilot AI Apr 12, 2026

Copy link

Choose a reason for hiding this comment

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

The allowSetupOnlyChannelRegistration boolean is computed via a ternary that always returns false when the guard condition fails. This can be simplified to a single boolean expression (e.g., candidate.origin !== "workspace" && !validateOnly && onlyPluginIdSet && manifestRecord.channels.length > 0) to reduce cognitive overhead and avoid accidental precedence mistakes during future edits.

Suggested change
candidate.origin !== "workspace" && !validateOnly && onlyPluginIdSet
? manifestRecord.channels.length > 0
: false;
candidate.origin !== "workspace" &&
!validateOnly &&
onlyPluginIdSet &&
manifestRecord.channels.length > 0;

Copilot uses AI. Check for mistakes.
…space setup-only

- channels add: switch from listSetupDiscoveryChannelPluginCatalogEntries
  to listTrustedChannelPluginCatalogEntries so untrusted workspace-only
  entries are rejected at target resolution instead of failing at loader

- loader: change allowSetupOnlyChannelRegistration gate from
  origin !== 'workspace' (which blocked all workspace plugins) to
  origin === 'workspace' && !enableState.enabled (only blocks untrusted),
  so trusted workspace channel plugins can still register in setup-only mode
…dance

- listChannelPluginCatalogEntries: @deprecated, point to trusted variant
- listSetupDiscoveryChannelPluginCatalogEntries: doc as UI-only, warn
  against use in execution paths
- listTrustedChannelPluginCatalogEntries: doc as recommended entry point
@hxy91819 hxy91819 force-pushed the fix/channels-add-shadow-bypass branch from 03e495a to 3ab1220 Compare April 15, 2026 16:33

@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: 3ab1220219

ℹ️ 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 +18 to +22
vi.mock("../../channels/plugins/catalog.js", () => ({
listChannelPluginCatalogEntries: (opts?: unknown) => listChannelPluginCatalogEntries(opts),
getChannelPluginCatalogEntry: (id?: unknown, opts?: unknown) =>
getChannelPluginCatalogEntry(id, opts),
}));

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 Export unfiltered catalog mock in trusted-catalog tests

trusted-catalog.ts now calls listChannelPluginCatalogEntriesUnfiltered, but this module mock only returns listChannelPluginCatalogEntries and getChannelPluginCatalogEntry. When listTrustedChannelPluginCatalogEntries/listSetupDiscoveryChannelPluginCatalogEntries execute, Vitest will hit a missing export (or undefined call) and the test fails before assertions run, so this regression suite is currently broken.

Useful? React with 👍 / 👎.

Comment on lines +471 to +474
catalogMocks.listChannelPluginCatalogEntries.mockImplementation(
(opts?: { excludeWorkspace?: boolean }) =>
opts?.excludeWorkspace ? [bundledEntry] : [workspaceShadow],
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Mock the trusted catalog source in new channels-add tests

These new cases stub catalogMocks.listChannelPluginCatalogEntries, but channels/add.ts no longer uses that path for configured runs; it resolves through listTrustedChannelPluginCatalogEntries, which reads listChannelPluginCatalogEntriesUnfiltered. That means this setup does not control the catalog entries used by the command, so the new workspace-shadow assertions can pass without actually exercising the intended trust/fallback branch.

Useful? React with 👍 / 👎.

@hxy91819

Copy link
Copy Markdown
Member Author

Superseded by #82579.

I rebased the still-relevant loader/registry/test fixes onto current main there and left out the older channels add / trusted-catalog diffs because those pieces are already on main.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

commands Command implementations maintainer Maintainer-authored PR size: L

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants