@archipelagolab/lobi 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (315) hide show
  1. package/CHANGELOG.md +164 -0
  2. package/ENDOFFILE +0 -0
  3. package/EOF +0 -0
  4. package/LICENSE +21 -0
  5. package/SPEC-SUPPORT.md +116 -0
  6. package/YAMLEND +0 -0
  7. package/api.ts +18 -0
  8. package/archipelagolab-lobi-1.0.0.tgz +0 -0
  9. package/auth-presence.ts +56 -0
  10. package/channel-plugin-api.ts +3 -0
  11. package/cli-metadata.ts +11 -0
  12. package/contract-api.ts +17 -0
  13. package/docs/CHECKLIST.md +83 -0
  14. package/docs/FORK_SDK_GUIDE.md +279 -0
  15. package/helper-api.ts +3 -0
  16. package/index.test.ts +61 -0
  17. package/index.ts +65 -0
  18. package/openclaw.plugin.json +23 -0
  19. package/package.json +52 -0
  20. package/plugin-entry.handlers.runtime.ts +1 -0
  21. package/runtime-api.ts +54 -0
  22. package/runtime-heavy-api.ts +1 -0
  23. package/scripts/migrate-to-lobi.sh +72 -0
  24. package/secret-contract-api.ts +5 -0
  25. package/setup-entry.ts +13 -0
  26. package/src/account-selection.test.ts +124 -0
  27. package/src/account-selection.ts +226 -0
  28. package/src/actions.account-propagation.test.ts +251 -0
  29. package/src/actions.test.ts +251 -0
  30. package/src/actions.ts +336 -0
  31. package/src/approval-auth.test.ts +23 -0
  32. package/src/approval-auth.ts +25 -0
  33. package/src/approval-handler.runtime.test.ts +46 -0
  34. package/src/approval-handler.runtime.ts +400 -0
  35. package/src/approval-ids.ts +6 -0
  36. package/src/approval-native.test.ts +329 -0
  37. package/src/approval-native.ts +336 -0
  38. package/src/approval-reactions.test.ts +107 -0
  39. package/src/approval-reactions.ts +158 -0
  40. package/src/auth-precedence.ts +61 -0
  41. package/src/channel-account-paths.ts +92 -0
  42. package/src/channel.account-paths.test.ts +102 -0
  43. package/src/channel.directory.test.ts +601 -0
  44. package/src/channel.resolve.test.ts +38 -0
  45. package/src/channel.runtime.ts +16 -0
  46. package/src/channel.setup.test.ts +269 -0
  47. package/src/channel.ts +570 -0
  48. package/src/cli-metadata.ts +19 -0
  49. package/src/cli.test.ts +1015 -0
  50. package/src/cli.ts +1198 -0
  51. package/src/config-adapter.ts +41 -0
  52. package/src/config-schema.test.ts +90 -0
  53. package/src/config-schema.ts +114 -0
  54. package/src/directory-live.test.ts +200 -0
  55. package/src/directory-live.ts +238 -0
  56. package/src/doctor-contract.ts +287 -0
  57. package/src/doctor.test.ts +440 -0
  58. package/src/doctor.ts +262 -0
  59. package/src/env-vars.ts +92 -0
  60. package/src/exec-approval-resolver.test.ts +68 -0
  61. package/src/exec-approval-resolver.ts +23 -0
  62. package/src/exec-approvals.test.ts +483 -0
  63. package/src/exec-approvals.ts +290 -0
  64. package/src/group-mentions.ts +41 -0
  65. package/src/legacy-crypto-inspector-availability.test.ts +81 -0
  66. package/src/legacy-crypto-inspector-availability.ts +60 -0
  67. package/src/legacy-crypto.test.ts +234 -0
  68. package/src/legacy-crypto.ts +549 -0
  69. package/src/legacy-state.test.ts +86 -0
  70. package/src/legacy-state.ts +156 -0
  71. package/src/matrix/account-config.ts +150 -0
  72. package/src/matrix/accounts.readiness.test.ts +27 -0
  73. package/src/matrix/accounts.test.ts +757 -0
  74. package/src/matrix/accounts.ts +194 -0
  75. package/src/matrix/actions/client.test.ts +215 -0
  76. package/src/matrix/actions/client.ts +31 -0
  77. package/src/matrix/actions/devices.test.ts +114 -0
  78. package/src/matrix/actions/devices.ts +34 -0
  79. package/src/matrix/actions/limits.test.ts +15 -0
  80. package/src/matrix/actions/limits.ts +6 -0
  81. package/src/matrix/actions/messages.test.ts +289 -0
  82. package/src/matrix/actions/messages.ts +123 -0
  83. package/src/matrix/actions/pins.test.ts +74 -0
  84. package/src/matrix/actions/pins.ts +64 -0
  85. package/src/matrix/actions/polls.test.ts +71 -0
  86. package/src/matrix/actions/polls.ts +109 -0
  87. package/src/matrix/actions/profile.test.ts +109 -0
  88. package/src/matrix/actions/profile.ts +37 -0
  89. package/src/matrix/actions/reactions.test.ts +135 -0
  90. package/src/matrix/actions/reactions.ts +59 -0
  91. package/src/matrix/actions/room.test.ts +79 -0
  92. package/src/matrix/actions/room.ts +71 -0
  93. package/src/matrix/actions/summary.test.ts +87 -0
  94. package/src/matrix/actions/summary.ts +88 -0
  95. package/src/matrix/actions/types.ts +82 -0
  96. package/src/matrix/actions/verification.test.ts +105 -0
  97. package/src/matrix/actions/verification.ts +237 -0
  98. package/src/matrix/actions.ts +37 -0
  99. package/src/matrix/active-client.ts +26 -0
  100. package/src/matrix/async-lock.ts +18 -0
  101. package/src/matrix/backup-health.ts +115 -0
  102. package/src/matrix/client/config-runtime-api.ts +14 -0
  103. package/src/matrix/client/config-secret-input.runtime.ts +1 -0
  104. package/src/matrix/client/config.ts +982 -0
  105. package/src/matrix/client/create-client.test.ts +115 -0
  106. package/src/matrix/client/create-client.ts +101 -0
  107. package/src/matrix/client/env-auth.ts +6 -0
  108. package/src/matrix/client/file-sync-store.test.ts +265 -0
  109. package/src/matrix/client/file-sync-store.ts +289 -0
  110. package/src/matrix/client/logging.ts +123 -0
  111. package/src/matrix/client/migration-snapshot.runtime.ts +1 -0
  112. package/src/matrix/client/private-network-host.ts +56 -0
  113. package/src/matrix/client/runtime.ts +4 -0
  114. package/src/matrix/client/shared.test.ts +344 -0
  115. package/src/matrix/client/shared.ts +306 -0
  116. package/src/matrix/client/storage.test.ts +634 -0
  117. package/src/matrix/client/storage.ts +544 -0
  118. package/src/matrix/client/types.ts +50 -0
  119. package/src/matrix/client-bootstrap.test.ts +84 -0
  120. package/src/matrix/client-bootstrap.ts +164 -0
  121. package/src/matrix/client-resolver.test-helpers.ts +147 -0
  122. package/src/matrix/client.test.ts +1521 -0
  123. package/src/matrix/client.ts +23 -0
  124. package/src/matrix/config-paths.ts +31 -0
  125. package/src/matrix/config-update.test.ts +237 -0
  126. package/src/matrix/config-update.ts +291 -0
  127. package/src/matrix/credentials-read.ts +206 -0
  128. package/src/matrix/credentials-write.runtime.ts +26 -0
  129. package/src/matrix/credentials.test.ts +501 -0
  130. package/src/matrix/credentials.ts +95 -0
  131. package/src/matrix/deps.test.ts +74 -0
  132. package/src/matrix/deps.ts +225 -0
  133. package/src/matrix/device-health.test.ts +45 -0
  134. package/src/matrix/device-health.ts +31 -0
  135. package/src/matrix/direct-management.test.ts +350 -0
  136. package/src/matrix/direct-management.ts +347 -0
  137. package/src/matrix/direct-room.test.ts +61 -0
  138. package/src/matrix/direct-room.ts +128 -0
  139. package/src/matrix/draft-stream.test.ts +406 -0
  140. package/src/matrix/draft-stream.ts +216 -0
  141. package/src/matrix/encryption-guidance.ts +27 -0
  142. package/src/matrix/errors.ts +21 -0
  143. package/src/matrix/format.test.ts +340 -0
  144. package/src/matrix/format.ts +428 -0
  145. package/src/matrix/legacy-crypto-inspector.ts +95 -0
  146. package/src/matrix/media-errors.ts +20 -0
  147. package/src/matrix/media-text.ts +169 -0
  148. package/src/matrix/monitor/access-state.test.ts +45 -0
  149. package/src/matrix/monitor/access-state.ts +77 -0
  150. package/src/matrix/monitor/ack-config.test.ts +57 -0
  151. package/src/matrix/monitor/ack-config.ts +26 -0
  152. package/src/matrix/monitor/allowlist.test.ts +45 -0
  153. package/src/matrix/monitor/allowlist.ts +94 -0
  154. package/src/matrix/monitor/auto-join.test.ts +203 -0
  155. package/src/matrix/monitor/auto-join.ts +86 -0
  156. package/src/matrix/monitor/config.test.ts +197 -0
  157. package/src/matrix/monitor/config.ts +303 -0
  158. package/src/matrix/monitor/context-summary.ts +43 -0
  159. package/src/matrix/monitor/direct.test.ts +529 -0
  160. package/src/matrix/monitor/direct.ts +270 -0
  161. package/src/matrix/monitor/events.test.ts +1524 -0
  162. package/src/matrix/monitor/events.ts +213 -0
  163. package/src/matrix/monitor/handler.body-for-agent.test.ts +396 -0
  164. package/src/matrix/monitor/handler.group-history.test.ts +648 -0
  165. package/src/matrix/monitor/handler.media-failure.test.ts +267 -0
  166. package/src/matrix/monitor/handler.test-helpers.ts +308 -0
  167. package/src/matrix/monitor/handler.test.ts +2952 -0
  168. package/src/matrix/monitor/handler.thread-root-media.test.ts +82 -0
  169. package/src/matrix/monitor/handler.ts +1679 -0
  170. package/src/matrix/monitor/inbound-dedupe.test.ts +146 -0
  171. package/src/matrix/monitor/inbound-dedupe.ts +267 -0
  172. package/src/matrix/monitor/index.test.ts +920 -0
  173. package/src/matrix/monitor/index.ts +434 -0
  174. package/src/matrix/monitor/legacy-crypto-restore.test.ts +206 -0
  175. package/src/matrix/monitor/legacy-crypto-restore.ts +139 -0
  176. package/src/matrix/monitor/location.ts +100 -0
  177. package/src/matrix/monitor/media.test.ts +159 -0
  178. package/src/matrix/monitor/media.ts +119 -0
  179. package/src/matrix/monitor/mentions.test.ts +289 -0
  180. package/src/matrix/monitor/mentions.ts +177 -0
  181. package/src/matrix/monitor/reaction-events.test.ts +326 -0
  182. package/src/matrix/monitor/reaction-events.ts +187 -0
  183. package/src/matrix/monitor/recent-invite.test.ts +92 -0
  184. package/src/matrix/monitor/recent-invite.ts +30 -0
  185. package/src/matrix/monitor/replies.test.ts +265 -0
  186. package/src/matrix/monitor/replies.ts +136 -0
  187. package/src/matrix/monitor/reply-context.test.ts +276 -0
  188. package/src/matrix/monitor/reply-context.ts +92 -0
  189. package/src/matrix/monitor/room-history.test.ts +258 -0
  190. package/src/matrix/monitor/room-history.ts +301 -0
  191. package/src/matrix/monitor/room-info.test.ts +201 -0
  192. package/src/matrix/monitor/room-info.ts +126 -0
  193. package/src/matrix/monitor/rooms.test.ts +121 -0
  194. package/src/matrix/monitor/rooms.ts +52 -0
  195. package/src/matrix/monitor/route.test.ts +255 -0
  196. package/src/matrix/monitor/route.ts +178 -0
  197. package/src/matrix/monitor/runtime-api.ts +31 -0
  198. package/src/matrix/monitor/startup-verification.test.ts +294 -0
  199. package/src/matrix/monitor/startup-verification.ts +237 -0
  200. package/src/matrix/monitor/startup.test.ts +257 -0
  201. package/src/matrix/monitor/startup.ts +218 -0
  202. package/src/matrix/monitor/status.ts +111 -0
  203. package/src/matrix/monitor/sync-lifecycle.test.ts +224 -0
  204. package/src/matrix/monitor/sync-lifecycle.ts +91 -0
  205. package/src/matrix/monitor/task-runner.ts +38 -0
  206. package/src/matrix/monitor/thread-context.test.ts +149 -0
  207. package/src/matrix/monitor/thread-context.ts +108 -0
  208. package/src/matrix/monitor/threads.test.ts +68 -0
  209. package/src/matrix/monitor/threads.ts +85 -0
  210. package/src/matrix/monitor/types.ts +30 -0
  211. package/src/matrix/monitor/verification-events.ts +627 -0
  212. package/src/matrix/monitor/verification-utils.test.ts +47 -0
  213. package/src/matrix/monitor/verification-utils.ts +46 -0
  214. package/src/matrix/outbound-media-runtime.ts +1 -0
  215. package/src/matrix/poll-summary.ts +110 -0
  216. package/src/matrix/poll-types.test.ts +205 -0
  217. package/src/matrix/poll-types.ts +433 -0
  218. package/src/matrix/probe.runtime.ts +4 -0
  219. package/src/matrix/probe.test.ts +154 -0
  220. package/src/matrix/probe.ts +96 -0
  221. package/src/matrix/profile.test.ts +154 -0
  222. package/src/matrix/profile.ts +184 -0
  223. package/src/matrix/reaction-common.test.ts +96 -0
  224. package/src/matrix/reaction-common.ts +147 -0
  225. package/src/matrix/sdk/crypto-bootstrap.test.ts +505 -0
  226. package/src/matrix/sdk/crypto-bootstrap.ts +341 -0
  227. package/src/matrix/sdk/crypto-facade.test.ts +197 -0
  228. package/src/matrix/sdk/crypto-facade.ts +207 -0
  229. package/src/matrix/sdk/crypto-node.runtime.test.ts +27 -0
  230. package/src/matrix/sdk/crypto-node.runtime.ts +9 -0
  231. package/src/matrix/sdk/crypto-runtime.ts +11 -0
  232. package/src/matrix/sdk/decrypt-bridge.ts +356 -0
  233. package/src/matrix/sdk/event-helpers.test.ts +60 -0
  234. package/src/matrix/sdk/event-helpers.ts +71 -0
  235. package/src/matrix/sdk/http-client.test.ts +134 -0
  236. package/src/matrix/sdk/http-client.ts +87 -0
  237. package/src/matrix/sdk/idb-persistence-lock.ts +51 -0
  238. package/src/matrix/sdk/idb-persistence.lock-order.test.ts +108 -0
  239. package/src/matrix/sdk/idb-persistence.test-helpers.ts +88 -0
  240. package/src/matrix/sdk/idb-persistence.test.ts +149 -0
  241. package/src/matrix/sdk/idb-persistence.ts +283 -0
  242. package/src/matrix/sdk/logger.test.ts +25 -0
  243. package/src/matrix/sdk/logger.ts +108 -0
  244. package/src/matrix/sdk/read-response-with-limit.ts +19 -0
  245. package/src/matrix/sdk/recovery-key-store.test.ts +385 -0
  246. package/src/matrix/sdk/recovery-key-store.ts +430 -0
  247. package/src/matrix/sdk/transport.test.ts +161 -0
  248. package/src/matrix/sdk/transport.ts +344 -0
  249. package/src/matrix/sdk/types.ts +236 -0
  250. package/src/matrix/sdk/verification-manager.test.ts +509 -0
  251. package/src/matrix/sdk/verification-manager.ts +694 -0
  252. package/src/matrix/sdk/verification-status.ts +23 -0
  253. package/src/matrix/sdk.test.ts +2568 -0
  254. package/src/matrix/sdk.ts +1789 -0
  255. package/src/matrix/send/client.test.ts +174 -0
  256. package/src/matrix/send/client.ts +90 -0
  257. package/src/matrix/send/formatting.ts +189 -0
  258. package/src/matrix/send/media.ts +244 -0
  259. package/src/matrix/send/targets.test.ts +254 -0
  260. package/src/matrix/send/targets.ts +104 -0
  261. package/src/matrix/send/types.ts +134 -0
  262. package/src/matrix/send.test.ts +958 -0
  263. package/src/matrix/send.ts +609 -0
  264. package/src/matrix/session-store-metadata.ts +108 -0
  265. package/src/matrix/startup-abort.ts +44 -0
  266. package/src/matrix/sync-state.ts +27 -0
  267. package/src/matrix/target-ids.ts +102 -0
  268. package/src/matrix/thread-bindings-shared.ts +201 -0
  269. package/src/matrix/thread-bindings.test.ts +673 -0
  270. package/src/matrix/thread-bindings.ts +577 -0
  271. package/src/matrix-migration.runtime.ts +9 -0
  272. package/src/migration-config.test.ts +228 -0
  273. package/src/migration-config.ts +243 -0
  274. package/src/migration-snapshot-backup.ts +117 -0
  275. package/src/migration-snapshot.test.ts +184 -0
  276. package/src/migration-snapshot.ts +55 -0
  277. package/src/onboarding.resolve.test.ts +55 -0
  278. package/src/onboarding.test-harness.ts +158 -0
  279. package/src/onboarding.test.ts +665 -0
  280. package/src/onboarding.ts +773 -0
  281. package/src/outbound.test.ts +173 -0
  282. package/src/outbound.ts +78 -0
  283. package/src/plugin-entry.runtime.js +159 -0
  284. package/src/plugin-entry.runtime.test.ts +108 -0
  285. package/src/plugin-entry.runtime.ts +68 -0
  286. package/src/profile-update.ts +68 -0
  287. package/src/record-shared.ts +3 -0
  288. package/src/resolve-targets.test.ts +178 -0
  289. package/src/resolve-targets.ts +175 -0
  290. package/src/resolver.ts +21 -0
  291. package/src/runtime-api.ts +144 -0
  292. package/src/runtime.ts +7 -0
  293. package/src/secret-contract.ts +174 -0
  294. package/src/session-route.test.ts +315 -0
  295. package/src/session-route.ts +113 -0
  296. package/src/setup-bootstrap.ts +94 -0
  297. package/src/setup-config.ts +222 -0
  298. package/src/setup-contract.ts +89 -0
  299. package/src/setup-core.test.ts +326 -0
  300. package/src/setup-core.ts +50 -0
  301. package/src/setup-surface.ts +4 -0
  302. package/src/startup-maintenance.test.ts +227 -0
  303. package/src/startup-maintenance.ts +114 -0
  304. package/src/storage-paths.ts +92 -0
  305. package/src/test-helpers.ts +42 -0
  306. package/src/test-mocks.ts +55 -0
  307. package/src/test-runtime.ts +72 -0
  308. package/src/test-support/monitor-route-test-support.ts +8 -0
  309. package/src/tool-actions.runtime.ts +1 -0
  310. package/src/tool-actions.test.ts +422 -0
  311. package/src/tool-actions.ts +498 -0
  312. package/src/types.ts +230 -0
  313. package/test-api.ts +2 -0
  314. package/thread-bindings-runtime.ts +4 -0
  315. package/tsconfig.json +16 -0
