@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.
- package/README.md +83 -2
- package/dist/chunk-GITV7CPT.js +84 -0
- package/dist/chunk-GITV7CPT.js.map +1 -0
- package/dist/chunk-J5SDVHOL.js +104 -0
- package/dist/chunk-J5SDVHOL.js.map +1 -0
- package/dist/chunk-MP6IIAIA.js +5651 -0
- package/dist/chunk-MP6IIAIA.js.map +1 -0
- package/dist/index.d.ts +70 -10
- package/dist/index.js +303 -21
- package/dist/index.js.map +1 -1
- package/dist/middleware.d.ts +7 -2
- package/dist/middleware.js +3 -2
- package/dist/nonce-store.d.ts +47 -11
- package/dist/nonce-store.js +9 -3
- package/dist/observer-types-A0RtA8uL.d.ts +95 -0
- package/dist/observer.d.ts +79 -0
- package/dist/observer.js +11 -0
- package/dist/observer.js.map +1 -0
- package/dist/{types-DEsMmS-X.d.ts → types-BHISsm7D.d.ts} +414 -170
- package/dist/types.d.ts +2 -1
- package/package.json +1 -1
- package/src/a2a/execution-fence.ts +162 -0
- package/src/a2a/handler.ts +506 -560
- package/src/a2a/message-send-execution.ts +241 -0
- package/src/a2a/message-stream-execution.ts +392 -0
- package/src/a2a/payment-recovery.ts +431 -0
- package/src/a2a/push-config-methods.ts +158 -0
- package/src/a2a/push-notifications.ts +172 -22
- package/src/a2a/task-cancellation.ts +50 -0
- package/src/a2a/task-finalization.ts +451 -0
- package/src/a2a/task-lifecycle.ts +54 -0
- package/src/a2a/task-methods.ts +163 -0
- package/src/a2a/task-push-delivery.ts +119 -0
- package/src/a2a/task-recovery.ts +11 -0
- package/src/a2a/task-state.ts +99 -0
- package/src/a2a/task-store-sql.ts +222 -24
- package/src/a2a/task-store.ts +58 -1
- package/src/a2a/task-submission-recovery.ts +178 -0
- package/src/a2a/types.ts +1 -0
- package/src/dispatch-authorization.ts +437 -0
- package/src/dispatch-payment-recovery.ts +248 -0
- package/src/dispatch-payment.ts +425 -0
- package/src/dispatch-pricing.ts +108 -0
- package/src/dispatch-sandbox.ts +422 -0
- package/src/dispatch-settlement.ts +139 -0
- package/src/dispatch-types.ts +81 -0
- package/src/dispatch.ts +35 -483
- package/src/index.ts +57 -1
- package/src/middleware.ts +307 -26
- package/src/mpp-payment.ts +117 -0
- package/src/nonce-store.ts +122 -20
- package/src/observer-types.ts +63 -0
- package/src/observer.ts +3 -63
- package/src/payment-operations.ts +485 -0
- package/src/payment-recovery-sql.ts +108 -0
- package/src/payment-recovery-worker.ts +488 -0
- package/src/payment-recovery.ts +331 -0
- package/src/payment-types.ts +48 -0
- package/src/types.ts +144 -46
- package/src/verify.ts +233 -71
- package/dist/chunk-M7ZJAK4K.js +0 -53
- package/dist/chunk-M7ZJAK4K.js.map +0 -1
- package/dist/chunk-Q4YAIEZY.js +0 -1763
- package/dist/chunk-Q4YAIEZY.js.map +0 -1
package/src/middleware.ts
CHANGED
|
@@ -6,12 +6,22 @@ import {
|
|
|
6
6
|
type AuthorizedRequest,
|
|
7
7
|
type GatewayState,
|
|
8
8
|
authenticateAndGuard,
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
beginPaymentExecution,
|
|
10
|
+
markPaymentExecutionStarted,
|
|
11
|
+
renewPaymentExecution,
|
|
12
|
+
claimPayment,
|
|
13
|
+
dispatchSandboxStreamRich,
|
|
14
|
+
releasePayment,
|
|
15
|
+
releasePaymentAfterFailure,
|
|
11
16
|
settleAndRecord,
|
|
12
17
|
} from './dispatch'
|
|
13
|
-
import { MemoryNonceStore } from './nonce-store'
|
|
18
|
+
import { isAtomicNonceStore, MemoryNonceStore } from './nonce-store'
|
|
14
19
|
import { type GatewayObserver, type RequestContext, generateRequestId } from './observer'
|
|
20
|
+
import {
|
|
21
|
+
MemoryPaymentRecoveryStore,
|
|
22
|
+
PaymentRecoveryReplayError,
|
|
23
|
+
assertPaymentRecoveryConfig,
|
|
24
|
+
} from './payment-recovery'
|
|
15
25
|
import { MemoryRateLimitStore, type RateLimitStore } from './rate-limit'
|
|
16
26
|
import type { ChatCompletionChunk, ChatCompletionRequest, GatewayConfig } from './types'
|
|
17
27
|
import { isApiKeyAuthEnabled, isMppAuthEnabled } from './verify'
|
|
@@ -26,7 +36,8 @@ import { isApiKeyAuthEnabled, isMppAuthEnabled } from './verify'
|
|
|
26
36
|
* GET /:slug/chat/completions — agent discovery metadata (Tangle-native shape)
|
|
27
37
|
* POST /:slug/chat/completions — OpenAI-compatible chat endpoint (paid)
|
|
28
38
|
*/
|
|
29
|
-
export function createAgentGateway(
|
|
39
|
+
export function createAgentGateway(inputConfig: GatewayConfig) {
|
|
40
|
+
let config = inputConfig
|
|
30
41
|
// Production gateways must verify x402 signatures. Tests and local
|
|
31
42
|
// dev can opt into the explicit demo path.
|
|
32
43
|
if (!config.x402.verifySigner && !config.x402.demoMode) {
|
|
@@ -35,6 +46,120 @@ export function createAgentGateway(config: GatewayConfig) {
|
|
|
35
46
|
'For tests, set x402.demoMode: true explicitly.',
|
|
36
47
|
)
|
|
37
48
|
}
|
|
49
|
+
const maxOutputTokens = config.maxOutputTokens ?? 4096
|
|
50
|
+
const defaultOutputTokens = config.defaultOutputTokens ?? 1024
|
|
51
|
+
if (!Number.isSafeInteger(maxOutputTokens) || maxOutputTokens <= 0) {
|
|
52
|
+
throw new Error('createAgentGateway: maxOutputTokens must be a positive integer')
|
|
53
|
+
}
|
|
54
|
+
if (
|
|
55
|
+
!Number.isSafeInteger(defaultOutputTokens) ||
|
|
56
|
+
defaultOutputTokens <= 0 ||
|
|
57
|
+
defaultOutputTokens > maxOutputTokens
|
|
58
|
+
) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
'createAgentGateway: defaultOutputTokens must be a positive integer no greater than maxOutputTokens',
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
if (
|
|
64
|
+
config.x402.currencyDecimals !== undefined &&
|
|
65
|
+
(!Number.isInteger(config.x402.currencyDecimals) ||
|
|
66
|
+
config.x402.currencyDecimals < 0 ||
|
|
67
|
+
config.x402.currencyDecimals > 18)
|
|
68
|
+
) {
|
|
69
|
+
throw new Error('createAgentGateway: x402.currencyDecimals must be an integer between 0 and 18')
|
|
70
|
+
}
|
|
71
|
+
const executionBudget = config.executionBudget
|
|
72
|
+
for (const [name, value] of [
|
|
73
|
+
['maxReasoningTokens', executionBudget?.maxReasoningTokens ?? maxOutputTokens],
|
|
74
|
+
['maxToolTokens', executionBudget?.maxToolTokens ?? maxOutputTokens],
|
|
75
|
+
['maxToolCalls', executionBudget?.maxToolCalls ?? 8],
|
|
76
|
+
] as const) {
|
|
77
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
78
|
+
throw new Error(`createAgentGateway: executionBudget.${name} must be a non-negative safe integer`)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (
|
|
82
|
+
executionBudget?.maxProviderCostUsd !== undefined &&
|
|
83
|
+
(!Number.isFinite(executionBudget.maxProviderCostUsd) || executionBudget.maxProviderCostUsd < 0)
|
|
84
|
+
) {
|
|
85
|
+
throw new Error('createAgentGateway: executionBudget.maxProviderCostUsd must be finite and non-negative')
|
|
86
|
+
}
|
|
87
|
+
if (config.x402.paymentOperations && config.x402.paymentProtocolVersion === undefined) {
|
|
88
|
+
throw new Error('createAgentGateway: paymentProtocolVersion must be explicit when durable payment operations are configured')
|
|
89
|
+
}
|
|
90
|
+
if (
|
|
91
|
+
config.x402.authorizePayment &&
|
|
92
|
+
config.x402.paymentProtocolVersion !== 2 &&
|
|
93
|
+
!config.x402.demoMode
|
|
94
|
+
) {
|
|
95
|
+
throw new Error(
|
|
96
|
+
'createAgentGateway: production x402 version 1 cannot use authorizePayment; ' +
|
|
97
|
+
'use paymentProtocolVersion: 2 with paymentOperations for durable payment ownership',
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
if (config.x402.paymentProtocolVersion === 2 &&
|
|
101
|
+
(!config.x402.paymentOperations || config.x402.paymentOperations.protocolVersion !== 2)) {
|
|
102
|
+
throw new Error('createAgentGateway: payment protocol version 2 requires durable payment operations')
|
|
103
|
+
}
|
|
104
|
+
if (config.x402.paymentProtocolVersion === 1 && config.x402.paymentOperations) {
|
|
105
|
+
throw new Error('createAgentGateway: version 1 cannot be combined with version 2 payment operations')
|
|
106
|
+
}
|
|
107
|
+
if (
|
|
108
|
+
config.a2a?.pushStore &&
|
|
109
|
+
!config.x402.demoMode &&
|
|
110
|
+
(!config.a2a.webhookSecret || config.a2a.webhookSecret.trim().length === 0)
|
|
111
|
+
) {
|
|
112
|
+
throw new Error('createAgentGateway: production A2A push requires a webhookSecret')
|
|
113
|
+
}
|
|
114
|
+
const mppMethod = (config.mpp?.method ?? 'blueprintevm').toLowerCase()
|
|
115
|
+
if (config.mpp?.authenticateCredential !== undefined &&
|
|
116
|
+
typeof config.mpp.authenticateCredential !== 'function') {
|
|
117
|
+
throw new Error('createAgentGateway: mpp.authenticateCredential must be a function')
|
|
118
|
+
}
|
|
119
|
+
if (config.mpp?.verifySigner !== undefined && typeof config.mpp.verifySigner !== 'function') {
|
|
120
|
+
throw new Error('createAgentGateway: mpp.verifySigner must be a function')
|
|
121
|
+
}
|
|
122
|
+
const mppAuthenticator = typeof config.mpp?.authenticateCredential === 'function'
|
|
123
|
+
? config.mpp.authenticateCredential
|
|
124
|
+
: typeof config.mpp?.verifySigner === 'function'
|
|
125
|
+
? config.mpp.verifySigner
|
|
126
|
+
: undefined
|
|
127
|
+
if (config.mpp?.charge && config.mpp.charge.protocolVersion !== 1) {
|
|
128
|
+
throw new Error('createAgentGateway: unsupported MPP charge lifecycle version')
|
|
129
|
+
}
|
|
130
|
+
if (
|
|
131
|
+
config.mpp?.charge &&
|
|
132
|
+
mppMethod !== 'blueprintevm' &&
|
|
133
|
+
!mppAuthenticator
|
|
134
|
+
) {
|
|
135
|
+
throw new Error('createAgentGateway: generic MPP methods require credential authentication')
|
|
136
|
+
}
|
|
137
|
+
if (
|
|
138
|
+
config.mpp &&
|
|
139
|
+
mppMethod !== 'blueprintevm' &&
|
|
140
|
+
mppAuthenticator &&
|
|
141
|
+
!config.mpp.charge
|
|
142
|
+
) {
|
|
143
|
+
throw new Error('createAgentGateway: generic MPP methods require a charge lifecycle')
|
|
144
|
+
}
|
|
145
|
+
if (config.mpp && mppMethod !== 'blueprintevm' && !mppAuthenticator) {
|
|
146
|
+
throw new Error('createAgentGateway: generic MPP methods require credential authentication')
|
|
147
|
+
}
|
|
148
|
+
if (config.nonceStore && !isAtomicNonceStore(config.nonceStore)) {
|
|
149
|
+
throw new Error('createAgentGateway: durable payment ownership requires an atomic nonce store')
|
|
150
|
+
}
|
|
151
|
+
const needsRecovery = config.x402.paymentProtocolVersion === 2 ||
|
|
152
|
+
(mppMethod !== 'blueprintevm' && config.mpp?.charge !== undefined)
|
|
153
|
+
if (needsRecovery && !config.paymentRecovery) {
|
|
154
|
+
if (!config.x402.demoMode) {
|
|
155
|
+
throw new Error('createAgentGateway: durable payment recovery is required in production')
|
|
156
|
+
}
|
|
157
|
+
config = {
|
|
158
|
+
...config,
|
|
159
|
+
paymentRecovery: { store: new MemoryPaymentRecoveryStore() },
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (config.paymentRecovery) assertPaymentRecoveryConfig(config.paymentRecovery)
|
|
38
163
|
const gw = new Hono()
|
|
39
164
|
const rateLimitStore: RateLimitStore = config.rateLimitStore ?? new MemoryRateLimitStore()
|
|
40
165
|
const state: GatewayState = {
|
|
@@ -43,6 +168,12 @@ export function createAgentGateway(config: GatewayConfig) {
|
|
|
43
168
|
globalRateLimit: config.rateLimit ?? { limit: 60, windowSeconds: 60 },
|
|
44
169
|
requiredScope: config.requiredScope ?? 'chat',
|
|
45
170
|
maxLen: config.maxMessageLength ?? 8000,
|
|
171
|
+
maxOutputTokens,
|
|
172
|
+
defaultOutputTokens,
|
|
173
|
+
maxReasoningTokens: config.executionBudget?.maxReasoningTokens ?? maxOutputTokens,
|
|
174
|
+
maxToolTokens: config.executionBudget?.maxToolTokens ?? maxOutputTokens,
|
|
175
|
+
maxToolCalls: config.executionBudget?.maxToolCalls ?? 8,
|
|
176
|
+
maxProviderCostUsd: config.executionBudget?.maxProviderCostUsd,
|
|
46
177
|
obs: config.observer,
|
|
47
178
|
}
|
|
48
179
|
const obs: GatewayObserver | undefined = state.obs
|
|
@@ -125,9 +256,62 @@ export function createAgentGateway(config: GatewayConfig) {
|
|
|
125
256
|
)
|
|
126
257
|
}
|
|
127
258
|
|
|
128
|
-
const guard = await authenticateAndGuard(
|
|
259
|
+
const guard = await authenticateAndGuard(
|
|
260
|
+
c,
|
|
261
|
+
slug,
|
|
262
|
+
body.messages,
|
|
263
|
+
config,
|
|
264
|
+
state,
|
|
265
|
+
body.max_tokens,
|
|
266
|
+
)
|
|
129
267
|
if (guard instanceof Response) return guard
|
|
130
268
|
const authz = guard
|
|
269
|
+
try {
|
|
270
|
+
await claimPayment(authz, config, state)
|
|
271
|
+
} catch (error) {
|
|
272
|
+
const replayedGenericMpp = error instanceof PaymentRecoveryReplayError &&
|
|
273
|
+
authz.paymentMethod === 'mpp' &&
|
|
274
|
+
authz.mppMethod !== 'blueprintevm'
|
|
275
|
+
try {
|
|
276
|
+
await releasePayment(authz, config, 'payment authorization failed')
|
|
277
|
+
} catch (releaseError) {
|
|
278
|
+
console.error(
|
|
279
|
+
`[agent-gateway] payment release failed for ${authz.requestId}:`,
|
|
280
|
+
releaseError instanceof Error ? releaseError.message : String(releaseError),
|
|
281
|
+
)
|
|
282
|
+
}
|
|
283
|
+
await obs?.onAuthFailure?.(
|
|
284
|
+
{
|
|
285
|
+
requestId: authz.requestId,
|
|
286
|
+
agentSlug: authz.agent.slug,
|
|
287
|
+
startMs: authz.startMs,
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
method: authz.paymentMethod,
|
|
291
|
+
code: replayedGenericMpp ? 'invalid_mpp_credential' : 'payment_authorization_failed',
|
|
292
|
+
httpStatus: replayedGenericMpp ? 401 : 402,
|
|
293
|
+
},
|
|
294
|
+
)
|
|
295
|
+
const status = replayedGenericMpp ? 401 : 402
|
|
296
|
+
return c.json(
|
|
297
|
+
{
|
|
298
|
+
error: {
|
|
299
|
+
message: replayedGenericMpp ? 'Invalid Payment credential' : 'Payment authorization failed',
|
|
300
|
+
type: replayedGenericMpp ? 'authentication_error' : 'payment_required',
|
|
301
|
+
code: replayedGenericMpp ? 'invalid_mpp_credential' : 'payment_authorization_failed',
|
|
302
|
+
},
|
|
303
|
+
},
|
|
304
|
+
replayedGenericMpp
|
|
305
|
+
? {
|
|
306
|
+
status,
|
|
307
|
+
headers: {
|
|
308
|
+
'WWW-Authenticate': `Payment realm="${config.mpp!.realm}", method="${config.mpp!.method ?? 'blueprintevm'}"`,
|
|
309
|
+
'X-Request-Id': authz.requestId,
|
|
310
|
+
},
|
|
311
|
+
}
|
|
312
|
+
: { status, headers: { 'X-Payment-Required': 'spendauth', 'X-Request-Id': authz.requestId } },
|
|
313
|
+
)
|
|
314
|
+
}
|
|
131
315
|
|
|
132
316
|
return streamChatCompletions(c, authz, config, obs)
|
|
133
317
|
})
|
|
@@ -139,13 +323,37 @@ export function createAgentGateway(config: GatewayConfig) {
|
|
|
139
323
|
// regardless of which protocol the caller used.
|
|
140
324
|
const taskStore = config.a2a?.taskStore ?? new InMemoryTaskStore()
|
|
141
325
|
const pushStore = config.a2a?.pushStore
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
326
|
+
try {
|
|
327
|
+
const a2a = createA2AHandlers({ config, state, taskStore, pushStore })
|
|
328
|
+
gw.get('/:slug/.well-known/agent.json', a2a.handleAgentCard)
|
|
329
|
+
gw.post('/:slug', a2a.handleJsonRpc)
|
|
330
|
+
} catch (error) {
|
|
331
|
+
// An older custom store must not take down the OpenAI surface. Keep the
|
|
332
|
+
// A2A surface unavailable until its owner supplies atomic methods.
|
|
333
|
+
console.error(
|
|
334
|
+
'[agent-gateway] A2A is unavailable until its task store is upgraded:',
|
|
335
|
+
error instanceof Error ? error.message : String(error),
|
|
336
|
+
)
|
|
337
|
+
const unavailable = (c: import('hono').Context) => c.json(
|
|
338
|
+
{ error: 'A2A task persistence is not configured for concurrent workers' },
|
|
339
|
+
503,
|
|
340
|
+
)
|
|
341
|
+
gw.get('/:slug/.well-known/agent.json', unavailable)
|
|
342
|
+
gw.post('/:slug', unavailable)
|
|
343
|
+
}
|
|
145
344
|
|
|
146
345
|
return gw
|
|
147
346
|
}
|
|
148
347
|
|
|
348
|
+
// Consumers that bind the gateway through a package boundary can fail closed
|
|
349
|
+
// when an old binary ignores the version 2 operation contract.
|
|
350
|
+
Object.assign(createAgentGateway, { paymentProtocolVersion: 2 as const })
|
|
351
|
+
|
|
352
|
+
/** Public package-boundary version marker for durable payment operations. */
|
|
353
|
+
export namespace createAgentGateway {
|
|
354
|
+
export const paymentProtocolVersion = 2 as const
|
|
355
|
+
}
|
|
356
|
+
|
|
149
357
|
/**
|
|
150
358
|
* Drain the sandbox stream into an OpenAI-shaped SSE response, settle the
|
|
151
359
|
* payment, fire observer hooks. Identical pre-refactor behavior, just lifted
|
|
@@ -158,9 +366,23 @@ function streamChatCompletions(
|
|
|
158
366
|
config: GatewayConfig,
|
|
159
367
|
obs: GatewayObserver | undefined,
|
|
160
368
|
): Response {
|
|
161
|
-
const {
|
|
162
|
-
|
|
163
|
-
|
|
369
|
+
const {
|
|
370
|
+
agent,
|
|
371
|
+
consumerId,
|
|
372
|
+
paymentMethod,
|
|
373
|
+
requestId,
|
|
374
|
+
userMessage,
|
|
375
|
+
rateLimitRemaining,
|
|
376
|
+
maxOutputTokens,
|
|
377
|
+
} = authz
|
|
378
|
+
let outputText = ''
|
|
379
|
+
let usage: import('./types').SandboxUsageReceipt | undefined
|
|
380
|
+
let workObserved = false
|
|
381
|
+
const requestSignal = c.req.raw.signal
|
|
382
|
+
const abortController = new AbortController()
|
|
383
|
+
const abortFromRequest = () => abortController.abort()
|
|
384
|
+
if (requestSignal.aborted) abortFromRequest()
|
|
385
|
+
else requestSignal.addEventListener('abort', abortFromRequest, { once: true })
|
|
164
386
|
const ctx: RequestContext = {
|
|
165
387
|
requestId,
|
|
166
388
|
agentSlug: agent.slug,
|
|
@@ -171,7 +393,8 @@ function streamChatCompletions(
|
|
|
171
393
|
async start(controller) {
|
|
172
394
|
const encoder = new TextEncoder()
|
|
173
395
|
const sendChunk = (delta: string) => {
|
|
174
|
-
|
|
396
|
+
if (controller.desiredSize === null) return
|
|
397
|
+
outputText += delta
|
|
175
398
|
const chunk: ChatCompletionChunk = {
|
|
176
399
|
id: `chatcmpl-${Date.now()}`,
|
|
177
400
|
object: 'chat.completion.chunk',
|
|
@@ -183,10 +406,41 @@ function streamChatCompletions(
|
|
|
183
406
|
}
|
|
184
407
|
|
|
185
408
|
try {
|
|
186
|
-
for await (const
|
|
187
|
-
|
|
409
|
+
for await (const event of dispatchSandboxStreamRich(
|
|
410
|
+
agent,
|
|
411
|
+
userMessage,
|
|
412
|
+
consumerId,
|
|
413
|
+
config,
|
|
414
|
+
abortController.signal,
|
|
415
|
+
undefined,
|
|
416
|
+
maxOutputTokens,
|
|
417
|
+
() => beginPaymentExecution(authz, config),
|
|
418
|
+
authz.paymentOperation !== undefined || authz.mppChargeOperation !== undefined,
|
|
419
|
+
async () => {
|
|
420
|
+
if (authz.paymentRecoveryId) workObserved = true
|
|
421
|
+
await markPaymentExecutionStarted(authz, config)
|
|
422
|
+
},
|
|
423
|
+
authz.executionBudget.maxInputTokens,
|
|
424
|
+
() => renewPaymentExecution(authz, config),
|
|
425
|
+
)) {
|
|
426
|
+
if (event.kind === 'text') {
|
|
427
|
+
sendChunk(event.delta)
|
|
428
|
+
workObserved = true
|
|
429
|
+
}
|
|
430
|
+
if (event.kind === 'activity') workObserved = true
|
|
431
|
+
if (event.kind === 'usage') usage = event.usage
|
|
188
432
|
}
|
|
189
433
|
|
|
434
|
+
if (!usage) throw new Error('sandbox did not provide a usage receipt')
|
|
435
|
+
|
|
436
|
+
await settleAndRecord(
|
|
437
|
+
agent,
|
|
438
|
+
authz,
|
|
439
|
+
usage,
|
|
440
|
+
config,
|
|
441
|
+
obs,
|
|
442
|
+
)
|
|
443
|
+
|
|
190
444
|
const done: ChatCompletionChunk = {
|
|
191
445
|
id: `chatcmpl-${Date.now()}`,
|
|
192
446
|
object: 'chat.completion.chunk',
|
|
@@ -194,10 +448,10 @@ function streamChatCompletions(
|
|
|
194
448
|
model: agent.slug,
|
|
195
449
|
choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],
|
|
196
450
|
}
|
|
197
|
-
controller.
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
451
|
+
if (controller.desiredSize !== null) {
|
|
452
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify(done)}\n\n`))
|
|
453
|
+
controller.enqueue(encoder.encode('data: [DONE]\n\n'))
|
|
454
|
+
}
|
|
201
455
|
} catch (err) {
|
|
202
456
|
const rawMessage = err instanceof Error ? err.message : String(err)
|
|
203
457
|
// Never expose stack traces / absolute paths from sandbox internals.
|
|
@@ -205,16 +459,37 @@ function streamChatCompletions(
|
|
|
205
459
|
rawMessage.includes('/') || rawMessage.includes('\\')
|
|
206
460
|
? 'Internal agent error'
|
|
207
461
|
: rawMessage
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
462
|
+
try {
|
|
463
|
+
await obs?.onStreamError?.(ctx, { consumerId, errorMessage: rawMessage })
|
|
464
|
+
} catch (observerError) {
|
|
465
|
+
console.error(
|
|
466
|
+
`[agent-gateway] stream observer failed for ${requestId}:`,
|
|
467
|
+
observerError instanceof Error ? observerError.message : String(observerError),
|
|
468
|
+
)
|
|
469
|
+
}
|
|
470
|
+
try {
|
|
471
|
+
await releasePaymentAfterFailure(authz, config, rawMessage, workObserved || usage !== undefined)
|
|
472
|
+
} catch (releaseError) {
|
|
473
|
+
console.error(
|
|
474
|
+
`[agent-gateway] payment release failed for ${authz.requestId}:`,
|
|
475
|
+
releaseError instanceof Error ? releaseError.message : String(releaseError),
|
|
476
|
+
)
|
|
477
|
+
}
|
|
478
|
+
if (!abortController.signal.aborted && controller.desiredSize !== null) {
|
|
479
|
+
controller.enqueue(
|
|
480
|
+
encoder.encode(
|
|
481
|
+
`data: ${JSON.stringify({ error: { message: safeMessage, type: 'server_error' } })}\n\n`,
|
|
482
|
+
),
|
|
483
|
+
)
|
|
484
|
+
}
|
|
214
485
|
} finally {
|
|
215
|
-
|
|
486
|
+
requestSignal.removeEventListener('abort', abortFromRequest)
|
|
487
|
+
if (controller.desiredSize !== null) controller.close()
|
|
216
488
|
}
|
|
217
489
|
},
|
|
490
|
+
cancel() {
|
|
491
|
+
abortController.abort()
|
|
492
|
+
},
|
|
218
493
|
})
|
|
219
494
|
|
|
220
495
|
return new Response(stream, {
|
|
@@ -225,7 +500,13 @@ function streamChatCompletions(
|
|
|
225
500
|
'X-Agent-Slug': agent.slug,
|
|
226
501
|
'X-Agent-Hosting': agent.sandboxEndpoint ? 'sovereign' : 'centralized',
|
|
227
502
|
'X-Payment-Method': paymentMethod,
|
|
228
|
-
'X-Payment-Settled': paymentMethod === 'x402' ? 'pending' : 'true',
|
|
503
|
+
'X-Payment-Settled': paymentMethod === 'x402' || authz.paymentOperation ? 'pending' : 'true',
|
|
504
|
+
...(authz.mppChargeOperation
|
|
505
|
+
? { 'Payment-Receipt': authz.mppChargeOperation.receipt }
|
|
506
|
+
: {}),
|
|
507
|
+
...(authz.paymentRecoveryId
|
|
508
|
+
? { 'X-Payment-Operation-Id': authz.paymentRecoveryId }
|
|
509
|
+
: {}),
|
|
229
510
|
...(rateLimitRemaining !== undefined
|
|
230
511
|
? { 'X-RateLimit-Remaining': String(rateLimitRemaining) }
|
|
231
512
|
: {}),
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/** Version of the gateway's method-specific MPP charge contract. */
|
|
2
|
+
export const MPP_CHARGE_PROTOCOL_VERSION = 1 as const
|
|
3
|
+
|
|
4
|
+
export type MppChargeOperationState = 'confirmed' | 'releasing' | 'released'
|
|
5
|
+
|
|
6
|
+
/** Pure authentication result for one method credential. */
|
|
7
|
+
export interface MppAuthenticatedCredential {
|
|
8
|
+
consumerId: string
|
|
9
|
+
/**
|
|
10
|
+
* Stable, non-secret processor identity for this payment credential.
|
|
11
|
+
* Return the same value for equivalent encodings of one credential.
|
|
12
|
+
* The gateway hashes this value before it persists or claims it.
|
|
13
|
+
*/
|
|
14
|
+
paymentIdentity: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Durable result of one immediate MPP charge. */
|
|
18
|
+
export interface MppChargeOperation {
|
|
19
|
+
protocolVersion: typeof MPP_CHARGE_PROTOCOL_VERSION
|
|
20
|
+
operationId: string
|
|
21
|
+
acquiredByRequestId: string
|
|
22
|
+
method: string
|
|
23
|
+
/** A complete Payment-Receipt header value. */
|
|
24
|
+
receipt: string
|
|
25
|
+
state: MppChargeOperationState
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface MppChargeRequest {
|
|
29
|
+
/**
|
|
30
|
+
* Stable provider idempotency key. The adapter must bind every processor
|
|
31
|
+
* operation to this value before it attempts confirmation.
|
|
32
|
+
*/
|
|
33
|
+
operationId: string
|
|
34
|
+
requestId: string
|
|
35
|
+
agentId: string
|
|
36
|
+
consumerId: string
|
|
37
|
+
method: string
|
|
38
|
+
/** Original decoded credential. It is available only on the live request. */
|
|
39
|
+
credential: string
|
|
40
|
+
amount: bigint
|
|
41
|
+
currencyDecimals: number
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export type MppChargeRecoveryResult =
|
|
45
|
+
| MppChargeOperation
|
|
46
|
+
/** `not-found` is final and must fence this operation ID against a later charge. */
|
|
47
|
+
| { operationId: string; state: 'not-found' | 'pending' }
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Immediate-charge lifecycle for a non-BlueprinTEVM MPP method.
|
|
51
|
+
*
|
|
52
|
+
* `confirmPayment` runs after every request denial and before a response or
|
|
53
|
+
* sandbox call. It must use `operationId` as its processor idempotency key,
|
|
54
|
+
* confirm payment, verify final success, and only then return `confirmed`.
|
|
55
|
+
*
|
|
56
|
+
* Every method must also support id-only recovery and an idempotent release.
|
|
57
|
+
* Recovery must inspect the existing processor operation. It must never
|
|
58
|
+
* create a second charge when an acknowledgement is ambiguous.
|
|
59
|
+
*/
|
|
60
|
+
export interface MppChargeLifecycle {
|
|
61
|
+
readonly protocolVersion: typeof MPP_CHARGE_PROTOCOL_VERSION
|
|
62
|
+
confirmPayment(request: MppChargeRequest): Promise<MppChargeOperation>
|
|
63
|
+
releasePayment(operation: MppChargeOperation, reason: string): Promise<MppChargeOperation>
|
|
64
|
+
recoverPayment(operationId: string): Promise<MppChargeRecoveryResult>
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Stable gateway identity. Neither the credential nor adapter identity is persisted. */
|
|
68
|
+
export async function mppPaymentOperationId(
|
|
69
|
+
method: string,
|
|
70
|
+
paymentIdentity: string,
|
|
71
|
+
): Promise<string> {
|
|
72
|
+
const normalizedMethod = method.trim().toLowerCase()
|
|
73
|
+
if (!normalizedMethod || normalizedMethod.length > 128) {
|
|
74
|
+
throw new Error('MPP payment method is invalid')
|
|
75
|
+
}
|
|
76
|
+
if (!paymentIdentity || paymentIdentity.length > 8192) {
|
|
77
|
+
throw new Error('MPP payment identity is invalid')
|
|
78
|
+
}
|
|
79
|
+
const digest = await globalThis.crypto.subtle.digest(
|
|
80
|
+
'SHA-256',
|
|
81
|
+
new TextEncoder().encode(`${normalizedMethod}\0${paymentIdentity}`),
|
|
82
|
+
)
|
|
83
|
+
const fingerprint = [...new Uint8Array(digest)]
|
|
84
|
+
.map((byte) => byte.toString(16).padStart(2, '0'))
|
|
85
|
+
.join('')
|
|
86
|
+
return `mpp:${normalizedMethod}:${fingerprint}`
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function assertMppChargeOperation(
|
|
90
|
+
operation: MppChargeOperation,
|
|
91
|
+
expected: { operationId: string; requestId: string; method: string },
|
|
92
|
+
allowedStates: readonly MppChargeOperationState[],
|
|
93
|
+
requireReceipt = true,
|
|
94
|
+
): void {
|
|
95
|
+
if (operation.protocolVersion !== MPP_CHARGE_PROTOCOL_VERSION) {
|
|
96
|
+
throw new Error('MPP charge operation protocol version mismatch')
|
|
97
|
+
}
|
|
98
|
+
if (operation.operationId !== expected.operationId) {
|
|
99
|
+
throw new Error('MPP charge operation id mismatch')
|
|
100
|
+
}
|
|
101
|
+
if (operation.acquiredByRequestId !== expected.requestId) {
|
|
102
|
+
throw new Error('MPP charge operation request owner mismatch')
|
|
103
|
+
}
|
|
104
|
+
if (operation.method.toLowerCase() !== expected.method.toLowerCase()) {
|
|
105
|
+
throw new Error('MPP charge operation method mismatch')
|
|
106
|
+
}
|
|
107
|
+
if (!allowedStates.includes(operation.state)) {
|
|
108
|
+
throw new Error(`MPP charge operation is in invalid state ${operation.state}`)
|
|
109
|
+
}
|
|
110
|
+
if (requireReceipt && (
|
|
111
|
+
!operation.receipt ||
|
|
112
|
+
operation.receipt.length > 8192 ||
|
|
113
|
+
/[^\x20-\x7e]/.test(operation.receipt)
|
|
114
|
+
)) {
|
|
115
|
+
throw new Error('MPP charge operation has an invalid payment receipt')
|
|
116
|
+
}
|
|
117
|
+
}
|