@tangle-network/agent-gateway 0.7.0 → 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 +108 -6
- 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 +76 -12
- package/dist/index.js +307 -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-CX2V06cN.d.ts → types-BHISsm7D.d.ts} +423 -166
- package/dist/types.d.ts +2 -1
- package/package.json +1 -1
- package/src/a2a/agent-card.ts +4 -3
- package/src/a2a/execution-fence.ts +162 -0
- package/src/a2a/handler.ts +507 -562
- 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 -462
- package/src/index.ts +64 -2
- package/src/middleware.ts +313 -32
- 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 +153 -42
- package/src/verify.ts +265 -36
- package/dist/chunk-3IKQWFKX.js +0 -1703
- package/dist/chunk-3IKQWFKX.js.map +0 -1
- package/dist/chunk-M7ZJAK4K.js +0 -53
- package/dist/chunk-M7ZJAK4K.js.map +0 -1
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { MppChargeOperation } from './mpp-payment'
|
|
2
|
+
import type { NonceStore } from './nonce-store'
|
|
3
|
+
import type { GatewayObserver } from './observer'
|
|
4
|
+
import type { RateLimitStore } from './rate-limit'
|
|
5
|
+
import type { PaymentOperation } from './payment-operations'
|
|
6
|
+
import type { PaymentSettlementBasis } from './payment-recovery'
|
|
7
|
+
import type {
|
|
8
|
+
AgentMeta,
|
|
9
|
+
ApiKeyInfo,
|
|
10
|
+
PaymentMethod,
|
|
11
|
+
SandboxExecutionBudget,
|
|
12
|
+
SandboxUsageReceipt,
|
|
13
|
+
} from './types'
|
|
14
|
+
|
|
15
|
+
/** Long-lived state shared by all handlers created for one gateway. */
|
|
16
|
+
export interface GatewayState {
|
|
17
|
+
rateLimitStore: RateLimitStore
|
|
18
|
+
nonceStore: NonceStore
|
|
19
|
+
globalRateLimit: { limit: number; windowSeconds: number }
|
|
20
|
+
requiredScope: string
|
|
21
|
+
maxLen: number
|
|
22
|
+
maxOutputTokens: number
|
|
23
|
+
defaultOutputTokens: number
|
|
24
|
+
maxReasoningTokens: number
|
|
25
|
+
maxToolTokens: number
|
|
26
|
+
maxToolCalls: number
|
|
27
|
+
maxProviderCostUsd?: number
|
|
28
|
+
obs?: GatewayObserver
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Successful output from the request authorization pipeline. */
|
|
32
|
+
export interface AuthorizedRequest {
|
|
33
|
+
agent: AgentMeta
|
|
34
|
+
consumerId: string
|
|
35
|
+
paymentMethod: PaymentMethod
|
|
36
|
+
keyInfo: ApiKeyInfo | null
|
|
37
|
+
userMessage: string
|
|
38
|
+
rateLimitRemaining: number | undefined
|
|
39
|
+
requestId: string
|
|
40
|
+
startMs: number
|
|
41
|
+
maxOutputTokens: number
|
|
42
|
+
executionBudget: SandboxExecutionBudget
|
|
43
|
+
requiredPaymentAmount: bigint
|
|
44
|
+
paymentPayload: Record<string, unknown> | null
|
|
45
|
+
paymentNonceKey?: string
|
|
46
|
+
mppMethod?: string
|
|
47
|
+
/** Live generic MPP credential. Never write it to the recovery store. */
|
|
48
|
+
mppCredential?: string
|
|
49
|
+
/** Stable method identity. Persist only its digest. */
|
|
50
|
+
mppPaymentIdentity?: string
|
|
51
|
+
mppChargeOperation?: MppChargeOperation
|
|
52
|
+
paymentOperation?: PaymentOperation
|
|
53
|
+
paymentOperationAcquired?: boolean
|
|
54
|
+
paymentRecoveryId?: string
|
|
55
|
+
/** Unique ownership fence for live or recovery transitions. */
|
|
56
|
+
paymentRecoveryFence?: string
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface PaymentClaimHooks {
|
|
60
|
+
/** Persist the recovery identity before the provider can mutate payment state. */
|
|
61
|
+
onRecoveryPrepared?: (recoveryId: string) => Promise<void>
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export type A2ADispatchEvent =
|
|
65
|
+
| { kind: 'text'; delta: string }
|
|
66
|
+
| { kind: 'input-required'; prompt?: string }
|
|
67
|
+
| { kind: 'activity' }
|
|
68
|
+
| { kind: 'usage'; usage: SandboxUsageReceipt }
|
|
69
|
+
|
|
70
|
+
export interface SettleAndRecordOptions {
|
|
71
|
+
/** Skip attribution after a durable finalization marker confirms it ran. */
|
|
72
|
+
usageAlreadyRecorded?: boolean
|
|
73
|
+
/** Skip provider settlement after an authoritative read found it settled. */
|
|
74
|
+
paymentAlreadySettled?: boolean
|
|
75
|
+
/** Persist the recovery marker after attribution succeeds. */
|
|
76
|
+
onUsageRecorded?: () => Promise<void>
|
|
77
|
+
/** Recovery uses the original quoted ceiling when no receipt arrives. */
|
|
78
|
+
settlementBasis?: PaymentSettlementBasis
|
|
79
|
+
/** Exact base-unit charge selected by the recovery policy. */
|
|
80
|
+
paymentAmount?: bigint
|
|
81
|
+
}
|
package/src/dispatch.ts
CHANGED
|
@@ -1,465 +1,38 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* own protocol's request body into a canonical `messages[]` form + headers,
|
|
5
|
-
* then calls into here for auth → rate-limit → injection filter →
|
|
6
|
-
* authorize → sandbox stream → settle. Keeping the pipeline single-sourced
|
|
7
|
-
* means every protocol surface gets the same security and billing guarantees
|
|
8
|
-
* for free; bugs fixed here fix every wrapper.
|
|
2
|
+
* Stable dispatch surface shared by the OpenAI-compatible and A2A handlers.
|
|
3
|
+
* Each implementation lives in the module that owns its state transitions.
|
|
9
4
|
*/
|
|
10
5
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
requestId: string
|
|
45
|
-
startMs: number
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Resolve the agent, then run the full pre-dispatch pipeline: payment +
|
|
50
|
-
* rate-limit + injection filter + user-message extraction + optional
|
|
51
|
-
* `authorizeConsumer` hook. Returns the success record on the happy path
|
|
52
|
-
* or a fully-formed `Response` (402/404/429/400/403) on any short-circuit.
|
|
53
|
-
*
|
|
54
|
-
* Body parsing is the caller's responsibility — different wire formats
|
|
55
|
-
* (OpenAI chat completions vs A2A JSON-RPC) have different envelopes; both
|
|
56
|
-
* still ultimately produce a `ChatMessage[]`.
|
|
57
|
-
*/
|
|
58
|
-
export async function authenticateAndGuard(
|
|
59
|
-
c: Context,
|
|
60
|
-
slug: string,
|
|
61
|
-
messages: ChatMessage[],
|
|
62
|
-
config: GatewayConfig,
|
|
63
|
-
state: GatewayState,
|
|
64
|
-
): Promise<AuthorizedRequest | Response> {
|
|
65
|
-
const startMs = Date.now()
|
|
66
|
-
const requestId = generateRequestId()
|
|
67
|
-
const ctx: RequestContext = { requestId, agentSlug: slug, startMs }
|
|
68
|
-
await state.obs?.onRequestStart?.(ctx)
|
|
69
|
-
|
|
70
|
-
const agent = await config.resolveAgent(slug)
|
|
71
|
-
if (!agent) {
|
|
72
|
-
return c.json({ error: { message: 'Agent not found', type: 'not_found' } }, 404)
|
|
73
|
-
}
|
|
74
|
-
if (!messages?.length) {
|
|
75
|
-
return c.json(
|
|
76
|
-
{ error: { message: 'messages array required', type: 'invalid_request' } },
|
|
77
|
-
400,
|
|
78
|
-
)
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// Payment / auth.
|
|
82
|
-
const spendAuthHeader = c.req.header('X-Payment-Signature')
|
|
83
|
-
const authHeader = c.req.header('Authorization') ?? ''
|
|
84
|
-
let consumerId: string | null = null
|
|
85
|
-
let paymentMethod: PaymentMethod = 'none'
|
|
86
|
-
let keyInfo: ApiKeyInfo | null = null
|
|
87
|
-
|
|
88
|
-
if (spendAuthHeader) {
|
|
89
|
-
const signer = await verifyX402(spendAuthHeader, config.x402, state.nonceStore)
|
|
90
|
-
if (!signer) {
|
|
91
|
-
await state.obs?.onAuthFailure?.(ctx, {
|
|
92
|
-
method: 'x402',
|
|
93
|
-
code: 'invalid_spend_auth',
|
|
94
|
-
httpStatus: 402,
|
|
95
|
-
})
|
|
96
|
-
return c.json(
|
|
97
|
-
{
|
|
98
|
-
error: {
|
|
99
|
-
message: 'Invalid X-Payment-Signature',
|
|
100
|
-
type: 'authentication_error',
|
|
101
|
-
code: 'invalid_spend_auth',
|
|
102
|
-
},
|
|
103
|
-
},
|
|
104
|
-
{
|
|
105
|
-
status: 402,
|
|
106
|
-
headers: { 'X-Payment-Required': 'spendauth', 'X-Request-Id': requestId },
|
|
107
|
-
},
|
|
108
|
-
)
|
|
109
|
-
}
|
|
110
|
-
consumerId = signer
|
|
111
|
-
paymentMethod = 'x402'
|
|
112
|
-
} else if (config.mpp && authHeader.toLowerCase().startsWith('payment ')) {
|
|
113
|
-
const signer = await verifyMpp(authHeader, config.mpp, config.x402)
|
|
114
|
-
if (!signer) {
|
|
115
|
-
const realm = config.mpp.realm
|
|
116
|
-
const method = config.mpp.method ?? 'blueprintevm'
|
|
117
|
-
await state.obs?.onAuthFailure?.(ctx, {
|
|
118
|
-
method: 'mpp',
|
|
119
|
-
code: 'invalid_mpp_credential',
|
|
120
|
-
httpStatus: 401,
|
|
121
|
-
})
|
|
122
|
-
return c.json(
|
|
123
|
-
{
|
|
124
|
-
error: {
|
|
125
|
-
message: 'Invalid Payment credential',
|
|
126
|
-
type: 'authentication_error',
|
|
127
|
-
code: 'invalid_mpp_credential',
|
|
128
|
-
},
|
|
129
|
-
},
|
|
130
|
-
{
|
|
131
|
-
status: 401,
|
|
132
|
-
headers: {
|
|
133
|
-
'WWW-Authenticate': `Payment realm="${realm}", method="${method}"`,
|
|
134
|
-
'X-Request-Id': requestId,
|
|
135
|
-
},
|
|
136
|
-
},
|
|
137
|
-
)
|
|
138
|
-
}
|
|
139
|
-
consumerId = signer
|
|
140
|
-
paymentMethod = 'mpp'
|
|
141
|
-
} else if (authHeader.startsWith('Bearer ')) {
|
|
142
|
-
const verify = config.verifyApiKey ?? defaultVerifyApiKey
|
|
143
|
-
const key = await verify(authHeader)
|
|
144
|
-
if (!key) {
|
|
145
|
-
await state.obs?.onAuthFailure?.(ctx, {
|
|
146
|
-
method: 'apikey',
|
|
147
|
-
code: 'invalid_api_key',
|
|
148
|
-
httpStatus: 401,
|
|
149
|
-
})
|
|
150
|
-
return c.json(
|
|
151
|
-
{ error: { message: 'Invalid API key', type: 'authentication_error' } },
|
|
152
|
-
{ status: 401, headers: { 'X-Request-Id': requestId } },
|
|
153
|
-
)
|
|
154
|
-
}
|
|
155
|
-
if (key.scopes && key.scopes.length > 0 && !key.scopes.includes(state.requiredScope)) {
|
|
156
|
-
await state.obs?.onAuthFailure?.(ctx, {
|
|
157
|
-
method: 'apikey',
|
|
158
|
-
code: 'insufficient_scope',
|
|
159
|
-
httpStatus: 403,
|
|
160
|
-
})
|
|
161
|
-
return c.json(
|
|
162
|
-
{
|
|
163
|
-
error: {
|
|
164
|
-
message: `API key missing required scope: ${state.requiredScope}`,
|
|
165
|
-
type: 'forbidden',
|
|
166
|
-
code: 'insufficient_scope',
|
|
167
|
-
},
|
|
168
|
-
},
|
|
169
|
-
{ status: 403, headers: { 'X-Request-Id': requestId } },
|
|
170
|
-
)
|
|
171
|
-
}
|
|
172
|
-
consumerId = key.consumerId
|
|
173
|
-
paymentMethod = 'apikey'
|
|
174
|
-
keyInfo = key
|
|
175
|
-
} else {
|
|
176
|
-
await state.obs?.onAuthFailure?.(ctx, {
|
|
177
|
-
method: 'none',
|
|
178
|
-
code: 'payment_required',
|
|
179
|
-
httpStatus: 402,
|
|
180
|
-
})
|
|
181
|
-
const methods: string[] = ['x402']
|
|
182
|
-
if (config.mpp) methods.push('mpp')
|
|
183
|
-
methods.push('api_key')
|
|
184
|
-
const headers: Record<string, string> = {
|
|
185
|
-
'X-Payment-Required': methods.join(', '),
|
|
186
|
-
'X-Request-Id': requestId,
|
|
187
|
-
}
|
|
188
|
-
if (config.mpp) {
|
|
189
|
-
headers['WWW-Authenticate'] =
|
|
190
|
-
`Payment realm="${config.mpp.realm}", method="${config.mpp.method ?? 'blueprintevm'}"`
|
|
191
|
-
}
|
|
192
|
-
return c.json(
|
|
193
|
-
{
|
|
194
|
-
error: {
|
|
195
|
-
message: 'Payment required',
|
|
196
|
-
type: 'payment_required',
|
|
197
|
-
payment_methods: methods,
|
|
198
|
-
x402: {
|
|
199
|
-
operator: config.x402.operatorAddress,
|
|
200
|
-
chain_id: config.x402.chainId,
|
|
201
|
-
credits_address: config.x402.creditsAddress,
|
|
202
|
-
estimated_amount_per_request: '20000',
|
|
203
|
-
},
|
|
204
|
-
...(config.mpp
|
|
205
|
-
? { mpp: { realm: config.mpp.realm, method: config.mpp.method ?? 'blueprintevm' } }
|
|
206
|
-
: {}),
|
|
207
|
-
api_key: {
|
|
208
|
-
purchase_url: config.baseUrl
|
|
209
|
-
? `${config.baseUrl}/agents/${slug}/api-keys`
|
|
210
|
-
: undefined,
|
|
211
|
-
},
|
|
212
|
-
},
|
|
213
|
-
},
|
|
214
|
-
{ status: 402, headers },
|
|
215
|
-
)
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
await state.obs?.onPaymentVerified?.(ctx, {
|
|
219
|
-
method: paymentMethod,
|
|
220
|
-
consumerId: consumerId,
|
|
221
|
-
keyId: keyInfo?.keyId,
|
|
222
|
-
})
|
|
223
|
-
|
|
224
|
-
// Rate limit.
|
|
225
|
-
const effectiveRateLimit = keyInfo?.rateLimitPerMinute
|
|
226
|
-
? { limit: keyInfo.rateLimitPerMinute, windowSeconds: 60 }
|
|
227
|
-
: state.globalRateLimit
|
|
228
|
-
const rl = await checkRateLimit(consumerId, effectiveRateLimit, state.rateLimitStore)
|
|
229
|
-
if (!rl.allowed) {
|
|
230
|
-
await state.obs?.onRateLimited?.(ctx, {
|
|
231
|
-
consumerId: consumerId,
|
|
232
|
-
retryAfterSeconds: rl.retryAfterSeconds ?? 60,
|
|
233
|
-
})
|
|
234
|
-
return c.json(
|
|
235
|
-
{
|
|
236
|
-
error: {
|
|
237
|
-
message: 'Rate limit exceeded',
|
|
238
|
-
type: 'rate_limit_error',
|
|
239
|
-
retry_after: rl.retryAfterSeconds,
|
|
240
|
-
},
|
|
241
|
-
},
|
|
242
|
-
{
|
|
243
|
-
status: 429,
|
|
244
|
-
headers: {
|
|
245
|
-
'Retry-After': String(rl.retryAfterSeconds ?? 60),
|
|
246
|
-
'X-Request-Id': requestId,
|
|
247
|
-
},
|
|
248
|
-
},
|
|
249
|
-
)
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
// Filter consumer messages — strip consumer-side system, length-cap, injection scan.
|
|
253
|
-
const { messages: filtered, injectionWarnings } = filterConsumerMessagesStrict(
|
|
254
|
-
messages,
|
|
255
|
-
state.maxLen,
|
|
256
|
-
)
|
|
257
|
-
if (injectionWarnings.length > 0) {
|
|
258
|
-
await state.obs?.onInjectionDetected?.(ctx, {
|
|
259
|
-
consumerId: consumerId,
|
|
260
|
-
patterns: injectionWarnings,
|
|
261
|
-
blocked: !!config.blockInjection,
|
|
262
|
-
})
|
|
263
|
-
if (config.blockInjection) {
|
|
264
|
-
return c.json(
|
|
265
|
-
{
|
|
266
|
-
error: {
|
|
267
|
-
message: 'Request rejected: potential prompt injection detected',
|
|
268
|
-
type: 'content_policy_violation',
|
|
269
|
-
},
|
|
270
|
-
},
|
|
271
|
-
{ status: 400, headers: { 'X-Request-Id': requestId } },
|
|
272
|
-
)
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
const userMessage = filtered
|
|
277
|
-
.filter((m) => m.role === 'user')
|
|
278
|
-
.map((m) => m.content)
|
|
279
|
-
.join('\n\n')
|
|
280
|
-
if (!userMessage) {
|
|
281
|
-
return c.json(
|
|
282
|
-
{ error: { message: 'No user message provided', type: 'invalid_request' } },
|
|
283
|
-
400,
|
|
284
|
-
)
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
if (config.authorizeConsumer) {
|
|
288
|
-
const authz = await config.authorizeConsumer(agent, {
|
|
289
|
-
method: paymentMethod,
|
|
290
|
-
consumerId: consumerId,
|
|
291
|
-
keyId: keyInfo?.keyId,
|
|
292
|
-
requestId,
|
|
293
|
-
})
|
|
294
|
-
if (!authz.allow) {
|
|
295
|
-
return c.json(
|
|
296
|
-
{
|
|
297
|
-
error: {
|
|
298
|
-
message: authz.reason,
|
|
299
|
-
type: 'authorization_denied',
|
|
300
|
-
code: authz.code,
|
|
301
|
-
},
|
|
302
|
-
},
|
|
303
|
-
{ status: 403, headers: { 'X-Request-Id': requestId } },
|
|
304
|
-
)
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
return {
|
|
309
|
-
agent,
|
|
310
|
-
consumerId,
|
|
311
|
-
paymentMethod,
|
|
312
|
-
keyInfo,
|
|
313
|
-
userMessage,
|
|
314
|
-
rateLimitRemaining: rl.remaining,
|
|
315
|
-
requestId,
|
|
316
|
-
startMs,
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
/**
|
|
321
|
-
* Yield the inner sandbox's response as text deltas, applying the
|
|
322
|
-
* system-prompt redaction filter on each delta so leakage of the agent's
|
|
323
|
-
* system prompt back through the model's output is suppressed identically
|
|
324
|
-
* whether the caller is on the OpenAI-compat path or A2A.
|
|
325
|
-
*
|
|
326
|
-
* Aborts when `signal` fires (used by A2A `tasks/cancel`).
|
|
327
|
-
*/
|
|
328
|
-
export async function* dispatchSandboxStream(
|
|
329
|
-
agent: AgentMeta,
|
|
330
|
-
userMessage: string,
|
|
331
|
-
consumerId: string,
|
|
332
|
-
config: GatewayConfig,
|
|
333
|
-
signal?: AbortSignal,
|
|
334
|
-
sessionId?: string,
|
|
335
|
-
): AsyncIterable<string> {
|
|
336
|
-
for await (const event of dispatchSandboxStreamRich(
|
|
337
|
-
agent,
|
|
338
|
-
userMessage,
|
|
339
|
-
consumerId,
|
|
340
|
-
config,
|
|
341
|
-
signal,
|
|
342
|
-
sessionId,
|
|
343
|
-
)) {
|
|
344
|
-
if (event.kind === 'text') yield event.delta
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
/**
|
|
349
|
-
* A2A-shaped dispatch event. Distinguishes text deltas from sandbox-signalled
|
|
350
|
-
* pause-for-input events. The A2A handler uses this richer variant so it can
|
|
351
|
-
* emit `input-required` status updates; the OpenAI-compat path consumes the
|
|
352
|
-
* text-only `dispatchSandboxStream` adapter above.
|
|
353
|
-
*/
|
|
354
|
-
export type A2ADispatchEvent =
|
|
355
|
-
| { kind: 'text'; delta: string }
|
|
356
|
-
| { kind: 'input-required'; prompt?: string }
|
|
357
|
-
|
|
358
|
-
/**
|
|
359
|
-
* Like `dispatchSandboxStream` but yields a discriminated union so callers can
|
|
360
|
-
* react to `input-required` signals from the sandbox. The sandbox opts in by
|
|
361
|
-
* emitting `{ type: 'input-required', data: { inputRequired: { prompt? } } }`
|
|
362
|
-
* (or by setting `data.inputRequired` on any event); sandboxes that don't
|
|
363
|
-
* emit such events see identical behavior.
|
|
364
|
-
*
|
|
365
|
-
* `sessionId` defaults to `consumer:<id>` matching the existing single-turn
|
|
366
|
-
* path; multi-turn continuations pass an explicit `taskId` so the sandbox can
|
|
367
|
-
* keep per-task conversation memory.
|
|
368
|
-
*/
|
|
369
|
-
export async function* dispatchSandboxStreamRich(
|
|
370
|
-
agent: AgentMeta,
|
|
371
|
-
userMessage: string,
|
|
372
|
-
consumerId: string,
|
|
373
|
-
config: GatewayConfig,
|
|
374
|
-
signal?: AbortSignal,
|
|
375
|
-
sessionId?: string,
|
|
376
|
-
): AsyncIterable<A2ADispatchEvent> {
|
|
377
|
-
const box = await config.getSandbox(agent)
|
|
378
|
-
const promptStream = box.streamPrompt(userMessage, {
|
|
379
|
-
sessionId: sessionId ?? `consumer:${consumerId}`,
|
|
380
|
-
systemPrompt: agent.systemPrompt,
|
|
381
|
-
})
|
|
382
|
-
for await (const event of promptStream) {
|
|
383
|
-
if (signal?.aborted) return
|
|
384
|
-
if (
|
|
385
|
-
event.type === 'message.part.updated' &&
|
|
386
|
-
event.data?.part?.type === 'text' &&
|
|
387
|
-
event.data.delta
|
|
388
|
-
) {
|
|
389
|
-
yield {
|
|
390
|
-
kind: 'text',
|
|
391
|
-
delta: redactSystemPromptFromOutput(event.data.delta, agent.systemPrompt),
|
|
392
|
-
}
|
|
393
|
-
continue
|
|
394
|
-
}
|
|
395
|
-
if (event.type === 'input-required' || event.data?.inputRequired) {
|
|
396
|
-
yield { kind: 'input-required', prompt: event.data?.inputRequired?.prompt }
|
|
397
|
-
// Terminal for the sandbox stream — sandbox SHOULD stop emitting until
|
|
398
|
-
// the gateway dispatches a continuation message with the new user input.
|
|
399
|
-
return
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
/**
|
|
405
|
-
* Record usage event + settle payment + invoke the observer. Both wire
|
|
406
|
-
* formats call this once their stream has drained, so settlement happens
|
|
407
|
-
* exactly once per request regardless of protocol.
|
|
408
|
-
*/
|
|
409
|
-
export async function settleAndRecord(
|
|
410
|
-
agent: AgentMeta,
|
|
411
|
-
authz: AuthorizedRequest,
|
|
412
|
-
inputTokens: number,
|
|
413
|
-
outputTokens: number,
|
|
414
|
-
config: GatewayConfig,
|
|
415
|
-
obs: GatewayObserver | undefined,
|
|
416
|
-
): Promise<void> {
|
|
417
|
-
const totalCost = (inputTokens + outputTokens) * agent.pricePerTokenUsd
|
|
418
|
-
const ownerEarned = totalCost * (1 - agent.platformFeePercent)
|
|
419
|
-
const platformFee = totalCost * agent.platformFeePercent
|
|
420
|
-
const usageEvent = {
|
|
421
|
-
requestId: authz.requestId,
|
|
422
|
-
agentId: agent.id,
|
|
423
|
-
agentSlug: agent.slug,
|
|
424
|
-
consumerId: authz.consumerId,
|
|
425
|
-
paymentMethod: authz.paymentMethod,
|
|
426
|
-
inputTokens,
|
|
427
|
-
outputTokens,
|
|
428
|
-
totalCostUsd: totalCost,
|
|
429
|
-
ownerEarnedUsd: ownerEarned,
|
|
430
|
-
platformFeeUsd: platformFee,
|
|
431
|
-
durationMs: Date.now() - authz.startMs,
|
|
432
|
-
}
|
|
433
|
-
await config.recordUsage(usageEvent)
|
|
434
|
-
const ctx: RequestContext = {
|
|
435
|
-
requestId: authz.requestId,
|
|
436
|
-
agentSlug: agent.slug,
|
|
437
|
-
startMs: authz.startMs,
|
|
438
|
-
}
|
|
439
|
-
await obs?.onRequestComplete?.(ctx, usageEvent)
|
|
440
|
-
if (config.settlePayment) {
|
|
441
|
-
await config
|
|
442
|
-
.settlePayment(
|
|
443
|
-
{
|
|
444
|
-
method: authz.paymentMethod,
|
|
445
|
-
consumerId: authz.consumerId,
|
|
446
|
-
requestId: authz.requestId,
|
|
447
|
-
},
|
|
448
|
-
totalCost,
|
|
449
|
-
)
|
|
450
|
-
.catch(async (err) => {
|
|
451
|
-
const msg = err instanceof Error ? err.message : String(err)
|
|
452
|
-
console.error(`[agent-gateway] settlement failed for ${authz.consumerId}: ${msg}`)
|
|
453
|
-
await obs?.onSettlementError?.(ctx, {
|
|
454
|
-
consumerId: authz.consumerId,
|
|
455
|
-
method: authz.paymentMethod,
|
|
456
|
-
errorMessage: msg,
|
|
457
|
-
})
|
|
458
|
-
})
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
/** Token estimate matching the existing chat-completions handler (4 chars ≈ 1 token). */
|
|
463
|
-
export function estimateTokens(text: string): number {
|
|
464
|
-
return Math.ceil(text.length / 4)
|
|
465
|
-
}
|
|
6
|
+
export type {
|
|
7
|
+
A2ADispatchEvent,
|
|
8
|
+
AuthorizedRequest,
|
|
9
|
+
GatewayState,
|
|
10
|
+
PaymentClaimHooks,
|
|
11
|
+
SettleAndRecordOptions,
|
|
12
|
+
} from './dispatch-types'
|
|
13
|
+
|
|
14
|
+
export {
|
|
15
|
+
estimateBillableInputTokens,
|
|
16
|
+
estimateTokens,
|
|
17
|
+
maximumBillableInputTokens,
|
|
18
|
+
requiredX402Amount,
|
|
19
|
+
} from './dispatch-pricing'
|
|
20
|
+
|
|
21
|
+
export { authenticateAndGuard } from './dispatch-authorization'
|
|
22
|
+
|
|
23
|
+
export {
|
|
24
|
+
beginPaymentExecution,
|
|
25
|
+
claimPayment,
|
|
26
|
+
markPaymentExecutionStarted,
|
|
27
|
+
reclaimPayment,
|
|
28
|
+
releasePayment,
|
|
29
|
+
releasePaymentAfterFailure,
|
|
30
|
+
renewPaymentExecution,
|
|
31
|
+
} from './dispatch-payment'
|
|
32
|
+
|
|
33
|
+
export {
|
|
34
|
+
dispatchSandboxStream,
|
|
35
|
+
dispatchSandboxStreamRich,
|
|
36
|
+
} from './dispatch-sandbox'
|
|
37
|
+
|
|
38
|
+
export { settleAndRecord } from './dispatch-settlement'
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,58 @@
|
|
|
1
1
|
export { createAgentGateway } from './middleware'
|
|
2
|
-
export {
|
|
2
|
+
export { reclaimPayment } from './dispatch'
|
|
3
|
+
export {
|
|
4
|
+
recoverPayment,
|
|
5
|
+
recoverPayments,
|
|
6
|
+
type RecoverPaymentOptions,
|
|
7
|
+
type RecoverPaymentsOptions,
|
|
8
|
+
type PaymentRecoveryRun,
|
|
9
|
+
} from './payment-recovery-worker'
|
|
10
|
+
export {
|
|
11
|
+
verifyX402,
|
|
12
|
+
verifyMpp,
|
|
13
|
+
verifyMppCredential,
|
|
14
|
+
defaultVerifyApiKey,
|
|
15
|
+
isApiKeyAuthEnabled,
|
|
16
|
+
isMppAuthEnabled,
|
|
17
|
+
mppReplayNonceKey,
|
|
18
|
+
mppPaymentCredential,
|
|
19
|
+
type VerifiedMppCredential,
|
|
20
|
+
} from './verify'
|
|
21
|
+
export {
|
|
22
|
+
MPP_CHARGE_PROTOCOL_VERSION,
|
|
23
|
+
mppPaymentOperationId,
|
|
24
|
+
type MppAuthenticatedCredential,
|
|
25
|
+
type MppChargeLifecycle,
|
|
26
|
+
type MppChargeOperation,
|
|
27
|
+
type MppChargeOperationState,
|
|
28
|
+
type MppChargeRecoveryResult,
|
|
29
|
+
type MppChargeRequest,
|
|
30
|
+
} from './mpp-payment'
|
|
31
|
+
export {
|
|
32
|
+
PAYMENT_RECOVERY_VERSION,
|
|
33
|
+
MemoryPaymentRecoveryStore,
|
|
34
|
+
PaymentRecoveryFenceError,
|
|
35
|
+
type PaymentRecoveryAttribution,
|
|
36
|
+
type PaymentRecoveryConfig,
|
|
37
|
+
type PaymentRecoveryRecord,
|
|
38
|
+
type PaymentRecoveryState,
|
|
39
|
+
type PaymentRecoveryStore,
|
|
40
|
+
type PaymentRecoveryTarget,
|
|
41
|
+
type PaymentSettlementBasis,
|
|
42
|
+
} from './payment-recovery'
|
|
43
|
+
export { SqlPaymentRecoveryStore } from './payment-recovery-sql'
|
|
44
|
+
export {
|
|
45
|
+
PAYMENT_PROTOCOL_VERSION,
|
|
46
|
+
MemoryPaymentOperations,
|
|
47
|
+
type MemoryPaymentOperationsOptions,
|
|
48
|
+
type PaymentAuthorizationContext,
|
|
49
|
+
type PaymentOperation,
|
|
50
|
+
type PaymentOperationNotFound,
|
|
51
|
+
type PaymentOperationRecoveryResult,
|
|
52
|
+
type PaymentOperationState,
|
|
53
|
+
type PaymentOperations,
|
|
54
|
+
type PaymentSettlementInput,
|
|
55
|
+
} from './payment-operations'
|
|
3
56
|
export {
|
|
4
57
|
filterConsumerMessages,
|
|
5
58
|
filterConsumerMessagesStrict,
|
|
@@ -25,6 +78,10 @@ export {
|
|
|
25
78
|
export {
|
|
26
79
|
MemoryNonceStore,
|
|
27
80
|
KvNonceStore,
|
|
81
|
+
isAtomicNonceStore,
|
|
82
|
+
type AtomicKvNonceClaim,
|
|
83
|
+
type AtomicNonceStore,
|
|
84
|
+
type KvNonceStoreOptions,
|
|
28
85
|
type NonceStore,
|
|
29
86
|
} from './nonce-store'
|
|
30
87
|
export {
|
|
@@ -50,6 +107,8 @@ export type {
|
|
|
50
107
|
PaymentResult,
|
|
51
108
|
ApiKeyInfo,
|
|
52
109
|
GatewayUsageEvent,
|
|
110
|
+
SandboxExecutionBudget,
|
|
111
|
+
SandboxUsageReceipt,
|
|
53
112
|
SandboxStreamEvent,
|
|
54
113
|
SandboxBox,
|
|
55
114
|
GatewayConfig,
|
|
@@ -60,7 +119,7 @@ export type {
|
|
|
60
119
|
|
|
61
120
|
// --- A2A protocol surface (Google Agent-to-Agent) ---
|
|
62
121
|
// Types + task-store adapter. Handlers are wired automatically by
|
|
63
|
-
// createAgentGateway
|
|
122
|
+
// createAgentGateway with an in-memory store by default;
|
|
64
123
|
// consumers only import these to BYO a durable TaskStore (D1, postgres, DO)
|
|
65
124
|
// or to declare richer AgentMeta.skills for the Agent Card.
|
|
66
125
|
export { InMemoryTaskStore, type TaskStore } from './a2a/task-store'
|
|
@@ -72,9 +131,12 @@ export {
|
|
|
72
131
|
SqlTaskStore,
|
|
73
132
|
} from './a2a/task-store-sql'
|
|
74
133
|
export {
|
|
134
|
+
deliverDemoPushNotifications,
|
|
75
135
|
deliverPushNotifications,
|
|
76
136
|
InMemoryPushNotificationStore,
|
|
137
|
+
validatePushNotificationUrl,
|
|
77
138
|
type PushDeliveryResult,
|
|
139
|
+
type PushNotificationDeliveryOptions,
|
|
78
140
|
type PushNotificationAuthentication,
|
|
79
141
|
type PushNotificationConfig,
|
|
80
142
|
type PushNotificationStore,
|