@agentlayer.tech/wallet 0.1.75 → 0.1.77

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 (36) hide show
  1. package/.openclaw/extensions/agent-wallet/README.md +4 -4
  2. package/.openclaw/extensions/agent-wallet/dist/index.js +57 -19
  3. package/.openclaw/extensions/agent-wallet/index.ts +57 -19
  4. package/.openclaw/extensions/agent-wallet/openclaw.plugin.json +1 -1
  5. package/.openclaw/extensions/agent-wallet/package.json +1 -1
  6. package/CHANGELOG.md +19 -0
  7. package/VERSION +1 -1
  8. package/agent-wallet/AGENTS.md +1 -0
  9. package/agent-wallet/UPGRADE_COMPATIBILITY.md +36 -0
  10. package/agent-wallet/agent_wallet/__init__.py +1 -1
  11. package/agent-wallet/agent_wallet/autonomous_permissions.py +2 -2
  12. package/agent-wallet/agent_wallet/autonomous_policy.py +1 -1
  13. package/agent-wallet/agent_wallet/boot_key_recovery.py +10 -1
  14. package/agent-wallet/agent_wallet/config.py +3 -3
  15. package/agent-wallet/agent_wallet/openclaw_adapter.py +24 -24
  16. package/agent-wallet/agent_wallet/providers/evm_portfolio.py +2 -2
  17. package/agent-wallet/agent_wallet/wallet_layer/wdk_evm.py +2 -2
  18. package/agent-wallet/openclaw.plugin.json +1 -1
  19. package/agent-wallet/pyproject.toml +1 -1
  20. package/agent-wallet/skills/wallet-operator/SKILL.md +4 -1
  21. package/bin/lib/boot-key.mjs +185 -0
  22. package/bin/lib/integrations.mjs +424 -0
  23. package/bin/lib/update-transaction.mjs +235 -0
  24. package/bin/openclaw-agent-wallet.mjs +225 -520
  25. package/claude-code/plugins/agent-wallet/.claude-plugin/plugin.json +1 -1
  26. package/codex/plugins/agent-wallet/.codex-plugin/plugin.json +1 -1
  27. package/codex/plugins/agent-wallet/server.py +13 -10
  28. package/codex/plugins/agent-wallet/skills/wallet-operator/SKILL.md +1 -1
  29. package/hermes/plugins/agent_wallet/plugin.yaml +1 -1
  30. package/package.json +2 -1
  31. package/wdk-btc-wallet/package.json +1 -1
  32. package/wdk-evm-wallet/.env.example +3 -2
  33. package/wdk-evm-wallet/package.json +5 -2
  34. package/wdk-evm-wallet/src/config.js +16 -2
  35. package/wdk-evm-wallet/src/network_state.js +5 -2
  36. package/wdk-evm-wallet/src/wdk_evm_wallet.js +10 -4
@@ -6,6 +6,9 @@ import fs from "node:fs";
6
6
  import os from "node:os";
7
7
  import path from "node:path";
8
8
  import { fileURLToPath } from "node:url";
9
+ import { createBootKeyManager } from "./lib/boot-key.mjs";
10
+ import { createHostIntegrationManager, createIntegrationManager } from "./lib/integrations.mjs";
11
+ import { createUpdateTransactionManager } from "./lib/update-transaction.mjs";
9
12
 
10
13
  const cliPath = fileURLToPath(import.meta.url);
11
14
  const packageRoot = path.resolve(path.dirname(cliPath), "..");
@@ -388,74 +391,85 @@ function stagingRootFor(version, env = process.env) {
388
391
  );
389
392
  }
390
393
 
