@agentlayer.tech/wallet 0.1.64 → 0.1.66
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.openclaw/extensions/agent-wallet/openclaw.plugin.json +1 -1
- package/.openclaw/extensions/agent-wallet/package.json +1 -1
- package/VERSION +1 -1
- package/agent-wallet/agent_wallet/__init__.py +1 -1
- package/agent-wallet/agent_wallet/config.py +44 -2
- package/agent-wallet/agent_wallet/encrypted_storage.py +77 -9
- package/agent-wallet/agent_wallet/keystore.py +25 -0
- package/agent-wallet/agent_wallet/openclaw_adapter.py +4 -0
- package/agent-wallet/agent_wallet/openclaw_cli.py +63 -4
- package/agent-wallet/agent_wallet/providers/x402.py +229 -148
- 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/agent-wallet/skills/wallet-operator/SKILL.md +1 -1
- package/claude-code/plugins/agent-wallet/.claude-plugin/plugin.json +1 -1
- package/claude-code/plugins/agent-wallet/skills/wallet-operator/SKILL.md +1 -1
- package/codex/plugins/agent-wallet/.codex-plugin/plugin.json +1 -1
- package/codex/plugins/agent-wallet/server.py +116 -47
- package/codex/plugins/agent-wallet/skills/wallet-operator/SKILL.md +1 -1
- package/hermes/plugins/agent_wallet/plugin.yaml +1 -1
- package/install-from-github.sh +6 -0
- package/package.json +1 -1
- package/wdk-btc-wallet/package.json +1 -1
- package/wdk-evm-wallet/package.json +1 -1
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "agent-wallet",
|
|
3
3
|
"name": "Agent Wallet",
|
|
4
4
|
"description": "Official OpenClaw plugin bridge for the agent-wallet backends, including Solana, local BTC, and local EVM.",
|
|
5
|
-
"version": "0.1.
|
|
5
|
+
"version": "0.1.66",
|
|
6
6
|
"contracts": {
|
|
7
7
|
"tools": [
|
|
8
8
|
"agentlayer_autonomous_approve",
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.1.
|
|
1
|
+
0.1.66
|
|
@@ -80,6 +80,7 @@ def reload_settings() -> Settings:
|
|
|
80
80
|
refreshed = Settings()
|
|
81
81
|
for field_name in Settings.model_fields:
|
|
82
82
|
setattr(settings, field_name, getattr(refreshed, field_name))
|
|
83
|
+
clear_secret_caches()
|
|
83
84
|
return settings
|
|
84
85
|
|
|
85
86
|
|
|
@@ -385,13 +386,54 @@ def _env_bool(name: str, default: bool) -> bool:
|
|
|
385
386
|
return raw.strip().lower() in {"1", "true", "yes", "on"}
|
|
386
387
|
|
|
387
388
|
|
|
389
|
+
def envelope_kdf_migration_enabled() -> bool:
|
|
390
|
+
"""Whether argon2id envelopes are lazily rewritten to hkdf-sha256 after a
|
|
391
|
+
successful decrypt. Disable with AGENT_WALLET_ENVELOPE_KDF_MIGRATION=0
|
|
392
|
+
(e.g. when a rollback to a pre-hkdf runtime must stay possible)."""
|
|
393
|
+
return _env_bool("AGENT_WALLET_ENVELOPE_KDF_MIGRATION", True)
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
_boot_key_keystore_cache: dict[str, str] = {}
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def clear_secret_caches() -> None:
|
|
400
|
+
"""Reset all process-local secret-resolution caches.
|
|
401
|
+
|
|
402
|
+
Covers the memoized keystore backend, the boot key read from the OS
|
|
403
|
+
keystore, unsealed secrets, and derived envelope keys. Wired into
|
|
404
|
+
``reload_settings`` so config reloads always observe fresh state; call it
|
|
405
|
+
directly in tests that rotate keystore contents in-process.
|
|
406
|
+
"""
|
|
407
|
+
_boot_key_keystore_cache.clear()
|
|
408
|
+
from agent_wallet.keystore import clear_keystore_cache
|
|
409
|
+
|
|
410
|
+
clear_keystore_cache()
|
|
411
|
+
from agent_wallet.sealed_keys import clear_unseal_cache
|
|
412
|
+
|
|
413
|
+
clear_unseal_cache()
|
|
414
|
+
from agent_wallet.encrypted_storage import clear_derived_key_cache
|
|
415
|
+
|
|
416
|
+
clear_derived_key_cache()
|
|
417
|
+
|
|
418
|
+
|
|
388
419
|
def read_boot_key_from_keystore() -> str:
|
|
389
|
-
"""Read the boot key from the OS keystore. Never raises; '' on any failure.
|
|
420
|
+
"""Read the boot key from the OS keystore. Never raises; '' on any failure.
|
|
421
|
+
|
|
422
|
+
Successful (non-empty) reads are memoized per keystore service for the
|
|
423
|
+
process lifetime — every uncached read costs a subprocess call.
|
|
424
|
+
"""
|
|
390
425
|
try:
|
|
391
426
|
from agent_wallet.keystore import BOOT_KEY_ITEM, resolve_keystore
|
|
392
427
|
|
|
428
|
+
service_key = os.getenv("AGENT_WALLET_KEYSTORE_SERVICE", "").strip()
|
|
429
|
+
cached = _boot_key_keystore_cache.get(service_key)
|
|
430
|
+
if cached:
|
|
431
|
+
return cached
|
|
393
432
|
value = resolve_keystore().get(BOOT_KEY_ITEM)
|
|
394
|
-
|
|
433
|
+
text = value.strip() if isinstance(value, str) else ""
|
|
434
|
+
if text:
|
|
435
|
+
_boot_key_keystore_cache[service_key] = text
|
|
436
|
+
return text
|
|
395
437
|
except Exception:
|
|
396
438
|
return ""
|
|
397
439
|
|
|
@@ -6,6 +6,7 @@ import base64
|
|
|
6
6
|
import hashlib
|
|
7
7
|
import hmac
|
|
8
8
|
import json
|
|
9
|
+
import os
|
|
9
10
|
from pathlib import Path
|
|
10
11
|
from typing import Any
|
|
11
12
|
|
|
@@ -17,6 +18,10 @@ ENCRYPTED_WALLET_KIND = "openclaw-agent-wallet-secret"
|
|
|
17
18
|
ENCRYPTED_WALLET_VERSION = 1
|
|
18
19
|
USER_SCOPED_KEY_SALT = b"openclaw-agent-wallet-user-key-v1"
|
|
19
20
|
|
|
21
|
+
KDF_ARGON2ID = "argon2id"
|
|
22
|
+
KDF_HKDF_SHA256 = "hkdf-sha256"
|
|
23
|
+
_HKDF_INFO = b"openclaw-agent-wallet-envelope-hkdf-v1"
|
|
24
|
+
|
|
20
25
|
|
|
21
26
|
def _load_secretbox():
|
|
22
27
|
try:
|
|
@@ -28,11 +33,19 @@ def _load_secretbox():
|
|
|
28
33
|
return pwhash, secret, utils
|
|
29
34
|
|
|
30
35
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
+
# Derived envelope keys are expensive (argon2id ~1s each), so cache them by
|
|
37
|
+
# (kdf, master_key, salt). In-memory only, never serialized; bounded so a
|
|
38
|
+
# pathological caller cannot grow it without limit.
|
|
39
|
+
_derived_key_cache: dict[tuple[str, str, bytes], bytes] = {}
|
|
40
|
+
_DERIVED_KEY_CACHE_MAX = 64
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def clear_derived_key_cache() -> None:
|
|
44
|
+
"""Drop cached derived keys (wired into config.clear_secret_caches)."""
|
|
45
|
+
_derived_key_cache.clear()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _kdf_argon2id(master_key: str, salt: bytes) -> bytes:
|
|
36
49
|
pwhash, secret, _ = _load_secretbox()
|
|
37
50
|
return pwhash.argon2id.kdf(
|
|
38
51
|
secret.SecretBox.KEY_SIZE,
|
|
@@ -43,6 +56,52 @@ def _derive_key(master_key: str, salt: bytes) -> bytes:
|
|
|
43
56
|
)
|
|
44
57
|
|
|
45
58
|
|
|
59
|
+
def _kdf_hkdf_sha256(master_key: str, salt: bytes) -> bytes:
|
|
60
|
+
# RFC 5869 extract+expand, one 32-byte block. Only safe because every
|
|
61
|
+
# caller passes machine-generated high-entropy key material (boot key /
|
|
62
|
+
# sealed master key) — password-hardness is what argon2id was paying for,
|
|
63
|
+
# and user passwords never reach this module.
|
|
64
|
+
prk = hmac.new(salt, master_key.encode("utf-8"), hashlib.sha256).digest()
|
|
65
|
+
return hmac.new(prk, _HKDF_INFO + b"\x01", hashlib.sha256).digest()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _default_write_kdf() -> str:
|
|
69
|
+
raw = os.getenv("AGENT_WALLET_ENVELOPE_KDF", "").strip().lower()
|
|
70
|
+
return KDF_ARGON2ID if raw == KDF_ARGON2ID else KDF_HKDF_SHA256
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def envelope_kdf(raw_text: str) -> str:
|
|
74
|
+
"""Return the kdf recorded in an encrypted envelope ('' when not one)."""
|
|
75
|
+
try:
|
|
76
|
+
payload = json.loads(raw_text)
|
|
77
|
+
except json.JSONDecodeError:
|
|
78
|
+
return ""
|
|
79
|
+
if not isinstance(payload, dict) or payload.get("kind") != ENCRYPTED_WALLET_KIND:
|
|
80
|
+
return ""
|
|
81
|
+
return str(payload.get("kdf") or KDF_ARGON2ID)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _derive_key(master_key: str, salt: bytes, *, kdf: str = KDF_ARGON2ID) -> bytes:
|
|
85
|
+
if not master_key.strip():
|
|
86
|
+
raise WalletBackendError(
|
|
87
|
+
"Encrypted wallet storage requires AGENT_WALLET_BOOT_KEY and a sealed master_key."
|
|
88
|
+
)
|
|
89
|
+
if kdf == KDF_HKDF_SHA256:
|
|
90
|
+
# Cheap enough to skip the cache entirely.
|
|
91
|
+
return _kdf_hkdf_sha256(master_key, bytes(salt))
|
|
92
|
+
if kdf != KDF_ARGON2ID:
|
|
93
|
+
raise WalletBackendError(f"Encrypted wallet file uses an unsupported kdf: {kdf}")
|
|
94
|
+
cache_key = (kdf, master_key, bytes(salt))
|
|
95
|
+
cached = _derived_key_cache.get(cache_key)
|
|
96
|
+
if cached is not None:
|
|
97
|
+
return cached
|
|
98
|
+
key = _kdf_argon2id(master_key, salt)
|
|
99
|
+
if len(_derived_key_cache) >= _DERIVED_KEY_CACHE_MAX:
|
|
100
|
+
_derived_key_cache.clear()
|
|
101
|
+
_derived_key_cache[cache_key] = key
|
|
102
|
+
return key
|
|
103
|
+
|
|
104
|
+
|
|
46
105
|
def _derive_user_scoped_key(
|
|
47
106
|
master_key: str,
|
|
48
107
|
*,
|
|
@@ -79,19 +138,27 @@ def encrypt_secret_material(
|
|
|
79
138
|
*,
|
|
80
139
|
master_key: str | None = None,
|
|
81
140
|
metadata: dict[str, Any] | None = None,
|
|
141
|
+
kdf: str | None = None,
|
|
82
142
|
) -> str:
|
|
83
|
-
"""Encrypt wallet secret material into a JSON envelope.
|
|
143
|
+
"""Encrypt wallet secret material into a JSON envelope.
|
|
144
|
+
|
|
145
|
+
``kdf`` selects the key derivation for this envelope; None resolves the
|
|
146
|
+
process default (hkdf-sha256 unless AGENT_WALLET_ENVELOPE_KDF=argon2id).
|
|
147
|
+
"""
|
|
148
|
+
effective_kdf = kdf if kdf is not None else _default_write_kdf()
|
|
149
|
+
if effective_kdf not in {KDF_ARGON2ID, KDF_HKDF_SHA256}:
|
|
150
|
+
raise WalletBackendError(f"Unsupported envelope kdf: {effective_kdf}")
|
|
84
151
|
_, secret, utils = _load_secretbox()
|
|
85
152
|
effective_master_key = master_key if master_key is not None else resolve_wallet_master_key()
|
|
86
153
|
salt = utils.random(16)
|
|
87
|
-
key = _derive_key(effective_master_key, salt)
|
|
154
|
+
key = _derive_key(effective_master_key, salt, kdf=effective_kdf)
|
|
88
155
|
box = secret.SecretBox(key)
|
|
89
156
|
encrypted = box.encrypt(secret_material.encode("utf-8"))
|
|
90
157
|
payload: dict[str, Any] = {
|
|
91
158
|
"kind": ENCRYPTED_WALLET_KIND,
|
|
92
159
|
"version": ENCRYPTED_WALLET_VERSION,
|
|
93
160
|
"cipher": "secretbox",
|
|
94
|
-
"kdf":
|
|
161
|
+
"kdf": effective_kdf,
|
|
95
162
|
"salt_b64": base64.b64encode(salt).decode("ascii"),
|
|
96
163
|
"nonce_b64": base64.b64encode(encrypted.nonce).decode("ascii"),
|
|
97
164
|
"ciphertext_b64": base64.b64encode(encrypted.ciphertext).decode("ascii"),
|
|
@@ -124,7 +191,8 @@ def decrypt_secret_material(
|
|
|
124
191
|
raise WalletBackendError("Encrypted wallet file is malformed.") from exc
|
|
125
192
|
|
|
126
193
|
effective_master_key = master_key if master_key is not None else resolve_wallet_master_key()
|
|
127
|
-
|
|
194
|
+
kdf = str(payload.get("kdf") or KDF_ARGON2ID)
|
|
195
|
+
key = _derive_key(effective_master_key, salt, kdf=kdf)
|
|
128
196
|
box = secret.SecretBox(key)
|
|
129
197
|
try:
|
|
130
198
|
plaintext = box.decrypt(ciphertext, nonce)
|
|
@@ -247,7 +247,32 @@ class PlaintextFileStore:
|
|
|
247
247
|
pass
|
|
248
248
|
|
|
249
249
|
|
|
250
|
+
_keystore_cache: dict[tuple[str, str], KeyStore] = {}
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def clear_keystore_cache() -> None:
|
|
254
|
+
"""Drop the memoized keystore backend (e.g. after env/service changes)."""
|
|
255
|
+
_keystore_cache.clear()
|
|
256
|
+
|
|
257
|
+
|
|
250
258
|
def resolve_keystore() -> KeyStore:
|
|
259
|
+
"""Memoized keystore resolution.
|
|
260
|
+
|
|
261
|
+
Resolving probes the OS keystore with subprocess calls (`security`,
|
|
262
|
+
`secret-tool`, powershell), so cache the winning backend per
|
|
263
|
+
(preference, service) for the process lifetime. Cleared by
|
|
264
|
+
``agent_wallet.config.clear_secret_caches`` / ``reload_settings``.
|
|
265
|
+
"""
|
|
266
|
+
cache_key = (_backend_preference(), _service())
|
|
267
|
+
cached = _keystore_cache.get(cache_key)
|
|
268
|
+
if cached is not None:
|
|
269
|
+
return cached
|
|
270
|
+
store = _resolve_keystore_uncached()
|
|
271
|
+
_keystore_cache[cache_key] = store
|
|
272
|
+
return store
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _resolve_keystore_uncached() -> KeyStore:
|
|
251
276
|
"""Return the first available OS-native backend, else the plaintext fallback."""
|
|
252
277
|
preference = _backend_preference()
|
|
253
278
|
if preference in {"plain", "plaintext", "plaintext-file", "file"}:
|
|
@@ -3928,6 +3928,9 @@ class OpenClawWalletAdapter:
|
|
|
3928
3928
|
raise WalletBackendError("text_body must be a string when provided.")
|
|
3929
3929
|
if not isinstance(purpose, str) or not purpose.strip():
|
|
3930
3930
|
raise WalletBackendError("purpose is required.")
|
|
3931
|
+
approved_preview = args.get("_approved_preview")
|
|
3932
|
+
if approved_preview is not None and not isinstance(approved_preview, dict):
|
|
3933
|
+
raise WalletBackendError("_approved_preview must be an object when provided.")
|
|
3931
3934
|
data = await x402.pay_and_fetch(
|
|
3932
3935
|
backend=active_backend,
|
|
3933
3936
|
url=url.strip(),
|
|
@@ -3936,6 +3939,7 @@ class OpenClawWalletAdapter:
|
|
|
3936
3939
|
query=query,
|
|
3937
3940
|
json_body=json_body,
|
|
3938
3941
|
text_body=text_body,
|
|
3942
|
+
approved_preview=approved_preview,
|
|
3939
3943
|
)
|
|
3940
3944
|
data["purpose"] = purpose.strip()
|
|
3941
3945
|
data = self._annotate_x402_payload(data, mode="execute")
|
|
@@ -33,6 +33,8 @@ def _parse_csv(value: Any) -> str:
|
|
|
33
33
|
|
|
34
34
|
|
|
35
35
|
SECRET_CONFIG_KEYS = {"privateKey", "masterKey", "approvalSecret"}
|
|
36
|
+
# Always-allowed floor kept for compatibility; the live gate is the dynamic
|
|
37
|
+
# set of adapter-declared read-only tools (_read_only_tool_names).
|
|
36
38
|
READ_ONLY_WORKER_TOOLS = frozenset(
|
|
37
39
|
{
|
|
38
40
|
"get_wallet_balance",
|
|
@@ -41,6 +43,20 @@ READ_ONLY_WORKER_TOOLS = frozenset(
|
|
|
41
43
|
)
|
|
42
44
|
|
|
43
45
|
|
|
46
|
+
def _read_only_tool_names(context: Any) -> frozenset[str]:
|
|
47
|
+
"""All tools the resident read worker may serve: the adapter's read-only
|
|
48
|
+
specs for this backend, plus the legacy floor."""
|
|
49
|
+
names = set(READ_ONLY_WORKER_TOOLS)
|
|
50
|
+
try:
|
|
51
|
+
for tool in context.adapter.list_tools():
|
|
52
|
+
spec = tool.model_dump() if hasattr(tool, "model_dump") else dict(tool)
|
|
53
|
+
if spec.get("read_only") is True and str(spec.get("name") or "").strip():
|
|
54
|
+
names.add(str(spec["name"]))
|
|
55
|
+
except Exception:
|
|
56
|
+
pass
|
|
57
|
+
return frozenset(names)
|
|
58
|
+
|
|
59
|
+
|
|
44
60
|
def _reject_secret_config_json(config: dict[str, Any]) -> None:
|
|
45
61
|
present = sorted(key for key in SECRET_CONFIG_KEYS if str(config.get(key) or "").strip())
|
|
46
62
|
if present:
|
|
@@ -180,8 +196,27 @@ async def _run_invoke(
|
|
|
180
196
|
tool_name: str,
|
|
181
197
|
arguments: dict[str, Any],
|
|
182
198
|
config: dict[str, Any],
|
|
199
|
+
*,
|
|
200
|
+
approval_summary: dict[str, Any] | None = None,
|
|
201
|
+
approval_mainnet_confirmed: bool = False,
|
|
202
|
+
approval_ttl_seconds: int | None = None,
|
|
183
203
|
) -> dict[str, Any]:
|
|
184
204
|
context = _build_runtime_context(user_id, config)
|
|
205
|
+
# Mint the approval token in-process when the host passed the confirmation
|
|
206
|
+
# summary along with the execute call: this replaces a second cold
|
|
207
|
+
# `issue-approval` subprocess (full interpreter boot + onboarding) with a
|
|
208
|
+
# local HMAC signature. A caller-provided token always wins.
|
|
209
|
+
if (
|
|
210
|
+
isinstance(approval_summary, dict)
|
|
211
|
+
and not str(arguments.get("approval_token") or "").strip()
|
|
212
|
+
):
|
|
213
|
+
arguments = dict(arguments)
|
|
214
|
+
arguments["approval_token"] = context.issue_execute_approval(
|
|
215
|
+
tool_name=tool_name,
|
|
216
|
+
confirmation_summary=approval_summary,
|
|
217
|
+
mainnet_confirmed=approval_mainnet_confirmed,
|
|
218
|
+
ttl_seconds=approval_ttl_seconds,
|
|
219
|
+
)
|
|
185
220
|
result = await context.adapter.invoke(tool_name, arguments)
|
|
186
221
|
return result.model_dump()
|
|
187
222
|
|
|
@@ -253,11 +288,16 @@ def _error_payload_for_exception(exc: Exception) -> dict[str, Any]:
|
|
|
253
288
|
return payload
|
|
254
289
|
|
|
255
290
|
|
|
256
|
-
async def _run_read_worker_tool(
|
|
291
|
+
async def _run_read_worker_tool(
|
|
292
|
+
context: Any,
|
|
293
|
+
request: dict[str, Any],
|
|
294
|
+
allowed_tools: frozenset[str] | None = None,
|
|
295
|
+
) -> dict[str, Any]:
|
|
257
296
|
tool_name = str(request.get("tool") or "").strip()
|
|
258
|
-
if
|
|
297
|
+
effective_allowed = allowed_tools if allowed_tools is not None else READ_ONLY_WORKER_TOOLS
|
|
298
|
+
if tool_name not in effective_allowed:
|
|
259
299
|
raise WalletBackendError(
|
|
260
|
-
f"read-worker only allows read-only tools
|
|
300
|
+
f"read-worker only allows read-only tools for this backend; '{tool_name}' is not one."
|
|
261
301
|
)
|
|
262
302
|
arguments = request.get("arguments") or {}
|
|
263
303
|
if not isinstance(arguments, dict):
|
|
@@ -268,6 +308,7 @@ async def _run_read_worker_tool(context: Any, request: dict[str, Any]) -> dict[s
|
|
|
268
308
|
|
|
269
309
|
async def _serve_read_worker(user_id: str, config: dict[str, Any]) -> int:
|
|
270
310
|
context = _build_runtime_context(user_id, config, read_only=True)
|
|
311
|
+
allowed_tools = _read_only_tool_names(context)
|
|
271
312
|
while True:
|
|
272
313
|
line = sys.stdin.readline()
|
|
273
314
|
if not line:
|
|
@@ -282,7 +323,7 @@ async def _serve_read_worker(user_id: str, config: dict[str, Any]) -> int:
|
|
|
282
323
|
if request.get("op") == "shutdown":
|
|
283
324
|
print(json.dumps({"id": request_id, "ok": True, "stopped": True}), flush=True)
|
|
284
325
|
break
|
|
285
|
-
payload = await _run_read_worker_tool(context, request)
|
|
326
|
+
payload = await _run_read_worker_tool(context, request, allowed_tools)
|
|
286
327
|
print(
|
|
287
328
|
json.dumps(
|
|
288
329
|
{
|
|
@@ -510,6 +551,15 @@ def main() -> int:
|
|
|
510
551
|
invoke_parser.add_argument("--tool", required=True)
|
|
511
552
|
invoke_parser.add_argument("--arguments-json", default="{}")
|
|
512
553
|
invoke_parser.add_argument("--config-json", default="{}")
|
|
554
|
+
invoke_parser.add_argument(
|
|
555
|
+
"--approval-summary-json",
|
|
556
|
+
help=(
|
|
557
|
+
"Confirmation summary for execute calls; when provided, the approval "
|
|
558
|
+
"token is minted in this process instead of a separate issue-approval run."
|
|
559
|
+
),
|
|
560
|
+
)
|
|
561
|
+
invoke_parser.add_argument("--approval-mainnet-confirmed", action="store_true")
|
|
562
|
+
invoke_parser.add_argument("--approval-ttl-seconds", type=int)
|
|
513
563
|
|
|
514
564
|
approval_parser = subparsers.add_parser("issue-approval")
|
|
515
565
|
approval_parser.add_argument("--user-id", required=True)
|
|
@@ -726,12 +776,21 @@ def main() -> int:
|
|
|
726
776
|
|
|
727
777
|
payload = import_boot_key(_read_stdin_secret("boot key"))
|
|
728
778
|
else:
|
|
779
|
+
approval_summary = None
|
|
780
|
+
raw_summary = getattr(args, "approval_summary_json", None)
|
|
781
|
+
if raw_summary:
|
|
782
|
+
approval_summary = _load_json(raw_summary)
|
|
729
783
|
payload = asyncio.run(
|
|
730
784
|
_run_invoke(
|
|
731
785
|
args.user_id,
|
|
732
786
|
args.tool,
|
|
733
787
|
_load_json(args.arguments_json),
|
|
734
788
|
config,
|
|
789
|
+
approval_summary=approval_summary,
|
|
790
|
+
approval_mainnet_confirmed=bool(
|
|
791
|
+
getattr(args, "approval_mainnet_confirmed", False)
|
|
792
|
+
),
|
|
793
|
+
approval_ttl_seconds=getattr(args, "approval_ttl_seconds", None),
|
|
735
794
|
)
|
|
736
795
|
)
|
|
737
796
|
except Exception as exc:
|