@agentlayer.tech/wallet 0.1.72 → 0.1.74

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.
@@ -380,6 +380,69 @@ function releaseRootFor(version, env = process.env) {
380
380
  return path.join(resolveRuntimeBase(env), "releases", version);
381
381
  }
382
382
 
383
+ function stagingRootFor(version, env = process.env) {
384
+ return path.join(
385
+ resolveRuntimeBase(env),
386
+ "releases",
387
+ `.staging-${version}-${process.pid}-${Date.now()}`,
388
+ );
389
+ }
390
+
391
+ function updateJournalPath(env = process.env) {
392
+ return path.join(resolveRuntimeBase(env), "update-journal.json");
393
+ }
394
+
395
+ function writeUpdateJournal(state, details = {}, env = process.env) {
396
+ writeJsonFile(updateJournalPath(env), {
397
+ schema_version: 1,
398
+ state,
399
+ version: packageVersion,
400
+ updated_at: new Date().toISOString(),
401
+ ...details,
402
+ });
403
+ }
404
+
405
+ function writeReleaseState(runtimeRoot, state, details = {}) {
406
+ writeJsonFile(path.join(runtimeRoot, ".agent-wallet-release.json"), {
407
+ schema_version: 1,
408
+ version: packageVersion,
409
+ state,
410
+ updated_at: new Date().toISOString(),
411
+ ...details,
412
+ });
413
+ }
414
+
415
+ function failStagingRuntime(stagingRoot, error) {
416
+ if (!stagingRoot || !fs.existsSync(stagingRoot)) return null;
417
+ writeReleaseState(stagingRoot, "failed", { error: String(error || "install failed") });
418
+ const failedRoot = path.join(
419
+ path.dirname(stagingRoot),
420
+ `.failed-${packageVersion}-${Date.now()}`,
421
+ );
422
+ fs.renameSync(stagingRoot, failedRoot);
423
+ return failedRoot;
424
+ }
425
+
426
+ function commitStagedRuntime(stagingRoot, releaseRoot) {
427
+ if (!stagingRoot) return null;
428
+ let replacedRoot = null;
429
+ if (fs.existsSync(releaseRoot)) {
430
+ replacedRoot = uniquePathWithSuffix(
431
+ path.join(path.dirname(releaseRoot), `${path.basename(releaseRoot)}-replaced`),
432
+ );
433
+ fs.renameSync(releaseRoot, replacedRoot);
434
+ }
435
+ try {
436
+ fs.renameSync(stagingRoot, releaseRoot);
437
+ } catch (error) {
438
+ if (replacedRoot && !fs.existsSync(releaseRoot)) {
439
+ fs.renameSync(replacedRoot, releaseRoot);
440
+ }
441
+ throw error;
442
+ }
443
+ return replacedRoot;
444
+ }
445
+
383
446
  function currentRuntimePath(env = process.env) {
384
447
  return path.join(resolveRuntimeBase(env), "current");
385
448
  }