391
- function updateJournalPath(env = process.env) {
392
- return path.join(resolveRuntimeBase(env), "update-journal.json");
394
+ function updateTransactions(env = process.env) {
395
+ return createUpdateTransactionManager({
396
+ runtimeBase: resolveRuntimeBase(env),
397
+ packageVersion,
398
+ env,
399
+ });
393
400
  }
394
401
 
395
- function integrationRegistryPath(env = process.env) {
396
- return path.join(resolveRuntimeBase(env), "integrations.json");
402
+ function acquireUpdateLock(env = process.env) {
403
+ return updateTransactions(env).acquireLock();
397
404
  }
398
405
 
399
- function readIntegrationRegistry(env = process.env) {
400
- const payload = readJsonFile(integrationRegistryPath(env));
401
- if (!payload || payload.schema_version !== 1 || typeof payload.integrations !== "object") {
402
- return { schema_version: 1, integrations: {} };
403
- }
404
- return payload;
406
+ function releaseUpdateLock(lock) {
407
+ return createUpdateTransactionManager({
408
+ runtimeBase: path.dirname(lock.path),
409
+ packageVersion,
410
+ }).releaseLock(lock);
405
411
  }
406
412
 
407
- function recordManagedIntegration(name, details = {}, env = process.env) {
408
- const registry = readIntegrationRegistry(env);
409
- registry.integrations[name] = {
410
- ...details,
411
- managed: true,
412
- installed_version: packageVersion,
413
- updated_at: new Date().toISOString(),
414
- };
415
- registry.updated_at = new Date().toISOString();
416
- writeJsonFileAtomic(integrationRegistryPath(env), registry);
417
- return registry.integrations[name];
413
+ function holdUpdateLockForTest(env = process.env) {
414
+ updateTransactions(env).holdLockForTest();
418
415
  }
419
416
 
420
- function managedIntegration(name, env = process.env) {
421
- const entry = readIntegrationRegistry(env).integrations[name];
422
- return entry && entry.managed === true ? entry : null;
417
+ function readUpdateJournal(env = process.env) {
418
+ return updateTransactions(env).readJournal();
419
+ }
420
+
421
+ function integrations(env = process.env) {
422
+ return createIntegrationManager({
423
+ runtimeBase: resolveRuntimeBase(env),
424
+ packageVersion,
425
+ activeVersion: () => activeVersion(env),
426
+ });
427
+ }
428
+
429
+ function recordManagedIntegration(name, details = {}, env = process.env) {
430
+ return integrations(env).record(name, details);
423
431
  }
424
432
 
425
433
  function integrationSyncStatus(env = process.env) {
426
- const active = activeVersion(env);
427
- const registry = readIntegrationRegistry(env);
428
- const integrations = Object.entries(registry.integrations)
429
- .filter(([, entry]) => entry?.managed === true)
430
- .sort(([left], [right]) => left.localeCompare(right))
431
- .map(([name, entry]) => {
432
- const versionInSync = active === null || entry.installed_version === active;
433
- const registrationOk = entry.registration_ok !== false;
434
- return {
435
- name,
436
- installed_version: entry.installed_version || null,
437
- active_version: active,
438
- in_sync: versionInSync && registrationOk,
439
- registration_ok: registrationOk,
440
- restart_required: Boolean(entry.restart_required),
441
- };
442
- });
443
- return {
444
- in_sync: integrations.every((entry) => entry.in_sync),
445
- integrations,
446
- };
434
+ return integrations(env).status();
447
435
  }
448
436
 
449
- function writeUpdateJournal(state, details = {}, env = process.env) {
450
- writeJsonFile(updateJournalPath(env), {
451
- schema_version: 1,
452
- state,
453
- version: packageVersion,
454
- updated_at: new Date().toISOString(),
455
- ...details,
437
+ function hostIntegrations(env = process.env) {
438
+ return createHostIntegrationManager({
439
+ env,
440
+ packageRoot,
441
+ registry: integrations(env),
442
+ currentRuntimePath: currentRuntimePath(env),
443
+ openclawHome: resolveOpenclawHome(env),
444
+ hermesHome: resolveHermesHome(env),
445
+ codexHome: resolveCodexHome(env),
446
+ codexPluginRoot: resolveCodexPluginInstallRoot(env),
447
+ codexMarketplacePath: resolveCodexMarketplacePath(env),
448
+ claudeMarketplaceDir: resolveClaudeCodeMarketplaceDir(env),
449
+ claudeMarketplaceName: CLAUDE_CODE_MARKETPLACE_NAME,
450
+ expandHome,
451
+ resolveVenvPython,
452
+ commandPath,
453
+ repairRuntimeSymlink,
454
+ repairHermesEnv,
455
+ ensureCodexMarketplaceEntry,
456
+ ensureClaudeCodeMarketplace,
457
+ pinClaudeCacheCopies,
456
458
  });
457
459
  }
458
460
 
461
+ function writeUpdateJournal(state, details = {}, env = process.env) {
462
+ updateTransactions(env).writeJournal(state, details);
463
+ }
464
+
465
+ function recoverInterruptedUpdate(env = process.env) {
466
+ return updateTransactions(env).recover();
467
+ }
468
+
469
+ function updateRecoveryStatus(env = process.env) {
470
+ return updateTransactions(env).status();
471
+ }
472
+
459
473
  function writeReleaseState(runtimeRoot, state, details = {}) {
460
474
  writeJsonFile(path.join(runtimeRoot, ".agent-wallet-release.json"), {
461
475
  schema_version: 1,
@@ -477,14 +491,14 @@ function failStagingRuntime(stagingRoot, error) {
477
491
  return failedRoot;
478
492
  }
479
493
 
480
- function commitStagedRuntime(stagingRoot, releaseRoot) {
494
+ function commitStagedRuntime(stagingRoot, releaseRoot, replacedRoot = null, env = process.env) {
481
495
  if (!stagingRoot) return null;
482
- let replacedRoot = null;
483
496
  if (fs.existsSync(releaseRoot)) {
484
- replacedRoot = uniquePathWithSuffix(
497
+ replacedRoot ||= uniquePathWithSuffix(
485
498
  path.join(path.dirname(releaseRoot), `${path.basename(releaseRoot)}-replaced`),
486
499
  );
487
500
  fs.renameSync(releaseRoot, replacedRoot);
501
+ if (env.AGENT_WALLET_TEST_EXIT_AFTER_RELEASE_RENAME === "1") process.exit(86);
488
502
  }
489
503
  try {
490
504
  fs.renameSync(stagingRoot, releaseRoot);
@@ -1014,146 +1028,36 @@ function writeJsonFileAtomic(pathname, value, mode = 0o600) {
1014
1028
  }
1015
1029
  }
1016
1030
 
1017
- function currentBootKey(env = process.env) {
1018
- const currentRoot = resolvedCurrentRuntimeRoot(env);
1019
- if (!currentRoot) return "";
1020
- return readEnvFile(path.join(currentRoot, "agent-wallet", ".env")).AGENT_WALLET_BOOT_KEY || "";
1021
- }
1022
-
1023
- function readTextIfExists(pathname) {
1024
- try {
1025
- return fs.readFileSync(pathname, "utf8");
1026
- } catch (error) {
1027
- if (error?.code === "ENOENT") return "";
1028
- throw error;
1029
- }
1030
- }
1031
-
1032
- function writeSecretFile(pathname, value) {
1033
- fs.mkdirSync(path.dirname(pathname), { recursive: true });
1034
- fs.writeFileSync(pathname, `${String(value || "").trim()}\n`, { mode: 0o600 });
1035
- try {
1036
- fs.chmodSync(pathname, 0o600);
1037
- } catch {
1038
- // ignored
1039
- }
1040
- }
1041
-
1042
- function resolveBootKeyFromFile(env = process.env) {
1043
- const keyFile = String(env.AGENT_WALLET_BOOT_KEY_FILE || "").trim();
1044
- if (!keyFile) return "";
1045
- return readTextIfExists(path.resolve(expandHome(keyFile))).trim();
1046
- }
1047
-
1048
- function defaultBootKeyFile(env = process.env) {
1049
- return path.join(resolveRuntimeBase(env), "boot-key");
1031
+ function bootKeys(env = process.env) {
1032
+ return createBootKeyManager({
1033
+ runtimeBase: resolveRuntimeBase(env),
1034
+ openclawHome: resolveOpenclawHome(env),
1035
+ currentRuntimeRoot: () => resolvedCurrentRuntimeRoot(env),
1036
+ resolveVenvPython,
1037
+ expandHome,
1038
+ bridgeTimeoutMs: positiveIntEnv(
1039
+ "AGENT_WALLET_KEYSTORE_BRIDGE_TIMEOUT_MS",
1040
+ KEYSTORE_BRIDGE_TIMEOUT_MS,
1041
+ env,
1042
+ ),
1043
+ env,
1044
+ });
1050
1045
  }
1051
1046
 
1052
1047
  function ensureBootKeyFile(env = process.env) {
1053
- const configuredFile = String(env.AGENT_WALLET_BOOT_KEY_FILE || "").trim();
1054
- const keyFile = configuredFile ? path.resolve(expandHome(configuredFile)) : defaultBootKeyFile(env);
1055
- const existing = readTextIfExists(keyFile).trim();
1056
- if (existing) {
1057
- return { path: keyFile, status: "existing" };
1058
- }
1059
- const bootKey = String(env.AGENT_WALLET_BOOT_KEY || "").trim() || resolveBootKeyFromFile(env) || currentBootKey(env);
1060
- if (!bootKey) {
1061
- return { path: keyFile, status: "missing" };
1062
- }
1063
- writeSecretFile(keyFile, bootKey);
1064
- return { path: keyFile, status: "created" };
1065
- }
1066
-
1067
- // Read the boot key from the OS keystore via the current runtime's Python
1068
- // (best-effort, "" on any failure). Lets a re-install after the runtime migration
1069
- // — which moves the key into the keystore and deletes every plaintext copy — still
1070
- // resolve the existing boot key instead of refusing to touch sealed secrets.
1071
- function readBootKeyFromKeystore(env = process.env) {
1072
- const runtimeRoot = resolvedCurrentRuntimeRoot(env);
1073
- if (!runtimeRoot) return "";
1074
- const py = resolveVenvPython(runtimeRoot);
1075
- if (!py) return "";
1076
- try {
1077
- const res = spawnSync(
1078
- py,
1079
- ["-c", "from agent_wallet.config import read_boot_key_from_keystore as r; print(r())"],
1080
- {
1081
- cwd: path.join(runtimeRoot, "agent-wallet"),
1082
- encoding: "utf8",
1083
- timeout: positiveIntEnv("AGENT_WALLET_KEYSTORE_BRIDGE_TIMEOUT_MS", KEYSTORE_BRIDGE_TIMEOUT_MS, env),
1084
- env: { ...env, OPENCLAW_HOME: resolveOpenclawHome(env) },
1085
- },
1086
- );
1087
- if ((res.status ?? 1) !== 0) return "";
1088
- return String(res.stdout || "").trim();
1089
- } catch {
1090
- return "";
1091
- }
1048
+ return bootKeys(env).ensureFile();
1092
1049
  }
1093
1050
 
1094
- // New runtimes own boot-key precedence and verify candidates against
1095
- // sealed_keys.json. An import failure means the active runtime predates this
1096
- // bridge, so callers may use the legacy JS fallback below.
1097
1051
  function readBootKeyFromRuntimeResolver(env = process.env) {
1098
- const runtimeRoot = resolvedCurrentRuntimeRoot(env);
1099
- if (!runtimeRoot) return { supported: false, key: "" };
1100
- const py = resolveVenvPython(runtimeRoot);
1101
- if (!py) return { supported: false, key: "" };
1102
- try {
1103
- const res = spawnSync(
1104
- py,
1105
- ["-c", "from agent_wallet.config import resolve_boot_key_for_installer as r; print(r())"],
1106
- {
1107
- cwd: path.join(runtimeRoot, "agent-wallet"),
1108
- encoding: "utf8",
1109
- timeout: positiveIntEnv("AGENT_WALLET_KEYSTORE_BRIDGE_TIMEOUT_MS", KEYSTORE_BRIDGE_TIMEOUT_MS, env),
1110
- env: { ...env, OPENCLAW_HOME: resolveOpenclawHome(env) },
1111
- },
1112
- );
1113
- if ((res.status ?? 1) !== 0) return { supported: false, key: "" };
1114
- return { supported: true, key: String(res.stdout || "").trim() };
1115
- } catch {
1116
- return { supported: false, key: "" };
1117
- }
1052
+ return bootKeys(env).resolveFromRuntime();
1118
1053
  }
1119
1054
 
1120
1055
  function resolveLegacyInstallerBootKey(env = process.env) {
1121
- for (const [source, key] of [
1122
- ["legacy_keystore", readBootKeyFromKeystore(env)],
1123
- ["current_runtime_env", currentBootKey(env)],
1124
- ["configured_file", resolveBootKeyFromFile(env)],
1125
- ["default_file", readTextIfExists(defaultBootKeyFile(env)).trim()],
1126
- ]) {
1127
- if (key) return { key, source };
1128
- }
1129
- return { key: "", source: "none" };
1056
+ return bootKeys(env).resolveLegacy();
1130
1057
  }
1131
1058
 
1132
- // Provision the boot key into the OS keystore via the freshly installed runtime's
1133
- // Python. Returns true only when the import stored AND verified the key, so the
1134
- // caller may safely drop the plaintext .env copy. Best-effort: false on any failure
1135
- // (e.g. no usable keystore), in which case the caller keeps the legacy .env write.
1136
1059
  function provisionBootKeyToKeystore(releaseRoot, env, bootKey) {
1137
- const key = String(bootKey || "").trim();
1138
- if (!key) return false;
1139
- const py = resolveVenvPython(releaseRoot);
1140
- if (!py) return false;
1141
- try {
1142
- const res = spawnSync(
1143
- py,
1144
- ["-m", "agent_wallet.openclaw_cli", "boot-key-import", "--key-stdin"],
1145
- {
1146
- cwd: path.join(releaseRoot, "agent-wallet"),
1147
- input: key,
1148
- encoding: "utf8",
1149
- timeout: positiveIntEnv("AGENT_WALLET_KEYSTORE_BRIDGE_TIMEOUT_MS", KEYSTORE_BRIDGE_TIMEOUT_MS, env),
1150
- env: { ...env, OPENCLAW_HOME: resolveOpenclawHome(env) },
1151
- },
1152
- );
1153
- return (res.status ?? 1) === 0;
1154
- } catch {
1155
- return false;
1156
- }
1060
+ return bootKeys(env).provision(releaseRoot, bootKey);
1157
1061
  }
1158
1062
 
1159
1063
  function resolveVenvPython(releaseRoot) {
@@ -1231,6 +1135,10 @@ function verifyRuntime(releaseRoot, env = process.env) {
1231
1135
  };
1232
1136
  }
1233
1137
 
1138
+ function verifyBootKeyWithRuntime(releaseRoot, env = process.env) {
1139
+ return bootKeys(env).verifyWithRuntime(releaseRoot);
1140
+ }
1141
+
1234
1142
  function resolveEditorServerChecks(env = process.env) {
1235
1143
  const checks = [];
1236
1144
  // Claude Code's launcher (run_mcp.sh) falls back to the runtime codex
@@ -1381,10 +1289,20 @@ function runDoctor(args = []) {
1381
1289
  fix: integrationSync.in_sync ? "" : "wallet update --yes",
1382
1290
  });
1383
1291
 
1292
+ const recoveryStatus = updateRecoveryStatus(env);
1293
+ checks.push({
1294
+ name: "update_recovery_state",
1295
+ ok: true,
1296
+ ...recoveryStatus,
1297
+ error: "",
1298
+ fix: recoveryStatus.needs_recovery ? "wallet install --yes" : "",
1299
+ });
1300
+
1384
1301
  const ok = checks.every((c) => c.ok);
1385
1302
  console.log(
1386
1303
  JSON.stringify(
1387
1304
  {
1305
+ schema_version: 1,
1388
1306
  ok,
1389
1307
  package_name: packageJson.name,
1390
1308
  package_version: packageVersion,
@@ -1404,6 +1322,7 @@ function runDoctor(args = []) {
1404
1322
 
1405
1323
  function runStatus(args = []) {
1406
1324
  const payload = {
1325
+ schema_version: 1,
1407
1326
  ok: true,
1408
1327
  package_name: packageJson.name,
1409
1328
  package_version: packageVersion,
@@ -1417,6 +1336,7 @@ function runStatus(args = []) {
1417
1336
  update_available: computeUpdateAvailability(),
1418
1337
  runtime_in_sync: computeRuntimeInSync(),
1419
1338
  framework_integrations: integrationSyncStatus(),
1339
+ update_recovery: updateRecoveryStatus(),
1420
1340
  };
1421
1341
  if (hasFlag(args, "--verbose")) {
1422
1342
  payload.verbose = true;
@@ -1434,19 +1354,18 @@ function buildInstallerEnv(args) {
1434
1354
  const sealedKeysExist = fs.existsSync(sealedKeysPath);
1435
1355
  const dryRun = hasFlag(args, "--dry-run");
1436
1356
  let bootKeySource = env.AGENT_WALLET_BOOT_KEY ? "environment" : "none";
1437
- if (!env.AGENT_WALLET_BOOT_KEY) {
1438
- const runtimeResolution = readBootKeyFromRuntimeResolver(env);
1439
- const fallback = runtimeResolution.supported
1440
- ? {
1441
- key: runtimeResolution.key,
1442
- source: runtimeResolution.key ? "runtime_verified" : "runtime_rejected",
1443
- }
1444
- : resolveLegacyInstallerBootKey(env);
1445
- const existingBootKey = fallback.key;
1446
- bootKeySource = fallback.source;
1447
- if (existingBootKey) {
1448
- env.AGENT_WALLET_BOOT_KEY = existingBootKey;
1357
+ const runtimeResolution = readBootKeyFromRuntimeResolver(env);
1358
+ if (runtimeResolution.supported) {
1359
+ bootKeySource = runtimeResolution.key ? "runtime_verified" : "runtime_rejected";
1360
+ if (runtimeResolution.key) {
1361
+ env.AGENT_WALLET_BOOT_KEY = runtimeResolution.key;
1362
+ } else {
1363
+ delete env.AGENT_WALLET_BOOT_KEY;
1449
1364
  }
1365
+ } else if (!env.AGENT_WALLET_BOOT_KEY) {
1366
+ const fallback = resolveLegacyInstallerBootKey(env);
1367
+ bootKeySource = fallback.source;
1368
+ if (fallback.key) env.AGENT_WALLET_BOOT_KEY = fallback.key;
1450
1369
  }
1451
1370
 
1452
1371
  const shouldGenerateSecrets =
@@ -1476,7 +1395,7 @@ function buildInstallerEnv(args) {
1476
1395
  return { env, generated, bootKeySource };
1477
1396
  }
1478
1397
 
1479
- function runInstall(args, { commandName = "install" } = {}) {
1398
+ function runInstallUnlocked(args, { commandName = "install" } = {}) {
1480
1399
  if (!fs.existsSync(setupPath)) {
1481
1400
  console.error(`Missing bundled setup.sh at ${setupPath}`);
1482
1401
  return 1;
@@ -1490,6 +1409,13 @@ function runInstall(args, { commandName = "install" } = {}) {
1490
1409
  const previousPath = previousRuntimePath();
1491
1410
  const installerArgs = withoutCliOnlyArgs(args);
1492
1411
  const dryRun = hasFlag(args, "--dry-run");
1412
+ const recovery = dryRun
1413
+ ? { attempted: false, ok: true, reason: "dry run" }
1414
+ : recoverInterruptedUpdate(process.env);
1415
+ if (!recovery.ok) {
1416
+ console.error(`Could not recover interrupted update: ${recovery.reason || recovery.error}`);
1417
+ return 1;
1418
+ }
1493
1419
  const stagingRoot = !explicitRuntimeRoot && !dryRun ? stagingRootFor(packageVersion) : null;
1494
1420
  const installRoot = stagingRoot || releaseRoot;
1495
1421
 
@@ -1510,7 +1436,17 @@ function runInstall(args, { commandName = "install" } = {}) {
1510
1436
  const { env, generated, bootKeySource } = installerEnv;
1511
1437
  if (stagingRoot) {
1512
1438
  env.OPENCLAW_INSTALL_FINAL_ROOT = releaseRoot;
1513
- writeUpdateJournal("preparing", { staging_root: stagingRoot, release_root: releaseRoot }, env);
1439
+ writeUpdateJournal(
1440
+ "preparing",
1441
+ {
1442
+ transaction_id: crypto.randomUUID(),
1443
+ staging_root: stagingRoot,
1444
+ release_root: releaseRoot,
1445
+ current_target_before: readLinkOrNull(currentPath),
1446
+ previous_target_before: readLinkOrNull(previousPath),
1447
+ },
1448
+ env,
1449
+ );
1514
1450
  }
1515
1451
  const result = spawnSync("sh", [setupPath, ...installerArgs], {
1516
1452
  cwd: packageRoot,
@@ -1604,6 +1540,34 @@ function runInstall(args, { commandName = "install" } = {}) {
1604
1540
  return 1;
1605
1541
  }
1606
1542
 
1543
+ const bootKeyVerification = verifyBootKeyWithRuntime(installRoot, env);
1544
+ if (!bootKeyVerification.ok) {
1545
+ const failedRoot = failStagingRuntime(stagingRoot, bootKeyVerification.error);
1546
+ writeUpdateJournal(
1547
+ "failed",
1548
+ { failed_runtime: failedRoot, error: bootKeyVerification.error },
1549
+ env,
1550
+ );
1551
+ console.error(
1552
+ JSON.stringify(
1553
+ {
1554
+ ok: false,
1555
+ command: commandName,
1556
+ version: packageVersion,
1557
+ category: "boot_key_rejected",
1558
+ error: bootKeyVerification.error,
1559
+ switched_current: false,
1560
+ failed_runtime: failedRoot,
1561
+ current_runtime_target: readLinkOrNull(currentPath),
1562
+ fix: "Remove stale AGENT_WALLET_BOOT_KEY overrides and retry the update.",
1563
+ },
1564
+ null,
1565
+ 2,
1566
+ ),
1567
+ );
1568
+ return 1;
1569
+ }
1570
+
1607
1571
  if (env.AGENT_WALLET_BOOT_KEY) {
1608
1572
  const envPath = path.join(installRoot, "agent-wallet", ".env");
1609
1573
  // Prefer the OS keystore: provision the boot key there and keep the plaintext
@@ -1621,11 +1585,32 @@ function runInstall(args, { commandName = "install" } = {}) {
1621
1585
  writeReleaseState(installRoot, "verified", {
1622
1586
  verification_skipped: Boolean(verification.skipped),
1623
1587
  });
1624
- writeUpdateJournal("verified", { staging_root: stagingRoot, release_root: releaseRoot }, env);
1588
+ writeUpdateJournal(
1589
+ "verified",
1590
+ { ...readUpdateJournal(env), staging_root: stagingRoot, release_root: releaseRoot },
1591
+ env,
1592
+ );
1625
1593
 
1626
1594
  let replacedRoot = null;
1627
1595
  try {
1628
- replacedRoot = commitStagedRuntime(stagingRoot, releaseRoot);
1596
+ replacedRoot = fs.existsSync(releaseRoot)
1597
+ ? uniquePathWithSuffix(
1598
+ path.join(path.dirname(releaseRoot), `${path.basename(releaseRoot)}-replaced`),
1599
+ )
1600
+ : null;
1601
+ writeUpdateJournal(
1602
+ "committing",
1603
+ {
1604
+ ...readUpdateJournal(env),
1605
+ staging_root: stagingRoot,
1606
+ release_root: releaseRoot,
1607
+ replaced_root: replacedRoot,
1608
+ current_target_before: readLinkOrNull(currentPath),
1609
+ previous_target_before: readLinkOrNull(previousPath),
1610
+ },
1611
+ env,
1612
+ );
1613
+ replacedRoot = commitStagedRuntime(stagingRoot, releaseRoot, replacedRoot, env);
1629
1614
  } catch (error) {
1630
1615
  const failedRoot = failStagingRuntime(stagingRoot, error.message);
1631
1616
  writeUpdateJournal("failed", { failed_runtime: failedRoot, error: error.message }, env);
@@ -1641,7 +1626,11 @@ function runInstall(args, { commandName = "install" } = {}) {
1641
1626
  switchSymlink(previousPath, previousTarget);
1642
1627
  }
1643
1628
  switchSymlink(currentPath, releaseRoot);
1644
- writeUpdateJournal("committed", { release_root: releaseRoot, previous_runtime: previousTarget }, env);
1629
+ writeUpdateJournal(
1630
+ "committed",
1631
+ { ...readUpdateJournal(env), release_root: releaseRoot, previous_runtime: previousTarget },
1632
+ env,
1633
+ );
1645
1634
 
1646
1635
  recordManagedIntegration(
1647
1636
  "openclaw",
@@ -1656,7 +1645,10 @@ function runInstall(args, { commandName = "install" } = {}) {
1656
1645
  );
1657
1646
 
1658
1647
  const integrationRefresh = repairInstalledEditorIntegrations(env);
1659
- const globalCliRefresh = refreshGlobalCliIfNeeded(env);
1648
+ const globalCliRefresh = safelyRefreshIntegration(
1649
+ "global-cli",
1650
+ () => refreshGlobalCliIfNeeded(env),
1651
+ );
1660
1652
 
1661
1653
  const pythonInfo = activePythonRuntimeInfo(env);
1662
1654
  const nodeInfo = activeNodeRuntimeInfo(env)
@@ -1679,6 +1671,7 @@ function runInstall(args, { commandName = "install" } = {}) {
1679
1671
  boot_key_source: bootKeySource,
1680
1672
  staged: Boolean(stagingRoot),
1681
1673
  release_state: "verified",
1674
+ recovery,
1682
1675
  integration_refresh: integrationRefresh,
1683
1676
  global_cli_refresh: globalCliRefresh,
1684
1677
  },
@@ -1689,6 +1682,34 @@ function runInstall(args, { commandName = "install" } = {}) {
1689
1682
  return 0;
1690
1683
  }
1691
1684
 
1685
+ function runInstall(args, options = {}) {
1686
+ if (hasFlag(args, "--dry-run")) return runInstallUnlocked(args, options);
1687
+ const lock = acquireUpdateLock(process.env);
1688
+ if (!lock.ok) {
1689
+ console.error(
1690
+ JSON.stringify(
1691
+ {
1692
+ ok: false,
1693
+ category: "update_locked",
1694
+ error: "Another wallet install or update is already running.",
1695
+ lock_path: lock.path,
1696
+ lock_owner: lock.owner,
1697
+ fix: "Wait for the active update to finish, then retry.",
1698
+ },
1699
+ null,
1700
+ 2,
1701
+ ),
1702
+ );
1703
+ return 1;
1704
+ }
1705
+ try {
1706
+ holdUpdateLockForTest(process.env);
1707
+ return runInstallUnlocked(args, options);
1708
+ } finally {
1709
+ releaseUpdateLock(lock);
1710
+ }
1711
+ }
1712
+
1692
1713
  function resolveUpdatePackageSpec(env = process.env) {
1693
1714
  const explicit = String(env[UPDATE_PACKAGE_SPEC_ENV] || "").trim();
1694
1715
  if (explicit) return explicit;
@@ -2500,189 +2521,6 @@ function repairHermesEnv(envPath, env = process.env) {
2500
2521
  return repaired;
2501
2522
  }
2502
2523
 
2503
- function legacyHermesIntegration(env = process.env) {
2504
- const hermesHome = resolveHermesHome(env);
2505
- const pluginTarget = path.join(hermesHome, "plugins", "agent_wallet");
2506
- let target;
2507
- try {
2508
- if (!fs.lstatSync(pluginTarget).isSymbolicLink()) return null;
2509
- target = path.resolve(path.dirname(pluginTarget), fs.readlinkSync(pluginTarget));
2510
- } catch {
2511
- return null;
2512
- }
2513
- const manifest = readTextIfExists(path.join(target, "plugin.yaml"));
2514
- if (!/^name:\s*agent[-_]wallet\s*$/m.test(manifest)) return null;
2515
- return recordManagedIntegration(
2516
- "hermes",
2517
- {
2518
- hermes_home: hermesHome,
2519
- plugin_target: pluginTarget,
2520
- env_path: path.join(hermesHome, ".env"),
2521
- adopted_legacy_install: true,
2522
- },
2523
- env,
2524
- );
2525
- }
2526
-
2527
- function repairOpenclawIntegration(env = process.env) {
2528
- const entry = managedIntegration("openclaw", env);
2529
- if (!entry) {
2530
- return { name: "openclaw", attempted: false, ok: true, repaired: false, reason: "not managed" };
2531
- }
2532
- const configPath = path.resolve(
2533
- expandHome(entry.config_path || path.join(resolveOpenclawHome(env), "openclaw.json")),
2534
- );
2535
- let config;
2536
- try {
2537
- config = readJsonFile(configPath);
2538
- } catch (error) {
2539
- return { name: "openclaw", attempted: true, ok: false, repaired: false, error: error.message };
2540
- }
2541
- if (!config || typeof config !== "object") {
2542
- return { name: "openclaw", attempted: true, ok: false, repaired: false, error: `missing ${configPath}` };
2543
- }
2544
-
2545
- const currentRoot = currentRuntimePath(env);
2546
- const extensionPath = path.join(currentRoot, ".openclaw", "extensions", "agent-wallet");
2547
- const packageRootPath = path.join(currentRoot, "agent-wallet");
2548
- const pythonBin = resolveVenvPython(currentRoot);
2549
- const plugins = config.plugins && typeof config.plugins === "object" ? config.plugins : (config.plugins = {});
2550
- const load = plugins.load && typeof plugins.load === "object" ? plugins.load : (plugins.load = {});
2551
- const paths = Array.isArray(load.paths) ? load.paths : [];
2552
- load.paths = [
2553
- ...paths.filter((item) => {
2554
- const value = String(item || "");
2555
- return !value.replaceAll("\\", "/").endsWith("/.openclaw/extensions/agent-wallet");
2556
- }),
2557
- extensionPath,
2558
- ];
2559
- const entries = plugins.entries && typeof plugins.entries === "object" ? plugins.entries : (plugins.entries = {});
2560
- const walletEntry = entries["agent-wallet"];
2561
- if (!walletEntry || typeof walletEntry !== "object") {
2562
- return {
2563
- name: "openclaw",
2564
- attempted: false,
2565
- ok: true,
2566
- repaired: false,
2567
- reason: "plugin entry not configured",
2568
- };
2569
- }
2570
- walletEntry.enabled = true;
2571
- walletEntry.config = walletEntry.config && typeof walletEntry.config === "object" ? walletEntry.config : {};
2572
- walletEntry.config.packageRoot = packageRootPath;
2573
- if (pythonBin) walletEntry.config.pythonBin = pythonBin;
2574
- writeJsonFileAtomic(configPath, config);
2575
- recordManagedIntegration(
2576
- "openclaw",
2577
- {
2578
- config_path: configPath,
2579
- extension_path: extensionPath,
2580
- package_root: packageRootPath,
2581
- restart_required: true,
2582
- },
2583
- env,
2584
- );
2585
- return {
2586
- name: "openclaw",
2587
- attempted: true,
2588
- ok: true,
2589
- repaired: true,
2590
- config_path: configPath,
2591
- restart_required: true,
2592
- };
2593
- }
2594
-
2595
- function symlinkManifestMatches(linkPath, manifestRelativePath, expectedName) {
2596
- let target;
2597
- try {
2598
- if (!fs.lstatSync(linkPath).isSymbolicLink()) return false;
2599
- target = path.resolve(path.dirname(linkPath), fs.readlinkSync(linkPath));
2600
- } catch {
2601
- return false;
2602
- }
2603
- try {
2604
- const manifest = readJsonFile(path.join(target, manifestRelativePath));
2605
- return manifest && manifest.name === expectedName;
2606
- } catch {
2607
- return false;
2608
- }
2609
- }
2610
-
2611
- function legacyCodexIntegration(env = process.env) {
2612
- const marketplacePath = resolveCodexMarketplacePath(env);
2613
- let marketplace;
2614
- try {
2615
- marketplace = readJsonFile(marketplacePath);
2616
- } catch {
2617
- return null;
2618
- }
2619
- const pluginTarget = path.join(resolveCodexPluginInstallRoot(env), "agent-wallet");
2620
- const registered = Array.isArray(marketplace?.plugins) && marketplace.plugins.some(
2621
- (item) => item?.name === "agent-wallet" && item?.source?.source === "local",
2622
- );
2623
- if (!registered || !symlinkManifestMatches(pluginTarget, ".codex-plugin/plugin.json", "agent-wallet")) {
2624
- return null;
2625
- }
2626
- return recordManagedIntegration(
2627
- "codex",
2628
- {
2629
- codex_home: resolveCodexHome(env),
2630
- plugin_target: pluginTarget,
2631
- marketplace_path: marketplacePath,
2632
- marketplace_name: String(marketplace.name || "local"),
2633
- adopted_legacy_install: true,
2634
- },
2635
- env,
2636
- );
2637
- }
2638
-
2639
- function legacyClaudeCodeIntegration(env = process.env) {
2640
- const marketplaceDir = resolveClaudeCodeMarketplaceDir(env);
2641
- let manifest;
2642
- try {
2643
- manifest = readJsonFile(path.join(marketplaceDir, ".claude-plugin", "marketplace.json"));
2644
- } catch {
2645
- return null;
2646
- }
2647
- const pluginTarget = path.join(marketplaceDir, "plugins", "agent-wallet");
2648
- const registered = manifest?.name === CLAUDE_CODE_MARKETPLACE_NAME &&
2649
- Array.isArray(manifest.plugins) && manifest.plugins.some((item) => item?.name === "agent-wallet");
2650
- if (!registered || !symlinkManifestMatches(pluginTarget, ".claude-plugin/plugin.json", "agent-wallet")) {
2651
- return null;
2652
- }
2653
- return recordManagedIntegration(
2654
- "claude-code",
2655
- {
2656
- marketplace_dir: marketplaceDir,
2657
- plugin_target: pluginTarget,
2658
- cache_root: path.resolve(
2659
- expandHome(env.AGENT_WALLET_CLAUDE_CODE_CACHE_ROOT || "~/.claude/plugins/cache"),
2660
- ),
2661
- adopted_legacy_install: true,
2662
- },
2663
- env,
2664
- );
2665
- }
2666
-
2667
- function runHostRefresh(command, args, env = process.env) {
2668
- const binary = commandPath(command);
2669
- if (!binary) {
2670
- return {
2671
- attempted: false,
2672
- ok: false,
2673
- error: `${command} CLI not found`,
2674
- fix: args.join(" "),
2675
- };
2676
- }
2677
- const result = spawnSync(binary, args, { cwd: packageRoot, encoding: "utf8", env });
2678
- return {
2679
- attempted: true,
2680
- ok: result.status === 0,
2681
- error: result.status === 0 ? "" : (result.stderr || result.stdout || "").trim(),
2682
- fix: result.status === 0 ? "" : `${command} ${args.join(" ")}`,
2683
- };
2684
- }
2685
-
2686
2524
  function globalNpmPackageInfo(env = process.env) {
2687
2525
  const npmBin = commandPath("npm");
2688
2526
  if (!npmBin) return null;
@@ -2734,145 +2572,12 @@ function refreshGlobalCliIfNeeded(env = process.env) {
2734
2572
  };
2735
2573
  }
2736
2574
 
2737
- function refreshCodexIntegration(entry, env = process.env) {
2738
- const pluginTarget = path.resolve(
2739
- expandHome(entry.plugin_target || path.join(resolveCodexPluginInstallRoot(env), "agent-wallet")),
2740
- );
2741
- const marketplacePath = path.resolve(
2742
- expandHome(entry.marketplace_path || resolveCodexMarketplacePath(env)),
2743
- );
2744
- const link = repairRuntimeSymlink(
2745
- "codex",
2746
- pluginTarget,
2747
- path.join(currentRuntimePath(env), "codex", "plugins", "agent-wallet"),
2748
- env,
2749
- { allowExternal: true },
2750
- );
2751
- if (!link.ok) return link;
2752
- const marketplace = ensureCodexMarketplaceEntry({ marketplacePath, pluginName: "agent-wallet" });
2753
- const registration = runHostRefresh(
2754
- "codex",
2755
- ["plugin", "add", `agent-wallet@${marketplace.marketplace_name}`],
2756
- { ...env, CODEX_HOME: entry.codex_home || resolveCodexHome(env) },
2757
- );
2758
- recordManagedIntegration(
2759
- "codex",
2760
- {
2761
- ...entry,
2762
- plugin_target: pluginTarget,
2763
- marketplace_path: marketplacePath,
2764
- marketplace_name: marketplace.marketplace_name,
2765
- registration_ok: registration.ok,
2766
- restart_required: true,
2767
- },
2768
- env,
2769
- );
2770
- return {
2771
- ...link,
2772
- ok: link.ok && registration.ok,
2773
- registration,
2774
- restart_required: true,
2775
- };
2776
- }
2777
-
2778
- function refreshClaudeCodeIntegration(entry, env = process.env) {
2779
- const marketplaceDir = path.resolve(
2780
- expandHome(entry.marketplace_dir || resolveClaudeCodeMarketplaceDir(env)),
2781
- );
2782
- const cacheRoot = path.resolve(
2783
- expandHome(entry.cache_root || env.AGENT_WALLET_CLAUDE_CODE_CACHE_ROOT || "~/.claude/plugins/cache"),
2784
- );
2785
- const pluginTarget = path.resolve(
2786
- expandHome(entry.plugin_target || path.join(marketplaceDir, "plugins", "agent-wallet")),
2787
- );
2788
- const link = repairRuntimeSymlink(
2789
- "claude-code",
2790
- pluginTarget,
2791
- path.join(currentRuntimePath(env), "claude-code", "plugins", "agent-wallet"),
2792
- env,
2793
- { allowExternal: true },
2794
- );
2795
- if (!link.ok) return link;
2796
- ensureClaudeCodeMarketplace(
2797
- marketplaceDir,
2798
- path.join(currentRuntimePath(env), "claude-code", "plugins", "agent-wallet"),
2799
- true,
2800
- );
2801
- const marketplaceAdd = runHostRefresh(
2802
- "claude",
2803
- ["plugin", "marketplace", "add", marketplaceDir, "--scope", "user"],
2804
- env,
2805
- );
2806
- const registration = marketplaceAdd.ok
2807
- ? runHostRefresh(
2808
- "claude",
2809
- ["plugin", "install", `agent-wallet@${CLAUDE_CODE_MARKETPLACE_NAME}`, "--scope", "user"],
2810
- env,
2811
- )
2812
- : { attempted: false, ok: false, error: "marketplace refresh failed", fix: marketplaceAdd.fix };
2813
- const cachePins = pinClaudeCacheCopies({
2814
- ...env,
2815
- AGENT_WALLET_CLAUDE_CODE_CACHE_ROOT: cacheRoot,
2816
- });
2817
- recordManagedIntegration(
2818
- "claude-code",
2819
- {
2820
- ...entry,
2821
- marketplace_dir: marketplaceDir,
2822
- plugin_target: pluginTarget,
2823
- cache_root: cacheRoot,
2824
- registration_ok: registration.ok,
2825
- restart_required: true,
2826
- },
2827
- env,
2828
- );
2829
- return {
2830
- ...link,
2831
- ok: link.ok && marketplaceAdd.ok && registration.ok,
2832
- marketplace_add: marketplaceAdd,
2833
- registration,
2834
- cache_pins: cachePins,
2835
- restart_required: true,
2836
- };
2575
+ function safelyRefreshIntegration(name, callback) {
2576
+ return integrations().safelyRefresh(name, callback);
2837
2577
  }
2838
2578
 
2839
2579
  function repairInstalledEditorIntegrations(env = process.env) {
2840
- const results = [repairOpenclawIntegration(env)];
2841
- const currentRoot = currentRuntimePath(env);
2842
- const hermesEntry = managedIntegration("hermes", env) || legacyHermesIntegration(env);
2843
- if (hermesEntry) {
2844
- const hermesTarget = path.resolve(expandHome(hermesEntry.plugin_target));
2845
- const hermesEnvPath = path.resolve(expandHome(hermesEntry.env_path));
2846
- const result = repairRuntimeSymlink(
2847
- "hermes",
2848
- hermesTarget,
2849
- path.join(currentRoot, "hermes", "plugins", "agent_wallet"),
2850
- env,
2851
- { allowExternal: true },
2852
- );
2853
- result.env_repaired = repairHermesEnv(hermesEnvPath, env);
2854
- result.restart_required = true;
2855
- if (result.ok) {
2856
- recordManagedIntegration(
2857
- "hermes",
2858
- {
2859
- ...hermesEntry,
2860
- plugin_target: hermesTarget,
2861
- env_path: hermesEnvPath,
2862
- restart_required: true,
2863
- },
2864
- env,
2865
- );
2866
- }
2867
- results.push(result);
2868
- }
2869
-
2870
- const codexEntry = managedIntegration("codex", env) || legacyCodexIntegration(env);
2871
- if (codexEntry) results.push(refreshCodexIntegration(codexEntry, env));
2872
-
2873
- const claudeEntry = managedIntegration("claude-code", env) || legacyClaudeCodeIntegration(env);
2874
- if (claudeEntry) results.push(refreshClaudeCodeIntegration(claudeEntry, env));
2875
- return results;
2580
+ return hostIntegrations(env).refreshAll();
2876
2581
  }
2877
2582
 
2878
2583
  const args = process.argv.slice(2);