@@ -0,0 +1,440 @@
1
+ import fs from "node:fs/promises";
2
+ import { tmpdir } from "node:os";
3
+ import path from "node:path";
4
+ import { beforeEach, describe, expect, it, vi } from "vitest";
5
+ import {
6
+ applyMatrixDoctorRepair,
7
+ cleanStaleMatrixPluginConfig,
8
+ collectMatrixInstallPathWarnings,
9
+ formatMatrixLegacyCryptoPreview,
10
+ formatMatrixLegacyStatePreview,
11
+ matrixDoctor,
12
+ runMatrixDoctorSequence,
13
+ } from "./doctor.js";
14
+
15
+ vi.mock("./matrix-migration.runtime.js", async () => {
16
+ const actual = await vi.importActual<typeof import("./matrix-migration.runtime.js")>(
17
+ "./matrix-migration.runtime.js",
18
+ );
19
+ return {
20
+ ...actual,
21
+ maybeCreateMatrixMigrationSnapshot: vi.fn(),
22
+ autoMigrateLegacyMatrixState: vi.fn(async () => ({ changes: [], warnings: [] })),
23
+ autoPrepareLegacyMatrixCrypto: vi.fn(async () => ({ changes: [], warnings: [] })),
24
+ resolveMatrixMigrationStatus: vi.fn(() => ({
25
+ legacyState: null,
26
+ legacyCrypto: { inspectorAvailable: true, warnings: [], plans: [] },
27
+ pending: false,
28
+ actionable: false,
29
+ })),
30
+ };
31
+ });
32
+
33
+ describe("matrix doctor", () => {
34
+ beforeEach(() => {
35
+ vi.clearAllMocks();
36
+ });
37
+
38
+ it("formats state and crypto previews", () => {
39
+ expect(
40
+ formatMatrixLegacyStatePreview({
41
+ accountId: "default",
42
+ legacyStoragePath: "/tmp/legacy-sync.json",
43
+ targetStoragePath: "/tmp/new-sync.json",
44
+ legacyCryptoPath: "/tmp/legacy-crypto.json",
45
+ targetCryptoPath: "/tmp/new-crypto.json",
46
+ selectionNote: "Picked the newest account.",
47
+ targetRootDir: "/tmp/account-root",
48
+ }),
49
+ ).toContain("Matrix plugin upgraded in place.");
50
+
51
+ const previews = formatMatrixLegacyCryptoPreview({
52
+ inspectorAvailable: true,
53
+ warnings: ["matrix warning"],
54
+ plans: [
55
+ {
56
+ accountId: "default",
57
+ rootDir: "/tmp/account-root",
58
+ homeserver: "https://matrix.example.org",
59
+ userId: "@bot:example.org",
60
+ accessToken: "tok-123",
61
+ deviceId: "DEVICE123",
62
+ legacyCryptoPath: "/tmp/legacy-crypto.json",
63
+ recoveryKeyPath: "/tmp/recovery-key.txt",
64
+ statePath: "/tmp/state.json",
65
+ },
66
+ ],
67
+ });
68
+ expect(previews[0]).toBe("- matrix warning");
69
+ expect(previews[1]).toContain("/tmp/recovery-key.txt");
70
+ });
71
+
72
+ it("warns on stale custom Matrix plugin paths and cleans them", async () => {
73
+ const missingPath = path.join(tmpdir(), `openclaw-matrix-missing-${Date.now()}`);
74
+ await fs.rm(missingPath, { recursive: true, force: true });
75
+
76
+ const warnings = await collectMatrixInstallPathWarnings({
77
+ plugins: {
78
+ installs: {
79
+ matrix: { source: "path", sourcePath: missingPath, installPath: missingPath },
80
+ },
81
+ },
82
+ });
83
+ expect(warnings[0]).toContain("custom path that no longer exists");
84
+
85
+ const cleaned = await cleanStaleMatrixPluginConfig({
86
+ plugins: {
87
+ installs: {
88
+ matrix: { source: "path", sourcePath: missingPath, installPath: missingPath },
89
+ },
90
+ load: { paths: [missingPath, "/other/path"] },
91
+ allow: ["matrix", "other-plugin"],
92
+ },
93
+ });
94
+ expect(cleaned.changes[0]).toContain("Removed stale Matrix plugin references");
95
+ expect(cleaned.config.plugins?.load?.paths).toEqual(["/other/path"]);
96
+ expect(cleaned.config.plugins?.allow).toEqual(["other-plugin"]);
97
+ });
98
+
99
+ it("surfaces matrix sequence warnings and repair changes", async () => {
100
+ const runtimeApi = await import("./matrix-migration.runtime.js");
101
+ vi.mocked(runtimeApi.resolveMatrixMigrationStatus).mockReturnValue({
102
+ legacyState: null,
103
+ legacyCrypto: { inspectorAvailable: true, warnings: [], plans: [] },
104
+ pending: true,
105
+ actionable: true,
106
+ });
107
+ vi.mocked(runtimeApi.maybeCreateMatrixMigrationSnapshot).mockResolvedValue({
108
+ archivePath: "/tmp/matrix-backup.tgz",
109
+ created: true,
110
+ markerPath: "/tmp/marker.json",
111
+ });
112
+ vi.mocked(runtimeApi.autoMigrateLegacyMatrixState).mockResolvedValue({
113
+ migrated: true,
114
+ changes: ["Migrated legacy sync state"],
115
+ warnings: [],
116
+ });
117
+ vi.mocked(runtimeApi.autoPrepareLegacyMatrixCrypto).mockResolvedValue({
118
+ migrated: true,
119
+ changes: ["Prepared recovery key export"],
120
+ warnings: [],
121
+ });
122
+
123
+ const cfg = {
124
+ channels: {
125
+ matrix: {},
126
+ },
127
+ } as never;
128
+
129
+ const repair = await applyMatrixDoctorRepair({ cfg, env: process.env });
130
+ expect(repair.changes.join("\n")).toContain("Matrix migration snapshot");
131
+
132
+ const sequence = await runMatrixDoctorSequence({
133
+ cfg,
134
+ env: process.env,
135
+ shouldRepair: true,
136
+ });
137
+ expect(sequence.changeNotes.join("\n")).toContain("Matrix migration snapshot");
138
+ });
139
+
140
+ it("normalizes legacy Matrix room allow aliases to enabled", () => {
141
+ const normalize = matrixDoctor.normalizeCompatibilityConfig;
142
+ expect(normalize).toBeDefined();
143
+ if (!normalize) {
144
+ return;
145
+ }
146
+
147
+ const result = normalize({
148
+ cfg: {
149
+ channels: {
150
+ matrix: {
151
+ groups: {
152
+ "!ops:example.org": {
153
+ allow: true,
154
+ },
155
+ },
156
+ accounts: {
157
+ work: {
158
+ rooms: {
159
+ "!legacy:example.org": {
160
+ allow: false,
161
+ },
162
+ },
163
+ },
164
+ },
165
+ },
166
+ },
167
+ } as never,
168
+ });
169
+
170
+ const matrixConfig = result.config.channels?.matrix as
171
+ | {
172
+ groups?: Record<string, unknown>;
173
+ accounts?: Record<string, unknown>;
174
+ network?: { dangerouslyAllowPrivateNetwork?: boolean };
175
+ }
176
+ | undefined;
177
+ const workAccount = matrixConfig?.accounts?.work as
178
+ | {
179
+ rooms?: Record<string, unknown>;
180
+ network?: { dangerouslyAllowPrivateNetwork?: boolean };
181
+ }
182
+ | undefined;
183
+
184
+ expect(matrixConfig?.groups?.["!ops:example.org"]).toEqual({
185
+ enabled: true,
186
+ });
187
+ expect(workAccount?.rooms?.["!legacy:example.org"]).toEqual({
188
+ enabled: false,
189
+ });
190
+ expect(result.changes).toEqual(
191
+ expect.arrayContaining([
192
+ "Moved channels.lobi.groups.!ops:example.org.allow → channels.lobi.groups.!ops:example.org.enabled (true).",
193
+ "Moved channels.lobi.accounts.work.rooms.!legacy:example.org.allow → channels.lobi.accounts.work.rooms.!legacy:example.org.enabled (false).",
194
+ ]),
195
+ );
196
+ });
197
+
198
+ it("normalizes legacy Matrix private-network aliases", () => {
199
+ const normalize = matrixDoctor.normalizeCompatibilityConfig;
200
+ expect(normalize).toBeDefined();
201
+ if (!normalize) {
202
+ return;
203
+ }
204
+
205
+ const result = normalize({
206
+ cfg: {
207
+ channels: {
208
+ matrix: {
209
+ allowPrivateNetwork: true,
210
+ accounts: {
211
+ work: {
212
+ allowPrivateNetwork: false,
213
+ },
214
+ },
215
+ },
216
+ },
217
+ } as never,
218
+ });
219
+
220
+ const matrixConfig = result.config.channels?.matrix as
221
+ | {
222
+ accounts?: Record<string, unknown>;
223
+ network?: { dangerouslyAllowPrivateNetwork?: boolean };
224
+ }
225
+ | undefined;
226
+ const workAccount = matrixConfig?.accounts?.work as
227
+ | {
228
+ network?: { dangerouslyAllowPrivateNetwork?: boolean };
229
+ }
230
+ | undefined;
231
+
232
+ expect(matrixConfig?.network).toEqual({
233
+ dangerouslyAllowPrivateNetwork: true,
234
+ });
235
+ expect(workAccount?.network).toEqual({
236
+ dangerouslyAllowPrivateNetwork: false,
237
+ });
238
+ expect(result.changes).toEqual(
239
+ expect.arrayContaining([
240
+ "Moved channels.lobi.allowPrivateNetwork → channels.lobi.network.dangerouslyAllowPrivateNetwork (true).",
241
+ "Moved channels.lobi.accounts.work.allowPrivateNetwork → channels.lobi.accounts.work.network.dangerouslyAllowPrivateNetwork (false).",
242
+ ]),
243
+ );
244
+ });
245
+
246
+ it("migrates legacy channels.lobi.dm.policy 'trusted' with allowFrom to 'allowlist'", () => {
247
+ const normalize = matrixDoctor.normalizeCompatibilityConfig;
248
+ expect(normalize).toBeDefined();
249
+ if (!normalize) {
250
+ return;
251
+ }
252
+
253
+ const result = normalize({
254
+ cfg: {
255
+ channels: {
256
+ matrix: {
257
+ dm: {
258
+ enabled: true,
259
+ policy: "trusted",
260
+ allowFrom: ["@alice:example.org", "@bob:example.org"],
261
+ },
262
+ },
263
+ },
264
+ } as never,
265
+ });
266
+
267
+ const matrixDm = (
268
+ result.config.channels?.matrix as { dm?: { policy?: string; allowFrom?: string[] } }
269
+ )?.dm;
270
+
271
+ expect(matrixDm?.policy).toBe("allowlist");
272
+ expect(matrixDm?.allowFrom).toEqual(["@alice:example.org", "@bob:example.org"]);
273
+ expect(result.changes).toEqual(
274
+ expect.arrayContaining([
275
+ expect.stringContaining('Migrated channels.lobi.dm.policy "trusted" → "allowlist"'),
276
+ expect.stringContaining("preserved 2 channels.lobi.dm.allowFrom entries"),
277
+ ]),
278
+ );
279
+ });
280
+
281
+ it("migrates legacy 'trusted' policy with whitespace-only allowFrom entries to 'pairing'", () => {
282
+ // Whitespace-only entries are dropped by downstream allowlist normalization,
283
+ // so they must not count toward the allowFrom population check — otherwise
284
+ // the migration would emit policy="allowlist" with an effectively empty
285
+ // allowlist, silently blocking all DMs.
286
+ const normalize = matrixDoctor.normalizeCompatibilityConfig;
287
+ expect(normalize).toBeDefined();
288
+ if (!normalize) {
289
+ return;
290
+ }
291
+
292
+ const result = normalize({
293
+ cfg: {
294
+ channels: {
295
+ matrix: {
296
+ dm: {
297
+ enabled: true,
298
+ policy: "trusted",
299
+ allowFrom: [" ", "\t", ""],
300
+ },
301
+ },
302
+ },
303
+ } as never,
304
+ });
305
+
306
+ const matrixDm = (result.config.channels?.matrix as { dm?: { policy?: string } })?.dm;
307
+ expect(matrixDm?.policy).toBe("pairing");
308
+ expect(result.changes).toEqual(
309
+ expect.arrayContaining([
310
+ expect.stringContaining('Migrated channels.lobi.dm.policy "trusted" → "pairing"'),
311
+ ]),
312
+ );
313
+ });
314
+
315
+ it("migrates legacy channels.lobi.dm.policy 'trusted' without allowFrom to 'pairing'", () => {
316
+ const normalize = matrixDoctor.normalizeCompatibilityConfig;
317
+ expect(normalize).toBeDefined();
318
+ if (!normalize) {
319
+ return;
320
+ }
321
+
322
+ const result = normalize({
323
+ cfg: {
324
+ channels: {
325
+ matrix: {
326
+ dm: {
327
+ enabled: true,
328
+ policy: "trusted",
329
+ },
330
+ },
331
+ },
332
+ } as never,
333
+ });
334
+
335
+ const matrixDm = (result.config.channels?.matrix as { dm?: { policy?: string } })?.dm;
336
+ expect(matrixDm?.policy).toBe("pairing");
337
+ expect(result.changes).toEqual(
338
+ expect.arrayContaining([
339
+ expect.stringContaining('Migrated channels.lobi.dm.policy "trusted" → "pairing"'),
340
+ ]),
341
+ );
342
+ });
343
+
344
+ it("migrates legacy per-account channels.lobi.accounts.<id>.dm.policy 'trusted'", () => {
345
+ const normalize = matrixDoctor.normalizeCompatibilityConfig;
346
+ expect(normalize).toBeDefined();
347
+ if (!normalize) {
348
+ return;
349
+ }
350
+
351
+ const result = normalize({
352
+ cfg: {
353
+ channels: {
354
+ matrix: {
355
+ accounts: {
356
+ work: {
357
+ dm: {
358
+ enabled: true,
359
+ policy: "trusted",
360
+ allowFrom: ["@boss:example.org"],
361
+ },
362
+ },
363
+ personal: {
364
+ dm: {
365
+ enabled: true,
366
+ policy: "trusted",
367
+ },
368
+ },
369
+ },
370
+ },
371
+ },
372
+ } as never,
373
+ });
374
+
375
+ const accounts = (
376
+ result.config.channels?.matrix as {
377
+ accounts?: Record<string, { dm?: { policy?: string; allowFrom?: string[] } }>;
378
+ }
379
+ )?.accounts;
380
+
381
+ expect(accounts?.work?.dm?.policy).toBe("allowlist");
382
+ expect(accounts?.work?.dm?.allowFrom).toEqual(["@boss:example.org"]);
383
+ expect(accounts?.personal?.dm?.policy).toBe("pairing");
384
+ expect(result.changes).toEqual(
385
+ expect.arrayContaining([
386
+ expect.stringContaining(
387
+ 'Migrated channels.lobi.accounts.work.dm.policy "trusted" → "allowlist"',
388
+ ),
389
+ expect.stringContaining(
390
+ 'Migrated channels.lobi.accounts.personal.dm.policy "trusted" → "pairing"',
391
+ ),
392
+ ]),
393
+ );
394
+ });
395
+
396
+ it("leaves modern dm.policy values untouched", () => {
397
+ const normalize = matrixDoctor.normalizeCompatibilityConfig;
398
+ expect(normalize).toBeDefined();
399
+ if (!normalize) {
400
+ return;
401
+ }
402
+
403
+ const result = normalize({
404
+ cfg: {
405
+ channels: {
406
+ matrix: {
407
+ dm: {
408
+ enabled: true,
409
+ policy: "allowlist",
410
+ allowFrom: ["@alice:example.org"],
411
+ },
412
+ accounts: {
413
+ work: {
414
+ dm: { enabled: true, policy: "pairing" },
415
+ },
416
+ },
417
+ },
418
+ },
419
+ } as never,
420
+ });
421
+
422
+ expect(result.changes).toEqual([]);
423
+ expect(result.config).toEqual({
424
+ channels: {
425
+ matrix: {
426
+ dm: {
427
+ enabled: true,
428
+ policy: "allowlist",
429
+ allowFrom: ["@alice:example.org"],
430
+ },
431
+ accounts: {
432
+ work: {
433
+ dm: { enabled: true, policy: "pairing" },
434
+ },
435
+ },
436
+ },
437
+ },
438
+ });
439
+ });
440
+ });
package/src/doctor.ts ADDED
@@ -0,0 +1,262 @@
1
+ import { type ChannelDoctorAdapter } from "openclaw/plugin-sdk/channel-contract";
2
+ import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
3
+ import {
4
+ detectPluginInstallPathIssue,
5
+ formatPluginInstallPathIssue,
6
+ removePluginFromConfig,
7
+ } from "openclaw/plugin-sdk/runtime-doctor";
8
+ import {
9
+ legacyConfigRules as MATRIX_LEGACY_CONFIG_RULES,
10
+ normalizeCompatibilityConfig as normalizeMatrixCompatibilityConfig,
11
+ } from "./doctor-contract.js";
12
+ import {
13
+ autoMigrateLegacyMatrixState,
14
+ autoPrepareLegacyMatrixCrypto,
15
+ detectLegacyMatrixCrypto,
16
+ detectLegacyMatrixState,
17
+ maybeCreateMatrixMigrationSnapshot,
18
+ resolveMatrixMigrationStatus,
19
+ } from "./matrix-migration.runtime.js";
20
+ import { isRecord } from "./record-shared.js";
21
+
22
+ function hasConfiguredMatrixChannel(cfg: OpenClawConfig): boolean {
23
+ const channels = cfg.channels as Record<string, unknown> | undefined;
24
+ return isRecord(channels?.matrix);
25
+ }
26
+
27
+ function hasConfiguredMatrixPluginSurface(cfg: OpenClawConfig): boolean {
28
+ return Boolean(
29
+ cfg.plugins?.installs?.matrix ||
30
+ cfg.plugins?.entries?.matrix ||
31
+ cfg.plugins?.allow?.includes("matrix") ||
32
+ cfg.plugins?.deny?.includes("matrix"),
33
+ );
34
+ }
35
+
36
+ function hasConfiguredMatrixEnv(env: NodeJS.ProcessEnv): boolean {
37
+ return Object.entries(env).some(
38
+ ([key, value]) => key.startsWith("MATRIX_") && typeof value === "string" && value.trim(),
39
+ );
40
+ }
41
+
42
+ function configMayNeedMatrixDoctorSequence(cfg: OpenClawConfig, env: NodeJS.ProcessEnv): boolean {
43
+ return (
44
+ hasConfiguredMatrixChannel(cfg) ||
45
+ hasConfiguredMatrixPluginSurface(cfg) ||
46
+ hasConfiguredMatrixEnv(env)
47
+ );
48
+ }
49
+
50
+ export function formatMatrixLegacyStatePreview(
51
+ detection: Exclude<ReturnType<typeof detectLegacyMatrixState>, null | { warning: string }>,
52
+ ): string {
53
+ return [
54
+ "- Matrix plugin upgraded in place.",
55
+ `- Legacy sync store: ${detection.legacyStoragePath} -> ${detection.targetStoragePath}`,
56
+ `- Legacy crypto store: ${detection.legacyCryptoPath} -> ${detection.targetCryptoPath}`,
57
+ ...(detection.selectionNote ? [`- ${detection.selectionNote}`] : []),
58
+ '- Run "openclaw doctor --fix" to migrate this Matrix state now.',
59
+ ].join("\n");
60
+ }
61
+
62
+ export function formatMatrixLegacyCryptoPreview(
63
+ detection: ReturnType<typeof detectLegacyMatrixCrypto>,
64
+ ): string[] {
65
+ const notes: string[] = [];
66
+ for (const warning of detection.warnings) {
67
+ notes.push(`- ${warning}`);
68
+ }
69
+ for (const plan of detection.plans) {
70
+ notes.push(
71
+ [
72
+ `- Matrix encrypted-state migration is pending for account "${plan.accountId}".`,
73
+ `- Legacy crypto store: ${plan.legacyCryptoPath}`,
74
+ `- New recovery key file: ${plan.recoveryKeyPath}`,
75
+ `- Migration state file: ${plan.statePath}`,
76
+ '- Run "openclaw doctor --fix" to extract any saved backup key now. Backed-up room keys will restore automatically on next gateway start.',
77
+ ].join("\n"),
78
+ );
79
+ }
80
+ return notes;
81
+ }
82
+
83
+ export async function collectMatrixInstallPathWarnings(cfg: OpenClawConfig): Promise<string[]> {
84
+ const issue = await detectPluginInstallPathIssue({
85
+ pluginId: "matrix",
86
+ install: cfg.plugins?.installs?.matrix,
87
+ });
88
+ if (!issue) {
89
+ return [];
90
+ }
91
+ return formatPluginInstallPathIssue({
92
+ issue,
93
+ pluginLabel: "Matrix",
94
+ defaultInstallCommand: "openclaw plugins install @openclaw/matrix",
95
+ }).map((entry) => `- ${entry}`);
96
+ }
97
+
98
+ export async function cleanStaleMatrixPluginConfig(cfg: OpenClawConfig) {
99
+ const issue = await detectPluginInstallPathIssue({
100
+ pluginId: "matrix",
101
+ install: cfg.plugins?.installs?.matrix,
102
+ });
103
+ if (!issue || issue.kind !== "missing-path") {
104
+ return { config: cfg, changes: [] };
105
+ }
106
+ const { config, actions } = removePluginFromConfig(cfg, "matrix");
107
+ const removed: string[] = [];
108
+ if (actions.install) {
109
+ removed.push("install record");
110
+ }
111
+ if (actions.loadPath) {
112
+ removed.push("load path");
113
+ }
114
+ if (actions.entry) {
115
+ removed.push("plugin entry");
116
+ }
117
+ if (actions.allowlist) {
118
+ removed.push("allowlist entry");
119
+ }
120
+ if (removed.length === 0) {
121
+ return { config: cfg, changes: [] };
122
+ }
123
+ return {
124
+ config,
125
+ changes: [
126
+ `Removed stale Matrix plugin references (${removed.join(", ")}). The previous install path no longer exists: ${issue.path}`,
127
+ ],
128
+ };
129
+ }
130
+
131
+ export async function applyMatrixDoctorRepair(params: {
132
+ cfg: OpenClawConfig;
133
+ env: NodeJS.ProcessEnv;
134
+ }): Promise<{ changes: string[]; warnings: string[] }> {
135
+ const changes: string[] = [];
136
+ const warnings: string[] = [];
137
+ const migrationStatus = resolveMatrixMigrationStatus({
138
+ cfg: params.cfg,
139
+ env: params.env,
140
+ });
141
+
142
+ let matrixSnapshotReady = true;
143
+ if (migrationStatus.actionable) {
144
+ try {
145
+ const snapshot = await maybeCreateMatrixMigrationSnapshot({
146
+ trigger: "doctor-fix",
147
+ env: params.env,
148
+ });
149
+ changes.push(
150
+ `Matrix migration snapshot ${snapshot.created ? "created" : "reused"} before applying Matrix upgrades.\n- ${snapshot.archivePath}`,
151
+ );
152
+ } catch (error) {
153
+ matrixSnapshotReady = false;
154
+ warnings.push(
155
+ `- Failed creating a Matrix migration snapshot before repair: ${String(error)}`,
156
+ );
157
+ warnings.push(
158
+ '- Skipping Matrix migration changes for now. Resolve the snapshot failure, then rerun "openclaw doctor --fix".',
159
+ );
160
+ }
161
+ } else if (migrationStatus.pending) {
162
+ warnings.push(
163
+ "- Matrix migration warnings are present, but no on-disk Matrix mutation is actionable yet. No pre-migration snapshot was needed.",
164
+ );
165
+ }
166
+
167
+ if (!matrixSnapshotReady) {
168
+ return { changes, warnings };
169
+ }
170
+
171
+ const matrixStateRepair = await autoMigrateLegacyMatrixState({
172
+ cfg: params.cfg,
173
+ env: params.env,
174
+ });
175
+ if (matrixStateRepair.changes.length > 0) {
176
+ changes.push(
177
+ [
178
+ "Matrix plugin upgraded in place.",
179
+ ...matrixStateRepair.changes.map((entry) => `- ${entry}`),
180
+ "- No user action required.",
181
+ ].join("\n"),
182
+ );
183
+ }
184
+ if (matrixStateRepair.warnings.length > 0) {
185
+ warnings.push(matrixStateRepair.warnings.map((entry) => `- ${entry}`).join("\n"));
186
+ }
187
+
188
+ const matrixCryptoRepair = await autoPrepareLegacyMatrixCrypto({
189
+ cfg: params.cfg,
190
+ env: params.env,
191
+ });
192
+ if (matrixCryptoRepair.changes.length > 0) {
193
+ changes.push(
194
+ [
195
+ "Matrix encrypted-state migration prepared.",
196
+ ...matrixCryptoRepair.changes.map((entry) => `- ${entry}`),
197
+ ].join("\n"),
198
+ );
199
+ }
200
+ if (matrixCryptoRepair.warnings.length > 0) {
201
+ warnings.push(matrixCryptoRepair.warnings.map((entry) => `- ${entry}`).join("\n"));
202
+ }
203
+
204
+ return { changes, warnings };
205
+ }
206
+
207
+ export async function runMatrixDoctorSequence(params: {
208
+ cfg: OpenClawConfig;
209
+ env: NodeJS.ProcessEnv;
210
+ shouldRepair: boolean;
211
+ }): Promise<{ changeNotes: string[]; warningNotes: string[] }> {
212
+ const warningNotes: string[] = [];
213
+ const changeNotes: string[] = [];
214
+ const installWarnings = await collectMatrixInstallPathWarnings(params.cfg);
215
+ if (installWarnings.length > 0) {
216
+ warningNotes.push(installWarnings.join("\n"));
217
+ }
218
+ if (!configMayNeedMatrixDoctorSequence(params.cfg, params.env)) {
219
+ return { changeNotes, warningNotes };
220
+ }
221
+
222
+ if (params.shouldRepair) {
223
+ const repair = await applyMatrixDoctorRepair({
224
+ cfg: params.cfg,
225
+ env: params.env,
226
+ });
227
+ changeNotes.push(...repair.changes);
228
+ warningNotes.push(...repair.warnings);
229
+ } else {
230
+ const migrationStatus = resolveMatrixMigrationStatus({
231
+ cfg: params.cfg,
232
+ env: params.env,
233
+ });
234
+ if (migrationStatus.legacyState) {
235
+ if ("warning" in migrationStatus.legacyState) {
236
+ warningNotes.push(`- ${migrationStatus.legacyState.warning}`);
237
+ } else {
238
+ warningNotes.push(formatMatrixLegacyStatePreview(migrationStatus.legacyState));
239
+ }
240
+ }
241
+ if (
242
+ migrationStatus.legacyCrypto.warnings.length > 0 ||
243
+ migrationStatus.legacyCrypto.plans.length > 0
244
+ ) {
245
+ warningNotes.push(...formatMatrixLegacyCryptoPreview(migrationStatus.legacyCrypto));
246
+ }
247
+ }
248
+
249
+ return { changeNotes, warningNotes };
250
+ }
251
+
252
+ export const matrixDoctor: ChannelDoctorAdapter = {
253
+ dmAllowFromMode: "nestedOnly",
254
+ groupModel: "sender",
255
+ groupAllowFromFallbackToAllowFrom: false,
256
+ warnOnEmptyGroupSenderAllowlist: true,
257
+ legacyConfigRules: MATRIX_LEGACY_CONFIG_RULES,
258
+ normalizeCompatibilityConfig: normalizeMatrixCompatibilityConfig,
259
+ runConfigSequence: async ({ cfg, env, shouldRepair }) =>
260
+ await runMatrixDoctorSequence({ cfg, env, shouldRepair }),
261
+ cleanStaleConfig: async ({ cfg }) => await cleanStaleMatrixPluginConfig(cfg),
262
+ };