@@ -507,7 +570,21 @@ function listReleases(env = process.env) {
507
570
  try {
508
571
  return fs
509
572
  .readdirSync(releasesDir, { withFileTypes: true })
510
- .filter((entry) => entry.isDirectory())
573
+ .filter((entry) => entry.isDirectory() && !entry.name.startsWith("."))
574
+ .map((entry) => entry.name)
575
+ .sort();
576
+ } catch (error) {
577
+ if (error?.code === "ENOENT") return [];
578
+ throw error;
579
+ }
580
+ }
581
+
582
+ function listFailedReleases(env = process.env) {
583
+ const releasesDir = path.join(resolveRuntimeBase(env), "releases");
584
+ try {
585
+ return fs
586
+ .readdirSync(releasesDir, { withFileTypes: true })
587
+ .filter((entry) => entry.isDirectory() && entry.name.startsWith(".failed-"))
511
588
  .map((entry) => entry.name)
512
589
  .sort();
513
590
  } catch (error) {
@@ -944,6 +1021,44 @@ function readBootKeyFromKeystore(env = process.env) {
944
1021
  }
945
1022
  }
946
1023
 
1024
+ // New runtimes own boot-key precedence and verify candidates against
1025
+ // sealed_keys.json. An import failure means the active runtime predates this
1026
+ // bridge, so callers may use the legacy JS fallback below.
1027
+ function readBootKeyFromRuntimeResolver(env = process.env) {
1028
+ const runtimeRoot = resolvedCurrentRuntimeRoot(env);
1029
+ if (!runtimeRoot) return { supported: false, key: "" };
1030
+ const py = resolveVenvPython(runtimeRoot);
1031
+ if (!py) return { supported: false, key: "" };
1032
+ try {
1033
+ const res = spawnSync(
1034
+ py,
1035
+ ["-c", "from agent_wallet.config import resolve_boot_key_for_installer as r; print(r())"],
1036
+ {
1037
+ cwd: path.join(runtimeRoot, "agent-wallet"),
1038
+ encoding: "utf8",
1039
+ timeout: positiveIntEnv("AGENT_WALLET_KEYSTORE_BRIDGE_TIMEOUT_MS", KEYSTORE_BRIDGE_TIMEOUT_MS, env),
1040
+ env: { ...env, OPENCLAW_HOME: resolveOpenclawHome(env) },
1041
+ },
1042
+ );
1043
+ if ((res.status ?? 1) !== 0) return { supported: false, key: "" };
1044
+ return { supported: true, key: String(res.stdout || "").trim() };
1045
+ } catch {
1046
+ return { supported: false, key: "" };
1047
+ }
1048
+ }
1049
+
1050
+ function resolveLegacyInstallerBootKey(env = process.env) {
1051
+ for (const [source, key] of [
1052
+ ["legacy_keystore", readBootKeyFromKeystore(env)],
1053
+ ["current_runtime_env", currentBootKey(env)],
1054
+ ["configured_file", resolveBootKeyFromFile(env)],
1055
+ ["default_file", readTextIfExists(defaultBootKeyFile(env)).trim()],
1056
+ ]) {
1057
+ if (key) return { key, source };
1058
+ }
1059
+ return { key: "", source: "none" };
1060
+ }
1061
+
947
1062
  // Provision the boot key into the OS keystore via the freshly installed runtime's
948
1063
  // Python. Returns true only when the import stored AND verified the key, so the
949
1064
  // caller may safely drop the plaintext .env copy. Best-effort: false on any failure
@@ -1218,6 +1333,7 @@ function runStatus(args = []) {
1218
1333
  previous_runtime: readLinkOrNull(previousRuntimePath()),
1219
1334
  active_version: activeVersion(),
1220
1335
  available_releases: listReleases(),
1336
+ failed_releases: listFailedReleases(),
1221
1337
  update_available: computeUpdateAvailability(),
1222
1338
  runtime_in_sync: computeRuntimeInSync(),
1223
1339
  };
@@ -1236,12 +1352,17 @@ function buildInstallerEnv(args) {
1236
1352
  const sealedKeysPath = path.join(resolveOpenclawHome(env), "sealed_keys.json");
1237
1353
  const sealedKeysExist = fs.existsSync(sealedKeysPath);
1238
1354
  const dryRun = hasFlag(args, "--dry-run");
1355
+ let bootKeySource = env.AGENT_WALLET_BOOT_KEY ? "environment" : "none";
1239
1356
  if (!env.AGENT_WALLET_BOOT_KEY) {
1240
- const existingBootKey =
1241
- resolveBootKeyFromFile(env) ||
1242
- readTextIfExists(defaultBootKeyFile(env)).trim() ||
1243
- currentBootKey(env) ||
1244
- readBootKeyFromKeystore(env);
1357
+ const runtimeResolution = readBootKeyFromRuntimeResolver(env);
1358
+ const fallback = runtimeResolution.supported
1359
+ ? {
1360
+ key: runtimeResolution.key,
1361
+ source: runtimeResolution.key ? "runtime_verified" : "runtime_rejected",
1362
+ }
1363
+ : resolveLegacyInstallerBootKey(env);
1364
+ const existingBootKey = fallback.key;
1365
+ bootKeySource = fallback.source;
1245
1366
  if (existingBootKey) {
1246
1367
  env.AGENT_WALLET_BOOT_KEY = existingBootKey;
1247
1368
  }
@@ -1261,6 +1382,7 @@ function buildInstallerEnv(args) {
1261
1382
  if (!sealedKeysExist && shouldGenerateSecrets && !env.AGENT_WALLET_BOOT_KEY) {
1262
1383
  generated.AGENT_WALLET_BOOT_KEY = token();
1263
1384
  env.AGENT_WALLET_BOOT_KEY = generated.AGENT_WALLET_BOOT_KEY;
1385
+ bootKeySource = "generated";
1264
1386
  }
1265
1387
  if (!sealedKeysExist && shouldGenerateSecrets && !env.AGENT_WALLET_MASTER_KEY) {
1266
1388
  generated.AGENT_WALLET_MASTER_KEY = token();
@@ -1270,7 +1392,7 @@ function buildInstallerEnv(args) {
1270
1392
  generated.AGENT_WALLET_APPROVAL_SECRET = token();
1271
1393
  env.AGENT_WALLET_APPROVAL_SECRET = generated.AGENT_WALLET_APPROVAL_SECRET;
1272
1394
  }
1273
- return { env, generated };
1395
+ return { env, generated, bootKeySource };
1274
1396
  }
1275
1397
 
1276
1398
  function runInstall(args, { commandName = "install" } = {}) {
@@ -1287,9 +1409,11 @@ function runInstall(args, { commandName = "install" } = {}) {
1287
1409
  const previousPath = previousRuntimePath();
1288
1410
  const installerArgs = withoutCliOnlyArgs(args);
1289
1411
  const dryRun = hasFlag(args, "--dry-run");
1412
+ const stagingRoot = !explicitRuntimeRoot && !dryRun ? stagingRootFor(packageVersion) : null;
1413
+ const installRoot = stagingRoot || releaseRoot;
1290
1414
 
1291
1415
  if (!hasFlag(installerArgs, "--runtime-root")) {
1292
- installerArgs.push("--runtime-root", releaseRoot);
1416
+ installerArgs.push("--runtime-root", installRoot);
1293
1417
  }
1294
1418
  if (!hasFlag(installerArgs, "--install-from-runtime")) {
1295
1419
  installerArgs.push("--install-from-runtime");
@@ -1302,7 +1426,11 @@ function runInstall(args, { commandName = "install" } = {}) {
1302
1426
  console.error(error.message);
1303
1427
  return 1;
1304
1428
  }
1305
- const { env, generated } = installerEnv;
1429
+ const { env, generated, bootKeySource } = installerEnv;
1430
+ if (stagingRoot) {
1431
+ env.OPENCLAW_INSTALL_FINAL_ROOT = releaseRoot;
1432
+ writeUpdateJournal("preparing", { staging_root: stagingRoot, release_root: releaseRoot }, env);
1433
+ }
1306
1434
  const result = spawnSync("sh", [setupPath, ...installerArgs], {
1307
1435
  cwd: packageRoot,
1308
1436
  stdio: "inherit",
@@ -1310,10 +1438,22 @@ function runInstall(args, { commandName = "install" } = {}) {
1310
1438
  });
1311
1439
 
1312
1440
  if (result.error) {
1441
+ const failedRoot = failStagingRuntime(stagingRoot, result.error.message);
1442
+ if (!dryRun) {
1443
+ writeUpdateJournal("failed", { failed_runtime: failedRoot, error: result.error.message }, env);
1444
+ }
1313
1445
  console.error(result.error.message);
1314
1446
  return 1;
1315
1447
  }
1316
1448
  if ((result.status ?? 1) !== 0) {
1449
+ const failedRoot = failStagingRuntime(stagingRoot, `installer exited with ${result.status ?? 1}`);
1450
+ if (!dryRun) {
1451
+ writeUpdateJournal(
1452
+ "failed",
1453
+ { failed_runtime: failedRoot, error: `installer exited with ${result.status ?? 1}` },
1454
+ env,
1455
+ );
1456
+ }
1317
1457
  return result.status ?? 1;
1318
1458
  }
1319
1459
 
@@ -1321,34 +1461,21 @@ function runInstall(args, { commandName = "install" } = {}) {
1321
1461
  return 0;
1322
1462
  }
1323
1463
 
1324
- const currentTarget = existingRuntimePointerTarget(currentPath);
1325
- if (currentTarget) {
1326
- switchSymlink(previousPath, currentTarget);
1327
- }
1328
- switchSymlink(currentPath, releaseRoot);
1329
-
1330
1464
  // Installs that pass --skip-python-setup may have no venv, so this handshake
1331
1465
  // would fail and trigger a spurious rollback; such flows must set
1332
1466
  // AGENT_WALLET_VERIFY_DISABLE=1 (verifyRuntime then skips).
1333
- const verification = verifyRuntime(releaseRoot, env);
1467
+ const currentTarget = existingRuntimePointerTarget(currentPath);
1468
+ const verification = verifyRuntime(installRoot, env);
1334
1469
  if (!verification.ok && !verification.skipped) {
1335
- const rollbackTarget = currentTarget; // pre-switch target captured before the switch, if any
1336
- const rolledBack = Boolean(rollbackTarget);
1337
- if (rolledBack) {
1338
- switchSymlink(currentPath, rollbackTarget);
1339
- }
1470
+ const failedRoot = failStagingRuntime(stagingRoot, verification.error);
1471
+ const rolledBack = Boolean(currentTarget);
1340
1472
  const previousVersion = rolledBack
1341
- ? path.basename(path.resolve(path.dirname(currentPath), rollbackTarget))
1473
+ ? path.basename(currentTarget)
1342
1474
  : null;
1343
1475
 
1344
1476
  let human;
1345
1477
  let fix;
1346
1478
  if (!rolledBack) {
1347
- // First install / no good fallback — leave no active runtime rather than a
1348
- // broken one. Deliberately do NOT point `previous` at the broken release,
1349
- // so a later `rollback` cannot reactivate it; the release stays under
1350
- // releases/<version> for inspection via `install --version`.
1351
- removeRuntimePointer(currentPath);
1352
1479
  human =
1353
1480
  verification.category === "broken_release"
1354
1481
  ? `Release ${packageVersion} is broken and there is no previous working version to fall back to. Nothing is active. This is a bad release — please report it; a patched version will follow.`
@@ -1377,7 +1504,9 @@ function runInstall(args, { commandName = "install" } = {}) {
1377
1504
  category: verification.category || "unknown",
1378
1505
  error: `runtime verification failed: ${verification.error}`,
1379
1506
  rolled_back: rolledBack,
1507
+ switched_current: false,
1380
1508
  kept_version: previousVersion,
1509
+ failed_runtime: failedRoot,
1381
1510
  current_runtime_target: readLinkOrNull(currentPath),
1382
1511
  message: human,
1383
1512
  fix,
@@ -1386,15 +1515,20 @@ function runInstall(args, { commandName = "install" } = {}) {
1386
1515
  2,
1387
1516
  ),
1388
1517
  );
1518
+ writeUpdateJournal(
1519
+ "failed",
1520
+ { failed_runtime: failedRoot, error: verification.error, kept_version: previousVersion },
1521
+ env,
1522
+ );
1389
1523
  return 1;
1390
1524
  }
1391
1525
 
1392
1526
  if (env.AGENT_WALLET_BOOT_KEY) {
1393
- const envPath = path.join(releaseRoot, "agent-wallet", ".env");
1527
+ const envPath = path.join(installRoot, "agent-wallet", ".env");
1394
1528
  // Prefer the OS keystore: provision the boot key there and keep the plaintext
1395
1529
  // out of the release .env entirely. Only fall back to the legacy .env write when
1396
1530
  // no keystore round-trip is possible, so the runtime can still resolve the key.
1397
- if (provisionBootKeyToKeystore(releaseRoot, env, env.AGENT_WALLET_BOOT_KEY)) {
1531
+ if (provisionBootKeyToKeystore(installRoot, env, env.AGENT_WALLET_BOOT_KEY)) {
1398
1532
  envFileUnset(envPath, ["AGENT_WALLET_BOOT_KEY"]);
1399
1533
  } else {
1400
1534
  envFileSet(envPath, {
@@ -1403,7 +1537,32 @@ function runInstall(args, { commandName = "install" } = {}) {
1403
1537
  }
1404
1538
  }
1405
1539
 
1406
- const integrationRefresh = refreshInstalledEditorIntegrations(env);
1540
+ writeReleaseState(installRoot, "verified", {
1541
+ verification_skipped: Boolean(verification.skipped),
1542
+ });
1543
+ writeUpdateJournal("verified", { staging_root: stagingRoot, release_root: releaseRoot }, env);
1544
+
1545
+ let replacedRoot = null;
1546
+ try {
1547
+ replacedRoot = commitStagedRuntime(stagingRoot, releaseRoot);
1548
+ } catch (error) {
1549
+ const failedRoot = failStagingRuntime(stagingRoot, error.message);
1550
+ writeUpdateJournal("failed", { failed_runtime: failedRoot, error: error.message }, env);
1551
+ console.error(`Could not commit staged runtime: ${error.message}`);
1552
+ return 1;
1553
+ }
1554
+
1555
+ const previousTarget =
1556
+ currentTarget && path.resolve(currentTarget) === path.resolve(releaseRoot) && replacedRoot
1557
+ ? replacedRoot
1558
+ : currentTarget;
1559
+ if (previousTarget) {
1560
+ switchSymlink(previousPath, previousTarget);
1561
+ }
1562
+ switchSymlink(currentPath, releaseRoot);
1563
+ writeUpdateJournal("committed", { release_root: releaseRoot, previous_runtime: previousTarget }, env);
1564
+
1565
+ const integrationRefresh = repairInstalledEditorIntegrations(env);
1407
1566
 
1408
1567
  const pythonInfo = activePythonRuntimeInfo(env);
1409
1568
  const nodeInfo = activeNodeRuntimeInfo(env)
@@ -1423,6 +1582,9 @@ function runInstall(args, { commandName = "install" } = {}) {
1423
1582
  current_runtime: currentPath,
1424
1583
  previous_runtime: readLinkOrNull(previousPath),
1425
1584
  generated_runtime_secrets: Object.keys(generated),
1585
+ boot_key_source: bootKeySource,
1586
+ staged: Boolean(stagingRoot),
1587
+ release_state: "verified",
1426
1588
  integration_refresh: integrationRefresh,
1427
1589
  },
1428
1590
  null,
@@ -1864,7 +2026,7 @@ function runHermesInstall(args) {
1864
2026
  function runCodexInstall(args) {
1865
2027
  const codexHome = resolveCodexHome();
1866
2028
  const pluginSource = resolveCodexPluginSource();
1867
- const pinnedEnv = pinEditorMcpEnv(pluginSource);
2029
+ const pinnedEnv = { pinned: false, reason: "plugin source is immutable" };
1868
2030
  const pluginRoot = resolveCodexPluginInstallRoot();
1869
2031
  const pluginTarget = path.join(pluginRoot, "agent-wallet");
1870
2032
  const marketplacePath = resolveCodexMarketplacePath();
@@ -1986,10 +2148,6 @@ function pinHomeIntoMcpFile(mcpPath, env = process.env) {
1986
2148
  return { pinned: true, openclaw_home: home, path: mcpPath };
1987
2149
  }
1988
2150
 
1989
- function pinEditorMcpEnv(pluginSource, env = process.env) {
1990
- return pinHomeIntoMcpFile(path.join(pluginSource, ".mcp.json"), env);
1991
- }
1992
-
1993
2151
  // Claude Code copies the plugin into a version-keyed cache and reads THAT copy,
1994
2152
  // so the bundle pin alone is ineffective once a cache exists. Pin every cached
1995
2153
  // copy too. Cache root is overridable for tests.
@@ -2062,8 +2220,7 @@ function ensureClaudeCodeMarketplace(marketplaceDir, pluginSource, force) {
2062
2220
 
2063
2221
  function runClaudeCodeInstall(args) {
2064
2222
  const pluginSource = resolveClaudeCodePluginSource();
2065
- const pinnedEnv = pinEditorMcpEnv(pluginSource);
2066
- const pinnedCache = pinClaudeCacheCopies();
2223
+ const pinnedEnv = { pinned: false, reason: "plugin source is immutable" };
2067
2224
  const force = hasFlag(args, "--force");
2068
2225
  const skipEnable = hasFlag(args, "--skip-enable");
2069
2226
  const claudeBin = commandPath("claude");
@@ -2125,6 +2282,7 @@ function runClaudeCodeInstall(args) {
2125
2282
  }
2126
2283
 
2127
2284
  const ok = enable.skipped || enable.ok;
2285
+ const pinnedCache = pinClaudeCacheCopies();
2128
2286
  const pluginDirFlagFull = `claude --plugin-dir ${pluginSource}`;
2129
2287
  console.log(
2130
2288
  JSON.stringify(
@@ -2149,75 +2307,115 @@ function runClaudeCodeInstall(args) {
2149
2307
  return enable.skipped || enable.ok ? 0 : 1;
2150
2308
  }
2151
2309
 
2152
- function hermesInstallPresent(env = process.env) {
2153
- const hermesHome = resolveHermesHome(env);
2154
- return (
2155
- fs.existsSync(path.join(hermesHome, "plugins", "agent_wallet")) ||
2156
- fs.existsSync(path.join(hermesHome, ".env"))
2157
- );
2158
- }
2159
-
2160
- function codexInstallPresent(env = process.env) {
2161
- return (
2162
- fs.existsSync(path.join(resolveCodexPluginInstallRoot(env), "agent-wallet")) ||
2163
- fs.existsSync(resolveCodexMarketplacePath(env))
2164
- );
2310
+ function runtimeReleasePath(value, env = process.env) {
2311
+ if (!value) return false;
2312
+ try {
2313
+ const releasesRoot = path.join(resolveRuntimeBase(env), "releases");
2314
+ const relative = path.relative(releasesRoot, path.resolve(expandHome(String(value))));
2315
+ return Boolean(relative) && relative !== ".." && !relative.startsWith(`..${path.sep}`);
2316
+ } catch {
2317
+ return false;
2318
+ }
2165
2319
  }
2166
2320
 
2167
- function claudeCodeInstallPresent(env = process.env) {
2168
- const marketplaceDir = resolveClaudeCodeMarketplaceDir(env);
2169
- const cacheRoot = path.resolve(
2170
- expandHome(env.AGENT_WALLET_CLAUDE_CODE_CACHE_ROOT || "~/.claude/plugins/cache"),
2171
- );
2172
- return (
2173
- fs.existsSync(path.join(marketplaceDir, "plugins", "agent-wallet")) ||
2174
- fs.existsSync(path.join(cacheRoot, CLAUDE_CODE_MARKETPLACE_NAME, "agent-wallet"))
2175
- );
2321
+ function pathEntryExists(pathname) {
2322
+ try {
2323
+ fs.lstatSync(pathname);
2324
+ return true;
2325
+ } catch (error) {
2326
+ if (error?.code === "ENOENT") return false;
2327
+ throw error;
2328
+ }
2176
2329
  }
2177
2330
 
2178
- function captureInstallerRefresh(name, runFn, args) {
2179
- const originalLog = console.log;
2180
- const lines = [];
2181
- console.log = (...items) => {
2182
- lines.push(items.join(" "));
2183
- };
2331
+ function repairRuntimeSymlink(name, linkPath, desiredTarget, env = process.env) {
2332
+ let rawTarget;
2184
2333
  try {
2185
- const code = runFn(args);
2186
- const output = lines.join("\n");
2187
- return {
2188
- name,
2189
- attempted: true,
2190
- ok: code === 0,
2191
- code,
2192
- payload: output ? extractTrailingJson(output) : null,
2193
- error: code === 0 ? "" : output.trim(),
2194
- };
2334
+ const stat = fs.lstatSync(linkPath);
2335
+ if (!stat.isSymbolicLink()) {
2336
+ return { name, attempted: false, ok: true, repaired: false, reason: "not a symlink" };
2337
+ }
2338
+ rawTarget = fs.readlinkSync(linkPath);
2195
2339
  } catch (error) {
2196
- return {
2197
- name,
2198
- attempted: true,
2199
- ok: false,
2200
- code: 1,
2201
- payload: null,
2202
- error: error?.message || String(error),
2203
- };
2204
- } finally {
2205
- console.log = originalLog;
2340
+ if (error?.code === "ENOENT") {
2341
+ return { name, attempted: false, ok: true, repaired: false, reason: "not installed" };
2342
+ }
2343
+ return { name, attempted: true, ok: false, repaired: false, error: error.message };
2344
+ }
2345
+
2346
+ const absoluteTarget = path.resolve(path.dirname(linkPath), rawTarget);
2347
+ const logicalCurrent = currentRuntimePath(env);
2348
+ if (absoluteTarget === path.resolve(desiredTarget) || absoluteTarget.startsWith(`${logicalCurrent}${path.sep}`)) {
2349
+ return { name, attempted: true, ok: true, repaired: false, reason: "already current" };
2350
+ }
2351
+ if (!runtimeReleasePath(absoluteTarget, env)) {
2352
+ return { name, attempted: false, ok: true, repaired: false, reason: "external target preserved" };
2206
2353
  }
2354
+ if (!fs.existsSync(desiredTarget)) {
2355
+ return { name, attempted: true, ok: false, repaired: false, error: `missing ${desiredTarget}` };
2356
+ }
2357
+ switchSymlink(linkPath, desiredTarget);
2358
+ return { name, attempted: true, ok: true, repaired: true, target: desiredTarget };
2207
2359
  }
2208
2360
 
2209
- function refreshInstalledEditorIntegrations(env = process.env) {
2210
- const results = [];
2211
- if (hermesInstallPresent(env)) {
2212
- results.push(captureInstallerRefresh("hermes", runHermesInstall, ["--yes", "--force", "--skip-enable"]));
2361
+ function repairHermesEnv(env = process.env) {
2362
+ const envPath = path.join(resolveHermesHome(env), ".env");
2363
+ const existing = readEnvFile(envPath);
2364
+ const repaired = [];
2365
+ const currentPackage = path.join(currentRuntimePath(env), "agent-wallet");
2366
+ if (runtimeReleasePath(existing.AGENT_WALLET_PACKAGE_ROOT, env)) {
2367
+ envFileSet(envPath, { AGENT_WALLET_PACKAGE_ROOT: currentPackage });
2368
+ repaired.push("AGENT_WALLET_PACKAGE_ROOT");
2369
+ }
2370
+ if (runtimeReleasePath(existing.AGENT_WALLET_PYTHON, env)) {
2371
+ const currentPython = resolveVenvPython(currentRuntimePath(env));
2372
+ if (currentPython) {
2373
+ envFileSet(envPath, { AGENT_WALLET_PYTHON: currentPython });
2374
+ } else {
2375
+ envFileUnset(envPath, ["AGENT_WALLET_PYTHON"]);
2376
+ }
2377
+ repaired.push("AGENT_WALLET_PYTHON");
2213
2378
  }
2214
- if (codexInstallPresent(env)) {
2215
- results.push(captureInstallerRefresh("codex", runCodexInstall, ["--yes", "--force", "--skip-enable"]));
2379
+ return repaired;
2380
+ }
2381
+
2382
+ function repairInstalledEditorIntegrations(env = process.env) {
2383
+ const results = [];
2384
+ const currentRoot = currentRuntimePath(env);
2385
+ const hermesTarget = path.join(resolveHermesHome(env), "plugins", "agent_wallet");
2386
+ if (pathEntryExists(hermesTarget) || fs.existsSync(path.join(resolveHermesHome(env), ".env"))) {
2387
+ const result = repairRuntimeSymlink(
2388
+ "hermes",
2389
+ hermesTarget,
2390
+ path.join(currentRoot, "hermes", "plugins", "agent_wallet"),
2391
+ env,
2392
+ );
2393
+ result.env_repaired = repairHermesEnv(env);
2394
+ results.push(result);
2216
2395
  }
2217
- if (claudeCodeInstallPresent(env)) {
2396
+
2397
+ const codexTarget = path.join(resolveCodexPluginInstallRoot(env), "agent-wallet");
2398
+ if (pathEntryExists(codexTarget)) {
2218
2399
  results.push(
2219
- captureInstallerRefresh("claude-code", runClaudeCodeInstall, ["--yes", "--force", "--skip-enable"]),
2400
+ repairRuntimeSymlink(
2401
+ "codex",
2402
+ codexTarget,
2403
+ path.join(currentRoot, "codex", "plugins", "agent-wallet"),
2404
+ env,
2405
+ ),
2406
+ );
2407
+ }
2408
+
2409
+ const claudeTarget = path.join(resolveClaudeCodeMarketplaceDir(env), "plugins", "agent-wallet");
2410
+ if (pathEntryExists(claudeTarget)) {
2411
+ const result = repairRuntimeSymlink(
2412
+ "claude-code",
2413
+ claudeTarget,
2414
+ path.join(currentRoot, "claude-code", "plugins", "agent-wallet"),
2415
+ env,
2220
2416
  );
2417
+ result.cache_pins = pinClaudeCacheCopies(env);
2418
+ results.push(result);
2221
2419
  }
2222
2420
  return results;
2223
2421
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "agent-wallet",
3
3
  "displayName": "Agent Wallet",
4
- "version": "0.1.72",
4
+ "version": "0.1.74",
5
5
  "description": "Claude Code bridge for the existing AgentLayer wallet runtime. Connects to Solana, Bitcoin, and EVM wallets without creating a new one.",
6
6
  "author": {
7
7
  "name": "AgentLayer"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-wallet",
3
- "version": "0.1.72",
3
+ "version": "0.1.74",
4
4
  "description": "Codex plugin bridge for the AgentLayer wallet runtime.",
5
5
  "author": {
6
6
  "name": "AgentLayer"
@@ -1,5 +1,5 @@
1
1
  name: agent-wallet
2
- version: 0.1.72
2
+ version: 0.1.74
3
3
  description: Thin Hermes Agent bridge to the existing AgentLayer/OpenClaw wallet backend
4
4
  provides_tools:
5
5
  - agent_wallet_tools
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentlayer.tech/wallet",
3
- "version": "0.1.72",
3
+ "version": "0.1.74",
4
4
  "description": "NPM installer for the OpenClaw Agent Wallet local runtime.",
5
5
  "type": "module",
6
6
  "repository": {
@@ -19,6 +19,7 @@
19
19
  "build:openclaw-plugins": "node scripts/manage_openclaw_plugin_packages.mjs build",
20
20
  "check:openclaw-plugins": "node scripts/manage_openclaw_plugin_packages.mjs check",
21
21
  "check:release-version": "node scripts/check_release_version.mjs",
22
+ "test:release-promotion": "node --test scripts/validate_npm_promotion.test.mjs",
22
23
  "version:sync": "node scripts/sync_version.mjs",
23
24
  "release:local": "node scripts/release_local.mjs",
24
25
  "test:npm-installer": "python3 agent-wallet/tests/smoke_npm_installer.py",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wdk-btc-wallet",
3
- "version": "0.1.72",
3
+ "version": "0.1.74",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "description": "Separate BTC-only wallet service built on Tether WDK.",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wdk-evm-wallet",
3
- "version": "0.1.72",
3
+ "version": "0.1.74",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "description": "Separate EVM wallet service built on Tether WDK.",
@@ -14,7 +14,8 @@
14
14
  "test:morpho-runtime": "node --test --test-concurrency=1 tests/smoke_morpho_runtime.mjs",
15
15
  "test:lido-runtime": "node --test --test-concurrency=1 tests/smoke_lido_runtime.mjs",
16
16
  "test:uniswap-runtime": "node --test --test-concurrency=1 tests/smoke_uniswap_runtime.mjs",
17
- "test:unit": "node --test tests/unit_uniswap_helpers.mjs"
17
+ "test:unit": "node --test tests/unit_uniswap_helpers.mjs",
18
+ "test:identity": "node --test tests/unit_instance_identity.mjs"
18
19
  },
19
20
  "dependencies": {
20
21
  "@morpho-org/morpho-sdk": "^3.2.0",
@@ -150,6 +150,27 @@ function ensureLocalAuthToken(tokenPath, configuredToken = "") {
150
150
  return generated;
151
151
  }
152
152
 
153
+ function ensureInstanceId(instanceIdPath, configuredInstanceId = "") {
154
+ const direct = String(configuredInstanceId ?? "").trim();
155
+ if (direct) {
156
+ return direct;
157
+ }
158
+ fs.mkdirSync(path.dirname(instanceIdPath), { recursive: true, mode: 0o700 });
159
+ try {
160
+ const existing = fs.readFileSync(instanceIdPath, "utf8").trim();
161
+ if (existing) {
162
+ fs.chmodSync(instanceIdPath, 0o600);
163
+ return existing;
164
+ }
165
+ } catch {
166
+ // Generate a stable install identity below.
167
+ }
168
+ const generated = crypto.randomUUID();
169
+ fs.writeFileSync(instanceIdPath, generated + "\n", { encoding: "utf8", mode: 0o600 });
170
+ fs.chmodSync(instanceIdPath, 0o600);
171
+ return generated;
172
+ }
173
+
153
174
  function normalizeNetworkKey(value) {
154
175
  const normalized = String(value ?? "").trim().toLowerCase();
155
176
  const aliases = {
@@ -201,6 +222,8 @@ export function loadConfig(env = process.env) {
201
222
  const authTokenPath =
202
223
  String(env.WDK_EVM_LOCAL_TOKEN_PATH ?? "").trim() ||
203
224
  path.join(openClawHome, "wdk-evm-wallet", "local-auth-token");
225
+ const instanceIdPath =
226
+ String(env.WDK_EVM_INSTANCE_ID_PATH ?? "").trim() || path.join(dataDir, "instance-id");
204
227
  const providerMode = parseProviderMode(env.WDK_EVM_RPC_PROVIDER_MODE, "gateway");
205
228
  const providerGatewayUrl =
206
229
  String(env.PROVIDER_GATEWAY_URL ?? "").trim() || DEFAULT_PROVIDER_GATEWAY_URL;
@@ -294,6 +317,7 @@ export function loadConfig(env = process.env) {
294
317
  network,
295
318
  openClawHome,
296
319
  dataDir,
320
+ instanceId: ensureInstanceId(instanceIdPath, env.WDK_EVM_INSTANCE_ID),
297
321
  authRequired: true,
298
322
  authTokenPath,
299
323
  authToken: ensureLocalAuthToken(authTokenPath, env.WDK_EVM_LOCAL_TOKEN),