@poolzin/pool-bot 2026.3.22 → 2026.3.24

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 (159) hide show
  1. package/CHANGELOG.md +111 -0
  2. package/dist/.buildstamp +1 -1
  3. package/dist/acp/bindings-store.js +209 -0
  4. package/dist/acp/control-plane/runtime-cache.js +54 -0
  5. package/dist/acp/control-plane/runtime-options.js +215 -0
  6. package/dist/acp/control-plane/session-actor-queue.js +36 -0
  7. package/dist/acp/policy.js +52 -0
  8. package/dist/acp/runtime/errors.js +47 -0
  9. package/dist/acp/runtime/registry.js +86 -0
  10. package/dist/acp/runtime/types.js +1 -0
  11. package/dist/acp/translator.js +97 -0
  12. package/dist/agents/btw.js +280 -0
  13. package/dist/agents/failover-error.js +145 -47
  14. package/dist/agents/fast-mode.js +24 -0
  15. package/dist/agents/live-model-errors.js +23 -0
  16. package/dist/agents/model-auth-env-vars.js +44 -0
  17. package/dist/agents/model-auth-markers.js +69 -0
  18. package/dist/agents/models-config.providers.discovery.js +180 -0
  19. package/dist/agents/models-config.providers.static.js +480 -0
  20. package/dist/auto-reply/reply/typing-policy.js +15 -0
  21. package/dist/browser/browser-profile-manager.js +319 -0
  22. package/dist/browser/cdp-proxy-bypass.js +129 -0
  23. package/dist/browser/cdp-timeouts.js +41 -0
  24. package/dist/browser/chrome-extension-validator.js +406 -0
  25. package/dist/browser/chrome-mcp-snapshot.js +222 -0
  26. package/dist/browser/chrome-mcp.js +421 -0
  27. package/dist/browser/chrome-mcp.snapshot.js +133 -0
  28. package/dist/browser/errors.js +67 -0
  29. package/dist/browser/form-fields.js +22 -0
  30. package/dist/browser/output-atomic.js +44 -0
  31. package/dist/browser/profile-capabilities.js +47 -0
  32. package/dist/browser/safe-filename.js +25 -0
  33. package/dist/browser/snapshot-roles.js +60 -0
  34. package/dist/build-info.json +3 -3
  35. package/dist/channels/account-snapshot-fields.js +176 -0
  36. package/dist/channels/draft-stream-controls.js +89 -0
  37. package/dist/channels/inbound-debounce-policy.js +28 -0
  38. package/dist/channels/typing-lifecycle.js +39 -0
  39. package/dist/cli/program/command-registry.js +52 -0
  40. package/dist/commands/agent-binding.js +123 -0
  41. package/dist/commands/agents.commands.bind.js +280 -0
  42. package/dist/commands/backup-shared.js +186 -0
  43. package/dist/commands/backup-verify.js +236 -0
  44. package/dist/commands/backup.js +166 -0
  45. package/dist/commands/channel-account-context.js +15 -0
  46. package/dist/commands/channel-account.js +190 -0
  47. package/dist/commands/gateway-install-token.js +117 -0
  48. package/dist/commands/oauth-tls-preflight.js +121 -0
  49. package/dist/commands/ollama-setup.js +402 -0
  50. package/dist/commands/security-owner-only.js +86 -0
  51. package/dist/commands/self-hosted-provider-setup.js +207 -0
  52. package/dist/commands/session-store-targets.js +12 -0
  53. package/dist/commands/sessions-cleanup.js +97 -0
  54. package/dist/control-ui/assets/{index-Dvkl4Xlx.js → index-D7shnQwQ.js} +404 -388
  55. package/dist/control-ui/assets/index-D7shnQwQ.js.map +1 -0
  56. package/dist/control-ui/index.html +1 -1
  57. package/dist/cron/cron-filters.js +150 -0
  58. package/dist/cron/heartbeat-policy.js +26 -0
  59. package/dist/gateway/device-pairing-security.js +197 -0
  60. package/dist/gateway/event-deduplication.js +167 -0
  61. package/dist/gateway/hooks-mapping.js +46 -7
  62. package/dist/gateway/run-tracker.js +253 -0
  63. package/dist/gateway/server-methods/nodes.js +14 -0
  64. package/dist/gateway/websocket-preauth-security.js +188 -0
  65. package/dist/hooks/module-loader.js +28 -0
  66. package/dist/infra/agent-command-binding.js +144 -0
  67. package/dist/infra/backup.js +328 -0
  68. package/dist/infra/channel-account-context.js +173 -0
  69. package/dist/infra/errors.js +53 -13
  70. package/dist/infra/exec-approvals-security.js +217 -0
  71. package/dist/infra/security/command-analyzer.js +257 -0
  72. package/dist/infra/session-cleanup.js +143 -0
  73. package/dist/plugins/loader.js +16 -8
  74. package/dist/security/external-content.js +51 -1
  75. package/dist/sessions/session-costs.js +228 -0
  76. package/dist/shared/param-key.js +16 -0
  77. package/dist/shared/poll-params.js +58 -0
  78. package/dist/shared/polls.js +55 -0
  79. package/docs/DASHBOARD-GAP-ANALYSIS-AND-PLAN.md +430 -0
  80. package/docs/FEATURES.md +523 -0
  81. package/docs/FINAL-IMPLEMENTATION-REVIEW.md +274 -0
  82. package/docs/FINAL-IMPLEMENTATION-SUMMARY.md +356 -0
  83. package/docs/FINAL-PROFESSIONAL-EVALUATION.md +312 -0
  84. package/docs/IMPLEMENTATION-PRIORITY-EVALUATION.md +298 -0
  85. package/docs/IMPLEMENTATION-PROGRESS.md +237 -0
  86. package/docs/IMPLEMENTATION-REVIEW-PHASE1-2.md +381 -0
  87. package/docs/IMPLEMENTATION-REVIEW-PHASE4.md +389 -0
  88. package/docs/IMPLEMENTATION-REVIEW-PHASE5.md +420 -0
  89. package/docs/IMPLEMENTATION-REVIEW-PHASE6.md +422 -0
  90. package/docs/IMPLEMENTATION-REVIEW-PHASE7-FINAL.md +184 -0
  91. package/docs/MIKRODASH-ANALYSIS.md +412 -0
  92. package/docs/OPENCLAW-GAP-ANALYSIS-FINAL.md +431 -0
  93. package/docs/OPENCLAW-VS-POOLBOT-ANALYSIS.md +351 -0
  94. package/docs/PHASE-7-SUMMARY.md +144 -0
  95. package/docs/POOLBOT-OFFICE-PLAN.md +697 -0
  96. package/docs/PROJECT-FINAL-STATUS.md +237 -0
  97. package/docs/README.md +116 -0
  98. package/docs/REAL-IMPROVEMENTS-EVALUATION.md +477 -0
  99. package/docs/SECURITY-HARDENING-IMPLEMENTATION.md +161 -0
  100. package/docs/channels/googlechat.md +235 -206
  101. package/docs/channels/irc.md +332 -0
  102. package/docs/channels/nostr.md +255 -168
  103. package/docs/components/command-palette.md +166 -0
  104. package/docs/components/login-gate.md +219 -0
  105. package/docs/getting-started/installation.md +191 -0
  106. package/docs/getting-started/introduction.md +120 -0
  107. package/docs/improvements/USAGE-GUIDE.md +359 -0
  108. package/docs/plans/2026-03-15-openclaw-features-implementation.md +1632 -0
  109. package/docs/reference/deadcode-detection.md +72 -0
  110. package/extensions/acpx/node_modules/.bin/acpx +21 -0
  111. package/extensions/agency-agents/node_modules/.bin/vite +4 -4
  112. package/extensions/agency-agents/node_modules/.bin/vitest +2 -2
  113. package/extensions/googlechat/node_modules/.bin/tsc +21 -0
  114. package/extensions/googlechat/node_modules/.bin/tsserver +21 -0
  115. package/extensions/googlechat/node_modules/.bin/vitest +21 -0
  116. package/extensions/googlechat/package.json +11 -28
  117. package/extensions/googlechat/src/googlechat-channel.test.ts +60 -0
  118. package/extensions/googlechat/src/googlechat-channel.ts +120 -0
  119. package/extensions/googlechat/src/index.ts +14 -0
  120. package/extensions/irc/node_modules/.bin/tsc +21 -0
  121. package/extensions/irc/node_modules/.bin/tsserver +21 -0
  122. package/extensions/irc/node_modules/.bin/vitest +21 -0
  123. package/extensions/irc/package.json +16 -8
  124. package/extensions/irc/src/index.ts +14 -0
  125. package/extensions/irc/src/irc-channel.test.ts +43 -0
  126. package/extensions/irc/src/irc-channel.ts +191 -0
  127. package/extensions/keyed-async-queue/node_modules/.bin/tsc +21 -0
  128. package/extensions/keyed-async-queue/node_modules/.bin/tsserver +21 -0
  129. package/extensions/keyed-async-queue/node_modules/.bin/vitest +21 -0
  130. package/extensions/keyed-async-queue/package.json +20 -0
  131. package/extensions/keyed-async-queue/src/index.ts +14 -0
  132. package/extensions/keyed-async-queue/src/queue.test.ts +135 -0
  133. package/extensions/keyed-async-queue/src/queue.ts +200 -0
  134. package/extensions/memory-core/node_modules/.bin/tsc +21 -0
  135. package/extensions/memory-core/node_modules/.bin/tsserver +21 -0
  136. package/extensions/memory-core/node_modules/.bin/vitest +21 -0
  137. package/extensions/memory-core/package.json +11 -8
  138. package/extensions/memory-core/src/index.ts +14 -0
  139. package/extensions/memory-core/src/memory-manager.test.ts +124 -0
  140. package/extensions/memory-core/src/memory-manager.ts +186 -0
  141. package/extensions/nostr/node_modules/.bin/tsc +2 -2
  142. package/extensions/nostr/node_modules/.bin/tsserver +2 -2
  143. package/extensions/nostr/node_modules/.bin/vitest +21 -0
  144. package/extensions/nostr/package.json +15 -24
  145. package/extensions/nostr/src/index.ts +14 -0
  146. package/extensions/nostr/src/nostr-channel.test.ts +55 -0
  147. package/extensions/nostr/src/nostr-channel.ts +228 -0
  148. package/extensions/page-agent/node_modules/.bin/vitest +2 -2
  149. package/extensions/test-utils/node_modules/.bin/jiti +21 -0
  150. package/extensions/test-utils/node_modules/.bin/playwright +21 -0
  151. package/extensions/test-utils/node_modules/.bin/tsx +21 -0
  152. package/extensions/test-utils/node_modules/.bin/vite +21 -0
  153. package/extensions/test-utils/node_modules/.bin/vitest +21 -0
  154. package/extensions/test-utils/node_modules/.bin/yaml +21 -0
  155. package/extensions/xyops/node_modules/.bin/vitest +2 -2
  156. package/package.json +2 -1
  157. package/dist/control-ui/assets/index-Dvkl4Xlx.js.map +0 -1
  158. package/extensions/googlechat/node_modules/.bin/poolbot +0 -21
  159. package/extensions/memory-core/node_modules/.bin/poolbot +0 -21
