fix(plugins): block untrusted workspace setup-only channel loads#82579
fix(plugins): block untrusted workspace setup-only channel loads#82579hxy91819 wants to merge 9 commits into
Conversation
|
Codex review: found issues before merge. Reviewed May 26, 2026, 11:35 AM ET / 15:35 UTC. Summary PR surface: Source +46, Tests +558, Docs +1. Total +605 across 20 files. Reproducibility: yes. source-reproducible: current main has a focused setup-only disabled-channel test, and the PR branch's Review metrics: 1 noteworthy metric.
Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Rank-up moves:
Risk before merge
Maintainer options:
Next step before merge Security Review findings
Review detailsBest possible solution: Keep the untrusted workspace hardening and the setup-entry error isolation, but narrow the loader and registry checks so they reject only untrusted workspace candidates while preserving current scoped setup-only behavior for trusted disabled plugins; remove the direct changelog entry and keep release-note context in the PR body or squash message. Do we have a high-confidence way to reproduce the issue? Yes, source-reproducible: current main has a focused setup-only disabled-channel test, and the PR branch's Is this the best way to solve the issue? No, not yet. The Full review comments:
Overall correctness: patch is incorrect AGENTS.md: found and applied where relevant. Codex review notes: model gpt-5.5, reasoning high; reviewed against e96cde7e1493. Label changesLabel changes:
Label justifications:
Evidence reviewedPR surface: Source +46, Tests +558, Docs +1. Total +605 across 20 files. View PR surface stats
What I checked:
Likely related people:
What the crustacean ranks mean
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics. How this review workflow works
|
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e6b62d7f8b
ℹ️ 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".
| (!enableState.enabled || forceSetupOnlyChannelPlugins); | ||
| const canLoadScopedSetupOnlyChannelPlugin = | ||
| scopedSetupOnlyChannelPluginRequested && | ||
| (candidate.origin !== "workspace" || enableState.enabled) && |
There was a problem hiding this comment.
Limit workspace setup-only gate to untrusted disable reason
This new predicate blocks all disabled workspace plugins from the scoped setup-only path, but that path is explicitly for loading disabled channel setup surfaces (scopedSetupOnlyChannelPluginRequested is driven by !enableState.enabled || forceSetupOnlyChannelPlugins). As written, a trusted workspace plugin that is disabled via config (for example plugins.entries.<id>.enabled=false) can no longer load its setup entry when explicitly scoped with onlyPluginIds, which regresses disabled-channel setup/inspection behavior; the condition should target only the untrusted workspace-default case instead of every disabled reason.
Useful? React with 👍 / 👎.
| return; | ||
| } | ||
| const id = plugin.id; | ||
| if (record.origin === "workspace" && !record.enabled) { |
There was a problem hiding this comment.
Keep trusted disabled workspace setup registrations allowed
This guard rejects channel registration for any disabled workspace record, regardless of why it is disabled. That means even trusted workspace plugins intentionally disabled in config cannot register setup-time channels when a caller explicitly requests setup-only loading, so channelSetups becomes empty for those plugins and setup/status flows lose expected metadata. The backstop should be keyed to untrusted workspace disablement, not blanket !record.enabled for workspace origin.
Useful? React with 👍 / 👎.
|
Cross-referencing from #85774 (now closed as superseded by this PR): #85774 added a try/catch around This PR addresses the trust policy side (blocking untrusted workspace loads), but the error resilience gap for trusted plugins remains. It may be worth including the try/catch + |
Thanks, I will take this. |
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Port the registerChannel guard from #85774 so a permitted but broken setup-entry channel reports a diagnostic without aborting sibling plugin loads. Co-authored-by: Sebastien Tardif <sebtardif@ncf.ca> Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
|
ClawSweeper PR egg 🔥 Warming up: real-behavior proof passed; findings, security review, or rank-up moves are still in progress. Hatch commandComment Hatchability rules:
What is this egg doing here?
|
|
/clawsweeper re-review |
|
🦞👀 Command router queued. I will update this comment with the next step. |
|
🦞🧹 I asked ClawSweeper to review this item again. Re-review progress:
|
|
Closing this PR in favor of a replacement PR from the same branch and commit history so the repository automation can process a fresh opened PR event. The replacement will be linked from this thread. |
Summary
mainalready keeps execution-facing channel setup discovery on the trusted catalog path, but lower-level setup-only scoped plugin loads could still import and execute disabled workspace channel plugins when callers passed explicitonlyPluginIds.mainalready kept from Plugins: keep untrusted workspace channel shadows out of setup-time loads #64154:channels add, onboarding/setup resolution, and setup discovery now use trust-aware catalog helpers; untrusted workspace shadows fall back to bundled/non-workspace entries; workspace-only candidates remain visible only in the setup wizard discovery bucket; the raw catalog listing is kept behind an internal/deprecated helper.Change Type (select all)
Scope (select all touched areas)
Linked Issue/PR
Current Main vs This PR
The #64154 design had two layers:
Latest
mainstill preserves the first layer.channels add, setup/onboarding, and channel setup plugin resolution uselistTrustedChannelPluginCatalogEntries/getTrustedChannelPluginCatalogEntry; the setup wizard's installable display can still show workspace-only candidates through the lenient discovery helper without treating them as executable/installed.This PR fills the second layer. It prevents explicit setup-only scoped loads from importing disabled workspace channel plugins, rejects disabled workspace channel registrations at the registry boundary, and keeps trusted workspace overrides working. It also ports #85774's try/catch around
api.registerChannel(mergedSetupPlugin), which addresses a related but distinct resilience gap: trusted setup-entry plugins are allowed to load, but one malformed registration should not stop healthy sibling plugins from loading.Diagram
graph TD SOURCES["Plugin source directories"] --> MANIFEST["Manifest registry<br/>(pluginId + origin + channels)"] MANIFEST -->|"setup / onboard / channels add"| TRUSTED["Trusted catalog helpers<br/>strict execution list<br/>untrusted workspace shadows fall back"] MANIFEST -->|"setup wizard display"| DISCOVERY["Setup discovery helper<br/>lenient display list<br/>workspace-only candidates visible"] MANIFEST -->|"gateway or explicit scoped setup load"| LOADER["loadOpenClawPlugins()"] TRUSTED --> SETUP["setup/add snapshot request"] DISCOVERY --> UI["installable UI bucket only<br/>not an execution decision"] SETUP --> LOADER LOADER --> GATE{"This PR:<br/>workspace channel plugin enabled?"} GATE -->|"disabled workspace"| BLOCK["reject before setup-only import<br/>no plugin code executes"] GATE -->|"trusted workspace or non-workspace"| IMPORT["import setup/runtime entry"] IMPORT --> REGISTER{"This PR + #85774:<br/>registerChannel succeeds?"} REGISTER -->|"yes"| REGISTRY["snapshot/runtime registry"] REGISTER -->|"throws"| DIAG["record plugin diagnostic<br/>continue loading siblings"] REGISTRY --> SNAPUSE["validateInput / applyAccountConfig<br/>write config / lifecycle callbacks"] REGISTRY --> RUNTIME["gateway/runtime channel lookup"] BLOCK:::fixed GATE:::fixed DIAG:::fixed TRUSTED:::kept DISCOVERY:::display UI:::display classDef fixed fill:#d4edda,stroke:#28a745,stroke-width:2px classDef kept fill:#d9edf7,stroke:#31708f,stroke-width:2px classDef display fill:#fff3cd,stroke:#ffc107,stroke-width:2pxThe important boundary is now shared: catalog helpers decide which channel catalog entries are trusted for setup/add flows, and the loader/registry enforce the same rule before runtime code can execute. If a trusted setup-entry channel still fails during registration, the error is isolated to that plugin and sibling plugins continue loading.
Real behavior proof (required for external PRs)
mainafter rebasing this PR.OPENCLAW_VITEST_MAX_WORKERS=1 pnpm test src/plugins/loader.test.ts -- -t "blocks untrusted setup-only workspace channel plugins|keeps trusted setup-only workspace channel plugins available|records a diagnostic when registerChannel throws in the setup-entry path|keeps healthy sibling channel plugins loadable when a setup entry throws" --reporter=verboseOPENCLAW_VITEST_MAX_WORKERS=1 pnpm test src/plugins/registry.channel-guard.test.ts src/commands/channel-setup/trusted-catalog.test.ts src/commands/channels.add.test.ts src/commands/channel-setup/channel-plugin-resolution.test.ts src/commands/channel-setup/discovery.test.ts src/commands/channel-setup/workspace-shadow-bypass.test.ts src/commands/channel-setup/plugin-install.test.ts -- --reporter=verbosepnpm check:changedpnpm build.agents/skills/autoreview/scripts/autoreview --mode branch --base origin/mainregisterChannelerror isolation; the registry/catalog/channel setup tests pass across 65 channel setup cases;check:changed,build, and autoreview are clean.origin/mainbefore this branch because the marker file was created during scoped setup-only load.Root Cause (if applicable)
onlyPluginIdsas sufficient to load a disabled workspace channel plugin, so workspace trust was bypassed below the trusted-catalog call sites.api.registerChannel(mergedSetupPlugin)itself was unguarded, so one throwing trusted channel registration could abort sibling plugin loading.mainalready absorbed the higher-levelchannels add/ trusted-catalog call-site fixes from Plugins: keep untrusted workspace channel shadows out of setup-time loads #64154, but not this lower-level loader/registry backstop or the setup-entry error-isolation guard.Regression Test Plan (if applicable)
src/plugins/loader.test.ts,src/plugins/registry.channel-guard.test.ts,src/commands/channel-setup/trusted-catalog.test.ts, and existing channel setup/add tests.only setup-loads a disabled channel plugin when the caller scopes to the selected pluginstill covers the non-workspace path.User-visible / Behavior Changes
Security Impact (required)
Yes/No) NoYes/No) NoYes/No) NoYes/No) NoYes/No) NoYes, explain risk + mitigation: N/ARepro + Verification
Environment
Steps
channelsmetadata and a side-effect marker write inregister().loadOpenClawPlugins({ includeSetupOnlyChannelPlugins: true, forceSetupOnlyChannelPlugins: true, onlyPluginIds: [...] }).registerChannelalongside a healthy sibling plugin.Expected
Actual
Evidence
Human Verification (required)
channels addworkspace shadow fallback, fix(plugins): guard registerChannel in setup-entry path with try/catch #85774 setup-entryregisterChannelerror isolation,pnpm check:changed,pnpm build, autoreview.Review Conversations
Compatibility / Migration
Yes/No) YesYes/No) NoYes/No) NoRisks and Mitigations
Proof 1: Pre-Fix Reproduction on origin/main
git fetch --depth=1 origin main && git checkout FETCH_HEADinside Crabbox container; (2)pnpm install --frozen-lockfile && pnpm build; (3) Deploy evi1-channel-probe plugin to /tmp/proof1-workspace/.openclaw/extensions/evi1-channel-probe/; (4) Confirm /tmp/openclaw-marker-evidence absent; (5) Runnode probe-untrusted.mjswith onlyPluginIds=["evi1-channel-probe"], includeSetupOnlyChannelPlugins=true, activate=false, cache=false; (6) Check /tmp/openclaw-marker-evidence existence.Proof 2: Post-Fix Verification on PR Head
git fetch origin pull/82579/head:pr-head && git checkout pr-headinside Crabbox container; (2)pnpm install --frozen-lockfile && pnpm build; (3)grep -r "channel registration rejected for disabled workspace plugin" dist/— fix code found in dist/loader-DM-dphfh.js; (4) Deploy evi1-channel-probe (untrusted), confirm marker ABSENT after probe; (5) Deploy evi1-trusted-probe (with plugins.allow), confirm marker EXISTS; (6) Deploy evi1-broken-register + evi1-sibling-probe (with plugins.allow), confirm sibling marker EXISTS and diagnostic recorded; (7) Run focused tests:pnpm test src/plugins/loader.test.ts(142 pass),pnpm test src/plugins/registry.channel-guard.test.ts(2 pass),pnpm test src/commands/channel-setup/trusted-catalog.test.ts(5 pass),pnpm test src/commands/channel-setup/workspace-shadow-bypass.test.ts(6 pass); (8)pnpm check:changed— 0 errors.Crabbox cross-reference: Proof 1 provider=docker, container=391883b7652bcb464c523f64ddcabb9bcad47f92e434737b7cc5391f7bc0922f, run=cbx_6dadb5d505eb, SHA=ead847f6065ac36031f9e7a95dd1ee90f171f79c (origin/main). Proof 2 provider=docker, container=df29879e2faef7ac8794cba519c1988aff410316a3718d90c6f638cd9977ce15, lease=cbx_0f687f5da081, SHA=b05a7b14fe9bd6c119b3b7fe60a1967d0987804c (PR #82579 head). Proof 1 SHA (ead847f) is an ancestor of Proof 2 SHA (b05a7b1) via the main branch lineage. Probe infrastructure file SHA-256 hashes are identical between both proofs (VAL-CROSS-01 confirmed).