fix(config): strip legacy installs/plugins from channel configs before validation#63474
fix(config): strip legacy installs/plugins from channel configs before validation#63474giulio-leone wants to merge 1 commit into
Conversation
…e validation (openclaw#63101) After upgrading from v4.5 to v4.8, gateway restart fails with "channels.feishu: invalid config: must NOT have additional properties" when legacy `installs` and `plugins` fields are present under a channel config block. These fields were never valid channel-level properties but could appear in v4.5-era configs due to top-level key nesting. Add a legacy config migration that auto-strips `installs` and `plugins` from every `channels.*` entry (including nested `accounts.*`) before schema validation runs. The migration logs a clear change message so the operator knows the fields were removed. - Add migration `channels.*.stale-top-level-keys` with legacy rule for `channels.feishu` - Add 6 regression tests for the migration in legacy-migrate.test.ts - Add 3 schema-level rejection tests in config-schema.test.ts Closes openclaw#63101 Co-authored-by: Copilot <[email protected]>
Greptile SummaryThis PR adds a legacy config migration ( Confidence Score: 5/5Safe to merge — the migration is correctly ordered, applies to all channels, uses established in-place mutation patterns, and is well-tested. All findings are P2 style observations. The migration logic itself is correct: stale keys are deleted before schema validation runs, the function is registered first in the migrations list, prototype-safe key checks are used throughout, and the apply/rules split (all channels stripped, feishu-only warning) is intentional and documented in the PR description. No files require special attention.
|
| describe("legacy migrate stale channel keys (issue #63101)", () => { | ||
| it("strips installs from channels.feishu", () => { | ||
| const { next, changes } = applyLegacyMigrations({ | ||
| channels: { | ||
| feishu: { | ||
| appId: "cli_test", | ||
| appSecret: "secret_test", // pragma: allowlist secret | ||
| installs: [{ source: "npm", spec: "@openclaw/[email protected]" }], | ||
| }, | ||
| }, | ||
| }); | ||
|
|
||
| expect(next).not.toBeNull(); | ||
| expect(changes.length).toBeGreaterThan(0); | ||
| expect(changes.some((c) => c.includes("channels.feishu.installs"))).toBe(true); | ||
| const feishu = (next?.channels as Record<string, Record<string, unknown>>)?.feishu; | ||
| expect(feishu).toBeDefined(); | ||
| expect(Object.prototype.hasOwnProperty.call(feishu, "installs")).toBe(false); | ||
| expect(feishu?.appId).toBe("cli_test"); | ||
| }); | ||
|
|
||
| it("strips plugins from channels.feishu", () => { | ||
| const { next, changes } = applyLegacyMigrations({ | ||
| channels: { | ||
| feishu: { | ||
| appId: "cli_test", | ||
| appSecret: "secret_test", // pragma: allowlist secret | ||
| plugins: { allow: ["@openclaw/plugin-feishu"] }, | ||
| }, | ||
| }, | ||
| }); | ||
|
|
||
| expect(next).not.toBeNull(); | ||
| expect(changes.some((c) => c.includes("channels.feishu.plugins"))).toBe(true); | ||
| const feishu = (next?.channels as Record<string, Record<string, unknown>>)?.feishu; | ||
| expect(Object.prototype.hasOwnProperty.call(feishu, "plugins")).toBe(false); | ||
| }); | ||
|
|
||
| it("strips both installs and plugins from channels.feishu", () => { | ||
| const { next, changes } = applyLegacyMigrations({ | ||
| channels: { | ||
| feishu: { | ||
| appId: "cli_test", | ||
| appSecret: "secret_test", // pragma: allowlist secret | ||
| installs: [{ source: "npm", spec: "@openclaw/[email protected]" }], | ||
| plugins: { allow: ["@openclaw/plugin-feishu"] }, | ||
| }, | ||
| }, | ||
| }); | ||
|
|
||
| expect(next).not.toBeNull(); | ||
| expect(changes.filter((c) => c.includes("channels.feishu")).length).toBe(2); | ||
| const feishu = (next?.channels as Record<string, Record<string, unknown>>)?.feishu; | ||
| expect(Object.prototype.hasOwnProperty.call(feishu, "installs")).toBe(false); | ||
| expect(Object.prototype.hasOwnProperty.call(feishu, "plugins")).toBe(false); | ||
| }); | ||
|
|
||
| it("does not modify channel config without stale keys", () => { | ||
| const { next, changes } = applyLegacyMigrations({ | ||
| channels: { | ||
| feishu: { | ||
| appId: "cli_test", | ||
| appSecret: "secret_test", // pragma: allowlist secret | ||
| }, | ||
| }, | ||
| }); | ||
|
|
||
| expect(next).toBeNull(); | ||
| expect(changes).toHaveLength(0); | ||
| }); | ||
|
|
||
| it("strips stale keys from other channels too", () => { | ||
| const { next, changes } = applyLegacyMigrations({ | ||
| channels: { | ||
| slack: { | ||
| botToken: "xoxb-test", | ||
| installs: [{ source: "npm", spec: "@openclaw/[email protected]" }], | ||
| }, | ||
| }, | ||
| }); | ||
|
|
||
| expect(next).not.toBeNull(); | ||
| expect(changes.some((c) => c.includes("channels.slack.installs"))).toBe(true); | ||
| }); | ||
|
|
||
| it("strips stale keys from nested account configs", () => { | ||
| const { next, changes } = applyLegacyMigrations({ | ||
| channels: { | ||
| feishu: { | ||
| appId: "cli_test", | ||
| appSecret: "secret_test", // pragma: allowlist secret | ||
| accounts: { | ||
| main: { | ||
| appId: "cli_main", | ||
| plugins: { allow: ["@openclaw/plugin-feishu"] }, | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| }); | ||
|
|
||
| expect(next).not.toBeNull(); | ||
| expect(changes.some((c) => c.includes("channels.feishu.accounts.main.plugins"))).toBe(true); | ||
| const accounts = (next?.channels as Record<string, Record<string, unknown>>)?.feishu | ||
| ?.accounts as Record<string, Record<string, unknown>>; | ||
| expect(Object.prototype.hasOwnProperty.call(accounts?.main, "plugins")).toBe(false); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Test helper inconsistency with rest of file
All other describe blocks in this file (legacy migrate audio transcription, legacy migrate heartbeat config, legacy migrate controlUi.allowedOrigins seed) call migrateLegacyConfig (which wraps applyLegacyMigrations with schema validation). The new stale-channel-keys suite calls applyLegacyMigrations directly instead.
Using migrateLegacyConfig here would also validate that the stripped config survives schema validation end-to-end — the most important invariant for the fix. The tests would need appId/appSecret values sufficient for the schema to accept the result, but most test fixtures already include them.
Not a blocking issue since the migration logic is still covered, but using the higher-level wrapper would add integration-level coverage consistent with the file's established pattern.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/config/legacy-migrate.test.ts
Line: 334-441
Comment:
**Test helper inconsistency with rest of file**
All other `describe` blocks in this file (`legacy migrate audio transcription`, `legacy migrate heartbeat config`, `legacy migrate controlUi.allowedOrigins seed`) call `migrateLegacyConfig` (which wraps `applyLegacyMigrations` with schema validation). The new stale-channel-keys suite calls `applyLegacyMigrations` directly instead.
Using `migrateLegacyConfig` here would also validate that the stripped config survives schema validation end-to-end — the most important invariant for the fix. The tests would need `appId`/`appSecret` values sufficient for the schema to accept the result, but most test fixtures already include them.
Not a blocking issue since the migration logic is still covered, but using the higher-level wrapper would add integration-level coverage consistent with the file's established pattern.
How can I resolve this? If you propose a fix, please make it concise.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 886aa1abae
ℹ️ About Codex in GitHub
Codex has been enabled to automatically 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 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| path: ["channels", "feishu"], | ||
| message: | ||
| 'channels.feishu contains legacy "installs" or "plugins" keys that are not valid channel properties (auto-removed on load).', | ||
| match: (value) => hasStaleChannelKeys(value), | ||
| }, |
There was a problem hiding this comment.
Match stale-key legacy rule for all channel configs
The load path only applies migrations when findLegacyConfigIssues finds at least one matching legacy rule, but this new rule is limited to path: ["channels", "feishu"]. As a result, configs that still have channels.<other>.installs/plugins (for example channels.slack.installs) never trigger migration and will continue to fail strict channel validation, so the new channels.*.stale-top-level-keys migration is not reachable for the non-Feishu cases it is intended to cover.
Useful? React with 👍 / 👎.
| function hasStaleChannelKeys(channelValue: unknown): boolean { | ||
| const entry = getRecord(channelValue); | ||
| return entry !== null && CHANNEL_LEVEL_STALE_KEYS.some((key) => hasOwnKey(entry, key)); |
There was a problem hiding this comment.
Detect stale keys under feishu account entries
The legacy-rule matcher only checks installs/plugins on the top-level channels.feishu object and ignores channels.feishu.accounts.<id>.*. If stale keys exist only inside an account block, no legacy issue is reported, the read flow skips migrations, and strict plugin schema validation will still fail on those account-level extra properties even though the migration includes code to strip them.
Useful? React with 👍 / 👎.
|
Thanks for the context here. I swept through the related work, and this is now duplicate or superseded. Keep open: the upgrade bug is valid and the PR has useful direction, but it is not merge-ready because current main moved the live doctor migration path, the rule/mutation scope is incomplete and compatibility-sensitive, and external real behavior proof is missing. Canonical path: Close this stale PR. The latest review rated it F, the branch still lacks merge-ready proof, and there has been no human follow-up after the durable review. So I’m closing this here because the remaining work is already tracked in the canonical issue. Review detailsBest possible solution: Close this stale PR. The latest review rated it F, the branch still lacks merge-ready proof, and there has been no human follow-up after the durable review. Do we have a high-confidence way to reproduce the issue? Yes, at source level: Feishu channel schemas are strict and plugin-aware validation rejects additional Is this the best way to solve the issue? No, not as written: the useful migration needs to move to the current doctor path and be scoped so it fixes the reported upgrade state without globally deleting possible plugin-owned channel fields. Security review: Security review cleared: No dependency, workflow, secret-handling, package, or code-execution surface was added; the remaining concern is compatibility, not supply chain security. AGENTS.md: found and applied where relevant. What I checked:
Likely related people:
Codex review notes: model gpt-5.5, reasoning high; reviewed against 45144ce2e856. |
|
ClawSweeper PR egg 🎁 Pass real behavior proof to wake the egg and unlock a hatchable treat. Where did the egg go?
|
|
This pull request has been automatically marked as stale due to inactivity. |
|
Closing due to inactivity. |
Summary
Fixes #63101
After upgrading from v4.5 to v4.8,
openclaw gateway restartfails with:when legacy
installsandpluginsfields are present underchannels.feishu.Root Cause
The Feishu channel config schema (
FeishuConfigSchema) uses Zod.strict()mode, which rejects any properties not explicitly declared in the schema. Theinstallsandpluginsfields were never valid channel-level properties but could appear in v4.5-era configs due to top-level key nesting.Fix
Add a legacy config migration (
channels.*.stale-top-level-keys) that auto-stripsinstallsandpluginsfrom everychannels.*entry (including nestedaccounts.*) before schema validation runs. The migration:channels.feishuspecificallyTests
src/config/legacy-migrate.test.tsextensions/feishu/src/config-schema.test.tsAll 51 tests pass.