@tangle-network/agent-gateway 0.7.1 → 0.8.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.
Files changed (64) hide show
  1. package/README.md +83 -2
  2. package/dist/chunk-GITV7CPT.js +84 -0
  3. package/dist/chunk-GITV7CPT.js.map +1 -0
  4. package/dist/chunk-J5SDVHOL.js +104 -0
  5. package/dist/chunk-J5SDVHOL.js.map +1 -0
  6. package/dist/chunk-MP6IIAIA.js +5651 -0
  7. package/dist/chunk-MP6IIAIA.js.map +1 -0
  8. package/dist/index.d.ts +70 -10
  9. package/dist/index.js +303 -21
  10. package/dist/index.js.map +1 -1
  11. package/dist/middleware.d.ts +7 -2
  12. package/dist/middleware.js +3 -2
  13. package/dist/nonce-store.d.ts +47 -11
  14. package/dist/nonce-store.js +9 -3
  15. package/dist/observer-types-A0RtA8uL.d.ts +95 -0
  16. package/dist/observer.d.ts +79 -0
  17. package/dist/observer.js +11 -0
  18. package/dist/observer.js.map +1 -0
  19. package/dist/{types-DEsMmS-X.d.ts → types-BHISsm7D.d.ts} +414 -170
  20. package/dist/types.d.ts +2 -1
  21. package/package.json +1 -1
  22. package/src/a2a/execution-fence.ts +162 -0
  23. package/src/a2a/handler.ts +506 -560
  24. package/src/a2a/message-send-execution.ts +241 -0
  25. package/src/a2a/message-stream-execution.ts +392 -0
  26. package/src/a2a/payment-recovery.ts +431 -0
  27. package/src/a2a/push-config-methods.ts +158 -0
  28. package/src/a2a/push-notifications.ts +172 -22
  29. package/src/a2a/task-cancellation.ts +50 -0
  30. package/src/a2a/task-finalization.ts +451 -0
  31. package/src/a2a/task-lifecycle.ts +54 -0
  32. package/src/a2a/task-methods.ts +163 -0
  33. package/src/a2a/task-push-delivery.ts +119 -0
  34. package/src/a2a/task-recovery.ts +11 -0
  35. package/src/a2a/task-state.ts +99 -0
  36. package/src/a2a/task-store-sql.ts +222 -24
  37. package/src/a2a/task-store.ts +58 -1
  38. package/src/a2a/task-submission-recovery.ts +178 -0
  39. package/src/a2a/types.ts +1 -0
  40. package/src/dispatch-authorization.ts +437 -0
  41. package/src/dispatch-payment-recovery.ts +248 -0
  42. package/src/dispatch-payment.ts +425 -0
  43. package/src/dispatch-pricing.ts +108 -0
  44. package/src/dispatch-sandbox.ts +422 -0
  45. package/src/dispatch-settlement.ts +139 -0
  46. package/src/dispatch-types.ts +81 -0
  47. package/src/dispatch.ts +35 -483
  48. package/src/index.ts +57 -1
  49. package/src/middleware.ts +307 -26
  50. package/src/mpp-payment.ts +117 -0
  51. package/src/nonce-store.ts +122 -20
  52. package/src/observer-types.ts +63 -0
  53. package/src/observer.ts +3 -63
  54. package/src/payment-operations.ts +485 -0
  55. package/src/payment-recovery-sql.ts +108 -0
  56. package/src/payment-recovery-worker.ts +488 -0
  57. package/src/payment-recovery.ts +331 -0
  58. package/src/payment-types.ts +48 -0
  59. package/src/types.ts +144 -46
  60. package/src/verify.ts +233 -71
  61. package/dist/chunk-M7ZJAK4K.js +0 -53
  62. package/dist/chunk-M7ZJAK4K.js.map +0 -1
  63. package/dist/chunk-Q4YAIEZY.js +0 -1763
  64. package/dist/chunk-Q4YAIEZY.js.map +0 -1
