Preserve update-time config state by snapshotting before repair/restart writes, keeping plugin install records available for migration, and blocking unsafe update-time config size drops.
Also documents the Codex reserved SDK subpaths needed by the plugin contract guardrail.
Fixes#80077.
Thanks @Jerry-Xin and @vincentkoc.
Co-authored-by: Jerry-Xin <3401616+Jerry-Xin@users.noreply.github.com>
Co-authored-by: Vincent Koc <25068+vincentkoc@users.noreply.github.com>
PR #75095 added `redactConfigAuditArgv` so newly-written entries in
`~/.openclaw/logs/config-audit.jsonl` mask token-shaped values, but the
audit log has no rotation, retention, or one-shot scrub. Users who ran
`openclaw config set <path> <secret-value>` between commit `748d6821d2`
(audit added 2026-02-14) and `a853c5e8c2` (redactor merged 2026-05-01)
still have plaintext Slack, Telegram, gateway, etc. tokens sitting in
`config-audit.jsonl` at rest after upgrading.
Add `scrubConfigAuditLog` in `src/config/io.audit.ts` that reads the
audit log, rewrites every record's `argv` / `execArgv` fields through
the existing forward redactor, and writes the file back atomically
(`*.scrub.tmp` + rename, mode 0o600). Idempotent — already-masked
entries pass through unchanged. Malformed lines are preserved verbatim
and counted as `skipped` so the scrub never destroys forensic content
it cannot understand. Concurrent-append guard: re-stat the audit log
immediately before rename and abort (no on-disk change) if the file
grew during the scrub, so a parallel appendConfigAuditRecord call is
never overwritten by the rename swap. Wire into the doctor health flow
as `runConfigAuditScrubHealth`: scan-only on a normal `openclaw doctor`
(prints the count and suggests `--fix`); rewrite atomically on
`openclaw doctor --fix` with a "rotate any credentials" note; print a
"stop the gateway and rerun" hint on the abort path. No plaintext
backup file is produced; file mode and parent dir mode are preserved.
Per repo CLAUDE.md ("Legacy config repair: doctor/fix paths, not
startup/load-time core migrations") the scrub is gated to `--fix`
and runs only when the user opts in.
Verified:
- pnpm install --frozen-lockfile
- pnpm test src/config/io.audit.test.ts
- pnpm test src/commands/doctor-config-flow.test.ts
- pnpm tsgo:core:test
- pnpm exec oxfmt --check --threads=1 src/config/io.audit.ts src/config/io.audit.test.ts src/commands/doctor-config-audit-scrub.ts src/flows/doctor-health-contributions.ts
Closes#80777
Summary:
- The branch adds an opt-in Feishu top-level group-send fallback for withdrawn or missing normal quoted thread replies, plus regression coverage, a changelog entry, and CI/lint typing and baseline refreshes.
- Reproducibility: yes. at source level. Current main hard-errors withdrawn/not-found Feishu reply targets when `replyInThread` is true, and the existing regression test asserts that no top-level create fallback occurs.
Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(feishu): fall back from missing thread replies
- PR branch already contained follow-up commit before automerge: fix(clawsweeper): address review for automerge-openclaw-openclaw-8030…
- PR branch already contained follow-up commit before automerge: fix(clawsweeper): reconcile automerge-openclaw-openclaw-80306 with ma…
- PR branch already contained follow-up commit before automerge: fix(ci): satisfy stricter lint and test types
- PR branch already contained follow-up commit before automerge: fix(ci): align Node 24 test typing
Validation:
- ClawSweeper review passed for head 93146f9d13.
- Required merge gates passed before the squash merge.
Prepared head SHA: 93146f9d13
Review: https://github.com/openclaw/openclaw/pull/80306#issuecomment-4415604729
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
When a user's config has a stale `channels.<id>` entry (e.g. `appId`
or tokens left over from an earlier install) and the plugin is no
longer on disk -- for instance because the externalized npm package
was uninstalled or pruned during an upgrade -- `handleChannelChoice`
used to dead-end with "<channel> plugin not available." and leave
onboard stuck until the user manually deleted the config entry and
re-ran the CLI.
Two discovery paths are affected:
1. The `installedCatalogEntry` branch: when
`loadScopedChannelPlugin` returns null but the catalog entry still
carries `install.npmSpec`, fall back to
`ensureChannelSetupPluginInstalled` with the same entry so onboard
can reinstall the plugin from the official catalog.
2. The bundled-enable `else` branch: with a non-empty
`channels.<id>` record, `isStaticallyChannelConfigured` drops the
channel from `installableCatalogEntries`; if the plugin is also
missing on disk (so it never enters `manifestInstalledIds`), both
discovery buckets come back empty and the channel falls through to
`enableBundledPluginForSetup`. Before delegating to that bundled
path, consult the trusted catalog via
`getTrustedChannelPluginCatalogEntry` and, if an `install.npmSpec`
is available, drive the same catalog install flow used by a fresh
pick of the channel.
Both new fallbacks re-apply the `resolveConfigDisabledHint` guard
that `enableBundledPluginForSetup` has always enforced, so an
operator-disabled channel (`plugins.entries.<id>.enabled === false`
or explicit `channels.<id>.enabled === false`) with a stale config
entry cannot be silently reinstalled or re-enabled through the
catalog path.
Both branches also keep their previous behavior when no catalog npm
spec is available (e.g. purely bundled channels), so this change is
a superset of the old flow rather than a replacement.
Affects all externalized channel plugins listed in the core
package's `files` exclusion (qqbot, bluebubbles, discord, whatsapp,
line, msteams, feishu, googlechat, nostr, zalo, zalouser,
synology-chat, tlon, twitch, and similar).
* fix(doctor): commit legacy migrations even when unrelated validation fails (#76798)
migrateLegacyConfig previously returned config: null when post-migration
validation found any issue (e.g. a missing plugin). The caller then kept
the unmigrated config as the candidate, so doctor --fix never wrote the
legacy migration to disk.
Now when validation fails after a successful migration, the migrated
config is returned with partiallyValid: true. applyLegacyCompatibilityStep
always commits the migrated config to state.candidate, ensuring
agents.defaults.llm and other known-legacy keys are cleaned up on
doctor --fix even when an unrelated provider or plugin issue blocks
the full validator.
Adds regression test asserting that candidate is updated to the migrated
shape when partiallyValid is set.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fixup: extend skipPluginValidation to write path with E2E coverage
Thread skipPluginValidationOnWrite through loadAndMaybeMigrateDoctorConfig
return value into runWriteConfigHealth so replaceConfigFile bypasses plugin
validation when migration is only partially valid. Add E2E test verifying
the flag propagates end-to-end.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fixup(doctor): wire skipPluginValidation through full write path (#76800)
Clawsweeper P2 x2:
1. io.ts exported writeConfigFile wrapper now passes skipPluginValidation to
createConfigIO so both write-phase validation and post-write loadConfig
re-read honor the flag.
2. mutate.ts tryWriteSingleTopLevelIncludeMutation now skips plugin validation
when writeOptions.skipPluginValidation is set, so include-write fast path
no longer blocks safe legacy migrations with unrelated plugin errors.
Adds regression test: skipPluginValidation bypasses plugin schema rejection
on writeConfigFile and falls back to throwing when flag is not set.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fixup(doctor): bail out of include fast path when skipPluginValidation is set (#76800)
Clawsweeper P2: after the include write, readConfigFileSnapshotForWrite()
calls loadConfig() which validates with plugins; refreshedSnapshot.valid is
false when an unrelated plugin issue exists, causing the include path to
throw even though skipPluginValidation was requested.
Simplest fix: return false from tryWriteSingleTopLevelIncludeMutation when
skipPluginValidation is set, letting the root writer handle the write with
plugin validation disabled end-to-end (including post-write readback via
createConfigIO({ pluginValidation: "skip" })).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(doctor): cover partial legacy migration writes
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Summary:
- The PR adds `sensitive` support to wizard text prompts, routes sensitive Clack prompts through `password()`, ... preserves existing gateway secrets through masked-preview confirms, and adds tests plus a changelog entry.
- Reproducibility: yes. Source inspection shows current main routes onboarding credential entry through visibl ... y provides a concrete Windows PowerShell `openclaw onboard --install-daemon` reproduction with screenshots.
Automerge notes:
- No ClawSweeper repair was needed after automerge opt-in.
Validation:
- ClawSweeper review passed for head a3db64c265.
- Required merge gates passed before the squash merge.
Prepared head SHA: a3db64c265
Review: https://github.com/openclaw/openclaw/pull/76693#issuecomment-4366253531
Co-authored-by: anurag-bg-neu <bheemappagnanamurt.a@northeastern.edu>