Plugins: keep untrusted workspace channel shadows out of setup-time loads#64154
Plugins: keep untrusted workspace channel shadows out of setup-time loads#64154hxy91819 wants to merge 8 commits into
Conversation
Greptile SummaryThis PR completes a trust-aware catalog resolution fix for Confidence Score: 5/5Safe to merge — the changes correctly fix a trust-bypass in 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 |
b3cd093 to
6e1d53a
Compare
|
@codex review |
|
Codex Review: Didn't find any major issues. Already looking forward to the next diff. ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
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". |
There was a problem hiding this comment.
💡 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".
| const allowSetupOnlyChannelRegistration = | ||
| candidate.origin !== "workspace" && !validateOnly && onlyPluginIdSet | ||
| ? manifestRecord.channels.length > 0 |
There was a problem hiding this comment.
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 👍 / 👎.
bb1630f to
1a09bd7
Compare
There was a problem hiding this comment.
💡 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".
| const catalogEntries = cfg | ||
| ? listSetupDiscoveryChannelPluginCatalogEntries({ | ||
| cfg, | ||
| workspaceDir, | ||
| env: process.env, |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
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 addcatalog 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). |
| candidate.origin !== "workspace" && !validateOnly && onlyPluginIdSet | ||
| ? manifestRecord.channels.length > 0 | ||
| : false; |
There was a problem hiding this comment.
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.
| candidate.origin !== "workspace" && !validateOnly && onlyPluginIdSet | |
| ? manifestRecord.channels.length > 0 | |
| : false; | |
| candidate.origin !== "workspace" && | |
| !validateOnly && | |
| onlyPluginIdSet && | |
| manifestRecord.channels.length > 0; |
1a09bd7 to
03e495a
Compare
…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
03e495a to
3ab1220
Compare
There was a problem hiding this comment.
💡 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".
| vi.mock("../../channels/plugins/catalog.js", () => ({ | ||
| listChannelPluginCatalogEntries: (opts?: unknown) => listChannelPluginCatalogEntries(opts), | ||
| getChannelPluginCatalogEntry: (id?: unknown, opts?: unknown) => | ||
| getChannelPluginCatalogEntry(id, opts), | ||
| })); |
There was a problem hiding this comment.
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 👍 / 👎.
| catalogMocks.listChannelPluginCatalogEntries.mockImplementation( | ||
| (opts?: { excludeWorkspace?: boolean }) => | ||
| opts?.excludeWorkspace ? [bundledEntry] : [workspaceShadow], | ||
| ); |
There was a problem hiding this comment.
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 👍 / 👎.
|
Superseded by #82579. I rebased the still-relevant loader/registry/test fixes onto current |
Summary
channels addthrough the same trust-aware catalog discovery used by setup flowslistChannelPluginCatalogEntriesas@deprecatedand migrate internal callers tolistChannelPluginCatalogEntriesUnfiltered(@internal)Why
The original fix aligned
channels addwith 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:#28a745The 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:onboard,setup, and nowchannels addall 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 anyplugin-sdkentrypoint and third-party plugins cannot import it. To prevent internal misuse:listChannelPluginCatalogEntriesis now@deprecated— IDE shows strikethrough, guiding developers to the safe alternativeslistChannelPluginCatalogEntriesUnfiltered(@internal) — extracted for use bytrusted-catalog.tsinternals without triggering deprecation warningslistTrustedChannelPluginCatalogEntries— the recommended entry point for all execution-facing pathslistSetupDiscoveryChannelPluginCatalogEntries— documented as UI-only, with explicit warning against execution useTests
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.tspnpm test src/commands/channel-setup/trusted-catalog.test.tsNotes
pnpm tsgoin this worktree still hits unrelated existing failures outside the touched surface.