@agentlayer.tech/wallet 0.1.18 → 0.1.19
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/AGENTS.md +0 -7
- package/.openclaw/extensions/agent-wallet/README.md +3 -2
- package/.openclaw/extensions/agent-wallet/package.json +1 -1
- package/README.md +1 -3
- package/RELEASING.md +5 -15
- package/agent-wallet/README.md +3 -0
- package/agent-wallet/agent_wallet/config.py +11 -0
- package/agent-wallet/agent_wallet/evm_user_wallets.py +310 -2
- package/agent-wallet/agent_wallet/openclaw_runtime.py +10 -41
- package/agent-wallet/agent_wallet/providers/wdk_evm_local.py +52 -0
- package/agent-wallet/pyproject.toml +1 -1
- package/agent-wallet/scripts/build_release_bundle.py +1 -0
- package/agent-wallet/scripts/install_agent_wallet.py +3 -0
- package/agent-wallet/scripts/install_openclaw_local_config.py +20 -51
- package/agent-wallet/scripts/install_openclaw_sealed_keys.py +9 -1
- package/package.json +1 -2
- package/.openclaw/extensions/pay-bridge/README.md +0 -38
- package/.openclaw/extensions/pay-bridge/core.mjs +0 -287
- package/.openclaw/extensions/pay-bridge/dist/core.mjs +0 -287
- package/.openclaw/extensions/pay-bridge/dist/index.js +0 -196
- package/.openclaw/extensions/pay-bridge/index.ts +0 -196
- package/.openclaw/extensions/pay-bridge/openclaw.plugin.json +0 -34
- package/.openclaw/extensions/pay-bridge/package.json +0 -49
- package/.openclaw/extensions/pay-bridge/skills/pay-operator/SKILL.md +0 -20
- package/.openclaw/extensions/pay-bridge/smoke_pay_bridge.mjs +0 -38
|
@@ -102,6 +102,35 @@ def _unwrap_payload(response: httpx.Response) -> dict[str, Any]:
|
|
|
102
102
|
return data
|
|
103
103
|
|
|
104
104
|
|
|
105
|
+
def _unwrap_list_payload(response: httpx.Response) -> list[dict[str, Any]]:
|
|
106
|
+
try:
|
|
107
|
+
payload = response.json()
|
|
108
|
+
except Exception as exc: # pragma: no cover - defensive
|
|
109
|
+
raise WalletBackendError(
|
|
110
|
+
f"wdk-evm-wallet returned a non-JSON response ({response.status_code}).",
|
|
111
|
+
code="network_unavailable",
|
|
112
|
+
details={
|
|
113
|
+
"service": "wdk-evm-wallet",
|
|
114
|
+
"http_status": response.status_code,
|
|
115
|
+
},
|
|
116
|
+
) from exc
|
|
117
|
+
if response.status_code >= 400 or payload.get("ok") is False:
|
|
118
|
+
detail = payload.get("error") or f"HTTP {response.status_code}"
|
|
119
|
+
raise WalletBackendError(
|
|
120
|
+
str(detail),
|
|
121
|
+
code=str(payload.get("error_code") or "").strip() or None,
|
|
122
|
+
details=_error_details_from_payload(payload),
|
|
123
|
+
)
|
|
124
|
+
data = payload.get("data")
|
|
125
|
+
if not isinstance(data, list):
|
|
126
|
+
raise WalletBackendError("wdk-evm-wallet returned an invalid list response payload.")
|
|
127
|
+
wallets: list[dict[str, Any]] = []
|
|
128
|
+
for item in data:
|
|
129
|
+
if isinstance(item, dict):
|
|
130
|
+
wallets.append(dict(item))
|
|
131
|
+
return wallets
|
|
132
|
+
|
|
133
|
+
|
|
105
134
|
class WdkEvmLocalClient:
|
|
106
135
|
"""Small client for the local EVM wallet service."""
|
|
107
136
|
|
|
@@ -203,3 +232,26 @@ class WdkEvmLocalClient:
|
|
|
203
232
|
details={"service": "wdk-evm-wallet", "path": path},
|
|
204
233
|
) from exc
|
|
205
234
|
return _unwrap_payload(response)
|
|
235
|
+
|
|
236
|
+
def list_wallets_sync(self) -> list[dict[str, Any]]:
|
|
237
|
+
try:
|
|
238
|
+
with httpx.Client(
|
|
239
|
+
timeout=float(settings.http_timeout),
|
|
240
|
+
headers=self._headers,
|
|
241
|
+
follow_redirects=False,
|
|
242
|
+
trust_env=False,
|
|
243
|
+
) as client:
|
|
244
|
+
response = client.get(f"{self.base_url}/v1/evm/wallets")
|
|
245
|
+
except httpx.TimeoutException as exc:
|
|
246
|
+
raise WalletBackendError(
|
|
247
|
+
"wdk-evm-wallet request timed out.",
|
|
248
|
+
code="network_unavailable",
|
|
249
|
+
details={"service": "wdk-evm-wallet", "path": "/v1/evm/wallets"},
|
|
250
|
+
) from exc
|
|
251
|
+
except httpx.RequestError as exc:
|
|
252
|
+
raise WalletBackendError(
|
|
253
|
+
f"wdk-evm-wallet request failed: {exc}",
|
|
254
|
+
code="network_unavailable",
|
|
255
|
+
details={"service": "wdk-evm-wallet", "path": "/v1/evm/wallets"},
|
|
256
|
+
) from exc
|
|
257
|
+
return _unwrap_list_payload(response)
|
|
@@ -201,6 +201,9 @@ def _ignore_runtime_entries(_directory: str, names: list[str]) -> set[str]:
|
|
|
201
201
|
keep_dist = ".openclaw" in directory.parts and "extensions" in directory.parts
|
|
202
202
|
ignored: set[str] = set()
|
|
203
203
|
for name in names:
|
|
204
|
+
if name == "pay-bridge" and directory.parts[-2:] == (".openclaw", "extensions"):
|
|
205
|
+
ignored.add(name)
|
|
206
|
+
continue
|
|
204
207
|
if name == "dist" and keep_dist:
|
|
205
208
|
continue
|
|
206
209
|
if name in EXCLUDED_RUNTIME_DIR_NAMES:
|
|
@@ -5,7 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
import argparse
|
|
6
6
|
import json
|
|
7
7
|
import os
|
|
8
|
-
import
|
|
8
|
+
import secrets
|
|
9
9
|
import sys
|
|
10
10
|
from datetime import datetime, timezone
|
|
11
11
|
from pathlib import Path
|
|
@@ -49,15 +49,6 @@ OPTIONAL_TOOLS = [
|
|
|
49
49
|
"flash_trade_close_position",
|
|
50
50
|
]
|
|
51
51
|
|
|
52
|
-
PAY_BRIDGE_PLUGIN_ID = "pay-bridge"
|
|
53
|
-
PAY_BRIDGE_TOOLS = [
|
|
54
|
-
"pay_status",
|
|
55
|
-
"pay_wallet_info",
|
|
56
|
-
"pay_search_services",
|
|
57
|
-
"pay_get_service_endpoints",
|
|
58
|
-
"pay_api_request",
|
|
59
|
-
]
|
|
60
|
-
|
|
61
52
|
X402_TOOLS = [
|
|
62
53
|
"x402_search_services",
|
|
63
54
|
"x402_get_service_details",
|
|
@@ -104,13 +95,6 @@ def _default_extension_path() -> Path:
|
|
|
104
95
|
return _repo_root() / ".openclaw" / "extensions" / "agent-wallet"
|
|
105
96
|
|
|
106
97
|
|
|
107
|
-
def _default_pay_bridge_extension_path() -> Path:
|
|
108
|
-
runtime_root = _trusted_runtime_root()
|
|
109
|
-
if runtime_root is not None:
|
|
110
|
-
return runtime_root / ".openclaw" / "extensions" / PAY_BRIDGE_PLUGIN_ID
|
|
111
|
-
return _repo_root() / ".openclaw" / "extensions" / PAY_BRIDGE_PLUGIN_ID
|
|
112
|
-
|
|
113
|
-
|
|
114
98
|
def _default_package_root() -> Path:
|
|
115
99
|
runtime_root = _trusted_runtime_root()
|
|
116
100
|
if runtime_root is not None:
|
|
@@ -133,14 +117,6 @@ def _default_python_bin() -> str:
|
|
|
133
117
|
return sys.executable
|
|
134
118
|
|
|
135
119
|
|
|
136
|
-
def _default_pay_binary() -> str:
|
|
137
|
-
explicit = os.getenv("OPENCLAW_PAY_BINARY", "").strip()
|
|
138
|
-
if explicit:
|
|
139
|
-
return explicit
|
|
140
|
-
resolved = shutil.which("pay")
|
|
141
|
-
return resolved or "pay"
|
|
142
|
-
|
|
143
|
-
|
|
144
120
|
def _default_user_id() -> str:
|
|
145
121
|
return f"{os.getenv('USER', 'openclaw-user')}-local"
|
|
146
122
|
|
|
@@ -178,10 +154,8 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
178
154
|
default=True,
|
|
179
155
|
)
|
|
180
156
|
parser.add_argument("--extension-path", default=str(_default_extension_path()))
|
|
181
|
-
parser.add_argument("--pay-bridge-extension-path", default=str(_default_pay_bridge_extension_path()))
|
|
182
157
|
parser.add_argument("--package-root", default=str(_default_package_root()))
|
|
183
158
|
parser.add_argument("--python-bin", default=_default_python_bin())
|
|
184
|
-
parser.add_argument("--pay-binary", default=_default_pay_binary())
|
|
185
159
|
parser.add_argument("--write-master-key", action=argparse.BooleanOptionalAction, default=False)
|
|
186
160
|
return parser
|
|
187
161
|
|
|
@@ -191,12 +165,15 @@ def _collect_sealed_secret_updates() -> dict[str, str]:
|
|
|
191
165
|
master_key = os.getenv("AGENT_WALLET_MASTER_KEY", "").strip()
|
|
192
166
|
approval_secret = os.getenv("AGENT_WALLET_APPROVAL_SECRET", "").strip()
|
|
193
167
|
private_key = os.getenv("SOLANA_AGENT_PRIVATE_KEY", "").strip()
|
|
168
|
+
evm_wallet_password = os.getenv("WDK_EVM_WALLET_PASSWORD", "").strip()
|
|
194
169
|
if master_key:
|
|
195
170
|
updates["master_key"] = master_key
|
|
196
171
|
if approval_secret:
|
|
197
172
|
updates["approval_secret"] = approval_secret
|
|
198
173
|
if private_key:
|
|
199
174
|
updates["private_key"] = private_key
|
|
175
|
+
if evm_wallet_password:
|
|
176
|
+
updates["wdk_evm_wallet_password"] = evm_wallet_password
|
|
200
177
|
return updates
|
|
201
178
|
|
|
202
179
|
|
|
@@ -205,10 +182,12 @@ def _maybe_install_sealed_keys() -> str | None:
|
|
|
205
182
|
if not boot_key:
|
|
206
183
|
return None
|
|
207
184
|
updates = _collect_sealed_secret_updates()
|
|
208
|
-
if not updates:
|
|
209
|
-
return None
|
|
210
185
|
sealed_path = resolve_sealed_keys_path()
|
|
211
186
|
existing = unseal_keys(boot_key) if sealed_path.exists() else {}
|
|
187
|
+
if "wdk_evm_wallet_password" not in existing and "wdk_evm_wallet_password" not in updates:
|
|
188
|
+
updates["wdk_evm_wallet_password"] = secrets.token_urlsafe(24)
|
|
189
|
+
if not updates:
|
|
190
|
+
return None
|
|
212
191
|
return str(seal_keys(boot_key, {**existing, **updates}))
|
|
213
192
|
|
|
214
193
|
|
|
@@ -255,17 +234,14 @@ def main() -> None:
|
|
|
255
234
|
allow = plugins.setdefault("allow", [])
|
|
256
235
|
if args.plugin_id not in allow:
|
|
257
236
|
allow.append(args.plugin_id)
|
|
258
|
-
|
|
259
|
-
allow.append(PAY_BRIDGE_PLUGIN_ID)
|
|
237
|
+
allow[:] = [item for item in allow if item != "pay-bridge"]
|
|
260
238
|
|
|
261
239
|
load = plugins.setdefault("load", {})
|
|
262
240
|
paths = load.setdefault("paths", [])
|
|
263
241
|
extension_path_text = str(Path(args.extension_path).expanduser().resolve())
|
|
264
242
|
if extension_path_text not in paths:
|
|
265
243
|
paths.append(extension_path_text)
|
|
266
|
-
|
|
267
|
-
if pay_bridge_extension_path_text not in paths:
|
|
268
|
-
paths.append(pay_bridge_extension_path_text)
|
|
244
|
+
paths[:] = [item for item in paths if "extensions/pay-bridge" not in str(item)]
|
|
269
245
|
|
|
270
246
|
entries = plugins.setdefault("entries", {})
|
|
271
247
|
effective_network = _normalize_network(args.backend, args.network)
|
|
@@ -318,25 +294,19 @@ def main() -> None:
|
|
|
318
294
|
"enabled": True,
|
|
319
295
|
"config": plugin_config,
|
|
320
296
|
}
|
|
321
|
-
|
|
322
|
-
existing_pay_config = (
|
|
323
|
-
dict(existing_pay_entry.get("config"))
|
|
324
|
-
if isinstance(existing_pay_entry.get("config"), dict)
|
|
325
|
-
else {}
|
|
326
|
-
)
|
|
327
|
-
pay_bridge_config = {
|
|
328
|
-
**existing_pay_config,
|
|
329
|
-
"payBinary": args.pay_binary.strip() or _default_pay_binary(),
|
|
330
|
-
"requireHttps": bool(existing_pay_config.get("requireHttps", True)),
|
|
331
|
-
}
|
|
332
|
-
entries[PAY_BRIDGE_PLUGIN_ID] = {
|
|
333
|
-
"enabled": True,
|
|
334
|
-
"config": pay_bridge_config,
|
|
335
|
-
}
|
|
297
|
+
entries.pop("pay-bridge", None)
|
|
336
298
|
|
|
337
299
|
tools = data.setdefault("tools", {})
|
|
338
300
|
also_allow = tools.setdefault("alsoAllow", [])
|
|
339
|
-
|
|
301
|
+
removed_pay_tools = {
|
|
302
|
+
"pay_status",
|
|
303
|
+
"pay_wallet_info",
|
|
304
|
+
"pay_search_services",
|
|
305
|
+
"pay_get_service_endpoints",
|
|
306
|
+
"pay_api_request",
|
|
307
|
+
}
|
|
308
|
+
also_allow[:] = [tool_name for tool_name in also_allow if tool_name not in removed_pay_tools]
|
|
309
|
+
for tool_name in OPTIONAL_TOOLS + X402_TOOLS:
|
|
340
310
|
if tool_name not in also_allow:
|
|
341
311
|
also_allow.append(tool_name)
|
|
342
312
|
|
|
@@ -352,7 +322,6 @@ def main() -> None:
|
|
|
352
322
|
"config_path": str(config_path),
|
|
353
323
|
"backup_path": str(backup_path),
|
|
354
324
|
"extension_path": extension_path_text,
|
|
355
|
-
"pay_bridge_extension_path": pay_bridge_extension_path_text,
|
|
356
325
|
"python_bin": args.python_bin,
|
|
357
326
|
"package_root": plugin_config["packageRoot"],
|
|
358
327
|
"plugin_id": args.plugin_id,
|
|
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
import argparse
|
|
6
6
|
import json
|
|
7
7
|
import os
|
|
8
|
+
import secrets as py_secrets
|
|
8
9
|
import sys
|
|
9
10
|
from pathlib import Path
|
|
10
11
|
|
|
@@ -49,12 +50,15 @@ def _collect_secret_updates() -> dict[str, str]:
|
|
|
49
50
|
master_key = os.getenv("AGENT_WALLET_MASTER_KEY", "").strip()
|
|
50
51
|
approval_secret = os.getenv("AGENT_WALLET_APPROVAL_SECRET", "").strip()
|
|
51
52
|
private_key = os.getenv("SOLANA_AGENT_PRIVATE_KEY", "").strip()
|
|
53
|
+
evm_wallet_password = os.getenv("WDK_EVM_WALLET_PASSWORD", "").strip()
|
|
52
54
|
if master_key:
|
|
53
55
|
updates["master_key"] = master_key
|
|
54
56
|
if approval_secret:
|
|
55
57
|
updates["approval_secret"] = approval_secret
|
|
56
58
|
if private_key:
|
|
57
59
|
updates["private_key"] = private_key
|
|
60
|
+
if evm_wallet_password:
|
|
61
|
+
updates["wdk_evm_wallet_password"] = evm_wallet_password
|
|
58
62
|
return updates
|
|
59
63
|
|
|
60
64
|
|
|
@@ -80,6 +84,10 @@ def main() -> None:
|
|
|
80
84
|
sealed_path = resolve_sealed_keys_path()
|
|
81
85
|
existing = unseal_keys(boot_key) if sealed_path.exists() and not args.replace else {}
|
|
82
86
|
secrets = {**existing, **updates}
|
|
87
|
+
generated_keys: list[str] = []
|
|
88
|
+
if "wdk_evm_wallet_password" not in secrets:
|
|
89
|
+
secrets["wdk_evm_wallet_password"] = py_secrets.token_urlsafe(24)
|
|
90
|
+
generated_keys.append("wdk_evm_wallet_password")
|
|
83
91
|
if not secrets:
|
|
84
92
|
raise SystemExit(
|
|
85
93
|
"No secrets provided. Set AGENT_WALLET_MASTER_KEY, AGENT_WALLET_APPROVAL_SECRET, "
|
|
@@ -93,7 +101,7 @@ def main() -> None:
|
|
|
93
101
|
"ok": True,
|
|
94
102
|
"path": str(path),
|
|
95
103
|
"stored_keys": sorted(secrets.keys()),
|
|
96
|
-
"updated_keys": sorted(updates.keys()),
|
|
104
|
+
"updated_keys": sorted(set(updates.keys()) | set(generated_keys)),
|
|
97
105
|
"replaced": bool(args.replace),
|
|
98
106
|
},
|
|
99
107
|
indent=2,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentlayer.tech/wallet",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.19",
|
|
4
4
|
"description": "NPM installer for the OpenClaw Agent Wallet local runtime.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -40,7 +40,6 @@
|
|
|
40
40
|
"agent-wallet/pyproject.toml",
|
|
41
41
|
".openclaw/AGENTS.md",
|
|
42
42
|
".openclaw/extensions/agent-wallet/",
|
|
43
|
-
".openclaw/extensions/pay-bridge/",
|
|
44
43
|
"hermes/plugins/agent_wallet/",
|
|
45
44
|
"wdk-btc-wallet/src/",
|
|
46
45
|
"wdk-btc-wallet/bootstrap.sh",
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
# pay-bridge
|
|
2
|
-
|
|
3
|
-
Thin OpenClaw bridge to the locally installed `pay` CLI.
|
|
4
|
-
|
|
5
|
-
External install path:
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
openclaw plugins install clawhub:@agentlayertech/pay-bridge-plugin
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
This plugin is intentionally separate from `agent-wallet`:
|
|
12
|
-
|
|
13
|
-
- `agent-wallet` remains the execution wallet stack for Solana/EVM/BTC
|
|
14
|
-
- `pay-bridge` only discovers and calls paid APIs through `pay`
|
|
15
|
-
- the `pay` wallet stays separate from the AgentLayer wallet runtime
|
|
16
|
-
|
|
17
|
-
## Exposed tools
|
|
18
|
-
|
|
19
|
-
- `pay_status`
|
|
20
|
-
- `pay_wallet_info`
|
|
21
|
-
- `pay_search_services`
|
|
22
|
-
- `pay_get_service_endpoints`
|
|
23
|
-
- `pay_api_request`
|
|
24
|
-
|
|
25
|
-
## Intended workflow
|
|
26
|
-
|
|
27
|
-
1. `pay_status`
|
|
28
|
-
2. `pay_search_services`
|
|
29
|
-
3. `pay_get_service_endpoints`
|
|
30
|
-
4. `pay_api_request`
|
|
31
|
-
|
|
32
|
-
`pay_api_request` is deliberately narrow:
|
|
33
|
-
|
|
34
|
-
- it requires a `service_fqn`, `resource`, and `url`
|
|
35
|
-
- it validates the URL against `pay skills endpoints`
|
|
36
|
-
- it requires `purpose` and `user_confirmed=true`
|
|
37
|
-
|
|
38
|
-
This keeps the bridge thin and prevents it from becoming a generic arbitrary paid-curl launcher.
|
|
@@ -1,287 +0,0 @@
|
|
|
1
|
-
import { execFile } from "node:child_process";
|
|
2
|
-
import { promisify } from "node:util";
|
|
3
|
-
|
|
4
|
-
const execFileAsync = promisify(execFile);
|
|
5
|
-
const ANSI_RE = /\u001b\[[0-9;]*m/g;
|
|
6
|
-
|
|
7
|
-
function stripAnsi(text) {
|
|
8
|
-
return String(text || "").replace(ANSI_RE, "");
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
function nonEmptyLines(text) {
|
|
12
|
-
return stripAnsi(text)
|
|
13
|
-
.split(/\r?\n/)
|
|
14
|
-
.map((line) => line.trim())
|
|
15
|
-
.filter(Boolean);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function extractLastJsonValue(text) {
|
|
19
|
-
const lines = nonEmptyLines(text);
|
|
20
|
-
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
21
|
-
const candidate = lines[i];
|
|
22
|
-
if (!candidate.startsWith("{") && !candidate.startsWith("[")) continue;
|
|
23
|
-
try {
|
|
24
|
-
return JSON.parse(candidate);
|
|
25
|
-
} catch {
|
|
26
|
-
continue;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
return null;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function collectStringLeaves(value, acc = new Set()) {
|
|
33
|
-
if (typeof value === "string") {
|
|
34
|
-
acc.add(value);
|
|
35
|
-
return acc;
|
|
36
|
-
}
|
|
37
|
-
if (Array.isArray(value)) {
|
|
38
|
-
for (const item of value) collectStringLeaves(item, acc);
|
|
39
|
-
return acc;
|
|
40
|
-
}
|
|
41
|
-
if (value && typeof value === "object") {
|
|
42
|
-
for (const item of Object.values(value)) collectStringLeaves(item, acc);
|
|
43
|
-
}
|
|
44
|
-
return acc;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function withAccountArgs(args, account) {
|
|
48
|
-
if (!account) return args;
|
|
49
|
-
return [...args, "--account", account];
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export function resolvePayBinary(config = {}) {
|
|
53
|
-
return (
|
|
54
|
-
config.payBinary ||
|
|
55
|
-
process.env.OPENCLAW_PAY_BINARY ||
|
|
56
|
-
"pay"
|
|
57
|
-
);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export async function runPayCommand(payBinary, args, options = {}) {
|
|
61
|
-
const { cwd, input = null } = options;
|
|
62
|
-
let stdout = "";
|
|
63
|
-
let stderr = "";
|
|
64
|
-
try {
|
|
65
|
-
const result = await execFileAsync(payBinary, args, {
|
|
66
|
-
cwd,
|
|
67
|
-
env: { ...process.env },
|
|
68
|
-
input,
|
|
69
|
-
maxBuffer: 1024 * 1024 * 8,
|
|
70
|
-
});
|
|
71
|
-
stdout = result.stdout ?? "";
|
|
72
|
-
stderr = result.stderr ?? "";
|
|
73
|
-
} catch (error) {
|
|
74
|
-
stdout = typeof error?.stdout === "string" ? error.stdout : "";
|
|
75
|
-
stderr = typeof error?.stderr === "string" ? error.stderr : "";
|
|
76
|
-
const payload = extractLastJsonValue(stdout) || extractLastJsonValue(stderr);
|
|
77
|
-
const message =
|
|
78
|
-
payload?.error?.message ||
|
|
79
|
-
payload?.message ||
|
|
80
|
-
stripAnsi(stderr || stdout || error?.message || "pay command failed").trim() ||
|
|
81
|
-
"pay command failed";
|
|
82
|
-
const wrapped = new Error(message);
|
|
83
|
-
wrapped.stdout = stdout;
|
|
84
|
-
wrapped.stderr = stderr;
|
|
85
|
-
wrapped.details = payload && typeof payload === "object" ? payload : null;
|
|
86
|
-
throw wrapped;
|
|
87
|
-
}
|
|
88
|
-
return { stdout, stderr };
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
export function parseWhoamiOutput(stdout) {
|
|
92
|
-
const lines = nonEmptyLines(stdout);
|
|
93
|
-
const systemUser = lines[0] || null;
|
|
94
|
-
const noAccount = lines.some((line) => /no mainnet account/i.test(line));
|
|
95
|
-
return {
|
|
96
|
-
system_user: systemUser,
|
|
97
|
-
has_mainnet_account: !noAccount,
|
|
98
|
-
raw_lines: lines,
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
export function parseAccountListOutput(stdout) {
|
|
103
|
-
const lines = nonEmptyLines(stdout);
|
|
104
|
-
const noAccounts = lines.some((line) => /no accounts found/i.test(line));
|
|
105
|
-
return {
|
|
106
|
-
has_accounts: !noAccounts,
|
|
107
|
-
raw_lines: lines,
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
export async function getPayStatus(config = {}, options = {}) {
|
|
112
|
-
const payBinary = resolvePayBinary(config);
|
|
113
|
-
const versionResult = await runPayCommand(payBinary, ["--version"], options);
|
|
114
|
-
const whoamiResult = await runPayCommand(
|
|
115
|
-
payBinary,
|
|
116
|
-
withAccountArgs(["whoami"], config.defaultAccount),
|
|
117
|
-
options
|
|
118
|
-
);
|
|
119
|
-
const accountListResult = await runPayCommand(payBinary, ["account", "list"], options);
|
|
120
|
-
return {
|
|
121
|
-
installed: true,
|
|
122
|
-
pay_binary: payBinary,
|
|
123
|
-
version: stripAnsi(versionResult.stdout).trim() || null,
|
|
124
|
-
account_configured: parseWhoamiOutput(whoamiResult.stdout).has_mainnet_account,
|
|
125
|
-
has_any_accounts: parseAccountListOutput(accountListResult.stdout).has_accounts,
|
|
126
|
-
whoami: parseWhoamiOutput(whoamiResult.stdout),
|
|
127
|
-
accounts: parseAccountListOutput(accountListResult.stdout),
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
export async function getPayWalletInfo(config = {}, options = {}) {
|
|
132
|
-
const payBinary = resolvePayBinary(config);
|
|
133
|
-
const whoamiResult = await runPayCommand(
|
|
134
|
-
payBinary,
|
|
135
|
-
withAccountArgs(["whoami"], config.defaultAccount),
|
|
136
|
-
options
|
|
137
|
-
);
|
|
138
|
-
const accountListResult = await runPayCommand(payBinary, ["account", "list"], options);
|
|
139
|
-
return {
|
|
140
|
-
pay_binary: payBinary,
|
|
141
|
-
default_account: config.defaultAccount || null,
|
|
142
|
-
whoami: parseWhoamiOutput(whoamiResult.stdout),
|
|
143
|
-
accounts: parseAccountListOutput(accountListResult.stdout),
|
|
144
|
-
notes: [
|
|
145
|
-
"This wallet is managed by pay.sh and is separate from the AgentLayer execution wallet.",
|
|
146
|
-
],
|
|
147
|
-
};
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
export async function searchPayServices(config = {}, params = {}, options = {}) {
|
|
151
|
-
const payBinary = resolvePayBinary(config);
|
|
152
|
-
const args = ["skills", "search"];
|
|
153
|
-
if (params.query) args.push(String(params.query));
|
|
154
|
-
if (params.category) args.push("--category", String(params.category));
|
|
155
|
-
args.push("--json");
|
|
156
|
-
const { stdout, stderr } = await runPayCommand(
|
|
157
|
-
payBinary,
|
|
158
|
-
withAccountArgs(args, params.account || config.defaultAccount),
|
|
159
|
-
options
|
|
160
|
-
);
|
|
161
|
-
const parsed = JSON.parse(stdout.trim() || "{}");
|
|
162
|
-
return {
|
|
163
|
-
query: params.query || "",
|
|
164
|
-
category: params.category || null,
|
|
165
|
-
results: parsed,
|
|
166
|
-
warnings: nonEmptyLines(stderr),
|
|
167
|
-
};
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
export async function getPayServiceEndpoints(config = {}, params = {}, options = {}) {
|
|
171
|
-
const payBinary = resolvePayBinary(config);
|
|
172
|
-
const args = [
|
|
173
|
-
"skills",
|
|
174
|
-
"endpoints",
|
|
175
|
-
String(params.service_fqn),
|
|
176
|
-
String(params.resource),
|
|
177
|
-
"--json",
|
|
178
|
-
];
|
|
179
|
-
const { stdout, stderr } = await runPayCommand(
|
|
180
|
-
payBinary,
|
|
181
|
-
withAccountArgs(args, params.account || config.defaultAccount),
|
|
182
|
-
options
|
|
183
|
-
);
|
|
184
|
-
const parsed = JSON.parse(stdout.trim() || "{}");
|
|
185
|
-
return {
|
|
186
|
-
service_fqn: String(params.service_fqn),
|
|
187
|
-
resource: String(params.resource),
|
|
188
|
-
endpoints: parsed,
|
|
189
|
-
warnings: nonEmptyLines(stderr),
|
|
190
|
-
};
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
function ensureHttps(url, requireHttps = true) {
|
|
194
|
-
if (!requireHttps) return;
|
|
195
|
-
const parsed = new URL(url);
|
|
196
|
-
if (parsed.protocol !== "https:") {
|
|
197
|
-
throw new Error("pay_api_request only allows https URLs.");
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
function appendQuery(url, query) {
|
|
202
|
-
const parsed = new URL(url);
|
|
203
|
-
if (query && typeof query === "object") {
|
|
204
|
-
for (const [key, value] of Object.entries(query)) {
|
|
205
|
-
if (value === undefined || value === null) continue;
|
|
206
|
-
parsed.searchParams.set(key, String(value));
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
return parsed.toString();
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
export function endpointPayloadContainsUrl(endpointPayload, url) {
|
|
213
|
-
const strings = collectStringLeaves(endpointPayload);
|
|
214
|
-
return strings.has(url);
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
export async function executePayApiRequest(config = {}, params = {}, options = {}) {
|
|
218
|
-
if (params.user_confirmed !== true) {
|
|
219
|
-
throw new Error("pay_api_request requires user_confirmed=true.");
|
|
220
|
-
}
|
|
221
|
-
if (!params.purpose || !String(params.purpose).trim()) {
|
|
222
|
-
throw new Error("pay_api_request requires a non-empty purpose.");
|
|
223
|
-
}
|
|
224
|
-
if (!params.service_fqn || !params.resource || !params.url) {
|
|
225
|
-
throw new Error("pay_api_request requires service_fqn, resource, and url.");
|
|
226
|
-
}
|
|
227
|
-
if (params.json_body !== undefined && params.text_body !== undefined) {
|
|
228
|
-
throw new Error("Provide either json_body or text_body, not both.");
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
const endpointData = await getPayServiceEndpoints(config, {
|
|
232
|
-
account: params.account,
|
|
233
|
-
resource: params.resource,
|
|
234
|
-
service_fqn: params.service_fqn,
|
|
235
|
-
}, options);
|
|
236
|
-
|
|
237
|
-
const finalUrl = appendQuery(String(params.url), params.query);
|
|
238
|
-
ensureHttps(finalUrl, config.requireHttps !== false);
|
|
239
|
-
if (!endpointPayloadContainsUrl(endpointData.endpoints, String(params.url))) {
|
|
240
|
-
throw new Error("The requested URL is not present in pay_get_service_endpoints for this service/resource.");
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
const payBinary = resolvePayBinary(config);
|
|
244
|
-
const method = String(params.method || "GET").toUpperCase();
|
|
245
|
-
const args = ["curl"];
|
|
246
|
-
if (params.account || config.defaultAccount) {
|
|
247
|
-
args.push("--account", String(params.account || config.defaultAccount));
|
|
248
|
-
}
|
|
249
|
-
args.push("--request", method);
|
|
250
|
-
|
|
251
|
-
const headers = params.headers && typeof params.headers === "object" ? params.headers : {};
|
|
252
|
-
for (const [key, value] of Object.entries(headers)) {
|
|
253
|
-
args.push("--header", `${key}: ${String(value)}`);
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
if (params.json_body !== undefined) {
|
|
257
|
-
const hasContentType = Object.keys(headers).some((key) => key.toLowerCase() === "content-type");
|
|
258
|
-
if (!hasContentType) {
|
|
259
|
-
args.push("--header", "content-type: application/json");
|
|
260
|
-
}
|
|
261
|
-
args.push("--data", JSON.stringify(params.json_body));
|
|
262
|
-
} else if (params.text_body !== undefined) {
|
|
263
|
-
args.push("--data", String(params.text_body));
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
args.push(finalUrl);
|
|
267
|
-
const { stdout, stderr } = await runPayCommand(payBinary, args, options);
|
|
268
|
-
const trimmed = stdout.trim();
|
|
269
|
-
let responseBody = trimmed;
|
|
270
|
-
if (params.parse_json_response !== false) {
|
|
271
|
-
try {
|
|
272
|
-
responseBody = JSON.parse(trimmed);
|
|
273
|
-
} catch {
|
|
274
|
-
responseBody = trimmed;
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
return {
|
|
278
|
-
method,
|
|
279
|
-
purpose: String(params.purpose),
|
|
280
|
-
request_url: finalUrl,
|
|
281
|
-
service_fqn: String(params.service_fqn),
|
|
282
|
-
resource: String(params.resource),
|
|
283
|
-
response: responseBody,
|
|
284
|
-
raw_response_text: trimmed,
|
|
285
|
-
warnings: nonEmptyLines(stderr),
|
|
286
|
-
};
|
|
287
|
-
}
|