@oneshot-agent/sdk 0.38.1 → 0.42.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
@@ -39,6 +39,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.OneShot = exports.executeSwap = exports.getSwapQuote = exports.ReadOnlyWalletProvider = exports.CdpWalletProvider = exports.EthersWalletProvider = void 0;
40
40
  const physical_mail_1 = require("./physical-mail");
41
41
  __exportStar(require("./physical-mail"), exports);
42
+ __exportStar(require("./approvals"), exports);
42
43
  const deadline_1 = require("./deadline");
43
44
  const errors_1 = require("./errors");
44
45
  const read_only_1 = require("./providers/read-only");
@@ -55,6 +56,7 @@ var swap_1 = require("./swap");
55
56
  Object.defineProperty(exports, "getSwapQuote", { enumerable: true, get: function () { return swap_1.getSwapQuote; } });
56
57
  Object.defineProperty(exports, "executeSwap", { enumerable: true, get: function () { return swap_1.executeSwap; } });
57
58
  __exportStar(require("./errors"), exports);
59
+ const approvals_1 = require("./approvals");
58
60
  // Receipt verification (`canonicalizeReceipt`, `verifyReceipt`,
59
61
  // `receiptFromWire`, and their supporting types `SignableReceipt`,
60
62
  // `ReceiptSignatureFields`, `ReceiptJwk`, `ReceiptJwks`,
@@ -80,7 +82,7 @@ __exportStar(require("./errors"), exports);
80
82
  // not removing the field. Import `SignableReceipt` from the `./receipt`
81
83
  // subpath directly if you need it.
82
84
  // Keep in sync with package.json `version`. Guarded by version.test.ts.
83
- const SDK_VERSION = '0.38.1';
85
+ const SDK_VERSION = '0.42.0';
84
86
  /** HTTP poll cadence while push is unconfirmed: fast first checks, settling at 2s. */
85
87
  const HTTP_POLL_BACKOFF_MS = [300, 600, 1000, 2000];
86
88
  /** HTTP poll cadence once the WebSocket has delivered for this request. */
@@ -226,6 +228,11 @@ class OneShot {
226
228
  await this.failFromResponse('Physical mail request failed', response);
227
229
  return await response.json();
228
230
  }, input => this.executeToolRequest('/v1/tools/physical-mail/send', { ...input, wait: false }));
231
+ /** Ask a named human to decide, and read decisions. See ./approvals.ts. */
232
+ this.approvals = new approvals_1.Approvals((path, method, body, scope) => this.agentJson(path, method, body, scope));
233
+ /** Which paid calls run, are denied, or wait for approval. Wallet sessions write it. */
234
+ this.policy = new approvals_1.ActionPolicies((path, method, body, scope) => this.agentJson(path, method, body, scope));
235
+ this.readProofWarned = false;
229
236
  /** ETH mode: signed payments not yet observed as settled, keyed by reservation id. */
230
237
  this._usdcPending = new Map();
231
238
  this._usdcReservationSeq = 0;
@@ -392,6 +399,10 @@ class OneShot {
392
399
  // Only the send call carries the key — the quote call has no side effects.
393
400
  payload.idempotencyKey = options.idempotencyKey;
394
401
  }
402
+ if (options.approvalId) {
403
+ // Releases a send the action policy held (see ApprovalRequiredError).
404
+ payload.approvalId = options.approvalId;
405
+ }
395
406
  return this.executeToolRequest('/v1/tools/email/send', payload, quote.quote_id);
396
407
  }
397
408
  /** List the caller's domain pool with warmup and rotation metadata. */
