@agentlayer.tech/wallet 0.1.32 → 0.1.33

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentlayertech/agent-wallet-plugin",
3
- "version": "0.1.32",
3
+ "version": "0.1.33",
4
4
  "description": "OpenClaw plugin bridge for the AgentLayer wallet runtime.",
5
5
  "type": "module",
6
6
  "license": "SEE LICENSE IN ../../../LICENSE",
package/CHANGELOG.md CHANGED
@@ -2,6 +2,31 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## v0.1.33 - 2026-06-01
6
+
7
+ - Hardened runtime resolution end-to-end so a broken or stale runtime can no
8
+ longer surface as an opaque MCP `-32000`.
9
+ - `run_mcp.sh` (Codex + Claude Code) now self-checks the resolved `server.py`
10
+ with `py_compile` and emits a structured, actionable JSON error (with a
11
+ `fix` command) when the server is missing or fails to parse, instead of
12
+ silently handing a broken file to Python. JSON errors are emitted safely so
13
+ arbitrary paths cannot produce invalid JSON.
14
+ - `doctor` / `doctor --deep` now validate the live `current` runtime — symlink
15
+ integrity, venv python, `server.py` parse, a real MCP `initialize` handshake
16
+ (under `--deep`), and per-editor resolution — and attach a `fix` command to
17
+ every failing check (output is a structured `checks[]` array).
18
+ - `install` / `update` now verify the newly-activated release via an MCP
19
+ handshake and auto-rollback `current → previous` on failure, with guidance
20
+ classified as `broken_release` (our side — you stay safe on the previous
21
+ version) vs `local_env` (fixable locally). A first install with no previous
22
+ version leaves no broken runtime active and does not park the broken release
23
+ under `previous`.
24
+ - `codex install` / `claude-code install` pin the resolved `OPENCLAW_HOME`
25
+ into the editor `.mcp.json` env (bundle and, for Claude Code, existing
26
+ version-keyed cache copies), eliminating launcher/installer home divergence.
27
+ The venv python is intentionally not pinned so it stays correct across
28
+ runtime upgrades.
29
+
5
30
  ## v0.1.32 - 2026-06-01
6
31
 
7
32
  - Fixed a `SyntaxError` in `codex/plugins/agent-wallet/server.py` introduced by
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "openclaw-agent-wallet"
7
- version = "0.1.32"
7
+ version = "0.1.33"
8
8
  description = "Plugin-friendly wallet backend for OpenClaw agents"
9
9
  requires-python = ">=3.10"