@@ -0,0 +1,331 @@
1
+ import type { MppChargeOperation } from './mpp-payment'
2
+ import type { PaymentOperation } from './payment-operations'
3
+ import type {
4
+ PaymentMethod,
5
+ PaymentSettlementBasis,
6
+ SandboxExecutionBudget,
7
+ SandboxUsageReceipt,
8
+ } from './payment-types'
9
+
10
+ export type { PaymentSettlementBasis } from './payment-types'
11
+
12
+ export const PAYMENT_RECOVERY_VERSION = 1 as const
13
+
14
+ export type PaymentRecoveryState =
15
+ | 'claiming'
16
+ | 'claimed'
17
+ | 'executing'
18
+ | 'retained'
19
+ | 'settling'
20
+ | 'releasing'
21
+ | 'reconciled'
22
+
23
+ export interface SerializedPaymentOperation {
24
+ protocolVersion: 2
25
+ operationId: string
26
+ acquiredByRequestId: string
27
+ executionStartedAt?: number
28
+ retentionReason?: string
29
+ nonceKey: string
30
+ authorizationId: string
31
+ reservedAmount: string
32
+ settledAmount: string
33
+ refundAmount: string
34
+ expiresAt: number
35
+ state: PaymentOperation['state']
36
+ }
37
+
38
+ export interface PaymentRecoveryAttribution {
39
+ requestId: string
40
+ agentId: string
41
+ agentSlug: string
42
+ consumerId: string
43
+ paymentMethod: PaymentMethod
44
+ startMs: number
45
+ pricePerTokenUsd: number
46
+ platformFeePercent: number
47
+ requiredAmount: string
48
+ currencyDecimals: number
49
+ maxOutputTokens: number
50
+ executionBudget: SandboxExecutionBudget
51
+ }
52
+
53
+ export type PaymentRecoveryTarget =
54
+ | {
55
+ kind: 'x402'
56
+ operationId: string
57
+ operation?: SerializedPaymentOperation
58
+ }
59
+ | {
60
+ kind: 'mpp-charge'
61
+ method: string
62
+ operationId: string
63
+ operation?: MppChargeOperation
64
+ }
65
+
66
+ /** Durable outbox row for one payment identity. Rows are never deleted here. */
67
+ export interface PaymentRecoveryRecord {
68
+ version: typeof PAYMENT_RECOVERY_VERSION
69
+ id: string
70
+ revision: number
71
+ state: PaymentRecoveryState
72
+ payment: PaymentRecoveryTarget
73
+ attribution: PaymentRecoveryAttribution
74
+ workStarted: boolean
75
+ /** Earliest time a missing receipt may settle at the quoted ceiling. */
76
+ fallbackAt?: number
77
+ usage?: SandboxUsageReceipt
78
+ usageRecorded: boolean
79
+ settlementBasis?: PaymentSettlementBasis
80
+ reason?: string
81
+ attempts: number
82
+ lastError?: string
83
+ nextAttemptAt: number
84
+ lease?: { id: string; expiresAt: number }
85
+ createdAt: number
86
+ updatedAt: number
87
+ reconciledAt?: number
88
+ }
89
+
90
+ export interface PaymentRecoveryStore {
91
+ createIfAbsent(record: PaymentRecoveryRecord): Promise<boolean>
92
+ get(id: string): Promise<PaymentRecoveryRecord | undefined>
93
+ compareAndSet(
94
+ expected: PaymentRecoveryRecord,
95
+ next: PaymentRecoveryRecord,
96
+ ): Promise<boolean>
97
+ listDue(now: number, limit: number): Promise<PaymentRecoveryRecord[]>
98
+ }
99
+
100
+ export interface PaymentRecoveryConfig {
101
+ store: PaymentRecoveryStore
102
+ /** Claimed payment with no execution becomes recoverable after this delay. */
103
+ staleRequestMs?: number
104
+ /** Work without a final receipt settles at the quoted ceiling after this delay. */
105
+ receiptTimeoutMs?: number
106
+ /** Failed provider recovery waits this long before its next attempt. */
107
+ retryDelayMs?: number
108
+ /** One recovery worker owns a row for this duration. */
109
+ leaseMs?: number
110
+ }
111
+
112
+ export const DEFAULT_STALE_REQUEST_MS = 30_000
113
+ export const DEFAULT_RECEIPT_TIMEOUT_MS = 5 * 60_000
114
+ export const DEFAULT_RECOVERY_RETRY_MS = 30_000
115
+ export const DEFAULT_RECOVERY_LEASE_MS = 30_000
116
+
117
+ /** Atomic single-process store for tests and explicit local demo mode. */
118
+ export class MemoryPaymentRecoveryStore implements PaymentRecoveryStore {
119
+ private readonly records = new Map<string, PaymentRecoveryRecord>()
120
+
121
+ async createIfAbsent(record: PaymentRecoveryRecord): Promise<boolean> {
122
+ if (this.records.has(record.id)) return false
123
+ this.records.set(record.id, clone(record))
124
+ return true
125
+ }
126
+
127
+ async get(id: string): Promise<PaymentRecoveryRecord | undefined> {
128
+ const record = this.records.get(id)
129
+ return record ? clone(record) : undefined
130
+ }
131
+
132
+ async compareAndSet(
133
+ expected: PaymentRecoveryRecord,
134
+ next: PaymentRecoveryRecord,
135
+ ): Promise<boolean> {
136
+ const current = this.records.get(expected.id)
137
+ if (!current || current.revision !== expected.revision) return false
138
+ this.records.set(expected.id, clone(next))
139
+ return true
140
+ }
141
+
142
+ async listDue(now: number, limit: number): Promise<PaymentRecoveryRecord[]> {
143
+ return [...this.records.values()]
144
+ .filter((record) => record.state !== 'reconciled' && record.nextAttemptAt <= now)
145
+ .sort((left, right) => left.nextAttemptAt - right.nextAttemptAt)
146
+ .slice(0, limit)
147
+ .map(clone)
148
+ }
149
+ }
150
+
151
+ export class PaymentRecoveryFenceError extends Error {
152
+ constructor(id: string) {
153
+ super(`payment recovery fence was lost for ${id}`)
154
+ this.name = 'PaymentRecoveryFenceError'
155
+ }
156
+ }
157
+
158
+ export class PaymentRecoveryReplayError extends Error {
159
+ constructor(id: string) {
160
+ super(`payment recovery identity was already reconciled: ${id}`)
161
+ this.name = 'PaymentRecoveryReplayError'
162
+ }
163
+ }
164
+
165
+ /** Update only while the caller still owns the durable row fence. */
166
+ export async function updateOwnedPaymentRecovery(
167
+ store: PaymentRecoveryStore,
168
+ id: string,
169
+ fenceId: string,
170
+ update: (record: PaymentRecoveryRecord) => PaymentRecoveryRecord,
171
+ now = Date.now(),
172
+ ): Promise<PaymentRecoveryRecord> {
173
+ for (let attempt = 0; attempt < 16; attempt += 1) {
174
+ const current = await store.get(id)
175
+ if (!current) throw new Error(`payment recovery record ${id} was not found`)
176
+ if (current.state === 'reconciled' || current.lease?.id !== fenceId) {
177
+ throw new PaymentRecoveryFenceError(id)
178
+ }
179
+ const candidate = update(clone(current))
180
+ assertRecoveryUpdate(current, candidate)
181
+ const next: PaymentRecoveryRecord = {
182
+ ...candidate,
183
+ id: current.id,
184
+ version: PAYMENT_RECOVERY_VERSION,
185
+ revision: current.revision + 1,
186
+ createdAt: current.createdAt,
187
+ updatedAt: now,
188
+ }
189
+ if (await store.compareAndSet(current, next)) return next
190
+ }
191
+ throw new Error(`payment recovery record ${id} changed too many times`)
192
+ }
193
+
194
+ const PAYMENT_RECOVERY_TRANSITIONS: Record<PaymentRecoveryState, ReadonlySet<PaymentRecoveryState>> = {
195
+ claiming: new Set(['claiming', 'claimed', 'releasing', 'reconciled']),
196
+ claimed: new Set(['claimed', 'executing', 'releasing', 'reconciled']),
197
+ executing: new Set(['executing', 'retained', 'settling', 'releasing', 'reconciled']),
198
+ retained: new Set(['retained', 'settling', 'reconciled']),
199
+ settling: new Set(['settling', 'reconciled']),
200
+ releasing: new Set(['releasing', 'reconciled']),
201
+ reconciled: new Set(['reconciled']),
202
+ }
203
+
204
+ function assertRecoveryUpdate(
205
+ current: PaymentRecoveryRecord,
206
+ candidate: PaymentRecoveryRecord,
207
+ ): void {
208
+ if (!PAYMENT_RECOVERY_TRANSITIONS[current.state].has(candidate.state)) {
209
+ throw new Error(`invalid payment recovery transition ${current.state} -> ${candidate.state}`)
210
+ }
211
+ if (JSON.stringify(current.attribution) !== JSON.stringify(candidate.attribution)) {
212
+ throw new Error('payment recovery attribution is immutable')
213
+ }
214
+ if (
215
+ current.payment.kind !== candidate.payment.kind ||
216
+ current.payment.operationId !== candidate.payment.operationId ||
217
+ (current.payment.kind === 'mpp-charge' &&
218
+ candidate.payment.kind === 'mpp-charge' &&
219
+ current.payment.method !== candidate.payment.method)
220
+ ) {
221
+ throw new Error('payment recovery identity is immutable')
222
+ }
223
+ if (current.workStarted && !candidate.workStarted) {
224
+ throw new Error('payment recovery cannot forget started work')
225
+ }
226
+ if (current.usageRecorded && !candidate.usageRecorded) {
227
+ throw new Error('payment recovery cannot forget usage attribution')
228
+ }
229
+ if (current.usage && (!candidate.usage || !sameRecoveryUsage(current.usage, candidate.usage))) {
230
+ throw new Error('payment recovery usage receipt is immutable')
231
+ }
232
+ if (
233
+ current.settlementBasis &&
234
+ current.settlementBasis !== candidate.settlementBasis
235
+ ) {
236
+ throw new Error('payment recovery settlement basis is immutable')
237
+ }
238
+ }
239
+
240
+ function sameRecoveryUsage(
241
+ left: SandboxUsageReceipt,
242
+ right: SandboxUsageReceipt,
243
+ ): boolean {
244
+ return left.inputTokens === right.inputTokens &&
245
+ left.outputTokens === right.outputTokens &&
246
+ left.reasoningTokens === right.reasoningTokens &&
247
+ left.toolTokens === right.toolTokens &&
248
+ left.toolCallCount === right.toolCallCount &&
249
+ left.providerCostUsd === right.providerCostUsd &&
250
+ left.budgetEnforced === right.budgetEnforced
251
+ }
252
+
253
+ export function serializePaymentOperation(
254
+ operation: PaymentOperation,
255
+ ): SerializedPaymentOperation {
256
+ return {
257
+ protocolVersion: 2,
258
+ operationId: operation.operationId,
259
+ acquiredByRequestId: operation.acquiredByRequestId,
260
+ ...(operation.executionStartedAt !== undefined
261
+ ? { executionStartedAt: operation.executionStartedAt }
262
+ : {}),
263
+ ...(operation.retentionReason ? { retentionReason: operation.retentionReason } : {}),
264
+ nonceKey: operation.nonceKey,
265
+ authorizationId: operation.authorizationId,
266
+ reservedAmount: operation.reservedAmount.toString(),
267
+ settledAmount: operation.settledAmount.toString(),
268
+ refundAmount: operation.refundAmount.toString(),
269
+ expiresAt: operation.expiresAt,
270
+ state: operation.state,
271
+ }
272
+ }
273
+
274
+ export function deserializePaymentOperation(
275
+ value: SerializedPaymentOperation,
276
+ ): PaymentOperation {
277
+ if (value.protocolVersion !== 2) throw new Error('unsupported payment operation version')
278
+ if (!value.operationId || !value.acquiredByRequestId || !value.nonceKey || !value.authorizationId) {
279
+ throw new Error('incomplete payment operation recovery record')
280
+ }
281
+ return {
282
+ protocolVersion: 2,
283
+ operationId: value.operationId,
284
+ acquiredByRequestId: value.acquiredByRequestId,
285
+ ...(value.executionStartedAt !== undefined ? { executionStartedAt: value.executionStartedAt } : {}),
286
+ ...(value.retentionReason ? { retentionReason: value.retentionReason } : {}),
287
+ nonceKey: value.nonceKey,
288
+ authorizationId: value.authorizationId,
289
+ reservedAmount: BigInt(value.reservedAmount),
290
+ settledAmount: BigInt(value.settledAmount),
291
+ refundAmount: BigInt(value.refundAmount),
292
+ expiresAt: value.expiresAt,
293
+ state: value.state,
294
+ }
295
+ }
296
+
297
+ export function recoveryTiming(config: PaymentRecoveryConfig): {
298
+ staleRequestMs: number
299
+ receiptTimeoutMs: number
300
+ retryDelayMs: number
301
+ leaseMs: number
302
+ } {
303
+ return {
304
+ staleRequestMs: config.staleRequestMs ?? DEFAULT_STALE_REQUEST_MS,
305
+ receiptTimeoutMs: config.receiptTimeoutMs ?? DEFAULT_RECEIPT_TIMEOUT_MS,
306
+ retryDelayMs: config.retryDelayMs ?? DEFAULT_RECOVERY_RETRY_MS,
307
+ leaseMs: config.leaseMs ?? DEFAULT_RECOVERY_LEASE_MS,
308
+ }
309
+ }
310
+
311
+ export function assertPaymentRecoveryConfig(config: PaymentRecoveryConfig): void {
312
+ for (const [name, value] of Object.entries(recoveryTiming(config))) {
313
+ if (!Number.isSafeInteger(value) || value <= 0) {
314
+ throw new Error(`payment recovery ${name} must be a positive safe integer`)
315
+ }
316
+ }
317
+ const store = config.store
318
+ if (
319
+ !store ||
320
+ typeof store.createIfAbsent !== 'function' ||
321
+ typeof store.get !== 'function' ||
322
+ typeof store.compareAndSet !== 'function' ||
323
+ typeof store.listDue !== 'function'
324
+ ) {
325
+ throw new Error('payment recovery store must provide atomic create, compare-and-set, and due scans')
326
+ }
327
+ }
328
+
329
+ function clone<T>(value: T): T {
330
+ return JSON.parse(JSON.stringify(value)) as T
331
+ }
@@ -0,0 +1,48 @@
1
+ export type PaymentMethod = 'x402' | 'mpp' | 'apikey' | 'none'
2
+
3
+ export interface SandboxExecutionBudget {
4
+ maxInputTokens: number
5
+ maxOutputTokens: number
6
+ maxReasoningTokens: number
7
+ maxToolTokens: number
8
+ maxToolCalls: number
9
+ maxProviderCostUsd: number
10
+ }
11
+
12
+ export interface SandboxUsageReceipt {
13
+ inputTokens: number
14
+ outputTokens: number
15
+ reasoningTokens: number
16
+ toolTokens: number
17
+ toolCallCount: number
18
+ providerCostUsd: number
19
+ /** True only when the provider/adapter enforced every supplied budget. */
20
+ budgetEnforced: boolean
21
+ }
22
+
23
+ export type PaymentSettlementBasis = 'usage-receipt' | 'quoted-ceiling'
24
+
25
+ export interface GatewayUsageEvent {
26
+ /** Correlates usage, settlement, and observer records for one request. */
27
+ requestId: string
28
+ agentId: string
29
+ agentSlug: string
30
+ consumerId: string
31
+ paymentMethod: PaymentMethod
32
+ inputTokens: number
33
+ outputTokens: number
34
+ /** Optional in 0.7.2 so 0.7.1 event constructors remain source-compatible. */
35
+ reasoningTokens?: number
36
+ /** Optional in 0.7.2 so 0.7.1 event constructors remain source-compatible. */
37
+ toolTokens?: number
38
+ /** Optional in 0.7.2 so 0.7.1 event constructors remain source-compatible. */
39
+ toolCallCount?: number
40
+ /** Optional in 0.7.2 so 0.7.1 event constructors remain source-compatible. */
41
+ providerCostUsd?: number
42
+ totalCostUsd: number
43
+ ownerEarnedUsd: number
44
+ platformFeeUsd: number
45
+ durationMs: number
46
+ /** Exact receipt in normal operation; quoted ceiling only after receipt timeout. */
47
+ settlementBasis?: PaymentSettlementBasis
48
+ }
package/src/types.ts CHANGED
@@ -1,3 +1,25 @@
1
+ import type {
2
+ PaymentAuthorizationContext,
3
+ PaymentOperation,
4
+ PaymentOperations,
5
+ } from './payment-operations'
6
+ import type { MppAuthenticatedCredential, MppChargeLifecycle } from './mpp-payment'
7
+ import type { PaymentRecoveryConfig } from './payment-recovery'
8
+ import type { GatewayObserver } from './observer-types'
9
+ import type {
10
+ GatewayUsageEvent,
11
+ PaymentMethod,
12
+ SandboxExecutionBudget,
13
+ SandboxUsageReceipt,
14
+ } from './payment-types'
15
+
16
+ export type {
17
+ GatewayUsageEvent,
18
+ PaymentMethod,
19
+ SandboxExecutionBudget,
20
+ SandboxUsageReceipt,
21
+ } from './payment-types'
22
+
1
23
  // --- Agent resolution ---
