@agentlayer.tech/wallet 0.1.13 → 0.1.14
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 +10 -1
- package/.openclaw/extensions/pay-bridge/README.md +32 -0
- package/.openclaw/extensions/pay-bridge/core.mjs +287 -0
- package/.openclaw/extensions/pay-bridge/index.ts +196 -0
- package/.openclaw/extensions/pay-bridge/openclaw.plugin.json +34 -0
- package/.openclaw/extensions/pay-bridge/package.json +11 -0
- package/.openclaw/extensions/pay-bridge/skills/pay-operator/SKILL.md +20 -0
- package/.openclaw/extensions/pay-bridge/smoke_pay_bridge.mjs +38 -0
- package/CHANGELOG.md +10 -0
- package/README.md +1 -1
- package/agent-wallet/pyproject.toml +1 -1
- package/agent-wallet/scripts/install_openclaw_local_config.py +53 -2
- package/package.json +3 -2
package/.openclaw/AGENTS.md
CHANGED
|
@@ -4,17 +4,25 @@
|
|
|
4
4
|
These instructions apply to the entire `.openclaw/` tree.
|
|
5
5
|
|
|
6
6
|
## Purpose
|
|
7
|
-
This tree contains local OpenClaw host-side workspace assets. In the current repo, its
|
|
7
|
+
This tree contains local OpenClaw host-side workspace assets. In the current repo, its main responsibilities are:
|
|
8
|
+
|
|
9
|
+
- the `agent-wallet` extension that bridges OpenClaw to the authoritative Python `agent-wallet` backend
|
|
10
|
+
- the `pay-bridge` extension that bridges OpenClaw to the local `pay` CLI for paid API discovery and execution
|
|
8
11
|
|
|
9
12
|
## Current structure
|
|
10
13
|
- `.openclaw/extensions/agent-wallet/index.ts` — TypeScript extension entrypoint registered by OpenClaw.
|
|
11
14
|
- `.openclaw/extensions/agent-wallet/openclaw.plugin.json` — plugin manifest and config schema.
|
|
12
15
|
- `.openclaw/extensions/agent-wallet/package.json` — extension package metadata.
|
|
13
16
|
- `.openclaw/extensions/agent-wallet/skills/wallet-operator/SKILL.md` — user-facing operational wallet safety guidance.
|
|
17
|
+
- `.openclaw/extensions/pay-bridge/index.ts` — TypeScript entrypoint for the local `pay` CLI bridge.
|
|
18
|
+
- `.openclaw/extensions/pay-bridge/openclaw.plugin.json` — plugin manifest and config schema for pay tools.
|
|
19
|
+
- `.openclaw/extensions/pay-bridge/core.mjs` — local `pay` command execution and output shaping.
|
|
20
|
+
- `.openclaw/extensions/pay-bridge/skills/pay-operator/SKILL.md` — user-facing operational guidance for paid API usage.
|
|
14
21
|
|
|
15
22
|
## Design intent
|
|
16
23
|
- Keep the TypeScript extension thin and host-oriented.
|
|
17
24
|
- Let Python own wallet logic, policy, approvals, signing rules, and Solana implementation details.
|
|
25
|
+
- Let `pay` remain the source of truth for paid API wallet/account behavior.
|
|
18
26
|
- Let the extension focus on:
|
|
19
27
|
- resolving config
|
|
20
28
|
- locating the Python package
|
|
@@ -27,6 +35,7 @@ This tree contains local OpenClaw host-side workspace assets. In the current rep
|
|
|
27
35
|
|
|
28
36
|
### Keep bridge logic thin
|
|
29
37
|
- Do not duplicate business logic from Python unless OpenClaw requires it at registration time.
|
|
38
|
+
- Do not duplicate payment protocol logic from `pay`; prefer invoking the local CLI and shaping its output.
|
|
30
39
|
- Do not reimplement approval validation, transaction policy, wallet derivation, or Solana-specific rules in TypeScript.
|
|
31
40
|
- Prefer forwarding config into the CLI bridge and letting Python decide runtime behavior.
|
|
32
41
|
- Treat this layer as a transport and schema bridge, not an execution authority.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# pay-bridge
|
|
2
|
+
|
|
3
|
+
Thin OpenClaw bridge to the locally installed `pay` CLI.
|
|
4
|
+
|
|
5
|
+
This plugin is intentionally separate from `agent-wallet`:
|
|
6
|
+
|
|
7
|
+
- `agent-wallet` remains the execution wallet stack for Solana/EVM/BTC
|
|
8
|
+
- `pay-bridge` only discovers and calls paid APIs through `pay`
|
|
9
|
+
- the `pay` wallet stays separate from the AgentLayer wallet runtime
|
|
10
|
+
|
|
11
|
+
## Exposed tools
|
|
12
|
+
|
|
13
|
+
- `pay_status`
|
|
14
|
+
- `pay_wallet_info`
|
|
15
|
+
- `pay_search_services`
|
|
16
|
+
- `pay_get_service_endpoints`
|
|
17
|
+
- `pay_api_request`
|
|
18
|
+
|
|
19
|
+
## Intended workflow
|
|
20
|
+
|
|
21
|
+
1. `pay_status`
|
|
22
|
+
2. `pay_search_services`
|
|
23
|
+
3. `pay_get_service_endpoints`
|
|
24
|
+
4. `pay_api_request`
|
|
25
|
+
|
|
26
|
+
`pay_api_request` is deliberately narrow:
|
|
27
|
+
|
|
28
|
+
- it requires a `service_fqn`, `resource`, and `url`
|
|
29
|
+
- it validates the URL against `pay skills endpoints`
|
|
30
|
+
- it requires `purpose` and `user_confirmed=true`
|
|
31
|
+
|
|
32
|
+
This keeps the bridge thin and prevents it from becoming a generic arbitrary paid-curl launcher.
|
|
@@ -0,0 +1,287 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import {
|
|
2
|
+
executePayApiRequest,
|
|
3
|
+
getPayServiceEndpoints,
|
|
4
|
+
getPayStatus,
|
|
5
|
+
getPayWalletInfo,
|
|
6
|
+
searchPayServices,
|
|
7
|
+
} from "./core.mjs";
|
|
8
|
+
|
|
9
|
+
const PLUGIN_ID = "pay-bridge";
|
|
10
|
+
|
|
11
|
+
function asContent(data) {
|
|
12
|
+
return {
|
|
13
|
+
content: [
|
|
14
|
+
{
|
|
15
|
+
type: "text",
|
|
16
|
+
text: JSON.stringify(data, null, 2),
|
|
17
|
+
},
|
|
18
|
+
],
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function resolvePluginConfig(api) {
|
|
23
|
+
const globalConfig = api?.config ?? {};
|
|
24
|
+
const pluginEntry = globalConfig?.plugins?.entries?.[PLUGIN_ID];
|
|
25
|
+
return pluginEntry?.config ?? globalConfig?.config ?? {};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function registerTool(api, definition) {
|
|
29
|
+
api.registerTool({
|
|
30
|
+
name: definition.name,
|
|
31
|
+
description: definition.description,
|
|
32
|
+
parameters: definition.parameters,
|
|
33
|
+
returns: {
|
|
34
|
+
type: "object",
|
|
35
|
+
additionalProperties: true,
|
|
36
|
+
},
|
|
37
|
+
async execute(_id, params = {}) {
|
|
38
|
+
const config = resolvePluginConfig(api);
|
|
39
|
+
let result;
|
|
40
|
+
if (definition.name === "pay_status") {
|
|
41
|
+
result = await getPayStatus(config, { cwd: process.cwd() });
|
|
42
|
+
} else if (definition.name === "pay_wallet_info") {
|
|
43
|
+
result = await getPayWalletInfo(config, { cwd: process.cwd() });
|
|
44
|
+
} else if (definition.name === "pay_search_services") {
|
|
45
|
+
result = await searchPayServices(config, params, { cwd: process.cwd() });
|
|
46
|
+
} else if (definition.name === "pay_get_service_endpoints") {
|
|
47
|
+
result = await getPayServiceEndpoints(config, params, { cwd: process.cwd() });
|
|
48
|
+
} else if (definition.name === "pay_api_request") {
|
|
49
|
+
result = await executePayApiRequest(config, params, { cwd: process.cwd() });
|
|
50
|
+
} else {
|
|
51
|
+
throw new Error(`Unsupported pay-bridge tool: ${definition.name}`);
|
|
52
|
+
}
|
|
53
|
+
return asContent(result);
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const toolDefinitions = [
|
|
59
|
+
{
|
|
60
|
+
name: "pay_status",
|
|
61
|
+
description:
|
|
62
|
+
"Check whether the local pay.sh CLI is installed and whether a pay wallet/account is configured. Use this before any paid API workflow.",
|
|
63
|
+
parameters: {
|
|
64
|
+
type: "object",
|
|
65
|
+
properties: {},
|
|
66
|
+
additionalProperties: false,
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
name: "pay_wallet_info",
|
|
71
|
+
description:
|
|
72
|
+
"Show pay wallet/account status. This wallet is separate from the AgentLayer execution wallet and is only for pay.sh API payments.",
|
|
73
|
+
parameters: {
|
|
74
|
+
type: "object",
|
|
75
|
+
properties: {},
|
|
76
|
+
additionalProperties: false,
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
name: "pay_search_services",
|
|
81
|
+
description:
|
|
82
|
+
"Search the pay.sh skills catalog for paid APIs. Prefer this instead of guessing URLs manually.",
|
|
83
|
+
parameters: {
|
|
84
|
+
type: "object",
|
|
85
|
+
properties: {
|
|
86
|
+
query: {
|
|
87
|
+
type: "string",
|
|
88
|
+
description: "Search text for service names, descriptions, or endpoint paths.",
|
|
89
|
+
},
|
|
90
|
+
category: {
|
|
91
|
+
type: "string",
|
|
92
|
+
description: "Optional category filter such as ai_ml, maps, data, compute, search, crypto_finance.",
|
|
93
|
+
},
|
|
94
|
+
account: {
|
|
95
|
+
type: "string",
|
|
96
|
+
description: "Optional pay account override.",
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
additionalProperties: false,
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
name: "pay_get_service_endpoints",
|
|
104
|
+
description:
|
|
105
|
+
"List the discoverable endpoints for a pay.sh service/resource pair and return their gateway URLs. Use the returned URL with pay_api_request.",
|
|
106
|
+
parameters: {
|
|
107
|
+
type: "object",
|
|
108
|
+
properties: {
|
|
109
|
+
service_fqn: {
|
|
110
|
+
type: "string",
|
|
111
|
+
description: "Fully qualified pay service name, for example solana-foundation/google/language.",
|
|
112
|
+
},
|
|
113
|
+
resource: {
|
|
114
|
+
type: "string",
|
|
115
|
+
description: "Resource name inside the service, for example entities or jobs.",
|
|
116
|
+
},
|
|
117
|
+
account: {
|
|
118
|
+
type: "string",
|
|
119
|
+
description: "Optional pay account override.",
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
required: ["service_fqn", "resource"],
|
|
123
|
+
additionalProperties: false,
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
name: "pay_api_request",
|
|
128
|
+
description:
|
|
129
|
+
"Call a paid API through the local pay.sh CLI using a URL returned by pay_get_service_endpoints. Requires explicit user_confirmed=true and keeps the pay wallet separate from AgentLayer execution wallets.",
|
|
130
|
+
optional: true,
|
|
131
|
+
parameters: {
|
|
132
|
+
type: "object",
|
|
133
|
+
properties: {
|
|
134
|
+
service_fqn: {
|
|
135
|
+
type: "string",
|
|
136
|
+
description: "Fully qualified pay service name used to validate the request URL.",
|
|
137
|
+
},
|
|
138
|
+
resource: {
|
|
139
|
+
type: "string",
|
|
140
|
+
description: "Resource name used to validate the request URL.",
|
|
141
|
+
},
|
|
142
|
+
url: {
|
|
143
|
+
type: "string",
|
|
144
|
+
description: "Exact HTTPS gateway URL returned by pay_get_service_endpoints.",
|
|
145
|
+
},
|
|
146
|
+
method: {
|
|
147
|
+
type: "string",
|
|
148
|
+
description: "HTTP method such as GET or POST.",
|
|
149
|
+
},
|
|
150
|
+
headers: {
|
|
151
|
+
type: "object",
|
|
152
|
+
additionalProperties: { type: "string" },
|
|
153
|
+
description: "Optional HTTP headers.",
|
|
154
|
+
},
|
|
155
|
+
query: {
|
|
156
|
+
type: "object",
|
|
157
|
+
additionalProperties: true,
|
|
158
|
+
description: "Optional query parameters to append to the URL.",
|
|
159
|
+
},
|
|
160
|
+
json_body: {
|
|
161
|
+
description: "Optional JSON request body.",
|
|
162
|
+
},
|
|
163
|
+
text_body: {
|
|
164
|
+
type: "string",
|
|
165
|
+
description: "Optional raw text request body. Do not provide with json_body.",
|
|
166
|
+
},
|
|
167
|
+
account: {
|
|
168
|
+
type: "string",
|
|
169
|
+
description: "Optional pay account override.",
|
|
170
|
+
},
|
|
171
|
+
parse_json_response: {
|
|
172
|
+
type: "boolean",
|
|
173
|
+
description: "If true, attempt to parse the response body as JSON.",
|
|
174
|
+
},
|
|
175
|
+
purpose: {
|
|
176
|
+
type: "string",
|
|
177
|
+
description: "Short user-facing reason for this paid API call.",
|
|
178
|
+
},
|
|
179
|
+
user_confirmed: {
|
|
180
|
+
type: "boolean",
|
|
181
|
+
description: "Must be true for paid API requests.",
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
required: ["service_fqn", "resource", "url", "method", "purpose", "user_confirmed"],
|
|
185
|
+
additionalProperties: false,
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
];
|
|
189
|
+
|
|
190
|
+
export default function registerPayBridgePlugin(api) {
|
|
191
|
+
api?.logger?.info?.("[pay-bridge] registering pay.sh OpenClaw plugin");
|
|
192
|
+
for (const definition of toolDefinitions) {
|
|
193
|
+
registerTool(api, definition);
|
|
194
|
+
}
|
|
195
|
+
api?.logger?.info?.(`[pay-bridge] registered ${toolDefinitions.length} pay tools`);
|
|
196
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "pay-bridge",
|
|
3
|
+
"name": "pay.sh Bridge",
|
|
4
|
+
"description": "Thin OpenClaw bridge to the local pay.sh CLI for paid API discovery and execution.",
|
|
5
|
+
"version": "0.1.0",
|
|
6
|
+
"contracts": {
|
|
7
|
+
"tools": [
|
|
8
|
+
"pay_status",
|
|
9
|
+
"pay_wallet_info",
|
|
10
|
+
"pay_search_services",
|
|
11
|
+
"pay_get_service_endpoints",
|
|
12
|
+
"pay_api_request"
|
|
13
|
+
]
|
|
14
|
+
},
|
|
15
|
+
"skills": ["skills/pay-operator"],
|
|
16
|
+
"configSchema": {
|
|
17
|
+
"type": "object",
|
|
18
|
+
"additionalProperties": false,
|
|
19
|
+
"properties": {
|
|
20
|
+
"payBinary": {
|
|
21
|
+
"type": "string",
|
|
22
|
+
"description": "Absolute path or executable name for the local pay.sh binary."
|
|
23
|
+
},
|
|
24
|
+
"defaultAccount": {
|
|
25
|
+
"type": "string",
|
|
26
|
+
"description": "Optional pay account name to use when a tool call does not specify one."
|
|
27
|
+
},
|
|
28
|
+
"requireHttps": {
|
|
29
|
+
"type": "boolean",
|
|
30
|
+
"description": "If true, pay_api_request refuses non-HTTPS URLs."
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# pay-operator
|
|
2
|
+
|
|
3
|
+
Use this skill when the user wants to discover or call paid APIs through `pay`.
|
|
4
|
+
|
|
5
|
+
## Rules
|
|
6
|
+
|
|
7
|
+
- Treat the `pay` wallet as separate from the AgentLayer execution wallet.
|
|
8
|
+
- Do not use `agent-wallet` tools for `pay` account management.
|
|
9
|
+
- Do not fall back to shell commands when the `pay-bridge` tools exist.
|
|
10
|
+
- Prefer this order:
|
|
11
|
+
1. `pay_status`
|
|
12
|
+
2. `pay_search_services`
|
|
13
|
+
3. `pay_get_service_endpoints`
|
|
14
|
+
4. `pay_api_request`
|
|
15
|
+
|
|
16
|
+
## Notes
|
|
17
|
+
|
|
18
|
+
- `pay_api_request` requires `purpose` and `user_confirmed=true`.
|
|
19
|
+
- Use the exact gateway URL returned by `pay_get_service_endpoints`.
|
|
20
|
+
- If `pay_status` shows no configured account, stop and ask the user to finish `pay setup`.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
endpointPayloadContainsUrl,
|
|
5
|
+
parseAccountListOutput,
|
|
6
|
+
parseWhoamiOutput,
|
|
7
|
+
} from "./core.mjs";
|
|
8
|
+
|
|
9
|
+
const whoami = parseWhoamiOutput(`
|
|
10
|
+
yuriytsygankov
|
|
11
|
+
\u001b[2m(no mainnet account — run \`pay setup\`)\u001b[0m
|
|
12
|
+
`);
|
|
13
|
+
assert.equal(whoami.system_user, "yuriytsygankov");
|
|
14
|
+
assert.equal(whoami.has_mainnet_account, false);
|
|
15
|
+
|
|
16
|
+
const accounts = parseAccountListOutput(`
|
|
17
|
+
\u001b[2mNo accounts found. Run \`pay account new\` to create one.\u001b[0m
|
|
18
|
+
`);
|
|
19
|
+
assert.equal(accounts.has_accounts, false);
|
|
20
|
+
|
|
21
|
+
const endpointPayload = {
|
|
22
|
+
endpoints: [
|
|
23
|
+
{
|
|
24
|
+
method: "POST",
|
|
25
|
+
url: "https://api.example.com/v1/invoke",
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
};
|
|
29
|
+
assert.equal(
|
|
30
|
+
endpointPayloadContainsUrl(endpointPayload, "https://api.example.com/v1/invoke"),
|
|
31
|
+
true
|
|
32
|
+
);
|
|
33
|
+
assert.equal(
|
|
34
|
+
endpointPayloadContainsUrl(endpointPayload, "https://api.example.com/v1/other"),
|
|
35
|
+
false
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
console.log("smoke_pay_bridge: ok");
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## v0.1.14 - 2026-05-13
|
|
6
|
+
|
|
7
|
+
- Added a separate `.openclaw/extensions/pay-bridge/` plugin that keeps
|
|
8
|
+
`pay.sh` API payments outside the main AgentLayer execution wallet stack.
|
|
9
|
+
- Added OpenClaw tools for local `pay` discovery and execution:
|
|
10
|
+
`pay_status`, `pay_wallet_info`, `pay_search_services`,
|
|
11
|
+
`pay_get_service_endpoints`, and `pay_api_request`.
|
|
12
|
+
- Updated the local OpenClaw installer/runtime config flow to package and
|
|
13
|
+
enable the `pay-bridge` plugin alongside `agent-wallet`, including its
|
|
14
|
+
tool allowlist and absolute `pay` binary path when available.
|
|
5
15
|
- Added an optional Hermes Agent bridge plugin under `hermes/plugins/agent_wallet`
|
|
6
16
|
that forwards into the existing Python wallet CLI instead of duplicating
|
|
7
17
|
OpenClaw wallet tools or policy.
|
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ AgentLayer is a beta local-first wallet and finance stack for agents.
|
|
|
18
18
|
The repository includes:
|
|
19
19
|
|
|
20
20
|
- `agent-wallet/` - the main wallet backend for AgentLayer
|
|
21
|
-
- `.openclaw/` - the local AgentLayer bridge layer
|
|
21
|
+
- `.openclaw/` - the local AgentLayer bridge layer, including the OpenClaw wallet bridge and the `pay.sh` API-payments bridge
|
|
22
22
|
- `hermes/` - optional Hermes Agent plugin bridge for the same wallet backend
|
|
23
23
|
- `wdk-btc-wallet/` - the local Bitcoin wallet service
|
|
24
24
|
- `wdk-evm-wallet/` - the local EVM wallet service
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
"""Patch an OpenClaw config file for the
|
|
1
|
+
"""Patch an OpenClaw config file for the AgentLayer OpenClaw plugins."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import argparse
|
|
6
6
|
import json
|
|
7
7
|
import os
|
|
8
|
+
import shutil
|
|
8
9
|
import sys
|
|
9
10
|
from datetime import datetime, timezone
|
|
10
11
|
from pathlib import Path
|
|
@@ -44,6 +45,15 @@ OPTIONAL_TOOLS = [
|
|
|
44
45
|
"request_devnet_airdrop",
|
|
45
46
|
]
|
|
46
47
|
|
|
48
|
+
PAY_BRIDGE_PLUGIN_ID = "pay-bridge"
|
|
49
|
+
PAY_BRIDGE_TOOLS = [
|
|
50
|
+
"pay_status",
|
|
51
|
+
"pay_wallet_info",
|
|
52
|
+
"pay_search_services",
|
|
53
|
+
"pay_get_service_endpoints",
|
|
54
|
+
"pay_api_request",
|
|
55
|
+
]
|
|
56
|
+
|
|
47
57
|
|
|
48
58
|
def _default_config_path() -> Path:
|
|
49
59
|
return Path(os.path.expanduser("~/.openclaw/openclaw.json"))
|
|
@@ -83,6 +93,13 @@ def _default_extension_path() -> Path:
|
|
|
83
93
|
return _repo_root() / ".openclaw" / "extensions" / "agent-wallet"
|
|
84
94
|
|
|
85
95
|
|
|
96
|
+
def _default_pay_bridge_extension_path() -> Path:
|
|
97
|
+
runtime_root = _trusted_runtime_root()
|
|
98
|
+
if runtime_root is not None:
|
|
99
|
+
return runtime_root / ".openclaw" / "extensions" / PAY_BRIDGE_PLUGIN_ID
|
|
100
|
+
return _repo_root() / ".openclaw" / "extensions" / PAY_BRIDGE_PLUGIN_ID
|
|
101
|
+
|
|
102
|
+
|
|
86
103
|
def _default_package_root() -> Path:
|
|
87
104
|
runtime_root = _trusted_runtime_root()
|
|
88
105
|
if runtime_root is not None:
|
|
@@ -105,6 +122,14 @@ def _default_python_bin() -> str:
|
|
|
105
122
|
return sys.executable
|
|
106
123
|
|
|
107
124
|
|
|
125
|
+
def _default_pay_binary() -> str:
|
|
126
|
+
explicit = os.getenv("OPENCLAW_PAY_BINARY", "").strip()
|
|
127
|
+
if explicit:
|
|
128
|
+
return explicit
|
|
129
|
+
resolved = shutil.which("pay")
|
|
130
|
+
return resolved or "pay"
|
|
131
|
+
|
|
132
|
+
|
|
108
133
|
def _default_user_id() -> str:
|
|
109
134
|
return f"{os.getenv('USER', 'openclaw-user')}-local"
|
|
110
135
|
|
|
@@ -142,8 +167,10 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
142
167
|
default=True,
|
|
143
168
|
)
|
|
144
169
|
parser.add_argument("--extension-path", default=str(_default_extension_path()))
|
|
170
|
+
parser.add_argument("--pay-bridge-extension-path", default=str(_default_pay_bridge_extension_path()))
|
|
145
171
|
parser.add_argument("--package-root", default=str(_default_package_root()))
|
|
146
172
|
parser.add_argument("--python-bin", default=_default_python_bin())
|
|
173
|
+
parser.add_argument("--pay-binary", default=_default_pay_binary())
|
|
147
174
|
parser.add_argument("--write-master-key", action=argparse.BooleanOptionalAction, default=False)
|
|
148
175
|
return parser
|
|
149
176
|
|
|
@@ -214,12 +241,20 @@ def main() -> None:
|
|
|
214
241
|
|
|
215
242
|
plugins = data.setdefault("plugins", {})
|
|
216
243
|
plugins["enabled"] = True
|
|
244
|
+
allow = plugins.setdefault("allow", [])
|
|
245
|
+
if args.plugin_id not in allow:
|
|
246
|
+
allow.append(args.plugin_id)
|
|
247
|
+
if PAY_BRIDGE_PLUGIN_ID not in allow:
|
|
248
|
+
allow.append(PAY_BRIDGE_PLUGIN_ID)
|
|
217
249
|
|
|
218
250
|
load = plugins.setdefault("load", {})
|
|
219
251
|
paths = load.setdefault("paths", [])
|
|
220
252
|
extension_path_text = str(Path(args.extension_path).expanduser().resolve())
|
|
221
253
|
if extension_path_text not in paths:
|
|
222
254
|
paths.append(extension_path_text)
|
|
255
|
+
pay_bridge_extension_path_text = str(Path(args.pay_bridge_extension_path).expanduser().resolve())
|
|
256
|
+
if pay_bridge_extension_path_text not in paths:
|
|
257
|
+
paths.append(pay_bridge_extension_path_text)
|
|
223
258
|
|
|
224
259
|
entries = plugins.setdefault("entries", {})
|
|
225
260
|
effective_network = _normalize_network(args.backend, args.network)
|
|
@@ -272,10 +307,25 @@ def main() -> None:
|
|
|
272
307
|
"enabled": True,
|
|
273
308
|
"config": plugin_config,
|
|
274
309
|
}
|
|
310
|
+
existing_pay_entry = entries.get(PAY_BRIDGE_PLUGIN_ID) if isinstance(entries.get(PAY_BRIDGE_PLUGIN_ID), dict) else {}
|
|
311
|
+
existing_pay_config = (
|
|
312
|
+
dict(existing_pay_entry.get("config"))
|
|
313
|
+
if isinstance(existing_pay_entry.get("config"), dict)
|
|
314
|
+
else {}
|
|
315
|
+
)
|
|
316
|
+
pay_bridge_config = {
|
|
317
|
+
**existing_pay_config,
|
|
318
|
+
"payBinary": args.pay_binary.strip() or _default_pay_binary(),
|
|
319
|
+
"requireHttps": bool(existing_pay_config.get("requireHttps", True)),
|
|
320
|
+
}
|
|
321
|
+
entries[PAY_BRIDGE_PLUGIN_ID] = {
|
|
322
|
+
"enabled": True,
|
|
323
|
+
"config": pay_bridge_config,
|
|
324
|
+
}
|
|
275
325
|
|
|
276
326
|
tools = data.setdefault("tools", {})
|
|
277
327
|
also_allow = tools.setdefault("alsoAllow", [])
|
|
278
|
-
for tool_name in OPTIONAL_TOOLS:
|
|
328
|
+
for tool_name in OPTIONAL_TOOLS + PAY_BRIDGE_TOOLS:
|
|
279
329
|
if tool_name not in also_allow:
|
|
280
330
|
also_allow.append(tool_name)
|
|
281
331
|
|
|
@@ -291,6 +341,7 @@ def main() -> None:
|
|
|
291
341
|
"config_path": str(config_path),
|
|
292
342
|
"backup_path": str(backup_path),
|
|
293
343
|
"extension_path": extension_path_text,
|
|
344
|
+
"pay_bridge_extension_path": pay_bridge_extension_path_text,
|
|
294
345
|
"python_bin": args.python_bin,
|
|
295
346
|
"package_root": plugin_config["packageRoot"],
|
|
296
347
|
"plugin_id": args.plugin_id,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentlayer.tech/wallet",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.14",
|
|
4
4
|
"description": "NPM installer for the OpenClaw Agent Wallet local runtime.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
"agent-wallet/pyproject.toml",
|
|
39
39
|
".openclaw/AGENTS.md",
|
|
40
40
|
".openclaw/extensions/agent-wallet/",
|
|
41
|
+
".openclaw/extensions/pay-bridge/",
|
|
41
42
|
"hermes/plugins/agent_wallet/",
|
|
42
43
|
"wdk-btc-wallet/src/",
|
|
43
44
|
"wdk-btc-wallet/bootstrap.sh",
|
|
@@ -69,4 +70,4 @@
|
|
|
69
70
|
"evm"
|
|
70
71
|
],
|
|
71
72
|
"license": "SEE LICENSE IN LICENSE"
|
|
72
|
-
}
|
|
73
|
+
}
|