10
10
  dependencies = [
@@ -392,6 +392,17 @@ function switchSymlink(linkPath, targetPath) {
392
392
  fs.renameSync(tempLink, linkPath);
393
393
  }
394
394
 
395
+ // Remove a runtime pointer symlink (current/previous). recursive+force tolerate
396
+ // a stray directory in that slot; on a symlink this unlinks the pointer only and
397
+ // never deletes the release directory it targets.
398
+ function removeRuntimePointer(pointerPath) {
399
+ try {
400
+ fs.rmSync(pointerPath, { recursive: true, force: true });
401
+ } catch (error) {
402
+ if (error?.code !== "ENOENT") throw error;
403
+ }
404
+ }
405
+
395
406
  function parseFlagValue(args, name) {
396
407
  const prefix = `${name}=`;
397
408
  for (let index = 0; index < args.length; index += 1) {
@@ -617,56 +628,214 @@ function ensureBootKeyFile(env = process.env) {
617
628
  return { path: keyFile, status: "created" };
618
629
  }
619
630
 
620
- function runDoctor() {
621
- const requiredPaths = [
622
- ["setup.sh", setupPath],
623
- ["agent-wallet", path.join(packageRoot, "agent-wallet")],
624
- ["OpenClaw extension", path.join(packageRoot, ".openclaw", "extensions", "agent-wallet")],
625
- ["Codex plugin", path.join(packageRoot, "codex", "plugins", "agent-wallet", ".codex-plugin", "plugin.json")],
626
- ["Claude Code plugin", path.join(packageRoot, "claude-code", "plugins", "agent-wallet", ".claude-plugin", "plugin.json")],
627
- ["wdk-btc-wallet", path.join(packageRoot, "wdk-btc-wallet", "package.json")],
628
- ["wdk-evm-wallet", path.join(packageRoot, "wdk-evm-wallet", "package.json")],
631
+ function resolveVenvPython(releaseRoot) {
632
+ const candidates = [
633
+ path.join(releaseRoot, "agent-wallet", ".venv", "bin", "python"),
634
+ path.join(releaseRoot, "agent-wallet", ".runtime-venv", "bin", "python"),
629
635
  ];
630
- const commands = ["node", "npm"];
631
- const missing = [];
632
- const python = selectedPythonProbe();
636
+ for (const candidate of candidates) {
637
+ if (fs.existsSync(candidate)) return candidate;
638
+ }
639
+ return null;
640
+ }
641
+
642
+ // Classify a verification failure so the caller can route the right guidance:
643
+ // broken_release -> our shipped code is bad; user cannot fix, stay on previous.
644
+ // local_env -> user's machine/runtime is fixable (python/venv/corrupt unpack).
645
+ // unknown -> fall back to generic guidance.
646
+ function classifyVerifyError(detail) {
647
+ const text = String(detail || "");
648
+ if (/SyntaxError|IndentationError|ImportError|ModuleNotFoundError|TabError|NameError/.test(text)) {
649
+ return "broken_release";
650
+ }
651
+ if (/ENOENT|python|venv|ensurepip|not found|No such file|Permission denied|spawn/i.test(text)) {
652
+ return "local_env";
653
+ }
654
+ return "unknown";
655
+ }
656
+
657
+ function verifyRuntime(releaseRoot, env = process.env) {
658
+ if (String(env.AGENT_WALLET_VERIFY_DISABLE || "") === "1") {
659
+ return { ok: true, skipped: true };
660
+ }
661
+ if (String(env.AGENT_WALLET_VERIFY_FORCE_FAIL || "") === "1") {
662
+ return { ok: false, error: "verify forced to fail (AGENT_WALLET_VERIFY_FORCE_FAIL)", category: "broken_release" };
663
+ }
664
+ const serverPy = path.join(releaseRoot, "codex", "plugins", "agent-wallet", "server.py");
665
+ if (!fs.existsSync(serverPy)) {
666
+ return { ok: false, error: `server.py missing at ${serverPy}`, category: "local_env" };
667
+ }
668
+ const python =
669
+ env.AGENT_WALLET_PYTHON ||
670
+ env.OPENCLAW_AGENT_WALLET_PYTHON ||
671
+ resolveVenvPython(releaseRoot) ||
672
+ commandPath("python3") ||
673
+ "python3";
674
+ const initLine = JSON.stringify({
675
+ jsonrpc: "2.0",
676
+ id: 1,
677
+ method: "initialize",
678
+ params: { protocolVersion: "2024-11-05", capabilities: {}, clientInfo: { name: "verify", version: "0" } },
679
+ });
680
+ const probe = spawnSync(python, [serverPy], {
681
+ input: initLine + "\n",
682
+ encoding: "utf8",
683
+ timeout: Number(env.AGENT_WALLET_VERIFY_TIMEOUT_MS || 25000),
684
+ env: { ...env, FASTMCP_SHOW_SERVER_BANNER: "false", FASTMCP_LOG_LEVEL: "ERROR" },
685
+ });
686
+ if (probe.error) {
687
+ const isTimeout = String(probe.error.message || "").includes("ETIMEDOUT");
688
+ return {
689
+ ok: false,
690
+ error: `handshake ${isTimeout ? "timed out (server did not respond)" : "spawn failed"}: ${probe.error.message}`,
691
+ category: isTimeout ? "broken_release" : "local_env",
692
+ };
693
+ }
694
+ const out = String(probe.stdout || "");
695
+ if (out.includes('"serverInfo"')) {
696
+ return { ok: true };
697
+ }
698
+ const detail = (probe.stderr || out || "").trim().split("\n").slice(-3).join(" ");
699
+ return {
700
+ ok: false,
701
+ error: `MCP initialize handshake failed: ${detail || "no serverInfo in response"}`,
702
+ category: classifyVerifyError(detail),
703
+ };
704
+ }
633
705
 
634
- for (const command of commands) {
635
- if (!hasCommand(command)) missing.push(`command:${command}`);
706
+ function resolveEditorServerChecks(env = process.env) {
707
+ const checks = [];
708
+ // Claude Code's launcher (run_mcp.sh) falls back to the runtime codex
709
+ // server.py when its own plugin-cache copy lacks one, so a present runtime
710
+ // server.py is exactly what that launcher will exec. We check its presence
711
+ // as the proxy for "Claude can resolve a server" (we do not inspect the
712
+ // version-specific cache copy directly).
713
+ const root = resolvedCurrentRuntimeRoot(env);
714
+ const runtimeCodex = root ? path.join(root, "codex", "plugins", "agent-wallet", "server.py") : null;
715
+ const claudeCacheRoot = expandHome("~/.claude/plugins/cache");
716
+ if (fs.existsSync(claudeCacheRoot)) {
717
+ const reachable = Boolean(runtimeCodex && fs.existsSync(runtimeCodex));
718
+ checks.push({
719
+ name: "editor:claude-code",
720
+ ok: reachable,
721
+ error: reachable ? "" : "Claude cache copy cannot resolve server.py from runtime",
722
+ fix: reachable ? "" : "npx @agentlayer.tech/wallet claude-code install --yes",
723
+ });
636
724
  }
637
- if (!python.path) {
638
- missing.push("command:python3.10-or-python3");
639
- } else if (!python.version_ok) {
640
- missing.push(`python>=3.10:selected:${python.version || "unknown"}`);
641
- } else if (!python.venv_ok) {
642
- missing.push(`python-venv-ensurepip:selected:${python.version || "unknown"}`);
725
+ // Codex: plugin symlink target under the codex plugin install root.
726
+ const codexInstallRoot = resolveCodexPluginInstallRoot(env);
727
+ const codexTarget = path.join(codexInstallRoot, "agent-wallet", "server.py");
728
+ if (fs.existsSync(codexInstallRoot)) {
729
+ const ok = fs.existsSync(codexTarget);
730
+ checks.push({
731
+ name: "editor:codex",
732
+ ok,
733
+ error: ok ? "" : `codex plugin server.py missing at ${codexTarget}`,
734
+ fix: ok ? "" : "npx @agentlayer.tech/wallet codex install --yes",
735
+ });
643
736
  }
644
- for (const [label, target] of requiredPaths) {
645
- if (!fs.existsSync(target)) missing.push(`${label}:${target}`);
737
+ return checks;
738
+ }
739
+
740
+ function runDoctor(args = []) {
741
+ const deep = hasFlag(args, "--deep");
742
+ const env = process.env;
743
+ const checks = [];
744
+ const fixInstall = "npx @agentlayer.tech/wallet install --yes";
745
+
746
+ for (const command of ["node", "npm"]) {
747
+ const ok = hasCommand(command);
748
+ checks.push({
749
+ name: `command:${command}`,
750
+ ok,
751
+ error: ok ? "" : `${command} not found on PATH`,
752
+ fix: ok ? "" : `install ${command}`,
753
+ });
646
754
  }
755
+ const python = selectedPythonProbe();
756
+ const pythonOk = Boolean(python.path && python.version_ok && python.venv_ok);
757
+ checks.push({
758
+ name: "python_version",
759
+ ok: pythonOk,
760
+ error: !python.path ? "python3 not found"
761
+ : !python.version_ok ? `selected python ${python.version} < 3.10`
762
+ : !python.venv_ok ? `python ${python.version} lacks venv/ensurepip` : "",
763
+ fix: pythonOk ? "" : "install python>=3.10 with venv",
764
+ });
765
+
766
+ const currentPath = currentRuntimePath(env);
767
+ const currentRoot = resolvedCurrentRuntimeRoot(env);
768
+ const symlinkOk = Boolean(currentRoot && fs.existsSync(currentRoot));
769
+ checks.push({
770
+ name: "current_symlink",
771
+ ok: symlinkOk,
772
+ target: readLinkOrNull(currentPath),
773
+ error: symlinkOk ? "" : `current does not resolve to an existing release (${currentPath})`,
774
+ fix: symlinkOk ? "" : fixInstall,
775
+ });
647
776
 
777
+ const venvPython = currentRoot ? resolveVenvPython(currentRoot) : null;
778
+ checks.push({
779
+ name: "runtime_venv_python",
780
+ ok: Boolean(venvPython),
781
+ path: venvPython,
782
+ error: venvPython ? "" : "runtime .runtime-venv/bin/python missing",
783
+ fix: venvPython ? "" : fixInstall,
784
+ });
785
+
786
+ const serverPy = currentRoot
787
+ ? path.join(currentRoot, "codex", "plugins", "agent-wallet", "server.py")
788
+ : null;
789
+ const serverExists = Boolean(serverPy && fs.existsSync(serverPy));
790
+ let parseOk = false;
791
+ if (serverExists && venvPython) {
792
+ const compiled = spawnSync(venvPython, ["-m", "py_compile", serverPy], { encoding: "utf8" });
793
+ parseOk = compiled.status === 0;
794
+ }
795
+ checks.push({
796
+ name: "server_py_parses",
797
+ ok: parseOk,
798
+ error: !serverExists ? "runtime codex server.py missing"
799
+ : !venvPython ? "server.py parse skipped (runtime venv missing)"
800
+ : parseOk ? "" : "server.py present but failed to parse",
801
+ fix: parseOk ? "" : fixInstall,
802
+ });
803
+
804
+ if (deep) {
805
+ const verify = currentRoot
806
+ ? verifyRuntime(currentRoot, env)
807
+ : { ok: false, error: "no runtime to handshake", category: "local_env" };
808
+ checks.push({
809
+ name: "mcp_initialize_handshake",
810
+ ok: verify.ok,
811
+ error: verify.ok ? "" : verify.error,
812
+ fix: verify.ok ? "" : `${fixInstall} (or: npx @agentlayer.tech/wallet rollback)`,
813
+ });
814
+ }
815
+
816
+ for (const editorCheck of resolveEditorServerChecks(env)) {
817
+ checks.push(editorCheck);
818
+ }
819
+
820
+ const ok = checks.every((c) => c.ok);
648
821
  console.log(
649
822
  JSON.stringify(
650
823
  {
651
- ok: missing.length === 0,
824
+ ok,
652
825
  package_name: packageJson.name,
653
826
  package_version: packageVersion,
654
- package_root: packageRoot,
655
- setup_path: setupPath,
656
827
  openclaw_home: resolveOpenclawHome(),
657
- runtime_base: resolveRuntimeBase(),
658
- current_runtime: currentRuntimePath(),
828
+ current_runtime: currentPath,
659
829
  active_version: activeVersion(),
660
830
  releases: listReleases(),
661
- python,
662
- commands: Object.fromEntries(commands.map((command) => [command, hasCommand(command)])),
663
- missing,
831
+ deep,
832
+ checks,
664
833
  },
665
834
  null,
666
835
  2,
667
836
  ),
668
837
  );
669
- return missing.length === 0 ? 0 : 1;
838
+ return ok ? 0 : 1;
670
839
  }
671
840
 
672
841
  function runStatus(args = []) {
@@ -786,6 +955,68 @@ function runInstall(args, { commandName = "install" } = {}) {
786
955
  }
787
956
  switchSymlink(currentPath, releaseRoot);
788
957
 
958
+ // Installs that pass --skip-python-setup may have no venv, so this handshake
959
+ // would fail and trigger a spurious rollback; such flows must set
960
+ // AGENT_WALLET_VERIFY_DISABLE=1 (verifyRuntime then skips).
961
+ const verification = verifyRuntime(releaseRoot, env);
962
+ if (!verification.ok && !verification.skipped) {
963
+ const rollbackTarget = currentTarget; // pre-switch target captured before the switch, if any
964
+ const rolledBack = Boolean(rollbackTarget);
965
+ if (rolledBack) {
966
+ switchSymlink(currentPath, rollbackTarget);
967
+ }
968
+ const previousVersion = rolledBack
969
+ ? path.basename(path.resolve(path.dirname(currentPath), rollbackTarget))
970
+ : null;
971
+
972
+ let human;
973
+ let fix;
974
+ if (!rolledBack) {
975
+ // First install / no good fallback — leave no active runtime rather than a
976
+ // broken one. Deliberately do NOT point `previous` at the broken release,
977
+ // so a later `rollback` cannot reactivate it; the release stays under
978
+ // releases/<version> for inspection via `install --version`.
979
+ removeRuntimePointer(currentPath);
980
+ human =
981
+ verification.category === "broken_release"
982
+ ? `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.`
983
+ : `Release ${packageVersion} failed to verify and there is no previous version. Your local environment looks incomplete: ${verification.error}.`;
984
+ fix =
985
+ verification.category === "local_env"
986
+ ? "Ensure python>=3.10 with venv is installed, then: npx @agentlayer.tech/wallet install --yes"
987
+ : "npx @agentlayer.tech/wallet install --version <known-good-version> --yes";
988
+ } else if (verification.category === "broken_release") {
989
+ human = `Release ${packageVersion} is broken; kept you on the working version ${previousVersion}. This is on our side — you are safe. Re-run update when a patched release ships.`;
990
+ fix = "npx @agentlayer.tech/wallet update --yes";
991
+ } else if (verification.category === "local_env") {
992
+ human = `Release ${packageVersion} could not start on this machine; kept you on ${previousVersion}. This looks fixable locally: ${verification.error}.`;
993
+ fix = "Fix python>=3.10/venv, then: npx @agentlayer.tech/wallet install --yes";
994
+ } else {
995
+ human = `Release ${packageVersion} failed verification; kept you on ${previousVersion}.`;
996
+ fix = "npx @agentlayer.tech/wallet doctor --deep (then install --yes once resolved)";
997
+ }
998
+
999
+ console.error(
1000
+ JSON.stringify(
1001
+ {
1002
+ ok: false,
1003
+ command: commandName,
1004
+ version: packageVersion,
1005
+ category: verification.category || "unknown",
1006
+ error: `runtime verification failed: ${verification.error}`,
1007
+ rolled_back: rolledBack,
1008
+ kept_version: previousVersion,
1009
+ current_runtime_target: readLinkOrNull(currentPath),
1010
+ message: human,
1011
+ fix,
1012
+ },
1013
+ null,
1014
+ 2,
1015
+ ),
1016
+ );
1017
+ return 1;
1018
+ }
1019
+
789
1020
  if (env.AGENT_WALLET_BOOT_KEY) {
790
1021
  envFileSet(path.join(releaseRoot, "agent-wallet", ".env"), {
791
1022
  AGENT_WALLET_BOOT_KEY: env.AGENT_WALLET_BOOT_KEY,
@@ -986,6 +1217,9 @@ function resolveHermesPluginSource() {
986
1217
  }
987
1218
 
988
1219
  function resolveCodexPluginSource() {
1220
+ // test/CI override: inject a staged bundle dir
1221
+ const override = String(process.env.AGENT_WALLET_CODEX_PLUGIN_SOURCE || "").trim();
1222
+ if (override) return path.resolve(expandHome(override));
989
1223
  const currentRoot = resolvedCurrentRuntimeRoot();
990
1224
  const candidates = [];
991
1225
  if (currentRoot) {
@@ -1001,6 +1235,9 @@ function resolveCodexPluginSource() {
1001
1235
  }
1002
1236
 
1003
1237
  function resolveClaudeCodePluginSource() {
1238
+ // test/CI override: inject a staged bundle dir
1239
+ const override = String(process.env.AGENT_WALLET_CLAUDE_CODE_PLUGIN_SOURCE || "").trim();
1240
+ if (override) return path.resolve(expandHome(override));
1004
1241
  const currentRoot = resolvedCurrentRuntimeRoot();
1005
1242
  const candidates = [];
1006
1243
  if (currentRoot) {
@@ -1190,6 +1427,7 @@ function runHermesInstall(args) {
1190
1427
  function runCodexInstall(args) {
1191
1428
  const codexHome = resolveCodexHome();
1192
1429
  const pluginSource = resolveCodexPluginSource();
1430
+ const pinnedEnv = pinEditorMcpEnv(pluginSource);
1193
1431
  const pluginRoot = resolveCodexPluginInstallRoot();
1194
1432
  const pluginTarget = path.join(pluginRoot, "agent-wallet");
1195
1433
  const marketplacePath = resolveCodexMarketplacePath();
@@ -1260,6 +1498,7 @@ function runCodexInstall(args) {
1260
1498
  marketplace_name: marketplace.marketplace_name,
1261
1499
  codex_add: add,
1262
1500
  restart_required: true,
1501
+ pinned_env: pinnedEnv,
1263
1502
  },
1264
1503
  null,
1265
1504
  2,
@@ -1276,6 +1515,53 @@ function resolveClaudeCodeMarketplaceDir(env = process.env) {
1276
1515
  );
1277
1516
  }
1278
1517
 
1518
+ // Pin OPENCLAW_HOME into one .mcp.json so run_mcp.sh uses the install-time home
1519
+ // instead of re-deriving the ~/.openclaw default. We deliberately do NOT pin
1520
+ // AGENT_WALLET_PYTHON: the launcher resolves the venv from OPENCLAW_HOME->current
1521
+ // dynamically, so a pinned python would go stale (and wrongly win) after upgrade.
1522
+ function pinHomeIntoMcpFile(mcpPath, env = process.env) {
1523
+ if (!fs.existsSync(mcpPath)) return { pinned: false, reason: "no .mcp.json", path: mcpPath };
1524
+ let doc;
1525
+ try {
1526
+ doc = JSON.parse(fs.readFileSync(mcpPath, "utf8"));
1527
+ } catch (error) {
1528
+ return { pinned: false, reason: `unreadable .mcp.json: ${error.message}`, path: mcpPath };
1529
+ }
1530
+ const entry = (doc.mcpServers || {})["agent-wallet"];
1531
+ if (!entry) return { pinned: false, reason: "no agent-wallet server entry", path: mcpPath };
1532
+ const home = resolveOpenclawHome(env);
1533
+ entry.env = { ...(entry.env || {}), OPENCLAW_HOME: home };
1534
+ writeJsonFile(mcpPath, doc);
1535
+ return { pinned: true, openclaw_home: home, path: mcpPath };
1536
+ }
1537
+
1538
+ function pinEditorMcpEnv(pluginSource, env = process.env) {
1539
+ return pinHomeIntoMcpFile(path.join(pluginSource, ".mcp.json"), env);
1540
+ }
1541
+
1542
+ // Claude Code copies the plugin into a version-keyed cache and reads THAT copy,
1543
+ // so the bundle pin alone is ineffective once a cache exists. Pin every cached
1544
+ // copy too. Cache root is overridable for tests.
1545
+ function pinClaudeCacheCopies(env = process.env) {
1546
+ const cacheRoot = path.resolve(
1547
+ expandHome(env.AGENT_WALLET_CLAUDE_CODE_CACHE_ROOT || "~/.claude/plugins/cache"),
1548
+ );
1549
+ const pluginCacheDir = path.join(cacheRoot, CLAUDE_CODE_MARKETPLACE_NAME, "agent-wallet");
1550
+ const results = [];
1551
+ if (!fs.existsSync(pluginCacheDir)) return results;
1552
+ let versions;
1553
+ try {
1554
+ versions = fs.readdirSync(pluginCacheDir);
1555
+ } catch {
1556
+ return results;
1557
+ }
1558
+ for (const version of versions) {
1559
+ const mcpPath = path.join(pluginCacheDir, version, ".mcp.json");
1560
+ if (fs.existsSync(mcpPath)) results.push(pinHomeIntoMcpFile(mcpPath, env));
1561
+ }
1562
+ return results;
1563
+ }
1564
+
1279
1565
  function ensureClaudeCodeMarketplace(marketplaceDir, pluginSource, force) {
1280
1566
  const pluginsDir = path.join(marketplaceDir, "plugins");
1281
1567
  const pluginLink = path.join(pluginsDir, "agent-wallet");
@@ -1325,6 +1611,8 @@ function ensureClaudeCodeMarketplace(marketplaceDir, pluginSource, force) {
1325
1611
 
1326
1612
  function runClaudeCodeInstall(args) {
1327
1613
  const pluginSource = resolveClaudeCodePluginSource();
1614
+ const pinnedEnv = pinEditorMcpEnv(pluginSource);
1615
+ const pinnedCache = pinClaudeCacheCopies();
1328
1616
  const force = hasFlag(args, "--force");
1329
1617
  const skipEnable = hasFlag(args, "--skip-enable");
1330
1618
  const claudeBin = commandPath("claude");
@@ -1397,6 +1685,8 @@ function runClaudeCodeInstall(args) {
1397
1685
  claude_code_install: enable,
1398
1686
  manual_load: pluginDirFlagFull,
1399
1687
  restart_required: true,
1688
+ pinned_env: pinnedEnv,
1689
+ pinned_cache: pinnedCache,
1400
1690
  note: ok
1401
1691
  ? "Plugin registered. Restart Claude Code to activate."
1402
1692
  : `If automatic registration failed, load the plugin with: ${pluginDirFlagFull}`,
@@ -1422,7 +1712,16 @@ if (command === "--version" || command === "-v" || command === "version") {
1422
1712
  }
1423
1713
 
1424
1714
  if (command === "doctor") {
1425
- process.exit(runDoctor());
1715
+ process.exit(runDoctor(args.slice(1)));
1716
+ }
1717
+
1718
+ if (command === "--self-verify") {
1719
+ const releaseRoot = args[1] ? path.resolve(expandHome(args[1])) : resolvedCurrentRuntimeRoot();
1720
+ const result = releaseRoot
1721
+ ? verifyRuntime(releaseRoot)
1722
+ : { ok: false, error: "no runtime to verify", category: "local_env" };
1723
+ console.log(JSON.stringify(result));
1724
+ process.exit(result.ok ? 0 : 1);
1426
1725
  }
1427
1726
 
1428
1727
  if (command === "status") {
@@ -20,7 +20,7 @@ elif [ -f "$CODEX_SERVER" ]; then
20
20
  elif [ -f "$RUNTIME_CODEX_DIR/server.py" ]; then
21
21
  SERVER_PY=$(CDPATH= cd -- "$RUNTIME_CODEX_DIR" && pwd)/server.py
22
22
  else
23
- printf '{"error":"agent-wallet server.py not found. Run: npx @agentlayer.tech/wallet install --yes"}\n' >&2
23
+ printf '{"error":"agent-wallet server.py not found in plugin, codex sibling, or runtime package.","fix":"npx @agentlayer.tech/wallet install --yes"}\n' >&2
24
24
  exit 1
25
25
  fi
26
26
 
@@ -36,4 +36,17 @@ else
36
36
  PYTHON_BIN=python3
37
37
  fi
38
38
 
39
+ # Fail loudly (not -32000) if the resolved server cannot even be parsed.
40
+ if ! "$PYTHON_BIN" -m py_compile "$SERVER_PY" 2>/dev/null; then
41
+ "$PYTHON_BIN" - "$SERVER_PY" >&2 <<'PY'
42
+ import json, sys
43
+ print(json.dumps({
44
+ "error": "agent-wallet server.py failed to parse — runtime likely broken.",
45
+ "server_py": sys.argv[1],
46
+ "fix": "npx @agentlayer.tech/wallet install --yes (or: npx @agentlayer.tech/wallet rollback)",
47
+ }))
48
+ PY
49
+ exit 1
50
+ fi
51
+
39
52
  exec "$PYTHON_BIN" "$SERVER_PY"
@@ -18,4 +18,22 @@ else
18
18
  PYTHON_BIN=python3
19
19
  fi
20
20
 
21
+ if [ ! -f "$PLUGIN_ROOT/server.py" ]; then
22
+ printf '{"error":"agent-wallet server.py not found in codex plugin.","fix":"npx @agentlayer.tech/wallet install --yes"}\n' >&2
23
+ exit 1
24
+ fi
25
+
26
+ # Fail loudly (not -32000) if the resolved server cannot even be parsed.
27
+ if ! "$PYTHON_BIN" -m py_compile "$PLUGIN_ROOT/server.py" 2>/dev/null; then
28
+ "$PYTHON_BIN" - "$PLUGIN_ROOT/server.py" >&2 <<'PY'
29
+ import json, sys
30
+ print(json.dumps({
31
+ "error": "agent-wallet server.py failed to parse — runtime likely broken.",
32
+ "server_py": sys.argv[1],
33
+ "fix": "npx @agentlayer.tech/wallet install --yes (or: npx @agentlayer.tech/wallet rollback)",
34
+ }))
35
+ PY
36
+ exit 1
37
+ fi
38
+
21
39
  exec "$PYTHON_BIN" "$PLUGIN_ROOT/server.py"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentlayer.tech/wallet",
3
- "version": "0.1.32",
3
+ "version": "0.1.33",
4
4
  "description": "NPM installer for the OpenClaw Agent Wallet local runtime.",
5
5
  "type": "module",
6
6
  "repository": {