agent402-openclaw 0.5.9 → 0.5.10
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/README.md +12 -0
- package/cli.js +1 -1
- package/index.js +8 -4
- package/known-assets.js +66 -8
- package/package.json +1 -1
- package/proxy.js +5 -2
package/README.md
CHANGED
|
@@ -87,6 +87,13 @@ signs; an option in any other asset is not signed while a ceiling is set. Once
|
|
|
87
87
|
the payment is built, the amount it actually signs is read back and checked
|
|
88
88
|
against the same price and ceiling before it is sent.
|
|
89
89
|
|
|
90
|
+
A signed payment also has a validity ceiling of 24 hours: a payment option
|
|
91
|
+
that asks to stay redeemable for longer (its `maxTimeoutSeconds`) is not
|
|
92
|
+
signed, and a built payment whose expiry (`validBefore` or the Permit2
|
|
93
|
+
`deadline`) lies more than 24 hours out is refused before it is sent. Any
|
|
94
|
+
refusal happens before the payment leaves the machine, and the call's error
|
|
95
|
+
names the reason.
|
|
96
|
+
|
|
90
97
|
To run with no ceiling, set `maxPerCallUsd` to `0` or `"off"` together with
|
|
91
98
|
`allowUncappedPayments: true` (or `AGENT402_ALLOW_UNCAPPED_PAYMENTS=true`).
|
|
92
99
|
Without that acknowledgment `off` keeps the default ceiling and the proxy
|
|
@@ -125,6 +132,11 @@ The proxy answers native clients on loopback only: requests carrying a browser
|
|
|
125
132
|
`Origin` header or a non-loopback `Host` are refused, so a web page cannot spend
|
|
126
133
|
the key.
|
|
127
134
|
|
|
135
|
+
The credits key stays out of what the proxy prints: an upstream or network
|
|
136
|
+
error is logged and answered (the 502 body) with the key cut out as
|
|
137
|
+
`[redacted]`, and `agent402-openclaw doctor` reports the key as `present` or
|
|
138
|
+
`none`, never any part of it.
|
|
139
|
+
|
|
128
140
|
## Commands
|
|
129
141
|
|
|
130
142
|
- `agent402-openclaw setup [--credits-key K | --credits-key - (stdin) | AGENT402_CREDITS_KEY env] [--no-wallet] [--write] [--port N] [--flat]`
|
package/cli.js
CHANGED
|
@@ -187,7 +187,7 @@ async function main() {
|
|
|
187
187
|
const key = resolveCreditsKey();
|
|
188
188
|
const walletPk = resolveWalletKey();
|
|
189
189
|
out(`upstream: ${upstream}`);
|
|
190
|
-
out(`credits key: ${key ?
|
|
190
|
+
out(`credits key: ${key ? "present" : "none"}`);
|
|
191
191
|
out(`wallet key: ${walletPk ? "present" : "none"}`);
|
|
192
192
|
if (walletPk) {
|
|
193
193
|
try {
|
package/index.js
CHANGED
|
@@ -8,8 +8,8 @@ import { join } from "node:path";
|
|
|
8
8
|
import { buildProvider, setActiveProxy, setCachedRoutes, DEFAULT_PORT, PLUGIN_ID } from "./provider.js";
|
|
9
9
|
import { startProxy, loadRoutes, DEFAULT_UPSTREAM } from "./proxy.js";
|
|
10
10
|
import { stripTrailingSlashes } from "./models.js";
|
|
11
|
-
import { acceptUsd, guardSignedAmount } from "./known-assets.js";
|
|
12
|
-
export { acceptUsd, acceptAmount, signedPaymentProblem, KNOWN_ASSETS } from "./known-assets.js";
|
|
11
|
+
import { acceptUsd, guardSignedAmount, acceptValidityTooLong, MAX_VALIDITY_SECONDS } from "./known-assets.js";
|
|
12
|
+
export { acceptUsd, acceptAmount, signedPaymentProblem, signedValidityProblem, KNOWN_ASSETS, MAX_VALIDITY_SECONDS } from "./known-assets.js";
|
|
13
13
|
|
|
14
14
|
export const STATE_DIR = () => join(process.env.AGENT402_OPENCLAW_HOME || join(homedir(), ".openclaw"), "agent402");
|
|
15
15
|
export const CREDITS_KEY_FILE = () => join(STATE_DIR(), "credits.key");
|
|
@@ -87,9 +87,13 @@ export function maxPerCallUsd(pluginConfig = {}) {
|
|
|
87
87
|
// at different values, reads as null, and null is REFUSED under a finite
|
|
88
88
|
// ceiling: a quote we cannot price is not one we sign. After the payload is
|
|
89
89
|
// built, guardSignedAmount checks the amount actually signed against the same
|
|
90
|
-
// figure and the ceiling before anything is sent.
|
|
90
|
+
// figure and the ceiling before anything is sent. An accept that asks to stay
|
|
91
|
+
// valid past MAX_VALIDITY_SECONDS (a day) is never a candidate, ceiling or no
|
|
92
|
+
// ceiling: the signed authorization would be a bearer claim for that long.
|
|
91
93
|
export function selectAccept(accepts, { preferUpto = true, maxUsd = Infinity, x402Version = undefined } = {}) {
|
|
92
|
-
const
|
|
94
|
+
const offered = Array.isArray(accepts) ? accepts : [];
|
|
95
|
+
const list = offered.filter((a) => !acceptValidityTooLong(a));
|
|
96
|
+
if (offered.length && !list.length) throw new Error(`agent402-openclaw: every payment option asks to stay valid for more than ${MAX_VALIDITY_SECONDS} seconds; nothing was signed`);
|
|
93
97
|
const pick = (preferUpto && list.find((a) => a?.scheme === "upto" && a?.network === BASE_CAIP2))
|
|
94
98
|
|| list.find((a) => a?.scheme === "exact" && a?.network === BASE_CAIP2) || list[0];
|
|
95
99
|
if (pick && Number.isFinite(maxUsd)) {
|
package/known-assets.js
CHANGED
|
@@ -134,6 +134,51 @@ export function quoteOf(accepts, x402Version = undefined) {
|
|
|
134
134
|
return { max, min, unknown, total: accepts.length };
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
+
// The longest a signed payment may stay redeemable. An EVM "exact" signer
|
|
138
|
+
// sets validBefore (EIP-3009) or deadline (Permit2) to the moment of signing
|
|
139
|
+
// plus the accept's maxTimeoutSeconds, and the signed payload is a bearer
|
|
140
|
+
// claim until then, so a seller that quotes years gets an authorization it
|
|
141
|
+
// could redeem for years and a held amount that never stops counting. Honest
|
|
142
|
+
// accepts ask for minutes (Agent402's own ask 300 s); a day admits every
|
|
143
|
+
// one of them. An accept that asks for longer is dropped before selection,
|
|
144
|
+
// and a payload signed past it is refused before it is sent.
|
|
145
|
+
export const MAX_VALIDITY_SECONDS = 24 * 3600;
|
|
146
|
+
// Signing reads the clock a moment before the check after it does.
|
|
147
|
+
const VALIDITY_CLOCK_SLACK_SECONDS = 60;
|
|
148
|
+
|
|
149
|
+
/** True when an accept asks for a validity window past MAX_VALIDITY_SECONDS.
|
|
150
|
+
* An accept with no readable maxTimeoutSeconds is left to the check of what
|
|
151
|
+
* was signed (signedValidityProblem). */
|
|
152
|
+
export function acceptValidityTooLong(accept, maxSeconds = MAX_VALIDITY_SECONDS) {
|
|
153
|
+
const raw = accept?.maxTimeoutSeconds;
|
|
154
|
+
if (raw == null || raw === "") return false;
|
|
155
|
+
const n = Number(raw);
|
|
156
|
+
return !Number.isFinite(n) || n > maxSeconds;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** The expiry a signed payload carries, in unix seconds: an EIP-3009
|
|
160
|
+
* `authorization.validBefore` or a Permit2 `deadline`. null when the payload
|
|
161
|
+
* names none (a Solana transaction, an unknown scheme). NaN when it names
|
|
162
|
+
* one that is not a whole number of seconds. */
|
|
163
|
+
export function signedExpirySeconds(paymentPayload) {
|
|
164
|
+
const p = paymentPayload?.payload;
|
|
165
|
+
const v = p?.authorization?.validBefore ?? p?.permit2Authorization?.deadline;
|
|
166
|
+
if (v == null) return null;
|
|
167
|
+
return /^\d{1,15}$/.test(String(v)) ? Number(v) : NaN;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Why a created payment payload stays redeemable for too long, or null: its
|
|
171
|
+
* expiry is past `nowMs` plus `maxSeconds` (and a minute of clock slack), or
|
|
172
|
+
* it names an expiry that cannot be read. */
|
|
173
|
+
export function signedValidityProblem(paymentPayload, { maxSeconds = MAX_VALIDITY_SECONDS, nowMs = Date.now() } = {}) {
|
|
174
|
+
const exp = signedExpirySeconds(paymentPayload);
|
|
175
|
+
if (exp == null) return null;
|
|
176
|
+
if (!Number.isFinite(exp)) return "the signed authorization's expiry cannot be read";
|
|
177
|
+
const left = exp - Math.floor(nowMs / 1000);
|
|
178
|
+
if (left > maxSeconds + VALIDITY_CLOCK_SLACK_SECONDS) return `the signed authorization stays valid for ${left} seconds, past the ${maxSeconds}-second ceiling`;
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
|
|
137
182
|
/** Enforce a USD allowance at signing on an @x402/core x402Client.
|
|
138
183
|
*
|
|
139
184
|
* `allowance()` is the most this buyer may sign for the payment being built
|
|
@@ -141,8 +186,10 @@ export function quoteOf(accepts, x402Version = undefined) {
|
|
|
141
186
|
* for none). A registered policy drops every accept that cannot be priced or
|
|
142
187
|
* is over the allowance, so the client selects among the ones that fit, and a
|
|
143
188
|
* hook before payload creation checks the selected accept again and passes
|
|
144
|
-
* its USD value to `onSign`.
|
|
145
|
-
*
|
|
189
|
+
* its USD value to `onSign`. An accept asking for a validity window past
|
|
190
|
+
* MAX_VALIDITY_SECONDS is dropped and refused the same way. A refusal throws
|
|
191
|
+
* (from the policy) or aborts (from the hook) before any signature exists;
|
|
192
|
+
* `onRefuse` hears why first. */
|
|
146
193
|
export function guardSigning(client, { allowance, onSign = null, onRefuse = null }) {
|
|
147
194
|
if (!client || typeof client.registerPolicy !== "function" || typeof client.onBeforePaymentCreation !== "function") {
|
|
148
195
|
throw new TypeError("guardSigning needs an @x402/core x402Client (registerPolicy and onBeforePaymentCreation)");
|
|
@@ -151,7 +198,12 @@ export function guardSigning(client, { allowance, onSign = null, onRefuse = null
|
|
|
151
198
|
const refuse = (detail) => { try { onRefuse?.(detail); } catch { /* the refusal stands */ } return detail; };
|
|
152
199
|
client.registerPolicy((version, accepts) => {
|
|
153
200
|
const cap = allowance();
|
|
154
|
-
const
|
|
201
|
+
const priced = accepts.filter((a) => { const usd = acceptUsd(a, version); return usd != null && usd <= cap; });
|
|
202
|
+
const kept = priced.filter((a) => !acceptValidityTooLong(a));
|
|
203
|
+
if (!kept.length && priced.length) {
|
|
204
|
+
const d = refuse({ reason: "validity", priceUsd: null, allowanceUsd: cap, detail: `every payment option it may sign asks to stay valid for more than ${MAX_VALIDITY_SECONDS} seconds` });
|
|
205
|
+
throw new Error(`spending guard: ${d.detail}; nothing was signed`);
|
|
206
|
+
}
|
|
155
207
|
if (!kept.length) {
|
|
156
208
|
const q = quoteOf(accepts, version);
|
|
157
209
|
const d = refuse({ reason: q && q.min != null ? "over-allowance" : "unknown-asset", priceUsd: q ? q.min : null, allowanceUsd: cap });
|
|
@@ -172,6 +224,10 @@ export function guardSigning(client, { allowance, onSign = null, onRefuse = null
|
|
|
172
224
|
refuse({ reason: "over-allowance", priceUsd: usd, allowanceUsd: cap });
|
|
173
225
|
return { abort: true, reason: `spending guard: the selected payment option asks $${usd}, above the $${cap} this buyer may sign; nothing was signed` };
|
|
174
226
|
}
|
|
227
|
+
if (acceptValidityTooLong(r)) {
|
|
228
|
+
const d = refuse({ reason: "validity", priceUsd: usd, allowanceUsd: cap, detail: `the selected payment option asks to stay valid for ${r.maxTimeoutSeconds} seconds, past the ${MAX_VALIDITY_SECONDS}-second ceiling` });
|
|
229
|
+
return { abort: true, reason: `spending guard: ${d.detail}; nothing was signed` };
|
|
230
|
+
}
|
|
175
231
|
onSign?.(usd, r);
|
|
176
232
|
return undefined;
|
|
177
233
|
});
|
|
@@ -252,9 +308,10 @@ export async function signedPaymentProblem(paymentPayload, requirements, x402Ver
|
|
|
252
308
|
}
|
|
253
309
|
|
|
254
310
|
/** Refuse to send a payment payload whose signed amount is not the one the
|
|
255
|
-
* allowance passed
|
|
256
|
-
*
|
|
257
|
-
*
|
|
311
|
+
* allowance passed, or whose expiry is past MAX_VALIDITY_SECONDS from now
|
|
312
|
+
* (signedValidityProblem): registered after payload creation, so the check
|
|
313
|
+
* reads what was actually signed, and a throw here stops the payment-aware
|
|
314
|
+
* fetch before the payload leaves the process. guardSigning registers it; a
|
|
258
315
|
* client with its own selector (and no guardSigning) can register it alone. */
|
|
259
316
|
export function guardSignedAmount(client, { allowance = () => Infinity, onRefuse = null } = {}) {
|
|
260
317
|
if (!client || typeof client.onAfterPaymentCreation !== "function") {
|
|
@@ -262,9 +319,10 @@ export function guardSignedAmount(client, { allowance = () => Infinity, onRefuse
|
|
|
262
319
|
}
|
|
263
320
|
client.onAfterPaymentCreation(async ({ paymentRequired, selectedRequirements: r, paymentPayload }) => {
|
|
264
321
|
const cap = allowance();
|
|
265
|
-
const
|
|
322
|
+
const amountProblem = await signedPaymentProblem(paymentPayload, r, paymentRequired?.x402Version, cap);
|
|
323
|
+
const problem = amountProblem || signedValidityProblem(paymentPayload);
|
|
266
324
|
if (!problem) return;
|
|
267
|
-
try { onRefuse?.({ reason: "signed-amount", priceUsd: null, allowanceUsd: cap, detail: problem }); } catch { /* the refusal stands */ }
|
|
325
|
+
try { onRefuse?.({ reason: amountProblem ? "signed-amount" : "signed-validity", priceUsd: null, allowanceUsd: cap, detail: problem }); } catch { /* the refusal stands */ }
|
|
268
326
|
throw new Error(`spending guard: ${problem}; the payment was not sent`);
|
|
269
327
|
});
|
|
270
328
|
return client;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent402-openclaw",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.10",
|
|
4
4
|
"description": "OpenClaw model provider for Agent402: an auto-routed tier at a flat per-call price and explicit LLM models priced per request, paid in USDC over x402, or with a prepaid credits key, through a local proxy. Zero dependencies; x402 wallet payment is an optional peer.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
package/proxy.js
CHANGED
|
@@ -270,9 +270,12 @@ export async function startProxy({ upstream = DEFAULT_UPSTREAM, creditsKey = nul
|
|
|
270
270
|
return json(res, 404, { error: { message: `No route for ${req.method} ${url.pathname}`, type: "invalid_request_error" } });
|
|
271
271
|
} catch (e) {
|
|
272
272
|
stats.errors++;
|
|
273
|
-
|
|
273
|
+
// A thrown message is the payment fetch's or the network's own text,
|
|
274
|
+
// so the credits key is cut out of it before it is logged or answered.
|
|
275
|
+
const msg = key ? String(e?.message || e).split(key).join("[redacted]") : String(e?.message || e);
|
|
276
|
+
log(`[agent402-openclaw] ${req.method} ${url.pathname}: ${msg}`);
|
|
274
277
|
const wire = url.pathname === "/v1/messages" ? ANTHROPIC_WIRE : OPENAI_WIRE;
|
|
275
|
-
if (!res.headersSent) return json(res, 502, wire.error("upstream_error", `Upstream error: ${
|
|
278
|
+
if (!res.headersSent) return json(res, 502, wire.error("upstream_error", `Upstream error: ${msg.slice(0, 200)}`));
|
|
276
279
|
try { res.end(); } catch { /* ignore */ }
|
|
277
280
|
}
|
|
278
281
|
});
|