@@ -0,0 +1,280 @@
1
+ import { resolveDefaultAgentId } from "../agents/agent-scope.js";
2
+ import { isRouteBinding, listRouteBindings } from "../config/bindings.js";
3
+ import { writeConfigFile } from "../config/config.js";
4
+ import { logConfigUpdated } from "../config/logging.js";
5
+ import { normalizeAgentId } from "../routing/session-key.js";
6
+ import { defaultRuntime } from "../runtime.js";
7
+ import { applyAgentBindings, describeBinding, parseBindingSpecs, removeAgentBindings, } from "./agents.bindings.js";
8
+ import { requireValidConfig } from "./agents.command-shared.js";
9
+ import { buildAgentSummaries } from "./agents.config.js";
10
+ function resolveAgentId(cfg, agentInput, params) {
11
+ if (!cfg) {
12
+ return null;
13
+ }
14
+ if (agentInput?.trim()) {
15
+ return normalizeAgentId(agentInput);
16
+ }
17
+ if (params?.fallbackToDefault) {
18
+ return resolveDefaultAgentId(cfg);
19
+ }
20
+ return null;
21
+ }
22
+ function hasAgent(cfg, agentId) {
23
+ if (!cfg) {
24
+ return false;
25
+ }
26
+ return buildAgentSummaries(cfg).some((summary) => summary.id === agentId);
27
+ }
28
+ function formatBindingOwnerLine(binding) {
29
+ return `${normalizeAgentId(binding.agentId)} <- ${describeBinding(binding)}`;
30
+ }
31
+ function resolveTargetAgentIdOrExit(params) {
32
+ const agentId = resolveAgentId(params.cfg, params.agentInput?.trim(), {
33
+ fallbackToDefault: true,
34
+ });
35
+ if (!agentId) {
36
+ params.runtime.error("Unable to resolve agent id.");
37
+ params.runtime.exit(1);
38
+ return null;
39
+ }
40
+ if (!hasAgent(params.cfg, agentId)) {
41
+ params.runtime.error(`Agent "${agentId}" not found.`);
42
+ params.runtime.exit(1);
43
+ return null;
44
+ }
45
+ return agentId;
46
+ }
47
+ function formatBindingConflicts(conflicts) {
48
+ return conflicts.map((conflict) => `${describeBinding(conflict.binding)} (agent=${conflict.existingAgentId})`);
49
+ }
50
+ function resolveParsedBindingsOrExit(params) {
51
+ const specs = (params.bindValues ?? []).map((value) => value.trim()).filter(Boolean);
52
+ if (specs.length === 0) {
53
+ params.runtime.error(params.emptyMessage);
54
+ params.runtime.exit(1);
55
+ return null;
56
+ }
57
+ const parsed = parseBindingSpecs({ agentId: params.agentId, specs, config: params.cfg });
58
+ if (parsed.errors.length > 0) {
59
+ params.runtime.error(parsed.errors.join("\n"));
60
+ params.runtime.exit(1);
61
+ return null;
62
+ }
63
+ return parsed;
64
+ }
65
+ function emitJsonPayload(params) {
66
+ if (!params.json) {
67
+ return false;
68
+ }
69
+ params.runtime.log(JSON.stringify(params.payload, null, 2));
70
+ if ((params.conflictCount ?? 0) > 0) {
71
+ params.runtime.exit(1);
72
+ }
73
+ return true;
74
+ }
75
+ async function resolveConfigAndTargetAgentIdOrExit(params) {
76
+ const cfg = await requireValidConfig(params.runtime);
77
+ if (!cfg) {
78
+ return null;
79
+ }
80
+ const agentId = resolveTargetAgentIdOrExit({
81
+ cfg,
82
+ runtime: params.runtime,
83
+ agentInput: params.agentInput,
84
+ });
85
+ if (!agentId) {
86
+ return null;
87
+ }
88
+ return { cfg, agentId };
89
+ }
90
+ export async function agentsBindingsCommand(opts, runtime = defaultRuntime) {
91
+ const cfg = await requireValidConfig(runtime);
92
+ if (!cfg) {
93
+ return;
94
+ }
95
+ const filterAgentId = resolveAgentId(cfg, opts.agent?.trim());
96
+ if (opts.agent && !filterAgentId) {
97
+ runtime.error("Agent id is required.");
98
+ runtime.exit(1);
99
+ return;
100
+ }
101
+ if (filterAgentId && !hasAgent(cfg, filterAgentId)) {
102
+ runtime.error(`Agent "${filterAgentId}" not found.`);
103
+ runtime.exit(1);
104
+ return;
105
+ }
106
+ const filtered = listRouteBindings(cfg).filter((binding) => !filterAgentId || normalizeAgentId(binding.agentId) === filterAgentId);
107
+ if (opts.json) {
108
+ runtime.log(JSON.stringify(filtered.map((binding) => ({
109
+ agentId: normalizeAgentId(binding.agentId),
110
+ match: binding.match,
111
+ description: describeBinding(binding),
112
+ })), null, 2));
113
+ return;
114
+ }
115
+ if (filtered.length === 0) {
116
+ runtime.log(filterAgentId ? `No routing bindings for agent "${filterAgentId}".` : "No routing bindings.");
117
+ return;
118
+ }
119
+ runtime.log([
120
+ "Routing bindings:",
121
+ ...filtered.map((binding) => `- ${formatBindingOwnerLine(binding)}`),
122
+ ].join("\n"));
123
+ }
124
+ export async function agentsBindCommand(opts, runtime = defaultRuntime) {
125
+ const resolved = await resolveConfigAndTargetAgentIdOrExit({
126
+ runtime,
127
+ agentInput: opts.agent,
128
+ });
129
+ if (!resolved) {
130
+ return;
131
+ }
132
+ const { cfg, agentId } = resolved;
133
+ const parsed = resolveParsedBindingsOrExit({
134
+ runtime,
135
+ cfg,
136
+ agentId,
137
+ bindValues: opts.bind,
138
+ emptyMessage: "Provide at least one --bind <channel[:accountId]>.",
139
+ });
140
+ if (!parsed) {
141
+ return;
142
+ }
143
+ const result = applyAgentBindings(cfg, parsed.bindings);
144
+ if (result.added.length > 0 || result.updated.length > 0) {
145
+ await writeConfigFile(result.config);
146
+ if (!opts.json) {
147
+ logConfigUpdated(runtime);
148
+ }
149
+ }
150
+ const payload = {
151
+ agentId,
152
+ added: result.added.map(describeBinding),
153
+ updated: result.updated.map(describeBinding),
154
+ skipped: result.skipped.map(describeBinding),
155
+ conflicts: formatBindingConflicts(result.conflicts),
156
+ };
157
+ if (emitJsonPayload({ runtime, json: opts.json, payload, conflictCount: result.conflicts.length })) {
158
+ return;
159
+ }
160
+ if (result.added.length > 0) {
161
+ runtime.log("Added bindings:");
162
+ for (const binding of result.added) {
163
+ runtime.log(`- ${describeBinding(binding)}`);
164
+ }
165
+ }
166
+ else if (result.updated.length === 0) {
167
+ runtime.log("No new bindings added.");
168
+ }
169
+ if (result.updated.length > 0) {
170
+ runtime.log("Updated bindings:");
171
+ for (const binding of result.updated) {
172
+ runtime.log(`- ${describeBinding(binding)}`);
173
+ }
174
+ }
175
+ if (result.skipped.length > 0) {
176
+ runtime.log("Already present:");
177
+ for (const binding of result.skipped) {
178
+ runtime.log(`- ${describeBinding(binding)}`);
179
+ }
180
+ }
181
+ if (result.conflicts.length > 0) {
182
+ runtime.error("Skipped bindings already claimed by another agent:");
183
+ for (const conflict of result.conflicts) {
184
+ runtime.error(`- ${describeBinding(conflict.binding)} (agent=${conflict.existingAgentId})`);
185
+ }
186
+ runtime.exit(1);
187
+ }
188
+ }
189
+ export async function agentsUnbindCommand(opts, runtime = defaultRuntime) {
190
+ const resolved = await resolveConfigAndTargetAgentIdOrExit({
191
+ runtime,
192
+ agentInput: opts.agent,
193
+ });
194
+ if (!resolved) {
195
+ return;
196
+ }
197
+ const { cfg, agentId } = resolved;
198
+ if (opts.all && (opts.bind?.length ?? 0) > 0) {
199
+ runtime.error("Use either --all or --bind, not both.");
200
+ runtime.exit(1);
201
+ return;
202
+ }
203
+ if (opts.all) {
204
+ const existing = listRouteBindings(cfg);
205
+ const removed = existing.filter((binding) => normalizeAgentId(binding.agentId) === agentId);
206
+ const keptRoutes = existing.filter((binding) => normalizeAgentId(binding.agentId) !== agentId);
207
+ const nonRoutes = (cfg.bindings ?? []).filter((binding) => !isRouteBinding(binding));
208
+ if (removed.length === 0) {
209
+ runtime.log(`No bindings to remove for agent "${agentId}".`);
210
+ return;
211
+ }
212
+ const next = {
213
+ ...cfg,
214
+ bindings: [...keptRoutes, ...nonRoutes].length > 0 ? [...keptRoutes, ...nonRoutes] : undefined,
215
+ };
216
+ await writeConfigFile(next);
217
+ if (!opts.json) {
218
+ logConfigUpdated(runtime);
219
+ }
220
+ const payload = {
221
+ agentId,
222
+ removed: removed.map(describeBinding),
223
+ missing: [],
224
+ conflicts: [],
225
+ };
226
+ if (emitJsonPayload({ runtime, json: opts.json, payload })) {
227
+ return;
228
+ }
229
+ runtime.log(`Removed ${removed.length} binding(s) for "${agentId}".`);
230
+ return;
231
+ }
232
+ const parsed = resolveParsedBindingsOrExit({
233
+ runtime,
234
+ cfg,
235
+ agentId,
236
+ bindValues: opts.bind,
237
+ emptyMessage: "Provide at least one --bind <channel[:accountId]> or use --all.",
238
+ });
239
+ if (!parsed) {
240
+ return;
241
+ }
242
+ const result = removeAgentBindings(cfg, parsed.bindings);
243
+ if (result.removed.length > 0) {
244
+ await writeConfigFile(result.config);
245
+ if (!opts.json) {
246
+ logConfigUpdated(runtime);
247
+ }
248
+ }
249
+ const payload = {
250
+ agentId,
251
+ removed: result.removed.map(describeBinding),
252
+ missing: result.missing.map(describeBinding),
253
+ conflicts: formatBindingConflicts(result.conflicts),
254
+ };
255
+ if (emitJsonPayload({ runtime, json: opts.json, payload, conflictCount: result.conflicts.length })) {
256
+ return;
257
+ }
258
+ if (result.removed.length > 0) {
259
+ runtime.log("Removed bindings:");
260
+ for (const binding of result.removed) {
261
+ runtime.log(`- ${describeBinding(binding)}`);
262
+ }
263
+ }
264
+ else {
265
+ runtime.log("No bindings removed.");
266
+ }
267
+ if (result.missing.length > 0) {
268
+ runtime.log("Not found:");
269
+ for (const binding of result.missing) {
270
+ runtime.log(`- ${describeBinding(binding)}`);
271
+ }
272
+ }
273
+ if (result.conflicts.length > 0) {
274
+ runtime.error("Bindings are owned by another agent:");
275
+ for (const conflict of result.conflicts) {
276
+ runtime.error(`- ${describeBinding(conflict.binding)} (agent=${conflict.existingAgentId})`);
277
+ }
278
+ runtime.exit(1);
279
+ }
280
+ }
@@ -0,0 +1,186 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { readConfigFileSnapshot, resolveConfigPath, resolveOAuthDir, resolveStateDir, } from "../config/config.js";
4
+ import { formatSessionArchiveTimestamp } from "../config/sessions/artifacts.js";
5
+ import { pathExists, shortenHomePath } from "../utils.js";
6
+ import { buildCleanupPlan, isPathWithin } from "./cleanup-utils.js";
7
+ function backupAssetPriority(kind) {
8
+ switch (kind) {
9
+ case "state":
10
+ return 0;
11
+ case "config":
12
+ return 1;
13
+ case "credentials":
14
+ return 2;
15
+ case "workspace":
16
+ return 3;
17
+ }
18
+ }
19
+ export function buildBackupArchiveRoot(nowMs = Date.now()) {
20
+ return `${formatSessionArchiveTimestamp(nowMs)}-openclaw-backup`;
21
+ }
22
+ export function buildBackupArchiveBasename(nowMs = Date.now()) {
23
+ return `${buildBackupArchiveRoot(nowMs)}.tar.gz`;
24
+ }
25
+ export function encodeAbsolutePathForBackupArchive(sourcePath) {
26
+ const normalized = sourcePath.replaceAll("\\", "/");
27
+ const windowsMatch = normalized.match(/^([A-Za-z]):\/(.*)$/);
28
+ if (windowsMatch) {
29
+ const drive = windowsMatch[1]?.toUpperCase() ?? "UNKNOWN";
30
+ const rest = windowsMatch[2] ?? "";
31
+ return path.posix.join("windows", drive, rest);
32
+ }
33
+ if (normalized.startsWith("/")) {
34
+ return path.posix.join("posix", normalized.slice(1));
35
+ }
36
+ return path.posix.join("relative", normalized);
37
+ }
38
+ export function buildBackupArchivePath(archiveRoot, sourcePath) {
39
+ return path.posix.join(archiveRoot, "payload", encodeAbsolutePathForBackupArchive(sourcePath));
40
+ }
41
+ function compareCandidates(left, right) {
42
+ const depthDelta = left.canonicalPath.length - right.canonicalPath.length;
43
+ if (depthDelta !== 0) {
44
+ return depthDelta;
45
+ }
46
+ const priorityDelta = backupAssetPriority(left.kind) - backupAssetPriority(right.kind);
47
+ if (priorityDelta !== 0) {
48
+ return priorityDelta;
49
+ }
50
+ return left.canonicalPath.localeCompare(right.canonicalPath);
51
+ }
52
+ async function canonicalizeExistingPath(targetPath) {
53
+ try {
54
+ return await fs.realpath(targetPath);
55
+ }
56
+ catch {
57
+ return path.resolve(targetPath);
58
+ }
59
+ }
60
+ export async function resolveBackupPlanFromDisk(params = {}) {
61
+ const includeWorkspace = params.includeWorkspace ?? true;
62
+ const onlyConfig = params.onlyConfig ?? false;
63
+ const stateDir = resolveStateDir();
64
+ const configPath = resolveConfigPath();
65
+ const oauthDir = resolveOAuthDir();
66
+ const archiveRoot = buildBackupArchiveRoot(params.nowMs);
67
+ if (onlyConfig) {
68
+ const resolvedConfigPath = path.resolve(configPath);
69
+ if (!(await pathExists(resolvedConfigPath))) {
70
+ return {
71
+ stateDir,
72
+ configPath,
73
+ oauthDir,
74
+ workspaceDirs: [],
75
+ included: [],
76
+ skipped: [
77
+ {
78
+ kind: "config",
79
+ sourcePath: resolvedConfigPath,
80
+ displayPath: shortenHomePath(resolvedConfigPath),
81
+ reason: "missing",
82
+ },
83
+ ],
84
+ };
85
+ }
86
+ const canonicalConfigPath = await canonicalizeExistingPath(resolvedConfigPath);
87
+ return {
88
+ stateDir,
89
+ configPath,
90
+ oauthDir,
91
+ workspaceDirs: [],
92
+ included: [
93
+ {
94
+ kind: "config",
95
+ sourcePath: canonicalConfigPath,
96
+ displayPath: shortenHomePath(canonicalConfigPath),
97
+ archivePath: buildBackupArchivePath(archiveRoot, canonicalConfigPath),
98
+ },
99
+ ],
100
+ skipped: [],
101
+ };
102
+ }
103
+ const configSnapshot = await readConfigFileSnapshot();
104
+ if (includeWorkspace && configSnapshot.exists && !configSnapshot.valid) {
105
+ throw new Error(`Config invalid at ${shortenHomePath(configSnapshot.path)}. OpenClaw cannot reliably discover custom workspaces for backup. Fix the config or rerun with --no-include-workspace for a partial backup.`);
106
+ }
107
+ const cleanupPlan = buildCleanupPlan({
108
+ cfg: configSnapshot.config,
109
+ stateDir,
110
+ configPath,
111
+ oauthDir,
112
+ });
113
+ const workspaceDirs = includeWorkspace ? cleanupPlan.workspaceDirs : [];
114
+ const rawCandidates = [
115
+ { kind: "state", sourcePath: path.resolve(stateDir) },
116
+ ...(cleanupPlan.configInsideState
117
+ ? []
118
+ : [{ kind: "config", sourcePath: path.resolve(configPath) }]),
119
+ ...(cleanupPlan.oauthInsideState
120
+ ? []
121
+ : [{ kind: "credentials", sourcePath: path.resolve(oauthDir) }]),
122
+ ...(includeWorkspace
123
+ ? workspaceDirs.map((workspaceDir) => ({
124
+ kind: "workspace",
125
+ sourcePath: path.resolve(workspaceDir),
126
+ }))
127
+ : []),
128
+ ];
129
+ const candidates = await Promise.all(rawCandidates.map(async (candidate) => {
130
+ const exists = await pathExists(candidate.sourcePath);
131
+ return {
132
+ ...candidate,
133
+ exists,
134
+ canonicalPath: exists
135
+ ? await canonicalizeExistingPath(candidate.sourcePath)
136
+ : path.resolve(candidate.sourcePath),
137
+ };
138
+ }));
139
+ const uniqueCandidates = [];
140
+ const seenCanonicalPaths = new Set();
141
+ for (const candidate of [...candidates].toSorted(compareCandidates)) {
142
+ if (seenCanonicalPaths.has(candidate.canonicalPath)) {
143
+ continue;
144
+ }
145
+ seenCanonicalPaths.add(candidate.canonicalPath);
146
+ uniqueCandidates.push(candidate);
147
+ }
148
+ const included = [];
149
+ const skipped = [];
150
+ for (const candidate of uniqueCandidates) {
151
+ if (!candidate.exists) {
152
+ skipped.push({
153
+ kind: candidate.kind,
154
+ sourcePath: candidate.sourcePath,
155
+ displayPath: shortenHomePath(candidate.sourcePath),
156
+ reason: "missing",
157
+ });
158
+ continue;
159
+ }
160
+ const coveredBy = included.find((asset) => isPathWithin(candidate.canonicalPath, asset.sourcePath));
161
+ if (coveredBy) {
162
+ skipped.push({
163
+ kind: candidate.kind,
164
+ sourcePath: candidate.canonicalPath,
165
+ displayPath: shortenHomePath(candidate.canonicalPath),
166
+ reason: "covered",
167
+ coveredBy: coveredBy.displayPath,
168
+ });
169
+ continue;
170
+ }
171
+ included.push({
172
+ kind: candidate.kind,
173
+ sourcePath: candidate.canonicalPath,
174
+ displayPath: shortenHomePath(candidate.canonicalPath),
175
+ archivePath: buildBackupArchivePath(archiveRoot, candidate.canonicalPath),
176
+ });
177
+ }
178
+ return {
179
+ stateDir,
180
+ configPath,
181
+ oauthDir,
182
+ workspaceDirs: workspaceDirs.map((entry) => path.resolve(entry)),
183
+ included,
184
+ skipped,
185
+ };
186
+ }
@@ -0,0 +1,236 @@
1
+ import path from "node:path";
2
+ import * as tar from "tar";
3
+ import { resolveUserPath } from "../utils.js";
4
+ const WINDOWS_ABSOLUTE_ARCHIVE_PATH_RE = /^[A-Za-z]:[\\/]/;
5
+ function isRecord(value) {
6
+ return typeof value === "object" && value !== null && !Array.isArray(value);
7
+ }
8
+ function stripTrailingSlashes(value) {
9
+ return value.replace(/\/+$/u, "");
10
+ }
11
+ function normalizeArchivePath(entryPath, label) {
12
+ const trimmed = stripTrailingSlashes(entryPath.trim());
13
+ if (!trimmed) {
14
+ throw new Error(`${label} is empty.`);
15
+ }
16
+ if (trimmed.startsWith("/") || WINDOWS_ABSOLUTE_ARCHIVE_PATH_RE.test(trimmed)) {
17
+ throw new Error(`${label} must be relative: ${entryPath}`);
18
+ }
19
+ if (trimmed.includes("\\")) {
20
+ throw new Error(`${label} must use forward slashes: ${entryPath}`);
21
+ }
22
+ if (trimmed.split("/").some((segment) => segment === "." || segment === "..")) {
23
+ throw new Error(`${label} contains path traversal segments: ${entryPath}`);
24
+ }
25
+ const normalized = stripTrailingSlashes(path.posix.normalize(trimmed));
26
+ if (!normalized || normalized === "." || normalized === ".." || normalized.startsWith("../")) {
27
+ throw new Error(`${label} resolves outside the archive root: ${entryPath}`);
28
+ }
29
+ return normalized;
30
+ }
31
+ function normalizeArchiveRoot(rootName) {
32
+ const normalized = normalizeArchivePath(rootName, "Backup manifest archiveRoot");
33
+ if (normalized.includes("/")) {
34
+ throw new Error(`Backup manifest archiveRoot must be a single path segment: ${rootName}`);
35
+ }
36
+ return normalized;
37
+ }
38
+ function isArchivePathWithin(child, parent) {
39
+ const relative = path.posix.relative(parent, child);
40
+ return relative === "" || (!relative.startsWith("../") && relative !== "..");
41
+ }
42
+ function parseManifest(raw) {
43
+ let parsed;
44
+ try {
45
+ parsed = JSON.parse(raw);
46
+ }
47
+ catch (err) {
48
+ throw new Error(`Backup manifest is not valid JSON: ${String(err)}`, { cause: err });
49
+ }
50
+ if (!isRecord(parsed)) {
51
+ throw new Error("Backup manifest must be an object.");
52
+ }
53
+ if (parsed.schemaVersion !== 1) {
54
+ throw new Error(`Unsupported backup manifest schemaVersion: ${String(parsed.schemaVersion)}`);
55
+ }
56
+ if (typeof parsed.archiveRoot !== "string" || !parsed.archiveRoot.trim()) {
57
+ throw new Error("Backup manifest is missing archiveRoot.");
58
+ }
59
+ if (typeof parsed.createdAt !== "string" || !parsed.createdAt.trim()) {
60
+ throw new Error("Backup manifest is missing createdAt.");
61
+ }
62
+ if (!Array.isArray(parsed.assets)) {
63
+ throw new Error("Backup manifest is missing assets.");
64
+ }
65
+ const assets = [];
66
+ for (const asset of parsed.assets) {
67
+ if (!isRecord(asset)) {
68
+ throw new Error("Backup manifest contains a non-object asset.");
69
+ }
70
+ if (typeof asset.kind !== "string" || !asset.kind.trim()) {
71
+ throw new Error("Backup manifest asset is missing kind.");
72
+ }
73
+ if (typeof asset.sourcePath !== "string" || !asset.sourcePath.trim()) {
74
+ throw new Error("Backup manifest asset is missing sourcePath.");
75
+ }
76
+ if (typeof asset.archivePath !== "string" || !asset.archivePath.trim()) {
77
+ throw new Error("Backup manifest asset is missing archivePath.");
78
+ }
79
+ assets.push({
80
+ kind: asset.kind,
81
+ sourcePath: asset.sourcePath,
82
+ archivePath: asset.archivePath,
83
+ });
84
+ }
85
+ return {
86
+ schemaVersion: 1,
87
+ archiveRoot: parsed.archiveRoot,
88
+ createdAt: parsed.createdAt,
89
+ runtimeVersion: typeof parsed.runtimeVersion === "string" && parsed.runtimeVersion.trim()
90
+ ? parsed.runtimeVersion
91
+ : "unknown",
92
+ platform: typeof parsed.platform === "string" ? parsed.platform : "unknown",
93
+ nodeVersion: typeof parsed.nodeVersion === "string" ? parsed.nodeVersion : "unknown",
94
+ options: isRecord(parsed.options)
95
+ ? { includeWorkspace: parsed.options.includeWorkspace }
96
+ : undefined,
97
+ paths: isRecord(parsed.paths)
98
+ ? {
99
+ stateDir: typeof parsed.paths.stateDir === "string" ? parsed.paths.stateDir : undefined,
100
+ configPath: typeof parsed.paths.configPath === "string" ? parsed.paths.configPath : undefined,
101
+ oauthDir: typeof parsed.paths.oauthDir === "string" ? parsed.paths.oauthDir : undefined,
102
+ workspaceDirs: Array.isArray(parsed.paths.workspaceDirs)
103
+ ? parsed.paths.workspaceDirs.filter((entry) => typeof entry === "string")
104
+ : undefined,
105
+ }
106
+ : undefined,
107
+ assets,
108
+ skipped: Array.isArray(parsed.skipped) ? parsed.skipped : undefined,
109
+ };
110
+ }
111
+ async function listArchiveEntries(archivePath) {
112
+ const entries = [];
113
+ await tar.t({
114
+ file: archivePath,
115
+ gzip: true,
116
+ onentry: (entry) => {
117
+ entries.push(entry.path);
118
+ },
119
+ });
120
+ return entries;
121
+ }
122
+ async function extractManifest(params) {
123
+ let manifestContentPromise;
124
+ await tar.t({
125
+ file: params.archivePath,
126
+ gzip: true,
127
+ onentry: (entry) => {
128
+ if (entry.path !== params.manifestEntryPath) {
129
+ entry.resume();
130
+ return;
131
+ }
132
+ manifestContentPromise = new Promise((resolve, reject) => {
133
+ const chunks = [];
134
+ entry.on("data", (chunk) => {
135
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
136
+ });
137
+ entry.on("error", reject);
138
+ entry.on("end", () => {
139
+ resolve(Buffer.concat(chunks).toString("utf8"));
140
+ });
141
+ });
142
+ },
143
+ });
144
+ if (!manifestContentPromise) {
145
+ throw new Error(`Archive is missing manifest entry: ${params.manifestEntryPath}`);
146
+ }
147
+ return await manifestContentPromise;
148
+ }
149
+ function isRootManifestEntry(entryPath) {
150
+ const parts = entryPath.split("/");
151
+ return parts.length === 2 && parts[0] !== "" && parts[1] === "manifest.json";
152
+ }
153
+ function verifyManifestAgainstEntries(manifest, entries) {
154
+ const archiveRoot = normalizeArchiveRoot(manifest.archiveRoot);
155
+ const manifestEntryPath = path.posix.join(archiveRoot, "manifest.json");
156
+ const normalizedEntries = [...entries];
157
+ const normalizedEntrySet = new Set(normalizedEntries);
158
+ if (!normalizedEntrySet.has(manifestEntryPath)) {
159
+ throw new Error(`Archive is missing manifest entry: ${manifestEntryPath}`);
160
+ }
161
+ for (const entry of normalizedEntries) {
162
+ if (!isArchivePathWithin(entry, archiveRoot)) {
163
+ throw new Error(`Archive entry is outside the declared archive root: ${entry}`);
164
+ }
165
+ }
166
+ const payloadRoot = path.posix.join(archiveRoot, "payload");
167
+ for (const asset of manifest.assets) {
168
+ const assetArchivePath = normalizeArchivePath(asset.archivePath, "Backup manifest asset path");
169
+ if (!isArchivePathWithin(assetArchivePath, payloadRoot)) {
170
+ throw new Error(`Manifest asset path is outside payload root: ${asset.archivePath}`);
171
+ }
172
+ const exact = normalizedEntrySet.has(assetArchivePath);
173
+ const nested = normalizedEntries.some((entry) => entry !== assetArchivePath && isArchivePathWithin(entry, assetArchivePath));
174
+ if (!exact && !nested) {
175
+ throw new Error(`Archive is missing payload for manifest asset: ${assetArchivePath}`);
176
+ }
177
+ }
178
+ }
179
+ function formatResult(result) {
180
+ return [
181
+ `Backup archive OK: ${result.archivePath}`,
182
+ `Archive root: ${result.archiveRoot}`,
183
+ `Created at: ${result.createdAt}`,
184
+ `Runtime version: ${result.runtimeVersion}`,
185
+ `Assets verified: ${result.assetCount}`,
186
+ `Archive entries scanned: ${result.entryCount}`,
187
+ ].join("\n");
188
+ }
189
+ function findDuplicateNormalizedEntryPath(entries) {
190
+ const seen = new Set();
191
+ for (const entry of entries) {
192
+ if (seen.has(entry.normalized)) {
193
+ return entry.normalized;
194
+ }
195
+ seen.add(entry.normalized);
196
+ }
197
+ return undefined;
198
+ }
199
+ export async function backupVerifyCommand(runtime, opts) {
200
+ const archivePath = resolveUserPath(opts.archive);
201
+ const rawEntries = await listArchiveEntries(archivePath);
202
+ if (rawEntries.length === 0) {
203
+ throw new Error("Backup archive is empty.");
204
+ }
205
+ const entries = rawEntries.map((entry) => ({
206
+ raw: entry,
207
+ normalized: normalizeArchivePath(entry, "Archive entry"),
208
+ }));
209
+ const normalizedEntrySet = new Set(entries.map((entry) => entry.normalized));
210
+ const manifestMatches = entries.filter((entry) => isRootManifestEntry(entry.normalized));
211
+ if (manifestMatches.length !== 1) {
212
+ throw new Error(`Expected exactly one backup manifest entry, found ${manifestMatches.length}.`);
213
+ }
214
+ const duplicateEntryPath = findDuplicateNormalizedEntryPath(entries);
215
+ if (duplicateEntryPath) {
216
+ throw new Error(`Archive contains duplicate entry path: ${duplicateEntryPath}`);
217
+ }
218
+ const manifestEntryPath = manifestMatches[0]?.raw;
219
+ if (!manifestEntryPath) {
220
+ throw new Error("Backup archive manifest entry could not be resolved.");
221
+ }
222
+ const manifestRaw = await extractManifest({ archivePath, manifestEntryPath });
223
+ const manifest = parseManifest(manifestRaw);
224
+ verifyManifestAgainstEntries(manifest, normalizedEntrySet);
225
+ const result = {
226
+ ok: true,
227
+ archivePath,
228
+ archiveRoot: manifest.archiveRoot,
229
+ createdAt: manifest.createdAt,
230
+ runtimeVersion: manifest.runtimeVersion,
231
+ assetCount: manifest.assets.length,
232
+ entryCount: rawEntries.length,
233
+ };
234
+ runtime.log(opts.json ? JSON.stringify(result, null, 2) : formatResult(result));
235
+ return result;
236
+ }