@@ -581,6 +592,7 @@ class OneShot {
581
592
  payload,
582
593
  signal: options.signal,
583
594
  maxCost: options.maxCost,
595
+ approvalId: options.approvalId,
584
596
  quoteTimeoutMs: 120000,
585
597
  execTimeoutMs: 60000,
586
598
  expectMsg: 'Expected 402 for quote',
@@ -649,6 +661,7 @@ class OneShot {
649
661
  payload,
650
662
  signal: options.signal,
651
663
  maxCost: options.maxCost,
664
+ approvalId: options.approvalId,
652
665
  expectMsg: 'Expected 402 for quote',
653
666
  totalOf: (ctx) => ctx.total,
654
667
  onQuote: (ctx) => this.log(`Voice quote: $${ctx.total} for ${ctx.estimated_duration_minutes}min call`),
@@ -711,6 +724,7 @@ class OneShot {
711
724
  payload,
712
725
  signal: options.signal,
713
726
  maxCost: options.maxCost,
727
+ approvalId: options.approvalId,
714
728
  expectMsg: 'Expected 402 for quote',
715
729
  totalOf: (ctx) => ctx.total,
716
730
  onQuote: (ctx) => this.log(`SMS quote: $${ctx.total} for ${ctx.segment_count} segment(s) to ${recipientCount} recipient(s)`),
@@ -1133,6 +1147,105 @@ class OneShot {
1133
1147
  }
1134
1148
  return response.json();
1135
1149
  }
1150
+ // ---------------------------------------------------------------------------
1151
+ // Inbound voice — the agent's number answers its own calls
1152
+ // ---------------------------------------------------------------------------
1153
+ /**
1154
+ * Get this agent a voice number without placing a call. Returns the existing
1155
+ * active number for free; otherwise buys one and charges the one-time phone
1156
+ * registration fee from prepaid credits (see `topUpCredits`). Too little
1157
+ * credit throws a 402 and buys nothing. Then configure it with
1158
+ * `setInboundVoice`.
1159
+ *
1160
+ * @example
1161
+ * ```typescript
1162
+ * const number = await agent.provisionVoiceNumber();
1163
+ * await agent.setInboundVoice(number.id, { prompt: 'You answer for Acme Dental.' });
1164
+ * ```
1165
+ */
1166
+ async provisionVoiceNumber() {
1167
+ return this.signedJson('POST', '/v1/tools/voice/numbers', {}, 'write', 'Failed to provision a phone number');
1168
+ }
1169
+ /**
1170
+ * List this agent's phone numbers. A number comes from `provisionVoiceNumber`
1171
+ * or the agent's first outbound voice call.
1172
+ *
1173
+ * @example
1174
+ * ```typescript
1175
+ * const { numbers } = await agent.voiceNumbers();
1176
+ * ```
1177
+ */
1178
+ async voiceNumbers() {
1179
+ return this.signedJson('GET', '/v1/tools/voice/numbers', undefined, 'read', 'Failed to list phone numbers');
1180
+ }
1181
+ /**
1182
+ * Answer calls to one of the agent's numbers. Replaces the whole config.
1183
+ * Inbound calls are paid from prepaid credits (see `topUpCredits`), per
1184
+ * started minute; a call is declined when credits cannot cover the minimum
1185
+ * fee. Keep the returned `webhook_secret` if you set `webhook_url`.
1186
+ *
1187
+ * @example
1188
+ * ```typescript
1189
+ * await agent.setInboundVoice(numberId, {
1190
+ * prompt: 'You answer for Acme Dental. Book cleanings; take a message for anything else.',
1191
+ * business_hours: { timezone: 'America/New_York', windows: [{ days: ['mon','tue','wed','thu','fri'], start: '09:00', end: '17:00' }] },
1192
+ * after_hours: 'take_message',
1193
+ * });
1194
+ * ```
1195
+ */
1196
+ async setInboundVoice(phoneNumberId, config) {
1197
+ this.validate(phoneNumberId, 'phoneNumberId');
1198
+ return this.signedJson('PUT', `/v1/tools/voice/numbers/${encodeURIComponent(phoneNumberId)}/inbound`, config, 'write', 'Failed to set inbound voice config');
1199
+ }
1200
+ /** Current inbound config for a number (throws 404 when none is set). */
1201
+ async getInboundVoice(phoneNumberId) {
1202
+ this.validate(phoneNumberId, 'phoneNumberId');
1203
+ return this.signedJson('GET', `/v1/tools/voice/numbers/${encodeURIComponent(phoneNumberId)}/inbound`, undefined, 'read', 'Failed to get inbound voice config');
1204
+ }
1205
+ /** Stop answering calls to a number. The settings are kept, disabled. */
1206
+ async disableInboundVoice(phoneNumberId) {
1207
+ this.validate(phoneNumberId, 'phoneNumberId');
1208
+ return this.signedJson('DELETE', `/v1/tools/voice/numbers/${encodeURIComponent(phoneNumberId)}/inbound`, undefined, 'write', 'Failed to disable inbound voice');
1209
+ }
1210
+ /**
1211
+ * List inbound calls, newest first.
1212
+ *
1213
+ * @example
1214
+ * ```typescript
1215
+ * const { calls } = await agent.inboundCalls({ limit: 20 });
1216
+ * for (const c of calls) console.log(c.from, c.summary);
1217
+ * ```
1218
+ */
1219
+ async inboundCalls(options = {}) {
1220
+ const qs = this.buildQuery({
1221
+ limit: options.limit || undefined,
1222
+ before: options.before || undefined,
1223
+ phone_number_id: options.phone_number_id || undefined,
1224
+ include_transcript: options.include_transcript ? 'true' : undefined,
1225
+ });
1226
+ return this.signedJson('GET', `/v1/tools/voice/inbound${qs ? `?${qs}` : ''}`, undefined, 'read', 'Failed to list inbound calls');
1227
+ }
1228
+ /** One inbound call, with transcript. */
1229
+ async inboundCall(callId) {
1230
+ this.validate(callId, 'callId');
1231
+ return this.signedJson('GET', `/v1/tools/voice/inbound/${encodeURIComponent(callId)}`, undefined, 'read', 'Failed to get inbound call');
1232
+ }
1233
+ /** Signed free request returning the `data` field of `{ success, data }`. */
1234
+ async signedJson(method, path, body, scope, failure) {
1235
+ const response = await fetch(`${this.baseUrl}${path}`, {
1236
+ method,
1237
+ headers: {
1238
+ ...(body !== undefined ? { 'Content-Type': 'application/json' } : {}),
1239
+ ...(await this.signedReadHeaders(scope)),
1240
+ },
1241
+ ...(body !== undefined ? { body: JSON.stringify(body) } : {}),
1242
+ });
1243
+ if (!response.ok) {
1244
+ throw new errors_2.ToolError(failure, response.status, await response.text());
1245
+ }
1246
+ const json = await response.json();
1247
+ return (json.data ?? json);
1248
+ }
1136
1249
  /**
1137
1250
  * Get a specific inbound SMS message
1138
1251
  *
@@ -1192,7 +1305,7 @@ class OneShot {
1192
1305
  this.validate(notificationId, 'notificationId');
1193
1306
  const response = await fetch(`${this.baseUrl}/v1/tools/notifications/${notificationId}/read`, {
1194
1307
  method: 'PATCH',
1195
- headers: await this.signedReadHeaders()
1308
+ headers: await this.signedReadHeaders('write')
1196
1309
  });
1197
1310
  if (response.status === 404) {
1198
1311
  throw new errors_2.ToolError('Notification not found', 404, 'Notification not found');
@@ -1781,7 +1894,7 @@ class OneShot {
1781
1894
  * read another agent's data by supplying its address. The proof binds this
1782
1895
  * request to the wallet the SDK controls; the server verifies the signature
1783
1896
  * locally. A fresh nonce per call prevents replay. Signing failure falls back
1784
- * to plain headers (the server runs log-only until enforcement is enabled).
1897
+ * to plain headers, with a one-time warning.
1785
1898
  */
1786
1899
  async signedReadHeaders(scope = 'read') {
1787
1900
  const base = this.headers();
@@ -1806,7 +1919,16 @@ class OneShot {
1806
1919
  return { ...base, 'x-agent-proof': proof };
1807
1920
  }
1808
1921
  catch (err) {
1809
- this.log(`Failed to sign read proof (continuing without): ${err}`);
1922
+ // Once per client, even without debug: when the API enforces proofs,
1923
+ // these requests will be rejected, and a silent fallback would leave
1924
+ // the caller with a 401 and no idea why.
1925
+ if (!this.readProofWarned) {
1926
+ this.readProofWarned = true;
1927
+ 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}`);
1928
+ }
1929
+ else {
1930
+ this.log(`Failed to sign read proof (continuing without): ${err}`);
1931
+ }
1810
1932
  return base;
1811
1933
  }
1812
1934
  }
@@ -1904,6 +2026,19 @@ class OneShot {
1904
2026
  * console.log(`${b.spent_today_usdc} of ${b.daily_usdc} spent, resets ${b.resets_at}`);
1905
2027
  * ```
1906
2028
  */
2029
+ /** Signed-proof JSON request to a free agent route; returns `data`. Errors map through failFromResponse. */
2030
+ async agentJson(path, method, body, scope = 'read') {
2031
+ const response = await fetch(`${this.baseUrl}${path}`, {
2032
+ method,
2033
+ headers: { 'Content-Type': 'application/json', ...(await this.signedReadHeaders(scope)) },
2034
+ ...(body === undefined || method === 'GET' ? {} : { body: JSON.stringify(body) }),
2035
+ signal: AbortSignal.timeout(30000),
2036
+ });
2037
+ if (!response.ok)
2038
+ await this.failFromResponse(`${method} ${path.split('?')[0]} failed`, response);
2039
+ const json = await response.json();
2040
+ return (json && typeof json === 'object' && 'data' in json ? json.data : json);
2041
+ }
1907
2042
  async budgets() {
1908
2043
  const response = await fetch(`${this.baseUrl}/v1/agents/me/budgets`, {
1909
2044
  headers: await this.signedReadHeaders(),
@@ -2018,6 +2153,9 @@ class OneShot {
2018
2153
  const rejection = [402, 500].includes(response.status) ? this.parsePaymentRejection(text) : undefined;
2019
2154
  if (rejection)
2020
2155
  throw rejection;
2156
+ const policy = response.status === 403 ? this.parsePolicyRejection(text) : undefined;
2157
+ if (policy)
2158
+ throw policy;
2021
2159
  const budget = response.status === 403 ? this.parseBudgetRejection(text) : undefined;
2022
2160
  if (budget)
2023
2161
  throw budget;
@@ -2098,6 +2236,23 @@ class OneShot {
2098
2236
  * "my own budget stopped this" separately from an auth failure or a payment
2099
2237
  * rejection. Any other 403 falls through to ToolError.
2100
2238
  */
2239
+ /** 403 from the action-policy gate: held for approval, or denied. */
2240
+ parsePolicyRejection(text) {
2241
+ let body;
2242
+ try {
2243
+ body = JSON.parse(text);
2244
+ }
2245
+ catch {
2246
+ return undefined;
2247
+ }
2248
+ if (body?.error === 'approval_required' && typeof body.approval_id === 'string') {
2249
+ return new errors_2.ApprovalRequiredError(body.message ?? 'This call needs a human approval', body.approval_id, body.approval, body.approval?.policy_rule ?? undefined);
2250
+ }
2251
+ if (body?.error === 'action_denied_by_policy') {
2252
+ return new errors_2.ActionDeniedError(body.message ?? 'Denied by the action policy', body.rule);
2253
+ }
2254
+ return undefined;
2255
+ }
2101
2256
  parseBudgetRejection(text) {
2102
2257
  try {
2103
2258
  const body = JSON.parse(text);
@@ -2163,6 +2318,12 @@ class OneShot {
2163
2318
  return undefined;
2164
2319
  return { 'Idempotency-Key': idempotencyKey };
2165
2320
  }
2321
+ /** X-Approval-Id, sent on both legs; the server reads it only on the paid leg. */
2322
+ approvalHeader(approvalId) {
2323
+ if (!approvalId)
2324
+ return undefined;
2325
+ return { 'X-Approval-Id': approvalId };
2326
+ }
2166
2327
  async readReliabilityJson(path, signed, allowDegraded = false) {
2167
2328
  const scope = (0, deadline_1.deadlineScope)(undefined, signed ? 10000 : 5000);
2168
2329
  try {
@@ -2216,11 +2377,13 @@ class OneShot {
2216
2377
  }
2217
2378
  }
2218
2379
  async executeToolRequestImpl(endpoint, options, quoteId, context = { phase: "initialization" }) {
2219
- const { totalTimeoutMs, onRequestCreated, onAccepted, signal, onStatusUpdate, wait = true, waitForPhones, phoneTimeoutSec, idempotencyKey, maxCost, ...payload } = options;
2380
+ const { totalTimeoutMs, onRequestCreated, onAccepted, signal, onStatusUpdate, wait = true, waitForPhones, phoneTimeoutSec, idempotencyKey, approvalId, maxCost, ...payload } = options;
2220
2381
  const extraHeaders = {
2221
2382
  ...this.maxCostHeader(maxCost),
2222
2383
  ...this.idempotencyHeader(idempotencyKey),
2223
2384
  };
2385
+ // Releases a call the action policy held (ToolOptions.approvalId).
2386
+ Object.assign(extraHeaders, this.approvalHeader(approvalId));
2224
2387
  if (payload.memo !== undefined) {
2225
2388
  if (typeof payload.memo !== 'string' || payload.memo.trim().length === 0) {
2226
2389
  delete payload.memo; // Drop invalid memo silently
@@ -2329,7 +2492,8 @@ class OneShot {
2329
2492
  */
2330
2493
  async runQuoteToPay(cfg) {
2331
2494
  await this.ensureBudgetsSynced();
2332
- const quoteResp = await this.makeRequest(cfg.endpoint, cfg.payload, undefined, undefined, cfg.signal, cfg.quoteTimeoutMs, this.maxCostHeader(cfg.maxCost));
2495
+ const approval = this.approvalHeader(cfg.approvalId);
2496
+ const quoteResp = await this.makeRequest(cfg.endpoint, cfg.payload, undefined, undefined, cfg.signal, cfg.quoteTimeoutMs, { ...this.maxCostHeader(cfg.maxCost), ...approval });
2333
2497
  if (quoteResp.status === 400 && cfg.on400) {
2334
2498
  await cfg.on400(quoteResp);
2335
2499
  }
@@ -2344,7 +2508,7 @@ class OneShot {
2344
2508
  this.assertWithinBudget(cfg.totalOf(quoteData.context));
2345
2509
  if (this._accessToken) {
2346
2510
  this.checkAbortBeforePayment(cfg.signal);
2347
- const execResp = await this.retryAsCreditsSession(quoteData, cfg.endpoint, cfg.payload, quoteData.context.quote_id, cfg.signal, cfg.execTimeoutMs);
2511
+ const execResp = await this.retryAsCreditsSession(quoteData, cfg.endpoint, cfg.payload, quoteData.context.quote_id, cfg.signal, cfg.execTimeoutMs, approval);
2348
2512
  return { context: quoteData.context, execResp };
2349
2513
  }
2350
2514
  const paymentInfo = {
@@ -2357,10 +2521,10 @@ class OneShot {
2357
2521
  token: { address: quoteData.payment_request.token_address, symbol: 'USDC', decimals: 6 }
2358
2522
  };
2359
2523
  this.checkAbortBeforePayment(cfg.signal);
2360
- const { accepted, resource, extensions } = await this.getAcceptedRequirements(quoteResp, cfg.endpoint, cfg.payload, quoteData.context.quote_id, cfg.signal);
2524
+ const { accepted, resource, extensions } = await this.getAcceptedRequirements(quoteResp, cfg.endpoint, cfg.payload, quoteData.context.quote_id, cfg.signal, approval);
2361
2525
  paymentInfo.amount = this.chargeAmount(accepted, quoteData.payment_request.amount);
2362
2526
  const signed = await this.signPaymentAuthorization(paymentInfo, accepted, resource, extensions);
2363
- const execResp = await this.makePaidRequest(signed, cfg.endpoint, cfg.payload, quoteData.context.quote_id, cfg.signal, cfg.execTimeoutMs);
2527
+ const execResp = await this.makePaidRequest(signed, cfg.endpoint, cfg.payload, quoteData.context.quote_id, cfg.signal, cfg.execTimeoutMs, approval);
2364
2528
  return { context: quoteData.context, execResp };
2365
2529
  }
2366
2530
  /**
@@ -2937,13 +3101,15 @@ class OneShot {
2937
3101
  * Quote-based routes don't include payment-required header on the initial 402.
2938
3102
  * If missing, probe with quote ID (no payment) to trigger the x402 middleware's 402.
2939
3103
  */
2940
- async getAcceptedRequirements(initialResp, endpoint, payload, quoteId, signal) {
3104
+ async getAcceptedRequirements(initialResp, endpoint, payload, quoteId, signal, extraHeaders) {
2941
3105
  const header = initialResp.headers.get('payment-required');
2942
3106
  if (header) {
2943
3107
  return this.parsePaymentRequired(header);
2944
3108
  }
2945
3109
  // Probe: send quote ID without payment to get x402 middleware's 402
2946
- const probeResp = await this.makeRequest(endpoint, payload, undefined, quoteId, signal);
3110
+ // Carries X-Approval-Id: a policy-held call's probe must reach the 402,
3111
+ // not be held again.
3112
+ const probeResp = await this.makeRequest(endpoint, payload, undefined, quoteId, signal, undefined, extraHeaders);
2947
3113
  return this.parsePaymentRequired(probeResp.headers.get('payment-required'));
2948
3114
  }
2949
3115
  async signPaymentAuthorization(paymentInfo, accepted, resource, extensions) {