@oneshot-agent/sdk 0.60.0 → 0.61.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/dist/index.js CHANGED
@@ -37,6 +37,7 @@ 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");
40
41
  const hunts_1 = require("./hunts");
41
42
  __exportStar(require("./solve"), exports);
42
43
  __exportStar(require("./predict"), exports);
@@ -61,13 +62,9 @@ Object.defineProperty(exports, "getSwapQuote", { enumerable: true, get: function
61
62
  Object.defineProperty(exports, "executeSwap", { enumerable: true, get: function () { return swap_1.executeSwap; } });
62
63
  __exportStar(require("./errors"), exports);
63
64
  const approvals_1 = require("./approvals");
64
- // `./receipt` is NOT re-exported here, values or types: it statically imports
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.
65
+ // Keep receipt on its subpath: node:crypto breaks browser bundles, and providerCost is not public.
69
66
  // Keep in sync with package.json `version`. Guarded by version.test.ts.
70
- const SDK_VERSION = '0.60.0';
67
+ const SDK_VERSION = '0.61.0';
71
68
  /** HTTP poll cadence while push is unconfirmed: fast first checks, settling at 2s. */
72
69
  const HTTP_POLL_BACKOFF_MS = [300, 600, 1000, 2000];
73
70
  /** HTTP poll cadence once the WebSocket has delivered for this request. */
@@ -106,12 +103,7 @@ function chainIdFromNetwork(network) {
106
103
  return m ? Number(m[1]) : undefined;
107
104
  }
108
105
  const USDC_ADDRESS = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';
109
- // ETH-currency mode. A payment is an EIP-3009 authorization that the
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).
106
+ // Reserve signed-but-unsettled payments against balanceOf so delayed settlement cannot overspend USDC.
115
107
  /** One balanceOf read per paid call, deduped within roughly one Base block. */
116
108
  const USDC_BALANCE_CACHE_MS = 2000;
117
109
  /** How long a signed authorization stays subtracted from the on-chain balance (settlement lands well within this). */
@@ -139,10 +131,7 @@ __exportStar(require("./types"), exports);
139
131
  * await agent.email({ to: 'user@example.com', subject: 'Hi', body: 'Hello' });
140
132
  * ```
141
133
  */
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
- */
134
+ /** Reject invalid budgets at construction so a typo cannot silently remove the cap. */
146
135
  function validateBudgetConfig(budgets) {
147
136
  if (!budgets)
148
137
  return undefined;
@@ -174,13 +163,13 @@ function validateBudgetConfig(budgets) {
174
163
  }
175
164
  /** The keys executeToolRequestImpl takes out of a request as SDK options, so they never reach the API body. */
176
165
  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 (#2153): the swap path needs a confirmed receipt CDP never returns. */
166
+ /** ETH mode is unsupported on CDP wallets: the swap path needs a confirmed receipt CDP never returns. */
178
167
  function cdpEthModeError() {
179
168
  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
169
  }
181
170
  class OneShot {
182
171
  /**
183
- * Async factory — required for CDP wallets (account creation is async).
172
+ * Async factory, required for CDP wallets (account creation is async).
184
173
  * Also works with privateKey and custom walletProvider.
185
174
  *
186
175
  * @example
@@ -196,8 +185,7 @@ class OneShot {
196
185
  if (config.accessToken) {
197
186
  if (config.address)
198
187
  return new OneShot(config);
199
- // The API keys job status and the WebSocket on X-Agent-ID, so the
200
- // session needs its wallet address even though it never signs with it.
188
+ // Job status and WebSocket requests need the wallet address even when the session never signs.
201
189
  const address = await OneShot.resolveTokenAddress(config);
202
190
  return new OneShot({ ...config, address });
203
191
  }
@@ -205,7 +193,7 @@ class OneShot {
205
193
  return new OneShot(config, config.walletProvider);
206
194
  }
207
195
  if (config.cdp) {
208
- // Reject before the CDP SDK is imported or an account is created (#2153).
196
+ // Reject before importing the CDP SDK or creating an account.
209
197
  if ((config.currency ?? 'USDC') === 'ETH')
210
198
  throw cdpEthModeError();
211
199
  const { CdpWalletProvider } = await Promise.resolve().then(() => __importStar(require('./providers/cdp')));
@@ -221,7 +209,7 @@ class OneShot {
221
209
  throw new errors_2.ValidationError('Provide one of: privateKey, cdp, or walletProvider', 'config');
222
210
  }
223
211
  /**
224
- * Sync constructor — works with privateKey (backwards compatible).
212
+ * Sync constructor; works with privateKey.
225
213
  * For CDP wallets, use OneShot.create() instead.
226
214
  */
227
215
  constructor(config, walletProvider) {
@@ -260,9 +248,7 @@ class OneShot {
260
248
  this._stagingKey = config.stagingKey;
261
249
  this.rpcProvider = new ethers_1.ethers.JsonRpcProvider(config.rpcUrl ?? RPC_URL);
262
250
  if (config.accessToken) {
263
- // Option D: access-token session. Exclusive with every wallet option,
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.
251
+ // Delegated tokens cannot change their own budgets; caps must be set from a wallet session.
266
252
  if (!/^oneshot_[0-9a-f]{64}$/.test(config.accessToken)) {
267
253
  throw new errors_2.ValidationError('accessToken must be oneshot_<64 lowercase hex characters>', 'accessToken');
268
254
  }
@@ -297,9 +283,7 @@ class OneShot {
297
283
  else {
298
284
  throw new errors_2.ValidationError('Provide privateKey or use OneShot.create() for CDP/custom wallets', 'config');
299
285
  }
300
- // CDP wallets cannot run ETH mode even though CdpWalletProvider exposes
301
- // sendTransaction: its wait() never observes a receipt, so the swap path
302
- // cannot confirm the ETH→USDC trade (#2153).
286
+ // CDP wait() never confirms a receipt, so ETH mode cannot confirm the swap.
303
287
  if (config.cdp && this._currency === 'ETH')
304
288
  throw cdpEthModeError();
305
289
  if (this._currency === 'ETH' && !this.provider.sendTransaction) {
@@ -342,16 +326,8 @@ class OneShot {
342
326
  this.validate(options.subject, 'subject');
343
327
  }
344
328
  this.validate(options.body, 'body');
345
- // Rotation mode: when the caller passes neither from_domain nor
346
- // from_mailbox, omit from_address entirely so the server picks from
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.
329
+ // Without a pinned sender, the quote locks an address from the agent's domain pool.
330
+ // A mailbox alone has no owned domain to send from.
355
331
  if (options.from_mailbox && !options.from_domain) {
356
332
  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
333
  }
@@ -359,24 +335,14 @@ class OneShot {
359
335
  const fromAddress = useRotation
360
336
  ? undefined
361
337
  : `${options.from_mailbox ?? 'agent'}@${options.from_domain}`;
362
- // Confirming a new-domain registration (#1070): the caller passes back the
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.
338
+ // Reuse the confirmed quote so the charge matches the amount shown; true alone confirms no amount.
370
339
  const confirmedQuote = typeof options.confirmNewDomain === 'string' && options.confirmNewDomain.trim()
371
340
  ? options.confirmNewDomain.trim() : undefined;
372
341
  if (confirmedQuote) {
373
342
  this.log(`Email: sending against confirmed quote ${confirmedQuote}`);
374
343
  return this.executeToolRequest('/v1/tools/email/send', this.emailSendPayload(options, fromAddress), confirmedQuote);
375
344
  }
376
- // `mailbox_provisioning_fee` (>0) means `from_address` is a new address that
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).
345
+ // Mailbox provisioning and domain registration are distinct fees; only a new domain needs confirmation.
380
346
  const quote = await this.tool('email/quote', {
381
347
  ...(fromAddress ? { from_address: fromAddress } : {}),
382
348
  ...(options.to !== undefined ? { to_address: options.to } : {}),
@@ -384,23 +350,12 @@ class OneShot {
384
350
  ...(options.reply_to_email_id ? { reply_to_email_id: options.reply_to_email_id } : {}),
385
351
  ...(options.mailbox_mode ? { mailbox_mode: options.mailbox_mode } : {}),
386
352
  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
353
  maxCost: options.maxCost,
390
354
  });
391
355
  this.log(`Email quote: $${quote.total_cost}`);
392
- // Local fast-fail — matches the per-tool check on commerce/voice/sms/build/
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).
356
+ // Enforce the cap locally even if the server did not check the header.
398
357
  this.assertWithinMaxCost(quote.total_cost, options.maxCost);
399
- // Registering a brand-new domain is charged only after its exact quote has
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.
358
+ // A new domain must be explicitly authorized against its exact quote before any charge or send.
404
359
  if (quote.is_new) {
405
360
  const domainName = quote.domain ?? options.from_domain ?? '';
406
361
  const breakdown = quote.registration_fee !== undefined && quote.service_fee !== undefined
@@ -423,16 +378,11 @@ class OneShot {
423
378
  /** The /email/send body for `email()`: the same whether it follows a fresh quote or a confirmed one. */
424
379
  emailSendPayload(options, resolvedFromAddress) {
425
380
  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
381
  ...(resolvedFromAddress ? { from_address: resolvedFromAddress } : {}),
430
382
  ...(options.to !== undefined ? { to_address: options.to } : {}),
431
383
  ...(options.subject !== undefined ? { subject: options.subject } : {}),
432
384
  ...(options.reply_to_email_id ? { reply_to_email_id: options.reply_to_email_id } : {}),
433
- // Re-passed on /send so a new-domain mailbox quote still provisions a real
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).
385
+ // Preserve mailbox provisioning when the new domain does not yet have a stored mode.
436
386
  ...(options.mailbox_mode ? { mailbox_mode: options.mailbox_mode } : {}),
437
387
  body: options.body,
438
388
  signal: options.signal,
@@ -446,15 +396,14 @@ class OneShot {
446
396
  payload.attachments = options.attachments;
447
397
  }
448
398
  if (options.idempotencyKey) {
449
- // Only the send call carries the key — the quote call has no side effects.
399
+ // Only sends need idempotency; quoting has no side effects.
450
400
  payload.idempotencyKey = options.idempotencyKey;
451
401
  }
452
402
  if (options.approvalId) {
453
403
  // Releases a send the action policy held (see ApprovalRequiredError).
454
404
  payload.approvalId = options.approvalId;
455
405
  }
456
- // Forward maxCost so the server-side X-Max-Cost-USDC header also guards
457
- // the send, which matters on a confirmed send that skipped the quote.
406
+ // Confirmed sends skip quoting, so the send must also carry the cap.
458
407
  if (options.maxCost !== undefined)
459
408
  payload.maxCost = options.maxCost;
460
409
  return payload;
@@ -515,7 +464,7 @@ class OneShot {
515
464
  const { includePhone, ...payload } = options;
516
465
  return this.tool('find/person', { ...payload, include_phone: includePhone, limit: options.limit ?? 100 });
517
466
  }
518
- /** At least one identity (#1899): name, email, phone or a profile URL under any of its names. */
467
+ /** At least one identity: name, email, phone or a profile URL under any of its names. */
519
468
  requirePersonIdentity(options) {
520
469
  const hasName = options.name || options.full_name || (options.first_name && options.last_name);
521
470
  if (!hasName && !options.email && !options.phone && !options.profile_url && !options.linkedin_url && !options.social_media_url) {
@@ -562,7 +511,7 @@ class OneShot {
562
511
  return this.tool('resolve/business', { ...options });
563
512
  }
564
513
  /**
565
- * Federal contract opportunities (SAM.gov) by NAICS code — Sources Sought
514
+ * Federal contract opportunities (SAM.gov) by NAICS code - Sources Sought
566
515
  * and Presolicitation notices with the contracting officer's published
567
516
  * contact. Flat price per search; zero notices is a completed result.
568
517
  * `data_as_of` on the result is the snapshot behind the rows (SAM.gov's
@@ -593,7 +542,7 @@ class OneShot {
593
542
  async personDossier(options) {
594
543
  this.requirePersonIdentity(options);
595
544
  const { includePhone, ...payload } = options;
596
- // The dossier takes 2 to 5 minutes; the generic 120 second poll gives up on a normal run (#1913).
545
+ // The dossier takes 2 to 5 minutes; the generic 120 second poll gives up on a normal run.
597
546
  return this.tool('lookup/person-dossier', { ...payload, include_phone: includePhone, timeout: options.timeout ?? 300 });
598
547
  }
599
548
  async personSocialAccounts(options) {
@@ -629,7 +578,7 @@ class OneShot {
629
578
  if (!response.ok) {
630
579
  throw new errors_2.ToolError('Failed to list inbox', response.status, await response.text(), response.headers.get('retry-after'));
631
580
  }
632
- return response.json();
581
+ return (0, untrusted_content_1.markUntrusted)(await response.json(), 'inbox_list');
633
582
  }
634
583
  async inboxGet(emailId) {
635
584
  this.validate(emailId, 'emailId');
@@ -642,7 +591,7 @@ class OneShot {
642
591
  if (!response.ok) {
643
592
  throw new errors_2.ToolError('Failed to get email', response.status, await response.text(), response.headers.get('retry-after'));
644
593
  }
645
- return response.json();
594
+ return (0, untrusted_content_1.markUntrusted)(await response.json(), 'inbox_get');
646
595
  }
647
596
  async commerceBuy(options) {
648
597
  this.validate(options.product_url, 'product_url');
@@ -754,9 +703,9 @@ class OneShot {
754
703
  if (options.wait !== false && result.request_id) {
755
704
  // Wait for the longest the call may run, plus two minutes to dial and settle.
756
705
  const timeout = options.timeout ?? (options.max_duration_minutes ?? 10) * 60 + 120;
757
- return this.pollJob(result.request_id, timeout, options.signal, options.onStatusUpdate);
706
+ return this.pollJob(result.request_id, timeout, options.signal, options.onStatusUpdate, undefined, 'voice');
758
707
  }
759
- return result;
708
+ return (0, untrusted_content_1.markUntrusted)(result, 'voice');
760
709
  }
761
710
  /**
762
711
  * Send an SMS message
@@ -947,8 +896,6 @@ class OneShot {
947
896
  payload.session_id = options.session_id;
948
897
  if (options.profile_id)
949
898
  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
899
  if (options.max_steps)
953
900
  payload.max_steps = options.max_steps;
954
901
  const { execResp } = await this.runQuoteToPay({
@@ -972,9 +919,9 @@ class OneShot {
972
919
  const result = await execResp.json();
973
920
  this.log(`Browser task initiated: ${result.request_id}`);
974
921
  if (options.wait !== false && result.request_id) {
975
- return this.pollJob(result.request_id, options.timeout ?? 300, options.signal, options.onStatusUpdate);
922
+ return this.pollJob(result.request_id, options.timeout ?? 300, options.signal, options.onStatusUpdate, undefined, 'browser');
976
923
  }
977
- return result;
924
+ return (0, untrusted_content_1.markUntrusted)(result, 'browser');
978
925
  }
979
926
  /**
980
927
  * Create a persistent browser profile for reusing cookies/localStorage across sessions
@@ -1094,7 +1041,7 @@ class OneShot {
1094
1041
  throw new errors_1.LinkedInConnectRequiredError(body);
1095
1042
  throw new errors_2.ToolError(`LinkedIn ${opts.what} failed`, response.status, body, response.headers.get('retry-after'));
1096
1043
  }
1097
- return response.json();
1044
+ return (0, untrusted_content_1.markUntrusted)(await response.json(), `/v1/tools/linkedin${path}`, method);
1098
1045
  }
1099
1046
  /**
1100
1047
  * Create a hosted LinkedIn login link. Show `url` to the human once; it
@@ -1222,7 +1169,6 @@ class OneShot {
1222
1169
  this.validate(options.identifier, 'identifier');
1223
1170
  const { accountId, identifier, notify, sections, ...rest } = options;
1224
1171
  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
1172
  if (sections?.length)
1227
1173
  body.sections = sections;
1228
1174
  const result = await this.tool('linkedin/profile-view', body);
@@ -1285,7 +1231,7 @@ class OneShot {
1285
1231
  if (!response.ok) {
1286
1232
  throw new errors_2.ToolError('Failed to list SMS inbox', response.status, await response.text(), response.headers.get('retry-after'));
1287
1233
  }
1288
- return response.json();
1234
+ return (0, untrusted_content_1.markUntrusted)(await response.json(), 'sms_inbox_list');
1289
1235
  }
1290
1236
  /**
1291
1237
  * Get this agent a voice number without placing a call. Returns the existing
@@ -1368,7 +1314,7 @@ class OneShot {
1368
1314
  return this.signedJson('GET', `/v1/tools/voice/inbound/${encodeURIComponent(callId)}`, undefined, 'read', 'Failed to get inbound call');
1369
1315
  }
1370
1316
  /**
1371
- * Call a declared route of the API by method and path (#1580), the one generic call for a route that has no
1317
+ * Call a declared route of the API by method and path, the one generic call for a route that has no
1372
1318
  * typed method. Follows the Python client's `call_free_get/post/patch/put/delete`:
1373
1319
  *
1374
1320
  * - `path` is the route, with `{name}` (or `:name`) for a path parameter, filled from `params` and URL-encoded
@@ -1448,7 +1394,7 @@ class OneShot {
1448
1394
  if (response.status === 204 || !text)
1449
1395
  return { success: true };
1450
1396
  const json = JSON.parse(text);
1451
- return (json && typeof json === 'object' && 'data' in json ? json.data : json);
1397
+ return (0, untrusted_content_1.markUntrusted)(json && typeof json === 'object' && 'data' in json ? json.data : json, target, method);
1452
1398
  })();
1453
1399
  return await (0, deadline_1.abortable)(request, scope.signal);
1454
1400
  }
@@ -1470,7 +1416,7 @@ class OneShot {
1470
1416
  throw new errors_2.ToolError(failure, response.status, await response.text(), response.headers.get('retry-after'));
1471
1417
  }
1472
1418
  const json = await response.json();
1473
- return (json.data ?? json);
1419
+ return (0, untrusted_content_1.markUntrusted)(json.data ?? json, path, method);
1474
1420
  }
1475
1421
  /**
1476
1422
  * Get a specific inbound SMS message
@@ -1492,7 +1438,7 @@ class OneShot {
1492
1438
  if (!response.ok) {
1493
1439
  throw new errors_2.ToolError('Failed to get SMS message', response.status, await response.text(), response.headers.get('retry-after'));
1494
1440
  }
1495
- return response.json();
1441
+ return (0, untrusted_content_1.markUntrusted)(await response.json(), 'sms_inbox_get');
1496
1442
  }
1497
1443
  /**
1498
1444
  * List notifications for the agent
@@ -1541,7 +1487,7 @@ class OneShot {
1541
1487
  }
1542
1488
  }
1543
1489
  /**
1544
- * The agent's connections (#2403): GitHub and business systems, each with `id`, `system`, `account_label` and
1490
+ * The agent's connections: GitHub and business systems, each with `id`, `system`, `account_label` and
1545
1491
  * `status`. Free. A connected-system read (ledger, CRM, shipping, tax, procurement) takes the `id` of an active
1546
1492
  * one as `connection_id`; pick it by `system` and `account_label`, then call the read with the generic
1547
1493
  * {@link OneShot.route}.
@@ -1570,12 +1516,10 @@ class OneShot {
1570
1516
  return response.json();
1571
1517
  }
1572
1518
  async getBalance(tokenAddress) {
1573
- // Use unified API endpoint for USDC (default) balance
1574
1519
  if (!tokenAddress || tokenAddress === this.usdcAddress) {
1575
1520
  const unified = await this.getUnifiedBalance();
1576
1521
  return unified.on_chain_balance;
1577
1522
  }
1578
- // Custom token: keep existing RPC logic
1579
1523
  const contract = new ethers_1.ethers.Contract(tokenAddress, ['function balanceOf(address) view returns (uint256)', 'function decimals() view returns (uint8)'], this.rpcProvider);
1580
1524
  const [balance, decimals] = await Promise.all([
1581
1525
  contract.balanceOf(this.provider.address),
@@ -1584,7 +1528,7 @@ class OneShot {
1584
1528
  return ethers_1.ethers.formatUnits(balance, decimals);
1585
1529
  }
1586
1530
  /**
1587
- * Create a compute goal — the orchestrator will plan, execute, and iterate autonomously.
1531
+ * Create a compute goal - the orchestrator will plan, execute, and iterate autonomously.
1588
1532
  *
1589
1533
  * Uses the quote-then-pay flow: first call gets a 402 with a budget estimate,
1590
1534
  * second call (with payment) creates the goal.
@@ -1623,7 +1567,9 @@ class OneShot {
1623
1567
  payload.approvals = options.approvals;
1624
1568
  if (options.done_check)
1625
1569
  payload.done_check = options.done_check;
1626
- // Compute returns the goal directly on 202 — no job polling, unlike the
1570
+ if (options.egress_allowlist)
1571
+ payload.egress_allowlist = options.egress_allowlist;
1572
+ // Compute returns the goal directly on 202 - no job polling, unlike the
1627
1573
  // other paid tools.
1628
1574
  const { execResp: createResp } = await this.runQuoteToPay({
1629
1575
  endpoint: '/v1/compute',
@@ -1681,7 +1627,7 @@ class OneShot {
1681
1627
  * with JobError (goal ID, end_reason code, snapshot in partialResult).
1682
1628
  * Budget exhaustion ends this wait; after fundComputeGoal revives the goal,
1683
1629
  * call waitForComputeGoal again with the same ID to observe the new run.
1684
- * Paused/awaiting_confirmation and recurring goals keep waiting until the
1630
+ * Paused/awaiting_confirmation/awaiting_connection and recurring goals keep waiting until the
1685
1631
  * goal ends or the timeout (seconds, default 120) expires with JobTimeoutError.
1686
1632
  * Infinity disables the local deadline.
1687
1633
  * onStatusUpdate receives each distinct observed status before settlement.
@@ -1907,6 +1853,30 @@ class OneShot {
1907
1853
  const json = await response.json();
1908
1854
  return json.data;
1909
1855
  }
1856
+ /**
1857
+ * Dispute a goal that ended `impossible_unverified`: the loop claimed it
1858
+ * cannot be done with no proof code could check. The dispute is recorded
1859
+ * on the goal and its receipt; nothing is refunded.
1860
+ *
1861
+ * @example
1862
+ * ```typescript
1863
+ * await agent.disputeComputeGoal(goalId, { reason: 'The data is public.' });
1864
+ * ```
1865
+ */
1866
+ async disputeComputeGoal(goalId, input = {}) {
1867
+ this.validate(goalId, 'goalId');
1868
+ const response = await this.fetchApi(`${this.baseUrl}/v1/compute/${goalId}/respond`, {
1869
+ method: 'POST',
1870
+ headers: { 'Content-Type': 'application/json', ...(await this.signedReadHeaders('write')) },
1871
+ body: JSON.stringify({ dispute: true, ...(input.reason ? { response: input.reason } : {}) }),
1872
+ });
1873
+ if (response.status === 404)
1874
+ throw new errors_2.ToolError('Goal not found', 404, await response.text(), response.headers.get('retry-after'));
1875
+ if (!response.ok)
1876
+ throw new errors_2.ToolError('Failed to dispute compute goal', response.status, await response.text(), response.headers.get('retry-after'));
1877
+ const json = await response.json();
1878
+ return json.data;
1879
+ }
1910
1880
  /**
1911
1881
  * Pause a recurring compute goal
1912
1882
  *
@@ -2106,7 +2076,7 @@ class OneShot {
2106
2076
  * List receipts with optional filtering
2107
2077
  *
2108
2078
  * Paginate with `limit`/`offset`, or page through an explicit time window with
2109
- * `since`/`until` (ISO string or Date) — useful for reaching receipts older than
2079
+ * `since`/`until` (ISO string or Date) - useful for reaching receipts older than
2110
2080
  * the server's 100-row cap when tagging value weeks after the originating call.
2111
2081
  *
2112
2082
  * @example
@@ -2144,8 +2114,8 @@ class OneShot {
2144
2114
  * Three ways to address the value:
2145
2115
  * - `rcpt_…` receipt id (from `result.receipt_id` or `receiptsList`), or
2146
2116
  * - the `request_id` returned by the originating tool call (`result.request_id`,
2147
- * resolved server-side via `Receipt.job_id`) — annotate without a list lookup, or
2148
- * - a `goalId` correlation key — attributes a whole *cadence* outcome (the email,
2117
+ * resolved server-side via `Receipt.job_id`) - annotate without a list lookup, or
2118
+ * - a `goalId` correlation key - attributes a whole *cadence* outcome (the email,
2149
2119
  * the follow-ups, the find/verify/enrich calls that share `decisionContext.goalId`)
2150
2120
  * in one call. The value is recorded once in the outcome ledger, so it can't
2151
2121
  * double-count across the cadence's receipts. Read it back with `rocsByGoal()`.
@@ -2159,13 +2129,12 @@ class OneShot {
2159
2129
  * const r = await agent.personEmailStatus({ email: 'x@y.com' });
2160
2130
  * await agent.tagReceiptValue({ requestId: r.request_id }, { type: 'lead', amount: 1 });
2161
2131
  *
2162
- * // By cadence correlation key — one call attributes a closed deal to the whole sequence:
2132
+ * // By cadence correlation key - one call attributes a closed deal to the whole sequence:
2163
2133
  * await agent.tagReceiptValue({ goalId: 'goal_q2_acme' }, { type: 'revenue', amount: 5000, label: 'Closed deal' });
2164
2134
  * ```
2165
2135
  */
2166
2136
  async tagReceiptValue(ref, valueTag) {
2167
2137
  this.validate(valueTag.type, 'valueTag.type');
2168
- // Cadence-level: route to the outcome ledger by correlation key.
2169
2138
  if (typeof ref === 'object' && ref.goalId && !ref.receiptId && !ref.requestId) {
2170
2139
  const response = await this.fetchApi(`${this.baseUrl}/v1/analytics/outcomes`, {
2171
2140
  method: 'POST',
@@ -2192,7 +2161,7 @@ class OneShot {
2192
2161
  }
2193
2162
  }
2194
2163
  /**
2195
- * Per-cadence RoCS rollup — spend (from receipts) vs. value (from outcomes)
2164
+ * Per-cadence RoCS rollup - spend (from receipts) vs. value (from outcomes)
2196
2165
  * grouped by `decisionContext.goalId`. Answers "what did this cadence cost vs.
2197
2166
  * earn" in one query. `value` counts judge-confirmed outcomes; `pending_value`
2198
2167
  * surfaces self-reported outcomes not yet confirmed.
@@ -2273,15 +2242,8 @@ class OneShot {
2273
2242
  jsonHeaders() {
2274
2243
  return { 'Content-Type': 'application/json', ...this.headers() };
2275
2244
  }
2276
- /**
2277
- * Auth headers plus a signed EIP-712 proof (`x-agent-proof`) for the read
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
- */
2245
+ /** Signed, single-use proofs bind requests to the wallet; an address alone cannot authenticate reads.
2246
+ * Signing failure falls back to plain headers with a one-time warning. */
2285
2247
  async signedReadHeaders(scope = 'read', domainName = 'OneShot Agent Auth') {
2286
2248
  const base = this.headers();
2287
2249
  // The bearer token already binds the request to the agent (and is
@@ -2293,12 +2255,7 @@ class OneShot {
2293
2255
  const issuedAt = Math.floor(Date.now() / 1000) + (domainName === 'OneShot Agent Auth' ? this.proofClockOffsetSec : 0);
2294
2256
  const nonce = ethers_1.ethers.hexlify(ethers_1.ethers.randomBytes(32));
2295
2257
  const signature = await this.provider.signTypedData(
2296
- // `domainName` lets a different origin (e.g. Soul Hunt, which keeps
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).
2258
+ // Separate EIP-712 domains prevent proof replay across services with independent nonce stores.
2302
2259
  { name: domainName, version: '1' }, {
2303
2260
  AgentReadAuth: [
2304
2261
  { name: 'agent', type: 'address' },
@@ -2312,9 +2269,7 @@ class OneShot {
2312
2269
  return { ...base, 'x-agent-proof': proof };
2313
2270
  }
2314
2271
  catch (err) {
2315
- // Once per client, even without debug: when the API enforces proofs,
2316
- // these requests will be rejected, and a silent fallback would leave
2317
- // the caller with a 401 and no idea why.
2272
+ // Warn once even without debug so a failed proof explains the ensuing authentication failure.
2318
2273
  if (!this.readProofWarned) {
2319
2274
  this.readProofWarned = true;
2320
2275
  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 +2280,13 @@ class OneShot {
2325
2280
  return base;
2326
2281
  }
2327
2282
  }
2328
- /** Retry a refused proof once, with a fresh proof, before the API has dispatched the request.
2329
- * Two refusals qualify. A clock-window refusal: the API's `server_time` corrects this client's
2330
- * proof clock (payments and other services keep their own clocks). A read whose nonce the API
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
- */
2283
+ /** Retry a clock-window refusal once using the API's server_time, or a spent nonce on GET/HEAD.
2284
+ * Never retry a spent nonce on a write: the first delivery may already have executed it.
2285
+ * Callers supply replayable JSON bodies and retain their original abort/deadline signal. */
2339
2286
  async fetchApi(url, init) {
2340
2287
  const proof = new Headers(init.headers).get('x-agent-proof');
2341
- // A proof is single-use, so the bytes that carry it must reach the API once. A runtime's
2342
- // fetch pools keep-alive connections and re-sends a request, byte for byte, when the pooled
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.
2288
+ // Disable connection reuse where supported to prevent transport retries from replaying a single-use proof.
2289
+ // Browsers control their own pooling; only replayed reads can be safely retried there.
2356
2290
  const deliver = (headers) => fetch(url, proof ? { ...init, headers: OneShot.singleDelivery(headers), keepalive: false } : init);
2357
2291
  const response = await deliver(init.headers);
2358
2292
  if (response.status !== 401 || !proof || this._accessToken)
@@ -2393,7 +2327,7 @@ class OneShot {
2393
2327
  init.signal?.throwIfAborted();
2394
2328
  if (!refreshed['x-agent-proof'])
2395
2329
  return response;
2396
- const retryHeaders = new Headers(init.headers); // The first send keeps its own headers; the caller's object is not touched.
2330
+ const retryHeaders = new Headers(init.headers); // Keep the caller's headers intact.
2397
2331
  retryHeaders.set('x-agent-proof', refreshed['x-agent-proof']);
2398
2332
  return deliver(retryHeaders);
2399
2333
  }
@@ -2407,8 +2341,7 @@ class OneShot {
2407
2341
  }
2408
2342
  return { ...Object.fromEntries(Object.entries(headers ?? {}).filter(([name]) => name.toLowerCase() !== 'connection')), Connection: 'close' };
2409
2343
  }
2410
- /** Build a query string, skipping null/undefined values. Pass falsy-but-valid
2411
- * values (0, '') as undefined at the call site to match prior `if (x)` guards. */
2344
+ /** Skip null/undefined query values; callers must explicitly omit other unwanted falsy values. */
2412
2345
  buildQuery(params) {
2413
2346
  const qs = new URLSearchParams();
2414
2347
  for (const [k, v] of Object.entries(params)) {
@@ -2417,20 +2350,8 @@ class OneShot {
2417
2350
  }
2418
2351
  return qs.toString();
2419
2352
  }
2420
- /**
2421
- * Push `config.budgets` to the server once, before the first paid call.
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
- */
2353
+ /** Sync budgets lazily because signing is async; concurrent calls share the cached promise.
2354
+ * Fail closed until the server confirms the budget, and clear failed syncs so the next call retries. */
2434
2355
  async ensureBudgetsSynced() {
2435
2356
  if (!this._budgets && !this._alertEmail)
2436
2357
  return;
@@ -2470,19 +2391,12 @@ class OneShot {
2470
2391
  await attempt;
2471
2392
  }
2472
2393
  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
2394
  this._budgetSync = undefined;
2476
2395
  this.log(`${err.message} — paid call refused until the budget is confirmed`);
2477
2396
  throw err;
2478
2397
  }
2479
2398
  }
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
- */
2399
+ /** Fail oversized calls locally before a round-trip; the server also enforces shared daily spend. */
2486
2400
  assertWithinBudget(total) {
2487
2401
  const cap = this._budgets?.perTransaction;
2488
2402
  if (!cap || cap <= 0)
@@ -2552,7 +2466,7 @@ class OneShot {
2552
2466
  const body = await response.json();
2553
2467
  return body.data;
2554
2468
  }
2555
- /** List this agent's access tokens (prefix, label, last use, spend today — never the token). */
2469
+ /** List this agent's access tokens (prefix, label, last use, spend today - never the token). */
2556
2470
  async listAccessTokens(opts = {}) {
2557
2471
  this.assertWalletSession('list access tokens');
2558
2472
  const response = await this.fetchApi(`${this.baseUrl}/v1/agents/me/access-tokens${opts.activeOnly ? '?active=true' : ''}`, {
@@ -2582,41 +2496,10 @@ class OneShot {
2582
2496
  throw new errors_2.OneShotError(`Quote $${total} exceeds maxCost $${maxCost}`);
2583
2497
  }
2584
2498
  }
2585
- /**
2586
- * Header that asks the API to reject the request when the computed quote
2587
- * exceeds the caller-supplied cap (commit 8f328a7). The SDK still does its
2588
- * own local `quote.total > maxCost` check after the 402 returns — this is
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
- */
2499
+ /** Ask the API to enforce the caller's cap before payment, in addition to the local quote check. */
2500
+ /** Resolve decimal USDC from the authoritative PAYMENT-REQUIRED header (atomic units);
2501
+ * fall back to the legacy body only when the header is missing or unparseable. */
2502
+ /** A 402 on the paid retry is a payment rejection, not a quote; expose its cause as PaymentError. */
2620
2503
  async failFromResponse(message, response) {
2621
2504
  const text = await response.text();
2622
2505
  const credits = response.status === 402 ? this.parseInsufficientCredits(text) : undefined;
@@ -2650,14 +2533,7 @@ class OneShot {
2650
2533
  const num = (v) => (typeof v === 'string' || typeof v === 'number') && Number.isFinite(parseFloat(String(v))) ? parseFloat(String(v)) : undefined;
2651
2534
  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
2535
  }
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
- */
2536
+ /** Token sessions never sign: retry the quote against credits and let the server ledger decide sufficiency. */
2661
2537
  async retryAsCreditsSession(body, endpoint, payload, quoteId, signal, timeoutMs, extraHeaders) {
2662
2538
  const credits = body ? this.parseInsufficientCredits(body) : undefined;
2663
2539
  if (credits)
@@ -2766,10 +2642,7 @@ class OneShot {
2766
2642
  return bodyAmount ?? '0';
2767
2643
  }
2768
2644
  const fromHeader = ethers_1.ethers.formatUnits(accepted.amount, 6);
2769
- // When the two agree (every fixed-price route), keep the body's string
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.
2645
+ // Preserve the body's amount formatting when it agrees with the authoritative header.
2773
2646
  if (bodyAmount != null && parseFloat(bodyAmount) === parseFloat(fromHeader)) {
2774
2647
  return bodyAmount;
2775
2648
  }
@@ -2780,11 +2653,7 @@ class OneShot {
2780
2653
  return undefined;
2781
2654
  return { 'X-Max-Cost-USDC': maxCost.toString() };
2782
2655
  }
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
- */
2656
+ /** Send the idempotency key on both legs so retries cannot double-charge or repeat the action. */
2788
2657
  idempotencyHeader(idempotencyKey) {
2789
2658
  if (!idempotencyKey)
2790
2659
  return undefined;
@@ -2835,7 +2704,7 @@ class OneShot {
2835
2704
  throw new errors_2.OneShotError('Operation cancelled');
2836
2705
  if (key)
2837
2706
  options.onRequestCreated?.({ idempotencyKey: key });
2838
- return await (0, deadline_1.abortable)(this.executeToolRequestImpl(endpoint, { ...options, idempotencyKey: key, signal: scope.signal }, quoteId, context), scope.signal);
2707
+ 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
2708
  }
2840
2709
  catch (error) {
2841
2710
  if (scope.timedOut())
@@ -2858,7 +2727,7 @@ class OneShot {
2858
2727
  Object.assign(extraHeaders, this.approvalHeader(approvalId));
2859
2728
  if (payload.memo !== undefined) {
2860
2729
  if (typeof payload.memo !== 'string' || payload.memo.trim().length === 0) {
2861
- delete payload.memo; // Drop invalid memo silently
2730
+ delete payload.memo; // Invalid optional metadata must not block the call.
2862
2731
  }
2863
2732
  else if (payload.memo.length > 1000) {
2864
2733
  payload.memo = payload.memo.slice(0, 1000);
@@ -2882,8 +2751,7 @@ class OneShot {
2882
2751
  if (signal?.aborted) {
2883
2752
  throw new errors_2.OneShotError('Operation cancelled');
2884
2753
  }
2885
- // One-time push of config.budgets before the first paid call, so the
2886
- // server-side gate knows about them on this very request.
2754
+ // The server must know the configured budget before the first paid request.
2887
2755
  await this.ensureBudgetsSynced();
2888
2756
  if (signal?.aborted)
2889
2757
  throw new errors_2.OneShotError('Operation cancelled');
@@ -2893,7 +2761,6 @@ class OneShot {
2893
2761
  throw new errors_2.OneShotError('Operation cancelled');
2894
2762
  context.phase = 'submission';
2895
2763
  let response = await this.makeRequest(endpoint, payload, undefined, quoteId, signal, undefined, extraHeaders);
2896
- // Handle 402 Payment Required
2897
2764
  if (response.status === 402 && /\/v1\/tools\/(solve|predict|find\/person|lookup\/person-(profile|dossier))$/.test(endpoint)) {
2898
2765
  ({ execResp: response } = await this.runQuoteToPay({
2899
2766
  endpoint, payload, signal, maxCost, quoteResponse: response, extraHeaders,
@@ -2903,10 +2770,8 @@ class OneShot {
2903
2770
  context.phase = 'submission';
2904
2771
  }
2905
2772
  else if (response.status === 402) {
2906
- // Parse x402 v2 PaymentRequired from PAYMENT-REQUIRED header
2907
2773
  const paymentRequiredHeader = response.headers.get('payment-required');
2908
2774
  const { accepted, resource, extensions } = this.parsePaymentRequired(paymentRequiredHeader);
2909
- // Fallback: parse legacy body format for amount display
2910
2775
  const data = await response.json();
2911
2776
  quoteId ?? (quoteId = data.context?.quote_id);
2912
2777
  if (this._accessToken) {
@@ -2946,7 +2811,6 @@ class OneShot {
2946
2811
  await this.failFromResponse('Tool request failed', response);
2947
2812
  }
2948
2813
  const result = await response.json();
2949
- // Handle async jobs
2950
2814
  if ((result.status === 'pending' || result.status === 'processing') && result.request_id) {
2951
2815
  context.requestId = String(result.request_id);
2952
2816
  context.receiptId = typeof result.receipt_id === 'string' ? result.receipt_id : undefined;
@@ -2956,7 +2820,7 @@ class OneShot {
2956
2820
  if (!wait) {
2957
2821
  return { ...result, idempotencyKey };
2958
2822
  }
2959
- const completed = await this.pollJob(result.request_id, timeout, signal, onStatusUpdate, waitForPhones ? { waitForPhones, phoneTimeoutSec } : undefined);
2823
+ const completed = await this.pollJob(result.request_id, timeout, signal, onStatusUpdate, waitForPhones ? { waitForPhones, phoneTimeoutSec } : undefined, endpoint);
2960
2824
  if (completed && typeof completed === 'object' && !Array.isArray(completed)) {
2961
2825
  return { ...completed, request_id: context.requestId, receipt_id: completed.receipt_id ?? context.receiptId, idempotencyKey };
2962
2826
  }
@@ -2964,13 +2828,7 @@ class OneShot {
2964
2828
  }
2965
2829
  return (result.data ?? result);
2966
2830
  }
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
- */
2831
+ /** Shared quote-to-pay flow; callers own post-202 handling because some tools poll and others return directly. */
2974
2832
  async runQuoteToPay(cfg) {
2975
2833
  await this.ensureBudgetsSynced();
2976
2834
  const approval = { ...cfg.extraHeaders, ...this.approvalHeader(cfg.approvalId) };
@@ -2979,8 +2837,6 @@ class OneShot {
2979
2837
  await cfg.on400(quoteResp);
2980
2838
  }
2981
2839
  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
2840
  await this.failFromResponse(cfg.expectMsg, quoteResp);
2985
2841
  }
2986
2842
  const quoteData = await quoteResp.json();
@@ -3029,14 +2885,20 @@ class OneShot {
3029
2885
  this.validate(requestId, 'requestId');
3030
2886
  return this.pollJob(requestId, options.timeout, options.signal, options.onStatusUpdate, options.waitForPhones ? { waitForPhones: true, phoneTimeoutSec: options.phoneTimeoutSec } : undefined);
3031
2887
  }
3032
- async pollJob(requestId, timeoutSec, signal, onStatusUpdate, phoneOpts) {
3033
- // HTTP polling is the source of truth; the WebSocket is an accelerator.
3034
- // Both run concurrently from the start and the first terminal outcome wins,
3035
- // so a broken push channel (Redis down, worker unconfigured, proxy that
3036
- // swallows frames) costs nothing beyond the poll cadence. Previously the
3037
- // client waited 60% of its timeout on the socket before polling at all —
3038
- // in prod that was ~72s of dead air on every call while the push never came.
3039
- // An already-aborted signal ends the wait before any proof is signed or sent.
2888
+ unwrapJobResult(job) {
2889
+ const result = job.result ?? job;
2890
+ if (result && typeof result === 'object' && !Array.isArray(result)) {
2891
+ const value = { ...result };
2892
+ for (const field of ['cached', 'fetched_at']) {
2893
+ if (field in job)
2894
+ value[field] = job[field];
2895
+ }
2896
+ return value;
2897
+ }
2898
+ return result;
2899
+ }
2900
+ async pollJob(requestId, timeoutSec, signal, onStatusUpdate, phoneOpts, tool = '') {
2901
+ // Race HTTP with WebSocket push so a broken push channel cannot stall job completion.
3040
2902
  if (signal?.aborted)
3041
2903
  throw new errors_2.OneShotError('Operation cancelled');
3042
2904
  const inner = new AbortController();
@@ -3088,31 +2950,15 @@ class OneShot {
3088
2950
  signal?.removeEventListener('abort', onOuterAbort);
3089
2951
  }
3090
2952
  this.log(`Job ${requestId} ready after ${Date.now() - startedAt}ms via ${wait.via ?? 'unknown'}`);
3091
- // Optional second phase: keep polling for the async phone-reveal webhook.
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).
2953
+ // Phone reveals are opt-in; retain the completed snapshot if every refresh fails.
3101
2954
  if (phoneOpts?.waitForPhones && this._isPhonesPending(result)) {
3102
2955
  result = await this._pollForPhones(requestId, phoneOpts.phoneTimeoutSec ?? 360, signal, result);
3103
2956
  }
3104
2957
  if (phoneOpts?.waitForPhones)
3105
2958
  notify('completed');
3106
- return result;
2959
+ return (0, untrusted_content_1.markUntrusted)(result, tool || wait.resultTool || '');
3107
2960
  }
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
- */
2961
+ /** Detect pending phone reveals in search rows, profiles, dossier sections and legacy results. */
3116
2962
  _isPhonesPending(result) {
3117
2963
  if (!result || typeof result !== 'object')
3118
2964
  return false;
@@ -3128,19 +2974,10 @@ class OneShot {
3128
2974
  || (Array.isArray(body.results) && body.results.some(pending));
3129
2975
  });
3130
2976
  }
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
- */
2977
+ /** Poll phone reveals every 5s to avoid wasting calls during slow callbacks; return the latest snapshot on timeout. */
3139
2978
  async _pollForPhones(requestId, timeoutSec, signal, initialResult) {
3140
2979
  const deadline = Date.now() + timeoutSec * 1000;
3141
2980
  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
2981
  let lastResult = initialResult;
3145
2982
  while (Date.now() < deadline) {
3146
2983
  if (signal?.aborted)
@@ -3151,14 +2988,13 @@ class OneShot {
3151
2988
  signal,
3152
2989
  });
3153
2990
  if (!resp.ok) {
3154
- // Soft-fail on transient errors during the phone wait — return
3155
- // whatever we last had so consumers don't lose the sync result.
2991
+ // Preserve the last successful snapshot across transient phone-wait errors.
3156
2992
  if (lastResult !== undefined)
3157
2993
  return lastResult;
3158
2994
  throw new errors_2.ToolError('Failed to check job status', resp.status, await resp.text(), resp.headers.get('retry-after'));
3159
2995
  }
3160
2996
  const job = await resp.json();
3161
- lastResult = (job.result ?? job);
2997
+ lastResult = this.unwrapJobResult(job);
3162
2998
  if (!this._isPhonesPending(lastResult)) {
3163
2999
  return lastResult;
3164
3000
  }
@@ -3174,38 +3010,17 @@ class OneShot {
3174
3010
  }
3175
3011
  await this.sleep(interval, signal);
3176
3012
  }
3177
- // Timeout — return the last snapshot. phones_pending will still be true
3178
- // so the consumer knows phones never arrived.
3179
3013
  return lastResult;
3180
3014
  }
3181
- /**
3182
- * WebSocket branch of a job wait. Resolves on a `completed` push for this
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
- */
3015
+ /** WebSocket transport failures leave HTTP polling in charge; job outcomes and cancellation settle the wait.
3016
+ * The HTTP branch owns the deadline and aborts this branch when it settles. */
3189
3017
  async waitViaWebSocket(requestId, signal, emit, wait) {
3190
- // Access-token sessions have no wallet signer to produce a signed proof,
3191
- // and the standard WebSocket constructor (browser and Node globals alike)
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.
3018
+ // WebSocket cannot carry an Authorization header; keep reusable bearer tokens out of logged URLs.
3019
+ // Token sessions use authenticated HTTP polling instead.
3199
3020
  if (this.isAccessTokenSession) {
3200
3021
  throw new Error('WebSocket push is not available for access-token sessions; falling back to HTTP polling');
3201
3022
  }
3202
- // Build the signed, single-use read proof BEFORE opening the socket: the
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.
3023
+ // Authenticate the upgrade with a short-lived, single-use proof; a wallet address alone proves nothing.
3209
3024
  const authHeaders = await this.signedReadHeaders();
3210
3025
  const authParams = new URLSearchParams();
3211
3026
  if (authHeaders['x-agent-proof'])
@@ -3267,7 +3082,8 @@ class OneShot {
3267
3082
  settle(() => {
3268
3083
  cleanup();
3269
3084
  wait.via = 'ws';
3270
- const result = (msg.result ?? msg);
3085
+ wait.resultTool = typeof msg.result_tool === 'string' ? msg.result_tool : undefined;
3086
+ const result = this.unwrapJobResult(msg);
3271
3087
  if (msg.request_id && typeof result === 'object' && result !== null && !('request_id' in result)) {
3272
3088
  result.request_id = msg.request_id;
3273
3089
  }
@@ -3293,7 +3109,6 @@ class OneShot {
3293
3109
  });
3294
3110
  };
3295
3111
  ws.onclose = () => {
3296
- // Any close before we got a result means no push is coming.
3297
3112
  settle(() => {
3298
3113
  cleanup();
3299
3114
  reject(new Error('WebSocket closed before result'));
@@ -3341,7 +3156,7 @@ class OneShot {
3341
3156
  });
3342
3157
  if (!resp.ok) {
3343
3158
  const body = await resp.text();
3344
- // 5xx / 429 from the poll endpoint are transient — keep polling.
3159
+ // 5xx / 429 from the poll endpoint are transient - keep polling.
3345
3160
  // Any other non-2xx (401/403/404) is a real answer about this job.
3346
3161
  if (resp.status >= 500 || resp.status === 429) {
3347
3162
  throw new Error(`Poll returned ${resp.status}: ${body.slice(0, 200)}`);
@@ -3352,10 +3167,11 @@ class OneShot {
3352
3167
  emit?.(String(job.status));
3353
3168
  if (job.status === 'completed') {
3354
3169
  this.log('Job completed');
3355
- if (wait)
3170
+ if (wait) {
3356
3171
  wait.via = 'http';
3357
- const result = (job.result ?? job);
3358
- // Propagate request_id into the result so callers always have it
3172
+ wait.resultTool = typeof job.result_tool === 'string' ? job.result_tool : undefined;
3173
+ }
3174
+ const result = this.unwrapJobResult(job);
3359
3175
  if (job.request_id && typeof result === 'object' && result !== null && !('request_id' in result)) {
3360
3176
  result.request_id = job.request_id;
3361
3177
  }
@@ -3410,9 +3226,6 @@ class OneShot {
3410
3226
  }
3411
3227
  async makeRequest(endpoint, data, payment, quoteId, signal, timeoutMs, extraHeaders) {
3412
3228
  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
3229
  for (let attempt = 0; resp.status === 401; attempt++) {
3417
3230
  const refusal = await this.readInvalidProof(resp);
3418
3231
  if (!refusal)
@@ -3441,22 +3254,10 @@ class OneShot {
3441
3254
  }
3442
3255
  }
3443
3256
  async sendLeg(endpoint, data, payment, quoteId, signal, timeoutMs, extraHeaders) {
3444
- // A leg with no payment attached proves the wallet with a signed agent
3445
- // proof: the API spends an agent's credits, and shows them on a quote, only
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).
3257
+ // Credit spending requires wallet proof unless a real payment signature already proves ownership.
3258
+ // Zero-cost authorizations are unsigned stubs, so those still need a proof.
3455
3259
  const unsignedStub = payment?.payload?.signature === '0x';
3456
- // Identity Verification (#2405) answers a retry from the start its key already made, before payment, and
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.
3260
+ // Identity Verification requires wallet proof on the payment leg too, to recover keyed submissions.
3460
3261
  const provenPayment = /(?:^|\/)identity\/start$/.test(endpoint);
3461
3262
  const askedForProof = !!extraHeaders?.['x-agent-proof'];
3462
3263
  const proofScope = /(lookup\/person-(profile|email|email-status)|lookup\/place-permits|physical-mail\/send|solve|predict)$/.test(endpoint)
@@ -3503,14 +3304,8 @@ class OneShot {
3503
3304
  throw new errors_2.OneShotError('Operation cancelled before payment');
3504
3305
  }
3505
3306
  }
3506
- /**
3507
- * ETH mode only. Make sure the wallet's *effective* USDC (on-chain balance
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
- */
3307
+ /** Reserve effective USDC (on-chain minus unsettled payments), swapping a buffer if needed.
3308
+ * Serialize reservations and swaps to avoid overspending and nonce races; release failed payments. */
3514
3309
  async ensureUsdcBalance(paymentInfo) {
3515
3310
  if (this._currency !== 'ETH')
3516
3311
  return undefined;
@@ -3591,18 +3386,10 @@ class OneShot {
3591
3386
  if (reservation)
3592
3387
  this._usdcPending.delete(reservation.id);
3593
3388
  }
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
- */
3389
+ /** Buy a buffer, or only the shortfall when getBalance shows insufficient ETH for the buffer. */
3601
3390
  async sizeSwap(charge, effective, chainId) {
3602
3391
  const shortfall = charge - effective;
3603
- // Integer math on a 10^6 scale (multiplier ≤ 1000, so ≤ 10^9 — always a
3604
- // finite, exact integer): keeps the multiplier's precision, no float→BigInt
3605
- // on an unbounded value.
3392
+ // Bounded fixed-point math preserves precision without converting an unbounded float to BigInt.
3606
3393
  const mScaled = BigInt(Math.round(this._swapBufferMultiplier * SWAP_MULTIPLIER_SCALE));
3607
3394
  const target = (charge * mScaled + BigInt(SWAP_MULTIPLIER_SCALE) - 1n) / BigInt(SWAP_MULTIPLIER_SCALE);
3608
3395
  let amount = target > effective ? target - effective : shortfall;
@@ -3616,7 +3403,7 @@ class OneShot {
3616
3403
  }
3617
3404
  }
3618
3405
  catch {
3619
- // Quoting failed — let executeSwap quote again and report properly.
3406
+ // Quoting failed - let executeSwap quote again and report properly.
3620
3407
  }
3621
3408
  }
3622
3409
  return amount;
@@ -3627,7 +3414,7 @@ class OneShot {
3627
3414
  const quote = await getSwapQuote(this.rpcProvider, usdcAmount, chainId, this._slippage);
3628
3415
  return quote?.amountInMax;
3629
3416
  }
3630
- /** Perform the on-chain swap. */
3417
+ /** Swap on the configured chain using the wallet provider. */
3631
3418
  async executeUsdcSwap(usdcAmount, chainId) {
3632
3419
  const { executeSwap } = await Promise.resolve().then(() => __importStar(require('./swap')));
3633
3420
  return executeSwap(this.provider, this.rpcProvider, usdcAmount, chainId, this._slippage);
@@ -3660,7 +3447,6 @@ class OneShot {
3660
3447
  ? Buffer.from(header, 'base64').toString()
3661
3448
  : atob(header);
3662
3449
  const parsed = JSON.parse(decoded);
3663
- // x402 v2: { x402Version: 2, accepts: [...], resource: {...}, extensions: {...} }
3664
3450
  if (parsed.accepts?.length > 0) {
3665
3451
  return {
3666
3452
  accepted: parsed.accepts[0],
@@ -3673,7 +3459,6 @@ class OneShot {
3673
3459
  this.log('Failed to parse PAYMENT-REQUIRED header, using defaults');
3674
3460
  }
3675
3461
  }
3676
- // Fallback: construct from known production values
3677
3462
  return {
3678
3463
  accepted: {
3679
3464
  scheme: 'exact',
@@ -3696,14 +3481,13 @@ class OneShot {
3696
3481
  if (header) {
3697
3482
  return this.parsePaymentRequired(header);
3698
3483
  }
3699
- // Probe: send quote ID without payment to get x402 middleware's 402
3700
3484
  // Carries X-Approval-Id: a policy-held call's probe must reach the 402,
3701
3485
  // not be held again.
3702
3486
  const probeResp = await this.makeRequest(endpoint, payload, undefined, quoteId, signal, undefined, extraHeaders);
3703
3487
  return this.parsePaymentRequired(probeResp.headers.get('payment-required'));
3704
3488
  }
3705
3489
  async signPaymentAuthorization(paymentInfo, accepted, resource, extensions) {
3706
- // Credits may cover the full cost — send a zero-cost authorization so the server
3490
+ // Credits may cover the full cost - send a zero-cost authorization so the server
3707
3491
  // always receives a payment-signature header (avoids 402 from x402 SDK).
3708
3492
  if (parseFloat(paymentInfo.amount) === 0) {
3709
3493
  this.log('Credits cover full cost — sending zero-cost authorization');
@@ -3725,12 +3509,9 @@ class OneShot {
3725
3509
  },
3726
3510
  } };
3727
3511
  }
3728
- // Belt and braces: every paid path branches to retryAsCreditsSession
3729
- // before reaching here in access-token mode.
3730
3512
  if (this._accessToken) {
3731
3513
  throw new errors_1.InsufficientCreditsError(`Credits do not cover $${paymentInfo.amount}; top up credits or use a wallet session`, parseFloat(paymentInfo.amount));
3732
3514
  }
3733
- // If paying with ETH, make sure USDC covers the charge (swapping a buffer if not) and reserve it
3734
3515
  const reservation = await this.ensureUsdcBalance(paymentInfo);
3735
3516
  const now = Math.floor(Date.now() / 1000);
3736
3517
  const nonce = ethers_1.ethers.randomBytes(32);
@@ -3738,14 +3519,10 @@ class OneShot {
3738
3519
  const validAfter = now - 300; // Buffer for clock skew
3739
3520
  const validBefore = now + 3600;
3740
3521
  const nonceHex = ethers_1.ethers.hexlify(nonce);
3741
- // Use the EIP-712 domain from the server's payment requirements — name,
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.
3522
+ // Use the server's full EIP-712 domain, including chainId, so the signature matches its network.
3745
3523
  const domainName = accepted.extra?.name || 'USD Coin';
3746
3524
  const domainVersion = accepted.extra?.version || '2';
3747
3525
  const chainId = chainIdFromNetwork(accepted.network ?? paymentInfo.network) ?? CHAIN_ID;
3748
- // Sign EIP-3009 TransferWithAuthorization
3749
3526
  let signature;
3750
3527
  try {
3751
3528
  signature = await this.provider.signTypedData({
@@ -3775,7 +3552,6 @@ class OneShot {
3775
3552
  this.releaseUsdcReservation(reservation);
3776
3553
  throw err;
3777
3554
  }
3778
- // Return x402 PaymentPayload v2 format (including resource + extensions for Bazaar discovery)
3779
3555
  return { reservation, auth: {
3780
3556
  x402Version: 2,
3781
3557
  ...(resource ? { resource } : {}),