@tangle-network/agent-gateway 0.7.1 → 0.8.1
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 +90 -3
- package/dist/chunk-C7Z2BRYV.js +5693 -0
- package/dist/chunk-C7Z2BRYV.js.map +1 -0
- 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/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-oQ58UakD.d.ts} +447 -172
- 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 +468 -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 +424 -0
- package/src/dispatch-settlement.ts +139 -0
- package/src/dispatch-types.ts +84 -0
- package/src/dispatch.ts +35 -483
- package/src/index.ts +59 -1
- package/src/middleware.ts +339 -35
- 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 +188 -49
- package/src/verify.ts +240 -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,15 +6,30 @@ 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
|
-
import type {
|
|
17
|
-
|
|
26
|
+
import type {
|
|
27
|
+
ChatCompletionChunk,
|
|
28
|
+
ChatCompletionRequest,
|
|
29
|
+
CreateAgentGatewayConfig,
|
|
30
|
+
GatewayConfig,
|
|
31
|
+
} from './types'
|
|
32
|
+
import { isApiKeyAuthEnabled, isMppAuthEnabled, isX402AuthEnabled } from './verify'
|
|
18
33
|
|
|
19
34
|
/**
|
|
20
35
|
* Create a Hono router that serves the agent gateway.
|
|
@@ -26,15 +41,136 @@ import { isApiKeyAuthEnabled, isMppAuthEnabled } from './verify'
|
|
|
26
41
|
* GET /:slug/chat/completions — agent discovery metadata (Tangle-native shape)
|
|
27
42
|
* POST /:slug/chat/completions — OpenAI-compatible chat endpoint (paid)
|
|
28
43
|
*/
|
|
29
|
-
export function createAgentGateway(
|
|
44
|
+
export function createAgentGateway(inputConfig: CreateAgentGatewayConfig) {
|
|
45
|
+
let config: GatewayConfig = inputConfig.x402
|
|
46
|
+
? inputConfig
|
|
47
|
+
: {
|
|
48
|
+
...inputConfig,
|
|
49
|
+
x402: { operatorAddress: '', chainId: 0 },
|
|
50
|
+
}
|
|
30
51
|
// Production gateways must verify x402 signatures. Tests and local
|
|
31
52
|
// dev can opt into the explicit demo path.
|
|
32
|
-
if (!config
|
|
53
|
+
if (!isX402AuthEnabled(config) && !config.verifyApiKey) {
|
|
33
54
|
throw new Error(
|
|
34
|
-
'createAgentGateway:
|
|
55
|
+
'createAgentGateway: verifySigner is required in production unless verifyApiKey is configured; ' +
|
|
56
|
+
'configure x402.verifySigner or verifyApiKey. ' +
|
|
35
57
|
'For tests, set x402.demoMode: true explicitly.',
|
|
36
58
|
)
|
|
37
59
|
}
|
|
60
|
+
const maxOutputTokens = config.maxOutputTokens ?? 4096
|
|
61
|
+
const defaultOutputTokens = config.defaultOutputTokens ?? 1024
|
|
62
|
+
if (!Number.isSafeInteger(maxOutputTokens) || maxOutputTokens <= 0) {
|
|
63
|
+
throw new Error('createAgentGateway: maxOutputTokens must be a positive integer')
|
|
64
|
+
}
|
|
65
|
+
if (
|
|
66
|
+
!Number.isSafeInteger(defaultOutputTokens) ||
|
|
67
|
+
defaultOutputTokens <= 0 ||
|
|
68
|
+
defaultOutputTokens > maxOutputTokens
|
|
69
|
+
) {
|
|
70
|
+
throw new Error(
|
|
71
|
+
'createAgentGateway: defaultOutputTokens must be a positive integer no greater than maxOutputTokens',
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
if (
|
|
75
|
+
config.x402.currencyDecimals !== undefined &&
|
|
76
|
+
(!Number.isInteger(config.x402.currencyDecimals) ||
|
|
77
|
+
config.x402.currencyDecimals < 0 ||
|
|
78
|
+
config.x402.currencyDecimals > 18)
|
|
79
|
+
) {
|
|
80
|
+
throw new Error('createAgentGateway: x402.currencyDecimals must be an integer between 0 and 18')
|
|
81
|
+
}
|
|
82
|
+
const executionBudget = config.executionBudget
|
|
83
|
+
for (const [name, value] of [
|
|
84
|
+
['maxReasoningTokens', executionBudget?.maxReasoningTokens ?? maxOutputTokens],
|
|
85
|
+
['maxToolTokens', executionBudget?.maxToolTokens ?? maxOutputTokens],
|
|
86
|
+
['maxToolCalls', executionBudget?.maxToolCalls ?? 8],
|
|
87
|
+
] as const) {
|
|
88
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
89
|
+
throw new Error(`createAgentGateway: executionBudget.${name} must be a non-negative safe integer`)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (
|
|
93
|
+
executionBudget?.maxProviderCostUsd !== undefined &&
|
|
94
|
+
(!Number.isFinite(executionBudget.maxProviderCostUsd) || executionBudget.maxProviderCostUsd < 0)
|
|
95
|
+
) {
|
|
96
|
+
throw new Error('createAgentGateway: executionBudget.maxProviderCostUsd must be finite and non-negative')
|
|
97
|
+
}
|
|
98
|
+
if (config.x402.paymentOperations && config.x402.paymentProtocolVersion === undefined) {
|
|
99
|
+
throw new Error('createAgentGateway: paymentProtocolVersion must be explicit when durable payment operations are configured')
|
|
100
|
+
}
|
|
101
|
+
if (
|
|
102
|
+
config.x402.authorizePayment &&
|
|
103
|
+
config.x402.paymentProtocolVersion !== 2 &&
|
|
104
|
+
!config.x402.demoMode
|
|
105
|
+
) {
|
|
106
|
+
throw new Error(
|
|
107
|
+
'createAgentGateway: production x402 version 1 cannot use authorizePayment; ' +
|
|
108
|
+
'use paymentProtocolVersion: 2 with paymentOperations for durable payment ownership',
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
if (config.x402.paymentProtocolVersion === 2 &&
|
|
112
|
+
(!config.x402.paymentOperations || config.x402.paymentOperations.protocolVersion !== 2)) {
|
|
113
|
+
throw new Error('createAgentGateway: payment protocol version 2 requires durable payment operations')
|
|
114
|
+
}
|
|
115
|
+
if (config.x402.paymentProtocolVersion === 1 && config.x402.paymentOperations) {
|
|
116
|
+
throw new Error('createAgentGateway: version 1 cannot be combined with version 2 payment operations')
|
|
117
|
+
}
|
|
118
|
+
if (
|
|
119
|
+
config.a2a?.pushStore &&
|
|
120
|
+
!config.x402.demoMode &&
|
|
121
|
+
(!config.a2a.webhookSecret || config.a2a.webhookSecret.trim().length === 0)
|
|
122
|
+
) {
|
|
123
|
+
throw new Error('createAgentGateway: production A2A push requires a webhookSecret')
|
|
124
|
+
}
|
|
125
|
+
const mppMethod = (config.mpp?.method ?? 'blueprintevm').toLowerCase()
|
|
126
|
+
if (config.mpp?.authenticateCredential !== undefined &&
|
|
127
|
+
typeof config.mpp.authenticateCredential !== 'function') {
|
|
128
|
+
throw new Error('createAgentGateway: mpp.authenticateCredential must be a function')
|
|
129
|
+
}
|
|
130
|
+
if (config.mpp?.verifySigner !== undefined && typeof config.mpp.verifySigner !== 'function') {
|
|
131
|
+
throw new Error('createAgentGateway: mpp.verifySigner must be a function')
|
|
132
|
+
}
|
|
133
|
+
const mppAuthenticator = typeof config.mpp?.authenticateCredential === 'function'
|
|
134
|
+
? config.mpp.authenticateCredential
|
|
135
|
+
: typeof config.mpp?.verifySigner === 'function'
|
|
136
|
+
? config.mpp.verifySigner
|
|
137
|
+
: undefined
|
|
138
|
+
if (config.mpp?.charge && config.mpp.charge.protocolVersion !== 1) {
|
|
139
|
+
throw new Error('createAgentGateway: unsupported MPP charge lifecycle version')
|
|
140
|
+
}
|
|
141
|
+
if (
|
|
142
|
+
config.mpp?.charge &&
|
|
143
|
+
mppMethod !== 'blueprintevm' &&
|
|
144
|
+
!mppAuthenticator
|
|
145
|
+
) {
|
|
146
|
+
throw new Error('createAgentGateway: generic MPP methods require credential authentication')
|
|
147
|
+
}
|
|
148
|
+
if (
|
|
149
|
+
config.mpp &&
|
|
150
|
+
mppMethod !== 'blueprintevm' &&
|
|
151
|
+
mppAuthenticator &&
|
|
152
|
+
!config.mpp.charge
|
|
153
|
+
) {
|
|
154
|
+
throw new Error('createAgentGateway: generic MPP methods require a charge lifecycle')
|
|
155
|
+
}
|
|
156
|
+
if (config.mpp && mppMethod !== 'blueprintevm' && !mppAuthenticator) {
|
|
157
|
+
throw new Error('createAgentGateway: generic MPP methods require credential authentication')
|
|
158
|
+
}
|
|
159
|
+
if (config.nonceStore && !isAtomicNonceStore(config.nonceStore)) {
|
|
160
|
+
throw new Error('createAgentGateway: durable payment ownership requires an atomic nonce store')
|
|
161
|
+
}
|
|
162
|
+
const needsRecovery = config.x402.paymentProtocolVersion === 2 ||
|
|
163
|
+
(mppMethod !== 'blueprintevm' && config.mpp?.charge !== undefined)
|
|
164
|
+
if (needsRecovery && !config.paymentRecovery) {
|
|
165
|
+
if (!config.x402.demoMode) {
|
|
166
|
+
throw new Error('createAgentGateway: durable payment recovery is required in production')
|
|
167
|
+
}
|
|
168
|
+
config = {
|
|
169
|
+
...config,
|
|
170
|
+
paymentRecovery: { store: new MemoryPaymentRecoveryStore() },
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (config.paymentRecovery) assertPaymentRecoveryConfig(config.paymentRecovery)
|
|
38
174
|
const gw = new Hono()
|
|
39
175
|
const rateLimitStore: RateLimitStore = config.rateLimitStore ?? new MemoryRateLimitStore()
|
|
40
176
|
const state: GatewayState = {
|
|
@@ -43,6 +179,12 @@ export function createAgentGateway(config: GatewayConfig) {
|
|
|
43
179
|
globalRateLimit: config.rateLimit ?? { limit: 60, windowSeconds: 60 },
|
|
44
180
|
requiredScope: config.requiredScope ?? 'chat',
|
|
45
181
|
maxLen: config.maxMessageLength ?? 8000,
|
|
182
|
+
maxOutputTokens,
|
|
183
|
+
defaultOutputTokens,
|
|
184
|
+
maxReasoningTokens: config.executionBudget?.maxReasoningTokens ?? maxOutputTokens,
|
|
185
|
+
maxToolTokens: config.executionBudget?.maxToolTokens ?? maxOutputTokens,
|
|
186
|
+
maxToolCalls: config.executionBudget?.maxToolCalls ?? 8,
|
|
187
|
+
maxProviderCostUsd: config.executionBudget?.maxProviderCostUsd,
|
|
46
188
|
obs: config.observer,
|
|
47
189
|
}
|
|
48
190
|
const obs: GatewayObserver | undefined = state.obs
|
|
@@ -54,14 +196,15 @@ export function createAgentGateway(config: GatewayConfig) {
|
|
|
54
196
|
const agent = await config.resolveAgent(slug)
|
|
55
197
|
if (!agent || !agent.enabled) return c.json({ error: 'Agent not found or not published' }, 404)
|
|
56
198
|
|
|
57
|
-
const paymentMethods: Array<Record<string, unknown>> = [
|
|
58
|
-
|
|
199
|
+
const paymentMethods: Array<Record<string, unknown>> = []
|
|
200
|
+
if (isX402AuthEnabled(config)) {
|
|
201
|
+
paymentMethods.push({
|
|
59
202
|
type: 'x402',
|
|
60
203
|
operator: config.x402.operatorAddress,
|
|
61
204
|
chain_id: config.x402.chainId,
|
|
62
205
|
credits_contract: config.x402.creditsAddress,
|
|
63
|
-
}
|
|
64
|
-
|
|
206
|
+
})
|
|
207
|
+
}
|
|
65
208
|
if (isMppAuthEnabled(config)) {
|
|
66
209
|
paymentMethods.push({
|
|
67
210
|
type: 'mpp',
|
|
@@ -69,7 +212,9 @@ export function createAgentGateway(config: GatewayConfig) {
|
|
|
69
212
|
method: config.mpp!.method ?? 'blueprintevm',
|
|
70
213
|
})
|
|
71
214
|
}
|
|
72
|
-
if (isApiKeyAuthEnabled(config))
|
|
215
|
+
if (isApiKeyAuthEnabled(config)) {
|
|
216
|
+
paymentMethods.push({ type: 'api_key', prefix: config.apiKeyPrefix ?? 'sk_agent_' })
|
|
217
|
+
}
|
|
73
218
|
|
|
74
219
|
return c.json({
|
|
75
220
|
slug: agent.slug,
|
|
@@ -125,9 +270,62 @@ export function createAgentGateway(config: GatewayConfig) {
|
|
|
125
270
|
)
|
|
126
271
|
}
|
|
127
272
|
|
|
128
|
-
const guard = await authenticateAndGuard(
|
|
273
|
+
const guard = await authenticateAndGuard(
|
|
274
|
+
c,
|
|
275
|
+
slug,
|
|
276
|
+
body.messages,
|
|
277
|
+
config,
|
|
278
|
+
state,
|
|
279
|
+
body.max_tokens,
|
|
280
|
+
)
|
|
129
281
|
if (guard instanceof Response) return guard
|
|
130
282
|
const authz = guard
|
|
283
|
+
try {
|
|
284
|
+
await claimPayment(authz, config, state)
|
|
285
|
+
} catch (error) {
|
|
286
|
+
const replayedGenericMpp = error instanceof PaymentRecoveryReplayError &&
|
|
287
|
+
authz.paymentMethod === 'mpp' &&
|
|
288
|
+
authz.mppMethod !== 'blueprintevm'
|
|
289
|
+
try {
|
|
290
|
+
await releasePayment(authz, config, 'payment authorization failed')
|
|
291
|
+
} catch (releaseError) {
|
|
292
|
+
console.error(
|
|
293
|
+
`[agent-gateway] payment release failed for ${authz.requestId}:`,
|
|
294
|
+
releaseError instanceof Error ? releaseError.message : String(releaseError),
|
|
295
|
+
)
|
|
296
|
+
}
|
|
297
|
+
await obs?.onAuthFailure?.(
|
|
298
|
+
{
|
|
299
|
+
requestId: authz.requestId,
|
|
300
|
+
agentSlug: authz.agent.slug,
|
|
301
|
+
startMs: authz.startMs,
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
method: authz.paymentMethod,
|
|
305
|
+
code: replayedGenericMpp ? 'invalid_mpp_credential' : 'payment_authorization_failed',
|
|
306
|
+
httpStatus: replayedGenericMpp ? 401 : 402,
|
|
307
|
+
},
|
|
308
|
+
)
|
|
309
|
+
const status = replayedGenericMpp ? 401 : 402
|
|
310
|
+
return c.json(
|
|
311
|
+
{
|
|
312
|
+
error: {
|
|
313
|
+
message: replayedGenericMpp ? 'Invalid Payment credential' : 'Payment authorization failed',
|
|
314
|
+
type: replayedGenericMpp ? 'authentication_error' : 'payment_required',
|
|
315
|
+
code: replayedGenericMpp ? 'invalid_mpp_credential' : 'payment_authorization_failed',
|
|
316
|
+
},
|
|
317
|
+
},
|
|
318
|
+
replayedGenericMpp
|
|
319
|
+
? {
|
|
320
|
+
status,
|
|
321
|
+
headers: {
|
|
322
|
+
'WWW-Authenticate': `Payment realm="${config.mpp!.realm}", method="${config.mpp!.method ?? 'blueprintevm'}"`,
|
|
323
|
+
'X-Request-Id': authz.requestId,
|
|
324
|
+
},
|
|
325
|
+
}
|
|
326
|
+
: { status, headers: { 'X-Payment-Required': 'spendauth', 'X-Request-Id': authz.requestId } },
|
|
327
|
+
)
|
|
328
|
+
}
|
|
131
329
|
|
|
132
330
|
return streamChatCompletions(c, authz, config, obs)
|
|
133
331
|
})
|
|
@@ -139,13 +337,37 @@ export function createAgentGateway(config: GatewayConfig) {
|
|
|
139
337
|
// regardless of which protocol the caller used.
|
|
140
338
|
const taskStore = config.a2a?.taskStore ?? new InMemoryTaskStore()
|
|
141
339
|
const pushStore = config.a2a?.pushStore
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
340
|
+
try {
|
|
341
|
+
const a2a = createA2AHandlers({ config, state, taskStore, pushStore })
|
|
342
|
+
gw.get('/:slug/.well-known/agent.json', a2a.handleAgentCard)
|
|
343
|
+
gw.post('/:slug', a2a.handleJsonRpc)
|
|
344
|
+
} catch (error) {
|
|
345
|
+
// An older custom store must not take down the OpenAI surface. Keep the
|
|
346
|
+
// A2A surface unavailable until its owner supplies atomic methods.
|
|
347
|
+
console.error(
|
|
348
|
+
'[agent-gateway] A2A is unavailable until its task store is upgraded:',
|
|
349
|
+
error instanceof Error ? error.message : String(error),
|
|
350
|
+
)
|
|
351
|
+
const unavailable = (c: import('hono').Context) => c.json(
|
|
352
|
+
{ error: 'A2A task persistence is not configured for concurrent workers' },
|
|
353
|
+
503,
|
|
354
|
+
)
|
|
355
|
+
gw.get('/:slug/.well-known/agent.json', unavailable)
|
|
356
|
+
gw.post('/:slug', unavailable)
|
|
357
|
+
}
|
|
145
358
|
|
|
146
359
|
return gw
|
|
147
360
|
}
|
|
148
361
|
|
|
362
|
+
// Consumers that bind the gateway through a package boundary can fail closed
|
|
363
|
+
// when an old binary ignores the version 2 operation contract.
|
|
364
|
+
Object.assign(createAgentGateway, { paymentProtocolVersion: 2 as const })
|
|
365
|
+
|
|
366
|
+
/** Public package-boundary version marker for durable payment operations. */
|
|
367
|
+
export namespace createAgentGateway {
|
|
368
|
+
export const paymentProtocolVersion = 2 as const
|
|
369
|
+
}
|
|
370
|
+
|
|
149
371
|
/**
|
|
150
372
|
* Drain the sandbox stream into an OpenAI-shaped SSE response, settle the
|
|
151
373
|
* payment, fire observer hooks. Identical pre-refactor behavior, just lifted
|
|
@@ -158,9 +380,23 @@ function streamChatCompletions(
|
|
|
158
380
|
config: GatewayConfig,
|
|
159
381
|
obs: GatewayObserver | undefined,
|
|
160
382
|
): Response {
|
|
161
|
-
const {
|
|
162
|
-
|
|
163
|
-
|
|
383
|
+
const {
|
|
384
|
+
agent,
|
|
385
|
+
consumerId,
|
|
386
|
+
paymentMethod,
|
|
387
|
+
requestId,
|
|
388
|
+
userMessage,
|
|
389
|
+
rateLimitRemaining,
|
|
390
|
+
maxOutputTokens,
|
|
391
|
+
} = authz
|
|
392
|
+
let outputText = ''
|
|
393
|
+
let usage: import('./types').SandboxUsageReceipt | undefined
|
|
394
|
+
let workObserved = false
|
|
395
|
+
const requestSignal = c.req.raw.signal
|
|
396
|
+
const abortController = new AbortController()
|
|
397
|
+
const abortFromRequest = () => abortController.abort()
|
|
398
|
+
if (requestSignal.aborted) abortFromRequest()
|
|
399
|
+
else requestSignal.addEventListener('abort', abortFromRequest, { once: true })
|
|
164
400
|
const ctx: RequestContext = {
|
|
165
401
|
requestId,
|
|
166
402
|
agentSlug: agent.slug,
|
|
@@ -171,7 +407,8 @@ function streamChatCompletions(
|
|
|
171
407
|
async start(controller) {
|
|
172
408
|
const encoder = new TextEncoder()
|
|
173
409
|
const sendChunk = (delta: string) => {
|
|
174
|
-
|
|
410
|
+
if (controller.desiredSize === null) return
|
|
411
|
+
outputText += delta
|
|
175
412
|
const chunk: ChatCompletionChunk = {
|
|
176
413
|
id: `chatcmpl-${Date.now()}`,
|
|
177
414
|
object: 'chat.completion.chunk',
|
|
@@ -183,10 +420,49 @@ function streamChatCompletions(
|
|
|
183
420
|
}
|
|
184
421
|
|
|
185
422
|
try {
|
|
186
|
-
for await (const
|
|
187
|
-
|
|
423
|
+
for await (const event of dispatchSandboxStreamRich(
|
|
424
|
+
agent,
|
|
425
|
+
userMessage,
|
|
426
|
+
consumerId,
|
|
427
|
+
config,
|
|
428
|
+
abortController.signal,
|
|
429
|
+
authz.threadId,
|
|
430
|
+
maxOutputTokens,
|
|
431
|
+
() => beginPaymentExecution(authz, config),
|
|
432
|
+
authz.paymentOperation !== undefined || authz.mppChargeOperation !== undefined,
|
|
433
|
+
async () => {
|
|
434
|
+
if (authz.paymentRecoveryId) workObserved = true
|
|
435
|
+
await markPaymentExecutionStarted(authz, config)
|
|
436
|
+
},
|
|
437
|
+
authz.executionBudget.maxInputTokens,
|
|
438
|
+
() => renewPaymentExecution(authz, config),
|
|
439
|
+
{
|
|
440
|
+
consumerId,
|
|
441
|
+
paymentMethod,
|
|
442
|
+
keyInfo: authz.keyInfo,
|
|
443
|
+
requestId,
|
|
444
|
+
messages: authz.messages ?? [],
|
|
445
|
+
...(authz.threadId ? { threadId: authz.threadId } : {}),
|
|
446
|
+
},
|
|
447
|
+
)) {
|
|
448
|
+
if (event.kind === 'text') {
|
|
449
|
+
sendChunk(event.delta)
|
|
450
|
+
workObserved = true
|
|
451
|
+
}
|
|
452
|
+
if (event.kind === 'activity') workObserved = true
|
|
453
|
+
if (event.kind === 'usage') usage = event.usage
|
|
188
454
|
}
|
|
189
455
|
|
|
456
|
+
if (!usage) throw new Error('sandbox did not provide a usage receipt')
|
|
457
|
+
|
|
458
|
+
await settleAndRecord(
|
|
459
|
+
agent,
|
|
460
|
+
authz,
|
|
461
|
+
usage,
|
|
462
|
+
config,
|
|
463
|
+
obs,
|
|
464
|
+
)
|
|
465
|
+
|
|
190
466
|
const done: ChatCompletionChunk = {
|
|
191
467
|
id: `chatcmpl-${Date.now()}`,
|
|
192
468
|
object: 'chat.completion.chunk',
|
|
@@ -194,10 +470,10 @@ function streamChatCompletions(
|
|
|
194
470
|
model: agent.slug,
|
|
195
471
|
choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],
|
|
196
472
|
}
|
|
197
|
-
controller.
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
473
|
+
if (controller.desiredSize !== null) {
|
|
474
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify(done)}\n\n`))
|
|
475
|
+
controller.enqueue(encoder.encode('data: [DONE]\n\n'))
|
|
476
|
+
}
|
|
201
477
|
} catch (err) {
|
|
202
478
|
const rawMessage = err instanceof Error ? err.message : String(err)
|
|
203
479
|
// Never expose stack traces / absolute paths from sandbox internals.
|
|
@@ -205,16 +481,37 @@ function streamChatCompletions(
|
|
|
205
481
|
rawMessage.includes('/') || rawMessage.includes('\\')
|
|
206
482
|
? 'Internal agent error'
|
|
207
483
|
: rawMessage
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
484
|
+
try {
|
|
485
|
+
await obs?.onStreamError?.(ctx, { consumerId, errorMessage: rawMessage })
|
|
486
|
+
} catch (observerError) {
|
|
487
|
+
console.error(
|
|
488
|
+
`[agent-gateway] stream observer failed for ${requestId}:`,
|
|
489
|
+
observerError instanceof Error ? observerError.message : String(observerError),
|
|
490
|
+
)
|
|
491
|
+
}
|
|
492
|
+
try {
|
|
493
|
+
await releasePaymentAfterFailure(authz, config, rawMessage, workObserved || usage !== undefined)
|
|
494
|
+
} catch (releaseError) {
|
|
495
|
+
console.error(
|
|
496
|
+
`[agent-gateway] payment release failed for ${authz.requestId}:`,
|
|
497
|
+
releaseError instanceof Error ? releaseError.message : String(releaseError),
|
|
498
|
+
)
|
|
499
|
+
}
|
|
500
|
+
if (!abortController.signal.aborted && controller.desiredSize !== null) {
|
|
501
|
+
controller.enqueue(
|
|
502
|
+
encoder.encode(
|
|
503
|
+
`data: ${JSON.stringify({ error: { message: safeMessage, type: 'server_error' } })}\n\n`,
|
|
504
|
+
),
|
|
505
|
+
)
|
|
506
|
+
}
|
|
214
507
|
} finally {
|
|
215
|
-
|
|
508
|
+
requestSignal.removeEventListener('abort', abortFromRequest)
|
|
509
|
+
if (controller.desiredSize !== null) controller.close()
|
|
216
510
|
}
|
|
217
511
|
},
|
|
512
|
+
cancel() {
|
|
513
|
+
abortController.abort()
|
|
514
|
+
},
|
|
218
515
|
})
|
|
219
516
|
|
|
220
517
|
return new Response(stream, {
|
|
@@ -224,8 +521,15 @@ function streamChatCompletions(
|
|
|
224
521
|
'X-Request-Id': requestId,
|
|
225
522
|
'X-Agent-Slug': agent.slug,
|
|
226
523
|
'X-Agent-Hosting': agent.sandboxEndpoint ? 'sovereign' : 'centralized',
|
|
524
|
+
...(authz.threadId ? { 'X-Tangle-Thread-Id': authz.threadId } : {}),
|
|
227
525
|
'X-Payment-Method': paymentMethod,
|
|
228
|
-
'X-Payment-Settled': paymentMethod === 'x402' ? 'pending' : 'true',
|
|
526
|
+
'X-Payment-Settled': paymentMethod === 'x402' || authz.paymentOperation ? 'pending' : 'true',
|
|
527
|
+
...(authz.mppChargeOperation
|
|
528
|
+
? { 'Payment-Receipt': authz.mppChargeOperation.receipt }
|
|
529
|
+
: {}),
|
|
530
|
+
...(authz.paymentRecoveryId
|
|
531
|
+
? { 'X-Payment-Operation-Id': authz.paymentRecoveryId }
|
|
532
|
+
: {}),
|
|
229
533
|
...(rateLimitRemaining !== undefined
|
|
230
534
|
? { 'X-RateLimit-Remaining': String(rateLimitRemaining) }
|
|
231
535
|
: {}),
|
|
@@ -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
|
+
}
|