2
24
 
3
25
  export interface AgentMeta {
@@ -71,8 +93,6 @@ export interface AgentMeta {
71
93
 
72
94
  // --- Payment ---
73
95
 
74
- export type PaymentMethod = 'x402' | 'mpp' | 'apikey' | 'none'
75
-
76
96
  export interface X402Config {
77
97
  /** Ethereum operator address for SpendAuth verification */
78
98
  operatorAddress: string
@@ -84,8 +104,35 @@ export interface X402Config {
84
104
  rpcUrl?: string
85
105
  /** Demo mode: skip signature verification (default: false). NEVER enable in production. */
86
106
  demoMode?: boolean
87
- /** Production signer verification. Called with the raw SpendAuth payload. Return true if signature is valid. */
88
- verifySigner?: (payload: Record<string, unknown>) => Promise<boolean>
107
+ /** Protocol version for new durable payment operations. Production version 1 is read-only. */
108
+ paymentProtocolVersion?: 1 | 2
109
+ /**
110
+ * Production signature verification. This callback must not reserve, claim,
111
+ * or mutate payment state.
112
+ */
113
+ verifySigner?: (
114
+ payload: Record<string, unknown>,
115
+ context?: { protocolVersion: 1 | 2; requestId?: string },
116
+ ) => Promise<boolean>
117
+ /**
118
+ * Claim the verified payment after all request checks pass and immediately
119
+ * before sandbox work starts. Version 2 returns durable operation ownership.
120
+ * A boolean return is the version 1 demo-only compatibility path.
121
+ * Production version 1 must omit this callback because it has no durable
122
+ * provider operation or recovery identity.
123
+ */
124
+ authorizePayment?: (
125
+ payload: Record<string, unknown>,
126
+ context: PaymentAuthorizationContext,
127
+ ) => Promise<boolean | PaymentOperation>
128
+ /** Version 2 operation store. It owns claim, settle, release, and reclaim. */
129
+ paymentOperations?: PaymentOperations
130
+ /**
131
+ * Number of base-unit decimals used by the payment token. Defaults to 6.
132
+ * The gateway uses this value to reject a payment that cannot cover the
133
+ * request's maximum token charge before it calls `verifySigner`.
134
+ */
135
+ currencyDecimals?: number
89
136
  }
90
137
 
91
138
  export interface MppConfig {
@@ -94,17 +141,24 @@ export interface MppConfig {
94
141
  /** MPP method name (default: "blueprintevm") */
95
142
  method?: string
96
143
  /**
97
- * Production verifier for the method-specific credential. Return the
98
- * authenticated consumer id, or null when the credential is invalid.
99
- * The callback receives the decoded JSON payload when one exists plus the
100
- * original decoded credential so non-JSON methods can verify their own form.
101
- * Omit only when x402.demoMode is explicitly enabled for local testing, or
102
- * when x402.verifySigner handles an x402-compatible MPP credential.
144
+ * Pure credential authentication. Return stable method-owned identity, or null.
145
+ * This callback must not consume a credential, create a processor object,
146
+ * reserve funds, confirm payment, or perform any other financial mutation.
147
+ */
148
+ authenticateCredential?: (
149
+ payload: Record<string, unknown>,
150
+ context: { method: string; credential: string },
151
+ ) => Promise<MppAuthenticatedCredential | null>
152
+ /**
153
+ * @deprecated Use authenticateCredential and return a stable payment identity.
154
+ * This 0.7.1 callback remains supported through an explicit compatibility adapter.
103
155
  */
104
156
  verifySigner?: (
105
157
  payload: Record<string, unknown>,
106
158
  context: { method: string; credential: string },
107
159
  ) => Promise<string | null>
160
+ /** Required immediate-charge lifecycle for every non-BlueprinTEVM method. */
161
+ charge?: MppChargeLifecycle
108
162
  }
109
163
 
110
164
  export interface PaymentResult {
@@ -133,30 +187,6 @@ export interface ApiKeyInfo {
133
187
  dailyLimit?: number
134
188
  }
135
189
 
136
- // --- Usage tracking ---
137
-
138
- export interface GatewayUsageEvent {
139
- /**
140
- * Per-request id (matches `RequestContext.requestId`). Lets
141
- * `recordUsage` correlate the usage row to the same request that
142
- * `settlePayment` settles, observability hooks observe, and
143
- * `onRequestComplete` reports — without re-deriving from a
144
- * synthetic key. Required field as of 0.4.0; the gateway always has
145
- * it in scope at the recordUsage call site.
146
- */
147
- requestId: string
148
- agentId: string
149
- agentSlug: string
150
- consumerId: string
151
- paymentMethod: PaymentMethod
152
- inputTokens: number
153
- outputTokens: number
154
- totalCostUsd: number
155
- ownerEarnedUsd: number
156
- platformFeeUsd: number
157
- durationMs: number
158
- }
159
-
160
190
  // --- Sandbox interface ---
161
191
 
162
192
  export interface SandboxStreamEvent {
@@ -174,11 +204,25 @@ export interface SandboxStreamEvent {
174
204
  * the caller (rendered as the input-required message body).
175
205
  */
176
206
  inputRequired?: { prompt?: string }
207
+ /** Provider receipt fields. Version 2 operations require every field. */
208
+ usage?: Partial<SandboxUsageReceipt>
209
+ /** Tool or reasoning events may carry hidden usage without visible text. */
210
+ tool?: { name?: string; inputTokens?: number; outputTokens?: number }
211
+ reasoning?: { tokens?: number }
177
212
  }
178
213
  }
179
214
 
180
215
  export interface SandboxBox {
181
- streamPrompt(message: string, opts?: { sessionId?: string; systemPrompt?: string }): AsyncIterable<SandboxStreamEvent>
216
+ streamPrompt(
217
+ message: string,
218
+ opts?: {
219
+ sessionId?: string
220
+ systemPrompt?: string
221
+ maxOutputTokens?: number
222
+ executionBudget?: SandboxExecutionBudget
223
+ signal?: AbortSignal
224
+ },
225
+ ): AsyncIterable<SandboxStreamEvent>
182
226
  }
183
227
 
184
228
  // --- Gateway config ---
@@ -200,7 +244,12 @@ export interface GatewayConfig {
200
244
  consumer: { method: PaymentMethod; consumerId: string; keyId?: string; requestId: string },
201
245
  ) => Promise<{ allow: true } | { allow: false; reason: string; code: string }>
202
246
 
203
- /** Record a usage event after request completes. */
247
+ /**
248
+ * Record a usage event after request completes.
249
+ * The implementation must atomically upsert by requestId and return
250
+ * success when the row already exists. Recovery may retry after an
251
+ * acknowledgement is lost, so one request ID must produce one usage row.
252
+ */
204
253
  recordUsage: (event: GatewayUsageEvent) => Promise<void>
205
254
 
206
255
  /** x402 payment configuration */
@@ -209,6 +258,12 @@ export interface GatewayConfig {
209
258
  /** MPP (Machine Payments Protocol) configuration. It is advertised only when a production verifier or explicit demo mode is available. */
210
259
  mpp?: MppConfig
211
260
 
261
+ /**
262
+ * Durable payment recovery outbox. Production payment protocol version 2
263
+ * and generic MPP charge methods require this configuration.
264
+ */
265
+ paymentRecovery?: PaymentRecoveryConfig
266
+
212
267
  /**
213
268
  * Verify an API key. Return key info if valid, null if invalid.
214
269
  * In explicit x402 demo mode, the built-in verifier accepts `sk_agent_*` keys.
@@ -217,10 +272,11 @@ export interface GatewayConfig {
217
272
  verifyApiKey?: (authHeader: string) => Promise<ApiKeyInfo | null>
218
273
 
219
274
  /**
220
- * Settle payment after successful response.
221
- * For x402: call ShieldedCredits.claimPayment()
222
- * For API key: deduct from spending limit
223
- * Default: no-op (demo mode).
275
+ * Settle a legacy payment after usage attribution is recorded.
276
+ * Version 2 x402 operations use `x402.paymentOperations` instead.
277
+ * Production x402 version 1 rejects this callback before nonce claim.
278
+ * For API keys, deduct from the spending limit.
279
+ * Default: no-op in explicit demo mode.
224
280
  */
225
281
  settlePayment?: (payment: PaymentResult, cost: number) => Promise<void>
226
282
 
@@ -230,6 +286,29 @@ export interface GatewayConfig {
230
286
  /** Max message length in chars (default: 8000) */
231
287
  maxMessageLength?: number
232
288
 
289
+ /** Maximum output token request the gateway accepts. Defaults to 4096. */
290
+ maxOutputTokens?: number
291
+
292
+ /** Output token limit used when a request omits `max_tokens`. Defaults to 1024. */
293
+ defaultOutputTokens?: number
294
+
295
+ /**
296
+ * Return a safe upper bound for the complete provider input.
297
+ * Include system, chat framing, retained history, tools, harness, and workspace context.
298
+ */
299
+ inputTokenBound?: (input: {
300
+ agent: AgentMeta
301
+ messages: ChatMessage[]
302
+ }) => number
303
+
304
+ /** Hidden provider spend limits included in the pre-execution payment quote. */
305
+ executionBudget?: {
306
+ maxReasoningTokens?: number
307
+ maxToolTokens?: number
308
+ maxToolCalls?: number
309
+ maxProviderCostUsd?: number
310
+ }
311
+
233
312
  /** Required scope for chat endpoint (default: "chat"). API keys must include this scope. */
234
313
  requiredScope?: string
235
314
 
@@ -251,19 +330,33 @@ export interface GatewayConfig {
251
330
  * and settlement failures. See ./observer.ts for the interface and
252
331
  * ConsoleObserver / CompositeObserver implementations.
253
332
  */
254
- observer?: import('./observer').GatewayObserver
333
+ observer?: GatewayObserver
255
334
 
256
335
  /**
257
- * A2A protocol configuration. When set, the gateway exposes the A2A
258
- * surface alongside its OpenAI-compatible endpoints:
336
+ * A2A protocol configuration. The gateway exposes A2A with an in-memory
337
+ * task store by default. Set this object to provide durable storage or push:
259
338
  * GET /:slug/.well-known/agent.json — AgentCard discovery
260
339
  * POST /:slug — JSON-RPC 2.0 endpoint
261
340
  * methods: message/send, message/stream, tasks/get, tasks/cancel
262
341
  * Auth + rate-limit + injection-filter + authorization all share the
263
342
  * same pipeline as the OpenAI-compat path. `taskStore` defaults to
264
343
  * `InMemoryTaskStore`; swap in D1/postgres/DO for durable deployments.
265
- */
344
+ */
266
345
  a2a?: {
346
+ /**
347
+ * Authorize reads, cancellation, resubscription, and push configuration
348
+ * for an existing task. Production control methods fail closed when this
349
+ * hook is absent; explicit demo mode permits local tests.
350
+ */
351
+ authorizeTaskAccess?: (
352
+ task: import('./a2a/types').Task,
353
+ context: {
354
+ method: string
355
+ agentSlug: string
356
+ authorization: string
357
+ paymentSignature: string
358
+ },
359
+ ) => Promise<boolean>
267
360
  /**
268
361
  * Where tasks live. Defaults to `InMemoryTaskStore`; swap in
269
362
  * `SqlTaskStore` (D1, postgres, sqlite, libSQL) for durability across
@@ -282,8 +375,8 @@ export interface GatewayConfig {
282
375
  * Shared HMAC secret used to sign webhook deliveries (`X-A2A-Signature:
283
376
  * sha256=<hex>`). The consumer's webhook verifies the body against this
284
377
  * secret to confirm the call originated from this gateway. Required when
285
- * `pushStore` is set; without it, deliveries fire unsigned and a
286
- * malicious party that knows the webhook URL can forge deliveries.
378
+ * `pushStore` is set in production. Explicit demo mode may omit it for
379
+ * local tests; production deliveries never run unsigned.
287
380
  */
288
381
  webhookSecret?: string
289
382
  /**
@@ -291,6 +384,11 @@ export interface GatewayConfig {
291
384
  * `fetch`. Override for tests or to wire a queue-backed sender.
292
385
  */
293
386
  pushFetcher?: typeof fetch
387
+ /**
388
+ * DNS-aware policy for push destinations. Required when production
389
+ * push delivery is enabled so private DNS names cannot receive task data.
390
+ */
391
+ pushUrlValidator?: (url: URL) => boolean | Promise<boolean>
294
392
  }
295
393
  }
296
394