From 8fe6ac7fb9f2524bcd95a2d3a06faaa13b072015 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 11 Jul 2026 13:22:27 -0700 Subject: [PATCH] refactor: remove pre-2026.4 compatibility shims and legacy migrations (#104650) * refactor(matrix): retire pre-2026.4 legacy crypto and flat-storage migrations * refactor(cli): drop pre-tsdown daemon-cli dist compat shim from the build * refactor(diffs): remove deprecated image*/format tool params and output aliases * refactor(ui): drop pre-gateway-scoped localStorage readers and legacy theme map * refactor: remove assorted pre-2026.4 compat shims and deprecated aliases * fix(discord): map legacy thread-binding fields in the doctor JSON import * fix(msteams): keep bot read fallback for legacy imported conversation rows * test(msteams): cover legacy bot-only imported conversation references * fix(ui): keep channel-prefixed session key display fallback * chore: refresh native i18n inventory and build-docker step count * chore(matrix): drop now-unused migration-config module * chore: re-pin plugin SDK public export budget after compat removals * chore: refresh native i18n inventory after rebase * chore: re-pin plugin SDK callable budget and refresh i18n inventory after rebase --- apps/.i18n/native-source.json | 4 +- apps/macos/Sources/OpenClaw/AppState.swift | 3 - .../OpenClaw/UserDefaultsMigration.swift | 16 - docs/channels/matrix-migration.md | 169 +----- docs/docs_map.md | 6 +- docs/tools/diffs.md | 29 +- extensions/diffs/README.md | 8 - extensions/diffs/src/config.test.ts | 1 - extensions/diffs/src/config.ts | 20 +- extensions/diffs/src/tool.test.ts | 57 +- extensions/diffs/src/tool.ts | 56 +- .../model-picker-preferences-migrations.ts | 43 +- .../monitor/thread-bindings.lifecycle.test.ts | 91 ---- .../src/monitor/thread-bindings.state.ts | 36 +- .../src/monitor/thread-bindings.types.ts | 6 +- extensions/matrix/api.ts | 2 - extensions/matrix/runtime-api.ts | 2 - extensions/matrix/runtime-heavy-api.ts | 2 - extensions/matrix/src/channel.ts | 4 - extensions/matrix/src/doctor.test.ts | 99 ---- extensions/matrix/src/doctor.ts | 180 +------ ...gacy-crypto-inspector-availability.test.ts | 84 --- .../legacy-crypto-inspector-availability.ts | 61 --- extensions/matrix/src/legacy-crypto.test.ts | 243 --------- extensions/matrix/src/legacy-crypto.ts | 502 ------------------ extensions/matrix/src/legacy-state.test.ts | 87 --- extensions/matrix/src/legacy-state.ts | 157 ------ .../matrix/src/matrix-migration.runtime.ts | 10 - .../client/migration-snapshot.runtime.ts | 2 - .../matrix/src/matrix/client/storage.test.ts | 226 -------- .../matrix/src/matrix/client/storage.ts | 122 +---- .../src/matrix/legacy-crypto-inspector.ts | 98 ---- .../matrix/src/matrix/monitor/index.test.ts | 4 - .../monitor/legacy-crypto-restore.test.ts | 212 -------- .../matrix/monitor/legacy-crypto-restore.ts | 136 ----- .../matrix/src/matrix/monitor/startup.test.ts | 27 - .../matrix/src/matrix/monitor/startup.ts | 59 +- .../matrix/src/migration-config.test.ts | 239 --------- extensions/matrix/src/migration-config.ts | 244 --------- .../matrix/src/migration-snapshot-backup.ts | 117 ---- .../matrix/src/migration-snapshot.test.ts | 171 ------ extensions/matrix/src/migration-snapshot.ts | 54 -- .../matrix/src/startup-maintenance.test.ts | 231 -------- extensions/matrix/src/startup-maintenance.ts | 115 ---- extensions/matrix/src/storage-paths.ts | 17 - .../msteams/src/conversation-store-state.ts | 1 - .../src/conversation-store.shared.test.ts | 4 - extensions/msteams/src/conversation-store.ts | 8 +- extensions/msteams/src/feedback-invoke.ts | 3 - extensions/msteams/src/messenger.test.ts | 17 + extensions/msteams/src/messenger.ts | 1 + .../src/monitor-handler.adaptive-card.test.ts | 1 - .../src/monitor-handler.test-helpers.ts | 1 - .../message-handler.authz.test.ts | 4 - .../message-handler.test-support.ts | 1 - .../src/monitor-handler/message-handler.ts | 1 - extensions/msteams/src/sdk-proactive.test.ts | 27 + extensions/msteams/src/sdk-proactive.ts | 1 + extensions/msteams/src/send-context.test.ts | 2 - extensions/ollama/runtime-api.ts | 1 - extensions/ollama/src/stream.ts | 3 - .../.boundary-stubs/ollama-runtime-api.d.ts | 1 - package.json | 2 +- packages/gateway-client/src/client.ts | 9 +- .../src/client.watchdog.test.ts | 11 +- packages/gateway-client/src/readiness.ts | 7 +- packages/net-policy/src/ipv4.test.ts | 7 +- packages/net-policy/src/ipv4.ts | 5 - packages/sdk/src/transport.ts | 1 - scripts/build-all.mjs | 8 - scripts/deadcode-unused-files.allowlist.mjs | 1 - scripts/plugin-sdk-surface-report.mjs | 4 +- scripts/write-cli-compat.ts | 194 ------- src/cli/daemon-cli-compat.test.ts | 86 --- src/cli/daemon-cli-compat.ts | 140 ----- src/commands/configure.gateway.ts | 4 +- src/infra/exec-approval-reply.test.ts | 33 -- src/infra/exec-approval-reply.ts | 32 -- .../plugin-sdk-runtime-api-guardrails.test.ts | 2 +- src/wizard/setup.gateway-config.ts | 4 +- test/package-scripts.test.ts | 2 +- test/scripts/build-all.test.ts | 8 - ui/src/app/settings.node.test.ts | 100 +--- ui/src/app/settings.ts | 68 +-- ui/src/app/theme.test.ts | 8 +- ui/src/app/theme.ts | 24 +- ui/src/lib/chat/message-extract.ts | 17 +- ui/src/lib/session-display.ts | 7 +- 88 files changed, 203 insertions(+), 4710 deletions(-) delete mode 100644 apps/macos/Sources/OpenClaw/UserDefaultsMigration.swift delete mode 100644 extensions/matrix/runtime-heavy-api.ts delete mode 100644 extensions/matrix/src/legacy-crypto-inspector-availability.test.ts delete mode 100644 extensions/matrix/src/legacy-crypto-inspector-availability.ts delete mode 100644 extensions/matrix/src/legacy-crypto.test.ts delete mode 100644 extensions/matrix/src/legacy-crypto.ts delete mode 100644 extensions/matrix/src/legacy-state.test.ts delete mode 100644 extensions/matrix/src/legacy-state.ts delete mode 100644 extensions/matrix/src/matrix-migration.runtime.ts delete mode 100644 extensions/matrix/src/matrix/client/migration-snapshot.runtime.ts delete mode 100644 extensions/matrix/src/matrix/legacy-crypto-inspector.ts delete mode 100644 extensions/matrix/src/matrix/monitor/legacy-crypto-restore.test.ts delete mode 100644 extensions/matrix/src/matrix/monitor/legacy-crypto-restore.ts delete mode 100644 extensions/matrix/src/migration-config.test.ts delete mode 100644 extensions/matrix/src/migration-config.ts delete mode 100644 extensions/matrix/src/migration-snapshot-backup.ts delete mode 100644 extensions/matrix/src/migration-snapshot.test.ts delete mode 100644 extensions/matrix/src/migration-snapshot.ts delete mode 100644 extensions/matrix/src/startup-maintenance.test.ts delete mode 100644 extensions/matrix/src/startup-maintenance.ts delete mode 100644 scripts/write-cli-compat.ts delete mode 100644 src/cli/daemon-cli-compat.test.ts delete mode 100644 src/cli/daemon-cli-compat.ts diff --git a/apps/.i18n/native-source.json b/apps/.i18n/native-source.json index 1f464aa8e8df..7759b027eeb0 100644 --- a/apps/.i18n/native-source.json +++ b/apps/.i18n/native-source.json @@ -15107,7 +15107,7 @@ }, { "kind": "conditional-branch", - "line": 742, + "line": 739, "path": "apps/macos/Sources/OpenClaw/AppState.swift", "source": "\\(user)@\\(host)", "surface": "apple", @@ -15115,7 +15115,7 @@ }, { "kind": "conditional-branch", - "line": 742, + "line": 739, "path": "apps/macos/Sources/OpenClaw/AppState.swift", "source": "\\(user)@\\(host):\\(port)", "surface": "apple", diff --git a/apps/macos/Sources/OpenClaw/AppState.swift b/apps/macos/Sources/OpenClaw/AppState.swift index 5351de51684b..d54e9fcbcd29 100644 --- a/apps/macos/Sources/OpenClaw/AppState.swift +++ b/apps/macos/Sources/OpenClaw/AppState.swift @@ -325,9 +325,6 @@ final class AppState { isPreview || ApplicationRelocator.currentBundleAllowsPersistentIntegration() self.execApprovalsDefaultsAsyncResolver = execApprovalsDefaultsAsyncResolver self.execApprovalsReadRetryDelay = execApprovalsReadRetryDelay - if !isPreview { - migrateLegacyDefaults() - } let onboardingSeen = UserDefaults.standard.bool(forKey: onboardingSeenKey) self.isPaused = UserDefaults.standard.bool(forKey: pauseDefaultsKey) self.launchAtLogin = false diff --git a/apps/macos/Sources/OpenClaw/UserDefaultsMigration.swift b/apps/macos/Sources/OpenClaw/UserDefaultsMigration.swift deleted file mode 100644 index 793e52baeb79..000000000000 --- a/apps/macos/Sources/OpenClaw/UserDefaultsMigration.swift +++ /dev/null @@ -1,16 +0,0 @@ -import Foundation - -private let legacyDefaultsPrefix = "openclaw." -private let defaultsPrefix = "openclaw." - -func migrateLegacyDefaults() { - let defaults = UserDefaults.standard - let snapshot = defaults.dictionaryRepresentation() - for (key, value) in snapshot where key.hasPrefix(legacyDefaultsPrefix) { - let suffix = key.dropFirst(legacyDefaultsPrefix.count) - let newKey = defaultsPrefix + suffix - if defaults.object(forKey: newKey) == nil { - defaults.set(value, forKey: newKey) - } - } -} diff --git a/docs/channels/matrix-migration.md b/docs/channels/matrix-migration.md index fb80a7f3db61..93e798b0a504 100644 --- a/docs/channels/matrix-migration.md +++ b/docs/channels/matrix-migration.md @@ -25,67 +25,38 @@ into the root OpenClaw package. ## What the migration does automatically -Matrix migration runs when the gateway starts (through the loaded Matrix plugin), when you run [`openclaw doctor --fix`](/gateway/doctor), and as a fallback when the Matrix client starts and still finds old on-disk state. Before any actionable migration step mutates on-disk state, OpenClaw creates or reuses a focused recovery snapshot. - -When you use `openclaw update`, the exact trigger depends on how OpenClaw is installed: - -- source installs run a non-interactive `openclaw doctor --fix` pass during the update flow, then restart the gateway by default -- package-manager installs update the package, run `openclaw doctor --non-interactive --fix`, then rely on the default gateway restart so startup can finish Matrix migration -- if you use `openclaw update --no-restart`, startup-backed Matrix migration is deferred until you later run `openclaw doctor --fix` and restart the gateway +Matrix migration runs when you run [`openclaw doctor --fix`](/gateway/doctor), and as a fallback when the Matrix client starts and still finds file-based sidecar state next to its SQLite store. Automatic migration covers: -- creating or reusing a pre-migration snapshot under `~/Backups/openclaw-migrations/` - reusing your cached Matrix credentials - keeping the same account selection and `channels.matrix` config -- moving the old flat Matrix sync store and crypto store into the current account-scoped location when the target account can be resolved safely - importing file-based sidecar state (`bot-storage.json` sync cache, `recovery-key.json`, `legacy-crypto-migration.json`, IndexedDB snapshots) into Matrix SQLite state; migrated files are archived with a `.migrated` suffix -- extracting a previously saved Matrix room-key backup decryption key from the old rust crypto store, when that key exists locally - reusing the most complete existing token-hash storage root for the same Matrix account, homeserver, user, and device when the access token changes later -- scanning sibling token-hash storage roots for pending encrypted-state restore metadata when the Matrix access token changed but the account/device identity stayed the same -- restoring backed-up room keys into the new crypto store on the next Matrix startup -Snapshot details: +## Upgrading from OpenClaw releases older than 2026.4 -- OpenClaw writes a marker file at `~/.openclaw/matrix/migration-snapshot.json` after a successful snapshot so later startup and repair passes can reuse the same archive. -- These automatic Matrix migration snapshots back up config + state only (`includeWorkspace: false`). -- If Matrix only has warning-only migration state, for example because `userId` or `accessToken` is still missing, OpenClaw does not create the snapshot yet because no Matrix mutation is actionable. -- If the snapshot step fails, OpenClaw skips Matrix migration for that run instead of mutating state without a recovery point. +Releases through the 2026.6 train also migrated the original flat single-store +Matrix layout (`~/.openclaw/matrix/bot-storage.json` plus +`~/.openclaw/matrix/crypto/`) and prepared encrypted-state recovery from the +old rust crypto store. Current releases no longer carry that migration. -About multi-account upgrades: +If you are upgrading an installation that still uses the flat layout, first +upgrade to a 2026.6 release, run `openclaw doctor --fix`, and start the gateway +once so the flat store and any recoverable room keys are migrated. Then update +to the latest release. -- the flat Matrix store (`~/.openclaw/matrix/bot-storage.json` and `~/.openclaw/matrix/crypto/`) came from a single-store layout, so OpenClaw can only migrate it into one resolved Matrix account target -- already account-scoped legacy Matrix stores are detected and prepared per configured Matrix account - -## What the migration cannot do automatically - -The previous public Matrix plugin did **not** automatically create Matrix room-key backups. It persisted local crypto state and requested device verification, but it did not guarantee that your room keys were backed up to the homeserver. - -That means some encrypted installs can only be migrated partially. - -OpenClaw cannot automatically recover: - -- local-only room keys that were never backed up -- encrypted state when the target Matrix account cannot be resolved yet because `homeserver`, `userId`, or `accessToken` are still unavailable -- encrypted state when the old crypto store has no recorded device ID for the account -- automatic migration of one shared flat Matrix store when multiple Matrix accounts are configured but `channels.matrix.defaultAccount` is not set -- custom plugin path installs that are pinned to a repo path instead of the standard Matrix package (surfaced by `openclaw doctor`) -- a missing recovery key when the old store had backed-up keys but did not keep the decryption key locally - -If your old installation had local-only encrypted history that was never backed up, some older encrypted messages may remain unreadable after the upgrade. +The previous public Matrix plugin did **not** automatically create Matrix room-key backups. If your old installation had local-only encrypted history that was never backed up, some older encrypted messages may remain unreadable after the upgrade regardless of the migration path. ## Recommended upgrade flow 1. Update OpenClaw and the Matrix plugin normally. - Prefer plain `openclaw update` without `--no-restart` so startup can finish the Matrix migration immediately. 2. Run: ```bash openclaw doctor --fix ``` - If Matrix has actionable migration work, doctor will create or reuse the pre-migration snapshot first and print the archive path. - 3. Start or restart the gateway. 4. Check current verification and backup state: @@ -135,77 +106,11 @@ If your old installation had local-only encrypted history that was never backed openclaw matrix verify bootstrap ``` -## How encrypted migration works - -Encrypted migration is a two-stage process: - -1. Startup or `openclaw doctor --fix` creates or reuses the pre-migration snapshot if encrypted migration is actionable, then inspects the old Matrix rust crypto store through the crypto inspector bundled with the Matrix plugin. -2. If a backup decryption key is found, OpenClaw imports it into Matrix SQLite state and marks room-key restore as pending. -3. On the next Matrix startup, OpenClaw restores backed-up room keys into the new crypto store automatically. Pending restore state is also picked up from sibling token-hash storage roots when the access token rotated in between. - -If the old store reports room keys that were never backed up, OpenClaw warns instead of pretending recovery succeeded. - ## Common messages and what they mean -### Upgrade and detection messages - -`Matrix plugin upgraded in place.` (doctor) or `matrix: plugin upgraded in place for account "..."` (startup) - -- Meaning: the old on-disk Matrix state was detected and migrated into the current layout. -- What to do: nothing unless the same output also includes warnings. - -`Matrix migration snapshot created before applying Matrix upgrades.` / `Matrix migration snapshot reused before applying Matrix upgrades.` - -- Meaning: doctor created a recovery archive before mutating Matrix state, or found an existing snapshot marker and reused that archive instead of creating a duplicate backup. Startup logs the same as `matrix: created pre-migration backup snapshot: ...` / `matrix: reusing existing pre-migration backup snapshot: ...`. -- What to do: keep the printed archive path until you confirm migration succeeded. - -`Legacy Matrix state detected at ... but channels.matrix is not configured yet.` - -- Meaning: old Matrix state exists, but OpenClaw cannot map it to a current Matrix account because Matrix is not configured. -- What to do: configure `channels.matrix`, then rerun `openclaw doctor --fix` or restart the gateway. - -`Legacy Matrix state detected at ... but the new account-scoped target could not be resolved yet (need homeserver, userId, and access token for channels.matrix...).` - -- Meaning: OpenClaw found old state, but it still cannot determine the exact current account/device root. -- What to do: start the gateway once with a working Matrix login, or rerun `openclaw doctor --fix` after cached credentials exist. - -`Legacy Matrix state detected at ... but multiple Matrix accounts are configured and channels.matrix.defaultAccount is not set.` - -- Meaning: OpenClaw found one shared flat Matrix store, but it refuses to guess which named Matrix account should receive it. -- What to do: set `channels.matrix.defaultAccount` to the intended account, then rerun `openclaw doctor --fix` or restart the gateway. - -The same three warnings also appear with the prefix `Legacy Matrix encrypted state detected at ...` when the blocked store is the old encrypted crypto store. - -`Matrix legacy sync store not migrated because the target already exists (...)` / `Matrix legacy crypto store not migrated because the target already exists (...)` - -- Meaning: the new account-scoped location already has a sync or crypto store, so OpenClaw did not overwrite it automatically. -- What to do: verify that the current account is the correct one before manually removing or moving the conflicting target. - -`Failed migrating Matrix legacy sync store (...)` or `Failed migrating Matrix legacy crypto store (...)` - -- Meaning: OpenClaw tried to move old Matrix state but the filesystem operation failed. -- What to do: inspect filesystem permissions and disk state, then rerun `openclaw doctor --fix`. - -`Matrix migration warnings are present, but no on-disk Matrix mutation is actionable yet. No pre-migration snapshot was needed.` - -- Meaning: OpenClaw detected old Matrix state, but the migration is still blocked on missing identity or credential data. Startup logs this as `matrix: migration remains in a warning-only state; no pre-migration snapshot was needed yet`. -- What to do: finish Matrix login or config setup, then rerun `openclaw doctor --fix` or restart the gateway. - -`Legacy Matrix encrypted state was detected, but the Matrix crypto inspector is unavailable.` - -- Meaning: OpenClaw found old encrypted Matrix state, but the Matrix plugin build is missing the crypto inspector module that inspects the old rust crypto store. -- What to do: reinstall or repair the Matrix plugin (`openclaw plugins install @openclaw/matrix`, or `openclaw plugins install ./path/to/local/matrix-plugin` for a repo checkout), then rerun `openclaw doctor --fix` or restart the gateway. - -`- Failed creating a Matrix migration snapshot before repair: ...` - -`- Skipping Matrix migration changes for now. Resolve the snapshot failure, then rerun "openclaw doctor --fix".` - -- Meaning: OpenClaw refused to mutate Matrix state because it could not create the recovery snapshot first. -- What to do: resolve the backup error, then rerun `openclaw doctor --fix` or restart the gateway. - `Failed migrating legacy Matrix client storage: ...` -- Meaning: the Matrix client-side fallback found old storage, but the migration failed. OpenClaw rolls back completed moves and aborts that fallback instead of silently starting with a fresh store. This error also appears when the flat store targets a different account than the one currently starting. +- Meaning: the Matrix client-side fallback found file-based sidecar state, but the import into SQLite failed. OpenClaw rolls back completed moves and aborts that fallback instead of silently starting with a fresh store. - What to do: inspect filesystem permissions or conflicts, keep the old state intact, and retry after fixing the error. `Matrix is installed from a custom path: ...` @@ -213,47 +118,10 @@ The same three warnings also appear with the prefix `Legacy Matrix encrypted sta - Meaning: Matrix is pinned to a path install, so mainline updates do not automatically replace it with the default Matrix package. - What to do: reinstall with `openclaw plugins install @openclaw/matrix` when you want to return to the default Matrix plugin. -### Encrypted-state recovery messages +`Matrix is installed from a custom path that no longer exists: ...` -`matrix: restored X/Y room key(s) from legacy encrypted-state backup` - -- Meaning: backed-up room keys were restored successfully into the new crypto store. -- What to do: usually nothing. - -`matrix: N legacy local-only room key(s) were never backed up and could not be restored automatically` - -- Meaning: some old room keys existed only in the old local store and had never been uploaded to Matrix backup. During preparation the same limit is reported as `Legacy Matrix encrypted state for account "..." contains N room key(s) that were never backed up.` -- What to do: expect some old encrypted history to remain unavailable unless you can recover those keys manually from another verified client. - -`Legacy Matrix encrypted state detected at ... but no device ID was found for account "..."` - -- Meaning: the old crypto store does not record which Matrix device it belonged to, so OpenClaw cannot inspect it safely. -- What to do: old encrypted history cannot be recovered automatically; OpenClaw continues without it. - -`Legacy Matrix encrypted state for account "..." has backed-up room keys, but no local backup decryption key was found. Ask the operator to run "openclaw matrix verify backup restore --recovery-key " after upgrade if they have the recovery key.` - -- Meaning: backup exists, but OpenClaw could not recover the recovery key automatically. -- What to do: run `printf '%s\n' "$MATRIX_RECOVERY_KEY" | openclaw matrix verify backup restore --recovery-key-stdin` (preferred over passing the key as an argument). - -`Failed inspecting legacy Matrix encrypted state for account "..." (...): ...` - -- Meaning: OpenClaw found the old encrypted store, but it could not inspect it safely enough to prepare recovery. -- What to do: rerun `openclaw doctor --fix`. If it repeats, keep the old state directory intact and recover using another verified Matrix client plus `printf '%s\n' "$MATRIX_RECOVERY_KEY" | openclaw matrix verify backup restore --recovery-key-stdin`. - -`Legacy Matrix backup key was found for account "...", but Matrix SQLite state already contains a different recovery key. Leaving the existing state unchanged.` - -- Meaning: OpenClaw detected a backup key conflict and refused to overwrite the current recovery-key state automatically. -- What to do: verify which recovery key is correct before retrying any restore command. - -`Legacy Matrix encrypted state for account "..." cannot be fully converted automatically because the old rust crypto store does not expose all local room keys for export.` - -- Meaning: this is the hard limit of the old storage format. -- What to do: backed-up keys can still be restored, but local-only encrypted history may remain unavailable. - -`matrix: failed restoring room keys from legacy encrypted-state backup: ...` - -- Meaning: the new plugin attempted restore but Matrix returned an error. -- What to do: run `openclaw matrix verify backup status`, then retry with `printf '%s\n' "$MATRIX_RECOVERY_KEY" | openclaw matrix verify backup restore --recovery-key-stdin` if needed. +- Meaning: your plugin install record points at a local path that is gone. +- What to do: reinstall with `openclaw plugins install @openclaw/matrix`, or if you are running from a repo checkout, `openclaw plugins install ./path/to/local/matrix-plugin`. `openclaw doctor --fix` can also remove the stale Matrix plugin references for you. ### Manual recovery messages @@ -291,13 +159,6 @@ current backup baseline with `openclaw matrix verify backup reset --yes`. When t stored backup secret is broken, that reset also repairs secret storage so the new backup key can load correctly after restart. -### Custom plugin install messages - -`Matrix is installed from a custom path that no longer exists: ...` - -- Meaning: your plugin install record points at a local path that is gone. -- What to do: reinstall with `openclaw plugins install @openclaw/matrix`, or if you are running from a repo checkout, `openclaw plugins install ./path/to/local/matrix-plugin`. `openclaw doctor --fix` can also remove the stale Matrix plugin references for you. - ## If encrypted history still does not come back Run these checks in order: diff --git a/docs/docs_map.md b/docs/docs_map.md index b24e944ea359..24f46e696cf4 100644 --- a/docs/docs_map.md +++ b/docs/docs_map.md @@ -538,14 +538,10 @@ Do not edit it by hand; run `pnpm docs:map:gen`. - Route: /channels/matrix-migration - Headings: - H2: What the migration does automatically - - H2: What the migration cannot do automatically + - H2: Upgrading from OpenClaw releases older than 2026.4 - H2: Recommended upgrade flow - - H2: How encrypted migration works - H2: Common messages and what they mean - - H3: Upgrade and detection messages - - H3: Encrypted-state recovery messages - H3: Manual recovery messages - - H3: Custom plugin install messages - H2: If encrypted history still does not come back - H2: If you want to start fresh for future messages - H2: Related diff --git a/docs/tools/diffs.md b/docs/tools/diffs.md index 4474c9f48ed6..1ab2d49da0cc 100644 --- a/docs/tools/diffs.md +++ b/docs/tools/diffs.md @@ -126,16 +126,6 @@ All fields are optional unless noted. - - Still accepted for backward compatibility: - - - `format` -> `fileFormat` - - `imageFormat` -> `fileFormat` - - `imageQuality` -> `fileQuality` - - `imageScale` -> `fileScale` - - `imageMaxWidth` -> `fileMaxWidth` - - - `before`/`after`: max 512 KiB each. - `patch`: max 2 MiB. @@ -200,22 +190,13 @@ All successful results include `changed`: identical before/after input returns ` - `fileMaxWidth` - - - `format` (= `fileFormat`) - - `imagePath` (= `filePath`) - - `imageBytes` (= `fileBytes`) - - `imageQuality` (= `fileQuality`) - - `imageScale` (= `fileScale`) - - `imageMaxWidth` (= `fileMaxWidth`) - - -| Mode | Returns | -| -------- | ------------------------------------------------------------------------------------------------------------ | -| `"view"` | Viewer fields only. | -| `"file"` | File fields only, no viewer artifact. | -| `"both"` | Viewer fields plus file fields. If file rendering fails, viewer still returns with `fileError`/`imageError`. | +| Mode | Returns | +| -------- | ----------------------------------------------------------------------------------------------- | +| `"view"` | Viewer fields only. | +| `"file"` | File fields only, no viewer artifact. | +| `"both"` | Viewer fields plus file fields. If file rendering fails, viewer still returns with `fileError`. | ### Collapsed unchanged sections diff --git a/extensions/diffs/README.md b/extensions/diffs/README.md index be7a5710575e..5960f06a441a 100644 --- a/extensions/diffs/README.md +++ b/extensions/diffs/README.md @@ -76,14 +76,6 @@ Useful options: - `baseUrl`: override the gateway base URL used in the returned viewer link (origin or origin+base path only; no query/hash) - `viewerBaseUrl` plugin config: persistent fallback used when a tool call omits `baseUrl` -Legacy input aliases still accepted for backward compatibility: - -- `format` -> `fileFormat` -- `imageFormat` -> `fileFormat` -- `imageQuality` -> `fileQuality` -- `imageScale` -> `fileScale` -- `imageMaxWidth` -> `fileMaxWidth` - Input safety limits: - `before` and `after`: max 512 KiB each diff --git a/extensions/diffs/src/config.test.ts b/extensions/diffs/src/config.test.ts index 3dbaf619067a..f0b7e0873cfa 100644 --- a/extensions/diffs/src/config.test.ts +++ b/extensions/diffs/src/config.test.ts @@ -181,7 +181,6 @@ describe("resolveDiffsPluginDefaults", () => { expect(resolveDiffImageRenderOptions({ defaults }).format).toBe("pdf"); expect(resolveDiffImageRenderOptions({ defaults, fileFormat: "png" }).format).toBe("png"); - expect(resolveDiffImageRenderOptions({ defaults, format: "png" }).format).toBe("png"); }); it("accepts format as a config alias for fileFormat", () => { diff --git a/extensions/diffs/src/config.ts b/extensions/diffs/src/config.ts index 2e8b481257f8..4f651fc1ac0b 100644 --- a/extensions/diffs/src/config.ts +++ b/extensions/diffs/src/config.ts @@ -388,14 +388,9 @@ function normalizeTtlSeconds(ttlSeconds?: number): number { export function resolveDiffImageRenderOptions(params: { defaults: DiffFileDefaults; fileFormat?: DiffOutputFormat; - format?: DiffOutputFormat; fileQuality?: DiffImageQualityPreset; fileScale?: number; fileMaxWidth?: number; - imageFormat?: DiffOutputFormat; - imageQuality?: DiffImageQualityPreset; - imageScale?: number; - imageMaxWidth?: number; }): { format: DiffOutputFormat; qualityPreset: DiffImageQualityPreset; @@ -403,22 +398,17 @@ export function resolveDiffImageRenderOptions(params: { maxWidth: number; maxPixels: number; } { - const format = normalizeFileFormat( - params.fileFormat ?? params.imageFormat ?? params.format ?? params.defaults.fileFormat, - ); - const qualityOverrideProvided = - params.fileQuality !== undefined || params.imageQuality !== undefined; - const qualityPreset = normalizeFileQuality( - params.fileQuality ?? params.imageQuality ?? params.defaults.fileQuality, - ); + const format = normalizeFileFormat(params.fileFormat ?? params.defaults.fileFormat); + const qualityOverrideProvided = params.fileQuality !== undefined; + const qualityPreset = normalizeFileQuality(params.fileQuality ?? params.defaults.fileQuality); const profile = DEFAULT_IMAGE_QUALITY_PROFILES[qualityPreset]; const scale = normalizeFileScale( - params.fileScale ?? params.imageScale, + params.fileScale, qualityOverrideProvided ? profile.scale : params.defaults.fileScale, ); const maxWidth = normalizeFileMaxWidth( - params.fileMaxWidth ?? params.imageMaxWidth, + params.fileMaxWidth, qualityOverrideProvided ? profile.maxWidth : params.defaults.fileMaxWidth, ); diff --git a/extensions/diffs/src/tool.test.ts b/extensions/diffs/src/tool.test.ts index 9bab645743d7..26fe9c702f45 100644 --- a/extensions/diffs/src/tool.test.ts +++ b/extensions/diffs/src/tool.test.ts @@ -165,14 +165,10 @@ describe("diffs tool", () => { expect(result?.content).toHaveLength(1); const details = readDetails(result); expect(requireString(details.filePath, "filePath")).toMatch(/preview\.png$/); - expect(requireString(details.imagePath, "imagePath")).toMatch(/preview\.png$/); - expect(details.format).toBe("png"); + expect(details.fileFormat).toBe("png"); expect(details.fileQuality).toBe("standard"); - expect(details.imageQuality).toBe("standard"); expect(details.fileScale).toBe(2); - expect(details.imageScale).toBe(2); expect(details.fileMaxWidth).toBe(960); - expect(details.imageMaxWidth).toBe(960); expect(details.viewerUrl).toBeUndefined(); expect(cleanupSpy).toHaveBeenCalledTimes(1); }); @@ -200,7 +196,7 @@ describe("diffs tool", () => { expect(screenshotter["screenshotHtml"]).toHaveBeenCalledTimes(1); expect(readTextContent(result, 0)).toContain("Diff PDF generated at:"); - expect((result.details as Record).format).toBe("pdf"); + expect((result.details as Record).fileFormat).toBe("pdf"); expect((result.details as Record).filePath).toMatch(/preview\.pdf$/); }); @@ -302,52 +298,6 @@ describe("diffs tool", () => { } }); - it("accepts image* tool options for backward compatibility", async () => { - const screenshotter = createPngScreenshotter({ - assertImage: (image) => { - expect(image.qualityPreset).toBe("hq"); - expect(image.scale).toBe(2.4); - expect(image.maxWidth).toBe(1100); - }, - }); - - const tool = createToolWithScreenshotter(store, screenshotter); - - const result = await tool.execute?.("tool-2legacy", { - before: "one\n", - after: "two\n", - mode: "file", - imageQuality: "hq", - imageScale: "2.4", - imageMaxWidth: "1100", - }); - - expect((result.details as Record).fileQuality).toBe("hq"); - expect((result.details as Record).fileScale).toBe(2.4); - expect((result.details as Record).fileMaxWidth).toBe(1100); - }); - - it("accepts deprecated format alias for fileFormat", async () => { - const screenshotter = createPdfScreenshotter(); - - const tool = createDiffsTool({ - api: createApi(), - store, - defaults: DEFAULT_DIFFS_TOOL_DEFAULTS, - screenshotter, - }); - - const result = await tool.execute?.("tool-2format", { - before: "one\n", - after: "two\n", - mode: "file", - format: "pdf", - }); - - expect((result.details as Record).fileFormat).toBe("pdf"); - expect((result.details as Record).filePath).toMatch(/preview\.pdf$/); - }); - it("honors defaults.mode=file when mode is omitted", async () => { const screenshotter = createPngScreenshotter(); const tool = createToolWithScreenshotter(store, screenshotter, { @@ -384,7 +334,6 @@ describe("diffs tool", () => { expect(result?.content).toHaveLength(1); expect(readTextContent(result, 0)).toContain("File rendering failed"); expect((result.details as Record).fileError).toBe("browser missing"); - expect((result.details as Record).imageError).toBe("browser missing"); }); it("rejects invalid base URLs as tool input errors", async () => { @@ -539,7 +488,7 @@ describe("diffs tool", () => { expect((result.details as Record).mode).toBe("both"); expect(screenshotter["screenshotHtml"]).toHaveBeenCalledTimes(1); - expect((result.details as Record).format).toBe("png"); + expect((result.details as Record).fileFormat).toBe("png"); expect((result.details as Record).fileQuality).toBe("print"); expect((result.details as Record).fileScale).toBe(2.75); expect((result.details as Record).fileMaxWidth).toBe(1320); diff --git a/extensions/diffs/src/tool.ts b/extensions/diffs/src/tool.ts index 593ed28ab9a0..9a5b6f51e0fb 100644 --- a/extensions/diffs/src/tool.ts +++ b/extensions/diffs/src/tool.ts @@ -95,34 +95,6 @@ const DiffsToolSchema = Type.Object( minimum: 640, maximum: 2400, }), - /** @deprecated Use fileQuality. */ - imageQuality: Type.Optional( - stringEnum(DIFF_IMAGE_QUALITY_PRESETS, { - description: "Deprecated alias for fileQuality.", - deprecated: true, - }), - ), - /** @deprecated Use fileFormat. */ - imageFormat: Type.Optional( - stringEnum(DIFF_OUTPUT_FORMATS, { - description: "Deprecated alias for fileFormat.", - deprecated: true, - }), - ), - /** @deprecated Use fileScale. */ - imageScale: optionalFiniteNumberSchema({ - description: "Deprecated alias for fileScale.", - deprecated: true, - minimum: 1, - maximum: 4, - }), - /** @deprecated Use fileMaxWidth. */ - imageMaxWidth: optionalFiniteNumberSchema({ - description: "Deprecated alias for fileMaxWidth.", - deprecated: true, - minimum: 640, - maximum: 2400, - }), expandUnchanged: Type.Optional( Type.Boolean({ description: "Expand unchanged sections instead of collapsing them." }), ), @@ -142,10 +114,6 @@ const DiffsToolSchema = Type.Object( ); type DiffsToolParams = Static; -type DiffsToolRawParams = DiffsToolParams & { - /** @deprecated Use fileFormat. */ - format?: DiffOutputFormat; -}; export function createDiffsTool(params: { api: OpenClawPluginApi; @@ -163,7 +131,7 @@ export function createDiffsTool(params: { "Create a read-only diff viewer from before/after text or a unified patch. Returns a gateway viewer URL for canvas use and can also render the same diff to a PNG or PDF.", parameters: DiffsToolSchema, execute: async (_toolCallId, rawParams) => { - const toolParams = rawParams as DiffsToolRawParams; + const toolParams = rawParams as DiffsToolParams; const rawRecord = rawParams as Record; const artifactContext = buildArtifactContext(params.context); const input = normalizeDiffInput(toolParams); @@ -187,19 +155,13 @@ export function createDiffsTool(params: { const expandUnchanged = toolParams.expandUnchanged === true; const ttlSeconds = readFiniteNumberParam(rawRecord, "ttlSeconds") ?? params.defaults.ttlSeconds; - const fileScale = - readFiniteNumberParam(rawRecord, "fileScale") ?? - readFiniteNumberParam(rawRecord, "imageScale"); - const fileMaxWidth = - readFiniteNumberParam(rawRecord, "fileMaxWidth") ?? - readFiniteNumberParam(rawRecord, "imageMaxWidth"); + const fileScale = readFiniteNumberParam(rawRecord, "fileScale"); + const fileMaxWidth = readFiniteNumberParam(rawRecord, "fileMaxWidth"); const ttlMs = normalizeTtlMs(ttlSeconds); const image = resolveDiffImageRenderOptions({ defaults: params.defaults, - fileFormat: normalizeOutputFormat( - toolParams.fileFormat ?? toolParams.imageFormat ?? toolParams.format, - ), - fileQuality: normalizeFileQuality(toolParams.fileQuality ?? toolParams.imageQuality), + fileFormat: normalizeOutputFormat(toolParams.fileFormat), + fileQuality: normalizeFileQuality(toolParams.fileQuality), fileScale, fileMaxWidth, }); @@ -347,7 +309,6 @@ export function createDiffsTool(params: { details: { ...baseDetails, fileError: errorMessage, - imageError: errorMessage, }, }; } @@ -396,18 +357,13 @@ function buildArtifactDetails(params: { return { ...params.baseDetails, filePath: params.artifactFile.path, - imagePath: params.artifactFile.path, + // `path` mirrors filePath so the message tool can send the artifact directly. path: params.artifactFile.path, fileBytes: params.artifactFile.bytes, - imageBytes: params.artifactFile.bytes, - format: params.image.format, fileFormat: params.image.format, fileQuality: params.image.qualityPreset, - imageQuality: params.image.qualityPreset, fileScale: params.image.scale, - imageScale: params.image.scale, fileMaxWidth: params.image.maxWidth, - imageMaxWidth: params.image.maxWidth, }; } diff --git a/extensions/discord/src/monitor/model-picker-preferences-migrations.ts b/extensions/discord/src/monitor/model-picker-preferences-migrations.ts index dfdeb27e8883..a00f7f98d292 100644 --- a/extensions/discord/src/monitor/model-picker-preferences-migrations.ts +++ b/extensions/discord/src/monitor/model-picker-preferences-migrations.ts @@ -125,6 +125,44 @@ function legacyUpdatedAtForIndex(updatedAt: unknown, index: number, total: numbe ); } +function readFiniteNumberField(entry: Record, key: string): number | undefined { + const value = entry[key]; + return typeof value === "number" && Number.isFinite(value) ? Math.floor(value) : undefined; +} + +// Doctor-owned legacy-shape repair: pre-account-scoped stores carried a flat +// `sessionKey` alias and an absolute `expiresAt`. Runtime normalization reads +// canonical fields only, so the one-time JSON import maps them here. +function upgradeLegacyThreadBindingShape(rawEntry: unknown): unknown { + if (!rawEntry || typeof rawEntry !== "object" || Array.isArray(rawEntry)) { + return rawEntry; + } + const entry = { ...(rawEntry as Record) }; + if (entry.targetSessionKey === undefined && typeof entry.sessionKey === "string") { + entry.targetSessionKey = entry.sessionKey; + } + delete entry.sessionKey; + const expiresAt = readFiniteNumberField(entry, "expiresAt"); + delete entry.expiresAt; + if ( + entry.idleTimeoutMs === undefined && + entry.maxAgeMs === undefined && + expiresAt !== undefined + ) { + // Legacy expiresAt was an absolute timestamp; map it to max-age and disable idle timeout. + entry.idleTimeoutMs = 0; + if (expiresAt <= 0) { + entry.maxAgeMs = 0; + } else { + const boundAt = readFiniteNumberField(entry, "boundAt") ?? 0; + const lastActivityAt = readFiniteNumberField(entry, "lastActivityAt") ?? 0; + const base = boundAt > 0 ? boundAt : lastActivityAt; + entry.maxAgeMs = Math.max(1, expiresAt - Math.max(0, base)); + } + } + return entry; +} + export const detectDiscordLegacyStateMigrations: BundledChannelLegacyStateMigrationDetector = ({ stateDir, }) => { @@ -193,7 +231,10 @@ export const detectDiscordLegacyStateMigrations: BundledChannelLegacyStateMigrat for (const [rawKey, rawEntry] of Object.entries( store.bindings as Record, )) { - const normalized = normalizePersistedBinding(rawKey, rawEntry); + const normalized = normalizePersistedBinding( + rawKey, + upgradeLegacyThreadBindingShape(rawEntry), + ); if (normalized) { out.push({ key: toBindingRecordKey(normalized), diff --git a/extensions/discord/src/monitor/thread-bindings.lifecycle.test.ts b/extensions/discord/src/monitor/thread-bindings.lifecycle.test.ts index e82e5f02cbe0..531257e15ec2 100644 --- a/extensions/discord/src/monitor/thread-bindings.lifecycle.test.ts +++ b/extensions/discord/src/monitor/thread-bindings.lifecycle.test.ts @@ -1876,97 +1876,6 @@ describe("thread binding lifecycle", () => { expect(maxInFlight).toBeLessThanOrEqual(PROBE_LIMIT); }); - it("migrates legacy expiresAt bindings to idle/max-age semantics", () => { - const previousStateDir = process.env.OPENCLAW_STATE_DIR; - const stateDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-thread-bindings-")); - process.env.OPENCLAW_STATE_DIR = stateDir; - try { - testing.resetThreadBindingsForTests(); - const boundAt = Date.now() - 10_000; - const expiresAt = boundAt + 60_000; - const store = createPluginStateSyncKeyedStoreForTests("discord", { - namespace: "thread-bindings", - maxEntries: 10_000, - }); - store.register("default:thread-legacy-active", { - accountId: "default", - channelId: "parent-1", - threadId: "thread-legacy-active", - targetKind: "subagent", - targetSessionKey: "agent:main:subagent:legacy-active", - agentId: "main", - boundBy: "system", - boundAt, - expiresAt, - }); - store.register("default:thread-legacy-disabled", { - accountId: "default", - channelId: "parent-1", - threadId: "thread-legacy-disabled", - targetKind: "subagent", - targetSessionKey: "agent:main:subagent:legacy-disabled", - agentId: "main", - boundBy: "system", - boundAt, - expiresAt: 0, - }); - - const manager = createTestThreadBindingManager({ - accountId: "default", - persist: false, - enableSweeper: false, - idleTimeoutMs: 24 * 60 * 60 * 1000, - maxAgeMs: 0, - }); - - const active = manager.getByThreadId("thread-legacy-active"); - if (!active) { - throw new Error("missing migrated legacy active thread binding"); - } - expect(active.idleTimeoutMs).toBe(0); - expect(active.maxAgeMs).toBe(expiresAt - boundAt); - expect( - resolveThreadBindingMaxAgeExpiresAt({ - record: active, - defaultMaxAgeMs: manager.getMaxAgeMs(), - }), - ).toBe(expiresAt); - expect( - resolveThreadBindingInactivityExpiresAt({ - record: active, - defaultIdleTimeoutMs: manager.getIdleTimeoutMs(), - }), - ).toBeUndefined(); - - const disabled = manager.getByThreadId("thread-legacy-disabled"); - if (!disabled) { - throw new Error("missing migrated legacy disabled thread binding"); - } - expect(disabled.idleTimeoutMs).toBe(0); - expect(disabled.maxAgeMs).toBe(0); - expect( - resolveThreadBindingMaxAgeExpiresAt({ - record: disabled, - defaultMaxAgeMs: manager.getMaxAgeMs(), - }), - ).toBeUndefined(); - expect( - resolveThreadBindingInactivityExpiresAt({ - record: disabled, - defaultIdleTimeoutMs: manager.getIdleTimeoutMs(), - }), - ).toBeUndefined(); - } finally { - testing.resetThreadBindingsForTests(); - if (previousStateDir === undefined) { - delete process.env.OPENCLAW_STATE_DIR; - } else { - process.env.OPENCLAW_STATE_DIR = previousStateDir; - } - fs.rmSync(stateDir, { recursive: true, force: true }); - } - }); - it("persists unbinds even when no manager is active", () => { const previousStateDir = process.env.OPENCLAW_STATE_DIR; const stateDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-thread-bindings-")); diff --git a/extensions/discord/src/monitor/thread-bindings.state.ts b/extensions/discord/src/monitor/thread-bindings.state.ts index 5adc0461c91c..afa64c09548e 100644 --- a/extensions/discord/src/monitor/thread-bindings.state.ts +++ b/extensions/discord/src/monitor/thread-bindings.state.ts @@ -156,10 +156,7 @@ export function normalizePersistedBinding( const value = raw as Partial; const threadId = normalizeThreadId(value.threadId ?? threadIdKey); const channelId = normalizeOptionalString(value.channelId) ?? ""; - const targetSessionKey = - normalizeOptionalString(value.targetSessionKey) ?? - normalizeOptionalString(value.sessionKey) ?? - ""; + const targetSessionKey = normalizeOptionalString(value.targetSessionKey) ?? ""; if (!threadId || !channelId || !targetSessionKey) { return null; } @@ -189,29 +186,6 @@ export function normalizePersistedBinding( : undefined; const metadata = value.metadata && typeof value.metadata === "object" ? { ...value.metadata } : undefined; - const legacyExpiresAt = - typeof (value as { expiresAt?: unknown }).expiresAt === "number" && - Number.isFinite((value as { expiresAt?: unknown }).expiresAt) - ? Math.max(0, Math.floor((value as { expiresAt?: number }).expiresAt ?? 0)) - : undefined; - - let migratedIdleTimeoutMs = idleTimeoutMs; - let migratedMaxAgeMs = maxAgeMs; - if ( - migratedIdleTimeoutMs === undefined && - migratedMaxAgeMs === undefined && - legacyExpiresAt != null - ) { - if (legacyExpiresAt <= 0) { - migratedIdleTimeoutMs = 0; - migratedMaxAgeMs = 0; - } else { - const baseBoundAt = boundAt > 0 ? boundAt : lastActivityAt; - // Legacy expiresAt represented an absolute timestamp; map it to max-age and disable idle timeout. - migratedIdleTimeoutMs = 0; - migratedMaxAgeMs = Math.max(1, legacyExpiresAt - Math.max(0, baseBoundAt)); - } - } const record: ThreadBindingRecord = { accountId, @@ -233,11 +207,11 @@ export function normalizePersistedBinding( if (webhookToken !== undefined) { record.webhookToken = webhookToken; } - if (migratedIdleTimeoutMs !== undefined) { - record.idleTimeoutMs = migratedIdleTimeoutMs; + if (idleTimeoutMs !== undefined) { + record.idleTimeoutMs = idleTimeoutMs; } - if (migratedMaxAgeMs !== undefined) { - record.maxAgeMs = migratedMaxAgeMs; + if (maxAgeMs !== undefined) { + record.maxAgeMs = maxAgeMs; } if (metadata !== undefined) { record.metadata = metadata; diff --git a/extensions/discord/src/monitor/thread-bindings.types.ts b/extensions/discord/src/monitor/thread-bindings.types.ts index 043ee3f17505..f05dbfa0b923 100644 --- a/extensions/discord/src/monitor/thread-bindings.types.ts +++ b/extensions/discord/src/monitor/thread-bindings.types.ts @@ -21,11 +21,7 @@ export type ThreadBindingRecord = { metadata?: Record; }; -export type PersistedThreadBindingRecord = ThreadBindingRecord & { - sessionKey?: string; - /** @deprecated Legacy absolute expiry timestamp; migrated on load. */ - expiresAt?: number; -}; +export type PersistedThreadBindingRecord = ThreadBindingRecord; export type ThreadBindingManager = { accountId: string; diff --git a/extensions/matrix/api.ts b/extensions/matrix/api.ts index 435701091485..683d3b4fc2c3 100644 --- a/extensions/matrix/api.ts +++ b/extensions/matrix/api.ts @@ -21,8 +21,6 @@ export { resolveMatrixCredentialsFilename, resolveMatrixCredentialsPath, resolveMatrixHomeserverKey, - resolveMatrixLegacyFlatStoragePaths, - resolveMatrixLegacyFlatStoreRoot, sanitizeMatrixPathSegment, } from "./src/storage-paths.js"; export { diff --git a/extensions/matrix/runtime-api.ts b/extensions/matrix/runtime-api.ts index d919b8e35dd6..721b53b650cc 100644 --- a/extensions/matrix/runtime-api.ts +++ b/extensions/matrix/runtime-api.ts @@ -25,8 +25,6 @@ export { resolveMatrixCredentialsFilename, resolveMatrixCredentialsPath, resolveMatrixHomeserverKey, - resolveMatrixLegacyFlatStoragePaths, - resolveMatrixLegacyFlatStoreRoot, sanitizeMatrixPathSegment, } from "./src/storage-paths.js"; export { ensureMatrixSdkInstalled, isMatrixSdkAvailable } from "./src/matrix/deps.js"; diff --git a/extensions/matrix/runtime-heavy-api.ts b/extensions/matrix/runtime-heavy-api.ts deleted file mode 100644 index 1ff51fef7c36..000000000000 --- a/extensions/matrix/runtime-heavy-api.ts +++ /dev/null @@ -1,2 +0,0 @@ -// Matrix API module exposes the plugin public contract. -export * from "./src/matrix-migration.runtime.js"; diff --git a/extensions/matrix/src/channel.ts b/extensions/matrix/src/channel.ts index e5f162645342..a54632787950 100644 --- a/extensions/matrix/src/channel.ts +++ b/extensions/matrix/src/channel.ts @@ -78,7 +78,6 @@ import { singleAccountKeysToMove, } from "./setup-contract.js"; import { createMatrixSetupWizardProxy, matrixSetupAdapter } from "./setup-core.js"; -import { runMatrixStartupMaintenance } from "./startup-maintenance.js"; import { defaultTopLevelPlacement, resolveMatrixInboundConversation, @@ -622,9 +621,6 @@ export const matrixPlugin: ChannelPlugin = }, }, doctor: matrixDoctor, - lifecycle: { - runStartupMaintenance: runMatrixStartupMaintenance, - }, heartbeat: { sendTyping: async ({ cfg, to, accountId }) => { await ( diff --git a/extensions/matrix/src/doctor.test.ts b/extensions/matrix/src/doctor.test.ts index ff322e102283..46869717fbed 100644 --- a/extensions/matrix/src/doctor.test.ts +++ b/extensions/matrix/src/doctor.test.ts @@ -4,33 +4,11 @@ import { tmpdir } from "node:os"; import path from "node:path"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { - applyMatrixDoctorRepair, cleanStaleMatrixPluginConfig, collectMatrixInstallPathWarnings, - formatMatrixLegacyCryptoPreview, - formatMatrixLegacyStatePreview, matrixDoctor, - runMatrixDoctorSequence, } from "./doctor.js"; -vi.mock("./matrix-migration.runtime.js", async () => { - const actual = await vi.importActual( - "./matrix-migration.runtime.js", - ); - return { - ...actual, - maybeCreateMatrixMigrationSnapshot: vi.fn(), - autoMigrateLegacyMatrixState: vi.fn(async () => ({ changes: [], warnings: [] })), - autoPrepareLegacyMatrixCrypto: vi.fn(async () => ({ changes: [], warnings: [] })), - resolveMatrixMigrationStatus: vi.fn(() => ({ - legacyState: null, - legacyCrypto: { inspectorAvailable: true, warnings: [], plans: [] }, - pending: false, - actionable: false, - })), - }; -}); - describe("matrix doctor", () => { beforeEach(() => { vi.clearAllMocks(); @@ -62,42 +40,6 @@ describe("matrix doctor", () => { expect(changes.join("\n")).toContain(fragment); } - it("formats state and crypto previews", () => { - expect( - formatMatrixLegacyStatePreview({ - accountId: "default", - legacyStoragePath: "/tmp/legacy-sync.json", - targetStoragePath: "/tmp/new-sync.json", - legacyCryptoPath: "/tmp/legacy-crypto.json", - targetCryptoPath: "/tmp/new-crypto.json", - selectionNote: "Picked the newest account.", - targetRootDir: "/tmp/account-root", - }), - ).toContain("Matrix plugin upgraded in place."); - - const previews = formatMatrixLegacyCryptoPreview({ - inspectorAvailable: true, - warnings: ["matrix warning"], - plans: [ - { - accountId: "default", - rootDir: "/tmp/account-root", - homeserver: "https://matrix.example.org", - userId: "@bot:example.org", - accessToken: "tok-123", - deviceId: "DEVICE123", - legacyCryptoPath: "/tmp/legacy-crypto.json", - recoveryKeyPath: "/tmp/recovery-key.txt", - statePath: "/tmp/state.json", - }, - ], - }); - expect(previews[0]).toBe("- matrix warning"); - expect(previews[1]).toContain("/tmp/recovery-key.txt"); - expect(previews[1]).toContain("Recovery key state: Matrix SQLite state"); - expect(previews[1]).toContain("Migration state: Matrix SQLite state"); - }); - it("warns on stale custom Matrix plugin paths and cleans them", async () => { const missingPath = path.join(tmpdir(), `openclaw-matrix-missing-${Date.now()}`); await fs.rm(missingPath, { recursive: true, force: true }); @@ -125,47 +67,6 @@ describe("matrix doctor", () => { expect(cleaned.config.plugins?.allow).toEqual(["other-plugin"]); }); - it("surfaces matrix sequence warnings and repair changes", async () => { - const runtimeApi = await import("./matrix-migration.runtime.js"); - vi.mocked(runtimeApi.resolveMatrixMigrationStatus).mockReturnValue({ - legacyState: null, - legacyCrypto: { inspectorAvailable: true, warnings: [], plans: [] }, - pending: true, - actionable: true, - }); - vi.mocked(runtimeApi.maybeCreateMatrixMigrationSnapshot).mockResolvedValue({ - archivePath: "/tmp/matrix-backup.tgz", - created: true, - markerPath: "/tmp/marker.json", - }); - vi.mocked(runtimeApi.autoMigrateLegacyMatrixState).mockResolvedValue({ - migrated: true, - changes: ["Migrated legacy sync state"], - warnings: [], - }); - vi.mocked(runtimeApi.autoPrepareLegacyMatrixCrypto).mockResolvedValue({ - migrated: true, - changes: ["Prepared recovery key export"], - warnings: [], - }); - - const cfg = { - channels: { - matrix: {}, - }, - } as never; - - const repair = await applyMatrixDoctorRepair({ cfg, env: process.env }); - expect(repair.changes.join("\n")).toContain("Matrix migration snapshot"); - - const sequence = await runMatrixDoctorSequence({ - cfg, - env: process.env, - shouldRepair: true, - }); - expect(sequence.changeNotes.join("\n")).toContain("Matrix migration snapshot"); - }); - it("normalizes legacy Matrix room allow aliases to enabled", () => { const result = runMatrixCompatibilityNormalize({ cfg: { diff --git a/extensions/matrix/src/doctor.ts b/extensions/matrix/src/doctor.ts index 438204018c1d..65ba0602b03e 100644 --- a/extensions/matrix/src/doctor.ts +++ b/extensions/matrix/src/doctor.ts @@ -10,76 +10,6 @@ import { legacyConfigRules as MATRIX_LEGACY_CONFIG_RULES, normalizeCompatibilityConfig as normalizeMatrixCompatibilityConfig, } from "./doctor-contract.js"; -import { - autoMigrateLegacyMatrixState, - autoPrepareLegacyMatrixCrypto, - detectLegacyMatrixCrypto, - detectLegacyMatrixState, - maybeCreateMatrixMigrationSnapshot, - resolveMatrixMigrationStatus, -} from "./matrix-migration.runtime.js"; -import { isRecord } from "./record-shared.js"; - -function hasConfiguredMatrixChannel(cfg: OpenClawConfig): boolean { - const channels = cfg.channels as Record | undefined; - return isRecord(channels?.matrix); -} - -function hasConfiguredMatrixPluginSurface(cfg: OpenClawConfig): boolean { - return Boolean( - cfg.plugins?.installs?.matrix || - cfg.plugins?.entries?.matrix || - cfg.plugins?.allow?.includes("matrix") || - cfg.plugins?.deny?.includes("matrix"), - ); -} - -function hasConfiguredMatrixEnv(env: NodeJS.ProcessEnv): boolean { - return Object.entries(env).some( - ([key, value]) => key.startsWith("MATRIX_") && typeof value === "string" && value.trim(), - ); -} - -function configMayNeedMatrixDoctorSequence(cfg: OpenClawConfig, env: NodeJS.ProcessEnv): boolean { - return ( - hasConfiguredMatrixChannel(cfg) || - hasConfiguredMatrixPluginSurface(cfg) || - hasConfiguredMatrixEnv(env) - ); -} - -export function formatMatrixLegacyStatePreview( - detection: Exclude, null | { warning: string }>, -): string { - return [ - "- Matrix plugin upgraded in place.", - `- Legacy sync store: ${detection.legacyStoragePath} -> ${detection.targetStoragePath}`, - `- Legacy crypto store: ${detection.legacyCryptoPath} -> ${detection.targetCryptoPath}`, - ...(detection.selectionNote ? [`- ${detection.selectionNote}`] : []), - '- Run "openclaw doctor --fix" to migrate this Matrix state now.', - ].join("\n"); -} - -export function formatMatrixLegacyCryptoPreview( - detection: ReturnType, -): string[] { - const notes: string[] = []; - for (const warning of detection.warnings) { - notes.push(`- ${warning}`); - } - for (const plan of detection.plans) { - notes.push( - [ - `- Matrix encrypted-state migration is pending for account "${plan.accountId}".`, - `- Legacy crypto store: ${plan.legacyCryptoPath}`, - `- Recovery key state: Matrix SQLite state (imports ${plan.recoveryKeyPath} if present)`, - `- Migration state: Matrix SQLite state (imports ${plan.statePath} if present)`, - '- Run "openclaw doctor --fix" to extract any saved backup key now. Backed-up room keys will restore automatically on next gateway start.', - ].join("\n"), - ); - } - return notes; -} export async function collectMatrixInstallPathWarnings(cfg: OpenClawConfig): Promise { const issue = await detectPluginInstallPathIssue({ @@ -129,125 +59,17 @@ export async function cleanStaleMatrixPluginConfig(cfg: OpenClawConfig) { }; } -export async function applyMatrixDoctorRepair(params: { - cfg: OpenClawConfig; - env: NodeJS.ProcessEnv; -}): Promise<{ changes: string[]; warnings: string[] }> { - const changes: string[] = []; - const warnings: string[] = []; - const migrationStatus = resolveMatrixMigrationStatus({ - cfg: params.cfg, - env: params.env, - }); - - let matrixSnapshotReady = true; - if (migrationStatus.actionable) { - try { - const snapshot = await maybeCreateMatrixMigrationSnapshot({ - trigger: "doctor-fix", - env: params.env, - }); - changes.push( - `Matrix migration snapshot ${snapshot.created ? "created" : "reused"} before applying Matrix upgrades.\n- ${snapshot.archivePath}`, - ); - } catch (error) { - matrixSnapshotReady = false; - warnings.push( - `- Failed creating a Matrix migration snapshot before repair: ${String(error)}`, - ); - warnings.push( - '- Skipping Matrix migration changes for now. Resolve the snapshot failure, then rerun "openclaw doctor --fix".', - ); - } - } else if (migrationStatus.pending) { - warnings.push( - "- Matrix migration warnings are present, but no on-disk Matrix mutation is actionable yet. No pre-migration snapshot was needed.", - ); - } - - if (!matrixSnapshotReady) { - return { changes, warnings }; - } - - const matrixStateRepair = await autoMigrateLegacyMatrixState({ - cfg: params.cfg, - env: params.env, - }); - if (matrixStateRepair.changes.length > 0) { - changes.push( - [ - "Matrix plugin upgraded in place.", - ...matrixStateRepair.changes.map((entry) => `- ${entry}`), - "- No user action required.", - ].join("\n"), - ); - } - if (matrixStateRepair.warnings.length > 0) { - warnings.push(matrixStateRepair.warnings.map((entry) => `- ${entry}`).join("\n")); - } - - const matrixCryptoRepair = await autoPrepareLegacyMatrixCrypto({ - cfg: params.cfg, - env: params.env, - }); - if (matrixCryptoRepair.changes.length > 0) { - changes.push( - [ - "Matrix encrypted-state migration prepared.", - ...matrixCryptoRepair.changes.map((entry) => `- ${entry}`), - ].join("\n"), - ); - } - if (matrixCryptoRepair.warnings.length > 0) { - warnings.push(matrixCryptoRepair.warnings.map((entry) => `- ${entry}`).join("\n")); - } - - return { changes, warnings }; -} - export async function runMatrixDoctorSequence(params: { cfg: OpenClawConfig; env: NodeJS.ProcessEnv; shouldRepair: boolean; }): Promise<{ changeNotes: string[]; warningNotes: string[] }> { const warningNotes: string[] = []; - const changeNotes: string[] = []; const installWarnings = await collectMatrixInstallPathWarnings(params.cfg); if (installWarnings.length > 0) { warningNotes.push(installWarnings.join("\n")); } - if (!configMayNeedMatrixDoctorSequence(params.cfg, params.env)) { - return { changeNotes, warningNotes }; - } - - if (params.shouldRepair) { - const repair = await applyMatrixDoctorRepair({ - cfg: params.cfg, - env: params.env, - }); - changeNotes.push(...repair.changes); - warningNotes.push(...repair.warnings); - } else { - const migrationStatus = resolveMatrixMigrationStatus({ - cfg: params.cfg, - env: params.env, - }); - if (migrationStatus.legacyState) { - if ("warning" in migrationStatus.legacyState) { - warningNotes.push(`- ${migrationStatus.legacyState.warning}`); - } else { - warningNotes.push(formatMatrixLegacyStatePreview(migrationStatus.legacyState)); - } - } - if ( - migrationStatus.legacyCrypto.warnings.length > 0 || - migrationStatus.legacyCrypto.plans.length > 0 - ) { - warningNotes.push(...formatMatrixLegacyCryptoPreview(migrationStatus.legacyCrypto)); - } - } - - return { changeNotes, warningNotes }; + return { changeNotes: [], warningNotes }; } export const matrixDoctor: ChannelDoctorAdapter = { diff --git a/extensions/matrix/src/legacy-crypto-inspector-availability.test.ts b/extensions/matrix/src/legacy-crypto-inspector-availability.test.ts deleted file mode 100644 index eaf2ec85cd47..000000000000 --- a/extensions/matrix/src/legacy-crypto-inspector-availability.test.ts +++ /dev/null @@ -1,84 +0,0 @@ -// Matrix tests cover legacy crypto inspector availability plugin behavior. -import path from "node:path"; -import { beforeEach, describe, expect, it, vi } from "vitest"; - -const availabilityState = vi.hoisted(() => ({ - currentFilePath: "/virtual/dist/matrix-migration.runtime.js", - existingPaths: new Set(), - dirEntries: [] as Array<{ name: string; isFile: () => boolean }>, -})); - -vi.mock("node:fs", async () => { - const { mockNodeBuiltinModule } = await import("openclaw/plugin-sdk/test-node-mocks"); - return mockNodeBuiltinModule( - () => vi.importActual("node:fs"), - { - existsSync: (candidate: unknown) => availabilityState.existingPaths.has(String(candidate)), - readdirSync: () => availabilityState.dirEntries as never, - }, - { mirrorToDefault: true }, - ); -}); - -vi.mock("node:url", async () => { - const actual = await vi.importActual("node:url"); - return { - ...actual, - fileURLToPath: () => availabilityState.currentFilePath, - }; -}); - -const { isMatrixLegacyCryptoInspectorAvailable } = - await import("./legacy-crypto-inspector-availability.js"); - -describe("isMatrixLegacyCryptoInspectorAvailable", () => { - beforeEach(() => { - availabilityState.currentFilePath = "/virtual/dist/matrix-migration.runtime.js"; - availabilityState.existingPaths.clear(); - availabilityState.dirEntries = []; - }); - - it("detects the source inspector module directly", () => { - availabilityState.currentFilePath = path.resolve( - "/virtual/extensions/matrix/src/legacy-crypto-inspector-availability.js", - ); - availabilityState.existingPaths.add( - path.resolve("/virtual/extensions/matrix/src/matrix/legacy-crypto-inspector.ts"), - ); - - expect(isMatrixLegacyCryptoInspectorAvailable()).toBe(true); - }); - - it("detects hashed built inspector chunks", () => { - availabilityState.dirEntries = [ - { - name: "legacy-crypto-inspector-TPlLnFSE.js", - isFile: () => true, - }, - ]; - - expect(isMatrixLegacyCryptoInspectorAvailable()).toBe(true); - }); - - it("does not confuse the availability helper artifact with the real inspector", () => { - availabilityState.dirEntries = [ - { - name: "legacy-crypto-inspector-availability.js", - isFile: () => true, - }, - ]; - - expect(isMatrixLegacyCryptoInspectorAvailable()).toBe(false); - }); - - it("does not confuse hashed availability helper chunks with the real inspector", () => { - availabilityState.dirEntries = [ - { - name: "legacy-crypto-inspector-availability-TPlLnFSE.js", - isFile: () => true, - }, - ]; - - expect(isMatrixLegacyCryptoInspectorAvailable()).toBe(false); - }); -}); diff --git a/extensions/matrix/src/legacy-crypto-inspector-availability.ts b/extensions/matrix/src/legacy-crypto-inspector-availability.ts deleted file mode 100644 index 5250d054a1bc..000000000000 --- a/extensions/matrix/src/legacy-crypto-inspector-availability.ts +++ /dev/null @@ -1,61 +0,0 @@ -// Matrix plugin module implements legacy crypto inspector availability behavior. -import fs from "node:fs"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; - -const LEGACY_CRYPTO_INSPECTOR_FILE = "legacy-crypto-inspector.js"; -const LEGACY_CRYPTO_INSPECTOR_CHUNK_PREFIX = "legacy-crypto-inspector-"; -const LEGACY_CRYPTO_INSPECTOR_HELPER_CHUNK_PREFIX = "availability-"; -const JAVASCRIPT_MODULE_SUFFIX = ".js"; - -function isLegacyCryptoInspectorArtifactName(name: string): boolean { - if (name === LEGACY_CRYPTO_INSPECTOR_FILE) { - return true; - } - if ( - !name.startsWith(LEGACY_CRYPTO_INSPECTOR_CHUNK_PREFIX) || - !name.endsWith(JAVASCRIPT_MODULE_SUFFIX) - ) { - return false; - } - const chunkSuffix = name.slice( - LEGACY_CRYPTO_INSPECTOR_CHUNK_PREFIX.length, - -JAVASCRIPT_MODULE_SUFFIX.length, - ); - return ( - chunkSuffix.length > 0 && - chunkSuffix !== "availability" && - !chunkSuffix.startsWith(LEGACY_CRYPTO_INSPECTOR_HELPER_CHUNK_PREFIX) - ); -} - -function hasSourceInspectorArtifact(currentDir: string): boolean { - return [ - path.resolve(currentDir, "matrix", "legacy-crypto-inspector.ts"), - path.resolve(currentDir, "matrix", "legacy-crypto-inspector.js"), - ].some((candidate) => fs.existsSync(candidate)); -} - -function hasBuiltInspectorArtifact(currentDir: string): boolean { - if (fs.existsSync(path.join(currentDir, "legacy-crypto-inspector.js"))) { - return true; - } - if (fs.existsSync(path.join(currentDir, "extensions", "matrix", "legacy-crypto-inspector.js"))) { - return true; - } - return fs - .readdirSync(currentDir, { withFileTypes: true }) - .some((entry) => entry.isFile() && isLegacyCryptoInspectorArtifactName(entry.name)); -} - -export function isMatrixLegacyCryptoInspectorAvailable(): boolean { - const currentDir = path.dirname(fileURLToPath(import.meta.url)); - if (hasSourceInspectorArtifact(currentDir)) { - return true; - } - try { - return hasBuiltInspectorArtifact(currentDir); - } catch { - return false; - } -} diff --git a/extensions/matrix/src/legacy-crypto.test.ts b/extensions/matrix/src/legacy-crypto.test.ts deleted file mode 100644 index b110c3c2ec92..000000000000 --- a/extensions/matrix/src/legacy-crypto.test.ts +++ /dev/null @@ -1,243 +0,0 @@ -// Matrix tests cover legacy crypto plugin behavior. -import fs from "node:fs"; -import path from "node:path"; -import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; -import { resetPluginStateStoreForTests } from "openclaw/plugin-sdk/plugin-state-test-runtime"; -import { withTempHome } from "openclaw/plugin-sdk/test-env"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; - -const legacyCryptoInspectorAvailability = vi.hoisted(() => ({ - available: true, -})); - -vi.mock("./legacy-crypto-inspector-availability.js", () => ({ - isMatrixLegacyCryptoInspectorAvailable: () => legacyCryptoInspectorAvailability.available, -})); - -import { autoPrepareLegacyMatrixCrypto, detectLegacyMatrixCrypto } from "./legacy-crypto.js"; -import { - readMatrixLegacyCryptoMigrationState, - readMatrixRecoveryKeyState, -} from "./matrix/crypto-state-store.js"; -import { resolveMatrixAccountStorageRoot } from "./storage-paths.js"; -import { - MATRIX_DEFAULT_ACCESS_TOKEN, - MATRIX_DEFAULT_DEVICE_ID, - MATRIX_DEFAULT_USER_ID, - MATRIX_OPS_ACCESS_TOKEN, - MATRIX_OPS_ACCOUNT_ID, - MATRIX_OPS_DEVICE_ID, - MATRIX_OPS_USER_ID, - MATRIX_TEST_HOMESERVER, - writeFile, - writeMatrixCredentials, -} from "./test-helpers.js"; -import { installMatrixTestRuntime } from "./test-runtime.js"; - -function createDefaultMatrixConfig(): OpenClawConfig { - return { - channels: { - matrix: { - homeserver: MATRIX_TEST_HOMESERVER, - userId: MATRIX_DEFAULT_USER_ID, - accessToken: MATRIX_DEFAULT_ACCESS_TOKEN, - }, - }, - }; -} - -function writeDefaultLegacyCryptoFixture(home: string) { - const stateDir = path.join(home, ".openclaw"); - const cfg = createDefaultMatrixConfig(); - const { rootDir } = resolveMatrixAccountStorageRoot({ - stateDir, - homeserver: MATRIX_TEST_HOMESERVER, - userId: MATRIX_DEFAULT_USER_ID, - accessToken: MATRIX_DEFAULT_ACCESS_TOKEN, - }); - writeFile( - path.join(rootDir, "crypto", "bot-sdk.json"), - JSON.stringify({ deviceId: MATRIX_DEFAULT_DEVICE_ID }), - ); - return { cfg, rootDir }; -} - -function createOpsLegacyCryptoFixture(params: { - home: string; - accessToken?: string; - includeStoredCredentials?: boolean; -}) { - const stateDir = path.join(params.home, ".openclaw"); - writeFile( - path.join(stateDir, "matrix", "crypto", "bot-sdk.json"), - JSON.stringify({ deviceId: MATRIX_OPS_DEVICE_ID }), - ); - if (params.includeStoredCredentials) { - writeMatrixCredentials(stateDir, { - accountId: MATRIX_OPS_ACCOUNT_ID, - accessToken: params.accessToken ?? MATRIX_OPS_ACCESS_TOKEN, - deviceId: MATRIX_OPS_DEVICE_ID, - }); - } - const { rootDir } = resolveMatrixAccountStorageRoot({ - stateDir, - homeserver: MATRIX_TEST_HOMESERVER, - userId: MATRIX_OPS_USER_ID, - accessToken: params.accessToken ?? MATRIX_OPS_ACCESS_TOKEN, - accountId: MATRIX_OPS_ACCOUNT_ID, - }); - return { rootDir }; -} - -describe("matrix legacy encrypted-state migration", () => { - beforeEach(() => { - resetPluginStateStoreForTests(); - installMatrixTestRuntime(); - }); - - afterEach(() => { - legacyCryptoInspectorAvailability.available = true; - resetPluginStateStoreForTests(); - }); - - it("extracts a saved backup key into the new recovery-key path", async () => { - await withTempHome(async (home) => { - const { cfg, rootDir } = writeDefaultLegacyCryptoFixture(home); - - const detection = detectLegacyMatrixCrypto({ cfg, env: process.env }); - expect(detection.inspectorAvailable).toBe(true); - expect(detection.warnings).toStrictEqual([]); - expect(detection.plans).toHaveLength(1); - - const result = await autoPrepareLegacyMatrixCrypto({ - cfg, - env: process.env, - deps: { - inspectLegacyStore: async () => ({ - deviceId: MATRIX_DEFAULT_DEVICE_ID, - roomKeyCounts: { total: 12, backedUp: 12 }, - backupVersion: "1", - decryptionKeyBase64: "YWJjZA==", - }), - }, - }); - - expect(result.migrated).toBe(true); - expect(result.warnings).toStrictEqual([]); - - expect(readMatrixRecoveryKeyState(rootDir)?.privateKeyBase64).toBe("YWJjZA=="); - expect(fs.existsSync(path.join(rootDir, "recovery-key.json"))).toBe(false); - }); - }); - - it("skips migration when no legacy Matrix plans exist", async () => { - await withTempHome(async () => { - const result = await autoPrepareLegacyMatrixCrypto({ - cfg: createDefaultMatrixConfig(), - env: process.env, - }); - - expect(result).toEqual({ - migrated: false, - changes: [], - warnings: [], - }); - }); - }); - - it("warns when legacy local-only room keys cannot be recovered automatically", async () => { - await withTempHome(async (home) => { - const { cfg, rootDir } = writeDefaultLegacyCryptoFixture(home); - - const result = await autoPrepareLegacyMatrixCrypto({ - cfg, - env: process.env, - deps: { - inspectLegacyStore: async () => ({ - deviceId: MATRIX_DEFAULT_DEVICE_ID, - roomKeyCounts: { total: 15, backedUp: 10 }, - backupVersion: null, - decryptionKeyBase64: null, - }), - }, - }); - - expect(result.migrated).toBe(true); - expect(result.warnings).toContain( - 'Legacy Matrix encrypted state for account "default" contains 5 room key(s) that were never backed up. Backed-up keys can be restored automatically, but local-only encrypted history may remain unavailable after upgrade.', - ); - expect(result.warnings).toContain( - 'Legacy Matrix encrypted state for account "default" cannot be fully converted automatically because the old rust crypto store does not expose all local room keys for export.', - ); - expect(readMatrixLegacyCryptoMigrationState(rootDir)?.restoreStatus).toBe( - "manual-action-required", - ); - }); - }); - - it("prefers stored credentials for named accounts when config is token-only", async () => { - await withTempHome(async (home) => { - const { rootDir } = createOpsLegacyCryptoFixture({ - home, - includeStoredCredentials: true, - }); - const cfg: OpenClawConfig = { - channels: { - matrix: { - accounts: { - ops: { - homeserver: MATRIX_TEST_HOMESERVER, - accessToken: MATRIX_OPS_ACCESS_TOKEN, - }, - }, - }, - }, - }; - - const result = await autoPrepareLegacyMatrixCrypto({ - cfg, - env: process.env, - deps: { - inspectLegacyStore: async () => ({ - deviceId: MATRIX_OPS_DEVICE_ID, - roomKeyCounts: { total: 1, backedUp: 1 }, - backupVersion: "1", - decryptionKeyBase64: "b3Bz", - }), - }, - }); - - expect(result.migrated).toBe(true); - expect(readMatrixRecoveryKeyState(rootDir)?.privateKeyBase64).toBe("b3Bz"); - expect(fs.existsSync(path.join(rootDir, "recovery-key.json"))).toBe(false); - }); - }); - - it("stays warning-only when the legacy crypto inspector artifact is unavailable", async () => { - legacyCryptoInspectorAvailability.available = false; - - await withTempHome(async (home) => { - const { cfg } = writeDefaultLegacyCryptoFixture(home); - - const detection = detectLegacyMatrixCrypto({ cfg, env: process.env }); - expect(detection.inspectorAvailable).toBe(false); - expect(detection.plans).toHaveLength(1); - expect(detection.warnings).toContain( - "Legacy Matrix encrypted state was detected, but the Matrix crypto inspector is unavailable.", - ); - - const result = await autoPrepareLegacyMatrixCrypto({ - cfg, - env: process.env, - }); - - expect(result).toEqual({ - migrated: false, - changes: [], - warnings: [ - "Legacy Matrix encrypted state was detected, but the Matrix crypto inspector is unavailable.", - ], - }); - }); - }); -}); diff --git a/extensions/matrix/src/legacy-crypto.ts b/extensions/matrix/src/legacy-crypto.ts deleted file mode 100644 index af0d19a47f00..000000000000 --- a/extensions/matrix/src/legacy-crypto.ts +++ /dev/null @@ -1,502 +0,0 @@ -// Matrix plugin module implements legacy crypto behavior. -import fs from "node:fs"; -import os from "node:os"; -import path from "node:path"; -import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; -import { loadJsonFile } from "openclaw/plugin-sdk/json-store"; -import { resolveStateDir } from "openclaw/plugin-sdk/state-paths"; -import { resolveConfiguredMatrixAccountIds } from "./account-selection.js"; -import { isMatrixLegacyCryptoInspectorAvailable } from "./legacy-crypto-inspector-availability.js"; -import { - migrateLegacyMatrixLegacyCryptoMigrationFileToStore, - migrateLegacyMatrixRecoveryKeyFileToStore, - readMatrixLegacyCryptoMigrationState, - readMatrixRecoveryKeyState, - writeMatrixLegacyCryptoMigrationState, - writeMatrixRecoveryKeyState, - type MatrixLegacyCryptoMigrationState, -} from "./matrix/crypto-state-store.js"; -import { formatMatrixErrorMessage } from "./matrix/errors.js"; -import type { MatrixStoredRecoveryKey } from "./matrix/sdk/types.js"; -import { - resolveLegacyMatrixFlatStoreTarget, - resolveMatrixMigrationAccountTarget, -} from "./migration-config.js"; -import { resolveMatrixLegacyFlatStoragePaths } from "./storage-paths.js"; - -const MATRIX_LEGACY_CRYPTO_INSPECTOR_UNAVAILABLE_MESSAGE = - "Legacy Matrix encrypted state was detected, but the Matrix crypto inspector is unavailable."; - -type MatrixLegacyCryptoCounts = { - total: number; - backedUp: number; -}; - -type MatrixLegacyCryptoSummary = { - deviceId: string | null; - roomKeyCounts: MatrixLegacyCryptoCounts | null; - backupVersion: string | null; - decryptionKeyBase64: string | null; -}; - -type MatrixLegacyCryptoPlan = { - accountId: string; - rootDir: string; - recoveryKeyPath: string; - statePath: string; - legacyCryptoPath: string; - homeserver: string; - userId: string; - accessToken: string; - deviceId: string | null; -}; - -type MatrixLegacyCryptoDetection = { - inspectorAvailable: boolean; - plans: MatrixLegacyCryptoPlan[]; - warnings: string[]; -}; - -type MatrixLegacyCryptoPreparationResult = { - migrated: boolean; - changes: string[]; - warnings: string[]; -}; - -type MatrixLegacyCryptoPrepareDeps = { - inspectLegacyStore: MatrixLegacyCryptoInspector; -}; - -type MatrixLegacyCryptoInspectorParams = { - cryptoRootDir: string; - userId: string; - deviceId: string; - log?: (message: string) => void; -}; - -type MatrixLegacyCryptoInspectorResult = { - deviceId: string | null; - roomKeyCounts: { - total: number; - backedUp: number; - } | null; - backupVersion: string | null; - decryptionKeyBase64: string | null; -}; - -type MatrixLegacyCryptoInspector = ( - params: MatrixLegacyCryptoInspectorParams, -) => Promise; - -type MatrixLegacyBotSdkMetadata = { - deviceId: string | null; -}; - -async function loadMatrixLegacyCryptoInspector(): Promise { - const module = await import("./matrix/legacy-crypto-inspector.js"); - return module.inspectLegacyMatrixCryptoStore as MatrixLegacyCryptoInspector; -} - -function detectLegacyBotSdkCryptoStore(cryptoRootDir: string): { - detected: boolean; - warning?: string; -} { - try { - const stat = fs.statSync(cryptoRootDir); - if (!stat.isDirectory()) { - return { - detected: false, - warning: - `Legacy Matrix encrypted state path exists but is not a directory: ${cryptoRootDir}. ` + - "OpenClaw skipped automatic crypto migration for that path.", - }; - } - } catch (err) { - return { - detected: false, - warning: - `Failed reading legacy Matrix encrypted state path (${cryptoRootDir}): ${String(err)}. ` + - "OpenClaw skipped automatic crypto migration for that path.", - }; - } - - try { - return { - detected: - fs.existsSync(path.join(cryptoRootDir, "bot-sdk.json")) || - fs.existsSync(path.join(cryptoRootDir, "matrix-sdk-crypto.sqlite3")) || - fs - .readdirSync(cryptoRootDir, { withFileTypes: true }) - .some( - (entry) => - entry.isDirectory() && - fs.existsSync(path.join(cryptoRootDir, entry.name, "matrix-sdk-crypto.sqlite3")), - ), - }; - } catch (err) { - return { - detected: false, - warning: - `Failed scanning legacy Matrix encrypted state path (${cryptoRootDir}): ${String(err)}. ` + - "OpenClaw skipped automatic crypto migration for that path.", - }; - } -} - -function resolveMatrixAccountIds(cfg: OpenClawConfig): string[] { - return resolveConfiguredMatrixAccountIds(cfg); -} - -function resolveLegacyMatrixFlatStorePlan(params: { - cfg: OpenClawConfig; - env: NodeJS.ProcessEnv; -}): MatrixLegacyCryptoPlan | { warning: string } | null { - const legacy = resolveMatrixLegacyFlatStoragePaths(resolveStateDir(params.env, os.homedir)); - if (!fs.existsSync(legacy.cryptoPath)) { - return null; - } - const legacyStore = detectLegacyBotSdkCryptoStore(legacy.cryptoPath); - if (legacyStore.warning) { - return { warning: legacyStore.warning }; - } - if (!legacyStore.detected) { - return null; - } - - const target = resolveLegacyMatrixFlatStoreTarget({ - cfg: params.cfg, - env: params.env, - detectedPath: legacy.cryptoPath, - detectedKind: "encrypted state", - }); - if ("warning" in target) { - return target; - } - - const metadata = loadLegacyBotSdkMetadata(legacy.cryptoPath); - return { - accountId: target.accountId, - rootDir: target.rootDir, - recoveryKeyPath: path.join(target.rootDir, "recovery-key.json"), - statePath: path.join(target.rootDir, "legacy-crypto-migration.json"), - legacyCryptoPath: legacy.cryptoPath, - homeserver: target.homeserver, - userId: target.userId, - accessToken: target.accessToken, - deviceId: metadata.deviceId ?? target.storedDeviceId, - }; -} - -function loadLegacyBotSdkMetadata(cryptoRootDir: string): MatrixLegacyBotSdkMetadata { - const metadataPath = path.join(cryptoRootDir, "bot-sdk.json"); - const fallback: MatrixLegacyBotSdkMetadata = { deviceId: null }; - const parsed = loadJsonFile<{ deviceId?: unknown }>(metadataPath); - return { - deviceId: - typeof parsed?.deviceId === "string" && parsed.deviceId.trim() - ? parsed.deviceId - : fallback.deviceId, - }; -} - -function resolveMatrixLegacyCryptoPlans(params: { - cfg: OpenClawConfig; - env: NodeJS.ProcessEnv; -}): Omit { - const warnings: string[] = []; - const plans: MatrixLegacyCryptoPlan[] = []; - - const flatPlan = resolveLegacyMatrixFlatStorePlan(params); - if (flatPlan) { - if ("warning" in flatPlan) { - warnings.push(flatPlan.warning); - } else { - plans.push(flatPlan); - } - } - - for (const accountId of resolveMatrixAccountIds(params.cfg)) { - const target = resolveMatrixMigrationAccountTarget({ - cfg: params.cfg, - env: params.env, - accountId, - }); - if (!target) { - continue; - } - const legacyCryptoPath = path.join(target.rootDir, "crypto"); - if (!fs.existsSync(legacyCryptoPath)) { - continue; - } - const detectedStore = detectLegacyBotSdkCryptoStore(legacyCryptoPath); - if (detectedStore.warning) { - warnings.push(detectedStore.warning); - continue; - } - if (!detectedStore.detected) { - continue; - } - if ( - plans.some( - (plan) => - plan.accountId === accountId && - path.resolve(plan.legacyCryptoPath) === path.resolve(legacyCryptoPath), - ) - ) { - continue; - } - const metadata = loadLegacyBotSdkMetadata(legacyCryptoPath); - plans.push({ - accountId: target.accountId, - rootDir: target.rootDir, - recoveryKeyPath: path.join(target.rootDir, "recovery-key.json"), - statePath: path.join(target.rootDir, "legacy-crypto-migration.json"), - legacyCryptoPath, - homeserver: target.homeserver, - userId: target.userId, - accessToken: target.accessToken, - deviceId: metadata.deviceId ?? target.storedDeviceId, - }); - } - - return { plans, warnings }; -} - -export function detectLegacyMatrixCrypto(params: { - cfg: OpenClawConfig; - env?: NodeJS.ProcessEnv; -}): MatrixLegacyCryptoDetection { - const detection = resolveMatrixLegacyCryptoPlans({ - cfg: params.cfg, - env: params.env ?? process.env, - }); - const inspectorAvailable = - detection.plans.length === 0 || isMatrixLegacyCryptoInspectorAvailable(); - if (!inspectorAvailable && detection.plans.length > 0) { - return { - inspectorAvailable, - plans: detection.plans, - warnings: [...detection.warnings, MATRIX_LEGACY_CRYPTO_INSPECTOR_UNAVAILABLE_MESSAGE], - }; - } - return { - inspectorAvailable, - plans: detection.plans, - warnings: detection.warnings, - }; -} - -export async function autoPrepareLegacyMatrixCrypto(params: { - cfg: OpenClawConfig; - env?: NodeJS.ProcessEnv; - log?: { info?: (message: string) => void; warn?: (message: string) => void }; - deps?: Partial; -}): Promise { - const env = params.env ?? process.env; - const detection = params.deps?.inspectLegacyStore - ? resolveMatrixLegacyCryptoPlans({ cfg: params.cfg, env }) - : detectLegacyMatrixCrypto({ cfg: params.cfg, env }); - const inspectorAvailable = - "inspectorAvailable" in detection ? detection.inspectorAvailable : true; - const warnings = [...detection.warnings]; - const changes: string[] = []; - if (detection.plans.length === 0) { - if (warnings.length > 0) { - params.log?.warn?.( - `matrix: legacy encrypted-state warnings:\n${warnings.map((entry) => `- ${entry}`).join("\n")}`, - ); - } - return { - migrated: false, - changes, - warnings, - }; - } - if (!params.deps?.inspectLegacyStore && !inspectorAvailable) { - if (warnings.length > 0) { - params.log?.warn?.( - `matrix: legacy encrypted-state warnings:\n${warnings.map((entry) => `- ${entry}`).join("\n")}`, - ); - } - return { - migrated: false, - changes, - warnings, - }; - } - - let inspectLegacyStore = params.deps?.inspectLegacyStore; - if (!inspectLegacyStore) { - try { - inspectLegacyStore = await loadMatrixLegacyCryptoInspector(); - } catch (err) { - const message = formatMatrixErrorMessage(err); - if (!warnings.includes(message)) { - warnings.push(message); - } - if (warnings.length > 0) { - params.log?.warn?.( - `matrix: legacy encrypted-state warnings:\n${warnings.map((entry) => `- ${entry}`).join("\n")}`, - ); - } - return { - migrated: false, - changes, - warnings, - }; - } - } - if (!inspectLegacyStore) { - return { - migrated: false, - changes, - warnings, - }; - } - - for (const plan of detection.plans) { - try { - migrateLegacyMatrixLegacyCryptoMigrationFileToStore(plan.rootDir); - migrateLegacyMatrixRecoveryKeyFileToStore(plan.rootDir); - } catch (err) { - warnings.push( - `Failed migrating Matrix crypto sidecar state for account "${plan.accountId}" (${plan.rootDir}): ${String(err)}`, - ); - } - const existingState = readMatrixLegacyCryptoMigrationState(plan.rootDir); - if (existingState?.version === 1) { - continue; - } - if (!plan.deviceId) { - warnings.push( - `Legacy Matrix encrypted state detected at ${plan.legacyCryptoPath}, but no device ID was found for account "${plan.accountId}". ` + - `OpenClaw will continue, but old encrypted history cannot be recovered automatically.`, - ); - continue; - } - - let summary: MatrixLegacyCryptoSummary; - try { - summary = await inspectLegacyStore({ - cryptoRootDir: plan.legacyCryptoPath, - userId: plan.userId, - deviceId: plan.deviceId, - log: params.log?.info, - }); - } catch (err) { - warnings.push( - `Failed inspecting legacy Matrix encrypted state for account "${plan.accountId}" (${plan.legacyCryptoPath}): ${String(err)}`, - ); - continue; - } - - let decryptionKeyImported = false; - if (summary.decryptionKeyBase64) { - const existingRecoveryKey = readMatrixRecoveryKeyState(plan.rootDir); - if ( - existingRecoveryKey?.privateKeyBase64 && - existingRecoveryKey.privateKeyBase64 !== summary.decryptionKeyBase64 - ) { - warnings.push( - `Legacy Matrix backup key was found for account "${plan.accountId}", but Matrix SQLite state already contains a different recovery key. Leaving the existing state unchanged.`, - ); - } else if (!existingRecoveryKey?.privateKeyBase64) { - const payload: MatrixStoredRecoveryKey = { - version: 1, - createdAt: new Date().toISOString(), - keyId: null, - privateKeyBase64: summary.decryptionKeyBase64, - }; - try { - writeMatrixRecoveryKeyState({ - storageRootDir: plan.rootDir, - payload, - }); - changes.push( - `Imported Matrix legacy backup key for account "${plan.accountId}" into SQLite`, - ); - decryptionKeyImported = true; - } catch (err) { - warnings.push( - `Failed writing Matrix recovery key for account "${plan.accountId}" to SQLite: ${String(err)}`, - ); - } - } else { - decryptionKeyImported = true; - } - } - - const localOnlyKeys = - summary.roomKeyCounts && summary.roomKeyCounts.total > summary.roomKeyCounts.backedUp - ? summary.roomKeyCounts.total - summary.roomKeyCounts.backedUp - : 0; - if (localOnlyKeys > 0) { - warnings.push( - `Legacy Matrix encrypted state for account "${plan.accountId}" contains ${localOnlyKeys} room key(s) that were never backed up. ` + - "Backed-up keys can be restored automatically, but local-only encrypted history may remain unavailable after upgrade.", - ); - } - if (!summary.decryptionKeyBase64 && (summary.roomKeyCounts?.backedUp ?? 0) > 0) { - warnings.push( - `Legacy Matrix encrypted state for account "${plan.accountId}" has backed-up room keys, but no local backup decryption key was found. ` + - `Ask the operator to run "openclaw matrix verify backup restore --recovery-key " after upgrade if they have the recovery key.`, - ); - } - if (!summary.decryptionKeyBase64 && (summary.roomKeyCounts?.total ?? 0) > 0) { - warnings.push( - `Legacy Matrix encrypted state for account "${plan.accountId}" cannot be fully converted automatically because the old rust crypto store does not expose all local room keys for export.`, - ); - } - // If recovery-key persistence failed, leave the migration state absent so the next startup can retry. - if ( - summary.decryptionKeyBase64 && - !decryptionKeyImported && - !readMatrixRecoveryKeyState(plan.rootDir) - ) { - continue; - } - - const state: MatrixLegacyCryptoMigrationState = { - version: 1, - source: "matrix-bot-sdk-rust", - accountId: plan.accountId, - deviceId: summary.deviceId, - roomKeyCounts: summary.roomKeyCounts, - backupVersion: summary.backupVersion, - decryptionKeyImported, - restoreStatus: decryptionKeyImported ? "pending" : "manual-action-required", - detectedAt: new Date().toISOString(), - lastError: null, - }; - try { - writeMatrixLegacyCryptoMigrationState({ - storageRootDir: plan.rootDir, - state, - }); - changes.push( - `Prepared Matrix legacy encrypted-state migration for account "${plan.accountId}" in SQLite`, - ); - } catch (err) { - warnings.push( - `Failed writing Matrix legacy encrypted-state migration record for account "${plan.accountId}" to SQLite: ${String(err)}`, - ); - } - } - - if (changes.length > 0) { - params.log?.info?.( - `matrix: prepared encrypted-state upgrade.\n${changes.map((entry) => `- ${entry}`).join("\n")}`, - ); - } - if (warnings.length > 0) { - params.log?.warn?.( - `matrix: legacy encrypted-state warnings:\n${warnings.map((entry) => `- ${entry}`).join("\n")}`, - ); - } - - return { - migrated: changes.length > 0, - changes, - warnings, - }; -} diff --git a/extensions/matrix/src/legacy-state.test.ts b/extensions/matrix/src/legacy-state.test.ts deleted file mode 100644 index 752d28a18c90..000000000000 --- a/extensions/matrix/src/legacy-state.test.ts +++ /dev/null @@ -1,87 +0,0 @@ -// Matrix tests cover legacy state plugin behavior. -import fs from "node:fs"; -import path from "node:path"; -import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; -import { withTempHome } from "openclaw/plugin-sdk/test-env"; -import { describe, expect, it } from "vitest"; -import { autoMigrateLegacyMatrixState, detectLegacyMatrixState } from "./legacy-state.js"; - -function writeFile(filePath: string, value: string) { - fs.mkdirSync(path.dirname(filePath), { recursive: true }); - fs.writeFileSync(filePath, value, "utf-8"); -} - -describe("matrix legacy state migration", () => { - it("migrates the flat legacy Matrix store into account-scoped storage", async () => { - await withTempHome(async (home) => { - const stateDir = path.join(home, ".openclaw"); - writeFile(path.join(stateDir, "matrix", "bot-storage.json"), '{"next_batch":"s1"}'); - writeFile(path.join(stateDir, "matrix", "crypto", "store.db"), "crypto"); - - const cfg: OpenClawConfig = { - channels: { - matrix: { - homeserver: "https://matrix.example.org", - userId: "@bot:example.org", - accessToken: "tok-123", - }, - }, - }; - - const detection = detectLegacyMatrixState({ cfg, env: process.env }); - expect(detection && "warning" in detection).toBe(false); - if (!detection || "warning" in detection) { - throw new Error("expected a migratable Matrix legacy state plan"); - } - - const result = await autoMigrateLegacyMatrixState({ cfg, env: process.env }); - expect(result.migrated).toBe(true); - expect(result.warnings).toStrictEqual([]); - expect(fs.existsSync(path.join(stateDir, "matrix", "bot-storage.json"))).toBe(false); - expect(fs.existsSync(path.join(stateDir, "matrix", "crypto"))).toBe(false); - expect(fs.existsSync(detection.targetStoragePath)).toBe(true); - expect(fs.existsSync(path.join(detection.targetCryptoPath, "store.db"))).toBe(true); - }); - }); - - it("uses cached Matrix credentials when the config no longer stores an access token", async () => { - await withTempHome(async (home) => { - const stateDir = path.join(home, ".openclaw"); - writeFile(path.join(stateDir, "matrix", "bot-storage.json"), '{"next_batch":"s1"}'); - writeFile( - path.join(stateDir, "credentials", "matrix", "credentials.json"), - JSON.stringify( - { - homeserver: "https://matrix.example.org", - userId: "@bot:example.org", - accessToken: "tok-from-cache", - }, - null, - 2, - ), - ); - - const cfg: OpenClawConfig = { - channels: { - matrix: { - homeserver: "https://matrix.example.org", - userId: "@bot:example.org", - password: "secret", - }, - }, - }; - - const detection = detectLegacyMatrixState({ cfg, env: process.env }); - expect(detection && "warning" in detection).toBe(false); - if (!detection || "warning" in detection) { - throw new Error("expected cached credentials to make Matrix migration resolvable"); - } - - expect(detection.targetRootDir).toContain("matrix.example.org__bot_example.org"); - - const result = await autoMigrateLegacyMatrixState({ cfg, env: process.env }); - expect(result.migrated).toBe(true); - expect(fs.existsSync(detection.targetStoragePath)).toBe(true); - }); - }); -}); diff --git a/extensions/matrix/src/legacy-state.ts b/extensions/matrix/src/legacy-state.ts deleted file mode 100644 index 52f00cd32031..000000000000 --- a/extensions/matrix/src/legacy-state.ts +++ /dev/null @@ -1,157 +0,0 @@ -// Matrix plugin module implements legacy state behavior. -import fs from "node:fs"; -import os from "node:os"; -import path from "node:path"; -import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; -import { resolveStateDir } from "openclaw/plugin-sdk/state-paths"; -import { resolveLegacyMatrixFlatStoreTarget } from "./migration-config.js"; -import { resolveMatrixLegacyFlatStoragePaths } from "./storage-paths.js"; - -type MatrixLegacyStateMigrationResult = { - migrated: boolean; - changes: string[]; - warnings: string[]; -}; - -type MatrixLegacyStatePlan = { - accountId: string; - legacyStoragePath: string; - legacyCryptoPath: string; - targetRootDir: string; - targetStoragePath: string; - targetCryptoPath: string; - selectionNote?: string; -}; - -function resolveLegacyMatrixPaths(env: NodeJS.ProcessEnv): { - rootDir: string; - storagePath: string; - cryptoPath: string; -} { - const stateDir = resolveStateDir(env, os.homedir); - return resolveMatrixLegacyFlatStoragePaths(stateDir); -} - -function resolveMatrixMigrationPlan(params: { - cfg: OpenClawConfig; - env: NodeJS.ProcessEnv; -}): MatrixLegacyStatePlan | { warning: string } | null { - const legacy = resolveLegacyMatrixPaths(params.env); - if (!fs.existsSync(legacy.storagePath) && !fs.existsSync(legacy.cryptoPath)) { - return null; - } - - const target = resolveLegacyMatrixFlatStoreTarget({ - cfg: params.cfg, - env: params.env, - detectedPath: legacy.rootDir, - detectedKind: "state", - }); - if ("warning" in target) { - return target; - } - - return { - accountId: target.accountId, - legacyStoragePath: legacy.storagePath, - legacyCryptoPath: legacy.cryptoPath, - targetRootDir: target.rootDir, - targetStoragePath: path.join(target.rootDir, "bot-storage.json"), - targetCryptoPath: path.join(target.rootDir, "crypto"), - selectionNote: target.selectionNote, - }; -} - -export function detectLegacyMatrixState(params: { - cfg: OpenClawConfig; - env?: NodeJS.ProcessEnv; -}): MatrixLegacyStatePlan | { warning: string } | null { - return resolveMatrixMigrationPlan({ - cfg: params.cfg, - env: params.env ?? process.env, - }); -} - -function moveLegacyPath(params: { - sourcePath: string; - targetPath: string; - label: string; - changes: string[]; - warnings: string[]; -}): void { - if (!fs.existsSync(params.sourcePath)) { - return; - } - if (fs.existsSync(params.targetPath)) { - params.warnings.push( - `Matrix legacy ${params.label} not migrated because the target already exists (${params.targetPath}).`, - ); - return; - } - try { - fs.mkdirSync(path.dirname(params.targetPath), { recursive: true }); - fs.renameSync(params.sourcePath, params.targetPath); - params.changes.push( - `Migrated Matrix legacy ${params.label}: ${params.sourcePath} -> ${params.targetPath}`, - ); - } catch (err) { - params.warnings.push( - `Failed migrating Matrix legacy ${params.label} (${params.sourcePath} -> ${params.targetPath}): ${String(err)}`, - ); - } -} - -export async function autoMigrateLegacyMatrixState(params: { - cfg: OpenClawConfig; - env?: NodeJS.ProcessEnv; - log?: { info?: (message: string) => void; warn?: (message: string) => void }; -}): Promise { - const env = params.env ?? process.env; - const detection = detectLegacyMatrixState({ cfg: params.cfg, env }); - if (!detection) { - return { migrated: false, changes: [], warnings: [] }; - } - if ("warning" in detection) { - params.log?.warn?.(`matrix: ${detection.warning}`); - return { migrated: false, changes: [], warnings: [detection.warning] }; - } - - const changes: string[] = []; - const warnings: string[] = []; - moveLegacyPath({ - sourcePath: detection.legacyStoragePath, - targetPath: detection.targetStoragePath, - label: "sync store", - changes, - warnings, - }); - moveLegacyPath({ - sourcePath: detection.legacyCryptoPath, - targetPath: detection.targetCryptoPath, - label: "crypto store", - changes, - warnings, - }); - - if (changes.length > 0) { - const details = [ - ...changes.map((entry) => `- ${entry}`), - ...(detection.selectionNote ? [`- ${detection.selectionNote}`] : []), - "- No user action required.", - ]; - params.log?.info?.( - `matrix: plugin upgraded in place for account "${detection.accountId}".\n${details.join("\n")}`, - ); - } - if (warnings.length > 0) { - params.log?.warn?.( - `matrix: legacy state migration warnings:\n${warnings.map((entry) => `- ${entry}`).join("\n")}`, - ); - } - - return { - migrated: changes.length > 0, - changes, - warnings, - }; -} diff --git a/extensions/matrix/src/matrix-migration.runtime.ts b/extensions/matrix/src/matrix-migration.runtime.ts deleted file mode 100644 index 61d60fbbfb32..000000000000 --- a/extensions/matrix/src/matrix-migration.runtime.ts +++ /dev/null @@ -1,10 +0,0 @@ -// Matrix plugin module implements matrix migration behavior. -export { autoMigrateLegacyMatrixState, detectLegacyMatrixState } from "./legacy-state.js"; -export { autoPrepareLegacyMatrixCrypto, detectLegacyMatrixCrypto } from "./legacy-crypto.js"; -export { - hasActionableMatrixMigration, - hasPendingMatrixMigration, - resolveMatrixMigrationStatus, - type MatrixMigrationStatus, -} from "./migration-snapshot.js"; -export { maybeCreateMatrixMigrationSnapshot } from "./migration-snapshot-backup.js"; diff --git a/extensions/matrix/src/matrix/client/migration-snapshot.runtime.ts b/extensions/matrix/src/matrix/client/migration-snapshot.runtime.ts deleted file mode 100644 index a706d8b4b98d..000000000000 --- a/extensions/matrix/src/matrix/client/migration-snapshot.runtime.ts +++ /dev/null @@ -1,2 +0,0 @@ -// Matrix plugin module implements migration snapshot behavior. -export { maybeCreateMatrixMigrationSnapshot } from "../../migration-snapshot-backup.js"; diff --git a/extensions/matrix/src/matrix/client/storage.test.ts b/extensions/matrix/src/matrix/client/storage.test.ts index cb9bedc17121..2b91cf195db6 100644 --- a/extensions/matrix/src/matrix/client/storage.test.ts +++ b/extensions/matrix/src/matrix/client/storage.test.ts @@ -22,32 +22,6 @@ import { writeStorageMeta, } from "./storage.js"; -const createBackupArchiveMock = vi.hoisted(() => - vi.fn(async (_params: unknown) => ({ - createdAt: "2026-03-17T00:00:00.000Z", - archiveRoot: "2026-03-17-openclaw-backup", - archivePath: "/tmp/matrix-migration-snapshot.tar.gz", - dryRun: false, - includeWorkspace: false, - onlyConfig: false, - verified: false, - assets: [], - skipped: [], - })), -); - -const maybeCreateMatrixMigrationSnapshotMock = vi.hoisted(() => - vi.fn(async (_params: unknown) => ({ - created: true, - archivePath: "/tmp/matrix-migration-snapshot.tar.gz", - markerPath: "/tmp/matrix-migration-snapshot.json", - })), -); - -vi.mock("./migration-snapshot.runtime.js", () => ({ - maybeCreateMatrixMigrationSnapshot: (params: unknown) => - maybeCreateMatrixMigrationSnapshotMock(params), -})); describe("matrix client storage paths", () => { const tempDirs: string[] = []; const defaultStorageAuth = { @@ -61,23 +35,6 @@ describe("matrix client storage paths", () => { }); afterEach(() => { - createBackupArchiveMock.mockReset(); - createBackupArchiveMock.mockImplementation(async (_params: unknown) => ({ - createdAt: "2026-03-17T00:00:00.000Z", - archiveRoot: "2026-03-17-openclaw-backup", - archivePath: "/tmp/matrix-migration-snapshot.tar.gz", - dryRun: false, - includeWorkspace: false, - onlyConfig: false, - verified: false, - assets: [], - skipped: [], - })); - maybeCreateMatrixMigrationSnapshotMock.mockReset().mockResolvedValue({ - created: true, - archivePath: "/tmp/matrix-migration-snapshot.tar.gz", - markerPath: "/tmp/matrix-migration-snapshot.json", - }); vi.restoreAllMocks(); resetPluginStateStoreForTests(); for (const dir of tempDirs.splice(0)) { @@ -124,24 +81,6 @@ describe("matrix client storage paths", () => { } as NodeJS.ProcessEnv; } - function expectFallbackMigrationSnapshot(env: NodeJS.ProcessEnv): void { - expect(maybeCreateMatrixMigrationSnapshotMock).toHaveBeenCalledTimes(1); - const [params] = maybeCreateMatrixMigrationSnapshotMock.mock.calls.at(0) ?? []; - expect(params).toEqual({ - env, - log: { - info: (params as { log?: { info?: unknown } })?.log?.info, - warn: (params as { log?: { warn?: unknown } })?.log?.warn, - error: (params as { log?: { error?: unknown } })?.log?.error, - }, - trigger: "matrix-client-fallback", - }); - const log = (params as { log?: { info?: unknown; warn?: unknown; error?: unknown } })?.log; - expect(typeof log?.info).toBe("function"); - expect(typeof log?.warn).toBe("function"); - expect(typeof log?.error).toBe("function"); - } - function resolveDefaultStoragePaths( overrides: Partial<{ homeserver: string; @@ -257,23 +196,6 @@ describe("matrix client storage paths", () => { ); }); - function writeLegacyMatrixStorage( - stateDir: string, - params: { - storageBody?: string; - withCrypto?: boolean; - } = {}, - ) { - const legacyRoot = path.join(stateDir, "matrix"); - if (params.withCrypto ?? true) { - fs.mkdirSync(path.join(legacyRoot, "crypto"), { recursive: true }); - } - if (params.storageBody !== undefined) { - fs.writeFileSync(path.join(legacyRoot, "bot-storage.json"), params.storageBody); - } - return legacyRoot; - } - function legacySyncCacheBody(nextBatch = "legacy-token"): string { return JSON.stringify({ version: 1, @@ -431,29 +353,6 @@ describe("matrix client storage paths", () => { ); }); - it("falls back to migrating the older flat matrix storage layout", async () => { - const stateDir = setupStateDir(); - const storagePaths = resolveDefaultStoragePaths(); - const legacyRoot = writeLegacyMatrixStorage(stateDir, { - storageBody: legacySyncCacheBody("legacy-token"), - }); - const env = createMigrationEnv(stateDir); - - await maybeMigrateLegacyStorage({ - storagePaths, - env, - }); - - expectFallbackMigrationSnapshot(env); - expect(fs.existsSync(path.join(legacyRoot, "bot-storage.json"))).toBe(false); - expect(fs.existsSync(path.join(legacyRoot, "bot-storage.json.migrated"))).toBe(true); - expect(fs.existsSync(storagePaths.storagePath)).toBe(false); - expect(fs.existsSync(storagePaths.cryptoPath)).toBe(true); - const syncStore = new SqliteBackedMatrixSyncStore(storagePaths.rootDir); - expect(syncStore.hasSavedSync()).toBe(true); - await expect(syncStore.getSavedSyncToken()).resolves.toBe("legacy-token"); - }); - it("migrates the previous account-scoped sync cache into sqlite before startup", async () => { const stateDir = setupStateDir(); const storagePaths = resolveDefaultStoragePaths(); @@ -466,7 +365,6 @@ describe("matrix client storage paths", () => { env, }); - expectFallbackMigrationSnapshot(env); expect(fs.existsSync(storagePaths.storagePath)).toBe(false); expect(fs.existsSync(`${storagePaths.storagePath}.migrated`)).toBe(true); const syncStore = new SqliteBackedMatrixSyncStore(storagePaths.rootDir); @@ -501,7 +399,6 @@ describe("matrix client storage paths", () => { env, }); - expectFallbackMigrationSnapshot(env); expect(readMatrixIdbSnapshotJson(storagePaths.rootDir)).toBe(currentSnapshot); expect(fs.existsSync(storagePaths.idbSnapshotPath)).toBe(false); expect(fs.existsSync(`${storagePaths.idbSnapshotPath}.migrated`)).toBe(true); @@ -519,132 +416,9 @@ describe("matrix client storage paths", () => { env, }); - expect(maybeCreateMatrixMigrationSnapshotMock).not.toHaveBeenCalled(); expect(fs.readFileSync(storagePaths.storagePath, "utf8")).toBe('{"new":true}'); }); - it("continues migrating whichever legacy artifact is still missing", async () => { - const stateDir = setupStateDir(); - const storagePaths = resolveDefaultStoragePaths(); - const legacyRoot = writeLegacyMatrixStorage(stateDir); - const env = createMigrationEnv(stateDir); - fs.mkdirSync(storagePaths.rootDir, { recursive: true }); - fs.writeFileSync(storagePaths.storagePath, '{"new":true}'); - - await maybeMigrateLegacyStorage({ - storagePaths, - env, - }); - - expectFallbackMigrationSnapshot(env); - expect(fs.readFileSync(storagePaths.storagePath, "utf8")).toBe('{"new":true}'); - expect(fs.existsSync(path.join(legacyRoot, "bot-storage.json"))).toBe(false); - expect(fs.existsSync(path.join(legacyRoot, "crypto"))).toBe(false); - expect(fs.existsSync(storagePaths.cryptoPath)).toBe(true); - }); - - it("refuses to migrate legacy storage when the snapshot step fails", async () => { - const stateDir = setupStateDir(); - const storagePaths = resolveDefaultStoragePaths(); - const legacyRoot = writeLegacyMatrixStorage(stateDir, { storageBody: '{"legacy":true}' }); - const env = createMigrationEnv(stateDir); - maybeCreateMatrixMigrationSnapshotMock.mockRejectedValueOnce(new Error("snapshot failed")); - - await expect( - maybeMigrateLegacyStorage({ - storagePaths, - env, - }), - ).rejects.toThrow("snapshot failed"); - expect(fs.existsSync(path.join(legacyRoot, "bot-storage.json"))).toBe(true); - expect(fs.existsSync(storagePaths.storagePath)).toBe(false); - }); - - it("rolls back moved legacy storage when the crypto move fails", async () => { - const stateDir = setupStateDir(); - const storagePaths = resolveDefaultStoragePaths(); - const legacyRoot = writeLegacyMatrixStorage(stateDir, { storageBody: '{"legacy":true}' }); - const env = createMigrationEnv(stateDir); - const realRenameSync = fs.renameSync.bind(fs); - const renameSync = vi.spyOn(fs, "renameSync"); - renameSync.mockImplementation((sourcePath, targetPath) => { - if (String(targetPath) === storagePaths.cryptoPath) { - throw new Error("disk full"); - } - return realRenameSync(sourcePath, targetPath); - }); - - await expect( - maybeMigrateLegacyStorage({ - storagePaths, - env, - }), - ).rejects.toThrow("disk full"); - expect(fs.existsSync(path.join(legacyRoot, "bot-storage.json"))).toBe(true); - expect(fs.existsSync(storagePaths.storagePath)).toBe(false); - expect(fs.existsSync(path.join(legacyRoot, "crypto"))).toBe(true); - }); - - it("refuses fallback migration when multiple Matrix accounts need explicit selection", async () => { - const stateDir = setupStateDir({ - channels: { - matrix: { - accounts: { - ops: {}, - work: {}, - }, - }, - }, - }); - const storagePaths = resolveDefaultStoragePaths({ accountId: "ops" }); - const legacyRoot = writeLegacyMatrixStorage(stateDir, { storageBody: '{"legacy":true}' }); - const env = createMigrationEnv(stateDir); - - await expect( - maybeMigrateLegacyStorage({ - storagePaths, - env, - }), - ).rejects.toThrow(/defaultAccount is not set/i); - expect(createBackupArchiveMock).not.toHaveBeenCalled(); - expect(fs.existsSync(path.join(legacyRoot, "bot-storage.json"))).toBe(true); - }); - - it("refuses fallback migration for a non-selected Matrix account", async () => { - const stateDir = setupStateDir({ - channels: { - matrix: { - defaultAccount: "ops", - homeserver: "https://matrix.default.example.org", - accessToken: "default-token", - accounts: { - ops: { - homeserver: "https://matrix.ops.example.org", - accessToken: "ops-token", - }, - }, - }, - }, - }); - const storagePaths = resolveMatrixStoragePaths({ - homeserver: "https://matrix.default.example.org", - userId: "@default:example.org", - accessToken: "default-token", - env: {}, - }); - const legacyRoot = writeLegacyMatrixStorage(stateDir, { storageBody: '{"legacy":true}' }); - const env = createMigrationEnv(stateDir); - - await expect( - maybeMigrateLegacyStorage({ - storagePaths, - env, - }), - ).rejects.toThrow(/targets account "ops"/i); - expect(createBackupArchiveMock).not.toHaveBeenCalled(); - expect(fs.existsSync(path.join(legacyRoot, "bot-storage.json"))).toBe(true); - }); - it("keeps the canonical current-token storage root when deviceId is still unknown", () => { const stateDir = setupStateDir(); const oldStoragePaths = seedExistingStorageRoot({ diff --git a/extensions/matrix/src/matrix/client/storage.ts b/extensions/matrix/src/matrix/client/storage.ts index a8905a83d91a..7889b504081a 100644 --- a/extensions/matrix/src/matrix/client/storage.ts +++ b/extensions/matrix/src/matrix/client/storage.ts @@ -3,22 +3,14 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { normalizeAccountId } from "openclaw/plugin-sdk/account-id"; -import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; import { loadJsonFile } from "openclaw/plugin-sdk/json-store"; import type { PluginStateKeyedStore, PluginStateSyncKeyedStore, } from "openclaw/plugin-sdk/plugin-state-runtime"; -import { - requiresExplicitMatrixDefaultAccount, - resolveMatrixDefaultOrOnlyAccountId, -} from "../../account-selection.js"; import { isRecord } from "../../record-shared.js"; import { getMatrixRuntime } from "../../runtime.js"; -import { - resolveMatrixAccountStorageRoot, - resolveMatrixLegacyFlatStoragePaths, -} from "../../storage-paths.js"; +import { resolveMatrixAccountStorageRoot } from "../../storage-paths.js"; import { MATRIX_IDB_SNAPSHOT_FILENAME, MATRIX_LEGACY_CRYPTO_MIGRATION_FILENAME, @@ -74,35 +66,6 @@ function openStorageMetaStore(rootDir: string): PluginStateSyncKeyedStore { - const legacy = resolveLegacyStoragePaths(params.env); - const hasFlatLegacyStorageFile = fs.existsSync(legacy.storagePath); const hasAccountScopedLegacyStorageFile = fs.existsSync(params.storagePaths.storagePath); - const syncCache = - hasFlatLegacyStorageFile || hasAccountScopedLegacyStorageFile - ? await import("./file-sync-store.js") - : null; - const hasFlatLegacyStorage = - hasFlatLegacyStorageFile && - (await syncCache?.readLegacyMatrixSyncCacheState(legacy.rootDir)) !== null; + const syncCache = hasAccountScopedLegacyStorageFile ? await import("./file-sync-store.js") : null; const hasAccountScopedLegacyStorage = hasAccountScopedLegacyStorageFile && (await syncCache?.readLegacyMatrixSyncCacheState(params.storagePaths.rootDir)) !== null; - const hasLegacyCrypto = fs.existsSync(legacy.cryptoPath); const hasAccountScopedRecoveryKey = fs.existsSync(params.storagePaths.recoveryKeyPath); const hasAccountScopedIdbSnapshot = fs.existsSync(params.storagePaths.idbSnapshotPath); const hasAccountScopedLegacyCryptoMigration = fs.existsSync( path.join(params.storagePaths.rootDir, MATRIX_LEGACY_CRYPTO_MIGRATION_FILENAME), ); if ( - !hasFlatLegacyStorage && !hasAccountScopedLegacyStorage && - !hasLegacyCrypto && !hasAccountScopedRecoveryKey && !hasAccountScopedIdbSnapshot && !hasAccountScopedLegacyCryptoMigration ) { return; } - const hasTargetCrypto = fs.existsSync(params.storagePaths.cryptoPath); - const shouldMigrateCrypto = hasLegacyCrypto && !hasTargetCrypto; - if ( - !hasFlatLegacyStorage && - !hasAccountScopedLegacyStorage && - !shouldMigrateCrypto && - !hasAccountScopedRecoveryKey && - !hasAccountScopedIdbSnapshot && - !hasAccountScopedLegacyCryptoMigration - ) { - return; - } - - if (hasFlatLegacyStorage || hasLegacyCrypto) { - assertLegacyMigrationAccountSelection({ - accountKey: params.storagePaths.accountKey, - }); - } const logger = getMatrixRuntime().logging.getChildLogger({ module: "matrix-storage" }); - const { maybeCreateMatrixMigrationSnapshot } = await import("./migration-snapshot.runtime.js"); - await maybeCreateMatrixMigrationSnapshot({ - trigger: "matrix-client-fallback", - env: params.env, - log: logger, - }); fs.mkdirSync(params.storagePaths.rootDir, { recursive: true }); const moved: LegacyMoveRecord[] = []; const pendingArchives: LegacyArchiveRecord[] = []; @@ -495,28 +423,6 @@ export async function maybeMigrateLegacyStorage(params: { pendingArchives, }); } - if (hasFlatLegacyStorage) { - await migrateLegacySyncCacheToSqlite({ - sourceRootDir: legacy.rootDir, - sourcePath: legacy.storagePath, - targetRootDir: params.storagePaths.rootDir, - label: "flat sync cache", - moved, - pendingArchives, - }); - } - if (shouldMigrateCrypto) { - moveLegacyStoragePathOrThrow({ - sourcePath: legacy.cryptoPath, - targetPath: params.storagePaths.cryptoPath, - label: "crypto store", - moved, - }); - } else if (hasLegacyCrypto) { - skippedExistingTargets.push( - `- crypto store remains at ${legacy.cryptoPath} because ${params.storagePaths.cryptoPath} already exists`, - ); - } if (hasAccountScopedRecoveryKey) { migrateLegacyMatrixRecoveryKeyFileToStore(params.storagePaths.rootDir); moved.push({ @@ -565,7 +471,7 @@ export async function maybeMigrateLegacyStorage(params: { } if (skippedExistingTargets.length > 0) { logger.warn?.( - `matrix: legacy client storage still exists in the flat path because some account-scoped targets already existed.\n${skippedExistingTargets.join("\n")}`, + `matrix: legacy client storage files were left in place because their migrated targets already existed.\n${skippedExistingTargets.join("\n")}`, ); } } @@ -661,28 +567,6 @@ function archiveLegacyStoragePath(params: { fs.renameSync(params.sourcePath, archivedLegacyStoragePath); } -function moveLegacyStoragePathOrThrow(params: { - sourcePath: string; - targetPath: string; - label: string; - moved: LegacyMoveRecord[]; -}): void { - if (!fs.existsSync(params.sourcePath)) { - return; - } - if (fs.existsSync(params.targetPath)) { - throw new Error( - `legacy Matrix ${params.label} target already exists (${params.targetPath}); refusing to overwrite it automatically`, - ); - } - fs.renameSync(params.sourcePath, params.targetPath); - params.moved.push({ - sourcePath: params.sourcePath, - targetPath: params.targetPath, - label: params.label, - }); -} - function rollbackLegacyMoves(moved: LegacyMoveRecord[]): string | null { for (const entry of moved.toReversed()) { try { diff --git a/extensions/matrix/src/matrix/legacy-crypto-inspector.ts b/extensions/matrix/src/matrix/legacy-crypto-inspector.ts deleted file mode 100644 index 569ba5f85537..000000000000 --- a/extensions/matrix/src/matrix/legacy-crypto-inspector.ts +++ /dev/null @@ -1,98 +0,0 @@ -// Matrix plugin module implements legacy crypto inspector behavior. -import crypto from "node:crypto"; -import fs from "node:fs"; -import { createRequire } from "node:module"; -import path from "node:path"; -import { ensureMatrixCryptoRuntime } from "./deps.js"; - -type MatrixLegacyCryptoInspectionResult = { - deviceId: string | null; - roomKeyCounts: { - total: number; - backedUp: number; - } | null; - backupVersion: string | null; - decryptionKeyBase64: string | null; -}; - -const MATRIX_CRYPTO_STORE_SQLITE = 0; - -function resolveLegacyMachineStorePath(params: { - cryptoRootDir: string; - deviceId: string; -}): string | null { - const hashedDir = path.join( - params.cryptoRootDir, - crypto.createHash("sha256").update(params.deviceId).digest("hex"), - ); - if (fs.existsSync(path.join(hashedDir, "matrix-sdk-crypto.sqlite3"))) { - return hashedDir; - } - if (fs.existsSync(path.join(params.cryptoRootDir, "matrix-sdk-crypto.sqlite3"))) { - return params.cryptoRootDir; - } - const match = fs - .readdirSync(params.cryptoRootDir, { withFileTypes: true }) - .find( - (entry) => - entry.isDirectory() && - fs.existsSync(path.join(params.cryptoRootDir, entry.name, "matrix-sdk-crypto.sqlite3")), - ); - return match ? path.join(params.cryptoRootDir, match.name) : null; -} - -export async function inspectLegacyMatrixCryptoStore(params: { - cryptoRootDir: string; - userId: string; - deviceId: string; - log?: (message: string) => void; -}): Promise { - const machineStorePath = resolveLegacyMachineStorePath(params); - if (!machineStorePath) { - throw new Error(`Matrix legacy crypto store not found for device ${params.deviceId}`); - } - - const requireFn = createRequire(import.meta.url); - await ensureMatrixCryptoRuntime({ - requireFn, - resolveFn: requireFn.resolve.bind(requireFn), - log: params.log, - }); - - const { DeviceId, OlmMachine, UserId } = requireFn( - "@matrix-org/matrix-sdk-crypto-nodejs", - ) as typeof import("@matrix-org/matrix-sdk-crypto-nodejs"); - const machine = await OlmMachine.initialize( - new UserId(params.userId), - new DeviceId(params.deviceId), - machineStorePath, - "", - MATRIX_CRYPTO_STORE_SQLITE, - ); - - try { - const [backupKeys, roomKeyCounts] = await Promise.all([ - machine.getBackupKeys(), - machine.roomKeyCounts(), - ]); - return { - deviceId: params.deviceId, - roomKeyCounts: roomKeyCounts - ? { - total: typeof roomKeyCounts.total === "number" ? roomKeyCounts.total : 0, - backedUp: typeof roomKeyCounts.backedUp === "number" ? roomKeyCounts.backedUp : 0, - } - : null, - backupVersion: - typeof backupKeys?.backupVersion === "string" && backupKeys.backupVersion.trim() - ? backupKeys.backupVersion - : null, - decryptionKeyBase64: - typeof backupKeys?.decryptionKeyBase64 === "string" && backupKeys.decryptionKeyBase64.trim() - ? backupKeys.decryptionKeyBase64 - : null, - }; - } finally { - machine.close(); - } -} diff --git a/extensions/matrix/src/matrix/monitor/index.test.ts b/extensions/matrix/src/matrix/monitor/index.test.ts index d636a62f81e9..5979d78f62ad 100644 --- a/extensions/matrix/src/matrix/monitor/index.test.ts +++ b/extensions/matrix/src/matrix/monitor/index.test.ts @@ -370,10 +370,6 @@ vi.mock("./inbound-dedupe.js", () => ({ createMatrixInboundEventDeduper: hoisted.createMatrixInboundEventDeduper, })); -vi.mock("./legacy-crypto-restore.js", () => ({ - maybeRestoreLegacyMatrixBackup: vi.fn(), -})); - vi.mock("./room-info.js", () => ({ createMatrixRoomInfoResolver: vi.fn(() => ({ getRoomInfo: hoisted.getRoomInfo, diff --git a/extensions/matrix/src/matrix/monitor/legacy-crypto-restore.test.ts b/extensions/matrix/src/matrix/monitor/legacy-crypto-restore.test.ts deleted file mode 100644 index d136d4186779..000000000000 --- a/extensions/matrix/src/matrix/monitor/legacy-crypto-restore.test.ts +++ /dev/null @@ -1,212 +0,0 @@ -// Matrix tests cover legacy crypto restore plugin behavior. -import fs from "node:fs"; -import path from "node:path"; -import { resetPluginStateStoreForTests } from "openclaw/plugin-sdk/plugin-state-test-runtime"; -import { withTempHome } from "openclaw/plugin-sdk/test-env"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { resolveMatrixAccountStorageRoot } from "../../storage-paths.js"; -import { installMatrixTestRuntime } from "../../test-runtime.js"; -import { readMatrixLegacyCryptoMigrationState } from "../crypto-state-store.js"; -import type { MatrixRoomKeyBackupRestoreResult } from "../sdk.js"; -import { maybeRestoreLegacyMatrixBackup } from "./legacy-crypto-restore.js"; - -function createBackupStatus() { - return { - serverVersion: "1", - activeVersion: "1", - trusted: true, - matchesDecryptionKey: true, - decryptionKeyCached: true, - keyLoadAttempted: true, - keyLoadError: null, - }; -} - -function writeFile(filePath: string, value: string) { - fs.mkdirSync(path.dirname(filePath), { recursive: true }); - fs.writeFileSync(filePath, value, "utf8"); -} - -const BASE_AUTH = { - accountId: "default", - homeserver: "https://matrix.example.org", - userId: "@bot:example.org", - accessToken: "tok-123", -}; - -type MatrixAuth = typeof BASE_AUTH; - -function readLegacyMigrationState(rootDir: string) { - return readMatrixLegacyCryptoMigrationState(rootDir) as Record | null; -} - -async function runLegacyRestoreScenario(params: { - migration: Record; - auth?: MatrixAuth; - sourceAuth?: MatrixAuth; - restoreRoomKeyBackup: () => Promise; -}) { - return withTempHome(async (home) => { - const stateDir = path.join(home, ".openclaw"); - const auth = params.auth ?? BASE_AUTH; - const sourceAuth = params.sourceAuth ?? auth; - const { rootDir } = resolveMatrixAccountStorageRoot({ - stateDir, - ...auth, - }); - const { rootDir: sourceRootDir } = resolveMatrixAccountStorageRoot({ - stateDir, - ...sourceAuth, - }); - - writeFile( - path.join(sourceRootDir, "legacy-crypto-migration.json"), - JSON.stringify(params.migration), - ); - - const restoreRoomKeyBackup = vi.fn(params.restoreRoomKeyBackup); - const result = await maybeRestoreLegacyMatrixBackup({ - client: { restoreRoomKeyBackup }, - auth, - stateDir, - env: { - ...process.env, - OPENCLAW_STATE_DIR: stateDir, - HOME: home, - }, - }); - - return { - result, - restoreRoomKeyBackup, - rootState: readLegacyMigrationState(rootDir), - rootStateExists: readLegacyMigrationState(rootDir) !== null, - sourceRootState: readLegacyMigrationState(sourceRootDir), - sourceRootStateExists: readLegacyMigrationState(sourceRootDir) !== null, - }; - }); -} - -describe("maybeRestoreLegacyMatrixBackup", () => { - beforeEach(() => { - resetPluginStateStoreForTests(); - installMatrixTestRuntime(); - }); - - afterEach(() => { - resetPluginStateStoreForTests(); - }); - - it("marks pending legacy backup restore as completed after success", async () => { - const { result, sourceRootState } = await runLegacyRestoreScenario({ - migration: { - version: 1, - accountId: "default", - roomKeyCounts: { total: 10, backedUp: 8 }, - restoreStatus: "pending", - }, - restoreRoomKeyBackup: async () => ({ - success: true, - restoredAt: "2026-03-08T10:00:00.000Z", - imported: 8, - total: 8, - loadedFromSecretStorage: true, - backupVersion: "1", - backup: createBackupStatus(), - }), - }); - - expect(result).toEqual({ - kind: "restored", - imported: 8, - total: 8, - localOnlyKeys: 2, - }); - const state = sourceRootState as { - restoreStatus: string; - importedCount: number; - totalCount: number; - }; - expect(state.restoreStatus).toBe("completed"); - expect(state.importedCount).toBe(8); - expect(state.totalCount).toBe(8); - }); - - it("keeps the restore pending when startup restore fails", async () => { - const { result, sourceRootState } = await runLegacyRestoreScenario({ - migration: { - version: 1, - accountId: "default", - roomKeyCounts: { total: 5, backedUp: 5 }, - restoreStatus: "pending", - }, - restoreRoomKeyBackup: async () => ({ - success: false, - error: "backup unavailable", - imported: 0, - total: 0, - loadedFromSecretStorage: false, - backupVersion: null, - backup: createBackupStatus(), - }), - }); - - expect(result).toEqual({ - kind: "failed", - error: "backup unavailable", - localOnlyKeys: 0, - }); - const state = sourceRootState as { - restoreStatus: string; - lastError: string; - }; - expect(state.restoreStatus).toBe("pending"); - expect(state.lastError).toBe("backup unavailable"); - }); - - it("restores from a sibling token-hash directory when the access token changed", async () => { - const oldAuth = { - ...BASE_AUTH, - accessToken: "tok-old", - }; - const newAuth = { - ...oldAuth, - accessToken: "tok-new", - }; - const { - result, - rootStateExists: newRootStateExists, - sourceRootState, - } = await runLegacyRestoreScenario({ - auth: newAuth, - sourceAuth: oldAuth, - migration: { - version: 1, - accountId: "default", - roomKeyCounts: { total: 3, backedUp: 3 }, - restoreStatus: "pending", - }, - restoreRoomKeyBackup: async () => ({ - success: true, - restoredAt: "2026-03-08T10:00:00.000Z", - imported: 3, - total: 3, - loadedFromSecretStorage: true, - backupVersion: "1", - backup: createBackupStatus(), - }), - }); - - expect(result).toEqual({ - kind: "restored", - imported: 3, - total: 3, - localOnlyKeys: 0, - }); - const oldState = sourceRootState as { - restoreStatus: string; - }; - expect(oldState.restoreStatus).toBe("completed"); - expect(newRootStateExists).toBe(false); - }); -}); diff --git a/extensions/matrix/src/matrix/monitor/legacy-crypto-restore.ts b/extensions/matrix/src/matrix/monitor/legacy-crypto-restore.ts deleted file mode 100644 index b56dc9719aa6..000000000000 --- a/extensions/matrix/src/matrix/monitor/legacy-crypto-restore.ts +++ /dev/null @@ -1,136 +0,0 @@ -// Matrix plugin module implements legacy crypto restore behavior. -import fs from "node:fs/promises"; -import os from "node:os"; -import path from "node:path"; -import { getMatrixRuntime } from "../../runtime.js"; -import { resolveMatrixStoragePaths } from "../client/storage.js"; -import type { MatrixAuth } from "../client/types.js"; -import { - migrateLegacyMatrixLegacyCryptoMigrationFileToStore, - readMatrixLegacyCryptoMigrationState, - writeMatrixLegacyCryptoMigrationState, - type MatrixLegacyCryptoMigrationState, -} from "../crypto-state-store.js"; -import type { MatrixClient } from "../sdk.js"; - -export type MatrixLegacyCryptoRestoreResult = - | { kind: "skipped" } - | { - kind: "restored"; - imported: number; - total: number; - localOnlyKeys: number; - } - | { - kind: "failed"; - error: string; - localOnlyKeys: number; - }; - -async function resolvePendingMigrationStateRoot(params: { - stateDir: string; - auth: Pick; -}): Promise<{ - storageRootDir: string; - value: MatrixLegacyCryptoMigrationState | null; -}> { - const { rootDir } = resolveMatrixStoragePaths({ - homeserver: params.auth.homeserver, - userId: params.auth.userId, - accessToken: params.auth.accessToken, - accountId: params.auth.accountId, - deviceId: params.auth.deviceId, - stateDir: params.stateDir, - }); - try { - migrateLegacyMatrixLegacyCryptoMigrationFileToStore(rootDir); - } catch { - // Startup restore can still proceed from any already-migrated SQLite state. - } - const directValue = readMatrixLegacyCryptoMigrationState(rootDir); - if (directValue?.restoreStatus === "pending") { - return { storageRootDir: rootDir, value: directValue }; - } - - const accountStorageDir = path.dirname(rootDir); - let siblingEntries: string[]; - try { - siblingEntries = (await fs.readdir(accountStorageDir, { withFileTypes: true })) - .filter((entry) => entry.isDirectory()) - .map((entry) => entry.name) - .filter((entry) => path.join(accountStorageDir, entry) !== rootDir) - .toSorted((left, right) => left.localeCompare(right)); - } catch { - return { storageRootDir: rootDir, value: directValue }; - } - - for (const sibling of siblingEntries) { - const siblingRootDir = path.join(accountStorageDir, sibling); - try { - migrateLegacyMatrixLegacyCryptoMigrationFileToStore(siblingRootDir); - } catch { - // Sibling scan is best-effort; unreadable roots are ignored. - } - const value = readMatrixLegacyCryptoMigrationState(siblingRootDir); - if (value?.restoreStatus === "pending") { - return { storageRootDir: siblingRootDir, value }; - } - } - return { storageRootDir: rootDir, value: directValue }; -} - -export async function maybeRestoreLegacyMatrixBackup(params: { - client: Pick; - auth: Pick; - env?: NodeJS.ProcessEnv; - stateDir?: string; -}): Promise { - const env = params.env ?? process.env; - const stateDir = params.stateDir ?? getMatrixRuntime().state.resolveStateDir(env, os.homedir); - const { storageRootDir, value } = await resolvePendingMigrationStateRoot({ - stateDir, - auth: params.auth, - }); - if (value?.restoreStatus !== "pending") { - return { kind: "skipped" }; - } - - const restore = await params.client.restoreRoomKeyBackup(); - const localOnlyKeys = - value.roomKeyCounts && value.roomKeyCounts.total > value.roomKeyCounts.backedUp - ? value.roomKeyCounts.total - value.roomKeyCounts.backedUp - : 0; - - if (restore.success) { - writeMatrixLegacyCryptoMigrationState({ - storageRootDir, - state: { - ...value, - restoreStatus: "completed", - restoredAt: restore.restoredAt ?? new Date().toISOString(), - importedCount: restore.imported, - totalCount: restore.total, - lastError: null, - } satisfies MatrixLegacyCryptoMigrationState, - }); - return { - kind: "restored", - imported: restore.imported, - total: restore.total, - localOnlyKeys, - }; - } - - writeMatrixLegacyCryptoMigrationState({ - storageRootDir, - state: { - ...value, - lastError: restore.error ?? "unknown", - } satisfies MatrixLegacyCryptoMigrationState, - }); - return { - kind: "failed", - error: restore.error ?? "unknown", - localOnlyKeys, - }; -} diff --git a/extensions/matrix/src/matrix/monitor/startup.test.ts b/extensions/matrix/src/matrix/monitor/startup.test.ts index b79701b8b27c..682457c5537a 100644 --- a/extensions/matrix/src/matrix/monitor/startup.test.ts +++ b/extensions/matrix/src/matrix/monitor/startup.test.ts @@ -5,7 +5,6 @@ import type { MatrixAccountPatch } from "../config-update.js"; import type { MatrixManagedDeviceInfo } from "../device-health.js"; import type { MatrixProfileSyncResult } from "../profile.js"; import type { MatrixOwnDeviceVerificationStatus } from "../sdk.js"; -import type { MatrixLegacyCryptoRestoreResult } from "./legacy-crypto-restore.js"; import type { MatrixStartupVerificationOutcome } from "./startup-verification.js"; import type { MatrixStartupMaintenanceDeps } from "./startup.js"; import { runMatrixStartupMaintenance } from "./startup.js"; @@ -78,20 +77,10 @@ async function expectMatrixStartupAbort(promise: Promise): Promise = {}, -): MatrixLegacyCryptoRestoreResult { - return { - kind: "skipped", - ...overrides, - } as MatrixLegacyCryptoRestoreResult; -} - function createDeps( overrides: Partial = {}, ): MatrixStartupMaintenanceDeps { return { - maybeRestoreLegacyMatrixBackup: vi.fn(async () => createLegacyCryptoRestoreResult()), summarizeMatrixDeviceHealth: vi.fn(() => ({ currentDeviceId: null, staleOpenClawDevices: [] as MatrixManagedDeviceInfo[], @@ -221,15 +210,6 @@ describe("runMatrixStartupMaintenance", () => { vi.mocked(deps.ensureMatrixStartupVerification).mockResolvedValue( createStartupVerificationOutcome("pending"), ); - vi.mocked(deps.maybeRestoreLegacyMatrixBackup).mockResolvedValue( - createLegacyCryptoRestoreResult({ - kind: "restored", - imported: 2, - total: 3, - localOnlyKeys: 1, - }), - ); - await runMatrixStartupMaintenance(params, deps); expect(params.logger.warn).toHaveBeenCalledWith( @@ -241,12 +221,6 @@ describe("runMatrixStartupMaintenance", () => { expect(params.logger.info).toHaveBeenCalledWith( "matrix: startup verification request is already pending; finish it in another Matrix client", ); - expect(params.logger.info).toHaveBeenCalledWith( - "matrix: restored 2/3 room key(s) from legacy encrypted-state backup", - ); - expect(params.logger.warn).toHaveBeenCalledWith( - "matrix: 1 legacy local-only room key(s) were never backed up and could not be restored automatically", - ); }); it("logs cooldown and request-failure verification outcomes without throwing", async () => { @@ -286,6 +260,5 @@ describe("runMatrixStartupMaintenance", () => { await expectMatrixStartupAbort(runMatrixStartupMaintenance(params, deps)); expect(deps.ensureMatrixStartupVerification).not.toHaveBeenCalled(); - expect(deps.maybeRestoreLegacyMatrixBackup).not.toHaveBeenCalled(); }); }); diff --git a/extensions/matrix/src/matrix/monitor/startup.ts b/extensions/matrix/src/matrix/monitor/startup.ts index f4e57f75657c..a2be86f4a51a 100644 --- a/extensions/matrix/src/matrix/monitor/startup.ts +++ b/extensions/matrix/src/matrix/monitor/startup.ts @@ -22,7 +22,6 @@ export type MatrixStartupMaintenanceDeps = { updateMatrixAccountConfig: typeof import("../config-update.js").updateMatrixAccountConfig; summarizeMatrixDeviceHealth: typeof import("../device-health.js").summarizeMatrixDeviceHealth; syncMatrixOwnProfile: typeof import("../profile.js").syncMatrixOwnProfile; - maybeRestoreLegacyMatrixBackup: typeof import("./legacy-crypto-restore.js").maybeRestoreLegacyMatrixBackup; ensureMatrixStartupVerification: typeof import("./startup-verification.js").ensureMatrixStartupVerification; }; @@ -31,23 +30,13 @@ const loadMatrixStartupMaintenanceDeps = createLazyRuntimeModule(() => import("../config-update.js"), import("../device-health.js"), import("../profile.js"), - import("./legacy-crypto-restore.js"), import("./startup-verification.js"), - ]).then( - ([ - configUpdateModule, - deviceHealthModule, - profileModule, - legacyCryptoRestoreModule, - startupVerificationModule, - ]) => ({ - updateMatrixAccountConfig: configUpdateModule.updateMatrixAccountConfig, - summarizeMatrixDeviceHealth: deviceHealthModule.summarizeMatrixDeviceHealth, - syncMatrixOwnProfile: profileModule.syncMatrixOwnProfile, - maybeRestoreLegacyMatrixBackup: legacyCryptoRestoreModule.maybeRestoreLegacyMatrixBackup, - ensureMatrixStartupVerification: startupVerificationModule.ensureMatrixStartupVerification, - }), - ), + ]).then(([configUpdateModule, deviceHealthModule, profileModule, startupVerificationModule]) => ({ + updateMatrixAccountConfig: configUpdateModule.updateMatrixAccountConfig, + summarizeMatrixDeviceHealth: deviceHealthModule.summarizeMatrixDeviceHealth, + syncMatrixOwnProfile: profileModule.syncMatrixOwnProfile, + ensureMatrixStartupVerification: startupVerificationModule.ensureMatrixStartupVerification, + })), ); export async function runMatrixStartupMaintenance( @@ -178,40 +167,4 @@ export async function runMatrixStartupMaintenance( error: String(err), }); } - - try { - throwIfMatrixStartupAborted(params.abortSignal); - const legacyCryptoRestore = await runtimeDeps.maybeRestoreLegacyMatrixBackup({ - client: params.client, - auth: params.auth, - env: params.env, - }); - throwIfMatrixStartupAborted(params.abortSignal); - if (legacyCryptoRestore.kind === "restored") { - params.logger.info( - `matrix: restored ${legacyCryptoRestore.imported}/${legacyCryptoRestore.total} room key(s) from legacy encrypted-state backup`, - ); - if (legacyCryptoRestore.localOnlyKeys > 0) { - params.logger.warn( - `matrix: ${legacyCryptoRestore.localOnlyKeys} legacy local-only room key(s) were never backed up and could not be restored automatically`, - ); - } - } else if (legacyCryptoRestore.kind === "failed") { - params.logger.warn( - `matrix: failed restoring room keys from legacy encrypted-state backup: ${legacyCryptoRestore.error}`, - ); - if (legacyCryptoRestore.localOnlyKeys > 0) { - params.logger.warn( - `matrix: ${legacyCryptoRestore.localOnlyKeys} legacy local-only room key(s) were never backed up and may remain unavailable until manually recovered`, - ); - } - } - } catch (err) { - if (isMatrixStartupAbortError(err)) { - throw err; - } - params.logger.warn("matrix: failed restoring legacy encrypted-state backup", { - error: String(err), - }); - } } diff --git a/extensions/matrix/src/migration-config.test.ts b/extensions/matrix/src/migration-config.test.ts deleted file mode 100644 index cbd0a5612e60..000000000000 --- a/extensions/matrix/src/migration-config.test.ts +++ /dev/null @@ -1,239 +0,0 @@ -// Matrix tests cover migration config plugin behavior. -import path from "node:path"; -import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; -import { withTempHome } from "openclaw/plugin-sdk/test-env"; -import { describe, expect, it } from "vitest"; -import { resolveMatrixMigrationAccountTarget } from "./migration-config.js"; -import { - MATRIX_OPS_ACCESS_TOKEN, - MATRIX_OPS_ACCOUNT_ID, - MATRIX_OPS_USER_ID, - MATRIX_TEST_HOMESERVER, - writeMatrixCredentials, -} from "./test-helpers.js"; - -function resolveOpsTarget(cfg: OpenClawConfig, env = process.env) { - return resolveMatrixMigrationAccountTarget({ - cfg, - env, - accountId: MATRIX_OPS_ACCOUNT_ID, - }); -} - -type MatrixMigrationTarget = NonNullable>; - -function expectMigrationTarget(target: ReturnType): MatrixMigrationTarget { - if (target === null) { - throw new Error("Expected Matrix migration account target"); - } - expect(typeof target.homeserver).toBe("string"); - return target; -} - -describe("resolveMatrixMigrationAccountTarget", () => { - it("reuses stored user identity for token-only configs when the access token matches", async () => { - await withTempHome(async (home) => { - const stateDir = path.join(home, ".openclaw"); - writeMatrixCredentials(stateDir, { - accountId: MATRIX_OPS_ACCOUNT_ID, - deviceId: "DEVICE-OPS", - accessToken: MATRIX_OPS_ACCESS_TOKEN, - }); - - const cfg: OpenClawConfig = { - channels: { - matrix: { - accounts: { - ops: { - homeserver: MATRIX_TEST_HOMESERVER, - accessToken: MATRIX_OPS_ACCESS_TOKEN, - }, - }, - }, - }, - }; - - const target = resolveOpsTarget(cfg); - - const migrationTarget = expectMigrationTarget(target); - expect(migrationTarget.userId).toBe(MATRIX_OPS_USER_ID); - expect(migrationTarget.storedDeviceId).toBe("DEVICE-OPS"); - }); - }); - - it("ignores stored device IDs from stale cached Matrix credentials", async () => { - await withTempHome(async (home) => { - const stateDir = path.join(home, ".openclaw"); - writeMatrixCredentials(stateDir, { - accountId: MATRIX_OPS_ACCOUNT_ID, - userId: "@old-bot:example.org", - accessToken: "tok-old", - deviceId: "DEVICE-OLD", - }); - - const cfg: OpenClawConfig = { - channels: { - matrix: { - accounts: { - ops: { - homeserver: MATRIX_TEST_HOMESERVER, - userId: "@new-bot:example.org", - accessToken: "tok-new", - }, - }, - }, - }, - }; - - const target = resolveOpsTarget(cfg); - - const migrationTarget = expectMigrationTarget(target); - expect(migrationTarget.userId).toBe("@new-bot:example.org"); - expect(migrationTarget.accessToken).toBe("tok-new"); - expect(migrationTarget.storedDeviceId).toBeNull(); - }); - }); - - it("does not trust stale stored creds on the same homeserver when the token changes", async () => { - await withTempHome(async (home) => { - const stateDir = path.join(home, ".openclaw"); - writeMatrixCredentials(stateDir, { - accountId: MATRIX_OPS_ACCOUNT_ID, - userId: "@old-bot:example.org", - accessToken: "tok-old", - deviceId: "DEVICE-OLD", - }); - - const cfg: OpenClawConfig = { - channels: { - matrix: { - accounts: { - ops: { - homeserver: MATRIX_TEST_HOMESERVER, - accessToken: "tok-new", - }, - }, - }, - }, - }; - - const target = resolveOpsTarget(cfg); - - expect(target).toBeNull(); - }); - }); - - it("does not inherit the base userId for non-default token-only accounts", async () => { - await withTempHome(async (home) => { - const stateDir = path.join(home, ".openclaw"); - writeMatrixCredentials(stateDir, { - accountId: MATRIX_OPS_ACCOUNT_ID, - deviceId: "DEVICE-OPS", - accessToken: MATRIX_OPS_ACCESS_TOKEN, - }); - - const cfg: OpenClawConfig = { - channels: { - matrix: { - homeserver: MATRIX_TEST_HOMESERVER, - userId: "@base-bot:example.org", - accounts: { - ops: { - homeserver: MATRIX_TEST_HOMESERVER, - accessToken: MATRIX_OPS_ACCESS_TOKEN, - }, - }, - }, - }, - }; - - const target = resolveOpsTarget(cfg); - - const migrationTarget = expectMigrationTarget(target); - expect(migrationTarget.userId).toBe(MATRIX_OPS_USER_ID); - expect(migrationTarget.storedDeviceId).toBe("DEVICE-OPS"); - }); - }); - - it("does not inherit the base access token for non-default accounts", async () => { - await withTempHome(async () => { - const cfg: OpenClawConfig = { - channels: { - matrix: { - homeserver: MATRIX_TEST_HOMESERVER, - userId: "@base-bot:example.org", - accessToken: "tok-base", - accounts: { - ops: { - homeserver: MATRIX_TEST_HOMESERVER, - userId: MATRIX_OPS_USER_ID, - }, - }, - }, - }, - }; - - const target = resolveOpsTarget(cfg); - - expect(target).toBeNull(); - }); - }); - - it("does not inherit the global Matrix access token for non-default accounts", async () => { - await withTempHome( - async () => { - const cfg: OpenClawConfig = { - channels: { - matrix: { - accounts: { - ops: { - homeserver: MATRIX_TEST_HOMESERVER, - userId: MATRIX_OPS_USER_ID, - }, - }, - }, - }, - }; - - const target = resolveOpsTarget(cfg); - - expect(target).toBeNull(); - }, - { - env: { - MATRIX_ACCESS_TOKEN: "tok-global", - }, - }, - ); - }); - - it("uses the same scoped env token encoding as runtime account auth", async () => { - await withTempHome(async () => { - const cfg: OpenClawConfig = { - channels: { - matrix: { - accounts: { - "ops-prod": {}, - }, - }, - }, - }; - const env = { - MATRIX_OPS_X2D_PROD_HOMESERVER: "https://matrix.example.org", - MATRIX_OPS_X2D_PROD_USER_ID: "@ops-prod:example.org", - MATRIX_OPS_X2D_PROD_ACCESS_TOKEN: "tok-ops-prod", - } as NodeJS.ProcessEnv; - - const target = resolveMatrixMigrationAccountTarget({ - cfg, - env, - accountId: "ops-prod", - }); - - const migrationTarget = expectMigrationTarget(target); - expect(migrationTarget.homeserver).toBe("https://matrix.example.org"); - expect(migrationTarget.userId).toBe("@ops-prod:example.org"); - expect(migrationTarget.accessToken).toBe("tok-ops-prod"); - }); - }); -}); diff --git a/extensions/matrix/src/migration-config.ts b/extensions/matrix/src/migration-config.ts deleted file mode 100644 index 4657688e887d..000000000000 --- a/extensions/matrix/src/migration-config.ts +++ /dev/null @@ -1,244 +0,0 @@ -// Matrix helper module supports migration config behavior. -import fs from "node:fs"; -import os from "node:os"; -import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id"; -import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; -import { resolveStateDir } from "openclaw/plugin-sdk/state-paths"; -import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime"; -import { - findMatrixAccountEntry, - requiresExplicitMatrixDefaultAccount, - resolveConfiguredMatrixAccountIds, - resolveMatrixChannelConfig, - resolveMatrixDefaultOrOnlyAccountId, -} from "./account-selection.js"; -import { resolveMatrixAccountStringValues } from "./auth-precedence.js"; -import { - resolveGlobalMatrixEnvConfig, - resolveScopedMatrixEnvConfig, -} from "./matrix/client/env-auth.js"; -import { resolveMatrixAccountStorageRoot, resolveMatrixCredentialsPath } from "./storage-paths.js"; - -type MatrixStoredCredentials = { - homeserver: string; - userId: string; - accessToken: string; - deviceId?: string; -}; - -type MatrixMigrationAccountTarget = { - accountId: string; - homeserver: string; - userId: string; - accessToken: string; - rootDir: string; - storedDeviceId: string | null; -}; - -type MatrixLegacyFlatStoreTarget = MatrixMigrationAccountTarget & { - selectionNote?: string; -}; - -type MatrixLegacyFlatStoreKind = "state" | "encrypted state"; - -function clean(value: unknown): string { - return normalizeOptionalString(value) ?? ""; -} - -function resolveMatrixAccountConfigEntry( - cfg: OpenClawConfig, - accountId: string, -): Record | null { - return findMatrixAccountEntry(cfg, accountId); -} - -function resolveMatrixFlatStoreSelectionNote( - cfg: OpenClawConfig, - accountId: string, -): string | undefined { - if (resolveConfiguredMatrixAccountIds(cfg).length <= 1) { - return undefined; - } - return ( - `Legacy Matrix flat store uses one shared on-disk state, so it will be migrated into ` + - `account "${accountId}".` - ); -} - -function resolveMatrixMigrationConfigFields(params: { - cfg: OpenClawConfig; - env: NodeJS.ProcessEnv; - accountId: string; -}): { - homeserver: string; - userId: string; - accessToken: string; -} { - const channel = resolveMatrixChannelConfig(params.cfg); - const account = resolveMatrixAccountConfigEntry(params.cfg, params.accountId); - const scopedEnv = resolveScopedMatrixEnvConfig(params.accountId, params.env); - const globalEnv = resolveGlobalMatrixEnvConfig(params.env); - const normalizedAccountId = normalizeAccountId(params.accountId); - const resolvedStrings = resolveMatrixAccountStringValues({ - accountId: normalizedAccountId, - account: { - homeserver: clean(account?.homeserver), - userId: clean(account?.userId), - accessToken: clean(account?.accessToken), - }, - scopedEnv, - channel: { - homeserver: clean(channel?.homeserver), - userId: clean(channel?.userId), - accessToken: clean(channel?.accessToken), - }, - globalEnv, - }); - - return { - homeserver: resolvedStrings.homeserver, - userId: resolvedStrings.userId, - accessToken: resolvedStrings.accessToken, - }; -} - -function loadStoredMatrixCredentials( - env: NodeJS.ProcessEnv, - accountId: string, -): MatrixStoredCredentials | null { - const stateDir = resolveStateDir(env, os.homedir); - const credentialsPath = resolveMatrixCredentialsPath({ - stateDir, - accountId: normalizeAccountId(accountId), - }); - try { - if (!fs.existsSync(credentialsPath)) { - return null; - } - const parsed = JSON.parse( - fs.readFileSync(credentialsPath, "utf8"), - ) as Partial; - if ( - typeof parsed.homeserver !== "string" || - typeof parsed.userId !== "string" || - typeof parsed.accessToken !== "string" - ) { - return null; - } - return { - homeserver: parsed.homeserver, - userId: parsed.userId, - accessToken: parsed.accessToken, - deviceId: typeof parsed.deviceId === "string" ? parsed.deviceId : undefined, - }; - } catch { - return null; - } -} - -function credentialsMatchResolvedIdentity( - stored: MatrixStoredCredentials | null, - identity: { - homeserver: string; - userId: string; - accessToken: string; - }, -): stored is MatrixStoredCredentials { - if (!stored || !identity.homeserver) { - return false; - } - if (!identity.userId) { - if (!identity.accessToken) { - return false; - } - return stored.homeserver === identity.homeserver && stored.accessToken === identity.accessToken; - } - return stored.homeserver === identity.homeserver && stored.userId === identity.userId; -} - -export function resolveMatrixMigrationAccountTarget(params: { - cfg: OpenClawConfig; - env: NodeJS.ProcessEnv; - accountId: string; -}): MatrixMigrationAccountTarget | null { - const stored = loadStoredMatrixCredentials(params.env, params.accountId); - const resolved = resolveMatrixMigrationConfigFields(params); - const matchingStored = credentialsMatchResolvedIdentity(stored, { - homeserver: resolved.homeserver, - userId: resolved.userId, - accessToken: resolved.accessToken, - }) - ? stored - : null; - const homeserver = resolved.homeserver; - const userId = resolved.userId || matchingStored?.userId || ""; - const accessToken = resolved.accessToken || matchingStored?.accessToken || ""; - if (!homeserver || !userId || !accessToken) { - return null; - } - - const stateDir = resolveStateDir(params.env, os.homedir); - const { rootDir } = resolveMatrixAccountStorageRoot({ - stateDir, - homeserver, - userId, - accessToken, - accountId: params.accountId, - }); - - return { - accountId: params.accountId, - homeserver, - userId, - accessToken, - rootDir, - storedDeviceId: matchingStored?.deviceId ?? null, - }; -} - -export function resolveLegacyMatrixFlatStoreTarget(params: { - cfg: OpenClawConfig; - env: NodeJS.ProcessEnv; - detectedPath: string; - detectedKind: MatrixLegacyFlatStoreKind; -}): MatrixLegacyFlatStoreTarget | { warning: string } { - const channel = resolveMatrixChannelConfig(params.cfg); - if (!channel) { - return { - warning: - `Legacy Matrix ${params.detectedKind} detected at ${params.detectedPath}, but channels.matrix is not configured yet. ` + - 'Configure Matrix, then rerun "openclaw doctor --fix" or restart the gateway.', - }; - } - if (requiresExplicitMatrixDefaultAccount(params.cfg)) { - return { - warning: - `Legacy Matrix ${params.detectedKind} detected at ${params.detectedPath}, but multiple Matrix accounts are configured and channels.matrix.defaultAccount is not set. ` + - 'Set "channels.matrix.defaultAccount" to the intended target account before rerunning "openclaw doctor --fix" or restarting the gateway.', - }; - } - - const accountId = resolveMatrixDefaultOrOnlyAccountId(params.cfg); - const target = resolveMatrixMigrationAccountTarget({ - cfg: params.cfg, - env: params.env, - accountId, - }); - if (!target) { - const targetDescription = - params.detectedKind === "state" - ? "the new account-scoped target" - : "the account-scoped target"; - return { - warning: - `Legacy Matrix ${params.detectedKind} detected at ${params.detectedPath}, but ${targetDescription} could not be resolved yet ` + - `(need homeserver, userId, and access token for channels.matrix${accountId === DEFAULT_ACCOUNT_ID ? "" : `.accounts.${accountId}`}). ` + - 'Start the gateway once with a working Matrix login, or rerun "openclaw doctor --fix" after cached credentials are available.', - }; - } - - return { - ...target, - selectionNote: resolveMatrixFlatStoreSelectionNote(params.cfg, accountId), - }; -} diff --git a/extensions/matrix/src/migration-snapshot-backup.ts b/extensions/matrix/src/migration-snapshot-backup.ts deleted file mode 100644 index 8d32467e8d5f..000000000000 --- a/extensions/matrix/src/migration-snapshot-backup.ts +++ /dev/null @@ -1,117 +0,0 @@ -// Matrix plugin module implements migration snapshot backup behavior. -import fs from "node:fs"; -import os from "node:os"; -import path from "node:path"; -import { writeJsonFileAtomically } from "openclaw/plugin-sdk/json-store"; -import { resolveRequiredHomeDir, resolveStateDir } from "openclaw/plugin-sdk/state-paths"; - -const MATRIX_MIGRATION_SNAPSHOT_DIRNAME = "openclaw-migrations"; - -type MatrixMigrationSnapshotMarker = { - version: 1; - createdAt: string; - archivePath: string; - trigger: string; - includeWorkspace: boolean; -}; - -type MatrixMigrationSnapshotResult = { - created: boolean; - archivePath: string; - markerPath: string; -}; - -function loadSnapshotMarker(filePath: string): MatrixMigrationSnapshotMarker | null { - try { - if (!fs.existsSync(filePath)) { - return null; - } - const parsed = JSON.parse( - fs.readFileSync(filePath, "utf8"), - ) as Partial; - if ( - parsed.version !== 1 || - typeof parsed.createdAt !== "string" || - typeof parsed.archivePath !== "string" || - typeof parsed.trigger !== "string" - ) { - return null; - } - return { - version: 1, - createdAt: parsed.createdAt, - archivePath: parsed.archivePath, - trigger: parsed.trigger, - includeWorkspace: parsed.includeWorkspace === true, - }; - } catch { - return null; - } -} - -export function resolveMatrixMigrationSnapshotMarkerPath( - env: NodeJS.ProcessEnv = process.env, -): string { - const stateDir = resolveStateDir(env, os.homedir); - return path.join(stateDir, "matrix", "migration-snapshot.json"); -} - -export function resolveMatrixMigrationSnapshotOutputDir( - env: NodeJS.ProcessEnv = process.env, -): string { - const homeDir = resolveRequiredHomeDir(env, os.homedir); - return path.join(homeDir, "Backups", MATRIX_MIGRATION_SNAPSHOT_DIRNAME); -} - -export async function maybeCreateMatrixMigrationSnapshot(params: { - trigger: string; - env?: NodeJS.ProcessEnv; - outputDir?: string; - createBackupArchive?: typeof import("openclaw/plugin-sdk/runtime").createBackupArchive; - log?: { info?: (message: string) => void; warn?: (message: string) => void }; -}): Promise { - const env = params.env ?? process.env; - const createBackupArchive = - params.createBackupArchive ?? (await import("openclaw/plugin-sdk/runtime")).createBackupArchive; - const markerPath = resolveMatrixMigrationSnapshotMarkerPath(env); - const existingMarker = loadSnapshotMarker(markerPath); - if (existingMarker?.archivePath && fs.existsSync(existingMarker.archivePath)) { - params.log?.info?.( - `matrix: reusing existing pre-migration backup snapshot: ${existingMarker.archivePath}`, - ); - return { - created: false, - archivePath: existingMarker.archivePath, - markerPath, - }; - } - if (existingMarker?.archivePath && !fs.existsSync(existingMarker.archivePath)) { - params.log?.warn?.( - `matrix: previous migration snapshot is missing (${existingMarker.archivePath}); creating a replacement backup before continuing`, - ); - } - - const snapshot = await createBackupArchive({ - output: (() => { - const outputDir = params.outputDir ?? resolveMatrixMigrationSnapshotOutputDir(env); - fs.mkdirSync(outputDir, { recursive: true }); - return outputDir; - })(), - includeWorkspace: false, - }); - - const marker: MatrixMigrationSnapshotMarker = { - version: 1, - createdAt: snapshot.createdAt, - archivePath: snapshot.archivePath, - trigger: params.trigger, - includeWorkspace: snapshot.includeWorkspace, - }; - await writeJsonFileAtomically(markerPath, marker); - params.log?.info?.(`matrix: created pre-migration backup snapshot: ${snapshot.archivePath}`); - return { - created: true, - archivePath: snapshot.archivePath, - markerPath, - }; -} diff --git a/extensions/matrix/src/migration-snapshot.test.ts b/extensions/matrix/src/migration-snapshot.test.ts deleted file mode 100644 index c1bf15b8df70..000000000000 --- a/extensions/matrix/src/migration-snapshot.test.ts +++ /dev/null @@ -1,171 +0,0 @@ -// Matrix tests cover migration snapshot plugin behavior. -import fs from "node:fs"; -import path from "node:path"; -import { withTempHome } from "openclaw/plugin-sdk/test-env"; -import { beforeEach, describe, expect, it, vi } from "vitest"; - -const legacyCryptoInspectorAvailability = vi.hoisted(() => ({ - available: true, -})); - -vi.mock("./legacy-crypto-inspector-availability.js", () => ({ - isMatrixLegacyCryptoInspectorAvailable: () => legacyCryptoInspectorAvailability.available, -})); - -import { detectLegacyMatrixCrypto } from "./legacy-crypto.js"; -import { - hasActionableMatrixMigration, - maybeCreateMatrixMigrationSnapshot, - resolveMatrixMigrationSnapshotMarkerPath, - resolveMatrixMigrationSnapshotOutputDir, -} from "./migration-snapshot.js"; -import { resolveMatrixAccountStorageRoot } from "./storage-paths.js"; - -const createBackupArchiveMock = vi.hoisted(() => vi.fn()); - -const MATRIX_CREDENTIALS = { - homeserver: "https://matrix.example.org", - userId: "@bot:example.org", - accessToken: "tok-123", -} as const; - -function makeMatrixMigrationConfig() { - return { - channels: { - matrix: MATRIX_CREDENTIALS, - }, - } as never; -} - -function seedLegacyMatrixCrypto(home: string) { - const stateDir = path.join(home, ".openclaw"); - const { rootDir } = resolveMatrixAccountStorageRoot({ - stateDir, - ...MATRIX_CREDENTIALS, - }); - fs.mkdirSync(path.join(rootDir, "crypto"), { recursive: true }); - fs.writeFileSync( - path.join(rootDir, "crypto", "bot-sdk.json"), - JSON.stringify({ deviceId: "DEVICE123" }), - "utf8", - ); -} - -describe("matrix migration snapshots", () => { - beforeEach(() => { - createBackupArchiveMock.mockReset(); - legacyCryptoInspectorAvailability.available = true; - createBackupArchiveMock.mockImplementation( - async (params: { output?: string; includeWorkspace?: boolean }) => { - const outputDir = params.output; - if (!outputDir) { - throw new Error("expected migration snapshot output dir"); - } - fs.mkdirSync(outputDir, { recursive: true }); - const archivePath = path.join(outputDir, "matrix-migration-backup.tar.gz"); - fs.writeFileSync(archivePath, "archive\n", "utf8"); - return { - createdAt: "2026-04-05T00:00:00.000Z", - archivePath, - includeWorkspace: params.includeWorkspace ?? true, - }; - }, - ); - }); - - it("creates a backup marker after writing a pre-migration snapshot", async () => { - await withTempHome(async (home) => { - fs.writeFileSync(path.join(home, ".openclaw", "openclaw.json"), "{}\n", "utf8"); - fs.writeFileSync(path.join(home, ".openclaw", "state.txt"), "state\n", "utf8"); - - const result = await maybeCreateMatrixMigrationSnapshot({ - trigger: "unit-test", - createBackupArchive: createBackupArchiveMock, - }); - - expect(result.created).toBe(true); - expect(result.markerPath).toBe(resolveMatrixMigrationSnapshotMarkerPath(process.env)); - expect( - result.archivePath.startsWith(resolveMatrixMigrationSnapshotOutputDir(process.env)), - ).toBe(true); - expect(fs.existsSync(result.archivePath)).toBe(true); - expect(createBackupArchiveMock).toHaveBeenCalledWith({ - output: resolveMatrixMigrationSnapshotOutputDir(process.env), - includeWorkspace: false, - }); - }); - }); - - it("treats resolvable Matrix legacy state as actionable", async () => { - await withTempHome(async (home) => { - const stateDir = path.join(home, ".openclaw"); - fs.mkdirSync(path.join(stateDir, "matrix"), { recursive: true }); - fs.writeFileSync( - path.join(stateDir, "matrix", "bot-storage.json"), - '{"legacy":true}', - "utf8", - ); - - expect( - hasActionableMatrixMigration({ - cfg: { - channels: { - matrix: { - homeserver: "https://matrix.example.org", - userId: "@bot:example.org", - accessToken: "tok-123", - }, - }, - } as never, - env: process.env, - }), - ).toBe(true); - }); - }); - - it("treats legacy Matrix crypto as actionable when the extension inspector is present", async () => { - await withTempHome(async (home) => { - seedLegacyMatrixCrypto(home); - const cfg = makeMatrixMigrationConfig(); - - const detection = detectLegacyMatrixCrypto({ - cfg, - env: process.env, - }); - expect(detection.inspectorAvailable).toBe(true); - expect(detection.plans).toHaveLength(1); - expect(detection.warnings).toStrictEqual([]); - expect( - hasActionableMatrixMigration({ - cfg, - env: process.env, - }), - ).toBe(true); - }); - }); - - it("keeps legacy Matrix crypto pending but not actionable when the inspector artifact is unavailable", async () => { - legacyCryptoInspectorAvailability.available = false; - - await withTempHome(async (home) => { - seedLegacyMatrixCrypto(home); - const cfg = makeMatrixMigrationConfig(); - - const detection = detectLegacyMatrixCrypto({ - cfg, - env: process.env, - }); - expect(detection.inspectorAvailable).toBe(false); - expect(detection.plans).toHaveLength(1); - expect(detection.warnings).toContain( - "Legacy Matrix encrypted state was detected, but the Matrix crypto inspector is unavailable.", - ); - expect( - hasActionableMatrixMigration({ - cfg, - env: process.env, - }), - ).toBe(false); - }); - }); -}); diff --git a/extensions/matrix/src/migration-snapshot.ts b/extensions/matrix/src/migration-snapshot.ts deleted file mode 100644 index e35060efd4d6..000000000000 --- a/extensions/matrix/src/migration-snapshot.ts +++ /dev/null @@ -1,54 +0,0 @@ -// Matrix plugin module implements migration snapshot behavior. -import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; -import { detectLegacyMatrixCrypto } from "./legacy-crypto.js"; -import { detectLegacyMatrixState } from "./legacy-state.js"; -import { - maybeCreateMatrixMigrationSnapshot, - resolveMatrixMigrationSnapshotMarkerPath, - resolveMatrixMigrationSnapshotOutputDir, -} from "./migration-snapshot-backup.js"; - -export type MatrixMigrationStatus = { - legacyState: ReturnType; - legacyCrypto: ReturnType; - pending: boolean; - actionable: boolean; -}; - -export function resolveMatrixMigrationStatus(params: { - cfg: OpenClawConfig; - env?: NodeJS.ProcessEnv; -}): MatrixMigrationStatus { - const env = params.env ?? process.env; - const legacyState = detectLegacyMatrixState({ cfg: params.cfg, env }); - const legacyCrypto = detectLegacyMatrixCrypto({ cfg: params.cfg, env }); - const actionableLegacyState = legacyState !== null && !("warning" in legacyState); - const actionableLegacyCrypto = legacyCrypto.plans.length > 0 && legacyCrypto.inspectorAvailable; - return { - legacyState, - legacyCrypto, - pending: - legacyState !== null || legacyCrypto.plans.length > 0 || legacyCrypto.warnings.length > 0, - actionable: actionableLegacyState || actionableLegacyCrypto, - }; -} - -export function hasPendingMatrixMigration(params: { - cfg: OpenClawConfig; - env?: NodeJS.ProcessEnv; -}): boolean { - return resolveMatrixMigrationStatus(params).pending; -} - -export function hasActionableMatrixMigration(params: { - cfg: OpenClawConfig; - env?: NodeJS.ProcessEnv; -}): boolean { - return resolveMatrixMigrationStatus(params).actionable; -} - -export { - maybeCreateMatrixMigrationSnapshot, - resolveMatrixMigrationSnapshotMarkerPath, - resolveMatrixMigrationSnapshotOutputDir, -}; diff --git a/extensions/matrix/src/startup-maintenance.test.ts b/extensions/matrix/src/startup-maintenance.test.ts deleted file mode 100644 index 1e63ea578218..000000000000 --- a/extensions/matrix/src/startup-maintenance.test.ts +++ /dev/null @@ -1,231 +0,0 @@ -// Matrix tests cover startup maintenance plugin behavior. -import fs from "node:fs/promises"; -import path from "node:path"; -import { withTempHome } from "openclaw/plugin-sdk/test-env"; -import { beforeEach, describe, expect, it, vi } from "vitest"; - -const legacyCryptoInspectorAvailability = vi.hoisted(() => ({ - available: true, -})); - -vi.mock("./legacy-crypto-inspector-availability.js", () => ({ - isMatrixLegacyCryptoInspectorAvailable: () => legacyCryptoInspectorAvailability.available, -})); - -import { runMatrixStartupMaintenance } from "./startup-maintenance.js"; -import { resolveMatrixAccountStorageRoot } from "./storage-paths.js"; - -async function seedLegacyMatrixState(home: string) { - const stateDir = path.join(home, ".openclaw"); - await fs.mkdir(path.join(stateDir, "matrix"), { recursive: true }); - await fs.writeFile(path.join(stateDir, "matrix", "bot-storage.json"), '{"legacy":true}'); -} - -function makeMatrixStartupConfig(includeCredentials = true) { - return { - channels: { - matrix: includeCredentials - ? { - homeserver: "https://matrix.example.org", - userId: "@bot:example.org", - accessToken: "tok-123", - } - : { - homeserver: "https://matrix.example.org", - }, - }, - } as const; -} - -async function seedLegacyMatrixCrypto(home: string) { - const stateDir = path.join(home, ".openclaw"); - const { rootDir } = resolveMatrixAccountStorageRoot({ - stateDir, - homeserver: "https://matrix.example.org", - userId: "@bot:example.org", - accessToken: "tok-123", - }); - await fs.mkdir(path.join(rootDir, "crypto"), { recursive: true }); - await fs.writeFile( - path.join(rootDir, "crypto", "bot-sdk.json"), - JSON.stringify({ deviceId: "DEVICE123" }), - "utf8", - ); -} - -function createSuccessfulMatrixMigrationDeps() { - return { - maybeCreateMatrixMigrationSnapshot: vi.fn(async () => ({ - created: true, - archivePath: "/tmp/snapshot.tar.gz", - markerPath: "/tmp/migration-snapshot.json", - })), - autoMigrateLegacyMatrixState: vi.fn(async () => ({ - migrated: true, - changes: [], - warnings: [], - })), - }; -} - -function createWarningOnlyMaintenanceHarness() { - return { - deps: { - maybeCreateMatrixMigrationSnapshot: vi.fn(), - autoMigrateLegacyMatrixState: vi.fn(), - autoPrepareLegacyMatrixCrypto: vi.fn(), - }, - log: { - info: vi.fn(), - warn: vi.fn(), - }, - }; -} - -function expectWarningOnlyMaintenanceSkipped( - harness: ReturnType, -) { - expect(harness.deps.maybeCreateMatrixMigrationSnapshot).not.toHaveBeenCalled(); - expect(harness.deps.autoMigrateLegacyMatrixState).not.toHaveBeenCalled(); - expect(harness.deps.autoPrepareLegacyMatrixCrypto).not.toHaveBeenCalled(); - expect(harness.log.info).toHaveBeenCalledWith( - "matrix: migration remains in a warning-only state; no pre-migration snapshot was needed yet", - ); -} - -describe("runMatrixStartupMaintenance", () => { - beforeEach(() => { - legacyCryptoInspectorAvailability.available = true; - }); - - it("creates a snapshot before actionable startup migration", async () => { - await withTempHome(async (home) => { - await seedLegacyMatrixState(home); - const deps = createSuccessfulMatrixMigrationDeps(); - const autoPrepareLegacyMatrixCryptoMock = vi.fn(async () => ({ - migrated: false, - changes: [], - warnings: [], - })); - - await runMatrixStartupMaintenance({ - cfg: makeMatrixStartupConfig(), - env: process.env, - deps: { - maybeCreateMatrixMigrationSnapshot: deps.maybeCreateMatrixMigrationSnapshot, - autoMigrateLegacyMatrixState: deps.autoMigrateLegacyMatrixState, - autoPrepareLegacyMatrixCrypto: autoPrepareLegacyMatrixCryptoMock, - }, - log: {}, - }); - - expect(deps.maybeCreateMatrixMigrationSnapshot).toHaveBeenCalledWith({ - trigger: "gateway-startup", - env: process.env, - log: {}, - }); - expect(deps.autoMigrateLegacyMatrixState).toHaveBeenCalledOnce(); - expect(autoPrepareLegacyMatrixCryptoMock).toHaveBeenCalledOnce(); - }); - }); - - it("skips snapshot creation when startup only has warning-only migration state", async () => { - await withTempHome(async (home) => { - await seedLegacyMatrixState(home); - const harness = createWarningOnlyMaintenanceHarness(); - - await runMatrixStartupMaintenance({ - cfg: makeMatrixStartupConfig(false), - env: process.env, - deps: harness.deps as never, - log: harness.log, - }); - - expectWarningOnlyMaintenanceSkipped(harness); - expect(harness.log.warn).toHaveBeenCalledWith( - `matrix: Legacy Matrix state detected at ${path.join(home, ".openclaw", "matrix")}, but the new account-scoped target could not be resolved yet (need homeserver, userId, and access token for channels.matrix). Start the gateway once with a working Matrix login, or rerun "openclaw doctor --fix" after cached credentials are available.`, - ); - }); - }); - - it("logs the concrete unavailable-inspector warning when startup migration is warning-only", async () => { - legacyCryptoInspectorAvailability.available = false; - - await withTempHome(async (home) => { - await seedLegacyMatrixCrypto(home); - const harness = createWarningOnlyMaintenanceHarness(); - - await runMatrixStartupMaintenance({ - cfg: makeMatrixStartupConfig(), - env: process.env, - deps: harness.deps as never, - log: harness.log, - }); - - expectWarningOnlyMaintenanceSkipped(harness); - expect(harness.log.warn).toHaveBeenCalledWith( - "matrix: legacy encrypted-state warnings:\n- Legacy Matrix encrypted state was detected, but the Matrix crypto inspector is unavailable.", - ); - }); - }); - - it("skips startup migration when snapshot creation fails", async () => { - await withTempHome(async (home) => { - await seedLegacyMatrixState(home); - const maybeCreateMatrixMigrationSnapshotMock = vi.fn(async () => { - throw new Error("backup failed"); - }); - const autoMigrateLegacyMatrixStateMock = vi.fn(); - const autoPrepareLegacyMatrixCryptoMock = vi.fn(); - const warn = vi.fn(); - - await runMatrixStartupMaintenance({ - cfg: makeMatrixStartupConfig(), - env: process.env, - deps: { - maybeCreateMatrixMigrationSnapshot: maybeCreateMatrixMigrationSnapshotMock, - autoMigrateLegacyMatrixState: autoMigrateLegacyMatrixStateMock as never, - autoPrepareLegacyMatrixCrypto: autoPrepareLegacyMatrixCryptoMock as never, - }, - log: { warn }, - }); - - expect(autoMigrateLegacyMatrixStateMock).not.toHaveBeenCalled(); - expect(autoPrepareLegacyMatrixCryptoMock).not.toHaveBeenCalled(); - expect(warn).toHaveBeenCalledWith( - "gateway: failed creating a Matrix migration snapshot; skipping Matrix migration for now: Error: backup failed", - ); - }); - }); - - it("downgrades migration step failures to warnings so startup can continue", async () => { - await withTempHome(async (home) => { - await seedLegacyMatrixState(home); - const deps = createSuccessfulMatrixMigrationDeps(); - const autoPrepareLegacyMatrixCryptoMock = vi.fn(async () => { - throw new Error("disk full"); - }); - const warn = vi.fn(); - - await expect( - runMatrixStartupMaintenance({ - cfg: makeMatrixStartupConfig(), - env: process.env, - deps: { - maybeCreateMatrixMigrationSnapshot: deps.maybeCreateMatrixMigrationSnapshot, - autoMigrateLegacyMatrixState: deps.autoMigrateLegacyMatrixState, - autoPrepareLegacyMatrixCrypto: autoPrepareLegacyMatrixCryptoMock, - }, - log: { warn }, - }), - ).resolves.toBeUndefined(); - - expect(deps.maybeCreateMatrixMigrationSnapshot).toHaveBeenCalledOnce(); - expect(deps.autoMigrateLegacyMatrixState).toHaveBeenCalledOnce(); - expect(autoPrepareLegacyMatrixCryptoMock).toHaveBeenCalledOnce(); - expect(warn).toHaveBeenCalledWith( - "gateway: legacy Matrix encrypted-state preparation failed during Matrix migration; continuing startup: Error: disk full", - ); - }); - }); -}); diff --git a/extensions/matrix/src/startup-maintenance.ts b/extensions/matrix/src/startup-maintenance.ts deleted file mode 100644 index fadc55170e9e..000000000000 --- a/extensions/matrix/src/startup-maintenance.ts +++ /dev/null @@ -1,115 +0,0 @@ -// Matrix plugin module implements startup maintenance behavior. -import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; -import { - autoMigrateLegacyMatrixState, - autoPrepareLegacyMatrixCrypto, - maybeCreateMatrixMigrationSnapshot, - resolveMatrixMigrationStatus, - type MatrixMigrationStatus, -} from "./matrix-migration.runtime.js"; - -type MatrixStartupLogger = { - info?: (message: string) => void; - warn?: (message: string) => void; -}; - -function logWarningOnlyMatrixMigrationReasons(params: { - status: MatrixMigrationStatus; - log: MatrixStartupLogger; -}): void { - if (params.status.legacyState && "warning" in params.status.legacyState) { - params.log.warn?.(`matrix: ${params.status.legacyState.warning}`); - } - - if (params.status.legacyCrypto.warnings.length > 0) { - params.log.warn?.( - `matrix: legacy encrypted-state warnings:\n${params.status.legacyCrypto.warnings.map((entry) => `- ${entry}`).join("\n")}`, - ); - } -} - -async function runBestEffortMatrixMigrationStep(params: { - label: string; - log: MatrixStartupLogger; - logPrefix?: string; - run: () => Promise; -}): Promise { - try { - await params.run(); - } catch (err) { - params.log.warn?.( - `${params.logPrefix?.trim() || "gateway"}: ${params.label} failed during Matrix migration; continuing startup: ${String(err)}`, - ); - } -} - -export async function runMatrixStartupMaintenance(params: { - cfg: OpenClawConfig; - env?: NodeJS.ProcessEnv; - log: MatrixStartupLogger; - trigger?: string; - logPrefix?: string; - deps?: { - maybeCreateMatrixMigrationSnapshot?: typeof maybeCreateMatrixMigrationSnapshot; - autoMigrateLegacyMatrixState?: typeof autoMigrateLegacyMatrixState; - autoPrepareLegacyMatrixCrypto?: typeof autoPrepareLegacyMatrixCrypto; - }; -}): Promise { - const env = params.env ?? process.env; - const createSnapshot = - params.deps?.maybeCreateMatrixMigrationSnapshot ?? maybeCreateMatrixMigrationSnapshot; - const migrateLegacyState = - params.deps?.autoMigrateLegacyMatrixState ?? autoMigrateLegacyMatrixState; - const prepareLegacyCrypto = - params.deps?.autoPrepareLegacyMatrixCrypto ?? autoPrepareLegacyMatrixCrypto; - const trigger = params.trigger?.trim() || "gateway-startup"; - const logPrefix = params.logPrefix?.trim() || "gateway"; - const migrationStatus = resolveMatrixMigrationStatus({ cfg: params.cfg, env }); - - if (!migrationStatus.pending) { - return; - } - if (!migrationStatus.actionable) { - params.log.info?.( - "matrix: migration remains in a warning-only state; no pre-migration snapshot was needed yet", - ); - logWarningOnlyMatrixMigrationReasons({ status: migrationStatus, log: params.log }); - return; - } - - try { - await createSnapshot({ - trigger, - env, - log: params.log, - }); - } catch (err) { - params.log.warn?.( - `${logPrefix}: failed creating a Matrix migration snapshot; skipping Matrix migration for now: ${String(err)}`, - ); - return; - } - - await runBestEffortMatrixMigrationStep({ - label: "legacy Matrix state migration", - log: params.log, - logPrefix, - run: () => - migrateLegacyState({ - cfg: params.cfg, - env, - log: params.log, - }), - }); - await runBestEffortMatrixMigrationStep({ - label: "legacy Matrix encrypted-state preparation", - log: params.log, - logPrefix, - run: () => - prepareLegacyCrypto({ - cfg: params.cfg, - env, - log: params.log, - }), - }); -} diff --git a/extensions/matrix/src/storage-paths.ts b/extensions/matrix/src/storage-paths.ts index 0b7250bbb93b..ff03df68f71d 100644 --- a/extensions/matrix/src/storage-paths.ts +++ b/extensions/matrix/src/storage-paths.ts @@ -46,23 +46,6 @@ export function resolveMatrixCredentialsPath(params: { ); } -export function resolveMatrixLegacyFlatStoreRoot(stateDir: string): string { - return path.join(stateDir, "matrix"); -} - -export function resolveMatrixLegacyFlatStoragePaths(stateDir: string): { - rootDir: string; - storagePath: string; - cryptoPath: string; -} { - const rootDir = resolveMatrixLegacyFlatStoreRoot(stateDir); - return { - rootDir, - storagePath: path.join(rootDir, "bot-storage.json"), - cryptoPath: path.join(rootDir, "crypto"), - }; -} - export function resolveMatrixAccountStorageRoot(params: { stateDir: string; homeserver: string; diff --git a/extensions/msteams/src/conversation-store-state.ts b/extensions/msteams/src/conversation-store-state.ts index a445ad7302e9..63a7bddeab1f 100644 --- a/extensions/msteams/src/conversation-store-state.ts +++ b/extensions/msteams/src/conversation-store-state.ts @@ -224,6 +224,5 @@ export function createMSTeamsConversationStoreState( list, remove, findPreferredDmByUserId, - findByUserId: findPreferredDmByUserId, }; } diff --git a/extensions/msteams/src/conversation-store.shared.test.ts b/extensions/msteams/src/conversation-store.shared.test.ts index 6d9617ecb9ae..76c15713d31c 100644 --- a/extensions/msteams/src/conversation-store.shared.test.ts +++ b/extensions/msteams/src/conversation-store.shared.test.ts @@ -53,7 +53,6 @@ function createMemoryConversationStore( list: async () => toConversationStoreEntries(map.entries()), remove: async (conversationId) => map.delete(normalizeStoredConversationId(conversationId)), findPreferredDmByUserId, - findByUserId: findPreferredDmByUserId, }; } @@ -175,9 +174,6 @@ describe.each(storeFactories)("msteams conversation store ($name)", ({ createSto lastSeenAt: "2026-03-25T20:00:00.000Z", }, }); - await expect(store.findByUserId("user-a")).resolves.toEqual( - await store.findPreferredDmByUserId("user-a"), - ); await expect(store.findPreferredDmByUserId(" ")).resolves.toBeNull(); await expect(store.remove("conv-a")).resolves.toBe(true); diff --git a/extensions/msteams/src/conversation-store.ts b/extensions/msteams/src/conversation-store.ts index a8dfdea7a407..cdf3c4326b14 100644 --- a/extensions/msteams/src/conversation-store.ts +++ b/extensions/msteams/src/conversation-store.ts @@ -17,7 +17,11 @@ export type StoredConversationReference = { user?: { id?: string; name?: string; aadObjectId?: string }; /** Agent/bot that received the message */ agent?: { id?: string; name?: string; aadObjectId?: string } | null; - /** @deprecated legacy field (pre-Agents SDK). Prefer `agent`. */ + /** + * Read-only legacy field: pre-Agents-SDK rows imported raw from the year-TTL + * msteams-conversations.json store may carry `bot` without `agent`. Writers + * are canonical (`agent`); drop this once those imported rows age out. + */ bot?: { id?: string; name?: string }; /** Conversation details */ conversation?: { id?: string; conversationType?: string; tenantId?: string }; @@ -59,6 +63,4 @@ export type MSTeamsConversationStore = { remove: (conversationId: string) => Promise; /** Person-targeted proactive lookup: prefer the freshest personal DM reference. */ findPreferredDmByUserId: (id: string) => Promise; - /** @deprecated Use `findPreferredDmByUserId` for proactive user-targeted sends. */ - findByUserId: (id: string) => Promise; }; diff --git a/extensions/msteams/src/feedback-invoke.ts b/extensions/msteams/src/feedback-invoke.ts index 0f65cd8cdf7a..a19103fbc8a2 100644 --- a/extensions/msteams/src/feedback-invoke.ts +++ b/extensions/msteams/src/feedback-invoke.ts @@ -160,9 +160,6 @@ export async function runMSTeamsFeedbackInvokeHandler( agent: activity.recipient ? { id: activity.recipient.id, name: activity.recipient.name } : undefined, - bot: activity.recipient - ? { id: activity.recipient.id, name: activity.recipient.name } - : undefined, conversation: { id: conversationId, conversationType: activity.conversation?.conversationType, diff --git a/extensions/msteams/src/messenger.test.ts b/extensions/msteams/src/messenger.test.ts index ae202fa473ae..292b06e10faa 100644 --- a/extensions/msteams/src/messenger.test.ts +++ b/extensions/msteams/src/messenger.test.ts @@ -806,6 +806,23 @@ describe("msteams messenger", () => { expect(reference.aadObjectId).toBe("aad-legacy"); }); + it("accepts a legacy bot-only imported reference and resolves the agent from bot", () => { + const botOnly: StoredConversationReference = { + activityId: "activity-bot-only", + user: { id: "user-legacy", name: "Legacy" }, + bot: { id: "bot-legacy", name: "Bot" }, + conversation: { + id: "a:personal-chat", + conversationType: "personal", + tenantId: "tenant-1", + }, + channelId: "msteams", + serviceUrl: "https://smba.trafficmanager.net/amer/", + }; + const reference = buildConversationReference(botOnly); + expect(reference.agent).toEqual({ id: "bot-legacy", name: "Bot" }); + }); + it("omits tenantId and aadObjectId when neither source is available", () => { const minimal: StoredConversationReference = { activityId: "activity-2", diff --git a/extensions/msteams/src/messenger.ts b/extensions/msteams/src/messenger.ts index ed5339b0ffab..a15766568d49 100644 --- a/extensions/msteams/src/messenger.ts +++ b/extensions/msteams/src/messenger.ts @@ -110,6 +110,7 @@ export function buildConversationReference( if (!conversationId) { throw new Error("Invalid stored reference: missing conversation.id"); } + // Legacy imported rows may only carry `bot`; see StoredConversationReference.bot. const agent = ref.agent ?? ref.bot ?? undefined; if (agent == null || !agent.id) { throw new Error("Invalid stored reference: missing agent.id"); diff --git a/extensions/msteams/src/monitor-handler.adaptive-card.test.ts b/extensions/msteams/src/monitor-handler.adaptive-card.test.ts index ff543793878d..43bfb6e084a3 100644 --- a/extensions/msteams/src/monitor-handler.adaptive-card.test.ts +++ b/extensions/msteams/src/monitor-handler.adaptive-card.test.ts @@ -57,7 +57,6 @@ function createDeps(): MSTeamsMessageHandlerDeps { list: vi.fn(async () => []), remove: vi.fn(async () => false), findPreferredDmByUserId: vi.fn(async () => null), - findByUserId: vi.fn(async () => null), } satisfies MSTeamsConversationStore, pollStore: { recordVote: vi.fn(async () => null), diff --git a/extensions/msteams/src/monitor-handler.test-helpers.ts b/extensions/msteams/src/monitor-handler.test-helpers.ts index 7cd537ebad4c..6dfc8e497018 100644 --- a/extensions/msteams/src/monitor-handler.test-helpers.ts +++ b/extensions/msteams/src/monitor-handler.test-helpers.ts @@ -171,7 +171,6 @@ export function createMSTeamsMessageHandlerDeps(params?: { list: async () => [], remove: async () => false, findPreferredDmByUserId: async () => null, - findByUserId: async () => null, }; const pollStore: MSTeamsPollStore = { createPoll: async () => {}, diff --git a/extensions/msteams/src/monitor-handler/message-handler.authz.test.ts b/extensions/msteams/src/monitor-handler/message-handler.authz.test.ts index 1da3dbe8a370..bd16abe43f41 100644 --- a/extensions/msteams/src/monitor-handler/message-handler.authz.test.ts +++ b/extensions/msteams/src/monitor-handler/message-handler.authz.test.ts @@ -440,10 +440,6 @@ describe("msteams monitor handler authz", () => { id: "bot-id", name: "Bot", }, - bot: { - id: "bot-id", - name: "Bot", - }, conversation: { id: "a:personal-chat", conversationType: "personal", diff --git a/extensions/msteams/src/monitor-handler/message-handler.test-support.ts b/extensions/msteams/src/monitor-handler/message-handler.test-support.ts index 4de737938b75..d3eeff53cf59 100644 --- a/extensions/msteams/src/monitor-handler/message-handler.test-support.ts +++ b/extensions/msteams/src/monitor-handler/message-handler.test-support.ts @@ -65,7 +65,6 @@ export function createMessageHandlerDeps( list: vi.fn(async () => []), remove: vi.fn(async () => false), findPreferredDmByUserId: vi.fn(async () => null), - findByUserId: vi.fn(async () => null), } satisfies MSTeamsMessageHandlerDeps["conversationStore"]; const deps: MSTeamsMessageHandlerDeps = { diff --git a/extensions/msteams/src/monitor-handler/message-handler.ts b/extensions/msteams/src/monitor-handler/message-handler.ts index 2a052ec947e3..6e83c0a02f43 100644 --- a/extensions/msteams/src/monitor-handler/message-handler.ts +++ b/extensions/msteams/src/monitor-handler/message-handler.ts @@ -157,7 +157,6 @@ function buildStoredConversationReference(params: { activityId: activity.id, user: from ? { id: from.id, name: from.name, aadObjectId: from.aadObjectId } : undefined, agent, - bot: agent ? { id: agent.id, name: agent.name } : undefined, conversation: { id: conversationId, conversationType, diff --git a/extensions/msteams/src/sdk-proactive.test.ts b/extensions/msteams/src/sdk-proactive.test.ts index ce1a4a7b73e1..6e23fe4b16c2 100644 --- a/extensions/msteams/src/sdk-proactive.test.ts +++ b/extensions/msteams/src/sdk-proactive.test.ts @@ -38,6 +38,33 @@ describe("sendMSTeamsActivityWithReference", () => { vi.unstubAllEnvs(); }); + it("sends a legacy bot-only imported reference instead of rejecting it", async () => { + vi.stubEnv("SERVICE_URL", "https://bot.example.com/api/messages"); + const app = { + client: { request: vi.fn() }, + api: { serviceUrl: "https://smba.trafficmanager.net/teams" }, + } as unknown as MSTeamsApp; + + const result = await sendMSTeamsActivityWithReference( + app, + { + serviceUrl: "https://smba.trafficmanager.net/amer/", + bot: { id: "28:legacy-bot", name: "OpenClaw" }, + user: { id: "29:user" }, + conversation: { + id: "19:conversation@thread.tacv2", + conversationType: "personal", + tenantId: "tenant-1", + }, + channelId: "msteams", + }, + { type: "message", text: "hello" }, + ); + + expect(result).toMatchObject({ id: "activity-1" }); + expect(clientState.create).toHaveBeenCalledTimes(1); + }); + it("sends through a reference-scoped API client without the protected SDK activitySender", async () => { vi.stubEnv("SERVICE_URL", "https://bot.example.com/api/messages"); const httpClient = { request: vi.fn() }; diff --git a/extensions/msteams/src/sdk-proactive.ts b/extensions/msteams/src/sdk-proactive.ts index e34b2f516291..6e99775c0d5a 100644 --- a/extensions/msteams/src/sdk-proactive.ts +++ b/extensions/msteams/src/sdk-proactive.ts @@ -18,6 +18,7 @@ type MSTeamsSdkReferenceSource = { activityId?: string; user?: MSTeamsAccountRef; agent?: MSTeamsAccountRef | null; + /** Legacy imported rows may only carry `bot`; see StoredConversationReference.bot. */ bot?: MSTeamsAccountRef | null; conversation: { id: string; conversationType?: string; tenantId?: string }; channelId?: string; diff --git a/extensions/msteams/src/send-context.test.ts b/extensions/msteams/src/send-context.test.ts index affb8d095743..845ab9aef20f 100644 --- a/extensions/msteams/src/send-context.test.ts +++ b/extensions/msteams/src/send-context.test.ts @@ -12,7 +12,6 @@ const sendContextMockState = vi.hoisted(() => { list: vi.fn(), remove: vi.fn(), findPreferredDmByUserId: vi.fn(), - findByUserId: vi.fn(), }; return { store, @@ -57,7 +56,6 @@ beforeEach(() => { sendContextMockState.store.list.mockReset(); sendContextMockState.store.remove.mockReset(); sendContextMockState.store.findPreferredDmByUserId.mockReset(); - sendContextMockState.store.findByUserId.mockReset(); sendContextMockState.loadMSTeamsSdkWithAuth.mockClear(); sendContextMockState.createMSTeamsTokenProvider.mockClear(); sendContextMockState.getAccessToken.mockReset(); diff --git a/extensions/ollama/runtime-api.ts b/extensions/ollama/runtime-api.ts index 6b53660a6c5c..f49f8254b3d6 100644 --- a/extensions/ollama/runtime-api.ts +++ b/extensions/ollama/runtime-api.ts @@ -4,7 +4,6 @@ export { buildOllamaChatRequest, createConfiguredOllamaCompatStreamWrapper, convertToOllamaMessages, - createConfiguredOllamaCompatNumCtxWrapper, createConfiguredOllamaStreamFn, createOllamaStreamFn, isOllamaCompatProvider, diff --git a/extensions/ollama/src/stream.ts b/extensions/ollama/src/stream.ts index 13fcac565f7d..562639124ae7 100644 --- a/extensions/ollama/src/stream.ts +++ b/extensions/ollama/src/stream.ts @@ -488,9 +488,6 @@ export function createConfiguredOllamaCompatStreamWrapper( return streamFn; } -/** @deprecated Use createConfiguredOllamaCompatStreamWrapper. */ -export const createConfiguredOllamaCompatNumCtxWrapper = createConfiguredOllamaCompatStreamWrapper; - export function buildOllamaChatRequest(params: { modelId: string; providerId?: string; diff --git a/extensions/xai/.boundary-stubs/ollama-runtime-api.d.ts b/extensions/xai/.boundary-stubs/ollama-runtime-api.d.ts index 3b6eed044d75..1fcaf053f9d4 100644 --- a/extensions/xai/.boundary-stubs/ollama-runtime-api.d.ts +++ b/extensions/xai/.boundary-stubs/ollama-runtime-api.d.ts @@ -4,7 +4,6 @@ export type OllamaEmbeddingClient = unknown; export const buildAssistantMessage: (...args: unknown[]) => unknown; export const buildOllamaChatRequest: (...args: unknown[]) => unknown; export const convertToOllamaMessages: (...args: unknown[]) => unknown; -export const createConfiguredOllamaCompatNumCtxWrapper: (...args: unknown[]) => unknown; export const createConfiguredOllamaCompatStreamWrapper: (...args: unknown[]) => unknown; export const createConfiguredOllamaStreamFn: (...args: unknown[]) => unknown; export const createOllamaStreamFn: (...args: unknown[]) => unknown; diff --git a/package.json b/package.json index 72831fb19c7a..1bb7c7e234a2 100644 --- a/package.json +++ b/package.json @@ -1520,7 +1520,7 @@ "audit:seams": "node scripts/audit-seams.mjs", "build": "node scripts/build-all.mjs", "build:ci-artifacts": "node scripts/build-all.mjs ciArtifacts", - "build:docker": "node scripts/tsdown-build.mjs && node scripts/check-cli-bootstrap-imports.mjs && node scripts/runtime-postbuild.mjs && node scripts/build-stamp.mjs && node scripts/runtime-postbuild-stamp.mjs && pnpm plugins:assets:build && pnpm plugins:assets:copy && node --import tsx scripts/copy-hook-metadata.ts && node --import tsx scripts/copy-export-html-templates.ts && node --import tsx scripts/write-build-info.ts && node --import tsx scripts/write-cli-startup-metadata.ts && node --import tsx scripts/write-cli-compat.ts", + "build:docker": "node scripts/tsdown-build.mjs && node scripts/check-cli-bootstrap-imports.mjs && node scripts/runtime-postbuild.mjs && node scripts/build-stamp.mjs && node scripts/runtime-postbuild-stamp.mjs && pnpm plugins:assets:build && pnpm plugins:assets:copy && node --import tsx scripts/copy-hook-metadata.ts && node --import tsx scripts/copy-export-html-templates.ts && node --import tsx scripts/write-build-info.ts && node --import tsx scripts/write-cli-startup-metadata.ts", "build:plugin-sdk:dts": "node scripts/run-tsgo.mjs -p tsconfig.plugin-sdk.dts.json --declaration true", "build:plugin-sdk:strict-smoke": "node scripts/tsdown-build.mjs && node scripts/runtime-postbuild.mjs && node scripts/run-with-env.mjs OPENCLAW_PLUGIN_SDK_CANONICAL_DTS=1 -- node --import tsx scripts/write-plugin-sdk-entry-dts.ts && node scripts/check-plugin-sdk-exports.mjs", "build:strict-smoke": "pnpm plugins:assets:build && node scripts/tsdown-build.mjs && node scripts/check-cli-bootstrap-imports.mjs && node scripts/runtime-postbuild.mjs && node scripts/build-stamp.mjs && node scripts/runtime-postbuild-stamp.mjs && node scripts/run-with-env.mjs OPENCLAW_PLUGIN_SDK_CANONICAL_DTS=1 -- node --import tsx scripts/write-plugin-sdk-entry-dts.ts && node scripts/check-plugin-sdk-exports.mjs", diff --git a/packages/gateway-client/src/client.ts b/packages/gateway-client/src/client.ts index 5e4c2e106cc9..e4e7c74f3a3c 100644 --- a/packages/gateway-client/src/client.ts +++ b/packages/gateway-client/src/client.ts @@ -355,8 +355,6 @@ export type GatewayClientOptions = { url?: string; // ws://127.0.0.1:18789 origin?: string; connectChallengeTimeoutMs?: number; - /** @deprecated Use connectChallengeTimeoutMs. */ - connectDelayMs?: number; /** * Server-side pre-auth handshake budget. Config-derived local clients use * this to keep the connect-challenge watchdog aligned with the gateway. @@ -418,7 +416,7 @@ export function describeGatewayCloseCode(code: number): string | undefined { } function readConnectChallengeTimeoutOverride( - opts: Pick, + opts: Pick, ): number | undefined { if ( typeof opts.connectChallengeTimeoutMs === "number" && @@ -426,9 +424,6 @@ function readConnectChallengeTimeoutOverride( ) { return opts.connectChallengeTimeoutMs; } - if (typeof opts.connectDelayMs === "number" && Number.isFinite(opts.connectDelayMs)) { - return opts.connectDelayMs; - } return undefined; } @@ -450,7 +445,7 @@ function formatGatewayClientErrorForLog(err: unknown): string { export function resolveGatewayClientConnectChallengeTimeoutMs( opts: Pick< GatewayClientOptions, - "connectChallengeTimeoutMs" | "connectDelayMs" | "env" | "preauthHandshakeTimeoutMs" + "connectChallengeTimeoutMs" | "env" | "preauthHandshakeTimeoutMs" >, ): number { return resolveConnectChallengeTimeoutMs(readConnectChallengeTimeoutOverride(opts), { diff --git a/packages/gateway-client/src/client.watchdog.test.ts b/packages/gateway-client/src/client.watchdog.test.ts index f0087faebac5..308d7728a1cd 100644 --- a/packages/gateway-client/src/client.watchdog.test.ts +++ b/packages/gateway-client/src/client.watchdog.test.ts @@ -152,19 +152,18 @@ describe("GatewayClient", () => { client.stop(); }); - test("prefers connectChallengeTimeoutMs and still honors the legacy alias", () => { + test("resolves connectChallengeTimeoutMs with clamping and config fallback", () => { expect(resolveGatewayClientConnectChallengeTimeoutMs({})).toBe( DEFAULT_PREAUTH_HANDSHAKE_TIMEOUT_MS, ); - expect(resolveGatewayClientConnectChallengeTimeoutMs({ connectDelayMs: 0 })).toBe( + expect(resolveGatewayClientConnectChallengeTimeoutMs({ connectChallengeTimeoutMs: 0 })).toBe( MIN_CONNECT_CHALLENGE_TIMEOUT_MS, ); - expect(resolveGatewayClientConnectChallengeTimeoutMs({ connectDelayMs: 20_000 })).toBe( - MAX_CONNECT_CHALLENGE_TIMEOUT_MS, - ); + expect( + resolveGatewayClientConnectChallengeTimeoutMs({ connectChallengeTimeoutMs: 20_000 }), + ).toBe(MAX_CONNECT_CHALLENGE_TIMEOUT_MS); expect( resolveGatewayClientConnectChallengeTimeoutMs({ - connectDelayMs: 2_000, connectChallengeTimeoutMs: 5_000, }), ).toBe(5_000); diff --git a/packages/gateway-client/src/readiness.ts b/packages/gateway-client/src/readiness.ts index 09b63029c202..fba370659846 100644 --- a/packages/gateway-client/src/readiness.ts +++ b/packages/gateway-client/src/readiness.ts @@ -21,7 +21,7 @@ export type GatewayClientStartReadinessOptions = { timeoutMs?: number; clientOptions?: Pick< GatewayClientOptions, - "connectChallengeTimeoutMs" | "connectDelayMs" | "env" | "preauthHandshakeTimeoutMs" + "connectChallengeTimeoutMs" | "env" | "preauthHandshakeTimeoutMs" >; signal?: AbortSignal; }; @@ -37,10 +37,7 @@ function resolveGatewayClientStartReadinessTimeoutMs( typeof clientOptions.connectChallengeTimeoutMs === "number" && Number.isFinite(clientOptions.connectChallengeTimeoutMs) ? clientOptions.connectChallengeTimeoutMs - : typeof clientOptions.connectDelayMs === "number" && - Number.isFinite(clientOptions.connectDelayMs) - ? clientOptions.connectDelayMs - : undefined; + : undefined; return resolveConnectChallengeTimeoutMs(timeoutOverride, { env: clientOptions.env, configuredTimeoutMs: clientOptions.preauthHandshakeTimeoutMs, diff --git a/packages/net-policy/src/ipv4.test.ts b/packages/net-policy/src/ipv4.test.ts index ca1f8a83e44d..3a30f546ebe7 100644 --- a/packages/net-policy/src/ipv4.test.ts +++ b/packages/net-policy/src/ipv4.test.ts @@ -1,6 +1,6 @@ // Network Policy tests cover ipv4 behavior. import { describe, expect, it } from "vitest"; -import { validateDottedDecimalIPv4Input, validateIPv4AddressInput } from "./ipv4.js"; +import { validateDottedDecimalIPv4Input } from "./ipv4.js"; describe("net-policy/ipv4", () => { it("requires a value for custom bind mode", () => { @@ -28,9 +28,4 @@ describe("net-policy/ipv4", () => { "Invalid IPv4 address (e.g., 192.168.1.100)", ); }); - - it("keeps the backward-compatible alias wired to the same validation", () => { - expect(validateIPv4AddressInput("192.168.1.100")).toBeUndefined(); - expect(validateIPv4AddressInput("bad-ip")).toBe("Invalid IPv4 address (e.g., 192.168.1.100)"); - }); }); diff --git a/packages/net-policy/src/ipv4.ts b/packages/net-policy/src/ipv4.ts index 19408eb046b9..91cfa10b764c 100644 --- a/packages/net-policy/src/ipv4.ts +++ b/packages/net-policy/src/ipv4.ts @@ -11,8 +11,3 @@ export function validateDottedDecimalIPv4Input(value: string | undefined): strin } return "Invalid IPv4 address (e.g., 192.168.1.100)"; } - -/** @deprecated Use validateDottedDecimalIPv4Input. */ -export function validateIPv4AddressInput(value: string | undefined): string | undefined { - return validateDottedDecimalIPv4Input(value); -} diff --git a/packages/sdk/src/transport.ts b/packages/sdk/src/transport.ts index 4e1a5acb68b3..a0cb94419d0c 100644 --- a/packages/sdk/src/transport.ts +++ b/packages/sdk/src/transport.ts @@ -25,7 +25,6 @@ const RAW_EVENT_REPLAY_LIMIT = 1000; export type GatewayClientTransportOptions = { url?: string; connectChallengeTimeoutMs?: number; - connectDelayMs?: number; preauthHandshakeTimeoutMs?: number; tickWatchMinIntervalMs?: number; requestTimeoutMs?: number; diff --git a/scripts/build-all.mjs b/scripts/build-all.mjs index 4101369a8de1..7e5b16b0a989 100644 --- a/scripts/build-all.mjs +++ b/scripts/build-all.mjs @@ -113,11 +113,6 @@ export const BUILD_ALL_STEPS = [ kind: "node", args: ["--import", "tsx", "scripts/write-cli-startup-metadata.ts"], }, - { - label: "write-cli-compat", - kind: "node", - args: ["--import", "tsx", "scripts/write-cli-compat.ts"], - }, ]; export const BUILD_ALL_PROFILES = { @@ -137,7 +132,6 @@ export const BUILD_ALL_PROFILES = { "ui:build", "write-build-info", "write-cli-startup-metadata", - "write-cli-compat", ], gatewayWatch: [ "tsdown", @@ -164,7 +158,6 @@ export const BUILD_ALL_PROFILES = { "build-stamp", "runtime-postbuild-stamp", "write-cli-startup-metadata", - "write-cli-compat", ], cliStartup: [ "tsdown", @@ -173,7 +166,6 @@ export const BUILD_ALL_PROFILES = { "build-stamp", "runtime-postbuild-stamp", "write-cli-startup-metadata", - "write-cli-compat", ], }; diff --git a/scripts/deadcode-unused-files.allowlist.mjs b/scripts/deadcode-unused-files.allowlist.mjs index 4bddd41e2cec..c03c71a5b05f 100644 --- a/scripts/deadcode-unused-files.allowlist.mjs +++ b/scripts/deadcode-unused-files.allowlist.mjs @@ -18,7 +18,6 @@ export const KNIP_OPTIONAL_UNUSED_FILE_ALLOWLIST = [ "extensions/matrix/src/plugin-entry.runtime.js", "src/agents/subagent-registry.runtime.ts", "src/auto-reply/reply/get-reply.test-loader.ts", - "src/cli/daemon-cli-compat.ts", "src/commands/doctor/shared/deprecation-compat.ts", "src/config/doc-baseline.runtime.ts", "src/config/doc-baseline.ts", diff --git a/scripts/plugin-sdk-surface-report.mjs b/scripts/plugin-sdk-surface-report.mjs index 715d24b3d316..a3b2c84613ac 100644 --- a/scripts/plugin-sdk-surface-report.mjs +++ b/scripts/plugin-sdk-surface-report.mjs @@ -195,12 +195,12 @@ export function readPluginSdkSurfaceBudgets(env = process.env) { ), publicExports: readPluginSdkSurfaceBudgetEnv( "OPENCLAW_PLUGIN_SDK_MAX_PUBLIC_EXPORTS", - 10542, + 10540, env, ), publicFunctionExports: readPluginSdkSurfaceBudgetEnv( "OPENCLAW_PLUGIN_SDK_MAX_PUBLIC_FUNCTION_EXPORTS", - 5252, + 5250, env, ), publicDeprecatedExports: readPluginSdkSurfaceBudgetEnv( diff --git a/scripts/write-cli-compat.ts b/scripts/write-cli-compat.ts deleted file mode 100644 index 78190551b7fb..000000000000 --- a/scripts/write-cli-compat.ts +++ /dev/null @@ -1,194 +0,0 @@ -// Write Cli Compat script supports OpenClaw repository automation. -import fs from "node:fs"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; -import { - LEGACY_DAEMON_CLI_EXPORTS, - resolveLegacyDaemonCliAccessors, - resolveLegacyDaemonCliRegisterAccessor, - resolveLegacyDaemonCliRunnerAccessors, -} from "../src/cli/daemon-cli-compat.ts"; - -const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); -const distDir = path.join(rootDir, "dist"); -const cliDir = path.join(distDir, "cli"); - -const findCandidates = () => - fs.readdirSync(distDir).filter((entry) => { - const isDaemonCliBundle = - entry === "daemon-cli.js" || entry === "daemon-cli.mjs" || entry.startsWith("daemon-cli-"); - if (!isDaemonCliBundle) { - return false; - } - // tsdown can emit either .js or .mjs depending on bundler settings/runtime. - return entry.endsWith(".js") || entry.endsWith(".mjs"); - }); - -const findRunnerCandidates = () => - fs.readdirSync(distDir).filter((entry) => { - const isRunnerBundle = - entry === "runners.js" || - entry === "runners.mjs" || - entry.startsWith("runners-") || - entry === "install.runtime.js" || - entry === "install.runtime.mjs" || - entry.startsWith("install.runtime-") || - entry === "lifecycle.runtime.js" || - entry === "lifecycle.runtime.mjs" || - entry.startsWith("lifecycle.runtime-") || - entry === "status.runtime.js" || - entry === "status.runtime.mjs" || - entry.startsWith("status.runtime-"); - if (!isRunnerBundle) { - return false; - } - return entry.endsWith(".js") || entry.endsWith(".mjs"); - }); - -// In rare cases, build output can land slightly after this script starts (depending on FS timing). -// Retry briefly to avoid flaky builds. -let candidates = findCandidates(); -for (let i = 0; i < 10 && candidates.length === 0; i++) { - await new Promise((resolve) => { - setTimeout(resolve, 50); - }); - candidates = findCandidates(); -} -let runnerCandidates = findRunnerCandidates(); -for (let i = 0; i < 10 && runnerCandidates.length === 0; i++) { - await new Promise((resolve) => { - setTimeout(resolve, 50); - }); - runnerCandidates = findRunnerCandidates(); -} - -if (candidates.length === 0) { - throw new Error("No daemon-cli bundle found in dist; cannot write legacy CLI shim."); -} - -const orderedCandidates = candidates.toSorted(); -const resolved = orderedCandidates - .map((entry) => { - const source = fs.readFileSync(path.join(distDir, entry), "utf8"); - const accessors = resolveLegacyDaemonCliAccessors(source); - return { entry, accessors }; - }) - .find((entry) => Boolean(entry.accessors)); -const orderedRunnerCandidates = runnerCandidates.toSorted(); - -let daemonTarget: string; -let accessors: Partial>; -let accessorSources: Partial>; -let extraRunnerTargets: Array<{ entry: string; binding: string }>; - -if (resolved?.accessors) { - daemonTarget = resolved.entry; - extraRunnerTargets = []; - accessors = resolved.accessors; - accessorSources = Object.fromEntries( - Object.keys(resolved.accessors).map((key) => [key, "daemonCli"]), - ) as typeof accessorSources; -} else { - const registerResolved = orderedCandidates - .map((entry) => { - const source = fs.readFileSync(path.join(distDir, entry), "utf8"); - const accessor = resolveLegacyDaemonCliRegisterAccessor(source); - return { entry, accessor }; - }) - .find((entry) => Boolean(entry.accessor)); - const runnerAccessors = new Map< - Exclude<(typeof LEGACY_DAEMON_CLI_EXPORTS)[number], "registerDaemonCli">, - { accessor: string; entry: string } - >(); - for (const entry of orderedRunnerCandidates) { - const source = fs.readFileSync(path.join(distDir, entry), "utf8"); - const resolvedAccessors = resolveLegacyDaemonCliRunnerAccessors(source); - if (!resolvedAccessors) { - continue; - } - for (const [name, accessor] of Object.entries(resolvedAccessors)) { - if ( - !accessor || - runnerAccessors.has( - name as Exclude<(typeof LEGACY_DAEMON_CLI_EXPORTS)[number], "registerDaemonCli">, - ) - ) { - continue; - } - runnerAccessors.set( - name as Exclude<(typeof LEGACY_DAEMON_CLI_EXPORTS)[number], "registerDaemonCli">, - { accessor, entry }, - ); - } - } - - if (!registerResolved?.accessor || !runnerAccessors.get("runDaemonRestart")) { - throw new Error( - `Could not resolve daemon-cli export aliases from dist bundles: ${orderedCandidates.join(", ")} | runners: ${orderedRunnerCandidates.join(", ")}`, - ); - } - - daemonTarget = registerResolved.entry; - const runnerBindingByEntry = new Map(); - extraRunnerTargets = []; - for (const { entry } of runnerAccessors.values()) { - if (runnerBindingByEntry.has(entry)) { - continue; - } - const binding = `daemonCliRunners${runnerBindingByEntry.size}`; - runnerBindingByEntry.set(entry, binding); - extraRunnerTargets.push({ entry, binding }); - } - accessors = { - registerDaemonCli: registerResolved.accessor, - ...Object.fromEntries( - [...runnerAccessors.entries()].map(([name, value]) => [name, value.accessor]), - ), - }; - accessorSources = { - registerDaemonCli: "daemonCli", - runDaemonInstall: runnerAccessors.get("runDaemonInstall") - ? runnerBindingByEntry.get(runnerAccessors.get("runDaemonInstall")!.entry) - : undefined, - runDaemonRestart: runnerBindingByEntry.get(runnerAccessors.get("runDaemonRestart")!.entry)!, - runDaemonStart: runnerAccessors.get("runDaemonStart") - ? runnerBindingByEntry.get(runnerAccessors.get("runDaemonStart")!.entry) - : undefined, - runDaemonStatus: runnerAccessors.get("runDaemonStatus") - ? runnerBindingByEntry.get(runnerAccessors.get("runDaemonStatus")!.entry) - : undefined, - runDaemonStop: runnerAccessors.get("runDaemonStop") - ? runnerBindingByEntry.get(runnerAccessors.get("runDaemonStop")!.entry) - : undefined, - runDaemonUninstall: runnerAccessors.get("runDaemonUninstall") - ? runnerBindingByEntry.get(runnerAccessors.get("runDaemonUninstall")!.entry) - : undefined, - }; -} - -const missingExportError = (name: string) => - `Legacy daemon CLI export "${name}" is unavailable in this build. Please upgrade OpenClaw.`; -const buildExportLine = (name: (typeof LEGACY_DAEMON_CLI_EXPORTS)[number]) => { - const accessor = accessors[name]; - if (accessor) { - const sourceBinding = accessorSources[name] ?? "daemonCli"; - return `export const ${name} = ${sourceBinding}.${accessor};`; - } - if (name === "registerDaemonCli") { - return `export const ${name} = () => { throw new Error(${JSON.stringify(missingExportError(name))}); };`; - } - return `export const ${name} = async () => { throw new Error(${JSON.stringify(missingExportError(name))}); };`; -}; - -const contents = - "// Legacy shim for pre-tsdown update-cli imports.\n" + - `import * as daemonCli from "../${daemonTarget}";\n` + - extraRunnerTargets - .map(({ entry, binding }) => `import * as ${binding} from "../${entry}";`) - .join("\n") + - (extraRunnerTargets.length > 0 ? "\n" : "") + - LEGACY_DAEMON_CLI_EXPORTS.map(buildExportLine).join("\n") + - "\n"; - -fs.mkdirSync(cliDir, { recursive: true }); -fs.writeFileSync(path.join(cliDir, "daemon-cli.js"), contents); diff --git a/src/cli/daemon-cli-compat.test.ts b/src/cli/daemon-cli-compat.test.ts deleted file mode 100644 index 757939e85c3d..000000000000 --- a/src/cli/daemon-cli-compat.test.ts +++ /dev/null @@ -1,86 +0,0 @@ -// Daemon CLI compatibility tests cover legacy daemon command aliases and output. -import { describe, expect, it } from "vitest"; -import { - resolveLegacyDaemonCliAccessors, - resolveLegacyDaemonCliRegisterAccessor, - resolveLegacyDaemonCliRunnerAccessors, -} from "./daemon-cli-compat.js"; - -describe("resolveLegacyDaemonCliAccessors", () => { - it("resolves aliased daemon-cli exports from a bundled chunk", () => { - const bundle = ` - var daemon_cli_exports = /* @__PURE__ */ __exportAll({ registerDaemonCli: () => registerDaemonCli }); - export { runDaemonStop as a, runDaemonStart as i, runDaemonStatus as n, runDaemonUninstall as o, runDaemonRestart as r, runDaemonInstall as s, daemon_cli_exports as t }; - `; - - expect(resolveLegacyDaemonCliAccessors(bundle)).toEqual({ - registerDaemonCli: "t.registerDaemonCli", - runDaemonInstall: "s", - runDaemonRestart: "r", - runDaemonStart: "i", - runDaemonStatus: "n", - runDaemonStop: "a", - runDaemonUninstall: "o", - }); - }); - - it("returns null when required aliases are missing", () => { - const bundle = ` - var daemon_cli_exports = /* @__PURE__ */ __exportAll({ registerDaemonCli: () => registerDaemonCli }); - export { runDaemonRestart as r, daemon_cli_exports as t }; - `; - - expect(resolveLegacyDaemonCliAccessors(bundle)).toEqual({ - registerDaemonCli: "t.registerDaemonCli", - runDaemonRestart: "r", - }); - }); - - it("returns null when the required restart alias is missing", () => { - const bundle = ` - var daemon_cli_exports = /* @__PURE__ */ __exportAll({ registerDaemonCli: () => registerDaemonCli }); - export { daemon_cli_exports as t }; - `; - - expect(resolveLegacyDaemonCliAccessors(bundle)).toBeNull(); - }); - - it("resolves split register and runner bundles", () => { - const daemonBundle = ` - var daemon_cli_exports = /* @__PURE__ */ __exportAll({ registerDaemonCli: () => registerDaemonCli }); - export { addGatewayServiceCommands as n, daemon_cli_exports as t }; - `; - const runnerBundle = ` - export { runDaemonInstall as a, runDaemonUninstall as i, runDaemonStart as n, runDaemonStop as r, runDaemonRestart as t, runDaemonStatus as u }; - `; - - expect(resolveLegacyDaemonCliRegisterAccessor(daemonBundle)).toBe("t.registerDaemonCli"); - expect(resolveLegacyDaemonCliRunnerAccessors(runnerBundle)).toEqual({ - runDaemonInstall: "a", - runDaemonRestart: "t", - runDaemonStart: "n", - runDaemonStatus: "u", - runDaemonStop: "r", - runDaemonUninstall: "i", - }); - }); - - it("resolves partial runner bundles for split runtime chunks", () => { - const installRuntimeBundle = ` - export { runDaemonInstall }; - `; - const lifecycleRuntimeBundle = ` - export { runDaemonRestart as t, runDaemonStart as n, runDaemonStop as r, runDaemonUninstall as i }; - `; - - expect(resolveLegacyDaemonCliRunnerAccessors(installRuntimeBundle)).toEqual({ - runDaemonInstall: "runDaemonInstall", - }); - expect(resolveLegacyDaemonCliRunnerAccessors(lifecycleRuntimeBundle)).toEqual({ - runDaemonRestart: "t", - runDaemonStart: "n", - runDaemonStop: "r", - runDaemonUninstall: "i", - }); - }); -}); diff --git a/src/cli/daemon-cli-compat.ts b/src/cli/daemon-cli-compat.ts deleted file mode 100644 index d5466fc519fd..000000000000 --- a/src/cli/daemon-cli-compat.ts +++ /dev/null @@ -1,140 +0,0 @@ -// Compatibility parser for older bundled daemon CLI exports inside generated bundles. -export const LEGACY_DAEMON_CLI_EXPORTS = [ - "registerDaemonCli", - "runDaemonInstall", - "runDaemonRestart", - "runDaemonStart", - "runDaemonStatus", - "runDaemonStop", - "runDaemonUninstall", -] as const; - -type LegacyDaemonCliExport = (typeof LEGACY_DAEMON_CLI_EXPORTS)[number]; -type LegacyDaemonCliRunnerExport = Exclude; - -/** Accessor names for legacy daemon bundle exports after minifier/export alias resolution. */ -export type LegacyDaemonCliAccessors = { - registerDaemonCli: string; - runDaemonRestart: string; -} & Partial< - Record, string> ->; - -const EXPORT_SPEC_RE = /^([A-Za-z_$][\w$]*)(?:\s+as\s+([A-Za-z_$][\w$]*))?$/; -const REGISTER_CONTAINER_RE = - /(?:var|const|let)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:\/\*[\s\S]*?\*\/\s*)?__exportAll\(\{\s*registerDaemonCli\s*:\s*\(\)\s*=>\s*registerDaemonCli\s*\}\)/; - -function parseExportAliases(bundleSource: string): Map | null { - const matches = [...bundleSource.matchAll(/export\s*\{([^}]+)\}\s*;?/g)]; - if (matches.length === 0) { - return null; - } - const last = matches.at(-1); - const body = last?.[1]; - if (!body) { - return null; - } - - const aliases = new Map(); - for (const chunk of body.split(",")) { - const spec = chunk.trim(); - if (!spec) { - continue; - } - const parsed = spec.match(EXPORT_SPEC_RE); - if (!parsed) { - return null; - } - const original = parsed[1]; - const alias = parsed[2] ?? original; - aliases.set(original, alias); - } - return aliases; -} - -function findRegisterContainerSymbol(bundleSource: string): string | null { - return bundleSource.match(REGISTER_CONTAINER_RE)?.[1] ?? null; -} - -/** Find the accessor for the old `registerDaemonCli` export shape, including esbuild containers. */ -export function resolveLegacyDaemonCliRegisterAccessor(bundleSource: string): string | null { - const aliases = parseExportAliases(bundleSource); - if (!aliases) { - return null; - } - - const registerContainer = findRegisterContainerSymbol(bundleSource); - const registerContainerAlias = registerContainer ? aliases.get(registerContainer) : undefined; - const registerDirectAlias = aliases.get("registerDaemonCli"); - return registerContainerAlias - ? `${registerContainerAlias}.registerDaemonCli` - : (registerDirectAlias ?? null); -} - -/** Find legacy daemon runner exports in generated bundle source. */ -export function resolveLegacyDaemonCliRunnerAccessors( - bundleSource: string, -): Partial> | null { - const aliases = parseExportAliases(bundleSource); - if (!aliases) { - return null; - } - - const runDaemonInstall = aliases.get("runDaemonInstall"); - const runDaemonRestart = aliases.get("runDaemonRestart"); - const runDaemonStart = aliases.get("runDaemonStart"); - const runDaemonStatus = aliases.get("runDaemonStatus"); - const runDaemonStop = aliases.get("runDaemonStop"); - const runDaemonUninstall = aliases.get("runDaemonUninstall"); - if ( - !runDaemonInstall && - !runDaemonRestart && - !runDaemonStart && - !runDaemonStatus && - !runDaemonStop && - !runDaemonUninstall - ) { - return null; - } - - return { - ...(runDaemonInstall ? { runDaemonInstall } : {}), - ...(runDaemonRestart ? { runDaemonRestart } : {}), - ...(runDaemonStart ? { runDaemonStart } : {}), - ...(runDaemonStatus ? { runDaemonStatus } : {}), - ...(runDaemonStop ? { runDaemonStop } : {}), - ...(runDaemonUninstall ? { runDaemonUninstall } : {}), - }; -} - -/** Resolve all legacy daemon accessors required to bridge old bundles into current CLI code. */ -export function resolveLegacyDaemonCliAccessors( - bundleSource: string, -): LegacyDaemonCliAccessors | null { - const registerDaemonCli = resolveLegacyDaemonCliRegisterAccessor(bundleSource); - const runnerAccessors = resolveLegacyDaemonCliRunnerAccessors(bundleSource); - if (!registerDaemonCli || !runnerAccessors?.runDaemonRestart) { - return null; - } - - const accessors: LegacyDaemonCliAccessors = { - registerDaemonCli, - runDaemonRestart: runnerAccessors.runDaemonRestart, - }; - if (runnerAccessors.runDaemonInstall) { - accessors.runDaemonInstall = runnerAccessors.runDaemonInstall; - } - if (runnerAccessors.runDaemonStart) { - accessors.runDaemonStart = runnerAccessors.runDaemonStart; - } - if (runnerAccessors.runDaemonStatus) { - accessors.runDaemonStatus = runnerAccessors.runDaemonStatus; - } - if (runnerAccessors.runDaemonStop) { - accessors.runDaemonStop = runnerAccessors.runDaemonStop; - } - if (runnerAccessors.runDaemonUninstall) { - accessors.runDaemonUninstall = runnerAccessors.runDaemonUninstall; - } - return accessors; -} diff --git a/src/commands/configure.gateway.ts b/src/commands/configure.gateway.ts index 3146ba2815fa..8b7c88536e51 100644 --- a/src/commands/configure.gateway.ts +++ b/src/commands/configure.gateway.ts @@ -1,5 +1,5 @@ // Configure wizard Gateway port, bind, auth, and Tailscale prompts. -import { validateIPv4AddressInput } from "@openclaw/net-policy/ipv4"; +import { validateDottedDecimalIPv4Input } from "@openclaw/net-policy/ipv4"; import { normalizeOptionalString, readStringValue, @@ -98,7 +98,7 @@ export async function promptGatewayConfig( await text({ message: "Custom IP address", placeholder: "192.168.1.100", - validate: validateIPv4AddressInput, + validate: validateDottedDecimalIPv4Input, }), runtime, ); diff --git a/src/infra/exec-approval-reply.test.ts b/src/infra/exec-approval-reply.test.ts index 3cfa299c201b..00296a41186e 100644 --- a/src/infra/exec-approval-reply.test.ts +++ b/src/infra/exec-approval-reply.test.ts @@ -40,7 +40,6 @@ vi.mock("./exec-approval-surface.js", () => ({ import { buildExecApprovalActionDescriptors, buildExecApprovalCommandText, - buildExecApprovalInteractiveReply, buildExecApprovalPendingReplyPayload, buildExecApprovalUnavailableReplyPayload, getExecApprovalApproverDmNoticeText, @@ -471,38 +470,6 @@ describe("exec approval reply helpers", () => { command: "/approve req-1 deny", }, ]); - - expect( - buildExecApprovalInteractiveReply({ - approvalCommandId: "req-1", - }), - ).toEqual({ - blocks: [ - { - type: "buttons", - buttons: [ - { - label: "Allow Once", - action: { type: "command", command: "/approve req-1 allow-once" }, - value: "/approve req-1 allow-once", - style: "success", - }, - { - label: "Allow Always", - action: { type: "command", command: "/approve req-1 allow-always" }, - value: "/approve req-1 allow-always", - style: "primary", - }, - { - label: "Deny", - action: { type: "command", command: "/approve req-1 deny" }, - value: "/approve req-1 deny", - style: "danger", - }, - ], - }, - ], - }); }); it("builds and parses shared exec approval command text", () => { diff --git a/src/infra/exec-approval-reply.ts b/src/infra/exec-approval-reply.ts index b3355e179229..9718ff0f94c4 100644 --- a/src/infra/exec-approval-reply.ts +++ b/src/infra/exec-approval-reply.ts @@ -242,38 +242,6 @@ export function buildApprovalInteractiveReplyFromActionDescriptors( return buttons.length > 0 ? { blocks: [{ type: "buttons", buttons }] } : undefined; } -/** - * @deprecated Use buildApprovalPresentation. - */ -export function buildApprovalInteractiveReply(params: { - approvalId: string; - ask?: string | null; - allowedDecisions?: readonly ExecApprovalReplyDecision[]; -}): InteractiveReply | undefined { - return buildApprovalInteractiveReplyFromActionDescriptors( - buildExecApprovalActionDescriptors({ - approvalCommandId: params.approvalId, - ask: params.ask, - allowedDecisions: params.allowedDecisions, - }), - ); -} - -/** - * @deprecated Use buildExecApprovalPresentation. - */ -export function buildExecApprovalInteractiveReply(params: { - approvalCommandId: string; - ask?: string | null; - allowedDecisions?: readonly ExecApprovalReplyDecision[]; -}): InteractiveReply | undefined { - return buildApprovalInteractiveReply({ - approvalId: params.approvalCommandId, - ask: params.ask, - allowedDecisions: params.allowedDecisions, - }); -} - export function getExecApprovalApproverDmNoticeText(): string { return "Approval required. I sent approval DMs to the approvers for this account."; } diff --git a/src/plugins/contracts/plugin-sdk-runtime-api-guardrails.test.ts b/src/plugins/contracts/plugin-sdk-runtime-api-guardrails.test.ts index 7d8cad371b0b..1cccb9723360 100644 --- a/src/plugins/contracts/plugin-sdk-runtime-api-guardrails.test.ts +++ b/src/plugins/contracts/plugin-sdk-runtime-api-guardrails.test.ts @@ -147,7 +147,7 @@ const RUNTIME_API_EXPORT_GUARDS: Record = { 'export { requiresExplicitMatrixDefaultAccount, resolveMatrixDefaultOrOnlyAccountId } from "./src/account-selection.js";', 'export { findMatrixAccountEntry, resolveConfiguredMatrixAccountIds, resolveMatrixChannelConfig } from "./src/account-selection.js";', 'export { getMatrixScopedEnvVarNames, listMatrixEnvAccountIds, resolveMatrixEnvAccountToken } from "./src/env-vars.js";', - 'export { hashMatrixAccessToken, resolveMatrixAccountStorageRoot, resolveMatrixCredentialsDir, resolveMatrixCredentialsFilename, resolveMatrixCredentialsPath, resolveMatrixHomeserverKey, resolveMatrixLegacyFlatStoragePaths, resolveMatrixLegacyFlatStoreRoot, sanitizeMatrixPathSegment } from "./src/storage-paths.js";', + 'export { hashMatrixAccessToken, resolveMatrixAccountStorageRoot, resolveMatrixCredentialsDir, resolveMatrixCredentialsFilename, resolveMatrixCredentialsPath, resolveMatrixHomeserverKey, sanitizeMatrixPathSegment } from "./src/storage-paths.js";', 'export { ensureMatrixSdkInstalled, isMatrixSdkAvailable } from "./src/matrix/deps.js";', 'export { assertHttpUrlTargetsPrivateNetwork, closeDispatcher, createPinnedDispatcher, resolvePinnedHostnameWithPolicy, ssrfPolicyFromDangerouslyAllowPrivateNetwork, ssrfPolicyFromAllowPrivateNetwork, type LookupFn, type SsrFPolicy } from "openclaw/plugin-sdk/ssrf-runtime";', 'export { setMatrixThreadBindingIdleTimeoutBySessionKey, setMatrixThreadBindingMaxAgeBySessionKey } from "./src/matrix/thread-bindings-shared.js";', diff --git a/src/wizard/setup.gateway-config.ts b/src/wizard/setup.gateway-config.ts index fc18de29262b..81038c24c6e9 100644 --- a/src/wizard/setup.gateway-config.ts +++ b/src/wizard/setup.gateway-config.ts @@ -1,5 +1,5 @@ // Setup gateway config helpers build gateway config from onboarding answers. -import { validateIPv4AddressInput } from "@openclaw/net-policy/ipv4"; +import { validateDottedDecimalIPv4Input } from "@openclaw/net-policy/ipv4"; import { formatPortRangeHint } from "../cli/error-format.js"; import { parsePort } from "../cli/shared/parse-port.js"; import { @@ -131,7 +131,7 @@ export async function configureGatewayForSetup( message: t("wizard.gateway.bindCustomIp"), placeholder: "192.168.1.100", initialValue: customBindHost ?? "", - validate: validateIPv4AddressInput, + validate: validateDottedDecimalIPv4Input, }); customBindHost = typeof input === "string" ? input.trim() : undefined; } diff --git a/test/package-scripts.test.ts b/test/package-scripts.test.ts index e6c387181e3b..a53c2cc22289 100644 --- a/test/package-scripts.test.ts +++ b/test/package-scripts.test.ts @@ -120,7 +120,7 @@ describe("package scripts", () => { }); it.each([ - { scriptName: "build:docker", expectedCount: 5 }, + { scriptName: "build:docker", expectedCount: 4 }, { scriptName: "build:plugin-sdk:strict-smoke", expectedCount: 1 }, { scriptName: "build:strict-smoke", expectedCount: 1 }, ])("runs TypeScript steps in $scriptName through tsx", ({ scriptName, expectedCount }) => { diff --git a/test/scripts/build-all.test.ts b/test/scripts/build-all.test.ts index fac84cf2f50b..2b1590c278c7 100644 --- a/test/scripts/build-all.test.ts +++ b/test/scripts/build-all.test.ts @@ -216,11 +216,6 @@ describe("resolveBuildAllStep", () => { scriptPath: "scripts/write-cli-startup-metadata.ts", expectedEnv: { FOO: "bar" }, }, - { - label: "write-cli-compat", - scriptPath: "scripts/write-cli-compat.ts", - expectedEnv: { FOO: "bar" }, - }, ])("runs the $label TypeScript step through tsx", ({ label, scriptPath, expectedEnv }) => { const step = getBuildAllStep(label); @@ -328,7 +323,6 @@ describe("resolveBuildAllSteps", () => { "ui:build", "write-build-info", "write-cli-startup-metadata", - "write-cli-compat", ]); }); @@ -420,7 +414,6 @@ describe("resolveBuildAllSteps", () => { "build-stamp", "runtime-postbuild-stamp", "write-cli-startup-metadata", - "write-cli-compat", ]); }); @@ -432,7 +425,6 @@ describe("resolveBuildAllSteps", () => { "build-stamp", "runtime-postbuild-stamp", "write-cli-startup-metadata", - "write-cli-compat", ]); }); diff --git a/ui/src/app/settings.node.test.ts b/ui/src/app/settings.node.test.ts index d554d4f30361..dea390e1cd9d 100644 --- a/ui/src/app/settings.node.test.ts +++ b/ui/src/app/settings.node.test.ts @@ -3,7 +3,6 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { createImportedCustomThemeFixture } from "../test-helpers/custom-theme.ts"; import { createStorageMock } from "../test-helpers/storage.ts"; import { - loadGatewaySessionSelection, loadLocalUserIdentity, loadSettings, resolveApplicationStartupSettings, @@ -180,39 +179,34 @@ describe("loadSettings default gateway URL derivation", () => { pathname: "/", }); sessionStorage.setItem("openclaw.control.token.v1", "legacy-session-token"); + const gatewayUrl = "wss://gateway.example:8443/openclaw"; + const scopedKey = `openclaw.control.settings.v1:${gatewayUrl}`; localStorage.setItem( - "openclaw.control.settings.v1", + scopedKey, JSON.stringify({ - gatewayUrl: "wss://gateway.example:8443/openclaw", + gatewayUrl, token: "persisted-token", sessionKey: "agent", }), ); + localStorage.setItem( + "openclaw.control.currentGateway.v1:wss://gateway.example:8443", + gatewayUrl, + ); const settings = loadSettings(); - expect(settings.gatewayUrl).toBe("wss://gateway.example:8443/openclaw"); + expect(settings.gatewayUrl).toBe(gatewayUrl); expect(settings.token).toBe(""); expect(settings.sessionKey).toBe("agent"); - const scopedKey = "openclaw.control.settings.v1:wss://gateway.example:8443/openclaw"; - expect(JSON.parse(localStorage.getItem(scopedKey) ?? "{}")).toEqual({ - gatewayUrl: "wss://gateway.example:8443/openclaw", - theme: "claw", - themeMode: "system", - chatShowThinking: true, - chatShowToolCalls: true, - chatPersistCommentary: false, - chatAutoScroll: "near-bottom", - splitRatio: 0.6, - navCollapsed: false, - navWidth: 258, - sidebarPinnedRoutes: ["overview"], - sidebarMoreExpanded: false, - textScale: 100, - sessionsByGateway: { - "wss://gateway.example:8443/openclaw": { - sessionKey: "agent", - lastActiveSessionKey: "agent", - }, + const rewritten = JSON.parse(localStorage.getItem(scopedKey) ?? "{}") as Record< + string, + unknown + >; + expect(rewritten.token).toBeUndefined(); + expect(rewritten.sessionsByGateway).toEqual({ + "wss://gateway.example:8443/openclaw": { + sessionKey: "agent", + lastActiveSessionKey: "agent", }, }); expect(sessionStorage.length).toBe(0); @@ -780,64 +774,6 @@ describe("loadSettings default gateway URL derivation", () => { expect(localStorage.getItem("openclaw.control.settings.v1")).toBeNull(); }); - it("leaves an ambiguous same-origin legacy sibling for its owning page", () => { - setTestLocation({ protocol: "https:", host: "multi.example:8443", pathname: "/gateway-b/" }); - setControlUiBasePath("/gateway-b"); - localStorage.setItem( - "openclaw.control.settings.v1", - JSON.stringify({ - gatewayUrl: "wss://multi.example:8443/gateway-a", - sessionKey: "agent:a:main", - }), - ); - - expect(loadSettings().gatewayUrl).toBe(expectedGatewayUrl("/gateway-b")); - expect(loadGatewaySessionSelection(expectedGatewayUrl("/gateway-b"))).toEqual({ - sessionKey: "main", - lastActiveSessionKey: "main", - }); - expect(localStorage.getItem("openclaw.control.settings.v1")).not.toBeNull(); - }); - - it("migrates a legacy record that matches the current page", () => { - setTestLocation({ protocol: "https:", host: "multi.example:8443", pathname: "/gateway-b/" }); - setControlUiBasePath("/gateway-b"); - const gatewayUrl = expectedGatewayUrl("/gateway-b"); - localStorage.setItem( - "openclaw.control.settings.v1", - JSON.stringify({ - gatewayUrl, - sessionKey: "agent:b:main", - lastActiveSessionKey: "agent:b:main", - }), - ); - - expect(loadSettings()).toMatchObject({ gatewayUrl, sessionKey: "agent:b:main" }); - expect( - localStorage.getItem("openclaw.control.currentGateway.v1:wss://multi.example:8443/gateway-b"), - ).toBe(gatewayUrl); - expect(localStorage.getItem(`openclaw.control.settings.v1:${gatewayUrl}`)).not.toBeNull(); - expect(localStorage.getItem("openclaw.control.settings.v1")).toBeNull(); - }); - - it("migrates a legacy custom gateway on a different host", () => { - setTestLocation({ protocol: "https:", host: "control.example:8443", pathname: "/openclaw/" }); - setControlUiBasePath("/openclaw"); - const remoteUrl = "wss://remote-gateway.example.com"; - localStorage.setItem( - "openclaw.control.settings.v1", - JSON.stringify({ gatewayUrl: remoteUrl, sessionKey: "remote-session" }), - ); - - expect(loadSettings()).toMatchObject({ gatewayUrl: remoteUrl, sessionKey: "remote-session" }); - expect( - localStorage.getItem( - "openclaw.control.currentGateway.v1:wss://control.example:8443/openclaw", - ), - ).toBe(remoteUrl); - expect(localStorage.getItem("openclaw.control.settings.v1")).toBeNull(); - }); - it("keeps custom gateway selections isolated per Control UI base path", () => { setTestLocation({ protocol: "https:", host: "multi.example:8443", pathname: "/gateway-a/" }); setControlUiBasePath("/gateway-a"); diff --git a/ui/src/app/settings.ts b/ui/src/app/settings.ts index cbd20e5d0ee0..1e4320b47916 100644 --- a/ui/src/app/settings.ts +++ b/ui/src/app/settings.ts @@ -348,7 +348,6 @@ function getSessionStorage(): Storage | null { type PersistedSettingsSource = { gatewayUrl: string; - legacy: boolean; parsed: PersistedUiSettings; }; @@ -363,42 +362,17 @@ function parsePersistedSettings(raw: string | null): PersistedUiSettings | null } } -function settingsMatchGatewayTarget( - parsed: PersistedUiSettings, - targetUrl: string, - aliases: readonly string[] = [], -): boolean { +function settingsMatchGatewayTarget(parsed: PersistedUiSettings, targetUrl: string): boolean { const storedUrl = normalizeOptionalString(parsed.gatewayUrl); if (!storedUrl) { return false; } - const storedScope = normalizeGatewayTokenScope(storedUrl); - return [targetUrl, ...aliases].some( - (candidate) => normalizeGatewayTokenScope(candidate) === storedScope, - ); -} - -function isClaimableLegacyGateway(storedUrl: string | undefined, pageUrl: string): boolean { - if (!storedUrl) { - return false; - } - try { - const stored = new URL(normalizeGatewayTokenScope(storedUrl)); - const page = new URL(normalizeGatewayTokenScope(pageUrl)); - return stored.host !== page.host || page.pathname === "/"; - } catch { - return false; - } + return normalizeGatewayTokenScope(storedUrl) === normalizeGatewayTokenScope(targetUrl); } function readSettingsForGateway( storage: Storage | null, targetUrl: string, - options: { - includeLegacy?: boolean; - legacyAliases?: readonly string[]; - remoteLegacyPageUrl?: string; - } = {}, ): PersistedSettingsSource | null { const scoped = parsePersistedSettings(storage?.getItem(settingsKeyForGateway(targetUrl)) ?? null); if ( @@ -407,31 +381,9 @@ function readSettingsForGateway( ) { return { gatewayUrl: normalizeOptionalString(scoped.gatewayUrl) ?? targetUrl, - legacy: false, parsed: scoped, }; } - if (!options.includeLegacy) { - return null; - } - for (const key of [`${SETTINGS_KEY_PREFIX}default`, LEGACY_SETTINGS_KEY]) { - const parsed = parsePersistedSettings(storage?.getItem(key) ?? null); - if (!parsed) { - continue; - } - const storedUrl = normalizeOptionalString(parsed.gatewayUrl); - const matchesTarget = settingsMatchGatewayTarget(parsed, targetUrl, options.legacyAliases); - const isRemote = options.remoteLegacyPageUrl - ? isClaimableLegacyGateway(storedUrl, options.remoteLegacyPageUrl) - : false; - if (matchesTarget || isRemote) { - return { - gatewayUrl: storedUrl ?? targetUrl, - legacy: true, - parsed, - }; - } - } return null; } @@ -471,7 +423,7 @@ export function loadGatewaySessionSelection(gatewayUrl: string): ScopedSessionSe const fallback = { sessionKey: "main", lastActiveSessionKey: "main" }; try { const storage = getSafeLocalStorage(); - const source = readSettingsForGateway(storage, gatewayUrl, { includeLegacy: true }); + const source = readSettingsForGateway(storage, gatewayUrl); return source ? resolveScopedSessionSelection(gatewayUrl, source.parsed, fallback) : fallback; } catch { return fallback; @@ -557,13 +509,7 @@ export function loadSettings(): UiSettings { const selected = selectedGatewayUrl ? readSettingsForGateway(storage, selectedGatewayUrl) : null; - // Legacy state has no page owner. Only the current page target or a deliberate - // cross-origin remote can claim it; ambiguous same-origin sibling paths cannot. - const defaultSource = readSettingsForGateway(storage, defaultUrl, { - includeLegacy: true, - legacyAliases: [pageDerivedUrl], - remoteLegacyPageUrl: pageDerivedUrl, - }); + const defaultSource = readSettingsForGateway(storage, defaultUrl); const source = selected ?? defaultSource; if (!source) { return defaults; @@ -628,7 +574,9 @@ export function loadSettings(): UiSettings { ...(parsed.lobsterPetVisits === false ? { lobsterPetVisits: false } : {}), ...(parsed.lobsterPetSounds === true ? { lobsterPetSounds: true } : {}), }; - if (source.legacy || "token" in parsed) { + // Scoped blobs from builds that persisted tokens durably get rewritten once + // so the plaintext token leaves localStorage. + if ("token" in parsed) { persistSettings(settings, { selectGateway: true }); } return settings; @@ -667,7 +615,7 @@ function persistSettings(next: UiSettings, options: { selectGateway?: boolean } const scopedKey = settingsKeyForGateway(next.gatewayUrl); let existingSessionsByGateway: Record = {}; try { - const source = readSettingsForGateway(storage, next.gatewayUrl, { includeLegacy: true }); + const source = readSettingsForGateway(storage, next.gatewayUrl); if (source) { const parsed = source.parsed; if (parsed.sessionsByGateway && typeof parsed.sessionsByGateway === "object") { diff --git a/ui/src/app/theme.test.ts b/ui/src/app/theme.test.ts index 2ab4efbaa4da..fe70cea8d365 100644 --- a/ui/src/app/theme.test.ts +++ b/ui/src/app/theme.test.ts @@ -16,14 +16,14 @@ describe("resolveTheme", () => { }); describe("parseThemeSelection", () => { - it("maps legacy stored values onto theme + mode", () => { - expect(parseThemeSelection("system", undefined)).toEqual({ + it("falls back to defaults for unknown stored values", () => { + expect(parseThemeSelection("fieldmanual", "invalid-mode")).toEqual({ theme: "claw", mode: "system", }); - expect(parseThemeSelection("fieldmanual", undefined)).toEqual({ + expect(parseThemeSelection("dash", "light")).toEqual({ theme: "dash", - mode: "dark", + mode: "light", }); }); }); diff --git a/ui/src/app/theme.ts b/ui/src/app/theme.ts index cd3e28aabd43..9c6102c3c950 100644 --- a/ui/src/app/theme.ts +++ b/ui/src/app/theme.ts @@ -14,22 +14,6 @@ export type ResolvedTheme = const VALID_THEME_NAMES = new Set(["claw", "knot", "dash", "custom"]); const VALID_THEME_MODES = new Set(["system", "light", "dark"]); -type ThemeSelection = { theme: ThemeName; mode: ThemeMode }; - -const LEGACY_MAP: Record = { - defaultTheme: { theme: "claw", mode: "dark" }, - docsTheme: { theme: "claw", mode: "light" }, - lightTheme: { theme: "knot", mode: "dark" }, - landingTheme: { theme: "knot", mode: "dark" }, - newTheme: { theme: "knot", mode: "dark" }, - dark: { theme: "claw", mode: "dark" }, - light: { theme: "claw", mode: "light" }, - openknot: { theme: "knot", mode: "dark" }, - fieldmanual: { theme: "dash", mode: "dark" }, - clawdash: { theme: "dash", mode: "light" }, - system: { theme: "claw", mode: "system" }, -}; - function prefersLightScheme(): boolean { if (typeof globalThis.matchMedia !== "function") { return false; @@ -44,12 +28,8 @@ export function parseThemeSelection( const theme = typeof themeRaw === "string" ? themeRaw : ""; const mode = typeof modeRaw === "string" ? modeRaw : ""; - const normalizedTheme = VALID_THEME_NAMES.has(theme as ThemeName) - ? (theme as ThemeName) - : (LEGACY_MAP[theme]?.theme ?? "claw"); - const normalizedMode = VALID_THEME_MODES.has(mode as ThemeMode) - ? (mode as ThemeMode) - : (LEGACY_MAP[theme]?.mode ?? "system"); + const normalizedTheme = VALID_THEME_NAMES.has(theme as ThemeName) ? (theme as ThemeName) : "claw"; + const normalizedMode = VALID_THEME_MODES.has(mode as ThemeMode) ? (mode as ThemeMode) : "system"; return { theme: normalizedTheme, mode: normalizedMode }; } diff --git a/ui/src/lib/chat/message-extract.ts b/ui/src/lib/chat/message-extract.ts index cef618ea84d8..b29aa9bade36 100644 --- a/ui/src/lib/chat/message-extract.ts +++ b/ui/src/lib/chat/message-extract.ts @@ -3,7 +3,7 @@ import { stripInternalRuntimeContext } from "../../../../src/agents/internal-run import { stripInboundMetadata } from "../../../../src/auto-reply/reply/strip-inbound-meta.js"; import { stripEnvelope } from "../../../../src/shared/chat-envelope.js"; import { extractAssistantVisibleText as extractSharedAssistantVisibleText } from "../../../../src/shared/chat-message-content.js"; -import { normalizeLowercaseStringOrEmpty, normalizeStringEntries } from "../string-coerce.ts"; +import { normalizeLowercaseStringOrEmpty } from "../string-coerce.ts"; import { stripThinkingTags } from "../strip-thinking-tags.ts"; const textCache = new WeakMap(); @@ -67,20 +67,7 @@ export function extractThinking(message: unknown): string | null { } } } - if (parts.length > 0) { - return parts.join("\n"); - } - - // Back-compat: older logs may still have tags inside text blocks. - const rawText = extractRawText(message); - if (!rawText) { - return null; - } - const matches = [ - ...rawText.matchAll(/<\s*think(?:ing)?\s*>([\s\S]*?)<\s*\/\s*think(?:ing)?\s*>/gi), - ]; - const extracted = normalizeStringEntries(matches.map((mLocal) => mLocal[1] ?? "")); - return extracted.length > 0 ? extracted.join("\n") : null; + return parts.length > 0 ? parts.join("\n") : null; } export function extractThinkingCached(message: unknown): string | null { diff --git a/ui/src/lib/session-display.ts b/ui/src/lib/session-display.ts index 159f2ef3d18a..c64556abd311 100644 --- a/ui/src/lib/session-display.ts +++ b/ui/src/lib/session-display.ts @@ -13,6 +13,8 @@ const CHANNEL_LABELS: Record = { sms: "SMS", }; +const KNOWN_CHANNEL_KEYS = Object.keys(CHANNEL_LABELS); + /** Human channel label for group headers and name fallbacks. */ export function channelDisplayLabel(channel: string): string { const normalized = normalizeLowercaseStringOrEmpty(channel); @@ -82,8 +84,6 @@ export function resolveSessionWorkSubtitle(row: SessionWorktreeDisplayRow): stri return checkout ?? node; } -const KNOWN_CHANNEL_KEYS = Object.keys(CHANNEL_LABELS); - /** Parsed type / context extracted from a session key. */ type SessionKeyInfo = { /** Prefix for typed sessions (Subagent:/Cron:). Empty for others. */ @@ -141,7 +141,8 @@ function parseSessionKey(key: string): SessionKeyInfo { return { prefix: "", fallbackName: `${channelLabel} Group` }; } - // Channel-prefixed legacy keys, for example "imessage:g-...". + // Channel-prefixed keys like "telegram:123": durable session rows written by + // pre-agent-scoped builds still surface in session lists; label, don't leak keys. for (const ch of KNOWN_CHANNEL_KEYS) { if (key === ch || key.startsWith(`${ch}:`)) { return { prefix: "", fallbackName: `${CHANNEL_LABELS[ch]} Session` };