@agentlayer.tech/wallet 0.1.75 → 0.1.76
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.
- package/.openclaw/extensions/agent-wallet/openclaw.plugin.json +1 -1
- package/.openclaw/extensions/agent-wallet/package.json +1 -1
- package/VERSION +1 -1
- package/agent-wallet/AGENTS.md +1 -0
- package/agent-wallet/UPGRADE_COMPATIBILITY.md +36 -0
- package/agent-wallet/agent_wallet/__init__.py +1 -1
- package/agent-wallet/agent_wallet/boot_key_recovery.py +10 -1
- package/agent-wallet/openclaw.plugin.json +1 -1
- package/agent-wallet/pyproject.toml +1 -1
- package/bin/lib/boot-key.mjs +185 -0
- package/bin/lib/integrations.mjs +424 -0
- package/bin/lib/update-transaction.mjs +235 -0
- package/bin/openclaw-agent-wallet.mjs +225 -520
- package/claude-code/plugins/agent-wallet/.claude-plugin/plugin.json +1 -1
- package/codex/plugins/agent-wallet/.codex-plugin/plugin.json +1 -1
- package/hermes/plugins/agent_wallet/plugin.yaml +1 -1
- package/package.json +2 -1
- package/wdk-btc-wallet/package.json +1 -1
- package/wdk-evm-wallet/package.json +1 -1
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
|
|
5
|
+
function readJson(pathname) {
|
|
6
|
+
try {
|
|
7
|
+
return JSON.parse(fs.readFileSync(pathname, "utf8"));
|
|
8
|
+
} catch (error) {
|
|
9
|
+
if (error?.code === "ENOENT") return null;
|
|
10
|
+
throw error;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function writeJsonAtomic(pathname, value) {
|
|
15
|
+
fs.mkdirSync(path.dirname(pathname), { recursive: true });
|
|
16
|
+
const temporary = `${pathname}.tmp-${process.pid}-${Date.now()}`;
|
|
17
|
+
try {
|
|
18
|
+
fs.writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
|
|
19
|
+
fs.renameSync(temporary, pathname);
|
|
20
|
+
fs.chmodSync(pathname, 0o600);
|
|
21
|
+
} finally {
|
|
22
|
+
fs.rmSync(temporary, { force: true });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function createIntegrationManager({ runtimeBase, packageVersion, activeVersion }) {
|
|
27
|
+
const registryPath = path.join(runtimeBase, "integrations.json");
|
|
28
|
+
|
|
29
|
+
function emptyRegistry(error = "") {
|
|
30
|
+
return {
|
|
31
|
+
schema_version: 1,
|
|
32
|
+
integrations: {},
|
|
33
|
+
...(error ? { registry_error: error } : {}),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function readRegistry() {
|
|
38
|
+
let payload;
|
|
39
|
+
try {
|
|
40
|
+
payload = readJson(registryPath);
|
|
41
|
+
} catch (error) {
|
|
42
|
+
return emptyRegistry(`integration registry is unreadable: ${error.message}`);
|
|
43
|
+
}
|
|
44
|
+
if (!payload) return emptyRegistry();
|
|
45
|
+
if (
|
|
46
|
+
payload.schema_version !== 1 ||
|
|
47
|
+
!payload.integrations ||
|
|
48
|
+
typeof payload.integrations !== "object" ||
|
|
49
|
+
Array.isArray(payload.integrations)
|
|
50
|
+
) {
|
|
51
|
+
return emptyRegistry("integration registry schema is invalid");
|
|
52
|
+
}
|
|
53
|
+
return payload;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function quarantineCorruptRegistry(registry) {
|
|
57
|
+
if (!registry.registry_error || !fs.existsSync(registryPath)) return null;
|
|
58
|
+
let backup = `${registryPath}.corrupt-${Date.now()}`;
|
|
59
|
+
let suffix = 2;
|
|
60
|
+
while (fs.existsSync(backup)) {
|
|
61
|
+
backup = `${registryPath}.corrupt-${Date.now()}-${suffix}`;
|
|
62
|
+
suffix += 1;
|
|
63
|
+
}
|
|
64
|
+
fs.renameSync(registryPath, backup);
|
|
65
|
+
return backup;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function record(name, details = {}) {
|
|
69
|
+
let registry = readRegistry();
|
|
70
|
+
const corruptBackup = quarantineCorruptRegistry(registry);
|
|
71
|
+
if (registry.registry_error) registry = emptyRegistry();
|
|
72
|
+
if (corruptBackup) registry.recovered_corrupt_registry = path.basename(corruptBackup);
|
|
73
|
+
registry.integrations[name] = {
|
|
74
|
+
...details,
|
|
75
|
+
managed: true,
|
|
76
|
+
installed_version: packageVersion,
|
|
77
|
+
updated_at: new Date().toISOString(),
|
|
78
|
+
};
|
|
79
|
+
registry.updated_at = new Date().toISOString();
|
|
80
|
+
writeJsonAtomic(registryPath, registry);
|
|
81
|
+
return registry.integrations[name];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function managed(name) {
|
|
85
|
+
const entry = readRegistry().integrations[name];
|
|
86
|
+
return entry?.managed === true ? entry : null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function status() {
|
|
90
|
+
const active = activeVersion();
|
|
91
|
+
const registry = readRegistry();
|
|
92
|
+
const managedIntegrations = Object.entries(registry.integrations)
|
|
93
|
+
.filter(([, entry]) => entry?.managed === true)
|
|
94
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
95
|
+
.map(([name, entry]) => {
|
|
96
|
+
const versionInSync = active === null || entry.installed_version === active;
|
|
97
|
+
const registrationOk = entry.registration_ok !== false;
|
|
98
|
+
return {
|
|
99
|
+
name,
|
|
100
|
+
installed_version: entry.installed_version || null,
|
|
101
|
+
active_version: active,
|
|
102
|
+
in_sync: versionInSync && registrationOk,
|
|
103
|
+
registration_ok: registrationOk,
|
|
104
|
+
restart_required: Boolean(entry.restart_required),
|
|
105
|
+
};
|
|
106
|
+
});
|
|
107
|
+
return {
|
|
108
|
+
in_sync: !registry.registry_error && managedIntegrations.every((entry) => entry.in_sync),
|
|
109
|
+
registry_ok: !registry.registry_error,
|
|
110
|
+
registry_error: registry.registry_error || "",
|
|
111
|
+
recovered_corrupt_registry: registry.recovered_corrupt_registry || null,
|
|
112
|
+
integrations: managedIntegrations,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function safelyRefresh(name, callback) {
|
|
117
|
+
try {
|
|
118
|
+
return callback();
|
|
119
|
+
} catch (error) {
|
|
120
|
+
const fix = name === "global-cli"
|
|
121
|
+
? "Re-run: wallet update --yes"
|
|
122
|
+
: name === "openclaw"
|
|
123
|
+
? "Re-run: wallet install --yes"
|
|
124
|
+
: `Re-run: wallet ${name} install --yes`;
|
|
125
|
+
return {
|
|
126
|
+
name,
|
|
127
|
+
attempted: true,
|
|
128
|
+
ok: false,
|
|
129
|
+
repaired: false,
|
|
130
|
+
error: error?.message || String(error),
|
|
131
|
+
restart_required: false,
|
|
132
|
+
fix,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function refreshAll(refreshers) {
|
|
138
|
+
return Object.entries(refreshers)
|
|
139
|
+
.map(([name, callback]) => safelyRefresh(name, callback))
|
|
140
|
+
.filter(Boolean);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return { registryPath, readRegistry, record, managed, status, safelyRefresh, refreshAll };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function createHostIntegrationManager({
|
|
147
|
+
env,
|
|
148
|
+
packageRoot,
|
|
149
|
+
registry,
|
|
150
|
+
currentRuntimePath,
|
|
151
|
+
openclawHome,
|
|
152
|
+
hermesHome,
|
|
153
|
+
codexHome,
|
|
154
|
+
codexPluginRoot,
|
|
155
|
+
codexMarketplacePath,
|
|
156
|
+
claudeMarketplaceDir,
|
|
157
|
+
claudeMarketplaceName,
|
|
158
|
+
expandHome,
|
|
159
|
+
resolveVenvPython,
|
|
160
|
+
commandPath,
|
|
161
|
+
repairRuntimeSymlink,
|
|
162
|
+
repairHermesEnv,
|
|
163
|
+
ensureCodexMarketplaceEntry,
|
|
164
|
+
ensureClaudeCodeMarketplace,
|
|
165
|
+
pinClaudeCacheCopies,
|
|
166
|
+
}) {
|
|
167
|
+
const runtimeBase = path.dirname(currentRuntimePath);
|
|
168
|
+
|
|
169
|
+
function runtimeOwnedTarget(target) {
|
|
170
|
+
const relative = path.relative(runtimeBase, path.resolve(target));
|
|
171
|
+
return relative === "" || (!relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function symlinkTarget(linkPath) {
|
|
175
|
+
try {
|
|
176
|
+
if (!fs.lstatSync(linkPath).isSymbolicLink()) return null;
|
|
177
|
+
return path.resolve(path.dirname(linkPath), fs.readlinkSync(linkPath));
|
|
178
|
+
} catch {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function symlinkManifestMatches(linkPath, manifestRelativePath, expectedName) {
|
|
184
|
+
try {
|
|
185
|
+
const target = symlinkTarget(linkPath);
|
|
186
|
+
if (!target || !runtimeOwnedTarget(target)) return false;
|
|
187
|
+
return readJson(path.join(target, manifestRelativePath))?.name === expectedName;
|
|
188
|
+
} catch {
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function adoptHermes() {
|
|
194
|
+
const pluginTarget = path.join(hermesHome, "plugins", "agent_wallet");
|
|
195
|
+
const target = symlinkTarget(pluginTarget);
|
|
196
|
+
if (!target || !runtimeOwnedTarget(target)) return null;
|
|
197
|
+
let manifest = "";
|
|
198
|
+
try {
|
|
199
|
+
manifest = fs.readFileSync(path.join(target, "plugin.yaml"), "utf8");
|
|
200
|
+
} catch {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
if (!/^name:\s*agent[-_]wallet\s*$/m.test(manifest)) return null;
|
|
204
|
+
return registry.record("hermes", {
|
|
205
|
+
hermes_home: hermesHome,
|
|
206
|
+
plugin_target: pluginTarget,
|
|
207
|
+
env_path: path.join(hermesHome, ".env"),
|
|
208
|
+
adopted_legacy_install: true,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function adoptCodex() {
|
|
213
|
+
let marketplace;
|
|
214
|
+
try {
|
|
215
|
+
marketplace = readJson(codexMarketplacePath);
|
|
216
|
+
} catch {
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
const pluginTarget = path.join(codexPluginRoot, "agent-wallet");
|
|
220
|
+
const registered = Array.isArray(marketplace?.plugins) && marketplace.plugins.some(
|
|
221
|
+
(item) => item?.name === "agent-wallet" && item?.source?.source === "local",
|
|
222
|
+
);
|
|
223
|
+
if (!registered || !symlinkManifestMatches(pluginTarget, ".codex-plugin/plugin.json", "agent-wallet")) {
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
return registry.record("codex", {
|
|
227
|
+
codex_home: codexHome,
|
|
228
|
+
plugin_target: pluginTarget,
|
|
229
|
+
marketplace_path: codexMarketplacePath,
|
|
230
|
+
marketplace_name: String(marketplace.name || "local"),
|
|
231
|
+
adopted_legacy_install: true,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function adoptClaudeCode() {
|
|
236
|
+
let manifest;
|
|
237
|
+
try {
|
|
238
|
+
manifest = readJson(path.join(claudeMarketplaceDir, ".claude-plugin", "marketplace.json"));
|
|
239
|
+
} catch {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
const pluginTarget = path.join(claudeMarketplaceDir, "plugins", "agent-wallet");
|
|
243
|
+
const registered = manifest?.name === claudeMarketplaceName &&
|
|
244
|
+
Array.isArray(manifest.plugins) && manifest.plugins.some((item) => item?.name === "agent-wallet");
|
|
245
|
+
if (!registered || !symlinkManifestMatches(pluginTarget, ".claude-plugin/plugin.json", "agent-wallet")) {
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
return registry.record("claude-code", {
|
|
249
|
+
marketplace_dir: claudeMarketplaceDir,
|
|
250
|
+
plugin_target: pluginTarget,
|
|
251
|
+
cache_root: path.resolve(expandHome(env.AGENT_WALLET_CLAUDE_CODE_CACHE_ROOT || "~/.claude/plugins/cache")),
|
|
252
|
+
adopted_legacy_install: true,
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function runHostRefresh(command, args, commandEnv = env) {
|
|
257
|
+
const binary = commandPath(command);
|
|
258
|
+
if (!binary) {
|
|
259
|
+
return { attempted: false, ok: false, error: `${command} CLI not found`, fix: args.join(" ") };
|
|
260
|
+
}
|
|
261
|
+
const result = spawnSync(binary, args, { cwd: packageRoot, encoding: "utf8", env: commandEnv });
|
|
262
|
+
return {
|
|
263
|
+
attempted: true,
|
|
264
|
+
ok: result.status === 0,
|
|
265
|
+
error: result.status === 0 ? "" : (result.stderr || result.stdout || "").trim(),
|
|
266
|
+
fix: result.status === 0 ? "" : `${command} ${args.join(" ")}`,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function refreshOpenclaw() {
|
|
271
|
+
const entry = registry.managed("openclaw");
|
|
272
|
+
if (!entry) {
|
|
273
|
+
return { name: "openclaw", attempted: false, ok: true, repaired: false, reason: "not managed" };
|
|
274
|
+
}
|
|
275
|
+
const configPath = path.resolve(expandHome(entry.config_path || path.join(openclawHome, "openclaw.json")));
|
|
276
|
+
let config;
|
|
277
|
+
try {
|
|
278
|
+
config = readJson(configPath);
|
|
279
|
+
} catch (error) {
|
|
280
|
+
return { name: "openclaw", attempted: true, ok: false, repaired: false, error: error.message };
|
|
281
|
+
}
|
|
282
|
+
if (!config || typeof config !== "object") {
|
|
283
|
+
return { name: "openclaw", attempted: true, ok: false, repaired: false, error: `missing ${configPath}` };
|
|
284
|
+
}
|
|
285
|
+
const extensionPath = path.join(currentRuntimePath, ".openclaw", "extensions", "agent-wallet");
|
|
286
|
+
const walletPackageRoot = path.join(currentRuntimePath, "agent-wallet");
|
|
287
|
+
const pythonBin = resolveVenvPython(currentRuntimePath);
|
|
288
|
+
const plugins = config.plugins && typeof config.plugins === "object" ? config.plugins : (config.plugins = {});
|
|
289
|
+
const load = plugins.load && typeof plugins.load === "object" ? plugins.load : (plugins.load = {});
|
|
290
|
+
const paths = Array.isArray(load.paths) ? load.paths : [];
|
|
291
|
+
load.paths = [
|
|
292
|
+
...paths.filter((item) => !String(item || "").replaceAll("\\", "/").endsWith("/.openclaw/extensions/agent-wallet")),
|
|
293
|
+
extensionPath,
|
|
294
|
+
];
|
|
295
|
+
const entries = plugins.entries && typeof plugins.entries === "object" ? plugins.entries : (plugins.entries = {});
|
|
296
|
+
const walletEntry = entries["agent-wallet"];
|
|
297
|
+
if (!walletEntry || typeof walletEntry !== "object") {
|
|
298
|
+
return { name: "openclaw", attempted: false, ok: true, repaired: false, reason: "plugin entry not configured" };
|
|
299
|
+
}
|
|
300
|
+
walletEntry.enabled = true;
|
|
301
|
+
walletEntry.config = walletEntry.config && typeof walletEntry.config === "object" ? walletEntry.config : {};
|
|
302
|
+
walletEntry.config.packageRoot = walletPackageRoot;
|
|
303
|
+
if (pythonBin) walletEntry.config.pythonBin = pythonBin;
|
|
304
|
+
writeJsonAtomic(configPath, config);
|
|
305
|
+
registry.record("openclaw", {
|
|
306
|
+
config_path: configPath,
|
|
307
|
+
extension_path: extensionPath,
|
|
308
|
+
package_root: walletPackageRoot,
|
|
309
|
+
restart_required: true,
|
|
310
|
+
});
|
|
311
|
+
return { name: "openclaw", attempted: true, ok: true, repaired: true, config_path: configPath, restart_required: true };
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function refreshHermes() {
|
|
315
|
+
const entry = registry.managed("hermes") || adoptHermes();
|
|
316
|
+
if (!entry) return null;
|
|
317
|
+
const target = path.resolve(expandHome(entry.plugin_target));
|
|
318
|
+
const envPath = path.resolve(expandHome(entry.env_path));
|
|
319
|
+
const result = repairRuntimeSymlink(
|
|
320
|
+
"hermes",
|
|
321
|
+
target,
|
|
322
|
+
path.join(currentRuntimePath, "hermes", "plugins", "agent_wallet"),
|
|
323
|
+
env,
|
|
324
|
+
{ allowExternal: true },
|
|
325
|
+
);
|
|
326
|
+
result.env_repaired = repairHermesEnv(envPath, env);
|
|
327
|
+
result.restart_required = true;
|
|
328
|
+
if (result.ok) {
|
|
329
|
+
registry.record("hermes", { ...entry, plugin_target: target, env_path: envPath, restart_required: true });
|
|
330
|
+
}
|
|
331
|
+
return result;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function refreshCodex() {
|
|
335
|
+
const entry = registry.managed("codex") || adoptCodex();
|
|
336
|
+
if (!entry) return null;
|
|
337
|
+
const pluginTarget = path.resolve(expandHome(entry.plugin_target || path.join(codexPluginRoot, "agent-wallet")));
|
|
338
|
+
const marketplacePath = path.resolve(expandHome(entry.marketplace_path || codexMarketplacePath));
|
|
339
|
+
const link = repairRuntimeSymlink(
|
|
340
|
+
"codex",
|
|
341
|
+
pluginTarget,
|
|
342
|
+
path.join(currentRuntimePath, "codex", "plugins", "agent-wallet"),
|
|
343
|
+
env,
|
|
344
|
+
{ allowExternal: true },
|
|
345
|
+
);
|
|
346
|
+
if (!link.ok) return link;
|
|
347
|
+
const marketplace = ensureCodexMarketplaceEntry({ marketplacePath, pluginName: "agent-wallet" });
|
|
348
|
+
const registration = runHostRefresh(
|
|
349
|
+
"codex",
|
|
350
|
+
["plugin", "add", `agent-wallet@${marketplace.marketplace_name}`],
|
|
351
|
+
{ ...env, CODEX_HOME: entry.codex_home || codexHome },
|
|
352
|
+
);
|
|
353
|
+
registry.record("codex", {
|
|
354
|
+
...entry,
|
|
355
|
+
plugin_target: pluginTarget,
|
|
356
|
+
marketplace_path: marketplacePath,
|
|
357
|
+
marketplace_name: marketplace.marketplace_name,
|
|
358
|
+
registration_ok: registration.ok,
|
|
359
|
+
restart_required: true,
|
|
360
|
+
});
|
|
361
|
+
return { ...link, ok: link.ok && registration.ok, registration, restart_required: true };
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function refreshClaudeCode() {
|
|
365
|
+
const entry = registry.managed("claude-code") || adoptClaudeCode();
|
|
366
|
+
if (!entry) return null;
|
|
367
|
+
const marketplaceDir = path.resolve(expandHome(entry.marketplace_dir || claudeMarketplaceDir));
|
|
368
|
+
const cacheRoot = path.resolve(
|
|
369
|
+
expandHome(entry.cache_root || env.AGENT_WALLET_CLAUDE_CODE_CACHE_ROOT || "~/.claude/plugins/cache"),
|
|
370
|
+
);
|
|
371
|
+
const pluginTarget = path.resolve(expandHome(entry.plugin_target || path.join(marketplaceDir, "plugins", "agent-wallet")));
|
|
372
|
+
const link = repairRuntimeSymlink(
|
|
373
|
+
"claude-code",
|
|
374
|
+
pluginTarget,
|
|
375
|
+
path.join(currentRuntimePath, "claude-code", "plugins", "agent-wallet"),
|
|
376
|
+
env,
|
|
377
|
+
{ allowExternal: true },
|
|
378
|
+
);
|
|
379
|
+
if (!link.ok) return link;
|
|
380
|
+
ensureClaudeCodeMarketplace(
|
|
381
|
+
marketplaceDir,
|
|
382
|
+
path.join(currentRuntimePath, "claude-code", "plugins", "agent-wallet"),
|
|
383
|
+
true,
|
|
384
|
+
);
|
|
385
|
+
const marketplaceAdd = runHostRefresh(
|
|
386
|
+
"claude",
|
|
387
|
+
["plugin", "marketplace", "add", marketplaceDir, "--scope", "user"],
|
|
388
|
+
);
|
|
389
|
+
const registration = marketplaceAdd.ok
|
|
390
|
+
? runHostRefresh(
|
|
391
|
+
"claude",
|
|
392
|
+
["plugin", "install", `agent-wallet@${claudeMarketplaceName}`, "--scope", "user"],
|
|
393
|
+
)
|
|
394
|
+
: { attempted: false, ok: false, error: "marketplace refresh failed", fix: marketplaceAdd.fix };
|
|
395
|
+
const cachePins = pinClaudeCacheCopies({ ...env, AGENT_WALLET_CLAUDE_CODE_CACHE_ROOT: cacheRoot });
|
|
396
|
+
registry.record("claude-code", {
|
|
397
|
+
...entry,
|
|
398
|
+
marketplace_dir: marketplaceDir,
|
|
399
|
+
plugin_target: pluginTarget,
|
|
400
|
+
cache_root: cacheRoot,
|
|
401
|
+
registration_ok: registration.ok,
|
|
402
|
+
restart_required: true,
|
|
403
|
+
});
|
|
404
|
+
return {
|
|
405
|
+
...link,
|
|
406
|
+
ok: link.ok && marketplaceAdd.ok && registration.ok,
|
|
407
|
+
marketplace_add: marketplaceAdd,
|
|
408
|
+
registration,
|
|
409
|
+
cache_pins: cachePins,
|
|
410
|
+
restart_required: true,
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function refreshAll() {
|
|
415
|
+
return registry.refreshAll({
|
|
416
|
+
openclaw: refreshOpenclaw,
|
|
417
|
+
hermes: refreshHermes,
|
|
418
|
+
codex: refreshCodex,
|
|
419
|
+
"claude-code": refreshClaudeCode,
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
return { refreshAll };
|
|
424
|
+
}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
function readJson(pathname) {
|
|
7
|
+
try {
|
|
8
|
+
return JSON.parse(fs.readFileSync(pathname, "utf8"));
|
|
9
|
+
} catch (error) {
|
|
10
|
+
if (error?.code === "ENOENT") return null;
|
|
11
|
+
throw error;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function writeJsonAtomic(pathname, value) {
|
|
16
|
+
fs.mkdirSync(path.dirname(pathname), { recursive: true });
|
|
17
|
+
const temporary = `${pathname}.tmp-${process.pid}-${Date.now()}`;
|
|
18
|
+
try {
|
|
19
|
+
fs.writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
|
|
20
|
+
fs.renameSync(temporary, pathname);
|
|
21
|
+
fs.chmodSync(pathname, 0o600);
|
|
22
|
+
} finally {
|
|
23
|
+
fs.rmSync(temporary, { force: true });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function uniquePath(target) {
|
|
28
|
+
if (!fs.existsSync(target)) return target;
|
|
29
|
+
let counter = 2;
|
|
30
|
+
while (fs.existsSync(`${target}-${counter}`)) counter += 1;
|
|
31
|
+
return `${target}-${counter}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function readLink(target) {
|
|
35
|
+
try {
|
|
36
|
+
return fs.lstatSync(target).isSymbolicLink() ? fs.readlinkSync(target) : null;
|
|
37
|
+
} catch (error) {
|
|
38
|
+
if (error?.code === "ENOENT") return null;
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function processIsRunning(pid) {
|
|
44
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
45
|
+
try {
|
|
46
|
+
process.kill(pid, 0);
|
|
47
|
+
return true;
|
|
48
|
+
} catch (error) {
|
|
49
|
+
return error?.code !== "ESRCH";
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function createUpdateTransactionManager({ runtimeBase, packageVersion, env = process.env }) {
|
|
54
|
+
const base = path.resolve(runtimeBase);
|
|
55
|
+
const journalPath = path.join(base, "update-journal.json");
|
|
56
|
+
const lockPath = path.join(base, "update.lock");
|
|
57
|
+
|
|
58
|
+
function readJournal() {
|
|
59
|
+
try {
|
|
60
|
+
return readJson(journalPath);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
return { schema_version: 1, state: "corrupt", error: error.message };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function writeJournal(state, details = {}) {
|
|
67
|
+
writeJsonAtomic(journalPath, {
|
|
68
|
+
...details,
|
|
69
|
+
schema_version: 2,
|
|
70
|
+
state,
|
|
71
|
+
version: details.version || packageVersion,
|
|
72
|
+
updated_at: new Date().toISOString(),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function runtimeOwned(candidate) {
|
|
77
|
+
if (!candidate) return false;
|
|
78
|
+
const relative = path.relative(base, path.resolve(candidate));
|
|
79
|
+
return relative !== "" && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function interruptedPath(version) {
|
|
83
|
+
return uniquePath(
|
|
84
|
+
path.join(base, "releases", `.interrupted-${version || "unknown"}-${Date.now()}`),
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function acquireLock(allowStaleRetry = true) {
|
|
89
|
+
const ownerPath = path.join(lockPath, "owner.json");
|
|
90
|
+
const token = crypto.randomUUID();
|
|
91
|
+
fs.mkdirSync(base, { recursive: true });
|
|
92
|
+
try {
|
|
93
|
+
fs.mkdirSync(lockPath);
|
|
94
|
+
} catch (error) {
|
|
95
|
+
if (error?.code !== "EEXIST") throw error;
|
|
96
|
+
let owner = null;
|
|
97
|
+
try {
|
|
98
|
+
owner = readJson(ownerPath);
|
|
99
|
+
} catch {
|
|
100
|
+
// An unreadable lock is not safe to steal automatically.
|
|
101
|
+
}
|
|
102
|
+
const stale = owner?.hostname === os.hostname() && !processIsRunning(Number(owner.pid));
|
|
103
|
+
if (stale && allowStaleRetry) {
|
|
104
|
+
fs.rmSync(lockPath, { recursive: true, force: true });
|
|
105
|
+
return acquireLock(false);
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
ok: false,
|
|
109
|
+
path: lockPath,
|
|
110
|
+
owner: owner ? { pid: owner.pid, hostname: owner.hostname, started_at: owner.started_at } : null,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
writeJsonAtomic(ownerPath, {
|
|
115
|
+
schema_version: 1,
|
|
116
|
+
pid: process.pid,
|
|
117
|
+
hostname: os.hostname(),
|
|
118
|
+
token,
|
|
119
|
+
started_at: new Date().toISOString(),
|
|
120
|
+
});
|
|
121
|
+
} catch (error) {
|
|
122
|
+
fs.rmSync(lockPath, { recursive: true, force: true });
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
return { ok: true, path: lockPath, owner_path: ownerPath, token };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function releaseLock(lock) {
|
|
129
|
+
if (!lock?.ok) return;
|
|
130
|
+
try {
|
|
131
|
+
const owner = readJson(lock.owner_path);
|
|
132
|
+
if (owner?.token === lock.token) fs.rmSync(lock.path, { recursive: true, force: true });
|
|
133
|
+
} catch {
|
|
134
|
+
// Never remove a lock whose ownership can no longer be verified.
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function recover() {
|
|
139
|
+
const journal = readJournal();
|
|
140
|
+
if (!journal || ["committed", "failed", "recovered"].includes(journal.state)) {
|
|
141
|
+
return { attempted: false, ok: true, reason: "no interrupted update" };
|
|
142
|
+
}
|
|
143
|
+
if (journal.state === "corrupt") {
|
|
144
|
+
return { attempted: false, ok: false, reason: "update journal is corrupt", error: journal.error };
|
|
145
|
+
}
|
|
146
|
+
const paths = [journal.staging_root, journal.release_root, journal.replaced_root].filter(Boolean);
|
|
147
|
+
if (paths.some((candidate) => !runtimeOwned(candidate))) {
|
|
148
|
+
return { attempted: false, ok: false, reason: "update journal contains an unsafe path" };
|
|
149
|
+
}
|
|
150
|
+
if (journal.state === "committing") {
|
|
151
|
+
const releaseExists = Boolean(journal.release_root && fs.existsSync(journal.release_root));
|
|
152
|
+
const replacedExists = Boolean(journal.replaced_root && fs.existsSync(journal.replaced_root));
|
|
153
|
+
if (!releaseExists && replacedExists) {
|
|
154
|
+
fs.renameSync(journal.replaced_root, journal.release_root);
|
|
155
|
+
writeJournal("recovered", { ...journal, action: "restored_replaced_release" });
|
|
156
|
+
return { attempted: true, ok: true, action: "restored_replaced_release" };
|
|
157
|
+
}
|
|
158
|
+
if (releaseExists) {
|
|
159
|
+
writeJournal("recovered", { ...journal, action: "release_already_present" });
|
|
160
|
+
return { attempted: true, ok: true, action: "release_already_present" };
|
|
161
|
+
}
|
|
162
|
+
return { attempted: true, ok: false, reason: "interrupted commit has no recoverable release" };
|
|
163
|
+
}
|
|
164
|
+
if (["preparing", "verified"].includes(journal.state)) {
|
|
165
|
+
let quarantined = null;
|
|
166
|
+
if (journal.staging_root && fs.existsSync(journal.staging_root)) {
|
|
167
|
+
quarantined = interruptedPath(journal.version);
|
|
168
|
+
fs.renameSync(journal.staging_root, quarantined);
|
|
169
|
+
}
|
|
170
|
+
writeJournal("recovered", {
|
|
171
|
+
...journal,
|
|
172
|
+
action: "quarantined_incomplete_staging",
|
|
173
|
+
quarantined_runtime: quarantined,
|
|
174
|
+
});
|
|
175
|
+
return {
|
|
176
|
+
attempted: true,
|
|
177
|
+
ok: true,
|
|
178
|
+
action: "quarantined_incomplete_staging",
|
|
179
|
+
quarantined_runtime: quarantined,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
return { attempted: false, ok: false, reason: `unsupported update journal state: ${journal.state}` };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function status() {
|
|
186
|
+
const journal = readJournal();
|
|
187
|
+
const state = journal?.state || null;
|
|
188
|
+
const currentPath = path.join(base, "current");
|
|
189
|
+
const currentTarget = readLink(currentPath);
|
|
190
|
+
const currentResolves = Boolean(
|
|
191
|
+
currentTarget && fs.existsSync(path.resolve(path.dirname(currentPath), currentTarget)),
|
|
192
|
+
);
|
|
193
|
+
let lockOwner = null;
|
|
194
|
+
try {
|
|
195
|
+
const owner = readJson(path.join(lockPath, "owner.json"));
|
|
196
|
+
if (owner) {
|
|
197
|
+
lockOwner = {
|
|
198
|
+
pid: owner.pid,
|
|
199
|
+
hostname: owner.hostname,
|
|
200
|
+
started_at: owner.started_at,
|
|
201
|
+
active: owner.hostname === os.hostname() ? processIsRunning(Number(owner.pid)) : null,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
} catch {
|
|
205
|
+
if (fs.existsSync(lockPath)) lockOwner = { unreadable: true };
|
|
206
|
+
}
|
|
207
|
+
return {
|
|
208
|
+
journal_schema_version: journal?.schema_version || null,
|
|
209
|
+
state,
|
|
210
|
+
journal_ok: state !== "corrupt",
|
|
211
|
+
needs_recovery: Boolean(state && !["committed", "failed", "recovered"].includes(state)),
|
|
212
|
+
current_resolves: currentTarget ? currentResolves : null,
|
|
213
|
+
lock: lockOwner,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function holdLockForTest() {
|
|
218
|
+
const milliseconds = Number.parseInt(String(env.AGENT_WALLET_TEST_HOLD_LOCK_MS || ""), 10);
|
|
219
|
+
if (Number.isFinite(milliseconds) && milliseconds > 0) {
|
|
220
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, milliseconds);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
journalPath,
|
|
226
|
+
lockPath,
|
|
227
|
+
readJournal,
|
|
228
|
+
writeJournal,
|
|
229
|
+
acquireLock,
|
|
230
|
+
releaseLock,
|
|
231
|
+
recover,
|
|
232
|
+
status,
|
|
233
|
+
holdLockForTest,
|
|
234
|
+
};
|
|
235
|
+
}
|