fix(plugins): block untrusted workspace setup-only channel loads#86953
Conversation
|
Codex review: passed. Reviewed June 1, 2026, 5:24 AM ET / 09:24 UTC. Summary PR surface: Source +46, Tests +610, Docs +13. Total +669 across 22 files. Reproducibility: yes. source inspection gives a high-confidence reproduction path: current main's setup-only scoped loader gate lacks a workspace-enabled check, so an explicitly scoped disabled workspace channel plugin can be imported before this PR. I did not run the repro locally in this read-only review. 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 detailsBest possible solution: Land the loader and registry backstops with the docs/tests intact once maintainers accept the fail-closed workspace-plugin policy and exact-head checks pass. Do we have a high-confidence way to reproduce the issue? Yes, source inspection gives a high-confidence reproduction path: current main's setup-only scoped loader gate lacks a workspace-enabled check, so an explicitly scoped disabled workspace channel plugin can be imported before this PR. I did not run the repro locally in this read-only review. Is this the best way to solve the issue? Yes, this is the best fix shape for the reported boundary: enforce before import in the loader and add a registry backstop, while keeping trusted workspace overrides and documenting the operator path. The only remaining decision is whether maintainers accept the intentional fail-closed upgrade behavior. AGENTS.md: found and applied where relevant. Codex review notes: model gpt-5.5, reasoning high; reviewed against 2bac970abc8e. Label changesLabel justifications:
Evidence reviewedPR surface: Source +46, Tests +610, Docs +13. Total +669 across 22 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
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b05a7b14fe
ℹ️ 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".
|
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 |
|
🦞🧹 I asked ClawSweeper to review this item again. Re-review progress:
|
|
@clawsweeper autofix |
|
🦞🔧
This is fix-only; I will not merge this PR. |
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>
7a7bc3b to
11438bc
Compare
…nclaw#86953) Summary: - This PR blocks disabled workspace-origin channel plugins from setup-only scoped imports, rejects their channel registrations at registry assembly, documents the trust rule, and adds regression coverage. - PR surface: Source +46, Tests +610, Docs +13. Total +669 across 22 files. - Reproducibility: yes. source inspection gives a high-confidence reproduction path: current main's setup-only ... ce channel plugin can be imported before this PR. I did not run the repro locally in this read-only review. Automerge notes: - PR branch already contained follow-up commit before automerge: test(plugins): cover workspace channel registry guard - PR branch already contained follow-up commit before automerge: fix(plugins): isolate setup channel registration errors - PR branch already contained follow-up commit before automerge: fix(channels): mark raw catalog listing internal - PR branch already contained follow-up commit before automerge: test(channels): cover trusted catalog filtering - PR branch already contained follow-up commit before automerge: test(channels): mock raw catalog helper - PR branch already contained follow-up commit before automerge: docs(changelog): credit setup channel hardening Validation: - ClawSweeper review passed for head 11438bc. - Required merge gates passed before the squash merge. Prepared head SHA: 11438bc Review: openclaw#86953 (comment) Co-authored-by: masonxhuang <masonxhuang@tencent.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Mason Huang <masonxhuang@tencent.com> Co-authored-by: Sebastien Tardif <sebtardif@ncf.ca> Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com> Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com> Approved-by: hxy91819 Co-authored-by: hxy91819 <8814856+hxy91819@users.noreply.github.com>
…nclaw#86953) Summary: - This PR blocks disabled workspace-origin channel plugins from setup-only scoped imports, rejects their channel registrations at registry assembly, documents the trust rule, and adds regression coverage. - PR surface: Source +46, Tests +610, Docs +13. Total +669 across 22 files. - Reproducibility: yes. source inspection gives a high-confidence reproduction path: current main's setup-only ... ce channel plugin can be imported before this PR. I did not run the repro locally in this read-only review. Automerge notes: - PR branch already contained follow-up commit before automerge: test(plugins): cover workspace channel registry guard - PR branch already contained follow-up commit before automerge: fix(plugins): isolate setup channel registration errors - PR branch already contained follow-up commit before automerge: fix(channels): mark raw catalog listing internal - PR branch already contained follow-up commit before automerge: test(channels): cover trusted catalog filtering - PR branch already contained follow-up commit before automerge: test(channels): mock raw catalog helper - PR branch already contained follow-up commit before automerge: docs(changelog): credit setup channel hardening Validation: - ClawSweeper review passed for head 11438bc. - Required merge gates passed before the squash merge. Prepared head SHA: 11438bc Review: openclaw#86953 (comment) Co-authored-by: masonxhuang <masonxhuang@tencent.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Mason Huang <masonxhuang@tencent.com> Co-authored-by: Sebastien Tardif <sebtardif@ncf.ca> Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com> Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com> Approved-by: hxy91819 Co-authored-by: hxy91819 <8814856+hxy91819@users.noreply.github.com>
…nclaw#86953) Summary: - This PR blocks disabled workspace-origin channel plugins from setup-only scoped imports, rejects their channel registrations at registry assembly, documents the trust rule, and adds regression coverage. - PR surface: Source +46, Tests +610, Docs +13. Total +669 across 22 files. - Reproducibility: yes. source inspection gives a high-confidence reproduction path: current main's setup-only ... ce channel plugin can be imported before this PR. I did not run the repro locally in this read-only review. Automerge notes: - PR branch already contained follow-up commit before automerge: test(plugins): cover workspace channel registry guard - PR branch already contained follow-up commit before automerge: fix(plugins): isolate setup channel registration errors - PR branch already contained follow-up commit before automerge: fix(channels): mark raw catalog listing internal - PR branch already contained follow-up commit before automerge: test(channels): cover trusted catalog filtering - PR branch already contained follow-up commit before automerge: test(channels): mock raw catalog helper - PR branch already contained follow-up commit before automerge: docs(changelog): credit setup channel hardening Validation: - ClawSweeper review passed for head 11438bc. - Required merge gates passed before the squash merge. Prepared head SHA: 11438bc Review: openclaw#86953 (comment) Co-authored-by: masonxhuang <masonxhuang@tencent.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Mason Huang <masonxhuang@tencent.com> Co-authored-by: Sebastien Tardif <sebtardif@ncf.ca> Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com> Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com> Approved-by: hxy91819 Co-authored-by: hxy91819 <8814856+hxy91819@users.noreply.github.com>
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)
upstream/main98b8e85bebdc5438e9ea5718af5749be0cc742fb; PR head9bfc1ca3c0e4e80fd5ae8a10d16a32b8dc4af972.pnpm docs:listpnpm test src/commands/onboard-channels.e2e.test.ts -- --reporter=verbosepnpm test src/plugins/loader.test.ts src/plugins/registry.channel-guard.test.ts src/commands/channel-setup/trusted-catalog.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 src/commands/channels.add.test.ts -- --reporter=verbosepnpm buildpnpm check:changedsrc/commands/onboard-channels.e2e.test.tspassed 16/16 after exposinglistRawChannelPluginCatalogEntriesthrough the same mock/actual fallback as the old catalog helper; the focused loader/registry/catalog/channel setup tests passed after the 2026-06-01 rebase;pnpm buildpassed on PR head9bfc1ca3c0e4e80fd5ae8a10d16a32b8dc4af972.pnpm check:changedfirst could not delegate because the local Crabbox binary is0.15.0and the Testbox wrapper requires>=0.22.0; the local child run reached typecheck and failed only in untouchedextensions/phone-control/index.test.tsextension-test mocks.CHANGELOG.mdand stale transcripts tool-display metadata are no longer in the final branch diff.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, andpnpm buildafter the 2026-06-01 rebase.pnpm check:changedhas a current local proof gap in untouchedextensions/phone-control/index.test.tsextension-test typecheck.Review Conversations
Compatibility / Migration
Yes/No) No. This is an intentional security hardening break for disabled workspace channel plugins that were previously reachable through setup-only scoped loads.Yes/No) NoYes/No) No automatic migration. Operators who intentionally develop or run a workspace/linked channel plugin must explicitly enable/trust that plugin before setup-time code can run.docs/cli/plugins.md.Risks 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
9bfc1ca3c0e4e80fd5ae8a10d16a32b8dc4af972)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 (#82579 head; superseded by #86953, refreshed head
2a9a2d79fd498be5fc62e83634a544f873b67b4c). 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).Blackbox Verification Evidence (2026-06-01)
PR Head SHA Tested
00f3c695439a7211d293b6f6ce203e4c0bea3a9fExact Commands Run
Blackbox probe (Linux, local worktree):
cd /tmp/proof-86953 pnpm install --frozen-lockfile pnpm build node blackbox-channel-guard-probe.mjsWhitebox unit tests:
node scripts/run-vitest.mjs src/plugins/registry.channel-guard.test.ts --reporter=verbose node scripts/run-vitest.mjs 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=verbose git diff --checkMarker-File Before/After Evidence
evil-workspace-probe, no allow/enable)trusted-workspace-probe,plugins.allow)trusted-workspace-probe-register-executedhealthy-sibling-probe-register-executedenabled-workspace-probe,plugins.entries.*.enabled=true)enabled-workspace-probe-register-executedWhether Disabled Workspace Setup Code Executed
No. In TEST1 and TEST3, the disabled workspace channel plugin (
evil-workspace-probe,evil-registry-probe) was discovered but not enabled or allowlisted. After theloadOpenClawPluginscall withincludeSetupOnlyChannelPlugins: true, forceSetupOnlyChannelPlugins: true, onlyPluginIds: [...]:register()callback ran — no register-executed marker"disabled"withorigin: "workspace"channelSetupsandchannelswere both empty — no channel registration occurredThe loader-level guard (
canLoadScopedSetupOnlyChannelPluginrequiringcandidate.origin !== "workspace" || enableState.enabled) correctly blocks disabled workspace plugins before module import. The registry defense-in-depth guard atregisterChannelwas not reached because the module was never imported.Whether Enabled/Allowlisted Setup Still Worked
Yes.
plugins.allow): TEST2 confirmed the allowlisted workspace plugin loaded with status"loaded",channelSetupspopulated, and marker file written.plugins.entries.*.enabled = true): TEST5 confirmed the explicitly enabled workspace plugin loaded with status"loaded"and marker file written.registerChannelin one allowlisted plugin records a diagnostic but does not prevent a healthy sibling from loading.Whitebox Unit Test Results
Tests verified:
blocks untrusted setup-only workspace channel plugins when explicitly scoped✓keeps trusted setup-only workspace channel plugins available when explicitly scoped✓records a diagnostic when registerChannel throws in the setup-entry path✓keeps healthy sibling channel plugins loadable when a setup entry throws✓Registry guard tests:
rejects channel registration from disabled workspace plugins✓rejects disabled workspace registration before reading channel data✓keeps channel registration available for trusted workspace plugins✓git diff --check: clean, no whitespace errors.Gaps or Skipped Checks
loadOpenClawPlugins()directly rather thanopenclaw channels addoropenclaw onboardCLI commands. The programmatic API exercises the same loader/registry guard code path; the CLI path is a thin wrapper calling the sameloadChannelSetupPluginRegistrySnapshotForChannel/loadChannelSetupPluginRegistryentry points.plugins.load.pathstesting: Per the prompt, this is a separate follow-up hardening topic and was explicitly excluded.origin: "workspace"from.openclaw/extensions/), which the fixtures exercise correctly.listAccountIdsgetter throwing. The actual fix(plugins): guard registerChannel in setup-entry path with try/catch #85774 guard also coversapi.registerChannel(mergedSetupPlugin)throwing; this is covered by the whitebox unit testrecords a diagnostic when registerChannel throws in the setup-entry path.