@oneshot-agent/sdk 0.38.1 → 0.41.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.41.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,10 @@ 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));
229
235
  /** ETH mode: signed payments not yet observed as settled, keyed by reservation id. */
230
236
  this._usdcPending = new Map();
231
237
  this._usdcReservationSeq = 0;
@@ -392,6 +398,10 @@ class OneShot {
392
398
  // Only the send call carries the key — the quote call has no side effects.
393
399
  payload.idempotencyKey = options.idempotencyKey;
394
400
  }
401
+ if (options.approvalId) {
402
+ // Releases a send the action policy held (see ApprovalRequiredError).
403
+ payload.approvalId = options.approvalId;
404
+ }
395
405
  return this.executeToolRequest('/v1/tools/email/send', payload, quote.quote_id);
396
406
  }
397
407
  /** List the caller's domain pool with warmup and rotation metadata. */
@@ -581,6 +591,7 @@ class OneShot {
581
591
  payload,
582
592
  signal: options.signal,
583
593
  maxCost: options.maxCost,
594
+ approvalId: options.approvalId,
584
595
  quoteTimeoutMs: 120000,
585
596
  execTimeoutMs: 60000,
586
597
  expectMsg: 'Expected 402 for quote',
@@ -649,6 +660,7 @@ class OneShot {
649
660
  payload,
650
661
  signal: options.signal,
651
662
  maxCost: options.maxCost,
663
+ approvalId: options.approvalId,
652
664
  expectMsg: 'Expected 402 for quote',
653
665
  totalOf: (ctx) => ctx.total,
654
666
  onQuote: (ctx) => this.log(`Voice quote: $${ctx.total} for ${ctx.estimated_duration_minutes}min call`),
@@ -711,6 +723,7 @@ class OneShot {
711
723
  payload,
712
724
  signal: options.signal,
713
725
  maxCost: options.maxCost,
726
+ approvalId: options.approvalId,
714
727
  expectMsg: 'Expected 402 for quote',
715
728
  totalOf: (ctx) => ctx.total,
716
729
  onQuote: (ctx) => this.log(`SMS quote: $${ctx.total} for ${ctx.segment_count} segment(s) to ${recipientCount} recipient(s)`),
@@ -1133,6 +1146,105 @@ class OneShot {
1133
1146
  }
1134
1147
  return response.json();
1135
1148
  }
1149
+ // ---------------------------------------------------------------------------
1150
+ // Inbound voice — the agent's number answers its own calls
1151
+ // ---------------------------------------------------------------------------
1152
+ /**
1153
+ * Get this agent a voice number without placing a call. Returns the existing
1154
+ * active number for free; otherwise buys one and charges the one-time phone
1155
+ * registration fee from prepaid credits (see `topUpCredits`). Too little
1156
+ * credit throws a 402 and buys nothing. Then configure it with
1157
+ * `setInboundVoice`.
1158
+ *
1159
+ * @example
1160
+ * ```typescript
1161
+ * const number = await agent.provisionVoiceNumber();
1162
+ * await agent.setInboundVoice(number.id, { prompt: 'You answer for Acme Dental.' });
1163
+ * ```
1164
+ */
1165
+ async provisionVoiceNumber() {
1166
+ return this.signedJson('POST', '/v1/tools/voice/numbers', {}, 'write', 'Failed to provision a phone number');
1167
+ }
1168
+ /**
1169
+ * List this agent's phone numbers. A number comes from `provisionVoiceNumber`
1170
+ * or the agent's first outbound voice call.
1171
+ *
1172
+ * @example
1173
+ * ```typescript
1174
+ * const { numbers } = await agent.voiceNumbers();
1175
+ * ```
1176
+ */
1177
+ async voiceNumbers() {
1178
+ return this.signedJson('GET', '/v1/tools/voice/numbers', undefined, 'read', 'Failed to list phone numbers');
1179
+ }
1180
+ /**
1181
+ * Answer calls to one of the agent's numbers. Replaces the whole config.
1182
+ * Inbound calls are paid from prepaid credits (see `topUpCredits`), per
1183
+ * started minute; a call is declined when credits cannot cover the minimum
1184
+ * fee. Keep the returned `webhook_secret` if you set `webhook_url`.
1185
+ *
1186
+ * @example
1187
+ * ```typescript
1188
+ * await agent.setInboundVoice(numberId, {
1189
+ * prompt: 'You answer for Acme Dental. Book cleanings; take a message for anything else.',
1190
+ * business_hours: { timezone: 'America/New_York', windows: [{ days: ['mon','tue','wed','thu','fri'], start: '09:00', end: '17:00' }] },
1191
+ * after_hours: 'take_message',
1192
+ * });
1193
+ * ```
1194
+ */
1195
+ async setInboundVoice(phoneNumberId, config) {
1196
+ this.validate(phoneNumberId, 'phoneNumberId');
1197
+ return this.signedJson('PUT', `/v1/tools/voice/numbers/${encodeURIComponent(phoneNumberId)}/inbound`, config, 'write', 'Failed to set inbound voice config');
1198
+ }
1199
+ /** Current inbound config for a number (throws 404 when none is set). */
1200
+ async getInboundVoice(phoneNumberId) {
1201
+ this.validate(phoneNumberId, 'phoneNumberId');
1202
+ return this.signedJson('GET', `/v1/tools/voice/numbers/${encodeURIComponent(phoneNumberId)}/inbound`, undefined, 'read', 'Failed to get inbound voice config');
1203
+ }
1204
+ /** Stop answering calls to a number. The settings are kept, disabled. */
1205
+ async disableInboundVoice(phoneNumberId) {
1206
+ this.validate(phoneNumberId, 'phoneNumberId');
1207
+ return this.signedJson('DELETE', `/v1/tools/voice/numbers/${encodeURIComponent(phoneNumberId)}/inbound`, undefined, 'write', 'Failed to disable inbound voice');
1208
+ }
1209
+ /**
1210
+ * List inbound calls, newest first.
1211
+ *
1212
+ * @example
1213
+ * ```typescript
1214
+ * const { calls } = await agent.inboundCalls({ limit: 20 });
1215
+ * for (const c of calls) console.log(c.from, c.summary);
1216
+ * ```
1217
+ */
1218
+ async inboundCalls(options = {}) {
1219
+ const qs = this.buildQuery({
1220
+ limit: options.limit || undefined,
1221
+ before: options.before || undefined,
1222
+ phone_number_id: options.phone_number_id || undefined,
1223
+ include_transcript: options.include_transcript ? 'true' : undefined,
1224
+ });
1225
+ return this.signedJson('GET', `/v1/tools/voice/inbound${qs ? `?${qs}` : ''}`, undefined, 'read', 'Failed to list inbound calls');
1226
+ }
1227
+ /** One inbound call, with transcript. */
1228
+ async inboundCall(callId) {
1229
+ this.validate(callId, 'callId');
1230
+ return this.signedJson('GET', `/v1/tools/voice/inbound/${encodeURIComponent(callId)}`, undefined, 'read', 'Failed to get inbound call');
1231
+ }
1232
+ /** Signed free request returning the `data` field of `{ success, data }`. */
1233
+ async signedJson(method, path, body, scope, failure) {
1234
+ const response = await fetch(`${this.baseUrl}${path}`, {
1235
+ method,
1236
+ headers: {
1237
+ ...(body !== undefined ? { 'Content-Type': 'application/json' } : {}),
1238
+ ...(await this.signedReadHeaders(scope)),
1239
+ },
1240
+ ...(body !== undefined ? { body: JSON.stringify(body) } : {}),
1241
+ });
1242
+ if (!response.ok) {
1243
+ throw new errors_2.ToolError(failure, response.status, await response.text());
1244
+ }
1245
+ const json = await response.json();
1246
+ return (json.data ?? json);
1247
+ }
1136
1248
  /**
1137
1249
  * Get a specific inbound SMS message
1138
1250
  *
@@ -1904,6 +2016,19 @@ class OneShot {
1904
2016
  * console.log(`${b.spent_today_usdc} of ${b.daily_usdc} spent, resets ${b.resets_at}`);
1905
2017
  * ```
1906
2018
  */
2019
+ /** Signed-proof JSON request to a free agent route; returns `data`. Errors map through failFromResponse. */
2020
+ async agentJson(path, method, body, scope = 'read') {
2021
+ const response = await fetch(`${this.baseUrl}${path}`, {
2022
+ method,
2023
+ headers: { 'Content-Type': 'application/json', ...(await this.signedReadHeaders(scope)) },
2024
+ ...(body === undefined || method === 'GET' ? {} : { body: JSON.stringify(body) }),
2025
+ signal: AbortSignal.timeout(30000),
2026
+ });
2027
+ if (!response.ok)
2028
+ await this.failFromResponse(`${method} ${path.split('?')[0]} failed`, response);
2029
+ const json = await response.json();
2030
+ return (json && typeof json === 'object' && 'data' in json ? json.data : json);
2031
+ }
1907
2032
  async budgets() {
1908
2033
  const response = await fetch(`${this.baseUrl}/v1/agents/me/budgets`, {
1909
2034
  headers: await this.signedReadHeaders(),
@@ -2018,6 +2143,9 @@ class OneShot {
2018
2143
  const rejection = [402, 500].includes(response.status) ? this.parsePaymentRejection(text) : undefined;
2019
2144
  if (rejection)
2020
2145
  throw rejection;
2146
+ const policy = response.status === 403 ? this.parsePolicyRejection(text) : undefined;
2147
+ if (policy)
2148
+ throw policy;
2021
2149
  const budget = response.status === 403 ? this.parseBudgetRejection(text) : undefined;
2022
2150
  if (budget)
2023
2151
  throw budget;
@@ -2098,6 +2226,23 @@ class OneShot {
2098
2226
  * "my own budget stopped this" separately from an auth failure or a payment
2099
2227
  * rejection. Any other 403 falls through to ToolError.
2100
2228
  */
2229
+ /** 403 from the action-policy gate: held for approval, or denied. */
2230
+ parsePolicyRejection(text) {
2231
+ let body;
2232
+ try {
2233
+ body = JSON.parse(text);
2234
+ }
2235
+ catch {
2236
+ return undefined;
2237
+ }
2238
+ if (body?.error === 'approval_required' && typeof body.approval_id === 'string') {
2239
+ return new errors_2.ApprovalRequiredError(body.message ?? 'This call needs a human approval', body.approval_id, body.approval, body.approval?.policy_rule ?? undefined);
2240
+ }
2241
+ if (body?.error === 'action_denied_by_policy') {
2242
+ return new errors_2.ActionDeniedError(body.message ?? 'Denied by the action policy', body.rule);
2243
+ }
2244
+ return undefined;
2245
+ }
2101
2246
  parseBudgetRejection(text) {
2102
2247
  try {
2103
2248
  const body = JSON.parse(text);
@@ -2163,6 +2308,12 @@ class OneShot {
2163
2308
  return undefined;
2164
2309
  return { 'Idempotency-Key': idempotencyKey };
2165
2310
  }
2311
+ /** X-Approval-Id, sent on both legs; the server reads it only on the paid leg. */
2312
+ approvalHeader(approvalId) {
2313
+ if (!approvalId)
2314
+ return undefined;
2315
+ return { 'X-Approval-Id': approvalId };
2316
+ }
2166
2317
  async readReliabilityJson(path, signed, allowDegraded = false) {
2167
2318
  const scope = (0, deadline_1.deadlineScope)(undefined, signed ? 10000 : 5000);
2168
2319
  try {
@@ -2216,11 +2367,13 @@ class OneShot {
2216
2367
  }
2217
2368
  }
2218
2369
  async executeToolRequestImpl(endpoint, options, quoteId, context = { phase: "initialization" }) {
2219
- const { totalTimeoutMs, onRequestCreated, onAccepted, signal, onStatusUpdate, wait = true, waitForPhones, phoneTimeoutSec, idempotencyKey, maxCost, ...payload } = options;
2370
+ const { totalTimeoutMs, onRequestCreated, onAccepted, signal, onStatusUpdate, wait = true, waitForPhones, phoneTimeoutSec, idempotencyKey, approvalId, maxCost, ...payload } = options;
2220
2371
  const extraHeaders = {
2221
2372
  ...this.maxCostHeader(maxCost),
2222
2373
  ...this.idempotencyHeader(idempotencyKey),
2223
2374
  };
2375
+ // Releases a call the action policy held (ToolOptions.approvalId).
2376
+ Object.assign(extraHeaders, this.approvalHeader(approvalId));
2224
2377
  if (payload.memo !== undefined) {
2225
2378
  if (typeof payload.memo !== 'string' || payload.memo.trim().length === 0) {
2226
2379
  delete payload.memo; // Drop invalid memo silently
@@ -2329,7 +2482,8 @@ class OneShot {
2329
2482
  */
2330
2483
  async runQuoteToPay(cfg) {
2331
2484
  await this.ensureBudgetsSynced();
2332
- const quoteResp = await this.makeRequest(cfg.endpoint, cfg.payload, undefined, undefined, cfg.signal, cfg.quoteTimeoutMs, this.maxCostHeader(cfg.maxCost));
2485
+ const approval = this.approvalHeader(cfg.approvalId);
2486
+ const quoteResp = await this.makeRequest(cfg.endpoint, cfg.payload, undefined, undefined, cfg.signal, cfg.quoteTimeoutMs, { ...this.maxCostHeader(cfg.maxCost), ...approval });
2333
2487
  if (quoteResp.status === 400 && cfg.on400) {
2334
2488
  await cfg.on400(quoteResp);
2335
2489
  }
@@ -2344,7 +2498,7 @@ class OneShot {
2344
2498
  this.assertWithinBudget(cfg.totalOf(quoteData.context));
2345
2499
  if (this._accessToken) {
2346
2500
  this.checkAbortBeforePayment(cfg.signal);
2347
- const execResp = await this.retryAsCreditsSession(quoteData, cfg.endpoint, cfg.payload, quoteData.context.quote_id, cfg.signal, cfg.execTimeoutMs);
2501
+ const execResp = await this.retryAsCreditsSession(quoteData, cfg.endpoint, cfg.payload, quoteData.context.quote_id, cfg.signal, cfg.execTimeoutMs, approval);
2348
2502
  return { context: quoteData.context, execResp };
2349
2503
  }
2350
2504
  const paymentInfo = {
@@ -2357,10 +2511,10 @@ class OneShot {
2357
2511
  token: { address: quoteData.payment_request.token_address, symbol: 'USDC', decimals: 6 }
2358
2512
  };
2359
2513
  this.checkAbortBeforePayment(cfg.signal);
2360
- const { accepted, resource, extensions } = await this.getAcceptedRequirements(quoteResp, cfg.endpoint, cfg.payload, quoteData.context.quote_id, cfg.signal);
2514
+ const { accepted, resource, extensions } = await this.getAcceptedRequirements(quoteResp, cfg.endpoint, cfg.payload, quoteData.context.quote_id, cfg.signal, approval);
2361
2515
  paymentInfo.amount = this.chargeAmount(accepted, quoteData.payment_request.amount);
2362
2516
  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);
2517
+ const execResp = await this.makePaidRequest(signed, cfg.endpoint, cfg.payload, quoteData.context.quote_id, cfg.signal, cfg.execTimeoutMs, approval);
2364
2518
  return { context: quoteData.context, execResp };
2365
2519
  }
2366
2520
  /**
@@ -2937,13 +3091,15 @@ class OneShot {
2937
3091
  * Quote-based routes don't include payment-required header on the initial 402.
2938
3092
  * If missing, probe with quote ID (no payment) to trigger the x402 middleware's 402.
2939
3093
  */
2940
- async getAcceptedRequirements(initialResp, endpoint, payload, quoteId, signal) {
3094
+ async getAcceptedRequirements(initialResp, endpoint, payload, quoteId, signal, extraHeaders) {
2941
3095
  const header = initialResp.headers.get('payment-required');
2942
3096
  if (header) {
2943
3097
  return this.parsePaymentRequired(header);
2944
3098
  }
2945
3099
  // Probe: send quote ID without payment to get x402 middleware's 402
2946
- const probeResp = await this.makeRequest(endpoint, payload, undefined, quoteId, signal);
3100
+ // Carries X-Approval-Id: a policy-held call's probe must reach the 402,
3101
+ // not be held again.
3102
+ const probeResp = await this.makeRequest(endpoint, payload, undefined, quoteId, signal, undefined, extraHeaders);
2947
3103
  return this.parsePaymentRequired(probeResp.headers.get('payment-required'));
2948
3104
  }
2949
3105
  async signPaymentAuthorization(paymentInfo, accepted, resource, extensions) {