@agentlayer.tech/wallet 0.1.58 → 0.1.65
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.openclaw/extensions/agent-wallet/openclaw.plugin.json +1 -1
- package/.openclaw/extensions/agent-wallet/package.json +1 -1
- package/CHANGELOG.md +30 -0
- package/README.md +1 -1
- package/VERSION +1 -1
- package/agent-wallet/agent_wallet/__init__.py +1 -1
- package/agent-wallet/agent_wallet/boot_key_migration.py +158 -0
- package/agent-wallet/agent_wallet/boot_key_recovery.py +46 -0
- package/agent-wallet/agent_wallet/config.py +63 -1
- package/agent-wallet/agent_wallet/encrypted_storage.py +77 -9
- package/agent-wallet/agent_wallet/keystore.py +328 -0
- package/agent-wallet/agent_wallet/openclaw_cli.py +83 -4
- package/agent-wallet/agent_wallet/openclaw_runtime.py +20 -0
- package/agent-wallet/agent_wallet/sealed_keys.py +58 -2
- package/agent-wallet/agent_wallet/user_wallets.py +93 -5
- package/agent-wallet/openclaw.plugin.json +1 -1
- package/agent-wallet/pyproject.toml +1 -1
- package/agent-wallet/scripts/build_release_bundle.py +10 -2
- package/bin/openclaw-agent-wallet.mjs +73 -4
- package/claude-code/plugins/agent-wallet/.claude-plugin/plugin.json +1 -1
- package/codex/plugins/agent-wallet/.codex-plugin/plugin.json +1 -1
- package/codex/plugins/agent-wallet/server.py +64 -44
- package/hermes/plugins/agent_wallet/plugin.yaml +1 -1
- package/package.json +1 -1
- package/wdk-btc-wallet/package.json +1 -1
- package/wdk-evm-wallet/package.json +1 -1
|
@@ -52,13 +52,27 @@ def resolve_user_wallet_path(user_id: str, network: str | None = None) -> Path:
|
|
|
52
52
|
return user_dir / f"solana-{effective_network}-agent.json"
|
|
53
53
|
|
|
54
54
|
|
|
55
|
-
def _user_wallet_metadata(
|
|
55
|
+
def _user_wallet_metadata(
|
|
56
|
+
user_id: str,
|
|
57
|
+
address: str,
|
|
58
|
+
network: str | None = None,
|
|
59
|
+
key_scope: str | None = None,
|
|
60
|
+
) -> dict[str, str]:
|
|
56
61
|
effective_network = normalize_solana_network(network or settings.solana_network)
|
|
57
|
-
|
|
62
|
+
metadata = {
|
|
58
63
|
"address": address,
|
|
59
64
|
"user_id": user_id,
|
|
60
65
|
"network": effective_network,
|
|
61
66
|
}
|
|
67
|
+
# Record which master-key scope encrypted this envelope so the loader can
|
|
68
|
+
# try the matching candidate first instead of paying a wasted KDF on the
|
|
69
|
+
# wrong one. Writes through _resolve_user_wallet_master_key follow
|
|
70
|
+
# use_per_user_key_derivation(), so that is the accurate default.
|
|
71
|
+
effective_scope = key_scope or (
|
|
72
|
+
"per-user-derived" if use_per_user_key_derivation() else "global-master"
|
|
73
|
+
)
|
|
74
|
+
metadata["key_scope"] = effective_scope
|
|
75
|
+
return metadata
|
|
62
76
|
|
|
63
77
|
|
|
64
78
|
def _resolve_effective_network(network: str | None = None) -> str:
|
|
@@ -126,12 +140,25 @@ def _load_user_wallet_secret_material(
|
|
|
126
140
|
if not is_encrypted_wallet_payload(raw_text):
|
|
127
141
|
return raw_text, "plaintext", None
|
|
128
142
|
|
|
129
|
-
|
|
130
|
-
for key_scope, candidate in _candidate_user_wallet_master_keys(
|
|
143
|
+
candidates = _candidate_user_wallet_master_keys(
|
|
131
144
|
user_id,
|
|
132
145
|
network,
|
|
133
146
|
raw_master_key=raw_master_key,
|
|
134
|
-
)
|
|
147
|
+
)
|
|
148
|
+
# Try the scope recorded at encryption time first: a mismatched candidate
|
|
149
|
+
# costs a full (failed) KDF before the right one is attempted. Files
|
|
150
|
+
# without the marker keep the legacy order.
|
|
151
|
+
try:
|
|
152
|
+
import json as _json
|
|
153
|
+
|
|
154
|
+
recorded_scope = (_json.loads(raw_text).get("metadata") or {}).get("key_scope")
|
|
155
|
+
except Exception:
|
|
156
|
+
recorded_scope = None
|
|
157
|
+
if isinstance(recorded_scope, str) and recorded_scope:
|
|
158
|
+
candidates.sort(key=lambda item: item[0] != recorded_scope)
|
|
159
|
+
|
|
160
|
+
last_exc: WalletBackendError | None = None
|
|
161
|
+
for key_scope, candidate in candidates:
|
|
135
162
|
try:
|
|
136
163
|
return (
|
|
137
164
|
decrypt_secret_material(raw_text, master_key=candidate),
|
|
@@ -147,6 +174,58 @@ def _load_user_wallet_secret_material(
|
|
|
147
174
|
)
|
|
148
175
|
|
|
149
176
|
|
|
177
|
+
def _maybe_migrate_wallet_envelope_kdf(
|
|
178
|
+
path: Path,
|
|
179
|
+
secret_material: str,
|
|
180
|
+
*,
|
|
181
|
+
user_id: str,
|
|
182
|
+
network: str,
|
|
183
|
+
key_scope: str | None,
|
|
184
|
+
address: str,
|
|
185
|
+
) -> None:
|
|
186
|
+
"""Lazily rewrite an argon2id wallet envelope as hkdf-sha256.
|
|
187
|
+
|
|
188
|
+
Re-encrypts with the SAME key scope that just decrypted the file, so this
|
|
189
|
+
never performs a scope migration through the back door. Best-effort: any
|
|
190
|
+
failure leaves the (still readable) argon2id file untouched. Kill switch:
|
|
191
|
+
AGENT_WALLET_ENVELOPE_KDF_MIGRATION=0.
|
|
192
|
+
"""
|
|
193
|
+
from agent_wallet.config import envelope_kdf_migration_enabled
|
|
194
|
+
from agent_wallet.encrypted_storage import (
|
|
195
|
+
KDF_ARGON2ID,
|
|
196
|
+
_default_write_kdf,
|
|
197
|
+
envelope_kdf,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
try:
|
|
201
|
+
if not envelope_kdf_migration_enabled():
|
|
202
|
+
return
|
|
203
|
+
if _default_write_kdf() == KDF_ARGON2ID:
|
|
204
|
+
return # forced-argon2id installs must not rewrite in place forever
|
|
205
|
+
if key_scope not in {"per-user-derived", "global-master"}:
|
|
206
|
+
return
|
|
207
|
+
if envelope_kdf(path.read_text(encoding="utf-8")) != KDF_ARGON2ID:
|
|
208
|
+
return
|
|
209
|
+
if key_scope == "per-user-derived":
|
|
210
|
+
master_key = _derive_user_scoped_key(
|
|
211
|
+
resolve_wallet_master_key(), user_id=user_id, network=network
|
|
212
|
+
)
|
|
213
|
+
else:
|
|
214
|
+
master_key = resolve_wallet_master_key()
|
|
215
|
+
if not master_key.strip():
|
|
216
|
+
return
|
|
217
|
+
write_encrypted_wallet_file(
|
|
218
|
+
path,
|
|
219
|
+
secret_material,
|
|
220
|
+
master_key=master_key,
|
|
221
|
+
metadata=_user_wallet_metadata(
|
|
222
|
+
user_id, address, network=network, key_scope=key_scope
|
|
223
|
+
),
|
|
224
|
+
)
|
|
225
|
+
except Exception:
|
|
226
|
+
pass
|
|
227
|
+
|
|
228
|
+
|
|
150
229
|
def ensure_user_solana_wallet(
|
|
151
230
|
user_id: str,
|
|
152
231
|
network: str | None = None,
|
|
@@ -206,6 +285,15 @@ def ensure_user_solana_wallet(
|
|
|
206
285
|
metadata=_user_wallet_metadata(user_id, signer.address, network=effective_network),
|
|
207
286
|
)
|
|
208
287
|
key_scope = "per-user-derived"
|
|
288
|
+
elif storage_format == "encrypted":
|
|
289
|
+
_maybe_migrate_wallet_envelope_kdf(
|
|
290
|
+
path,
|
|
291
|
+
secret_material,
|
|
292
|
+
user_id=user_id,
|
|
293
|
+
network=effective_network,
|
|
294
|
+
key_scope=key_scope,
|
|
295
|
+
address=signer.address,
|
|
296
|
+
)
|
|
209
297
|
return {
|
|
210
298
|
"user_id": user_id,
|
|
211
299
|
"address": signer.address,
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "agent-wallet",
|
|
3
3
|
"name": "Agent Wallet",
|
|
4
4
|
"description": "Plugin-friendly wallet backend for OpenClaw agents with safe wallet tools and runtime instructions across Solana, local BTC, and local EVM.",
|
|
5
|
-
"version": "0.1.
|
|
5
|
+
"version": "0.1.65",
|
|
6
6
|
"skills": ["skills/wallet-operator"],
|
|
7
7
|
"configSchema": {
|
|
8
8
|
"type": "object",
|
|
@@ -19,15 +19,23 @@ INCLUDED_ROOT_FILES = [
|
|
|
19
19
|
"LICENSE",
|
|
20
20
|
"README.md",
|
|
21
21
|
"RELEASING.md",
|
|
22
|
+
"VERSION",
|
|
22
23
|
"install-from-github.sh",
|
|
23
24
|
"requirements.txt",
|
|
24
25
|
"setup.sh",
|
|
25
26
|
]
|
|
27
|
+
# Must cover every setup.sh require_path and mirror the npm package `files`
|
|
28
|
+
# allowlist (RELEASING.md "What Ships") — the bundle is the same runtime users
|
|
29
|
+
# get from npx, just delivered as a GitHub Release asset.
|
|
26
30
|
INCLUDED_TOP_LEVEL_DIRS = [
|
|
27
|
-
"
|
|
31
|
+
".claude-plugin",
|
|
28
32
|
".openclaw",
|
|
29
|
-
"agent-a2a-gateway",
|
|
30
33
|
"agent-wallet",
|
|
34
|
+
"bin",
|
|
35
|
+
"claude-code",
|
|
36
|
+
"codex",
|
|
37
|
+
"hermes",
|
|
38
|
+
"scripts",
|
|
31
39
|
"wdk-btc-wallet",
|
|
32
40
|
"wdk-evm-wallet",
|
|
33
41
|
]
|
|
@@ -23,6 +23,7 @@ const TELEMETRY_FLUSH_THROTTLE_SECONDS = 20;
|
|
|
23
23
|
const TELEMETRY_FORCE_LINES = 25;
|
|
24
24
|
const TELEMETRY_MAX_EVENTS_PER_FLUSH = 100;
|
|
25
25
|
const TELEMETRY_HTTP_TIMEOUT_MS = 1500;
|
|
26
|
+
const KEYSTORE_BRIDGE_TIMEOUT_MS = 30000;
|
|
26
27
|
|
|
27
28
|
function printHelp() {
|
|
28
29
|
console.log(`openclaw-agent-wallet
|
|
@@ -148,6 +149,11 @@ function telemetrySource(env = process.env) {
|
|
|
148
149
|
: "global_cli";
|
|
149
150
|
}
|
|
150
151
|
|
|
152
|
+
function positiveIntEnv(name, fallback, env = process.env) {
|
|
153
|
+
const parsed = Number.parseInt(String(env[name] || ""), 10);
|
|
154
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
155
|
+
}
|
|
156
|
+
|
|
151
157
|
function telemetryGatewayUrl(env = process.env) {
|
|
152
158
|
return String(env.PROVIDER_GATEWAY_URL || DEFAULT_PROVIDER_GATEWAY_URL).trim().replace(/\/+$/, "");
|
|
153
159
|
}
|
|
@@ -900,6 +906,60 @@ function ensureBootKeyFile(env = process.env) {
|
|
|
900
906
|
return { path: keyFile, status: "created" };
|
|
901
907
|
}
|
|
902
908
|
|
|
909
|
+
// Read the boot key from the OS keystore via the current runtime's Python
|
|
910
|
+
// (best-effort, "" on any failure). Lets a re-install after the runtime migration
|
|
911
|
+
// — which moves the key into the keystore and deletes every plaintext copy — still
|
|
912
|
+
// resolve the existing boot key instead of refusing to touch sealed secrets.
|
|
913
|
+
function readBootKeyFromKeystore(env = process.env) {
|
|
914
|
+
const runtimeRoot = resolvedCurrentRuntimeRoot(env);
|
|
915
|
+
if (!runtimeRoot) return "";
|
|
916
|
+
const py = resolveVenvPython(runtimeRoot);
|
|
917
|
+
if (!py) return "";
|
|
918
|
+
try {
|
|
919
|
+
const res = spawnSync(
|
|
920
|
+
py,
|
|
921
|
+
["-c", "from agent_wallet.config import read_boot_key_from_keystore as r; print(r())"],
|
|
922
|
+
{
|
|
923
|
+
cwd: path.join(runtimeRoot, "agent-wallet"),
|
|
924
|
+
encoding: "utf8",
|
|
925
|
+
timeout: positiveIntEnv("AGENT_WALLET_KEYSTORE_BRIDGE_TIMEOUT_MS", KEYSTORE_BRIDGE_TIMEOUT_MS, env),
|
|
926
|
+
env: { ...env, OPENCLAW_HOME: resolveOpenclawHome(env) },
|
|
927
|
+
},
|
|
928
|
+
);
|
|
929
|
+
if ((res.status ?? 1) !== 0) return "";
|
|
930
|
+
return String(res.stdout || "").trim();
|
|
931
|
+
} catch {
|
|
932
|
+
return "";
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
// Provision the boot key into the OS keystore via the freshly installed runtime's
|
|
937
|
+
// Python. Returns true only when the import stored AND verified the key, so the
|
|
938
|
+
// caller may safely drop the plaintext .env copy. Best-effort: false on any failure
|
|
939
|
+
// (e.g. no usable keystore), in which case the caller keeps the legacy .env write.
|
|
940
|
+
function provisionBootKeyToKeystore(releaseRoot, env, bootKey) {
|
|
941
|
+
const key = String(bootKey || "").trim();
|
|
942
|
+
if (!key) return false;
|
|
943
|
+
const py = resolveVenvPython(releaseRoot);
|
|
944
|
+
if (!py) return false;
|
|
945
|
+
try {
|
|
946
|
+
const res = spawnSync(
|
|
947
|
+
py,
|
|
948
|
+
["-m", "agent_wallet.openclaw_cli", "boot-key-import", "--key-stdin"],
|
|
949
|
+
{
|
|
950
|
+
cwd: path.join(releaseRoot, "agent-wallet"),
|
|
951
|
+
input: key,
|
|
952
|
+
encoding: "utf8",
|
|
953
|
+
timeout: positiveIntEnv("AGENT_WALLET_KEYSTORE_BRIDGE_TIMEOUT_MS", KEYSTORE_BRIDGE_TIMEOUT_MS, env),
|
|
954
|
+
env: { ...env, OPENCLAW_HOME: resolveOpenclawHome(env) },
|
|
955
|
+
},
|
|
956
|
+
);
|
|
957
|
+
return (res.status ?? 1) === 0;
|
|
958
|
+
} catch {
|
|
959
|
+
return false;
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
|
|
903
963
|
function resolveVenvPython(releaseRoot) {
|
|
904
964
|
const candidates = [
|
|
905
965
|
path.join(releaseRoot, "agent-wallet", ".venv", "bin", "python"),
|
|
@@ -1169,7 +1229,8 @@ function buildInstallerEnv(args) {
|
|
|
1169
1229
|
const existingBootKey =
|
|
1170
1230
|
resolveBootKeyFromFile(env) ||
|
|
1171
1231
|
readTextIfExists(defaultBootKeyFile(env)).trim() ||
|
|
1172
|
-
currentBootKey(env)
|
|
1232
|
+
currentBootKey(env) ||
|
|
1233
|
+
readBootKeyFromKeystore(env);
|
|
1173
1234
|
if (existingBootKey) {
|
|
1174
1235
|
env.AGENT_WALLET_BOOT_KEY = existingBootKey;
|
|
1175
1236
|
}
|
|
@@ -1318,9 +1379,17 @@ function runInstall(args, { commandName = "install" } = {}) {
|
|
|
1318
1379
|
}
|
|
1319
1380
|
|
|
1320
1381
|
if (env.AGENT_WALLET_BOOT_KEY) {
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1382
|
+
const envPath = path.join(releaseRoot, "agent-wallet", ".env");
|
|
1383
|
+
// Prefer the OS keystore: provision the boot key there and keep the plaintext
|
|
1384
|
+
// out of the release .env entirely. Only fall back to the legacy .env write when
|
|
1385
|
+
// no keystore round-trip is possible, so the runtime can still resolve the key.
|
|
1386
|
+
if (provisionBootKeyToKeystore(releaseRoot, env, env.AGENT_WALLET_BOOT_KEY)) {
|
|
1387
|
+
envFileUnset(envPath, ["AGENT_WALLET_BOOT_KEY"]);
|
|
1388
|
+
} else {
|
|
1389
|
+
envFileSet(envPath, {
|
|
1390
|
+
AGENT_WALLET_BOOT_KEY: env.AGENT_WALLET_BOOT_KEY,
|
|
1391
|
+
});
|
|
1392
|
+
}
|
|
1324
1393
|
}
|
|
1325
1394
|
|
|
1326
1395
|
const pythonInfo = activePythonRuntimeInfo(env);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-wallet",
|
|
3
3
|
"displayName": "Agent Wallet",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.65",
|
|
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"
|
|
@@ -78,6 +78,9 @@ APPROVAL_CONTEXT_MISSING_MESSAGE = (
|
|
|
78
78
|
"operation again, wait for explicit user confirmation, then retry execute. Do not ask the "
|
|
79
79
|
"user for a manual approval token."
|
|
80
80
|
)
|
|
81
|
+
# Tools served by the long-lived resident read worker instead of a cold CLI
|
|
82
|
+
# subprocess. Seeded with the legacy floor; _build_tool_definitions() extends
|
|
83
|
+
# it with every adapter-declared read-only tool at server start.
|
|
81
84
|
RESIDENT_READ_ONLY_TOOLS = {
|
|
82
85
|
"get_wallet_balance",
|
|
83
86
|
"get_wallet_portfolio",
|
|
@@ -622,20 +625,31 @@ def _call_wallet_cli(command: str, extra_args: list[str]) -> dict[str, Any]:
|
|
|
622
625
|
raise WalletCliError(f"agent-wallet CLI returned invalid JSON: {exc}") from exc
|
|
623
626
|
|
|
624
627
|
|
|
625
|
-
def _invoke_tool(
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
628
|
+
def _invoke_tool(
|
|
629
|
+
tool_name: str,
|
|
630
|
+
arguments: dict[str, Any],
|
|
631
|
+
config: dict[str, Any],
|
|
632
|
+
*,
|
|
633
|
+
approval_summary: dict[str, Any] | None = None,
|
|
634
|
+
approval_mainnet_confirmed: bool = False,
|
|
635
|
+
) -> dict[str, Any]:
|
|
636
|
+
extra_args = [
|
|
637
|
+
"--user-id",
|
|
638
|
+
_user_id(),
|
|
639
|
+
"--tool",
|
|
640
|
+
tool_name,
|
|
641
|
+
"--arguments-json",
|
|
642
|
+
json.dumps(arguments),
|
|
643
|
+
"--config-json",
|
|
644
|
+
json.dumps(config),
|
|
645
|
+
]
|
|
646
|
+
if approval_summary is not None:
|
|
647
|
+
# The invoke subprocess mints the approval token itself, so an execute
|
|
648
|
+
# costs one cold start instead of two (issue-approval + invoke).
|
|
649
|
+
extra_args.extend(["--approval-summary-json", json.dumps(approval_summary)])
|
|
650
|
+
if approval_mainnet_confirmed:
|
|
651
|
+
extra_args.append("--approval-mainnet-confirmed")
|
|
652
|
+
return _call_wallet_cli("invoke", extra_args)
|
|
639
653
|
|
|
640
654
|
|
|
641
655
|
class _ResidentReadWorker:
|
|
@@ -943,33 +957,16 @@ def _prewarm_resident_read_worker() -> None:
|
|
|
943
957
|
).start()
|
|
944
958
|
|
|
945
959
|
|
|
946
|
-
def
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
) -> str:
|
|
960
|
+
def _approval_summary_for_preview(tool_name: str, preview_payload: dict[str, Any]) -> dict[str, Any]:
|
|
961
|
+
"""Build the digest-bound confirmation summary the invoke subprocess will
|
|
962
|
+
mint an approval token from (in-process, replacing the old standalone
|
|
963
|
+
issue-approval subprocess round trip)."""
|
|
951
964
|
summary = preview_payload.get("confirmation_summary")
|
|
952
965
|
if not isinstance(summary, dict):
|
|
953
966
|
raise RuntimeError(f"No confirmation_summary available for {tool_name}.")
|
|
954
967
|
summary_for_token = dict(summary)
|
|
955
968
|
summary_for_token["_preview_digest"] = _preview_digest(preview_payload)
|
|
956
|
-
|
|
957
|
-
"--user-id",
|
|
958
|
-
_user_id(),
|
|
959
|
-
"--tool",
|
|
960
|
-
tool_name,
|
|
961
|
-
"--summary-json",
|
|
962
|
-
json.dumps(summary_for_token),
|
|
963
|
-
"--config-json",
|
|
964
|
-
json.dumps(config),
|
|
965
|
-
]
|
|
966
|
-
if preview_payload.get("is_mainnet") is True:
|
|
967
|
-
extra_args.append("--mainnet-confirmed")
|
|
968
|
-
payload = _call_wallet_cli("issue-approval", extra_args)
|
|
969
|
-
token = str(payload.get("approval_token") or "").strip()
|
|
970
|
-
if not token:
|
|
971
|
-
raise RuntimeError(f"issue-approval did not return an approval_token for {tool_name}.")
|
|
972
|
-
return token
|
|
969
|
+
return summary_for_token
|
|
973
970
|
|
|
974
971
|
|
|
975
972
|
def _invoke_resident_read_tool(tool_name: str, arguments: dict[str, Any], config: dict[str, Any]) -> dict[str, Any]:
|
|
@@ -1041,10 +1038,17 @@ def _attach_approval_for_execute(
|
|
|
1041
1038
|
tool_name: str,
|
|
1042
1039
|
config: dict[str, Any],
|
|
1043
1040
|
effective_params: dict[str, Any],
|
|
1044
|
-
) -> dict[str, Any] | None:
|
|
1041
|
+
) -> tuple[dict[str, Any] | None, dict[str, Any] | None]:
|
|
1042
|
+
"""Prepare approval context for an execute call.
|
|
1043
|
+
|
|
1044
|
+
Returns ``(used_cache, approval_args)``: ``used_cache`` is the consumed
|
|
1045
|
+
bridge-managed preview (if any); ``approval_args`` carries the
|
|
1046
|
+
digest-bound summary the invoke subprocess mints its own token from,
|
|
1047
|
+
so no separate issue-approval subprocess is spawned.
|
|
1048
|
+
"""
|
|
1045
1049
|
mode = str(effective_params.get("mode") or "")
|
|
1046
1050
|
if mode not in {"execute", "intent_execute"}:
|
|
1047
|
-
return None
|
|
1051
|
+
return None, None
|
|
1048
1052
|
if tool_name == "swap_solana_tokens" and mode == "execute":
|
|
1049
1053
|
raise RuntimeError(
|
|
1050
1054
|
"Legacy exact-preview execute is disabled for Solana Jupiter swaps in Codex. "
|
|
@@ -1053,19 +1057,22 @@ def _attach_approval_for_execute(
|
|
|
1053
1057
|
cached = _latest_cached_preview(_user_id(), tool_name)
|
|
1054
1058
|
if cached and isinstance(cached.get("preview"), dict):
|
|
1055
1059
|
preview = cached["preview"]
|
|
1056
|
-
|
|
1060
|
+
approval_args = {
|
|
1061
|
+
"summary": _approval_summary_for_preview(tool_name, preview),
|
|
1062
|
+
"mainnet_confirmed": preview.get("is_mainnet") is True,
|
|
1063
|
+
}
|
|
1057
1064
|
if _requires_approved_preview_payload(tool_name, effective_params):
|
|
1058
1065
|
effective_params["_approved_preview"] = preview
|
|
1059
|
-
return cached
|
|
1066
|
+
return cached, approval_args
|
|
1060
1067
|
approval_token = str(effective_params.get("approval_token") or "").strip()
|
|
1061
1068
|
if approval_token and _requires_approved_preview_payload(tool_name, effective_params):
|
|
1062
1069
|
cached_preview = _cached_preview_for_token(_user_id(), tool_name, approval_token)
|
|
1063
1070
|
if cached_preview is not None and "_approved_preview" not in effective_params:
|
|
1064
1071
|
effective_params["_approved_preview"] = cached_preview
|
|
1065
1072
|
if effective_params.get("approval_token"):
|
|
1066
|
-
return None
|
|
1073
|
+
return None, None
|
|
1067
1074
|
if _should_let_backend_authorize_autonomous_execution(tool_name, effective_params, config):
|
|
1068
|
-
return None
|
|
1075
|
+
return None, None
|
|
1069
1076
|
raise RuntimeError(APPROVAL_CONTEXT_MISSING_MESSAGE)
|
|
1070
1077
|
|
|
1071
1078
|
|
|
@@ -1242,6 +1249,13 @@ def _build_tool_definitions() -> list[dict[str, Any]]:
|
|
|
1242
1249
|
merged.setdefault(spec["name"], spec)
|
|
1243
1250
|
for spec in merged.values():
|
|
1244
1251
|
spec["input_schema"] = _sanitize_schema(spec["input_schema"])
|
|
1252
|
+
if spec.get("read_only") is True:
|
|
1253
|
+
# Route every adapter-declared read-only tool through the resident
|
|
1254
|
+
# read worker: reads then cost one warm request instead of a cold
|
|
1255
|
+
# interpreter boot + onboarding per call. The worker enforces the
|
|
1256
|
+
# same read_only contract on its side, and transport failures fall
|
|
1257
|
+
# back to the cold subprocess path.
|
|
1258
|
+
RESIDENT_READ_ONLY_TOOLS.add(spec["name"])
|
|
1245
1259
|
if spec.get("read_only") is False:
|
|
1246
1260
|
spec["description"] = (
|
|
1247
1261
|
f"{spec['description']} Preview first when supported. Execute reuses cached "
|
|
@@ -1388,9 +1402,15 @@ def _invoke_wallet_tool_blocking(
|
|
|
1388
1402
|
call never freezes the MCP server (tools/list, read-only calls, and
|
|
1389
1403
|
cancellation stay responsive).
|
|
1390
1404
|
"""
|
|
1391
|
-
used_cache = _attach_approval_for_execute(tool_name, config, effective_params)
|
|
1405
|
+
used_cache, approval_args = _attach_approval_for_execute(tool_name, config, effective_params)
|
|
1392
1406
|
try:
|
|
1393
|
-
payload = _invoke_tool(
|
|
1407
|
+
payload = _invoke_tool(
|
|
1408
|
+
tool_name,
|
|
1409
|
+
effective_params,
|
|
1410
|
+
config,
|
|
1411
|
+
approval_summary=(approval_args or {}).get("summary"),
|
|
1412
|
+
approval_mainnet_confirmed=bool((approval_args or {}).get("mainnet_confirmed")),
|
|
1413
|
+
)
|
|
1394
1414
|
except Exception as exc:
|
|
1395
1415
|
raise _normalize_approval_context_error(exc) from exc
|
|
1396
1416
|
_cache_preview_for_approval(_user_id(), tool_name, payload)
|
package/package.json
CHANGED