@oneshot-agent/sdk 0.60.0 → 0.62.0
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 +16 -3
- package/dist/index.d.ts +95 -161
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +183 -372
- package/dist/index.js.map +1 -1
- package/dist/predict-input.generated.d.ts +3 -3
- package/dist/predict-input.generated.d.ts.map +1 -1
- package/dist/recovery.d.ts +13 -0
- package/dist/recovery.d.ts.map +1 -0
- package/dist/recovery.js +3 -0
- package/dist/recovery.js.map +1 -0
- package/dist/solve-input.generated.d.ts +14 -14
- package/dist/solve-input.generated.d.ts.map +1 -1
- package/dist/tool-timeouts.generated.d.ts +3 -0
- package/dist/tool-timeouts.generated.d.ts.map +1 -0
- package/dist/tool-timeouts.generated.js +9 -0
- package/dist/tool-timeouts.generated.js.map +1 -0
- package/dist/types.d.ts +279 -90
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +0 -4
- package/dist/types.js.map +1 -1
- package/dist/untrusted-content.d.ts +8 -0
- package/dist/untrusted-content.d.ts.map +1 -0
- package/dist/untrusted-content.js +26 -0
- package/dist/untrusted-content.js.map +1 -0
- package/dist/untrusted-tools.generated.d.ts +3 -0
- package/dist/untrusted-tools.generated.d.ts.map +1 -0
- package/dist/untrusted-tools.generated.js +415 -0
- package/dist/untrusted-tools.generated.js.map +1 -0
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -37,6 +37,8 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
37
37
|
})();
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
39
|
exports.OneShot = exports.executeSwap = exports.getSwapQuote = exports.ReadOnlyWalletProvider = exports.CdpWalletProvider = exports.EthersWalletProvider = void 0;
|
|
40
|
+
const untrusted_content_1 = require("./untrusted-content");
|
|
41
|
+
const tool_timeouts_generated_1 = require("./tool-timeouts.generated");
|
|
40
42
|
const hunts_1 = require("./hunts");
|
|
41
43
|
__exportStar(require("./solve"), exports);
|
|
42
44
|
__exportStar(require("./predict"), exports);
|
|
@@ -61,13 +63,9 @@ Object.defineProperty(exports, "getSwapQuote", { enumerable: true, get: function
|
|
|
61
63
|
Object.defineProperty(exports, "executeSwap", { enumerable: true, get: function () { return swap_1.executeSwap; } });
|
|
62
64
|
__exportStar(require("./errors"), exports);
|
|
63
65
|
const approvals_1 = require("./approvals");
|
|
64
|
-
//
|
|
65
|
-
// `node:crypto`, which would force a hard Node dependency on every consumer
|
|
66
|
-
// including browser bundles; and its `SignableReceipt.providerCost` field must
|
|
67
|
-
// stay off this public surface (see tests/unit/sdk-provider-cost-removed.test.ts).
|
|
68
|
-
// Import from the `@oneshot-agent/sdk/receipt` subpath instead.
|
|
66
|
+
// Keep receipt on its subpath: node:crypto breaks browser bundles, and providerCost is not public.
|
|
69
67
|
// Keep in sync with package.json `version`. Guarded by version.test.ts.
|
|
70
|
-
const SDK_VERSION = '0.
|
|
68
|
+
const SDK_VERSION = '0.62.0';
|
|
71
69
|
/** HTTP poll cadence while push is unconfirmed: fast first checks, settling at 2s. */
|
|
72
70
|
const HTTP_POLL_BACKOFF_MS = [300, 600, 1000, 2000];
|
|
73
71
|
/** HTTP poll cadence once the WebSocket has delivered for this request. */
|
|
@@ -106,12 +104,7 @@ function chainIdFromNetwork(network) {
|
|
|
106
104
|
return m ? Number(m[1]) : undefined;
|
|
107
105
|
}
|
|
108
106
|
const USDC_ADDRESS = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';
|
|
109
|
-
//
|
|
110
|
-
// facilitator settles AFTER the API has responded, so for seconds after a
|
|
111
|
-
// successful call `balanceOf` still shows pre-payment funds. The SDK therefore
|
|
112
|
-
// keeps a ledger of signed-but-unsettled authorizations and works from
|
|
113
|
-
// `balanceOf − pending`, over-reserving (one early buffered swap) rather than
|
|
114
|
-
// under-reserving (a failed settlement).
|
|
107
|
+
// Reserve signed-but-unsettled payments against balanceOf so delayed settlement cannot overspend USDC.
|
|
115
108
|
/** One balanceOf read per paid call, deduped within roughly one Base block. */
|
|
116
109
|
const USDC_BALANCE_CACHE_MS = 2000;
|
|
117
110
|
/** How long a signed authorization stays subtracted from the on-chain balance (settlement lands well within this). */
|
|
@@ -139,10 +132,7 @@ __exportStar(require("./types"), exports);
|
|
|
139
132
|
* await agent.email({ to: 'user@example.com', subject: 'Hi', body: 'Hello' });
|
|
140
133
|
* ```
|
|
141
134
|
*/
|
|
142
|
-
/**
|
|
143
|
-
* Reject a budget config the server would reject, at construction rather than
|
|
144
|
-
* on the first paid call — a typo'd cap must not become "no cap".
|
|
145
|
-
*/
|
|
135
|
+
/** Reject invalid budgets at construction so a typo cannot silently remove the cap. */
|
|
146
136
|
function validateBudgetConfig(budgets) {
|
|
147
137
|
if (!budgets)
|
|
148
138
|
return undefined;
|
|
@@ -174,13 +164,13 @@ function validateBudgetConfig(budgets) {
|
|
|
174
164
|
}
|
|
175
165
|
/** The keys executeToolRequestImpl takes out of a request as SDK options, so they never reach the API body. */
|
|
176
166
|
const PAID_ROUTE_OPTION_KEYS = ['timeout', 'totalTimeoutMs', 'onRequestCreated', 'onAccepted', 'signal', 'onStatusUpdate', 'wait', 'waitForPhones', 'phoneTimeoutSec', 'idempotencyKey', 'approvalId', 'maxCost'];
|
|
177
|
-
/** ETH mode is unsupported on CDP wallets
|
|
167
|
+
/** ETH mode is unsupported on CDP wallets: the swap path needs a confirmed receipt CDP never returns. */
|
|
178
168
|
function cdpEthModeError() {
|
|
179
169
|
return new errors_2.ValidationError("ETH currency mode is not supported with CDP wallets; fund the CDP wallet with USDC or use currency: 'USDC' (or a privateKey / custom walletProvider for ETH mode)", 'currency');
|
|
180
170
|
}
|
|
181
171
|
class OneShot {
|
|
182
172
|
/**
|
|
183
|
-
* Async factory
|
|
173
|
+
* Async factory, required for CDP wallets (account creation is async).
|
|
184
174
|
* Also works with privateKey and custom walletProvider.
|
|
185
175
|
*
|
|
186
176
|
* @example
|
|
@@ -196,8 +186,7 @@ class OneShot {
|
|
|
196
186
|
if (config.accessToken) {
|
|
197
187
|
if (config.address)
|
|
198
188
|
return new OneShot(config);
|
|
199
|
-
//
|
|
200
|
-
// session needs its wallet address even though it never signs with it.
|
|
189
|
+
// Job status and WebSocket requests need the wallet address even when the session never signs.
|
|
201
190
|
const address = await OneShot.resolveTokenAddress(config);
|
|
202
191
|
return new OneShot({ ...config, address });
|
|
203
192
|
}
|
|
@@ -205,7 +194,7 @@ class OneShot {
|
|
|
205
194
|
return new OneShot(config, config.walletProvider);
|
|
206
195
|
}
|
|
207
196
|
if (config.cdp) {
|
|
208
|
-
// Reject before the CDP SDK
|
|
197
|
+
// Reject before importing the CDP SDK or creating an account.
|
|
209
198
|
if ((config.currency ?? 'USDC') === 'ETH')
|
|
210
199
|
throw cdpEthModeError();
|
|
211
200
|
const { CdpWalletProvider } = await Promise.resolve().then(() => __importStar(require('./providers/cdp')));
|
|
@@ -221,7 +210,7 @@ class OneShot {
|
|
|
221
210
|
throw new errors_2.ValidationError('Provide one of: privateKey, cdp, or walletProvider', 'config');
|
|
222
211
|
}
|
|
223
212
|
/**
|
|
224
|
-
* Sync constructor
|
|
213
|
+
* Sync constructor; works with privateKey.
|
|
225
214
|
* For CDP wallets, use OneShot.create() instead.
|
|
226
215
|
*/
|
|
227
216
|
constructor(config, walletProvider) {
|
|
@@ -260,9 +249,7 @@ class OneShot {
|
|
|
260
249
|
this._stagingKey = config.stagingKey;
|
|
261
250
|
this.rpcProvider = new ethers_1.ethers.JsonRpcProvider(config.rpcUrl ?? RPC_URL);
|
|
262
251
|
if (config.accessToken) {
|
|
263
|
-
//
|
|
264
|
-
// and budgets are read-only (a delegated credential that could raise its
|
|
265
|
-
// own cap would make the cap meaningless) — set them from a wallet.
|
|
252
|
+
// Delegated tokens cannot change their own budgets; caps must be set from a wallet session.
|
|
266
253
|
if (!/^oneshot_[0-9a-f]{64}$/.test(config.accessToken)) {
|
|
267
254
|
throw new errors_2.ValidationError('accessToken must be oneshot_<64 lowercase hex characters>', 'accessToken');
|
|
268
255
|
}
|
|
@@ -297,9 +284,7 @@ class OneShot {
|
|
|
297
284
|
else {
|
|
298
285
|
throw new errors_2.ValidationError('Provide privateKey or use OneShot.create() for CDP/custom wallets', 'config');
|
|
299
286
|
}
|
|
300
|
-
// CDP
|
|
301
|
-
// sendTransaction: its wait() never observes a receipt, so the swap path
|
|
302
|
-
// cannot confirm the ETH→USDC trade (#2153).
|
|
287
|
+
// CDP wait() never confirms a receipt, so ETH mode cannot confirm the swap.
|
|
303
288
|
if (config.cdp && this._currency === 'ETH')
|
|
304
289
|
throw cdpEthModeError();
|
|
305
290
|
if (this._currency === 'ETH' && !this.provider.sendTransaction) {
|
|
@@ -342,16 +327,8 @@ class OneShot {
|
|
|
342
327
|
this.validate(options.subject, 'subject');
|
|
343
328
|
}
|
|
344
329
|
this.validate(options.body, 'body');
|
|
345
|
-
//
|
|
346
|
-
//
|
|
347
|
-
// the agent's domain pool. The chosen address comes back on the
|
|
348
|
-
// quote response (`quote.from_address`) and we replay it on /send.
|
|
349
|
-
// When the caller pins from_domain (with or without from_mailbox), we
|
|
350
|
-
// build `${mailbox ?? 'agent'}@${from_domain}`. Pinning from_mailbox
|
|
351
|
-
// alone has no domain to attach it to — `oneshotagent.com` is OneShot's
|
|
352
|
-
// own domain and no agent can send from it — so that combination is a
|
|
353
|
-
// client-side validation error rather than a guaranteed domain_not_owned
|
|
354
|
-
// rejection from the server.
|
|
330
|
+
// Without a pinned sender, the quote locks an address from the agent's domain pool.
|
|
331
|
+
// A mailbox alone has no owned domain to send from.
|
|
355
332
|
if (options.from_mailbox && !options.from_domain) {
|
|
356
333
|
throw new errors_2.ValidationError('from_mailbox was pinned without from_domain. Pass from_domain (a domain you own) as well, or omit both to rotate across your warmed domains.', 'from_domain');
|
|
357
334
|
}
|
|
@@ -359,24 +336,14 @@ class OneShot {
|
|
|
359
336
|
const fromAddress = useRotation
|
|
360
337
|
? undefined
|
|
361
338
|
: `${options.from_mailbox ?? 'agent'}@${options.from_domain}`;
|
|
362
|
-
//
|
|
363
|
-
// quote_id of the `confirmation_required` result it was shown. That quote
|
|
364
|
-
// is sent as-is — no second quote — so the server charges the stored total
|
|
365
|
-
// of the quote the user saw (x402-legacy validates the id and takes the
|
|
366
|
-
// amount from the stored quote). A bare `true` is not a confirmation: it
|
|
367
|
-
// names no amount, so it falls through to the quote-only path below. An
|
|
368
|
-
// expired or already-used quote is refused by the server, and the next
|
|
369
|
-
// unconfirmed call produces a fresh quote to show again.
|
|
339
|
+
// Reuse the confirmed quote so the charge matches the amount shown; true alone confirms no amount.
|
|
370
340
|
const confirmedQuote = typeof options.confirmNewDomain === 'string' && options.confirmNewDomain.trim()
|
|
371
341
|
? options.confirmNewDomain.trim() : undefined;
|
|
372
342
|
if (confirmedQuote) {
|
|
373
343
|
this.log(`Email: sending against confirmed quote ${confirmedQuote}`);
|
|
374
344
|
return this.executeToolRequest('/v1/tools/email/send', this.emailSendPayload(options, fromAddress), confirmedQuote);
|
|
375
345
|
}
|
|
376
|
-
//
|
|
377
|
-
// provisions a mailbox on first send — a one-time fee folded into `total_cost`.
|
|
378
|
-
// `is_new` (true) means this quote would register a brand-new domain, which
|
|
379
|
-
// drives the confirm-before-charge gate below (#1070 round 2).
|
|
346
|
+
// Mailbox provisioning and domain registration are distinct fees; only a new domain needs confirmation.
|
|
380
347
|
const quote = await this.tool('email/quote', {
|
|
381
348
|
...(fromAddress ? { from_address: fromAddress } : {}),
|
|
382
349
|
...(options.to !== undefined ? { to_address: options.to } : {}),
|
|
@@ -384,23 +351,12 @@ class OneShot {
|
|
|
384
351
|
...(options.reply_to_email_id ? { reply_to_email_id: options.reply_to_email_id } : {}),
|
|
385
352
|
...(options.mailbox_mode ? { mailbox_mode: options.mailbox_mode } : {}),
|
|
386
353
|
body: options.body,
|
|
387
|
-
// Forward maxCost so the server-side X-Max-Cost-USDC header is set on
|
|
388
|
-
// the quote fetch (executeToolRequest destructures + threads it).
|
|
389
354
|
maxCost: options.maxCost,
|
|
390
355
|
});
|
|
391
356
|
this.log(`Email quote: $${quote.total_cost}`);
|
|
392
|
-
//
|
|
393
|
-
// browser/compute. If the server-side header guard fired we'd never reach
|
|
394
|
-
// here; this catches the case where the server's enforcement is bypassed
|
|
395
|
-
// (e.g. a future API revision without the header check) or skipped (cap
|
|
396
|
-
// un-set so the server returned a 200 with the quote and the caller still
|
|
397
|
-
// wants the local guard to apply).
|
|
357
|
+
// Enforce the cap locally even if the server did not check the header.
|
|
398
358
|
this.assertWithinMaxCost(quote.total_cost, options.maxCost);
|
|
399
|
-
//
|
|
400
|
-
// been shown and explicitly authorized (#1070 round 2) — this call stops
|
|
401
|
-
// here, before /email/send, and charges nothing. Pass confirmNewDomain:
|
|
402
|
-
// true (with the same from_domain) to register the domain, pay the quoted
|
|
403
|
-
// total, and send the email in one follow-up call.
|
|
359
|
+
// A new domain must be explicitly authorized against its exact quote before any charge or send.
|
|
404
360
|
if (quote.is_new) {
|
|
405
361
|
const domainName = quote.domain ?? options.from_domain ?? '';
|
|
406
362
|
const breakdown = quote.registration_fee !== undefined && quote.service_fee !== undefined
|
|
@@ -423,16 +379,11 @@ class OneShot {
|
|
|
423
379
|
/** The /email/send body for `email()`: the same whether it follows a fresh quote or a confirmed one. */
|
|
424
380
|
emailSendPayload(options, resolvedFromAddress) {
|
|
425
381
|
const payload = {
|
|
426
|
-
// Server replays the locked address from the quote when from_address
|
|
427
|
-
// is absent, but sending it anyway is harmless and forward-compatible
|
|
428
|
-
// with future SDK versions that talk directly to /send without quoting.
|
|
429
382
|
...(resolvedFromAddress ? { from_address: resolvedFromAddress } : {}),
|
|
430
383
|
...(options.to !== undefined ? { to_address: options.to } : {}),
|
|
431
384
|
...(options.subject !== undefined ? { subject: options.subject } : {}),
|
|
432
385
|
...(options.reply_to_email_id ? { reply_to_email_id: options.reply_to_email_id } : {}),
|
|
433
|
-
//
|
|
434
|
-
// mailbox even though the domain row doesn't exist yet (server also falls back
|
|
435
|
-
// to 'mailbox' when a provisioning fee was paid on the quote).
|
|
386
|
+
// Preserve mailbox provisioning when the new domain does not yet have a stored mode.
|
|
436
387
|
...(options.mailbox_mode ? { mailbox_mode: options.mailbox_mode } : {}),
|
|
437
388
|
body: options.body,
|
|
438
389
|
signal: options.signal,
|
|
@@ -446,15 +397,14 @@ class OneShot {
|
|
|
446
397
|
payload.attachments = options.attachments;
|
|
447
398
|
}
|
|
448
399
|
if (options.idempotencyKey) {
|
|
449
|
-
// Only
|
|
400
|
+
// Only sends need idempotency; quoting has no side effects.
|
|
450
401
|
payload.idempotencyKey = options.idempotencyKey;
|
|
451
402
|
}
|
|
452
403
|
if (options.approvalId) {
|
|
453
404
|
// Releases a send the action policy held (see ApprovalRequiredError).
|
|
454
405
|
payload.approvalId = options.approvalId;
|
|
455
406
|
}
|
|
456
|
-
//
|
|
457
|
-
// the send, which matters on a confirmed send that skipped the quote.
|
|
407
|
+
// Confirmed sends skip quoting, so the send must also carry the cap.
|
|
458
408
|
if (options.maxCost !== undefined)
|
|
459
409
|
payload.maxCost = options.maxCost;
|
|
460
410
|
return payload;
|
|
@@ -515,7 +465,7 @@ class OneShot {
|
|
|
515
465
|
const { includePhone, ...payload } = options;
|
|
516
466
|
return this.tool('find/person', { ...payload, include_phone: includePhone, limit: options.limit ?? 100 });
|
|
517
467
|
}
|
|
518
|
-
/** At least one identity
|
|
468
|
+
/** At least one identity: name, email, phone or a profile URL under any of its names. */
|
|
519
469
|
requirePersonIdentity(options) {
|
|
520
470
|
const hasName = options.name || options.full_name || (options.first_name && options.last_name);
|
|
521
471
|
if (!hasName && !options.email && !options.phone && !options.profile_url && !options.linkedin_url && !options.social_media_url) {
|
|
@@ -562,7 +512,7 @@ class OneShot {
|
|
|
562
512
|
return this.tool('resolve/business', { ...options });
|
|
563
513
|
}
|
|
564
514
|
/**
|
|
565
|
-
* Federal contract opportunities (SAM.gov) by NAICS code
|
|
515
|
+
* Federal contract opportunities (SAM.gov) by NAICS code - Sources Sought
|
|
566
516
|
* and Presolicitation notices with the contracting officer's published
|
|
567
517
|
* contact. Flat price per search; zero notices is a completed result.
|
|
568
518
|
* `data_as_of` on the result is the snapshot behind the rows (SAM.gov's
|
|
@@ -593,7 +543,7 @@ class OneShot {
|
|
|
593
543
|
async personDossier(options) {
|
|
594
544
|
this.requirePersonIdentity(options);
|
|
595
545
|
const { includePhone, ...payload } = options;
|
|
596
|
-
// The dossier takes 2 to 5 minutes; the generic 120 second poll gives up on a normal run
|
|
546
|
+
// The dossier takes 2 to 5 minutes; the generic 120 second poll gives up on a normal run.
|
|
597
547
|
return this.tool('lookup/person-dossier', { ...payload, include_phone: includePhone, timeout: options.timeout ?? 300 });
|
|
598
548
|
}
|
|
599
549
|
async personSocialAccounts(options) {
|
|
@@ -629,7 +579,7 @@ class OneShot {
|
|
|
629
579
|
if (!response.ok) {
|
|
630
580
|
throw new errors_2.ToolError('Failed to list inbox', response.status, await response.text(), response.headers.get('retry-after'));
|
|
631
581
|
}
|
|
632
|
-
return response.json();
|
|
582
|
+
return (0, untrusted_content_1.markUntrusted)(await response.json(), 'inbox_list');
|
|
633
583
|
}
|
|
634
584
|
async inboxGet(emailId) {
|
|
635
585
|
this.validate(emailId, 'emailId');
|
|
@@ -642,7 +592,7 @@ class OneShot {
|
|
|
642
592
|
if (!response.ok) {
|
|
643
593
|
throw new errors_2.ToolError('Failed to get email', response.status, await response.text(), response.headers.get('retry-after'));
|
|
644
594
|
}
|
|
645
|
-
return response.json();
|
|
595
|
+
return (0, untrusted_content_1.markUntrusted)(await response.json(), 'inbox_get');
|
|
646
596
|
}
|
|
647
597
|
async commerceBuy(options) {
|
|
648
598
|
this.validate(options.product_url, 'product_url');
|
|
@@ -754,9 +704,9 @@ class OneShot {
|
|
|
754
704
|
if (options.wait !== false && result.request_id) {
|
|
755
705
|
// Wait for the longest the call may run, plus two minutes to dial and settle.
|
|
756
706
|
const timeout = options.timeout ?? (options.max_duration_minutes ?? 10) * 60 + 120;
|
|
757
|
-
return this.pollJob(result.request_id, timeout, options.signal, options.onStatusUpdate);
|
|
707
|
+
return this.pollJob(result.request_id, timeout, options.signal, options.onStatusUpdate, undefined, 'voice');
|
|
758
708
|
}
|
|
759
|
-
return result;
|
|
709
|
+
return (0, untrusted_content_1.markUntrusted)(result, 'voice');
|
|
760
710
|
}
|
|
761
711
|
/**
|
|
762
712
|
* Send an SMS message
|
|
@@ -947,8 +897,6 @@ class OneShot {
|
|
|
947
897
|
payload.session_id = options.session_id;
|
|
948
898
|
if (options.profile_id)
|
|
949
899
|
payload.profile_id = options.profile_id;
|
|
950
|
-
if (options.secrets !== undefined)
|
|
951
|
-
throw new errors_2.ValidationError('secrets is unsupported; import cookies or storage_state with createBrowserProfile', 'secrets');
|
|
952
900
|
if (options.max_steps)
|
|
953
901
|
payload.max_steps = options.max_steps;
|
|
954
902
|
const { execResp } = await this.runQuoteToPay({
|
|
@@ -972,9 +920,9 @@ class OneShot {
|
|
|
972
920
|
const result = await execResp.json();
|
|
973
921
|
this.log(`Browser task initiated: ${result.request_id}`);
|
|
974
922
|
if (options.wait !== false && result.request_id) {
|
|
975
|
-
return this.pollJob(result.request_id, options.timeout ?? 300, options.signal, options.onStatusUpdate);
|
|
923
|
+
return this.pollJob(result.request_id, options.timeout ?? 300, options.signal, options.onStatusUpdate, undefined, 'browser');
|
|
976
924
|
}
|
|
977
|
-
return result;
|
|
925
|
+
return (0, untrusted_content_1.markUntrusted)(result, 'browser');
|
|
978
926
|
}
|
|
979
927
|
/**
|
|
980
928
|
* Create a persistent browser profile for reusing cookies/localStorage across sessions
|
|
@@ -1094,7 +1042,7 @@ class OneShot {
|
|
|
1094
1042
|
throw new errors_1.LinkedInConnectRequiredError(body);
|
|
1095
1043
|
throw new errors_2.ToolError(`LinkedIn ${opts.what} failed`, response.status, body, response.headers.get('retry-after'));
|
|
1096
1044
|
}
|
|
1097
|
-
return response.json();
|
|
1045
|
+
return (0, untrusted_content_1.markUntrusted)(await response.json(), `/v1/tools/linkedin${path}`, method);
|
|
1098
1046
|
}
|
|
1099
1047
|
/**
|
|
1100
1048
|
* Create a hosted LinkedIn login link. Show `url` to the human once; it
|
|
@@ -1222,7 +1170,6 @@ class OneShot {
|
|
|
1222
1170
|
this.validate(options.identifier, 'identifier');
|
|
1223
1171
|
const { accountId, identifier, notify, sections, ...rest } = options;
|
|
1224
1172
|
const body = { ...rest, account_id: accountId, identifier, notify: !!notify, timeout: options.timeout ?? 180 };
|
|
1225
|
-
// Left off the wire when empty, so a view without sections is the request it always was.
|
|
1226
1173
|
if (sections?.length)
|
|
1227
1174
|
body.sections = sections;
|
|
1228
1175
|
const result = await this.tool('linkedin/profile-view', body);
|
|
@@ -1285,7 +1232,7 @@ class OneShot {
|
|
|
1285
1232
|
if (!response.ok) {
|
|
1286
1233
|
throw new errors_2.ToolError('Failed to list SMS inbox', response.status, await response.text(), response.headers.get('retry-after'));
|
|
1287
1234
|
}
|
|
1288
|
-
return response.json();
|
|
1235
|
+
return (0, untrusted_content_1.markUntrusted)(await response.json(), 'sms_inbox_list');
|
|
1289
1236
|
}
|
|
1290
1237
|
/**
|
|
1291
1238
|
* Get this agent a voice number without placing a call. Returns the existing
|
|
@@ -1368,7 +1315,7 @@ class OneShot {
|
|
|
1368
1315
|
return this.signedJson('GET', `/v1/tools/voice/inbound/${encodeURIComponent(callId)}`, undefined, 'read', 'Failed to get inbound call');
|
|
1369
1316
|
}
|
|
1370
1317
|
/**
|
|
1371
|
-
* Call a declared route of the API by method and path
|
|
1318
|
+
* Call a declared route of the API by method and path, the one generic call for a route that has no
|
|
1372
1319
|
* typed method. Follows the Python client's `call_free_get/post/patch/put/delete`:
|
|
1373
1320
|
*
|
|
1374
1321
|
* - `path` is the route, with `{name}` (or `:name`) for a path parameter, filled from `params` and URL-encoded
|
|
@@ -1448,7 +1395,7 @@ class OneShot {
|
|
|
1448
1395
|
if (response.status === 204 || !text)
|
|
1449
1396
|
return { success: true };
|
|
1450
1397
|
const json = JSON.parse(text);
|
|
1451
|
-
return (json && typeof json === 'object' && 'data' in json ? json.data : json);
|
|
1398
|
+
return (0, untrusted_content_1.markUntrusted)(json && typeof json === 'object' && 'data' in json ? json.data : json, target, method);
|
|
1452
1399
|
})();
|
|
1453
1400
|
return await (0, deadline_1.abortable)(request, scope.signal);
|
|
1454
1401
|
}
|
|
@@ -1470,7 +1417,7 @@ class OneShot {
|
|
|
1470
1417
|
throw new errors_2.ToolError(failure, response.status, await response.text(), response.headers.get('retry-after'));
|
|
1471
1418
|
}
|
|
1472
1419
|
const json = await response.json();
|
|
1473
|
-
return (json.data ?? json);
|
|
1420
|
+
return (0, untrusted_content_1.markUntrusted)(json.data ?? json, path, method);
|
|
1474
1421
|
}
|
|
1475
1422
|
/**
|
|
1476
1423
|
* Get a specific inbound SMS message
|
|
@@ -1492,7 +1439,7 @@ class OneShot {
|
|
|
1492
1439
|
if (!response.ok) {
|
|
1493
1440
|
throw new errors_2.ToolError('Failed to get SMS message', response.status, await response.text(), response.headers.get('retry-after'));
|
|
1494
1441
|
}
|
|
1495
|
-
return response.json();
|
|
1442
|
+
return (0, untrusted_content_1.markUntrusted)(await response.json(), 'sms_inbox_get');
|
|
1496
1443
|
}
|
|
1497
1444
|
/**
|
|
1498
1445
|
* List notifications for the agent
|
|
@@ -1541,8 +1488,8 @@ class OneShot {
|
|
|
1541
1488
|
}
|
|
1542
1489
|
}
|
|
1543
1490
|
/**
|
|
1544
|
-
* The agent's connections
|
|
1545
|
-
* `status`. Free. A connected-system read (ledger, CRM, shipping, tax, procurement) takes the `id` of an active
|
|
1491
|
+
* The agent's connections: GitHub and business systems, each with `id`, `system`, `account_label` and
|
|
1492
|
+
* `status`. Free. A connected-system read (ledger, CRM, shipping, tax, procurement, meetings) takes the `id` of an active
|
|
1546
1493
|
* one as `connection_id`; pick it by `system` and `account_label`, then call the read with the generic
|
|
1547
1494
|
* {@link OneShot.route}.
|
|
1548
1495
|
*
|
|
@@ -1570,12 +1517,10 @@ class OneShot {
|
|
|
1570
1517
|
return response.json();
|
|
1571
1518
|
}
|
|
1572
1519
|
async getBalance(tokenAddress) {
|
|
1573
|
-
// Use unified API endpoint for USDC (default) balance
|
|
1574
1520
|
if (!tokenAddress || tokenAddress === this.usdcAddress) {
|
|
1575
1521
|
const unified = await this.getUnifiedBalance();
|
|
1576
1522
|
return unified.on_chain_balance;
|
|
1577
1523
|
}
|
|
1578
|
-
// Custom token: keep existing RPC logic
|
|
1579
1524
|
const contract = new ethers_1.ethers.Contract(tokenAddress, ['function balanceOf(address) view returns (uint256)', 'function decimals() view returns (uint8)'], this.rpcProvider);
|
|
1580
1525
|
const [balance, decimals] = await Promise.all([
|
|
1581
1526
|
contract.balanceOf(this.provider.address),
|
|
@@ -1584,11 +1529,16 @@ class OneShot {
|
|
|
1584
1529
|
return ethers_1.ethers.formatUnits(balance, decimals);
|
|
1585
1530
|
}
|
|
1586
1531
|
/**
|
|
1587
|
-
* Create a compute goal
|
|
1532
|
+
* Create a compute goal - the orchestrator will plan, execute, and iterate autonomously.
|
|
1588
1533
|
*
|
|
1589
1534
|
* Uses the quote-then-pay flow: first call gets a 402 with a budget estimate,
|
|
1590
1535
|
* second call (with payment) creates the goal.
|
|
1591
1536
|
*
|
|
1537
|
+
* Name the unit of work, how work starts and what each unit needs in the objective, or pass
|
|
1538
|
+
* `scope`. The quote carries `scope`, `price` (per unit expected and ceiling, overhead, total or
|
|
1539
|
+
* units covered, unpriced, unmeasured) and the `done_check` the goal is held to; `onQuote` reads
|
|
1540
|
+
* them before paying and may throw to stop. A `scope` you pass is priced again in code.
|
|
1541
|
+
*
|
|
1592
1542
|
* @example
|
|
1593
1543
|
* ```typescript
|
|
1594
1544
|
* const goal = await agent.compute({
|
|
@@ -1623,7 +1573,11 @@ class OneShot {
|
|
|
1623
1573
|
payload.approvals = options.approvals;
|
|
1624
1574
|
if (options.done_check)
|
|
1625
1575
|
payload.done_check = options.done_check;
|
|
1626
|
-
|
|
1576
|
+
if (options.scope)
|
|
1577
|
+
payload.scope = options.scope;
|
|
1578
|
+
if (options.egress_allowlist)
|
|
1579
|
+
payload.egress_allowlist = options.egress_allowlist;
|
|
1580
|
+
// Compute returns the goal directly on 202 - no job polling, unlike the
|
|
1627
1581
|
// other paid tools.
|
|
1628
1582
|
const { execResp: createResp } = await this.runQuoteToPay({
|
|
1629
1583
|
endpoint: '/v1/compute',
|
|
@@ -1632,7 +1586,10 @@ class OneShot {
|
|
|
1632
1586
|
maxCost: options.maxCost,
|
|
1633
1587
|
expectMsg: 'Expected 402 for compute quote',
|
|
1634
1588
|
totalOf: (ctx) => ctx.total_budget,
|
|
1635
|
-
onQuote: (ctx) =>
|
|
1589
|
+
onQuote: (ctx) => {
|
|
1590
|
+
this.log(`Compute quote: $${ctx.total_budget} — ${ctx.objective_summary}`);
|
|
1591
|
+
options.onQuote?.(ctx);
|
|
1592
|
+
},
|
|
1636
1593
|
on400: async (resp) => {
|
|
1637
1594
|
const errorData = await resp.json();
|
|
1638
1595
|
if (errorData.error === 'content_blocked') {
|
|
@@ -1679,9 +1636,9 @@ class OneShot {
|
|
|
1679
1636
|
* Poll a compute goal using its goal_id, not its creation request_id.
|
|
1680
1637
|
* Returns the complete goal snapshot on completed; failed/cancelled reject
|
|
1681
1638
|
* with JobError (goal ID, end_reason code, snapshot in partialResult).
|
|
1682
|
-
* Budget exhaustion
|
|
1639
|
+
* Budget exhaustion waits in awaiting_budget for funding; after seven days it ends. After fundComputeGoal revives an ended goal,
|
|
1683
1640
|
* call waitForComputeGoal again with the same ID to observe the new run.
|
|
1684
|
-
* Paused/awaiting_confirmation and recurring goals keep waiting until the
|
|
1641
|
+
* Paused/awaiting_confirmation/awaiting_connection/awaiting_budget/awaiting_tool and recurring goals keep waiting until the
|
|
1685
1642
|
* goal ends or the timeout (seconds, default 120) expires with JobTimeoutError.
|
|
1686
1643
|
* Infinity disables the local deadline.
|
|
1687
1644
|
* onStatusUpdate receives each distinct observed status before settlement.
|
|
@@ -1907,6 +1864,54 @@ class OneShot {
|
|
|
1907
1864
|
const json = await response.json();
|
|
1908
1865
|
return json.data;
|
|
1909
1866
|
}
|
|
1867
|
+
/**
|
|
1868
|
+
* Dispute a goal that ended `impossible_unverified`: the loop claimed it
|
|
1869
|
+
* cannot be done with no proof code could check. The dispute is recorded
|
|
1870
|
+
* on the goal and its receipt; nothing is refunded.
|
|
1871
|
+
*
|
|
1872
|
+
* @example
|
|
1873
|
+
* ```typescript
|
|
1874
|
+
* await agent.disputeComputeGoal(goalId, { reason: 'The data is public.' });
|
|
1875
|
+
* ```
|
|
1876
|
+
*/
|
|
1877
|
+
async disputeComputeGoal(goalId, input = {}) {
|
|
1878
|
+
this.validate(goalId, 'goalId');
|
|
1879
|
+
const response = await this.fetchApi(`${this.baseUrl}/v1/compute/${goalId}/respond`, {
|
|
1880
|
+
method: 'POST',
|
|
1881
|
+
headers: { 'Content-Type': 'application/json', ...(await this.signedReadHeaders('write')) },
|
|
1882
|
+
body: JSON.stringify({ dispute: true, ...(input.reason ? { response: input.reason } : {}) }),
|
|
1883
|
+
});
|
|
1884
|
+
if (response.status === 404)
|
|
1885
|
+
throw new errors_2.ToolError('Goal not found', 404, await response.text(), response.headers.get('retry-after'));
|
|
1886
|
+
if (!response.ok)
|
|
1887
|
+
throw new errors_2.ToolError('Failed to dispute compute goal', response.status, await response.text(), response.headers.get('retry-after'));
|
|
1888
|
+
const json = await response.json();
|
|
1889
|
+
return json.data;
|
|
1890
|
+
}
|
|
1891
|
+
/**
|
|
1892
|
+
* Continue a goal held in `awaiting_tool` without the tool it requested
|
|
1893
|
+
* (#3173). The goal returns to active and resumes with `message` as what
|
|
1894
|
+
* its owner said, or a default note that it continues without the tool.
|
|
1895
|
+
*
|
|
1896
|
+
* @example
|
|
1897
|
+
* ```typescript
|
|
1898
|
+
* await agent.continueComputeGoalWithoutTool(goalId, { message: 'Use the public filings instead.' });
|
|
1899
|
+
* ```
|
|
1900
|
+
*/
|
|
1901
|
+
async continueComputeGoalWithoutTool(goalId, input = {}) {
|
|
1902
|
+
this.validate(goalId, 'goalId');
|
|
1903
|
+
const response = await this.fetchApi(`${this.baseUrl}/v1/compute/${goalId}/respond`, {
|
|
1904
|
+
method: 'POST',
|
|
1905
|
+
headers: { 'Content-Type': 'application/json', ...(await this.signedReadHeaders('write')) },
|
|
1906
|
+
body: JSON.stringify({ continue_without_tool: true, ...(input.message ? { response: input.message } : {}) }),
|
|
1907
|
+
});
|
|
1908
|
+
if (response.status === 404)
|
|
1909
|
+
throw new errors_2.ToolError('Goal not found', 404, await response.text(), response.headers.get('retry-after'));
|
|
1910
|
+
if (!response.ok)
|
|
1911
|
+
throw new errors_2.ToolError('Failed to continue compute goal without the tool', response.status, await response.text(), response.headers.get('retry-after'));
|
|
1912
|
+
const json = await response.json();
|
|
1913
|
+
return json.data;
|
|
1914
|
+
}
|
|
1910
1915
|
/**
|
|
1911
1916
|
* Pause a recurring compute goal
|
|
1912
1917
|
*
|
|
@@ -1952,7 +1957,7 @@ class OneShot {
|
|
|
1952
1957
|
}
|
|
1953
1958
|
/**
|
|
1954
1959
|
* Top up budget for a recurring compute goal, or for a goal (one-off or recurring) that
|
|
1955
|
-
* ended because it ran out of budget (`end_reason: 'budget_exhausted'`).
|
|
1960
|
+
* is awaiting budget or ended because it ran out of budget (`end_reason: 'budget_exhausted'`).
|
|
1956
1961
|
*
|
|
1957
1962
|
* Uses the quote-then-pay flow: the first call gets a 402 with the top-up price,
|
|
1958
1963
|
* the second call (with payment) credits the budget. The amount IS the price.
|
|
@@ -2106,7 +2111,7 @@ class OneShot {
|
|
|
2106
2111
|
* List receipts with optional filtering
|
|
2107
2112
|
*
|
|
2108
2113
|
* Paginate with `limit`/`offset`, or page through an explicit time window with
|
|
2109
|
-
* `since`/`until` (ISO string or Date)
|
|
2114
|
+
* `since`/`until` (ISO string or Date) - useful for reaching receipts older than
|
|
2110
2115
|
* the server's 100-row cap when tagging value weeks after the originating call.
|
|
2111
2116
|
*
|
|
2112
2117
|
* @example
|
|
@@ -2144,8 +2149,8 @@ class OneShot {
|
|
|
2144
2149
|
* Three ways to address the value:
|
|
2145
2150
|
* - `rcpt_…` receipt id (from `result.receipt_id` or `receiptsList`), or
|
|
2146
2151
|
* - the `request_id` returned by the originating tool call (`result.request_id`,
|
|
2147
|
-
* resolved server-side via `Receipt.job_id`)
|
|
2148
|
-
* - a `goalId` correlation key
|
|
2152
|
+
* resolved server-side via `Receipt.job_id`) - annotate without a list lookup, or
|
|
2153
|
+
* - a `goalId` correlation key - attributes a whole *cadence* outcome (the email,
|
|
2149
2154
|
* the follow-ups, the find/verify/enrich calls that share `decisionContext.goalId`)
|
|
2150
2155
|
* in one call. The value is recorded once in the outcome ledger, so it can't
|
|
2151
2156
|
* double-count across the cadence's receipts. Read it back with `rocsByGoal()`.
|
|
@@ -2159,13 +2164,12 @@ class OneShot {
|
|
|
2159
2164
|
* const r = await agent.personEmailStatus({ email: 'x@y.com' });
|
|
2160
2165
|
* await agent.tagReceiptValue({ requestId: r.request_id }, { type: 'lead', amount: 1 });
|
|
2161
2166
|
*
|
|
2162
|
-
* // By cadence correlation key
|
|
2167
|
+
* // By cadence correlation key - one call attributes a closed deal to the whole sequence:
|
|
2163
2168
|
* await agent.tagReceiptValue({ goalId: 'goal_q2_acme' }, { type: 'revenue', amount: 5000, label: 'Closed deal' });
|
|
2164
2169
|
* ```
|
|
2165
2170
|
*/
|
|
2166
2171
|
async tagReceiptValue(ref, valueTag) {
|
|
2167
2172
|
this.validate(valueTag.type, 'valueTag.type');
|
|
2168
|
-
// Cadence-level: route to the outcome ledger by correlation key.
|
|
2169
2173
|
if (typeof ref === 'object' && ref.goalId && !ref.receiptId && !ref.requestId) {
|
|
2170
2174
|
const response = await this.fetchApi(`${this.baseUrl}/v1/analytics/outcomes`, {
|
|
2171
2175
|
method: 'POST',
|
|
@@ -2192,7 +2196,7 @@ class OneShot {
|
|
|
2192
2196
|
}
|
|
2193
2197
|
}
|
|
2194
2198
|
/**
|
|
2195
|
-
* Per-cadence RoCS rollup
|
|
2199
|
+
* Per-cadence RoCS rollup - spend (from receipts) vs. value (from outcomes)
|
|
2196
2200
|
* grouped by `decisionContext.goalId`. Answers "what did this cadence cost vs.
|
|
2197
2201
|
* earn" in one query. `value` counts judge-confirmed outcomes; `pending_value`
|
|
2198
2202
|
* surfaces self-reported outcomes not yet confirmed.
|
|
@@ -2273,15 +2277,8 @@ class OneShot {
|
|
|
2273
2277
|
jsonHeaders() {
|
|
2274
2278
|
return { 'Content-Type': 'application/json', ...this.headers() };
|
|
2275
2279
|
}
|
|
2276
|
-
/**
|
|
2277
|
-
*
|
|
2278
|
-
* routes (inbox, sms inbox, notifications, balance). These identify the caller
|
|
2279
|
-
* by wallet, and wallet addresses are public, so without a proof anyone could
|
|
2280
|
-
* read another agent's data by supplying its address. The proof binds this
|
|
2281
|
-
* request to the wallet the SDK controls; the server verifies the signature
|
|
2282
|
-
* locally. A fresh nonce per call prevents replay. Signing failure falls back
|
|
2283
|
-
* to plain headers, with a one-time warning.
|
|
2284
|
-
*/
|
|
2280
|
+
/** Signed, single-use proofs bind requests to the wallet; an address alone cannot authenticate reads.
|
|
2281
|
+
* Signing failure falls back to plain headers with a one-time warning. */
|
|
2285
2282
|
async signedReadHeaders(scope = 'read', domainName = 'OneShot Agent Auth') {
|
|
2286
2283
|
const base = this.headers();
|
|
2287
2284
|
// The bearer token already binds the request to the agent (and is
|
|
@@ -2293,12 +2290,7 @@ class OneShot {
|
|
|
2293
2290
|
const issuedAt = Math.floor(Date.now() / 1000) + (domainName === 'OneShot Agent Auth' ? this.proofClockOffsetSec : 0);
|
|
2294
2291
|
const nonce = ethers_1.ethers.hexlify(ethers_1.ethers.randomBytes(32));
|
|
2295
2292
|
const signature = await this.provider.signTypedData(
|
|
2296
|
-
//
|
|
2297
|
-
// its own nonce store — see hunts.ts) use a DIFFERENT EIP-712 domain
|
|
2298
|
-
// than api-service's. EIP-712 mixes the domain into the signed hash,
|
|
2299
|
-
// so a proof is only valid for the one origin it was signed for and
|
|
2300
|
-
// can't be replayed across service boundaries (security finding
|
|
2301
|
-
// F-t_d4805d6f-3, round 2).
|
|
2293
|
+
// Separate EIP-712 domains prevent proof replay across services with independent nonce stores.
|
|
2302
2294
|
{ name: domainName, version: '1' }, {
|
|
2303
2295
|
AgentReadAuth: [
|
|
2304
2296
|
{ name: 'agent', type: 'address' },
|
|
@@ -2312,9 +2304,7 @@ class OneShot {
|
|
|
2312
2304
|
return { ...base, 'x-agent-proof': proof };
|
|
2313
2305
|
}
|
|
2314
2306
|
catch (err) {
|
|
2315
|
-
//
|
|
2316
|
-
// these requests will be rejected, and a silent fallback would leave
|
|
2317
|
-
// the caller with a 401 and no idea why.
|
|
2307
|
+
// Warn once even without debug so a failed proof explains the ensuing authentication failure.
|
|
2318
2308
|
if (!this.readProofWarned) {
|
|
2319
2309
|
this.readProofWarned = true;
|
|
2320
2310
|
this.logger(`[OneShot] Warning: could not sign the x-agent-proof read proof, so requests go without it and will be rejected where the API enforces proofs. Pass a signer that supports signTypedData, or use an access token. Cause: ${err}`);
|
|
@@ -2325,34 +2315,13 @@ class OneShot {
|
|
|
2325
2315
|
return base;
|
|
2326
2316
|
}
|
|
2327
2317
|
}
|
|
2328
|
-
/** Retry a
|
|
2329
|
-
*
|
|
2330
|
-
*
|
|
2331
|
-
* already spent: the SDK signs every send once and, where the runtime lets it, sends it on a
|
|
2332
|
-
* connection of its own (below), so a spent nonce means something between this process and the API
|
|
2333
|
-
* delivered the request twice (a retrying proxy, a wrapper re-sending a captured header, or a browser's
|
|
2334
|
-
* own retry on a pooled connection); the first copy was answered to nobody.
|
|
2335
|
-
* Only a GET or HEAD is sent again for that reason: a read has no side effects, where a write the
|
|
2336
|
-
* API already ran could run twice (#2517).
|
|
2337
|
-
* Callers supply replayable JSON bodies and retain their original abort/deadline signal.
|
|
2338
|
-
*/
|
|
2318
|
+
/** Retry a clock-window refusal once using the API's server_time, or a spent nonce on GET/HEAD.
|
|
2319
|
+
* Never retry a spent nonce on a write: the first delivery may already have executed it.
|
|
2320
|
+
* Callers supply replayable JSON bodies and retain their original abort/deadline signal. */
|
|
2339
2321
|
async fetchApi(url, init) {
|
|
2340
2322
|
const proof = new Headers(init.headers).get('x-agent-proof');
|
|
2341
|
-
//
|
|
2342
|
-
//
|
|
2343
|
-
// socket it chose closes before the response (Bun 1.3.13; reproduction in docs/status/2517.md):
|
|
2344
|
-
// the first copy spends the nonce, the second is rejected as a replay. Each signed send
|
|
2345
|
-
// therefore asks for a connection of its own that no later send reuses: `Connection: close` for
|
|
2346
|
-
// the server and for a client that honours the header, `keepalive: false` for Bun's pool. In Bun
|
|
2347
|
-
// (measured, tests/unit/sdk-proof-single-delivery.test.ts) a socket that dies after the API read
|
|
2348
|
-
// the request is then an error to the caller, not a silent duplicate (#2517). This is a request,
|
|
2349
|
-
// not a guarantee: a browser's fetch drops `Connection` as a forbidden request header and reads
|
|
2350
|
-
// `keepalive` as "may outlive the page", so there the browser's HTTP stack keeps its own pooling and
|
|
2351
|
-
// retry rules, and the replayed-read recovery above is the only defence the SDK has; a browser's
|
|
2352
|
-
// duplicated write is reported by the API as a replay and is not repeated by the SDK.
|
|
2353
|
-
// Both sends below (the first and the one-time refresh) go through `deliver`, so the refresh
|
|
2354
|
-
// carries the same boundary and a caller-supplied `Connection` never reaches the wire; the
|
|
2355
|
-
// caller's `init` and headers object are left as they were.
|
|
2323
|
+
// Disable connection reuse where supported to prevent transport retries from replaying a single-use proof.
|
|
2324
|
+
// Browsers control their own pooling; only replayed reads can be safely retried there.
|
|
2356
2325
|
const deliver = (headers) => fetch(url, proof ? { ...init, headers: OneShot.singleDelivery(headers), keepalive: false } : init);
|
|
2357
2326
|
const response = await deliver(init.headers);
|
|
2358
2327
|
if (response.status !== 401 || !proof || this._accessToken)
|
|
@@ -2393,7 +2362,7 @@ class OneShot {
|
|
|
2393
2362
|
init.signal?.throwIfAborted();
|
|
2394
2363
|
if (!refreshed['x-agent-proof'])
|
|
2395
2364
|
return response;
|
|
2396
|
-
const retryHeaders = new Headers(init.headers); //
|
|
2365
|
+
const retryHeaders = new Headers(init.headers); // Keep the caller's headers intact.
|
|
2397
2366
|
retryHeaders.set('x-agent-proof', refreshed['x-agent-proof']);
|
|
2398
2367
|
return deliver(retryHeaders);
|
|
2399
2368
|
}
|
|
@@ -2407,8 +2376,7 @@ class OneShot {
|
|
|
2407
2376
|
}
|
|
2408
2377
|
return { ...Object.fromEntries(Object.entries(headers ?? {}).filter(([name]) => name.toLowerCase() !== 'connection')), Connection: 'close' };
|
|
2409
2378
|
}
|
|
2410
|
-
/**
|
|
2411
|
-
* values (0, '') as undefined at the call site to match prior `if (x)` guards. */
|
|
2379
|
+
/** Skip null/undefined query values; callers must explicitly omit other unwanted falsy values. */
|
|
2412
2380
|
buildQuery(params) {
|
|
2413
2381
|
const qs = new URLSearchParams();
|
|
2414
2382
|
for (const [k, v] of Object.entries(params)) {
|
|
@@ -2417,20 +2385,8 @@ class OneShot {
|
|
|
2417
2385
|
}
|
|
2418
2386
|
return qs.toString();
|
|
2419
2387
|
}
|
|
2420
|
-
/**
|
|
2421
|
-
*
|
|
2422
|
-
*
|
|
2423
|
-
* Lazy rather than in the constructor because the PUT is signed (async) and
|
|
2424
|
-
* the constructor is sync. Idempotent per instance via the cached promise,
|
|
2425
|
-
* which is only kept once the sync SUCCEEDS.
|
|
2426
|
-
*
|
|
2427
|
-
* FAILS CLOSED. If the server can't confirm the budget — network error,
|
|
2428
|
-
* 5xx, 429, or a rejected config — this throws BudgetSyncError and the paid
|
|
2429
|
-
* call is not made. Proceeding would silently drop the guardrail the
|
|
2430
|
-
* developer configured, which is exactly the "empty wallet at 3am" the
|
|
2431
|
-
* budget exists to prevent. The marker is cleared so the next paid call
|
|
2432
|
-
* retries.
|
|
2433
|
-
*/
|
|
2388
|
+
/** Sync budgets lazily because signing is async; concurrent calls share the cached promise.
|
|
2389
|
+
* Fail closed until the server confirms the budget, and clear failed syncs so the next call retries. */
|
|
2434
2390
|
async ensureBudgetsSynced() {
|
|
2435
2391
|
if (!this._budgets && !this._alertEmail)
|
|
2436
2392
|
return;
|
|
@@ -2470,19 +2426,12 @@ class OneShot {
|
|
|
2470
2426
|
await attempt;
|
|
2471
2427
|
}
|
|
2472
2428
|
catch (err) {
|
|
2473
|
-
// Not synced: clear the marker so the next paid call retries, then
|
|
2474
|
-
// refuse this one rather than run it unguarded.
|
|
2475
2429
|
this._budgetSync = undefined;
|
|
2476
2430
|
this.log(`${err.message} — paid call refused until the budget is confirmed`);
|
|
2477
2431
|
throw err;
|
|
2478
2432
|
}
|
|
2479
2433
|
}
|
|
2480
|
-
/**
|
|
2481
|
-
* Local fast-fail on the per-transaction cap, so an oversized call fails
|
|
2482
|
-
* without a network round-trip. The server enforces the same cap (and the
|
|
2483
|
-
* daily one, which needs the ledger) regardless of which client calls —
|
|
2484
|
-
* this is the same SDK-checks/server-enforces split as maxCost.
|
|
2485
|
-
*/
|
|
2434
|
+
/** Fail oversized calls locally before a round-trip; the server also enforces shared daily spend. */
|
|
2486
2435
|
assertWithinBudget(total) {
|
|
2487
2436
|
const cap = this._budgets?.perTransaction;
|
|
2488
2437
|
if (!cap || cap <= 0)
|
|
@@ -2552,7 +2501,7 @@ class OneShot {
|
|
|
2552
2501
|
const body = await response.json();
|
|
2553
2502
|
return body.data;
|
|
2554
2503
|
}
|
|
2555
|
-
/** List this agent's access tokens (prefix, label, last use, spend today
|
|
2504
|
+
/** List this agent's access tokens (prefix, label, last use, spend today - never the token). */
|
|
2556
2505
|
async listAccessTokens(opts = {}) {
|
|
2557
2506
|
this.assertWalletSession('list access tokens');
|
|
2558
2507
|
const response = await this.fetchApi(`${this.baseUrl}/v1/agents/me/access-tokens${opts.activeOnly ? '?active=true' : ''}`, {
|
|
@@ -2582,41 +2531,10 @@ class OneShot {
|
|
|
2582
2531
|
throw new errors_2.OneShotError(`Quote $${total} exceeds maxCost $${maxCost}`);
|
|
2583
2532
|
}
|
|
2584
2533
|
}
|
|
2585
|
-
/**
|
|
2586
|
-
|
|
2587
|
-
*
|
|
2588
|
-
|
|
2589
|
-
* a server-side enforcement layer so non-SDK callers (MCP server, custom
|
|
2590
|
-
* integrations) can't ignore the cap.
|
|
2591
|
-
*/
|
|
2592
|
-
/**
|
|
2593
|
-
* The amount to sign, in decimal USDC.
|
|
2594
|
-
*
|
|
2595
|
-
* A 402 advertises its price twice: the x402 v2 `PAYMENT-REQUIRED` header
|
|
2596
|
-
* (`accepts[0].amount`, atomic units) and the legacy JSON body
|
|
2597
|
-
* (`payment_request.amount`, decimal). The header is authoritative — it is
|
|
2598
|
-
* what the server rebuilds its requirement from and what
|
|
2599
|
-
* `findMatchingRequirements` compares the signature against.
|
|
2600
|
-
*
|
|
2601
|
-
* Preferring the body is how quote-based routes (email/send, sms, voice,
|
|
2602
|
-
* build, commerce/buy, compute) silently failed: the body carried a
|
|
2603
|
-
* hardcoded "0.00", so the SDK signed a zero-cost authorization against a
|
|
2604
|
-
* real charge and the server answered with a bodiless 402. It stayed hidden
|
|
2605
|
-
* for as long as credits covered those calls, since the credit path returns
|
|
2606
|
-
* 202 without any payment handshake.
|
|
2607
|
-
*
|
|
2608
|
-
* Falls back to the body when the header is missing or unparseable.
|
|
2609
|
-
*/
|
|
2610
|
-
/**
|
|
2611
|
-
* Throw the most specific error a failed response supports.
|
|
2612
|
-
*
|
|
2613
|
-
* A 402 arriving on the PAID retry means the facilitator refused the
|
|
2614
|
-
* signature — not the ordinary "here is your quote" 402. The API names the
|
|
2615
|
-
* cause in that body (`payment_verification_failed` + reason + expected vs
|
|
2616
|
-
* received amount); surface it as a `PaymentError` so callers can branch on
|
|
2617
|
-
* `err.reason` instead of string-matching. Anything else keeps the existing
|
|
2618
|
-
* `ToolError` shape.
|
|
2619
|
-
*/
|
|
2534
|
+
/** Ask the API to enforce the caller's cap before payment, in addition to the local quote check. */
|
|
2535
|
+
/** Resolve decimal USDC from the authoritative PAYMENT-REQUIRED header (atomic units);
|
|
2536
|
+
* fall back to the legacy body only when the header is missing or unparseable. */
|
|
2537
|
+
/** A 402 on the paid retry is a payment rejection, not a quote; expose its cause as PaymentError. */
|
|
2620
2538
|
async failFromResponse(message, response) {
|
|
2621
2539
|
const text = await response.text();
|
|
2622
2540
|
const credits = response.status === 402 ? this.parseInsufficientCredits(text) : undefined;
|
|
@@ -2650,14 +2568,7 @@ class OneShot {
|
|
|
2650
2568
|
const num = (v) => (typeof v === 'string' || typeof v === 'number') && Number.isFinite(parseFloat(String(v))) ? parseFloat(String(v)) : undefined;
|
|
2651
2569
|
return new errors_1.InsufficientCreditsError(body.message ?? 'Credit balance does not cover this call', num(body.required_usdc), num(body.credits_balance), num(body.shortfall_usdc));
|
|
2652
2570
|
}
|
|
2653
|
-
/**
|
|
2654
|
-
* Access-token sessions never sign. When a paid call answers 402 the SDK
|
|
2655
|
-
* either got the server's `insufficient_credits` verdict (throw it) or a
|
|
2656
|
-
* quote (re-POST with `x-quote-id` and no payment: the server executes
|
|
2657
|
-
* when the quote's credits cover it, otherwise answers `insufficient_credits`
|
|
2658
|
-
* with the balance and shortfall, which failFromResponse turns into the
|
|
2659
|
-
* typed error). Server-decided on purpose — the SDK holds no ledger.
|
|
2660
|
-
*/
|
|
2571
|
+
/** Token sessions never sign: retry the quote against credits and let the server ledger decide sufficiency. */
|
|
2661
2572
|
async retryAsCreditsSession(body, endpoint, payload, quoteId, signal, timeoutMs, extraHeaders) {
|
|
2662
2573
|
const credits = body ? this.parseInsufficientCredits(body) : undefined;
|
|
2663
2574
|
if (credits)
|
|
@@ -2766,10 +2677,7 @@ class OneShot {
|
|
|
2766
2677
|
return bodyAmount ?? '0';
|
|
2767
2678
|
}
|
|
2768
2679
|
const fromHeader = ethers_1.ethers.formatUnits(accepted.amount, 6);
|
|
2769
|
-
//
|
|
2770
|
-
// verbatim — downstream consumers such as the ETH auto-swap pass this
|
|
2771
|
-
// value along, and there is no reason to reformat it. Only a genuine
|
|
2772
|
-
// disagreement flips to the header.
|
|
2680
|
+
// Preserve the body's amount formatting when it agrees with the authoritative header.
|
|
2773
2681
|
if (bodyAmount != null && parseFloat(bodyAmount) === parseFloat(fromHeader)) {
|
|
2774
2682
|
return bodyAmount;
|
|
2775
2683
|
}
|
|
@@ -2780,11 +2688,7 @@ class OneShot {
|
|
|
2780
2688
|
return undefined;
|
|
2781
2689
|
return { 'X-Max-Cost-USDC': maxCost.toString() };
|
|
2782
2690
|
}
|
|
2783
|
-
/**
|
|
2784
|
-
* Idempotency-Key header, sent on both legs (pre-402 and paid retry) so
|
|
2785
|
-
* the server can replay a cached result instead of double-charging and
|
|
2786
|
-
* double-executing on a client retry.
|
|
2787
|
-
*/
|
|
2691
|
+
/** Send the idempotency key on both legs so retries cannot double-charge or repeat the action. */
|
|
2788
2692
|
idempotencyHeader(idempotencyKey) {
|
|
2789
2693
|
if (!idempotencyKey)
|
|
2790
2694
|
return undefined;
|
|
@@ -2822,7 +2726,7 @@ class OneShot {
|
|
|
2822
2726
|
}
|
|
2823
2727
|
async executeToolRequest(endpoint, options, quoteId) {
|
|
2824
2728
|
// LinkedIn writes act through a human's account and cannot be recalled: always keyed.
|
|
2825
|
-
const reliable = /(?:^|\/)(lookup\/person-(profile|email|email-status)|lookup\/place-permits|solve|predict|linkedin\/(reply|message|profile-view|react|invite(?:\/withdraw)?))$/.test(endpoint);
|
|
2729
|
+
const reliable = /(?:^|\/)(lookup\/person-(profile|email|email-status)|lookup\/place-permits|lookup\/company-private-raises|solve|predict|linkedin\/(reply|message|profile-view|react|invite(?:\/withdraw)?))$/.test(endpoint);
|
|
2826
2730
|
const key = options.idempotencyKey ?? (reliable ? ethers_1.ethers.hexlify(ethers_1.ethers.randomBytes(16)) : undefined);
|
|
2827
2731
|
if (options.totalTimeoutMs !== undefined && (!Number.isFinite(options.totalTimeoutMs) || options.totalTimeoutMs <= 0)) {
|
|
2828
2732
|
throw new errors_2.ValidationError('totalTimeoutMs must be positive', 'totalTimeoutMs');
|
|
@@ -2835,7 +2739,7 @@ class OneShot {
|
|
|
2835
2739
|
throw new errors_2.OneShotError('Operation cancelled');
|
|
2836
2740
|
if (key)
|
|
2837
2741
|
options.onRequestCreated?.({ idempotencyKey: key });
|
|
2838
|
-
return await (0, deadline_1.abortable)(this.executeToolRequestImpl(endpoint, { ...options, idempotencyKey: key, signal: scope.signal }, quoteId, context), scope.signal);
|
|
2742
|
+
return (0, untrusted_content_1.markUntrusted)(await (0, deadline_1.abortable)(this.executeToolRequestImpl(endpoint, { ...options, idempotencyKey: key, signal: scope.signal }, quoteId, context), scope.signal), endpoint);
|
|
2839
2743
|
}
|
|
2840
2744
|
catch (error) {
|
|
2841
2745
|
if (scope.timedOut())
|
|
@@ -2858,7 +2762,7 @@ class OneShot {
|
|
|
2858
2762
|
Object.assign(extraHeaders, this.approvalHeader(approvalId));
|
|
2859
2763
|
if (payload.memo !== undefined) {
|
|
2860
2764
|
if (typeof payload.memo !== 'string' || payload.memo.trim().length === 0) {
|
|
2861
|
-
delete payload.memo; //
|
|
2765
|
+
delete payload.memo; // Invalid optional metadata must not block the call.
|
|
2862
2766
|
}
|
|
2863
2767
|
else if (payload.memo.length > 1000) {
|
|
2864
2768
|
payload.memo = payload.memo.slice(0, 1000);
|
|
@@ -2882,8 +2786,7 @@ class OneShot {
|
|
|
2882
2786
|
if (signal?.aborted) {
|
|
2883
2787
|
throw new errors_2.OneShotError('Operation cancelled');
|
|
2884
2788
|
}
|
|
2885
|
-
//
|
|
2886
|
-
// server-side gate knows about them on this very request.
|
|
2789
|
+
// The server must know the configured budget before the first paid request.
|
|
2887
2790
|
await this.ensureBudgetsSynced();
|
|
2888
2791
|
if (signal?.aborted)
|
|
2889
2792
|
throw new errors_2.OneShotError('Operation cancelled');
|
|
@@ -2893,7 +2796,6 @@ class OneShot {
|
|
|
2893
2796
|
throw new errors_2.OneShotError('Operation cancelled');
|
|
2894
2797
|
context.phase = 'submission';
|
|
2895
2798
|
let response = await this.makeRequest(endpoint, payload, undefined, quoteId, signal, undefined, extraHeaders);
|
|
2896
|
-
// Handle 402 Payment Required
|
|
2897
2799
|
if (response.status === 402 && /\/v1\/tools\/(solve|predict|find\/person|lookup\/person-(profile|dossier))$/.test(endpoint)) {
|
|
2898
2800
|
({ execResp: response } = await this.runQuoteToPay({
|
|
2899
2801
|
endpoint, payload, signal, maxCost, quoteResponse: response, extraHeaders,
|
|
@@ -2903,10 +2805,8 @@ class OneShot {
|
|
|
2903
2805
|
context.phase = 'submission';
|
|
2904
2806
|
}
|
|
2905
2807
|
else if (response.status === 402) {
|
|
2906
|
-
// Parse x402 v2 PaymentRequired from PAYMENT-REQUIRED header
|
|
2907
2808
|
const paymentRequiredHeader = response.headers.get('payment-required');
|
|
2908
2809
|
const { accepted, resource, extensions } = this.parsePaymentRequired(paymentRequiredHeader);
|
|
2909
|
-
// Fallback: parse legacy body format for amount display
|
|
2910
2810
|
const data = await response.json();
|
|
2911
2811
|
quoteId ?? (quoteId = data.context?.quote_id);
|
|
2912
2812
|
if (this._accessToken) {
|
|
@@ -2946,7 +2846,6 @@ class OneShot {
|
|
|
2946
2846
|
await this.failFromResponse('Tool request failed', response);
|
|
2947
2847
|
}
|
|
2948
2848
|
const result = await response.json();
|
|
2949
|
-
// Handle async jobs
|
|
2950
2849
|
if ((result.status === 'pending' || result.status === 'processing') && result.request_id) {
|
|
2951
2850
|
context.requestId = String(result.request_id);
|
|
2952
2851
|
context.receiptId = typeof result.receipt_id === 'string' ? result.receipt_id : undefined;
|
|
@@ -2956,7 +2855,7 @@ class OneShot {
|
|
|
2956
2855
|
if (!wait) {
|
|
2957
2856
|
return { ...result, idempotencyKey };
|
|
2958
2857
|
}
|
|
2959
|
-
const completed = await this.pollJob(result.request_id, timeout, signal, onStatusUpdate, waitForPhones ? { waitForPhones, phoneTimeoutSec } : undefined);
|
|
2858
|
+
const completed = await this.pollJob(result.request_id, timeout ?? tool_timeouts_generated_1.TOOL_TIMEOUTS[endpoint], signal, onStatusUpdate, waitForPhones ? { waitForPhones, phoneTimeoutSec } : undefined, endpoint);
|
|
2960
2859
|
if (completed && typeof completed === 'object' && !Array.isArray(completed)) {
|
|
2961
2860
|
return { ...completed, request_id: context.requestId, receipt_id: completed.receipt_id ?? context.receiptId, idempotencyKey };
|
|
2962
2861
|
}
|
|
@@ -2964,13 +2863,7 @@ class OneShot {
|
|
|
2964
2863
|
}
|
|
2965
2864
|
return (result.data ?? result);
|
|
2966
2865
|
}
|
|
2967
|
-
/**
|
|
2968
|
-
* Shared x402 quote-to-pay flow for paid tools. Fetches the 402 quote, runs
|
|
2969
|
-
* any per-tool 400 handling, enforces maxCost, signs the payment auth, and
|
|
2970
|
-
* POSTs the paid request. Returns the parsed quote context plus the raw paid
|
|
2971
|
-
* Response — the caller owns the post-202 handling (poll a job vs. return the
|
|
2972
|
-
* body directly), since that differs per tool.
|
|
2973
|
-
*/
|
|
2866
|
+
/** Shared quote-to-pay flow; callers own post-202 handling because some tools poll and others return directly. */
|
|
2974
2867
|
async runQuoteToPay(cfg) {
|
|
2975
2868
|
await this.ensureBudgetsSynced();
|
|
2976
2869
|
const approval = { ...cfg.extraHeaders, ...this.approvalHeader(cfg.approvalId) };
|
|
@@ -2979,8 +2872,6 @@ class OneShot {
|
|
|
2979
2872
|
await cfg.on400(quoteResp);
|
|
2980
2873
|
}
|
|
2981
2874
|
if (quoteResp.status !== 402) {
|
|
2982
|
-
// Routes the budget gate on the quote leg (fixed-price ones) surface a
|
|
2983
|
-
// 403 here; failFromResponse maps it to BudgetExceededError.
|
|
2984
2875
|
await this.failFromResponse(cfg.expectMsg, quoteResp);
|
|
2985
2876
|
}
|
|
2986
2877
|
const quoteData = await quoteResp.json();
|
|
@@ -3029,14 +2920,20 @@ class OneShot {
|
|
|
3029
2920
|
this.validate(requestId, 'requestId');
|
|
3030
2921
|
return this.pollJob(requestId, options.timeout, options.signal, options.onStatusUpdate, options.waitForPhones ? { waitForPhones: true, phoneTimeoutSec: options.phoneTimeoutSec } : undefined);
|
|
3031
2922
|
}
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
2923
|
+
unwrapJobResult(job) {
|
|
2924
|
+
const result = job.result ?? job;
|
|
2925
|
+
if (result && typeof result === 'object' && !Array.isArray(result)) {
|
|
2926
|
+
const value = { ...result };
|
|
2927
|
+
for (const field of ['cached', 'fetched_at']) {
|
|
2928
|
+
if (field in job)
|
|
2929
|
+
value[field] = job[field];
|
|
2930
|
+
}
|
|
2931
|
+
return value;
|
|
2932
|
+
}
|
|
2933
|
+
return result;
|
|
2934
|
+
}
|
|
2935
|
+
async pollJob(requestId, timeoutSec, signal, onStatusUpdate, phoneOpts, tool = '') {
|
|
2936
|
+
// Race HTTP with WebSocket push so a broken push channel cannot stall job completion.
|
|
3040
2937
|
if (signal?.aborted)
|
|
3041
2938
|
throw new errors_2.OneShotError('Operation cancelled');
|
|
3042
2939
|
const inner = new AbortController();
|
|
@@ -3088,31 +2985,15 @@ class OneShot {
|
|
|
3088
2985
|
signal?.removeEventListener('abort', onOuterAbort);
|
|
3089
2986
|
}
|
|
3090
2987
|
this.log(`Job ${requestId} ready after ${Date.now() - startedAt}ms via ${wait.via ?? 'unknown'}`);
|
|
3091
|
-
//
|
|
3092
|
-
// Only kicks in when the caller explicitly opts in AND the result still
|
|
3093
|
-
// has phones_pending=true (set by the worker when the upstream enrichment
|
|
3094
|
-
// has a pending async phone callback). This is opt-in so existing callers
|
|
3095
|
-
// see no behavior change — the WebSocket/HTTP polls return as soon as the
|
|
3096
|
-
// worker sets status=completed, with phones=null.
|
|
3097
|
-
//
|
|
3098
|
-
// Pass the existing result as the initial fallback: if the polling GETs
|
|
3099
|
-
// hit a transient error before any successful refresh, we return what we
|
|
3100
|
-
// already have (with phones_pending=true still set so consumers know).
|
|
2988
|
+
// Phone reveals are opt-in; retain the completed snapshot if every refresh fails.
|
|
3101
2989
|
if (phoneOpts?.waitForPhones && this._isPhonesPending(result)) {
|
|
3102
2990
|
result = await this._pollForPhones(requestId, phoneOpts.phoneTimeoutSec ?? 360, signal, result);
|
|
3103
2991
|
}
|
|
3104
2992
|
if (phoneOpts?.waitForPhones)
|
|
3105
2993
|
notify('completed');
|
|
3106
|
-
return result;
|
|
2994
|
+
return (0, untrusted_content_1.markUntrusted)(result, tool || wait.resultTool || '');
|
|
3107
2995
|
}
|
|
3108
|
-
/**
|
|
3109
|
-
* Detects whether a job result is still waiting for an async phone reveal.
|
|
3110
|
-
* The worker sets `result.phones_pending=true` when the upstream enrichment
|
|
3111
|
-
* has a pending async webhook and the webhook URL is configured; the webhook
|
|
3112
|
-
* handler flips it to false (or removes it) once phones arrive.
|
|
3113
|
-
*
|
|
3114
|
-
* Accepts worker search rows, profile objects, dossier sections and legacy results.
|
|
3115
|
-
*/
|
|
2996
|
+
/** Detect pending phone reveals in search rows, profiles, dossier sections and legacy results. */
|
|
3116
2997
|
_isPhonesPending(result) {
|
|
3117
2998
|
if (!result || typeof result !== 'object')
|
|
3118
2999
|
return false;
|
|
@@ -3128,19 +3009,10 @@ class OneShot {
|
|
|
3128
3009
|
|| (Array.isArray(body.results) && body.results.some(pending));
|
|
3129
3010
|
});
|
|
3130
3011
|
}
|
|
3131
|
-
/**
|
|
3132
|
-
* Slow poll loop that waits for async phone-reveal callbacks to arrive via
|
|
3133
|
-
* the webhook handler (which UPDATEs jobs.result_data.result.phones). Polls
|
|
3134
|
-
* GET /v1/requests/{id} every 5s — the upstream provider can take several
|
|
3135
|
-
* minutes to deliver, so a tight loop just wastes API calls. Returns the
|
|
3136
|
-
* latest snapshot whether or not phones arrived (consumer can re-check
|
|
3137
|
-
* `phones_pending`).
|
|
3138
|
-
*/
|
|
3012
|
+
/** Poll phone reveals every 5s to avoid wasting calls during slow callbacks; return the latest snapshot on timeout. */
|
|
3139
3013
|
async _pollForPhones(requestId, timeoutSec, signal, initialResult) {
|
|
3140
3014
|
const deadline = Date.now() + timeoutSec * 1000;
|
|
3141
3015
|
const interval = 5000;
|
|
3142
|
-
// Seed with the snapshot the caller already has so a transient first-poll
|
|
3143
|
-
// failure can still return the sync data (with phones_pending=true).
|
|
3144
3016
|
let lastResult = initialResult;
|
|
3145
3017
|
while (Date.now() < deadline) {
|
|
3146
3018
|
if (signal?.aborted)
|
|
@@ -3151,14 +3023,13 @@ class OneShot {
|
|
|
3151
3023
|
signal,
|
|
3152
3024
|
});
|
|
3153
3025
|
if (!resp.ok) {
|
|
3154
|
-
//
|
|
3155
|
-
// whatever we last had so consumers don't lose the sync result.
|
|
3026
|
+
// Preserve the last successful snapshot across transient phone-wait errors.
|
|
3156
3027
|
if (lastResult !== undefined)
|
|
3157
3028
|
return lastResult;
|
|
3158
3029
|
throw new errors_2.ToolError('Failed to check job status', resp.status, await resp.text(), resp.headers.get('retry-after'));
|
|
3159
3030
|
}
|
|
3160
3031
|
const job = await resp.json();
|
|
3161
|
-
lastResult = (job
|
|
3032
|
+
lastResult = this.unwrapJobResult(job);
|
|
3162
3033
|
if (!this._isPhonesPending(lastResult)) {
|
|
3163
3034
|
return lastResult;
|
|
3164
3035
|
}
|
|
@@ -3174,38 +3045,17 @@ class OneShot {
|
|
|
3174
3045
|
}
|
|
3175
3046
|
await this.sleep(interval, signal);
|
|
3176
3047
|
}
|
|
3177
|
-
// Timeout — return the last snapshot. phones_pending will still be true
|
|
3178
|
-
// so the consumer knows phones never arrived.
|
|
3179
3048
|
return lastResult;
|
|
3180
3049
|
}
|
|
3181
|
-
/**
|
|
3182
|
-
*
|
|
3183
|
-
* request, rejects with `JobError` on `failed` and with `OneShotError` on
|
|
3184
|
-
* abort. Every other failure (no WebSocket global, handshake refused, socket
|
|
3185
|
-
* closed early) rejects with a plain `Error`, which `pollJob` treats as
|
|
3186
|
-
* "no push available" rather than as an outcome. There is no timeout here:
|
|
3187
|
-
* the HTTP branch owns the deadline and aborts this one when it settles.
|
|
3188
|
-
*/
|
|
3050
|
+
/** WebSocket transport failures leave HTTP polling in charge; job outcomes and cancellation settle the wait.
|
|
3051
|
+
* The HTTP branch owns the deadline and aborts this branch when it settles. */
|
|
3189
3052
|
async waitViaWebSocket(requestId, signal, emit, wait) {
|
|
3190
|
-
//
|
|
3191
|
-
//
|
|
3192
|
-
// cannot set a custom `Authorization` header on the handshake request —
|
|
3193
|
-
// so there is no way to authenticate this upgrade without putting the
|
|
3194
|
-
// reusable bearer token in the URL, which issue #971 round 1 (finding
|
|
3195
|
-
// F-t_4d087f2a-5) forbids: a bearer secret must never ride in a URL that
|
|
3196
|
-
// proxies, access logs, and telemetry can capture. These sessions skip
|
|
3197
|
-
// the WebSocket accelerator entirely and rely on HTTP polling, which
|
|
3198
|
-
// authenticates via the Authorization header on every request.
|
|
3053
|
+
// WebSocket cannot carry an Authorization header; keep reusable bearer tokens out of logged URLs.
|
|
3054
|
+
// Token sessions use authenticated HTTP polling instead.
|
|
3199
3055
|
if (this.isAccessTokenSession) {
|
|
3200
3056
|
throw new Error('WebSocket push is not available for access-token sessions; falling back to HTTP polling');
|
|
3201
3057
|
}
|
|
3202
|
-
//
|
|
3203
|
-
// server authenticates the upgrade itself (not just the post-upgrade
|
|
3204
|
-
// connection) the same as agentProofAuth does for every other free-read
|
|
3205
|
-
// route (issue #971) — an unsigned ?wallet= alone is no longer
|
|
3206
|
-
// sufficient. Unlike a bearer token, a proof is short-lived and single-use
|
|
3207
|
-
// (5-minute TTL, Redis-enforced nonce), so passing it as a URL query
|
|
3208
|
-
// param is an acceptable channel for it.
|
|
3058
|
+
// Authenticate the upgrade with a short-lived, single-use proof; a wallet address alone proves nothing.
|
|
3209
3059
|
const authHeaders = await this.signedReadHeaders();
|
|
3210
3060
|
const authParams = new URLSearchParams();
|
|
3211
3061
|
if (authHeaders['x-agent-proof'])
|
|
@@ -3267,7 +3117,8 @@ class OneShot {
|
|
|
3267
3117
|
settle(() => {
|
|
3268
3118
|
cleanup();
|
|
3269
3119
|
wait.via = 'ws';
|
|
3270
|
-
|
|
3120
|
+
wait.resultTool = typeof msg.result_tool === 'string' ? msg.result_tool : undefined;
|
|
3121
|
+
const result = this.unwrapJobResult(msg);
|
|
3271
3122
|
if (msg.request_id && typeof result === 'object' && result !== null && !('request_id' in result)) {
|
|
3272
3123
|
result.request_id = msg.request_id;
|
|
3273
3124
|
}
|
|
@@ -3293,7 +3144,6 @@ class OneShot {
|
|
|
3293
3144
|
});
|
|
3294
3145
|
};
|
|
3295
3146
|
ws.onclose = () => {
|
|
3296
|
-
// Any close before we got a result means no push is coming.
|
|
3297
3147
|
settle(() => {
|
|
3298
3148
|
cleanup();
|
|
3299
3149
|
reject(new Error('WebSocket closed before result'));
|
|
@@ -3341,7 +3191,7 @@ class OneShot {
|
|
|
3341
3191
|
});
|
|
3342
3192
|
if (!resp.ok) {
|
|
3343
3193
|
const body = await resp.text();
|
|
3344
|
-
// 5xx / 429 from the poll endpoint are transient
|
|
3194
|
+
// 5xx / 429 from the poll endpoint are transient - keep polling.
|
|
3345
3195
|
// Any other non-2xx (401/403/404) is a real answer about this job.
|
|
3346
3196
|
if (resp.status >= 500 || resp.status === 429) {
|
|
3347
3197
|
throw new Error(`Poll returned ${resp.status}: ${body.slice(0, 200)}`);
|
|
@@ -3352,10 +3202,11 @@ class OneShot {
|
|
|
3352
3202
|
emit?.(String(job.status));
|
|
3353
3203
|
if (job.status === 'completed') {
|
|
3354
3204
|
this.log('Job completed');
|
|
3355
|
-
if (wait)
|
|
3205
|
+
if (wait) {
|
|
3356
3206
|
wait.via = 'http';
|
|
3357
|
-
|
|
3358
|
-
|
|
3207
|
+
wait.resultTool = typeof job.result_tool === 'string' ? job.result_tool : undefined;
|
|
3208
|
+
}
|
|
3209
|
+
const result = this.unwrapJobResult(job);
|
|
3359
3210
|
if (job.request_id && typeof result === 'object' && result !== null && !('request_id' in result)) {
|
|
3360
3211
|
result.request_id = job.request_id;
|
|
3361
3212
|
}
|
|
@@ -3410,9 +3261,6 @@ class OneShot {
|
|
|
3410
3261
|
}
|
|
3411
3262
|
async makeRequest(endpoint, data, payment, quoteId, signal, timeoutMs, extraHeaders) {
|
|
3412
3263
|
let resp = await this.sendLeg(endpoint, data, payment, quoteId, signal, timeoutMs, extraHeaders);
|
|
3413
|
-
// The API refuses a request whose wallet proof it rejected, saying why (#1437).
|
|
3414
|
-
// A stale clock window or a proof sent twice is cured by signing a new proof:
|
|
3415
|
-
// send the leg again once. Nothing is paid in answer to this refusal.
|
|
3416
3264
|
for (let attempt = 0; resp.status === 401; attempt++) {
|
|
3417
3265
|
const refusal = await this.readInvalidProof(resp);
|
|
3418
3266
|
if (!refusal)
|
|
@@ -3441,25 +3289,13 @@ class OneShot {
|
|
|
3441
3289
|
}
|
|
3442
3290
|
}
|
|
3443
3291
|
async sendLeg(endpoint, data, payment, quoteId, signal, timeoutMs, extraHeaders) {
|
|
3444
|
-
//
|
|
3445
|
-
//
|
|
3446
|
-
// for a caller that has proven it holds the wallet (a bare X-Agent-ID is a
|
|
3447
|
-
// public address). A leg that carries a payment signature needs none. The
|
|
3448
|
-
// endpoints with durable admission read a 'submit' proof; a caller that
|
|
3449
|
-
// asked for a proof explicitly keeps the scope it always had.
|
|
3450
|
-
//
|
|
3451
|
-
// One payment leg does need a proof: when credits cover the whole cost the
|
|
3452
|
-
// "payment" is the zero-cost authorization, an unsigned stub (signature
|
|
3453
|
-
// '0x'). It proves nothing, and the API refuses to spend credits on it
|
|
3454
|
-
// alone (401 proof_required).
|
|
3292
|
+
// Credit spending requires wallet proof unless a real payment signature already proves ownership.
|
|
3293
|
+
// Zero-cost authorizations are unsigned stubs, so those still need a proof.
|
|
3455
3294
|
const unsignedStub = payment?.payload?.signature === '0x';
|
|
3456
|
-
// Identity Verification
|
|
3457
|
-
// only for a caller that proves the wallet: its payment leg carries the agent proof too, however the key was
|
|
3458
|
-
// given (an option, or the idempotency_key body field the generated MCP tool sends). The API refuses a
|
|
3459
|
-
// payment signature alone there with agent_proof_required. Same list as oneshot-python.
|
|
3295
|
+
// Identity Verification requires wallet proof on the payment leg too, to recover keyed submissions.
|
|
3460
3296
|
const provenPayment = /(?:^|\/)identity\/start$/.test(endpoint);
|
|
3461
3297
|
const askedForProof = !!extraHeaders?.['x-agent-proof'];
|
|
3462
|
-
const proofScope = /(lookup\/person-(profile|email|email-status)|lookup\/place-permits|physical-mail\/send|solve|predict)$/.test(endpoint)
|
|
3298
|
+
const proofScope = /(lookup\/person-(profile|email|email-status)|lookup\/place-permits|lookup\/company-private-raises|physical-mail\/send|solve|predict)$/.test(endpoint)
|
|
3463
3299
|
? 'submit'
|
|
3464
3300
|
: askedForProof ? 'read' : 'write';
|
|
3465
3301
|
const headers = {
|
|
@@ -3503,14 +3339,8 @@ class OneShot {
|
|
|
3503
3339
|
throw new errors_2.OneShotError('Operation cancelled before payment');
|
|
3504
3340
|
}
|
|
3505
3341
|
}
|
|
3506
|
-
/**
|
|
3507
|
-
*
|
|
3508
|
-
* minus payments signed but not yet settled) covers this charge, swapping
|
|
3509
|
-
* ETH→USDC for a buffer of `swapBufferMultiplier` payments when it does not,
|
|
3510
|
-
* then reserve the charge. Returns the reservation so the caller can release
|
|
3511
|
-
* it if the payment never settles (request failed). Runs under a per-instance
|
|
3512
|
-
* lock so concurrent calls never double-swap or race the wallet nonce.
|
|
3513
|
-
*/
|
|
3342
|
+
/** Reserve effective USDC (on-chain minus unsettled payments), swapping a buffer if needed.
|
|
3343
|
+
* Serialize reservations and swaps to avoid overspending and nonce races; release failed payments. */
|
|
3514
3344
|
async ensureUsdcBalance(paymentInfo) {
|
|
3515
3345
|
if (this._currency !== 'ETH')
|
|
3516
3346
|
return undefined;
|
|
@@ -3591,18 +3421,10 @@ class OneShot {
|
|
|
3591
3421
|
if (reservation)
|
|
3592
3422
|
this._usdcPending.delete(reservation.id);
|
|
3593
3423
|
}
|
|
3594
|
-
/**
|
|
3595
|
-
* How much USDC to buy: top up to `charge × swapBufferMultiplier`, counting
|
|
3596
|
-
* whatever effective balance is already there. When the wallet provider
|
|
3597
|
-
* exposes `getBalance` and its ETH cannot cover the buffered quote, fall
|
|
3598
|
-
* back to the bare shortfall so an ETH-poor wallet can still make the one
|
|
3599
|
-
* payment in front of it (a send-only provider always gets the buffer).
|
|
3600
|
-
*/
|
|
3424
|
+
/** Buy a buffer, or only the shortfall when getBalance shows insufficient ETH for the buffer. */
|
|
3601
3425
|
async sizeSwap(charge, effective, chainId) {
|
|
3602
3426
|
const shortfall = charge - effective;
|
|
3603
|
-
//
|
|
3604
|
-
// finite, exact integer): keeps the multiplier's precision, no float→BigInt
|
|
3605
|
-
// on an unbounded value.
|
|
3427
|
+
// Bounded fixed-point math preserves precision without converting an unbounded float to BigInt.
|
|
3606
3428
|
const mScaled = BigInt(Math.round(this._swapBufferMultiplier * SWAP_MULTIPLIER_SCALE));
|
|
3607
3429
|
const target = (charge * mScaled + BigInt(SWAP_MULTIPLIER_SCALE) - 1n) / BigInt(SWAP_MULTIPLIER_SCALE);
|
|
3608
3430
|
let amount = target > effective ? target - effective : shortfall;
|
|
@@ -3616,7 +3438,7 @@ class OneShot {
|
|
|
3616
3438
|
}
|
|
3617
3439
|
}
|
|
3618
3440
|
catch {
|
|
3619
|
-
// Quoting failed
|
|
3441
|
+
// Quoting failed - let executeSwap quote again and report properly.
|
|
3620
3442
|
}
|
|
3621
3443
|
}
|
|
3622
3444
|
return amount;
|
|
@@ -3627,7 +3449,7 @@ class OneShot {
|
|
|
3627
3449
|
const quote = await getSwapQuote(this.rpcProvider, usdcAmount, chainId, this._slippage);
|
|
3628
3450
|
return quote?.amountInMax;
|
|
3629
3451
|
}
|
|
3630
|
-
/**
|
|
3452
|
+
/** Swap on the configured chain using the wallet provider. */
|
|
3631
3453
|
async executeUsdcSwap(usdcAmount, chainId) {
|
|
3632
3454
|
const { executeSwap } = await Promise.resolve().then(() => __importStar(require('./swap')));
|
|
3633
3455
|
return executeSwap(this.provider, this.rpcProvider, usdcAmount, chainId, this._slippage);
|
|
@@ -3644,7 +3466,7 @@ class OneShot {
|
|
|
3644
3466
|
}
|
|
3645
3467
|
catch (err) {
|
|
3646
3468
|
// Keyed writes may already be queued server-side after a transport failure: keep the reservation.
|
|
3647
|
-
if (!/(lookup\/person-(profile|email|email-status)|lookup\/place-permits|physical-mail\/send|solve|predict|linkedin\/(reply|message|profile-view|react|invite(?:\/withdraw)?))$/.test(endpoint))
|
|
3469
|
+
if (!/(lookup\/person-(profile|email|email-status)|lookup\/place-permits|lookup\/company-private-raises|physical-mail\/send|solve|predict|linkedin\/(reply|message|profile-view|react|invite(?:\/withdraw)?))$/.test(endpoint))
|
|
3648
3470
|
this.releaseUsdcReservation(signed.reservation);
|
|
3649
3471
|
throw err;
|
|
3650
3472
|
}
|
|
@@ -3660,7 +3482,6 @@ class OneShot {
|
|
|
3660
3482
|
? Buffer.from(header, 'base64').toString()
|
|
3661
3483
|
: atob(header);
|
|
3662
3484
|
const parsed = JSON.parse(decoded);
|
|
3663
|
-
// x402 v2: { x402Version: 2, accepts: [...], resource: {...}, extensions: {...} }
|
|
3664
3485
|
if (parsed.accepts?.length > 0) {
|
|
3665
3486
|
return {
|
|
3666
3487
|
accepted: parsed.accepts[0],
|
|
@@ -3673,7 +3494,6 @@ class OneShot {
|
|
|
3673
3494
|
this.log('Failed to parse PAYMENT-REQUIRED header, using defaults');
|
|
3674
3495
|
}
|
|
3675
3496
|
}
|
|
3676
|
-
// Fallback: construct from known production values
|
|
3677
3497
|
return {
|
|
3678
3498
|
accepted: {
|
|
3679
3499
|
scheme: 'exact',
|
|
@@ -3696,14 +3516,13 @@ class OneShot {
|
|
|
3696
3516
|
if (header) {
|
|
3697
3517
|
return this.parsePaymentRequired(header);
|
|
3698
3518
|
}
|
|
3699
|
-
// Probe: send quote ID without payment to get x402 middleware's 402
|
|
3700
3519
|
// Carries X-Approval-Id: a policy-held call's probe must reach the 402,
|
|
3701
3520
|
// not be held again.
|
|
3702
3521
|
const probeResp = await this.makeRequest(endpoint, payload, undefined, quoteId, signal, undefined, extraHeaders);
|
|
3703
3522
|
return this.parsePaymentRequired(probeResp.headers.get('payment-required'));
|
|
3704
3523
|
}
|
|
3705
3524
|
async signPaymentAuthorization(paymentInfo, accepted, resource, extensions) {
|
|
3706
|
-
// Credits may cover the full cost
|
|
3525
|
+
// Credits may cover the full cost - send a zero-cost authorization so the server
|
|
3707
3526
|
// always receives a payment-signature header (avoids 402 from x402 SDK).
|
|
3708
3527
|
if (parseFloat(paymentInfo.amount) === 0) {
|
|
3709
3528
|
this.log('Credits cover full cost — sending zero-cost authorization');
|
|
@@ -3725,12 +3544,9 @@ class OneShot {
|
|
|
3725
3544
|
},
|
|
3726
3545
|
} };
|
|
3727
3546
|
}
|
|
3728
|
-
// Belt and braces: every paid path branches to retryAsCreditsSession
|
|
3729
|
-
// before reaching here in access-token mode.
|
|
3730
3547
|
if (this._accessToken) {
|
|
3731
3548
|
throw new errors_1.InsufficientCreditsError(`Credits do not cover $${paymentInfo.amount}; top up credits or use a wallet session`, parseFloat(paymentInfo.amount));
|
|
3732
3549
|
}
|
|
3733
|
-
// If paying with ETH, make sure USDC covers the charge (swapping a buffer if not) and reserve it
|
|
3734
3550
|
const reservation = await this.ensureUsdcBalance(paymentInfo);
|
|
3735
3551
|
const now = Math.floor(Date.now() / 1000);
|
|
3736
3552
|
const nonce = ethers_1.ethers.randomBytes(32);
|
|
@@ -3738,14 +3554,10 @@ class OneShot {
|
|
|
3738
3554
|
const validAfter = now - 300; // Buffer for clock skew
|
|
3739
3555
|
const validBefore = now + 3600;
|
|
3740
3556
|
const nonceHex = ethers_1.ethers.hexlify(nonce);
|
|
3741
|
-
// Use the EIP-712 domain
|
|
3742
|
-
// version AND chain. The chain used to be the mainnet constant, which made
|
|
3743
|
-
// every signature invalid against Base Sepolia (the domain separator
|
|
3744
|
-
// includes chainId), so the SDK could never pay on staging.
|
|
3557
|
+
// Use the server's full EIP-712 domain, including chainId, so the signature matches its network.
|
|
3745
3558
|
const domainName = accepted.extra?.name || 'USD Coin';
|
|
3746
3559
|
const domainVersion = accepted.extra?.version || '2';
|
|
3747
3560
|
const chainId = chainIdFromNetwork(accepted.network ?? paymentInfo.network) ?? CHAIN_ID;
|
|
3748
|
-
// Sign EIP-3009 TransferWithAuthorization
|
|
3749
3561
|
let signature;
|
|
3750
3562
|
try {
|
|
3751
3563
|
signature = await this.provider.signTypedData({
|
|
@@ -3775,7 +3587,6 @@ class OneShot {
|
|
|
3775
3587
|
this.releaseUsdcReservation(reservation);
|
|
3776
3588
|
throw err;
|
|
3777
3589
|
}
|
|
3778
|
-
// Return x402 PaymentPayload v2 format (including resource + extensions for Bazaar discovery)
|
|
3779
3590
|
return { reservation, auth: {
|
|
3780
3591
|
x402Version: 2,
|
|
3781
3592
|
...(resource ? { resource } : {}),
|